From b22763b37d7780140698725a0ed2632e2eb7a7c2 Mon Sep 17 00:00:00 2001 From: Ruud Burger Date: Thu, 26 Apr 2012 10:57:34 +0300 Subject: [PATCH 001/207] Use master branch to update master.. --- couchpotato/core/_base/updater/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py index f5182228..9b4d8ca4 100644 --- a/couchpotato/core/_base/updater/main.py +++ b/couchpotato/core/_base/updater/main.py @@ -78,7 +78,7 @@ class BaseUpdater(Plugin): repo_user = 'RuudBurger' repo_name = 'CouchPotatoServer' - branch = 'develop' + branch = 'master' version = None update_failed = False From d1c2869f2cb4b3fac555cce732e92bb8fec97363 Mon Sep 17 00:00:00 2001 From: Ken Garland Date: Thu, 26 Apr 2012 23:30:33 -0300 Subject: [PATCH 002/207] Removed RUN_AS for group, don't assume the default group is the same as the username. Specifying group is not needed anyways. --- init/ubuntu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/init/ubuntu b/init/ubuntu index 8abc9e79..d6af148a 100644 --- a/init/ubuntu +++ b/init/ubuntu @@ -45,7 +45,7 @@ case "$1" in start) echo "Starting $DESC" rm -rf $PID_PATH || return 1 - install -d --mode=0755 -o $RUN_AS -g $RUN_AS $PID_PATH || return 1 + install -d --mode=0755 -o $RUN_AS $PID_PATH || return 1 start-stop-daemon -d $APP_PATH -c $RUN_AS --start --background --pidfile $PID_FILE --exec $DAEMON -- $DAEMON_OPTS ;; stop) From ba8fef3c87484e4898a196ef9b25c69be68f9825 Mon Sep 17 00:00:00 2001 From: Riccardo Sirchia Date: Sun, 6 May 2012 18:02:51 +0200 Subject: [PATCH 003/207] Added support for direct messages in twitter notifications --- .../core/notifications/twitter/__init__.py | 7 +++++++ couchpotato/core/notifications/twitter/main.py | 16 ++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/notifications/twitter/__init__.py b/couchpotato/core/notifications/twitter/__init__.py index 266a2877..5910b0a2 100644 --- a/couchpotato/core/notifications/twitter/__init__.py +++ b/couchpotato/core/notifications/twitter/__init__.py @@ -38,6 +38,13 @@ config = [{ 'advanced': True, 'description': 'Also send message when movie is snatched.', }, + { + 'name': 'direct_message', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Use direct messages for the notifications (Also applies to the mentioned users).', + }, ], } ], diff --git a/couchpotato/core/notifications/twitter/main.py b/couchpotato/core/notifications/twitter/main.py index 700f7fa3..dcac0673 100644 --- a/couchpotato/core/notifications/twitter/main.py +++ b/couchpotato/core/notifications/twitter/main.py @@ -36,12 +36,24 @@ class Twitter(Notification): api = Api(self.consumer_key, self.consumer_secret, self.conf('access_token_key'), self.conf('access_token_secret')) + direct_message = self.conf('direct_message') + direct_message_users = self.conf('screen_name') + mention = self.conf('mention') if mention: - message = '%s @%s' % (message, mention.lstrip('@')) + if direct_message: + direct_message_users = '%s %s' % (direct_message_users, mention) + direct_message_users = direct_message_users.replace('@',' ') + direct_message_users = direct_message_users.replace(',',' ') + else: + message = '%s @%s' % (message, mention.lstrip('@')) try: - api.PostUpdate('[%s] %s' % (self.default_title, message)) + if direct_message: + for user in direct_message_users.split(): + api.PostDirectMessage(user, '[%s] %s' % (self.default_title, message)) + else: + api.PostUpdate('[%s] %s' % (self.default_title, message)) except Exception, e: log.error('Error sending tweet: %s' % e) return False From d4a5483fa034d960bb7160a076ee11507e3bdc78 Mon Sep 17 00:00:00 2001 From: Ruud Burger Date: Thu, 26 Apr 2012 10:57:34 +0300 Subject: [PATCH 004/207] Use master branch to update master.. --- couchpotato/core/_base/updater/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py index c12c4b9f..17749239 100644 --- a/couchpotato/core/_base/updater/main.py +++ b/couchpotato/core/_base/updater/main.py @@ -80,7 +80,7 @@ class BaseUpdater(Plugin): repo_user = 'RuudBurger' repo_name = 'CouchPotatoServer' - branch = 'develop' + branch = 'master' version = None update_failed = False From 10a80e61f729e6caaf2ff061bd8e99e87ddb6714 Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 17 May 2012 11:03:29 +0200 Subject: [PATCH 005/207] Don't blame sex and the city for being a stupid movie. fixes #280 --- couchpotato/core/plugins/searcher/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py index 254919f0..e2933462 100644 --- a/couchpotato/core/plugins/searcher/main.py +++ b/couchpotato/core/plugins/searcher/main.py @@ -230,7 +230,7 @@ class Searcher(Plugin): pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs'] for p_tag in pron_tags: - if p_tag in movie_name: + if p_tag in nzb_words and p_tag not in movie_name: log.info('Wrong: %s, probably pr0n' % (nzb['name'])) return False From 217785bcda8343fd5e88c08555ff8ad2ac294542 Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 17 May 2012 11:30:21 +0200 Subject: [PATCH 006/207] Order movie search on imdbapi result. fix #279 --- couchpotato/core/providers/movie/_modifier/main.py | 5 +++++ couchpotato/core/providers/movie/imdbapi/main.py | 1 + couchpotato/core/providers/movie/themoviedb/main.py | 1 + 3 files changed, 7 insertions(+) diff --git a/couchpotato/core/providers/movie/_modifier/main.py b/couchpotato/core/providers/movie/_modifier/main.py index 60e4c274..e9ad7633 100644 --- a/couchpotato/core/providers/movie/_modifier/main.py +++ b/couchpotato/core/providers/movie/_modifier/main.py @@ -28,6 +28,11 @@ class MovieResultModifier(Plugin): temp[imdb] = self.getLibraryTags(imdb) order.append(imdb) + if item.get('via_imdb'): + if order.index(imdb): + order.remove(imdb) + order.insert(0, imdb) + # Merge dicts temp[imdb] = mergeDicts(temp[imdb], item) diff --git a/couchpotato/core/providers/movie/imdbapi/main.py b/couchpotato/core/providers/movie/imdbapi/main.py index 71255e60..7aa3d978 100644 --- a/couchpotato/core/providers/movie/imdbapi/main.py +++ b/couchpotato/core/providers/movie/imdbapi/main.py @@ -82,6 +82,7 @@ class IMDBAPI(MovieProvider): year = tryInt(movie.get('Year', '')) movie_data = { + 'via_imdb': True, 'titles': [movie.get('Title')] if movie.get('Title') else [], 'original_title': movie.get('Title', ''), 'images': { diff --git a/couchpotato/core/providers/movie/themoviedb/main.py b/couchpotato/core/providers/movie/themoviedb/main.py index f66f2f7a..667f563f 100644 --- a/couchpotato/core/providers/movie/themoviedb/main.py +++ b/couchpotato/core/providers/movie/themoviedb/main.py @@ -148,6 +148,7 @@ class TheMovieDb(MovieProvider): year = None movie_data = { + 'via_tmdb': True, 'id': int(movie.get('id', 0)), 'titles': [toUnicode(movie.get('name'))], 'original_title': movie.get('original_name'), From 1b9d124965a05fc2454cadc1399e0ad3c3c38190 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 18 May 2012 12:12:43 +0200 Subject: [PATCH 007/207] Remove first in renamer. fix #283 --- couchpotato/core/plugins/renamer/main.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 24b031e4..07826091 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -325,6 +325,18 @@ class Renamer(Plugin): elif not remove_leftovers: # Don't remove anything remove_files = [] + # Remove files + for src in remove_files: + + if isinstance(src, File): + src = src.path + + log.info('Removing "%s"' % src) + try: + os.remove(src) + except: + log.error('Failed removing %s: %s' % (src, traceback.format_exc())) + # Rename all files marked group['renamed_files'] = [] for src in rename_files: @@ -341,18 +353,6 @@ class Renamer(Plugin): except: log.error('Failed moving the file "%s" : %s' % (os.path.basename(src), traceback.format_exc())) - # Remove files - for src in remove_files: - - if isinstance(src, File): - src = src.path - - log.info('Removing "%s"' % src) - try: - os.remove(src) - except: - log.error('Failed removing %s: %s' % (src, traceback.format_exc())) - # Remove matching releases for release in remove_releases: log.debug('Removing release %s' % release.identifier) From c1f0a98e967fe2a19dd341f27fb1b2d9411a15dd Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 18 May 2012 12:50:31 +0200 Subject: [PATCH 008/207] Add filename to renamer choices --- couchpotato/core/plugins/renamer/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py index b1b53395..3fb29f27 100644 --- a/couchpotato/core/plugins/renamer/__init__.py +++ b/couchpotato/core/plugins/renamer/__init__.py @@ -17,7 +17,7 @@ rename_options = { 'audio': 'Audio (DTS)', 'group': 'Releasegroup name', 'source': 'Source media (Bluray)', - 'original': 'Original filename', + 'filename': 'Original filename', 'original_folder': 'Original foldername', }, } From 87e6e8e27144a795e4157fecb965860cad273572 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 18 May 2012 12:51:29 +0200 Subject: [PATCH 009/207] Update library before getting release_dates. fix #288 --- couchpotato/core/plugins/library/main.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/library/main.py b/couchpotato/core/plugins/library/main.py index 4cdc323f..347d8580 100644 --- a/couchpotato/core/plugins/library/main.py +++ b/couchpotato/core/plugins/library/main.py @@ -136,7 +136,12 @@ class LibraryPlugin(Plugin): db = get_session() library = db.query(Library).filter_by(identifier = identifier).first() - dates = library.info.get('release_date') + + if not library.info: + self.update(identifier) + dates = library.get('info', {}).get('release_dates') + else: + dates = library.info.get('release_date') if dates and dates.get('expires', 0) < time.time(): dates = fireEvent('movie.release_date', identifier = identifier, merge = True) From 25bef53c2a74b42edbd85a6eb408b9447a00367d Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 18 May 2012 19:45:33 +0200 Subject: [PATCH 010/207] Use tornado wsgi server as werkzeug crashes on Internet Explorer --- couchpotato/runner.py | 136 +- libs/tornado/__init__.py | 27 + libs/tornado/auth.py | 1134 +++++++++ libs/tornado/autoreload.py | 250 ++ libs/tornado/ca-certificates.crt | 3576 ++++++++++++++++++++++++++++ libs/tornado/curl_httpclient.py | 435 ++++ libs/tornado/database.py | 229 ++ libs/tornado/epoll.c | 112 + libs/tornado/escape.py | 327 +++ libs/tornado/gen.py | 382 +++ libs/tornado/httpclient.py | 417 ++++ libs/tornado/httpserver.py | 476 ++++ libs/tornado/httputil.py | 280 +++ libs/tornado/ioloop.py | 643 +++++ libs/tornado/iostream.py | 728 ++++++ libs/tornado/locale.py | 472 ++++ libs/tornado/netutil.py | 320 +++ libs/tornado/options.py | 422 ++++ libs/tornado/platform/__init__.py | 0 libs/tornado/platform/auto.py | 31 + libs/tornado/platform/interface.py | 57 + libs/tornado/platform/posix.py | 62 + libs/tornado/platform/twisted.py | 330 +++ libs/tornado/platform/windows.py | 97 + libs/tornado/process.py | 149 ++ libs/tornado/simple_httpclient.py | 509 ++++ libs/tornado/stack_context.py | 244 ++ libs/tornado/template.py | 826 +++++++ libs/tornado/testing.py | 382 +++ libs/tornado/util.py | 47 + libs/tornado/web.py | 1985 +++++++++++++++ libs/tornado/websocket.py | 650 +++++ libs/tornado/wsgi.py | 296 +++ 33 files changed, 15975 insertions(+), 56 deletions(-) create mode 100644 libs/tornado/__init__.py create mode 100644 libs/tornado/auth.py create mode 100644 libs/tornado/autoreload.py create mode 100644 libs/tornado/ca-certificates.crt create mode 100644 libs/tornado/curl_httpclient.py create mode 100644 libs/tornado/database.py create mode 100644 libs/tornado/epoll.c create mode 100644 libs/tornado/escape.py create mode 100644 libs/tornado/gen.py create mode 100644 libs/tornado/httpclient.py create mode 100644 libs/tornado/httpserver.py create mode 100644 libs/tornado/httputil.py create mode 100644 libs/tornado/ioloop.py create mode 100644 libs/tornado/iostream.py create mode 100644 libs/tornado/locale.py create mode 100644 libs/tornado/netutil.py create mode 100644 libs/tornado/options.py create mode 100644 libs/tornado/platform/__init__.py create mode 100644 libs/tornado/platform/auto.py create mode 100644 libs/tornado/platform/interface.py create mode 100644 libs/tornado/platform/posix.py create mode 100644 libs/tornado/platform/twisted.py create mode 100644 libs/tornado/platform/windows.py create mode 100644 libs/tornado/process.py create mode 100644 libs/tornado/simple_httpclient.py create mode 100644 libs/tornado/stack_context.py create mode 100644 libs/tornado/template.py create mode 100644 libs/tornado/testing.py create mode 100644 libs/tornado/util.py create mode 100644 libs/tornado/web.py create mode 100644 libs/tornado/websocket.py create mode 100644 libs/tornado/wsgi.py diff --git a/couchpotato/runner.py b/couchpotato/runner.py index cf0e5fb9..37c0f348 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -4,6 +4,11 @@ from couchpotato.api import api from couchpotato.core.event import fireEventAsync, fireEvent from couchpotato.core.helpers.variable import getDataDir, tryInt from logging import handlers +from tornado import autoreload +from tornado.httpserver import HTTPServer +from tornado.ioloop import IOLoop +from tornado.web import RequestHandler +from tornado.wsgi import WSGIContainer from werkzeug.contrib.cache import FileSystemCache import atexit import locale @@ -44,6 +49,20 @@ def cleanup(): fireEvent('app.crappy_shutdown', single = True) time.sleep(1) +# Tornado monkey patch logging.. +def _log(status_code, request): + + if status_code < 400: + return + elif status_code < 500: + log_method = logging.warning + else: + log_method = logging.error + request_time = 1000.0 * request.request_time() + summary = request.method + " " + request.uri + " (" + \ + request.remote_ip + ")" + log_method("%d %s %.2fms", status_code, summary, request_time) + def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, Env = None, desktop = None): @@ -117,78 +136,72 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En for logger_name in ['enzyme', 'guessit', 'subliminal', 'apscheduler']: logging.getLogger(logger_name).setLevel(logging.ERROR) - for logger_name in ['gntp', 'werkzeug', 'migrate']: + for logger_name in ['gntp', 'migrate']: logging.getLogger(logger_name).setLevel(logging.WARNING) # Use reloader reloader = debug is True and development and not Env.get('desktop') and not options.daemon - # Only run once when debugging - fire_load = False - if os.environ.get('WERKZEUG_RUN_MAIN') or not reloader: + # Logger + logger = logging.getLogger() + formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', '%m-%d %H:%M:%S') + level = logging.DEBUG if debug else logging.INFO + logger.setLevel(level) - # Logger - logger = logging.getLogger() - formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', '%m-%d %H:%M:%S') - level = logging.DEBUG if debug else logging.INFO - logger.setLevel(level) + # To screen + if (debug or options.console_log) and not options.quiet and not options.daemon: + hdlr = logging.StreamHandler(sys.stderr) + hdlr.setFormatter(formatter) + logger.addHandler(hdlr) - # To screen - if (debug or options.console_log) and not options.quiet and not options.daemon: - hdlr = logging.StreamHandler(sys.stderr) - hdlr.setFormatter(formatter) - logger.addHandler(hdlr) + # To file + hdlr2 = handlers.RotatingFileHandler(Env.get('log_path'), 'a', 500000, 10) + hdlr2.setFormatter(formatter) + logger.addHandler(hdlr2) - # To file - hdlr2 = handlers.RotatingFileHandler(Env.get('log_path'), 'a', 500000, 10) - hdlr2.setFormatter(formatter) - logger.addHandler(hdlr2) + # Start logging & enable colors + import color_logs + from couchpotato.core.logger import CPLog + log = CPLog(__name__) + log.debug('Started with options %s' % options) - # Start logging & enable colors - import color_logs - from couchpotato.core.logger import CPLog - log = CPLog(__name__) - log.debug('Started with options %s' % options) - - def customwarn(message, category, filename, lineno, file = None, line = None): - log.warning('%s %s %s line:%s' % (category, message, filename, lineno)) - warnings.showwarning = customwarn + def customwarn(message, category, filename, lineno, file = None, line = None): + log.warning('%s %s %s line:%s' % (category, message, filename, lineno)) + warnings.showwarning = customwarn - # Load configs & plugins - loader = Env.get('loader') - loader.preload(root = base_path) - loader.run() + # Load configs & plugins + loader = Env.get('loader') + loader.preload(root = base_path) + loader.run() - # Load migrations - initialize = True - db = Env.get('db_path') - if os.path.isfile(db_path): - initialize = False + # Load migrations + initialize = True + db = Env.get('db_path') + if os.path.isfile(db_path): + initialize = False - from migrate.versioning.api import version_control, db_version, version, upgrade - repo = os.path.join(base_path, 'couchpotato', 'core', 'migration') + from migrate.versioning.api import version_control, db_version, version, upgrade + repo = os.path.join(base_path, 'couchpotato', 'core', 'migration') - latest_db_version = version(repo) - try: - current_db_version = db_version(db, repo) - except: - version_control(db, repo, version = latest_db_version) - current_db_version = db_version(db, repo) + latest_db_version = version(repo) + try: + current_db_version = db_version(db, repo) + except: + version_control(db, repo, version = latest_db_version) + current_db_version = db_version(db, repo) - if current_db_version < latest_db_version and not debug: - log.info('Doing database upgrade. From %d to %d' % (current_db_version, latest_db_version)) - upgrade(db, repo) + if current_db_version < latest_db_version and not debug: + log.info('Doing database upgrade. From %d to %d' % (current_db_version, latest_db_version)) + upgrade(db, repo) - # Configure Database - from couchpotato.core.settings.model import setup - setup() + # Configure Database + from couchpotato.core.settings.model import setup + setup() - if initialize: - fireEvent('app.initialize', in_order = True) - - fire_load = True + if initialize: + fireEvent('app.initialize', in_order = True) # Create app from couchpotato import app @@ -197,6 +210,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En # Basic config app.secret_key = api_key + # app.debug = development config = { 'use_reloader': reloader, 'host': Env.setting('host', default = '0.0.0.0'), @@ -216,14 +230,24 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En # Some logging and fire load event try: log.info('Starting server on port %(port)s' % config) except: pass - if fire_load: fireEventAsync('app.load') + fireEventAsync('app.load') # Go go go! try_restart = True restart_tries = 5 while try_restart: try: - app.run(**config) + web_container = WSGIContainer(app) + web_container._log = _log + + http_server = HTTPServer(web_container) + http_server.listen(config['port'], config['host']) + loop = IOLoop.instance() + + if config['use_reloader']: + autoreload.start(loop) + + loop.start() except Exception, e: try: nr, msg = e diff --git a/libs/tornado/__init__.py b/libs/tornado/__init__.py new file mode 100644 index 00000000..45d1b7ef --- /dev/null +++ b/libs/tornado/__init__.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# +# Copyright 2009 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""The Tornado web server and tools.""" + +# version is a human-readable version number. + +# version_info is a four-tuple for programmatic comparison. The first +# three numbers are the components of the version number. The fourth +# is zero for an official release, positive for a development branch, +# or negative for a release candidate (after the base version number +# has been incremented) +version = "2.2.1" +version_info = (2, 2, 1, 0) diff --git a/libs/tornado/auth.py b/libs/tornado/auth.py new file mode 100644 index 00000000..a7162105 --- /dev/null +++ b/libs/tornado/auth.py @@ -0,0 +1,1134 @@ +#!/usr/bin/env python +# +# Copyright 2009 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Implementations of various third-party authentication schemes. + +All the classes in this file are class Mixins designed to be used with +web.py RequestHandler classes. The primary methods for each service are +authenticate_redirect(), authorize_redirect(), and get_authenticated_user(). +The former should be called to redirect the user to, e.g., the OpenID +authentication page on the third party service, and the latter should +be called upon return to get the user data from the data returned by +the third party service. + +They all take slightly different arguments due to the fact all these +services implement authentication and authorization slightly differently. +See the individual service classes below for complete documentation. + +Example usage for Google OpenID:: + + class GoogleHandler(tornado.web.RequestHandler, tornado.auth.GoogleMixin): + @tornado.web.asynchronous + def get(self): + if self.get_argument("openid.mode", None): + self.get_authenticated_user(self.async_callback(self._on_auth)) + return + self.authenticate_redirect() + + def _on_auth(self, user): + if not user: + raise tornado.web.HTTPError(500, "Google auth failed") + # Save the user with, e.g., set_secure_cookie() +""" + +import base64 +import binascii +import hashlib +import hmac +import logging +import time +import urllib +import urlparse +import uuid + +from tornado import httpclient +from tornado import escape +from tornado.httputil import url_concat +from tornado.util import bytes_type, b + +class OpenIdMixin(object): + """Abstract implementation of OpenID and Attribute Exchange. + + See GoogleMixin below for example implementations. + """ + def authenticate_redirect(self, callback_uri=None, + ax_attrs=["name","email","language","username"]): + """Returns the authentication URL for this service. + + After authentication, the service will redirect back to the given + callback URI. + + We request the given attributes for the authenticated user by + default (name, email, language, and username). If you don't need + all those attributes for your app, you can request fewer with + the ax_attrs keyword argument. + """ + callback_uri = callback_uri or self.request.uri + args = self._openid_args(callback_uri, ax_attrs=ax_attrs) + self.redirect(self._OPENID_ENDPOINT + "?" + urllib.urlencode(args)) + + def get_authenticated_user(self, callback, http_client=None): + """Fetches the authenticated user data upon redirect. + + This method should be called by the handler that receives the + redirect from the authenticate_redirect() or authorize_redirect() + methods. + """ + # Verify the OpenID response via direct request to the OP + args = dict((k, v[-1]) for k, v in self.request.arguments.iteritems()) + args["openid.mode"] = u"check_authentication" + url = self._OPENID_ENDPOINT + if http_client is None: http_client = httpclient.AsyncHTTPClient() + http_client.fetch(url, self.async_callback( + self._on_authentication_verified, callback), + method="POST", body=urllib.urlencode(args)) + + def _openid_args(self, callback_uri, ax_attrs=[], oauth_scope=None): + url = urlparse.urljoin(self.request.full_url(), callback_uri) + args = { + "openid.ns": "http://specs.openid.net/auth/2.0", + "openid.claimed_id": + "http://specs.openid.net/auth/2.0/identifier_select", + "openid.identity": + "http://specs.openid.net/auth/2.0/identifier_select", + "openid.return_to": url, + "openid.realm": urlparse.urljoin(url, '/'), + "openid.mode": "checkid_setup", + } + if ax_attrs: + args.update({ + "openid.ns.ax": "http://openid.net/srv/ax/1.0", + "openid.ax.mode": "fetch_request", + }) + ax_attrs = set(ax_attrs) + required = [] + if "name" in ax_attrs: + ax_attrs -= set(["name", "firstname", "fullname", "lastname"]) + required += ["firstname", "fullname", "lastname"] + args.update({ + "openid.ax.type.firstname": + "http://axschema.org/namePerson/first", + "openid.ax.type.fullname": + "http://axschema.org/namePerson", + "openid.ax.type.lastname": + "http://axschema.org/namePerson/last", + }) + known_attrs = { + "email": "http://axschema.org/contact/email", + "language": "http://axschema.org/pref/language", + "username": "http://axschema.org/namePerson/friendly", + } + for name in ax_attrs: + args["openid.ax.type." + name] = known_attrs[name] + required.append(name) + args["openid.ax.required"] = ",".join(required) + if oauth_scope: + args.update({ + "openid.ns.oauth": + "http://specs.openid.net/extensions/oauth/1.0", + "openid.oauth.consumer": self.request.host.split(":")[0], + "openid.oauth.scope": oauth_scope, + }) + return args + + def _on_authentication_verified(self, callback, response): + if response.error or b("is_valid:true") not in response.body: + logging.warning("Invalid OpenID response: %s", response.error or + response.body) + callback(None) + return + + # Make sure we got back at least an email from attribute exchange + ax_ns = None + for name in self.request.arguments.iterkeys(): + if name.startswith("openid.ns.") and \ + self.get_argument(name) == u"http://openid.net/srv/ax/1.0": + ax_ns = name[10:] + break + def get_ax_arg(uri): + if not ax_ns: return u"" + prefix = "openid." + ax_ns + ".type." + ax_name = None + for name in self.request.arguments.iterkeys(): + if self.get_argument(name) == uri and name.startswith(prefix): + part = name[len(prefix):] + ax_name = "openid." + ax_ns + ".value." + part + break + if not ax_name: return u"" + return self.get_argument(ax_name, u"") + + email = get_ax_arg("http://axschema.org/contact/email") + name = get_ax_arg("http://axschema.org/namePerson") + first_name = get_ax_arg("http://axschema.org/namePerson/first") + last_name = get_ax_arg("http://axschema.org/namePerson/last") + username = get_ax_arg("http://axschema.org/namePerson/friendly") + locale = get_ax_arg("http://axschema.org/pref/language").lower() + user = dict() + name_parts = [] + if first_name: + user["first_name"] = first_name + name_parts.append(first_name) + if last_name: + user["last_name"] = last_name + name_parts.append(last_name) + if name: + user["name"] = name + elif name_parts: + user["name"] = u" ".join(name_parts) + elif email: + user["name"] = email.split("@")[0] + if email: user["email"] = email + if locale: user["locale"] = locale + if username: user["username"] = username + callback(user) + + +class OAuthMixin(object): + """Abstract implementation of OAuth. + + See TwitterMixin and FriendFeedMixin below for example implementations. + """ + + def authorize_redirect(self, callback_uri=None, extra_params=None, + http_client=None): + """Redirects the user to obtain OAuth authorization for this service. + + Twitter and FriendFeed both require that you register a Callback + URL with your application. You should call this method to log the + user in, and then call get_authenticated_user() in the handler + you registered as your Callback URL to complete the authorization + process. + + This method sets a cookie called _oauth_request_token which is + subsequently used (and cleared) in get_authenticated_user for + security purposes. + """ + if callback_uri and getattr(self, "_OAUTH_NO_CALLBACKS", False): + raise Exception("This service does not support oauth_callback") + if http_client is None: + http_client = httpclient.AsyncHTTPClient() + if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a": + http_client.fetch( + self._oauth_request_token_url(callback_uri=callback_uri, + extra_params=extra_params), + self.async_callback( + self._on_request_token, + self._OAUTH_AUTHORIZE_URL, + callback_uri)) + else: + http_client.fetch( + self._oauth_request_token_url(), + self.async_callback( + self._on_request_token, self._OAUTH_AUTHORIZE_URL, + callback_uri)) + + + def get_authenticated_user(self, callback, http_client=None): + """Gets the OAuth authorized user and access token on callback. + + This method should be called from the handler for your registered + OAuth Callback URL to complete the registration process. We call + callback with the authenticated user, which in addition to standard + attributes like 'name' includes the 'access_key' attribute, which + contains the OAuth access you can use to make authorized requests + to this service on behalf of the user. + + """ + request_key = escape.utf8(self.get_argument("oauth_token")) + oauth_verifier = self.get_argument("oauth_verifier", None) + request_cookie = self.get_cookie("_oauth_request_token") + if not request_cookie: + logging.warning("Missing OAuth request token cookie") + callback(None) + return + self.clear_cookie("_oauth_request_token") + cookie_key, cookie_secret = [base64.b64decode(escape.utf8(i)) for i in request_cookie.split("|")] + if cookie_key != request_key: + logging.info((cookie_key, request_key, request_cookie)) + logging.warning("Request token does not match cookie") + callback(None) + return + token = dict(key=cookie_key, secret=cookie_secret) + if oauth_verifier: + token["verifier"] = oauth_verifier + if http_client is None: + http_client = httpclient.AsyncHTTPClient() + http_client.fetch(self._oauth_access_token_url(token), + self.async_callback(self._on_access_token, callback)) + + def _oauth_request_token_url(self, callback_uri= None, extra_params=None): + consumer_token = self._oauth_consumer_token() + url = self._OAUTH_REQUEST_TOKEN_URL + args = dict( + oauth_consumer_key=consumer_token["key"], + oauth_signature_method="HMAC-SHA1", + oauth_timestamp=str(int(time.time())), + oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes), + oauth_version=getattr(self, "_OAUTH_VERSION", "1.0a"), + ) + if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a": + if callback_uri: + args["oauth_callback"] = urlparse.urljoin( + self.request.full_url(), callback_uri) + if extra_params: args.update(extra_params) + signature = _oauth10a_signature(consumer_token, "GET", url, args) + else: + signature = _oauth_signature(consumer_token, "GET", url, args) + + args["oauth_signature"] = signature + return url + "?" + urllib.urlencode(args) + + def _on_request_token(self, authorize_url, callback_uri, response): + if response.error: + raise Exception("Could not get request token") + request_token = _oauth_parse_response(response.body) + data = (base64.b64encode(request_token["key"]) + b("|") + + base64.b64encode(request_token["secret"])) + self.set_cookie("_oauth_request_token", data) + args = dict(oauth_token=request_token["key"]) + if callback_uri: + args["oauth_callback"] = urlparse.urljoin( + self.request.full_url(), callback_uri) + self.redirect(authorize_url + "?" + urllib.urlencode(args)) + + def _oauth_access_token_url(self, request_token): + consumer_token = self._oauth_consumer_token() + url = self._OAUTH_ACCESS_TOKEN_URL + args = dict( + oauth_consumer_key=consumer_token["key"], + oauth_token=request_token["key"], + oauth_signature_method="HMAC-SHA1", + oauth_timestamp=str(int(time.time())), + oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes), + oauth_version=getattr(self, "_OAUTH_VERSION", "1.0a"), + ) + if "verifier" in request_token: + args["oauth_verifier"]=request_token["verifier"] + + if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a": + signature = _oauth10a_signature(consumer_token, "GET", url, args, + request_token) + else: + signature = _oauth_signature(consumer_token, "GET", url, args, + request_token) + + args["oauth_signature"] = signature + return url + "?" + urllib.urlencode(args) + + def _on_access_token(self, callback, response): + if response.error: + logging.warning("Could not fetch access token") + callback(None) + return + + access_token = _oauth_parse_response(response.body) + self._oauth_get_user(access_token, self.async_callback( + self._on_oauth_get_user, access_token, callback)) + + def _oauth_get_user(self, access_token, callback): + raise NotImplementedError() + + def _on_oauth_get_user(self, access_token, callback, user): + if not user: + callback(None) + return + user["access_token"] = access_token + callback(user) + + def _oauth_request_parameters(self, url, access_token, parameters={}, + method="GET"): + """Returns the OAuth parameters as a dict for the given request. + + parameters should include all POST arguments and query string arguments + that will be sent with the request. + """ + consumer_token = self._oauth_consumer_token() + base_args = dict( + oauth_consumer_key=consumer_token["key"], + oauth_token=access_token["key"], + oauth_signature_method="HMAC-SHA1", + oauth_timestamp=str(int(time.time())), + oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes), + oauth_version=getattr(self, "_OAUTH_VERSION", "1.0a"), + ) + args = {} + args.update(base_args) + args.update(parameters) + if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a": + signature = _oauth10a_signature(consumer_token, method, url, args, + access_token) + else: + signature = _oauth_signature(consumer_token, method, url, args, + access_token) + base_args["oauth_signature"] = signature + return base_args + +class OAuth2Mixin(object): + """Abstract implementation of OAuth v 2.""" + + def authorize_redirect(self, redirect_uri=None, client_id=None, + client_secret=None, extra_params=None ): + """Redirects the user to obtain OAuth authorization for this service. + + Some providers require that you register a Callback + URL with your application. You should call this method to log the + user in, and then call get_authenticated_user() in the handler + you registered as your Callback URL to complete the authorization + process. + """ + args = { + "redirect_uri": redirect_uri, + "client_id": client_id + } + if extra_params: args.update(extra_params) + self.redirect( + url_concat(self._OAUTH_AUTHORIZE_URL, args)) + + def _oauth_request_token_url(self, redirect_uri= None, client_id = None, + client_secret=None, code=None, + extra_params=None): + url = self._OAUTH_ACCESS_TOKEN_URL + args = dict( + redirect_uri=redirect_uri, + code=code, + client_id=client_id, + client_secret=client_secret, + ) + if extra_params: args.update(extra_params) + return url_concat(url, args) + +class TwitterMixin(OAuthMixin): + """Twitter OAuth authentication. + + To authenticate with Twitter, register your application with + Twitter at http://twitter.com/apps. Then copy your Consumer Key and + Consumer Secret to the application settings 'twitter_consumer_key' and + 'twitter_consumer_secret'. Use this Mixin on the handler for the URL + you registered as your application's Callback URL. + + When your application is set up, you can use this Mixin like this + to authenticate the user with Twitter and get access to their stream:: + + class TwitterHandler(tornado.web.RequestHandler, + tornado.auth.TwitterMixin): + @tornado.web.asynchronous + def get(self): + if self.get_argument("oauth_token", None): + self.get_authenticated_user(self.async_callback(self._on_auth)) + return + self.authorize_redirect() + + def _on_auth(self, user): + if not user: + raise tornado.web.HTTPError(500, "Twitter auth failed") + # Save the user using, e.g., set_secure_cookie() + + The user object returned by get_authenticated_user() includes the + attributes 'username', 'name', and all of the custom Twitter user + attributes describe at + http://apiwiki.twitter.com/Twitter-REST-API-Method%3A-users%C2%A0show + in addition to 'access_token'. You should save the access token with + the user; it is required to make requests on behalf of the user later + with twitter_request(). + """ + _OAUTH_REQUEST_TOKEN_URL = "http://api.twitter.com/oauth/request_token" + _OAUTH_ACCESS_TOKEN_URL = "http://api.twitter.com/oauth/access_token" + _OAUTH_AUTHORIZE_URL = "http://api.twitter.com/oauth/authorize" + _OAUTH_AUTHENTICATE_URL = "http://api.twitter.com/oauth/authenticate" + _OAUTH_NO_CALLBACKS = False + + + def authenticate_redirect(self, callback_uri = None): + """Just like authorize_redirect(), but auto-redirects if authorized. + + This is generally the right interface to use if you are using + Twitter for single-sign on. + """ + http = httpclient.AsyncHTTPClient() + http.fetch(self._oauth_request_token_url(callback_uri = callback_uri), self.async_callback( + self._on_request_token, self._OAUTH_AUTHENTICATE_URL, None)) + + def twitter_request(self, path, callback, access_token=None, + post_args=None, **args): + """Fetches the given API path, e.g., "/statuses/user_timeline/btaylor" + + The path should not include the format (we automatically append + ".json" and parse the JSON output). + + If the request is a POST, post_args should be provided. Query + string arguments should be given as keyword arguments. + + All the Twitter methods are documented at + http://apiwiki.twitter.com/Twitter-API-Documentation. + + Many methods require an OAuth access token which you can obtain + through authorize_redirect() and get_authenticated_user(). The + user returned through that process includes an 'access_token' + attribute that can be used to make authenticated requests via + this method. Example usage:: + + class MainHandler(tornado.web.RequestHandler, + tornado.auth.TwitterMixin): + @tornado.web.authenticated + @tornado.web.asynchronous + def get(self): + self.twitter_request( + "/statuses/update", + post_args={"status": "Testing Tornado Web Server"}, + access_token=user["access_token"], + callback=self.async_callback(self._on_post)) + + def _on_post(self, new_entry): + if not new_entry: + # Call failed; perhaps missing permission? + self.authorize_redirect() + return + self.finish("Posted a message!") + + """ + if path.startswith('http:') or path.startswith('https:'): + # Raw urls are useful for e.g. search which doesn't follow the + # usual pattern: http://search.twitter.com/search.json + url = path + else: + url = "http://api.twitter.com/1" + path + ".json" + # Add the OAuth resource request signature if we have credentials + if access_token: + all_args = {} + all_args.update(args) + all_args.update(post_args or {}) + method = "POST" if post_args is not None else "GET" + oauth = self._oauth_request_parameters( + url, access_token, all_args, method=method) + args.update(oauth) + if args: url += "?" + urllib.urlencode(args) + callback = self.async_callback(self._on_twitter_request, callback) + http = httpclient.AsyncHTTPClient() + if post_args is not None: + http.fetch(url, method="POST", body=urllib.urlencode(post_args), + callback=callback) + else: + http.fetch(url, callback=callback) + + def _on_twitter_request(self, callback, response): + if response.error: + logging.warning("Error response %s fetching %s", response.error, + response.request.url) + callback(None) + return + callback(escape.json_decode(response.body)) + + def _oauth_consumer_token(self): + self.require_setting("twitter_consumer_key", "Twitter OAuth") + self.require_setting("twitter_consumer_secret", "Twitter OAuth") + return dict( + key=self.settings["twitter_consumer_key"], + secret=self.settings["twitter_consumer_secret"]) + + def _oauth_get_user(self, access_token, callback): + callback = self.async_callback(self._parse_user_response, callback) + self.twitter_request( + "/users/show/" + access_token["screen_name"], + access_token=access_token, callback=callback) + + def _parse_user_response(self, callback, user): + if user: + user["username"] = user["screen_name"] + callback(user) + + +class FriendFeedMixin(OAuthMixin): + """FriendFeed OAuth authentication. + + To authenticate with FriendFeed, register your application with + FriendFeed at http://friendfeed.com/api/applications. Then + copy your Consumer Key and Consumer Secret to the application settings + 'friendfeed_consumer_key' and 'friendfeed_consumer_secret'. Use + this Mixin on the handler for the URL you registered as your + application's Callback URL. + + When your application is set up, you can use this Mixin like this + to authenticate the user with FriendFeed and get access to their feed:: + + class FriendFeedHandler(tornado.web.RequestHandler, + tornado.auth.FriendFeedMixin): + @tornado.web.asynchronous + def get(self): + if self.get_argument("oauth_token", None): + self.get_authenticated_user(self.async_callback(self._on_auth)) + return + self.authorize_redirect() + + def _on_auth(self, user): + if not user: + raise tornado.web.HTTPError(500, "FriendFeed auth failed") + # Save the user using, e.g., set_secure_cookie() + + The user object returned by get_authenticated_user() includes the + attributes 'username', 'name', and 'description' in addition to + 'access_token'. You should save the access token with the user; + it is required to make requests on behalf of the user later with + friendfeed_request(). + """ + _OAUTH_VERSION = "1.0" + _OAUTH_REQUEST_TOKEN_URL = "https://friendfeed.com/account/oauth/request_token" + _OAUTH_ACCESS_TOKEN_URL = "https://friendfeed.com/account/oauth/access_token" + _OAUTH_AUTHORIZE_URL = "https://friendfeed.com/account/oauth/authorize" + _OAUTH_NO_CALLBACKS = True + _OAUTH_VERSION = "1.0" + + + def friendfeed_request(self, path, callback, access_token=None, + post_args=None, **args): + """Fetches the given relative API path, e.g., "/bret/friends" + + If the request is a POST, post_args should be provided. Query + string arguments should be given as keyword arguments. + + All the FriendFeed methods are documented at + http://friendfeed.com/api/documentation. + + Many methods require an OAuth access token which you can obtain + through authorize_redirect() and get_authenticated_user(). The + user returned through that process includes an 'access_token' + attribute that can be used to make authenticated requests via + this method. Example usage:: + + class MainHandler(tornado.web.RequestHandler, + tornado.auth.FriendFeedMixin): + @tornado.web.authenticated + @tornado.web.asynchronous + def get(self): + self.friendfeed_request( + "/entry", + post_args={"body": "Testing Tornado Web Server"}, + access_token=self.current_user["access_token"], + callback=self.async_callback(self._on_post)) + + def _on_post(self, new_entry): + if not new_entry: + # Call failed; perhaps missing permission? + self.authorize_redirect() + return + self.finish("Posted a message!") + + """ + # Add the OAuth resource request signature if we have credentials + url = "http://friendfeed-api.com/v2" + path + if access_token: + all_args = {} + all_args.update(args) + all_args.update(post_args or {}) + method = "POST" if post_args is not None else "GET" + oauth = self._oauth_request_parameters( + url, access_token, all_args, method=method) + args.update(oauth) + if args: url += "?" + urllib.urlencode(args) + callback = self.async_callback(self._on_friendfeed_request, callback) + http = httpclient.AsyncHTTPClient() + if post_args is not None: + http.fetch(url, method="POST", body=urllib.urlencode(post_args), + callback=callback) + else: + http.fetch(url, callback=callback) + + def _on_friendfeed_request(self, callback, response): + if response.error: + logging.warning("Error response %s fetching %s", response.error, + response.request.url) + callback(None) + return + callback(escape.json_decode(response.body)) + + def _oauth_consumer_token(self): + self.require_setting("friendfeed_consumer_key", "FriendFeed OAuth") + self.require_setting("friendfeed_consumer_secret", "FriendFeed OAuth") + return dict( + key=self.settings["friendfeed_consumer_key"], + secret=self.settings["friendfeed_consumer_secret"]) + + def _oauth_get_user(self, access_token, callback): + callback = self.async_callback(self._parse_user_response, callback) + self.friendfeed_request( + "/feedinfo/" + access_token["username"], + include="id,name,description", access_token=access_token, + callback=callback) + + def _parse_user_response(self, callback, user): + if user: + user["username"] = user["id"] + callback(user) + + +class GoogleMixin(OpenIdMixin, OAuthMixin): + """Google Open ID / OAuth authentication. + + No application registration is necessary to use Google for authentication + or to access Google resources on behalf of a user. To authenticate with + Google, redirect with authenticate_redirect(). On return, parse the + response with get_authenticated_user(). We send a dict containing the + values for the user, including 'email', 'name', and 'locale'. + Example usage:: + + class GoogleHandler(tornado.web.RequestHandler, tornado.auth.GoogleMixin): + @tornado.web.asynchronous + def get(self): + if self.get_argument("openid.mode", None): + self.get_authenticated_user(self.async_callback(self._on_auth)) + return + self.authenticate_redirect() + + def _on_auth(self, user): + if not user: + raise tornado.web.HTTPError(500, "Google auth failed") + # Save the user with, e.g., set_secure_cookie() + + """ + _OPENID_ENDPOINT = "https://www.google.com/accounts/o8/ud" + _OAUTH_ACCESS_TOKEN_URL = "https://www.google.com/accounts/OAuthGetAccessToken" + + def authorize_redirect(self, oauth_scope, callback_uri=None, + ax_attrs=["name","email","language","username"]): + """Authenticates and authorizes for the given Google resource. + + Some of the available resources are: + + * Gmail Contacts - http://www.google.com/m8/feeds/ + * Calendar - http://www.google.com/calendar/feeds/ + * Finance - http://finance.google.com/finance/feeds/ + + You can authorize multiple resources by separating the resource + URLs with a space. + """ + callback_uri = callback_uri or self.request.uri + args = self._openid_args(callback_uri, ax_attrs=ax_attrs, + oauth_scope=oauth_scope) + self.redirect(self._OPENID_ENDPOINT + "?" + urllib.urlencode(args)) + + def get_authenticated_user(self, callback): + """Fetches the authenticated user data upon redirect.""" + # Look to see if we are doing combined OpenID/OAuth + oauth_ns = "" + for name, values in self.request.arguments.iteritems(): + if name.startswith("openid.ns.") and \ + values[-1] == u"http://specs.openid.net/extensions/oauth/1.0": + oauth_ns = name[10:] + break + token = self.get_argument("openid." + oauth_ns + ".request_token", "") + if token: + http = httpclient.AsyncHTTPClient() + token = dict(key=token, secret="") + http.fetch(self._oauth_access_token_url(token), + self.async_callback(self._on_access_token, callback)) + else: + OpenIdMixin.get_authenticated_user(self, callback) + + def _oauth_consumer_token(self): + self.require_setting("google_consumer_key", "Google OAuth") + self.require_setting("google_consumer_secret", "Google OAuth") + return dict( + key=self.settings["google_consumer_key"], + secret=self.settings["google_consumer_secret"]) + + def _oauth_get_user(self, access_token, callback): + OpenIdMixin.get_authenticated_user(self, callback) + +class FacebookMixin(object): + """Facebook Connect authentication. + + New applications should consider using `FacebookGraphMixin` below instead + of this class. + + To authenticate with Facebook, register your application with + Facebook at http://www.facebook.com/developers/apps.php. Then + copy your API Key and Application Secret to the application settings + 'facebook_api_key' and 'facebook_secret'. + + When your application is set up, you can use this Mixin like this + to authenticate the user with Facebook:: + + class FacebookHandler(tornado.web.RequestHandler, + tornado.auth.FacebookMixin): + @tornado.web.asynchronous + def get(self): + if self.get_argument("session", None): + self.get_authenticated_user(self.async_callback(self._on_auth)) + return + self.authenticate_redirect() + + def _on_auth(self, user): + if not user: + raise tornado.web.HTTPError(500, "Facebook auth failed") + # Save the user using, e.g., set_secure_cookie() + + The user object returned by get_authenticated_user() includes the + attributes 'facebook_uid' and 'name' in addition to session attributes + like 'session_key'. You should save the session key with the user; it is + required to make requests on behalf of the user later with + facebook_request(). + """ + def authenticate_redirect(self, callback_uri=None, cancel_uri=None, + extended_permissions=None): + """Authenticates/installs this app for the current user.""" + self.require_setting("facebook_api_key", "Facebook Connect") + callback_uri = callback_uri or self.request.uri + args = { + "api_key": self.settings["facebook_api_key"], + "v": "1.0", + "fbconnect": "true", + "display": "page", + "next": urlparse.urljoin(self.request.full_url(), callback_uri), + "return_session": "true", + } + if cancel_uri: + args["cancel_url"] = urlparse.urljoin( + self.request.full_url(), cancel_uri) + if extended_permissions: + if isinstance(extended_permissions, (unicode, bytes_type)): + extended_permissions = [extended_permissions] + args["req_perms"] = ",".join(extended_permissions) + self.redirect("http://www.facebook.com/login.php?" + + urllib.urlencode(args)) + + def authorize_redirect(self, extended_permissions, callback_uri=None, + cancel_uri=None): + """Redirects to an authorization request for the given FB resource. + + The available resource names are listed at + http://wiki.developers.facebook.com/index.php/Extended_permission. + The most common resource types include: + + * publish_stream + * read_stream + * email + * sms + + extended_permissions can be a single permission name or a list of + names. To get the session secret and session key, call + get_authenticated_user() just as you would with + authenticate_redirect(). + """ + self.authenticate_redirect(callback_uri, cancel_uri, + extended_permissions) + + def get_authenticated_user(self, callback): + """Fetches the authenticated Facebook user. + + The authenticated user includes the special Facebook attributes + 'session_key' and 'facebook_uid' in addition to the standard + user attributes like 'name'. + """ + self.require_setting("facebook_api_key", "Facebook Connect") + session = escape.json_decode(self.get_argument("session")) + self.facebook_request( + method="facebook.users.getInfo", + callback=self.async_callback( + self._on_get_user_info, callback, session), + session_key=session["session_key"], + uids=session["uid"], + fields="uid,first_name,last_name,name,locale,pic_square," \ + "profile_url,username") + + def facebook_request(self, method, callback, **args): + """Makes a Facebook API REST request. + + We automatically include the Facebook API key and signature, but + it is the callers responsibility to include 'session_key' and any + other required arguments to the method. + + The available Facebook methods are documented here: + http://wiki.developers.facebook.com/index.php/API + + Here is an example for the stream.get() method:: + + class MainHandler(tornado.web.RequestHandler, + tornado.auth.FacebookMixin): + @tornado.web.authenticated + @tornado.web.asynchronous + def get(self): + self.facebook_request( + method="stream.get", + callback=self.async_callback(self._on_stream), + session_key=self.current_user["session_key"]) + + def _on_stream(self, stream): + if stream is None: + # Not authorized to read the stream yet? + self.redirect(self.authorize_redirect("read_stream")) + return + self.render("stream.html", stream=stream) + + """ + self.require_setting("facebook_api_key", "Facebook Connect") + self.require_setting("facebook_secret", "Facebook Connect") + if not method.startswith("facebook."): + method = "facebook." + method + args["api_key"] = self.settings["facebook_api_key"] + args["v"] = "1.0" + args["method"] = method + args["call_id"] = str(long(time.time() * 1e6)) + args["format"] = "json" + args["sig"] = self._signature(args) + url = "http://api.facebook.com/restserver.php?" + \ + urllib.urlencode(args) + http = httpclient.AsyncHTTPClient() + http.fetch(url, callback=self.async_callback( + self._parse_response, callback)) + + def _on_get_user_info(self, callback, session, users): + if users is None: + callback(None) + return + callback({ + "name": users[0]["name"], + "first_name": users[0]["first_name"], + "last_name": users[0]["last_name"], + "uid": users[0]["uid"], + "locale": users[0]["locale"], + "pic_square": users[0]["pic_square"], + "profile_url": users[0]["profile_url"], + "username": users[0].get("username"), + "session_key": session["session_key"], + "session_expires": session.get("expires"), + }) + + def _parse_response(self, callback, response): + if response.error: + logging.warning("HTTP error from Facebook: %s", response.error) + callback(None) + return + try: + json = escape.json_decode(response.body) + except Exception: + logging.warning("Invalid JSON from Facebook: %r", response.body) + callback(None) + return + if isinstance(json, dict) and json.get("error_code"): + logging.warning("Facebook error: %d: %r", json["error_code"], + json.get("error_msg")) + callback(None) + return + callback(json) + + def _signature(self, args): + parts = ["%s=%s" % (n, args[n]) for n in sorted(args.keys())] + body = "".join(parts) + self.settings["facebook_secret"] + if isinstance(body, unicode): body = body.encode("utf-8") + return hashlib.md5(body).hexdigest() + +class FacebookGraphMixin(OAuth2Mixin): + """Facebook authentication using the new Graph API and OAuth2.""" + _OAUTH_ACCESS_TOKEN_URL = "https://graph.facebook.com/oauth/access_token?" + _OAUTH_AUTHORIZE_URL = "https://graph.facebook.com/oauth/authorize?" + _OAUTH_NO_CALLBACKS = False + + def get_authenticated_user(self, redirect_uri, client_id, client_secret, + code, callback, extra_fields=None): + """Handles the login for the Facebook user, returning a user object. + + Example usage:: + + class FacebookGraphLoginHandler(LoginHandler, tornado.auth.FacebookGraphMixin): + @tornado.web.asynchronous + def get(self): + if self.get_argument("code", False): + self.get_authenticated_user( + redirect_uri='/auth/facebookgraph/', + client_id=self.settings["facebook_api_key"], + client_secret=self.settings["facebook_secret"], + code=self.get_argument("code"), + callback=self.async_callback( + self._on_login)) + return + self.authorize_redirect(redirect_uri='/auth/facebookgraph/', + client_id=self.settings["facebook_api_key"], + extra_params={"scope": "read_stream,offline_access"}) + + def _on_login(self, user): + logging.error(user) + self.finish() + + """ + http = httpclient.AsyncHTTPClient() + args = { + "redirect_uri": redirect_uri, + "code": code, + "client_id": client_id, + "client_secret": client_secret, + } + + fields = set(['id', 'name', 'first_name', 'last_name', + 'locale', 'picture', 'link']) + if extra_fields: fields.update(extra_fields) + + http.fetch(self._oauth_request_token_url(**args), + self.async_callback(self._on_access_token, redirect_uri, client_id, + client_secret, callback, fields)) + + def _on_access_token(self, redirect_uri, client_id, client_secret, + callback, fields, response): + if response.error: + logging.warning('Facebook auth error: %s' % str(response)) + callback(None) + return + + args = escape.parse_qs_bytes(escape.native_str(response.body)) + session = { + "access_token": args["access_token"][-1], + "expires": args.get("expires") + } + + self.facebook_request( + path="/me", + callback=self.async_callback( + self._on_get_user_info, callback, session, fields), + access_token=session["access_token"], + fields=",".join(fields) + ) + + + def _on_get_user_info(self, callback, session, fields, user): + if user is None: + callback(None) + return + + fieldmap = {} + for field in fields: + fieldmap[field] = user.get(field) + + fieldmap.update({"access_token": session["access_token"], "session_expires": session.get("expires")}) + callback(fieldmap) + + def facebook_request(self, path, callback, access_token=None, + post_args=None, **args): + """Fetches the given relative API path, e.g., "/btaylor/picture" + + If the request is a POST, post_args should be provided. Query + string arguments should be given as keyword arguments. + + An introduction to the Facebook Graph API can be found at + http://developers.facebook.com/docs/api + + Many methods require an OAuth access token which you can obtain + through authorize_redirect() and get_authenticated_user(). The + user returned through that process includes an 'access_token' + attribute that can be used to make authenticated requests via + this method. Example usage:: + + class MainHandler(tornado.web.RequestHandler, + tornado.auth.FacebookGraphMixin): + @tornado.web.authenticated + @tornado.web.asynchronous + def get(self): + self.facebook_request( + "/me/feed", + post_args={"message": "I am posting from my Tornado application!"}, + access_token=self.current_user["access_token"], + callback=self.async_callback(self._on_post)) + + def _on_post(self, new_entry): + if not new_entry: + # Call failed; perhaps missing permission? + self.authorize_redirect() + return + self.finish("Posted a message!") + + """ + url = "https://graph.facebook.com" + path + all_args = {} + if access_token: + all_args["access_token"] = access_token + all_args.update(args) + all_args.update(post_args or {}) + if all_args: url += "?" + urllib.urlencode(all_args) + callback = self.async_callback(self._on_facebook_request, callback) + http = httpclient.AsyncHTTPClient() + if post_args is not None: + http.fetch(url, method="POST", body=urllib.urlencode(post_args), + callback=callback) + else: + http.fetch(url, callback=callback) + + def _on_facebook_request(self, callback, response): + if response.error: + logging.warning("Error response %s fetching %s", response.error, + response.request.url) + callback(None) + return + callback(escape.json_decode(response.body)) + +def _oauth_signature(consumer_token, method, url, parameters={}, token=None): + """Calculates the HMAC-SHA1 OAuth signature for the given request. + + See http://oauth.net/core/1.0/#signing_process + """ + parts = urlparse.urlparse(url) + scheme, netloc, path = parts[:3] + normalized_url = scheme.lower() + "://" + netloc.lower() + path + + base_elems = [] + base_elems.append(method.upper()) + base_elems.append(normalized_url) + base_elems.append("&".join("%s=%s" % (k, _oauth_escape(str(v))) + for k, v in sorted(parameters.items()))) + base_string = "&".join(_oauth_escape(e) for e in base_elems) + + key_elems = [escape.utf8(consumer_token["secret"])] + key_elems.append(escape.utf8(token["secret"] if token else "")) + key = b("&").join(key_elems) + + hash = hmac.new(key, escape.utf8(base_string), hashlib.sha1) + return binascii.b2a_base64(hash.digest())[:-1] + +def _oauth10a_signature(consumer_token, method, url, parameters={}, token=None): + """Calculates the HMAC-SHA1 OAuth 1.0a signature for the given request. + + See http://oauth.net/core/1.0a/#signing_process + """ + parts = urlparse.urlparse(url) + scheme, netloc, path = parts[:3] + normalized_url = scheme.lower() + "://" + netloc.lower() + path + + base_elems = [] + base_elems.append(method.upper()) + base_elems.append(normalized_url) + base_elems.append("&".join("%s=%s" % (k, _oauth_escape(str(v))) + for k, v in sorted(parameters.items()))) + + base_string = "&".join(_oauth_escape(e) for e in base_elems) + key_elems = [escape.utf8(urllib.quote(consumer_token["secret"], safe='~'))] + key_elems.append(escape.utf8(urllib.quote(token["secret"], safe='~') if token else "")) + key = b("&").join(key_elems) + + hash = hmac.new(key, escape.utf8(base_string), hashlib.sha1) + return binascii.b2a_base64(hash.digest())[:-1] + +def _oauth_escape(val): + if isinstance(val, unicode): + val = val.encode("utf-8") + return urllib.quote(val, safe="~") + + +def _oauth_parse_response(body): + p = escape.parse_qs(body, keep_blank_values=False) + token = dict(key=p[b("oauth_token")][0], secret=p[b("oauth_token_secret")][0]) + + # Add the extra parameters the Provider included to the token + special = (b("oauth_token"), b("oauth_token_secret")) + token.update((k, p[k][0]) for k in p if k not in special) + return token + + diff --git a/libs/tornado/autoreload.py b/libs/tornado/autoreload.py new file mode 100644 index 00000000..7e3a3d73 --- /dev/null +++ b/libs/tornado/autoreload.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python +# +# Copyright 2009 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""A module to automatically restart the server when a module is modified. + +Most applications should not call this module directly. Instead, pass the +keyword argument ``debug=True`` to the `tornado.web.Application` constructor. +This will enable autoreload mode as well as checking for changes to templates +and static resources. + +This module depends on IOLoop, so it will not work in WSGI applications +and Google AppEngine. It also will not work correctly when HTTPServer's +multi-process mode is used. +""" + +from __future__ import with_statement + +import functools +import logging +import os +import pkgutil +import sys +import types +import subprocess + +from tornado import ioloop +from tornado import process + +try: + import signal +except ImportError: + signal = None + +def start(io_loop=None, check_time=500): + """Restarts the process automatically when a module is modified. + + We run on the I/O loop, and restarting is a destructive operation, + so will terminate any pending requests. + """ + io_loop = io_loop or ioloop.IOLoop.instance() + add_reload_hook(functools.partial(_close_all_fds, io_loop)) + modify_times = {} + callback = functools.partial(_reload_on_update, modify_times) + scheduler = ioloop.PeriodicCallback(callback, check_time, io_loop=io_loop) + scheduler.start() + +def wait(): + """Wait for a watched file to change, then restart the process. + + Intended to be used at the end of scripts like unit test runners, + to run the tests again after any source file changes (but see also + the command-line interface in `main`) + """ + io_loop = ioloop.IOLoop() + start(io_loop) + io_loop.start() + +_watched_files = set() + +def watch(filename): + """Add a file to the watch list. + + All imported modules are watched by default. + """ + _watched_files.add(filename) + +_reload_hooks = [] + +def add_reload_hook(fn): + """Add a function to be called before reloading the process. + + Note that for open file and socket handles it is generally + preferable to set the ``FD_CLOEXEC`` flag (using `fcntl` or + `tornado.platform.auto.set_close_exec`) instead of using a reload + hook to close them. + """ + _reload_hooks.append(fn) + +def _close_all_fds(io_loop): + for fd in io_loop._handlers.keys(): + try: + os.close(fd) + except Exception: + pass + +_reload_attempted = False + +def _reload_on_update(modify_times): + if _reload_attempted: + # We already tried to reload and it didn't work, so don't try again. + return + if process.task_id() is not None: + # We're in a child process created by fork_processes. If child + # processes restarted themselves, they'd all restart and then + # all call fork_processes again. + return + for module in sys.modules.values(): + # Some modules play games with sys.modules (e.g. email/__init__.py + # in the standard library), and occasionally this can cause strange + # failures in getattr. Just ignore anything that's not an ordinary + # module. + if not isinstance(module, types.ModuleType): continue + path = getattr(module, "__file__", None) + if not path: continue + if path.endswith(".pyc") or path.endswith(".pyo"): + path = path[:-1] + _check_file(modify_times, path) + for path in _watched_files: + _check_file(modify_times, path) + +def _check_file(modify_times, path): + try: + modified = os.stat(path).st_mtime + except Exception: + return + if path not in modify_times: + modify_times[path] = modified + return + if modify_times[path] != modified: + logging.info("%s modified; restarting server", path) + _reload() + +def _reload(): + global _reload_attempted + _reload_attempted = True + for fn in _reload_hooks: + fn() + if hasattr(signal, "setitimer"): + # Clear the alarm signal set by + # ioloop.set_blocking_log_threshold so it doesn't fire + # after the exec. + signal.setitimer(signal.ITIMER_REAL, 0, 0) + if sys.platform == 'win32': + # os.execv is broken on Windows and can't properly parse command line + # arguments and executable name if they contain whitespaces. subprocess + # fixes that behavior. + subprocess.Popen([sys.executable] + sys.argv) + sys.exit(0) + else: + try: + os.execv(sys.executable, [sys.executable] + sys.argv) + except OSError: + # Mac OS X versions prior to 10.6 do not support execv in + # a process that contains multiple threads. Instead of + # re-executing in the current process, start a new one + # and cause the current process to exit. This isn't + # ideal since the new process is detached from the parent + # terminal and thus cannot easily be killed with ctrl-C, + # but it's better than not being able to autoreload at + # all. + # Unfortunately the errno returned in this case does not + # appear to be consistent, so we can't easily check for + # this error specifically. + os.spawnv(os.P_NOWAIT, sys.executable, + [sys.executable] + sys.argv) + sys.exit(0) + +_USAGE = """\ +Usage: + python -m tornado.autoreload -m module.to.run [args...] + python -m tornado.autoreload path/to/script.py [args...] +""" +def main(): + """Command-line wrapper to re-run a script whenever its source changes. + + Scripts may be specified by filename or module name:: + + python -m tornado.autoreload -m tornado.test.runtests + python -m tornado.autoreload tornado/test/runtests.py + + Running a script with this wrapper is similar to calling + `tornado.autoreload.wait` at the end of the script, but this wrapper + can catch import-time problems like syntax errors that would otherwise + prevent the script from reaching its call to `wait`. + """ + original_argv = sys.argv + sys.argv = sys.argv[:] + if len(sys.argv) >= 3 and sys.argv[1] == "-m": + mode = "module" + module = sys.argv[2] + del sys.argv[1:3] + elif len(sys.argv) >= 2: + mode = "script" + script = sys.argv[1] + sys.argv = sys.argv[1:] + else: + print >>sys.stderr, _USAGE + sys.exit(1) + + try: + if mode == "module": + import runpy + runpy.run_module(module, run_name="__main__", alter_sys=True) + elif mode == "script": + with open(script) as f: + global __file__ + __file__ = script + # Use globals as our "locals" dictionary so that + # something that tries to import __main__ (e.g. the unittest + # module) will see the right things. + exec f.read() in globals(), globals() + except SystemExit, e: + logging.info("Script exited with status %s", e.code) + except Exception, e: + logging.warning("Script exited with uncaught exception", exc_info=True) + if isinstance(e, SyntaxError): + watch(e.filename) + else: + logging.info("Script exited normally") + # restore sys.argv so subsequent executions will include autoreload + sys.argv = original_argv + + if mode == 'module': + # runpy did a fake import of the module as __main__, but now it's + # no longer in sys.modules. Figure out where it is and watch it. + watch(pkgutil.get_loader(module).get_filename()) + + wait() + + +if __name__ == "__main__": + # If this module is run with "python -m tornado.autoreload", the current + # directory is automatically prepended to sys.path, but not if it is + # run as "path/to/tornado/autoreload.py". The processing for "-m" rewrites + # the former to the latter, so subsequent executions won't have the same + # path as the original. Modify os.environ here to ensure that the + # re-executed process will have the same path. + # Conversely, when run as path/to/tornado/autoreload.py, the directory + # containing autoreload.py gets added to the path, but we don't want + # tornado modules importable at top level, so remove it. + path_prefix = '.' + os.pathsep + if (sys.path[0] == '' and + not os.environ.get("PYTHONPATH", "").startswith(path_prefix)): + os.environ["PYTHONPATH"] = path_prefix + os.environ.get("PYTHONPATH", "") + elif sys.path[0] == os.path.dirname(__file__): + del sys.path[0] + main() diff --git a/libs/tornado/ca-certificates.crt b/libs/tornado/ca-certificates.crt new file mode 100644 index 00000000..26971c8b --- /dev/null +++ b/libs/tornado/ca-certificates.crt @@ -0,0 +1,3576 @@ +# This file contains certificates of known certificate authorities +# for use with SimpleAsyncHTTPClient. +# +# It was copied from /etc/ssl/certs/ca-certificates.crt +# on a stock install of Ubuntu 11.04 (ca-certificates package +# version 20090814+nmu2ubuntu0.1). This data file is licensed +# under the MPL/GPL. +-----BEGIN CERTIFICATE----- +MIIEuDCCA6CgAwIBAgIBBDANBgkqhkiG9w0BAQUFADCBtDELMAkGA1UEBhMCQlIx +EzARBgNVBAoTCklDUC1CcmFzaWwxPTA7BgNVBAsTNEluc3RpdHV0byBOYWNpb25h +bCBkZSBUZWNub2xvZ2lhIGRhIEluZm9ybWFjYW8gLSBJVEkxETAPBgNVBAcTCEJy +YXNpbGlhMQswCQYDVQQIEwJERjExMC8GA1UEAxMoQXV0b3JpZGFkZSBDZXJ0aWZp +Y2Fkb3JhIFJhaXogQnJhc2lsZWlyYTAeFw0wMTExMzAxMjU4MDBaFw0xMTExMzAy +MzU5MDBaMIG0MQswCQYDVQQGEwJCUjETMBEGA1UEChMKSUNQLUJyYXNpbDE9MDsG +A1UECxM0SW5zdGl0dXRvIE5hY2lvbmFsIGRlIFRlY25vbG9naWEgZGEgSW5mb3Jt +YWNhbyAtIElUSTERMA8GA1UEBxMIQnJhc2lsaWExCzAJBgNVBAgTAkRGMTEwLwYD +VQQDEyhBdXRvcmlkYWRlIENlcnRpZmljYWRvcmEgUmFpeiBCcmFzaWxlaXJhMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwPMudwX/hvm+Uh2b/lQAcHVA +isamaLkWdkwP9/S/tOKIgRrL6Oy+ZIGlOUdd6uYtk9Ma/3pUpgcfNAj0vYm5gsyj +Qo9emsc+x6m4VWwk9iqMZSCK5EQkAq/Ut4n7KuLE1+gdftwdIgxfUsPt4CyNrY50 +QV57KM2UT8x5rrmzEjr7TICGpSUAl2gVqe6xaii+bmYR1QrmWaBSAG59LrkrjrYt +bRhFboUDe1DK+6T8s5L6k8c8okpbHpa9veMztDVC9sPJ60MWXh6anVKo1UcLcbUR +yEeNvZneVRKAAU6ouwdjDvwlsaKydFKwed0ToQ47bmUKgcm+wV3eTRk36UOnTwID +AQABo4HSMIHPME4GA1UdIARHMEUwQwYFYEwBAQAwOjA4BggrBgEFBQcCARYsaHR0 +cDovL2FjcmFpei5pY3BicmFzaWwuZ292LmJyL0RQQ2FjcmFpei5wZGYwPQYDVR0f +BDYwNDAyoDCgLoYsaHR0cDovL2FjcmFpei5pY3BicmFzaWwuZ292LmJyL0xDUmFj +cmFpei5jcmwwHQYDVR0OBBYEFIr68VeEERM1kEL6V0lUaQ2kxPA3MA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQAZA5c1 +U/hgIh6OcgLAfiJgFWpvmDZWqlV30/bHFpj8iBobJSm5uDpt7TirYh1Uxe3fQaGl +YjJe+9zd+izPRbBqXPVQA34EXcwk4qpWuf1hHriWfdrx8AcqSqr6CuQFwSr75Fos +SzlwDADa70mT7wZjAmQhnZx2xJ6wfWlT9VQfS//JYeIc7Fue2JNLd00UOSMMaiK/ +t79enKNHEA2fupH3vEigf5Eh4bVAN5VohrTm6MY53x7XQZZr1ME7a55lFEnSeT0u +mlOAjR2mAbvSM5X5oSZNrmetdzyTj2flCM8CC7MLab0kkdngRIlUBGHF1/S5nmPb +K+9A46sd33oqK8n8 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIHPTCCBSWgAwIBAgIBADANBgkqhkiG9w0BAQQFADB5MRAwDgYDVQQKEwdSb290 +IENBMR4wHAYDVQQLExVodHRwOi8vd3d3LmNhY2VydC5vcmcxIjAgBgNVBAMTGUNB +IENlcnQgU2lnbmluZyBBdXRob3JpdHkxITAfBgkqhkiG9w0BCQEWEnN1cHBvcnRA +Y2FjZXJ0Lm9yZzAeFw0wMzAzMzAxMjI5NDlaFw0zMzAzMjkxMjI5NDlaMHkxEDAO +BgNVBAoTB1Jvb3QgQ0ExHjAcBgNVBAsTFWh0dHA6Ly93d3cuY2FjZXJ0Lm9yZzEi +MCAGA1UEAxMZQ0EgQ2VydCBTaWduaW5nIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJ +ARYSc3VwcG9ydEBjYWNlcnQub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAziLA4kZ97DYoB1CW8qAzQIxL8TtmPzHlawI229Z89vGIj053NgVBlfkJ +8BLPRoZzYLdufujAWGSuzbCtRRcMY/pnCujW0r8+55jE8Ez64AO7NV1sId6eINm6 +zWYyN3L69wj1x81YyY7nDl7qPv4coRQKFWyGhFtkZip6qUtTefWIonvuLwphK42y +fk1WpRPs6tqSnqxEQR5YYGUFZvjARL3LlPdCfgv3ZWiYUQXw8wWRBB0bF4LsyFe7 +w2t6iPGwcswlWyCR7BYCEo8y6RcYSNDHBS4CMEK4JZwFaz+qOqfrU0j36NK2B5jc +G8Y0f3/JHIJ6BVgrCFvzOKKrF11myZjXnhCLotLddJr3cQxyYN/Nb5gznZY0dj4k +epKwDpUeb+agRThHqtdB7Uq3EvbXG4OKDy7YCbZZ16oE/9KTfWgu3YtLq1i6L43q +laegw1SJpfvbi1EinbLDvhG+LJGGi5Z4rSDTii8aP8bQUWWHIbEZAWV/RRyH9XzQ +QUxPKZgh/TMfdQwEUfoZd9vUFBzugcMd9Zi3aQaRIt0AUMyBMawSB3s42mhb5ivU +fslfrejrckzzAeVLIL+aplfKkQABi6F1ITe1Yw1nPkZPcCBnzsXWWdsC4PDSy826 +YreQQejdIOQpvGQpQsgi3Hia/0PsmBsJUUtaWsJx8cTLc6nloQsCAwEAAaOCAc4w +ggHKMB0GA1UdDgQWBBQWtTIb1Mfz4OaO873SsDrusjkY0TCBowYDVR0jBIGbMIGY +gBQWtTIb1Mfz4OaO873SsDrusjkY0aF9pHsweTEQMA4GA1UEChMHUm9vdCBDQTEe +MBwGA1UECxMVaHR0cDovL3d3dy5jYWNlcnQub3JnMSIwIAYDVQQDExlDQSBDZXJ0 +IFNpZ25pbmcgQXV0aG9yaXR5MSEwHwYJKoZIhvcNAQkBFhJzdXBwb3J0QGNhY2Vy +dC5vcmeCAQAwDwYDVR0TAQH/BAUwAwEB/zAyBgNVHR8EKzApMCegJaAjhiFodHRw +czovL3d3dy5jYWNlcnQub3JnL3Jldm9rZS5jcmwwMAYJYIZIAYb4QgEEBCMWIWh0 +dHBzOi8vd3d3LmNhY2VydC5vcmcvcmV2b2tlLmNybDA0BglghkgBhvhCAQgEJxYl +aHR0cDovL3d3dy5jYWNlcnQub3JnL2luZGV4LnBocD9pZD0xMDBWBglghkgBhvhC +AQ0ESRZHVG8gZ2V0IHlvdXIgb3duIGNlcnRpZmljYXRlIGZvciBGUkVFIGhlYWQg +b3ZlciB0byBodHRwOi8vd3d3LmNhY2VydC5vcmcwDQYJKoZIhvcNAQEEBQADggIB +ACjH7pyCArpcgBLKNQodgW+JapnM8mgPf6fhjViVPr3yBsOQWqy1YPaZQwGjiHCc +nWKdpIevZ1gNMDY75q1I08t0AoZxPuIrA2jxNGJARjtT6ij0rPtmlVOKTV39O9lg +18p5aTuxZZKmxoGCXJzN600BiqXfEVWqFcofN8CCmHBh22p8lqOOLlQ+TyGpkO/c +gr/c6EWtTZBzCDyUZbAEmXZ/4rzCahWqlwQ3JNgelE5tDlG+1sSPypZt90Pf6DBl +Jzt7u0NDY8RD97LsaMzhGY4i+5jhe1o+ATc7iwiwovOVThrLm82asduycPAtStvY +sONvRUgzEv/+PDIqVPfE94rwiCPCR/5kenHA0R6mY7AHfqQv0wGP3J8rtsYIqQ+T +SCX8Ev2fQtzzxD72V7DX3WnRBnc0CkvSyqD/HMaMyRa+xMwyN2hzXwj7UfdJUzYF +CpUCTPJ5GhD22Dp1nPMd8aINcGeGG7MW9S/lpOt5hvk9C8JzC6WZrG/8Z7jlLwum +GCSNe9FINSkYQKyTYOGWhlC0elnYjyELn8+CkcY7v2vcB5G5l1YjqrZslMZIBjzk +zk6q5PYvCdxTby78dOs6Y5nCpqyJvKeyRKANihDjbPIky/qbn3BHLt4Ui9SyIAmW +omTxJBzcoTWcFbLUvFUufQb1nA5V9FrWk9p2rSVzTMVD +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGCDCCA/CgAwIBAgIBATANBgkqhkiG9w0BAQQFADB5MRAwDgYDVQQKEwdSb290 +IENBMR4wHAYDVQQLExVodHRwOi8vd3d3LmNhY2VydC5vcmcxIjAgBgNVBAMTGUNB +IENlcnQgU2lnbmluZyBBdXRob3JpdHkxITAfBgkqhkiG9w0BCQEWEnN1cHBvcnRA +Y2FjZXJ0Lm9yZzAeFw0wNTEwMTQwNzM2NTVaFw0zMzAzMjgwNzM2NTVaMFQxFDAS +BgNVBAoTC0NBY2VydCBJbmMuMR4wHAYDVQQLExVodHRwOi8vd3d3LkNBY2VydC5v +cmcxHDAaBgNVBAMTE0NBY2VydCBDbGFzcyAzIFJvb3QwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCrSTURSHzSJn5TlM9Dqd0o10Iqi/OHeBlYfA+e2ol9 +4fvrcpANdKGWZKufoCSZc9riVXbHF3v1BKxGuMO+f2SNEGwk82GcwPKQ+lHm9WkB +Y8MPVuJKQs/iRIwlKKjFeQl9RrmK8+nzNCkIReQcn8uUBByBqBSzmGXEQ+xOgo0J +0b2qW42S0OzekMV/CsLj6+YxWl50PpczWejDAz1gM7/30W9HxM3uYoNSbi4ImqTZ +FRiRpoWSR7CuSOtttyHshRpocjWr//AQXcD0lKdq1TuSfkyQBX6TwSyLpI5idBVx +bgtxA+qvFTia1NIFcm+M+SvrWnIl+TlG43IbPgTDZCciECqKT1inA62+tC4T7V2q +SNfVfdQqe1z6RgRQ5MwOQluM7dvyz/yWk+DbETZUYjQ4jwxgmzuXVjit89Jbi6Bb +6k6WuHzX1aCGcEDTkSm3ojyt9Yy7zxqSiuQ0e8DYbF/pCsLDpyCaWt8sXVJcukfV +m+8kKHA4IC/VfynAskEDaJLM4JzMl0tF7zoQCqtwOpiVcK01seqFK6QcgCExqa5g +eoAmSAC4AcCTY1UikTxW56/bOiXzjzFU6iaLgVn5odFTEcV7nQP2dBHgbbEsPyyG +kZlxmqZ3izRg0RS0LKydr4wQ05/EavhvE/xzWfdmQnQeiuP43NJvmJzLR5iVQAX7 +6QIDAQABo4G/MIG8MA8GA1UdEwEB/wQFMAMBAf8wXQYIKwYBBQUHAQEEUTBPMCMG +CCsGAQUFBzABhhdodHRwOi8vb2NzcC5DQWNlcnQub3JnLzAoBggrBgEFBQcwAoYc +aHR0cDovL3d3dy5DQWNlcnQub3JnL2NhLmNydDBKBgNVHSAEQzBBMD8GCCsGAQQB +gZBKMDMwMQYIKwYBBQUHAgEWJWh0dHA6Ly93d3cuQ0FjZXJ0Lm9yZy9pbmRleC5w +aHA/aWQ9MTAwDQYJKoZIhvcNAQEEBQADggIBAH8IiKHaGlBJ2on7oQhy84r3HsQ6 +tHlbIDCxRd7CXdNlafHCXVRUPIVfuXtCkcKZ/RtRm6tGpaEQU55tiKxzbiwzpvD0 +nuB1wT6IRanhZkP+VlrRekF490DaSjrxC1uluxYG5sLnk7mFTZdPsR44Q4Dvmw2M +77inYACHV30eRBzLI++bPJmdr7UpHEV5FpZNJ23xHGzDwlVks7wU4vOkHx4y/CcV +Bc/dLq4+gmF78CEQGPZE6lM5+dzQmiDgxrvgu1pPxJnIB721vaLbLmINQjRBvP+L +ivVRIqqIMADisNS8vmW61QNXeZvo3MhN+FDtkaVSKKKs+zZYPumUK5FQhxvWXtaM +zPcPEAxSTtAWYeXlCmy/F8dyRlecmPVsYGN6b165Ti/Iubm7aoW8mA3t+T6XhDSU +rgCvoeXnkm5OvfPi2RSLXNLrAWygF6UtEOucekq9ve7O/e0iQKtwOIj1CodqwqsF +YMlIBdpTwd5Ed2qz8zw87YC8pjhKKSRf/lk7myV6VmMAZLldpGJ9VzZPrYPvH5JT +oI53V93lYRE9IwCQTDz6o2CTBKOvNfYOao9PSmCnhQVsRqGP9Md246FZV/dxssRu +FFxtbUFm3xuTsdQAw+7Lzzw9IYCpX2Nl/N3gX6T0K/CFcUHUZyX7GrGXrtaZghNB +0m6lG5kngOcLqagA +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIESzCCAzOgAwIBAgIJAJigUTEEXRQpMA0GCSqGSIb3DQEBBQUAMHYxCzAJBgNV +BAYTAkRFMQ8wDQYDVQQIEwZIZXNzZW4xDjAMBgNVBAcTBUZ1bGRhMRAwDgYDVQQK +EwdEZWJjb25mMRMwEQYDVQQDEwpEZWJjb25mIENBMR8wHQYJKoZIhvcNAQkBFhBq +b2VyZ0BkZWJpYW4ub3JnMB4XDTA1MTEwNTE3NTUxNFoXDTE1MTEwMzE3NTUxNFow +djELMAkGA1UEBhMCREUxDzANBgNVBAgTBkhlc3NlbjEOMAwGA1UEBxMFRnVsZGEx +EDAOBgNVBAoTB0RlYmNvbmYxEzARBgNVBAMTCkRlYmNvbmYgQ0ExHzAdBgkqhkiG +9w0BCQEWEGpvZXJnQGRlYmlhbi5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCvbOo0SrIwI5IMlsshH8WF3dHB9r9JlSKhMPaybawa1EyvZspMQ3wa +F5qxNf3Sj+NElEmjseEqvCZiIIzqwerHu0Qw62cDYCdCd2+Wb5m0bPYB5CGHiyU1 +eNP0je42O0YeXG2BvUujN8AviocVo39X2YwNQ0ryy4OaqYgm2pRlbtT2ESbF+SfV +Y2iqQj/f8ymF+lHo/pz8tbAqxWcqaSiHFAVQJrdqtFhtoodoNiE3q76zJoUkZTXB +k60Yc3MJSnatZCpnsSBr/D7zpntl0THrUjjtdRWCjQVhqfhM1yZJV+ApbLdheFh0 +ZWlSxdnp25p0q0XYw/7G92ELyFDfBUUNAgMBAAGjgdswgdgwHQYDVR0OBBYEFMuV +dFNb4mCWUFbcP5LOtxFLrEVTMIGoBgNVHSMEgaAwgZ2AFMuVdFNb4mCWUFbcP5LO +txFLrEVToXqkeDB2MQswCQYDVQQGEwJERTEPMA0GA1UECBMGSGVzc2VuMQ4wDAYD +VQQHEwVGdWxkYTEQMA4GA1UEChMHRGViY29uZjETMBEGA1UEAxMKRGViY29uZiBD +QTEfMB0GCSqGSIb3DQEJARYQam9lcmdAZGViaWFuLm9yZ4IJAJigUTEEXRQpMAwG +A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAGZXxHg4mnkvilRIM1EQfGdY +S5b/WcyF2MYSTeTvK4aIB6VHwpZoZCnDGj2m2D3CkHT0upAD9o0zM1tdsfncLzV+ +mDT/jNmBtYo4QXx5vEPwvEIcgrWjwk7SyaEUhZjtolTkHB7ACl0oD0r71St4iEPR +qTUCEXk2E47bg1Fz58wNt/yo2+4iqiRjg1XCH4evkQuhpW+dTZnDyFNqwSYZapOE +TBA+9zBb6xD1KM2DdY7r4GiyYItN0BKLfuWbh9LXGbl1C+f4P11g+m2MPiavIeCe +1iazG5pcS3KoTLACsYlEX24TINtg4kcuS81XdllcnsV3Kdts0nIqPj6uhTTZD0k= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDvjCCA3ygAwIBAgIFJQaThoEwCwYHKoZIzjgEAwUAMIGFMQswCQYDVQQGEwJG +UjEPMA0GA1UECBMGRnJhbmNlMQ4wDAYDVQQHEwVQYXJpczEQMA4GA1UEChMHUE0v +U0dETjEOMAwGA1UECxMFRENTU0kxDjAMBgNVBAMTBUlHQy9BMSMwIQYJKoZIhvcN +AQkBFhRpZ2NhQHNnZG4ucG0uZ291di5mcjAeFw0wMjEyMTMxNDM5MTVaFw0yMDEw +MTcxNDM5MTRaMIGFMQswCQYDVQQGEwJGUjEPMA0GA1UECBMGRnJhbmNlMQ4wDAYD +VQQHEwVQYXJpczEQMA4GA1UEChMHUE0vU0dETjEOMAwGA1UECxMFRENTU0kxDjAM +BgNVBAMTBUlHQy9BMSMwIQYJKoZIhvcNAQkBFhRpZ2NhQHNnZG4ucG0uZ291di5m +cjCCAbYwggErBgcqhkjOOAQBMIIBHgKBgQCFkMImdk9zDzJfTO4XPdAAmLbAdWws +ZiEMZh19RyTo3CyhFqO77OIXrwY6vc1pcc3MgWJ0dgQpAgrDMtmFFxpUu4gmjVsx +8GpxQC+4VOgLY8Cvmcd/UDzYg07EIRto8BwCpPJ/JfUxwzV2V3N713aAX+cEoKZ/ +s+kgxC6nZCA7oQIVALME/JYjkdW2uKIGngsEPbXAjdhDAoGADh/uqWJx94UBm31c +9d8ZTBfRGRnmSSRVFDgPWgA69JD4BR5da8tKz+1HjfMhDXljbMH86ixpD5Ka1Z0V +pRYUPbyAoB37tsmXMJY7kjyD19d5VdaZboUjVvhH6UJy5lpNNNGSvFl4fqkxyvw+ +pq1QV0N5RcvK120hlXdfHUX+YKYDgYQAAoGAQGr7IuKJcYIvJRMjxwl43KxXY2xC +aoCiM/bv117MfI94aNf1UusGhp7CbYAY9CXuL60P0oPMAajbaTE5Z34AuITeHq3Y +CNMHwxalip8BHqSSGmGiQsXeK7T+r1rPXsccZ1c5ikGDZ4xn5gUaCyy2rCmb+fOJ +6VAfCbAbAjmNKwejdzB1MA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgFGMBUG +A1UdIAQOMAwwCgYIKoF6AXkBAQEwHQYDVR0OBBYEFPkeNRcUf8idzpKblYbLNxs0 +MQhSMB8GA1UdIwQYMBaAFPkeNRcUf8idzpKblYbLNxs0MQhSMAsGByqGSM44BAMF +AAMvADAsAhRVh+CJA5eVyEYU5AO9Tm7GxX0rmQIUBCqsU5u1WxoZ5lEXicDX5/Ob +sRQ= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEAjCCAuqgAwIBAgIFORFFEJQwDQYJKoZIhvcNAQEFBQAwgYUxCzAJBgNVBAYT +AkZSMQ8wDQYDVQQIEwZGcmFuY2UxDjAMBgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQ +TS9TR0ROMQ4wDAYDVQQLEwVEQ1NTSTEOMAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG +9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2LmZyMB4XDTAyMTIxMzE0MjkyM1oXDTIw +MTAxNzE0MjkyMlowgYUxCzAJBgNVBAYTAkZSMQ8wDQYDVQQIEwZGcmFuY2UxDjAM +BgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQTS9TR0ROMQ4wDAYDVQQLEwVEQ1NTSTEO +MAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2 +LmZyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsh/R0GLFMzvABIaI +s9z4iPf930Pfeo2aSVz2TqrMHLmh6yeJ8kbpO0px1R2OLc/mratjUMdUC24SyZA2 +xtgv2pGqaMVy/hcKshd+ebUyiHDKcMCWSo7kVc0dJ5S/znIq7Fz5cyD+vfcuiWe4 +u0dzEvfRNWk68gq5rv9GQkaiv6GFGvm/5P9JhfejcIYyHF2fYPepraX/z9E0+X1b +F8bc1g4oa8Ld8fUzaJ1O/Id8NhLWo4DoQw1VYZTqZDdH6nfK0LJYBcNdfrGoRpAx +Vs5wKpayMLh35nnAvSk7/ZR3TL0gzUEl4C7HG7vupARB0l2tEmqKm0f7yd1GQOGd +PDPQtQIDAQABo3cwdTAPBgNVHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBRjAVBgNV +HSAEDjAMMAoGCCqBegF5AQEBMB0GA1UdDgQWBBSjBS8YYFDCiQrdKyFP/45OqDAx +NjAfBgNVHSMEGDAWgBSjBS8YYFDCiQrdKyFP/45OqDAxNjANBgkqhkiG9w0BAQUF +AAOCAQEABdwm2Pp3FURo/C9mOnTgXeQp/wYHE4RKq89toB9RlPhJy3Q2FLwV3duJ +L92PoF189RLrn544pEfMs5bZvpwlqwN+Mw+VgQ39FuCIvjfwbF3QMZsyK10XZZOY +YLxuj7GoPB7ZHPOpJkL5ZB3C55L29B5aqhlSXa/oovdgoPaN8In1buAKBQGVyYsg +Crpa/JosPL3Dt8ldeCUFP1YUmwza+zpI/pdpXsoQhvdOlgQITeywvl3cO45Pwf2a +NjSaTFR+FwNIlQgRHAdvhQh+XU3Endv7rs6y0bO4g2wdsrN58dhwmX7wEwLOXt1R +0982gaEbeC9xs/FZTEYYKKuF0mBWWg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIRANAeQJAAAEZSAAAAAQAAAAQwDQYJKoZIhvcNAQEFBQAw +gYkxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJEQzETMBEGA1UEBxMKV2FzaGluZ3Rv +bjEXMBUGA1UEChMOQUJBLkVDT00sIElOQy4xGTAXBgNVBAMTEEFCQS5FQ09NIFJv +b3QgQ0ExJDAiBgkqhkiG9w0BCQEWFWFkbWluQGRpZ3NpZ3RydXN0LmNvbTAeFw05 +OTA3MTIxNzMzNTNaFw0wOTA3MDkxNzMzNTNaMIGJMQswCQYDVQQGEwJVUzELMAkG +A1UECBMCREMxEzARBgNVBAcTCldhc2hpbmd0b24xFzAVBgNVBAoTDkFCQS5FQ09N +LCBJTkMuMRkwFwYDVQQDExBBQkEuRUNPTSBSb290IENBMSQwIgYJKoZIhvcNAQkB +FhVhZG1pbkBkaWdzaWd0cnVzdC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCx0xHgeVVDBwhMywVCAOINg0Y95JO6tgbTDVm9PsHOQ2cBiiGo77zM +0KLMsFWWU4RmBQDaREmA2FQKpSWGlO1jVv9wbKOhGdJ4vmgqRF4vz8wYXke8OrFG +PR7wuSw0X4x8TAgpnUBV6zx9g9618PeKgw6hTLQ6pbNfWiKX7BmbwQVo/ea3qZGU +LOR4SCQaJRk665WcOQqKz0Ky8BzVX/tr7WhWezkscjiw7pOp03t3POtxA6k4ShZs +iSrK2jMTecJVjO2cu/LLWxD4LmE1xilMKtAqY9FlWbT4zfn0AIS2V0KFnTKo+SpU ++/94Qby9cSj0u5C8/5Y0BONFnqFGKECBAgMBAAGjFjAUMBIGA1UdEwEB/wQIMAYB +Af8CAQgwDQYJKoZIhvcNAQEFBQADggEBAARvJYbk5pYntNlCwNDJALF/VD6Hsm0k +qS8Kfv2kRLD4VAe9G52dyntQJHsRW0mjpr8SdNWJt7cvmGQlFLdh6X9ggGvTZOir +vRrWUfrAtF13Gn9kCF55xgVM8XrdTX3O5kh7VNJhkoHWG9YA8A6eKHegTYjHInYZ +w8eeG6Z3ePhfm1bR8PIXrI6dWeYf/le22V7hXZ9F7GFoGUHhsiAm/lowdiT/QHI8 +eZ98IkirRs3bs4Ysj78FQdPB4xTjQRcm0HyncUwZ6EoPclgxfexgeqMiKL0ZJGA/ +O4dzwGvky663qyVDslUte6sGDnVdNOVdc22esnVApVnJTzFxiNmIf1Q= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs +IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290 +MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux +FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h +bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v +dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt +H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9 +uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX +mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX +a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN +E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0 +WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD +VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0 +Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU +cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx +IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN +AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH +YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 +6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC +Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX +c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a +mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEGDCCAwCgAwIBAgIBATANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 +b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwHhcNMDAwNTMw +MTAzODMxWhcNMjAwNTMwMTAzODMxWjBlMQswCQYDVQQGEwJTRTEUMBIGA1UEChML +QWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYD +VQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCWltQhSWDia+hBBwzexODcEyPNwTXH+9ZOEQpnXvUGW2ul +CDtbKRY654eyNAbFvAWlA3yCyykQruGIgb3WntP+LVbBFc7jJp0VLhD7Bo8wBN6n +tGO0/7Gcrjyvd7ZWxbWroulpOj0OM3kyP3CCkplhbY0wCI9xP6ZIVxn4JdxLZlyl +dI+Yrsj5wAYi56xz36Uu+1LcsRVlIPo1Zmne3yzxbrww2ywkEtvrNTVokMsAsJch +PXQhI2U0K7t4WaPW4XY5mqRJjox0r26kmqPZm9I4XJuiGMx1I4S+6+JNM3GOGvDC ++Mcdoq0Dlyz4zyXG9rgkMbFjXZJ/Y/AlyVMuH79NAgMBAAGjgdIwgc8wHQYDVR0O +BBYEFJWxtPCUtr3H2tERCSG+wa9J/RB7MAsGA1UdDwQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MIGPBgNVHSMEgYcwgYSAFJWxtPCUtr3H2tERCSG+wa9J/RB7oWmkZzBl +MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFk +ZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENB +IFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBACxtZBsfzQ3duQH6lmM0MkhHma6X +7f1yFqZzR1r0693p9db7RcwpiURdv0Y5PejuvE1Uhh4dbOMXJ0PhiVYrqW9yTkkz +43J8KiOavD7/KCrto/8cI7pDVwlnTUtiBi34/2ydYB7YHEt9tTEv2dB8Xfjea4MY +eDdXL+gzB2ffHsdrKpV2ro9Xo/D0UrSpUwjP4E/TelOL/bscVjby/rK25Xa71SJl +pz/+0WatC7xrmYbvP33zGDLKe8bjq2RGlfgmadlVg3sslgf/WSxEo8bl6ancoWOA +WiFeIc9TVPC6b4nbqKqVz4vjccweGyBECMB6tkD9xOQ14R0WHNC8K47Wcdk= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIBATANBgkqhkiG9w0BAQUFADBkMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 +b3JrMSAwHgYDVQQDExdBZGRUcnVzdCBQdWJsaWMgQ0EgUm9vdDAeFw0wMDA1MzAx +MDQxNTBaFw0yMDA1MzAxMDQxNTBaMGQxCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtB +ZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5ldHdvcmsxIDAeBgNV +BAMTF0FkZFRydXN0IFB1YmxpYyBDQSBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA6Rowj4OIFMEg2Dybjxt+A3S72mnTRqX4jsIMEZBRpS9mVEBV +6tsfSlbunyNu9DnLoblv8n75XYcmYZ4c+OLspoH4IcUkzBEMP9smcnrHAZcHF/nX +GCwwfQ56HmIexkvA/X1id9NEHif2P0tEs7c42TkfYNVRknMDtABp4/MUTu7R3AnP +dzRGULD4EfL+OHn3Bzn+UZKXC1sIXzSGAa2Il+tmzV7R/9x98oTaunet3IAIx6eH +1lWfl2royBFkuucZKT8Rs3iQhCBSWxHveNCD9tVIkNAwHM+A+WD+eeSI8t0A65RF +62WUaUC6wNW0uLp9BBGo6zEFlpROWCGOn9Bg/QIDAQABo4HRMIHOMB0GA1UdDgQW +BBSBPjfYkrAfd59ctKtzquf2NGAv+jALBgNVHQ8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zCBjgYDVR0jBIGGMIGDgBSBPjfYkrAfd59ctKtzquf2NGAv+qFopGYwZDEL +MAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQLExRBZGRU +cnVzdCBUVFAgTmV0d29yazEgMB4GA1UEAxMXQWRkVHJ1c3QgUHVibGljIENBIFJv +b3SCAQEwDQYJKoZIhvcNAQEFBQADggEBAAP3FUr4JNojVhaTdt02KLmuG7jD8WS6 +IBh4lSknVwW8fCr0uVFV2ocC3g8WFzH4qnkuCRO7r7IgGRLlk/lL+YPoRNWyQSW/ +iHVv/xD8SlTQX/D67zZzfRs2RcYhbbQVuE7PnFylPVoAjgbjPGsye/Kf8Lb93/Ao +GEjwxrzQvzSAlsJKsW2Ox5BF3i9nrEUEo3rcVZLJR2bYGozH7ZxOmuASu7VqTITh +4SINhwBk/ox9Yjllpu9CtoAlEmEBqCQTcAARJl/6NVDFSMwGR+gn2HCNX2TmoUQm +XiLsks3/QppEIW1cxeMiHV9HEufOX1362KqxMy3ZdvJOOjMMK7MtkAY= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 +b3JrMSMwIQYDVQQDExpBZGRUcnVzdCBRdWFsaWZpZWQgQ0EgUm9vdDAeFw0wMDA1 +MzAxMDQ0NTBaFw0yMDA1MzAxMDQ0NTBaMGcxCzAJBgNVBAYTAlNFMRQwEgYDVQQK +EwtBZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5ldHdvcmsxIzAh +BgNVBAMTGkFkZFRydXN0IFF1YWxpZmllZCBDQSBSb290MIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA5B6a/twJWoekn0e+EV+vhDTbYjx5eLfpMLXsDBwq +xBb/4Oxx64r1EW7tTw2R0hIYLUkVAcKkIhPHEWT/IhKauY5cLwjPcWqzZwFZ8V1G +87B4pfYOQnrjfxvM0PC3KP0q6p6zsLkEqv32x7SxuCqg+1jxGaBvcCV+PmlKfw8i +2O+tCBGaKZnhqkRFmhJePp1tUvznoD1oL/BLcHwTOK28FSXx1s6rosAx1i+f4P8U +WfyEk9mHfExUE+uf0S0R+Bg6Ot4l2ffTQO2kBhLEO+GRwVY18BTcZTYJbqukB8c1 +0cIDMzZbdSZtQvESa0NvS3GU+jQd7RNuyoB/mC9suWXY6QIDAQABo4HUMIHRMB0G +A1UdDgQWBBQ5lYtii1zJ1IC6WA+XPxUIQ8yYpzALBgNVHQ8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zCBkQYDVR0jBIGJMIGGgBQ5lYtii1zJ1IC6WA+XPxUIQ8yYp6Fr +pGkwZzELMAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQL +ExRBZGRUcnVzdCBUVFAgTmV0d29yazEjMCEGA1UEAxMaQWRkVHJ1c3QgUXVhbGlm +aWVkIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBABmrder4i2VhlRO6aQTv +hsoToMeqT2QbPxj2qC0sVY8FtzDqQmodwCVRLae/DLPt7wh/bDxGGuoYQ992zPlm +hpwsaPXpF/gxsxjE1kh9I0xowX67ARRvxdlu3rsEQmr49lx95dr6h+sNNVJn0J6X +dgWTP5XHAeZpVTh/EGGZyeNfpso+gmNIquIISD6q8rKFYqa0p9m9N5xotS1WfbC3 +P6CxB9bpT9zeRXEwMn8bLgn5v1Kh7sKAPgZcLlVAwRv1cEWw3F369nJad9Jjzc9Y +iQBCYz95OdBEsIJuQRno3eDBiFrRHnGTHyQwdOUeqN48Jzd/g66ed8/wMLH/S5no +xqE= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDpDCCAoygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTQW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBP +bmxpbmUgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAxMB4XDTAyMDUyODA2 +MDAwMFoXDTM3MTExOTIwNDMwMFowYzELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0Ft +ZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2EgT25saW5lIFJvb3Qg +Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAKgv6KRpBgNHw+kqmP8ZonCaxlCyfqXfaE0bfA+2l2h9LaaLl+lk +hsmj76CGv2BlnEtUiMJIxUo5vxTjWVXlGbR0yLQFOVwWpeKVBeASrlmLojNoWBym +1BW32J/X3HGrfpq/m44zDyL9Hy7nBzbvYjnF3cu6JRQj3gzGPTzOggjmZj7aUTsW +OqMFf6Dch9Wc/HKpoH145LcxVR5lu9RhsCFg7RAycsWSJR74kEoYeEfffjA3PlAb +2xzTa5qGUwew76wGePiEmf4hjUyAtgyC9mZweRrTT6PP8c9GsEsPPt2IYriMqQko +O3rHl+Ee5fSfwMCuJKDIodkP1nsmgmkyPacCAwEAAaNjMGEwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUAK3Zo/Z59m50qX8zPYEX10zPM94wHwYDVR0jBBgwFoAU +AK3Zo/Z59m50qX8zPYEX10zPM94wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +BQUAA4IBAQB8itEfGDeC4Liwo+1WlchiYZwFos3CYiZhzRAW18y0ZTTQEYqtqKkF +Zu90821fnZmv9ov761KyBZiibyrFVL0lvV+uyIbqRizBs73B6UlwGBaXCBOMIOAb +LjpHyx7kADCVW/RFo8AasAFOq73AI25jP4BKxQft3OJvx8Fi8eNy1gTIdGcL+oir +oQHIb/AUr9KZzVGTfu0uOMe9zkZQPXLjeSWdm4grECDdpbgyn43gKd8hdIaC2y+C +MMbHNYaz+ZZfRtsMRf3zUMNvxsNIrUam4SdHCh0Om7bCd39j8uB9Gr784N/Xx6ds +sPmuujz9dLQR6FgNgLzTqIA6me11zEZ7 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFpDCCA4ygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTQW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBP +bmxpbmUgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAyMB4XDTAyMDUyODA2 +MDAwMFoXDTM3MDkyOTE0MDgwMFowYzELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0Ft +ZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2EgT25saW5lIFJvb3Qg +Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAMxBRR3pPU0Q9oyxQcngXssNt79Hc9PwVU3dxgz6sWYFas14tNwC +206B89enfHG8dWOgXeMHDEjsJcQDIPT/DjsS/5uN4cbVG7RtIuOx238hZK+GvFci +KtZHgVdEglZTvYYUAQv8f3SkWq7xuhG1m1hagLQ3eAkzfDJHA1zEpYNI9FdWboE2 +JxhP7JsowtS013wMPgwr38oE18aO6lhOqKSlGBxsRZijQdEt0sdtjRnxrXm3gT+9 +BoInLRBYBbV4Bbkv2wxrkJB+FFk4u5QkE+XRnRTf04JNRvCAOVIyD+OEsnpD8l7e +Xz8d3eOyG6ChKiMDbi4BFYdcpnV1x5dhvt6G3NRI270qv0pV2uh9UPu0gBe4lL8B +PeraunzgWGcXuVjgiIZGZ2ydEEdYMtA1fHkqkKJaEBEjNa0vzORKW6fIJ/KD3l67 +Xnfn6KVuY8INXWHQjNJsWiEOyiijzirplcdIz5ZvHZIlyMbGwcEMBawmxNJ10uEq +Z8A9W6Wa6897GqidFEXlD6CaZd4vKL3Ob5Rmg0gp2OpljK+T2WSfVVcmv2/LNzGZ +o2C7HK2JNDJiuEMhBnIMoVxtRsX6Kc8w3onccVvdtjc+31D1uAclJuW8tf48ArO3 ++L5DwYcRlJ4jbBeKuIonDFRH8KmzwICMoCfrHRnjB453cMor9H124HhnAgMBAAGj +YzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFE1FwWg4u3OpaaEg5+31IqEj +FNeeMB8GA1UdIwQYMBaAFE1FwWg4u3OpaaEg5+31IqEjFNeeMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQUFAAOCAgEAZ2sGuV9FOypLM7PmG2tZTiLMubekJcmn +xPBUlgtk87FYT15R/LKXeydlwuXK5w0MJXti4/qftIe3RUavg6WXSIylvfEWK5t2 +LHo1YGwRgJfMqZJS5ivmae2p+DYtLHe/YUjRYwu5W1LtGLBDQiKmsXeu3mnFzccc +obGlHBD7GL4acN3Bkku+KVqdPzW+5X1R+FXgJXUjhx5c3LqdsKyzadsXg8n33gy8 +CNyRnqjQ1xU3c6U1uPx+xURABsPr+CKAXEfOAuMRn0T//ZoyzH1kUQ7rVyZ2OuMe +IjzCpjbdGe+n/BLzJsBZMYVMnNjP36TMzCmT/5RtdlwTCJfy7aULTd3oyWgOZtMA +DjMSW7yV5TKQqLPGbIOtd+6Lfn6xqavT4fG2wLHqiMDn05DpKJKUe2h7lyoKZy2F +AjgQ5ANh1NolNscIWC2hp1GvMApJ9aZphwctREZ2jirlmjvXGKL8nDgQzMY70rUX +Om/9riW99XJZZLF0KjhfGEzfz3EEWjbUvy+ZnOjZurGV5gJLIaFb1cFPj65pbVPb +AZO1XB4Y3WRayhgoPmMEEf0cjQAPuDffZ4qdZqkCapH/E8ovXYO8h5Ns3CRRFgQl +Zvqz2cK6Kb6aSDiCmfS/O0oxGfm/jiEzFMpPVF/7zvuPcX/9XhmgD0uRuMRUvAaw +RY8mkaKO/qk= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzELMAkGA1UEBhMCVVMx +HTAbBgNVBAoTFEFPTCBUaW1lIFdhcm5lciBJbmMuMRwwGgYDVQQLExNBbWVyaWNh +IE9ubGluZSBJbmMuMTcwNQYDVQQDEy5BT0wgVGltZSBXYXJuZXIgUm9vdCBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eSAxMB4XDTAyMDUyOTA2MDAwMFoXDTM3MTEyMDE1 +MDMwMFowgYMxCzAJBgNVBAYTAlVTMR0wGwYDVQQKExRBT0wgVGltZSBXYXJuZXIg +SW5jLjEcMBoGA1UECxMTQW1lcmljYSBPbmxpbmUgSW5jLjE3MDUGA1UEAxMuQU9M +IFRpbWUgV2FybmVyIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJnej8Mlo2k06AX3dLm/WpcZuS+U +0pPlLYnKhHw/EEMbjIt8hFj4JHxIzyr9wBXZGH6EGhfT257XyuTZ16pYUYfw8ItI +TuLCxFlpMGK2MKKMCxGZYTVtfu/FsRkGIBKOQuHfD5YQUqjPnF+VFNivO3ULMSAf +RC+iYkGzuxgh28pxPIzstrkNn+9R7017EvILDOGsQI93f7DKeHEMXRZxcKLXwjqF +zQ6axOAAsNUl6twr5JQtOJyJQVdkKGUZHLZEtMgxa44Be3ZZJX8VHIQIfHNlIAqh +BC4aMqiaILGcLCFZ5/vP7nAtCMpjPiybkxlqpMKX/7eGV4iFbJ4VFitNLLMCAwEA +AaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUoTYwFsuGkABFgFOxj8jY +PXy+XxIwHwYDVR0jBBgwFoAUoTYwFsuGkABFgFOxj8jYPXy+XxIwDgYDVR0PAQH/ +BAQDAgGGMA0GCSqGSIb3DQEBBQUAA4IBAQCKIBilvrMvtKaEAEAwKfq0FHNMeUWn +9nDg6H5kHgqVfGphwu9OH77/yZkfB2FK4V1Mza3u0FIy2VkyvNp5ctZ7CegCgTXT +Ct8RHcl5oIBN/lrXVtbtDyqvpxh1MwzqwWEFT2qaifKNuZ8u77BfWgDrvq2g+EQF +Z7zLBO+eZMXpyD8Fv8YvBxzDNnGGyjhmSs3WuEvGbKeXO/oTLW4jYYehY0KswsuX +n2Fozy1MBJ3XJU8KDk2QixhWqJNIV9xvrr2eZ1d3iVCzvhGbRWeDhhmH05i9CBoW +H1iCC+GWaQVLjuyDUTEH1dSf/1l7qG6Fz9NLqUmwX7A5KGgOc90lmt4S +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF5jCCA86gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzELMAkGA1UEBhMCVVMx +HTAbBgNVBAoTFEFPTCBUaW1lIFdhcm5lciBJbmMuMRwwGgYDVQQLExNBbWVyaWNh +IE9ubGluZSBJbmMuMTcwNQYDVQQDEy5BT0wgVGltZSBXYXJuZXIgUm9vdCBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eSAyMB4XDTAyMDUyOTA2MDAwMFoXDTM3MDkyODIz +NDMwMFowgYMxCzAJBgNVBAYTAlVTMR0wGwYDVQQKExRBT0wgVGltZSBXYXJuZXIg +SW5jLjEcMBoGA1UECxMTQW1lcmljYSBPbmxpbmUgSW5jLjE3MDUGA1UEAxMuQU9M +IFRpbWUgV2FybmVyIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMjCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALQ3WggWmRToVbEbJGv8x4vmh6mJ +7ouZzU9AhqS2TcnZsdw8TQ2FTBVsRotSeJ/4I/1n9SQ6aF3Q92RhQVSji6UI0ilb +m2BPJoPRYxJWSXakFsKlnUWsi4SVqBax7J/qJBrvuVdcmiQhLE0OcR+mrF1FdAOY +xFSMFkpBd4aVdQxHAWZg/BXxD+r1FHjHDtdugRxev17nOirYlxcwfACtCJ0zr7iZ +YYCLqJV+FNwSbKTQ2O9ASQI2+W6p1h2WVgSysy0WVoaP2SBXgM1nEG2wTPDaRrbq +JS5Gr42whTg0ixQmgiusrpkLjhTXUr2eacOGAgvqdnUxCc4zGSGFQ+aJLZ8lN2fx +I2rSAG2X+Z/nKcrdH9cG6rjJuQkhn8g/BsXS6RJGAE57COtCPStIbp1n3UsC5ETz +kxmlJ85per5n0/xQpCyrw2u544BMzwVhSyvcG7mm0tCq9Stz+86QNZ8MUhy/XCFh +EVsVS6kkUfykXPcXnbDS+gfpj1bkGoxoigTTfFrjnqKhynFbotSg5ymFXQNoKk/S +Btc9+cMDLz9l+WceR0DTYw/j1Y75hauXTLPXJuuWCpTehTacyH+BCQJJKg71ZDIM +gtG6aoIbs0t0EfOMd9afv9w3pKdVBC/UMejTRrkDfNoSTllkt1ExMVCgyhwn2RAu +rda9EGYrw7AiShJbAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FE9pbQN+nZ8HGEO8txBO1b+pxCAoMB8GA1UdIwQYMBaAFE9pbQN+nZ8HGEO8txBO +1b+pxCAoMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQUFAAOCAgEAO/Ouyugu +h4X7ZVnnrREUpVe8WJ8kEle7+z802u6teio0cnAxa8cZmIDJgt43d15Ui47y6mdP +yXSEkVYJ1eV6moG2gcKtNuTxVBFT8zRFASbI5Rq8NEQh3q0l/HYWdyGQgJhXnU7q +7C+qPBR7V8F+GBRn7iTGvboVsNIYvbdVgaxTwOjdaRITQrcCtQVBynlQboIOcXKT +RuidDV29rs4prWPVVRaAMCf/drr3uNZK49m1+VLQTkCpx+XCMseqdiThawVQ68W/ +ClTluUI8JPu3B5wwn3la5uBAUhX0/Kr0VvlEl4ftDmVyXr4m+02kLQgH3thcoNyB +M5kYJRF3p+v9WAksmWsbivNSPxpNSGDxoPYzAlOL7SUJuA0t7Zdz7NeWH45gDtoQ +my8YJPamTQr5O8t1wswvziRpyQoijlmn94IM19drNZxDAGrElWe6nEXLuA4399xO +AU++CrYD062KRffaJ00psUjf5BHklka9bAI+1lHIlRcBFanyqqryvy9lG2/QuRqT +9Y41xICHPpQvZuTpqP9BnHAqTyo5GJUefvthATxRCC4oGKQWDzH9OmwjkyB24f0H +hdFbP9IcczLd+rn4jM8Ch3qaluTtT4mNU0OrDhPAARW0eTjb/G49nlG2uBOLZ8/5 +fNkiHfZdxRwBL5joeiQYvITX+txyW/fBOmg= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ +RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD +VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX +DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y +ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy +VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr +mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr +IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK +mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu +XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy +dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye +jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1 +BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3 +DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92 +9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx +jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0 +Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz +ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS +R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFajCCBFKgAwIBAgIEPLU9RjANBgkqhkiG9w0BAQUFADBmMRIwEAYDVQQKEwli +ZVRSVVNUZWQxGzAZBgNVBAsTEmJlVFJVU1RlZCBSb290IENBczEzMDEGA1UEAxMq +YmVUUlVTVGVkIFJvb3QgQ0EtQmFsdGltb3JlIEltcGxlbWVudGF0aW9uMB4XDTAy +MDQxMTA3Mzg1MVoXDTIyMDQxMTA3Mzg1MVowZjESMBAGA1UEChMJYmVUUlVTVGVk +MRswGQYDVQQLExJiZVRSVVNUZWQgUm9vdCBDQXMxMzAxBgNVBAMTKmJlVFJVU1Rl +ZCBSb290IENBLUJhbHRpbW9yZSBJbXBsZW1lbnRhdGlvbjCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBALx+xDmcjOPWHIb/ymKt4H8wRXqOGrO4x/nRNv8i +805qX4QQ+2aBw5R5MdKR4XeOGCrDFN5R9U+jK7wYFuK13XneIviCfsuBH/0nLI/6 +l2Qijvj/YaOcGx6Sj8CoCd8JEey3fTGaGuqDIQY8n7pc/5TqarjDa1U0Tz0yH92B +FODEPM2dMPgwqZfT7syj0B9fHBOB1BirlNFjw55/NZKeX0Tq7PQiXLfoPX2k+Ymp +kbIq2eszh+6l/ePazIjmiSZuxyuC0F6dWdsU7JGDBcNeDsYq0ATdcT0gTlgn/FP7 +eHgZFLL8kFKJOGJgB7Sg7KxrUNb9uShr71ItOrL/8QFArDcCAwEAAaOCAh4wggIa +MA8GA1UdEwEB/wQFMAMBAf8wggG1BgNVHSAEggGsMIIBqDCCAaQGDysGAQQBsT4A +AAEJKIORMTCCAY8wggFIBggrBgEFBQcCAjCCAToaggE2UmVsaWFuY2Ugb24gb3Ig +dXNlIG9mIHRoaXMgQ2VydGlmaWNhdGUgY3JlYXRlcyBhbiBhY2tub3dsZWRnbWVu +dCBhbmQgYWNjZXB0YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJk +IHRlcm1zIGFuZCBjb25kaXRpb25zIG9mIHVzZSwgdGhlIENlcnRpZmljYXRpb24g +UHJhY3RpY2UgU3RhdGVtZW50IGFuZCB0aGUgUmVseWluZyBQYXJ0eSBBZ3JlZW1l +bnQsIHdoaWNoIGNhbiBiZSBmb3VuZCBhdCB0aGUgYmVUUlVTVGVkIHdlYiBzaXRl +LCBodHRwOi8vd3d3LmJldHJ1c3RlZC5jb20vcHJvZHVjdHNfc2VydmljZXMvaW5k +ZXguaHRtbDBBBggrBgEFBQcCARY1aHR0cDovL3d3dy5iZXRydXN0ZWQuY29tL3By +b2R1Y3RzX3NlcnZpY2VzL2luZGV4Lmh0bWwwHQYDVR0OBBYEFEU9w6nR3D8kVpgc +cxiIav+DR+22MB8GA1UdIwQYMBaAFEU9w6nR3D8kVpgccxiIav+DR+22MA4GA1Ud +DwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEASZK8o+6svfoNyYt5hhwjdrCA +WXf82n+0S9/DZEtqTg6t8n1ZdwWtColzsPq8y9yNAIiPpqCy6qxSJ7+hSHyXEHu6 +7RMdmgduyzFiEuhjA6p9beP4G3YheBufS0OM00mG9htc9i5gFdPp43t1P9ACg9AY +gkHNZTfqjjJ+vWuZXTARyNtIVBw74acT02pIk/c9jH8F6M7ziCpjBLjqflh8AXtb +4cV97yHgjQ5dUX2xZ/2jvTg2xvI4hocalmhgRvsoFEdV4aeADGvi6t9NfJBIoDa9 +CReJf8Py05yc493EG931t3GzUwWJBtDLSoDByFOQtTwxiBdQn8nEDovYqAJjDQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFLDCCBBSgAwIBAgIEOU99hzANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJX +VzESMBAGA1UEChMJYmVUUlVTVGVkMRswGQYDVQQDExJiZVRSVVNUZWQgUm9vdCBD +QXMxGjAYBgNVBAMTEWJlVFJVU1RlZCBSb290IENBMB4XDTAwMDYyMDE0MjEwNFoX +DTEwMDYyMDEzMjEwNFowWjELMAkGA1UEBhMCV1cxEjAQBgNVBAoTCWJlVFJVU1Rl +ZDEbMBkGA1UEAxMSYmVUUlVTVGVkIFJvb3QgQ0FzMRowGAYDVQQDExFiZVRSVVNU +ZWQgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANS0c3oT +CjhVAb6JVuGUntS+WutKNHUbYSnE4a0IYCF4SP+00PpeQY1hRIfo7clY+vyTmt9P +6j41ffgzeubx181vSUs9Ty1uDoM6GHh3o8/n9E1z2Jo7Gh2+lVPPIJfCzz4kUmwM +jmVZxXH/YgmPqsWPzGCgc0rXOD8Vcr+il7dw6K/ifhYGTPWqZCZyByWtNfwYsSbX +2P8ZDoMbjNx4RWc0PfSvHI3kbWvtILNnmrRhyxdviTX/507AMhLn7uzf/5cwdO2N +R47rtMNE5qdMf1ZD6Li8tr76g5fmu/vEtpO+GRg+jIG5c4gW9JZDnGdzF5DYCW5j +rEq2I8QBoa2k5MUCAwEAAaOCAfgwggH0MA8GA1UdEwEB/wQFMAMBAf8wggFZBgNV +HSAEggFQMIIBTDCCAUgGCisGAQQBsT4BAAAwggE4MIIBAQYIKwYBBQUHAgIwgfQa +gfFSZWxpYW5jZSBvbiB0aGlzIGNlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBhc3N1 +bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFyZCB0 +ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGFuZCBjZXJ0aWZpY2F0aW9uIHBy +YWN0aWNlIHN0YXRlbWVudCwgd2hpY2ggY2FuIGJlIGZvdW5kIGF0IGJlVFJVU1Rl +ZCdzIHdlYiBzaXRlLCBodHRwczovL3d3dy5iZVRSVVNUZWQuY29tL3ZhdWx0L3Rl +cm1zMDEGCCsGAQUFBwIBFiVodHRwczovL3d3dy5iZVRSVVNUZWQuY29tL3ZhdWx0 +L3Rlcm1zMDQGA1UdHwQtMCswKaAnoCWkIzAhMRIwEAYDVQQKEwliZVRSVVNUZWQx +CzAJBgNVBAYTAldXMB0GA1UdDgQWBBQquZtpLjub2M3eKjEENGvKBxirZzAfBgNV +HSMEGDAWgBQquZtpLjub2M3eKjEENGvKBxirZzAOBgNVHQ8BAf8EBAMCAf4wDQYJ +KoZIhvcNAQEFBQADggEBAHlh26Nebhax6nZR+csVm8tpvuaBa58oH2U+3RGFktTo +Qb9+M70j5/Egv6S0phkBxoyNNXxlpE8JpNbYIxUFE6dDea/bow6be3ga8wSGWsb2 +jCBHOElQBp1yZzrwmAOtlmdE/D8QDYZN5AA7KXvOOzuZhmElQITcE2K3+spZ1gMe +1lMBzW1MaFVA4e5rxyoAAEiCswoBw2AqDPeCNe5IhpbkdNQ96gFxugR1QKepfzk5 +mlWXKWWuGVUlBXJH0+gY3Ljpr0NzARJ0o+FcXxVdJPP55PS2Z2cS52QiivalQaYc +tmBjRYoQtLpGEK5BV2VsPyMQPyEQWbfkQN0mDCP2qq4= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGUTCCBTmgAwIBAgIEPLVPQDANBgkqhkiG9w0BAQUFADBmMRIwEAYDVQQKEwli +ZVRSVVNUZWQxGzAZBgNVBAsTEmJlVFJVU1RlZCBSb290IENBczEzMDEGA1UEAxMq +YmVUUlVTVGVkIFJvb3QgQ0EgLSBFbnRydXN0IEltcGxlbWVudGF0aW9uMB4XDTAy +MDQxMTA4MjQyN1oXDTIyMDQxMTA4NTQyN1owZjESMBAGA1UEChMJYmVUUlVTVGVk +MRswGQYDVQQLExJiZVRSVVNUZWQgUm9vdCBDQXMxMzAxBgNVBAMTKmJlVFJVU1Rl +ZCBSb290IENBIC0gRW50cnVzdCBJbXBsZW1lbnRhdGlvbjCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBALr0RAOqEmq1Q+xVkrYwfTVXDNvzDSduTPdQqJtO +K2/b9a0cS12zqcH+e0TrW6MFDR/FNCswACnxeECypP869AGIF37m1CbTukzqMvtD +d5eHI8XbQ6P1KqNRXuE70mVpflUVm3rnafdE4Fe1FehmYA8NA/uCjqPoEXtsvsdj +DheT389Lrm5zdeDzqrmkwAkbhepxKYhBMvnwKg5sCfJ0a2ZsUhMfGLzUPvfYbiCe +yv78IZTuEyhL11xeDGbu6bsPwTSxfwh28z0mcMmLJR1iJAzqHHVOwBLkuhMdMCkt +VjMFu5dZfsZJT4nXLySotohAtWSSU1Yk5KKghbNekLQSM80CAwEAAaOCAwUwggMB +MIIBtwYDVR0gBIIBrjCCAaowggGmBg8rBgEEAbE+AAACCSiDkTEwggGRMIIBSQYI +KwYBBQUHAgIwggE7GoIBN1JlbGlhbmNlIG9uIG9yIHVzZSBvZiB0aGlzIENlcnRp +ZmljYXRlIGNyZWF0ZXMgYW4gYWNrbm93bGVkZ21lbnQgYW5kIGFjY2VwdGFuY2Ug +b2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFyZCB0ZXJtcyBhbmQgY29uZGl0 +aW9ucyBvZiB1c2UsIHRoZSBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVu +dCBhbmQgdGhlIFJlbHlpbmcgUGFydHkgQWdyZWVtZW50LCB3aGljaCBjYW4gYmUg +Zm91bmQgYXQgdGhlIGJlVFJVU1RlZCB3ZWIgc2l0ZSwgaHR0cHM6Ly93d3cuYmV0 +cnVzdGVkLmNvbS9wcm9kdWN0c19zZXJ2aWNlcy9pbmRleC5odG1sMEIGCCsGAQUF +BwIBFjZodHRwczovL3d3dy5iZXRydXN0ZWQuY29tL3Byb2R1Y3RzX3NlcnZpY2Vz +L2luZGV4Lmh0bWwwEQYJYIZIAYb4QgEBBAQDAgAHMIGJBgNVHR8EgYEwfzB9oHug +eaR3MHUxEjAQBgNVBAoTCWJlVFJVU1RlZDEbMBkGA1UECxMSYmVUUlVTVGVkIFJv +b3QgQ0FzMTMwMQYDVQQDEypiZVRSVVNUZWQgUm9vdCBDQSAtIEVudHJ1c3QgSW1w +bGVtZW50YXRpb24xDTALBgNVBAMTBENSTDEwKwYDVR0QBCQwIoAPMjAwMjA0MTEw +ODI0MjdagQ8yMDIyMDQxMTA4NTQyN1owCwYDVR0PBAQDAgEGMB8GA1UdIwQYMBaA +FH1w5a44iwY/qhwaj/nPJDCqhIQWMB0GA1UdDgQWBBR9cOWuOIsGP6ocGo/5zyQw +qoSEFjAMBgNVHRMEBTADAQH/MB0GCSqGSIb2fQdBAAQQMA4bCFY2LjA6NC4wAwIE +kDANBgkqhkiG9w0BAQUFAAOCAQEAKrgXzh8QlOu4mre5X+za95IkrNySO8cgjfKZ +5V04ocI07cUTWVwFtStPYZuR+0H8/NU8TZh2BvWBfevdkObRVlTa4y0MnxEylCIB +evZsLHRnBMylj44ss0O1lKLQfelifwa+JwGDnjr9iu6YQ0pr17WXOzq/T220Y/oz +ADQuLW2WyXvKmWO6vvT2MKAtmJbpVkQFqUSjYRDrgqFnXbxdJ3Wqiig2KjiS2d2k +XgClzMx8KSreKJCrt+G2/30lC0DYqjSjLd4H61/OCt3Kfjp9JsFiaDrmLzfzgYYh +xKlkqu9FNtEaZnz46TfW1mG+oq1I59/mdP7TbX3SJdysYlep9w== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFaDCCBFCgAwIBAgIQO1nHe81bV569N1KsdrSqGjANBgkqhkiG9w0BAQUFADBi +MRIwEAYDVQQKEwliZVRSVVNUZWQxGzAZBgNVBAsTEmJlVFJVU1RlZCBSb290IENB +czEvMC0GA1UEAxMmYmVUUlVTVGVkIFJvb3QgQ0EgLSBSU0EgSW1wbGVtZW50YXRp +b24wHhcNMDIwNDExMTExODEzWhcNMjIwNDEyMTEwNzI1WjBiMRIwEAYDVQQKEwli +ZVRSVVNUZWQxGzAZBgNVBAsTEmJlVFJVU1RlZCBSb290IENBczEvMC0GA1UEAxMm +YmVUUlVTVGVkIFJvb3QgQ0EgLSBSU0EgSW1wbGVtZW50YXRpb24wggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkujQwCY5X0LkGLG9uJIAiv11DpvpPrILn +HGhwhRujbrWqeNluB0s/6d/16uhUoWGKDi9pdRi3DOUUjXFumLhV/AyV0Jtu4S2I +1DpAa5LxmZZk3tv/ePTulh1HiXzUvrmIdyM6CeYEnm2qXtLIvZpOGd+J6lsOfsPk +tPDgaTuID0GQ+NRxQyTBjyZLO1bp/4xsN+lFrYWMU8NghpBKlsmzVLC7F/AcRdnU +GxlkVgoZ98zh/4avflherHqQH8koOUV7orbHnB/ahdQhhlkwk75TMzf270HPM8er +cmsl9fNTGwxMLvF1S++gh/f+ihXQbNXL+WhTuXAVE8L1LvtDNXUtAgMBAAGjggIY +MIICFDAMBgNVHRMEBTADAQH/MIIBtQYDVR0gBIIBrDCCAagwggGkBg8rBgEEAbE+ +AAADCSiDkTEwggGPMEEGCCsGAQUFBwIBFjVodHRwOi8vd3d3LmJldHJ1c3RlZC5j +b20vcHJvZHVjdHNfc2VydmljZXMvaW5kZXguaHRtbDCCAUgGCCsGAQUFBwICMIIB +OhqCATZSZWxpYW5jZSBvbiBvciB1c2Ugb2YgdGhpcyBDZXJ0aWZpY2F0ZSBjcmVh +dGVzIGFuIGFja25vd2xlZGdtZW50IGFuZCBhY2NlcHRhbmNlIG9mIHRoZSB0aGVu +IGFwcGxpY2FibGUgc3RhbmRhcmQgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YgdXNl +LCB0aGUgQ2VydGlmaWNhdGlvbiBQcmFjdGljZSBTdGF0ZW1lbnQgYW5kIHRoZSBS +ZWx5aW5nIFBhcnR5IEFncmVlbWVudCwgd2hpY2ggY2FuIGJlIGZvdW5kIGF0IHRo +ZSBiZVRSVVNUZWQgd2ViIHNpdGUsIGh0dHA6Ly93d3cuYmV0cnVzdGVkLmNvbS9w +cm9kdWN0c19zZXJ2aWNlcy9pbmRleC5odG1sMAsGA1UdDwQEAwIBBjAfBgNVHSME +GDAWgBSp7BR++dlDzFMrFK3P9/BZiUHNGTAdBgNVHQ4EFgQUqewUfvnZQ8xTKxSt +z/fwWYlBzRkwDQYJKoZIhvcNAQEFBQADggEBANuXsHXqDMTBmMpWBcCorSZIry0g +6IHHtt9DwSwddUvUQo3neqh03GZCWYez9Wlt2ames30cMcH1VOJZJEnl7r05pmuK +mET7m9cqg5c0Lcd9NUwtNLg+DcTsiCevnpL9UGGCqGAHFFPMZRPB9kdEadIxyKbd +LrML3kqNWz2rDcI1UqJWN8wyiyiFQpyRQHpwKzg21eFzGh/l+n5f3NacOzDq28Bb +J1zTcwfBwvNMm2+fG8oeqqg4MwlYsq78B+g23FW6L09A/nq9BqaBwZMifIYRCgZ3 +SK41ty8ymmFei74pnykkiFY5LKjSq5YDWtRIn7lAhAuYaPsBQ9Yb4gmxlxw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEvTCCA6WgAwIBAgIBADANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJFVTEn +MCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQL +ExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEiMCAGA1UEAxMZQ2hhbWJlcnMg +b2YgQ29tbWVyY2UgUm9vdDAeFw0wMzA5MzAxNjEzNDNaFw0zNzA5MzAxNjEzNDRa +MH8xCzAJBgNVBAYTAkVVMScwJQYDVQQKEx5BQyBDYW1lcmZpcm1hIFNBIENJRiBB +ODI3NDMyODcxIzAhBgNVBAsTGmh0dHA6Ly93d3cuY2hhbWJlcnNpZ24ub3JnMSIw +IAYDVQQDExlDaGFtYmVycyBvZiBDb21tZXJjZSBSb290MIIBIDANBgkqhkiG9w0B +AQEFAAOCAQ0AMIIBCAKCAQEAtzZV5aVdGDDg2olUkfzIx1L4L1DZ77F1c2VHfRtb +unXF/KGIJPov7coISjlUxFF6tdpg6jg8gbLL8bvZkSM/SAFwdakFKq0fcfPJVD0d +BmpAPrMMhe5cG3nCYsS4No41XQEMIwRHNaqbYE6gZj3LJgqcQKH0XZi/caulAGgq +7YN6D6IUtdQis4CwPAxaUWktWBiP7Zme8a7ileb2R6jWDA+wWFjbw2Y3npuRVDM3 +0pQcakjJyfKl2qUMI/cjDpwyVV5xnIQFUZot/eZOKjRa3spAN2cMVCFVd9oKDMyX +roDclDZK9D7ONhMeU+SsTjoF7Nuucpw4i9A5O4kKPnf+dQIBA6OCAUQwggFAMBIG +A1UdEwEB/wQIMAYBAf8CAQwwPAYDVR0fBDUwMzAxoC+gLYYraHR0cDovL2NybC5j +aGFtYmVyc2lnbi5vcmcvY2hhbWJlcnNyb290LmNybDAdBgNVHQ4EFgQU45T1sU3p +26EpW1eLTXYGduHRooowDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIA +BzAnBgNVHREEIDAegRxjaGFtYmVyc3Jvb3RAY2hhbWJlcnNpZ24ub3JnMCcGA1Ud +EgQgMB6BHGNoYW1iZXJzcm9vdEBjaGFtYmVyc2lnbi5vcmcwWAYDVR0gBFEwTzBN +BgsrBgEEAYGHLgoDATA+MDwGCCsGAQUFBwIBFjBodHRwOi8vY3BzLmNoYW1iZXJz +aWduLm9yZy9jcHMvY2hhbWJlcnNyb290Lmh0bWwwDQYJKoZIhvcNAQEFBQADggEB +AAxBl8IahsAifJ/7kPMa0QOx7xP5IV8EnNrJpY0nbJaHkb5BkAFyk+cefV/2icZd +p0AJPaxJRUXcLo0waLIJuvvDL8y6C98/d3tGfToSJI6WjzwFCm/SlCgdbQzALogi +1djPHRPH8EjX1wWnz8dHnjs8NMiAT9QUu/wNUPf6s+xCX6ndbcj0dc97wXImsQEc +XCz9ek60AcUFV7nnPKoF2YjpB0ZBzu9Bga5Y34OirsrXdx/nADydb47kMgkdTXg0 +eDQ8lJsm7U9xxhl6vSAiSFr+S30Dt+dYvsYyTnQeaN2oaFuzPu5ifdmA6Ap1erfu +tGWaIZDgqtCYvDi1czyL+Nw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIExTCCA62gAwIBAgIBADANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJFVTEn +MCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQL +ExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEgMB4GA1UEAxMXR2xvYmFsIENo +YW1iZXJzaWduIFJvb3QwHhcNMDMwOTMwMTYxNDE4WhcNMzcwOTMwMTYxNDE4WjB9 +MQswCQYDVQQGEwJFVTEnMCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgy +NzQzMjg3MSMwIQYDVQQLExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEgMB4G +A1UEAxMXR2xvYmFsIENoYW1iZXJzaWduIFJvb3QwggEgMA0GCSqGSIb3DQEBAQUA +A4IBDQAwggEIAoIBAQCicKLQn0KuWxfH2H3PFIP8T8mhtxOviteePgQKkotgVvq0 +Mi+ITaFgCPS3CU6gSS9J1tPfnZdan5QEcOw/Wdm3zGaLmFIoCQLfxS+EjXqXd7/s +QJ0lcqu1PzKY+7e3/HKE5TWH+VX6ox8Oby4o3Wmg2UIQxvi1RMLQQ3/bvOSiPGpV +eAp3qdjqGTK3L/5cPxvusZjsyq16aUXjlg9V9ubtdepl6DJWk0aJqCWKZQbua795 +B9Dxt6/tLE2Su8CoX6dnfQTyFQhwrJLWfQTSM/tMtgsL+xrJxI0DqX5c8lCrEqWh +z0hQpe/SyBoT+rB/sYIcd2oPX9wLlY/vQ37mRQklAgEDo4IBUDCCAUwwEgYDVR0T +AQH/BAgwBgEB/wIBDDA/BgNVHR8EODA2MDSgMqAwhi5odHRwOi8vY3JsLmNoYW1i +ZXJzaWduLm9yZy9jaGFtYmVyc2lnbnJvb3QuY3JsMB0GA1UdDgQWBBRDnDafsJ4w +TcbOX60Qq+UDpfqpFDAOBgNVHQ8BAf8EBAMCAQYwEQYJYIZIAYb4QgEBBAQDAgAH +MCoGA1UdEQQjMCGBH2NoYW1iZXJzaWducm9vdEBjaGFtYmVyc2lnbi5vcmcwKgYD +VR0SBCMwIYEfY2hhbWJlcnNpZ25yb290QGNoYW1iZXJzaWduLm9yZzBbBgNVHSAE +VDBSMFAGCysGAQQBgYcuCgEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly9jcHMuY2hh +bWJlcnNpZ24ub3JnL2Nwcy9jaGFtYmVyc2lnbnJvb3QuaHRtbDANBgkqhkiG9w0B +AQUFAAOCAQEAPDtwkfkEVCeR4e3t/mh/YV3lQWVPMvEYBZRqHN4fcNs+ezICNLUM +bKGKfKX0j//U2K0X1S0E0T9YgOKBWYi+wONGkyT+kL0mojAt6JcmVzWJdJYY9hXi +ryQZVgICsroPFOrGimbBhkVVi76SvpykBMdJPJ7oKXqJ1/6v/2j1pReQvayZzKWG +VwlnRtvWFsJG8eSpUPWP0ZIV018+xgBJOm5YstHRJw0lyDL4IBHNfTIzSJRUTN3c +ecQwn+uOuFW114hcxWokPbLTBQNRxgfvzBRydD1ucs4YKIxKoHflCStFREest2d/ +AYoFWpO+ocH/+OcOZ6RHSXZddZAa9SaP8A== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDkjCCAnqgAwIBAgIRAIW9S/PY2uNp9pTXX8OlRCMwDQYJKoZIhvcNAQEFBQAw +PTELMAkGA1UEBhMCRlIxETAPBgNVBAoTCENlcnRwbHVzMRswGQYDVQQDExJDbGFz +cyAyIFByaW1hcnkgQ0EwHhcNOTkwNzA3MTcwNTAwWhcNMTkwNzA2MjM1OTU5WjA9 +MQswCQYDVQQGEwJGUjERMA8GA1UEChMIQ2VydHBsdXMxGzAZBgNVBAMTEkNsYXNz +IDIgUHJpbWFyeSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANxQ +ltAS+DXSCHh6tlJw/W/uz7kRy1134ezpfgSN1sxvc0NXYKwzCkTsA18cgCSR5aiR +VhKC9+Ar9NuuYS6JEI1rbLqzAr3VNsVINyPi8Fo3UjMXEuLRYE2+L0ER4/YXJQyL +kcAbmXuZVg2v7tK8R1fjeUl7NIknJITesezpWE7+Tt9avkGtrAjFGA7v0lPubNCd +EgETjdyAYveVqUSISnFOYFWe2yMZeVYHDD9jC1yw4r5+FfyUM1hBOHTE4Y+L3yas +H7WLO7dDWWuwJKZtkIvEcupdM5i3y95ee++U8Rs+yskhwcWYAqqi9lt3m/V+llU0 +HGdpwPFC40es/CgcZlUCAwEAAaOBjDCBiTAPBgNVHRMECDAGAQH/AgEKMAsGA1Ud +DwQEAwIBBjAdBgNVHQ4EFgQU43Mt38sOKAze3bOkynm4jrvoMIkwEQYJYIZIAYb4 +QgEBBAQDAgEGMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly93d3cuY2VydHBsdXMu +Y29tL0NSTC9jbGFzczIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCnVM+IRBnL39R/ +AN9WM2K191EBkOvDP9GIROkkXe/nFL0gt5o8AP5tn9uQ3Nf0YtaLcF3n5QRIqWh8 +yfFC82x/xXp8HVGIutIKPidd3i1RTtMTZGnkLuPT55sJmabglZvOGtd/vjzOUrMR +FcEPF80Du5wlFbqidon8BvEY0JNLDnyCt6X09l/+7UCmnYR0ObncHoUW2ikbhiMA +ybuJfm6AiB4vFLQDJKgybwOaRywwvlbGp0ICcBvqQNi6BQNwB6SW//1IMwrh3KWB +kJtN3X3n57LNXMhqlfil9o3EXXgIvnsG1knPGTZQIy4I5p4FTUcY1Rbpsda2ENW7 +l7+ijrRU +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDDDCCAfSgAwIBAgIDAQAgMA0GCSqGSIb3DQEBBQUAMD4xCzAJBgNVBAYTAlBM +MRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD +QTAeFw0wMjA2MTExMDQ2MzlaFw0yNzA2MTExMDQ2MzlaMD4xCzAJBgNVBAYTAlBM +MRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD +QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM6xwS7TT3zNJc4YPk/E +jG+AanPIW1H4m9LcuwBcsaD8dQPugfCI7iNS6eYVM42sLQnFdvkrOYCJ5JdLkKWo +ePhzQ3ukYbDYWMzhbGZ+nPMJXlVjhNWo7/OxLjBos8Q82KxujZlakE403Daaj4GI +ULdtlkIJ89eVgw1BS7Bqa/j8D35in2fE7SZfECYPCE/wpFcozo+47UX2bu4lXapu +Ob7kky/ZR6By6/qmW6/KUz/iDsaWVhFu9+lmqSbYf5VT7QqFiLpPKaVCjF62/IUg +AKpoC6EahQGcxEZjgoi2IrHu/qpGWX7PNSzVttpd90gzFFS269lvzs2I1qsb2pY7 +HVkCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEA +uI3O7+cUus/usESSbLQ5PqKEbq24IXfS1HeCh+YgQYHu4vgRt2PRFze+GXYkHAQa +TOs9qmdvLdTN/mUxcMUbpgIKumB7bVjCmkn+YzILa+M6wKyrO7Do0wlRjBCDxjTg +xSvgGrZgFCdsMneMvLJymM/NzD+5yCRCFNZX/OYmQ6kd5YCQzgNUKD73P9P4Te1q +CjqTE5s7FCMTY5w/0YcneeVMUeMBrYVdGjux1XMQpNPyvG5k9VpWkKjHDkx0Dy5x +O/fIR/RpbxXyEV6DHpx8Uq79AtoSqFlnGNu8cN2bsWntgM6JQEhqDjXKKWYVIZQs +6GAqm4VKQPNriiTsBhYscw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj +YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM +GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua +BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe +3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 +YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR +rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm +ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU +oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v +QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t +b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF +AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q +GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 +G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi +l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 +smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB +gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV +BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw +MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl +YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P +RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3 +UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI +2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8 +Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp ++2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+ +DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O +nKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW +/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g +PKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u +QXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY +SdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv +IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ +RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4 +zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd +BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB +ZQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEPzCCAyegAwIBAgIBATANBgkqhkiG9w0BAQUFADB+MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEkMCIGA1UEAwwbU2VjdXJlIENlcnRp +ZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVow +fjELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxJDAiBgNV +BAMMG1NlY3VyZSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAMBxM4KK0HDrc4eCQNUd5MvJDkKQ+d40uaG6EfQlhfPM +cm3ye5drswfxdySRXyWP9nQ95IDC+DwN879A6vfIUtFyb+/Iq0G4bi4XKpVpDM3S +HpR7LZQdqnXXs5jLrLxkU0C8j6ysNstcrbvd4JQX7NFc0L/vpZXJkMWwrPsbQ996 +CF23uPJAGysnnlDOXmWCiIxe004MeuoIkbY2qitC++rCoznl2yY4rYsK7hljxxwk +3wN42ubqwUcaCwtGCd0C/N7Lh1/XMGNooa7cMqG6vv5Eq2i2pRcV/b3Vp6ea5EQz +6YiO/O1R65NxTq0B50SOqy3LqP4BSUjwwN3HaNiS/j0CAwEAAaOBxzCBxDAdBgNV +HQ4EFgQUPNiTiMLAggnMAZkGkyDpnnAJY08wDgYDVR0PAQH/BAQDAgEGMA8GA1Ud +EwEB/wQFMAMBAf8wgYEGA1UdHwR6MHgwO6A5oDeGNWh0dHA6Ly9jcmwuY29tb2Rv +Y2EuY29tL1NlY3VyZUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDmgN6A1hjNodHRw +Oi8vY3JsLmNvbW9kby5uZXQvU2VjdXJlQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmww +DQYJKoZIhvcNAQEFBQADggEBAIcBbSMdflsXfcFhMs+P5/OKlFlm4J4oqF7Tt/Q0 +5qo5spcWxYJvMqTpjOev/e/C6LlLqqP05tqNZSH7uoDrJiiFGv45jN5bBAS0VPmj +Z55B+glSzAVIqMk/IQQezkhr/IXownuvf7fM+F86/TXGDe+X3EyrEeFryzHRbPtI +gKvcnDe4IRRLDXE97IMzbtFuMhbsmMcWi1mmNKsFVy2T96oTy9IT4rcuO81rUBcJ +aD61JlfutuC23bkpgHl9j6PwpCikFcSF9CfUa7/lXORlAnZUtOM3ZiTTGWHIUhDl +izeauan5Hb/qmZJhlv8BzaFfDbxxvA6sCx1HRR3B7Hzs/Sk= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIBATANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDElMCMGA1UEAwwcVHJ1c3RlZCBDZXJ0 +aWZpY2F0ZSBTZXJ2aWNlczAeFw0wNDAxMDEwMDAwMDBaFw0yODEyMzEyMzU5NTla +MH8xCzAJBgNVBAYTAkdCMRswGQYDVQQIDBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO +BgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoMEUNvbW9kbyBDQSBMaW1pdGVkMSUwIwYD +VQQDDBxUcnVzdGVkIENlcnRpZmljYXRlIFNlcnZpY2VzMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA33FvNlhTWvI2VFeAxHQIIO0Yfyod5jWaHiWsnOWW +fnJSoBVC21ndZHoa0Lh73TkVvFVIxO06AOoxEbrycXQaZ7jPM8yoMa+j49d/vzMt +TGo87IvDktJTdyR0nAducPy9C1t2ul/y/9c3S0pgePfw+spwtOpZqqPOSC+pw7IL +fhdyFgymBwwbOM/JYrc/oJOlh0Hyt3BAd9i+FHzjqMB6juljatEPmsbS9Is6FARW +1O24zG71++IsWL1/T2sr92AkWCTOJu80kTrV44HQsvAEAtdbtz6SrGsSivnkBbA7 +kUlcsutT6vifR4buv5XAwAaf0lteERv0xwQ1KdJVXOTt6wIDAQABo4HJMIHGMB0G +A1UdDgQWBBTFe1i97doladL3WRaoszLAeydb9DAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zCBgwYDVR0fBHwwejA8oDqgOIY2aHR0cDovL2NybC5jb21v +ZG9jYS5jb20vVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMuY3JsMDqgOKA2hjRo +dHRwOi8vY3JsLmNvbW9kby5uZXQvVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMu +Y3JsMA0GCSqGSIb3DQEBBQUAA4IBAQDIk4E7ibSvuIQSTI3S8NtwuleGFTQQuS9/ +HrCoiWChisJ3DFBKmwCL2Iv0QeLQg4pKHBQGsKNoBXAxMKdTmw7pSqBYaWcOrp32 +pSxBvzwGa+RZzG0Q8ZZvH9/0BAKkn0U+yNj6NkZEUD+Cl5EfKNsYEYwq5GWDVxIS +jBc/lDb+XbDABHcTuPQV1T84zJQ6VdCsmPW6AF/ghhmBeC8owH7TzEIK9a5QoNE+ +xqFx7D+gIIxmOom0jtTYsU0lR+4viMi14QVFwL4Ucd56/Y57fU0IlqUSc/Atyjcn +dBInTMu2l+nZrghtWjlA3QVHdWpaIbOjGM9O9y5Xt5hwXsjEeLBi +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c +JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP +mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ +wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 +VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ +AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB +AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun +pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC +dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf +fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm +NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx +H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDKTCCApKgAwIBAgIENnAVljANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJV +UzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMREwDwYDVQQL +EwhEU1RDQSBFMTAeFw05ODEyMTAxODEwMjNaFw0xODEyMTAxODQwMjNaMEYxCzAJ +BgNVBAYTAlVTMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4x +ETAPBgNVBAsTCERTVENBIEUxMIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQCg +bIGpzzQeJN3+hijM3oMv+V7UQtLodGBmE5gGHKlREmlvMVW5SXIACH7TpWJENySZ +j9mDSI+ZbZUTu0M7LklOiDfBu1h//uG9+LthzfNHwJmm8fOR6Hh8AMthyUQncWlV +Sn5JTe2io74CTADKAqjuAQIxZA9SLRN0dja1erQtcQIBA6OCASQwggEgMBEGCWCG +SAGG+EIBAQQEAwIABzBoBgNVHR8EYTBfMF2gW6BZpFcwVTELMAkGA1UEBhMCVVMx +JDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0dXJlIFRydXN0IENvLjERMA8GA1UECxMI +RFNUQ0EgRTExDTALBgNVBAMTBENSTDEwKwYDVR0QBCQwIoAPMTk5ODEyMTAxODEw +MjNagQ8yMDE4MTIxMDE4MTAyM1owCwYDVR0PBAQDAgEGMB8GA1UdIwQYMBaAFGp5 +fpFpRhgTCgJ3pVlbYJglDqL4MB0GA1UdDgQWBBRqeX6RaUYYEwoCd6VZW2CYJQ6i ++DAMBgNVHRMEBTADAQH/MBkGCSqGSIb2fQdBAAQMMAobBFY0LjADAgSQMA0GCSqG +SIb3DQEBBQUAA4GBACIS2Hod3IEGtgllsofIH160L+nEHvI8wbsEkBFKg05+k7lN +QseSJqBcNJo4cvj9axY+IO6CizEqkzaFI4iKPANo08kJD038bKTaKHKTDomAsH3+ +gG9lbRgzl4vCa4nuYD3Im+9/KzJic5PLPON74nZ4RbyhkwS7hp86W0N6w4pl +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID2DCCAsACEQDQHkCLAAACfAAAAAIAAAABMA0GCSqGSIb3DQEBBQUAMIGpMQsw +CQYDVQQGEwJ1czENMAsGA1UECBMEVXRhaDEXMBUGA1UEBxMOU2FsdCBMYWtlIENp +dHkxJDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0dXJlIFRydXN0IENvLjERMA8GA1UE +CxMIRFNUQ0EgWDExFjAUBgNVBAMTDURTVCBSb290Q0EgWDExITAfBgkqhkiG9w0B +CQEWEmNhQGRpZ3NpZ3RydXN0LmNvbTAeFw05ODEyMDExODE4NTVaFw0wODExMjgx +ODE4NTVaMIGpMQswCQYDVQQGEwJ1czENMAsGA1UECBMEVXRhaDEXMBUGA1UEBxMO +U2FsdCBMYWtlIENpdHkxJDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0dXJlIFRydXN0 +IENvLjERMA8GA1UECxMIRFNUQ0EgWDExFjAUBgNVBAMTDURTVCBSb290Q0EgWDEx +ITAfBgkqhkiG9w0BCQEWEmNhQGRpZ3NpZ3RydXN0LmNvbTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBANLGJrbnpT3BxGjVUG9TxW9JEwm4ryxIjRRqoxdf +WvnTLnUv2Chi0ZMv/E3Uq4flCMeZ55I/db3rJbQVwZsZPdJEjdd0IG03Ao9pk1uK +xBmd9LIO/BZsubEFkoPRhSxglD5FVaDZqwgh5mDoO3TymVBRaNADLbGAvqPYUrBE +zUNKcI5YhZXhTizWLUFv1oTnyJhEykfbLCSlaSbPa7gnYsP0yXqSI+0TZ4KuRS5F +5X5yP4WdlGIQ5jyRoa13AOAV7POEgHJ6jm5gl8ckWRA0g1vhpaRptlc1HHhZxtMv +OnNn7pTKBBMFYgZwI7P0fO5F2WQLW0mqpEPOJsREEmy43XkCAwEAATANBgkqhkiG +9w0BAQUFAAOCAQEAojeyP2n714Z5VEkxlTMr89EJFEliYIalsBHiUMIdBlc+Legz +ZL6bqq1fG03UmZWii5rJYnK1aerZWKs17RWiQ9a2vAd5ZWRzfdd5ynvVWlHG4VME +lo04z6MXrDlxawHDi1M8Y+nuecDkvpIyZHqzH5eUYr3qsiAVlfuX8ngvYzZAOONG +Dx3drJXK50uQe7FLqdTF65raqtWjlBRGjS0f8zrWkzr2Pnn86Oawde3uPclwx12q +gUtGJRzHbBXjlU4PqjI3lAoXJJIThFjSY28r9+ZbYgsTF7ANUkz+/m9c4pFuHf2k +Ytdo+o56T9II2pPc8JIRetDccpMMc5NihWjQ9A== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDKTCCApKgAwIBAgIENm7TzjANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJV +UzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMREwDwYDVQQL +EwhEU1RDQSBFMjAeFw05ODEyMDkxOTE3MjZaFw0xODEyMDkxOTQ3MjZaMEYxCzAJ +BgNVBAYTAlVTMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4x +ETAPBgNVBAsTCERTVENBIEUyMIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQC/ +k48Xku8zExjrEH9OFr//Bo8qhbxe+SSmJIi2A7fBw18DW9Fvrn5C6mYjuGODVvso +LeE4i7TuqAHhzhy2iCoiRoX7n6dwqUcUP87eZfCocfdPJmyMvMa1795JJ/9IKn3o +TQPMx7JSxhcxEzu1TdvIxPbDDyQq2gyd55FbgM2UnQIBA6OCASQwggEgMBEGCWCG +SAGG+EIBAQQEAwIABzBoBgNVHR8EYTBfMF2gW6BZpFcwVTELMAkGA1UEBhMCVVMx +JDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0dXJlIFRydXN0IENvLjERMA8GA1UECxMI +RFNUQ0EgRTIxDTALBgNVBAMTBENSTDEwKwYDVR0QBCQwIoAPMTk5ODEyMDkxOTE3 +MjZagQ8yMDE4MTIwOTE5MTcyNlowCwYDVR0PBAQDAgEGMB8GA1UdIwQYMBaAFB6C +TShlgDzJQW6sNS5ay97u+DlbMB0GA1UdDgQWBBQegk0oZYA8yUFurDUuWsve7vg5 +WzAMBgNVHRMEBTADAQH/MBkGCSqGSIb2fQdBAAQMMAobBFY0LjADAgSQMA0GCSqG +SIb3DQEBBQUAA4GBAEeNg61i8tuwnkUiBbmi1gMOOHLnnvx75pO2mqWilMg0HZHR +xdf0CiUPPXiBng+xZ8SQTGPdXqfiup/1902lMXucKS1M/mQ+7LZT/uqb7YLbdHVL +B3luHtgZg3Pe9T7Qtd7nS2h9Qy4qIOF+oHhEngj1mPnHfxsb1gYgAlihw6ID +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID2DCCAsACEQDQHkCLAAB3bQAAAAEAAAAEMA0GCSqGSIb3DQEBBQUAMIGpMQsw +CQYDVQQGEwJ1czENMAsGA1UECBMEVXRhaDEXMBUGA1UEBxMOU2FsdCBMYWtlIENp +dHkxJDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0dXJlIFRydXN0IENvLjERMA8GA1UE +CxMIRFNUQ0EgWDIxFjAUBgNVBAMTDURTVCBSb290Q0EgWDIxITAfBgkqhkiG9w0B +CQEWEmNhQGRpZ3NpZ3RydXN0LmNvbTAeFw05ODExMzAyMjQ2MTZaFw0wODExMjcy +MjQ2MTZaMIGpMQswCQYDVQQGEwJ1czENMAsGA1UECBMEVXRhaDEXMBUGA1UEBxMO +U2FsdCBMYWtlIENpdHkxJDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0dXJlIFRydXN0 +IENvLjERMA8GA1UECxMIRFNUQ0EgWDIxFjAUBgNVBAMTDURTVCBSb290Q0EgWDIx +ITAfBgkqhkiG9w0BCQEWEmNhQGRpZ3NpZ3RydXN0LmNvbTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBANx18IzAdZaawGIfJvfE4Zrq4FZzW5nNAUSoCLbV +p9oaBBg5kkp4o4HC9Xd6ULRw/5qrxsfKboNPQpj7Jgva3G3WqZlVUmfpKAOS3OWw +BZoPFflrWXJW8vo5/Kpo7g8fEIMv/J36F5bdguPmRX3AS4BEH+0s4IT9kVySVGkl +5WJp3OXuAFK9MwutdQKFp2RQLcUZGTDAJtvJ0/0uma1ZtQtN1EGuhUhDWdy3qOKi +3sOP17ihYqZoUFLkzzGnlIXan0YyF1bl8utmPRL/Q9uY73fPy4GNNLHGUEom0eQ+ +QVCvbK4iNC7Va26Dunm4dmVI2gkpZGMiuftHdoWMhkTLCdsCAwEAATANBgkqhkiG +9w0BAQUFAAOCAQEAtTYOXeFhKFoRZcA/gwN5Tb4opgsHAlKFzfiR0BBstWogWxyQ +2TA8xkieil5k+aFxd+8EJx8H6+Qm93N0yUQYGmbT4EOvkTvRyyzYdFQ6HE3K1GjN +I3wdEJ5F6fYAbqbNGf9PLCmPV03Ed5K+4EwJ+11EhmYhqLkyolbV6YyDfFk/xPEL +553snr2cGA4+wjl5KLcDDQjLxufZATdQEOzMYRZA1K8xdHv8PzGn0EdzMzkbzE5q +10mDEQb+64JYMzJM8FasHpwvVpp7wUocpf1VNs78lk30sPDst2yC7S8xmUJMqbIN +uBVd8d+6ybVK1GSYsyapMMj9puyrliGtf8J4tg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgIQDV6ZCtadt3js2AdWO4YV2TANBgkqhkiG9w0BAQUFADBb +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3Qx +ETAPBgNVBAsTCERTVCBBQ0VTMRcwFQYDVQQDEw5EU1QgQUNFUyBDQSBYNjAeFw0w +MzExMjAyMTE5NThaFw0xNzExMjAyMTE5NThaMFsxCzAJBgNVBAYTAlVTMSAwHgYD +VQQKExdEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdDERMA8GA1UECxMIRFNUIEFDRVMx +FzAVBgNVBAMTDkRTVCBBQ0VTIENBIFg2MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAuT31LMmU3HWKlV1j6IR3dma5WZFcRt2SPp/5DgO0PWGSvSMmtWPu +ktKe1jzIDZBfZIGxqAgNTNj50wUoUrQBJcWVHAx+PhCEdc/BGZFjz+iokYi5Q1K7 +gLFViYsx+tC3dr5BPTCapCIlF3PoHuLTrCq9Wzgh1SpL11V94zpVvddtawJXa+ZH +fAjIgrrep4c9oW24MFbCswKBXy314powGCi4ZtPLAZZv6opFVdbgnf9nKxcCpk4a +ahELfrd755jWjHZvwTvbUJN+5dCOHze4vbrGn2zpfDPyMjwmR/onJALJfh1biEIT +ajV8fTXpLmaRcpPVMibEdPVTo7NdmvYJywIDAQABo4HIMIHFMA8GA1UdEwEB/wQF +MAMBAf8wDgYDVR0PAQH/BAQDAgHGMB8GA1UdEQQYMBaBFHBraS1vcHNAdHJ1c3Rk +c3QuY29tMGIGA1UdIARbMFkwVwYKYIZIAWUDAgEBATBJMEcGCCsGAQUFBwIBFjto +dHRwOi8vd3d3LnRydXN0ZHN0LmNvbS9jZXJ0aWZpY2F0ZXMvcG9saWN5L0FDRVMt +aW5kZXguaHRtbDAdBgNVHQ4EFgQUCXIGThhDD+XWzMNqizF7eI+og7gwDQYJKoZI +hvcNAQEFBQADggEBAKPYjtay284F5zLNAdMEA+V25FYrnJmQ6AgwbN99Pe7lv7Uk +QIRJ4dEorsTCOlMwiPH1d25Ryvr/ma8kXxug/fKshMrfqfBfBC6tFr8hlxCBPeP/ +h40y3JTlR4peahPJlJU90u7INJXQgNStMgiAVDzgvVJT11J8smk/f3rPanTK+gQq +nExaBqXpIK1FZg9p8d2/6eMyi/rgwYZNcjwu2JN4Cir42NInPRmJX1p7ijvMDNpR +rscL9yuwNwXsvFcj4jjSm2jzVhKIT0J8uDHEtdvkyCE06UgRNe76x5JXxZ805Mf2 +9w4LTJxoeHtxMcfrHuBnQfO3oKfN5XozNmr6mis= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow +PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD +Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O +rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq +OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b +xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw +7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD +aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG +SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69 +ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr +AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz +R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 +JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo +Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEgzCCA+ygAwIBAgIEOJ725DANBgkqhkiG9w0BAQQFADCBtDEUMBIGA1UEChML +RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9HQ0NBX0NQUyBp +bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAyMDAw +IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENsaWVu +dCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMDAyMDcxNjE2NDBaFw0yMDAy +MDcxNjQ2NDBaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3 +LmVudHJ1c3QubmV0L0dDQ0FfQ1BTIGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp +YWIuKTElMCMGA1UECxMcKGMpIDIwMDAgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG +A1UEAxMqRW50cnVzdC5uZXQgQ2xpZW50IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCTdLS25MVL1qFof2LV7PdRV7Ny +Spj10InJrWPNTTVRaoTUrcloeW+46xHbh65cJFET8VQlhK8pK5/jgOLZy93GRUk0 +iJBeAZfv6lOm3fzB3ksqJeTpNfpVBQbliXrqpBFXO/x8PTbNZzVtpKklWb1m9fkn +5JVn1j+SgF7yNH0rhQIDAQABo4IBnjCCAZowEQYJYIZIAYb4QgEBBAQDAgAHMIHd +BgNVHR8EgdUwgdIwgc+ggcyggcmkgcYwgcMxFDASBgNVBAoTC0VudHJ1c3QubmV0 +MUAwPgYDVQQLFDd3d3cuZW50cnVzdC5uZXQvR0NDQV9DUFMgaW5jb3JwLiBieSBy +ZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMjAwMCBFbnRydXN0Lm5l +dCBMaW1pdGVkMTMwMQYDVQQDEypFbnRydXN0Lm5ldCBDbGllbnQgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkxDTALBgNVBAMTBENSTDEwKwYDVR0QBCQwIoAPMjAwMDAy +MDcxNjE2NDBagQ8yMDIwMDIwNzE2NDY0MFowCwYDVR0PBAQDAgEGMB8GA1UdIwQY +MBaAFISLdP3FjcD/J20gN0V8/i3OutN9MB0GA1UdDgQWBBSEi3T9xY3A/ydtIDdF +fP4tzrrTfTAMBgNVHRMEBTADAQH/MB0GCSqGSIb2fQdBAAQQMA4bCFY1LjA6NC4w +AwIEkDANBgkqhkiG9w0BAQQFAAOBgQBObzWAO9GK9Q6nIMstZVXQkvTnhLUGJoMS +hAusO7JE7r3PQNsgDrpuFOow4DtifH+La3xKp9U1PL6oXOpLu5OOgGarDyn9TS2/ +GpsKkMWr2tGzhtQvJFJcem3G8v7lTRowjJDyutdKPkN+1MhQGof4T4HHdguEOnKd +zmVml64mXg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIElTCCA/6gAwIBAgIEOJsRPDANBgkqhkiG9w0BAQQFADCBujEUMBIGA1UEChML +RW50cnVzdC5uZXQxPzA9BgNVBAsUNnd3dy5lbnRydXN0Lm5ldC9TU0xfQ1BTIGlu +Y29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMcKGMpIDIwMDAg +RW50cnVzdC5uZXQgTGltaXRlZDE6MDgGA1UEAxMxRW50cnVzdC5uZXQgU2VjdXJl +IFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMDAyMDQxNzIwMDBa +Fw0yMDAyMDQxNzUwMDBaMIG6MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDE/MD0GA1UE +CxQ2d3d3LmVudHJ1c3QubmV0L1NTTF9DUFMgaW5jb3JwLiBieSByZWYuIChsaW1p +dHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMjAwMCBFbnRydXN0Lm5ldCBMaW1pdGVk +MTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENlcnRpZmljYXRp +b24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDHwV9OcfHO +8GCGD9JYf9Mzly0XonUwtZZkJi9ow0SrqHXmAGc0V55lxyKbc+bT3QgON1WqJUaB +bL3+qPZ1V1eMkGxKwz6LS0MKyRFWmponIpnPVZ5h2QLifLZ8OAfc439PmrkDQYC2 +dWcTC5/oVzbIXQA23mYU2m52H083jIITiQIDAQABo4IBpDCCAaAwEQYJYIZIAYb4 +QgEBBAQDAgAHMIHjBgNVHR8EgdswgdgwgdWggdKggc+kgcwwgckxFDASBgNVBAoT +C0VudHJ1c3QubmV0MT8wPQYDVQQLFDZ3d3cuZW50cnVzdC5uZXQvU1NMX0NQUyBp +bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAyMDAw +IEVudHJ1c3QubmV0IExpbWl0ZWQxOjA4BgNVBAMTMUVudHJ1c3QubmV0IFNlY3Vy +ZSBTZXJ2ZXIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxDTALBgNVBAMTBENSTDEw +KwYDVR0QBCQwIoAPMjAwMDAyMDQxNzIwMDBagQ8yMDIwMDIwNDE3NTAwMFowCwYD +VR0PBAQDAgEGMB8GA1UdIwQYMBaAFMtswGvjuz7L/CKc/vuLkpyw8m4iMB0GA1Ud +DgQWBBTLbMBr47s+y/winP77i5KcsPJuIjAMBgNVHRMEBTADAQH/MB0GCSqGSIb2 +fQdBAAQQMA4bCFY1LjA6NC4wAwIEkDANBgkqhkiG9w0BAQQFAAOBgQBi24GRzsia +d0Iv7L0no1MPUBvqTpLwqa+poLpIYcvvyQbvH9X07t9WLebKahlzqlO+krNQAraF +JnJj2HVQYnUUt7NQGj/KEQALhUVpbbalrlHhStyCP2yMNLJ3a9kC9n8O6mUE8c1U +yrrJzOCE98g+EZfTYAkYvAX/bIkz8OwVDw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEXDCCA0SgAwIBAgIEOGO5ZjANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML +RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp +bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5 +IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0xOTEy +MjQxODIwNTFaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3 +LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp +YWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG +A1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq +K0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe +sYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX +MlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT +XTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/ +HoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH +4QIDAQABo3QwcjARBglghkgBhvhCAQEEBAMCAAcwHwYDVR0jBBgwFoAUVeSB0RGA +vtiJuQijMfmhJAkWuXAwHQYDVR0OBBYEFFXkgdERgL7YibkIozH5oSQJFrlwMB0G +CSqGSIb2fQdBAAQQMA4bCFY1LjA6NC4wAwIEkDANBgkqhkiG9w0BAQUFAAOCAQEA +WUesIYSKF8mciVMeuoCFGsY8Tj6xnLZ8xpJdGGQC49MGCBFhfGPjK50xA3B20qMo +oPS7mmNz7W3lKtvtFKkrxjYR0CvrB4ul2p5cGZ1WEvVUKcgF7bISKo30Axv/55IQ +h7A6tcOdBTcSo8f0FbnVpDkWm1M6I5HxqIKiaohowXkCIryqptau37AUX7iH0N18 +f3v/rxzP5tsHrV7bhZ3QKw0z2wTR5klAEyt2+z7pnIkPFc4YsIV4IU9rTw76NmfN +B/L/CNDi3tm/Kq+4h4YhPATKt5Rof8886ZjXOP/swNlQ8C5LWK5Gb9Auw2DaclVy +vUxFnmG6v4SBkgPR0ml8xQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIE7TCCBFagAwIBAgIEOAOR7jANBgkqhkiG9w0BAQQFADCByTELMAkGA1UEBhMC +VVMxFDASBgNVBAoTC0VudHJ1c3QubmV0MUgwRgYDVQQLFD93d3cuZW50cnVzdC5u +ZXQvQ2xpZW50X0NBX0luZm8vQ1BTIGluY29ycC4gYnkgcmVmLiBsaW1pdHMgbGlh +Yi4xJTAjBgNVBAsTHChjKSAxOTk5IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNV +BAMTKkVudHJ1c3QubmV0IENsaWVudCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw05OTEwMTIxOTI0MzBaFw0xOTEwMTIxOTU0MzBaMIHJMQswCQYDVQQGEwJVUzEU +MBIGA1UEChMLRW50cnVzdC5uZXQxSDBGBgNVBAsUP3d3dy5lbnRydXN0Lm5ldC9D +bGllbnRfQ0FfSW5mby9DUFMgaW5jb3JwLiBieSByZWYuIGxpbWl0cyBsaWFiLjEl +MCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEGA1UEAxMq +RW50cnVzdC5uZXQgQ2xpZW50IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGdMA0G +CSqGSIb3DQEBAQUAA4GLADCBhwKBgQDIOpleMRffrCdvkHvkGf9FozTC28GoT/Bo +6oT9n3V5z8GKUZSvx1cDR2SerYIbWtp/N3hHuzeYEpbOxhN979IMMFGpOZ5V+Pux +5zDeg7K6PvHViTs7hbqqdCz+PzFur5GVbgbUB01LLFZHGARS2g4Qk79jkJvh34zm +AqTmT173iwIBA6OCAeAwggHcMBEGCWCGSAGG+EIBAQQEAwIABzCCASIGA1UdHwSC +ARkwggEVMIHkoIHhoIHepIHbMIHYMQswCQYDVQQGEwJVUzEUMBIGA1UEChMLRW50 +cnVzdC5uZXQxSDBGBgNVBAsUP3d3dy5lbnRydXN0Lm5ldC9DbGllbnRfQ0FfSW5m +by9DUFMgaW5jb3JwLiBieSByZWYuIGxpbWl0cyBsaWFiLjElMCMGA1UECxMcKGMp +IDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEGA1UEAxMqRW50cnVzdC5uZXQg +Q2xpZW50IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMCyg +KqAohiZodHRwOi8vd3d3LmVudHJ1c3QubmV0L0NSTC9DbGllbnQxLmNybDArBgNV +HRAEJDAigA8xOTk5MTAxMjE5MjQzMFqBDzIwMTkxMDEyMTkyNDMwWjALBgNVHQ8E +BAMCAQYwHwYDVR0jBBgwFoAUxPucKXuXzUyW/O5bs8qZdIuV6kwwHQYDVR0OBBYE +FMT7nCl7l81MlvzuW7PKmXSLlepMMAwGA1UdEwQFMAMBAf8wGQYJKoZIhvZ9B0EA +BAwwChsEVjQuMAMCBJAwDQYJKoZIhvcNAQEEBQADgYEAP66K8ddmAwWePvrqHEa7 +pFuPeJoSSJn59DXeDDYHAmsQOokUgZwxpnyyQbJq5wcBoUv5nyU7lsqZwz6hURzz +wy5E97BnRqqS5TvaHBkUODDV4qIxJS7x7EU47fgGWANzYrAQMY9Av2TgXD7FTx/a +EkP/TOYGJqibGapEPHayXOw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIE2DCCBEGgAwIBAgIEN0rSQzANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMC +VVMxFDASBgNVBAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50cnVzdC5u +ZXQvQ1BTIGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMc +KGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDE6MDgGA1UEAxMxRW50cnVzdC5u +ZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw05OTA1 +MjUxNjA5NDBaFw0xOTA1MjUxNjM5NDBaMIHDMQswCQYDVQQGEwJVUzEUMBIGA1UE +ChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5j +b3JwLiBieSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBF +bnRydXN0Lm5ldCBMaW1pdGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUg +U2VydmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGdMA0GCSqGSIb3DQEBAQUA +A4GLADCBhwKBgQDNKIM0VBuJ8w+vN5Ex/68xYMmo6LIQaO2f55M28Qpku0f1BBc/ +I0dNxScZgSYMVHINiC3ZH5oSn7yzcdOAGT9HZnuMNSjSuQrfJNqc1lB5gXpa0zf3 +wkrYKZImZNHkmGw6AIr1NJtl+O3jEP/9uElY3KDegjlrgbEWGWG5VLbmQwIBA6OC +AdcwggHTMBEGCWCGSAGG+EIBAQQEAwIABzCCARkGA1UdHwSCARAwggEMMIHeoIHb +oIHYpIHVMIHSMQswCQYDVQQGEwJVUzEUMBIGA1UEChMLRW50cnVzdC5uZXQxOzA5 +BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5jb3JwLiBieSByZWYuIChsaW1p +dHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBFbnRydXN0Lm5ldCBMaW1pdGVk +MTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENlcnRpZmljYXRp +b24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMCmgJ6AlhiNodHRwOi8vd3d3LmVu +dHJ1c3QubmV0L0NSTC9uZXQxLmNybDArBgNVHRAEJDAigA8xOTk5MDUyNTE2MDk0 +MFqBDzIwMTkwNTI1MTYwOTQwWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAU8Bdi +E1U9s/8KAGv7UISX8+1i0BowHQYDVR0OBBYEFPAXYhNVPbP/CgBr+1CEl/PtYtAa +MAwGA1UdEwQFMAMBAf8wGQYJKoZIhvZ9B0EABAwwChsEVjQuMAMCBJAwDQYJKoZI +hvcNAQEFBQADgYEAkNwwAvpkdMKnCqV8IY00F6j7Rw7/JXyNEwr75Ji174z4xRAN +95K+8cPV1ZVqBLssziY2ZcgxxufuP+NXdYR6Ee9GTxj005i7qIcyunL2POI9n9cd +2cNgQ4xYDiKWL2KjLB+6rQXvqzJ4h6BUcxm1XAX5Uj5tLUUL9wqT6u0G+bI= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 +Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW +KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw +NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw +NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy +ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV +BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo +Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 +4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 +KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI +rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi +94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB +sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi +gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo +kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE +vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t +O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua +AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP +9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ +eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m +0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV +UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy +dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1 +MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0VxdWlmYXgxLTArBgNVBAsTJEVx +dWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPRfM6f +BeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+A +cJkVV5MW8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kC +AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQ +MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw +ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj +IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF +MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA +A4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y +7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh +1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICgjCCAeugAwIBAgIBBDANBgkqhkiG9w0BAQQFADBTMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBT +ZWN1cmUgZUJ1c2luZXNzIENBLTEwHhcNOTkwNjIxMDQwMDAwWhcNMjAwNjIxMDQw +MDAwWjBTMQswCQYDVQQGEwJVUzEcMBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5j +LjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNzIENBLTEwgZ8wDQYJ +KoZIhvcNAQEBBQADgY0AMIGJAoGBAM4vGbwXt3fek6lfWg0XTzQaDJj0ItlZ1MRo +RvC0NcWFAyDGr0WlIVFFQesWWDYyb+JQYmT5/VGcqiTZ9J2DKocKIdMSODRsjQBu +WqDZQu4aIZX5UkxVWsUPOE9G+m34LjXWHXzr4vCwdYDIqROsvojvOm6rXyo4YgKw +Env+j6YDAgMBAAGjZjBkMBEGCWCGSAGG+EIBAQQEAwIABzAPBgNVHRMBAf8EBTAD +AQH/MB8GA1UdIwQYMBaAFEp4MlIR21kWNl7fwRQ2QGpHfEyhMB0GA1UdDgQWBBRK +eDJSEdtZFjZe38EUNkBqR3xMoTANBgkqhkiG9w0BAQQFAAOBgQB1W6ibAxHm6VZM +zfmpTMANmvPMZWnmJXbMWbfWVMMdzZmsGd20hdXgPfxiIKeES1hl8eL5lSE/9dR+ +WB5Hh1Q+WKG1tfgq73HnvMP2sUlG4tega+VWeponmHxGYhTnyfxuAxJ5gDgdSIKN +/Bf+KpYrtWKmpj29f5JZzVoqgrI3eQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDIDCCAomgAwIBAgIEN3DPtTANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV +UzEXMBUGA1UEChMORXF1aWZheCBTZWN1cmUxJjAkBgNVBAsTHUVxdWlmYXggU2Vj +dXJlIGVCdXNpbmVzcyBDQS0yMB4XDTk5MDYyMzEyMTQ0NVoXDTE5MDYyMzEyMTQ0 +NVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkVxdWlmYXggU2VjdXJlMSYwJAYD +VQQLEx1FcXVpZmF4IFNlY3VyZSBlQnVzaW5lc3MgQ0EtMjCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEA5Dk5kx5SBhsoNviyoynF7Y6yEb3+6+e0dMKP/wXn2Z0G +vxLIPw7y1tEkshHe0XMJitSxLJgJDR5QRrKDpkWNYmi7hRsgcDKqQM2mll/EcTc/ +BPO3QSQ5BxoeLmFYoBIL5aXfxavqN3HMHMg3OrmXUqesxWoklE6ce8/AatbfIb0C +AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEX +MBUGA1UEChMORXF1aWZheCBTZWN1cmUxJjAkBgNVBAsTHUVxdWlmYXggU2VjdXJl +IGVCdXNpbmVzcyBDQS0yMQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTkw +NjIzMTIxNDQ1WjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUUJ4L6q9euSBIplBq +y/3YIHqngnYwHQYDVR0OBBYEFFCeC+qvXrkgSKZQasv92CB6p4J2MAwGA1UdEwQF +MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA +A4GBAAyGgq3oThr1jokn4jVYPSm0B482UJW/bsGe68SQsoWou7dC4A8HOd/7npCy +0cE+U58DRLB+S/Rv5Hwf5+Kx5Lia78O9zt4LMjTZ3ijtM2vE1Nc9ElirfQkty3D1 +E4qUoSek1nDFbZS1yX2doNLGCEnZZpum0/QL3MUmV+GRMOrN +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICkDCCAfmgAwIBAgIBATANBgkqhkiG9w0BAQQFADBaMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEtMCsGA1UEAxMkRXF1aWZheCBT +ZWN1cmUgR2xvYmFsIGVCdXNpbmVzcyBDQS0xMB4XDTk5MDYyMTA0MDAwMFoXDTIw +MDYyMTA0MDAwMFowWjELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0VxdWlmYXggU2Vj +dXJlIEluYy4xLTArBgNVBAMTJEVxdWlmYXggU2VjdXJlIEdsb2JhbCBlQnVzaW5l +c3MgQ0EtMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuucXkAJlsTRVPEnC +UdXfp9E3j9HngXNBUmCbnaEXJnitx7HoJpQytd4zjTov2/KaelpzmKNc6fuKcxtc +58O/gGzNqfTWK8D3+ZmqY6KxRwIP1ORROhI8bIpaVIRw28HFkM9yRcuoWcDNM50/ +o5brhTMhHD4ePmBudpxnhcXIw2ECAwEAAaNmMGQwEQYJYIZIAYb4QgEBBAQDAgAH +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUvqigdHJQa0S3ySPY+6j/s1dr +aGwwHQYDVR0OBBYEFL6ooHRyUGtEt8kj2Puo/7NXa2hsMA0GCSqGSIb3DQEBBAUA +A4GBADDiAVGqx+pf2rnQZQ8w1j7aDRRJbpGTJxQx78T3LUX47Me/okENI7SS+RkA +Z70Br83gcfxaz2TE4JaY0KNA4gGK7ycH8WUBikQtBmV1UsCGECAhX2xrD2yuCRyv +8qIYNMR1pHMc8Y3c7635s3a0kr/clRAevsvIO1qEYBlWlKlV +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEVzCCAz+gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBnTELMAkGA1UEBhMCRVMx +IjAgBgNVBAcTGUMvIE11bnRhbmVyIDI0NCBCYXJjZWxvbmExQjBABgNVBAMTOUF1 +dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2 +MjYzNDA2ODEmMCQGCSqGSIb3DQEJARYXY2FAZmlybWFwcm9mZXNpb25hbC5jb20w +HhcNMDExMDI0MjIwMDAwWhcNMTMxMDI0MjIwMDAwWjCBnTELMAkGA1UEBhMCRVMx +IjAgBgNVBAcTGUMvIE11bnRhbmVyIDI0NCBCYXJjZWxvbmExQjBABgNVBAMTOUF1 +dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2 +MjYzNDA2ODEmMCQGCSqGSIb3DQEJARYXY2FAZmlybWFwcm9mZXNpb25hbC5jb20w +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDnIwNvbyOlXnjOlSztlB5u +Cp4Bx+ow0Syd3Tfom5h5VtP8c9/Qit5Vj1H5WuretXDE7aTt/6MNbg9kUDGvASdY +rv5sp0ovFy3Tc9UTHI9ZpTQsHVQERc1ouKDAA6XPhUJHlShbz++AbOCQl4oBPB3z +hxAwJkh91/zpnZFx/0GaqUC1N5wpIE8fUuOgfRNtVLcK3ulqTgesrBlf3H5idPay +BQC6haD9HThuy1q7hryUZzM1gywfI834yJFxzJeL764P3CkDG8A563DtwW4O2GcL +iam8NeTvtjS0pbbELaW+0MOUJEjb35bTALVmGotmBQ/dPz/LP6pemkr4tErvlTcb +AgMBAAGjgZ8wgZwwKgYDVR0RBCMwIYYfaHR0cDovL3d3dy5maXJtYXByb2Zlc2lv +bmFsLmNvbTASBgNVHRMBAf8ECDAGAQH/AgEBMCsGA1UdEAQkMCKADzIwMDExMDI0 +MjIwMDAwWoEPMjAxMzEwMjQyMjAwMDBaMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4E +FgQUMwugZtHq2s7eYpMEKFK1FH84aLcwDQYJKoZIhvcNAQEFBQADggEBAEdz/o0n +VPD11HecJ3lXV7cVVuzH2Fi3AQL0M+2TUIiefEaxvT8Ub/GzR0iLjJcG1+p+o1wq +u00vR+L4OQbJnC4xGgN49Lw4xiKLMzHwFgQEffl25EvXwOaD7FnMP97/T2u3Z36m +hoEyIwOdyPdfwUpgpZKpsaSgYMN4h7Mi8yrrW6ntBas3D7Hi05V2Y1Z0jFhyGzfl +ZKG+TQyTmAyX9odtsz/ny4Cm7YjHX1BiAuiZdBbQ5rQ58SfLyEDW44YQqSMSkuBp +QWOnryULwMWSyx6Yo1q6xTMPoJcB3X/ge9YGVM+h4k0460tQtcsm9MracEpqoeJ5 +quGnM/b9Sh/22WA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDZjCCAk6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBEMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3QgR2xvYmFs +IENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMTkwMzA0MDUwMDAwWjBEMQswCQYDVQQG +EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3Qg +R2xvYmFsIENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDvPE1A +PRDfO1MA4Wf+lGAVPoWI8YkNkMgoI5kF6CsgncbzYEbYwbLVjDHZ3CB5JIG/NTL8 +Y2nbsSpr7iFY8gjpeMtvy/wWUsiRxP89c96xPqfCfWbB9X5SJBri1WeR0IIQ13hL +TytCOb1kLUCgsBDTOEhGiKEMuzozKmKY+wCdE1l/bztyqu6mD4b5BWHqZ38MN5aL +5mkWRxHCJ1kDs6ZgwiFAVvqgx306E+PsV8ez1q6diYD3Aecs9pYrEw15LNnA5IZ7 +S4wMcoKK+xfNAGw6EzywhIdLFnopsk/bHdQL82Y3vdj2V7teJHq4PIu5+pIaGoSe +2HSPqht/XvT+RSIhAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FHE4NvICMVNHK266ZUapEBVYIAUJMB8GA1UdIwQYMBaAFHE4NvICMVNHK266ZUap +EBVYIAUJMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQUFAAOCAQEAA/e1K6td +EPx7srJerJsOflN4WT5CBP51o62sgU7XAotexC3IUnbHLB/8gTKY0UvGkpMzNTEv +/NgdRN3ggX+d6YvhZJFiCzkIjKx0nVnZellSlxG5FntvRdOW2TF9AjYPnDtuzywN +A0ZF66D0f0hExghAzN4bcLUprbqLOzRldRtxIR0sFAqwlpW41uryZfspuk/qkZN0 +abby/+Ea0AzRdoXLiiW9l14sbxWZJue2Kf8i7MkCx1YAzUm5s2x7UwQa4qjJqhIF +I8LO57sEAszAR6LkxCkvW0VXiVHuPOtSCP8HNR6fNWpHSlaY0VqFH4z1Ir+rzoPz +4iIprn2DQKi6bA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT +MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i +YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG +EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg +R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 +9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq +fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv +iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU +1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ +bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW +MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA +ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l +uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn +Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS +tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF +PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un +hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV +5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBY +MQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMo +R2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEx +MjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgxCzAJBgNVBAYTAlVTMRYwFAYDVQQK +Ew1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQcmltYXJ5IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9 +AWbK7hWNb6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjA +ZIVcFU2Ix7e64HXprQU9nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE0 +7e9GceBrAqg1cmuXm2bgyxx5X9gaBGgeRwLmnWDiNpcB3841kt++Z8dtd1k7j53W +kBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGttm/81w7a4DSwDRp35+MI +mO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJ +KoZIhvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ1 +6CePbJC/kRYkRj5KTs4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl +4b7UVXGYNTq+k+qurUKykG/g/CFNNWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6K +oKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHaFloxt/m0cYASSJlyc1pZU8Fj +UjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG1riR/aYNKxoU +AT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVy +c2FsIENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYD +VQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1 +c3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0DE81 +WzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUG +FF+3Qs17j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdq +XbboW0W63MOhBW9Wjo8QJqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxL +se4YuU6W3Nx2/zu+z18DwPw76L5GG//aQMJS9/7jOvdqdzXQ2o3rXhhqMcceujwb +KNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2WP0+GfPtDCapkzj4T8Fd +IgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP20gaXT73 +y/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRt +hAAnZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgoc +QIgfksILAAX/8sgCSqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4 +Lt1ZrtmhN79UNdxzMk+MBB4zsslG8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAfBgNV +HSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8EBAMCAYYwDQYJ +KoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z +dXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQ +L1EuxBRa3ugZ4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgr +Fg5fNuH8KrUwJM/gYwx7WBr+mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSo +ag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpqA1Ihn0CoZ1Dy81of398j9tx4TuaY +T1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpgY+RdM4kX2TGq2tbz +GDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiPpm8m +1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJV +OCiNUW7dFGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH +6aLcr34YEoP9VhdBLtUpgn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwX +QMAJKOSLakhT2+zNVVXxxvjpoixMptEmX36vWkzaH6byHCx+rgIW0lbQL1dTR+iS +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVy +c2FsIENBMB4XDTA0MDMwNDA1MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UE +BhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xHjAcBgNVBAMTFUdlb1RydXN0 +IFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKYV +VaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9tJPi8 +cQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTT +QjOgNB0eRXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFh +F7em6fgemdtzbvQKoiFs7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2v +c7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d8Lsrlh/eezJS/R27tQahsiFepdaVaH/w +mZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7VqnJNk22CDtucvc+081xd +VHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3CgaRr0BHdCX +teGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZ +f9hBZ3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfRe +Bi9Fi1jUIxaS5BZuKGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+ +nhutxx9z3SxPGWX9f5NAEC7S8O08ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB +/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0XG0D08DYj3rWMB8GA1UdIwQY +MBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG +9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc +aanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fX +IwjhmF7DWgh2qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzyn +ANXH/KttgCJwpQzgXQQpAvvLoJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0z +uzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsKxr2EoyNB3tZ3b4XUhRxQ4K5RirqN +Pnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxFKyDuSN/n3QmOGKja +QI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2DFKW +koRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9 +ER/frslKxfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQt +DF4JbAiXfKM9fJP/P6EUp8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/Sfuvm +bJxPgWp6ZKy7PtXny3YuxadIwVyQD8vIP/rmMuGNG2+k5o7Y+SlIis5z/iw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG +A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv +b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw +MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i +YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT +aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ +jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp +xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp +1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG +snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ +U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 +9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B +AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz +yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE +38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP +AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad +DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME +HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1 +MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL +v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8 +eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq +tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd +C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa +zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB +mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH +V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n +bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG +3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs +J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO +291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS +ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd +AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 +TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh +MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE +YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3 +MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo +ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg +MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN +ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA +PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w +wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi +EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY +avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+ +YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE +sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h +/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5 +IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD +ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy +OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P +TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ +HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER +dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf +ReYNnyicsbkqWletNw+vHX/bvZ8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICWjCCAcMCAgGlMA0GCSqGSIb3DQEBBAUAMHUxCzAJBgNVBAYTAlVTMRgwFgYD +VQQKEw9HVEUgQ29ycG9yYXRpb24xJzAlBgNVBAsTHkdURSBDeWJlclRydXN0IFNv +bHV0aW9ucywgSW5jLjEjMCEGA1UEAxMaR1RFIEN5YmVyVHJ1c3QgR2xvYmFsIFJv +b3QwHhcNOTgwODEzMDAyOTAwWhcNMTgwODEzMjM1OTAwWjB1MQswCQYDVQQGEwJV +UzEYMBYGA1UEChMPR1RFIENvcnBvcmF0aW9uMScwJQYDVQQLEx5HVEUgQ3liZXJU +cnVzdCBTb2x1dGlvbnMsIEluYy4xIzAhBgNVBAMTGkdURSBDeWJlclRydXN0IEds +b2JhbCBSb290MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVD6C28FCc6HrH +iM3dFw4usJTQGz0O9pTAipTHBsiQl8i4ZBp6fmw8U+E3KHNgf7KXUwefU/ltWJTS +r41tiGeA5u2ylc9yMcqlHHK6XALnZELn+aks1joNrI1CqiQBOeacPwGFVw1Yh0X4 +04Wqk2kmhXBIgD8SFcd5tB8FLztimQIDAQABMA0GCSqGSIb3DQEBBAUAA4GBAG3r +GwnpXtlR22ciYaQqPEh346B8pt5zohQDhT37qw4wxYMWM4ETCJ57NE7fQMh017l9 +3PR2VX2bY1QY6fDq81yx2YtCHrnAlU66+tXifPVoYb+O7AWXX1uw16OFNMQkpw0P +lZPvy5TYnh+dXIVtx6quTx8itc2VrbqnzPmrC3p/ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB+jCCAWMCAgGjMA0GCSqGSIb3DQEBBAUAMEUxCzAJBgNVBAYTAlVTMRgwFgYD +VQQKEw9HVEUgQ29ycG9yYXRpb24xHDAaBgNVBAMTE0dURSBDeWJlclRydXN0IFJv +b3QwHhcNOTYwMjIzMjMwMTAwWhcNMDYwMjIzMjM1OTAwWjBFMQswCQYDVQQGEwJV +UzEYMBYGA1UEChMPR1RFIENvcnBvcmF0aW9uMRwwGgYDVQQDExNHVEUgQ3liZXJU +cnVzdCBSb290MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC45k+625h8cXyv +RLfTD0bZZOWTwUKOx7pJjTUteueLveUFMVnGsS8KDPufpz+iCWaEVh43KRuH6X4M +ypqfpX/1FZSj1aJGgthoTNE3FQZor734sLPwKfWVWgkWYXcKIiXUT0Wqx73llt/5 +1KiOQswkwB6RJ0q1bQaAYznEol44AwIDAQABMA0GCSqGSIb3DQEBBAUAA4GBABKz +dcZfHeFhVYAA1IFLezEPI2PnPfMD+fQ2qLvZ46WXTeorKeDWanOB5sCJo9Px4KWl +IjeaY8JIILTbcuPI9tl8vrGvU9oUtCG41tWW4/5ODFlitppK+ULdjG+BqXH/9Apy +bW1EDp3zdHSo1TRJ6V6e6bR64eVaH4QwnNOfpSXY +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIH9zCCB2CgAwIBAgIBADANBgkqhkiG9w0BAQUFADCCARwxCzAJBgNVBAYTAkVT +MRIwEAYDVQQIEwlCYXJjZWxvbmExEjAQBgNVBAcTCUJhcmNlbG9uYTEuMCwGA1UE +ChMlSVBTIEludGVybmV0IHB1Ymxpc2hpbmcgU2VydmljZXMgcy5sLjErMCkGA1UE +ChQiaXBzQG1haWwuaXBzLmVzIEMuSS5GLiAgQi02MDkyOTQ1MjEzMDEGA1UECxMq +SVBTIENBIENoYWluZWQgQ0FzIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MTMwMQYD +VQQDEypJUFMgQ0EgQ2hhaW5lZCBDQXMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkx +HjAcBgkqhkiG9w0BCQEWD2lwc0BtYWlsLmlwcy5lczAeFw0wMTEyMjkwMDUzNTha +Fw0yNTEyMjcwMDUzNThaMIIBHDELMAkGA1UEBhMCRVMxEjAQBgNVBAgTCUJhcmNl +bG9uYTESMBAGA1UEBxMJQmFyY2Vsb25hMS4wLAYDVQQKEyVJUFMgSW50ZXJuZXQg +cHVibGlzaGluZyBTZXJ2aWNlcyBzLmwuMSswKQYDVQQKFCJpcHNAbWFpbC5pcHMu +ZXMgQy5JLkYuICBCLTYwOTI5NDUyMTMwMQYDVQQLEypJUFMgQ0EgQ2hhaW5lZCBD +QXMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxMzAxBgNVBAMTKklQUyBDQSBDaGFp +bmVkIENBcyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEeMBwGCSqGSIb3DQEJARYP +aXBzQG1haWwuaXBzLmVzMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDcVpJJ +spQgvJhPUOtopKdJC7/SMejHT8KGC/po/UNaivNgkjWZOLtNA1IhW/A3mTXhQSCB +hYEFcYGdtJUZqV92NC5jNzVXjrQfQj8VXOF6wV8TGDIxya2+o8eDZh65nAQTy2nB +Bt4wBrszo7Uf8I9vzv+W6FS+ZoCua9tBhDaiPQIDAQABo4IEQzCCBD8wHQYDVR0O +BBYEFKGtMbH5PuEXpsirNPxShwkeYlJBMIIBTgYDVR0jBIIBRTCCAUGAFKGtMbH5 +PuEXpsirNPxShwkeYlJBoYIBJKSCASAwggEcMQswCQYDVQQGEwJFUzESMBAGA1UE +CBMJQmFyY2Vsb25hMRIwEAYDVQQHEwlCYXJjZWxvbmExLjAsBgNVBAoTJUlQUyBJ +bnRlcm5ldCBwdWJsaXNoaW5nIFNlcnZpY2VzIHMubC4xKzApBgNVBAoUImlwc0Bt +YWlsLmlwcy5lcyBDLkkuRi4gIEItNjA5Mjk0NTIxMzAxBgNVBAsTKklQUyBDQSBD +aGFpbmVkIENBcyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEzMDEGA1UEAxMqSVBT +IENBIENoYWluZWQgQ0FzIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MR4wHAYJKoZI +hvcNAQkBFg9pcHNAbWFpbC5pcHMuZXOCAQAwDAYDVR0TBAUwAwEB/zAMBgNVHQ8E +BQMDB/+AMGsGA1UdJQRkMGIGCCsGAQUFBwMBBggrBgEFBQcDAgYIKwYBBQUHAwMG +CCsGAQUFBwMEBggrBgEFBQcDCAYKKwYBBAGCNwIBFQYKKwYBBAGCNwIBFgYKKwYB +BAGCNwoDAQYKKwYBBAGCNwoDBDARBglghkgBhvhCAQEEBAMCAAcwGgYDVR0RBBMw +EYEPaXBzQG1haWwuaXBzLmVzMBoGA1UdEgQTMBGBD2lwc0BtYWlsLmlwcy5lczBC +BglghkgBhvhCAQ0ENRYzQ2hhaW5lZCBDQSBDZXJ0aWZpY2F0ZSBpc3N1ZWQgYnkg +aHR0cDovL3d3dy5pcHMuZXMvMCkGCWCGSAGG+EIBAgQcFhpodHRwOi8vd3d3Lmlw +cy5lcy9pcHMyMDAyLzA3BglghkgBhvhCAQQEKhYoaHR0cDovL3d3dy5pcHMuZXMv +aXBzMjAwMi9pcHMyMDAyQ0FDLmNybDA8BglghkgBhvhCAQMELxYtaHR0cDovL3d3 +dy5pcHMuZXMvaXBzMjAwMi9yZXZvY2F0aW9uQ0FDLmh0bWw/MDkGCWCGSAGG+EIB +BwQsFipodHRwOi8vd3d3Lmlwcy5lcy9pcHMyMDAyL3JlbmV3YWxDQUMuaHRtbD8w +NwYJYIZIAYb4QgEIBCoWKGh0dHA6Ly93d3cuaXBzLmVzL2lwczIwMDIvcG9saWN5 +Q0FDLmh0bWwwbQYDVR0fBGYwZDAuoCygKoYoaHR0cDovL3d3dy5pcHMuZXMvaXBz +MjAwMi9pcHMyMDAyQ0FDLmNybDAyoDCgLoYsaHR0cDovL3d3d2JhY2suaXBzLmVz +L2lwczIwMDIvaXBzMjAwMkNBQy5jcmwwLwYIKwYBBQUHAQEEIzAhMB8GCCsGAQUF +BzABhhNodHRwOi8vb2NzcC5pcHMuZXMvMA0GCSqGSIb3DQEBBQUAA4GBAERyMJ1W +WKJBGyi3leGmGpVfp3hAK+/blkr8THFj2XOVvQLiogbHvpcqk4A0hgP63Ng9HgfN +HnNDJGD1HWHc3JagvPsd4+cSACczAsDAK1M92GsDgaPb1pOVIO/Tln4mkImcJpvN +b2ar7QMiRDjMWb2f2/YHogF/JsRj9SVCXmK9 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIH6jCCB1OgAwIBAgIBADANBgkqhkiG9w0BAQUFADCCARIxCzAJBgNVBAYTAkVT +MRIwEAYDVQQIEwlCYXJjZWxvbmExEjAQBgNVBAcTCUJhcmNlbG9uYTEuMCwGA1UE +ChMlSVBTIEludGVybmV0IHB1Ymxpc2hpbmcgU2VydmljZXMgcy5sLjErMCkGA1UE +ChQiaXBzQG1haWwuaXBzLmVzIEMuSS5GLiAgQi02MDkyOTQ1MjEuMCwGA1UECxMl +SVBTIENBIENMQVNFMSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEuMCwGA1UEAxMl +SVBTIENBIENMQVNFMSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEeMBwGCSqGSIb3 +DQEJARYPaXBzQG1haWwuaXBzLmVzMB4XDTAxMTIyOTAwNTkzOFoXDTI1MTIyNzAw +NTkzOFowggESMQswCQYDVQQGEwJFUzESMBAGA1UECBMJQmFyY2Vsb25hMRIwEAYD +VQQHEwlCYXJjZWxvbmExLjAsBgNVBAoTJUlQUyBJbnRlcm5ldCBwdWJsaXNoaW5n +IFNlcnZpY2VzIHMubC4xKzApBgNVBAoUImlwc0BtYWlsLmlwcy5lcyBDLkkuRi4g +IEItNjA5Mjk0NTIxLjAsBgNVBAsTJUlQUyBDQSBDTEFTRTEgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkxLjAsBgNVBAMTJUlQUyBDQSBDTEFTRTEgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkxHjAcBgkqhkiG9w0BCQEWD2lwc0BtYWlsLmlwcy5lczCBnzAN +BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA4FEnpwvdr9G5Q1uCN0VWcu+atsIS7ywS +zHb5BlmvXSHU0lq4oNTzav3KaY1mSPd05u42veiWkXWmcSjK5yISMmmwPh5r9FBS +YmL9Yzt9fuzuOOpi9GyocY3h6YvJP8a1zZRCb92CRTzo3wno7wpVqVZHYUxJZHMQ +KD/Kvwn/xi8CAwEAAaOCBEowggRGMB0GA1UdDgQWBBTrsxl588GlHKzcuh9morKb +adB4CDCCAUQGA1UdIwSCATswggE3gBTrsxl588GlHKzcuh9morKbadB4CKGCARqk +ggEWMIIBEjELMAkGA1UEBhMCRVMxEjAQBgNVBAgTCUJhcmNlbG9uYTESMBAGA1UE +BxMJQmFyY2Vsb25hMS4wLAYDVQQKEyVJUFMgSW50ZXJuZXQgcHVibGlzaGluZyBT +ZXJ2aWNlcyBzLmwuMSswKQYDVQQKFCJpcHNAbWFpbC5pcHMuZXMgQy5JLkYuICBC +LTYwOTI5NDUyMS4wLAYDVQQLEyVJUFMgQ0EgQ0xBU0UxIENlcnRpZmljYXRpb24g +QXV0aG9yaXR5MS4wLAYDVQQDEyVJUFMgQ0EgQ0xBU0UxIENlcnRpZmljYXRpb24g +QXV0aG9yaXR5MR4wHAYJKoZIhvcNAQkBFg9pcHNAbWFpbC5pcHMuZXOCAQAwDAYD +VR0TBAUwAwEB/zAMBgNVHQ8EBQMDB/+AMGsGA1UdJQRkMGIGCCsGAQUFBwMBBggr +BgEFBQcDAgYIKwYBBQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYKKwYBBAGCNwIB +FQYKKwYBBAGCNwIBFgYKKwYBBAGCNwoDAQYKKwYBBAGCNwoDBDARBglghkgBhvhC +AQEEBAMCAAcwGgYDVR0RBBMwEYEPaXBzQG1haWwuaXBzLmVzMBoGA1UdEgQTMBGB +D2lwc0BtYWlsLmlwcy5lczBBBglghkgBhvhCAQ0ENBYyQ0xBU0UxIENBIENlcnRp +ZmljYXRlIGlzc3VlZCBieSBodHRwOi8vd3d3Lmlwcy5lcy8wKQYJYIZIAYb4QgEC +BBwWGmh0dHA6Ly93d3cuaXBzLmVzL2lwczIwMDIvMDoGCWCGSAGG+EIBBAQtFito +dHRwOi8vd3d3Lmlwcy5lcy9pcHMyMDAyL2lwczIwMDJDTEFTRTEuY3JsMD8GCWCG +SAGG+EIBAwQyFjBodHRwOi8vd3d3Lmlwcy5lcy9pcHMyMDAyL3Jldm9jYXRpb25D +TEFTRTEuaHRtbD8wPAYJYIZIAYb4QgEHBC8WLWh0dHA6Ly93d3cuaXBzLmVzL2lw +czIwMDIvcmVuZXdhbENMQVNFMS5odG1sPzA6BglghkgBhvhCAQgELRYraHR0cDov +L3d3dy5pcHMuZXMvaXBzMjAwMi9wb2xpY3lDTEFTRTEuaHRtbDBzBgNVHR8EbDBq +MDGgL6AthitodHRwOi8vd3d3Lmlwcy5lcy9pcHMyMDAyL2lwczIwMDJDTEFTRTEu +Y3JsMDWgM6Axhi9odHRwOi8vd3d3YmFjay5pcHMuZXMvaXBzMjAwMi9pcHMyMDAy +Q0xBU0UxLmNybDAvBggrBgEFBQcBAQQjMCEwHwYIKwYBBQUHMAGGE2h0dHA6Ly9v +Y3NwLmlwcy5lcy8wDQYJKoZIhvcNAQEFBQADgYEAK9Dr/drIyllq2tPMMi7JVBuK +Yn4VLenZMdMu9Ccj/1urxUq2ckCuU3T0vAW0xtnIyXf7t/k0f3gA+Nak5FI/LEpj +V4F1Wo7ojPsCwJTGKbqz3Bzosq/SLmJbGqmODszFV0VRFOlOHIilkfSj945RyKm+ +hjM+5i9Ibq9UkE6tsSU= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIH6jCCB1OgAwIBAgIBADANBgkqhkiG9w0BAQUFADCCARIxCzAJBgNVBAYTAkVT +MRIwEAYDVQQIEwlCYXJjZWxvbmExEjAQBgNVBAcTCUJhcmNlbG9uYTEuMCwGA1UE +ChMlSVBTIEludGVybmV0IHB1Ymxpc2hpbmcgU2VydmljZXMgcy5sLjErMCkGA1UE +ChQiaXBzQG1haWwuaXBzLmVzIEMuSS5GLiAgQi02MDkyOTQ1MjEuMCwGA1UECxMl +SVBTIENBIENMQVNFMyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEuMCwGA1UEAxMl +SVBTIENBIENMQVNFMyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEeMBwGCSqGSIb3 +DQEJARYPaXBzQG1haWwuaXBzLmVzMB4XDTAxMTIyOTAxMDE0NFoXDTI1MTIyNzAx +MDE0NFowggESMQswCQYDVQQGEwJFUzESMBAGA1UECBMJQmFyY2Vsb25hMRIwEAYD +VQQHEwlCYXJjZWxvbmExLjAsBgNVBAoTJUlQUyBJbnRlcm5ldCBwdWJsaXNoaW5n +IFNlcnZpY2VzIHMubC4xKzApBgNVBAoUImlwc0BtYWlsLmlwcy5lcyBDLkkuRi4g +IEItNjA5Mjk0NTIxLjAsBgNVBAsTJUlQUyBDQSBDTEFTRTMgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkxLjAsBgNVBAMTJUlQUyBDQSBDTEFTRTMgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkxHjAcBgkqhkiG9w0BCQEWD2lwc0BtYWlsLmlwcy5lczCBnzAN +BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAqxf+DrDGaBtT8FK+n/ra+osTBLsBjzLZ +H49NzjaY2uQARIwo2BNEKqRrThckQpzTiKRBgtYj+4vJhuW5qYIF3PHeH+AMmVWY +8jjsbJ0gA8DvqqPGZARRLXgNo9KoOtYkTOmWehisEyMiG3zoMRGzXwmqMHBxRiVr +SXGAK5UBsh8CAwEAAaOCBEowggRGMB0GA1UdDgQWBBS4k/8uy9wsjqLnev42USGj +mFsMNDCCAUQGA1UdIwSCATswggE3gBS4k/8uy9wsjqLnev42USGjmFsMNKGCARqk +ggEWMIIBEjELMAkGA1UEBhMCRVMxEjAQBgNVBAgTCUJhcmNlbG9uYTESMBAGA1UE +BxMJQmFyY2Vsb25hMS4wLAYDVQQKEyVJUFMgSW50ZXJuZXQgcHVibGlzaGluZyBT +ZXJ2aWNlcyBzLmwuMSswKQYDVQQKFCJpcHNAbWFpbC5pcHMuZXMgQy5JLkYuICBC +LTYwOTI5NDUyMS4wLAYDVQQLEyVJUFMgQ0EgQ0xBU0UzIENlcnRpZmljYXRpb24g +QXV0aG9yaXR5MS4wLAYDVQQDEyVJUFMgQ0EgQ0xBU0UzIENlcnRpZmljYXRpb24g +QXV0aG9yaXR5MR4wHAYJKoZIhvcNAQkBFg9pcHNAbWFpbC5pcHMuZXOCAQAwDAYD +VR0TBAUwAwEB/zAMBgNVHQ8EBQMDB/+AMGsGA1UdJQRkMGIGCCsGAQUFBwMBBggr +BgEFBQcDAgYIKwYBBQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYKKwYBBAGCNwIB +FQYKKwYBBAGCNwIBFgYKKwYBBAGCNwoDAQYKKwYBBAGCNwoDBDARBglghkgBhvhC +AQEEBAMCAAcwGgYDVR0RBBMwEYEPaXBzQG1haWwuaXBzLmVzMBoGA1UdEgQTMBGB +D2lwc0BtYWlsLmlwcy5lczBBBglghkgBhvhCAQ0ENBYyQ0xBU0UzIENBIENlcnRp +ZmljYXRlIGlzc3VlZCBieSBodHRwOi8vd3d3Lmlwcy5lcy8wKQYJYIZIAYb4QgEC +BBwWGmh0dHA6Ly93d3cuaXBzLmVzL2lwczIwMDIvMDoGCWCGSAGG+EIBBAQtFito +dHRwOi8vd3d3Lmlwcy5lcy9pcHMyMDAyL2lwczIwMDJDTEFTRTMuY3JsMD8GCWCG +SAGG+EIBAwQyFjBodHRwOi8vd3d3Lmlwcy5lcy9pcHMyMDAyL3Jldm9jYXRpb25D +TEFTRTMuaHRtbD8wPAYJYIZIAYb4QgEHBC8WLWh0dHA6Ly93d3cuaXBzLmVzL2lw +czIwMDIvcmVuZXdhbENMQVNFMy5odG1sPzA6BglghkgBhvhCAQgELRYraHR0cDov +L3d3dy5pcHMuZXMvaXBzMjAwMi9wb2xpY3lDTEFTRTMuaHRtbDBzBgNVHR8EbDBq +MDGgL6AthitodHRwOi8vd3d3Lmlwcy5lcy9pcHMyMDAyL2lwczIwMDJDTEFTRTMu +Y3JsMDWgM6Axhi9odHRwOi8vd3d3YmFjay5pcHMuZXMvaXBzMjAwMi9pcHMyMDAy +Q0xBU0UzLmNybDAvBggrBgEFBQcBAQQjMCEwHwYIKwYBBQUHMAGGE2h0dHA6Ly9v +Y3NwLmlwcy5lcy8wDQYJKoZIhvcNAQEFBQADgYEAF2VcmZVDAyevJuXr0LMXI/dD +qsfwfewPxqmurpYPdikc4gYtfibFPPqhwYHOU7BC0ZdXGhd+pFFhxu7pXu8Fuuu9 +D6eSb9ijBmgpjnn1/7/5p6/ksc7C0YBCJwUENPjDfxZ4IwwHJPJGR607VNCv1TGy +r33I6unUVtkOE7LFRVA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIH9zCCB2CgAwIBAgIBADANBgkqhkiG9w0BAQUFADCCARQxCzAJBgNVBAYTAkVT +MRIwEAYDVQQIEwlCYXJjZWxvbmExEjAQBgNVBAcTCUJhcmNlbG9uYTEuMCwGA1UE +ChMlSVBTIEludGVybmV0IHB1Ymxpc2hpbmcgU2VydmljZXMgcy5sLjErMCkGA1UE +ChQiaXBzQG1haWwuaXBzLmVzIEMuSS5GLiAgQi02MDkyOTQ1MjEvMC0GA1UECxMm +SVBTIENBIENMQVNFQTEgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxLzAtBgNVBAMT +JklQUyBDQSBDTEFTRUExIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MR4wHAYJKoZI +hvcNAQkBFg9pcHNAbWFpbC5pcHMuZXMwHhcNMDExMjI5MDEwNTMyWhcNMjUxMjI3 +MDEwNTMyWjCCARQxCzAJBgNVBAYTAkVTMRIwEAYDVQQIEwlCYXJjZWxvbmExEjAQ +BgNVBAcTCUJhcmNlbG9uYTEuMCwGA1UEChMlSVBTIEludGVybmV0IHB1Ymxpc2hp +bmcgU2VydmljZXMgcy5sLjErMCkGA1UEChQiaXBzQG1haWwuaXBzLmVzIEMuSS5G +LiAgQi02MDkyOTQ1MjEvMC0GA1UECxMmSVBTIENBIENMQVNFQTEgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkxLzAtBgNVBAMTJklQUyBDQSBDTEFTRUExIENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5MR4wHAYJKoZIhvcNAQkBFg9pcHNAbWFpbC5pcHMuZXMw +gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALsw19zQVL01Tp/FTILq0VA8R5j8 +m2mdd81u4D/u6zJfX5/S0HnllXNEITLgCtud186Nq1KLK3jgm1t99P1tCeWu4Wwd +ByOgF9H5fahGRpEiqLJpxq339fWUoTCUvQDMRH/uxJ7JweaPCjbB/SQ9AaD1e+J8 +eGZDi09Z8pvZ+kmzAgMBAAGjggRTMIIETzAdBgNVHQ4EFgQUZyaW56G/2LUDnf47 +3P7yiuYV3TAwggFGBgNVHSMEggE9MIIBOYAUZyaW56G/2LUDnf473P7yiuYV3TCh +ggEcpIIBGDCCARQxCzAJBgNVBAYTAkVTMRIwEAYDVQQIEwlCYXJjZWxvbmExEjAQ +BgNVBAcTCUJhcmNlbG9uYTEuMCwGA1UEChMlSVBTIEludGVybmV0IHB1Ymxpc2hp +bmcgU2VydmljZXMgcy5sLjErMCkGA1UEChQiaXBzQG1haWwuaXBzLmVzIEMuSS5G +LiAgQi02MDkyOTQ1MjEvMC0GA1UECxMmSVBTIENBIENMQVNFQTEgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkxLzAtBgNVBAMTJklQUyBDQSBDTEFTRUExIENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5MR4wHAYJKoZIhvcNAQkBFg9pcHNAbWFpbC5pcHMuZXOC +AQAwDAYDVR0TBAUwAwEB/zAMBgNVHQ8EBQMDB/+AMGsGA1UdJQRkMGIGCCsGAQUF +BwMBBggrBgEFBQcDAgYIKwYBBQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYKKwYB +BAGCNwIBFQYKKwYBBAGCNwIBFgYKKwYBBAGCNwoDAQYKKwYBBAGCNwoDBDARBglg +hkgBhvhCAQEEBAMCAAcwGgYDVR0RBBMwEYEPaXBzQG1haWwuaXBzLmVzMBoGA1Ud +EgQTMBGBD2lwc0BtYWlsLmlwcy5lczBCBglghkgBhvhCAQ0ENRYzQ0xBU0VBMSBD +QSBDZXJ0aWZpY2F0ZSBpc3N1ZWQgYnkgaHR0cDovL3d3dy5pcHMuZXMvMCkGCWCG +SAGG+EIBAgQcFhpodHRwOi8vd3d3Lmlwcy5lcy9pcHMyMDAyLzA7BglghkgBhvhC +AQQELhYsaHR0cDovL3d3dy5pcHMuZXMvaXBzMjAwMi9pcHMyMDAyQ0xBU0VBMS5j +cmwwQAYJYIZIAYb4QgEDBDMWMWh0dHA6Ly93d3cuaXBzLmVzL2lwczIwMDIvcmV2 +b2NhdGlvbkNMQVNFQTEuaHRtbD8wPQYJYIZIAYb4QgEHBDAWLmh0dHA6Ly93d3cu +aXBzLmVzL2lwczIwMDIvcmVuZXdhbENMQVNFQTEuaHRtbD8wOwYJYIZIAYb4QgEI +BC4WLGh0dHA6Ly93d3cuaXBzLmVzL2lwczIwMDIvcG9saWN5Q0xBU0VBMS5odG1s +MHUGA1UdHwRuMGwwMqAwoC6GLGh0dHA6Ly93d3cuaXBzLmVzL2lwczIwMDIvaXBz +MjAwMkNMQVNFQTEuY3JsMDagNKAyhjBodHRwOi8vd3d3YmFjay5pcHMuZXMvaXBz +MjAwMi9pcHMyMDAyQ0xBU0VBMS5jcmwwLwYIKwYBBQUHAQEEIzAhMB8GCCsGAQUF +BzABhhNodHRwOi8vb2NzcC5pcHMuZXMvMA0GCSqGSIb3DQEBBQUAA4GBAH66iqyA +AIQVCtWYUQxkxZwCWINmyq0eB81+atqAB98DNEock8RLWCA1NnHtogo1EqWmZaeF +aQoO42Hu6r4okzPV7Oi+xNtff6j5YzHIa5biKcJboOeXNp13XjFr/tOn2yrb25aL +H2betgPAK7N41lUH5Y85UN4HI3LmvSAUS7SG +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIH9zCCB2CgAwIBAgIBADANBgkqhkiG9w0BAQUFADCCARQxCzAJBgNVBAYTAkVT +MRIwEAYDVQQIEwlCYXJjZWxvbmExEjAQBgNVBAcTCUJhcmNlbG9uYTEuMCwGA1UE +ChMlSVBTIEludGVybmV0IHB1Ymxpc2hpbmcgU2VydmljZXMgcy5sLjErMCkGA1UE +ChQiaXBzQG1haWwuaXBzLmVzIEMuSS5GLiAgQi02MDkyOTQ1MjEvMC0GA1UECxMm +SVBTIENBIENMQVNFQTMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxLzAtBgNVBAMT +JklQUyBDQSBDTEFTRUEzIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MR4wHAYJKoZI +hvcNAQkBFg9pcHNAbWFpbC5pcHMuZXMwHhcNMDExMjI5MDEwNzUwWhcNMjUxMjI3 +MDEwNzUwWjCCARQxCzAJBgNVBAYTAkVTMRIwEAYDVQQIEwlCYXJjZWxvbmExEjAQ +BgNVBAcTCUJhcmNlbG9uYTEuMCwGA1UEChMlSVBTIEludGVybmV0IHB1Ymxpc2hp +bmcgU2VydmljZXMgcy5sLjErMCkGA1UEChQiaXBzQG1haWwuaXBzLmVzIEMuSS5G +LiAgQi02MDkyOTQ1MjEvMC0GA1UECxMmSVBTIENBIENMQVNFQTMgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkxLzAtBgNVBAMTJklQUyBDQSBDTEFTRUEzIENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5MR4wHAYJKoZIhvcNAQkBFg9pcHNAbWFpbC5pcHMuZXMw +gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAO6AAPYaZC6tasiDsYun7o/ZttvN +G7uGBiJ2MwwSbUhWYdLcgiViL5/SaTBlA0IjWLxH3GvWdV0XPOH/8lhneaDBgbHU +VqLyjRGZ/fZ98cfEXgIqmuJKtROKAP2Md4bm15T1IHUuDky/dMQ/gT6DtKM4Ninn +6Cr1jIhBqoCm42zvAgMBAAGjggRTMIIETzAdBgNVHQ4EFgQUHp9XUEe2YZM50yz8 +2l09BXW3mQIwggFGBgNVHSMEggE9MIIBOYAUHp9XUEe2YZM50yz82l09BXW3mQKh +ggEcpIIBGDCCARQxCzAJBgNVBAYTAkVTMRIwEAYDVQQIEwlCYXJjZWxvbmExEjAQ +BgNVBAcTCUJhcmNlbG9uYTEuMCwGA1UEChMlSVBTIEludGVybmV0IHB1Ymxpc2hp +bmcgU2VydmljZXMgcy5sLjErMCkGA1UEChQiaXBzQG1haWwuaXBzLmVzIEMuSS5G +LiAgQi02MDkyOTQ1MjEvMC0GA1UECxMmSVBTIENBIENMQVNFQTMgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkxLzAtBgNVBAMTJklQUyBDQSBDTEFTRUEzIENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5MR4wHAYJKoZIhvcNAQkBFg9pcHNAbWFpbC5pcHMuZXOC +AQAwDAYDVR0TBAUwAwEB/zAMBgNVHQ8EBQMDB/+AMGsGA1UdJQRkMGIGCCsGAQUF +BwMBBggrBgEFBQcDAgYIKwYBBQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYKKwYB +BAGCNwIBFQYKKwYBBAGCNwIBFgYKKwYBBAGCNwoDAQYKKwYBBAGCNwoDBDARBglg +hkgBhvhCAQEEBAMCAAcwGgYDVR0RBBMwEYEPaXBzQG1haWwuaXBzLmVzMBoGA1Ud +EgQTMBGBD2lwc0BtYWlsLmlwcy5lczBCBglghkgBhvhCAQ0ENRYzQ0xBU0VBMyBD +QSBDZXJ0aWZpY2F0ZSBpc3N1ZWQgYnkgaHR0cDovL3d3dy5pcHMuZXMvMCkGCWCG +SAGG+EIBAgQcFhpodHRwOi8vd3d3Lmlwcy5lcy9pcHMyMDAyLzA7BglghkgBhvhC +AQQELhYsaHR0cDovL3d3dy5pcHMuZXMvaXBzMjAwMi9pcHMyMDAyQ0xBU0VBMy5j +cmwwQAYJYIZIAYb4QgEDBDMWMWh0dHA6Ly93d3cuaXBzLmVzL2lwczIwMDIvcmV2 +b2NhdGlvbkNMQVNFQTMuaHRtbD8wPQYJYIZIAYb4QgEHBDAWLmh0dHA6Ly93d3cu +aXBzLmVzL2lwczIwMDIvcmVuZXdhbENMQVNFQTMuaHRtbD8wOwYJYIZIAYb4QgEI +BC4WLGh0dHA6Ly93d3cuaXBzLmVzL2lwczIwMDIvcG9saWN5Q0xBU0VBMy5odG1s +MHUGA1UdHwRuMGwwMqAwoC6GLGh0dHA6Ly93d3cuaXBzLmVzL2lwczIwMDIvaXBz +MjAwMkNMQVNFQTMuY3JsMDagNKAyhjBodHRwOi8vd3d3YmFjay5pcHMuZXMvaXBz +MjAwMi9pcHMyMDAyQ0xBU0VBMy5jcmwwLwYIKwYBBQUHAQEEIzAhMB8GCCsGAQUF +BzABhhNodHRwOi8vb2NzcC5pcHMuZXMvMA0GCSqGSIb3DQEBBQUAA4GBAEo9IEca +2on0eisxeewBwMwB9dbB/MjD81ACUZBYKp/nNQlbMAqBACVHr9QPDp5gJqiVp4MI +3y2s6Q73nMify5NF8bpqxmdRSmlPa/59Cy9SKcJQrSRE7SOzSMtEQMEDlQwKeAYS +AfWRMS1Jjbs/RU4s4OjNtckUFQzjB4ObJnXv +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICtzCCAiACAQAwDQYJKoZIhvcNAQEEBQAwgaMxCzAJBgNVBAYTAkVTMRIwEAYD +VQQIEwlCQVJDRUxPTkExEjAQBgNVBAcTCUJBUkNFTE9OQTEZMBcGA1UEChMQSVBT +IFNlZ3VyaWRhZCBDQTEYMBYGA1UECxMPQ2VydGlmaWNhY2lvbmVzMRcwFQYDVQQD +Ew5JUFMgU0VSVklET1JFUzEeMBwGCSqGSIb3DQEJARYPaXBzQG1haWwuaXBzLmVz +MB4XDTk4MDEwMTIzMjEwN1oXDTA5MTIyOTIzMjEwN1owgaMxCzAJBgNVBAYTAkVT +MRIwEAYDVQQIEwlCQVJDRUxPTkExEjAQBgNVBAcTCUJBUkNFTE9OQTEZMBcGA1UE +ChMQSVBTIFNlZ3VyaWRhZCBDQTEYMBYGA1UECxMPQ2VydGlmaWNhY2lvbmVzMRcw +FQYDVQQDEw5JUFMgU0VSVklET1JFUzEeMBwGCSqGSIb3DQEJARYPaXBzQG1haWwu +aXBzLmVzMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCsT1J0nznqjtwlxLyY +XZhkJAk8IbPMGbWOlI6H0fg3PqHILVikgDVboXVsHUUMH2Fjal5vmwpMwci4YSM1 +gf/+rHhwLWjhOgeYlQJU3c0jt4BT18g3RXIGJBK6E2Ehim51KODFDzT9NthFf+G4 +Nu+z4cYgjui0OLzhPvYR3oydAQIDAQABMA0GCSqGSIb3DQEBBAUAA4GBACzzw3lY +JN7GO9HgQmm47mSzPWIBubOE3yN93ZjPEKn+ANgilgUTB1RXxafey9m4iEL2mdsU +dx+2/iU94aI+A6mB0i1sR/WWRowiq8jMDQ6XXotBtDvECgZAHd1G9AHduoIuPD14 +cJ58GNCr+Lh3B0Zx8coLY1xq+XKU1QFPoNtC +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIIODCCB6GgAwIBAgIBADANBgkqhkiG9w0BAQUFADCCAR4xCzAJBgNVBAYTAkVT +MRIwEAYDVQQIEwlCYXJjZWxvbmExEjAQBgNVBAcTCUJhcmNlbG9uYTEuMCwGA1UE +ChMlSVBTIEludGVybmV0IHB1Ymxpc2hpbmcgU2VydmljZXMgcy5sLjErMCkGA1UE +ChQiaXBzQG1haWwuaXBzLmVzIEMuSS5GLiAgQi02MDkyOTQ1MjE0MDIGA1UECxMr +SVBTIENBIFRpbWVzdGFtcGluZyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTE0MDIG +A1UEAxMrSVBTIENBIFRpbWVzdGFtcGluZyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eTEeMBwGCSqGSIb3DQEJARYPaXBzQG1haWwuaXBzLmVzMB4XDTAxMTIyOTAxMTAx +OFoXDTI1MTIyNzAxMTAxOFowggEeMQswCQYDVQQGEwJFUzESMBAGA1UECBMJQmFy +Y2Vsb25hMRIwEAYDVQQHEwlCYXJjZWxvbmExLjAsBgNVBAoTJUlQUyBJbnRlcm5l +dCBwdWJsaXNoaW5nIFNlcnZpY2VzIHMubC4xKzApBgNVBAoUImlwc0BtYWlsLmlw +cy5lcyBDLkkuRi4gIEItNjA5Mjk0NTIxNDAyBgNVBAsTK0lQUyBDQSBUaW1lc3Rh +bXBpbmcgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxNDAyBgNVBAMTK0lQUyBDQSBU +aW1lc3RhbXBpbmcgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHjAcBgkqhkiG9w0B +CQEWD2lwc0BtYWlsLmlwcy5lczCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA +vLjuVqWajOY2ycJioGaBjRrVetJznw6EZLqVtJCneK/K/lRhW86yIFcBrkSSQxA4 +Efdo/BdApWgnMjvEp+ZCccWZ73b/K5Uk9UmSGGjKALWkWi9uy9YbLA1UZ2t6KaFY +q6JaANZbuxjC3/YeE1Z2m6Vo4pjOxgOKNNtMg0GmqaMCAwEAAaOCBIAwggR8MB0G +A1UdDgQWBBSL0BBQCYHynQnVDmB4AyKiP8jKZjCCAVAGA1UdIwSCAUcwggFDgBSL +0BBQCYHynQnVDmB4AyKiP8jKZqGCASakggEiMIIBHjELMAkGA1UEBhMCRVMxEjAQ +BgNVBAgTCUJhcmNlbG9uYTESMBAGA1UEBxMJQmFyY2Vsb25hMS4wLAYDVQQKEyVJ +UFMgSW50ZXJuZXQgcHVibGlzaGluZyBTZXJ2aWNlcyBzLmwuMSswKQYDVQQKFCJp +cHNAbWFpbC5pcHMuZXMgQy5JLkYuICBCLTYwOTI5NDUyMTQwMgYDVQQLEytJUFMg +Q0EgVGltZXN0YW1waW5nIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MTQwMgYDVQQD +EytJUFMgQ0EgVGltZXN0YW1waW5nIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MR4w +HAYJKoZIhvcNAQkBFg9pcHNAbWFpbC5pcHMuZXOCAQAwDAYDVR0TBAUwAwEB/zAM +BgNVHQ8EBQMDB/+AMGsGA1UdJQRkMGIGCCsGAQUFBwMBBggrBgEFBQcDAgYIKwYB +BQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYKKwYBBAGCNwIBFQYKKwYBBAGCNwIB +FgYKKwYBBAGCNwoDAQYKKwYBBAGCNwoDBDARBglghkgBhvhCAQEEBAMCAAcwGgYD +VR0RBBMwEYEPaXBzQG1haWwuaXBzLmVzMBoGA1UdEgQTMBGBD2lwc0BtYWlsLmlw +cy5lczBHBglghkgBhvhCAQ0EOhY4VGltZXN0YW1waW5nIENBIENlcnRpZmljYXRl +IGlzc3VlZCBieSBodHRwOi8vd3d3Lmlwcy5lcy8wKQYJYIZIAYb4QgECBBwWGmh0 +dHA6Ly93d3cuaXBzLmVzL2lwczIwMDIvMEAGCWCGSAGG+EIBBAQzFjFodHRwOi8v +d3d3Lmlwcy5lcy9pcHMyMDAyL2lwczIwMDJUaW1lc3RhbXBpbmcuY3JsMEUGCWCG +SAGG+EIBAwQ4FjZodHRwOi8vd3d3Lmlwcy5lcy9pcHMyMDAyL3Jldm9jYXRpb25U +aW1lc3RhbXBpbmcuaHRtbD8wQgYJYIZIAYb4QgEHBDUWM2h0dHA6Ly93d3cuaXBz +LmVzL2lwczIwMDIvcmVuZXdhbFRpbWVzdGFtcGluZy5odG1sPzBABglghkgBhvhC +AQgEMxYxaHR0cDovL3d3dy5pcHMuZXMvaXBzMjAwMi9wb2xpY3lUaW1lc3RhbXBp +bmcuaHRtbDB/BgNVHR8EeDB2MDegNaAzhjFodHRwOi8vd3d3Lmlwcy5lcy9pcHMy +MDAyL2lwczIwMDJUaW1lc3RhbXBpbmcuY3JsMDugOaA3hjVodHRwOi8vd3d3YmFj +ay5pcHMuZXMvaXBzMjAwMi9pcHMyMDAyVGltZXN0YW1waW5nLmNybDAvBggrBgEF +BQcBAQQjMCEwHwYIKwYBBQUHMAGGE2h0dHA6Ly9vY3NwLmlwcy5lcy8wDQYJKoZI +hvcNAQEFBQADgYEAZbrBzAAalZHK6Ww6vzoeFAh8+4Pua2JR0zORtWB5fgTYXXk3 +6MNbsMRnLWhasl8OCvrNPzpFoeo2zyYepxEoxZSPhExTCMWTs/zif/WN87GphV+I +3pGW7hdbrqXqcGV4LCFkAZXOzkw+UPS2Wctjjba9GNSHSl/c7+lW8AoM6HU= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFSzCCBLSgAwIBAgIBaTANBgkqhkiG9w0BAQQFADCBmTELMAkGA1UEBhMCSFUx +ETAPBgNVBAcTCEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0 +b25zYWdpIEtmdC4xGjAYBgNVBAsTEVRhbnVzaXR2YW55a2lhZG9rMTIwMAYDVQQD +EylOZXRMb2NrIFV6bGV0aSAoQ2xhc3MgQikgVGFudXNpdHZhbnlraWFkbzAeFw05 +OTAyMjUxNDEwMjJaFw0xOTAyMjAxNDEwMjJaMIGZMQswCQYDVQQGEwJIVTERMA8G +A1UEBxMIQnVkYXBlc3QxJzAlBgNVBAoTHk5ldExvY2sgSGFsb3phdGJpenRvbnNh +Z2kgS2Z0LjEaMBgGA1UECxMRVGFudXNpdHZhbnlraWFkb2sxMjAwBgNVBAMTKU5l +dExvY2sgVXpsZXRpIChDbGFzcyBCKSBUYW51c2l0dmFueWtpYWRvMIGfMA0GCSqG +SIb3DQEBAQUAA4GNADCBiQKBgQCx6gTsIKAjwo84YM/HRrPVG/77uZmeBNwcf4xK +gZjupNTKihe5In+DCnVMm8Bp2GQ5o+2So/1bXHQawEfKOml2mrriRBf8TKPV/riX +iK+IA4kfpPIEPsgHC+b5sy96YhQJRhTKZPWLgLViqNhr1nGTLbO/CVRY7QbrqHvc +Q7GhaQIDAQABo4ICnzCCApswEgYDVR0TAQH/BAgwBgEB/wIBBDAOBgNVHQ8BAf8E +BAMCAAYwEQYJYIZIAYb4QgEBBAQDAgAHMIICYAYJYIZIAYb4QgENBIICURaCAk1G +SUdZRUxFTSEgRXplbiB0YW51c2l0dmFueSBhIE5ldExvY2sgS2Z0LiBBbHRhbGFu +b3MgU3pvbGdhbHRhdGFzaSBGZWx0ZXRlbGVpYmVuIGxlaXJ0IGVsamFyYXNvayBh +bGFwamFuIGtlc3p1bHQuIEEgaGl0ZWxlc2l0ZXMgZm9seWFtYXRhdCBhIE5ldExv +Y2sgS2Z0LiB0ZXJtZWtmZWxlbG9zc2VnLWJpenRvc2l0YXNhIHZlZGkuIEEgZGln +aXRhbGlzIGFsYWlyYXMgZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUgYXogZWxvaXJ0 +IGVsbGVub3J6ZXNpIGVsamFyYXMgbWVndGV0ZWxlLiBBeiBlbGphcmFzIGxlaXJh +c2EgbWVndGFsYWxoYXRvIGEgTmV0TG9jayBLZnQuIEludGVybmV0IGhvbmxhcGph +biBhIGh0dHBzOi8vd3d3Lm5ldGxvY2submV0L2RvY3MgY2ltZW4gdmFneSBrZXJo +ZXRvIGF6IGVsbGVub3J6ZXNAbmV0bG9jay5uZXQgZS1tYWlsIGNpbWVuLiBJTVBP +UlRBTlQhIFRoZSBpc3N1YW5jZSBhbmQgdGhlIHVzZSBvZiB0aGlzIGNlcnRpZmlj +YXRlIGlzIHN1YmplY3QgdG8gdGhlIE5ldExvY2sgQ1BTIGF2YWlsYWJsZSBhdCBo +dHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIG9yIGJ5IGUtbWFpbCBhdCBjcHNA +bmV0bG9jay5uZXQuMA0GCSqGSIb3DQEBBAUAA4GBAATbrowXr/gOkDFOzT4JwG06 +sPgzTEdM43WIEJessDgVkcYplswhwG08pXTP2IKlOcNl40JwuyKQ433bNXbhoLXa +n3BukxowOR0w2y7jfLKRstE3Kfq51hdcR0/jHTjrn9V7lagonhVK0dHQKwCXoOKS +NitjrFgBazMpUIaD8QFI +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFTzCCBLigAwIBAgIBaDANBgkqhkiG9w0BAQQFADCBmzELMAkGA1UEBhMCSFUx +ETAPBgNVBAcTCEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0 +b25zYWdpIEtmdC4xGjAYBgNVBAsTEVRhbnVzaXR2YW55a2lhZG9rMTQwMgYDVQQD +EytOZXRMb2NrIEV4cHJlc3N6IChDbGFzcyBDKSBUYW51c2l0dmFueWtpYWRvMB4X +DTk5MDIyNTE0MDgxMVoXDTE5MDIyMDE0MDgxMVowgZsxCzAJBgNVBAYTAkhVMREw +DwYDVQQHEwhCdWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6dG9u +c2FnaSBLZnQuMRowGAYDVQQLExFUYW51c2l0dmFueWtpYWRvazE0MDIGA1UEAxMr +TmV0TG9jayBFeHByZXNzeiAoQ2xhc3MgQykgVGFudXNpdHZhbnlraWFkbzCBnzAN +BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA6+ywbGGKIyWvYCDj2Z/8kwvbXY2wobNA +OoLO/XXgeDIDhlqGlZHtU/qdQPzm6N3ZW3oDvV3zOwzDUXmbrVWg6dADEK8KuhRC +2VImESLH0iDMgqSaqf64gXadarfSNnU+sYYJ9m5tfk63euyucYT2BDMIJTLrdKwW +RMbkQJMdf60CAwEAAaOCAp8wggKbMBIGA1UdEwEB/wQIMAYBAf8CAQQwDgYDVR0P +AQH/BAQDAgAGMBEGCWCGSAGG+EIBAQQEAwIABzCCAmAGCWCGSAGG+EIBDQSCAlEW +ggJNRklHWUVMRU0hIEV6ZW4gdGFudXNpdHZhbnkgYSBOZXRMb2NrIEtmdC4gQWx0 +YWxhbm9zIFN6b2xnYWx0YXRhc2kgRmVsdGV0ZWxlaWJlbiBsZWlydCBlbGphcmFz +b2sgYWxhcGphbiBrZXN6dWx0LiBBIGhpdGVsZXNpdGVzIGZvbHlhbWF0YXQgYSBO +ZXRMb2NrIEtmdC4gdGVybWVrZmVsZWxvc3NlZy1iaXp0b3NpdGFzYSB2ZWRpLiBB +IGRpZ2l0YWxpcyBhbGFpcmFzIGVsZm9nYWRhc2FuYWsgZmVsdGV0ZWxlIGF6IGVs +b2lydCBlbGxlbm9yemVzaSBlbGphcmFzIG1lZ3RldGVsZS4gQXogZWxqYXJhcyBs +ZWlyYXNhIG1lZ3RhbGFsaGF0byBhIE5ldExvY2sgS2Z0LiBJbnRlcm5ldCBob25s +YXBqYW4gYSBodHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIGNpbWVuIHZhZ3kg +a2VyaGV0byBheiBlbGxlbm9yemVzQG5ldGxvY2submV0IGUtbWFpbCBjaW1lbi4g +SU1QT1JUQU5UISBUaGUgaXNzdWFuY2UgYW5kIHRoZSB1c2Ugb2YgdGhpcyBjZXJ0 +aWZpY2F0ZSBpcyBzdWJqZWN0IHRvIHRoZSBOZXRMb2NrIENQUyBhdmFpbGFibGUg +YXQgaHR0cHM6Ly93d3cubmV0bG9jay5uZXQvZG9jcyBvciBieSBlLW1haWwgYXQg +Y3BzQG5ldGxvY2submV0LjANBgkqhkiG9w0BAQQFAAOBgQAQrX/XDDKACtiG8XmY +ta3UzbM2xJZIwVzNmtkFLp++UOv0JhQQLdRmF/iewSf98e3ke0ugbLWrmldwpu2g +pO0u9f38vf5NNwgMvOOWgyL1SRt/Syu0VMGAfJlOHdCM7tCs5ZL6dVb+ZKATj7i4 +Fp1hBWeAyNDYpQcCNJgEjTME1A== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGfTCCBWWgAwIBAgICAQMwDQYJKoZIhvcNAQEEBQAwga8xCzAJBgNVBAYTAkhV +MRAwDgYDVQQIEwdIdW5nYXJ5MREwDwYDVQQHEwhCdWRhcGVzdDEnMCUGA1UEChMe +TmV0TG9jayBIYWxvemF0Yml6dG9uc2FnaSBLZnQuMRowGAYDVQQLExFUYW51c2l0 +dmFueWtpYWRvazE2MDQGA1UEAxMtTmV0TG9jayBLb3pqZWd5em9pIChDbGFzcyBB +KSBUYW51c2l0dmFueWtpYWRvMB4XDTk5MDIyNDIzMTQ0N1oXDTE5MDIxOTIzMTQ0 +N1owga8xCzAJBgNVBAYTAkhVMRAwDgYDVQQIEwdIdW5nYXJ5MREwDwYDVQQHEwhC +dWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6dG9uc2FnaSBLZnQu +MRowGAYDVQQLExFUYW51c2l0dmFueWtpYWRvazE2MDQGA1UEAxMtTmV0TG9jayBL +b3pqZWd5em9pIChDbGFzcyBBKSBUYW51c2l0dmFueWtpYWRvMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvHSMD7tM9DceqQWC2ObhbHDqeLVu0ThEDaiD +zl3S1tWBxdRL51uUcCbbO51qTGL3cfNk1mE7PetzozfZz+qMkjvN9wfcZnSX9EUi +3fRc4L9t875lM+QVOr/bmJBVOMTtplVjC7B4BPTjbsE/jvxReB+SnoPC/tmwqcm8 +WgD/qaiYdPv2LD4VOQ22BFWoDpggQrOxJa1+mm9dU7GrDPzr4PN6s6iz/0b2Y6LY +Oph7tqyF/7AlT3Rj5xMHpQqPBffAZG9+pyeAlt7ULoZgx2srXnN7F+eRP2QM2Esi +NCubMvJIH5+hCoR64sKtlz2O1cH5VqNQ6ca0+pii7pXmKgOM3wIDAQABo4ICnzCC +ApswDgYDVR0PAQH/BAQDAgAGMBIGA1UdEwEB/wQIMAYBAf8CAQQwEQYJYIZIAYb4 +QgEBBAQDAgAHMIICYAYJYIZIAYb4QgENBIICURaCAk1GSUdZRUxFTSEgRXplbiB0 +YW51c2l0dmFueSBhIE5ldExvY2sgS2Z0LiBBbHRhbGFub3MgU3pvbGdhbHRhdGFz +aSBGZWx0ZXRlbGVpYmVuIGxlaXJ0IGVsamFyYXNvayBhbGFwamFuIGtlc3p1bHQu +IEEgaGl0ZWxlc2l0ZXMgZm9seWFtYXRhdCBhIE5ldExvY2sgS2Z0LiB0ZXJtZWtm +ZWxlbG9zc2VnLWJpenRvc2l0YXNhIHZlZGkuIEEgZGlnaXRhbGlzIGFsYWlyYXMg +ZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUgYXogZWxvaXJ0IGVsbGVub3J6ZXNpIGVs +amFyYXMgbWVndGV0ZWxlLiBBeiBlbGphcmFzIGxlaXJhc2EgbWVndGFsYWxoYXRv +IGEgTmV0TG9jayBLZnQuIEludGVybmV0IGhvbmxhcGphbiBhIGh0dHBzOi8vd3d3 +Lm5ldGxvY2submV0L2RvY3MgY2ltZW4gdmFneSBrZXJoZXRvIGF6IGVsbGVub3J6 +ZXNAbmV0bG9jay5uZXQgZS1tYWlsIGNpbWVuLiBJTVBPUlRBTlQhIFRoZSBpc3N1 +YW5jZSBhbmQgdGhlIHVzZSBvZiB0aGlzIGNlcnRpZmljYXRlIGlzIHN1YmplY3Qg +dG8gdGhlIE5ldExvY2sgQ1BTIGF2YWlsYWJsZSBhdCBodHRwczovL3d3dy5uZXRs +b2NrLm5ldC9kb2NzIG9yIGJ5IGUtbWFpbCBhdCBjcHNAbmV0bG9jay5uZXQuMA0G +CSqGSIb3DQEBBAUAA4IBAQBIJEb3ulZv+sgoA0BO5TE5ayZrU3/b39/zcT0mwBQO +xmd7I6gMc90Bu8bKbjc5VdXHjFYgDigKDtIqpLBJUsY4B/6+CgmM0ZjPytoUMaFP +0jn8DxEsQ8Pdq5PHVT5HfBgaANzze9jyf1JsIPQLX2lS9O74silg6+NJMSEN1rUQ +QeJBCWziGppWS3cC9qCbmieH6FUpccKQn0V4GuEVZD3QDtigdp+uxdAu6tYPVuxk +f1qbFFgBJ34TUMdrKuZoPL9coAob4Q566eKAw+np9v1sEZ7Q5SgnK1QyQhSCdeZK +8CtmdWOMovsEPoMOmzbwGOQmIMOM8CgHrTwXZoi1/baI +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIG0TCCBbmgAwIBAgIBezANBgkqhkiG9w0BAQUFADCByTELMAkGA1UEBhMCSFUx +ETAPBgNVBAcTCEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0 +b25zYWdpIEtmdC4xGjAYBgNVBAsTEVRhbnVzaXR2YW55a2lhZG9rMUIwQAYDVQQD +EzlOZXRMb2NrIE1pbm9zaXRldHQgS296amVneXpvaSAoQ2xhc3MgUUEpIFRhbnVz +aXR2YW55a2lhZG8xHjAcBgkqhkiG9w0BCQEWD2luZm9AbmV0bG9jay5odTAeFw0w +MzAzMzAwMTQ3MTFaFw0yMjEyMTUwMTQ3MTFaMIHJMQswCQYDVQQGEwJIVTERMA8G +A1UEBxMIQnVkYXBlc3QxJzAlBgNVBAoTHk5ldExvY2sgSGFsb3phdGJpenRvbnNh +Z2kgS2Z0LjEaMBgGA1UECxMRVGFudXNpdHZhbnlraWFkb2sxQjBABgNVBAMTOU5l +dExvY2sgTWlub3NpdGV0dCBLb3pqZWd5em9pIChDbGFzcyBRQSkgVGFudXNpdHZh +bnlraWFkbzEeMBwGCSqGSIb3DQEJARYPaW5mb0BuZXRsb2NrLmh1MIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx1Ilstg91IRVCacbvWy5FPSKAtt2/Goq +eKvld/Bu4IwjZ9ulZJm53QE+b+8tmjwi8F3JV6BVQX/yQ15YglMxZc4e8ia6AFQe +r7C8HORSjKAyr7c3sVNnaHRnUPYtLmTeriZ539+Zhqurf4XsoPuAzPS4DB6TRWO5 +3Lhbm+1bOdRfYrCnjnxmOCyqsQhjF2d9zL2z8cM/z1A57dEZgxXbhxInlrfa6uWd +vLrqOU+L73Sa58XQ0uqGURzk/mQIKAR5BevKxXEOC++r6uwSEaEYBTJp0QwsGj0l +mT+1fMptsK6ZmfoIYOcZwvK9UdPM0wKswREMgM6r3JSda6M5UzrWhQIDAMV9o4IC +wDCCArwwEgYDVR0TAQH/BAgwBgEB/wIBBDAOBgNVHQ8BAf8EBAMCAQYwggJ1Bglg +hkgBhvhCAQ0EggJmFoICYkZJR1lFTEVNISBFemVuIHRhbnVzaXR2YW55IGEgTmV0 +TG9jayBLZnQuIE1pbm9zaXRldHQgU3pvbGdhbHRhdGFzaSBTemFiYWx5emF0YWJh +biBsZWlydCBlbGphcmFzb2sgYWxhcGphbiBrZXN6dWx0LiBBIG1pbm9zaXRldHQg +ZWxla3Ryb25pa3VzIGFsYWlyYXMgam9naGF0YXMgZXJ2ZW55ZXN1bGVzZW5laywg +dmFsYW1pbnQgZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUgYSBNaW5vc2l0ZXR0IFN6 +b2xnYWx0YXRhc2kgU3phYmFseXphdGJhbiwgYXogQWx0YWxhbm9zIFN6ZXJ6b2Rl +c2kgRmVsdGV0ZWxla2JlbiBlbG9pcnQgZWxsZW5vcnplc2kgZWxqYXJhcyBtZWd0 +ZXRlbGUuIEEgZG9rdW1lbnR1bW9rIG1lZ3RhbGFsaGF0b2sgYSBodHRwczovL3d3 +dy5uZXRsb2NrLmh1L2RvY3MvIGNpbWVuIHZhZ3kga2VyaGV0b2sgYXogaW5mb0Bu +ZXRsb2NrLm5ldCBlLW1haWwgY2ltZW4uIFdBUk5JTkchIFRoZSBpc3N1YW5jZSBh +bmQgdGhlIHVzZSBvZiB0aGlzIGNlcnRpZmljYXRlIGFyZSBzdWJqZWN0IHRvIHRo +ZSBOZXRMb2NrIFF1YWxpZmllZCBDUFMgYXZhaWxhYmxlIGF0IGh0dHBzOi8vd3d3 +Lm5ldGxvY2suaHUvZG9jcy8gb3IgYnkgZS1tYWlsIGF0IGluZm9AbmV0bG9jay5u +ZXQwHQYDVR0OBBYEFAlqYhaSsFq7VQ7LdTI6MuWyIckoMA0GCSqGSIb3DQEBBQUA +A4IBAQCRalCc23iBmz+LQuM7/KbD7kPgz/PigDVJRXYC4uMvBcXxKufAQTPGtpvQ +MznNwNuhrWw3AkxYQTvyl5LGSKjN5Yo5iWH5Upfpvfb5lHTocQ68d4bDBsxafEp+ +NFAwLvt/MpqNPfMgW/hqyobzMUwsWYACff44yTB1HLdV47yfuqhthCgFdbOLDcCR +VCHnpgu0mfVRQdzNo0ci2ccBgcTcR08m6h/t280NmPSjnLRzMkqWmf68f8glWPhY +83ZmiVSkpj7EUFy6iRiCdUgh0k8T6GB+B3bbELVR5qq5aKrN9p2QdRLqOBrKROi3 +macqaJVmlaut74nLYKkGEsaUR+ko +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBi +MQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu +MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3Jp +dHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMxMjM1OTU5WjBiMQswCQYDVQQGEwJV +UzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydO +ZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwz +c7MEL7xxjOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPP +OCwGJgl6cvf6UDL4wpPTaaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rl +mGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXTcrA/vGp97Eh/jcOrqnErU2lBUzS1sLnF +BgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc/Qzpf14Dl847ABSHJ3A4 +qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMBAAGjgZcw +gZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIB +BjAPBgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwu +bmV0c29sc3NsLmNvbS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3Jp +dHkuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc8 +6fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q4LqILPxFzBiwmZVRDuwduIj/ +h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/GGUsyfJj4akH +/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv +wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHN +pGxlaKFJdlxDydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa +GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg +Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J +WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB +rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp ++ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1 +ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i +Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz +PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og +/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH +oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI +yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud +EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2 +A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL +MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT +ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f +BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn +g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl +fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K +WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha +B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc +hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR +TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD +mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z +ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y +4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza +8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMzAeFw0wNjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM +V0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNggDhoB +4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUr +H556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd +8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv +vWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLT +mZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhe +btfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjc +T5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDt +WAEXMJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZ +c6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A +4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYD +VR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHTBgkrBgEEAb5YAAMwgcUwgZMG +CCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0 +aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 +aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVu +dC4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2Nw +czALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4G +A1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4ywLQoUmkRzBFMQswCQYDVQQGEwJC +TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UEAxMSUXVvVmFkaXMg +Um9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZVqyM0 +7ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSem +d1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd ++LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B +4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadN +t54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6x +DYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57 +k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6s +zHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0j +Wy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT +mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK +4SVhM7JZG+Ju1zdXtg2pEto= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJC +TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDElMCMGA1UECxMcUm9vdCBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eTEuMCwGA1UEAxMlUXVvVmFkaXMgUm9vdCBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMTAzMTkxODMzMzNaFw0yMTAzMTcxODMz +MzNaMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMSUw +IwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYDVQQDEyVR +dW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv2G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Yp +li4kVEAkOPcahdxYTMukJ0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2D +rOpm2RgbaIr1VxqYuvXtdj182d6UajtLF8HVj71lODqV0D1VNk7feVcxKh7YWWVJ +WCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeLYzcS19Dsw3sgQUSj7cug +F+FxZc4dZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWenAScOospU +xbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuwIDAQABo4ICUjCC +Ak4wPQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwczovL29jc3AucXVv +dmFkaXNvZmZzaG9yZS5jb20wDwYDVR0TAQH/BAUwAwEB/zCCARoGA1UdIASCAREw +ggENMIIBCQYJKwYBBAG+WAABMIH7MIHUBggrBgEFBQcCAjCBxxqBxFJlbGlhbmNl +IG9uIHRoZSBRdW9WYWRpcyBSb290IENlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBh +c3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFy +ZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRpb24gcHJh +Y3RpY2VzLCBhbmQgdGhlIFF1b1ZhZGlzIENlcnRpZmljYXRlIFBvbGljeS4wIgYI +KwYBBQUHAgEWFmh0dHA6Ly93d3cucXVvdmFkaXMuYm0wHQYDVR0OBBYEFItLbe3T +KbkGGew5Oanwl4Rqy+/fMIGuBgNVHSMEgaYwgaOAFItLbe3TKbkGGew5Oanwl4Rq +y+/foYGEpIGBMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1p +dGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYD +VQQDEyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggQ6tlCL +MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAitQUtf70mpKnGdSk +fnIYj9lofFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf8 +7C9TqnN7Az10buYWnuulLsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1R +cHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2xgI4JVrmcGmD+XcHXetwReNDWXcG31a0y +mQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi5upZIof4l/UO/erMkqQW +xFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi5nrQNiOK +SnQ2+Q== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0 +IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAz +BgNVBAsTLFZhbGlDZXJ0IENsYXNzIDMgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9y +aXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG +9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAwMjIzM1oXDTE5MDYy +NjAwMjIzM1owgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0d29y +azEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs +YXNzIDMgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRw +Oi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNl +cnQuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjmFGWHOjVsQaBalfD +cnWTq8+epvzzFlLWLU2fNUSoLgRNB0mKOCn1dzfnt6td3zZxFJmP3MKS8edgkpfs +2Ejcv8ECIMYkpChMMFp2bbFc893enhBxoYjHW5tBbcqwuI4V7q0zK89HBFx1cQqY +JJgpp0lZpd34t0NiYfPT4tBVPwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFa7AliE +Zwgs3x/be0kz9dNnnfS0ChCzycUs4pJqcXgn8nCDQtM+z6lU9PHYkhaM0QTLS6vJ +n0WuPIqpsHEzXcjFV9+vqDWzf4mH6eglkrh/hXqu1rweN1gqZ8mRzyqBPu3GOd/A +PhmcGcwTTYJBtYze4D1gCCAPRX5ron+jjBXu +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICXDCCAcWgAwIBAgIQCgEBAQAAAnwAAAALAAAAAjANBgkqhkiG9w0BAQUFADA6 +MRkwFwYDVQQKExBSU0EgU2VjdXJpdHkgSW5jMR0wGwYDVQQLExRSU0EgU2VjdXJp +dHkgMTAyNCBWMzAeFw0wMTAyMjIyMTAxNDlaFw0yNjAyMjIyMDAxNDlaMDoxGTAX +BgNVBAoTEFJTQSBTZWN1cml0eSBJbmMxHTAbBgNVBAsTFFJTQSBTZWN1cml0eSAx +MDI0IFYzMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDV3f5mCc8kPD6ugU5O +isRpgFtZO9+5TUzKtS3DJy08rwBCbbwoppbPf9dYrIMKo1W1exeQFYRMiu4mmdxY +78c4pqqv0I5CyGLXq6yp+0p9v+r+Ek3d/yYtbzZUaMjShFbuklNhCbM/OZuoyZu9 +zp9+1BlqFikYvtc6adwlWzMaUQIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBTEwBykB5T9zU0B1FTapQxf3q4FWjAd +BgNVHQ4EFgQUxMAcpAeU/c1NAdRU2qUMX96uBVowDQYJKoZIhvcNAQEFBQADgYEA +Py1q4yZDlX2Jl2X7deRyHUZXxGFraZ8SmyzVWujAovBDleMf6XbN3Ou8k6BlCsdN +T1+nr6JGFLkM88y9am63nd4lQtBU/55oc2PcJOsiv6hy8l4A4Q1OOkNumU4/iXgD +mMrzVcydro7BqkWY+o8aoI2II/EVQQ2lRj6RP4vr93E= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDYTCCAkmgAwIBAgIQCgEBAQAAAnwAAAAKAAAAAjANBgkqhkiG9w0BAQUFADA6 +MRkwFwYDVQQKExBSU0EgU2VjdXJpdHkgSW5jMR0wGwYDVQQLExRSU0EgU2VjdXJp +dHkgMjA0OCBWMzAeFw0wMTAyMjIyMDM5MjNaFw0yNjAyMjIyMDM5MjNaMDoxGTAX +BgNVBAoTEFJTQSBTZWN1cml0eSBJbmMxHTAbBgNVBAsTFFJTQSBTZWN1cml0eSAy +MDQ4IFYzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt49VcdKA3Xtp +eafwGFAyPGJn9gqVB93mG/Oe2dJBVGutn3y+Gc37RqtBaB4Y6lXIL5F4iSj7Jylg +/9+PjDvJSZu1pJTOAeo+tWN7fyb9Gd3AIb2E0S1PRsNO3Ng3OTsor8udGuorryGl +wSMiuLgbWhOHV4PR8CDn6E8jQrAApX2J6elhc5SYcSa8LWrg903w8bYqODGBDSnh +AMFRD0xS+ARaqn1y07iHKrtjEAMqs6FPDVpeRrc9DvV07Jmf+T0kgYim3WBU6JU2 +PcYJk5qjEoAAVZkZR73QpXzDuvsf9/UP+Ky5tfQ3mBMY3oVbtwyCO4dvlTlYMNpu +AWgXIszACwIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAfBgNVHSMEGDAWgBQHw1EwpKrpRa41JPr/JCwz0LGdjDAdBgNVHQ4EFgQUB8NR +MKSq6UWuNST6/yQsM9CxnYwwDQYJKoZIhvcNAQEFBQADggEBAF8+hnZuuDU8TjYc +HnmYv/3VEhF5Ug7uMYm83X/50cYVIeiKAVQNOvtUudZj1LGqlk2iQk3UUx+LEN5/ +Zb5gEydxiKRz44Rj0aRV4VCT5hsOedBnvEbIvz8XDZXmxpBp3ue0L96VfdASPz0+ +f00/FGj1EVDVwfSQpQgdMWD/YIwjVAqv/qFuxdF6Kmh4zx6CCiC0H63lhbJqaHVO +rSU3lIW+vaHU6rcMSzyd6BIA8F+sDeGscGNz9395nzIlQnQFgCi/vcEkllgVsRch +6YlL2weIZ/QVrXA+L02FO8K32/6YaCOJ4XQP3vTFhGMpG8zLB8kApKnXwiJPZ9d3 +7CAFYd4= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx +MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg +Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ +iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa +/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ +jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI +HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7 +sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w +gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw +KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG +AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L +URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO +H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm +I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY +iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc +f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz +MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv +cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz +Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO +0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao +wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj +7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS +8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT +BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg +JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3 +6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/ +3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm +D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS +CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR +3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEY +MBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21t +dW5pY2F0aW9uIFJvb3RDQTEwHhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5 +WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYD +VQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw8yl8 +9f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJ +DKaVv0uMDPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9 +Ms+k2Y7CI9eNqPPYJayX5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/N +QV3Is00qVUarH9oe4kA92819uZKAnDfdDJZkndwi92SL32HeFZRSFaB9UslLqCHJ +xrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2JChzAgMBAAGjPzA9MB0G +A1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vG +kl3g0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfr +Uj94nK9NrvjVT8+amCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5 +Bw+SUEmK3TGXX8npN6o7WWWXlDLJs58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJU +JRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ6rBK+1YWc26sTfcioU+tHXot +RSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAiFL39vmwLAw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDIDCCAgigAwIBAgIBJDANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEP +MA0GA1UEChMGU29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MxIENBMB4XDTAx +MDQwNjEwNDkxM1oXDTIxMDQwNjEwNDkxM1owOTELMAkGA1UEBhMCRkkxDzANBgNV +BAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJhIENsYXNzMSBDQTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBALWJHytPZwp5/8Ue+H887dF+2rDNbS82rDTG +29lkFwhjMDMiikzujrsPDUJVyZ0upe/3p4zDq7mXy47vPxVnqIJyY1MPQYx9EJUk +oVqlBvqSV536pQHydekfvFYmUk54GWVYVQNYwBSujHxVX3BbdyMGNpfzJLWaRpXk +3w0LBUXl0fIdgrvGE+D+qnr9aTCU89JFhfzyMlsy3uhsXR/LpCJ0sICOXZT3BgBL +qdReLjVQCfOAl/QMF6452F/NM8EcyonCIvdFEu1eEpOdY6uCLrnrQkFEy0oaAIIN +nvmLVz5MxxftLItyM19yejhW1ebZrgUaHXVFsculJRwSVzb9IjcCAwEAAaMzMDEw +DwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQIR+IMi/ZTiFIwCwYDVR0PBAQDAgEG +MA0GCSqGSIb3DQEBBQUAA4IBAQCLGrLJXWG04bkruVPRsoWdd44W7hE928Jj2VuX +ZfsSZ9gqXLar5V7DtxYvyOirHYr9qxp81V9jz9yw3Xe5qObSIjiHBxTZ/75Wtf0H +DjxVyhbMp6Z3N/vbXB9OWQaHowND9Rart4S9Tu+fMTfwRvFAttEMpWT4Y14h21VO +TzF2nBBhjrZTOqMRvq9tfB69ri3iDGnHhVNoomG6xT60eVR4ngrHAr5i0RGCS2Uv +kVrCqIexVmiUefkl98HVrhq4uz2PqYo4Ffdz0Fpg0YCw8NzVUM1O7pJIae2yIx4w +zMiUyLb1O4Z/P6Yun/Y+LLWSlj7fLJOK/4GMDw9ZIRlXvVWa +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEP +MA0GA1UEChMGU29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAx +MDQwNjA3Mjk0MFoXDTIxMDQwNjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNV +BAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJhIENsYXNzMiBDQTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJAXSjWdyvANlsdE+hY3/Ei9vX+ALTU74W+o +Z6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybTdXnt +5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s +3TmVToMGf+dJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2Ej +vOr7nQKV0ba5cTppCD8PtOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu +8nYybieDwnPz3BjotJPqdURrBGAgcVeHnfO+oJAjPYok4doh28MCAwEAAaMzMDEw +DwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQISqCqWITTXjwwCwYDVR0PBAQDAgEG +MA0GCSqGSIb3DQEBBQUAA4IBAQBazof5FnIVV0sd2ZvnoiYw7JNn39Yt0jSv9zil +zqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/ +3DEIcbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvD +FNr450kkkdAdavphOe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6 +Tk6ezAyNlNzZRZxe7EJQY670XcSxEtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2 +ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLHllpwrN9M +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgIEAJiWijANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJO +TDEeMBwGA1UEChMVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSYwJAYDVQQDEx1TdGFh +dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQTAeFw0wMjEyMTcwOTIzNDlaFw0xNTEy +MTYwOTE1MzhaMFUxCzAJBgNVBAYTAk5MMR4wHAYDVQQKExVTdGFhdCBkZXIgTmVk +ZXJsYW5kZW4xJjAkBgNVBAMTHVN0YWF0IGRlciBOZWRlcmxhbmRlbiBSb290IENB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmNK1URF6gaYUmHFtvszn +ExvWJw56s2oYHLZhWtVhCb/ekBPHZ+7d89rFDBKeNVU+LCeIQGv33N0iYfXCxw71 +9tV2U02PjLwYdjeFnejKScfST5gTCaI+Ioicf9byEGW07l8Y1Rfj+MX94p2i71MO +hXeiD+EwR+4A5zN9RGcaC1Hoi6CeUJhoNFIfLm0B8mBF8jHrqTFoKbt6QZ7GGX+U +tFE5A3+y3qcym7RHjm+0Sq7lr7HcsBthvJly3uSJt3omXdozSVtSnA71iq3DuD3o +BmrC1SoLbHuEvVYFy4ZlkuxEK7COudxwC0barbxjiDn622r+I/q85Ej0ZytqERAh +SQIDAQABo4GRMIGOMAwGA1UdEwQFMAMBAf8wTwYDVR0gBEgwRjBEBgRVHSAAMDww +OgYIKwYBBQUHAgEWLmh0dHA6Ly93d3cucGtpb3ZlcmhlaWQubmwvcG9saWNpZXMv +cm9vdC1wb2xpY3kwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSofeu8Y6R0E3QA +7Jbg0zTBLL9s+DANBgkqhkiG9w0BAQUFAAOCAQEABYSHVXQ2YcG70dTGFagTtJ+k +/rvuFbQvBgwp8qiSpGEN/KtcCFtREytNwiphyPgJWPwtArI5fZlmgb9uXJVFIGzm +eafR2Bwp/MIgJ1HI8XxdNGdphREwxgDS1/PTfLbwMVcoEoJz6TMvplW0C5GUR5z6 +u3pCMuiufi3IvKwUv9kP2Vv8wfl6leF9fpb8cbDCTMjfRTTJzg3ynGQI0DvDKcWy +7ZAEwbEpkcUwb8GpcjPM/l0WFywRaed+/sWDCN+83CI6LiBpIzlWYGeQiy52OfsR +iJf2fL1LuCAWZwWN4jvBcj+UlTfHXbme2JOhF4//DGYVwSR8MnwDHTuhWEUykw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl +MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp +U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw +NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE +ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp +ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3 +DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf +8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN ++lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0 +X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa +K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA +1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G +A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR +zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0 +YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD +bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w +DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3 +L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D +eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl +xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp +VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY +WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEW +MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg +Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM2WhcNMzYwOTE3MTk0NjM2WjB9 +MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi +U2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh +cnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk +pMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf +OQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C +Ji/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT +Kqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi +HzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM +Av+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w ++2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+ +Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3 +Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B +26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID +AQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE +FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9j +ZXJ0LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3Js +LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFM +BgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUHAgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0 +Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRwOi8vY2VydC5zdGFy +dGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYgU3Rh +cnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlh +YmlsaXR5LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2Yg +dGhlIFN0YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFp +bGFibGUgYXQgaHR0cDovL2NlcnQuc3RhcnRjb20ub3JnL3BvbGljeS5wZGYwEQYJ +YIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNT +TCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOCAgEAFmyZ +9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8 +jhvh3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUW +FjgKXlf2Ysd6AgXmvB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJz +ewT4F+irsfMuXGRuczE6Eri8sxHkfY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1 +ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3fsNrarnDy0RLrHiQi+fHLB5L +EUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZEoalHmdkrQYu +L6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq +yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuC +O3NJo2pXh5Tl1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6V +um0ABj6y6koQOdjQK/W/7HW/lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkySh +NOsF/5oirpt9P/FlUQqmMGqz9IgcgA38corog14= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFFjCCBH+gAwIBAgIBADANBgkqhkiG9w0BAQQFADCBsDELMAkGA1UEBhMCSUwx +DzANBgNVBAgTBklzcmFlbDEOMAwGA1UEBxMFRWlsYXQxFjAUBgNVBAoTDVN0YXJ0 +Q29tIEx0ZC4xGjAYBgNVBAsTEUNBIEF1dGhvcml0eSBEZXAuMSkwJwYDVQQDEyBG +cmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJARYS +YWRtaW5Ac3RhcnRjb20ub3JnMB4XDTA1MDMxNzE3Mzc0OFoXDTM1MDMxMDE3Mzc0 +OFowgbAxCzAJBgNVBAYTAklMMQ8wDQYDVQQIEwZJc3JhZWwxDjAMBgNVBAcTBUVp +bGF0MRYwFAYDVQQKEw1TdGFydENvbSBMdGQuMRowGAYDVQQLExFDQSBBdXRob3Jp +dHkgRGVwLjEpMCcGA1UEAxMgRnJlZSBTU0wgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkxITAfBgkqhkiG9w0BCQEWEmFkbWluQHN0YXJ0Y29tLm9yZzCBnzANBgkqhkiG +9w0BAQEFAAOBjQAwgYkCgYEA7YRgACOeyEpRKSfeOqE5tWmrCbIvNP1h3D3TsM+x +18LEwrHkllbEvqoUDufMOlDIOmKdw6OsWXuO7lUaHEe+o5c5s7XvIywI6Nivcy+5 +yYPo7QAPyHWlLzRMGOh2iCNJitu27Wjaw7ViKUylS7eYtAkUEKD4/mJ2IhULpNYI +LzUCAwEAAaOCAjwwggI4MA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgHmMB0G +A1UdDgQWBBQcicOWzL3+MtUNjIExtpidjShkjTCB3QYDVR0jBIHVMIHSgBQcicOW +zL3+MtUNjIExtpidjShkjaGBtqSBszCBsDELMAkGA1UEBhMCSUwxDzANBgNVBAgT +BklzcmFlbDEOMAwGA1UEBxMFRWlsYXQxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4x +GjAYBgNVBAsTEUNBIEF1dGhvcml0eSBEZXAuMSkwJwYDVQQDEyBGcmVlIFNTTCBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJARYSYWRtaW5Ac3Rh +cnRjb20ub3JnggEAMB0GA1UdEQQWMBSBEmFkbWluQHN0YXJ0Y29tLm9yZzAdBgNV +HRIEFjAUgRJhZG1pbkBzdGFydGNvbS5vcmcwEQYJYIZIAYb4QgEBBAQDAgAHMC8G +CWCGSAGG+EIBDQQiFiBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAy +BglghkgBhvhCAQQEJRYjaHR0cDovL2NlcnQuc3RhcnRjb20ub3JnL2NhLWNybC5j +cmwwKAYJYIZIAYb4QgECBBsWGWh0dHA6Ly9jZXJ0LnN0YXJ0Y29tLm9yZy8wOQYJ +YIZIAYb4QgEIBCwWKmh0dHA6Ly9jZXJ0LnN0YXJ0Y29tLm9yZy9pbmRleC5waHA/ +YXBwPTExMTANBgkqhkiG9w0BAQQFAAOBgQBscSXhnjSRIe/bbL0BCFaPiNhBOlP1 +ct8nV0t2hPdopP7rPwl+KLhX6h/BquL/lp9JmeaylXOWxkjHXo0Hclb4g4+fd68p +00UOpO6wNnQt8M2YI3s3S9r+UZjEHjQ8iP2ZO1CnwYszx8JSFhKVU2Ui77qLzmLb +cCOxgN8aIDjnfg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF2TCCA8GgAwIBAgIQXAuFXAvnWUHfV8w/f52oNjANBgkqhkiG9w0BAQUFADBk +MQswCQYDVQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0 +YWwgQ2VydGlmaWNhdGUgU2VydmljZXMxGzAZBgNVBAMTElN3aXNzY29tIFJvb3Qg +Q0EgMTAeFw0wNTA4MTgxMjA2MjBaFw0yNTA4MTgyMjA2MjBaMGQxCzAJBgNVBAYT +AmNoMREwDwYDVQQKEwhTd2lzc2NvbTElMCMGA1UECxMcRGlnaXRhbCBDZXJ0aWZp +Y2F0ZSBTZXJ2aWNlczEbMBkGA1UEAxMSU3dpc3Njb20gUm9vdCBDQSAxMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0LmwqAzZuz8h+BvVM5OAFmUgdbI9 +m2BtRsiMMW8Xw/qabFbtPMWRV8PNq5ZJkCoZSx6jbVfd8StiKHVFXqrWW/oLJdih +FvkcxC7mlSpnzNApbjyFNDhhSbEAn9Y6cV9Nbc5fuankiX9qUvrKm/LcqfmdmUc/ +TilftKaNXXsLmREDA/7n29uj/x2lzZAeAR81sH8A25Bvxn570e56eqeqDFdvpG3F +EzuwpdntMhy0XmeLVNxzh+XTF3xmUHJd1BpYwdnP2IkCb6dJtDZd0KTeByy2dbco +kdaXvij1mB7qWybJvbCXc9qukSbraMH5ORXWZ0sKbU/Lz7DkQnGMU3nn7uHbHaBu +HYwadzVcFh4rUx80i9Fs/PJnB3r1re3WmquhsUvhzDdf/X/NTa64H5xD+SpYVUNF +vJbNcA78yeNmuk6NO4HLFWR7uZToXTNShXEuT46iBhFRyePLoW4xCGQMwtI89Tbo +19AOeCMgkckkKmUpWyL3Ic6DXqTz3kvTaI9GdVyDCW4pa8RwjPWd1yAv/0bSKzjC +L3UcPX7ape8eYIVpQtPM+GP+HkM5haa2Y0EQs3MevNP6yn0WR+Kn1dCjigoIlmJW +bjTb2QK5MHXjBNLnj8KwEUAKrNVxAmKLMb7dxiNYMUJDLXT5xp6mig/p/r+D5kNX +JLrvRjSq1xIBOO0CAwEAAaOBhjCBgzAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0hBBYw +FDASBgdghXQBUwABBgdghXQBUwABMBIGA1UdEwEB/wQIMAYBAf8CAQcwHwYDVR0j +BBgwFoAUAyUv3m+CATpcLNwroWm1Z9SM0/0wHQYDVR0OBBYEFAMlL95vggE6XCzc +K6FptWfUjNP9MA0GCSqGSIb3DQEBBQUAA4ICAQA1EMvspgQNDQ/NwNurqPKIlwzf +ky9NfEBWMXrrpA9gzXrzvsMnjgM+pN0S734edAY8PzHyHHuRMSG08NBsl9Tpl7Ik +Vh5WwzW9iAUPWxAaZOHHgjD5Mq2eUCzneAXQMbFamIp1TpBcahQq4FJHgmDmHtqB +sfsUC1rxn9KVuj7QG9YVHaO+htXbD8BJZLsuUBlL0iT43R4HVtA4oJVwIHaM190e +3p9xxCPvgxNcoyQVTSlAPGrEqdi3pkSlDfTgnXceQHAm/NrZNuR55LU/vJtlvrsR +ls/bxig5OgjOR1tTWsWZ/l2p3e9M1MalrQLmjAcSHm8D0W+go/MpvRLHUKKwf4ip +mXeascClOS5cfGniLLDqN2qk4Vrh9VDlg++luyqI54zb/W1elxmofmZ1a3Hqv7HH +b6D0jqTsNFFbjCYDcKF31QESVwA12yPeDooomf2xEG9L/zgtYE4snOtnta1J7ksf +rK/7DZBaZmBwXarNeNQk7shBoJMBkpxqnvy5JMWzFYJ+vq6VK+uxwNrjAWALXmms +hFZhvnEX/h0TD/7Gh0Xp/jKgGg0TpJRVcaUWi7rKibCyx/yP2FS1k2Kdzs9Z+z0Y +zirLNRWCXf9UIltxUvu3yf5gmwBBZPCqKuy2QkPOiWaByIufOVQDJdMWNY6E0F/6 +MBr1mmz0DlP5OlvRHA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFwTCCA6mgAwIBAgIITrIAZwwDXU8wDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE +BhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEjMCEGA1UEAxMaU3dpc3NTaWdu +IFBsYXRpbnVtIENBIC0gRzIwHhcNMDYxMDI1MDgzNjAwWhcNMzYxMDI1MDgzNjAw +WjBJMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMSMwIQYDVQQD +ExpTd2lzc1NpZ24gUGxhdGludW0gQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAMrfogLi2vj8Bxax3mCq3pZcZB/HL37PZ/pEQtZ2Y5Wu669y +IIpFR4ZieIbWIDkm9K6j/SPnpZy1IiEZtzeTIsBQnIJ71NUERFzLtMKfkr4k2Htn +IuJpX+UFeNSH2XFwMyVTtIc7KZAoNppVRDBopIOXfw0enHb/FZ1glwCNioUD7IC+ +6ixuEFGSzH7VozPY1kneWCqv9hbrS3uQMpe5up1Y8fhXSQQeol0GcN1x2/ndi5ob +jM89o03Oy3z2u5yg+gnOI2Ky6Q0f4nIoj5+saCB9bzuohTEJfwvH6GXp43gOCWcw +izSC+13gzJ2BbWLuCB4ELE6b7P6pT1/9aXjvCR+htL/68++QHkwFix7qepF6w9fl ++zC8bBsQWJj3Gl/QKTIDE0ZNYWqFTFJ0LwYfexHihJfGmfNtf9dng34TaNhxKFrY +zt3oEBSa/m0jh26OWnA81Y0JAKeqvLAxN23IhBQeW71FYyBrS3SMvds6DsHPWhaP +pZjydomyExI7C3d3rLvlPClKknLKYRorXkzig3R3+jVIeoVNjZpTxN94ypeRSCtF +KwH3HBqi7Ri6Cr2D+m+8jVeTO9TUps4e8aCxzqv9KyiaTxvXw3LbpMS/XUz13XuW +ae5ogObnmLo2t/5u7Su9IPhlGdpVCX4l3P5hYnL5fhgC72O00Puv5TtjjGePAgMB +AAGjgawwgakwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFFCvzAeHFUdvOMW0ZdHelarp35zMMB8GA1UdIwQYMBaAFFCvzAeHFUdvOMW0 +ZdHelarp35zMMEYGA1UdIAQ/MD0wOwYJYIV0AVkBAQEBMC4wLAYIKwYBBQUHAgEW +IGh0dHA6Ly9yZXBvc2l0b3J5LnN3aXNzc2lnbi5jb20vMA0GCSqGSIb3DQEBBQUA +A4ICAQAIhab1Fgz8RBrBY+D5VUYI/HAcQiiWjrfFwUF1TglxeeVtlspLpYhg0DB0 +uMoI3LQwnkAHFmtllXcBrqS3NQuB2nEVqXQXOHtYyvkv+8Bldo1bAbl93oI9ZLi+ +FHSjClTTLJUYFzX1UWs/j6KWYTl4a0vlpqD4U99REJNi54Av4tHgvI42Rncz7Lj7 +jposiU0xEQ8mngS7twSNC/K5/FqdOxa3L8iYq/6KUFkuozv8KV2LwUvJ4ooTHbG/ +u0IdUt1O2BReEMYxB+9xJ/cbOQncguqLs5WGXv312l0xpuAxtpTmREl0xRbl9x8D +YSjFyMsSoEJL+WuICI20MhjzdZ/EfwBPBZWcoxcCw7NTm6ogOSkrZvqdr16zktK1 +puEa+S1BaYEUtLS17Yk9zvupnTVCRLEcFHOBzyoBNZox1S2PbYTfgE1X4z/FhHXa +icYwu+uPyyIIoK6q8QNsOktNCaUOcsZWayFCTiMlFGiudgp8DAdwZPmaL/YFOSbG +DI8Zf0NebvRbFS/bYV3mZy8/CJT5YLSYMdp08YSTcU1f+2BY0fvEwW2JorsgH51x +kcsymxM9Pn2SUjWskpSi0xjCfMfqr3YFFt1nJ8J+HAciIfNAChs0B0QTwoRqjt8Z +Wr9/6x3iGjjRXK9HkmuAtTClyY3YqzGBH9/CZjfTk6mFhnll0g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIQH51ZWtcvwgZEpYAIaeNe9jANBgkqhkiG9w0BAQUFADA/ +MQswCQYDVQQGEwJUVzEwMC4GA1UECgwnR292ZXJubWVudCBSb290IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5MB4XDTAyMTIwNTEzMjMzM1oXDTMyMTIwNTEzMjMzM1ow +PzELMAkGA1UEBhMCVFcxMDAuBgNVBAoMJ0dvdmVybm1lbnQgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +AJoluOzMonWoe/fOW1mKydGGEghU7Jzy50b2iPN86aXfTEc2pBsBHH8eV4qNw8XR +IePaJD9IK/ufLqGU5ywck9G/GwGHU5nOp/UKIXZ3/6m3xnOUT0b3EEk3+qhZSV1q +gQdW8or5BtD3cCJNtLdBuTK4sfCxw5w/cP1T3YGq2GN49thTbqGsaoQkclSGxtKy +yhwOeYHWtXBiCAEuTk8O1RGvqa/lmr/czIdtJuTJV6L7lvnM4T9TjGxMfptTCAts +F/tnyMKtsc2AtJfcdgEWFelq16TheEfOhtX7MfP6Mb40qij7cEwdScevLJ1tZqa2 +jWR+tSBqnTuBto9AAGdLiYa4zGX+FVPpBMHWXx1E1wovJ5pGfaENda1UhhXcSTvx +ls4Pm6Dso3pdvtUqdULle96ltqqvKKyskKw4t9VoNSZ63Pc78/1Fm9G7Q3hub/FC +VGqY8A2tl+lSXunVanLeavcbYBT0peS2cWeqH+riTcFCQP5nRhc4L0c/cZyu5SHK +YS1tB6iEfC3uUSXxY5Ce/eFXiGvviiNtsea9P63RPZYLhY3Naye7twWb7LuRqQoH +EgKXTiCQ8P8NHuJBO9NAOueNXdpm5AKwB1KYXA6OM5zCppX7VRluTI6uSw+9wThN +Xo+EHWbNxWCWtFJaBYmOlXqYwZE8lSOyDvR5tMl8wUohAgMBAAGjajBoMB0GA1Ud +DgQWBBTMzO/MKWCkO7GStjz6MmKPrCUVOzAMBgNVHRMEBTADAQH/MDkGBGcqBwAE +MTAvMC0CAQAwCQYFKw4DAhoFADAHBgVnKgMAAAQUA5vwIhP/lSg209yewDL7MTqK +UWUwDQYJKoZIhvcNAQEFBQADggIBAECASvomyc5eMN1PhnR2WPWus4MzeKR6dBcZ +TulStbngCnRiqmjKeKBMmo4sIy7VahIkv9Ro04rQ2JyftB8M3jh+Vzj8jeJPXgyf +qzvS/3WXy6TjZwj/5cAWtUgBfen5Cv8b5Wppv3ghqMKnI6mGq3ZW6A4M9hPdKmaK +ZEk9GhiHkASfQlK3T8v+R0F2Ne//AHY2RTKbxkaFXeIksB7jSJaYV0eUVXoPQbFE +JPPB/hprv4j9wabak2BegUqZIJxIZhm1AHlUD7gsL0u8qV1bYH+Mh6XgUmMqvtg7 +hUAV/h62ZT/FS9p+tXo1KaMuephgIqP0fSdOLeq0dDzpD6QzDxARvBMB1uUO07+1 +EqLhRSPAzAhuYbeJq4PjJB7mXQfnHyA+z2fI56wwbSdLaG5LKlwCCDTb+HbkZ6Mm +nD+iMsJKxYEYMRBWqoTvLQr/uB930r+lWKBi5NdLkXWNiYCYfm3LU05er/ayl4WX +udpVBrkk7tfGOB5jGxI7leFYrPLfhNVfmS8NVVvmONsuP3LpSIXLuykTjx44Vbnz +ssQwmSNOXfJIoRIM3BKQCZBUkQM8R+XVyWXgt0t97EfTsws+rZ7QdAAO671RrcDe +LMDDav7v3Aun+kbfYNucpllQdSNpc5Oy+fwC00fmcc4QAu4njIT/rEUNE1yDMuAl +pYYsfPQS +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDXDCCAsWgAwIBAgICA+owDQYJKoZIhvcNAQEEBQAwgbwxCzAJBgNVBAYTAkRF +MRAwDgYDVQQIEwdIYW1idXJnMRAwDgYDVQQHEwdIYW1idXJnMTowOAYDVQQKEzFU +QyBUcnVzdENlbnRlciBmb3IgU2VjdXJpdHkgaW4gRGF0YSBOZXR3b3JrcyBHbWJI +MSIwIAYDVQQLExlUQyBUcnVzdENlbnRlciBDbGFzcyAyIENBMSkwJwYJKoZIhvcN +AQkBFhpjZXJ0aWZpY2F0ZUB0cnVzdGNlbnRlci5kZTAeFw05ODAzMDkxMTU5NTla +Fw0xMTAxMDExMTU5NTlaMIG8MQswCQYDVQQGEwJERTEQMA4GA1UECBMHSGFtYnVy +ZzEQMA4GA1UEBxMHSGFtYnVyZzE6MDgGA1UEChMxVEMgVHJ1c3RDZW50ZXIgZm9y +IFNlY3VyaXR5IGluIERhdGEgTmV0d29ya3MgR21iSDEiMCAGA1UECxMZVEMgVHJ1 +c3RDZW50ZXIgQ2xhc3MgMiBDQTEpMCcGCSqGSIb3DQEJARYaY2VydGlmaWNhdGVA +dHJ1c3RjZW50ZXIuZGUwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANo46O0y +AClxgwENv4wB3NrGrTmkqYov1YtcaF9QxmL1Zr3KkSLsqh1R1z2zUbKDTl3LSbDw +TFXlay3HhQswHJJOgtTKAu33b77c4OMUuAVT8pr0VotanoWT0bSCVq5Nu6hLVxa8 +/vhYnvgpjbB7zXjJT6yLZwzxnPv8V5tXXE8NAgMBAAGjazBpMA8GA1UdEwEB/wQF +MAMBAf8wDgYDVR0PAQH/BAQDAgGGMDMGCWCGSAGG+EIBCAQmFiRodHRwOi8vd3d3 +LnRydXN0Y2VudGVyLmRlL2d1aWRlbGluZXMwEQYJYIZIAYb4QgEBBAQDAgAHMA0G +CSqGSIb3DQEBBAUAA4GBAIRS+yjf/x91AbwBvgRWl2p0QiQxg/lGsQaKic+WLDO/ +jLVfenKhhQbOhvgFjuj5Jcrag4wGrOs2bYWRNAQ29ELw+HkuCkhcq8xRT3h2oNms +Gb0q0WkEKJHKNhAngFdb0lz1wlurZIFjdFH0l7/NEij3TWZ/p/AcASZ4smZHcFFk +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDXDCCAsWgAwIBAgICA+swDQYJKoZIhvcNAQEEBQAwgbwxCzAJBgNVBAYTAkRF +MRAwDgYDVQQIEwdIYW1idXJnMRAwDgYDVQQHEwdIYW1idXJnMTowOAYDVQQKEzFU +QyBUcnVzdENlbnRlciBmb3IgU2VjdXJpdHkgaW4gRGF0YSBOZXR3b3JrcyBHbWJI +MSIwIAYDVQQLExlUQyBUcnVzdENlbnRlciBDbGFzcyAzIENBMSkwJwYJKoZIhvcN +AQkBFhpjZXJ0aWZpY2F0ZUB0cnVzdGNlbnRlci5kZTAeFw05ODAzMDkxMTU5NTla +Fw0xMTAxMDExMTU5NTlaMIG8MQswCQYDVQQGEwJERTEQMA4GA1UECBMHSGFtYnVy +ZzEQMA4GA1UEBxMHSGFtYnVyZzE6MDgGA1UEChMxVEMgVHJ1c3RDZW50ZXIgZm9y +IFNlY3VyaXR5IGluIERhdGEgTmV0d29ya3MgR21iSDEiMCAGA1UECxMZVEMgVHJ1 +c3RDZW50ZXIgQ2xhc3MgMyBDQTEpMCcGCSqGSIb3DQEJARYaY2VydGlmaWNhdGVA +dHJ1c3RjZW50ZXIuZGUwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALa0wTUF +Lg2N7KBAahwOJ6ZQkmtQGwfeLud2zODa/ISoXoxjaitN2U4CdhHBC/KNecoAtvGw +Dtf7pBc9r6tpepYnv68zoZoqWarEtTcI8hKlMbZD9TKWcSgoq40oht+77uMMfTDW +w1Krj10nnGvAo+cFa1dJRLNu6mTP0o56UHd3AgMBAAGjazBpMA8GA1UdEwEB/wQF +MAMBAf8wDgYDVR0PAQH/BAQDAgGGMDMGCWCGSAGG+EIBCAQmFiRodHRwOi8vd3d3 +LnRydXN0Y2VudGVyLmRlL2d1aWRlbGluZXMwEQYJYIZIAYb4QgEBBAQDAgAHMA0G +CSqGSIb3DQEBBAUAA4GBABY9xs3Bu4VxhUafPiCPUSiZ7C1FIWMjWwS7TJC4iJIE +Tb19AaM/9uzO8d7+feXhPrvGq14L3T2WxMup1Pkm5gZOngylerpuw3yCGdHHsbHD +2w2Om0B8NwvxXej9H5CIpQ5ON2QhqE6NtJ/x3kit1VYYUimLRzQSCdS7kjXvD9s0 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEKzCCAxOgAwIBAgIEOsylTDANBgkqhkiG9w0BAQUFADBDMQswCQYDVQQGEwJE +SzEVMBMGA1UEChMMVERDIEludGVybmV0MR0wGwYDVQQLExRUREMgSW50ZXJuZXQg +Um9vdCBDQTAeFw0wMTA0MDUxNjMzMTdaFw0yMTA0MDUxNzAzMTdaMEMxCzAJBgNV +BAYTAkRLMRUwEwYDVQQKEwxUREMgSW50ZXJuZXQxHTAbBgNVBAsTFFREQyBJbnRl +cm5ldCBSb290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxLhA +vJHVYx/XmaCLDEAedLdInUaMArLgJF/wGROnN4NrXceO+YQwzho7+vvOi20jxsNu +Zp+Jpd/gQlBn+h9sHvTQBda/ytZO5GhgbEaqHF1j4QeGDmUApy6mcca8uYGoOn0a +0vnRrEvLznWv3Hv6gXPU/Lq9QYjUdLP5Xjg6PEOo0pVOd20TDJ2PeAG3WiAfAzc1 +4izbSysseLlJ28TQx5yc5IogCSEWVmb/Bexb4/DPqyQkXsN/cHoSxNK1EKC2IeGN +eGlVRGn1ypYcNIUXJXfi9i8nmHj9eQY6otZaQ8H/7AQ77hPv01ha/5Lr7K7a8jcD +R0G2l8ktCkEiu7vmpwIDAQABo4IBJTCCASEwEQYJYIZIAYb4QgEBBAQDAgAHMGUG +A1UdHwReMFwwWqBYoFakVDBSMQswCQYDVQQGEwJESzEVMBMGA1UEChMMVERDIElu +dGVybmV0MR0wGwYDVQQLExRUREMgSW50ZXJuZXQgUm9vdCBDQTENMAsGA1UEAxME +Q1JMMTArBgNVHRAEJDAigA8yMDAxMDQwNTE2MzMxN1qBDzIwMjEwNDA1MTcwMzE3 +WjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUbGQBx/2FbazI2p5QCIUItTxWqFAw +HQYDVR0OBBYEFGxkAcf9hW2syNqeUAiFCLU8VqhQMAwGA1UdEwQFMAMBAf8wHQYJ +KoZIhvZ9B0EABBAwDhsIVjUuMDo0LjADAgSQMA0GCSqGSIb3DQEBBQUAA4IBAQBO +Q8zR3R0QGwZ/t6T609lN+yOfI1Rb5osvBCiLtSdtiaHsmGnc540mgwV5dOy0uaOX +wTUA/RXaOYE6lTGQ3pfphqiZdwzlWqCE/xIWrG64jcN7ksKsLtB9KOy282A4aW8+ +2ARVPp7MVdK6/rtHBNcK2RYKNCn1WBPVT8+PVkuzHu7TmHnaCB4Mb7j4Fifvwm89 +9qNLPg7kbWzbO0ESm70NRyN/PErQr8Cv9u8btRXE64PECV90i9kR+8JWsTz4cMo0 +jUNAE4z9mQNUecYu6oah9jrUCbz0vGbMPVjQV0kK7iXiQe4T+Zs4NNEA9X7nlB38 +aQNiuJkFBT1reBK9sG9l +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFGTCCBAGgAwIBAgIEPki9xDANBgkqhkiG9w0BAQUFADAxMQswCQYDVQQGEwJE +SzEMMAoGA1UEChMDVERDMRQwEgYDVQQDEwtUREMgT0NFUyBDQTAeFw0wMzAyMTEw +ODM5MzBaFw0zNzAyMTEwOTA5MzBaMDExCzAJBgNVBAYTAkRLMQwwCgYDVQQKEwNU +REMxFDASBgNVBAMTC1REQyBPQ0VTIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEArGL2YSCyz8DGhdfjeebM7fI5kqSXLmSjhFuHnEz9pPPEXyG9VhDr +2y5h7JNp46PMvZnDBfwGuMo2HP6QjklMxFaaL1a8z3sM8W9Hpg1DTeLpHTk0zY0s +2RKY+ePhwUp8hjjEqcRhiNJerxomTdXkoCJHhNlktxmW/OwZ5LKXJk5KTMuPJItU +GBxIYXvViGjaXbXqzRowwYCDdlCqT9HU3Tjw7xb04QxQBr/q+3pJoSgrHPb8FTKj +dGqPqcNiKXEx5TukYBdedObaE+3pHx8b0bJoc8YQNHVGEBDjkAB2QMuLt0MJIf+r +TpPGWOmlgtt3xDqZsXKVSQTwtyv6e1mO3QIDAQABo4ICNzCCAjMwDwYDVR0TAQH/ +BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwgewGA1UdIASB5DCB4TCB3gYIKoFQgSkB +AQEwgdEwLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuY2VydGlmaWthdC5kay9yZXBv +c2l0b3J5MIGdBggrBgEFBQcCAjCBkDAKFgNUREMwAwIBARqBgUNlcnRpZmlrYXRl +ciBmcmEgZGVubmUgQ0EgdWRzdGVkZXMgdW5kZXIgT0lEIDEuMi4yMDguMTY5LjEu +MS4xLiBDZXJ0aWZpY2F0ZXMgZnJvbSB0aGlzIENBIGFyZSBpc3N1ZWQgdW5kZXIg +T0lEIDEuMi4yMDguMTY5LjEuMS4xLjARBglghkgBhvhCAQEEBAMCAAcwgYEGA1Ud +HwR6MHgwSKBGoESkQjBAMQswCQYDVQQGEwJESzEMMAoGA1UEChMDVERDMRQwEgYD +VQQDEwtUREMgT0NFUyBDQTENMAsGA1UEAxMEQ1JMMTAsoCqgKIYmaHR0cDovL2Ny +bC5vY2VzLmNlcnRpZmlrYXQuZGsvb2Nlcy5jcmwwKwYDVR0QBCQwIoAPMjAwMzAy +MTEwODM5MzBagQ8yMDM3MDIxMTA5MDkzMFowHwYDVR0jBBgwFoAUYLWF7FZkfhIZ +J2cdUBVLc647+RIwHQYDVR0OBBYEFGC1hexWZH4SGSdnHVAVS3OuO/kSMB0GCSqG +SIb2fQdBAAQQMA4bCFY2LjA6NC4wAwIEkDANBgkqhkiG9w0BAQUFAAOCAQEACrom +JkbTc6gJ82sLMJn9iuFXehHTuJTXCRBuo7E4A9G28kNBKWKnctj7fAXmMXAnVBhO +inxO5dHKjHiIzxvTkIvmI/gLDjNDfZziChmPyQE+dF10yYscA+UYyAFMP8uXBV2Y +caaYb7Z8vTd/vuGTJW1v8AqtFxjhA7wHKcitJuj4YfD9IQl+mo6paH1IYnK9AOoB +mbgGglGBTvH1tJFUuSN6AJqfXY3gPGS5GhKSKseCRHI53OI8xthV9RVOyAUO28bQ +YqbsFbS1AoLbrIyigfCbmTH1ICCoiGEKB5+U/NDXG8wuF/MEJ3Zn61SD/aSQfgY9 +BKNDLdr8C2LqL19iUw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDITCCAoqgAwIBAgIBADANBgkqhkiG9w0BAQQFADCByzELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMRowGAYD +VQQKExFUaGF3dGUgQ29uc3VsdGluZzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBT +ZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UEAxMYVGhhd3RlIFBlcnNvbmFsIEJhc2lj +IENBMSgwJgYJKoZIhvcNAQkBFhlwZXJzb25hbC1iYXNpY0B0aGF3dGUuY29tMB4X +DTk2MDEwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgcsxCzAJBgNVBAYTAlpBMRUw +EwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEaMBgGA1UE +ChMRVGhhd3RlIENvbnN1bHRpbmcxKDAmBgNVBAsTH0NlcnRpZmljYXRpb24gU2Vy +dmljZXMgRGl2aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQZXJzb25hbCBCYXNpYyBD +QTEoMCYGCSqGSIb3DQEJARYZcGVyc29uYWwtYmFzaWNAdGhhd3RlLmNvbTCBnzAN +BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAvLyTU23AUE+CFeZIlDWmWr5vQvoPR+53 +dXLdjUmbllegeNTKP1GzaQuRdhciB5dqxFGTS+CN7zeVoQxN2jSQHReJl+A1OFdK +wPQIcOk8RHtQfmGakOMj04gRRif1CwcOu93RfyAKiLlWCy4cgNrx454p7xS9CkT7 +G1sY0b8jkyECAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQQF +AAOBgQAt4plrsD16iddZopQBHyvdEktTwq1/qqcAXJFAVyVKOKqEcLnZgA+le1z7 +c8a914phXAPjLSeoF+CEhULcXpvGt7Jtu3Sv5D/Lp7ew4F2+eIMllNLbgQ95B21P +9DkVWlIBe94y1k049hJcBlDfBVu9FEuh3ym6O0GN92NWod8isQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDLTCCApagAwIBAgIBADANBgkqhkiG9w0BAQQFADCB0TELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMRowGAYD +VQQKExFUaGF3dGUgQ29uc3VsdGluZzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBT +ZXJ2aWNlcyBEaXZpc2lvbjEkMCIGA1UEAxMbVGhhd3RlIFBlcnNvbmFsIEZyZWVt +YWlsIENBMSswKQYJKoZIhvcNAQkBFhxwZXJzb25hbC1mcmVlbWFpbEB0aGF3dGUu +Y29tMB4XDTk2MDEwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgdExCzAJBgNVBAYT +AlpBMRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEa +MBgGA1UEChMRVGhhd3RlIENvbnN1bHRpbmcxKDAmBgNVBAsTH0NlcnRpZmljYXRp +b24gU2VydmljZXMgRGl2aXNpb24xJDAiBgNVBAMTG1RoYXd0ZSBQZXJzb25hbCBG +cmVlbWFpbCBDQTErMCkGCSqGSIb3DQEJARYccGVyc29uYWwtZnJlZW1haWxAdGhh +d3RlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1GnX1LCUZFtx6UfY +DFG26nKRsIRefS0Nj3sS34UldSh0OkIsYyeflXtL734Zhx2G6qPduc6WZBrCFG5E +rHzmj+hND3EfQDimAKOHePb5lIZererAXnbr2RSjXW56fAylS1V/Bhkpf56aJtVq +uzgkCGqYx7Hao5iR/Xnb5VrEHLkCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zAN +BgkqhkiG9w0BAQQFAAOBgQDH7JJ+Tvj1lqVnYiqk8E0RYNBvjWBYYawmu1I1XAjP +MPuoSpaKH2JCI4wXD/S6ZJwXrEcp352YXtJsYHFcoqzceePnbgBHH7UNKOgCneSa +/RP0ptl8sfjcXyMmCZGAc9AUG95DqYMl8uacLxXK/qarigd1iwzdUYRr5PjRznei +gQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDKTCCApKgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBzzELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMRowGAYD +VQQKExFUaGF3dGUgQ29uc3VsdGluZzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBT +ZXJ2aWNlcyBEaXZpc2lvbjEjMCEGA1UEAxMaVGhhd3RlIFBlcnNvbmFsIFByZW1p +dW0gQ0ExKjAoBgkqhkiG9w0BCQEWG3BlcnNvbmFsLXByZW1pdW1AdGhhd3RlLmNv +bTAeFw05NjAxMDEwMDAwMDBaFw0yMDEyMzEyMzU5NTlaMIHPMQswCQYDVQQGEwJa +QTEVMBMGA1UECBMMV2VzdGVybiBDYXBlMRIwEAYDVQQHEwlDYXBlIFRvd24xGjAY +BgNVBAoTEVRoYXd0ZSBDb25zdWx0aW5nMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9u +IFNlcnZpY2VzIERpdmlzaW9uMSMwIQYDVQQDExpUaGF3dGUgUGVyc29uYWwgUHJl +bWl1bSBDQTEqMCgGCSqGSIb3DQEJARYbcGVyc29uYWwtcHJlbWl1bUB0aGF3dGUu +Y29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJZtn4B0TPuYwu8KHvE0Vs +Bd/eJxZRNkERbGw77f4QfRKe5ZtCmv5gMcNmt3M6SK5O0DI3lIi1DbbZ8/JE2dWI +Et12TfIa/G8jHnrx2JhFTgcQ7xZC0EN1bUre4qrJMf8fAHB8Zs8QJQi6+u4A6UYD +ZicRFTuqW/KY3TZCstqIdQIDAQABoxMwETAPBgNVHRMBAf8EBTADAQH/MA0GCSqG +SIb3DQEBBAUAA4GBAGk2ifc0KjNyL2071CKyuG+axTZmDhs8obF1Wub9NdP4qPIH +b4Vnjt4rueIXsDqg8A6iAJrf8xQVbrvIhVqYgPn/vnQdPfP+MCXRNzRn+qVxeTBh +KXLA4CxM+1bkOqhv5TJZUtt1KFBZDPgLGeSs2a+WjS9Q2wfD6h+rM+D1KzGJ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDJzCCApCgAwIBAgIBATANBgkqhkiG9w0BAQQFADCBzjELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD +VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UEAxMYVGhhd3RlIFByZW1pdW0gU2Vy +dmVyIENBMSgwJgYJKoZIhvcNAQkBFhlwcmVtaXVtLXNlcnZlckB0aGF3dGUuY29t +MB4XDTk2MDgwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgc4xCzAJBgNVBAYTAlpB +MRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEdMBsG +A1UEChMUVGhhd3RlIENvbnN1bHRpbmcgY2MxKDAmBgNVBAsTH0NlcnRpZmljYXRp +b24gU2VydmljZXMgRGl2aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQcmVtaXVtIFNl +cnZlciBDQTEoMCYGCSqGSIb3DQEJARYZcHJlbWl1bS1zZXJ2ZXJAdGhhd3RlLmNv +bTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0jY2aovXwlue2oFBYo847kkE +VdbQ7xwblRZH7xhINTpS9CtqBo87L+pW46+GjZ4X9560ZXUCTe/LCaIhUdib0GfQ +ug2SBhRz1JPLlyoAnFxODLz6FVL88kRu2hFKbgifLy3j+ao6hnO2RlNYyIkFvYMR +uHM/qgeN9EJN50CdHDcCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG +9w0BAQQFAAOBgQAmSCwWwlj66BZ0DKqqX1Q/8tfJeGBeXm43YyJ3Nn6yF8Q0ufUI +hfzJATj/Tb7yFkJD57taRvvBxhEf8UqwKEbJw8RCfbz6q1lu1bdRiBHjpIUZa4JM +pAwSremkrj/xw0llmozFyD4lt5SZu5IycQfwhl7tUCemDaYj+bvLpgcUQg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCB +qTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf +Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw +MDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNV +BAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3MDAwMDAwWhcNMzYw +NzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5j +LjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYG +A1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl +IG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsoPD7gFnUnMekz52hWXMJEEUMDSxuaPFs +W0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ1CRfBsDMRJSUjQJib+ta +3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGcq/gcfomk +6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6 +Sk/KaAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94J +NqR32HuHUETVPm4pafs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XP +r87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUFAAOCAQEAeRHAS7ORtvzw6WfU +DW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeEuzLlQRHAd9mz +YJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX +xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2 +/qxAeeWsEG89jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/ +LHbTY5xZ3Y+m4Q6gLkH3LpVHz7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7 +jVaMaA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD +VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEm +MCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wHhcNOTYwODAx +MDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT +DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3 +dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNl +cyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3 +DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQAD +gY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl/Kj0R1HahbUgdJSGHg91 +yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg71CcEJRCX +L+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGj +EzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG +7oWDTSEwjsrZqG9JGubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6e +QNuozDJ0uW8NxuOzRAvZim+aKZuZGCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZ +qdq5snUb9kLy78fyGPmJvKP/iiMucEc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICoTCCAgqgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBizELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTEUMBIGA1UEBxMLRHVyYmFudmlsbGUxDzAN +BgNVBAoTBlRoYXd0ZTEdMBsGA1UECxMUVGhhd3RlIENlcnRpZmljYXRpb24xHzAd +BgNVBAMTFlRoYXd0ZSBUaW1lc3RhbXBpbmcgQ0EwHhcNOTcwMTAxMDAwMDAwWhcN +MjAxMjMxMjM1OTU5WjCBizELMAkGA1UEBhMCWkExFTATBgNVBAgTDFdlc3Rlcm4g +Q2FwZTEUMBIGA1UEBxMLRHVyYmFudmlsbGUxDzANBgNVBAoTBlRoYXd0ZTEdMBsG +A1UECxMUVGhhd3RlIENlcnRpZmljYXRpb24xHzAdBgNVBAMTFlRoYXd0ZSBUaW1l +c3RhbXBpbmcgQ0EwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANYrWHhhRYZT +6jR7UZztsOYuGA7+4F+oJ9O0yeB8WU4WDnNUYMF/9p8u6TqFJBU820cEY8OexJQa +Wt9MevPZQx08EHp5JduQ/vBR5zDWQQD9nyjfeb6Uu522FOMjhdepQeBMpHmwKxqL +8vg7ij5FrHGSALSQQZj7X+36ty6K+Ig3AgMBAAGjEzARMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEEBQADgYEAZ9viwuaHPUCDhjc1fR/OmsMMZiCouqoEiYbC +9RAIDb/LogWK0E02PvTX72nGXuSwlG9KuefeW4i2e9vjJ+V2w/A1wcu1J5szedyQ +pgCed/r8zSeUQhac0xxo7L9c3eWpexAKMnRUEzGLhQOEkbdYATAUOK8oyvyxUBkZ +CayJSdM= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID+zCCAuOgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBtzE/MD0GA1UEAww2VMOc +UktUUlVTVCBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx +c8SxMQswCQYDVQQGDAJUUjEPMA0GA1UEBwwGQU5LQVJBMVYwVAYDVQQKDE0oYykg +MjAwNSBUw5xSS1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8 +dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLjAeFw0wNTA1MTMxMDI3MTdaFw0xNTAz +MjIxMDI3MTdaMIG3MT8wPQYDVQQDDDZUw5xSS1RSVVNUIEVsZWt0cm9uaWsgU2Vy +dGlmaWthIEhpem1ldCBTYcSfbGF5xLFjxLFzxLExCzAJBgNVBAYMAlRSMQ8wDQYD +VQQHDAZBTktBUkExVjBUBgNVBAoMTShjKSAyMDA1IFTDnFJLVFJVU1QgQmlsZ2kg +xLBsZXRpxZ9pbSB2ZSBCaWxpxZ9pbSBHw7x2ZW5sacSfaSBIaXptZXRsZXJpIEEu +xZ4uMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAylIF1mMD2Bxf3dJ7 +XfIMYGFbazt0K3gNfUW9InTojAPBxhEqPZW8qZSwu5GXyGl8hMW0kWxsE2qkVa2k +heiVfrMArwDCBRj1cJ02i67L5BuBf5OI+2pVu32Fks66WJ/bMsW9Xe8iSi9BB35J +YbOG7E6mQW6EvAPs9TscyB/C7qju6hJKjRTP8wrgUDn5CDX4EVmt5yLqS8oUBt5C +urKZ8y1UiBAG6uEaPj1nH/vO+3yC6BFdSsG5FOpU2WabfIl9BJpiyelSPJ6c79L1 +JuTm5Rh8i27fbMx4W09ysstcP4wFjdFMjK2Sx+F4f2VsSQZQLJ4ywtdKxnWKWU51 +b0dewQIDAQABoxAwDjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAV +9VX/N5aAWSGk/KEVTCD21F/aAyT8z5Aa9CEKmu46sWrv7/hg0Uw2ZkUd82YCdAR7 +kjCo3gp2D++Vbr3JN+YaDayJSFvMgzbC9UZcWYJWtNX+I7TYVBxEq8Sn5RTOPEFh +fEPmzcSBCYsk+1Ql1haolgxnB2+zUEfjHCQo3SqYpGH+2+oSN7wBGjSFvW5P55Fy +B0SFHljKVETd96y5y4khctuPwGkplyqjrhgjlxxBKot8KsF8kOipKMDTkcatKIdA +aLX/7KfS0zgYnNN9aV3wxqUeJBujR/xpB2jn5Jq07Q+hh4cCzofSSE7hvP/L8XKS +RGQDJereW26fyfJOrN3H +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEPDCCAySgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBvjE/MD0GA1UEAww2VMOc +UktUUlVTVCBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx +c8SxMQswCQYDVQQGEwJUUjEPMA0GA1UEBwwGQW5rYXJhMV0wWwYDVQQKDFRUw5xS +S1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kg +SGl6bWV0bGVyaSBBLsWeLiAoYykgS2FzxLFtIDIwMDUwHhcNMDUxMTA3MTAwNzU3 +WhcNMTUwOTE2MTAwNzU3WjCBvjE/MD0GA1UEAww2VMOcUktUUlVTVCBFbGVrdHJv +bmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxMQswCQYDVQQGEwJU +UjEPMA0GA1UEBwwGQW5rYXJhMV0wWwYDVQQKDFRUw5xSS1RSVVNUIEJpbGdpIMSw +bGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWe +LiAoYykgS2FzxLFtIDIwMDUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCpNn7DkUNMwxmYCMjHWHtPFoylzkkBH3MOrHUTpvqeLCDe2JAOCtFp0if7qnef +J1Il4std2NiDUBd9irWCPwSOtNXwSadktx4uXyCcUHVPr+G1QRT0mJKIx+XlZEdh +R3n9wFHxwZnn3M5q+6+1ATDcRhzviuyV79z/rxAc653YsKpqhRgNF8k+v/Gb0AmJ +Qv2gQrSdiVFVKc8bcLyEVK3BEx+Y9C52YItdP5qtygy/p1Zbj3e41Z55SZI/4PGX +JHpsmxcPbe9TmJEr5A++WXkHeLuXlfSfadRYhwqp48y2WBmfJiGxxFmNskF1wK1p +zpwACPI2/z7woQ8arBT9pmAPAgMBAAGjQzBBMB0GA1UdDgQWBBTZN7NOBf3Zz58S +Fq62iS/rJTqIHDAPBgNVHQ8BAf8EBQMDBwYAMA8GA1UdEwEB/wQFMAMBAf8wDQYJ +KoZIhvcNAQEFBQADggEBAHJglrfJ3NgpXiOFX7KzLXb7iNcX/nttRbj2hWyfIvwq +ECLsqrkw9qtY1jkQMZkpAL2JZkH7dN6RwRgLn7Vhy506vvWolKMiVW4XSf/SKfE4 +Jl3vpao6+XF75tpYHdN0wgH6PmlYX63LaL4ULptswLbcoCb6dxriJNoaN+BnrdFz +gw2lGh1uEpJ+hGIAF728JRhX8tepb1mIvDS3LoV4nZbcFMMsilKbloxSZj2GFotH +uFEJjOp9zYhys2AzsfAKRO8P9Qk3iCQOLGsgOqL6EfJANZxEaGM7rDNvY7wsu/LS +y3Z9fYjYHcgFHW68lKlmjHdxx/qR+i9Rnuk5UrbnBEI= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEXjCCA0agAwIBAgIQRL4Mi1AAIbQR0ypoBqmtaTANBgkqhkiG9w0BAQUFADCB +kzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug +Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho +dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xGzAZBgNVBAMTElVUTiAtIERBVEFDb3Jw +IFNHQzAeFw05OTA2MjQxODU3MjFaFw0xOTA2MjQxOTA2MzBaMIGTMQswCQYDVQQG +EwJVUzELMAkGA1UECBMCVVQxFzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4wHAYD +VQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxITAfBgNVBAsTGGh0dHA6Ly93d3cu +dXNlcnRydXN0LmNvbTEbMBkGA1UEAxMSVVROIC0gREFUQUNvcnAgU0dDMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3+5YEKIrblXEjr8uRgnn4AgPLit6 +E5Qbvfa2gI5lBZMAHryv4g+OGQ0SR+ysraP6LnD43m77VkIVni5c7yPeIbkFdicZ +D0/Ww5y0vpQZY/KmEQrrU0icvvIpOxboGqBMpsn0GFlowHDyUwDAXlCCpVZvNvlK +4ESGoE1O1kduSUrLZ9emxAW5jh70/P/N5zbgnAVssjMiFdC04MwXwLLA9P4yPykq +lXvY8qdOD1R8oQ2AswkDwf9c3V6aPryuvEeKaq5xyh+xKrhfQgUL7EYw0XILyulW +bfXv33i+Ybqypa4ETLyorGkVl73v67SMvzX41MPRKA5cOp9wGDMgd8SirwIDAQAB +o4GrMIGoMAsGA1UdDwQEAwIBxjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRT +MtGzz3/64PGgXYVOktKeRR20TzA9BgNVHR8ENjA0MDKgMKAuhixodHRwOi8vY3Js +LnVzZXJ0cnVzdC5jb20vVVROLURBVEFDb3JwU0dDLmNybDAqBgNVHSUEIzAhBggr +BgEFBQcDAQYKKwYBBAGCNwoDAwYJYIZIAYb4QgQBMA0GCSqGSIb3DQEBBQUAA4IB +AQAnNZcAiosovcYzMB4p/OL31ZjUQLtgyr+rFywJNn9Q+kHcrpY6CiM+iVnJowft +Gzet/Hy+UUla3joKVAgWRcKZsYfNjGjgaQPpxE6YsjuMFrMOoAyYUJuTqXAJyCyj +j98C5OBxOvG0I3KgqgHf35g+FFCgMSa9KOlaMCZ1+XtgHI3zzVAmbQQnmt/VDUVH +KWss5nbZqSl9Mt3JNjy9rjXxEZ4du5A/EkdOjtd+D2JzHVImOBwYSf0wdJrE5SIv +2MCN7ZF6TACPcn9d2t0bi0Vr591pl6jFVkwPDPafepE39peC4N1xaf92P2BNPM/3 +mfnGV/TJVTl4uix5yaaIK/QI +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEojCCA4qgAwIBAgIQRL4Mi1AAJLQR0zYlJWfJiTANBgkqhkiG9w0BAQUFADCB +rjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug +Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho +dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xNjA0BgNVBAMTLVVUTi1VU0VSRmlyc3Qt +Q2xpZW50IEF1dGhlbnRpY2F0aW9uIGFuZCBFbWFpbDAeFw05OTA3MDkxNzI4NTBa +Fw0xOTA3MDkxNzM2NThaMIGuMQswCQYDVQQGEwJVUzELMAkGA1UECBMCVVQxFzAV +BgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5l +dHdvcmsxITAfBgNVBAsTGGh0dHA6Ly93d3cudXNlcnRydXN0LmNvbTE2MDQGA1UE +AxMtVVROLVVTRVJGaXJzdC1DbGllbnQgQXV0aGVudGljYXRpb24gYW5kIEVtYWls +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsjmFpPJ9q0E7YkY3rs3B +YHW8OWX5ShpHornMSMxqmNVNNRm5pELlzkniii8efNIxB8dOtINknS4p1aJkxIW9 +hVE1eaROaJB7HHqkkqgX8pgV8pPMyaQylbsMTzC9mKALi+VuG6JG+ni8om+rWV6l +L8/K2m2qL+usobNqqrcuZzWLeeEeaYji5kbNoKXqvgvOdjp6Dpvq/NonWz1zHyLm +SGHGTPNpsaguG7bUMSAsvIKKjqQOpdeJQ/wWWq8dcdcRWdq6hw2v+vPhwvCkxWeM +1tZUOt4KpLoDd7NlyP0e03RiqhjKaJMeoYV+9Udly/hNVyh00jT/MLbu9mIwFIws +6wIDAQABo4G5MIG2MAsGA1UdDwQEAwIBxjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBSJgmd9xJ0mcABLtFBIfN49rgRufTBYBgNVHR8EUTBPME2gS6BJhkdodHRw +Oi8vY3JsLnVzZXJ0cnVzdC5jb20vVVROLVVTRVJGaXJzdC1DbGllbnRBdXRoZW50 +aWNhdGlvbmFuZEVtYWlsLmNybDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH +AwQwDQYJKoZIhvcNAQEFBQADggEBALFtYV2mGn98q0rkMPxTbyUkxsrt4jFcKw7u +7mFVbwQ+zznexRtJlOTrIEy05p5QLnLZjfWqo7NK2lYcYJeA3IKirUq9iiv/Cwm0 +xtcgBEXkzYABurorbs6q15L+5K/r9CYdFip/bDCVNy8zEqx/3cfREYxRmLLQo5HQ +rfafnoOTHh1CuEava2bwm3/q4wMC5QJRwarVNZ1yQAOJujEdxRBoUp7fooXFXAim +eOZTT7Hot9MUnpOmw2TjrH5xzbyf6QMbzPvprDHBr3wVdAKZw7JHpsIyYdfHb0gk +USeh1YdV8nuPmD0Wnu51tvjQjvLzxq4oW6fw8zYX/MMF08oDSlQ= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEdDCCA1ygAwIBAgIQRL4Mi1AAJLQR0zYq/mUK/TANBgkqhkiG9w0BAQUFADCB +lzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug +Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho +dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3Qt +SGFyZHdhcmUwHhcNOTkwNzA5MTgxMDQyWhcNMTkwNzA5MTgxOTIyWjCBlzELMAkG +A1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEe +MBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8v +d3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3QtSGFyZHdh +cmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCx98M4P7Sof885glFn +0G2f0v9Y8+efK+wNiVSZuTiZFvfgIXlIwrthdBKWHTxqctU8EGc6Oe0rE81m65UJ +M6Rsl7HoxuzBdXmcRl6Nq9Bq/bkqVRcQVLMZ8Jr28bFdtqdt++BxF2uiiPsA3/4a +MXcMmgF6sTLjKwEHOG7DpV4jvEWbe1DByTCP2+UretNb+zNAHqDVmBe8i4fDidNd +oI6yqqr2jmmIBsX6iSHzCJ1pLgkzmykNRg+MzEk0sGlRvfkGzWitZky8PqxhvQqI +DsjfPe58BEydCl5rkdbux+0ojatNh4lz0G6k0B4WixThdkQDf2Os5M1JnMWS9Ksy +oUhbAgMBAAGjgbkwgbYwCwYDVR0PBAQDAgHGMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFKFyXyYbKJhDlV0HN9WFlp1L0sNFMEQGA1UdHwQ9MDswOaA3oDWGM2h0 +dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9VVE4tVVNFUkZpcnN0LUhhcmR3YXJlLmNy +bDAxBgNVHSUEKjAoBggrBgEFBQcDAQYIKwYBBQUHAwUGCCsGAQUFBwMGBggrBgEF +BQcDBzANBgkqhkiG9w0BAQUFAAOCAQEARxkP3nTGmZev/K0oXnWO6y1n7k57K9cM +//bey1WiCuFMVGWTYGufEpytXoMs61quwOQt9ABjHbjAbPLPSbtNk28Gpgoiskli +CE7/yMgUsogWXecB5BKV5UU0s4tpvc+0hY91UZ59Ojg6FEgSxvunOxqNDYJAB+gE +CJChicsZUN/KHAG8HQQZexB2lzvukJDKxA4fFm517zP4029bHpbj4HR3dHuKom4t +3XbWOTCC8KucUvIqx69JXn7HaOWCgchqJ/kniCrVWFCVH/A7HFe7fRQ5YiuayZSS +KqMiDP+JJn1fIytH1xUdqWqeUQ0qUZ6B+dQ7XnASfxAynB67nfhmqA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEZDCCA0ygAwIBAgIQRL4Mi1AAJLQR0zYwS8AzdzANBgkqhkiG9w0BAQUFADCB +ozELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug +Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho +dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xKzApBgNVBAMTIlVUTi1VU0VSRmlyc3Qt +TmV0d29yayBBcHBsaWNhdGlvbnMwHhcNOTkwNzA5MTg0ODM5WhcNMTkwNzA5MTg1 +NzQ5WjCBozELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0 +IExha2UgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYD +VQQLExhodHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xKzApBgNVBAMTIlVUTi1VU0VS +Rmlyc3QtTmV0d29yayBBcHBsaWNhdGlvbnMwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCz+5Gh5DZVhawGNFugmliy+LUPBXeDrjKxdpJo7CNKyXY/45y2 +N3kDuatpjQclthln5LAbGHNhSuh+zdMvZOOmfAz6F4CjDUeJT1FxL+78P/m4FoCH +iZMlIJpDgmkkdihZNaEdwH+DBmQWICzTSaSFtMBhf1EI+GgVkYDLpdXuOzr0hARe +YFmnjDRy7rh4xdE7EkpvfmUnuaRVxblvQ6TFHSyZwFKkeEwVs0CYCGtDxgGwenv1 +axwiP8vv/6jQOkt2FZ7S0cYu49tXGzKiuG/ohqY/cKvlcJKrRB5AUPuco2LkbG6g +yN7igEL66S/ozjIEj3yNtxyjNTwV3Z7DrpelAgMBAAGjgZEwgY4wCwYDVR0PBAQD +AgHGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFPqGydvguul49Uuo1hXf8NPh +ahQ8ME8GA1UdHwRIMEYwRKBCoECGPmh0dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9V +VE4tVVNFUkZpcnN0LU5ldHdvcmtBcHBsaWNhdGlvbnMuY3JsMA0GCSqGSIb3DQEB +BQUAA4IBAQCk8yXM0dSRgyLQzDKrm5ZONJFUICU0YV8qAhXhi6r/fWRRzwr/vH3Y +IWp4yy9Rb/hCHTO967V7lMPDqaAt39EpHx3+jz+7qEUqf9FuVSTiuwL7MT++6Lzs +QCv4AdRWOOTKRIK1YSAhZ2X28AvnNPilwpyjXEAfhZOVBt5P1CeptqX8Fs1zMT+4 +ZSfP1FMa8Kxun08FDAOBp4QpxFq9ZFdyrTvPNximmMatBrTcCKME1SmklpoSZ0qM +YEWd8SOasACcaLWYUNPvji6SZbFIPiG+FTAqDbUMo2s/rn9X9R+WfN9v3YIwLGUb +QErNaLly7HF27FSOH4UMAWr6pjisH8SE +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0 +IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAz +BgNVBAsTLFZhbGlDZXJ0IENsYXNzIDEgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9y +aXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG +9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNTIyMjM0OFoXDTE5MDYy +NTIyMjM0OFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0d29y +azEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs +YXNzIDEgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRw +Oi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNl +cnQuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDYWYJ6ibiWuqYvaG9Y +LqdUHAZu9OqNSLwxlBfw8068srg1knaw0KWlAdcAAxIiGQj4/xEjm84H9b9pGib+ +TunRf50sQB1ZaG6m+FiwnRqP0z/x3BkGgagO4DrdyFNFCQbmD3DD+kCmDuJWBQ8Y +TfwggtFzVXSNdnKgHZ0dwN0/cQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFBoPUn0 +LBwGlN+VYH+Wexf+T3GtZMjdd9LvWVXoP+iOBSoh8gfStadS/pyxtuJbdxdA6nLW +I8sogTLDAHkY7FkXicnGah5xyf23dKUlRWnFSKsZ4UWKJWsZ7uW7EvV/96aNUcPw +nXS3qT6gpf+2SQMT2iLM7XGCK5nPOrf1LXLI +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0 +IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAz +BgNVBAsTLFZhbGlDZXJ0IENsYXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9y +aXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG +9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAwMTk1NFoXDTE5MDYy +NjAwMTk1NFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0d29y +azEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs +YXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRw +Oi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNl +cnQuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOOnHK5avIWZJV16vY +dA757tn2VUdZZUcOBVXc65g2PFxTXdMwzzjsvUGJ7SVCCSRrCl6zfN1SLUzm1NZ9 +WlmpZdRJEy0kTRxQb7XBhVQ7/nHk01xC+YDgkRoKWzk2Z/M/VXwbP7RfZHM047QS +v4dk+NoS/zcnwbNDu+97bi5p9wIDAQABMA0GCSqGSIb3DQEBBQUAA4GBADt/UG9v +UJSZSWI4OB9L+KXIPqeCgfYrx+jFzug6EILLGACOTb2oWH+heQC1u+mNr0HZDzTu +IYEZoDJJKPTEjlbVUjP9UNV+mWwD5MlM/Mtsq2azSiGM5bUMMj4QssxsodyamEwC +W/POuZ6lcg5Ktz885hZo+L7tdEy8W9ViH0Pd +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICPTCCAaYCEQDNun9W8N/kvFT+IqyzcqpVMA0GCSqGSIb3DQEBAgUAMF8xCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE3MDUGA1UECxMuQ2xh +c3MgMSBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw05 +NjAxMjkwMDAwMDBaFw0yODA4MDEyMzU5NTlaMF8xCzAJBgNVBAYTAlVTMRcwFQYD +VQQKEw5WZXJpU2lnbiwgSW5jLjE3MDUGA1UECxMuQ2xhc3MgMSBQdWJsaWMgUHJp +bWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCBnzANBgkqhkiG9w0BAQEFAAOB +jQAwgYkCgYEA5Rm/baNWYS2ZSHH2Z965jeu3noaACpEO+jglr0aIguVzqKCbJF0N +H8xlbgyw0FaEGIeaBpsQoXPftFg5a27B9hXVqKg/qhIGjTGsf7A01480Z4gJzRQR +4k5FVmkfeAKA2txHkSm7NsljXMXg1y2He6G3MrB7MLoqLzGq7qNn2tsCAwEAATAN +BgkqhkiG9w0BAQIFAAOBgQBMP7iLxmjf7kMzDl3ppssHhE16M/+SG/Q2rdiVIjZo +EWx8QszznC7EBz8UsA9P/5CSdvnivErpj82ggAr3xSnxgiJduLHdgSOjeyUVRjB5 +FvjqBUuUfx3CHMjjt/QQQDwTw18fU+hI5Ia0e6E1sHslurjTjqs/OJ0ANACY89Fx +lA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDAjCCAmsCEEzH6qqYPnHTkxD4PTqJkZIwDQYJKoZIhvcNAQEFBQAwgcExCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh +c3MgMSBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy +MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp +emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X +DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw +FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMSBQdWJsaWMg +UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo +YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5 +MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQCq0Lq+Fi24g9TK0g+8djHKlNgdk4xWArzZbxpvUjZudVYK +VdPfQ4chEWWKfo+9Id5rMj8bhDSVBZ1BNeuS65bdqlk/AVNtmU/t5eIqWpDBucSm +Fc/IReumXY6cPvBkJHalzasab7bYe1FhbqZ/h8jit+U03EGI6glAvnOSPWvndQID +AQABMA0GCSqGSIb3DQEBBQUAA4GBAKlPww3HZ74sy9mozS11534Vnjty637rXC0J +h9ZrbWB85a7FkCMMXErQr7Fd88e2CtvgFZMN3QO8x3aKtd1Pw5sTdbgBwObJW2ul +uIncrKTdcu1OofdPvAbT6shkdHvClUGcZXNY8ZCaPGqxmMnEh7zPRW1F4m4iP/68 +DzFc6PLZ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQCLW3VWhFSFCwDPrzhIzrGkMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDEgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDEgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN2E1Lm0+afY8wR4 +nN493GwTFtl63SRRZsDHJlkNrAYIwpTRMx/wgzUfbhvI3qpuFU5UJ+/EbRrsC+MO +8ESlV8dAWB6jRx9x7GD2bZTIGDnt/kIYVt/kTEkQeE4BdjVjEjbdZrwBBDajVWjV +ojYJrKshJlQGrT/KFOCsyq0GHZXi+J3x4GD/wn91K0zM2v6HmSHquv4+VNfSWXjb +PG7PoBMAGrgnoeS+Z5bKoMWznN3JdZ7rMJpfo83ZrngZPyPpXNspva1VyBtUjGP2 +6KbqxzcSXKMpHgLZ2x87tNcPVkeBFQRKr4Mn0cVYiMHd9qqnoxjaaKptEVHhv2Vr +n5Z20T0CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAq2aN17O6x5q25lXQBfGfMY1a +qtmqRiYPce2lrVNWYgFHKkTp/j90CxObufRNG7LRX7K20ohcs5/Ny9Sn2WCVhDr4 +wTcdYcrnsMXlkdpUpqwxga6X3s0IrLjAl4B/bnKk52kTlWUfxJM8/XmPBNQ+T+r3 +ns7NZ3xPZQL/kYVUc8f/NveGLezQXk//EZ9yBta4GvFMDSZl4kSAHsef493oCtrs +pSCAaWihT37ha88HQfqDjrw43bAuEbFrskLMmrz5SCJ5ShkPshw+IHTZasO+8ih4 +E1Z5T21Q6huwtVexN2ZYI/PcD98Kh8TvhgXVOBRgmaNL3gaWcSzy27YfpO8/7g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICPDCCAaUCEC0b/EoXjaOR6+f/9YtFvgswDQYJKoZIhvcNAQECBQAwXzELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz +cyAyIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 +MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV +BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAyIFB1YmxpYyBQcmlt +YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN +ADCBiQKBgQC2WoujDWojg4BrzzmH9CETMwZMJaLtVRKXxaeAufqDwSCg+i8VDXyh +YGt+eSz6Bg86rvYbb7HS/y8oUl+DfUvEerf4Zh+AVPy3wo5ZShRXRtGak75BkQO7 +FYCTXOvnzAhsPz6zSvz/S2wj1VCCJkQZjiPDceoZJEcEnnW/yKYAHwIDAQABMA0G +CSqGSIb3DQEBAgUAA4GBAIobK/o5wXTXXtgZZKJYSi034DNHD6zt96rbHuSLBlxg +J8pFUs4W7z8GZOeUaHxgMxURaa+dYo2jA1Rrpr7l7gUYYAS/QoD90KioHgE796Nc +r6Pc5iaAIzy4RHT3Cq5Ji2F4zCS/iIqnDupzGUH9TQPwiNHleI2lKk/2lw0Xd8rY +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDAzCCAmwCEQC5L2DMiJ+hekYJuFtwbIqvMA0GCSqGSIb3DQEBBQUAMIHBMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0Ns +YXNzIDIgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH +MjE6MDgGA1UECxMxKGMpIDE5OTggVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9y +aXplZCB1c2Ugb25seTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazAe +Fw05ODA1MTgwMDAwMDBaFw0yODA4MDEyMzU5NTlaMIHBMQswCQYDVQQGEwJVUzEX +MBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0NsYXNzIDIgUHVibGlj +IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjE6MDgGA1UECxMx +KGMpIDE5OTggVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s +eTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEAp4gBIXQs5xoD8JjhlzwPIQjxnNuX6Zr8wgQGE75fUsjM +HiwSViy4AWkszJkfrbCWrnkE8hM5wXuYuggs6MKEEyyqaekJ9MepAqRCwiNPStjw +DqL7MWzJ5m+ZJwf15vRMeJ5t60aG+rmGyVTyssSv1EYcWskVMP8NbPUtDm3Of3cC +AwEAATANBgkqhkiG9w0BAQUFAAOBgQByLvl/0fFx+8Se9sVeUYpAmLho+Jscg9ji +nb3/7aHmZuovCfTK1+qlK5X2JGCGTUQug6XELaDTrnhpb3LabK4I8GOSN+a7xDAX +rXfMSTWqz9iP0b63GJZHc2pUIjRkLbYWm1lbtFFZOrMLFPQS32eg9K0yZF6xRnIn +jBJ7xUS0rg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEGTCCAwECEGFwy0mMX5hFKeewptlQW3owDQYJKoZIhvcNAQEFBQAwgcoxCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVy +aVNpZ24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24s +IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNp +Z24gQ2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eSAtIEczMB4XDTk5MTAwMTAwMDAwMFoXDTM2MDcxNjIzNTk1OVowgcoxCzAJBgNV +BAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVyaVNp +Z24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24sIElu +Yy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNpZ24g +Q2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAt +IEczMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArwoNwtUs22e5LeWU +J92lvuCwTY+zYVY81nzD9M0+hsuiiOLh2KRpxbXiv8GmR1BeRjmL1Za6tW8UvxDO +JxOeBUebMXoT2B/Z0wI3i60sR/COgQanDTAM6/c8DyAd3HJG7qUCyFvDyVZpTMUY +wZF7C9UTAJu878NIPkZgIIUq1ZC2zYugzDLdt/1AVbJQHFauzI13TccgTacxdu9o +koqQHgiBVrKtaaNS0MscxCM9H5n+TOgWY47GCI72MfbS+uV23bUckqNJzc0BzWjN +qWm6o+sdDZykIKbBoMXRRkwXbdKsZj+WjOCE1Db/IlnF+RFgqF8EffIa9iVCYQ/E +Srg+iQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQA0JhU8wI1NQ0kdvekhktdmnLfe +xbjQ5F1fdiLAJvmEOjr5jLX77GDx6M4EsMjdpwOPMPOY36TmpDHf0xwLRtxyID+u +7gU8pDM/CzmscHhzS5kr3zDCVLCoO1Wh/hYozUK9dG6A2ydEp85EXdQbkJgNHkKU +sQAsBNB0owIFImNjzYO1+8FtYmtpdf1dcEG59b98377BMnMiIYtYgXsVkXq642RI +sH/7NiXaldDxJBQX3RiAa0YjOVT1jmIJBB2UkKab5iXiQkWquJCtvgiPqQtCGJTP +cjnhsUPgKM+351psE2tJs//jGHyJizNdrDPXp/naOlXJWBD5qu9ats9LS98q +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz +cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 +MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV +BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt +YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN +ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE +BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is +I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G +CSqGSIb3DQEBAgUAA4GBALtMEivPLCYATxQT3ab7/AoRhIzzKBxnki98tsX63/Do +lbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59AhWM1pF+NEHJwZRDmJXNyc +AA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2OmufTqj/ZA1k +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDAjCCAmsCEH3Z/gfPqB63EHln+6eJNMYwDQYJKoZIhvcNAQEFBQAwgcExCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh +c3MgMyBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy +MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp +emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X +DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw +FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMg +UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo +YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5 +MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQDMXtERXVxp0KvTuWpMmR9ZmDCOFoUgRm1HP9SFIIThbbP4 +pO0M8RcPO/mn+SXXwc+EY/J8Y8+iR/LGWzOOZEAEaMGAuWQcRXfH2G71lSk8UOg0 +13gfqLptQ5GVj0VXXn7F+8qkBOvqlzdUMG+7AUcyM83cV5tkaWH4mx0ciU9cZwID +AQABMA0GCSqGSIb3DQEBBQUAA4GBAFFNzb5cy5gZnBWyATl4Lk0PZ3BwmcYQWpSk +U01UbSuvDV1Ai2TT1+7eVmGSX6bEHRBhNtMsJzzoKQm5EWR0zLVznxxIqbxhAe7i +F6YM40AIOw7n60RzKprxaZLvcRTDOaxxp5EJb+RxBrO6WVcmeQD2+A2iMzAo1KpY +oJ2daZH9 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b +N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t +KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu +kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm +CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ +Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu +imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te +2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe +DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC +/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p +F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt +TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB +yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW +ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp +U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y +aXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1 +nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex +t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz +SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG +BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+ +rCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/ +NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E +BAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH +BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy +aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv +MzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE +p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y +5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK +WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ +4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N +hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDAjCCAmsCEDKIjprS9esTR/h/xCA3JfgwDQYJKoZIhvcNAQEFBQAwgcExCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh +c3MgNCBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy +MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp +emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X +DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw +FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgNCBQdWJsaWMg +UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo +YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5 +MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQC68OTP+cSuhVS5B1f5j8V/aBH4xBewRNzjMHPVKmIquNDM +HO0oW369atyzkSTKQWI8/AIBvxwWMZQFl3Zuoq29YRdsTjCG8FE3KlDHqGKB3FtK +qsGgtG7rL+VXxbErQHDbWk2hjh+9Ax/YA9SPTJlxvOKCzFjomDqG04Y48wApHwID +AQABMA0GCSqGSIb3DQEBBQUAA4GBAIWMEsGnuVAVess+rLhDityq3RS6iYF+ATwj +cSGIL4LcY/oCRaxFWdcqWERbt5+BO5JoPeI3JPV7bI92NZYJqFmduc4jq3TWg/0y +cyfYaT5DdPauxYma51N86Xv2S/PBZYPejYqcPIiNOVn8qj8ijaHBZlCBckztImRP +T8qAkbYp +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQDsoKeLbnVqAc/EfMwvlF7XMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK3LpRFpxlmr8Y+1 +GQ9Wzsy1HyDkniYlS+BzZYlZ3tCD5PUPtbut8XzoIfzk6AzufEUiGXaStBO3IFsJ ++mGuqPKljYXCKtbeZjbSmwL0qJJgfJxptI8kHtCGUvYynEFYHiK9zUVilQhu0Gbd +U6LM8BDcVHOLBKFGMzNcF0C5nk3T875Vg+ixiY5afJqWIpA7iCXy0lOIAgwLePLm +NxdLMEYH5IBtptiWLugs+BGzOA1mppvqySNb247i8xOOGlktqgLw7KSHZtzBP/XY +ufTsgsbSPZUd5cBPhMnZo0QoBmrXRazwa2rvTl/4EYIeOGM0ZlDUPpNz+jDDZq3/ +ky2X7wMCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAj/ola09b5KROJ1WrIhVZPMq1 +CtRK26vdoV9TxaBXOcLORyu+OshWv8LZJxA6sQU8wHcxuzrTBXttmhwwjIDLk5Mq +g6sFUYICABFna/OIYUdfA5PVWw3g8dShMjWFsjrbsIKr0csKvE+MW8VLADsfKoKm +fjaF3H48ZwC15DtS4KjrXRX5xm3wrR0OhbepmnMUWluPQSjA1egtTaRezarZ7c7c +2NU8Qh0XwRJdRTjDOPP8hS6DRkiy1yBfkjaP53kPmF6Z6PDQpLv1U70qzlmwr25/ +bLvSHgCwIe34QWKCudiyxLtGUPMxxY8BqHTr9Xgn2uf3ZkPznoM+IKrDNWCRzg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICNDCCAaECEAKtZn5ORf5eV288mBle3cAwDQYJKoZIhvcNAQECBQAwXzELMAkG +A1UEBhMCVVMxIDAeBgNVBAoTF1JTQSBEYXRhIFNlY3VyaXR5LCBJbmMuMS4wLAYD +VQQLEyVTZWN1cmUgU2VydmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk0 +MTEwOTAwMDAwMFoXDTEwMDEwNzIzNTk1OVowXzELMAkGA1UEBhMCVVMxIDAeBgNV +BAoTF1JTQSBEYXRhIFNlY3VyaXR5LCBJbmMuMS4wLAYDVQQLEyVTZWN1cmUgU2Vy +dmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGbMA0GCSqGSIb3DQEBAQUAA4GJ +ADCBhQJ+AJLOesGugz5aqomDV6wlAXYMra6OLDfO6zV4ZFQD5YRAUcm/jwjiioII +0haGN1XpsSECrXZogZoFokvJSyVmIlZsiAeP94FZbYQHZXATcXY+m3dM41CJVphI +uR2nKRoTLkoRWZweFdVJVCxzOmmCsZc5nG1wZ0jl3S3WyB57AgMBAAEwDQYJKoZI +hvcNAQECBQADfgBl3X7hsuyw4jrg7HFGmhkRuNPHoLQDQCYCPgmc4RKz0Vr2N6W3 +YQO2WxZpO8ZECAyIUwxrl0nHPjXcbLm7qt9cuzovk2C2qUtN8iD3zV9/ZHuO3ABc +1/p3yjkWWW8O6tO1g39NTUJWdrTJXwT4OPjr0l91X817/OWOgHz8UA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDzTCCAzagAwIBAgIQU2GyYK7bcY6nlLMTM/QHCTANBgkqhkiG9w0BAQUFADCB +wTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTwwOgYDVQQL +EzNDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +IC0gRzIxOjA4BgNVBAsTMShjKSAxOTk4IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1 +dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0IE5ldHdv +cmswHhcNMDAwOTI2MDAwMDAwWhcNMTAwOTI1MjM1OTU5WjCBpTEXMBUGA1UEChMO +VmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0IE5ldHdvcmsx +OzA5BgNVBAsTMlRlcm1zIG9mIHVzZSBhdCBodHRwczovL3d3dy52ZXJpc2lnbi5j +b20vcnBhIChjKTAwMSwwKgYDVQQDEyNWZXJpU2lnbiBUaW1lIFN0YW1waW5nIEF1 +dGhvcml0eSBDQTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0hmdZ8IAIVli +zrQJIkRpivglWtvtDbc2fk7gu5Q+kCWHwmFHKdm9VLhjzCx9abQzNvQ3B5rB3UBU +/OB4naCTuQk9I1F/RMIUdNsKvsvJMDRAmD7Q1yUQgZS9B0+c1lQn3y6ov8uQjI11 +S7zi6ESHzeZBCiVu6PQkAsVSD27smHUCAwEAAaOB3zCB3DAPBgNVHRMECDAGAQH/ +AgEAMEUGA1UdIAQ+MDwwOgYMYIZIAYb4RQEHFwEDMCowKAYIKwYBBQUHAgEWHGh0 +dHBzOi8vd3d3LnZlcmlzaWduLmNvbS9ycGEwMQYDVR0fBCowKDAmoCSgIoYgaHR0 +cDovL2NybC52ZXJpc2lnbi5jb20vcGNhMy5jcmwwCwYDVR0PBAQDAgEGMEIGCCsG +AQUFBwEBBDYwNDAyBggrBgEFBQcwAaYmFiRodHRwOi8vb2NzcC52ZXJpc2lnbi5j +b20vb2NzcC9zdGF0dXMwDQYJKoZIhvcNAQEFBQADgYEAgnBold+2DcIBcBlK0lRW +HqzyRUyHuPU163hLBanInTsZIS5wNEqi9YngFXVF5yg3ADQnKeg3S/LvRJdrF1Ea +w1adPBqK9kpGRjeM+sv1ZFo4aC4cw+9wzrhGBha/937ntag+RaypJXUie28/sJyU +58dzq6wf7iWbwBbtt8pb8BQ= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDojCCAoqgAwIBAgIQE4Y1TR0/BvLB+WUF1ZAcYjANBgkqhkiG9w0BAQUFADBr +MQswCQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRl +cm5hdGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNv +bW1lcmNlIFJvb3QwHhcNMDIwNjI2MDIxODM2WhcNMjIwNjI0MDAxNjEyWjBrMQsw +CQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRlcm5h +dGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNvbW1l +cmNlIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvV95WHm6h +2mCxlCfLF9sHP4CFT8icttD0b0/Pmdjh28JIXDqsOTPHH2qLJj0rNfVIsZHBAk4E +lpF7sDPwsRROEW+1QK8bRaVK7362rPKgH1g/EkZgPI2h4H3PVz4zHvtH8aoVlwdV +ZqW1LS7YgFmypw23RuwhY/81q6UCzyr0TP579ZRdhE2o8mCP2w4lPJ9zcc+U30rq +299yOIzzlr3xF7zSujtFWsan9sYXiwGd/BmoKoMWuDpI/k4+oKsGGelT84ATB+0t +vz8KPFUgOSwsAGl0lUq8ILKpeeUYiZGo3BxN77t+Nwtd/jmliFKMAGzsGHxBvfaL +dXe6YJ2E5/4tAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBQVOIMPPyw/cDMezUb+B4wg4NfDtzANBgkqhkiG9w0BAQUF +AAOCAQEAX/FBfXxcCLkr4NWSR/pnXKUTwwMhmytMiUbPWU3J/qVAtmPN3XEolWcR +zCSs00Rsca4BIGsDoo8Ytyk6feUWYFN4PMCvFYP3j1IzJL1kk5fui/fbGKhtcbP3 +LBfQdCVp9/5rPJS+TUtBjE7ic9DjkCJzQ83z7+pzzkWKsKZJ/0x9nXGIxHYdkFsd +7v3M9+79YKWxehZx0RbQfBI8bGmX265fOZpwLwU8GUYEmSA20GBuYQa7FkKMcPcw +++DbZqMAAb3mLNqRX6BGi01qnD093QVG/na/oAo85ADmJ7f/hC3euiInlhBx6yLt +398znM/jra6O1I7mT1GvFpLgXPYHDw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDgDCCAmigAwIBAgICAx4wDQYJKoZIhvcNAQEFBQAwYTELMAkGA1UEBhMCVVMx +DTALBgNVBAoTBFZJU0ExLzAtBgNVBAsTJlZpc2EgSW50ZXJuYXRpb25hbCBTZXJ2 +aWNlIEFzc29jaWF0aW9uMRIwEAYDVQQDEwlHUCBSb290IDIwHhcNMDAwODE2MjI1 +MTAwWhcNMjAwODE1MjM1OTAwWjBhMQswCQYDVQQGEwJVUzENMAsGA1UEChMEVklT +QTEvMC0GA1UECxMmVmlzYSBJbnRlcm5hdGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRp +b24xEjAQBgNVBAMTCUdQIFJvb3QgMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAKkBcLWqxEDwq2omYXkZAPy/mzdZDK9vZBv42pWUJGkzEXDK41Z0ohdX +ZFwgBuHW73G3O/erwWnQSaSxBNf0V2KJXLB1LRckaeNCYOTudNargFbYiCjh+20i +/SN8RnNPflRzHqgsVVh1t0zzWkWlAhr62p3DRcMiXvOL8WAp0sdftAw6UYPvMPjU +58fy+pmjIlC++QU3o63tmsPm7IgbthknGziLgE3sucfFicv8GjLtI/C1AVj59o/g +halMCXI5Etuz9c9OYmTaxhkVOmMd6RdVoUwiPDQyRvhlV7or7zaMavrZ2UT0qt2E +1w0cslSsMoW0ZA3eQbuxNMYBhjJk1Z8CAwEAAaNCMEAwHQYDVR0OBBYEFJ59SzS/ +ca3CBfYDdYDOqU8axCRMMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEG +MA0GCSqGSIb3DQEBBQUAA4IBAQAhpXYUVfmtJ3CPPPTVbMjMCqujmAuKBiPFyWHb +mQdpNSYx/scuhMKZYdQN6X0uEyt8joW2hcdLzzW2LEc9zikv2G+fiRxkk78IvXbQ +kIqUs38oW26sTTMs7WXcFsziza6kPWKSBpUmv9+55CCmc2rBvveURNZNbyoLaxhN +dBA2aGpawWqn3TYpjLgwi08hPwAuVDAHOrqK5MOeyti12HvOdUVmB/RtLdh6yumJ +ivIj2C/LbgA2T/vwLwHMD8AiZfSr4k5hLQOCfZEWtTDVFN5ex5D8ofyrEK9ca3Cn +B+8phuiyJccg/ybdd+95RBTEvd07xQObdyPsoOy7Wjm1zK0G +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID5TCCAs2gAwIBAgIEOeSXnjANBgkqhkiG9w0BAQUFADCBgjELMAkGA1UEBhMC +VVMxFDASBgNVBAoTC1dlbGxzIEZhcmdvMSwwKgYDVQQLEyNXZWxscyBGYXJnbyBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEvMC0GA1UEAxMmV2VsbHMgRmFyZ28gUm9v +dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDAxMDExMTY0MTI4WhcNMjEwMTE0 +MTY0MTI4WjCBgjELMAkGA1UEBhMCVVMxFDASBgNVBAoTC1dlbGxzIEZhcmdvMSww +KgYDVQQLEyNXZWxscyBGYXJnbyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEvMC0G +A1UEAxMmV2VsbHMgRmFyZ28gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVqDM7Jvk0/82bfuUER84A4n13 +5zHCLielTWi5MbqNQ1mXx3Oqfz1cQJ4F5aHiidlMuD+b+Qy0yGIZLEWukR5zcUHE +SxP9cMIlrCL1dQu3U+SlK93OvRw6esP3E48mVJwWa2uv+9iWsWCaSOAlIiR5NM4O +JgALTqv9i86C1y8IcGjBqAr5dE8Hq6T54oN+J3N0Prj5OEL8pahbSCOz6+MlsoCu +ltQKnMJ4msZoGK43YjdeUXWoWGPAUe5AeH6orxqg4bB4nVCMe+ez/I4jsNtlAHCE +AQgAFG5Uhpq6zPk3EPbg3oQtnaSFN9OH4xXQwReQfhkhahKpdv0SAulPIV4XAgMB +AAGjYTBfMA8GA1UdEwEB/wQFMAMBAf8wTAYDVR0gBEUwQzBBBgtghkgBhvt7hwcB +CzAyMDAGCCsGAQUFBwIBFiRodHRwOi8vd3d3LndlbGxzZmFyZ28uY29tL2NlcnRw +b2xpY3kwDQYJKoZIhvcNAQEFBQADggEBANIn3ZwKdyu7IvICtUpKkfnRLb7kuxpo +7w6kAOnu5+/u9vnldKTC2FJYxHT7zmu1Oyl5GFrvm+0fazbuSCUlFLZWohDo7qd/ +0D+j0MNdJu4HzMPBJCGHHt8qElNvQRbn7a6U+oxy+hNH8Dx+rn0ROhPs7fpvcmR7 +nX1/Jv16+yWt6j4pf0zjAFcysLPp7VMX2YuyFA4w6OXVE8Zkr8QA1dhYJPz1j+zx +x32l2w8n0cbyQIjmH/ZhqPRCyLk306m+LFZ4wnKbWV01QIroTmMatukgalHizqSQ +33ZwmVxwQ023tqcZZE6St8WRPH9IFmV7Fv3L/PvZ1dZPIWU7Sn9Ho/s= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEvTCCA6WgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBhTELMAkGA1UEBhMCVVMx +IDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxs +cyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9v +dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDcxMjEzMTcwNzU0WhcNMjIxMjE0 +MDAwNzU0WjCBhTELMAkGA1UEBhMCVVMxIDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdl +bGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQD +DC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDub7S9eeKPCCGeOARBJe+r +WxxTkqxtnt3CxC5FlAM1iGd0V+PfjLindo8796jE2yljDpFoNoqXjopxaAkH5OjU +Dk/41itMpBb570OYj7OeUt9tkTmPOL13i0Nj67eT/DBMHAGTthP796EfvyXhdDcs +HqRePGj4S78NuR4uNuip5Kf4D8uCdXw1LSLWwr8L87T8bJVhHlfXBIEyg1J55oNj +z7fLY4sR4r1e6/aN7ZVyKLSsEmLpSjPmgzKuBXWVvYSV2ypcm44uDLiBK0HmOFaf +SZtsdvqKXfcBeYF8wYNABf5x/Qw/zE5gCQ5lRxAvAcAFP4/4s0HvWkJ+We/Slwxl +AgMBAAGjggE0MIIBMDAPBgNVHRMBAf8EBTADAQH/MDkGA1UdHwQyMDAwLqAsoCqG +KGh0dHA6Ly9jcmwucGtpLndlbGxzZmFyZ28uY29tL3dzcHJjYS5jcmwwDgYDVR0P +AQH/BAQDAgHGMB0GA1UdDgQWBBQmlRkQ2eihl5H/3BnZtQQ+0nMKajCBsgYDVR0j +BIGqMIGngBQmlRkQ2eihl5H/3BnZtQQ+0nMKaqGBi6SBiDCBhTELMAkGA1UEBhMC +VVMxIDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNX +ZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMg +Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHmCAQEwDQYJKoZIhvcNAQEFBQADggEB +ALkVsUSRzCPIK0134/iaeycNzXK7mQDKfGYZUMbVmO2rvwNa5U3lHshPcZeG1eMd +/ZDJPHV3V3p9+N701NX3leZ0bh08rnyd2wIDBSxxSyU+B+NemvVmFymIGjifz6pB +A4SXa5M4esowRBskRDPQ5NHcKDj0E0M1NSljqHyita04pO2t/caaH/+Xc/77szWn +k4bGdpEA5qxRFsQnMlzbc9qlk1eOPm01JghZ1edE13YgY+esE2fDbbFwRnzVlhE9 +iW9dqKHrjQrawx0zbKPqZxmamX9LPYNRKh3KL4YMon4QLSvUFpULB6ouFJJJtylv +2G0xffX8oRAHh84vWdw+WNs= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB +gjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk +MCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY +UmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx +NDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3 +dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy +dmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6 +38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP +KZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q +DxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4 +qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa +JSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi +PvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P +BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs +jVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0 +eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD +ggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR +vbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt +qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa +IR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy +i6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ +O+7ETPTsJ3xCwnR8gooJybQDJbw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIETzCCAzegAwIBAgIEO63vKTANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJQTDEfMB0GA1UE +ChMWVFAgSW50ZXJuZXQgU3AuIHogby5vLjEkMCIGA1UECxMbQ2VudHJ1bSBDZXJ0eWZpa2Fjamkg +U2lnbmV0MRswGQYDVQQDExJDQyBTaWduZXQgLSBSb290Q0EwHhcNMDEwOTIzMTQxODE3WhcNMTEw +OTIzMTMxODE3WjB1MQswCQYDVQQGEwJQTDEfMB0GA1UEChMWVFAgSW50ZXJuZXQgU3AuIHogby5v +LjEkMCIGA1UECxMbQ2VudHJ1bSBDZXJ0eWZpa2FjamkgU2lnbmV0MR8wHQYDVQQDExZDQyBTaWdu +ZXQgLSBDQSBLbGFzYSAxMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4SRW9Q58g5DY1Hw7h +gCRKBEdPdGn0MFHsfw7rlu/oQm7IChI/uWd9q5wwo77YojtTDjRnpgZsjqBeynX8T90vFILqsY2K +5CF1OESalwvVr3sZiQX79lisuFKat92u6hBFikFIVxfHHB67Af+g7u0dEHdDW7lwy81MwFYxBTRy +9wIDAQABo4IBbTCCAWkwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwggEEBgNVHSAE +gfwwgfkwgfYGDSsGAQQBvj8CAQoBAQAwgeQwgZoGCCsGAQUFBwICMIGNGoGKQ2VydHlmaWthdCB3 +eXN0YXdpb255IHpnb2RuaWUgeiBkb2t1bWVudGVtOiAiUG9saXR5a2EgQ2VydHlmaWthY2ppIGRs +YSBSb290Q0EiLiBDZXJ0eWZpa2F0IHd5c3Rhd2lvbnkgcHJ6ZXogUm9vdENBIHcgaGllcmFyY2hp +aSBDQyBTaWduZXQuMEUGCCsGAQUFBwIBFjlodHRwOi8vd3d3LnNpZ25ldC5wbC9yZXBvenl0b3Jp +dW0vZG9rdW1lbnR5L3BjX3Jvb3RjYS50eHQwHwYDVR0jBBgwFoAUwJvFIw0C4aZOSGsfAOnjmhQb +sa8wHQYDVR0OBBYEFMODHtVZd1T7TftXR/nEI1zR54njMA0GCSqGSIb3DQEBBQUAA4IBAQBRIHQB +FIGh8Jpxt87AgSLwIEEk4+oGy769u3NtoaR0R3WNMdmt7fXTi0tyTQ9V4AIszxVjhnUPaKnF1KYy +f8Tl+YTzk9ZfFkZ3kCdSaILZAOIrmqWNLPmjUQ5/JiMGho0e1YmWUcMci84+pIisTsytFzVP32/W ++sz2H4FQAvOIMmxB7EJX9AdbnXn9EXZ+4nCqi0ft5z96ZqOJJiCB3vSaoYg+wdkcvb6souMJzuc2 +uptXtR1Xf3ihlHaGW+hmnpcwFA6AoNrom6Vgzk6U1ienx0Cw28BhRSKqzKkyXkuK8gRflZUx84uf +tXncwKJrMiE3lvgOOBITRzcahirLer4c +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIE9zCCA9+gAwIBAgIEPL/xoTANBgkqhkiG9w0BAQUFADB2MQswCQYDVQQGEwJQTDEfMB0GA1UE +ChMWVFAgSW50ZXJuZXQgU3AuIHogby5vLjEkMCIGA1UECxMbQ2VudHJ1bSBDZXJ0eWZpa2Fjamkg +U2lnbmV0MSAwHgYDVQQDExdDQyBTaWduZXQgLSBQQ0EgS2xhc2EgMjAeFw0wMjA0MTkxMDI5NTNa +Fw0xNzA0MTgxMjUzMDdaMHUxCzAJBgNVBAYTAlBMMR8wHQYDVQQKExZUUCBJbnRlcm5ldCBTcC4g +eiBvLm8uMSQwIgYDVQQLExtDZW50cnVtIENlcnR5ZmlrYWNqaSBTaWduZXQxHzAdBgNVBAMTFkND +IFNpZ25ldCAtIENBIEtsYXNhIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqgLJu +QqY4yavbSgHg8CyfKTx4BokNSDOVz4eD9vptUr11Kqd06ED1hlH7Sg0goBFAfntNU/QTKwSBaNui +me7C4sSEdgsKrPoAhGb4Mq8y7Ty7RqZz7mkzNMqzL2L2U4yQ2QjvpH8MH0IBqOWEcpSkpwnrCDIm +RoTfd+YlZWKi2JceQixUUYIQ45Ox8+x8hHbvvZdgqtcvo8PW27qoHkp/7hMuJ44kDAGrmxffBXl/ +OBRZp0uO1CSLcMcVJzyr2phKhy406MYdWrtNPEluGs0GFDzd0nrIctiWAO4cmct4S72S9Q6e//0G +O9f3/Ca5Kb2I1xYLj/xE+HgjHX9aD2MhAgMBAAGjggGMMIIBiDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjCB4wYDVR0gBIHbMIHYMIHVBg0rBgEEAb4/AhQKAQEAMIHDMHUGCCsGAQUF +BwICMGkaZ0NlcnR5ZmlrYXQgd3lzdGF3aW9ueSB6Z29kbmllIHogZG9rdW1lbnRlbTogIlBvbGl0 +eWthIENlcnR5ZmlrYWNqaSBQQ0EyIC0gQ2VydHlmaWthdHkgVXJ6ZWRvdyBLbGFzeSAyIi4wSgYI +KwYBBQUHAgEWPmh0dHA6Ly93d3cuc2lnbmV0LnBsL3JlcG96eXRvcml1bS9kb2t1bWVudHkva2xh +c2EyL3BjX3BjYTIudHh0MD8GA1UdHwQ4MDYwNKAyoDCGLmh0dHA6Ly93d3cuc2lnbmV0LnBsL3Jl +cG96eXRvcml1bS9jcmwvcGNhMi5jcmwwHwYDVR0jBBgwFoAUwGxGyl2CfpYHRonE82AVXO08kMIw +HQYDVR0OBBYEFLtFBlILy4HNKVSzvHxBTM0HDowlMA0GCSqGSIb3DQEBBQUAA4IBAQBWTsCbqXrX +hBBev5v5cIuc6gJM8ww7oR0uMQRZoFSqvQUPWBYM2/TLI/f8UM9hSShUVj3zEsSj/vFHagUVmzuV +Xo5u0WK8iaqATSyEVBhADHrPG6wYcLKJlagge/ILA0m+SieyP2sjYD9MUB9KZIEyBKv0429UuDTw +6P7pslxMWJBSNyQxaLIs0SRKsqZZWkc7ZYAj2apSkBMX2Is1oHA+PwkF6jQMwCao/+CndXPUzfCF +6caa9WwW31W26MlXCvSmJgfiTPwGvm4PkPmOnmWZ3CczzhHl4q7ztHFzshJH3sZWDnrWwBFjzz5e +Pr3WHV1wA7EY6oT4zBx+2gT9XBTB +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEUzCCAzugAwIBAgIEPq+qjzANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQGEwJQTDE3MDUGA1UE +ChMuQ1ppQyBDZW50cmFzdCBTQSB3IGltaWVuaXUgTWluaXN0cmEgR29zcG9kYXJraTEZMBcGA1UE +AxMQQ1ppQyBDZW50cmFzdCBTQTAeFw0wMzA0MzAxMDUwNTVaFw0wODA0MjgxMDUwNTVaMGgxCzAJ +BgNVBAYTAlBMMR8wHQYDVQQKExZUUCBJbnRlcm5ldCBTcC4geiBvLm8uMR8wHQYDVQQDExZDQyBT +aWduZXQgLSBDQSBLbGFzYSAzMRcwFQYDVQQFEw5OdW1lciB3cGlzdTogNDCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBALVdeOM62cPH2NERFxbS5FIp/HSv3fgesdVsTUFxZbGtE+/E0RMl +KZQJHH9emx7vRYubsi4EOLCjYsCOTFvgGRIpZzx7R7T5c0Di5XFkRU4gjBl7aHJoKb5SLzGlWdoX +GsekVtl6keEACrizV2EafqjI8cnBWY7OxQ1ooLQp5AeFjXg+5PT0lO6TUZAubqjFbhVbxSWjqvdj +93RGfyYE76MnNn4c2xWySD07n7uno06TC0IJe6+3WSX1h+76VsIFouWBXOoM7cxxiLjoqdBVu24+ +P8e81SukE7qEvOwDPmk9ZJFtt1nBNg8a1kaixcljrA/43XwOPz6qnJ+cIj/xywECAwEAAaOCAQow +ggEGMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMDMGA1UdIAEB/wQpMCcwJQYEVR0g +ADAdMBsGCCsGAQUFBwIBFg93d3cuY2VudHJhc3QucGwwgY4GA1UdIwSBhjCBg4AU2a7r85Cp1iJN +W0Ca1LR6VG3996ShZaRjMGExCzAJBgNVBAYTAlBMMTcwNQYDVQQKEy5DWmlDIENlbnRyYXN0IFNB +IHcgaW1pZW5pdSBNaW5pc3RyYSBHb3Nwb2RhcmtpMRkwFwYDVQQDExBDWmlDIENlbnRyYXN0IFNB +ggQ9/0sQMB0GA1UdDgQWBBR7Y8wZkHq0zrY7nn1tFSdQ0PlJuTANBgkqhkiG9w0BAQUFAAOCAQEA +ldt/svO5c1MU08FKgrOXCGEbEPbQxhpM0xcd6Iv3dCo6qugEgjEs9Qm5CwUNKMnFsvR27cJWUvZb +MVcvwlwCwclOdwF6u/QRS8bC2HYErhYo9bp9yuxxzuow2A94c5fPqfVrjXy+vDouchAm6+A5Wjzv +J8wxVFDCs+9iGACmyUWr/JGXCYiQIbQkwlkRKHHlan9ymKf1NvIej/3EpeT8fKr6ywxGuhAfqofW +pg3WJY/RCB4lTzD8vZGNwfMFGkWhJkypad3i9w3lGmDVpsHaWtCgGfd0H7tUtWPkP+t7EjIRCD9J +HYnTR+wbbewc5vOI+UobR15ynGfFIaSIiMTVtQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEejCCA2KgAwIBAgIEP4vk6TANBgkqhkiG9w0BAQUFADB1MQswCQYDVQQGEwJQ +TDEfMB0GA1UEChMWVFAgSW50ZXJuZXQgU3AuIHogby5vLjEkMCIGA1UECxMbQ2Vu +dHJ1bSBDZXJ0eWZpa2FjamkgU2lnbmV0MR8wHQYDVQQDExZDQyBTaWduZXQgLSBD +QSBLbGFzYSAyMB4XDTAzMTAxNDExNTgyMloXDTE3MDQxODEyNTMwN1owdzELMAkG +A1UEBhMCUEwxHzAdBgNVBAoTFlRQIEludGVybmV0IFNwLiB6IG8uby4xJDAiBgNV +BAsTG0NlbnRydW0gQ2VydHlmaWthY2ppIFNpZ25ldDEhMB8GA1UEAxMYQ0MgU2ln +bmV0IC0gT0NTUCBLbGFzYSAyMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCo +VCsaBStblXQYVNthe3dvaCrfvKpPXngh4almm988iIlEv9CVTaAdCfaJNihvA+Vs +Qw8++ix1VqteMQE474/MV/YaXigP0Zr0QB+g+/7PWVlv+5U9Gzp9+Xx4DJay8AoI +iB7Iy5Qf9iZiHm5BiPRIuUXT4ZRbZRYPh0/76vgRsQIDAQABo4IBkjCCAY4wDgYD +VR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMJMEEGA1UdHwQ6MDgwNqA0 +oDKGMGh0dHA6Ly93d3cuc2lnbmV0LnBsL3JlcG96eXRvcml1bS9jcmwva2xhc2Ey +LmNybDCB2AYDVR0gBIHQMIHNMIHKBg4rBgEEAb4/AoFICgwBADCBtzBsBggrBgEF +BQcCAjBgGl5DZXJ0eWZpa2F0IHd5ZGFueSB6Z29kbmllIHogZG9rdW1lbnRlbSAi +UG9saXR5a2EgQ2VydHlmaWthY2ppIC0gQ2VydHlmaWthdHkgcmVzcG9uZGVyb3cg +T0NTUCIuMEcGCCsGAQUFBwIBFjtodHRwOi8vd3d3LnNpZ25ldC5wbC9yZXBvenl0 +b3JpdW0vZG9rdW1lbnR5L3BjX29jc3BfMV8wLnBkZjAfBgNVHSMEGDAWgBS7RQZS +C8uBzSlUs7x8QUzNBw6MJTAdBgNVHQ4EFgQUKEVrOY7cEHvsVgvoyZdytlbtgwEw +CQYDVR0TBAIwADANBgkqhkiG9w0BAQUFAAOCAQEAQrRg5MV6dxr0HU2IsLInxhvt +iUVmSFkIUsBCjzLoewOXA16d2oDyHhI/eE+VgAsp+2ANjZu4xRteHIHoYMsN218M +eD2MLRsYS0U9xxAFK9gDj/KscPbrrdoqLvtPSMhUb4adJS9HLhvUe6BicvBf3A71 +iCNe431axGNDWKnpuj2KUpj4CFHYsWCXky847YtTXDjri9NIwJJauazsrSjK+oXp +ngRS506mdQ7vWrtApkh8zhhWp7duCkjcCo1O8JxqYr2qEW1fXmgOISe010v2mmuv +hHxPyVwoAU4KkOw0nbXZn53yak0is5+XmAjh0wWue44AssHrjC9nUh3mkLt6eQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEezCCA2OgAwIBAgIEP4vnLzANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJQ +TDEfMB0GA1UEChMWVFAgSW50ZXJuZXQgU3AuIHogby5vLjEfMB0GA1UEAxMWQ0Mg +U2lnbmV0IC0gQ0EgS2xhc2EgMzEXMBUGA1UEBRMOTnVtZXIgd3Bpc3U6IDQwHhcN +MDMxMDE0MTIwODAwWhcNMDgwNDI4MTA1MDU1WjB3MQswCQYDVQQGEwJQTDEfMB0G +A1UEChMWVFAgSW50ZXJuZXQgU3AuIHogby5vLjEkMCIGA1UECxMbQ2VudHJ1bSBD +ZXJ0eWZpa2FjamkgU2lnbmV0MSEwHwYDVQQDExhDQyBTaWduZXQgLSBPQ1NQIEts +YXNhIDMwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM/9GwvARNuCVN+PqZmO +4FqH8vTqhenUyqRkmAVT4YhLu0a9AXeLAYVDu+NTkYzsAUMAfu55rIKHNLlm6WbF +KvLiKKz4p4pbUr+ToPcwl/TDotidloUdBAxDg0SL+PmQqACZDe3seJho2IYf2vDL +/G4TLMbKmNB0mlWFuN0f4fJNAgMBAAGjggGgMIIBnDAOBgNVHQ8BAf8EBAMCB4Aw +EwYDVR0lBAwwCgYIKwYBBQUHAwkwTwYDVR0fBEgwRjBEoEKgQIY+aHR0cDovL3d3 +dy5zaWduZXQucGwva3dhbGlmaWtvd2FuZS9yZXBvenl0b3JpdW0vY3JsL2tsYXNh +My5jcmwwgdgGA1UdIASB0DCBzTCBygYOKwYBBAG+PwKCLAoCAQAwgbcwbAYIKwYB +BQUHAgIwYBpeQ2VydHlmaWthdCB3eWRhbnkgemdvZG5pZSB6IGRva3VtZW50ZW0g +IlBvbGl0eWthIENlcnR5ZmlrYWNqaSAtIENlcnR5ZmlrYXR5IHJlc3BvbmRlcm93 +IE9DU1AiLjBHBggrBgEFBQcCARY7aHR0cDovL3d3dy5zaWduZXQucGwvcmVwb3p5 +dG9yaXVtL2Rva3VtZW50eS9wY19vY3NwXzFfMC5wZGYwHwYDVR0jBBgwFoAUe2PM +GZB6tM62O559bRUnUND5SbkwHQYDVR0OBBYEFG4jnCMvBALRQXtmDn9TyXQ/EKP+ +MAkGA1UdEwQCMAAwDQYJKoZIhvcNAQEFBQADggEBACXrKG5Def5lpRwmZom3UEDq +bl7y4U3qomG4B+ok2FVZGgPZti+ZgvrenPj7PtbYCUBPsCSTNrznKinoT3gD9lQQ +xkEHwdc6VD1GlFp+qI64u0+wS9Epatrdf7aBnizrOIB4LJd4E2TWQ6trspetjMIU +upyWls1BmYUxB91R7QkTiAUSNZ87s3auhZuG4f0V0JLVCcg2rn7AN1rfMkgxCbHk +GxiQbYWFljl6aatxR3odnnzVUe1I8uoY2JXpmmUcOG4dNGuQYziyKG3mtXCQWvug +5qi9Mf3KUh1oSTKx6HfLjjNl1+wMB5Mdb8LF0XyZLdJM9yIZh7SBRsYm9QiXevY= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFGjCCBAKgAwIBAgIEPL7eEDANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJQTDEfMB0GA1UE +ChMWVFAgSW50ZXJuZXQgU3AuIHogby5vLjEkMCIGA1UECxMbQ2VudHJ1bSBDZXJ0eWZpa2Fjamkg +U2lnbmV0MRswGQYDVQQDExJDQyBTaWduZXQgLSBSb290Q0EwHhcNMDIwNDE4MTQ1NDA4WhcNMjYw +OTIxMTU0MjE5WjB2MQswCQYDVQQGEwJQTDEfMB0GA1UEChMWVFAgSW50ZXJuZXQgU3AuIHogby5v +LjEkMCIGA1UECxMbQ2VudHJ1bSBDZXJ0eWZpa2FjamkgU2lnbmV0MSAwHgYDVQQDExdDQyBTaWdu +ZXQgLSBQQ0EgS2xhc2EgMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM7BrBlbN5ma +M5eg0BOTqoZ+9NBDvU8Lm5rTdrMswFTCathzpVVLK/JD4K3+4oCZ9SRAspEXE4gvwb08ASY6w5s+ +HpRkeJw8YzMFR5kDZD5adgnCAy4vDfIXYZgppXPaTQ8wnfUZ7BZ7Zfa7QBemUIcJIzJBB0UqgtxW +Ceol9IekpBRVmuuSA6QG0Jkm+pGDJ05yj2eQG8jTcBENM7sVA8rGRMyFA4skSZ+D0OG6FS2xC1i9 +JyN0ag1yII/LPx8HK5J4W9MaPRNjAEeaa2qI9EpchwrOxnyVbQfSedCG1VRJfAsE/9tT9CMUPZ3x +W20QjQcSZJqVcmGW9gVsXKQOVLsCAwEAAaOCAbMwggGvMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P +AQH/BAQDAgEGMIIBBAYDVR0gBIH8MIH5MIH2Bg0rBgEEAb4/AgEKAQEBMIHkMIGaBggrBgEFBQcC +AjCBjRqBikNlcnR5ZmlrYXQgd3lzdGF3aW9ueSB6Z29kbmllIHogZG9rdW1lbnRlbTogIlBvbGl0 +eWthIENlcnR5ZmlrYWNqaSBkbGEgUm9vdENBIi4gQ2VydHlmaWthdCB3eXN0YXdpb255IHByemV6 +IFJvb3RDQSB3IGhpZXJhcmNoaWkgQ0MgU2lnbmV0LjBFBggrBgEFBQcCARY5aHR0cDovL3d3dy5z +aWduZXQucGwvcmVwb3p5dG9yaXVtL2Rva3VtZW50eS9wY19yb290Y2EudHh0MEQGA1UdHwQ9MDsw +OaA3oDWGM2h0dHA6Ly93d3cuc2lnbmV0LnBsL3JlcG96eXRvcml1bS9yb290Y2Evcm9vdGNhLmNy +bDAfBgNVHSMEGDAWgBTAm8UjDQLhpk5Iax8A6eOaFBuxrzAdBgNVHQ4EFgQUwGxGyl2CfpYHRonE +82AVXO08kMIwDQYJKoZIhvcNAQEFBQADggEBABp1TAUsa+BeVWg4cjowc8yTJ5XN3GvN96GObMkx +UGY7U9kVrLI71xBgoNVyzXTiMNDBvjh7vdPWjpl5SDiRpnnKiOFXA43HvNWzUaOkTu1mxjJsZsan +ot1Xt6j0ZDC+03FjLHdYMyM9kSWp6afb4980EPYZCcSzgM5TOGfJmNii5Tq468VFKrX+52Aou1G2 +2Ohu+EEOlOrG7ylKv1hHUJJCjwN0ZVEIn1nDbrU9FeGCz8J9ihVUvnENEBbBkU37PWqWuHitKQDV +tcwTwJJdR8cmKq3NmkwAm9fPacidQLpaw0WkuGrS+fEDhu1Nhy9xELP6NA9GRTCNxm/dXlcwnmY= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFGjCCBAKgAwIBAgIEPV0tNDANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJQTDEfMB0GA1UE +ChMWVFAgSW50ZXJuZXQgU3AuIHogby5vLjEkMCIGA1UECxMbQ2VudHJ1bSBDZXJ0eWZpa2Fjamkg +U2lnbmV0MRswGQYDVQQDExJDQyBTaWduZXQgLSBSb290Q0EwHhcNMDIwODE2MTY0OTU2WhcNMjYw +OTIxMTU0MjE5WjB2MQswCQYDVQQGEwJQTDEfMB0GA1UEChMWVFAgSW50ZXJuZXQgU3AuIHogby5v +LjEkMCIGA1UECxMbQ2VudHJ1bSBDZXJ0eWZpa2FjamkgU2lnbmV0MSAwHgYDVQQDExdDQyBTaWdu +ZXQgLSBQQ0EgS2xhc2EgMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALN3LanJtdue +Ne6geWUTFENa+lEuzqELcoqhYB+a/tJcPEkc6TX/bYPzalRRjqs+quMP6KZTU0DixOrV+K7iWaqA +iQ913HX5IBLmKDCrTVW/ZvSDpiBKbxlHfSNuJxAuVT6HdbzK7yAW38ssX+yS2tZYHZ5FhZcfqzPE +OpO94mAKcBUhk6T/ki0evXX/ZvvktwmF3hKattzwtM4JMLurAEl8SInyEYULw5JdlfcBez2Tg6Db +w34hA1A+ckTwhxzecrB8TUe2BnQKOs9vr2cCACpFFcOmPkM0Drtjctr1QHm1tYSqRFRf9VcV5tfC +3P8QqoK4ONjtLPHc9x5NE1uK/FMCAwEAAaOCAbMwggGvMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P +AQH/BAQDAgEGMIIBBAYDVR0gBIH8MIH5MIH2Bg0rBgEEAb4/AgEKAQECMIHkMIGaBggrBgEFBQcC +AjCBjRqBikNlcnR5ZmlrYXQgd3lzdGF3aW9ueSB6Z29kbmllIHogZG9rdW1lbnRlbTogIlBvbGl0 +eWthIENlcnR5ZmlrYWNqaSBkbGEgUm9vdENBIi4gQ2VydHlmaWthdCB3eXN0YXdpb255IHByemV6 +IFJvb3RDQSB3IGhpZXJhcmNoaWkgQ0MgU2lnbmV0LjBFBggrBgEFBQcCARY5aHR0cDovL3d3dy5z +aWduZXQucGwvcmVwb3p5dG9yaXVtL2Rva3VtZW50eS9wY19yb290Y2EudHh0MEQGA1UdHwQ9MDsw +OaA3oDWGM2h0dHA6Ly93d3cuc2lnbmV0LnBsL3JlcG96eXRvcml1bS9yb290Y2Evcm9vdGNhLmNy +bDAfBgNVHSMEGDAWgBTAm8UjDQLhpk5Iax8A6eOaFBuxrzAdBgNVHQ4EFgQUXvthcPHlH5BgGhlM +ErJNXWlhlgAwDQYJKoZIhvcNAQEFBQADggEBACIce95Mvn710KCAISA0CuHD4aznTU6pLoCDShW4 +7OR+GTpJUm1coTcUqlBHV9mra4VFrBcBuOkHZoBLq/jmE0QJWnpSEULDcH9J3mF0nqO9SM+mWyJG +dsJF/XU/7smummgjMNQXwzQTtWORF+6v5KUbWX85anO2wR+M6YTBWC55zWpWi4RG3vkHFs5Ze2oF +JTlpuxw9ZgxTnWlwI9QR2MvEhYIUMKMOWxw1nt0kKj+5TCNQQGh/VJJ1dsiroGh/io1DOcePEhKz +1Ag52y6Wf0nJJB9yk0sFakqZH18F7eQecQImgZyyeRtsG95leNugB3BXWCW+KxwiBrtQTXv4dTE= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEzzCCA7egAwIBAgIEO6ocGTANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJQTDEfMB0GA1UE +ChMWVFAgSW50ZXJuZXQgU3AuIHogby5vLjEkMCIGA1UECxMbQ2VudHJ1bSBDZXJ0eWZpa2Fjamkg +U2lnbmV0MRswGQYDVQQDExJDQyBTaWduZXQgLSBSb290Q0EwHhcNMDEwOTIwMTY0MjE5WhcNMjYw +OTIxMTU0MjE5WjBxMQswCQYDVQQGEwJQTDEfMB0GA1UEChMWVFAgSW50ZXJuZXQgU3AuIHogby5v +LjEkMCIGA1UECxMbQ2VudHJ1bSBDZXJ0eWZpa2FjamkgU2lnbmV0MRswGQYDVQQDExJDQyBTaWdu +ZXQgLSBSb290Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrr2vydnNpELfGW3Ks +ARiDhJvwDtUe4AbWev+OfMc3+vA29nX8ZmIwno3gmItjo5DbUCCRiCMq5c9epcGu+kg4a3BJChVX +REl8gVh0ST15rr3RKrSc4VgsvQzl0ZUraeQLl8JoRT5PLsUj3qwF78jUCQVckiiLVcnGfZtFCm+D +CJXliQBDMB9XFAUEiO/DtEBs0B7wJGx7lgJeJpQUcGiaOPjcJDYOk7rNAYmmD2gWeSlepufO8luU +YG/YDxTC4mqhRqfa4MnVO5dqy+ICj2UvUpHbZDB0KfGRibgBYeQP1kuqgIzJN4UqknVAJb0aMBSP +l+9k2fAUdchx1njlbdcbAgMBAAGjggFtMIIBaTAPBgNVHRMBAf8EBTADAQH/MIIBBAYDVR0gBIH8 +MIH5MIH2Bg0rBgEEAb4/AgEKAQEAMIHkMIGaBggrBgEFBQcCAjCBjRqBikNlcnR5ZmlrYXQgd3lz +dGF3aW9ueSB6Z29kbmllIHogZG9rdW1lbnRlbTogIlBvbGl0eWthIENlcnR5ZmlrYWNqaSBkbGEg +Um9vdENBIi4gQ2VydHlmaWthdCB3eXN0YXdpb255IHByemV6IFJvb3RDQSB3IGhpZXJhcmNoaWkg +Q0MgU2lnbmV0LjBFBggrBgEFBQcCARY5aHR0cDovL3d3dy5zaWduZXQucGwvcmVwb3p5dG9yaXVt +L2Rva3VtZW50eS9wY19yb290Y2EudHh0MB0GA1UdDgQWBBTAm8UjDQLhpk5Iax8A6eOaFBuxrzAf +BgNVHSMEGDAWgBTAm8UjDQLhpk5Iax8A6eOaFBuxrzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcN +AQEFBQADggEBAGnY5QmYqnnO9OqFOWZxxb25UHRnaRF6IV9aaGit5BZufZj2Tq3v8L3SgE34GOoI +cdRMMG5JEpEU4mN/Ef3oY6Eo+7HfqaPHI4KFmbDSPiK5s+wmf+bQSm0Yq5/h4ZOdcAESlLQeLSt1 +CQk2JoKQJ6pyAf6xJBgWEIlm4RXE4J3324PUiOp83kW6MDvaa1xY976WyInr4rwoLgxVl11LZeKW +ha0RJJxJgw/NyWpKG7LWCm1fglF8JH51vZNndGYq1iKtfnrIOvLZq6bzaCiZm1EurD8HE6P7pmAB +KK6o3C2OXlNfNIgwkDN/cDqk5TYsTkrpfriJPdxXBH8hQOkW89g= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID/TCCA2agAwIBAgIEP4/gkTANBgkqhkiG9w0BAQUFADB1MQswCQYDVQQGEwJQTDEfMB0GA1UE +ChMWVFAgSW50ZXJuZXQgU3AuIHogby5vLjEkMCIGA1UECxMbQ2VudHJ1bSBDZXJ0eWZpa2Fjamkg +U2lnbmV0MR8wHQYDVQQDExZDQyBTaWduZXQgLSBDQSBLbGFzYSAxMB4XDTAzMTAxNzEyMjkwMloX +DTExMDkyMzExMTgxN1owdjELMAkGA1UEBhMCUEwxHzAdBgNVBAoTFlRQIEludGVybmV0IFNwLiB6 +IG8uby4xJDAiBgNVBAsTG0NlbnRydW0gQ2VydHlmaWthY2ppIFNpZ25ldDEgMB4GA1UEAxMXQ0Mg +U2lnbmV0IC0gVFNBIEtsYXNhIDEwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOJYrISEtSsd +uHajROh5/n7NGrkpYTT9NEaPe9+ucuQ37KxIbfJwXJjgUc1dw4wCkcQ12FJarD1X6mSQ4cfN/60v +LfKI5ZD4nhJTMKlAj1pX9ScQ/MuyvKStCbn5WTkjPhjRAM0tdwXSnzuTEunfw0Oup559y3Iqxg1c +ExflB6cfAgMBAAGjggGXMIIBkzBBBgNVHR8EOjA4MDagNKAyhjBodHRwOi8vd3d3LnNpZ25ldC5w +bC9yZXBvenl0b3JpdW0vY3JsL2tsYXNhMS5jcmwwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQM +MAoGCCsGAQUFBwMIMIHaBgNVHSAEgdIwgc8wgcwGDSsGAQQBvj8CZAoRAgEwgbowbwYIKwYBBQUH +AgIwYxphQ2VydHlmaWthdCB3eXN0YXdpb255IHpnb2RuaWUgeiBkb2t1bWVudGVtICJQb2xpdHlr +YSBDZXJ0eWZpa2FjamkgQ0MgU2lnbmV0IC0gWm5ha293YW5pZSBjemFzZW0iLjBHBggrBgEFBQcC +ARY7aHR0cDovL3d3dy5zaWduZXQucGwvcmVwb3p5dG9yaXVtL2Rva3VtZW50eS9wY190c2ExXzJf +MS5wZGYwHwYDVR0jBBgwFoAUw4Me1Vl3VPtN+1dH+cQjXNHnieMwHQYDVR0OBBYEFJdDwEqtcavO +Yd9u9tej53vWXwNBMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQEFBQADgYEAnpiQkqLCJQYXUrqMHUEz ++z3rOqS0XzSFnVVLhkVssvXc8S3FkJIiQTUrkScjI4CToCzujj3EyfNxH6yiLlMbskF8I31JxIeB +vueqV+s+o76CZm3ycu9hb0I4lswuxoT+q5ZzPR8Irrb51rZXlolR+7KtwMg4sFDJZ8RNgOf7tbA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEFTCCA36gAwIBAgIBADANBgkqhkiG9w0BAQQFADCBvjELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0luZGlhbmExFTATBgNVBAcTDEluZGlhbmFwb2xpczEoMCYGA1UE +ChMfU29mdHdhcmUgaW4gdGhlIFB1YmxpYyBJbnRlcmVzdDETMBEGA1UECxMKaG9z +dG1hc3RlcjEgMB4GA1UEAxMXQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxJTAjBgkq +hkiG9w0BCQEWFmhvc3RtYXN0ZXJAc3BpLWluYy5vcmcwHhcNMDMwMTE1MTYyOTE3 +WhcNMDcwMTE0MTYyOTE3WjCBvjELMAkGA1UEBhMCVVMxEDAOBgNVBAgTB0luZGlh +bmExFTATBgNVBAcTDEluZGlhbmFwb2xpczEoMCYGA1UEChMfU29mdHdhcmUgaW4g +dGhlIFB1YmxpYyBJbnRlcmVzdDETMBEGA1UECxMKaG9zdG1hc3RlcjEgMB4GA1UE +AxMXQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxJTAjBgkqhkiG9w0BCQEWFmhvc3Rt +YXN0ZXJAc3BpLWluYy5vcmcwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAPB6 +rdoiLR3RodtM22LMcfwfqb5OrJNl7fwmvskgF7yP6sdD2bOfDIXhg9852jhY8/kL +VOFe1ELAL2OyN4RAxk0rliZQVgeTgqvgkOVIBbNwgnjN6mqtuWzFiPL+NXQExq40 +I3whM+4lEiwSHaV+MYxWanMdhc+kImT50LKfkxcdAgMBAAGjggEfMIIBGzAdBgNV +HQ4EFgQUB63oQR1/vda/G4F6P4xLiN4E0vowgesGA1UdIwSB4zCB4IAUB63oQR1/ +vda/G4F6P4xLiN4E0vqhgcSkgcEwgb4xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdJ +bmRpYW5hMRUwEwYDVQQHEwxJbmRpYW5hcG9saXMxKDAmBgNVBAoTH1NvZnR3YXJl +IGluIHRoZSBQdWJsaWMgSW50ZXJlc3QxEzARBgNVBAsTCmhvc3RtYXN0ZXIxIDAe +BgNVBAMTF0NlcnRpZmljYXRpb24gQXV0aG9yaXR5MSUwIwYJKoZIhvcNAQkBFhZo +b3N0bWFzdGVyQHNwaS1pbmMub3JnggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcN +AQEEBQADgYEAm/Abn8c2y1nO3fgpAIslxvi9iNBZDhQtJ0VQZY6wgSfANyDOR4DW +iexO/AlorB49KnkFS7TjCAoLOZhcg5FaNiKnlstMI5krQmau1Qnb/vGSNsE/UGms +1ts+QYPUs0KmGEAFUri2XzLy+aQo9Kw74VBvqnxvaaMeY5yMcKNOieY= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIIDjCCBfagAwIBAgIJAOiOtsn4KhQoMA0GCSqGSIb3DQEBBQUAMIG8MQswCQYD +VQQGEwJVUzEQMA4GA1UECBMHSW5kaWFuYTEVMBMGA1UEBxMMSW5kaWFuYXBvbGlz +MSgwJgYDVQQKEx9Tb2Z0d2FyZSBpbiB0aGUgUHVibGljIEludGVyZXN0MRMwEQYD +VQQLEwpob3N0bWFzdGVyMR4wHAYDVQQDExVDZXJ0aWZpY2F0ZSBBdXRob3JpdHkx +JTAjBgkqhkiG9w0BCQEWFmhvc3RtYXN0ZXJAc3BpLWluYy5vcmcwHhcNMDgwNTEz +MDgwNzU2WhcNMTgwNTExMDgwNzU2WjCBvDELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0luZGlhbmExFTATBgNVBAcTDEluZGlhbmFwb2xpczEoMCYGA1UEChMfU29mdHdh +cmUgaW4gdGhlIFB1YmxpYyBJbnRlcmVzdDETMBEGA1UECxMKaG9zdG1hc3RlcjEe +MBwGA1UEAxMVQ2VydGlmaWNhdGUgQXV0aG9yaXR5MSUwIwYJKoZIhvcNAQkBFhZo +b3N0bWFzdGVyQHNwaS1pbmMub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEA3DbmR0LCxFF1KYdAw9iOIQbSGE7r7yC9kDyFEBOMKVuUY/b0LfEGQpG5 +GcRCaQi/izZF6igFM0lIoCdDkzWKQdh4s/Dvs24t3dHLfer0dSbTPpA67tfnLAS1 +fOH1fMVO73e9XKKTM5LOfYFIz2u1IiwIg/3T1c87Lf21SZBb9q1NE8re06adU1Fx +Y0b4ShZcmO4tbZoWoXaQ4mBDmdaJ1mwuepiyCwMs43pPx93jzONKao15Uvr0wa8u +jyoIyxspgpJyQ7zOiKmqp4pRQ1WFmjcDeJPI8L20QcgHQprLNZd6ioFl3h1UCAHx +ZFy3FxpRvB7DWYd2GBaY7r/2Z4GLBjXFS21ZGcfSxki+bhQog0oQnBv1b7ypjvVp +/rLBVcznFMn5WxRTUQfqzj3kTygfPGEJ1zPSbqdu1McTCW9rXRTunYkbpWry9vjQ +co7qch8vNGopCsUK7BxAhRL3pqXTT63AhYxMfHMgzFMY8bJYTAH1v+pk1Vw5xc5s +zFNaVrpBDyXfa1C2x4qgvQLCxTtVpbJkIoRRKFauMe5e+wsWTUYFkYBE7axt8Feo ++uthSKDLG7Mfjs3FIXcDhB78rKNDCGOM7fkn77SwXWfWT+3Qiz5dW8mRvZYChD3F +TbxCP3T9PF2sXEg2XocxLxhsxGjuoYvJWdAY4wCAs1QnLpnwFVMCAwEAAaOCAg8w +ggILMB0GA1UdDgQWBBQ0cdE41xU2g0dr1zdkQjuOjVKdqzCB8QYDVR0jBIHpMIHm +gBQ0cdE41xU2g0dr1zdkQjuOjVKdq6GBwqSBvzCBvDELMAkGA1UEBhMCVVMxEDAO +BgNVBAgTB0luZGlhbmExFTATBgNVBAcTDEluZGlhbmFwb2xpczEoMCYGA1UEChMf +U29mdHdhcmUgaW4gdGhlIFB1YmxpYyBJbnRlcmVzdDETMBEGA1UECxMKaG9zdG1h +c3RlcjEeMBwGA1UEAxMVQ2VydGlmaWNhdGUgQXV0aG9yaXR5MSUwIwYJKoZIhvcN +AQkBFhZob3N0bWFzdGVyQHNwaS1pbmMub3JnggkA6I62yfgqFCgwDwYDVR0TAQH/ +BAUwAwEB/zARBglghkgBhvhCAQEEBAMCAAcwCQYDVR0SBAIwADAuBglghkgBhvhC +AQ0EIRYfU29mdHdhcmUgaW4gdGhlIFB1YmxpYyBJbnRlcmVzdDAwBglghkgBhvhC +AQQEIxYhaHR0cHM6Ly9jYS5zcGktaW5jLm9yZy9jYS1jcmwucGVtMDIGCWCGSAGG ++EIBAwQlFiNodHRwczovL2NhLnNwaS1pbmMub3JnL2NlcnQtY3JsLnBlbTAhBgNV +HREEGjAYgRZob3N0bWFzdGVyQHNwaS1pbmMub3JnMA4GA1UdDwEB/wQEAwIBBjAN +BgkqhkiG9w0BAQUFAAOCAgEAtM294LnqsgMrfjLp3nI/yUuCXp3ir1UJogxU6M8Y +PCggHam7AwIvUjki+RfPrWeQswN/2BXja367m1YBrzXU2rnHZxeb1NUON7MgQS4M +AcRb+WU+wmHo0vBqlXDDxm/VNaSsWXLhid+hoJ0kvSl56WEq2dMeyUakCHhBknIP +qxR17QnwovBc78MKYiC3wihmrkwvLo9FYyaW8O4x5otVm6o6+YI5HYg84gd1GuEP +sTC8cTLSOv76oYnzQyzWcsR5pxVIBcDYLXIC48s9Fmq6ybgREOJJhcyWR2AFJS7v +dVkz9UcZFu/abF8HyKZQth3LZjQl/GaD68W2MEH4RkRiqMEMVObqTFoo5q7Gt/5/ +O5aoLu7HaD7dAD0prypjq1/uSSotxdz70cbT0ZdWUoa2lOvUYFG3/B6bzAKb1B+P ++UqPti4oOxfMxaYF49LTtcYDyeFIQpvLP+QX4P4NAZUJurgNceQJcHdC2E3hQqlg +g9cXiUPS1N2nGLar1CQlh7XU4vwuImm9rWgs/3K1mKoGnOcqarihk3bOsPN/nOHg +T7jYhkalMwIsJWE3KpLIrIF0aGOHM3a9BX9e1dUCbb2v/ypaqknsmHlHU5H2DjRa +yaXG67Ljxay2oHA1u8hRadDytaIybrw/oDc5fHE2pgXfDBLkFqfF1stjo5VwP+YE +o2A= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDnzCCAoegAwIBAgIBJjANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJERTEc +MBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxlU2Vj +IFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290IENB +IDIwHhcNOTkwNzA5MTIxMTAwWhcNMTkwNzA5MjM1OTAwWjBxMQswCQYDVQQGEwJE +RTEcMBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxl +U2VjIFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290 +IENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrC6M14IspFLEU +ha88EOQ5bzVdSq7d6mGNlUn0b2SjGmBmpKlAIoTZ1KXleJMOaAGtuU1cOs7TuKhC +QN/Po7qCWWqSG6wcmtoIKyUn+WkjR/Hg6yx6m/UTAtB+NHzCnjwAWav12gz1Mjwr +rFDa1sPeg5TKqAyZMg4ISFZbavva4VhYAUlfckE8FQYBjl2tqriTtM2e66foai1S +NNs671x1Udrb8zH57nGYMsRUFUQM+ZtV7a3fGAigo4aKSe5TBY8ZTNXeWHmb0moc +QqvF1afPaA+W5OFhmHZhyJF81j4A4pFQh+GdCuatl9Idxjp9y7zaAzTVjlsB9WoH +txa2bkp/AgMBAAGjQjBAMB0GA1UdDgQWBBQxw3kbuvVT1xfgiXotF2wKsyudMzAP +BgNVHRMECDAGAQH/AgEFMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOC +AQEAlGRZrTlk5ynrE/5aw4sTV8gEJPB0d8Bg42f76Ymmg7+Wgnxu1MM9756Abrsp +tJh6sTtU6zkXR34ajgv8HzFZMQSyzhfzLMdiNlXiItiJVbSYSKpk+tYcNthEeFpa +IzpXl/V6ME+un2pMSyuOoAPjPuCp1NJ70rOo4nI8rZ7/gFnkm0W09juwzTkZmDLl +6iFhkOQxIY40sfcvNUqFENrnijchvllj4PKFiDFT1FQUhXB59C4Gdyd1Lx+4ivn+ +xbrYNuSD7Odlt79jWvNGr4GUN9RBjNYj1h7P9WgbRGOiWrqnNVmh5XAFmw4jV5mU +Cm26OWMohpLzGITY+9HPBVZkVw== +-----END CERTIFICATE----- + diff --git a/libs/tornado/curl_httpclient.py b/libs/tornado/curl_httpclient.py new file mode 100644 index 00000000..a338cb8d --- /dev/null +++ b/libs/tornado/curl_httpclient.py @@ -0,0 +1,435 @@ +#!/usr/bin/env python +# +# Copyright 2009 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Blocking and non-blocking HTTP client implementations using pycurl.""" + +from __future__ import with_statement + +import cStringIO +import collections +import logging +import pycurl +import threading +import time + +from tornado import httputil +from tornado import ioloop +from tornado import stack_context + +from tornado.escape import utf8 +from tornado.httpclient import HTTPRequest, HTTPResponse, HTTPError, AsyncHTTPClient, main + +class CurlAsyncHTTPClient(AsyncHTTPClient): + def initialize(self, io_loop=None, max_clients=10, + max_simultaneous_connections=None): + self.io_loop = io_loop + self._multi = pycurl.CurlMulti() + self._multi.setopt(pycurl.M_TIMERFUNCTION, self._set_timeout) + self._multi.setopt(pycurl.M_SOCKETFUNCTION, self._handle_socket) + self._curls = [_curl_create(max_simultaneous_connections) + for i in xrange(max_clients)] + self._free_list = self._curls[:] + self._requests = collections.deque() + self._fds = {} + self._timeout = None + + try: + self._socket_action = self._multi.socket_action + except AttributeError: + # socket_action is found in pycurl since 7.18.2 (it's been + # in libcurl longer than that but wasn't accessible to + # python). + logging.warning("socket_action method missing from pycurl; " + "falling back to socket_all. Upgrading " + "libcurl and pycurl will improve performance") + self._socket_action = \ + lambda fd, action: self._multi.socket_all() + + # libcurl has bugs that sometimes cause it to not report all + # relevant file descriptors and timeouts to TIMERFUNCTION/ + # SOCKETFUNCTION. Mitigate the effects of such bugs by + # forcing a periodic scan of all active requests. + self._force_timeout_callback = ioloop.PeriodicCallback( + self._handle_force_timeout, 1000, io_loop=io_loop) + self._force_timeout_callback.start() + + def close(self): + self._force_timeout_callback.stop() + for curl in self._curls: + curl.close() + self._multi.close() + self._closed = True + super(CurlAsyncHTTPClient, self).close() + + def fetch(self, request, callback, **kwargs): + if not isinstance(request, HTTPRequest): + request = HTTPRequest(url=request, **kwargs) + self._requests.append((request, stack_context.wrap(callback))) + self._process_queue() + self._set_timeout(0) + + def _handle_socket(self, event, fd, multi, data): + """Called by libcurl when it wants to change the file descriptors + it cares about. + """ + event_map = { + pycurl.POLL_NONE: ioloop.IOLoop.NONE, + pycurl.POLL_IN: ioloop.IOLoop.READ, + pycurl.POLL_OUT: ioloop.IOLoop.WRITE, + pycurl.POLL_INOUT: ioloop.IOLoop.READ | ioloop.IOLoop.WRITE + } + if event == pycurl.POLL_REMOVE: + self.io_loop.remove_handler(fd) + del self._fds[fd] + else: + ioloop_event = event_map[event] + if fd not in self._fds: + self._fds[fd] = ioloop_event + self.io_loop.add_handler(fd, self._handle_events, + ioloop_event) + else: + self._fds[fd] = ioloop_event + self.io_loop.update_handler(fd, ioloop_event) + + def _set_timeout(self, msecs): + """Called by libcurl to schedule a timeout.""" + if self._timeout is not None: + self.io_loop.remove_timeout(self._timeout) + self._timeout = self.io_loop.add_timeout( + time.time() + msecs/1000.0, self._handle_timeout) + + def _handle_events(self, fd, events): + """Called by IOLoop when there is activity on one of our + file descriptors. + """ + action = 0 + if events & ioloop.IOLoop.READ: action |= pycurl.CSELECT_IN + if events & ioloop.IOLoop.WRITE: action |= pycurl.CSELECT_OUT + while True: + try: + ret, num_handles = self._socket_action(fd, action) + except pycurl.error, e: + ret = e.args[0] + if ret != pycurl.E_CALL_MULTI_PERFORM: + break + self._finish_pending_requests() + + def _handle_timeout(self): + """Called by IOLoop when the requested timeout has passed.""" + with stack_context.NullContext(): + self._timeout = None + while True: + try: + ret, num_handles = self._socket_action( + pycurl.SOCKET_TIMEOUT, 0) + except pycurl.error, e: + ret = e.args[0] + if ret != pycurl.E_CALL_MULTI_PERFORM: + break + self._finish_pending_requests() + + # In theory, we shouldn't have to do this because curl will + # call _set_timeout whenever the timeout changes. However, + # sometimes after _handle_timeout we will need to reschedule + # immediately even though nothing has changed from curl's + # perspective. This is because when socket_action is + # called with SOCKET_TIMEOUT, libcurl decides internally which + # timeouts need to be processed by using a monotonic clock + # (where available) while tornado uses python's time.time() + # to decide when timeouts have occurred. When those clocks + # disagree on elapsed time (as they will whenever there is an + # NTP adjustment), tornado might call _handle_timeout before + # libcurl is ready. After each timeout, resync the scheduled + # timeout with libcurl's current state. + new_timeout = self._multi.timeout() + if new_timeout != -1: + self._set_timeout(new_timeout) + + def _handle_force_timeout(self): + """Called by IOLoop periodically to ask libcurl to process any + events it may have forgotten about. + """ + with stack_context.NullContext(): + while True: + try: + ret, num_handles = self._multi.socket_all() + except pycurl.error, e: + ret = e.args[0] + if ret != pycurl.E_CALL_MULTI_PERFORM: + break + self._finish_pending_requests() + + def _finish_pending_requests(self): + """Process any requests that were completed by the last + call to multi.socket_action. + """ + while True: + num_q, ok_list, err_list = self._multi.info_read() + for curl in ok_list: + self._finish(curl) + for curl, errnum, errmsg in err_list: + self._finish(curl, errnum, errmsg) + if num_q == 0: + break + self._process_queue() + + def _process_queue(self): + with stack_context.NullContext(): + while True: + started = 0 + while self._free_list and self._requests: + started += 1 + curl = self._free_list.pop() + (request, callback) = self._requests.popleft() + curl.info = { + "headers": httputil.HTTPHeaders(), + "buffer": cStringIO.StringIO(), + "request": request, + "callback": callback, + "curl_start_time": time.time(), + } + # Disable IPv6 to mitigate the effects of this bug + # on curl versions <= 7.21.0 + # http://sourceforge.net/tracker/?func=detail&aid=3017819&group_id=976&atid=100976 + if pycurl.version_info()[2] <= 0x71500: # 7.21.0 + curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_V4) + _curl_setup_request(curl, request, curl.info["buffer"], + curl.info["headers"]) + self._multi.add_handle(curl) + + if not started: + break + + def _finish(self, curl, curl_error=None, curl_message=None): + info = curl.info + curl.info = None + self._multi.remove_handle(curl) + self._free_list.append(curl) + buffer = info["buffer"] + if curl_error: + error = CurlError(curl_error, curl_message) + code = error.code + effective_url = None + buffer.close() + buffer = None + else: + error = None + code = curl.getinfo(pycurl.HTTP_CODE) + effective_url = curl.getinfo(pycurl.EFFECTIVE_URL) + buffer.seek(0) + # the various curl timings are documented at + # http://curl.haxx.se/libcurl/c/curl_easy_getinfo.html + time_info = dict( + queue=info["curl_start_time"] - info["request"].start_time, + namelookup=curl.getinfo(pycurl.NAMELOOKUP_TIME), + connect=curl.getinfo(pycurl.CONNECT_TIME), + pretransfer=curl.getinfo(pycurl.PRETRANSFER_TIME), + starttransfer=curl.getinfo(pycurl.STARTTRANSFER_TIME), + total=curl.getinfo(pycurl.TOTAL_TIME), + redirect=curl.getinfo(pycurl.REDIRECT_TIME), + ) + try: + info["callback"](HTTPResponse( + request=info["request"], code=code, headers=info["headers"], + buffer=buffer, effective_url=effective_url, error=error, + request_time=time.time() - info["curl_start_time"], + time_info=time_info)) + except Exception: + self.handle_callback_exception(info["callback"]) + + + def handle_callback_exception(self, callback): + self.io_loop.handle_callback_exception(callback) + + +class CurlError(HTTPError): + def __init__(self, errno, message): + HTTPError.__init__(self, 599, message) + self.errno = errno + + +def _curl_create(max_simultaneous_connections=None): + curl = pycurl.Curl() + if logging.getLogger().isEnabledFor(logging.DEBUG): + curl.setopt(pycurl.VERBOSE, 1) + curl.setopt(pycurl.DEBUGFUNCTION, _curl_debug) + curl.setopt(pycurl.MAXCONNECTS, max_simultaneous_connections or 5) + return curl + + +def _curl_setup_request(curl, request, buffer, headers): + curl.setopt(pycurl.URL, utf8(request.url)) + + # libcurl's magic "Expect: 100-continue" behavior causes delays + # with servers that don't support it (which include, among others, + # Google's OpenID endpoint). Additionally, this behavior has + # a bug in conjunction with the curl_multi_socket_action API + # (https://sourceforge.net/tracker/?func=detail&atid=100976&aid=3039744&group_id=976), + # which increases the delays. It's more trouble than it's worth, + # so just turn off the feature (yes, setting Expect: to an empty + # value is the official way to disable this) + if "Expect" not in request.headers: + request.headers["Expect"] = "" + + # libcurl adds Pragma: no-cache by default; disable that too + if "Pragma" not in request.headers: + request.headers["Pragma"] = "" + + # Request headers may be either a regular dict or HTTPHeaders object + if isinstance(request.headers, httputil.HTTPHeaders): + curl.setopt(pycurl.HTTPHEADER, + [utf8("%s: %s" % i) for i in request.headers.get_all()]) + else: + curl.setopt(pycurl.HTTPHEADER, + [utf8("%s: %s" % i) for i in request.headers.iteritems()]) + + if request.header_callback: + curl.setopt(pycurl.HEADERFUNCTION, request.header_callback) + else: + curl.setopt(pycurl.HEADERFUNCTION, + lambda line: _curl_header_callback(headers, line)) + if request.streaming_callback: + curl.setopt(pycurl.WRITEFUNCTION, request.streaming_callback) + else: + curl.setopt(pycurl.WRITEFUNCTION, buffer.write) + curl.setopt(pycurl.FOLLOWLOCATION, request.follow_redirects) + curl.setopt(pycurl.MAXREDIRS, request.max_redirects) + curl.setopt(pycurl.CONNECTTIMEOUT_MS, int(1000 * request.connect_timeout)) + curl.setopt(pycurl.TIMEOUT_MS, int(1000 * request.request_timeout)) + if request.user_agent: + curl.setopt(pycurl.USERAGENT, utf8(request.user_agent)) + else: + curl.setopt(pycurl.USERAGENT, "Mozilla/5.0 (compatible; pycurl)") + if request.network_interface: + curl.setopt(pycurl.INTERFACE, request.network_interface) + if request.use_gzip: + curl.setopt(pycurl.ENCODING, "gzip,deflate") + else: + curl.setopt(pycurl.ENCODING, "none") + if request.proxy_host and request.proxy_port: + curl.setopt(pycurl.PROXY, request.proxy_host) + curl.setopt(pycurl.PROXYPORT, request.proxy_port) + if request.proxy_username: + credentials = '%s:%s' % (request.proxy_username, + request.proxy_password) + curl.setopt(pycurl.PROXYUSERPWD, credentials) + else: + curl.setopt(pycurl.PROXY, '') + if request.validate_cert: + curl.setopt(pycurl.SSL_VERIFYPEER, 1) + curl.setopt(pycurl.SSL_VERIFYHOST, 2) + else: + curl.setopt(pycurl.SSL_VERIFYPEER, 0) + curl.setopt(pycurl.SSL_VERIFYHOST, 0) + if request.ca_certs is not None: + curl.setopt(pycurl.CAINFO, request.ca_certs) + else: + # There is no way to restore pycurl.CAINFO to its default value + # (Using unsetopt makes it reject all certificates). + # I don't see any way to read the default value from python so it + # can be restored later. We'll have to just leave CAINFO untouched + # if no ca_certs file was specified, and require that if any + # request uses a custom ca_certs file, they all must. + pass + + if request.allow_ipv6 is False: + # Curl behaves reasonably when DNS resolution gives an ipv6 address + # that we can't reach, so allow ipv6 unless the user asks to disable. + # (but see version check in _process_queue above) + curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_V4) + + # Set the request method through curl's irritating interface which makes + # up names for almost every single method + curl_options = { + "GET": pycurl.HTTPGET, + "POST": pycurl.POST, + "PUT": pycurl.UPLOAD, + "HEAD": pycurl.NOBODY, + } + custom_methods = set(["DELETE"]) + for o in curl_options.values(): + curl.setopt(o, False) + if request.method in curl_options: + curl.unsetopt(pycurl.CUSTOMREQUEST) + curl.setopt(curl_options[request.method], True) + elif request.allow_nonstandard_methods or request.method in custom_methods: + curl.setopt(pycurl.CUSTOMREQUEST, request.method) + else: + raise KeyError('unknown method ' + request.method) + + # Handle curl's cryptic options for every individual HTTP method + if request.method in ("POST", "PUT"): + request_buffer = cStringIO.StringIO(utf8(request.body)) + curl.setopt(pycurl.READFUNCTION, request_buffer.read) + if request.method == "POST": + def ioctl(cmd): + if cmd == curl.IOCMD_RESTARTREAD: + request_buffer.seek(0) + curl.setopt(pycurl.IOCTLFUNCTION, ioctl) + curl.setopt(pycurl.POSTFIELDSIZE, len(request.body)) + else: + curl.setopt(pycurl.INFILESIZE, len(request.body)) + + if request.auth_username is not None: + userpwd = "%s:%s" % (request.auth_username, request.auth_password or '') + curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC) + curl.setopt(pycurl.USERPWD, utf8(userpwd)) + logging.debug("%s %s (username: %r)", request.method, request.url, + request.auth_username) + else: + curl.unsetopt(pycurl.USERPWD) + logging.debug("%s %s", request.method, request.url) + + if request.client_key is not None or request.client_cert is not None: + raise ValueError("Client certificate not supported with curl_httpclient") + + if threading.activeCount() > 1: + # libcurl/pycurl is not thread-safe by default. When multiple threads + # are used, signals should be disabled. This has the side effect + # of disabling DNS timeouts in some environments (when libcurl is + # not linked against ares), so we don't do it when there is only one + # thread. Applications that use many short-lived threads may need + # to set NOSIGNAL manually in a prepare_curl_callback since + # there may not be any other threads running at the time we call + # threading.activeCount. + curl.setopt(pycurl.NOSIGNAL, 1) + if request.prepare_curl_callback is not None: + request.prepare_curl_callback(curl) + + +def _curl_header_callback(headers, header_line): + # header_line as returned by curl includes the end-of-line characters. + header_line = header_line.strip() + if header_line.startswith("HTTP/"): + headers.clear() + return + if not header_line: + return + headers.parse_line(header_line) + +def _curl_debug(debug_type, debug_msg): + debug_types = ('I', '<', '>', '<', '>') + if debug_type == 0: + logging.debug('%s', debug_msg.strip()) + elif debug_type in (1, 2): + for line in debug_msg.splitlines(): + logging.debug('%s %s', debug_types[debug_type], line) + elif debug_type == 4: + logging.debug('%s %r', debug_types[debug_type], debug_msg) + +if __name__ == "__main__": + AsyncHTTPClient.configure(CurlAsyncHTTPClient) + main() diff --git a/libs/tornado/database.py b/libs/tornado/database.py new file mode 100644 index 00000000..97717137 --- /dev/null +++ b/libs/tornado/database.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python +# +# Copyright 2009 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""A lightweight wrapper around MySQLdb.""" + +import copy +import MySQLdb.constants +import MySQLdb.converters +import MySQLdb.cursors +import itertools +import logging +import time + +class Connection(object): + """A lightweight wrapper around MySQLdb DB-API connections. + + The main value we provide is wrapping rows in a dict/object so that + columns can be accessed by name. Typical usage:: + + db = database.Connection("localhost", "mydatabase") + for article in db.query("SELECT * FROM articles"): + print article.title + + Cursors are hidden by the implementation, but other than that, the methods + are very similar to the DB-API. + + We explicitly set the timezone to UTC and the character encoding to + UTF-8 on all connections to avoid time zone and encoding errors. + """ + def __init__(self, host, database, user=None, password=None, + max_idle_time=7*3600): + self.host = host + self.database = database + self.max_idle_time = max_idle_time + + args = dict(conv=CONVERSIONS, use_unicode=True, charset="utf8", + db=database, init_command='SET time_zone = "+0:00"', + sql_mode="TRADITIONAL") + if user is not None: + args["user"] = user + if password is not None: + args["passwd"] = password + + # We accept a path to a MySQL socket file or a host(:port) string + if "/" in host: + args["unix_socket"] = host + else: + self.socket = None + pair = host.split(":") + if len(pair) == 2: + args["host"] = pair[0] + args["port"] = int(pair[1]) + else: + args["host"] = host + args["port"] = 3306 + + self._db = None + self._db_args = args + self._last_use_time = time.time() + try: + self.reconnect() + except Exception: + logging.error("Cannot connect to MySQL on %s", self.host, + exc_info=True) + + def __del__(self): + self.close() + + def close(self): + """Closes this database connection.""" + if getattr(self, "_db", None) is not None: + self._db.close() + self._db = None + + def reconnect(self): + """Closes the existing database connection and re-opens it.""" + self.close() + self._db = MySQLdb.connect(**self._db_args) + self._db.autocommit(True) + + def iter(self, query, *parameters): + """Returns an iterator for the given query and parameters.""" + self._ensure_connected() + cursor = MySQLdb.cursors.SSCursor(self._db) + try: + self._execute(cursor, query, parameters) + column_names = [d[0] for d in cursor.description] + for row in cursor: + yield Row(zip(column_names, row)) + finally: + cursor.close() + + def query(self, query, *parameters): + """Returns a row list for the given query and parameters.""" + cursor = self._cursor() + try: + self._execute(cursor, query, parameters) + column_names = [d[0] for d in cursor.description] + return [Row(itertools.izip(column_names, row)) for row in cursor] + finally: + cursor.close() + + def get(self, query, *parameters): + """Returns the first row returned for the given query.""" + rows = self.query(query, *parameters) + if not rows: + return None + elif len(rows) > 1: + raise Exception("Multiple rows returned for Database.get() query") + else: + return rows[0] + + # rowcount is a more reasonable default return value than lastrowid, + # but for historical compatibility execute() must return lastrowid. + def execute(self, query, *parameters): + """Executes the given query, returning the lastrowid from the query.""" + return self.execute_lastrowid(query, *parameters) + + def execute_lastrowid(self, query, *parameters): + """Executes the given query, returning the lastrowid from the query.""" + cursor = self._cursor() + try: + self._execute(cursor, query, parameters) + return cursor.lastrowid + finally: + cursor.close() + + def execute_rowcount(self, query, *parameters): + """Executes the given query, returning the rowcount from the query.""" + cursor = self._cursor() + try: + self._execute(cursor, query, parameters) + return cursor.rowcount + finally: + cursor.close() + + def executemany(self, query, parameters): + """Executes the given query against all the given param sequences. + + We return the lastrowid from the query. + """ + return self.executemany_lastrowid(query, parameters) + + def executemany_lastrowid(self, query, parameters): + """Executes the given query against all the given param sequences. + + We return the lastrowid from the query. + """ + cursor = self._cursor() + try: + cursor.executemany(query, parameters) + return cursor.lastrowid + finally: + cursor.close() + + def executemany_rowcount(self, query, parameters): + """Executes the given query against all the given param sequences. + + We return the rowcount from the query. + """ + cursor = self._cursor() + try: + cursor.executemany(query, parameters) + return cursor.rowcount + finally: + cursor.close() + + def _ensure_connected(self): + # Mysql by default closes client connections that are idle for + # 8 hours, but the client library does not report this fact until + # you try to perform a query and it fails. Protect against this + # case by preemptively closing and reopening the connection + # if it has been idle for too long (7 hours by default). + if (self._db is None or + (time.time() - self._last_use_time > self.max_idle_time)): + self.reconnect() + self._last_use_time = time.time() + + def _cursor(self): + self._ensure_connected() + return self._db.cursor() + + def _execute(self, cursor, query, parameters): + try: + return cursor.execute(query, parameters) + except OperationalError: + logging.error("Error connecting to MySQL on %s", self.host) + self.close() + raise + + +class Row(dict): + """A dict that allows for object-like property access syntax.""" + def __getattr__(self, name): + try: + return self[name] + except KeyError: + raise AttributeError(name) + + +# Fix the access conversions to properly recognize unicode/binary +FIELD_TYPE = MySQLdb.constants.FIELD_TYPE +FLAG = MySQLdb.constants.FLAG +CONVERSIONS = copy.copy(MySQLdb.converters.conversions) + +field_types = [FIELD_TYPE.BLOB, FIELD_TYPE.STRING, FIELD_TYPE.VAR_STRING] +if 'VARCHAR' in vars(FIELD_TYPE): + field_types.append(FIELD_TYPE.VARCHAR) + +for field_type in field_types: + CONVERSIONS[field_type] = [(FLAG.BINARY, str)] + CONVERSIONS[field_type] + + +# Alias some common MySQL exceptions +IntegrityError = MySQLdb.IntegrityError +OperationalError = MySQLdb.OperationalError diff --git a/libs/tornado/epoll.c b/libs/tornado/epoll.c new file mode 100644 index 00000000..9a2e3a37 --- /dev/null +++ b/libs/tornado/epoll.c @@ -0,0 +1,112 @@ +/* + * Copyright 2009 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#include "Python.h" +#include +#include + +#define MAX_EVENTS 24 + +/* + * Simple wrapper around epoll_create. + */ +static PyObject* _epoll_create(void) { + int fd = epoll_create(MAX_EVENTS); + if (fd == -1) { + PyErr_SetFromErrno(PyExc_Exception); + return NULL; + } + + return PyInt_FromLong(fd); +} + +/* + * Simple wrapper around epoll_ctl. We throw an exception if the call fails + * rather than returning the error code since it is an infrequent (and likely + * catastrophic) event when it does happen. + */ +static PyObject* _epoll_ctl(PyObject* self, PyObject* args) { + int epfd, op, fd, events; + struct epoll_event event; + + if (!PyArg_ParseTuple(args, "iiiI", &epfd, &op, &fd, &events)) { + return NULL; + } + + memset(&event, 0, sizeof(event)); + event.events = events; + event.data.fd = fd; + if (epoll_ctl(epfd, op, fd, &event) == -1) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + + Py_INCREF(Py_None); + return Py_None; +} + +/* + * Simple wrapper around epoll_wait. We return None if the call times out and + * throw an exception if an error occurs. Otherwise, we return a list of + * (fd, event) tuples. + */ +static PyObject* _epoll_wait(PyObject* self, PyObject* args) { + struct epoll_event events[MAX_EVENTS]; + int epfd, timeout, num_events, i; + PyObject* list; + PyObject* tuple; + + if (!PyArg_ParseTuple(args, "ii", &epfd, &timeout)) { + return NULL; + } + + Py_BEGIN_ALLOW_THREADS + num_events = epoll_wait(epfd, events, MAX_EVENTS, timeout); + Py_END_ALLOW_THREADS + if (num_events == -1) { + PyErr_SetFromErrno(PyExc_Exception); + return NULL; + } + + list = PyList_New(num_events); + for (i = 0; i < num_events; i++) { + tuple = PyTuple_New(2); + PyTuple_SET_ITEM(tuple, 0, PyInt_FromLong(events[i].data.fd)); + PyTuple_SET_ITEM(tuple, 1, PyInt_FromLong(events[i].events)); + PyList_SET_ITEM(list, i, tuple); + } + return list; +} + +/* + * Our method declararations + */ +static PyMethodDef kEpollMethods[] = { + {"epoll_create", (PyCFunction)_epoll_create, METH_NOARGS, + "Create an epoll file descriptor"}, + {"epoll_ctl", _epoll_ctl, METH_VARARGS, + "Control an epoll file descriptor"}, + {"epoll_wait", _epoll_wait, METH_VARARGS, + "Wait for events on an epoll file descriptor"}, + {NULL, NULL, 0, NULL} +}; + +/* + * Module initialization + */ +PyMODINIT_FUNC initepoll(void) { + Py_InitModule("epoll", kEpollMethods); +} diff --git a/libs/tornado/escape.py b/libs/tornado/escape.py new file mode 100644 index 00000000..4010b1c9 --- /dev/null +++ b/libs/tornado/escape.py @@ -0,0 +1,327 @@ +#!/usr/bin/env python +# +# Copyright 2009 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Escaping/unescaping methods for HTML, JSON, URLs, and others. + +Also includes a few other miscellaneous string manipulation functions that +have crept in over time. +""" + +import htmlentitydefs +import re +import sys +import urllib + +# Python3 compatibility: On python2.5, introduce the bytes alias from 2.6 +try: bytes +except Exception: bytes = str + +try: + from urlparse import parse_qs # Python 2.6+ +except ImportError: + from cgi import parse_qs + +# json module is in the standard library as of python 2.6; fall back to +# simplejson if present for older versions. +try: + import json + assert hasattr(json, "loads") and hasattr(json, "dumps") + _json_decode = json.loads + _json_encode = json.dumps +except Exception: + try: + import simplejson + _json_decode = lambda s: simplejson.loads(_unicode(s)) + _json_encode = lambda v: simplejson.dumps(v) + except ImportError: + try: + # For Google AppEngine + from django.utils import simplejson + _json_decode = lambda s: simplejson.loads(_unicode(s)) + _json_encode = lambda v: simplejson.dumps(v) + except ImportError: + def _json_decode(s): + raise NotImplementedError( + "A JSON parser is required, e.g., simplejson at " + "http://pypi.python.org/pypi/simplejson/") + _json_encode = _json_decode + + +_XHTML_ESCAPE_RE = re.compile('[&<>"]') +_XHTML_ESCAPE_DICT = {'&': '&', '<': '<', '>': '>', '"': '"'} +def xhtml_escape(value): + """Escapes a string so it is valid within XML or XHTML.""" + return _XHTML_ESCAPE_RE.sub(lambda match: _XHTML_ESCAPE_DICT[match.group(0)], + to_basestring(value)) + + +def xhtml_unescape(value): + """Un-escapes an XML-escaped string.""" + return re.sub(r"&(#?)(\w+?);", _convert_entity, _unicode(value)) + + +def json_encode(value): + """JSON-encodes the given Python object.""" + # JSON permits but does not require forward slashes to be escaped. + # This is useful when json data is emitted in a tags from prematurely terminating + # the javscript. Some json libraries do this escaping by default, + # although python's standard library does not, so we do it here. + # http://stackoverflow.com/questions/1580647/json-why-are-forward-slashes-escaped + return _json_encode(recursive_unicode(value)).replace("?@\[\]^`{|}~\s]))|(?:\((?:[^\s&()]|&|")*\)))+)""") + + +def linkify(text, shorten=False, extra_params="", + require_protocol=False, permitted_protocols=["http", "https"]): + """Converts plain text into HTML with links. + + For example: ``linkify("Hello http://tornadoweb.org!")`` would return + ``Hello http://tornadoweb.org!`` + + Parameters: + + shorten: Long urls will be shortened for display. + + extra_params: Extra text to include in the link tag, + e.g. linkify(text, extra_params='rel="nofollow" class="external"') + + require_protocol: Only linkify urls which include a protocol. If this is + False, urls such as www.facebook.com will also be linkified. + + permitted_protocols: List (or set) of protocols which should be linkified, + e.g. linkify(text, permitted_protocols=["http", "ftp", "mailto"]). + It is very unsafe to include protocols such as "javascript". + """ + if extra_params: + extra_params = " " + extra_params.strip() + + def make_link(m): + url = m.group(1) + proto = m.group(2) + if require_protocol and not proto: + return url # not protocol, no linkify + + if proto and proto not in permitted_protocols: + return url # bad protocol, no linkify + + href = m.group(1) + if not proto: + href = "http://" + href # no proto specified, use http + + params = extra_params + + # clip long urls. max_len is just an approximation + max_len = 30 + if shorten and len(url) > max_len: + before_clip = url + if proto: + proto_len = len(proto) + 1 + len(m.group(3) or "") # +1 for : + else: + proto_len = 0 + + parts = url[proto_len:].split("/") + if len(parts) > 1: + # Grab the whole host part plus the first bit of the path + # The path is usually not that interesting once shortened + # (no more slug, etc), so it really just provides a little + # extra indication of shortening. + url = url[:proto_len] + parts[0] + "/" + \ + parts[1][:8].split('?')[0].split('.')[0] + + if len(url) > max_len * 1.5: # still too long + url = url[:max_len] + + if url != before_clip: + amp = url.rfind('&') + # avoid splitting html char entities + if amp > max_len - 5: + url = url[:amp] + url += "..." + + if len(url) >= len(before_clip): + url = before_clip + else: + # full url is visible on mouse-over (for those who don't + # have a status bar, such as Safari by default) + params += ' title="%s"' % href + + return u'%s' % (href, params, url) + + # First HTML-escape so that our strings are all safe. + # The regex is modified to avoid character entites other than & so + # that we won't pick up ", etc. + text = _unicode(xhtml_escape(text)) + return _URL_RE.sub(make_link, text) + + +def _convert_entity(m): + if m.group(1) == "#": + try: + return unichr(int(m.group(2))) + except ValueError: + return "&#%s;" % m.group(2) + try: + return _HTML_UNICODE_MAP[m.group(2)] + except KeyError: + return "&%s;" % m.group(2) + + +def _build_unicode_map(): + unicode_map = {} + for name, value in htmlentitydefs.name2codepoint.iteritems(): + unicode_map[name] = unichr(value) + return unicode_map + +_HTML_UNICODE_MAP = _build_unicode_map() diff --git a/libs/tornado/gen.py b/libs/tornado/gen.py new file mode 100644 index 00000000..51be5376 --- /dev/null +++ b/libs/tornado/gen.py @@ -0,0 +1,382 @@ +"""``tornado.gen`` is a generator-based interface to make it easier to +work in an asynchronous environment. Code using the ``gen`` module +is technically asynchronous, but it is written as a single generator +instead of a collection of separate functions. + +For example, the following asynchronous handler:: + + class AsyncHandler(RequestHandler): + @asynchronous + def get(self): + http_client = AsyncHTTPClient() + http_client.fetch("http://example.com", + callback=self.on_fetch) + + def on_fetch(self, response): + do_something_with_response(response) + self.render("template.html") + +could be written with ``gen`` as:: + + class GenAsyncHandler(RequestHandler): + @asynchronous + @gen.engine + def get(self): + http_client = AsyncHTTPClient() + response = yield gen.Task(http_client.fetch, "http://example.com") + do_something_with_response(response) + self.render("template.html") + +`Task` works with any function that takes a ``callback`` keyword +argument. You can also yield a list of ``Tasks``, which will be +started at the same time and run in parallel; a list of results will +be returned when they are all finished:: + + def get(self): + http_client = AsyncHTTPClient() + response1, response2 = yield [gen.Task(http_client.fetch, url1), + gen.Task(http_client.fetch, url2)] + +For more complicated interfaces, `Task` can be split into two parts: +`Callback` and `Wait`:: + + class GenAsyncHandler2(RequestHandler): + @asynchronous + @gen.engine + def get(self): + http_client = AsyncHTTPClient() + http_client.fetch("http://example.com", + callback=(yield gen.Callback("key")) + response = yield gen.Wait("key") + do_something_with_response(response) + self.render("template.html") + +The ``key`` argument to `Callback` and `Wait` allows for multiple +asynchronous operations to be started at different times and proceed +in parallel: yield several callbacks with different keys, then wait +for them once all the async operations have started. + +The result of a `Wait` or `Task` yield expression depends on how the callback +was run. If it was called with no arguments, the result is ``None``. If +it was called with one argument, the result is that argument. If it was +called with more than one argument or any keyword arguments, the result +is an `Arguments` object, which is a named tuple ``(args, kwargs)``. +""" +from __future__ import with_statement + +import functools +import operator +import sys +import types + +from tornado.stack_context import ExceptionStackContext + +class KeyReuseError(Exception): pass +class UnknownKeyError(Exception): pass +class LeakedCallbackError(Exception): pass +class BadYieldError(Exception): pass + +def engine(func): + """Decorator for asynchronous generators. + + Any generator that yields objects from this module must be wrapped + in this decorator. The decorator only works on functions that are + already asynchronous. For `~tornado.web.RequestHandler` + ``get``/``post``/etc methods, this means that both the + `tornado.web.asynchronous` and `tornado.gen.engine` decorators + must be used (for proper exception handling, ``asynchronous`` + should come before ``gen.engine``). In most other cases, it means + that it doesn't make sense to use ``gen.engine`` on functions that + don't already take a callback argument. + """ + @functools.wraps(func) + def wrapper(*args, **kwargs): + runner = None + def handle_exception(typ, value, tb): + # if the function throws an exception before its first "yield" + # (or is not a generator at all), the Runner won't exist yet. + # However, in that case we haven't reached anything asynchronous + # yet, so we can just let the exception propagate. + if runner is not None: + return runner.handle_exception(typ, value, tb) + return False + with ExceptionStackContext(handle_exception): + gen = func(*args, **kwargs) + if isinstance(gen, types.GeneratorType): + runner = Runner(gen) + runner.run() + return + assert gen is None, gen + # no yield, so we're done + return wrapper + +class YieldPoint(object): + """Base class for objects that may be yielded from the generator.""" + def start(self, runner): + """Called by the runner after the generator has yielded. + + No other methods will be called on this object before ``start``. + """ + raise NotImplementedError() + + def is_ready(self): + """Called by the runner to determine whether to resume the generator. + + Returns a boolean; may be called more than once. + """ + raise NotImplementedError() + + def get_result(self): + """Returns the value to use as the result of the yield expression. + + This method will only be called once, and only after `is_ready` + has returned true. + """ + raise NotImplementedError() + +class Callback(YieldPoint): + """Returns a callable object that will allow a matching `Wait` to proceed. + + The key may be any value suitable for use as a dictionary key, and is + used to match ``Callbacks`` to their corresponding ``Waits``. The key + must be unique among outstanding callbacks within a single run of the + generator function, but may be reused across different runs of the same + function (so constants generally work fine). + + The callback may be called with zero or one arguments; if an argument + is given it will be returned by `Wait`. + """ + def __init__(self, key): + self.key = key + + def start(self, runner): + self.runner = runner + runner.register_callback(self.key) + + def is_ready(self): + return True + + def get_result(self): + return self.runner.result_callback(self.key) + +class Wait(YieldPoint): + """Returns the argument passed to the result of a previous `Callback`.""" + def __init__(self, key): + self.key = key + + def start(self, runner): + self.runner = runner + + def is_ready(self): + return self.runner.is_ready(self.key) + + def get_result(self): + return self.runner.pop_result(self.key) + +class WaitAll(YieldPoint): + """Returns the results of multiple previous `Callbacks`. + + The argument is a sequence of `Callback` keys, and the result is + a list of results in the same order. + + `WaitAll` is equivalent to yielding a list of `Wait` objects. + """ + def __init__(self, keys): + self.keys = keys + + def start(self, runner): + self.runner = runner + + def is_ready(self): + return all(self.runner.is_ready(key) for key in self.keys) + + def get_result(self): + return [self.runner.pop_result(key) for key in self.keys] + + +class Task(YieldPoint): + """Runs a single asynchronous operation. + + Takes a function (and optional additional arguments) and runs it with + those arguments plus a ``callback`` keyword argument. The argument passed + to the callback is returned as the result of the yield expression. + + A `Task` is equivalent to a `Callback`/`Wait` pair (with a unique + key generated automatically):: + + result = yield gen.Task(func, args) + + func(args, callback=(yield gen.Callback(key))) + result = yield gen.Wait(key) + """ + def __init__(self, func, *args, **kwargs): + assert "callback" not in kwargs + self.args = args + self.kwargs = kwargs + self.func = func + + def start(self, runner): + self.runner = runner + self.key = object() + runner.register_callback(self.key) + self.kwargs["callback"] = runner.result_callback(self.key) + self.func(*self.args, **self.kwargs) + + def is_ready(self): + return self.runner.is_ready(self.key) + + def get_result(self): + return self.runner.pop_result(self.key) + +class Multi(YieldPoint): + """Runs multiple asynchronous operations in parallel. + + Takes a list of ``Tasks`` or other ``YieldPoints`` and returns a list of + their responses. It is not necessary to call `Multi` explicitly, + since the engine will do so automatically when the generator yields + a list of ``YieldPoints``. + """ + def __init__(self, children): + assert all(isinstance(i, YieldPoint) for i in children) + self.children = children + + def start(self, runner): + for i in self.children: + i.start(runner) + + def is_ready(self): + return all(i.is_ready() for i in self.children) + + def get_result(self): + return [i.get_result() for i in self.children] + +class _NullYieldPoint(YieldPoint): + def start(self, runner): + pass + def is_ready(self): + return True + def get_result(self): + return None + +class Runner(object): + """Internal implementation of `tornado.gen.engine`. + + Maintains information about pending callbacks and their results. + """ + def __init__(self, gen): + self.gen = gen + self.yield_point = _NullYieldPoint() + self.pending_callbacks = set() + self.results = {} + self.running = False + self.finished = False + self.exc_info = None + self.had_exception = False + + def register_callback(self, key): + """Adds ``key`` to the list of callbacks.""" + if key in self.pending_callbacks: + raise KeyReuseError("key %r is already pending" % key) + self.pending_callbacks.add(key) + + def is_ready(self, key): + """Returns true if a result is available for ``key``.""" + if key not in self.pending_callbacks: + raise UnknownKeyError("key %r is not pending" % key) + return key in self.results + + def set_result(self, key, result): + """Sets the result for ``key`` and attempts to resume the generator.""" + self.results[key] = result + self.run() + + def pop_result(self, key): + """Returns the result for ``key`` and unregisters it.""" + self.pending_callbacks.remove(key) + return self.results.pop(key) + + def run(self): + """Starts or resumes the generator, running until it reaches a + yield point that is not ready. + """ + if self.running or self.finished: + return + try: + self.running = True + while True: + if self.exc_info is None: + try: + if not self.yield_point.is_ready(): + return + next = self.yield_point.get_result() + except Exception: + self.exc_info = sys.exc_info() + try: + if self.exc_info is not None: + self.had_exception = True + exc_info = self.exc_info + self.exc_info = None + yielded = self.gen.throw(*exc_info) + else: + yielded = self.gen.send(next) + except StopIteration: + self.finished = True + if self.pending_callbacks and not self.had_exception: + # If we ran cleanly without waiting on all callbacks + # raise an error (really more of a warning). If we + # had an exception then some callbacks may have been + # orphaned, so skip the check in that case. + raise LeakedCallbackError( + "finished without waiting for callbacks %r" % + self.pending_callbacks) + return + except Exception: + self.finished = True + raise + if isinstance(yielded, list): + yielded = Multi(yielded) + if isinstance(yielded, YieldPoint): + self.yield_point = yielded + try: + self.yield_point.start(self) + except Exception: + self.exc_info = sys.exc_info() + else: + self.exc_info = (BadYieldError("yielded unknown object %r" % yielded),) + finally: + self.running = False + + def result_callback(self, key): + def inner(*args, **kwargs): + if kwargs or len(args) > 1: + result = Arguments(args, kwargs) + elif args: + result = args[0] + else: + result = None + self.set_result(key, result) + return inner + + def handle_exception(self, typ, value, tb): + if not self.running and not self.finished: + self.exc_info = (typ, value, tb) + self.run() + return True + else: + return False + +# in python 2.6+ this could be a collections.namedtuple +class Arguments(tuple): + """The result of a yield expression whose callback had more than one + argument (or keyword arguments). + + The `Arguments` object can be used as a tuple ``(args, kwargs)`` + or an object with attributes ``args`` and ``kwargs``. + """ + __slots__ = () + + def __new__(cls, args, kwargs): + return tuple.__new__(cls, (args, kwargs)) + + args = property(operator.itemgetter(0)) + kwargs = property(operator.itemgetter(1)) diff --git a/libs/tornado/httpclient.py b/libs/tornado/httpclient.py new file mode 100644 index 00000000..354d907c --- /dev/null +++ b/libs/tornado/httpclient.py @@ -0,0 +1,417 @@ +"""Blocking and non-blocking HTTP client interfaces. + +This module defines a common interface shared by two implementations, +`simple_httpclient` and `curl_httpclient`. Applications may either +instantiate their chosen implementation class directly or use the +`AsyncHTTPClient` class from this module, which selects an implementation +that can be overridden with the `AsyncHTTPClient.configure` method. + +The default implementation is `simple_httpclient`, and this is expected +to be suitable for most users' needs. However, some applications may wish +to switch to `curl_httpclient` for reasons such as the following: + +* `curl_httpclient` has some features not found in `simple_httpclient`, + including support for HTTP proxies and the ability to use a specified + network interface. + +* `curl_httpclient` is more likely to be compatible with sites that are + not-quite-compliant with the HTTP spec, or sites that use little-exercised + features of HTTP. + +* `simple_httpclient` only supports SSL on Python 2.6 and above. + +* `curl_httpclient` is faster + +* `curl_httpclient` was the default prior to Tornado 2.0. + +Note that if you are using `curl_httpclient`, it is highly recommended that +you use a recent version of ``libcurl`` and ``pycurl``. Currently the minimum +supported version is 7.18.2, and the recommended version is 7.21.1 or newer. +""" + +import calendar +import email.utils +import httplib +import time +import weakref + +from tornado.escape import utf8 +from tornado import httputil +from tornado.ioloop import IOLoop +from tornado.util import import_object, bytes_type + +class HTTPClient(object): + """A blocking HTTP client. + + This interface is provided for convenience and testing; most applications + that are running an IOLoop will want to use `AsyncHTTPClient` instead. + Typical usage looks like this:: + + http_client = httpclient.HTTPClient() + try: + response = http_client.fetch("http://www.google.com/") + print response.body + except httpclient.HTTPError, e: + print "Error:", e + """ + def __init__(self, async_client_class=None): + self._io_loop = IOLoop() + if async_client_class is None: + async_client_class = AsyncHTTPClient + self._async_client = async_client_class(self._io_loop) + self._response = None + self._closed = False + + def __del__(self): + self.close() + + def close(self): + """Closes the HTTPClient, freeing any resources used.""" + if not self._closed: + self._async_client.close() + self._io_loop.close() + self._closed = True + + def fetch(self, request, **kwargs): + """Executes a request, returning an `HTTPResponse`. + + The request may be either a string URL or an `HTTPRequest` object. + If it is a string, we construct an `HTTPRequest` using any additional + kwargs: ``HTTPRequest(request, **kwargs)`` + + If an error occurs during the fetch, we raise an `HTTPError`. + """ + def callback(response): + self._response = response + self._io_loop.stop() + self._async_client.fetch(request, callback, **kwargs) + self._io_loop.start() + response = self._response + self._response = None + response.rethrow() + return response + +class AsyncHTTPClient(object): + """An non-blocking HTTP client. + + Example usage:: + + import ioloop + + def handle_request(response): + if response.error: + print "Error:", response.error + else: + print response.body + ioloop.IOLoop.instance().stop() + + http_client = httpclient.AsyncHTTPClient() + http_client.fetch("http://www.google.com/", handle_request) + ioloop.IOLoop.instance().start() + + The constructor for this class is magic in several respects: It actually + creates an instance of an implementation-specific subclass, and instances + are reused as a kind of pseudo-singleton (one per IOLoop). The keyword + argument force_instance=True can be used to suppress this singleton + behavior. Constructor arguments other than io_loop and force_instance + are deprecated. The implementation subclass as well as arguments to + its constructor can be set with the static method configure() + """ + _impl_class = None + _impl_kwargs = None + + @classmethod + def _async_clients(cls): + assert cls is not AsyncHTTPClient, "should only be called on subclasses" + if not hasattr(cls, '_async_client_dict'): + cls._async_client_dict = weakref.WeakKeyDictionary() + return cls._async_client_dict + + def __new__(cls, io_loop=None, max_clients=10, force_instance=False, + **kwargs): + io_loop = io_loop or IOLoop.instance() + if cls is AsyncHTTPClient: + if cls._impl_class is None: + from tornado.simple_httpclient import SimpleAsyncHTTPClient + AsyncHTTPClient._impl_class = SimpleAsyncHTTPClient + impl = AsyncHTTPClient._impl_class + else: + impl = cls + if io_loop in impl._async_clients() and not force_instance: + return impl._async_clients()[io_loop] + else: + instance = super(AsyncHTTPClient, cls).__new__(impl) + args = {} + if cls._impl_kwargs: + args.update(cls._impl_kwargs) + args.update(kwargs) + instance.initialize(io_loop, max_clients, **args) + if not force_instance: + impl._async_clients()[io_loop] = instance + return instance + + def close(self): + """Destroys this http client, freeing any file descriptors used. + Not needed in normal use, but may be helpful in unittests that + create and destroy http clients. No other methods may be called + on the AsyncHTTPClient after close(). + """ + if self._async_clients().get(self.io_loop) is self: + del self._async_clients()[self.io_loop] + + def fetch(self, request, callback, **kwargs): + """Executes a request, calling callback with an `HTTPResponse`. + + The request may be either a string URL or an `HTTPRequest` object. + If it is a string, we construct an `HTTPRequest` using any additional + kwargs: ``HTTPRequest(request, **kwargs)`` + + If an error occurs during the fetch, the HTTPResponse given to the + callback has a non-None error attribute that contains the exception + encountered during the request. You can call response.rethrow() to + throw the exception (if any) in the callback. + """ + raise NotImplementedError() + + @staticmethod + def configure(impl, **kwargs): + """Configures the AsyncHTTPClient subclass to use. + + AsyncHTTPClient() actually creates an instance of a subclass. + This method may be called with either a class object or the + fully-qualified name of such a class (or None to use the default, + SimpleAsyncHTTPClient) + + If additional keyword arguments are given, they will be passed + to the constructor of each subclass instance created. The + keyword argument max_clients determines the maximum number of + simultaneous fetch() operations that can execute in parallel + on each IOLoop. Additional arguments may be supported depending + on the implementation class in use. + + Example:: + + AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient") + """ + if isinstance(impl, (unicode, bytes_type)): + impl = import_object(impl) + if impl is not None and not issubclass(impl, AsyncHTTPClient): + raise ValueError("Invalid AsyncHTTPClient implementation") + AsyncHTTPClient._impl_class = impl + AsyncHTTPClient._impl_kwargs = kwargs + +class HTTPRequest(object): + """HTTP client request object.""" + def __init__(self, url, method="GET", headers=None, body=None, + auth_username=None, auth_password=None, + connect_timeout=20.0, request_timeout=20.0, + if_modified_since=None, follow_redirects=True, + max_redirects=5, user_agent=None, use_gzip=True, + network_interface=None, streaming_callback=None, + header_callback=None, prepare_curl_callback=None, + proxy_host=None, proxy_port=None, proxy_username=None, + proxy_password='', allow_nonstandard_methods=False, + validate_cert=True, ca_certs=None, + allow_ipv6=None, + client_key=None, client_cert=None): + """Creates an `HTTPRequest`. + + All parameters except `url` are optional. + + :arg string url: URL to fetch + :arg string method: HTTP method, e.g. "GET" or "POST" + :arg headers: Additional HTTP headers to pass on the request + :type headers: `~tornado.httputil.HTTPHeaders` or `dict` + :arg string auth_username: Username for HTTP "Basic" authentication + :arg string auth_password: Password for HTTP "Basic" authentication + :arg float connect_timeout: Timeout for initial connection in seconds + :arg float request_timeout: Timeout for entire request in seconds + :arg datetime if_modified_since: Timestamp for ``If-Modified-Since`` + header + :arg bool follow_redirects: Should redirects be followed automatically + or return the 3xx response? + :arg int max_redirects: Limit for `follow_redirects` + :arg string user_agent: String to send as ``User-Agent`` header + :arg bool use_gzip: Request gzip encoding from the server + :arg string network_interface: Network interface to use for request + :arg callable streaming_callback: If set, `streaming_callback` will + be run with each chunk of data as it is received, and + `~HTTPResponse.body` and `~HTTPResponse.buffer` will be empty in + the final response. + :arg callable header_callback: If set, `header_callback` will + be run with each header line as it is received, and + `~HTTPResponse.headers` will be empty in the final response. + :arg callable prepare_curl_callback: If set, will be called with + a `pycurl.Curl` object to allow the application to make additional + `setopt` calls. + :arg string proxy_host: HTTP proxy hostname. To use proxies, + `proxy_host` and `proxy_port` must be set; `proxy_username` and + `proxy_pass` are optional. Proxies are currently only support + with `curl_httpclient`. + :arg int proxy_port: HTTP proxy port + :arg string proxy_username: HTTP proxy username + :arg string proxy_password: HTTP proxy password + :arg bool allow_nonstandard_methods: Allow unknown values for `method` + argument? + :arg bool validate_cert: For HTTPS requests, validate the server's + certificate? + :arg string ca_certs: filename of CA certificates in PEM format, + or None to use defaults. Note that in `curl_httpclient`, if + any request uses a custom `ca_certs` file, they all must (they + don't have to all use the same `ca_certs`, but it's not possible + to mix requests with ca_certs and requests that use the defaults. + :arg bool allow_ipv6: Use IPv6 when available? Default is false in + `simple_httpclient` and true in `curl_httpclient` + :arg string client_key: Filename for client SSL key, if any + :arg string client_cert: Filename for client SSL certificate, if any + """ + if headers is None: + headers = httputil.HTTPHeaders() + if if_modified_since: + timestamp = calendar.timegm(if_modified_since.utctimetuple()) + headers["If-Modified-Since"] = email.utils.formatdate( + timestamp, localtime=False, usegmt=True) + self.proxy_host = proxy_host + self.proxy_port = proxy_port + self.proxy_username = proxy_username + self.proxy_password = proxy_password + self.url = url + self.method = method + self.headers = headers + self.body = utf8(body) + self.auth_username = auth_username + self.auth_password = auth_password + self.connect_timeout = connect_timeout + self.request_timeout = request_timeout + self.follow_redirects = follow_redirects + self.max_redirects = max_redirects + self.user_agent = user_agent + self.use_gzip = use_gzip + self.network_interface = network_interface + self.streaming_callback = streaming_callback + self.header_callback = header_callback + self.prepare_curl_callback = prepare_curl_callback + self.allow_nonstandard_methods = allow_nonstandard_methods + self.validate_cert = validate_cert + self.ca_certs = ca_certs + self.allow_ipv6 = allow_ipv6 + self.client_key = client_key + self.client_cert = client_cert + self.start_time = time.time() + + +class HTTPResponse(object): + """HTTP Response object. + + Attributes: + + * request: HTTPRequest object + + * code: numeric HTTP status code, e.g. 200 or 404 + + * headers: httputil.HTTPHeaders object + + * buffer: cStringIO object for response body + + * body: respose body as string (created on demand from self.buffer) + + * error: Exception object, if any + + * request_time: seconds from request start to finish + + * time_info: dictionary of diagnostic timing information from the request. + Available data are subject to change, but currently uses timings + available from http://curl.haxx.se/libcurl/c/curl_easy_getinfo.html, + plus 'queue', which is the delay (if any) introduced by waiting for + a slot under AsyncHTTPClient's max_clients setting. + """ + def __init__(self, request, code, headers={}, buffer=None, + effective_url=None, error=None, request_time=None, + time_info={}): + self.request = request + self.code = code + self.headers = headers + self.buffer = buffer + self._body = None + if effective_url is None: + self.effective_url = request.url + else: + self.effective_url = effective_url + if error is None: + if self.code < 200 or self.code >= 300: + self.error = HTTPError(self.code, response=self) + else: + self.error = None + else: + self.error = error + self.request_time = request_time + self.time_info = time_info + + def _get_body(self): + if self.buffer is None: + return None + elif self._body is None: + self._body = self.buffer.getvalue() + + return self._body + + body = property(_get_body) + + def rethrow(self): + """If there was an error on the request, raise an `HTTPError`.""" + if self.error: + raise self.error + + def __repr__(self): + args = ",".join("%s=%r" % i for i in self.__dict__.iteritems()) + return "%s(%s)" % (self.__class__.__name__, args) + + +class HTTPError(Exception): + """Exception thrown for an unsuccessful HTTP request. + + Attributes: + + code - HTTP error integer error code, e.g. 404. Error code 599 is + used when no HTTP response was received, e.g. for a timeout. + + response - HTTPResponse object, if any. + + Note that if follow_redirects is False, redirects become HTTPErrors, + and you can look at error.response.headers['Location'] to see the + destination of the redirect. + """ + def __init__(self, code, message=None, response=None): + self.code = code + message = message or httplib.responses.get(code, "Unknown") + self.response = response + Exception.__init__(self, "HTTP %d: %s" % (self.code, message)) + + +def main(): + from tornado.options import define, options, parse_command_line + define("print_headers", type=bool, default=False) + define("print_body", type=bool, default=True) + define("follow_redirects", type=bool, default=True) + define("validate_cert", type=bool, default=True) + args = parse_command_line() + client = HTTPClient() + for arg in args: + try: + response = client.fetch(arg, + follow_redirects=options.follow_redirects, + validate_cert=options.validate_cert, + ) + except HTTPError, e: + if e.response is not None: + response = e.response + else: + raise + if options.print_headers: + print response.headers + if options.print_body: + print response.body + client.close() + +if __name__ == "__main__": + main() diff --git a/libs/tornado/httpserver.py b/libs/tornado/httpserver.py new file mode 100644 index 00000000..e24c3768 --- /dev/null +++ b/libs/tornado/httpserver.py @@ -0,0 +1,476 @@ +#!/usr/bin/env python +# +# Copyright 2009 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""A non-blocking, single-threaded HTTP server. + +Typical applications have little direct interaction with the `HTTPServer` +class except to start a server at the beginning of the process +(and even that is often done indirectly via `tornado.web.Application.listen`). + +This module also defines the `HTTPRequest` class which is exposed via +`tornado.web.RequestHandler.request`. +""" + +import Cookie +import logging +import socket +import time +import urlparse + +from tornado.escape import utf8, native_str, parse_qs_bytes +from tornado import httputil +from tornado import iostream +from tornado.netutil import TCPServer +from tornado import stack_context +from tornado.util import b, bytes_type + +try: + import ssl # Python 2.6+ +except ImportError: + ssl = None + +class HTTPServer(TCPServer): + r"""A non-blocking, single-threaded HTTP server. + + A server is defined by a request callback that takes an HTTPRequest + instance as an argument and writes a valid HTTP response with + `HTTPRequest.write`. `HTTPRequest.finish` finishes the request (but does + not necessarily close the connection in the case of HTTP/1.1 keep-alive + requests). A simple example server that echoes back the URI you + requested:: + + import httpserver + import ioloop + + def handle_request(request): + message = "You requested %s\n" % request.uri + request.write("HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\n%s" % ( + len(message), message)) + request.finish() + + http_server = httpserver.HTTPServer(handle_request) + http_server.listen(8888) + ioloop.IOLoop.instance().start() + + `HTTPServer` is a very basic connection handler. Beyond parsing the + HTTP request body and headers, the only HTTP semantics implemented + in `HTTPServer` is HTTP/1.1 keep-alive connections. We do not, however, + implement chunked encoding, so the request callback must provide a + ``Content-Length`` header or implement chunked encoding for HTTP/1.1 + requests for the server to run correctly for HTTP/1.1 clients. If + the request handler is unable to do this, you can provide the + ``no_keep_alive`` argument to the `HTTPServer` constructor, which will + ensure the connection is closed on every request no matter what HTTP + version the client is using. + + If ``xheaders`` is ``True``, we support the ``X-Real-Ip`` and ``X-Scheme`` + headers, which override the remote IP and HTTP scheme for all requests. + These headers are useful when running Tornado behind a reverse proxy or + load balancer. + + `HTTPServer` can serve SSL traffic with Python 2.6+ and OpenSSL. + To make this server serve SSL traffic, send the ssl_options dictionary + argument with the arguments required for the `ssl.wrap_socket` method, + including "certfile" and "keyfile":: + + HTTPServer(applicaton, ssl_options={ + "certfile": os.path.join(data_dir, "mydomain.crt"), + "keyfile": os.path.join(data_dir, "mydomain.key"), + }) + + `HTTPServer` initialization follows one of three patterns (the + initialization methods are defined on `tornado.netutil.TCPServer`): + + 1. `~tornado.netutil.TCPServer.listen`: simple single-process:: + + server = HTTPServer(app) + server.listen(8888) + IOLoop.instance().start() + + In many cases, `tornado.web.Application.listen` can be used to avoid + the need to explicitly create the `HTTPServer`. + + 2. `~tornado.netutil.TCPServer.bind`/`~tornado.netutil.TCPServer.start`: + simple multi-process:: + + server = HTTPServer(app) + server.bind(8888) + server.start(0) # Forks multiple sub-processes + IOLoop.instance().start() + + When using this interface, an `IOLoop` must *not* be passed + to the `HTTPServer` constructor. `start` will always start + the server on the default singleton `IOLoop`. + + 3. `~tornado.netutil.TCPServer.add_sockets`: advanced multi-process:: + + sockets = tornado.netutil.bind_sockets(8888) + tornado.process.fork_processes(0) + server = HTTPServer(app) + server.add_sockets(sockets) + IOLoop.instance().start() + + The `add_sockets` interface is more complicated, but it can be + used with `tornado.process.fork_processes` to give you more + flexibility in when the fork happens. `add_sockets` can + also be used in single-process servers if you want to create + your listening sockets in some way other than + `tornado.netutil.bind_sockets`. + + """ + def __init__(self, request_callback, no_keep_alive=False, io_loop=None, + xheaders=False, ssl_options=None, **kwargs): + self.request_callback = request_callback + self.no_keep_alive = no_keep_alive + self.xheaders = xheaders + TCPServer.__init__(self, io_loop=io_loop, ssl_options=ssl_options, + **kwargs) + + def handle_stream(self, stream, address): + HTTPConnection(stream, address, self.request_callback, + self.no_keep_alive, self.xheaders) + +class _BadRequestException(Exception): + """Exception class for malformed HTTP requests.""" + pass + +class HTTPConnection(object): + """Handles a connection to an HTTP client, executing HTTP requests. + + We parse HTTP headers and bodies, and execute the request callback + until the HTTP conection is closed. + """ + def __init__(self, stream, address, request_callback, no_keep_alive=False, + xheaders=False): + self.stream = stream + if self.stream.socket.family not in (socket.AF_INET, socket.AF_INET6): + # Unix (or other) socket; fake the remote address + address = ('0.0.0.0', 0) + self.address = address + self.request_callback = request_callback + self.no_keep_alive = no_keep_alive + self.xheaders = xheaders + self._request = None + self._request_finished = False + # Save stack context here, outside of any request. This keeps + # contexts from one request from leaking into the next. + self._header_callback = stack_context.wrap(self._on_headers) + self.stream.read_until(b("\r\n\r\n"), self._header_callback) + self._write_callback = None + + def write(self, chunk, callback=None): + """Writes a chunk of output to the stream.""" + assert self._request, "Request closed" + if not self.stream.closed(): + self._write_callback = stack_context.wrap(callback) + self.stream.write(chunk, self._on_write_complete) + + def finish(self): + """Finishes the request.""" + assert self._request, "Request closed" + self._request_finished = True + if not self.stream.writing(): + self._finish_request() + + def _on_write_complete(self): + if self._write_callback is not None: + callback = self._write_callback + self._write_callback = None + callback() + # _on_write_complete is enqueued on the IOLoop whenever the + # IOStream's write buffer becomes empty, but it's possible for + # another callback that runs on the IOLoop before it to + # simultaneously write more data and finish the request. If + # there is still data in the IOStream, a future + # _on_write_complete will be responsible for calling + # _finish_request. + if self._request_finished and not self.stream.writing(): + self._finish_request() + + def _finish_request(self): + if self.no_keep_alive: + disconnect = True + else: + connection_header = self._request.headers.get("Connection") + if connection_header is not None: + connection_header = connection_header.lower() + if self._request.supports_http_1_1(): + disconnect = connection_header == "close" + elif ("Content-Length" in self._request.headers + or self._request.method in ("HEAD", "GET")): + disconnect = connection_header != "keep-alive" + else: + disconnect = True + self._request = None + self._request_finished = False + if disconnect: + self.stream.close() + return + self.stream.read_until(b("\r\n\r\n"), self._header_callback) + + def _on_headers(self, data): + try: + data = native_str(data.decode('latin1')) + eol = data.find("\r\n") + start_line = data[:eol] + try: + method, uri, version = start_line.split(" ") + except ValueError: + raise _BadRequestException("Malformed HTTP request line") + if not version.startswith("HTTP/"): + raise _BadRequestException("Malformed HTTP version in HTTP Request-Line") + headers = httputil.HTTPHeaders.parse(data[eol:]) + self._request = HTTPRequest( + connection=self, method=method, uri=uri, version=version, + headers=headers, remote_ip=self.address[0]) + + content_length = headers.get("Content-Length") + if content_length: + content_length = int(content_length) + if content_length > self.stream.max_buffer_size: + raise _BadRequestException("Content-Length too long") + if headers.get("Expect") == "100-continue": + self.stream.write(b("HTTP/1.1 100 (Continue)\r\n\r\n")) + self.stream.read_bytes(content_length, self._on_request_body) + return + + self.request_callback(self._request) + except _BadRequestException, e: + logging.info("Malformed HTTP request from %s: %s", + self.address[0], e) + self.stream.close() + return + + def _on_request_body(self, data): + self._request.body = data + content_type = self._request.headers.get("Content-Type", "") + if self._request.method in ("POST", "PUT"): + if content_type.startswith("application/x-www-form-urlencoded"): + arguments = parse_qs_bytes(native_str(self._request.body)) + for name, values in arguments.iteritems(): + values = [v for v in values if v] + if values: + self._request.arguments.setdefault(name, []).extend( + values) + elif content_type.startswith("multipart/form-data"): + fields = content_type.split(";") + for field in fields: + k, sep, v = field.strip().partition("=") + if k == "boundary" and v: + httputil.parse_multipart_form_data( + utf8(v), data, + self._request.arguments, + self._request.files) + break + else: + logging.warning("Invalid multipart/form-data") + self.request_callback(self._request) + + +class HTTPRequest(object): + """A single HTTP request. + + All attributes are type `str` unless otherwise noted. + + .. attribute:: method + + HTTP request method, e.g. "GET" or "POST" + + .. attribute:: uri + + The requested uri. + + .. attribute:: path + + The path portion of `uri` + + .. attribute:: query + + The query portion of `uri` + + .. attribute:: version + + HTTP version specified in request, e.g. "HTTP/1.1" + + .. attribute:: headers + + `HTTPHeader` dictionary-like object for request headers. Acts like + a case-insensitive dictionary with additional methods for repeated + headers. + + .. attribute:: body + + Request body, if present, as a byte string. + + .. attribute:: remote_ip + + Client's IP address as a string. If `HTTPServer.xheaders` is set, + will pass along the real IP address provided by a load balancer + in the ``X-Real-Ip`` header + + .. attribute:: protocol + + The protocol used, either "http" or "https". If `HTTPServer.xheaders` + is set, will pass along the protocol used by a load balancer if + reported via an ``X-Scheme`` header. + + .. attribute:: host + + The requested hostname, usually taken from the ``Host`` header. + + .. attribute:: arguments + + GET/POST arguments are available in the arguments property, which + maps arguments names to lists of values (to support multiple values + for individual names). Names are of type `str`, while arguments + are byte strings. Note that this is different from + `RequestHandler.get_argument`, which returns argument values as + unicode strings. + + .. attribute:: files + + File uploads are available in the files property, which maps file + names to lists of :class:`HTTPFile`. + + .. attribute:: connection + + An HTTP request is attached to a single HTTP connection, which can + be accessed through the "connection" attribute. Since connections + are typically kept open in HTTP/1.1, multiple requests can be handled + sequentially on a single connection. + """ + def __init__(self, method, uri, version="HTTP/1.0", headers=None, + body=None, remote_ip=None, protocol=None, host=None, + files=None, connection=None): + self.method = method + self.uri = uri + self.version = version + self.headers = headers or httputil.HTTPHeaders() + self.body = body or "" + if connection and connection.xheaders: + # Squid uses X-Forwarded-For, others use X-Real-Ip + self.remote_ip = self.headers.get( + "X-Real-Ip", self.headers.get("X-Forwarded-For", remote_ip)) + if not self._valid_ip(self.remote_ip): + self.remote_ip = remote_ip + # AWS uses X-Forwarded-Proto + self.protocol = self.headers.get( + "X-Scheme", self.headers.get("X-Forwarded-Proto", protocol)) + if self.protocol not in ("http", "https"): + self.protocol = "http" + else: + self.remote_ip = remote_ip + if protocol: + self.protocol = protocol + elif connection and isinstance(connection.stream, + iostream.SSLIOStream): + self.protocol = "https" + else: + self.protocol = "http" + self.host = host or self.headers.get("Host") or "127.0.0.1" + self.files = files or {} + self.connection = connection + self._start_time = time.time() + self._finish_time = None + + scheme, netloc, path, query, fragment = urlparse.urlsplit(native_str(uri)) + self.path = path + self.query = query + arguments = parse_qs_bytes(query) + self.arguments = {} + for name, values in arguments.iteritems(): + values = [v for v in values if v] + if values: self.arguments[name] = values + + def supports_http_1_1(self): + """Returns True if this request supports HTTP/1.1 semantics""" + return self.version == "HTTP/1.1" + + @property + def cookies(self): + """A dictionary of Cookie.Morsel objects.""" + if not hasattr(self, "_cookies"): + self._cookies = Cookie.SimpleCookie() + if "Cookie" in self.headers: + try: + self._cookies.load( + native_str(self.headers["Cookie"])) + except Exception: + self._cookies = {} + return self._cookies + + def write(self, chunk, callback=None): + """Writes the given chunk to the response stream.""" + assert isinstance(chunk, bytes_type) + self.connection.write(chunk, callback=callback) + + def finish(self): + """Finishes this HTTP request on the open connection.""" + self.connection.finish() + self._finish_time = time.time() + + def full_url(self): + """Reconstructs the full URL for this request.""" + return self.protocol + "://" + self.host + self.uri + + def request_time(self): + """Returns the amount of time it took for this request to execute.""" + if self._finish_time is None: + return time.time() - self._start_time + else: + return self._finish_time - self._start_time + + def get_ssl_certificate(self): + """Returns the client's SSL certificate, if any. + + To use client certificates, the HTTPServer must have been constructed + with cert_reqs set in ssl_options, e.g.:: + + server = HTTPServer(app, + ssl_options=dict( + certfile="foo.crt", + keyfile="foo.key", + cert_reqs=ssl.CERT_REQUIRED, + ca_certs="cacert.crt")) + + The return value is a dictionary, see SSLSocket.getpeercert() in + the standard library for more details. + http://docs.python.org/library/ssl.html#sslsocket-objects + """ + try: + return self.connection.stream.socket.getpeercert() + except ssl.SSLError: + return None + + def __repr__(self): + attrs = ("protocol", "host", "method", "uri", "version", "remote_ip", + "body") + args = ", ".join(["%s=%r" % (n, getattr(self, n)) for n in attrs]) + return "%s(%s, headers=%s)" % ( + self.__class__.__name__, args, dict(self.headers)) + + def _valid_ip(self, ip): + try: + res = socket.getaddrinfo(ip, 0, socket.AF_UNSPEC, + socket.SOCK_STREAM, + 0, socket.AI_NUMERICHOST) + return bool(res) + except socket.gaierror, e: + if e.args[0] == socket.EAI_NONAME: + return False + raise + return True + diff --git a/libs/tornado/httputil.py b/libs/tornado/httputil.py new file mode 100644 index 00000000..8aec4b46 --- /dev/null +++ b/libs/tornado/httputil.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python +# +# Copyright 2009 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""HTTP utility code shared by clients and servers.""" + +import logging +import urllib +import re + +from tornado.util import b, ObjectDict + +class HTTPHeaders(dict): + """A dictionary that maintains Http-Header-Case for all keys. + + Supports multiple values per key via a pair of new methods, + add() and get_list(). The regular dictionary interface returns a single + value per key, with multiple values joined by a comma. + + >>> h = HTTPHeaders({"content-type": "text/html"}) + >>> h.keys() + ['Content-Type'] + >>> h["Content-Type"] + 'text/html' + + >>> h.add("Set-Cookie", "A=B") + >>> h.add("Set-Cookie", "C=D") + >>> h["set-cookie"] + 'A=B,C=D' + >>> h.get_list("set-cookie") + ['A=B', 'C=D'] + + >>> for (k,v) in sorted(h.get_all()): + ... print '%s: %s' % (k,v) + ... + Content-Type: text/html + Set-Cookie: A=B + Set-Cookie: C=D + """ + def __init__(self, *args, **kwargs): + # Don't pass args or kwargs to dict.__init__, as it will bypass + # our __setitem__ + dict.__init__(self) + self._as_list = {} + self._last_key = None + self.update(*args, **kwargs) + + # new public methods + + def add(self, name, value): + """Adds a new value for the given key.""" + norm_name = HTTPHeaders._normalize_name(name) + self._last_key = norm_name + if norm_name in self: + # bypass our override of __setitem__ since it modifies _as_list + dict.__setitem__(self, norm_name, self[norm_name] + ',' + value) + self._as_list[norm_name].append(value) + else: + self[norm_name] = value + + def get_list(self, name): + """Returns all values for the given header as a list.""" + norm_name = HTTPHeaders._normalize_name(name) + return self._as_list.get(norm_name, []) + + def get_all(self): + """Returns an iterable of all (name, value) pairs. + + If a header has multiple values, multiple pairs will be + returned with the same name. + """ + for name, list in self._as_list.iteritems(): + for value in list: + yield (name, value) + + def parse_line(self, line): + """Updates the dictionary with a single header line. + + >>> h = HTTPHeaders() + >>> h.parse_line("Content-Type: text/html") + >>> h.get('content-type') + 'text/html' + """ + if line[0].isspace(): + # continuation of a multi-line header + new_part = ' ' + line.lstrip() + self._as_list[self._last_key][-1] += new_part + dict.__setitem__(self, self._last_key, + self[self._last_key] + new_part) + else: + name, value = line.split(":", 1) + self.add(name, value.strip()) + + @classmethod + def parse(cls, headers): + """Returns a dictionary from HTTP header text. + + >>> h = HTTPHeaders.parse("Content-Type: text/html\\r\\nContent-Length: 42\\r\\n") + >>> sorted(h.iteritems()) + [('Content-Length', '42'), ('Content-Type', 'text/html')] + """ + h = cls() + for line in headers.splitlines(): + if line: + h.parse_line(line) + return h + + # dict implementation overrides + + def __setitem__(self, name, value): + norm_name = HTTPHeaders._normalize_name(name) + dict.__setitem__(self, norm_name, value) + self._as_list[norm_name] = [value] + + def __getitem__(self, name): + return dict.__getitem__(self, HTTPHeaders._normalize_name(name)) + + def __delitem__(self, name): + norm_name = HTTPHeaders._normalize_name(name) + dict.__delitem__(self, norm_name) + del self._as_list[norm_name] + + def __contains__(self, name): + norm_name = HTTPHeaders._normalize_name(name) + return dict.__contains__(self, norm_name) + + def get(self, name, default=None): + return dict.get(self, HTTPHeaders._normalize_name(name), default) + + def update(self, *args, **kwargs): + # dict.update bypasses our __setitem__ + for k, v in dict(*args, **kwargs).iteritems(): + self[k] = v + + _NORMALIZED_HEADER_RE = re.compile(r'^[A-Z0-9][a-z0-9]*(-[A-Z0-9][a-z0-9]*)*$') + _normalized_headers = {} + + @staticmethod + def _normalize_name(name): + """Converts a name to Http-Header-Case. + + >>> HTTPHeaders._normalize_name("coNtent-TYPE") + 'Content-Type' + """ + try: + return HTTPHeaders._normalized_headers[name] + except KeyError: + if HTTPHeaders._NORMALIZED_HEADER_RE.match(name): + normalized = name + else: + normalized = "-".join([w.capitalize() for w in name.split("-")]) + HTTPHeaders._normalized_headers[name] = normalized + return normalized + + +def url_concat(url, args): + """Concatenate url and argument dictionary regardless of whether + url has existing query parameters. + + >>> url_concat("http://example.com/foo?a=b", dict(c="d")) + 'http://example.com/foo?a=b&c=d' + """ + if not args: return url + if url[-1] not in ('?', '&'): + url += '&' if ('?' in url) else '?' + return url + urllib.urlencode(args) + + +class HTTPFile(ObjectDict): + """Represents an HTTP file. For backwards compatibility, its instance + attributes are also accessible as dictionary keys. + + :ivar filename: + :ivar body: + :ivar content_type: The content_type comes from the provided HTTP header + and should not be trusted outright given that it can be easily forged. + """ + pass + + +def parse_multipart_form_data(boundary, data, arguments, files): + """Parses a multipart/form-data body. + + The boundary and data parameters are both byte strings. + The dictionaries given in the arguments and files parameters + will be updated with the contents of the body. + """ + # The standard allows for the boundary to be quoted in the header, + # although it's rare (it happens at least for google app engine + # xmpp). I think we're also supposed to handle backslash-escapes + # here but I'll save that until we see a client that uses them + # in the wild. + if boundary.startswith(b('"')) and boundary.endswith(b('"')): + boundary = boundary[1:-1] + if data.endswith(b("\r\n")): + footer_length = len(boundary) + 6 + else: + footer_length = len(boundary) + 4 + parts = data[:-footer_length].split(b("--") + boundary + b("\r\n")) + for part in parts: + if not part: continue + eoh = part.find(b("\r\n\r\n")) + if eoh == -1: + logging.warning("multipart/form-data missing headers") + continue + headers = HTTPHeaders.parse(part[:eoh].decode("utf-8")) + disp_header = headers.get("Content-Disposition", "") + disposition, disp_params = _parse_header(disp_header) + if disposition != "form-data" or not part.endswith(b("\r\n")): + logging.warning("Invalid multipart/form-data") + continue + value = part[eoh + 4:-2] + if not disp_params.get("name"): + logging.warning("multipart/form-data value missing name") + continue + name = disp_params["name"] + if disp_params.get("filename"): + ctype = headers.get("Content-Type", "application/unknown") + files.setdefault(name, []).append(HTTPFile( + filename=disp_params["filename"], body=value, + content_type=ctype)) + else: + arguments.setdefault(name, []).append(value) + + +# _parseparam and _parse_header are copied and modified from python2.7's cgi.py +# The original 2.7 version of this code did not correctly support some +# combinations of semicolons and double quotes. +def _parseparam(s): + while s[:1] == ';': + s = s[1:] + end = s.find(';') + while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2: + end = s.find(';', end + 1) + if end < 0: + end = len(s) + f = s[:end] + yield f.strip() + s = s[end:] + +def _parse_header(line): + """Parse a Content-type like header. + + Return the main content-type and a dictionary of options. + + """ + parts = _parseparam(';' + line) + key = parts.next() + pdict = {} + for p in parts: + i = p.find('=') + if i >= 0: + name = p[:i].strip().lower() + value = p[i+1:].strip() + if len(value) >= 2 and value[0] == value[-1] == '"': + value = value[1:-1] + value = value.replace('\\\\', '\\').replace('\\"', '"') + pdict[name] = value + return key, pdict + + +def doctests(): + import doctest + return doctest.DocTestSuite() + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/libs/tornado/ioloop.py b/libs/tornado/ioloop.py new file mode 100644 index 00000000..edd2fec2 --- /dev/null +++ b/libs/tornado/ioloop.py @@ -0,0 +1,643 @@ +#!/usr/bin/env python +# +# Copyright 2009 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""An I/O event loop for non-blocking sockets. + +Typical applications will use a single `IOLoop` object, in the +`IOLoop.instance` singleton. The `IOLoop.start` method should usually +be called at the end of the ``main()`` function. Atypical applications may +use more than one `IOLoop`, such as one `IOLoop` per thread, or per `unittest` +case. + +In addition to I/O events, the `IOLoop` can also schedule time-based events. +`IOLoop.add_timeout` is a non-blocking alternative to `time.sleep`. +""" + +from __future__ import with_statement + +import datetime +import errno +import heapq +import os +import logging +import select +import thread +import threading +import time +import traceback + +from tornado import stack_context + +try: + import signal +except ImportError: + signal = None + +from tornado.platform.auto import set_close_exec, Waker + + +class IOLoop(object): + """A level-triggered I/O loop. + + We use epoll (Linux) or kqueue (BSD and Mac OS X; requires python + 2.6+) if they are available, or else we fall back on select(). If + you are implementing a system that needs to handle thousands of + simultaneous connections, you should use a system that supports either + epoll or queue. + + Example usage for a simple TCP server:: + + import errno + import functools + import ioloop + import socket + + def connection_ready(sock, fd, events): + while True: + try: + connection, address = sock.accept() + except socket.error, e: + if e.args[0] not in (errno.EWOULDBLOCK, errno.EAGAIN): + raise + return + connection.setblocking(0) + handle_connection(connection, address) + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.setblocking(0) + sock.bind(("", port)) + sock.listen(128) + + io_loop = ioloop.IOLoop.instance() + callback = functools.partial(connection_ready, sock) + io_loop.add_handler(sock.fileno(), callback, io_loop.READ) + io_loop.start() + + """ + # Constants from the epoll module + _EPOLLIN = 0x001 + _EPOLLPRI = 0x002 + _EPOLLOUT = 0x004 + _EPOLLERR = 0x008 + _EPOLLHUP = 0x010 + _EPOLLRDHUP = 0x2000 + _EPOLLONESHOT = (1 << 30) + _EPOLLET = (1 << 31) + + # Our events map exactly to the epoll events + NONE = 0 + READ = _EPOLLIN + WRITE = _EPOLLOUT + ERROR = _EPOLLERR | _EPOLLHUP + + def __init__(self, impl=None): + self._impl = impl or _poll() + if hasattr(self._impl, 'fileno'): + set_close_exec(self._impl.fileno()) + self._handlers = {} + self._events = {} + self._callbacks = [] + self._callback_lock = threading.Lock() + self._timeouts = [] + self._running = False + self._stopped = False + self._thread_ident = None + self._blocking_signal_threshold = None + + # Create a pipe that we send bogus data to when we want to wake + # the I/O loop when it is idle + self._waker = Waker() + self.add_handler(self._waker.fileno(), + lambda fd, events: self._waker.consume(), + self.READ) + + @staticmethod + def instance(): + """Returns a global IOLoop instance. + + Most single-threaded applications have a single, global IOLoop. + Use this method instead of passing around IOLoop instances + throughout your code. + + A common pattern for classes that depend on IOLoops is to use + a default argument to enable programs with multiple IOLoops + but not require the argument for simpler applications:: + + class MyClass(object): + def __init__(self, io_loop=None): + self.io_loop = io_loop or IOLoop.instance() + """ + if not hasattr(IOLoop, "_instance"): + IOLoop._instance = IOLoop() + return IOLoop._instance + + @staticmethod + def initialized(): + """Returns true if the singleton instance has been created.""" + return hasattr(IOLoop, "_instance") + + def install(self): + """Installs this IOloop object as the singleton instance. + + This is normally not necessary as `instance()` will create + an IOLoop on demand, but you may want to call `install` to use + a custom subclass of IOLoop. + """ + assert not IOLoop.initialized() + IOLoop._instance = self + + def close(self, all_fds=False): + """Closes the IOLoop, freeing any resources used. + + If ``all_fds`` is true, all file descriptors registered on the + IOLoop will be closed (not just the ones created by the IOLoop itself. + """ + self.remove_handler(self._waker.fileno()) + if all_fds: + for fd in self._handlers.keys()[:]: + try: + os.close(fd) + except Exception: + logging.debug("error closing fd %s", fd, exc_info=True) + self._waker.close() + self._impl.close() + + def add_handler(self, fd, handler, events): + """Registers the given handler to receive the given events for fd.""" + self._handlers[fd] = stack_context.wrap(handler) + self._impl.register(fd, events | self.ERROR) + + def update_handler(self, fd, events): + """Changes the events we listen for fd.""" + self._impl.modify(fd, events | self.ERROR) + + def remove_handler(self, fd): + """Stop listening for events on fd.""" + self._handlers.pop(fd, None) + self._events.pop(fd, None) + try: + self._impl.unregister(fd) + except (OSError, IOError): + logging.debug("Error deleting fd from IOLoop", exc_info=True) + + def set_blocking_signal_threshold(self, seconds, action): + """Sends a signal if the ioloop is blocked for more than s seconds. + + Pass seconds=None to disable. Requires python 2.6 on a unixy + platform. + + The action parameter is a python signal handler. Read the + documentation for the python 'signal' module for more information. + If action is None, the process will be killed if it is blocked for + too long. + """ + if not hasattr(signal, "setitimer"): + logging.error("set_blocking_signal_threshold requires a signal module " + "with the setitimer method") + return + self._blocking_signal_threshold = seconds + if seconds is not None: + signal.signal(signal.SIGALRM, + action if action is not None else signal.SIG_DFL) + + def set_blocking_log_threshold(self, seconds): + """Logs a stack trace if the ioloop is blocked for more than s seconds. + Equivalent to set_blocking_signal_threshold(seconds, self.log_stack) + """ + self.set_blocking_signal_threshold(seconds, self.log_stack) + + def log_stack(self, signal, frame): + """Signal handler to log the stack trace of the current thread. + + For use with set_blocking_signal_threshold. + """ + logging.warning('IOLoop blocked for %f seconds in\n%s', + self._blocking_signal_threshold, + ''.join(traceback.format_stack(frame))) + + def start(self): + """Starts the I/O loop. + + The loop will run until one of the I/O handlers calls stop(), which + will make the loop stop after the current event iteration completes. + """ + if self._stopped: + self._stopped = False + return + self._thread_ident = thread.get_ident() + self._running = True + while True: + poll_timeout = 3600.0 + + # Prevent IO event starvation by delaying new callbacks + # to the next iteration of the event loop. + with self._callback_lock: + callbacks = self._callbacks + self._callbacks = [] + for callback in callbacks: + self._run_callback(callback) + + if self._timeouts: + now = time.time() + while self._timeouts: + if self._timeouts[0].callback is None: + # the timeout was cancelled + heapq.heappop(self._timeouts) + elif self._timeouts[0].deadline <= now: + timeout = heapq.heappop(self._timeouts) + self._run_callback(timeout.callback) + else: + seconds = self._timeouts[0].deadline - now + poll_timeout = min(seconds, poll_timeout) + break + + if self._callbacks: + # If any callbacks or timeouts called add_callback, + # we don't want to wait in poll() before we run them. + poll_timeout = 0.0 + + if not self._running: + break + + if self._blocking_signal_threshold is not None: + # clear alarm so it doesn't fire while poll is waiting for + # events. + signal.setitimer(signal.ITIMER_REAL, 0, 0) + + try: + event_pairs = self._impl.poll(poll_timeout) + except Exception, e: + # Depending on python version and IOLoop implementation, + # different exception types may be thrown and there are + # two ways EINTR might be signaled: + # * e.errno == errno.EINTR + # * e.args is like (errno.EINTR, 'Interrupted system call') + if (getattr(e, 'errno', None) == errno.EINTR or + (isinstance(getattr(e, 'args', None), tuple) and + len(e.args) == 2 and e.args[0] == errno.EINTR)): + continue + else: + raise + + if self._blocking_signal_threshold is not None: + signal.setitimer(signal.ITIMER_REAL, + self._blocking_signal_threshold, 0) + + # Pop one fd at a time from the set of pending fds and run + # its handler. Since that handler may perform actions on + # other file descriptors, there may be reentrant calls to + # this IOLoop that update self._events + self._events.update(event_pairs) + while self._events: + fd, events = self._events.popitem() + try: + self._handlers[fd](fd, events) + except (OSError, IOError), e: + if e.args[0] == errno.EPIPE: + # Happens when the client closes the connection + pass + else: + logging.error("Exception in I/O handler for fd %s", + fd, exc_info=True) + except Exception: + logging.error("Exception in I/O handler for fd %s", + fd, exc_info=True) + # reset the stopped flag so another start/stop pair can be issued + self._stopped = False + if self._blocking_signal_threshold is not None: + signal.setitimer(signal.ITIMER_REAL, 0, 0) + + def stop(self): + """Stop the loop after the current event loop iteration is complete. + If the event loop is not currently running, the next call to start() + will return immediately. + + To use asynchronous methods from otherwise-synchronous code (such as + unit tests), you can start and stop the event loop like this:: + + ioloop = IOLoop() + async_method(ioloop=ioloop, callback=ioloop.stop) + ioloop.start() + + ioloop.start() will return after async_method has run its callback, + whether that callback was invoked before or after ioloop.start. + """ + self._running = False + self._stopped = True + self._waker.wake() + + def running(self): + """Returns true if this IOLoop is currently running.""" + return self._running + + def add_timeout(self, deadline, callback): + """Calls the given callback at the time deadline from the I/O loop. + + Returns a handle that may be passed to remove_timeout to cancel. + + ``deadline`` may be a number denoting a unix timestamp (as returned + by ``time.time()`` or a ``datetime.timedelta`` object for a deadline + relative to the current time. + + Note that it is not safe to call `add_timeout` from other threads. + Instead, you must use `add_callback` to transfer control to the + IOLoop's thread, and then call `add_timeout` from there. + """ + timeout = _Timeout(deadline, stack_context.wrap(callback)) + heapq.heappush(self._timeouts, timeout) + return timeout + + def remove_timeout(self, timeout): + """Cancels a pending timeout. + + The argument is a handle as returned by add_timeout. + """ + # Removing from a heap is complicated, so just leave the defunct + # timeout object in the queue (see discussion in + # http://docs.python.org/library/heapq.html). + # If this turns out to be a problem, we could add a garbage + # collection pass whenever there are too many dead timeouts. + timeout.callback = None + + def add_callback(self, callback): + """Calls the given callback on the next I/O loop iteration. + + It is safe to call this method from any thread at any time. + Note that this is the *only* method in IOLoop that makes this + guarantee; all other interaction with the IOLoop must be done + from that IOLoop's thread. add_callback() may be used to transfer + control from other threads to the IOLoop's thread. + """ + with self._callback_lock: + list_empty = not self._callbacks + self._callbacks.append(stack_context.wrap(callback)) + if list_empty and thread.get_ident() != self._thread_ident: + # If we're in the IOLoop's thread, we know it's not currently + # polling. If we're not, and we added the first callback to an + # empty list, we may need to wake it up (it may wake up on its + # own, but an occasional extra wake is harmless). Waking + # up a polling IOLoop is relatively expensive, so we try to + # avoid it when we can. + self._waker.wake() + + def _run_callback(self, callback): + try: + callback() + except Exception: + self.handle_callback_exception(callback) + + def handle_callback_exception(self, callback): + """This method is called whenever a callback run by the IOLoop + throws an exception. + + By default simply logs the exception as an error. Subclasses + may override this method to customize reporting of exceptions. + + The exception itself is not passed explicitly, but is available + in sys.exc_info. + """ + logging.error("Exception in callback %r", callback, exc_info=True) + + +class _Timeout(object): + """An IOLoop timeout, a UNIX timestamp and a callback""" + + # Reduce memory overhead when there are lots of pending callbacks + __slots__ = ['deadline', 'callback'] + + def __init__(self, deadline, callback): + if isinstance(deadline, (int, long, float)): + self.deadline = deadline + elif isinstance(deadline, datetime.timedelta): + self.deadline = time.time() + _Timeout.timedelta_to_seconds(deadline) + else: + raise TypeError("Unsupported deadline %r" % deadline) + self.callback = callback + + @staticmethod + def timedelta_to_seconds(td): + """Equivalent to td.total_seconds() (introduced in python 2.7).""" + return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / float(10**6) + + # Comparison methods to sort by deadline, with object id as a tiebreaker + # to guarantee a consistent ordering. The heapq module uses __le__ + # in python2.5, and __lt__ in 2.6+ (sort() and most other comparisons + # use __lt__). + def __lt__(self, other): + return ((self.deadline, id(self)) < + (other.deadline, id(other))) + + def __le__(self, other): + return ((self.deadline, id(self)) <= + (other.deadline, id(other))) + + +class PeriodicCallback(object): + """Schedules the given callback to be called periodically. + + The callback is called every callback_time milliseconds. + + `start` must be called after the PeriodicCallback is created. + """ + def __init__(self, callback, callback_time, io_loop=None): + self.callback = callback + self.callback_time = callback_time + self.io_loop = io_loop or IOLoop.instance() + self._running = False + self._timeout = None + + def start(self): + """Starts the timer.""" + self._running = True + self._next_timeout = time.time() + self._schedule_next() + + def stop(self): + """Stops the timer.""" + self._running = False + if self._timeout is not None: + self.io_loop.remove_timeout(self._timeout) + self._timeout = None + + def _run(self): + if not self._running: return + try: + self.callback() + except Exception: + logging.error("Error in periodic callback", exc_info=True) + self._schedule_next() + + def _schedule_next(self): + if self._running: + current_time = time.time() + while self._next_timeout <= current_time: + self._next_timeout += self.callback_time / 1000.0 + self._timeout = self.io_loop.add_timeout(self._next_timeout, self._run) + + +class _EPoll(object): + """An epoll-based event loop using our C module for Python 2.5 systems""" + _EPOLL_CTL_ADD = 1 + _EPOLL_CTL_DEL = 2 + _EPOLL_CTL_MOD = 3 + + def __init__(self): + self._epoll_fd = epoll.epoll_create() + + def fileno(self): + return self._epoll_fd + + def close(self): + os.close(self._epoll_fd) + + def register(self, fd, events): + epoll.epoll_ctl(self._epoll_fd, self._EPOLL_CTL_ADD, fd, events) + + def modify(self, fd, events): + epoll.epoll_ctl(self._epoll_fd, self._EPOLL_CTL_MOD, fd, events) + + def unregister(self, fd): + epoll.epoll_ctl(self._epoll_fd, self._EPOLL_CTL_DEL, fd, 0) + + def poll(self, timeout): + return epoll.epoll_wait(self._epoll_fd, int(timeout * 1000)) + + +class _KQueue(object): + """A kqueue-based event loop for BSD/Mac systems.""" + def __init__(self): + self._kqueue = select.kqueue() + self._active = {} + + def fileno(self): + return self._kqueue.fileno() + + def close(self): + self._kqueue.close() + + def register(self, fd, events): + self._control(fd, events, select.KQ_EV_ADD) + self._active[fd] = events + + def modify(self, fd, events): + self.unregister(fd) + self.register(fd, events) + + def unregister(self, fd): + events = self._active.pop(fd) + self._control(fd, events, select.KQ_EV_DELETE) + + def _control(self, fd, events, flags): + kevents = [] + if events & IOLoop.WRITE: + kevents.append(select.kevent( + fd, filter=select.KQ_FILTER_WRITE, flags=flags)) + if events & IOLoop.READ or not kevents: + # Always read when there is not a write + kevents.append(select.kevent( + fd, filter=select.KQ_FILTER_READ, flags=flags)) + # Even though control() takes a list, it seems to return EINVAL + # on Mac OS X (10.6) when there is more than one event in the list. + for kevent in kevents: + self._kqueue.control([kevent], 0) + + def poll(self, timeout): + kevents = self._kqueue.control(None, 1000, timeout) + events = {} + for kevent in kevents: + fd = kevent.ident + if kevent.filter == select.KQ_FILTER_READ: + events[fd] = events.get(fd, 0) | IOLoop.READ + if kevent.filter == select.KQ_FILTER_WRITE: + if kevent.flags & select.KQ_EV_EOF: + # If an asynchronous connection is refused, kqueue + # returns a write event with the EOF flag set. + # Turn this into an error for consistency with the + # other IOLoop implementations. + # Note that for read events, EOF may be returned before + # all data has been consumed from the socket buffer, + # so we only check for EOF on write events. + events[fd] = IOLoop.ERROR + else: + events[fd] = events.get(fd, 0) | IOLoop.WRITE + if kevent.flags & select.KQ_EV_ERROR: + events[fd] = events.get(fd, 0) | IOLoop.ERROR + return events.items() + + +class _Select(object): + """A simple, select()-based IOLoop implementation for non-Linux systems""" + def __init__(self): + self.read_fds = set() + self.write_fds = set() + self.error_fds = set() + self.fd_sets = (self.read_fds, self.write_fds, self.error_fds) + + def close(self): + pass + + def register(self, fd, events): + if events & IOLoop.READ: self.read_fds.add(fd) + if events & IOLoop.WRITE: self.write_fds.add(fd) + if events & IOLoop.ERROR: + self.error_fds.add(fd) + # Closed connections are reported as errors by epoll and kqueue, + # but as zero-byte reads by select, so when errors are requested + # we need to listen for both read and error. + self.read_fds.add(fd) + + def modify(self, fd, events): + self.unregister(fd) + self.register(fd, events) + + def unregister(self, fd): + self.read_fds.discard(fd) + self.write_fds.discard(fd) + self.error_fds.discard(fd) + + def poll(self, timeout): + readable, writeable, errors = select.select( + self.read_fds, self.write_fds, self.error_fds, timeout) + events = {} + for fd in readable: + events[fd] = events.get(fd, 0) | IOLoop.READ + for fd in writeable: + events[fd] = events.get(fd, 0) | IOLoop.WRITE + for fd in errors: + events[fd] = events.get(fd, 0) | IOLoop.ERROR + return events.items() + + +# Choose a poll implementation. Use epoll if it is available, fall back to +# select() for non-Linux platforms +if hasattr(select, "epoll"): + # Python 2.6+ on Linux + _poll = select.epoll +elif hasattr(select, "kqueue"): + # Python 2.6+ on BSD or Mac + _poll = _KQueue +else: + try: + # Linux systems with our C module installed + import epoll + _poll = _EPoll + except Exception: + # All other systems + import sys + if "linux" in sys.platform: + logging.warning("epoll module not found; using select()") + _poll = _Select diff --git a/libs/tornado/iostream.py b/libs/tornado/iostream.py new file mode 100644 index 00000000..db7895f0 --- /dev/null +++ b/libs/tornado/iostream.py @@ -0,0 +1,728 @@ +#!/usr/bin/env python +# +# Copyright 2009 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""A utility class to write to and read from a non-blocking socket.""" + +from __future__ import with_statement + +import collections +import errno +import logging +import socket +import sys +import re + +from tornado import ioloop +from tornado import stack_context +from tornado.util import b, bytes_type + +try: + import ssl # Python 2.6+ +except ImportError: + ssl = None + +class IOStream(object): + r"""A utility class to write to and read from a non-blocking socket. + + We support a non-blocking ``write()`` and a family of ``read_*()`` methods. + All of the methods take callbacks (since writing and reading are + non-blocking and asynchronous). + + The socket parameter may either be connected or unconnected. For + server operations the socket is the result of calling socket.accept(). + For client operations the socket is created with socket.socket(), + and may either be connected before passing it to the IOStream or + connected with IOStream.connect. + + A very simple (and broken) HTTP client using this class:: + + from tornado import ioloop + from tornado import iostream + import socket + + def send_request(): + stream.write("GET / HTTP/1.0\r\nHost: friendfeed.com\r\n\r\n") + stream.read_until("\r\n\r\n", on_headers) + + def on_headers(data): + headers = {} + for line in data.split("\r\n"): + parts = line.split(":") + if len(parts) == 2: + headers[parts[0].strip()] = parts[1].strip() + stream.read_bytes(int(headers["Content-Length"]), on_body) + + def on_body(data): + print data + stream.close() + ioloop.IOLoop.instance().stop() + + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) + stream = iostream.IOStream(s) + stream.connect(("friendfeed.com", 80), send_request) + ioloop.IOLoop.instance().start() + + """ + def __init__(self, socket, io_loop=None, max_buffer_size=104857600, + read_chunk_size=4096): + self.socket = socket + self.socket.setblocking(False) + self.io_loop = io_loop or ioloop.IOLoop.instance() + self.max_buffer_size = max_buffer_size + self.read_chunk_size = read_chunk_size + self._read_buffer = collections.deque() + self._write_buffer = collections.deque() + self._read_buffer_size = 0 + self._write_buffer_frozen = False + self._read_delimiter = None + self._read_regex = None + self._read_bytes = None + self._read_until_close = False + self._read_callback = None + self._streaming_callback = None + self._write_callback = None + self._close_callback = None + self._connect_callback = None + self._connecting = False + self._state = None + self._pending_callbacks = 0 + + def connect(self, address, callback=None): + """Connects the socket to a remote address without blocking. + + May only be called if the socket passed to the constructor was + not previously connected. The address parameter is in the + same format as for socket.connect, i.e. a (host, port) tuple. + If callback is specified, it will be called when the + connection is completed. + + Note that it is safe to call IOStream.write while the + connection is pending, in which case the data will be written + as soon as the connection is ready. Calling IOStream read + methods before the socket is connected works on some platforms + but is non-portable. + """ + self._connecting = True + try: + self.socket.connect(address) + except socket.error, e: + # In non-blocking mode we expect connect() to raise an + # exception with EINPROGRESS or EWOULDBLOCK. + # + # On freebsd, other errors such as ECONNREFUSED may be + # returned immediately when attempting to connect to + # localhost, so handle them the same way as an error + # reported later in _handle_connect. + if e.args[0] not in (errno.EINPROGRESS, errno.EWOULDBLOCK): + logging.warning("Connect error on fd %d: %s", + self.socket.fileno(), e) + self.close() + return + self._connect_callback = stack_context.wrap(callback) + self._add_io_state(self.io_loop.WRITE) + + def read_until_regex(self, regex, callback): + """Call callback when we read the given regex pattern.""" + assert not self._read_callback, "Already reading" + self._read_regex = re.compile(regex) + self._read_callback = stack_context.wrap(callback) + while True: + # See if we've already got the data from a previous read + if self._read_from_buffer(): + return + self._check_closed() + if self._read_to_buffer() == 0: + break + self._add_io_state(self.io_loop.READ) + + def read_until(self, delimiter, callback): + """Call callback when we read the given delimiter.""" + assert not self._read_callback, "Already reading" + self._read_delimiter = delimiter + self._read_callback = stack_context.wrap(callback) + while True: + # See if we've already got the data from a previous read + if self._read_from_buffer(): + return + self._check_closed() + if self._read_to_buffer() == 0: + break + self._add_io_state(self.io_loop.READ) + + def read_bytes(self, num_bytes, callback, streaming_callback=None): + """Call callback when we read the given number of bytes. + + If a ``streaming_callback`` is given, it will be called with chunks + of data as they become available, and the argument to the final + ``callback`` will be empty. + """ + assert not self._read_callback, "Already reading" + assert isinstance(num_bytes, (int, long)) + self._read_bytes = num_bytes + self._read_callback = stack_context.wrap(callback) + self._streaming_callback = stack_context.wrap(streaming_callback) + while True: + if self._read_from_buffer(): + return + self._check_closed() + if self._read_to_buffer() == 0: + break + self._add_io_state(self.io_loop.READ) + + def read_until_close(self, callback, streaming_callback=None): + """Reads all data from the socket until it is closed. + + If a ``streaming_callback`` is given, it will be called with chunks + of data as they become available, and the argument to the final + ``callback`` will be empty. + + Subject to ``max_buffer_size`` limit from `IOStream` constructor if + a ``streaming_callback`` is not used. + """ + assert not self._read_callback, "Already reading" + if self.closed(): + self._run_callback(callback, self._consume(self._read_buffer_size)) + return + self._read_until_close = True + self._read_callback = stack_context.wrap(callback) + self._streaming_callback = stack_context.wrap(streaming_callback) + self._add_io_state(self.io_loop.READ) + + def write(self, data, callback=None): + """Write the given data to this stream. + + If callback is given, we call it when all of the buffered write + data has been successfully written to the stream. If there was + previously buffered write data and an old write callback, that + callback is simply overwritten with this new callback. + """ + assert isinstance(data, bytes_type) + self._check_closed() + if data: + # We use bool(_write_buffer) as a proxy for write_buffer_size>0, + # so never put empty strings in the buffer. + self._write_buffer.append(data) + self._write_callback = stack_context.wrap(callback) + self._handle_write() + if self._write_buffer: + self._add_io_state(self.io_loop.WRITE) + self._maybe_add_error_listener() + + def set_close_callback(self, callback): + """Call the given callback when the stream is closed.""" + self._close_callback = stack_context.wrap(callback) + + def close(self): + """Close this stream.""" + if self.socket is not None: + if self._read_until_close: + callback = self._read_callback + self._read_callback = None + self._read_until_close = False + self._run_callback(callback, + self._consume(self._read_buffer_size)) + if self._state is not None: + self.io_loop.remove_handler(self.socket.fileno()) + self._state = None + self.socket.close() + self.socket = None + if self._close_callback and self._pending_callbacks == 0: + # if there are pending callbacks, don't run the close callback + # until they're done (see _maybe_add_error_handler) + cb = self._close_callback + self._close_callback = None + self._run_callback(cb) + + def reading(self): + """Returns true if we are currently reading from the stream.""" + return self._read_callback is not None + + def writing(self): + """Returns true if we are currently writing to the stream.""" + return bool(self._write_buffer) + + def closed(self): + """Returns true if the stream has been closed.""" + return self.socket is None + + def _handle_events(self, fd, events): + if not self.socket: + logging.warning("Got events for closed stream %d", fd) + return + try: + if events & self.io_loop.READ: + self._handle_read() + if not self.socket: + return + if events & self.io_loop.WRITE: + if self._connecting: + self._handle_connect() + self._handle_write() + if not self.socket: + return + if events & self.io_loop.ERROR: + # We may have queued up a user callback in _handle_read or + # _handle_write, so don't close the IOStream until those + # callbacks have had a chance to run. + self.io_loop.add_callback(self.close) + return + state = self.io_loop.ERROR + if self.reading(): + state |= self.io_loop.READ + if self.writing(): + state |= self.io_loop.WRITE + if state == self.io_loop.ERROR: + state |= self.io_loop.READ + if state != self._state: + assert self._state is not None, \ + "shouldn't happen: _handle_events without self._state" + self._state = state + self.io_loop.update_handler(self.socket.fileno(), self._state) + except Exception: + logging.error("Uncaught exception, closing connection.", + exc_info=True) + self.close() + raise + + def _run_callback(self, callback, *args): + def wrapper(): + self._pending_callbacks -= 1 + try: + callback(*args) + except Exception: + logging.error("Uncaught exception, closing connection.", + exc_info=True) + # Close the socket on an uncaught exception from a user callback + # (It would eventually get closed when the socket object is + # gc'd, but we don't want to rely on gc happening before we + # run out of file descriptors) + self.close() + # Re-raise the exception so that IOLoop.handle_callback_exception + # can see it and log the error + raise + self._maybe_add_error_listener() + # We schedule callbacks to be run on the next IOLoop iteration + # rather than running them directly for several reasons: + # * Prevents unbounded stack growth when a callback calls an + # IOLoop operation that immediately runs another callback + # * Provides a predictable execution context for e.g. + # non-reentrant mutexes + # * Ensures that the try/except in wrapper() is run outside + # of the application's StackContexts + with stack_context.NullContext(): + # stack_context was already captured in callback, we don't need to + # capture it again for IOStream's wrapper. This is especially + # important if the callback was pre-wrapped before entry to + # IOStream (as in HTTPConnection._header_callback), as we could + # capture and leak the wrong context here. + self._pending_callbacks += 1 + self.io_loop.add_callback(wrapper) + + def _handle_read(self): + while True: + try: + # Read from the socket until we get EWOULDBLOCK or equivalent. + # SSL sockets do some internal buffering, and if the data is + # sitting in the SSL object's buffer select() and friends + # can't see it; the only way to find out if it's there is to + # try to read it. + result = self._read_to_buffer() + except Exception: + self.close() + return + if result == 0: + break + else: + if self._read_from_buffer(): + return + + def _read_from_socket(self): + """Attempts to read from the socket. + + Returns the data read or None if there is nothing to read. + May be overridden in subclasses. + """ + try: + chunk = self.socket.recv(self.read_chunk_size) + except socket.error, e: + if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN): + return None + else: + raise + if not chunk: + self.close() + return None + return chunk + + def _read_to_buffer(self): + """Reads from the socket and appends the result to the read buffer. + + Returns the number of bytes read. Returns 0 if there is nothing + to read (i.e. the read returns EWOULDBLOCK or equivalent). On + error closes the socket and raises an exception. + """ + try: + chunk = self._read_from_socket() + except socket.error, e: + # ssl.SSLError is a subclass of socket.error + logging.warning("Read error on %d: %s", + self.socket.fileno(), e) + self.close() + raise + if chunk is None: + return 0 + self._read_buffer.append(chunk) + self._read_buffer_size += len(chunk) + if self._read_buffer_size >= self.max_buffer_size: + logging.error("Reached maximum read buffer size") + self.close() + raise IOError("Reached maximum read buffer size") + return len(chunk) + + def _read_from_buffer(self): + """Attempts to complete the currently-pending read from the buffer. + + Returns True if the read was completed. + """ + if self._read_bytes is not None: + if self._streaming_callback is not None and self._read_buffer_size: + bytes_to_consume = min(self._read_bytes, self._read_buffer_size) + self._read_bytes -= bytes_to_consume + self._run_callback(self._streaming_callback, + self._consume(bytes_to_consume)) + if self._read_buffer_size >= self._read_bytes: + num_bytes = self._read_bytes + callback = self._read_callback + self._read_callback = None + self._streaming_callback = None + self._read_bytes = None + self._run_callback(callback, self._consume(num_bytes)) + return True + elif self._read_delimiter is not None: + # Multi-byte delimiters (e.g. '\r\n') may straddle two + # chunks in the read buffer, so we can't easily find them + # without collapsing the buffer. However, since protocols + # using delimited reads (as opposed to reads of a known + # length) tend to be "line" oriented, the delimiter is likely + # to be in the first few chunks. Merge the buffer gradually + # since large merges are relatively expensive and get undone in + # consume(). + loc = -1 + if self._read_buffer: + loc = self._read_buffer[0].find(self._read_delimiter) + while loc == -1 and len(self._read_buffer) > 1: + # Grow by doubling, but don't split the second chunk just + # because the first one is small. + new_len = max(len(self._read_buffer[0]) * 2, + (len(self._read_buffer[0]) + + len(self._read_buffer[1]))) + _merge_prefix(self._read_buffer, new_len) + loc = self._read_buffer[0].find(self._read_delimiter) + if loc != -1: + callback = self._read_callback + delimiter_len = len(self._read_delimiter) + self._read_callback = None + self._streaming_callback = None + self._read_delimiter = None + self._run_callback(callback, + self._consume(loc + delimiter_len)) + return True + elif self._read_regex is not None: + m = None + if self._read_buffer: + m = self._read_regex.search(self._read_buffer[0]) + while m is None and len(self._read_buffer) > 1: + # Grow by doubling, but don't split the second chunk just + # because the first one is small. + new_len = max(len(self._read_buffer[0]) * 2, + (len(self._read_buffer[0]) + + len(self._read_buffer[1]))) + _merge_prefix(self._read_buffer, new_len) + m = self._read_regex.search(self._read_buffer[0]) + _merge_prefix(self._read_buffer, sys.maxint) + m = self._read_regex.search(self._read_buffer[0]) + if m: + callback = self._read_callback + self._read_callback = None + self._streaming_callback = None + self._read_regex = None + self._run_callback(callback, self._consume(m.end())) + return True + elif self._read_until_close: + if self._streaming_callback is not None and self._read_buffer_size: + self._run_callback(self._streaming_callback, + self._consume(self._read_buffer_size)) + return False + + def _handle_connect(self): + err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) + if err != 0: + # IOLoop implementations may vary: some of them return + # an error state before the socket becomes writable, so + # in that case a connection failure would be handled by the + # error path in _handle_events instead of here. + logging.warning("Connect error on fd %d: %s", + self.socket.fileno(), errno.errorcode[err]) + self.close() + return + if self._connect_callback is not None: + callback = self._connect_callback + self._connect_callback = None + self._run_callback(callback) + self._connecting = False + + def _handle_write(self): + while self._write_buffer: + try: + if not self._write_buffer_frozen: + # On windows, socket.send blows up if given a + # write buffer that's too large, instead of just + # returning the number of bytes it was able to + # process. Therefore we must not call socket.send + # with more than 128KB at a time. + _merge_prefix(self._write_buffer, 128 * 1024) + num_bytes = self.socket.send(self._write_buffer[0]) + if num_bytes == 0: + # With OpenSSL, if we couldn't write the entire buffer, + # the very same string object must be used on the + # next call to send. Therefore we suppress + # merging the write buffer after an incomplete send. + # A cleaner solution would be to set + # SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER, but this is + # not yet accessible from python + # (http://bugs.python.org/issue8240) + self._write_buffer_frozen = True + break + self._write_buffer_frozen = False + _merge_prefix(self._write_buffer, num_bytes) + self._write_buffer.popleft() + except socket.error, e: + if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN): + self._write_buffer_frozen = True + break + else: + logging.warning("Write error on %d: %s", + self.socket.fileno(), e) + self.close() + return + if not self._write_buffer and self._write_callback: + callback = self._write_callback + self._write_callback = None + self._run_callback(callback) + + def _consume(self, loc): + if loc == 0: + return b("") + _merge_prefix(self._read_buffer, loc) + self._read_buffer_size -= loc + return self._read_buffer.popleft() + + def _check_closed(self): + if not self.socket: + raise IOError("Stream is closed") + + def _maybe_add_error_listener(self): + if self._state is None and self._pending_callbacks == 0: + if self.socket is None: + cb = self._close_callback + if cb is not None: + self._close_callback = None + self._run_callback(cb) + else: + self._add_io_state(ioloop.IOLoop.READ) + + def _add_io_state(self, state): + """Adds `state` (IOLoop.{READ,WRITE} flags) to our event handler. + + Implementation notes: Reads and writes have a fast path and a + slow path. The fast path reads synchronously from socket + buffers, while the slow path uses `_add_io_state` to schedule + an IOLoop callback. Note that in both cases, the callback is + run asynchronously with `_run_callback`. + + To detect closed connections, we must have called + `_add_io_state` at some point, but we want to delay this as + much as possible so we don't have to set an `IOLoop.ERROR` + listener that will be overwritten by the next slow-path + operation. As long as there are callbacks scheduled for + fast-path ops, those callbacks may do more reads. + If a sequence of fast-path ops do not end in a slow-path op, + (e.g. for an @asynchronous long-poll request), we must add + the error handler. This is done in `_run_callback` and `write` + (since the write callback is optional so we can have a + fast-path write with no `_run_callback`) + """ + if self.socket is None: + # connection has been closed, so there can be no future events + return + if self._state is None: + self._state = ioloop.IOLoop.ERROR | state + with stack_context.NullContext(): + self.io_loop.add_handler( + self.socket.fileno(), self._handle_events, self._state) + elif not self._state & state: + self._state = self._state | state + self.io_loop.update_handler(self.socket.fileno(), self._state) + + +class SSLIOStream(IOStream): + """A utility class to write to and read from a non-blocking SSL socket. + + If the socket passed to the constructor is already connected, + it should be wrapped with:: + + ssl.wrap_socket(sock, do_handshake_on_connect=False, **kwargs) + + before constructing the SSLIOStream. Unconnected sockets will be + wrapped when IOStream.connect is finished. + """ + def __init__(self, *args, **kwargs): + """Creates an SSLIOStream. + + If a dictionary is provided as keyword argument ssl_options, + it will be used as additional keyword arguments to ssl.wrap_socket. + """ + self._ssl_options = kwargs.pop('ssl_options', {}) + super(SSLIOStream, self).__init__(*args, **kwargs) + self._ssl_accepting = True + self._handshake_reading = False + self._handshake_writing = False + + def reading(self): + return self._handshake_reading or super(SSLIOStream, self).reading() + + def writing(self): + return self._handshake_writing or super(SSLIOStream, self).writing() + + def _do_ssl_handshake(self): + # Based on code from test_ssl.py in the python stdlib + try: + self._handshake_reading = False + self._handshake_writing = False + self.socket.do_handshake() + except ssl.SSLError, err: + if err.args[0] == ssl.SSL_ERROR_WANT_READ: + self._handshake_reading = True + return + elif err.args[0] == ssl.SSL_ERROR_WANT_WRITE: + self._handshake_writing = True + return + elif err.args[0] in (ssl.SSL_ERROR_EOF, + ssl.SSL_ERROR_ZERO_RETURN): + return self.close() + elif err.args[0] == ssl.SSL_ERROR_SSL: + logging.warning("SSL Error on %d: %s", self.socket.fileno(), err) + return self.close() + raise + except socket.error, err: + if err.args[0] == errno.ECONNABORTED: + return self.close() + else: + self._ssl_accepting = False + super(SSLIOStream, self)._handle_connect() + + def _handle_read(self): + if self._ssl_accepting: + self._do_ssl_handshake() + return + super(SSLIOStream, self)._handle_read() + + def _handle_write(self): + if self._ssl_accepting: + self._do_ssl_handshake() + return + super(SSLIOStream, self)._handle_write() + + def _handle_connect(self): + self.socket = ssl.wrap_socket(self.socket, + do_handshake_on_connect=False, + **self._ssl_options) + # Don't call the superclass's _handle_connect (which is responsible + # for telling the application that the connection is complete) + # until we've completed the SSL handshake (so certificates are + # available, etc). + + + def _read_from_socket(self): + if self._ssl_accepting: + # If the handshake hasn't finished yet, there can't be anything + # to read (attempting to read may or may not raise an exception + # depending on the SSL version) + return None + try: + # SSLSocket objects have both a read() and recv() method, + # while regular sockets only have recv(). + # The recv() method blocks (at least in python 2.6) if it is + # called when there is nothing to read, so we have to use + # read() instead. + chunk = self.socket.read(self.read_chunk_size) + except ssl.SSLError, e: + # SSLError is a subclass of socket.error, so this except + # block must come first. + if e.args[0] == ssl.SSL_ERROR_WANT_READ: + return None + else: + raise + except socket.error, e: + if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN): + return None + else: + raise + if not chunk: + self.close() + return None + return chunk + +def _merge_prefix(deque, size): + """Replace the first entries in a deque of strings with a single + string of up to size bytes. + + >>> d = collections.deque(['abc', 'de', 'fghi', 'j']) + >>> _merge_prefix(d, 5); print d + deque(['abcde', 'fghi', 'j']) + + Strings will be split as necessary to reach the desired size. + >>> _merge_prefix(d, 7); print d + deque(['abcdefg', 'hi', 'j']) + + >>> _merge_prefix(d, 3); print d + deque(['abc', 'defg', 'hi', 'j']) + + >>> _merge_prefix(d, 100); print d + deque(['abcdefghij']) + """ + if len(deque) == 1 and len(deque[0]) <= size: + return + prefix = [] + remaining = size + while deque and remaining > 0: + chunk = deque.popleft() + if len(chunk) > remaining: + deque.appendleft(chunk[remaining:]) + chunk = chunk[:remaining] + prefix.append(chunk) + remaining -= len(chunk) + # This data structure normally just contains byte strings, but + # the unittest gets messy if it doesn't use the default str() type, + # so do the merge based on the type of data that's actually present. + if prefix: + deque.appendleft(type(prefix[0])().join(prefix)) + if not deque: + deque.appendleft(b("")) + +def doctests(): + import doctest + return doctest.DocTestSuite() diff --git a/libs/tornado/locale.py b/libs/tornado/locale.py new file mode 100644 index 00000000..61cdb7e7 --- /dev/null +++ b/libs/tornado/locale.py @@ -0,0 +1,472 @@ +#!/usr/bin/env python +# +# Copyright 2009 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Translation methods for generating localized strings. + +To load a locale and generate a translated string:: + + user_locale = locale.get("es_LA") + print user_locale.translate("Sign out") + +locale.get() returns the closest matching locale, not necessarily the +specific locale you requested. You can support pluralization with +additional arguments to translate(), e.g.:: + + people = [...] + message = user_locale.translate( + "%(list)s is online", "%(list)s are online", len(people)) + print message % {"list": user_locale.list(people)} + +The first string is chosen if len(people) == 1, otherwise the second +string is chosen. + +Applications should call one of load_translations (which uses a simple +CSV format) or load_gettext_translations (which uses the .mo format +supported by gettext and related tools). If neither method is called, +the locale.translate method will simply return the original string. +""" + +import csv +import datetime +import logging +import os +import re + +_default_locale = "en_US" +_translations = {} +_supported_locales = frozenset([_default_locale]) +_use_gettext = False + +def get(*locale_codes): + """Returns the closest match for the given locale codes. + + We iterate over all given locale codes in order. If we have a tight + or a loose match for the code (e.g., "en" for "en_US"), we return + the locale. Otherwise we move to the next code in the list. + + By default we return en_US if no translations are found for any of + the specified locales. You can change the default locale with + set_default_locale() below. + """ + return Locale.get_closest(*locale_codes) + + +def set_default_locale(code): + """Sets the default locale, used in get_closest_locale(). + + The default locale is assumed to be the language used for all strings + in the system. The translations loaded from disk are mappings from + the default locale to the destination locale. Consequently, you don't + need to create a translation file for the default locale. + """ + global _default_locale + global _supported_locales + _default_locale = code + _supported_locales = frozenset(_translations.keys() + [_default_locale]) + + +def load_translations(directory): + u"""Loads translations from CSV files in a directory. + + Translations are strings with optional Python-style named placeholders + (e.g., "My name is %(name)s") and their associated translations. + + The directory should have translation files of the form LOCALE.csv, + e.g. es_GT.csv. The CSV files should have two or three columns: string, + translation, and an optional plural indicator. Plural indicators should + be one of "plural" or "singular". A given string can have both singular + and plural forms. For example "%(name)s liked this" may have a + different verb conjugation depending on whether %(name)s is one + name or a list of names. There should be two rows in the CSV file for + that string, one with plural indicator "singular", and one "plural". + For strings with no verbs that would change on translation, simply + use "unknown" or the empty string (or don't include the column at all). + + The file is read using the csv module in the default "excel" dialect. + In this format there should not be spaces after the commas. + + Example translation es_LA.csv: + + "I love you","Te amo" + "%(name)s liked this","A %(name)s les gust\u00f3 esto","plural" + "%(name)s liked this","A %(name)s le gust\u00f3 esto","singular" + + """ + global _translations + global _supported_locales + _translations = {} + for path in os.listdir(directory): + if not path.endswith(".csv"): continue + locale, extension = path.split(".") + if not re.match("[a-z]+(_[A-Z]+)?$", locale): + logging.error("Unrecognized locale %r (path: %s)", locale, + os.path.join(directory, path)) + continue + f = open(os.path.join(directory, path), "r") + _translations[locale] = {} + for i, row in enumerate(csv.reader(f)): + if not row or len(row) < 2: continue + row = [c.decode("utf-8").strip() for c in row] + english, translation = row[:2] + if len(row) > 2: + plural = row[2] or "unknown" + else: + plural = "unknown" + if plural not in ("plural", "singular", "unknown"): + logging.error("Unrecognized plural indicator %r in %s line %d", + plural, path, i + 1) + continue + _translations[locale].setdefault(plural, {})[english] = translation + f.close() + _supported_locales = frozenset(_translations.keys() + [_default_locale]) + logging.info("Supported locales: %s", sorted(_supported_locales)) + +def load_gettext_translations(directory, domain): + """Loads translations from gettext's locale tree + + Locale tree is similar to system's /usr/share/locale, like: + + {directory}/{lang}/LC_MESSAGES/{domain}.mo + + Three steps are required to have you app translated: + + 1. Generate POT translation file + xgettext --language=Python --keyword=_:1,2 -d cyclone file1.py file2.html etc + + 2. Merge against existing POT file: + msgmerge old.po cyclone.po > new.po + + 3. Compile: + msgfmt cyclone.po -o {directory}/pt_BR/LC_MESSAGES/cyclone.mo + """ + import gettext + global _translations + global _supported_locales + global _use_gettext + _translations = {} + for lang in os.listdir(directory): + if lang.startswith('.'): continue # skip .svn, etc + if os.path.isfile(os.path.join(directory, lang)): continue + try: + os.stat(os.path.join(directory, lang, "LC_MESSAGES", domain+".mo")) + _translations[lang] = gettext.translation(domain, directory, + languages=[lang]) + except Exception, e: + logging.error("Cannot load translation for '%s': %s", lang, str(e)) + continue + _supported_locales = frozenset(_translations.keys() + [_default_locale]) + _use_gettext = True + logging.info("Supported locales: %s", sorted(_supported_locales)) + + +def get_supported_locales(cls): + """Returns a list of all the supported locale codes.""" + return _supported_locales + + +class Locale(object): + """Object representing a locale. + + After calling one of `load_translations` or `load_gettext_translations`, + call `get` or `get_closest` to get a Locale object. + """ + @classmethod + def get_closest(cls, *locale_codes): + """Returns the closest match for the given locale code.""" + for code in locale_codes: + if not code: continue + code = code.replace("-", "_") + parts = code.split("_") + if len(parts) > 2: + continue + elif len(parts) == 2: + code = parts[0].lower() + "_" + parts[1].upper() + if code in _supported_locales: + return cls.get(code) + if parts[0].lower() in _supported_locales: + return cls.get(parts[0].lower()) + return cls.get(_default_locale) + + @classmethod + def get(cls, code): + """Returns the Locale for the given locale code. + + If it is not supported, we raise an exception. + """ + if not hasattr(cls, "_cache"): + cls._cache = {} + if code not in cls._cache: + assert code in _supported_locales + translations = _translations.get(code, None) + if translations is None: + locale = CSVLocale(code, {}) + elif _use_gettext: + locale = GettextLocale(code, translations) + else: + locale = CSVLocale(code, translations) + cls._cache[code] = locale + return cls._cache[code] + + def __init__(self, code, translations): + self.code = code + self.name = LOCALE_NAMES.get(code, {}).get("name", u"Unknown") + self.rtl = False + for prefix in ["fa", "ar", "he"]: + if self.code.startswith(prefix): + self.rtl = True + break + self.translations = translations + + # Initialize strings for date formatting + _ = self.translate + self._months = [ + _("January"), _("February"), _("March"), _("April"), + _("May"), _("June"), _("July"), _("August"), + _("September"), _("October"), _("November"), _("December")] + self._weekdays = [ + _("Monday"), _("Tuesday"), _("Wednesday"), _("Thursday"), + _("Friday"), _("Saturday"), _("Sunday")] + + def translate(self, message, plural_message=None, count=None): + """Returns the translation for the given message for this locale. + + If plural_message is given, you must also provide count. We return + plural_message when count != 1, and we return the singular form + for the given message when count == 1. + """ + raise NotImplementedError() + + def format_date(self, date, gmt_offset=0, relative=True, shorter=False, + full_format=False): + """Formats the given date (which should be GMT). + + By default, we return a relative time (e.g., "2 minutes ago"). You + can return an absolute date string with relative=False. + + You can force a full format date ("July 10, 1980") with + full_format=True. + + This method is primarily intended for dates in the past. + For dates in the future, we fall back to full format. + """ + if self.code.startswith("ru"): + relative = False + if type(date) in (int, long, float): + date = datetime.datetime.utcfromtimestamp(date) + now = datetime.datetime.utcnow() + if date > now: + if relative and (date - now).seconds < 60: + # Due to click skew, things are some things slightly + # in the future. Round timestamps in the immediate + # future down to now in relative mode. + date = now + else: + # Otherwise, future dates always use the full format. + full_format = True + local_date = date - datetime.timedelta(minutes=gmt_offset) + local_now = now - datetime.timedelta(minutes=gmt_offset) + local_yesterday = local_now - datetime.timedelta(hours=24) + difference = now - date + seconds = difference.seconds + days = difference.days + + _ = self.translate + format = None + if not full_format: + if relative and days == 0: + if seconds < 50: + return _("1 second ago", "%(seconds)d seconds ago", + seconds) % { "seconds": seconds } + + if seconds < 50 * 60: + minutes = round(seconds / 60.0) + return _("1 minute ago", "%(minutes)d minutes ago", + minutes) % { "minutes": minutes } + + hours = round(seconds / (60.0 * 60)) + return _("1 hour ago", "%(hours)d hours ago", + hours) % { "hours": hours } + + if days == 0: + format = _("%(time)s") + elif days == 1 and local_date.day == local_yesterday.day and \ + relative: + format = _("yesterday") if shorter else \ + _("yesterday at %(time)s") + elif days < 5: + format = _("%(weekday)s") if shorter else \ + _("%(weekday)s at %(time)s") + elif days < 334: # 11mo, since confusing for same month last year + format = _("%(month_name)s %(day)s") if shorter else \ + _("%(month_name)s %(day)s at %(time)s") + + if format is None: + format = _("%(month_name)s %(day)s, %(year)s") if shorter else \ + _("%(month_name)s %(day)s, %(year)s at %(time)s") + + tfhour_clock = self.code not in ("en", "en_US", "zh_CN") + if tfhour_clock: + str_time = "%d:%02d" % (local_date.hour, local_date.minute) + elif self.code == "zh_CN": + str_time = "%s%d:%02d" % ( + (u'\u4e0a\u5348', u'\u4e0b\u5348')[local_date.hour >= 12], + local_date.hour % 12 or 12, local_date.minute) + else: + str_time = "%d:%02d %s" % ( + local_date.hour % 12 or 12, local_date.minute, + ("am", "pm")[local_date.hour >= 12]) + + return format % { + "month_name": self._months[local_date.month - 1], + "weekday": self._weekdays[local_date.weekday()], + "day": str(local_date.day), + "year": str(local_date.year), + "time": str_time + } + + def format_day(self, date, gmt_offset=0, dow=True): + """Formats the given date as a day of week. + + Example: "Monday, January 22". You can remove the day of week with + dow=False. + """ + local_date = date - datetime.timedelta(minutes=gmt_offset) + _ = self.translate + if dow: + return _("%(weekday)s, %(month_name)s %(day)s") % { + "month_name": self._months[local_date.month - 1], + "weekday": self._weekdays[local_date.weekday()], + "day": str(local_date.day), + } + else: + return _("%(month_name)s %(day)s") % { + "month_name": self._months[local_date.month - 1], + "day": str(local_date.day), + } + + def list(self, parts): + """Returns a comma-separated list for the given list of parts. + + The format is, e.g., "A, B and C", "A and B" or just "A" for lists + of size 1. + """ + _ = self.translate + if len(parts) == 0: return "" + if len(parts) == 1: return parts[0] + comma = u' \u0648 ' if self.code.startswith("fa") else u", " + return _("%(commas)s and %(last)s") % { + "commas": comma.join(parts[:-1]), + "last": parts[len(parts) - 1], + } + + def friendly_number(self, value): + """Returns a comma-separated number for the given integer.""" + if self.code not in ("en", "en_US"): + return str(value) + value = str(value) + parts = [] + while value: + parts.append(value[-3:]) + value = value[:-3] + return ",".join(reversed(parts)) + +class CSVLocale(Locale): + """Locale implementation using tornado's CSV translation format.""" + def translate(self, message, plural_message=None, count=None): + if plural_message is not None: + assert count is not None + if count != 1: + message = plural_message + message_dict = self.translations.get("plural", {}) + else: + message_dict = self.translations.get("singular", {}) + else: + message_dict = self.translations.get("unknown", {}) + return message_dict.get(message, message) + +class GettextLocale(Locale): + """Locale implementation using the gettext module.""" + def translate(self, message, plural_message=None, count=None): + if plural_message is not None: + assert count is not None + return self.translations.ungettext(message, plural_message, count) + else: + return self.translations.ugettext(message) + +LOCALE_NAMES = { + "af_ZA": {"name_en": u"Afrikaans", "name": u"Afrikaans"}, + "am_ET": {"name_en": u"Amharic", "name": u'\u12a0\u121b\u122d\u129b'}, + "ar_AR": {"name_en": u"Arabic", "name": u"\u0627\u0644\u0639\u0631\u0628\u064a\u0629"}, + "bg_BG": {"name_en": u"Bulgarian", "name": u"\u0411\u044a\u043b\u0433\u0430\u0440\u0441\u043a\u0438"}, + "bn_IN": {"name_en": u"Bengali", "name": u"\u09ac\u09be\u0982\u09b2\u09be"}, + "bs_BA": {"name_en": u"Bosnian", "name": u"Bosanski"}, + "ca_ES": {"name_en": u"Catalan", "name": u"Catal\xe0"}, + "cs_CZ": {"name_en": u"Czech", "name": u"\u010ce\u0161tina"}, + "cy_GB": {"name_en": u"Welsh", "name": u"Cymraeg"}, + "da_DK": {"name_en": u"Danish", "name": u"Dansk"}, + "de_DE": {"name_en": u"German", "name": u"Deutsch"}, + "el_GR": {"name_en": u"Greek", "name": u"\u0395\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac"}, + "en_GB": {"name_en": u"English (UK)", "name": u"English (UK)"}, + "en_US": {"name_en": u"English (US)", "name": u"English (US)"}, + "es_ES": {"name_en": u"Spanish (Spain)", "name": u"Espa\xf1ol (Espa\xf1a)"}, + "es_LA": {"name_en": u"Spanish", "name": u"Espa\xf1ol"}, + "et_EE": {"name_en": u"Estonian", "name": u"Eesti"}, + "eu_ES": {"name_en": u"Basque", "name": u"Euskara"}, + "fa_IR": {"name_en": u"Persian", "name": u"\u0641\u0627\u0631\u0633\u06cc"}, + "fi_FI": {"name_en": u"Finnish", "name": u"Suomi"}, + "fr_CA": {"name_en": u"French (Canada)", "name": u"Fran\xe7ais (Canada)"}, + "fr_FR": {"name_en": u"French", "name": u"Fran\xe7ais"}, + "ga_IE": {"name_en": u"Irish", "name": u"Gaeilge"}, + "gl_ES": {"name_en": u"Galician", "name": u"Galego"}, + "he_IL": {"name_en": u"Hebrew", "name": u"\u05e2\u05d1\u05e8\u05d9\u05ea"}, + "hi_IN": {"name_en": u"Hindi", "name": u"\u0939\u093f\u0928\u094d\u0926\u0940"}, + "hr_HR": {"name_en": u"Croatian", "name": u"Hrvatski"}, + "hu_HU": {"name_en": u"Hungarian", "name": u"Magyar"}, + "id_ID": {"name_en": u"Indonesian", "name": u"Bahasa Indonesia"}, + "is_IS": {"name_en": u"Icelandic", "name": u"\xcdslenska"}, + "it_IT": {"name_en": u"Italian", "name": u"Italiano"}, + "ja_JP": {"name_en": u"Japanese", "name": u"\u65e5\u672c\u8a9e"}, + "ko_KR": {"name_en": u"Korean", "name": u"\ud55c\uad6d\uc5b4"}, + "lt_LT": {"name_en": u"Lithuanian", "name": u"Lietuvi\u0173"}, + "lv_LV": {"name_en": u"Latvian", "name": u"Latvie\u0161u"}, + "mk_MK": {"name_en": u"Macedonian", "name": u"\u041c\u0430\u043a\u0435\u0434\u043e\u043d\u0441\u043a\u0438"}, + "ml_IN": {"name_en": u"Malayalam", "name": u"\u0d2e\u0d32\u0d2f\u0d3e\u0d33\u0d02"}, + "ms_MY": {"name_en": u"Malay", "name": u"Bahasa Melayu"}, + "nb_NO": {"name_en": u"Norwegian (bokmal)", "name": u"Norsk (bokm\xe5l)"}, + "nl_NL": {"name_en": u"Dutch", "name": u"Nederlands"}, + "nn_NO": {"name_en": u"Norwegian (nynorsk)", "name": u"Norsk (nynorsk)"}, + "pa_IN": {"name_en": u"Punjabi", "name": u"\u0a2a\u0a70\u0a1c\u0a3e\u0a2c\u0a40"}, + "pl_PL": {"name_en": u"Polish", "name": u"Polski"}, + "pt_BR": {"name_en": u"Portuguese (Brazil)", "name": u"Portugu\xeas (Brasil)"}, + "pt_PT": {"name_en": u"Portuguese (Portugal)", "name": u"Portugu\xeas (Portugal)"}, + "ro_RO": {"name_en": u"Romanian", "name": u"Rom\xe2n\u0103"}, + "ru_RU": {"name_en": u"Russian", "name": u"\u0420\u0443\u0441\u0441\u043a\u0438\u0439"}, + "sk_SK": {"name_en": u"Slovak", "name": u"Sloven\u010dina"}, + "sl_SI": {"name_en": u"Slovenian", "name": u"Sloven\u0161\u010dina"}, + "sq_AL": {"name_en": u"Albanian", "name": u"Shqip"}, + "sr_RS": {"name_en": u"Serbian", "name": u"\u0421\u0440\u043f\u0441\u043a\u0438"}, + "sv_SE": {"name_en": u"Swedish", "name": u"Svenska"}, + "sw_KE": {"name_en": u"Swahili", "name": u"Kiswahili"}, + "ta_IN": {"name_en": u"Tamil", "name": u"\u0ba4\u0bae\u0bbf\u0bb4\u0bcd"}, + "te_IN": {"name_en": u"Telugu", "name": u"\u0c24\u0c46\u0c32\u0c41\u0c17\u0c41"}, + "th_TH": {"name_en": u"Thai", "name": u"\u0e20\u0e32\u0e29\u0e32\u0e44\u0e17\u0e22"}, + "tl_PH": {"name_en": u"Filipino", "name": u"Filipino"}, + "tr_TR": {"name_en": u"Turkish", "name": u"T\xfcrk\xe7e"}, + "uk_UA": {"name_en": u"Ukraini ", "name": u"\u0423\u043a\u0440\u0430\u0457\u043d\u0441\u044c\u043a\u0430"}, + "vi_VN": {"name_en": u"Vietnamese", "name": u"Ti\u1ebfng Vi\u1ec7t"}, + "zh_CN": {"name_en": u"Chinese (Simplified)", "name": u"\u4e2d\u6587(\u7b80\u4f53)"}, + "zh_TW": {"name_en": u"Chinese (Traditional)", "name": u"\u4e2d\u6587(\u7e41\u9ad4)"}, +} diff --git a/libs/tornado/netutil.py b/libs/tornado/netutil.py new file mode 100644 index 00000000..1e1bcbf9 --- /dev/null +++ b/libs/tornado/netutil.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python +# +# Copyright 2011 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Miscellaneous network utility code.""" + +import errno +import logging +import os +import socket +import stat + +from tornado import process +from tornado.ioloop import IOLoop +from tornado.iostream import IOStream, SSLIOStream +from tornado.platform.auto import set_close_exec + +try: + import ssl # Python 2.6+ +except ImportError: + ssl = None + +class TCPServer(object): + r"""A non-blocking, single-threaded TCP server. + + To use `TCPServer`, define a subclass which overrides the `handle_stream` + method. + + `TCPServer` can serve SSL traffic with Python 2.6+ and OpenSSL. + To make this server serve SSL traffic, send the ssl_options dictionary + argument with the arguments required for the `ssl.wrap_socket` method, + including "certfile" and "keyfile":: + + TCPServer(ssl_options={ + "certfile": os.path.join(data_dir, "mydomain.crt"), + "keyfile": os.path.join(data_dir, "mydomain.key"), + }) + + `TCPServer` initialization follows one of three patterns: + + 1. `listen`: simple single-process:: + + server = TCPServer() + server.listen(8888) + IOLoop.instance().start() + + 2. `bind`/`start`: simple multi-process:: + + server = TCPServer() + server.bind(8888) + server.start(0) # Forks multiple sub-processes + IOLoop.instance().start() + + When using this interface, an `IOLoop` must *not* be passed + to the `TCPServer` constructor. `start` will always start + the server on the default singleton `IOLoop`. + + 3. `add_sockets`: advanced multi-process:: + + sockets = bind_sockets(8888) + tornado.process.fork_processes(0) + server = TCPServer() + server.add_sockets(sockets) + IOLoop.instance().start() + + The `add_sockets` interface is more complicated, but it can be + used with `tornado.process.fork_processes` to give you more + flexibility in when the fork happens. `add_sockets` can + also be used in single-process servers if you want to create + your listening sockets in some way other than + `bind_sockets`. + """ + def __init__(self, io_loop=None, ssl_options=None): + self.io_loop = io_loop + self.ssl_options = ssl_options + self._sockets = {} # fd -> socket object + self._pending_sockets = [] + self._started = False + + def listen(self, port, address=""): + """Starts accepting connections on the given port. + + This method may be called more than once to listen on multiple ports. + `listen` takes effect immediately; it is not necessary to call + `TCPServer.start` afterwards. It is, however, necessary to start + the `IOLoop`. + """ + sockets = bind_sockets(port, address=address) + self.add_sockets(sockets) + + def add_sockets(self, sockets): + """Makes this server start accepting connections on the given sockets. + + The ``sockets`` parameter is a list of socket objects such as + those returned by `bind_sockets`. + `add_sockets` is typically used in combination with that + method and `tornado.process.fork_processes` to provide greater + control over the initialization of a multi-process server. + """ + if self.io_loop is None: + self.io_loop = IOLoop.instance() + + for sock in sockets: + self._sockets[sock.fileno()] = sock + add_accept_handler(sock, self._handle_connection, + io_loop=self.io_loop) + + def add_socket(self, socket): + """Singular version of `add_sockets`. Takes a single socket object.""" + self.add_sockets([socket]) + + def bind(self, port, address=None, family=socket.AF_UNSPEC, backlog=128): + """Binds this server to the given port on the given address. + + To start the server, call `start`. If you want to run this server + in a single process, you can call `listen` as a shortcut to the + sequence of `bind` and `start` calls. + + Address may be either an IP address or hostname. If it's a hostname, + the server will listen on all IP addresses associated with the + name. Address may be an empty string or None to listen on all + available interfaces. Family may be set to either ``socket.AF_INET`` + or ``socket.AF_INET6`` to restrict to ipv4 or ipv6 addresses, otherwise + both will be used if available. + + The ``backlog`` argument has the same meaning as for + `socket.listen`. + + This method may be called multiple times prior to `start` to listen + on multiple ports or interfaces. + """ + sockets = bind_sockets(port, address=address, family=family, + backlog=backlog) + if self._started: + self.add_sockets(sockets) + else: + self._pending_sockets.extend(sockets) + + def start(self, num_processes=1): + """Starts this server in the IOLoop. + + By default, we run the server in this process and do not fork any + additional child process. + + If num_processes is ``None`` or <= 0, we detect the number of cores + available on this machine and fork that number of child + processes. If num_processes is given and > 1, we fork that + specific number of sub-processes. + + Since we use processes and not threads, there is no shared memory + between any server code. + + Note that multiple processes are not compatible with the autoreload + module (or the ``debug=True`` option to `tornado.web.Application`). + When using multiple processes, no IOLoops can be created or + referenced until after the call to ``TCPServer.start(n)``. + """ + assert not self._started + self._started = True + if num_processes != 1: + process.fork_processes(num_processes) + sockets = self._pending_sockets + self._pending_sockets = [] + self.add_sockets(sockets) + + def stop(self): + """Stops listening for new connections. + + Requests currently in progress may still continue after the + server is stopped. + """ + for fd, sock in self._sockets.iteritems(): + self.io_loop.remove_handler(fd) + sock.close() + + def handle_stream(self, stream, address): + """Override to handle a new `IOStream` from an incoming connection.""" + raise NotImplementedError() + + def _handle_connection(self, connection, address): + if self.ssl_options is not None: + assert ssl, "Python 2.6+ and OpenSSL required for SSL" + try: + connection = ssl.wrap_socket(connection, + server_side=True, + do_handshake_on_connect=False, + **self.ssl_options) + except ssl.SSLError, err: + if err.args[0] == ssl.SSL_ERROR_EOF: + return connection.close() + else: + raise + except socket.error, err: + if err.args[0] == errno.ECONNABORTED: + return connection.close() + else: + raise + try: + if self.ssl_options is not None: + stream = SSLIOStream(connection, io_loop=self.io_loop) + else: + stream = IOStream(connection, io_loop=self.io_loop) + self.handle_stream(stream, address) + except Exception: + logging.error("Error in connection callback", exc_info=True) + + +def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128): + """Creates listening sockets bound to the given port and address. + + Returns a list of socket objects (multiple sockets are returned if + the given address maps to multiple IP addresses, which is most common + for mixed IPv4 and IPv6 use). + + Address may be either an IP address or hostname. If it's a hostname, + the server will listen on all IP addresses associated with the + name. Address may be an empty string or None to listen on all + available interfaces. Family may be set to either socket.AF_INET + or socket.AF_INET6 to restrict to ipv4 or ipv6 addresses, otherwise + both will be used if available. + + The ``backlog`` argument has the same meaning as for + ``socket.listen()``. + """ + sockets = [] + if address == "": + address = None + flags = socket.AI_PASSIVE + if hasattr(socket, "AI_ADDRCONFIG"): + # AI_ADDRCONFIG ensures that we only try to bind on ipv6 + # if the system is configured for it, but the flag doesn't + # exist on some platforms (specifically WinXP, although + # newer versions of windows have it) + flags |= socket.AI_ADDRCONFIG + for res in set(socket.getaddrinfo(address, port, family, socket.SOCK_STREAM, + 0, flags)): + af, socktype, proto, canonname, sockaddr = res + sock = socket.socket(af, socktype, proto) + set_close_exec(sock.fileno()) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if af == socket.AF_INET6: + # On linux, ipv6 sockets accept ipv4 too by default, + # but this makes it impossible to bind to both + # 0.0.0.0 in ipv4 and :: in ipv6. On other systems, + # separate sockets *must* be used to listen for both ipv4 + # and ipv6. For consistency, always disable ipv4 on our + # ipv6 sockets and use a separate ipv4 socket when needed. + # + # Python 2.x on windows doesn't have IPPROTO_IPV6. + if hasattr(socket, "IPPROTO_IPV6"): + sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1) + sock.setblocking(0) + sock.bind(sockaddr) + sock.listen(backlog) + sockets.append(sock) + return sockets + +if hasattr(socket, 'AF_UNIX'): + def bind_unix_socket(file, mode=0600, backlog=128): + """Creates a listening unix socket. + + If a socket with the given name already exists, it will be deleted. + If any other file with that name exists, an exception will be + raised. + + Returns a socket object (not a list of socket objects like + `bind_sockets`) + """ + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + set_close_exec(sock.fileno()) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.setblocking(0) + try: + st = os.stat(file) + except OSError, err: + if err.errno != errno.ENOENT: + raise + else: + if stat.S_ISSOCK(st.st_mode): + os.remove(file) + else: + raise ValueError("File %s exists and is not a socket", file) + sock.bind(file) + os.chmod(file, mode) + sock.listen(backlog) + return sock + +def add_accept_handler(sock, callback, io_loop=None): + """Adds an ``IOLoop`` event handler to accept new connections on ``sock``. + + When a connection is accepted, ``callback(connection, address)`` will + be run (``connection`` is a socket object, and ``address`` is the + address of the other end of the connection). Note that this signature + is different from the ``callback(fd, events)`` signature used for + ``IOLoop`` handlers. + """ + if io_loop is None: + io_loop = IOLoop.instance() + def accept_handler(fd, events): + while True: + try: + connection, address = sock.accept() + except socket.error, e: + if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN): + return + raise + callback(connection, address) + io_loop.add_handler(sock.fileno(), accept_handler, IOLoop.READ) diff --git a/libs/tornado/options.py b/libs/tornado/options.py new file mode 100644 index 00000000..5fb91e1f --- /dev/null +++ b/libs/tornado/options.py @@ -0,0 +1,422 @@ +#!/usr/bin/env python +# +# Copyright 2009 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""A command line parsing module that lets modules define their own options. + +Each module defines its own options, e.g.:: + + from tornado.options import define, options + + define("mysql_host", default="127.0.0.1:3306", help="Main user DB") + define("memcache_hosts", default="127.0.0.1:11011", multiple=True, + help="Main user memcache servers") + + def connect(): + db = database.Connection(options.mysql_host) + ... + +The main() method of your application does not need to be aware of all of +the options used throughout your program; they are all automatically loaded +when the modules are loaded. Your main() method can parse the command line +or parse a config file with:: + + import tornado.options + tornado.options.parse_config_file("/etc/server.conf") + tornado.options.parse_command_line() + +Command line formats are what you would expect ("--myoption=myvalue"). +Config files are just Python files. Global names become options, e.g.:: + + myoption = "myvalue" + myotheroption = "myothervalue" + +We support datetimes, timedeltas, ints, and floats (just pass a 'type' +kwarg to define). We also accept multi-value options. See the documentation +for define() below. +""" + +import datetime +import logging +import logging.handlers +import re +import sys +import time + +from tornado.escape import _unicode + +# For pretty log messages, if available +try: + import curses +except ImportError: + curses = None + + +def define(name, default=None, type=None, help=None, metavar=None, + multiple=False, group=None): + """Defines a new command line option. + + If type is given (one of str, float, int, datetime, or timedelta) + or can be inferred from the default, we parse the command line + arguments based on the given type. If multiple is True, we accept + comma-separated values, and the option value is always a list. + + For multi-value integers, we also accept the syntax x:y, which + turns into range(x, y) - very useful for long integer ranges. + + help and metavar are used to construct the automatically generated + command line help string. The help message is formatted like:: + + --name=METAVAR help string + + group is used to group the defined options in logical groups. By default, + command line options are grouped by the defined file. + + Command line option names must be unique globally. They can be parsed + from the command line with parse_command_line() or parsed from a + config file with parse_config_file. + """ + if name in options: + raise Error("Option %r already defined in %s", name, + options[name].file_name) + frame = sys._getframe(0) + options_file = frame.f_code.co_filename + file_name = frame.f_back.f_code.co_filename + if file_name == options_file: file_name = "" + if type is None: + if not multiple and default is not None: + type = default.__class__ + else: + type = str + if group: + group_name = group + else: + group_name = file_name + options[name] = _Option(name, file_name=file_name, default=default, + type=type, help=help, metavar=metavar, + multiple=multiple, group_name=group_name) + + +def parse_command_line(args=None): + """Parses all options given on the command line. + + We return all command line arguments that are not options as a list. + """ + if args is None: args = sys.argv + remaining = [] + for i in xrange(1, len(args)): + # All things after the last option are command line arguments + if not args[i].startswith("-"): + remaining = args[i:] + break + if args[i] == "--": + remaining = args[i+1:] + break + arg = args[i].lstrip("-") + name, equals, value = arg.partition("=") + name = name.replace('-', '_') + if not name in options: + print_help() + raise Error('Unrecognized command line option: %r' % name) + option = options[name] + if not equals: + if option.type == bool: + value = "true" + else: + raise Error('Option %r requires a value' % name) + option.parse(value) + if options.help: + print_help() + sys.exit(0) + + # Set up log level and pretty console logging by default + if options.logging != 'none': + logging.getLogger().setLevel(getattr(logging, options.logging.upper())) + enable_pretty_logging() + + return remaining + + +def parse_config_file(path): + """Parses and loads the Python config file at the given path.""" + config = {} + execfile(path, config, config) + for name in config: + if name in options: + options[name].set(config[name]) + + +def print_help(file=sys.stdout): + """Prints all the command line options to stdout.""" + print >> file, "Usage: %s [OPTIONS]" % sys.argv[0] + print >> file, "" + print >> file, "Options:" + by_group = {} + for option in options.itervalues(): + by_group.setdefault(option.group_name, []).append(option) + + for filename, o in sorted(by_group.items()): + if filename: print >> file, filename + o.sort(key=lambda option: option.name) + for option in o: + prefix = option.name + if option.metavar: + prefix += "=" + option.metavar + print >> file, " --%-30s %s" % (prefix, option.help or "") + print >> file + + +class _Options(dict): + """Our global program options, an dictionary with object-like access.""" + @classmethod + def instance(cls): + if not hasattr(cls, "_instance"): + cls._instance = cls() + return cls._instance + + def __getattr__(self, name): + if isinstance(self.get(name), _Option): + return self[name].value() + raise AttributeError("Unrecognized option %r" % name) + + +class _Option(object): + def __init__(self, name, default=None, type=str, help=None, metavar=None, + multiple=False, file_name=None, group_name=None): + if default is None and multiple: + default = [] + self.name = name + self.type = type + self.help = help + self.metavar = metavar + self.multiple = multiple + self.file_name = file_name + self.group_name = group_name + self.default = default + self._value = None + + def value(self): + return self.default if self._value is None else self._value + + def parse(self, value): + _parse = { + datetime.datetime: self._parse_datetime, + datetime.timedelta: self._parse_timedelta, + bool: self._parse_bool, + str: self._parse_string, + }.get(self.type, self.type) + if self.multiple: + if self._value is None: + self._value = [] + for part in value.split(","): + if self.type in (int, long): + # allow ranges of the form X:Y (inclusive at both ends) + lo, _, hi = part.partition(":") + lo = _parse(lo) + hi = _parse(hi) if hi else lo + self._value.extend(range(lo, hi+1)) + else: + self._value.append(_parse(part)) + else: + self._value = _parse(value) + return self.value() + + def set(self, value): + if self.multiple: + if not isinstance(value, list): + raise Error("Option %r is required to be a list of %s" % + (self.name, self.type.__name__)) + for item in value: + if item != None and not isinstance(item, self.type): + raise Error("Option %r is required to be a list of %s" % + (self.name, self.type.__name__)) + else: + if value != None and not isinstance(value, self.type): + raise Error("Option %r is required to be a %s" % + (self.name, self.type.__name__)) + self._value = value + + # Supported date/time formats in our options + _DATETIME_FORMATS = [ + "%a %b %d %H:%M:%S %Y", + "%Y-%m-%d %H:%M:%S", + "%Y-%m-%d %H:%M", + "%Y-%m-%dT%H:%M", + "%Y%m%d %H:%M:%S", + "%Y%m%d %H:%M", + "%Y-%m-%d", + "%Y%m%d", + "%H:%M:%S", + "%H:%M", + ] + + def _parse_datetime(self, value): + for format in self._DATETIME_FORMATS: + try: + return datetime.datetime.strptime(value, format) + except ValueError: + pass + raise Error('Unrecognized date/time format: %r' % value) + + _TIMEDELTA_ABBREVS = [ + ('hours', ['h']), + ('minutes', ['m', 'min']), + ('seconds', ['s', 'sec']), + ('milliseconds', ['ms']), + ('microseconds', ['us']), + ('days', ['d']), + ('weeks', ['w']), + ] + + _TIMEDELTA_ABBREV_DICT = dict( + (abbrev, full) for full, abbrevs in _TIMEDELTA_ABBREVS + for abbrev in abbrevs) + + _FLOAT_PATTERN = r'[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?' + + _TIMEDELTA_PATTERN = re.compile( + r'\s*(%s)\s*(\w*)\s*' % _FLOAT_PATTERN, re.IGNORECASE) + + def _parse_timedelta(self, value): + try: + sum = datetime.timedelta() + start = 0 + while start < len(value): + m = self._TIMEDELTA_PATTERN.match(value, start) + if not m: + raise Exception() + num = float(m.group(1)) + units = m.group(2) or 'seconds' + units = self._TIMEDELTA_ABBREV_DICT.get(units, units) + sum += datetime.timedelta(**{units: num}) + start = m.end() + return sum + except Exception: + raise + + def _parse_bool(self, value): + return value.lower() not in ("false", "0", "f") + + def _parse_string(self, value): + return _unicode(value) + + +class Error(Exception): + """Exception raised by errors in the options module.""" + pass + + +def enable_pretty_logging(): + """Turns on formatted logging output as configured. + + This is called automatically by `parse_command_line`. + """ + root_logger = logging.getLogger() + if options.log_file_prefix: + channel = logging.handlers.RotatingFileHandler( + filename=options.log_file_prefix, + maxBytes=options.log_file_max_size, + backupCount=options.log_file_num_backups) + channel.setFormatter(_LogFormatter(color=False)) + root_logger.addHandler(channel) + + if (options.log_to_stderr or + (options.log_to_stderr is None and not root_logger.handlers)): + # Set up color if we are in a tty and curses is installed + color = False + if curses and sys.stderr.isatty(): + try: + curses.setupterm() + if curses.tigetnum("colors") > 0: + color = True + except Exception: + pass + channel = logging.StreamHandler() + channel.setFormatter(_LogFormatter(color=color)) + root_logger.addHandler(channel) + + + +class _LogFormatter(logging.Formatter): + def __init__(self, color, *args, **kwargs): + logging.Formatter.__init__(self, *args, **kwargs) + self._color = color + if color: + # The curses module has some str/bytes confusion in + # python3. Until version 3.2.3, most methods return + # bytes, but only accept strings. In addition, we want to + # output these strings with the logging module, which + # works with unicode strings. The explicit calls to + # unicode() below are harmless in python2 but will do the + # right conversion in python 3. + fg_color = (curses.tigetstr("setaf") or + curses.tigetstr("setf") or "") + if (3, 0) < sys.version_info < (3, 2, 3): + fg_color = unicode(fg_color, "ascii") + self._colors = { + logging.DEBUG: unicode(curses.tparm(fg_color, 4), # Blue + "ascii"), + logging.INFO: unicode(curses.tparm(fg_color, 2), # Green + "ascii"), + logging.WARNING: unicode(curses.tparm(fg_color, 3), # Yellow + "ascii"), + logging.ERROR: unicode(curses.tparm(fg_color, 1), # Red + "ascii"), + } + self._normal = unicode(curses.tigetstr("sgr0"), "ascii") + + def format(self, record): + try: + record.message = record.getMessage() + except Exception, e: + record.message = "Bad message (%r): %r" % (e, record.__dict__) + record.asctime = time.strftime( + "%y%m%d %H:%M:%S", self.converter(record.created)) + prefix = '[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]' % \ + record.__dict__ + if self._color: + prefix = (self._colors.get(record.levelno, self._normal) + + prefix + self._normal) + formatted = prefix + " " + record.message + if record.exc_info: + if not record.exc_text: + record.exc_text = self.formatException(record.exc_info) + if record.exc_text: + formatted = formatted.rstrip() + "\n" + record.exc_text + return formatted.replace("\n", "\n ") + + +options = _Options.instance() + + +# Default options +define("help", type=bool, help="show this help information") +define("logging", default="info", + help=("Set the Python log level. If 'none', tornado won't touch the " + "logging configuration."), + metavar="debug|info|warning|error|none") +define("log_to_stderr", type=bool, default=None, + help=("Send log output to stderr (colorized if possible). " + "By default use stderr if --log_file_prefix is not set and " + "no other logging is configured.")) +define("log_file_prefix", type=str, default=None, metavar="PATH", + help=("Path prefix for log files. " + "Note that if you are running multiple tornado processes, " + "log_file_prefix must be different for each of them (e.g. " + "include the port number)")) +define("log_file_max_size", type=int, default=100 * 1000 * 1000, + help="max size of log files before rollover") +define("log_file_num_backups", type=int, default=10, + help="number of log files to keep") diff --git a/libs/tornado/platform/__init__.py b/libs/tornado/platform/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/tornado/platform/auto.py b/libs/tornado/platform/auto.py new file mode 100644 index 00000000..e76d731b --- /dev/null +++ b/libs/tornado/platform/auto.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# +# Copyright 2011 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Implementation of platform-specific functionality. + +For each function or class described in `tornado.platform.interface`, +the appropriate platform-specific implementation exists in this module. +Most code that needs access to this functionality should do e.g.:: + + from tornado.platform.auto import set_close_exec +""" + +import os + +if os.name == 'nt': + from tornado.platform.windows import set_close_exec, Waker +else: + from tornado.platform.posix import set_close_exec, Waker diff --git a/libs/tornado/platform/interface.py b/libs/tornado/platform/interface.py new file mode 100644 index 00000000..20f0f716 --- /dev/null +++ b/libs/tornado/platform/interface.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# +# Copyright 2011 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Interfaces for platform-specific functionality. + +This module exists primarily for documentation purposes and as base classes +for other tornado.platform modules. Most code should import the appropriate +implementation from `tornado.platform.auto`. +""" + +def set_close_exec(fd): + """Sets the close-on-exec bit (``FD_CLOEXEC``)for a file descriptor.""" + raise NotImplementedError() + +class Waker(object): + """A socket-like object that can wake another thread from ``select()``. + + The `~tornado.ioloop.IOLoop` will add the Waker's `fileno()` to + its ``select`` (or ``epoll`` or ``kqueue``) calls. When another + thread wants to wake up the loop, it calls `wake`. Once it has woken + up, it will call `consume` to do any necessary per-wake cleanup. When + the ``IOLoop`` is closed, it closes its waker too. + """ + def fileno(self): + """Returns a file descriptor for this waker. + + Must be suitable for use with ``select()`` or equivalent on the + local platform. + """ + raise NotImplementedError() + + def wake(self): + """Triggers activity on the waker's file descriptor.""" + raise NotImplementedError() + + def consume(self): + """Called after the listen has woken up to do any necessary cleanup.""" + raise NotImplementedError() + + def close(self): + """Closes the waker's file descriptor(s).""" + raise NotImplementedError() + + diff --git a/libs/tornado/platform/posix.py b/libs/tornado/platform/posix.py new file mode 100644 index 00000000..aa09b31c --- /dev/null +++ b/libs/tornado/platform/posix.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +# +# Copyright 2011 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Posix implementations of platform-specific functionality.""" + +import fcntl +import os + +from tornado.platform import interface +from tornado.util import b + +def set_close_exec(fd): + flags = fcntl.fcntl(fd, fcntl.F_GETFD) + fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC) + +def _set_nonblocking(fd): + flags = fcntl.fcntl(fd, fcntl.F_GETFL) + fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) + +class Waker(interface.Waker): + def __init__(self): + r, w = os.pipe() + _set_nonblocking(r) + _set_nonblocking(w) + set_close_exec(r) + set_close_exec(w) + self.reader = os.fdopen(r, "rb", 0) + self.writer = os.fdopen(w, "wb", 0) + + def fileno(self): + return self.reader.fileno() + + def wake(self): + try: + self.writer.write(b("x")) + except IOError: + pass + + def consume(self): + try: + while True: + result = self.reader.read() + if not result: break; + except IOError: + pass + + def close(self): + self.reader.close() + self.writer.close() diff --git a/libs/tornado/platform/twisted.py b/libs/tornado/platform/twisted.py new file mode 100644 index 00000000..5d406d34 --- /dev/null +++ b/libs/tornado/platform/twisted.py @@ -0,0 +1,330 @@ +# Author: Ovidiu Predescu +# Date: July 2011 +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# Note: This module's docs are not currently extracted automatically, +# so changes must be made manually to twisted.rst +# TODO: refactor doc build process to use an appropriate virtualenv +"""A Twisted reactor built on the Tornado IOLoop. + +This module lets you run applications and libraries written for +Twisted in a Tornado application. To use it, simply call `install` at +the beginning of the application:: + + import tornado.platform.twisted + tornado.platform.twisted.install() + from twisted.internet import reactor + +When the app is ready to start, call `IOLoop.instance().start()` +instead of `reactor.run()`. This will allow you to use a mixture of +Twisted and Tornado code in the same process. + +It is also possible to create a non-global reactor by calling +`tornado.platform.twisted.TornadoReactor(io_loop)`. However, if +the `IOLoop` and reactor are to be short-lived (such as those used in +unit tests), additional cleanup may be required. Specifically, it is +recommended to call:: + + reactor.fireSystemEvent('shutdown') + reactor.disconnectAll() + +before closing the `IOLoop`. + +This module has been tested with Twisted versions 11.0.0 and 11.1.0. +""" + +from __future__ import with_statement, absolute_import + +import functools +import logging +import time + +from twisted.internet.posixbase import PosixReactorBase +from twisted.internet.interfaces import \ + IReactorFDSet, IDelayedCall, IReactorTime +from twisted.python import failure, log +from twisted.internet import error + +from zope.interface import implements + +import tornado +import tornado.ioloop +from tornado.stack_context import NullContext +from tornado.ioloop import IOLoop + + +class TornadoDelayedCall(object): + """DelayedCall object for Tornado.""" + implements(IDelayedCall) + + def __init__(self, reactor, seconds, f, *args, **kw): + self._reactor = reactor + self._func = functools.partial(f, *args, **kw) + self._time = self._reactor.seconds() + seconds + self._timeout = self._reactor._io_loop.add_timeout(self._time, + self._called) + self._active = True + + def _called(self): + self._active = False + self._reactor._removeDelayedCall(self) + try: + self._func() + except: + logging.error("_called caught exception", exc_info=True) + + def getTime(self): + return self._time + + def cancel(self): + self._active = False + self._reactor._io_loop.remove_timeout(self._timeout) + self._reactor._removeDelayedCall(self) + + def delay(self, seconds): + self._reactor._io_loop.remove_timeout(self._timeout) + self._time += seconds + self._timeout = self._reactor._io_loop.add_timeout(self._time, + self._called) + + def reset(self, seconds): + self._reactor._io_loop.remove_timeout(self._timeout) + self._time = self._reactor.seconds() + seconds + self._timeout = self._reactor._io_loop.add_timeout(self._time, + self._called) + + def active(self): + return self._active + +class TornadoReactor(PosixReactorBase): + """Twisted reactor built on the Tornado IOLoop. + + Since it is intented to be used in applications where the top-level + event loop is ``io_loop.start()`` rather than ``reactor.run()``, + it is implemented a little differently than other Twisted reactors. + We override `mainLoop` instead of `doIteration` and must implement + timed call functionality on top of `IOLoop.add_timeout` rather than + using the implementation in `PosixReactorBase`. + """ + implements(IReactorTime, IReactorFDSet) + + def __init__(self, io_loop=None): + if not io_loop: + io_loop = tornado.ioloop.IOLoop.instance() + self._io_loop = io_loop + self._readers = {} # map of reader objects to fd + self._writers = {} # map of writer objects to fd + self._fds = {} # a map of fd to a (reader, writer) tuple + self._delayedCalls = {} + PosixReactorBase.__init__(self) + + # IOLoop.start() bypasses some of the reactor initialization. + # Fire off the necessary events if they weren't already triggered + # by reactor.run(). + def start_if_necessary(): + if not self._started: + self.fireSystemEvent('startup') + self._io_loop.add_callback(start_if_necessary) + + # IReactorTime + def seconds(self): + return time.time() + + def callLater(self, seconds, f, *args, **kw): + dc = TornadoDelayedCall(self, seconds, f, *args, **kw) + self._delayedCalls[dc] = True + return dc + + def getDelayedCalls(self): + return [x for x in self._delayedCalls if x._active] + + def _removeDelayedCall(self, dc): + if dc in self._delayedCalls: + del self._delayedCalls[dc] + + # IReactorThreads + def callFromThread(self, f, *args, **kw): + """See `twisted.internet.interfaces.IReactorThreads.callFromThread`""" + assert callable(f), "%s is not callable" % f + p = functools.partial(f, *args, **kw) + self._io_loop.add_callback(p) + + # We don't need the waker code from the super class, Tornado uses + # its own waker. + def installWaker(self): + pass + + def wakeUp(self): + pass + + # IReactorFDSet + def _invoke_callback(self, fd, events): + (reader, writer) = self._fds[fd] + if reader: + err = None + if reader.fileno() == -1: + err = error.ConnectionLost() + elif events & IOLoop.READ: + err = log.callWithLogger(reader, reader.doRead) + if err is None and events & IOLoop.ERROR: + err = error.ConnectionLost() + if err is not None: + self.removeReader(reader) + reader.readConnectionLost(failure.Failure(err)) + if writer: + err = None + if writer.fileno() == -1: + err = error.ConnectionLost() + elif events & IOLoop.WRITE: + err = log.callWithLogger(writer, writer.doWrite) + if err is None and events & IOLoop.ERROR: + err = error.ConnectionLost() + if err is not None: + self.removeWriter(writer) + writer.writeConnectionLost(failure.Failure(err)) + + def addReader(self, reader): + """Add a FileDescriptor for notification of data available to read.""" + if reader in self._readers: + # Don't add the reader if it's already there + return + fd = reader.fileno() + self._readers[reader] = fd + if fd in self._fds: + (_, writer) = self._fds[fd] + self._fds[fd] = (reader, writer) + if writer: + # We already registered this fd for write events, + # update it for read events as well. + self._io_loop.update_handler(fd, IOLoop.READ | IOLoop.WRITE) + else: + with NullContext(): + self._fds[fd] = (reader, None) + self._io_loop.add_handler(fd, self._invoke_callback, + IOLoop.READ) + + def addWriter(self, writer): + """Add a FileDescriptor for notification of data available to write.""" + if writer in self._writers: + return + fd = writer.fileno() + self._writers[writer] = fd + if fd in self._fds: + (reader, _) = self._fds[fd] + self._fds[fd] = (reader, writer) + if reader: + # We already registered this fd for read events, + # update it for write events as well. + self._io_loop.update_handler(fd, IOLoop.READ | IOLoop.WRITE) + else: + with NullContext(): + self._fds[fd] = (None, writer) + self._io_loop.add_handler(fd, self._invoke_callback, + IOLoop.WRITE) + + def removeReader(self, reader): + """Remove a Selectable for notification of data available to read.""" + if reader in self._readers: + fd = self._readers.pop(reader) + (_, writer) = self._fds[fd] + if writer: + # We have a writer so we need to update the IOLoop for + # write events only. + self._fds[fd] = (None, writer) + self._io_loop.update_handler(fd, IOLoop.WRITE) + else: + # Since we have no writer registered, we remove the + # entry from _fds and unregister the handler from the + # IOLoop + del self._fds[fd] + self._io_loop.remove_handler(fd) + + def removeWriter(self, writer): + """Remove a Selectable for notification of data available to write.""" + if writer in self._writers: + fd = self._writers.pop(writer) + (reader, _) = self._fds[fd] + if reader: + # We have a reader so we need to update the IOLoop for + # read events only. + self._fds[fd] = (reader, None) + self._io_loop.update_handler(fd, IOLoop.READ) + else: + # Since we have no reader registered, we remove the + # entry from the _fds and unregister the handler from + # the IOLoop. + del self._fds[fd] + self._io_loop.remove_handler(fd) + + def removeAll(self): + return self._removeAll(self._readers, self._writers) + + def getReaders(self): + return self._readers.keys() + + def getWriters(self): + return self._writers.keys() + + # The following functions are mainly used in twisted-style test cases; + # it is expected that most users of the TornadoReactor will call + # IOLoop.start() instead of Reactor.run(). + def stop(self): + PosixReactorBase.stop(self) + self._io_loop.stop() + + def crash(self): + PosixReactorBase.crash(self) + self._io_loop.stop() + + def doIteration(self, delay): + raise NotImplementedError("doIteration") + + def mainLoop(self): + self._io_loop.start() + if self._stopped: + self.fireSystemEvent("shutdown") + +class _TestReactor(TornadoReactor): + """Subclass of TornadoReactor for use in unittests. + + This can't go in the test.py file because of import-order dependencies + with the Twisted reactor test builder. + """ + def __init__(self): + # always use a new ioloop + super(_TestReactor, self).__init__(IOLoop()) + + def listenTCP(self, port, factory, backlog=50, interface=''): + # default to localhost to avoid firewall prompts on the mac + if not interface: + interface = '127.0.0.1' + return super(_TestReactor, self).listenTCP( + port, factory, backlog=backlog, interface=interface) + + def listenUDP(self, port, protocol, interface='', maxPacketSize=8192): + if not interface: + interface = '127.0.0.1' + return super(_TestReactor, self).listenUDP( + port, protocol, interface=interface, maxPacketSize=maxPacketSize) + + + +def install(io_loop=None): + """Install this package as the default Twisted reactor.""" + if not io_loop: + io_loop = tornado.ioloop.IOLoop.instance() + reactor = TornadoReactor(io_loop) + from twisted.internet.main import installReactor + installReactor(reactor) + return reactor diff --git a/libs/tornado/platform/windows.py b/libs/tornado/platform/windows.py new file mode 100644 index 00000000..1735f1b3 --- /dev/null +++ b/libs/tornado/platform/windows.py @@ -0,0 +1,97 @@ +# NOTE: win32 support is currently experimental, and not recommended +# for production use. + +import ctypes +import ctypes.wintypes +import socket +import errno + +from tornado.platform import interface +from tornado.util import b + +# See: http://msdn.microsoft.com/en-us/library/ms724935(VS.85).aspx +SetHandleInformation = ctypes.windll.kernel32.SetHandleInformation +SetHandleInformation.argtypes = (ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD) +SetHandleInformation.restype = ctypes.wintypes.BOOL + +HANDLE_FLAG_INHERIT = 0x00000001 + + +def set_close_exec(fd): + success = SetHandleInformation(fd, HANDLE_FLAG_INHERIT, 0) + if not success: + raise ctypes.GetLastError() + + +class Waker(interface.Waker): + """Create an OS independent asynchronous pipe""" + def __init__(self): + # Based on Zope async.py: http://svn.zope.org/zc.ngi/trunk/src/zc/ngi/async.py + + self.writer = socket.socket() + # Disable buffering -- pulling the trigger sends 1 byte, + # and we want that sent immediately, to wake up ASAP. + self.writer.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + + count = 0 + while 1: + 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: + self.writer.connect(connect_address) + break # success + except socket.error, 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() + self.writer.close() + raise socket.error("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() + + self.reader, addr = a.accept() + self.reader.setblocking(0) + self.writer.setblocking(0) + a.close() + self.reader_fd = self.reader.fileno() + + def fileno(self): + return self.reader.fileno() + + def wake(self): + try: + self.writer.send(b("x")) + except IOError: + pass + + def consume(self): + try: + while True: + result = self.reader.recv(1024) + if not result: break + except IOError: + pass + + def close(self): + self.reader.close() + self.writer.close() diff --git a/libs/tornado/process.py b/libs/tornado/process.py new file mode 100644 index 00000000..06f6aa9b --- /dev/null +++ b/libs/tornado/process.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python +# +# Copyright 2011 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Utilities for working with multiple processes.""" + +import errno +import logging +import os +import sys +import time + +from binascii import hexlify + +from tornado import ioloop + +try: + import multiprocessing # Python 2.6+ +except ImportError: + multiprocessing = None + +def cpu_count(): + """Returns the number of processors on this machine.""" + if multiprocessing is not None: + try: + return multiprocessing.cpu_count() + except NotImplementedError: + pass + try: + return os.sysconf("SC_NPROCESSORS_CONF") + except ValueError: + pass + logging.error("Could not detect number of processors; assuming 1") + return 1 + +def _reseed_random(): + if 'random' not in sys.modules: + return + import random + # If os.urandom is available, this method does the same thing as + # random.seed (at least as of python 2.6). If os.urandom is not + # available, we mix in the pid in addition to a timestamp. + try: + seed = long(hexlify(os.urandom(16)), 16) + except NotImplementedError: + seed = int(time.time() * 1000) ^ os.getpid() + random.seed(seed) + + +_task_id = None + +def fork_processes(num_processes, max_restarts=100): + """Starts multiple worker processes. + + If ``num_processes`` is None or <= 0, we detect the number of cores + available on this machine and fork that number of child + processes. If ``num_processes`` is given and > 0, we fork that + specific number of sub-processes. + + Since we use processes and not threads, there is no shared memory + between any server code. + + Note that multiple processes are not compatible with the autoreload + module (or the debug=True option to `tornado.web.Application`). + When using multiple processes, no IOLoops can be created or + referenced until after the call to ``fork_processes``. + + In each child process, ``fork_processes`` returns its *task id*, a + number between 0 and ``num_processes``. Processes that exit + abnormally (due to a signal or non-zero exit status) are restarted + with the same id (up to ``max_restarts`` times). In the parent + process, ``fork_processes`` returns None if all child processes + have exited normally, but will otherwise only exit by throwing an + exception. + """ + global _task_id + assert _task_id is None + if num_processes is None or num_processes <= 0: + num_processes = cpu_count() + if ioloop.IOLoop.initialized(): + raise RuntimeError("Cannot run in multiple processes: IOLoop instance " + "has already been initialized. You cannot call " + "IOLoop.instance() before calling start_processes()") + logging.info("Starting %d processes", num_processes) + children = {} + def start_child(i): + pid = os.fork() + if pid == 0: + # child process + _reseed_random() + global _task_id + _task_id = i + return i + else: + children[pid] = i + return None + for i in range(num_processes): + id = start_child(i) + if id is not None: return id + num_restarts = 0 + while children: + try: + pid, status = os.wait() + except OSError, e: + if e.errno == errno.EINTR: + continue + raise + if pid not in children: + continue + id = children.pop(pid) + if os.WIFSIGNALED(status): + logging.warning("child %d (pid %d) killed by signal %d, restarting", + id, pid, os.WTERMSIG(status)) + elif os.WEXITSTATUS(status) != 0: + logging.warning("child %d (pid %d) exited with status %d, restarting", + id, pid, os.WEXITSTATUS(status)) + else: + logging.info("child %d (pid %d) exited normally", id, pid) + continue + num_restarts += 1 + if num_restarts > max_restarts: + raise RuntimeError("Too many child restarts, giving up") + new_id = start_child(id) + if new_id is not None: return new_id + # All child processes exited cleanly, so exit the master process + # instead of just returning to right after the call to + # fork_processes (which will probably just start up another IOLoop + # unless the caller checks the return value). + sys.exit(0) + +def task_id(): + """Returns the current task id, if any. + + Returns None if this process was not created by `fork_processes`. + """ + global _task_id + return _task_id diff --git a/libs/tornado/simple_httpclient.py b/libs/tornado/simple_httpclient.py new file mode 100644 index 00000000..376d410c --- /dev/null +++ b/libs/tornado/simple_httpclient.py @@ -0,0 +1,509 @@ +#!/usr/bin/env python +from __future__ import with_statement + +from tornado.escape import utf8, _unicode, native_str +from tornado.httpclient import HTTPRequest, HTTPResponse, HTTPError, AsyncHTTPClient, main +from tornado.httputil import HTTPHeaders +from tornado.iostream import IOStream, SSLIOStream +from tornado import stack_context +from tornado.util import b + +import base64 +import collections +import contextlib +import copy +import functools +import logging +import os.path +import re +import socket +import sys +import time +import urlparse +import zlib + +try: + from io import BytesIO # python 3 +except ImportError: + from cStringIO import StringIO as BytesIO # python 2 + +try: + import ssl # python 2.6+ +except ImportError: + ssl = None + +_DEFAULT_CA_CERTS = os.path.dirname(__file__) + '/ca-certificates.crt' + +class SimpleAsyncHTTPClient(AsyncHTTPClient): + """Non-blocking HTTP client with no external dependencies. + + This class implements an HTTP 1.1 client on top of Tornado's IOStreams. + It does not currently implement all applicable parts of the HTTP + specification, but it does enough to work with major web service APIs + (mostly tested against the Twitter API so far). + + This class has not been tested extensively in production and + should be considered somewhat experimental as of the release of + tornado 1.2. It is intended to become the default AsyncHTTPClient + implementation in a future release. It may either be used + directly, or to facilitate testing of this class with an existing + application, setting the environment variable + USE_SIMPLE_HTTPCLIENT=1 will cause this class to transparently + replace tornado.httpclient.AsyncHTTPClient. + + Some features found in the curl-based AsyncHTTPClient are not yet + supported. In particular, proxies are not supported, connections + are not reused, and callers cannot select the network interface to be + used. + + Python 2.6 or higher is required for HTTPS support. Users of Python 2.5 + should use the curl-based AsyncHTTPClient if HTTPS support is required. + + """ + def initialize(self, io_loop=None, max_clients=10, + max_simultaneous_connections=None, + hostname_mapping=None, max_buffer_size=104857600): + """Creates a AsyncHTTPClient. + + Only a single AsyncHTTPClient instance exists per IOLoop + in order to provide limitations on the number of pending connections. + force_instance=True may be used to suppress this behavior. + + max_clients is the number of concurrent requests that can be in + progress. max_simultaneous_connections has no effect and is accepted + only for compatibility with the curl-based AsyncHTTPClient. Note + that these arguments are only used when the client is first created, + and will be ignored when an existing client is reused. + + hostname_mapping is a dictionary mapping hostnames to IP addresses. + It can be used to make local DNS changes when modifying system-wide + settings like /etc/hosts is not possible or desirable (e.g. in + unittests). + + max_buffer_size is the number of bytes that can be read by IOStream. It + defaults to 100mb. + """ + self.io_loop = io_loop + self.max_clients = max_clients + self.queue = collections.deque() + self.active = {} + self.hostname_mapping = hostname_mapping + self.max_buffer_size = max_buffer_size + + def fetch(self, request, callback, **kwargs): + if not isinstance(request, HTTPRequest): + request = HTTPRequest(url=request, **kwargs) + if not isinstance(request.headers, HTTPHeaders): + request.headers = HTTPHeaders(request.headers) + callback = stack_context.wrap(callback) + self.queue.append((request, callback)) + self._process_queue() + if self.queue: + logging.debug("max_clients limit reached, request queued. " + "%d active, %d queued requests." % ( + len(self.active), len(self.queue))) + + def _process_queue(self): + with stack_context.NullContext(): + while self.queue and len(self.active) < self.max_clients: + request, callback = self.queue.popleft() + key = object() + self.active[key] = (request, callback) + _HTTPConnection(self.io_loop, self, request, + functools.partial(self._release_fetch, key), + callback, + self.max_buffer_size) + + def _release_fetch(self, key): + del self.active[key] + self._process_queue() + + + +class _HTTPConnection(object): + _SUPPORTED_METHODS = set(["GET", "HEAD", "POST", "PUT", "DELETE"]) + + def __init__(self, io_loop, client, request, release_callback, + final_callback, max_buffer_size): + self.start_time = time.time() + self.io_loop = io_loop + self.client = client + self.request = request + self.release_callback = release_callback + self.final_callback = final_callback + self.code = None + self.headers = None + self.chunks = None + self._decompressor = None + # Timeout handle returned by IOLoop.add_timeout + self._timeout = None + with stack_context.StackContext(self.cleanup): + parsed = urlparse.urlsplit(_unicode(self.request.url)) + if ssl is None and parsed.scheme == "https": + raise ValueError("HTTPS requires either python2.6+ or " + "curl_httpclient") + if parsed.scheme not in ("http", "https"): + raise ValueError("Unsupported url scheme: %s" % + self.request.url) + # urlsplit results have hostname and port results, but they + # didn't support ipv6 literals until python 2.7. + netloc = parsed.netloc + if "@" in netloc: + userpass, _, netloc = netloc.rpartition("@") + match = re.match(r'^(.+):(\d+)$', netloc) + if match: + host = match.group(1) + port = int(match.group(2)) + else: + host = netloc + port = 443 if parsed.scheme == "https" else 80 + if re.match(r'^\[.*\]$', host): + # raw ipv6 addresses in urls are enclosed in brackets + host = host[1:-1] + if self.client.hostname_mapping is not None: + host = self.client.hostname_mapping.get(host, host) + + if request.allow_ipv6: + af = socket.AF_UNSPEC + else: + # We only try the first IP we get from getaddrinfo, + # so restrict to ipv4 by default. + af = socket.AF_INET + + addrinfo = socket.getaddrinfo(host, port, af, socket.SOCK_STREAM, + 0, 0) + af, socktype, proto, canonname, sockaddr = addrinfo[0] + + if parsed.scheme == "https": + ssl_options = {} + if request.validate_cert: + ssl_options["cert_reqs"] = ssl.CERT_REQUIRED + if request.ca_certs is not None: + ssl_options["ca_certs"] = request.ca_certs + else: + ssl_options["ca_certs"] = _DEFAULT_CA_CERTS + if request.client_key is not None: + ssl_options["keyfile"] = request.client_key + if request.client_cert is not None: + ssl_options["certfile"] = request.client_cert + + # SSL interoperability is tricky. We want to disable + # SSLv2 for security reasons; it wasn't disabled by default + # until openssl 1.0. The best way to do this is to use + # the SSL_OP_NO_SSLv2, but that wasn't exposed to python + # until 3.2. Python 2.7 adds the ciphers argument, which + # can also be used to disable SSLv2. As a last resort + # on python 2.6, we set ssl_version to SSLv3. This is + # more narrow than we'd like since it also breaks + # compatibility with servers configured for TLSv1 only, + # but nearly all servers support SSLv3: + # http://blog.ivanristic.com/2011/09/ssl-survey-protocol-support.html + if sys.version_info >= (2,7): + ssl_options["ciphers"] = "DEFAULT:!SSLv2" + else: + # This is really only necessary for pre-1.0 versions + # of openssl, but python 2.6 doesn't expose version + # information. + ssl_options["ssl_version"] = ssl.PROTOCOL_SSLv3 + + self.stream = SSLIOStream(socket.socket(af, socktype, proto), + io_loop=self.io_loop, + ssl_options=ssl_options, + max_buffer_size=max_buffer_size) + else: + self.stream = IOStream(socket.socket(af, socktype, proto), + io_loop=self.io_loop, + max_buffer_size=max_buffer_size) + timeout = min(request.connect_timeout, request.request_timeout) + if timeout: + self._timeout = self.io_loop.add_timeout( + self.start_time + timeout, + self._on_timeout) + self.stream.set_close_callback(self._on_close) + self.stream.connect(sockaddr, + functools.partial(self._on_connect, parsed)) + + def _on_timeout(self): + self._timeout = None + self._run_callback(HTTPResponse(self.request, 599, + request_time=time.time() - self.start_time, + error=HTTPError(599, "Timeout"))) + self.stream.close() + + def _on_connect(self, parsed): + if self._timeout is not None: + self.io_loop.remove_timeout(self._timeout) + self._timeout = None + if self.request.request_timeout: + self._timeout = self.io_loop.add_timeout( + self.start_time + self.request.request_timeout, + self._on_timeout) + if (self.request.validate_cert and + isinstance(self.stream, SSLIOStream)): + match_hostname(self.stream.socket.getpeercert(), + parsed.hostname) + if (self.request.method not in self._SUPPORTED_METHODS and + not self.request.allow_nonstandard_methods): + raise KeyError("unknown method %s" % self.request.method) + for key in ('network_interface', + 'proxy_host', 'proxy_port', + 'proxy_username', 'proxy_password'): + if getattr(self.request, key, None): + raise NotImplementedError('%s not supported' % key) + if "Host" not in self.request.headers: + self.request.headers["Host"] = parsed.netloc + username, password = None, None + if parsed.username is not None: + username, password = parsed.username, parsed.password + elif self.request.auth_username is not None: + username = self.request.auth_username + password = self.request.auth_password or '' + if username is not None: + auth = utf8(username) + b(":") + utf8(password) + self.request.headers["Authorization"] = (b("Basic ") + + base64.b64encode(auth)) + if self.request.user_agent: + self.request.headers["User-Agent"] = self.request.user_agent + if not self.request.allow_nonstandard_methods: + if self.request.method in ("POST", "PUT"): + assert self.request.body is not None + else: + assert self.request.body is None + if self.request.body is not None: + self.request.headers["Content-Length"] = str(len( + self.request.body)) + if (self.request.method == "POST" and + "Content-Type" not in self.request.headers): + self.request.headers["Content-Type"] = "application/x-www-form-urlencoded" + if self.request.use_gzip: + self.request.headers["Accept-Encoding"] = "gzip" + req_path = ((parsed.path or '/') + + (('?' + parsed.query) if parsed.query else '')) + request_lines = [utf8("%s %s HTTP/1.1" % (self.request.method, + req_path))] + for k, v in self.request.headers.get_all(): + line = utf8(k) + b(": ") + utf8(v) + if b('\n') in line: + raise ValueError('Newline in header: ' + repr(line)) + request_lines.append(line) + self.stream.write(b("\r\n").join(request_lines) + b("\r\n\r\n")) + if self.request.body is not None: + self.stream.write(self.request.body) + self.stream.read_until_regex(b("\r?\n\r?\n"), self._on_headers) + + def _release(self): + if self.release_callback is not None: + release_callback = self.release_callback + self.release_callback = None + release_callback() + + def _run_callback(self, response): + self._release() + if self.final_callback is not None: + final_callback = self.final_callback + self.final_callback = None + final_callback(response) + + @contextlib.contextmanager + def cleanup(self): + try: + yield + except Exception, e: + logging.warning("uncaught exception", exc_info=True) + self._run_callback(HTTPResponse(self.request, 599, error=e, + request_time=time.time() - self.start_time, + )) + + def _on_close(self): + self._run_callback(HTTPResponse( + self.request, 599, + request_time=time.time() - self.start_time, + error=HTTPError(599, "Connection closed"))) + + def _on_headers(self, data): + data = native_str(data.decode("latin1")) + first_line, _, header_data = data.partition("\n") + match = re.match("HTTP/1.[01] ([0-9]+)", first_line) + assert match + self.code = int(match.group(1)) + self.headers = HTTPHeaders.parse(header_data) + + if "Content-Length" in self.headers: + if "," in self.headers["Content-Length"]: + # Proxies sometimes cause Content-Length headers to get + # duplicated. If all the values are identical then we can + # use them but if they differ it's an error. + pieces = re.split(r',\s*', self.headers["Content-Length"]) + if any(i != pieces[0] for i in pieces): + raise ValueError("Multiple unequal Content-Lengths: %r" % + self.headers["Content-Length"]) + self.headers["Content-Length"] = pieces[0] + content_length = int(self.headers["Content-Length"]) + else: + content_length = None + + if self.request.header_callback is not None: + for k, v in self.headers.get_all(): + self.request.header_callback("%s: %s\r\n" % (k, v)) + + if self.request.method == "HEAD": + # HEAD requests never have content, even though they may have + # content-length headers + self._on_body(b("")) + return + if 100 <= self.code < 200 or self.code in (204, 304): + # These response codes never have bodies + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3 + assert "Transfer-Encoding" not in self.headers + assert content_length in (None, 0) + self._on_body(b("")) + return + + if (self.request.use_gzip and + self.headers.get("Content-Encoding") == "gzip"): + # Magic parameter makes zlib module understand gzip header + # http://stackoverflow.com/questions/1838699/how-can-i-decompress-a-gzip-stream-with-zlib + self._decompressor = zlib.decompressobj(16+zlib.MAX_WBITS) + if self.headers.get("Transfer-Encoding") == "chunked": + self.chunks = [] + self.stream.read_until(b("\r\n"), self._on_chunk_length) + elif content_length is not None: + self.stream.read_bytes(content_length, self._on_body) + else: + self.stream.read_until_close(self._on_body) + + def _on_body(self, data): + if self._timeout is not None: + self.io_loop.remove_timeout(self._timeout) + self._timeout = None + original_request = getattr(self.request, "original_request", + self.request) + if (self.request.follow_redirects and + self.request.max_redirects > 0 and + self.code in (301, 302, 303, 307)): + new_request = copy.copy(self.request) + new_request.url = urlparse.urljoin(self.request.url, + self.headers["Location"]) + new_request.max_redirects -= 1 + del new_request.headers["Host"] + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4 + # client SHOULD make a GET request + if self.code == 303: + new_request.method = "GET" + new_request.body = None + for h in ["Content-Length", "Content-Type", + "Content-Encoding", "Transfer-Encoding"]: + try: + del self.request.headers[h] + except KeyError: + pass + new_request.original_request = original_request + final_callback = self.final_callback + self.final_callback = None + self._release() + self.client.fetch(new_request, final_callback) + self.stream.close() + return + if self._decompressor: + data = self._decompressor.decompress(data) + if self.request.streaming_callback: + if self.chunks is None: + # if chunks is not None, we already called streaming_callback + # in _on_chunk_data + self.request.streaming_callback(data) + buffer = BytesIO() + else: + buffer = BytesIO(data) # TODO: don't require one big string? + response = HTTPResponse(original_request, + self.code, headers=self.headers, + request_time=time.time() - self.start_time, + buffer=buffer, + effective_url=self.request.url) + self._run_callback(response) + self.stream.close() + + def _on_chunk_length(self, data): + # TODO: "chunk extensions" http://tools.ietf.org/html/rfc2616#section-3.6.1 + length = int(data.strip(), 16) + if length == 0: + # all the data has been decompressed, so we don't need to + # decompress again in _on_body + self._decompressor = None + self._on_body(b('').join(self.chunks)) + else: + self.stream.read_bytes(length + 2, # chunk ends with \r\n + self._on_chunk_data) + + def _on_chunk_data(self, data): + assert data[-2:] == b("\r\n") + chunk = data[:-2] + if self._decompressor: + chunk = self._decompressor.decompress(chunk) + if self.request.streaming_callback is not None: + self.request.streaming_callback(chunk) + else: + self.chunks.append(chunk) + self.stream.read_until(b("\r\n"), self._on_chunk_length) + + +# match_hostname was added to the standard library ssl module in python 3.2. +# The following code was backported for older releases and copied from +# https://bitbucket.org/brandon/backports.ssl_match_hostname +class CertificateError(ValueError): + pass + +def _dnsname_to_pat(dn): + pats = [] + for frag in dn.split(r'.'): + if frag == '*': + # When '*' is a fragment by itself, it matches a non-empty dotless + # fragment. + pats.append('[^.]+') + else: + # Otherwise, '*' matches any dotless fragment. + frag = re.escape(frag) + pats.append(frag.replace(r'\*', '[^.]*')) + return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE) + +def match_hostname(cert, hostname): + """Verify that *cert* (in decoded format as returned by + SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 rules + are mostly followed, but IP addresses are not accepted for *hostname*. + + CertificateError is raised on failure. On success, the function + returns nothing. + """ + if not cert: + raise ValueError("empty or no certificate") + dnsnames = [] + san = cert.get('subjectAltName', ()) + for key, value in san: + if key == 'DNS': + if _dnsname_to_pat(value).match(hostname): + return + dnsnames.append(value) + if not san: + # The subject is only checked when subjectAltName is empty + for sub in cert.get('subject', ()): + for key, value in sub: + # XXX according to RFC 2818, the most specific Common Name + # must be used. + if key == 'commonName': + if _dnsname_to_pat(value).match(hostname): + return + dnsnames.append(value) + if len(dnsnames) > 1: + raise CertificateError("hostname %r " + "doesn't match either of %s" + % (hostname, ', '.join(map(repr, dnsnames)))) + elif len(dnsnames) == 1: + raise CertificateError("hostname %r " + "doesn't match %r" + % (hostname, dnsnames[0])) + else: + raise CertificateError("no appropriate commonName or " + "subjectAltName fields were found") + +if __name__ == "__main__": + AsyncHTTPClient.configure(SimpleAsyncHTTPClient) + main() diff --git a/libs/tornado/stack_context.py b/libs/tornado/stack_context.py new file mode 100644 index 00000000..1ba3730c --- /dev/null +++ b/libs/tornado/stack_context.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python +# +# Copyright 2010 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +'''StackContext allows applications to maintain threadlocal-like state +that follows execution as it moves to other execution contexts. + +The motivating examples are to eliminate the need for explicit +async_callback wrappers (as in tornado.web.RequestHandler), and to +allow some additional context to be kept for logging. + +This is slightly magic, but it's an extension of the idea that an exception +handler is a kind of stack-local state and when that stack is suspended +and resumed in a new context that state needs to be preserved. StackContext +shifts the burden of restoring that state from each call site (e.g. +wrapping each AsyncHTTPClient callback in async_callback) to the mechanisms +that transfer control from one context to another (e.g. AsyncHTTPClient +itself, IOLoop, thread pools, etc). + +Example usage:: + + @contextlib.contextmanager + def die_on_error(): + try: + yield + except Exception: + logging.error("exception in asynchronous operation",exc_info=True) + sys.exit(1) + + with StackContext(die_on_error): + # Any exception thrown here *or in callback and its desendents* + # will cause the process to exit instead of spinning endlessly + # in the ioloop. + http_client.fetch(url, callback) + ioloop.start() + +Most applications shouln't have to work with `StackContext` directly. +Here are a few rules of thumb for when it's necessary: + +* If you're writing an asynchronous library that doesn't rely on a + stack_context-aware library like `tornado.ioloop` or `tornado.iostream` + (for example, if you're writing a thread pool), use + `stack_context.wrap()` before any asynchronous operations to capture the + stack context from where the operation was started. + +* If you're writing an asynchronous library that has some shared + resources (such as a connection pool), create those shared resources + within a ``with stack_context.NullContext():`` block. This will prevent + ``StackContexts`` from leaking from one request to another. + +* If you want to write something like an exception handler that will + persist across asynchronous calls, create a new `StackContext` (or + `ExceptionStackContext`), and make your asynchronous calls in a ``with`` + block that references your `StackContext`. +''' + +from __future__ import with_statement + +import contextlib +import functools +import itertools +import sys +import threading + +class _State(threading.local): + def __init__(self): + self.contexts = () +_state = _State() + +class StackContext(object): + '''Establishes the given context as a StackContext that will be transferred. + + Note that the parameter is a callable that returns a context + manager, not the context itself. That is, where for a + non-transferable context manager you would say:: + + with my_context(): + + StackContext takes the function itself rather than its result:: + + with StackContext(my_context): + ''' + def __init__(self, context_factory): + self.context_factory = context_factory + + # Note that some of this code is duplicated in ExceptionStackContext + # below. ExceptionStackContext is more common and doesn't need + # the full generality of this class. + def __enter__(self): + self.old_contexts = _state.contexts + # _state.contexts is a tuple of (class, arg) pairs + _state.contexts = (self.old_contexts + + ((StackContext, self.context_factory),)) + try: + self.context = self.context_factory() + self.context.__enter__() + except Exception: + _state.contexts = self.old_contexts + raise + + def __exit__(self, type, value, traceback): + try: + return self.context.__exit__(type, value, traceback) + finally: + _state.contexts = self.old_contexts + +class ExceptionStackContext(object): + '''Specialization of StackContext for exception handling. + + The supplied exception_handler function will be called in the + event of an uncaught exception in this context. The semantics are + similar to a try/finally clause, and intended use cases are to log + an error, close a socket, or similar cleanup actions. The + exc_info triple (type, value, traceback) will be passed to the + exception_handler function. + + If the exception handler returns true, the exception will be + consumed and will not be propagated to other exception handlers. + ''' + def __init__(self, exception_handler): + self.exception_handler = exception_handler + + def __enter__(self): + self.old_contexts = _state.contexts + _state.contexts = (self.old_contexts + + ((ExceptionStackContext, self.exception_handler),)) + + def __exit__(self, type, value, traceback): + try: + if type is not None: + return self.exception_handler(type, value, traceback) + finally: + _state.contexts = self.old_contexts + +class NullContext(object): + '''Resets the StackContext. + + Useful when creating a shared resource on demand (e.g. an AsyncHTTPClient) + where the stack that caused the creating is not relevant to future + operations. + ''' + def __enter__(self): + self.old_contexts = _state.contexts + _state.contexts = () + + def __exit__(self, type, value, traceback): + _state.contexts = self.old_contexts + +class _StackContextWrapper(functools.partial): + pass + +def wrap(fn): + '''Returns a callable object that will restore the current StackContext + when executed. + + Use this whenever saving a callback to be executed later in a + different execution context (either in a different thread or + asynchronously in the same thread). + ''' + if fn is None or fn.__class__ is _StackContextWrapper: + return fn + # functools.wraps doesn't appear to work on functools.partial objects + #@functools.wraps(fn) + def wrapped(callback, contexts, *args, **kwargs): + if contexts is _state.contexts or not contexts: + callback(*args, **kwargs) + return + if not _state.contexts: + new_contexts = [cls(arg) for (cls, arg) in contexts] + # If we're moving down the stack, _state.contexts is a prefix + # of contexts. For each element of contexts not in that prefix, + # create a new StackContext object. + # If we're moving up the stack (or to an entirely different stack), + # _state.contexts will have elements not in contexts. Use + # NullContext to clear the state and then recreate from contexts. + elif (len(_state.contexts) > len(contexts) or + any(a[1] is not b[1] + for a, b in itertools.izip(_state.contexts, contexts))): + # contexts have been removed or changed, so start over + new_contexts = ([NullContext()] + + [cls(arg) for (cls,arg) in contexts]) + else: + new_contexts = [cls(arg) + for (cls, arg) in contexts[len(_state.contexts):]] + if len(new_contexts) > 1: + with _nested(*new_contexts): + callback(*args, **kwargs) + elif new_contexts: + with new_contexts[0]: + callback(*args, **kwargs) + else: + callback(*args, **kwargs) + if _state.contexts: + return _StackContextWrapper(wrapped, fn, _state.contexts) + else: + return _StackContextWrapper(fn) + +@contextlib.contextmanager +def _nested(*managers): + """Support multiple context managers in a single with-statement. + + Copied from the python 2.6 standard library. It's no longer present + in python 3 because the with statement natively supports multiple + context managers, but that doesn't help if the list of context + managers is not known until runtime. + """ + exits = [] + vars = [] + exc = (None, None, None) + try: + for mgr in managers: + exit = mgr.__exit__ + enter = mgr.__enter__ + vars.append(enter()) + exits.append(exit) + yield vars + except: + exc = sys.exc_info() + finally: + while exits: + exit = exits.pop() + try: + if exit(*exc): + exc = (None, None, None) + except: + exc = sys.exc_info() + if exc != (None, None, None): + # Don't rely on sys.exc_info() still containing + # the right information. Another exception may + # have been raised and caught by an exit method + raise exc[0], exc[1], exc[2] + diff --git a/libs/tornado/template.py b/libs/tornado/template.py new file mode 100644 index 00000000..139667dc --- /dev/null +++ b/libs/tornado/template.py @@ -0,0 +1,826 @@ +#!/usr/bin/env python +# +# Copyright 2009 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""A simple template system that compiles templates to Python code. + +Basic usage looks like:: + + t = template.Template("{{ myvalue }}") + print t.generate(myvalue="XXX") + +Loader is a class that loads templates from a root directory and caches +the compiled templates:: + + loader = template.Loader("/home/btaylor") + print loader.load("test.html").generate(myvalue="XXX") + +We compile all templates to raw Python. Error-reporting is currently... uh, +interesting. Syntax for the templates:: + + ### base.html + + + {% block title %}Default title{% end %} + + +
    + {% for student in students %} + {% block student %} +
  • {{ escape(student.name) }}
  • + {% end %} + {% end %} +
+ + + + ### bold.html + {% extends "base.html" %} + + {% block title %}A bolder title{% end %} + + {% block student %} +
  • {{ escape(student.name) }}
  • + {% end %} + +Unlike most other template systems, we do not put any restrictions on the +expressions you can include in your statements. if and for blocks get +translated exactly into Python, you can do complex expressions like:: + + {% for student in [p for p in people if p.student and p.age > 23] %} +
  • {{ escape(student.name) }}
  • + {% end %} + +Translating directly to Python means you can apply functions to expressions +easily, like the escape() function in the examples above. You can pass +functions in to your template just like any other variable:: + + ### Python code + def add(x, y): + return x + y + template.execute(add=add) + + ### The template + {{ add(1, 2) }} + +We provide the functions escape(), url_escape(), json_encode(), and squeeze() +to all templates by default. + +Typical applications do not create `Template` or `Loader` instances by +hand, but instead use the `render` and `render_string` methods of +`tornado.web.RequestHandler`, which load templates automatically based +on the ``template_path`` `Application` setting. + +Syntax Reference +---------------- + +Template expressions are surrounded by double curly braces: ``{{ ... }}``. +The contents may be any python expression, which will be escaped according +to the current autoescape setting and inserted into the output. Other +template directives use ``{% %}``. These tags may be escaped as ``{{!`` +and ``{%!`` if you need to include a literal ``{{`` or ``{%`` in the output. + +To comment out a section so that it is omitted from the output, surround it +with ``{# ... #}``. + +``{% apply *function* %}...{% end %}`` + Applies a function to the output of all template code between ``apply`` + and ``end``:: + + {% apply linkify %}{{name}} said: {{message}}{% end %} + +``{% autoescape *function* %}`` + Sets the autoescape mode for the current file. This does not affect + other files, even those referenced by ``{% include %}``. Note that + autoescaping can also be configured globally, at the `Application` + or `Loader`.:: + + {% autoescape xhtml_escape %} + {% autoescape None %} + +``{% block *name* %}...{% end %}`` + Indicates a named, replaceable block for use with ``{% extends %}``. + Blocks in the parent template will be replaced with the contents of + the same-named block in a child template.:: + + + {% block title %}Default title{% end %} + + + {% extends "base.html" %} + {% block title %}My page title{% end %} + +``{% comment ... %}`` + A comment which will be removed from the template output. Note that + there is no ``{% end %}`` tag; the comment goes from the word ``comment`` + to the closing ``%}`` tag. + +``{% extends *filename* %}`` + Inherit from another template. Templates that use ``extends`` should + contain one or more ``block`` tags to replace content from the parent + template. Anything in the child template not contained in a ``block`` + tag will be ignored. For an example, see the ``{% block %}`` tag. + +``{% for *var* in *expr* %}...{% end %}`` + Same as the python ``for`` statement. + +``{% from *x* import *y* %}`` + Same as the python ``import`` statement. + +``{% if *condition* %}...{% elif *condition* %}...{% else %}...{% end %}`` + Conditional statement - outputs the first section whose condition is + true. (The ``elif`` and ``else`` sections are optional) + +``{% import *module* %}`` + Same as the python ``import`` statement. + +``{% include *filename* %}`` + Includes another template file. The included file can see all the local + variables as if it were copied directly to the point of the ``include`` + directive (the ``{% autoescape %}`` directive is an exception). + Alternately, ``{% module Template(filename, **kwargs) %}`` may be used + to include another template with an isolated namespace. + +``{% module *expr* %}`` + Renders a `~tornado.web.UIModule`. The output of the ``UIModule`` is + not escaped:: + + {% module Template("foo.html", arg=42) %} + +``{% raw *expr* %}`` + Outputs the result of the given expression without autoescaping. + +``{% set *x* = *y* %}`` + Sets a local variable. + +``{% try %}...{% except %}...{% finally %}...{% end %}`` + Same as the python ``try`` statement. + +``{% while *condition* %}... {% end %}`` + Same as the python ``while`` statement. +""" + +from __future__ import with_statement + +import cStringIO +import datetime +import linecache +import logging +import os.path +import posixpath +import re +import threading + +from tornado import escape +from tornado.util import bytes_type, ObjectDict + +_DEFAULT_AUTOESCAPE = "xhtml_escape" +_UNSET = object() + +class Template(object): + """A compiled template. + + We compile into Python from the given template_string. You can generate + the template from variables with generate(). + """ + def __init__(self, template_string, name="", loader=None, + compress_whitespace=None, autoescape=_UNSET): + self.name = name + if compress_whitespace is None: + compress_whitespace = name.endswith(".html") or \ + name.endswith(".js") + if autoescape is not _UNSET: + self.autoescape = autoescape + elif loader: + self.autoescape = loader.autoescape + else: + self.autoescape = _DEFAULT_AUTOESCAPE + self.namespace = loader.namespace if loader else {} + reader = _TemplateReader(name, escape.native_str(template_string)) + self.file = _File(self, _parse(reader, self)) + self.code = self._generate_python(loader, compress_whitespace) + self.loader = loader + try: + # Under python2.5, the fake filename used here must match + # the module name used in __name__ below. + self.compiled = compile( + escape.to_unicode(self.code), + "%s.generated.py" % self.name.replace('.','_'), + "exec") + except Exception: + formatted_code = _format_code(self.code).rstrip() + logging.error("%s code:\n%s", self.name, formatted_code) + raise + + def generate(self, **kwargs): + """Generate this template with the given arguments.""" + namespace = { + "escape": escape.xhtml_escape, + "xhtml_escape": escape.xhtml_escape, + "url_escape": escape.url_escape, + "json_encode": escape.json_encode, + "squeeze": escape.squeeze, + "linkify": escape.linkify, + "datetime": datetime, + "_utf8": escape.utf8, # for internal use + "_string_types": (unicode, bytes_type), + # __name__ and __loader__ allow the traceback mechanism to find + # the generated source code. + "__name__": self.name.replace('.', '_'), + "__loader__": ObjectDict(get_source=lambda name: self.code), + } + namespace.update(self.namespace) + namespace.update(kwargs) + exec self.compiled in namespace + execute = namespace["_execute"] + # Clear the traceback module's cache of source data now that + # we've generated a new template (mainly for this module's + # unittests, where different tests reuse the same name). + linecache.clearcache() + try: + return execute() + except Exception: + formatted_code = _format_code(self.code).rstrip() + logging.error("%s code:\n%s", self.name, formatted_code) + raise + + def _generate_python(self, loader, compress_whitespace): + buffer = cStringIO.StringIO() + try: + # named_blocks maps from names to _NamedBlock objects + named_blocks = {} + ancestors = self._get_ancestors(loader) + ancestors.reverse() + for ancestor in ancestors: + ancestor.find_named_blocks(loader, named_blocks) + self.file.find_named_blocks(loader, named_blocks) + writer = _CodeWriter(buffer, named_blocks, loader, ancestors[0].template, + compress_whitespace) + ancestors[0].generate(writer) + return buffer.getvalue() + finally: + buffer.close() + + def _get_ancestors(self, loader): + ancestors = [self.file] + for chunk in self.file.body.chunks: + if isinstance(chunk, _ExtendsBlock): + if not loader: + raise ParseError("{% extends %} block found, but no " + "template loader") + template = loader.load(chunk.name, self.name) + ancestors.extend(template._get_ancestors(loader)) + return ancestors + + +class BaseLoader(object): + """Base class for template loaders.""" + def __init__(self, autoescape=_DEFAULT_AUTOESCAPE, namespace=None): + """Creates a template loader. + + root_directory may be the empty string if this loader does not + use the filesystem. + + autoescape must be either None or a string naming a function + in the template namespace, such as "xhtml_escape". + """ + self.autoescape = autoescape + self.namespace = namespace or {} + self.templates = {} + # self.lock protects self.templates. It's a reentrant lock + # because templates may load other templates via `include` or + # `extends`. Note that thanks to the GIL this code would be safe + # even without the lock, but could lead to wasted work as multiple + # threads tried to compile the same template simultaneously. + self.lock = threading.RLock() + + def reset(self): + """Resets the cache of compiled templates.""" + with self.lock: + self.templates = {} + + def resolve_path(self, name, parent_path=None): + """Converts a possibly-relative path to absolute (used internally).""" + raise NotImplementedError() + + def load(self, name, parent_path=None): + """Loads a template.""" + name = self.resolve_path(name, parent_path=parent_path) + with self.lock: + if name not in self.templates: + self.templates[name] = self._create_template(name) + return self.templates[name] + + def _create_template(self, name): + raise NotImplementedError() + +class Loader(BaseLoader): + """A template loader that loads from a single root directory. + + You must use a template loader to use template constructs like + {% extends %} and {% include %}. Loader caches all templates after + they are loaded the first time. + """ + def __init__(self, root_directory, **kwargs): + super(Loader, self).__init__(**kwargs) + self.root = os.path.abspath(root_directory) + + def resolve_path(self, name, parent_path=None): + if parent_path and not parent_path.startswith("<") and \ + not parent_path.startswith("/") and \ + not name.startswith("/"): + current_path = os.path.join(self.root, parent_path) + file_dir = os.path.dirname(os.path.abspath(current_path)) + relative_path = os.path.abspath(os.path.join(file_dir, name)) + if relative_path.startswith(self.root): + name = relative_path[len(self.root) + 1:] + return name + + def _create_template(self, name): + path = os.path.join(self.root, name) + f = open(path, "r") + template = Template(f.read(), name=name, loader=self) + f.close() + return template + + +class DictLoader(BaseLoader): + """A template loader that loads from a dictionary.""" + def __init__(self, dict, **kwargs): + super(DictLoader, self).__init__(**kwargs) + self.dict = dict + + def resolve_path(self, name, parent_path=None): + if parent_path and not parent_path.startswith("<") and \ + not parent_path.startswith("/") and \ + not name.startswith("/"): + file_dir = posixpath.dirname(parent_path) + name = posixpath.normpath(posixpath.join(file_dir, name)) + return name + + def _create_template(self, name): + return Template(self.dict[name], name=name, loader=self) + + +class _Node(object): + def each_child(self): + return () + + def generate(self, writer): + raise NotImplementedError() + + def find_named_blocks(self, loader, named_blocks): + for child in self.each_child(): + child.find_named_blocks(loader, named_blocks) + + +class _File(_Node): + def __init__(self, template, body): + self.template = template + self.body = body + self.line = 0 + + def generate(self, writer): + writer.write_line("def _execute():", self.line) + with writer.indent(): + writer.write_line("_buffer = []", self.line) + writer.write_line("_append = _buffer.append", self.line) + self.body.generate(writer) + writer.write_line("return _utf8('').join(_buffer)", self.line) + + def each_child(self): + return (self.body,) + + + +class _ChunkList(_Node): + def __init__(self, chunks): + self.chunks = chunks + + def generate(self, writer): + for chunk in self.chunks: + chunk.generate(writer) + + def each_child(self): + return self.chunks + + +class _NamedBlock(_Node): + def __init__(self, name, body, template, line): + self.name = name + self.body = body + self.template = template + self.line = line + + def each_child(self): + return (self.body,) + + def generate(self, writer): + block = writer.named_blocks[self.name] + with writer.include(block.template, self.line): + block.body.generate(writer) + + def find_named_blocks(self, loader, named_blocks): + named_blocks[self.name] = self + _Node.find_named_blocks(self, loader, named_blocks) + + +class _ExtendsBlock(_Node): + def __init__(self, name): + self.name = name + + +class _IncludeBlock(_Node): + def __init__(self, name, reader, line): + self.name = name + self.template_name = reader.name + self.line = line + + def find_named_blocks(self, loader, named_blocks): + included = loader.load(self.name, self.template_name) + included.file.find_named_blocks(loader, named_blocks) + + def generate(self, writer): + included = writer.loader.load(self.name, self.template_name) + with writer.include(included, self.line): + included.file.body.generate(writer) + + +class _ApplyBlock(_Node): + def __init__(self, method, line, body=None): + self.method = method + self.line = line + self.body = body + + def each_child(self): + return (self.body,) + + def generate(self, writer): + method_name = "apply%d" % writer.apply_counter + writer.apply_counter += 1 + writer.write_line("def %s():" % method_name, self.line) + with writer.indent(): + writer.write_line("_buffer = []", self.line) + writer.write_line("_append = _buffer.append", self.line) + self.body.generate(writer) + writer.write_line("return _utf8('').join(_buffer)", self.line) + writer.write_line("_append(%s(%s()))" % ( + self.method, method_name), self.line) + + +class _ControlBlock(_Node): + def __init__(self, statement, line, body=None): + self.statement = statement + self.line = line + self.body = body + + def each_child(self): + return (self.body,) + + def generate(self, writer): + writer.write_line("%s:" % self.statement, self.line) + with writer.indent(): + self.body.generate(writer) + + +class _IntermediateControlBlock(_Node): + def __init__(self, statement, line): + self.statement = statement + self.line = line + + def generate(self, writer): + writer.write_line("%s:" % self.statement, self.line, writer.indent_size() - 1) + + +class _Statement(_Node): + def __init__(self, statement, line): + self.statement = statement + self.line = line + + def generate(self, writer): + writer.write_line(self.statement, self.line) + + +class _Expression(_Node): + def __init__(self, expression, line, raw=False): + self.expression = expression + self.line = line + self.raw = raw + + def generate(self, writer): + writer.write_line("_tmp = %s" % self.expression, self.line) + writer.write_line("if isinstance(_tmp, _string_types):" + " _tmp = _utf8(_tmp)", self.line) + writer.write_line("else: _tmp = _utf8(str(_tmp))", self.line) + if not self.raw and writer.current_template.autoescape is not None: + # In python3 functions like xhtml_escape return unicode, + # so we have to convert to utf8 again. + writer.write_line("_tmp = _utf8(%s(_tmp))" % + writer.current_template.autoescape, self.line) + writer.write_line("_append(_tmp)", self.line) + +class _Module(_Expression): + def __init__(self, expression, line): + super(_Module, self).__init__("_modules." + expression, line, + raw=True) + +class _Text(_Node): + def __init__(self, value, line): + self.value = value + self.line = line + + def generate(self, writer): + value = self.value + + # Compress lots of white space to a single character. If the whitespace + # breaks a line, have it continue to break a line, but just with a + # single \n character + if writer.compress_whitespace and "
    " not in value:
    +            value = re.sub(r"([\t ]+)", " ", value)
    +            value = re.sub(r"(\s*\n\s*)", "\n", value)
    +
    +        if value:
    +            writer.write_line('_append(%r)' % escape.utf8(value), self.line)
    +
    +
    +class ParseError(Exception):
    +    """Raised for template syntax errors."""
    +    pass
    +
    +
    +class _CodeWriter(object):
    +    def __init__(self, file, named_blocks, loader, current_template,
    +                 compress_whitespace):
    +        self.file = file
    +        self.named_blocks = named_blocks
    +        self.loader = loader
    +        self.current_template = current_template
    +        self.compress_whitespace = compress_whitespace
    +        self.apply_counter = 0
    +        self.include_stack = []
    +        self._indent = 0
    +
    +    def indent_size(self):
    +        return self._indent
    +
    +    def indent(self):
    +        class Indenter(object):
    +            def __enter__(_):
    +                self._indent += 1
    +                return self
    +
    +            def __exit__(_, *args):
    +                assert self._indent > 0
    +                self._indent -= 1
    +
    +        return Indenter()
    +
    +    def include(self, template, line):
    +        self.include_stack.append((self.current_template, line))
    +        self.current_template = template
    +
    +        class IncludeTemplate(object):
    +            def __enter__(_):
    +                return self
    +
    +            def __exit__(_, *args):
    +                self.current_template = self.include_stack.pop()[0]
    +
    +        return IncludeTemplate()
    +
    +    def write_line(self, line, line_number, indent=None):
    +        if indent == None:
    +            indent = self._indent
    +        line_comment = '  # %s:%d' % (self.current_template.name, line_number)
    +        if self.include_stack:
    +            ancestors = ["%s:%d" % (tmpl.name, lineno)
    +                         for (tmpl, lineno) in self.include_stack]
    +            line_comment += ' (via %s)' % ', '.join(reversed(ancestors))
    +        print >> self.file, "    "*indent + line + line_comment
    +
    +
    +class _TemplateReader(object):
    +    def __init__(self, name, text):
    +        self.name = name
    +        self.text = text
    +        self.line = 1
    +        self.pos = 0
    +
    +    def find(self, needle, start=0, end=None):
    +        assert start >= 0, start
    +        pos = self.pos
    +        start += pos
    +        if end is None:
    +            index = self.text.find(needle, start)
    +        else:
    +            end += pos
    +            assert end >= start
    +            index = self.text.find(needle, start, end)
    +        if index != -1:
    +            index -= pos
    +        return index
    +
    +    def consume(self, count=None):
    +        if count is None:
    +            count = len(self.text) - self.pos
    +        newpos = self.pos + count
    +        self.line += self.text.count("\n", self.pos, newpos)
    +        s = self.text[self.pos:newpos]
    +        self.pos = newpos
    +        return s
    +
    +    def remaining(self):
    +        return len(self.text) - self.pos
    +
    +    def __len__(self):
    +        return self.remaining()
    +
    +    def __getitem__(self, key):
    +        if type(key) is slice:
    +            size = len(self)
    +            start, stop, step = key.indices(size)
    +            if start is None: start = self.pos
    +            else: start += self.pos
    +            if stop is not None: stop += self.pos
    +            return self.text[slice(start, stop, step)]
    +        elif key < 0:
    +            return self.text[key]
    +        else:
    +            return self.text[self.pos + key]
    +
    +    def __str__(self):
    +        return self.text[self.pos:]
    +
    +
    +def _format_code(code):
    +    lines = code.splitlines()
    +    format = "%%%dd  %%s\n" % len(repr(len(lines) + 1))
    +    return "".join([format % (i + 1, line) for (i, line) in enumerate(lines)])
    +
    +
    +def _parse(reader, template, in_block=None):
    +    body = _ChunkList([])
    +    while True:
    +        # Find next template directive
    +        curly = 0
    +        while True:
    +            curly = reader.find("{", curly)
    +            if curly == -1 or curly + 1 == reader.remaining():
    +                # EOF
    +                if in_block:
    +                    raise ParseError("Missing {%% end %%} block for %s" %
    +                                     in_block)
    +                body.chunks.append(_Text(reader.consume(), reader.line))
    +                return body
    +            # If the first curly brace is not the start of a special token,
    +            # start searching from the character after it
    +            if reader[curly + 1] not in ("{", "%", "#"):
    +                curly += 1
    +                continue
    +            # When there are more than 2 curlies in a row, use the
    +            # innermost ones.  This is useful when generating languages
    +            # like latex where curlies are also meaningful
    +            if (curly + 2 < reader.remaining() and
    +                reader[curly + 1] == '{' and reader[curly + 2] == '{'):
    +                curly += 1
    +                continue
    +            break
    +
    +        # Append any text before the special token
    +        if curly > 0:
    +            cons = reader.consume(curly)
    +            body.chunks.append(_Text(cons, reader.line))
    +
    +        start_brace = reader.consume(2)
    +        line = reader.line
    +
    +        # Template directives may be escaped as "{{!" or "{%!".
    +        # In this case output the braces and consume the "!".
    +        # This is especially useful in conjunction with jquery templates,
    +        # which also use double braces.
    +        if reader.remaining() and reader[0] == "!":
    +            reader.consume(1)
    +            body.chunks.append(_Text(start_brace, line))
    +            continue
    +
    +        # Comment
    +        if start_brace == "{#":
    +            end = reader.find("#}")
    +            if end == -1:
    +                raise ParseError("Missing end expression #} on line %d" % line)
    +            contents = reader.consume(end).strip()
    +            reader.consume(2)
    +            continue
    +
    +        # Expression
    +        if start_brace == "{{":
    +            end = reader.find("}}")
    +            if end == -1:
    +                raise ParseError("Missing end expression }} on line %d" % line)
    +            contents = reader.consume(end).strip()
    +            reader.consume(2)
    +            if not contents:
    +                raise ParseError("Empty expression on line %d" % line)
    +            body.chunks.append(_Expression(contents, line))
    +            continue
    +
    +        # Block
    +        assert start_brace == "{%", start_brace
    +        end = reader.find("%}")
    +        if end == -1:
    +            raise ParseError("Missing end block %%} on line %d" % line)
    +        contents = reader.consume(end).strip()
    +        reader.consume(2)
    +        if not contents:
    +            raise ParseError("Empty block tag ({%% %%}) on line %d" % line)
    +
    +        operator, space, suffix = contents.partition(" ")
    +        suffix = suffix.strip()
    +
    +        # Intermediate ("else", "elif", etc) blocks
    +        intermediate_blocks = {
    +            "else": set(["if", "for", "while"]),
    +            "elif": set(["if"]),
    +            "except": set(["try"]),
    +            "finally": set(["try"]),
    +        }
    +        allowed_parents = intermediate_blocks.get(operator)
    +        if allowed_parents is not None:
    +            if not in_block:
    +                raise ParseError("%s outside %s block" %
    +                            (operator, allowed_parents))
    +            if in_block not in allowed_parents:
    +                raise ParseError("%s block cannot be attached to %s block" % (operator, in_block))
    +            body.chunks.append(_IntermediateControlBlock(contents, line))
    +            continue
    +
    +        # End tag
    +        elif operator == "end":
    +            if not in_block:
    +                raise ParseError("Extra {%% end %%} block on line %d" % line)
    +            return body
    +
    +        elif operator in ("extends", "include", "set", "import", "from",
    +                          "comment", "autoescape", "raw", "module"):
    +            if operator == "comment":
    +                continue
    +            if operator == "extends":
    +                suffix = suffix.strip('"').strip("'")
    +                if not suffix:
    +                    raise ParseError("extends missing file path on line %d" % line)
    +                block = _ExtendsBlock(suffix)
    +            elif operator in ("import", "from"):
    +                if not suffix:
    +                    raise ParseError("import missing statement on line %d" % line)
    +                block = _Statement(contents, line)
    +            elif operator == "include":
    +                suffix = suffix.strip('"').strip("'")
    +                if not suffix:
    +                    raise ParseError("include missing file path on line %d" % line)
    +                block = _IncludeBlock(suffix, reader, line)
    +            elif operator == "set":
    +                if not suffix:
    +                    raise ParseError("set missing statement on line %d" % line)
    +                block = _Statement(suffix, line)
    +            elif operator == "autoescape":
    +                fn = suffix.strip()
    +                if fn == "None": fn = None
    +                template.autoescape = fn
    +                continue
    +            elif operator == "raw":
    +                block = _Expression(suffix, line, raw=True)
    +            elif operator == "module":
    +                block = _Module(suffix, line)
    +            body.chunks.append(block)
    +            continue
    +
    +        elif operator in ("apply", "block", "try", "if", "for", "while"):
    +            # parse inner body recursively
    +            block_body = _parse(reader, template, operator)
    +            if operator == "apply":
    +                if not suffix:
    +                    raise ParseError("apply missing method name on line %d" % line)
    +                block = _ApplyBlock(suffix, line, block_body)
    +            elif operator == "block":
    +                if not suffix:
    +                    raise ParseError("block missing name on line %d" % line)
    +                block = _NamedBlock(suffix, block_body, template, line)
    +            else:
    +                block = _ControlBlock(contents, line, block_body)
    +            body.chunks.append(block)
    +            continue
    +
    +        else:
    +            raise ParseError("unknown operator: %r" % operator)
    diff --git a/libs/tornado/testing.py b/libs/tornado/testing.py
    new file mode 100644
    index 00000000..b2b983dd
    --- /dev/null
    +++ b/libs/tornado/testing.py
    @@ -0,0 +1,382 @@
    +#!/usr/bin/env python
    +"""Support classes for automated testing.
    +
    +This module contains three parts:
    +
    +* `AsyncTestCase`/`AsyncHTTPTestCase`:  Subclasses of unittest.TestCase
    +  with additional support for testing asynchronous (IOLoop-based) code.
    +
    +* `LogTrapTestCase`:  Subclass of unittest.TestCase that discards log output
    +  from tests that pass and only produces output for failing tests.
    +
    +* `main()`: A simple test runner (wrapper around unittest.main()) with support
    +  for the tornado.autoreload module to rerun the tests when code changes.
    +
    +These components may be used together or independently.  In particular,
    +it is safe to combine AsyncTestCase and LogTrapTestCase via multiple
    +inheritance.  See the docstrings for each class/function below for more
    +information.
    +"""
    +
    +from __future__ import with_statement
    +
    +from cStringIO import StringIO
    +try:
    +    from tornado.httpclient import AsyncHTTPClient
    +    from tornado.httpserver import HTTPServer
    +    from tornado.ioloop import IOLoop
    +except ImportError:
    +    # These modules are not importable on app engine.  Parts of this module
    +    # won't work, but e.g. LogTrapTestCase and main() will.
    +    AsyncHTTPClient = None
    +    HTTPServer = None
    +    IOLoop = None
    +from tornado.stack_context import StackContext, NullContext
    +import contextlib
    +import logging
    +import signal
    +import sys
    +import time
    +import unittest
    +
    +_next_port = 10000
    +def get_unused_port():
    +    """Returns a (hopefully) unused port number."""
    +    global _next_port
    +    port = _next_port
    +    _next_port = _next_port + 1
    +    return port
    +
    +class AsyncTestCase(unittest.TestCase):
    +    """TestCase subclass for testing IOLoop-based asynchronous code.
    +
    +    The unittest framework is synchronous, so the test must be complete
    +    by the time the test method returns.  This method provides the stop()
    +    and wait() methods for this purpose.  The test method itself must call
    +    self.wait(), and asynchronous callbacks should call self.stop() to signal
    +    completion.
    +
    +    By default, a new IOLoop is constructed for each test and is available
    +    as self.io_loop.  This IOLoop should be used in the construction of
    +    HTTP clients/servers, etc.  If the code being tested requires a
    +    global IOLoop, subclasses should override get_new_ioloop to return it.
    +
    +    The IOLoop's start and stop methods should not be called directly.
    +    Instead, use self.stop self.wait.  Arguments passed to self.stop are
    +    returned from self.wait.  It is possible to have multiple
    +    wait/stop cycles in the same test.
    +
    +    Example::
    +
    +        # This test uses an asynchronous style similar to most async
    +        # application code.
    +        class MyTestCase(AsyncTestCase):
    +            def test_http_fetch(self):
    +                client = AsyncHTTPClient(self.io_loop)
    +                client.fetch("http://www.tornadoweb.org/", self.handle_fetch)
    +                self.wait()
    +
    +            def handle_fetch(self, response):
    +                # Test contents of response (failures and exceptions here
    +                # will cause self.wait() to throw an exception and end the
    +                # test).
    +                # Exceptions thrown here are magically propagated to
    +                # self.wait() in test_http_fetch() via stack_context.
    +                self.assertIn("FriendFeed", response.body)
    +                self.stop()
    +
    +        # This test uses the argument passing between self.stop and self.wait
    +        # for a simpler, more synchronous style.
    +        # This style is recommended over the preceding example because it
    +        # keeps the assertions in the test method itself, and is therefore
    +        # less sensitive to the subtleties of stack_context.
    +        class MyTestCase2(AsyncTestCase):
    +            def test_http_fetch(self):
    +                client = AsyncHTTPClient(self.io_loop)
    +                client.fetch("http://www.tornadoweb.org/", self.stop)
    +                response = self.wait()
    +                # Test contents of response
    +                self.assertIn("FriendFeed", response.body)
    +    """
    +    def __init__(self, *args, **kwargs):
    +        super(AsyncTestCase, self).__init__(*args, **kwargs)
    +        self.__stopped = False
    +        self.__running = False
    +        self.__failure = None
    +        self.__stop_args = None
    +
    +    def setUp(self):
    +        super(AsyncTestCase, self).setUp()
    +        self.io_loop = self.get_new_ioloop()
    +
    +    def tearDown(self):
    +        if (not IOLoop.initialized() or
    +            self.io_loop is not IOLoop.instance()):
    +            # Try to clean up any file descriptors left open in the ioloop.
    +            # This avoids leaks, especially when tests are run repeatedly
    +            # in the same process with autoreload (because curl does not
    +            # set FD_CLOEXEC on its file descriptors)
    +            self.io_loop.close(all_fds=True)
    +        super(AsyncTestCase, self).tearDown()
    +
    +    def get_new_ioloop(self):
    +        '''Creates a new IOLoop for this test.  May be overridden in
    +        subclasses for tests that require a specific IOLoop (usually
    +        the singleton).
    +        '''
    +        return IOLoop()
    +
    +    @contextlib.contextmanager
    +    def _stack_context(self):
    +        try:
    +            yield
    +        except Exception:
    +            self.__failure = sys.exc_info()
    +            self.stop()
    +
    +    def run(self, result=None):
    +        with StackContext(self._stack_context):
    +            super(AsyncTestCase, self).run(result)
    +
    +    def stop(self, _arg=None, **kwargs):
    +        '''Stops the ioloop, causing one pending (or future) call to wait()
    +        to return.
    +
    +        Keyword arguments or a single positional argument passed to stop() are
    +        saved and will be returned by wait().
    +        '''
    +        assert _arg is None or not kwargs
    +        self.__stop_args = kwargs or _arg
    +        if self.__running:
    +            self.io_loop.stop()
    +            self.__running = False
    +        self.__stopped = True
    +
    +    def wait(self, condition=None, timeout=5):
    +        """Runs the IOLoop until stop is called or timeout has passed.
    +
    +        In the event of a timeout, an exception will be thrown.
    +
    +        If condition is not None, the IOLoop will be restarted after stop()
    +        until condition() returns true.
    +        """
    +        if not self.__stopped:
    +            if timeout:
    +                def timeout_func():
    +                    try:
    +                        raise self.failureException(
    +                          'Async operation timed out after %d seconds' %
    +                          timeout)
    +                    except Exception:
    +                        self.__failure = sys.exc_info()
    +                    self.stop()
    +                self.io_loop.add_timeout(time.time() + timeout, timeout_func)
    +            while True:
    +                self.__running = True
    +                with NullContext():
    +                    # Wipe out the StackContext that was established in
    +                    # self.run() so that all callbacks executed inside the
    +                    # IOLoop will re-run it.
    +                    self.io_loop.start()
    +                if (self.__failure is not None or
    +                    condition is None or condition()):
    +                    break
    +        assert self.__stopped
    +        self.__stopped = False
    +        if self.__failure is not None:
    +            # 2to3 isn't smart enough to convert three-argument raise
    +            # statements correctly in some cases.
    +            if isinstance(self.__failure[1], self.__failure[0]):
    +                raise self.__failure[1], None, self.__failure[2]
    +            else:
    +                raise self.__failure[0], self.__failure[1], self.__failure[2]
    +        result = self.__stop_args
    +        self.__stop_args = None
    +        return result
    +
    +
    +class AsyncHTTPTestCase(AsyncTestCase):
    +    '''A test case that starts up an HTTP server.
    +
    +    Subclasses must override get_app(), which returns the
    +    tornado.web.Application (or other HTTPServer callback) to be tested.
    +    Tests will typically use the provided self.http_client to fetch
    +    URLs from this server.
    +
    +    Example::
    +
    +        class MyHTTPTest(AsyncHTTPTestCase):
    +            def get_app(self):
    +                return Application([('/', MyHandler)...])
    +
    +            def test_homepage(self):
    +                # The following two lines are equivalent to
    +                #   response = self.fetch('/')
    +                # but are shown in full here to demonstrate explicit use
    +                # of self.stop and self.wait.
    +                self.http_client.fetch(self.get_url('/'), self.stop)
    +                response = self.wait()
    +                # test contents of response
    +    '''
    +    def setUp(self):
    +        super(AsyncHTTPTestCase, self).setUp()
    +        self.__port = None
    +
    +        self.http_client = AsyncHTTPClient(io_loop=self.io_loop)
    +        self._app = self.get_app()
    +        self.http_server = HTTPServer(self._app, io_loop=self.io_loop,
    +                                      **self.get_httpserver_options())
    +        self.http_server.listen(self.get_http_port(), address="127.0.0.1")
    +
    +    def get_app(self):
    +        """Should be overridden by subclasses to return a
    +        tornado.web.Application or other HTTPServer callback.
    +        """
    +        raise NotImplementedError()
    +
    +    def fetch(self, path, **kwargs):
    +        """Convenience method to synchronously fetch a url.
    +
    +        The given path will be appended to the local server's host and port.
    +        Any additional kwargs will be passed directly to
    +        AsyncHTTPClient.fetch (and so could be used to pass method="POST",
    +        body="...", etc).
    +        """
    +        self.http_client.fetch(self.get_url(path), self.stop, **kwargs)
    +        return self.wait()
    +
    +    def get_httpserver_options(self):
    +        """May be overridden by subclasses to return additional
    +        keyword arguments for HTTPServer.
    +        """
    +        return {}
    +
    +    def get_http_port(self):
    +        """Returns the port used by the HTTPServer.
    +
    +        A new port is chosen for each test.
    +        """
    +        if self.__port is None:
    +            self.__port = get_unused_port()
    +        return self.__port
    +
    +    def get_url(self, path):
    +        """Returns an absolute url for the given path on the test server."""
    +        return 'http://localhost:%s%s' % (self.get_http_port(), path)
    +
    +    def tearDown(self):
    +        self.http_server.stop()
    +        self.http_client.close()
    +        super(AsyncHTTPTestCase, self).tearDown()
    +
    +class LogTrapTestCase(unittest.TestCase):
    +    """A test case that captures and discards all logging output
    +    if the test passes.
    +
    +    Some libraries can produce a lot of logging output even when
    +    the test succeeds, so this class can be useful to minimize the noise.
    +    Simply use it as a base class for your test case.  It is safe to combine
    +    with AsyncTestCase via multiple inheritance
    +    ("class MyTestCase(AsyncHTTPTestCase, LogTrapTestCase):")
    +
    +    This class assumes that only one log handler is configured and that
    +    it is a StreamHandler.  This is true for both logging.basicConfig
    +    and the "pretty logging" configured by tornado.options.
    +    """
    +    def run(self, result=None):
    +        logger = logging.getLogger()
    +        if len(logger.handlers) > 1:
    +            # Multiple handlers have been defined.  It gets messy to handle
    +            # this, especially since the handlers may have different
    +            # formatters.  Just leave the logging alone in this case.
    +            super(LogTrapTestCase, self).run(result)
    +            return
    +        if not logger.handlers:
    +            logging.basicConfig()
    +        self.assertEqual(len(logger.handlers), 1)
    +        handler = logger.handlers[0]
    +        assert isinstance(handler, logging.StreamHandler)
    +        old_stream = handler.stream
    +        try:
    +            handler.stream = StringIO()
    +            logging.info("RUNNING TEST: " + str(self))
    +            old_error_count = len(result.failures) + len(result.errors)
    +            super(LogTrapTestCase, self).run(result)
    +            new_error_count = len(result.failures) + len(result.errors)
    +            if new_error_count != old_error_count:
    +                old_stream.write(handler.stream.getvalue())
    +        finally:
    +            handler.stream = old_stream
    +
    +def main():
    +    """A simple test runner.
    +
    +    This test runner is essentially equivalent to `unittest.main` from
    +    the standard library, but adds support for tornado-style option
    +    parsing and log formatting.
    +
    +    The easiest way to run a test is via the command line::
    +
    +        python -m tornado.testing tornado.test.stack_context_test
    +
    +    See the standard library unittest module for ways in which tests can
    +    be specified.
    +
    +    Projects with many tests may wish to define a test script like
    +    tornado/test/runtests.py.  This script should define a method all()
    +    which returns a test suite and then call tornado.testing.main().
    +    Note that even when a test script is used, the all() test suite may
    +    be overridden by naming a single test on the command line::
    +
    +        # Runs all tests
    +        tornado/test/runtests.py
    +        # Runs one test
    +        tornado/test/runtests.py tornado.test.stack_context_test
    +
    +    """
    +    from tornado.options import define, options, parse_command_line
    +
    +    define('autoreload', type=bool, default=False,
    +           help="DEPRECATED: use tornado.autoreload.main instead")
    +    define('httpclient', type=str, default=None)
    +    define('exception_on_interrupt', type=bool, default=True,
    +           help=("If true (default), ctrl-c raises a KeyboardInterrupt "
    +                 "exception.  This prints a stack trace but cannot interrupt "
    +                 "certain operations.  If false, the process is more reliably "
    +                 "killed, but does not print a stack trace."))
    +    argv = [sys.argv[0]] + parse_command_line(sys.argv)
    +
    +    if options.httpclient:
    +        from tornado.httpclient import AsyncHTTPClient
    +        AsyncHTTPClient.configure(options.httpclient)
    +
    +    if not options.exception_on_interrupt:
    +        signal.signal(signal.SIGINT, signal.SIG_DFL)
    +
    +    if __name__ == '__main__' and len(argv) == 1:
    +        print >> sys.stderr, "No tests specified"
    +        sys.exit(1)
    +    try:
    +        # In order to be able to run tests by their fully-qualified name
    +        # on the command line without importing all tests here,
    +        # module must be set to None.  Python 3.2's unittest.main ignores
    +        # defaultTest if no module is given (it tries to do its own
    +        # test discovery, which is incompatible with auto2to3), so don't
    +        # set module if we're not asking for a specific test.
    +        if len(argv) > 1:
    +            unittest.main(module=None, argv=argv)
    +        else:
    +            unittest.main(defaultTest="all", argv=argv)
    +    except SystemExit, e:
    +        if e.code == 0:
    +            logging.info('PASS')
    +        else:
    +            logging.error('FAIL')
    +        if not options.autoreload:
    +            raise
    +    if options.autoreload:
    +        import tornado.autoreload
    +        tornado.autoreload.wait()
    +
    +if __name__ == '__main__':
    +    main()
    diff --git a/libs/tornado/util.py b/libs/tornado/util.py
    new file mode 100644
    index 00000000..6752401a
    --- /dev/null
    +++ b/libs/tornado/util.py
    @@ -0,0 +1,47 @@
    +"""Miscellaneous utility functions."""
    +
    +class ObjectDict(dict):
    +    """Makes a dictionary behave like an object."""
    +    def __getattr__(self, name):
    +        try:
    +            return self[name]
    +        except KeyError:
    +            raise AttributeError(name)
    +
    +    def __setattr__(self, name, value):
    +        self[name] = value
    +
    +
    +def import_object(name):
    +    """Imports an object by name.
    +
    +    import_object('x.y.z') is equivalent to 'from x.y import z'.
    +
    +    >>> import tornado.escape
    +    >>> import_object('tornado.escape') is tornado.escape
    +    True
    +    >>> import_object('tornado.escape.utf8') is tornado.escape.utf8
    +    True
    +    """
    +    parts = name.split('.')
    +    obj = __import__('.'.join(parts[:-1]), None, None, [parts[-1]], 0)
    +    return getattr(obj, parts[-1])
    +
    +# Fake byte literal support:  In python 2.6+, you can say b"foo" to get
    +# a byte literal (str in 2.x, bytes in 3.x).  There's no way to do this
    +# in a way that supports 2.5, though, so we need a function wrapper
    +# to convert our string literals.  b() should only be applied to literal
    +# latin1 strings.  Once we drop support for 2.5, we can remove this function
    +# and just use byte literals.
    +if str is unicode:
    +    def b(s):
    +        return s.encode('latin1')
    +    bytes_type = bytes
    +else:
    +    def b(s):
    +        return s
    +    bytes_type = str
    +
    +def doctests():
    +    import doctest
    +    return doctest.DocTestSuite()
    diff --git a/libs/tornado/web.py b/libs/tornado/web.py
    new file mode 100644
    index 00000000..76392b75
    --- /dev/null
    +++ b/libs/tornado/web.py
    @@ -0,0 +1,1985 @@
    +#!/usr/bin/env python
    +#
    +# Copyright 2009 Facebook
    +#
    +# Licensed under the Apache License, Version 2.0 (the "License"); you may
    +# not use this file except in compliance with the License. You may obtain
    +# a copy of the License at
    +#
    +#     http://www.apache.org/licenses/LICENSE-2.0
    +#
    +# Unless required by applicable law or agreed to in writing, software
    +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    +# License for the specific language governing permissions and limitations
    +# under the License.
    +
    +"""
    +The Tornado web framework looks a bit like web.py (http://webpy.org/) or
    +Google's webapp (http://code.google.com/appengine/docs/python/tools/webapp/),
    +but with additional tools and optimizations to take advantage of the
    +Tornado non-blocking web server and tools.
    +
    +Here is the canonical "Hello, world" example app::
    +
    +    import tornado.ioloop
    +    import tornado.web
    +
    +    class MainHandler(tornado.web.RequestHandler):
    +        def get(self):
    +            self.write("Hello, world")
    +
    +    if __name__ == "__main__":
    +        application = tornado.web.Application([
    +            (r"/", MainHandler),
    +        ])
    +        application.listen(8888)
    +        tornado.ioloop.IOLoop.instance().start()
    +
    +See the Tornado walkthrough on http://tornadoweb.org for more details
    +and a good getting started guide.
    +
    +Thread-safety notes
    +-------------------
    +
    +In general, methods on RequestHandler and elsewhere in tornado are not
    +thread-safe.  In particular, methods such as write(), finish(), and
    +flush() must only be called from the main thread.  If you use multiple
    +threads it is important to use IOLoop.add_callback to transfer control
    +back to the main thread before finishing the request.
    +"""
    +
    +from __future__ import with_statement
    +
    +import Cookie
    +import base64
    +import binascii
    +import calendar
    +import datetime
    +import email.utils
    +import functools
    +import gzip
    +import hashlib
    +import hmac
    +import httplib
    +import itertools
    +import logging
    +import mimetypes
    +import os.path
    +import re
    +import stat
    +import sys
    +import threading
    +import time
    +import tornado
    +import traceback
    +import types
    +import urllib
    +import urlparse
    +import uuid
    +
    +from tornado import escape
    +from tornado import locale
    +from tornado import stack_context
    +from tornado import template
    +from tornado.escape import utf8, _unicode
    +from tornado.util import b, bytes_type, import_object, ObjectDict
    +
    +try:
    +    from io import BytesIO  # python 3
    +except ImportError:
    +    from cStringIO import StringIO as BytesIO  # python 2
    +
    +class RequestHandler(object):
    +    """Subclass this class and define get() or post() to make a handler.
    +
    +    If you want to support more methods than the standard GET/HEAD/POST, you
    +    should override the class variable SUPPORTED_METHODS in your
    +    RequestHandler class.
    +    """
    +    SUPPORTED_METHODS = ("GET", "HEAD", "POST", "DELETE", "PUT", "OPTIONS")
    +
    +    _template_loaders = {}  # {path: template.BaseLoader}
    +    _template_loader_lock = threading.Lock()
    +
    +    def __init__(self, application, request, **kwargs):
    +        self.application = application
    +        self.request = request
    +        self._headers_written = False
    +        self._finished = False
    +        self._auto_finish = True
    +        self._transforms = None  # will be set in _execute
    +        self.ui = ObjectDict((n, self._ui_method(m)) for n, m in
    +                     application.ui_methods.iteritems())
    +        # UIModules are available as both `modules` and `_modules` in the
    +        # template namespace.  Historically only `modules` was available
    +        # but could be clobbered by user additions to the namespace.
    +        # The template {% module %} directive looks in `_modules` to avoid
    +        # possible conflicts.
    +        self.ui["_modules"] = ObjectDict((n, self._ui_module(n, m)) for n, m in
    +                                 application.ui_modules.iteritems())
    +        self.ui["modules"] = self.ui["_modules"]
    +        self.clear()
    +        # Check since connection is not available in WSGI
    +        if hasattr(self.request, "connection"):
    +            self.request.connection.stream.set_close_callback(
    +                self.on_connection_close)
    +        self.initialize(**kwargs)
    +
    +    def initialize(self):
    +        """Hook for subclass initialization.
    +
    +        A dictionary passed as the third argument of a url spec will be
    +        supplied as keyword arguments to initialize().
    +
    +        Example::
    +
    +            class ProfileHandler(RequestHandler):
    +                def initialize(self, database):
    +                    self.database = database
    +
    +                def get(self, username):
    +                    ...
    +
    +            app = Application([
    +                (r'/user/(.*)', ProfileHandler, dict(database=database)),
    +                ])
    +        """
    +        pass
    +
    +    @property
    +    def settings(self):
    +        """An alias for `self.application.settings`."""
    +        return self.application.settings
    +
    +    def head(self, *args, **kwargs):
    +        raise HTTPError(405)
    +
    +    def get(self, *args, **kwargs):
    +        raise HTTPError(405)
    +
    +    def post(self, *args, **kwargs):
    +        raise HTTPError(405)
    +
    +    def delete(self, *args, **kwargs):
    +        raise HTTPError(405)
    +
    +    def put(self, *args, **kwargs):
    +        raise HTTPError(405)
    +
    +    def options(self, *args, **kwargs):
    +        raise HTTPError(405)
    +
    +    def prepare(self):
    +        """Called at the beginning of a request before `get`/`post`/etc.
    +
    +        Override this method to perform common initialization regardless
    +        of the request method.
    +        """
    +        pass
    +
    +    def on_finish(self):
    +        """Called after the end of a request.
    +
    +        Override this method to perform cleanup, logging, etc.
    +        This method is a counterpart to `prepare`.  ``on_finish`` may
    +        not produce any output, as it is called after the response
    +        has been sent to the client.
    +        """
    +        pass
    +
    +    def on_connection_close(self):
    +        """Called in async handlers if the client closed the connection.
    +
    +        Override this to clean up resources associated with
    +        long-lived connections.  Note that this method is called only if
    +        the connection was closed during asynchronous processing; if you
    +        need to do cleanup after every request override `on_finish`
    +        instead.
    +
    +        Proxies may keep a connection open for a time (perhaps
    +        indefinitely) after the client has gone away, so this method
    +        may not be called promptly after the end user closes their
    +        connection.
    +        """
    +        pass
    +
    +    def clear(self):
    +        """Resets all headers and content for this response."""
    +        # The performance cost of tornado.httputil.HTTPHeaders is significant
    +        # (slowing down a benchmark with a trivial handler by more than 10%),
    +        # and its case-normalization is not generally necessary for 
    +        # headers we generate on the server side, so use a plain dict
    +        # and list instead.
    +        self._headers = {
    +            "Server": "TornadoServer/%s" % tornado.version,
    +            "Content-Type": "text/html; charset=UTF-8",
    +        }
    +        self._list_headers = []
    +        self.set_default_headers()
    +        if not self.request.supports_http_1_1():
    +            if self.request.headers.get("Connection") == "Keep-Alive":
    +                self.set_header("Connection", "Keep-Alive")
    +        self._write_buffer = []
    +        self._status_code = 200
    +
    +    def set_default_headers(self):
    +        """Override this to set HTTP headers at the beginning of the request.
    +
    +        For example, this is the place to set a custom ``Server`` header.
    +        Note that setting such headers in the normal flow of request
    +        processing may not do what you want, since headers may be reset
    +        during error handling.
    +        """
    +        pass
    +
    +    def set_status(self, status_code):
    +        """Sets the status code for our response."""
    +        assert status_code in httplib.responses
    +        self._status_code = status_code
    +
    +    def get_status(self):
    +        """Returns the status code for our response."""
    +        return self._status_code
    +
    +    def set_header(self, name, value):
    +        """Sets the given response header name and value.
    +
    +        If a datetime is given, we automatically format it according to the
    +        HTTP specification. If the value is not a string, we convert it to
    +        a string. All header values are then encoded as UTF-8.
    +        """
    +        self._headers[name] = self._convert_header_value(value)
    +
    +    def add_header(self, name, value):
    +        """Adds the given response header and value.
    +
    +        Unlike `set_header`, `add_header` may be called multiple times
    +        to return multiple values for the same header.
    +        """
    +        self._list_headers.append((name, self._convert_header_value(value)))
    +
    +    def _convert_header_value(self, value):
    +        if isinstance(value, bytes_type):
    +            pass
    +        elif isinstance(value, unicode):
    +            value = value.encode('utf-8')
    +        elif isinstance(value, (int, long)):
    +            # return immediately since we know the converted value will be safe
    +            return str(value)
    +        elif isinstance(value, datetime.datetime):
    +            t = calendar.timegm(value.utctimetuple())
    +            return email.utils.formatdate(t, localtime=False, usegmt=True)
    +        else:
    +            raise TypeError("Unsupported header value %r" % value)
    +        # If \n is allowed into the header, it is possible to inject
    +        # additional headers or split the request. Also cap length to
    +        # prevent obviously erroneous values.
    +        if len(value) > 4000 or re.search(b(r"[\x00-\x1f]"), value):
    +            raise ValueError("Unsafe header value %r", value)
    +        return value
    +
    +
    +    _ARG_DEFAULT = []
    +    def get_argument(self, name, default=_ARG_DEFAULT, strip=True):
    +        """Returns the value of the argument with the given name.
    +
    +        If default is not provided, the argument is considered to be
    +        required, and we throw an HTTP 400 exception if it is missing.
    +
    +        If the argument appears in the url more than once, we return the
    +        last value.
    +
    +        The returned value is always unicode.
    +        """
    +        args = self.get_arguments(name, strip=strip)
    +        if not args:
    +            if default is self._ARG_DEFAULT:
    +                raise HTTPError(400, "Missing argument %s" % name)
    +            return default
    +        return args[-1]
    +
    +    def get_arguments(self, name, strip=True):
    +        """Returns a list of the arguments with the given name.
    +
    +        If the argument is not present, returns an empty list.
    +
    +        The returned values are always unicode.
    +        """
    +        values = []
    +        for v in self.request.arguments.get(name, []):
    +            v = self.decode_argument(v, name=name)
    +            if isinstance(v, unicode):
    +                # Get rid of any weird control chars (unless decoding gave
    +                # us bytes, in which case leave it alone)
    +                v = re.sub(r"[\x00-\x08\x0e-\x1f]", " ", v)
    +            if strip:
    +                v = v.strip()
    +            values.append(v)
    +        return values
    +
    +    def decode_argument(self, value, name=None):
    +        """Decodes an argument from the request.
    +
    +        The argument has been percent-decoded and is now a byte string.
    +        By default, this method decodes the argument as utf-8 and returns
    +        a unicode string, but this may be overridden in subclasses.
    +
    +        This method is used as a filter for both get_argument() and for
    +        values extracted from the url and passed to get()/post()/etc.
    +
    +        The name of the argument is provided if known, but may be None
    +        (e.g. for unnamed groups in the url regex).
    +        """
    +        return _unicode(value)
    +
    +    @property
    +    def cookies(self):
    +        return self.request.cookies
    +
    +    def get_cookie(self, name, default=None):
    +        """Gets the value of the cookie with the given name, else default."""
    +        if self.request.cookies is not None and name in self.request.cookies:
    +            return self.request.cookies[name].value
    +        return default
    +
    +    def set_cookie(self, name, value, domain=None, expires=None, path="/",
    +                   expires_days=None, **kwargs):
    +        """Sets the given cookie name/value with the given options.
    +
    +        Additional keyword arguments are set on the Cookie.Morsel
    +        directly.
    +        See http://docs.python.org/library/cookie.html#morsel-objects
    +        for available attributes.
    +        """
    +        # The cookie library only accepts type str, in both python 2 and 3
    +        name = escape.native_str(name)
    +        value = escape.native_str(value)
    +        if re.search(r"[\x00-\x20]", name + value):
    +            # Don't let us accidentally inject bad stuff
    +            raise ValueError("Invalid cookie %r: %r" % (name, value))
    +        if not hasattr(self, "_new_cookies"):
    +            self._new_cookies = []
    +        new_cookie = Cookie.SimpleCookie()
    +        self._new_cookies.append(new_cookie)
    +        new_cookie[name] = value
    +        if domain:
    +            new_cookie[name]["domain"] = domain
    +        if expires_days is not None and not expires:
    +            expires = datetime.datetime.utcnow() + datetime.timedelta(
    +                days=expires_days)
    +        if expires:
    +            timestamp = calendar.timegm(expires.utctimetuple())
    +            new_cookie[name]["expires"] = email.utils.formatdate(
    +                timestamp, localtime=False, usegmt=True)
    +        if path:
    +            new_cookie[name]["path"] = path
    +        for k, v in kwargs.iteritems():
    +            if k == 'max_age': k = 'max-age'
    +            new_cookie[name][k] = v
    +
    +    def clear_cookie(self, name, path="/", domain=None):
    +        """Deletes the cookie with the given name."""
    +        expires = datetime.datetime.utcnow() - datetime.timedelta(days=365)
    +        self.set_cookie(name, value="", path=path, expires=expires,
    +                        domain=domain)
    +
    +    def clear_all_cookies(self):
    +        """Deletes all the cookies the user sent with this request."""
    +        for name in self.request.cookies.iterkeys():
    +            self.clear_cookie(name)
    +
    +    def set_secure_cookie(self, name, value, expires_days=30, **kwargs):
    +        """Signs and timestamps a cookie so it cannot be forged.
    +
    +        You must specify the ``cookie_secret`` setting in your Application
    +        to use this method. It should be a long, random sequence of bytes
    +        to be used as the HMAC secret for the signature.
    +
    +        To read a cookie set with this method, use `get_secure_cookie()`.
    +
    +        Note that the ``expires_days`` parameter sets the lifetime of the
    +        cookie in the browser, but is independent of the ``max_age_days``
    +        parameter to `get_secure_cookie`.
    +        """
    +        self.set_cookie(name, self.create_signed_value(name, value),
    +                        expires_days=expires_days, **kwargs)
    +
    +    def create_signed_value(self, name, value):
    +        """Signs and timestamps a string so it cannot be forged.
    +
    +        Normally used via set_secure_cookie, but provided as a separate
    +        method for non-cookie uses.  To decode a value not stored
    +        as a cookie use the optional value argument to get_secure_cookie.
    +        """
    +        self.require_setting("cookie_secret", "secure cookies")
    +        return create_signed_value(self.application.settings["cookie_secret"],
    +                                   name, value)
    +
    +    def get_secure_cookie(self, name, value=None, max_age_days=31):
    +        """Returns the given signed cookie if it validates, or None."""
    +        self.require_setting("cookie_secret", "secure cookies")
    +        if value is None: value = self.get_cookie(name)
    +        return decode_signed_value(self.application.settings["cookie_secret"],
    +                                   name, value, max_age_days=max_age_days)
    +
    +    def redirect(self, url, permanent=False, status=None):
    +        """Sends a redirect to the given (optionally relative) URL.
    +
    +        If the ``status`` argument is specified, that value is used as the
    +        HTTP status code; otherwise either 301 (permanent) or 302
    +        (temporary) is chosen based on the ``permanent`` argument.
    +        The default is 302 (temporary).
    +        """
    +        if self._headers_written:
    +            raise Exception("Cannot redirect after headers have been written")
    +        if status is None:
    +            status = 301 if permanent else 302
    +        else:
    +            assert isinstance(status, int) and 300 <= status <= 399
    +        self.set_status(status)
    +        # Remove whitespace
    +        url = re.sub(b(r"[\x00-\x20]+"), "", utf8(url))
    +        self.set_header("Location", urlparse.urljoin(utf8(self.request.uri),
    +                                                     url))
    +        self.finish()
    +
    +    def write(self, chunk):
    +        """Writes the given chunk to the output buffer.
    +
    +        To write the output to the network, use the flush() method below.
    +
    +        If the given chunk is a dictionary, we write it as JSON and set
    +        the Content-Type of the response to be application/json.
    +        (if you want to send JSON as a different Content-Type, call
    +        set_header *after* calling write()).
    +
    +        Note that lists are not converted to JSON because of a potential
    +        cross-site security vulnerability.  All JSON output should be
    +        wrapped in a dictionary.  More details at
    +        http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx
    +        """
    +        if self._finished:
    +            raise RuntimeError("Cannot write() after finish().  May be caused "
    +                               "by using async operations without the "
    +                               "@asynchronous decorator.")
    +        if isinstance(chunk, dict):
    +            chunk = escape.json_encode(chunk)
    +            self.set_header("Content-Type", "application/json; charset=UTF-8")
    +        chunk = utf8(chunk)
    +        self._write_buffer.append(chunk)
    +
    +    def render(self, template_name, **kwargs):
    +        """Renders the template with the given arguments as the response."""
    +        html = self.render_string(template_name, **kwargs)
    +
    +        # Insert the additional JS and CSS added by the modules on the page
    +        js_embed = []
    +        js_files = []
    +        css_embed = []
    +        css_files = []
    +        html_heads = []
    +        html_bodies = []
    +        for module in getattr(self, "_active_modules", {}).itervalues():
    +            embed_part = module.embedded_javascript()
    +            if embed_part: js_embed.append(utf8(embed_part))
    +            file_part = module.javascript_files()
    +            if file_part:
    +                if isinstance(file_part, (unicode, bytes_type)):
    +                    js_files.append(file_part)
    +                else:
    +                    js_files.extend(file_part)
    +            embed_part = module.embedded_css()
    +            if embed_part: css_embed.append(utf8(embed_part))
    +            file_part = module.css_files()
    +            if file_part:
    +                if isinstance(file_part, (unicode, bytes_type)):
    +                    css_files.append(file_part)
    +                else:
    +                    css_files.extend(file_part)
    +            head_part = module.html_head()
    +            if head_part: html_heads.append(utf8(head_part))
    +            body_part = module.html_body()
    +            if body_part: html_bodies.append(utf8(body_part))
    +        def is_absolute(path):
    +            return any(path.startswith(x) for x in ["/", "http:", "https:"])
    +        if js_files:
    +            # Maintain order of JavaScript files given by modules
    +            paths = []
    +            unique_paths = set()
    +            for path in js_files:
    +                if not is_absolute(path):
    +                    path = self.static_url(path)
    +                if path not in unique_paths:
    +                    paths.append(path)
    +                    unique_paths.add(path)
    +            js = ''.join(''
    +                         for p in paths)
    +            sloc = html.rindex(b(''))
    +            html = html[:sloc] + utf8(js) + b('\n') + html[sloc:]
    +        if js_embed:
    +            js = b('')
    +            sloc = html.rindex(b(''))
    +            html = html[:sloc] + js + b('\n') + html[sloc:]
    +        if css_files:
    +            paths = []
    +            unique_paths = set()
    +            for path in css_files:
    +                if not is_absolute(path):
    +                    path = self.static_url(path)
    +                if path not in unique_paths:
    +                    paths.append(path)
    +                    unique_paths.add(path)
    +            css = ''.join(''
    +                          for p in paths)
    +            hloc = html.index(b(''))
    +            html = html[:hloc] + utf8(css) + b('\n') + html[hloc:]
    +        if css_embed:
    +            css = b('')
    +            hloc = html.index(b(''))
    +            html = html[:hloc] + css + b('\n') + html[hloc:]
    +        if html_heads:
    +            hloc = html.index(b(''))
    +            html = html[:hloc] + b('').join(html_heads) + b('\n') + html[hloc:]
    +        if html_bodies:
    +            hloc = html.index(b(''))
    +            html = html[:hloc] + b('').join(html_bodies) + b('\n') + html[hloc:]
    +        self.finish(html)
    +
    +    def render_string(self, template_name, **kwargs):
    +        """Generate the given template with the given arguments.
    +
    +        We return the generated string. To generate and write a template
    +        as a response, use render() above.
    +        """
    +        # If no template_path is specified, use the path of the calling file
    +        template_path = self.get_template_path()
    +        if not template_path:
    +            frame = sys._getframe(0)
    +            web_file = frame.f_code.co_filename
    +            while frame.f_code.co_filename == web_file:
    +                frame = frame.f_back
    +            template_path = os.path.dirname(frame.f_code.co_filename)
    +        with RequestHandler._template_loader_lock:
    +            if template_path not in RequestHandler._template_loaders:
    +                loader = self.create_template_loader(template_path)
    +                RequestHandler._template_loaders[template_path] = loader
    +            else:
    +                loader = RequestHandler._template_loaders[template_path]
    +        t = loader.load(template_name)
    +        args = dict(
    +            handler=self,
    +            request=self.request,
    +            current_user=self.current_user,
    +            locale=self.locale,
    +            _=self.locale.translate,
    +            static_url=self.static_url,
    +            xsrf_form_html=self.xsrf_form_html,
    +            reverse_url=self.application.reverse_url
    +        )
    +        args.update(self.ui)
    +        args.update(kwargs)
    +        return t.generate(**args)
    +
    +    def create_template_loader(self, template_path):
    +        settings = self.application.settings
    +        if "template_loader" in settings:
    +            return settings["template_loader"]
    +        kwargs = {}
    +        if "autoescape" in settings:
    +            # autoescape=None means "no escaping", so we have to be sure
    +            # to only pass this kwarg if the user asked for it.
    +            kwargs["autoescape"] = settings["autoescape"]
    +        return template.Loader(template_path, **kwargs)
    +
    +
    +    def flush(self, include_footers=False, callback=None):
    +        """Flushes the current output buffer to the network.
    +        
    +        The ``callback`` argument, if given, can be used for flow control:
    +        it will be run when all flushed data has been written to the socket.
    +        Note that only one flush callback can be outstanding at a time;
    +        if another flush occurs before the previous flush's callback
    +        has been run, the previous callback will be discarded.
    +        """
    +        if self.application._wsgi:
    +            raise Exception("WSGI applications do not support flush()")
    +
    +        chunk = b("").join(self._write_buffer)
    +        self._write_buffer = []
    +        if not self._headers_written:
    +            self._headers_written = True
    +            for transform in self._transforms:
    +                self._headers, chunk = transform.transform_first_chunk(
    +                    self._headers, chunk, include_footers)
    +            headers = self._generate_headers()
    +        else:
    +            for transform in self._transforms:
    +                chunk = transform.transform_chunk(chunk, include_footers)
    +            headers = b("")
    +
    +        # Ignore the chunk and only write the headers for HEAD requests
    +        if self.request.method == "HEAD":
    +            if headers: self.request.write(headers, callback=callback)
    +            return
    +
    +        if headers or chunk:
    +            self.request.write(headers + chunk, callback=callback)
    +
    +    def finish(self, chunk=None):
    +        """Finishes this response, ending the HTTP request."""
    +        if self._finished:
    +            raise RuntimeError("finish() called twice.  May be caused "
    +                               "by using async operations without the "
    +                               "@asynchronous decorator.")
    +
    +        if chunk is not None: self.write(chunk)
    +
    +        # Automatically support ETags and add the Content-Length header if
    +        # we have not flushed any content yet.
    +        if not self._headers_written:
    +            if (self._status_code == 200 and
    +                self.request.method in ("GET", "HEAD") and
    +                "Etag" not in self._headers):
    +                etag = self.compute_etag()
    +                if etag is not None:
    +                    inm = self.request.headers.get("If-None-Match")
    +                    if inm and inm.find(etag) != -1:
    +                        self._write_buffer = []
    +                        self.set_status(304)
    +                    else:
    +                        self.set_header("Etag", etag)
    +            if "Content-Length" not in self._headers:
    +                content_length = sum(len(part) for part in self._write_buffer)
    +                self.set_header("Content-Length", content_length)
    +
    +        if hasattr(self.request, "connection"):
    +            # Now that the request is finished, clear the callback we
    +            # set on the IOStream (which would otherwise prevent the
    +            # garbage collection of the RequestHandler when there
    +            # are keepalive connections)
    +            self.request.connection.stream.set_close_callback(None)
    +
    +        if not self.application._wsgi:
    +            self.flush(include_footers=True)
    +            self.request.finish()
    +            self._log()
    +        self._finished = True
    +        self.on_finish()
    +
    +    def send_error(self, status_code=500, **kwargs):
    +        """Sends the given HTTP error code to the browser.
    +
    +        If `flush()` has already been called, it is not possible to send
    +        an error, so this method will simply terminate the response.
    +        If output has been written but not yet flushed, it will be discarded
    +        and replaced with the error page.
    +
    +        Override `write_error()` to customize the error page that is returned.
    +        Additional keyword arguments are passed through to `write_error`.
    +        """
    +        if self._headers_written:
    +            logging.error("Cannot send error response after headers written")
    +            if not self._finished:
    +                self.finish()
    +            return
    +        self.clear()
    +        self.set_status(status_code)
    +        try:
    +            self.write_error(status_code, **kwargs)
    +        except Exception:
    +            logging.error("Uncaught exception in write_error", exc_info=True)
    +        if not self._finished:
    +            self.finish()
    +
    +    def write_error(self, status_code, **kwargs):
    +        """Override to implement custom error pages.
    +
    +        ``write_error`` may call `write`, `render`, `set_header`, etc
    +        to produce output as usual.
    +
    +        If this error was caused by an uncaught exception, an ``exc_info``
    +        triple will be available as ``kwargs["exc_info"]``.  Note that this
    +        exception may not be the "current" exception for purposes of
    +        methods like ``sys.exc_info()`` or ``traceback.format_exc``.
    +
    +        For historical reasons, if a method ``get_error_html`` exists,
    +        it will be used instead of the default ``write_error`` implementation.
    +        ``get_error_html`` returned a string instead of producing output
    +        normally, and had different semantics for exception handling.
    +        Users of ``get_error_html`` are encouraged to convert their code
    +        to override ``write_error`` instead.
    +        """
    +        if hasattr(self, 'get_error_html'):
    +            if 'exc_info' in kwargs:
    +                exc_info = kwargs.pop('exc_info')
    +                kwargs['exception'] = exc_info[1]
    +                try:
    +                    # Put the traceback into sys.exc_info()
    +                    raise exc_info[0], exc_info[1], exc_info[2]
    +                except Exception:
    +                    self.finish(self.get_error_html(status_code, **kwargs))
    +            else:
    +                self.finish(self.get_error_html(status_code, **kwargs))
    +            return
    +        if self.settings.get("debug") and "exc_info" in kwargs:
    +            # in debug mode, try to send a traceback
    +            self.set_header('Content-Type', 'text/plain')
    +            for line in traceback.format_exception(*kwargs["exc_info"]):
    +                self.write(line)
    +            self.finish()
    +        else:
    +            self.finish("%(code)d: %(message)s" 
    +                        "%(code)d: %(message)s" % {
    +                    "code": status_code,
    +                    "message": httplib.responses[status_code],
    +                    })
    +
    +    @property
    +    def locale(self):
    +        """The local for the current session.
    +
    +        Determined by either get_user_locale, which you can override to
    +        set the locale based on, e.g., a user preference stored in a
    +        database, or get_browser_locale, which uses the Accept-Language
    +        header.
    +        """
    +        if not hasattr(self, "_locale"):
    +            self._locale = self.get_user_locale()
    +            if not self._locale:
    +                self._locale = self.get_browser_locale()
    +                assert self._locale
    +        return self._locale
    +
    +    def get_user_locale(self):
    +        """Override to determine the locale from the authenticated user.
    +
    +        If None is returned, we fall back to get_browser_locale().
    +
    +        This method should return a tornado.locale.Locale object,
    +        most likely obtained via a call like tornado.locale.get("en")
    +        """
    +        return None
    +
    +    def get_browser_locale(self, default="en_US"):
    +        """Determines the user's locale from Accept-Language header.
    +
    +        See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
    +        """
    +        if "Accept-Language" in self.request.headers:
    +            languages = self.request.headers["Accept-Language"].split(",")
    +            locales = []
    +            for language in languages:
    +                parts = language.strip().split(";")
    +                if len(parts) > 1 and parts[1].startswith("q="):
    +                    try:
    +                        score = float(parts[1][2:])
    +                    except (ValueError, TypeError):
    +                        score = 0.0
    +                else:
    +                    score = 1.0
    +                locales.append((parts[0], score))
    +            if locales:
    +                locales.sort(key=lambda (l, s): s, reverse=True)
    +                codes = [l[0] for l in locales]
    +                return locale.get(*codes)
    +        return locale.get(default)
    +
    +    @property
    +    def current_user(self):
    +        """The authenticated user for this request.
    +
    +        Determined by either get_current_user, which you can override to
    +        set the user based on, e.g., a cookie. If that method is not
    +        overridden, this method always returns None.
    +
    +        We lazy-load the current user the first time this method is called
    +        and cache the result after that.
    +        """
    +        if not hasattr(self, "_current_user"):
    +            self._current_user = self.get_current_user()
    +        return self._current_user
    +
    +    def get_current_user(self):
    +        """Override to determine the current user from, e.g., a cookie."""
    +        return None
    +
    +    def get_login_url(self):
    +        """Override to customize the login URL based on the request.
    +
    +        By default, we use the 'login_url' application setting.
    +        """
    +        self.require_setting("login_url", "@tornado.web.authenticated")
    +        return self.application.settings["login_url"]
    +
    +    def get_template_path(self):
    +        """Override to customize template path for each handler.
    +
    +        By default, we use the 'template_path' application setting.
    +        Return None to load templates relative to the calling file.
    +        """
    +        return self.application.settings.get("template_path")
    +
    +    @property
    +    def xsrf_token(self):
    +        """The XSRF-prevention token for the current user/session.
    +
    +        To prevent cross-site request forgery, we set an '_xsrf' cookie
    +        and include the same '_xsrf' value as an argument with all POST
    +        requests. If the two do not match, we reject the form submission
    +        as a potential forgery.
    +
    +        See http://en.wikipedia.org/wiki/Cross-site_request_forgery
    +        """
    +        if not hasattr(self, "_xsrf_token"):
    +            token = self.get_cookie("_xsrf")
    +            if not token:
    +                token = binascii.b2a_hex(uuid.uuid4().bytes)
    +                expires_days = 30 if self.current_user else None
    +                self.set_cookie("_xsrf", token, expires_days=expires_days)
    +            self._xsrf_token = token
    +        return self._xsrf_token
    +
    +    def check_xsrf_cookie(self):
    +        """Verifies that the '_xsrf' cookie matches the '_xsrf' argument.
    +
    +        To prevent cross-site request forgery, we set an '_xsrf'
    +        cookie and include the same value as a non-cookie
    +        field with all POST requests. If the two do not match, we
    +        reject the form submission as a potential forgery.
    +
    +        The _xsrf value may be set as either a form field named _xsrf
    +        or in a custom HTTP header named X-XSRFToken or X-CSRFToken
    +        (the latter is accepted for compatibility with Django).
    +
    +        See http://en.wikipedia.org/wiki/Cross-site_request_forgery
    +
    +        Prior to release 1.1.1, this check was ignored if the HTTP header
    +        "X-Requested-With: XMLHTTPRequest" was present.  This exception
    +        has been shown to be insecure and has been removed.  For more
    +        information please see
    +        http://www.djangoproject.com/weblog/2011/feb/08/security/
    +        http://weblog.rubyonrails.org/2011/2/8/csrf-protection-bypass-in-ruby-on-rails
    +        """
    +        token = (self.get_argument("_xsrf", None) or
    +                 self.request.headers.get("X-Xsrftoken") or
    +                 self.request.headers.get("X-Csrftoken"))
    +        if not token:
    +            raise HTTPError(403, "'_xsrf' argument missing from POST")
    +        if self.xsrf_token != token:
    +            raise HTTPError(403, "XSRF cookie does not match POST argument")
    +
    +    def xsrf_form_html(self):
    +        """An HTML  element to be included with all POST forms.
    +
    +        It defines the _xsrf input value, which we check on all POST
    +        requests to prevent cross-site request forgery. If you have set
    +        the 'xsrf_cookies' application setting, you must include this
    +        HTML within all of your HTML forms.
    +
    +        See check_xsrf_cookie() above for more information.
    +        """
    +        return ''
    +
    +    def static_url(self, path, include_host=None):
    +        """Returns a static URL for the given relative static file path.
    +
    +        This method requires you set the 'static_path' setting in your
    +        application (which specifies the root directory of your static
    +        files).
    +
    +        We append ?v= to the returned URL, which makes our
    +        static file handler set an infinite expiration header on the
    +        returned content. The signature is based on the content of the
    +        file.
    +
    +        By default this method returns URLs relative to the current
    +        host, but if ``include_host`` is true the URL returned will be
    +        absolute.  If this handler has an ``include_host`` attribute,
    +        that value will be used as the default for all `static_url`
    +        calls that do not pass ``include_host`` as a keyword argument.
    +        """
    +        self.require_setting("static_path", "static_url")
    +        static_handler_class = self.settings.get(
    +            "static_handler_class", StaticFileHandler)
    +
    +        if include_host is None:
    +            include_host = getattr(self, "include_host", False)
    +
    +        if include_host:
    +            base = self.request.protocol + "://" + self.request.host
    +        else:
    +            base = ""
    +        return base + static_handler_class.make_static_url(self.settings, path)
    +
    +    def async_callback(self, callback, *args, **kwargs):
    +        """Obsolete - catches exceptions from the wrapped function.
    +
    +        This function is unnecessary since Tornado 1.1.
    +        """
    +        if callback is None:
    +            return None
    +        if args or kwargs:
    +            callback = functools.partial(callback, *args, **kwargs)
    +        def wrapper(*args, **kwargs):
    +            try:
    +                return callback(*args, **kwargs)
    +            except Exception, e:
    +                if self._headers_written:
    +                    logging.error("Exception after headers written",
    +                                  exc_info=True)
    +                else:
    +                    self._handle_request_exception(e)
    +        return wrapper
    +
    +    def require_setting(self, name, feature="this feature"):
    +        """Raises an exception if the given app setting is not defined."""
    +        if not self.application.settings.get(name):
    +            raise Exception("You must define the '%s' setting in your "
    +                            "application to use %s" % (name, feature))
    +
    +    def reverse_url(self, name, *args):
    +        """Alias for `Application.reverse_url`."""
    +        return self.application.reverse_url(name, *args)
    +
    +    def compute_etag(self):
    +        """Computes the etag header to be used for this request.
    +
    +        May be overridden to provide custom etag implementations,
    +        or may return None to disable tornado's default etag support.
    +        """
    +        hasher = hashlib.sha1()
    +        for part in self._write_buffer:
    +            hasher.update(part)
    +        return '"%s"' % hasher.hexdigest()
    +
    +    def _stack_context_handle_exception(self, type, value, traceback):
    +        try:
    +            # For historical reasons _handle_request_exception only takes
    +            # the exception value instead of the full triple,
    +            # so re-raise the exception to ensure that it's in
    +            # sys.exc_info()
    +            raise type, value, traceback
    +        except Exception:
    +            self._handle_request_exception(value)
    +        return True
    +
    +    def _execute(self, transforms, *args, **kwargs):
    +        """Executes this request with the given output transforms."""
    +        self._transforms = transforms
    +        try:
    +            if self.request.method not in self.SUPPORTED_METHODS:
    +                raise HTTPError(405)
    +            # If XSRF cookies are turned on, reject form submissions without
    +            # the proper cookie
    +            if self.request.method not in ("GET", "HEAD", "OPTIONS") and \
    +               self.application.settings.get("xsrf_cookies"):
    +                self.check_xsrf_cookie()
    +            self.prepare()
    +            if not self._finished:
    +                args = [self.decode_argument(arg) for arg in args]
    +                kwargs = dict((k, self.decode_argument(v, name=k))
    +                              for (k,v) in kwargs.iteritems())
    +                getattr(self, self.request.method.lower())(*args, **kwargs)
    +                if self._auto_finish and not self._finished:
    +                    self.finish()
    +        except Exception, e:
    +            self._handle_request_exception(e)
    +
    +    def _generate_headers(self):
    +        lines = [utf8(self.request.version + " " +
    +                      str(self._status_code) +
    +                      " " + httplib.responses[self._status_code])]
    +        lines.extend([(utf8(n) + b(": ") + utf8(v)) for n, v in 
    +                      itertools.chain(self._headers.iteritems(), self._list_headers)])
    +        for cookie_dict in getattr(self, "_new_cookies", []):
    +            for cookie in cookie_dict.values():
    +                lines.append(utf8("Set-Cookie: " + cookie.OutputString(None)))
    +        return b("\r\n").join(lines) + b("\r\n\r\n")
    +
    +    def _log(self):
    +        """Logs the current request.
    +
    +        Sort of deprecated since this functionality was moved to the
    +        Application, but left in place for the benefit of existing apps
    +        that have overridden this method.
    +        """
    +        self.application.log_request(self)
    +
    +    def _request_summary(self):
    +        return self.request.method + " " + self.request.uri + \
    +            " (" + self.request.remote_ip + ")"
    +
    +    def _handle_request_exception(self, e):
    +        if isinstance(e, HTTPError):
    +            if e.log_message:
    +                format = "%d %s: " + e.log_message
    +                args = [e.status_code, self._request_summary()] + list(e.args)
    +                logging.warning(format, *args)
    +            if e.status_code not in httplib.responses:
    +                logging.error("Bad HTTP status code: %d", e.status_code)
    +                self.send_error(500, exc_info=sys.exc_info())
    +            else:
    +                self.send_error(e.status_code, exc_info=sys.exc_info())
    +        else:
    +            logging.error("Uncaught exception %s\n%r", self._request_summary(),
    +                          self.request, exc_info=True)
    +            self.send_error(500, exc_info=sys.exc_info())
    +
    +    def _ui_module(self, name, module):
    +        def render(*args, **kwargs):
    +            if not hasattr(self, "_active_modules"):
    +                self._active_modules = {}
    +            if name not in self._active_modules:
    +                self._active_modules[name] = module(self)
    +            rendered = self._active_modules[name].render(*args, **kwargs)
    +            return rendered
    +        return render
    +
    +    def _ui_method(self, method):
    +        return lambda *args, **kwargs: method(self, *args, **kwargs)
    +
    +
    +def asynchronous(method):
    +    """Wrap request handler methods with this if they are asynchronous.
    +
    +    If this decorator is given, the response is not finished when the
    +    method returns. It is up to the request handler to call self.finish()
    +    to finish the HTTP request. Without this decorator, the request is
    +    automatically finished when the get() or post() method returns. ::
    +
    +       class MyRequestHandler(web.RequestHandler):
    +           @web.asynchronous
    +           def get(self):
    +              http = httpclient.AsyncHTTPClient()
    +              http.fetch("http://friendfeed.com/", self._on_download)
    +
    +           def _on_download(self, response):
    +              self.write("Downloaded!")
    +              self.finish()
    +
    +    """
    +    @functools.wraps(method)
    +    def wrapper(self, *args, **kwargs):
    +        if self.application._wsgi:
    +            raise Exception("@asynchronous is not supported for WSGI apps")
    +        self._auto_finish = False
    +        with stack_context.ExceptionStackContext(
    +            self._stack_context_handle_exception):
    +            return method(self, *args, **kwargs)
    +    return wrapper
    +
    +
    +def removeslash(method):
    +    """Use this decorator to remove trailing slashes from the request path.
    +
    +    For example, a request to ``'/foo/'`` would redirect to ``'/foo'`` with this
    +    decorator. Your request handler mapping should use a regular expression
    +    like ``r'/foo/*'`` in conjunction with using the decorator.
    +    """
    +    @functools.wraps(method)
    +    def wrapper(self, *args, **kwargs):
    +        if self.request.path.endswith("/"):
    +            if self.request.method in ("GET", "HEAD"):
    +                uri = self.request.path.rstrip("/")
    +                if uri:  # don't try to redirect '/' to ''
    +                    if self.request.query: uri += "?" + self.request.query
    +                    self.redirect(uri)
    +                    return
    +            else:
    +                raise HTTPError(404)
    +        return method(self, *args, **kwargs)
    +    return wrapper
    +
    +
    +def addslash(method):
    +    """Use this decorator to add a missing trailing slash to the request path.
    +
    +    For example, a request to '/foo' would redirect to '/foo/' with this
    +    decorator. Your request handler mapping should use a regular expression
    +    like r'/foo/?' in conjunction with using the decorator.
    +    """
    +    @functools.wraps(method)
    +    def wrapper(self, *args, **kwargs):
    +        if not self.request.path.endswith("/"):
    +            if self.request.method in ("GET", "HEAD"):
    +                uri = self.request.path + "/"
    +                if self.request.query: uri += "?" + self.request.query
    +                self.redirect(uri)
    +                return
    +            raise HTTPError(404)
    +        return method(self, *args, **kwargs)
    +    return wrapper
    +
    +
    +class Application(object):
    +    """A collection of request handlers that make up a web application.
    +
    +    Instances of this class are callable and can be passed directly to
    +    HTTPServer to serve the application::
    +
    +        application = web.Application([
    +            (r"/", MainPageHandler),
    +        ])
    +        http_server = httpserver.HTTPServer(application)
    +        http_server.listen(8080)
    +        ioloop.IOLoop.instance().start()
    +
    +    The constructor for this class takes in a list of URLSpec objects
    +    or (regexp, request_class) tuples. When we receive requests, we
    +    iterate over the list in order and instantiate an instance of the
    +    first request class whose regexp matches the request path.
    +
    +    Each tuple can contain an optional third element, which should be a
    +    dictionary if it is present. That dictionary is passed as keyword
    +    arguments to the contructor of the handler. This pattern is used
    +    for the StaticFileHandler below (note that a StaticFileHandler
    +    can be installed automatically with the static_path setting described
    +    below)::
    +
    +        application = web.Application([
    +            (r"/static/(.*)", web.StaticFileHandler, {"path": "/var/www"}),
    +        ])
    +
    +    We support virtual hosts with the add_handlers method, which takes in
    +    a host regular expression as the first argument::
    +
    +        application.add_handlers(r"www\.myhost\.com", [
    +            (r"/article/([0-9]+)", ArticleHandler),
    +        ])
    +
    +    You can serve static files by sending the static_path setting as a
    +    keyword argument. We will serve those files from the /static/ URI
    +    (this is configurable with the static_url_prefix setting),
    +    and we will serve /favicon.ico and /robots.txt from the same directory.
    +    A custom subclass of StaticFileHandler can be specified with the
    +    static_handler_class setting.
    +
    +    .. attribute:: settings
    +
    +       Additonal keyword arguments passed to the constructor are saved in the
    +       `settings` dictionary, and are often referred to in documentation as
    +       "application settings".
    +    """
    +    def __init__(self, handlers=None, default_host="", transforms=None,
    +                 wsgi=False, **settings):
    +        if transforms is None:
    +            self.transforms = []
    +            if settings.get("gzip"):
    +                self.transforms.append(GZipContentEncoding)
    +            self.transforms.append(ChunkedTransferEncoding)
    +        else:
    +            self.transforms = transforms
    +        self.handlers = []
    +        self.named_handlers = {}
    +        self.default_host = default_host
    +        self.settings = settings
    +        self.ui_modules = {'linkify': _linkify,
    +                           'xsrf_form_html': _xsrf_form_html,
    +                           'Template': TemplateModule,
    +                           }
    +        self.ui_methods = {}
    +        self._wsgi = wsgi
    +        self._load_ui_modules(settings.get("ui_modules", {}))
    +        self._load_ui_methods(settings.get("ui_methods", {}))
    +        if self.settings.get("static_path"):
    +            path = self.settings["static_path"]
    +            handlers = list(handlers or [])
    +            static_url_prefix = settings.get("static_url_prefix",
    +                                             "/static/")
    +            static_handler_class = settings.get("static_handler_class",
    +                                                StaticFileHandler)
    +            static_handler_args = settings.get("static_handler_args", {})
    +            static_handler_args['path'] = path
    +            for pattern in [re.escape(static_url_prefix) + r"(.*)",
    +                            r"/(favicon\.ico)", r"/(robots\.txt)"]:
    +                handlers.insert(0, (pattern, static_handler_class,
    +                                    static_handler_args))
    +        if handlers: self.add_handlers(".*$", handlers)
    +
    +        # Automatically reload modified modules
    +        if self.settings.get("debug") and not wsgi:
    +            from tornado import autoreload
    +            autoreload.start()
    +
    +    def listen(self, port, address="", **kwargs):
    +        """Starts an HTTP server for this application on the given port.
    +
    +        This is a convenience alias for creating an HTTPServer object
    +        and calling its listen method.  Keyword arguments not
    +        supported by HTTPServer.listen are passed to the HTTPServer
    +        constructor.  For advanced uses (e.g. preforking), do not use
    +        this method; create an HTTPServer and call its bind/start
    +        methods directly.
    +
    +        Note that after calling this method you still need to call
    +        IOLoop.instance().start() to start the server.
    +        """
    +        # import is here rather than top level because HTTPServer
    +        # is not importable on appengine
    +        from tornado.httpserver import HTTPServer
    +        server = HTTPServer(self, **kwargs)
    +        server.listen(port, address)
    +
    +    def add_handlers(self, host_pattern, host_handlers):
    +        """Appends the given handlers to our handler list.
    +
    +        Note that host patterns are processed sequentially in the
    +        order they were added, and only the first matching pattern is
    +        used.  This means that all handlers for a given host must be
    +        added in a single add_handlers call.
    +        """
    +        if not host_pattern.endswith("$"):
    +            host_pattern += "$"
    +        handlers = []
    +        # The handlers with the wildcard host_pattern are a special
    +        # case - they're added in the constructor but should have lower
    +        # precedence than the more-precise handlers added later.
    +        # If a wildcard handler group exists, it should always be last
    +        # in the list, so insert new groups just before it.
    +        if self.handlers and self.handlers[-1][0].pattern == '.*$':
    +            self.handlers.insert(-1, (re.compile(host_pattern), handlers))
    +        else:
    +            self.handlers.append((re.compile(host_pattern), handlers))
    +
    +        for spec in host_handlers:
    +            if type(spec) is type(()):
    +                assert len(spec) in (2, 3)
    +                pattern = spec[0]
    +                handler = spec[1]
    +
    +                if isinstance(handler, str):
    +                    # import the Module and instantiate the class
    +                    # Must be a fully qualified name (module.ClassName)
    +                    handler = import_object(handler)
    +
    +                if len(spec) == 3:
    +                    kwargs = spec[2]
    +                else:
    +                    kwargs = {}
    +                spec = URLSpec(pattern, handler, kwargs)
    +            handlers.append(spec)
    +            if spec.name:
    +                if spec.name in self.named_handlers:
    +                    logging.warning(
    +                        "Multiple handlers named %s; replacing previous value",
    +                        spec.name)
    +                self.named_handlers[spec.name] = spec
    +
    +    def add_transform(self, transform_class):
    +        """Adds the given OutputTransform to our transform list."""
    +        self.transforms.append(transform_class)
    +
    +    def _get_host_handlers(self, request):
    +        host = request.host.lower().split(':')[0]
    +        for pattern, handlers in self.handlers:
    +            if pattern.match(host):
    +                return handlers
    +        # Look for default host if not behind load balancer (for debugging)
    +        if "X-Real-Ip" not in request.headers:
    +            for pattern, handlers in self.handlers:
    +                if pattern.match(self.default_host):
    +                    return handlers
    +        return None
    +
    +    def _load_ui_methods(self, methods):
    +        if type(methods) is types.ModuleType:
    +            self._load_ui_methods(dict((n, getattr(methods, n))
    +                                       for n in dir(methods)))
    +        elif isinstance(methods, list):
    +            for m in methods: self._load_ui_methods(m)
    +        else:
    +            for name, fn in methods.iteritems():
    +                if not name.startswith("_") and hasattr(fn, "__call__") \
    +                   and name[0].lower() == name[0]:
    +                    self.ui_methods[name] = fn
    +
    +    def _load_ui_modules(self, modules):
    +        if type(modules) is types.ModuleType:
    +            self._load_ui_modules(dict((n, getattr(modules, n))
    +                                       for n in dir(modules)))
    +        elif isinstance(modules, list):
    +            for m in modules: self._load_ui_modules(m)
    +        else:
    +            assert isinstance(modules, dict)
    +            for name, cls in modules.iteritems():
    +                try:
    +                    if issubclass(cls, UIModule):
    +                        self.ui_modules[name] = cls
    +                except TypeError:
    +                    pass
    +
    +    def __call__(self, request):
    +        """Called by HTTPServer to execute the request."""
    +        transforms = [t(request) for t in self.transforms]
    +        handler = None
    +        args = []
    +        kwargs = {}
    +        handlers = self._get_host_handlers(request)
    +        if not handlers:
    +            handler = RedirectHandler(
    +                self, request, url="http://" + self.default_host + "/")
    +        else:
    +            for spec in handlers:
    +                match = spec.regex.match(request.path)
    +                if match:
    +                    handler = spec.handler_class(self, request, **spec.kwargs)
    +                    if spec.regex.groups:
    +                        # None-safe wrapper around url_unescape to handle
    +                        # unmatched optional groups correctly
    +                        def unquote(s):
    +                            if s is None: return s
    +                            return escape.url_unescape(s, encoding=None)
    +                        # Pass matched groups to the handler.  Since
    +                        # match.groups() includes both named and unnamed groups,
    +                        # we want to use either groups or groupdict but not both.
    +                        # Note that args are passed as bytes so the handler can
    +                        # decide what encoding to use.
    +
    +                        if spec.regex.groupindex:
    +                            kwargs = dict(
    +                                (k, unquote(v))
    +                                for (k, v) in match.groupdict().iteritems())
    +                        else:
    +                            args = [unquote(s) for s in match.groups()]
    +                    break
    +            if not handler:
    +                handler = ErrorHandler(self, request, status_code=404)
    +
    +        # In debug mode, re-compile templates and reload static files on every
    +        # request so you don't need to restart to see changes
    +        if self.settings.get("debug"):
    +            with RequestHandler._template_loader_lock:
    +                for loader in RequestHandler._template_loaders.values():
    +                    loader.reset()
    +            StaticFileHandler.reset()
    +
    +        handler._execute(transforms, *args, **kwargs)
    +        return handler
    +
    +    def reverse_url(self, name, *args):
    +        """Returns a URL path for handler named `name`
    +
    +        The handler must be added to the application as a named URLSpec
    +        """
    +        if name in self.named_handlers:
    +            return self.named_handlers[name].reverse(*args)
    +        raise KeyError("%s not found in named urls" % name)
    +
    +    def log_request(self, handler):
    +        """Writes a completed HTTP request to the logs.
    +
    +        By default writes to the python root logger.  To change
    +        this behavior either subclass Application and override this method,
    +        or pass a function in the application settings dictionary as
    +        'log_function'.
    +        """
    +        if "log_function" in self.settings:
    +            self.settings["log_function"](handler)
    +            return
    +        if handler.get_status() < 400:
    +            log_method = logging.info
    +        elif handler.get_status() < 500:
    +            log_method = logging.warning
    +        else:
    +            log_method = logging.error
    +        request_time = 1000.0 * handler.request.request_time()
    +        log_method("%d %s %.2fms", handler.get_status(),
    +                   handler._request_summary(), request_time)
    +
    +
    +
    +class HTTPError(Exception):
    +    """An exception that will turn into an HTTP error response."""
    +    def __init__(self, status_code, log_message=None, *args):
    +        self.status_code = status_code
    +        self.log_message = log_message
    +        self.args = args
    +
    +    def __str__(self):
    +        message = "HTTP %d: %s" % (
    +            self.status_code, httplib.responses[self.status_code])
    +        if self.log_message:
    +            return message + " (" + (self.log_message % self.args) + ")"
    +        else:
    +            return message
    +
    +
    +class ErrorHandler(RequestHandler):
    +    """Generates an error response with status_code for all requests."""
    +    def initialize(self, status_code):
    +        self.set_status(status_code)
    +
    +    def prepare(self):
    +        raise HTTPError(self._status_code)
    +
    +
    +class RedirectHandler(RequestHandler):
    +    """Redirects the client to the given URL for all GET requests.
    +
    +    You should provide the keyword argument "url" to the handler, e.g.::
    +
    +        application = web.Application([
    +            (r"/oldpath", web.RedirectHandler, {"url": "/newpath"}),
    +        ])
    +    """
    +    def initialize(self, url, permanent=True):
    +        self._url = url
    +        self._permanent = permanent
    +
    +    def get(self):
    +        self.redirect(self._url, permanent=self._permanent)
    +
    +
    +class StaticFileHandler(RequestHandler):
    +    """A simple handler that can serve static content from a directory.
    +
    +    To map a path to this handler for a static data directory /var/www,
    +    you would add a line to your application like::
    +
    +        application = web.Application([
    +            (r"/static/(.*)", web.StaticFileHandler, {"path": "/var/www"}),
    +        ])
    +
    +    The local root directory of the content should be passed as the "path"
    +    argument to the handler.
    +
    +    To support aggressive browser caching, if the argument "v" is given
    +    with the path, we set an infinite HTTP expiration header. So, if you
    +    want browsers to cache a file indefinitely, send them to, e.g.,
    +    /static/images/myimage.png?v=xxx. Override ``get_cache_time`` method for
    +    more fine-grained cache control.
    +    """
    +    CACHE_MAX_AGE = 86400*365*10 #10 years
    +
    +    _static_hashes = {}
    +    _lock = threading.Lock()  # protects _static_hashes
    +
    +    def initialize(self, path, default_filename=None):
    +        self.root = os.path.abspath(path) + os.path.sep
    +        self.default_filename = default_filename
    +
    +    @classmethod
    +    def reset(cls):
    +        with cls._lock:
    +            cls._static_hashes = {}
    +
    +    def head(self, path):
    +        self.get(path, include_body=False)
    +
    +    def get(self, path, include_body=True):
    +        path = self.parse_url_path(path)
    +        abspath = os.path.abspath(os.path.join(self.root, path))
    +        # os.path.abspath strips a trailing /
    +        # it needs to be temporarily added back for requests to root/
    +        if not (abspath + os.path.sep).startswith(self.root):
    +            raise HTTPError(403, "%s is not in root static directory", path)
    +        if os.path.isdir(abspath) and self.default_filename is not None:
    +            # need to look at the request.path here for when path is empty
    +            # but there is some prefix to the path that was already
    +            # trimmed by the routing
    +            if not self.request.path.endswith("/"):
    +                self.redirect(self.request.path + "/")
    +                return
    +            abspath = os.path.join(abspath, self.default_filename)
    +        if not os.path.exists(abspath):
    +            raise HTTPError(404)
    +        if not os.path.isfile(abspath):
    +            raise HTTPError(403, "%s is not a file", path)
    +
    +        stat_result = os.stat(abspath)
    +        modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME])
    +
    +        self.set_header("Last-Modified", modified)
    +
    +        mime_type, encoding = mimetypes.guess_type(abspath)
    +        if mime_type:
    +            self.set_header("Content-Type", mime_type)
    +
    +        cache_time = self.get_cache_time(path, modified, mime_type)
    +
    +        if cache_time > 0:
    +            self.set_header("Expires", datetime.datetime.utcnow() + \
    +                                       datetime.timedelta(seconds=cache_time))
    +            self.set_header("Cache-Control", "max-age=" + str(cache_time))
    +        else:
    +            self.set_header("Cache-Control", "public")
    +
    +        self.set_extra_headers(path)
    +
    +        # Check the If-Modified-Since, and don't send the result if the
    +        # content has not been modified
    +        ims_value = self.request.headers.get("If-Modified-Since")
    +        if ims_value is not None:
    +            date_tuple = email.utils.parsedate(ims_value)
    +            if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple))
    +            if if_since >= modified:
    +                self.set_status(304)
    +                return
    +
    +        with open(abspath, "rb") as file:
    +            data = file.read()
    +            hasher = hashlib.sha1()
    +            hasher.update(data)
    +            self.set_header("Etag", '"%s"' % hasher.hexdigest())
    +            if include_body:
    +                self.write(data)
    +            else:
    +                assert self.request.method == "HEAD"
    +                self.set_header("Content-Length", len(data))
    +
    +    def set_extra_headers(self, path):
    +        """For subclass to add extra headers to the response"""
    +        pass
    +
    +    def get_cache_time(self, path, modified, mime_type):
    +        """Override to customize cache control behavior.
    +
    +        Return a positive number of seconds to trigger aggressive caching or 0
    +        to mark resource as cacheable, only.
    +
    +        By default returns cache expiry of 10 years for resources requested
    +        with "v" argument.
    +        """
    +        return self.CACHE_MAX_AGE if "v" in self.request.arguments else 0
    +
    +    @classmethod
    +    def make_static_url(cls, settings, path):
    +        """Constructs a versioned url for the given path.
    +
    +        This method may be overridden in subclasses (but note that it is
    +        a class method rather than an instance method).
    +        
    +        ``settings`` is the `Application.settings` dictionary.  ``path``
    +        is the static path being requested.  The url returned should be
    +        relative to the current host.
    +        """
    +        static_url_prefix = settings.get('static_url_prefix', '/static/')
    +        version_hash = cls.get_version(settings, path)
    +        if version_hash:
    +            return static_url_prefix + path + "?v=" + version_hash
    +        return static_url_prefix + path
    +
    +    @classmethod
    +    def get_version(cls, settings, path):
    +        """Generate the version string to be used in static URLs.
    +
    +        This method may be overridden in subclasses (but note that it
    +        is a class method rather than a static method).  The default
    +        implementation uses a hash of the file's contents.
    +
    +        ``settings`` is the `Application.settings` dictionary and ``path``
    +        is the relative location of the requested asset on the filesystem.
    +        The returned value should be a string, or ``None`` if no version
    +        could be determined.
    +        """
    +        abs_path = os.path.join(settings["static_path"], path)
    +        with cls._lock:
    +            hashes = cls._static_hashes
    +            if abs_path not in hashes:
    +                try:
    +                    f = open(abs_path, "rb")
    +                    hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
    +                    f.close()
    +                except Exception:
    +                    logging.error("Could not open static file %r", path)
    +                    hashes[abs_path] = None
    +            hsh = hashes.get(abs_path)
    +            if hsh:
    +                return hsh[:5]
    +        return None
    +
    +    def parse_url_path(self, url_path):
    +        """Converts a static URL path into a filesystem path.
    +
    +        ``url_path`` is the path component of the URL with
    +        ``static_url_prefix`` removed.  The return value should be
    +        filesystem path relative to ``static_path``.
    +        """
    +        if os.path.sep != "/":
    +            url_path = url_path.replace("/", os.path.sep)
    +        return url_path
    +
    +
    +class FallbackHandler(RequestHandler):
    +    """A RequestHandler that wraps another HTTP server callback.
    +
    +    The fallback is a callable object that accepts an HTTPRequest,
    +    such as an Application or tornado.wsgi.WSGIContainer.  This is most
    +    useful to use both tornado RequestHandlers and WSGI in the same server.
    +    Typical usage::
    +
    +        wsgi_app = tornado.wsgi.WSGIContainer(
    +            django.core.handlers.wsgi.WSGIHandler())
    +        application = tornado.web.Application([
    +            (r"/foo", FooHandler),
    +            (r".*", FallbackHandler, dict(fallback=wsgi_app),
    +        ])
    +    """
    +    def initialize(self, fallback):
    +        self.fallback = fallback
    +
    +    def prepare(self):
    +        self.fallback(self.request)
    +        self._finished = True
    +
    +
    +class OutputTransform(object):
    +    """A transform modifies the result of an HTTP request (e.g., GZip encoding)
    +
    +    A new transform instance is created for every request. See the
    +    ChunkedTransferEncoding example below if you want to implement a
    +    new Transform.
    +    """
    +    def __init__(self, request):
    +        pass
    +
    +    def transform_first_chunk(self, headers, chunk, finishing):
    +        return headers, chunk
    +
    +    def transform_chunk(self, chunk, finishing):
    +        return chunk
    +
    +
    +class GZipContentEncoding(OutputTransform):
    +    """Applies the gzip content encoding to the response.
    +
    +    See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
    +    """
    +    CONTENT_TYPES = set([
    +        "text/plain", "text/html", "text/css", "text/xml", "application/javascript", 
    +        "application/x-javascript", "application/xml", "application/atom+xml",
    +        "text/javascript", "application/json", "application/xhtml+xml"])
    +    MIN_LENGTH = 5
    +
    +    def __init__(self, request):
    +        self._gzipping = request.supports_http_1_1() and \
    +            "gzip" in request.headers.get("Accept-Encoding", "")
    +
    +    def transform_first_chunk(self, headers, chunk, finishing):
    +        if self._gzipping:
    +            ctype = _unicode(headers.get("Content-Type", "")).split(";")[0]
    +            self._gzipping = (ctype in self.CONTENT_TYPES) and \
    +                (not finishing or len(chunk) >= self.MIN_LENGTH) and \
    +                (finishing or "Content-Length" not in headers) and \
    +                ("Content-Encoding" not in headers)
    +        if self._gzipping:
    +            headers["Content-Encoding"] = "gzip"
    +            self._gzip_value = BytesIO()
    +            self._gzip_file = gzip.GzipFile(mode="w", fileobj=self._gzip_value)
    +            chunk = self.transform_chunk(chunk, finishing)
    +            if "Content-Length" in headers:
    +                headers["Content-Length"] = str(len(chunk))
    +        return headers, chunk
    +
    +    def transform_chunk(self, chunk, finishing):
    +        if self._gzipping:
    +            self._gzip_file.write(chunk)
    +            if finishing:
    +                self._gzip_file.close()
    +            else:
    +                self._gzip_file.flush()
    +            chunk = self._gzip_value.getvalue()
    +            self._gzip_value.truncate(0)
    +            self._gzip_value.seek(0)
    +        return chunk
    +
    +
    +class ChunkedTransferEncoding(OutputTransform):
    +    """Applies the chunked transfer encoding to the response.
    +
    +    See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1
    +    """
    +    def __init__(self, request):
    +        self._chunking = request.supports_http_1_1()
    +
    +    def transform_first_chunk(self, headers, chunk, finishing):
    +        if self._chunking:
    +            # No need to chunk the output if a Content-Length is specified
    +            if "Content-Length" in headers or "Transfer-Encoding" in headers:
    +                self._chunking = False
    +            else:
    +                headers["Transfer-Encoding"] = "chunked"
    +                chunk = self.transform_chunk(chunk, finishing)
    +        return headers, chunk
    +
    +    def transform_chunk(self, block, finishing):
    +        if self._chunking:
    +            # Don't write out empty chunks because that means END-OF-STREAM
    +            # with chunked encoding
    +            if block:
    +                block = utf8("%x" % len(block)) + b("\r\n") + block + b("\r\n")
    +            if finishing:
    +                block += b("0\r\n\r\n")
    +        return block
    +
    +
    +def authenticated(method):
    +    """Decorate methods with this to require that the user be logged in."""
    +    @functools.wraps(method)
    +    def wrapper(self, *args, **kwargs):
    +        if not self.current_user:
    +            if self.request.method in ("GET", "HEAD"):
    +                url = self.get_login_url()
    +                if "?" not in url:
    +                    if urlparse.urlsplit(url).scheme:
    +                        # if login url is absolute, make next absolute too
    +                        next_url = self.request.full_url()
    +                    else:
    +                        next_url = self.request.uri
    +                    url += "?" + urllib.urlencode(dict(next=next_url))
    +                self.redirect(url)
    +                return
    +            raise HTTPError(403)
    +        return method(self, *args, **kwargs)
    +    return wrapper
    +
    +
    +class UIModule(object):
    +    """A UI re-usable, modular unit on a page.
    +
    +    UI modules often execute additional queries, and they can include
    +    additional CSS and JavaScript that will be included in the output
    +    page, which is automatically inserted on page render.
    +    """
    +    def __init__(self, handler):
    +        self.handler = handler
    +        self.request = handler.request
    +        self.ui = handler.ui
    +        self.current_user = handler.current_user
    +        self.locale = handler.locale
    +
    +    def render(self, *args, **kwargs):
    +        """Overridden in subclasses to return this module's output."""
    +        raise NotImplementedError()
    +
    +    def embedded_javascript(self):
    +        """Returns a JavaScript string that will be embedded in the page."""
    +        return None
    +
    +    def javascript_files(self):
    +        """Returns a list of JavaScript files required by this module."""
    +        return None
    +
    +    def embedded_css(self):
    +        """Returns a CSS string that will be embedded in the page."""
    +        return None
    +
    +    def css_files(self):
    +        """Returns a list of CSS files required by this module."""
    +        return None
    +
    +    def html_head(self):
    +        """Returns a CSS string that will be put in the  element"""
    +        return None
    +
    +    def html_body(self):
    +        """Returns an HTML string that will be put in the  element"""
    +        return None
    +
    +    def render_string(self, path, **kwargs):
    +        """Renders a template and returns it as a string."""
    +        return self.handler.render_string(path, **kwargs)
    +
    +class _linkify(UIModule):
    +    def render(self, text, **kwargs):
    +        return escape.linkify(text, **kwargs)
    +
    +class _xsrf_form_html(UIModule):
    +    def render(self):
    +        return self.handler.xsrf_form_html()
    +
    +class TemplateModule(UIModule):
    +    """UIModule that simply renders the given template.
    +
    +    {% module Template("foo.html") %} is similar to {% include "foo.html" %},
    +    but the module version gets its own namespace (with kwargs passed to
    +    Template()) instead of inheriting the outer template's namespace.
    +
    +    Templates rendered through this module also get access to UIModule's
    +    automatic javascript/css features.  Simply call set_resources
    +    inside the template and give it keyword arguments corresponding to
    +    the methods on UIModule: {{ set_resources(js_files=static_url("my.js")) }}
    +    Note that these resources are output once per template file, not once
    +    per instantiation of the template, so they must not depend on 
    +    any arguments to the template.
    +    """
    +    def __init__(self, handler):
    +        super(TemplateModule, self).__init__(handler)
    +        # keep resources in both a list and a dict to preserve order
    +        self._resource_list = []
    +        self._resource_dict = {}
    +
    +    def render(self, path, **kwargs):
    +        def set_resources(**kwargs):
    +            if path not in self._resource_dict:
    +                self._resource_list.append(kwargs)
    +                self._resource_dict[path] = kwargs
    +            else:
    +                if self._resource_dict[path] != kwargs:
    +                    raise ValueError("set_resources called with different "
    +                                     "resources for the same template")
    +            return ""
    +        return self.render_string(path, set_resources=set_resources,
    +                                  **kwargs)
    +
    +    def _get_resources(self, key):
    +        return (r[key] for r in self._resource_list if key in r)
    +
    +    def embedded_javascript(self):
    +        return "\n".join(self._get_resources("embedded_javascript"))
    +
    +    def javascript_files(self):
    +        result = []
    +        for f in self._get_resources("javascript_files"):
    +            if isinstance(f, (unicode, bytes_type)):
    +                result.append(f)
    +            else:
    +                result.extend(f)
    +        return result
    +
    +    def embedded_css(self):
    +        return "\n".join(self._get_resources("embedded_css"))
    +
    +    def css_files(self):
    +        result = []
    +        for f in self._get_resources("css_files"):
    +            if isinstance(f, (unicode, bytes_type)):
    +                result.append(f)
    +            else:
    +                result.extend(f)
    +        return result
    +
    +    def html_head(self):
    +        return "".join(self._get_resources("html_head"))
    +
    +    def html_body(self):
    +        return "".join(self._get_resources("html_body"))
    +
    +
    +
    +class URLSpec(object):
    +    """Specifies mappings between URLs and handlers."""
    +    def __init__(self, pattern, handler_class, kwargs={}, name=None):
    +        """Creates a URLSpec.
    +
    +        Parameters:
    +
    +        pattern: Regular expression to be matched.  Any groups in the regex
    +            will be passed in to the handler's get/post/etc methods as
    +            arguments.
    +
    +        handler_class: RequestHandler subclass to be invoked.
    +
    +        kwargs (optional): A dictionary of additional arguments to be passed
    +            to the handler's constructor.
    +
    +        name (optional): A name for this handler.  Used by
    +            Application.reverse_url.
    +        """
    +        if not pattern.endswith('$'):
    +            pattern += '$'
    +        self.regex = re.compile(pattern)
    +        assert len(self.regex.groupindex) in (0, self.regex.groups), \
    +            ("groups in url regexes must either be all named or all "
    +             "positional: %r" % self.regex.pattern)
    +        self.handler_class = handler_class
    +        self.kwargs = kwargs
    +        self.name = name
    +        self._path, self._group_count = self._find_groups()
    +
    +    def _find_groups(self):
    +        """Returns a tuple (reverse string, group count) for a url.
    +
    +        For example: Given the url pattern /([0-9]{4})/([a-z-]+)/, this method
    +        would return ('/%s/%s/', 2).
    +        """
    +        pattern = self.regex.pattern
    +        if pattern.startswith('^'):
    +            pattern = pattern[1:]
    +        if pattern.endswith('$'):
    +            pattern = pattern[:-1]
    +
    +        if self.regex.groups != pattern.count('('):
    +            # The pattern is too complicated for our simplistic matching,
    +            # so we can't support reversing it.
    +            return (None, None)
    +
    +        pieces = []
    +        for fragment in pattern.split('('):
    +            if ')' in fragment:
    +                paren_loc = fragment.index(')')
    +                if paren_loc >= 0:
    +                    pieces.append('%s' + fragment[paren_loc + 1:])
    +            else:
    +                pieces.append(fragment)
    +
    +        return (''.join(pieces), self.regex.groups)
    +
    +    def reverse(self, *args):
    +        assert self._path is not None, \
    +            "Cannot reverse url regex " + self.regex.pattern
    +        assert len(args) == self._group_count, "required number of arguments "\
    +            "not found"
    +        if not len(args):
    +            return self._path
    +        return self._path % tuple([str(a) for a in args])
    +
    +url = URLSpec
    +
    +
    +def _time_independent_equals(a, b):
    +    if len(a) != len(b):
    +        return False
    +    result = 0
    +    if type(a[0]) is int:  # python3 byte strings
    +        for x, y in zip(a,b):
    +            result |= x ^ y
    +    else:  # python2
    +        for x, y in zip(a, b):
    +            result |= ord(x) ^ ord(y)
    +    return result == 0
    +
    +def create_signed_value(secret, name, value):
    +    timestamp = utf8(str(int(time.time())))
    +    value = base64.b64encode(utf8(value))
    +    signature = _create_signature(secret, name, value, timestamp)
    +    value = b("|").join([value, timestamp, signature])
    +    return value
    +
    +def decode_signed_value(secret, name, value, max_age_days=31):
    +    if not value: return None
    +    parts = utf8(value).split(b("|"))
    +    if len(parts) != 3: return None
    +    signature = _create_signature(secret, name, parts[0], parts[1])
    +    if not _time_independent_equals(parts[2], signature):
    +        logging.warning("Invalid cookie signature %r", value)
    +        return None
    +    timestamp = int(parts[1])
    +    if timestamp < time.time() - max_age_days * 86400:
    +        logging.warning("Expired cookie %r", value)
    +        return None
    +    if timestamp > time.time() + 31 * 86400:
    +        # _cookie_signature does not hash a delimiter between the
    +        # parts of the cookie, so an attacker could transfer trailing
    +        # digits from the payload to the timestamp without altering the
    +        # signature.  For backwards compatibility, sanity-check timestamp
    +        # here instead of modifying _cookie_signature.
    +        logging.warning("Cookie timestamp in future; possible tampering %r", value)
    +        return None
    +    if parts[1].startswith(b("0")):
    +        logging.warning("Tampered cookie %r", value)
    +    try:
    +        return base64.b64decode(parts[0])
    +    except Exception:
    +        return None
    +
    +def _create_signature(secret, *parts):
    +    hash = hmac.new(utf8(secret), digestmod=hashlib.sha1)
    +    for part in parts: hash.update(utf8(part))
    +    return utf8(hash.hexdigest())
    diff --git a/libs/tornado/websocket.py b/libs/tornado/websocket.py
    new file mode 100644
    index 00000000..8aa77771
    --- /dev/null
    +++ b/libs/tornado/websocket.py
    @@ -0,0 +1,650 @@
    +"""Server-side implementation of the WebSocket protocol.
    +
    +`WebSockets `_ allow for bidirectional
    +communication between the browser and server.
    +
    +.. warning::
    +
    +   The WebSocket protocol was recently finalized as `RFC 6455
    +   `_ and is not yet supported in
    +   all browsers.  Refer to http://caniuse.com/websockets for details
    +   on compatibility.  In addition, during development the protocol
    +   went through several incompatible versions, and some browsers only
    +   support older versions.  By default this module only supports the
    +   latest version of the protocol, but optional support for an older
    +   version (known as "draft 76" or "hixie-76") can be enabled by
    +   overriding `WebSocketHandler.allow_draft76` (see that method's
    +   documentation for caveats).
    +"""
    +# Author: Jacob Kristhammar, 2010
    +
    +import array
    +import functools
    +import hashlib
    +import logging
    +import struct
    +import time
    +import base64
    +import tornado.escape
    +import tornado.web
    +
    +from tornado.util import bytes_type, b
    +
    +class WebSocketHandler(tornado.web.RequestHandler):
    +    """Subclass this class to create a basic WebSocket handler.
    +
    +    Override on_message to handle incoming messages. You can also override
    +    open and on_close to handle opened and closed connections.
    +
    +    See http://dev.w3.org/html5/websockets/ for details on the
    +    JavaScript interface.  The protocol is specified at
    +    http://tools.ietf.org/html/rfc6455.
    +
    +    Here is an example Web Socket handler that echos back all received messages
    +    back to the client::
    +
    +      class EchoWebSocket(websocket.WebSocketHandler):
    +          def open(self):
    +              print "WebSocket opened"
    +
    +          def on_message(self, message):
    +              self.write_message(u"You said: " + message)
    +
    +          def on_close(self):
    +              print "WebSocket closed"
    +
    +    Web Sockets are not standard HTTP connections. The "handshake" is HTTP,
    +    but after the handshake, the protocol is message-based. Consequently,
    +    most of the Tornado HTTP facilities are not available in handlers of this
    +    type. The only communication methods available to you are write_message()
    +    and close(). Likewise, your request handler class should
    +    implement open() method rather than get() or post().
    +
    +    If you map the handler above to "/websocket" in your application, you can
    +    invoke it in JavaScript with::
    +
    +      var ws = new WebSocket("ws://localhost:8888/websocket");
    +      ws.onopen = function() {
    +         ws.send("Hello, world");
    +      };
    +      ws.onmessage = function (evt) {
    +         alert(evt.data);
    +      };
    +
    +    This script pops up an alert box that says "You said: Hello, world".
    +    """
    +    def __init__(self, application, request, **kwargs):
    +        tornado.web.RequestHandler.__init__(self, application, request,
    +                                            **kwargs)
    +        self.stream = request.connection.stream
    +        self.ws_connection = None
    +
    +    def _execute(self, transforms, *args, **kwargs):
    +        self.open_args = args
    +        self.open_kwargs = kwargs
    +
    +        # Websocket only supports GET method
    +        if self.request.method != 'GET':
    +            self.stream.write(tornado.escape.utf8(
    +                "HTTP/1.1 405 Method Not Allowed\r\n\r\n"
    +            ))
    +            self.stream.close()
    +            return
    +
    +        # Upgrade header should be present and should be equal to WebSocket
    +        if self.request.headers.get("Upgrade", "").lower() != 'websocket':
    +            self.stream.write(tornado.escape.utf8(
    +                "HTTP/1.1 400 Bad Request\r\n\r\n"
    +                "Can \"Upgrade\" only to \"WebSocket\"."
    +            ))
    +            self.stream.close()
    +            return
    +
    +        # Connection header should be upgrade. Some proxy servers/load balancers
    +        # might mess with it.
    +        headers = self.request.headers
    +        connection = map(lambda s: s.strip().lower(), headers.get("Connection", "").split(","))
    +        if 'upgrade' not in connection:
    +            self.stream.write(tornado.escape.utf8(
    +                "HTTP/1.1 400 Bad Request\r\n\r\n"
    +                "\"Connection\" must be \"Upgrade\"."
    +            ))
    +            self.stream.close()
    +            return
    +
    +        # The difference between version 8 and 13 is that in 8 the
    +        # client sends a "Sec-Websocket-Origin" header and in 13 it's
    +        # simply "Origin".
    +        if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
    +            self.ws_connection = WebSocketProtocol13(self)
    +            self.ws_connection.accept_connection()
    +        elif (self.allow_draft76() and
    +              "Sec-WebSocket-Version" not in self.request.headers):
    +            self.ws_connection = WebSocketProtocol76(self)
    +            self.ws_connection.accept_connection()
    +        else:
    +            self.stream.write(tornado.escape.utf8(
    +                "HTTP/1.1 426 Upgrade Required\r\n"
    +                "Sec-WebSocket-Version: 8\r\n\r\n"))
    +            self.stream.close()
    +
    +    def write_message(self, message, binary=False):
    +        """Sends the given message to the client of this Web Socket.
    +
    +        The message may be either a string or a dict (which will be
    +        encoded as json).  If the ``binary`` argument is false, the
    +        message will be sent as utf8; in binary mode any byte string
    +        is allowed.
    +        """
    +        if isinstance(message, dict):
    +            message = tornado.escape.json_encode(message)
    +        self.ws_connection.write_message(message, binary=binary)
    +
    +    def select_subprotocol(self, subprotocols):
    +        """Invoked when a new WebSocket requests specific subprotocols.
    +
    +        ``subprotocols`` is a list of strings identifying the
    +        subprotocols proposed by the client.  This method may be
    +        overridden to return one of those strings to select it, or
    +        ``None`` to not select a subprotocol.  Failure to select a
    +        subprotocol does not automatically abort the connection,
    +        although clients may close the connection if none of their
    +        proposed subprotocols was selected.
    +        """
    +        return None
    +
    +    def open(self):
    +        """Invoked when a new WebSocket is opened.
    +
    +        The arguments to `open` are extracted from the `tornado.web.URLSpec`
    +        regular expression, just like the arguments to
    +        `tornado.web.RequestHandler.get`.
    +        """
    +        pass
    +
    +    def on_message(self, message):
    +        """Handle incoming messages on the WebSocket
    +
    +        This method must be overridden.
    +        """
    +        raise NotImplementedError
    +
    +    def on_close(self):
    +        """Invoked when the WebSocket is closed."""
    +        pass
    +
    +    def close(self):
    +        """Closes this Web Socket.
    +
    +        Once the close handshake is successful the socket will be closed.
    +        """
    +        self.ws_connection.close()
    +
    +    def allow_draft76(self):
    +        """Override to enable support for the older "draft76" protocol.
    +
    +        The draft76 version of the websocket protocol is disabled by
    +        default due to security concerns, but it can be enabled by
    +        overriding this method to return True.
    +
    +        Connections using the draft76 protocol do not support the
    +        ``binary=True`` flag to `write_message`.
    +
    +        Support for the draft76 protocol is deprecated and will be
    +        removed in a future version of Tornado.
    +        """
    +        return False
    +
    +    def get_websocket_scheme(self):
    +        """Return the url scheme used for this request, either "ws" or "wss".
    +
    +        This is normally decided by HTTPServer, but applications
    +        may wish to override this if they are using an SSL proxy
    +        that does not provide the X-Scheme header as understood
    +        by HTTPServer.
    +        
    +        Note that this is only used by the draft76 protocol.
    +        """
    +        return "wss" if self.request.protocol == "https" else "ws"
    +
    +    def async_callback(self, callback, *args, **kwargs):
    +        """Wrap callbacks with this if they are used on asynchronous requests.
    +
    +        Catches exceptions properly and closes this WebSocket if an exception
    +        is uncaught.  (Note that this is usually unnecessary thanks to
    +        `tornado.stack_context`)
    +        """
    +        return self.ws_connection.async_callback(callback, *args, **kwargs)
    +
    +    def _not_supported(self, *args, **kwargs):
    +        raise Exception("Method not supported for Web Sockets")
    +
    +    def on_connection_close(self):
    +        if self.ws_connection:
    +            self.ws_connection.on_connection_close()
    +            self.ws_connection = None
    +            self.on_close()
    +
    +
    +for method in ["write", "redirect", "set_header", "send_error", "set_cookie",
    +               "set_status", "flush", "finish"]:
    +    setattr(WebSocketHandler, method, WebSocketHandler._not_supported)
    +
    +
    +class WebSocketProtocol(object):
    +    """Base class for WebSocket protocol versions.
    +    """
    +    def __init__(self, handler):
    +        self.handler = handler
    +        self.request = handler.request
    +        self.stream = handler.stream
    +        self.client_terminated = False
    +        self.server_terminated = False
    +
    +    def async_callback(self, callback, *args, **kwargs):
    +        """Wrap callbacks with this if they are used on asynchronous requests.
    +
    +        Catches exceptions properly and closes this WebSocket if an exception
    +        is uncaught.
    +        """
    +        if args or kwargs:
    +            callback = functools.partial(callback, *args, **kwargs)
    +        def wrapper(*args, **kwargs):
    +            try:
    +                return callback(*args, **kwargs)
    +            except Exception:
    +                logging.error("Uncaught exception in %s",
    +                              self.request.path, exc_info=True)
    +                self._abort()
    +        return wrapper
    +
    +    def on_connection_close(self):
    +        self._abort()
    +
    +    def _abort(self):
    +        """Instantly aborts the WebSocket connection by closing the socket"""
    +        self.client_terminated = True
    +        self.server_terminated = True
    +        self.stream.close()  # forcibly tear down the connection
    +        self.close()  # let the subclass cleanup
    +
    +
    +class WebSocketProtocol76(WebSocketProtocol):
    +    """Implementation of the WebSockets protocol, version hixie-76.
    +
    +    This class provides basic functionality to process WebSockets requests as
    +    specified in
    +    http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
    +    """
    +    def __init__(self, handler):
    +        WebSocketProtocol.__init__(self, handler)
    +        self.challenge = None
    +        self._waiting = None
    +
    +    def accept_connection(self):
    +        try:
    +            self._handle_websocket_headers()
    +        except ValueError:
    +            logging.debug("Malformed WebSocket request received")
    +            self._abort()
    +            return
    +
    +        scheme = self.handler.get_websocket_scheme()
    +
    +        # draft76 only allows a single subprotocol
    +        subprotocol_header = ''
    +        subprotocol = self.request.headers.get("Sec-WebSocket-Protocol", None)
    +        if subprotocol:
    +            selected = self.handler.select_subprotocol([subprotocol])
    +            if selected:
    +                assert selected == subprotocol
    +                subprotocol_header = "Sec-WebSocket-Protocol: %s\r\n" % selected
    +
    +        # Write the initial headers before attempting to read the challenge.
    +        # This is necessary when using proxies (such as HAProxy), which
    +        # need to see the Upgrade headers before passing through the
    +        # non-HTTP traffic that follows.
    +        self.stream.write(tornado.escape.utf8(
    +            "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
    +            "Upgrade: WebSocket\r\n"
    +            "Connection: Upgrade\r\n"
    +            "Server: TornadoServer/%(version)s\r\n"
    +            "Sec-WebSocket-Origin: %(origin)s\r\n"
    +            "Sec-WebSocket-Location: %(scheme)s://%(host)s%(uri)s\r\n"
    +            "%(subprotocol)s"
    +            "\r\n" % (dict(
    +                    version=tornado.version,
    +                    origin=self.request.headers["Origin"],
    +                    scheme=scheme,
    +                    host=self.request.host,
    +                    uri=self.request.uri,
    +                    subprotocol=subprotocol_header))))
    +        self.stream.read_bytes(8, self._handle_challenge)
    +
    +    def challenge_response(self, challenge):
    +        """Generates the challenge response that's needed in the handshake
    +
    +        The challenge parameter should be the raw bytes as sent from the
    +        client.
    +        """
    +        key_1 = self.request.headers.get("Sec-Websocket-Key1")
    +        key_2 = self.request.headers.get("Sec-Websocket-Key2")
    +        try:
    +            part_1 = self._calculate_part(key_1)
    +            part_2 = self._calculate_part(key_2)
    +        except ValueError:
    +            raise ValueError("Invalid Keys/Challenge")
    +        return self._generate_challenge_response(part_1, part_2, challenge)
    +
    +    def _handle_challenge(self, challenge):
    +        try:
    +            challenge_response = self.challenge_response(challenge)
    +        except ValueError:
    +            logging.debug("Malformed key data in WebSocket request")
    +            self._abort()
    +            return
    +        self._write_response(challenge_response)
    +
    +    def _write_response(self, challenge):
    +        self.stream.write(challenge)
    +        self.async_callback(self.handler.open)(*self.handler.open_args, **self.handler.open_kwargs)
    +        self._receive_message()
    +
    +    def _handle_websocket_headers(self):
    +        """Verifies all invariant- and required headers
    +
    +        If a header is missing or have an incorrect value ValueError will be
    +        raised
    +        """
    +        fields = ("Origin", "Host", "Sec-Websocket-Key1",
    +                  "Sec-Websocket-Key2")
    +        if not all(map(lambda f: self.request.headers.get(f), fields)):
    +            raise ValueError("Missing/Invalid WebSocket headers")
    +
    +    def _calculate_part(self, key):
    +        """Processes the key headers and calculates their key value.
    +
    +        Raises ValueError when feed invalid key."""
    +        number = int(''.join(c for c in key if c.isdigit()))
    +        spaces = len([c for c in key if c.isspace()])
    +        try:
    +            key_number = number // spaces
    +        except (ValueError, ZeroDivisionError):
    +            raise ValueError
    +        return struct.pack(">I", key_number)
    +
    +    def _generate_challenge_response(self, part_1, part_2, part_3):
    +        m = hashlib.md5()
    +        m.update(part_1)
    +        m.update(part_2)
    +        m.update(part_3)
    +        return m.digest()
    +
    +    def _receive_message(self):
    +        self.stream.read_bytes(1, self._on_frame_type)
    +
    +    def _on_frame_type(self, byte):
    +        frame_type = ord(byte)
    +        if frame_type == 0x00:
    +            self.stream.read_until(b("\xff"), self._on_end_delimiter)
    +        elif frame_type == 0xff:
    +            self.stream.read_bytes(1, self._on_length_indicator)
    +        else:
    +            self._abort()
    +
    +    def _on_end_delimiter(self, frame):
    +        if not self.client_terminated:
    +            self.async_callback(self.handler.on_message)(
    +                    frame[:-1].decode("utf-8", "replace"))
    +        if not self.client_terminated:
    +            self._receive_message()
    +
    +    def _on_length_indicator(self, byte):
    +        if ord(byte) != 0x00:
    +            self._abort()
    +            return
    +        self.client_terminated = True
    +        self.close()
    +
    +    def write_message(self, message, binary=False):
    +        """Sends the given message to the client of this Web Socket."""
    +        if binary:
    +            raise ValueError(
    +                "Binary messages not supported by this version of websockets")
    +        if isinstance(message, unicode):
    +            message = message.encode("utf-8")
    +        assert isinstance(message, bytes_type)
    +        self.stream.write(b("\x00") + message + b("\xff"))
    +
    +    def close(self):
    +        """Closes the WebSocket connection."""
    +        if not self.server_terminated:
    +            if not self.stream.closed():
    +                self.stream.write("\xff\x00")
    +            self.server_terminated = True
    +        if self.client_terminated:
    +            if self._waiting is not None:
    +                self.stream.io_loop.remove_timeout(self._waiting)
    +            self._waiting = None
    +            self.stream.close()
    +        elif self._waiting is None:
    +            self._waiting = self.stream.io_loop.add_timeout(
    +                time.time() + 5, self._abort)
    +
    +
    +class WebSocketProtocol13(WebSocketProtocol):
    +    """Implementation of the WebSocket protocol from RFC 6455.
    +
    +    This class supports versions 7 and 8 of the protocol in addition to the
    +    final version 13.
    +    """
    +    def __init__(self, handler):
    +        WebSocketProtocol.__init__(self, handler)
    +        self._final_frame = False
    +        self._frame_opcode = None
    +        self._frame_mask = None
    +        self._frame_length = None
    +        self._fragmented_message_buffer = None
    +        self._fragmented_message_opcode = None
    +        self._waiting = None
    +
    +    def accept_connection(self):
    +        try:
    +            self._handle_websocket_headers()
    +            self._accept_connection()
    +        except ValueError:
    +            logging.debug("Malformed WebSocket request received")
    +            self._abort()
    +            return
    +
    +    def _handle_websocket_headers(self):
    +        """Verifies all invariant- and required headers
    +
    +        If a header is missing or have an incorrect value ValueError will be
    +        raised
    +        """
    +        fields = ("Host", "Sec-Websocket-Key", "Sec-Websocket-Version")
    +        if not all(map(lambda f: self.request.headers.get(f), fields)):
    +            raise ValueError("Missing/Invalid WebSocket headers")
    +
    +    def _challenge_response(self):
    +        sha1 = hashlib.sha1()
    +        sha1.update(tornado.escape.utf8(
    +                self.request.headers.get("Sec-Websocket-Key")))
    +        sha1.update(b("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")) # Magic value
    +        return tornado.escape.native_str(base64.b64encode(sha1.digest()))
    +
    +    def _accept_connection(self):
    +        subprotocol_header = ''
    +        subprotocols = self.request.headers.get("Sec-WebSocket-Protocol", '')
    +        subprotocols = [s.strip() for s in subprotocols.split(',')]
    +        if subprotocols:
    +            selected = self.handler.select_subprotocol(subprotocols)
    +            if selected:
    +                assert selected in subprotocols
    +                subprotocol_header = "Sec-WebSocket-Protocol: %s\r\n" % selected
    +
    +        self.stream.write(tornado.escape.utf8(
    +            "HTTP/1.1 101 Switching Protocols\r\n"
    +            "Upgrade: websocket\r\n"
    +            "Connection: Upgrade\r\n"
    +            "Sec-WebSocket-Accept: %s\r\n"
    +            "%s"
    +            "\r\n" % (self._challenge_response(), subprotocol_header)))
    +
    +        self.async_callback(self.handler.open)(*self.handler.open_args, **self.handler.open_kwargs)
    +        self._receive_frame()
    +
    +    def _write_frame(self, fin, opcode, data):
    +        if fin:
    +            finbit = 0x80
    +        else:
    +            finbit = 0
    +        frame = struct.pack("B", finbit | opcode)
    +        l = len(data)
    +        if l < 126:
    +            frame += struct.pack("B", l)
    +        elif l <= 0xFFFF:
    +            frame += struct.pack("!BH", 126, l)
    +        else:
    +            frame += struct.pack("!BQ", 127, l)
    +        frame += data
    +        self.stream.write(frame)
    +
    +    def write_message(self, message, binary=False):
    +        """Sends the given message to the client of this Web Socket."""
    +        if binary:
    +            opcode = 0x2
    +        else:
    +            opcode = 0x1
    +        message = tornado.escape.utf8(message)
    +        assert isinstance(message, bytes_type)
    +        self._write_frame(True, opcode, message)
    +
    +    def _receive_frame(self):
    +        self.stream.read_bytes(2, self._on_frame_start)
    +
    +    def _on_frame_start(self, data):
    +        header, payloadlen = struct.unpack("BB", data)
    +        self._final_frame = header & 0x80
    +        reserved_bits = header & 0x70
    +        self._frame_opcode = header & 0xf
    +        self._frame_opcode_is_control = self._frame_opcode & 0x8
    +        if reserved_bits:
    +            # client is using as-yet-undefined extensions; abort
    +            self._abort()
    +            return
    +        if not (payloadlen & 0x80):
    +            # Unmasked frame -> abort connection
    +            self._abort()
    +            return
    +        payloadlen = payloadlen & 0x7f
    +        if self._frame_opcode_is_control and payloadlen >= 126:
    +            # control frames must have payload < 126
    +            self._abort()
    +            return
    +        if payloadlen < 126:
    +            self._frame_length = payloadlen
    +            self.stream.read_bytes(4, self._on_masking_key)
    +        elif payloadlen == 126:
    +            self.stream.read_bytes(2, self._on_frame_length_16)
    +        elif payloadlen == 127:
    +            self.stream.read_bytes(8, self._on_frame_length_64)
    +
    +    def _on_frame_length_16(self, data):
    +        self._frame_length = struct.unpack("!H", data)[0];
    +        self.stream.read_bytes(4, self._on_masking_key);
    +
    +    def _on_frame_length_64(self, data):
    +        self._frame_length = struct.unpack("!Q", data)[0];
    +        self.stream.read_bytes(4, self._on_masking_key);
    +
    +    def _on_masking_key(self, data):
    +        self._frame_mask = array.array("B", data)
    +        self.stream.read_bytes(self._frame_length, self._on_frame_data)
    +
    +    def _on_frame_data(self, data):
    +        unmasked = array.array("B", data)
    +        for i in xrange(len(data)):
    +            unmasked[i] = unmasked[i] ^ self._frame_mask[i % 4]
    +
    +        if self._frame_opcode_is_control:
    +            # control frames may be interleaved with a series of fragmented
    +            # data frames, so control frames must not interact with
    +            # self._fragmented_*
    +            if not self._final_frame:
    +                # control frames must not be fragmented
    +                self._abort()
    +                return
    +            opcode = self._frame_opcode
    +        elif self._frame_opcode == 0:  # continuation frame
    +            if self._fragmented_message_buffer is None:
    +                # nothing to continue
    +                self._abort()
    +                return
    +            self._fragmented_message_buffer += unmasked
    +            if self._final_frame:
    +                opcode = self._fragmented_message_opcode
    +                unmasked = self._fragmented_message_buffer
    +                self._fragmented_message_buffer = None
    +        else:  # start of new data message
    +            if self._fragmented_message_buffer is not None:
    +                # can't start new message until the old one is finished
    +                self._abort()
    +                return
    +            if self._final_frame:
    +                opcode = self._frame_opcode
    +            else:
    +                self._fragmented_message_opcode = self._frame_opcode
    +                self._fragmented_message_buffer = unmasked
    +
    +        if self._final_frame:
    +            self._handle_message(opcode, unmasked.tostring())
    +
    +        if not self.client_terminated:
    +            self._receive_frame()
    +
    +
    +    def _handle_message(self, opcode, data):
    +        if self.client_terminated: return
    +
    +        if opcode == 0x1:
    +            # UTF-8 data
    +            try:
    +                decoded = data.decode("utf-8")
    +            except UnicodeDecodeError:
    +                self._abort()
    +                return
    +            self.async_callback(self.handler.on_message)(decoded)
    +        elif opcode == 0x2:
    +            # Binary data
    +            self.async_callback(self.handler.on_message)(data)
    +        elif opcode == 0x8:
    +            # Close
    +            self.client_terminated = True
    +            self.close()
    +        elif opcode == 0x9:
    +            # Ping
    +            self._write_frame(True, 0xA, data)
    +        elif opcode == 0xA:
    +            # Pong
    +            pass
    +        else:
    +            self._abort()
    +
    +    def close(self):
    +        """Closes the WebSocket connection."""
    +        if not self.server_terminated:
    +            if not self.stream.closed():
    +                self._write_frame(True, 0x8, b(""))
    +            self.server_terminated = True
    +        if self.client_terminated:
    +            if self._waiting is not None:
    +                self.stream.io_loop.remove_timeout(self._waiting)
    +                self._waiting = None
    +            self.stream.close()
    +        elif self._waiting is None:
    +            # Give the client a few seconds to complete a clean shutdown,
    +            # otherwise just close the connection.
    +            self._waiting = self.stream.io_loop.add_timeout(
    +                time.time() + 5, self._abort)
    diff --git a/libs/tornado/wsgi.py b/libs/tornado/wsgi.py
    new file mode 100644
    index 00000000..e8f878bb
    --- /dev/null
    +++ b/libs/tornado/wsgi.py
    @@ -0,0 +1,296 @@
    +#!/usr/bin/env python
    +#
    +# Copyright 2009 Facebook
    +#
    +# Licensed under the Apache License, Version 2.0 (the "License"); you may
    +# not use this file except in compliance with the License. You may obtain
    +# a copy of the License at
    +#
    +#     http://www.apache.org/licenses/LICENSE-2.0
    +#
    +# Unless required by applicable law or agreed to in writing, software
    +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    +# License for the specific language governing permissions and limitations
    +# under the License.
    +
    +"""WSGI support for the Tornado web framework.
    +
    +WSGI is the Python standard for web servers, and allows for interoperability
    +between Tornado and other Python web frameworks and servers.  This module
    +provides WSGI support in two ways:
    +
    +* `WSGIApplication` is a version of `tornado.web.Application` that can run 
    +  inside a WSGI server.  This is useful for running a Tornado app on another
    +  HTTP server, such as Google App Engine.  See the `WSGIApplication` class
    +  documentation for limitations that apply.
    +* `WSGIContainer` lets you run other WSGI applications and frameworks on the
    +  Tornado HTTP server.  For example, with this class you can mix Django
    +  and Tornado handlers in a single server.
    +"""
    +
    +import Cookie
    +import cgi
    +import httplib
    +import logging
    +import sys
    +import time
    +import tornado
    +import urllib
    +
    +from tornado import escape
    +from tornado import httputil
    +from tornado import web
    +from tornado.escape import native_str, utf8
    +from tornado.util import b
    +
    +try:
    +    from io import BytesIO  # python 3
    +except ImportError:
    +    from cStringIO import StringIO as BytesIO  # python 2
    +
    +class WSGIApplication(web.Application):
    +    """A WSGI equivalent of `tornado.web.Application`.
    +
    +    WSGIApplication is very similar to web.Application, except no
    +    asynchronous methods are supported (since WSGI does not support
    +    non-blocking requests properly). If you call self.flush() or other
    +    asynchronous methods in your request handlers running in a
    +    WSGIApplication, we throw an exception.
    +
    +    Example usage::
    +
    +        import tornado.web
    +        import tornado.wsgi
    +        import wsgiref.simple_server
    +
    +        class MainHandler(tornado.web.RequestHandler):
    +            def get(self):
    +                self.write("Hello, world")
    +
    +        if __name__ == "__main__":
    +            application = tornado.wsgi.WSGIApplication([
    +                (r"/", MainHandler),
    +            ])
    +            server = wsgiref.simple_server.make_server('', 8888, application)
    +            server.serve_forever()
    +
    +    See the 'appengine' demo for an example of using this module to run
    +    a Tornado app on Google AppEngine.
    +
    +    Since no asynchronous methods are available for WSGI applications, the
    +    httpclient and auth modules are both not available for WSGI applications.
    +    We support the same interface, but handlers running in a WSGIApplication
    +    do not support flush() or asynchronous methods. 
    +    """
    +    def __init__(self, handlers=None, default_host="", **settings):
    +        web.Application.__init__(self, handlers, default_host, transforms=[],
    +                                 wsgi=True, **settings)
    +
    +    def __call__(self, environ, start_response):
    +        handler = web.Application.__call__(self, HTTPRequest(environ))
    +        assert handler._finished
    +        status = str(handler._status_code) + " " + \
    +            httplib.responses[handler._status_code]
    +        headers = handler._headers.items()
    +        for cookie_dict in getattr(handler, "_new_cookies", []):
    +            for cookie in cookie_dict.values():
    +                headers.append(("Set-Cookie", cookie.OutputString(None)))
    +        start_response(status,
    +                       [(native_str(k), native_str(v)) for (k,v) in headers])
    +        return handler._write_buffer
    +
    +
    +class HTTPRequest(object):
    +    """Mimics `tornado.httpserver.HTTPRequest` for WSGI applications."""
    +    def __init__(self, environ):
    +        """Parses the given WSGI environ to construct the request."""
    +        self.method = environ["REQUEST_METHOD"]
    +        self.path = urllib.quote(environ.get("SCRIPT_NAME", ""))
    +        self.path += urllib.quote(environ.get("PATH_INFO", ""))
    +        self.uri = self.path
    +        self.arguments = {}
    +        self.query = environ.get("QUERY_STRING", "")
    +        if self.query:
    +            self.uri += "?" + self.query
    +            arguments = cgi.parse_qs(self.query)
    +            for name, values in arguments.iteritems():
    +                values = [v for v in values if v]
    +                if values: self.arguments[name] = values
    +        self.version = "HTTP/1.1"
    +        self.headers = httputil.HTTPHeaders()
    +        if environ.get("CONTENT_TYPE"):
    +            self.headers["Content-Type"] = environ["CONTENT_TYPE"]
    +        if environ.get("CONTENT_LENGTH"):
    +            self.headers["Content-Length"] = environ["CONTENT_LENGTH"]
    +        for key in environ:
    +            if key.startswith("HTTP_"):
    +                self.headers[key[5:].replace("_", "-")] = environ[key]
    +        if self.headers.get("Content-Length"):
    +            self.body = environ["wsgi.input"].read(
    +                int(self.headers["Content-Length"]))
    +        else:
    +            self.body = ""
    +        self.protocol = environ["wsgi.url_scheme"]
    +        self.remote_ip = environ.get("REMOTE_ADDR", "")
    +        if environ.get("HTTP_HOST"):
    +            self.host = environ["HTTP_HOST"]
    +        else:
    +            self.host = environ["SERVER_NAME"]
    +
    +        # Parse request body
    +        self.files = {}
    +        content_type = self.headers.get("Content-Type", "")
    +        if content_type.startswith("application/x-www-form-urlencoded"):
    +            for name, values in cgi.parse_qs(self.body).iteritems():
    +                self.arguments.setdefault(name, []).extend(values)
    +        elif content_type.startswith("multipart/form-data"):
    +            if 'boundary=' in content_type:
    +                boundary = content_type.split('boundary=',1)[1]
    +                if boundary:
    +                    httputil.parse_multipart_form_data(
    +                        utf8(boundary), self.body, self.arguments, self.files)
    +            else:
    +                logging.warning("Invalid multipart/form-data")
    +
    +        self._start_time = time.time()
    +        self._finish_time = None
    +
    +    def supports_http_1_1(self):
    +        """Returns True if this request supports HTTP/1.1 semantics"""
    +        return self.version == "HTTP/1.1"
    +
    +    @property
    +    def cookies(self):
    +        """A dictionary of Cookie.Morsel objects."""
    +        if not hasattr(self, "_cookies"):
    +            self._cookies = Cookie.SimpleCookie()
    +            if "Cookie" in self.headers:
    +                try:
    +                    self._cookies.load(
    +                        native_str(self.headers["Cookie"]))
    +                except Exception:
    +                    self._cookies = None
    +        return self._cookies
    +
    +    def full_url(self):
    +        """Reconstructs the full URL for this request."""
    +        return self.protocol + "://" + self.host + self.uri
    +
    +    def request_time(self):
    +        """Returns the amount of time it took for this request to execute."""
    +        if self._finish_time is None:
    +            return time.time() - self._start_time
    +        else:
    +            return self._finish_time - self._start_time
    +
    +
    +class WSGIContainer(object):
    +    r"""Makes a WSGI-compatible function runnable on Tornado's HTTP server.
    +
    +    Wrap a WSGI function in a WSGIContainer and pass it to HTTPServer to
    +    run it. For example::
    +
    +        def simple_app(environ, start_response):
    +            status = "200 OK"
    +            response_headers = [("Content-type", "text/plain")]
    +            start_response(status, response_headers)
    +            return ["Hello world!\n"]
    +
    +        container = tornado.wsgi.WSGIContainer(simple_app)
    +        http_server = tornado.httpserver.HTTPServer(container)
    +        http_server.listen(8888)
    +        tornado.ioloop.IOLoop.instance().start()
    +
    +    This class is intended to let other frameworks (Django, web.py, etc)
    +    run on the Tornado HTTP server and I/O loop.
    +
    +    The `tornado.web.FallbackHandler` class is often useful for mixing
    +    Tornado and WSGI apps in the same server.  See
    +    https://github.com/bdarnell/django-tornado-demo for a complete example.
    +    """
    +    def __init__(self, wsgi_application):
    +        self.wsgi_application = wsgi_application
    +
    +    def __call__(self, request):
    +        data = {}
    +        response = []
    +        def start_response(status, response_headers, exc_info=None):
    +            data["status"] = status
    +            data["headers"] = response_headers
    +            return response.append
    +        app_response = self.wsgi_application(
    +            WSGIContainer.environ(request), start_response)
    +        response.extend(app_response)
    +        body = b("").join(response)
    +        if hasattr(app_response, "close"):
    +            app_response.close()
    +        if not data: raise Exception("WSGI app did not call start_response")
    +
    +        status_code = int(data["status"].split()[0])
    +        headers = data["headers"]
    +        header_set = set(k.lower() for (k,v) in headers)
    +        body = escape.utf8(body)
    +        if "content-length" not in header_set:
    +            headers.append(("Content-Length", str(len(body))))
    +        if "content-type" not in header_set:
    +            headers.append(("Content-Type", "text/html; charset=UTF-8"))
    +        if "server" not in header_set:
    +            headers.append(("Server", "TornadoServer/%s" % tornado.version))
    +
    +        parts = [escape.utf8("HTTP/1.1 " + data["status"] + "\r\n")]
    +        for key, value in headers:
    +            parts.append(escape.utf8(key) + b(": ") + escape.utf8(value) + b("\r\n"))
    +        parts.append(b("\r\n"))
    +        parts.append(body)
    +        request.write(b("").join(parts))
    +        request.finish()
    +        self._log(status_code, request)
    +
    +    @staticmethod
    +    def environ(request):
    +        """Converts a `tornado.httpserver.HTTPRequest` to a WSGI environment.
    +        """
    +        hostport = request.host.split(":")
    +        if len(hostport) == 2:
    +            host = hostport[0]
    +            port = int(hostport[1])
    +        else:
    +            host = request.host
    +            port = 443 if request.protocol == "https" else 80
    +        environ = {
    +            "REQUEST_METHOD": request.method,
    +            "SCRIPT_NAME": "",
    +            "PATH_INFO": urllib.unquote(request.path),
    +            "QUERY_STRING": request.query,
    +            "REMOTE_ADDR": request.remote_ip,
    +            "SERVER_NAME": host,
    +            "SERVER_PORT": str(port),
    +            "SERVER_PROTOCOL": request.version,
    +            "wsgi.version": (1, 0),
    +            "wsgi.url_scheme": request.protocol,
    +            "wsgi.input": BytesIO(escape.utf8(request.body)),
    +            "wsgi.errors": sys.stderr,
    +            "wsgi.multithread": False,
    +            "wsgi.multiprocess": True,
    +            "wsgi.run_once": False,
    +        }
    +        if "Content-Type" in request.headers:
    +            environ["CONTENT_TYPE"] = request.headers.pop("Content-Type")
    +        if "Content-Length" in request.headers:
    +            environ["CONTENT_LENGTH"] = request.headers.pop("Content-Length")
    +        for key, value in request.headers.iteritems():
    +            environ["HTTP_" + key.replace("-", "_").upper()] = value
    +        return environ
    +
    +    def _log(self, status_code, request):
    +        if status_code < 400:
    +            log_method = logging.info
    +        elif status_code < 500:
    +            log_method = logging.warning
    +        else:
    +            log_method = logging.error
    +        request_time = 1000.0 * request.request_time()
    +        summary = request.method + " " + request.uri + " (" + \
    +            request.remote_ip + ")"
    +        log_method("%d %s %.2fms", status_code, summary, request_time)
    
    From 2393b43ebf69ab1fd8b939191e78f14071a01aed Mon Sep 17 00:00:00 2001
    From: Ruud 
    Date: Fri, 18 May 2012 22:23:59 +0200
    Subject: [PATCH 011/207] Try and catch newznab limits. fixes #292 #264
    
    ---
     .../core/downloaders/blackhole/main.py        | 10 +++----
     couchpotato/core/downloaders/nzbget/main.py   | 19 ++++--------
     couchpotato/core/downloaders/sabnzbd/main.py  | 14 ++++-----
     .../core/downloaders/transmission/main.py     |  6 ++--
     couchpotato/core/plugins/searcher/main.py     | 15 ++++++++--
     .../core/providers/nzb/newznab/main.py        | 29 +++++++++++++++++++
     6 files changed, 60 insertions(+), 33 deletions(-)
    
    diff --git a/couchpotato/core/downloaders/blackhole/main.py b/couchpotato/core/downloaders/blackhole/main.py
    index a24e71b1..c12c8c6e 100644
    --- a/couchpotato/core/downloaders/blackhole/main.py
    +++ b/couchpotato/core/downloaders/blackhole/main.py
    @@ -10,7 +10,7 @@ class Blackhole(Downloader):
     
         type = ['nzb', 'torrent']
     
    -    def download(self, data = {}, movie = {}, manual = False):
    +    def download(self, data = {}, movie = {}, manual = False, filedata = None):
             if self.isDisabled(manual) or (not self.isCorrectType(data.get('type')) or (not self.conf('use_for') in ['both', data.get('type')])):
                 return
     
    @@ -19,10 +19,8 @@ class Blackhole(Downloader):
                 log.error('No directory set for blackhole %s download.' % data.get('type'))
             else:
                 try:
    -                filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
    -
    -                if len(filedata) < 50:
    -                    log.error('No nzb available!')
    +                if not filedata or len(filedata) < 50:
    +                    log.error('No nzb/torrent available!')
                         return False
     
                     fullPath = os.path.join(directory, self.createFileName(data, filedata, movie))
    @@ -42,6 +40,6 @@ class Blackhole(Downloader):
                         pass
     
                 except:
    -                log.debug('Failed to download file %s: %s' % (data.get('name'), traceback.format_exc()))
    +                log.info('Failed to download file %s: %s' % (data.get('name'), traceback.format_exc()))
                     return False
             return False
    diff --git a/couchpotato/core/downloaders/nzbget/main.py b/couchpotato/core/downloaders/nzbget/main.py
    index 15243ca4..904a3a4e 100644
    --- a/couchpotato/core/downloaders/nzbget/main.py
    +++ b/couchpotato/core/downloaders/nzbget/main.py
    @@ -14,11 +14,15 @@ class NZBGet(Downloader):
     
         url = 'http://nzbget:%(password)s@%(host)s/xmlrpc'
     
    -    def download(self, data = {}, movie = {}, manual = False):
    +    def download(self, data = {}, movie = {}, manual = False, filedata = None):
     
             if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
                 return
     
    +        if not filedata:
    +            log.error('Unable to get NZB file: %s' % traceback.format_exc())
    +            return False
    +
             log.info('Sending "%s" to NZBGet.' % data.get('name'))
     
             url = self.url % {'host': self.conf('host'), 'password': self.conf('password')}
    @@ -40,19 +44,6 @@ class NZBGet(Downloader):
                     log.error('Protocol Error: %s' % e)
                 return False
     
    -        try:
    -            if isfunction(data.get('download')):
    -                filedata = data.get('download')()
    -                if not filedata:
    -                    log.error('Failed download file: %s' % nzb_name)
    -                    return False
    -            else:
    -                log.info('Downloading: %s' % data.get('url'))
    -                filedata = self.urlopen(data.get('url'))
    -        except:
    -            log.error('Unable to get NZB file: %s' % traceback.format_exc())
    -            return False
    -
             if rpc.append(nzb_name, self.conf('category'), False, standard_b64encode(filedata.strip())):
                 log.info('NZB sent successfully to NZBGet')
                 return True
    diff --git a/couchpotato/core/downloaders/sabnzbd/main.py b/couchpotato/core/downloaders/sabnzbd/main.py
    index 5505438e..d6c5af6c 100644
    --- a/couchpotato/core/downloaders/sabnzbd/main.py
    +++ b/couchpotato/core/downloaders/sabnzbd/main.py
    @@ -15,7 +15,7 @@ class Sabnzbd(Downloader):
     
         type = ['nzb']
     
    -    def download(self, data = {}, movie = {}, manual = False):
    +    def download(self, data = {}, movie = {}, manual = False, filedata = None):
     
             if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
                 return
    @@ -42,15 +42,13 @@ class Sabnzbd(Downloader):
                 'nzbname': self.createNzbName(data, movie),
             }
     
    -        if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))):
    -            nzb_file = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
    -
    -            if not nzb_file or len(nzb_file) < 50:
    -                log.error('No nzb available!')
    +        if filedata:
    +            if len(filedata) < 50:
    +                log.error('No proper nzb available!')
                     return False
     
                 # If it's a .rar, it adds the .rar extension, otherwise it stays .nzb
    -            nzb_filename = self.createFileName(data, nzb_file, movie)
    +            nzb_filename = self.createFileName(data, filedata, movie)
                 params['mode'] = 'addfile'
             else:
                 params['name'] = data.get('url')
    @@ -62,7 +60,7 @@ class Sabnzbd(Downloader):
     
             try:
                 if params.get('mode') is 'addfile':
    -                data = self.urlopen(url, params = {"nzbfile": (nzb_filename, nzb_file)}, multipart = True, show_error = False)
    +                data = self.urlopen(url, params = {"nzbfile": (nzb_filename, filedata)}, multipart = True, show_error = False)
                 else:
                     data = self.urlopen(url, show_error = False)
             except:
    diff --git a/couchpotato/core/downloaders/transmission/main.py b/couchpotato/core/downloaders/transmission/main.py
    index 4ed3e583..55e4e305 100644
    --- a/couchpotato/core/downloaders/transmission/main.py
    +++ b/couchpotato/core/downloaders/transmission/main.py
    @@ -11,7 +11,7 @@ class Transmission(Downloader):
     
         type = ['torrent']
     
    -    def download(self, data = {}, movie = {}, manual = False):
    +    def download(self, data = {}, movie = {}, manual = False, filedata = None):
     
             if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
                 return
    @@ -31,8 +31,10 @@ class Transmission(Downloader):
             }
     
             try:
    +            if not filedata:
    +                log.error('Failed sending torrent to transmission, no data')
    +
                 tc = transmissionrpc.Client(host[0], port = host[1], user = self.conf('username'), password = self.conf('password'))
    -            filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
                 torrent = tc.add_torrent(b64encode(filedata), **params)
     
                 # Change settings of added torrents
    diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py
    index e2933462..57e1f37c 100644
    --- a/couchpotato/core/plugins/searcher/main.py
    +++ b/couchpotato/core/plugins/searcher/main.py
    @@ -6,6 +6,7 @@ from couchpotato.core.logger import CPLog
     from couchpotato.core.plugins.base import Plugin
     from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
     from couchpotato.environment import Env
    +from inspect import ismethod, isfunction
     from sqlalchemy.exc import InterfaceError
     import datetime
     import re
    @@ -142,9 +143,9 @@ class Searcher(Plugin):
     
                     for nzb in sorted_results:
                         downloaded = self.download(data = nzb, movie = movie)
    -                    if downloaded:
    +                    if downloaded is True:
                             return True
    -                    else:
    +                    elif downloaded != 'try_next':
                             break
                 else:
                     log.info('Better quality (%s) already available or snatched for %s' % (quality_type['quality']['label'], default_title))
    @@ -161,7 +162,15 @@ class Searcher(Plugin):
         def download(self, data, movie, manual = False):
     
             snatched_status = fireEvent('status.get', 'snatched', single = True)
    -        successful = fireEvent('download', data = data, movie = movie, manual = manual, single = True)
    +
    +        # Download movie to temp
    +        filedata = None
    +        if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))):
    +            filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
    +            if filedata is 'try_next':
    +                return filedata
    +
    +        successful = fireEvent('download', data = data, movie = movie, manual = manual, single = True, filedata = filedata)
     
             if successful:
     
    diff --git a/couchpotato/core/providers/nzb/newznab/main.py b/couchpotato/core/providers/nzb/newznab/main.py
    index 0d648a95..8b959b54 100644
    --- a/couchpotato/core/providers/nzb/newznab/main.py
    +++ b/couchpotato/core/providers/nzb/newznab/main.py
    @@ -6,7 +6,10 @@ from couchpotato.core.logger import CPLog
     from couchpotato.core.providers.nzb.base import NZBProvider
     from couchpotato.environment import Env
     from dateutil.parser import parse
    +from urllib2 import HTTPError
    +from urlparse import urlparse
     import time
    +import traceback
     import xml.etree.ElementTree as XMLTree
     
     log = CPLog(__name__)
    @@ -20,6 +23,8 @@ class Newznab(NZBProvider, RSS):
             'search': 'movie',
         }
     
    +    limits_reached = {}
    +
         cat_ids = [
             ([2010], ['dvdr']),
             ([2030], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']),
    @@ -194,3 +199,27 @@ class Newznab(NZBProvider, RSS):
     
         def getApiExt(self, host):
             return '&apikey=%s' % host['api_key']
    +
    +    def download(self, url = '', nzb_id = ''):
    +        host = urlparse(url).hostname
    +
    +        if self.limits_reached.get(host):
    +            # Try again in 3 hours
    +            if self.limits_reached[host] > time.time() - 10800:
    +                return 'try_next'
    +
    +        try:
    +            data = self.urlopen(url, show_error = False)
    +            self.limits_reached[host] = False
    +            return data
    +        except HTTPError, e:
    +            if e.code == 503:
    +                response = e.read().lower()
    +                if 'maximum api' in response or 'download limit' in response:
    +                    if not self.limits_reached.get(host):
    +                        log.error('Limit reached for newznab provider: %s' % host)
    +                    self.limits_reached[host] = time.time()
    +                    return 'try_next'
    +
    +            log.error('Failed download from %s' % (host, traceback.format_exc()))
    +            raise
    
    From 442c4e5aead552919f2d49f273d9ff3a48c9703a Mon Sep 17 00:00:00 2001
    From: Ruud 
    Date: Fri, 18 May 2012 22:54:13 +0200
    Subject: [PATCH 012/207] Don't use fanart that doesn't exist. fix #282
    
    ---
     couchpotato/core/providers/metadata/base.py | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/couchpotato/core/providers/metadata/base.py b/couchpotato/core/providers/metadata/base.py
    index 0f59c9f5..d7e9e035 100644
    --- a/couchpotato/core/providers/metadata/base.py
    +++ b/couchpotato/core/providers/metadata/base.py
    @@ -75,7 +75,7 @@ class MetaDataBase(Plugin):
                     break
     
             for cur_file in data['library'].get('files', []):
    -            if cur_file.get('type_id') is file_type.get('id'):
    +            if cur_file.get('type_id') is file_type.get('id') and os.path.isfile(cur_file.get('path')):
                     return cur_file.get('path')
     
         def getFanart(self, movie_info = {}, data = {}):
    
    From 8d76a1d5e91ad6eda4642c3e6db5fe4aefeea409 Mon Sep 17 00:00:00 2001
    From: Ruud 
    Date: Fri, 18 May 2012 23:16:14 +0200
    Subject: [PATCH 013/207] KAT missing http in url. fix #293
    
    ---
     couchpotato/core/providers/torrent/kickasstorrents/main.py | 2 ++
     1 file changed, 2 insertions(+)
    
    diff --git a/couchpotato/core/providers/torrent/kickasstorrents/main.py b/couchpotato/core/providers/torrent/kickasstorrents/main.py
    index 0f386043..4fd7b760 100644
    --- a/couchpotato/core/providers/torrent/kickasstorrents/main.py
    +++ b/couchpotato/core/providers/torrent/kickasstorrents/main.py
    @@ -77,6 +77,8 @@ class KickAssTorrents(TorrentProvider):
                                                 new['id'] = temp.get('id')[-8:]
                                                 new['name'] = link.text
                                                 new['url'] = td.findAll('a', 'idownload')[1]['href']
    +                                            if new['url'][:2] == '//':
    +                                                new['url'] = 'http:%s' % new['url']
                                                 new['score'] = 20 if td.find('a', 'iverif') else 0
                                             elif column_name is 'size':
                                                 new['size'] = self.parseSize(td.text)
    
    From eb48a72a907988900d2f410320638642c5cfe94e Mon Sep 17 00:00:00 2001
    From: Ruud 
    Date: Fri, 18 May 2012 23:35:05 +0200
    Subject: [PATCH 014/207] Case insensitive ignored words. fix #284
    
    ---
     couchpotato/core/plugins/searcher/main.py | 14 ++++++++------
     1 file changed, 8 insertions(+), 6 deletions(-)
    
    diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py
    index 57e1f37c..7983f928 100644
    --- a/couchpotato/core/plugins/searcher/main.py
    +++ b/couchpotato/core/plugins/searcher/main.py
    @@ -223,15 +223,17 @@ class Searcher(Plugin):
                 log.info('Wrong: Outside retention, age is %s, needs %s or lower: %s' % (nzb['age'], retention, nzb['name']))
                 return False
     
    -        movie_name = simplifyString(nzb['name'])
    -        nzb_words = re.split('\W+', movie_name)
    -        required_words = [x.strip() for x in self.conf('required_words').split(',')]
    +        movie_name = getTitle(movie['library'])
    +        movie_words = re.split('\W+', simplifyString(movie_name))
    +        nzb_name = simplifyString(nzb['name'])
    +        nzb_words = re.split('\W+', nzb_name)
    +        required_words = [x.strip().lower() for x in self.conf('required_words').lower().split(',')]
     
             if self.conf('required_words') and not list(set(nzb_words) & set(required_words)):
                 log.info("NZB doesn't contain any of the required words.")
                 return False
     
    -        ignored_words = [x.strip() for x in self.conf('ignored_words').split(',')]
    +        ignored_words = [x.strip().lower() for x in self.conf('ignored_words').split(',')]
             blacklisted = list(set(nzb_words) & set(ignored_words))
             if self.conf('ignored_words') and blacklisted:
                 log.info("Wrong: '%s' blacklisted words: %s" % (nzb['name'], ", ".join(blacklisted)))
    @@ -239,7 +241,7 @@ class Searcher(Plugin):
     
             pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs']
             for p_tag in pron_tags:
    -            if p_tag in nzb_words and p_tag not in movie_name:
    +            if p_tag in nzb_words and p_tag not in movie_words:
                     log.info('Wrong: %s, probably pr0n' % (nzb['name']))
                     return False
     
    @@ -286,7 +288,7 @@ class Searcher(Plugin):
             if self.checkNFO(nzb['name'], movie['library']['identifier']):
                 return True
     
    -        log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], getTitle(movie['library']), movie['library']['year']))
    +        log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], movie_name, movie['library']['year']))
             return False
     
         def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = {}, single_category = False):
    
    From b59861c22a78e520e40f2113d3a25ac85c97d4df Mon Sep 17 00:00:00 2001
    From: Ruud 
    Date: Sat, 19 May 2012 00:19:59 +0200
    Subject: [PATCH 015/207] Normalize folder paths. fix #275
    
    ---
     couchpotato/core/plugins/scanner/main.py | 6 ++++++
     1 file changed, 6 insertions(+)
    
    diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py
    index 136a1964..0a220bc1 100644
    --- a/couchpotato/core/plugins/scanner/main.py
    +++ b/couchpotato/core/plugins/scanner/main.py
    @@ -95,6 +95,8 @@ class Scanner(Plugin):
     
         def scanFilesToLibrary(self, folder = None, files = None):
     
    +        folder = os.path.normpath(folder)
    +
             groups = self.scan(folder = folder, files = files)
     
             for group in groups.itervalues():
    @@ -103,6 +105,8 @@ class Scanner(Plugin):
     
         def scanFolderToLibrary(self, folder = None, newer_than = None, simple = True):
     
    +        folder = os.path.normpath(folder)
    +
             if not os.path.isdir(folder):
                 return
     
    @@ -129,6 +133,8 @@ class Scanner(Plugin):
     
         def scan(self, folder = None, files = [], simple = False):
     
    +        folder = os.path.normpath(folder)
    +
             if not folder or not os.path.isdir(folder):
                 log.error('Folder doesn\'t exists: %s' % folder)
                 return {}
    
    From 63f46fae757227de79c4b47ab063ba1a6b27c790 Mon Sep 17 00:00:00 2001
    From: Ruud 
    Date: Sat, 19 May 2012 00:45:28 +0200
    Subject: [PATCH 016/207] Extra check for imdb file. close #273
    
    ---
     couchpotato/core/helpers/variable.py     |  4 ++--
     couchpotato/core/plugins/scanner/main.py | 12 +++++++++++-
     2 files changed, 13 insertions(+), 3 deletions(-)
    
    diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py
    index 29e4bf4d..bf5b2362 100644
    --- a/couchpotato/core/helpers/variable.py
    +++ b/couchpotato/core/helpers/variable.py
    @@ -77,9 +77,9 @@ def cleanHost(host):
     
         return host
     
    -def getImdb(txt):
    +def getImdb(txt, check_inside = True):
     
    -    if os.path.isfile(txt):
    +    if check_inside and os.path.isfile(txt):
             output = open(txt, 'r')
             txt = output.read()
             output.close()
    diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py
    index 0a220bc1..76e243ee 100644
    --- a/couchpotato/core/plugins/scanner/main.py
    +++ b/couchpotato/core/plugins/scanner/main.py
    @@ -443,7 +443,7 @@ class Scanner(Plugin):
                     log.debug('Found movie via CP tag: %s' % cur_file)
                     break
     
    -        # Check and see if nfo contains the imdb-id
    +        # Check and see if nfo or filenames contains the imdb-id
             if not imdb_id:
                 try:
                     for nfo_file in files['nfo']:
    @@ -454,6 +454,16 @@ class Scanner(Plugin):
                 except:
                     pass
     
    +            try:
    +                for filetype in files:
    +                    for filetype_file in files[filetype]:
    +                        imdb_id = getImdb(filetype_file, check_inside = False)
    +                        if imdb_id:
    +                            log.debug('Found movie via imdb in filename: %s' % nfo_file)
    +                            break
    +            except:
    +                pass
    +
             # Check if path is already in db
             if not imdb_id:
                 db = get_session()
    
    From 082af9a3077f07fc036561e522f679cfa9d6dd4d Mon Sep 17 00:00:00 2001
    From: Ruud 
    Date: Sat, 19 May 2012 00:45:42 +0200
    Subject: [PATCH 017/207] Don't fire events for unknown movie
    
    ---
     couchpotato/core/plugins/renamer/main.py | 13 ++++++++-----
     1 file changed, 8 insertions(+), 5 deletions(-)
    
    diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py
    index 07826091..f2b98564 100644
    --- a/couchpotato/core/plugins/renamer/main.py
    +++ b/couchpotato/core/plugins/renamer/main.py
    @@ -85,6 +85,7 @@ class Renamer(Plugin):
                 movie_title = getTitle(group['library'])
     
                 # Add _UNKNOWN_ if no library item is connected
    +            unknown = False
                 if not group['library'] or not movie_title:
                     if group['dirname']:
                         rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_UNKNOWN_%s' % group['dirname'])
    @@ -94,6 +95,7 @@ class Renamer(Plugin):
                                 filename = os.path.basename(rename_me)
                                 rename_files[rename_me] = rename_me.replace(filename, '_UNKNOWN_%s' % filename)
     
    +                unknown = True
                 # Rename the files using the library data
                 else:
                     group['library'] = fireEvent('library.update', identifier = group['library']['identifier'], single = True)
    @@ -368,12 +370,13 @@ class Renamer(Plugin):
                     except:
                         log.error('Failed removing %s: %s' % (group['parentdir'], traceback.format_exc()))
     
    -            # Search for trailers etc
    -            fireEventAsync('renamer.after', group)
    +            if not unknown:
    +                # Search for trailers etc
    +                fireEventAsync('renamer.after', group)
     
    -            # Notify on download
    -            download_message = 'Downloaded %s (%s)' % (movie_title, replacements['quality'])
    -            fireEventAsync('movie.downloaded', message = download_message, data = group)
    +                # Notify on download
    +                download_message = 'Downloaded %s (%s)' % (movie_title, replacements['quality'])
    +                fireEventAsync('movie.downloaded', message = download_message, data = group)
     
                 # Break if CP wants to shut down
                 if self.shuttingDown():
    
    From 0b034eb0faafa6e0cdd4f61be078a57eebc0e59e Mon Sep 17 00:00:00 2001
    From: Ruud 
    Date: Sat, 19 May 2012 01:01:09 +0200
    Subject: [PATCH 018/207] To a proper random string when no IMDB is found
    
    ---
     couchpotato/core/helpers/variable.py               | 5 +++++
     couchpotato/core/providers/movie/_modifier/main.py | 7 +++++--
     2 files changed, 10 insertions(+), 2 deletions(-)
    
    diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py
    index bf5b2362..9a4bdc13 100644
    --- a/couchpotato/core/helpers/variable.py
    +++ b/couchpotato/core/helpers/variable.py
    @@ -2,7 +2,9 @@ from couchpotato.core.logger import CPLog
     import hashlib
     import os.path
     import platform
    +import random
     import re
    +import string
     
     log = CPLog(__name__)
     
    @@ -117,3 +119,6 @@ def getTitle(library_dict):
             log.error('Could not get title for library item: %s' % library_dict)
             return None
     
    +def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
    +    return ''.join(random.choice(chars) for x in range(size))
    +
    diff --git a/couchpotato/core/providers/movie/_modifier/main.py b/couchpotato/core/providers/movie/_modifier/main.py
    index e9ad7633..5ed42141 100644
    --- a/couchpotato/core/providers/movie/_modifier/main.py
    +++ b/couchpotato/core/providers/movie/_modifier/main.py
    @@ -1,6 +1,6 @@
     from couchpotato import get_session
     from couchpotato.core.event import addEvent, fireEvent
    -from couchpotato.core.helpers.variable import mergeDicts
    +from couchpotato.core.helpers.variable import mergeDicts, randomString
     from couchpotato.core.logger import CPLog
     from couchpotato.core.plugins.base import Plugin
     from couchpotato.core.settings.model import Library
    @@ -23,7 +23,10 @@ class MovieResultModifier(Plugin):
     
             # Combine on imdb id
             for item in results:
    -            imdb = item.get('imdb', 'random-%s' % time.time())
    +            random_string = randomString()
    +            imdb = item.get('imdb', random_string)
    +            imdb = imdb if imdb else random_string
    +
                 if not temp.get(imdb):
                     temp[imdb] = self.getLibraryTags(imdb)
                     order.append(imdb)
    
    From a5c8747fee9c161c9e502ea56a8c9e29b3277ff5 Mon Sep 17 00:00:00 2001
    From: Ruud 
    Date: Sat, 19 May 2012 17:32:00 +0200
    Subject: [PATCH 019/207] Proper restarting of server
    
    ---
     couchpotato/core/_base/_core/main.py | 33 ++++++++++++++--------------
     couchpotato/environment.py           |  1 +
     couchpotato/runner.py                | 12 +++++-----
     3 files changed, 24 insertions(+), 22 deletions(-)
    
    diff --git a/couchpotato/core/_base/_core/main.py b/couchpotato/core/_base/_core/main.py
    index a496df60..127a0cf6 100644
    --- a/couchpotato/core/_base/_core/main.py
    +++ b/couchpotato/core/_base/_core/main.py
    @@ -5,7 +5,7 @@ from couchpotato.core.helpers.variable import cleanHost, md5
     from couchpotato.core.logger import CPLog
     from couchpotato.core.plugins.base import Plugin
     from couchpotato.environment import Env
    -from flask import request
    +from tornado.ioloop import IOLoop
     from uuid import uuid4
     import os
     import platform
    @@ -18,7 +18,7 @@ log = CPLog(__name__)
     
     class Core(Plugin):
     
    -    ignore_restart = ['Core.crappyRestart', 'Core.crappyShutdown']
    +    ignore_restart = ['Core.crappyRestart', 'Core.crappyShutdown', 'Updater.check']
         shutdown_started = False
     
         def __init__(self):
    @@ -63,30 +63,28 @@ class Core(Plugin):
             if self.shutdown_started:
                 return
     
    -        try:
    -            self.urlopen('%s/app.shutdown' % self.createApiUrl(), show_error = False)
    -            return True
    -        except:
    -            self.initShutdown()
    -            return False
    +        self.initShutdown()
    +        return True
     
         def crappyRestart(self):
             if self.shutdown_started:
                 return
     
    -        try:
    -            self.urlopen('%s/app.restart' % self.createApiUrl(), show_error = False)
    -            return True
    -        except:
    -            self.initShutdown(restart = True)
    -            return False
    +        self.initShutdown(restart = True)
    +        return True
     
         def shutdown(self):
    -        self.initShutdown()
    +        def shutdown():
    +            self.initShutdown()
    +        IOLoop.instance().add_callback(shutdown)
    +
             return 'shutdown'
     
         def restart(self):
    -        self.initShutdown(restart = True)
    +        def restart():
    +            self.initShutdown(restart = True)
    +        IOLoop.instance().add_callback(restart)
    +
             return 'restarting'
     
         def initShutdown(self, restart = False):
    @@ -121,7 +119,8 @@ class Core(Plugin):
             log.debug('Save to shutdown/restart')
     
             try:
    -            request.environ.get('werkzeug.server.shutdown')()
    +            Env.get('httpserver').stop()
    +            IOLoop.instance().stop()
             except RuntimeError:
                 pass
             except:
    diff --git a/couchpotato/environment.py b/couchpotato/environment.py
    index e804170d..a6f3ebb3 100644
    --- a/couchpotato/environment.py
    +++ b/couchpotato/environment.py
    @@ -23,6 +23,7 @@ class Env(object):
         _deamonize = False
         _desktop = None
         _session = None
    +    _httpserver = None
     
         ''' Data paths and directories '''
         _app_dir = ""
    diff --git a/couchpotato/runner.py b/couchpotato/runner.py
    index 37c0f348..fd7ba8e6 100644
    --- a/couchpotato/runner.py
    +++ b/couchpotato/runner.py
    @@ -233,16 +233,18 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
         fireEventAsync('app.load')
     
         # Go go go!
    +    web_container = WSGIContainer(app)
    +    web_container._log = _log
    +    http_server = HTTPServer(web_container, no_keep_alive = True)
    +    Env.set('httpserver', http_server)
    +    loop = IOLoop.instance()
    +
         try_restart = True
         restart_tries = 5
    +
         while try_restart:
             try:
    -            web_container = WSGIContainer(app)
    -            web_container._log = _log
    -
    -            http_server = HTTPServer(web_container)
                 http_server.listen(config['port'], config['host'])
    -            loop = IOLoop.instance()
     
                 if config['use_reloader']:
                     autoreload.start(loop)
    
    From a4dec10796e5e75b07a639b92596906391c14fa8 Mon Sep 17 00:00:00 2001
    From: Ruud 
    Date: Sat, 19 May 2012 17:55:41 +0200
    Subject: [PATCH 020/207] Cleanup
    
    ---
     couchpotato/core/providers/movie/_modifier/main.py  | 1 -
     couchpotato/core/providers/movie/themoviedb/main.py | 4 +++-
     2 files changed, 3 insertions(+), 2 deletions(-)
    
    diff --git a/couchpotato/core/providers/movie/_modifier/main.py b/couchpotato/core/providers/movie/_modifier/main.py
    index 5ed42141..4b2ebb66 100644
    --- a/couchpotato/core/providers/movie/_modifier/main.py
    +++ b/couchpotato/core/providers/movie/_modifier/main.py
    @@ -4,7 +4,6 @@ from couchpotato.core.helpers.variable import mergeDicts, randomString
     from couchpotato.core.logger import CPLog
     from couchpotato.core.plugins.base import Plugin
     from couchpotato.core.settings.model import Library
    -import time
     import traceback
     
     log = CPLog(__name__)
    diff --git a/couchpotato/core/providers/movie/themoviedb/main.py b/couchpotato/core/providers/movie/themoviedb/main.py
    index 667f563f..387fcac4 100644
    --- a/couchpotato/core/providers/movie/themoviedb/main.py
    +++ b/couchpotato/core/providers/movie/themoviedb/main.py
    @@ -3,7 +3,6 @@ from couchpotato.core.helpers.encoding import simplifyString, toUnicode
     from couchpotato.core.logger import CPLog
     from couchpotato.core.providers.movie.base import MovieProvider
     from libs.themoviedb import tmdb
    -import re
     
     log = CPLog(__name__)
     
    @@ -88,6 +87,9 @@ class TheMovieDb(MovieProvider):
     
         def getInfo(self, identifier = None):
     
    +        if not identifier:
    +            return {}
    +
             cache_key = 'tmdb.cache.%s' % identifier
             result = self.getCache(cache_key)
     
    
    From c6a1a19bf035249e6d316a65749b024de04eae88 Mon Sep 17 00:00:00 2001
    From: Ruud 
    Date: Sat, 19 May 2012 19:27:24 +0200
    Subject: [PATCH 021/207] Runtime missing first hour. fix #308
    
    ---
     couchpotato/core/providers/movie/imdbapi/main.py | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/couchpotato/core/providers/movie/imdbapi/main.py b/couchpotato/core/providers/movie/imdbapi/main.py
    index 7aa3d978..7456a3c9 100644
    --- a/couchpotato/core/providers/movie/imdbapi/main.py
    +++ b/couchpotato/core/providers/movie/imdbapi/main.py
    @@ -110,10 +110,10 @@ class IMDBAPI(MovieProvider):
         def runtimeToMinutes(self, runtime_str):
             runtime = 0
     
    -        regex = '(\d*.?\d+).(hr|hrs|mins|min)+'
    +        regex = '(\d*.?\d+).(h|hr|hrs|mins|min)+'
             matches = re.findall(regex, runtime_str)
             for match in matches:
                 nr, size = match
    -            runtime += tryInt(nr) * (60 if 'hr' in str(size) else 1)
    +            runtime += tryInt(nr) * (60 if 'h' is str(size)[0] else 1)
     
             return runtime
    
    From 80afd9a67c3dc21e93152a1cb509acbc77162478 Mon Sep 17 00:00:00 2001
    From: Ruud 
    Date: Sun, 20 May 2012 11:23:56 +0200
    Subject: [PATCH 022/207] Removed non-working proivders. fix #309
    
    ---
     .../core/providers/nzb/moovee/__init__.py     | 23 ------
     couchpotato/core/providers/nzb/moovee/main.py | 66 -----------------
     .../core/providers/nzb/x264/__init__.py       | 23 ------
     couchpotato/core/providers/nzb/x264/main.py   | 70 -------------------
     4 files changed, 182 deletions(-)
     delete mode 100644 couchpotato/core/providers/nzb/moovee/__init__.py
     delete mode 100644 couchpotato/core/providers/nzb/moovee/main.py
     delete mode 100644 couchpotato/core/providers/nzb/x264/__init__.py
     delete mode 100644 couchpotato/core/providers/nzb/x264/main.py
    
    diff --git a/couchpotato/core/providers/nzb/moovee/__init__.py b/couchpotato/core/providers/nzb/moovee/__init__.py
    deleted file mode 100644
    index f2f85d18..00000000
    --- a/couchpotato/core/providers/nzb/moovee/__init__.py
    +++ /dev/null
    @@ -1,23 +0,0 @@
    -from .main import Moovee
    -
    -def start():
    -    return Moovee()
    -
    -config = [{
    -    'name': 'moovee',
    -    'groups': [
    -        {
    -            'tab': 'searcher',
    -            'subtab': 'providers',
    -            'name': '#alt.binaries.moovee',
    -            'description': 'SD movies only',
    -            'options': [
    -                {
    -                    'name': 'enabled',
    -                    'type': 'enabler',
    -                    'default': False,
    -                },
    -            ],
    -        },
    -    ],
    -}]
    diff --git a/couchpotato/core/providers/nzb/moovee/main.py b/couchpotato/core/providers/nzb/moovee/main.py
    deleted file mode 100644
    index 65f118d7..00000000
    --- a/couchpotato/core/providers/nzb/moovee/main.py
    +++ /dev/null
    @@ -1,66 +0,0 @@
    -from couchpotato.core.event import fireEvent
    -from couchpotato.core.helpers.encoding import tryUrlencode
    -from couchpotato.core.helpers.variable import getTitle
    -from couchpotato.core.logger import CPLog
    -from couchpotato.core.providers.nzb.base import NZBProvider
    -from dateutil.parser import parse
    -import re
    -import time
    -
    -log = CPLog(__name__)
    -
    -
    -class Moovee(NZBProvider):
    -
    -    urls = {
    -        'download': 'http://85.214.105.230/get_nzb.php?id=%s§ion=moovee',
    -        'search': 'http://abmoovee.allfilled.com/search.php?q=%s&Search=Search',
    -    }
    -
    -    regex = '(?P.*?).+?(?P.*?)</td>.+?<td class="cell_statuschange">(?P<age>.*?)</td>'
    -
    -    http_time_between_calls = 2 # Seconds
    -
    -    def search(self, movie, quality):
    -
    -        results = []
    -        if self.isDisabled() or not self.isAvailable(self.urls['search']) or quality.get('hd', False):
    -            return results
    -
    -        q = '%s %s' % (getTitle(movie['library']), quality.get('identifier'))
    -        url = self.urls['search'] % tryUrlencode(q)
    -
    -        cache_key = 'moovee.%s' % q
    -        data = self.getCache(cache_key, url)
    -        if data:
    -            match = re.compile(self.regex, re.DOTALL).finditer(data)
    -
    -            for nzb in match:
    -                new = {
    -                    'id': nzb.group('reqid'),
    -                    'name': nzb.group('title'),
    -                    'type': 'nzb',
    -                    'provider': self.getName(),
    -                    'age': self.calculateAge(time.mktime(parse(nzb.group('age')).timetuple())),
    -                    'size': None,
    -                    'url': self.urls['download'] % (nzb.group('reqid')),
    -                    'detail_url': '',
    -                    'description': '',
    -                    'check_nzb': False,
    -                }
    -
    -                new['score'] = fireEvent('score.calculate', new, movie, single = True)
    -                is_correct_movie = fireEvent('searcher.correct_movie',
    -                                                    nzb = new, movie = movie, quality = quality,
    -                                                    imdb_results = False, single_category = False, single = True)
    -                if is_correct_movie:
    -                    results.append(new)
    -                    self.found(new)
    -
    -        return results
    -
    -    def belongsTo(self, url, host = None):
    -        match = re.match('http://85\.214\.105\.230/get_nzb\.php\?id=[0-9]*§ion=moovee', url)
    -        if match:
    -            return self
    -        return
    diff --git a/couchpotato/core/providers/nzb/x264/__init__.py b/couchpotato/core/providers/nzb/x264/__init__.py
    deleted file mode 100644
    index 152be009..00000000
    --- a/couchpotato/core/providers/nzb/x264/__init__.py
    +++ /dev/null
    @@ -1,23 +0,0 @@
    -from .main import X264
    -
    -def start():
    -    return X264()
    -
    -config = [{
    -    'name': 'x264',
    -    'groups': [
    -        {
    -            'tab': 'searcher',
    -            'subtab': 'providers',
    -            'name': '#alt.binaries.hdtv.x264',
    -            'description': 'HD movies only',
    -            'options': [
    -                {
    -                    'name': 'enabled',
    -                    'type': 'enabler',
    -                    'default': False,
    -                },
    -            ],
    -        },
    -    ],
    -}]
    diff --git a/couchpotato/core/providers/nzb/x264/main.py b/couchpotato/core/providers/nzb/x264/main.py
    deleted file mode 100644
    index 4292dee5..00000000
    --- a/couchpotato/core/providers/nzb/x264/main.py
    +++ /dev/null
    @@ -1,70 +0,0 @@
    -from couchpotato.core.event import fireEvent
    -from couchpotato.core.helpers.encoding import tryUrlencode
    -from couchpotato.core.helpers.variable import tryInt, getTitle
    -from couchpotato.core.logger import CPLog
    -from couchpotato.core.providers.nzb.base import NZBProvider
    -import re
    -
    -log = CPLog(__name__)
    -
    -
    -class X264(NZBProvider):
    -
    -    urls = {
    -        'download': 'http://85.214.105.230/get_nzb.php?id=%s§ion=hd',
    -        'search': 'http://85.214.105.230/x264/requests.php?release=%s&status=FILLED&age=1300&sort=ID',
    -    }
    -
    -    regex = '<tr class="req_filled"><td class="reqid">(?P<id>.*?)</td><td class="release">(?P<title>.*?)</td>.+?<td class="age">(?P<age>.*?)</td>'
    -
    -    http_time_between_calls = 2 # Seconds
    -
    -    def search(self, movie, quality):
    -
    -        results = []
    -        if self.isDisabled() or not self.isAvailable(self.urls['search'].split('requests')[0]) or not quality.get('hd', False):
    -            return results
    -
    -        q = '%s %s %s' % (getTitle(movie['library']), movie['library']['year'], quality.get('identifier'))
    -        url = self.urls['search'] % tryUrlencode(q)
    -
    -        cache_key = 'x264.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
    -        data = self.getCache(cache_key, url)
    -        if data:
    -            match = re.compile(self.regex, re.DOTALL).finditer(data)
    -
    -            for nzb in match:
    -                try:
    -                    age_match = re.match('((?P<day>\d+)d)', nzb.group('age'))
    -                    age = age_match.group('day')
    -                except:
    -                    age = 1
    -
    -                new = {
    -                    'id': nzb.group('id'),
    -                    'name': nzb.group('title'),
    -                    'type': 'nzb',
    -                    'provider': self.getName(),
    -                    'age': tryInt(age),
    -                    'size': None,
    -                    'url': self.urls['download'] % (nzb.group('id')),
    -                    'detail_url': '',
    -                    'description': '',
    -                    'check_nzb': False,
    -                }
    -
    -                new['score'] = fireEvent('score.calculate', new, movie, single = True)
    -                is_correct_movie = fireEvent('searcher.correct_movie',
    -                                                    nzb = new, movie = movie, quality = quality,
    -                                                    imdb_results = False, single_category = False, single = True)
    -                if is_correct_movie:
    -                    results.append(new)
    -                    self.found(new)
    -
    -        return results
    -
    -    def belongsTo(self, url, host = None):
    -        match = re.match('http://85\.214\.105\.230/get_nzb\.php\?id=[0-9]*§ion=hd', url)
    -        if match:
    -            return self
    -        return
    
    From 85cb9be0640050c0ef88d52ad8579393ced1b673 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 20 May 2012 12:03:29 +0200
    Subject: [PATCH 023/207] IPv6 start error. fix #306
    
    ---
     libs/tornado/netutil.py | 4 +++-
     1 file changed, 3 insertions(+), 1 deletion(-)
    
    diff --git a/libs/tornado/netutil.py b/libs/tornado/netutil.py
    index 1e1bcbf9..290f33db 100644
    --- a/libs/tornado/netutil.py
    +++ b/libs/tornado/netutil.py
    @@ -238,12 +238,14 @@ def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128):
         if address == "":
             address = None
         flags = socket.AI_PASSIVE
    -    if hasattr(socket, "AI_ADDRCONFIG"):
    +    try:
             # AI_ADDRCONFIG ensures that we only try to bind on ipv6
             # if the system is configured for it, but the flag doesn't
             # exist on some platforms (specifically WinXP, although
             # newer versions of windows have it)
             flags |= socket.AI_ADDRCONFIG
    +    except:
    +        pass
         for res in set(socket.getaddrinfo(address, port, family, socket.SOCK_STREAM,
                                       0, flags)):
             af, socktype, proto, canonname, sockaddr = res
    
    From cba99169328376ab23e6b55cbc7fca70c95347c3 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 20 May 2012 12:25:34 +0200
    Subject: [PATCH 024/207] Revert "IPv6 start error. fix #306"
    
    This reverts commit 85cb9be0640050c0ef88d52ad8579393ced1b673.
    ---
     libs/tornado/netutil.py | 4 +---
     1 file changed, 1 insertion(+), 3 deletions(-)
    
    diff --git a/libs/tornado/netutil.py b/libs/tornado/netutil.py
    index 290f33db..1e1bcbf9 100644
    --- a/libs/tornado/netutil.py
    +++ b/libs/tornado/netutil.py
    @@ -238,14 +238,12 @@ def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128):
         if address == "":
             address = None
         flags = socket.AI_PASSIVE
    -    try:
    +    if hasattr(socket, "AI_ADDRCONFIG"):
             # AI_ADDRCONFIG ensures that we only try to bind on ipv6
             # if the system is configured for it, but the flag doesn't
             # exist on some platforms (specifically WinXP, although
             # newer versions of windows have it)
             flags |= socket.AI_ADDRCONFIG
    -    except:
    -        pass
         for res in set(socket.getaddrinfo(address, port, family, socket.SOCK_STREAM,
                                       0, flags)):
             af, socktype, proto, canonname, sockaddr = res
    
    From 483372f43ad9bba29fa9eac8bad2631c2c61b8f7 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 20 May 2012 12:43:28 +0200
    Subject: [PATCH 025/207] AI flag error
    
    ---
     libs/tornado/netutil.py | 6 ------
     1 file changed, 6 deletions(-)
    
    diff --git a/libs/tornado/netutil.py b/libs/tornado/netutil.py
    index 1e1bcbf9..dcffd4a3 100644
    --- a/libs/tornado/netutil.py
    +++ b/libs/tornado/netutil.py
    @@ -238,12 +238,6 @@ def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128):
         if address == "":
             address = None
         flags = socket.AI_PASSIVE
    -    if hasattr(socket, "AI_ADDRCONFIG"):
    -        # AI_ADDRCONFIG ensures that we only try to bind on ipv6
    -        # if the system is configured for it, but the flag doesn't
    -        # exist on some platforms (specifically WinXP, although
    -        # newer versions of windows have it)
    -        flags |= socket.AI_ADDRCONFIG
         for res in set(socket.getaddrinfo(address, port, family, socket.SOCK_STREAM,
                                       0, flags)):
             af, socktype, proto, canonname, sockaddr = res
    
    From ee163d74d6f661fa3eae20f8db8a93a079a8637b Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 20 May 2012 12:44:14 +0200
    Subject: [PATCH 026/207] Do normal shutdown
    
    ---
     couchpotato/core/_base/_core/main.py   | 26 +++++++++-----------------
     couchpotato/core/_base/desktop/main.py |  2 +-
     couchpotato/core/_base/updater/main.py |  2 +-
     couchpotato/runner.py                  |  2 +-
     4 files changed, 12 insertions(+), 20 deletions(-)
    
    diff --git a/couchpotato/core/_base/_core/main.py b/couchpotato/core/_base/_core/main.py
    index 127a0cf6..f7abddec 100644
    --- a/couchpotato/core/_base/_core/main.py
    +++ b/couchpotato/core/_base/_core/main.py
    @@ -18,7 +18,7 @@ log = CPLog(__name__)
     
     class Core(Plugin):
     
    -    ignore_restart = ['Core.crappyRestart', 'Core.crappyShutdown', 'Updater.check']
    +    ignore_restart = ['Core.restart', 'Core.shutdown', 'Updater.check']
         shutdown_started = False
     
         def __init__(self):
    @@ -37,8 +37,8 @@ class Core(Plugin):
                 'desc': 'Get version.'
             })
     
    -        addEvent('app.crappy_shutdown', self.crappyShutdown)
    -        addEvent('app.crappy_restart', self.crappyRestart)
    +        addEvent('app.shutdown', self.shutdown)
    +        addEvent('app.restart', self.restart)
             addEvent('app.load', self.launchBrowser, priority = 1)
             addEvent('app.base_url', self.createBaseUrl)
             addEvent('app.api_url', self.createApiUrl)
    @@ -59,21 +59,10 @@ class Core(Plugin):
                 'succes': True
             })
     
    -    def crappyShutdown(self):
    -        if self.shutdown_started:
    -            return
    -
    -        self.initShutdown()
    -        return True
    -
    -    def crappyRestart(self):
    -        if self.shutdown_started:
    -            return
    -
    -        self.initShutdown(restart = True)
    -        return True
    -
         def shutdown(self):
    +        if self.shutdown_started:
    +            return False
    +
             def shutdown():
                 self.initShutdown()
             IOLoop.instance().add_callback(shutdown)
    @@ -81,6 +70,9 @@ class Core(Plugin):
             return 'shutdown'
     
         def restart(self):
    +        if self.shutdown_started:
    +            return False
    +
             def restart():
                 self.initShutdown(restart = True)
             IOLoop.instance().add_callback(restart)
    diff --git a/couchpotato/core/_base/desktop/main.py b/couchpotato/core/_base/desktop/main.py
    index ce1ff282..dcec7050 100644
    --- a/couchpotato/core/_base/desktop/main.py
    +++ b/couchpotato/core/_base/desktop/main.py
    @@ -27,7 +27,7 @@ if Env.get('desktop'):
                 addEvent('app.after_shutdown', desktop.afterShutdown)
     
             def onClose(self, event):
    -            return fireEvent('app.crappy_shutdown', single = True)
    +            return fireEvent('app.shutdown', single = True)
     
     else:
     
    diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py
    index c12c4b9f..eacc993a 100644
    --- a/couchpotato/core/_base/updater/main.py
    +++ b/couchpotato/core/_base/updater/main.py
    @@ -55,7 +55,7 @@ class Updater(Plugin):
             if self.updater.check():
                 if self.conf('automatic') and not self.updater.update_failed:
                     if self.updater.doUpdate():
    -                    fireEventAsync('app.crappy_restart')
    +                    fireEventAsync('app.restart')
                 else:
                     if self.conf('notification'):
                         fireEvent('updater.available', message = 'A new update is available', data = self.updater.info())
    diff --git a/couchpotato/runner.py b/couchpotato/runner.py
    index fd7ba8e6..e2b5bdf9 100644
    --- a/couchpotato/runner.py
    +++ b/couchpotato/runner.py
    @@ -46,7 +46,7 @@ def getOptions(base_path, args):
     
     
     def cleanup():
    -    fireEvent('app.crappy_shutdown', single = True)
    +    fireEvent('app.shutdown', single = True)
         time.sleep(1)
     
     # Tornado monkey patch logging..
    
    From f8bae9e84e0237bc03d7a1adbfe421f9e8bbc9dd Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 20 May 2012 13:15:55 +0200
    Subject: [PATCH 027/207] Don't remove pidfile twice
    
    ---
     CouchPotato.py        | 1 -
     couchpotato/runner.py | 8 --------
     2 files changed, 9 deletions(-)
    
    diff --git a/CouchPotato.py b/CouchPotato.py
    index dd1e6549..00992780 100755
    --- a/CouchPotato.py
    +++ b/CouchPotato.py
    @@ -89,7 +89,6 @@ class Loader(object):
                     if self.runAsDaemon():
                         try: self.daemon.stop()
                         except: pass
    -                    self.daemon.delpid()
                 except:
                     self.log.critical(traceback.format_exc())
     
    diff --git a/couchpotato/runner.py b/couchpotato/runner.py
    index e2b5bdf9..280ef758 100644
    --- a/couchpotato/runner.py
    +++ b/couchpotato/runner.py
    @@ -10,7 +10,6 @@ from tornado.ioloop import IOLoop
     from tornado.web import RequestHandler
     from tornado.wsgi import WSGIContainer
     from werkzeug.contrib.cache import FileSystemCache
    -import atexit
     import locale
     import logging
     import os.path
    @@ -44,11 +43,6 @@ def getOptions(base_path, args):
     
         return options
     
    -
    -def cleanup():
    -    fireEvent('app.shutdown', single = True)
    -    time.sleep(1)
    -
     # Tornado monkey patch logging..
     def _log(status_code, request):
     
    @@ -129,8 +123,6 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
         # Development
         development = Env.setting('development', default = False, type = 'bool')
         Env.set('dev', development)
    -    if not development:
    -        atexit.register(cleanup)
     
         # Disable logging for some modules
         for logger_name in ['enzyme', 'guessit', 'subliminal', 'apscheduler']:
    
    From 32de242760a3b0dddb8f3c5a19ad961aa75c65d8 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 20 May 2012 13:24:34 +0200
    Subject: [PATCH 028/207] Desktop updater
    
    ---
     couchpotato/core/_base/updater/main.py | 12 +++++++++---
     1 file changed, 9 insertions(+), 3 deletions(-)
    
    diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py
    index eacc993a..0d7b04f6 100644
    --- a/couchpotato/core/_base/updater/main.py
    +++ b/couchpotato/core/_base/updater/main.py
    @@ -338,7 +338,7 @@ class SourceUpdater(BaseUpdater):
             return {}
     
     
    -class DesktopUpdater(Plugin):
    +class DesktopUpdater(BaseUpdater):
     
         version = None
         update_failed = False
    @@ -350,9 +350,15 @@ class DesktopUpdater(Plugin):
     
         def doUpdate(self):
             try:
    -            self.desktop.CheckForUpdate(silentUnlessUpdate = True)
    +            def do_restart(e):
    +                if e['status'] == 'done':
    +                    fireEventAsync('app.restart')
    +                else:
    +                    log.error('Failed updating desktop: %s' % e['exception'])
    +                    self.update_failed = True
    +
    +            self.desktop._esky.auto_update(callback = do_restart)
             except:
    -            log.error('Failed updating desktop: %s' % traceback.format_exc())
                 self.update_failed = True
     
             return False
    
    From 82aba8868c0782e263bf9245d7a0bf4e6b288585 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 20 May 2012 15:39:14 +0200
    Subject: [PATCH 029/207] Only get nzb details when needed
    
    ---
     couchpotato/core/plugins/score/main.py        |  5 +++
     couchpotato/core/plugins/searcher/main.py     | 10 +++++
     .../core/providers/nzb/newzbin/main.py        |  2 +-
     .../core/providers/nzb/newznab/main.py        |  3 +-
     .../core/providers/nzb/nzbclub/main.py        | 40 ++++++++++++++-----
     .../core/providers/nzb/nzbindex/main.py       | 19 +++++----
     .../core/providers/nzb/nzbmatrix/main.py      |  2 +-
     7 files changed, 60 insertions(+), 21 deletions(-)
    
    diff --git a/couchpotato/core/plugins/score/main.py b/couchpotato/core/plugins/score/main.py
    index 74fed1da..8b4fedb4 100644
    --- a/couchpotato/core/plugins/score/main.py
    +++ b/couchpotato/core/plugins/score/main.py
    @@ -38,4 +38,9 @@ class Score(Plugin):
             # Duplicates in name
             score += duplicateScore(nzb['name'], getTitle(movie['library']))
     
    +        # Extra provider specific check
    +        extra_score = nzb.get('extra_score')
    +        if extra_score:
    +            score += extra_score(nzb)
    +
             return score
    diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py
    index 7983f928..99392ac7 100644
    --- a/couchpotato/core/plugins/searcher/main.py
    +++ b/couchpotato/core/plugins/searcher/main.py
    @@ -265,6 +265,16 @@ class Searcher(Plugin):
                 return False
     
     
    +        # Provider specific functions
    +        get_more = nzb.get('get_more_info')
    +        if get_more:
    +            get_more(nzb)
    +
    +        extra_check = nzb.get('extra_check')
    +        if extra_check and not extra_check(nzb):
    +            return False
    +
    +
             if imdb_results:
                 return True
     
    diff --git a/couchpotato/core/providers/nzb/newzbin/main.py b/couchpotato/core/providers/nzb/newzbin/main.py
    index 80f856ec..02efffa6 100644
    --- a/couchpotato/core/providers/nzb/newzbin/main.py
    +++ b/couchpotato/core/providers/nzb/newzbin/main.py
    @@ -115,12 +115,12 @@ class Newzbin(NZBProvider, RSS):
                             'description': self.getTextElement(nzb, "description"),
                             'check_nzb': False,
                         }
    -                    new['score'] = fireEvent('score.calculate', new, movie, single = True)
     
                         is_correct_movie = fireEvent('searcher.correct_movie',
                                                      nzb = new, movie = movie, quality = quality,
                                                      imdb_results = True, single_category = single_cat, single = True)
                         if is_correct_movie:
    +                        new['score'] = fireEvent('score.calculate', new, movie, single = True)
                             results.append(new)
                             self.found(new)
     
    diff --git a/couchpotato/core/providers/nzb/newznab/main.py b/couchpotato/core/providers/nzb/newznab/main.py
    index 8b959b54..691f5e6f 100644
    --- a/couchpotato/core/providers/nzb/newznab/main.py
    +++ b/couchpotato/core/providers/nzb/newznab/main.py
    @@ -144,13 +144,12 @@ class Newznab(NZBProvider, RSS):
                         }
     
                         if not for_feed:
    -                        new['score'] = fireEvent('score.calculate', new, movie, single = True)
    -
                             is_correct_movie = fireEvent('searcher.correct_movie',
                                                          nzb = new, movie = movie, quality = quality,
                                                          imdb_results = True, single_category = single_cat, single = True)
     
                             if is_correct_movie:
    +                            new['score'] = fireEvent('score.calculate', new, movie, single = True)
                                 results.append(new)
                                 self.found(new)
                         else:
    diff --git a/couchpotato/core/providers/nzb/nzbclub/main.py b/couchpotato/core/providers/nzb/nzbclub/main.py
    index 40bc2d1c..2005fd27 100644
    --- a/couchpotato/core/providers/nzb/nzbclub/main.py
    +++ b/couchpotato/core/providers/nzb/nzbclub/main.py
    @@ -58,10 +58,14 @@ class NZBClub(NZBProvider, RSS):
                         size = enclosure['length']
                         date = self.getTextElement(nzb, "pubDate")
     
    -                    full_description = self.getCache('nzbclub.%s' % nzbclub_id, self.getTextElement(nzb, "link"), cache_timeout = 25920000)
    -                    html = BeautifulSoup(full_description)
    -                    nfo_pre = html.find('pre', attrs = {'class':'nfo'})
    -                    description = toUnicode(nfo_pre.text) if nfo_pre else ''
    +                    def extra_check(item):
    +                        full_description = self.getCache('nzbclub.%s' % nzbclub_id, item['detail_url'], cache_timeout = 25920000)
    +
    +                        if 'ARCHIVE inside ARCHIVE' in full_description:
    +                            log.info('Wrong: Seems to be passworded files: %s' % new['name'])
    +                            return False
    +
    +                        return True
     
                         new = {
                             'id': nzbclub_id,
    @@ -73,19 +77,17 @@ class NZBClub(NZBProvider, RSS):
                             'url': enclosure['url'].replace(' ', '_'),
                             'download': self.download,
                             'detail_url': self.getTextElement(nzb, "link"),
    -                        'description': description,
    +                        'description': '',
    +                        'get_more_info': self.getMoreInfo,
    +                        'extra_check': extra_check
                         }
    -                    new['score'] = fireEvent('score.calculate', new, movie, single = True)
    -
    -                    if 'ARCHIVE inside ARCHIVE' in full_description:
    -                        log.info('Wrong: Seems to be passworded files: %s' % new['name'])
    -                        continue
     
                         is_correct_movie = fireEvent('searcher.correct_movie',
                                                      nzb = new, movie = movie, quality = quality,
                                                      imdb_results = False, single_category = False, single = True)
     
                         if is_correct_movie:
    +                        new['score'] = fireEvent('score.calculate', new, movie, single = True)
                             results.append(new)
                             self.found(new)
     
    @@ -94,3 +96,21 @@ class NZBClub(NZBProvider, RSS):
                     log.error('Failed to parse XML response from NZBClub')
     
             return results
    +
    +    def getMoreInfo(self, item):
    +        full_description = self.getCache('nzbclub.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
    +        html = BeautifulSoup(full_description)
    +        nfo_pre = html.find('pre', attrs = {'class':'nfo'})
    +        description = toUnicode(nfo_pre.text) if nfo_pre else ''
    +
    +        item['description'] = description
    +        return item
    +
    +    def extraCheck(self, item):
    +        full_description = self.getCache('nzbclub.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
    +
    +        if 'ARCHIVE inside ARCHIVE' in full_description:
    +            log.info('Wrong: Seems to be passworded files: %s' % new['name'])
    +            return False
    +
    +        return True
    diff --git a/couchpotato/core/providers/nzb/nzbindex/main.py b/couchpotato/core/providers/nzb/nzbindex/main.py
    index 831ea9c6..96e875a8 100644
    --- a/couchpotato/core/providers/nzb/nzbindex/main.py
    +++ b/couchpotato/core/providers/nzb/nzbindex/main.py
    @@ -63,13 +63,8 @@ class NzbIndex(NZBProvider, RSS):
     
                         try:
                             description = self.getTextElement(nzb, "description")
    -                        if '/nfo/' in description.lower():
    -                            nfo_url = re.search('href=\"(?P<nfo>.+)\" ', description).group('nfo')
    -                            full_description = self.getCache('nzbindex.%s' % nzbindex_id, url = nfo_url, cache_timeout = 25920000)
    -                            html = BeautifulSoup(full_description)
    -                            description = toUnicode(html.find('pre', attrs = {'id':'nfo0'}).text)
                         except:
    -                        pass
    +                        description = ''
     
                         new = {
                             'id': nzbindex_id,
    @@ -81,15 +76,16 @@ class NzbIndex(NZBProvider, RSS):
                             'url': enclosure['url'],
                             'detail_url': enclosure['url'].replace('/download/', '/release/'),
                             'description': description,
    +                        'get_more_info': self.getMoreInfo,
                             'check_nzb': True,
                         }
    -                    new['score'] = fireEvent('score.calculate', new, movie, single = True)
     
                         is_correct_movie = fireEvent('searcher.correct_movie',
                                                      nzb = new, movie = movie, quality = quality,
                                                      imdb_results = False, single_category = False, single = True)
     
                         if is_correct_movie:
    +                        new['score'] = fireEvent('score.calculate', new, movie, single = True)
                             results.append(new)
                             self.found(new)
     
    @@ -99,6 +95,15 @@ class NzbIndex(NZBProvider, RSS):
     
             return results
     
    +    def getMoreInfo(self, item):
    +        try:
    +            if '/nfo/' in item['description'].lower():
    +                nfo_url = re.search('href=\"(?P<nfo>.+)\" ', item['description']).group('nfo')
    +                full_description = self.getCache('nzbindex.%s' % item['id'], url = nfo_url, cache_timeout = 25920000)
    +                html = BeautifulSoup(full_description)
    +                item['description'] = toUnicode(html.find('pre', attrs = {'id':'nfo0'}).text)
    +        except:
    +            pass
     
         def isEnabled(self):
             return NZBProvider.isEnabled(self) and self.conf('enabled')
    diff --git a/couchpotato/core/providers/nzb/nzbmatrix/main.py b/couchpotato/core/providers/nzb/nzbmatrix/main.py
    index 2a6e5a87..c1e579c9 100644
    --- a/couchpotato/core/providers/nzb/nzbmatrix/main.py
    +++ b/couchpotato/core/providers/nzb/nzbmatrix/main.py
    @@ -83,13 +83,13 @@ class NZBMatrix(NZBProvider, RSS):
                             'description': self.getTextElement(nzb, "description"),
                             'check_nzb': True,
                         }
    -                    new['score'] = fireEvent('score.calculate', new, movie, single = True)
     
                         is_correct_movie = fireEvent('searcher.correct_movie',
                                                      nzb = new, movie = movie, quality = quality,
                                                      imdb_results = True, single_category = single_cat, single = True)
     
                         if is_correct_movie:
    +                        new['score'] = fireEvent('score.calculate', new, movie, single = True)
                             results.append(new)
                             self.found(new)
     
    
    From a9c27e3057a6d04327fd220e819d91d0491d9d98 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 20 May 2012 19:29:38 +0200
    Subject: [PATCH 030/207] Compare guessit with own name_year method. fix #312
    
    ---
     couchpotato/core/plugins/scanner/main.py | 19 ++++++++++++++-----
     1 file changed, 14 insertions(+), 5 deletions(-)
    
    diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py
    index 76e243ee..e287fd33 100644
    --- a/couchpotato/core/plugins/scanner/main.py
    +++ b/couchpotato/core/plugins/scanner/main.py
    @@ -443,7 +443,7 @@ class Scanner(Plugin):
                     log.debug('Found movie via CP tag: %s' % cur_file)
                     break
     
    -        # Check and see if nfo or filenames contains the imdb-id
    +        # Check and see if nfo contains the imdb-id
             if not imdb_id:
                 try:
                     for nfo_file in files['nfo']:
    @@ -454,6 +454,8 @@ class Scanner(Plugin):
                 except:
                     pass
     
    +        # Check and see if filenames contains the imdb-id
    +        if not imdb_id:
                 try:
                     for filetype in files:
                         for filetype_file in files[filetype]:
    @@ -719,11 +721,12 @@ class Scanner(Plugin):
         def getReleaseNameYear(self, release_name, file_name = None):
     
             # Use guessit first
    +        guess = {}
             if file_name:
                 try:
                     guess = guess_movie_info(file_name)
                     if guess.get('title') and guess.get('year'):
    -                    return {
    +                    guess = {
                             'name': guess.get('title'),
                             'year': guess.get('year'),
                         }
    @@ -734,11 +737,12 @@ class Scanner(Plugin):
             cleaned = ' '.join(re.split('\W+', simplifyString(release_name)))
             cleaned = re.sub(self.clean, ' ', cleaned)
             year = self.findYear(cleaned)
    +        cp_guess = {}
     
             if year: # Split name on year
                 try:
                     movie_name = cleaned.split(year).pop(0).strip()
    -                return {
    +                cp_guess = {
                         'name': movie_name,
                         'year': int(year),
                     }
    @@ -747,11 +751,16 @@ class Scanner(Plugin):
             else: # Split name on multiple spaces
                 try:
                     movie_name = cleaned.split('  ').pop(0).strip()
    -                return {
    +                cp_guess = {
                         'name': movie_name,
                         'year': int(year),
                     }
                 except:
                     pass
     
    -        return {}
    +        if cp_guess.get('year') == guess.get('year') and len(cp_guess.get('name', '')) > len(guess.get('name', '')):
    +            return cp_guess
    +        elif guess == {}:
    +            return cp_guess
    +
    +        return guess
    
    From 19640a91929e5c80d456d4cfeb5e8ff574dd823b Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 20 May 2012 19:56:41 +0200
    Subject: [PATCH 031/207] Removed isAvailableCheck
    
    ---
     couchpotato/core/providers/nzb/mysterbin/main.py           | 4 ++--
     couchpotato/core/providers/nzb/newzbin/main.py             | 2 +-
     couchpotato/core/providers/nzb/newznab/main.py             | 4 ++--
     couchpotato/core/providers/nzb/nzbclub/main.py             | 4 ++--
     couchpotato/core/providers/nzb/nzbindex/main.py            | 2 +-
     couchpotato/core/providers/nzb/nzbmatrix/main.py           | 2 +-
     couchpotato/core/providers/torrent/kickasstorrents/main.py | 2 +-
     couchpotato/core/providers/torrent/thepiratebay/main.py    | 2 +-
     8 files changed, 11 insertions(+), 11 deletions(-)
    
    diff --git a/couchpotato/core/providers/nzb/mysterbin/main.py b/couchpotato/core/providers/nzb/mysterbin/main.py
    index 7e19d1ba..baf60558 100644
    --- a/couchpotato/core/providers/nzb/mysterbin/main.py
    +++ b/couchpotato/core/providers/nzb/mysterbin/main.py
    @@ -22,7 +22,7 @@ class Mysterbin(NZBProvider):
         def search(self, movie, quality):
     
             results = []
    -        if self.isDisabled() or not self.isAvailable(self.urls['search']):
    +        if self.isDisabled():
                 return results
     
             q = '"%s" %s %s' % (getTitle(movie['library']), movie['library']['year'], quality.get('identifier'))
    @@ -39,7 +39,7 @@ class Mysterbin(NZBProvider):
                 'nopasswd': 'on',
             }
     
    -        cache_key = 'mysterbin.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
    +        cache_key = 'mysterbin.%s.%s.%s' % (movie['library']['identifier'], quality.get('identifier'), q)
             data = self.getCache(cache_key, self.urls['search'] % tryUrlencode(params))
             if data:
     
    diff --git a/couchpotato/core/providers/nzb/newzbin/main.py b/couchpotato/core/providers/nzb/newzbin/main.py
    index 02efffa6..c6de2a29 100644
    --- a/couchpotato/core/providers/nzb/newzbin/main.py
    +++ b/couchpotato/core/providers/nzb/newzbin/main.py
    @@ -39,7 +39,7 @@ class Newzbin(NZBProvider, RSS):
         def search(self, movie, quality):
     
             results = []
    -        if self.isDisabled() or not self.isAvailable(self.urls['search']):
    +        if self.isDisabled():
                 return results
     
             format_id = self.getFormatId(type)
    diff --git a/couchpotato/core/providers/nzb/newznab/main.py b/couchpotato/core/providers/nzb/newznab/main.py
    index 691f5e6f..448ab35e 100644
    --- a/couchpotato/core/providers/nzb/newznab/main.py
    +++ b/couchpotato/core/providers/nzb/newznab/main.py
    @@ -51,7 +51,7 @@ class Newznab(NZBProvider, RSS):
         def singleFeed(self, host):
     
             results = []
    -        if self.isDisabled(host) or not self.isAvailable(self.getUrl(host['host'], self.urls['search'])):
    +        if self.isDisabled(host):
                 return results
     
             arguments = tryUrlencode({
    @@ -83,7 +83,7 @@ class Newznab(NZBProvider, RSS):
         def singleSearch(self, host, movie, quality):
     
             results = []
    -        if self.isDisabled(host) or not self.isAvailable(self.getUrl(host['host'], self.urls['search'])):
    +        if self.isDisabled(host):
                 return results
     
             cat_id = self.getCatId(quality['identifier'])
    diff --git a/couchpotato/core/providers/nzb/nzbclub/main.py b/couchpotato/core/providers/nzb/nzbclub/main.py
    index 2005fd27..3e07356d 100644
    --- a/couchpotato/core/providers/nzb/nzbclub/main.py
    +++ b/couchpotato/core/providers/nzb/nzbclub/main.py
    @@ -24,7 +24,7 @@ class NZBClub(NZBProvider, RSS):
         def search(self, movie, quality):
     
             results = []
    -        if self.isDisabled() or not self.isAvailable(self.urls['search']):
    +        if self.isDisabled():
                 return results
     
             q = '"%s" %s %s' % (getTitle(movie['library']), movie['library']['year'], quality.get('identifier'))
    @@ -40,7 +40,7 @@ class NZBClub(NZBProvider, RSS):
                 'ns': 1,
             }
     
    -        cache_key = 'nzbclub.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
    +        cache_key = 'nzbclub.%s.%s.%s' % (movie['library']['identifier'], quality.get('identifier'), q)
             data = self.getCache(cache_key, self.urls['search'] % tryUrlencode(params))
             if data:
                 try:
    diff --git a/couchpotato/core/providers/nzb/nzbindex/main.py b/couchpotato/core/providers/nzb/nzbindex/main.py
    index 96e875a8..f0e7ea8b 100644
    --- a/couchpotato/core/providers/nzb/nzbindex/main.py
    +++ b/couchpotato/core/providers/nzb/nzbindex/main.py
    @@ -26,7 +26,7 @@ class NzbIndex(NZBProvider, RSS):
         def search(self, movie, quality):
     
             results = []
    -        if self.isDisabled() or not self.isAvailable(self.urls['api']):
    +        if self.isDisabled():
                 return results
     
             q = '%s %s %s' % (getTitle(movie['library']), movie['library']['year'], quality.get('identifier'))
    diff --git a/couchpotato/core/providers/nzb/nzbmatrix/main.py b/couchpotato/core/providers/nzb/nzbmatrix/main.py
    index c1e579c9..b512790d 100644
    --- a/couchpotato/core/providers/nzb/nzbmatrix/main.py
    +++ b/couchpotato/core/providers/nzb/nzbmatrix/main.py
    @@ -32,7 +32,7 @@ class NZBMatrix(NZBProvider, RSS):
     
             results = []
     
    -        if self.isDisabled() or not self.isAvailable(self.urls['search']):
    +        if self.isDisabled():
                 return results
     
             cat_ids = ','.join(['%s' % x for x in self.getCatId(quality.get('identifier'))])
    diff --git a/couchpotato/core/providers/torrent/kickasstorrents/main.py b/couchpotato/core/providers/torrent/kickasstorrents/main.py
    index 4fd7b760..1101d321 100644
    --- a/couchpotato/core/providers/torrent/kickasstorrents/main.py
    +++ b/couchpotato/core/providers/torrent/kickasstorrents/main.py
    @@ -34,7 +34,7 @@ class KickAssTorrents(TorrentProvider):
         def search(self, movie, quality):
     
             results = []
    -        if self.isDisabled() or not self.isAvailable(self.urls['test']):
    +        if self.isDisabled():
                 return results
     
             cache_key = 'kickasstorrents.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
    diff --git a/couchpotato/core/providers/torrent/thepiratebay/main.py b/couchpotato/core/providers/torrent/thepiratebay/main.py
    index d9b06bf2..74055df8 100644
    --- a/couchpotato/core/providers/torrent/thepiratebay/main.py
    +++ b/couchpotato/core/providers/torrent/thepiratebay/main.py
    @@ -31,7 +31,7 @@ class ThePirateBay(TorrentProvider):
         def find(self, movie, quality, type):
     
             results = []
    -        if not self.enabled() or not self.isAvailable(self.apiUrl):
    +        if not self.enabled():
                 return results
     
             url = self.apiUrl % (quote_plus(self.toSearchString(movie.name + ' ' + quality) + self.makeIgnoreString(type)), self.getCatId(type))
    
    From 747594d626adceb9131d658336f93ed099d279ee Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 20 May 2012 21:34:42 +0200
    Subject: [PATCH 032/207] Simplify cache_key
    
    ---
     couchpotato/core/plugins/base.py | 3 ++-
     1 file changed, 2 insertions(+), 1 deletion(-)
    
    diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py
    index 998097d8..142c1eba 100644
    --- a/couchpotato/core/plugins/base.py
    +++ b/couchpotato/core/plugins/base.py
    @@ -1,6 +1,6 @@
     from couchpotato import addView
     from couchpotato.core.event import fireEvent, addEvent
    -from couchpotato.core.helpers.encoding import tryUrlencode
    +from couchpotato.core.helpers.encoding import tryUrlencode, simplifyString
     from couchpotato.core.helpers.variable import getExt
     from couchpotato.core.logger import CPLog
     from couchpotato.environment import Env
    @@ -199,6 +199,7 @@ class Plugin(object):
     
     
         def getCache(self, cache_key, url = None, **kwargs):
    +        cache_key = simplifyString(cache_key)
             cache = Env.get('cache').get(cache_key)
             if cache:
                 if not Env.get('dev'): log.debug('Getting cache %s' % cache_key)
    
    From 1340ec5d55b99556eab671fdc69c2281eb466f1d Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 20 May 2012 21:41:02 +0200
    Subject: [PATCH 033/207] No need to encode params. fix #311
    
    ---
     couchpotato/core/helpers/request.py | 6 +++---
     1 file changed, 3 insertions(+), 3 deletions(-)
    
    diff --git a/couchpotato/core/helpers/request.py b/couchpotato/core/helpers/request.py
    index 8bedf6c4..7ff4bb29 100644
    --- a/couchpotato/core/helpers/request.py
    +++ b/couchpotato/core/helpers/request.py
    @@ -26,7 +26,7 @@ def getParams():
     
                 for item in nested:
                     if item is nested[-1]:
    -                    current[item] = toUnicode(unquote(value)).encode('utf-8')
    +                    current[item] = toUnicode(unquote(value))
                     else:
                         try:
                             current[item]
    @@ -35,7 +35,7 @@ def getParams():
     
                         current = current[item]
             else:
    -            temp[param] = toUnicode(unquote(value)).encode('utf-8')
    +            temp[param] = toUnicode(unquote(value))
     
         return dictToList(temp)
     
    @@ -57,7 +57,7 @@ def dictToList(params):
     
     def getParam(attr, default = None):
         try:
    -        return toUnicode(unquote(getattr(flask.request, 'args').get(attr, default))).encode('utf-8')
    +        return toUnicode(unquote(getattr(flask.request, 'args').get(attr, default)))
         except:
             return default
     
    
    From 4b49decd10db5bdddd06f66fc3d3e3fdf58bc0a5 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 20 May 2012 22:04:14 +0200
    Subject: [PATCH 034/207] Simplify free provider searchers
    
    ---
     couchpotato/core/providers/nzb/mysterbin/main.py | 7 +++++--
     couchpotato/core/providers/nzb/nzbclub/main.py   | 7 ++++---
     couchpotato/core/providers/nzb/nzbindex/main.py  | 5 +++--
     3 files changed, 12 insertions(+), 7 deletions(-)
    
    diff --git a/couchpotato/core/providers/nzb/mysterbin/main.py b/couchpotato/core/providers/nzb/mysterbin/main.py
    index baf60558..902c37c5 100644
    --- a/couchpotato/core/providers/nzb/mysterbin/main.py
    +++ b/couchpotato/core/providers/nzb/mysterbin/main.py
    @@ -1,6 +1,7 @@
     from BeautifulSoup import BeautifulSoup
     from couchpotato.core.event import fireEvent
    -from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
    +from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode, \
    +    simplifyString
     from couchpotato.core.helpers.variable import tryInt, getTitle
     from couchpotato.core.logger import CPLog
     from couchpotato.core.providers.nzb.base import NZBProvider
    @@ -25,8 +26,10 @@ class Mysterbin(NZBProvider):
             if self.isDisabled():
                 return results
     
    -        q = '"%s" %s %s' % (getTitle(movie['library']), movie['library']['year'], quality.get('identifier'))
    +        q = '"%s" %s %s' % (simplifyString(getTitle(movie['library'])), movie['library']['year'], quality.get('identifier'))
             for ignored in Env.setting('ignored_words', 'searcher').split(','):
    +            if len(q) + len(ignored.strip()) > 126:
    +                break
                 q = '%s -%s' % (q, ignored.strip())
     
             params = {
    diff --git a/couchpotato/core/providers/nzb/nzbclub/main.py b/couchpotato/core/providers/nzb/nzbclub/main.py
    index 3e07356d..e6dbad90 100644
    --- a/couchpotato/core/providers/nzb/nzbclub/main.py
    +++ b/couchpotato/core/providers/nzb/nzbclub/main.py
    @@ -1,6 +1,7 @@
     from BeautifulSoup import BeautifulSoup
     from couchpotato.core.event import fireEvent
    -from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
    +from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode, \
    +    simplifyString
     from couchpotato.core.helpers.rss import RSS
     from couchpotato.core.helpers.variable import tryInt, getTitle
     from couchpotato.core.logger import CPLog
    @@ -27,7 +28,7 @@ class NZBClub(NZBProvider, RSS):
             if self.isDisabled():
                 return results
     
    -        q = '"%s" %s %s' % (getTitle(movie['library']), movie['library']['year'], quality.get('identifier'))
    +        q = '"%s" %s %s' % (simplifyString(getTitle(movie['library'])), movie['library']['year'], quality.get('identifier'))
             for ignored in Env.setting('ignored_words', 'searcher').split(','):
                 q = '%s -%s' % (q, ignored.strip())
     
    @@ -110,7 +111,7 @@ class NZBClub(NZBProvider, RSS):
             full_description = self.getCache('nzbclub.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
     
             if 'ARCHIVE inside ARCHIVE' in full_description:
    -            log.info('Wrong: Seems to be passworded files: %s' % new['name'])
    +            log.info('Wrong: Seems to be passworded files: %s' % item['name'])
                 return False
     
             return True
    diff --git a/couchpotato/core/providers/nzb/nzbindex/main.py b/couchpotato/core/providers/nzb/nzbindex/main.py
    index f0e7ea8b..43364aca 100644
    --- a/couchpotato/core/providers/nzb/nzbindex/main.py
    +++ b/couchpotato/core/providers/nzb/nzbindex/main.py
    @@ -1,6 +1,7 @@
     from BeautifulSoup import BeautifulSoup
     from couchpotato.core.event import fireEvent
    -from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
    +from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode, \
    +    simplifyString
     from couchpotato.core.helpers.rss import RSS
     from couchpotato.core.helpers.variable import tryInt, getTitle
     from couchpotato.core.logger import CPLog
    @@ -29,7 +30,7 @@ class NzbIndex(NZBProvider, RSS):
             if self.isDisabled():
                 return results
     
    -        q = '%s %s %s' % (getTitle(movie['library']), movie['library']['year'], quality.get('identifier'))
    +        q = '%s %s %s' % (simplifyString(getTitle(movie['library'])), movie['library']['year'], quality.get('identifier'))
             arguments = tryUrlencode({
                 'q': q,
                 'age': Env.setting('retention', 'nzb'),
    
    From 4711e09f91a34b4ce6865dc80507a5bb0663e344 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 20 May 2012 22:32:46 +0200
    Subject: [PATCH 035/207] Add imdb id to renaming options
    
    ---
     couchpotato/core/plugins/renamer/__init__.py | 1 +
     couchpotato/core/plugins/renamer/main.py     | 1 +
     2 files changed, 2 insertions(+)
    
    diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py
    index 3fb29f27..d7e78511 100644
    --- a/couchpotato/core/plugins/renamer/__init__.py
    +++ b/couchpotato/core/plugins/renamer/__init__.py
    @@ -19,6 +19,7 @@ rename_options = {
             'source': 'Source media (Bluray)',
             'filename': 'Original filename',
             'original_folder': 'Original foldername',
    +        'imdb_id': 'IMDB id (tt0123456)',
         },
     }
     
    diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py
    index f2b98564..53310c47 100644
    --- a/couchpotato/core/plugins/renamer/main.py
    +++ b/couchpotato/core/plugins/renamer/main.py
    @@ -131,6 +131,7 @@ class Renamer(Plugin):
                          'source': group['meta_data']['source'],
                          'resolution_width': group['meta_data'].get('resolution_width'),
                          'resolution_height': group['meta_data'].get('resolution_height'),
    +                     'imdb_id': library['identifier'],
                     }
     
                     for file_type in group['files']:
    
    From a4c9af24f2809baa51862c4148e88bbda5946919 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 20 May 2012 22:47:17 +0200
    Subject: [PATCH 036/207] Add nzbname for requiredwords log
    
    ---
     couchpotato/core/plugins/searcher/main.py | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py
    index 99392ac7..03d9261f 100644
    --- a/couchpotato/core/plugins/searcher/main.py
    +++ b/couchpotato/core/plugins/searcher/main.py
    @@ -230,7 +230,7 @@ class Searcher(Plugin):
             required_words = [x.strip().lower() for x in self.conf('required_words').lower().split(',')]
     
             if self.conf('required_words') and not list(set(nzb_words) & set(required_words)):
    -            log.info("NZB doesn't contain any of the required words.")
    +            log.info("Wrong: Required word missing: %s" % nzb['name'])
                 return False
     
             ignored_words = [x.strip().lower() for x in self.conf('ignored_words').split(',')]
    
    From cb89e12e9740332e80e8cfc59c25a3938c926868 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 20 May 2012 23:13:53 +0200
    Subject: [PATCH 037/207] Force quit after 30seconds
    
    ---
     couchpotato/core/_base/_core/main.py | 3 +++
     1 file changed, 3 insertions(+)
    
    diff --git a/couchpotato/core/_base/_core/main.py b/couchpotato/core/_base/_core/main.py
    index f7abddec..c1a24c3d 100644
    --- a/couchpotato/core/_base/_core/main.py
    +++ b/couchpotato/core/_base/_core/main.py
    @@ -92,6 +92,7 @@ class Core(Plugin):
             log.debug('Every plugin got shutdown event')
     
             loop = True
    +        starttime = time.time()
             while loop:
                 log.debug('Asking who is running')
                 still_running = fireEvent('plugin.running', merge = True)
    @@ -99,6 +100,8 @@ class Core(Plugin):
     
                 if len(still_running) == 0:
                     break
    +            elif starttime < time.time() - 30: # Always force break after 30s wait
    +                break
     
                 running = list(set(still_running) - set(self.ignore_restart))
                 if len(running) > 0:
    
    From fdf0d2d5b403bad860a06475b1dafce7c28e2da5 Mon Sep 17 00:00:00 2001
    From: Ruud Burger <ruud@crashdummy.nl>
    Date: Thu, 26 Apr 2012 10:57:34 +0300
    Subject: [PATCH 038/207] Use master branch to update master..
    
    ---
     couchpotato/core/_base/updater/main.py | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py
    index 0d7b04f6..87716610 100644
    --- a/couchpotato/core/_base/updater/main.py
    +++ b/couchpotato/core/_base/updater/main.py
    @@ -80,7 +80,7 @@ class BaseUpdater(Plugin):
     
         repo_user = 'RuudBurger'
         repo_name = 'CouchPotatoServer'
    -    branch = 'develop'
    +    branch = 'master'
     
         version = None
         update_failed = False
    
    From b350f1e798540dc67c0bf7ef077dc6ff8a206136 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Tue, 22 May 2012 00:14:09 +0200
    Subject: [PATCH 039/207] OnComplete handler for events and async re-use
    
    ---
     couchpotato/core/event.py | 25 ++++++++++++++++---------
     1 file changed, 16 insertions(+), 9 deletions(-)
    
    diff --git a/couchpotato/core/event.py b/couchpotato/core/event.py
    index 82f81e21..e8fe5b9b 100644
    --- a/couchpotato/core/event.py
    +++ b/couchpotato/core/event.py
    @@ -1,6 +1,7 @@
     from axl.axel import Event
     from couchpotato.core.helpers.variable import mergeDicts, natcmp
     from couchpotato.core.logger import CPLog
    +import thread
     import threading
     import traceback
     
    @@ -53,6 +54,13 @@ def fireEvent(name, *args, **kwargs):
                 is_after_event = True
             except: pass
     
    +        # onComplete event
    +        on_complete = False
    +        try:
    +            on_complete = kwargs['on_complete']
    +            del kwargs['on_complete']
    +        except: pass
    +
             # Return single handler
             single = False
             try:
    @@ -129,24 +137,23 @@ def fireEvent(name, *args, **kwargs):
             if not is_after_event:
                 fireEvent('%s.after' % name, is_after_event = True)
     
    +        if on_complete:
    +            on_complete()
    +
             return results
         except KeyError, e:
             pass
         except Exception:
             log.error('%s: %s' % (name, traceback.format_exc()))
     
    -def fireEventAsync(name, *args, **kwargs):
    -    #log.debug('Async "%s": %s, %s' % (name, args, kwargs))
    +def fireEventAsync(*args, **kwargs):
         try:
    -        e = events[name]
    -        e.lock.acquire()
    -        e.asynchronous = True
    -        e.error_handler = errorHandler
    -        e(*args, **kwargs)
    -        e.lock.release()
    +        my_thread = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
    +        my_thread.setDaemon(True)
    +        my_thread.start()
             return True
         except Exception, e:
    -        log.error('%s: %s' % (name, e))
    +        log.error('%s: %s' % (args[0], e))
     
     def errorHandler(error):
         etype, value, tb = error
    
    From 06db2d98502642253e25fe39f2e16515970c96dd Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Tue, 22 May 2012 00:14:46 +0200
    Subject: [PATCH 040/207] Single movie UI update
    
    ---
     couchpotato/core/notifications/core/main.py   |   9 +-
     .../notifications/core/static/notification.js |   2 +-
     couchpotato/core/plugins/library/main.py      |   2 +-
     couchpotato/core/plugins/movie/main.py        |  12 +-
     .../core/plugins/movie/static/movie.js        | 139 +++++++++++++-----
     couchpotato/core/plugins/searcher/main.py     |  14 +-
     couchpotato/static/scripts/page/wanted.js     |   2 +-
     7 files changed, 124 insertions(+), 56 deletions(-)
    
    diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py
    index 10a1401d..5419049e 100644
    --- a/couchpotato/core/notifications/core/main.py
    +++ b/couchpotato/core/notifications/core/main.py
    @@ -1,6 +1,6 @@
     from couchpotato import get_session
     from couchpotato.api import addApiView
    -from couchpotato.core.event import addEvent, fireEvent
    +from couchpotato.core.event import addEvent
     from couchpotato.core.helpers.encoding import toUnicode
     from couchpotato.core.helpers.request import jsonified, getParam
     from couchpotato.core.helpers.variable import tryInt
    @@ -48,13 +48,6 @@ class CoreNotifier(Notification):
     
             addApiView('notification.listener', self.listener)
     
    -        self.registerEvents()
    -
    -    def registerEvents(self):
    -
    -        # Library update, frontend refresh
    -        addEvent('library.update_finish', lambda data: fireEvent('notify.frontend', type = 'library.update', data = data))
    -
         def markAsRead(self):
             ids = [x.strip() for x in getParam('ids').split(',')]
     
    diff --git a/couchpotato/core/notifications/core/static/notification.js b/couchpotato/core/notifications/core/static/notification.js
    index 371b95ed..e35a394c 100644
    --- a/couchpotato/core/notifications/core/static/notification.js
    +++ b/couchpotato/core/notifications/core/static/notification.js
    @@ -88,7 +88,7 @@ var NotificationBase = new Class({
     
     		self.request = Api.request('notification.listener', {
     			'initialDelay': 100,
    -    		'delay': 3000,
    +    		'delay': 1500,
         		'data': {'init':true},
         		'onSuccess': self.processData.bind(self)
     		})
    diff --git a/couchpotato/core/plugins/library/main.py b/couchpotato/core/plugins/library/main.py
    index 347d8580..0e4fc8cc 100644
    --- a/couchpotato/core/plugins/library/main.py
    +++ b/couchpotato/core/plugins/library/main.py
    @@ -127,7 +127,7 @@ class LibraryPlugin(Plugin):
     
                 library_dict = library.to_dict(self.default_dict)
     
    -        fireEvent('library.update_finish', data = library_dict)
    +        fireEvent('notify.frontend', type = 'library.update.%s' % identifier, data = library_dict)
     
             #db.close()
             return library_dict
    diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py
    index 2c5ec731..60aa7adc 100644
    --- a/couchpotato/core/plugins/movie/main.py
    +++ b/couchpotato/core/plugins/movie/main.py
    @@ -247,8 +247,16 @@ class MoviePlugin(Plugin):
                     if title.default: default_title = title.title
     
                 if movie:
    -                fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True)
    -                fireEventAsync('searcher.single', movie.to_dict(self.default_dict))
    +                def notifyFront():
    +                    movie = db.query(Movie).filter_by(id = id).first()
    +                    fireEvent('notify.frontend', type = 'movie.update.%s' % movie.id, data = movie.to_dict(self.default_dict))
    +
    +                def afterUpdate():
    +                    movie = db.query(Movie).filter_by(id = id).first()
    +                    fireEventAsync('searcher.single', movie.to_dict(self.default_dict), on_complete = notifyFront)
    +
    +                fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = afterUpdate)
    +
     
             #db.close()
             return jsonified({
    diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js
    index 39b12eed..9b8fa22b 100644
    --- a/couchpotato/core/plugins/movie/static/movie.js
    +++ b/couchpotato/core/plugins/movie/static/movie.js
    @@ -11,53 +11,114 @@ var Movie = new Class({
     		self.view = options.view || 'thumbs';
     		self.list = list;
     
    +		self.el = new Element('div.movie.inlay');
    +
     		self.profile = Quality.getProfile(data.profile_id) || {};
     		self.parent(self, options);
    +
    +		App.addEvent('movie.update.'+data.id, self.update.bind(self));
    +		App.addEvent('searcher.started.'+data.id, self.searching.bind(self));
    +		App.addEvent('searcher.ended.'+data.id, self.searching.bind(self));
    +	},
    +
    +	searching: function(notification){
    +		var self = this;
    +
    +		if(notification && notification.type.indexOf('ended') > -1){
    +			if(self.spinner){
    +				self.mask.fade('out');
    +				setTimeout(function(){
    +					self.mask.destroy();
    +					self.spinner.el.destroy();
    +					self.spinner = null;
    +					self.mask = null;
    +				}, 400);
    +			}
    +		}
    +		else if(!self.spinner) {
    +			self.createMask();
    +			self.spinner = createSpinner(self.mask);
    +			self.positionMask();
    +			self.mask.fade('in');
    +		}
    +	},
    +
    +	createMask: function(){
    +		var self = this;
    +		self.mask = new Element('div.mask', {
    +			'styles': {
    +				'z-index': '1'
    +			}
    +		}).inject(self.el, 'top').fade('hide');
    +		self.positionMask();
    +	},
    +
    +	positionMask: function(){
    +		var self = this,
    +			s = self.el.getSize()
    +
    +		return self.mask.setStyles({
    +			'width': s.x,
    +			'height': s.y
    +		}).position({
    +			'relativeTo': self.el
    +		})
    +	},
    +
    +	update: function(notification){
    +		var self = this;
    +
    +		self.data = notification.data;
    +		self.container.destroy();
    +		self.profile = Quality.getProfile(self.data.profile_id) || {};
    +		self.create();
     	},
     
     	create: function(){
     		var self = this;
     
    -		self.el = new Element('div.movie.inlay').adopt(
    -			self.select_checkbox = new Element('input[type=checkbox].inlay', {
    -				'events': {
    -					'change': function(){
    -						self.fireEvent('select')
    -					}
    -				}
    -			}),
    -			self.thumbnail = File.Select.single('poster', self.data.library.files),
    -			self.data_container = new Element('div.data.inlay.light', {
    -				'tween': {
    -					duration: 400,
    -					transition: 'quint:in:out',
    -					onComplete: self.fireEvent.bind(self, 'slideEnd')
    -				}
    -			}).adopt(
    -				self.info_container = new Element('div.info').adopt(
    -					self.title = new Element('div.title', {
    -						'text': self.getTitle() || 'n/a'
    -					}),
    -					self.year = new Element('div.year', {
    -						'text': self.data.library.year || 'n/a'
    -					}),
    -					self.rating = new Element('div.rating.icon', {
    -						'text': self.data.library.rating
    -					}),
    -					self.description = new Element('div.description', {
    -						'text': self.data.library.plot
    -					}),
    -					self.quality = new Element('div.quality', {
    -						'events': {
    -							'click': function(e){
    -								var releases = self.el.getElement('.actions .releases');
    -									if(releases)
    -										releases.fireEvent('click', [e])
    -							}
    +		self.el.adopt(
    +			self.container = new Element('div').adopt(
    +				self.select_checkbox = new Element('input[type=checkbox].inlay', {
    +					'events': {
    +						'change': function(){
    +							self.fireEvent('select')
     						}
    -					})
    -				),
    -				self.actions = new Element('div.actions')
    +					}
    +				}),
    +				self.thumbnail = File.Select.single('poster', self.data.library.files),
    +				self.data_container = new Element('div.data.inlay.light', {
    +					'tween': {
    +						duration: 400,
    +						transition: 'quint:in:out',
    +						onComplete: self.fireEvent.bind(self, 'slideEnd')
    +					}
    +				}).adopt(
    +					self.info_container = new Element('div.info').adopt(
    +						self.title = new Element('div.title', {
    +							'text': self.getTitle() || 'n/a'
    +						}),
    +						self.year = new Element('div.year', {
    +							'text': self.data.library.year || 'n/a'
    +						}),
    +						self.rating = new Element('div.rating.icon', {
    +							'text': self.data.library.rating
    +						}),
    +						self.description = new Element('div.description', {
    +							'text': self.data.library.plot
    +						}),
    +						self.quality = new Element('div.quality', {
    +							'events': {
    +								'click': function(e){
    +									var releases = self.el.getElement('.actions .releases');
    +										if(releases)
    +											releases.fireEvent('click', [e])
    +								}
    +							}
    +						})
    +					),
    +					self.actions = new Element('div.actions')
    +				)
     			)
     		);
     
    diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py
    index 03d9261f..93eb55ed 100644
    --- a/couchpotato/core/plugins/searcher/main.py
    +++ b/couchpotato/core/plugins/searcher/main.py
    @@ -83,6 +83,9 @@ class Searcher(Plugin):
             if not default_title:
                 return
     
    +        fireEvent('notify.frontend', type = 'searcher.started.%s' % movie['id'], data = True)
    +
    +        ret = False
             for quality_type in movie['profile']['types']:
                 if not self.couldBeReleased(quality_type['quality']['identifier'], release_dates, pre_releases):
                     log.info('To early to search for %s, %s' % (quality_type['quality']['identifier'], default_title))
    @@ -107,7 +110,7 @@ class Searcher(Plugin):
     
                     # Check if movie isn't deleted while searching
                     if not db.query(Movie).filter_by(id = movie.get('id')).first():
    -                    return
    +                    break
     
                     # Add them to this movie releases list
                     for nzb in sorted_results:
    @@ -144,7 +147,8 @@ class Searcher(Plugin):
                     for nzb in sorted_results:
                         downloaded = self.download(data = nzb, movie = movie)
                         if downloaded is True:
    -                        return True
    +                        ret = True
    +                        break
                         elif downloaded != 'try_next':
                             break
                 else:
    @@ -153,11 +157,13 @@ class Searcher(Plugin):
                     break
     
                 # Break if CP wants to shut down
    -            if self.shuttingDown():
    +            if self.shuttingDown() or ret:
                     break
     
    +        fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True)
    +
             #db.close()
    -        return False
    +        return ret
     
         def download(self, data, movie, manual = False):
     
    diff --git a/couchpotato/static/scripts/page/wanted.js b/couchpotato/static/scripts/page/wanted.js
    index 1f811c69..f8858e72 100644
    --- a/couchpotato/static/scripts/page/wanted.js
    +++ b/couchpotato/static/scripts/page/wanted.js
    @@ -17,7 +17,6 @@ Page.Wanted = new Class({
     				'actions': MovieActions
     			});
     			$(self.wanted).inject(self.el);
    -			App.addEvent('library.update', self.wanted.update.bind(self.wanted));
     		}
     
     	}
    @@ -131,6 +130,7 @@ window.addEvent('domready', function(){
     				var self = this;
     				(e).preventDefault();
     
    +				self.movie.searching();
     				Api.request('movie.refresh', {
     					'data': {
     						'id': self.movie.get('id')
    
    From f874e9c4e3e7a74ee96de2622eb966e807312b8a Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Mon, 21 May 2012 10:59:05 +0200
    Subject: [PATCH 041/207] Always return application/json
    
    ---
     couchpotato/core/helpers/request.py | 3 +--
     1 file changed, 1 insertion(+), 2 deletions(-)
    
    diff --git a/couchpotato/core/helpers/request.py b/couchpotato/core/helpers/request.py
    index 7ff4bb29..16a2f73d 100644
    --- a/couchpotato/core/helpers/request.py
    +++ b/couchpotato/core/helpers/request.py
    @@ -70,9 +70,8 @@ def jsonify(mimetype, *args, **kwargs):
         return getattr(current_app, 'response_class')(content, mimetype = mimetype)
     
     def jsonified(*args, **kwargs):
    -    from couchpotato.environment import Env
         callback = getParam('callback_func', None)
         if callback:
             return padded_jsonify(callback, *args, **kwargs)
         else:
    -        return jsonify('text/javascript' if Env.doDebug() else 'application/json', *args, **kwargs)
    +        return jsonify('application/json', *args, **kwargs)
    
    From 30fa8a7531b3d2ad3d7198c27653928aa4407d10 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Tue, 22 May 2012 20:59:26 +0200
    Subject: [PATCH 042/207] Cleanup
    
    ---
     couchpotato/core/event.py | 1 -
     1 file changed, 1 deletion(-)
    
    diff --git a/couchpotato/core/event.py b/couchpotato/core/event.py
    index e8fe5b9b..b7bed808 100644
    --- a/couchpotato/core/event.py
    +++ b/couchpotato/core/event.py
    @@ -1,7 +1,6 @@
     from axl.axel import Event
     from couchpotato.core.helpers.variable import mergeDicts, natcmp
     from couchpotato.core.logger import CPLog
    -import thread
     import threading
     import traceback
     
    
    From 4774deaa80d86c20ce7beb133ccf51fc7d04dbdc Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Tue, 22 May 2012 21:28:20 +0200
    Subject: [PATCH 043/207] Don't hide options on slidein
    
    ---
     couchpotato/core/plugins/movie/static/movie.js | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js
    index 9b8fa22b..57b58dc7 100644
    --- a/couchpotato/core/plugins/movie/static/movie.js
    +++ b/couchpotato/core/plugins/movie/static/movie.js
    @@ -78,7 +78,7 @@ var Movie = new Class({
     		var self = this;
     
     		self.el.adopt(
    -			self.container = new Element('div').adopt(
    +			self.container = new Element('div.movie_container').adopt(
     				self.select_checkbox = new Element('input[type=checkbox].inlay', {
     					'events': {
     						'change': function(){
    @@ -211,7 +211,7 @@ var Movie = new Class({
     			self.el.removeEvents('outerClick')
     
     			self.addEvent('slideEnd:once', function(){
    -				self.el.getElements('> :not(.data):not(.poster)').hide();
    +				self.el.getElements('> :not(.data):not(.poster):not(.movie_container)').hide();
     			});
     
     			self.data_container.tween('right', -840, 0);
    
    From d362827a8badcfa4e8a1cf6fc88b593ceac874b8 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Tue, 22 May 2012 21:52:06 +0200
    Subject: [PATCH 044/207] Update on edit
    
    ---
     couchpotato/core/plugins/movie/main.py    | 31 +++++++++++++++--------
     couchpotato/static/scripts/page/wanted.js |  8 +++++-
     2 files changed, 28 insertions(+), 11 deletions(-)
    
    diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py
    index 60aa7adc..de307799 100644
    --- a/couchpotato/core/plugins/movie/main.py
    +++ b/couchpotato/core/plugins/movie/main.py
    @@ -247,15 +247,7 @@ class MoviePlugin(Plugin):
                     if title.default: default_title = title.title
     
                 if movie:
    -                def notifyFront():
    -                    movie = db.query(Movie).filter_by(id = id).first()
    -                    fireEvent('notify.frontend', type = 'movie.update.%s' % movie.id, data = movie.to_dict(self.default_dict))
    -
    -                def afterUpdate():
    -                    movie = db.query(Movie).filter_by(id = id).first()
    -                    fireEventAsync('searcher.single', movie.to_dict(self.default_dict), on_complete = notifyFront)
    -
    -                fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = afterUpdate)
    +                fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(id))
     
     
             #db.close()
    @@ -377,7 +369,7 @@ class MoviePlugin(Plugin):
                 fireEvent('movie.restatus', m.id)
     
                 movie_dict = m.to_dict(self.default_dict)
    -            fireEventAsync('searcher.single', movie_dict)
    +            fireEventAsync('searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id))
     
             #db.close()
             return jsonified({
    @@ -466,3 +458,22 @@ class MoviePlugin(Plugin):
             #db.close()
     
             return True
    +
    +    def createOnComplete(self, movie_id):
    +
    +        def onComplete():
    +            db = get_session()
    +            movie = db.query(Movie).filter_by(id = movie_id).first()
    +            fireEventAsync('searcher.single', movie.to_dict(self.default_dict), on_complete = self.createNotifyFront(movie_id))
    +
    +        return onComplete
    +
    +
    +    def createNotifyFront(self, movie_id):
    +
    +        def notifyFront():
    +            db = get_session()
    +            movie = db.query(Movie).filter_by(id = movie_id).first()
    +            fireEvent('notify.frontend', type = 'movie.update.%s' % movie.id, data = movie.to_dict(self.default_dict))
    +
    +        return notifyFront
    diff --git a/couchpotato/static/scripts/page/wanted.js b/couchpotato/static/scripts/page/wanted.js
    index f8858e72..9566058e 100644
    --- a/couchpotato/static/scripts/page/wanted.js
    +++ b/couchpotato/static/scripts/page/wanted.js
    @@ -72,14 +72,20 @@ window.addEvent('domready', function(){
     						new Element('option', {
     							'text': alt.title
     						}).inject(self.title_select);
    +						
    +						if(alt['default'])
    +							self.title_select.set('value', alt.title);
     					});
     
    +
     					Quality.getActiveProfiles().each(function(profile){
     						new Element('option', {
     							'value': profile.id ? profile.id : profile.data.id,
     							'text': profile.label ? profile.label : profile.data.label
     						}).inject(self.profile_select);
    -						self.profile_select.set('value', (self.movie.profile || {})['id']);
    +
    +						if(self.movie.profile)
    +							self.profile_select.set('value', self.movie.profile.data.id);
     					});
     
     				}
    
    From a1ac73a9a1be603e3ef58864a46ab5f4ab7ec6dd Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Tue, 22 May 2012 22:07:35 +0200
    Subject: [PATCH 045/207] Add m4v extension. fix #332
    
    ---
     couchpotato/core/plugins/scanner/main.py | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py
    index e287fd33..bbf25239 100644
    --- a/couchpotato/core/plugins/scanner/main.py
    +++ b/couchpotato/core/plugins/scanner/main.py
    @@ -27,7 +27,7 @@ class Scanner(Plugin):
         ignored_in_path = ['_unpack', '_failed_', '_unknown_', '_exists_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo'] #unpacking, smb-crap, hidden files
         ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts', 'bdmv', 'certificate']
         extensions = {
    -        'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img', 'mdf', 'ts'],
    +        'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img', 'mdf', 'ts', 'm4v'],
             'movie_extra': ['mds'],
             'dvd': ['vts_*', 'vob'],
             'nfo': ['nfo', 'txt', 'tag'],
    
    From 75b734db7230fe8203b95fe6cab09fef47febfe7 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Tue, 22 May 2012 23:12:24 +0200
    Subject: [PATCH 046/207] No error when trailers aren't found. fix #327
    
    ---
     couchpotato/core/plugins/trailer/main.py | 5 ++++-
     1 file changed, 4 insertions(+), 1 deletion(-)
    
    diff --git a/couchpotato/core/plugins/trailer/main.py b/couchpotato/core/plugins/trailer/main.py
    index 8f8e4ab2..7c6d5d3f 100644
    --- a/couchpotato/core/plugins/trailer/main.py
    +++ b/couchpotato/core/plugins/trailer/main.py
    @@ -1,5 +1,5 @@
     from couchpotato.core.event import addEvent, fireEvent
    -from couchpotato.core.helpers.variable import getExt
    +from couchpotato.core.helpers.variable import getExt, getTitle
     from couchpotato.core.logger import CPLog
     from couchpotato.core.plugins.base import Plugin
     import os
    @@ -17,6 +17,9 @@ class Trailer(Plugin):
             if self.isDisabled() or len(group['files']['trailer']) > 0: return
     
             trailers = fireEvent('trailer.search', group = group, merge = True)
    +        if not trailers or trailers == []:
    +            log.info('No trailers found for: %s' % getTitle(group['library']))
    +            return
     
             for trailer in trailers.get(self.conf('quality'), []):
                 destination = '%s-trailer.%s' % (self.getRootName(group), getExt(trailer))
    
    From 11958334ac01ffac6aba1bc2896a362483edc94f Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Wed, 23 May 2012 23:00:27 +0200
    Subject: [PATCH 047/207] Add new movie to top of wanted list. closes #301
    
    ---
     couchpotato/core/plugins/library/main.py      |  4 +-
     couchpotato/core/plugins/movie/main.py        | 21 ++++++--
     couchpotato/core/plugins/movie/static/list.js | 52 +++++++++++++------
     .../core/plugins/movie/static/movie.js        |  6 ++-
     couchpotato/static/scripts/page/wanted.js     |  3 +-
     5 files changed, 59 insertions(+), 27 deletions(-)
    
    diff --git a/couchpotato/core/plugins/library/main.py b/couchpotato/core/plugins/library/main.py
    index 0e4fc8cc..a7960d7c 100644
    --- a/couchpotato/core/plugins/library/main.py
    +++ b/couchpotato/core/plugins/library/main.py
    @@ -138,8 +138,8 @@ class LibraryPlugin(Plugin):
             library = db.query(Library).filter_by(identifier = identifier).first()
     
             if not library.info:
    -            self.update(identifier)
    -            dates = library.get('info', {}).get('release_dates')
    +            library_dict = self.update(identifier)
    +            dates = library_dict.get('info', {}).get('release_dates')
             else:
                 dates = library.info.get('release_date')
     
    diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py
    index de307799..70ee39a5 100644
    --- a/couchpotato/core/plugins/movie/main.py
    +++ b/couchpotato/core/plugins/movie/main.py
    @@ -287,7 +287,7 @@ class MoviePlugin(Plugin):
     
             db = get_session()
             m = db.query(Movie).filter_by(library_id = library.get('id')).first()
    -        do_search = False
    +        added = True
             if not m:
                 m = Movie(
                     library_id = library.get('id'),
    @@ -295,8 +295,13 @@ class MoviePlugin(Plugin):
                     status_id = status_active.get('id'),
                 )
                 db.add(m)
    -            fireEvent('library.update', params.get('identifier'), default_title = params.get('title', ''))
    -            do_search = True
    +            db.commit()
    +
    +            onComplete = None
    +            if search_after:
    +                onComplete = self.createOnComplete(m.id)
    +
    +            fireEventAsync('library.update', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete)
             elif force_readd:
                 # Clean snatched history
                 for release in m.releases:
    @@ -306,9 +311,11 @@ class MoviePlugin(Plugin):
                 m.profile_id = params.get('profile_id', default_profile.get('id'))
             else:
                 log.debug('Movie already exists, not updating: %s' % params)
    +            added = False
     
             if force_readd:
                 m.status_id = status_active.get('id')
    +            do_search = True
     
             db.commit()
     
    @@ -321,8 +328,12 @@ class MoviePlugin(Plugin):
     
             movie_dict = m.to_dict(self.default_dict)
     
    -        if (force_readd or do_search) and search_after:
    -            fireEventAsync('searcher.single', movie_dict)
    +        if do_search and search_after:
    +            onComplete = self.createOnComplete(m.id)
    +            onComplete()
    +
    +        if added:
    +            fireEvent('notify.frontend', type = 'movie.added', data = movie_dict)
     
             #db.close()
             return movie_dict
    diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js
    index 23bdcf89..e8266acb 100644
    --- a/couchpotato/core/plugins/movie/static/list.js
    +++ b/couchpotato/core/plugins/movie/static/list.js
    @@ -5,10 +5,12 @@ var MovieList = new Class({
     	options: {
     		navigation: true,
     		limit: 50,
    -		menu: []
    +		menu: [],
    +		add_new: false
     	},
     
     	movies: [],
    +	movies_added: [],
     	letters: {},
     	filter: {
     		'startswith': null,
    @@ -30,6 +32,17 @@ var MovieList = new Class({
     			})
     		);
     		self.getMovies();
    +
    +		if(options.add_new)
    +			App.addEvent('movie.added', self.movieAdded.bind(self))
    +	},
    +
    +	movieAdded: function(notification){
    +		var self = this;
    +		window.scroll(0,0);
    +
    +		if(self.movies_added[notification.data.id])
    +			self.createMovie(notification.data, 'top');
     	},
     
     	create: function(){
    @@ -71,26 +84,31 @@ var MovieList = new Class({
     		}
     
     		Object.each(movies, function(movie){
    -
    -			// Attach proper actions
    -			var a = self.options.actions,
    -				status = Status.get(movie.status_id);
    -			var actions = a[status.identifier.capitalize()] || a.Wanted || {};
    -
    -			var m = new Movie(self, {
    -				'actions': actions,
    -				'view': self.current_view,
    -				'onSelect': self.calculateSelected.bind(self)
    -			}, movie);
    -			$(m).inject(self.movie_list);
    -			m.fireEvent('injected');
    -
    -			self.movies.include(m)
    -
    +			self.createMovie(movie);
     		});
     
     	},
     
    +	createMovie: function(movie, inject_at){
    +		var self = this;
    +
    +		// Attach proper actions
    +		var a = self.options.actions,
    +			status = Status.get(movie.status_id);
    +		var actions = a[status.identifier.capitalize()] || a.Wanted || {};
    +
    +		var m = new Movie(self, {
    +			'actions': actions,
    +			'view': self.current_view,
    +			'onSelect': self.calculateSelected.bind(self)
    +		}, movie);
    +		$(m).inject(self.movie_list, inject_at || 'bottom');
    +		m.fireEvent('injected');
    +
    +		self.movies.include(m)
    +		self.movies_added.include(movie.id);
    +	},
    +
     	createNavigation: function(){
     		var self = this;
     		var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ';
    diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js
    index 57b58dc7..c93a98ea 100644
    --- a/couchpotato/core/plugins/movie/static/movie.js
    +++ b/couchpotato/core/plugins/movie/static/movie.js
    @@ -28,8 +28,10 @@ var Movie = new Class({
     			if(self.spinner){
     				self.mask.fade('out');
     				setTimeout(function(){
    -					self.mask.destroy();
    -					self.spinner.el.destroy();
    +					if(self.mask)
    +						self.mask.destroy();
    +					if(self.spinner)
    +						self.spinner.el.destroy();
     					self.spinner = null;
     					self.mask = null;
     				}, 400);
    diff --git a/couchpotato/static/scripts/page/wanted.js b/couchpotato/static/scripts/page/wanted.js
    index 9566058e..2ec49e2b 100644
    --- a/couchpotato/static/scripts/page/wanted.js
    +++ b/couchpotato/static/scripts/page/wanted.js
    @@ -14,7 +14,8 @@ Page.Wanted = new Class({
     			self.wanted = new MovieList({
     				'identifier': 'wanted',
     				'status': 'active',
    -				'actions': MovieActions
    +				'actions': MovieActions,
    +				'add_new': true
     			});
     			$(self.wanted).inject(self.el);
     		}
    
    From 556752fd3f4068b2764cc219478f9e404ef624f3 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Thu, 24 May 2012 22:58:09 +0200
    Subject: [PATCH 048/207] Original filename
    
    ---
     couchpotato/core/plugins/renamer/__init__.py | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py
    index d7e78511..27566d17 100644
    --- a/couchpotato/core/plugins/renamer/__init__.py
    +++ b/couchpotato/core/plugins/renamer/__init__.py
    @@ -17,7 +17,7 @@ rename_options = {
             'audio': 'Audio (DTS)',
             'group': 'Releasegroup name',
             'source': 'Source media (Bluray)',
    -        'filename': 'Original filename',
    +        'original': 'Original filename',
             'original_folder': 'Original foldername',
             'imdb_id': 'IMDB id (tt0123456)',
         },
    
    From 7604a36cd79acc0e670e398dc96d029908a9732d Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Thu, 24 May 2012 23:48:39 +0200
    Subject: [PATCH 049/207] Explain imdb watchlist. fix #342
    
    ---
     couchpotato/core/providers/automation/imdb/__init__.py | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/couchpotato/core/providers/automation/imdb/__init__.py b/couchpotato/core/providers/automation/imdb/__init__.py
    index 338a8b62..142ebab4 100644
    --- a/couchpotato/core/providers/automation/imdb/__init__.py
    +++ b/couchpotato/core/providers/automation/imdb/__init__.py
    @@ -10,7 +10,7 @@ config = [{
                 'tab': 'automation',
                 'name': 'imdb_automation',
                 'label': 'IMDB',
    -            'description': 'From any <strong>public</strong> IMDB watchlists',
    +            'description': 'From any <strong>public</strong> IMDB watchlists. Url should end with <strong>export?list_id=XXXXX&author_id=XXXXX<strong>',
                 'options': [
                     {
                         'name': 'automation_enabled',
    
    From 91bae8c1b5a1f998160663e9394bbe9bb9455f46 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Fri, 25 May 2012 16:11:54 +0200
    Subject: [PATCH 050/207] Don't search twice.
    
    ---
     couchpotato/core/plugins/movie/main.py | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py
    index 70ee39a5..f8415d1e 100644
    --- a/couchpotato/core/plugins/movie/main.py
    +++ b/couchpotato/core/plugins/movie/main.py
    @@ -302,6 +302,7 @@ class MoviePlugin(Plugin):
                     onComplete = self.createOnComplete(m.id)
     
                 fireEventAsync('library.update', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete)
    +            do_search = False
             elif force_readd:
                 # Clean snatched history
                 for release in m.releases:
    
    From 84eecaac6180bbc98b933e83e1ba69dad3365954 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Fri, 25 May 2012 17:21:40 +0200
    Subject: [PATCH 051/207] Movies didn't add
    
    ---
     couchpotato/core/plugins/movie/static/list.js | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js
    index e8266acb..800f1711 100644
    --- a/couchpotato/core/plugins/movie/static/list.js
    +++ b/couchpotato/core/plugins/movie/static/list.js
    @@ -41,7 +41,7 @@ var MovieList = new Class({
     		var self = this;
     		window.scroll(0,0);
     
    -		if(self.movies_added[notification.data.id])
    +		if(!self.movies_added[notification.data.id])
     			self.createMovie(notification.data, 'top');
     	},
     
    
    From bc47b5fad559831e28770dc71d71e9d0e69b6db7 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Fri, 25 May 2012 20:18:03 +0200
    Subject: [PATCH 052/207] IMDB automation crashed on second watchlist. fix #322
    
    ---
     .../core/providers/automation/imdb/main.py      |   9 +++++++++
     couchpotato/static/images/imdb_watchlist.png    | Bin 0 -> 32721 bytes
     couchpotato/static/style/page/settings.css      |   7 ++++++-
     3 files changed, 15 insertions(+), 1 deletion(-)
     create mode 100644 couchpotato/static/images/imdb_watchlist.png
    
    diff --git a/couchpotato/core/providers/automation/imdb/main.py b/couchpotato/core/providers/automation/imdb/main.py
    index 02fde26b..248db2d2 100644
    --- a/couchpotato/core/providers/automation/imdb/main.py
    +++ b/couchpotato/core/providers/automation/imdb/main.py
    @@ -23,7 +23,14 @@ class IMDB(Automation):
             movies = []
             headers = {}
     
    +        enablers = self.conf('automation_urls_use').split(',')
    +
    +        index = -1
             for csv_url in self.conf('automation_urls').split(','):
    +            index += 1
    +            if not enablers[index]:
    +                continue
    +
                 prop_name = 'automation.imdb.last_update.%s' % md5(csv_url)
                 last_update = float(Env.prop(prop_name, default = 0))
     
    @@ -36,6 +43,8 @@ class IMDB(Automation):
                         for column in csv_reader.next():
                             headers[column] = nr
                             nr += 1
    +                else:
    +                    csv_reader.next()
     
                     for row in csv_reader:
                         created = int(time.mktime(parse(row[headers['created']]).timetuple()))
    diff --git a/couchpotato/static/images/imdb_watchlist.png b/couchpotato/static/images/imdb_watchlist.png
    new file mode 100644
    index 0000000000000000000000000000000000000000..b16fd0940abf5d140eba24c8c0d8e5b7d432e488
    GIT binary patch
    literal 32721
    zcmaI7V_;-W*Df5}b~4d)Y))+3?%1|%Yhv5BolNXWGO_JU{N;Y|p6|zbPyg6mRlQWJ
    zu3cKIBNXJs5#jLQKtMncB_%|ZKtMp1zshScP+w2{p#0gdFI;C)4QFLLQ)f4zqX~$h
    zv7Mm_iKGqC%tXlqXzbxIZo&fs0#0tBqT#F|E5mJMXG0JChlbwW#{P>N1cZm*-5zLU
    zW#UX?Xkunz%S(FI*-J`dVa!Xa&L+z!YcFhKZXx06Xrk;Xr()!3WyEDn%Fjo_<Ieph
    zz{bQGNaAi|ZR^DC&P)0)x!hmnf0`LcN&ZFRY{g6ZZ=*D16-b2b98E~r=o#sZ7}=Rf
    z*jeeBSQ)ukxoAn48JQRvn3x$DIp~-ex!E|l8JS4_^CA7x=4fontt2A$A6;KHUQ%;s
    zXM1i21~)f1dN&q&J4Z7HCN3^621aHEW@frC3OXkbTW6p<ovjnue<g^RI2k!w*gIR;
    z*^>Mt5ol=V;>=6>W$C}WU}OJ3Vr`xN<EAf%F}MTm8JOr9|LM|y31wyf-=sD+|3mHM
    ztYq?k?ESwAc2e=MH(^jRak6u9H2NAiQ?h?l*>ej!ngE^c998V>tpDpp1#>%RJ128H
    zdlF$~HhLx!S)h@H?LRI5;*gc)mb7(p2HF~#NQ&^1e#xM>urTIk6JlXuVPX;C5*6ZP
    zViIK%5@ZtQV&h_AWM*U(WMmioFRzH5k&BIqt@D3*jsFj?=>N+54;XChzch=OI9j-x
    z7>haD*^vCJ)7%#SyDdWhtGxg48vpOM2>-9V3}41D{4=!wzoGtb(-(gJDgF;|zbgL&
    zeG}U+ygPmY`(3#269mLtSW-k##eMBu7g9h)6Z>P65kWX8Z#5u$do5zM{&CH!)vBug
    zP^&kaW@N3}iq@6Jpyp3xKm-_6{(T=p*Jn>!ItMJE?D)q^S~~XAAFr*>E#9LIuNm<<
    zRzy;)K-r2`O}yX67S?JfZ>+7jowdH3!OvI+KT_PTHv6ZhE}}MQ*w`?ssHhaEQzYc%
    zmaVB%B6&xw3&IPqA`AMA0Pqq8;iPa3Xc57GXlZFllEyU*OxU4B(!~mGIlqV)NYf%o
    zrSS49pcGIgL{a-yFckR)qm9@VMI;-+u_^^H6h%-)A(a1lV$4lqizeG?rUjH16_LA=
    z=l&MSEeHIIA4-swi76o)&BVk6vRwzMT!s2Y{@=ppX%PHJhKK7(PS4M2155u+E*8Rc
    zvbVRlv%TGCSx^w3h>8lG_oW~&ft#B<Qh+)VrlcVJmTKUy#FD^-%sjRz2C9rG>H&H%
    zuC<|eQGwWh2ynHijCR*oSHn}j{hOnF0D+rHtDMu4;j7M{*VXBRFaEySufo`dk%AT>
    zS)QBrNJqef6^h}aB?(7f*|XJQOu476r4`AL0?4J~_fG<#LXVt>`h~Y_<@H&7V@Es%
    z%6-1+Q>X)fG7Nkc<8!;vPe9qR(T}$^+=207R0o%+60ms<Uj6C68H^}fm_>XPtMP0z
    zp;04di22=Vg~MwrH*oc5xLmk+=MOEzd6J%i_spjN$Nk8F@AJH3&PEZ>rRl_-O~&(X
    zUecFiO4MH!6yj5<V(4YF)0YPasD8trWlihT$-^?}Z=do}aicM^(P0S5g>H*=J3`1b
    zd(oNf*Z&?{g;^gVuT|G|racIkzX8*lpcVYBVb2gMO7Bd(3(K8rwRQmO8&T<%e%oeN
    z=@t~BJQ2Kx{*Ox+$XF3ecQ4D7*uwhr4L+4MI>jRn3-R+!6{k1gJDrhemPHDO7Z1xc
    zIdRR@h(>j}IUGfx_D+2$K$(9u;mx5S@_KYLd>}qnw}mfoplNJre^_G98c9I5X*y)w
    zq*Hvq?h^1wZiE|7XGxG~ZWY}hQ~^tgt2dB=K_jDL#o0o8kd|_3ns1~*-qs0(xJUJy
    z^fnGDBFDbZJrIo)zD>U-Ds_e0zVA^0jPpjZSA~AOM-FD_;Jz|La1bw)X|a5l7DRD9
    zp>DDDimV$`nO#-`s5O&!Jl2A!7s~6`edtgNPbM9~6yNn1mSL(~<r=M=YPUJ^2L$MH
    zVoil0k##BXd{C-gNrO`COcaJE_IX*uvjS3hC(Y*c|H0RZ4J`z)z;>e_n9ggqoy1?U
    zZ_J572X0q80cd_gah2l%J9@BDpgpBLq=yyT#Cm{@cRbWzY{cR5Aq+E&fstTO*_r#O
    z<m5bJB=F`gtgwPmdw3WWKHE5cG2?A3?i#6ScVm2w0Au;AnaKE^@NKTm=Y;gKtu4<J
    z@jJjm97D!8=oipqXQ>8-Ruv6ntlG|ULZL#A3ZugnzSa?2Fo~TjLdgc+jH_QU(!&~+
    z(}j-M7grg(JnpjZY0QM6MhAwF$3(&#J=AQ)8ioLUismo%;WI>kp_Pj>wN(%@!=SyP
    zHsgR-8kn-4!;!OR*XtG8r50sAI-kL>@K5_z30Q=5T^wf>4BPYGC^H~cnF1+4HMWm`
    zR1wd^8g?jTyF)+E)=Xte22Nn>6)D#fN--ju4G;+59)wRoc)`zgF$RtA@O-WxE*n2E
    zz>LY<I$ox6jZm}c3j$9OVrYY$=MB2fpsof<b+#^1<NYYQb7*Kb#Bqyb#(^{dopHmZ
    zE}GqQw@r!Oe;)>-e7GbZ6Wte->7X@5Lj_sGPO+mq4B1QnI5L`JR%f}|8&3f+Q0|8M
    zIL?1SosYmc*Jd5KCTjn^l|nOi(=`gI)BS*IvH^bMe}2YXidxYarpvhN=YYd;z%<QU
    z5sp<nMh^-3qjoKgw<w%)%Iq~~`?jB;Osz3klDJU2T?t_61n+PljZ)2BnkNt;@`FC&
    zcT|OAi8rQM7f%Cm>y;QPBSm^Vvq_Ud^L%oVam&q*AHT`OFS7tTtPIrbjFm%Y1r=<v
    zx8kmp;$-A0Z_W(?Nl|2um@dL6)Wy$tw|Pn?Tpwfy<ywWDJ*}=OZ>I2Iu5y7|0maWB
    zp@+R19rS`pd}Q0nf-6=Vdt98Z1_dRbN(pj+6Bj@tft%nBQ`R4B^iLD<&&0vD24;?*
    zDczB=)fG5gC%l_sxq@h~WLOi)_a#IznP*woj9KM_BiBR@V9(Yfj8l3-eUr@&ywMkl
    z3x6Fd!EiV`F~sD(At1Au9E_Q8P9r*&M&6KoP{E@TMO2F}cBt)pC;m7N<oE_a6uXN2
    znV8o44r!7YX%l_8C5-8nrB)H0R*a<unaI?x+ZT|N6KHXyhlNNJ(q}g;nYA<C8_8A?
    zz4<PMPnc!6uneuzVuC5wx+!1pj6T-^y=gj=)QK&yHP@^#APT37_H#7(>iQv{!cmWG
    z$@1b{y{94>wRdj}$<6t>*(9`Nbf{;uP?A>M#7~8aOx#!oX}^g&(6I$JKK@?UaiZ56
    zNLSTQvt7r`_N%`Gf!gCrgrGcLWj|QB!%JSf2(VQ96TL{Sz%+vwKX`s)4sfn{AD=aZ
    z<49cRA<a1Q1SqOp*MiG{O%Zu9>g%pTS3=WI+^P>ZO4j>0P!JROmO7^afJ&5=SKK9;
    zs|pIVYD!h&mCa-a>l6~eFi%r5Yr<Q{tyYN$_r#9-u5N4iT61<?c^KPrP(Ar7^Khx$
    zWFh>QRrMP;GtyN~?@&(2Q=NrA(<D~(y<eO#7hW#)v;B2fq}o&k2^$xjJh(5#@;H;6
    zbPR$K8=K2d+|L(?8mP(s&bme&XtK)i4NR;8)54eiED;F8Tz_am4%U+r5rW~1&&YyX
    zhM)vAcd&#V)OZw@trih&zJ40ci%wiA3tH=qu!6jMT3bCl=#cry?JawiXA?@eBeXoN
    zU8jwk%&5FPIteK`(pLLmn|_P3d1uDkc8&CU8S}^b%(x9paMcZ6kzuKLp`Sv)nCDL<
    zL2UJ%IUjv;a-MHjdQ@XJJli^L@YE}{VYABJH-E{Vv=kR@b^d6AurK}?i+V?HuE0X=
    zXZ0eXiaF-UZ}a0Psu)t4k~Z2)MW%o?&B<^;!6wUQbQZ>6)#HlxsGI@HZss!O<u^ht
    z@@;wrh_e+J@yCR2WxzwoWN)jEqq;J??GMvf>pA0ciFx!<ozOkf`U=f6gl8P2ogOIg
    zXzE)Ti|wNaq1ab4@(?7(pJw1c6HtWqiG7IcArCBc3|W8X30*q2Pja$2gQhI`Uh4M+
    zb}KnwDB?jnt$mZ%t7-qSrk%ll_b_6FvqgiJ1H7CKE90x8=Gh?JPF(?2+wU^QT_IFZ
    zE8>9_<n-x;;W@{ooVY8GyRw2dDPdC;lIH4EA^jBqCVIMkI%SkaDbB6oLcgU?6q{7~
    z^W+60(Qaqz0c!Qt7;_Er8S)C;2j%BvCp-=~r#mViT3?hj5yI)aT9=1+5U+u<1x6%V
    zyh<0mfcdHv{gnFpwbGp_rC>@I2==o0fa|&EK6xA&#VsdNjYTK~A+|f~ZlL(?WQIi<
    zHwoOZE;%JPQvV%?=7p39Z(2;e`=OUn;*qU=15Ds&L!x^UEhpUx(k|K<7HW7wv;4QW
    zemNVnnKni}Iw1rj_1{R}{X`k3kUSxa<XBZWu7Fdry3MmSK%aYu&QRe1EOXv!NGV3&
    zkC&<0n;YA6Jue)yD3>7Y6W8KpR;3*1ovV7mx8a&&o=RA$x}Ik+9QLd4w1i&hQNCUS
    zZW|tqf9)9d(MXcJi))ay8bk?I?w<zd+Ql7`-)ip6#GM&uF{<YbBqVh{^ifh{edXrs
    zNAezeQ7&c@2~}9kAyZG+6Go|zv;s|#4CQ#+LV8De=gc)t)|Z8<2hfsgA(GhX%D#Jv
    z(fs5h@r#u>&-Ma)b$1JJQFKGFo_S7?X0HX8?^Wg$n-JcHZ2s=TsFX9ah|s(Hqo^jX
    z==#q6h7$~S$LwbNO$Cv0>{i|IeZ~2nDeE90*)b~=>dy#Ss|#lhI^DS<z{wkhsP{bD
    zaLPNu^8&jIZm&-I@`FPPlWIC+vVO?fR4^6oJ`&3jk+JdUh!7n6j3k)v$C%L;RF``@
    z4p)>?TYDV15?+cz%b#y5wCU);pW-{L<JgKc_D|;&7<THvs)mbRhZV?EJC0~J34j5~
    zQ>}Xh?rx{>(DS@qK5gLY)5PRgQM^*Dx)F8YLBFTd=aS)<HX+Q)!RB?Idy$KFynNvM
    zDVNr1W2_rk*(;rWGuEU3sPo$mc$+1V8_w8uI@D?z$($vi-`r4-*&K-3rU^}MiQKM$
    z$<Z&1P3a0FN{2w0`U_gZ;?uB*Do~+jCc~wt5U9<|FU%+IqW8726}Pn1CHQufj8=t5
    zsuwjZln_`KfB7_8t9;ShgBftO*&URbsh!I6XKYNOt*tH14;)6~;y066p=d=jJ6pWg
    zp-u#4H#F)+bm;Cg!Qab_q1<&;3adp^T^U8>Ff!5{@XW55!EIeCj?F9!sL!bHtJ%e!
    z<oQZKq<o>bjJ?U`z4NDC!UBcrG<1x9zkat@^T(W9X7$+~7tEF(JG;~!KIF0CAHD)h
    z-cNN`wH-WZi8Vd9N@J=RfF)6gKU@6=>#neCF*rz8MuYJEQ9G~5>S{k`GR^-?E{ti;
    zwY5y(3KF*k6utE33dBA|t)6i@1S{u%*iR|rdPJ%S%iW$^R~=Yucht{gA>%6NF3J|x
    zcW#>|u&Ir%9Q*frO*y-lR%cAGS<%C3!wpv_&gaPI-Gmc|5-+j%0QS__-D*|#EZ{N5
    z7gfk8F-?{|Ins0He7K+UT^2ah)PfxGCG|fSCcC{|KQb&#ebv%ma3wRb{L8MnQfxPg
    zV-r=DMQ<P5Yx68!$&Tl339WLU5&WLv?&WPI6fG*%$|Jw8{5a(yc!&n2kJGwNohb6o
    zq1)CfLoKUQKKFg(F;p8H^SJR&(9<Wjy(Yl5Khh~|N_g3}9k3i`)a7Uzsg+84N(=p3
    zWf?<td5~M?`r*f+c)BOwh$20Of0E|vCc643#s*SH6xqp|!Mfx9a#I68cSA0bc60gM
    zw{q&>BH8t^e!<V&<|$T!4H4?6SC^5Llx!uw_9C^33J)Jkb}lF=Xs{kX3PUEIuevKj
    zz6p;x(iJwDDU<})&eDHI-@GGR_<ZL@Y53-}Z7A8-KZ0Km+uCMk!3(ZugHKAqlyH_9
    zVIxyaDR7z=SDiT)Hy$p%?^B|P8g9S;b~fqz@NUQ_mgtls5gAE#a(g9lFE0B>OtgfA
    zG6#Guj(Dt{eWwa4t4~i46mzYH!qX5;3AdM_y%&R=u8o9`Gz*$<u%W@ju#cH<bBHCn
    zf0UDiK7flMeg7Mn+H$dC<0}OZPa|xtUahE*P(>cPX6)zmlSmNtG|BMxgdhrUpw9QC
    zg<Qm}!$+1Y#6~6_y#X8qP0*@>bUUF8R*OLN?{Y=bl}r31{s3XWZ{LQSnj0yR6@~Qn
    zi5*f9L##X7!hK4zOh;ru0u>R}<W)<9R|eW}g%!byQ_*l>LQRpXasL)LKPt@<QWUtD
    zMqi)Oh@QQWYmB1H!;GPUOF1uEMP|vDR{Y8s_?rKpF%RQd%FTOX1#aZ%mhqHVBKpqJ
    zHl;Pftkr1$4x=Qq`=wiZFiMA}%PlM~r$R1O&8rQYw^8nY1mWj86BZ^g3s~A6fuyIH
    ztmVAj<EK%+!Q*y6D&Glb-o&M?W@f)4M=noVZ`(&0f{MG{{zTaf_C+6@{%N@UxgZ3t
    z2NV5*DR#GT@9aZuiDDv{x*J!&i5mgJn69kECNKrs5Dcy0l1dd+Dt^{xORj^*N&z5)
    z$eZ{RH~-{@GP~c06X;pMstZ{S%Ahr~mF&U*=fp3@GL=9UHZp)}SvKi3K`VgSzZh)1
    zSf#>|6C4b&y8feYf|_LoR8jwAtxDoMzQqzAAi+KZ-ws3(FD4!ulTE+^M6pb9Udv`q
    z;I82QvIGm(yioWwc}Eq)q$o29#vpomLBr*$sNXRocVSXTkjW(Rc(<rS9n=H@1Tq`k
    zra$+)3O`p~Y4Elr{$rcwb0|~4byilEh9_PqqheW#DOO(Oj-$-j+4fUMcWg2|iB6Y_
    zZIiX~C?K9WS7y;wTDm^&g{V)3mjpV-Vls2|+_eAQG$3_r^{V4F?Jn(3b1HeaNj8#s
    zBd^inM8e%h$w&X<87pH;=8>G+;^aWIV4C6YyHJuAeEV9Dz<?Xk=^6Q2gH@M>H!fS|
    zOD?zkn$^;Z#jkIGs6%h571_iP|A{7fpciJ#&J1BKO8;Uw`FGHR36=S`8m)HrSi&D;
    zYOXonv7AkxJg;c(9AzbUB#R%oOhxN%FlF)P@3DDJudE<2pa6vqg931c<UW-Wh$)4%
    z8m&^5+Jq#S>7Ot%X0Tpap}rYtHbi+)4DdF3F8(T`0pmg4+*fFb?q{xmWMDhnYsiX4
    zfQ~j&0CCEEYRU_k@3#_N<_qOjJr<`vb?XN(i}@y^SG2JzoZJ%|UpqM8i#f;`JT{=;
    zQq3T!c@X7+{n{s-<B<y^h>9Y!d>d$<9f$(%!p}bt6Io0)(rp&3oc;V$4qEnvlSMQt
    zT2!(`fFLK}zRm<l+0qdnG`)*vNFDtgT^N$aX{t8(2=~!h>YO>Rk05lX?~f(B#@q#8
    zQO1K}xomDuhc^V<KOMt-25xQ-Yu@k(`0OH1o97nVtMa{jPpdO-c7}+o3k)|x-Z04G
    zoQ*!=uc073G1I}h4C%I#9ua;&8TxvUgjsa>)3uSRO{Er4%mXc~lM|){qF7tXkAC8<
    z1ExwZ*+Ne!x_IM?;oZK`j<3E?ITLZOHJ87YHcpDoB;nBA{Y3GZVr2nUg;3TrG%~um
    zJ&<<y^lV6ZetUMBs;^Y9ZOAp7$uT*K*6VUlcqb$vu%2BvVXG76TZ{9>q!`;d9I1hG
    zbgB=N&g6HX(zsbVyIxL}8%jQn@NyW59WhXrhb36EX5;RiD<cmzdcNALJ2!kesvP0b
    zki4BGdfMY_B|w|xtb*R9l4l%}3RL`&V`{<c+x5_D>Py}zNsg(QvoGFOJ9X=2(OJ2v
    zGk9fmwAU?Vy*0wRFu5p&iz+VVj?3jp{-gE&<>9QZwbe|MM)Nu{TBOl*N+a)LrIt8{
    z$0KmLTDwu}8+hLU^w7QM2<5R^>JUUtkX8GKbzJZMtc`QUVeqK<XQ*54?=p{Tm^Wd&
    z0^iNrRenkqAdLzo8&O0yrLjQAmYi2)1e~}@QL_MN=)1+i_9aL00b^76@2K3r8X?ZR
    z;$5V$(u3#?O?Wo&Dx;pcOA6!FBuPF)^}S*Sc9w3m_<ulL$?f27ZBxxv05;Ge6paR~
    z&_xADlIz5Yj~13X!kh)4RAg_YX}9x@X7U=^+J#O>)0~d`Fdh-@<+2D~6w2k3^`-K;
    zyHlAQ86KA3<#Sk%FB+yN<A!6$-!=2DZ@i**9@-hH+r1`-g*!QuEqlTK96@fU$O|to
    z$={tJQ{9%Upj4~UhpG5!A@~tFWf`i4<7g(9_8RnWz>A$t6)a+U-D@O7UJghKuujcv
    zVOT<rP`)Tbtp$~Pb0!lC<@6h5voh-;HJ=Y|kC)mzNRqY4QI%WRh!3yiC!@)flp^An
    z`94RpX-Th09rH#LgA^wv)aCkHI2+xcJy(9a`k5oI`JL`TBaXt2ID|B$3kr*$RW@pN
    za;3JhX_Ibla>n2Jd=-_$cBAiNy_M}1Cn+-hoF+vof#1(}b#;{_L7#}z^WOY?y;UB(
    zK(joZviEVc;oV=ia_U%Hp94XE+*O%z934ix15zHxO8*9A50Aa?UN)@$az?AJp*bY_
    ze&}=ELHwvAxb=v!D<8t~(OuNK3#EV;s|oSpOuj$cjDx9}(8y5AKobb_mRn*|gs&cq
    zR_}H@W6o|#Y<K9?`(tJ!tP^{=uqJsU=%35aZ1=)NWmdLZJQ^sEol^EFR8t{PIrlEa
    z2cD0<K@FCJ>%WK}GrLosi0xvKIu91n)g0)o(B|D5xv+NA4K8Dg^~FVK?AgdbtL=}P
    z9r9e7Yq>w+MzuhZ!CRb`^Tfk7b3+Sv_t;^wMtaz3AK^L4Q10GaLf0w0@LMI=D<t!=
    zEq%Z<3hHs#CNr$HjY-|@al~F6n(r!m&9Y2LUd6{pJ858f4y|4n&co@_VD=A!>$Z*#
    z!{JEGbGH%1?c?Kc#X>RQ-Qm=E54+9UvA>0yMM*>g5QcolOCzp>(HLSPWVx5A5wT_0
    z0w73AP1JuRXXJUMcFf-iky`U8Xa?4@(BWyg^9;C5Y^yv@*@CAT6yLk{@SD8M7s%y?
    zw7;eM>6gL`Wi~w-Y*s&^fyM&G$G4|=`a$iS=7|HBL}FYcu1<P?tN3>-a#v6=B)+e3
    zmA9CX=k!s$-r*OcnXz=%^-18vDUT4ZsLn*-)nPv*ll2AU8&fm?ZN@B`YQ+<$+Z7ht
    zQZMAeUm8zV?byU8wSqm2g#!aH7m%QOvl*Sok8Uq|F_<lBg}}|g*v?Qr$Wx%|5Do#~
    zQJm(Vy}8j!61i^4kmhW}O9H&u%9%EhT)SH0p$30}DjWw4pf`iVP3}jV_>mxer~Lir
    z*RAR1$;_PJI`HqS9a*+Di04Xnus9r0z6tW7l69o8ho#Q)#J=p(PPlPCNak;Iub*Fe
    zT{a$m@Jp5h^4#P~QjKnBgQ@r4NQ}ef23InzCRz|QvT0nkc5Cc=7LNzB72Nq&?^d6)
    zg5Qj_vJ~mwJdM&2CvP)kTlm*p1RK3yiXzU)G~++j*4yy|M{YTvkQXI!5PkuRU}T${
    z7scE7+Xsb|v;UUR;xEa<-}2h_Ky5Z_E}ZZY@CmDV8*U)7w^=I%NDho<pAG_IjM1Lr
    z1WR601yFHhk~FG-jUgMecWd+tGjwG}KE9THnvOTP@(I>?X(nHAq3h9sv$2K4v$}-w
    zbJfcEeY}6tr!DIwtY?{O^rRgJ3u{ztghMyB6|mrlixjODq!#sk&kz3xP}Z*8v@e6}
    zsDB6YTDExG&k|HSOg5eSt(3ckz|()~0<VCUE!SD4c6PoSQ1fcDJIF^k(!Gs4rp*Y`
    zi46k2(jpYgdhZ>{tk;^{#q3J;&RWv5UfHcdj1ncnyB2cN6Zdu9=kRFO*nk9BHK3f{
    zd=z|Mo95SfUAC)?zFJK7$enYg4J=1Jnav09?C~86N2T>={!peYp=|UFyvbx<qb8da
    zNUl@=CB>qTo~2=8NY@p;yjJq|dozXF3ctdL%lRIvW(e?`(L7RvKKrGef*bxN!%qgS
    zOXGpbN=RD|T@BX%2OT)Bsi3MdDP@jJce6<0ZV>n#(@Ah7-kpzZZiR+0UuShs6o1CP
    zw0_8maWS8%{30Fnf^U)OS%z4{JQ!W6ogjSfn#Q^rLB|Rqe5mL-xSx^|?NJz8+2Ap`
    zgbq!<8LuJYc{gZo4%KQa3B&@st&l;l9;eF{uO*F~A&W=q=xH%3FsA@vc>&%DkI)-E
    z4EgWw-)8C6mJ5R4E4m8whifgzvt4bb^08fzzh|O2^o@>@VM;7n^e=>0h^IaYC?p`1
    z(dR4G8KQ~#i6G%H7(KmLYIG^5!m5(9N3Ve9OBqm<_l@hHi8D}TIzxJmmt1Q3i-Op;
    z7Osi0u`F({-&CAu6DwD-sRuZe=~1Q0Y}Q$~%_e!2pb6)<u)hz1eU!*t#^rVfhx;Dv
    zWDp%;RA&M$peFJ)Lx#3%eCVpUAEDt`_54RJas(5E@}u;GWC%UE%Ya(2uLDI$7G{eV
    z^Efx}GrQ*}0Ba@ozCW%P3flq<GEe+5o(wJi^sO+eaGFKBNa)H;WHlclSxX~*n_jFf
    zGC2`qAs+%Wnhu_XQmn~hZ@(UwQ~8ZM)mjxF0zr*T-%F81A`C3W*sWLlkl2~}rPDwG
    zrbYz~EDHr`{nWq%vj_7Y;dHe$12zb!68gT7{#ONUfu;)g7U+f4c&PaX^o?wshZNy&
    z&@}yfvfhE{rJzSW9-0ek)#F%q)}bTCETb+pNQj0OCU`i`LC(hk5VB{6P*afjL{0DP
    zP~$84hOYqtv%T=r${+o)QY<W#E+#I02doKcy_|l9{Pp#tp!b`OKOyD4+B15RelaMA
    z$wn^%e{&O#ZGD)p`3LG&(gA?-_vgz#*A?n!P2?vC8*N;O=(#iSh&WZQdFb}$&*rx#
    zXhcwxB7BZ-)5_47-tf9{jY=qy3@1hiovl&_1N^jUc$rp~)W?!yO{%CeQxab*Kn!<T
    zI`qJ#uxcj4NY+aF6~|PV;t>|m_JOG^sgm$fHz!K#vIVBo4@DRssULh^#QW%T!b5Jn
    z+h)AF6`49}!lSl8+2jX-9wVN(1)O#$2hb-4bNw?G%ja{L!2}N&)r)+J)jX0Zu717P
    zK<Qhpukqb9Q7h3feguy4a2p<ifHS}VG0vGeIrY+9rI#e`W(^{7o_Xf^1s)NaCzK&%
    z>#fxMuecejYrSw^7S_mnc1bEF-0hT1nC#XfKvq38htI0sp%Z&Lp@*40K^dE-1wX*l
    zo6z=w%td1Z4|vLbPmh5Agm)~ewOWE<AP~o0v`N*bUOVc%V2z}`Oq0Ur2WcXbQbL>1
    zNwRm#iLfPHt@!Jay8?+v)YohKFa{;LHaj<t0ngvEd31aA@f$#HYcNf#aGZr8CiI}n
    zZ;aihqS`bx+#q8b+Q2fGZ_Knf{YuLPV{?<nADBZpS2N-}<-6`#=-_(cyhvqP9Y%$*
    z+*%%~=*`pG{>dli=0es`9r0&o1}JpZas0b3@UY_p|8h?~X8f|~u44ZVf}BIP*S&>)
    zc$`iiXg+Gn{hEHFvjZV>0AX-IwKf|(X;+Dn-%e%3CMVrqtfoxS8+EYXmHjY!9&j}p
    z`q#u&TBZsmcvv?weSYsj2ifzWOrqkDax%FP;u*grN>|RM{$^VP<JIFEIr|%5r`GEG
    zIqk^Pqfnd}CM#hdNDmU#FEn%h9uoE`+SpV!F1LgjFI5Ce)w=RSp+!J4Uz9(^Y~xC;
    zoAly&4_Nb2Q~%iVk^~j+^%Fn7Lx&f`eZ@K&rlD<rwm$w=Z@)HcjY4$od=BT6BNp6e
    znp#guOa*ifv+^^L?4sA}NiMo67b^ZVZW;b4rGh^l_G7Z>Lo5c*Cvk@2y+G*iI!uoI
    zC|<kMk5eKw9bx#+3VXdLh;>iQfzfbti3=T|nfEcC!yIvbL2+G6V^GR!w{PN*54nqk
    zo+MYf@bk|qVO7#y6^tS!E-!tP4d|gV>+N0R;hyhski&9|W{=`YSC2JvbuWUss0+5~
    z9{ZEt4kIR|oyO_~jTYd;Bt~C%U`+hJ@>=Cudl*Qp<pIf{ifn1j$rPFlO8b-9>jcXU
    zEtc0mLZKRPvi|;tE*YAhlk}d=rE-<SEf;11)dx@;0r`uT>(KpMi?dmDAg&@Zii3V%
    zS$^j$z5fcpJI*uQlT7kv*^ve&E#k(A#R`3hg$#$K4$~6@3#H}v3L)PVplacI-+q2z
    zC}18W^)0G+ePC27K`hKzD~xP_=<DteU%w8B?gqVp&G3?~Ayj`m3F^=A)RifoB>Kd=
    z?XEiFXt_OcYEnd%VbPSHdP(qB?7<{qGsCMmqQmog6e3yjQI7|OQtu{V6a(S#xytTQ
    zt0LABv(H+IXA5xZNis99diS->#O!B<pW;*O`HW@tAt@;l3yt8+u#_-@5u{?dn=z-$
    zEr*>_CC*}dQVb$BCv)mGXJ2jd{9Q49u>^ZdAAUaZ$A;E1tz1qKV!;kWNA&p|ZmI=Q
    zLmTQLOE<#|e-_**;L(h;Q=K6>7Zqg*@xsi|{!F<x2<w;&?(F=Xv!TH!Qn8Tv1k_s+
    z#pDvRIG--!-Sz7v4BCc(JX@}3_;d@o>a%k;n>w4G++qrfUY+wKl?lQ9&h@Lj+_U!q
    zO1_Ctz&BKq+QvCvRG((KU9Rr)A;m_c{SFNc@ly20Ksw51Ez5KQ;s#}y2Ly)06fgNj
    znU^B+cN%tb#AI#?+VfwK5m(C~L^p~zOE0`wy6?Nk^W$ZT5+AB(r_0N7f6Ge=+>BcC
    zy1GB+aI7y6!t;q+xBmoOiVeWveWkESYYWX-M8-BRU~iLCUfXe|-jR8%T9G54(2io2
    z5Pa_XK?^gsoAAZp%F_@VmDx~f*uV`G$X#+%`f8EAVA!4G4=VWxm`uc1&I7D_Zcqt-
    z**6NE*z>be|7E$I?1{E1q`qAdG1BhU(a@&_`RLUy2(uzoxU(}nn{^D;qHs1tJ%-p@
    zu97aiIb0(_AZ~}r{-F|t*{MJ9lLk{iqn4^tkhM|P*CEFOs$a#gpa(@JJtl%ZMHQ`D
    zSlbakyMk-)W)2f?Ezy4#|9aZ$=HZk&Jp#Ofd2Umpu-QaSz-0fTh-)n|`W`jC-?@Ar
    z#8|2lXin6Po$?n_RYi$aTH6pcp$t6ZBpt6fR$WNhg2v62K13lj%Xwdk5p)6VyvGfB
    zdJn8!!*!Rt05*;J7pwU&{P}NCl^Z&V`B=lLt9el!)<wE56WcV@Lax0^9X&LF)i9cb
    z6D9A}6{bjKaU)le>7g^=>-?CC7L;wdhW6QtGgkoS*oozjEv(&e#L@7H<eTj$h7OQl
    z1qEP6C}bEwnZ$i}9F^^MKFJog`cU0BmmX;SsF;(QnhIxJ`nxa@CX5@}GNdj}GiA=z
    zgxQSTD|-_J^;w+UVua#27F0+#S1qiC;v+#Q85i~~lDQI5a)UY#Q&9q$xHH84<N7Z1
    z`)}5ynZG+8w9wY|4D`Di)FG59Z~}&V5!DWoX{$V~sWY1z1Po+-@(WUc6e&JhpxlK+
    zBw8C)(<K<NAao{bdR{QcPV*~NPMLU5?O#k+Z(=Up^<XZ&cH2b3mwBTitgS@6z)slj
    z`DJBjAzgaUUdy!0*QwG1z=5-tC~GmBI)>AM+}K2cVc^`9hRS9$4vJ_q&KKHZ4E5y^
    zP#3zzCW=lMb7^OT4Bwbu=#@cv8?0}O<Tm4K?jF%|3IS!m9JG50X69Q+aA@0i7bFC<
    z3u0l%oHp-VII996^}{-vC@|Nn`n+5W9ae6<jO5>X-XfuNjKEe3rBWoxYteY)>HXIV
    zZ%IDth=mf#tCl^EZt3{op|7`9Nwq2BW~?a%*wBeQ0FUYNmM`cZ8-zo+dy>e%w^-bA
    z1|tx`Ng5jhF@Iy7dy&r;1%!=a<s~)~N|S1O9V_X0-y<lgfBUIU9{b7bA9L~xLbkPL
    z6}?A&t6>S{&K>_$gLNohoTnBv+ux_LBjJbzf97w9KkN9)YUn}ziB$4eZkIQHK`~u<
    z5|oW3v$(e7Bf`f@SUw$9tO<|J_`NM={qY<0Wukkiqwvp`A`W5g+fXYB$`ig4LMEaM
    z$uekMRYg@Eo39?8+%!=ruMZa5@?+rlA~=aR55VS5uiEbf>2)Zz<lExupa`#IjS?$E
    zljKsZB_`Wv^}0dBdaErSl#QhtA*9l0=jGEe=^~lpUK;#;v9ypDTEe5DfUzU=BkYD>
    z9y}dm;&kK_5Z2xmKaJ0)YyUNy%A?)@Lg4y9lFHIVK^C`%NCAvFr0ERXCc5pL%6aOP
    za%ZH-mO9bu9)GfFd(8lGyLCF}MTpM$foKB2`w6xcUuon3ytBa|(1*8`@k|uq;lvIP
    zL#6he=NHyR569)j%Rk6lNo%%*<yOmMDqm26piX-@!r0W$2#dTZA?Z33%HsCHvB5ZF
    z^tNE!M}ol7P>>%Axbc`X-8a3@*^7!Ydjg6~S0Ag8F)ZuFhGuJnI?92#X782ND(_ty
    zT{IE4IhixpbnkQL9fyt99p8iRPhF4)mIJoNf_8)G+Ofxh1JBhZbc%H`KNTmx5G>)z
    z;(NxA@R`LIwN?}(t81UOgn&DN_=WZvzi$~!Rbe-kEqU#bbqBLPgaX#w4h)-6v_o}3
    zWyU5%COTomDSLr*j>l?(wnEF=8qDGDK)#RKdWT1a=xh$csd0RdgH5aKvs><=o%@c>
    z<8DgACW@x45MgQF;+FAX-Xa2cM(1N=1fvO5JTo*IuIxLyi59#V@WPFN*CZyNJ#_yF
    zQ&B6j8|bfh@w+^oz(R(eH6eS-?5Z~aMYMvciGmd=2i|n12emGNBQtLMgC_LbJ*+o2
    z=(|`;Fbya>dtYdui$Y%^6nxgvRaemN1#gzTh5sX=Ho7bdr{_DHQevR}KGBIzX3A0r
    z{$B+j1RQBvz$2Ho3Qh<<sxQu#9RQ2rBZ*-1YvzG-v?M@cNQbM~K-gG&`*FHt`*pGf
    zb1?R#Cm)qsWld&?%iGh(sEFeaa`9^tZC<Npyt<~lPoBr&Jpw@^<h`-7>U+v{qP0?6
    z9K7R99T1#2`PFZI<TUPvaP0U?u{vJlEA_Gmbw{ut!v@y4yyKC|;^qOei6U8<%lR70
    z0x(`>nl`kM>fY%vnTpl2r1`lnp3@R691WGmn;%1O1Dg^%KHhYa-Ly&n%hPIhC6l;Z
    zeg~@7*<5<;imkh{;KTpVam%UP+Lyq~@6gc?Oya`Y1&aiY5@d+QQjw_(tp$)=3Hfwf
    z^42R^Xf~T6O%yVOQWuunoaKYa2sRPF<Y{fj`MvDor@zCzVg&=&LSv2r-H^Snb_v%*
    zxy5wP#W|g^pDDYASepBEK_d{IaeAGxoo@8?CIH<s)(TfBq4%rp!zB<bgR^9(gW4rN
    zPpsr;A?KHB2I|A@4K$?owr$S(2&%k9J%2#qm|{NX*T&{2NvSp@xwzw?tYpNEwP*L5
    z*X>CXzuINlBTM`>GlWAjxTVXve^H~?4Xs$nI`l|9k~dpwTv0u6!A0p3o3jn&m~Ra~
    zmR|rJ^ZSJv3zM97!9e4Bk9APx`LwvUnYSZc@i)2I4$p%PKcvC}-s*fh2Y`+U2%u>j
    zGJhefO7;vqgQ+`(8K;&}f_yssE)iFr&*9SX%YmNWlBfO1<<FD2Qy5b!kv_7iEk$`5
    zEh5Fmbc)FH$MdO-E@Lv8XtSvh*G&``>xF&&dTED#P@4>2%l1SFoy<OfxNCHORKKyr
    zX#V6!8Pg6z766wg?f6}L)Zh(J0Pw06pfY=KTB7Uh$Vqn-;FU-%C<o2BXxkkVzX{|b
    zS)xU!!0X0Kj%cZRM*1UhFEe?v*a=rAp^H&DA4_l+>16hPongM#OK208tJ-?KzAMe?
    z#f?;%f-s{RL0n)HX^<fsTuhhn8Oo)Mz5ABeT}Flzq{O4Xkm`JBA<fBCw~|li5s7Yw
    zyO!u}%_B{?mZ+qHXPNWp<tb)r;>e;PeRi3DFBm<)q=#!j*!GSE^=qZT@D&tbx99);
    z&|I38CPXGh*z7R=+0n{en$jqUX${Q~F|Ck}s<ed8LZTkDL3(vP(Do00t(VJ=xiq_V
    zK-4&q6x2+I2x4|KirJN78dEQ;b`xnz*I2!u!zF!)EsfP+!U~QuRdrHGHu|p_p;Q=E
    z7pd*BI#&o`{sTWMn42Lm2gC0JJ);MAr6f)lLx_nt(QRgS!3BO4;vW+dJX|9h7$Mst
    zdJOGeY;SU&6$#ZD^{JC7kzDcm{cW6U_0Dw5n?-{F;~U{7#jjlrdm?e+44tWup39##
    z;OD_83^MXyw*OZ&MZ+w$#dz<{md6IxjOdf4jMf`TcW|8}uFiHIhk%0eIB-W+lkvg&
    z>4#O%hf>G47DxzE&cC?yxqrjF{iqn@T*MEP=JQ^+V|l_p1JF4ggt`fbfQi0nM(n3R
    z9cB{(m<K}-64tCh@%4l2%q@hgWmmDi2=RE+MGdL65~OF0<oc6xJK*mN&9y{s9?BuA
    zHvGCK3aqdFUc=NUU-@9E4q?vW>1;+R9q{sNZl_ZFqn*^9+bx|z=dT^21(BRq<({Vn
    zsqUI<<RHJ?I(2TVQ=FD}3i$H?lKf%p!&_U|1mT;W2t95VX1G16+h)IsE$llH)V~fG
    zBW!CgUZz+ZDCLW}0Ph4YMF<45T+JO5X%r+IY^0TRtrs!k$ybl$T+Mc{OrXhXCXS-V
    zJUhBw&8wj3t1iljK8p<%Pe^oi>w^sPaOdgDezO_J$<pOOblI?RZf3b{Z*ftL4)$*;
    zqOE0LTN=8=K4R=~)4ey>&`VnOM+~4nfch>6hO%oeU4~zBXSSylSV(tb5Zf}OlvD(2
    z14|du9h}Zt^XEIzLb}ayiMSEFhZw&|P<y~XtPn&vdZG3UdG%kvIsdF<ZlcIUjuoBD
    z#YU@+&E@`@OJx{EwS_<`-`fILSt}v5mTAYpt=SMrhu3wizFC$q0mz&x{n^QDk(Gbs
    zSrhqEB<fPJTEG(g;m1+^wR(KdO*<@2f58{2qoi`3xuiTU>5;u}&DTBp5e6BTxf55e
    z8&M;je;Ay-x?%^<ebKxfwHvsF1o_RwKpMe9jY#<Kt?cU&C1uN4DV1GuD50QiduUJg
    znFEM4wpdj8QoXSw^BM>J&0v#!B*(hQ8d}n2V|2B@=ff$c<#JW|L%B&r8OjZN;L{v#
    zKAmJoC>A?{jU-CT1ATN{!dzXGOGx$CMyOV_{e}=$JuI~d$)-`4aKsluJaGc5rlth@
    znQ~<KTgXk7bhhmLmiMm$S9#qocT`Atti?J+_!H^X0R9hUGX(|X@9#Z`aOzG?6mM<H
    z*RSR+$c~PZK`ld)MRd<P<I^tYSIOuWAxs8;`$bUu?<=1|HH-5z5LT^DqC<P*L;^-c
    zhr&oAztY`J?NExNt9nbKC*gO=q)Y*h5POD<m|KZ|>{JoRu<6)gFW=Hpj5MN1r4GkS
    z;|@pphc6MKQ4`bNX?6~(NzRpPd&Q{H&{c9%a^lTGu^@1mV2(<mxNze5)3FrJN{#M`
    zD1*Fa4N0sNeI=xCGQk(pO2e4!h8?@qO`fRb20kuk81KGe|0O-Vu}Q`~iz`M>;>zoG
    z#ddlMD`CPE5BPMY8x1(KV_VCo4^Ib?(8BlmGgipqh*-TOW}&4%$cBwLj?fnAPdyPw
    zsww;Z{v@MZMU43f=9jhpFqXXwL>65;Ea-AOoPp|SqIr6pHUcs6BNokj!9vr3{yD!t
    zI5K*Sd6*F5yaG$H#XE}GOGYLAa|V=>ta*0HJX5oLS`Faq9}p*o^JXvmt1^?^2f?8f
    zdcg4x5Lf+$9=<fjHmn~%qch2A%|jx8t~NVEVV(C3emGZe;NN|6;9xp3xo3x?^BL7&
    zUqgET?ekYdQnC0Ay*J__26W93#gMDXiAw1yz0M$*QLLlZdc=wTW;20aro6jZzm&#f
    zf-Yok8}oepIVE<K;USAfeQLsJYJ)hC)4BoHA*W};ly3w&Wj**LpKEwJg3U>k4adP1
    z;|2$7Ss9d6iRN(X$ttB{vP=wJP)jU(dyU6y7EHzsb-d6ErG>q`BqdKv9BD(obw53x
    z=AjxZ+#`O^D$}A55fVN7<aiV`ukT@O9P@)&YA{bW^X|9llYQ@(8xVsm2G%p%{4Ozx
    zM&0^z7mfmbeb5@0V+5*x^<JN71b_cjLe2^{09LZE+N`7g`b<Z3J5zZ3(TVTL7B!rY
    zzn2gPV(8dF(-ET+K`rVO5Ze{mPKH&WT>w8lZ4=xV^Uml{jb2i<5v#=R0Mq4&)5xJO
    zDi2m7&+a<Vhk1k3Vo#It(`D#ADe7kQS}oqXH#n98@nLspCGL75@Ta*-BP&>F2PxY<
    zW@r_hh<q^tbjN{+;TV~U?k(E#^R2;pt;e@_oLJ0teKN6eP+1{k2=83+LTPFRa&>y=
    zDPn|Ndu(UBQz&W7?aK;cYF#AcE2J_{QzBa|^XSxOS=od+GBFm$r+kk1p_))=zL37?
    zoio9Tc65<z4AEo;vqL|K3r+~%O7TM3s8I7C=2&9t5re~opw?}4-)t;pwgu0l6AO;Q
    zV*$p5Veb1O>XokX3Z*x!Sr&^(=Fg)ltPCb$;VZ~D_uXjEaA^KMZ*Z^*J5vjp<keYY
    z2fLwxA{5r^M}BQBwG0FpZgq~LjE>C&F)tJsDEOI?N^IJNq@qT40`YS?`VW4B21Bsm
    zUAiPO%0h>!*t}6CyVFzl>|3ofUvFtPG&ZGsp4v%PnOY%4SC+ZC8jchN?Q&grw5%K}
    zz)W)5O(2LGa<<IV7sG)SB7eiATVa|H5|Zb%udvH(Z7ttmA#G#iI)z;n2ST_}@9#Qo
    z{OQ>jFi53RElaXKDA3RAgUzz%CH|0@F-YwMDb@82inpxtSTQa}5m0$~VF2RsnG_}c
    z&p}6Aj-fvR@EA4cka6>n&l-scE72IuFEv6JmFSF3h5T-GOb33iPTZcVih(ym6_-^)
    zxkQ$2PX0atLS_4gHZr**7o`}WH=GFC&XRkP_6>=Jo~Va<`|f#gSj%YriRBoK!m^K^
    zpx+jIlP6Rj!{9a===NNSg_yw&r6?(^TvbJvBRGpCF%+3MEPDhB6EavJT;wH&me2*`
    zR~LkuOhi*q27RZueBmA(aZ50rj(FY*90!h$B^M9lpbvOr?(o~L9xr8goGPd=9IkHo
    z&LO?Ap9UJ{;1yo28HSRi%2s_K51XfwVt2<BdF0{BhP<z}Rfs~Z*p(7EkP#)z5VQa-
    z{({K{*`Thr!DGybh3VC3(ifUQAaEC-HWIKEzdx1vbmmWmP7oN`6OFE3nJ)zjdr7Dt
    zViK@?+)>B$0|b*yz$;8?vrN)vxXjdu$x7AF9jirD=Gezt!y7F7QxC#zxfd^FedQ|O
    zTvWWP<As6p9TQB_x9%SP2b9}jqn6QJqa@e<m^Mg=P9luJL0EV8_fVIw{X$i$Mv+!P
    zZ1cu9)vnO3N4mVsNs+j(<AGk3C89(D^5CpTI|7ULkvP2vvmU=^?C?rfov(`G+kFzc
    z5>Z#*;vf=^(l{D&4Fu4aG-Qo(HnD+RH5mhJ>$P2RT@JKZ7zv|^rG^Pgzr+9M@I+pQ
    zRPnFB)Zy3`wjX7_nt(|54t7r7g$#r^+!1tHWYKy+fj4p@J?U^2%N_3E6Y~eheuJt0
    zdVMCNRJCUVTqfa)ST#@G@hToI!5{P_XlV0kw{ljP!2^Gzb|jq3W^C!w`w>b?n<L9I
    zT}s%`;xUcDh>e_(I`i?X)m{-@dxp1mUyu!n$BhrpSj|UeMvG0m)qKfzFs%Dz+^JS+
    zD&(w7s{q9_{k)69pBZr07b#&&^ze&i(J65Z2G{jJR$a3EBT@+3@9$-_wjr*~H9)%=
    z^hT4}ey%dd;izB-MV^hS)(_4ULT&erA*L~X!_C%iW(FqWGtDhk=0)4zI=%u5DWfbb
    zwOGJ)^;gePDSTqylyi{Zslby8jH_w-RTj%S@z11RiO$J*8?V%*C3UhWC|Vnnr59rH
    zio;(?l*^3O9Diuza_N7UQaK-eQiEF^bL9SwD;A?fF|oUwXiP0(;wtCLT&_0+&_cDs
    zt(HiTI&f9`(auV!Sg3@8Ul=?g1m~q)E?A3-kB%%F37M6BV7jvC$eRJjbVEIh2EUrP
    zkrs(ptTMDS*Qjp3tt&r^6aCD2;yiP)xI(~^B(~yBB>FrLl5yx%AS+wVa`SfFRJv>8
    zSz6&F&vKP=&0};!(NH$sE45g*zYi!2OgZn1GJ+x<8N0{=QVad;R;FK=v-pYvI5n`p
    z&mKJ5ONk%X4hYBHEkrbJ3oEXrCl6r%?(<f2d-g2hIubq*8w*NhWz7~ZCumu|bxxbc
    z1!n+q6WkR&icUt4kbs@E!WUV%=J6CuYmJ}EnM^_$$gBUJFK10_nr`@+2C=|{Ia2sk
    zMoKAO*Q@H$-er;cS1b5NhUI>sxNv2vuH1}XmSrux$4Y?pp)r$@i5Q?8;;#-S^SlK4
    z0wRtf>X+kcNo_7~`j@P+^K5`T?XYqVchD-l_Ug#OTcsrDryg6<ixOovKwqhJ38eg{
    zUv$m6bi%h(rUyN@Ap6vL@}!+lS0cR|J1P9zUy)~1eT#)4>)*=YFDe%?%b|n|wEMGB
    zCFb5XVX-L%c8*RpB?w0mZY(AU!4@&_bW;J%3~!evhcLYoWk0JG=&S2Ib7`nIcpBjR
    zUiCQAJYR=JAJ9JOwf+BK3-PzxMlY%EuHRj2)`90oHqE~fAkM!fH%AxA(&+cO4GX<V
    zp9*#p$#EFq*x=lY@v|1DqeSDbm$5c0yew3lv!IW8o_z%*uA~g>OUxv*QAbC3DF?51
    zMrFFhNk3k#7(?YcZy)+usla~u5{H>!lP$QsR6oeF6*efr!&>w3vbhW|se5fif+ue2
    z4URe7O$EnB50~hnt+s9k{*@O9$;|wb;Po)@&S`|m0hQ^c*;oFj#koWlg!YCvHI5+c
    z;Ag?2f+YwyRKg&_m37&bMY7I>ahL&2U-{cR^C?~)vpwf)C?opRn>Bgl>i2y+H`*-?
    z=s0BB{sx=I?bZu6B1&s9s<X-Vu*sY*3~H_(?uGW`Q1Fvey8V*{0!PpL(RtP0zD=pn
    z#Wd|>mJMjRYDz=NPBaNhU123hx&1_Do$JZucmh*>?+7}B>=#&1C~$>w8HjL<g*L(Y
    zmV&9@hn`2${WbfI@n_j`#RpiZB2<M4H&Q6us4setO%As#AXhXm@jYurK8iiOKvOjh
    zcQ&D|oIk*&J9QOe!=M@(qkLfmgXoc=bLrwAvQg}YZYK<2d60Z8DS@lM9Jy#cd|y<G
    zBmI9;R26mjO1@*K&-0G4SxDj3#4|IO7-lHb_@(GGHbt(9DnO2kz4SE<*B<|FY?}69
    zYQid7eXCSC*AceKcs#BrO>x%jKr!MZ98XRZ?O#kL$#4~z!K69{by*eeIrsMD{)Xle
    z#jP5|+XN!YkB)(3Ura%{ptz;2{e8o->z+E}ux|yB>if-qDiRl_AU#($#Oo-L6=UYI
    zn1<y#50Z`>O+57}D44^hn#a@d>41aVqN}Zrh_0yXS$cB&#%h{mIxky6VehUvdrwhr
    z9wm&}$qOy0CExd*s_pR@?&Gf{7>zc*V=k4uq`*BE*UG>I+%hxV;L2O6115ls-pKlg
    z-R6F3U-6Xj2BNw=ld{3pe(s#fA^qR0bU7Jg_&1)<*b?PgXT*D}DxC9>+%;M;4Xtw|
    zs+;idKZmK4DP<<!!r{9ks4gTvA}DF@zEaNQ_gHIUdCB>w#4Z-nb`t`iXKMqNO&A>)
    z23cj)l(Y0AimIw-z3&CtZ8GEx{M@qJiQQ)G{C_S6LOx;@y6w;rqb1G@3Pe_iEeYf2
    zSwa(cNiy-aX9el7z=+O?Rr}GUSsh8i2P7-Gk*6P9ElwT%RMuwkvR7V|X-r_8(G#K&
    z{Qm;QJvzdnRO0>3eONT|I$VEU19v+f1V+2!m%k|VnYRZ6Yf0fdwG}$m*ou#rV=$na
    zJ>LB{8}4-`!MXKBoSN7gZ%!^jk3rrz_wqC@%&Etk_Ow6?etvQas&-k37V|EkSxN3y
    zQ7U6_=FCh~Ei((1yUoDLZowG*$4nHF&17HB$v&&`(SHDz4f+k8JKjbuXCNWuBK)GO
    zVCV5^uzzzLEqcvD<3DR6WOZ}$0wyCQ@F;feKZ@Pt`Y|)+!=oxgO&!LySK5+niP$u_
    zAcn1>@Xz(b$SX}(uVY4k+kzU!w!zMO419c~SUr3$I!yJ#o9&%Qxs?dLd=vqycD%9v
    z9)JJZ2Afo)u-E@*P_OReO-`G%O-Z9SCuO~S1gBHm@M$X%^6&_D?K{Gjy~p@vXrVZX
    ziA%a--Or6M$=g|rqwGS#1Z*4Tj^S%1*n0IK@>>dl$k2kIG6kHBFEF7p<Q>(z9E>fe
    z&WL;UTQ>PcFwIVSDfyp#W*Bl>4dWY+C+@874@dGV>G>f7q(Jovr?Gyl7kfq3!?zJ`
    z?}<^f_MzLsHrTar64^Tn*L-K8DD&j2Zie-TOZa8(4BQ-74^^DFciADIE#BSVilR+s
    zAosw1*fq9>)`!Z!?0uK~hgHLCkQe9!!nd!(<lAlG_i_j77qmh`10|Ln_ze$U#u6~K
    zhok2x+zL>?z_qY)|2zzT`~nPxp#uZlY?>bMy>}Vy%JR$H6k%*T%j+12GwXVxHv!W5
    z$6lk$PYxNeW=)vkK+_4n@SWWjwxko1ot}$fVR5V`ZN<PAvrO*;zeYLf4es3ooI87<
    zVLfkhzIXbRG85-mG1>pfrA_gj7L(wTQ^6QkKOgAinmE7o5UvG=0t8Gc{?X^QkHy*b
    zebHx)1oMvvqiZEcXlLUKK#$IRP)chhU%G(Flxsw9|CiEBvtr7fXy#`hy_9Q=UJDQu
    z7K~H>Oe5y^ZFtxY!A;WelM=ajuYEfFq`gG$j_N-H@h@H@PVpEg4iPAu-kJkaRxIJw
    zegOFl!0qQTn%hME(*=dgGoHPfxWB3m8zoFv8el~K8hlSIMb09OFSjjf739{-sc3yI
    zO^GL04q*PD1JSl^V=UXx_yW~~ToenMmi30ANm<?X!geGtYkF6O_ki8#y6O)8Xp%1j
    zng>kyEt#G5F?(WLzFaEJ$hL*r=%fVh1*@_|V=PGOq!;WZ6)|nCA0m`#H&V)^vr_jr
    zb6=U)TJ))(pWBtHWbbUN4s;`tu=!luw;CD@@+I|f3!@w5(=o?&H}!3d9D~Hj`P%C!
    z!00hk<y0TaJYdS+i%LW={f5m3W7cab<%B%B3!p*gsW|07h3r|0HOHSqN|`a6?k7U2
    z;OyR~AyGQho-A#w3~!?GnO(>2V1@CVr0D*X5EE8qv}q*b;Gga&QKKzx&{Z&U;uJKm
    zuiI=WW78KYf#yQKpE!33k#r_BOWvm`6~?QjV4%`z0YZwOXFkHULo3nQ^EPG-YK&Z!
    zW+7B*<{_D|@->5MS|_(&J}FIh;%L3qMZ5-g^4wKK>4ToWrbn6Z*;2Im8q)(s)4)rm
    zU(+)N_Ga8zBAANuMV4wZw@9UByhcdF&XEfq81nif>#$*RaOPTw;ED)5K1>dI6@jx;
    zL3nup?=FQPQ2-=2PVHn(>|%NBG;JzJx19)md6;Ov`_Zs)S6q2-q~Xze=s}staNI2{
    zm=l5)bFX3d?EdhoQVZ^mdSGL0EQIW129XACzlB?Sh7!$iAj%Egg}Bu8qp9gfLL2<l
    zovolhF<y>JEe2x8EgJhL<OhB4Cf=tp5!X^~?5?z?keZ+)lzCub^2H2R4cdMiH+_FY
    z#6JU2e$c-<gN85zdY`j>ZX2MCn)b#(mHIB0I*VYMoqVQ@L|wwMfB^VzUVu(jhO!N*
    zx<?+^lxPc|hQL3Ax?<&tK!m?}h;^fS;x842abubiQ!y9XOzsUTa2N*8+=tg6-s9Td
    z>1Z(aI$XL<K-E0ltB^!)6B7~xhl0&<;rcp~{C-q-8-j<irol+PDksI5T4-sDyu^M!
    zeQZC@+`NhXD|%oEW9CP_#IyID)|+H3RbIOa3#Z`Gb{YKVH^sZPozZyG5hy;#S~Q}Z
    zCB^E3XR;886?ZUk(4RPR`96Y!@8VVDYhGV9C(jUY-~>*bP;Uou_;euA-6@tLN1I0p
    zTi-&nS?eYc01WvEB^{b)+okZE-vn<-Jv5ngH0^r$N=ilgz&L%{YIYKF04f{#OeuQ;
    zLibmpUBgxE9$ERe_-pD^4CQ@^!wx+`^1v4Otyy{e#$Pp!Xm*o+=8l(Wy8`Do+M`bI
    z_V{-ycf85KHCXpoCyifJ?f_WRoaygau`ga-h(*ELqfoJRIfMobfhF7codkfzH3rvG
    zE`kVSTq9w_cUyU2^!gF_x7RGxuRIGj^(JA%>fh04;4FB!IpZVUAJ#-uwMw-#aC$Pg
    zD9VqVdym<Re?K-rhuJMLu|g0QcPVD3u4Jsg)tGk3HF6<?tR?b!5?H&TMN?~12l7;z
    zCavg=v(a+x94=c_MJzqJ80GDZgSf40F>;TS7}}wk&H=lYn-E^F0Oa;e+EyAowIOAf
    z(^dj?S_XTHiAX8c!Tw@ywk*QeAH4^@q#k~m!_>oLEbd%PY#QH63pEuSTCiCf*<@*K
    zR_XjSw;8*ye%{Okht51-sD21OQ^BE`?bWMSs9Ce-7c>2hi6TA@Dq2Nq$O(4FV!$e5
    z5l1xE6jG&*c3&pQqB8CS;#HP#a^g_vV?+d`cFrV!7TKbqB*w)dg*;zNJ12HQf+*R{
    zuKSw7p+(8QBDYN=m^V?6?7ncGED3N5^+6|B*KAd%)^>K{izSL#5q+kjh$4z8BAALI
    ziYTIpVEWz4mzZ-ZiYTIdTkTQ=(<~5?9z+yTe!O-mf~hFqirOI7fheMxFUl9h9Ayl`
    z!^08r;R6ztU)N-!P1B%e3>#H1v3uD9{QF<%r)DPGx>n3ltQa{v1LtO}?lP7SQA9A!
    z=JNRP97tr2aC37*E*BTr$SCZ*pZMi7qPT`mmJ)Pdd(X&0p7x{7*7VbU@DwFWFGPGo
    zW<FqbPYEQFhB*B;do=#^fR%A@(*;%ibIk!SOay(QjomLpou+#Jb$ZJzf~hEJyeL^M
    z%D4Mbs^vsTD~!UKyU%dv`~i$<TN<&+UqL-(){B!OM<*Jk#N=o{95Rre1xHchY@KnR
    zR?_RU7_^o77J&5!9(~8}iXk}a1C(*#)m&`=z@|VO2Eb}0Jp)FE^lG@@uf{7*+Rqeh
    z$@&4OgtuYr=gRH#P`4cO*ot65t(ImRsybiXe#14;`c%p^t%O2BK`ys2UnVp3Axwl)
    z1KJM_Er}sDhh9w@lqIdYy|aI(=F07Z_F(?NKatlQfSI8j`Gz5wuX0`O6xx7Ng5Xj#
    zOOi*Mct;HYRn2H1sBfLR*U+kH*`hK746};HThvTNNsn#K{=I)U$f&8rP=k!qp$7Sr
    zrv`bC<vr`6--5F`A@X7a*Q1W4Cjx@G@FYPe|3Z0*47Rp1*wvqkJ5j1Mnz#0^uBd50
    z_VZ?bB1=osdB#56`)>p4T1eRCdd)`e#9O64KI>DIEHWMHj>|PRj@||5R+BJ$U>)*%
    zGW-H{!+$W8BmL7$Ig%ZK)QHBXtDRCh6wjbdXvDOd6vnj6z<@O%y?)emg6;RFpNe3b
    zO~kQSb+oVme7cn-K(rYz^uUgx200mB8M}@L;OM6D_;_$O8cjM*05lg$mVAN@<7VKV
    zhJILu<IDTuKNLi%LKb*)dJ0N58I3X%cH&&XdU&6o0Z*@0i20;8qe2)%`*Ax`OU?1%
    zD0WY6hYEH2q1gCkm^Z2wPWW`e`twnG+M<jkn!r<(T)-bw2OyuN>H9}sJB(EXY)1_l
    zg<->5bDI0s3xYG=V<kg55-}S4`~$FmlMhOnp&a=pO?+DTNgDu=t|?9%_&$YTX>d$K
    z_iX4I(1LSDVJ}rj)6ERf6~R=L%oV$u_&4+vdJ}LtwX_%9T`V!(_krepWT-)I2Q0)d
    zjl9sL`*f^q3w#WE!D@%Kf0;+X${)U`IadMw@gkP30S0XBMNHyY_|BRElwFFovpS)c
    zS1+tNI)<pg!*Jt`=}`p=rs9RtkU4PePaR2L9V5xd?-e%A9*VK^|A*$}&AW0xM5j^Z
    zv5&=>b$v7xC0)%@lqeCfel!^B>tDQbz_x6Aj3jk)dEOS>S1YHAFx<TdI8~^C2KBtq
    zvimR$YEdZr=%o67Ni(2ia!@M`%~S`XDU&Fk0nIWk(>?XOZK5KWW+Sn7YlscWF}Sc}
    z8tZSG)}bs0uHi@2>0|_5Ie-O|2cccNCK_rG#=B}$(i_t~f%%J$vfvtYd?U`MdZNeA
    z#R%X$!tpa?XW%e6TX5!Z{-%rA^aEd+7>}nYGfm8RW3u#Gi@s`V4rMZXnlN+-OHtB?
    zp(xpfuB&dRNl`*a-@vA|fK|O}u!;%g;-2<p!qIKTzB7C@Yk668A76l{9Qg@T*PJp-
    zIr6QEj!%&?jLUCokVk0d)6g^Zbu?Tx1FA8q73vdBa_TdWi(o3s7ZZEb=rk3I;FD-h
    zRKK;$Pa%q>1}Tb?EHy}F{61j{ntKa1$NXqJvIA6SSK#u?r`SJl6WsbwMKvaHQwn#I
    zMfuU#d;B;K9XiBr2YnBrK~bwuY7(>#54vgZYMdtQJP*zqijvESNTc-z`ESKhqU0o>
    z9fqR03n0H+5%gaDj7@Rg-^Y1>ttDE{e2i;{SE94WZOk0h7&$A=MyM+LK7H*oHPrAl
    z)0615G)PMKQxj(H`wQ6oB}E*~&qg8~0hJY~i<Yf`qx+%|^YONZ8f4el$}EB1Lt-J*
    zAGFP=Z1esoi^gbJq&WL~^{V>Z!?!JpCSD|+0Bcll*a*2Ll%NSxW(_<Cxn<hEK2wCl
    zw9xdV*{N0xHOC<NI#g^w5anzf@pN@ZqOlsw!+RW9@i$O@76Jo)Lr!q&rc|1Mb-fGW
    zkNt~r?ai;Kmq)@;jx-;L9c1hJX92XDbsg^#fqeFAKck)oD42O*tlq!$^VS7GTJR|`
    zfJsUM-Dk&uBEu!cPg|mx6SVlr4pwnfnuK*AA*ZAYQqCx0G_BLA{z#xKYNpvlVjiqV
    zyY35d>dJk*dGiJrcg)7%eH7dV)<gko8{}cBK@PCgAp4eg$B+X=>5K}-^Kgz7gmSHh
    zG2PXvO~TLB)c#0kw3|GIy!10sy6tRSdh`k}pWVZOO>1!eRkG>!LK;cXbw8X4IIf}R
    zICSDFqE&$YRY}Ik@+ci%IX21AaKNMOQuxnr#!-|^JPL&o4K?BUaf}N9(ZD;Rw5uar
    zU0vDD$*CZ^jbKOy@8F+9kI9YiV8WnD8p@I2h!FP1>MkJU%qq0&u^7)Z695u%WKlm1
    zU3f}M38Hv_IX${#RY1tM=?0**1b~z=vw>1ibCU3<YK?lKHmXU3qV+hXv<YU){7`Yy
    zz9?Vagdl3<fq%xKNsS7~moFb`cl#e24_t#QOPa&N$^&E8k3iyCh8m<Yy6-EFjcY~%
    z_h+F(uH}g4^hh-6Gn;K712=ZlnTwgf-gMkMGza+({fSy-3d6m)2io=-jL?J>lQup>
    zt#N(b2sCTh9L<_FW4BgKC*iq5!cw1DIcl|5hL%L`BoibcgS|vovz3tF#d31wDGWu4
    z@1z>IJg+4t?0jZ$`^N!0$lrG8-qet?BTw~C7)+Agy7M%mX-mXznuC@#E1*abPYhdK
    z5kAKkqr5$L?uS<a*uU)}Vl-yI3a>A2z}ln10`k>(T->=G7oNp_MXglVhboNzP6<F&
    z%T{WPWmG`VEMPU&i6(D?Mbs3fvne`j9->WXrQffrD$<su<E8#)XiHQIeE7)GXDH>)
    z$m68z(^STN<f%b8P|7ehh$<0r3Jyl(c6Q&54@eOg$37dWofG6=xlsBbX-?bIYK6vE
    zAf^8ulW-bmCTPN$x_apgE}#%;OGMFoT5>qM%1s_-_=!G?8boT1?~$ED{tXu@eGOGl
    z%M&;w(|5fI;MJRYB?LE9tHv@DP`@`FRRmK}MESzt`PpEUL5<EN;F?Lt97uD6R#f_~
    zp=qOT(yu?DCxWRcqWn;R`faXXm*~Y=NE@)zb?!G|1&Cn!P09Bd#6gto)v&*{GJFf=
    z6=_Svh9Zh6qGW@1DT1jeX?5p6!YxG+MFi6yO;UxlCCOQgI;!{Bzjhh+Uwvm5`OJY|
    zMx(1M-kpcf{2d5Yh|gLS5lltNB6)mdE-Zw!C03HGr7cN%g++r#VDTAKtAAp#mZ2zV
    zy~Q|12~Tg*ZQ0%Q-}22hj2u4?@08;67DWV8QL;dk?+&2U&l4c6Fj`An(k6>(OBgy4
    z3UD&B20t;D0AP%XWv7&k9rZ2a?vnl&Qf5v5y1-I=<f3FV5l8d0i6p$?m^)W)pM$#P
    z7^pF%E#b`SWGI*=o%+-x>7@=~&SO?{F{?u?Y+y@{Cz>(StpJ#ut!U+eoyn<s)F}Mg
    zZ#q>QQcgHo+(>LqQ?x6hh$3pHqI`m=X-l4%^g%v8@fXTlSn<>&Q*if#deJ#U9nun=
    zXY9q@3+v$}mB2zGLGw|&@J<+xj(&?Rf3}3g$_iE%78pI%2mhvU>+p?>v7o}jEI+{5
    z2IVke$3xBUaShAke>=|N=-L60@JsjR`34#)wftxN^Zn>j(G$O~yC=Q^qKIHB%IAw?
    z@#<(H<!DQqkJ^HlA5wHymN3*KrJ9aLnejVu&VL>1ou3Jhx~upgCn|&l9>MOZ?a99W
    zC^mjM=8SBK<4ZbY?YStmBLTjn^PuOPqZq&XF#L~iK+ycPY2=DZBMMMF9-MuE`{BGR
    zf{Mq>|Fd@;@KqFB|G&58h6G4JL<Ip+AJQJ+vpqzbfPfV{$`dPA6cu|{>{t-H;(Lk$
    zc0NUvry$a7G%1208c>0Ult4mq%if)DX4{>e-MtAWHzY9U_nTzz?(FQ8bN+MAnfBFk
    z+<RV6oOt<KJoM5S4DJlvcGg|#-SJ7<X3~nk#@UAs!rMQbj2r*eQ8Ixf38s>&f%3ZI
    zz285_#SviIn4vhRQ3&_HZyj4=9zD|G8NB@9**Lc6MR>(LdSuZ?O-HC4bJGlVbjWKD
    zUX5GKqeH$kj}A$0{2cc_4V?JMcX;@+f#}umLQE;1tH-GjPyGN@--*b%Pc+-NG|=$|
    zEZq7zZo6zao_vaJnfD=<6_s%+X)^>$*5IyQT`^S~O5=_~i>PD)NfJyYRTD*<_QAOL
    zcFdh%9$Ru|dklMd2@qe2Prrl?o{h{MaGXbv7;qNu$sHY{8rq_@1xVS(A}pss*S|;W
    z^asOX{ngTn@HB%Pz&^B%qrul)iY|r5b%TAI>i<j1R8BJ;kM{c>f`=A&#<Zf>?6{I7
    z)n<|dP<4(P;GgH-gV?dhVo<BTc=g%O@yPkzbwFcBkGwa!AHqokD784jA+6bNh6x|2
    zO9Q214xcy7%jF`3E8NUC;~Y{VxKqtdp?Ad6GZ0du!`1xRxfAfi4~sEl?n?A+(M}FN
    zNs?eHsrpDfw&WysY{}$pD2*P16ME9?=}{chw=edykw-I)kGOD5NE)|9Fq}2_R}ASu
    z!Hd2S-&BMh^m%iV(aL8|1n&)HEi<1#3B|3udb~IY6X!gO&xba{wdc1-Uj1Kj=gF-c
    z+rx=V16}t#jvtXENt*eRuw%J-Y{@s~u_cop##NJzV@sNc3UT_K?C6mPaoA}OVV-^T
    z$Sda2BMRcY+Q%WG1wakj^u&c!5q<TF^YHZhGx5d5#}RFHI_5ARXRg2Msi(VT9;SXa
    z!#=)b%9JViksX&JBDLohZC>eaM>2E(hQG54H+BK;KII_1JbR<@h#ey`^a4!ycB>tG
    z)hCb8x_QEJ{K)6ejlk89e+9XaN|Mw}B{>@^Y#dv{f<^4ul9$=BB`}U2SvI8}&N=y3
    zbo=-&JI;h3FVO$%M~7(qRQRxdymG|adOp++W0t;+P3_OYjc4`NZ$0$VOIUo>F#U)%
    zKfRP4QIcBrBF-H6qT|Wf`P<R1odMs6G^uB6i;mw&ADgj%0gu=X*#rcd;X^Tc)^~^>
    zcNDJZJ^-zXzQGCmm0{k*H}UNeH{raa8yV3P>oNJQ6}an#k@OvQBZ(E0pTRio?|As;
    z9*|>Kl4^qFu_cu-t!vM*CFLkC-l>1b>(~-2PL+-xQDZ1BEul49h=TgEua>xX2jE
    zV@o8Nt(A*{eR3;|Q>9ZnLQxbHNL@~nq+KQHGnFJsk|YVHk|arzR7(s;Sq0Tk)-)Zt
    z$#D(UH>!TpC3oOw#(R?Lwp>autz$G~)DCmXkF^nql9o>EH}QZ21wQe(=dSnQ{Dv1w
    z@Nq2UvHug{J9cF5d!&NAaHu2RBVyUa^x*uCowitaLR>{aQo<Ju&IpR)Jx$Azj_?6K
    z@2)Zvk2cJ3U#xG}L$U7pJQ9I_9ErKx)LAJW<?!P_z{OyGWfXbS_ZwUVJ9y29C-^Rr
    zZ&Od+5qZUfCmr<*eeU#CeI%IHDXN#7>tr0?`IR0F<fnFVjA>3s_hvdC6cish5y8hL
    z7+!*}_+WsKV8e&WMd0)T1TR7Ik`Lk`xW2;WCD8B?U}WgjeCpA+qY=s|-16-_h({+N
    z)~%>Ii{<fZ1HX>8qK-lF<4NH=UI6ORM|)$qP!`WR_3HQ)j}A-d;H{WhwiG3pW`R_d
    zZ)C+oW3U=MeQmGcAskp*_pNo9R&@H_{`)9s90Azg&^yWrHtw`!L-E#9T*72AuHx2i
    ziHCN|ffPyaL~6%jd4eML5sG7%KH|FY0kd!rSDqC>@b?6so$Y?^>`W9pJ$6UmC6y}&
    zLAqY;I_LA|0rXb+h?S}Wf9;s?&lNjwRvHR2t;pr;R}nx?am2AqRE7IX1ii~ukABt&
    z#5VA<lG1ILu5TwmrSqh?^W<qoDV_?jqaOKh>UmJyJgbO&>jCmy-CFt2w#!C>X%;Az
    zBAynf(XkSaVc^;nS>bwCUKh2}JG5gCt{kACNyPP0ygoLGFG{^EHsT{_=}e>*3*z4F
    z*}<&oGo7rwDR~h*dwl@Pni#H}XhV>hegHR&h@eX_ft#Mxu#xaN;^J}?Xb?c>Ry4gJ
    zE!Irp;}^B+$9R6?nNgg3n1+eZ#WA(mEDxuW5F+4-!o^YiEnP=WZQ<nG$;<0N5Xg_<
    znGpe?D2A)wgg>PES`Q?xtofPRJa4$b#NWT{XnIdgk$BXNb;FB)iJg}59`VskiNCAH
    z)9XRJ!CBBktrk-Jz}k8)!L;sCQsbo6S1IA7*;h+2;t_M@&_PE>ad&^?_J)-SeDEF6
    z;D7+Sv<;$tvjB2K1j&>)LlF-POL3UU*7pMT_bNqfQgg~+B>F1Ul>1FWHFb?@5)|)|
    zaDZF@*3MGA3<YCs0~F}1pb{yM%~HJ9_?yc%Qnc=qs<&0jm(@>xl?>Lb=Rg-tbKh2B
    z+<GFxe<vjH*;WHef+{T+&CB-9G(Rctv@&^jTzujO#C*3VwA-!>%vCC`kQb6cx2=rl
    zszP3LSxm$?E~h)28m+q%%r;3dtwyrU9>dJ{GHZArnitq94o%3(j6qz`-MIQ~ehk-t
    zWSWv|2qw=`p+soRMB3caHDHdZG+r|cD7ybh7rqEPaY@0JBm*`67?HFFYUL$B-y@os
    zKbnS?j%R==&$${NdMajn39No2U<tp%bsCyc3mOMLsgxpUUy5Lc3ogTy0bXu5sN!Mu
    zx(D7A#c6`GJc$~dJyXMcEnXPF0gZtp4<-2IYvWm}M8&1|CG|KO2LOuTizb4W=jCSC
    z+s@$vg2UZnTQ!-0$`=e3&ZH*3oe0kf3Y={q-pi;7`?x2JcX<GGyT^!#ubkh=nOEZJ
    znNhe-M}lb`0o1rE`o7(E94{1NU(Wjkt$2lph0&HRX)jCSkq_CXdc<Mgkn*%Z6&x7C
    zsPltpQQt_hyjaDAH<OsWLH8Wt=BsniNljwLA7Pw&locn5G0&thHy*^Jw}x?0Jb@dZ
    z((t>PpXObnc=}9&1@Fi4$U>(sr&!=efi98aT^fb}+&nx7M}(3zy_BBb&DR>=|6o-Z
    z=k_wz7ME{M;PK}P=EtF@X8~s-#lBASbW<3IMpQg8TE)D$SXTyM>A=G@)0E=jNC;rW
    z#bNY57`6)g^^61_nVGKrp2tUUNsj;;np!T|p1{?Q5&RJe;?_&TII_7VFd7!qckiAK
    zH0wy~#XO3uegArx&?|-4pG#m$JdEdVpb4#t;mS!yUJF`=F=}wY-7f15kZs;22Tz`o
    z!uua8=yp;NE%OZb&9rxucy6h?%$axwLg(z{IeuJ<xB<y-J3o*n8T=HY6Ni6Sar<D?
    z-H4EP_q8JA(^V!%F1!GrWOaI!oV1?>VgjwD6y0}f1ULeFrm5C%V#YPDQ!26F)%3VV
    zyvF7R$(z<_Sj*n0ftcx7@mNp_!+7=z2BZXwzfa<$MHB!ELb&9bF#1#BOsNVAsW8`X
    zW&r8>wHkij2ILlmam&>K<kFA*wuAoOCxX*kTj?q|>o^8_8W#Oz=36D{{A+2(w(?6f
    zL&MbQ_Zk(ljaCSv`m=!j{h<^{cm6Mh)qfh_8`{%IkG*SYwM;X;XJi=d3V=m3l9*k_
    z*CqEoOT)9itJh)r`lb;EkZxI<#Jkf|_(KigpM9fvu)C(ee?-41Zs^T`l$P;36`wEE
    zP_%{iT`cWT2yL4YtXZ1GCtqpUk_e!4?=ViL^{JN7`ufxOb}=o>iX!08a+Z$(*wzUR
    z^Njqlb{RX20%^X{F8{ZXnC%il|9m5l(GU%h5Y9h`wqHoY%3lc7U=V#zq;+VQmFVs6
    z<g4^E>v@`olr+!T^<vZ$;rT9={++JFbo;0I)l5UR{(JMQo{sfx6|Oj%Lmtw<HCIsV
    z`&OBWS(cO#hgfB>WI}bOndU2L=GPfYs6;R)i9L9e?p+<mCKrd(Hi>4Nl}U4d=Z!@v
    z#8Zlgxn;b6THgQ~(BF%ul;h^FSPZIdZi(X2J|UcY5*6a{#)eD`s6Ki<j%SzY!zFn5
    zt_Y53976xX7~cL8IAI8IR5!MXzlLDr930)00?Qiu{x<0O;x~U#Ql<vMwDOdwK`7Mp
    zH;Pume5UlfmP>Qx%yL}&35}7X;Db>CG-+n|d{yT7q}nJuW%crls8&nC_~&`$xML>l
    z`7H{ZBj={~feMeN*N?)DF7MMhjN_WnZ*PoY@U!fmidW}GF!Amn+8-Cf0bc-T9Bss&
    zIU$Az7dkh$AjL79b+g%4ii&kja&cul1@(diYgZ+4&x|16ItTcLs?4{{Grk?DZ<K-6
    zNkTQ+r6IkukhaUMvjTb^=55WT?GmKzlE7rvE@r2+b3+^>o}^$-(|P>fDE=N+(16zK
    zdP}oX1e4nuRaY!CR^v2FtBy3M8_DD?LpwFw%ia@~xU^flu1(jBkq^R|A)*^V@7dh>
    z;J)kBRf#!SfY&*VV;a)sB6fQG-nQwqcr9Gx@@u&=<fQ#N<uBEJrbH1&#Z6|=Or}ye
    zvxWm%y}^m%cB<1zCSIXlu44EEdC=udyhzjl%7qjVRI?|ZKUc%fK2+<ZT2ewYSw5~}
    z;u4yc$#@IbQSGdQVOsyXD2boX4C9bCAskJ+_rm@GFpG2A7ZmI?Gw&Q<2DodMx3sW1
    zr_1PrL&Wd_BA&owA5$})#tVi`U$Z%Q$I@d(Y}76Xr_le~e=5gi?>hwpAc9GkmTOhe
    zKnkPA@0H(cH2r|_*7g+El?Bl#CxE8;3R=;6Bk?5OSP+0}4_aFQ0JQBG!iC*~Xw!t6
    z>kW(uabo1Rz}m$yl>T;*YsVzL4(Ax)vS^W_w*gCcf!Qw6JOfCLHrDXL>nZ%K=QCh;
    zNTsaq!244b&gZOHeKiNHPLI2D8Ykg=zr=f5FoAGHH<u=Xg9q&6yG1`ZAJ;u$KrqcM
    z0Co-2r{#mdmLI3zI}OA<DD!o}t^yv@x+Iv^a#B;A#^9PDPFxE{EaphhwR28j)}Nal
    zdW%lf#F-)v4!D_>giO<o33kc=Du!99R61TX1zStBb~+7Fq+?tBO<fwswMT|<N*@gk
    z52NwR5@07bJI2<`d<N9Cz-gM|6fQ1bgqdc1XOS|GEipCi5HaeGrN^^+gZ)D|oZ4>R
    zIp)d0F|XYIj%J#C-1}u*CR8>3lHLYEAg5k>@XE*&uLV&Jy<aY#7)Z+&(=h9674_PN
    zanyda9rU{5nwP6zTU9pi+4vK3#z`6atD(J{krK2Fs%hOQw)=#Bi@*U|&PwoDZKv>F
    zd5=|83I}3BecIq9%ou<Goas5K;`BQa^gQyxy=QBmwl1uAPvjRz?#v9QWr(XIho@F?
    zPFa;c)4HLZvZ*<7mFQZU9yG=LZ(Jv=>lF9fx~FpSRbw!PJzxB73jbr8XOkQ}bTrY0
    zkKJc31$NCw(b+qiIW4Ps0d(_{rEy>z!^Bio#W77MWqpIDagcGY9ZJfL@4fRW)tVTP
    z42Yl&)!J6iQL#d`^5-m<YI3P+PG!>XtFX&&eUAY|B9%7FRIc2X6tcW9&|tm%mc=oQ
    z`<~uy5XR_xf;h;E&!uhEoh4hbtz6x!#y3>cMbh$bid|1C)%^Bl>ULbis&y&+8l&H-
    z7s5%cv~=0@@+vsD8%sZl`)*F)whvQSw9OD^LhFjfv#f?h!fF#E@Y=csl+7Ea`J#E7
    zpze}k`Aeou&6F7aJ)>@k-M?z7G@ls%6(^tQ*TQerSw{}uVtl8!htdA&dqzB)NA0>K
    zp7n0?f|Vc5XhWN?**DBIthBA?c6xkXta#Qp)U+Jf@+6i&rEuj?9BHr+H`7k2+0#m;
    zW?BVP-tRuURyE5E<EOfaCWhjeIL(_dUY##?Ch`3AAnrY0LASxVc)LRqljacw3W7MQ
    zV-Vfj5xjn146jTj_*Yv6{V$1N`-Bu$*9+sybBwe<`Opvu{i8V%Ja>B%<37^Rv`Z8{
    zn^30vErEY;F@bSI65nsiLC<{+mF>N00b9dT1Vu$tjg02*8^+y(3~i4+kI>lfOeVU<
    zX={zN|F)Qp4G^fNhH532GzSs%^eEUp|6~k}8b>jxbp)^85yv?X5Ulf9MQMRmkF$el
    z9SfMs5jC%Ark{H^fu*NM(DAS+UOqdC*Ow?5d`<|>Xq?{`rtl*L%=f+pI`jzOjNv(`
    z|8WW{Ne}}mh<^5b5}S51Ko4Q?V4!tb7(?3{ddNSzDR^meTGMRZDU5SVG|XR0uv+8X
    z4)poIKZVgQm(q5bNX_`%5Ux1K;Dt{n6CLF3fjqNQWxNmy7<H~$TD{^nyPd0Qg+&vb
    z*6ALDR~+sDLOd2v5yfK)6gW2lAJ>^Dg<IL=R4j#6O{NgIRtKkZ39s3$iffvI|A`PR
    zy+FV%tA6tQS2lK+V44L=a`P@-i$REc%}<(%IP;TZ7K_OzzZ*l@_6Tl2C4d7vhH+EJ
    zG=J^bl)|!N`kxJ6Op4&qvqL!hBC~lZx_>$;j)&JOPT(#K<GRa@wBwtS82OOl)3heG
    zyhQ<~=gGkK)d@`6skm1TartNO)hi5PKrcth@Y{@pzT}lzy-f2;ni`xIQuKQX)4XTz
    zD@=1X%gX?zq!hZAdhg?LG`uqhy$;C1^Ou#Q|4WL?s@+~{2!4knBj~{AU}p+r-_>y8
    zy(zqLYY-idi{SC&jAy?tisQo9skTbf_+VlT&2po-^vEFko=Ssh_iN<@?|nhgdx(NI
    z$Ar;_mTAdC4K2C^(WPGwuKqfPXTC|`$AJ;FJ(SkxAq4-vw;U_tMjqLr1<Xg9TouEJ
    zw{whkxiDl-%GL1M+i^U)&YYWS`e_-c>irYFxtYF?`exk`0h9>bOJNCT+Iq~M7kSHT
    zMVB8SiSwOYE40LWLVB!CdHw^x#8ZRMo-c}Xl^(ymS96Tn!e@?@ohL=xHN|OH0aYGb
    zQi)3Ck1Wv#gScZ$_#;b_$)tX4$ra;|aP-vf9yjx<9wPMgF4rr4p<8<e`!Nty2-dEl
    z0$wg^j0(E8q}m;QXYp?YTWlc=;PoeRaJZ7d$v33Xu!VwpG~us*b9VgfTUTC@i)#)C
    zJ{?<zo0fTKAso267%PLTOMK5&FN#x}a=kB(Pk7Zeyf<$;ct1rSOe561U%D;5PT$f}
    zLIn!iM~#pFurZx}Aj<9&lu&RjqCnZCDFyBb!Hjk3^gGkK%uUd`-01DzvvSe31#K5L
    z<AS!!R=<_LiqAeK@LmGy0z|(V{9YLAQJ)pnH9XP~-KNMO1Af@;;(L#dz{N9)0OCG*
    zWY3=;GG9NoBpeRAjxMo|Eny%k9$g|e(;77OdD1jgxJKH+5DJ88CmaWuy}2gz>a&(s
    zlsLhhA~^OrG4e%CU)t&l4Nd6pZ;FU#K0{P%9eg;`>=GFJgMt)i+OdHbJx>niPLHJW
    zBgOEtz$&Ujg&)A<v=-tgc-*(Vc9G*iWz~AG-G<UHBq+Kq63~sMpDH6*ywlDcHqvx9
    z3cK7bEJ}|ayLf;r)10;ZfRD4Dcr)2mf3EU7N3S(*GZbsUGSj{TjyCk+SLdj>(e|G8
    zuQ^A~UEd_`_cP0elb<Sn0rpH7;>e>S%y6iFjZ}hZ7Ad8rr=0k0Z3%d-7jY{U{90q;
    zugp4`#^<}nS2X;z251!0upwodlcGjR@3m>EU?){)7k#N>b;`LXr04o_m&V~y9-$mw
    zWw06A=R0Ywhc7R2$8pR}6kI8$n&Y=-->D%wrw_raEd({xYuTf?Q3~I)@byEub*e6)
    zb$JbmV=gnF*Gzk!*zv^po>Pa2t4;a$)9vN0^}6Z;-m<}`<>0dn)0rIkFzd~|>WbLB
    z63Fs!y9CoL3`VKqJs<Zt5lv=zuca0R#FefONI8Ij1Cd)sm%Dc35$FH?upogR3!J%D
    zJitT|@(B2Efyqz6_S&r3vCSN3yNQDDxyGE!mm&zb3sHyw)Wi$WJhW@WFXq^MM$D(H
    zV<Yg)lW#kF@k1p*Qt{zQ#Y2@R&QUw(1bCnKc+hfm+Cm2|0-RXSzMZ{UXcrg~;@*`@
    zY=58ciS4V&{Y+~o`I!Rj{??M6o=okxV-WGtJbXb3{?@bKh7x|8SNN}EhNq^MNy{dg
    zhb}UfBh!0i7oPcT^5IuEq*7^m>Asos?Vm6An)k}4RZ)KVO<nx*o7rl?F7bepp{^?8
    z2a-W9+~xAJcPdUtjF;_spb|{8;5*&+PNPbLuOvy5YEE9~%-MB$mSDOYHC8VtHj$_6
    z$qq)+9?f%d1ymM$LJ6Rmdw*F!lJ+FhHPewJN6MP01PJhRPWic~d?+a?LD#Nb_u6{O
    z)bo2itIAXqNs?-l#;Swq=+UEPZC0RRdI6{3r2W0sjFq4$sjgW^)vlRJs$L2Nr1wOU
    zq`j@9mtdMDTEBk1tOQAtY7rf9zyT6W>sk-TOo{#X-(Q-&l{B$rJxQtw-m8p>Ulm@I
    z_GmSOs|1GKYidc7w7cY0f@w8_fdmXmlJ-s+mS8G1FG-T5oJ!49f~X`(l4^(CN-&iq
    zNs=T9rcyiI(}|E>*{?~Gq}?a45=`qBvS3eK&hE8LSEp1F-W~fm-ktM@Y<o#kGnG_b
    zRDxG7450q0Z(wH~0?7)?G_;L<_Kp<-fitkQls=Aa#@G?JVC=ljwY{W}d$vfDllJQn
    zmG1!JI}&vxuN!CM{zren&<C!;Vfp?b6f0Rx|NjePUppT8Np?PT42eV&?8^?dQwbW-
    z?mif;%`TKO%$@iwKKt`%+;MFWL?xR_FqKqw%C_G$M?4Ij5c}+N!kxnB%!Xzk|10K^
    zn8$6fBO?;A7?fxfAw_ZT?oSek#S}!NA>W;!xLPod)!n=Vk32mb2Mrm9Hiw5j-(&Y!
    znL<7L;gSo7xPd68_-ec=mtXOjwWq8akI_ntB5WSMq$QI`AcCBb`*1ZDKS|RJRFI}v
    zkL=!pakt=6^%dMPygPRmy`;TOPNkV&=g1Q1a=d@nNjUd`53ywKYv>-NrZP2`2V6f1
    z|FaLFD#yG@_oHJlV4BZ|<Aa~djYqK+xcT_rxc5I(@X8&>BAS<n2z~#)35y+HTlL?k
    z==(Xy%gaH2?|ZOhTgvhL*ST-uye?@oeg7-o!JogpgMOW^(Vt&_L=>HSkHpGSFHp4L
    zH;$BFS%m}W$~Mfxb$tTJq4^Ed^89=D?`ds#)tl(u`)+){@*CWAIE_=U9&$R}jUo!7
    zizeNQAO+Ukh=S{0nMc3qO!HcrW>inJizCf4GtI?o=Hs5zjdDghU5)9#l<E0f{`PQO
    zHyucR^(+P+-w7v<e5cO1VI)Z~-Fsn_@V%;v^*>I<JNKN9PRCt}LvMQ)4_|*Wri~el
    zS7&e2W5m}@!?A<!LyOzqz?ZW>!?*_qB5W(LJF#}wOx%CbDH!(f-|);!V=%ZgaNqe|
    zFxq^7!<ToV%?UT6-EEUFYx+3!n*9L%)(hBSCb8kmyV1Ja5KQ{vbd39W8a{ph1q|=d
    z6p_4^IBCQwdYqxRKZ9$|>xTLv|GptWwqet@ZTR!gKlM;5oJeS&2~_eOE;_IWp5J^U
    zrp%s!hb}0`#oZ6Y=NpZ4jg!e8m^pJ4j%wExvpSB#3y)n2EFFb|A`$%KyvK3Zn3pj8
    zG+^xIS7Cm!nP|!PdYUh89*HTlrsKg2VtShY-C(4tsz#b!4m}D_?-+?OV{Sp4<uBmu
    zyFaFYte~KEH?(KoiH?8A8H2CG8QoeVC|PxH6;OTjG)O~)hFluz(@;o56B_oXp#=>G
    z({LCK9ceh6hGS^xPD39W2GVdE4QJCZgocZ0xRQqJXt<e%J85`;hDT|5nuh0T7)Qf+
    z8s4ViLmEDz;d2_kqTy>A7SOPSh7~mYOv73l*3+<whOIR0prMS0I1MVvB59gNQmGVC
    zRh1-@NfM98Nm*GLDJ?A}+qZ8gTeof{n>KAC>({R*>(;F!bLPy^-(jJ8X*+pw5J<S^
    ztE5!_y@R}XntqNg88hQ|BT8}+85jULdCU*`&*DW-=;z;__rfxFeq!J2arYng2H9-g
    zEB=ZcNAKmG@&^6J4l?F=ntq3|q*ycR;g4w}_4H<}PwDx2hCN4T^6{FKE4T5VUeV*e
    zu`KDT&vY=##NyFT!VaUq*<t5#0L{-yW_gxPI@9?6|IBpCzSYZc{!2@Y8e0CG{``_L
    z^N8`)E#&^rEd4=bdQnWjv(t?8@`CM-G{4-GIn4pLPd4gZ+ex0M^&aW@3Mn>!ze<0H
    zdcHwoL{e=fi|3epGC5`P%H)>GFOy>?&rGhFd^0&`^3LRbSL9e~rghldXHp*S`3f%S
    z)y(uUCiQ=LW{ieykH<w}VB%FB(6IMVjGz81H|lI0t=sD#N1(BFt6>M6*$YVQNI;FP
    z#I!Hzy=B8uXu1iS_kTjyjF+#YhHU&Nd`h2(dR&8Et=()COBw@Zv3Oa9CCTmiAeOJ+
    zfTE%zJzUbMzO9NYoMsv?F}~A#Khv#{cMLuy3W|$)qrEq-yX>FlU_coA9s~q?3_{nI
    zIi^1E^p_>fG<!7_(@Z$heDJnY&3aWW$D8>L6-nL1i@T2amb7<~9Du6Rj&x!0$Go(C
    zb~&B!+SVcr{Lj<4@4BaPL7(xM_R<o(Ijo}*mxdkXNvA=r>$)+D5eDc4I^2NwpEv>G
    zq!CY}xOH&ri2j_#$Ze?kWRA;Z1^(Y8X^Iy6HMKhdM;aclKQdtoI@c#Ac*W76Rcpsf
    zO3L{OhfmgL4$~uVeT*&*fTWpb{g$nK1;zXLZ8_>B!BkRRflgyEhKMKLD^7G31wF4N
    z&b;O+oO#7%xH7jrUYjrr&rvXKXeQAh%CSKCYE1hID7>T@qWP_H0@YHdPOpb!`t-$q
    z0r3>|*67cke>4e;{=5v`nxx%E{Ag)3oSV5!dWsQmMkG$`Qz8a%AJS<6PH5{fJHR|4
    z-F0rd7oc1BB1Y_kI}bp=wt<RpA6}=yvl(4_2PCOZxi_nuq?uobC@z3!yn^^;tKP-P
    zQEy=J%60g8>1u4D{F77Azy>a+2~N18J;r?eJvMAuiC6C!iK&``$A+JP95lk|civ4g
    zXA}+_^bo$LX{=qd6qEn+GG?!1Uv7j!x7<XZ&p^-Pufo&?KVikf>3H;pC+(4Ymea4M
    ze}<K7)?%mX4W)6$(_6pB)M+y?ZQ3+FOqntTEB=hT`iCy3Uyl=M=Fe=`2jizN!`ih!
    z<C|#{@aC6AzQ9LJWmNSMrY>iUz`!7IR=d6!KZ8qiMv*h8ij|^*L<yf#eH`8UA4tp^
    zj`7o$W8L~<$*6V0s;hoWLTaXUh;nGxms>z=pva|WX-+E4#R#K51%`0cyhpXAr|!9s
    z2IFV2<4DYX@qDK_O_}P|(~iRR=3LZA-u(#|bTG8Z<~{GjvMKd&&Pli8*pKhBUzxb#
    z0^@n#$FS)AJPJtn;iMy9G(qYHoc1u)c>i`P9>3rj+;sgZ7&oFb7VKDvF8RcTd8`cD
    zvKMj2fEOLl#?CKB+ja(q9o)psaei04z2+<2esK>B?fZ8-?y%REVTg^P1x{U+Ca}+8
    zjqN$COo;UPtyYx0u6Xy?S-AD0?ikvKPjk7RrqOvdv;mrVN}F|}7Yh};{@M(<6a52&
    zaMgg0wEUw{OzWVbWY@h9pv@jS$c2y)!X6<~LCC4(|4hN_yJFt>d}qxZ5Kky&r;=L#
    z#S==|>7>?C&$VOo6BVV!rSxwY1qB?8#^>Y2yd%-*)zuh(VJmFgT#Ru3KFIf4!lTAe
    z8Z&geXnwxC2acDbG->qB`9cDv#l=X4a?vn9r&^CKVd<*$dn`ZEYElVGSYH2fn&l`i
    zrf)}cX&uVRQMJ{qpEqwFTC`|^P$*=F^fKmv{om?ztspg1Nd~|b6u56E4OMRKrtNy1
    z!hQD1kVKS%WG0n4ANe`nkF0O=Kx$oArL*xYQx&OxmD0>b7P2I%PDz@izwC3V)7$Qa
    z3()ua8>p$?s>ZYe@mi9kJxP`&nEu7e_gbf2r@%Gm5R4f8galGad!_t($!k^RSB~aq
    zp@Af5NFbF|JNYr2ijM@-T>#3;5OZL&fvCM5Kq>>I)_n=Il4^n%nOrjY%!b@cGd~MJ
    z8;L|vT3U+2!a_$wR;FR<8mNd9^eZZlmuqBi)Lbj7H<`!k0XLDacJAD%%V%|(_EIyg
    zriSHB1}oI7R}Vk__@fS}Vxv|R_7shsFOOgO_GYbgR|R-?)KP5&`^w0WpG+}%T)up{
    zE}!C*CpiGMn}tEtIIRZB&CNyq`t`A3!2+~x+ZGKPG|+p)z2(5YOxmcV>Z?~t70W1_
    z3!Y3SQBqQ(11Yl>nS6>sT3sE81k=Bujf&=~7#Ntc|EpH5LRncE5{ZOkVn09OpQXkA
    zGh+As1g9<u)B5%6^|Bvx%rS^YqmJK<ng96X5B&1WFX+~-Tcy%t!ocb@8^lb%yu7?U
    zR-4-sl@}finGcj*^Aqp+3HeNsVcKY;Mvai4pYJKJeio+$(;8?{QFy!>h=E2p9M*eA
    zYn7A*p!^Cd_TO4rRh{O3b%U=}{)!lF?b@~2ym>RuI_oT*iT!>zZrm6xTehs!w~LF5
    zQIMq>@!14wN!gP{ew7lxa*02f(OTWa<d9wa0jb}SIucB4NIT6GSfg5jH6{Zg>%W(K
    zfZbzpEzPv9YNc#1(xpq6qF=v$I&fzCJ#*&FN{_5s^_UIHX4+C)tr;2Q!IYOfultpe
    z-zu5;FTeb<Y@teMc-FNAdc0OC23`ZT;#p=ruZ3oF5Xx^R!LEL1R~U?$^|NQsuJE^|
    zIaG>CO_MDflqv0+8qt=3`_}=L2T4&o<)86@DuQZtwSmq>!-fr$ty2LYiJP5NwCM?d
    z&7O<CQXY6kFqMN1wF68gh}I45RMbd$-)W}JOR9SUzclkJ(P&-+PL|f<XL4AYt98v9
    z)|$+3jWl|JX-xrXU6|`=(XME*wJB{$HD~6FK?G7BWHW(kRhF1aF4{#cLNuLO<5)gV
    zUURj~4)J}_9bm=h-3L-L-8(c>si{^COfC8I0#p%1y?|F7$nr{GYNoqMTeSeyYe2(l
    zG%ql%jETQD11uYjWuYo<RGn!{K&%~|PA2VCoHFI*N3HISP9&KAMZx47)B;idT8zv5
    zq!pR>b#*0FO=zao33xS?wxpUd^Zm5WOaoG)R$3j?qa>JCLF?fCj;vw)$Z9lx<X;@n
    zlIFcXv5D8>n(DfcwxsOKq6mm0n2Mn4HKOkY&{8w4;l||WhI)ai)i9P1Rh$-OUGqRH
    z0kvke$LcooCDp8%&yae56*d4Z{QU1dI%P4m|p(rXT<)i7S>hy}d-KvhMXfJn+d
    z;7K#TZd-?$Ony;Q^_#darX|6&1{yKb7IKz$Do*m}r&9R=sT_35Oxc{iw7W<1<$zVy
    z>#w5Kh+Zy$%B<0sU|Pe#>IbTpd2a!#7kJ8v`nz8<m1h2)VeV(zC&O#7$xkztX8vvj
    zQNPLW{5^4y$qP&+pk~%g>+0HkN!i!v{U+Y?zEI2kR+$x0asaB90IH>t@@BudbzdeB
    zm4I5afR|06mQ*u9S`ny<0PCmKR#iKdV45W}pMTDq`W1nv1k;)YJPD?C8%+Jc)$6~P
    zHY&~hS_r5-VESpI;!FSupb|_aWfxd8fo63AsRYw3YN(<wRdfkdG&4YwQ`671D59!u
    zn60!W?H*vw<OZndT7qdU0o2MgjGuNYY4-qLHi25w?vce^0nO@EWF(kYV~;4c&Ft0h
    zU#-uIwi>LilPM+DPCoH#+ibMC1k)^lYGr`6vb!2dsv6)`#!`@Ax@XB=RpLl6ts{-=
    zxAT;w>H*Wgvf;G^)4dm5C78+vmS9?q^((<tQgxB^yi1ZKNm4b?{|hhx7^@_!y@%ZL
    P00000NkvXXu0mjfl2;((
    
    literal 0
    HcmV?d00001
    
    diff --git a/couchpotato/static/style/page/settings.css b/couchpotato/static/style/page/settings.css
    index 44390f95..d85279be 100644
    --- a/couchpotato/static/style/page/settings.css
    +++ b/couchpotato/static/style/page/settings.css
    @@ -587,4 +587,9 @@
     		.group_userscript .bookmarklet span {
     			margin-left: 10px;
     			display: inline-block;
    -		}
    \ No newline at end of file
    +		}
    +		
    +.active .group_imdb_automation:not(.disabled) {
    +	background: url('../../images/imdb_watchlist.png') no-repeat right 50px;
    +	min-height: 210px;
    +}
    \ No newline at end of file
    
    From 95f87396f96cd0984bdb89b89872932adf734217 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Fri, 25 May 2012 20:21:35 +0200
    Subject: [PATCH 053/207] Notify when wrong watchlist url is usud
    
    ---
     couchpotato/core/providers/automation/imdb/main.py | 3 +++
     1 file changed, 3 insertions(+)
    
    diff --git a/couchpotato/core/providers/automation/imdb/main.py b/couchpotato/core/providers/automation/imdb/main.py
    index 248db2d2..1f9c62c5 100644
    --- a/couchpotato/core/providers/automation/imdb/main.py
    +++ b/couchpotato/core/providers/automation/imdb/main.py
    @@ -30,6 +30,9 @@ class IMDB(Automation):
                 index += 1
                 if not enablers[index]:
                     continue
    +            elif 'author_id=' in csv_url:
    +                log.error('This isn\'t the correct url.: %s' % csv_url)
    +                continue
     
                 prop_name = 'automation.imdb.last_update.%s' % md5(csv_url)
                 last_update = float(Env.prop(prop_name, default = 0))
    
    From 30256f4f36ae3e2395043111cec84164fc98b53c Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Fri, 25 May 2012 20:45:12 +0200
    Subject: [PATCH 054/207] Give some feedback while deleting movies. fix #314
    
    ---
     couchpotato/core/plugins/movie/static/list.js | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js
    index 800f1711..db4abd47 100644
    --- a/couchpotato/core/plugins/movie/static/list.js
    +++ b/couchpotato/core/plugins/movie/static/list.js
    @@ -278,6 +278,7 @@ var MovieList = new Class({
     			'events': {
     				'click': function(e){
     					(e).preventDefault();
    +					this.set('text', 'Deleting..')
     					Api.request('movie.delete', {
     						'data': {
     							'id': ids.join(','),
    
    From bd8bd14cc8d5be5cb11a215d7ae3618e387ec1aa Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Fri, 25 May 2012 21:19:23 +0200
    Subject: [PATCH 055/207] Add download handler to providers
    
    ---
     couchpotato/core/providers/nzb/mysterbin/main.py | 1 +
     couchpotato/core/providers/nzb/nzbindex/main.py  | 1 +
     2 files changed, 2 insertions(+)
    
    diff --git a/couchpotato/core/providers/nzb/mysterbin/main.py b/couchpotato/core/providers/nzb/mysterbin/main.py
    index 902c37c5..5e619792 100644
    --- a/couchpotato/core/providers/nzb/mysterbin/main.py
    +++ b/couchpotato/core/providers/nzb/mysterbin/main.py
    @@ -81,6 +81,7 @@ class Mysterbin(NZBProvider):
                                 'size': size,
                                 'url': self.urls['download'] % myster_id,
                                 'description': description,
    +                            'download': self.download,
                                 'check_nzb': False,
                             }
     
    diff --git a/couchpotato/core/providers/nzb/nzbindex/main.py b/couchpotato/core/providers/nzb/nzbindex/main.py
    index 43364aca..fd53cdde 100644
    --- a/couchpotato/core/providers/nzb/nzbindex/main.py
    +++ b/couchpotato/core/providers/nzb/nzbindex/main.py
    @@ -71,6 +71,7 @@ class NzbIndex(NZBProvider, RSS):
                             'id': nzbindex_id,
                             'type': 'nzb',
                             'provider': self.getName(),
    +                        'download': self.download,
                             'name': self.getTextElement(nzb, "title"),
                             'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
                             'size': tryInt(enclosure['length']) / 1024 / 1024,
    
    From a3d812ece6f7fb37afe70053b8a4dd720bcd3299 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Fri, 25 May 2012 22:10:52 +0200
    Subject: [PATCH 056/207] Cleanup
    
    ---
     couchpotato/core/notifications/growl/main.py | 5 ++---
     1 file changed, 2 insertions(+), 3 deletions(-)
    
    diff --git a/couchpotato/core/notifications/growl/main.py b/couchpotato/core/notifications/growl/main.py
    index b98888e3..72ba2a56 100644
    --- a/couchpotato/core/notifications/growl/main.py
    +++ b/couchpotato/core/notifications/growl/main.py
    @@ -1,9 +1,8 @@
    -from couchpotato.core.event import fireEvent
    +from couchpotato.core.event import fireEvent, addEvent
     from couchpotato.core.logger import CPLog
     from couchpotato.core.notifications.base import Notification
     from couchpotato.environment import Env
     from gntp import notifier
    -import logging
     import traceback
     
     log = CPLog(__name__)
    @@ -17,7 +16,7 @@ class Growl(Notification):
             super(Growl, self).__init__()
     
             if self.isEnabled():
    -            self.register()
    +            addEvent('app.load', self.register)
     
         def register(self):
             if self.registered: return
    
    From 30ef0e45e0ebdec9b056587c0122b45fad0ede3b Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Fri, 25 May 2012 22:23:18 +0200
    Subject: [PATCH 057/207] Show correct diff. fix #319
    
    ---
     couchpotato/core/_base/updater/static/updater.js | 3 ++-
     1 file changed, 2 insertions(+), 1 deletion(-)
    
    diff --git a/couchpotato/core/_base/updater/static/updater.js b/couchpotato/core/_base/updater/static/updater.js
    index bcbf48a8..54310d7c 100644
    --- a/couchpotato/core/_base/updater/static/updater.js
    +++ b/couchpotato/core/_base/updater/static/updater.js
    @@ -52,7 +52,7 @@ var UpdaterBase = new Class({
     	createMessage: function(data){
     		var self = this;
     
    -		var changelog = 'https://github.com/'+data.repo_name+'/compare/'+data.version.hash+'...'+data.update_version.hash;
    +		var changelog = 'https://github.com/'+data.repo_name+'/compare/'+data.version.hash+'...'+data.branch;
     		if(data.update_version.changelog)
     			changelog = data.update_version.changelog + '#' + data.version.hash+'...'+data.update_version.hash
     
    @@ -85,6 +85,7 @@ var UpdaterBase = new Class({
     					App.checkAvailable.delay(500, App);
     					if(self.message)
     						self.message.destroy();
    +					window.location.reload();
     				}
     			}
     		});
    
    From 7fa06eade472fdc36d3882a0a214287b4c2f871c Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Fri, 25 May 2012 23:40:54 +0200
    Subject: [PATCH 058/207] Select folder, show root
    
    ---
     couchpotato/core/plugins/browser/main.py    | 6 ++++--
     couchpotato/static/scripts/page/settings.js | 2 +-
     2 files changed, 5 insertions(+), 3 deletions(-)
    
    diff --git a/couchpotato/core/plugins/browser/main.py b/couchpotato/core/plugins/browser/main.py
    index 887edc30..90f2673c 100644
    --- a/couchpotato/core/plugins/browser/main.py
    +++ b/couchpotato/core/plugins/browser/main.py
    @@ -62,13 +62,15 @@ class FileBrowser(Plugin):
     
         def view(self):
     
    +        path = getParam('path', '/')
    +
             try:
    -            dirs = self.getDirectories(path = getParam('path', '/'), show_hidden = getParam('show_hidden', True))
    +            dirs = self.getDirectories(path = path, show_hidden = getParam('show_hidden', True))
             except:
                 dirs = []
     
             return jsonified({
    -            'is_root': getParam('path', '/') == '/',
    +            'is_root': path == '/' or not path,
                 'empty': len(dirs) == 0,
                 'dirs': dirs,
             })
    diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js
    index 20ca457e..39ef9465 100644
    --- a/couchpotato/static/scripts/page/settings.js
    +++ b/couchpotato/static/scripts/page/settings.js
    @@ -702,7 +702,7 @@ Option.Directory = new Class({
     		var v = self.input.get('text');
     		var previous_dir = self.getParentDir();
     
    -		if(previous_dir != v && previous_dir.length > 1){
    +		if(previous_dir != v && previous_dir.length >= 1 && !json.is_root){
     			self.back_button.set('data-value', previous_dir)
     			self.back_button.set('html', '« '+self.getCurrentDirname(previous_dir))
     			self.back_button.show()
    
    From f6afd4a0a26d038e2467321005fd01b2e1bb15e3 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Fri, 25 May 2012 23:41:14 +0200
    Subject: [PATCH 059/207] Add movie to list, fix.
    
    ---
     couchpotato/core/plugins/movie/main.py        | 2 +-
     couchpotato/core/plugins/movie/static/list.js | 4 ++--
     2 files changed, 3 insertions(+), 3 deletions(-)
    
    diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py
    index f8415d1e..ce386d14 100644
    --- a/couchpotato/core/plugins/movie/main.py
    +++ b/couchpotato/core/plugins/movie/main.py
    @@ -302,7 +302,7 @@ class MoviePlugin(Plugin):
                     onComplete = self.createOnComplete(m.id)
     
                 fireEventAsync('library.update', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete)
    -            do_search = False
    +            search_after = False
             elif force_readd:
                 # Clean snatched history
                 for release in m.releases:
    diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js
    index db4abd47..208c4e28 100644
    --- a/couchpotato/core/plugins/movie/static/list.js
    +++ b/couchpotato/core/plugins/movie/static/list.js
    @@ -10,7 +10,7 @@ var MovieList = new Class({
     	},
     
     	movies: [],
    -	movies_added: [],
    +	movies_added: {},
     	letters: {},
     	filter: {
     		'startswith': null,
    @@ -106,7 +106,7 @@ var MovieList = new Class({
     		m.fireEvent('injected');
     
     		self.movies.include(m)
    -		self.movies_added.include(movie.id);
    +		self.movies_added[movie.id] = true;
     	},
     
     	createNavigation: function(){
    
    From 0a9c8abcad1d75f97779baaf0cc04db225627bdf Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sat, 26 May 2012 00:04:50 +0200
    Subject: [PATCH 060/207] Move srt files. fix #274
    
    ---
     couchpotato/core/plugins/renamer/main.py | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py
    index 53310c47..89c33e5e 100644
    --- a/couchpotato/core/plugins/renamer/main.py
    +++ b/couchpotato/core/plugins/renamer/main.py
    @@ -211,7 +211,7 @@ class Renamer(Plugin):
                             if file_type is 'subtitle':
     
                                 # rename subtitles with or without language
    -                            #rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
    +                            rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
                                 sub_langs = group['subtitle_language'].get(current_file, [])
     
                                 rename_extras = self.getRenameExtras(
    
    From 5ae29bd2824f25f6894e257d9dce117e46317d26 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sat, 26 May 2012 00:10:39 +0200
    Subject: [PATCH 061/207] Rename id to tmdb_id. fix #287
    
    ---
     couchpotato/core/providers/movie/themoviedb/main.py | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/couchpotato/core/providers/movie/themoviedb/main.py b/couchpotato/core/providers/movie/themoviedb/main.py
    index 387fcac4..b1a1b62b 100644
    --- a/couchpotato/core/providers/movie/themoviedb/main.py
    +++ b/couchpotato/core/providers/movie/themoviedb/main.py
    @@ -151,7 +151,7 @@ class TheMovieDb(MovieProvider):
     
             movie_data = {
                 'via_tmdb': True,
    -            'id': int(movie.get('id', 0)),
    +            'tmdb_id': int(movie.get('id', 0)),
                 'titles': [toUnicode(movie.get('name'))],
                 'original_title': movie.get('original_name'),
                 'images': {
    
    From 2c3e53eb1f341ba93fce20f75c9756a9d7d8206e Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sat, 26 May 2012 10:01:34 +0200
    Subject: [PATCH 062/207] Use CP quality check for newzbin. fix #348
    
    ---
     couchpotato/core/providers/nzb/newzbin/main.py | 3 +--
     1 file changed, 1 insertion(+), 2 deletions(-)
    
    diff --git a/couchpotato/core/providers/nzb/newzbin/main.py b/couchpotato/core/providers/nzb/newzbin/main.py
    index c6de2a29..9239e84f 100644
    --- a/couchpotato/core/providers/nzb/newzbin/main.py
    +++ b/couchpotato/core/providers/nzb/newzbin/main.py
    @@ -61,7 +61,6 @@ class Newzbin(NZBProvider, RSS):
     
             url = "%s?%s" % (self.urls['search'], arguments)
             cache_key = str('newzbin.%s.%s.%s' % (movie['library']['identifier'], str(format_id), str(cat_id)))
    -        single_cat = True
     
             data = self.getCache(cache_key)
             if not data:
    @@ -118,7 +117,7 @@ class Newzbin(NZBProvider, RSS):
     
                         is_correct_movie = fireEvent('searcher.correct_movie',
                                                      nzb = new, movie = movie, quality = quality,
    -                                                 imdb_results = True, single_category = single_cat, single = True)
    +                                                 imdb_results = True, single = True)
                         if is_correct_movie:
                             new['score'] = fireEvent('score.calculate', new, movie, single = True)
                             results.append(new)
    
    From 98c8a47dd0abab808aa9331d26db24e6c49615a3 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sat, 26 May 2012 11:01:55 +0200
    Subject: [PATCH 063/207] IE9 fixes
    
    ---
     couchpotato/core/helpers/request.py             | 11 ++++++++---
     couchpotato/core/plugins/movie/static/search.js | 10 +++++++---
     couchpotato/static/scripts/couchpotato.js       |  4 ++--
     couchpotato/static/style/main.css               |  1 +
     4 files changed, 18 insertions(+), 8 deletions(-)
    
    diff --git a/couchpotato/core/helpers/request.py b/couchpotato/core/helpers/request.py
    index 16a2f73d..07aa18e8 100644
    --- a/couchpotato/core/helpers/request.py
    +++ b/couchpotato/core/helpers/request.py
    @@ -1,7 +1,7 @@
     from couchpotato.core.helpers.encoding import toUnicode
     from couchpotato.core.helpers.variable import natcmp
     from flask.globals import current_app
    -from flask.helpers import json
    +from flask.helpers import json, make_response
     from libs.werkzeug.urls import url_decode
     from urllib import unquote
     import flask
    @@ -72,6 +72,11 @@ def jsonify(mimetype, *args, **kwargs):
     def jsonified(*args, **kwargs):
         callback = getParam('callback_func', None)
         if callback:
    -        return padded_jsonify(callback, *args, **kwargs)
    +        content = padded_jsonify(callback, *args, **kwargs)
         else:
    -        return jsonify('application/json', *args, **kwargs)
    +        content = jsonify('application/json', *args, **kwargs)
    +
    +    response = make_response(content)
    +    response.cache_control.no_cache = True
    +
    +    return response
    diff --git a/couchpotato/core/plugins/movie/static/search.js b/couchpotato/core/plugins/movie/static/search.js
    index 438ba9aa..bfb3b0d4 100644
    --- a/couchpotato/core/plugins/movie/static/search.js
    +++ b/couchpotato/core/plugins/movie/static/search.js
    @@ -221,7 +221,9 @@ Block.Search.Item = new Class({
     				}
     			}).adopt(
     				self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
    -					'src': info.images.poster[0]
    +					'src': info.images.poster[0],
    +					'height': null,
    +					'width': null
     				}) : null,
     				new Element('div.info').adopt(
     					self.title = new Element('h2', {
    @@ -332,8 +334,10 @@ Block.Search.Item = new Class({
     
     			self.options.adopt(
     				new Element('div').adopt(
    -					self.info.images && self.info.images.poster.length > 0 ? new Element('img.thumbnail', {
    -						'src': self.info.images.poster[0]
    +					self.option_thumbnail = self.info.images && self.info.images.poster.length > 0 ? new Element('img.thumbnail', {
    +						'src': self.info.images.poster[0],
    +						'height': null,
    +						'width': null
     					}) : null,
     					self.info.in_wanted ? new Element('span.in_wanted', {
     						'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label
    diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js
    index e2c23b82..c3a63397 100644
    --- a/couchpotato/static/scripts/couchpotato.js
    +++ b/couchpotato/static/scripts/couchpotato.js
    @@ -24,8 +24,8 @@ var CouchPotato = new Class({
     
     		if(window.location.hash)
     			History.handleInitialState();
    -		else
    -			self.openPage(window.location.pathname);
    +		
    +		self.openPage(window.location.pathname);
     
     		History.addEvent('change', self.openPage.bind(self));
     		self.c.addEvent('click:relay(a[href^=/]:not([target]))', self.pushState.bind(self));
    diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css
    index d580368c..884d8724 100644
    --- a/couchpotato/static/style/main.css
    +++ b/couchpotato/static/style/main.css
    @@ -437,6 +437,7 @@ body > .spinner, .mask{
     	border-radius:3px;
     	border: 1px solid #252930;
     	box-shadow: inset 0 1px 0px rgba(255,255,255,0.20), 0 0 3px rgba(0,0,0, 0.2);
    +	background: rgb(55,62,74);
     	background-image: -webkit-gradient(
     	    linear,
     	    left bottom,
    
    From fcd13fcb85838e5c0f6b1bbef56bef3e36ec3494 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sat, 26 May 2012 22:55:11 +0200
    Subject: [PATCH 064/207] Nonblocking update listener
    
    ---
     couchpotato/api.py                            | 32 +++++++
     couchpotato/core/_base/_core/main.py          |  1 -
     couchpotato/core/notifications/core/main.py   | 87 ++++++++++++++++---
     .../notifications/core/static/notification.js | 59 +++++++++----
     couchpotato/core/plugins/library/main.py      |  3 -
     couchpotato/core/plugins/movie/main.py        |  1 +
     .../core/plugins/movie/static/movie.js        | 13 ++-
     couchpotato/environment.py                    |  1 -
     couchpotato/runner.py                         | 20 +++--
     couchpotato/static/scripts/page/wanted.js     |  1 -
     10 files changed, 170 insertions(+), 48 deletions(-)
    
    diff --git a/couchpotato/api.py b/couchpotato/api.py
    index b1dee1b3..a0fd69b7 100644
    --- a/couchpotato/api.py
    +++ b/couchpotato/api.py
    @@ -1,10 +1,34 @@
     from flask.blueprints import Blueprint
     from flask.helpers import url_for
    +from tornado.ioloop import IOLoop
    +from tornado.web import RequestHandler, asynchronous
     from werkzeug.utils import redirect
     
     api = Blueprint('api', __name__)
     api_docs = {}
     api_docs_missing = []
    +api_nonblock = {}
    +
    +
    +class NonBlockHandler(RequestHandler):
    +    stoppers = []
    +
    +    @asynchronous
    +    def get(self, route):
    +        start, stop = api_nonblock[route]
    +        self.stoppers.append(stop)
    +
    +        start(self.on_new_messages, last_id = self.get_argument("last_id", None))
    +
    +    def on_new_messages(self, response):
    +        if self.request.connection.stream.closed():
    +            return
    +        self.finish(response)
    +
    +    def on_connection_close(self):
    +        for stop in self.stoppers:
    +            stop(self.on_new_messages)
    +
     
     def addApiView(route, func, static = False, docs = None, **kwargs):
         api.add_url_rule(route + ('' if static else '/'), endpoint = route.replace('.', '::') if route else 'index', view_func = func, **kwargs)
    @@ -13,6 +37,14 @@ def addApiView(route, func, static = False, docs = None, **kwargs):
         else:
             api_docs_missing.append(route)
     
    +def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
    +    api_nonblock[route] = func_tuple
    +
    +    if docs:
    +        api_docs[route[4:] if route[0:4] == 'api.' else route] = docs
    +    else:
    +        api_docs_missing.append(route)
    +
     """ Api view """
     def index():
         index_url = url_for('web.index')
    diff --git a/couchpotato/core/_base/_core/main.py b/couchpotato/core/_base/_core/main.py
    index c1a24c3d..23deedfa 100644
    --- a/couchpotato/core/_base/_core/main.py
    +++ b/couchpotato/core/_base/_core/main.py
    @@ -114,7 +114,6 @@ class Core(Plugin):
             log.debug('Save to shutdown/restart')
     
             try:
    -            Env.get('httpserver').stop()
                 IOLoop.instance().stop()
             except RuntimeError:
                 pass
    diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py
    index 5419049e..6a299c48 100644
    --- a/couchpotato/core/notifications/core/main.py
    +++ b/couchpotato/core/notifications/core/main.py
    @@ -1,5 +1,5 @@
     from couchpotato import get_session
    -from couchpotato.api import addApiView
    +from couchpotato.api import addApiView, addNonBlockApiView
     from couchpotato.core.event import addEvent
     from couchpotato.core.helpers.encoding import toUnicode
     from couchpotato.core.helpers.request import jsonified, getParam
    @@ -8,14 +8,19 @@ from couchpotato.core.logger import CPLog
     from couchpotato.core.notifications.base import Notification
     from couchpotato.core.settings.model import Notification as Notif
     from sqlalchemy.sql.expression import or_
    +import threading
     import time
    +import uuid
     
     log = CPLog(__name__)
     
     
     class CoreNotifier(Notification):
     
    +    m_lock = threading.RLock()
         messages = []
    +    listeners = []
    +
         listen_to = [
             'movie.downloaded', 'movie.snatched',
             'updater.available', 'updater.updated',
    @@ -46,8 +51,17 @@ class CoreNotifier(Notification):
     }"""}
             })
     
    +        addNonBlockApiView('notification.listener', (self.addListener, self.removeListener))
             addApiView('notification.listener', self.listener)
     
    +
    +        def test():
    +            while True:
    +                time.sleep(1)
    +
    +        addEvent('app.load', test)
    +
    +
         def markAsRead(self):
             ids = [x.strip() for x in getParam('ids').split(',')]
     
    @@ -107,25 +121,79 @@ class CoreNotifier(Notification):
             ndict = n.to_dict()
             ndict['type'] = 'notification'
             ndict['time'] = time.time()
    -        self.messages.append(ndict)
    +
    +        self.frontend(type = listener, data = data)
     
             #db.close()
             return True
     
         def frontend(self, type = 'notification', data = {}):
    -        self.messages.append({
    +
    +        self.m_lock.acquire()
    +        message = {
    +            'id': str(uuid.uuid4()),
                 'time': time.time(),
                 'type': type,
                 'data': data,
    -        })
    +        }
    +        self.messages.append(message)
    +
    +        while True and not self.shuttingDown():
    +            try:
    +                listener, last_id = self.listeners.pop()
    +                listener({
    +                    'success': True,
    +                    'result': [message],
    +                })
    +            except:
    +                break
    +
    +        self.m_lock.release()
    +
    +        self.cleanMessages()
    +
    +    def addListener(self, callback, last_id = None):
    +
    +        if last_id:
    +            messages = self.getMessages(last_id)
    +            if len(messages) > 0:
    +                return callback({
    +                    'success': True,
    +                    'result': messages,
    +                })
    +
    +        self.listeners.append((callback, last_id))
    +
    +    def removeListener(self, callback):
    +        for list_tuple in self.listeners:
    +            try:
    +                listener, last_id = list_tuple
    +                if listener == callback:
    +                    self.listeners.remove(list_tuple)
    +            except:
    +                pass
    +
    +    def cleanMessages(self):
    +
    +        for message in self.messages:
    +            if message['time'] < (time.time() - 15):
    +                self.messages.remove(message)
    +
    +    def getMessages(self, last_id):
    +        self.m_lock.acquire()
    +        recent = []
    +        index = 0
    +        for i in xrange(len(self.messages)):
    +            index = len(self.messages) - i - 1
    +            if self.messages[index]["id"] == last_id: break
    +            recent = self.messages[index + 1:]
    +
    +        self.m_lock.release()
    +        return recent or []
     
         def listener(self):
     
             messages = []
    -        for message in self.messages:
    -            #delete message older then 15s
    -            if message['time'] > (time.time() - 15):
    -                messages.append(message)
     
             # Get unread
             if getParam('init'):
    @@ -139,9 +207,6 @@ class CoreNotifier(Notification):
                     ndict['type'] = 'notification'
                     messages.append(ndict)
     
    -            #db.close()
    -
    -        self.messages = []
             return jsonified({
                 'success': True,
                 'result': messages,
    diff --git a/couchpotato/core/notifications/core/static/notification.js b/couchpotato/core/notifications/core/static/notification.js
    index e35a394c..350c65f6 100644
    --- a/couchpotato/core/notifications/core/static/notification.js
    +++ b/couchpotato/core/notifications/core/static/notification.js
    @@ -8,8 +8,7 @@ var NotificationBase = new Class({
     		self.setOptions(options);
     
     		// Listener
    -		App.addEvent('load', self.startInterval.bind(self));
    -		App.addEvent('unload', self.stopTimer.bind(self));
    +		App.addEvent('unload', self.stopPoll.bind(self));
     		App.addEvent('notification', self.notify.bind(self));
     
     		// Add test buttons to settings page
    @@ -30,7 +29,11 @@ var NotificationBase = new Class({
     				'href': App.createUrl('notifications'),
     				'text': 'Show older notifications'
     			})); */
    -		})
    +		});
    +
    +		window.addEvent('load', function(){
    +			self.startInterval()
    +		});
     
     	},
     
    @@ -85,35 +88,55 @@ var NotificationBase = new Class({
     
     	startInterval: function(){
     		var self = this;
    +		
    +		if(self.stopped) return;
     
    -		self.request = Api.request('notification.listener', {
    -			'initialDelay': 100,
    -    		'delay': 1500,
    +		Api.request('notification.listener', {
         		'data': {'init':true},
         		'onSuccess': self.processData.bind(self)
    -		})
    -
    -		self.request.startTimer()
    +		}).send()
     
     	},
     
    -	startTimer: function(){
    -		if(this.request)
    -			this.request.startTimer()
    +	startPoll: function(){
    +		var self = this;
    +
    +		if(self.stopped || (self.request && self.request.isRunning()))
    +			return;
    +
    +		self.request = Api.request('nonblock/notification.listener', {
    +    		'onSuccess': self.processData.bind(self),
    +    		'data': {
    +    			'last_id': self.last_id
    +    		},
    +    		'onFailure': function(){
    +    			self.startPoll.delay(2000, self)
    +    		}
    +		}).send()
    +
     	},
     
    -	stopTimer: function(){
    +	stopPoll: function(){
     		if(this.request)
    -			this.request.stopTimer()
    +			this.request.cancel()
    +		this.stopped = true;
     	},
     
     	processData: function(json){
     		var self = this;
     
    -		self.request.options.data = {}
    -		Array.each(json.result, function(result){
    -			App.fireEvent(result.type, result)
    -		})
    +
    +		// Process data
    +		if(json){
    +			Array.each(json.result, function(result){
    +				App.fireEvent(result.type, result)
    +			})
    +
    +			self.last_id = json.result.getLast().id
    +		}
    +
    +		// Restart poll
    +		self.startPoll()
     	},
     
     	addTestButtons: function(){
    diff --git a/couchpotato/core/plugins/library/main.py b/couchpotato/core/plugins/library/main.py
    index a7960d7c..741f48bd 100644
    --- a/couchpotato/core/plugins/library/main.py
    +++ b/couchpotato/core/plugins/library/main.py
    @@ -127,9 +127,6 @@ class LibraryPlugin(Plugin):
     
                 library_dict = library.to_dict(self.default_dict)
     
    -        fireEvent('notify.frontend', type = 'library.update.%s' % identifier, data = library_dict)
    -
    -        #db.close()
             return library_dict
     
         def updateReleaseDate(self, identifier):
    diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py
    index ce386d14..83a20247 100644
    --- a/couchpotato/core/plugins/movie/main.py
    +++ b/couchpotato/core/plugins/movie/main.py
    @@ -239,6 +239,7 @@ class MoviePlugin(Plugin):
             db = get_session()
     
             for id in getParam('id').split(','):
    +            fireEvent('notify.frontend', type = 'movie.busy.%s' % id, data = True)
                 movie = db.query(Movie).filter_by(id = id).first()
     
                 # Get current selected title
    diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js
    index c93a98ea..9877b12f 100644
    --- a/couchpotato/core/plugins/movie/static/movie.js
    +++ b/couchpotato/core/plugins/movie/static/movie.js
    @@ -17,14 +17,16 @@ var Movie = new Class({
     		self.parent(self, options);
     
     		App.addEvent('movie.update.'+data.id, self.update.bind(self));
    -		App.addEvent('searcher.started.'+data.id, self.searching.bind(self));
    -		App.addEvent('searcher.ended.'+data.id, self.searching.bind(self));
    +		App.addEvent('movie.busy.'+data.id, function(notification){
    +			if(notification.data)
    +				self.busy(true)
    +		});
     	},
     
    -	searching: function(notification){
    +	busy: function(set_busy){
     		var self = this;
     
    -		if(notification && notification.type.indexOf('ended') > -1){
    +		if(!set_busy){
     			if(self.spinner){
     				self.mask.fade('out');
     				setTimeout(function(){
    @@ -72,8 +74,11 @@ var Movie = new Class({
     
     		self.data = notification.data;
     		self.container.destroy();
    +
     		self.profile = Quality.getProfile(self.data.profile_id) || {};
     		self.create();
    +		
    +		self.busy(false);
     	},
     
     	create: function(){
    diff --git a/couchpotato/environment.py b/couchpotato/environment.py
    index a6f3ebb3..e804170d 100644
    --- a/couchpotato/environment.py
    +++ b/couchpotato/environment.py
    @@ -23,7 +23,6 @@ class Env(object):
         _deamonize = False
         _desktop = None
         _session = None
    -    _httpserver = None
     
         ''' Data paths and directories '''
         _app_dir = ""
    diff --git a/couchpotato/runner.py b/couchpotato/runner.py
    index 280ef758..35a3bf96 100644
    --- a/couchpotato/runner.py
    +++ b/couchpotato/runner.py
    @@ -1,13 +1,13 @@
     from argparse import ArgumentParser
     from couchpotato import web
    -from couchpotato.api import api
    +from couchpotato.api import api, NonBlockHandler
     from couchpotato.core.event import fireEventAsync, fireEvent
     from couchpotato.core.helpers.variable import getDataDir, tryInt
     from logging import handlers
     from tornado import autoreload
     from tornado.httpserver import HTTPServer
     from tornado.ioloop import IOLoop
    -from tornado.web import RequestHandler
    +from tornado.web import RequestHandler, Application, FallbackHandler
     from tornado.wsgi import WSGIContainer
     from werkzeug.contrib.cache import FileSystemCache
     import locale
    @@ -227,20 +227,22 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
         # Go go go!
         web_container = WSGIContainer(app)
         web_container._log = _log
    -    http_server = HTTPServer(web_container, no_keep_alive = True)
    -    Env.set('httpserver', http_server)
         loop = IOLoop.instance()
     
    +    application = Application([
    +        (r'%s/api/%s/nonblock/(.*)/' % (url_base, api_key), NonBlockHandler),
    +        (r'.*', FallbackHandler, dict(fallback = web_container)),
    +    ],
    +        log_function = lambda x : None,
    +        debug = config['use_reloader']
    +    )
    +
         try_restart = True
         restart_tries = 5
     
         while try_restart:
             try:
    -            http_server.listen(config['port'], config['host'])
    -
    -            if config['use_reloader']:
    -                autoreload.start(loop)
    -
    +            application.listen(config['port'], config['host'], no_keep_alive = True)
                 loop.start()
             except Exception, e:
                 try:
    diff --git a/couchpotato/static/scripts/page/wanted.js b/couchpotato/static/scripts/page/wanted.js
    index 2ec49e2b..26d04666 100644
    --- a/couchpotato/static/scripts/page/wanted.js
    +++ b/couchpotato/static/scripts/page/wanted.js
    @@ -137,7 +137,6 @@ window.addEvent('domready', function(){
     				var self = this;
     				(e).preventDefault();
     
    -				self.movie.searching();
     				Api.request('movie.refresh', {
     					'data': {
     						'id': self.movie.get('id')
    
    From b65923b36d40d0ca116abd20ede169f464dc825a Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sat, 26 May 2012 23:11:58 +0200
    Subject: [PATCH 065/207] Leftover print. fix #221
    
    ---
     couchpotato/core/plugins/renamer/main.py | 1 -
     1 file changed, 1 deletion(-)
    
    diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py
    index 89c33e5e..d33ba89f 100644
    --- a/couchpotato/core/plugins/renamer/main.py
    +++ b/couchpotato/core/plugins/renamer/main.py
    @@ -314,7 +314,6 @@ class Renamer(Plugin):
     
                                     break
                             elif release.status_id is snatched_status.get('id'):
    -                            print release.quality.label, group['meta_data']['quality']['label']
                                 if release.quality.id is group['meta_data']['quality']['id']:
                                     log.debug('Marking release as downloaded')
                                     release.status_id = downloaded_status.get('id')
    
    From ed1ae6e81a6d0d7b95c51f9ec8e95463a85978d6 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sat, 26 May 2012 23:12:48 +0200
    Subject: [PATCH 066/207] Aquire lock for long poll
    
    ---
     couchpotato/core/notifications/core/main.py | 4 ++++
     1 file changed, 4 insertions(+)
    
    diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py
    index 6a299c48..196a9ac1 100644
    --- a/couchpotato/core/notifications/core/main.py
    +++ b/couchpotato/core/notifications/core/main.py
    @@ -165,6 +165,7 @@ class CoreNotifier(Notification):
             self.listeners.append((callback, last_id))
     
         def removeListener(self, callback):
    +        self.m_lock.aquire()
             for list_tuple in self.listeners:
                 try:
                     listener, last_id = list_tuple
    @@ -172,12 +173,15 @@ class CoreNotifier(Notification):
                         self.listeners.remove(list_tuple)
                 except:
                     pass
    +        self.m_lock.release()
     
         def cleanMessages(self):
     
    +        self.m_lock.aquire()
             for message in self.messages:
                 if message['time'] < (time.time() - 15):
                     self.messages.remove(message)
    +        self.m_lock.release()
     
         def getMessages(self, last_id):
             self.m_lock.acquire()
    
    From b4d98878588990721f6c4627656eb226bb735804 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sat, 26 May 2012 23:15:01 +0200
    Subject: [PATCH 067/207] Typo
    
    ---
     couchpotato/core/notifications/core/main.py | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py
    index 196a9ac1..b5f51eae 100644
    --- a/couchpotato/core/notifications/core/main.py
    +++ b/couchpotato/core/notifications/core/main.py
    @@ -165,7 +165,7 @@ class CoreNotifier(Notification):
             self.listeners.append((callback, last_id))
     
         def removeListener(self, callback):
    -        self.m_lock.aquire()
    +        self.m_lock.acquire()
             for list_tuple in self.listeners:
                 try:
                     listener, last_id = list_tuple
    @@ -177,7 +177,7 @@ class CoreNotifier(Notification):
     
         def cleanMessages(self):
     
    -        self.m_lock.aquire()
    +        self.m_lock.acquire()
             for message in self.messages:
                 if message['time'] < (time.time() - 15):
                     self.messages.remove(message)
    
    From ab7989773ad28902c09e188a9cf47bbd72c53de8 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 27 May 2012 12:47:30 +0200
    Subject: [PATCH 068/207] Add truefrench to ignored words
    
    ---
     couchpotato/core/plugins/searcher/__init__.py | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/couchpotato/core/plugins/searcher/__init__.py b/couchpotato/core/plugins/searcher/__init__.py
    index a3f88555..7ecd29b7 100644
    --- a/couchpotato/core/plugins/searcher/__init__.py
    +++ b/couchpotato/core/plugins/searcher/__init__.py
    @@ -29,7 +29,7 @@ config = [{
                     {
                         'name': 'ignored_words',
                         'label': 'Ignored words',
    -                    'default': 'german, dutch, french, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub',
    +                    'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub',
                     },
                 ],
             }, {
    
    From a378778d10dfd9847e66c4558d45ce891fd42f71 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 27 May 2012 12:50:40 +0200
    Subject: [PATCH 069/207] Group files based on foldername. fix #352
    
    ---
     couchpotato/core/plugins/scanner/main.py | 44 ++++++++++++++++++++----
     1 file changed, 38 insertions(+), 6 deletions(-)
    
    diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py
    index bbf25239..d6057229 100644
    --- a/couchpotato/core/plugins/scanner/main.py
    +++ b/couchpotato/core/plugins/scanner/main.py
    @@ -237,19 +237,51 @@ class Scanner(Plugin):
     
     
             # Group the files based on the identifier
    -        for identifier, group in movie_files.iteritems():
    +        delete_identifiers = []
    +        for identifier, found_files in self.path_identifiers.iteritems():
                 log.debug('Grouping files on identifier: %s' % identifier)
     
    -            found_files = set(self.path_identifiers.get(identifier, []))
    -            group['unsorted_files'].extend(found_files)
    +            group = movie_files.get(identifier)
    +            if group:
    +                group['unsorted_files'].extend(found_files)
    +                delete_identifiers.append(identifier)
     
    -            # Remove the found files from the leftover stack
    -            leftovers = leftovers - found_files
    +                # Remove the found files from the leftover stack
    +                leftovers = leftovers - set(found_files)
     
                 # Break if CP wants to shut down
                 if self.shuttingDown():
                     break
     
    +        # Cleaning up used
    +        for identifier in delete_identifiers:
    +            del self.path_identifiers[identifier]
    +        del delete_identifiers
    +
    +        # Group based on folder
    +        delete_identifiers = []
    +        for identifier, found_files in self.path_identifiers.iteritems():
    +            log.debug('Grouping files on foldername: %s' % identifier)
    +
    +            for ff in found_files:
    +                new_identifier = self.createStringIdentifier(os.path.dirname(ff), folder)
    +
    +                group = movie_files.get(new_identifier)
    +                if group:
    +                    group['unsorted_files'].extend([ff])
    +                    delete_identifiers.append(identifier)
    +
    +                    # Remove the found files from the leftover stack
    +                    leftovers = leftovers - set([ff])
    +
    +            # Break if CP wants to shut down
    +            if self.shuttingDown():
    +                break
    +
    +        # Cleaning up used
    +        for identifier in delete_identifiers:
    +            del self.path_identifiers[identifier]
    +        del delete_identifiers
     
             # Determine file types
             processed_movies = {}
    @@ -647,7 +679,7 @@ class Scanner(Plugin):
             identifier = self.removeCPTag(identifier)
     
             # groups, release tags, scenename cleaner, regex isn't correct
    -        identifier = re.sub(self.clean, '::', simplifyString(identifier))
    +        identifier = re.sub(self.clean, '::', simplifyString(identifier)).strip(':')
     
             # Year
             year = self.findYear(identifier)
    
    From 52dd73dc88b455de26bed69b7979cdc9569072e7 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 27 May 2012 12:54:20 +0200
    Subject: [PATCH 070/207] Leftover debug code
    
    ---
     couchpotato/core/notifications/core/main.py | 7 -------
     1 file changed, 7 deletions(-)
    
    diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py
    index b5f51eae..162e8653 100644
    --- a/couchpotato/core/notifications/core/main.py
    +++ b/couchpotato/core/notifications/core/main.py
    @@ -55,13 +55,6 @@ class CoreNotifier(Notification):
             addApiView('notification.listener', self.listener)
     
     
    -        def test():
    -            while True:
    -                time.sleep(1)
    -
    -        addEvent('app.load', test)
    -
    -
         def markAsRead(self):
             ids = [x.strip() for x in getParam('ids').split(',')]
     
    
    From ca7ae89c855a55bc2db7b7ea0cea9bf20dd3f5d9 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 27 May 2012 22:28:11 +0200
    Subject: [PATCH 071/207] Imdb watchlist not working. fix #356
    
    ---
     couchpotato/core/providers/automation/imdb/main.py | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/couchpotato/core/providers/automation/imdb/main.py b/couchpotato/core/providers/automation/imdb/main.py
    index 1f9c62c5..7a09053c 100644
    --- a/couchpotato/core/providers/automation/imdb/main.py
    +++ b/couchpotato/core/providers/automation/imdb/main.py
    @@ -30,7 +30,7 @@ class IMDB(Automation):
                 index += 1
                 if not enablers[index]:
                     continue
    -            elif 'author_id=' in csv_url:
    +            elif 'author_id=' not in csv_url:
                     log.error('This isn\'t the correct url.: %s' % csv_url)
                     continue
     
    
    From ac96b33611e52d81b89dd6a5a3db88b7b0ea1682 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 27 May 2012 22:39:05 +0200
    Subject: [PATCH 072/207] Check title when movie exists only
    
    thanks ipsec
    ---
     couchpotato/core/plugins/movie/main.py | 11 ++++++-----
     1 file changed, 6 insertions(+), 5 deletions(-)
    
    diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py
    index 83a20247..66609303 100644
    --- a/couchpotato/core/plugins/movie/main.py
    +++ b/couchpotato/core/plugins/movie/main.py
    @@ -242,12 +242,13 @@ class MoviePlugin(Plugin):
                 fireEvent('notify.frontend', type = 'movie.busy.%s' % id, data = True)
                 movie = db.query(Movie).filter_by(id = id).first()
     
    -            # Get current selected title
    -            default_title = ''
    -            for title in movie.library.titles:
    -                if title.default: default_title = title.title
    -
                 if movie:
    +
    +                # Get current selected title
    +                default_title = ''
    +                for title in movie.library.titles:
    +                    if title.default: default_title = title.title
    +
                     fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(id))
     
     
    
    From a8095d5d42eaa13f4662e229fbfc8e7213749f60 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 27 May 2012 22:39:23 +0200
    Subject: [PATCH 073/207] variable not assigned
    
    thanks ipsec
    ---
     couchpotato/core/plugins/movie/main.py | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py
    index 66609303..bcd5d63e 100644
    --- a/couchpotato/core/plugins/movie/main.py
    +++ b/couchpotato/core/plugins/movie/main.py
    @@ -290,6 +290,7 @@ class MoviePlugin(Plugin):
             db = get_session()
             m = db.query(Movie).filter_by(library_id = library.get('id')).first()
             added = True
    +        do_search = False
             if not m:
                 m = Movie(
                     library_id = library.get('id'),
    
    From 5a3919cb087f2ef38673d72c39d4971d57774c6d Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 27 May 2012 23:28:59 +0200
    Subject: [PATCH 074/207] Better locking for longpoll
    
    ---
     couchpotato/api.py                             | 15 ++++++++++-----
     couchpotato/core/notifications/core/main.py    | 18 ++++++++++--------
     .../notifications/core/static/notification.js  | 10 +++++++---
     3 files changed, 27 insertions(+), 16 deletions(-)
    
    diff --git a/couchpotato/api.py b/couchpotato/api.py
    index a0fd69b7..15ef2b4c 100644
    --- a/couchpotato/api.py
    +++ b/couchpotato/api.py
    @@ -15,19 +15,24 @@ class NonBlockHandler(RequestHandler):
     
         @asynchronous
         def get(self, route):
    +        cls = NonBlockHandler
             start, stop = api_nonblock[route]
    -        self.stoppers.append(stop)
    +        cls.stoppers.append(stop)
     
    -        start(self.on_new_messages, last_id = self.get_argument("last_id", None))
    +        start(self.onNewMessage, last_id = self.get_argument("last_id", None))
     
    -    def on_new_messages(self, response):
    +    def onNewMessage(self, response):
             if self.request.connection.stream.closed():
                 return
             self.finish(response)
     
         def on_connection_close(self):
    -        for stop in self.stoppers:
    -            stop(self.on_new_messages)
    +        cls = NonBlockHandler
    +
    +        for stop in cls.stoppers:
    +            stop(self.onNewMessage)
    +
    +        cls.stoppers = []
     
     
     def addApiView(route, func, static = False, docs = None, **kwargs):
    diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py
    index 162e8653..d9789488 100644
    --- a/couchpotato/core/notifications/core/main.py
    +++ b/couchpotato/core/notifications/core/main.py
    @@ -17,7 +17,7 @@ log = CPLog(__name__)
     
     class CoreNotifier(Notification):
     
    -    m_lock = threading.RLock()
    +    m_lock = threading.Lock()
         messages = []
         listeners = []
     
    @@ -124,14 +124,14 @@ class CoreNotifier(Notification):
     
             self.m_lock.acquire()
             message = {
    -            'id': str(uuid.uuid4()),
    +            'message_id': str(uuid.uuid4()),
                 'time': time.time(),
                 'type': type,
                 'data': data,
             }
             self.messages.append(message)
     
    -        while True and not self.shuttingDown():
    +        while len(self.listeners) > 0 and not self.shuttingDown():
                 try:
                     listener, last_id = self.listeners.pop()
                     listener({
    @@ -142,7 +142,6 @@ class CoreNotifier(Notification):
                     break
     
             self.m_lock.release()
    -
             self.cleanMessages()
     
         def addListener(self, callback, last_id = None):
    @@ -157,8 +156,9 @@ class CoreNotifier(Notification):
     
             self.listeners.append((callback, last_id))
     
    +
         def removeListener(self, callback):
    -        self.m_lock.acquire()
    +
             for list_tuple in self.listeners:
                 try:
                     listener, last_id = list_tuple
    @@ -166,26 +166,28 @@ class CoreNotifier(Notification):
                         self.listeners.remove(list_tuple)
                 except:
                     pass
    -        self.m_lock.release()
     
         def cleanMessages(self):
    -
             self.m_lock.acquire()
    +
             for message in self.messages:
                 if message['time'] < (time.time() - 15):
                     self.messages.remove(message)
    +
             self.m_lock.release()
     
         def getMessages(self, last_id):
             self.m_lock.acquire()
    +
             recent = []
             index = 0
             for i in xrange(len(self.messages)):
                 index = len(self.messages) - i - 1
    -            if self.messages[index]["id"] == last_id: break
    +            if self.messages[index]["message_id"] == last_id: break
                 recent = self.messages[index + 1:]
     
             self.m_lock.release()
    +
             return recent or []
     
         def listener(self):
    diff --git a/couchpotato/core/notifications/core/static/notification.js b/couchpotato/core/notifications/core/static/notification.js
    index 350c65f6..4cf71e00 100644
    --- a/couchpotato/core/notifications/core/static/notification.js
    +++ b/couchpotato/core/notifications/core/static/notification.js
    @@ -9,6 +9,7 @@ var NotificationBase = new Class({
     
     		// Listener
     		App.addEvent('unload', self.stopPoll.bind(self));
    +		App.addEvent('reload', self.startInterval.bind(self, [true]));
     		App.addEvent('notification', self.notify.bind(self));
     
     		// Add test buttons to settings page
    @@ -86,10 +87,13 @@ var NotificationBase = new Class({
     
     	},
     
    -	startInterval: function(){
    +	startInterval: function(force){
     		var self = this;
     		
    -		if(self.stopped) return;
    +		if(self.stopped && !force){
    +			self.stopped = false;
    +			return;
    +		}
     
     		Api.request('notification.listener', {
         		'data': {'init':true},
    @@ -132,7 +136,7 @@ var NotificationBase = new Class({
     				App.fireEvent(result.type, result)
     			})
     
    -			self.last_id = json.result.getLast().id
    +			self.last_id = json.result.getLast().message_id
     		}
     
     		// Restart poll
    
    From 767ad46d90ccc9a70dce9c5f149420a524b88435 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 27 May 2012 23:30:29 +0200
    Subject: [PATCH 075/207] Reload after update
    
    ---
     couchpotato/core/_base/updater/static/updater.js |  5 +++--
     couchpotato/static/scripts/couchpotato.js        | 14 +++++---------
     2 files changed, 8 insertions(+), 11 deletions(-)
    
    diff --git a/couchpotato/core/_base/updater/static/updater.js b/couchpotato/core/_base/updater/static/updater.js
    index 54310d7c..a2660086 100644
    --- a/couchpotato/core/_base/updater/static/updater.js
    +++ b/couchpotato/core/_base/updater/static/updater.js
    @@ -82,10 +82,11 @@ var UpdaterBase = new Class({
     			'onComplete': function(json){
     				if(json.success){
     					App.restart('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
    -					App.checkAvailable.delay(500, App);
    +					App.checkAvailable.delay(500, App, [1000, function(){
    +						window.location.reload();
    +					}]);
     					if(self.message)
     						self.message.destroy();
    -					window.location.reload();
     				}
     			}
     		});
    diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js
    index c3a63397..21cd315b 100644
    --- a/couchpotato/static/scripts/couchpotato.js
    +++ b/couchpotato/static/scripts/couchpotato.js
    @@ -220,18 +220,21 @@ var CouchPotato = new Class({
     		self.checkAvailable(3000);
     	},
     
    -	checkAvailable: function(delay){
    +	checkAvailable: function(delay, onAvailable){
     		var self = this;
     
     		(function(){
     
     			Api.request('app.available', {
     				'onFailure': function(){
    -					self.checkAvailable.delay(1000, self);
    +					self.checkAvailable.delay(1000, self, [delay, onAvailable]);
     					self.fireEvent('unload');
     				},
     				'onSuccess': function(){
    +					if(onAvailable)
    +						onAvailable()
     					self.unBlockPage();
    +					self.fireEvent('reload');
     				}
     			});
     
    @@ -263,13 +266,6 @@ var CouchPotato = new Class({
     
     	createUrl: function(action, params){
     		return this.options.base_url + (action ? action+'/' : '') + (params ? '?'+Object.toQueryString(params) : '')
    -	},
    -
    -	notify: function(options){
    -		return this.growl.notify({
    -            title: "this scrolls away",
    -            text: "test - hello there. mouseover to pause away action"
    -        });
     	}
     
     });
    
    From 9575dff83870a4b2ca6dba565d3beb20ef1ae122 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Mon, 28 May 2012 00:26:49 +0200
    Subject: [PATCH 076/207] One message missing in return
    
    ---
     couchpotato/core/notifications/core/main.py | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py
    index d9789488..e2c7c6a0 100644
    --- a/couchpotato/core/notifications/core/main.py
    +++ b/couchpotato/core/notifications/core/main.py
    @@ -184,7 +184,7 @@ class CoreNotifier(Notification):
             for i in xrange(len(self.messages)):
                 index = len(self.messages) - i - 1
                 if self.messages[index]["message_id"] == last_id: break
    -            recent = self.messages[index + 1:]
    +            recent = self.messages[index:]
     
             self.m_lock.release()
     
    
    From 57b8f0e9b73bcf87d90c8239c0325e0004d2e806 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Mon, 28 May 2012 12:21:07 +0200
    Subject: [PATCH 077/207] Return movie_id after rename/download. fix #362
    
    ---
     couchpotato/core/plugins/scanner/main.py | 8 +++++++-
     1 file changed, 7 insertions(+), 1 deletion(-)
    
    diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py
    index d6057229..6fa3f7d5 100644
    --- a/couchpotato/core/plugins/scanner/main.py
    +++ b/couchpotato/core/plugins/scanner/main.py
    @@ -4,7 +4,7 @@ from couchpotato.core.helpers.encoding import toUnicode, simplifyString
     from couchpotato.core.helpers.variable import getExt, getImdb, tryInt
     from couchpotato.core.logger import CPLog
     from couchpotato.core.plugins.base import Plugin
    -from couchpotato.core.settings.model import File
    +from couchpotato.core.settings.model import File, Movie
     from couchpotato.environment import Env
     from enzyme.exceptions import NoParserError, ParseError
     from guessit import guess_movie_info
    @@ -161,6 +161,8 @@ class Scanner(Plugin):
                     except:
                         log.error('Failed getting files from %s: %s' % (folder, traceback.format_exc()))
     
    +        db = get_session()
    +
             for file_path in files:
     
                 if not os.path.exists(file_path):
    @@ -359,6 +361,10 @@ class Scanner(Plugin):
                 group['library'] = self.determineMovie(group)
                 if not group['library']:
                     log.error('Unable to determine movie: %s' % group['identifiers'])
    +            else:
    +                movie = db.query(Movie).filter_by(library_id = group['library']['id']).first()
    +                group['movie_id'] = None if not movie else movie.id
    +
     
                 processed_movies[identifier] = group
     
    
    From b2ccca9299c57733f672ade3cd1a6bd0abe43863 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Mon, 28 May 2012 14:05:27 +0200
    Subject: [PATCH 078/207] Updated freebsd script
    
    ---
     init/freebsd | 24 +++++++++---------------
     1 file changed, 9 insertions(+), 15 deletions(-)
    
    diff --git a/init/freebsd b/init/freebsd
    index 770e22cd..e3cf408e 100644
    --- a/init/freebsd
    +++ b/init/freebsd
    @@ -31,24 +31,14 @@ load_rc_config ${name}
     : ${couchpotato_user:="_sabnzbd"}
     : ${couchpotato_dir:="/usr/local/couchpotato"}
     : ${couchpotato_chdir:="${couchpotato_dir}"}
    -: ${couchpotato_pid:="${couchpotato_dir}/couchpotato.pid"}
    -
    -WGET="/usr/local/bin/wget" # You need wget for this script to safely shutdown CouchPotato.
    -HOST="127.0.0.1" # Set CouchPotato address here.
    -PORT="8081" # Set CouchPotato port here.
    -CPAPI="" # Set CouchPotato API key
    +: ${couchpotato_pid:="/var/run/couchpotato.pid"}
     
    +pidfile="${couchpotato_pid}"
     status_cmd="${name}_status"
     stop_cmd="${name}_stop"
     
     command="/usr/sbin/daemon"
    -command_args="-f -p ${couchpotato_pid} python ${couchpotato_dir}/couchpotato.py ${couchpotato_flags}"
    -
    -# Check for wget and refuse to start without it.
    -if [ ! -x "${WGET}" ]; then
    -  warn "couchpotato not started: You need wget to safely shut down CouchPotato."
    -  exit 1
    -fi
    +command_args="-f -p ${couchpotato_pid} python ${couchpotato_dir}/CouchPotato.py ${couchpotato_flags} --pid_file=${couchpotato_pid}"
     
     # Ensure user is root when running this script.
     if [ `id -u` != "0" ]; then
    @@ -59,19 +49,23 @@ fi
     verify_couchpotato_pid() {
         # Make sure the pid corresponds to the CouchPotato process.
         pid=`cat ${couchpotato_pid} 2>/dev/null`
    -    ps -p ${pid} | grep -q "python ${couchpotato_dir}/couchpotato.py"
    +    ps -p ${pid} | grep -q "python ${couchpotato_dir}/CouchPotato.py"
         return $?
     }
     
     # Try to stop CouchPotato cleanly by calling shutdown over http.
     couchpotato_stop() {
    +    
         echo "Stopping $name"
         verify_couchpotato_pid
    -    ${WGET} -O - -q "http://${HOST}:${PORT}/${CPAPI}/app.shutdown/" >/dev/null
    +
         if [ -n "${pid}" ]; then
    +      kill -SIGTERM ${pid} 2> /dev/null
           wait_for_pids ${pid}
    +      kill -9 ${pid} 2> /dev/null
           echo "Stopped"
         fi
    +
     }
     
     couchpotato_status() {
    
    From 523a1e7d63d4e4b83b87b4933c319d1870896ab3 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Mon, 28 May 2012 20:50:21 +0200
    Subject: [PATCH 079/207] Always do restart after update. fix #363
    
    ---
     couchpotato/core/_base/updater/main.py        | 38 ++++++++++++++-----
     .../core/_base/updater/static/updater.js      | 26 +++++++++----
     couchpotato/static/scripts/couchpotato.js     | 15 +++++---
     couchpotato/static/scripts/page/about.js      |  2 +-
     4 files changed, 57 insertions(+), 24 deletions(-)
    
    diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py
    index 0d7b04f6..5d2e7e2e 100644
    --- a/couchpotato/core/_base/updater/main.py
    +++ b/couchpotato/core/_base/updater/main.py
    @@ -28,7 +28,7 @@ class Updater(Plugin):
             else:
                 self.updater = SourceUpdater()
     
    -        fireEvent('schedule.interval', 'updater.check', self.check, hours = 6)
    +        fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6)
             addEvent('app.load', self.check)
             addEvent('updater.info', self.info)
     
    @@ -48,17 +48,20 @@ class Updater(Plugin):
                 'return': {'type': 'see updater.info'}
             })
     
    +    def autoUpdate(self):
    +        if self.check() and self.conf('automatic') and not self.updater.update_failed:
    +            self.updater.doUpdate()
    +
         def check(self):
             if self.isDisabled():
                 return
     
             if self.updater.check():
    -            if self.conf('automatic') and not self.updater.update_failed:
    -                if self.updater.doUpdate():
    -                    fireEventAsync('app.restart')
    -            else:
    -                if self.conf('notification'):
    -                    fireEvent('updater.available', message = 'A new update is available', data = self.updater.info())
    +            if self.conf('notification') and not self.conf('automatic'):
    +                fireEvent('updater.available', message = 'A new update is available', data = self.updater.info())
    +            return True
    +
    +        return False
     
         def info(self):
             return self.updater.info()
    @@ -67,12 +70,22 @@ class Updater(Plugin):
             return jsonified(self.updater.info())
     
         def checkView(self):
    -        self.check()
    -        return self.updater.getInfo()
    +        return jsonified({
    +            'update_available': self.check(),
    +            'info': self.updater.info()
    +        })
     
         def doUpdateView(self):
    +
    +        self.check()
    +        if not self.update_version:
    +            log.error('Trying to update when no update is available.')
    +            success = False
    +        else:
    +            success = self.updater.doUpdate()
    +
             return jsonified({
    -            'success': self.updater.doUpdate()
    +            'success': success
             })
     
     
    @@ -137,6 +150,7 @@ class GitUpdater(BaseUpdater):
             self.repo = LocalRepository(Env.get('app_dir'), command = git_command)
     
         def doUpdate(self):
    +
             try:
                 log.debug('Stashing local changes')
                 self.repo.saveStash()
    @@ -152,6 +166,8 @@ class GitUpdater(BaseUpdater):
                 version_date = datetime.fromtimestamp(info['update_version']['date'])
                 fireEvent('updater.updated', 'Updated to a new version with hash "%s", this version is from %s' % (info['update_version']['hash'], version_date), data = info)
     
    +            fireEventAsync('app.restart')
    +
                 return True
             except:
                 log.error('Failed updating via GIT: %s' % traceback.format_exc())
    @@ -243,6 +259,8 @@ class SourceUpdater(BaseUpdater):
                 # Write update version to file
                 self.createFile(self.version_file, json.dumps(self.update_version))
     
    +            fireEventAsync('app.restart')
    +
                 return True
             except:
                 log.error('Failed updating: %s' % traceback.format_exc())
    diff --git a/couchpotato/core/_base/updater/static/updater.js b/couchpotato/core/_base/updater/static/updater.js
    index a2660086..fe0a632c 100644
    --- a/couchpotato/core/_base/updater/static/updater.js
    +++ b/couchpotato/core/_base/updater/static/updater.js
    @@ -16,7 +16,15 @@ var UpdaterBase = new Class({
     		var self = this;
     
     		Api.request('updater.check', {
    -			'onComplete': onComplete || Function.from()
    +			'onComplete': function(json){
    +				if(onComplete)
    +					onComplete(json);
    +
    +				if(json.update_available)
    +					self.doUpdate();
    +				else
    +					App.unBlockPage()
    +			}
     		})
     
     	},
    @@ -81,15 +89,19 @@ var UpdaterBase = new Class({
     		Api.request('updater.update', {
     			'onComplete': function(json){
     				if(json.success){
    -					App.restart('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
    -					App.checkAvailable.delay(500, App, [1000, function(){
    -						window.location.reload();
    -					}]);
    -					if(self.message)
    -						self.message.destroy();
    +					self.updating();
     				}
     			}
     		});
    +	},
    +
    +	updating: function(){
    +		App.blockPage('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
    +		App.checkAvailable.delay(500, App, [1000, function(){
    +			window.location.reload();
    +		}]);
    +		if(self.message)
    +			self.message.destroy();
     	}
     
     });
    diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js
    index 21cd315b..b983fb5a 100644
    --- a/couchpotato/static/scripts/couchpotato.js
    +++ b/couchpotato/static/scripts/couchpotato.js
    @@ -24,7 +24,7 @@ var CouchPotato = new Class({
     
     		if(window.location.hash)
     			History.handleInitialState();
    -		
    +
     		self.openPage(window.location.pathname);
     
     		History.addEvent('change', self.openPage.bind(self));
    @@ -211,10 +211,10 @@ var CouchPotato = new Class({
     		}]);
     	},
     
    -	checkForUpdate: function(func){
    +	checkForUpdate: function(onComplete){
     		var self = this;
     
    -		Updater.check(func)
    +		Updater.check(onComplete)
     
     		self.blockPage('Please wait. If this takes to long, something must have gone wrong.', 'Checking for updates');
     		self.checkAvailable(3000);
    @@ -244,6 +244,8 @@ var CouchPotato = new Class({
     	blockPage: function(message, title){
     		var self = this;
     
    +		self.unBlockPage();
    +
     		var body = $(document.body);
     		self.mask = new Element('div.mask').adopt(
     			new Element('div').adopt(
    @@ -259,9 +261,10 @@ var CouchPotato = new Class({
     
     	unBlockPage: function(){
     		var self = this;
    -		self.mask.get('tween').start('opacity', 0).chain(function(){
    -			this.element.destroy()
    -		});
    +		if(self.mask)
    +			self.mask.get('tween').start('opacity', 0).chain(function(){
    +				this.element.destroy()
    +			});
     	},
     
     	createUrl: function(action, params){
    diff --git a/couchpotato/static/scripts/page/about.js b/couchpotato/static/scripts/page/about.js
    index ad0dd5b9..93687b49 100644
    --- a/couchpotato/static/scripts/page/about.js
    +++ b/couchpotato/static/scripts/page/about.js
    @@ -48,7 +48,7 @@ var AboutSettingTab = new Class({
     					'text': 'Getting version...',
     					'events': {
     						'click': App.checkForUpdate.bind(App, function(json){
    -							self.fillVersion(json)
    +							self.fillVersion(json.info)
     						}),
     						'mouseenter': function(){
     							this.set('text', 'Check for updates')
    
    From 4b946b9d5f7e3e67a3ce6b7af9181bbf2b99e4ae Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Mon, 28 May 2012 21:47:34 +0200
    Subject: [PATCH 080/207] Use rss for imdb watchlist. fix #322
    
    ---
     .../providers/automation/imdb/__init__.py     |   2 +-
     .../core/providers/automation/imdb/main.py    |  49 ++++++++----------
     couchpotato/static/images/imdb_watchlist.png  | Bin 32721 -> 42410 bytes
     3 files changed, 23 insertions(+), 28 deletions(-)
    
    diff --git a/couchpotato/core/providers/automation/imdb/__init__.py b/couchpotato/core/providers/automation/imdb/__init__.py
    index 142ebab4..925138d0 100644
    --- a/couchpotato/core/providers/automation/imdb/__init__.py
    +++ b/couchpotato/core/providers/automation/imdb/__init__.py
    @@ -10,7 +10,7 @@ config = [{
                 'tab': 'automation',
                 'name': 'imdb_automation',
                 'label': 'IMDB',
    -            'description': 'From any <strong>public</strong> IMDB watchlists. Url should end with <strong>export?list_id=XXXXX&author_id=XXXXX<strong>',
    +            'description': 'From any <strong>public</strong> IMDB watchlists. Url should be the RSS link.',
                 'options': [
                     {
                         'name': 'automation_enabled',
    diff --git a/couchpotato/core/providers/automation/imdb/main.py b/couchpotato/core/providers/automation/imdb/main.py
    index 7a09053c..6364a75f 100644
    --- a/couchpotato/core/providers/automation/imdb/main.py
    +++ b/couchpotato/core/providers/automation/imdb/main.py
    @@ -1,17 +1,17 @@
    -from couchpotato.core.helpers.variable import md5
    +from couchpotato.core.helpers.rss import RSS
    +from couchpotato.core.helpers.variable import md5, getImdb
     from couchpotato.core.logger import CPLog
     from couchpotato.core.providers.automation.base import Automation
     from couchpotato.environment import Env
     from dateutil.parser import parse
    -import StringIO
    -import csv
     import time
     import traceback
    +import xml.etree.ElementTree as XMLTree
     
     log = CPLog(__name__)
     
     
    -class IMDB(Automation):
    +class IMDB(Automation, RSS):
     
         interval = 1800
     
    @@ -21,46 +21,41 @@ class IMDB(Automation):
                 return
     
             movies = []
    -        headers = {}
     
             enablers = self.conf('automation_urls_use').split(',')
     
             index = -1
    -        for csv_url in self.conf('automation_urls').split(','):
    +        for rss_url in self.conf('automation_urls').split(','):
    +
                 index += 1
                 if not enablers[index]:
                     continue
    -            elif 'author_id=' not in csv_url:
    -                log.error('This isn\'t the correct url.: %s' % csv_url)
    +            elif 'rss.imdb' not in rss_url:
    +                log.error('This isn\'t the correct url.: %s' % rss_url)
                     continue
     
    -            prop_name = 'automation.imdb.last_update.%s' % md5(csv_url)
    +            prop_name = 'automation.imdb.last_update.%s' % md5(rss_url)
                 last_update = float(Env.prop(prop_name, default = 0))
     
                 try:
    -                cache_key = 'imdb_csv.%s' % md5(csv_url)
    -                csv_data = self.getCache(cache_key, csv_url)
    -                csv_reader = csv.reader(StringIO.StringIO(csv_data))
    -                if not headers:
    -                    nr = 0
    -                    for column in csv_reader.next():
    -                        headers[column] = nr
    -                        nr += 1
    -                else:
    -                    csv_reader.next()
    +                cache_key = 'imdb.rss.%s' % md5(rss_url)
     
    -                for row in csv_reader:
    -                    created = int(time.mktime(parse(row[headers['created']]).timetuple()))
    -                    if created < last_update:
    +                rss_data = self.getCache(cache_key, rss_url)
    +                data = XMLTree.fromstring(rss_data)
    +                rss_movies = self.getElements(data, 'channel/item')
    +
    +                for movie in rss_movies:
    +                    created = int(time.mktime(parse(self.getTextElement(movie, "pubDate")).timetuple()))
    +                    imdb = getImdb(self.getTextElement(movie, "link"))
    +
    +                    if not imdb or created < last_update:
                             continue
     
    -                    imdb = row[headers['const']]
    -                    if imdb:
    -                        movies.append(imdb)
    +                    movies.append(imdb)
    +
                 except:
    -                log.error('Failed loading IMDB watchlist: %s %s' % (csv_url, traceback.format_exc()))
    +                log.error('Failed loading IMDB watchlist: %s %s' % (rss_url, traceback.format_exc()))
     
                 Env.prop(prop_name, time.time())
     
    -
             return movies
    diff --git a/couchpotato/static/images/imdb_watchlist.png b/couchpotato/static/images/imdb_watchlist.png
    index b16fd0940abf5d140eba24c8c0d8e5b7d432e488..fc4158bd8e46efb88d00a7e382a1528a67ccf2cf 100644
    GIT binary patch
    delta 42021
    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
    V6-vco`g;HX002ovPDHLkV1n@RkE{Ry
    
    delta 32256
    zcmZ^KV{{%-yLN2bNgMQuZQHihIB6O;oY=N)+ji2}P14wQ8lSx1`<<WX�Tu6@tU
    zweEd;m;Z*C{Q$3(f*@lTVP$1yW)<TW7vW-N7H1X_W)|gU=VoPMVPX<y;t)=-gdqA~
    zkof-zDuiI7{{NCh{x8s1GACyhGjpEr2@VjL|0`1T|3W7)L5cuxqP-trU|yoqV!~>_
    zR?qaIh19fh-#3_$L<4eH{Ia%I!&d4ZR&82rD(en(da`JTSF3F3TxpG}|Aza8LBQnQ
    z^&)kCbf>0q!h=eWc%M_#a3BAAZgy<)AFg{&OU|+(f5q{aFK^K%`19S$R^#}Utp&fM
    z#%Ck&31^=-$?X!@=$o88k65Q=XUC?drdFa!l2TAuvZYB1=O4Ds56#C3&+jz_Axh<k
    zennu!2n+m6M@L7NFs5Z>#sMRiCQ<N%>l4KIH8uRJ3_)%=j1sz(IC`HNma@P=q$!88
    zm~;aqPK6MbvKYEJl<I%3ShEwj;)(X!seUDeg%qw7K+YeroHEe=696O3#>^a-g<)o9
    z2HmEIQl>`ptoUDPbF@gl!$U)LWG825bp9p(g-Zl6AMft&Zf|Y%TIc77#-pRd=6=2*
    zH;#vgCtQdo9IiM&^oF|spVXqzxZE7BI2O8`IQl+jAik}MS7E-ye*wS^z7DnN&f3aK
    zXwsMeE-C9r;$hY)<FaP_ycf*v?C>U(eB0<#V`{}p!U&Tt%SnBpC*s2i#&Xe-MxdzZ
    z-faI)wX3V66V8|f%AptZO#q?84xa@Bq0O7Qy;fg1kWY*iK3)weHH}jk`#*{Zd0ZIA
    zVeHu%##-xdApilas=#7(B6iP#%fEfs17W4}Gsq7T)gG;8v>K$0QGYsY@c4fy^k4oR
    zDibZ*{!7PrmSAAyHT@yPc{kke^E9WNy<W(7VLpCqm;SVqoA60ZvF3}CQfxAH6oY(L
    z+R{Kj^&iC3%qc^9MR-QTtrG!i9t<XSdMpuz;4O(RM*vE$$&=n}ukO$23f$T-MUAGu
    zGu?iu;x&ZMIGykxt)Gm+;tbBDJMcU?Hmm#aK4BG}X*aDF6>b4xs^fvHnEzpQo}3N2
    zWapw(g*~J%&*(!{t3xvEpnx#XTzP68vBMdKc1f&YXyKq#n+xAUgJeXXhtp9EX7|LK
    z5}bt>Xe5|LL*{qyVthw_sA>(J=fu$3)P1+co-vhzZq;^3zfPn4cGW55p4fmel*Sq-
    z)zl)sH=qWQ6jNs;2ZupU&4#y$aW5<5(m2;Zi@K%f4|RtQOn7|{Dx|=@%h?wX7rjZl
    zA}Mi&*}Cgi0*&!Ua8w4rzl9H^>*2q!Kyi`+^QAhh-(-c+T#sp*e|U!1j;hTpX@E4E
    zDB2%tz%&aK4Qt=^XhbIx4&jP!`wB|2)h=^PmrrzC9R>aT^to^*gHXsjmH6JNG%jVq
    zDYwT9LgRZqZ4uc(N&FKQvxfgsvST|sDC7Je>wU&){1#gYg5`VPxv=OV?Q6zCO^*PY
    zs{$Xy;k}&_-3iqJ1H8lr&OLmr<ALEq10J6@afnG2oD@gW_UwBF7uO*Zkrz)vxeb)Y
    z{r!OG>H5*L1%GQ%=WunK8`Db|1nWojc>15XFSD)Q$6qg6TXQ{-zk#eIvE+OLejB@Q
    zFIJ<`sbgS`R@qyRE0rryW3{^?);I#V!U-JQVJdcr7TkTx;qJERTrTvaKKQD*Wic1M
    zkE3Qp)p~Hmd}dN!n86myws1t4leGV64xXU;3T#}QX>0;e83%rv=rZ+trb4J1I2<~A
    zbiQ1YU+7TfVG0=i4*mGqA_b3>rjO^WhV|pDC&I#*x>V__APug!Z$u&A{VFaH4Bh6?
    z$G16Mk(7?--*Wkt`w^`O8N<%l7_lvYkcjwPko$ZTmeAqp%rI0ww!fbRo26x}RO<?<
    zdcy|{kuu1{4mH;ce2r041Dg7DO{&`KL45nb#A1;12G4>Mr5`r^np;ymtNV6~3bXGn
    z1Wff{Q86mAHz317XOfm0x|#!^+}0n0?qPTs9?7<-wO;9or34wNcEP+K<=vytg<+lP
    zvh`n)wEfvkq8+{N9D&yBx<@x#hrISZJ7p<BFK-CZXW9WA@HqFGr})c5af(J6prLtd
    zR#W*4L#ZY$Ub44t`iM$38Um$B3v}C5K-Nx(4*RlbRXioRLSbUO3}L{Zh;qkbFKmlW
    zzIxJ@O9^x)%CuM(vqqz)xx~Wn&DXrVf5;^-GC_K5j5Hif6@#bw<?Qk|lCD&e<P=G-
    z&h>r?5#)~8E~3XYMNhXkxhiJd@8tVsIt5(aEv`wg=7<ok3jR8NMNjX+2R&Nt48jQl
    z<Xeft%Qow~++40k`NhD8N}K}d*aZ|%<R*O0ocR|Q^TSN?BYvQ@o`o|dsVh9XsvM8|
    zn13TAM;POU9A`Z7u9ze$<23V%DYI-~_=>~<;>lKwY0^NXccRIGKk__&{+~ky1Rhrh
    zmV}}g6m%xD!*^!9ld$&1;a3!IbjXN!G4;apZJM7w<9{9dvwZ+Rs3KRfzvELn-=NLn
    z!|fsuHbt>LGd0R1Q;TqPpyQd_^n3lXv;D0O4RDZYgL>_Uq%*h2dcxVuBRAe;2#GUI
    z=9ggAn$55!S~e8xoH1wHVK>aD6FP8(HfNiZ`o$5{F;YenFR$+NC>;&R7p>3FG`q_a
    z(R+4BQQVxLn#_RU;*r7bjRI*pNi#r=nq2a`9LioJjlW|vd~EEUzT<e0tucLNAMI8x
    z3;XZBb|f12OEIFdShc-C(RNQo-9pe}O$uhAM!tDEKVjh9`Yh;7`z|(f5YLgc)?Joq
    z_z_fCv8IEN4xc3UY}(sZiK&8N7{6H;YMN;9zON)9_610u)dIo9%PT7Hkj+*G_}erl
    ztMJQba6oj32w_>Ks#r7<tl?Lwg@t<H#(dNKVe(RadR1`{-Mn8l@gjGBq1tFA`j1Wh
    z3l9s*Wp>YCcF<$3l_B#4PUIaBBg##XL-X|Wsxw?;vYd>a8$l7$hjM9*SwS`m$&{Vl
    zB?bTE849R|nds}NZP0@yuLxbo#>qD?c;3qtgCfrHg%##xJ1!O@8an?7&%a>|h{N~=
    z5x0#Vi^jUyET${aN6U5IfiGi4XS*JfpL<95!+-!bXfAwf^QYRA85P1IMy}3|)B1Hr
    zL~bs<l#IgHmY;!keO9G&&P+FLT4{B1mJf9qG3$Uer24wP*pN)DNQ#JY)YAuwFs|nI
    zthXTr1>cuT1L{#bzAe30M4IKAkQvpk>wn~rI?4+_^!{ptaV+qTM!cammE)lIv3ZhF
    zM;&qIwE}`9<%4RIvZlMqsFd*L+35~wxa3((&Z2}X2HcVE71PG@8#&Cmc?~cN0vn!w
    zlI%cHp<q<-W;!C2T-KKQ7`iKq+ujhZt${N>w}g8y^)dYeosY;IV`%z*>dC$mpSGdR
    zcZogB0JNVKOngM~u_x(-Pehc#y%O(|2B`h>?Sr--xgr;iZ4+Fq&frOlJ{N|){#`20
    z=gI`oPOD!O4XWFCS9Q}lZtsUp@iu8OvW<a@nUGR}N*ca(;;rOmaE-mr@A%8aN*aZH
    z@WNc)9dLYS*i_@UWigjFux7>V>LRk-9co{H`$34GY@JM+W>QJ=Xt^+K8j?gOl%yO#
    zL&e)~Pu|0<ynM%AMSg<5g!D#Bndm^ogXH>!E`ZS+AxnaE@}|-0?iIjqq-uo~ju8u}
    zbs`E`uE;P<YOY<W+?rDfCv}41E=l&go_Xw1#E?_oaG_LN1w#?z{$ktlm)x01w<_f!
    zLm1MhpyEO4yXDkAml5Mnjf(ws;At9v_~U0ioc~9C{I3K$F8X7X9gI;N^w9h!#V@aY
    z3U(ILtxN{=B1op1e^9;w;!Kk$9?*q=0-GA=rSYV^e$z~~vG<)rN3f_LjwOE;v<#Ea
    z`}5??_4SW415Z4Q2$uldW7nc3HkEAH?aMmh*P-eoz6yAm+U_R^JdVq6bi|&R5k8*%
    zZtLz$|Lhs}Fvt?SimFj`>cxrG?j8qb+aw(lU#o8|B%PUNu&QQ_q@?xU4bhT;Xdi{S
    zy5ZdW9<=l6cw#kHOX%d2wYU+QLmhuJ6cYvh)}Wpd{#i?Hv$Z9Ws(y@w8mI&g`qFQn
    z60|AYWI(jkS(Ycn%P%)S7iBjj+v%q`S&kY=#U52IiE+^_=%#NjOe)#a3rIaXyoJ><
    zg;%$~uDKxKw=J%>Ue%D9MsG9$lecB(JLb%NzeLB(V3@zd<Si~-)tK~W${;5%G?JdP
    zNRvsgIFEDOPK4cB*^75h8EopQ^ohDbXLI3XjJt3gM`Wgk!$V?7+*7hZ0p3y5O_<JK
    zZFt-fDy?lXkSYX8O3i=2sL`ci8mCBZvyI^@)Bb!sqr|e;{9QRz_%fsfP$ah>(ryqL
    z`z21c>=ONQJ3)k<<L~rth18rPrMQgXmtoTntAz~sGnF=*h`6``Wl;t(r}xx@TDa}$
    zjo3%ExJDOcThGQ(;p~&X7Wr3G(7xZxB96jj+P=e~M#og{G!FCnnr75)U*boq$i$}D
    z%`$`n!;-|Lz9_P65R|zuAZ!g!NXsg&M2()2h>(^<q%o&BKNr7)+1tuq)ZAPf=hI$1
    zQW+YqSy(?`Ok`X1No%A|*@Bll3+QsAD<C67H<|D6=%`d{YilY12`6>_hgqUPyu68n
    zJyz#HFN~@S7X3Ujc;|`e-$nXh&Kf$U&4RhUoHA+%`PXd7jLs-vU`wBxb0gCV<|E?U
    zN>)(^MV^W=N}kAT`tC&2?%CrGalTSjDkfGR(C7AI`H+3XqB+y;g5BJ0Z=bv^fI2$F
    z>m#)2^;mmZ)6SO~U)_D9GOCURS`>%+yV<wD<_f<Wg@<BeI)K<0vHgOoslhv)VflAr
    zepGw5wRs$0n6wq}D}3(B5sH3{SUKf#2vp5`|2e6O?;frus&I2=Te)wm+g>+^gNm=3
    zvmjqk*Rf?D$F4E5eB|5XIqCeXq$+)!-G%`{7h$L>elA-v_d1j`m~@fV8?>vz;Z~!z
    zYlVn4wxC8%g>AOv!I_pl>&^3&=Q7W!p%LInC~f#LKhXtvxxS}cnfqv@J>yGf;P{qa
    zaVP!QAdQY!TN1x{XsgM!b|pWW`$24z^MnLEA>1kckW#j)&?pQ4w#<9NM|2+v&Jd$>
    zl{{YPl}*2;Q;J?%t9s`1z-OW{I_iGy6=z^b`s0cS|L37zL1WzW){lPcAtrs!#^D;7
    zgvZq2|M*;{V0}K+=Gi{PF&MtCiC2<v57CsbIr{OgzVXriG+~AI@)q#F2!6Y1K%Tjw
    z7R$Q1eC*jcwR4m0c-uY`W^C~lslW$`^f744NlQz&kY0IywTlQ19Zhu3&(E*79Xkv`
    zC7r9hEkwNzjXKm9HJdJwhSbe8e8SwgC7=Iz<3|JPzc_7~NcZ*)6V}1Ev|3p4LmJo-
    zex+oNJB<&slPjVUI!TSG${39q3zgmTE>=bl{kivgI^lEwW+EUF@027J9!`FIb18Kv
    zDgReOyqJtC8*(*<bhM3QyAmd|*T4WAd$pR<!vsPFzlX7{2aAHfl}zAkCal0feZ9L$
    zFN*-MF~}O(H^N26;K$9Fw)X`>W2s2F;f0cquK~WsphjFoq&yc>JNjelQ7nLFifm|W
    zTo{etU+-JOd=7Hv!2|0hasxA;K|danHh5)zn!QLmo0UK2H-$pkibcU;Uyvy9<;zfG
    zQv(&MvWUSRsY4QSkZnh6sCRLu`LG<AzcMnQp{QOGxZK~0FRBbtl#GD~7i^AFh5s+#
    z`9Wodm@?nRJo4&<R{ZptLTdz55pEO%QpS0~COlKIr2Kb!|EDi_!aj&$Ewk*77P?lT
    zUm{Rl4(mO`*pSr<vDKjaH-whR0i@ZwGsy<0Da<b|B||S(&S?x;wo>iA2N32s6Bht+
    zEJD`yhhS+*W~<q+cZ8`_uZZ|v530AK8P_qXD;ZfYsNqW!wp%~LOu!}GZc@-T0(~$C
    zrcz9nKITOr4d5c5u_bQj@0`79tkKMblXqh3Ht@s1n9@{L*o7v+>jPnxT#~5+N+eHP
    ze^BTVuu+1@p>oIn#>_psq0Q{|;`sv}`E2^oRp5*|)0>Gdj0jGG60DPP<RQcT=+>nZ
    zPUCb!*nJCu-xn&?II{x-p;p#-d&g;5m%)_{k5?<Dz7bk25`f}<rW4wODHBA+!eX-v
    zS%E2+D$nWI%?jP--(3{rz*`oGo+NIoW0@6ZB)}O(F3oGXTo(2@rsvF0=m`VNX7NWm
    zh3%T)W>8?zS&(*pIp5R-xpPYbx1<RlTD6~onfq)rGc&b32!ffEOOwoTa>KVB<wj4p
    z9^1R36A{VuI@NwO+NzF#Vp($J7F=az>tdcsde!*JV56)iGDgnK``*m`k~dc_+h0;|
    zQ*X5=6L%Wr!&%mI8yt?Me%S#k-iGH-IO&^m4-`CB$NS>>Q;h%KL=tom+g7`U`rSxQ
    zPAOLFZ91*I@Yyq-b9fY2Z5EfUet!W)9C*nr%f|=#jyEbAdtx_lPZQUm^(}-_d;{Mf
    zSDSmS)@kF2CgvU0aLx9L=4$-ld%^g{Sz3Hcw(yS6T)5^2R~l>i7M%+;zOaG8frFIV
    zjq)Lt5_{E(p(d45t944$YT^>$rc&VKEZ{vegMHF5>_~E97!mCZTzu6={Kf*hcrG!J
    zf1SF55{=u~UxJpc{Pc8D{794Ll9Qexe7+Rxvz)807_d6^YTDjISj{z(ykLx0;^iFM
    z`Pd`)oX<i>5wIHri`BpYxMcv<{?FA91jj=cCNMQ+7R6TZTzfDj{P~o>P~(}*cCxKj
    zY+QYUlMXsRi6;ta)pV%kNk9Qk#(R3>V5N(P1h5P)+CjApv-BY-S|`c6ki$HOr^&OH
    z{NBQ_9X`B^_6<4nKH^OKMG9FwTn?{DKmN842^hJ#-LHBfA`t={Von=pR=O*SJ-d%9
    z({A=A$ZYeB*CJkUsFGX_-k~qSV0=+ifjNw6KcwBmfE32wo<mVqJ;5|xlqz$Xc{EFZ
    zE8E1lNuda~=CZ>Sf;G@&$pw4xF=Z!zOcA2n7rL>Pw@GIbp4FzZ*OG<_iRlD9`r8yV
    z?@2aRaCInE0~5g1^!jFB_Lql8ebUqGlhb5fg=S5Cj>UAg*<qwX=dZXoVj?2jnKd)^
    zT5*BZ7$0oP(d~obY6M58x)9k6K?iEB>&4TnrDTP{#FH>jhvDd9BUMFsqE%aVo}SrK
    zieS^H%iY>Dljp;VVLmPCn;DYFU4a%Nj0vtv*d1y`ra?f)Uzs=C+=}0)^S;I0hoV86
    z0$VwIPqMXU^2XDuqhdpE;L`MPw@b!$bC_*@VnGHUT~g*3KDQ$UZ_C~D{b_AWi-k6=
    z_EmVKScCbbR_^(74QV!?yZ=&^ZiCJj$liX~!8?y(sw0c!L8$5go3?k`n4Y~EJLmF)
    zz!AxhU^k%VPpSJA+^eX4zRyO@iXauMF|8UEJ4sj;)pw!xO$E>JFa$}n!X_cE;5V!N
    ztqabgeWu3HKM^_qw1S*>Bs;&t%MM`HHxk$(s*QN$EGmsvktKK!*7Zmj*;~8O5&i{p
    zrLaf1`H^g?2C{<<qHHi?gDuQIlwKo^f3ULF6XgQ(Kd8xHzoy>IHCQNW>FO3Z9ZqpM
    z?!mc-wUx;uc~YvDP1Kbr=Il&naHhLke^bn6J36nQnur;S9(&Wyy}I^{*uHOLqG|J-
    z7!vK^O0@2Q_<IPwm82-Tw5WJ{ib{P`riNCf&Jdyo=pX?kPMIbep?KQyB|S!c>xdGk
    zlli~`w&$HzT=+%5v=H0m^d^=y^f1-4D$Hs?nHN_gu}F5GQ5GAE0ZP-^z}8sFPX}r8
    zW(DdpD?7=d<-9};x#ALJ!cw27NOm3BWtk)XNK&w(gt*!qUn^(RyVIwNFPACVirU}k
    z@3mqm-AIGJ26e*V2(rmX%uFoTG&F9|&rSf-zRqVW=$t>+d(YQe*kAAx!qd)ZlVsuq
    z0iTtX6|y)(5-yKB%d@o>MaX>ZvNWomhmra>U;T>7BV9vIB*QURRi-gaINf$=MLZkB
    zYp`7cj@~=@kh+U$o!a`Qpvb$yk2MF$!}h?IL#EC=D949i;<lY=`TRJI$oHp;eOZ77
    zCvy|Asfmn{wlUmmPO)7fp=KaPo!iZ{C5JVs{ee>t@AP^|2kufqb>e!!f6Am;zX}#q
    z+1PIgXkpy9OE@CX%|*Z!esv<>^S$>DXtC~J{X>4A-kJ13Zj*r4ySIw0;>2Wwwd~T$
    zfw!NkcNtx%D=Ng`$U-%?*?O<u{;Yq~UMc(yHLWog8@R!1K8rtCwKTE%^(%Uaynz9J
    z%3E|+I+&;DhS+tIAoNBB{u0G<bkh*@gob_;vcUpxYiCw_a}>54gW<EnQN1J=lw1D(
    z-uBhMESt`t6Yu_HaUhGA=&H57-DD^n`^;?^dF$vXRJlOH`1WA(teeAb^$75_(y%HH
    zi!+9!nD*3)X=nNlH6FCo!`y(}yki9t{z^mAcc@_Maj9`6*a4MX{UB@s(LCSoVY2-M
    zx`=PBI7-??q#cmly>bU8F7kwOc%XmY(5C=NEHD;R6M+_W<60Q3FaiR*$|vtM&Z!=F
    z2nl50t0gtR-rXqw+YH|k76yW1dka?hi-`G79wh4=eluBom(9F7_P;;j6X6%v8~49F
    z=!0gqJ%@f}ZW6pnpTSTsf8=tz#KBnXf!_Z|>%pcQ9sj72zl$}$Zv^227Eos~t@rTW
    z<;frcw<)XSzu_O<5o`c`Vyr%hM<j3<qy2YxcBFz#p-VcbDGT|6hyaMLm~I8jv9BQ=
    ztoIeF#B;zh_F{Cn&UtT@JQQZ=P`vy2EU<1IPtO8t#{XXI$+K+1JeIQpB@v7j8=>zj
    z*oF(bS!=D2f0ka@i#E&!$o*^T0f4gFECRy7=S&CG*@@+ZYW<FSbFbauD2I!6?nF9m
    zi~v|v^O!2#mgu)kKER#D2H|Y8XS3H?37EE3mHE0mN2@Z(#oq+o8v1FJ;3M}+&?K3d
    zr+p_|d)j&N6qfRc`O)GB5a)4-hPQb5(Y(HYd8d?d_T3a&_$^)VM^X2uu`atUH(uy4
    z<hae89gi`pmqiODST>w?uO1R|l<BVWIBRY~xv}!_#Mg*?c7QPpyJvIc5<7T#S~0eU
    zV~U<Xu;LNkd2u>Vc)s(&fvcgJ)1#`G>0`yl`E9Ik!n-x|IHY@tdgQnb4+m#NVwh7u
    zx&<`vh>sGf6QB|CZ5JTqHI}z8Gw)63KJ43uzLGE60<6JxL*&zVUQ2kIi9CEK&k6GB
    z*mIoKYG&rT08sUd-Ohjj@o?7`{-`b!Tn8=~<Z`n}G~1n5IEz6`Vke6$^&4Ap_gY1l
    z7AabU82@U}ad*sTS<vCZqM;rIv|>axwecYQv^piI_p)SP5qY_g=$<p{O6OmOems*0
    z+0pGY7>Z8k%kr*DRZP|3;eVaMvPwffA(U9F`CEn+FvQH%GBcs?j9gkRe*LqNL}No(
    zZp!U^2U9%=`om-yuEmh`+(yZR_?!;NVRUNUGuw#h8enR``|{F5;+qSrtA3@*cIj#o
    zE7%Euyk$NP3@5ntmd`2I5*6sE>W&ah-;*^AI{scHV6M184?FKuXnvY5Q9lPkUt%we
    zn6s({tXUAXFB2mMi=ROPDk^k`A?&3CM-)<ew0Rc%Cdg-9;5pgUD=lPD^Bg}!jCu^X
    zTrT;oX%$RZ-IGU7iqMU-^O2V35uFH#y)Z*i|Ly#1l3i&&C;GFjuf%Y$+I%$A*=nvB
    z-3k3?I)c;C^avHM*qYVwTy&Xq@`H#{3Q83dn5)oaj3gB#fkwb$^6*-&)~A{bsZ7io
    zxiq$1OoySmYgqe;pN1*b8#HLR;MT}n5XSvs<r*Iy&Fc2@Ma_98zG4NJrk_)l0bQ2d
    zZjEirVuDWvmUwOx_uC-Ed$HU_Oio8&sL%d(I>{kcRfe$@%y^!5(BM|JH+?0~11tg?
    zVBkA^o-G_Fk{4khB1i1OQ);XO|I%NGVr8*#K8JVxHobFp46;?>=>6+@uCyh@DEBBB
    z<-yqO%g_R+j-XwrkAkVnOkVvSl({(EyWz>!ESDW75%exJt?l4JEW?&4@%sB=DOvD)
    zhenI?y+63A`CAE!SeTKu1c&WPFA4`U&?lP;4l*~*uV<alPwk^I-nY27>=sSaNHt=I
    zaw=x%4eEPQ(iLhf|J4jTpBxJ_SC6@#g?FDM`URG@Z&%*SAF~Afu-jdGUZZLZ=hilO
    zxQKPcr5Xj<#LA2S&pE*P$PY^X)C6V{nvkUNjRR(EInU(94`i_$T2jH=7yYS!tW+*$
    zF1`C~aj89AK!IRg-3a*IhGPn}qGwxrcfxN*)e!l}1>-+F#G{+<maD%0`W5sbW5v6(
    zCGV?pO^Zf~W2E&~Ze+}yX+&haO4nS>pO#OSH)a?lFcV?|j;~Xyuoqs4`Y{bEXyJ^<
    zrbrzvGW-34bg2XxHr6yp(h`k;I=bAX)TcX$;wepo?Vk`;&p;Z^Tu!^>oD5Mu#4-MH
    z&s>^NLHwYf9iem44A%jmi4vmpLC%SJ9e#|vD~xs7ja9Xv(nO5A*W|03@e&y@5lC7g
    z=mv8ddnaM9eMIAUe+)625g14Gpq}6~4QEJdUM<v9`BWKdeKU{Oi8ldahY_d_wh*C+
    zxcrQeW1LwMlP@gQddOm~SE1tPSf-zz5s_i}f*FH0UrQ|iNm{VE)`|9J;taoK6(>_6
    z+)T=a$Zsxy<TZn{1#IdZI&i1rx>-2lRB>sW3H{8yh=1IZyJ)Qw7@zRmF(45>5*&%^
    ztQ2Dz3B~XfZcw*s){Fqo^R_6uOSDM>0JND{QZZdz2ifio7t*F^mGbWgo^ljEaUajE
    zgDAAbnyj1{Mgm{!rjf0chc6(7&4E;%f-zR2sNnrdV3fnIyvjT{)F^!l*2p?XVAQ-Q
    z?NY}DYh#1f*EpMawtCoS(r3-1z`^z0d4bxxDufzqsiiDj*$d!nY5Ne6aC0HAuL}D+
    zJ#8#<*?#n=*8iaWo$z8;GivOj@V0#K7K(yXzUNmn!_XMLqOs+OInPVlvEDY6+&+}i
    zKK1HM;DmhzR$d#mDZ7GfTakt;WlzNZUT4<*$eG{eNbo;1S6R7Aw7?<#@U*$zdp%T-
    z{Zgs&1FDI{0w~~#usA|r!KLncOAG78{R;)ht3Zd&%G(*;@Z*C>j083taW7am3iWRc
    zOTlh3j!C-cWOi=1xF}C`Br5gVvICI?P@+JDFXc?ba*dnp!dW*&(_v%Z=+dGTHUHJ4
    zAfZFMC*xiD8V0t>kG?EJ!p)vOUAAha$eOuqu17~4gbyH9qq{h&95$Op^~sq0yvOrV
    zA+j+CCiWy|3GpzgTrds(eWLJPB8tE}ewy+vU*z8!T(;r}L7Nlr35kZDC}KzXPlHFO
    zH4p6mkx)yib3J1VuOkA7S<<}xqT1$$fTWc!pZGy<3KuB@Y3?%7r<6)j^{+c>ScNLw
    zo`z=Yu!BIU?bgosq3&-8&_fCf77vmMmk-qnwa>yi=<`3)-1jED9EQzGI=*Y>H&{Uq
    zk(qv0%9sUx6m`mUcCk>{%KQ?+mDy8S5-GJARrV&b)`*tso2{>SgJJ6NGXMR7Egqbj
    zmG+v+p>|ckFB4@2Hw4j`8VeRK)nfX#6lJmML0tl2=|us5F0H=_l-zwb@EzwE@5m<j
    zGwr|nCoJGcNkoghO9TytBo8r=8W+ea?iN75D#6qs^t`6LV<}<pC-g3;dA?&+s6fq6
    z+bRvOgBj}Y4PCwTi|+(HLrn9NuOd}_IS%Mc_t2LsnjrZgxaq1q<ZQk<c4|~cmt)nI
    zoqPu3yp+4K$=EFj$`9!YJRd~J7QHoN!C^GJ$e1L+IK3~kx-}|E^(1~~F2}O_Idvyk
    zSXRFI{K&xWV?&%2Q11SSX7eU1E|v%m<4U)dGKCYSX1$%Zq|YgXpHwH!WPel+_-aY+
    z)MLr9(&+K0eCm7={)Qp+Z2Yesonva5f-)2^Z;z!X{&a>g*^I2E3v-{TpKd`o18M5_
    zV8PX)$(Wdfj<$$=ZejBCRJA4m=ZG8O^z4nRzTP`rxq#&u+)EnG>;k(ek3Q_p^)pWf
    zZ$(0#DKjv6yn$Zv-aeg4o=Hn=HV4P7%6^o|fZ}=M{#{n)(Q^-@*eD?66D&<*=Nt=&
    z8`3VdDb#-4C)sJW-C|%MUx;5D$wt_%W}1&fU84>0fx&T_6C^&X@>7QYNySYJo5)GR
    zc=`u6>}nl=>_+)&?MV<#|83`JZmd*U>RtWxWNAs^Us(x}n`v`yXV?2Ip6$hcXdY?H
    z)?dF1iGDbO&p{YjU6E<4@aU#_+${=#>dKxg`Ig*$#fAd)m~I58nCN2%fE8tGGZToy
    zSEMC1Ew!W8vP0<4SGeGz^3frG#<D*n98mEVG8>Prm;>2%U!xQM{@EaM{8NyP<{#_L
    zM0ccJ0nN>_n5k}$o|YjU*n5v|euxdR(yhJ8>5OBr4yCgN`VrLbQl)Ic^}#AYhD6#1
    zm&L0VfZbu(pF)dmm|jC&A<Whw@8ghd1=FVn<aeXVrA38tB&lOmiRwBcW|edAUe96^
    ztj7D!5ME8$T;HG2q=iA2v&?Qt6*QS?2$}7j7jm!0N8X{Q^*NXAfqj>$Ft#M=!cF=I
    zt*)lRCaY_L9#;yPew;>76s-w}s9MpwxiSPP1!p?%sW5@hW1Mxnp-$~Wv}w8S@Z`g%
    zvixSV96~(%1Fm*WFEtl!GI=>CuE(}O-)Z(E6}^Caw?fYV17tIVA>~BHe|d>5R#DWz
    zU1)yb44R#oPR0nxvR=jbXu+G!hj8q`@x>L@tv}?f|3LA{0wgd&iYov(2s=zE-3Z1k
    z=9}Y)e3$camZ;6U`ktk1f6IIMtjy$ODAVG<`SB1@{NUz6O-b5GOYTPOCe$AJ>j0RK
    zqQquXG{@0^0{Yo1Q5`gIDPrlEkT2mZ70A--G`ZNyQmCXILBHOwZZp39VN00)x9v^`
    zYg@<2u%ks2M3safWU>o{RXIqfuJE-aPj6@uF_QNx&dY$3WCZAp70w;PF<Pk`FCdKb
    zgQp{==7h8DwLd%URPlE-zD4wPW|p#@_m;A&H;t468P{r}x+<je9K?MdpPw0AK%dsV
    z+dSp+nQ~fzIPq5F<*jCtNAcQG8yYFGjGP-&(b?_Bz>)371c2b??=YXFfV<EyG*WiB
    zSjsvZrTawnz%CCc+Tnazps*X$_~jlstK?Vu+d;R7XnL-h43DmDXI@H3H$NJF)M?|^
    zg{#sJ+AyTOkrI2Yve(na#9{f`(^T=b`!yU!&lF<0Kqg6=q6ULMmce(m;D+qImQ*C3
    zqH@Xo@P=Lh5rDngQva$;88dB5CB%+N;tqO9Q?!1@d|xLX#NU-h{j<sHmOT)L3`y2d
    z?~nZl=ggC0rqC~B1SdDXiCFflw&#(Gp4T0cisqLTO^WCbe&49$-%#=`)hn3Ynw#~D
    zXt%!zPqf$u^CbCdz_Wb48`|RzSqZ2A1^Kd#Ew2Rc*8#^eiJyaFUWEBY^koS!cG4`8
    zx{eP>@5>>1^wiO2e0F1ZKd|eLUSTide+4^=rZg9Fit643+elFz3ltMGlblPJ!s4qd
    ztMl1?{^Rj=Bc<xvK*0||EW#cnC&{LM_?)SgpW6Wj?JCWAKM3^DM3*y1NL69Ua;R71
    z6Mts*xB;;6UK$JgrK8EFNXZOYxq0-=`Y7i37e@b{t*vB5771u6;p~Y4;x5Fcfs;{Y
    zE=K_&QQaNMlh{1^w%;?!e46!OM6UN_$*hf(<T1O*l*Um9v>hQ^Bsaa2*^eDEznCbq
    zWR7*Z#vbijU(&(cZk*2eks{N1k<CB^DG-~n6@aOO@vR*eks;y_IgfZTJ}%tQ5Of-!
    zIYCi<%usxOg1r6Q<<ur?cpi;h=CXM;D4Nv!L#z$M^pNoLVzSOtkxU+MJUgsYCNC?d
    zJrpQBEhWW4ziand^F8ys?A?e6i$`Ozsj4G2a>gZt=-@0}a7P6&*R0*r8r9tk)AL5+
    zR=~-UrMhd6C-*31r1t0za&PjS!oLi(IU2ANK;MQtX59Z&RZOp38<nCw@u|mB9;`m6
    zf=C~k0uif)QS$mfQx}o&$B{pEej50azE~M@UD2G|23@;9<4r7N%j3Yf0Yf)fYplxD
    zh|EkcYBKp#D2?->im0`~x~3X?sLNjfc(18*cu<PW;v}9NBXr;2u*o{T;Thb%Yu`BP
    zq9SgjY|IQ2mE|vL9t-3zBtm3zKKhPiI*v|Yfg#78bxS|qOb`WGu<rMg!0f$?=^JJ)
    zZbN<z`?)2*DbfqgryJN3b0p2Ic!AKw%b6P~*-)|(&8NH3YvVXGVz%CCgTLGX@Lss!
    zZxYRcwBQ^Zy}`XMO1%Xzh?$3%odG-N{F#bYz7NE@nDS^`9&hX_@%}&eNRIU~k`~(u
    z|0#JR;mOj09=LVY@PY`@eegEzK{$->2}B#8CJ4dNnh1q44WWD;X?^vT_hj+MXWk3p
    z@ZE!fVnk+zEs-%Mch?ZBJVp=*Z~+>{n^vosF0bhC66f&v55X{q`LAtkdLMI~=xj6=
    z25z~M`-R7kfA`rQI*qv@9XbA1u8I}=9Ovvt-xlt}vV;F#*8V_kb$t)rNSUb0?R*7e
    z1sba~PaT|3{?*|yk&M&4sQs}fncW;H8VQripBKeo2cHx@Hr9BY)wlsL{PwV!S<WCW
    zQ{0BBaki8ly=3pI$ba|!d(?cQu=*+S;u~z_J+q{!ZvFyMgA_S(kxY2<d`mtwcU&Gl
    zx1#NGCWhTcP$Q)rvCO&k7FXE-Dw18;Z$&!0F+orJ*r{)DFF1k5tHDu6pf2d17yG!Y
    z!JH!cr=skR=#QkG0vtelk3L`+sw2jrBf7(lq0S7XU&>bQ3M2A%xplAzhGTS^=(Jz6
    zDByvU_$1=|T*XLpu(ghX($>1gRToB`8*ktXDi~GH<N6GBQpielp(!LC2jr#0u5CTK
    zRy}Wy69hFbN*`EbuUMcQ5+SW!&U_0SJg?~_g4SS%V^O@=lYyARI^%g4l?z<1R<t95
    zRl;aNAx!LV=N7EY3cC6I4Qt)D0TpLclDZaNjtE6x6lU5z_SXTFf_(m}JbDL^o){RY
    zaSJ+cKC@E(6f&K;D~ScKhDnNID)crUUs1r}!ts+r4=?GHKGd?O3H(W{Nwsiq`Q+xp
    z-1KI#q9S@_0CnzYE}6+?R4xN!CK>9gk@9@4ptsK;^#Fj`VElCB<3aRtdmz%zk-ZVa
    zhGNsXW8PBcZKO;PK40q5o9>9wD<~i2StCSkaqqN9-_f3(<|f21m7HG&o_^lCGb(xQ
    z&rP;She=7$MUWWQT=|6ZSL#k~;&`D0p;SsAt6~m_COQpwvUs~nw_NQZwhPHoZ@F6A
    zk>&E_L8(YWnpO`Z&9@6TN|z5TqL2Fs=2pesdCl!ACC3X;;nSQ?c0RC@<>ISd&ZBn^
    z$F#s-jrX$UlO<k_S5YId&VKOpkT5rMWL1(qy~w*0j+|RGz&9dpeZztIy_|3I0uHj@
    z_5B7MSjw`|2FWFfS{%eaIoeptQke!YuVR=WrxwstmlV@mN!5YZ%dV{X|M*K-<LR<(
    zDa&E&7coX612f$&hMd)eW^t*U%G|@I+xRu9bF>a{xL^qSL2EM*w~VJsU6l}&h5370
    zBpFWKMP_TX))h)ra37$CyB>saF!|QsJ+cqnmXJA}4<g53N48qn2j&BmlJDbEeB8rY
    zSV3E228?Z<?5_$Q<#AQ%b;%P+;oPx?eXU%pb<Xrl8-)X)@7JP@%Aau#M?7i4G`+dE
    zfy>`j<B$CjI8@YuEZ@%tjFv@m^Y`7?AAEN379=06rF32>`U7j6F||MD@Q5g>j{Jdb
    zd2Ob9+ecoT?st{;FU`<UU%CF_GvxdW@dBusVq7E-66SJWwxan$KKwAb97MW^2aV%>
    zFibg4jP+Q|h~Vx`+{xH7{Uz7-uQE1~E|**-cEcoN%@?#}Qj1ZZuoCNz%j}K+oNKSf
    z^YBp(QnwP;Hd5kz#(?#c9|C0qC3*mH_QA<adI`Pp#iyO8*7&QN(3R6An@;bm8>9o3
    zm|FQOR|i_tHOJIJaj9kU?1x@aYVHZ>?>$)J`*&~tn%YJvpR{<`F^dqBtqJ|spBuQM
    zKH~v>YoJl$*0!Q0%GLf7fv9uH4&%izp+MHl*&`CI{6wSm)Z)&yLM8&ms$n4ea;BYi
    z97A3^egre>$<gg{P7TdaeL+tANn)^QT&lCn5Nv>tCs$woi`^JrramXK%etL&6YEV|
    zvx|CUpl@>_T@A<T;@}1DAyc=T{+*?kLBf(RvLD?(%r^xHv>i*?Qo`a}i(M7}0{Ua4
    z=;lF{ghFsTc>18Oz%;Jvzuy33E7?}ZMbZY`Zc@TRVcmY;kODB#$oZO2^{e}Q=A2T?
    z(ny(s8ZADXgNsoWox}5KR^^z)bw$7`-dg=v*ealOmgq(yZP}5&4z20ge6cQO29Y~e
    z_;OIxpsMob+LHKC#_LnFS-}(i6~t5jy>fKNLpLPLa4rz8r=oV10W7MHNxNt5*$Q-x
    zyoW%?WNgQj>4#Oz<{boPtt{Ik@|-tqMeO)*qCkIfH<CrN(jXE2cO(CDNJZ5=T0(7K
    z6ih5E-xl1Rb?N{niz^XPwpjPwk!6*W;d-D^F`RQvY!xHnq9L-%*W>;K+j^<8?7qw_
    ztQ75<!~by>KaXCz9SFwZK(doYYrbcQjES4AZFC8$`izZg#M`ck;Wa~&3sLME^ofUk
    zkR;>BVXCW(ai6G$hrR?|SITC|&ux1B&UaPR@BD=hjfk^Qi;Q?IyW%JKu4<vA^!?jg
    zH!^~zQzPYTtLoK@WizUyqjW&?pmZVqlit{ri{)h^rd1HL5%8~1488BJ;xSmeC@&pp
    z#pXCNxF<%;Z&-XVge?4Xn%>+Vtthgxr#Nx~afe*S9OMYKYr=%R8UNQ_9f=&5o&)~k
    zH4V*FE8?rn!B|Pm!HD3{1u`speCiwR_I?%FnQBds1PunJT24}StVJ*m6dp6&VF?U3
    zUW{NGj?!s~DR4(Z72r8zLT01vBPDyC0Xd&q62fdh<k+cc_DG}9|9(Erbo&MO-`9g{
    zyF~ocm?G2!?%Xa{T&Ks7VrFbfzYkaX5x-M=_SHOw&@?b99YXKFqXnFf$W@CHRyvvk
    z?6}BdNUh<%G~+Q}wdKFv9jBM6Nw6Hk{kAn6!uja}l?l+d!GkZgAsDHT#9OAt=pvDl
    zKH$*4<<B?v8=eXJLZV_uS%!!p&ndAMS-qiIJf~MMJf*{^$XjL=&oMVCrdESK4_O?W
    z%vn6|t;kLA><0!@8W@kYgSi^ccMGI4wc_xmL}pOXSq6ottTZ{p;GA_2ygOH|6W)Gs
    z;$b^7{{pf?F$GNPuCAcH{`LB5pr~2>f!!T;kuY{m7spbl&W=dxF1bo4npUo*(Rsj&
    z{9-qbS*p6TQMZ`NXND<a`6KG-=wnjiFx_1qhvvkL$=nXPKf7feqFuqjj5*H~eA0H{
    zQ8CBlWEhu=BnyF)JIW0K-nt?np#sC<#Dh&n%?wx~h0U)amA|<n;I{}Q=Yctz?}5?5
    zU0Re;q$3Ttqu9Kg8cTIoj~4BgyknDVHiQa_oOyIS445-?|NcFSmqlhES3cwRkNM-z
    zo=+l>1FZVjGFpKPY?2YTzMT0(e;;p*hNUQ>%HKU#$6A5kK2%UM1C5L;I9BY|(0_lV
    zAp>rwO0T>fgdXe>LwSU|aWP;fj`g(dQF>uC;!b|io#AcdIQhEyh*MK`fxS_0Ob%6;
    z#g*&PDjW`QosM`7oQC3x5XFiduKm5(*Lcl8Y130&2Hz4Qu1BskVr_c@qbZT^cLtYZ
    zuIBwyEY%v=AcEV!vfp6`S0ach77@X=?*n2cqvUG(HyBG#H%4nU?qA;UqOsQu$tA|X
    z<wd?jdF4nJ$kHfLXfilYk|O2&#C5hmfsw`Dx+o{5(MLhOL@D(!C;5S68JXN9FCRBc
    zF2Tz5n8z7ASRD*25Y!vFeJWhuhADQ1C7#G=aR5M_b3yr3NEXOP1Y7c2;z($Q4FE&L
    z;I^&wU+k>qwuH|j<MR(gqe0(^Lw@ZAX;!$#DwSNbWm+wuSU!!Yu`!y3gf63A-*sU;
    zAz=7=zaqdZZBNc;P*i1(?(YQqi&5IH9RjV*HH<`9Znci$OpZ-NQO}g;XoMNzD(t!i
    zU&T%Bg<@y*4DSJ9qd|DcPJOZ{RY2q*8J9nzcxP(zC&y;X^yhBPg2ko!m8)@_S*lS0
    z)tPB&seva$Nw-wn6)CU42C|TzauW)mfu1S#@WFCmgUVYs>r$E$fQIHf=`HBASY6FC
    znonIHzDnW{$Ac1WF!;Ad7khI0sT^NrlC6s~-zhQA>H;mY=A{0TSuo0M0|6?vy#tcX
    zD||Lg3lT)ro}O5sSVCrHY2P#OVV5J=4-g_&^%-=`9Q2b`JkoL`R?~B}$aw`OQ)7Xk
    z8$I(r@Z!Ydp|0$IEmD3_DUw5C-Rk7)?I%*YXJRLpGkjix1%Azir0Xob8~(FCzQ6<h
    zz+mrJE&|RHMqhjx7L%wv@ZbUdWuYf=T<swQVZEMy*QH2=1=2)@iqghaU3@8wt4JD4
    znPuI&Tc{u|ofXPOQEG4zQ#f{IUZl}XJPB>UXKK?2;ocFy7~AQP?=|1C|L{n9;UETf
    zpD*f`u=VocLVnw+oEppF@><{w+6(uwzkU``>BW|DFhQnt#T)tnXqrrj-WgTqQ$#2o
    z^t#ejBMG+QP>JJ2MV2l_(g8L53MU$6fxG?)9A!Z+NUOq-J=ZoiHr^qm3kPk+?oDPq
    zp88T_68VRBM`CJL<jH`;Ul1DxnfWapwbwH9f?<;jd4{NLluFwTm71F}+o%J-aGJ&C
    zj=XKPydbhZ44~YAr5=KywdKn^OL580_Gd<_H*5%LpV~V_UKqE51|8Gc25IiSQC+Yg
    zy?8jG{gAG#Z^15~0Yqi8R-ukxbkq75_0HhU2m0KM39*>Zw80?48d)kIbzsK54T)9v
    zP?Eu&#X!&_dT2Sb)<;eG^)3NZg`~50VE_eBWefwg8p;^(`5LrJH4|S?p`M5ZvH8-b
    zyruwNB#MI7$Xd+|W7zJSGBlo>E>raTA5AE()sOd5A8k-LM>_`>|9m=94E`{tJgRsd
    zDBlY;p5g0ICF?Ezz$42$=U%<J;aXh=lT4LIJwgWYvP2bM?a>MW9noLR1Xx(hDYr5<
    zxPg6N(>4?U@1hA;w&ZS@ipuWLx>TPE{-bDA%Rg*AJE+!j>~f_?O#dh2Ynu<)y41t^
    zJ6E*!gDR8NhW$#ObQ=WDT_XNui!3#2=7o))@~L6&c|l4#g6(-y$RY#cLTO}D45QIi
    z9q)=urf*mhQQO^}oX!^1m8BMV7o)*QB74dTOAH>Mh8qxmI-*`TFkJw%)jNuu%KQaC
    zOSg%|IG&JscCjKi^5>1?3#fo9!pd5Q6;j`D<qVzDJL*+68}*GEG9mwa6>Xo|LTLx#
    zsq72M899H$rKYU3UM3}FOGBdUd^AB(=nI)@>30oBUR``{!*4QbXCsdq2rHwGJb&;d
    zqEsk>@tvLc?=(_ot_rR!Wd?&F9dsM~DycY`eODFUHa24A0u?mEg1~VR1W(;E;Tm*8
    zOjPl3=*+Br^W_Cc{&WQ9YnmAh#FhB<)Nq0#wZZM#22IN?eZ?8P$S1C2=jro>Wg^xD
    ziDfSm@uxYk^aH1SdHE{V>(`^kk{vUTl5!_Nk@Yg^iqG_#vc7bxM`odPZx2-JpLEt6
    zVG8qgc=SBem_{U}OO;`M*6Oou;8f4?HnacmQ%3Trre8GXc0R0eOH_F^EwLZ>Pp_AT
    z+mlBz_o3*%#ArY=8(WrS8Bz1njdSW0J|v?t57BMmgZM<`Fd4*g3u2*_Yc5}rtj-v4
    zqF^=wWu&P2XReGbwQ;IGB^7F(4|}-avGgmIWNnYSdt0Yf@;{xxYdO}t{-T2A$=Wgt
    z26@)i&~6(cw)=(*CT3EQevq#wgxu32^fQ<wmN<~@YE5IQX#S79q2silBK4qR7JtAd
    zwC3{A%1f;{JH>!K;aP<$3uLHLvIqv0UH6HvI+u+5l*n~s=HzFcI8Pk63Fu3ub>Sw2
    ze)%W%WNv6RA7J}O74li_JZdSJc%E)=CZgEV%Pu53Dc|1Fsk#{HFwBkB3@Okm3Xy)&
    zuZi*X!t4O9N2)ZXN{OMW)+2|OW}UAd0eCUsO!asf62HgzV9@pbiz_17_9GHlRNq;<
    zz0$6Q%nNUvdnQ7jdrfSLER?4;>~$Lwd6hj8?jlj(G{UpPyOR)PD@a3	u3AYf^fi
    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
    N002ovPDHLkV1j0UxYhsw
    
    
    From acba3788ed08ed588bcf8e9cbe7afc3cfc467cb9 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Mon, 28 May 2012 22:02:05 +0200
    Subject: [PATCH 081/207] Quality list not wide enough. fix #351
    
    ---
     couchpotato/core/plugins/movie/static/movie.css | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css
    index 6365f798..f626f2ab 100644
    --- a/couchpotato/core/plugins/movie/static/movie.css
    +++ b/couchpotato/core/plugins/movie/static/movie.css
    @@ -89,7 +89,7 @@
     					font-size: 16px;
     					font-weight: normal;
     					text-overflow: ellipsis;
    -					width: 64%;
    +					width: auto;
     				}
     
     			.movies .info .year {
    @@ -152,7 +152,6 @@
     			.movies .list_view .data .quality, .movies .mass_edit_view .data .quality {
     				text-align: right;
     				float: right;
    -				width: 30%;
     			}
     
     		.movies .data .quality .available, .movies .data .quality .snatched {
    @@ -200,6 +199,7 @@
     		.movies .list_view .data:hover .actions, .movies .mass_edit_view .data:hover .actions {
     			margin: -34px 2px 0 0;
     			background: #4e5969;
    +			position: relative;
     		}
     
     		.movies .delete_container {
    
    From e62b177940f1b41555804f1f8d5263672712fdb3 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Mon, 28 May 2012 22:05:40 +0200
    Subject: [PATCH 082/207] Add cd number to renaming options
    
    ---
     couchpotato/core/plugins/renamer/__init__.py | 2 ++
     1 file changed, 2 insertions(+)
    
    diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py
    index 27566d17..9ceccb8b 100644
    --- a/couchpotato/core/plugins/renamer/__init__.py
    +++ b/couchpotato/core/plugins/renamer/__init__.py
    @@ -20,6 +20,8 @@ rename_options = {
             'original': 'Original filename',
             'original_folder': 'Original foldername',
             'imdb_id': 'IMDB id (tt0123456)',
    +        'cd': 'CD number (cd1)',
    +        'cd_nr': 'Just the cd nr. (1)',
         },
     }
     
    
    From ef8dfc082c49de9d347999f5e1557f8cd916b2ec Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Mon, 28 May 2012 22:09:21 +0200
    Subject: [PATCH 083/207] Don't get last of empty list
    
    ---
     couchpotato/core/notifications/core/static/notification.js | 3 ++-
     1 file changed, 2 insertions(+), 1 deletion(-)
    
    diff --git a/couchpotato/core/notifications/core/static/notification.js b/couchpotato/core/notifications/core/static/notification.js
    index 4cf71e00..d11b6400 100644
    --- a/couchpotato/core/notifications/core/static/notification.js
    +++ b/couchpotato/core/notifications/core/static/notification.js
    @@ -136,7 +136,8 @@ var NotificationBase = new Class({
     				App.fireEvent(result.type, result)
     			})
     
    -			self.last_id = json.result.getLast().message_id
    +			if(json.result.length > 0)
    +				self.last_id = json.result.getLast().message_id
     		}
     
     		// Restart poll
    
    From d935fbb82b1f3a2d9fb44ec6a7c7695947bc4cc3 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Tue, 29 May 2012 22:02:06 +0200
    Subject: [PATCH 084/207] Navigate renaming choices with keyboard
    
    ---
     couchpotato/static/scripts/page/settings.js | 106 +++++++++++++++++++-
     couchpotato/static/style/page/settings.css  |  14 +--
     2 files changed, 112 insertions(+), 8 deletions(-)
    
    diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js
    index 39ef9465..8bf04053 100644
    --- a/couchpotato/static/scripts/page/settings.js
    +++ b/couchpotato/static/scripts/page/settings.js
    @@ -909,6 +909,7 @@ Option.Choice = new Class({
     						var input = self.tag_input.getElement('li:last-child input');
     						input.fireEvent('focus');
     						input.focus();
    +						input.setCaretPosition(input.get('value').length);
     					}
     
     					self.el.addEvent('outerClick', function(){
    @@ -965,6 +966,12 @@ Option.Choice = new Class({
     			'onChange': self.setOrder.bind(self),
     			'onBlur': function(){
     				self.addLastTag();
    +			},
    +			'onGoLeft': function(){
    +				self.goLeft(this)
    +			},
    +			'onGoRight': function(){
    +				self.goRight(this)
     			}
     		});
     		$(tag).inject(self.tag_input);
    @@ -979,6 +986,30 @@ Option.Choice = new Class({
     		return tag;
     	},
     
    +	goLeft: function(from_tag){
    +		var self = this;
    +
    +		from_tag.blur();
    +
    +		var prev_index = self.tags.indexOf(from_tag)-1;
    +		if(prev_index >= 0)
    +			self.tags[prev_index].selectFrom('right')
    +		else
    +			from_tag.focus();
    +
    +	},
    +	goRight: function(from_tag){
    +		var self = this;
    +
    +		from_tag.blur();
    +
    +		var next_index = self.tags.indexOf(from_tag)+1;
    +		if(next_index < self.tags.length)
    +			self.tags[next_index].selectFrom('left')
    +		else
    +			from_tag.focus();
    +	},
    +
     	setOrder: function(){
     		var self = this;
     
    @@ -1059,7 +1090,16 @@ Option.Choice.Tag = new Class({
     					'width': 0
     				},
     				'events': {
    -					'keyup': self.is_choice ? null : function(){
    +					'keyup': self.is_choice ? null : function(e){
    +						var current_caret_pos = self.input.getCaretPosition();
    +						if(e.key == 'left' && current_caret_pos == self.last_caret_pos){
    +							self.fireEvent('goLeft');
    +						}
    +						else if (e.key == 'right' && self.last_caret_pos === current_caret_pos){
    +							self.fireEvent('goRight');
    +						}
    +						self.last_caret_pos = self.input.getCaretPosition();
    +
     						self.setWidth();
     						self.fireEvent('change');
     					},
    @@ -1081,8 +1121,70 @@ Option.Choice.Tag = new Class({
     
     	},
     
    +	blur: function(){
    +		var self = this;
    +
    +		self.input.blur();
    +
    +		self.selected = false;
    +		self.el.removeClass('selected');
    +		self.input.removeEvents('outerClick');
    +	},
    +
     	focus: function(){
    -		this.input.focus();
    +		var self = this;
    +		if(!self.is_choice){
    +			this.input.focus();
    +		}
    +		else {
    +			if(self.selected) return;
    +			self.selected = true;
    +			self.el.addClass('selected');
    +			self.input.addEvent('outerClick', self.blur.bind(self));
    +
    +			var temp_input = new Element('input', {
    +				'events': {
    +					'keyup': function(e){
    +						e.stop();
    +
    +						if(e.key == 'right'){
    +							self.fireEvent('goRight');
    +							this.destroy();
    +						}
    +						else if (e.key == 'left'){
    +							self.fireEvent('goLeft');
    +							this.destroy();
    +						}
    +						else if (e.key == 'backspace'){
    +							self.del();
    +							this.destroy();
    +						}
    +					}
    +				},
    +				'styles': {
    +					'height': 0,
    +					'width': 0,
    +					'position': 'absolute',
    +					'top': -200
    +				}
    +			});
    +			self.el.adopt(temp_input)
    +			temp_input.focus();
    +		}
    +	},
    +
    +	selectFrom: function(direction){
    +		var self = this;
    +
    +		if(!direction || self.is_choice){
    +			self.focus();
    +		}
    +		else {
    +			self.focus();
    +			var position = direction == 'left' ? 0 : self.input.get('value').length;
    +			self.input.setCaretPosition(position);
    +		}
    +
     	},
     
     	setWidth: function(){
    diff --git a/couchpotato/static/style/page/settings.css b/couchpotato/static/style/page/settings.css
    index d85279be..59a03c82 100644
    --- a/couchpotato/static/style/page/settings.css
    +++ b/couchpotato/static/style/page/settings.css
    @@ -362,28 +362,29 @@
     				border-radius: 2px;
     			}
     			.page .tag_input > ul:hover > li.choice {
    -				background: url('../../images/sprite.png') no-repeat 94% -53px, -webkit-gradient(
    +				background: -webkit-gradient(
     				    linear,
     				    left bottom,
     				    left top,
     				    color-stop(0, rgba(255,255,255,0.1)),
     				    color-stop(1, rgba(255,255,255,0.3))
     				);
    -				background: url('../../images/sprite.png') no-repeat 94% -53px, -moz-linear-gradient(
    +				background: -moz-linear-gradient(
     				    center top,
     				    rgba(255,255,255,0.3) 0%,
     				    rgba(255,255,255,0.1) 100%
     				);
     			}
    -			.page .tag_input > ul > li.choice:hover {
    -				background: url('../../images/sprite.png') no-repeat 94% -53px, -webkit-gradient(
    +			.page .tag_input > ul > li.choice:hover,
    +			.page .tag_input > ul > li.choice.selected {
    +				background: -webkit-gradient(
     				    linear,
     				    left bottom,
     				    left top,
     				    color-stop(0, #406db8),
     				    color-stop(1, #5b9bd1)
     				);
    -				background: url('../../images/sprite.png') no-repeat 94% -53px, -moz-linear-gradient(
    +				background: -moz-linear-gradient(
     				    center top,
     				    #5b9bd1 0%,
     				    #406db8 100%
    @@ -436,7 +437,8 @@
     				);
     				background-size: 65%;
     			}
    -			.page .tag_input .choice:hover .delete { display: inline-block; }
    +			.page .tag_input .choice:hover .delete, 
    +			.page .tag_input .choice.selected .delete { display: inline-block; }
     			.page .tag_input .choice .delete:hover {
     				height: 14px;
     				margin-top: -13px;
    
    From 95e5282e2c2c2ebb775cfe3f0256003d71943ddc Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Tue, 29 May 2012 22:38:49 +0200
    Subject: [PATCH 085/207] Delete all movies from view in mass edit. fix #371
    
    ---
     couchpotato/core/plugins/movie/static/list.js | 9 +++++++--
     1 file changed, 7 insertions(+), 2 deletions(-)
    
    diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js
    index 208c4e28..52afbc20 100644
    --- a/couchpotato/core/plugins/movie/static/list.js
    +++ b/couchpotato/core/plugins/movie/static/list.js
    @@ -287,14 +287,19 @@ var MovieList = new Class({
     						'onSuccess': function(){
     							qObj.close();
     
    +							var erase_movies = [];
     							self.movies.each(function(movie){
     								if (movie.isSelected()){
     									$(movie).destroy()
    -									self.movies.erase(movie)
    +									erase_movies.include(movie)
     								}
     							});
     
    -							self.calculateSelected()
    +							erase_movies.each(function(movie){
    +								self.movies.erase(movie);
    +							});
    +
    +							self.calculateSelected();
     						}
     					});
     
    
    From 4bd2ba2b8dc733870117241dc59b42fa67580f08 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Thu, 31 May 2012 20:11:34 +0200
    Subject: [PATCH 086/207] Decode html tags in log. fix #386
    
    ---
     couchpotato/core/plugins/log/static/log.js | 6 +++++-
     1 file changed, 5 insertions(+), 1 deletion(-)
    
    diff --git a/couchpotato/core/plugins/log/static/log.js b/couchpotato/core/plugins/log/static/log.js
    index 1668ded6..7a91d4cf 100644
    --- a/couchpotato/core/plugins/log/static/log.js
    +++ b/couchpotato/core/plugins/log/static/log.js
    @@ -45,7 +45,7 @@ Page.Log = new Class({
     				new Fx.Scroll(window, {'duration': 0}).toBottom();
     
     				var nav = new Element('ul.nav').inject(self.log, 'top');
    -				for (var i = 0; i < json.total; i++) {
    +				for (var i = 0; i <= json.total; i++) {
     					new Element('li', {
     						'text': i+1,
     						'class': nr == i ? 'active': '',
    @@ -78,6 +78,10 @@ Page.Log = new Class({
     	addColors: function(text){
     		var self = this;
     
    +		var text = new Element('div', {
    +			'html': text
    +		}).get('text')
    +
     		text = text.replace(/\u001b\[31m/gi, '</span><span class="error">')
     		text = text.replace(/\u001b\[36m/gi, '</span><span class="debug">')
     		text = text.replace(/\u001b\[33m/gi, '</span><span class="debug">')
    
    From 3790b04de4860118934d9d46c0cd6c8ecf8177b1 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Thu, 31 May 2012 20:24:07 +0200
    Subject: [PATCH 087/207] FreeBSD init
    
    commit 0bd182ebb5d9da183d7d3950c77418cf02dd8706
    Merge: 5fac3d7 dadbd0f
    Author: Ruud <ruud@crashdummy.nl>
    Date:   Thu May 31 20:21:17 2012 +0200
    
        Merge branch 'master' of https://github.com/jallakim/CouchPotatoServer into jallakim-master
    
        Conflicts:
        	init/freebsd
    
    commit 5fac3d708d2af21e5834aea9bb7224139beb75f0
    Merge: 0ba2e57 95e5282
    Author: Ruud <ruud@crashdummy.nl>
    Date:   Thu May 31 19:30:18 2012 +0200
    
        Merge branch 'refs/heads/develop'
    
    commit dadbd0f1fd61fe6c8a233b823bbf4ed29a9032f0
    Author: Joachim Tingvold <joachim@tingvold.com>
    Date:   Wed May 30 22:35:27 2012 +0200
    
        Useless Use of Cat Award (-:
    
    commit 9fe11355a9671a1f38a6b982b50034f075c34b8d
    Author: Joachim Tingvold <joachim@tingvold.com>
    Date:   Wed May 30 22:30:26 2012 +0200
    
        Ooops. PORT and CPAPI got mixed around in FreeBSD init-script.
    
    commit e39be8970ca0a4365ff15e6d8b3d64864c2bbdc4
    Author: Joachim Tingvold <joachim@tingvold.com>
    Date:   Wed May 30 22:25:46 2012 +0200
    
        Cosmetics again.
    
    commit 5cca98db31caaf6ca7032a3d948c4feb6ca30e2e
    Author: Joachim Tingvold <joachim@tingvold.com>
    Date:   Wed May 30 22:22:54 2012 +0200
    
        Check that settings.conf exists in FreeBSD init-script
    
    commit d1e98602d8d86200431ff1d109d89853fb6ea243
    Author: Joachim Tingvold <joachim@tingvold.com>
    Date:   Wed May 30 22:16:47 2012 +0200
    
        FreeBSD init-script fetches variables from CP's settings.conf
    
    commit 42dbe607cfeca13efc76e6db7c5df4907bf7fc80
    Author: Joachim Tingvold <joachim@tingvold.com>
    Date:   Wed May 30 16:02:46 2012 +0200
    
        Should work for all FreeBSD versions now.
    
    commit f0bcf6a0f5c401a1f96f98ef35b82c3e45f05e11
    Author: Joachim Tingvold <joachim@tingvold.com>
    Date:   Wed May 30 15:17:53 2012 +0200
    
        Don't 'import sys' before we need it
    
    commit 8e13fcd0d137e304b5c49aaec4397ca58c2ef431
    Author: Joachim Tingvold <joachim@tingvold.com>
    Date:   Wed May 30 15:09:58 2012 +0200
    
        Cosmetics! (-:
    
    commit 04af454c0362dcabb1cec06863f1defabf7a3736
    Author: Joachim Tingvold <joachim@tingvold.com>
    Date:   Wed May 30 15:05:32 2012 +0200
    
        Added /data/ to .gitignore
    
        Used as data-dir if CouchPotato is installed on FreeBSD.
    
    commit 055d47d7b4a9401fcab1e4cdb319c3e95a91e1ca
    Author: Joachim Tingvold <joachim@tingvold.com>
    Date:   Wed May 30 15:04:36 2012 +0200
    
        Fixed getDataDir so that it works for FreeBSD9.
    
        Assumes that '/usr/local/couchpotato/' is where CouchPotato resides.
    
    commit 4859f1b9f23e1d994e7fca24e25bae0226717006
    Author: Joachim Tingvold <joachim@tingvold.com>
    Date:   Wed May 30 15:03:26 2012 +0200
    
        Fixed the FreeBSD init-script.
    
    commit 0ba2e579f9658dfbf7ab65245bee9a04d1c09473
    Merge: fdf0d2d a2a3896
    Author: Ruud <ruud@crashdummy.nl>
    Date:   Sun May 20 23:50:54 2012 +0200
    
        Merge branch 'master' of github.com:RuudBurger/CouchPotatoServer
    
    commit fdf0d2d5b403bad860a06475b1dafce7c28e2da5
    Author: Ruud Burger <ruud@crashdummy.nl>
    Date:   Thu Apr 26 10:57:34 2012 +0300
    
        Use master branch to update master..
    
    commit a2a3896b1fe16bdc4d7d32af330a123645e93dba
    Merge: 5017b01 19640a9
    Author: Ruud <ruud@crashdummy.nl>
    Date:   Sun May 20 19:57:05 2012 +0200
    
        Merge branch 'refs/heads/develop'
    
    commit 5017b019344b19a2e7dafe7c3114d01306166722
    Merge: d4a5483 f2f524c
    Author: Ruud <ruud@crashdummy.nl>
    Date:   Tue May 15 23:23:25 2012 +0200
    
        Merge branch 'master' of github.com:RuudBurger/CouchPotatoServer
    
    commit d4a5483fa034d960bb7160a076ee11507e3bdc78
    Author: Ruud Burger <ruud@crashdummy.nl>
    Date:   Thu Apr 26 10:57:34 2012 +0300
    
        Use master branch to update master..
    
    commit f2f524c4e446814448a4a1a3c8e385b8beb1e5c6
    Merge: 577aed3 edbeb02
    Author: Ruud <ruud@crashdummy.nl>
    Date:   Tue May 15 23:16:12 2012 +0200
    
        Merge branch 'refs/heads/develop'
    
    commit 577aed3a754ae783c3224bbc8e051164f981309f
    Merge: cb975f6 176f4b0
    Author: Ruud <ruud@crashdummy.nl>
    Date:   Mon May 14 23:36:53 2012 +0200
    
        Merge branch 'refs/heads/develop'
    
    commit cb975f61801f83cc8b16f3785195eb23cc967aff
    Merge: 3e369c5 a71fb1a
    Author: Ruud <ruud@crashdummy.nl>
    Date:   Mon May 14 20:23:05 2012 +0200
    
        Merge branch 'refs/heads/develop'
    
    commit 3e369c583851d14fe5d6cd292c63a4c1e603f634
    Merge: d89130d 0847ffd
    Author: Ruud <ruud@crashdummy.nl>
    Date:   Sun May 13 12:56:37 2012 +0200
    
        Merge branch 'refs/heads/develop'
    
    commit d89130dc308a1017b6d9bb0038d0d615a333ad8f
    Merge: ee5e1d1 c34f392
    Author: Ruud <ruud@crashdummy.nl>
    Date:   Sat May 12 00:36:03 2012 +0200
    
        Merge branch 'refs/heads/develop'
    
    commit ee5e1d19d1e19f7ad4b3cc33e8642f606b3f4043
    Merge: 7d2eb23 b2be9ef
    Author: Ruud <ruud@crashdummy.nl>
    Date:   Wed May 9 22:19:29 2012 +0200
    
        Merge branch 'refs/heads/develop'
    
    commit 7d2eb2376f55cec38b1fd581b1df74e97d6e75f6
    Merge: 40ff984 ba8fef3
    Author: Ruud Burger <ruud@crashdummy.nl>
    Date:   Mon May 7 13:35:13 2012 -0700
    
        Merge pull request #228 from sirchia/twitterDirectMessage
    
        Added support for direct messages in twitter notifications
    
    commit 40ff984e192a4d7d7dcf177d009960073ea49346
    Merge: 1f3e22c 7ca509c
    Author: Ruud <ruud@crashdummy.nl>
    Date:   Mon May 7 22:26:55 2012 +0200
    
        Merge branch 'refs/heads/develop'
    
    commit ba8fef3c87484e4898a196ef9b25c69be68f9825
    Author: Riccardo Sirchia <sirchia.r@gmail.com>
    Date:   Sun May 6 18:02:51 2012 +0200
    
        Added support for direct messages in twitter notifications
    
    commit 1f3e22c4ed4e36c809045beae404cdc0a941fda9
    Merge: 89315cb 74226f0
    Author: Ruud <ruud@crashdummy.nl>
    Date:   Fri May 4 17:27:36 2012 +0200
    
        Merge branch 'refs/heads/develop'
    
    commit 89315cb2912bcdf4d8ac4f375a562f7d8966acd5
    Merge: 829b3cf f6de117
    Author: Ruud <ruud@crashdummy.nl>
    Date:   Fri May 4 17:27:12 2012 +0200
    
        Merge branch 'refs/heads/develop'
    
    commit 829b3cfb3bd7d9a8d4b6d4ac09231b4c735fc1d1
    Merge: e3cc15d 1345e98
    Author: Ruud <ruud@crashdummy.nl>
    Date:   Wed May 2 21:40:01 2012 +0200
    
        Merge branch 'refs/heads/develop'
    
    commit e3cc15d2b638ed88404f3f1902361249fb306245
    Merge: ca87f2c c161bdd
    Author: Ruud <ruud@crashdummy.nl>
    Date:   Sun Apr 29 00:00:14 2012 +0200
    
        Merge branch 'refs/heads/develop'
    
    commit ca87f2c231eb5095efba749c39a221e274c238fb
    Merge: e929af7 c7bc0f4
    Author: Ruud <ruud@crashdummy.nl>
    Date:   Sat Apr 28 23:14:18 2012 +0200
    
        Merge branch 'refs/heads/develop'
    
    commit e929af76d056af7210d09e807ce8a17305003407
    Merge: b22763b d1c2869
    Author: Ruud Burger <ruud@crashdummy.nl>
    Date:   Fri Apr 27 02:04:35 2012 -0700
    
        Merge pull request #152 from garlandkr/master
    
        Removed group setting for init
    
    commit d1c2869f2cb4b3fac555cce732e92bb8fec97363
    Author: Ken Garland <garlandkr@gmail.com>
    Date:   Thu Apr 26 23:30:33 2012 -0300
    
        Removed RUN_AS for group, don't assume the default group is the same as the username. Specifying group is not needed anyways.
    
    commit b22763b37d7780140698725a0ed2632e2eb7a7c2
    Author: Ruud Burger <ruud@crashdummy.nl>
    Date:   Thu Apr 26 10:57:34 2012 +0300
    
        Use master branch to update master..
    ---
     .gitignore                           |  3 ++-
     couchpotato/core/helpers/variable.py |  5 +++++
     init/freebsd                         | 29 ++++++++++++++++++++--------
     3 files changed, 28 insertions(+), 9 deletions(-)
    
    diff --git a/.gitignore b/.gitignore
    index f903669d..bd4bca0a 100644
    --- a/.gitignore
    +++ b/.gitignore
    @@ -1 +1,2 @@
    -/_source/
    \ No newline at end of file
    +/_source/
    +/data/
    diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py
    index 9a4bdc13..493ac79f 100644
    --- a/couchpotato/core/helpers/variable.py
    +++ b/couchpotato/core/helpers/variable.py
    @@ -5,6 +5,7 @@ import platform
     import random
     import re
     import string
    +import sys
     
     log = CPLog(__name__)
     
    @@ -22,6 +23,10 @@ def getDataDir():
         if 'darwin' in platform.platform().lower():
             return os.path.join(user_dir, 'Library', 'Application Support', 'CouchPotato')
     
    +    # FreeBSD
    +    if 'freebsd' in sys.platform:
    +        return os.path.join('/usr/local/', 'couchpotato', 'data')
    +
         # Linux
         return os.path.join(user_dir, '.couchpotato')
     
    diff --git a/init/freebsd b/init/freebsd
    index e3cf408e..eeba51d3 100644
    --- a/init/freebsd
    +++ b/init/freebsd
    @@ -31,14 +31,27 @@ load_rc_config ${name}
     : ${couchpotato_user:="_sabnzbd"}
     : ${couchpotato_dir:="/usr/local/couchpotato"}
     : ${couchpotato_chdir:="${couchpotato_dir}"}
    -: ${couchpotato_pid:="/var/run/couchpotato.pid"}
    +: ${couchpotato_pid:="${couchpotato_dir}/couchpotato.pid"}
    +: ${couchpotato_conf:="${couchpotato_dir}/data/settings.conf"}
    +
    +WGET="/usr/local/bin/wget" # You need wget for this script to safely shutdown CouchPotato.
    +if [ -e "${couchpotato_conf}" ]; then
    +        HOST=`grep -A14 "\[core\]" "${couchpotato_conf}"|egrep "^host"|perl -wple 's/^host = (.*)$/$1/'`
    +        PORT=`grep -A14 "\[core\]" "${couchpotato_conf}"|egrep "^port"|perl -wple 's/^port = (.*)$/$1/'`
    +        CPAPI=`grep -A14 "\[core\]" "${couchpotato_conf}"|egrep "^api_key"|perl -wple 's/^api_key = (.*)$/$1/'`
    +fi
     
    -pidfile="${couchpotato_pid}"
     status_cmd="${name}_status"
     stop_cmd="${name}_stop"
     
     command="/usr/sbin/daemon"
    -command_args="-f -p ${couchpotato_pid} python ${couchpotato_dir}/CouchPotato.py ${couchpotato_flags} --pid_file=${couchpotato_pid}"
    +command_args="-f -p ${couchpotato_pid} python ${couchpotato_dir}/CouchPotato.py ${couchpotato_flags}"
    +
    +# Check for wget and refuse to start without it.
    +if [ ! -x "${WGET}" ]; then
    +  warn "couchpotato not started: You need wget to safely shut down CouchPotato."
    +  exit 1
    +fi
     
     # Ensure user is root when running this script.
     if [ `id -u` != "0" ]; then
    @@ -55,17 +68,17 @@ verify_couchpotato_pid() {
     
     # Try to stop CouchPotato cleanly by calling shutdown over http.
     couchpotato_stop() {
    -    
    +    if [ ! -e "${couchpotato_conf}" ]; then
    +        echo "CouchPotato's settings file does not exist. Try starting CouchPotato, as this should create the file."
    +        exit 1
    +    fi
         echo "Stopping $name"
         verify_couchpotato_pid
    -
    +    ${WGET} -O - -q "http://${HOST}:${PORT}/api/${CPAPI}/app.shutdown/" >/dev/null
         if [ -n "${pid}" ]; then
    -      kill -SIGTERM ${pid} 2> /dev/null
           wait_for_pids ${pid}
    -      kill -9 ${pid} 2> /dev/null
           echo "Stopped"
         fi
    -
     }
     
     couchpotato_status() {
    
    From 5fb0dbf5d99b8a5af3c78340073265fabd66149c Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Thu, 31 May 2012 22:09:42 +0200
    Subject: [PATCH 088/207] Show active page. fix #373
    
    ---
     .../static/scripts/block/navigation.js        | 12 +++++++--
     couchpotato/static/scripts/page.js            |  3 ++-
     couchpotato/static/scripts/page/settings.js   |  2 ++
     couchpotato/static/style/main.css             | 27 ++++++++++++-------
     4 files changed, 31 insertions(+), 13 deletions(-)
    
    diff --git a/couchpotato/static/scripts/block/navigation.js b/couchpotato/static/scripts/block/navigation.js
    index b6886f8d..85f20c49 100644
    --- a/couchpotato/static/scripts/block/navigation.js
    +++ b/couchpotato/static/scripts/block/navigation.js
    @@ -32,13 +32,21 @@ Block.Navigation = new Class({
     
     	},
     
    -	addTab: function(tab){
    +	addTab: function(name, tab){
     		var self = this
     
    -		return new Element('li.tab_'+(tab.text.toLowerCase() || 'unknown')).adopt(
    +		return new Element('li.tab_'+(name || 'unknown')).adopt(
     			new Element('a', tab)
     		).inject(self.nav)
     
    +	},
    +
    +	activate: function(name){
    +		var self = this;
    +
    +		self.nav.getElements('.active').removeClass('active');
    +		self.nav.getElements('.tab_'+name).addClass('active');
    +
     	}
     
     });
    \ No newline at end of file
    diff --git a/couchpotato/static/scripts/page.js b/couchpotato/static/scripts/page.js
    index 589fa3ed..1af800e8 100644
    --- a/couchpotato/static/scripts/page.js
    +++ b/couchpotato/static/scripts/page.js
    @@ -20,7 +20,7 @@ var PageBase = new Class({
     		// Create tab for page
     		if(self.has_tab){
     			var nav = App.getBlock('navigation');
    -			self.tab = nav.addTab({
    +			self.tab = nav.addTab(self.name, {
     				'href': App.createUrl(self.name),
     				'title': self.title,
     				'text': self.name.capitalize()
    @@ -39,6 +39,7 @@ var PageBase = new Class({
     				self.el.adopt(elements);
     			}
     
    +			App.getBlock('navigation').activate(self.name);
     			self.fireEvent('opened');
     		}
     		catch (e){
    diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js
    index 8bf04053..91848556 100644
    --- a/couchpotato/static/scripts/page/settings.js
    +++ b/couchpotato/static/scripts/page/settings.js
    @@ -34,6 +34,8 @@ Page.Settings = new Class({
     		else {
     			self.openTab(action);
     		}
    +
    +		App.getBlock('navigation').activate(self.name);
     	},
     
     	openTab: function(action){
    diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css
    index 884d8724..679748b6 100644
    --- a/couchpotato/static/style/main.css
    +++ b/couchpotato/static/style/main.css
    @@ -203,18 +203,25 @@ body > .spinner, .mask{
     				display: block;
     				margin-top: 5px;
     			}
    -
    -		.header .navigation li.disabled {
    -			color: #e5e5e5;
    +		
    +		.header .navigation li a:after {
    +			content: '';
    +			display: inline-block;
    +			height: 2px;
    +			width: 100%;
    +			position: relative;
    +			top: -20px;
    +			background-color: #46505e;
    +			outline: none;
    +			box-shadow: inset 0 1px 8px rgba(0,0,0,0.05), 0 1px 0px rgba(255,255,255,0.15);
    +			transition: all .4s cubic-bezier(0.9,0,0.1,1);
     		}
    +		
    +		.header .navigation li:hover a:after { background-color: #047792; }
    +		.header .navigation li.active a:after { background-color: #04bce6; }
     
    -		.header .navigation li a:link, .header .navigation li a:visited {
    -			color: #fff;
    -		}
    -
    -		.header .navigation li a:hover, .header .navigation li a:active {
    -			color: #b1d8dc;
    -		}
    +		.header .navigation li.disabled { color: #e5e5e5; }
    +		.header .navigation li a { color: #fff; }
     
     		.header .navigation .backtotop {
     			opacity: 0;
    
    From 136bc30d928158b4be39698b98e388aa57dea173 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Thu, 31 May 2012 22:40:17 +0200
    Subject: [PATCH 089/207] Broken updater
    
    ---
     couchpotato/core/_base/updater/main.py    | 2 +-
     couchpotato/static/scripts/couchpotato.js | 2 +-
     2 files changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py
    index 5d2e7e2e..0b03b4c9 100644
    --- a/couchpotato/core/_base/updater/main.py
    +++ b/couchpotato/core/_base/updater/main.py
    @@ -78,7 +78,7 @@ class Updater(Plugin):
         def doUpdateView(self):
     
             self.check()
    -        if not self.update_version:
    +        if not self.updater.update_version:
                 log.error('Trying to update when no update is available.')
                 success = False
             else:
    diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js
    index b983fb5a..9a15bbab 100644
    --- a/couchpotato/static/scripts/couchpotato.js
    +++ b/couchpotato/static/scripts/couchpotato.js
    @@ -82,7 +82,7 @@ var CouchPotato = new Class({
     		new Element('a', {
     			'text': 'Check for updates',
     			'events': {
    -				'click': self.checkForUpdate.bind(self)
    +				'click': self.checkForUpdate.bind(self, null)
     			}
     		}),
     		new Element('a', {
    
    From ac5b2484f9fb95b9ad2491ee74f085d3e514b371 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Thu, 31 May 2012 23:06:06 +0200
    Subject: [PATCH 090/207] Redirect to complete request url. fix #375
    
    ---
     couchpotato/__init__.py | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/couchpotato/__init__.py b/couchpotato/__init__.py
    index 3e283639..c3b1bd81 100644
    --- a/couchpotato/__init__.py
    +++ b/couchpotato/__init__.py
    @@ -69,10 +69,10 @@ def getApiKey():
     @app.errorhandler(404)
     def page_not_found(error):
         index_url = url_for('web.index')
    -    url = getattr(request, 'path')[len(index_url):]
    +    url = request.path[len(index_url):]
     
         if url[:3] != 'api':
    -        return redirect(index_url + '#' + url)
    +        return redirect(request.url.replace(request.path, index_url + '#' + url))
         else:
             time.sleep(0.1)
             return 'Wrong API key used', 404
    
    From 2010c41ef7588880960179a03db99752a5f109a4 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 3 Jun 2012 12:55:37 +0200
    Subject: [PATCH 091/207] Put update message on top again
    
    ---
     couchpotato/static/style/main.css | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css
    index 679748b6..d7011fc1 100644
    --- a/couchpotato/static/style/main.css
    +++ b/couchpotato/static/style/main.css
    @@ -311,7 +311,7 @@ body > .spinner, .mask{
     	.header .message.update {
     		text-align: center;
     		position: relative;
    -		top: -70px;
    +		top: -100px;
     		padding: 2px 0;
     		background: #ff6134;
     		font-size: 12px;
    
    From 16e4ed6c5df8f007433999d20c5aba03b40400f1 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 3 Jun 2012 23:28:12 +0200
    Subject: [PATCH 092/207] Remove ppdir option for sabnzbd
    
    ---
     .../core/downloaders/sabnzbd/__init__.py      |  6 --
     couchpotato/core/downloaders/sabnzbd/main.py  | 66 -------------------
     2 files changed, 72 deletions(-)
    
    diff --git a/couchpotato/core/downloaders/sabnzbd/__init__.py b/couchpotato/core/downloaders/sabnzbd/__init__.py
    index 23918fe2..ac0ce05c 100644
    --- a/couchpotato/core/downloaders/sabnzbd/__init__.py
    +++ b/couchpotato/core/downloaders/sabnzbd/__init__.py
    @@ -33,12 +33,6 @@ config = [{
                         'label': 'Category',
                         'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>',
                     },
    -                {
    -                    'advanced': True,
    -                    'name': 'pp_directory',
    -                    'type': 'directory',
    -                    'description': 'Your Post-Processing Script directory, set in Sabnzbd > Config > Directories.',
    -                },
                     {
                         'name': 'manual',
                         'default': 0,
    diff --git a/couchpotato/core/downloaders/sabnzbd/main.py b/couchpotato/core/downloaders/sabnzbd/main.py
    index d6c5af6c..d2c42856 100644
    --- a/couchpotato/core/downloaders/sabnzbd/main.py
    +++ b/couchpotato/core/downloaders/sabnzbd/main.py
    @@ -2,11 +2,6 @@ from couchpotato.core.downloaders.base import Downloader
     from couchpotato.core.helpers.encoding import tryUrlencode
     from couchpotato.core.helpers.variable import cleanHost
     from couchpotato.core.logger import CPLog
    -from inspect import ismethod, isfunction
    -from tempfile import mkstemp
    -import base64
    -import os
    -import re
     import traceback
     
     log = CPLog(__name__)
    @@ -22,19 +17,6 @@ class Sabnzbd(Downloader):
     
             log.info("Sending '%s' to SABnzbd." % data.get('name'))
     
    -        if self.conf('ppDir') and data.get('imdb_id'):
    -            try:
    -                pp_script_fn = self.buildPp(data.get('imdb_id'))
    -            except:
    -                log.info("Failed to create post-processing script.")
    -                pp_script_fn = False
    -            if not pp_script_fn:
    -                pp = False
    -            else:
    -                pp = True
    -        else:
    -            pp = False
    -
             params = {
                 'apikey': self.conf('api_key'),
                 'cat': self.conf('category'),
    @@ -53,9 +35,6 @@ class Sabnzbd(Downloader):
             else:
                 params['name'] = data.get('url')
     
    -        if pp:
    -            params['script'] = pp_script_fn
    -
             url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
     
             try:
    @@ -82,48 +61,3 @@ class Sabnzbd(Downloader):
             else:
                 log.error("Unknown error: " + result[:40])
                 return False
    -
    -    def buildPp(self, imdb_id):
    -
    -        pp_script_path = self.getPpFile()
    -
    -        scriptB64 = '''IyEvdXNyL2Jpbi9weXRob24KaW1wb3J0IG9zCmltcG9ydCBzeXMKcHJpbnQgIkNyZWF0aW5nIGNwLmNw
    -bmZvIGZvciAlcyIgJSBzeXMuYXJndlsxXQppbWRiSWQgPSB7W0lNREJJREhFUkVdfQpwYXRoID0gb3Mu
    -cGF0aC5qb2luKHN5cy5hcmd2WzFdLCAiY3AuY3BuZm8iKQp0cnk6CiBmID0gb3BlbihwYXRoLCAndycp
    -CmV4Y2VwdCBJT0Vycm9yOgogcHJpbnQgIlVuYWJsZSB0byBvcGVuICVzIGZvciB3cml0aW5nIiAlIHBh
    -dGgKIHN5cy5leGl0KDEpCnRyeToKIGYud3JpdGUob3MucGF0aC5iYXNlbmFtZShzeXMuYXJndlswXSkr
    -IlxuIitpbWRiSWQpCmV4Y2VwdDoKIHByaW50ICJVbmFibGUgdG8gd3JpdGUgdG8gZmlsZTogJXMiICUg
    -cGF0aAogc3lzLmV4aXQoMikKZi5jbG9zZSgpCnByaW50ICJXcm90ZSBpbWRiIGlkLCAlcywgdG8gZmls
    -ZTogJXMiICUgKGltZGJJZCwgcGF0aCkK'''
    -
    -        script = re.sub(r"\{\[IMDBIDHERE\]\}", "'%s'" % imdb_id, base64.b64decode(scriptB64))
    -
    -        try:
    -            f = open(pp_script_path, 'wb')
    -        except:
    -            log.info("Unable to open post-processing script for writing. Check permissions: %s" % pp_script_path)
    -            return False
    -
    -        try:
    -            f.write(script)
    -            f.close()
    -        except:
    -            log.info("Unable to write to post-processing script. Check permissions: %s" % pp_script_path)
    -            return False
    -
    -        log.info("Wrote post-processing script to: %s" % pp_script_path)
    -
    -        return os.path.basename(pp_script_path)
    -
    -    def getPpFile(self):
    -
    -        pp_script_handle, pp_script_path = mkstemp(suffix = '.py', dir = self.conf('ppDir'))
    -        pp_sh = os.fdopen(pp_script_handle)
    -        pp_sh.close()
    -
    -        try:
    -            os.chmod(pp_script_path, int('777', 8))
    -        except:
    -            log.info("Unable to set post-processing script permissions to 777 (may still work correctly): %s" % pp_script_path)
    -
    -        return pp_script_path
    
    From 90be81bc373b26be2a224206c6ab708c0873d4de Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sat, 9 Jun 2012 22:45:01 +0200
    Subject: [PATCH 093/207] User agent missing for nzbmatrix. fix #388
    
    ---
     couchpotato/core/providers/nzb/nzbmatrix/main.py | 3 +++
     1 file changed, 3 insertions(+)
    
    diff --git a/couchpotato/core/providers/nzb/nzbmatrix/main.py b/couchpotato/core/providers/nzb/nzbmatrix/main.py
    index b512790d..816b8e1a 100644
    --- a/couchpotato/core/providers/nzb/nzbmatrix/main.py
    +++ b/couchpotato/core/providers/nzb/nzbmatrix/main.py
    @@ -99,6 +99,9 @@ class NZBMatrix(NZBProvider, RSS):
     
             return results
     
    +    def download(self, url = '', nzb_id = ''):
    +        return self.urlopen(url, headers = {'User-Agent': Env.getIdentifier()})
    +
         def getApiExt(self):
             return '&username=%s&apikey=%s' % (self.conf('username'), self.conf('api_key'))
     
    
    From 483bf3307170574dbec56a306efb84efab7dfd63 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 10 Jun 2012 14:12:48 +0200
    Subject: [PATCH 094/207] Show totals. close #393
    
    ---
     couchpotato/core/plugins/movie/main.py          | 11 ++++++-----
     couchpotato/core/plugins/movie/static/list.js   | 16 ++++++++++++++--
     couchpotato/core/plugins/movie/static/movie.css | 16 +++++++++++-----
     3 files changed, 31 insertions(+), 12 deletions(-)
    
    diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py
    index bcd5d63e..f59ba0ba 100644
    --- a/couchpotato/core/plugins/movie/main.py
    +++ b/couchpotato/core/plugins/movie/main.py
    @@ -130,6 +130,8 @@ class MoviePlugin(Plugin):
                 .filter(or_(*[Movie.status.has(identifier = s) for s in status])) \
                 .group_by(Movie.id)
     
    +        total_count = q.count()
    +
             filter_or = []
             if starts_with:
                 starts_with = toUnicode(starts_with.lower())
    @@ -156,8 +158,7 @@ class MoviePlugin(Plugin):
                 .options(joinedload_all('library.titles')) \
                 .options(joinedload_all('library.files')) \
                 .options(joinedload_all('status')) \
    -            .options(joinedload_all('files')) \
    -
    +            .options(joinedload_all('files'))
     
             if limit_offset:
                 splt = [x.strip() for x in limit_offset.split(',')]
    @@ -165,7 +166,6 @@ class MoviePlugin(Plugin):
                 offset = 0 if len(splt) is 1 else splt[1]
                 q2 = q2.limit(limit).offset(offset)
     
    -
             results = q2.all()
             movies = []
             for movie in results:
    @@ -178,7 +178,7 @@ class MoviePlugin(Plugin):
                 movies.append(temp)
     
             #db.close()
    -        return movies
    +        return (total_count, movies)
     
         def availableChars(self, status = ['active']):
     
    @@ -214,11 +214,12 @@ class MoviePlugin(Plugin):
             starts_with = params.get('starts_with', None)
             search = params.get('search', None)
     
    -        movies = self.list(status = status, limit_offset = limit_offset, starts_with = starts_with, search = search)
    +        total_movies, movies = self.list(status = status, limit_offset = limit_offset, starts_with = starts_with, search = search)
     
             return jsonified({
                 'success': True,
                 'empty': len(movies) == 0,
    +            'total': total_movies,
                 'movies': movies,
             })
     
    diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js
    index 52afbc20..38fc7109 100644
    --- a/couchpotato/core/plugins/movie/static/list.js
    +++ b/couchpotato/core/plugins/movie/static/list.js
    @@ -72,7 +72,7 @@ var MovieList = new Class({
     		self.created = true;
     	},
     
    -	addMovies: function(movies){
    +	addMovies: function(movies, total){
     		var self = this;
     
     		if(!self.created) self.create();
    @@ -86,8 +86,19 @@ var MovieList = new Class({
     		Object.each(movies, function(movie){
     			self.createMovie(movie);
     		});
    +		
    +		self.setCounter(total);
     
     	},
    +	
    +	setCounter: function(count){
    +		var self = this;
    +		
    +		if(!self.navigation_counter) return;
    +		
    +		self.navigation_counter.set('text', (count || 0));
    +		
    +	},
     
     	createMovie: function(movie, inject_at){
     		var self = this;
    @@ -118,6 +129,7 @@ var MovieList = new Class({
     
     		self.navigation = new Element('div.alph_nav').adopt(
     			self.navigation_actions = new Element('ul.inlay.actions.reversed'),
    +			self.navigation_counter = new Element('span.counter[title=Total]'),
     			self.navigation_alpha = new Element('ul.numbers', {
     				'events': {
     					'click:relay(li)': function(e, el){
    @@ -443,7 +455,7 @@ var MovieList = new Class({
     			}, self.filter),
     			'onComplete': function(json){
     				self.store(json.movies);
    -				self.addMovies(json.movies);
    +				self.addMovies(json.movies, json.total);
     				self.load_more.set('text', 'load more movies');
     				if(self.scrollspy) self.scrollspy.start();
     			}
    diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css
    index f626f2ab..c2cae584 100644
    --- a/couchpotato/core/plugins/movie/static/movie.css
    +++ b/couchpotato/core/plugins/movie/static/movie.css
    @@ -338,7 +338,9 @@
     		background: #4e5969;
     	}
     
    -.movies .alph_nav ul.numbers, .movies .alph_nav ul.actions {
    +.movies .alph_nav ul.numbers, 
    +.movies .alph_nav .counter, 
    +.movies .alph_nav ul.actions {
     	list-style: none;
     	padding: 0 0 1px;
     	margin: 0;
    @@ -346,10 +348,15 @@
     	user-select: none;
     }
     
    +	.movies .alph_nav .counter {
    +		width: 60px;
    +		text-align: center;
    +	}
    +
     	.movies .alph_nav .numbers li, .movies .alph_nav .actions li {
     		display: inline-block;
     		vertical-align: top;
    -		width: 22px;
    +		width: 20px;
     		height: 24px;
     		line-height: 26px;
     		text-align: center;
    @@ -361,7 +368,6 @@
     	}
     		.movies .alph_nav .numbers li:first-child {
     			width: 43px;
    -			margin-left: 7px;
     		}
     		.movies .alph_nav li.available {
     			color: rgba(255,255,255,0.8);
    @@ -370,8 +376,8 @@
     		}
     		.movies .alph_nav li.active.available, .movies .alph_nav li.available:hover {
     			color: #fff;
    -			font-size: 24px;
    -			line-height: 24px;
    +			font-size: 20px;
    +			line-height: 20px;
     		}
     
     	.movies .alph_nav input {
    
    From eeaf4c5ac8948e88fe999150b3763823f8822016 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 10 Jun 2012 15:59:30 +0200
    Subject: [PATCH 095/207] Add movie to manage after download. fix #291 #395
    
    ---
     couchpotato/core/plugins/scanner/main.py | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py
    index 6fa3f7d5..b7104644 100644
    --- a/couchpotato/core/plugins/scanner/main.py
    +++ b/couchpotato/core/plugins/scanner/main.py
    @@ -89,9 +89,9 @@ class Scanner(Plugin):
             addEvent('scanner.partnumber', self.getPartNumber)
     
             def after_rename(group):
    -            return self.scanFilesToLibrary(self, folder = group['destination_dir'], files = group['renamed_files'])
    +            return self.scanFilesToLibrary(folder = group['destination_dir'], files = group['renamed_files'])
     
    -        addEvent('rename.after', after_rename)
    +        addEvent('renamer.after', after_rename)
     
         def scanFilesToLibrary(self, folder = None, files = None):
     
    
    From eafffaab7649a4159bf7c1952a3d55d3bb56964e Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 10 Jun 2012 17:08:15 +0200
    Subject: [PATCH 096/207] Folder name change bugs out when using same tag
     twice. fix #377
    
    ---
     couchpotato/static/scripts/page/settings.js | 19 ++++++++++++-------
     1 file changed, 12 insertions(+), 7 deletions(-)
    
    diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js
    index 91848556..ed75cfd7 100644
    --- a/couchpotato/static/scripts/page/settings.js
    +++ b/couchpotato/static/scripts/page/settings.js
    @@ -926,14 +926,19 @@ Option.Choice = new Class({
     		var mtches = []
     		if(matches)
     			matches.each(function(match, mnr){
    -				var msplit = value.split(match);
    -				msplit.each(function(matchsplit, snr){
    -					if(msplit.length-1 == snr)
    -						value = matchsplit;
    -					mtches.append([value == matchsplit ? match : matchsplit]);
    +				var pos = value.indexOf(match),
    +					msplit = [value.substr(0, pos), value.substr(pos, match.length), value.substr(pos+match.length)];
     
    -					if(matches.length*2 == mtches.length)
    -						mtches.append([value]);
    +				msplit.each(function(matchsplit, snr){
    +					if(msplit.length-1 == snr){
    +						value = matchsplit;
    +
    +						if(matches.length-1 == mnr)
    +							mtches.append([value]);
    +
    +						return;
    +					}
    +					mtches.append([value == matchsplit ? match : matchsplit]);
     				});
     			});
     
    
    From 1f496b520979c7c911fed552a2356dff45a7c073 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 10 Jun 2012 17:29:29 +0200
    Subject: [PATCH 097/207] Use last movie date in imdb automation. fix #410
    
    ---
     couchpotato/core/providers/automation/imdb/main.py | 8 ++++++--
     1 file changed, 6 insertions(+), 2 deletions(-)
    
    diff --git a/couchpotato/core/providers/automation/imdb/main.py b/couchpotato/core/providers/automation/imdb/main.py
    index 6364a75f..86891a73 100644
    --- a/couchpotato/core/providers/automation/imdb/main.py
    +++ b/couchpotato/core/providers/automation/imdb/main.py
    @@ -37,6 +37,7 @@ class IMDB(Automation, RSS):
                 prop_name = 'automation.imdb.last_update.%s' % md5(rss_url)
                 last_update = float(Env.prop(prop_name, default = 0))
     
    +            last_movie_added = 0
                 try:
                     cache_key = 'imdb.rss.%s' % md5(rss_url)
     
    @@ -48,7 +49,10 @@ class IMDB(Automation, RSS):
                         created = int(time.mktime(parse(self.getTextElement(movie, "pubDate")).timetuple()))
                         imdb = getImdb(self.getTextElement(movie, "link"))
     
    -                    if not imdb or created < last_update:
    +                    if created > last_movie_added:
    +                        last_movie_added = created
    +
    +                    if not imdb or created <= last_update:
                             continue
     
                         movies.append(imdb)
    @@ -56,6 +60,6 @@ class IMDB(Automation, RSS):
                 except:
                     log.error('Failed loading IMDB watchlist: %s %s' % (rss_url, traceback.format_exc()))
     
    -            Env.prop(prop_name, time.time())
    +            Env.prop(prop_name, last_movie_added)
     
             return movies
    
    From 350185b9fd98c8a38342c28c94a3ecb3f643226a Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 10 Jun 2012 18:45:13 +0200
    Subject: [PATCH 098/207] Use new movie.list
    
    ---
     couchpotato/core/plugins/manage/main.py | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/couchpotato/core/plugins/manage/main.py b/couchpotato/core/plugins/manage/main.py
    index 73e10693..ce5edd13 100644
    --- a/couchpotato/core/plugins/manage/main.py
    +++ b/couchpotato/core/plugins/manage/main.py
    @@ -67,7 +67,7 @@ class Manage(Plugin):
             if self.conf('cleanup') and full and not self.shuttingDown():
     
                 # Get movies with done status
    -            done_movies = fireEvent('movie.list', status = 'done', single = True)
    +            total_movies, done_movies = fireEvent('movie.list', status = 'done', single = True)
     
                 for done_movie in done_movies:
                     if done_movie['library']['identifier'] not in added_identifiers:
    
    From 49a386106fa2f38cbc3adeaa5fff50b13eb0ce5d Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 10 Jun 2012 19:14:07 +0200
    Subject: [PATCH 099/207] Use quality size for newzbin. fix #429
    
    ---
     couchpotato/core/providers/nzb/newzbin/main.py | 2 ++
     1 file changed, 2 insertions(+)
    
    diff --git a/couchpotato/core/providers/nzb/newzbin/main.py b/couchpotato/core/providers/nzb/newzbin/main.py
    index 9239e84f..3a1c79e1 100644
    --- a/couchpotato/core/providers/nzb/newzbin/main.py
    +++ b/couchpotato/core/providers/nzb/newzbin/main.py
    @@ -57,6 +57,8 @@ class Newzbin(NZBProvider, RSS):
                 'category': '6',
                 'ps_rb_video_format': str(cat_id),
                 'ps_rb_source': str(format_id),
    +            'u_post_larger_than': quality.get('size_min'),
    +            'u_post_smaller_than': quality.get('size_max'),
             })
     
             url = "%s?%s" % (self.urls['search'], arguments)
    
    From e114d871c543c4e162e51cf27784b24bb132d1ce Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 10 Jun 2012 20:43:31 +0200
    Subject: [PATCH 100/207] Set movie profile to none when deleting movie. fix
     #313
    
    ---
     couchpotato/core/plugins/movie/main.py | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py
    index f59ba0ba..e58d1ddb 100644
    --- a/couchpotato/core/plugins/movie/main.py
    +++ b/couchpotato/core/plugins/movie/main.py
    @@ -437,6 +437,7 @@ class MoviePlugin(Plugin):
                         db.commit()
                     elif new_movie_status:
                         new_status = fireEvent('status.get', new_movie_status, single = True)
    +                    movie.profile_id = None
                         movie.status_id = new_status.get('id')
                         db.commit()
                     else:
    
    From 9018d5a585f2c22e9f86195de48d4bc81f24a570 Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 10 Jun 2012 21:37:39 +0200
    Subject: [PATCH 101/207] Get create and modified date to check if unpacking.
     fix #325
    
    ---
     couchpotato/core/plugins/scanner/main.py | 17 +++++++++++++----
     1 file changed, 13 insertions(+), 4 deletions(-)
    
    diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py
    index b7104644..f122f4bf 100644
    --- a/couchpotato/core/plugins/scanner/main.py
    +++ b/couchpotato/core/plugins/scanner/main.py
    @@ -296,13 +296,22 @@ class Scanner(Plugin):
                 # Check if movie is fresh and maybe still unpacking, ignore files new then 1 minute
                 file_too_new = False
                 for cur_file in group['unsorted_files']:
    -                file_time = os.path.getmtime(cur_file)
    -                if file_time > time.time() - 60:
    -                    file_too_new = tryInt(time.time() - file_time)
    +                file_time = [os.path.getmtime(cur_file), os.path.getctime(cur_file)]
    +                for t in file_time:
    +                    if t > time.time() - 60:
    +                        file_too_new = tryInt(time.time() - t)
    +                        break
    +
    +                if file_too_new:
                         break
     
                 if file_too_new:
    -                log.info('Files seem to be still unpacking or just unpacked (created on %s), ignoring for now: %s' % (time.ctime(file_time), identifier))
    +                log.info('Files seem to be still unpacking or just unpacked (created on %s), ignoring for now: %s' % (time.ctime(file_time[0]), identifier))
    +
    +                # Delete the unsorted list
    +                del group['unsorted_files']
    +
    +                continue
                     continue
     
                 # Group extra (and easy) files first
    
    From f410ba13f4bad867f8223d28465732314deb357d Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 10 Jun 2012 21:58:07 +0200
    Subject: [PATCH 102/207] Working "full/quick" scan
    
    ---
     couchpotato/core/plugins/manage/main.py   | 15 ++++++++-------
     couchpotato/core/plugins/scanner/main.py  | 19 ++++++++++++++++---
     couchpotato/static/scripts/page/manage.js |  2 +-
     3 files changed, 25 insertions(+), 11 deletions(-)
    
    diff --git a/couchpotato/core/plugins/manage/main.py b/couchpotato/core/plugins/manage/main.py
    index ce5edd13..33a6a5c5 100644
    --- a/couchpotato/core/plugins/manage/main.py
    +++ b/couchpotato/core/plugins/manage/main.py
    @@ -1,6 +1,6 @@
     from couchpotato.api import addApiView
     from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
    -from couchpotato.core.helpers.request import jsonified, getParams
    +from couchpotato.core.helpers.request import jsonified, getParam
     from couchpotato.core.logger import CPLog
     from couchpotato.core.plugins.base import Plugin
     from couchpotato.environment import Env
    @@ -25,13 +25,14 @@ class Manage(Plugin):
             })
     
             if not Env.get('dev'):
    -            addEvent('app.load', self.updateLibrary)
    +            def updateLibrary():
    +                self.updateLibrary(full = False)
    +            addEvent('app.load', updateLibrary)
     
         def updateLibraryView(self):
     
    -        params = getParams()
    -
    -        fireEventAsync('manage.update', full = params.get('full', True))
    +        full = getParam('full', default = 1)
    +        fireEventAsync('manage.update', full = True if full == '1' else False)
     
             return jsonified({
                 'success': True
    @@ -55,7 +56,7 @@ class Manage(Plugin):
                     continue
     
                 log.info('Updating manage library: %s' % directory)
    -            identifiers = fireEvent('scanner.folder', folder = directory, newer_than = last_update, single = True)
    +            identifiers = fireEvent('scanner.folder', folder = directory, newer_than = last_update if not full else 0, single = True)
                 if identifiers:
                     added_identifiers.extend(identifiers)
     
    @@ -71,7 +72,7 @@ class Manage(Plugin):
     
                 for done_movie in done_movies:
                     if done_movie['library']['identifier'] not in added_identifiers:
    -                    fireEvent('movie.delete', movie_id = done_movie['id'])
    +                    fireEvent('movie.delete', movie_id = done_movie['id'], delete_from = 'all')
     
             Env.prop('manage.last_update', time.time())
     
    diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py
    index f122f4bf..00e4299a 100644
    --- a/couchpotato/core/plugins/scanner/main.py
    +++ b/couchpotato/core/plugins/scanner/main.py
    @@ -103,14 +103,14 @@ class Scanner(Plugin):
                 if group['library']:
                     fireEvent('release.add', group = group)
     
    -    def scanFolderToLibrary(self, folder = None, newer_than = None, simple = True):
    +    def scanFolderToLibrary(self, folder = None, newer_than = 0, simple = True):
     
             folder = os.path.normpath(folder)
     
             if not os.path.isdir(folder):
                 return
     
    -        groups = self.scan(folder = folder, simple = simple)
    +        groups = self.scan(folder = folder, simple = simple, newer_than = newer_than)
     
             added_identifier = []
             while True and not self.shuttingDown():
    @@ -131,7 +131,7 @@ class Scanner(Plugin):
             return added_identifier
     
     
    -    def scan(self, folder = None, files = [], simple = False):
    +    def scan(self, folder = None, files = [], simple = False, newer_than = 0):
     
             folder = os.path.normpath(folder)
     
    @@ -312,6 +312,19 @@ class Scanner(Plugin):
                     del group['unsorted_files']
     
                     continue
    +
    +            # Only process movies newer than x
    +            if newer_than and newer_than > 0:
    +                for cur_file in group['unsorted_files']:
    +                    file_time = [os.path.getmtime(cur_file), os.path.getctime(cur_file)]
    +                    if file_time[0] > time.time() or file_time[1] > time.time():
    +                        break
    +
    +                log.debug('None of the files have changed since %s for %s, skipping.' % (time.ctime(newer_than), identifier))
    +
    +                # Delete the unsorted list
    +                del group['unsorted_files']
    +
                     continue
     
                 # Group extra (and easy) files first
    diff --git a/couchpotato/static/scripts/page/manage.js b/couchpotato/static/scripts/page/manage.js
    index 0385ee0f..aefbb42e 100644
    --- a/couchpotato/static/scripts/page/manage.js
    +++ b/couchpotato/static/scripts/page/manage.js
    @@ -41,7 +41,7 @@ Page.Manage = new Class({
     
     		Api.request('manage.update', {
     			'data': {
    -				'full': full ? 1 : null
    +				'full': +full
     			}
     		})
     
    
    From bd5170bc8e4e2638708a85619c8952c72b3c8daf Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Sun, 10 Jun 2012 22:05:28 +0200
    Subject: [PATCH 103/207] Use file permissions
    
    ---
     couchpotato/core/plugins/renamer/main.py | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py
    index d33ba89f..6adf1657 100644
    --- a/couchpotato/core/plugins/renamer/main.py
    +++ b/couchpotato/core/plugins/renamer/main.py
    @@ -406,7 +406,7 @@ class Renamer(Plugin):
                 shutil.move(old, dest)
     
                 try:
    -                os.chmod(dest, Env.getPermission('folder'))
    +                os.chmod(dest, Env.getPermission('file'))
                 except:
                     log.error('Failed setting permissions for file: %s' % dest)
     
    
    From 02855c7b9c2e20be74ad97fb9d3692380377cf3d Mon Sep 17 00:00:00 2001
    From: Ruud <ruud@crashdummy.nl>
    Date: Mon, 11 Jun 2012 09:54:15 +0200
    Subject: [PATCH 104/207] Library update
    
    ---
     .../core/providers/nzb/mysterbin/main.py      |   10 +-
     .../core/providers/nzb/nzbclub/main.py        |    2 +-
     .../core/providers/nzb/nzbindex/main.py       |    2 +-
     .../providers/torrent/kickasstorrents/main.py |   12 +-
     .../core/providers/trailer/hdtrailers/main.py |   14 +-
     .../providers/userscript/allocine/main.py     |    2 +-
     .../userscript/rottentomatoes/main.py         |    2 +-
     libs/BeautifulSoup.py                         | 2015 ----------
     libs/bs4/__init__.py                          |  355 ++
     libs/bs4/builder/__init__.py                  |  307 ++
     libs/bs4/builder/_html5lib.py                 |  222 ++
     libs/bs4/builder/_htmlparser.py               |  244 ++
     libs/bs4/builder/_lxml.py                     |  179 +
     libs/bs4/dammit.py                            |  792 ++++
     libs/bs4/element.py                           | 1347 +++++++
     libs/bs4/testing.py                           |  515 +++
     libs/certifi/__init__.py                      |    1 +
     libs/certifi/cacert.pem                       | 3338 +++++++++++++++++
     libs/certifi/core.py                          |   19 +
     libs/guessit/ISO-3166-1_utf8.txt              |    0
     libs/guessit/ISO-639-2_utf-8.txt              |    0
     libs/guessit/__init__.py                      |    2 +-
     libs/guessit/country.py                       |    4 +-
     libs/guessit/date.py                          |    0
     libs/guessit/fileutils.py                     |    7 +-
     libs/guessit/guess.py                         |    0
     libs/guessit/hash_ed2k.py                     |    0
     libs/guessit/hash_mpc.py                      |    0
     libs/guessit/language.py                      |   81 +-
     libs/guessit/matcher.py                       |    0
     libs/guessit/matchtree.py                     |    0
     libs/guessit/patterns.py                      |    6 +-
     libs/guessit/slogging.py                      |    0
     libs/guessit/textutils.py                     |   22 +
     libs/guessit/transfo/__init__.py              |    0
     libs/guessit/transfo/guess_bonus_features.py  |    0
     libs/guessit/transfo/guess_date.py            |    0
     .../guess_episode_info_from_position.py       |    0
     libs/guessit/transfo/guess_episodes_rexps.py  |   23 +-
     libs/guessit/transfo/guess_filetype.py        |    3 +
     libs/guessit/transfo/guess_language.py        |    0
     .../guess_movie_title_from_position.py        |    0
     libs/guessit/transfo/guess_properties.py      |    0
     libs/guessit/transfo/guess_release_group.py   |    0
     libs/guessit/transfo/guess_video_rexps.py     |    0
     .../transfo/guess_weak_episodes_rexps.py      |    0
     libs/guessit/transfo/guess_website.py         |    0
     libs/guessit/transfo/guess_year.py            |    0
     libs/guessit/transfo/post_process.py          |    0
     libs/guessit/transfo/split_explicit_groups.py |    0
     libs/guessit/transfo/split_on_dash.py         |    0
     libs/guessit/transfo/split_path_components.py |    0
     libs/html5lib/__init__.py                     |   17 +
     libs/html5lib/constants.py                    | 3085 +++++++++++++++
     libs/html5lib/filters/__init__.py             |    0
     libs/html5lib/filters/_base.py                |   10 +
     libs/html5lib/filters/formfiller.py           |  127 +
     libs/html5lib/filters/inject_meta_charset.py  |   62 +
     libs/html5lib/filters/lint.py                 |   88 +
     libs/html5lib/filters/optionaltags.py         |  202 +
     libs/html5lib/filters/sanitizer.py            |    8 +
     libs/html5lib/filters/whitespace.py           |   41 +
     libs/html5lib/html5parser.py                  | 2733 ++++++++++++++
     libs/html5lib/ihatexml.py                     |  177 +
     libs/html5lib/inputstream.py                  |  782 ++++
     libs/html5lib/sanitizer.py                    |  258 ++
     libs/html5lib/serializer/__init__.py          |   17 +
     libs/html5lib/serializer/htmlserializer.py    |  312 ++
     libs/html5lib/serializer/xhtmlserializer.py   |    9 +
     libs/html5lib/tokenizer.py                    | 1744 +++++++++
     libs/html5lib/treebuilders/__init__.py        |   96 +
     libs/html5lib/treebuilders/_base.py           |  377 ++
     libs/html5lib/treebuilders/dom.py             |  291 ++
     libs/html5lib/treebuilders/etree.py           |  344 ++
     libs/html5lib/treebuilders/etree_lxml.py      |  336 ++
     libs/html5lib/treebuilders/simpletree.py      |  256 ++
     libs/html5lib/treebuilders/soup.py            |  236 ++
     libs/html5lib/treewalkers/__init__.py         |   52 +
     libs/html5lib/treewalkers/_base.py            |  176 +
     libs/html5lib/treewalkers/dom.py              |   41 +
     libs/html5lib/treewalkers/etree.py            |  141 +
     libs/html5lib/treewalkers/genshistream.py     |   70 +
     libs/html5lib/treewalkers/lxmletree.py        |  186 +
     libs/html5lib/treewalkers/pulldom.py          |   60 +
     libs/html5lib/treewalkers/simpletree.py       |   78 +
     libs/html5lib/treewalkers/soup.py             |   60 +
     libs/html5lib/utils.py                        |  175 +
     libs/oauthlib/__init__.py                     |    0
     libs/oauthlib/common.py                       |  155 +
     libs/oauthlib/oauth1/__init__.py              |   13 +
     libs/oauthlib/oauth1/rfc5849/__init__.py      |  350 ++
     libs/oauthlib/oauth1/rfc5849/parameters.py    |  134 +
     libs/oauthlib/oauth1/rfc5849/signature.py     |  501 +++
     libs/oauthlib/oauth1/rfc5849/utils.py         |  141 +
     libs/oauthlib/oauth2/__init__.py              |   13 +
     libs/oauthlib/oauth2/draft25/__init__.py      |   14 +
     libs/oauthlib/oauth2/draft25/tokens.py        |  131 +
     libs/oauthlib/oauth2/draft25/utils.py         |  128 +
     libs/subliminal/api.py                        |   17 +-
     libs/subliminal/async.py                      |   14 +-
     libs/subliminal/cache.py                      |  132 +
     libs/subliminal/core.py                       |   75 +-
     libs/subliminal/infos.py                      |    2 +-
     libs/subliminal/languages.py                  |  547 ---
     libs/subliminal/services/__init__.py          |  171 +-
     libs/subliminal/services/addic7ed.py          |  161 +
     libs/subliminal/services/bierdopje.py         |   78 +-
     libs/subliminal/services/opensubtitles.py     |  105 +-
     libs/subliminal/services/podnapisi.py         |  106 +
     libs/subliminal/services/subswiki.py          |   23 +-
     libs/subliminal/services/subtitulos.py        |   31 +-
     libs/subliminal/services/thesubdb.py          |   25 +-
     libs/subliminal/services/tvsubtitles.py       |  146 +
     libs/subliminal/subtitles.py                  |   36 +-
     libs/subliminal/videos.py                     |   33 +-
     115 files changed, 22575 insertions(+), 2880 deletions(-)
     delete mode 100644 libs/BeautifulSoup.py
     create mode 100644 libs/bs4/__init__.py
     create mode 100644 libs/bs4/builder/__init__.py
     create mode 100644 libs/bs4/builder/_html5lib.py
     create mode 100644 libs/bs4/builder/_htmlparser.py
     create mode 100644 libs/bs4/builder/_lxml.py
     create mode 100644 libs/bs4/dammit.py
     create mode 100644 libs/bs4/element.py
     create mode 100644 libs/bs4/testing.py
     create mode 100644 libs/certifi/__init__.py
     create mode 100644 libs/certifi/cacert.pem
     create mode 100644 libs/certifi/core.py
     mode change 100644 => 100755 libs/guessit/ISO-3166-1_utf8.txt
     mode change 100644 => 100755 libs/guessit/ISO-639-2_utf-8.txt
     mode change 100644 => 100755 libs/guessit/__init__.py
     mode change 100644 => 100755 libs/guessit/country.py
     mode change 100644 => 100755 libs/guessit/date.py
     mode change 100644 => 100755 libs/guessit/fileutils.py
     mode change 100644 => 100755 libs/guessit/guess.py
     mode change 100644 => 100755 libs/guessit/hash_ed2k.py
     mode change 100644 => 100755 libs/guessit/hash_mpc.py
     mode change 100644 => 100755 libs/guessit/language.py
     mode change 100644 => 100755 libs/guessit/matcher.py
     mode change 100644 => 100755 libs/guessit/matchtree.py
     mode change 100644 => 100755 libs/guessit/slogging.py
     mode change 100644 => 100755 libs/guessit/textutils.py
     mode change 100644 => 100755 libs/guessit/transfo/__init__.py
     mode change 100644 => 100755 libs/guessit/transfo/guess_bonus_features.py
     mode change 100644 => 100755 libs/guessit/transfo/guess_date.py
     mode change 100644 => 100755 libs/guessit/transfo/guess_episode_info_from_position.py
     mode change 100644 => 100755 libs/guessit/transfo/guess_episodes_rexps.py
     mode change 100644 => 100755 libs/guessit/transfo/guess_filetype.py
     mode change 100644 => 100755 libs/guessit/transfo/guess_language.py
     mode change 100644 => 100755 libs/guessit/transfo/guess_movie_title_from_position.py
     mode change 100644 => 100755 libs/guessit/transfo/guess_properties.py
     mode change 100644 => 100755 libs/guessit/transfo/guess_release_group.py
     mode change 100644 => 100755 libs/guessit/transfo/guess_video_rexps.py
     mode change 100644 => 100755 libs/guessit/transfo/guess_weak_episodes_rexps.py
     mode change 100644 => 100755 libs/guessit/transfo/guess_website.py
     mode change 100644 => 100755 libs/guessit/transfo/guess_year.py
     mode change 100644 => 100755 libs/guessit/transfo/post_process.py
     mode change 100644 => 100755 libs/guessit/transfo/split_explicit_groups.py
     mode change 100644 => 100755 libs/guessit/transfo/split_on_dash.py
     mode change 100644 => 100755 libs/guessit/transfo/split_path_components.py
     create mode 100644 libs/html5lib/__init__.py
     create mode 100644 libs/html5lib/constants.py
     create mode 100644 libs/html5lib/filters/__init__.py
     create mode 100644 libs/html5lib/filters/_base.py
     create mode 100644 libs/html5lib/filters/formfiller.py
     create mode 100644 libs/html5lib/filters/inject_meta_charset.py
     create mode 100644 libs/html5lib/filters/lint.py
     create mode 100644 libs/html5lib/filters/optionaltags.py
     create mode 100644 libs/html5lib/filters/sanitizer.py
     create mode 100644 libs/html5lib/filters/whitespace.py
     create mode 100644 libs/html5lib/html5parser.py
     create mode 100644 libs/html5lib/ihatexml.py
     create mode 100644 libs/html5lib/inputstream.py
     create mode 100644 libs/html5lib/sanitizer.py
     create mode 100644 libs/html5lib/serializer/__init__.py
     create mode 100644 libs/html5lib/serializer/htmlserializer.py
     create mode 100644 libs/html5lib/serializer/xhtmlserializer.py
     create mode 100644 libs/html5lib/tokenizer.py
     create mode 100755 libs/html5lib/treebuilders/__init__.py
     create mode 100755 libs/html5lib/treebuilders/_base.py
     create mode 100644 libs/html5lib/treebuilders/dom.py
     create mode 100755 libs/html5lib/treebuilders/etree.py
     create mode 100644 libs/html5lib/treebuilders/etree_lxml.py
     create mode 100755 libs/html5lib/treebuilders/simpletree.py
     create mode 100644 libs/html5lib/treebuilders/soup.py
     create mode 100644 libs/html5lib/treewalkers/__init__.py
     create mode 100644 libs/html5lib/treewalkers/_base.py
     create mode 100644 libs/html5lib/treewalkers/dom.py
     create mode 100644 libs/html5lib/treewalkers/etree.py
     create mode 100644 libs/html5lib/treewalkers/genshistream.py
     create mode 100644 libs/html5lib/treewalkers/lxmletree.py
     create mode 100644 libs/html5lib/treewalkers/pulldom.py
     create mode 100644 libs/html5lib/treewalkers/simpletree.py
     create mode 100644 libs/html5lib/treewalkers/soup.py
     create mode 100644 libs/html5lib/utils.py
     create mode 100644 libs/oauthlib/__init__.py
     create mode 100644 libs/oauthlib/common.py
     create mode 100644 libs/oauthlib/oauth1/__init__.py
     create mode 100644 libs/oauthlib/oauth1/rfc5849/__init__.py
     create mode 100644 libs/oauthlib/oauth1/rfc5849/parameters.py
     create mode 100644 libs/oauthlib/oauth1/rfc5849/signature.py
     create mode 100644 libs/oauthlib/oauth1/rfc5849/utils.py
     create mode 100644 libs/oauthlib/oauth2/__init__.py
     create mode 100644 libs/oauthlib/oauth2/draft25/__init__.py
     create mode 100644 libs/oauthlib/oauth2/draft25/tokens.py
     create mode 100644 libs/oauthlib/oauth2/draft25/utils.py
     create mode 100755 libs/subliminal/cache.py
     delete mode 100755 libs/subliminal/languages.py
     create mode 100755 libs/subliminal/services/addic7ed.py
     create mode 100755 libs/subliminal/services/podnapisi.py
     create mode 100755 libs/subliminal/services/tvsubtitles.py
    
    diff --git a/couchpotato/core/providers/nzb/mysterbin/main.py b/couchpotato/core/providers/nzb/mysterbin/main.py
    index 5e619792..008f24f8 100644
    --- a/couchpotato/core/providers/nzb/mysterbin/main.py
    +++ b/couchpotato/core/providers/nzb/mysterbin/main.py
    @@ -1,4 +1,4 @@
    -from BeautifulSoup import BeautifulSoup
    +from bs4 import BeautifulSoup
     from couchpotato.core.event import fireEvent
     from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode, \
         simplifyString
    @@ -49,21 +49,21 @@ class Mysterbin(NZBProvider):
                 try:
                     html = BeautifulSoup(data)
                     resultable = html.find('table', attrs = {'class':'t'})
    -                for result in resultable.findAll('tr'):
    +                for result in resultable.find_all('tr'):
     
                         try:
                             myster_id = result.find('input', attrs = {'class': 'check4nzb'})['value']
     
                             # Age
                             age = ''
    -                        for temp in result.find('td', attrs = {'class': 'cdetail'}).findAll(text = True):
    +                        for temp in result.find('td', attrs = {'class': 'cdetail'}).find_all(text = True):
                                 if 'days' in temp:
                                     age = tryInt(temp.split(' ')[0])
                                     break
     
                             # size
                             size = None
    -                        for temp in result.find('div', attrs = {'class': 'cdetail'}).findAll(text = True):
    +                        for temp in result.find('div', attrs = {'class': 'cdetail'}).find_all(text = True):
                                 if 'gb' in temp.lower() or 'mb' in temp.lower() or 'kb' in temp.lower():
                                     size = self.parseSize(temp)
                                     break
    @@ -74,7 +74,7 @@ class Mysterbin(NZBProvider):
     
                             new = {
                                 'id': myster_id,
    -                            'name': ''.join(result.find('span', attrs = {'class': 'cname'}).findAll(text = True)),
    +                            'name': ''.join(result.find('span', attrs = {'class': 'cname'}).find_all(text = True)),
                                 'type': 'nzb',
                                 'provider': self.getName(),
                                 'age': age,
    diff --git a/couchpotato/core/providers/nzb/nzbclub/main.py b/couchpotato/core/providers/nzb/nzbclub/main.py
    index e6dbad90..3632c32b 100644
    --- a/couchpotato/core/providers/nzb/nzbclub/main.py
    +++ b/couchpotato/core/providers/nzb/nzbclub/main.py
    @@ -1,4 +1,4 @@
    -from BeautifulSoup import BeautifulSoup
    +from bs4 import BeautifulSoup
     from couchpotato.core.event import fireEvent
     from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode, \
         simplifyString
    diff --git a/couchpotato/core/providers/nzb/nzbindex/main.py b/couchpotato/core/providers/nzb/nzbindex/main.py
    index fd53cdde..5f74c09d 100644
    --- a/couchpotato/core/providers/nzb/nzbindex/main.py
    +++ b/couchpotato/core/providers/nzb/nzbindex/main.py
    @@ -1,4 +1,4 @@
    -from BeautifulSoup import BeautifulSoup
    +from bs4 import BeautifulSoup
     from couchpotato.core.event import fireEvent
     from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode, \
         simplifyString
    diff --git a/couchpotato/core/providers/torrent/kickasstorrents/main.py b/couchpotato/core/providers/torrent/kickasstorrents/main.py
    index 1101d321..2d82a4dc 100644
    --- a/couchpotato/core/providers/torrent/kickasstorrents/main.py
    +++ b/couchpotato/core/providers/torrent/kickasstorrents/main.py
    @@ -1,4 +1,4 @@
    -from BeautifulSoup import BeautifulSoup
    +from bs4 import BeautifulSoup
     from couchpotato.core.event import fireEvent
     from couchpotato.core.helpers.variable import tryInt, getTitle
     from couchpotato.core.logger import CPLog
    @@ -47,14 +47,14 @@ class KickAssTorrents(TorrentProvider):
                 try:
                     html = BeautifulSoup(data)
                     resultdiv = html.find('div', attrs = {'class':'tabs'})
    -                for result in resultdiv.findAll('div', recursive = False):
    +                for result in resultdiv.find_all('div', recursive = False):
                         if result.get('id').lower() not in cat_ids:
                             continue
     
                         try:
     
                             try:
    -                            for temp in result.findAll('tr'):
    +                            for temp in result.find_all('tr'):
                                     if temp['class'] is 'firstr' or not temp.get('id'):
                                         continue
     
    @@ -68,15 +68,15 @@ class KickAssTorrents(TorrentProvider):
                                     }
     
                                     nr = 0
    -                                for td in temp.findAll('td'):
    +                                for td in temp.find_all('td'):
                                         column_name = table_order[nr]
                                         if column_name:
     
                                             if column_name is 'name':
    -                                            link = td.find('div', {'class': 'torrentname'}).findAll('a')[1]
    +                                            link = td.find('div', {'class': 'torrentname'}).find_all('a')[1]
                                                 new['id'] = temp.get('id')[-8:]
                                                 new['name'] = link.text
    -                                            new['url'] = td.findAll('a', 'idownload')[1]['href']
    +                                            new['url'] = td.find_all('a', 'idownload')[1]['href']
                                                 if new['url'][:2] == '//':
                                                     new['url'] = 'http:%s' % new['url']
                                                 new['score'] = 20 if td.find('a', 'iverif') else 0
    diff --git a/couchpotato/core/providers/trailer/hdtrailers/main.py b/couchpotato/core/providers/trailer/hdtrailers/main.py
    index b68f76f7..642079fd 100644
    --- a/couchpotato/core/providers/trailer/hdtrailers/main.py
    +++ b/couchpotato/core/providers/trailer/hdtrailers/main.py
    @@ -1,4 +1,4 @@
    -from BeautifulSoup import SoupStrainer, BeautifulSoup
    +from bs4 import SoupStrainer, BeautifulSoup
     from couchpotato.core.helpers.encoding import tryUrlencode
     from couchpotato.core.helpers.variable import mergeDicts, getTitle
     from couchpotato.core.logger import CPLog
    @@ -51,13 +51,13 @@ class HDTrailers(TrailerProvider):
     
             try:
                 tables = SoupStrainer('div')
    -            html = BeautifulSoup(data, parseOnlyThese = tables)
    -            result_table = html.findAll('h2', text = re.compile(movie_name))
    +            html = BeautifulSoup(data, parse_only = tables)
    +            result_table = html.find_all('h2', text = re.compile(movie_name))
     
                 for h2 in result_table:
                     if 'trailer' in h2.lower():
                         parent = h2.parent.parent.parent
    -                    trailerLinks = parent.findAll('a', text = re.compile('480p|720p|1080p'))
    +                    trailerLinks = parent.find_all('a', text = re.compile('480p|720p|1080p'))
                         try:
                             for trailer in trailerLinks:
                                 results[trailer].insert(0, trailer.parent['href'])
    @@ -74,11 +74,11 @@ class HDTrailers(TrailerProvider):
             results = {'480p':[], '720p':[], '1080p':[]}
             try:
                 tables = SoupStrainer('table')
    -            html = BeautifulSoup(data, parseOnlyThese = tables)
    +            html = BeautifulSoup(data, parse_only = tables)
                 result_table = html.find('table', attrs = {'class':'bottomTable'})
     
     
    -            for tr in result_table.findAll('tr'):
    +            for tr in result_table.find_all('tr'):
                     trtext = str(tr).lower()
                     if 'clips' in trtext:
                         break
    @@ -86,7 +86,7 @@ class HDTrailers(TrailerProvider):
                         nr = 0
                         if 'trailer' not in tr.find('span', 'standardTrailerName').text.lower():
                             continue
    -                    resolutions = tr.findAll('td', attrs = {'class':'bottomTableResolution'})
    +                    resolutions = tr.find_all('td', attrs = {'class':'bottomTableResolution'})
                         for res in resolutions:
                             results[str(res.a.contents[0])].insert(0, res.a['href'])
                             nr += 1
    diff --git a/couchpotato/core/providers/userscript/allocine/main.py b/couchpotato/core/providers/userscript/allocine/main.py
    index 8213ac2f..890ae223 100644
    --- a/couchpotato/core/providers/userscript/allocine/main.py
    +++ b/couchpotato/core/providers/userscript/allocine/main.py
    @@ -1,4 +1,4 @@
    -from BeautifulSoup import BeautifulSoup
    +from bs4 import BeautifulSoup
     from couchpotato.core.providers.userscript.base import UserscriptBase
     
     class AlloCine(UserscriptBase):
    diff --git a/couchpotato/core/providers/userscript/rottentomatoes/main.py b/couchpotato/core/providers/userscript/rottentomatoes/main.py
    index 1d685903..cd869b8f 100644
    --- a/couchpotato/core/providers/userscript/rottentomatoes/main.py
    +++ b/couchpotato/core/providers/userscript/rottentomatoes/main.py
    @@ -1,4 +1,4 @@
    -from BeautifulSoup import BeautifulSoup
    +from bs4 import BeautifulSoup
     from couchpotato.core.event import fireEvent
     from couchpotato.core.providers.userscript.base import UserscriptBase
     
    diff --git a/libs/BeautifulSoup.py b/libs/BeautifulSoup.py
    deleted file mode 100644
    index addd35df..00000000
    --- a/libs/BeautifulSoup.py
    +++ /dev/null
    @@ -1,2015 +0,0 @@
    -"""Beautiful Soup
    -Elixir and Tonic
    -"The Screen-Scraper's Friend"
    -http://www.crummy.com/software/BeautifulSoup/
    -
    -Beautiful Soup parses a (possibly invalid) XML or HTML document into a
    -tree representation. It provides methods and Pythonic idioms that make
    -it easy to navigate, search, and modify the tree.
    -
    -A well-formed XML/HTML document yields a well-formed data
    -structure. An ill-formed XML/HTML document yields a correspondingly
    -ill-formed data structure. If your document is only locally
    -well-formed, you can use this library to find and process the
    -well-formed part of it.
    -
    -Beautiful Soup works with Python 2.2 and up. It has no external
    -dependencies, but you'll have more success at converting data to UTF-8
    -if you also install these three packages:
    -
    -* chardet, for auto-detecting character encodings
    -  http://chardet.feedparser.org/
    -* cjkcodecs and iconv_codec, which add more encodings to the ones supported
    -  by stock Python.
    -  http://cjkpython.i18n.org/
    -
    -Beautiful Soup defines classes for two main parsing strategies:
    -
    - * BeautifulStoneSoup, for parsing XML, SGML, or your domain-specific
    -   language that kind of looks like XML.
    -
    - * BeautifulSoup, for parsing run-of-the-mill HTML code, be it valid
    -   or invalid. This class has web browser-like heuristics for
    -   obtaining a sensible parse tree in the face of common HTML errors.
    -
    -Beautiful Soup also defines a class (UnicodeDammit) for autodetecting
    -the encoding of an HTML or XML document, and converting it to
    -Unicode. Much of this code is taken from Mark Pilgrim's Universal Feed Parser.
    -
    -For more than you ever wanted to know about Beautiful Soup, see the
    -documentation:
    -http://www.crummy.com/software/BeautifulSoup/documentation.html
    -
    -Here, have some legalese:
    -
    -Copyright (c) 2004-2010, Leonard Richardson
    -
    -All rights reserved.
    -
    -Redistribution and use in source and binary forms, with or without
    -modification, are permitted provided that the following conditions are
    -met:
    -
    -  * Redistributions of source code must retain the above copyright
    -    notice, this list of conditions and the following disclaimer.
    -
    -  * Redistributions in binary form must reproduce the above
    -    copyright notice, this list of conditions and the following
    -    disclaimer in the documentation and/or other materials provided
    -    with the distribution.
    -
    -  * Neither the name of the the Beautiful Soup Consortium and All
    -    Night Kosher Bakery nor the names of its contributors may be
    -    used to endorse or promote products derived from this software
    -    without specific prior written permission.
    -
    -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
    -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
    -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
    -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
    -CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
    -EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
    -PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
    -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
    -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
    -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
    -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE, DAMMIT.
    -
    -"""
    -from __future__ import generators
    -
    -__author__ = "Leonard Richardson (leonardr@segfault.org)"
    -__version__ = "3.2.0"
    -__copyright__ = "Copyright (c) 2004-2010 Leonard Richardson"
    -__license__ = "New-style BSD"
    -
    -from sgmllib import SGMLParser, SGMLParseError
    -import codecs
    -import markupbase
    -import types
    -import re
    -import sgmllib
    -try:
    -    from htmlentitydefs import name2codepoint
    -except ImportError:
    -    name2codepoint = {}
    -try:
    -    set
    -except NameError:
    -    from sets import Set as set
    -
    -#These hacks make Beautiful Soup able to parse XML with namespaces
    -sgmllib.tagfind = re.compile('[a-zA-Z][-_.:a-zA-Z0-9]*')
    -markupbase._declname_match = re.compile(r'[a-zA-Z][-_.:a-zA-Z0-9]*\s*').match
    -
    -DEFAULT_OUTPUT_ENCODING = "utf-8"
    -
    -def _match_css_class(str):
    -    """Build a RE to match the given CSS class."""
    -    return re.compile(r"(^|.*\s)%s($|\s)" % str)
    -
    -# First, the classes that represent markup elements.
    -
    -class PageElement(object):
    -    """Contains the navigational information for some part of the page
    -    (either a tag or a piece of text)"""
    -
    -    def setup(self, parent = None, previous = None):
    -        """Sets up the initial relations between this element and
    -        other elements."""
    -        self.parent = parent
    -        self.previous = previous
    -        self.next = None
    -        self.previousSibling = None
    -        self.nextSibling = None
    -        if self.parent and self.parent.contents:
    -            self.previousSibling = self.parent.contents[-1]
    -            self.previousSibling.nextSibling = self
    -
    -    def replaceWith(self, replaceWith):
    -        oldParent = self.parent
    -        myIndex = self.parent.index(self)
    -        if hasattr(replaceWith, "parent")\
    -                  and replaceWith.parent is self.parent:
    -            # We're replacing this element with one of its siblings.
    -            index = replaceWith.parent.index(replaceWith)
    -            if index and index < myIndex:
    -                # Furthermore, it comes before this element. That
    -                # means that when we extract it, the index of this
    -                # element will change.
    -                myIndex = myIndex - 1
    -        self.extract()
    -        oldParent.insert(myIndex, replaceWith)
    -
    -    def replaceWithChildren(self):
    -        myParent = self.parent
    -        myIndex = self.parent.index(self)
    -        self.extract()
    -        reversedChildren = list(self.contents)
    -        reversedChildren.reverse()
    -        for child in reversedChildren:
    -            myParent.insert(myIndex, child)
    -
    -    def extract(self):
    -        """Destructively rips this element out of the tree."""
    -        if self.parent:
    -            try:
    -                del self.parent.contents[self.parent.index(self)]
    -            except ValueError:
    -                pass
    -
    -        #Find the two elements that would be next to each other if
    -        #this element (and any children) hadn't been parsed. Connect
    -        #the two.
    -        lastChild = self._lastRecursiveChild()
    -        nextElement = lastChild.next
    -
    -        if self.previous:
    -            self.previous.next = nextElement
    -        if nextElement:
    -            nextElement.previous = self.previous
    -        self.previous = None
    -        lastChild.next = None
    -
    -        self.parent = None
    -        if self.previousSibling:
    -            self.previousSibling.nextSibling = self.nextSibling
    -        if self.nextSibling:
    -            self.nextSibling.previousSibling = self.previousSibling
    -        self.previousSibling = self.nextSibling = None
    -        return self
    -
    -    def _lastRecursiveChild(self):
    -        "Finds the last element beneath this object to be parsed."
    -        lastChild = self
    -        while hasattr(lastChild, 'contents') and lastChild.contents:
    -            lastChild = lastChild.contents[-1]
    -        return lastChild
    -
    -    def insert(self, position, newChild):
    -        if isinstance(newChild, basestring) \
    -            and not isinstance(newChild, NavigableString):
    -            newChild = NavigableString(newChild)
    -
    -        position = min(position, len(self.contents))
    -        if hasattr(newChild, 'parent') and newChild.parent is not None:
    -            # We're 'inserting' an element that's already one
    -            # of this object's children.
    -            if newChild.parent is self:
    -                index = self.index(newChild)
    -                if index > position:
    -                    # Furthermore we're moving it further down the
    -                    # list of this object's children. That means that
    -                    # when we extract this element, our target index
    -                    # will jump down one.
    -                    position = position - 1
    -            newChild.extract()
    -
    -        newChild.parent = self
    -        previousChild = None
    -        if position == 0:
    -            newChild.previousSibling = None
    -            newChild.previous = self
    -        else:
    -            previousChild = self.contents[position - 1]
    -            newChild.previousSibling = previousChild
    -            newChild.previousSibling.nextSibling = newChild
    -            newChild.previous = previousChild._lastRecursiveChild()
    -        if newChild.previous:
    -            newChild.previous.next = newChild
    -
    -        newChildsLastElement = newChild._lastRecursiveChild()
    -
    -        if position >= len(self.contents):
    -            newChild.nextSibling = None
    -
    -            parent = self
    -            parentsNextSibling = None
    -            while not parentsNextSibling:
    -                parentsNextSibling = parent.nextSibling
    -                parent = parent.parent
    -                if not parent: # This is the last element in the document.
    -                    break
    -            if parentsNextSibling:
    -                newChildsLastElement.next = parentsNextSibling
    -            else:
    -                newChildsLastElement.next = None
    -        else:
    -            nextChild = self.contents[position]
    -            newChild.nextSibling = nextChild
    -            if newChild.nextSibling:
    -                newChild.nextSibling.previousSibling = newChild
    -            newChildsLastElement.next = nextChild
    -
    -        if newChildsLastElement.next:
    -            newChildsLastElement.next.previous = newChildsLastElement
    -        self.contents.insert(position, newChild)
    -
    -    def append(self, tag):
    -        """Appends the given tag to the contents of this tag."""
    -        self.insert(len(self.contents), tag)
    -
    -    def findNext(self, name = None, attrs = {}, text = None, **kwargs):
    -        """Returns the first item that matches the given criteria and
    -        appears after this Tag in the document."""
    -        return self._findOne(self.findAllNext, name, attrs, text, **kwargs)
    -
    -    def findAllNext(self, name = None, attrs = {}, text = None, limit = None,
    -                    **kwargs):
    -        """Returns all items that match the given criteria and appear
    -        after this Tag in the document."""
    -        return self._findAll(name, attrs, text, limit, self.nextGenerator,
    -                             **kwargs)
    -
    -    def findNextSibling(self, name = None, attrs = {}, text = None, **kwargs):
    -        """Returns the closest sibling to this Tag that matches the
    -        given criteria and appears after this Tag in the document."""
    -        return self._findOne(self.findNextSiblings, name, attrs, text,
    -                             **kwargs)
    -
    -    def findNextSiblings(self, name = None, attrs = {}, text = None, limit = None,
    -                         **kwargs):
    -        """Returns the siblings of this Tag that match the given
    -        criteria and appear after this Tag in the document."""
    -        return self._findAll(name, attrs, text, limit,
    -                             self.nextSiblingGenerator, **kwargs)
    -    fetchNextSiblings = findNextSiblings # Compatibility with pre-3.x
    -
    -    def findPrevious(self, name = None, attrs = {}, text = None, **kwargs):
    -        """Returns the first item that matches the given criteria and
    -        appears before this Tag in the document."""
    -        return self._findOne(self.findAllPrevious, name, attrs, text, **kwargs)
    -
    -    def findAllPrevious(self, name = None, attrs = {}, text = None, limit = None,
    -                        **kwargs):
    -        """Returns all items that match the given criteria and appear
    -        before this Tag in the document."""
    -        return self._findAll(name, attrs, text, limit, self.previousGenerator,
    -                           **kwargs)
    -    fetchPrevious = findAllPrevious # Compatibility with pre-3.x
    -
    -    def findPreviousSibling(self, name = None, attrs = {}, text = None, **kwargs):
    -        """Returns the closest sibling to this Tag that matches the
    -        given criteria and appears before this Tag in the document."""
    -        return self._findOne(self.findPreviousSiblings, name, attrs, text,
    -                             **kwargs)
    -
    -    def findPreviousSiblings(self, name = None, attrs = {}, text = None,
    -                             limit = None, **kwargs):
    -        """Returns the siblings of this Tag that match the given
    -        criteria and appear before this Tag in the document."""
    -        return self._findAll(name, attrs, text, limit,
    -                             self.previousSiblingGenerator, **kwargs)
    -    fetchPreviousSiblings = findPreviousSiblings # Compatibility with pre-3.x
    -
    -    def findParent(self, name = None, attrs = {}, **kwargs):
    -        """Returns the closest parent of this Tag that matches the given
    -        criteria."""
    -        # NOTE: We can't use _findOne because findParents takes a different
    -        # set of arguments.
    -        r = None
    -        l = self.findParents(name, attrs, 1)
    -        if l:
    -            r = l[0]
    -        return r
    -
    -    def findParents(self, name = None, attrs = {}, limit = None, **kwargs):
    -        """Returns the parents of this Tag that match the given
    -        criteria."""
    -
    -        return self._findAll(name, attrs, None, limit, self.parentGenerator,
    -                             **kwargs)
    -    fetchParents = findParents # Compatibility with pre-3.x
    -
    -    #These methods do the real heavy lifting.
    -
    -    def _findOne(self, method, name, attrs, text, **kwargs):
    -        r = None
    -        l = method(name, attrs, text, 1, **kwargs)
    -        if l:
    -            r = l[0]
    -        return r
    -
    -    def _findAll(self, name, attrs, text, limit, generator, **kwargs):
    -        "Iterates over a generator looking for things that match."
    -
    -        if isinstance(name, SoupStrainer):
    -            strainer = name
    -        # (Possibly) special case some findAll*(...) searches
    -        elif text is None and not limit and not attrs and not kwargs:
    -            # findAll*(True)
    -            if name is True:
    -                return [element for element in generator()
    -                        if isinstance(element, Tag)]
    -            # findAll*('tag-name')
    -            elif isinstance(name, basestring):
    -                return [element for element in generator()
    -                        if isinstance(element, Tag) and
    -                        element.name == name]
    -            else:
    -                strainer = SoupStrainer(name, attrs, text, **kwargs)
    -        # Build a SoupStrainer
    -        else:
    -            strainer = SoupStrainer(name, attrs, text, **kwargs)
    -        results = ResultSet(strainer)
    -        g = generator()
    -        while True:
    -            try:
    -                i = g.next()
    -            except StopIteration:
    -                break
    -            if i:
    -                found = strainer.search(i)
    -                if found:
    -                    results.append(found)
    -                    if limit and len(results) >= limit:
    -                        break
    -        return results
    -
    -    #These Generators can be used to navigate starting from both
    -    #NavigableStrings and Tags.
    -    def nextGenerator(self):
    -        i = self
    -        while i is not None:
    -            i = i.next
    -            yield i
    -
    -    def nextSiblingGenerator(self):
    -        i = self
    -        while i is not None:
    -            i = i.nextSibling
    -            yield i
    -
    -    def previousGenerator(self):
    -        i = self
    -        while i is not None:
    -            i = i.previous
    -            yield i
    -
    -    def previousSiblingGenerator(self):
    -        i = self
    -        while i is not None:
    -            i = i.previousSibling
    -            yield i
    -
    -    def parentGenerator(self):
    -        i = self
    -        while i is not None:
    -            i = i.parent
    -            yield i
    -
    -    # Utility methods
    -    def substituteEncoding(self, str, encoding = None):
    -        encoding = encoding or "utf-8"
    -        return str.replace("%SOUP-ENCODING%", encoding)
    -
    -    def toEncoding(self, s, encoding = None):
    -        """Encodes an object to a string in some encoding, or to Unicode.
    -        ."""
    -        if isinstance(s, unicode):
    -            if encoding:
    -                s = s.encode(encoding)
    -        elif isinstance(s, str):
    -            if encoding:
    -                s = s.encode(encoding)
    -            else:
    -                s = unicode(s)
    -        else:
    -            if encoding:
    -                s = self.toEncoding(str(s), encoding)
    -            else:
    -                s = unicode(s)
    -        return s
    -
    -class NavigableString(unicode, PageElement):
    -
    -    def __new__(cls, value):
    -        """Create a new NavigableString.
    -
    -        When unpickling a NavigableString, this method is called with
    -        the string in DEFAULT_OUTPUT_ENCODING. That encoding needs to be
    -        passed in to the superclass's __new__ or the superclass won't know
    -        how to handle non-ASCII characters.
    -        """
    -        if isinstance(value, unicode):
    -            return unicode.__new__(cls, value)
    -        return unicode.__new__(cls, value, DEFAULT_OUTPUT_ENCODING)
    -
    -    def __getnewargs__(self):
    -        return (NavigableString.__str__(self),)
    -
    -    def __getattr__(self, attr):
    -        """text.string gives you text. This is for backwards
    -        compatibility for Navigable*String, but for CData* it lets you
    -        get the string without the CData wrapper."""
    -        if attr == 'string':
    -            return self
    -        else:
    -            raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__.__name__, attr)
    -
    -    def __unicode__(self):
    -        return str(self).decode(DEFAULT_OUTPUT_ENCODING)
    -
    -    def __str__(self, encoding = DEFAULT_OUTPUT_ENCODING):
    -        if encoding:
    -            return self.encode(encoding)
    -        else:
    -            return self
    -
    -class CData(NavigableString):
    -
    -    def __str__(self, encoding = DEFAULT_OUTPUT_ENCODING):
    -        return "<![CDATA[%s]]>" % NavigableString.__str__(self, encoding)
    -
    -class ProcessingInstruction(NavigableString):
    -    def __str__(self, encoding = DEFAULT_OUTPUT_ENCODING):
    -        output = self
    -        if "%SOUP-ENCODING%" in output:
    -            output = self.substituteEncoding(output, encoding)
    -        return "<?%s?>" % self.toEncoding(output, encoding)
    -
    -class Comment(NavigableString):
    -    def __str__(self, encoding = DEFAULT_OUTPUT_ENCODING):
    -        return "<!--%s-->" % NavigableString.__str__(self, encoding)
    -
    -class Declaration(NavigableString):
    -    def __str__(self, encoding = DEFAULT_OUTPUT_ENCODING):
    -        return "<!%s>" % NavigableString.__str__(self, encoding)
    -
    -class Tag(PageElement):
    -
    -    """Represents a found HTML tag with its attributes and contents."""
    -
    -    def _invert(h):
    -        "Cheap function to invert a hash."
    -        i = {}
    -        for k, v in h.items():
    -            i[v] = k
    -        return i
    -
    -    XML_ENTITIES_TO_SPECIAL_CHARS = { "apos" : "'",
    -                                      "quot" : '"',
    -                                      "amp" : "&",
    -                                      "lt" : "<",
    -                                      "gt" : ">" }
    -
    -    XML_SPECIAL_CHARS_TO_ENTITIES = _invert(XML_ENTITIES_TO_SPECIAL_CHARS)
    -
    -    def _convertEntities(self, match):
    -        """Used in a call to re.sub to replace HTML, XML, and numeric
    -        entities with the appropriate Unicode characters. If HTML
    -        entities are being converted, any unrecognized entities are
    -        escaped."""
    -        x = match.group(1)
    -        if self.convertHTMLEntities and x in name2codepoint:
    -            return unichr(name2codepoint[x])
    -        elif x in self.XML_ENTITIES_TO_SPECIAL_CHARS:
    -            if self.convertXMLEntities:
    -                return self.XML_ENTITIES_TO_SPECIAL_CHARS[x]
    -            else:
    -                return u'&%s;' % x
    -        elif len(x) > 0 and x[0] == '#':
    -            # Handle numeric entities
    -            if len(x) > 1 and x[1] == 'x':
    -                return unichr(int(x[2:], 16))
    -            else:
    -                return unichr(int(x[1:]))
    -
    -        elif self.escapeUnrecognizedEntities:
    -            return u'&%s;' % x
    -        else:
    -            return u'&%s;' % x
    -
    -    def __init__(self, parser, name, attrs = None, parent = None,
    -                 previous = None):
    -        "Basic constructor."
    -
    -        # We don't actually store the parser object: that lets extracted
    -        # chunks be garbage-collected
    -        self.parserClass = parser.__class__
    -        self.isSelfClosing = parser.isSelfClosingTag(name)
    -        self.name = name
    -        if attrs is None:
    -            attrs = []
    -        elif isinstance(attrs, dict):
    -            attrs = attrs.items()
    -        self.attrs = attrs
    -        self.contents = []
    -        self.setup(parent, previous)
    -        self.hidden = False
    -        self.containsSubstitutions = False
    -        self.convertHTMLEntities = parser.convertHTMLEntities
    -        self.convertXMLEntities = parser.convertXMLEntities
    -        self.escapeUnrecognizedEntities = parser.escapeUnrecognizedEntities
    -
    -        # Convert any HTML, XML, or numeric entities in the attribute values.
    -        convert = lambda(k, val): (k,
    -                                   re.sub("&(#\d+|#x[0-9a-fA-F]+|\w+);",
    -                                          self._convertEntities,
    -                                          val))
    -        self.attrs = map(convert, self.attrs)
    -
    -    def getString(self):
    -        if (len(self.contents) == 1
    -            and isinstance(self.contents[0], NavigableString)):
    -            return self.contents[0]
    -
    -    def setString(self, string):
    -        """Replace the contents of the tag with a string"""
    -        self.clear()
    -        self.append(string)
    -
    -    string = property(getString, setString)
    -
    -    def getText(self, separator = u""):
    -        if not len(self.contents):
    -            return u""
    -        stopNode = self._lastRecursiveChild().next
    -        strings = []
    -        current = self.contents[0]
    -        while current is not stopNode:
    -            if isinstance(current, NavigableString):
    -                strings.append(current.strip())
    -            current = current.next
    -        return separator.join(strings)
    -
    -    text = property(getText)
    -
    -    def get(self, key, default = None):
    -        """Returns the value of the 'key' attribute for the tag, or
    -        the value given for 'default' if it doesn't have that
    -        attribute."""
    -        return self._getAttrMap().get(key, default)
    -
    -    def clear(self):
    -        """Extract all children."""
    -        for child in self.contents[:]:
    -            child.extract()
    -
    -    def index(self, element):
    -        for i, child in enumerate(self.contents):
    -            if child is element:
    -                return i
    -        raise ValueError("Tag.index: element not in tag")
    -
    -    def has_key(self, key):
    -        return self._getAttrMap().has_key(key)
    -
    -    def __getitem__(self, key):
    -        """tag[key] returns the value of the 'key' attribute for the tag,
    -        and throws an exception if it's not there."""
    -        return self._getAttrMap()[key]
    -
    -    def __iter__(self):
    -        "Iterating over a tag iterates over its contents."
    -        return iter(self.contents)
    -
    -    def __len__(self):
    -        "The length of a tag is the length of its list of contents."
    -        return len(self.contents)
    -
    -    def __contains__(self, x):
    -        return x in self.contents
    -
    -    def __nonzero__(self):
    -        "A tag is non-None even if it has no contents."
    -        return True
    -
    -    def __setitem__(self, key, value):
    -        """Setting tag[key] sets the value of the 'key' attribute for the
    -        tag."""
    -        self._getAttrMap()
    -        self.attrMap[key] = value
    -        found = False
    -        for i in range(0, len(self.attrs)):
    -            if self.attrs[i][0] == key:
    -                self.attrs[i] = (key, value)
    -                found = True
    -        if not found:
    -            self.attrs.append((key, value))
    -        self._getAttrMap()[key] = value
    -
    -    def __delitem__(self, key):
    -        "Deleting tag[key] deletes all 'key' attributes for the tag."
    -        for item in self.attrs:
    -            if item[0] == key:
    -                self.attrs.remove(item)
    -                #We don't break because bad HTML can define the same
    -                #attribute multiple times.
    -            self._getAttrMap()
    -            if self.attrMap.has_key(key):
    -                del self.attrMap[key]
    -
    -    def __call__(self, *args, **kwargs):
    -        """Calling a tag like a function is the same as calling its
    -        findAll() method. Eg. tag('a') returns a list of all the A tags
    -        found within this tag."""
    -        return apply(self.findAll, args, kwargs)
    -
    -    def __getattr__(self, tag):
    -        #print "Getattr %s.%s" % (self.__class__, tag)
    -        if len(tag) > 3 and tag.rfind('Tag') == len(tag) - 3:
    -            return self.find(tag[:-3])
    -        elif tag.find('__') != 0:
    -            return self.find(tag)
    -        raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__, tag)
    -
    -    def __eq__(self, other):
    -        """Returns true iff this tag has the same name, the same attributes,
    -        and the same contents (recursively) as the given tag.
    -
    -        NOTE: right now this will return false if two tags have the
    -        same attributes in a different order. Should this be fixed?"""
    -        if other is self:
    -            return True
    -        if not hasattr(other, 'name') or not hasattr(other, 'attrs') or not hasattr(other, 'contents') or self.name != other.name or self.attrs != other.attrs or len(self) != len(other):
    -            return False
    -        for i in range(0, len(self.contents)):
    -            if self.contents[i] != other.contents[i]:
    -                return False
    -        return True
    -
    -    def __ne__(self, other):
    -        """Returns true iff this tag is not identical to the other tag,
    -        as defined in __eq__."""
    -        return not self == other
    -
    -    def __repr__(self, encoding = DEFAULT_OUTPUT_ENCODING):
    -        """Renders this tag as a string."""
    -        return self.__str__(encoding)
    -
    -    def __unicode__(self):
    -        return self.__str__(None)
    -
    -    BARE_AMPERSAND_OR_BRACKET = re.compile("([<>]|"
    -                                           + "&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)"
    -                                           + ")")
    -
    -    def _sub_entity(self, x):
    -        """Used with a regular expression to substitute the
    -        appropriate XML entity for an XML special character."""
    -        return "&" + self.XML_SPECIAL_CHARS_TO_ENTITIES[x.group(0)[0]] + ";"
    -
    -    def __str__(self, encoding = DEFAULT_OUTPUT_ENCODING,
    -                prettyPrint = False, indentLevel = 0):
    -        """Returns a string or Unicode representation of this tag and
    -        its contents. To get Unicode, pass None for encoding.
    -
    -        NOTE: since Python's HTML parser consumes whitespace, this
    -        method is not certain to reproduce the whitespace present in
    -        the original string."""
    -
    -        encodedName = self.toEncoding(self.name, encoding)
    -
    -        attrs = []
    -        if self.attrs:
    -            for key, val in self.attrs:
    -                fmt = '%s="%s"'
    -                if isinstance(val, basestring):
    -                    if self.containsSubstitutions and '%SOUP-ENCODING%' in val:
    -                        val = self.substituteEncoding(val, encoding)
    -
    -                    # The attribute value either:
    -                    #
    -                    # * Contains no embedded double quotes or single quotes.
    -                    #   No problem: we enclose it in double quotes.
    -                    # * Contains embedded single quotes. No problem:
    -                    #   double quotes work here too.
    -                    # * Contains embedded double quotes. No problem:
    -                    #   we enclose it in single quotes.
    -                    # * Embeds both single _and_ double quotes. This
    -                    #   can't happen naturally, but it can happen if
    -                    #   you modify an attribute value after parsing
    -                    #   the document. Now we have a bit of a
    -                    #   problem. We solve it by enclosing the
    -                    #   attribute in single quotes, and escaping any
    -                    #   embedded single quotes to XML entities.
    -                    if '"' in val:
    -                        fmt = "%s='%s'"
    -                        if "'" in val:
    -                            # TODO: replace with apos when
    -                            # appropriate.
    -                            val = val.replace("'", "&squot;")
    -
    -                    # Now we're okay w/r/t quotes. But the attribute
    -                    # value might also contain angle brackets, or
    -                    # ampersands that aren't part of entities. We need
    -                    # to escape those to XML entities too.
    -                    val = self.BARE_AMPERSAND_OR_BRACKET.sub(self._sub_entity, val)
    -
    -                attrs.append(fmt % (self.toEncoding(key, encoding),
    -                                    self.toEncoding(val, encoding)))
    -        close = ''
    -        closeTag = ''
    -        if self.isSelfClosing:
    -            close = ' /'
    -        else:
    -            closeTag = '</%s>' % encodedName
    -
    -        indentTag, indentContents = 0, 0
    -        if prettyPrint:
    -            indentTag = indentLevel
    -            space = (' ' * (indentTag - 1))
    -            indentContents = indentTag + 1
    -        contents = self.renderContents(encoding, prettyPrint, indentContents)
    -        if self.hidden:
    -            s = contents
    -        else:
    -            s = []
    -            attributeString = ''
    -            if attrs:
    -                attributeString = ' ' + ' '.join(attrs)
    -            if prettyPrint:
    -                s.append(space)
    -            s.append('<%s%s%s>' % (encodedName, attributeString, close))
    -            if prettyPrint:
    -                s.append("\n")
    -            s.append(contents)
    -            if prettyPrint and contents and contents[-1] != "\n":
    -                s.append("\n")
    -            if prettyPrint and closeTag:
    -                s.append(space)
    -            s.append(closeTag)
    -            if prettyPrint and closeTag and self.nextSibling:
    -                s.append("\n")
    -            s = ''.join(s)
    -        return s
    -
    -    def decompose(self):
    -        """Recursively destroys the contents of this tree."""
    -        self.extract()
    -        if len(self.contents) == 0:
    -            return
    -        current = self.contents[0]
    -        while current is not None:
    -            next = current.next
    -            if isinstance(current, Tag):
    -                del current.contents[:]
    -            current.parent = None
    -            current.previous = None
    -            current.previousSibling = None
    -            current.next = None
    -            current.nextSibling = None
    -            current = next
    -
    -    def prettify(self, encoding = DEFAULT_OUTPUT_ENCODING):
    -        return self.__str__(encoding, True)
    -
    -    def renderContents(self, encoding = DEFAULT_OUTPUT_ENCODING,
    -                       prettyPrint = False, indentLevel = 0):
    -        """Renders the contents of this tag as a string in the given
    -        encoding. If encoding is None, returns a Unicode string.."""
    -        s = []
    -        for c in self:
    -            text = None
    -            if isinstance(c, NavigableString):
    -                text = c.__str__(encoding)
    -            elif isinstance(c, Tag):
    -                s.append(c.__str__(encoding, prettyPrint, indentLevel))
    -            if text and prettyPrint:
    -                text = text.strip()
    -            if text:
    -                if prettyPrint:
    -                    s.append(" " * (indentLevel - 1))
    -                s.append(text)
    -                if prettyPrint:
    -                    s.append("\n")
    -        return ''.join(s)
    -
    -    #Soup methods
    -
    -    def find(self, name = None, attrs = {}, recursive = True, text = None,
    -             **kwargs):
    -        """Return only the first child of this Tag matching the given
    -        criteria."""
    -        r = None
    -        l = self.findAll(name, attrs, recursive, text, 1, **kwargs)
    -        if l:
    -            r = l[0]
    -        return r
    -    findChild = find
    -
    -    def findAll(self, name = None, attrs = {}, recursive = True, text = None,
    -                limit = None, **kwargs):
    -        """Extracts a list of Tag objects that match the given
    -        criteria.  You can specify the name of the Tag and any
    -        attributes you want the Tag to have.
    -
    -        The value of a key-value pair in the 'attrs' map can be a
    -        string, a list of strings, a regular expression object, or a
    -        callable that takes a string and returns whether or not the
    -        string matches for some custom definition of 'matches'. The
    -        same is true of the tag name."""
    -        generator = self.recursiveChildGenerator
    -        if not recursive:
    -            generator = self.childGenerator
    -        return self._findAll(name, attrs, text, limit, generator, **kwargs)
    -    findChildren = findAll
    -
    -    # Pre-3.x compatibility methods
    -    first = find
    -    fetch = findAll
    -
    -    def fetchText(self, text = None, recursive = True, limit = None):
    -        return self.findAll(text = text, recursive = recursive, limit = limit)
    -
    -    def firstText(self, text = None, recursive = True):
    -        return self.find(text = text, recursive = recursive)
    -
    -    #Private methods
    -
    -    def _getAttrMap(self):
    -        """Initializes a map representation of this tag's attributes,
    -        if not already initialized."""
    -        if not getattr(self, 'attrMap'):
    -            self.attrMap = {}
    -            for (key, value) in self.attrs:
    -                self.attrMap[key] = value
    -        return self.attrMap
    -
    -    #Generator methods
    -    def childGenerator(self):
    -        # Just use the iterator from the contents
    -        return iter(self.contents)
    -
    -    def recursiveChildGenerator(self):
    -        if not len(self.contents):
    -            raise StopIteration
    -        stopNode = self._lastRecursiveChild().next
    -        current = self.contents[0]
    -        while current is not stopNode:
    -            yield current
    -            current = current.next
    -
    -
    -# Next, a couple classes to represent queries and their results.
    -class SoupStrainer:
    -    """Encapsulates a number of ways of matching a markup element (tag or
    -    text)."""
    -
    -    def __init__(self, name = None, attrs = {}, text = None, **kwargs):
    -        self.name = name
    -        if isinstance(attrs, basestring):
    -            kwargs['class'] = _match_css_class(attrs)
    -            attrs = None
    -        if kwargs:
    -            if attrs:
    -                attrs = attrs.copy()
    -                attrs.update(kwargs)
    -            else:
    -                attrs = kwargs
    -        self.attrs = attrs
    -        self.text = text
    -
    -    def __str__(self):
    -        if self.text:
    -            return self.text
    -        else:
    -            return "%s|%s" % (self.name, self.attrs)
    -
    -    def searchTag(self, markupName = None, markupAttrs = {}):
    -        found = None
    -        markup = None
    -        if isinstance(markupName, Tag):
    -            markup = markupName
    -            markupAttrs = markup
    -        callFunctionWithTagData = callable(self.name) \
    -                                and not isinstance(markupName, Tag)
    -
    -        if (not self.name) \
    -               or callFunctionWithTagData \
    -               or (markup and self._matches(markup, self.name)) \
    -               or (not markup and self._matches(markupName, self.name)):
    -            if callFunctionWithTagData:
    -                match = self.name(markupName, markupAttrs)
    -            else:
    -                match = True
    -                markupAttrMap = None
    -                for attr, matchAgainst in self.attrs.items():
    -                    if not markupAttrMap:
    -                         if hasattr(markupAttrs, 'get'):
    -                            markupAttrMap = markupAttrs
    -                         else:
    -                            markupAttrMap = {}
    -                            for k, v in markupAttrs:
    -                                markupAttrMap[k] = v
    -                    attrValue = markupAttrMap.get(attr)
    -                    if not self._matches(attrValue, matchAgainst):
    -                        match = False
    -                        break
    -            if match:
    -                if markup:
    -                    found = markup
    -                else:
    -                    found = markupName
    -        return found
    -
    -    def search(self, markup):
    -        #print 'looking for %s in %s' % (self, markup)
    -        found = None
    -        # If given a list of items, scan it for a text element that
    -        # matches.
    -        if hasattr(markup, "__iter__") \
    -                and not isinstance(markup, Tag):
    -            for element in markup:
    -                if isinstance(element, NavigableString) \
    -                       and self.search(element):
    -                    found = element
    -                    break
    -        # If it's a Tag, make sure its name or attributes match.
    -        # Don't bother with Tags if we're searching for text.
    -        elif isinstance(markup, Tag):
    -            if not self.text:
    -                found = self.searchTag(markup)
    -        # If it's text, make sure the text matches.
    -        elif isinstance(markup, NavigableString) or \
    -                 isinstance(markup, basestring):
    -            if self._matches(markup, self.text):
    -                found = markup
    -        else:
    -            raise Exception, "I don't know how to match against a %s" \
    -                  % markup.__class__
    -        return found
    -
    -    def _matches(self, markup, matchAgainst):
    -        #print "Matching %s against %s" % (markup, matchAgainst)
    -        result = False
    -        if matchAgainst is True:
    -            result = markup is not None
    -        elif callable(matchAgainst):
    -            result = matchAgainst(markup)
    -        else:
    -            #Custom match methods take the tag as an argument, but all
    -            #other ways of matching match the tag name as a string.
    -            if isinstance(markup, Tag):
    -                markup = markup.name
    -            if markup and not isinstance(markup, basestring):
    -                markup = unicode(markup)
    -            #Now we know that chunk is either a string, or None.
    -            if hasattr(matchAgainst, 'match'):
    -                # It's a regexp object.
    -                result = markup and matchAgainst.search(markup)
    -            elif hasattr(matchAgainst, '__iter__'): # list-like
    -                result = markup in matchAgainst
    -            elif hasattr(matchAgainst, 'items'):
    -                result = markup.has_key(matchAgainst)
    -            elif matchAgainst and isinstance(markup, basestring):
    -                if isinstance(markup, unicode):
    -                    matchAgainst = unicode(matchAgainst)
    -                else:
    -                    matchAgainst = str(matchAgainst)
    -
    -            if not result:
    -                result = matchAgainst == markup
    -        return result
    -
    -class ResultSet(list):
    -    """A ResultSet is just a list that keeps track of the SoupStrainer
    -    that created it."""
    -    def __init__(self, source):
    -        list.__init__([])
    -        self.source = source
    -
    -# Now, some helper functions.
    -
    -def buildTagMap(default, *args):
    -    """Turns a list of maps, lists, or scalars into a single map.
    -    Used to build the SELF_CLOSING_TAGS, NESTABLE_TAGS, and
    -    NESTING_RESET_TAGS maps out of lists and partial maps."""
    -    built = {}
    -    for portion in args:
    -        if hasattr(portion, 'items'):
    -            #It's a map. Merge it.
    -            for k, v in portion.items():
    -                built[k] = v
    -        elif hasattr(portion, '__iter__'): # is a list
    -            #It's a list. Map each item to the default.
    -            for k in portion:
    -                built[k] = default
    -        else:
    -            #It's a scalar. Map it to the default.
    -            built[portion] = default
    -    return built
    -
    -# Now, the parser classes.
    -
    -class BeautifulStoneSoup(Tag, SGMLParser):
    -
    -    """This class contains the basic parser and search code. It defines
    -    a parser that knows nothing about tag behavior except for the
    -    following:
    -
    -      You can't close a tag without closing all the tags it encloses.
    -      That is, "<foo><bar></foo>" actually means
    -      "<foo><bar></bar></foo>".
    -
    -    [Another possible explanation is "<foo><bar /></foo>", but since
    -    this class defines no SELF_CLOSING_TAGS, it will never use that
    -    explanation.]
    -
    -    This class is useful for parsing XML or made-up markup languages,
    -    or when BeautifulSoup makes an assumption counter to what you were
    -    expecting."""
    -
    -    SELF_CLOSING_TAGS = {}
    -    NESTABLE_TAGS = {}
    -    RESET_NESTING_TAGS = {}
    -    QUOTE_TAGS = {}
    -    PRESERVE_WHITESPACE_TAGS = []
    -
    -    MARKUP_MASSAGE = [(re.compile('(<[^<>]*)/>'),
    -                       lambda x: x.group(1) + ' />'),
    -                      (re.compile('<!\s+([^<>]*)>'),
    -                       lambda x: '<!' + x.group(1) + '>')
    -                      ]
    -
    -    ROOT_TAG_NAME = u'[document]'
    -
    -    HTML_ENTITIES = "html"
    -    XML_ENTITIES = "xml"
    -    XHTML_ENTITIES = "xhtml"
    -    # TODO: This only exists for backwards-compatibility
    -    ALL_ENTITIES = XHTML_ENTITIES
    -
    -    # Used when determining whether a text node is all whitespace and
    -    # can be replaced with a single space. A text node that contains
    -    # fancy Unicode spaces (usually non-breaking) should be left
    -    # alone.
    -    STRIP_ASCII_SPACES = { 9: None, 10: None, 12: None, 13: None, 32: None, }
    -
    -    def __init__(self, markup = "", parseOnlyThese = None, fromEncoding = None,
    -                 markupMassage = True, smartQuotesTo = XML_ENTITIES,
    -                 convertEntities = None, selfClosingTags = None, isHTML = False):
    -        """The Soup object is initialized as the 'root tag', and the
    -        provided markup (which can be a string or a file-like object)
    -        is fed into the underlying parser.
    -
    -        sgmllib will process most bad HTML, and the BeautifulSoup
    -        class has some tricks for dealing with some HTML that kills
    -        sgmllib, but Beautiful Soup can nonetheless choke or lose data
    -        if your data uses self-closing tags or declarations
    -        incorrectly.
    -
    -        By default, Beautiful Soup uses regexes to sanitize input,
    -        avoiding the vast majority of these problems. If the problems
    -        don't apply to you, pass in False for markupMassage, and
    -        you'll get better performance.
    -
    -        The default parser massage techniques fix the two most common
    -        instances of invalid HTML that choke sgmllib:
    -
    -         <br/> (No space between name of closing tag and tag close)
    -         <! --Comment--> (Extraneous whitespace in declaration)
    -
    -        You can pass in a custom list of (RE object, replace method)
    -        tuples to get Beautiful Soup to scrub your input the way you
    -        want."""
    -
    -        self.parseOnlyThese = parseOnlyThese
    -        self.fromEncoding = fromEncoding
    -        self.smartQuotesTo = smartQuotesTo
    -        self.convertEntities = convertEntities
    -        # Set the rules for how we'll deal with the entities we
    -        # encounter
    -        if self.convertEntities:
    -            # It doesn't make sense to convert encoded characters to
    -            # entities even while you're converting entities to Unicode.
    -            # Just convert it all to Unicode.
    -            self.smartQuotesTo = None
    -            if convertEntities == self.HTML_ENTITIES:
    -                self.convertXMLEntities = False
    -                self.convertHTMLEntities = True
    -                self.escapeUnrecognizedEntities = True
    -            elif convertEntities == self.XHTML_ENTITIES:
    -                self.convertXMLEntities = True
    -                self.convertHTMLEntities = True
    -                self.escapeUnrecognizedEntities = False
    -            elif convertEntities == self.XML_ENTITIES:
    -                self.convertXMLEntities = True
    -                self.convertHTMLEntities = False
    -                self.escapeUnrecognizedEntities = False
    -        else:
    -            self.convertXMLEntities = False
    -            self.convertHTMLEntities = False
    -            self.escapeUnrecognizedEntities = False
    -
    -        self.instanceSelfClosingTags = buildTagMap(None, selfClosingTags)
    -        SGMLParser.__init__(self)
    -
    -        if hasattr(markup, 'read'):        # It's a file-type object.
    -            markup = markup.read()
    -        self.markup = markup
    -        self.markupMassage = markupMassage
    -        try:
    -            self._feed(isHTML = isHTML)
    -        except StopParsing:
    -            pass
    -        self.markup = None                 # The markup can now be GCed
    -
    -    def convert_charref(self, name):
    -        """This method fixes a bug in Python's SGMLParser."""
    -        try:
    -            n = int(name)
    -        except ValueError:
    -            return
    -        if not 0 <= n <= 127 : # ASCII ends at 127, not 255
    -            return
    -        return self.convert_codepoint(n)
    -
    -    def _feed(self, inDocumentEncoding = None, isHTML = False):
    -        # Convert the document to Unicode.
    -        markup = self.markup
    -        if isinstance(markup, unicode):
    -            if not hasattr(self, 'originalEncoding'):
    -                self.originalEncoding = None
    -        else:
    -            dammit = UnicodeDammit\
    -                     (markup, [self.fromEncoding, inDocumentEncoding],
    -                      smartQuotesTo = self.smartQuotesTo, isHTML = isHTML)
    -            markup = dammit.unicode
    -            self.originalEncoding = dammit.originalEncoding
    -            self.declaredHTMLEncoding = dammit.declaredHTMLEncoding
    -        if markup:
    -            if self.markupMassage:
    -                if not hasattr(self.markupMassage, "__iter__"):
    -                    self.markupMassage = self.MARKUP_MASSAGE
    -                for fix, m in self.markupMassage:
    -                    markup = fix.sub(m, markup)
    -                # TODO: We get rid of markupMassage so that the
    -                # soup object can be deepcopied later on. Some
    -                # Python installations can't copy regexes. If anyone
    -                # was relying on the existence of markupMassage, this
    -                # might cause problems.
    -                del(self.markupMassage)
    -        self.reset()
    -
    -        SGMLParser.feed(self, markup)
    -        # Close out any unfinished strings and close all the open tags.
    -        self.endData()
    -        while self.currentTag.name != self.ROOT_TAG_NAME:
    -            self.popTag()
    -
    -    def __getattr__(self, methodName):
    -        """This method routes method call requests to either the SGMLParser
    -        superclass or the Tag superclass, depending on the method name."""
    -        #print "__getattr__ called on %s.%s" % (self.__class__, methodName)
    -
    -        if methodName.startswith('start_') or methodName.startswith('end_') \
    -               or methodName.startswith('do_'):
    -            return SGMLParser.__getattr__(self, methodName)
    -        elif not methodName.startswith('__'):
    -            return Tag.__getattr__(self, methodName)
    -        else:
    -            raise AttributeError
    -
    -    def isSelfClosingTag(self, name):
    -        """Returns true iff the given string is the name of a
    -        self-closing tag according to this parser."""
    -        return self.SELF_CLOSING_TAGS.has_key(name) \
    -               or self.instanceSelfClosingTags.has_key(name)
    -
    -    def reset(self):
    -        Tag.__init__(self, self, self.ROOT_TAG_NAME)
    -        self.hidden = 1
    -        SGMLParser.reset(self)
    -        self.currentData = []
    -        self.currentTag = None
    -        self.tagStack = []
    -        self.quoteStack = []
    -        self.pushTag(self)
    -
    -    def popTag(self):
    -        tag = self.tagStack.pop()
    -
    -        #print "Pop", tag.name
    -        if self.tagStack:
    -            self.currentTag = self.tagStack[-1]
    -        return self.currentTag
    -
    -    def pushTag(self, tag):
    -        #print "Push", tag.name
    -        if self.currentTag:
    -            self.currentTag.contents.append(tag)
    -        self.tagStack.append(tag)
    -        self.currentTag = self.tagStack[-1]
    -
    -    def endData(self, containerClass = NavigableString):
    -        if self.currentData:
    -            currentData = u''.join(self.currentData)
    -            if (currentData.translate(self.STRIP_ASCII_SPACES) == '' and
    -                not set([tag.name for tag in self.tagStack]).intersection(
    -                    self.PRESERVE_WHITESPACE_TAGS)):
    -                if '\n' in currentData:
    -                    currentData = '\n'
    -                else:
    -                    currentData = ' '
    -            self.currentData = []
    -            if self.parseOnlyThese and len(self.tagStack) <= 1 and \
    -                   (not self.parseOnlyThese.text or \
    -                    not self.parseOnlyThese.search(currentData)):
    -                return
    -            o = containerClass(currentData)
    -            o.setup(self.currentTag, self.previous)
    -            if self.previous:
    -                self.previous.next = o
    -            self.previous = o
    -            self.currentTag.contents.append(o)
    -
    -
    -    def _popToTag(self, name, inclusivePop = True):
    -        """Pops the tag stack up to and including the most recent
    -        instance of the given tag. If inclusivePop is false, pops the tag
    -        stack up to but *not* including the most recent instqance of
    -        the given tag."""
    -        #print "Popping to %s" % name
    -        if name == self.ROOT_TAG_NAME:
    -            return
    -
    -        numPops = 0
    -        mostRecentTag = None
    -        for i in range(len(self.tagStack) - 1, 0, -1):
    -            if name == self.tagStack[i].name:
    -                numPops = len(self.tagStack) - i
    -                break
    -        if not inclusivePop:
    -            numPops = numPops - 1
    -
    -        for i in range(0, numPops):
    -            mostRecentTag = self.popTag()
    -        return mostRecentTag
    -
    -    def _smartPop(self, name):
    -
    -        """We need to pop up to the previous tag of this type, unless
    -        one of this tag's nesting reset triggers comes between this
    -        tag and the previous tag of this type, OR unless this tag is a
    -        generic nesting trigger and another generic nesting trigger
    -        comes between this tag and the previous tag of this type.
    -
    -        Examples:
    -         <p>Foo<b>Bar *<p>* should pop to 'p', not 'b'.
    -         <p>Foo<table>Bar *<p>* should pop to 'table', not 'p'.
    -         <p>Foo<table><tr>Bar *<p>* should pop to 'tr', not 'p'.
    -
    -         <li><ul><li> *<li>* should pop to 'ul', not the first 'li'.
    -         <tr><table><tr> *<tr>* should pop to 'table', not the first 'tr'
    -         <td><tr><td> *<td>* should pop to 'tr', not the first 'td'
    -        """
    -
    -        nestingResetTriggers = self.NESTABLE_TAGS.get(name)
    -        isNestable = nestingResetTriggers != None
    -        isResetNesting = self.RESET_NESTING_TAGS.has_key(name)
    -        popTo = None
    -        inclusive = True
    -        for i in range(len(self.tagStack) - 1, 0, -1):
    -            p = self.tagStack[i]
    -            if (not p or p.name == name) and not isNestable:
    -                #Non-nestable tags get popped to the top or to their
    -                #last occurance.
    -                popTo = name
    -                break
    -            if (nestingResetTriggers is not None
    -                and p.name in nestingResetTriggers) \
    -                or (nestingResetTriggers is None and isResetNesting
    -                    and self.RESET_NESTING_TAGS.has_key(p.name)):
    -
    -                #If we encounter one of the nesting reset triggers
    -                #peculiar to this tag, or we encounter another tag
    -                #that causes nesting to reset, pop up to but not
    -                #including that tag.
    -                popTo = p.name
    -                inclusive = False
    -                break
    -            p = p.parent
    -        if popTo:
    -            self._popToTag(popTo, inclusive)
    -
    -    def unknown_starttag(self, name, attrs, selfClosing = 0):
    -        #print "Start tag %s: %s" % (name, attrs)
    -        if self.quoteStack:
    -            #This is not a real tag.
    -            #print "<%s> is not real!" % name
    -            attrs = ''.join([' %s="%s"' % (x, y) for x, y in attrs])
    -            self.handle_data('<%s%s>' % (name, attrs))
    -            return
    -        self.endData()
    -
    -        if not self.isSelfClosingTag(name) and not selfClosing:
    -            self._smartPop(name)
    -
    -        if self.parseOnlyThese and len(self.tagStack) <= 1 \
    -               and (self.parseOnlyThese.text or not self.parseOnlyThese.searchTag(name, attrs)):
    -            return
    -
    -        tag = Tag(self, name, attrs, self.currentTag, self.previous)
    -        if self.previous:
    -            self.previous.next = tag
    -        self.previous = tag
    -        self.pushTag(tag)
    -        if selfClosing or self.isSelfClosingTag(name):
    -            self.popTag()
    -        if name in self.QUOTE_TAGS:
    -            #print "Beginning quote (%s)" % name
    -            self.quoteStack.append(name)
    -            self.literal = 1
    -        return tag
    -
    -    def unknown_endtag(self, name):
    -        #print "End tag %s" % name
    -        if self.quoteStack and self.quoteStack[-1] != name:
    -            #This is not a real end tag.
    -            #print "</%s> is not real!" % name
    -            self.handle_data('</%s>' % name)
    -            return
    -        self.endData()
    -        self._popToTag(name)
    -        if self.quoteStack and self.quoteStack[-1] == name:
    -            self.quoteStack.pop()
    -            self.literal = (len(self.quoteStack) > 0)
    -
    -    def handle_data(self, data):
    -        self.currentData.append(data)
    -
    -    def _toStringSubclass(self, text, subclass):
    -        """Adds a certain piece of text to the tree as a NavigableString
    -        subclass."""
    -        self.endData()
    -        self.handle_data(text)
    -        self.endData(subclass)
    -
    -    def handle_pi(self, text):
    -        """Handle a processing instruction as a ProcessingInstruction
    -        object, possibly one with a %SOUP-ENCODING% slot into which an
    -        encoding will be plugged later."""
    -        if text[:3] == "xml":
    -            text = u"xml version='1.0' encoding='%SOUP-ENCODING%'"
    -        self._toStringSubclass(text, ProcessingInstruction)
    -
    -    def handle_comment(self, text):
    -        "Handle comments as Comment objects."
    -        self._toStringSubclass(text, Comment)
    -
    -    def handle_charref(self, ref):
    -        "Handle character references as data."
    -        if self.convertEntities:
    -            data = unichr(int(ref))
    -        else:
    -            data = '&#%s;' % ref
    -        self.handle_data(data)
    -
    -    def handle_entityref(self, ref):
    -        """Handle entity references as data, possibly converting known
    -        HTML and/or XML entity references to the corresponding Unicode
    -        characters."""
    -        data = None
    -        if self.convertHTMLEntities:
    -            try:
    -                data = unichr(name2codepoint[ref])
    -            except KeyError:
    -                pass
    -
    -        if not data and self.convertXMLEntities:
    -                data = self.XML_ENTITIES_TO_SPECIAL_CHARS.get(ref)
    -
    -        if not data and self.convertHTMLEntities and \
    -            not self.XML_ENTITIES_TO_SPECIAL_CHARS.get(ref):
    -                # TODO: We've got a problem here. We're told this is
    -                # an entity reference, but it's not an XML entity
    -                # reference or an HTML entity reference. Nonetheless,
    -                # the logical thing to do is to pass it through as an
    -                # unrecognized entity reference.
    -                #
    -                # Except: when the input is "&carol;" this function
    -                # will be called with input "carol". When the input is
    -                # "AT&T", this function will be called with input
    -                # "T". We have no way of knowing whether a semicolon
    -                # was present originally, so we don't know whether
    -                # this is an unknown entity or just a misplaced
    -                # ampersand.
    -                #
    -                # The more common case is a misplaced ampersand, so I
    -                # escape the ampersand and omit the trailing semicolon.
    -                data = "&%s" % ref
    -        if not data:
    -            # This case is different from the one above, because we
    -            # haven't already gone through a supposedly comprehensive
    -            # mapping of entities to Unicode characters. We might not
    -            # have gone through any mapping at all. So the chances are
    -            # very high that this is a real entity, and not a
    -            # misplaced ampersand.
    -            data = "&%s;" % ref
    -        self.handle_data(data)
    -
    -    def handle_decl(self, data):
    -        "Handle DOCTYPEs and the like as Declaration objects."
    -        self._toStringSubclass(data, Declaration)
    -
    -    def parse_declaration(self, i):
    -        """Treat a bogus SGML declaration as raw data. Treat a CDATA
    -        declaration as a CData object."""
    -        j = None
    -        if self.rawdata[i:i + 9] == '<![CDATA[':
    -             k = self.rawdata.find(']]>', i)
    -             if k == -1:
    -                 k = len(self.rawdata)
    -             data = self.rawdata[i + 9:k]
    -             j = k + 3
    -             self._toStringSubclass(data, CData)
    -        else:
    -            try:
    -                j = SGMLParser.parse_declaration(self, i)
    -            except SGMLParseError:
    -                toHandle = self.rawdata[i:]
    -                self.handle_data(toHandle)
    -                j = i + len(toHandle)
    -        return j
    -
    -class BeautifulSoup(BeautifulStoneSoup):
    -
    -    """This parser knows the following facts about HTML:
    -
    -    * Some tags have no closing tag and should be interpreted as being
    -      closed as soon as they are encountered.
    -
    -    * The text inside some tags (ie. 'script') may contain tags which
    -      are not really part of the document and which should be parsed
    -      as text, not tags. If you want to parse the text as tags, you can
    -      always fetch it and parse it explicitly.
    -
    -    * Tag nesting rules:
    -
    -      Most tags can't be nested at all. For instance, the occurance of
    -      a <p> tag should implicitly close the previous <p> tag.
    -
    -       <p>Para1<p>Para2
    -        should be transformed into:
    -       <p>Para1</p><p>Para2
    -
    -      Some tags can be nested arbitrarily. For instance, the occurance
    -      of a <blockquote> tag should _not_ implicitly close the previous
    -      <blockquote> tag.
    -
    -       Alice said: <blockquote>Bob said: <blockquote>Blah
    -        should NOT be transformed into:
    -       Alice said: <blockquote>Bob said: </blockquote><blockquote>Blah
    -
    -      Some tags can be nested, but the nesting is reset by the
    -      interposition of other tags. For instance, a <tr> tag should
    -      implicitly close the previous <tr> tag within the same <table>,
    -      but not close a <tr> tag in another table.
    -
    -       <table><tr>Blah<tr>Blah
    -        should be transformed into:
    -       <table><tr>Blah</tr><tr>Blah
    -        but,
    -       <tr>Blah<table><tr>Blah
    -        should NOT be transformed into
    -       <tr>Blah<table></tr><tr>Blah
    -
    -    Differing assumptions about tag nesting rules are a major source
    -    of problems with the BeautifulSoup class. If BeautifulSoup is not
    -    treating as nestable a tag your page author treats as nestable,
    -    try ICantBelieveItsBeautifulSoup, MinimalSoup, or
    -    BeautifulStoneSoup before writing your own subclass."""
    -
    -    def __init__(self, *args, **kwargs):
    -        if not kwargs.has_key('smartQuotesTo'):
    -            kwargs['smartQuotesTo'] = self.HTML_ENTITIES
    -        kwargs['isHTML'] = True
    -        BeautifulStoneSoup.__init__(self, *args, **kwargs)
    -
    -    SELF_CLOSING_TAGS = buildTagMap(None,
    -                                    ('br' , 'hr', 'input', 'img', 'meta',
    -                                    'spacer', 'link', 'frame', 'base', 'col'))
    -
    -    PRESERVE_WHITESPACE_TAGS = set(['pre', 'textarea'])
    -
    -    QUOTE_TAGS = {'script' : None, 'textarea' : None}
    -
    -    #According to the HTML standard, each of these inline tags can
    -    #contain another tag of the same type. Furthermore, it's common
    -    #to actually use these tags this way.
    -    NESTABLE_INLINE_TAGS = ('span', 'font', 'q', 'object', 'bdo', 'sub', 'sup',
    -                            'center')
    -
    -    #According to the HTML standard, these block tags can contain
    -    #another tag of the same type. Furthermore, it's common
    -    #to actually use these tags this way.
    -    NESTABLE_BLOCK_TAGS = ('blockquote', 'div', 'fieldset', 'ins', 'del')
    -
    -    #Lists can contain other lists, but there are restrictions.
    -    NESTABLE_LIST_TAGS = { 'ol' : [],
    -                           'ul' : [],
    -                           'li' : ['ul', 'ol'],
    -                           'dl' : [],
    -                           'dd' : ['dl'],
    -                           'dt' : ['dl'] }
    -
    -    #Tables can contain other tables, but there are restrictions.
    -    NESTABLE_TABLE_TAGS = {'table' : [],
    -                           'tr' : ['table', 'tbody', 'tfoot', 'thead'],
    -                           'td' : ['tr'],
    -                           'th' : ['tr'],
    -                           'thead' : ['table'],
    -                           'tbody' : ['table'],
    -                           'tfoot' : ['table'],
    -                           }
    -
    -    NON_NESTABLE_BLOCK_TAGS = ('address', 'form', 'p', 'pre')
    -
    -    #If one of these tags is encountered, all tags up to the next tag of
    -    #this type are popped.
    -    RESET_NESTING_TAGS = buildTagMap(None, NESTABLE_BLOCK_TAGS, 'noscript',
    -                                     NON_NESTABLE_BLOCK_TAGS,
    -                                     NESTABLE_LIST_TAGS,
    -                                     NESTABLE_TABLE_TAGS)
    -
    -    NESTABLE_TAGS = buildTagMap([], NESTABLE_INLINE_TAGS, NESTABLE_BLOCK_TAGS,
    -                                NESTABLE_LIST_TAGS, NESTABLE_TABLE_TAGS)
    -
    -    # Used to detect the charset in a META tag; see start_meta
    -    CHARSET_RE = re.compile("((^|;)\s*charset=)([^;]*)", re.M)
    -
    -    def start_meta(self, attrs):
    -        """Beautiful Soup can detect a charset included in a META tag,
    -        try to convert the document to that charset, and re-parse the
    -        document from the beginning."""
    -        httpEquiv = None
    -        contentType = None
    -        contentTypeIndex = None
    -        tagNeedsEncodingSubstitution = False
    -
    -        for i in range(0, len(attrs)):
    -            key, value = attrs[i]
    -            key = key.lower()
    -            if key == 'http-equiv':
    -                httpEquiv = value
    -            elif key == 'content':
    -                contentType = value
    -                contentTypeIndex = i
    -
    -        if httpEquiv and contentType: # It's an interesting meta tag.
    -            match = self.CHARSET_RE.search(contentType)
    -            if match:
    -                if (self.declaredHTMLEncoding is not None or
    -                    self.originalEncoding == self.fromEncoding):
    -                    # An HTML encoding was sniffed while converting
    -                    # the document to Unicode, or an HTML encoding was
    -                    # sniffed during a previous pass through the
    -                    # document, or an encoding was specified
    -                    # explicitly and it worked. Rewrite the meta tag.
    -                    def rewrite(match):
    -                        return match.group(1) + "%SOUP-ENCODING%"
    -                    newAttr = self.CHARSET_RE.sub(rewrite, contentType)
    -                    attrs[contentTypeIndex] = (attrs[contentTypeIndex][0],
    -                                               newAttr)
    -                    tagNeedsEncodingSubstitution = True
    -                else:
    -                    # This is our first pass through the document.
    -                    # Go through it again with the encoding information.
    -                    newCharset = match.group(3)
    -                    if newCharset and newCharset != self.originalEncoding:
    -                        self.declaredHTMLEncoding = newCharset
    -                        self._feed(self.declaredHTMLEncoding)
    -                        raise StopParsing
    -                    pass
    -        tag = self.unknown_starttag("meta", attrs)
    -        if tag and tagNeedsEncodingSubstitution:
    -            tag.containsSubstitutions = True
    -
    -class StopParsing(Exception):
    -    pass
    -
    -class ICantBelieveItsBeautifulSoup(BeautifulSoup):
    -
    -    """The BeautifulSoup class is oriented towards skipping over
    -    common HTML errors like unclosed tags. However, sometimes it makes
    -    errors of its own. For instance, consider this fragment:
    -
    -     <b>Foo<b>Bar</b></b>
    -
    -    This is perfectly valid (if bizarre) HTML. However, the
    -    BeautifulSoup class will implicitly close the first b tag when it
    -    encounters the second 'b'. It will think the author wrote
    -    "<b>Foo<b>Bar", and didn't close the first 'b' tag, because
    -    there's no real-world reason to bold something that's already
    -    bold. When it encounters '</b></b>' it will close two more 'b'
    -    tags, for a grand total of three tags closed instead of two. This
    -    can throw off the rest of your document structure. The same is
    -    true of a number of other tags, listed below.
    -
    -    It's much more common for someone to forget to close a 'b' tag
    -    than to actually use nested 'b' tags, and the BeautifulSoup class
    -    handles the common case. This class handles the not-co-common
    -    case: where you can't believe someone wrote what they did, but
    -    it's valid HTML and BeautifulSoup screwed up by assuming it
    -    wouldn't be."""
    -
    -    I_CANT_BELIEVE_THEYRE_NESTABLE_INLINE_TAGS = \
    -     ('em', 'big', 'i', 'small', 'tt', 'abbr', 'acronym', 'strong',
    -      'cite', 'code', 'dfn', 'kbd', 'samp', 'strong', 'var', 'b',
    -      'big')
    -
    -    I_CANT_BELIEVE_THEYRE_NESTABLE_BLOCK_TAGS = ('noscript',)
    -
    -    NESTABLE_TAGS = buildTagMap([], BeautifulSoup.NESTABLE_TAGS,
    -                                I_CANT_BELIEVE_THEYRE_NESTABLE_BLOCK_TAGS,
    -                                I_CANT_BELIEVE_THEYRE_NESTABLE_INLINE_TAGS)
    -
    -class MinimalSoup(BeautifulSoup):
    -    """The MinimalSoup class is for parsing HTML that contains
    -    pathologically bad markup. It makes no assumptions about tag
    -    nesting, but it does know which tags are self-closing, that
    -    <script> tags contain Javascript and should not be parsed, that
    -    META tags may contain encoding information, and so on.
    -
    -    This also makes it better for subclassing than BeautifulStoneSoup
    -    or BeautifulSoup."""
    -
    -    RESET_NESTING_TAGS = buildTagMap('noscript')
    -    NESTABLE_TAGS = {}
    -
    -class BeautifulSOAP(BeautifulStoneSoup):
    -    """This class will push a tag with only a single string child into
    -    the tag's parent as an attribute. The attribute's name is the tag
    -    name, and the value is the string child. An example should give
    -    the flavor of the change:
    -
    -    <foo><bar>baz</bar></foo>
    -     =>
    -    <foo bar="baz"><bar>baz</bar></foo>
    -
    -    You can then access fooTag['bar'] instead of fooTag.barTag.string.
    -
    -    This is, of course, useful for scraping structures that tend to
    -    use subelements instead of attributes, such as SOAP messages. Note
    -    that it modifies its input, so don't print the modified version
    -    out.
    -
    -    I'm not sure how many people really want to use this class; let me
    -    know if you do. Mainly I like the name."""
    -
    -    def popTag(self):
    -        if len(self.tagStack) > 1:
    -            tag = self.tagStack[-1]
    -            parent = self.tagStack[-2]
    -            parent._getAttrMap()
    -            if (isinstance(tag, Tag) and len(tag.contents) == 1 and
    -                isinstance(tag.contents[0], NavigableString) and
    -                not parent.attrMap.has_key(tag.name)):
    -                parent[tag.name] = tag.contents[0]
    -        BeautifulStoneSoup.popTag(self)
    -
    -#Enterprise class names! It has come to our attention that some people
    -#think the names of the Beautiful Soup parser classes are too silly
    -#and "unprofessional" for use in enterprise screen-scraping. We feel
    -#your pain! For such-minded folk, the Beautiful Soup Consortium And
    -#All-Night Kosher Bakery recommends renaming this file to
    -#"RobustParser.py" (or, in cases of extreme enterprisiness,
    -#"RobustParserBeanInterface.class") and using the following
    -#enterprise-friendly class aliases:
    -class RobustXMLParser(BeautifulStoneSoup):
    -    pass
    -class RobustHTMLParser(BeautifulSoup):
    -    pass
    -class RobustWackAssHTMLParser(ICantBelieveItsBeautifulSoup):
    -    pass
    -class RobustInsanelyWackAssHTMLParser(MinimalSoup):
    -    pass
    -class SimplifyingSOAPParser(BeautifulSOAP):
    -    pass
    -
    -######################################################
    -#
    -# Bonus library: Unicode, Dammit
    -#
    -# This class forces XML data into a standard format (usually to UTF-8
    -# or Unicode).  It is heavily based on code from Mark Pilgrim's
    -# Universal Feed Parser. It does not rewrite the XML or HTML to
    -# reflect a new encoding: that happens in BeautifulStoneSoup.handle_pi
    -# (XML) and BeautifulSoup.start_meta (HTML).
    -
    -# Autodetects character encodings.
    -# Download from http://chardet.feedparser.org/
    -try:
    -    import chardet
    -#    import chardet.constants
    -#    chardet.constants._debug = 1
    -except ImportError:
    -    chardet = None
    -
    -# cjkcodecs and iconv_codec make Python know about more character encodings.
    -# Both are available from http://cjkpython.i18n.org/
    -# They're built in if you use Python 2.4.
    -try:
    -    import cjkcodecs.aliases
    -except ImportError:
    -    pass
    -try:
    -    import iconv_codec
    -except ImportError:
    -    pass
    -
    -class UnicodeDammit:
    -    """A class for detecting the encoding of a *ML document and
    -    converting it to a Unicode string. If the source encoding is
    -    windows-1252, can replace MS smart quotes with their HTML or XML
    -    equivalents."""
    -
    -    # This dictionary maps commonly seen values for "charset" in HTML
    -    # meta tags to the corresponding Python codec names. It only covers
    -    # values that aren't in Python's aliases and can't be determined
    -    # by the heuristics in find_codec.
    -    CHARSET_ALIASES = { "macintosh" : "mac-roman",
    -                        "x-sjis" : "shift-jis" }
    -
    -    def __init__(self, markup, overrideEncodings = [],
    -                 smartQuotesTo = 'xml', isHTML = False):
    -        self.declaredHTMLEncoding = None
    -        self.markup, documentEncoding, sniffedEncoding = \
    -                     self._detectEncoding(markup, isHTML)
    -        self.smartQuotesTo = smartQuotesTo
    -        self.triedEncodings = []
    -        if markup == '' or isinstance(markup, unicode):
    -            self.originalEncoding = None
    -            self.unicode = unicode(markup)
    -            return
    -
    -        u = None
    -        for proposedEncoding in overrideEncodings:
    -            u = self._convertFrom(proposedEncoding)
    -            if u: break
    -        if not u:
    -            for proposedEncoding in (documentEncoding, sniffedEncoding):
    -                u = self._convertFrom(proposedEncoding)
    -                if u: break
    -
    -        # If no luck and we have auto-detection library, try that:
    -        if not u and chardet and not isinstance(self.markup, unicode):
    -            u = self._convertFrom(chardet.detect(self.markup)['encoding'])
    -
    -        # As a last resort, try utf-8 and windows-1252:
    -        if not u:
    -            for proposed_encoding in ("utf-8", "windows-1252"):
    -                u = self._convertFrom(proposed_encoding)
    -                if u: break
    -
    -        self.unicode = u
    -        if not u: self.originalEncoding = None
    -
    -    def _subMSChar(self, orig):
    -        """Changes a MS smart quote character to an XML or HTML
    -        entity."""
    -        sub = self.MS_CHARS.get(orig)
    -        if isinstance(sub, tuple):
    -            if self.smartQuotesTo == 'xml':
    -                sub = '&#x%s;' % sub[1]
    -            else:
    -                sub = '&%s;' % sub[0]
    -        return sub
    -
    -    def _convertFrom(self, proposed):
    -        proposed = self.find_codec(proposed)
    -        if not proposed or proposed in self.triedEncodings:
    -            return None
    -        self.triedEncodings.append(proposed)
    -        markup = self.markup
    -
    -        # Convert smart quotes to HTML if coming from an encoding
    -        # that might have them.
    -        if self.smartQuotesTo and proposed.lower() in("windows-1252",
    -                                                      "iso-8859-1",
    -                                                      "iso-8859-2"):
    -            markup = re.compile("([\x80-\x9f])").sub \
    -                     (lambda(x): self._subMSChar(x.group(1)),
    -                      markup)
    -
    -        try:
    -            # print "Trying to convert document to %s" % proposed
    -            u = self._toUnicode(markup, proposed)
    -            self.markup = u
    -            self.originalEncoding = proposed
    -        except Exception, e:
    -            # print "That didn't work!"
    -            # print e
    -            return None
    -        #print "Correct encoding: %s" % proposed
    -        return self.markup
    -
    -    def _toUnicode(self, data, encoding):
    -        '''Given a string and its encoding, decodes the string into Unicode.
    -        %encoding is a string recognized by encodings.aliases'''
    -
    -        # strip Byte Order Mark (if present)
    -        if (len(data) >= 4) and (data[:2] == '\xfe\xff') \
    -               and (data[2:4] != '\x00\x00'):
    -            encoding = 'utf-16be'
    -            data = data[2:]
    -        elif (len(data) >= 4) and (data[:2] == '\xff\xfe') \
    -                 and (data[2:4] != '\x00\x00'):
    -            encoding = 'utf-16le'
    -            data = data[2:]
    -        elif data[:3] == '\xef\xbb\xbf':
    -            encoding = 'utf-8'
    -            data = data[3:]
    -        elif data[:4] == '\x00\x00\xfe\xff':
    -            encoding = 'utf-32be'
    -            data = data[4:]
    -        elif data[:4] == '\xff\xfe\x00\x00':
    -            encoding = 'utf-32le'
    -            data = data[4:]
    -        newdata = unicode(data, encoding)
    -        return newdata
    -
    -    def _detectEncoding(self, xml_data, isHTML = False):
    -        """Given a document, tries to detect its XML encoding."""
    -        xml_encoding = sniffed_xml_encoding = None
    -        try:
    -            if xml_data[:4] == '\x4c\x6f\xa7\x94':
    -                # EBCDIC
    -                xml_data = self._ebcdic_to_ascii(xml_data)
    -            elif xml_data[:4] == '\x00\x3c\x00\x3f':
    -                # UTF-16BE
    -                sniffed_xml_encoding = 'utf-16be'
    -                xml_data = unicode(xml_data, 'utf-16be').encode('utf-8')
    -            elif (len(xml_data) >= 4) and (xml_data[:2] == '\xfe\xff') \
    -                     and (xml_data[2:4] != '\x00\x00'):
    -                # UTF-16BE with BOM
    -                sniffed_xml_encoding = 'utf-16be'
    -                xml_data = unicode(xml_data[2:], 'utf-16be').encode('utf-8')
    -            elif xml_data[:4] == '\x3c\x00\x3f\x00':
    -                # UTF-16LE
    -                sniffed_xml_encoding = 'utf-16le'
    -                xml_data = unicode(xml_data, 'utf-16le').encode('utf-8')
    -            elif (len(xml_data) >= 4) and (xml_data[:2] == '\xff\xfe') and \
    -                     (xml_data[2:4] != '\x00\x00'):
    -                # UTF-16LE with BOM
    -                sniffed_xml_encoding = 'utf-16le'
    -                xml_data = unicode(xml_data[2:], 'utf-16le').encode('utf-8')
    -            elif xml_data[:4] == '\x00\x00\x00\x3c':
    -                # UTF-32BE
    -                sniffed_xml_encoding = 'utf-32be'
    -                xml_data = unicode(xml_data, 'utf-32be').encode('utf-8')
    -            elif xml_data[:4] == '\x3c\x00\x00\x00':
    -                # UTF-32LE
    -                sniffed_xml_encoding = 'utf-32le'
    -                xml_data = unicode(xml_data, 'utf-32le').encode('utf-8')
    -            elif xml_data[:4] == '\x00\x00\xfe\xff':
    -                # UTF-32BE with BOM
    -                sniffed_xml_encoding = 'utf-32be'
    -                xml_data = unicode(xml_data[4:], 'utf-32be').encode('utf-8')
    -            elif xml_data[:4] == '\xff\xfe\x00\x00':
    -                # UTF-32LE with BOM
    -                sniffed_xml_encoding = 'utf-32le'
    -                xml_data = unicode(xml_data[4:], 'utf-32le').encode('utf-8')
    -            elif xml_data[:3] == '\xef\xbb\xbf':
    -                # UTF-8 with BOM
    -                sniffed_xml_encoding = 'utf-8'
    -                xml_data = unicode(xml_data[3:], 'utf-8').encode('utf-8')
    -            else:
    -                sniffed_xml_encoding = 'ascii'
    -                pass
    -        except:
    -            xml_encoding_match = None
    -        xml_encoding_match = re.compile(
    -            '^<\?.*encoding=[\'"](.*?)[\'"].*\?>').match(xml_data)
    -        if not xml_encoding_match and isHTML:
    -            regexp = re.compile('<\s*meta[^>]+charset=([^>]*?)[;\'">]', re.I)
    -            xml_encoding_match = regexp.search(xml_data)
    -        if xml_encoding_match is not None:
    -            xml_encoding = xml_encoding_match.groups()[0].lower()
    -            if isHTML:
    -                self.declaredHTMLEncoding = xml_encoding
    -            if sniffed_xml_encoding and \
    -               (xml_encoding in ('iso-10646-ucs-2', 'ucs-2', 'csunicode',
    -                                 'iso-10646-ucs-4', 'ucs-4', 'csucs4',
    -                                 'utf-16', 'utf-32', 'utf_16', 'utf_32',
    -                                 'utf16', 'u16')):
    -                xml_encoding = sniffed_xml_encoding
    -        return xml_data, xml_encoding, sniffed_xml_encoding
    -
    -
    -    def find_codec(self, charset):
    -        return self._codec(self.CHARSET_ALIASES.get(charset, charset)) \
    -               or (charset and self._codec(charset.replace("-", ""))) \
    -               or (charset and self._codec(charset.replace("-", "_"))) \
    -               or charset
    -
    -    def _codec(self, charset):
    -        if not charset: return charset
    -        codec = None
    -        try:
    -            codecs.lookup(charset)
    -            codec = charset
    -        except (LookupError, ValueError):
    -            pass
    -        return codec
    -
    -    EBCDIC_TO_ASCII_MAP = None
    -    def _ebcdic_to_ascii(self, s):
    -        c = self.__class__
    -        if not c.EBCDIC_TO_ASCII_MAP:
    -            emap = (0, 1, 2, 3, 156, 9, 134, 127, 151, 141, 142, 11, 12, 13, 14, 15,
    -                    16, 17, 18, 19, 157, 133, 8, 135, 24, 25, 146, 143, 28, 29, 30, 31,
    -                    128, 129, 130, 131, 132, 10, 23, 27, 136, 137, 138, 139, 140, 5, 6, 7,
    -                    144, 145, 22, 147, 148, 149, 150, 4, 152, 153, 154, 155, 20, 21, 158, 26,
    -                    32, 160, 161, 162, 163, 164, 165, 166, 167, 168, 91, 46, 60, 40, 43, 33,
    -                    38, 169, 170, 171, 172, 173, 174, 175, 176, 177, 93, 36, 42, 41, 59, 94,
    -                    45, 47, 178, 179, 180, 181, 182, 183, 184, 185, 124, 44, 37, 95, 62, 63,
    -                    186, 187, 188, 189, 190, 191, 192, 193, 194, 96, 58, 35, 64, 39, 61, 34,
    -                    195, 97, 98, 99, 100, 101, 102, 103, 104, 105, 196, 197, 198, 199, 200,
    -                    201, 202, 106, 107, 108, 109, 110, 111, 112, 113, 114, 203, 204, 205,
    -                    206, 207, 208, 209, 126, 115, 116, 117, 118, 119, 120, 121, 122, 210,
    -                    211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224,
    -                    225, 226, 227, 228, 229, 230, 231, 123, 65, 66, 67, 68, 69, 70, 71, 72,
    -                    73, 232, 233, 234, 235, 236, 237, 125, 74, 75, 76, 77, 78, 79, 80, 81,
    -                    82, 238, 239, 240, 241, 242, 243, 92, 159, 83, 84, 85, 86, 87, 88, 89,
    -                    90, 244, 245, 246, 247, 248, 249, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57,
    -                    250, 251, 252, 253, 254, 255)
    -            import string
    -            c.EBCDIC_TO_ASCII_MAP = string.maketrans(\
    -            ''.join(map(chr, range(256))), ''.join(map(chr, emap)))
    -        return s.translate(c.EBCDIC_TO_ASCII_MAP)
    -
    -    MS_CHARS = { '\x80' : ('euro', '20AC'),
    -                 '\x81' : ' ',
    -                 '\x82' : ('sbquo', '201A'),
    -                 '\x83' : ('fnof', '192'),
    -                 '\x84' : ('bdquo', '201E'),
    -                 '\x85' : ('hellip', '2026'),
    -                 '\x86' : ('dagger', '2020'),
    -                 '\x87' : ('Dagger', '2021'),
    -                 '\x88' : ('circ', '2C6'),
    -                 '\x89' : ('permil', '2030'),
    -                 '\x8A' : ('Scaron', '160'),
    -                 '\x8B' : ('lsaquo', '2039'),
    -                 '\x8C' : ('OElig', '152'),
    -                 '\x8D' : '?',
    -                 '\x8E' : ('#x17D', '17D'),
    -                 '\x8F' : '?',
    -                 '\x90' : '?',
    -                 '\x91' : ('lsquo', '2018'),
    -                 '\x92' : ('rsquo', '2019'),
    -                 '\x93' : ('ldquo', '201C'),
    -                 '\x94' : ('rdquo', '201D'),
    -                 '\x95' : ('bull', '2022'),
    -                 '\x96' : ('ndash', '2013'),
    -                 '\x97' : ('mdash', '2014'),
    -                 '\x98' : ('tilde', '2DC'),
    -                 '\x99' : ('trade', '2122'),
    -                 '\x9a' : ('scaron', '161'),
    -                 '\x9b' : ('rsaquo', '203A'),
    -                 '\x9c' : ('oelig', '153'),
    -                 '\x9d' : '?',
    -                 '\x9e' : ('#x17E', '17E'),
    -                 '\x9f' : ('Yuml', ''), }
    -
    -#######################################################################
    -
    -
    -#By default, act as an HTML pretty-printer.
    -if __name__ == '__main__':
    -    import sys
    -    soup = BeautifulSoup(sys.stdin)
    -    print soup.prettify()
    -
    diff --git a/libs/bs4/__init__.py b/libs/bs4/__init__.py
    new file mode 100644
    index 00000000..af8c718d
    --- /dev/null
    +++ b/libs/bs4/__init__.py
    @@ -0,0 +1,355 @@
    +"""Beautiful Soup
    +Elixir and Tonic
    +"The Screen-Scraper's Friend"
    +http://www.crummy.com/software/BeautifulSoup/
    +
    +Beautiful Soup uses a pluggable XML or HTML parser to parse a
    +(possibly invalid) document into a tree representation. Beautiful Soup
    +provides provides methods and Pythonic idioms that make it easy to
    +navigate, search, and modify the parse tree.
    +
    +Beautiful Soup works with Python 2.6 and up. It works better if lxml
    +and/or html5lib is installed.
    +
    +For more than you ever wanted to know about Beautiful Soup, see the
    +documentation:
    +http://www.crummy.com/software/BeautifulSoup/bs4/doc/
    +"""
    +
    +__author__ = "Leonard Richardson (leonardr@segfault.org)"
    +__version__ = "4.1.0"
    +__copyright__ = "Copyright (c) 2004-2012 Leonard Richardson"
    +__license__ = "MIT"
    +
    +__all__ = ['BeautifulSoup']
    +
    +import re
    +import warnings
    +
    +from .builder import builder_registry
    +from .dammit import UnicodeDammit
    +from .element import (
    +    CData,
    +    Comment,
    +    DEFAULT_OUTPUT_ENCODING,
    +    Declaration,
    +    Doctype,
    +    NavigableString,
    +    PageElement,
    +    ProcessingInstruction,
    +    ResultSet,
    +    SoupStrainer,
    +    Tag,
    +    )
    +
    +# The very first thing we do is give a useful error if someone is
    +# running this code under Python 3 without converting it.
    +syntax_error = u'You are trying to run the Python 2 version of Beautiful Soup under Python 3. This will not work. You need to convert the code, either by installing it (`python setup.py install`) or by running 2to3 (`2to3 -w bs4`).'
    +
    +class BeautifulSoup(Tag):
    +    """
    +    This class defines the basic interface called by the tree builders.
    +
    +    These methods will be called by the parser:
    +      reset()
    +      feed(markup)
    +
    +    The tree builder may call these methods from its feed() implementation:
    +      handle_starttag(name, attrs) # See note about return value
    +      handle_endtag(name)
    +      handle_data(data) # Appends to the current data node
    +      endData(containerClass=NavigableString) # Ends the current data node
    +
    +    No matter how complicated the underlying parser is, you should be
    +    able to build a tree using 'start tag' events, 'end tag' events,
    +    'data' events, and "done with data" events.
    +
    +    If you encounter an empty-element tag (aka a self-closing tag,
    +    like HTML's <br> tag), call handle_starttag and then
    +    handle_endtag.
    +    """
    +    ROOT_TAG_NAME = u'[document]'
    +
    +    # If the end-user gives no indication which tree builder they
    +    # want, look for one with these features.
    +    DEFAULT_BUILDER_FEATURES = ['html', 'fast']
    +
    +    # Used when determining whether a text node is all whitespace and
    +    # can be replaced with a single space. A text node that contains
    +    # fancy Unicode spaces (usually non-breaking) should be left
    +    # alone.
    +    STRIP_ASCII_SPACES = {9: None, 10: None, 12: None, 13: None, 32: None, }
    +
    +    def __init__(self, markup="", features=None, builder=None,
    +                 parse_only=None, from_encoding=None, **kwargs):
    +        """The Soup object is initialized as the 'root tag', and the
    +        provided markup (which can be a string or a file-like object)
    +        is fed into the underlying parser."""
    +
    +        if 'convertEntities' in kwargs:
    +            warnings.warn(
    +                "BS4 does not respect the convertEntities argument to the "
    +                "BeautifulSoup constructor. Entities are always converted "
    +                "to Unicode characters.")
    +
    +        if 'markupMassage' in kwargs:
    +            del kwargs['markupMassage']
    +            warnings.warn(
    +                "BS4 does not respect the markupMassage argument to the "
    +                "BeautifulSoup constructor. The tree builder is responsible "
    +                "for any necessary markup massage.")
    +
    +        if 'smartQuotesTo' in kwargs:
    +            del kwargs['smartQuotesTo']
    +            warnings.warn(
    +                "BS4 does not respect the smartQuotesTo argument to the "
    +                "BeautifulSoup constructor. Smart quotes are always converted "
    +                "to Unicode characters.")
    +
    +        if 'selfClosingTags' in kwargs:
    +            del kwargs['selfClosingTags']
    +            warnings.warn(
    +                "BS4 does not respect the selfClosingTags argument to the "
    +                "BeautifulSoup constructor. The tree builder is responsible "
    +                "for understanding self-closing tags.")
    +
    +        if 'isHTML' in kwargs:
    +            del kwargs['isHTML']
    +            warnings.warn(
    +                "BS4 does not respect the isHTML argument to the "
    +                "BeautifulSoup constructor. You can pass in features='html' "
    +                "or features='xml' to get a builder capable of handling "
    +                "one or the other.")
    +
    +        def deprecated_argument(old_name, new_name):
    +            if old_name in kwargs:
    +                warnings.warn(
    +                    'The "%s" argument to the BeautifulSoup constructor '
    +                    'has been renamed to "%s."' % (old_name, new_name))
    +                value = kwargs[old_name]
    +                del kwargs[old_name]
    +                return value
    +            return None
    +
    +        parse_only = parse_only or deprecated_argument(
    +            "parseOnlyThese", "parse_only")
    +
    +        from_encoding = from_encoding or deprecated_argument(
    +            "fromEncoding", "from_encoding")
    +
    +        if len(kwargs) > 0:
    +            arg = kwargs.keys().pop()
    +            raise TypeError(
    +                "__init__() got an unexpected keyword argument '%s'" % arg)
    +
    +        if builder is None:
    +            if isinstance(features, basestring):
    +                features = [features]
    +            if features is None or len(features) == 0:
    +                features = self.DEFAULT_BUILDER_FEATURES
    +            builder_class = builder_registry.lookup(*features)
    +            if builder_class is None:
    +                raise ValueError(
    +                    "Couldn't find a tree builder with the features you "
    +                    "requested: %s. Do you need to install a parser library?"
    +                    % ",".join(features))
    +            builder = builder_class()
    +        self.builder = builder
    +        self.is_xml = builder.is_xml
    +        self.builder.soup = self
    +
    +        self.parse_only = parse_only
    +
    +        self.reset()
    +
    +        if hasattr(markup, 'read'):        # It's a file-type object.
    +            markup = markup.read()
    +        (self.markup, self.original_encoding, self.declared_html_encoding,
    +         self.contains_replacement_characters) = (
    +            self.builder.prepare_markup(markup, from_encoding))
    +
    +        try:
    +            self._feed()
    +        except StopParsing:
    +            pass
    +
    +        # Clear out the markup and remove the builder's circular
    +        # reference to this object.
    +        self.markup = None
    +        self.builder.soup = None
    +
    +    def _feed(self):
    +        # Convert the document to Unicode.
    +        self.builder.reset()
    +
    +        self.builder.feed(self.markup)
    +        # Close out any unfinished strings and close all the open tags.
    +        self.endData()
    +        while self.currentTag.name != self.ROOT_TAG_NAME:
    +            self.popTag()
    +
    +    def reset(self):
    +        Tag.__init__(self, self, self.builder, self.ROOT_TAG_NAME)
    +        self.hidden = 1
    +        self.builder.reset()
    +        self.currentData = []
    +        self.currentTag = None
    +        self.tagStack = []
    +        self.pushTag(self)
    +
    +    def new_tag(self, name, namespace=None, nsprefix=None, **attrs):
    +        """Create a new tag associated with this soup."""
    +        return Tag(None, self.builder, name, namespace, nsprefix, attrs)
    +
    +    def new_string(self, s):
    +        """Create a new NavigableString associated with this soup."""
    +        navigable = NavigableString(s)
    +        navigable.setup()
    +        return navigable
    +
    +    def insert_before(self, successor):
    +        raise ValueError("BeautifulSoup objects don't support insert_before().")
    +
    +    def insert_after(self, successor):
    +        raise ValueError("BeautifulSoup objects don't support insert_after().")
    +
    +    def popTag(self):
    +        tag = self.tagStack.pop()
    +        #print "Pop", tag.name
    +        if self.tagStack:
    +            self.currentTag = self.tagStack[-1]
    +        return self.currentTag
    +
    +    def pushTag(self, tag):
    +        #print "Push", tag.name
    +        if self.currentTag:
    +            self.currentTag.contents.append(tag)
    +        self.tagStack.append(tag)
    +        self.currentTag = self.tagStack[-1]
    +
    +    def endData(self, containerClass=NavigableString):
    +        if self.currentData:
    +            currentData = u''.join(self.currentData)
    +            if (currentData.translate(self.STRIP_ASCII_SPACES) == '' and
    +                not set([tag.name for tag in self.tagStack]).intersection(
    +                    self.builder.preserve_whitespace_tags)):
    +                if '\n' in currentData:
    +                    currentData = '\n'
    +                else:
    +                    currentData = ' '
    +            self.currentData = []
    +            if self.parse_only and len(self.tagStack) <= 1 and \
    +                   (not self.parse_only.text or \
    +                    not self.parse_only.search(currentData)):
    +                return
    +            o = containerClass(currentData)
    +            self.object_was_parsed(o)
    +
    +    def object_was_parsed(self, o):
    +        """Add an object to the parse tree."""
    +        o.setup(self.currentTag, self.previous_element)
    +        if self.previous_element:
    +            self.previous_element.next_element = o
    +        self.previous_element = o
    +        self.currentTag.contents.append(o)
    +
    +    def _popToTag(self, name, nsprefix=None, inclusivePop=True):
    +        """Pops the tag stack up to and including the most recent
    +        instance of the given tag. If inclusivePop is false, pops the tag
    +        stack up to but *not* including the most recent instqance of
    +        the given tag."""
    +        #print "Popping to %s" % name
    +        if name == self.ROOT_TAG_NAME:
    +            return
    +
    +        numPops = 0
    +        mostRecentTag = None
    +
    +        for i in range(len(self.tagStack) - 1, 0, -1):
    +            if (name == self.tagStack[i].name
    +                and nsprefix == self.tagStack[i].nsprefix == nsprefix):
    +                numPops = len(self.tagStack) - i
    +                break
    +        if not inclusivePop:
    +            numPops = numPops - 1
    +
    +        for i in range(0, numPops):
    +            mostRecentTag = self.popTag()
    +        return mostRecentTag
    +
    +    def handle_starttag(self, name, namespace, nsprefix, attrs):
    +        """Push a start tag on to the stack.
    +
    +        If this method returns None, the tag was rejected by the
    +        SoupStrainer. You should proceed as if the tag had not occured
    +        in the document. For instance, if this was a self-closing tag,
    +        don't call handle_endtag.
    +        """
    +
    +        # print "Start tag %s: %s" % (name, attrs)
    +        self.endData()
    +
    +        if (self.parse_only and len(self.tagStack) <= 1
    +            and (self.parse_only.text
    +                 or not self.parse_only.search_tag(name, attrs))):
    +            return None
    +
    +        tag = Tag(self, self.builder, name, namespace, nsprefix, attrs,
    +                  self.currentTag, self.previous_element)
    +        if tag is None:
    +            return tag
    +        if self.previous_element:
    +            self.previous_element.next_element = tag
    +        self.previous_element = tag
    +        self.pushTag(tag)
    +        return tag
    +
    +    def handle_endtag(self, name, nsprefix=None):
    +        #print "End tag: " + name
    +        self.endData()
    +        self._popToTag(name, nsprefix)
    +
    +    def handle_data(self, data):
    +        self.currentData.append(data)
    +
    +    def decode(self, pretty_print=False,
    +               eventual_encoding=DEFAULT_OUTPUT_ENCODING,
    +               formatter="minimal"):
    +        """Returns a string or Unicode representation of this document.
    +        To get Unicode, pass None for encoding."""
    +
    +        if self.is_xml:
    +            # Print the XML declaration
    +            encoding_part = ''
    +            if eventual_encoding != None:
    +                encoding_part = ' encoding="%s"' % eventual_encoding
    +            prefix = u'<?xml version="1.0"%s?>\n' % encoding_part
    +        else:
    +            prefix = u''
    +        if not pretty_print:
    +            indent_level = None
    +        else:
    +            indent_level = 0
    +        return prefix + super(BeautifulSoup, self).decode(
    +            indent_level, eventual_encoding, formatter)
    +
    +class BeautifulStoneSoup(BeautifulSoup):
    +    """Deprecated interface to an XML parser."""
    +
    +    def __init__(self, *args, **kwargs):
    +        kwargs['features'] = 'xml'
    +        warnings.warn(
    +            'The BeautifulStoneSoup class is deprecated. Instead of using '
    +            'it, pass features="xml" into the BeautifulSoup constructor.')
    +        super(BeautifulStoneSoup, self).__init__(*args, **kwargs)
    +
    +
    +class StopParsing(Exception):
    +    pass
    +
    +
    +#By default, act as an HTML pretty-printer.
    +if __name__ == '__main__':
    +    import sys
    +    soup = BeautifulSoup(sys.stdin)
    +    print soup.prettify()
    diff --git a/libs/bs4/builder/__init__.py b/libs/bs4/builder/__init__.py
    new file mode 100644
    index 00000000..4c22b864
    --- /dev/null
    +++ b/libs/bs4/builder/__init__.py
    @@ -0,0 +1,307 @@
    +from collections import defaultdict
    +import itertools
    +import sys
    +from bs4.element import (
    +    CharsetMetaAttributeValue,
    +    ContentMetaAttributeValue,
    +    whitespace_re
    +    )
    +
    +__all__ = [
    +    'HTMLTreeBuilder',
    +    'SAXTreeBuilder',
    +    'TreeBuilder',
    +    'TreeBuilderRegistry',
    +    ]
    +
    +# Some useful features for a TreeBuilder to have.
    +FAST = 'fast'
    +PERMISSIVE = 'permissive'
    +STRICT = 'strict'
    +XML = 'xml'
    +HTML = 'html'
    +HTML_5 = 'html5'
    +
    +
    +class TreeBuilderRegistry(object):
    +
    +    def __init__(self):
    +        self.builders_for_feature = defaultdict(list)
    +        self.builders = []
    +
    +    def register(self, treebuilder_class):
    +        """Register a treebuilder based on its advertised features."""
    +        for feature in treebuilder_class.features:
    +            self.builders_for_feature[feature].insert(0, treebuilder_class)
    +        self.builders.insert(0, treebuilder_class)
    +
    +    def lookup(self, *features):
    +        if len(self.builders) == 0:
    +            # There are no builders at all.
    +            return None
    +
    +        if len(features) == 0:
    +            # They didn't ask for any features. Give them the most
    +            # recently registered builder.
    +            return self.builders[0]
    +
    +        # Go down the list of features in order, and eliminate any builders
    +        # that don't match every feature.
    +        features = list(features)
    +        features.reverse()
    +        candidates = None
    +        candidate_set = None
    +        while len(features) > 0:
    +            feature = features.pop()
    +            we_have_the_feature = self.builders_for_feature.get(feature, [])
    +            if len(we_have_the_feature) > 0:
    +                if candidates is None:
    +                    candidates = we_have_the_feature
    +                    candidate_set = set(candidates)
    +                else:
    +                    # Eliminate any candidates that don't have this feature.
    +                    candidate_set = candidate_set.intersection(
    +                        set(we_have_the_feature))
    +
    +        # The only valid candidates are the ones in candidate_set.
    +        # Go through the original list of candidates and pick the first one
    +        # that's in candidate_set.
    +        if candidate_set is None:
    +            return None
    +        for candidate in candidates:
    +            if candidate in candidate_set:
    +                return candidate
    +        return None
    +
    +# The BeautifulSoup class will take feature lists from developers and use them
    +# to look up builders in this registry.
    +builder_registry = TreeBuilderRegistry()
    +
    +class TreeBuilder(object):
    +    """Turn a document into a Beautiful Soup object tree."""
    +
    +    features = []
    +
    +    is_xml = False
    +    preserve_whitespace_tags = set()
    +    empty_element_tags = None # A tag will be considered an empty-element
    +                              # tag when and only when it has no contents.
    +
    +    # A value for these tag/attribute combinations is a space- or
    +    # comma-separated list of CDATA, rather than a single CDATA.
    +    cdata_list_attributes = {}
    +
    +
    +    def __init__(self):
    +        self.soup = None
    +
    +    def reset(self):
    +        pass
    +
    +    def can_be_empty_element(self, tag_name):
    +        """Might a tag with this name be an empty-element tag?
    +
    +        The final markup may or may not actually present this tag as
    +        self-closing.
    +
    +        For instance: an HTMLBuilder does not consider a <p> tag to be
    +        an empty-element tag (it's not in
    +        HTMLBuilder.empty_element_tags). This means an empty <p> tag
    +        will be presented as "<p></p>", not "<p />".
    +
    +        The default implementation has no opinion about which tags are
    +        empty-element tags, so a tag will be presented as an
    +        empty-element tag if and only if it has no contents.
    +        "<foo></foo>" will become "<foo />", and "<foo>bar</foo>" will
    +        be left alone.
    +        """
    +        if self.empty_element_tags is None:
    +            return True
    +        return tag_name in self.empty_element_tags
    +
    +    def feed(self, markup):
    +        raise NotImplementedError()
    +
    +    def prepare_markup(self, markup, user_specified_encoding=None,
    +                       document_declared_encoding=None):
    +        return markup, None, None, False
    +
    +    def test_fragment_to_document(self, fragment):
    +        """Wrap an HTML fragment to make it look like a document.
    +
    +        Different parsers do this differently. For instance, lxml
    +        introduces an empty <head> tag, and html5lib
    +        doesn't. Abstracting this away lets us write simple tests
    +        which run HTML fragments through the parser and compare the
    +        results against other HTML fragments.
    +
    +        This method should not be used outside of tests.
    +        """
    +        return fragment
    +
    +    def set_up_substitutions(self, tag):
    +        return False
    +
    +    def _replace_cdata_list_attribute_values(self, tag_name, attrs):
    +        """Replaces class="foo bar" with class=["foo", "bar"]
    +
    +        Modifies its input in place.
    +        """
    +        if self.cdata_list_attributes:
    +            universal = self.cdata_list_attributes.get('*', [])
    +            tag_specific = self.cdata_list_attributes.get(
    +                tag_name.lower(), [])
    +            for cdata_list_attr in itertools.chain(universal, tag_specific):
    +                if cdata_list_attr in dict(attrs):
    +                    # Basically, we have a "class" attribute whose
    +                    # value is a whitespace-separated list of CSS
    +                    # classes. Split it into a list.
    +                    value = attrs[cdata_list_attr]
    +                    values = whitespace_re.split(value)
    +                    attrs[cdata_list_attr] = values
    +        return attrs
    +
    +class SAXTreeBuilder(TreeBuilder):
    +    """A Beautiful Soup treebuilder that listens for SAX events."""
    +
    +    def feed(self, markup):
    +        raise NotImplementedError()
    +
    +    def close(self):
    +        pass
    +
    +    def startElement(self, name, attrs):
    +        attrs = dict((key[1], value) for key, value in list(attrs.items()))
    +        #print "Start %s, %r" % (name, attrs)
    +        self.soup.handle_starttag(name, attrs)
    +
    +    def endElement(self, name):
    +        #print "End %s" % name
    +        self.soup.handle_endtag(name)
    +
    +    def startElementNS(self, nsTuple, nodeName, attrs):
    +        # Throw away (ns, nodeName) for now.
    +        self.startElement(nodeName, attrs)
    +
    +    def endElementNS(self, nsTuple, nodeName):
    +        # Throw away (ns, nodeName) for now.
    +        self.endElement(nodeName)
    +        #handler.endElementNS((ns, node.nodeName), node.nodeName)
    +
    +    def startPrefixMapping(self, prefix, nodeValue):
    +        # Ignore the prefix for now.
    +        pass
    +
    +    def endPrefixMapping(self, prefix):
    +        # Ignore the prefix for now.
    +        # handler.endPrefixMapping(prefix)
    +        pass
    +
    +    def characters(self, content):
    +        self.soup.handle_data(content)
    +
    +    def startDocument(self):
    +        pass
    +
    +    def endDocument(self):
    +        pass
    +
    +
    +class HTMLTreeBuilder(TreeBuilder):
    +    """This TreeBuilder knows facts about HTML.
    +
    +    Such as which tags are empty-element tags.
    +    """
    +
    +    preserve_whitespace_tags = set(['pre', 'textarea'])
    +    empty_element_tags = set(['br' , 'hr', 'input', 'img', 'meta',
    +                              'spacer', 'link', 'frame', 'base'])
    +
    +    # The HTML standard defines these attributes as containing a
    +    # space-separated list of values, not a single value. That is,
    +    # class="foo bar" means that the 'class' attribute has two values,
    +    # 'foo' and 'bar', not the single value 'foo bar'.  When we
    +    # encounter one of these attributes, we will parse its value into
    +    # a list of values if possible. Upon output, the list will be
    +    # converted back into a string.
    +    cdata_list_attributes = {
    +        "*" : ['class', 'accesskey', 'dropzone'],
    +        "a" : ['rel', 'rev'],
    +        "link" :  ['rel', 'rev'],
    +        "td" : ["headers"],
    +        "th" : ["headers"],
    +        "td" : ["headers"],
    +        "form" : ["accept-charset"],
    +        "object" : ["archive"],
    +
    +        # These are HTML5 specific, as are *.accesskey and *.dropzone above.
    +        "area" : ["rel"],
    +        "icon" : ["sizes"],
    +        "iframe" : ["sandbox"],
    +        "output" : ["for"],
    +        }
    +
    +    def set_up_substitutions(self, tag):
    +        # We are only interested in <meta> tags
    +        if tag.name != 'meta':
    +            return False
    +
    +        http_equiv = tag.get('http-equiv')
    +        content = tag.get('content')
    +        charset = tag.get('charset')
    +
    +        # We are interested in <meta> tags that say what encoding the
    +        # document was originally in. This means HTML 5-style <meta>
    +        # tags that provide the "charset" attribute. It also means
    +        # HTML 4-style <meta> tags that provide the "content"
    +        # attribute and have "http-equiv" set to "content-type".
    +        #
    +        # In both cases we will replace the value of the appropriate
    +        # attribute with a standin object that can take on any
    +        # encoding.
    +        meta_encoding = None
    +        if charset is not None:
    +            # HTML 5 style:
    +            # <meta charset="utf8">
    +            meta_encoding = charset
    +            tag['charset'] = CharsetMetaAttributeValue(charset)
    +
    +        elif (content is not None and http_equiv is not None
    +              and http_equiv.lower() == 'content-type'):
    +            # HTML 4 style:
    +            # <meta http-equiv="content-type" content="text/html; charset=utf8">
    +            tag['content'] = ContentMetaAttributeValue(content)
    +
    +        return (meta_encoding is not None)
    +
    +def register_treebuilders_from(module):
    +    """Copy TreeBuilders from the given module into this module."""
    +    # I'm fairly sure this is not the best way to do this.
    +    this_module = sys.modules['bs4.builder']
    +    for name in module.__all__:
    +        obj = getattr(module, name)
    +
    +        if issubclass(obj, TreeBuilder):
    +            setattr(this_module, name, obj)
    +            this_module.__all__.append(name)
    +            # Register the builder while we're at it.
    +            this_module.builder_registry.register(obj)
    +
    +# Builders are registered in reverse order of priority, so that custom
    +# builder registrations will take precedence. In general, we want lxml
    +# to take precedence over html5lib, because it's faster. And we only
    +# want to use HTMLParser as a last result.
    +from . import _htmlparser
    +register_treebuilders_from(_htmlparser)
    +try:
    +    from . import _html5lib
    +    register_treebuilders_from(_html5lib)
    +except ImportError:
    +    # They don't have html5lib installed.
    +    pass
    +try:
    +    from . import _lxml
    +    register_treebuilders_from(_lxml)
    +except ImportError:
    +    # They don't have lxml installed.
    +    pass
    diff --git a/libs/bs4/builder/_html5lib.py b/libs/bs4/builder/_html5lib.py
    new file mode 100644
    index 00000000..6001e386
    --- /dev/null
    +++ b/libs/bs4/builder/_html5lib.py
    @@ -0,0 +1,222 @@
    +__all__ = [
    +    'HTML5TreeBuilder',
    +    ]
    +
    +import warnings
    +from bs4.builder import (
    +    PERMISSIVE,
    +    HTML,
    +    HTML_5,
    +    HTMLTreeBuilder,
    +    )
    +from bs4.element import NamespacedAttribute
    +import html5lib
    +from html5lib.constants import namespaces
    +from bs4.element import (
    +    Comment,
    +    Doctype,
    +    NavigableString,
    +    Tag,
    +    )
    +
    +class HTML5TreeBuilder(HTMLTreeBuilder):
    +    """Use html5lib to build a tree."""
    +
    +    features = ['html5lib', PERMISSIVE, HTML_5, HTML]
    +
    +    def prepare_markup(self, markup, user_specified_encoding):
    +        # Store the user-specified encoding for use later on.
    +        self.user_specified_encoding = user_specified_encoding
    +        return markup, None, None, False
    +
    +    # These methods are defined by Beautiful Soup.
    +    def feed(self, markup):
    +        if self.soup.parse_only is not None:
    +            warnings.warn("You provided a value for parse_only, but the html5lib tree builder doesn't support parse_only. The entire document will be parsed.")
    +        parser = html5lib.HTMLParser(tree=self.create_treebuilder)
    +        doc = parser.parse(markup, encoding=self.user_specified_encoding)
    +
    +        # Set the character encoding detected by the tokenizer.
    +        if isinstance(markup, unicode):
    +            # We need to special-case this because html5lib sets
    +            # charEncoding to UTF-8 if it gets Unicode input.
    +            doc.original_encoding = None
    +        else:
    +            doc.original_encoding = parser.tokenizer.stream.charEncoding[0]
    +
    +    def create_treebuilder(self, namespaceHTMLElements):
    +        self.underlying_builder = TreeBuilderForHtml5lib(
    +            self.soup, namespaceHTMLElements)
    +        return self.underlying_builder
    +
    +    def test_fragment_to_document(self, fragment):
    +        """See `TreeBuilder`."""
    +        return u'<html><head></head><body>%s</body></html>' % fragment
    +
    +
    +class TreeBuilderForHtml5lib(html5lib.treebuilders._base.TreeBuilder):
    +
    +    def __init__(self, soup, namespaceHTMLElements):
    +        self.soup = soup
    +        super(TreeBuilderForHtml5lib, self).__init__(namespaceHTMLElements)
    +
    +    def documentClass(self):
    +        self.soup.reset()
    +        return Element(self.soup, self.soup, None)
    +
    +    def insertDoctype(self, token):
    +        name = token["name"]
    +        publicId = token["publicId"]
    +        systemId = token["systemId"]
    +
    +        doctype = Doctype.for_name_and_ids(name, publicId, systemId)
    +        self.soup.object_was_parsed(doctype)
    +
    +    def elementClass(self, name, namespace):
    +        tag = self.soup.new_tag(name, namespace)
    +        return Element(tag, self.soup, namespace)
    +
    +    def commentClass(self, data):
    +        return TextNode(Comment(data), self.soup)
    +
    +    def fragmentClass(self):
    +        self.soup = BeautifulSoup("")
    +        self.soup.name = "[document_fragment]"
    +        return Element(self.soup, self.soup, None)
    +
    +    def appendChild(self, node):
    +        # XXX This code is not covered by the BS4 tests.
    +        self.soup.append(node.element)
    +
    +    def getDocument(self):
    +        return self.soup
    +
    +    def getFragment(self):
    +        return html5lib.treebuilders._base.TreeBuilder.getFragment(self).element
    +
    +class AttrList(object):
    +    def __init__(self, element):
    +        self.element = element
    +        self.attrs = dict(self.element.attrs)
    +    def __iter__(self):
    +        return list(self.attrs.items()).__iter__()
    +    def __setitem__(self, name, value):
    +        "set attr", name, value
    +        self.element[name] = value
    +    def items(self):
    +        return list(self.attrs.items())
    +    def keys(self):
    +        return list(self.attrs.keys())
    +    def __len__(self):
    +        return len(self.attrs)
    +    def __getitem__(self, name):
    +        return self.attrs[name]
    +    def __contains__(self, name):
    +        return name in list(self.attrs.keys())
    +
    +
    +class Element(html5lib.treebuilders._base.Node):
    +    def __init__(self, element, soup, namespace):
    +        html5lib.treebuilders._base.Node.__init__(self, element.name)
    +        self.element = element
    +        self.soup = soup
    +        self.namespace = namespace
    +
    +    def appendChild(self, node):
    +        if (node.element.__class__ == NavigableString and self.element.contents
    +            and self.element.contents[-1].__class__ == NavigableString):
    +            # Concatenate new text onto old text node
    +            # XXX This has O(n^2) performance, for input like
    +            # "a</a>a</a>a</a>..."
    +            old_element = self.element.contents[-1]
    +            new_element = self.soup.new_string(old_element + node.element)
    +            old_element.replace_with(new_element)
    +        else:
    +            self.element.append(node.element)
    +            node.parent = self
    +
    +    def getAttributes(self):
    +        return AttrList(self.element)
    +
    +    def setAttributes(self, attributes):
    +        if attributes is not None and len(attributes) > 0:
    +
    +            converted_attributes = []
    +            for name, value in list(attributes.items()):
    +                if isinstance(name, tuple):
    +                    new_name = NamespacedAttribute(*name)
    +                    del attributes[name]
    +                    attributes[new_name] = value
    +
    +            self.soup.builder._replace_cdata_list_attribute_values(
    +                self.name, attributes)
    +            for name, value in attributes.items():
    +                self.element[name] = value
    +
    +            # The attributes may contain variables that need substitution.
    +            # Call set_up_substitutions manually.
    +            #
    +            # The Tag constructor called this method when the Tag was created,
    +            # but we just set/changed the attributes, so call it again.
    +            self.soup.builder.set_up_substitutions(self.element)
    +    attributes = property(getAttributes, setAttributes)
    +
    +    def insertText(self, data, insertBefore=None):
    +        text = TextNode(self.soup.new_string(data), self.soup)
    +        if insertBefore:
    +            self.insertBefore(text, insertBefore)
    +        else:
    +            self.appendChild(text)
    +
    +    def insertBefore(self, node, refNode):
    +        index = self.element.index(refNode.element)
    +        if (node.element.__class__ == NavigableString and self.element.contents
    +            and self.element.contents[index-1].__class__ == NavigableString):
    +            # (See comments in appendChild)
    +            old_node = self.element.contents[index-1]
    +            new_str = self.soup.new_string(old_node + node.element)
    +            old_node.replace_with(new_str)
    +        else:
    +            self.element.insert(index, node.element)
    +            node.parent = self
    +
    +    def removeChild(self, node):
    +        node.element.extract()
    +
    +    def reparentChildren(self, newParent):
    +        while self.element.contents:
    +            child = self.element.contents[0]
    +            child.extract()
    +            if isinstance(child, Tag):
    +                newParent.appendChild(
    +                    Element(child, self.soup, namespaces["html"]))
    +            else:
    +                newParent.appendChild(
    +                    TextNode(child, self.soup))
    +
    +    def cloneNode(self):
    +        tag = self.soup.new_tag(self.element.name, self.namespace)
    +        node = Element(tag, self.soup, self.namespace)
    +        for key,value in self.attributes:
    +            node.attributes[key] = value
    +        return node
    +
    +    def hasContent(self):
    +        return self.element.contents
    +
    +    def getNameTuple(self):
    +        if self.namespace == None:
    +            return namespaces["html"], self.name
    +        else:
    +            return self.namespace, self.name
    +
    +    nameTuple = property(getNameTuple)
    +
    +class TextNode(Element):
    +    def __init__(self, element, soup):
    +        html5lib.treebuilders._base.Node.__init__(self, None)
    +        self.element = element
    +        self.soup = soup
    +
    +    def cloneNode(self):
    +        raise NotImplementedError
    diff --git a/libs/bs4/builder/_htmlparser.py b/libs/bs4/builder/_htmlparser.py
    new file mode 100644
    index 00000000..ede5cecb
    --- /dev/null
    +++ b/libs/bs4/builder/_htmlparser.py
    @@ -0,0 +1,244 @@
    +"""Use the HTMLParser library to parse HTML files that aren't too bad."""
    +
    +__all__ = [
    +    'HTMLParserTreeBuilder',
    +    ]
    +
    +from HTMLParser import (
    +    HTMLParser,
    +    HTMLParseError,
    +    )
    +import sys
    +import warnings
    +
    +# Starting in Python 3.2, the HTMLParser constructor takes a 'strict'
    +# argument, which we'd like to set to False. Unfortunately,
    +# http://bugs.python.org/issue13273 makes strict=True a better bet
    +# before Python 3.2.3.
    +#
    +# At the end of this file, we monkeypatch HTMLParser so that
    +# strict=True works well on Python 3.2.2.
    +major, minor, release = sys.version_info[:3]
    +CONSTRUCTOR_TAKES_STRICT = (
    +    major > 3
    +    or (major == 3 and minor > 2)
    +    or (major == 3 and minor == 2 and release >= 3))
    +
    +from bs4.element import (
    +    CData,
    +    Comment,
    +    Declaration,
    +    Doctype,
    +    ProcessingInstruction,
    +    )
    +from bs4.dammit import EntitySubstitution, UnicodeDammit
    +
    +from bs4.builder import (
    +    HTML,
    +    HTMLTreeBuilder,
    +    STRICT,
    +    )
    +
    +
    +HTMLPARSER = 'html.parser'
    +
    +class BeautifulSoupHTMLParser(HTMLParser):
    +    def handle_starttag(self, name, attrs):
    +        # XXX namespace
    +        self.soup.handle_starttag(name, None, None, dict(attrs))
    +
    +    def handle_endtag(self, name):
    +        self.soup.handle_endtag(name)
    +
    +    def handle_data(self, data):
    +        self.soup.handle_data(data)
    +
    +    def handle_charref(self, name):
    +        # XXX workaround for a bug in HTMLParser. Remove this once
    +        # it's fixed.
    +        if name.startswith('x'):
    +            real_name = int(name.lstrip('x'), 16)
    +        else:
    +            real_name = int(name)
    +
    +        try:
    +            data = unichr(real_name)
    +        except (ValueError, OverflowError), e:
    +            data = u"\N{REPLACEMENT CHARACTER}"
    +
    +        self.handle_data(data)
    +
    +    def handle_entityref(self, name):
    +        character = EntitySubstitution.HTML_ENTITY_TO_CHARACTER.get(name)
    +        if character is not None:
    +            data = character
    +        else:
    +            data = "&%s;" % name
    +        self.handle_data(data)
    +
    +    def handle_comment(self, data):
    +        self.soup.endData()
    +        self.soup.handle_data(data)
    +        self.soup.endData(Comment)
    +
    +    def handle_decl(self, data):
    +        self.soup.endData()
    +        if data.startswith("DOCTYPE "):
    +            data = data[len("DOCTYPE "):]
    +        self.soup.handle_data(data)
    +        self.soup.endData(Doctype)
    +
    +    def unknown_decl(self, data):
    +        if data.upper().startswith('CDATA['):
    +            cls = CData
    +            data = data[len('CDATA['):]
    +        else:
    +            cls = Declaration
    +        self.soup.endData()
    +        self.soup.handle_data(data)
    +        self.soup.endData(cls)
    +
    +    def handle_pi(self, data):
    +        self.soup.endData()
    +        if data.endswith("?") and data.lower().startswith("xml"):
    +            # "An XHTML processing instruction using the trailing '?'
    +            # will cause the '?' to be included in data." - HTMLParser
    +            # docs.
    +            #
    +            # Strip the question mark so we don't end up with two
    +            # question marks.
    +            data = data[:-1]
    +        self.soup.handle_data(data)
    +        self.soup.endData(ProcessingInstruction)
    +
    +
    +class HTMLParserTreeBuilder(HTMLTreeBuilder):
    +
    +    is_xml = False
    +    features = [HTML, STRICT, HTMLPARSER]
    +
    +    def __init__(self, *args, **kwargs):
    +        if CONSTRUCTOR_TAKES_STRICT:
    +            kwargs['strict'] = False
    +        self.parser_args = (args, kwargs)
    +
    +    def prepare_markup(self, markup, user_specified_encoding=None,
    +                       document_declared_encoding=None):
    +        """
    +        :return: A 4-tuple (markup, original encoding, encoding
    +        declared within markup, whether any characters had to be
    +        replaced with REPLACEMENT CHARACTER).
    +        """
    +        if isinstance(markup, unicode):
    +            return markup, None, None, False
    +
    +        try_encodings = [user_specified_encoding, document_declared_encoding]
    +        dammit = UnicodeDammit(markup, try_encodings, is_html=True)
    +        return (dammit.markup, dammit.original_encoding,
    +                dammit.declared_html_encoding,
    +                dammit.contains_replacement_characters)
    +
    +    def feed(self, markup):
    +        args, kwargs = self.parser_args
    +        parser = BeautifulSoupHTMLParser(*args, **kwargs)
    +        parser.soup = self.soup
    +        try:
    +            parser.feed(markup)
    +        except HTMLParseError, e:
    +            warnings.warn(RuntimeWarning(
    +                "Python's built-in HTMLParser cannot parse the given document. This is not a bug in Beautiful Soup. The best solution is to install an external parser (lxml or html5lib), and use Beautiful Soup with that parser. See http://www.crummy.com/software/BeautifulSoup/bs4/doc/#installing-a-parser for help."))
    +            raise e
    +
    +# Patch 3.2 versions of HTMLParser earlier than 3.2.3 to use some
    +# 3.2.3 code. This ensures they don't treat markup like <p></p> as a
    +# string.
    +#
    +# XXX This code can be removed once most Python 3 users are on 3.2.3.
    +if major == 3 and minor == 2 and not CONSTRUCTOR_TAKES_STRICT:
    +    import re
    +    attrfind_tolerant = re.compile(
    +        r'\s*((?<=[\'"\s])[^\s/>][^\s/=>]*)(\s*=+\s*'
    +        r'(\'[^\']*\'|"[^"]*"|(?![\'"])[^>\s]*))?')
    +    HTMLParserTreeBuilder.attrfind_tolerant = attrfind_tolerant
    +
    +    locatestarttagend = re.compile(r"""
    +  <[a-zA-Z][-.a-zA-Z0-9:_]*          # tag name
    +  (?:\s+                             # whitespace before attribute name
    +    (?:[a-zA-Z_][-.:a-zA-Z0-9_]*     # attribute name
    +      (?:\s*=\s*                     # value indicator
    +        (?:'[^']*'                   # LITA-enclosed value
    +          |\"[^\"]*\"                # LIT-enclosed value
    +          |[^'\">\s]+                # bare value
    +         )
    +       )?
    +     )
    +   )*
    +  \s*                                # trailing whitespace
    +""", re.VERBOSE)
    +    BeautifulSoupHTMLParser.locatestarttagend = locatestarttagend
    +
    +    from html.parser import tagfind, attrfind
    +
    +    def parse_starttag(self, i):
    +        self.__starttag_text = None
    +        endpos = self.check_for_whole_start_tag(i)
    +        if endpos < 0:
    +            return endpos
    +        rawdata = self.rawdata
    +        self.__starttag_text = rawdata[i:endpos]
    +
    +        # Now parse the data between i+1 and j into a tag and attrs
    +        attrs = []
    +        match = tagfind.match(rawdata, i+1)
    +        assert match, 'unexpected call to parse_starttag()'
    +        k = match.end()
    +        self.lasttag = tag = rawdata[i+1:k].lower()
    +        while k < endpos:
    +            if self.strict:
    +                m = attrfind.match(rawdata, k)
    +            else:
    +                m = attrfind_tolerant.match(rawdata, k)
    +            if not m:
    +                break
    +            attrname, rest, attrvalue = m.group(1, 2, 3)
    +            if not rest:
    +                attrvalue = None
    +            elif attrvalue[:1] == '\'' == attrvalue[-1:] or \
    +                 attrvalue[:1] == '"' == attrvalue[-1:]:
    +                attrvalue = attrvalue[1:-1]
    +            if attrvalue:
    +                attrvalue = self.unescape(attrvalue)
    +            attrs.append((attrname.lower(), attrvalue))
    +            k = m.end()
    +
    +        end = rawdata[k:endpos].strip()
    +        if end not in (">", "/>"):
    +            lineno, offset = self.getpos()
    +            if "\n" in self.__starttag_text:
    +                lineno = lineno + self.__starttag_text.count("\n")
    +                offset = len(self.__starttag_text) \
    +                         - self.__starttag_text.rfind("\n")
    +            else:
    +                offset = offset + len(self.__starttag_text)
    +            if self.strict:
    +                self.error("junk characters in start tag: %r"
    +                           % (rawdata[k:endpos][:20],))
    +            self.handle_data(rawdata[i:endpos])
    +            return endpos
    +        if end.endswith('/>'):
    +            # XHTML-style empty tag: <span attr="value" />
    +            self.handle_startendtag(tag, attrs)
    +        else:
    +            self.handle_starttag(tag, attrs)
    +            if tag in self.CDATA_CONTENT_ELEMENTS:
    +                self.set_cdata_mode(tag)
    +        return endpos
    +
    +    def set_cdata_mode(self, elem):
    +        self.cdata_elem = elem.lower()
    +        self.interesting = re.compile(r'</\s*%s\s*>' % self.cdata_elem, re.I)
    +
    +    BeautifulSoupHTMLParser.parse_starttag = parse_starttag
    +    BeautifulSoupHTMLParser.set_cdata_mode = set_cdata_mode
    +
    +    CONSTRUCTOR_TAKES_STRICT = True
    diff --git a/libs/bs4/builder/_lxml.py b/libs/bs4/builder/_lxml.py
    new file mode 100644
    index 00000000..c78fdff6
    --- /dev/null
    +++ b/libs/bs4/builder/_lxml.py
    @@ -0,0 +1,179 @@
    +__all__ = [
    +    'LXMLTreeBuilderForXML',
    +    'LXMLTreeBuilder',
    +    ]
    +
    +from StringIO import StringIO
    +import collections
    +from lxml import etree
    +from bs4.element import Comment, Doctype, NamespacedAttribute
    +from bs4.builder import (
    +    FAST,
    +    HTML,
    +    HTMLTreeBuilder,
    +    PERMISSIVE,
    +    TreeBuilder,
    +    XML)
    +from bs4.dammit import UnicodeDammit
    +
    +LXML = 'lxml'
    +
    +class LXMLTreeBuilderForXML(TreeBuilder):
    +    DEFAULT_PARSER_CLASS = etree.XMLParser
    +
    +    is_xml = True
    +
    +    # Well, it's permissive by XML parser standards.
    +    features = [LXML, XML, FAST, PERMISSIVE]
    +
    +    CHUNK_SIZE = 512
    +
    +    @property
    +    def default_parser(self):
    +        # This can either return a parser object or a class, which
    +        # will be instantiated with default arguments.
    +        return etree.XMLParser(target=self, strip_cdata=False, recover=True)
    +
    +    def __init__(self, parser=None, empty_element_tags=None):
    +        if empty_element_tags is not None:
    +            self.empty_element_tags = set(empty_element_tags)
    +        if parser is None:
    +            # Use the default parser.
    +            parser = self.default_parser
    +        if isinstance(parser, collections.Callable):
    +            # Instantiate the parser with default arguments
    +            parser = parser(target=self, strip_cdata=False)
    +        self.parser = parser
    +        self.soup = None
    +        self.nsmaps = None
    +
    +    def _getNsTag(self, tag):
    +        # Split the namespace URL out of a fully-qualified lxml tag
    +        # name. Copied from lxml's src/lxml/sax.py.
    +        if tag[0] == '{':
    +            return tuple(tag[1:].split('}', 1))
    +        else:
    +            return (None, tag)
    +
    +    def prepare_markup(self, markup, user_specified_encoding=None,
    +                       document_declared_encoding=None):
    +        """
    +        :return: A 3-tuple (markup, original encoding, encoding
    +        declared within markup).
    +        """
    +        if isinstance(markup, unicode):
    +            return markup, None, None, False
    +
    +        try_encodings = [user_specified_encoding, document_declared_encoding]
    +        dammit = UnicodeDammit(markup, try_encodings, is_html=True)
    +        return (dammit.markup, dammit.original_encoding,
    +                dammit.declared_html_encoding,
    +                dammit.contains_replacement_characters)
    +
    +    def feed(self, markup):
    +        if isinstance(markup, basestring):
    +            markup = StringIO(markup)
    +        # Call feed() at least once, even if the markup is empty,
    +        # or the parser won't be initialized.
    +        data = markup.read(self.CHUNK_SIZE)
    +        self.parser.feed(data)
    +        while data != '':
    +            # Now call feed() on the rest of the data, chunk by chunk.
    +            data = markup.read(self.CHUNK_SIZE)
    +            if data != '':
    +                self.parser.feed(data)
    +        self.parser.close()
    +
    +    def close(self):
    +        self.nsmaps = None
    +
    +    def start(self, name, attrs, nsmap={}):
    +        # Make sure attrs is a mutable dict--lxml may send an immutable dictproxy.
    +        attrs = dict(attrs)
    +
    +        nsprefix = None
    +        # Invert each namespace map as it comes in.
    +        if len(nsmap) == 0 and self.nsmaps != None:
    +            # There are no new namespaces for this tag, but namespaces
    +            # are in play, so we need a separate tag stack to know
    +            # when they end.
    +            self.nsmaps.append(None)
    +        elif len(nsmap) > 0:
    +            # A new namespace mapping has come into play.
    +            if self.nsmaps is None:
    +                self.nsmaps = []
    +            inverted_nsmap = dict((value, key) for key, value in nsmap.items())
    +            self.nsmaps.append(inverted_nsmap)
    +            # Also treat the namespace mapping as a set of attributes on the
    +            # tag, so we can recreate it later.
    +            attrs = attrs.copy()
    +            for prefix, namespace in nsmap.items():
    +                attribute = NamespacedAttribute(
    +                    "xmlns", prefix, "http://www.w3.org/2000/xmlns/")
    +                attrs[attribute] = namespace
    +        namespace, name = self._getNsTag(name)
    +        if namespace is not None:
    +            for inverted_nsmap in reversed(self.nsmaps):
    +                if inverted_nsmap is not None and namespace in inverted_nsmap:
    +                    nsprefix = inverted_nsmap[namespace]
    +                    break
    +        self.soup.handle_starttag(name, namespace, nsprefix, attrs)
    +
    +    def end(self, name):
    +        self.soup.endData()
    +        completed_tag = self.soup.tagStack[-1]
    +        namespace, name = self._getNsTag(name)
    +        nsprefix = None
    +        if namespace is not None:
    +            for inverted_nsmap in reversed(self.nsmaps):
    +                if inverted_nsmap is not None and namespace in inverted_nsmap:
    +                    nsprefix = inverted_nsmap[namespace]
    +                    break
    +        self.soup.handle_endtag(name, nsprefix)
    +        if self.nsmaps != None:
    +            # This tag, or one of its parents, introduced a namespace
    +            # mapping, so pop it off the stack.
    +            self.nsmaps.pop()
    +            if len(self.nsmaps) == 0:
    +                # Namespaces are no longer in play, so don't bother keeping
    +                # track of the namespace stack.
    +                self.nsmaps = None
    +
    +    def pi(self, target, data):
    +        pass
    +
    +    def data(self, content):
    +        self.soup.handle_data(content)
    +
    +    def doctype(self, name, pubid, system):
    +        self.soup.endData()
    +        doctype = Doctype.for_name_and_ids(name, pubid, system)
    +        self.soup.object_was_parsed(doctype)
    +
    +    def comment(self, content):
    +        "Handle comments as Comment objects."
    +        self.soup.endData()
    +        self.soup.handle_data(content)
    +        self.soup.endData(Comment)
    +
    +    def test_fragment_to_document(self, fragment):
    +        """See `TreeBuilder`."""
    +        return u'<?xml version="1.0" encoding="utf-8"?>\n%s' % fragment
    +
    +
    +class LXMLTreeBuilder(HTMLTreeBuilder, LXMLTreeBuilderForXML):
    +
    +    features = [LXML, HTML, FAST, PERMISSIVE]
    +    is_xml = False
    +
    +    @property
    +    def default_parser(self):
    +        return etree.HTMLParser
    +
    +    def feed(self, markup):
    +        self.parser.feed(markup)
    +        self.parser.close()
    +
    +    def test_fragment_to_document(self, fragment):
    +        """See `TreeBuilder`."""
    +        return u'<html><body>%s</body></html>' % fragment
    diff --git a/libs/bs4/dammit.py b/libs/bs4/dammit.py
    new file mode 100644
    index 00000000..58cad9ba
    --- /dev/null
    +++ b/libs/bs4/dammit.py
    @@ -0,0 +1,792 @@
    +# -*- coding: utf-8 -*-
    +"""Beautiful Soup bonus library: Unicode, Dammit
    +
    +This class forces XML data into a standard format (usually to UTF-8 or
    +Unicode).  It is heavily based on code from Mark Pilgrim's Universal
    +Feed Parser. It does not rewrite the XML or HTML to reflect a new
    +encoding; that's the tree builder's job.
    +"""
    +
    +import codecs
    +from htmlentitydefs import codepoint2name
    +import re
    +import warnings
    +
    +# Autodetects character encodings. Very useful.
    +# Download from http://chardet.feedparser.org/
    +#  or 'apt-get install python-chardet'
    +#  or 'easy_install chardet'
    +try:
    +    import chardet
    +    #import chardet.constants
    +    #chardet.constants._debug = 1
    +except ImportError:
    +    chardet = None
    +
    +# Available from http://cjkpython.i18n.org/.
    +try:
    +    import iconv_codec
    +except ImportError:
    +    pass
    +
    +xml_encoding_re = re.compile(
    +    '^<\?.*encoding=[\'"](.*?)[\'"].*\?>'.encode(), re.I)
    +html_meta_re = re.compile(
    +    '<\s*meta[^>]+charset\s*=\s*["\']?([^>]*?)[ /;\'">]'.encode(), re.I)
    +
    +class EntitySubstitution(object):
    +
    +    """Substitute XML or HTML entities for the corresponding characters."""
    +
    +    def _populate_class_variables():
    +        lookup = {}
    +        reverse_lookup = {}
    +        characters_for_re = []
    +        for codepoint, name in list(codepoint2name.items()):
    +            character = unichr(codepoint)
    +            if codepoint != 34:
    +                # There's no point in turning the quotation mark into
    +                # ", unless it happens within an attribute value, which
    +                # is handled elsewhere.
    +                characters_for_re.append(character)
    +                lookup[character] = name
    +            # But we do want to turn " into the quotation mark.
    +            reverse_lookup[name] = character
    +        re_definition = "[%s]" % "".join(characters_for_re)
    +        return lookup, reverse_lookup, re.compile(re_definition)
    +    (CHARACTER_TO_HTML_ENTITY, HTML_ENTITY_TO_CHARACTER,
    +     CHARACTER_TO_HTML_ENTITY_RE) = _populate_class_variables()
    +
    +    CHARACTER_TO_XML_ENTITY = {
    +        "'": "apos",
    +        '"': "quot",
    +        "&": "amp",
    +        "<": "lt",
    +        ">": "gt",
    +        }
    +
    +    BARE_AMPERSAND_OR_BRACKET = re.compile("([<>]|"
    +                                           "&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)"
    +                                           ")")
    +
    +    @classmethod
    +    def _substitute_html_entity(cls, matchobj):
    +        entity = cls.CHARACTER_TO_HTML_ENTITY.get(matchobj.group(0))
    +        return "&%s;" % entity
    +
    +    @classmethod
    +    def _substitute_xml_entity(cls, matchobj):
    +        """Used with a regular expression to substitute the
    +        appropriate XML entity for an XML special character."""
    +        entity = cls.CHARACTER_TO_XML_ENTITY[matchobj.group(0)]
    +        return "&%s;" % entity
    +
    +    @classmethod
    +    def quoted_attribute_value(self, value):
    +        """Make a value into a quoted XML attribute, possibly escaping it.
    +
    +         Most strings will be quoted using double quotes.
    +
    +          Bob's Bar -> "Bob's Bar"
    +
    +         If a string contains double quotes, it will be quoted using
    +         single quotes.
    +
    +          Welcome to "my bar" -> 'Welcome to "my bar"'
    +
    +         If a string contains both single and double quotes, the
    +         double quotes will be escaped, and the string will be quoted
    +         using double quotes.
    +
    +          Welcome to "Bob's Bar" -> "Welcome to "Bob's bar"
    +        """
    +        quote_with = '"'
    +        if '"' in value:
    +            if "'" in value:
    +                # The string contains both single and double
    +                # quotes.  Turn the double quotes into
    +                # entities. We quote the double quotes rather than
    +                # the single quotes because the entity name is
    +                # """ whether this is HTML or XML.  If we
    +                # quoted the single quotes, we'd have to decide
    +                # between ' and &squot;.
    +                replace_with = """
    +                value = value.replace('"', replace_with)
    +            else:
    +                # There are double quotes but no single quotes.
    +                # We can use single quotes to quote the attribute.
    +                quote_with = "'"
    +        return quote_with + value + quote_with
    +
    +    @classmethod
    +    def substitute_xml(cls, value, make_quoted_attribute=False):
    +        """Substitute XML entities for special XML characters.
    +
    +        :param value: A string to be substituted. The less-than sign will
    +          become <, the greater-than sign will become >, and any
    +          ampersands that are not part of an entity defition will
    +          become &.
    +
    +        :param make_quoted_attribute: If True, then the string will be
    +         quoted, as befits an attribute value.
    +        """
    +        # Escape angle brackets, and ampersands that aren't part of
    +        # entities.
    +        value = cls.BARE_AMPERSAND_OR_BRACKET.sub(
    +            cls._substitute_xml_entity, value)
    +
    +        if make_quoted_attribute:
    +            value = cls.quoted_attribute_value(value)
    +        return value
    +
    +    @classmethod
    +    def substitute_html(cls, s):
    +        """Replace certain Unicode characters with named HTML entities.
    +
    +        This differs from data.encode(encoding, 'xmlcharrefreplace')
    +        in that the goal is to make the result more readable (to those
    +        with ASCII displays) rather than to recover from
    +        errors. There's absolutely nothing wrong with a UTF-8 string
    +        containg a LATIN SMALL LETTER E WITH ACUTE, but replacing that
    +        character with "é" will make it more readable to some
    +        people.
    +        """
    +        return cls.CHARACTER_TO_HTML_ENTITY_RE.sub(
    +            cls._substitute_html_entity, s)
    +
    +
    +class UnicodeDammit:
    +    """A class for detecting the encoding of a *ML document and
    +    converting it to a Unicode string. If the source encoding is
    +    windows-1252, can replace MS smart quotes with their HTML or XML
    +    equivalents."""
    +
    +    # This dictionary maps commonly seen values for "charset" in HTML
    +    # meta tags to the corresponding Python codec names. It only covers
    +    # values that aren't in Python's aliases and can't be determined
    +    # by the heuristics in find_codec.
    +    CHARSET_ALIASES = {"macintosh": "mac-roman",
    +                       "x-sjis": "shift-jis"}
    +
    +    ENCODINGS_WITH_SMART_QUOTES = [
    +        "windows-1252",
    +        "iso-8859-1",
    +        "iso-8859-2",
    +        ]
    +
    +    def __init__(self, markup, override_encodings=[],
    +                 smart_quotes_to=None, is_html=False):
    +        self.declared_html_encoding = None
    +        self.smart_quotes_to = smart_quotes_to
    +        self.tried_encodings = []
    +        self.contains_replacement_characters = False
    +
    +        if markup == '' or isinstance(markup, unicode):
    +            self.markup = markup
    +            self.unicode_markup = unicode(markup)
    +            self.original_encoding = None
    +            return
    +
    +        new_markup, document_encoding, sniffed_encoding = \
    +            self._detectEncoding(markup, is_html)
    +        self.markup = new_markup
    +
    +        u = None
    +        if new_markup != markup:
    +            # _detectEncoding modified the markup, then converted it to
    +            # Unicode and then to UTF-8. So convert it from UTF-8.
    +            u = self._convert_from("utf8")
    +            self.original_encoding = sniffed_encoding
    +
    +        if not u:
    +            for proposed_encoding in (
    +                override_encodings + [document_encoding, sniffed_encoding]):
    +                if proposed_encoding is not None:
    +                    u = self._convert_from(proposed_encoding)
    +                    if u:
    +                        break
    +
    +        # If no luck and we have auto-detection library, try that:
    +        if not u and chardet and not isinstance(self.markup, unicode):
    +            u = self._convert_from(chardet.detect(self.markup)['encoding'])
    +
    +        # As a last resort, try utf-8 and windows-1252:
    +        if not u:
    +            for proposed_encoding in ("utf-8", "windows-1252"):
    +                u = self._convert_from(proposed_encoding)
    +                if u:
    +                    break
    +
    +        # As an absolute last resort, try the encodings again with
    +        # character replacement.
    +        if not u:
    +            for proposed_encoding in (
    +                override_encodings + [
    +                    document_encoding, sniffed_encoding, "utf-8", "windows-1252"]):
    +                if proposed_encoding != "ascii":
    +                    u = self._convert_from(proposed_encoding, "replace")
    +                if u is not None:
    +                    warnings.warn(
    +                        UnicodeWarning(
    +                            "Some characters could not be decoded, and were "
    +                            "replaced with REPLACEMENT CHARACTER."))
    +                    self.contains_replacement_characters = True
    +                    break
    +
    +        # We could at this point force it to ASCII, but that would
    +        # destroy so much data that I think giving up is better
    +        self.unicode_markup = u
    +        if not u:
    +            self.original_encoding = None
    +
    +    def _sub_ms_char(self, match):
    +        """Changes a MS smart quote character to an XML or HTML
    +        entity, or an ASCII character."""
    +        orig = match.group(1)
    +        if self.smart_quotes_to == 'ascii':
    +            sub = self.MS_CHARS_TO_ASCII.get(orig).encode()
    +        else:
    +            sub = self.MS_CHARS.get(orig)
    +            if type(sub) == tuple:
    +                if self.smart_quotes_to == 'xml':
    +                    sub = '&#x'.encode() + sub[1].encode() + ';'.encode()
    +                else:
    +                    sub = '&'.encode() + sub[0].encode() + ';'.encode()
    +            else:
    +                sub = sub.encode()
    +        return sub
    +
    +    def _convert_from(self, proposed, errors="strict"):
    +        proposed = self.find_codec(proposed)
    +        if not proposed or (proposed, errors) in self.tried_encodings:
    +            return None
    +        self.tried_encodings.append((proposed, errors))
    +        markup = self.markup
    +
    +        # Convert smart quotes to HTML if coming from an encoding
    +        # that might have them.
    +        if (self.smart_quotes_to is not None
    +            and proposed.lower() in self.ENCODINGS_WITH_SMART_QUOTES):
    +            smart_quotes_re = b"([\x80-\x9f])"
    +            smart_quotes_compiled = re.compile(smart_quotes_re)
    +            markup = smart_quotes_compiled.sub(self._sub_ms_char, markup)
    +
    +        try:
    +            #print "Trying to convert document to %s (errors=%s)" % (
    +            #    proposed, errors)
    +            u = self._to_unicode(markup, proposed, errors)
    +            self.markup = u
    +            self.original_encoding = proposed
    +        except Exception as e:
    +            #print "That didn't work!"
    +            #print e
    +            return None
    +        #print "Correct encoding: %s" % proposed
    +        return self.markup
    +
    +    def _to_unicode(self, data, encoding, errors="strict"):
    +        '''Given a string and its encoding, decodes the string into Unicode.
    +        %encoding is a string recognized by encodings.aliases'''
    +
    +        # strip Byte Order Mark (if present)
    +        if (len(data) >= 4) and (data[:2] == '\xfe\xff') \
    +               and (data[2:4] != '\x00\x00'):
    +            encoding = 'utf-16be'
    +            data = data[2:]
    +        elif (len(data) >= 4) and (data[:2] == '\xff\xfe') \
    +                 and (data[2:4] != '\x00\x00'):
    +            encoding = 'utf-16le'
    +            data = data[2:]
    +        elif data[:3] == '\xef\xbb\xbf':
    +            encoding = 'utf-8'
    +            data = data[3:]
    +        elif data[:4] == '\x00\x00\xfe\xff':
    +            encoding = 'utf-32be'
    +            data = data[4:]
    +        elif data[:4] == '\xff\xfe\x00\x00':
    +            encoding = 'utf-32le'
    +            data = data[4:]
    +        newdata = unicode(data, encoding, errors)
    +        return newdata
    +
    +    def _detectEncoding(self, xml_data, is_html=False):
    +        """Given a document, tries to detect its XML encoding."""
    +        xml_encoding = sniffed_xml_encoding = None
    +        try:
    +            if xml_data[:4] == b'\x4c\x6f\xa7\x94':
    +                # EBCDIC
    +                xml_data = self._ebcdic_to_ascii(xml_data)
    +            elif xml_data[:4] == b'\x00\x3c\x00\x3f':
    +                # UTF-16BE
    +                sniffed_xml_encoding = 'utf-16be'
    +                xml_data = unicode(xml_data, 'utf-16be').encode('utf-8')
    +            elif (len(xml_data) >= 4) and (xml_data[:2] == b'\xfe\xff') \
    +                     and (xml_data[2:4] != b'\x00\x00'):
    +                # UTF-16BE with BOM
    +                sniffed_xml_encoding = 'utf-16be'
    +                xml_data = unicode(xml_data[2:], 'utf-16be').encode('utf-8')
    +            elif xml_data[:4] == b'\x3c\x00\x3f\x00':
    +                # UTF-16LE
    +                sniffed_xml_encoding = 'utf-16le'
    +                xml_data = unicode(xml_data, 'utf-16le').encode('utf-8')
    +            elif (len(xml_data) >= 4) and (xml_data[:2] == b'\xff\xfe') and \
    +                     (xml_data[2:4] != b'\x00\x00'):
    +                # UTF-16LE with BOM
    +                sniffed_xml_encoding = 'utf-16le'
    +                xml_data = unicode(xml_data[2:], 'utf-16le').encode('utf-8')
    +            elif xml_data[:4] == b'\x00\x00\x00\x3c':
    +                # UTF-32BE
    +                sniffed_xml_encoding = 'utf-32be'
    +                xml_data = unicode(xml_data, 'utf-32be').encode('utf-8')
    +            elif xml_data[:4] == b'\x3c\x00\x00\x00':
    +                # UTF-32LE
    +                sniffed_xml_encoding = 'utf-32le'
    +                xml_data = unicode(xml_data, 'utf-32le').encode('utf-8')
    +            elif xml_data[:4] == b'\x00\x00\xfe\xff':
    +                # UTF-32BE with BOM
    +                sniffed_xml_encoding = 'utf-32be'
    +                xml_data = unicode(xml_data[4:], 'utf-32be').encode('utf-8')
    +            elif xml_data[:4] == b'\xff\xfe\x00\x00':
    +                # UTF-32LE with BOM
    +                sniffed_xml_encoding = 'utf-32le'
    +                xml_data = unicode(xml_data[4:], 'utf-32le').encode('utf-8')
    +            elif xml_data[:3] == b'\xef\xbb\xbf':
    +                # UTF-8 with BOM
    +                sniffed_xml_encoding = 'utf-8'
    +                xml_data = unicode(xml_data[3:], 'utf-8').encode('utf-8')
    +            else:
    +                sniffed_xml_encoding = 'ascii'
    +                pass
    +        except:
    +            xml_encoding_match = None
    +        xml_encoding_match = xml_encoding_re.match(xml_data)
    +        if not xml_encoding_match and is_html:
    +            xml_encoding_match = html_meta_re.search(xml_data)
    +        if xml_encoding_match is not None:
    +            xml_encoding = xml_encoding_match.groups()[0].decode(
    +                'ascii').lower()
    +            if is_html:
    +                self.declared_html_encoding = xml_encoding
    +            if sniffed_xml_encoding and \
    +               (xml_encoding in ('iso-10646-ucs-2', 'ucs-2', 'csunicode',
    +                                 'iso-10646-ucs-4', 'ucs-4', 'csucs4',
    +                                 'utf-16', 'utf-32', 'utf_16', 'utf_32',
    +                                 'utf16', 'u16')):
    +                xml_encoding = sniffed_xml_encoding
    +        return xml_data, xml_encoding, sniffed_xml_encoding
    +
    +    def find_codec(self, charset):
    +        return self._codec(self.CHARSET_ALIASES.get(charset, charset)) \
    +               or (charset and self._codec(charset.replace("-", ""))) \
    +               or (charset and self._codec(charset.replace("-", "_"))) \
    +               or charset
    +
    +    def _codec(self, charset):
    +        if not charset:
    +            return charset
    +        codec = None
    +        try:
    +            codecs.lookup(charset)
    +            codec = charset
    +        except (LookupError, ValueError):
    +            pass
    +        return codec
    +
    +    EBCDIC_TO_ASCII_MAP = None
    +
    +    def _ebcdic_to_ascii(self, s):
    +        c = self.__class__
    +        if not c.EBCDIC_TO_ASCII_MAP:
    +            emap = (0,1,2,3,156,9,134,127,151,141,142,11,12,13,14,15,
    +                    16,17,18,19,157,133,8,135,24,25,146,143,28,29,30,31,
    +                    128,129,130,131,132,10,23,27,136,137,138,139,140,5,6,7,
    +                    144,145,22,147,148,149,150,4,152,153,154,155,20,21,158,26,
    +                    32,160,161,162,163,164,165,166,167,168,91,46,60,40,43,33,
    +                    38,169,170,171,172,173,174,175,176,177,93,36,42,41,59,94,
    +                    45,47,178,179,180,181,182,183,184,185,124,44,37,95,62,63,
    +                    186,187,188,189,190,191,192,193,194,96,58,35,64,39,61,34,
    +                    195,97,98,99,100,101,102,103,104,105,196,197,198,199,200,
    +                    201,202,106,107,108,109,110,111,112,113,114,203,204,205,
    +                    206,207,208,209,126,115,116,117,118,119,120,121,122,210,
    +                    211,212,213,214,215,216,217,218,219,220,221,222,223,224,
    +                    225,226,227,228,229,230,231,123,65,66,67,68,69,70,71,72,
    +                    73,232,233,234,235,236,237,125,74,75,76,77,78,79,80,81,
    +                    82,238,239,240,241,242,243,92,159,83,84,85,86,87,88,89,
    +                    90,244,245,246,247,248,249,48,49,50,51,52,53,54,55,56,57,
    +                    250,251,252,253,254,255)
    +            import string
    +            c.EBCDIC_TO_ASCII_MAP = string.maketrans(
    +            ''.join(map(chr, list(range(256)))), ''.join(map(chr, emap)))
    +        return s.translate(c.EBCDIC_TO_ASCII_MAP)
    +
    +    # A partial mapping of ISO-Latin-1 to HTML entities/XML numeric entities.
    +    MS_CHARS = {b'\x80': ('euro', '20AC'),
    +                b'\x81': ' ',
    +                b'\x82': ('sbquo', '201A'),
    +                b'\x83': ('fnof', '192'),
    +                b'\x84': ('bdquo', '201E'),
    +                b'\x85': ('hellip', '2026'),
    +                b'\x86': ('dagger', '2020'),
    +                b'\x87': ('Dagger', '2021'),
    +                b'\x88': ('circ', '2C6'),
    +                b'\x89': ('permil', '2030'),
    +                b'\x8A': ('Scaron', '160'),
    +                b'\x8B': ('lsaquo', '2039'),
    +                b'\x8C': ('OElig', '152'),
    +                b'\x8D': '?',
    +                b'\x8E': ('#x17D', '17D'),
    +                b'\x8F': '?',
    +                b'\x90': '?',
    +                b'\x91': ('lsquo', '2018'),
    +                b'\x92': ('rsquo', '2019'),
    +                b'\x93': ('ldquo', '201C'),
    +                b'\x94': ('rdquo', '201D'),
    +                b'\x95': ('bull', '2022'),
    +                b'\x96': ('ndash', '2013'),
    +                b'\x97': ('mdash', '2014'),
    +                b'\x98': ('tilde', '2DC'),
    +                b'\x99': ('trade', '2122'),
    +                b'\x9a': ('scaron', '161'),
    +                b'\x9b': ('rsaquo', '203A'),
    +                b'\x9c': ('oelig', '153'),
    +                b'\x9d': '?',
    +                b'\x9e': ('#x17E', '17E'),
    +                b'\x9f': ('Yuml', ''),}
    +
    +    # A parochial partial mapping of ISO-Latin-1 to ASCII. Contains
    +    # horrors like stripping diacritical marks to turn á into a, but also
    +    # contains non-horrors like turning “ into ".
    +    MS_CHARS_TO_ASCII = {
    +        b'\x80' : 'EUR',
    +        b'\x81' : ' ',
    +        b'\x82' : ',',
    +        b'\x83' : 'f',
    +        b'\x84' : ',,',
    +        b'\x85' : '...',
    +        b'\x86' : '+',
    +        b'\x87' : '++',
    +        b'\x88' : '^',
    +        b'\x89' : '%',
    +        b'\x8a' : 'S',
    +        b'\x8b' : '<',
    +        b'\x8c' : 'OE',
    +        b'\x8d' : '?',
    +        b'\x8e' : 'Z',
    +        b'\x8f' : '?',
    +        b'\x90' : '?',
    +        b'\x91' : "'",
    +        b'\x92' : "'",
    +        b'\x93' : '"',
    +        b'\x94' : '"',
    +        b'\x95' : '*',
    +        b'\x96' : '-',
    +        b'\x97' : '--',
    +        b'\x98' : '~',
    +        b'\x99' : '(TM)',
    +        b'\x9a' : 's',
    +        b'\x9b' : '>',
    +        b'\x9c' : 'oe',
    +        b'\x9d' : '?',
    +        b'\x9e' : 'z',
    +        b'\x9f' : 'Y',
    +        b'\xa0' : ' ',
    +        b'\xa1' : '!',
    +        b'\xa2' : 'c',
    +        b'\xa3' : 'GBP',
    +        b'\xa4' : '$', #This approximation is especially parochial--this is the
    +                       #generic currency symbol.
    +        b'\xa5' : 'YEN',
    +        b'\xa6' : '|',
    +        b'\xa7' : 'S',
    +        b'\xa8' : '..',
    +        b'\xa9' : '',
    +        b'\xaa' : '(th)',
    +        b'\xab' : '<<',
    +        b'\xac' : '!',
    +        b'\xad' : ' ',
    +        b'\xae' : '(R)',
    +        b'\xaf' : '-',
    +        b'\xb0' : 'o',
    +        b'\xb1' : '+-',
    +        b'\xb2' : '2',
    +        b'\xb3' : '3',
    +        b'\xb4' : ("'", 'acute'),
    +        b'\xb5' : 'u',
    +        b'\xb6' : 'P',
    +        b'\xb7' : '*',
    +        b'\xb8' : ',',
    +        b'\xb9' : '1',
    +        b'\xba' : '(th)',
    +        b'\xbb' : '>>',
    +        b'\xbc' : '1/4',
    +        b'\xbd' : '1/2',
    +        b'\xbe' : '3/4',
    +        b'\xbf' : '?',
    +        b'\xc0' : 'A',
    +        b'\xc1' : 'A',
    +        b'\xc2' : 'A',
    +        b'\xc3' : 'A',
    +        b'\xc4' : 'A',
    +        b'\xc5' : 'A',
    +        b'\xc6' : 'AE',
    +        b'\xc7' : 'C',
    +        b'\xc8' : 'E',
    +        b'\xc9' : 'E',
    +        b'\xca' : 'E',
    +        b'\xcb' : 'E',
    +        b'\xcc' : 'I',
    +        b'\xcd' : 'I',
    +        b'\xce' : 'I',
    +        b'\xcf' : 'I',
    +        b'\xd0' : 'D',
    +        b'\xd1' : 'N',
    +        b'\xd2' : 'O',
    +        b'\xd3' : 'O',
    +        b'\xd4' : 'O',
    +        b'\xd5' : 'O',
    +        b'\xd6' : 'O',
    +        b'\xd7' : '*',
    +        b'\xd8' : 'O',
    +        b'\xd9' : 'U',
    +        b'\xda' : 'U',
    +        b'\xdb' : 'U',
    +        b'\xdc' : 'U',
    +        b'\xdd' : 'Y',
    +        b'\xde' : 'b',
    +        b'\xdf' : 'B',
    +        b'\xe0' : 'a',
    +        b'\xe1' : 'a',
    +        b'\xe2' : 'a',
    +        b'\xe3' : 'a',
    +        b'\xe4' : 'a',
    +        b'\xe5' : 'a',
    +        b'\xe6' : 'ae',
    +        b'\xe7' : 'c',
    +        b'\xe8' : 'e',
    +        b'\xe9' : 'e',
    +        b'\xea' : 'e',
    +        b'\xeb' : 'e',
    +        b'\xec' : 'i',
    +        b'\xed' : 'i',
    +        b'\xee' : 'i',
    +        b'\xef' : 'i',
    +        b'\xf0' : 'o',
    +        b'\xf1' : 'n',
    +        b'\xf2' : 'o',
    +        b'\xf3' : 'o',
    +        b'\xf4' : 'o',
    +        b'\xf5' : 'o',
    +        b'\xf6' : 'o',
    +        b'\xf7' : '/',
    +        b'\xf8' : 'o',
    +        b'\xf9' : 'u',
    +        b'\xfa' : 'u',
    +        b'\xfb' : 'u',
    +        b'\xfc' : 'u',
    +        b'\xfd' : 'y',
    +        b'\xfe' : 'b',
    +        b'\xff' : 'y',
    +        }
    +
    +    # A map used when removing rogue Windows-1252/ISO-8859-1
    +    # characters in otherwise UTF-8 documents.
    +    #
    +    # Note that \x81, \x8d, \x8f, \x90, and \x9d are undefined in
    +    # Windows-1252.
    +    WINDOWS_1252_TO_UTF8 = {
    +        0x80 : b'\xe2\x82\xac', # €
    +        0x82 : b'\xe2\x80\x9a', # ‚
    +        0x83 : b'\xc6\x92',     # ƒ
    +        0x84 : b'\xe2\x80\x9e', # „
    +        0x85 : b'\xe2\x80\xa6', # …
    +        0x86 : b'\xe2\x80\xa0', # †
    +        0x87 : b'\xe2\x80\xa1', # ‡
    +        0x88 : b'\xcb\x86',     # ˆ
    +        0x89 : b'\xe2\x80\xb0', # ‰
    +        0x8a : b'\xc5\xa0',     # Š
    +        0x8b : b'\xe2\x80\xb9', # ‹
    +        0x8c : b'\xc5\x92',     # Œ
    +        0x8e : b'\xc5\xbd',     # Ž
    +        0x91 : b'\xe2\x80\x98', # ‘
    +        0x92 : b'\xe2\x80\x99', # ’
    +        0x93 : b'\xe2\x80\x9c', # “
    +        0x94 : b'\xe2\x80\x9d', # ”
    +        0x95 : b'\xe2\x80\xa2', # •
    +        0x96 : b'\xe2\x80\x93', # –
    +        0x97 : b'\xe2\x80\x94', # —
    +        0x98 : b'\xcb\x9c',     # ˜
    +        0x99 : b'\xe2\x84\xa2', # ™
    +        0x9a : b'\xc5\xa1',     # š
    +        0x9b : b'\xe2\x80\xba', # ›
    +        0x9c : b'\xc5\x93',     # œ
    +        0x9e : b'\xc5\xbe',     # ž
    +        0x9f : b'\xc5\xb8',     # Ÿ
    +        0xa0 : b'\xc2\xa0',     #  
    +        0xa1 : b'\xc2\xa1',     # ¡
    +        0xa2 : b'\xc2\xa2',     # ¢
    +        0xa3 : b'\xc2\xa3',     # £
    +        0xa4 : b'\xc2\xa4',     # ¤
    +        0xa5 : b'\xc2\xa5',     # ¥
    +        0xa6 : b'\xc2\xa6',     # ¦
    +        0xa7 : b'\xc2\xa7',     # §
    +        0xa8 : b'\xc2\xa8',     # ¨
    +        0xa9 : b'\xc2\xa9',     # ©
    +        0xaa : b'\xc2\xaa',     # ª
    +        0xab : b'\xc2\xab',     # «
    +        0xac : b'\xc2\xac',     # ¬
    +        0xad : b'\xc2\xad',     # ­
    +        0xae : b'\xc2\xae',     # ®
    +        0xaf : b'\xc2\xaf',     # ¯
    +        0xb0 : b'\xc2\xb0',     # °
    +        0xb1 : b'\xc2\xb1',     # ±
    +        0xb2 : b'\xc2\xb2',     # ²
    +        0xb3 : b'\xc2\xb3',     # ³
    +        0xb4 : b'\xc2\xb4',     # ´
    +        0xb5 : b'\xc2\xb5',     # µ
    +        0xb6 : b'\xc2\xb6',     # ¶
    +        0xb7 : b'\xc2\xb7',     # ·
    +        0xb8 : b'\xc2\xb8',     # ¸
    +        0xb9 : b'\xc2\xb9',     # ¹
    +        0xba : b'\xc2\xba',     # º
    +        0xbb : b'\xc2\xbb',     # »
    +        0xbc : b'\xc2\xbc',     # ¼
    +        0xbd : b'\xc2\xbd',     # ½
    +        0xbe : b'\xc2\xbe',     # ¾
    +        0xbf : b'\xc2\xbf',     # ¿
    +        0xc0 : b'\xc3\x80',     # À
    +        0xc1 : b'\xc3\x81',     # Á
    +        0xc2 : b'\xc3\x82',     # Â
    +        0xc3 : b'\xc3\x83',     # Ã
    +        0xc4 : b'\xc3\x84',     # Ä
    +        0xc5 : b'\xc3\x85',     # Å
    +        0xc6 : b'\xc3\x86',     # Æ
    +        0xc7 : b'\xc3\x87',     # Ç
    +        0xc8 : b'\xc3\x88',     # È
    +        0xc9 : b'\xc3\x89',     # É
    +        0xca : b'\xc3\x8a',     # Ê
    +        0xcb : b'\xc3\x8b',     # Ë
    +        0xcc : b'\xc3\x8c',     # Ì
    +        0xcd : b'\xc3\x8d',     # Í
    +        0xce : b'\xc3\x8e',     # Î
    +        0xcf : b'\xc3\x8f',     # Ï
    +        0xd0 : b'\xc3\x90',     # Ð
    +        0xd1 : b'\xc3\x91',     # Ñ
    +        0xd2 : b'\xc3\x92',     # Ò
    +        0xd3 : b'\xc3\x93',     # Ó
    +        0xd4 : b'\xc3\x94',     # Ô
    +        0xd5 : b'\xc3\x95',     # Õ
    +        0xd6 : b'\xc3\x96',     # Ö
    +        0xd7 : b'\xc3\x97',     # ×
    +        0xd8 : b'\xc3\x98',     # Ø
    +        0xd9 : b'\xc3\x99',     # Ù
    +        0xda : b'\xc3\x9a',     # Ú
    +        0xdb : b'\xc3\x9b',     # Û
    +        0xdc : b'\xc3\x9c',     # Ü
    +        0xdd : b'\xc3\x9d',     # Ý
    +        0xde : b'\xc3\x9e',     # Þ
    +        0xdf : b'\xc3\x9f',     # ß
    +        0xe0 : b'\xc3\xa0',     # à
    +        0xe1 : b'\xa1',     # á
    +        0xe2 : b'\xc3\xa2',     # â
    +        0xe3 : b'\xc3\xa3',     # ã
    +        0xe4 : b'\xc3\xa4',     # ä
    +        0xe5 : b'\xc3\xa5',     # å
    +        0xe6 : b'\xc3\xa6',     # æ
    +        0xe7 : b'\xc3\xa7',     # ç
    +        0xe8 : b'\xc3\xa8',     # è
    +        0xe9 : b'\xc3\xa9',     # é
    +        0xea : b'\xc3\xaa',     # ê
    +        0xeb : b'\xc3\xab',     # ë
    +        0xec : b'\xc3\xac',     # ì
    +        0xed : b'\xc3\xad',     # í
    +        0xee : b'\xc3\xae',     # î
    +        0xef : b'\xc3\xaf',     # ï
    +        0xf0 : b'\xc3\xb0',     # ð
    +        0xf1 : b'\xc3\xb1',     # ñ
    +        0xf2 : b'\xc3\xb2',     # ò
    +        0xf3 : b'\xc3\xb3',     # ó
    +        0xf4 : b'\xc3\xb4',     # ô
    +        0xf5 : b'\xc3\xb5',     # õ
    +        0xf6 : b'\xc3\xb6',     # ö
    +        0xf7 : b'\xc3\xb7',     # ÷
    +        0xf8 : b'\xc3\xb8',     # ø
    +        0xf9 : b'\xc3\xb9',     # ù
    +        0xfa : b'\xc3\xba',     # ú
    +        0xfb : b'\xc3\xbb',     # û
    +        0xfc : b'\xc3\xbc',     # ü
    +        0xfd : b'\xc3\xbd',     # ý
    +        0xfe : b'\xc3\xbe',     # þ
    +        }
    +
    +    MULTIBYTE_MARKERS_AND_SIZES = [
    +        (0xc2, 0xdf, 2), # 2-byte characters start with a byte C2-DF
    +        (0xe0, 0xef, 3), # 3-byte characters start with E0-EF
    +        (0xf0, 0xf4, 4), # 4-byte characters start with F0-F4
    +        ]
    +
    +    FIRST_MULTIBYTE_MARKER = MULTIBYTE_MARKERS_AND_SIZES[0][0]
    +    LAST_MULTIBYTE_MARKER = MULTIBYTE_MARKERS_AND_SIZES[-1][1]
    +
    +    @classmethod
    +    def detwingle(cls, in_bytes, main_encoding="utf8",
    +                  embedded_encoding="windows-1252"):
    +        """Fix characters from one encoding embedded in some other encoding.
    +
    +        Currently the only situation supported is Windows-1252 (or its
    +        subset ISO-8859-1), embedded in UTF-8.
    +
    +        The input must be a bytestring. If you've already converted
    +        the document to Unicode, you're too late.
    +
    +        The output is a bytestring in which `embedded_encoding`
    +        characters have been converted to their `main_encoding`
    +        equivalents.
    +        """
    +        if embedded_encoding.replace('_', '-').lower() not in (
    +            'windows-1252', 'windows_1252'):
    +            raise NotImplementedError(
    +                "Windows-1252 and ISO-8859-1 are the only currently supported "
    +                "embedded encodings.")
    +
    +        if main_encoding.lower() not in ('utf8', 'utf-8'):
    +            raise NotImplementedError(
    +                "UTF-8 is the only currently supported main encoding.")
    +
    +        byte_chunks = []
    +
    +        chunk_start = 0
    +        pos = 0
    +        while pos < len(in_bytes):
    +            byte = in_bytes[pos]
    +            if not isinstance(byte, int):
    +                # Python 2.x
    +                byte = ord(byte)
    +            if (byte >= cls.FIRST_MULTIBYTE_MARKER
    +                and byte <= cls.LAST_MULTIBYTE_MARKER):
    +                # This is the start of a UTF-8 multibyte character. Skip
    +                # to the end.
    +                for start, end, size in cls.MULTIBYTE_MARKERS_AND_SIZES:
    +                    if byte >= start and byte <= end:
    +                        pos += size
    +                        break
    +            elif byte >= 0x80 and byte in cls.WINDOWS_1252_TO_UTF8:
    +                # We found a Windows-1252 character!
    +                # Save the string up to this point as a chunk.
    +                byte_chunks.append(in_bytes[chunk_start:pos])
    +
    +                # Now translate the Windows-1252 character into UTF-8
    +                # and add it as another, one-byte chunk.
    +                byte_chunks.append(cls.WINDOWS_1252_TO_UTF8[byte])
    +                pos += 1
    +                chunk_start = pos
    +            else:
    +                # Go on to the next character.
    +                pos += 1
    +        if chunk_start == 0:
    +            # The string is unchanged.
    +            return in_bytes
    +        else:
    +            # Store the final chunk.
    +            byte_chunks.append(in_bytes[chunk_start:])
    +        return b''.join(byte_chunks)
    +
    diff --git a/libs/bs4/element.py b/libs/bs4/element.py
    new file mode 100644
    index 00000000..91a40078
    --- /dev/null
    +++ b/libs/bs4/element.py
    @@ -0,0 +1,1347 @@
    +import collections
    +import re
    +import sys
    +import warnings
    +from bs4.dammit import EntitySubstitution
    +
    +DEFAULT_OUTPUT_ENCODING = "utf-8"
    +PY3K = (sys.version_info[0] > 2)
    +
    +whitespace_re = re.compile("\s+")
    +
    +def _alias(attr):
    +    """Alias one attribute name to another for backward compatibility"""
    +    @property
    +    def alias(self):
    +        return getattr(self, attr)
    +
    +    @alias.setter
    +    def alias(self):
    +        return setattr(self, attr)
    +    return alias
    +
    +
    +class NamespacedAttribute(unicode):
    +
    +    def __new__(cls, prefix, name, namespace=None):
    +        if name is None:
    +            obj = unicode.__new__(cls, prefix)
    +        else:
    +            obj = unicode.__new__(cls, prefix + ":" + name)
    +        obj.prefix = prefix
    +        obj.name = name
    +        obj.namespace = namespace
    +        return obj
    +
    +class AttributeValueWithCharsetSubstitution(unicode):
    +    """A stand-in object for a character encoding specified in HTML."""
    +
    +class CharsetMetaAttributeValue(AttributeValueWithCharsetSubstitution):
    +    """A generic stand-in for the value of a meta tag's 'charset' attribute.
    +
    +    When Beautiful Soup parses the markup '<meta charset="utf8">', the
    +    value of the 'charset' attribute will be one of these objects.
    +    """
    +
    +    def __new__(cls, original_value):
    +        obj = unicode.__new__(cls, original_value)
    +        obj.original_value = original_value
    +        return obj
    +
    +    def encode(self, encoding):
    +        return encoding
    +
    +
    +class ContentMetaAttributeValue(AttributeValueWithCharsetSubstitution):
    +    """A generic stand-in for the value of a meta tag's 'content' attribute.
    +
    +    When Beautiful Soup parses the markup:
    +     <meta http-equiv="content-type" content="text/html; charset=utf8">
    +
    +    The value of the 'content' attribute will be one of these objects.
    +    """
    +
    +    CHARSET_RE = re.compile("((^|;)\s*charset=)([^;]*)", re.M)
    +
    +    def __new__(cls, original_value):
    +        match = cls.CHARSET_RE.search(original_value)
    +        if match is None:
    +            # No substitution necessary.
    +            return unicode.__new__(unicode, original_value)
    +
    +        obj = unicode.__new__(cls, original_value)
    +        obj.original_value = original_value
    +        return obj
    +
    +    def encode(self, encoding):
    +        def rewrite(match):
    +            return match.group(1) + encoding
    +        return self.CHARSET_RE.sub(rewrite, self.original_value)
    +
    +
    +class PageElement(object):
    +    """Contains the navigational information for some part of the page
    +    (either a tag or a piece of text)"""
    +
    +    # There are five possible values for the "formatter" argument passed in
    +    # to methods like encode() and prettify():
    +    #
    +    # "html" - All Unicode characters with corresponding HTML entities
    +    #   are converted to those entities on output.
    +    # "minimal" - Bare ampersands and angle brackets are converted to
    +    #   XML entities: & < >
    +    # None - The null formatter. Unicode characters are never
    +    #   converted to entities.  This is not recommended, but it's
    +    #   faster than "minimal".
    +    # A function - This function will be called on every string that
    +    #  needs to undergo entity substition
    +    FORMATTERS = {
    +        "html" : EntitySubstitution.substitute_html,
    +        "minimal" : EntitySubstitution.substitute_xml,
    +        None : None
    +        }
    +
    +    @classmethod
    +    def format_string(self, s, formatter='minimal'):
    +        """Format the given string using the given formatter."""
    +        if not callable(formatter):
    +            formatter = self.FORMATTERS.get(
    +                formatter, EntitySubstitution.substitute_xml)
    +        if formatter is None:
    +            output = s
    +        else:
    +            output = formatter(s)
    +        return output
    +
    +    def setup(self, parent=None, previous_element=None):
    +        """Sets up the initial relations between this element and
    +        other elements."""
    +        self.parent = parent
    +        self.previous_element = previous_element
    +        if previous_element is not None:
    +            self.previous_element.next_element = self
    +        self.next_element = None
    +        self.previous_sibling = None
    +        self.next_sibling = None
    +        if self.parent is not None and self.parent.contents:
    +            self.previous_sibling = self.parent.contents[-1]
    +            self.previous_sibling.next_sibling = self
    +
    +    nextSibling = _alias("next_sibling")  # BS3
    +    previousSibling = _alias("previous_sibling")  # BS3
    +
    +    def replace_with(self, replace_with):
    +        if replace_with is self:
    +            return
    +        if replace_with is self.parent:
    +            raise ValueError("Cannot replace a Tag with its parent.")
    +        old_parent = self.parent
    +        my_index = self.parent.index(self)
    +        self.extract()
    +        old_parent.insert(my_index, replace_with)
    +        return self
    +    replaceWith = replace_with  # BS3
    +
    +    def unwrap(self):
    +        my_parent = self.parent
    +        my_index = self.parent.index(self)
    +        self.extract()
    +        for child in reversed(self.contents[:]):
    +            my_parent.insert(my_index, child)
    +        return self
    +    replace_with_children = unwrap
    +    replaceWithChildren = unwrap  # BS3
    +
    +    def wrap(self, wrap_inside):
    +        me = self.replace_with(wrap_inside)
    +        wrap_inside.append(me)
    +        return wrap_inside
    +
    +    def extract(self):
    +        """Destructively rips this element out of the tree."""
    +        if self.parent is not None:
    +            del self.parent.contents[self.parent.index(self)]
    +
    +        #Find the two elements that would be next to each other if
    +        #this element (and any children) hadn't been parsed. Connect
    +        #the two.
    +        last_child = self._last_descendant()
    +        next_element = last_child.next_element
    +
    +        if self.previous_element is not None:
    +            self.previous_element.next_element = next_element
    +        if next_element is not None:
    +            next_element.previous_element = self.previous_element
    +        self.previous_element = None
    +        last_child.next_element = None
    +
    +        self.parent = None
    +        if self.previous_sibling is not None:
    +            self.previous_sibling.next_sibling = self.next_sibling
    +        if self.next_sibling is not None:
    +            self.next_sibling.previous_sibling = self.previous_sibling
    +        self.previous_sibling = self.next_sibling = None
    +        return self
    +
    +    def _last_descendant(self):
    +        "Finds the last element beneath this object to be parsed."
    +        last_child = self
    +        while hasattr(last_child, 'contents') and last_child.contents:
    +            last_child = last_child.contents[-1]
    +        return last_child
    +    # BS3: Not part of the API!
    +    _lastRecursiveChild = _last_descendant
    +
    +    def insert(self, position, new_child):
    +        if new_child is self:
    +            raise ValueError("Cannot insert a tag into itself.")
    +        if (isinstance(new_child, basestring)
    +            and not isinstance(new_child, NavigableString)):
    +            new_child = NavigableString(new_child)
    +
    +        position = min(position, len(self.contents))
    +        if hasattr(new_child, 'parent') and new_child.parent is not None:
    +            # We're 'inserting' an element that's already one
    +            # of this object's children.
    +            if new_child.parent is self:
    +                current_index = self.index(new_child)
    +                if current_index < position:
    +                    # We're moving this element further down the list
    +                    # of this object's children. That means that when
    +                    # we extract this element, our target index will
    +                    # jump down one.
    +                    position -= 1
    +            new_child.extract()
    +
    +        new_child.parent = self
    +        previous_child = None
    +        if position == 0:
    +            new_child.previous_sibling = None
    +            new_child.previous_element = self
    +        else:
    +            previous_child = self.contents[position - 1]
    +            new_child.previous_sibling = previous_child
    +            new_child.previous_sibling.next_sibling = new_child
    +            new_child.previous_element = previous_child._last_descendant()
    +        if new_child.previous_element is not None:
    +            new_child.previous_element.next_element = new_child
    +
    +        new_childs_last_element = new_child._last_descendant()
    +
    +        if position >= len(self.contents):
    +            new_child.next_sibling = None
    +
    +            parent = self
    +            parents_next_sibling = None
    +            while parents_next_sibling is None and parent is not None:
    +                parents_next_sibling = parent.next_sibling
    +                parent = parent.parent
    +                if parents_next_sibling is not None:
    +                    # We found the element that comes next in the document.
    +                    break
    +            if parents_next_sibling is not None:
    +                new_childs_last_element.next_element = parents_next_sibling
    +            else:
    +                # The last element of this tag is the last element in
    +                # the document.
    +                new_childs_last_element.next_element = None
    +        else:
    +            next_child = self.contents[position]
    +            new_child.next_sibling = next_child
    +            if new_child.next_sibling is not None:
    +                new_child.next_sibling.previous_sibling = new_child
    +            new_childs_last_element.next_element = next_child
    +
    +        if new_childs_last_element.next_element is not None:
    +            new_childs_last_element.next_element.previous_element = new_childs_last_element
    +        self.contents.insert(position, new_child)
    +
    +    def append(self, tag):
    +        """Appends the given tag to the contents of this tag."""
    +        self.insert(len(self.contents), tag)
    +
    +    def insert_before(self, predecessor):
    +        """Makes the given element the immediate predecessor of this one.
    +
    +        The two elements will have the same parent, and the given element
    +        will be immediately before this one.
    +        """
    +        if self is predecessor:
    +            raise ValueError("Can't insert an element before itself.")
    +        parent = self.parent
    +        if parent is None:
    +            raise ValueError(
    +                "Element has no parent, so 'before' has no meaning.")
    +        # Extract first so that the index won't be screwed up if they
    +        # are siblings.
    +        if isinstance(predecessor, PageElement):
    +            predecessor.extract()
    +        index = parent.index(self)
    +        parent.insert(index, predecessor)
    +
    +    def insert_after(self, successor):
    +        """Makes the given element the immediate successor of this one.
    +
    +        The two elements will have the same parent, and the given element
    +        will be immediately after this one.
    +        """
    +        if self is successor:
    +            raise ValueError("Can't insert an element after itself.")
    +        parent = self.parent
    +        if parent is None:
    +            raise ValueError(
    +                "Element has no parent, so 'after' has no meaning.")
    +        # Extract first so that the index won't be screwed up if they
    +        # are siblings.
    +        if isinstance(successor, PageElement):
    +            successor.extract()
    +        index = parent.index(self)
    +        parent.insert(index+1, successor)
    +
    +    def find_next(self, name=None, attrs={}, text=None, **kwargs):
    +        """Returns the first item that matches the given criteria and
    +        appears after this Tag in the document."""
    +        return self._find_one(self.find_all_next, name, attrs, text, **kwargs)
    +    findNext = find_next  # BS3
    +
    +    def find_all_next(self, name=None, attrs={}, text=None, limit=None,
    +                    **kwargs):
    +        """Returns all items that match the given criteria and appear
    +        after this Tag in the document."""
    +        return self._find_all(name, attrs, text, limit, self.next_elements,
    +                             **kwargs)
    +    findAllNext = find_all_next  # BS3
    +
    +    def find_next_sibling(self, name=None, attrs={}, text=None, **kwargs):
    +        """Returns the closest sibling to this Tag that matches the
    +        given criteria and appears after this Tag in the document."""
    +        return self._find_one(self.find_next_siblings, name, attrs, text,
    +                             **kwargs)
    +    findNextSibling = find_next_sibling  # BS3
    +
    +    def find_next_siblings(self, name=None, attrs={}, text=None, limit=None,
    +                           **kwargs):
    +        """Returns the siblings of this Tag that match the given
    +        criteria and appear after this Tag in the document."""
    +        return self._find_all(name, attrs, text, limit,
    +                              self.next_siblings, **kwargs)
    +    findNextSiblings = find_next_siblings   # BS3
    +    fetchNextSiblings = find_next_siblings  # BS2
    +
    +    def find_previous(self, name=None, attrs={}, text=None, **kwargs):
    +        """Returns the first item that matches the given criteria and
    +        appears before this Tag in the document."""
    +        return self._find_one(
    +            self.find_all_previous, name, attrs, text, **kwargs)
    +    findPrevious = find_previous  # BS3
    +
    +    def find_all_previous(self, name=None, attrs={}, text=None, limit=None,
    +                        **kwargs):
    +        """Returns all items that match the given criteria and appear
    +        before this Tag in the document."""
    +        return self._find_all(name, attrs, text, limit, self.previous_elements,
    +                           **kwargs)
    +    findAllPrevious = find_all_previous  # BS3
    +    fetchPrevious = find_all_previous    # BS2
    +
    +    def find_previous_sibling(self, name=None, attrs={}, text=None, **kwargs):
    +        """Returns the closest sibling to this Tag that matches the
    +        given criteria and appears before this Tag in the document."""
    +        return self._find_one(self.find_previous_siblings, name, attrs, text,
    +                             **kwargs)
    +    findPreviousSibling = find_previous_sibling  # BS3
    +
    +    def find_previous_siblings(self, name=None, attrs={}, text=None,
    +                               limit=None, **kwargs):
    +        """Returns the siblings of this Tag that match the given
    +        criteria and appear before this Tag in the document."""
    +        return self._find_all(name, attrs, text, limit,
    +                              self.previous_siblings, **kwargs)
    +    findPreviousSiblings = find_previous_siblings   # BS3
    +    fetchPreviousSiblings = find_previous_siblings  # BS2
    +
    +    def find_parent(self, name=None, attrs={}, **kwargs):
    +        """Returns the closest parent of this Tag that matches the given
    +        criteria."""
    +        # NOTE: We can't use _find_one because findParents takes a different
    +        # set of arguments.
    +        r = None
    +        l = self.find_parents(name, attrs, 1)
    +        if l:
    +            r = l[0]
    +        return r
    +    findParent = find_parent  # BS3
    +
    +    def find_parents(self, name=None, attrs={}, limit=None, **kwargs):
    +        """Returns the parents of this Tag that match the given
    +        criteria."""
    +
    +        return self._find_all(name, attrs, None, limit, self.parents,
    +                             **kwargs)
    +    findParents = find_parents   # BS3
    +    fetchParents = find_parents  # BS2
    +
    +    @property
    +    def next(self):
    +        return self.next_element
    +
    +    @property
    +    def previous(self):
    +        return self.previous_element
    +
    +    #These methods do the real heavy lifting.
    +
    +    def _find_one(self, method, name, attrs, text, **kwargs):
    +        r = None
    +        l = method(name, attrs, text, 1, **kwargs)
    +        if l:
    +            r = l[0]
    +        return r
    +
    +    def _find_all(self, name, attrs, text, limit, generator, **kwargs):
    +        "Iterates over a generator looking for things that match."
    +
    +        if isinstance(name, SoupStrainer):
    +            strainer = name
    +        elif text is None and not limit and not attrs and not kwargs:
    +            # Optimization to find all tags.
    +            if name is True or name is None:
    +                return [element for element in generator
    +                        if isinstance(element, Tag)]
    +            # Optimization to find all tags with a given name.
    +            elif isinstance(name, basestring):
    +                return [element for element in generator
    +                        if isinstance(element, Tag) and element.name == name]
    +            else:
    +                strainer = SoupStrainer(name, attrs, text, **kwargs)
    +        else:
    +            # Build a SoupStrainer
    +            strainer = SoupStrainer(name, attrs, text, **kwargs)
    +        results = ResultSet(strainer)
    +        while True:
    +            try:
    +                i = next(generator)
    +            except StopIteration:
    +                break
    +            if i:
    +                found = strainer.search(i)
    +                if found:
    +                    results.append(found)
    +                    if limit and len(results) >= limit:
    +                        break
    +        return results
    +
    +    #These generators can be used to navigate starting from both
    +    #NavigableStrings and Tags.
    +    @property
    +    def next_elements(self):
    +        i = self.next_element
    +        while i is not None:
    +            yield i
    +            i = i.next_element
    +
    +    @property
    +    def next_siblings(self):
    +        i = self.next_sibling
    +        while i is not None:
    +            yield i
    +            i = i.next_sibling
    +
    +    @property
    +    def previous_elements(self):
    +        i = self.previous_element
    +        while i is not None:
    +            yield i
    +            i = i.previous_element
    +
    +    @property
    +    def previous_siblings(self):
    +        i = self.previous_sibling
    +        while i is not None:
    +            yield i
    +            i = i.previous_sibling
    +
    +    @property
    +    def parents(self):
    +        i = self.parent
    +        while i is not None:
    +            yield i
    +            i = i.parent
    +
    +    # Methods for supporting CSS selectors.
    +
    +    tag_name_re = re.compile('^[a-z0-9]+$')
    +
    +    # /^(\w+)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/
    +    #   \---/  \---/\-------------/    \-------/
    +    #     |      |         |               |
    +    #     |      |         |           The value
    +    #     |      |    ~,|,^,$,* or =
    +    #     |   Attribute
    +    #    Tag
    +    attribselect_re = re.compile(
    +        r'^(?P<tag>\w+)?\[(?P<attribute>\w+)(?P<operator>[=~\|\^\$\*]?)' +
    +        r'=?"?(?P<value>[^\]"]*)"?\]$'
    +        )
    +
    +    def _attr_value_as_string(self, value, default=None):
    +        """Force an attribute value into a string representation.
    +
    +        A multi-valued attribute will be converted into a
    +        space-separated stirng.
    +        """
    +        value = self.get(value, default)
    +        if isinstance(value, list) or isinstance(value, tuple):
    +            value =" ".join(value)
    +        return value
    +
    +    def _attribute_checker(self, operator, attribute, value=''):
    +        """Create a function that performs a CSS selector operation.
    +
    +        Takes an operator, attribute and optional value. Returns a
    +        function that will return True for elements that match that
    +        combination.
    +        """
    +        if operator == '=':
    +            # string representation of `attribute` is equal to `value`
    +            return lambda el: el._attr_value_as_string(attribute) == value
    +        elif operator == '~':
    +            # space-separated list representation of `attribute`
    +            # contains `value`
    +            def _includes_value(element):
    +                attribute_value = element.get(attribute, [])
    +                if not isinstance(attribute_value, list):
    +                    attribute_value = attribute_value.split()
    +                return value in attribute_value
    +            return _includes_value
    +        elif operator == '^':
    +            # string representation of `attribute` starts with `value`
    +            return lambda el: el._attr_value_as_string(
    +                attribute, '').startswith(value)
    +        elif operator == '$':
    +            # string represenation of `attribute` ends with `value`
    +            return lambda el: el._attr_value_as_string(
    +                attribute, '').endswith(value)
    +        elif operator == '*':
    +            # string representation of `attribute` contains `value`
    +            return lambda el: value in el._attr_value_as_string(attribute, '')
    +        elif operator == '|':
    +            # string representation of `attribute` is either exactly
    +            # `value` or starts with `value` and then a dash.
    +            def _is_or_starts_with_dash(element):
    +                attribute_value = element._attr_value_as_string(attribute, '')
    +                return (attribute_value == value or attribute_value.startswith(
    +                        value + '-'))
    +            return _is_or_starts_with_dash
    +        else:
    +            return lambda el: el.has_attr(attribute)
    +
    +    def select(self, selector):
    +        """Perform a CSS selection operation on the current element."""
    +        tokens = selector.split()
    +        current_context = [self]
    +        for index, token in enumerate(tokens):
    +            if tokens[index - 1] == '>':
    +                # already found direct descendants in last step. skip this
    +                # step.
    +                continue
    +            m = self.attribselect_re.match(token)
    +            if m is not None:
    +                # Attribute selector
    +                tag, attribute, operator, value = m.groups()
    +                if not tag:
    +                    tag = True
    +                checker = self._attribute_checker(operator, attribute, value)
    +                found = []
    +                for context in current_context:
    +                    found.extend(
    +                        [el for el in context.find_all(tag) if checker(el)])
    +                current_context = found
    +                continue
    +
    +            if '#' in token:
    +                # ID selector
    +                tag, id = token.split('#', 1)
    +                if tag == "":
    +                    tag = True
    +                el = current_context[0].find(tag, {'id': id})
    +                if el is None:
    +                    return [] # No match
    +                current_context = [el]
    +                continue
    +
    +            if '.' in token:
    +                # Class selector
    +                tag_name, klass = token.split('.', 1)
    +                if not tag_name:
    +                    tag_name = True
    +                classes = set(klass.split('.'))
    +                found = []
    +                def classes_match(tag):
    +                    if tag_name is not True and tag.name != tag_name:
    +                        return False
    +                    if not tag.has_attr('class'):
    +                        return False
    +                    return classes.issubset(tag['class'])
    +                for context in current_context:
    +                    found.extend(context.find_all(classes_match))
    +                current_context = found
    +                continue
    +
    +            if token == '*':
    +                # Star selector
    +                found = []
    +                for context in current_context:
    +                    found.extend(context.findAll(True))
    +                current_context = found
    +                continue
    +
    +            if token == '>':
    +                # Child selector
    +                tag = tokens[index + 1]
    +                if not tag:
    +                    tag = True
    +
    +                found = []
    +                for context in current_context:
    +                    found.extend(context.find_all(tag, recursive=False))
    +                current_context = found
    +                continue
    +
    +            # Here we should just have a regular tag
    +            if not self.tag_name_re.match(token):
    +                return []
    +            found = []
    +            for context in current_context:
    +                found.extend(context.findAll(token))
    +            current_context = found
    +        return current_context
    +
    +    # Old non-property versions of the generators, for backwards
    +    # compatibility with BS3.
    +    def nextGenerator(self):
    +        return self.next_elements
    +
    +    def nextSiblingGenerator(self):
    +        return self.next_siblings
    +
    +    def previousGenerator(self):
    +        return self.previous_elements
    +
    +    def previousSiblingGenerator(self):
    +        return self.previous_siblings
    +
    +    def parentGenerator(self):
    +        return self.parents
    +
    +
    +class NavigableString(unicode, PageElement):
    +
    +    PREFIX = ''
    +    SUFFIX = ''
    +
    +    def __new__(cls, value):
    +        """Create a new NavigableString.
    +
    +        When unpickling a NavigableString, this method is called with
    +        the string in DEFAULT_OUTPUT_ENCODING. That encoding needs to be
    +        passed in to the superclass's __new__ or the superclass won't know
    +        how to handle non-ASCII characters.
    +        """
    +        if isinstance(value, unicode):
    +            return unicode.__new__(cls, value)
    +        return unicode.__new__(cls, value, DEFAULT_OUTPUT_ENCODING)
    +
    +    def __getnewargs__(self):
    +        return (unicode(self),)
    +
    +    def __getattr__(self, attr):
    +        """text.string gives you text. This is for backwards
    +        compatibility for Navigable*String, but for CData* it lets you
    +        get the string without the CData wrapper."""
    +        if attr == 'string':
    +            return self
    +        else:
    +            raise AttributeError(
    +                "'%s' object has no attribute '%s'" % (
    +                    self.__class__.__name__, attr))
    +
    +    def output_ready(self, formatter="minimal"):
    +        output = self.format_string(self, formatter)
    +        return self.PREFIX + output + self.SUFFIX
    +
    +
    +class PreformattedString(NavigableString):
    +    """A NavigableString not subject to the normal formatting rules.
    +
    +    The string will be passed into the formatter (to trigger side effects),
    +    but the return value will be ignored.
    +    """
    +
    +    def output_ready(self, formatter="minimal"):
    +        """CData strings are passed into the formatter.
    +        But the return value is ignored."""
    +        self.format_string(self, formatter)
    +        return self.PREFIX + self + self.SUFFIX
    +
    +class CData(PreformattedString):
    +
    +    PREFIX = u'<![CDATA['
    +    SUFFIX = u']]>'
    +
    +class ProcessingInstruction(PreformattedString):
    +
    +    PREFIX = u'<?'
    +    SUFFIX = u'?>'
    +
    +class Comment(PreformattedString):
    +
    +    PREFIX = u'<!--'
    +    SUFFIX = u'-->'
    +
    +
    +class Declaration(PreformattedString):
    +    PREFIX = u'<!'
    +    SUFFIX = u'!>'
    +
    +
    +class Doctype(PreformattedString):
    +
    +    @classmethod
    +    def for_name_and_ids(cls, name, pub_id, system_id):
    +        value = name
    +        if pub_id is not None:
    +            value += ' PUBLIC "%s"' % pub_id
    +            if system_id is not None:
    +                value += ' "%s"' % system_id
    +        elif system_id is not None:
    +            value += ' SYSTEM "%s"' % system_id
    +
    +        return Doctype(value)
    +
    +    PREFIX = u'<!DOCTYPE '
    +    SUFFIX = u'>\n'
    +
    +
    +class Tag(PageElement):
    +
    +    """Represents a found HTML tag with its attributes and contents."""
    +
    +    def __init__(self, parser=None, builder=None, name=None, namespace=None,
    +                 prefix=None, attrs=None, parent=None, previous=None):
    +        "Basic constructor."
    +
    +        if parser is None:
    +            self.parser_class = None
    +        else:
    +            # We don't actually store the parser object: that lets extracted
    +            # chunks be garbage-collected.
    +            self.parser_class = parser.__class__
    +        if name is None:
    +            raise ValueError("No value provided for new tag's name.")
    +        self.name = name
    +        self.namespace = namespace
    +        self.prefix = prefix
    +        if attrs is None:
    +            attrs = {}
    +        elif builder.cdata_list_attributes:
    +            attrs = builder._replace_cdata_list_attribute_values(
    +                self.name, attrs)
    +        else:
    +            attrs = dict(attrs)
    +        self.attrs = attrs
    +        self.contents = []
    +        self.setup(parent, previous)
    +        self.hidden = False
    +
    +        # Set up any substitutions, such as the charset in a META tag.
    +        if builder is not None:
    +            builder.set_up_substitutions(self)
    +            self.can_be_empty_element = builder.can_be_empty_element(name)
    +        else:
    +            self.can_be_empty_element = False
    +
    +    parserClass = _alias("parser_class")  # BS3
    +
    +    @property
    +    def is_empty_element(self):
    +        """Is this tag an empty-element tag? (aka a self-closing tag)
    +
    +        A tag that has contents is never an empty-element tag.
    +
    +        A tag that has no contents may or may not be an empty-element
    +        tag. It depends on the builder used to create the tag. If the
    +        builder has a designated list of empty-element tags, then only
    +        a tag whose name shows up in that list is considered an
    +        empty-element tag.
    +
    +        If the builder has no designated list of empty-element tags,
    +        then any tag with no contents is an empty-element tag.
    +        """
    +        return len(self.contents) == 0 and self.can_be_empty_element
    +    isSelfClosing = is_empty_element  # BS3
    +
    +    @property
    +    def string(self):
    +        """Convenience property to get the single string within this tag.
    +
    +        :Return: If this tag has a single string child, return value
    +         is that string. If this tag has no children, or more than one
    +         child, return value is None. If this tag has one child tag,
    +         return value is the 'string' attribute of the child tag,
    +         recursively.
    +        """
    +        if len(self.contents) != 1:
    +            return None
    +        child = self.contents[0]
    +        if isinstance(child, NavigableString):
    +            return child
    +        return child.string
    +
    +    @string.setter
    +    def string(self, string):
    +        self.clear()
    +        self.append(string.__class__(string))
    +
    +    def _all_strings(self, strip=False):
    +        """Yield all child strings, possibly stripping them."""
    +        for descendant in self.descendants:
    +            if not isinstance(descendant, NavigableString):
    +                continue
    +            if strip:
    +                descendant = descendant.strip()
    +                if len(descendant) == 0:
    +                    continue
    +            yield descendant
    +    strings = property(_all_strings)
    +
    +    @property
    +    def stripped_strings(self):
    +        for string in self._all_strings(True):
    +            yield string
    +
    +    def get_text(self, separator="", strip=False):
    +        """
    +        Get all child strings, concatenated using the given separator.
    +        """
    +        return separator.join([s for s in self._all_strings(strip)])
    +    getText = get_text
    +    text = property(get_text)
    +
    +    def decompose(self):
    +        """Recursively destroys the contents of this tree."""
    +        self.extract()
    +        i = self
    +        while i is not None:
    +            next = i.next_element
    +            i.__dict__.clear()
    +            i = next
    +
    +    def clear(self, decompose=False):
    +        """
    +        Extract all children. If decompose is True, decompose instead.
    +        """
    +        if decompose:
    +            for element in self.contents[:]:
    +                if isinstance(element, Tag):
    +                    element.decompose()
    +                else:
    +                    element.extract()
    +        else:
    +            for element in self.contents[:]:
    +                element.extract()
    +
    +    def index(self, element):
    +        """
    +        Find the index of a child by identity, not value. Avoids issues with
    +        tag.contents.index(element) getting the index of equal elements.
    +        """
    +        for i, child in enumerate(self.contents):
    +            if child is element:
    +                return i
    +        raise ValueError("Tag.index: element not in tag")
    +
    +    def get(self, key, default=None):
    +        """Returns the value of the 'key' attribute for the tag, or
    +        the value given for 'default' if it doesn't have that
    +        attribute."""
    +        return self.attrs.get(key, default)
    +
    +    def has_attr(self, key):
    +        return key in self.attrs
    +
    +    def __hash__(self):
    +        return str(self).__hash__()
    +
    +    def __getitem__(self, key):
    +        """tag[key] returns the value of the 'key' attribute for the tag,
    +        and throws an exception if it's not there."""
    +        return self.attrs[key]
    +
    +    def __iter__(self):
    +        "Iterating over a tag iterates over its contents."
    +        return iter(self.contents)
    +
    +    def __len__(self):
    +        "The length of a tag is the length of its list of contents."
    +        return len(self.contents)
    +
    +    def __contains__(self, x):
    +        return x in self.contents
    +
    +    def __nonzero__(self):
    +        "A tag is non-None even if it has no contents."
    +        return True
    +
    +    def __setitem__(self, key, value):
    +        """Setting tag[key] sets the value of the 'key' attribute for the
    +        tag."""
    +        self.attrs[key] = value
    +
    +    def __delitem__(self, key):
    +        "Deleting tag[key] deletes all 'key' attributes for the tag."
    +        self.attrs.pop(key, None)
    +
    +    def __call__(self, *args, **kwargs):
    +        """Calling a tag like a function is the same as calling its
    +        find_all() method. Eg. tag('a') returns a list of all the A tags
    +        found within this tag."""
    +        return self.find_all(*args, **kwargs)
    +
    +    def __getattr__(self, tag):
    +        #print "Getattr %s.%s" % (self.__class__, tag)
    +        if len(tag) > 3 and tag.endswith('Tag'):
    +            # BS3: soup.aTag -> "soup.find("a")
    +            tag_name = tag[:-3]
    +            warnings.warn(
    +                '.%sTag is deprecated, use .find("%s") instead.' % (
    +                    tag_name, tag_name))
    +            return self.find(tag_name)
    +        # We special case contents to avoid recursion.
    +        elif not tag.startswith("__") and not tag=="contents":
    +            return self.find(tag)
    +        raise AttributeError(
    +            "'%s' object has no attribute '%s'" % (self.__class__, tag))
    +
    +    def __eq__(self, other):
    +        """Returns true iff this tag has the same name, the same attributes,
    +        and the same contents (recursively) as the given tag."""
    +        if self is other:
    +            return True
    +        if (not hasattr(other, 'name') or
    +            not hasattr(other, 'attrs') or
    +            not hasattr(other, 'contents') or
    +            self.name != other.name or
    +            self.attrs != other.attrs or
    +            len(self) != len(other)):
    +            return False
    +        for i, my_child in enumerate(self.contents):
    +            if my_child != other.contents[i]:
    +                return False
    +        return True
    +
    +    def __ne__(self, other):
    +        """Returns true iff this tag is not identical to the other tag,
    +        as defined in __eq__."""
    +        return not self == other
    +
    +    def __repr__(self, encoding=DEFAULT_OUTPUT_ENCODING):
    +        """Renders this tag as a string."""
    +        return self.encode(encoding)
    +
    +    def __unicode__(self):
    +        return self.decode()
    +
    +    def __str__(self):
    +        return self.encode()
    +
    +    if PY3K:
    +        __str__ = __repr__ = __unicode__
    +
    +    def encode(self, encoding=DEFAULT_OUTPUT_ENCODING,
    +               indent_level=None, formatter="minimal",
    +               errors="xmlcharrefreplace"):
    +        # Turn the data structure into Unicode, then encode the
    +        # Unicode.
    +        u = self.decode(indent_level, encoding, formatter)
    +        return u.encode(encoding, errors)
    +
    +    def decode(self, indent_level=None,
    +               eventual_encoding=DEFAULT_OUTPUT_ENCODING,
    +               formatter="minimal"):
    +        """Returns a Unicode representation of this tag and its contents.
    +
    +        :param eventual_encoding: The tag is destined to be
    +           encoded into this encoding. This method is _not_
    +           responsible for performing that encoding. This information
    +           is passed in so that it can be substituted in if the
    +           document contains a <META> tag that mentions the document's
    +           encoding.
    +        """
    +        attrs = []
    +        if self.attrs:
    +            for key, val in sorted(self.attrs.items()):
    +                if val is None:
    +                    decoded = key
    +                else:
    +                    if isinstance(val, list) or isinstance(val, tuple):
    +                        val = ' '.join(val)
    +                    elif not isinstance(val, basestring):
    +                        val = str(val)
    +                    elif (
    +                        isinstance(val, AttributeValueWithCharsetSubstitution)
    +                        and eventual_encoding is not None):
    +                        val = val.encode(eventual_encoding)
    +
    +                    text = self.format_string(val, formatter)
    +                    decoded = (
    +                        str(key) + '='
    +                        + EntitySubstitution.quoted_attribute_value(text))
    +                attrs.append(decoded)
    +        close = ''
    +        closeTag = ''
    +        if self.is_empty_element:
    +            close = '/'
    +        else:
    +            closeTag = '</%s>' % self.name
    +
    +        prefix = ''
    +        if self.prefix:
    +            prefix = self.prefix + ":"
    +
    +        pretty_print = (indent_level is not None)
    +        if pretty_print:
    +            space = (' ' * (indent_level - 1))
    +            indent_contents = indent_level + 1
    +        else:
    +            space = ''
    +            indent_contents = None
    +        contents = self.decode_contents(
    +            indent_contents, eventual_encoding, formatter)
    +
    +        if self.hidden:
    +            # This is the 'document root' object.
    +            s = contents
    +        else:
    +            s = []
    +            attribute_string = ''
    +            if attrs:
    +                attribute_string = ' ' + ' '.join(attrs)
    +            if pretty_print:
    +                s.append(space)
    +            s.append('<%s%s%s%s>' % (
    +                    prefix, self.name, attribute_string, close))
    +            if pretty_print:
    +                s.append("\n")
    +            s.append(contents)
    +            if pretty_print and contents and contents[-1] != "\n":
    +                s.append("\n")
    +            if pretty_print and closeTag:
    +                s.append(space)
    +            s.append(closeTag)
    +            if pretty_print and closeTag and self.next_sibling:
    +                s.append("\n")
    +            s = ''.join(s)
    +        return s
    +
    +    def prettify(self, encoding=None, formatter="minimal"):
    +        if encoding is None:
    +            return self.decode(True, formatter=formatter)
    +        else:
    +            return self.encode(encoding, True, formatter=formatter)
    +
    +    def decode_contents(self, indent_level=None,
    +                       eventual_encoding=DEFAULT_OUTPUT_ENCODING,
    +                       formatter="minimal"):
    +        """Renders the contents of this tag as a Unicode string.
    +
    +        :param eventual_encoding: The tag is destined to be
    +           encoded into this encoding. This method is _not_
    +           responsible for performing that encoding. This information
    +           is passed in so that it can be substituted in if the
    +           document contains a <META> tag that mentions the document's
    +           encoding.
    +        """
    +        pretty_print = (indent_level is not None)
    +        s = []
    +        for c in self:
    +            text = None
    +            if isinstance(c, NavigableString):
    +                text = c.output_ready(formatter)
    +            elif isinstance(c, Tag):
    +                s.append(c.decode(indent_level, eventual_encoding,
    +                                  formatter))
    +            if text and indent_level:
    +                text = text.strip()
    +            if text:
    +                if pretty_print:
    +                    s.append(" " * (indent_level - 1))
    +                s.append(text)
    +                if pretty_print:
    +                    s.append("\n")
    +        return ''.join(s)
    +
    +    def encode_contents(
    +        self, indent_level=None, encoding=DEFAULT_OUTPUT_ENCODING,
    +        formatter="minimal"):
    +        """Renders the contents of this tag as a bytestring."""
    +        contents = self.decode_contents(indent_level, encoding, formatter)
    +        return contents.encode(encoding)
    +
    +    # Old method for BS3 compatibility
    +    def renderContents(self, encoding=DEFAULT_OUTPUT_ENCODING,
    +                       prettyPrint=False, indentLevel=0):
    +        if not prettyPrint:
    +            indentLevel = None
    +        return self.encode_contents(
    +            indent_level=indentLevel, encoding=encoding)
    +
    +    #Soup methods
    +
    +    def find(self, name=None, attrs={}, recursive=True, text=None,
    +             **kwargs):
    +        """Return only the first child of this Tag matching the given
    +        criteria."""
    +        r = None
    +        l = self.find_all(name, attrs, recursive, text, 1, **kwargs)
    +        if l:
    +            r = l[0]
    +        return r
    +    findChild = find
    +
    +    def find_all(self, name=None, attrs={}, recursive=True, text=None,
    +                 limit=None, **kwargs):
    +        """Extracts a list of Tag objects that match the given
    +        criteria.  You can specify the name of the Tag and any
    +        attributes you want the Tag to have.
    +
    +        The value of a key-value pair in the 'attrs' map can be a
    +        string, a list of strings, a regular expression object, or a
    +        callable that takes a string and returns whether or not the
    +        string matches for some custom definition of 'matches'. The
    +        same is true of the tag name."""
    +        generator = self.descendants
    +        if not recursive:
    +            generator = self.children
    +        return self._find_all(name, attrs, text, limit, generator, **kwargs)
    +    findAll = find_all       # BS3
    +    findChildren = find_all  # BS2
    +
    +    #Generator methods
    +    @property
    +    def children(self):
    +        # return iter() to make the purpose of the method clear
    +        return iter(self.contents)  # XXX This seems to be untested.
    +
    +    @property
    +    def descendants(self):
    +        if not len(self.contents):
    +            return
    +        stopNode = self._last_descendant().next_element
    +        current = self.contents[0]
    +        while current is not stopNode:
    +            yield current
    +            current = current.next_element
    +
    +    # Old names for backwards compatibility
    +    def childGenerator(self):
    +        return self.children
    +
    +    def recursiveChildGenerator(self):
    +        return self.descendants
    +
    +    # This was kind of misleading because has_key() (attributes) was
    +    # different from __in__ (contents). has_key() is gone in Python 3,
    +    # anyway.
    +    has_key = has_attr
    +
    +# Next, a couple classes to represent queries and their results.
    +class SoupStrainer(object):
    +    """Encapsulates a number of ways of matching a markup element (tag or
    +    text)."""
    +
    +    def __init__(self, name=None, attrs={}, text=None, **kwargs):
    +        self.name = self._normalize_search_value(name)
    +        if not isinstance(attrs, dict):
    +            # Treat a non-dict value for attrs as a search for the 'class'
    +            # attribute.
    +            kwargs['class'] = attrs
    +            attrs = None
    +
    +        if kwargs:
    +            if attrs:
    +                attrs = attrs.copy()
    +                attrs.update(kwargs)
    +            else:
    +                attrs = kwargs
    +        normalized_attrs = {}
    +        for key, value in attrs.items():
    +            normalized_attrs[key] = self._normalize_search_value(value)
    +
    +        self.attrs = normalized_attrs
    +        self.text = self._normalize_search_value(text)
    +
    +    def _normalize_search_value(self, value):
    +        # Leave it alone if it's a Unicode string, a callable, a
    +        # regular expression, a boolean, or None.
    +        if (isinstance(value, unicode) or callable(value) or hasattr(value, 'match')
    +            or isinstance(value, bool) or value is None):
    +            return value
    +
    +        # If it's a bytestring, convert it to Unicode, treating it as UTF-8.
    +        if isinstance(value, bytes):
    +            return value.decode("utf8")
    +
    +        # If it's listlike, convert it into a list of strings.
    +        if hasattr(value, '__iter__'):
    +            new_value = []
    +            for v in value:
    +                if (hasattr(v, '__iter__') and not isinstance(v, bytes)
    +                    and not isinstance(v, unicode)):
    +                    # This is almost certainly the user's mistake. In the
    +                    # interests of avoiding infinite loops, we'll let
    +                    # it through as-is rather than doing a recursive call.
    +                    new_value.append(v)
    +                else:
    +                    new_value.append(self._normalize_search_value(v))
    +            return new_value
    +
    +        # Otherwise, convert it into a Unicode string.
    +        # The unicode(str()) thing is so this will do the same thing on Python 2
    +        # and Python 3.
    +        return unicode(str(value))
    +
    +    def __str__(self):
    +        if self.text:
    +            return self.text
    +        else:
    +            return "%s|%s" % (self.name, self.attrs)
    +
    +    def search_tag(self, markup_name=None, markup_attrs={}):
    +        found = None
    +        markup = None
    +        if isinstance(markup_name, Tag):
    +            markup = markup_name
    +            markup_attrs = markup
    +        call_function_with_tag_data = (
    +            isinstance(self.name, collections.Callable)
    +            and not isinstance(markup_name, Tag))
    +
    +        if ((not self.name)
    +            or call_function_with_tag_data
    +            or (markup and self._matches(markup, self.name))
    +            or (not markup and self._matches(markup_name, self.name))):
    +            if call_function_with_tag_data:
    +                match = self.name(markup_name, markup_attrs)
    +            else:
    +                match = True
    +                markup_attr_map = None
    +                for attr, match_against in list(self.attrs.items()):
    +                    if not markup_attr_map:
    +                        if hasattr(markup_attrs, 'get'):
    +                            markup_attr_map = markup_attrs
    +                        else:
    +                            markup_attr_map = {}
    +                            for k, v in markup_attrs:
    +                                markup_attr_map[k] = v
    +                    attr_value = markup_attr_map.get(attr)
    +                    if not self._matches(attr_value, match_against):
    +                        match = False
    +                        break
    +            if match:
    +                if markup:
    +                    found = markup
    +                else:
    +                    found = markup_name
    +        if found and self.text and not self._matches(found.string, self.text):
    +            found = None
    +        return found
    +    searchTag = search_tag
    +
    +    def search(self, markup):
    +        # print 'looking for %s in %s' % (self, markup)
    +        found = None
    +        # If given a list of items, scan it for a text element that
    +        # matches.
    +        if hasattr(markup, '__iter__') and not isinstance(markup, (Tag, basestring)):
    +            for element in markup:
    +                if isinstance(element, NavigableString) \
    +                       and self.search(element):
    +                    found = element
    +                    break
    +        # If it's a Tag, make sure its name or attributes match.
    +        # Don't bother with Tags if we're searching for text.
    +        elif isinstance(markup, Tag):
    +            if not self.text or self.name or self.attrs:
    +                found = self.search_tag(markup)
    +        # If it's text, make sure the text matches.
    +        elif isinstance(markup, NavigableString) or \
    +                 isinstance(markup, basestring):
    +            if not self.name and not self.attrs and self._matches(markup, self.text):
    +                found = markup
    +        else:
    +            raise Exception(
    +                "I don't know how to match against a %s" % markup.__class__)
    +        return found
    +
    +    def _matches(self, markup, match_against):
    +        # print u"Matching %s against %s" % (markup, match_against)
    +        result = False
    +        if isinstance(markup, list) or isinstance(markup, tuple):
    +            # This should only happen when searching a multi-valued attribute
    +            # like 'class'.
    +            if (isinstance(match_against, unicode)
    +                and ' ' in match_against):
    +                # A bit of a special case. If they try to match "foo
    +                # bar" on a multivalue attribute's value, only accept
    +                # the literal value "foo bar"
    +                #
    +                # XXX This is going to be pretty slow because we keep
    +                # splitting match_against. But it shouldn't come up
    +                # too often.
    +                return (whitespace_re.split(match_against) == markup)
    +            else:
    +                for item in markup:
    +                    if self._matches(item, match_against):
    +                        return True
    +                return False
    +
    +        if match_against is True:
    +            # True matches any non-None value.
    +            return markup is not None
    +
    +        if isinstance(match_against, collections.Callable):
    +            return match_against(markup)
    +
    +        # Custom callables take the tag as an argument, but all
    +        # other ways of matching match the tag name as a string.
    +        if isinstance(markup, Tag):
    +            markup = markup.name
    +
    +        # Ensure that `markup` is either a Unicode string, or None.
    +        markup = self._normalize_search_value(markup)
    +
    +        if markup is None:
    +            # None matches None, False, an empty string, an empty list, and so on.
    +            return not match_against
    +
    +        if isinstance(match_against, unicode):
    +            # Exact string match
    +            return markup == match_against
    +
    +        if hasattr(match_against, 'match'):
    +            # Regexp match
    +            return match_against.search(markup)
    +
    +        if hasattr(match_against, '__iter__'):
    +            # The markup must be an exact match against something
    +            # in the iterable.
    +            return markup in match_against
    +
    +
    +class ResultSet(list):
    +    """A ResultSet is just a list that keeps track of the SoupStrainer
    +    that created it."""
    +    def __init__(self, source):
    +        list.__init__([])
    +        self.source = source
    diff --git a/libs/bs4/testing.py b/libs/bs4/testing.py
    new file mode 100644
    index 00000000..5a84b0ba
    --- /dev/null
    +++ b/libs/bs4/testing.py
    @@ -0,0 +1,515 @@
    +"""Helper classes for tests."""
    +
    +import copy
    +import functools
    +import unittest
    +from unittest import TestCase
    +from bs4 import BeautifulSoup
    +from bs4.element import (
    +    CharsetMetaAttributeValue,
    +    Comment,
    +    ContentMetaAttributeValue,
    +    Doctype,
    +    SoupStrainer,
    +)
    +
    +from bs4.builder import HTMLParserTreeBuilder
    +default_builder = HTMLParserTreeBuilder
    +
    +
    +class SoupTest(unittest.TestCase):
    +
    +    @property
    +    def default_builder(self):
    +        return default_builder()
    +
    +    def soup(self, markup, **kwargs):
    +        """Build a Beautiful Soup object from markup."""
    +        builder = kwargs.pop('builder', self.default_builder)
    +        return BeautifulSoup(markup, builder=builder, **kwargs)
    +
    +    def document_for(self, markup):
    +        """Turn an HTML fragment into a document.
    +
    +        The details depend on the builder.
    +        """
    +        return self.default_builder.test_fragment_to_document(markup)
    +
    +    def assertSoupEquals(self, to_parse, compare_parsed_to=None):
    +        builder = self.default_builder
    +        obj = BeautifulSoup(to_parse, builder=builder)
    +        if compare_parsed_to is None:
    +            compare_parsed_to = to_parse
    +
    +        self.assertEqual(obj.decode(), self.document_for(compare_parsed_to))
    +
    +
    +class HTMLTreeBuilderSmokeTest(object):
    +
    +    """A basic test of a treebuilder's competence.
    +
    +    Any HTML treebuilder, present or future, should be able to pass
    +    these tests. With invalid markup, there's room for interpretation,
    +    and different parsers can handle it differently. But with the
    +    markup in these tests, there's not much room for interpretation.
    +    """
    +
    +    def assertDoctypeHandled(self, doctype_fragment):
    +        """Assert that a given doctype string is handled correctly."""
    +        doctype_str, soup = self._document_with_doctype(doctype_fragment)
    +
    +        # Make sure a Doctype object was created.
    +        doctype = soup.contents[0]
    +        self.assertEqual(doctype.__class__, Doctype)
    +        self.assertEqual(doctype, doctype_fragment)
    +        self.assertEqual(str(soup)[:len(doctype_str)], doctype_str)
    +
    +        # Make sure that the doctype was correctly associated with the
    +        # parse tree and that the rest of the document parsed.
    +        self.assertEqual(soup.p.contents[0], 'foo')
    +
    +    def _document_with_doctype(self, doctype_fragment):
    +        """Generate and parse a document with the given doctype."""
    +        doctype = '<!DOCTYPE %s>' % doctype_fragment
    +        markup = doctype + '\n<p>foo</p>'
    +        soup = self.soup(markup)
    +        return doctype, soup
    +
    +    def test_normal_doctypes(self):
    +        """Make sure normal, everyday HTML doctypes are handled correctly."""
    +        self.assertDoctypeHandled("html")
    +        self.assertDoctypeHandled(
    +            'html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"')
    +
    +    def test_public_doctype_with_url(self):
    +        doctype = 'html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"'
    +        self.assertDoctypeHandled(doctype)
    +
    +    def test_system_doctype(self):
    +        self.assertDoctypeHandled('foo SYSTEM "http://www.example.com/"')
    +
    +    def test_namespaced_system_doctype(self):
    +        # We can handle a namespaced doctype with a system ID.
    +        self.assertDoctypeHandled('xsl:stylesheet SYSTEM "htmlent.dtd"')
    +
    +    def test_namespaced_public_doctype(self):
    +        # Test a namespaced doctype with a public id.
    +        self.assertDoctypeHandled('xsl:stylesheet PUBLIC "htmlent.dtd"')
    +
    +    def test_real_xhtml_document(self):
    +        """A real XHTML document should come out more or less the same as it went in."""
    +        markup = b"""<?xml version="1.0" encoding="utf-8"?>
    +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN">
    +<html xmlns="http://www.w3.org/1999/xhtml">
    +<head><title>Hello.
    +Goodbye.
    +"""
    +        soup = self.soup(markup)
    +        self.assertEqual(
    +            soup.encode("utf-8").replace(b"\n", b""),
    +            markup.replace(b"\n", b""))
    +
    +    def test_deepcopy(self):
    +        """Make sure you can copy the tree builder.
    +
    +        This is important because the builder is part of a
    +        BeautifulSoup object, and we want to be able to copy that.
    +        """
    +        copy.deepcopy(self.default_builder)
    +
    +    def test_p_tag_is_never_empty_element(self):
    +        """A 

    tag is never designated as an empty-element tag. + + Even if the markup shows it as an empty-element tag, it + shouldn't be presented that way. + """ + soup = self.soup("

    ") + self.assertFalse(soup.p.is_empty_element) + self.assertEqual(str(soup.p), "

    ") + + def test_unclosed_tags_get_closed(self): + """A tag that's not closed by the end of the document should be closed. + + This applies to all tags except empty-element tags. + """ + self.assertSoupEquals("

    ", "

    ") + self.assertSoupEquals("", "") + + self.assertSoupEquals("
    ", "
    ") + + def test_br_is_always_empty_element_tag(self): + """A
    tag is designated as an empty-element tag. + + Some parsers treat

    as one
    tag, some parsers as + two tags, but it should always be an empty-element tag. + """ + soup = self.soup("

    ") + self.assertTrue(soup.br.is_empty_element) + self.assertEqual(str(soup.br), "
    ") + + def test_nested_formatting_elements(self): + self.assertSoupEquals("") + + def test_comment(self): + # Comments are represented as Comment objects. + markup = "

    foobaz

    " + self.assertSoupEquals(markup) + + soup = self.soup(markup) + comment = soup.find(text="foobar") + self.assertEqual(comment.__class__, Comment) + + def test_preserved_whitespace_in_pre_and_textarea(self): + """Whitespace must be preserved in
     and ")
    +
    +    def test_nested_inline_elements(self):
    +        """Inline elements can be nested indefinitely."""
    +        b_tag = "Inside a B tag"
    +        self.assertSoupEquals(b_tag)
    +
    +        nested_b_tag = "

    A nested tag

    " + self.assertSoupEquals(nested_b_tag) + + double_nested_b_tag = "

    A doubly nested tag

    " + self.assertSoupEquals(nested_b_tag) + + def test_nested_block_level_elements(self): + """Block elements can be nested.""" + soup = self.soup('

    Foo

    ') + blockquote = soup.blockquote + self.assertEqual(blockquote.p.b.string, 'Foo') + self.assertEqual(blockquote.b.string, 'Foo') + + def test_correctly_nested_tables(self): + """One table can go inside another one.""" + markup = ('' + '' + "') + + self.assertSoupEquals( + markup, + '
    Here's another table:" + '' + '' + '
    foo
    Here\'s another table:' + '
    foo
    ' + '
    ') + + self.assertSoupEquals( + "" + "" + "
    Foo
    Bar
    Baz
    ") + + def test_angle_brackets_in_attribute_values_are_escaped(self): + self.assertSoupEquals('', '') + + def test_entities_in_attributes_converted_to_unicode(self): + expect = u'

    ' + self.assertSoupEquals('

    ', expect) + self.assertSoupEquals('

    ', expect) + self.assertSoupEquals('

    ', expect) + + def test_entities_in_text_converted_to_unicode(self): + expect = u'

    pi\N{LATIN SMALL LETTER N WITH TILDE}ata

    ' + self.assertSoupEquals("

    piñata

    ", expect) + self.assertSoupEquals("

    piñata

    ", expect) + self.assertSoupEquals("

    piñata

    ", expect) + + def test_quot_entity_converted_to_quotation_mark(self): + self.assertSoupEquals("

    I said "good day!"

    ", + '

    I said "good day!"

    ') + + def test_out_of_range_entity(self): + expect = u"\N{REPLACEMENT CHARACTER}" + self.assertSoupEquals("�", expect) + self.assertSoupEquals("�", expect) + self.assertSoupEquals("�", expect) + + def test_basic_namespaces(self): + """Parsers don't need to *understand* namespaces, but at the + very least they should not choke on namespaces or lose + data.""" + + markup = b'4' + soup = self.soup(markup) + self.assertEqual(markup, soup.encode()) + html = soup.html + self.assertEqual('http://www.w3.org/1999/xhtml', soup.html['xmlns']) + self.assertEqual( + 'http://www.w3.org/1998/Math/MathML', soup.html['xmlns:mathml']) + self.assertEqual( + 'http://www.w3.org/2000/svg', soup.html['xmlns:svg']) + + def test_multivalued_attribute_value_becomes_list(self): + markup = b'' + soup = self.soup(markup) + self.assertEqual(['foo', 'bar'], soup.a['class']) + + # + # Generally speaking, tests below this point are more tests of + # Beautiful Soup than tests of the tree builders. But parsers are + # weird, so we run these tests separately for every tree builder + # to detect any differences between them. + # + + def test_soupstrainer(self): + """Parsers should be able to work with SoupStrainers.""" + strainer = SoupStrainer("b") + soup = self.soup("A bold statement", + parse_only=strainer) + self.assertEqual(soup.decode(), "bold") + + def test_single_quote_attribute_values_become_double_quotes(self): + self.assertSoupEquals("", + '') + + def test_attribute_values_with_nested_quotes_are_left_alone(self): + text = """a""" + self.assertSoupEquals(text) + + def test_attribute_values_with_double_nested_quotes_get_quoted(self): + text = """a""" + soup = self.soup(text) + soup.foo['attr'] = 'Brawls happen at "Bob\'s Bar"' + self.assertSoupEquals( + soup.foo.decode(), + """a""") + + def test_ampersand_in_attribute_value_gets_escaped(self): + self.assertSoupEquals('', + '') + + self.assertSoupEquals( + 'foo', + 'foo') + + def test_escaped_ampersand_in_attribute_value_is_left_alone(self): + self.assertSoupEquals('') + + def test_entities_in_strings_converted_during_parsing(self): + # Both XML and HTML entities are converted to Unicode characters + # during parsing. + text = "

    <<sacré bleu!>>

    " + expected = u"

    <<sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!>>

    " + self.assertSoupEquals(text, expected) + + def test_smart_quotes_converted_on_the_way_in(self): + # Microsoft smart quotes are converted to Unicode characters during + # parsing. + quote = b"

    \x91Foo\x92

    " + soup = self.soup(quote) + self.assertEqual( + soup.p.string, + u"\N{LEFT SINGLE QUOTATION MARK}Foo\N{RIGHT SINGLE QUOTATION MARK}") + + def test_non_breaking_spaces_converted_on_the_way_in(self): + soup = self.soup("  ") + self.assertEqual(soup.a.string, u"\N{NO-BREAK SPACE}" * 2) + + def test_entities_converted_on_the_way_out(self): + text = "

    <<sacré bleu!>>

    " + expected = u"

    <<sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!>>

    ".encode("utf-8") + soup = self.soup(text) + self.assertEqual(soup.p.encode("utf-8"), expected) + + def test_real_iso_latin_document(self): + # Smoke test of interrelated functionality, using an + # easy-to-understand document. + + # Here it is in Unicode. Note that it claims to be in ISO-Latin-1. + unicode_html = u'

    Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!

    ' + + # That's because we're going to encode it into ISO-Latin-1, and use + # that to test. + iso_latin_html = unicode_html.encode("iso-8859-1") + + # Parse the ISO-Latin-1 HTML. + soup = self.soup(iso_latin_html) + # Encode it to UTF-8. + result = soup.encode("utf-8") + + # What do we expect the result to look like? Well, it would + # look like unicode_html, except that the META tag would say + # UTF-8 instead of ISO-Latin-1. + expected = unicode_html.replace("ISO-Latin-1", "utf-8") + + # And, of course, it would be in UTF-8, not Unicode. + expected = expected.encode("utf-8") + + # Ta-da! + self.assertEqual(result, expected) + + def test_real_shift_jis_document(self): + # Smoke test to make sure the parser can handle a document in + # Shift-JIS encoding, without choking. + shift_jis_html = ( + b'
    '
    +            b'\x82\xb1\x82\xea\x82\xcdShift-JIS\x82\xc5\x83R\x81[\x83f'
    +            b'\x83B\x83\x93\x83O\x82\xb3\x82\xea\x82\xbd\x93\xfa\x96{\x8c'
    +            b'\xea\x82\xcc\x83t\x83@\x83C\x83\x8b\x82\xc5\x82\xb7\x81B'
    +            b'
    ') + unicode_html = shift_jis_html.decode("shift-jis") + soup = self.soup(unicode_html) + + # Make sure the parse tree is correctly encoded to various + # encodings. + self.assertEqual(soup.encode("utf-8"), unicode_html.encode("utf-8")) + self.assertEqual(soup.encode("euc_jp"), unicode_html.encode("euc_jp")) + + def test_real_hebrew_document(self): + # A real-world test to make sure we can convert ISO-8859-9 (a + # Hebrew encoding) to UTF-8. + hebrew_document = b'Hebrew (ISO 8859-8) in Visual Directionality

    Hebrew (ISO 8859-8) in Visual Directionality

    \xed\xe5\xec\xf9' + soup = self.soup( + hebrew_document, from_encoding="iso8859-8") + self.assertEqual(soup.original_encoding, 'iso8859-8') + self.assertEqual( + soup.encode('utf-8'), + hebrew_document.decode("iso8859-8").encode("utf-8")) + + def test_meta_tag_reflects_current_encoding(self): + # Here's the tag saying that a document is + # encoded in Shift-JIS. + meta_tag = ('') + + # Here's a document incorporating that meta tag. + shift_jis_html = ( + '\n%s\n' + '' + 'Shift-JIS markup goes here.') % meta_tag + soup = self.soup(shift_jis_html) + + # Parse the document, and the charset is seemingly unaffected. + parsed_meta = soup.find('meta', {'http-equiv': 'Content-type'}) + content = parsed_meta['content'] + self.assertEqual('text/html; charset=x-sjis', content) + + # But that value is actually a ContentMetaAttributeValue object. + self.assertTrue(isinstance(content, ContentMetaAttributeValue)) + + # And it will take on a value that reflects its current + # encoding. + self.assertEqual('text/html; charset=utf8', content.encode("utf8")) + + # For the rest of the story, see TestSubstitutions in + # test_tree.py. + + def test_html5_style_meta_tag_reflects_current_encoding(self): + # Here's the tag saying that a document is + # encoded in Shift-JIS. + meta_tag = ('') + + # Here's a document incorporating that meta tag. + shift_jis_html = ( + '\n%s\n' + '' + 'Shift-JIS markup goes here.') % meta_tag + soup = self.soup(shift_jis_html) + + # Parse the document, and the charset is seemingly unaffected. + parsed_meta = soup.find('meta', id="encoding") + charset = parsed_meta['charset'] + self.assertEqual('x-sjis', charset) + + # But that value is actually a CharsetMetaAttributeValue object. + self.assertTrue(isinstance(charset, CharsetMetaAttributeValue)) + + # And it will take on a value that reflects its current + # encoding. + self.assertEqual('utf8', charset.encode("utf8")) + + def test_tag_with_no_attributes_can_have_attributes_added(self): + data = self.soup("text") + data.a['foo'] = 'bar' + self.assertEqual('text', data.a.decode()) + +class XMLTreeBuilderSmokeTest(object): + + def test_docstring_generated(self): + soup = self.soup("") + self.assertEqual( + soup.encode(), b'\n') + + def test_real_xhtml_document(self): + """A real XHTML document should come out *exactly* the same as it went in.""" + markup = b""" + + +Hello. +Goodbye. +""" + soup = self.soup(markup) + self.assertEqual( + soup.encode("utf-8"), markup) + + + def test_docstring_includes_correct_encoding(self): + soup = self.soup("") + self.assertEqual( + soup.encode("latin1"), + b'\n') + + def test_large_xml_document(self): + """A large XML document should come out the same as it went in.""" + markup = (b'\n' + + b'0' * (2**12) + + b'') + soup = self.soup(markup) + self.assertEqual(soup.encode("utf-8"), markup) + + + def test_tags_are_empty_element_if_and_only_if_they_are_empty(self): + self.assertSoupEquals("

    ", "

    ") + self.assertSoupEquals("

    foo

    ") + + def test_namespaces_are_preserved(self): + markup = 'This tag is in the a namespaceThis tag is in the b namespace' + soup = self.soup(markup) + root = soup.root + self.assertEqual("http://example.com/", root['xmlns:a']) + self.assertEqual("http://example.net/", root['xmlns:b']) + + +class HTML5TreeBuilderSmokeTest(HTMLTreeBuilderSmokeTest): + """Smoke test for a tree builder that supports HTML5.""" + + def test_real_xhtml_document(self): + # Since XHTML is not HTML5, HTML5 parsers are not tested to handle + # XHTML documents in any particular way. + pass + + def test_html_tags_have_namespace(self): + markup = "" + soup = self.soup(markup) + self.assertEqual("http://www.w3.org/1999/xhtml", soup.a.namespace) + + def test_svg_tags_have_namespace(self): + markup = '' + soup = self.soup(markup) + namespace = "http://www.w3.org/2000/svg" + self.assertEqual(namespace, soup.svg.namespace) + self.assertEqual(namespace, soup.circle.namespace) + + + def test_mathml_tags_have_namespace(self): + markup = '5' + soup = self.soup(markup) + namespace = 'http://www.w3.org/1998/Math/MathML' + self.assertEqual(namespace, soup.math.namespace) + self.assertEqual(namespace, soup.msqrt.namespace) + + +def skipIf(condition, reason): + def nothing(test, *args, **kwargs): + return None + + def decorator(test_item): + if condition: + return nothing + else: + return test_item + + return decorator diff --git a/libs/certifi/__init__.py b/libs/certifi/__init__.py new file mode 100644 index 00000000..853c08c2 --- /dev/null +++ b/libs/certifi/__init__.py @@ -0,0 +1 @@ +from .core import where \ No newline at end of file diff --git a/libs/certifi/cacert.pem b/libs/certifi/cacert.pem new file mode 100644 index 00000000..7da84474 --- /dev/null +++ b/libs/certifi/cacert.pem @@ -0,0 +1,3338 @@ +## +## ca-bundle.crt -- Bundle of CA Root Certificates +## +## Certificate data from Mozilla as of: Wed Jan 18 00:04:16 2012 +## +## This is a bundle of X.509 certificates of public Certificate Authorities +## (CA). These were automatically extracted from Mozilla's root certificates +## file (certdata.txt). This file can be found in the mozilla source tree: +## http://mxr.mozilla.org/mozilla/source/security/nss/lib/ckfw/builtins/certdata.txt?raw=1 +## +## It contains the certificates in PEM format and therefore +## can be directly used with curl / libcurl / php_curl, or with +## an Apache+mod_ssl webserver for SSL client authentication. +## Just configure this file as the SSLCACertificateFile. +## + +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is the Netscape security libraries. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1994-2000 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** +# @(#) $RCSfile: certdata.txt,v $ $Revision: 1.81 $ $Date: 2012/01/17 22:02:37 $ + +GTE CyberTrust Global Root +========================== +-----BEGIN CERTIFICATE----- +MIICWjCCAcMCAgGlMA0GCSqGSIb3DQEBBAUAMHUxCzAJBgNVBAYTAlVTMRgwFgYDVQQKEw9HVEUg +Q29ycG9yYXRpb24xJzAlBgNVBAsTHkdURSBDeWJlclRydXN0IFNvbHV0aW9ucywgSW5jLjEjMCEG +A1UEAxMaR1RFIEN5YmVyVHJ1c3QgR2xvYmFsIFJvb3QwHhcNOTgwODEzMDAyOTAwWhcNMTgwODEz +MjM1OTAwWjB1MQswCQYDVQQGEwJVUzEYMBYGA1UEChMPR1RFIENvcnBvcmF0aW9uMScwJQYDVQQL +Ex5HVEUgQ3liZXJUcnVzdCBTb2x1dGlvbnMsIEluYy4xIzAhBgNVBAMTGkdURSBDeWJlclRydXN0 +IEdsb2JhbCBSb290MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVD6C28FCc6HrHiM3dFw4u +sJTQGz0O9pTAipTHBsiQl8i4ZBp6fmw8U+E3KHNgf7KXUwefU/ltWJTSr41tiGeA5u2ylc9yMcql +HHK6XALnZELn+aks1joNrI1CqiQBOeacPwGFVw1Yh0X404Wqk2kmhXBIgD8SFcd5tB8FLztimQID +AQABMA0GCSqGSIb3DQEBBAUAA4GBAG3rGwnpXtlR22ciYaQqPEh346B8pt5zohQDhT37qw4wxYMW +M4ETCJ57NE7fQMh017l93PR2VX2bY1QY6fDq81yx2YtCHrnAlU66+tXifPVoYb+O7AWXX1uw16OF +NMQkpw0PlZPvy5TYnh+dXIVtx6quTx8itc2VrbqnzPmrC3p/ +-----END CERTIFICATE----- + +Thawte Server CA +================ +-----BEGIN CERTIFICATE----- +MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT +DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3dGUgQ29uc3Vs +dGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UE +AxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5j +b20wHhcNOTYwODAxMDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNV +BAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3dGUgQ29u +c3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcG +A1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0 +ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl +/Kj0R1HahbUgdJSGHg91yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg7 +1CcEJRCXL+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGjEzAR +MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG7oWDTSEwjsrZqG9J +GubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6eQNuozDJ0uW8NxuOzRAvZim+aKZuZ +GCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZqdq5snUb9kLy78fyGPmJvKP/iiMucEc= +-----END CERTIFICATE----- + +Thawte Premium Server CA +======================== +-----BEGIN CERTIFICATE----- +MIIDJzCCApCgAwIBAgIBATANBgkqhkiG9w0BAQQFADCBzjELMAkGA1UEBhMCWkExFTATBgNVBAgT +DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3dGUgQ29uc3Vs +dGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UE +AxMYVGhhd3RlIFByZW1pdW0gU2VydmVyIENBMSgwJgYJKoZIhvcNAQkBFhlwcmVtaXVtLXNlcnZl +ckB0aGF3dGUuY29tMB4XDTk2MDgwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgc4xCzAJBgNVBAYT +AlpBMRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEdMBsGA1UEChMU +VGhhd3RlIENvbnN1bHRpbmcgY2MxKDAmBgNVBAsTH0NlcnRpZmljYXRpb24gU2VydmljZXMgRGl2 +aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQcmVtaXVtIFNlcnZlciBDQTEoMCYGCSqGSIb3DQEJARYZ +cHJlbWl1bS1zZXJ2ZXJAdGhhd3RlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0jY2 +aovXwlue2oFBYo847kkEVdbQ7xwblRZH7xhINTpS9CtqBo87L+pW46+GjZ4X9560ZXUCTe/LCaIh +Udib0GfQug2SBhRz1JPLlyoAnFxODLz6FVL88kRu2hFKbgifLy3j+ao6hnO2RlNYyIkFvYMRuHM/ +qgeN9EJN50CdHDcCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQQFAAOBgQAm +SCwWwlj66BZ0DKqqX1Q/8tfJeGBeXm43YyJ3Nn6yF8Q0ufUIhfzJATj/Tb7yFkJD57taRvvBxhEf +8UqwKEbJw8RCfbz6q1lu1bdRiBHjpIUZa4JMpAwSremkrj/xw0llmozFyD4lt5SZu5IycQfwhl7t +UCemDaYj+bvLpgcUQg== +-----END CERTIFICATE----- + +Equifax Secure CA +================= +-----BEGIN CERTIFICATE----- +MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJVUzEQMA4GA1UE +ChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 +MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoT +B0VxdWlmYXgxLTArBgNVBAsTJEVxdWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCB +nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPR +fM6fBeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+AcJkVV5MW +8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kCAwEAAaOCAQkwggEFMHAG +A1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UE +CxMkRXF1aWZheCBTZWN1cmUgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoG +A1UdEAQTMBGBDzIwMTgwODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvS +spXXR9gjIBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQFMAMB +Af8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUAA4GBAFjOKer89961 +zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y7qj/WsjTVbJmcVfewCHrPSqnI0kB +BIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee95 +70+sB3c4 +-----END CERTIFICATE----- + +Digital Signature Trust Co. Global CA 1 +======================================= +-----BEGIN CERTIFICATE----- +MIIDKTCCApKgAwIBAgIENnAVljANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJVUzEkMCIGA1UE +ChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMREwDwYDVQQLEwhEU1RDQSBFMTAeFw05ODEy +MTAxODEwMjNaFw0xODEyMTAxODQwMjNaMEYxCzAJBgNVBAYTAlVTMSQwIgYDVQQKExtEaWdpdGFs +IFNpZ25hdHVyZSBUcnVzdCBDby4xETAPBgNVBAsTCERTVENBIEUxMIGdMA0GCSqGSIb3DQEBAQUA +A4GLADCBhwKBgQCgbIGpzzQeJN3+hijM3oMv+V7UQtLodGBmE5gGHKlREmlvMVW5SXIACH7TpWJE +NySZj9mDSI+ZbZUTu0M7LklOiDfBu1h//uG9+LthzfNHwJmm8fOR6Hh8AMthyUQncWlVSn5JTe2i +o74CTADKAqjuAQIxZA9SLRN0dja1erQtcQIBA6OCASQwggEgMBEGCWCGSAGG+EIBAQQEAwIABzBo +BgNVHR8EYTBfMF2gW6BZpFcwVTELMAkGA1UEBhMCVVMxJDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0 +dXJlIFRydXN0IENvLjERMA8GA1UECxMIRFNUQ0EgRTExDTALBgNVBAMTBENSTDEwKwYDVR0QBCQw +IoAPMTk5ODEyMTAxODEwMjNagQ8yMDE4MTIxMDE4MTAyM1owCwYDVR0PBAQDAgEGMB8GA1UdIwQY +MBaAFGp5fpFpRhgTCgJ3pVlbYJglDqL4MB0GA1UdDgQWBBRqeX6RaUYYEwoCd6VZW2CYJQ6i+DAM +BgNVHRMEBTADAQH/MBkGCSqGSIb2fQdBAAQMMAobBFY0LjADAgSQMA0GCSqGSIb3DQEBBQUAA4GB +ACIS2Hod3IEGtgllsofIH160L+nEHvI8wbsEkBFKg05+k7lNQseSJqBcNJo4cvj9axY+IO6CizEq +kzaFI4iKPANo08kJD038bKTaKHKTDomAsH3+gG9lbRgzl4vCa4nuYD3Im+9/KzJic5PLPON74nZ4 +RbyhkwS7hp86W0N6w4pl +-----END CERTIFICATE----- + +Digital Signature Trust Co. Global CA 3 +======================================= +-----BEGIN CERTIFICATE----- +MIIDKTCCApKgAwIBAgIENm7TzjANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJVUzEkMCIGA1UE +ChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMREwDwYDVQQLEwhEU1RDQSBFMjAeFw05ODEy +MDkxOTE3MjZaFw0xODEyMDkxOTQ3MjZaMEYxCzAJBgNVBAYTAlVTMSQwIgYDVQQKExtEaWdpdGFs +IFNpZ25hdHVyZSBUcnVzdCBDby4xETAPBgNVBAsTCERTVENBIEUyMIGdMA0GCSqGSIb3DQEBAQUA +A4GLADCBhwKBgQC/k48Xku8zExjrEH9OFr//Bo8qhbxe+SSmJIi2A7fBw18DW9Fvrn5C6mYjuGOD +VvsoLeE4i7TuqAHhzhy2iCoiRoX7n6dwqUcUP87eZfCocfdPJmyMvMa1795JJ/9IKn3oTQPMx7JS +xhcxEzu1TdvIxPbDDyQq2gyd55FbgM2UnQIBA6OCASQwggEgMBEGCWCGSAGG+EIBAQQEAwIABzBo +BgNVHR8EYTBfMF2gW6BZpFcwVTELMAkGA1UEBhMCVVMxJDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0 +dXJlIFRydXN0IENvLjERMA8GA1UECxMIRFNUQ0EgRTIxDTALBgNVBAMTBENSTDEwKwYDVR0QBCQw +IoAPMTk5ODEyMDkxOTE3MjZagQ8yMDE4MTIwOTE5MTcyNlowCwYDVR0PBAQDAgEGMB8GA1UdIwQY +MBaAFB6CTShlgDzJQW6sNS5ay97u+DlbMB0GA1UdDgQWBBQegk0oZYA8yUFurDUuWsve7vg5WzAM +BgNVHRMEBTADAQH/MBkGCSqGSIb2fQdBAAQMMAobBFY0LjADAgSQMA0GCSqGSIb3DQEBBQUAA4GB +AEeNg61i8tuwnkUiBbmi1gMOOHLnnvx75pO2mqWilMg0HZHRxdf0CiUPPXiBng+xZ8SQTGPdXqfi +up/1902lMXucKS1M/mQ+7LZT/uqb7YLbdHVLB3luHtgZg3Pe9T7Qtd7nS2h9Qy4qIOF+oHhEngj1 +mPnHfxsb1gYgAlihw6ID +-----END CERTIFICATE----- + +Verisign Class 3 Public Primary Certification Authority +======================================================= +-----BEGIN CERTIFICATE----- +MIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkGA1UEBhMCVVMx +FzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmltYXJ5 +IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVow +XzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAz +IFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUA +A4GNADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhEBarsAx94 +f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/isI19wKTakyYbnsZogy1Ol +hec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0GCSqGSIb3DQEBAgUAA4GBALtMEivPLCYA +TxQT3ab7/AoRhIzzKBxnki98tsX63/Dolbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59Ah +WM1pF+NEHJwZRDmJXNycAA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2Omuf +Tqj/ZA1k +-----END CERTIFICATE----- + +Verisign Class 3 Public Primary Certification Authority - G2 +============================================================ +-----BEGIN CERTIFICATE----- +MIIDAjCCAmsCEH3Z/gfPqB63EHln+6eJNMYwDQYJKoZIhvcNAQEFBQAwgcExCzAJBgNVBAYTAlVT +MRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMgUHJpbWFy +eSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVz +dCBOZXR3b3JrMB4XDTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVT +MRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMgUHJpbWFy +eSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVz +dCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMXtERXVxp0KvTuWpMmR9ZmDCO +FoUgRm1HP9SFIIThbbP4pO0M8RcPO/mn+SXXwc+EY/J8Y8+iR/LGWzOOZEAEaMGAuWQcRXfH2G71 +lSk8UOg013gfqLptQ5GVj0VXXn7F+8qkBOvqlzdUMG+7AUcyM83cV5tkaWH4mx0ciU9cZwIDAQAB +MA0GCSqGSIb3DQEBBQUAA4GBAFFNzb5cy5gZnBWyATl4Lk0PZ3BwmcYQWpSkU01UbSuvDV1Ai2TT +1+7eVmGSX6bEHRBhNtMsJzzoKQm5EWR0zLVznxxIqbxhAe7iF6YM40AIOw7n60RzKprxaZLvcRTD +Oaxxp5EJb+RxBrO6WVcmeQD2+A2iMzAo1KpYoJ2daZH9 +-----END CERTIFICATE----- + +GlobalSign Root CA +================== +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkGA1UEBhMCQkUx +GTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jvb3QgQ0ExGzAZBgNVBAMTEkds +b2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAwMDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNV +BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYD +VQQDExJHbG9iYWxTaWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDa +DuaZjc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavpxy0Sy6sc +THAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp1Wrjsok6Vjk4bwY8iGlb +Kk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdGsnUOhugZitVtbNV4FpWi6cgKOOvyJBNP +c1STE4U6G7weNLWLBYy5d4ux2x8gkasJU26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrX +gzT/LCrBbBlDSgeF59N89iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0BAQUF +AAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOzyj1hTdNGCbM+w6Dj +Y1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE38NflNUVyRRBnMRddWQVDf9VMOyG +j/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymPAbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhH +hm4qxFYxldBniYUr+WymXUadDKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveC +X4XSQRjbgbMEHMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE----- + +GlobalSign Root CA - R2 +======================= +-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4GA1UECxMXR2xv +YmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh +bFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT +aWduIFJvb3QgQ0EgLSBSMjETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln +bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6 +ErPLv4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8eoLrvozp +s6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklqtTleiDTsvHgMCJiEbKjN +S7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzdC9XZzPnqJworc5HGnRusyMvo4KD0L5CL +TfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pazq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6C +ygPCm48CAwEAAaOBnDCBmTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQUm+IHV2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5nbG9i +YWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG3lm0mi3f3BmGLjAN +BgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4GsJ0/WwbgcQ3izDJr86iw8bmEbTUsp +9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu +01yiPqFbQfXf5WRDLenVOavSot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG7 +9G+dwfCMNYxdAfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 +TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== +-----END CERTIFICATE----- + +ValiCert Class 1 VA +=================== +-----BEGIN CERTIFICATE----- +MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRp +b24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs +YXNzIDEgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZh +bGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNTIy +MjM0OFoXDTE5MDYyNTIyMjM0OFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0 +d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENsYXNzIDEg +UG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0 +LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMIGfMA0GCSqGSIb3DQEBAQUA +A4GNADCBiQKBgQDYWYJ6ibiWuqYvaG9YLqdUHAZu9OqNSLwxlBfw8068srg1knaw0KWlAdcAAxIi +GQj4/xEjm84H9b9pGib+TunRf50sQB1ZaG6m+FiwnRqP0z/x3BkGgagO4DrdyFNFCQbmD3DD+kCm +DuJWBQ8YTfwggtFzVXSNdnKgHZ0dwN0/cQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFBoPUn0LBwG +lN+VYH+Wexf+T3GtZMjdd9LvWVXoP+iOBSoh8gfStadS/pyxtuJbdxdA6nLWI8sogTLDAHkY7FkX +icnGah5xyf23dKUlRWnFSKsZ4UWKJWsZ7uW7EvV/96aNUcPwnXS3qT6gpf+2SQMT2iLM7XGCK5nP +Orf1LXLI +-----END CERTIFICATE----- + +ValiCert Class 2 VA +=================== +-----BEGIN CERTIFICATE----- +MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRp +b24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs +YXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZh +bGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAw +MTk1NFoXDTE5MDYyNjAwMTk1NFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0 +d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENsYXNzIDIg +UG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0 +LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMIGfMA0GCSqGSIb3DQEBAQUA +A4GNADCBiQKBgQDOOnHK5avIWZJV16vYdA757tn2VUdZZUcOBVXc65g2PFxTXdMwzzjsvUGJ7SVC +CSRrCl6zfN1SLUzm1NZ9WlmpZdRJEy0kTRxQb7XBhVQ7/nHk01xC+YDgkRoKWzk2Z/M/VXwbP7Rf +ZHM047QSv4dk+NoS/zcnwbNDu+97bi5p9wIDAQABMA0GCSqGSIb3DQEBBQUAA4GBADt/UG9vUJSZ +SWI4OB9L+KXIPqeCgfYrx+jFzug6EILLGACOTb2oWH+heQC1u+mNr0HZDzTuIYEZoDJJKPTEjlbV +UjP9UNV+mWwD5MlM/Mtsq2azSiGM5bUMMj4QssxsodyamEwCW/POuZ6lcg5Ktz885hZo+L7tdEy8 +W9ViH0Pd +-----END CERTIFICATE----- + +RSA Root Certificate 1 +====================== +-----BEGIN CERTIFICATE----- +MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRp +b24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs +YXNzIDMgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZh +bGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAw +MjIzM1oXDTE5MDYyNjAwMjIzM1owgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0 +d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENsYXNzIDMg +UG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0 +LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMIGfMA0GCSqGSIb3DQEBAQUA +A4GNADCBiQKBgQDjmFGWHOjVsQaBalfDcnWTq8+epvzzFlLWLU2fNUSoLgRNB0mKOCn1dzfnt6td +3zZxFJmP3MKS8edgkpfs2Ejcv8ECIMYkpChMMFp2bbFc893enhBxoYjHW5tBbcqwuI4V7q0zK89H +BFx1cQqYJJgpp0lZpd34t0NiYfPT4tBVPwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFa7AliEZwgs +3x/be0kz9dNnnfS0ChCzycUs4pJqcXgn8nCDQtM+z6lU9PHYkhaM0QTLS6vJn0WuPIqpsHEzXcjF +V9+vqDWzf4mH6eglkrh/hXqu1rweN1gqZ8mRzyqBPu3GOd/APhmcGcwTTYJBtYze4D1gCCAPRX5r +on+jjBXu +-----END CERTIFICATE----- + +Verisign Class 3 Public Primary Certification Authority - G3 +============================================================ +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQswCQYDVQQGEwJV +UzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0IE5ldHdv +cmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl +IG9ubHkxRTBDBgNVBAMTPFZlcmlTaWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRy +dXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhv +cml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWduIENsYXNzIDMgUHVibGljIFByaW1hcnkg +Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAMu6nFL8eB8aHm8bN3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1 +EUGO+i2tKmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGukxUc +cLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBmCC+Vk7+qRy+oRpfw +EuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJXwzw3sJ2zq/3avL6QaaiMxTJ5Xpj +055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWuimi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEA +ERSWwauSCPc/L8my/uRan2Te2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5f +j267Cz3qWhMeDGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC +/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565pF4ErWjfJXir0 +xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGtTxzhT5yvDwyd93gN2PQ1VoDa +t20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ== +-----END CERTIFICATE----- + +Verisign Class 4 Public Primary Certification Authority - G3 +============================================================ +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQDsoKeLbnVqAc/EfMwvlF7XMA0GCSqGSIb3DQEBBQUAMIHKMQswCQYDVQQGEwJV +UzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0IE5ldHdv +cmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl +IG9ubHkxRTBDBgNVBAMTPFZlcmlTaWduIENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRy +dXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhv +cml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWduIENsYXNzIDQgUHVibGljIFByaW1hcnkg +Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAK3LpRFpxlmr8Y+1GQ9Wzsy1HyDkniYlS+BzZYlZ3tCD5PUPtbut8XzoIfzk6AzufEUiGXaS +tBO3IFsJ+mGuqPKljYXCKtbeZjbSmwL0qJJgfJxptI8kHtCGUvYynEFYHiK9zUVilQhu0GbdU6LM +8BDcVHOLBKFGMzNcF0C5nk3T875Vg+ixiY5afJqWIpA7iCXy0lOIAgwLePLmNxdLMEYH5IBtptiW +Lugs+BGzOA1mppvqySNb247i8xOOGlktqgLw7KSHZtzBP/XYufTsgsbSPZUd5cBPhMnZo0QoBmrX +Razwa2rvTl/4EYIeOGM0ZlDUPpNz+jDDZq3/ky2X7wMCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEA +j/ola09b5KROJ1WrIhVZPMq1CtRK26vdoV9TxaBXOcLORyu+OshWv8LZJxA6sQU8wHcxuzrTBXtt +mhwwjIDLk5Mqg6sFUYICABFna/OIYUdfA5PVWw3g8dShMjWFsjrbsIKr0csKvE+MW8VLADsfKoKm +fjaF3H48ZwC15DtS4KjrXRX5xm3wrR0OhbepmnMUWluPQSjA1egtTaRezarZ7c7c2NU8Qh0XwRJd +RTjDOPP8hS6DRkiy1yBfkjaP53kPmF6Z6PDQpLv1U70qzlmwr25/bLvSHgCwIe34QWKCudiyxLtG +UPMxxY8BqHTr9Xgn2uf3ZkPznoM+IKrDNWCRzg== +-----END CERTIFICATE----- + +Entrust.net Secure Server CA +============================ +-----BEGIN CERTIFICATE----- +MIIE2DCCBEGgAwIBAgIEN0rSQzANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMCVVMxFDASBgNV +BAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50cnVzdC5uZXQvQ1BTIGluY29ycC4gYnkg +cmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRl +ZDE6MDgGA1UEAxMxRW50cnVzdC5uZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhv +cml0eTAeFw05OTA1MjUxNjA5NDBaFw0xOTA1MjUxNjM5NDBaMIHDMQswCQYDVQQGEwJVUzEUMBIG +A1UEChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5jb3JwLiBi +eSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBFbnRydXN0Lm5ldCBMaW1p +dGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQDNKIM0VBuJ8w+vN5Ex/68xYMmo6LIQ +aO2f55M28Qpku0f1BBc/I0dNxScZgSYMVHINiC3ZH5oSn7yzcdOAGT9HZnuMNSjSuQrfJNqc1lB5 +gXpa0zf3wkrYKZImZNHkmGw6AIr1NJtl+O3jEP/9uElY3KDegjlrgbEWGWG5VLbmQwIBA6OCAdcw +ggHTMBEGCWCGSAGG+EIBAQQEAwIABzCCARkGA1UdHwSCARAwggEMMIHeoIHboIHYpIHVMIHSMQsw +CQYDVQQGEwJVUzEUMBIGA1UEChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5l +dC9DUFMgaW5jb3JwLiBieSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBF +bnRydXN0Lm5ldCBMaW1pdGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMCmgJ6AlhiNodHRwOi8vd3d3LmVu +dHJ1c3QubmV0L0NSTC9uZXQxLmNybDArBgNVHRAEJDAigA8xOTk5MDUyNTE2MDk0MFqBDzIwMTkw +NTI1MTYwOTQwWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAU8BdiE1U9s/8KAGv7UISX8+1i0Bow +HQYDVR0OBBYEFPAXYhNVPbP/CgBr+1CEl/PtYtAaMAwGA1UdEwQFMAMBAf8wGQYJKoZIhvZ9B0EA +BAwwChsEVjQuMAMCBJAwDQYJKoZIhvcNAQEFBQADgYEAkNwwAvpkdMKnCqV8IY00F6j7Rw7/JXyN +Ewr75Ji174z4xRAN95K+8cPV1ZVqBLssziY2ZcgxxufuP+NXdYR6Ee9GTxj005i7qIcyunL2POI9 +n9cd2cNgQ4xYDiKWL2KjLB+6rQXvqzJ4h6BUcxm1XAX5Uj5tLUUL9wqT6u0G+bI= +-----END CERTIFICATE----- + +Entrust.net Premium 2048 Secure Server CA +========================================= +-----BEGIN CERTIFICATE----- +MIIEXDCCA0SgAwIBAgIEOGO5ZjANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChMLRW50cnVzdC5u +ZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBpbmNvcnAuIGJ5IHJlZi4gKGxp +bWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNV +BAMTKkVudHJ1c3QubmV0IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQx +NzUwNTFaFw0xOTEyMjQxODIwNTFaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3 +d3d3LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTEl +MCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEGA1UEAxMqRW50cnVzdC5u +ZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEArU1LqRKGsuqjIAcVFmQqK0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOL +Gp18EzoOH1u3Hs/lJBQesYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSr +hRSGlVuXMlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVTXTzW +nLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/HoZdenoVve8AjhUi +VBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH4QIDAQABo3QwcjARBglghkgBhvhC +AQEEBAMCAAcwHwYDVR0jBBgwFoAUVeSB0RGAvtiJuQijMfmhJAkWuXAwHQYDVR0OBBYEFFXkgdER +gL7YibkIozH5oSQJFrlwMB0GCSqGSIb2fQdBAAQQMA4bCFY1LjA6NC4wAwIEkDANBgkqhkiG9w0B +AQUFAAOCAQEAWUesIYSKF8mciVMeuoCFGsY8Tj6xnLZ8xpJdGGQC49MGCBFhfGPjK50xA3B20qMo +oPS7mmNz7W3lKtvtFKkrxjYR0CvrB4ul2p5cGZ1WEvVUKcgF7bISKo30Axv/55IQh7A6tcOdBTcS +o8f0FbnVpDkWm1M6I5HxqIKiaohowXkCIryqptau37AUX7iH0N18f3v/rxzP5tsHrV7bhZ3QKw0z +2wTR5klAEyt2+z7pnIkPFc4YsIV4IU9rTw76NmfNB/L/CNDi3tm/Kq+4h4YhPATKt5Rof8886ZjX +OP/swNlQ8C5LWK5Gb9Auw2DaclVyvUxFnmG6v4SBkgPR0ml8xQ== +-----END CERTIFICATE----- + +Baltimore CyberTrust Root +========================= +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJRTESMBAGA1UE +ChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYDVQQDExlCYWx0aW1vcmUgQ3li +ZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoXDTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMC +SUUxEjAQBgNVBAoTCUJhbHRpbW9yZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFs +dGltb3JlIEN5YmVyVHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKME +uyKrmD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjrIZ3AQSsB +UnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeKmpYcqWe4PwzV9/lSEy/C +G9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSuXmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9 +XbIGevOF6uvUA65ehD5f/xXtabz5OTZydc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjpr +l3RjM71oGDHweI12v/yejl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoI +VDaGezq1BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEB +BQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT929hkTI7gQCvlYpNRh +cL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3WgxjkzSswF07r51XgdIGn9w/xZchMB5 +hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsa +Y71k5h+3zvDyny67G7fyUIhzksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9H +RCwBXbsdtTLSR9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp +-----END CERTIFICATE----- + +Equifax Secure Global eBusiness CA +================================== +-----BEGIN CERTIFICATE----- +MIICkDCCAfmgAwIBAgIBATANBgkqhkiG9w0BAQQFADBaMQswCQYDVQQGEwJVUzEcMBoGA1UEChMT +RXF1aWZheCBTZWN1cmUgSW5jLjEtMCsGA1UEAxMkRXF1aWZheCBTZWN1cmUgR2xvYmFsIGVCdXNp +bmVzcyBDQS0xMB4XDTk5MDYyMTA0MDAwMFoXDTIwMDYyMTA0MDAwMFowWjELMAkGA1UEBhMCVVMx +HDAaBgNVBAoTE0VxdWlmYXggU2VjdXJlIEluYy4xLTArBgNVBAMTJEVxdWlmYXggU2VjdXJlIEds +b2JhbCBlQnVzaW5lc3MgQ0EtMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuucXkAJlsTRV +PEnCUdXfp9E3j9HngXNBUmCbnaEXJnitx7HoJpQytd4zjTov2/KaelpzmKNc6fuKcxtc58O/gGzN +qfTWK8D3+ZmqY6KxRwIP1ORROhI8bIpaVIRw28HFkM9yRcuoWcDNM50/o5brhTMhHD4ePmBudpxn +hcXIw2ECAwEAAaNmMGQwEQYJYIZIAYb4QgEBBAQDAgAHMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0j +BBgwFoAUvqigdHJQa0S3ySPY+6j/s1draGwwHQYDVR0OBBYEFL6ooHRyUGtEt8kj2Puo/7NXa2hs +MA0GCSqGSIb3DQEBBAUAA4GBADDiAVGqx+pf2rnQZQ8w1j7aDRRJbpGTJxQx78T3LUX47Me/okEN +I7SS+RkAZ70Br83gcfxaz2TE4JaY0KNA4gGK7ycH8WUBikQtBmV1UsCGECAhX2xrD2yuCRyv8qIY +NMR1pHMc8Y3c7635s3a0kr/clRAevsvIO1qEYBlWlKlV +-----END CERTIFICATE----- + +Equifax Secure eBusiness CA 1 +============================= +-----BEGIN CERTIFICATE----- +MIICgjCCAeugAwIBAgIBBDANBgkqhkiG9w0BAQQFADBTMQswCQYDVQQGEwJVUzEcMBoGA1UEChMT +RXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNzIENB +LTEwHhcNOTkwNjIxMDQwMDAwWhcNMjAwNjIxMDQwMDAwWjBTMQswCQYDVQQGEwJVUzEcMBoGA1UE +ChMTRXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNz +IENBLTEwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM4vGbwXt3fek6lfWg0XTzQaDJj0ItlZ +1MRoRvC0NcWFAyDGr0WlIVFFQesWWDYyb+JQYmT5/VGcqiTZ9J2DKocKIdMSODRsjQBuWqDZQu4a +IZX5UkxVWsUPOE9G+m34LjXWHXzr4vCwdYDIqROsvojvOm6rXyo4YgKwEnv+j6YDAgMBAAGjZjBk +MBEGCWCGSAGG+EIBAQQEAwIABzAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFEp4MlIR21kW +Nl7fwRQ2QGpHfEyhMB0GA1UdDgQWBBRKeDJSEdtZFjZe38EUNkBqR3xMoTANBgkqhkiG9w0BAQQF +AAOBgQB1W6ibAxHm6VZMzfmpTMANmvPMZWnmJXbMWbfWVMMdzZmsGd20hdXgPfxiIKeES1hl8eL5 +lSE/9dR+WB5Hh1Q+WKG1tfgq73HnvMP2sUlG4tega+VWeponmHxGYhTnyfxuAxJ5gDgdSIKN/Bf+ +KpYrtWKmpj29f5JZzVoqgrI3eQ== +-----END CERTIFICATE----- + +Equifax Secure eBusiness CA 2 +============================= +-----BEGIN CERTIFICATE----- +MIIDIDCCAomgAwIBAgIEN3DPtTANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJVUzEXMBUGA1UE +ChMORXF1aWZheCBTZWN1cmUxJjAkBgNVBAsTHUVxdWlmYXggU2VjdXJlIGVCdXNpbmVzcyBDQS0y +MB4XDTk5MDYyMzEyMTQ0NVoXDTE5MDYyMzEyMTQ0NVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoT +DkVxdWlmYXggU2VjdXJlMSYwJAYDVQQLEx1FcXVpZmF4IFNlY3VyZSBlQnVzaW5lc3MgQ0EtMjCB +nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA5Dk5kx5SBhsoNviyoynF7Y6yEb3+6+e0dMKP/wXn +2Z0GvxLIPw7y1tEkshHe0XMJitSxLJgJDR5QRrKDpkWNYmi7hRsgcDKqQM2mll/EcTc/BPO3QSQ5 +BxoeLmFYoBIL5aXfxavqN3HMHMg3OrmXUqesxWoklE6ce8/AatbfIb0CAwEAAaOCAQkwggEFMHAG +A1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORXF1aWZheCBTZWN1cmUx +JjAkBgNVBAsTHUVxdWlmYXggU2VjdXJlIGVCdXNpbmVzcyBDQS0yMQ0wCwYDVQQDEwRDUkwxMBoG +A1UdEAQTMBGBDzIwMTkwNjIzMTIxNDQ1WjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUUJ4L6q9e +uSBIplBqy/3YIHqngnYwHQYDVR0OBBYEFFCeC+qvXrkgSKZQasv92CB6p4J2MAwGA1UdEwQFMAMB +Af8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUAA4GBAAyGgq3oThr1 +jokn4jVYPSm0B482UJW/bsGe68SQsoWou7dC4A8HOd/7npCy0cE+U58DRLB+S/Rv5Hwf5+Kx5Lia +78O9zt4LMjTZ3ijtM2vE1Nc9ElirfQkty3D1E4qUoSek1nDFbZS1yX2doNLGCEnZZpum0/QL3MUm +V+GRMOrN +-----END CERTIFICATE----- + +AddTrust Low-Value Services Root +================================ +-----BEGIN CERTIFICATE----- +MIIEGDCCAwCgAwIBAgIBATANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQGEwJTRTEUMBIGA1UEChML +QWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYDVQQDExhBZGRU +cnVzdCBDbGFzcyAxIENBIFJvb3QwHhcNMDAwNTMwMTAzODMxWhcNMjAwNTMwMTAzODMxWjBlMQsw +CQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBO +ZXR3b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCWltQhSWDia+hBBwzexODcEyPNwTXH+9ZOEQpnXvUGW2ulCDtbKRY6 +54eyNAbFvAWlA3yCyykQruGIgb3WntP+LVbBFc7jJp0VLhD7Bo8wBN6ntGO0/7Gcrjyvd7ZWxbWr +oulpOj0OM3kyP3CCkplhbY0wCI9xP6ZIVxn4JdxLZlyldI+Yrsj5wAYi56xz36Uu+1LcsRVlIPo1 +Zmne3yzxbrww2ywkEtvrNTVokMsAsJchPXQhI2U0K7t4WaPW4XY5mqRJjox0r26kmqPZm9I4XJui +GMx1I4S+6+JNM3GOGvDC+Mcdoq0Dlyz4zyXG9rgkMbFjXZJ/Y/AlyVMuH79NAgMBAAGjgdIwgc8w +HQYDVR0OBBYEFJWxtPCUtr3H2tERCSG+wa9J/RB7MAsGA1UdDwQEAwIBBjAPBgNVHRMBAf8EBTAD +AQH/MIGPBgNVHSMEgYcwgYSAFJWxtPCUtr3H2tERCSG+wa9J/RB7oWmkZzBlMQswCQYDVQQGEwJT +RTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSEw +HwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBACxt +ZBsfzQ3duQH6lmM0MkhHma6X7f1yFqZzR1r0693p9db7RcwpiURdv0Y5PejuvE1Uhh4dbOMXJ0Ph +iVYrqW9yTkkz43J8KiOavD7/KCrto/8cI7pDVwlnTUtiBi34/2ydYB7YHEt9tTEv2dB8Xfjea4MY +eDdXL+gzB2ffHsdrKpV2ro9Xo/D0UrSpUwjP4E/TelOL/bscVjby/rK25Xa71SJlpz/+0WatC7xr +mYbvP33zGDLKe8bjq2RGlfgmadlVg3sslgf/WSxEo8bl6ancoWOAWiFeIc9TVPC6b4nbqKqVz4vj +ccweGyBECMB6tkD9xOQ14R0WHNC8K47Wcdk= +-----END CERTIFICATE----- + +AddTrust External Root +====================== +-----BEGIN CERTIFICATE----- +MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEUMBIGA1UEChML +QWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYD +VQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEw +NDgzOFowbzELMAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRU +cnVzdCBFeHRlcm5hbCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0Eg +Um9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvtH7xsD821 ++iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9uMq/NzgtHj6RQa1wVsfw +Tz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzXmk6vBbOmcZSccbNQYArHE504B4YCqOmo +aSYYkKtMsE8jqzpPhNjfzp/haW+710LXa0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy +2xSoRcRdKn23tNbE7qzNE0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv7 +7+ldU9U0WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYDVR0P +BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0Jvf6xCZU7wO94CTL +VBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRUcnVzdCBBQjEmMCQGA1UECxMdQWRk +VHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsxIjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENB +IFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZl +j7DYd7usQWxHYINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 +6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvCNr4TDea9Y355 +e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEXc4g/VhsxOBi0cQ+azcgOno4u +G+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5amnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= +-----END CERTIFICATE----- + +AddTrust Public Services Root +============================= +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIBATANBgkqhkiG9w0BAQUFADBkMQswCQYDVQQGEwJTRTEUMBIGA1UEChML +QWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSAwHgYDVQQDExdBZGRU +cnVzdCBQdWJsaWMgQ0EgUm9vdDAeFw0wMDA1MzAxMDQxNTBaFw0yMDA1MzAxMDQxNTBaMGQxCzAJ +BgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5l +dHdvcmsxIDAeBgNVBAMTF0FkZFRydXN0IFB1YmxpYyBDQSBSb290MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA6Rowj4OIFMEg2Dybjxt+A3S72mnTRqX4jsIMEZBRpS9mVEBV6tsfSlbu +nyNu9DnLoblv8n75XYcmYZ4c+OLspoH4IcUkzBEMP9smcnrHAZcHF/nXGCwwfQ56HmIexkvA/X1i +d9NEHif2P0tEs7c42TkfYNVRknMDtABp4/MUTu7R3AnPdzRGULD4EfL+OHn3Bzn+UZKXC1sIXzSG +Aa2Il+tmzV7R/9x98oTaunet3IAIx6eH1lWfl2royBFkuucZKT8Rs3iQhCBSWxHveNCD9tVIkNAw +HM+A+WD+eeSI8t0A65RF62WUaUC6wNW0uLp9BBGo6zEFlpROWCGOn9Bg/QIDAQABo4HRMIHOMB0G +A1UdDgQWBBSBPjfYkrAfd59ctKtzquf2NGAv+jALBgNVHQ8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zCBjgYDVR0jBIGGMIGDgBSBPjfYkrAfd59ctKtzquf2NGAv+qFopGYwZDELMAkGA1UEBhMCU0Ux +FDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQLExRBZGRUcnVzdCBUVFAgTmV0d29yazEgMB4G +A1UEAxMXQWRkVHJ1c3QgUHVibGljIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBAAP3FUr4 +JNojVhaTdt02KLmuG7jD8WS6IBh4lSknVwW8fCr0uVFV2ocC3g8WFzH4qnkuCRO7r7IgGRLlk/lL ++YPoRNWyQSW/iHVv/xD8SlTQX/D67zZzfRs2RcYhbbQVuE7PnFylPVoAjgbjPGsye/Kf8Lb93/Ao +GEjwxrzQvzSAlsJKsW2Ox5BF3i9nrEUEo3rcVZLJR2bYGozH7ZxOmuASu7VqTITh4SINhwBk/ox9 +Yjllpu9CtoAlEmEBqCQTcAARJl/6NVDFSMwGR+gn2HCNX2TmoUQmXiLsks3/QppEIW1cxeMiHV9H +EufOX1362KqxMy3ZdvJOOjMMK7MtkAY= +-----END CERTIFICATE----- + +AddTrust Qualified Certificates Root +==================================== +-----BEGIN CERTIFICATE----- +MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJTRTEUMBIGA1UEChML +QWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSMwIQYDVQQDExpBZGRU +cnVzdCBRdWFsaWZpZWQgQ0EgUm9vdDAeFw0wMDA1MzAxMDQ0NTBaFw0yMDA1MzAxMDQ0NTBaMGcx +CzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQ +IE5ldHdvcmsxIzAhBgNVBAMTGkFkZFRydXN0IFF1YWxpZmllZCBDQSBSb290MIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5B6a/twJWoekn0e+EV+vhDTbYjx5eLfpMLXsDBwqxBb/4Oxx +64r1EW7tTw2R0hIYLUkVAcKkIhPHEWT/IhKauY5cLwjPcWqzZwFZ8V1G87B4pfYOQnrjfxvM0PC3 +KP0q6p6zsLkEqv32x7SxuCqg+1jxGaBvcCV+PmlKfw8i2O+tCBGaKZnhqkRFmhJePp1tUvznoD1o +L/BLcHwTOK28FSXx1s6rosAx1i+f4P8UWfyEk9mHfExUE+uf0S0R+Bg6Ot4l2ffTQO2kBhLEO+GR +wVY18BTcZTYJbqukB8c10cIDMzZbdSZtQvESa0NvS3GU+jQd7RNuyoB/mC9suWXY6QIDAQABo4HU +MIHRMB0GA1UdDgQWBBQ5lYtii1zJ1IC6WA+XPxUIQ8yYpzALBgNVHQ8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zCBkQYDVR0jBIGJMIGGgBQ5lYtii1zJ1IC6WA+XPxUIQ8yYp6FrpGkwZzELMAkGA1UE +BhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQLExRBZGRUcnVzdCBUVFAgTmV0d29y +azEjMCEGA1UEAxMaQWRkVHJ1c3QgUXVhbGlmaWVkIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQAD +ggEBABmrder4i2VhlRO6aQTvhsoToMeqT2QbPxj2qC0sVY8FtzDqQmodwCVRLae/DLPt7wh/bDxG +GuoYQ992zPlmhpwsaPXpF/gxsxjE1kh9I0xowX67ARRvxdlu3rsEQmr49lx95dr6h+sNNVJn0J6X +dgWTP5XHAeZpVTh/EGGZyeNfpso+gmNIquIISD6q8rKFYqa0p9m9N5xotS1WfbC3P6CxB9bpT9ze +RXEwMn8bLgn5v1Kh7sKAPgZcLlVAwRv1cEWw3F369nJad9Jjzc9YiQBCYz95OdBEsIJuQRno3eDB +iFrRHnGTHyQwdOUeqN48Jzd/g66ed8/wMLH/S5noxqE= +-----END CERTIFICATE----- + +Entrust Root Certification Authority +==================================== +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMCVVMxFjAUBgNV +BAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0Lm5ldC9DUFMgaXMgaW5jb3Jw +b3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMWKGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsG +A1UEAxMkRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0 +MloXDTI2MTEyNzIwNTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMu +MTkwNwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSByZWZlcmVu +Y2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNVBAMTJEVudHJ1c3QgUm9v +dCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ALaVtkNC+sZtKm9I35RMOVcF7sN5EUFoNu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYsz +A9u3g3s+IIRe7bJWKKf44LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOww +Cj0Yzfv9KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGIrb68 +j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi94DkZfs0Nw4pgHBN +rziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOBsDCBrTAOBgNVHQ8BAf8EBAMCAQYw +DwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAigA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1 +MzQyWjAfBgNVHSMEGDAWgBRokORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DH +hmak8fdLQ/uEvW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9tO1KzKtvn1ISM +Y/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6ZuaAGAT/3B+XxFNSRuzFVJ7yVTa +v52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTS +W3iDVuycNsMm4hH2Z0kdkquM++v/eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0 +tHuu2guQOHXvgR1m0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- + +RSA Security 2048 v3 +==================== +-----BEGIN CERTIFICATE----- +MIIDYTCCAkmgAwIBAgIQCgEBAQAAAnwAAAAKAAAAAjANBgkqhkiG9w0BAQUFADA6MRkwFwYDVQQK +ExBSU0EgU2VjdXJpdHkgSW5jMR0wGwYDVQQLExRSU0EgU2VjdXJpdHkgMjA0OCBWMzAeFw0wMTAy +MjIyMDM5MjNaFw0yNjAyMjIyMDM5MjNaMDoxGTAXBgNVBAoTEFJTQSBTZWN1cml0eSBJbmMxHTAb +BgNVBAsTFFJTQSBTZWN1cml0eSAyMDQ4IFYzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAt49VcdKA3XtpeafwGFAyPGJn9gqVB93mG/Oe2dJBVGutn3y+Gc37RqtBaB4Y6lXIL5F4iSj7 +Jylg/9+PjDvJSZu1pJTOAeo+tWN7fyb9Gd3AIb2E0S1PRsNO3Ng3OTsor8udGuorryGlwSMiuLgb +WhOHV4PR8CDn6E8jQrAApX2J6elhc5SYcSa8LWrg903w8bYqODGBDSnhAMFRD0xS+ARaqn1y07iH +KrtjEAMqs6FPDVpeRrc9DvV07Jmf+T0kgYim3WBU6JU2PcYJk5qjEoAAVZkZR73QpXzDuvsf9/UP ++Ky5tfQ3mBMY3oVbtwyCO4dvlTlYMNpuAWgXIszACwIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/ +MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQHw1EwpKrpRa41JPr/JCwz0LGdjDAdBgNVHQ4E +FgQUB8NRMKSq6UWuNST6/yQsM9CxnYwwDQYJKoZIhvcNAQEFBQADggEBAF8+hnZuuDU8TjYcHnmY +v/3VEhF5Ug7uMYm83X/50cYVIeiKAVQNOvtUudZj1LGqlk2iQk3UUx+LEN5/Zb5gEydxiKRz44Rj +0aRV4VCT5hsOedBnvEbIvz8XDZXmxpBp3ue0L96VfdASPz0+f00/FGj1EVDVwfSQpQgdMWD/YIwj +VAqv/qFuxdF6Kmh4zx6CCiC0H63lhbJqaHVOrSU3lIW+vaHU6rcMSzyd6BIA8F+sDeGscGNz9395 +nzIlQnQFgCi/vcEkllgVsRch6YlL2weIZ/QVrXA+L02FO8K32/6YaCOJ4XQP3vTFhGMpG8zLB8kA +pKnXwiJPZ9d37CAFYd4= +-----END CERTIFICATE----- + +GeoTrust Global CA +================== +-----BEGIN CERTIFICATE----- +MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVTMRYwFAYDVQQK +Ew1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9iYWwgQ0EwHhcNMDIwNTIxMDQw +MDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5j +LjEbMBkGA1UEAxMSR2VvVHJ1c3QgR2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA2swYYzD99BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjo +BbdqfnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDviS2Aelet +8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU1XupGc1V3sjs0l44U+Vc +T4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+bw8HHa8sHo9gOeL6NlMTOdReJivbPagU +vTLrGAMoUgRx5aszPeE4uwc2hGKceeoWMPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBTAephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVk +DBF9qn1luMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKInZ57Q +zxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfStQWVYrmm3ok9Nns4 +d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcFPseKUgzbFbS9bZvlxrFUaKnjaZC2 +mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Unhw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6p +XE0zX5IJL4hmXXeXxx12E6nV5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvm +Mw== +-----END CERTIFICATE----- + +GeoTrust Global CA 2 +==================== +-----BEGIN CERTIFICATE----- +MIIDZjCCAk6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBEMQswCQYDVQQGEwJVUzEWMBQGA1UEChMN +R2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3QgR2xvYmFsIENBIDIwHhcNMDQwMzA0MDUw +MDAwWhcNMTkwMzA0MDUwMDAwWjBEMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5j +LjEdMBsGA1UEAxMUR2VvVHJ1c3QgR2xvYmFsIENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDvPE1APRDfO1MA4Wf+lGAVPoWI8YkNkMgoI5kF6CsgncbzYEbYwbLVjDHZ3CB5JIG/ +NTL8Y2nbsSpr7iFY8gjpeMtvy/wWUsiRxP89c96xPqfCfWbB9X5SJBri1WeR0IIQ13hLTytCOb1k +LUCgsBDTOEhGiKEMuzozKmKY+wCdE1l/bztyqu6mD4b5BWHqZ38MN5aL5mkWRxHCJ1kDs6ZgwiFA +Vvqgx306E+PsV8ez1q6diYD3Aecs9pYrEw15LNnA5IZ7S4wMcoKK+xfNAGw6EzywhIdLFnopsk/b +HdQL82Y3vdj2V7teJHq4PIu5+pIaGoSe2HSPqht/XvT+RSIhAgMBAAGjYzBhMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFHE4NvICMVNHK266ZUapEBVYIAUJMB8GA1UdIwQYMBaAFHE4NvICMVNH +K266ZUapEBVYIAUJMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQUFAAOCAQEAA/e1K6tdEPx7 +srJerJsOflN4WT5CBP51o62sgU7XAotexC3IUnbHLB/8gTKY0UvGkpMzNTEv/NgdRN3ggX+d6Yvh +ZJFiCzkIjKx0nVnZellSlxG5FntvRdOW2TF9AjYPnDtuzywNA0ZF66D0f0hExghAzN4bcLUprbqL +OzRldRtxIR0sFAqwlpW41uryZfspuk/qkZN0abby/+Ea0AzRdoXLiiW9l14sbxWZJue2Kf8i7MkC +x1YAzUm5s2x7UwQa4qjJqhIFI8LO57sEAszAR6LkxCkvW0VXiVHuPOtSCP8HNR6fNWpHSlaY0VqF +H4z1Ir+rzoPz4iIprn2DQKi6bA== +-----END CERTIFICATE----- + +GeoTrust Universal CA +===================== +-----BEGIN CERTIFICATE----- +MIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEWMBQGA1UEChMN +R2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVyc2FsIENBMB4XDTA0MDMwNDA1 +MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IElu +Yy4xHjAcBgNVBAMTFUdlb1RydXN0IFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAKYVVaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9t +JPi8cQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTTQjOgNB0e +RXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFhF7em6fgemdtzbvQKoiFs +7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2vc7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d +8Lsrlh/eezJS/R27tQahsiFepdaVaH/wmZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7V +qnJNk22CDtucvc+081xdVHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3Cga +Rr0BHdCXteGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZf9hB +Z3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfReBi9Fi1jUIxaS5BZu +KGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+nhutxx9z3SxPGWX9f5NAEC7S8O08 +ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0 +XG0D08DYj3rWMB8GA1UdIwQYMBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIB +hjANBgkqhkiG9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc +aanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fXIwjhmF7DWgh2 +qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzynANXH/KttgCJwpQzgXQQpAvvL +oJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0zuzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsK +xr2EoyNB3tZ3b4XUhRxQ4K5RirqNPnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxF +KyDuSN/n3QmOGKjaQI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2 +DFKWkoRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9ER/frslK +xfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQtDF4JbAiXfKM9fJP/P6EU +p8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/SfuvmbJxPgWp6ZKy7PtXny3YuxadIwVyQD8vI +P/rmMuGNG2+k5o7Y+SlIis5z/iw= +-----END CERTIFICATE----- + +GeoTrust Universal CA 2 +======================= +-----BEGIN CERTIFICATE----- +MIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEWMBQGA1UEChMN +R2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVyc2FsIENBIDIwHhcNMDQwMzA0 +MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3Qg +SW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0 +DE81WzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUGFF+3Qs17 +j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdqXbboW0W63MOhBW9Wjo8Q +JqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxLse4YuU6W3Nx2/zu+z18DwPw76L5GG//a +QMJS9/7jOvdqdzXQ2o3rXhhqMcceujwbKNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2 +WP0+GfPtDCapkzj4T8FdIgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP +20gaXT73y/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRthAAn +ZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgocQIgfksILAAX/8sgC +SqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4Lt1ZrtmhN79UNdxzMk+MBB4zsslG +8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2 ++/CfXGJx7Tz0RzgQKzAfBgNVHSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8E +BAMCAYYwDQYJKoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z +dXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQL1EuxBRa3ugZ +4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgrFg5fNuH8KrUwJM/gYwx7WBr+ +mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSoag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpq +A1Ihn0CoZ1Dy81of398j9tx4TuaYT1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpg +Y+RdM4kX2TGq2tbzGDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiP +pm8m1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJVOCiNUW7d +FGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH6aLcr34YEoP9VhdBLtUp +gn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwXQMAJKOSLakhT2+zNVVXxxvjpoixMptEm +X36vWkzaH6byHCx+rgIW0lbQL1dTR+iS +-----END CERTIFICATE----- + +America Online Root Certification Authority 1 +============================================= +-----BEGIN CERTIFICATE----- +MIIDpDCCAoygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEcMBoGA1UEChMT +QW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBPbmxpbmUgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSAxMB4XDTAyMDUyODA2MDAwMFoXDTM3MTExOTIwNDMwMFowYzELMAkG +A1UEBhMCVVMxHDAaBgNVBAoTE0FtZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2Eg +T25saW5lIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAKgv6KRpBgNHw+kqmP8ZonCaxlCyfqXfaE0bfA+2l2h9LaaLl+lkhsmj76CG +v2BlnEtUiMJIxUo5vxTjWVXlGbR0yLQFOVwWpeKVBeASrlmLojNoWBym1BW32J/X3HGrfpq/m44z +DyL9Hy7nBzbvYjnF3cu6JRQj3gzGPTzOggjmZj7aUTsWOqMFf6Dch9Wc/HKpoH145LcxVR5lu9Rh +sCFg7RAycsWSJR74kEoYeEfffjA3PlAb2xzTa5qGUwew76wGePiEmf4hjUyAtgyC9mZweRrTT6PP +8c9GsEsPPt2IYriMqQkoO3rHl+Ee5fSfwMCuJKDIodkP1nsmgmkyPacCAwEAAaNjMGEwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUAK3Zo/Z59m50qX8zPYEX10zPM94wHwYDVR0jBBgwFoAUAK3Z +o/Z59m50qX8zPYEX10zPM94wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBBQUAA4IBAQB8itEf +GDeC4Liwo+1WlchiYZwFos3CYiZhzRAW18y0ZTTQEYqtqKkFZu90821fnZmv9ov761KyBZiibyrF +VL0lvV+uyIbqRizBs73B6UlwGBaXCBOMIOAbLjpHyx7kADCVW/RFo8AasAFOq73AI25jP4BKxQft +3OJvx8Fi8eNy1gTIdGcL+oiroQHIb/AUr9KZzVGTfu0uOMe9zkZQPXLjeSWdm4grECDdpbgyn43g +Kd8hdIaC2y+CMMbHNYaz+ZZfRtsMRf3zUMNvxsNIrUam4SdHCh0Om7bCd39j8uB9Gr784N/Xx6ds +sPmuujz9dLQR6FgNgLzTqIA6me11zEZ7 +-----END CERTIFICATE----- + +America Online Root Certification Authority 2 +============================================= +-----BEGIN CERTIFICATE----- +MIIFpDCCA4ygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEcMBoGA1UEChMT +QW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBPbmxpbmUgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSAyMB4XDTAyMDUyODA2MDAwMFoXDTM3MDkyOTE0MDgwMFowYzELMAkG +A1UEBhMCVVMxHDAaBgNVBAoTE0FtZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2Eg +T25saW5lIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMjCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAMxBRR3pPU0Q9oyxQcngXssNt79Hc9PwVU3dxgz6sWYFas14tNwC206B89en +fHG8dWOgXeMHDEjsJcQDIPT/DjsS/5uN4cbVG7RtIuOx238hZK+GvFciKtZHgVdEglZTvYYUAQv8 +f3SkWq7xuhG1m1hagLQ3eAkzfDJHA1zEpYNI9FdWboE2JxhP7JsowtS013wMPgwr38oE18aO6lhO +qKSlGBxsRZijQdEt0sdtjRnxrXm3gT+9BoInLRBYBbV4Bbkv2wxrkJB+FFk4u5QkE+XRnRTf04JN +RvCAOVIyD+OEsnpD8l7eXz8d3eOyG6ChKiMDbi4BFYdcpnV1x5dhvt6G3NRI270qv0pV2uh9UPu0 +gBe4lL8BPeraunzgWGcXuVjgiIZGZ2ydEEdYMtA1fHkqkKJaEBEjNa0vzORKW6fIJ/KD3l67Xnfn +6KVuY8INXWHQjNJsWiEOyiijzirplcdIz5ZvHZIlyMbGwcEMBawmxNJ10uEqZ8A9W6Wa6897Gqid +FEXlD6CaZd4vKL3Ob5Rmg0gp2OpljK+T2WSfVVcmv2/LNzGZo2C7HK2JNDJiuEMhBnIMoVxtRsX6 +Kc8w3onccVvdtjc+31D1uAclJuW8tf48ArO3+L5DwYcRlJ4jbBeKuIonDFRH8KmzwICMoCfrHRnj +B453cMor9H124HhnAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFE1FwWg4u3Op +aaEg5+31IqEjFNeeMB8GA1UdIwQYMBaAFE1FwWg4u3OpaaEg5+31IqEjFNeeMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQUFAAOCAgEAZ2sGuV9FOypLM7PmG2tZTiLMubekJcmnxPBUlgtk87FY +T15R/LKXeydlwuXK5w0MJXti4/qftIe3RUavg6WXSIylvfEWK5t2LHo1YGwRgJfMqZJS5ivmae2p ++DYtLHe/YUjRYwu5W1LtGLBDQiKmsXeu3mnFzcccobGlHBD7GL4acN3Bkku+KVqdPzW+5X1R+FXg +JXUjhx5c3LqdsKyzadsXg8n33gy8CNyRnqjQ1xU3c6U1uPx+xURABsPr+CKAXEfOAuMRn0T//Zoy +zH1kUQ7rVyZ2OuMeIjzCpjbdGe+n/BLzJsBZMYVMnNjP36TMzCmT/5RtdlwTCJfy7aULTd3oyWgO +ZtMADjMSW7yV5TKQqLPGbIOtd+6Lfn6xqavT4fG2wLHqiMDn05DpKJKUe2h7lyoKZy2FAjgQ5ANh +1NolNscIWC2hp1GvMApJ9aZphwctREZ2jirlmjvXGKL8nDgQzMY70rUXOm/9riW99XJZZLF0Kjhf +GEzfz3EEWjbUvy+ZnOjZurGV5gJLIaFb1cFPj65pbVPbAZO1XB4Y3WRayhgoPmMEEf0cjQAPuDff +Z4qdZqkCapH/E8ovXYO8h5Ns3CRRFgQlZvqz2cK6Kb6aSDiCmfS/O0oxGfm/jiEzFMpPVF/7zvuP +cX/9XhmgD0uRuMRUvAawRY8mkaKO/qk= +-----END CERTIFICATE----- + +Visa eCommerce Root +=================== +-----BEGIN CERTIFICATE----- +MIIDojCCAoqgAwIBAgIQE4Y1TR0/BvLB+WUF1ZAcYjANBgkqhkiG9w0BAQUFADBrMQswCQYDVQQG +EwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRlcm5hdGlvbmFsIFNlcnZpY2Ug +QXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNvbW1lcmNlIFJvb3QwHhcNMDIwNjI2MDIxODM2 +WhcNMjIwNjI0MDAxNjEyWjBrMQswCQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMm +VmlzYSBJbnRlcm5hdGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNv +bW1lcmNlIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvV95WHm6h2mCxlCfL +F9sHP4CFT8icttD0b0/Pmdjh28JIXDqsOTPHH2qLJj0rNfVIsZHBAk4ElpF7sDPwsRROEW+1QK8b +RaVK7362rPKgH1g/EkZgPI2h4H3PVz4zHvtH8aoVlwdVZqW1LS7YgFmypw23RuwhY/81q6UCzyr0 +TP579ZRdhE2o8mCP2w4lPJ9zcc+U30rq299yOIzzlr3xF7zSujtFWsan9sYXiwGd/BmoKoMWuDpI +/k4+oKsGGelT84ATB+0tvz8KPFUgOSwsAGl0lUq8ILKpeeUYiZGo3BxN77t+Nwtd/jmliFKMAGzs +GHxBvfaLdXe6YJ2E5/4tAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEG +MB0GA1UdDgQWBBQVOIMPPyw/cDMezUb+B4wg4NfDtzANBgkqhkiG9w0BAQUFAAOCAQEAX/FBfXxc +CLkr4NWSR/pnXKUTwwMhmytMiUbPWU3J/qVAtmPN3XEolWcRzCSs00Rsca4BIGsDoo8Ytyk6feUW +YFN4PMCvFYP3j1IzJL1kk5fui/fbGKhtcbP3LBfQdCVp9/5rPJS+TUtBjE7ic9DjkCJzQ83z7+pz +zkWKsKZJ/0x9nXGIxHYdkFsd7v3M9+79YKWxehZx0RbQfBI8bGmX265fOZpwLwU8GUYEmSA20GBu +YQa7FkKMcPcw++DbZqMAAb3mLNqRX6BGi01qnD093QVG/na/oAo85ADmJ7f/hC3euiInlhBx6yLt +398znM/jra6O1I7mT1GvFpLgXPYHDw== +-----END CERTIFICATE----- + +Certum Root CA +============== +-----BEGIN CERTIFICATE----- +MIIDDDCCAfSgAwIBAgIDAQAgMA0GCSqGSIb3DQEBBQUAMD4xCzAJBgNVBAYTAlBMMRswGQYDVQQK +ExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBDQTAeFw0wMjA2MTExMDQ2Mzla +Fw0yNzA2MTExMDQ2MzlaMD4xCzAJBgNVBAYTAlBMMRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8u +by4xEjAQBgNVBAMTCUNlcnR1bSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM6x +wS7TT3zNJc4YPk/EjG+AanPIW1H4m9LcuwBcsaD8dQPugfCI7iNS6eYVM42sLQnFdvkrOYCJ5JdL +kKWoePhzQ3ukYbDYWMzhbGZ+nPMJXlVjhNWo7/OxLjBos8Q82KxujZlakE403Daaj4GIULdtlkIJ +89eVgw1BS7Bqa/j8D35in2fE7SZfECYPCE/wpFcozo+47UX2bu4lXapuOb7kky/ZR6By6/qmW6/K +Uz/iDsaWVhFu9+lmqSbYf5VT7QqFiLpPKaVCjF62/IUgAKpoC6EahQGcxEZjgoi2IrHu/qpGWX7P +NSzVttpd90gzFFS269lvzs2I1qsb2pY7HVkCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQUFAAOCAQEAuI3O7+cUus/usESSbLQ5PqKEbq24IXfS1HeCh+YgQYHu4vgRt2PRFze+ +GXYkHAQaTOs9qmdvLdTN/mUxcMUbpgIKumB7bVjCmkn+YzILa+M6wKyrO7Do0wlRjBCDxjTgxSvg +GrZgFCdsMneMvLJymM/NzD+5yCRCFNZX/OYmQ6kd5YCQzgNUKD73P9P4Te1qCjqTE5s7FCMTY5w/ +0YcneeVMUeMBrYVdGjux1XMQpNPyvG5k9VpWkKjHDkx0Dy5xO/fIR/RpbxXyEV6DHpx8Uq79AtoS +qFlnGNu8cN2bsWntgM6JQEhqDjXKKWYVIZQs6GAqm4VKQPNriiTsBhYscw== +-----END CERTIFICATE----- + +Comodo AAA Services root +======================== +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEbMBkGA1UECAwS +R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0Eg +TGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAw +MFoXDTI4MTIzMTIzNTk1OVowezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hl +c3RlcjEQMA4GA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV +BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQuaBtDFcCLNSS1UY8y2bmhG +C1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe3M/vg4aijJRPn2jymJBGhCfHdr/jzDUs +i14HZGWCwEiwqJH5YZ92IFCokcdmtet4YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszW +Y19zjNoFmag4qMsXeDZRrOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjH +Ypy+g8cmez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQUoBEK +Iz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wewYDVR0f +BHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNl +cy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29tb2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2Vz +LmNybDANBgkqhkiG9w0BAQUFAAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm +7l3sAg9g1o1QGE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2G9w84FoVxp7Z +8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsil2D4kF501KKaU73yqWjgom7C +12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- + +Comodo Secure Services root +=========================== +-----BEGIN CERTIFICATE----- +MIIEPzCCAyegAwIBAgIBATANBgkqhkiG9w0BAQUFADB+MQswCQYDVQQGEwJHQjEbMBkGA1UECAwS +R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0Eg +TGltaXRlZDEkMCIGA1UEAwwbU2VjdXJlIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAw +MDAwMFoXDTI4MTIzMTIzNTk1OVowfjELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFu +Y2hlc3RlcjEQMA4GA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxJDAi +BgNVBAMMG1NlY3VyZSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAMBxM4KK0HDrc4eCQNUd5MvJDkKQ+d40uaG6EfQlhfPMcm3ye5drswfxdySRXyWP +9nQ95IDC+DwN879A6vfIUtFyb+/Iq0G4bi4XKpVpDM3SHpR7LZQdqnXXs5jLrLxkU0C8j6ysNstc +rbvd4JQX7NFc0L/vpZXJkMWwrPsbQ996CF23uPJAGysnnlDOXmWCiIxe004MeuoIkbY2qitC++rC +oznl2yY4rYsK7hljxxwk3wN42ubqwUcaCwtGCd0C/N7Lh1/XMGNooa7cMqG6vv5Eq2i2pRcV/b3V +p6ea5EQz6YiO/O1R65NxTq0B50SOqy3LqP4BSUjwwN3HaNiS/j0CAwEAAaOBxzCBxDAdBgNVHQ4E +FgQUPNiTiMLAggnMAZkGkyDpnnAJY08wDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8w +gYEGA1UdHwR6MHgwO6A5oDeGNWh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL1NlY3VyZUNlcnRpZmlj +YXRlU2VydmljZXMuY3JsMDmgN6A1hjNodHRwOi8vY3JsLmNvbW9kby5uZXQvU2VjdXJlQ2VydGlm +aWNhdGVTZXJ2aWNlcy5jcmwwDQYJKoZIhvcNAQEFBQADggEBAIcBbSMdflsXfcFhMs+P5/OKlFlm +4J4oqF7Tt/Q05qo5spcWxYJvMqTpjOev/e/C6LlLqqP05tqNZSH7uoDrJiiFGv45jN5bBAS0VPmj +Z55B+glSzAVIqMk/IQQezkhr/IXownuvf7fM+F86/TXGDe+X3EyrEeFryzHRbPtIgKvcnDe4IRRL +DXE97IMzbtFuMhbsmMcWi1mmNKsFVy2T96oTy9IT4rcuO81rUBcJaD61JlfutuC23bkpgHl9j6Pw +pCikFcSF9CfUa7/lXORlAnZUtOM3ZiTTGWHIUhDlizeauan5Hb/qmZJhlv8BzaFfDbxxvA6sCx1H +RR3B7Hzs/Sk= +-----END CERTIFICATE----- + +Comodo Trusted Services root +============================ +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIBATANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJHQjEbMBkGA1UECAwS +R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0Eg +TGltaXRlZDElMCMGA1UEAwwcVHJ1c3RlZCBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0wNDAxMDEw +MDAwMDBaFw0yODEyMzEyMzU5NTlaMH8xCzAJBgNVBAYTAkdCMRswGQYDVQQIDBJHcmVhdGVyIE1h +bmNoZXN0ZXIxEDAOBgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoMEUNvbW9kbyBDQSBMaW1pdGVkMSUw +IwYDVQQDDBxUcnVzdGVkIENlcnRpZmljYXRlIFNlcnZpY2VzMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA33FvNlhTWvI2VFeAxHQIIO0Yfyod5jWaHiWsnOWWfnJSoBVC21ndZHoa0Lh7 +3TkVvFVIxO06AOoxEbrycXQaZ7jPM8yoMa+j49d/vzMtTGo87IvDktJTdyR0nAducPy9C1t2ul/y +/9c3S0pgePfw+spwtOpZqqPOSC+pw7ILfhdyFgymBwwbOM/JYrc/oJOlh0Hyt3BAd9i+FHzjqMB6 +juljatEPmsbS9Is6FARW1O24zG71++IsWL1/T2sr92AkWCTOJu80kTrV44HQsvAEAtdbtz6SrGsS +ivnkBbA7kUlcsutT6vifR4buv5XAwAaf0lteERv0xwQ1KdJVXOTt6wIDAQABo4HJMIHGMB0GA1Ud +DgQWBBTFe1i97doladL3WRaoszLAeydb9DAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zCBgwYDVR0fBHwwejA8oDqgOIY2aHR0cDovL2NybC5jb21vZG9jYS5jb20vVHJ1c3RlZENlcnRp +ZmljYXRlU2VydmljZXMuY3JsMDqgOKA2hjRodHRwOi8vY3JsLmNvbW9kby5uZXQvVHJ1c3RlZENl +cnRpZmljYXRlU2VydmljZXMuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQDIk4E7ibSvuIQSTI3S8Ntw +uleGFTQQuS9/HrCoiWChisJ3DFBKmwCL2Iv0QeLQg4pKHBQGsKNoBXAxMKdTmw7pSqBYaWcOrp32 +pSxBvzwGa+RZzG0Q8ZZvH9/0BAKkn0U+yNj6NkZEUD+Cl5EfKNsYEYwq5GWDVxISjBc/lDb+XbDA +BHcTuPQV1T84zJQ6VdCsmPW6AF/ghhmBeC8owH7TzEIK9a5QoNE+xqFx7D+gIIxmOom0jtTYsU0l +R+4viMi14QVFwL4Ucd56/Y57fU0IlqUSc/AtyjcndBInTMu2l+nZrghtWjlA3QVHdWpaIbOjGM9O +9y5Xt5hwXsjEeLBi +-----END CERTIFICATE----- + +QuoVadis Root CA +================ +-----BEGIN CERTIFICATE----- +MIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJCTTEZMBcGA1UE +ChMQUXVvVmFkaXMgTGltaXRlZDElMCMGA1UECxMcUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eTEuMCwGA1UEAxMlUXVvVmFkaXMgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMTAz +MTkxODMzMzNaFw0yMTAzMTcxODMzMzNaMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRp +cyBMaW1pdGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYDVQQD +EyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAv2G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Ypli4kVEAkOPcahdxYTMuk +J0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2DrOpm2RgbaIr1VxqYuvXtdj182d6UajtL +F8HVj71lODqV0D1VNk7feVcxKh7YWWVJWCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeL +YzcS19Dsw3sgQUSj7cugF+FxZc4dZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWen +AScOospUxbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuwIDAQABo4ICUjCCAk4w +PQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwczovL29jc3AucXVvdmFkaXNvZmZzaG9y +ZS5jb20wDwYDVR0TAQH/BAUwAwEB/zCCARoGA1UdIASCAREwggENMIIBCQYJKwYBBAG+WAABMIH7 +MIHUBggrBgEFBQcCAjCBxxqBxFJlbGlhbmNlIG9uIHRoZSBRdW9WYWRpcyBSb290IENlcnRpZmlj +YXRlIGJ5IGFueSBwYXJ0eSBhc3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJs +ZSBzdGFuZGFyZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRpb24gcHJh +Y3RpY2VzLCBhbmQgdGhlIFF1b1ZhZGlzIENlcnRpZmljYXRlIFBvbGljeS4wIgYIKwYBBQUHAgEW +Fmh0dHA6Ly93d3cucXVvdmFkaXMuYm0wHQYDVR0OBBYEFItLbe3TKbkGGew5Oanwl4Rqy+/fMIGu +BgNVHSMEgaYwgaOAFItLbe3TKbkGGew5Oanwl4Rqy+/foYGEpIGBMH8xCzAJBgNVBAYTAkJNMRkw +FwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MS4wLAYDVQQDEyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggQ6 +tlCLMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAitQUtf70mpKnGdSkfnIYj9lo +fFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf87C9TqnN7Az10buYWnuul +LsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1RcHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2x +gI4JVrmcGmD+XcHXetwReNDWXcG31a0ymQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi +5upZIof4l/UO/erMkqQWxFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi +5nrQNiOKSnQ2+Q== +-----END CERTIFICATE----- + +QuoVadis Root CA 2 +================== +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- + +QuoVadis Root CA 3 +================== +-----BEGIN CERTIFICATE----- +MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT +EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMzAeFw0wNjExMjQx +OTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDMV0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNgg +DhoB4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUrH556VOij +KTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd8lyyBTNvijbO0BNO/79K +DDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9CabwvvWhDFlaJKjdhkf2mrk7AyxRllDdLkgbv +BNDInIjbC3uBr7E9KsRlOni27tyAsdLTmZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwp +p5ijJUMv7/FfJuGITfhebtfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8 +nT8KKdjcT5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDtWAEX +MJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZc6tsgLjoC2SToJyM +Gf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A4iLItLRkT9a6fUg+qGkM17uGcclz +uD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYDVR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHT +BgkrBgEEAb5YAAMwgcUwgZMGCCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmlj +YXRlIGNvbnN0aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 +aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVudC4wLQYIKwYB +BQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2NwczALBgNVHQ8EBAMCAQYwHQYD +VR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4GA1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4 +ywLQoUmkRzBFMQswCQYDVQQGEwJCTTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UE +AxMSUXVvVmFkaXMgUm9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZV +qyM07ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSemd1o417+s +hvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd+LJ2w/w4E6oM3kJpK27z +POuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2 +Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadNt54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp +8kokUvd0/bpO5qgdAm6xDYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBC +bjPsMZ57k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6szHXu +g/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0jWy10QJLZYxkNc91p +vGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeTmJlglFwjz1onl14LBQaTNx47aTbr +qZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK4SVhM7JZG+Ju1zdXtg2pEto= +-----END CERTIFICATE----- + +Security Communication Root CA +============================== +-----BEGIN CERTIFICATE----- +MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMP +U0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEw +HhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMP +U0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw +8yl89f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJDKaVv0uM +DPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9Ms+k2Y7CI9eNqPPYJayX +5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/NQV3Is00qVUarH9oe4kA92819uZKAnDfd +DJZkndwi92SL32HeFZRSFaB9UslLqCHJxrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2 +JChzAgMBAAGjPzA9MB0GA1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYw +DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vGkl3g +0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfrUj94nK9NrvjVT8+a +mCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5Bw+SUEmK3TGXX8npN6o7WWWXlDLJ +s58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJUJRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ +6rBK+1YWc26sTfcioU+tHXotRSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAi +FL39vmwLAw== +-----END CERTIFICATE----- + +Sonera Class 2 Root CA +====================== +-----BEGIN CERTIFICATE----- +MIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEPMA0GA1UEChMG +U29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAxMDQwNjA3Mjk0MFoXDTIxMDQw +NjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNVBAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJh +IENsYXNzMiBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJAXSjWdyvANlsdE+hY3 +/Ei9vX+ALTU74W+oZ6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybT +dXnt5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s3TmVToMG +f+dJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2EjvOr7nQKV0ba5cTppCD8P +tOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu8nYybieDwnPz3BjotJPqdURrBGAgcVeH +nfO+oJAjPYok4doh28MCAwEAAaMzMDEwDwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQISqCqWITT +XjwwCwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQBazof5FnIVV0sd2ZvnoiYw7JNn39Yt +0jSv9zilzqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/3DEI +cbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvDFNr450kkkdAdavph +Oe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6Tk6ezAyNlNzZRZxe7EJQY670XcSx +EtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLH +llpwrN9M +-----END CERTIFICATE----- + +Staat der Nederlanden Root CA +============================= +-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgIEAJiWijANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJOTDEeMBwGA1UE +ChMVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSYwJAYDVQQDEx1TdGFhdCBkZXIgTmVkZXJsYW5kZW4g +Um9vdCBDQTAeFw0wMjEyMTcwOTIzNDlaFw0xNTEyMTYwOTE1MzhaMFUxCzAJBgNVBAYTAk5MMR4w +HAYDVQQKExVTdGFhdCBkZXIgTmVkZXJsYW5kZW4xJjAkBgNVBAMTHVN0YWF0IGRlciBOZWRlcmxh +bmRlbiBSb290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmNK1URF6gaYUmHFt +vsznExvWJw56s2oYHLZhWtVhCb/ekBPHZ+7d89rFDBKeNVU+LCeIQGv33N0iYfXCxw719tV2U02P +jLwYdjeFnejKScfST5gTCaI+Ioicf9byEGW07l8Y1Rfj+MX94p2i71MOhXeiD+EwR+4A5zN9RGca +C1Hoi6CeUJhoNFIfLm0B8mBF8jHrqTFoKbt6QZ7GGX+UtFE5A3+y3qcym7RHjm+0Sq7lr7HcsBth +vJly3uSJt3omXdozSVtSnA71iq3DuD3oBmrC1SoLbHuEvVYFy4ZlkuxEK7COudxwC0barbxjiDn6 +22r+I/q85Ej0ZytqERAhSQIDAQABo4GRMIGOMAwGA1UdEwQFMAMBAf8wTwYDVR0gBEgwRjBEBgRV +HSAAMDwwOgYIKwYBBQUHAgEWLmh0dHA6Ly93d3cucGtpb3ZlcmhlaWQubmwvcG9saWNpZXMvcm9v +dC1wb2xpY3kwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSofeu8Y6R0E3QA7Jbg0zTBLL9s+DAN +BgkqhkiG9w0BAQUFAAOCAQEABYSHVXQ2YcG70dTGFagTtJ+k/rvuFbQvBgwp8qiSpGEN/KtcCFtR +EytNwiphyPgJWPwtArI5fZlmgb9uXJVFIGzmeafR2Bwp/MIgJ1HI8XxdNGdphREwxgDS1/PTfLbw +MVcoEoJz6TMvplW0C5GUR5z6u3pCMuiufi3IvKwUv9kP2Vv8wfl6leF9fpb8cbDCTMjfRTTJzg3y +nGQI0DvDKcWy7ZAEwbEpkcUwb8GpcjPM/l0WFywRaed+/sWDCN+83CI6LiBpIzlWYGeQiy52OfsR +iJf2fL1LuCAWZwWN4jvBcj+UlTfHXbme2JOhF4//DGYVwSR8MnwDHTuhWEUykw== +-----END CERTIFICATE----- + +UTN DATACorp SGC Root CA +======================== +-----BEGIN CERTIFICATE----- +MIIEXjCCA0agAwIBAgIQRL4Mi1AAIbQR0ypoBqmtaTANBgkqhkiG9w0BAQUFADCBkzELMAkGA1UE +BhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEeMBwGA1UEChMVVGhl +IFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xGzAZ +BgNVBAMTElVUTiAtIERBVEFDb3JwIFNHQzAeFw05OTA2MjQxODU3MjFaFw0xOTA2MjQxOTA2MzBa +MIGTMQswCQYDVQQGEwJVUzELMAkGA1UECBMCVVQxFzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4w +HAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxITAfBgNVBAsTGGh0dHA6Ly93d3cudXNlcnRy +dXN0LmNvbTEbMBkGA1UEAxMSVVROIC0gREFUQUNvcnAgU0dDMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA3+5YEKIrblXEjr8uRgnn4AgPLit6E5Qbvfa2gI5lBZMAHryv4g+OGQ0SR+ys +raP6LnD43m77VkIVni5c7yPeIbkFdicZD0/Ww5y0vpQZY/KmEQrrU0icvvIpOxboGqBMpsn0GFlo +wHDyUwDAXlCCpVZvNvlK4ESGoE1O1kduSUrLZ9emxAW5jh70/P/N5zbgnAVssjMiFdC04MwXwLLA +9P4yPykqlXvY8qdOD1R8oQ2AswkDwf9c3V6aPryuvEeKaq5xyh+xKrhfQgUL7EYw0XILyulWbfXv +33i+Ybqypa4ETLyorGkVl73v67SMvzX41MPRKA5cOp9wGDMgd8SirwIDAQABo4GrMIGoMAsGA1Ud +DwQEAwIBxjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRTMtGzz3/64PGgXYVOktKeRR20TzA9 +BgNVHR8ENjA0MDKgMKAuhixodHRwOi8vY3JsLnVzZXJ0cnVzdC5jb20vVVROLURBVEFDb3JwU0dD +LmNybDAqBgNVHSUEIzAhBggrBgEFBQcDAQYKKwYBBAGCNwoDAwYJYIZIAYb4QgQBMA0GCSqGSIb3 +DQEBBQUAA4IBAQAnNZcAiosovcYzMB4p/OL31ZjUQLtgyr+rFywJNn9Q+kHcrpY6CiM+iVnJowft +Gzet/Hy+UUla3joKVAgWRcKZsYfNjGjgaQPpxE6YsjuMFrMOoAyYUJuTqXAJyCyjj98C5OBxOvG0 +I3KgqgHf35g+FFCgMSa9KOlaMCZ1+XtgHI3zzVAmbQQnmt/VDUVHKWss5nbZqSl9Mt3JNjy9rjXx +EZ4du5A/EkdOjtd+D2JzHVImOBwYSf0wdJrE5SIv2MCN7ZF6TACPcn9d2t0bi0Vr591pl6jFVkwP +DPafepE39peC4N1xaf92P2BNPM/3mfnGV/TJVTl4uix5yaaIK/QI +-----END CERTIFICATE----- + +UTN USERFirst Hardware Root CA +============================== +-----BEGIN CERTIFICATE----- +MIIEdDCCA1ygAwIBAgIQRL4Mi1AAJLQR0zYq/mUK/TANBgkqhkiG9w0BAQUFADCBlzELMAkGA1UE +BhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEeMBwGA1UEChMVVGhl +IFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHzAd +BgNVBAMTFlVUTi1VU0VSRmlyc3QtSGFyZHdhcmUwHhcNOTkwNzA5MTgxMDQyWhcNMTkwNzA5MTgx +OTIyWjCBlzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0 +eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8vd3d3LnVz +ZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3QtSGFyZHdhcmUwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCx98M4P7Sof885glFn0G2f0v9Y8+efK+wNiVSZuTiZFvfgIXlI +wrthdBKWHTxqctU8EGc6Oe0rE81m65UJM6Rsl7HoxuzBdXmcRl6Nq9Bq/bkqVRcQVLMZ8Jr28bFd +tqdt++BxF2uiiPsA3/4aMXcMmgF6sTLjKwEHOG7DpV4jvEWbe1DByTCP2+UretNb+zNAHqDVmBe8 +i4fDidNdoI6yqqr2jmmIBsX6iSHzCJ1pLgkzmykNRg+MzEk0sGlRvfkGzWitZky8PqxhvQqIDsjf +Pe58BEydCl5rkdbux+0ojatNh4lz0G6k0B4WixThdkQDf2Os5M1JnMWS9KsyoUhbAgMBAAGjgbkw +gbYwCwYDVR0PBAQDAgHGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFKFyXyYbKJhDlV0HN9WF +lp1L0sNFMEQGA1UdHwQ9MDswOaA3oDWGM2h0dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9VVE4tVVNF +UkZpcnN0LUhhcmR3YXJlLmNybDAxBgNVHSUEKjAoBggrBgEFBQcDAQYIKwYBBQUHAwUGCCsGAQUF +BwMGBggrBgEFBQcDBzANBgkqhkiG9w0BAQUFAAOCAQEARxkP3nTGmZev/K0oXnWO6y1n7k57K9cM +//bey1WiCuFMVGWTYGufEpytXoMs61quwOQt9ABjHbjAbPLPSbtNk28GpgoiskliCE7/yMgUsogW +XecB5BKV5UU0s4tpvc+0hY91UZ59Ojg6FEgSxvunOxqNDYJAB+gECJChicsZUN/KHAG8HQQZexB2 +lzvukJDKxA4fFm517zP4029bHpbj4HR3dHuKom4t3XbWOTCC8KucUvIqx69JXn7HaOWCgchqJ/kn +iCrVWFCVH/A7HFe7fRQ5YiuayZSSKqMiDP+JJn1fIytH1xUdqWqeUQ0qUZ6B+dQ7XnASfxAynB67 +nfhmqA== +-----END CERTIFICATE----- + +Camerfirma Chambers of Commerce Root +==================================== +-----BEGIN CERTIFICATE----- +MIIEvTCCA6WgAwIBAgIBADANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJFVTEnMCUGA1UEChMe +QUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQLExpodHRwOi8vd3d3LmNoYW1i +ZXJzaWduLm9yZzEiMCAGA1UEAxMZQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdDAeFw0wMzA5MzAx +NjEzNDNaFw0zNzA5MzAxNjEzNDRaMH8xCzAJBgNVBAYTAkVVMScwJQYDVQQKEx5BQyBDYW1lcmZp +cm1hIFNBIENJRiBBODI3NDMyODcxIzAhBgNVBAsTGmh0dHA6Ly93d3cuY2hhbWJlcnNpZ24ub3Jn +MSIwIAYDVQQDExlDaGFtYmVycyBvZiBDb21tZXJjZSBSb290MIIBIDANBgkqhkiG9w0BAQEFAAOC +AQ0AMIIBCAKCAQEAtzZV5aVdGDDg2olUkfzIx1L4L1DZ77F1c2VHfRtbunXF/KGIJPov7coISjlU +xFF6tdpg6jg8gbLL8bvZkSM/SAFwdakFKq0fcfPJVD0dBmpAPrMMhe5cG3nCYsS4No41XQEMIwRH +NaqbYE6gZj3LJgqcQKH0XZi/caulAGgq7YN6D6IUtdQis4CwPAxaUWktWBiP7Zme8a7ileb2R6jW +DA+wWFjbw2Y3npuRVDM30pQcakjJyfKl2qUMI/cjDpwyVV5xnIQFUZot/eZOKjRa3spAN2cMVCFV +d9oKDMyXroDclDZK9D7ONhMeU+SsTjoF7Nuucpw4i9A5O4kKPnf+dQIBA6OCAUQwggFAMBIGA1Ud +EwEB/wQIMAYBAf8CAQwwPAYDVR0fBDUwMzAxoC+gLYYraHR0cDovL2NybC5jaGFtYmVyc2lnbi5v +cmcvY2hhbWJlcnNyb290LmNybDAdBgNVHQ4EFgQU45T1sU3p26EpW1eLTXYGduHRooowDgYDVR0P +AQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzAnBgNVHREEIDAegRxjaGFtYmVyc3Jvb3RAY2hh +bWJlcnNpZ24ub3JnMCcGA1UdEgQgMB6BHGNoYW1iZXJzcm9vdEBjaGFtYmVyc2lnbi5vcmcwWAYD +VR0gBFEwTzBNBgsrBgEEAYGHLgoDATA+MDwGCCsGAQUFBwIBFjBodHRwOi8vY3BzLmNoYW1iZXJz +aWduLm9yZy9jcHMvY2hhbWJlcnNyb290Lmh0bWwwDQYJKoZIhvcNAQEFBQADggEBAAxBl8IahsAi +fJ/7kPMa0QOx7xP5IV8EnNrJpY0nbJaHkb5BkAFyk+cefV/2icZdp0AJPaxJRUXcLo0waLIJuvvD +L8y6C98/d3tGfToSJI6WjzwFCm/SlCgdbQzALogi1djPHRPH8EjX1wWnz8dHnjs8NMiAT9QUu/wN +UPf6s+xCX6ndbcj0dc97wXImsQEcXCz9ek60AcUFV7nnPKoF2YjpB0ZBzu9Bga5Y34OirsrXdx/n +ADydb47kMgkdTXg0eDQ8lJsm7U9xxhl6vSAiSFr+S30Dt+dYvsYyTnQeaN2oaFuzPu5ifdmA6Ap1 +erfutGWaIZDgqtCYvDi1czyL+Nw= +-----END CERTIFICATE----- + +Camerfirma Global Chambersign Root +================================== +-----BEGIN CERTIFICATE----- +MIIExTCCA62gAwIBAgIBADANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJFVTEnMCUGA1UEChMe +QUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQLExpodHRwOi8vd3d3LmNoYW1i +ZXJzaWduLm9yZzEgMB4GA1UEAxMXR2xvYmFsIENoYW1iZXJzaWduIFJvb3QwHhcNMDMwOTMwMTYx +NDE4WhcNMzcwOTMwMTYxNDE4WjB9MQswCQYDVQQGEwJFVTEnMCUGA1UEChMeQUMgQ2FtZXJmaXJt +YSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQLExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEg +MB4GA1UEAxMXR2xvYmFsIENoYW1iZXJzaWduIFJvb3QwggEgMA0GCSqGSIb3DQEBAQUAA4IBDQAw +ggEIAoIBAQCicKLQn0KuWxfH2H3PFIP8T8mhtxOviteePgQKkotgVvq0Mi+ITaFgCPS3CU6gSS9J +1tPfnZdan5QEcOw/Wdm3zGaLmFIoCQLfxS+EjXqXd7/sQJ0lcqu1PzKY+7e3/HKE5TWH+VX6ox8O +by4o3Wmg2UIQxvi1RMLQQ3/bvOSiPGpVeAp3qdjqGTK3L/5cPxvusZjsyq16aUXjlg9V9ubtdepl +6DJWk0aJqCWKZQbua795B9Dxt6/tLE2Su8CoX6dnfQTyFQhwrJLWfQTSM/tMtgsL+xrJxI0DqX5c +8lCrEqWhz0hQpe/SyBoT+rB/sYIcd2oPX9wLlY/vQ37mRQklAgEDo4IBUDCCAUwwEgYDVR0TAQH/ +BAgwBgEB/wIBDDA/BgNVHR8EODA2MDSgMqAwhi5odHRwOi8vY3JsLmNoYW1iZXJzaWduLm9yZy9j +aGFtYmVyc2lnbnJvb3QuY3JsMB0GA1UdDgQWBBRDnDafsJ4wTcbOX60Qq+UDpfqpFDAOBgNVHQ8B +Af8EBAMCAQYwEQYJYIZIAYb4QgEBBAQDAgAHMCoGA1UdEQQjMCGBH2NoYW1iZXJzaWducm9vdEBj +aGFtYmVyc2lnbi5vcmcwKgYDVR0SBCMwIYEfY2hhbWJlcnNpZ25yb290QGNoYW1iZXJzaWduLm9y +ZzBbBgNVHSAEVDBSMFAGCysGAQQBgYcuCgEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly9jcHMuY2hh +bWJlcnNpZ24ub3JnL2Nwcy9jaGFtYmVyc2lnbnJvb3QuaHRtbDANBgkqhkiG9w0BAQUFAAOCAQEA +PDtwkfkEVCeR4e3t/mh/YV3lQWVPMvEYBZRqHN4fcNs+ezICNLUMbKGKfKX0j//U2K0X1S0E0T9Y +gOKBWYi+wONGkyT+kL0mojAt6JcmVzWJdJYY9hXiryQZVgICsroPFOrGimbBhkVVi76SvpykBMdJ +PJ7oKXqJ1/6v/2j1pReQvayZzKWGVwlnRtvWFsJG8eSpUPWP0ZIV018+xgBJOm5YstHRJw0lyDL4 +IBHNfTIzSJRUTN3cecQwn+uOuFW114hcxWokPbLTBQNRxgfvzBRydD1ucs4YKIxKoHflCStFREes +t2d/AYoFWpO+ocH/+OcOZ6RHSXZddZAa9SaP8A== +-----END CERTIFICATE----- + +NetLock Notary (Class A) Root +============================= +-----BEGIN CERTIFICATE----- +MIIGfTCCBWWgAwIBAgICAQMwDQYJKoZIhvcNAQEEBQAwga8xCzAJBgNVBAYTAkhVMRAwDgYDVQQI +EwdIdW5nYXJ5MREwDwYDVQQHEwhCdWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6 +dG9uc2FnaSBLZnQuMRowGAYDVQQLExFUYW51c2l0dmFueWtpYWRvazE2MDQGA1UEAxMtTmV0TG9j +ayBLb3pqZWd5em9pIChDbGFzcyBBKSBUYW51c2l0dmFueWtpYWRvMB4XDTk5MDIyNDIzMTQ0N1oX +DTE5MDIxOTIzMTQ0N1owga8xCzAJBgNVBAYTAkhVMRAwDgYDVQQIEwdIdW5nYXJ5MREwDwYDVQQH +EwhCdWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6dG9uc2FnaSBLZnQuMRowGAYD +VQQLExFUYW51c2l0dmFueWtpYWRvazE2MDQGA1UEAxMtTmV0TG9jayBLb3pqZWd5em9pIChDbGFz +cyBBKSBUYW51c2l0dmFueWtpYWRvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvHSM +D7tM9DceqQWC2ObhbHDqeLVu0ThEDaiDzl3S1tWBxdRL51uUcCbbO51qTGL3cfNk1mE7PetzozfZ +z+qMkjvN9wfcZnSX9EUi3fRc4L9t875lM+QVOr/bmJBVOMTtplVjC7B4BPTjbsE/jvxReB+SnoPC +/tmwqcm8WgD/qaiYdPv2LD4VOQ22BFWoDpggQrOxJa1+mm9dU7GrDPzr4PN6s6iz/0b2Y6LYOph7 +tqyF/7AlT3Rj5xMHpQqPBffAZG9+pyeAlt7ULoZgx2srXnN7F+eRP2QM2EsiNCubMvJIH5+hCoR6 +4sKtlz2O1cH5VqNQ6ca0+pii7pXmKgOM3wIDAQABo4ICnzCCApswDgYDVR0PAQH/BAQDAgAGMBIG +A1UdEwEB/wQIMAYBAf8CAQQwEQYJYIZIAYb4QgEBBAQDAgAHMIICYAYJYIZIAYb4QgENBIICURaC +Ak1GSUdZRUxFTSEgRXplbiB0YW51c2l0dmFueSBhIE5ldExvY2sgS2Z0LiBBbHRhbGFub3MgU3pv +bGdhbHRhdGFzaSBGZWx0ZXRlbGVpYmVuIGxlaXJ0IGVsamFyYXNvayBhbGFwamFuIGtlc3p1bHQu +IEEgaGl0ZWxlc2l0ZXMgZm9seWFtYXRhdCBhIE5ldExvY2sgS2Z0LiB0ZXJtZWtmZWxlbG9zc2Vn +LWJpenRvc2l0YXNhIHZlZGkuIEEgZGlnaXRhbGlzIGFsYWlyYXMgZWxmb2dhZGFzYW5hayBmZWx0 +ZXRlbGUgYXogZWxvaXJ0IGVsbGVub3J6ZXNpIGVsamFyYXMgbWVndGV0ZWxlLiBBeiBlbGphcmFz +IGxlaXJhc2EgbWVndGFsYWxoYXRvIGEgTmV0TG9jayBLZnQuIEludGVybmV0IGhvbmxhcGphbiBh +IGh0dHBzOi8vd3d3Lm5ldGxvY2submV0L2RvY3MgY2ltZW4gdmFneSBrZXJoZXRvIGF6IGVsbGVu +b3J6ZXNAbmV0bG9jay5uZXQgZS1tYWlsIGNpbWVuLiBJTVBPUlRBTlQhIFRoZSBpc3N1YW5jZSBh +bmQgdGhlIHVzZSBvZiB0aGlzIGNlcnRpZmljYXRlIGlzIHN1YmplY3QgdG8gdGhlIE5ldExvY2sg +Q1BTIGF2YWlsYWJsZSBhdCBodHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIG9yIGJ5IGUtbWFp +bCBhdCBjcHNAbmV0bG9jay5uZXQuMA0GCSqGSIb3DQEBBAUAA4IBAQBIJEb3ulZv+sgoA0BO5TE5 +ayZrU3/b39/zcT0mwBQOxmd7I6gMc90Bu8bKbjc5VdXHjFYgDigKDtIqpLBJUsY4B/6+CgmM0ZjP +ytoUMaFP0jn8DxEsQ8Pdq5PHVT5HfBgaANzze9jyf1JsIPQLX2lS9O74silg6+NJMSEN1rUQQeJB +CWziGppWS3cC9qCbmieH6FUpccKQn0V4GuEVZD3QDtigdp+uxdAu6tYPVuxkf1qbFFgBJ34TUMdr +KuZoPL9coAob4Q566eKAw+np9v1sEZ7Q5SgnK1QyQhSCdeZK8CtmdWOMovsEPoMOmzbwGOQmIMOM +8CgHrTwXZoi1/baI +-----END CERTIFICATE----- + +NetLock Business (Class B) Root +=============================== +-----BEGIN CERTIFICATE----- +MIIFSzCCBLSgAwIBAgIBaTANBgkqhkiG9w0BAQQFADCBmTELMAkGA1UEBhMCSFUxETAPBgNVBAcT +CEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0b25zYWdpIEtmdC4xGjAYBgNV +BAsTEVRhbnVzaXR2YW55a2lhZG9rMTIwMAYDVQQDEylOZXRMb2NrIFV6bGV0aSAoQ2xhc3MgQikg +VGFudXNpdHZhbnlraWFkbzAeFw05OTAyMjUxNDEwMjJaFw0xOTAyMjAxNDEwMjJaMIGZMQswCQYD +VQQGEwJIVTERMA8GA1UEBxMIQnVkYXBlc3QxJzAlBgNVBAoTHk5ldExvY2sgSGFsb3phdGJpenRv +bnNhZ2kgS2Z0LjEaMBgGA1UECxMRVGFudXNpdHZhbnlraWFkb2sxMjAwBgNVBAMTKU5ldExvY2sg +VXpsZXRpIChDbGFzcyBCKSBUYW51c2l0dmFueWtpYWRvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB +iQKBgQCx6gTsIKAjwo84YM/HRrPVG/77uZmeBNwcf4xKgZjupNTKihe5In+DCnVMm8Bp2GQ5o+2S +o/1bXHQawEfKOml2mrriRBf8TKPV/riXiK+IA4kfpPIEPsgHC+b5sy96YhQJRhTKZPWLgLViqNhr +1nGTLbO/CVRY7QbrqHvcQ7GhaQIDAQABo4ICnzCCApswEgYDVR0TAQH/BAgwBgEB/wIBBDAOBgNV +HQ8BAf8EBAMCAAYwEQYJYIZIAYb4QgEBBAQDAgAHMIICYAYJYIZIAYb4QgENBIICURaCAk1GSUdZ +RUxFTSEgRXplbiB0YW51c2l0dmFueSBhIE5ldExvY2sgS2Z0LiBBbHRhbGFub3MgU3pvbGdhbHRh +dGFzaSBGZWx0ZXRlbGVpYmVuIGxlaXJ0IGVsamFyYXNvayBhbGFwamFuIGtlc3p1bHQuIEEgaGl0 +ZWxlc2l0ZXMgZm9seWFtYXRhdCBhIE5ldExvY2sgS2Z0LiB0ZXJtZWtmZWxlbG9zc2VnLWJpenRv +c2l0YXNhIHZlZGkuIEEgZGlnaXRhbGlzIGFsYWlyYXMgZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUg +YXogZWxvaXJ0IGVsbGVub3J6ZXNpIGVsamFyYXMgbWVndGV0ZWxlLiBBeiBlbGphcmFzIGxlaXJh +c2EgbWVndGFsYWxoYXRvIGEgTmV0TG9jayBLZnQuIEludGVybmV0IGhvbmxhcGphbiBhIGh0dHBz +Oi8vd3d3Lm5ldGxvY2submV0L2RvY3MgY2ltZW4gdmFneSBrZXJoZXRvIGF6IGVsbGVub3J6ZXNA +bmV0bG9jay5uZXQgZS1tYWlsIGNpbWVuLiBJTVBPUlRBTlQhIFRoZSBpc3N1YW5jZSBhbmQgdGhl +IHVzZSBvZiB0aGlzIGNlcnRpZmljYXRlIGlzIHN1YmplY3QgdG8gdGhlIE5ldExvY2sgQ1BTIGF2 +YWlsYWJsZSBhdCBodHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIG9yIGJ5IGUtbWFpbCBhdCBj +cHNAbmV0bG9jay5uZXQuMA0GCSqGSIb3DQEBBAUAA4GBAATbrowXr/gOkDFOzT4JwG06sPgzTEdM +43WIEJessDgVkcYplswhwG08pXTP2IKlOcNl40JwuyKQ433bNXbhoLXan3BukxowOR0w2y7jfLKR +stE3Kfq51hdcR0/jHTjrn9V7lagonhVK0dHQKwCXoOKSNitjrFgBazMpUIaD8QFI +-----END CERTIFICATE----- + +NetLock Express (Class C) Root +============================== +-----BEGIN CERTIFICATE----- +MIIFTzCCBLigAwIBAgIBaDANBgkqhkiG9w0BAQQFADCBmzELMAkGA1UEBhMCSFUxETAPBgNVBAcT +CEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0b25zYWdpIEtmdC4xGjAYBgNV +BAsTEVRhbnVzaXR2YW55a2lhZG9rMTQwMgYDVQQDEytOZXRMb2NrIEV4cHJlc3N6IChDbGFzcyBD +KSBUYW51c2l0dmFueWtpYWRvMB4XDTk5MDIyNTE0MDgxMVoXDTE5MDIyMDE0MDgxMVowgZsxCzAJ +BgNVBAYTAkhVMREwDwYDVQQHEwhCdWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6 +dG9uc2FnaSBLZnQuMRowGAYDVQQLExFUYW51c2l0dmFueWtpYWRvazE0MDIGA1UEAxMrTmV0TG9j +ayBFeHByZXNzeiAoQ2xhc3MgQykgVGFudXNpdHZhbnlraWFkbzCBnzANBgkqhkiG9w0BAQEFAAOB +jQAwgYkCgYEA6+ywbGGKIyWvYCDj2Z/8kwvbXY2wobNAOoLO/XXgeDIDhlqGlZHtU/qdQPzm6N3Z +W3oDvV3zOwzDUXmbrVWg6dADEK8KuhRC2VImESLH0iDMgqSaqf64gXadarfSNnU+sYYJ9m5tfk63 +euyucYT2BDMIJTLrdKwWRMbkQJMdf60CAwEAAaOCAp8wggKbMBIGA1UdEwEB/wQIMAYBAf8CAQQw +DgYDVR0PAQH/BAQDAgAGMBEGCWCGSAGG+EIBAQQEAwIABzCCAmAGCWCGSAGG+EIBDQSCAlEWggJN +RklHWUVMRU0hIEV6ZW4gdGFudXNpdHZhbnkgYSBOZXRMb2NrIEtmdC4gQWx0YWxhbm9zIFN6b2xn +YWx0YXRhc2kgRmVsdGV0ZWxlaWJlbiBsZWlydCBlbGphcmFzb2sgYWxhcGphbiBrZXN6dWx0LiBB +IGhpdGVsZXNpdGVzIGZvbHlhbWF0YXQgYSBOZXRMb2NrIEtmdC4gdGVybWVrZmVsZWxvc3NlZy1i +aXp0b3NpdGFzYSB2ZWRpLiBBIGRpZ2l0YWxpcyBhbGFpcmFzIGVsZm9nYWRhc2FuYWsgZmVsdGV0 +ZWxlIGF6IGVsb2lydCBlbGxlbm9yemVzaSBlbGphcmFzIG1lZ3RldGVsZS4gQXogZWxqYXJhcyBs +ZWlyYXNhIG1lZ3RhbGFsaGF0byBhIE5ldExvY2sgS2Z0LiBJbnRlcm5ldCBob25sYXBqYW4gYSBo +dHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIGNpbWVuIHZhZ3kga2VyaGV0byBheiBlbGxlbm9y +emVzQG5ldGxvY2submV0IGUtbWFpbCBjaW1lbi4gSU1QT1JUQU5UISBUaGUgaXNzdWFuY2UgYW5k +IHRoZSB1c2Ugb2YgdGhpcyBjZXJ0aWZpY2F0ZSBpcyBzdWJqZWN0IHRvIHRoZSBOZXRMb2NrIENQ +UyBhdmFpbGFibGUgYXQgaHR0cHM6Ly93d3cubmV0bG9jay5uZXQvZG9jcyBvciBieSBlLW1haWwg +YXQgY3BzQG5ldGxvY2submV0LjANBgkqhkiG9w0BAQQFAAOBgQAQrX/XDDKACtiG8XmYta3UzbM2 +xJZIwVzNmtkFLp++UOv0JhQQLdRmF/iewSf98e3ke0ugbLWrmldwpu2gpO0u9f38vf5NNwgMvOOW +gyL1SRt/Syu0VMGAfJlOHdCM7tCs5ZL6dVb+ZKATj7i4Fp1hBWeAyNDYpQcCNJgEjTME1A== +-----END CERTIFICATE----- + +XRamp Global CA Root +==================== +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCBgjELMAkGA1UE +BhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2Vj +dXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwHhcNMDQxMTAxMTcxNDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMx +HjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkg +U2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS638eMpSe2OAtp87ZOqCwu +IR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCPKZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMx +foArtYzAQDsRhtDLooY2YKTVMIJt2W7QDxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FE +zG+gSqmUsE3a56k0enI4qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqs +AxcZZPRaJSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNViPvry +xS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASsjVy16bYbMDYGA1UdHwQvMC0wK6Ap +oCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMC +AQEwDQYJKoZIhvcNAQEFBQADggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc +/Kh4ZzXxHfARvbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt +qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLaIR9NmXmd4c8n +nxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSyi6mx5O+aGtA9aZnuqCij4Tyz +8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQO+7ETPTsJ3xCwnR8gooJybQDJbw= +-----END CERTIFICATE----- + +Go Daddy Class 2 CA +=================== +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMY +VGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkG +A1UEBhMCVVMxITAfBgNVBAoTGFRoZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28g +RGFkZHkgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQAD +ggENADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCAPVYYYwhv +2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6wwdhFJ2+qN1j3hybX2C32 +qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXiEqITLdiOr18SPaAIBQi2XKVlOARFmR6j +YGB0xUGlcmIbYsUfb18aQr4CUWWoriMYavx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmY +vLEHZ6IVDd2gWMZEewo+YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0O +BBYEFNLEsNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h/t2o +atTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMu +MTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwG +A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wim +PQoZ+YeAEW5p5JYXMP80kWNyOO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKt +I3lpjbi2Tc7PTMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ +HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mERdEr/VxqHD3VI +Ls9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5CufReYNnyicsbkqWletNw+vHX/b +vZ8= +-----END CERTIFICATE----- + +Starfield Class 2 CA +==================== +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzElMCMGA1UEChMc +U3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZpZWxkIENsYXNzIDIg +Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQwNjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBo +MQswCQYDVQQGEwJVUzElMCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAG +A1UECxMpU3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqG +SIb3DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf8MOh2tTY +bitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN+lq2cwQlZut3f+dZxkqZ +JRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVm +epsZGD3/cVE8MC5fvj13c7JdBmzDI1aaK4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSN +F4Azbl5KXZnJHoe0nRrA1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HF +MIHCMB0GA1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fRzt0f +hvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNo +bm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBDbGFzcyAyIENlcnRpZmljYXRpb24g +QXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGs +afPzWdqbAYcaT1epoXkJKtv3L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLM +PUxA2IGvd56Deruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl +xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynpVSJYACPq4xJD +KVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEYWQPJIrSPnNVeKtelttQKbfi3 +QBFGmh95DmK/D5fs4C8fF5Q= +-----END CERTIFICATE----- + +StartCom Certification Authority +================================ +-----BEGIN CERTIFICATE----- +MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMN +U3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmlu +ZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0 +NjM2WhcNMzYwOTE3MTk0NjM2WjB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRk +LjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMg +U3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZkpMyONvg45iPwbm2xPN1y +o4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rfOQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/ +Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/CJi/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/d +eMotHweXMAEtcnn6RtYTKqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt +2PZE4XNiHzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMMAv+Z +6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w+2OqqGwaVLRcJXrJ +osmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/ +untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVc +UjyJthkqcwEKDwOzEmDyei+B26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT +37uMdBNSSwIDAQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE +FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9jZXJ0LnN0YXJ0 +Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3JsLnN0YXJ0Y29tLm9yZy9zZnNj +YS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFMBgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUH +AgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRw +Oi8vY2VydC5zdGFydGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYg +U3RhcnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlhYmlsaXR5 +LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2YgdGhlIFN0YXJ0Q29tIENl +cnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFpbGFibGUgYXQgaHR0cDovL2NlcnQuc3Rh +cnRjb20ub3JnL3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilT +dGFydENvbSBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOC +AgEAFmyZ9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8jhvh +3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUWFjgKXlf2Ysd6AgXm +vB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJzewT4F+irsfMuXGRuczE6Eri8sxHk +fY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3 +fsNrarnDy0RLrHiQi+fHLB5LEUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZ +EoalHmdkrQYuL6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq +yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuCO3NJo2pXh5Tl +1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6Vum0ABj6y6koQOdjQK/W/7HW/ +lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkyShNOsF/5oirpt9P/FlUQqmMGqz9IgcgA38coro +g14= +-----END CERTIFICATE----- + +Taiwan GRCA +=========== +-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIQH51ZWtcvwgZEpYAIaeNe9jANBgkqhkiG9w0BAQUFADA/MQswCQYDVQQG +EwJUVzEwMC4GA1UECgwnR292ZXJubWVudCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4X +DTAyMTIwNTEzMjMzM1oXDTMyMTIwNTEzMjMzM1owPzELMAkGA1UEBhMCVFcxMDAuBgNVBAoMJ0dv +dmVybm1lbnQgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAJoluOzMonWoe/fOW1mKydGGEghU7Jzy50b2iPN86aXfTEc2pBsBHH8eV4qN +w8XRIePaJD9IK/ufLqGU5ywck9G/GwGHU5nOp/UKIXZ3/6m3xnOUT0b3EEk3+qhZSV1qgQdW8or5 +BtD3cCJNtLdBuTK4sfCxw5w/cP1T3YGq2GN49thTbqGsaoQkclSGxtKyyhwOeYHWtXBiCAEuTk8O +1RGvqa/lmr/czIdtJuTJV6L7lvnM4T9TjGxMfptTCAtsF/tnyMKtsc2AtJfcdgEWFelq16TheEfO +htX7MfP6Mb40qij7cEwdScevLJ1tZqa2jWR+tSBqnTuBto9AAGdLiYa4zGX+FVPpBMHWXx1E1wov +J5pGfaENda1UhhXcSTvxls4Pm6Dso3pdvtUqdULle96ltqqvKKyskKw4t9VoNSZ63Pc78/1Fm9G7 +Q3hub/FCVGqY8A2tl+lSXunVanLeavcbYBT0peS2cWeqH+riTcFCQP5nRhc4L0c/cZyu5SHKYS1t +B6iEfC3uUSXxY5Ce/eFXiGvviiNtsea9P63RPZYLhY3Naye7twWb7LuRqQoHEgKXTiCQ8P8NHuJB +O9NAOueNXdpm5AKwB1KYXA6OM5zCppX7VRluTI6uSw+9wThNXo+EHWbNxWCWtFJaBYmOlXqYwZE8 +lSOyDvR5tMl8wUohAgMBAAGjajBoMB0GA1UdDgQWBBTMzO/MKWCkO7GStjz6MmKPrCUVOzAMBgNV +HRMEBTADAQH/MDkGBGcqBwAEMTAvMC0CAQAwCQYFKw4DAhoFADAHBgVnKgMAAAQUA5vwIhP/lSg2 +09yewDL7MTqKUWUwDQYJKoZIhvcNAQEFBQADggIBAECASvomyc5eMN1PhnR2WPWus4MzeKR6dBcZ +TulStbngCnRiqmjKeKBMmo4sIy7VahIkv9Ro04rQ2JyftB8M3jh+Vzj8jeJPXgyfqzvS/3WXy6Tj +Zwj/5cAWtUgBfen5Cv8b5Wppv3ghqMKnI6mGq3ZW6A4M9hPdKmaKZEk9GhiHkASfQlK3T8v+R0F2 +Ne//AHY2RTKbxkaFXeIksB7jSJaYV0eUVXoPQbFEJPPB/hprv4j9wabak2BegUqZIJxIZhm1AHlU +D7gsL0u8qV1bYH+Mh6XgUmMqvtg7hUAV/h62ZT/FS9p+tXo1KaMuephgIqP0fSdOLeq0dDzpD6Qz +DxARvBMB1uUO07+1EqLhRSPAzAhuYbeJq4PjJB7mXQfnHyA+z2fI56wwbSdLaG5LKlwCCDTb+Hbk +Z6MmnD+iMsJKxYEYMRBWqoTvLQr/uB930r+lWKBi5NdLkXWNiYCYfm3LU05er/ayl4WXudpVBrkk +7tfGOB5jGxI7leFYrPLfhNVfmS8NVVvmONsuP3LpSIXLuykTjx44VbnzssQwmSNOXfJIoRIM3BKQ +CZBUkQM8R+XVyWXgt0t97EfTsws+rZ7QdAAO671RrcDeLMDDav7v3Aun+kbfYNucpllQdSNpc5Oy ++fwC00fmcc4QAu4njIT/rEUNE1yDMuAlpYYsfPQS +-----END CERTIFICATE----- + +Firmaprofesional Root CA +======================== +-----BEGIN CERTIFICATE----- +MIIEVzCCAz+gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBnTELMAkGA1UEBhMCRVMxIjAgBgNVBAcT +GUMvIE11bnRhbmVyIDI0NCBCYXJjZWxvbmExQjBABgNVBAMTOUF1dG9yaWRhZCBkZSBDZXJ0aWZp +Y2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODEmMCQGCSqGSIb3DQEJARYXY2FA +ZmlybWFwcm9mZXNpb25hbC5jb20wHhcNMDExMDI0MjIwMDAwWhcNMTMxMDI0MjIwMDAwWjCBnTEL +MAkGA1UEBhMCRVMxIjAgBgNVBAcTGUMvIE11bnRhbmVyIDI0NCBCYXJjZWxvbmExQjBABgNVBAMT +OUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2 +ODEmMCQGCSqGSIb3DQEJARYXY2FAZmlybWFwcm9mZXNpb25hbC5jb20wggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDnIwNvbyOlXnjOlSztlB5uCp4Bx+ow0Syd3Tfom5h5VtP8c9/Qit5V +j1H5WuretXDE7aTt/6MNbg9kUDGvASdYrv5sp0ovFy3Tc9UTHI9ZpTQsHVQERc1ouKDAA6XPhUJH +lShbz++AbOCQl4oBPB3zhxAwJkh91/zpnZFx/0GaqUC1N5wpIE8fUuOgfRNtVLcK3ulqTgesrBlf +3H5idPayBQC6haD9HThuy1q7hryUZzM1gywfI834yJFxzJeL764P3CkDG8A563DtwW4O2GcLiam8 +NeTvtjS0pbbELaW+0MOUJEjb35bTALVmGotmBQ/dPz/LP6pemkr4tErvlTcbAgMBAAGjgZ8wgZww +KgYDVR0RBCMwIYYfaHR0cDovL3d3dy5maXJtYXByb2Zlc2lvbmFsLmNvbTASBgNVHRMBAf8ECDAG +AQH/AgEBMCsGA1UdEAQkMCKADzIwMDExMDI0MjIwMDAwWoEPMjAxMzEwMjQyMjAwMDBaMA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQUMwugZtHq2s7eYpMEKFK1FH84aLcwDQYJKoZIhvcNAQEFBQAD +ggEBAEdz/o0nVPD11HecJ3lXV7cVVuzH2Fi3AQL0M+2TUIiefEaxvT8Ub/GzR0iLjJcG1+p+o1wq +u00vR+L4OQbJnC4xGgN49Lw4xiKLMzHwFgQEffl25EvXwOaD7FnMP97/T2u3Z36mhoEyIwOdyPdf +wUpgpZKpsaSgYMN4h7Mi8yrrW6ntBas3D7Hi05V2Y1Z0jFhyGzflZKG+TQyTmAyX9odtsz/ny4Cm +7YjHX1BiAuiZdBbQ5rQ58SfLyEDW44YQqSMSkuBpQWOnryULwMWSyx6Yo1q6xTMPoJcB3X/ge9YG +VM+h4k0460tQtcsm9MracEpqoeJ5quGnM/b9Sh/22WA= +-----END CERTIFICATE----- + +Wells Fargo Root CA +=================== +-----BEGIN CERTIFICATE----- +MIID5TCCAs2gAwIBAgIEOeSXnjANBgkqhkiG9w0BAQUFADCBgjELMAkGA1UEBhMCVVMxFDASBgNV +BAoTC1dlbGxzIEZhcmdvMSwwKgYDVQQLEyNXZWxscyBGYXJnbyBDZXJ0aWZpY2F0aW9uIEF1dGhv +cml0eTEvMC0GA1UEAxMmV2VsbHMgRmFyZ28gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcN +MDAxMDExMTY0MTI4WhcNMjEwMTE0MTY0MTI4WjCBgjELMAkGA1UEBhMCVVMxFDASBgNVBAoTC1dl +bGxzIEZhcmdvMSwwKgYDVQQLEyNXZWxscyBGYXJnbyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEv +MC0GA1UEAxMmV2VsbHMgRmFyZ28gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVqDM7Jvk0/82bfuUER84A4n135zHCLielTWi5MbqNQ1mX +x3Oqfz1cQJ4F5aHiidlMuD+b+Qy0yGIZLEWukR5zcUHESxP9cMIlrCL1dQu3U+SlK93OvRw6esP3 +E48mVJwWa2uv+9iWsWCaSOAlIiR5NM4OJgALTqv9i86C1y8IcGjBqAr5dE8Hq6T54oN+J3N0Prj5 +OEL8pahbSCOz6+MlsoCultQKnMJ4msZoGK43YjdeUXWoWGPAUe5AeH6orxqg4bB4nVCMe+ez/I4j +sNtlAHCEAQgAFG5Uhpq6zPk3EPbg3oQtnaSFN9OH4xXQwReQfhkhahKpdv0SAulPIV4XAgMBAAGj +YTBfMA8GA1UdEwEB/wQFMAMBAf8wTAYDVR0gBEUwQzBBBgtghkgBhvt7hwcBCzAyMDAGCCsGAQUF +BwIBFiRodHRwOi8vd3d3LndlbGxzZmFyZ28uY29tL2NlcnRwb2xpY3kwDQYJKoZIhvcNAQEFBQAD +ggEBANIn3ZwKdyu7IvICtUpKkfnRLb7kuxpo7w6kAOnu5+/u9vnldKTC2FJYxHT7zmu1Oyl5GFrv +m+0fazbuSCUlFLZWohDo7qd/0D+j0MNdJu4HzMPBJCGHHt8qElNvQRbn7a6U+oxy+hNH8Dx+rn0R +OhPs7fpvcmR7nX1/Jv16+yWt6j4pf0zjAFcysLPp7VMX2YuyFA4w6OXVE8Zkr8QA1dhYJPz1j+zx +x32l2w8n0cbyQIjmH/ZhqPRCyLk306m+LFZ4wnKbWV01QIroTmMatukgalHizqSQ33ZwmVxwQ023 +tqcZZE6St8WRPH9IFmV7Fv3L/PvZ1dZPIWU7Sn9Ho/s= +-----END CERTIFICATE----- + +Swisscom Root CA 1 +================== +-----BEGIN CERTIFICATE----- +MIIF2TCCA8GgAwIBAgIQXAuFXAvnWUHfV8w/f52oNjANBgkqhkiG9w0BAQUFADBkMQswCQYDVQQG +EwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0YWwgQ2VydGlmaWNhdGUgU2Vy +dmljZXMxGzAZBgNVBAMTElN3aXNzY29tIFJvb3QgQ0EgMTAeFw0wNTA4MTgxMjA2MjBaFw0yNTA4 +MTgyMjA2MjBaMGQxCzAJBgNVBAYTAmNoMREwDwYDVQQKEwhTd2lzc2NvbTElMCMGA1UECxMcRGln +aXRhbCBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczEbMBkGA1UEAxMSU3dpc3Njb20gUm9vdCBDQSAxMIIC +IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0LmwqAzZuz8h+BvVM5OAFmUgdbI9m2BtRsiM +MW8Xw/qabFbtPMWRV8PNq5ZJkCoZSx6jbVfd8StiKHVFXqrWW/oLJdihFvkcxC7mlSpnzNApbjyF +NDhhSbEAn9Y6cV9Nbc5fuankiX9qUvrKm/LcqfmdmUc/TilftKaNXXsLmREDA/7n29uj/x2lzZAe +AR81sH8A25Bvxn570e56eqeqDFdvpG3FEzuwpdntMhy0XmeLVNxzh+XTF3xmUHJd1BpYwdnP2IkC +b6dJtDZd0KTeByy2dbcokdaXvij1mB7qWybJvbCXc9qukSbraMH5ORXWZ0sKbU/Lz7DkQnGMU3nn +7uHbHaBuHYwadzVcFh4rUx80i9Fs/PJnB3r1re3WmquhsUvhzDdf/X/NTa64H5xD+SpYVUNFvJbN +cA78yeNmuk6NO4HLFWR7uZToXTNShXEuT46iBhFRyePLoW4xCGQMwtI89Tbo19AOeCMgkckkKmUp +WyL3Ic6DXqTz3kvTaI9GdVyDCW4pa8RwjPWd1yAv/0bSKzjCL3UcPX7ape8eYIVpQtPM+GP+HkM5 +haa2Y0EQs3MevNP6yn0WR+Kn1dCjigoIlmJWbjTb2QK5MHXjBNLnj8KwEUAKrNVxAmKLMb7dxiNY +MUJDLXT5xp6mig/p/r+D5kNXJLrvRjSq1xIBOO0CAwEAAaOBhjCBgzAOBgNVHQ8BAf8EBAMCAYYw +HQYDVR0hBBYwFDASBgdghXQBUwABBgdghXQBUwABMBIGA1UdEwEB/wQIMAYBAf8CAQcwHwYDVR0j +BBgwFoAUAyUv3m+CATpcLNwroWm1Z9SM0/0wHQYDVR0OBBYEFAMlL95vggE6XCzcK6FptWfUjNP9 +MA0GCSqGSIb3DQEBBQUAA4ICAQA1EMvspgQNDQ/NwNurqPKIlwzfky9NfEBWMXrrpA9gzXrzvsMn +jgM+pN0S734edAY8PzHyHHuRMSG08NBsl9Tpl7IkVh5WwzW9iAUPWxAaZOHHgjD5Mq2eUCzneAXQ +MbFamIp1TpBcahQq4FJHgmDmHtqBsfsUC1rxn9KVuj7QG9YVHaO+htXbD8BJZLsuUBlL0iT43R4H +VtA4oJVwIHaM190e3p9xxCPvgxNcoyQVTSlAPGrEqdi3pkSlDfTgnXceQHAm/NrZNuR55LU/vJtl +vrsRls/bxig5OgjOR1tTWsWZ/l2p3e9M1MalrQLmjAcSHm8D0W+go/MpvRLHUKKwf4ipmXeascCl +OS5cfGniLLDqN2qk4Vrh9VDlg++luyqI54zb/W1elxmofmZ1a3Hqv7HHb6D0jqTsNFFbjCYDcKF3 +1QESVwA12yPeDooomf2xEG9L/zgtYE4snOtnta1J7ksfrK/7DZBaZmBwXarNeNQk7shBoJMBkpxq +nvy5JMWzFYJ+vq6VK+uxwNrjAWALXmmshFZhvnEX/h0TD/7Gh0Xp/jKgGg0TpJRVcaUWi7rKibCy +x/yP2FS1k2Kdzs9Z+z0YzirLNRWCXf9UIltxUvu3yf5gmwBBZPCqKuy2QkPOiWaByIufOVQDJdMW +NY6E0F/6MBr1mmz0DlP5OlvRHA== +-----END CERTIFICATE----- + +DigiCert Assured ID Root CA +=========================== +-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw +IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzEx +MTEwMDAwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL +ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0Ew +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7cJpSIqvTO +9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYPmDI2dsze3Tyoou9q+yHy +UmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW +/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpy +oeb6pNnVFzF1roV9Iq4/AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whf +GHdPAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRF +66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzANBgkq +hkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRCdWKuh+vy1dneVrOfzM4UKLkNl2Bc +EkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTffwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38Fn +SbNd67IJKusm7Xi+fT8r87cmNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i +8b5QZ7dsvfPxH2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE----- + +DigiCert Global Root CA +======================= +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw +HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAw +MDAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 +dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsBCSDMAZOn +TjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97nh6Vfe63SKMI2tavegw5 +BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt43C/dxC//AH2hdmoRBBYMql1GNXRor5H +4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7PT19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y +7vrTC0LUq7dBMtoM1O/4gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQAB +o2MwYTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbRTLtm +8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDQYJKoZIhvcNAQEF +BQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/EsrhMAtudXH/vTBH1jLuG2cenTnmCmr +EbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIt +tep3Sp+dWOIrWcBAI+0tKIJFPnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886 +UAb3LujEV0lsYSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- + +DigiCert High Assurance EV Root CA +================================== +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBsMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSsw +KQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5jZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAw +MFoXDTMxMTExMDAwMDAwMFowbDELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ +MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFu +Y2UgRVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm+9S75S0t +Mqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTWPNt0OKRKzE0lgvdKpVMS +OO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEMxChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3 +MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFBIk5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQ +NAQTXKFx01p8VdteZOE3hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUe +h10aUAsgEsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaAFLE+w2kD+L9HAdSY +JhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3NecnzyIZgYIVyHbIUf4KmeqvxgydkAQ +V8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6zeM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFp +myPInngiK3BD41VHMWEZ71jFhS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkK +mNEVX58Svnw2Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep+OkuE6N36B9K +-----END CERTIFICATE----- + +Certplus Class 2 Primary CA +=========================== +-----BEGIN CERTIFICATE----- +MIIDkjCCAnqgAwIBAgIRAIW9S/PY2uNp9pTXX8OlRCMwDQYJKoZIhvcNAQEFBQAwPTELMAkGA1UE +BhMCRlIxETAPBgNVBAoTCENlcnRwbHVzMRswGQYDVQQDExJDbGFzcyAyIFByaW1hcnkgQ0EwHhcN +OTkwNzA3MTcwNTAwWhcNMTkwNzA2MjM1OTU5WjA9MQswCQYDVQQGEwJGUjERMA8GA1UEChMIQ2Vy +dHBsdXMxGzAZBgNVBAMTEkNsYXNzIDIgUHJpbWFyeSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBANxQltAS+DXSCHh6tlJw/W/uz7kRy1134ezpfgSN1sxvc0NXYKwzCkTsA18cgCSR +5aiRVhKC9+Ar9NuuYS6JEI1rbLqzAr3VNsVINyPi8Fo3UjMXEuLRYE2+L0ER4/YXJQyLkcAbmXuZ +Vg2v7tK8R1fjeUl7NIknJITesezpWE7+Tt9avkGtrAjFGA7v0lPubNCdEgETjdyAYveVqUSISnFO +YFWe2yMZeVYHDD9jC1yw4r5+FfyUM1hBOHTE4Y+L3yasH7WLO7dDWWuwJKZtkIvEcupdM5i3y95e +e++U8Rs+yskhwcWYAqqi9lt3m/V+llU0HGdpwPFC40es/CgcZlUCAwEAAaOBjDCBiTAPBgNVHRME +CDAGAQH/AgEKMAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQU43Mt38sOKAze3bOkynm4jrvoMIkwEQYJ +YIZIAYb4QgEBBAQDAgEGMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly93d3cuY2VydHBsdXMuY29t +L0NSTC9jbGFzczIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCnVM+IRBnL39R/AN9WM2K191EBkOvD +P9GIROkkXe/nFL0gt5o8AP5tn9uQ3Nf0YtaLcF3n5QRIqWh8yfFC82x/xXp8HVGIutIKPidd3i1R +TtMTZGnkLuPT55sJmabglZvOGtd/vjzOUrMRFcEPF80Du5wlFbqidon8BvEY0JNLDnyCt6X09l/+ +7UCmnYR0ObncHoUW2ikbhiMAybuJfm6AiB4vFLQDJKgybwOaRywwvlbGp0ICcBvqQNi6BQNwB6SW +//1IMwrh3KWBkJtN3X3n57LNXMhqlfil9o3EXXgIvnsG1knPGTZQIy4I5p4FTUcY1Rbpsda2ENW7 +l7+ijrRU +-----END CERTIFICATE----- + +DST Root CA X3 +============== +-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/MSQwIgYDVQQK +ExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4X +DTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVowPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1 +cmUgVHJ1c3QgQ28uMRcwFQYDVQQDEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmT +rE4Orz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEqOLl5CjH9 +UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9bxiqKqy69cK3FCxolkHRy +xXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40d +utolucbY38EVAjqr2m7xPi71XAicPNaDaeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQ +MA0GCSqGSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69ikug +dB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXrAvHRAosZy5Q6XkjE +GB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZzR8srzJmwN0jP41ZL9c8PDHIyh8bw +RLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubS +fZGL+T0yjWW06XyxV3bqxbYoOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ +-----END CERTIFICATE----- + +DST ACES CA X6 +============== +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgIQDV6ZCtadt3js2AdWO4YV2TANBgkqhkiG9w0BAQUFADBbMQswCQYDVQQG +EwJVUzEgMB4GA1UEChMXRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QxETAPBgNVBAsTCERTVCBBQ0VT +MRcwFQYDVQQDEw5EU1QgQUNFUyBDQSBYNjAeFw0wMzExMjAyMTE5NThaFw0xNzExMjAyMTE5NTha +MFsxCzAJBgNVBAYTAlVTMSAwHgYDVQQKExdEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdDERMA8GA1UE +CxMIRFNUIEFDRVMxFzAVBgNVBAMTDkRTVCBBQ0VTIENBIFg2MIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAuT31LMmU3HWKlV1j6IR3dma5WZFcRt2SPp/5DgO0PWGSvSMmtWPuktKe1jzI +DZBfZIGxqAgNTNj50wUoUrQBJcWVHAx+PhCEdc/BGZFjz+iokYi5Q1K7gLFViYsx+tC3dr5BPTCa +pCIlF3PoHuLTrCq9Wzgh1SpL11V94zpVvddtawJXa+ZHfAjIgrrep4c9oW24MFbCswKBXy314pow +GCi4ZtPLAZZv6opFVdbgnf9nKxcCpk4aahELfrd755jWjHZvwTvbUJN+5dCOHze4vbrGn2zpfDPy +MjwmR/onJALJfh1biEITajV8fTXpLmaRcpPVMibEdPVTo7NdmvYJywIDAQABo4HIMIHFMA8GA1Ud +EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgHGMB8GA1UdEQQYMBaBFHBraS1vcHNAdHJ1c3Rkc3Qu +Y29tMGIGA1UdIARbMFkwVwYKYIZIAWUDAgEBATBJMEcGCCsGAQUFBwIBFjtodHRwOi8vd3d3LnRy +dXN0ZHN0LmNvbS9jZXJ0aWZpY2F0ZXMvcG9saWN5L0FDRVMtaW5kZXguaHRtbDAdBgNVHQ4EFgQU +CXIGThhDD+XWzMNqizF7eI+og7gwDQYJKoZIhvcNAQEFBQADggEBAKPYjtay284F5zLNAdMEA+V2 +5FYrnJmQ6AgwbN99Pe7lv7UkQIRJ4dEorsTCOlMwiPH1d25Ryvr/ma8kXxug/fKshMrfqfBfBC6t +Fr8hlxCBPeP/h40y3JTlR4peahPJlJU90u7INJXQgNStMgiAVDzgvVJT11J8smk/f3rPanTK+gQq +nExaBqXpIK1FZg9p8d2/6eMyi/rgwYZNcjwu2JN4Cir42NInPRmJX1p7ijvMDNpRrscL9yuwNwXs +vFcj4jjSm2jzVhKIT0J8uDHEtdvkyCE06UgRNe76x5JXxZ805Mf29w4LTJxoeHtxMcfrHuBnQfO3 +oKfN5XozNmr6mis= +-----END CERTIFICATE----- + +TURKTRUST Certificate Services Provider Root 1 +============================================== +-----BEGIN CERTIFICATE----- +MIID+zCCAuOgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBtzE/MD0GA1UEAww2VMOcUktUUlVTVCBF +bGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxMQswCQYDVQQGDAJUUjEP +MA0GA1UEBwwGQU5LQVJBMVYwVAYDVQQKDE0oYykgMjAwNSBUw5xSS1RSVVNUIEJpbGdpIMSwbGV0 +acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLjAeFw0wNTA1MTMx +MDI3MTdaFw0xNTAzMjIxMDI3MTdaMIG3MT8wPQYDVQQDDDZUw5xSS1RSVVNUIEVsZWt0cm9uaWsg +U2VydGlmaWthIEhpem1ldCBTYcSfbGF5xLFjxLFzxLExCzAJBgNVBAYMAlRSMQ8wDQYDVQQHDAZB +TktBUkExVjBUBgNVBAoMTShjKSAyMDA1IFTDnFJLVFJVU1QgQmlsZ2kgxLBsZXRpxZ9pbSB2ZSBC +aWxpxZ9pbSBHw7x2ZW5sacSfaSBIaXptZXRsZXJpIEEuxZ4uMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAylIF1mMD2Bxf3dJ7XfIMYGFbazt0K3gNfUW9InTojAPBxhEqPZW8qZSwu5GX +yGl8hMW0kWxsE2qkVa2kheiVfrMArwDCBRj1cJ02i67L5BuBf5OI+2pVu32Fks66WJ/bMsW9Xe8i +Si9BB35JYbOG7E6mQW6EvAPs9TscyB/C7qju6hJKjRTP8wrgUDn5CDX4EVmt5yLqS8oUBt5CurKZ +8y1UiBAG6uEaPj1nH/vO+3yC6BFdSsG5FOpU2WabfIl9BJpiyelSPJ6c79L1JuTm5Rh8i27fbMx4 +W09ysstcP4wFjdFMjK2Sx+F4f2VsSQZQLJ4ywtdKxnWKWU51b0dewQIDAQABoxAwDjAMBgNVHRME +BTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAV9VX/N5aAWSGk/KEVTCD21F/aAyT8z5Aa9CEKmu46 +sWrv7/hg0Uw2ZkUd82YCdAR7kjCo3gp2D++Vbr3JN+YaDayJSFvMgzbC9UZcWYJWtNX+I7TYVBxE +q8Sn5RTOPEFhfEPmzcSBCYsk+1Ql1haolgxnB2+zUEfjHCQo3SqYpGH+2+oSN7wBGjSFvW5P55Fy +B0SFHljKVETd96y5y4khctuPwGkplyqjrhgjlxxBKot8KsF8kOipKMDTkcatKIdAaLX/7KfS0zgY +nNN9aV3wxqUeJBujR/xpB2jn5Jq07Q+hh4cCzofSSE7hvP/L8XKSRGQDJereW26fyfJOrN3H +-----END CERTIFICATE----- + +TURKTRUST Certificate Services Provider Root 2 +============================================== +-----BEGIN CERTIFICATE----- +MIIEPDCCAySgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBvjE/MD0GA1UEAww2VMOcUktUUlVTVCBF +bGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxMQswCQYDVQQGEwJUUjEP +MA0GA1UEBwwGQW5rYXJhMV0wWwYDVQQKDFRUw5xSS1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUg +QmlsacWfaW0gR8O8dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLiAoYykgS2FzxLFtIDIwMDUwHhcN +MDUxMTA3MTAwNzU3WhcNMTUwOTE2MTAwNzU3WjCBvjE/MD0GA1UEAww2VMOcUktUUlVTVCBFbGVr +dHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxMQswCQYDVQQGEwJUUjEPMA0G +A1UEBwwGQW5rYXJhMV0wWwYDVQQKDFRUw5xSS1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmls +acWfaW0gR8O8dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLiAoYykgS2FzxLFtIDIwMDUwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpNn7DkUNMwxmYCMjHWHtPFoylzkkBH3MOrHUTpvqe +LCDe2JAOCtFp0if7qnefJ1Il4std2NiDUBd9irWCPwSOtNXwSadktx4uXyCcUHVPr+G1QRT0mJKI +x+XlZEdhR3n9wFHxwZnn3M5q+6+1ATDcRhzviuyV79z/rxAc653YsKpqhRgNF8k+v/Gb0AmJQv2g +QrSdiVFVKc8bcLyEVK3BEx+Y9C52YItdP5qtygy/p1Zbj3e41Z55SZI/4PGXJHpsmxcPbe9TmJEr +5A++WXkHeLuXlfSfadRYhwqp48y2WBmfJiGxxFmNskF1wK1pzpwACPI2/z7woQ8arBT9pmAPAgMB +AAGjQzBBMB0GA1UdDgQWBBTZN7NOBf3Zz58SFq62iS/rJTqIHDAPBgNVHQ8BAf8EBQMDBwYAMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAHJglrfJ3NgpXiOFX7KzLXb7iNcX/ntt +Rbj2hWyfIvwqECLsqrkw9qtY1jkQMZkpAL2JZkH7dN6RwRgLn7Vhy506vvWolKMiVW4XSf/SKfE4 +Jl3vpao6+XF75tpYHdN0wgH6PmlYX63LaL4ULptswLbcoCb6dxriJNoaN+BnrdFzgw2lGh1uEpJ+ +hGIAF728JRhX8tepb1mIvDS3LoV4nZbcFMMsilKbloxSZj2GFotHuFEJjOp9zYhys2AzsfAKRO8P +9Qk3iCQOLGsgOqL6EfJANZxEaGM7rDNvY7wsu/LSy3Z9fYjYHcgFHW68lKlmjHdxx/qR+i9Rnuk5 +UrbnBEI= +-----END CERTIFICATE----- + +SwissSign Gold CA - G2 +====================== +-----BEGIN CERTIFICATE----- +MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkNIMRUw +EwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2lnbiBHb2xkIENBIC0gRzIwHhcN +MDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBFMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dp +c3NTaWduIEFHMR8wHQYDVQQDExZTd2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUq +t2/876LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+bbqBHH5C +jCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c6bM8K8vzARO/Ws/BtQpg +vd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqEemA8atufK+ze3gE/bk3lUIbLtK/tREDF +ylqM2tIrfKjuvqblCqoOpd8FUrdVxyJdMmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvR +AiTysybUa9oEVeXBCsdtMDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuend +jIj3o02yMszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69yFGkO +peUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPiaG59je883WX0XaxR +7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxMgI93e2CaHt+28kgeDrpOVG2Y4OGi +GqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUWyV7lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64 +OfPAeGZe6Drn8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov +L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe645R88a7A3hfm +5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczOUYrHUDFu4Up+GC9pWbY9ZIEr +44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOf +Mke6UiI0HTJ6CVanfCU2qT1L2sCCbwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6m +Gu6uLftIdxf+u+yvGPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxp +mo/a77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCChdiDyyJk +vC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid392qgQmwLOM7XdVAyksLf +KzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEppLd6leNcG2mqeSz53OiATIgHQv2ieY2Br +NU0LbbqhPcCT4H8js1WtciVORvnSFu+wZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6Lqj +viOvrv1vA+ACOzB2+httQc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ +-----END CERTIFICATE----- + +SwissSign Silver CA - G2 +======================== +-----BEGIN CERTIFICATE----- +MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCQ0gxFTAT +BgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMB4X +DTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0NlowRzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3 +aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG +9w0BAQEFAAOCAg8AMIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644 +N0MvFz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7brYT7QbNHm ++/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieFnbAVlDLaYQ1HTWBCrpJH +6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH6ATK72oxh9TAtvmUcXtnZLi2kUpCe2Uu +MGoM9ZDulebyzYLs2aFK7PayS+VFheZteJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5h +qAaEuSh6XzjZG6k4sIN/c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5 +FZGkECwJMoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRHHTBs +ROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTfjNFusB3hB48IHpmc +celM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb65i/4z3GcRm25xBWNOHkDRUjvxF3X +CO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUF6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRB +tjpbO8tFnb0cwpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0 +cDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBAHPGgeAn0i0P +4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShpWJHckRE1qTodvBqlYJ7YH39F +kWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L +3XWgwF15kIwb4FDm3jH+mHtwX6WQ2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx +/uNncqCxv1yL5PqZIseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFa +DGi8aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2Xem1ZqSqP +e97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQRdAtq/gsD/KNVV4n+Ssuu +WxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/OMpXEA29MC/HpeZBoNquBYeaoKRlbEwJ +DIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ub +DgEj8Z+7fNzcbBGXJbLytGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u +-----END CERTIFICATE----- + +GeoTrust Primary Certification Authority +======================================== +-----BEGIN CERTIFICATE----- +MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQG +EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMoR2VvVHJ1c3QgUHJpbWFyeSBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjExMjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgx +CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQ +cmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9AWbK7hWN +b6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjAZIVcFU2Ix7e64HXprQU9 +nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE07e9GceBrAqg1cmuXm2bgyxx5X9gaBGge +RwLmnWDiNpcB3841kt++Z8dtd1k7j53WkBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGt +tm/81w7a4DSwDRp35+MImO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJKoZI +hvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ16CePbJC/kRYkRj5K +Ts4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl4b7UVXGYNTq+k+qurUKykG/g/CFN +NWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6KoKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHa +Floxt/m0cYASSJlyc1pZU8FjUjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG +1riR/aYNKxoUAT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk= +-----END CERTIFICATE----- + +thawte Primary Root CA +====================== +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCBqTELMAkGA1UE +BhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2 +aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhv +cml6ZWQgdXNlIG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3 +MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwg +SW5jLjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMv +KGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNVBAMT +FnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCs +oPD7gFnUnMekz52hWXMJEEUMDSxuaPFsW0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ +1CRfBsDMRJSUjQJib+ta3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGc +q/gcfomk6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6Sk/K +aAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94JNqR32HuHUETVPm4p +afs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD +VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XPr87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUF +AAOCAQEAeRHAS7ORtvzw6WfUDW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeE +uzLlQRHAd9mzYJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX +xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2/qxAeeWsEG89 +jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/LHbTY5xZ3Y+m4Q6gLkH3LpVH +z7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7jVaMaA== +-----END CERTIFICATE----- + +VeriSign Class 3 Public Primary Certification Authority - G5 +============================================================ +-----BEGIN CERTIFICATE----- +MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCByjELMAkGA1UE +BhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBO +ZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVk +IHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCB +yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2ln +biBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2lnbiwgSW5jLiAtIEZvciBh +dXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmlt +YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCvJAgIKXo1nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKz +j/i5Vbext0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIzSdhD +Y2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQGBO+QueQA5N06tRn/ +Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+rCpSx4/VBEnkjWNHiDxpg8v+R70r +fk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/ +BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2Uv +Z2lmMCEwHzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy +aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKvMzEzMA0GCSqG +SIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzEp6B4Eq1iDkVwZMXnl2YtmAl+ +X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKE +KQsTb47bDN0lAtukixlE0kF6BWlKWE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiC +Km0oHw0LxOXnGiYZ4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vE +ZV8NhnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq +-----END CERTIFICATE----- + +SecureTrust CA +============== +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBIMQswCQYDVQQG +EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xFzAVBgNVBAMTDlNlY3VyZVRy +dXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIzMTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAe +BgNVBAoTF1NlY3VyZVRydXN0IENvcnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQX +OZEzZum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO0gMdA+9t +DWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIaowW8xQmxSPmjL8xk037uH +GFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b +01k/unK8RCSc43Oz969XL0Imnal0ugBS8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmH +ursCAwEAAaOBnTCBmjATBgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCegJYYj +aHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQAwDQYJ +KoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt36Z3q059c4EVlew3KW+JwULKUBRSu +SceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHf +mbx8IVQr5Fiiu1cprp6poxkmD5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZ +nMUFdAvnZyPSCPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR +3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= +-----END CERTIFICATE----- + +Secure Global CA +================ +-----BEGIN CERTIFICATE----- +MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQG +EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBH +bG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkxMjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEg +MB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwg +Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jx +YDiJiQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa/FHtaMbQ +bqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJjnIFHovdRIWCQtBJwB1g +8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnIHmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYV +HDGA76oYa8J719rO+TMg1fW9ajMtgQT7sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi +0XPnj3pDAgMBAAGjgZ0wgZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCswKaAn +oCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsGAQQBgjcVAQQDAgEA +MA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0LURYD7xh8yOOvaliTFGCRsoTciE6+ +OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXOH0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cn +CDpOGR86p1hcF895P4vkp9MmI50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/5 +3CYNv6ZHdAbYiNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc +f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW +-----END CERTIFICATE----- + +COMODO Certification Authority +============================== +-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCBgTELMAkGA1UE +BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG +A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNVBAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1 +dGhvcml0eTAeFw0wNjEyMDEwMDAwMDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEb +MBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFD +T01PRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3UcEbVASY06m/weaKXTuH ++7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI2GqGd0S7WWaXUF601CxwRM/aN5VCaTww +xHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV +4EajcNxo2f8ESIl33rXp+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA +1KGzqSX+DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5OnKVI +rLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW/zAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6gPKA6hjhodHRwOi8vY3JsLmNvbW9k +b2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOC +AQEAPpiem/Yb6dc5t3iuHXIYSdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CP +OGEIqB6BCsAvIC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ +RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4zJVSk/BwJVmc +IGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5ddBA6+C4OmF4O5MBKgxTMVBbkN ++8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IBZQ== +-----END CERTIFICATE----- + +Network Solutions Certificate Authority +======================================= +-----BEGIN CERTIFICATE----- +MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQG +EwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydOZXR3b3Jr +IFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMx +MjM1OTU5WjBiMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu +MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwzc7MEL7xx +jOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPPOCwGJgl6cvf6UDL4wpPT +aaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rlmGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXT +crA/vGp97Eh/jcOrqnErU2lBUzS1sLnFBgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc +/Qzpf14Dl847ABSHJ3A4qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMB +AAGjgZcwgZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwubmV0c29sc3NsLmNv +bS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3JpdHkuY3JsMA0GCSqGSIb3DQEBBQUA +A4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc86fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q +4LqILPxFzBiwmZVRDuwduIj/h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/ +GGUsyfJj4akH/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv +wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHNpGxlaKFJdlxD +ydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey +-----END CERTIFICATE----- + +WellsSecure Public Root Certificate Authority +============================================= +-----BEGIN CERTIFICATE----- +MIIEvTCCA6WgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBhTELMAkGA1UEBhMCVVMxIDAeBgNVBAoM +F1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxscyBGYXJnbyBCYW5rIE5BMTYw +NAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcN +MDcxMjEzMTcwNzU0WhcNMjIxMjE0MDAwNzU0WjCBhTELMAkGA1UEBhMCVVMxIDAeBgNVBAoMF1dl +bGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYD +VQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDub7S9eeKPCCGeOARBJe+rWxxTkqxtnt3CxC5FlAM1 +iGd0V+PfjLindo8796jE2yljDpFoNoqXjopxaAkH5OjUDk/41itMpBb570OYj7OeUt9tkTmPOL13 +i0Nj67eT/DBMHAGTthP796EfvyXhdDcsHqRePGj4S78NuR4uNuip5Kf4D8uCdXw1LSLWwr8L87T8 +bJVhHlfXBIEyg1J55oNjz7fLY4sR4r1e6/aN7ZVyKLSsEmLpSjPmgzKuBXWVvYSV2ypcm44uDLiB +K0HmOFafSZtsdvqKXfcBeYF8wYNABf5x/Qw/zE5gCQ5lRxAvAcAFP4/4s0HvWkJ+We/SlwxlAgMB +AAGjggE0MIIBMDAPBgNVHRMBAf8EBTADAQH/MDkGA1UdHwQyMDAwLqAsoCqGKGh0dHA6Ly9jcmwu +cGtpLndlbGxzZmFyZ28uY29tL3dzcHJjYS5jcmwwDgYDVR0PAQH/BAQDAgHGMB0GA1UdDgQWBBQm +lRkQ2eihl5H/3BnZtQQ+0nMKajCBsgYDVR0jBIGqMIGngBQmlRkQ2eihl5H/3BnZtQQ+0nMKaqGB +i6SBiDCBhTELMAkGA1UEBhMCVVMxIDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRww +GgYDVQQLDBNXZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMg +Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHmCAQEwDQYJKoZIhvcNAQEFBQADggEBALkVsUSRzCPI +K0134/iaeycNzXK7mQDKfGYZUMbVmO2rvwNa5U3lHshPcZeG1eMd/ZDJPHV3V3p9+N701NX3leZ0 +bh08rnyd2wIDBSxxSyU+B+NemvVmFymIGjifz6pBA4SXa5M4esowRBskRDPQ5NHcKDj0E0M1NSlj +qHyita04pO2t/caaH/+Xc/77szWnk4bGdpEA5qxRFsQnMlzbc9qlk1eOPm01JghZ1edE13YgY+es +E2fDbbFwRnzVlhE9iW9dqKHrjQrawx0zbKPqZxmamX9LPYNRKh3KL4YMon4QLSvUFpULB6ouFJJJ +tylv2G0xffX8oRAHh84vWdw+WNs= +-----END CERTIFICATE----- + +COMODO ECC Certification Authority +================================== +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTELMAkGA1UEBhMC +R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE +ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwHhcNMDgwMzA2MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0Ix +GzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRo +b3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSRFtSrYpn1PlILBs5BAH+X +4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0JcfRK9ChQtP6IHG4/bC8vCVlbpVsLM5ni +wz2J+Wos77LTBumjQjBAMB0GA1UdDgQWBBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VG +FAkK+qDmfQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdvGDeA +U/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +IGC/A +===== +-----BEGIN CERTIFICATE----- +MIIEAjCCAuqgAwIBAgIFORFFEJQwDQYJKoZIhvcNAQEFBQAwgYUxCzAJBgNVBAYTAkZSMQ8wDQYD +VQQIEwZGcmFuY2UxDjAMBgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQTS9TR0ROMQ4wDAYDVQQLEwVE +Q1NTSTEOMAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2LmZy +MB4XDTAyMTIxMzE0MjkyM1oXDTIwMTAxNzE0MjkyMlowgYUxCzAJBgNVBAYTAkZSMQ8wDQYDVQQI +EwZGcmFuY2UxDjAMBgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQTS9TR0ROMQ4wDAYDVQQLEwVEQ1NT +STEOMAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2LmZyMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsh/R0GLFMzvABIaIs9z4iPf930Pfeo2aSVz2 +TqrMHLmh6yeJ8kbpO0px1R2OLc/mratjUMdUC24SyZA2xtgv2pGqaMVy/hcKshd+ebUyiHDKcMCW +So7kVc0dJ5S/znIq7Fz5cyD+vfcuiWe4u0dzEvfRNWk68gq5rv9GQkaiv6GFGvm/5P9JhfejcIYy +HF2fYPepraX/z9E0+X1bF8bc1g4oa8Ld8fUzaJ1O/Id8NhLWo4DoQw1VYZTqZDdH6nfK0LJYBcNd +frGoRpAxVs5wKpayMLh35nnAvSk7/ZR3TL0gzUEl4C7HG7vupARB0l2tEmqKm0f7yd1GQOGdPDPQ +tQIDAQABo3cwdTAPBgNVHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBRjAVBgNVHSAEDjAMMAoGCCqB +egF5AQEBMB0GA1UdDgQWBBSjBS8YYFDCiQrdKyFP/45OqDAxNjAfBgNVHSMEGDAWgBSjBS8YYFDC +iQrdKyFP/45OqDAxNjANBgkqhkiG9w0BAQUFAAOCAQEABdwm2Pp3FURo/C9mOnTgXeQp/wYHE4RK +q89toB9RlPhJy3Q2FLwV3duJL92PoF189RLrn544pEfMs5bZvpwlqwN+Mw+VgQ39FuCIvjfwbF3Q +MZsyK10XZZOYYLxuj7GoPB7ZHPOpJkL5ZB3C55L29B5aqhlSXa/oovdgoPaN8In1buAKBQGVyYsg +Crpa/JosPL3Dt8ldeCUFP1YUmwza+zpI/pdpXsoQhvdOlgQITeywvl3cO45Pwf2aNjSaTFR+FwNI +lQgRHAdvhQh+XU3Endv7rs6y0bO4g2wdsrN58dhwmX7wEwLOXt1R0982gaEbeC9xs/FZTEYYKKuF +0mBWWg== +-----END CERTIFICATE----- + +Security Communication EV RootCA1 +================================= +-----BEGIN CERTIFICATE----- +MIIDfTCCAmWgAwIBAgIBADANBgkqhkiG9w0BAQUFADBgMQswCQYDVQQGEwJKUDElMCMGA1UEChMc +U0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEqMCgGA1UECxMhU2VjdXJpdHkgQ29tbXVuaWNh +dGlvbiBFViBSb290Q0ExMB4XDTA3MDYwNjAyMTIzMloXDTM3MDYwNjAyMTIzMlowYDELMAkGA1UE +BhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKjAoBgNVBAsTIVNl +Y3VyaXR5IENvbW11bmljYXRpb24gRVYgUm9vdENBMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBALx/7FebJOD+nLpCeamIivqA4PUHKUPqjgo0No0c+qe1OXj/l3X3L+SqawSERMqm4miO +/VVQYg+kcQ7OBzgtQoVQrTyWb4vVog7P3kmJPdZkLjjlHmy1V4qe70gOzXppFodEtZDkBp2uoQSX +WHnvIEqCa4wiv+wfD+mEce3xDuS4GBPMVjZd0ZoeUWs5bmB2iDQL87PRsJ3KYeJkHcFGB7hj3R4z +ZbOOCVVSPbW9/wfrrWFVGCypaZhKqkDFMxRldAD5kd6vA0jFQFTcD4SQaCDFkpbcLuUCRarAX1T4 +bepJz11sS6/vmsJWXMY1VkJqMF/Cq/biPT+zyRGPMUzXn0kCAwEAAaNCMEAwHQYDVR0OBBYEFDVK +9U2vP9eCOKyrcWUXdYydVZPmMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqG +SIb3DQEBBQUAA4IBAQCoh+ns+EBnXcPBZsdAS5f8hxOQWsTvoMpfi7ent/HWtWS3irO4G8za+6xm +iEHO6Pzk2x6Ipu0nUBsCMCRGef4Eh3CXQHPRwMFXGZpppSeZq51ihPZRwSzJIxXYKLerJRO1RuGG +Av8mjMSIkh1W/hln8lXkgKNrnKt34VFxDSDbEJrbvXZ5B3eZKK2aXtqxT0QsNY6llsf9g/BYxnnW +mHyojf6GPgcWkuF75x3sM3Z+Qi5KhfmRiWiEA4Glm5q+4zfFVKtWOxgtQaQM+ELbmaDgcm+7XeEW +T1MKZPlO9L9OVL14bIjqv5wTJMJwaaJ/D8g8rQjJsJhAoyrniIPtd490 +-----END CERTIFICATE----- + +OISTE WISeKey Global Root GA CA +=============================== +-----BEGIN CERTIFICATE----- +MIID8TCCAtmgAwIBAgIQQT1yx/RrH4FDffHSKFTfmjANBgkqhkiG9w0BAQUFADCBijELMAkGA1UE +BhMCQ0gxEDAOBgNVBAoTB1dJU2VLZXkxGzAZBgNVBAsTEkNvcHlyaWdodCAoYykgMjAwNTEiMCAG +A1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBH +bG9iYWwgUm9vdCBHQSBDQTAeFw0wNTEyMTExNjAzNDRaFw0zNzEyMTExNjA5NTFaMIGKMQswCQYD +VQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEbMBkGA1UECxMSQ29weXJpZ2h0IChjKSAyMDA1MSIw +IAYDVQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5 +IEdsb2JhbCBSb290IEdBIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy0+zAJs9 +Nt350UlqaxBJH+zYK7LG+DKBKUOVTJoZIyEVRd7jyBxRVVuuk+g3/ytr6dTqvirdqFEr12bDYVxg +Asj1znJ7O7jyTmUIms2kahnBAbtzptf2w93NvKSLtZlhuAGio9RN1AU9ka34tAhxZK9w8RxrfvbD +d50kc3vkDIzh2TbhmYsFmQvtRTEJysIA2/dyoJaqlYfQjse2YXMNdmaM3Bu0Y6Kff5MTMPGhJ9vZ +/yxViJGg4E8HsChWjBgbl0SOid3gF27nKu+POQoxhILYQBRJLnpB5Kf+42TMwVlxSywhp1t94B3R +LoGbw9ho972WG6xwsRYUC9tguSYBBQIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUswN+rja8sHnR3JQmthG+IbJphpQwEAYJKwYBBAGCNxUBBAMCAQAwDQYJ +KoZIhvcNAQEFBQADggEBAEuh/wuHbrP5wUOxSPMowB0uyQlB+pQAHKSkq0lPjz0e701vvbyk9vIm +MMkQyh2I+3QZH4VFvbBsUfk2ftv1TDI6QU9bR8/oCy22xBmddMVHxjtqD6wU2zz0c5ypBd8A3HR4 ++vg1YFkCExh8vPtNsCBtQ7tgMHpnM1zFmdH4LTlSc/uMqpclXHLZCB6rTjzjgTGfA6b7wP4piFXa +hNVQA7bihKOmNqoROgHhGEvWRGizPflTdISzRpFGlgC3gCy24eMQ4tui5yiPAZZiFj4A4xylNoEY +okxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ/L7fCg0= +-----END CERTIFICATE----- + +Microsec e-Szigno Root CA +========================= +-----BEGIN CERTIFICATE----- +MIIHqDCCBpCgAwIBAgIRAMy4579OKRr9otxmpRwsDxEwDQYJKoZIhvcNAQEFBQAwcjELMAkGA1UE +BhMCSFUxETAPBgNVBAcTCEJ1ZGFwZXN0MRYwFAYDVQQKEw1NaWNyb3NlYyBMdGQuMRQwEgYDVQQL +EwtlLVN6aWdubyBDQTEiMCAGA1UEAxMZTWljcm9zZWMgZS1Temlnbm8gUm9vdCBDQTAeFw0wNTA0 +MDYxMjI4NDRaFw0xNzA0MDYxMjI4NDRaMHIxCzAJBgNVBAYTAkhVMREwDwYDVQQHEwhCdWRhcGVz +dDEWMBQGA1UEChMNTWljcm9zZWMgTHRkLjEUMBIGA1UECxMLZS1Temlnbm8gQ0ExIjAgBgNVBAMT +GU1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQDtyADVgXvNOABHzNuEwSFpLHSQDCHZU4ftPkNEU6+r+ICbPHiN1I2uuO/TEdyB5s87lozWbxXG +d36hL+BfkrYn13aaHUM86tnsL+4582pnS4uCzyL4ZVX+LMsvfUh6PXX5qqAnu3jCBspRwn5mS6/N +oqdNAoI/gqyFxuEPkEeZlApxcpMqyabAvjxWTHOSJ/FrtfX9/DAFYJLG65Z+AZHCabEeHXtTRbjc +QR/Ji3HWVBTji1R4P770Yjtb9aPs1ZJ04nQw7wHb4dSrmZsqa/i9phyGI0Jf7Enemotb9HI6QMVJ +PqW+jqpx62z69Rrkav17fVVA71hu5tnVvCSrwe+3AgMBAAGjggQ3MIIEMzBnBggrBgEFBQcBAQRb +MFkwKAYIKwYBBQUHMAGGHGh0dHBzOi8vcmNhLmUtc3ppZ25vLmh1L29jc3AwLQYIKwYBBQUHMAKG +IWh0dHA6Ly93d3cuZS1zemlnbm8uaHUvUm9vdENBLmNydDAPBgNVHRMBAf8EBTADAQH/MIIBcwYD +VR0gBIIBajCCAWYwggFiBgwrBgEEAYGoGAIBAQEwggFQMCgGCCsGAQUFBwIBFhxodHRwOi8vd3d3 +LmUtc3ppZ25vLmh1L1NaU1ovMIIBIgYIKwYBBQUHAgIwggEUHoIBEABBACAAdABhAG4A+gBzAO0A +dAB2AOEAbgB5ACAA6QByAHQAZQBsAG0AZQB6AOkAcwDpAGgAZQB6ACAA6QBzACAAZQBsAGYAbwBn +AGEAZADhAHMA4QBoAG8AegAgAGEAIABTAHoAbwBsAGcA4QBsAHQAYQB0APMAIABTAHoAbwBsAGcA +4QBsAHQAYQB0AOEAcwBpACAAUwB6AGEAYgDhAGwAeQB6AGEAdABhACAAcwB6AGUAcgBpAG4AdAAg +AGsAZQBsAGwAIABlAGwAagDhAHIAbgBpADoAIABoAHQAdABwADoALwAvAHcAdwB3AC4AZQAtAHMA +egBpAGcAbgBvAC4AaAB1AC8AUwBaAFMAWgAvMIHIBgNVHR8EgcAwgb0wgbqggbeggbSGIWh0dHA6 +Ly93d3cuZS1zemlnbm8uaHUvUm9vdENBLmNybIaBjmxkYXA6Ly9sZGFwLmUtc3ppZ25vLmh1L0NO +PU1pY3Jvc2VjJTIwZS1Temlnbm8lMjBSb290JTIwQ0EsT1U9ZS1Temlnbm8lMjBDQSxPPU1pY3Jv +c2VjJTIwTHRkLixMPUJ1ZGFwZXN0LEM9SFU/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDtiaW5h +cnkwDgYDVR0PAQH/BAQDAgEGMIGWBgNVHREEgY4wgYuBEGluZm9AZS1zemlnbm8uaHWkdzB1MSMw +IQYDVQQDDBpNaWNyb3NlYyBlLVN6aWduw7MgUm9vdCBDQTEWMBQGA1UECwwNZS1TemlnbsOzIEhT +WjEWMBQGA1UEChMNTWljcm9zZWMgS2Z0LjERMA8GA1UEBxMIQnVkYXBlc3QxCzAJBgNVBAYTAkhV +MIGsBgNVHSMEgaQwgaGAFMegSXUWYYTbMUuE0vE3QJDvTtz3oXakdDByMQswCQYDVQQGEwJIVTER +MA8GA1UEBxMIQnVkYXBlc3QxFjAUBgNVBAoTDU1pY3Jvc2VjIEx0ZC4xFDASBgNVBAsTC2UtU3pp +Z25vIENBMSIwIAYDVQQDExlNaWNyb3NlYyBlLVN6aWdubyBSb290IENBghEAzLjnv04pGv2i3Gal +HCwPETAdBgNVHQ4EFgQUx6BJdRZhhNsxS4TS8TdAkO9O3PcwDQYJKoZIhvcNAQEFBQADggEBANMT +nGZjWS7KXHAM/IO8VbH0jgdsZifOwTsgqRy7RlRw7lrMoHfqaEQn6/Ip3Xep1fvj1KcExJW4C+FE +aGAHQzAxQmHl7tnlJNUb3+FKG6qfx1/4ehHqE5MAyopYse7tDk2016g2JnzgOsHVV4Lxdbb9iV/a +86g4nzUGCM4ilb7N1fy+W955a9x6qWVmvrElWl/tftOsRm1M9DKHtCAE4Gx4sHfRhUZLphK3dehK +yVZs15KrnfVJONJPU+NVkBHbmJbGSfI+9J8b4PeI3CVimUTYc78/MPMMNz7UwiiAc7EBt51alhQB +S6kRnSlqLtBdgcDPsiBDxwPgN05dCtxZICU= +-----END CERTIFICATE----- + +Certigna +======== +-----BEGIN CERTIFICATE----- +MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNVBAYTAkZSMRIw +EAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4XDTA3MDYyOTE1MTMwNVoXDTI3 +MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwI +Q2VydGlnbmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7q +XOEm7RFHYeGifBZ4QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyH +GxnygQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbwzBfsV1/p +ogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q130yGLMLLGq/jj8UEYkg +DncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKf +Irjxwo1p3Po6WAbfAgMBAAGjgbwwgbkwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQ +tCRZvgHyUtVF9lo53BEwZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJ +BgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzjAQ/J +SP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG9w0BAQUFAAOCAQEA +hQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8hbV6lUmPOEvjvKtpv6zf+EwLHyzs+ +ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFncfca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1klu +PBS1xp81HlDQwY9qcEQCYsuuHWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY +1gkIl2PlwS6wt0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw +WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== +-----END CERTIFICATE----- + +AC Ra\xC3\xADz Certic\xC3\xA1mara S.A. +====================================== +-----BEGIN CERTIFICATE----- +MIIGZjCCBE6gAwIBAgIPB35Sk3vgFeNX8GmMy+wMMA0GCSqGSIb3DQEBBQUAMHsxCzAJBgNVBAYT +AkNPMUcwRQYDVQQKDD5Tb2NpZWRhZCBDYW1lcmFsIGRlIENlcnRpZmljYWNpw7NuIERpZ2l0YWwg +LSBDZXJ0aWPDoW1hcmEgUy5BLjEjMCEGA1UEAwwaQUMgUmHDrXogQ2VydGljw6FtYXJhIFMuQS4w +HhcNMDYxMTI3MjA0NjI5WhcNMzAwNDAyMjE0MjAyWjB7MQswCQYDVQQGEwJDTzFHMEUGA1UECgw+ +U29jaWVkYWQgQ2FtZXJhbCBkZSBDZXJ0aWZpY2FjacOzbiBEaWdpdGFsIC0gQ2VydGljw6FtYXJh +IFMuQS4xIzAhBgNVBAMMGkFDIFJhw616IENlcnRpY8OhbWFyYSBTLkEuMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAq2uJo1PMSCMI+8PPUZYILrgIem08kBeGqentLhM0R7LQcNzJPNCN +yu5LF6vQhbCnIwTLqKL85XXbQMpiiY9QngE9JlsYhBzLfDe3fezTf3MZsGqy2IiKLUV0qPezuMDU +2s0iiXRNWhU5cxh0T7XrmafBHoi0wpOQY5fzp6cSsgkiBzPZkc0OnB8OIMfuuzONj8LSWKdf/WU3 +4ojC2I+GdV75LaeHM/J4Ny+LvB2GNzmxlPLYvEqcgxhaBvzz1NS6jBUJJfD5to0EfhcSM2tXSExP +2yYe68yQ54v5aHxwD6Mq0Do43zeX4lvegGHTgNiRg0JaTASJaBE8rF9ogEHMYELODVoqDA+bMMCm +8Ibbq0nXl21Ii/kDwFJnmxL3wvIumGVC2daa49AZMQyth9VXAnow6IYm+48jilSH5L887uvDdUhf +HjlvgWJsxS3EF1QZtzeNnDeRyPYL1epjb4OsOMLzP96a++EjYfDIJss2yKHzMI+ko6Kh3VOz3vCa +Mh+DkXkwwakfU5tTohVTP92dsxA7SH2JD/ztA/X7JWR1DhcZDY8AFmd5ekD8LVkH2ZD6mq093ICK +5lw1omdMEWux+IBkAC1vImHFrEsm5VoQgpukg3s0956JkSCXjrdCx2bD0Omk1vUgjcTDlaxECp1b +czwmPS9KvqfJpxAe+59QafMCAwEAAaOB5jCB4zAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE +AwIBBjAdBgNVHQ4EFgQU0QnQ6dfOeXRU+Tows/RtLAMDG2gwgaAGA1UdIASBmDCBlTCBkgYEVR0g +ADCBiTArBggrBgEFBQcCARYfaHR0cDovL3d3dy5jZXJ0aWNhbWFyYS5jb20vZHBjLzBaBggrBgEF +BQcCAjBOGkxMaW1pdGFjaW9uZXMgZGUgZ2FyYW507WFzIGRlIGVzdGUgY2VydGlmaWNhZG8gc2Ug +cHVlZGVuIGVuY29udHJhciBlbiBsYSBEUEMuMA0GCSqGSIb3DQEBBQUAA4ICAQBclLW4RZFNjmEf +AygPU3zmpFmps4p6xbD/CHwso3EcIRNnoZUSQDWDg4902zNc8El2CoFS3UnUmjIz75uny3XlesuX +EpBcunvFm9+7OSPI/5jOCk0iAUgHforA1SBClETvv3eiiWdIG0ADBaGJ7M9i4z0ldma/Jre7Ir5v +/zlXdLp6yQGVwZVR6Kss+LGGIOk/yzVb0hfpKv6DExdA7ohiZVvVO2Dpezy4ydV/NgIlqmjCMRW3 +MGXrfx1IebHPOeJCgBbT9ZMj/EyXyVo3bHwi2ErN0o42gzmRkBDI8ck1fj+404HGIGQatlDCIaR4 +3NAvO2STdPCWkPHv+wlaNECW8DYSwaN0jJN+Qd53i+yG2dIPPy3RzECiiWZIHiCznCNZc6lEc7wk +eZBWN7PGKX6jD/EpOe9+XCgycDWs2rjIdWb8m0w5R44bb5tNAlQiM+9hup4phO9OSzNHdpdqy35f +/RWmnkJDW2ZaiogN9xa5P1FlK2Zqi9E4UqLWRhH6/JocdJ6PlwsCT2TG9WjTSy3/pDceiz+/RL5h +RqGEPQgnTIEgd4kI6mdAXmwIUV80WoyWaM3X94nCHNMyAK9Sy9NgWyo6R35rMDOhYil/SrnhLecU +Iw4OGEfhefwVVdCx/CVxY3UzHCMrr1zZ7Ud3YA47Dx7SwNxkBYn8eNZcLCZDqQ== +-----END CERTIFICATE----- + +TC TrustCenter Class 2 CA II +============================ +-----BEGIN CERTIFICATE----- +MIIEqjCCA5KgAwIBAgIOLmoAAQACH9dSISwRXDswDQYJKoZIhvcNAQEFBQAwdjELMAkGA1UEBhMC +REUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNVBAsTGVRDIFRydXN0Q2VudGVy +IENsYXNzIDIgQ0ExJTAjBgNVBAMTHFRDIFRydXN0Q2VudGVyIENsYXNzIDIgQ0EgSUkwHhcNMDYw +MTEyMTQzODQzWhcNMjUxMjMxMjI1OTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1 +c3RDZW50ZXIgR21iSDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMiBDQTElMCMGA1UE +AxMcVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMiBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAKuAh5uO8MN8h9foJIIRszzdQ2Lu+MNF2ujhoF/RKrLqk2jftMjWQ+nEdVl//OEd+DFw +IxuInie5e/060smp6RQvkL4DUsFJzfb95AhmC1eKokKguNV/aVyQMrKXDcpK3EY+AlWJU+MaWss2 +xgdW94zPEfRMuzBwBJWl9jmM/XOBCH2JXjIeIqkiRUuwZi4wzJ9l/fzLganx4Duvo4bRierERXlQ +Xa7pIXSSTYtZgo+U4+lK8edJsBTj9WLL1XK9H7nSn6DNqPoByNkN39r8R52zyFTfSUrxIan+GE7u +SNQZu+995OKdy1u2bv/jzVrndIIFuoAlOMvkaZ6vQaoahPUCAwEAAaOCATQwggEwMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTjq1RMgKHbVkO3kUrL84J6E1wIqzCB +7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRydXN0Y2VudGVyLmRlL2NybC92Mi90 +Y19jbGFzc18yX2NhX0lJLmNybIaBn2xkYXA6Ly93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBU +cnVzdENlbnRlciUyMENsYXNzJTIwMiUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21i +SCxPVT1yb290Y2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u +TGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEAjNfffu4bgBCzg/XbEeprS6iSGNn3Bzn1LL4G +dXpoUxUc6krtXvwjshOg0wn/9vYua0Fxec3ibf2uWWuFHbhOIprtZjluS5TmVfwLG4t3wVMTZonZ +KNaL80VKY7f9ewthXbhtvsPcW3nS7Yblok2+XnR8au0WOB9/WIFaGusyiC2y8zl3gK9etmF1Kdsj +TYjKUCjLhdLTEKJZbtOTVAB6okaVhgWcqRmY5TFyDADiZ9lA4CQze28suVyrZZ0srHbqNZn1l7kP +JOzHdiEoZa5X6AeIdUpWoNIFOqTmjZKILPPy4cHGYdtBxceb9w4aUUXCYWvcZCcXjFq32nQozZfk +vQ== +-----END CERTIFICATE----- + +TC TrustCenter Class 3 CA II +============================ +-----BEGIN CERTIFICATE----- +MIIEqjCCA5KgAwIBAgIOSkcAAQAC5aBd1j8AUb8wDQYJKoZIhvcNAQEFBQAwdjELMAkGA1UEBhMC +REUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNVBAsTGVRDIFRydXN0Q2VudGVy +IENsYXNzIDMgQ0ExJTAjBgNVBAMTHFRDIFRydXN0Q2VudGVyIENsYXNzIDMgQ0EgSUkwHhcNMDYw +MTEyMTQ0MTU3WhcNMjUxMjMxMjI1OTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1 +c3RDZW50ZXIgR21iSDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQTElMCMGA1UE +AxMcVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBALTgu1G7OVyLBMVMeRwjhjEQY0NVJz/GRcekPewJDRoeIMJWHt4bNwcwIi9v8Qbxq63W +yKthoy9DxLCyLfzDlml7forkzMA5EpBCYMnMNWju2l+QVl/NHE1bWEnrDgFPZPosPIlY2C8u4rBo +6SI7dYnWRBpl8huXJh0obazovVkdKyT21oQDZogkAHhg8fir/gKya/si+zXmFtGt9i4S5Po1auUZ +uV3bOx4a+9P/FRQI2AlqukWdFHlgfa9Aigdzs5OW03Q0jTo3Kd5c7PXuLjHCINy+8U9/I1LZW+Jk +2ZyqBwi1Rb3R0DHBq1SfqdLDYmAD8bs5SpJKPQq5ncWg/jcCAwEAAaOCATQwggEwMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTUovyfs8PYA9NXXAek0CSnwPIA1DCB +7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRydXN0Y2VudGVyLmRlL2NybC92Mi90 +Y19jbGFzc18zX2NhX0lJLmNybIaBn2xkYXA6Ly93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBU +cnVzdENlbnRlciUyMENsYXNzJTIwMyUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21i +SCxPVT1yb290Y2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u +TGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEANmDkcPcGIEPZIxpC8vijsrlNirTzwppVMXzE +O2eatN9NDoqTSheLG43KieHPOh6sHfGcMrSOWXaiQYUlN6AT0PV8TtXqluJucsG7Kv5sbviRmEb8 +yRtXW+rIGjs/sFGYPAfaLFkB2otE6OF0/ado3VS6g0bsyEa1+K+XwDsJHI/OcpY9M1ZwvJbL2NV9 +IJqDnxrcOfHFcqMRA/07QlIp2+gB95tejNaNhk4Z+rwcvsUhpYeeeC422wlxo3I0+GzjBgnyXlal +092Y+tTmBvTwtiBjS+opvaqCZh77gaqnN60TGOaSw4HBM7uIHqHn4rS9MWwOUT1v+5ZWgOI2F9Hc +5A== +-----END CERTIFICATE----- + +TC TrustCenter Universal CA I +============================= +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIOHaIAAQAC7LdggHiNtgYwDQYJKoZIhvcNAQEFBQAweTELMAkGA1UEBhMC +REUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxJDAiBgNVBAsTG1RDIFRydXN0Q2VudGVy +IFVuaXZlcnNhbCBDQTEmMCQGA1UEAxMdVEMgVHJ1c3RDZW50ZXIgVW5pdmVyc2FsIENBIEkwHhcN +MDYwMzIyMTU1NDI4WhcNMjUxMjMxMjI1OTU5WjB5MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMg +VHJ1c3RDZW50ZXIgR21iSDEkMCIGA1UECxMbVEMgVHJ1c3RDZW50ZXIgVW5pdmVyc2FsIENBMSYw +JAYDVQQDEx1UQyBUcnVzdENlbnRlciBVbml2ZXJzYWwgQ0EgSTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAKR3I5ZEr5D0MacQ9CaHnPM42Q9e3s9B6DGtxnSRJJZ4Hgmgm5qVSkr1YnwC +qMqs+1oEdjneX/H5s7/zA1hV0qq34wQi0fiU2iIIAI3TfCZdzHd55yx4Oagmcw6iXSVphU9VDprv +xrlE4Vc93x9UIuVvZaozhDrzznq+VZeujRIPFDPiUHDDSYcTvFHe15gSWu86gzOSBnWLknwSaHtw +ag+1m7Z3W0hZneTvWq3zwZ7U10VOylY0Ibw+F1tvdwxIAUMpsN0/lm7mlaoMwCC2/T42J5zjXM9O +gdwZu5GQfezmlwQek8wiSdeXhrYTCjxDI3d+8NzmzSQfO4ObNDqDNOMCAwEAAaNjMGEwHwYDVR0j +BBgwFoAUkqR1LKSevoFE63n8isWVpesQdXMwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFJKkdSyknr6BROt5/IrFlaXrEHVzMA0GCSqGSIb3DQEBBQUAA4IBAQAo0uCG +1eb4e/CX3CJrO5UUVg8RMKWaTzqwOuAGy2X17caXJ/4l8lfmXpWMPmRgFVp/Lw0BxbFg/UU1z/Cy +vwbZ71q+s2IhtNerNXxTPqYn8aEt2hojnczd7Dwtnic0XQ/CNnm8yUpiLe1r2X1BQ3y2qsrtYbE3 +ghUJGooWMNjsydZHcnhLEEYUjl8Or+zHL6sQ17bxbuyGssLoDZJz3KL0Dzq/YSMQiZxIQG5wALPT +ujdEWBF6AmqI8Dc08BnprNRlc/ZpjGSUOnmFKbAWKwyCPwacx/0QK54PLLae4xW/2TYcuiUaUj0a +7CIMHOCkoj3w6DnPgcB77V0fb8XQC9eY +-----END CERTIFICATE----- + +Deutsche Telekom Root CA 2 +========================== +-----BEGIN CERTIFICATE----- +MIIDnzCCAoegAwIBAgIBJjANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJERTEcMBoGA1UEChMT +RGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxlU2VjIFRydXN0IENlbnRlcjEjMCEG +A1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290IENBIDIwHhcNOTkwNzA5MTIxMTAwWhcNMTkwNzA5 +MjM1OTAwWjBxMQswCQYDVQQGEwJERTEcMBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0G +A1UECxMWVC1UZWxlU2VjIFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBS +b290IENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrC6M14IspFLEUha88EOQ5 +bzVdSq7d6mGNlUn0b2SjGmBmpKlAIoTZ1KXleJMOaAGtuU1cOs7TuKhCQN/Po7qCWWqSG6wcmtoI +KyUn+WkjR/Hg6yx6m/UTAtB+NHzCnjwAWav12gz1MjwrrFDa1sPeg5TKqAyZMg4ISFZbavva4VhY +AUlfckE8FQYBjl2tqriTtM2e66foai1SNNs671x1Udrb8zH57nGYMsRUFUQM+ZtV7a3fGAigo4aK +Se5TBY8ZTNXeWHmb0mocQqvF1afPaA+W5OFhmHZhyJF81j4A4pFQh+GdCuatl9Idxjp9y7zaAzTV +jlsB9WoHtxa2bkp/AgMBAAGjQjBAMB0GA1UdDgQWBBQxw3kbuvVT1xfgiXotF2wKsyudMzAPBgNV +HRMECDAGAQH/AgEFMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAlGRZrTlk5ynr +E/5aw4sTV8gEJPB0d8Bg42f76Ymmg7+Wgnxu1MM9756AbrsptJh6sTtU6zkXR34ajgv8HzFZMQSy +zhfzLMdiNlXiItiJVbSYSKpk+tYcNthEeFpaIzpXl/V6ME+un2pMSyuOoAPjPuCp1NJ70rOo4nI8 +rZ7/gFnkm0W09juwzTkZmDLl6iFhkOQxIY40sfcvNUqFENrnijchvllj4PKFiDFT1FQUhXB59C4G +dyd1Lx+4ivn+xbrYNuSD7Odlt79jWvNGr4GUN9RBjNYj1h7P9WgbRGOiWrqnNVmh5XAFmw4jV5mU +Cm26OWMohpLzGITY+9HPBVZkVw== +-----END CERTIFICATE----- + +ComSign Secured CA +================== +-----BEGIN CERTIFICATE----- +MIIDqzCCApOgAwIBAgIRAMcoRwmzuGxFjB36JPU2TukwDQYJKoZIhvcNAQEFBQAwPDEbMBkGA1UE +AxMSQ29tU2lnbiBTZWN1cmVkIENBMRAwDgYDVQQKEwdDb21TaWduMQswCQYDVQQGEwJJTDAeFw0w +NDAzMjQxMTM3MjBaFw0yOTAzMTYxNTA0NTZaMDwxGzAZBgNVBAMTEkNvbVNpZ24gU2VjdXJlZCBD +QTEQMA4GA1UEChMHQ29tU2lnbjELMAkGA1UEBhMCSUwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDGtWhfHZQVw6QIVS3joFd67+l0Kru5fFdJGhFeTymHDEjWaueP1H5XJLkGieQcPOqs +49ohgHMhCu95mGwfCP+hUH3ymBvJVG8+pSjsIQQPRbsHPaHA+iqYHU4Gk/v1iDurX8sWv+bznkqH +7Rnqwp9D5PGBpX8QTz7RSmKtUxvLg/8HZaWSLWapW7ha9B20IZFKF3ueMv5WJDmyVIRD9YTC2LxB +kMyd1mja6YJQqTtoz7VdApRgFrFD2UNd3V2Hbuq7s8lr9gOUCXDeFhF6K+h2j0kQmHe5Y1yLM5d1 +9guMsqtb3nQgJT/j8xH5h2iGNXHDHYwt6+UarA9z1YJZQIDTAgMBAAGjgacwgaQwDAYDVR0TBAUw +AwEB/zBEBgNVHR8EPTA7MDmgN6A1hjNodHRwOi8vZmVkaXIuY29tc2lnbi5jby5pbC9jcmwvQ29t +U2lnblNlY3VyZWRDQS5jcmwwDgYDVR0PAQH/BAQDAgGGMB8GA1UdIwQYMBaAFMFL7XC29z58ADsA +j8c+DkWfHl3sMB0GA1UdDgQWBBTBS+1wtvc+fAA7AI/HPg5Fnx5d7DANBgkqhkiG9w0BAQUFAAOC +AQEAFs/ukhNQq3sUnjO2QiBq1BW9Cav8cujvR3qQrFHBZE7piL1DRYHjZiM/EoZNGeQFsOY3wo3a +BijJD4mkU6l1P7CW+6tMM1X5eCZGbxs2mPtCdsGCuY7e+0X5YxtiOzkGynd6qDwJz2w2PQ8KRUtp +FhpFfTMDZflScZAmlaxMDPWLkz/MdXSFmLr/YnpNH4n+rr2UAJm/EaXc4HnFFgt9AmEd6oX5AhVP +51qJThRv4zdLhfXBPGHg/QVBspJ/wx2g0K5SZGBrGMYmnNj1ZOQ2GmKfig8+/21OGVZOIJFsnzQz +OjRXUDpvgV4GxvU+fE6OK85lBi5d0ipTdF7Tbieejw== +-----END CERTIFICATE----- + +Cybertrust Global Root +====================== +-----BEGIN CERTIFICATE----- +MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYGA1UEChMPQ3li +ZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBSb290MB4XDTA2MTIxNTA4 +MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQD +ExZDeWJlcnRydXN0IEdsb2JhbCBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA ++Mi8vRRQZhP/8NN57CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW +0ozSJ8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2yHLtgwEZL +AfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iPt3sMpTjr3kfb1V05/Iin +89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNzFtApD0mpSPCzqrdsxacwOUBdrsTiXSZT +8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAYXSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2 +MDSgMqAwhi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3JsMB8G +A1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUAA4IBAQBW7wojoFRO +lZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMjWqd8BfP9IjsO0QbE2zZMcwSO5bAi +5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUxXOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2 +hO0j9n0Hq0V+09+zv+mKts2oomcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+T +X3EJIrduPuocA06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW +WL1WMRJOEcgh4LMRkWXbtKaIOM5V +-----END CERTIFICATE----- + +ePKI Root Certification Authority +================================= +-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBeMQswCQYDVQQG +EwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0ZC4xKjAoBgNVBAsMIWVQS0kg +Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMx +MjdaMF4xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEq +MCgGA1UECwwhZVBLSSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAHSyZbCUNs +IZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAhijHyl3SJCRImHJ7K2RKi +lTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3XDZoTM1PRYfl61dd4s5oz9wCGzh1NlDiv +qOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX +12ruOzjjK9SXDrkb5wdJfzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0O +WQqraffAsgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uUWH1+ +ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLSnT0IFaUQAS2zMnao +lQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pHdmX2Os+PYhcZewoozRrSgx4hxyy/ +vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJipNiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXi +Zo1jDiVN1Rmy5nk3pyKdVDECAwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/Qkqi +MAwGA1UdEwQFMAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH +ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGBuvl2ICO1J2B0 +1GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6YlPwZpVnPDimZI+ymBV3QGypzq +KOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkPJXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdV +xrsStZf0X4OFunHB2WyBEXYKCrC/gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEP +NXubrjlpC2JgQCA2j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+r +GNm65ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUBo2M3IUxE +xJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS/jQ6fbjpKdx2qcgw+BRx +gMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2zGp1iro2C6pSe3VkQw63d4k3jMdXH7Ojy +sP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTEW9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmOD +BCEIZ43ygknQW/2xzQ+DhNQ+IIX3Sj0rnP0qCglN6oH4EZw= +-----END CERTIFICATE----- + +T\xc3\x9c\x42\xC4\xB0TAK UEKAE K\xC3\xB6k Sertifika Hizmet Sa\xC4\x9Flay\xc4\xb1\x63\xc4\xb1s\xc4\xb1 - S\xC3\xBCr\xC3\xBCm 3 +============================================================================================================================= +-----BEGIN CERTIFICATE----- +MIIFFzCCA/+gAwIBAgIBETANBgkqhkiG9w0BAQUFADCCASsxCzAJBgNVBAYTAlRSMRgwFgYDVQQH +DA9HZWJ6ZSAtIEtvY2FlbGkxRzBFBgNVBAoMPlTDvHJraXllIEJpbGltc2VsIHZlIFRla25vbG9q +aWsgQXJhxZ90xLFybWEgS3VydW11IC0gVMOcQsSwVEFLMUgwRgYDVQQLDD9VbHVzYWwgRWxla3Ry +b25payB2ZSBLcmlwdG9sb2ppIEFyYcWfdMSxcm1hIEVuc3RpdMO8c8O8IC0gVUVLQUUxIzAhBgNV +BAsMGkthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppMUowSAYDVQQDDEFUw5xCxLBUQUsgVUVLQUUg +S8O2ayBTZXJ0aWZpa2EgSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsSAtIFPDvHLDvG0gMzAeFw0wNzA4 +MjQxMTM3MDdaFw0xNzA4MjExMTM3MDdaMIIBKzELMAkGA1UEBhMCVFIxGDAWBgNVBAcMD0dlYnpl +IC0gS29jYWVsaTFHMEUGA1UECgw+VMO8cmtpeWUgQmlsaW1zZWwgdmUgVGVrbm9sb2ppayBBcmHF +n3TEsXJtYSBLdXJ1bXUgLSBUw5xCxLBUQUsxSDBGBgNVBAsMP1VsdXNhbCBFbGVrdHJvbmlrIHZl +IEtyaXB0b2xvamkgQXJhxZ90xLFybWEgRW5zdGl0w7xzw7wgLSBVRUtBRTEjMCEGA1UECwwaS2Ft +dSBTZXJ0aWZpa2FzeW9uIE1lcmtlemkxSjBIBgNVBAMMQVTDnELEsFRBSyBVRUtBRSBLw7ZrIFNl +cnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxIC0gU8O8csO8bSAzMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAim1L/xCIOsP2fpTo6iBkcK4hgb46ezzb8R1Sf1n68yJMlaCQvEhO +Eav7t7WNeoMojCZG2E6VQIdhn8WebYGHV2yKO7Rm6sxA/OOqbLLLAdsyv9Lrhc+hDVXDWzhXcLh1 +xnnRFDDtG1hba+818qEhTsXOfJlfbLm4IpNQp81McGq+agV/E5wrHur+R84EpW+sky58K5+eeROR +6Oqeyjh1jmKwlZMq5d/pXpduIF9fhHpEORlAHLpVK/swsoHvhOPc7Jg4OQOFCKlUAwUp8MmPi+oL +hmUZEdPpCSPeaJMDyTYcIW7OjGbxmTDY17PDHfiBLqi9ggtm/oLL4eAagsNAgQIDAQABo0IwQDAd +BgNVHQ4EFgQUvYiHyY/2pAoLquvF/pEjnatKijIwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAB18+kmPNOm3JpIWmgV050vQbTlswyb2zrgxvMTfvCr4 +N5EY3ATIZJkrGG2AA1nJrvhY0D7twyOfaTyGOBye79oneNGEN3GKPEs5z35FBtYt2IpNeBLWrcLT +y9LQQfMmNkqblWwM7uXRQydmwYj3erMgbOqwaSvHIOgMA8RBBZniP+Rr+KCGgceExh/VS4ESshYh +LBOhgLJeDEoTniDYYkCrkOpkSi+sDQESeUWoL4cZaMjihccwsnX5OD+ywJO0a+IDRM5noN+J1q2M +dqMTw5RhK2vZbMEHCiIHhWyFJEapvj+LeISCfiQMnf2BN+MlqO02TpUsyZyQ2uypQjyttgI= +-----END CERTIFICATE----- + +Buypass Class 2 CA 1 +==================== +-----BEGIN CERTIFICATE----- +MIIDUzCCAjugAwIBAgIBATANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU +QnV5cGFzcyBBUy05ODMxNjMzMjcxHTAbBgNVBAMMFEJ1eXBhc3MgQ2xhc3MgMiBDQSAxMB4XDTA2 +MTAxMzEwMjUwOVoXDTE2MTAxMzEwMjUwOVowSzELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBh +c3MgQVMtOTgzMTYzMzI3MR0wGwYDVQQDDBRCdXlwYXNzIENsYXNzIDIgQ0EgMTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAIs8B0XY9t/mx8q6jUPFR42wWsE425KEHK8T1A9vNkYgxC7M +cXA0ojTTNy7Y3Tp3L8DrKehc0rWpkTSHIln+zNvnma+WwajHQN2lFYxuyHyXA8vmIPLXl18xoS83 +0r7uvqmtqEyeIWZDO6i88wmjONVZJMHCR3axiFyCO7srpgTXjAePzdVBHfCuuCkslFJgNJQ72uA4 +0Z0zPhX0kzLFANq1KWYOOngPIVJfAuWSeyXTkh4vFZ2B5J2O6O+JzhRMVB0cgRJNcKi+EAUXfh/R +uFdV7c27UsKwHnjCTTZoy1YmwVLBvXb3WNVyfh9EdrsAiR0WnVE1703CVu9r4Iw7DekCAwEAAaNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUP42aWYv8e3uco684sDntkHGA1sgwDgYDVR0P +AQH/BAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQAVGn4TirnoB6NLJzKyQJHyIdFkhb5jatLPgcIV +1Xp+DCmsNx4cfHZSldq1fyOhKXdlyTKdqC5Wq2B2zha0jX94wNWZUYN/Xtm+DKhQ7SLHrQVMdvvt +7h5HZPb3J31cKA9FxVxiXqaakZG3Uxcu3K1gnZZkOb1naLKuBctN518fV4bVIJwo+28TOPX2EZL2 +fZleHwzoq0QkKXJAPTZSr4xYkHPB7GEseaHsh7U/2k3ZIQAw3pDaDtMaSKk+hQsUi4y8QZ5q9w5w +wDX3OaJdZtB7WZ+oRxKaJyOkLY4ng5IgodcVf/EuGO70SH8vf/GhGLWhC5SgYiAynB321O+/TIho +-----END CERTIFICATE----- + +Buypass Class 3 CA 1 +==================== +-----BEGIN CERTIFICATE----- +MIIDUzCCAjugAwIBAgIBAjANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU +QnV5cGFzcyBBUy05ODMxNjMzMjcxHTAbBgNVBAMMFEJ1eXBhc3MgQ2xhc3MgMyBDQSAxMB4XDTA1 +MDUwOTE0MTMwM1oXDTE1MDUwOTE0MTMwM1owSzELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBh +c3MgQVMtOTgzMTYzMzI3MR0wGwYDVQQDDBRCdXlwYXNzIENsYXNzIDMgQ0EgMTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAKSO13TZKWTeXx+HgJHqTjnmGcZEC4DVC69TB4sSveZn8AKx +ifZgisRbsELRwCGoy+Gb72RRtqfPFfV0gGgEkKBYouZ0plNTVUhjP5JW3SROjvi6K//zNIqeKNc0 +n6wv1g/xpC+9UrJJhW05NfBEMJNGJPO251P7vGGvqaMU+8IXF4Rs4HyI+MkcVyzwPX6UvCWThOia +AJpFBUJXgPROztmuOfbIUxAMZTpHe2DC1vqRycZxbL2RhzyRhkmr8w+gbCZ2Xhysm3HljbybIR6c +1jh+JIAVMYKWsUnTYjdbiAwKYjT+p0h+mbEwi5A3lRyoH6UsjfRVyNvdWQrCrXig9IsCAwEAAaNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUOBTmyPCppAP0Tj4io1vy1uCtQHQwDgYDVR0P +AQH/BAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQABZ6OMySU9E2NdFm/soT4JXJEVKirZgCFPBdy7 +pYmrEzMqnji3jG8CcmPHc3ceCQa6Oyh7pEfJYWsICCD8igWKH7y6xsL+z27sEzNxZy5p+qksP2bA +EllNC1QCkoS72xLvg3BweMhT+t/Gxv/ciC8HwEmdMldg0/L2mSlf56oBzKwzqBwKu5HEA6BvtjT5 +htOzdlSY9EqBs1OdTUDs5XcTRa9bqh/YL0yCe/4qxFi7T/ye/QNlGioOw6UgFpRreaaiErS7GqQj +el/wroQk5PMr+4okoyeYZdowdXb8GZHo2+ubPzK/QJcHJrrM85SFSnonk8+QQtS4Wxam58tAA915 +-----END CERTIFICATE----- + +EBG Elektronik Sertifika Hizmet Sa\xC4\x9Flay\xc4\xb1\x63\xc4\xb1s\xc4\xb1 +========================================================================== +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- + +certSIGN ROOT CA +================ +-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYTAlJPMREwDwYD +VQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTAeFw0wNjA3MDQxNzIwMDRa +Fw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UE +CxMQY2VydFNJR04gUk9PVCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7I +JUqOtdu0KBuqV5Do0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHH +rfAQUySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5dRdY4zTW2 +ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQOA7+j0xbm0bqQfWwCHTD +0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwvJoIQ4uNllAoEwF73XVv4EOLQunpL+943 +AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B +Af8EBAMCAcYwHQYDVR0OBBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IB +AQA+0hyJLjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecYMnQ8 +SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ44gx+FkagQnIl6Z0 +x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6IJd1hJyMctTEHBDa0GpC9oHRxUIlt +vBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNwi/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7Nz +TogVZ96edhBiIL5VaZVDADlN9u6wWk5JRFRYX0KD +-----END CERTIFICATE----- + +CNNIC ROOT +========== +-----BEGIN CERTIFICATE----- +MIIDVTCCAj2gAwIBAgIESTMAATANBgkqhkiG9w0BAQUFADAyMQswCQYDVQQGEwJDTjEOMAwGA1UE +ChMFQ05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1QwHhcNMDcwNDE2MDcwOTE0WhcNMjcwNDE2MDcw +OTE0WjAyMQswCQYDVQQGEwJDTjEOMAwGA1UEChMFQ05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1Qw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDTNfc/c3et6FtzF8LRb+1VvG7q6KR5smzD +o+/hn7E7SIX1mlwhIhAsxYLO2uOabjfhhyzcuQxauohV3/2q2x8x6gHx3zkBwRP9SFIhxFXf2tiz +VHa6dLG3fdfA6PZZxU3Iva0fFNrfWEQlMhkqx35+jq44sDB7R3IJMfAw28Mbdim7aXZOV/kbZKKT +VrdvmW7bCgScEeOAH8tjlBAKqeFkgjH5jCftppkA9nCTGPihNIaj3XrCGHn2emU1z5DrvTOTn1Or +czvmmzQgLx3vqR1jGqCA2wMv+SYahtKNu6m+UjqHZ0gNv7Sg2Ca+I19zN38m5pIEo3/PIKe38zrK +y5nLAgMBAAGjczBxMBEGCWCGSAGG+EIBAQQEAwIABzAfBgNVHSMEGDAWgBRl8jGtKvf33VKWCscC +wQ7vptU7ETAPBgNVHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIB/jAdBgNVHQ4EFgQUZfIxrSr3991S +lgrHAsEO76bVOxEwDQYJKoZIhvcNAQEFBQADggEBAEs17szkrr/Dbq2flTtLP1se31cpolnKOOK5 +Gv+e5m4y3R6u6jW39ZORTtpC4cMXYFDy0VwmuYK36m3knITnA3kXr5g9lNvHugDnuL8BV8F3RTIM +O/G0HAiw/VGgod2aHRM2mm23xzy54cXZF/qD1T0VoDy7HgviyJA/qIYM/PmLXoXLT1tLYhFHxUV8 +BS9BsZ4QaRuZluBVeftOhpm4lNqGOGqTo+fLbuXf6iFViZx9fX+Y9QCJ7uOEwFyWtcVG6kbghVW2 +G8kS1sHNzYDzAgE8yGnLRUhj2JTQ7IUOO04RZfSCjKY9ri4ilAnIXOo8gV0WKgOXFlUJ24pBgp5m +mxE= +-----END CERTIFICATE----- + +ApplicationCA - Japanese Government +=================================== +-----BEGIN CERTIFICATE----- +MIIDoDCCAoigAwIBAgIBMTANBgkqhkiG9w0BAQUFADBDMQswCQYDVQQGEwJKUDEcMBoGA1UEChMT +SmFwYW5lc2UgR292ZXJubWVudDEWMBQGA1UECxMNQXBwbGljYXRpb25DQTAeFw0wNzEyMTIxNTAw +MDBaFw0xNzEyMTIxNTAwMDBaMEMxCzAJBgNVBAYTAkpQMRwwGgYDVQQKExNKYXBhbmVzZSBHb3Zl +cm5tZW50MRYwFAYDVQQLEw1BcHBsaWNhdGlvbkNBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAp23gdE6Hj6UG3mii24aZS2QNcfAKBZuOquHMLtJqO8F6tJdhjYq+xpqcBrSGUeQ3DnR4 +fl+Kf5Sk10cI/VBaVuRorChzoHvpfxiSQE8tnfWuREhzNgaeZCw7NCPbXCbkcXmP1G55IrmTwcrN +wVbtiGrXoDkhBFcsovW8R0FPXjQilbUfKW1eSvNNcr5BViCH/OlQR9cwFO5cjFW6WY2H/CPek9AE +jP3vbb3QesmlOmpyM8ZKDQUXKi17safY1vC+9D/qDihtQWEjdnjDuGWk81quzMKq2edY3rZ+nYVu +nyoKb58DKTCXKB28t89UKU5RMfkntigm/qJj5kEW8DOYRwIDAQABo4GeMIGbMB0GA1UdDgQWBBRU +WssmP3HMlEYNllPqa0jQk/5CdTAOBgNVHQ8BAf8EBAMCAQYwWQYDVR0RBFIwUKROMEwxCzAJBgNV +BAYTAkpQMRgwFgYDVQQKDA/ml6XmnKzlm73mlL/lupwxIzAhBgNVBAsMGuOCouODl+ODquOCseOD +vOOCt+ODp+ODs0NBMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBADlqRHZ3ODrs +o2dGD/mLBqj7apAxzn7s2tGJfHrrLgy9mTLnsCTWw//1sogJhyzjVOGjprIIC8CFqMjSnHH2HZ9g +/DgzE+Ge3Atf2hZQKXsvcJEPmbo0NI2VdMV+eKlmXb3KIXdCEKxmJj3ekav9FfBv7WxfEPjzFvYD +io+nEhEMy/0/ecGc/WLuo89UDNErXxc+4z6/wCs+CZv+iKZ+tJIX/COUgb1up8WMwusRRdv4QcmW +dupwX3kSa+SjB1oF7ydJzyGfikwJcGapJsErEU4z0g781mzSDjJkaP+tBXhfAx2o45CsJOAPQKdL +rosot4LKGAfmt1t06SAZf7IbiVQ= +-----END CERTIFICATE----- + +GeoTrust Primary Certification Authority - G3 +============================================= +-----BEGIN CERTIFICATE----- +MIID/jCCAuagAwIBAgIQFaxulBmyeUtB9iepwxgPHzANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UE +BhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChjKSAyMDA4IEdlb1RydXN0 +IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFy +eSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEczMB4XDTA4MDQwMjAwMDAwMFoXDTM3MTIwMTIz +NTk1OVowgZgxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAo +YykgMjAwOCBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNVBAMT +LUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMzCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANziXmJYHTNXOTIz+uvLh4yn1ErdBojqZI4xmKU4kB6Yzy5j +K/BGvESyiaHAKAxJcCGVn2TAppMSAmUmhsalifD614SgcK9PGpc/BkTVyetyEH3kMSj7HGHmKAdE +c5IiaacDiGydY8hS2pgn5whMcD60yRLBxWeDXTPzAxHsatBT4tG6NmCUgLthY2xbF37fQJQeqw3C +IShwiP/WJmxsYAQlTlV+fe+/lEjetx3dcI0FX4ilm/LC7urRQEFtYjgdVgbFA0dRIBn8exALDmKu +dlW/X3e+PkkBUz2YJQN2JFodtNuJ6nnltrM7P7pMKEF/BqxqjsHQ9gUdfeZChuOl1UcCAwEAAaNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMR5yo6hTgMdHNxr +2zFblD4/MH8tMA0GCSqGSIb3DQEBCwUAA4IBAQAtxRPPVoB7eni9n64smefv2t+UXglpp+duaIy9 +cr5HqQ6XErhK8WTTOd8lNNTBzU6B8A8ExCSzNJbGpqow32hhc9f5joWJ7w5elShKKiePEI4ufIbE +Ap7aDHdlDkQNkv39sxY2+hENHYwOB4lqKVb3cvTdFZx3NWZXqxNT2I7BQMXXExZacse3aQHEerGD +AWh9jUGhlBjBJVz88P6DAod8DQ3PLghcSkANPuyBYeYk28rgDi0Hsj5W3I31QYUHSJsMC8tJP33s +t/3LjWeJGqvtux6jAAgIFyqCXDFdRootD4abdNlF+9RAsXqqaC2Gspki4cErx5z481+oghLrGREt +-----END CERTIFICATE----- + +thawte Primary Root CA - G2 +=========================== +-----BEGIN CERTIFICATE----- +MIICiDCCAg2gAwIBAgIQNfwmXNmET8k9Jj1Xm67XVjAKBggqhkjOPQQDAzCBhDELMAkGA1UEBhMC +VVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjE4MDYGA1UECxMvKGMpIDIwMDcgdGhhd3RlLCBJbmMu +IC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3Qg +Q0EgLSBHMjAeFw0wNzExMDUwMDAwMDBaFw0zODAxMTgyMzU5NTlaMIGEMQswCQYDVQQGEwJVUzEV +MBMGA1UEChMMdGhhd3RlLCBJbmMuMTgwNgYDVQQLEy8oYykgMjAwNyB0aGF3dGUsIEluYy4gLSBG +b3IgYXV0aG9yaXplZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAt +IEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEotWcgnuVnfFSeIf+iha/BebfowJPDQfGAFG6DAJS +LSKkQjnE/o/qycG+1E3/n3qe4rF8mq2nhglzh9HnmuN6papu+7qzcMBniKI11KOasf2twu8x+qi5 +8/sIxpHR+ymVo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU +mtgAMADna3+FGO6Lts6KDPgR4bswCgYIKoZIzj0EAwMDaQAwZgIxAN344FdHW6fmCsO99YCKlzUN +G4k8VIZ3KMqh9HneteY4sPBlcIx/AlTCv//YoT7ZzwIxAMSNlPzcU9LcnXgWHxUzI1NS41oxXZ3K +rr0TKUQNJ1uo52icEvdYPy5yAlejj6EULg== +-----END CERTIFICATE----- + +thawte Primary Root CA - G3 +=========================== +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCBrjELMAkGA1UE +BhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2 +aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIwMDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhv +cml6ZWQgdXNlIG9ubHkxJDAiBgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0w +ODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhh +d3RlLCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9uMTgwNgYD +VQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTEkMCIG +A1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEczMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAsr8nLPvb2FvdeHsbnndmgcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2At +P0LMqmsywCPLLEHd5N/8YZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC ++BsUa0Lfb1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS99irY +7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2SzhkGcuYMXDhpxwTW +vGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUkOQIDAQABo0IwQDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJ +KoZIhvcNAQELBQADggEBABpA2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweK +A3rD6z8KLFIWoCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu +t8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7cKUGRIjxpp7sC +8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fMm7v/OeZWYdMKp8RcTGB7BXcm +er/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZuMdRAGmI0Nj81Aa6sY6A= +-----END CERTIFICATE----- + +GeoTrust Primary Certification Authority - G2 +============================================= +-----BEGIN CERTIFICATE----- +MIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChjKSAyMDA3IEdlb1RydXN0IElu +Yy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1 +OVowgZgxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykg +MjAwNyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNVBAMTLUdl +b1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjB2MBAGByqGSM49AgEG +BSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcLSo17VDs6bl8VAsBQps8lL33KSLjHUGMc +KiEIfJo22Av+0SbFWDEwKCXzXV2juLaltJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+ +EVXVMAoGCCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGTqQ7m +ndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBuczrD6ogRLQy7rQkgu2 +npaqBA+K +-----END CERTIFICATE----- + +VeriSign Universal Root Certification Authority +=============================================== +-----BEGIN CERTIFICATE----- +MIIEuTCCA6GgAwIBAgIQQBrEZCGzEyEDDrvkEhrFHTANBgkqhkiG9w0BAQsFADCBvTELMAkGA1UE +BhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBO +ZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwOCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVk +IHVzZSBvbmx5MTgwNgYDVQQDEy9WZXJpU2lnbiBVbml2ZXJzYWwgUm9vdCBDZXJ0aWZpY2F0aW9u +IEF1dGhvcml0eTAeFw0wODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIG9MQswCQYDVQQGEwJV +UzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0IE5ldHdv +cmsxOjA4BgNVBAsTMShjKSAyMDA4IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl +IG9ubHkxODA2BgNVBAMTL1ZlcmlTaWduIFVuaXZlcnNhbCBSb290IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx2E3XrEBNNti1xWb/1hajCMj +1mCOkdeQmIN65lgZOIzF9uVkhbSicfvtvbnazU0AtMgtc6XHaXGVHzk8skQHnOgO+k1KxCHfKWGP +MiJhgsWHH26MfF8WIFFE0XBPV+rjHOPMee5Y2A7Cs0WTwCznmhcrewA3ekEzeOEz4vMQGn+HLL72 +9fdC4uW/h2KJXwBL38Xd5HVEMkE6HnFuacsLdUYI0crSK5XQz/u5QGtkjFdN/BMReYTtXlT2NJ8I +AfMQJQYXStrxHXpma5hgZqTZ79IugvHw7wnqRMkVauIDbjPTrJ9VAMf2CGqUuV/c4DPxhGD5WycR +tPwW8rtWaoAljQIDAQABo4GyMIGvMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMG0G +CCsGAQUFBwEMBGEwX6FdoFswWTBXMFUWCWltYWdlL2dpZjAhMB8wBwYFKw4DAhoEFI/l0xqGrI2O +a8PPgGrUSBgsexkuMCUWI2h0dHA6Ly9sb2dvLnZlcmlzaWduLmNvbS92c2xvZ28uZ2lmMB0GA1Ud +DgQWBBS2d/ppSEefUxLVwuoHMnYH0ZcHGTANBgkqhkiG9w0BAQsFAAOCAQEASvj4sAPmLGd75JR3 +Y8xuTPl9Dg3cyLk1uXBPY/ok+myDjEedO2Pzmvl2MpWRsXe8rJq+seQxIcaBlVZaDrHC1LGmWazx +Y8u4TB1ZkErvkBYoH1quEPuBUDgMbMzxPcP1Y+Oz4yHJJDnp/RVmRvQbEdBNc6N9Rvk97ahfYtTx +P/jgdFcrGJ2BtMQo2pSXpXDrrB2+BxHw1dvd5Yzw1TKwg+ZX4o+/vqGqvz0dtdQ46tewXDpPaj+P +wGZsY6rp2aQW9IHRlRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4 +mJO37M2CYfE45k+XmCpajQ== +-----END CERTIFICATE----- + +VeriSign Class 3 Public Primary Certification Authority - G4 +============================================================ +-----BEGIN CERTIFICATE----- +MIIDhDCCAwqgAwIBAgIQL4D+I4wOIg9IZxIokYesszAKBggqhkjOPQQDAzCByjELMAkGA1UEBhMC +VVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3 +b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVz +ZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5IC0gRzQwHhcNMDcxMTA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBU +cnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRo +b3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5 +IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASnVnp8 +Utpkmw4tXNherJI9/gHmGUo9FANL+mAnINmDiWn6VMaaGF5VKmTeBvaNSjutEDxlPZCIBIngMGGz +rl0Bp3vefLK+ymVhAIau2o970ImtTR1ZmkGxvEeA3J5iw/mjgbIwga8wDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEw +HzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVyaXNpZ24u +Y29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFLMWkf3upm7ktS5Jj4d4gYDs5bG1MAoGCCqGSM49BAMD +A2gAMGUCMGYhDBgmYFo4e1ZC4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIx +AJw9SDkjOVgaFRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA== +-----END CERTIFICATE----- + +NetLock Arany (Class Gold) Főtanúsítvány +============================================ +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQGEwJIVTERMA8G +A1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3MDUGA1UECwwuVGFuw7pzw610 +dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBB +cmFueSAoQ2xhc3MgR29sZCkgRsWRdGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgx +MjA2MTUwODIxWjCBpzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxO +ZXRMb2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNzIEdvbGQpIEbFkXRhbsO6 +c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxCRec75LbRTDofTjl5Bu +0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrTlF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw +/HpYzY6b7cNGbIRwXdrzAZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAk +H3B5r9s5VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRGILdw +fzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2BJtr+UBdADTHLpl1 +neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2MU9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwW +qZw8UQCgwBEIBaeZ5m8BiFRhbvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTta +YtOUZcTh5m2C+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2FuLjbvrW5Kfna +NwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2XjG4Kvte9nHfRCaexOYNkbQu +dZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- + +Staat der Nederlanden Root CA - G2 +================================== +-----BEGIN CERTIFICATE----- +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 +66+KAQ== +-----END CERTIFICATE----- + +CA Disig +======== +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBATANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQGEwJTSzETMBEGA1UEBxMK +QnJhdGlzbGF2YTETMBEGA1UEChMKRGlzaWcgYS5zLjERMA8GA1UEAxMIQ0EgRGlzaWcwHhcNMDYw +MzIyMDEzOTM0WhcNMTYwMzIyMDEzOTM0WjBKMQswCQYDVQQGEwJTSzETMBEGA1UEBxMKQnJhdGlz +bGF2YTETMBEGA1UEChMKRGlzaWcgYS5zLjERMA8GA1UEAxMIQ0EgRGlzaWcwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCS9jHBfYj9mQGp2HvycXXxMcbzdWb6UShGhJd4NLxs/LxFWYgm +GErENx+hSkS943EE9UQX4j/8SFhvXJ56CbpRNyIjZkMhsDxkovhqFQ4/61HhVKndBpnXmjxUizkD +Pw/Fzsbrg3ICqB9x8y34dQjbYkzo+s7552oftms1grrijxaSfQUMbEYDXcDtab86wYqg6I7ZuUUo +hwjstMoVvoLdtUSLLa2GDGhibYVW8qwUYzrG0ZmsNHhWS8+2rT+MitcE5eN4TPWGqvWP+j1scaMt +ymfraHtuM6kMgiioTGohQBUgDCZbg8KpFhXAJIJdKxatymP2dACw30PEEGBWZ2NFAgMBAAGjgf8w +gfwwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUjbJJaJ1yCCW5wCf1UJNWSEZx+Y8wDgYDVR0P +AQH/BAQDAgEGMDYGA1UdEQQvMC2BE2Nhb3BlcmF0b3JAZGlzaWcuc2uGFmh0dHA6Ly93d3cuZGlz +aWcuc2svY2EwZgYDVR0fBF8wXTAtoCugKYYnaHR0cDovL3d3dy5kaXNpZy5zay9jYS9jcmwvY2Ff +ZGlzaWcuY3JsMCygKqAohiZodHRwOi8vY2EuZGlzaWcuc2svY2EvY3JsL2NhX2Rpc2lnLmNybDAa +BgNVHSAEEzARMA8GDSuBHpGT5goAAAABAQEwDQYJKoZIhvcNAQEFBQADggEBAF00dGFMrzvY/59t +WDYcPQuBDRIrRhCA/ec8J9B6yKm2fnQwM6M6int0wHl5QpNt/7EpFIKrIYwvF/k/Ji/1WcbvgAa3 +mkkp7M5+cTxqEEHA9tOasnxakZzArFvITV734VP/Q3f8nktnbNfzg9Gg4H8l37iYC5oyOGwwoPP/ +CBUz91BKez6jPiCp3C9WgArtQVCwyfTssuMmRAAOb54GvCKWU3BlxFAKRmukLyeBEicTXxChds6K +ezfqwzlhA5WYOudsiCUI/HloDYd9Yvi0X/vF2Ey9WLw/Q1vUHgFNPGO+I++MzVpQuGhU+QqZMxEA +4Z7CRneC9VkGjCFMhwnN5ag= +-----END CERTIFICATE----- + +Juur-SK +======= +-----BEGIN CERTIFICATE----- +MIIE5jCCA86gAwIBAgIEO45L/DANBgkqhkiG9w0BAQUFADBdMRgwFgYJKoZIhvcNAQkBFglwa2lA +c2suZWUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKExlBUyBTZXJ0aWZpdHNlZXJpbWlza2Vza3VzMRAw +DgYDVQQDEwdKdXVyLVNLMB4XDTAxMDgzMDE0MjMwMVoXDTE2MDgyNjE0MjMwMVowXTEYMBYGCSqG +SIb3DQEJARYJcGtpQHNrLmVlMQswCQYDVQQGEwJFRTEiMCAGA1UEChMZQVMgU2VydGlmaXRzZWVy +aW1pc2tlc2t1czEQMA4GA1UEAxMHSnV1ci1TSzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAIFxNj4zB9bjMI0TfncyRsvPGbJgMUaXhvSYRqTCZUXP00B841oiqBB4M8yIsdOBSvZiF3tf +TQou0M+LI+5PAk676w7KvRhj6IAcjeEcjT3g/1tf6mTll+g/mX8MCgkzABpTpyHhOEvWgxutr2TC ++Rx6jGZITWYfGAriPrsfB2WThbkasLnE+w0R9vXW+RvHLCu3GFH+4Hv2qEivbDtPL+/40UceJlfw +UR0zlv/vWT3aTdEVNMfqPxZIe5EcgEMPPbgFPtGzlc3Yyg/CQ2fbt5PgIoIuvvVoKIO5wTtpeyDa +Tpxt4brNj3pssAki14sL2xzVWiZbDcDq5WDQn/413z8CAwEAAaOCAawwggGoMA8GA1UdEwEB/wQF +MAMBAf8wggEWBgNVHSAEggENMIIBCTCCAQUGCisGAQQBzh8BAQEwgfYwgdAGCCsGAQUFBwICMIHD +HoHAAFMAZQBlACAAcwBlAHIAdABpAGYAaQBrAGEAYQB0ACAAbwBuACAAdgDkAGwAagBhAHMAdABh +AHQAdQBkACAAQQBTAC0AaQBzACAAUwBlAHIAdABpAGYAaQB0AHMAZQBlAHIAaQBtAGkAcwBrAGUA +cwBrAHUAcwAgAGEAbABhAG0ALQBTAEsAIABzAGUAcgB0AGkAZgBpAGsAYQBhAHQAaQBkAGUAIABr +AGkAbgBuAGkAdABhAG0AaQBzAGUAawBzMCEGCCsGAQUFBwIBFhVodHRwOi8vd3d3LnNrLmVlL2Nw +cy8wKwYDVR0fBCQwIjAgoB6gHIYaaHR0cDovL3d3dy5zay5lZS9qdXVyL2NybC8wHQYDVR0OBBYE +FASqekej5ImvGs8KQKcYP2/v6X2+MB8GA1UdIwQYMBaAFASqekej5ImvGs8KQKcYP2/v6X2+MA4G +A1UdDwEB/wQEAwIB5jANBgkqhkiG9w0BAQUFAAOCAQEAe8EYlFOiCfP+JmeaUOTDBS8rNXiRTHyo +ERF5TElZrMj3hWVcRrs7EKACr81Ptcw2Kuxd/u+gkcm2k298gFTsxwhwDY77guwqYHhpNjbRxZyL +abVAyJRld/JXIWY7zoVAtjNjGr95HvxcHdMdkxuLDF2FvZkwMhgJkVLpfKG6/2SSmuz+Ne6ML678 +IIbsSt4beDI3poHSna9aEhbKmVv8b20OxaAehsmR0FyYgl9jDIpaq9iVpszLita/ZEuOyoqysOkh +Mp6qqIWYNIE5ITuoOlIyPfZrN4YGWhWY3PARZv40ILcD9EEQfTmEeZZyY7aWAuVrua0ZTbvGRNs2 +yyqcjg== +-----END CERTIFICATE----- + +Hongkong Post Root CA 1 +======================= +-----BEGIN CERTIFICATE----- +MIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsxFjAUBgNVBAoT +DUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3QgUm9vdCBDQSAxMB4XDTAzMDUx +NTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkGA1UEBhMCSEsxFjAUBgNVBAoTDUhvbmdrb25n +IFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3QgUm9vdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEArP84tulmAknjorThkPlAj3n54r15/gK97iSSHSL22oVyaf7XPwnU3ZG1 +ApzQjVrhVcNQhrkpJsLj2aDxaQMoIIBFIi1WpztUlVYiWR8o3x8gPW2iNr4joLFutbEnPzlTCeqr +auh0ssJlXI6/fMN4hM2eFvz1Lk8gKgifd/PFHsSaUmYeSF7jEAaPIpjhZY4bXSNmO7ilMlHIhqqh +qZ5/dpTCpmy3QfDVyAY45tQM4vM7TG1QjMSDJ8EThFk9nnV0ttgCXjqQesBCNnLsak3c78QA3xMY +V18meMjWCnl3v/evt3a5pQuEF10Q6m/hq5URX208o1xNg1vysxmKgIsLhwIDAQABoyYwJDASBgNV +HRMBAf8ECDAGAQH/AgEDMA4GA1UdDwEB/wQEAwIBxjANBgkqhkiG9w0BAQUFAAOCAQEADkbVPK7i +h9legYsCmEEIjEy82tvuJxuC52pF7BaLT4Wg87JwvVqWuspube5Gi27nKi6Wsxkz67SfqLI37pio +l7Yutmcn1KZJ/RyTZXaeQi/cImyaT/JaFTmxcdcrUehtHJjA2Sr0oYJ71clBoiMBdDhViw+5Lmei +IAQ32pwL0xch4I+XeTRvhEgCIDMb5jREn5Fw9IBehEPCKdJsEhTkYY2sEJCehFC78JZvRZ+K88ps +T/oROhUVRsPNH4NbLUES7VBnQRM9IauUiqpOfMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilT +c4afU9hDDl3WY4JxHYB0yvbiAmvZWg== +-----END CERTIFICATE----- + +SecureSign RootCA11 +=================== +-----BEGIN CERTIFICATE----- +MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDErMCkGA1UEChMi +SmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoGA1UEAxMTU2VjdXJlU2lnbiBS +b290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSsw +KQYDVQQKEyJKYXBhbiBDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1 +cmVTaWduIFJvb3RDQTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvL +TJszi1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8h9uuywGO +wvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOVMdrAG/LuYpmGYz+/3ZMq +g6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rP +O7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitA +bpSACW22s293bzUIUPsCh8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZX +t94wDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAKCh +OBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xmKbabfSVSSUOrTC4r +bnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQX5Ucv+2rIrVls4W6ng+4reV6G4pQ +Oh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWrQbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01 +y8hSyn+B/tlr0/cR7SXf+Of5pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061 +lgeLKBObjBmNQSdJQO7e5iNEOdyhIta6A/I= +-----END CERTIFICATE----- + +ACEDICOM Root +============= +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- + +Verisign Class 3 Public Primary Certification Authority +======================================================= +-----BEGIN CERTIFICATE----- +MIICPDCCAaUCEDyRMcsf9tAbDpq40ES/Er4wDQYJKoZIhvcNAQEFBQAwXzELMAkGA1UEBhMCVVMx +FzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmltYXJ5 +IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVow +XzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAz +IFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUA +A4GNADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhEBarsAx94 +f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/isI19wKTakyYbnsZogy1Ol +hec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBABByUqkFFBky +CEHwxWsKzH4PIRnN5GfcX6kb5sroc50i2JhucwNhkcV8sEVAbkSdjbCxlnRhLQ2pRdKkkirWmnWX +bj9T/UWZYB2oK0z5XqcJ2HUw19JlYD1n1khVdWk/kfVIC0dpImmClr7JyDiGSnoscxlIaU5rfGW/ +D/xwzoiQ +-----END CERTIFICATE----- + +Microsec e-Szigno Root CA 2009 +============================== +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYDVQQGEwJIVTER +MA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jv +c2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTAeFw0wOTA2MTYxMTMwMThaFw0yOTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UE +BwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUt +U3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvPkd6mJviZpWNwrZuuyjNA +fW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tccbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG +0IMZfcChEhyVbUr02MelTTMuhTlAdX4UfIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKA +pxn1ntxVUwOXewdI/5n7N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm +1HxdrtbCxkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1+rUC +AwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTLD8bf +QkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAbBgNVHREE +FDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqGSIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0o +lZMEyL/azXm4Q5DwpL7v8u8hmLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfX +I/OMn74dseGkddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c2Pm2G2JwCz02 +yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5tHMN1Rq41Bab2XD0h7lbwyYIi +LXpUq3DDfSJlgnCW +-----END CERTIFICATE----- + +E-Guven Kok Elektronik Sertifika Hizmet Saglayicisi +=================================================== +-----BEGIN CERTIFICATE----- +MIIDtjCCAp6gAwIBAgIQRJmNPMADJ72cdpW56tustTANBgkqhkiG9w0BAQUFADB1MQswCQYDVQQG +EwJUUjEoMCYGA1UEChMfRWxla3Ryb25payBCaWxnaSBHdXZlbmxpZ2kgQS5TLjE8MDoGA1UEAxMz +ZS1HdXZlbiBLb2sgRWxla3Ryb25payBTZXJ0aWZpa2EgSGl6bWV0IFNhZ2xheWljaXNpMB4XDTA3 +MDEwNDExMzI0OFoXDTE3MDEwNDExMzI0OFowdTELMAkGA1UEBhMCVFIxKDAmBgNVBAoTH0VsZWt0 +cm9uaWsgQmlsZ2kgR3V2ZW5saWdpIEEuUy4xPDA6BgNVBAMTM2UtR3V2ZW4gS29rIEVsZWt0cm9u +aWsgU2VydGlmaWthIEhpem1ldCBTYWdsYXlpY2lzaTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAMMSIJ6wXgBljU5Gu4Bc6SwGl9XzcslwuedLZYDBS75+PNdUMZTe1RK6UxYC6lhj71vY +8+0qGqpxSKPcEC1fX+tcS5yWCEIlKBHMilpiAVDV6wlTL/jDj/6z/P2douNffb7tC+Bg62nsM+3Y +jfsSSYMAyYuXjDtzKjKzEve5TfL0TW3H5tYmNwjy2f1rXKPlSFxYvEK+A1qBuhw1DADT9SN+cTAI +JjjcJRFHLfO6IxClv7wC90Nex/6wN1CZew+TzuZDLMN+DfIcQ2Zgy2ExR4ejT669VmxMvLz4Bcpk +9Ok0oSy1c+HCPujIyTQlCFzz7abHlJ+tiEMl1+E5YP6sOVkCAwEAAaNCMEAwDgYDVR0PAQH/BAQD +AgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJ/uRLOU1fqRTy7ZVZoEVtstxNulMA0GCSqG +SIb3DQEBBQUAA4IBAQB/X7lTW2M9dTLn+sR0GstG30ZpHFLPqk/CaOv/gKlR6D1id4k9CnU58W5d +F4dvaAXBlGzZXd/aslnLpRCKysw5zZ/rTt5S/wzw9JKp8mxTq5vSR6AfdPebmvEvFZ96ZDAYBzwq +D2fK/A+JYZ1lpTzlvBNbCNvj/+27BrtqBrF6T2XGgv0enIu1De5Iu7i9qgi0+6N8y5/NkHZchpZ4 +Vwpm+Vganf2XKWDeEaaQHBkc7gGWIjQ0LpH5t8Qn0Xvmv/uARFoW5evg1Ao4vOSR49XrXMGs3xtq +fJ7lddK2l4fbzIcrQzqECK+rPNv3PGYxhrCdU3nt+CPeQuMtgvEP5fqX +-----END CERTIFICATE----- + +GlobalSign Root CA - R3 +======================= +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4GA1UECxMXR2xv +YmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh +bFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT +aWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln +bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWt +iHL8RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsTgHeMCOFJ +0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmmKPZpO/bLyCiR5Z2KYVc3 +rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zdQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjl +OCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2 +xmmFghcCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FI/wS3+oLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZURUm7 +lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMpjjM5RcOO5LlXbKr8 +EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK6fBdRoyV3XpYKBovHd7NADdBj+1E +bddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQXmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18 +YIvDQVETI53O9zJrlAGomecsMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7r +kpeDMdmztcpHWD9f +-----END CERTIFICATE----- + +TC TrustCenter Universal CA III +=============================== +-----BEGIN CERTIFICATE----- +MIID4TCCAsmgAwIBAgIOYyUAAQACFI0zFQLkbPQwDQYJKoZIhvcNAQEFBQAwezELMAkGA1UEBhMC +REUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxJDAiBgNVBAsTG1RDIFRydXN0Q2VudGVy +IFVuaXZlcnNhbCBDQTEoMCYGA1UEAxMfVEMgVHJ1c3RDZW50ZXIgVW5pdmVyc2FsIENBIElJSTAe +Fw0wOTA5MDkwODE1MjdaFw0yOTEyMzEyMzU5NTlaMHsxCzAJBgNVBAYTAkRFMRwwGgYDVQQKExNU +QyBUcnVzdENlbnRlciBHbWJIMSQwIgYDVQQLExtUQyBUcnVzdENlbnRlciBVbml2ZXJzYWwgQ0Ex +KDAmBgNVBAMTH1RDIFRydXN0Q2VudGVyIFVuaXZlcnNhbCBDQSBJSUkwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDC2pxisLlxErALyBpXsq6DFJmzNEubkKLF5+cvAqBNLaT6hdqbJYUt +QCggbergvbFIgyIpRJ9Og+41URNzdNW88jBmlFPAQDYvDIRlzg9uwliT6CwLOunBjvvya8o84pxO +juT5fdMnnxvVZ3iHLX8LR7PH6MlIfK8vzArZQe+f/prhsq75U7Xl6UafYOPfjdN/+5Z+s7Vy+Eut +CHnNaYlAJ/Uqwa1D7KRTyGG299J5KmcYdkhtWyUB0SbFt1dpIxVbYYqt8Bst2a9c8SaQaanVDED1 +M4BDj5yjdipFtK+/fz6HP3bFzSreIMUWWMv5G/UPyw0RUmS40nZid4PxWJ//AgMBAAGjYzBhMB8G +A1UdIwQYMBaAFFbn4VslQ4Dg9ozhcbyO5YAvxEjiMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgEGMB0GA1UdDgQWBBRW5+FbJUOA4PaM4XG8juWAL8RI4jANBgkqhkiG9w0BAQUFAAOCAQEA +g8ev6n9NCjw5sWi+e22JLumzCecYV42FmhfzdkJQEw/HkG8zrcVJYCtsSVgZ1OK+t7+rSbyUyKu+ +KGwWaODIl0YgoGhnYIg5IFHYaAERzqf2EQf27OysGh+yZm5WZ2B6dF7AbZc2rrUNXWZzwCUyRdhK +BgePxLcHsU0GDeGl6/R1yrqc0L2z0zIkTO5+4nYES0lT2PLpVDP85XEfPRRclkvxOvIAu2y0+pZV +CIgJwcyRGSmwIC3/yzikQOEXvnlhgP8HA4ZMTnsGnxGGjYnuJ8Tb4rwZjgvDwxPHLQNjO9Po5KIq +woIIlBZU8O8fJ5AluA0OKBtHd0e9HKgl8ZS0Zg== +-----END CERTIFICATE----- + +Autoridad de Certificacion Firmaprofesional CIF A62634068 +========================================================= +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UEBhMCRVMxQjBA +BgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2 +MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEyMzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIw +QAYDVQQDDDlBdXRvcmlkYWQgZGUgQ2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBB +NjI2MzQwNjgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDD +Utd9thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQMcas9UX4P +B99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefGL9ItWY16Ck6WaVICqjaY +7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15iNA9wBj4gGFrO93IbJWyTdBSTo3OxDqqH +ECNZXyAFGUftaI6SEspd/NYrspI8IM/hX68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyI +plD9amML9ZMWGxmPsu2bm8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctX +MbScyJCyZ/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirjaEbsX +LZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/TKI8xWVvTyQKmtFLK +bpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF6NkBiDkal4ZkQdU7hwxu+g/GvUgU +vzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVhOSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1Ud +EwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNH +DhpkLzCBpgYDVR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp +cm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBvACAAZABlACAA +bABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBlAGwAbwBuAGEAIAAwADgAMAAx +ADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx +51tkljYyGOylMnfX40S2wBEqgLk9am58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qk +R71kMrv2JYSiJ0L1ILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaP +T481PyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS3a/DTg4f +Jl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5kSeTy36LssUzAKh3ntLFl +osS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF3dvd6qJ2gHN99ZwExEWN57kci57q13XR +crHedUTnQn3iV2t93Jm8PYMo6oCTjcVMZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoR +saS8I8nkvof/uZS2+F0gStRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTD +KCOM/iczQ0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQBjLMi +6Et8Vcad+qMUu2WFbm5PEn4KPJ2V +-----END CERTIFICATE----- + +Izenpe.com +========== +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4MQswCQYDVQQG +EwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wHhcNMDcxMjEz +MTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMu +QS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ +03rKDx6sp4boFmVqscIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAK +ClaOxdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6HLmYRY2xU ++zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFXuaOKmMPsOzTFlUFpfnXC +PCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQDyCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxT +OTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbK +F7jJeodWLBoBHmy+E60QrLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK +0GqfvEyNBjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8Lhij+ +0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIBQFqNeb+Lz0vPqhbB +leStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+HMh3/1uaD7euBUbl8agW7EekFwID +AQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2luZm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+ +SVpFTlBFIFMuQS4gLSBDSUYgQTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBG +NjIgUzgxQzBBBgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O +BBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUAA4ICAQB4pgwWSp9MiDrAyw6l +Fn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWblaQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbga +kEyrkgPH7UIBzg/YsfqikuFgba56awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8q +hT/AQKM6WfxZSzwoJNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Cs +g1lwLDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCTVyvehQP5 +aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGkLhObNA5me0mrZJfQRsN5 +nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJbUjWumDqtujWTI6cfSN01RpiyEGjkpTHC +ClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZo +Q0iy2+tzJOeRf1SktoA+naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1Z +WrOZyGlsQyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- + +Chambers of Commerce Root - 2008 +================================ +-----BEGIN CERTIFICATE----- +MIIHTzCCBTegAwIBAgIJAKPaQn6ksa7aMA0GCSqGSIb3DQEBBQUAMIGuMQswCQYDVQQGEwJFVTFD +MEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNv +bS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMu +QS4xKTAnBgNVBAMTIENoYW1iZXJzIG9mIENvbW1lcmNlIFJvb3QgLSAyMDA4MB4XDTA4MDgwMTEy +Mjk1MFoXDTM4MDczMTEyMjk1MFowga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNl +ZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29tL2FkZHJlc3MpMRIwEAYDVQQF +EwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVyZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJl +cnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQCvAMtwNyuAWko6bHiUfaN/Gh/2NdW928sNRHI+JrKQUrpjOyhYb6WzbZSm891kDFX29ufyIiKA +XuFixrYp4YFs8r/lfTJqVKAyGVn+H4vXPWCGhSRv4xGzdz4gljUha7MI2XAuZPeEklPWDrCQiorj +h40G072QDuKZoRuGDtqaCrsLYVAGUvGef3bsyw/QHg3PmTA9HMRFEFis1tPo1+XqxQEHd9ZR5gN/ +ikilTWh1uem8nk4ZcfUyS5xtYBkL+8ydddy/Js2Pk3g5eXNeJQ7KXOt3EgfLZEFHcpOrUMPrCXZk +NNI5t3YRCQ12RcSprj1qr7V9ZS+UWBDsXHyvfuK2GNnQm05aSd+pZgvMPMZ4fKecHePOjlO+Bd5g +D2vlGts/4+EhySnB8esHnFIbAURRPHsl18TlUlRdJQfKFiC4reRB7noI/plvg6aRArBsNlVq5331 +lubKgdaX8ZSD6e2wsWsSaR6s+12pxZjptFtYer49okQ6Y1nUCyXeG0+95QGezdIp1Z8XGQpvvwyQ +0wlf2eOKNcx5Wk0ZN5K3xMGtr/R5JJqyAQuxr1yW84Ay+1w9mPGgP0revq+ULtlVmhduYJ1jbLhj +ya6BXBg14JC7vjxPNyK5fuvPnnchpj04gftI2jE9K+OJ9dC1vX7gUMQSibMjmhAxhduub+84Mxh2 +EQIDAQABo4IBbDCCAWgwEgYDVR0TAQH/BAgwBgEB/wIBDDAdBgNVHQ4EFgQU+SSsD7K1+HnA+mCI +G8TZTQKeFxkwgeMGA1UdIwSB2zCB2IAU+SSsD7K1+HnA+mCIG8TZTQKeFxmhgbSkgbEwga4xCzAJ +BgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNlZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNh +bWVyZmlybWEuY29tL2FkZHJlc3MpMRIwEAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENh +bWVyZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDiC +CQCj2kJ+pLGu2jAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUH +AgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZIhvcNAQEFBQADggIBAJASryI1 +wqM58C7e6bXpeHxIvj99RZJe6dqxGfwWPJ+0W2aeaufDuV2I6A+tzyMP3iU6XsxPpcG1Lawk0lgH +3qLPaYRgM+gQDROpI9CF5Y57pp49chNyM/WqfcZjHwj0/gF/JM8rLFQJ3uIrbZLGOU8W6jx+ekbU +RWpGqOt1glanq6B8aBMz9p0w8G8nOSQjKpD9kCk18pPfNKXG9/jvjA9iSnyu0/VU+I22mlaHFoI6 +M6taIgj3grrqLuBHmrS1RaMFO9ncLkVAO+rcf+g769HsJtg1pDDFOqxXnrN2pSB7+R5KBWIBpih1 +YJeSDW4+TTdDDZIVnBgizVGZoCkaPF+KMjNbMMeJL0eYD6MDxvbxrN8y8NmBGuScvfaAFPDRLLmF +9dijscilIeUcE5fuDr3fKanvNFNb0+RqE4QGtjICxFKuItLcsiFCGtpA8CnJ7AoMXOLQusxI0zcK +zBIKinmwPQN/aUv0NCB9szTqjktk9T79syNnFQ0EuPAtwQlRPLJsFfClI9eDdOTlLsn+mCdCxqvG +nrDQWzilm1DefhiYtUU79nm06PcaewaD+9CL2rvHvRirCG88gGtAPxkZumWK5r7VXNM21+9AUiRg +OGcEMeyP84LG3rlV8zsxkVrctQgVrXYlCg17LofiDKYGvCYQbTed7N14jHyAxfDZd0jQ +-----END CERTIFICATE----- + +Global Chambersign Root - 2008 +============================== +-----BEGIN CERTIFICATE----- +MIIHSTCCBTGgAwIBAgIJAMnN0+nVfSPOMA0GCSqGSIb3DQEBBQUAMIGsMQswCQYDVQQGEwJFVTFD +MEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNv +bS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMu +QS4xJzAlBgNVBAMTHkdsb2JhbCBDaGFtYmVyc2lnbiBSb290IC0gMjAwODAeFw0wODA4MDExMjMx +NDBaFw0zODA3MzExMjMxNDBaMIGsMQswCQYDVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUg +Y3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJ +QTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAlBgNVBAMTHkdsb2JhbCBD +aGFtYmVyc2lnbiBSb290IC0gMjAwODCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDf +VtPkOpt2RbQT2//BthmLN0EYlVJH6xedKYiONWwGMi5HYvNJBL99RDaxccy9Wglz1dmFRP+RVyXf +XjaOcNFccUMd2drvXNL7G706tcuto8xEpw2uIRU/uXpbknXYpBI4iRmKt4DS4jJvVpyR1ogQC7N0 +ZJJ0YPP2zxhPYLIj0Mc7zmFLmY/CDNBAspjcDahOo7kKrmCgrUVSY7pmvWjg+b4aqIG7HkF4ddPB +/gBVsIdU6CeQNR1MM62X/JcumIS/LMmjv9GYERTtY/jKmIhYF5ntRQOXfjyGHoiMvvKRhI9lNNgA +TH23MRdaKXoKGCQwoze1eqkBfSbW+Q6OWfH9GzO1KTsXO0G2Id3UwD2ln58fQ1DJu7xsepeY7s2M +H/ucUa6LcL0nn3HAa6x9kGbo1106DbDVwo3VyJ2dwW3Q0L9R5OP4wzg2rtandeavhENdk5IMagfe +Ox2YItaswTXbo6Al/3K1dh3ebeksZixShNBFks4c5eUzHdwHU1SjqoI7mjcv3N2gZOnm3b2u/GSF +HTynyQbehP9r6GsaPMWis0L7iwk+XwhSx2LE1AVxv8Rk5Pihg+g+EpuoHtQ2TS9x9o0o9oOpE9Jh +wZG7SMA0j0GMS0zbaRL/UJScIINZc+18ofLx/d33SdNDWKBWY8o9PeU1VlnpDsogzCtLkykPAgMB +AAGjggFqMIIBZjASBgNVHRMBAf8ECDAGAQH/AgEMMB0GA1UdDgQWBBS5CcqcHtvTbDprru1U8VuT +BjUuXjCB4QYDVR0jBIHZMIHWgBS5CcqcHtvTbDprru1U8VuTBjUuXqGBsqSBrzCBrDELMAkGA1UE +BhMCRVUxQzBBBgNVBAcTOk1hZHJpZCAoc2VlIGN1cnJlbnQgYWRkcmVzcyBhdCB3d3cuY2FtZXJm +aXJtYS5jb20vYWRkcmVzcykxEjAQBgNVBAUTCUE4Mjc0MzI4NzEbMBkGA1UEChMSQUMgQ2FtZXJm +aXJtYSBTLkEuMScwJQYDVQQDEx5HbG9iYWwgQ2hhbWJlcnNpZ24gUm9vdCAtIDIwMDiCCQDJzdPp +1X0jzjAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUHAgEWHGh0 +dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZIhvcNAQEFBQADggIBAICIf3DekijZBZRG +/5BXqfEv3xoNa/p8DhxJJHkn2EaqbylZUohwEurdPfWbU1Rv4WCiqAm57OtZfMY18dwY6fFn5a+6 +ReAJ3spED8IXDneRRXozX1+WLGiLwUePmJs9wOzL9dWCkoQ10b42OFZyMVtHLaoXpGNR6woBrX/s +dZ7LoR/xfxKxueRkf2fWIyr0uDldmOghp+G9PUIadJpwr2hsUF1Jz//7Dl3mLEfXgTpZALVza2Mg +9jFFCDkO9HB+QHBaP9BrQql0PSgvAm11cpUJjUhjxsYjV5KTXjXBjfkK9yydYhz2rXzdpjEetrHH +foUm+qRqtdpjMNHvkzeyZi99Bffnt0uYlDXA2TopwZ2yUDMdSqlapskD7+3056huirRXhOukP9Du +qqqHW2Pok+JrqNS4cnhrG+055F3Lm6qH1U9OAP7Zap88MQ8oAgF9mOinsKJknnn4SPIVqczmyETr +P3iZ8ntxPjzxmKfFGBI/5rsoM0LpRQp8bfKGeS/Fghl9CYl8slR2iK7ewfPM4W7bMdaTrpmg7yVq +c5iJWzouE4gev8CSlDQb4ye3ix5vQv/n6TebUB0tovkC7stYWDpxvGjjqsGvHCgfotwjZT+B6q6Z +09gwzxMNTxXJhLynSC34MCN32EZLeW32jO06f2ARePTpm67VVMB0gNELQp/B +-----END CERTIFICATE----- + +Go Daddy Root Certificate Authority - G2 +======================================== +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoTEUdvRGFkZHkuY29tLCBJbmMu +MTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8G +A1UEAxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKDE6bFIEMBO4Tx5oVJnyfq +9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD ++qK+ihVqf94Lw7YZFAXK6sOoBJQ7RnwyDfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutd +fMh8+7ArU6SSYmlRJQVhGkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMl +NAJWJwGRtDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEAAaNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFDqahQcQZyi27/a9 +BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmXWWcDYfF+OwYxdS2hII5PZYe096ac +vNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r +5N9ss4UXnT3ZJE95kTXWXwTrgIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYV +N8Gb5DKj7Tjo2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI4uJEvlz36hz1 +-----END CERTIFICATE----- + +Starfield Root Certificate Authority - G2 +========================================= +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s +b2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVsZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0 +eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAw +DgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQg +VGVjaG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZpY2F0ZSBB +dXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3twQP89o/8ArFv +W59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMgnLRJdzIpVv257IzdIvpy3Cdhl+72WoTs +bhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNk +N3mSwOxGXn/hbVNMYq/NHwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7Nf +ZTD4p7dNdloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0HZbU +JtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0GCSqGSIb3DQEBCwUAA4IBAQARWfol +TwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjUsHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx +4mcujJUDJi5DnUox9g61DLu34jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUw +F5okxBDgBPfg8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1mMpYjn0q7pBZ +c2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +Starfield Services Root Certificate Authority - G2 +================================================== +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s +b2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVsZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRl +IEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNV +BAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxT +dGFyZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2VydmljZXMg +Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20pOsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2 +h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm28xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4Pa +hHQUw2eeBGg6345AWh1KTs9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLP +LJGmpufehRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk6mFB +rMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+qAdcwKziIorhtSpzyEZGDMA0GCSqG +SIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMIbw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPP +E95Dz+I0swSdHynVv/heyNXBve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTy +xQGjhdByPq1zqwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn0q23KXB56jza +YyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCNsSi6 +-----END CERTIFICATE----- + +AffirmTrust Commercial +====================== +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UEBhMCVVMxFDAS +BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMB4XDTEw +MDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly +bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6Eqdb +DuKPHx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yrba0F8PrV +C8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPALMeIrJmqbTFeurCA+ukV6 +BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1yHp52UKqK39c/s4mT6NmgTWvRLpUHhww +MmWd5jyTXlBOeuM61G7MGvv50jeuJCqrVwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNV +HQ4EFgQUnZPGU4teyq8/nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYGXUPG +hi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNjvbz4YYCanrHOQnDi +qX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivtZ8SOyUOyXGsViQK8YvxO8rUzqrJv +0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9gN53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0kh +sUlHRUe072o0EclNmsxZt9YCnlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= +-----END CERTIFICATE----- + +AffirmTrust Networking +====================== +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UEBhMCVVMxFDAS +BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMB4XDTEw +MDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly +bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SE +Hi3yYJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbuakCNrmreI +dIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRLQESxG9fhwoXA3hA/Pe24 +/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gb +h+0t+nvujArjqWaJGctB+d1ENmHP4ndGyH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNV +HQ4EFgQUBx/S55zawm6iQLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfOtDIu +UFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzuQY0x2+c06lkh1QF6 +12S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZLgo/bNjR9eUJtGxUAArgFU2HdW23 +WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4uolu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9 +/ZFvgrG+CJPbFEfxojfHRZ48x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= +-----END CERTIFICATE----- + +AffirmTrust Premium +=================== +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UEBhMCVVMxFDAS +BgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMB4XDTEwMDEy +OTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRy +dXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEAxBLfqV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtn +BKAQJG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ+jjeRFcV +5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrSs8PhaJyJ+HoAVt70VZVs ++7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmd +GPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d770O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5R +p9EixAqnOEhss/n/fauGV+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NI +S+LI+H+SqHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S5u04 +6uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4IaC1nEWTJ3s7xgaVY5 +/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TXOwF0lkLgAOIua+rF7nKsu7/+6qqo ++Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYEFJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByv +MiPIs0laUZx2KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg +Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B8OWycvpEgjNC +6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQMKSOyARiqcTtNd56l+0OOF6S +L5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK ++4w1IX2COPKpVJEZNZOUbWo6xbLQu4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmV +BtWVyuEklut89pMFu+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFg +IxpHYoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8GKa1qF60 +g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaORtGdFNrHF+QFlozEJLUb +zxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6eKeC2uAloGRwYQw== +-----END CERTIFICATE----- + +AffirmTrust Premium ECC +======================= +-----BEGIN CERTIFICATE----- +MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMCVVMxFDASBgNV +BAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQcmVtaXVtIEVDQzAeFw0xMDAx +MjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1U +cnVzdDEgMB4GA1UEAwwXQWZmaXJtVHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAQNMF4bFZ0D0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQ +N8O9ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0GA1UdDgQW +BBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAK +BggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/VsaobgxCd05DhT1wV/GzTjxi+zygk8N53X +57hG8f2h4nECMEJZh0PUUd+60wkyWs6Iflc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKM +eQ== +-----END CERTIFICATE----- + +Certum Trusted Network CA +========================= +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK +ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIy +MTIwNzM3WhcNMjkxMjMxMTIwNzM3WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBU +ZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +MSIwIAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rHUV+rpDKmYYe2bg+G0jAC +l/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LMTXPb865Px1bVWqeWifrzq2jUI4ZZJ88J +J7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVUBBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4 +fOQtf/WsX+sWn7Et0brMkUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0 +cvW0QM8xAcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNVHRMB +Af8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNVHQ8BAf8EBAMCAQYw +DQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15ysHhE49wcrwn9I0j6vSrEuVUEtRCj +jSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfLI9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1 +mS1FhIrlQgnXdAIv94nYmem8J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5aj +Zt3hrvJBW8qYVoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- + +Certinomis - Autorité Racine +============================= +-----BEGIN CERTIFICATE----- +MIIFnDCCA4SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJGUjETMBEGA1UEChMK +Q2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxJjAkBgNVBAMMHUNlcnRpbm9taXMg +LSBBdXRvcml0w6kgUmFjaW5lMB4XDTA4MDkxNzA4Mjg1OVoXDTI4MDkxNzA4Mjg1OVowYzELMAkG +A1UEBhMCRlIxEzARBgNVBAoTCkNlcnRpbm9taXMxFzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMSYw +JAYDVQQDDB1DZXJ0aW5vbWlzIC0gQXV0b3JpdMOpIFJhY2luZTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAJ2Fn4bT46/HsmtuM+Cet0I0VZ35gb5j2CN2DpdUzZlMGvE5x4jYF1AMnmHa +wE5V3udauHpOd4cN5bjr+p5eex7Ezyh0x5P1FMYiKAT5kcOrJ3NqDi5N8y4oH3DfVS9O7cdxbwly +Lu3VMpfQ8Vh30WC8Tl7bmoT2R2FFK/ZQpn9qcSdIhDWerP5pqZ56XjUl+rSnSTV3lqc2W+HN3yNw +2F1MpQiD8aYkOBOo7C+ooWfHpi2GR+6K/OybDnT0K0kCe5B1jPyZOQE51kqJ5Z52qz6WKDgmi92N +jMD2AR5vpTESOH2VwnHu7XSu5DaiQ3XV8QCb4uTXzEIDS3h65X27uK4uIJPT5GHfceF2Z5c/tt9q +c1pkIuVC28+BA5PY9OMQ4HL2AHCs8MF6DwV/zzRpRbWT5BnbUhYjBYkOjUjkJW+zeL9i9Qf6lSTC +lrLooyPCXQP8w9PlfMl1I9f09bze5N/NgL+RiH2nE7Q5uiy6vdFrzPOlKO1Enn1So2+WLhl+HPNb +xxaOu2B9d2ZHVIIAEWBsMsGoOBvrbpgT1u449fCfDu/+MYHB0iSVL1N6aaLwD4ZFjliCK0wi1F6g +530mJ0jfJUaNSih8hp75mxpZuWW/Bd22Ql095gBIgl4g9xGC3srYn+Y3RyYe63j3YcNBZFgCQfna +4NH4+ej9Uji29YnfAgMBAAGjWzBZMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBQNjLZh2kS40RR9w759XkjwzspqsDAXBgNVHSAEEDAOMAwGCiqBegFWAgIAAQEwDQYJ +KoZIhvcNAQEFBQADggIBACQ+YAZ+He86PtvqrxyaLAEL9MW12Ukx9F1BjYkMTv9sov3/4gbIOZ/x +WqndIlgVqIrTseYyCYIDbNc/CMf4uboAbbnW/FIyXaR/pDGUu7ZMOH8oMDX/nyNTt7buFHAAQCva +R6s0fl6nVjBhK4tDrP22iCj1a7Y+YEq6QpA0Z43q619FVDsXrIvkxmUP7tCMXWY5zjKn2BCXwH40 +nJ+U8/aGH88bc62UeYdocMMzpXDn2NU4lG9jeeu/Cg4I58UvD0KgKxRA/yHgBcUn4YQRE7rWhh1B +CxMjidPJC+iKunqjo3M3NYB9Ergzd0A4wPpeMNLytqOx1qKVl4GbUu1pTP+A5FPbVFsDbVRfsbjv +JL1vnxHDx2TCDyhihWZeGnuyt++uNckZM6i4J9szVb9o4XVIRFb7zdNIu0eJOqxp9YDG5ERQL1TE +qkPFMTFYvZbF6nVsmnWxTfj3l/+WFvKXTej28xH5On2KOG4Ey+HTRRWqpdEdnV1j6CTmNhTih60b +WfVEm/vXd3wfAXBioSAaosUaKPQhA+4u2cGA6rnZgtZbdsLLO7XSAPCjDuGtbkD326C00EauFddE +wk01+dIL8hf2rGbVJLJP0RyZwG71fet0BLj5TXcJ17TPBzAJ8bgAVtkXFhYKK4bfjwEZGuW7gmP/ +vgt2Fl43N+bYdJeimUV5 +-----END CERTIFICATE----- + +Root CA Generalitat Valenciana +============================== +-----BEGIN CERTIFICATE----- +MIIGizCCBXOgAwIBAgIEO0XlaDANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJFUzEfMB0GA1UE +ChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJR1ZBMScwJQYDVQQDEx5Sb290 +IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwHhcNMDEwNzA2MTYyMjQ3WhcNMjEwNzAxMTUyMjQ3 +WjBoMQswCQYDVQQGEwJFUzEfMB0GA1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UE +CxMGUEtJR1ZBMScwJQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGKqtXETcvIorKA3Qdyu0togu8M1JAJke+WmmmO3I2 +F0zo37i7L3bhQEZ0ZQKQUgi0/6iMweDHiVYQOTPvaLRfX9ptI6GJXiKjSgbwJ/BXufjpTjJ3Cj9B +ZPPrZe52/lSqfR0grvPXdMIKX/UIKFIIzFVd0g/bmoGlu6GzwZTNVOAydTGRGmKy3nXiz0+J2ZGQ +D0EbtFpKd71ng+CT516nDOeB0/RSrFOyA8dEJvt55cs0YFAQexvba9dHq198aMpunUEDEO5rmXte +JajCq+TA81yc477OMUxkHl6AovWDfgzWyoxVjr7gvkkHD6MkQXpYHYTqWBLI4bft75PelAgxAgMB +AAGjggM7MIIDNzAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAGGFmh0dHA6Ly9vY3NwLnBraS5n +dmEuZXMwEgYDVR0TAQH/BAgwBgEB/wIBAjCCAjQGA1UdIASCAiswggInMIICIwYKKwYBBAG/VQIB +ADCCAhMwggHoBggrBgEFBQcCAjCCAdoeggHWAEEAdQB0AG8AcgBpAGQAYQBkACAAZABlACAAQwBl +AHIAdABpAGYAaQBjAGEAYwBpAPMAbgAgAFIAYQDtAHoAIABkAGUAIABsAGEAIABHAGUAbgBlAHIA +YQBsAGkAdABhAHQAIABWAGEAbABlAG4AYwBpAGEAbgBhAC4ADQAKAEwAYQAgAEQAZQBjAGwAYQBy +AGEAYwBpAPMAbgAgAGQAZQAgAFAAcgDhAGMAdABpAGMAYQBzACAAZABlACAAQwBlAHIAdABpAGYA +aQBjAGEAYwBpAPMAbgAgAHEAdQBlACAAcgBpAGcAZQAgAGUAbAAgAGYAdQBuAGMAaQBvAG4AYQBt +AGkAZQBuAHQAbwAgAGQAZQAgAGwAYQAgAHAAcgBlAHMAZQBuAHQAZQAgAEEAdQB0AG8AcgBpAGQA +YQBkACAAZABlACAAQwBlAHIAdABpAGYAaQBjAGEAYwBpAPMAbgAgAHMAZQAgAGUAbgBjAHUAZQBu +AHQAcgBhACAAZQBuACAAbABhACAAZABpAHIAZQBjAGMAaQDzAG4AIAB3AGUAYgAgAGgAdAB0AHAA +OgAvAC8AdwB3AHcALgBwAGsAaQAuAGcAdgBhAC4AZQBzAC8AYwBwAHMwJQYIKwYBBQUHAgEWGWh0 +dHA6Ly93d3cucGtpLmd2YS5lcy9jcHMwHQYDVR0OBBYEFHs100DSHHgZZu90ECjcPk+yeAT8MIGV +BgNVHSMEgY0wgYqAFHs100DSHHgZZu90ECjcPk+yeAT8oWykajBoMQswCQYDVQQGEwJFUzEfMB0G +A1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJR1ZBMScwJQYDVQQDEx5S +b290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmGCBDtF5WgwDQYJKoZIhvcNAQEFBQADggEBACRh +TvW1yEICKrNcda3FbcrnlD+laJWIwVTAEGmiEi8YPyVQqHxK6sYJ2fR1xkDar1CdPaUWu20xxsdz +Ckj+IHLtb8zog2EWRpABlUt9jppSCS/2bxzkoXHPjCpaF3ODR00PNvsETUlR4hTJZGH71BTg9J63 +NI8KJr2XXPR5OkowGcytT6CYirQxlyric21+eLj4iIlPsSKRZEv1UN4D2+XFducTZnV+ZfsBn5OH +iJ35Rld8TWCvmHMTI6QgkYH60GFmuH3Rr9ZvHmw96RH9qfmCIoaZM3Fa6hlXPZHNqcCjbgcTpsnt ++GijnsNacgmHKNHEc8RzGF9QdRYxn7fofMM= +-----END CERTIFICATE----- + +A-Trust-nQual-03 +================ +-----BEGIN CERTIFICATE----- +MIIDzzCCAregAwIBAgIDAWweMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYDVQQGEwJBVDFIMEYGA1UE +Cgw/QS1UcnVzdCBHZXMuIGYuIFNpY2hlcmhlaXRzc3lzdGVtZSBpbSBlbGVrdHIuIERhdGVudmVy +a2VociBHbWJIMRkwFwYDVQQLDBBBLVRydXN0LW5RdWFsLTAzMRkwFwYDVQQDDBBBLVRydXN0LW5R +dWFsLTAzMB4XDTA1MDgxNzIyMDAwMFoXDTE1MDgxNzIyMDAwMFowgY0xCzAJBgNVBAYTAkFUMUgw +RgYDVQQKDD9BLVRydXN0IEdlcy4gZi4gU2ljaGVyaGVpdHNzeXN0ZW1lIGltIGVsZWt0ci4gRGF0 +ZW52ZXJrZWhyIEdtYkgxGTAXBgNVBAsMEEEtVHJ1c3QtblF1YWwtMDMxGTAXBgNVBAMMEEEtVHJ1 +c3QtblF1YWwtMDMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtPWFuA/OQO8BBC4SA +zewqo51ru27CQoT3URThoKgtUaNR8t4j8DRE/5TrzAUjlUC5B3ilJfYKvUWG6Nm9wASOhURh73+n +yfrBJcyFLGM/BWBzSQXgYHiVEEvc+RFZznF/QJuKqiTfC0Li21a8StKlDJu3Qz7dg9MmEALP6iPE +SU7l0+m0iKsMrmKS1GWH2WrX9IWf5DMiJaXlyDO6w8dB3F/GaswADm0yqLaHNgBid5seHzTLkDx4 +iHQF63n1k3Flyp3HaxgtPVxO59X4PzF9j4fsCiIvI+n+u33J4PTs63zEsMMtYrWacdaxaujs2e3V +cuy+VwHOBVWf3tFgiBCzAgMBAAGjNjA0MA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0OBAoECERqlWdV +eRFPMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAVdRU0VlIXLOThaq/Yy/kgM40 +ozRiPvbY7meIMQQDbwvUB/tOdQ/TLtPAF8fGKOwGDREkDg6lXb+MshOWcdzUzg4NCmgybLlBMRmr +sQd7TZjTXLDR8KdCoLXEjq/+8T/0709GAHbrAvv5ndJAlseIOrifEXnzgGWovR/TeIGgUUw3tKZd +JXDRZslo+S4RFGjxVJgIrCaSD96JntT6s3kr0qN51OyLrIdTaEJMUVF0HhsnLuP1Hyl0Te2v9+GS +mYHovjrHF1D2t8b8m7CKa9aIA5GPBnc6hQLdmNVDeD/GMBWsm2vLV7eJUYs66MmEDNuxUCAKGkq6 +ahq97BvIxYSazQ== +-----END CERTIFICATE----- + +TWCA Root Certification Authority +================================= +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJ +VEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMzWhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQG +EwJUVzESMBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NB +IFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFEAcK0HMMx +QhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HHK3XLfJ+utdGdIzdjp9xC +oi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeXRfwZVzsrb+RH9JlF/h3x+JejiB03HFyP +4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/zrX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1r +y+UPizgN7gr8/g+YnzAx3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIB +BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkqhkiG +9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeCMErJk/9q56YAf4lC +mtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdlsXebQ79NqZp4VKIV66IIArB6nCWlW +QtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62Dlhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVY +T0bf+215WfKEIlKuD8z7fDvnaspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocny +Yh0igzyXxfkZYiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- + +Security Communication RootCA2 +============================== +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDElMCMGA1UEChMc +U0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMeU2VjdXJpdHkgQ29tbXVuaWNh +dGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoXDTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMC +SlAxJTAjBgNVBAoTHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3Vy +aXR5IENvbW11bmljYXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ANAVOVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGrzbl+dp++ ++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVMVAX3NuRFg3sUZdbcDE3R +3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQhNBqyjoGADdH5H5XTz+L62e4iKrFvlNV +spHEfbmwhRkGeC7bYRr6hfVKkaHnFtWOojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1K +EOtOghY6rCcMU/Gt1SSwawNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8 +QIH4D5csOPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB +CwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpFcoJxDjrSzG+ntKEj +u/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXcokgfGT+Ok+vx+hfuzU7jBBJV1uXk +3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6q +tnRGEmyR7jTV7JqR50S+kDFy1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29 +mvVXIwAHIRc/SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- + +EC-ACC +====== +-----BEGIN CERTIFICATE----- +MIIFVjCCBD6gAwIBAgIQ7is969Qh3hSoYqwE893EATANBgkqhkiG9w0BAQUFADCB8zELMAkGA1UE +BhMCRVMxOzA5BgNVBAoTMkFnZW5jaWEgQ2F0YWxhbmEgZGUgQ2VydGlmaWNhY2lvIChOSUYgUS0w +ODAxMTc2LUkpMSgwJgYDVQQLEx9TZXJ2ZWlzIFB1YmxpY3MgZGUgQ2VydGlmaWNhY2lvMTUwMwYD +VQQLEyxWZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAoYykwMzE1MDMGA1UE +CxMsSmVyYXJxdWlhIEVudGl0YXRzIGRlIENlcnRpZmljYWNpbyBDYXRhbGFuZXMxDzANBgNVBAMT +BkVDLUFDQzAeFw0wMzAxMDcyMzAwMDBaFw0zMTAxMDcyMjU5NTlaMIHzMQswCQYDVQQGEwJFUzE7 +MDkGA1UEChMyQWdlbmNpYSBDYXRhbGFuYSBkZSBDZXJ0aWZpY2FjaW8gKE5JRiBRLTA4MDExNzYt +SSkxKDAmBgNVBAsTH1NlcnZlaXMgUHVibGljcyBkZSBDZXJ0aWZpY2FjaW8xNTAzBgNVBAsTLFZl +Z2V1IGh0dHBzOi8vd3d3LmNhdGNlcnQubmV0L3ZlcmFycmVsIChjKTAzMTUwMwYDVQQLEyxKZXJh +cnF1aWEgRW50aXRhdHMgZGUgQ2VydGlmaWNhY2lvIENhdGFsYW5lczEPMA0GA1UEAxMGRUMtQUND +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyLHT+KXQpWIR4NA9h0X84NzJB5R85iK +w5K4/0CQBXCHYMkAqbWUZRkiFRfCQ2xmRJoNBD45b6VLeqpjt4pEndljkYRm4CgPukLjbo73FCeT +ae6RDqNfDrHrZqJyTxIThmV6PttPB/SnCWDaOkKZx7J/sxaVHMf5NLWUhdWZXqBIoH7nF2W4onW4 +HvPlQn2v7fOKSGRdghST2MDk/7NQcvJ29rNdQlB50JQ+awwAvthrDk4q7D7SzIKiGGUzE3eeml0a +E9jD2z3Il3rucO2n5nzbcc8tlGLfbdb1OL4/pYUKGbio2Al1QnDE6u/LDsg0qBIimAy4E5S2S+zw +0JDnJwIDAQABo4HjMIHgMB0GA1UdEQQWMBSBEmVjX2FjY0BjYXRjZXJ0Lm5ldDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUoMOLRKo3pUW/l4Ba0fF4opvpXY0wfwYD +VR0gBHgwdjB0BgsrBgEEAfV4AQMBCjBlMCwGCCsGAQUFBwIBFiBodHRwczovL3d3dy5jYXRjZXJ0 +Lm5ldC92ZXJhcnJlbDA1BggrBgEFBQcCAjApGidWZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5l +dC92ZXJhcnJlbCAwDQYJKoZIhvcNAQEFBQADggEBAKBIW4IB9k1IuDlVNZyAelOZ1Vr/sXE7zDkJ +lF7W2u++AVtd0x7Y/X1PzaBB4DSTv8vihpw3kpBWHNzrKQXlxJ7HNd+KDM3FIUPpqojlNcAZQmNa +Al6kSBg6hW/cnbw/nZzBh7h6YQjpdwt/cKt63dmXLGQehb+8dJahw3oS7AwaboMMPOhyRp/7SNVe +l+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOSAgu+TGbrIP65y7WZf+a2 +E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xlnJ2lYJU6Un/10asIbvPuW/mIPX64b24D +5EI= +-----END CERTIFICATE----- + +Hellenic Academic and Research Institutions RootCA 2011 +======================================================= +-----BEGIN CERTIFICATE----- +MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1IxRDBCBgNVBAoT +O0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9y +aXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25z +IFJvb3RDQSAyMDExMB4XDTExMTIwNjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYT +AkdSMUQwQgYDVQQKEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25z +IENlcnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNo +IEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPzdYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI +1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJfel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa +71HFK9+WXesyHgLacEnsbgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u +8yBRQlqD75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSPFEDH +3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNVHRMBAf8EBTADAQH/ +MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp5dgTBCPuQSUwRwYDVR0eBEAwPqA8 +MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQub3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQu +b3JnMA0GCSqGSIb3DQEBBQUAA4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVt +XdMiKahsog2p6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8 +TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7dIsXRSZMFpGD +/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8AcysNnq/onN694/BtZqhFLKPM58N +7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXIl7WdmplNsDz4SgCbZN2fOUvRJ9e4 +-----END CERTIFICATE----- diff --git a/libs/certifi/core.py b/libs/certifi/core.py new file mode 100644 index 00000000..20020692 --- /dev/null +++ b/libs/certifi/core.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +ceritfi.py +~~~~~~~~~~ + +This module returns the installation location of cacert.pem. +""" + +import os + +def where(): + f = os.path.split(__file__)[0] + + return os.path.join(f, 'cacert.pem') + +if __name__ == '__main__': + print(where()) diff --git a/libs/guessit/ISO-3166-1_utf8.txt b/libs/guessit/ISO-3166-1_utf8.txt old mode 100644 new mode 100755 diff --git a/libs/guessit/ISO-639-2_utf-8.txt b/libs/guessit/ISO-639-2_utf-8.txt old mode 100644 new mode 100755 diff --git a/libs/guessit/__init__.py b/libs/guessit/__init__.py old mode 100644 new mode 100755 index 9c7c9d06..3880e20c --- a/libs/guessit/__init__.py +++ b/libs/guessit/__init__.py @@ -18,7 +18,7 @@ # along with this program. If not, see . # -__version__ = '0.4' +__version__ = '0.5-dev' __all__ = ['Guess', 'Language', 'guess_file_info', 'guess_video_info', 'guess_movie_info', 'guess_episode_info'] diff --git a/libs/guessit/country.py b/libs/guessit/country.py old mode 100644 new mode 100755 index f5297281..b5f6d6e2 --- a/libs/guessit/country.py +++ b/libs/guessit/country.py @@ -20,6 +20,7 @@ from __future__ import unicode_literals from guessit import fileutils +from guessit.textutils import to_unicode import logging log = logging.getLogger(__name__) @@ -66,7 +67,8 @@ class Country(object): """ def __init__(self, country, strict=False): - self.alpha3 = country_to_alpha3.get(country.lower()) + country = to_unicode(country.strip().lower()) + self.alpha3 = country_to_alpha3.get(country) if self.alpha3 is None and strict: msg = 'The given string "%s" could not be identified as a country' diff --git a/libs/guessit/date.py b/libs/guessit/date.py old mode 100644 new mode 100755 diff --git a/libs/guessit/fileutils.py b/libs/guessit/fileutils.py old mode 100644 new mode 100755 index bd315bc1..16e7348d --- a/libs/guessit/fileutils.py +++ b/libs/guessit/fileutils.py @@ -29,6 +29,7 @@ def split_path(path): If the given path was an absolute path, the first element will always be: - the '/' root folder on Unix systems - the drive letter on Windows systems (eg: r'C:\') + - the mount point '\\' on Windows systems (eg: r'\\host\share') >>> split_path('/usr/bin/smewt') ['/', 'usr', 'bin', 'smewt'] @@ -36,12 +37,6 @@ def split_path(path): >>> split_path('relative_path/to/my_folder/') ['relative_path', 'to', 'my_folder'] - >>> split_path(r'C:\Program Files\Smewt\smewt.exe') - ['C:\\', 'Program Files', 'Smewt', 'smewt.exe'] - - >>> split_path(r'Documents and Settings\User\config\\') - ['Documents and Settings', 'User', 'config'] - """ result = [] while True: diff --git a/libs/guessit/guess.py b/libs/guessit/guess.py old mode 100644 new mode 100755 diff --git a/libs/guessit/hash_ed2k.py b/libs/guessit/hash_ed2k.py old mode 100644 new mode 100755 diff --git a/libs/guessit/hash_mpc.py b/libs/guessit/hash_mpc.py old mode 100644 new mode 100755 diff --git a/libs/guessit/language.py b/libs/guessit/language.py old mode 100644 new mode 100755 index b0433469..c1fa32f2 --- a/libs/guessit/language.py +++ b/libs/guessit/language.py @@ -21,11 +21,13 @@ from __future__ import unicode_literals from guessit import fileutils from guessit.country import Country +from guessit.textutils import to_unicode import re import logging __all__ = [ 'is_iso_language', 'is_language', 'lang_set', 'Language', - 'ALL_LANGUAGES', 'ALL_LANGUAGES_NAMES', 'search_language' ] + 'ALL_LANGUAGES', 'ALL_LANGUAGES_NAMES', 'UNDETERMINED', + 'search_language' ] log = logging.getLogger(__name__) @@ -46,14 +48,21 @@ _iso639_contents = _iso639_contents[1:] language_matrix = [ l.strip().split('|') for l in _iso639_contents.strip().split('\n') ] -language_matrix += [ [ 'unk', '', 'un', 'Unknown', 'inconnu' ] ] +# update information in the language matrix +language_matrix += [['mol', '', 'mo', 'Moldavian', 'moldave'], + ['ass', '', '', 'Assyrian', 'assyrien']] -# remove unused languages that shadow other common ones with a non-official form for lang in language_matrix: + # remove unused languages that shadow other common ones with a non-official form if (lang[2] == 'se' or # Northern Sami shadows Swedish lang[2] == 'br'): # Breton shadows Brazilian - language_matrix.remove(lang) + lang[2] = '' + # add missing information + if lang[0] == 'und': + lang[2] = 'un' + if lang[0] == 'srp': + lang[1] = 'scc' # from OpenSubtitles lng3 = frozenset(l[0] for l in language_matrix if l[0]) @@ -87,12 +96,17 @@ lng_fr_name_to_lng3 = dict((fr_name.lower(), l[0]) # contains a list of exceptions: strings that should be parsed as a language # but which are not in an ISO form -lng_exceptions = { 'gr': ('gre', None), +lng_exceptions = { 'unknown': ('und', None), + 'inconnu': ('und', None), + 'unk': ('und', None), + 'un': ('und', None), + 'gr': ('gre', None), 'greek': ('gre', None), 'esp': ('spa', None), 'español': ('spa', None), 'se': ('swe', None), 'po': ('pt', 'br'), + 'pb': ('pt', 'br'), 'pob': ('pt', 'br'), 'br': ('pt', 'br'), 'brazilian': ('pt', 'br'), @@ -101,7 +115,8 @@ lng_exceptions = { 'gr': ('gre', None), 'ua': ('ukr', None), 'cn': ('chi', None), 'chs': ('chi', None), - 'jp': ('jpn', None) + 'jp': ('jpn', None), + 'scr': ('hrv', None) } @@ -130,6 +145,11 @@ class Language(object): You can also distinguish languages for specific countries, such as Portuguese and Brazilian Portuguese. + There are various properties on the language object that give you the + representation of the language for a specific usage, such as .alpha3 + to get the ISO 3-letter code, or .opensubtitles to get the OpenSubtitles + language code. + >>> Language('fr') Language(French) @@ -146,16 +166,19 @@ class Language(object): True >>> Language('zz', strict=False).english_name - u'Unknown' + u'Undetermined' + + >>> Language('pt(br)').opensubtitles + u'pob' """ _with_country_regexp = re.compile('(.*)\((.*)\)') + _with_country_regexp2 = re.compile('(.*)-(.*)') - def __init__(self, language, country=None, strict=False): - language = language.strip().lower() - if isinstance(language, str): - language = language.decode('utf-8') - with_country = Language._with_country_regexp.match(language) + def __init__(self, language, country=None, strict=False, scheme=None): + language = to_unicode(language.strip().lower()) + with_country = (Language._with_country_regexp.match(language) or + Language._with_country_regexp2.match(language)) if with_country: self.lang = Language(with_country.group(1)).lang self.country = Country(with_country.group(2)) @@ -164,6 +187,18 @@ class Language(object): self.lang = None self.country = Country(country) if country else None + # first look for scheme specific languages + if scheme == 'opensubtitles': + if language == 'br': + self.lang = 'bre' + return + elif language == 'se': + self.lang = 'sme' + return + elif scheme is not None: + log.warning('Unrecognized scheme: "%s" - Proceeding with standard one' % scheme) + + # look for ISO language codes if len(language) == 2: self.lang = lng2_to_lng3.get(language) elif len(language) == 3: @@ -174,6 +209,7 @@ class Language(object): self.lang = (lng_en_name_to_lng3.get(language) or lng_fr_name_to_lng3.get(language)) + # general language exceptions if self.lang is None and language in lng_exceptions: lang, country = lng_exceptions[language] self.lang = Language(lang).alpha3 @@ -186,7 +222,7 @@ class Language(object): if self.lang is None: log.debug(msg) - self.lang = 'unk' + self.lang = 'und' @property def alpha2(self): @@ -208,6 +244,20 @@ class Language(object): def french_name(self): return lng3_to_lng_fr_name[self.lang] + @property + def opensubtitles(self): + if self.lang == 'por' and self.country and self.country.alpha2 == 'br': + return 'pob' + elif self.lang in ['gre', 'srp']: + return self.alpha3term + return self.alpha3 + + @property + def tmdb(self): + if self.country: + return '%s-%s' % (self.alpha2, self.country.alpha2.upper()) + return self.alpha2 + def __hash__(self): return hash(self.lang) @@ -227,7 +277,7 @@ class Language(object): return not self == other def __nonzero__(self): - return self.lang != 'unk' + return self.lang != 'und' def __unicode__(self): if self.country: @@ -245,7 +295,8 @@ class Language(object): return 'Language(%s)' % self.english_name -ALL_LANGUAGES = frozenset(Language(lng) for lng in lng_all_names) - frozenset([Language('unk')]) +UNDETERMINED = Language('und') +ALL_LANGUAGES = frozenset(Language(lng) for lng in lng_all_names) - frozenset([UNDETERMINED]) ALL_LANGUAGES_NAMES = lng_all_names def search_language(string, lang_filter=None): diff --git a/libs/guessit/matcher.py b/libs/guessit/matcher.py old mode 100644 new mode 100755 diff --git a/libs/guessit/matchtree.py b/libs/guessit/matchtree.py old mode 100644 new mode 100755 diff --git a/libs/guessit/patterns.py b/libs/guessit/patterns.py index 42235859..5fa540ce 100755 --- a/libs/guessit/patterns.py +++ b/libs/guessit/patterns.py @@ -40,10 +40,10 @@ episode_rexps = [ # ... Season 2 ... (r'saison (?P[0-9]+)', 1.0, (0, 0)), # ... s02e13 ... - (r'[Ss](?P[0-9]{1,2}).{,3}[EeXx](?P[0-9]{1,2})[^0-9]', 1.0, (0, -1)), + (r'[Ss](?P[0-9]{1,2}).{,3}(?P(?:[EeXx][0-9]{1,2})+)[^0-9]', 1.0, (0, -1)), # ... 2x13 ... - (r'[^0-9](?P[0-9]{1,2})x(?P[0-9]{2})[^0-9]', 0.8, (1, -1)), + (r'[^0-9](?P[0-9]{1,2})(?P(?:[xX][0-9]{1,2})+)[^0-9]', 0.8, (1, -1)), # ... s02 ... #(sep + r's(?P[0-9]{1,2})' + sep, 0.6, (1, -1)), @@ -61,7 +61,7 @@ weak_episode_rexps = [ # ... 213 or 0106 ... (sep + r'(?P[0-9]{1,4})' + sep, (1, -1)), # ... 2x13 ... - (sep + r'[^0-9](?P[0-9]{1,2})\.(?P[0-9]{2})[^0-9]' + sep, (1, -1)), + (sep + r'[^0-9](?P[0-9]{1,2})\.(?P[0-9]{1,2})[^0-9]' + sep, (1, -1)), # ... e13 ... for a mini-series without a season number (r'e(?P[0-9]{1,4})[^0-9]', (0, -1)), diff --git a/libs/guessit/slogging.py b/libs/guessit/slogging.py old mode 100644 new mode 100755 diff --git a/libs/guessit/textutils.py b/libs/guessit/textutils.py old mode 100644 new mode 100755 index cfe31c4d..82c82b97 --- a/libs/guessit/textutils.py +++ b/libs/guessit/textutils.py @@ -19,6 +19,7 @@ # from guessit.patterns import sep +import unicodedata import copy # string-related functions @@ -70,6 +71,7 @@ def to_utf8(o): return [ to_utf8(i) for i in o ] elif isinstance(o, dict): # need to do it like that to handle Guess instances correctly + # FIXME: why is that necessary? result = copy.deepcopy(o) for key, value in o.items(): result[to_utf8(key)] = to_utf8(value) @@ -78,6 +80,26 @@ def to_utf8(o): else: return o +def to_unicode(o): + """Convert all strings found in the given object to normalized + unicode strings, using the UTF-8 codec if needed.""" + + if isinstance(o, unicode): + return unicodedata.normalize('NFC', o) + if isinstance(o, str): + return unicodedata.normalize('NFC', o.decode('utf-8')) + elif isinstance(o, list): + return [ to_unicode(i) for i in o ] + elif isinstance(o, dict): + # need to do it like that to handle Guess instances correctly + #result = copy.deepcopy(o) + for key, value in o.items(): + result[to_unicode(key)] = to_unicode(value) + return result + + else: + return o + def levenshtein(a, b): if not a: diff --git a/libs/guessit/transfo/__init__.py b/libs/guessit/transfo/__init__.py old mode 100644 new mode 100755 diff --git a/libs/guessit/transfo/guess_bonus_features.py b/libs/guessit/transfo/guess_bonus_features.py old mode 100644 new mode 100755 diff --git a/libs/guessit/transfo/guess_date.py b/libs/guessit/transfo/guess_date.py old mode 100644 new mode 100755 diff --git a/libs/guessit/transfo/guess_episode_info_from_position.py b/libs/guessit/transfo/guess_episode_info_from_position.py old mode 100644 new mode 100755 diff --git a/libs/guessit/transfo/guess_episodes_rexps.py b/libs/guessit/transfo/guess_episodes_rexps.py old mode 100644 new mode 100755 index dfaa944d..690c3ed8 --- a/libs/guessit/transfo/guess_episodes_rexps.py +++ b/libs/guessit/transfo/guess_episodes_rexps.py @@ -26,14 +26,31 @@ import logging log = logging.getLogger(__name__) +def number_list(s): + return re.sub('[^0-9]+', ' ', s).split() def guess_episodes_rexps(string): for rexp, confidence, span_adjust in episode_rexps: match = re.search(rexp, string, re.IGNORECASE) if match: - return (Guess(match.groupdict(), confidence=confidence), - (match.start() + span_adjust[0], - match.end() + span_adjust[1])) + result = (Guess(match.groupdict(), confidence=confidence), + (match.start() + span_adjust[0], + match.end() + span_adjust[1])) + # episodes which have a season > 25 are most likely errors + # (Simpsons is at 23!) + if int(result[0].get('season', 0)) > 25: + continue + + # decide whether we have only a single episode number or an + # episode list + if result[0].get('episodeNumber'): + eplist = number_list(result[0]['episodeNumber']) + result[0].set('episodeNumber', int(eplist[0]), confidence=confidence) + + if len(eplist) > 1: + result[0].set('episodeList', map(int, eplist), confidence=confidence) + + return result return None, None diff --git a/libs/guessit/transfo/guess_filetype.py b/libs/guessit/transfo/guess_filetype.py old mode 100644 new mode 100755 index bf0a80ac..56fb3fe9 --- a/libs/guessit/transfo/guess_filetype.py +++ b/libs/guessit/transfo/guess_filetype.py @@ -87,6 +87,9 @@ def guess_filetype(filename, filetype): upgrade_episode() break + if 'tvu.org.ru' in filename: + upgrade_episode() + # if no episode info found, assume it's a movie upgrade_movie() diff --git a/libs/guessit/transfo/guess_language.py b/libs/guessit/transfo/guess_language.py old mode 100644 new mode 100755 diff --git a/libs/guessit/transfo/guess_movie_title_from_position.py b/libs/guessit/transfo/guess_movie_title_from_position.py old mode 100644 new mode 100755 diff --git a/libs/guessit/transfo/guess_properties.py b/libs/guessit/transfo/guess_properties.py old mode 100644 new mode 100755 diff --git a/libs/guessit/transfo/guess_release_group.py b/libs/guessit/transfo/guess_release_group.py old mode 100644 new mode 100755 diff --git a/libs/guessit/transfo/guess_video_rexps.py b/libs/guessit/transfo/guess_video_rexps.py old mode 100644 new mode 100755 diff --git a/libs/guessit/transfo/guess_weak_episodes_rexps.py b/libs/guessit/transfo/guess_weak_episodes_rexps.py old mode 100644 new mode 100755 diff --git a/libs/guessit/transfo/guess_website.py b/libs/guessit/transfo/guess_website.py old mode 100644 new mode 100755 diff --git a/libs/guessit/transfo/guess_year.py b/libs/guessit/transfo/guess_year.py old mode 100644 new mode 100755 diff --git a/libs/guessit/transfo/post_process.py b/libs/guessit/transfo/post_process.py old mode 100644 new mode 100755 diff --git a/libs/guessit/transfo/split_explicit_groups.py b/libs/guessit/transfo/split_explicit_groups.py old mode 100644 new mode 100755 diff --git a/libs/guessit/transfo/split_on_dash.py b/libs/guessit/transfo/split_on_dash.py old mode 100644 new mode 100755 diff --git a/libs/guessit/transfo/split_path_components.py b/libs/guessit/transfo/split_path_components.py old mode 100644 new mode 100755 diff --git a/libs/html5lib/__init__.py b/libs/html5lib/__init__.py new file mode 100644 index 00000000..16537aad --- /dev/null +++ b/libs/html5lib/__init__.py @@ -0,0 +1,17 @@ +""" +HTML parsing library based on the WHATWG "HTML5" +specification. The parser is designed to be compatible with existing +HTML found in the wild and implements well-defined error recovery that +is largely compatible with modern desktop web browsers. + +Example usage: + +import html5lib +f = open("my_document.html") +tree = html5lib.parse(f) +""" +__version__ = "0.95-dev" +from html5parser import HTMLParser, parse, parseFragment +from treebuilders import getTreeBuilder +from treewalkers import getTreeWalker +from serializer import serialize diff --git a/libs/html5lib/constants.py b/libs/html5lib/constants.py new file mode 100644 index 00000000..b533018e --- /dev/null +++ b/libs/html5lib/constants.py @@ -0,0 +1,3085 @@ +import string, gettext +_ = gettext.gettext + +try: + frozenset +except NameError: + # Import from the sets module for python 2.3 + from sets import Set as set + from sets import ImmutableSet as frozenset + +EOF = None + +E = { + "null-character": + _(u"Null character in input stream, replaced with U+FFFD."), + "invalid-codepoint": + _(u"Invalid codepoint in stream."), + "incorrectly-placed-solidus": + _(u"Solidus (/) incorrectly placed in tag."), + "incorrect-cr-newline-entity": + _(u"Incorrect CR newline entity, replaced with LF."), + "illegal-windows-1252-entity": + _(u"Entity used with illegal number (windows-1252 reference)."), + "cant-convert-numeric-entity": + _(u"Numeric entity couldn't be converted to character " + u"(codepoint U+%(charAsInt)08x)."), + "illegal-codepoint-for-numeric-entity": + _(u"Numeric entity represents an illegal codepoint: " + u"U+%(charAsInt)08x."), + "numeric-entity-without-semicolon": + _(u"Numeric entity didn't end with ';'."), + "expected-numeric-entity-but-got-eof": + _(u"Numeric entity expected. Got end of file instead."), + "expected-numeric-entity": + _(u"Numeric entity expected but none found."), + "named-entity-without-semicolon": + _(u"Named entity didn't end with ';'."), + "expected-named-entity": + _(u"Named entity expected. Got none."), + "attributes-in-end-tag": + _(u"End tag contains unexpected attributes."), + 'self-closing-flag-on-end-tag': + _(u"End tag contains unexpected self-closing flag."), + "expected-tag-name-but-got-right-bracket": + _(u"Expected tag name. Got '>' instead."), + "expected-tag-name-but-got-question-mark": + _(u"Expected tag name. Got '?' instead. (HTML doesn't " + u"support processing instructions.)"), + "expected-tag-name": + _(u"Expected tag name. Got something else instead"), + "expected-closing-tag-but-got-right-bracket": + _(u"Expected closing tag. Got '>' instead. Ignoring ''."), + "expected-closing-tag-but-got-eof": + _(u"Expected closing tag. Unexpected end of file."), + "expected-closing-tag-but-got-char": + _(u"Expected closing tag. Unexpected character '%(data)s' found."), + "eof-in-tag-name": + _(u"Unexpected end of file in the tag name."), + "expected-attribute-name-but-got-eof": + _(u"Unexpected end of file. Expected attribute name instead."), + "eof-in-attribute-name": + _(u"Unexpected end of file in attribute name."), + "invalid-character-in-attribute-name": + _(u"Invalid chracter in attribute name"), + "duplicate-attribute": + _(u"Dropped duplicate attribute on tag."), + "expected-end-of-tag-name-but-got-eof": + _(u"Unexpected end of file. Expected = or end of tag."), + "expected-attribute-value-but-got-eof": + _(u"Unexpected end of file. Expected attribute value."), + "expected-attribute-value-but-got-right-bracket": + _(u"Expected attribute value. Got '>' instead."), + 'equals-in-unquoted-attribute-value': + _(u"Unexpected = in unquoted attribute"), + 'unexpected-character-in-unquoted-attribute-value': + _(u"Unexpected character in unquoted attribute"), + "invalid-character-after-attribute-name": + _(u"Unexpected character after attribute name."), + "unexpected-character-after-attribute-value": + _(u"Unexpected character after attribute value."), + "eof-in-attribute-value-double-quote": + _(u"Unexpected end of file in attribute value (\")."), + "eof-in-attribute-value-single-quote": + _(u"Unexpected end of file in attribute value (')."), + "eof-in-attribute-value-no-quotes": + _(u"Unexpected end of file in attribute value."), + "unexpected-EOF-after-solidus-in-tag": + _(u"Unexpected end of file in tag. Expected >"), + "unexpected-character-after-soldius-in-tag": + _(u"Unexpected character after / in tag. Expected >"), + "expected-dashes-or-doctype": + _(u"Expected '--' or 'DOCTYPE'. Not found."), + "unexpected-bang-after-double-dash-in-comment": + _(u"Unexpected ! after -- in comment"), + "unexpected-space-after-double-dash-in-comment": + _(u"Unexpected space after -- in comment"), + "incorrect-comment": + _(u"Incorrect comment."), + "eof-in-comment": + _(u"Unexpected end of file in comment."), + "eof-in-comment-end-dash": + _(u"Unexpected end of file in comment (-)"), + "unexpected-dash-after-double-dash-in-comment": + _(u"Unexpected '-' after '--' found in comment."), + "eof-in-comment-double-dash": + _(u"Unexpected end of file in comment (--)."), + "eof-in-comment-end-space-state": + _(u"Unexpected end of file in comment."), + "eof-in-comment-end-bang-state": + _(u"Unexpected end of file in comment."), + "unexpected-char-in-comment": + _(u"Unexpected character in comment found."), + "need-space-after-doctype": + _(u"No space after literal string 'DOCTYPE'."), + "expected-doctype-name-but-got-right-bracket": + _(u"Unexpected > character. Expected DOCTYPE name."), + "expected-doctype-name-but-got-eof": + _(u"Unexpected end of file. Expected DOCTYPE name."), + "eof-in-doctype-name": + _(u"Unexpected end of file in DOCTYPE name."), + "eof-in-doctype": + _(u"Unexpected end of file in DOCTYPE."), + "expected-space-or-right-bracket-in-doctype": + _(u"Expected space or '>'. Got '%(data)s'"), + "unexpected-end-of-doctype": + _(u"Unexpected end of DOCTYPE."), + "unexpected-char-in-doctype": + _(u"Unexpected character in DOCTYPE."), + "eof-in-innerhtml": + _(u"XXX innerHTML EOF"), + "unexpected-doctype": + _(u"Unexpected DOCTYPE. Ignored."), + "non-html-root": + _(u"html needs to be the first start tag."), + "expected-doctype-but-got-eof": + _(u"Unexpected End of file. Expected DOCTYPE."), + "unknown-doctype": + _(u"Erroneous DOCTYPE."), + "expected-doctype-but-got-chars": + _(u"Unexpected non-space characters. Expected DOCTYPE."), + "expected-doctype-but-got-start-tag": + _(u"Unexpected start tag (%(name)s). Expected DOCTYPE."), + "expected-doctype-but-got-end-tag": + _(u"Unexpected end tag (%(name)s). Expected DOCTYPE."), + "end-tag-after-implied-root": + _(u"Unexpected end tag (%(name)s) after the (implied) root element."), + "expected-named-closing-tag-but-got-eof": + _(u"Unexpected end of file. Expected end tag (%(name)s)."), + "two-heads-are-not-better-than-one": + _(u"Unexpected start tag head in existing head. Ignored."), + "unexpected-end-tag": + _(u"Unexpected end tag (%(name)s). Ignored."), + "unexpected-start-tag-out-of-my-head": + _(u"Unexpected start tag (%(name)s) that can be in head. Moved."), + "unexpected-start-tag": + _(u"Unexpected start tag (%(name)s)."), + "missing-end-tag": + _(u"Missing end tag (%(name)s)."), + "missing-end-tags": + _(u"Missing end tags (%(name)s)."), + "unexpected-start-tag-implies-end-tag": + _(u"Unexpected start tag (%(startName)s) " + u"implies end tag (%(endName)s)."), + "unexpected-start-tag-treated-as": + _(u"Unexpected start tag (%(originalName)s). Treated as %(newName)s."), + "deprecated-tag": + _(u"Unexpected start tag %(name)s. Don't use it!"), + "unexpected-start-tag-ignored": + _(u"Unexpected start tag %(name)s. Ignored."), + "expected-one-end-tag-but-got-another": + _(u"Unexpected end tag (%(gotName)s). " + u"Missing end tag (%(expectedName)s)."), + "end-tag-too-early": + _(u"End tag (%(name)s) seen too early. Expected other end tag."), + "end-tag-too-early-named": + _(u"Unexpected end tag (%(gotName)s). Expected end tag (%(expectedName)s)."), + "end-tag-too-early-ignored": + _(u"End tag (%(name)s) seen too early. Ignored."), + "adoption-agency-1.1": + _(u"End tag (%(name)s) violates step 1, " + u"paragraph 1 of the adoption agency algorithm."), + "adoption-agency-1.2": + _(u"End tag (%(name)s) violates step 1, " + u"paragraph 2 of the adoption agency algorithm."), + "adoption-agency-1.3": + _(u"End tag (%(name)s) violates step 1, " + u"paragraph 3 of the adoption agency algorithm."), + "unexpected-end-tag-treated-as": + _(u"Unexpected end tag (%(originalName)s). Treated as %(newName)s."), + "no-end-tag": + _(u"This element (%(name)s) has no end tag."), + "unexpected-implied-end-tag-in-table": + _(u"Unexpected implied end tag (%(name)s) in the table phase."), + "unexpected-implied-end-tag-in-table-body": + _(u"Unexpected implied end tag (%(name)s) in the table body phase."), + "unexpected-char-implies-table-voodoo": + _(u"Unexpected non-space characters in " + u"table context caused voodoo mode."), + "unexpected-hidden-input-in-table": + _(u"Unexpected input with type hidden in table context."), + "unexpected-form-in-table": + _(u"Unexpected form in table context."), + "unexpected-start-tag-implies-table-voodoo": + _(u"Unexpected start tag (%(name)s) in " + u"table context caused voodoo mode."), + "unexpected-end-tag-implies-table-voodoo": + _(u"Unexpected end tag (%(name)s) in " + u"table context caused voodoo mode."), + "unexpected-cell-in-table-body": + _(u"Unexpected table cell start tag (%(name)s) " + u"in the table body phase."), + "unexpected-cell-end-tag": + _(u"Got table cell end tag (%(name)s) " + u"while required end tags are missing."), + "unexpected-end-tag-in-table-body": + _(u"Unexpected end tag (%(name)s) in the table body phase. Ignored."), + "unexpected-implied-end-tag-in-table-row": + _(u"Unexpected implied end tag (%(name)s) in the table row phase."), + "unexpected-end-tag-in-table-row": + _(u"Unexpected end tag (%(name)s) in the table row phase. Ignored."), + "unexpected-select-in-select": + _(u"Unexpected select start tag in the select phase " + u"treated as select end tag."), + "unexpected-input-in-select": + _(u"Unexpected input start tag in the select phase."), + "unexpected-start-tag-in-select": + _(u"Unexpected start tag token (%(name)s in the select phase. " + u"Ignored."), + "unexpected-end-tag-in-select": + _(u"Unexpected end tag (%(name)s) in the select phase. Ignored."), + "unexpected-table-element-start-tag-in-select-in-table": + _(u"Unexpected table element start tag (%(name)s) in the select in table phase."), + "unexpected-table-element-end-tag-in-select-in-table": + _(u"Unexpected table element end tag (%(name)s) in the select in table phase."), + "unexpected-char-after-body": + _(u"Unexpected non-space characters in the after body phase."), + "unexpected-start-tag-after-body": + _(u"Unexpected start tag token (%(name)s)" + u" in the after body phase."), + "unexpected-end-tag-after-body": + _(u"Unexpected end tag token (%(name)s)" + u" in the after body phase."), + "unexpected-char-in-frameset": + _(u"Unepxected characters in the frameset phase. Characters ignored."), + "unexpected-start-tag-in-frameset": + _(u"Unexpected start tag token (%(name)s)" + u" in the frameset phase. Ignored."), + "unexpected-frameset-in-frameset-innerhtml": + _(u"Unexpected end tag token (frameset) " + u"in the frameset phase (innerHTML)."), + "unexpected-end-tag-in-frameset": + _(u"Unexpected end tag token (%(name)s)" + u" in the frameset phase. Ignored."), + "unexpected-char-after-frameset": + _(u"Unexpected non-space characters in the " + u"after frameset phase. Ignored."), + "unexpected-start-tag-after-frameset": + _(u"Unexpected start tag (%(name)s)" + u" in the after frameset phase. Ignored."), + "unexpected-end-tag-after-frameset": + _(u"Unexpected end tag (%(name)s)" + u" in the after frameset phase. Ignored."), + "unexpected-end-tag-after-body-innerhtml": + _(u"Unexpected end tag after body(innerHtml)"), + "expected-eof-but-got-char": + _(u"Unexpected non-space characters. Expected end of file."), + "expected-eof-but-got-start-tag": + _(u"Unexpected start tag (%(name)s)" + u". Expected end of file."), + "expected-eof-but-got-end-tag": + _(u"Unexpected end tag (%(name)s)" + u". Expected end of file."), + "eof-in-table": + _(u"Unexpected end of file. Expected table content."), + "eof-in-select": + _(u"Unexpected end of file. Expected select content."), + "eof-in-frameset": + _(u"Unexpected end of file. Expected frameset content."), + "eof-in-script-in-script": + _(u"Unexpected end of file. Expected script content."), + "eof-in-foreign-lands": + _(u"Unexpected end of file. Expected foreign content"), + "non-void-element-with-trailing-solidus": + _(u"Trailing solidus not allowed on element %(name)s"), + "unexpected-html-element-in-foreign-content": + _(u"Element %(name)s not allowed in a non-html context"), + "unexpected-end-tag-before-html": + _(u"Unexpected end tag (%(name)s) before html."), + "XXX-undefined-error": + (u"Undefined error (this sucks and should be fixed)"), +} + +namespaces = { + "html":"http://www.w3.org/1999/xhtml", + "mathml":"http://www.w3.org/1998/Math/MathML", + "svg":"http://www.w3.org/2000/svg", + "xlink":"http://www.w3.org/1999/xlink", + "xml":"http://www.w3.org/XML/1998/namespace", + "xmlns":"http://www.w3.org/2000/xmlns/" +} + +scopingElements = frozenset(( + (namespaces["html"], "applet"), + (namespaces["html"], "caption"), + (namespaces["html"], "html"), + (namespaces["html"], "marquee"), + (namespaces["html"], "object"), + (namespaces["html"], "table"), + (namespaces["html"], "td"), + (namespaces["html"], "th"), + (namespaces["mathml"], "mi"), + (namespaces["mathml"], "mo"), + (namespaces["mathml"], "mn"), + (namespaces["mathml"], "ms"), + (namespaces["mathml"], "mtext"), + (namespaces["mathml"], "annotation-xml"), + (namespaces["svg"], "foreignObject"), + (namespaces["svg"], "desc"), + (namespaces["svg"], "title"), +)) + +formattingElements = frozenset(( + (namespaces["html"], "a"), + (namespaces["html"], "b"), + (namespaces["html"], "big"), + (namespaces["html"], "code"), + (namespaces["html"], "em"), + (namespaces["html"], "font"), + (namespaces["html"], "i"), + (namespaces["html"], "nobr"), + (namespaces["html"], "s"), + (namespaces["html"], "small"), + (namespaces["html"], "strike"), + (namespaces["html"], "strong"), + (namespaces["html"], "tt"), + (namespaces["html"], "u") +)) + +specialElements = frozenset(( + (namespaces["html"], "address"), + (namespaces["html"], "applet"), + (namespaces["html"], "area"), + (namespaces["html"], "article"), + (namespaces["html"], "aside"), + (namespaces["html"], "base"), + (namespaces["html"], "basefont"), + (namespaces["html"], "bgsound"), + (namespaces["html"], "blockquote"), + (namespaces["html"], "body"), + (namespaces["html"], "br"), + (namespaces["html"], "button"), + (namespaces["html"], "caption"), + (namespaces["html"], "center"), + (namespaces["html"], "col"), + (namespaces["html"], "colgroup"), + (namespaces["html"], "command"), + (namespaces["html"], "dd"), + (namespaces["html"], "details"), + (namespaces["html"], "dir"), + (namespaces["html"], "div"), + (namespaces["html"], "dl"), + (namespaces["html"], "dt"), + (namespaces["html"], "embed"), + (namespaces["html"], "fieldset"), + (namespaces["html"], "figure"), + (namespaces["html"], "footer"), + (namespaces["html"], "form"), + (namespaces["html"], "frame"), + (namespaces["html"], "frameset"), + (namespaces["html"], "h1"), + (namespaces["html"], "h2"), + (namespaces["html"], "h3"), + (namespaces["html"], "h4"), + (namespaces["html"], "h5"), + (namespaces["html"], "h6"), + (namespaces["html"], "head"), + (namespaces["html"], "header"), + (namespaces["html"], "hr"), + (namespaces["html"], "html"), + (namespaces["html"], "iframe"), + # Note that image is commented out in the spec as "this isn't an + # element that can end up on the stack, so it doesn't matter," + (namespaces["html"], "image"), + (namespaces["html"], "img"), + (namespaces["html"], "input"), + (namespaces["html"], "isindex"), + (namespaces["html"], "li"), + (namespaces["html"], "link"), + (namespaces["html"], "listing"), + (namespaces["html"], "marquee"), + (namespaces["html"], "menu"), + (namespaces["html"], "meta"), + (namespaces["html"], "nav"), + (namespaces["html"], "noembed"), + (namespaces["html"], "noframes"), + (namespaces["html"], "noscript"), + (namespaces["html"], "object"), + (namespaces["html"], "ol"), + (namespaces["html"], "p"), + (namespaces["html"], "param"), + (namespaces["html"], "plaintext"), + (namespaces["html"], "pre"), + (namespaces["html"], "script"), + (namespaces["html"], "section"), + (namespaces["html"], "select"), + (namespaces["html"], "style"), + (namespaces["html"], "table"), + (namespaces["html"], "tbody"), + (namespaces["html"], "td"), + (namespaces["html"], "textarea"), + (namespaces["html"], "tfoot"), + (namespaces["html"], "th"), + (namespaces["html"], "thead"), + (namespaces["html"], "title"), + (namespaces["html"], "tr"), + (namespaces["html"], "ul"), + (namespaces["html"], "wbr"), + (namespaces["html"], "xmp"), + (namespaces["svg"], "foreignObject") +)) + +htmlIntegrationPointElements = frozenset(( + (namespaces["mathml"], "annotaion-xml"), + (namespaces["svg"], "foreignObject"), + (namespaces["svg"], "desc"), + (namespaces["svg"], "title") +)) + +mathmlTextIntegrationPointElements = frozenset(( + (namespaces["mathml"], "mi"), + (namespaces["mathml"], "mo"), + (namespaces["mathml"], "mn"), + (namespaces["mathml"], "ms"), + (namespaces["mathml"], "mtext") +)) + +spaceCharacters = frozenset(( + u"\t", + u"\n", + u"\u000C", + u" ", + u"\r" +)) + +tableInsertModeElements = frozenset(( + "table", + "tbody", + "tfoot", + "thead", + "tr" +)) + +asciiLowercase = frozenset(string.ascii_lowercase) +asciiUppercase = frozenset(string.ascii_uppercase) +asciiLetters = frozenset(string.ascii_letters) +digits = frozenset(string.digits) +hexDigits = frozenset(string.hexdigits) + +asciiUpper2Lower = dict([(ord(c),ord(c.lower())) + for c in string.ascii_uppercase]) + +# Heading elements need to be ordered +headingElements = ( + "h1", + "h2", + "h3", + "h4", + "h5", + "h6" +) + +voidElements = frozenset(( + "base", + "command", + "event-source", + "link", + "meta", + "hr", + "br", + "img", + "embed", + "param", + "area", + "col", + "input", + "source", + "track" +)) + +cdataElements = frozenset(('title', 'textarea')) + +rcdataElements = frozenset(( + 'style', + 'script', + 'xmp', + 'iframe', + 'noembed', + 'noframes', + 'noscript' +)) + +booleanAttributes = { + "": frozenset(("irrelevant",)), + "style": frozenset(("scoped",)), + "img": frozenset(("ismap",)), + "audio": frozenset(("autoplay","controls")), + "video": frozenset(("autoplay","controls")), + "script": frozenset(("defer", "async")), + "details": frozenset(("open",)), + "datagrid": frozenset(("multiple", "disabled")), + "command": frozenset(("hidden", "disabled", "checked", "default")), + "hr": frozenset(("noshade")), + "menu": frozenset(("autosubmit",)), + "fieldset": frozenset(("disabled", "readonly")), + "option": frozenset(("disabled", "readonly", "selected")), + "optgroup": frozenset(("disabled", "readonly")), + "button": frozenset(("disabled", "autofocus")), + "input": frozenset(("disabled", "readonly", "required", "autofocus", "checked", "ismap")), + "select": frozenset(("disabled", "readonly", "autofocus", "multiple")), + "output": frozenset(("disabled", "readonly")), +} + +# entitiesWindows1252 has to be _ordered_ and needs to have an index. It +# therefore can't be a frozenset. +entitiesWindows1252 = ( + 8364, # 0x80 0x20AC EURO SIGN + 65533, # 0x81 UNDEFINED + 8218, # 0x82 0x201A SINGLE LOW-9 QUOTATION MARK + 402, # 0x83 0x0192 LATIN SMALL LETTER F WITH HOOK + 8222, # 0x84 0x201E DOUBLE LOW-9 QUOTATION MARK + 8230, # 0x85 0x2026 HORIZONTAL ELLIPSIS + 8224, # 0x86 0x2020 DAGGER + 8225, # 0x87 0x2021 DOUBLE DAGGER + 710, # 0x88 0x02C6 MODIFIER LETTER CIRCUMFLEX ACCENT + 8240, # 0x89 0x2030 PER MILLE SIGN + 352, # 0x8A 0x0160 LATIN CAPITAL LETTER S WITH CARON + 8249, # 0x8B 0x2039 SINGLE LEFT-POINTING ANGLE QUOTATION MARK + 338, # 0x8C 0x0152 LATIN CAPITAL LIGATURE OE + 65533, # 0x8D UNDEFINED + 381, # 0x8E 0x017D LATIN CAPITAL LETTER Z WITH CARON + 65533, # 0x8F UNDEFINED + 65533, # 0x90 UNDEFINED + 8216, # 0x91 0x2018 LEFT SINGLE QUOTATION MARK + 8217, # 0x92 0x2019 RIGHT SINGLE QUOTATION MARK + 8220, # 0x93 0x201C LEFT DOUBLE QUOTATION MARK + 8221, # 0x94 0x201D RIGHT DOUBLE QUOTATION MARK + 8226, # 0x95 0x2022 BULLET + 8211, # 0x96 0x2013 EN DASH + 8212, # 0x97 0x2014 EM DASH + 732, # 0x98 0x02DC SMALL TILDE + 8482, # 0x99 0x2122 TRADE MARK SIGN + 353, # 0x9A 0x0161 LATIN SMALL LETTER S WITH CARON + 8250, # 0x9B 0x203A SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + 339, # 0x9C 0x0153 LATIN SMALL LIGATURE OE + 65533, # 0x9D UNDEFINED + 382, # 0x9E 0x017E LATIN SMALL LETTER Z WITH CARON + 376 # 0x9F 0x0178 LATIN CAPITAL LETTER Y WITH DIAERESIS +) + +xmlEntities = frozenset(('lt;', 'gt;', 'amp;', 'apos;', 'quot;')) + +entities = { + "AElig": u"\xc6", + "AElig;": u"\xc6", + "AMP": u"&", + "AMP;": u"&", + "Aacute": u"\xc1", + "Aacute;": u"\xc1", + "Abreve;": u"\u0102", + "Acirc": u"\xc2", + "Acirc;": u"\xc2", + "Acy;": u"\u0410", + "Afr;": u"\U0001d504", + "Agrave": u"\xc0", + "Agrave;": u"\xc0", + "Alpha;": u"\u0391", + "Amacr;": u"\u0100", + "And;": u"\u2a53", + "Aogon;": u"\u0104", + "Aopf;": u"\U0001d538", + "ApplyFunction;": u"\u2061", + "Aring": u"\xc5", + "Aring;": u"\xc5", + "Ascr;": u"\U0001d49c", + "Assign;": u"\u2254", + "Atilde": u"\xc3", + "Atilde;": u"\xc3", + "Auml": u"\xc4", + "Auml;": u"\xc4", + "Backslash;": u"\u2216", + "Barv;": u"\u2ae7", + "Barwed;": u"\u2306", + "Bcy;": u"\u0411", + "Because;": u"\u2235", + "Bernoullis;": u"\u212c", + "Beta;": u"\u0392", + "Bfr;": u"\U0001d505", + "Bopf;": u"\U0001d539", + "Breve;": u"\u02d8", + "Bscr;": u"\u212c", + "Bumpeq;": u"\u224e", + "CHcy;": u"\u0427", + "COPY": u"\xa9", + "COPY;": u"\xa9", + "Cacute;": u"\u0106", + "Cap;": u"\u22d2", + "CapitalDifferentialD;": u"\u2145", + "Cayleys;": u"\u212d", + "Ccaron;": u"\u010c", + "Ccedil": u"\xc7", + "Ccedil;": u"\xc7", + "Ccirc;": u"\u0108", + "Cconint;": u"\u2230", + "Cdot;": u"\u010a", + "Cedilla;": u"\xb8", + "CenterDot;": u"\xb7", + "Cfr;": u"\u212d", + "Chi;": u"\u03a7", + "CircleDot;": u"\u2299", + "CircleMinus;": u"\u2296", + "CirclePlus;": u"\u2295", + "CircleTimes;": u"\u2297", + "ClockwiseContourIntegral;": u"\u2232", + "CloseCurlyDoubleQuote;": u"\u201d", + "CloseCurlyQuote;": u"\u2019", + "Colon;": u"\u2237", + "Colone;": u"\u2a74", + "Congruent;": u"\u2261", + "Conint;": u"\u222f", + "ContourIntegral;": u"\u222e", + "Copf;": u"\u2102", + "Coproduct;": u"\u2210", + "CounterClockwiseContourIntegral;": u"\u2233", + "Cross;": u"\u2a2f", + "Cscr;": u"\U0001d49e", + "Cup;": u"\u22d3", + "CupCap;": u"\u224d", + "DD;": u"\u2145", + "DDotrahd;": u"\u2911", + "DJcy;": u"\u0402", + "DScy;": u"\u0405", + "DZcy;": u"\u040f", + "Dagger;": u"\u2021", + "Darr;": u"\u21a1", + "Dashv;": u"\u2ae4", + "Dcaron;": u"\u010e", + "Dcy;": u"\u0414", + "Del;": u"\u2207", + "Delta;": u"\u0394", + "Dfr;": u"\U0001d507", + "DiacriticalAcute;": u"\xb4", + "DiacriticalDot;": u"\u02d9", + "DiacriticalDoubleAcute;": u"\u02dd", + "DiacriticalGrave;": u"`", + "DiacriticalTilde;": u"\u02dc", + "Diamond;": u"\u22c4", + "DifferentialD;": u"\u2146", + "Dopf;": u"\U0001d53b", + "Dot;": u"\xa8", + "DotDot;": u"\u20dc", + "DotEqual;": u"\u2250", + "DoubleContourIntegral;": u"\u222f", + "DoubleDot;": u"\xa8", + "DoubleDownArrow;": u"\u21d3", + "DoubleLeftArrow;": u"\u21d0", + "DoubleLeftRightArrow;": u"\u21d4", + "DoubleLeftTee;": u"\u2ae4", + "DoubleLongLeftArrow;": u"\u27f8", + "DoubleLongLeftRightArrow;": u"\u27fa", + "DoubleLongRightArrow;": u"\u27f9", + "DoubleRightArrow;": u"\u21d2", + "DoubleRightTee;": u"\u22a8", + "DoubleUpArrow;": u"\u21d1", + "DoubleUpDownArrow;": u"\u21d5", + "DoubleVerticalBar;": u"\u2225", + "DownArrow;": u"\u2193", + "DownArrowBar;": u"\u2913", + "DownArrowUpArrow;": u"\u21f5", + "DownBreve;": u"\u0311", + "DownLeftRightVector;": u"\u2950", + "DownLeftTeeVector;": u"\u295e", + "DownLeftVector;": u"\u21bd", + "DownLeftVectorBar;": u"\u2956", + "DownRightTeeVector;": u"\u295f", + "DownRightVector;": u"\u21c1", + "DownRightVectorBar;": u"\u2957", + "DownTee;": u"\u22a4", + "DownTeeArrow;": u"\u21a7", + "Downarrow;": u"\u21d3", + "Dscr;": u"\U0001d49f", + "Dstrok;": u"\u0110", + "ENG;": u"\u014a", + "ETH": u"\xd0", + "ETH;": u"\xd0", + "Eacute": u"\xc9", + "Eacute;": u"\xc9", + "Ecaron;": u"\u011a", + "Ecirc": u"\xca", + "Ecirc;": u"\xca", + "Ecy;": u"\u042d", + "Edot;": u"\u0116", + "Efr;": u"\U0001d508", + "Egrave": u"\xc8", + "Egrave;": u"\xc8", + "Element;": u"\u2208", + "Emacr;": u"\u0112", + "EmptySmallSquare;": u"\u25fb", + "EmptyVerySmallSquare;": u"\u25ab", + "Eogon;": u"\u0118", + "Eopf;": u"\U0001d53c", + "Epsilon;": u"\u0395", + "Equal;": u"\u2a75", + "EqualTilde;": u"\u2242", + "Equilibrium;": u"\u21cc", + "Escr;": u"\u2130", + "Esim;": u"\u2a73", + "Eta;": u"\u0397", + "Euml": u"\xcb", + "Euml;": u"\xcb", + "Exists;": u"\u2203", + "ExponentialE;": u"\u2147", + "Fcy;": u"\u0424", + "Ffr;": u"\U0001d509", + "FilledSmallSquare;": u"\u25fc", + "FilledVerySmallSquare;": u"\u25aa", + "Fopf;": u"\U0001d53d", + "ForAll;": u"\u2200", + "Fouriertrf;": u"\u2131", + "Fscr;": u"\u2131", + "GJcy;": u"\u0403", + "GT": u">", + "GT;": u">", + "Gamma;": u"\u0393", + "Gammad;": u"\u03dc", + "Gbreve;": u"\u011e", + "Gcedil;": u"\u0122", + "Gcirc;": u"\u011c", + "Gcy;": u"\u0413", + "Gdot;": u"\u0120", + "Gfr;": u"\U0001d50a", + "Gg;": u"\u22d9", + "Gopf;": u"\U0001d53e", + "GreaterEqual;": u"\u2265", + "GreaterEqualLess;": u"\u22db", + "GreaterFullEqual;": u"\u2267", + "GreaterGreater;": u"\u2aa2", + "GreaterLess;": u"\u2277", + "GreaterSlantEqual;": u"\u2a7e", + "GreaterTilde;": u"\u2273", + "Gscr;": u"\U0001d4a2", + "Gt;": u"\u226b", + "HARDcy;": u"\u042a", + "Hacek;": u"\u02c7", + "Hat;": u"^", + "Hcirc;": u"\u0124", + "Hfr;": u"\u210c", + "HilbertSpace;": u"\u210b", + "Hopf;": u"\u210d", + "HorizontalLine;": u"\u2500", + "Hscr;": u"\u210b", + "Hstrok;": u"\u0126", + "HumpDownHump;": u"\u224e", + "HumpEqual;": u"\u224f", + "IEcy;": u"\u0415", + "IJlig;": u"\u0132", + "IOcy;": u"\u0401", + "Iacute": u"\xcd", + "Iacute;": u"\xcd", + "Icirc": u"\xce", + "Icirc;": u"\xce", + "Icy;": u"\u0418", + "Idot;": u"\u0130", + "Ifr;": u"\u2111", + "Igrave": u"\xcc", + "Igrave;": u"\xcc", + "Im;": u"\u2111", + "Imacr;": u"\u012a", + "ImaginaryI;": u"\u2148", + "Implies;": u"\u21d2", + "Int;": u"\u222c", + "Integral;": u"\u222b", + "Intersection;": u"\u22c2", + "InvisibleComma;": u"\u2063", + "InvisibleTimes;": u"\u2062", + "Iogon;": u"\u012e", + "Iopf;": u"\U0001d540", + "Iota;": u"\u0399", + "Iscr;": u"\u2110", + "Itilde;": u"\u0128", + "Iukcy;": u"\u0406", + "Iuml": u"\xcf", + "Iuml;": u"\xcf", + "Jcirc;": u"\u0134", + "Jcy;": u"\u0419", + "Jfr;": u"\U0001d50d", + "Jopf;": u"\U0001d541", + "Jscr;": u"\U0001d4a5", + "Jsercy;": u"\u0408", + "Jukcy;": u"\u0404", + "KHcy;": u"\u0425", + "KJcy;": u"\u040c", + "Kappa;": u"\u039a", + "Kcedil;": u"\u0136", + "Kcy;": u"\u041a", + "Kfr;": u"\U0001d50e", + "Kopf;": u"\U0001d542", + "Kscr;": u"\U0001d4a6", + "LJcy;": u"\u0409", + "LT": u"<", + "LT;": u"<", + "Lacute;": u"\u0139", + "Lambda;": u"\u039b", + "Lang;": u"\u27ea", + "Laplacetrf;": u"\u2112", + "Larr;": u"\u219e", + "Lcaron;": u"\u013d", + "Lcedil;": u"\u013b", + "Lcy;": u"\u041b", + "LeftAngleBracket;": u"\u27e8", + "LeftArrow;": u"\u2190", + "LeftArrowBar;": u"\u21e4", + "LeftArrowRightArrow;": u"\u21c6", + "LeftCeiling;": u"\u2308", + "LeftDoubleBracket;": u"\u27e6", + "LeftDownTeeVector;": u"\u2961", + "LeftDownVector;": u"\u21c3", + "LeftDownVectorBar;": u"\u2959", + "LeftFloor;": u"\u230a", + "LeftRightArrow;": u"\u2194", + "LeftRightVector;": u"\u294e", + "LeftTee;": u"\u22a3", + "LeftTeeArrow;": u"\u21a4", + "LeftTeeVector;": u"\u295a", + "LeftTriangle;": u"\u22b2", + "LeftTriangleBar;": u"\u29cf", + "LeftTriangleEqual;": u"\u22b4", + "LeftUpDownVector;": u"\u2951", + "LeftUpTeeVector;": u"\u2960", + "LeftUpVector;": u"\u21bf", + "LeftUpVectorBar;": u"\u2958", + "LeftVector;": u"\u21bc", + "LeftVectorBar;": u"\u2952", + "Leftarrow;": u"\u21d0", + "Leftrightarrow;": u"\u21d4", + "LessEqualGreater;": u"\u22da", + "LessFullEqual;": u"\u2266", + "LessGreater;": u"\u2276", + "LessLess;": u"\u2aa1", + "LessSlantEqual;": u"\u2a7d", + "LessTilde;": u"\u2272", + "Lfr;": u"\U0001d50f", + "Ll;": u"\u22d8", + "Lleftarrow;": u"\u21da", + "Lmidot;": u"\u013f", + "LongLeftArrow;": u"\u27f5", + "LongLeftRightArrow;": u"\u27f7", + "LongRightArrow;": u"\u27f6", + "Longleftarrow;": u"\u27f8", + "Longleftrightarrow;": u"\u27fa", + "Longrightarrow;": u"\u27f9", + "Lopf;": u"\U0001d543", + "LowerLeftArrow;": u"\u2199", + "LowerRightArrow;": u"\u2198", + "Lscr;": u"\u2112", + "Lsh;": u"\u21b0", + "Lstrok;": u"\u0141", + "Lt;": u"\u226a", + "Map;": u"\u2905", + "Mcy;": u"\u041c", + "MediumSpace;": u"\u205f", + "Mellintrf;": u"\u2133", + "Mfr;": u"\U0001d510", + "MinusPlus;": u"\u2213", + "Mopf;": u"\U0001d544", + "Mscr;": u"\u2133", + "Mu;": u"\u039c", + "NJcy;": u"\u040a", + "Nacute;": u"\u0143", + "Ncaron;": u"\u0147", + "Ncedil;": u"\u0145", + "Ncy;": u"\u041d", + "NegativeMediumSpace;": u"\u200b", + "NegativeThickSpace;": u"\u200b", + "NegativeThinSpace;": u"\u200b", + "NegativeVeryThinSpace;": u"\u200b", + "NestedGreaterGreater;": u"\u226b", + "NestedLessLess;": u"\u226a", + "NewLine;": u"\n", + "Nfr;": u"\U0001d511", + "NoBreak;": u"\u2060", + "NonBreakingSpace;": u"\xa0", + "Nopf;": u"\u2115", + "Not;": u"\u2aec", + "NotCongruent;": u"\u2262", + "NotCupCap;": u"\u226d", + "NotDoubleVerticalBar;": u"\u2226", + "NotElement;": u"\u2209", + "NotEqual;": u"\u2260", + "NotEqualTilde;": u"\u2242\u0338", + "NotExists;": u"\u2204", + "NotGreater;": u"\u226f", + "NotGreaterEqual;": u"\u2271", + "NotGreaterFullEqual;": u"\u2267\u0338", + "NotGreaterGreater;": u"\u226b\u0338", + "NotGreaterLess;": u"\u2279", + "NotGreaterSlantEqual;": u"\u2a7e\u0338", + "NotGreaterTilde;": u"\u2275", + "NotHumpDownHump;": u"\u224e\u0338", + "NotHumpEqual;": u"\u224f\u0338", + "NotLeftTriangle;": u"\u22ea", + "NotLeftTriangleBar;": u"\u29cf\u0338", + "NotLeftTriangleEqual;": u"\u22ec", + "NotLess;": u"\u226e", + "NotLessEqual;": u"\u2270", + "NotLessGreater;": u"\u2278", + "NotLessLess;": u"\u226a\u0338", + "NotLessSlantEqual;": u"\u2a7d\u0338", + "NotLessTilde;": u"\u2274", + "NotNestedGreaterGreater;": u"\u2aa2\u0338", + "NotNestedLessLess;": u"\u2aa1\u0338", + "NotPrecedes;": u"\u2280", + "NotPrecedesEqual;": u"\u2aaf\u0338", + "NotPrecedesSlantEqual;": u"\u22e0", + "NotReverseElement;": u"\u220c", + "NotRightTriangle;": u"\u22eb", + "NotRightTriangleBar;": u"\u29d0\u0338", + "NotRightTriangleEqual;": u"\u22ed", + "NotSquareSubset;": u"\u228f\u0338", + "NotSquareSubsetEqual;": u"\u22e2", + "NotSquareSuperset;": u"\u2290\u0338", + "NotSquareSupersetEqual;": u"\u22e3", + "NotSubset;": u"\u2282\u20d2", + "NotSubsetEqual;": u"\u2288", + "NotSucceeds;": u"\u2281", + "NotSucceedsEqual;": u"\u2ab0\u0338", + "NotSucceedsSlantEqual;": u"\u22e1", + "NotSucceedsTilde;": u"\u227f\u0338", + "NotSuperset;": u"\u2283\u20d2", + "NotSupersetEqual;": u"\u2289", + "NotTilde;": u"\u2241", + "NotTildeEqual;": u"\u2244", + "NotTildeFullEqual;": u"\u2247", + "NotTildeTilde;": u"\u2249", + "NotVerticalBar;": u"\u2224", + "Nscr;": u"\U0001d4a9", + "Ntilde": u"\xd1", + "Ntilde;": u"\xd1", + "Nu;": u"\u039d", + "OElig;": u"\u0152", + "Oacute": u"\xd3", + "Oacute;": u"\xd3", + "Ocirc": u"\xd4", + "Ocirc;": u"\xd4", + "Ocy;": u"\u041e", + "Odblac;": u"\u0150", + "Ofr;": u"\U0001d512", + "Ograve": u"\xd2", + "Ograve;": u"\xd2", + "Omacr;": u"\u014c", + "Omega;": u"\u03a9", + "Omicron;": u"\u039f", + "Oopf;": u"\U0001d546", + "OpenCurlyDoubleQuote;": u"\u201c", + "OpenCurlyQuote;": u"\u2018", + "Or;": u"\u2a54", + "Oscr;": u"\U0001d4aa", + "Oslash": u"\xd8", + "Oslash;": u"\xd8", + "Otilde": u"\xd5", + "Otilde;": u"\xd5", + "Otimes;": u"\u2a37", + "Ouml": u"\xd6", + "Ouml;": u"\xd6", + "OverBar;": u"\u203e", + "OverBrace;": u"\u23de", + "OverBracket;": u"\u23b4", + "OverParenthesis;": u"\u23dc", + "PartialD;": u"\u2202", + "Pcy;": u"\u041f", + "Pfr;": u"\U0001d513", + "Phi;": u"\u03a6", + "Pi;": u"\u03a0", + "PlusMinus;": u"\xb1", + "Poincareplane;": u"\u210c", + "Popf;": u"\u2119", + "Pr;": u"\u2abb", + "Precedes;": u"\u227a", + "PrecedesEqual;": u"\u2aaf", + "PrecedesSlantEqual;": u"\u227c", + "PrecedesTilde;": u"\u227e", + "Prime;": u"\u2033", + "Product;": u"\u220f", + "Proportion;": u"\u2237", + "Proportional;": u"\u221d", + "Pscr;": u"\U0001d4ab", + "Psi;": u"\u03a8", + "QUOT": u"\"", + "QUOT;": u"\"", + "Qfr;": u"\U0001d514", + "Qopf;": u"\u211a", + "Qscr;": u"\U0001d4ac", + "RBarr;": u"\u2910", + "REG": u"\xae", + "REG;": u"\xae", + "Racute;": u"\u0154", + "Rang;": u"\u27eb", + "Rarr;": u"\u21a0", + "Rarrtl;": u"\u2916", + "Rcaron;": u"\u0158", + "Rcedil;": u"\u0156", + "Rcy;": u"\u0420", + "Re;": u"\u211c", + "ReverseElement;": u"\u220b", + "ReverseEquilibrium;": u"\u21cb", + "ReverseUpEquilibrium;": u"\u296f", + "Rfr;": u"\u211c", + "Rho;": u"\u03a1", + "RightAngleBracket;": u"\u27e9", + "RightArrow;": u"\u2192", + "RightArrowBar;": u"\u21e5", + "RightArrowLeftArrow;": u"\u21c4", + "RightCeiling;": u"\u2309", + "RightDoubleBracket;": u"\u27e7", + "RightDownTeeVector;": u"\u295d", + "RightDownVector;": u"\u21c2", + "RightDownVectorBar;": u"\u2955", + "RightFloor;": u"\u230b", + "RightTee;": u"\u22a2", + "RightTeeArrow;": u"\u21a6", + "RightTeeVector;": u"\u295b", + "RightTriangle;": u"\u22b3", + "RightTriangleBar;": u"\u29d0", + "RightTriangleEqual;": u"\u22b5", + "RightUpDownVector;": u"\u294f", + "RightUpTeeVector;": u"\u295c", + "RightUpVector;": u"\u21be", + "RightUpVectorBar;": u"\u2954", + "RightVector;": u"\u21c0", + "RightVectorBar;": u"\u2953", + "Rightarrow;": u"\u21d2", + "Ropf;": u"\u211d", + "RoundImplies;": u"\u2970", + "Rrightarrow;": u"\u21db", + "Rscr;": u"\u211b", + "Rsh;": u"\u21b1", + "RuleDelayed;": u"\u29f4", + "SHCHcy;": u"\u0429", + "SHcy;": u"\u0428", + "SOFTcy;": u"\u042c", + "Sacute;": u"\u015a", + "Sc;": u"\u2abc", + "Scaron;": u"\u0160", + "Scedil;": u"\u015e", + "Scirc;": u"\u015c", + "Scy;": u"\u0421", + "Sfr;": u"\U0001d516", + "ShortDownArrow;": u"\u2193", + "ShortLeftArrow;": u"\u2190", + "ShortRightArrow;": u"\u2192", + "ShortUpArrow;": u"\u2191", + "Sigma;": u"\u03a3", + "SmallCircle;": u"\u2218", + "Sopf;": u"\U0001d54a", + "Sqrt;": u"\u221a", + "Square;": u"\u25a1", + "SquareIntersection;": u"\u2293", + "SquareSubset;": u"\u228f", + "SquareSubsetEqual;": u"\u2291", + "SquareSuperset;": u"\u2290", + "SquareSupersetEqual;": u"\u2292", + "SquareUnion;": u"\u2294", + "Sscr;": u"\U0001d4ae", + "Star;": u"\u22c6", + "Sub;": u"\u22d0", + "Subset;": u"\u22d0", + "SubsetEqual;": u"\u2286", + "Succeeds;": u"\u227b", + "SucceedsEqual;": u"\u2ab0", + "SucceedsSlantEqual;": u"\u227d", + "SucceedsTilde;": u"\u227f", + "SuchThat;": u"\u220b", + "Sum;": u"\u2211", + "Sup;": u"\u22d1", + "Superset;": u"\u2283", + "SupersetEqual;": u"\u2287", + "Supset;": u"\u22d1", + "THORN": u"\xde", + "THORN;": u"\xde", + "TRADE;": u"\u2122", + "TSHcy;": u"\u040b", + "TScy;": u"\u0426", + "Tab;": u"\t", + "Tau;": u"\u03a4", + "Tcaron;": u"\u0164", + "Tcedil;": u"\u0162", + "Tcy;": u"\u0422", + "Tfr;": u"\U0001d517", + "Therefore;": u"\u2234", + "Theta;": u"\u0398", + "ThickSpace;": u"\u205f\u200a", + "ThinSpace;": u"\u2009", + "Tilde;": u"\u223c", + "TildeEqual;": u"\u2243", + "TildeFullEqual;": u"\u2245", + "TildeTilde;": u"\u2248", + "Topf;": u"\U0001d54b", + "TripleDot;": u"\u20db", + "Tscr;": u"\U0001d4af", + "Tstrok;": u"\u0166", + "Uacute": u"\xda", + "Uacute;": u"\xda", + "Uarr;": u"\u219f", + "Uarrocir;": u"\u2949", + "Ubrcy;": u"\u040e", + "Ubreve;": u"\u016c", + "Ucirc": u"\xdb", + "Ucirc;": u"\xdb", + "Ucy;": u"\u0423", + "Udblac;": u"\u0170", + "Ufr;": u"\U0001d518", + "Ugrave": u"\xd9", + "Ugrave;": u"\xd9", + "Umacr;": u"\u016a", + "UnderBar;": u"_", + "UnderBrace;": u"\u23df", + "UnderBracket;": u"\u23b5", + "UnderParenthesis;": u"\u23dd", + "Union;": u"\u22c3", + "UnionPlus;": u"\u228e", + "Uogon;": u"\u0172", + "Uopf;": u"\U0001d54c", + "UpArrow;": u"\u2191", + "UpArrowBar;": u"\u2912", + "UpArrowDownArrow;": u"\u21c5", + "UpDownArrow;": u"\u2195", + "UpEquilibrium;": u"\u296e", + "UpTee;": u"\u22a5", + "UpTeeArrow;": u"\u21a5", + "Uparrow;": u"\u21d1", + "Updownarrow;": u"\u21d5", + "UpperLeftArrow;": u"\u2196", + "UpperRightArrow;": u"\u2197", + "Upsi;": u"\u03d2", + "Upsilon;": u"\u03a5", + "Uring;": u"\u016e", + "Uscr;": u"\U0001d4b0", + "Utilde;": u"\u0168", + "Uuml": u"\xdc", + "Uuml;": u"\xdc", + "VDash;": u"\u22ab", + "Vbar;": u"\u2aeb", + "Vcy;": u"\u0412", + "Vdash;": u"\u22a9", + "Vdashl;": u"\u2ae6", + "Vee;": u"\u22c1", + "Verbar;": u"\u2016", + "Vert;": u"\u2016", + "VerticalBar;": u"\u2223", + "VerticalLine;": u"|", + "VerticalSeparator;": u"\u2758", + "VerticalTilde;": u"\u2240", + "VeryThinSpace;": u"\u200a", + "Vfr;": u"\U0001d519", + "Vopf;": u"\U0001d54d", + "Vscr;": u"\U0001d4b1", + "Vvdash;": u"\u22aa", + "Wcirc;": u"\u0174", + "Wedge;": u"\u22c0", + "Wfr;": u"\U0001d51a", + "Wopf;": u"\U0001d54e", + "Wscr;": u"\U0001d4b2", + "Xfr;": u"\U0001d51b", + "Xi;": u"\u039e", + "Xopf;": u"\U0001d54f", + "Xscr;": u"\U0001d4b3", + "YAcy;": u"\u042f", + "YIcy;": u"\u0407", + "YUcy;": u"\u042e", + "Yacute": u"\xdd", + "Yacute;": u"\xdd", + "Ycirc;": u"\u0176", + "Ycy;": u"\u042b", + "Yfr;": u"\U0001d51c", + "Yopf;": u"\U0001d550", + "Yscr;": u"\U0001d4b4", + "Yuml;": u"\u0178", + "ZHcy;": u"\u0416", + "Zacute;": u"\u0179", + "Zcaron;": u"\u017d", + "Zcy;": u"\u0417", + "Zdot;": u"\u017b", + "ZeroWidthSpace;": u"\u200b", + "Zeta;": u"\u0396", + "Zfr;": u"\u2128", + "Zopf;": u"\u2124", + "Zscr;": u"\U0001d4b5", + "aacute": u"\xe1", + "aacute;": u"\xe1", + "abreve;": u"\u0103", + "ac;": u"\u223e", + "acE;": u"\u223e\u0333", + "acd;": u"\u223f", + "acirc": u"\xe2", + "acirc;": u"\xe2", + "acute": u"\xb4", + "acute;": u"\xb4", + "acy;": u"\u0430", + "aelig": u"\xe6", + "aelig;": u"\xe6", + "af;": u"\u2061", + "afr;": u"\U0001d51e", + "agrave": u"\xe0", + "agrave;": u"\xe0", + "alefsym;": u"\u2135", + "aleph;": u"\u2135", + "alpha;": u"\u03b1", + "amacr;": u"\u0101", + "amalg;": u"\u2a3f", + "amp": u"&", + "amp;": u"&", + "and;": u"\u2227", + "andand;": u"\u2a55", + "andd;": u"\u2a5c", + "andslope;": u"\u2a58", + "andv;": u"\u2a5a", + "ang;": u"\u2220", + "ange;": u"\u29a4", + "angle;": u"\u2220", + "angmsd;": u"\u2221", + "angmsdaa;": u"\u29a8", + "angmsdab;": u"\u29a9", + "angmsdac;": u"\u29aa", + "angmsdad;": u"\u29ab", + "angmsdae;": u"\u29ac", + "angmsdaf;": u"\u29ad", + "angmsdag;": u"\u29ae", + "angmsdah;": u"\u29af", + "angrt;": u"\u221f", + "angrtvb;": u"\u22be", + "angrtvbd;": u"\u299d", + "angsph;": u"\u2222", + "angst;": u"\xc5", + "angzarr;": u"\u237c", + "aogon;": u"\u0105", + "aopf;": u"\U0001d552", + "ap;": u"\u2248", + "apE;": u"\u2a70", + "apacir;": u"\u2a6f", + "ape;": u"\u224a", + "apid;": u"\u224b", + "apos;": u"'", + "approx;": u"\u2248", + "approxeq;": u"\u224a", + "aring": u"\xe5", + "aring;": u"\xe5", + "ascr;": u"\U0001d4b6", + "ast;": u"*", + "asymp;": u"\u2248", + "asympeq;": u"\u224d", + "atilde": u"\xe3", + "atilde;": u"\xe3", + "auml": u"\xe4", + "auml;": u"\xe4", + "awconint;": u"\u2233", + "awint;": u"\u2a11", + "bNot;": u"\u2aed", + "backcong;": u"\u224c", + "backepsilon;": u"\u03f6", + "backprime;": u"\u2035", + "backsim;": u"\u223d", + "backsimeq;": u"\u22cd", + "barvee;": u"\u22bd", + "barwed;": u"\u2305", + "barwedge;": u"\u2305", + "bbrk;": u"\u23b5", + "bbrktbrk;": u"\u23b6", + "bcong;": u"\u224c", + "bcy;": u"\u0431", + "bdquo;": u"\u201e", + "becaus;": u"\u2235", + "because;": u"\u2235", + "bemptyv;": u"\u29b0", + "bepsi;": u"\u03f6", + "bernou;": u"\u212c", + "beta;": u"\u03b2", + "beth;": u"\u2136", + "between;": u"\u226c", + "bfr;": u"\U0001d51f", + "bigcap;": u"\u22c2", + "bigcirc;": u"\u25ef", + "bigcup;": u"\u22c3", + "bigodot;": u"\u2a00", + "bigoplus;": u"\u2a01", + "bigotimes;": u"\u2a02", + "bigsqcup;": u"\u2a06", + "bigstar;": u"\u2605", + "bigtriangledown;": u"\u25bd", + "bigtriangleup;": u"\u25b3", + "biguplus;": u"\u2a04", + "bigvee;": u"\u22c1", + "bigwedge;": u"\u22c0", + "bkarow;": u"\u290d", + "blacklozenge;": u"\u29eb", + "blacksquare;": u"\u25aa", + "blacktriangle;": u"\u25b4", + "blacktriangledown;": u"\u25be", + "blacktriangleleft;": u"\u25c2", + "blacktriangleright;": u"\u25b8", + "blank;": u"\u2423", + "blk12;": u"\u2592", + "blk14;": u"\u2591", + "blk34;": u"\u2593", + "block;": u"\u2588", + "bne;": u"=\u20e5", + "bnequiv;": u"\u2261\u20e5", + "bnot;": u"\u2310", + "bopf;": u"\U0001d553", + "bot;": u"\u22a5", + "bottom;": u"\u22a5", + "bowtie;": u"\u22c8", + "boxDL;": u"\u2557", + "boxDR;": u"\u2554", + "boxDl;": u"\u2556", + "boxDr;": u"\u2553", + "boxH;": u"\u2550", + "boxHD;": u"\u2566", + "boxHU;": u"\u2569", + "boxHd;": u"\u2564", + "boxHu;": u"\u2567", + "boxUL;": u"\u255d", + "boxUR;": u"\u255a", + "boxUl;": u"\u255c", + "boxUr;": u"\u2559", + "boxV;": u"\u2551", + "boxVH;": u"\u256c", + "boxVL;": u"\u2563", + "boxVR;": u"\u2560", + "boxVh;": u"\u256b", + "boxVl;": u"\u2562", + "boxVr;": u"\u255f", + "boxbox;": u"\u29c9", + "boxdL;": u"\u2555", + "boxdR;": u"\u2552", + "boxdl;": u"\u2510", + "boxdr;": u"\u250c", + "boxh;": u"\u2500", + "boxhD;": u"\u2565", + "boxhU;": u"\u2568", + "boxhd;": u"\u252c", + "boxhu;": u"\u2534", + "boxminus;": u"\u229f", + "boxplus;": u"\u229e", + "boxtimes;": u"\u22a0", + "boxuL;": u"\u255b", + "boxuR;": u"\u2558", + "boxul;": u"\u2518", + "boxur;": u"\u2514", + "boxv;": u"\u2502", + "boxvH;": u"\u256a", + "boxvL;": u"\u2561", + "boxvR;": u"\u255e", + "boxvh;": u"\u253c", + "boxvl;": u"\u2524", + "boxvr;": u"\u251c", + "bprime;": u"\u2035", + "breve;": u"\u02d8", + "brvbar": u"\xa6", + "brvbar;": u"\xa6", + "bscr;": u"\U0001d4b7", + "bsemi;": u"\u204f", + "bsim;": u"\u223d", + "bsime;": u"\u22cd", + "bsol;": u"\\", + "bsolb;": u"\u29c5", + "bsolhsub;": u"\u27c8", + "bull;": u"\u2022", + "bullet;": u"\u2022", + "bump;": u"\u224e", + "bumpE;": u"\u2aae", + "bumpe;": u"\u224f", + "bumpeq;": u"\u224f", + "cacute;": u"\u0107", + "cap;": u"\u2229", + "capand;": u"\u2a44", + "capbrcup;": u"\u2a49", + "capcap;": u"\u2a4b", + "capcup;": u"\u2a47", + "capdot;": u"\u2a40", + "caps;": u"\u2229\ufe00", + "caret;": u"\u2041", + "caron;": u"\u02c7", + "ccaps;": u"\u2a4d", + "ccaron;": u"\u010d", + "ccedil": u"\xe7", + "ccedil;": u"\xe7", + "ccirc;": u"\u0109", + "ccups;": u"\u2a4c", + "ccupssm;": u"\u2a50", + "cdot;": u"\u010b", + "cedil": u"\xb8", + "cedil;": u"\xb8", + "cemptyv;": u"\u29b2", + "cent": u"\xa2", + "cent;": u"\xa2", + "centerdot;": u"\xb7", + "cfr;": u"\U0001d520", + "chcy;": u"\u0447", + "check;": u"\u2713", + "checkmark;": u"\u2713", + "chi;": u"\u03c7", + "cir;": u"\u25cb", + "cirE;": u"\u29c3", + "circ;": u"\u02c6", + "circeq;": u"\u2257", + "circlearrowleft;": u"\u21ba", + "circlearrowright;": u"\u21bb", + "circledR;": u"\xae", + "circledS;": u"\u24c8", + "circledast;": u"\u229b", + "circledcirc;": u"\u229a", + "circleddash;": u"\u229d", + "cire;": u"\u2257", + "cirfnint;": u"\u2a10", + "cirmid;": u"\u2aef", + "cirscir;": u"\u29c2", + "clubs;": u"\u2663", + "clubsuit;": u"\u2663", + "colon;": u":", + "colone;": u"\u2254", + "coloneq;": u"\u2254", + "comma;": u",", + "commat;": u"@", + "comp;": u"\u2201", + "compfn;": u"\u2218", + "complement;": u"\u2201", + "complexes;": u"\u2102", + "cong;": u"\u2245", + "congdot;": u"\u2a6d", + "conint;": u"\u222e", + "copf;": u"\U0001d554", + "coprod;": u"\u2210", + "copy": u"\xa9", + "copy;": u"\xa9", + "copysr;": u"\u2117", + "crarr;": u"\u21b5", + "cross;": u"\u2717", + "cscr;": u"\U0001d4b8", + "csub;": u"\u2acf", + "csube;": u"\u2ad1", + "csup;": u"\u2ad0", + "csupe;": u"\u2ad2", + "ctdot;": u"\u22ef", + "cudarrl;": u"\u2938", + "cudarrr;": u"\u2935", + "cuepr;": u"\u22de", + "cuesc;": u"\u22df", + "cularr;": u"\u21b6", + "cularrp;": u"\u293d", + "cup;": u"\u222a", + "cupbrcap;": u"\u2a48", + "cupcap;": u"\u2a46", + "cupcup;": u"\u2a4a", + "cupdot;": u"\u228d", + "cupor;": u"\u2a45", + "cups;": u"\u222a\ufe00", + "curarr;": u"\u21b7", + "curarrm;": u"\u293c", + "curlyeqprec;": u"\u22de", + "curlyeqsucc;": u"\u22df", + "curlyvee;": u"\u22ce", + "curlywedge;": u"\u22cf", + "curren": u"\xa4", + "curren;": u"\xa4", + "curvearrowleft;": u"\u21b6", + "curvearrowright;": u"\u21b7", + "cuvee;": u"\u22ce", + "cuwed;": u"\u22cf", + "cwconint;": u"\u2232", + "cwint;": u"\u2231", + "cylcty;": u"\u232d", + "dArr;": u"\u21d3", + "dHar;": u"\u2965", + "dagger;": u"\u2020", + "daleth;": u"\u2138", + "darr;": u"\u2193", + "dash;": u"\u2010", + "dashv;": u"\u22a3", + "dbkarow;": u"\u290f", + "dblac;": u"\u02dd", + "dcaron;": u"\u010f", + "dcy;": u"\u0434", + "dd;": u"\u2146", + "ddagger;": u"\u2021", + "ddarr;": u"\u21ca", + "ddotseq;": u"\u2a77", + "deg": u"\xb0", + "deg;": u"\xb0", + "delta;": u"\u03b4", + "demptyv;": u"\u29b1", + "dfisht;": u"\u297f", + "dfr;": u"\U0001d521", + "dharl;": u"\u21c3", + "dharr;": u"\u21c2", + "diam;": u"\u22c4", + "diamond;": u"\u22c4", + "diamondsuit;": u"\u2666", + "diams;": u"\u2666", + "die;": u"\xa8", + "digamma;": u"\u03dd", + "disin;": u"\u22f2", + "div;": u"\xf7", + "divide": u"\xf7", + "divide;": u"\xf7", + "divideontimes;": u"\u22c7", + "divonx;": u"\u22c7", + "djcy;": u"\u0452", + "dlcorn;": u"\u231e", + "dlcrop;": u"\u230d", + "dollar;": u"$", + "dopf;": u"\U0001d555", + "dot;": u"\u02d9", + "doteq;": u"\u2250", + "doteqdot;": u"\u2251", + "dotminus;": u"\u2238", + "dotplus;": u"\u2214", + "dotsquare;": u"\u22a1", + "doublebarwedge;": u"\u2306", + "downarrow;": u"\u2193", + "downdownarrows;": u"\u21ca", + "downharpoonleft;": u"\u21c3", + "downharpoonright;": u"\u21c2", + "drbkarow;": u"\u2910", + "drcorn;": u"\u231f", + "drcrop;": u"\u230c", + "dscr;": u"\U0001d4b9", + "dscy;": u"\u0455", + "dsol;": u"\u29f6", + "dstrok;": u"\u0111", + "dtdot;": u"\u22f1", + "dtri;": u"\u25bf", + "dtrif;": u"\u25be", + "duarr;": u"\u21f5", + "duhar;": u"\u296f", + "dwangle;": u"\u29a6", + "dzcy;": u"\u045f", + "dzigrarr;": u"\u27ff", + "eDDot;": u"\u2a77", + "eDot;": u"\u2251", + "eacute": u"\xe9", + "eacute;": u"\xe9", + "easter;": u"\u2a6e", + "ecaron;": u"\u011b", + "ecir;": u"\u2256", + "ecirc": u"\xea", + "ecirc;": u"\xea", + "ecolon;": u"\u2255", + "ecy;": u"\u044d", + "edot;": u"\u0117", + "ee;": u"\u2147", + "efDot;": u"\u2252", + "efr;": u"\U0001d522", + "eg;": u"\u2a9a", + "egrave": u"\xe8", + "egrave;": u"\xe8", + "egs;": u"\u2a96", + "egsdot;": u"\u2a98", + "el;": u"\u2a99", + "elinters;": u"\u23e7", + "ell;": u"\u2113", + "els;": u"\u2a95", + "elsdot;": u"\u2a97", + "emacr;": u"\u0113", + "empty;": u"\u2205", + "emptyset;": u"\u2205", + "emptyv;": u"\u2205", + "emsp13;": u"\u2004", + "emsp14;": u"\u2005", + "emsp;": u"\u2003", + "eng;": u"\u014b", + "ensp;": u"\u2002", + "eogon;": u"\u0119", + "eopf;": u"\U0001d556", + "epar;": u"\u22d5", + "eparsl;": u"\u29e3", + "eplus;": u"\u2a71", + "epsi;": u"\u03b5", + "epsilon;": u"\u03b5", + "epsiv;": u"\u03f5", + "eqcirc;": u"\u2256", + "eqcolon;": u"\u2255", + "eqsim;": u"\u2242", + "eqslantgtr;": u"\u2a96", + "eqslantless;": u"\u2a95", + "equals;": u"=", + "equest;": u"\u225f", + "equiv;": u"\u2261", + "equivDD;": u"\u2a78", + "eqvparsl;": u"\u29e5", + "erDot;": u"\u2253", + "erarr;": u"\u2971", + "escr;": u"\u212f", + "esdot;": u"\u2250", + "esim;": u"\u2242", + "eta;": u"\u03b7", + "eth": u"\xf0", + "eth;": u"\xf0", + "euml": u"\xeb", + "euml;": u"\xeb", + "euro;": u"\u20ac", + "excl;": u"!", + "exist;": u"\u2203", + "expectation;": u"\u2130", + "exponentiale;": u"\u2147", + "fallingdotseq;": u"\u2252", + "fcy;": u"\u0444", + "female;": u"\u2640", + "ffilig;": u"\ufb03", + "fflig;": u"\ufb00", + "ffllig;": u"\ufb04", + "ffr;": u"\U0001d523", + "filig;": u"\ufb01", + "fjlig;": u"fj", + "flat;": u"\u266d", + "fllig;": u"\ufb02", + "fltns;": u"\u25b1", + "fnof;": u"\u0192", + "fopf;": u"\U0001d557", + "forall;": u"\u2200", + "fork;": u"\u22d4", + "forkv;": u"\u2ad9", + "fpartint;": u"\u2a0d", + "frac12": u"\xbd", + "frac12;": u"\xbd", + "frac13;": u"\u2153", + "frac14": u"\xbc", + "frac14;": u"\xbc", + "frac15;": u"\u2155", + "frac16;": u"\u2159", + "frac18;": u"\u215b", + "frac23;": u"\u2154", + "frac25;": u"\u2156", + "frac34": u"\xbe", + "frac34;": u"\xbe", + "frac35;": u"\u2157", + "frac38;": u"\u215c", + "frac45;": u"\u2158", + "frac56;": u"\u215a", + "frac58;": u"\u215d", + "frac78;": u"\u215e", + "frasl;": u"\u2044", + "frown;": u"\u2322", + "fscr;": u"\U0001d4bb", + "gE;": u"\u2267", + "gEl;": u"\u2a8c", + "gacute;": u"\u01f5", + "gamma;": u"\u03b3", + "gammad;": u"\u03dd", + "gap;": u"\u2a86", + "gbreve;": u"\u011f", + "gcirc;": u"\u011d", + "gcy;": u"\u0433", + "gdot;": u"\u0121", + "ge;": u"\u2265", + "gel;": u"\u22db", + "geq;": u"\u2265", + "geqq;": u"\u2267", + "geqslant;": u"\u2a7e", + "ges;": u"\u2a7e", + "gescc;": u"\u2aa9", + "gesdot;": u"\u2a80", + "gesdoto;": u"\u2a82", + "gesdotol;": u"\u2a84", + "gesl;": u"\u22db\ufe00", + "gesles;": u"\u2a94", + "gfr;": u"\U0001d524", + "gg;": u"\u226b", + "ggg;": u"\u22d9", + "gimel;": u"\u2137", + "gjcy;": u"\u0453", + "gl;": u"\u2277", + "glE;": u"\u2a92", + "gla;": u"\u2aa5", + "glj;": u"\u2aa4", + "gnE;": u"\u2269", + "gnap;": u"\u2a8a", + "gnapprox;": u"\u2a8a", + "gne;": u"\u2a88", + "gneq;": u"\u2a88", + "gneqq;": u"\u2269", + "gnsim;": u"\u22e7", + "gopf;": u"\U0001d558", + "grave;": u"`", + "gscr;": u"\u210a", + "gsim;": u"\u2273", + "gsime;": u"\u2a8e", + "gsiml;": u"\u2a90", + "gt": u">", + "gt;": u">", + "gtcc;": u"\u2aa7", + "gtcir;": u"\u2a7a", + "gtdot;": u"\u22d7", + "gtlPar;": u"\u2995", + "gtquest;": u"\u2a7c", + "gtrapprox;": u"\u2a86", + "gtrarr;": u"\u2978", + "gtrdot;": u"\u22d7", + "gtreqless;": u"\u22db", + "gtreqqless;": u"\u2a8c", + "gtrless;": u"\u2277", + "gtrsim;": u"\u2273", + "gvertneqq;": u"\u2269\ufe00", + "gvnE;": u"\u2269\ufe00", + "hArr;": u"\u21d4", + "hairsp;": u"\u200a", + "half;": u"\xbd", + "hamilt;": u"\u210b", + "hardcy;": u"\u044a", + "harr;": u"\u2194", + "harrcir;": u"\u2948", + "harrw;": u"\u21ad", + "hbar;": u"\u210f", + "hcirc;": u"\u0125", + "hearts;": u"\u2665", + "heartsuit;": u"\u2665", + "hellip;": u"\u2026", + "hercon;": u"\u22b9", + "hfr;": u"\U0001d525", + "hksearow;": u"\u2925", + "hkswarow;": u"\u2926", + "hoarr;": u"\u21ff", + "homtht;": u"\u223b", + "hookleftarrow;": u"\u21a9", + "hookrightarrow;": u"\u21aa", + "hopf;": u"\U0001d559", + "horbar;": u"\u2015", + "hscr;": u"\U0001d4bd", + "hslash;": u"\u210f", + "hstrok;": u"\u0127", + "hybull;": u"\u2043", + "hyphen;": u"\u2010", + "iacute": u"\xed", + "iacute;": u"\xed", + "ic;": u"\u2063", + "icirc": u"\xee", + "icirc;": u"\xee", + "icy;": u"\u0438", + "iecy;": u"\u0435", + "iexcl": u"\xa1", + "iexcl;": u"\xa1", + "iff;": u"\u21d4", + "ifr;": u"\U0001d526", + "igrave": u"\xec", + "igrave;": u"\xec", + "ii;": u"\u2148", + "iiiint;": u"\u2a0c", + "iiint;": u"\u222d", + "iinfin;": u"\u29dc", + "iiota;": u"\u2129", + "ijlig;": u"\u0133", + "imacr;": u"\u012b", + "image;": u"\u2111", + "imagline;": u"\u2110", + "imagpart;": u"\u2111", + "imath;": u"\u0131", + "imof;": u"\u22b7", + "imped;": u"\u01b5", + "in;": u"\u2208", + "incare;": u"\u2105", + "infin;": u"\u221e", + "infintie;": u"\u29dd", + "inodot;": u"\u0131", + "int;": u"\u222b", + "intcal;": u"\u22ba", + "integers;": u"\u2124", + "intercal;": u"\u22ba", + "intlarhk;": u"\u2a17", + "intprod;": u"\u2a3c", + "iocy;": u"\u0451", + "iogon;": u"\u012f", + "iopf;": u"\U0001d55a", + "iota;": u"\u03b9", + "iprod;": u"\u2a3c", + "iquest": u"\xbf", + "iquest;": u"\xbf", + "iscr;": u"\U0001d4be", + "isin;": u"\u2208", + "isinE;": u"\u22f9", + "isindot;": u"\u22f5", + "isins;": u"\u22f4", + "isinsv;": u"\u22f3", + "isinv;": u"\u2208", + "it;": u"\u2062", + "itilde;": u"\u0129", + "iukcy;": u"\u0456", + "iuml": u"\xef", + "iuml;": u"\xef", + "jcirc;": u"\u0135", + "jcy;": u"\u0439", + "jfr;": u"\U0001d527", + "jmath;": u"\u0237", + "jopf;": u"\U0001d55b", + "jscr;": u"\U0001d4bf", + "jsercy;": u"\u0458", + "jukcy;": u"\u0454", + "kappa;": u"\u03ba", + "kappav;": u"\u03f0", + "kcedil;": u"\u0137", + "kcy;": u"\u043a", + "kfr;": u"\U0001d528", + "kgreen;": u"\u0138", + "khcy;": u"\u0445", + "kjcy;": u"\u045c", + "kopf;": u"\U0001d55c", + "kscr;": u"\U0001d4c0", + "lAarr;": u"\u21da", + "lArr;": u"\u21d0", + "lAtail;": u"\u291b", + "lBarr;": u"\u290e", + "lE;": u"\u2266", + "lEg;": u"\u2a8b", + "lHar;": u"\u2962", + "lacute;": u"\u013a", + "laemptyv;": u"\u29b4", + "lagran;": u"\u2112", + "lambda;": u"\u03bb", + "lang;": u"\u27e8", + "langd;": u"\u2991", + "langle;": u"\u27e8", + "lap;": u"\u2a85", + "laquo": u"\xab", + "laquo;": u"\xab", + "larr;": u"\u2190", + "larrb;": u"\u21e4", + "larrbfs;": u"\u291f", + "larrfs;": u"\u291d", + "larrhk;": u"\u21a9", + "larrlp;": u"\u21ab", + "larrpl;": u"\u2939", + "larrsim;": u"\u2973", + "larrtl;": u"\u21a2", + "lat;": u"\u2aab", + "latail;": u"\u2919", + "late;": u"\u2aad", + "lates;": u"\u2aad\ufe00", + "lbarr;": u"\u290c", + "lbbrk;": u"\u2772", + "lbrace;": u"{", + "lbrack;": u"[", + "lbrke;": u"\u298b", + "lbrksld;": u"\u298f", + "lbrkslu;": u"\u298d", + "lcaron;": u"\u013e", + "lcedil;": u"\u013c", + "lceil;": u"\u2308", + "lcub;": u"{", + "lcy;": u"\u043b", + "ldca;": u"\u2936", + "ldquo;": u"\u201c", + "ldquor;": u"\u201e", + "ldrdhar;": u"\u2967", + "ldrushar;": u"\u294b", + "ldsh;": u"\u21b2", + "le;": u"\u2264", + "leftarrow;": u"\u2190", + "leftarrowtail;": u"\u21a2", + "leftharpoondown;": u"\u21bd", + "leftharpoonup;": u"\u21bc", + "leftleftarrows;": u"\u21c7", + "leftrightarrow;": u"\u2194", + "leftrightarrows;": u"\u21c6", + "leftrightharpoons;": u"\u21cb", + "leftrightsquigarrow;": u"\u21ad", + "leftthreetimes;": u"\u22cb", + "leg;": u"\u22da", + "leq;": u"\u2264", + "leqq;": u"\u2266", + "leqslant;": u"\u2a7d", + "les;": u"\u2a7d", + "lescc;": u"\u2aa8", + "lesdot;": u"\u2a7f", + "lesdoto;": u"\u2a81", + "lesdotor;": u"\u2a83", + "lesg;": u"\u22da\ufe00", + "lesges;": u"\u2a93", + "lessapprox;": u"\u2a85", + "lessdot;": u"\u22d6", + "lesseqgtr;": u"\u22da", + "lesseqqgtr;": u"\u2a8b", + "lessgtr;": u"\u2276", + "lesssim;": u"\u2272", + "lfisht;": u"\u297c", + "lfloor;": u"\u230a", + "lfr;": u"\U0001d529", + "lg;": u"\u2276", + "lgE;": u"\u2a91", + "lhard;": u"\u21bd", + "lharu;": u"\u21bc", + "lharul;": u"\u296a", + "lhblk;": u"\u2584", + "ljcy;": u"\u0459", + "ll;": u"\u226a", + "llarr;": u"\u21c7", + "llcorner;": u"\u231e", + "llhard;": u"\u296b", + "lltri;": u"\u25fa", + "lmidot;": u"\u0140", + "lmoust;": u"\u23b0", + "lmoustache;": u"\u23b0", + "lnE;": u"\u2268", + "lnap;": u"\u2a89", + "lnapprox;": u"\u2a89", + "lne;": u"\u2a87", + "lneq;": u"\u2a87", + "lneqq;": u"\u2268", + "lnsim;": u"\u22e6", + "loang;": u"\u27ec", + "loarr;": u"\u21fd", + "lobrk;": u"\u27e6", + "longleftarrow;": u"\u27f5", + "longleftrightarrow;": u"\u27f7", + "longmapsto;": u"\u27fc", + "longrightarrow;": u"\u27f6", + "looparrowleft;": u"\u21ab", + "looparrowright;": u"\u21ac", + "lopar;": u"\u2985", + "lopf;": u"\U0001d55d", + "loplus;": u"\u2a2d", + "lotimes;": u"\u2a34", + "lowast;": u"\u2217", + "lowbar;": u"_", + "loz;": u"\u25ca", + "lozenge;": u"\u25ca", + "lozf;": u"\u29eb", + "lpar;": u"(", + "lparlt;": u"\u2993", + "lrarr;": u"\u21c6", + "lrcorner;": u"\u231f", + "lrhar;": u"\u21cb", + "lrhard;": u"\u296d", + "lrm;": u"\u200e", + "lrtri;": u"\u22bf", + "lsaquo;": u"\u2039", + "lscr;": u"\U0001d4c1", + "lsh;": u"\u21b0", + "lsim;": u"\u2272", + "lsime;": u"\u2a8d", + "lsimg;": u"\u2a8f", + "lsqb;": u"[", + "lsquo;": u"\u2018", + "lsquor;": u"\u201a", + "lstrok;": u"\u0142", + "lt": u"<", + "lt;": u"<", + "ltcc;": u"\u2aa6", + "ltcir;": u"\u2a79", + "ltdot;": u"\u22d6", + "lthree;": u"\u22cb", + "ltimes;": u"\u22c9", + "ltlarr;": u"\u2976", + "ltquest;": u"\u2a7b", + "ltrPar;": u"\u2996", + "ltri;": u"\u25c3", + "ltrie;": u"\u22b4", + "ltrif;": u"\u25c2", + "lurdshar;": u"\u294a", + "luruhar;": u"\u2966", + "lvertneqq;": u"\u2268\ufe00", + "lvnE;": u"\u2268\ufe00", + "mDDot;": u"\u223a", + "macr": u"\xaf", + "macr;": u"\xaf", + "male;": u"\u2642", + "malt;": u"\u2720", + "maltese;": u"\u2720", + "map;": u"\u21a6", + "mapsto;": u"\u21a6", + "mapstodown;": u"\u21a7", + "mapstoleft;": u"\u21a4", + "mapstoup;": u"\u21a5", + "marker;": u"\u25ae", + "mcomma;": u"\u2a29", + "mcy;": u"\u043c", + "mdash;": u"\u2014", + "measuredangle;": u"\u2221", + "mfr;": u"\U0001d52a", + "mho;": u"\u2127", + "micro": u"\xb5", + "micro;": u"\xb5", + "mid;": u"\u2223", + "midast;": u"*", + "midcir;": u"\u2af0", + "middot": u"\xb7", + "middot;": u"\xb7", + "minus;": u"\u2212", + "minusb;": u"\u229f", + "minusd;": u"\u2238", + "minusdu;": u"\u2a2a", + "mlcp;": u"\u2adb", + "mldr;": u"\u2026", + "mnplus;": u"\u2213", + "models;": u"\u22a7", + "mopf;": u"\U0001d55e", + "mp;": u"\u2213", + "mscr;": u"\U0001d4c2", + "mstpos;": u"\u223e", + "mu;": u"\u03bc", + "multimap;": u"\u22b8", + "mumap;": u"\u22b8", + "nGg;": u"\u22d9\u0338", + "nGt;": u"\u226b\u20d2", + "nGtv;": u"\u226b\u0338", + "nLeftarrow;": u"\u21cd", + "nLeftrightarrow;": u"\u21ce", + "nLl;": u"\u22d8\u0338", + "nLt;": u"\u226a\u20d2", + "nLtv;": u"\u226a\u0338", + "nRightarrow;": u"\u21cf", + "nVDash;": u"\u22af", + "nVdash;": u"\u22ae", + "nabla;": u"\u2207", + "nacute;": u"\u0144", + "nang;": u"\u2220\u20d2", + "nap;": u"\u2249", + "napE;": u"\u2a70\u0338", + "napid;": u"\u224b\u0338", + "napos;": u"\u0149", + "napprox;": u"\u2249", + "natur;": u"\u266e", + "natural;": u"\u266e", + "naturals;": u"\u2115", + "nbsp": u"\xa0", + "nbsp;": u"\xa0", + "nbump;": u"\u224e\u0338", + "nbumpe;": u"\u224f\u0338", + "ncap;": u"\u2a43", + "ncaron;": u"\u0148", + "ncedil;": u"\u0146", + "ncong;": u"\u2247", + "ncongdot;": u"\u2a6d\u0338", + "ncup;": u"\u2a42", + "ncy;": u"\u043d", + "ndash;": u"\u2013", + "ne;": u"\u2260", + "neArr;": u"\u21d7", + "nearhk;": u"\u2924", + "nearr;": u"\u2197", + "nearrow;": u"\u2197", + "nedot;": u"\u2250\u0338", + "nequiv;": u"\u2262", + "nesear;": u"\u2928", + "nesim;": u"\u2242\u0338", + "nexist;": u"\u2204", + "nexists;": u"\u2204", + "nfr;": u"\U0001d52b", + "ngE;": u"\u2267\u0338", + "nge;": u"\u2271", + "ngeq;": u"\u2271", + "ngeqq;": u"\u2267\u0338", + "ngeqslant;": u"\u2a7e\u0338", + "nges;": u"\u2a7e\u0338", + "ngsim;": u"\u2275", + "ngt;": u"\u226f", + "ngtr;": u"\u226f", + "nhArr;": u"\u21ce", + "nharr;": u"\u21ae", + "nhpar;": u"\u2af2", + "ni;": u"\u220b", + "nis;": u"\u22fc", + "nisd;": u"\u22fa", + "niv;": u"\u220b", + "njcy;": u"\u045a", + "nlArr;": u"\u21cd", + "nlE;": u"\u2266\u0338", + "nlarr;": u"\u219a", + "nldr;": u"\u2025", + "nle;": u"\u2270", + "nleftarrow;": u"\u219a", + "nleftrightarrow;": u"\u21ae", + "nleq;": u"\u2270", + "nleqq;": u"\u2266\u0338", + "nleqslant;": u"\u2a7d\u0338", + "nles;": u"\u2a7d\u0338", + "nless;": u"\u226e", + "nlsim;": u"\u2274", + "nlt;": u"\u226e", + "nltri;": u"\u22ea", + "nltrie;": u"\u22ec", + "nmid;": u"\u2224", + "nopf;": u"\U0001d55f", + "not": u"\xac", + "not;": u"\xac", + "notin;": u"\u2209", + "notinE;": u"\u22f9\u0338", + "notindot;": u"\u22f5\u0338", + "notinva;": u"\u2209", + "notinvb;": u"\u22f7", + "notinvc;": u"\u22f6", + "notni;": u"\u220c", + "notniva;": u"\u220c", + "notnivb;": u"\u22fe", + "notnivc;": u"\u22fd", + "npar;": u"\u2226", + "nparallel;": u"\u2226", + "nparsl;": u"\u2afd\u20e5", + "npart;": u"\u2202\u0338", + "npolint;": u"\u2a14", + "npr;": u"\u2280", + "nprcue;": u"\u22e0", + "npre;": u"\u2aaf\u0338", + "nprec;": u"\u2280", + "npreceq;": u"\u2aaf\u0338", + "nrArr;": u"\u21cf", + "nrarr;": u"\u219b", + "nrarrc;": u"\u2933\u0338", + "nrarrw;": u"\u219d\u0338", + "nrightarrow;": u"\u219b", + "nrtri;": u"\u22eb", + "nrtrie;": u"\u22ed", + "nsc;": u"\u2281", + "nsccue;": u"\u22e1", + "nsce;": u"\u2ab0\u0338", + "nscr;": u"\U0001d4c3", + "nshortmid;": u"\u2224", + "nshortparallel;": u"\u2226", + "nsim;": u"\u2241", + "nsime;": u"\u2244", + "nsimeq;": u"\u2244", + "nsmid;": u"\u2224", + "nspar;": u"\u2226", + "nsqsube;": u"\u22e2", + "nsqsupe;": u"\u22e3", + "nsub;": u"\u2284", + "nsubE;": u"\u2ac5\u0338", + "nsube;": u"\u2288", + "nsubset;": u"\u2282\u20d2", + "nsubseteq;": u"\u2288", + "nsubseteqq;": u"\u2ac5\u0338", + "nsucc;": u"\u2281", + "nsucceq;": u"\u2ab0\u0338", + "nsup;": u"\u2285", + "nsupE;": u"\u2ac6\u0338", + "nsupe;": u"\u2289", + "nsupset;": u"\u2283\u20d2", + "nsupseteq;": u"\u2289", + "nsupseteqq;": u"\u2ac6\u0338", + "ntgl;": u"\u2279", + "ntilde": u"\xf1", + "ntilde;": u"\xf1", + "ntlg;": u"\u2278", + "ntriangleleft;": u"\u22ea", + "ntrianglelefteq;": u"\u22ec", + "ntriangleright;": u"\u22eb", + "ntrianglerighteq;": u"\u22ed", + "nu;": u"\u03bd", + "num;": u"#", + "numero;": u"\u2116", + "numsp;": u"\u2007", + "nvDash;": u"\u22ad", + "nvHarr;": u"\u2904", + "nvap;": u"\u224d\u20d2", + "nvdash;": u"\u22ac", + "nvge;": u"\u2265\u20d2", + "nvgt;": u">\u20d2", + "nvinfin;": u"\u29de", + "nvlArr;": u"\u2902", + "nvle;": u"\u2264\u20d2", + "nvlt;": u"<\u20d2", + "nvltrie;": u"\u22b4\u20d2", + "nvrArr;": u"\u2903", + "nvrtrie;": u"\u22b5\u20d2", + "nvsim;": u"\u223c\u20d2", + "nwArr;": u"\u21d6", + "nwarhk;": u"\u2923", + "nwarr;": u"\u2196", + "nwarrow;": u"\u2196", + "nwnear;": u"\u2927", + "oS;": u"\u24c8", + "oacute": u"\xf3", + "oacute;": u"\xf3", + "oast;": u"\u229b", + "ocir;": u"\u229a", + "ocirc": u"\xf4", + "ocirc;": u"\xf4", + "ocy;": u"\u043e", + "odash;": u"\u229d", + "odblac;": u"\u0151", + "odiv;": u"\u2a38", + "odot;": u"\u2299", + "odsold;": u"\u29bc", + "oelig;": u"\u0153", + "ofcir;": u"\u29bf", + "ofr;": u"\U0001d52c", + "ogon;": u"\u02db", + "ograve": u"\xf2", + "ograve;": u"\xf2", + "ogt;": u"\u29c1", + "ohbar;": u"\u29b5", + "ohm;": u"\u03a9", + "oint;": u"\u222e", + "olarr;": u"\u21ba", + "olcir;": u"\u29be", + "olcross;": u"\u29bb", + "oline;": u"\u203e", + "olt;": u"\u29c0", + "omacr;": u"\u014d", + "omega;": u"\u03c9", + "omicron;": u"\u03bf", + "omid;": u"\u29b6", + "ominus;": u"\u2296", + "oopf;": u"\U0001d560", + "opar;": u"\u29b7", + "operp;": u"\u29b9", + "oplus;": u"\u2295", + "or;": u"\u2228", + "orarr;": u"\u21bb", + "ord;": u"\u2a5d", + "order;": u"\u2134", + "orderof;": u"\u2134", + "ordf": u"\xaa", + "ordf;": u"\xaa", + "ordm": u"\xba", + "ordm;": u"\xba", + "origof;": u"\u22b6", + "oror;": u"\u2a56", + "orslope;": u"\u2a57", + "orv;": u"\u2a5b", + "oscr;": u"\u2134", + "oslash": u"\xf8", + "oslash;": u"\xf8", + "osol;": u"\u2298", + "otilde": u"\xf5", + "otilde;": u"\xf5", + "otimes;": u"\u2297", + "otimesas;": u"\u2a36", + "ouml": u"\xf6", + "ouml;": u"\xf6", + "ovbar;": u"\u233d", + "par;": u"\u2225", + "para": u"\xb6", + "para;": u"\xb6", + "parallel;": u"\u2225", + "parsim;": u"\u2af3", + "parsl;": u"\u2afd", + "part;": u"\u2202", + "pcy;": u"\u043f", + "percnt;": u"%", + "period;": u".", + "permil;": u"\u2030", + "perp;": u"\u22a5", + "pertenk;": u"\u2031", + "pfr;": u"\U0001d52d", + "phi;": u"\u03c6", + "phiv;": u"\u03d5", + "phmmat;": u"\u2133", + "phone;": u"\u260e", + "pi;": u"\u03c0", + "pitchfork;": u"\u22d4", + "piv;": u"\u03d6", + "planck;": u"\u210f", + "planckh;": u"\u210e", + "plankv;": u"\u210f", + "plus;": u"+", + "plusacir;": u"\u2a23", + "plusb;": u"\u229e", + "pluscir;": u"\u2a22", + "plusdo;": u"\u2214", + "plusdu;": u"\u2a25", + "pluse;": u"\u2a72", + "plusmn": u"\xb1", + "plusmn;": u"\xb1", + "plussim;": u"\u2a26", + "plustwo;": u"\u2a27", + "pm;": u"\xb1", + "pointint;": u"\u2a15", + "popf;": u"\U0001d561", + "pound": u"\xa3", + "pound;": u"\xa3", + "pr;": u"\u227a", + "prE;": u"\u2ab3", + "prap;": u"\u2ab7", + "prcue;": u"\u227c", + "pre;": u"\u2aaf", + "prec;": u"\u227a", + "precapprox;": u"\u2ab7", + "preccurlyeq;": u"\u227c", + "preceq;": u"\u2aaf", + "precnapprox;": u"\u2ab9", + "precneqq;": u"\u2ab5", + "precnsim;": u"\u22e8", + "precsim;": u"\u227e", + "prime;": u"\u2032", + "primes;": u"\u2119", + "prnE;": u"\u2ab5", + "prnap;": u"\u2ab9", + "prnsim;": u"\u22e8", + "prod;": u"\u220f", + "profalar;": u"\u232e", + "profline;": u"\u2312", + "profsurf;": u"\u2313", + "prop;": u"\u221d", + "propto;": u"\u221d", + "prsim;": u"\u227e", + "prurel;": u"\u22b0", + "pscr;": u"\U0001d4c5", + "psi;": u"\u03c8", + "puncsp;": u"\u2008", + "qfr;": u"\U0001d52e", + "qint;": u"\u2a0c", + "qopf;": u"\U0001d562", + "qprime;": u"\u2057", + "qscr;": u"\U0001d4c6", + "quaternions;": u"\u210d", + "quatint;": u"\u2a16", + "quest;": u"?", + "questeq;": u"\u225f", + "quot": u"\"", + "quot;": u"\"", + "rAarr;": u"\u21db", + "rArr;": u"\u21d2", + "rAtail;": u"\u291c", + "rBarr;": u"\u290f", + "rHar;": u"\u2964", + "race;": u"\u223d\u0331", + "racute;": u"\u0155", + "radic;": u"\u221a", + "raemptyv;": u"\u29b3", + "rang;": u"\u27e9", + "rangd;": u"\u2992", + "range;": u"\u29a5", + "rangle;": u"\u27e9", + "raquo": u"\xbb", + "raquo;": u"\xbb", + "rarr;": u"\u2192", + "rarrap;": u"\u2975", + "rarrb;": u"\u21e5", + "rarrbfs;": u"\u2920", + "rarrc;": u"\u2933", + "rarrfs;": u"\u291e", + "rarrhk;": u"\u21aa", + "rarrlp;": u"\u21ac", + "rarrpl;": u"\u2945", + "rarrsim;": u"\u2974", + "rarrtl;": u"\u21a3", + "rarrw;": u"\u219d", + "ratail;": u"\u291a", + "ratio;": u"\u2236", + "rationals;": u"\u211a", + "rbarr;": u"\u290d", + "rbbrk;": u"\u2773", + "rbrace;": u"}", + "rbrack;": u"]", + "rbrke;": u"\u298c", + "rbrksld;": u"\u298e", + "rbrkslu;": u"\u2990", + "rcaron;": u"\u0159", + "rcedil;": u"\u0157", + "rceil;": u"\u2309", + "rcub;": u"}", + "rcy;": u"\u0440", + "rdca;": u"\u2937", + "rdldhar;": u"\u2969", + "rdquo;": u"\u201d", + "rdquor;": u"\u201d", + "rdsh;": u"\u21b3", + "real;": u"\u211c", + "realine;": u"\u211b", + "realpart;": u"\u211c", + "reals;": u"\u211d", + "rect;": u"\u25ad", + "reg": u"\xae", + "reg;": u"\xae", + "rfisht;": u"\u297d", + "rfloor;": u"\u230b", + "rfr;": u"\U0001d52f", + "rhard;": u"\u21c1", + "rharu;": u"\u21c0", + "rharul;": u"\u296c", + "rho;": u"\u03c1", + "rhov;": u"\u03f1", + "rightarrow;": u"\u2192", + "rightarrowtail;": u"\u21a3", + "rightharpoondown;": u"\u21c1", + "rightharpoonup;": u"\u21c0", + "rightleftarrows;": u"\u21c4", + "rightleftharpoons;": u"\u21cc", + "rightrightarrows;": u"\u21c9", + "rightsquigarrow;": u"\u219d", + "rightthreetimes;": u"\u22cc", + "ring;": u"\u02da", + "risingdotseq;": u"\u2253", + "rlarr;": u"\u21c4", + "rlhar;": u"\u21cc", + "rlm;": u"\u200f", + "rmoust;": u"\u23b1", + "rmoustache;": u"\u23b1", + "rnmid;": u"\u2aee", + "roang;": u"\u27ed", + "roarr;": u"\u21fe", + "robrk;": u"\u27e7", + "ropar;": u"\u2986", + "ropf;": u"\U0001d563", + "roplus;": u"\u2a2e", + "rotimes;": u"\u2a35", + "rpar;": u")", + "rpargt;": u"\u2994", + "rppolint;": u"\u2a12", + "rrarr;": u"\u21c9", + "rsaquo;": u"\u203a", + "rscr;": u"\U0001d4c7", + "rsh;": u"\u21b1", + "rsqb;": u"]", + "rsquo;": u"\u2019", + "rsquor;": u"\u2019", + "rthree;": u"\u22cc", + "rtimes;": u"\u22ca", + "rtri;": u"\u25b9", + "rtrie;": u"\u22b5", + "rtrif;": u"\u25b8", + "rtriltri;": u"\u29ce", + "ruluhar;": u"\u2968", + "rx;": u"\u211e", + "sacute;": u"\u015b", + "sbquo;": u"\u201a", + "sc;": u"\u227b", + "scE;": u"\u2ab4", + "scap;": u"\u2ab8", + "scaron;": u"\u0161", + "sccue;": u"\u227d", + "sce;": u"\u2ab0", + "scedil;": u"\u015f", + "scirc;": u"\u015d", + "scnE;": u"\u2ab6", + "scnap;": u"\u2aba", + "scnsim;": u"\u22e9", + "scpolint;": u"\u2a13", + "scsim;": u"\u227f", + "scy;": u"\u0441", + "sdot;": u"\u22c5", + "sdotb;": u"\u22a1", + "sdote;": u"\u2a66", + "seArr;": u"\u21d8", + "searhk;": u"\u2925", + "searr;": u"\u2198", + "searrow;": u"\u2198", + "sect": u"\xa7", + "sect;": u"\xa7", + "semi;": u";", + "seswar;": u"\u2929", + "setminus;": u"\u2216", + "setmn;": u"\u2216", + "sext;": u"\u2736", + "sfr;": u"\U0001d530", + "sfrown;": u"\u2322", + "sharp;": u"\u266f", + "shchcy;": u"\u0449", + "shcy;": u"\u0448", + "shortmid;": u"\u2223", + "shortparallel;": u"\u2225", + "shy": u"\xad", + "shy;": u"\xad", + "sigma;": u"\u03c3", + "sigmaf;": u"\u03c2", + "sigmav;": u"\u03c2", + "sim;": u"\u223c", + "simdot;": u"\u2a6a", + "sime;": u"\u2243", + "simeq;": u"\u2243", + "simg;": u"\u2a9e", + "simgE;": u"\u2aa0", + "siml;": u"\u2a9d", + "simlE;": u"\u2a9f", + "simne;": u"\u2246", + "simplus;": u"\u2a24", + "simrarr;": u"\u2972", + "slarr;": u"\u2190", + "smallsetminus;": u"\u2216", + "smashp;": u"\u2a33", + "smeparsl;": u"\u29e4", + "smid;": u"\u2223", + "smile;": u"\u2323", + "smt;": u"\u2aaa", + "smte;": u"\u2aac", + "smtes;": u"\u2aac\ufe00", + "softcy;": u"\u044c", + "sol;": u"/", + "solb;": u"\u29c4", + "solbar;": u"\u233f", + "sopf;": u"\U0001d564", + "spades;": u"\u2660", + "spadesuit;": u"\u2660", + "spar;": u"\u2225", + "sqcap;": u"\u2293", + "sqcaps;": u"\u2293\ufe00", + "sqcup;": u"\u2294", + "sqcups;": u"\u2294\ufe00", + "sqsub;": u"\u228f", + "sqsube;": u"\u2291", + "sqsubset;": u"\u228f", + "sqsubseteq;": u"\u2291", + "sqsup;": u"\u2290", + "sqsupe;": u"\u2292", + "sqsupset;": u"\u2290", + "sqsupseteq;": u"\u2292", + "squ;": u"\u25a1", + "square;": u"\u25a1", + "squarf;": u"\u25aa", + "squf;": u"\u25aa", + "srarr;": u"\u2192", + "sscr;": u"\U0001d4c8", + "ssetmn;": u"\u2216", + "ssmile;": u"\u2323", + "sstarf;": u"\u22c6", + "star;": u"\u2606", + "starf;": u"\u2605", + "straightepsilon;": u"\u03f5", + "straightphi;": u"\u03d5", + "strns;": u"\xaf", + "sub;": u"\u2282", + "subE;": u"\u2ac5", + "subdot;": u"\u2abd", + "sube;": u"\u2286", + "subedot;": u"\u2ac3", + "submult;": u"\u2ac1", + "subnE;": u"\u2acb", + "subne;": u"\u228a", + "subplus;": u"\u2abf", + "subrarr;": u"\u2979", + "subset;": u"\u2282", + "subseteq;": u"\u2286", + "subseteqq;": u"\u2ac5", + "subsetneq;": u"\u228a", + "subsetneqq;": u"\u2acb", + "subsim;": u"\u2ac7", + "subsub;": u"\u2ad5", + "subsup;": u"\u2ad3", + "succ;": u"\u227b", + "succapprox;": u"\u2ab8", + "succcurlyeq;": u"\u227d", + "succeq;": u"\u2ab0", + "succnapprox;": u"\u2aba", + "succneqq;": u"\u2ab6", + "succnsim;": u"\u22e9", + "succsim;": u"\u227f", + "sum;": u"\u2211", + "sung;": u"\u266a", + "sup1": u"\xb9", + "sup1;": u"\xb9", + "sup2": u"\xb2", + "sup2;": u"\xb2", + "sup3": u"\xb3", + "sup3;": u"\xb3", + "sup;": u"\u2283", + "supE;": u"\u2ac6", + "supdot;": u"\u2abe", + "supdsub;": u"\u2ad8", + "supe;": u"\u2287", + "supedot;": u"\u2ac4", + "suphsol;": u"\u27c9", + "suphsub;": u"\u2ad7", + "suplarr;": u"\u297b", + "supmult;": u"\u2ac2", + "supnE;": u"\u2acc", + "supne;": u"\u228b", + "supplus;": u"\u2ac0", + "supset;": u"\u2283", + "supseteq;": u"\u2287", + "supseteqq;": u"\u2ac6", + "supsetneq;": u"\u228b", + "supsetneqq;": u"\u2acc", + "supsim;": u"\u2ac8", + "supsub;": u"\u2ad4", + "supsup;": u"\u2ad6", + "swArr;": u"\u21d9", + "swarhk;": u"\u2926", + "swarr;": u"\u2199", + "swarrow;": u"\u2199", + "swnwar;": u"\u292a", + "szlig": u"\xdf", + "szlig;": u"\xdf", + "target;": u"\u2316", + "tau;": u"\u03c4", + "tbrk;": u"\u23b4", + "tcaron;": u"\u0165", + "tcedil;": u"\u0163", + "tcy;": u"\u0442", + "tdot;": u"\u20db", + "telrec;": u"\u2315", + "tfr;": u"\U0001d531", + "there4;": u"\u2234", + "therefore;": u"\u2234", + "theta;": u"\u03b8", + "thetasym;": u"\u03d1", + "thetav;": u"\u03d1", + "thickapprox;": u"\u2248", + "thicksim;": u"\u223c", + "thinsp;": u"\u2009", + "thkap;": u"\u2248", + "thksim;": u"\u223c", + "thorn": u"\xfe", + "thorn;": u"\xfe", + "tilde;": u"\u02dc", + "times": u"\xd7", + "times;": u"\xd7", + "timesb;": u"\u22a0", + "timesbar;": u"\u2a31", + "timesd;": u"\u2a30", + "tint;": u"\u222d", + "toea;": u"\u2928", + "top;": u"\u22a4", + "topbot;": u"\u2336", + "topcir;": u"\u2af1", + "topf;": u"\U0001d565", + "topfork;": u"\u2ada", + "tosa;": u"\u2929", + "tprime;": u"\u2034", + "trade;": u"\u2122", + "triangle;": u"\u25b5", + "triangledown;": u"\u25bf", + "triangleleft;": u"\u25c3", + "trianglelefteq;": u"\u22b4", + "triangleq;": u"\u225c", + "triangleright;": u"\u25b9", + "trianglerighteq;": u"\u22b5", + "tridot;": u"\u25ec", + "trie;": u"\u225c", + "triminus;": u"\u2a3a", + "triplus;": u"\u2a39", + "trisb;": u"\u29cd", + "tritime;": u"\u2a3b", + "trpezium;": u"\u23e2", + "tscr;": u"\U0001d4c9", + "tscy;": u"\u0446", + "tshcy;": u"\u045b", + "tstrok;": u"\u0167", + "twixt;": u"\u226c", + "twoheadleftarrow;": u"\u219e", + "twoheadrightarrow;": u"\u21a0", + "uArr;": u"\u21d1", + "uHar;": u"\u2963", + "uacute": u"\xfa", + "uacute;": u"\xfa", + "uarr;": u"\u2191", + "ubrcy;": u"\u045e", + "ubreve;": u"\u016d", + "ucirc": u"\xfb", + "ucirc;": u"\xfb", + "ucy;": u"\u0443", + "udarr;": u"\u21c5", + "udblac;": u"\u0171", + "udhar;": u"\u296e", + "ufisht;": u"\u297e", + "ufr;": u"\U0001d532", + "ugrave": u"\xf9", + "ugrave;": u"\xf9", + "uharl;": u"\u21bf", + "uharr;": u"\u21be", + "uhblk;": u"\u2580", + "ulcorn;": u"\u231c", + "ulcorner;": u"\u231c", + "ulcrop;": u"\u230f", + "ultri;": u"\u25f8", + "umacr;": u"\u016b", + "uml": u"\xa8", + "uml;": u"\xa8", + "uogon;": u"\u0173", + "uopf;": u"\U0001d566", + "uparrow;": u"\u2191", + "updownarrow;": u"\u2195", + "upharpoonleft;": u"\u21bf", + "upharpoonright;": u"\u21be", + "uplus;": u"\u228e", + "upsi;": u"\u03c5", + "upsih;": u"\u03d2", + "upsilon;": u"\u03c5", + "upuparrows;": u"\u21c8", + "urcorn;": u"\u231d", + "urcorner;": u"\u231d", + "urcrop;": u"\u230e", + "uring;": u"\u016f", + "urtri;": u"\u25f9", + "uscr;": u"\U0001d4ca", + "utdot;": u"\u22f0", + "utilde;": u"\u0169", + "utri;": u"\u25b5", + "utrif;": u"\u25b4", + "uuarr;": u"\u21c8", + "uuml": u"\xfc", + "uuml;": u"\xfc", + "uwangle;": u"\u29a7", + "vArr;": u"\u21d5", + "vBar;": u"\u2ae8", + "vBarv;": u"\u2ae9", + "vDash;": u"\u22a8", + "vangrt;": u"\u299c", + "varepsilon;": u"\u03f5", + "varkappa;": u"\u03f0", + "varnothing;": u"\u2205", + "varphi;": u"\u03d5", + "varpi;": u"\u03d6", + "varpropto;": u"\u221d", + "varr;": u"\u2195", + "varrho;": u"\u03f1", + "varsigma;": u"\u03c2", + "varsubsetneq;": u"\u228a\ufe00", + "varsubsetneqq;": u"\u2acb\ufe00", + "varsupsetneq;": u"\u228b\ufe00", + "varsupsetneqq;": u"\u2acc\ufe00", + "vartheta;": u"\u03d1", + "vartriangleleft;": u"\u22b2", + "vartriangleright;": u"\u22b3", + "vcy;": u"\u0432", + "vdash;": u"\u22a2", + "vee;": u"\u2228", + "veebar;": u"\u22bb", + "veeeq;": u"\u225a", + "vellip;": u"\u22ee", + "verbar;": u"|", + "vert;": u"|", + "vfr;": u"\U0001d533", + "vltri;": u"\u22b2", + "vnsub;": u"\u2282\u20d2", + "vnsup;": u"\u2283\u20d2", + "vopf;": u"\U0001d567", + "vprop;": u"\u221d", + "vrtri;": u"\u22b3", + "vscr;": u"\U0001d4cb", + "vsubnE;": u"\u2acb\ufe00", + "vsubne;": u"\u228a\ufe00", + "vsupnE;": u"\u2acc\ufe00", + "vsupne;": u"\u228b\ufe00", + "vzigzag;": u"\u299a", + "wcirc;": u"\u0175", + "wedbar;": u"\u2a5f", + "wedge;": u"\u2227", + "wedgeq;": u"\u2259", + "weierp;": u"\u2118", + "wfr;": u"\U0001d534", + "wopf;": u"\U0001d568", + "wp;": u"\u2118", + "wr;": u"\u2240", + "wreath;": u"\u2240", + "wscr;": u"\U0001d4cc", + "xcap;": u"\u22c2", + "xcirc;": u"\u25ef", + "xcup;": u"\u22c3", + "xdtri;": u"\u25bd", + "xfr;": u"\U0001d535", + "xhArr;": u"\u27fa", + "xharr;": u"\u27f7", + "xi;": u"\u03be", + "xlArr;": u"\u27f8", + "xlarr;": u"\u27f5", + "xmap;": u"\u27fc", + "xnis;": u"\u22fb", + "xodot;": u"\u2a00", + "xopf;": u"\U0001d569", + "xoplus;": u"\u2a01", + "xotime;": u"\u2a02", + "xrArr;": u"\u27f9", + "xrarr;": u"\u27f6", + "xscr;": u"\U0001d4cd", + "xsqcup;": u"\u2a06", + "xuplus;": u"\u2a04", + "xutri;": u"\u25b3", + "xvee;": u"\u22c1", + "xwedge;": u"\u22c0", + "yacute": u"\xfd", + "yacute;": u"\xfd", + "yacy;": u"\u044f", + "ycirc;": u"\u0177", + "ycy;": u"\u044b", + "yen": u"\xa5", + "yen;": u"\xa5", + "yfr;": u"\U0001d536", + "yicy;": u"\u0457", + "yopf;": u"\U0001d56a", + "yscr;": u"\U0001d4ce", + "yucy;": u"\u044e", + "yuml": u"\xff", + "yuml;": u"\xff", + "zacute;": u"\u017a", + "zcaron;": u"\u017e", + "zcy;": u"\u0437", + "zdot;": u"\u017c", + "zeetrf;": u"\u2128", + "zeta;": u"\u03b6", + "zfr;": u"\U0001d537", + "zhcy;": u"\u0436", + "zigrarr;": u"\u21dd", + "zopf;": u"\U0001d56b", + "zscr;": u"\U0001d4cf", + "zwj;": u"\u200d", + "zwnj;": u"\u200c", +} + +replacementCharacters = { + 0x0:u"\uFFFD", + 0x0d:u"\u000D", + 0x80:u"\u20AC", + 0x81:u"\u0081", + 0x81:u"\u0081", + 0x82:u"\u201A", + 0x83:u"\u0192", + 0x84:u"\u201E", + 0x85:u"\u2026", + 0x86:u"\u2020", + 0x87:u"\u2021", + 0x88:u"\u02C6", + 0x89:u"\u2030", + 0x8A:u"\u0160", + 0x8B:u"\u2039", + 0x8C:u"\u0152", + 0x8D:u"\u008D", + 0x8E:u"\u017D", + 0x8F:u"\u008F", + 0x90:u"\u0090", + 0x91:u"\u2018", + 0x92:u"\u2019", + 0x93:u"\u201C", + 0x94:u"\u201D", + 0x95:u"\u2022", + 0x96:u"\u2013", + 0x97:u"\u2014", + 0x98:u"\u02DC", + 0x99:u"\u2122", + 0x9A:u"\u0161", + 0x9B:u"\u203A", + 0x9C:u"\u0153", + 0x9D:u"\u009D", + 0x9E:u"\u017E", + 0x9F:u"\u0178", +} + +encodings = { + '437': 'cp437', + '850': 'cp850', + '852': 'cp852', + '855': 'cp855', + '857': 'cp857', + '860': 'cp860', + '861': 'cp861', + '862': 'cp862', + '863': 'cp863', + '865': 'cp865', + '866': 'cp866', + '869': 'cp869', + 'ansix341968': 'ascii', + 'ansix341986': 'ascii', + 'arabic': 'iso8859-6', + 'ascii': 'ascii', + 'asmo708': 'iso8859-6', + 'big5': 'big5', + 'big5hkscs': 'big5hkscs', + 'chinese': 'gbk', + 'cp037': 'cp037', + 'cp1026': 'cp1026', + 'cp154': 'ptcp154', + 'cp367': 'ascii', + 'cp424': 'cp424', + 'cp437': 'cp437', + 'cp500': 'cp500', + 'cp775': 'cp775', + 'cp819': 'windows-1252', + 'cp850': 'cp850', + 'cp852': 'cp852', + 'cp855': 'cp855', + 'cp857': 'cp857', + 'cp860': 'cp860', + 'cp861': 'cp861', + 'cp862': 'cp862', + 'cp863': 'cp863', + 'cp864': 'cp864', + 'cp865': 'cp865', + 'cp866': 'cp866', + 'cp869': 'cp869', + 'cp936': 'gbk', + 'cpgr': 'cp869', + 'cpis': 'cp861', + 'csascii': 'ascii', + 'csbig5': 'big5', + 'cseuckr': 'cp949', + 'cseucpkdfmtjapanese': 'euc_jp', + 'csgb2312': 'gbk', + 'cshproman8': 'hp-roman8', + 'csibm037': 'cp037', + 'csibm1026': 'cp1026', + 'csibm424': 'cp424', + 'csibm500': 'cp500', + 'csibm855': 'cp855', + 'csibm857': 'cp857', + 'csibm860': 'cp860', + 'csibm861': 'cp861', + 'csibm863': 'cp863', + 'csibm864': 'cp864', + 'csibm865': 'cp865', + 'csibm866': 'cp866', + 'csibm869': 'cp869', + 'csiso2022jp': 'iso2022_jp', + 'csiso2022jp2': 'iso2022_jp_2', + 'csiso2022kr': 'iso2022_kr', + 'csiso58gb231280': 'gbk', + 'csisolatin1': 'windows-1252', + 'csisolatin2': 'iso8859-2', + 'csisolatin3': 'iso8859-3', + 'csisolatin4': 'iso8859-4', + 'csisolatin5': 'windows-1254', + 'csisolatin6': 'iso8859-10', + 'csisolatinarabic': 'iso8859-6', + 'csisolatincyrillic': 'iso8859-5', + 'csisolatingreek': 'iso8859-7', + 'csisolatinhebrew': 'iso8859-8', + 'cskoi8r': 'koi8-r', + 'csksc56011987': 'cp949', + 'cspc775baltic': 'cp775', + 'cspc850multilingual': 'cp850', + 'cspc862latinhebrew': 'cp862', + 'cspc8codepage437': 'cp437', + 'cspcp852': 'cp852', + 'csptcp154': 'ptcp154', + 'csshiftjis': 'shift_jis', + 'csunicode11utf7': 'utf-7', + 'cyrillic': 'iso8859-5', + 'cyrillicasian': 'ptcp154', + 'ebcdiccpbe': 'cp500', + 'ebcdiccpca': 'cp037', + 'ebcdiccpch': 'cp500', + 'ebcdiccphe': 'cp424', + 'ebcdiccpnl': 'cp037', + 'ebcdiccpus': 'cp037', + 'ebcdiccpwt': 'cp037', + 'ecma114': 'iso8859-6', + 'ecma118': 'iso8859-7', + 'elot928': 'iso8859-7', + 'eucjp': 'euc_jp', + 'euckr': 'cp949', + 'extendedunixcodepackedformatforjapanese': 'euc_jp', + 'gb18030': 'gb18030', + 'gb2312': 'gbk', + 'gb231280': 'gbk', + 'gbk': 'gbk', + 'greek': 'iso8859-7', + 'greek8': 'iso8859-7', + 'hebrew': 'iso8859-8', + 'hproman8': 'hp-roman8', + 'hzgb2312': 'hz', + 'ibm037': 'cp037', + 'ibm1026': 'cp1026', + 'ibm367': 'ascii', + 'ibm424': 'cp424', + 'ibm437': 'cp437', + 'ibm500': 'cp500', + 'ibm775': 'cp775', + 'ibm819': 'windows-1252', + 'ibm850': 'cp850', + 'ibm852': 'cp852', + 'ibm855': 'cp855', + 'ibm857': 'cp857', + 'ibm860': 'cp860', + 'ibm861': 'cp861', + 'ibm862': 'cp862', + 'ibm863': 'cp863', + 'ibm864': 'cp864', + 'ibm865': 'cp865', + 'ibm866': 'cp866', + 'ibm869': 'cp869', + 'iso2022jp': 'iso2022_jp', + 'iso2022jp2': 'iso2022_jp_2', + 'iso2022kr': 'iso2022_kr', + 'iso646irv1991': 'ascii', + 'iso646us': 'ascii', + 'iso88591': 'windows-1252', + 'iso885910': 'iso8859-10', + 'iso8859101992': 'iso8859-10', + 'iso885911987': 'windows-1252', + 'iso885913': 'iso8859-13', + 'iso885914': 'iso8859-14', + 'iso8859141998': 'iso8859-14', + 'iso885915': 'iso8859-15', + 'iso885916': 'iso8859-16', + 'iso8859162001': 'iso8859-16', + 'iso88592': 'iso8859-2', + 'iso885921987': 'iso8859-2', + 'iso88593': 'iso8859-3', + 'iso885931988': 'iso8859-3', + 'iso88594': 'iso8859-4', + 'iso885941988': 'iso8859-4', + 'iso88595': 'iso8859-5', + 'iso885951988': 'iso8859-5', + 'iso88596': 'iso8859-6', + 'iso885961987': 'iso8859-6', + 'iso88597': 'iso8859-7', + 'iso885971987': 'iso8859-7', + 'iso88598': 'iso8859-8', + 'iso885981988': 'iso8859-8', + 'iso88599': 'windows-1254', + 'iso885991989': 'windows-1254', + 'isoceltic': 'iso8859-14', + 'isoir100': 'windows-1252', + 'isoir101': 'iso8859-2', + 'isoir109': 'iso8859-3', + 'isoir110': 'iso8859-4', + 'isoir126': 'iso8859-7', + 'isoir127': 'iso8859-6', + 'isoir138': 'iso8859-8', + 'isoir144': 'iso8859-5', + 'isoir148': 'windows-1254', + 'isoir149': 'cp949', + 'isoir157': 'iso8859-10', + 'isoir199': 'iso8859-14', + 'isoir226': 'iso8859-16', + 'isoir58': 'gbk', + 'isoir6': 'ascii', + 'koi8r': 'koi8-r', + 'koi8u': 'koi8-u', + 'korean': 'cp949', + 'ksc5601': 'cp949', + 'ksc56011987': 'cp949', + 'ksc56011989': 'cp949', + 'l1': 'windows-1252', + 'l10': 'iso8859-16', + 'l2': 'iso8859-2', + 'l3': 'iso8859-3', + 'l4': 'iso8859-4', + 'l5': 'windows-1254', + 'l6': 'iso8859-10', + 'l8': 'iso8859-14', + 'latin1': 'windows-1252', + 'latin10': 'iso8859-16', + 'latin2': 'iso8859-2', + 'latin3': 'iso8859-3', + 'latin4': 'iso8859-4', + 'latin5': 'windows-1254', + 'latin6': 'iso8859-10', + 'latin8': 'iso8859-14', + 'latin9': 'iso8859-15', + 'ms936': 'gbk', + 'mskanji': 'shift_jis', + 'pt154': 'ptcp154', + 'ptcp154': 'ptcp154', + 'r8': 'hp-roman8', + 'roman8': 'hp-roman8', + 'shiftjis': 'shift_jis', + 'tis620': 'cp874', + 'unicode11utf7': 'utf-7', + 'us': 'ascii', + 'usascii': 'ascii', + 'utf16': 'utf-16', + 'utf16be': 'utf-16-be', + 'utf16le': 'utf-16-le', + 'utf8': 'utf-8', + 'windows1250': 'cp1250', + 'windows1251': 'cp1251', + 'windows1252': 'cp1252', + 'windows1253': 'cp1253', + 'windows1254': 'cp1254', + 'windows1255': 'cp1255', + 'windows1256': 'cp1256', + 'windows1257': 'cp1257', + 'windows1258': 'cp1258', + 'windows936': 'gbk', + 'x-x-big5': 'big5'} + +tokenTypes = { + "Doctype":0, + "Characters":1, + "SpaceCharacters":2, + "StartTag":3, + "EndTag":4, + "EmptyTag":5, + "Comment":6, + "ParseError":7 +} + +tagTokenTypes = frozenset((tokenTypes["StartTag"], tokenTypes["EndTag"], + tokenTypes["EmptyTag"])) + + +prefixes = dict([(v,k) for k,v in namespaces.iteritems()]) +prefixes["http://www.w3.org/1998/Math/MathML"] = "math" + +class DataLossWarning(UserWarning): + pass + +class ReparseException(Exception): + pass diff --git a/libs/html5lib/filters/__init__.py b/libs/html5lib/filters/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/html5lib/filters/_base.py b/libs/html5lib/filters/_base.py new file mode 100644 index 00000000..bca94ada --- /dev/null +++ b/libs/html5lib/filters/_base.py @@ -0,0 +1,10 @@ + +class Filter(object): + def __init__(self, source): + self.source = source + + def __iter__(self): + return iter(self.source) + + def __getattr__(self, name): + return getattr(self.source, name) diff --git a/libs/html5lib/filters/formfiller.py b/libs/html5lib/filters/formfiller.py new file mode 100644 index 00000000..94001714 --- /dev/null +++ b/libs/html5lib/filters/formfiller.py @@ -0,0 +1,127 @@ +# +# The goal is to finally have a form filler where you pass data for +# each form, using the algorithm for "Seeding a form with initial values" +# See http://www.whatwg.org/specs/web-forms/current-work/#seeding +# + +import _base + +from html5lib.constants import spaceCharacters +spaceCharacters = u"".join(spaceCharacters) + +class SimpleFilter(_base.Filter): + def __init__(self, source, fieldStorage): + _base.Filter.__init__(self, source) + self.fieldStorage = fieldStorage + + def __iter__(self): + field_indices = {} + state = None + field_name = None + for token in _base.Filter.__iter__(self): + type = token["type"] + if type in ("StartTag", "EmptyTag"): + name = token["name"].lower() + if name == "input": + field_name = None + field_type = None + input_value_index = -1 + input_checked_index = -1 + for i,(n,v) in enumerate(token["data"]): + n = n.lower() + if n == u"name": + field_name = v.strip(spaceCharacters) + elif n == u"type": + field_type = v.strip(spaceCharacters) + elif n == u"checked": + input_checked_index = i + elif n == u"value": + input_value_index = i + + value_list = self.fieldStorage.getlist(field_name) + field_index = field_indices.setdefault(field_name, 0) + if field_index < len(value_list): + value = value_list[field_index] + else: + value = "" + + if field_type in (u"checkbox", u"radio"): + if value_list: + if token["data"][input_value_index][1] == value: + if input_checked_index < 0: + token["data"].append((u"checked", u"")) + field_indices[field_name] = field_index + 1 + elif input_checked_index >= 0: + del token["data"][input_checked_index] + + elif field_type not in (u"button", u"submit", u"reset"): + if input_value_index >= 0: + token["data"][input_value_index] = (u"value", value) + else: + token["data"].append((u"value", value)) + field_indices[field_name] = field_index + 1 + + field_type = None + field_name = None + + elif name == "textarea": + field_type = "textarea" + field_name = dict((token["data"])[::-1])["name"] + + elif name == "select": + field_type = "select" + attributes = dict(token["data"][::-1]) + field_name = attributes.get("name") + is_select_multiple = "multiple" in attributes + is_selected_option_found = False + + elif field_type == "select" and field_name and name == "option": + option_selected_index = -1 + option_value = None + for i,(n,v) in enumerate(token["data"]): + n = n.lower() + if n == "selected": + option_selected_index = i + elif n == "value": + option_value = v.strip(spaceCharacters) + if option_value is None: + raise NotImplementedError("