diff --git a/.gitignore b/.gitignore
index f903669d..e134ddb6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
+*.pyc
+/data/
/_source/
\ No newline at end of file
diff --git a/CouchPotato.py b/CouchPotato.py
index dd1e6549..21b208a2 100755
--- a/CouchPotato.py
+++ b/CouchPotato.py
@@ -35,7 +35,11 @@ class Loader(object):
settings.setFile(self.options.config_file)
# Create data dir if needed
- self.data_dir = os.path.expanduser(Env.setting('data_dir'))
+ if self.options.data_dir:
+ self.data_dir = self.options.data_dir
+ else:
+ self.data_dir = os.path.expanduser(Env.setting('data_dir'))
+
if self.data_dir == '':
self.data_dir = getDataDir()
@@ -89,7 +93,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/__init__.py b/couchpotato/__init__.py
index 3e283639..38b36174 100644
--- a/couchpotato/__init__.py
+++ b/couchpotato/__init__.py
@@ -69,10 +69,14 @@ 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)
+ if request.path != '/':
+ r = request.url.replace(request.path, index_url + '#' + url)
+ else:
+ r = '%s%s' % (request.url.rstrip('/'), index_url + '#' + url)
+ return redirect(r)
else:
time.sleep(0.1)
return 'Wrong API key used', 404
diff --git a/couchpotato/api.py b/couchpotato/api.py
index b1dee1b3..15ef2b4c 100644
--- a/couchpotato/api.py
+++ b/couchpotato/api.py
@@ -1,10 +1,39 @@
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):
+ cls = NonBlockHandler
+ start, stop = api_nonblock[route]
+ cls.stoppers.append(stop)
+
+ start(self.onNewMessage, last_id = self.get_argument("last_id", None))
+
+ def onNewMessage(self, response):
+ if self.request.connection.stream.closed():
+ return
+ self.finish(response)
+
+ def on_connection_close(self):
+ cls = NonBlockHandler
+
+ for stop in cls.stoppers:
+ stop(self.onNewMessage)
+
+ cls.stoppers = []
+
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 +42,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 a496df60..58bd1d19 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,10 @@ log = CPLog(__name__)
class Core(Plugin):
- ignore_restart = ['Core.crappyRestart', 'Core.crappyShutdown']
+ ignore_restart = [
+ 'Core.restart', 'Core.shutdown',
+ 'Updater.check', 'Updater.autoUpdate',
+ ]
shutdown_started = False
def __init__(self):
@@ -37,12 +40,13 @@ 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)
addEvent('app.version', self.version)
+ addEvent('app.load', self.checkDataDir)
addEvent('setting.save.core.password', self.md5Password)
addEvent('setting.save.core.api_key', self.checkApikey)
@@ -54,39 +58,35 @@ class Core(Plugin):
def checkApikey(self, value):
return value if value and len(value) > 3 else uuid4().hex
+ def checkDataDir(self):
+ if Env.get('app_dir') in Env.get('data_dir'):
+ log.error('You should NOT use your CouchPotato directory to save your settings in. Files will get overwritten or be deleted.')
+
+ return True
+
def available(self):
return jsonified({
'succes': True
})
- def crappyShutdown(self):
- if self.shutdown_started:
- return
-
- try:
- self.urlopen('%s/app.shutdown' % self.createApiUrl(), show_error = False)
- return True
- except:
- self.initShutdown()
- return False
-
- 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
-
def shutdown(self):
- self.initShutdown()
+ if self.shutdown_started:
+ return False
+
+ def shutdown():
+ self.initShutdown()
+ IOLoop.instance().add_callback(shutdown)
+
return 'shutdown'
def restart(self):
- self.initShutdown(restart = True)
+ if self.shutdown_started:
+ return False
+
+ def restart():
+ self.initShutdown(restart = True)
+ IOLoop.instance().add_callback(restart)
+
return 'restarting'
def initShutdown(self, restart = False):
@@ -102,17 +102,20 @@ 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)
- log.debug('Still running: %s' % still_running)
+ log.debug('Still running: %s', still_running)
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:
- log.info('Waiting on plugins to finish: %s' % running)
+ log.info('Waiting on plugins to finish: %s', running)
else:
loop = False
@@ -121,11 +124,11 @@ class Core(Plugin):
log.debug('Save to shutdown/restart')
try:
- request.environ.get('werkzeug.server.shutdown')()
+ IOLoop.instance().stop()
except RuntimeError:
pass
except:
- log.error('Failed shutting down the server: %s' % traceback.format_exc())
+ log.error('Failed shutting down the server: %s', traceback.format_exc())
fireEvent('app.after_shutdown', restart = 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/scheduler/main.py b/couchpotato/core/_base/scheduler/main.py
index d09efedb..d442722d 100644
--- a/couchpotato/core/_base/scheduler/main.py
+++ b/couchpotato/core/_base/scheduler/main.py
@@ -28,7 +28,7 @@ class Scheduler(Plugin):
for type in ['interval', 'cron']:
try:
self.sched.unschedule_job(getattr(self, type)[identifier]['job'])
- log.debug('%s unscheduled %s' % (type.capitalize(), identifier))
+ log.debug('%s unscheduled %s', (type.capitalize(), identifier))
except:
pass
@@ -45,7 +45,7 @@ class Scheduler(Plugin):
job = self.sched.add_cron_job(cron['handle'], day = cron['day'], hour = cron['hour'], minute = cron['minute'])
cron['job'] = job
except ValueError, e:
- log.error("Failed adding cronjob: %s" % e)
+ log.error('Failed adding cronjob: %s', e)
# Intervals
for identifier in self.intervals:
@@ -55,7 +55,7 @@ class Scheduler(Plugin):
job = self.sched.add_interval_job(interval['handle'], hours = interval['hours'], minutes = interval['minutes'], seconds = interval['seconds'])
interval['job'] = job
except ValueError, e:
- log.error("Failed adding interval cronjob: %s" % e)
+ log.error('Failed adding interval cronjob: %s', e)
# Start it
log.debug('Starting scheduler')
@@ -75,7 +75,7 @@ class Scheduler(Plugin):
self.started = False
def cron(self, identifier = '', handle = None, day = '*', hour = '*', minute = '*'):
- log.info('Scheduling "%s", cron: day = %s, hour = %s, minute = %s' % (identifier, day, hour, minute))
+ log.info('Scheduling "%s", cron: day = %s, hour = %s, minute = %s', (identifier, day, hour, minute))
self.remove(identifier)
self.crons[identifier] = {
@@ -86,7 +86,7 @@ class Scheduler(Plugin):
}
def interval(self, identifier = '', handle = None, hours = 0, minutes = 0, seconds = 0):
- log.info('Scheduling %s, interval: hours = %s, minutes = %s, seconds = %s' % (identifier, hours, minutes, seconds))
+ log.info('Scheduling %s, interval: hours = %s, minutes = %s, seconds = %s', (identifier, hours, minutes, seconds))
self.remove(identifier)
self.intervals[identifier] = {
diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py
index dd1c3d00..aeec429f 100644
--- a/couchpotato/core/_base/updater/main.py
+++ b/couchpotato/core/_base/updater/main.py
@@ -1,5 +1,6 @@
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
+from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
@@ -19,6 +20,8 @@ log = CPLog(__name__)
class Updater(Plugin):
+ available_notified = False
+
def __init__(self):
if Env.get('desktop'):
@@ -28,7 +31,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 +51,36 @@ class Updater(Plugin):
'return': {'type': 'see updater.info'}
})
+ def autoUpdate(self):
+ if self.check() and self.conf('automatic') and not self.updater.update_failed:
+ if self.updater.doUpdate():
+
+ # Notify before restarting
+ try:
+ if self.conf('notification'):
+ info = self.updater.info()
+ 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)
+ except:
+ log.error('Failed notifying for update: %s', traceback.format_exc())
+
+ fireEventAsync('app.restart')
+
+ return True
+
+ return False
+
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.crappy_restart')
- else:
- if self.conf('notification'):
- fireEvent('updater.available', message = 'A new update is available', data = self.updater.info())
+ if not self.available_notified and self.conf('notification') and not self.conf('automatic'):
+ fireEvent('updater.available', message = 'A new update is available', data = self.updater.info())
+ self.available_notified = True
+ return True
+
+ return False
def info(self):
return self.updater.info()
@@ -67,12 +89,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.updater.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
})
@@ -107,7 +139,7 @@ class BaseUpdater(Plugin):
def deletePyc(self, only_excess = True):
- for root, dirs, files in os.walk(Env.get('app_dir')):
+ for root, dirs, files in os.walk(ss(Env.get('app_dir'))):
pyc_files = filter(lambda filename: filename.endswith('.pyc'), files)
py_files = set(filter(lambda filename: filename.endswith('.py'), files))
@@ -115,11 +147,11 @@ class BaseUpdater(Plugin):
for excess_pyc_file in excess_pyc_files:
full_path = os.path.join(root, excess_pyc_file)
- log.debug('Removing old PYC file: %s' % full_path)
+ log.debug('Removing old PYC file: %s', full_path)
try:
os.remove(full_path)
except:
- log.error('Couldn\'t remove %s: %s' % (full_path, traceback.format_exc()))
+ log.error('Couldn\'t remove %s: %s', (full_path, traceback.format_exc()))
for dir_name in dirs:
full_path = os.path.join(root, dir_name)
@@ -127,7 +159,7 @@ class BaseUpdater(Plugin):
try:
os.rmdir(full_path)
except:
- log.error('Couldn\'t remove empty directory %s: %s' % (full_path, traceback.format_exc()))
+ log.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc()))
@@ -137,24 +169,20 @@ 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()
log.info('Updating to latest version')
- info = self.info()
self.repo.pull()
# Delete leftover .pyc files
self.deletePyc()
- # Notify before returning and restarting
- 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)
-
return True
except:
- log.error('Failed updating via GIT: %s' % traceback.format_exc())
+ log.error('Failed updating via GIT: %s', traceback.format_exc())
self.update_failed = True
@@ -165,14 +193,14 @@ class GitUpdater(BaseUpdater):
if not self.version:
try:
output = self.repo.getHead() # Yes, please
- log.debug('Git version output: %s' % output.hash)
+ log.debug('Git version output: %s', output.hash)
self.version = {
'hash': output.hash[:8],
'date': output.getDate(),
'type': 'git',
}
except Exception, e:
- log.error('Failed using GIT updater, running from source, you need to have GIT installed. %s' % e)
+ log.error('Failed using GIT updater, running from source, you need to have GIT installed. %s', e)
return 'No GIT'
return self.version
@@ -182,7 +210,7 @@ class GitUpdater(BaseUpdater):
if self.update_version:
return
- log.info('Checking for new version on github for %s' % self.repo_name)
+ log.info('Checking for new version on github for %s', self.repo_name)
if not Env.get('dev'):
self.repo.fetch()
@@ -194,7 +222,7 @@ class GitUpdater(BaseUpdater):
local = self.repo.getHead()
remote = branch.getHead()
- log.info('Versions, local:%s, remote:%s' % (local.hash[:8], remote.hash[:8]))
+ log.info('Versions, local:%s, remote:%s', (local.hash[:8], remote.hash[:8]))
if local.getDate() < remote.getDate():
self.update_version = {
@@ -237,24 +265,24 @@ class SourceUpdater(BaseUpdater):
tar.close()
os.remove(destination)
- self.replaceWith(os.path.join(extracted_path, os.listdir(extracted_path)[0]))
- self.removeDir(extracted_path)
+ if self.replaceWith(os.path.join(extracted_path, os.listdir(extracted_path)[0])):
+ self.removeDir(extracted_path)
- # Write update version to file
- self.createFile(self.version_file, json.dumps(self.update_version))
+ # Write update version to file
+ self.createFile(self.version_file, json.dumps(self.update_version))
- return True
+ return True
except:
- log.error('Failed updating: %s' % traceback.format_exc())
+ log.error('Failed updating: %s', traceback.format_exc())
self.update_failed = True
return False
def replaceWith(self, path):
- app_dir = Env.get('app_dir')
+ app_dir = ss(Env.get('app_dir'))
# Get list of files we want to overwrite
- self.deletePyc(only_excess = False)
+ self.deletePyc()
existing_files = []
for root, subfiles, filenames in os.walk(app_dir):
for filename in filenames:
@@ -267,18 +295,30 @@ class SourceUpdater(BaseUpdater):
if not Env.get('dev'):
try:
- os.remove(tofile)
- except:
- pass
+ if os.path.isfile(tofile):
+ os.remove(tofile)
- try:
- os.renames(fromfile, tofile)
+ dirname = os.path.dirname(tofile)
+ if not os.path.isdir(dirname):
+ self.makeDir(dirname)
+
+ os.rename(fromfile, tofile)
try:
existing_files.remove(tofile)
except ValueError:
pass
- except Exception, e:
- log.error('Failed overwriting file: %s' % e)
+ except:
+ log.error('Failed overwriting file "%s": %s', (tofile, traceback.format_exc()))
+ return False
+
+ if Env.get('app_dir') not in Env.get('data_dir'):
+ for still_exists in existing_files:
+ try:
+ os.remove(still_exists)
+ except:
+ log.error('Failed removing non-used file: %s', traceback.format_exc())
+
+ return True
def removeDir(self, path):
@@ -297,11 +337,11 @@ class SourceUpdater(BaseUpdater):
output = json.loads(f.read())
f.close()
- log.debug('Source version output: %s' % output)
+ log.debug('Source version output: %s', output)
self.version = output
self.version['type'] = 'source'
except Exception, e:
- log.error('Failed using source updater. %s' % e)
+ log.error('Failed using source updater. %s', e)
return {}
return self.version
@@ -318,7 +358,7 @@ class SourceUpdater(BaseUpdater):
self.last_check = time.time()
except:
- log.error('Failed updating via source: %s' % traceback.format_exc())
+ log.error('Failed updating via source: %s', traceback.format_exc())
return self.update_version is not None
@@ -333,12 +373,12 @@ class SourceUpdater(BaseUpdater):
'date': int(time.mktime(parse(commit['commit']['committer']['date']).timetuple())),
}
except:
- log.error('Failed getting latest request from github: %s' % traceback.format_exc())
+ log.error('Failed getting latest request from github: %s', traceback.format_exc())
return {}
-class DesktopUpdater(Plugin):
+class DesktopUpdater(BaseUpdater):
version = None
update_failed = False
@@ -350,9 +390,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
@@ -379,7 +425,7 @@ class DesktopUpdater(Plugin):
self.last_check = time.time()
except:
- log.error('Failed updating desktop: %s' % traceback.format_exc())
+ log.error('Failed updating desktop: %s', traceback.format_exc())
return self.update_version is not None
diff --git a/couchpotato/core/_base/updater/static/updater.js b/couchpotato/core/_base/updater/static/updater.js
index bcbf48a8..df6cf351 100644
--- a/couchpotato/core/_base/updater/static/updater.js
+++ b/couchpotato/core/_base/updater/static/updater.js
@@ -16,7 +16,17 @@ 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();
+ App.fireEvent('message', 'No updates available');
+ }
+ }
})
},
@@ -52,7 +62,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
@@ -81,13 +91,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);
- 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/core/downloaders/base.py b/couchpotato/core/downloaders/base.py
index 710251aa..74e04de6 100644
--- a/couchpotato/core/downloaders/base.py
+++ b/couchpotato/core/downloaders/base.py
@@ -23,7 +23,7 @@ class Downloader(Plugin):
def createFileName(self, data, filedata, movie):
name = os.path.join(self.createNzbName(data, movie))
- if data.get('type') == 'nzb' and "DOCTYPE nzb" not in filedata:
+ if data.get('type') == 'nzb' and 'DOCTYPE nzb' not in filedata and '' not in filedata:
return '%s.%s' % (name, 'rar')
return '%s.%s' % (name, data.get('type'))
diff --git a/couchpotato/core/downloaders/blackhole/__init__.py b/couchpotato/core/downloaders/blackhole/__init__.py
index b9796051..c287538b 100644
--- a/couchpotato/core/downloaders/blackhole/__init__.py
+++ b/couchpotato/core/downloaders/blackhole/__init__.py
@@ -30,7 +30,7 @@ config = [{
'label': 'Use for',
'default': 'both',
'type': 'dropdown',
- 'values': [('nzbs & torrents', 'both'), ('nzb', 'nzb'), ('torrent', 'torrent')],
+ 'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')],
},
{
'name': 'manual',
diff --git a/couchpotato/core/downloaders/blackhole/main.py b/couchpotato/core/downloaders/blackhole/main.py
index a24e71b1..a3f03617 100644
--- a/couchpotato/core/downloaders/blackhole/main.py
+++ b/couchpotato/core/downloaders/blackhole/main.py
@@ -10,38 +10,36 @@ 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
directory = self.conf('directory')
if not directory or not os.path.isdir(directory):
- log.error('No directory set for blackhole %s download.' % data.get('type'))
+ 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))
try:
if not os.path.isfile(fullPath):
- log.info('Downloading %s to %s.' % (data.get('type'), fullPath))
+ log.info('Downloading %s to %s.', (data.get('type'), fullPath))
with open(fullPath, 'wb') as f:
f.write(filedata)
return True
else:
- log.info('File %s already exists.' % fullPath)
+ log.info('File %s already exists.', fullPath)
return True
except:
- log.error('Failed to download to blackhole %s' % traceback.format_exc())
+ log.error('Failed to download to blackhole %s', traceback.format_exc())
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..0d9c52aa 100644
--- a/couchpotato/core/downloaders/nzbget/main.py
+++ b/couchpotato/core/downloaders/nzbget/main.py
@@ -14,12 +14,16 @@ 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
- log.info('Sending "%s" to NZBGet.' % data.get('name'))
+ 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')}
nzb_name = '%s.nzb' % self.createNzbName(data, movie)
@@ -37,25 +41,12 @@ class NZBGet(Downloader):
if e.errcode == 401:
log.error('Password is incorrect.')
else:
- 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())
+ log.error('Protocol Error: %s', e)
return False
if rpc.append(nzb_name, self.conf('category'), False, standard_b64encode(filedata.strip())):
log.info('NZB sent successfully to NZBGet')
return True
else:
- log.error('NZBGet could not add %s to the queue.' % nzb_name)
+ log.error('NZBGet could not add %s to the queue.', nzb_name)
return False
diff --git a/couchpotato/core/downloaders/pneumatic/__init__.py b/couchpotato/core/downloaders/pneumatic/__init__.py
new file mode 100644
index 00000000..dedcde19
--- /dev/null
+++ b/couchpotato/core/downloaders/pneumatic/__init__.py
@@ -0,0 +1,37 @@
+from .main import Pneumatic
+
+def start():
+ return Pneumatic()
+
+config = [{
+ 'name': 'pneumatic',
+ 'order': 30,
+ 'groups': [
+ {
+ 'tab': 'downloaders',
+ 'name': 'pneumatic',
+ 'label': 'Pneumatic',
+ 'description': 'Download the .strm file to a specific folder.',
+ 'wizard': True,
+ 'options': [
+ {
+ 'name': 'enabled',
+ 'default': 0,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'directory',
+ 'type': 'directory',
+ 'description': 'Directory where the .strm file is saved to.',
+ },
+ {
+ 'name': 'manual',
+ 'default': 0,
+ 'type': 'bool',
+ 'advanced': True,
+ 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
+ },
+ ],
+ }
+ ],
+}]
diff --git a/couchpotato/core/downloaders/pneumatic/main.py b/couchpotato/core/downloaders/pneumatic/main.py
new file mode 100644
index 00000000..57e47e60
--- /dev/null
+++ b/couchpotato/core/downloaders/pneumatic/main.py
@@ -0,0 +1,56 @@
+from __future__ import with_statement
+from couchpotato.core.downloaders.base import Downloader
+from couchpotato.core.logger import CPLog
+import os
+import traceback
+
+log = CPLog(__name__)
+
+class Pneumatic(Downloader):
+
+ type = ['nzb']
+ strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s'
+
+ def download(self, data = {}, movie = {}, manual = False, filedata = None):
+ if self.isDisabled(manual) or (not self.isCorrectType(data.get('type'))):
+ return
+
+ directory = self.conf('directory')
+ if not directory or not os.path.isdir(directory):
+ log.error('No directory set for .strm downloads.')
+ else:
+ try:
+ if not filedata or len(filedata) < 50:
+ log.error('No nzb available!')
+ return False
+
+ fullPath = os.path.join(directory, self.createFileName(data, filedata, movie))
+
+ try:
+ if not os.path.isfile(fullPath):
+ log.info('Downloading %s to %s.', (data.get('type'), fullPath))
+ with open(fullPath, 'wb') as f:
+ f.write(filedata)
+
+ nzb_name = self.createNzbName(data, movie)
+ strm_path = os.path.join(directory, nzb_name)
+
+ strm_file = open(strm_path + '.strm', 'wb')
+ strmContent = self.strm_syntax % (fullPath, nzb_name)
+ strm_file.write(strmContent)
+ strm_file.close()
+
+ return True
+
+ else:
+ log.info('File %s already exists.', fullPath)
+ return True
+
+ except:
+ log.error('Failed to download .strm: %s', traceback.format_exc())
+ pass
+
+ except:
+ log.info('Failed to download file %s: %s', (data.get('name'), traceback.format_exc()))
+ return False
+ return False
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 movies or couchpotato',
},
- {
- '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 5505438e..64b24509 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__)
@@ -15,25 +10,12 @@ 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
- 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
+ log.info('Sending "%s" to SABnzbd.', data.get('name'))
params = {
'apikey': self.conf('api_key'),
@@ -42,29 +24,24 @@ 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')
- if pp:
- params['script'] = pp_script_fn
-
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
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, timeout = 60, params = {"nzbfile": (nzb_filename, filedata)}, multipart = True, show_error = False)
else:
- data = self.urlopen(url, show_error = False)
+ data = self.urlopen(url, timeout = 60, show_error = False)
except:
log.error(traceback.format_exc())
return False
@@ -84,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
diff --git a/couchpotato/core/downloaders/transmission/__init__.py b/couchpotato/core/downloaders/transmission/__init__.py
index f72dea6b..189fcd91 100644
--- a/couchpotato/core/downloaders/transmission/__init__.py
+++ b/couchpotato/core/downloaders/transmission/__init__.py
@@ -34,6 +34,7 @@ config = [{
{
'name': 'paused',
'type': 'bool',
+ 'default': False,
'description': 'Add the torrent paused.',
},
{
diff --git a/couchpotato/core/downloaders/transmission/main.py b/couchpotato/core/downloaders/transmission/main.py
index 4ed3e583..1819f310 100644
--- a/couchpotato/core/downloaders/transmission/main.py
+++ b/couchpotato/core/downloaders/transmission/main.py
@@ -2,48 +2,139 @@ from base64 import b64encode
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import isInt
from couchpotato.core.logger import CPLog
-from libs import transmissionrpc
+import httplib
+import json
+import re
+import urllib2
log = CPLog(__name__)
class Transmission(Downloader):
- type = ['torrent']
+ type = ['torrent', 'torrent_magnet']
+ log = CPLog(__name__)
- 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
- log.info('Sending "%s" to Transmission.' % data.get('name'))
+ log.debug('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type')))
# Load host from config and split out port.
host = self.conf('host').split(':')
if not isInt(host[1]):
- log.error("Config properties are not filled in correctly, port is missing.")
+ log.error('Config properties are not filled in correctly, port is missing.')
return False
# Set parameters for Transmission
params = {
'paused': self.conf('paused', default = 0),
- 'download_dir': self.conf('directory', default = None)
+ 'download-dir': self.conf('directory', default = None)
}
+ torrent_params = {
+ 'seedRatioLimit': self.conf('ratio'),
+ 'seedRatioMode': (0 if self.conf('ratio') else 1)
+ }
+
+ if not filedata and data.get('type') == 'torrent':
+ log.error('Failed sending torrent, no data')
+ return False
+
+ # Send request to Transmission
try:
- 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)
+ trpc = TransmissionRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
+ if data.get('type') == 'torrent_magnet':
+ remote_torrent = trpc.add_torrent_uri(data.get('url'), arguments = params)
+ else:
+ remote_torrent = trpc.add_torrent_file(b64encode(filedata), arguments = params)
# Change settings of added torrents
- try:
- torrent.seed_ratio_limit = self.conf('ratio')
- torrent.seed_ratio_mode = 'single' if self.conf('ratio') else 'global'
- except transmissionrpc.TransmissionError, e:
- log.error('Failed to change settings for transfer in transmission: %s' % e)
+ trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
return True
-
- except transmissionrpc.TransmissionError, e:
- log.error('Failed to send link to transmission: %s' % e)
+ except Exception, err:
+ log.error('Failed to change settings for transfer: %s', err)
return False
+
+
+class TransmissionRPC(object):
+
+ """TransmissionRPC lite library"""
+
+ def __init__(self, host = 'localhost', port = 9091, username = None, password = None):
+
+ super(TransmissionRPC, self).__init__()
+
+ self.url = 'http://' + host + ':' + str(port) + '/transmission/rpc'
+ self.tag = 0
+ self.session_id = 0
+ self.session = {}
+ if username and password:
+ password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
+ password_manager.add_password(realm = None, uri = self.url, user = username, passwd = password)
+ opener = urllib2.build_opener(urllib2.HTTPBasicAuthHandler(password_manager), urllib2.HTTPDigestAuthHandler(password_manager))
+ opener.addheaders = [('User-agent', 'couchpotato-transmission-client/1.0')]
+ urllib2.install_opener(opener)
+ elif username or password:
+ log.debug('User or password missing, not using authentication.')
+ self.session = self.get_session()
+
+ def _request(self, ojson):
+ self.tag += 1
+ headers = {'x-transmission-session-id': str(self.session_id)}
+ request = urllib2.Request(self.url, json.dumps(ojson).encode('utf-8'), headers)
+ try:
+ open_request = urllib2.urlopen(request)
+ response = json.loads(open_request.read())
+ log.debug('response: %s', json.dumps(response))
+ if response['result'] == 'success':
+ log.debug('Transmission action successfull')
+ return response['arguments']
+ else:
+ log.debug('Unknown failure sending command to Transmission. Return text is: %s', response['result'])
+ return False
+ except httplib.InvalidURL, err:
+ log.error('Invalid Transmission host, check your config %s', err)
+ return False
+ except urllib2.HTTPError, err:
+ if err.code == 401:
+ log.error('Invalid Transmission Username or Password, check your config')
+ return False
+ elif err.code == 409:
+ msg = str(err.read())
+ try:
+ self.session_id = \
+ re.search('X-Transmission-Session-Id:\s*(\w+)', msg).group(1)
+ log.debug('X-Transmission-Session-Id: %s', self.session_id)
+
+ # #resend request with the updated header
+
+ return self._request(ojson)
+ except:
+ log.error('Unable to get Transmission Session-Id %s', err)
+ else:
+ log.error('TransmissionRPC HTTPError: %s', err)
+ except urllib2.URLError, err:
+ log.error('Unable to connect to Transmission %s', err)
+
+ def get_session(self):
+ post_data = {'method': 'session-get', 'tag': self.tag}
+ return self._request(post_data)
+
+ def add_torrent_uri(self, torrent, arguments):
+ arguments['filename'] = torrent
+ post_data = {'arguments': arguments, 'method': 'torrent-add', 'tag': self.tag}
+ return self._request(post_data)
+
+ def add_torrent_file(self, torrent, arguments):
+ arguments['metainfo'] = torrent
+ post_data = {'arguments': arguments, 'method': 'torrent-add', 'tag': self.tag}
+ return self._request(post_data)
+
+ def set_torrent(self, torrent_id, arguments):
+ arguments['ids'] = torrent_id
+ post_data = {'arguments': arguments, 'method': 'torrent-set', 'tag': self.tag}
+ return self._request(post_data)
diff --git a/couchpotato/core/event.py b/couchpotato/core/event.py
index 82f81e21..8319150a 100644
--- a/couchpotato/core/event.py
+++ b/couchpotato/core/event.py
@@ -12,7 +12,7 @@ def runHandler(name, handler, *args, **kwargs):
return handler(*args, **kwargs)
except:
from couchpotato.environment import Env
- log.error('Error in event "%s", that wasn\'t caught: %s%s' % (name, traceback.format_exc(), Env.all()))
+ log.error('Error in event "%s", that wasn\'t caught: %s%s', (name, traceback.format_exc(), Env.all()))
def addEvent(name, handler, priority = 100):
@@ -43,7 +43,7 @@ def removeEvent(name, handler):
def fireEvent(name, *args, **kwargs):
if not events.get(name): return
- #log.debug('Firing event %s' % name)
+ #log.debug('Firing event %s', name)
try:
# Fire after event
@@ -53,6 +53,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:
@@ -93,7 +100,7 @@ def fireEvent(name, *args, **kwargs):
elif r[1]:
errorHandler(r[1])
else:
- log.debug('Assume disabled eventhandler for: %s' % name)
+ log.debug('Assume disabled eventhandler for: %s', name)
else:
results = []
@@ -123,30 +130,29 @@ def fireEvent(name, *args, **kwargs):
modified_results = fireEvent('result.modify.%s' % name, results, single = True)
if modified_results:
- log.debug('Return modified results for %s' % name)
+ log.debug('Return modified results for %s', name)
results = modified_results
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()))
+ 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
diff --git a/couchpotato/core/helpers/encoding.py b/couchpotato/core/helpers/encoding.py
index a9f09edd..9b1f575c 100644
--- a/couchpotato/core/helpers/encoding.py
+++ b/couchpotato/core/helpers/encoding.py
@@ -31,10 +31,14 @@ def toUnicode(original, *args):
except:
raise
except UnicodeDecodeError:
- log.error('Unable to decode value: %s... ' % repr(original)[:20])
+ log.error('Unable to decode value: %s... ', repr(original)[:20])
ascii_text = str(original).encode('string_escape')
return toUnicode(ascii_text)
+def ss(original, *args):
+ from couchpotato.environment import Env
+ return toUnicode(original, *args).encode(Env.get('encoding'))
+
def ek(original, *args):
if isinstance(original, (str, unicode)):
try:
diff --git a/couchpotato/core/helpers/request.py b/couchpotato/core/helpers/request.py
index 8bedf6c4..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
@@ -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
@@ -70,9 +70,13 @@ 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)
+ content = padded_jsonify(callback, *args, **kwargs)
else:
- return jsonify('text/javascript' if Env.doDebug() else '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/helpers/rss.py b/couchpotato/core/helpers/rss.py
index 582d38ac..d88fdb53 100644
--- a/couchpotato/core/helpers/rss.py
+++ b/couchpotato/core/helpers/rss.py
@@ -47,5 +47,5 @@ class RSS(object):
try:
return XMLTree.parse(data).findall(path)
except Exception, e:
- log.error('Error parsing RSS. %s' % e)
+ log.error('Error parsing RSS. %s', e)
return []
diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py
index 29e4bf4d..1f1d3ba2 100644
--- a/couchpotato/core/helpers/variable.py
+++ b/couchpotato/core/helpers/variable.py
@@ -2,7 +2,10 @@ from couchpotato.core.logger import CPLog
import hashlib
import os.path
import platform
+import random
import re
+import string
+import sys
log = CPLog(__name__)
@@ -20,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')
@@ -77,9 +84,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()
@@ -94,7 +101,7 @@ def getImdb(txt):
def tryInt(s):
try: return int(s)
- except: return s
+ except: return 0
def tryFloat(s):
try: return float(s) if '.' in s else tryInt(s)
@@ -111,9 +118,12 @@ def getTitle(library_dict):
try:
return library_dict['titles'][0]['title']
except:
- log.error('Could not get title for %s' % library_dict['identifier'])
+ log.error('Could not get title for %s', library_dict['identifier'])
return None
except:
- log.error('Could not get title for library item: %s' % 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/loader.py b/couchpotato/core/loader.py
index 957a6997..30395195 100644
--- a/couchpotato/core/loader.py
+++ b/couchpotato/core/loader.py
@@ -45,7 +45,7 @@ class Loader(object):
try:
m = getattr(self.loadModule(module_name), plugin.get('name'))
- log.info("Loading %s: %s" % (plugin['type'], plugin['name']))
+ log.info('Loading %s: %s', (plugin['type'], plugin['name']))
# Save default settings for plugin/provider
did_save += self.loadSettings(m, module_name, save = False)
@@ -57,10 +57,10 @@ class Loader(object):
log.error(e.message)
pass
# todo:: this needs to be more descriptive.
- log.error('Import error, remove the empty folder: %s' % plugin.get('module'))
- log.debug('Can\'t import %s: %s' % (module_name, traceback.format_exc()))
+ log.error('Import error, remove the empty folder: %s', plugin.get('module'))
+ log.debug('Can\'t import %s: %s', (module_name, traceback.format_exc()))
except:
- log.error('Can\'t import %s: %s' % (module_name, traceback.format_exc()))
+ log.error('Can\'t import %s: %s', (module_name, traceback.format_exc()))
if did_save:
fireEvent('settings.save')
@@ -84,7 +84,7 @@ class Loader(object):
fireEvent('settings.register', section_name = section['name'], options = options, save = save)
return True
except:
- log.debug("Failed loading settings for '%s': %s" % (name, traceback.format_exc()))
+ log.debug('Failed loading settings for "%s": %s', (name, traceback.format_exc()))
return False
def loadPlugins(self, module, name):
@@ -97,7 +97,7 @@ class Loader(object):
return True
except Exception, e:
- log.error("Failed loading plugin '%s': %s" % (module.__file__, traceback.format_exc()))
+ log.error('Failed loading plugin "%s": %s', (module.__file__, traceback.format_exc()))
return False
def addModule(self, priority, plugin_type, module, name):
diff --git a/couchpotato/core/logger.py b/couchpotato/core/logger.py
index 2c539060..8d8c9f59 100644
--- a/couchpotato/core/logger.py
+++ b/couchpotato/core/logger.py
@@ -1,5 +1,6 @@
import logging
import re
+import traceback
class CPLog(object):
@@ -13,31 +14,42 @@ class CPLog(object):
self.context = context
self.logger = logging.getLogger()
- def info(self, msg):
- self.logger.info(self.addContext(msg))
+ def info(self, msg, replace_tuple = ()):
+ self.logger.info(self.addContext(msg, replace_tuple))
- def debug(self, msg):
- self.logger.debug(self.addContext(msg))
+ def debug(self, msg, replace_tuple = ()):
+ self.logger.debug(self.addContext(msg, replace_tuple))
- def error(self, msg):
- self.logger.error(self.addContext(msg))
+ def error(self, msg, replace_tuple = ()):
+ self.logger.error(self.addContext(msg, replace_tuple))
- def warning(self, msg):
- self.logger.warning(self.addContext(msg))
+ def warning(self, msg, replace_tuple = ()):
+ self.logger.warning(self.addContext(msg, replace_tuple))
- def critical(self, msg):
- self.logger.critical(self.addContext(msg), exc_info = 1)
+ def critical(self, msg, replace_tuple = ()):
+ self.logger.critical(self.addContext(msg, replace_tuple), exc_info = 1)
- def addContext(self, msg):
- return '[%+25.25s] %s' % (self.context[-25:], self.removePrivateData(msg))
+ def addContext(self, msg, replace_tuple = ()):
+ return '[%+25.25s] %s' % (self.context[-25:], self.safeMessage(msg, replace_tuple))
- def removePrivateData(self, msg):
- try:
- msg = unicode(msg)
- except:
- pass
+ def safeMessage(self, msg, replace_tuple = ()):
from couchpotato.environment import Env
+ from couchpotato.core.helpers.encoding import ss
+
+ msg = ss(msg)
+
+ try:
+ msg = msg % replace_tuple
+ except:
+ try:
+ if isinstance(replace_tuple, tuple):
+ msg = msg % tuple([ss(x) for x in list(replace_tuple)])
+ else:
+ msg = msg % ss(replace_tuple)
+ except:
+ self.logger.error(u'Failed encoding stuff to log: %s' % traceback.format_exc())
+
if not Env.get('dev'):
for replace in self.replace_private:
diff --git a/couchpotato/core/notifications/base.py b/couchpotato/core/notifications/base.py
index 2663c333..c1c63503 100644
--- a/couchpotato/core/notifications/base.py
+++ b/couchpotato/core/notifications/base.py
@@ -13,7 +13,10 @@ class Notification(Plugin):
default_title = Env.get('appname')
test_message = 'ZOMG Lazors Pewpewpew!'
- listen_to = ['movie.downloaded', 'movie.snatched', 'updater.available']
+ listen_to = [
+ 'movie.downloaded', 'movie.snatched',
+ 'updater.available', 'updater.updated',
+ ]
dont_listen_to = []
def __init__(self):
@@ -41,7 +44,7 @@ class Notification(Plugin):
test_type = self.testNotifyName()
- log.info('Sending test to %s' % test_type)
+ log.info('Sending test to %s', test_type)
success = self.notify(
message = self.test_message,
diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py
index 10a1401d..e0af50de 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.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
from couchpotato.core.helpers.variable import tryInt
@@ -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.Lock()
messages = []
+ listeners = []
+
listen_to = [
'movie.downloaded', 'movie.snatched',
'updater.available', 'updater.updated',
@@ -30,7 +35,7 @@ class CoreNotifier(Notification):
addApiView('notification.markread', self.markAsRead, docs = {
'desc': 'Mark notifications as read',
'params': {
- 'ids': {'desc': 'Notification id you want to mark as read.', 'type': 'int (comma separated)'},
+ 'ids': {'desc': 'Notification id you want to mark as read. All if ids is empty.', 'type': 'int (comma separated)'},
},
})
@@ -46,22 +51,31 @@ class CoreNotifier(Notification):
}"""}
})
+ addNonBlockApiView('notification.listener', (self.addListener, self.removeListener))
addApiView('notification.listener', self.listener)
- self.registerEvents()
+ addEvent('app.load', self.clean)
- def registerEvents(self):
+ def clean(self):
+
+ db = get_session()
+ db.query(Notif).filter(Notif.added <= (int(time.time()) - 2419200)).delete()
+ db.commit()
- # 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(',')]
+
+ ids = None
+ if getParam('ids'):
+ ids = [x.strip() for x in getParam('ids').split(',')]
db = get_session()
- q = db.query(Notif) \
- .filter(or_(*[Notif.id == tryInt(s) for s in ids]))
+ if ids:
+ q = db.query(Notif).filter(or_(*[Notif.id == tryInt(s) for s in ids]))
+ else:
+ q = db.query(Notif).filter_by(read = False)
+
q.update({Notif.read: True})
db.commit()
@@ -83,6 +97,8 @@ class CoreNotifier(Notification):
limit = splt[0]
offset = 0 if len(splt) is 1 else splt[1]
q = q.limit(limit).offset(offset)
+ else:
+ q = q.limit(200)
results = q.all()
notifications = []
@@ -114,25 +130,86 @@ 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({
+ def frontend(self, type = 'notification', data = {}, message = None):
+
+ self.m_lock.acquire()
+ notification = {
+ 'message_id': str(uuid.uuid4()),
'time': time.time(),
'type': type,
'data': data,
- })
+ 'message': message,
+ }
+ self.messages.append(notification)
+
+ while len(self.listeners) > 0 and not self.shuttingDown():
+ try:
+ listener, last_id = self.listeners.pop()
+ listener({
+ 'success': True,
+ 'result': [notification],
+ })
+ 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):
+ 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]["message_id"] == last_id: break
+ recent = self.messages[index:]
+
+ 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'):
@@ -146,9 +223,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 371b95ed..595a39ed 100644
--- a/couchpotato/core/notifications/core/static/notification.js
+++ b/couchpotato/core/notifications/core/static/notification.js
@@ -8,9 +8,10 @@ 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('reload', self.startInterval.bind(self, [true]));
App.addEvent('notification', self.notify.bind(self));
+ App.addEvent('message', self.showMessage.bind(self));
// Add test buttons to settings page
App.addEvent('load', self.addTestButtons.bind(self));
@@ -30,7 +31,11 @@ var NotificationBase = new Class({
'href': App.createUrl('notifications'),
'text': 'Show older notifications'
})); */
- })
+ });
+
+ window.addEvent('load', function(){
+ self.startInterval.delay(Browser.safari ? 100 : 0, self)
+ });
},
@@ -73,9 +78,6 @@ var NotificationBase = new Class({
if(ids.length > 0)
Api.request('notification.markread', {
- 'data': {
- 'ids': ids.join(',')
- },
'onSuccess': function(){
self.setBadge('')
}
@@ -83,39 +85,88 @@ var NotificationBase = new Class({
},
- startInterval: function(){
+ startInterval: function(force){
var self = this;
- self.request = Api.request('notification.listener', {
- 'initialDelay': 100,
- 'delay': 3000,
+ if(self.stopped && !force){
+ self.stopped = false;
+ return;
+ }
+
+ 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);
+ if(result.message && result.read === undefined)
+ self.showMessage(result.message);
+ })
+
+ if(json.result.length > 0)
+ self.last_id = json.result.getLast().message_id
+ }
+
+ // Restart poll
+ self.startPoll()
},
+ showMessage: function(message){
+ var self = this;
+
+ if(!self.message_container)
+ self.message_container = new Element('div.messages').inject(document.body);
+
+ var new_message = new Element('div.message', {
+ 'text': message
+ }).inject(self.message_container);
+
+ setTimeout(function(){
+ new_message.addClass('show')
+ }, 10);
+
+ setTimeout(function(){
+ new_message.addClass('hide')
+ setTimeout(function(){
+ new_message.destroy();
+ }, 1000);
+ }, 4000);
+
+ },
+
+ // Notification setting tests
addTestButtons: function(){
var self = this;
diff --git a/couchpotato/core/notifications/growl/main.py b/couchpotato/core/notifications/growl/main.py
index b98888e3..4aa1c312 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
@@ -39,7 +38,7 @@ class Growl(Notification):
self.growl.register()
self.registered = True
except:
- log.error('Failed register of growl: %s' % traceback.format_exc())
+ log.error('Failed register of growl: %s', traceback.format_exc())
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
diff --git a/couchpotato/core/notifications/history/__init__.py b/couchpotato/core/notifications/history/__init__.py
deleted file mode 100644
index f4c53efa..00000000
--- a/couchpotato/core/notifications/history/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from .main import History
-
-def start():
- return History()
-
-config = []
diff --git a/couchpotato/core/notifications/history/main.py b/couchpotato/core/notifications/history/main.py
deleted file mode 100644
index cdd46a41..00000000
--- a/couchpotato/core/notifications/history/main.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from couchpotato import get_session
-from couchpotato.core.helpers.encoding import toUnicode
-from couchpotato.core.logger import CPLog
-from couchpotato.core.notifications.base import Notification
-from couchpotato.core.settings.model import History as Hist
-import time
-
-log = CPLog(__name__)
-
-
-class History(Notification):
-
- listen_to = ['movie.downloaded', 'movie.snatched', 'renamer.canceled']
-
- def notify(self, message = '', data = {}, listener = None):
-
- db = get_session()
- history = Hist(
- added = int(time.time()),
- message = toUnicode(message),
- release_id = data.get('id', 0)
- )
- db.add(history)
- db.commit()
- #db.close()
-
- return True
diff --git a/couchpotato/core/notifications/nmj/main.py b/couchpotato/core/notifications/nmj/main.py
index 89aa8729..8f0e201d 100644
--- a/couchpotato/core/notifications/nmj/main.py
+++ b/couchpotato/core/notifications/nmj/main.py
@@ -33,10 +33,10 @@ class NMJ(Notification):
try:
terminal = telnetlib.Telnet(host)
except Exception:
- log.error('Warning: unable to get a telnet session to %s' % (host))
+ log.error('Warning: unable to get a telnet session to %s', (host))
return self.failed()
- log.debug('Connected to %s via telnet' % (host))
+ log.debug('Connected to %s via telnet', (host))
terminal.read_until('sh-3.00# ')
terminal.write('cat /tmp/source\n')
terminal.write('cat /tmp/netshare\n')
@@ -48,9 +48,9 @@ class NMJ(Notification):
if match:
database = match.group(1)
device = match.group(2)
- log.info('Found NMJ database %s on device %s' % (database, device))
+ log.info('Found NMJ database %s on device %s', (database, device))
else:
- log.error('Could not get current NMJ database on %s, NMJ is probably not running!' % (host))
+ log.error('Could not get current NMJ database on %s, NMJ is probably not running!', (host))
return self.failed()
if device.startswith('NETWORK_SHARE/'):
@@ -58,7 +58,7 @@ class NMJ(Notification):
if match:
mount = match.group().replace('127.0.0.1', host)
- log.info('Found mounting url on the Popcorn Hour in configuration: %s' % (mount))
+ log.info('Found mounting url on the Popcorn Hour in configuration: %s', (mount))
else:
log.error('Detected a network share on the Popcorn Hour, but could not get the mounting url')
return self.failed()
@@ -77,7 +77,7 @@ class NMJ(Notification):
database = self.conf('database')
if self.mount:
- log.debug('Try to mount network drive via url: %s' % (mount))
+ log.debug('Try to mount network drive via url: %s', (mount))
try:
data = self.urlopen(mount)
except:
@@ -102,11 +102,11 @@ class NMJ(Notification):
et = etree.fromstring(response)
result = et.findtext('returnValue')
except SyntaxError, e:
- log.error('Unable to parse XML returned from the Popcorn Hour: %s' % (e))
+ log.error('Unable to parse XML returned from the Popcorn Hour: %s', (e))
return False
if int(result) > 0:
- log.error('Popcorn Hour returned an errorcode: %s' % (result))
+ log.error('Popcorn Hour returned an errorcode: %s', (result))
return False
else:
log.info('NMJ started background scan')
diff --git a/couchpotato/core/notifications/notifo/main.py b/couchpotato/core/notifications/notifo/main.py
index e372f28c..64eee5ec 100644
--- a/couchpotato/core/notifications/notifo/main.py
+++ b/couchpotato/core/notifications/notifo/main.py
@@ -32,7 +32,7 @@ class Notifo(Notification):
raise Exception
except:
- log.error('Notification failed: %s' % traceback.format_exc())
+ log.error('Notification failed: %s', traceback.format_exc())
return False
log.info('Notifo notification successful.')
diff --git a/couchpotato/core/notifications/notifymyandroid/main.py b/couchpotato/core/notifications/notifymyandroid/main.py
index 195278e8..a6a316da 100644
--- a/couchpotato/core/notifications/notifymyandroid/main.py
+++ b/couchpotato/core/notifications/notifymyandroid/main.py
@@ -23,6 +23,6 @@ class NotifyMyAndroid(Notification):
for key in keys:
if not response[str(key)]['code'] == u'200':
- log.error('Could not send notification to NotifyMyAndroid (%s). %s' % (key, response[key]['message']))
+ log.error('Could not send notification to NotifyMyAndroid (%s). %s', (key, response[key]['message']))
return response
diff --git a/couchpotato/core/notifications/notifymywp/main.py b/couchpotato/core/notifications/notifymywp/main.py
index 17252c1a..fafffd55 100644
--- a/couchpotato/core/notifications/notifymywp/main.py
+++ b/couchpotato/core/notifications/notifymywp/main.py
@@ -17,7 +17,7 @@ class NotifyMyWP(Notification):
for key in keys:
if not response[key]['Code'] == u'200':
- log.error('Could not send notification to NotifyMyWindowsPhone (%s). %s' % (key, response[key]['message']))
+ log.error('Could not send notification to NotifyMyWindowsPhone (%s). %s', (key, response[key]['message']))
return False
return response
diff --git a/couchpotato/core/notifications/plex/__init__.py b/couchpotato/core/notifications/plex/__init__.py
index 6714aba9..f908cbb2 100644
--- a/couchpotato/core/notifications/plex/__init__.py
+++ b/couchpotato/core/notifications/plex/__init__.py
@@ -17,8 +17,9 @@ config = [{
},
{
'name': 'host',
- 'default': 'localhost:32400',
- 'description': 'Default should be on localhost:32400',
+ 'default': 'localhost',
+ 'description': 'Default should be on localhost',
+ 'advanced': True,
},
],
}
diff --git a/couchpotato/core/notifications/plex/main.py b/couchpotato/core/notifications/plex/main.py
index 72fdef17..23fd39df 100644
--- a/couchpotato/core/notifications/plex/main.py
+++ b/couchpotato/core/notifications/plex/main.py
@@ -1,4 +1,5 @@
from couchpotato.core.event import addEvent
+from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
@@ -11,13 +12,14 @@ log = CPLog(__name__)
class Plex(Notification):
def __init__(self):
+ super(Plex, self).__init__()
addEvent('renamer.after', self.addToLibrary)
def addToLibrary(self, group = {}):
if self.isDisabled(): return
log.info('Sending notification to Plex')
- hosts = [cleanHost(x.strip()) for x in self.conf('host').split(",")]
+ hosts = [cleanHost(x.strip() + ':32400') for x in self.conf('host').split(",")]
for host in hosts:
@@ -36,7 +38,33 @@ class Plex(Notification):
x = self.urlopen(url)
except:
- log.error('Plex library update failed for %s: %s' % (host, traceback.format_exc()))
+ log.error('Plex library update failed for %s: %s', (host, traceback.format_exc()))
return False
return True
+
+ def notify(self, message = '', data = {}, listener = None):
+ if self.isDisabled(): return
+
+ hosts = [x.strip() + ':3000' for x in self.conf('host').split(",")]
+ successful = 0
+ for host in hosts:
+ if self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host):
+ successful += 1
+
+ return successful == len(hosts)
+
+ def send(self, command, host):
+
+ url = 'http://%s/xbmcCmds/xbmcHttp/?%s' % (host, tryUrlencode(command))
+
+ headers = {}
+
+ try:
+ self.urlopen(url, headers = headers, show_error = False)
+ except:
+ log.error("Couldn't sent command to Plex: %s", traceback.format_exc())
+ return False
+
+ log.info('Plex notification to %s successful.', host)
+ return True
diff --git a/couchpotato/core/notifications/prowl/main.py b/couchpotato/core/notifications/prowl/main.py
index 5f24d4e6..715965db 100644
--- a/couchpotato/core/notifications/prowl/main.py
+++ b/couchpotato/core/notifications/prowl/main.py
@@ -32,7 +32,7 @@ class Prowl(Notification):
log.info('Prowl notifications sent.')
return True
elif request_status == 401:
- log.error('Prowl auth failed: %s' % response.reason)
+ log.error('Prowl auth failed: %s', response.reason)
return False
else:
log.error('Prowl notification failed.')
diff --git a/couchpotato/core/notifications/pushover/main.py b/couchpotato/core/notifications/pushover/main.py
index be99df12..bcd3245c 100644
--- a/couchpotato/core/notifications/pushover/main.py
+++ b/couchpotato/core/notifications/pushover/main.py
@@ -35,7 +35,7 @@ class Pushover(Notification):
log.info('Pushover notifications sent.')
return True
elif request_status == 401:
- log.error('Pushover auth failed: %s' % response.reason)
+ log.error('Pushover auth failed: %s', response.reason)
return False
else:
log.error('Pushover notification failed.')
diff --git a/couchpotato/core/notifications/synoindex/main.py b/couchpotato/core/notifications/synoindex/main.py
index 9d06b781..93d6cdbd 100644
--- a/couchpotato/core/notifications/synoindex/main.py
+++ b/couchpotato/core/notifications/synoindex/main.py
@@ -15,14 +15,14 @@ class Synoindex(Notification):
if self.isDisabled(): return
command = ['/usr/syno/bin/synoindex', '-A', group.get('destination_dir')]
- log.info(u'Executing synoindex command: %s ' % command)
+ log.info(u'Executing synoindex command: %s ', command)
try:
p = subprocess.Popen(command, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
out = p.communicate()
- log.info('Result from synoindex: %s' % str(out))
+ log.info('Result from synoindex: %s', str(out))
return True
except OSError, e:
- log.error('Unable to run synoindex: %s' % e)
+ log.error('Unable to run synoindex: %s', e)
return False
return True
diff --git a/couchpotato/core/notifications/twitter/main.py b/couchpotato/core/notifications/twitter/main.py
index d800369d..a5c16a73 100644
--- a/couchpotato/core/notifications/twitter/main.py
+++ b/couchpotato/core/notifications/twitter/main.py
@@ -55,7 +55,7 @@ class Twitter(Notification):
else:
api.PostUpdate('[%s] %s' % (self.default_title, message))
except Exception, e:
- log.error('Error sending tweet: %s' % e)
+ log.error('Error sending tweet: %s', e)
return False
return True
@@ -71,7 +71,7 @@ class Twitter(Notification):
resp, content = oauth_client.request(self.urls['request'], 'POST', body = tryUrlencode({'oauth_callback': callback_url}))
if resp['status'] != '200':
- log.error('Invalid response from Twitter requesting temp token: %s' % resp['status'])
+ log.error('Invalid response from Twitter requesting temp token: %s', resp['status'])
return jsonified({
'success': False,
})
@@ -80,7 +80,7 @@ class Twitter(Notification):
auth_url = self.urls['authorize'] + ("?oauth_token=%s" % self.request_token['oauth_token'])
- log.info('Redirecting to "%s"' % auth_url)
+ log.info('Redirecting to "%s"', auth_url)
return jsonified({
'success': True,
'url': auth_url,
@@ -100,10 +100,10 @@ class Twitter(Notification):
access_token = dict(parse_qsl(content))
if resp['status'] != '200':
- log.error('The request for an access token did not succeed: %s' % resp['status'])
+ log.error('The request for an access token did not succeed: %s', resp['status'])
return 'Twitter auth failed'
else:
- log.debug('Tokens: %s, %s' % (access_token['oauth_token'], access_token['oauth_token_secret']))
+ log.debug('Tokens: %s, %s', (access_token['oauth_token'], access_token['oauth_token_secret']))
self.conf('access_token_key', value = access_token['oauth_token'])
self.conf('access_token_secret', value = access_token['oauth_token_secret'])
diff --git a/couchpotato/core/notifications/xbmc/main.py b/couchpotato/core/notifications/xbmc/main.py
index eff591ad..4f6e62a7 100644
--- a/couchpotato/core/notifications/xbmc/main.py
+++ b/couchpotato/core/notifications/xbmc/main.py
@@ -35,5 +35,5 @@ class XBMC(Notification):
log.error("Couldn't sent command to XBMC")
return False
- log.info('XBMC notification to %s successful.' % host)
+ log.info('XBMC notification to %s successful.', host)
return True
diff --git a/couchpotato/core/plugins/automation/__init__.py b/couchpotato/core/plugins/automation/__init__.py
index 2eac7dac..75e0d28f 100644
--- a/couchpotato/core/plugins/automation/__init__.py
+++ b/couchpotato/core/plugins/automation/__init__.py
@@ -25,7 +25,7 @@ config = [{
},
{
'name': 'rating',
- 'default': 6.0,
+ 'default': 7.0,
'type': 'float',
},
{
diff --git a/couchpotato/core/plugins/automation/main.py b/couchpotato/core/plugins/automation/main.py
index a29d8810..12571e4f 100644
--- a/couchpotato/core/plugins/automation/main.py
+++ b/couchpotato/core/plugins/automation/main.py
@@ -19,4 +19,8 @@ class Automation(Plugin):
movies = fireEvent('automation.get_movies', merge = True)
for imdb_id in movies:
- fireEvent('movie.add', params = {'identifier': imdb_id}, force_readd = False)
+ prop_name = 'automation.added.%s' % imdb_id
+ added = Env.prop(prop_name, default = False)
+ if not added:
+ fireEvent('movie.add', params = {'identifier': imdb_id}, force_readd = False)
+ Env.prop(prop_name, True)
diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py
index 998097d8..3d1682cb 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, ss
from couchpotato.core.helpers.variable import getExt
from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
@@ -71,6 +71,7 @@ class Plugin(object):
return send_from_directory(d, filename)
def createFile(self, path, content, binary = False):
+ path = ss(path)
self.makeDir(os.path.dirname(path))
@@ -80,20 +81,21 @@ class Plugin(object):
f.close()
os.chmod(path, Env.getPermission('file'))
except Exception, e:
- log.error('Unable writing to file "%s": %s' % (path, e))
+ log.error('Unable writing to file "%s": %s', (path, e))
def makeDir(self, path):
+ path = ss(path)
try:
if not os.path.isdir(path):
os.makedirs(path, Env.getPermission('folder'))
return True
except Exception, e:
- log.error('Unable to create folder "%s": %s' % (path, e))
+ log.error('Unable to create folder "%s": %s', (path, e))
return False
# http request
- def urlopen(self, url, timeout = 10, params = {}, headers = {}, multipart = False, show_error = True):
+ def urlopen(self, url, timeout = 30, params = {}, headers = {}, opener = None, multipart = False, show_error = True):
# Fill in some headers
if not headers.get('Referer'):
@@ -106,7 +108,7 @@ class Plugin(object):
# Don't try for failed requests
if self.http_failed_disabled.get(host, 0) > 0:
if self.http_failed_disabled[host] > (time.time() - 900):
- log.info('Disabled calls to %s for 15 minutes because so many failed requests.' % host)
+ log.info('Disabled calls to %s for 15 minutes because so many failed requests.', host)
raise Exception
else:
del self.http_failed_request[host]
@@ -116,7 +118,7 @@ class Plugin(object):
try:
if multipart:
- log.info('Opening multipart url: %s, params: %s' % (url, [x for x in params.iterkeys()]))
+ log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()]))
request = urllib2.Request(url, params, headers)
cookies = cookielib.CookieJar()
@@ -124,16 +126,19 @@ class Plugin(object):
data = opener.open(request, timeout = timeout).read()
else:
- log.info('Opening url: %s, params: %s' % (url, [x for x in params.iterkeys()]))
+ log.info('Opening url: %s, params: %s', (url, [x for x in params.iterkeys()]))
data = tryUrlencode(params) if len(params) > 0 else None
request = urllib2.Request(url, data, headers)
- data = urllib2.urlopen(request, timeout = timeout).read()
+ if opener:
+ data = opener.open(request, timeout = timeout).read()
+ else:
+ data = urllib2.urlopen(request, timeout = timeout).read()
self.http_failed_request[host] = 0
except IOError:
if show_error:
- log.error('Failed opening url in %s: %s %s' % (self.getName(), url, traceback.format_exc(1)))
+ log.error('Failed opening url in %s: %s %s', (self.getName(), url, traceback.format_exc(1)))
# Save failed requests by hosts
try:
@@ -147,7 +152,7 @@ class Plugin(object):
self.http_failed_disabled[host] = time.time()
except:
- log.debug('Failed logging failed requests for %s: %s' % (url, traceback.format_exc()))
+ log.debug('Failed logging failed requests for %s: %s', (url, traceback.format_exc()))
raise
@@ -163,7 +168,7 @@ class Plugin(object):
wait = math.ceil(last_use - now + self.http_time_between_calls)
if wait > 0:
- log.debug('Waiting for %s, %d seconds' % (self.getName(), wait))
+ log.debug('Waiting for %s, %d seconds', (self.getName(), wait))
time.sleep(last_use - now + self.http_time_between_calls)
def beforeCall(self, handler):
@@ -199,9 +204,10 @@ 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)
+ if not Env.get('dev'): log.debug('Getting cache %s', cache_key)
return cache
if url:
@@ -213,13 +219,15 @@ class Plugin(object):
del kwargs['cache_timeout']
data = self.urlopen(url, **kwargs)
- self.setCache(cache_key, data, timeout = cache_timeout)
+
+ if data:
+ self.setCache(cache_key, data, timeout = cache_timeout)
return data
except:
pass
def setCache(self, cache_key, value, timeout = 300):
- log.debug('Setting cache %s' % cache_key)
+ log.debug('Setting cache %s', cache_key)
Env.get('cache').set(cache_key, value, timeout)
return value
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/core/plugins/file/main.py b/couchpotato/core/plugins/file/main.py
index 125748fb..5f381198 100644
--- a/couchpotato/core/plugins/file/main.py
+++ b/couchpotato/core/plugins/file/main.py
@@ -8,6 +8,7 @@ from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import FileType, File
from couchpotato.environment import Env
import os.path
+import traceback
log = CPLog(__name__)
@@ -46,6 +47,7 @@ class FileManager(Plugin):
try:
filedata = self.urlopen(url, **urlopen_kwargs)
except:
+ log.error('Failed downloading file %s: %s', (url, traceback.format_exc()))
return False
self.createFile(dest, filedata, binary = True)
diff --git a/couchpotato/core/plugins/library/main.py b/couchpotato/core/plugins/library/main.py
index 4cdc323f..d24dfca6 100644
--- a/couchpotato/core/plugins/library/main.py
+++ b/couchpotato/core/plugins/library/main.py
@@ -78,7 +78,7 @@ class LibraryPlugin(Plugin):
except: pass
if not info or len(info) == 0:
- log.error('Could not update, no movie info to work with: %s' % identifier)
+ log.error('Could not update, no movie info to work with: %s', identifier)
return False
# Main info
@@ -95,7 +95,7 @@ class LibraryPlugin(Plugin):
db.commit()
titles = info.get('titles', [])
- log.debug('Adding titles: %s' % titles)
+ log.debug('Adding titles: %s', titles)
for title in titles:
if not title:
continue
@@ -117,26 +117,29 @@ class LibraryPlugin(Plugin):
continue
file_path = fireEvent('file.download', url = image, single = True)
- file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', type), single = True)
- try:
- file_obj = db.query(File).filter_by(id = file_obj.get('id')).one()
- library.files.append(file_obj)
- db.commit()
- except:
- log.debug('Failed to attach to library: %s' % traceback.format_exc())
+ if file_path:
+ file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', type), single = True)
+ try:
+ file_obj = db.query(File).filter_by(id = file_obj.get('id')).one()
+ library.files.append(file_obj)
+ db.commit()
+ except:
+ log.debug('Failed to attach to library: %s', traceback.format_exc())
library_dict = library.to_dict(self.default_dict)
- fireEvent('library.update_finish', data = library_dict)
-
- #db.close()
return library_dict
def updateReleaseDate(self, identifier):
db = get_session()
library = db.query(Library).filter_by(identifier = identifier).first()
- dates = library.info.get('release_date')
+
+ if not library.info:
+ library_dict = self.update(identifier)
+ dates = library_dict.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)
diff --git a/couchpotato/core/plugins/log/main.py b/couchpotato/core/plugins/log/main.py
index a29f1e12..e6ff1133 100644
--- a/couchpotato/core/plugins/log/main.py
+++ b/couchpotato/core/plugins/log/main.py
@@ -1,5 +1,7 @@
from couchpotato.api import addApiView
+from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParam, getParams
+from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
@@ -71,14 +73,14 @@ class Logging(Plugin):
return jsonified({
'success': True,
- 'log': log,
+ 'log': toUnicode(log),
'total': total,
})
def partial(self):
log_type = getParam('type', 'all')
- total_lines = getParam('lines', 30)
+ total_lines = tryInt(getParam('lines', 30))
log_lines = []
@@ -92,13 +94,12 @@ class Logging(Plugin):
reversed_lines = []
f = open(path, 'r')
- reversed_lines = f.read().split('[0m\n')
+ reversed_lines = toUnicode(f.read()).split('[0m\n')
reversed_lines.reverse()
brk = False
for line in reversed_lines:
- #print '%s ' % log_type in line.lower()
if log_type == 'all' or '%s ' % log_type.upper() in line:
log_lines.append(line)
@@ -149,7 +150,7 @@ class Logging(Plugin):
except:
log.error(log_message)
except:
- log.error('Couldn\'t log via API: %s' % params)
+ log.error('Couldn\'t log via API: %s', params)
return jsonified({
diff --git a/couchpotato/core/plugins/log/static/log.css b/couchpotato/core/plugins/log/static/log.css
index ec1f838c..222b8efa 100644
--- a/couchpotato/core/plugins/log/static/log.css
+++ b/couchpotato/core/plugins/log/static/log.css
@@ -59,6 +59,7 @@
width: 14%;
color: lightgrey;
padding: 3px 0;
+ font-size: 10px;
}
.page.log .container .time:last-child { display: none; }
diff --git a/couchpotato/core/plugins/log/static/log.js b/couchpotato/core/plugins/log/static/log.js
index 1668ded6..0e276b58 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,11 +78,16 @@ Page.Log = new Class({
addColors: function(text){
var self = this;
- text = text.replace(/\u001b\[31m/gi, '')
- text = text.replace(/\u001b\[36m/gi, '')
- text = text.replace(/\u001b\[33m/gi, '')
- text = text.replace(/\u001b\[0m\n/gi, '')
- text = text.replace(/\u001b\[0m/gi, '')
+ text = text
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/\u001b\[31m/gi, '')
+ .replace(/\u001b\[36m/gi, '')
+ .replace(/\u001b\[33m/gi, '')
+ .replace(/\u001b\[0m\n/gi, '')
+ .replace(/\u001b\[0m/gi, '')
return '' + text + '';
}
diff --git a/couchpotato/core/plugins/manage/main.py b/couchpotato/core/plugins/manage/main.py
index 73e10693..bea2fe4f 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
@@ -51,11 +52,11 @@ class Manage(Plugin):
if not os.path.isdir(directory):
if len(directory) > 0:
- log.error('Directory doesn\'t exist: %s' % directory)
+ log.error('Directory doesn\'t exist: %s', directory)
continue
- log.info('Updating manage library: %s' % directory)
- identifiers = fireEvent('scanner.folder', folder = directory, newer_than = last_update, single = True)
+ log.info('Updating manage library: %s', directory)
+ identifiers = fireEvent('scanner.folder', folder = directory, newer_than = last_update if not full else 0, single = True)
if identifiers:
added_identifiers.extend(identifiers)
@@ -67,11 +68,11 @@ 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:
- 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/movie/main.py b/couchpotato/core/plugins/movie/main.py
index 2c5ec731..ab23a18c 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,
})
@@ -241,14 +242,16 @@ class MoviePlugin(Plugin):
for id in getParam('id').split(','):
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:
- fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True)
- fireEventAsync('searcher.single', movie.to_dict(self.default_dict))
+
+ # Get current selected title
+ default_title = ''
+ for title in movie.library.titles:
+ if title.default: default_title = title.title
+
+ fireEvent('notify.frontend', type = 'movie.busy.%s' % id, data = True, message = 'Updating "%s"' % default_title)
+ fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(id))
+
#db.close()
return jsonified({
@@ -277,6 +280,10 @@ class MoviePlugin(Plugin):
def add(self, params = {}, force_readd = True, search_after = True):
+ if not params.get('identifier'):
+ log.error('Can\'t add movie without imdb identifier.')
+ return False
+
library = fireEvent('library.add', single = True, attrs = params, update_after = False)
# Status
@@ -287,6 +294,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(
@@ -295,8 +303,14 @@ 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)
+ search_after = False
elif force_readd:
# Clean snatched history
for release in m.releases:
@@ -305,10 +319,12 @@ class MoviePlugin(Plugin):
m.profile_id = params.get('profile_id', default_profile.get('id'))
else:
- log.debug('Movie already exists, not updating: %s' % params)
+ 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 +337,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, message = 'Successfully added "%s" to your wanted list.' % params.get('title', ''))
#db.close()
return movie_dict
@@ -336,7 +356,7 @@ class MoviePlugin(Plugin):
return jsonified({
'success': True,
- 'added': True,
+ 'added': True if movie_dict else False,
'movie': movie_dict,
})
@@ -351,6 +371,9 @@ class MoviePlugin(Plugin):
for movie_id in ids:
m = db.query(Movie).filter_by(id = movie_id).first()
+ if not m:
+ continue
+
m.profile_id = params.get('profile_id')
# Remove releases
@@ -369,7 +392,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({
@@ -421,6 +444,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:
@@ -441,7 +465,7 @@ class MoviePlugin(Plugin):
log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
return False
- log.debug('Changing status for %s' % (m.library.titles[0].title))
+ log.debug('Changing status for %s', (m.library.titles[0].title))
if not m.profile:
m.status_id = done_status.get('id')
else:
@@ -458,3 +482,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/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js
index 23bdcf89..38fc7109 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(){
@@ -59,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();
@@ -71,25 +84,41 @@ 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);
});
+
+ 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;
+
+ // 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[movie.id] = true;
+ },
createNavigation: function(){
var self = this;
@@ -100,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){
@@ -260,6 +290,7 @@ var MovieList = new Class({
'events': {
'click': function(e){
(e).preventDefault();
+ this.set('text', 'Deleting..')
Api.request('movie.delete', {
'data': {
'id': ids.join(','),
@@ -268,14 +299,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();
}
});
@@ -419,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 6365f798..c30ee99a 100644
--- a/couchpotato/core/plugins/movie/static/movie.css
+++ b/couchpotato/core/plugins/movie/static/movie.css
@@ -24,6 +24,10 @@
.movies .movie.list_view:hover, .movies .movie.mass_edit_view:hover {
background: rgba(255,255,255,0.03);
}
+
+ .movies .movie_container {
+ overflow: hidden;
+ }
.movies .data {
padding: 20px;
@@ -32,7 +36,6 @@
position: relative;
float: right;
border-radius: 0;
- overflow: hidden;
transition: all 0.2s linear;
}
.movies .list_view .data, .movies .mass_edit_view .data {
@@ -89,7 +92,7 @@
font-size: 16px;
font-weight: normal;
text-overflow: ellipsis;
- width: 64%;
+ width: auto;
}
.movies .info .year {
@@ -152,7 +155,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 +202,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 {
@@ -310,6 +313,33 @@
padding-bottom: 4px;
height: auto;
}
+
+ .movies .movie .trailer_container {
+ width: 100%;
+ background: #000;
+ text-align: center;
+ transition: all .6s cubic-bezier(0.9,0,0.1,1);
+ overflow: hidden;
+ }
+ .movies .movie .trailer_container.hide {
+ height: 0 !important;
+ }
+
+ .movies .movie .hide_trailer {
+ position: absolute;
+ top: 0;
+ left: 50%;
+ margin-left: -50px;
+ width: 100px;
+ text-align: center;
+ padding: 3px 10px;
+ background: #4e5969;
+ border-radius: 0 0 2px 2px;
+ transition: all .6s cubic-bezier(0.9,0,0.1,1) .2s;
+ }
+ .movies .movie .hide_trailer.hide {
+ top: -30px;
+ }
.movies .load_more {
display: block;
@@ -330,15 +360,17 @@
width: 1080px;
margin: 0 -60px;
box-shadow: 0 20px 20px -22px rgba(0,0,0,0.1);
+ background: #4e5969;
}
.movies .alph_nav.float {
box-shadow: 0 30px 30px -32px rgba(0,0,0,0.5);
border-radius: 0;
- 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 +378,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 +398,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 +406,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 {
diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js
index 39b12eed..d71eb648 100644
--- a/couchpotato/core/plugins/movie/static/movie.js
+++ b/couchpotato/core/plugins/movie/static/movie.js
@@ -11,53 +11,124 @@ 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));
+
+ ['movie.busy', 'searcher.started'].each(function(listener){
+ App.addEvent(listener+'.'+data.id, function(notification){
+ if(notification.data)
+ self.busy(true)
+ });
+ })
+ },
+
+ busy: function(set_busy){
+ var self = this;
+
+ if(!set_busy){
+ if(self.spinner){
+ self.mask.fade('out');
+ setTimeout(function(){
+ if(self.mask)
+ self.mask.destroy();
+ if(self.spinner)
+ 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();
+
+ self.busy(false);
},
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.movie_container').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')
+ )
)
);
@@ -150,7 +221,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);
@@ -178,6 +249,10 @@ var Movie = new Class({
isSelected: function(){
return this.select_checkbox.get('checked');
+ },
+
+ toElement: function(){
+ return this.el;
}
});
@@ -290,7 +365,7 @@ var ReleaseAction = new Class({
} catch(e){}
new Element('div', {
- 'class': 'item'
+ 'class': 'item '+status.identifier
}).adopt(
new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}),
new Element('span.status', {'text': status.identifier, 'class': 'release_status '+status.identifier}),
@@ -357,4 +432,109 @@ var ReleaseAction = new Class({
}
+});
+
+var TrailerAction = new Class({
+
+ Extends: MovieAction,
+ id: null,
+
+ create: function(){
+ var self = this;
+
+ self.el = new Element('a.trailer', {
+ 'title': 'Watch the trailer of ' + self.movie.getTitle(),
+ 'events': {
+ 'click': self.watch.bind(self)
+ }
+ });
+
+ },
+
+ watch: function(offset){
+ var self = this;
+
+ var data_url = 'http://gdata.youtube.com/feeds/videos?vq="{title}" {year} trailer&max-results=1&alt=json-in-script&orderby=relevance&sortorder=descending&format=5&fmt=18'
+ var url = data_url.substitute({
+ 'title': self.movie.getTitle(),
+ 'year': self.movie.get('year'),
+ 'offset': offset || 1
+ }),
+ size = $(self.movie).getSize(),
+ height = (size.x/16)*9,
+ id = 'trailer-'+randomString();
+
+ self.player_container = new Element('div[id='+id+']');
+ self.container = new Element('div.hide.trailer_container')
+ .adopt(self.player_container)
+ .inject(self.movie.container, 'top');
+
+ self.container.setStyle('height', 0);
+ self.container.removeClass('hide');
+
+ self.close_button = new Element('a.hide.hide_trailer', {
+ 'text': 'Hide trailer',
+ 'events': {
+ 'click': self.stop.bind(self)
+ }
+ }).inject(self.movie);
+
+ setTimeout(function(){
+ $(self.movie).setStyle('max-height', height);
+ self.container.setStyle('height', height);
+ }, 100)
+
+ new Request.JSONP({
+ 'url': url,
+ 'onComplete': function(json){
+ var video_url = json.feed.entry[0].id.$t.split('/'),
+ video_id = video_url[video_url.length-1];
+
+ self.player = new YT.Player(id, {
+ 'height': height,
+ 'width': size.x,
+ 'videoId': video_id,
+ 'playerVars': {
+ 'autoplay': 1,
+ 'showsearch': 0,
+ 'wmode': 'transparent',
+ 'iv_load_policy': 3
+ }
+ });
+
+ self.close_button.removeClass('hide');
+
+ var quality_set = false;
+ var change_quality = function(state){
+ if(!quality_set && (state.data == 1 || state.data || 2)){
+ try {
+ self.player.setPlaybackQuality('hd720');
+ quality_set = true;
+ }
+ catch(e){
+
+ }
+ }
+ }
+ self.player.addEventListener('onStateChange', change_quality);
+
+ }
+ }).send()
+
+ },
+
+ stop: function(){
+ var self = this;
+
+ self.player.stopVideo();
+ self.container.addClass('hide');
+ self.close_button.addClass('hide');
+
+ setTimeout(function(){
+ self.container.destroy()
+ self.close_button.destroy();
+ }, 1800)
+ }
+
+
});
\ No newline at end of file
diff --git a/couchpotato/core/plugins/movie/static/search.js b/couchpotato/core/plugins/movie/static/search.js
index 438ba9aa..6c6c94e9 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', {
@@ -299,11 +301,11 @@ Block.Search.Item = new Class({
'title': self.title_select.get('value'),
'profile_id': self.profile_select.get('value')
},
- 'onComplete': function(){
+ 'onComplete': function(json){
self.options.empty();
self.options.adopt(
new Element('div.message', {
- 'text': 'Movie succesfully added.'
+ 'text': json.added ? 'Movie succesfully added.' : 'Movie didn\'t add properly. Check logs'
})
);
},
@@ -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/core/plugins/profile/main.py b/couchpotato/core/plugins/profile/main.py
index 279d6507..76fb37c5 100644
--- a/couchpotato/core/plugins/profile/main.py
+++ b/couchpotato/core/plugins/profile/main.py
@@ -135,8 +135,7 @@ class ProfilePlugin(Plugin):
success = True
except Exception, e:
- message = 'Failed deleting Profile: %s' % e
- log.error(message)
+ message = log.error('Failed deleting Profile: %s', e)
#db.close()
@@ -163,7 +162,7 @@ class ProfilePlugin(Plugin):
# Create default quality profile
order = -2
for profile in profiles:
- log.info('Creating default profile: %s' % profile.get('label'))
+ log.info('Creating default profile: %s', profile.get('label'))
p = Profile(
label = toUnicode(profile.get('label')),
order = order
diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py
index fd4f1f65..84ac80a8 100644
--- a/couchpotato/core/plugins/quality/main.py
+++ b/couchpotato/core/plugins/quality/main.py
@@ -17,8 +17,8 @@ class QualityPlugin(Plugin):
qualities = [
{'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]},
- {'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts']},
- {'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts', 'ts']},
+ {'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
+ {'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['avi']},
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']},
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
@@ -26,7 +26,7 @@ class QualityPlugin(Plugin):
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
- {'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': [], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}
+ {'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}
]
pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr']
@@ -116,7 +116,7 @@ class QualityPlugin(Plugin):
quality = db.query(Quality).filter_by(identifier = q.get('identifier')).first()
if not quality:
- log.info('Creating quality: %s' % q.get('label'))
+ log.info('Creating quality: %s', q.get('label'))
quality = Quality()
db.add(quality)
@@ -133,7 +133,7 @@ class QualityPlugin(Plugin):
).all()
if not profile:
- log.info('Creating profile: %s' % q.get('label'))
+ log.info('Creating profile: %s', q.get('label'))
profile = Profile(
core = True,
label = toUnicode(quality.label),
@@ -170,20 +170,20 @@ class QualityPlugin(Plugin):
# Check tags
if quality['identifier'] in words:
- log.debug('Found via identifier "%s" in %s' % (quality['identifier'], cur_file))
+ log.debug('Found via identifier "%s" in %s', (quality['identifier'], cur_file))
return self.setCache(hash, quality)
if list(set(quality.get('alternative', [])) & set(words)):
- log.debug('Found %s via alt %s in %s' % (quality['identifier'], quality.get('alternative'), cur_file))
+ log.debug('Found %s via alt %s in %s', (quality['identifier'], quality.get('alternative'), cur_file))
return self.setCache(hash, quality)
for tag in quality.get('tags', []):
if isinstance(tag, tuple) and '.'.join(tag) in '.'.join(words):
- log.debug('Found %s via tag %s in %s' % (quality['identifier'], quality.get('tags'), cur_file))
+ log.debug('Found %s via tag %s in %s', (quality['identifier'], quality.get('tags'), cur_file))
return self.setCache(hash, quality)
if list(set(quality.get('tags', [])) & set(words)):
- log.debug('Found %s via tag %s in %s' % (quality['identifier'], quality.get('tags'), cur_file))
+ log.debug('Found %s via tag %s in %s', (quality['identifier'], quality.get('tags'), cur_file))
return self.setCache(hash, quality)
# Try again with loose testing
@@ -191,7 +191,7 @@ class QualityPlugin(Plugin):
if quality:
return self.setCache(hash, quality)
- log.debug('Could not identify quality for: %s' % files)
+ log.debug('Could not identify quality for: %s', files)
return None
def guessLoose(self, hash, extra):
@@ -200,7 +200,7 @@ class QualityPlugin(Plugin):
# Last check on resolution only
if quality.get('width', 480) == extra.get('resolution_width', 0):
- log.debug('Found %s via resolution_width: %s == %s' % (quality['identifier'], quality.get('width', 480), extra.get('resolution_width', 0)))
+ log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width', 480), extra.get('resolution_width', 0)))
return self.setCache(hash, quality)
if 480 <= extra.get('resolution_width', 0) <= 720:
diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py
index fff53135..10de0541 100644
--- a/couchpotato/core/plugins/release/main.py
+++ b/couchpotato/core/plugins/release/main.py
@@ -79,7 +79,7 @@ class Release(Plugin):
rel.files.append(added_file)
db.commit()
except Exception, e:
- log.debug('Failed to attach "%s" to release: %s' % (cur_file, e))
+ log.debug('Failed to attach "%s" to release: %s', (cur_file, e))
fireEvent('movie.restatus', movie.id)
@@ -143,7 +143,7 @@ class Release(Plugin):
item[info.identifier] = info.value
# Get matching provider
- provider = fireEvent('provider.belongs_to', item['url'], single = True)
+ provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True)
item['download'] = provider.download
fireEvent('searcher.download', data = item, movie = rel.movie.to_dict({
@@ -158,7 +158,7 @@ class Release(Plugin):
'success': True
})
else:
- log.error('Couldn\'t find release with id: %s' % id)
+ log.error('Couldn\'t find release with id: %s', id)
#db.close()
return jsonified({
diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py
index b1b53395..9ceccb8b 100644
--- a/couchpotato/core/plugins/renamer/__init__.py
+++ b/couchpotato/core/plugins/renamer/__init__.py
@@ -19,6 +19,9 @@ rename_options = {
'source': 'Source media (Bluray)',
'original': 'Original filename',
'original_folder': 'Original foldername',
+ 'imdb_id': 'IMDB id (tt0123456)',
+ 'cd': 'CD number (cd1)',
+ 'cd_nr': 'Just the cd nr. (1)',
},
}
diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py
index 24b031e4..50f7e13f 100644
--- a/couchpotato/core/plugins/renamer/main.py
+++ b/couchpotato/core/plugins/renamer/main.py
@@ -86,19 +86,13 @@ class Renamer(Plugin):
# Add _UNKNOWN_ if no library item is connected
if not group['library'] or not movie_title:
- if group['dirname']:
- rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_UNKNOWN_%s' % group['dirname'])
- else: # Add it to filename
- for file_type in group['files']:
- for rename_me in group['files'][file_type]:
- filename = os.path.basename(rename_me)
- rename_files[rename_me] = rename_me.replace(filename, '_UNKNOWN_%s' % filename)
-
+ self.tagDir(group, 'unknown')
+ continue
# Rename the files using the library data
else:
group['library'] = fireEvent('library.update', identifier = group['library']['identifier'], single = True)
if not group['library']:
- log.error('Could not rename, no library item to work with: %s' % group_identifier)
+ log.error('Could not rename, no library item to work with: %s', group_identifier)
continue
library = group['library']
@@ -129,13 +123,14 @@ 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']:
# Move nfo depending on settings
if file_type is 'nfo' and not self.conf('rename_nfo'):
- log.debug('Skipping, renaming of %s disabled' % file_type)
+ log.debug('Skipping, renaming of %s disabled', file_type)
if self.conf('cleanup'):
for current_file in group['files'][file_type]:
remove_files.append(current_file)
@@ -194,7 +189,7 @@ class Renamer(Plugin):
break
if not found:
- log.error('Could not determine dvd structure for: %s' % current_file)
+ log.error('Could not determine dvd structure for: %s', current_file)
# Do rename others
else:
@@ -208,7 +203,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(
@@ -263,13 +258,13 @@ class Renamer(Plugin):
# Mark movie "done" onces it found the quality with the finish check
try:
- if movie.status_id == active_status.get('id'):
+ if movie.status_id == active_status.get('id') and movie.profile:
for profile_type in movie.profile.types:
if profile_type.quality_id == group['meta_data']['quality']['id'] and profile_type.finish:
movie.status_id = done_status.get('id')
db.commit()
except Exception, e:
- log.error('Failed marking movie finished: %s %s' % (e, traceback.format_exc()))
+ log.error('Failed marking movie finished: %s %s', (e, traceback.format_exc()))
# Go over current movie releases
for release in movie.releases:
@@ -279,30 +274,23 @@ class Renamer(Plugin):
# This is where CP removes older, lesser quality releases
if release.quality.order > group['meta_data']['quality']['order']:
- log.info('Removing lesser quality %s for %s.' % (movie.library.titles[0].title, release.quality.label))
+ log.info('Removing lesser quality %s for %s.', (movie.library.titles[0].title, release.quality.label))
for current_file in release.files:
remove_files.append(current_file)
remove_releases.append(release)
# Same quality, but still downloaded, so maybe repack/proper/unrated/directors cut etc
elif release.quality.order is group['meta_data']['quality']['order']:
- log.info('Same quality release already exists for %s, with quality %s. Assuming repack.' % (movie.library.titles[0].title, release.quality.label))
+ log.info('Same quality release already exists for %s, with quality %s. Assuming repack.', (movie.library.titles[0].title, release.quality.label))
for current_file in release.files:
remove_files.append(current_file)
remove_releases.append(release)
# Downloaded a lower quality, rename the newly downloaded files/folder to exclude them from scan
else:
- log.info('Better quality release already exists for %s, with quality %s' % (movie.library.titles[0].title, release.quality.label))
+ log.info('Better quality release already exists for %s, with quality %s', (movie.library.titles[0].title, release.quality.label))
# Add _EXISTS_ to the parent dir
- if group['dirname']:
- for rename_me in rename_files: # Don't rename anything in this group
- rename_files[rename_me] = None
- rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_EXISTS_%s' % group['dirname'])
- else: # Add it to filename
- for rename_me in rename_files:
- filename = os.path.basename(rename_me)
- rename_files[rename_me] = rename_me.replace(filename, '_EXISTS_%s' % filename)
+ self.tagDir(group, 'exists')
# Notify on rename fail
download_message = 'Renaming of %s (%s) canceled, exists in %s already.' % (movie.library.titles[0].title, group['meta_data']['quality']['label'], release.quality.label)
@@ -311,7 +299,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')
@@ -323,14 +310,32 @@ class Renamer(Plugin):
for current_file in group['files']['leftover']:
remove_files.append(current_file)
elif not remove_leftovers: # Don't remove anything
- remove_files = []
+ break
+
+ # Remove files
+ for src in remove_files:
+
+ if isinstance(src, File):
+ src = src.path
+
+ if rename_files.get(src):
+ log.debug('Not removing file that will be renamed: %s', src)
+ continue
+
+ log.info('Removing "%s"', src)
+ try:
+ if os.path.isfile(src):
+ os.remove(src)
+ except:
+ log.error('Failed removing %s: %s', (src, traceback.format_exc()))
+ self.tagDir(group, 'failed_remove')
# Rename all files marked
group['renamed_files'] = []
for src in rename_files:
if rename_files[src]:
dst = rename_files[src]
- log.info('Renaming "%s" to "%s"' % (src, dst))
+ log.info('Renaming "%s" to "%s"', (src, dst))
# Create dir
self.makeDir(os.path.dirname(dst))
@@ -339,34 +344,23 @@ class Renamer(Plugin):
self.moveFile(src, dst)
group['renamed_files'].append(dst)
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()))
+ log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc()))
+ self.tagDir(group, 'failed_rename')
# Remove matching releases
for release in remove_releases:
- log.debug('Removing release %s' % release.identifier)
+ log.debug('Removing release %s', release.identifier)
try:
db.delete(release)
except:
- log.error('Failed removing %s: %s' % (release.identifier, traceback.format_exc()))
+ log.error('Failed removing %s: %s', (release.identifier, traceback.format_exc()))
if group['dirname'] and group['parentdir']:
try:
- log.info('Deleting folder: %s' % group['parentdir'])
+ log.info('Deleting folder: %s', group['parentdir'])
self.deleteEmptyFolder(group['parentdir'])
except:
- log.error('Failed removing %s: %s' % (group['parentdir'], traceback.format_exc()))
+ log.error('Failed removing %s: %s', (group['parentdir'], traceback.format_exc()))
# Search for trailers etc
fireEventAsync('renamer.after', group)
@@ -398,17 +392,43 @@ class Renamer(Plugin):
return rename_files
+ def tagDir(self, group, tag):
+
+ rename_files = {}
+
+ if group['dirname']:
+ rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_%s_%s' % (tag.upper(), group['dirname']))
+ else: # Add it to filename
+ for file_type in group['files']:
+ for rename_me in group['files'][file_type]:
+ filename = os.path.basename(rename_me)
+ rename_files[rename_me] = rename_me.replace(filename, '_%s_%s' % (tag.upper(), filename))
+
+ for src in rename_files:
+ if rename_files[src]:
+ dst = rename_files[src]
+ log.info('Renaming "%s" to "%s"', (src, dst))
+
+ # Create dir
+ self.makeDir(os.path.dirname(dst))
+
+ try:
+ self.moveFile(src, dst)
+ except:
+ log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc()))
+ raise
+
def moveFile(self, old, dest):
try:
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)
+ log.error('Failed setting permissions for file: %s, %s', (dest, traceback.format_exc(1)))
except:
- log.error("Couldn't move file '%s' to '%s': %s" % (old, dest, traceback.format_exc()))
+ log.error('Couldn\'t move file "%s" to "%s": %s', (old, dest, traceback.format_exc()))
raise Exception
return True
@@ -421,7 +441,7 @@ class Renamer(Plugin):
replaced = toUnicode(string)
for x, r in replacements.iteritems():
if r is not None:
- replaced = replaced.replace('<%s>' % toUnicode(x), toUnicode(r))
+ replaced = replaced.replace(u'<%s>' % toUnicode(x), toUnicode(r))
else:
#If information is not available, we don't want the tag in the filename
replaced = replaced.replace('<' + x + '>', '')
@@ -444,9 +464,9 @@ class Renamer(Plugin):
try:
os.rmdir(full_path)
except:
- log.error('Couldn\'t remove empty directory %s: %s' % (full_path, traceback.format_exc()))
+ log.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc()))
try:
os.rmdir(folder)
except:
- log.error('Couldn\'t remove empty directory %s: %s' % (folder, traceback.format_exc()))
+ log.error('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc()))
diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py
index 136a1964..43547b89 100644
--- a/couchpotato/core/plugins/scanner/main.py
+++ b/couchpotato/core/plugins/scanner/main.py
@@ -1,11 +1,10 @@
from couchpotato import get_session
from couchpotato.core.event import fireEvent, addEvent
-from couchpotato.core.helpers.encoding import toUnicode, simplifyString
+from couchpotato.core.helpers.encoding import toUnicode, simplifyString, ss
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.environment import Env
+from couchpotato.core.settings.model import File, Movie
from enzyme.exceptions import NoParserError, ParseError
from guessit import guess_movie_info
from subliminal.videos import Video
@@ -24,10 +23,10 @@ class Scanner(Plugin):
'media': 314572800, # 300MB
'trailer': 1048576, # 1MB
}
- ignored_in_path = ['_unpack', '_failed_', '_unknown_', '_exists_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo'] #unpacking, smb-crap, hidden files
+ ignored_in_path = ['_unpack', '_failed_', '_unknown_', '_exists_', '_failed_remove_', '_failed_rename_', '.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'],
@@ -89,24 +88,28 @@ 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):
+ folder = os.path.normpath(folder)
+
groups = self.scan(folder = folder, files = files)
for group in groups.itervalues():
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():
@@ -127,10 +130,12 @@ 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 = ss(os.path.normpath(folder))
if not folder or not os.path.isdir(folder):
- log.error('Folder doesn\'t exists: %s' % folder)
+ log.error('Folder doesn\'t exists: %s', folder)
return {}
# Get movie "master" files
@@ -141,19 +146,15 @@ class Scanner(Plugin):
if len(files) == 0:
try:
files = []
- for root, dirs, walk_files in os.walk(toUnicode(folder)):
+ for root, dirs, walk_files in os.walk(folder):
for filename in walk_files:
files.append(os.path.join(root, filename))
except:
- try:
- files = []
- folder = toUnicode(folder).encode(Env.get('encoding'))
- log.info('Trying to convert unicode to str path: %s, %s' % (folder, type(folder)))
- for root, dirs, walk_files in os.walk(folder):
- for filename in walk_files:
- files.append(os.path.join(root, filename))
- except:
- log.error('Failed getting files from %s: %s' % (folder, traceback.format_exc()))
+ log.error('Failed getting files from %s: %s', (folder, traceback.format_exc()))
+ else:
+ files = [ss(x) for x in files]
+
+ db = get_session()
for file_path in files:
@@ -207,7 +208,7 @@ class Scanner(Plugin):
for identifier, group in movie_files.iteritems():
if identifier not in group['identifiers'] and len(identifier) > 0: group['identifiers'].append(identifier)
- log.debug('Grouping files: %s' % identifier)
+ log.debug('Grouping files: %s', identifier)
for file_path in group['unsorted_files']:
wo_ext = file_path[:-(len(getExt(file_path)) + 1)]
@@ -231,19 +232,53 @@ class Scanner(Plugin):
# Group the files based on the identifier
- for identifier, group in movie_files.iteritems():
- log.debug('Grouping files on identifier: %s' % identifier)
+ 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:
+ if self.path_identifiers.get(identifier):
+ 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:
+ if self.path_identifiers.get(identifier):
+ del self.path_identifiers[identifier]
+ del delete_identifiers
# Determine file types
processed_movies = {}
@@ -256,13 +291,38 @@ 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)
+ if not os.path.isfile(cur_file):
+ file_too_new = time.time()
+ break
+ 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
+
+ # 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
@@ -284,10 +344,10 @@ class Scanner(Plugin):
group['files']['movie'] = self.getMediaFiles(group['unsorted_files'])
if len(group['files']['movie']) == 0:
- log.error('Couldn\t find any movie files for %s' % identifier)
+ log.error('Couldn\t find any movie files for %s', identifier)
continue
- log.debug('Getting metadata for %s' % identifier)
+ log.debug('Getting metadata for %s', identifier)
group['meta_data'] = self.getMetaData(group)
# Subtitle meta
@@ -320,7 +380,11 @@ class Scanner(Plugin):
# Determine movie
group['library'] = self.determineMovie(group)
if not group['library']:
- log.error('Unable to determine movie: %s' % group['identifiers'])
+ 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
@@ -329,9 +393,9 @@ class Scanner(Plugin):
self.path_identifiers = {}
if len(processed_movies) > 0:
- log.info('Found %s movies in the folder %s' % (len(processed_movies), folder))
+ log.info('Found %s movies in the folder %s', (len(processed_movies), folder))
else:
- log.debug('Found no movies in the folder %s' % (folder))
+ log.debug('Found no movies in the folder %s', (folder))
return processed_movies
def getMetaData(self, group):
@@ -351,7 +415,7 @@ class Scanner(Plugin):
data['resolution_height'] = meta.get('resolution_height', 480)
data['aspect'] = meta.get('resolution_width', 720) / meta.get('resolution_height', 480)
except:
- log.debug('Error parsing metadata: %s %s' % (cur_file, traceback.format_exc()))
+ log.debug('Error parsing metadata: %s %s', (cur_file, traceback.format_exc()))
pass
if data.get('audio'): break
@@ -379,11 +443,11 @@ class Scanner(Plugin):
'resolution_height': tryInt(p.video[0].height),
}
except ParseError:
- log.debug('Failed to parse meta for %s' % filename)
+ log.debug('Failed to parse meta for %s', filename)
except NoParserError:
- log.debug('No parser found for %s' % filename)
+ log.debug('No parser found for %s', filename)
except:
- log.debug('Failed parsing %s' % filename)
+ log.debug('Failed parsing %s', filename)
return {}
@@ -396,7 +460,7 @@ class Scanner(Plugin):
scan_result = []
for p in paths:
if not group['is_dvd']:
- video = Video.from_path(p)
+ video = Video.from_path(toUnicode(p))
video_result = [(video, video.scan())]
scan_result.extend(video_result)
@@ -405,7 +469,7 @@ class Scanner(Plugin):
if s.language and s.path not in paths:
detected_languages[s.path] = [s.language]
except:
- log.debug('Failed parsing subtitle languages for %s: %s' % (paths, traceback.format_exc()))
+ log.debug('Failed parsing subtitle languages for %s: %s', (paths, traceback.format_exc()))
# IDX
for extra in group['files']['subtitle_extra']:
@@ -421,7 +485,7 @@ class Scanner(Plugin):
if len(idx_langs) > 0 and os.path.isfile(sub_file):
detected_languages[sub_file] = idx_langs
except:
- log.error('Failed parsing subtitle idx for %s: %s' % (extra, traceback.format_exc()))
+ log.error('Failed parsing subtitle idx for %s: %s', (extra, traceback.format_exc()))
return detected_languages
@@ -434,7 +498,7 @@ class Scanner(Plugin):
for cur_file in files['movie']:
imdb_id = self.getCPImdb(cur_file)
if imdb_id:
- log.debug('Found movie via CP tag: %s' % cur_file)
+ log.debug('Found movie via CP tag: %s', cur_file)
break
# Check and see if nfo contains the imdb-id
@@ -443,11 +507,23 @@ class Scanner(Plugin):
for nfo_file in files['nfo']:
imdb_id = getImdb(nfo_file)
if imdb_id:
- log.debug('Found movie via nfo file: %s' % nfo_file)
+ log.debug('Found movie via nfo file: %s', nfo_file)
break
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]:
+ 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()
@@ -455,7 +531,7 @@ class Scanner(Plugin):
f = db.query(File).filter_by(path = toUnicode(cur_file)).first()
try:
imdb_id = f.library[0].identifier
- log.debug('Found movie via database: %s' % cur_file)
+ log.debug('Found movie via database: %s', cur_file)
break
except:
pass
@@ -469,7 +545,7 @@ class Scanner(Plugin):
if len(movie) > 0:
imdb_id = movie[0]['imdb']
if imdb_id:
- log.debug('Found movie via OpenSubtitleHash: %s' % cur_file)
+ log.debug('Found movie via OpenSubtitleHash: %s', cur_file)
break
# Search based on identifiers
@@ -486,17 +562,17 @@ class Scanner(Plugin):
if len(movie) > 0:
imdb_id = movie[0]['imdb']
- log.debug('Found movie via search: %s' % cur_file)
+ log.debug('Found movie via search: %s', cur_file)
if imdb_id: break
else:
- log.debug('Identifier to short to use for search: %s' % identifier)
+ log.debug('Identifier to short to use for search: %s', identifier)
if imdb_id:
return fireEvent('library.add', attrs = {
'identifier': imdb_id
}, update_after = False, single = True)
- log.error('No imdb_id found for %s. Add a NFO file with IMDB id or add the year to the filename.' % group['identifiers'])
+ log.error('No imdb_id found for %s. Add a NFO file with IMDB id or add the year to the filename.', group['identifiers'])
return {}
def getCPImdb(self, string):
@@ -585,17 +661,17 @@ class Scanner(Plugin):
# ignoredpaths
for i in self.ignored_in_path:
if i in filename.lower():
- log.debug('Ignored "%s" contains "%s".' % (filename, i))
+ log.debug('Ignored "%s" contains "%s".', (filename, i))
return False
# Sample file
if self.isSampleFile(filename):
- log.debug('Is sample file "%s".' % filename)
+ log.debug('Is sample file "%s".', filename)
return False
# Minimal size
if self.filesizeBetween(filename, self.minimal_filesize['media']):
- log.debug('File to small: %s' % filename)
+ log.debug('File to small: %s', filename)
return False
# All is OK
@@ -603,14 +679,14 @@ class Scanner(Plugin):
def isSampleFile(self, filename):
is_sample = re.search('(^|[\W_])sample\d*[\W_]', filename.lower())
- if is_sample: log.debug('Is sample file: %s' % filename)
+ if is_sample: log.debug('Is sample file: %s', filename)
return is_sample
def filesizeBetween(self, file, min = 0, max = 100000):
try:
return (min * 1048576) < os.path.getsize(file) < (max * 1048576)
except:
- log.error('Couldn\'t get filesize of %s.' % file)
+ log.error('Couldn\'t get filesize of %s.', file)
return False
@@ -629,7 +705,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)
@@ -703,26 +779,28 @@ 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)
+ guess = guess_movie_info(toUnicode(file_name))
if guess.get('title') and guess.get('year'):
- return {
+ guess = {
'name': guess.get('title'),
'year': guess.get('year'),
}
except:
- log.debug('Could not detect via guessit "%s": %s' % (file_name, traceback.format_exc()))
+ log.debug('Could not detect via guessit "%s": %s', (file_name, traceback.format_exc()))
# Backup to simple
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),
}
@@ -731,11 +809,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
diff --git a/couchpotato/core/plugins/score/main.py b/couchpotato/core/plugins/score/main.py
index 74fed1da..f6f3232c 100644
--- a/couchpotato/core/plugins/score/main.py
+++ b/couchpotato/core/plugins/score/main.py
@@ -4,7 +4,8 @@ from couchpotato.core.helpers.variable import getTitle
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.plugins.score.scores import nameScore, nameRatioScore, \
- sizeScore, providerScore, duplicateScore
+ sizeScore, providerScore, duplicateScore, partialIgnoredScore, namePositionScore, \
+ halfMultipartScore
log = CPLog(__name__)
@@ -21,6 +22,7 @@ class Score(Plugin):
for movie_title in movie['library']['titles']:
score += nameRatioScore(toUnicode(nzb['name']), toUnicode(movie_title['title']))
+ score += namePositionScore(toUnicode(nzb['name']), toUnicode(movie_title['title']))
score += sizeScore(nzb['size'])
@@ -38,4 +40,15 @@ class Score(Plugin):
# Duplicates in name
score += duplicateScore(nzb['name'], getTitle(movie['library']))
+ # Partial ignored words
+ score += partialIgnoredScore(nzb['name'], getTitle(movie['library']))
+
+ # Ignore single downloads from multipart
+ score += halfMultipartScore(nzb['name'])
+
+ # 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/score/scores.py b/couchpotato/core/plugins/score/scores.py
index 2cace048..385e3a95 100644
--- a/couchpotato/core/plugins/score/scores.py
+++ b/couchpotato/core/plugins/score/scores.py
@@ -1,5 +1,6 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import simplifyString
+from couchpotato.core.helpers.variable import tryInt
from couchpotato.environment import Env
import re
@@ -16,11 +17,12 @@ name_scores = [
'german:-10', 'french:-10', 'spanish:-10', 'swesub:-20', 'danish:-10', 'dutch:-10',
# Release groups
'imbt:1', 'cocain:1', 'vomit:1', 'fico:1', 'arrow:1', 'pukka:1', 'prism:1', 'devise:1', 'esir:1', 'ctrlhd:1',
- 'metis:1', 'diamond:1', 'wiki:1', 'cbgb:1', 'crossbow:1', 'sinners:1', 'amiable:1', 'refined:1', 'twizted:1', 'felony:1', 'hubris:1', 'machd:1',
+ 'metis:10', 'diamond:10', 'wiki:10', 'cbgb:10', 'crossbow:1', 'sinners:10', 'amiable:10', 'refined:1', 'twizted:1', 'felony:1', 'hubris:1', 'machd:1',
# Extras
'extras:-40', 'trilogy:-40',
]
+
def nameScore(name, year):
''' Calculate score for words in the NZB name '''
@@ -47,8 +49,8 @@ def nameScore(name, year):
return score
-def nameRatioScore(nzb_name, movie_name):
+def nameRatioScore(nzb_name, movie_name):
nzb_words = re.split('\W+', fireEvent('scanner.create_file_identifier', nzb_name, single = True))
movie_words = re.split('\W+', simplifyString(movie_name))
@@ -56,15 +58,68 @@ def nameRatioScore(nzb_name, movie_name):
return 10 - len(left_over)
+def namePositionScore(nzb_name, movie_name):
+ score = 0
+
+ nzb_words = re.split('\W+', simplifyString(nzb_name))
+ qualities = fireEvent('quality.all', single = True)
+
+ try:
+ nzb_name = re.search(r'([\'"])[^\1]*\1', nzb_name).group(0)
+ except:
+ pass
+
+ name_year = fireEvent('scanner.name_year', nzb_name, single = True)
+
+ # Give points for movies beginning with the correct name
+ name_split = simplifyString(nzb_name).split(simplifyString(movie_name))
+ if name_split[0].strip() == '':
+ score += 10
+
+ # If year is second in line, give more points
+ if len(name_split) > 1 and name_year:
+ after_name = name_split[1].strip()
+ if tryInt(after_name[:4]) == name_year.get('year', None):
+ score += 10
+ after_name = after_name[4:]
+
+ # Give -point to crap between year and quality
+ found_quality = None
+ for quality in qualities:
+ # Main in words
+ if quality['identifier'] in nzb_words:
+ found_quality = quality['identifier']
+
+ # Alt in words
+ for alt in quality['alternative']:
+ if alt in nzb_words:
+ found_quality = alt
+ break
+
+ if not found_quality:
+ return score - 20
+
+ allowed = []
+ for value in name_scores:
+ name, sc = value.split(':')
+ allowed.append(name)
+
+ inbetween = re.split('\W+', after_name.split(found_quality)[0].strip())
+
+ score -= (10 * len(set(inbetween) - set(allowed)))
+
+ return score
+
+
def sizeScore(size):
return 0 if size else -20
def providerScore(provider):
if provider in ['NZBMatrix', 'Nzbs', 'Newzbin']:
- return 30
+ return 20
- if provider in ['Newznab', 'Moovee', 'X264']:
+ if provider in ['Newznab']:
return 10
return 0
@@ -79,3 +134,31 @@ def duplicateScore(nzb_name, movie_name):
duplicates = [x for i, x in enumerate(nzb_words) if nzb_words[i:].count(x) > 1]
return len(list(set(duplicates) - set(movie_words))) * -4
+
+
+def partialIgnoredScore(nzb_name, movie_name):
+
+ nzb_name = nzb_name.lower()
+ movie_name = movie_name.lower()
+
+ ignored_words = [x.strip().lower() for x in Env.setting('ignored_words', section = 'searcher').split(',')]
+
+ score = 0
+ for ignored_word in ignored_words:
+ if ignored_word in nzb_name and ignored_word not in movie_name:
+ score -= 5
+
+ return score
+
+def halfMultipartScore(nzb_name):
+
+ wrong_found = 0
+ for nr in [1, 2, 3, 4, 5, 'i', 'ii', 'iii', 'iv', 'v', 'a', 'b', 'c', 'd', 'e']:
+ for wrong in ['cd', 'part', 'dis', 'disc', 'dvd']:
+ if '%s%s' % (wrong, nr) in nzb_name.lower():
+ wrong_found += 1
+
+ if wrong_found == 1:
+ return -30
+
+ return 0
diff --git a/couchpotato/core/plugins/searcher/__init__.py b/couchpotato/core/plugins/searcher/__init__.py
index a3f88555..f499e2bd 100644
--- a/couchpotato/core/plugins/searcher/__init__.py
+++ b/couchpotato/core/plugins/searcher/__init__.py
@@ -29,7 +29,15 @@ 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',
+ },
+ {
+ 'name': 'preferred_method',
+ 'label': 'First search',
+ 'description': 'Which of the methods do you prefer',
+ 'default': 'nzb',
+ 'type': 'dropdown',
+ 'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrents', 'torrent')],
},
],
}, {
diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py
index 254919f0..9ebdec66 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
@@ -28,6 +29,9 @@ class Searcher(Plugin):
# Schedule cronjob
fireEvent('schedule.cron', 'searcher.all', self.all_movies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
+
+ addEvent('app.load', self.all_movies)
+
def all_movies(self):
if self.in_progress:
@@ -55,7 +59,7 @@ class Searcher(Plugin):
except IndexError:
fireEvent('library.update', movie_dict['library']['identifier'], force = True)
except:
- log.error('Search failed for %s: %s' % (movie_dict['library']['identifier'], traceback.format_exc()))
+ log.error('Search failed for %s: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
# Break if CP wants to shut down
if self.shuttingDown():
@@ -77,36 +81,46 @@ class Searcher(Plugin):
pre_releases = fireEvent('quality.pre_releases', single = True)
release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True)
available_status = fireEvent('status.get', 'available', single = True)
+ ignored_status = fireEvent('status.get', 'ignored', single = True)
default_title = getTitle(movie['library'])
if not default_title:
return
+ fireEvent('notify.frontend', type = 'searcher.started.%s' % movie['id'], data = True, message = 'Searching for "%s"' % default_title)
+
+ 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))
+ log.info('Too early to search for %s, %s', (quality_type['quality']['identifier'], default_title))
continue
has_better_quality = 0
- # See if beter quality is available
+ # See if better quality is available
for release in movie['releases']:
- if release['quality']['order'] <= quality_type['quality']['order'] and release['status_id'] is not available_status.get('id'):
+ if release['quality']['order'] <= quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id')]:
has_better_quality += 1
# Don't search for quality lower then already available.
if has_better_quality is 0:
- log.info('Search for %s in %s' % (default_title, quality_type['quality']['label']))
+ log.info('Search for %s in %s', (default_title, quality_type['quality']['label']))
quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True)
+
results = fireEvent('yarr.search', movie, quality, merge = True)
+
sorted_results = sorted(results, key = lambda k: k['score'], reverse = True)
if len(sorted_results) == 0:
- log.debug('Nothing found for %s in %s' % (default_title, quality_type['quality']['label']))
+ log.debug('Nothing found for %s in %s', (default_title, quality_type['quality']['label']))
+
+ download_preference = self.conf('preferred_method')
+ if download_preference != 'both':
+ sorted_results = sorted(sorted_results, key = lambda k: k['type'], reverse = (download_preference == 'torrent'))
# 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:
@@ -137,31 +151,52 @@ class Searcher(Plugin):
rls.info.append(rls_info)
db.commit()
except InterfaceError:
- log.debug('Couldn\'t add %s to ReleaseInfo: %s' % (info, traceback.format_exc()))
+ log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc()))
+
+ nzb['status_id'] = rls.status_id
for nzb in sorted_results:
+ if nzb['status_id'] == ignored_status.get('id'):
+ log.info('Ignored: %s', nzb['name'])
+ continue
+
+ if nzb['score'] <= 0:
+ log.info('Ignored, score to low: %s', nzb['name'])
+ continue
+
downloaded = self.download(data = nzb, movie = movie)
- if downloaded:
- return True
- else:
+ if downloaded is True:
+ ret = True
+ break
+ elif downloaded != 'try_next':
break
else:
- log.info('Better quality (%s) already available or snatched for %s' % (quality_type['quality']['label'], default_title))
+ log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title))
fireEvent('movie.restatus', movie['id'])
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):
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, filedata = filedata, single = True)
if successful:
@@ -185,7 +220,7 @@ class Searcher(Plugin):
if movie['status_id'] == active_status.get('id'):
for profile_type in movie['profile']['types']:
if profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
- log.info('Renamer disabled, marking movie as finished: %s' % log_movie)
+ log.info('Renamer disabled, marking movie as finished: %s', log_movie)
# Mark release done
rls.status_id = done_status.get('id')
@@ -196,7 +231,7 @@ class Searcher(Plugin):
mvie.status_id = done_status.get('id')
db.commit()
except Exception, e:
- log.error('Failed marking movie finished: %s %s' % (e, traceback.format_exc()))
+ log.error('Failed marking movie finished: %s %s', (e, traceback.format_exc()))
#db.close()
return True
@@ -211,27 +246,29 @@ class Searcher(Plugin):
retention = Env.setting('retention', section = 'nzb')
if nzb.get('seeds') is None and retention < nzb.get('age', 0):
- log.info('Wrong: Outside retention, age is %s, needs %s or lower: %s' % (nzb['age'], retention, nzb['name']))
+ 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.")
+ log.info("Wrong: Required word missing: %s" % nzb['name'])
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)))
return False
- pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs']
+ pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic']
for p_tag in pron_tags:
- if p_tag in movie_name:
- log.info('Wrong: %s, probably pr0n' % (nzb['name']))
+ if p_tag in nzb_words and p_tag not in movie_words:
+ log.info('Wrong: %s, probably pr0n', (nzb['name']))
return False
#qualities = fireEvent('quality.all', single = True)
@@ -239,18 +276,28 @@ class Searcher(Plugin):
# Contains lower quality string
if self.containsOtherQuality(nzb, movie_year = movie['library']['year'], preferred_quality = preferred_quality, single_category = single_category):
- log.info('Wrong: %s, looking for %s' % (nzb['name'], quality['label']))
+ log.info('Wrong: %s, looking for %s', (nzb['name'], quality['label']))
return False
# File to small
if nzb['size'] and preferred_quality['size_min'] > nzb['size']:
- log.info('"%s" is too small to be %s. %sMB instead of the minimal of %sMB.' % (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_min']))
+ log.info('"%s" is too small to be %s. %sMB instead of the minimal of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_min']))
return False
# File to large
if nzb['size'] and preferred_quality.get('size_max') < nzb['size']:
- log.info('"%s" is too large to be %s. %sMB instead of the maximum of %sMB.' % (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_max']))
+ log.info('"%s" is too large to be %s. %sMB instead of the maximum of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_max']))
+ 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
@@ -277,7 +324,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):
@@ -379,17 +426,19 @@ class Searcher(Plugin):
if dates.get('theater') - 604800 < now:
return True
else:
- # 6 weeks after theater release
- if dates.get('theater') + 3628800 < now:
+ # 12 weeks after theater release
+ if dates.get('theater') > 0 and dates.get('theater') + 7257600 < now:
return True
- # 6 weeks before dvd release
- if dates.get('dvd') - 3628800 < now:
- return True
+ if dates.get('dvd') > 0:
- # Dvd should be released
- if dates.get('dvd') > 0 and dates.get('dvd') < now:
- return True
+ # 3 weeks before dvd release
+ if dates.get('dvd') - 1814400 < now:
+ return True
+
+ # Dvd should be released
+ if dates.get('dvd') < now:
+ return True
return False
diff --git a/couchpotato/core/plugins/status/main.py b/couchpotato/core/plugins/status/main.py
index edf2753e..af2e8792 100644
--- a/couchpotato/core/plugins/status/main.py
+++ b/couchpotato/core/plugins/status/main.py
@@ -92,7 +92,7 @@ class StatusPlugin(Plugin):
for identifier, label in self.statuses.iteritems():
s = db.query(Status).filter_by(identifier = identifier).first()
if not s:
- log.info('Creating status: %s' % label)
+ log.info('Creating status: %s', label)
s = Status(
identifier = identifier,
label = toUnicode(label)
diff --git a/couchpotato/core/plugins/subtitle/main.py b/couchpotato/core/plugins/subtitle/main.py
index badc78d6..0efb9110 100644
--- a/couchpotato/core/plugins/subtitle/main.py
+++ b/couchpotato/core/plugins/subtitle/main.py
@@ -1,10 +1,12 @@
from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEvent
+from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library, FileType
from couchpotato.environment import Env
import subliminal
+import traceback
log = CPLog(__name__)
@@ -44,16 +46,26 @@ class Subtitle(Plugin):
if self.isDisabled(): return
- available_languages = sum(group['subtitle_language'].itervalues(), [])
- downloaded = []
- for lang in self.getLanguages():
- if lang not in available_languages:
- download = subliminal.download_subtitles(group['files']['movie'], multi = True, force = False, languages = [lang], services = self.services, cache_dir = Env.get('cache_dir'))
- downloaded.extend(download)
+ try:
+ available_languages = sum(group['subtitle_language'].itervalues(), [])
+ downloaded = []
+ files = [toUnicode(x) for x in group['files']['movie']]
- for d_sub in downloaded:
- group['files']['subtitle'].add(d_sub.path)
- group['subtitle_language'][d_sub.path] = [d_sub.language]
+ for lang in self.getLanguages():
+ if lang not in available_languages:
+ download = subliminal.download_subtitles(files, multi = True, force = False, languages = [lang], services = self.services, cache_dir = Env.get('cache_dir'))
+ downloaded.extend(download)
+
+ for d_sub in downloaded:
+ group['files']['subtitle'].add(d_sub.path)
+ group['subtitle_language'][d_sub.path] = [d_sub.language]
+
+ return True
+
+ except:
+ log.error('Failed searching for subtitle: %s', (traceback.format_exc()))
+
+ return False
def getLanguages(self):
return [x.strip() for x in self.conf('languages').split(',')]
diff --git a/couchpotato/core/plugins/trailer/main.py b/couchpotato/core/plugins/trailer/main.py
index 8f8e4ab2..05246845 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,13 +17,16 @@ 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))
if not os.path.isfile(destination):
fireEvent('file.download', url = trailer, dest = destination, urlopen_kwargs = {'headers': {'User-Agent': 'Quicktime'}}, single = True)
else:
- log.debug('Trailer already exists: %s' % destination)
+ log.debug('Trailer already exists: %s', destination)
# Download first and break
break
diff --git a/couchpotato/core/plugins/userscript/main.py b/couchpotato/core/plugins/userscript/main.py
index 359fd552..9c5f05f0 100644
--- a/couchpotato/core/plugins/userscript/main.py
+++ b/couchpotato/core/plugins/userscript/main.py
@@ -73,7 +73,7 @@ class Userscript(Plugin):
'movie': fireEvent('userscript.get_movie_via_url', url = url, single = True)
}
if not isDict(params['movie']):
- log.error('Failed adding movie via url: %s' % url)
+ log.error('Failed adding movie via url: %s', url)
params['error'] = params['movie'] if params['movie'] else 'Failed getting movie info'
return jsonified(params)
diff --git a/couchpotato/core/providers/automation/base.py b/couchpotato/core/providers/automation/base.py
index f2e94cec..df04b13b 100644
--- a/couchpotato/core/providers/automation/base.py
+++ b/couchpotato/core/providers/automation/base.py
@@ -20,7 +20,7 @@ class Automation(Plugin):
def _getMovies(self):
if not self.canCheck():
- log.debug('Just checked, skipping %s' % self.getName())
+ log.debug('Just checked, skipping %s', self.getName())
return []
self.last_checked = time.time()
@@ -35,19 +35,28 @@ class Automation(Plugin):
else:
return None
- def isMinimal(self, identifier):
-
- movie = fireEvent('movie.info', identifier = identifier, merge = True)
-
+ def isMinimalMovie(self, movie):
+ if movie['rating'] and movie['rating'].get('imdb'):
+ movie['votes'] = movie['rating']['imdb'][1]
+ movie['rating'] = movie['rating']['imdb'][0]
+ identifier = movie['imdb']
for minimal_type in ['year', 'rating', 'votes']:
type_value = movie.get(minimal_type, 0)
type_min = self.getMinimal(minimal_type)
if type_value < type_min:
- log.info('%s to low for %s, need %s has %s' % (minimal_type, identifier, type_min, type_value))
+ log.info('%s too low for %s, need %s has %s', (minimal_type, identifier, type_min, type_value))
return False
return True
+ def getIMDBFromTitle(self, name, year = None):
+ result = fireEvent('movie.search', q = '%s %s' % (name, year), limit = 1, merge = True)
+
+ if len(result) > 0:
+ return result[0]
+ else:
+ return None
+
def getMinimal(self, min_type):
return Env.setting(min_type, 'automation')
diff --git a/couchpotato/core/providers/automation/bluray/__init__.py b/couchpotato/core/providers/automation/bluray/__init__.py
new file mode 100644
index 00000000..6e9d831d
--- /dev/null
+++ b/couchpotato/core/providers/automation/bluray/__init__.py
@@ -0,0 +1,23 @@
+from .main import Bluray
+
+def start():
+ return Bluray()
+
+config = [{
+ 'name': 'bluray',
+ 'groups': [
+ {
+ 'tab': 'automation',
+ 'name': 'bluray_automation',
+ 'label': 'Blu-ray.com',
+ 'description': 'Imports movies from blu-ray.com. (uses minimal requirements)',
+ 'options': [
+ {
+ 'name': 'automation_enabled',
+ 'default': False,
+ 'type': 'enabler',
+ },
+ ],
+ },
+ ],
+}]
diff --git a/couchpotato/core/providers/automation/bluray/main.py b/couchpotato/core/providers/automation/bluray/main.py
new file mode 100644
index 00000000..cd7c0c90
--- /dev/null
+++ b/couchpotato/core/providers/automation/bluray/main.py
@@ -0,0 +1,62 @@
+from couchpotato.core.helpers.rss import RSS
+from couchpotato.core.helpers.variable import md5
+from couchpotato.core.logger import CPLog
+from couchpotato.core.providers.automation.base import Automation
+from couchpotato.environment import Env
+import xml.etree.ElementTree as XMLTree
+
+log = CPLog(__name__)
+
+
+class Bluray(Automation, RSS):
+
+ interval = 1800
+ rss_url = 'http://www.blu-ray.com/rss/newreleasesfeed.xml'
+
+ def getIMDBids(self):
+
+ if self.isDisabled():
+ return
+
+ movies = []
+ RSSMovie = {'name': 'placeholder', 'year' : 'placeholder'}
+ RSSMovies = []
+
+ cache_key = 'bluray.%s' % md5(self.rss_url)
+ rss_data = self.getCache(cache_key, self.rss_url)
+ data = XMLTree.fromstring(rss_data)
+
+ if data:
+ rss_movies = self.getElements(data, 'channel/item')
+
+ for movie in rss_movies:
+ RSSMovie['name'] = self.getTextElement(movie, "title").lower().split("blu-ray")[0].strip("(").rstrip()
+ RSSMovie['year'] = self.getTextElement(movie, "description").split("|")[1].strip("(").strip()
+
+ if not RSSMovie['name'].find("/") == -1: # make sure it is not a double movie release
+ continue
+
+ if int(RSSMovie['year']) < Env.setting('year', 'automation'): #do year filtering
+ continue
+
+ for test in RSSMovies:
+ if test.values() == RSSMovie.values(): # make sure we did not already include it...
+ break
+ else:
+ log.info('Release found: %s.' % RSSMovie)
+ RSSMovies.append(RSSMovie.copy())
+
+ if not RSSMovies:
+ log.info('No movies found.')
+ return
+
+ log.debug("Applying IMDB filter to found movies...")
+
+ for RSSMovie in RSSMovies:
+ imdb = self.getIMDBFromTitle(RSSMovie['name'] + ' ' + RSSMovie['year'])
+
+ if imdb:
+ if self.isMinimalMovie(imdb):
+ movies.append(imdb['imdb'])
+
+ return movies
diff --git a/couchpotato/core/providers/automation/imdb/__init__.py b/couchpotato/core/providers/automation/imdb/__init__.py
index 338a8b62..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 public IMDB watchlists',
+ 'description': 'From any public 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 02fde26b..6b8cbf79 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,34 +21,45 @@ class IMDB(Automation):
return
movies = []
- headers = {}
- for csv_url in self.conf('automation_urls').split(','):
- prop_name = 'automation.imdb.last_update.%s' % md5(csv_url)
+ enablers = self.conf('automation_urls_use').split(',')
+
+ index = -1
+ for rss_url in self.conf('automation_urls').split(','):
+
+ index += 1
+ if not enablers[index]:
+ continue
+ 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(rss_url)
last_update = float(Env.prop(prop_name, default = 0))
+ last_movie_added = 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
+ 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 created > last_movie_added:
+ last_movie_added = created
+
+ 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()))
-
- Env.prop(prop_name, time.time())
+ log.error('Failed loading IMDB watchlist: %s %s', (rss_url, traceback.format_exc()))
+ Env.prop(prop_name, last_movie_added)
return movies
diff --git a/couchpotato/core/providers/automation/kinepolis/__init__.py b/couchpotato/core/providers/automation/kinepolis/__init__.py
new file mode 100644
index 00000000..eea36016
--- /dev/null
+++ b/couchpotato/core/providers/automation/kinepolis/__init__.py
@@ -0,0 +1,23 @@
+from .main import Kinepolis
+
+def start():
+ return Kinepolis()
+
+config = [{
+ 'name': 'kinepolis',
+ 'groups': [
+ {
+ 'tab': 'automation',
+ 'name': 'kinepolis_automation',
+ 'label': 'Kinepolis',
+ 'description': 'Imports movies from the current top 10 of kinepolis. (uses minimal requirements)',
+ 'options': [
+ {
+ 'name': 'automation_enabled',
+ 'default': False,
+ 'type': 'enabler',
+ },
+ ],
+ },
+ ],
+}]
diff --git a/couchpotato/core/providers/automation/kinepolis/main.py b/couchpotato/core/providers/automation/kinepolis/main.py
new file mode 100644
index 00000000..031bb444
--- /dev/null
+++ b/couchpotato/core/providers/automation/kinepolis/main.py
@@ -0,0 +1,42 @@
+from couchpotato.core.helpers.rss import RSS
+from couchpotato.core.helpers.variable import md5
+from couchpotato.core.logger import CPLog
+from couchpotato.core.providers.automation.base import Automation
+import datetime
+import xml.etree.ElementTree as XMLTree
+
+log = CPLog(__name__)
+
+
+class Kinepolis(Automation, RSS):
+
+ interval = 1800
+ rss_url = 'http://kinepolis.be/nl/top10-box-office/feed'
+
+ def getIMDBids(self):
+
+ if self.isDisabled():
+ return
+
+ movies = []
+ RSSMovie = {'name': 'placeholder', 'year' : 'placeholder'}
+
+ cache_key = 'kinepolis.%s' % md5(self.rss_url)
+ rss_data = self.getCache(cache_key, self.rss_url)
+ data = XMLTree.fromstring(rss_data)
+
+ if data is not None:
+ rss_movies = self.getElements(data, 'channel/item')
+
+ for movie in rss_movies:
+ RSSMovie['name'] = self.getTextElement(movie, "title")
+ currentYear = datetime.datetime.now().strftime("%Y")
+ RSSMovie['year'] = currentYear
+
+ log.debug('Release found: %s.', RSSMovie)
+ imdb = self.getIMDBFromTitle(RSSMovie['name'], RSSMovie['year'])
+
+ if imdb:
+ movies.append(imdb['imdb'])
+
+ return movies
diff --git a/couchpotato/core/providers/automation/trakt/main.py b/couchpotato/core/providers/automation/trakt/main.py
index e623b8fe..764f6cfc 100644
--- a/couchpotato/core/providers/automation/trakt/main.py
+++ b/couchpotato/core/providers/automation/trakt/main.py
@@ -41,13 +41,19 @@ class Trakt(Automation):
def call(self, method_url):
- if self.conf('automation_password'):
- headers = {
- 'Authorization': "Basic %s" % base64.encodestring('%s:%s' % (self.conf('automation_username'), self.conf('automation_password')))[:-1]
- }
- else:
- headers = {}
+ try:
+ if self.conf('automation_password'):
+ headers = {
+ 'Authorization': 'Basic %s' % base64.encodestring('%s:%s' % (self.conf('automation_username'), self.conf('automation_password')))[:-1]
+ }
+ else:
+ headers = {}
- cache_key = 'trakt.%s' % md5(method_url)
- json_string = self.getCache(cache_key, self.urls['base'] + method_url, headers = headers)
- return json.loads(json_string)
+ cache_key = 'trakt.%s' % md5(method_url)
+ json_string = self.getCache(cache_key, self.urls['base'] + method_url, headers = headers)
+ if json_string:
+ return json.loads(json_string)
+ except:
+ log.error('Failed to get data from trakt, check your login.')
+
+ return []
diff --git a/couchpotato/core/providers/base.py b/couchpotato/core/providers/base.py
index 826c7d96..d25377b4 100644
--- a/couchpotato/core/providers/base.py
+++ b/couchpotato/core/providers/base.py
@@ -3,6 +3,9 @@ from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
from urlparse import urlparse
+from urllib import quote_plus
+from couchpotato.core.helpers.encoding import simplifyString
+
import re
import time
@@ -31,7 +34,7 @@ class Provider(Plugin):
self.urlopen(test_url, 30)
self.is_available[host] = True
except:
- log.error('"%s" unavailable, trying again in an 15 minutes.' % host)
+ log.error('"%s" unavailable, trying again in an 15 minutes.', host)
self.is_available[host] = False
return self.is_available.get(host, False)
@@ -62,8 +65,11 @@ class YarrProvider(Provider):
def search(self, movie, quality):
return []
- def belongsTo(self, url, host = None):
+ def belongsTo(self, url, provider = None, host = None):
try:
+ if provider and provider == self.getName():
+ return self
+
hostname = urlparse(url).hostname
if host and hostname in host:
return self
@@ -73,7 +79,7 @@ class YarrProvider(Provider):
if hostname in download_url:
return self
except:
- log.debug('Url % s doesn\'t belong to %s' % (url, self.getName()))
+ log.debug('Url % s doesn\'t belong to %s', (url, self.getName()))
return
@@ -106,4 +112,4 @@ class YarrProvider(Provider):
return [self.cat_backup_id]
def found(self, new):
- log.info('Found: score(%(score)s) on %(provider)s: %(name)s' % new)
+ log.info('Found: score(%(score)s) on %(provider)s: %(name)s', new)
diff --git a/couchpotato/core/providers/metadata/base.py b/couchpotato/core/providers/metadata/base.py
index 0f59c9f5..a10f9c82 100644
--- a/couchpotato/core/providers/metadata/base.py
+++ b/couchpotato/core/providers/metadata/base.py
@@ -19,14 +19,14 @@ class MetaDataBase(Plugin):
def create(self, release):
if self.isDisabled(): return
- log.info('Creating %s metadata.' % self.getName())
+ log.info('Creating %s metadata.', self.getName())
# Update library to get latest info
try:
updated_library = fireEvent('library.update', release['library']['identifier'], force = True, single = True)
release['library'] = mergeDicts(release['library'], updated_library)
except:
- log.error('Failed to update movie, before creating metadata: %s' % traceback.format_exc())
+ log.error('Failed to update movie, before creating metadata: %s', traceback.format_exc())
root_name = self.getRootName(release)
meta_name = os.path.basename(root_name)
@@ -44,14 +44,14 @@ class MetaDataBase(Plugin):
# Get file content
content = getattr(self, 'get' + file_type.capitalize())(movie_info = movie_info, data = release)
if content:
- log.debug('Creating %s file: %s' % (file_type, name))
+ log.debug('Creating %s file: %s', (file_type, name))
if os.path.isfile(content):
shutil.copy2(content, name)
else:
self.createFile(name, content)
except:
- log.error('Unable to create %s file: %s' % (file_type, traceback.format_exc()))
+ log.error('Unable to create %s file: %s', (file_type, traceback.format_exc()))
def getRootName(self, data):
return
@@ -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 = {}):
diff --git a/couchpotato/core/providers/metadata/xbmc/main.py b/couchpotato/core/providers/metadata/xbmc/main.py
index d6c9bed9..361ec120 100644
--- a/couchpotato/core/providers/metadata/xbmc/main.py
+++ b/couchpotato/core/providers/metadata/xbmc/main.py
@@ -77,7 +77,7 @@ class XBMC(MetaDataBase):
votes.text = str(v)
break
except:
- log.debug('Failed adding rating info from %s: %s' % (rating_type, traceback.format_exc()))
+ log.debug('Failed adding rating info from %s: %s', (rating_type, traceback.format_exc()))
# Genre
for genre in movie_info.get('genres', []):
diff --git a/couchpotato/core/providers/movie/_modifier/main.py b/couchpotato/core/providers/movie/_modifier/main.py
index 60e4c274..d0886702 100644
--- a/couchpotato/core/providers/movie/_modifier/main.py
+++ b/couchpotato/core/providers/movie/_modifier/main.py
@@ -1,10 +1,9 @@
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
-import time
import traceback
log = CPLog(__name__)
@@ -23,11 +22,19 @@ 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)
+ if item.get('via_imdb'):
+ if order.index(imdb):
+ order.remove(imdb)
+ order.insert(0, imdb)
+
# Merge dicts
temp[imdb] = mergeDicts(temp[imdb], item)
@@ -61,7 +68,7 @@ class MovieResultModifier(Plugin):
if release.status_id == done_status['id']:
temp['in_library'] = fireEvent('movie.get', movie.id, single = True)
except:
- log.error('Tried getting more info on searched movies: %s' % traceback.format_exc())
+ log.error('Tried getting more info on searched movies: %s', traceback.format_exc())
#db.close()
return temp
diff --git a/couchpotato/core/providers/movie/couchpotatoapi/main.py b/couchpotato/core/providers/movie/couchpotatoapi/main.py
index 298c06bf..4379bb89 100644
--- a/couchpotato/core/providers/movie/couchpotatoapi/main.py
+++ b/couchpotato/core/providers/movie/couchpotatoapi/main.py
@@ -32,10 +32,10 @@ class CouchPotatoApi(MovieProvider):
headers = {'X-CP-Version': fireEvent('app.version', single = True)}
data = self.urlopen((self.api_url % ('eta')) + (identifier + '/'), headers = headers)
dates = json.loads(data)
- log.debug('Found ETA for %s: %s' % (identifier, dates))
+ log.debug('Found ETA for %s: %s', (identifier, dates))
return dates
except Exception, e:
- log.error('Error getting ETA for %s: %s' % (identifier, e))
+ log.error('Error getting ETA for %s: %s', (identifier, e))
return {}
@@ -43,9 +43,9 @@ class CouchPotatoApi(MovieProvider):
try:
data = self.urlopen((self.api_url % ('suggest')) + ','.join(movies) + '/' + ','.join(ignore) + '/')
suggestions = json.loads(data)
- log.info('Found Suggestions for %s' % (suggestions))
+ log.info('Found Suggestions for %s', (suggestions))
except Exception, e:
- log.error('Error getting suggestions for %s: %s' % (movies, e))
+ log.error('Error getting suggestions for %s: %s', (movies, e))
return suggestions
diff --git a/couchpotato/core/providers/movie/imdbapi/main.py b/couchpotato/core/providers/movie/imdbapi/main.py
index 71255e60..535e6b24 100644
--- a/couchpotato/core/providers/movie/imdbapi/main.py
+++ b/couchpotato/core/providers/movie/imdbapi/main.py
@@ -21,6 +21,7 @@ class IMDBAPI(MovieProvider):
def __init__(self):
addEvent('movie.search', self.search)
+ addEvent('movie.searchimdb', self.search)
addEvent('movie.info', self.getInfo)
def search(self, q, limit = 12):
@@ -36,7 +37,7 @@ class IMDBAPI(MovieProvider):
if cached:
result = self.parseMovie(cached)
if result.get('titles') and len(result.get('titles')) > 0:
- log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')')
+ log.info('Found: %s', result['titles'][0] + ' (' + str(result['year']) + ')')
return [result]
return []
@@ -54,7 +55,7 @@ class IMDBAPI(MovieProvider):
if cached:
result = self.parseMovie(cached)
if result.get('titles') and len(result.get('titles')) > 0:
- log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')')
+ log.info('Found: %s', result['titles'][0] + ' (' + str(result['year']) + ')')
return result
return {}
@@ -82,13 +83,14 @@ 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': {
'poster': [movie.get('Poster', '')] if movie.get('Poster') and len(movie.get('Poster', '')) > 4 else [],
},
'rating': {
- 'imdb': (tryFloat(movie.get('imdbRating', 0)), tryInt(movie.get('imdbVotes', ''))),
+ 'imdb': (tryFloat(movie.get('imdbRating', 0)), tryInt(movie.get('imdbVotes', '').replace(',', ''))),
#'rotten': (tryFloat(movie.get('tomatoRating', 0)), tryInt(movie.get('tomatoReviews', 0))),
},
'imdb': str(movie.get('imdbID', '')),
@@ -102,17 +104,17 @@ class IMDBAPI(MovieProvider):
'actors': movie.get('Actors', '').split(','),
}
except:
- log.error('Failed parsing IMDB API json: %s' % traceback.format_exc())
+ log.error('Failed parsing IMDB API json: %s', traceback.format_exc())
return movie_data
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
diff --git a/couchpotato/core/providers/movie/themoviedb/main.py b/couchpotato/core/providers/movie/themoviedb/main.py
index f66f2f7a..9f9fe4fe 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__)
@@ -29,7 +28,7 @@ class TheMovieDb(MovieProvider):
results = self.getCache(cache_key)
if not results:
- log.debug('Searching for movie by hash: %s' % file)
+ log.debug('Searching for movie by hash: %s', file)
try:
raw = tmdb.searchByHashingFile(file)
@@ -37,15 +36,15 @@ class TheMovieDb(MovieProvider):
if raw:
try:
results = self.parseMovie(raw)
- log.info('Found: %s' % results['titles'][0] + ' (' + str(results['year']) + ')')
+ log.info('Found: %s', results['titles'][0] + ' (' + str(results['year']) + ')')
self.setCache(cache_key, results)
return results
except SyntaxError, e:
- log.error('Failed to parse XML response: %s' % e)
+ log.error('Failed to parse XML response: %s', e)
return False
except:
- log.debug('No movies known by hash for: %s' % file)
+ log.debug('No movies known by hash for: %s', file)
pass
return results
@@ -61,7 +60,7 @@ class TheMovieDb(MovieProvider):
results = self.getCache(cache_key)
if not results:
- log.debug('Searching for movie: %s' % q)
+ log.debug('Searching for movie: %s', q)
raw = tmdb.search(search_string)
results = []
@@ -76,18 +75,21 @@ class TheMovieDb(MovieProvider):
if nr == limit:
break
- log.info('Found: %s' % [result['titles'][0] + ' (' + str(result['year']) + ')' for result in results])
+ log.info('Found: %s', [result['titles'][0] + ' (' + str(result['year']) + ')' for result in results])
self.setCache(cache_key, results)
return results
except SyntaxError, e:
- log.error('Failed to parse XML response: %s' % e)
+ log.error('Failed to parse XML response: %s', e)
return False
return results
def getInfo(self, identifier = None):
+ if not identifier:
+ return {}
+
cache_key = 'tmdb.cache.%s' % identifier
result = self.getCache(cache_key)
@@ -96,7 +98,7 @@ class TheMovieDb(MovieProvider):
movie = None
try:
- log.debug('Getting info: %s' % cache_key)
+ log.debug('Getting info: %s', cache_key)
movie = tmdb.imdbLookup(id = identifier)
except:
pass
@@ -117,7 +119,7 @@ class TheMovieDb(MovieProvider):
movie = None
try:
- log.debug('Getting info: %s' % cache_key)
+ log.debug('Getting info: %s', cache_key)
movie = tmdb.getMovieInfo(id = id)
except:
pass
@@ -148,7 +150,8 @@ class TheMovieDb(MovieProvider):
year = None
movie_data = {
- 'id': int(movie.get('id', 0)),
+ 'via_tmdb': True,
+ 'tmdb_id': int(movie.get('id', 0)),
'titles': [toUnicode(movie.get('name'))],
'original_title': movie.get('original_name'),
'images': {
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.*?)
.+?
(?P.*?)
'
-
- 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/mysterbin/main.py b/couchpotato/core/providers/nzb/mysterbin/main.py
index 7e19d1ba..008f24f8 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 bs4 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
@@ -22,11 +23,13 @@ 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'))
+ 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 = {
@@ -39,28 +42,28 @@ 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:
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
@@ -71,13 +74,14 @@ 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,
'size': size,
'url': self.urls['download'] % myster_id,
'description': description,
+ 'download': self.download,
'check_nzb': False,
}
diff --git a/couchpotato/core/providers/nzb/newzbin/main.py b/couchpotato/core/providers/nzb/newzbin/main.py
index 80f856ec..06a09a94 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)
@@ -57,11 +57,12 @@ 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)
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:
@@ -81,7 +82,7 @@ class Newzbin(NZBProvider, RSS):
data = XMLTree.fromstring(data)
nzbs = self.getElements(data, 'channel/item')
except Exception, e:
- log.debug('%s, %s' % (self.getName(), e))
+ log.debug('%s, %s', (self.getName(), e))
return results
for nzb in nzbs:
@@ -115,12 +116,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)
+ imdb_results = True, single = True)
if is_correct_movie:
+ new['score'] = fireEvent('score.calculate', new, movie, single = True)
results.append(new)
self.found(new)
@@ -132,7 +133,7 @@ class Newzbin(NZBProvider, RSS):
def download(self, url = '', nzb_id = ''):
try:
- log.info('Download nzb from newzbin, report id: %s ' % nzb_id)
+ log.info('Download nzb from newzbin, report id: %s ', nzb_id)
return self.urlopen(self.urls['download'], params = {
'username' : self.conf('username'),
@@ -140,7 +141,7 @@ class Newzbin(NZBProvider, RSS):
'reportid' : nzb_id
}, show_error = False)
except Exception, e:
- log.error('Failed downloading from newzbin, check credit: %s' % e)
+ log.error('Failed downloading from newzbin, check credit: %s', e)
return False
def getFormatId(self, format):
diff --git a/couchpotato/core/providers/nzb/newznab/main.py b/couchpotato/core/providers/nzb/newznab/main.py
index 0d648a95..ff2aba86 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']),
@@ -46,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({
@@ -78,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'])
@@ -107,7 +112,7 @@ class Newznab(NZBProvider, RSS):
data = XMLTree.fromstring(data)
nzbs = self.getElements(data, 'channel/item')
except Exception, e:
- log.debug('%s, %s' % (self.getName(), e))
+ log.debug('%s, %s', (self.getName(), e))
return results
results = []
@@ -121,8 +126,8 @@ class Newznab(NZBProvider, RSS):
elif item.attrib.get('name') == 'usenetdate':
date = item.attrib.get('value')
- if date is '': log.debug('Date not parsed properly or not available for %s: %s' % (host['host'], self.getTextElement(nzb, "title")))
- if size is 0: log.debug('Size not parsed properly or not available for %s: %s' % (host['host'], self.getTextElement(nzb, "title")))
+ if date is '': log.debug('Date not parsed properly or not available for %s: %s', (host['host'], self.getTextElement(nzb, "title")))
+ if size is 0: log.debug('Size not parsed properly or not available for %s: %s', (host['host'], self.getTextElement(nzb, "title")))
id = self.getTextElement(nzb, "guid").split('/')[-1:].pop()
new = {
@@ -139,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:
@@ -153,7 +157,7 @@ class Newznab(NZBProvider, RSS):
return results
except SyntaxError:
- log.error('Failed to parse XML response from Newznab: %s' % host)
+ log.error('Failed to parse XML response from Newznab: %s', host)
return results
def getHosts(self):
@@ -172,17 +176,15 @@ class Newznab(NZBProvider, RSS):
return list
- def belongsTo(self, url):
+ def belongsTo(self, url, provider = None):
hosts = self.getHosts()
for host in hosts:
- result = super(Newznab, self).belongsTo(url, host = host['host'])
+ result = super(Newznab, self).belongsTo(url, host = host['host'], provider = provider)
if result:
return result
- return
-
def getUrl(self, host, type):
return cleanHost(host) + 'api?t=' + type
@@ -194,3 +196,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
diff --git a/couchpotato/core/providers/nzb/nzbclub/main.py b/couchpotato/core/providers/nzb/nzbclub/main.py
index 40bc2d1c..dedddb85 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 bs4 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
@@ -19,15 +20,15 @@ class NZBClub(NZBProvider, RSS):
'search': 'https://www.nzbclub.com/nzbfeed.aspx?%s',
}
- http_time_between_calls = 3 #seconds
+ http_time_between_calls = 4 #seconds
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'))
+ 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())
@@ -40,7 +41,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:
@@ -48,7 +49,7 @@ class NZBClub(NZBProvider, RSS):
data = XMLTree.fromstring(data)
nzbs = self.getElements(data, 'channel/item')
except Exception, e:
- log.debug('%s, %s' % (self.getName(), e))
+ log.debug('%s, %s', (self.getName(), e))
return results
for nzb in nzbs:
@@ -58,10 +59,15 @@ 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)
+
+ for ignored in ['ARCHIVE inside ARCHIVE', 'Incomplete', 'repair impossible']:
+ if ignored in full_description:
+ log.info('Wrong: Seems to be passworded or corrupted files: %s', new['name'])
+ return False
+
+ return True
new = {
'id': nzbclub_id,
@@ -73,19 +79,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 +98,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', 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 831ea9c6..3d6384c0 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 bs4 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
@@ -9,6 +10,7 @@ from couchpotato.environment import Env
from dateutil.parser import parse
import re
import time
+import traceback
import xml.etree.ElementTree as XMLTree
log = CPLog(__name__)
@@ -26,10 +28,10 @@ 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'))
+ q = '"%s %s" %s' % (simplifyString(getTitle(movie['library'])), movie['library']['year'], quality.get('identifier'))
arguments = tryUrlencode({
'q': q,
'age': Env.setting('retention', 'nzb'),
@@ -52,7 +54,7 @@ class NzbIndex(NZBProvider, RSS):
data = XMLTree.fromstring(data)
nzbs = self.getElements(data, 'channel/item')
except Exception, e:
- log.debug('%s, %s' % (self.getName(), e))
+ log.debug('%s, %s', (self.getName(), e))
return results
for nzb in nzbs:
@@ -63,42 +65,56 @@ class NzbIndex(NZBProvider, RSS):
try:
description = self.getTextElement(nzb, "description")
- if '/nfo/' in description.lower():
- nfo_url = re.search('href=\"(?P.+)\" ', 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 = ''
+
+ def extra_check(new):
+ if '#c20000' in new['description'].lower():
+ log.info('Wrong: Seems to be passworded: %s', new['name'])
+ return False
+
+ return True
new = {
'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,
'url': enclosure['url'],
'detail_url': enclosure['url'].replace('/download/', '/release/'),
'description': description,
+ 'get_more_info': self.getMoreInfo,
+ 'extra_check': extra_check,
'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)
return results
- except SyntaxError:
- log.error('Failed to parse XML response from NZBMatrix.com')
+ except:
+ log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
return results
+ def getMoreInfo(self, item):
+ try:
+ if '/nfo/' in item['description'].lower():
+ nfo_url = re.search('href=\"(?P.+)\" ', 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..203ada1d 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'))])
@@ -58,7 +58,7 @@ class NZBMatrix(NZBProvider, RSS):
data = XMLTree.fromstring(data)
nzbs = self.getElements(data, 'channel/item')
except Exception, e:
- log.debug('%s, %s' % (self.getName(), e))
+ log.debug('%s, %s', (self.getName(), e))
return results
for nzb in nzbs:
@@ -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)
@@ -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'))
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 = '
(?P.*?)
(?P.*?)
.+?
(?P.*?)
'
-
- 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\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
diff --git a/couchpotato/core/providers/torrent/base.py b/couchpotato/core/providers/torrent/base.py
index e136a999..fc2d35e2 100644
--- a/couchpotato/core/providers/torrent/base.py
+++ b/couchpotato/core/providers/torrent/base.py
@@ -1,5 +1,57 @@
+from couchpotato.core.helpers.variable import getImdb, md5
+from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import YarrProvider
+import cookielib
+import traceback
+import urllib2
+
+log = CPLog(__name__)
class TorrentProvider(YarrProvider):
+
type = 'torrent'
+ login_opener = None
+
+ def imdbMatch(self, url, imdbId):
+ if getImdb(url) == imdbId:
+ return True
+
+ if url[:4] == 'http':
+ try:
+ cache_key = md5(url)
+ data = self.getCache(cache_key, url)
+ except IOError:
+ log.error('Failed to open %s.', url)
+ return False
+
+ return getImdb(data) == imdbId
+
+ return False
+
+ def login(self):
+
+ try:
+ cookiejar = cookielib.CookieJar()
+ opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar))
+ urllib2.install_opener(opener)
+ f = opener.open(self.urls['login'], self.getLoginParams())
+ f.read()
+ f.close()
+ self.login_opener = opener
+ return True
+ except:
+ log.error('Failed to login %s: %s', (self.getName(), traceback.format_exc()))
+
+ return False
+
+ def loginDownload(self, url = '', nzb_id = ''):
+ try:
+ if not self.login_opener and not self.login():
+ log.error('Failed downloading from %s', self.getName())
+ return self.urlopen(url, opener = self.login_opener)
+ except:
+ log.error('Failed downloading from %s: %s', (self.getName(), traceback.format_exc()))
+
+ def getLoginParams(self):
+ return ''
diff --git a/couchpotato/core/providers/torrent/kickasstorrents/main.py b/couchpotato/core/providers/torrent/kickasstorrents/main.py
index 0f386043..2c9b8939 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
@@ -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'))
@@ -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,17 @@ 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
elif column_name is 'size':
new['size'] = self.parseSize(td.text)
@@ -97,7 +99,7 @@ class KickAssTorrents(TorrentProvider):
results.append(new)
self.found(new)
except:
- log.error('Failed parsing KickAssTorrents: %s' % traceback.format_exc())
+ log.error('Failed parsing KickAssTorrents: %s', traceback.format_exc())
except:
pass
diff --git a/couchpotato/core/providers/nzb/moovee/__init__.py b/couchpotato/core/providers/torrent/publichd/__init__.py
similarity index 64%
rename from couchpotato/core/providers/nzb/moovee/__init__.py
rename to couchpotato/core/providers/torrent/publichd/__init__.py
index f2f85d18..c28781e3 100644
--- a/couchpotato/core/providers/nzb/moovee/__init__.py
+++ b/couchpotato/core/providers/torrent/publichd/__init__.py
@@ -1,16 +1,16 @@
-from .main import Moovee
+from .main import PublicHD
def start():
- return Moovee()
+ return PublicHD()
config = [{
- 'name': 'moovee',
+ 'name': 'publichd',
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
- 'name': '#alt.binaries.moovee',
- 'description': 'SD movies only',
+ 'name': 'PublicHD',
+ 'description': 'Public Torrent site with only HD content.',
'options': [
{
'name': 'enabled',
@@ -20,4 +20,4 @@ config = [{
],
},
],
-}]
+}]
\ No newline at end of file
diff --git a/couchpotato/core/providers/torrent/publichd/main.py b/couchpotato/core/providers/torrent/publichd/main.py
new file mode 100644
index 00000000..c962f935
--- /dev/null
+++ b/couchpotato/core/providers/torrent/publichd/main.py
@@ -0,0 +1,105 @@
+from bs4 import BeautifulSoup
+from couchpotato.core.event import fireEvent
+from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode
+from couchpotato.core.helpers.variable import getTitle, tryInt
+from couchpotato.core.logger import CPLog
+from couchpotato.core.providers.torrent.base import TorrentProvider
+from urlparse import parse_qs
+import re
+import traceback
+
+log = CPLog(__name__)
+
+
+class PublicHD(TorrentProvider):
+
+ urls = {
+ 'test': 'http://publichd.eu',
+ 'download': 'http://publichd.eu/%s',
+ 'detail': 'http://publichd.eu/index.php?page=torrent-details&id=%s',
+ 'search': 'http://publichd.eu/index.php',
+ }
+
+ cat_ids = [
+ ([9], ['bd50']),
+ ([5], ['1080p']),
+ ([2], ['720p']),
+ ([15, 16], ['brrip']),
+ ]
+
+ cat_backup_id = 0
+ http_time_between_calls = 0
+
+ def search(self, movie, quality):
+
+ results = []
+
+ if self.isDisabled() or quality['hd'] != True:
+ return results
+
+ params = tryUrlencode({
+ 'page':'torrents',
+ 'search': getTitle(movie['library']) + ' ' + quality['identifier'],
+ 'active': 1,
+ 'category': self.getCatId(quality['identifier'])[0]
+ })
+ url = '%s?%s' % (self.urls['search'], params)
+
+ cache_key = 'publichd.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
+ data = self.getCache(cache_key, url)
+
+ if data:
+
+ try:
+ soup = BeautifulSoup(data)
+
+ results_table = soup.find('table', attrs = {'id': 'bgtorrlist2'})
+ entries = results_table.find_all('tr')
+
+ for result in entries[2:len(entries) - 1]:
+ info_url = result.find(href = re.compile('torrent-details'))
+ download = result.find(href = re.compile('\.torrent'))
+
+ if info_url and download:
+
+ url = parse_qs(info_url['href'])
+
+ new = {
+ 'id': url['id'][0],
+ 'name': info_url.string,
+ 'type': 'torrent',
+ 'check_nzb': False,
+ 'description': '',
+ 'provider': self.getName(),
+ 'download': self.download,
+ 'url': self.urls['download'] % download['href'],
+ 'detail_url': self.urls['detail'] % url['id'][0],
+ 'size': self.parseSize(result.find_all('td')[7].string),
+ 'seeders': tryInt(result.find_all('td')[4].string),
+ 'leechers': tryInt(result.find_all('td')[5].string),
+ 'get_more_info': self.getMoreInfo
+ }
+
+ 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
+
+ except:
+ log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
+
+ return []
+
+ def getMoreInfo(self, item):
+ full_description = self.getCache('publichd.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
+ html = BeautifulSoup(full_description)
+ nfo_pre = html.find('div', attrs = {'id':'torrmain'})
+ description = toUnicode(nfo_pre.text) if nfo_pre else ''
+
+ item['description'] = description
+ return item
diff --git a/couchpotato/core/providers/torrent/sceneaccess/__init__.py b/couchpotato/core/providers/torrent/sceneaccess/__init__.py
new file mode 100644
index 00000000..3a128638
--- /dev/null
+++ b/couchpotato/core/providers/torrent/sceneaccess/__init__.py
@@ -0,0 +1,31 @@
+from .main import SceneAccess
+
+def start():
+ return SceneAccess()
+
+config = [{
+ 'name': 'sceneaccess',
+ 'groups': [
+ {
+ 'tab': 'searcher',
+ 'subtab': 'providers',
+ 'name': 'SceneAccess',
+ 'options': [
+ {
+ 'name': 'enabled',
+ 'type': 'enabler',
+ 'default': False,
+ },
+ {
+ 'name': 'username',
+ 'default': '',
+ },
+ {
+ 'name': 'password',
+ 'default': '',
+ 'type': 'password',
+ },
+ ],
+ },
+ ],
+}]
diff --git a/couchpotato/core/providers/torrent/sceneaccess/main.py b/couchpotato/core/providers/torrent/sceneaccess/main.py
new file mode 100644
index 00000000..20542941
--- /dev/null
+++ b/couchpotato/core/providers/torrent/sceneaccess/main.py
@@ -0,0 +1,112 @@
+from bs4 import BeautifulSoup
+from couchpotato.core.event import fireEvent
+from couchpotato.core.helpers.encoding import simplifyString, tryUrlencode, \
+ toUnicode
+from couchpotato.core.helpers.variable import getTitle, tryInt
+from couchpotato.core.logger import CPLog
+from couchpotato.core.providers.torrent.base import TorrentProvider
+import traceback
+
+log = CPLog(__name__)
+
+
+class SceneAccess(TorrentProvider):
+
+ urls = {
+ 'test': 'https://www.sceneaccess.eu/',
+ 'login' : 'https://www.sceneaccess.eu/login',
+ 'detail': 'https://www.sceneaccess.eu/details?id=%s',
+ 'search': 'https://www.sceneaccess.eu/browse?method=2&c%d=%d',
+ 'download': 'https://www.sceneaccess.eu/%s',
+ }
+
+ cat_ids = [
+ ([22], ['720p', '1080p']),
+ ([7], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr', 'brrip']),
+ ([8], ['dvdr']),
+ ]
+
+ http_time_between_calls = 1 #seconds
+
+ def search(self, movie, quality):
+
+ results = []
+ if self.isDisabled():
+ return results
+
+ url = self.urls['search'] % (
+ self.getCatId(quality['identifier'])[0],
+ self.getCatId(quality['identifier'])[0]
+ )
+
+ q = '"%s %s" %s' % (simplifyString(getTitle(movie['library'])), movie['library']['year'], quality.get('identifier'))
+ arguments = tryUrlencode({
+ 'search': q,
+ })
+ url = "%s&%s" % (url, arguments)
+
+ # Do login for the cookies
+ if not self.login_opener and not self.login():
+ return results
+
+ cache_key = 'sceneaccess.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
+ data = self.getCache(cache_key, url, opener = self.login_opener)
+
+ if data:
+ html = BeautifulSoup(data)
+
+ try:
+ resultsTable = html.find('table', attrs = {'id' : 'torrents-table'})
+ entries = resultsTable.findAll('tr', attrs = {'class' : 'tt_row'})
+ for result in entries:
+
+ link = result.find('td', attrs = {'class' : 'ttr_name'}).find('a')
+ url = result.find('td', attrs = {'class' : 'td_dl'}).find('a')
+ leechers = result.find('td', attrs = {'class' : 'ttr_leechers'}).find('a')
+ id = link['href'].replace('details?id=', '')
+
+ new = {
+ 'id': id,
+ 'type': 'torrent',
+ 'check_nzb': False,
+ 'description': '',
+ 'provider': self.getName(),
+ 'name': link['title'],
+ 'url': self.urls['download'] % url['href'],
+ 'detail_url': self.urls['detail'] % id,
+ 'size': self.parseSize(result.find('td', attrs = {'class' : 'ttr_size'}).contents[0]),
+ 'seeders': tryInt(result.find('td', attrs = {'class' : 'ttr_seeders'}).find('a').string),
+ 'leechers': tryInt(leechers.string) if leechers else 0,
+ 'download': self.loginDownload,
+ 'get_more_info': self.getMoreInfo,
+ }
+
+ 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
+ except:
+ log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
+
+ return []
+
+ def getLoginParams(self):
+ return tryUrlencode({
+ 'username': self.conf('username'),
+ 'password': self.conf('password'),
+ 'submit': 'come on in',
+ })
+
+ def getMoreInfo(self, item):
+ full_description = self.getCache('sceneaccess.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
+ html = BeautifulSoup(full_description)
+ nfo_pre = html.find('div', attrs = {'id':'details_table'})
+ description = toUnicode(nfo_pre.text) if nfo_pre else ''
+
+ item['description'] = description
+ return item
diff --git a/couchpotato/core/providers/torrent/scenehd/__init__.py b/couchpotato/core/providers/torrent/scenehd/__init__.py
new file mode 100644
index 00000000..d4b7a0b8
--- /dev/null
+++ b/couchpotato/core/providers/torrent/scenehd/__init__.py
@@ -0,0 +1,31 @@
+from .main import SceneHD
+
+def start():
+ return SceneHD()
+
+config = [{
+ 'name': 'scenehd',
+ 'groups': [
+ {
+ 'tab': 'searcher',
+ 'subtab': 'providers',
+ 'name': 'SceneHD',
+ 'options': [
+ {
+ 'name': 'enabled',
+ 'type': 'enabler',
+ 'default': False,
+ },
+ {
+ 'name': 'username',
+ 'default': '',
+ },
+ {
+ 'name': 'password',
+ 'default': '',
+ 'type': 'password',
+ },
+ ],
+ },
+ ],
+}]
diff --git a/couchpotato/core/providers/torrent/scenehd/main.py b/couchpotato/core/providers/torrent/scenehd/main.py
new file mode 100644
index 00000000..89d847d2
--- /dev/null
+++ b/couchpotato/core/providers/torrent/scenehd/main.py
@@ -0,0 +1,101 @@
+from bs4 import BeautifulSoup
+from couchpotato.core.event import fireEvent
+from couchpotato.core.helpers.encoding import simplifyString, tryUrlencode
+from couchpotato.core.helpers.variable import getTitle, tryInt
+from couchpotato.core.logger import CPLog
+from couchpotato.core.providers.torrent.base import TorrentProvider
+import traceback
+
+log = CPLog(__name__)
+
+
+class SceneHD(TorrentProvider):
+
+ urls = {
+ 'test': 'http://scenehd.org/',
+ 'login' : 'http://scenehd.org/takelogin.php',
+ 'detail': 'http://scenehd.org/details.php?id=%s',
+ 'search': 'http://scenehd.org/browse.php?ajax',
+ 'download': 'http://scenehd.org/download.php?id=%s',
+ }
+
+ http_time_between_calls = 1 #seconds
+
+ def search(self, movie, quality):
+
+ results = []
+ if self.isDisabled():
+ return results
+
+ q = '"%s %s" %s' % (simplifyString(getTitle(movie['library'])), movie['library']['year'], quality.get('identifier'))
+ arguments = tryUrlencode({
+ 'search': q,
+ })
+ url = "%s&%s" % (self.urls['search'], arguments)
+
+ # Cookie login
+ if not self.login_opener and not self.login():
+ return results
+
+ cache_key = 'scenehd.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
+ data = self.getCache(cache_key, url, opener = self.login_opener)
+
+ if data:
+ html = BeautifulSoup(data)
+
+ try:
+ resultsTable = html.find_all('table')[6]
+ entries = resultsTable.find_all('tr')
+ for result in entries[1:]:
+
+ all_cells = result.find_all('td')
+
+ detail_link = all_cells[2].find('a')
+ details = detail_link['href']
+ id = details.replace('details.php?id=', '')
+
+ leechers = all_cells[11].find('a')
+ if leechers:
+ leechers = leechers.string
+ else:
+ leechers = all_cells[11].string
+
+ new = {
+ 'id': id,
+ 'name': detail_link['title'],
+ 'type': 'torrent',
+ 'check_nzb': False,
+ 'description': '',
+ 'provider': self.getName(),
+ 'size': self.parseSize(all_cells[7].string),
+ 'seeders': tryInt(all_cells[10].find('a').string),
+ 'leechers': tryInt(leechers),
+ 'url': self.urls['download'] % id,
+ 'download': self.loginDownload,
+ }
+
+ imdb_link = all_cells[1].find('a')
+ imdb_results = self.imdbMatch(imdb_link['href'], movie['library']['identifier']) if imdb_link else 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 = imdb_results, single_category = False, single = True)
+
+ if is_correct_movie:
+ results.append(new)
+ self.found(new)
+
+ return results
+
+ except:
+ log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
+
+ return []
+
+
+ def getLoginParams(self, params):
+ return tryUrlencode({
+ 'username': self.conf('username'),
+ 'password': self.conf('password'),
+ 'ssl': 'yes',
+ })
diff --git a/couchpotato/core/providers/torrent/thepiratebay/__init__.py b/couchpotato/core/providers/torrent/thepiratebay/__init__.py
index 810e713a..8aa49111 100644
--- a/couchpotato/core/providers/torrent/thepiratebay/__init__.py
+++ b/couchpotato/core/providers/torrent/thepiratebay/__init__.py
@@ -1,6 +1,27 @@
-from .main import ThePirateBay
+from main import ThePirateBay
def start():
return ThePirateBay()
-config = []
+config = [{
+ 'name': 'thepiratebay',
+ 'groups': [{
+ 'tab': 'searcher',
+ 'subtab': 'providers',
+ 'name': 'ThePirateBay',
+ 'description': 'The world\'s largest bittorrent tracker.',
+ 'options': [
+ {
+ 'name': 'enabled',
+ 'type': 'enabler',
+ 'default': False
+ },
+ {
+ 'name': 'domain',
+ 'advanced': True,
+ 'label': 'Proxy server',
+ 'description': 'Domain for requests, keep empty to let CouchPotato pick.',
+ }
+ ],
+ }]
+}]
diff --git a/couchpotato/core/providers/torrent/thepiratebay/main.py b/couchpotato/core/providers/torrent/thepiratebay/main.py
index d9b06bf2..cdb86a75 100644
--- a/couchpotato/core/providers/torrent/thepiratebay/main.py
+++ b/couchpotato/core/providers/torrent/thepiratebay/main.py
@@ -1,5 +1,14 @@
+from bs4 import BeautifulSoup
+from couchpotato.core.event import fireEvent
+from couchpotato.core.helpers.encoding import toUnicode
+from couchpotato.core.helpers.variable import getTitle, tryInt, cleanHost
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider
+from couchpotato.environment import Env
+from urllib import quote_plus
+import re
+import time
+import traceback
log = CPLog(__name__)
@@ -7,140 +16,127 @@ log = CPLog(__name__)
class ThePirateBay(TorrentProvider):
urls = {
- 'download': 'http://torrents.depiraatbaai.be/%s/%s.torrent',
- 'nfo': 'https://depiraatbaai.be/torrent/%s',
- 'detail': 'https://depiraatbaai.be/torrent/%s',
- 'search': 'https://depiraatbaai.be/search/%s/0/7/%d',
+ 'detail': '%s/torrent/%s',
+ 'search': '%s/search/%s/0/7/%d'
}
cat_ids = [
- ([207], ['720p', '1080p']),
- ([200], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr', 'brrip']),
- ([202], ['dvdr'])
+ ([207], ['720p', '1080p']),
+ ([201], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr', 'brrip']),
+ ([202], ['dvdr'])
]
- cat_backup_id = 200
- ignore_string = {
- '720p': ' -brrip -bdrip',
- '1080p': ' -brrip -bdrip'
- }
+ cat_backup_id = 200
+ disable_provider = False
+ http_time_between_calls = 0
+
+ proxy_list = [
+ 'https://thepiratebay.se',
+ 'https://tpb.ipredator.se',
+ 'https://depiraatbaai.be',
+ 'https://piratereverse.info',
+ 'https://tpb.pirateparty.org.uk',
+ 'https://argumentomteemigreren.nl',
+ ]
def __init__(self):
- pass
+ self.domain = self.conf('domain')
+ super(ThePirateBay, self).__init__()
- def find(self, movie, quality, type):
+ def getDomain(self, url = ''):
+
+ if not self.domain:
+ for proxy in self.proxy_list:
+
+ prop_name = 'tpb_proxy.%s' % proxy
+ last_check = float(Env.prop(prop_name, default = 0))
+ if last_check > time.time() - 1209600:
+ continue
+
+ data = ''
+ try:
+ data = self.urlopen(proxy, timeout = 3)
+ except:
+ log.debug('Failed tpb proxy %s', proxy)
+
+ if 'title="Pirate Search"' in data:
+ log.debug('Using proxy: %s', proxy)
+ self.domain = proxy
+ break
+
+ Env.prop(prop_name, time.time())
+
+ if not self.domain:
+ log.error('No TPB proxies left, please add one in settings, or let us know which one to add on the forum.')
+ return None
+
+ return cleanHost(self.domain).rstrip('/') + url
+
+ def search(self, movie, quality):
results = []
- if not self.enabled() or not self.isAvailable(self.apiUrl):
+ if self.isDisabled() or not self.getDomain():
return results
- url = self.apiUrl % (quote_plus(self.toSearchString(movie.name + ' ' + quality) + self.makeIgnoreString(type)), self.getCatId(type))
+ cache_key = 'thepiratebay.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
+ search_url = self.urls['search'] % (self.getDomain(), quote_plus(getTitle(movie['library']) + ' ' + quality['identifier']), self.getCatId(quality['identifier'])[0])
+ data = self.getCache(cache_key, search_url)
- log.info('Searching: %s' % url)
+ if data:
+ try:
+ soup = BeautifulSoup(data)
+ results_table = soup.find('table', attrs = {'id': 'searchResult'})
+ entries = results_table.find_all('tr')
+ for result in entries[1:]:
+ link = result.find(href = re.compile('torrent\/\d+\/'))
+ download = result.find(href = re.compile('magnet:'))
- data = self.urlopen(url)
- if not data:
- log.error('Failed to get data from %s.' % url)
- return results
+ size = re.search('Size (?P.+),', unicode(result.select('font.detDesc')[0])).group('size')
+ if link and download:
- try:
- tables = SoupStrainer('table')
- html = BeautifulSoup(data, parseOnlyThese = tables)
- resultTable = html.find('table', attrs = {'id':'searchResult'})
- for result in resultTable.findAll('tr'):
- details = result.find('a', attrs = {'class':'detLink'})
- if details:
- href = re.search('/(?P\d+)/', details['href'])
- id = href.group('id')
- name = self.toSaveString(details.contents[0])
- desc = result.find('font', attrs = {'class':'detDesc'}).contents[0].split(',')
- date = ''
- size = 0
- for item in desc:
- # Weird date stuff
- if 'uploaded' in item.lower():
- date = item.replace('Uploaded', '')
- date = date.replace('Today', '')
+ def extra_score(item):
+ trusted = (0, 10)[result.find('img', alt = re.compile('Trusted')) != None]
+ vip = (0, 20)[result.find('img', alt = re.compile('VIP')) != None]
+ confirmed = (0, 30)[result.find('img', alt = re.compile('Helpers')) != None]
+ moderated = (0, 50)[result.find('img', alt = re.compile('Moderator')) != None]
- # Do something with yesterday
- yesterdayMinus = 0
- if 'Y-day' in date:
- date = date.replace('Y-day', '')
- yesterdayMinus = 86400
+ return confirmed + trusted + vip + moderated
- datestring = date.replace(' ', ' ').strip()
- date = int(time.mktime(parse(datestring).timetuple())) - yesterdayMinus
- # size
- elif 'size' in item.lower():
- size = item.replace('Size', '')
+ new = {
+ 'id': re.search('/(?P\d+)/', link['href']).group('id'),
+ 'type': 'torrent_magnet',
+ 'name': link.string,
+ 'check_nzb': False,
+ 'description': '',
+ 'provider': self.getName(),
+ 'url': download['href'],
+ 'detail_url': self.getDomain(link['href']),
+ 'size': self.parseSize(size),
+ 'seeders': tryInt(result.find_all('td')[2].string),
+ 'leechers': tryInt(result.find_all('td')[3].string),
+ 'extra_score': extra_score,
+ 'get_more_info': self.getMoreInfo
+ }
- seedleech = []
- for td in result.findAll('td'):
- try:
- seedleech.append(int(td.contents[0]))
- except ValueError:
- pass
+ 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)
- seeders = 0
- leechers = 0
- if len(seedleech) == 2 and seedleech[0] > 0 and seedleech[1] > 0:
- seeders = seedleech[0]
- leechers = seedleech[1]
-
- # to item
- new = self.feedItem()
- new.id = id
- new.type = 'torrent'
- new.name = name
- new.date = date
- new.size = self.parseSize(size)
- new.seeders = seeders
- new.leechers = leechers
- new.url = self.downloadLink(id, name)
- new.score = self.calcScore(new, movie) + self.uploader(result) + (seeders / 10)
-
- if seeders > 0 and (new.date + (int(self.conf('wait')) * 60 * 60) < time.time()) and Qualities.types.get(type).get('minSize') <= new.size:
- new.detailUrl = self.detailLink(id)
- new.content = self.getInfo(new.detailUrl)
- if self.isCorrectMovie(new, movie, type):
+ if is_correct_movie:
results.append(new)
- log.info('Found: %s' % new.name)
+ self.found(new)
- return results
-
- except AttributeError:
- log.debug('No search results found.')
+ return results
+ except:
+ log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
return []
- def makeIgnoreString(self, type):
- ignore = self.ignoreString.get(type)
- return ignore if ignore else ''
+ def getMoreInfo(self, item):
+ full_description = self.getCache('tpb.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
+ html = BeautifulSoup(full_description)
+ nfo_pre = html.find('div', attrs = {'class':'nfo'})
+ description = toUnicode(nfo_pre.text) if nfo_pre else ''
- def uploader(self, html):
- score = 0
- if html.find('img', attr = {'alt':'VIP'}):
- score += 3
- if html.find('img', attr = {'alt':'Trusted'}):
- score += 1
- return score
-
-
- def getInfo(self, url):
- log.debug('Getting info: %s' % url)
-
- data = self.urlopen(url)
- if not data:
- log.error('Failed to get data from %s.' % url)
- return ''
-
- div = SoupStrainer('div')
- html = BeautifulSoup(data, parseOnlyThese = div)
- html = html.find('div', attrs = {'class':'nfo'})
- return str(html).decode("utf-8", "replace")
-
- def downloadLink(self, id, name):
- return self.downloadUrl % (id, quote_plus(name))
-
- def isEnabled(self):
- return self.conf('enabled') and TorrentProvider.isEnabled(self)
+ item['description'] = description
+ return item
diff --git a/couchpotato/core/providers/torrent/torrentleech/__init__.py b/couchpotato/core/providers/torrent/torrentleech/__init__.py
new file mode 100644
index 00000000..19627e11
--- /dev/null
+++ b/couchpotato/core/providers/torrent/torrentleech/__init__.py
@@ -0,0 +1,31 @@
+from .main import TorrentLeech
+
+def start():
+ return TorrentLeech()
+
+config = [{
+ 'name': 'torrentleech',
+ 'groups': [
+ {
+ 'tab': 'searcher',
+ 'subtab': 'providers',
+ 'name': 'TorrentLeech',
+ 'options': [
+ {
+ 'name': 'enabled',
+ 'type': 'enabler',
+ 'default': False,
+ },
+ {
+ 'name': 'username',
+ 'default': '',
+ },
+ {
+ 'name': 'password',
+ 'default': '',
+ 'type': 'password',
+ },
+ ],
+ },
+ ],
+}]
diff --git a/couchpotato/core/providers/torrent/torrentleech/main.py b/couchpotato/core/providers/torrent/torrentleech/main.py
new file mode 100644
index 00000000..e106a5a6
--- /dev/null
+++ b/couchpotato/core/providers/torrent/torrentleech/main.py
@@ -0,0 +1,98 @@
+from bs4 import BeautifulSoup
+from couchpotato.core.event import fireEvent
+from couchpotato.core.helpers.encoding import tryUrlencode
+from couchpotato.core.helpers.variable import getTitle, tryInt
+from couchpotato.core.logger import CPLog
+from couchpotato.core.providers.torrent.base import TorrentProvider
+from urllib import quote_plus
+import traceback
+
+
+log = CPLog(__name__)
+
+
+class TorrentLeech(TorrentProvider):
+
+ urls = {
+ 'test' : 'http://torrentleech.org/',
+ 'login' : 'http://torrentleech.org/user/account/login/',
+ 'detail' : 'http://torrentleech.org/torrent/%s',
+ 'search' : 'http://torrentleech.org/torrents/browse/index/query/%s/categories/%d',
+ 'download' : 'http://torrentleech.org%s',
+ }
+
+ cat_ids = [
+ ([13], ['720p', '1080p']),
+ ([8], ['cam']),
+ ([9], ['ts', 'tc']),
+ ([10], ['r5', 'scr']),
+ ([11], ['dvdrip']),
+ ([14], ['brrip']),
+ ([12], ['dvdr']),
+ ]
+
+ http_time_between_calls = 1 #seconds
+
+ def search(self, movie, quality):
+
+ results = []
+ if self.isDisabled():
+ return results
+
+ # Cookie login
+ if not self.login_opener and not self.login():
+ return results
+
+ cache_key = 'torrentleech.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
+ url = self.urls['search'] % (quote_plus(getTitle(movie['library']).replace(':', '') + ' ' + quality['identifier']), self.getCatId(quality['identifier'])[0])
+ data = self.getCache(cache_key, url, opener = self.login_opener)
+
+ if data:
+ html = BeautifulSoup(data)
+
+ try:
+ result_table = html.find('table', attrs = {'id' : 'torrenttable'})
+ entries = result_table.find_all('tr')
+
+ for result in entries[1:]:
+
+ link = result.find('td', attrs = {'class' : 'name'}).find('a')
+ url = result.find('td', attrs = {'class' : 'quickdownload'}).find('a')
+
+ new = {
+ 'id': link['href'].replace('/torrent/', ''),
+ 'name': link.string,
+ 'type': 'torrent',
+ 'check_nzb': False,
+ 'description': '',
+ 'provider': self.getName(),
+ 'url': self.urls['download'] % url['href'],
+ 'download': self.loginDownload,
+ 'size': self.parseSize(result.find_all('td')[4].string),
+ 'seeders': tryInt(result.find('td', attrs = {'class' : 'seeders'}).string),
+ 'leechers': tryInt(result.find('td', attrs = {'class' : 'leechers'}).string),
+ }
+
+ imdb_results = self.imdbMatch(self.urls['detail'] % new['id'], movie['library']['identifier'])
+
+ new['score'] = fireEvent('score.calculate', new, movie, single = True)
+ is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality,
+ imdb_results = imdb_results, single_category = False, single = True)
+
+ if is_correct_movie:
+ results.append(new)
+ self.found(new)
+
+ return results
+ except:
+ log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
+
+ return []
+
+ def getLoginParams(self):
+ return tryUrlencode({
+ 'username': self.conf('username'),
+ 'password': self.conf('password'),
+ 'remember_me': 'on',
+ 'login': 'submit',
+ })
diff --git a/couchpotato/core/providers/trailer/hdtrailers/main.py b/couchpotato/core/providers/trailer/hdtrailers/main.py
index b68f76f7..d11f9231 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
@@ -94,7 +94,7 @@ class HDTrailers(TrailerProvider):
return results
except AttributeError:
- log.debug('No trailers found in provider %s.' % provider)
+ log.debug('No trailers found in provider %s.', provider)
results['404'] = True
return results
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/couchpotato/core/providers/userscript/youteather/main.py b/couchpotato/core/providers/userscript/youteather/main.py
index 314495f2..3efd3686 100644
--- a/couchpotato/core/providers/userscript/youteather/main.py
+++ b/couchpotato/core/providers/userscript/youteather/main.py
@@ -5,8 +5,8 @@ class YouTheater(UserscriptBase):
id_re = re.compile("view\.php\?id=(\d+)")
includes = ['http://www.youtheater.com/view.php?id=*', 'http://youtheater.com/view.php?id=*',
'http://www.sratim.co.il/view.php?id=*', 'http://sratim.co.il/view.php?id=*']
-
+
def getMovie(self, url):
id = self.id_re.findall(url)[0]
- url = "http://www.youtheater.com/view.php?id=%s" % id
- return super(YouTheater, self).getMovie(url)
\ No newline at end of file
+ url = 'http://www.youtheater.com/view.php?id=%s' % id
+ return super(YouTheater, self).getMovie(url)
diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py
index 950a67a5..553c0357 100644
--- a/couchpotato/core/settings/model.py
+++ b/couchpotato/core/settings/model.py
@@ -28,7 +28,13 @@ class JsonType(TypeDecorator):
impl = UnicodeText
def process_bind_param(self, value, dialect):
- return toUnicode(json.dumps(value, cls = SetEncoder))
+ try:
+ return toUnicode(json.dumps(value, cls = SetEncoder))
+ except:
+ try:
+ return toUnicode(json.dumps(value, cls = SetEncoder, encoding = 'latin-1'))
+ except:
+ raise
def process_result_value(self, value, dialect):
return json.loads(value if value else '{}')
@@ -95,7 +101,6 @@ class Release(Entity):
status = ManyToOne('Status')
quality = ManyToOne('Quality')
files = ManyToMany('File', cascade = 'all, delete-orphan', single_parent = True)
- history = OneToMany('History', cascade = 'all, delete-orphan')
info = OneToMany('ReleaseInfo', cascade = 'all, delete-orphan')
@@ -193,17 +198,6 @@ class FileProperty(Entity):
file = ManyToOne('File')
-class History(Entity):
- """History of actions that are connected to a certain release,
- such as, renamed to, downloaded, deleted, download subtitles etc"""
-
- added = Field(Integer, default = lambda: int(time.time()))
- message = Field(UnicodeText)
- type = Field(Unicode(50))
-
- release = ManyToOne('Release')
-
-
class RenameHistory(Entity):
"""Remembers from where to where files have been moved."""
diff --git a/couchpotato/environment.py b/couchpotato/environment.py
index e804170d..d8c03c7c 100644
--- a/couchpotato/environment.py
+++ b/couchpotato/environment.py
@@ -11,7 +11,7 @@ class Env(object):
_appname = 'CouchPotato'
''' Environment variables '''
- _encoding = ''
+ _encoding = 'UTF-8'
_debug = False
_dev = False
_settings = Settings()
diff --git a/couchpotato/runner.py b/couchpotato/runner.py
index cf0e5fb9..31a8ce32 100644
--- a/couchpotato/runner.py
+++ b/couchpotato/runner.py
@@ -1,11 +1,15 @@
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, Application, FallbackHandler
+from tornado.wsgi import WSGIContainer
from werkzeug.contrib.cache import FileSystemCache
-import atexit
import locale
import logging
import os.path
@@ -16,12 +20,12 @@ import warnings
def getOptions(base_path, args):
- data_dir = getDataDir()
-
# Options
parser = ArgumentParser(prog = 'CouchPotato.py')
- parser.add_argument('--config_file', default = os.path.join(data_dir, 'settings.conf'),
- dest = 'config_file', help = 'Absolute or ~/ path of the settings file (default ./_data/settings.conf)')
+ parser.add_argument('--data_dir',
+ dest = 'data_dir', help = 'Absolute or ~/ path of the data dir')
+ parser.add_argument('--config_file',
+ dest = 'config_file', help = 'Absolute or ~/ path of the settings file (default DATA_DIR/settings.conf)')
parser.add_argument('--debug', action = 'store_true',
dest = 'debug', help = 'Debug mode')
parser.add_argument('--console_log', action = 'store_true',
@@ -30,19 +34,37 @@ def getOptions(base_path, args):
dest = 'quiet', help = 'No console logging')
parser.add_argument('--daemon', action = 'store_true',
dest = 'daemon', help = 'Daemonize the app')
- parser.add_argument('--pid_file', default = os.path.join(data_dir, 'couchpotato.pid'),
+ parser.add_argument('--pid_file',
dest = 'pid_file', help = 'Path to pidfile needed for daemon')
options = parser.parse_args(args)
+ data_dir = os.path.expanduser(options.data_dir if options.data_dir else getDataDir())
+
+ if not options.config_file:
+ options.config_file = os.path.join(data_dir, 'settings.conf')
+
+ if not options.pid_file:
+ options.pid_file = os.path.join(data_dir, 'couchpotato.pid')
+
options.config_file = os.path.expanduser(options.config_file)
+ options.pid_file = os.path.expanduser(options.pid_file)
return options
+# Tornado monkey patch logging..
+def _log(status_code, request):
-def cleanup():
- fireEvent('app.crappy_shutdown', single = True)
- time.sleep(1)
+ 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):
@@ -110,85 +132,77 @@ 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']:
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 +211,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'),
@@ -214,21 +229,35 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
app.register_blueprint(api, url_prefix = '%s/api/%s/' % (url_base, api_key))
# Some logging and fire load event
- try: log.info('Starting server on port %(port)s' % config)
+ try: log.info('Starting server on port %(port)s', config)
except: pass
- if fire_load: fireEventAsync('app.load')
+ fireEventAsync('app.load')
# Go go go!
+ web_container = WSGIContainer(app)
+ web_container._log = _log
+ 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:
- app.run(**config)
+ application.listen(config['port'], config['host'], no_keep_alive = True)
+ loop.start()
except Exception, e:
try:
nr, msg = e
if nr == 48:
- log.info('Already in use, try %s more time after few seconds' % restart_tries)
+ log.info('Already in use, try %s more time after few seconds', restart_tries)
time.sleep(1)
restart_tries -= 1
diff --git a/couchpotato/static/images/icon.trailer.png b/couchpotato/static/images/icon.trailer.png
new file mode 100644
index 00000000..6a382dc7
Binary files /dev/null and b/couchpotato/static/images/icon.trailer.png differ
diff --git a/couchpotato/static/images/imdb_watchlist.png b/couchpotato/static/images/imdb_watchlist.png
new file mode 100644
index 00000000..fc4158bd
Binary files /dev/null and b/couchpotato/static/images/imdb_watchlist.png differ
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/couchpotato.js b/couchpotato/static/scripts/couchpotato.js
index e2c23b82..c0860a99 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));
@@ -80,9 +80,9 @@ var CouchPotato = new Class({
}
}),
new Element('a', {
- 'text': 'Check for updates',
+ 'text': 'Update to latest',
'events': {
- 'click': self.checkForUpdate.bind(self)
+ 'click': self.checkForUpdate.bind(self, null)
}
}),
new Element('a', {
@@ -211,27 +211,30 @@ 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);
},
- 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');
}
});
@@ -241,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(
@@ -256,20 +261,14 @@ 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){
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"
- });
}
});
diff --git a/couchpotato/static/scripts/library/prefix_free.js b/couchpotato/static/scripts/library/prefix_free.js
index 4876187b..a918d048 100644
--- a/couchpotato/static/scripts/library/prefix_free.js
+++ b/couchpotato/static/scripts/library/prefix_free.js
@@ -1,5 +1,5 @@
/**
- * StyleFix 1.0.2
+ * StyleFix 1.0.3
* @author Lea Verou
* MIT license
*/
@@ -52,8 +52,10 @@ var self = window.StyleFix = {
});
// behavior URLs shoudn’t be converted (Issue #19)
- css = css.replace(RegExp('\\b(behavior:\\s*?url\\(\'?"?)' + base, 'gi'), '$1');
- }
+ // base should be escaped before added to RegExp (Issue #81)
+ var escaped_base = base.replace(/([\\\^\$*+[\]?{}.=!:(|)])/g,"\\$1");
+ css = css.replace(RegExp('\\b(behavior:\\s*?url\\(\'?"?)' + escaped_base, 'gi'), '$1');
+ }
var style = document.createElement('style');
style.textContent = css;
@@ -63,6 +65,8 @@ var self = window.StyleFix = {
parent.insertBefore(style, link);
parent.removeChild(link);
+
+ style.media = link.media; // Duplicate is intentional. See issue #31
}
};
@@ -84,6 +88,9 @@ var self = window.StyleFix = {
},
styleElement: function(style) {
+ if (style.hasAttribute('data-noprefix')) {
+ return;
+ }
var disabled = style.disabled;
style.textContent = self.fix(style.textContent, true, style);
@@ -150,7 +157,7 @@ function $(expr, con) {
})();
/**
- * PrefixFree 1.0.5
+ * PrefixFree 1.0.6
* @author Lea Verou
* MIT license
*/
@@ -160,36 +167,39 @@ if(!window.StyleFix || !window.getComputedStyle) {
return;
}
+// Private helper
+function fix(what, before, after, replacement, css) {
+ what = self[what];
+
+ if(what.length) {
+ var regex = RegExp(before + '(' + what.join('|') + ')' + after, 'gi');
+
+ css = css.replace(regex, replacement);
+ }
+
+ return css;
+}
+
var self = window.PrefixFree = {
prefixCSS: function(css, raw) {
var prefix = self.prefix;
- function fix(what, before, after, replacement) {
- what = self[what];
-
- if(what.length) {
- var regex = RegExp(before + '(' + what.join('|') + ')' + after, 'gi');
-
- css = css.replace(regex, replacement);
- }
- }
-
- fix('functions', '(\\s|:|,)', '\\s*\\(', '$1' + prefix + '$2(');
- fix('keywords', '(\\s|:)', '(\\s|;|\\}|$)', '$1' + prefix + '$2$3');
- fix('properties', '(^|\\{|\\s|;)', '\\s*:', '$1' + prefix + '$2:');
+ css = fix('functions', '(\\s|:|,)', '\\s*\\(', '$1' + prefix + '$2(', css);
+ css = fix('keywords', '(\\s|:)', '(\\s|;|\\}|$)', '$1' + prefix + '$2$3', css);
+ css = fix('properties', '(^|\\{|\\s|;)', '\\s*:', '$1' + prefix + '$2:', css);
// Prefix properties *inside* values (issue #8)
if (self.properties.length) {
var regex = RegExp('\\b(' + self.properties.join('|') + ')(?!:)', 'gi');
- fix('valueProperties', '\\b', ':(.+?);', function($0) {
+ css = fix('valueProperties', '\\b', ':(.+?);', function($0) {
return $0.replace(regex, prefix + "$1")
- });
+ }, css);
}
if(raw) {
- fix('selectors', '', '\\b', self.prefixSelector);
- fix('atrules', '@', '\\b', '@' + prefix + '$1');
+ css = fix('selectors', '', '\\b', self.prefixSelector, css);
+ css = fix('atrules', '@', '\\b', '@' + prefix + '$1', css);
}
// Fix double prefixing
@@ -198,11 +208,25 @@ var self = window.PrefixFree = {
return css;
},
- // Warning: prefixXXX functions prefix no matter what, even if the XXX is supported prefix-less
+ property: function(property) {
+ return (self.properties.indexOf(property)? self.prefix : '') + property;
+ },
+
+ value: function(value, property) {
+ value = fix('functions', '(^|\\s|,)', '\\s*\\(', '$1' + self.prefix + '$2(', value);
+ value = fix('keywords', '(^|\\s)', '(\\s|$)', '$1' + self.prefix + '$2$3', value);
+
+ // TODO properties inside values
+
+ return value;
+ },
+
+ // Warning: Prefixes no matter what, even if the selector is supported prefix-less
prefixSelector: function(selector) {
return selector.replace(/^:{1,2}/, function($0) { return $0 + self.prefix })
},
+ // Warning: Prefixes no matter what, even if the property is supported prefix-less
prefixProperty: function(property, camelCase) {
var prefixed = self.prefix + property;
@@ -334,7 +358,9 @@ var keywords = {
'zoom-out': 'cursor',
'box': 'display',
'flexbox': 'display',
- 'inline-flexbox': 'display'
+ 'inline-flexbox': 'display',
+ 'flex': 'display',
+ 'inline-flex': 'display'
};
self.functions = [];
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/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')
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
}
})
diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js
index 20ca457e..576ad44c 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){
@@ -702,7 +704,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()
@@ -909,6 +911,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(){
@@ -923,16 +926,24 @@ 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]);
});
});
+
+ if(mtches.length == 0 && value != '')
+ mtches.include(value);
mtches.each(self.addTag.bind(self));
@@ -955,7 +966,7 @@ Option.Choice = new Class({
},
addLastTag: function(){
- if(this.tag_input.getElement('li.choice:last-child'))
+ if(this.tag_input.getElement('li.choice:last-child') || !this.tag_input.getElement('li'))
this.addTag('');
},
@@ -965,6 +976,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 +996,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 +1100,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 +1131,71 @@ 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': {
+ 'keydown': 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();
+ self.fireEvent('goLeft');
+ }
+ }
+ },
+ '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/scripts/page/wanted.js b/couchpotato/static/scripts/page/wanted.js
index 1f811c69..6c918cea 100644
--- a/couchpotato/static/scripts/page/wanted.js
+++ b/couchpotato/static/scripts/page/wanted.js
@@ -14,10 +14,10 @@ Page.Wanted = new Class({
self.wanted = new MovieList({
'identifier': 'wanted',
'status': 'active',
- 'actions': MovieActions
+ 'actions': MovieActions,
+ 'add_new': true
});
$(self.wanted).inject(self.el);
- App.addEvent('library.update', self.wanted.update.bind(self.wanted));
}
}
@@ -28,8 +28,9 @@ var MovieActions = {};
window.addEvent('domready', function(){
MovieActions.Wanted = {
- 'IMBD': IMDBAction
- ,'releases': ReleaseAction
+ 'IMDB': IMDBAction
+ ,'Trailer': TrailerAction
+ ,'Releases': ReleaseAction
,'Edit': new Class({
@@ -73,14 +74,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', profile.id ? profile.id : profile.data.id);
});
}
@@ -230,12 +237,12 @@ window.addEvent('domready', function(){
};
MovieActions.Snatched = {
- 'IMBD': IMDBAction
+ 'IMDB': IMDBAction
,'Delete': MovieActions.Wanted.Delete
};
MovieActions.Done = {
- 'IMBD': IMDBAction
+ 'IMDB': IMDBAction
,'Edit': MovieActions.Wanted.Edit
,'Files': new Class({
diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css
index d580368c..f5630f33 100644
--- a/couchpotato/static/style/main.css
+++ b/couchpotato/static/style/main.css
@@ -21,9 +21,9 @@ body {
}
* {
- -moz-box-sizing: border-box;
- -webkit-box-sizing: border-box;
- box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
}
pre {
@@ -154,6 +154,7 @@ body > .spinner, .mask{
.icon.rating { background-image: url('../images/icon.rating.png'); }
.icon.files { background-image: url('../images/icon.files.png'); }
.icon.info { background-image: url('../images/icon.info.png'); }
+.icon.trailer { background-image: url('../images/icon.trailer.png'); }
/*** Navigation ***/
.header {
@@ -197,24 +198,33 @@ body > .spinner, .mask{
.header .navigation li a {
display: block;
padding: 15px;
+ position: relative;
}
.header .navigation li:first-child a { padding-left: 10px; }
.header .navigation li span {
display: block;
margin-top: 5px;
}
-
- .header .navigation li.disabled {
- color: #e5e5e5;
+
+ .header .navigation li a:after {
+ content: '';
+ display: inline-block;
+ height: 2px;
+ width: 76%;
+ left: 12%;
+ position: absolute;
+ top: 46px;
+ 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;
@@ -257,10 +267,9 @@ body > .spinner, .mask{
font-size: 8px;
margin: -5px 0 0 15px;
box-shadow: inset 0 1px 0 rgba(255,255,255,.6), 0 0 3px rgba(0,0,0,.7);
- background: -webkit-gradient(linear, left bottom, left top, from(rgba(255,255,255,.3)), to(rgba(255,255,255,.1)));
- background: -moz-linear-gradient(center bottom, rgba(255,255,255,.3) 0%, rgba(255,255,255,.1) 100%);
background-color: #1b79b8;
text-shadow: none;
+ background-image: linear-gradient(0deg, rgba(255,255,255,.3) 0%, rgba(255,255,255,.1) 100%);
}
.header .notification_menu .wrapper {
@@ -304,7 +313,7 @@ body > .spinner, .mask{
.header .message.update {
text-align: center;
position: relative;
- top: -70px;
+ top: -100px;
padding: 2px 0;
background: #ff6134;
font-size: 12px;
@@ -344,16 +353,8 @@ body > .spinner, .mask{
border-radius:30px;
box-shadow: 0 1px 1px rgba(0,0,0,0.35), inset 0 1px 0px rgba(255,255,255,0.20);
-
- background: url('../images/sprite.png') no-repeat 94% -53px, -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(
- center top,
+ background: url('../images/sprite.png') no-repeat 94% -53px, linear-gradient(
+ 270deg,
#5b9bd1 0%,
#406db8 100%
);
@@ -437,15 +438,9 @@ 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-image: -webkit-gradient(
- linear,
- left bottom,
- left top,
- color-stop(0, rgb(55,62,74)),
- color-stop(1, rgb(73,83,98))
- );
- background-image: -moz-linear-gradient(
- center bottom,
+ background: rgb(55,62,74);
+ background-image: linear-gradient(
+ 90deg,
rgb(55,62,74) 0%,
rgb(73,83,98) 100%
);
@@ -533,15 +528,8 @@ body > .spinner, .mask{
text-align: center;
color: #000;
text-shadow: none;
- background-image: -webkit-gradient(
- linear,
- left bottom,
- right top,
- color-stop(0, rgb(200,200,200)),
- color-stop(1, rgb(255,255,255))
- );
- background-image: -moz-linear-gradient(
- left bottom,
+ background-image: linear-gradient(
+ 45deg,
rgb(200,200,200) 0%,
rgb(255,255,255) 100%
);
@@ -590,4 +578,47 @@ body > .spinner, .mask{
}
.more_menu .wrapper li a:hover {
background: rgba(0,0,0,0.05);
- }
\ No newline at end of file
+ }
+
+.messages {
+ position: fixed;
+ right: 0;
+ bottom: 0;
+ padding: 2px;
+ width: 240px;
+ z-index: 2;
+ overflow: hidden;
+ font-size: 14px;
+ font-weight: bold;
+}
+
+ .messages .message {
+ text-align: center;
+ border-radius: 2px;
+ margin: 2px 0 0 0;
+ height: 0;
+ overflow: hidden;
+ transition: all .6s cubic-bezier(0.9,0,0.1,1);
+ box-shadow: 0 1px 1px rgba(0,0,0,0.35), inset 0 1px 0px rgba(255,255,255,0.20);
+ background-image: linear-gradient(
+ 270deg,
+ #5b9bd1 0%,
+ #406db8 100%
+ );
+ width: 100%;
+ padding: 0 5px;
+ visibility: hidden;
+ max-height: 0;
+ }
+ .messages .message.show {
+ visibility: visible;
+ height: auto;
+ padding-top: 3px;
+ padding-bottom: 3px;
+ min-height: 1px;
+ max-height: 400px;
+ }
+ .messages .message.hide {
+ margin-left: 240px;
+ opacity: 0;
+ }
\ No newline at end of file
diff --git a/couchpotato/static/style/page/settings.css b/couchpotato/static/style/page/settings.css
index 44390f95..b7fbaabf 100644
--- a/couchpotato/static/style/page/settings.css
+++ b/couchpotato/static/style/page/settings.css
@@ -16,17 +16,9 @@
padding: 40px 0;
margin: 0;
min-height: 470px;
-
- background-image: -webkit-gradient(
- linear,
- right top,
- 40% 4%,
- color-stop(0, rgba(0,0,0, 0.3)),
- color-stop(1, rgba(0,0,0, 0))
- );
- background-image: -moz-linear-gradient(
- 10% 0% 16deg,
- rgba(0,0,0,0) 0%,
+ background-image: linear-gradient(
+ 20deg,
+ rgba(0,0,0,0) 50%,
rgba(0,0,0,0.3) 100%
);
}
@@ -330,15 +322,16 @@
.page .tag_input > ul {
list-style: none;
- line-height: 20px;
padding: 3px 0;
border-radius: 3px;
cursor: text;
- width: 31%;
+ width: 30%;
margin: 0 !important;
+ height: 27px;
+ line-height: 0;
+ display: inline-block;
}
.page .tag_input:hover > ul {
- width: 50%;
border-radius: 3px 0 0 3px;
}
.page .tag_input:hover .formHint { display: none; }
@@ -352,6 +345,7 @@
margin: 0 !important;
border-width: 0;
background: 0;
+ line-height: 20px;
}
.page .tag_input > ul > li:first-child { min-width: 4px; }
.page .tag_input li.choice {
@@ -362,29 +356,16 @@
border-radius: 2px;
}
.page .tag_input > ul:hover > li.choice {
- background: url('../../images/sprite.png') no-repeat 94% -53px, -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(
- center top,
+ background: linear-gradient(
+ 270deg,
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(
- 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(
- center top,
+ .page .tag_input > ul > li.choice:hover,
+ .page .tag_input > ul > li.choice.selected {
+ background: linear-gradient(
+ 270deg,
#5b9bd1 0%,
#406db8 100%
);
@@ -422,21 +403,15 @@
margin: -9px 0 0 -16px;
border-radius: 30px 30px 0 0;
cursor: pointer;
- background: url('../../images/icon.delete.png') no-repeat center 2px, -webkit-gradient(
- linear,
- left bottom,
- left top,
- color-stop(0, #5b9bd1),
- color-stop(1, #5b9bd1)
- );
- background: url('../../images/icon.delete.png') no-repeat center 2px, -moz-linear-gradient(
- center top,
+ background: url('../../images/icon.delete.png') no-repeat center 2px, -webkit-linear-gradient(
+ 270deg,
#5b9bd1 0%,
#5b9bd1 100%
);
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;
@@ -587,4 +562,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
diff --git a/couchpotato/templates/_desktop.html b/couchpotato/templates/_desktop.html
index e6d1ccb2..0c228dca 100644
--- a/couchpotato/templates/_desktop.html
+++ b/couchpotato/templates/_desktop.html
@@ -10,9 +10,7 @@
- {% if not env.get('dev') %}
- {% endif %}
@@ -45,6 +43,8 @@
+
+
')
+ # => <script> do_nasty_stuff() </script>
+ # sanitize_html('Click here for $100')
+ # => Click here for $100
+ def sanitize_token(self, token):
+
+ # accommodate filters which use token_type differently
+ token_type = token["type"]
+ if token_type in tokenTypes.keys():
+ token_type = tokenTypes[token_type]
+
+ if token_type in (tokenTypes["StartTag"], tokenTypes["EndTag"],
+ tokenTypes["EmptyTag"]):
+ if token["name"] in self.allowed_elements:
+ if token.has_key("data"):
+ attrs = dict([(name,val) for name,val in
+ token["data"][::-1]
+ if name in self.allowed_attributes])
+ for attr in self.attr_val_is_uri:
+ if not attrs.has_key(attr):
+ continue
+ val_unescaped = re.sub("[`\000-\040\177-\240\s]+", '',
+ unescape(attrs[attr])).lower()
+ #remove replacement characters from unescaped characters
+ val_unescaped = val_unescaped.replace(u"\ufffd", "")
+ if (re.match("^[a-z0-9][-+.a-z0-9]*:",val_unescaped) and
+ (val_unescaped.split(':')[0] not in
+ self.allowed_protocols)):
+ del attrs[attr]
+ for attr in self.svg_attr_val_allows_ref:
+ if attr in attrs:
+ attrs[attr] = re.sub(r'url\s*\(\s*[^#\s][^)]+?\)',
+ ' ',
+ unescape(attrs[attr]))
+ if (token["name"] in self.svg_allow_local_href and
+ 'xlink:href' in attrs and re.search('^\s*[^#\s].*',
+ attrs['xlink:href'])):
+ del attrs['xlink:href']
+ if attrs.has_key('style'):
+ attrs['style'] = self.sanitize_css(attrs['style'])
+ token["data"] = [[name,val] for name,val in attrs.items()]
+ return token
+ else:
+ if token_type == tokenTypes["EndTag"]:
+ token["data"] = "%s>" % token["name"]
+ elif token["data"]:
+ attrs = ''.join([' %s="%s"' % (k,escape(v)) for k,v in token["data"]])
+ token["data"] = "<%s%s>" % (token["name"],attrs)
+ else:
+ token["data"] = "<%s>" % token["name"]
+ if token.get("selfClosing"):
+ token["data"]=token["data"][:-1] + "/>"
+
+ if token["type"] in tokenTypes.keys():
+ token["type"] = "Characters"
+ else:
+ token["type"] = tokenTypes["Characters"]
+
+ del token["name"]
+ return token
+ elif token_type == tokenTypes["Comment"]:
+ pass
+ else:
+ return token
+
+ def sanitize_css(self, style):
+ # disallow urls
+ style=re.compile('url\s*\(\s*[^\s)]+?\s*\)\s*').sub(' ',style)
+
+ # gauntlet
+ if not re.match("""^([:,;#%.\sa-zA-Z0-9!]|\w-\w|'[\s\w]+'|"[\s\w]+"|\([\d,\s]+\))*$""", style): return ''
+ if not re.match("^\s*([-\w]+\s*:[^:;]*(;\s*|$))*$", style): return ''
+
+ clean = []
+ for prop,value in re.findall("([-\w]+)\s*:\s*([^:;]*)",style):
+ if not value: continue
+ if prop.lower() in self.allowed_css_properties:
+ clean.append(prop + ': ' + value + ';')
+ elif prop.split('-')[0].lower() in ['background','border','margin',
+ 'padding']:
+ for keyword in value.split():
+ if not keyword in self.acceptable_css_keywords and \
+ not re.match("^(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$",keyword):
+ break
+ else:
+ clean.append(prop + ': ' + value + ';')
+ elif prop.lower() in self.allowed_svg_properties:
+ clean.append(prop + ': ' + value + ';')
+
+ return ' '.join(clean)
+
+class HTMLSanitizer(HTMLTokenizer, HTMLSanitizerMixin):
+ def __init__(self, stream, encoding=None, parseMeta=True, useChardet=True,
+ lowercaseElementName=False, lowercaseAttrName=False, parser=None):
+ #Change case matching defaults as we only output lowercase html anyway
+ #This solution doesn't seem ideal...
+ HTMLTokenizer.__init__(self, stream, encoding, parseMeta, useChardet,
+ lowercaseElementName, lowercaseAttrName, parser=parser)
+
+ def __iter__(self):
+ for token in HTMLTokenizer.__iter__(self):
+ token = self.sanitize_token(token)
+ if token:
+ yield token
diff --git a/libs/html5lib/serializer/__init__.py b/libs/html5lib/serializer/__init__.py
new file mode 100644
index 00000000..1b746655
--- /dev/null
+++ b/libs/html5lib/serializer/__init__.py
@@ -0,0 +1,17 @@
+
+from html5lib import treewalkers
+
+from htmlserializer import HTMLSerializer
+from xhtmlserializer import XHTMLSerializer
+
+def serialize(input, tree="simpletree", format="html", encoding=None,
+ **serializer_opts):
+ # XXX: Should we cache this?
+ walker = treewalkers.getTreeWalker(tree)
+ if format == "html":
+ s = HTMLSerializer(**serializer_opts)
+ elif format == "xhtml":
+ s = XHTMLSerializer(**serializer_opts)
+ else:
+ raise ValueError, "type must be either html or xhtml"
+ return s.render(walker(input), encoding)
diff --git a/libs/html5lib/serializer/htmlserializer.py b/libs/html5lib/serializer/htmlserializer.py
new file mode 100644
index 00000000..8dd0a815
--- /dev/null
+++ b/libs/html5lib/serializer/htmlserializer.py
@@ -0,0 +1,312 @@
+try:
+ frozenset
+except NameError:
+ # Import from the sets module for python 2.3
+ from sets import ImmutableSet as frozenset
+
+import gettext
+_ = gettext.gettext
+
+from html5lib.constants import voidElements, booleanAttributes, spaceCharacters
+from html5lib.constants import rcdataElements, entities, xmlEntities
+from html5lib import utils
+from xml.sax.saxutils import escape
+
+spaceCharacters = u"".join(spaceCharacters)
+
+try:
+ from codecs import register_error, xmlcharrefreplace_errors
+except ImportError:
+ unicode_encode_errors = "strict"
+else:
+ unicode_encode_errors = "htmlentityreplace"
+
+ from html5lib.constants import entities
+
+ encode_entity_map = {}
+ is_ucs4 = len(u"\U0010FFFF") == 1
+ for k, v in entities.items():
+ #skip multi-character entities
+ if ((is_ucs4 and len(v) > 1) or
+ (not is_ucs4 and len(v) > 2)):
+ continue
+ if v != "&":
+ if len(v) == 2:
+ v = utils.surrogatePairToCodepoint(v)
+ else:
+ try:
+ v = ord(v)
+ except:
+ print v
+ raise
+ if not v in encode_entity_map or k.islower():
+ # prefer < over < and similarly for &, >, etc.
+ encode_entity_map[v] = k
+
+ def htmlentityreplace_errors(exc):
+ if isinstance(exc, (UnicodeEncodeError, UnicodeTranslateError)):
+ res = []
+ codepoints = []
+ skip = False
+ for i, c in enumerate(exc.object[exc.start:exc.end]):
+ if skip:
+ skip = False
+ continue
+ index = i + exc.start
+ if utils.isSurrogatePair(exc.object[index:min([exc.end, index+2])]):
+ codepoint = utils.surrogatePairToCodepoint(exc.object[index:index+2])
+ skip = True
+ else:
+ codepoint = ord(c)
+ codepoints.append(codepoint)
+ for cp in codepoints:
+ e = encode_entity_map.get(cp)
+ if e:
+ res.append("&")
+ res.append(e)
+ if not e.endswith(";"):
+ res.append(";")
+ else:
+ res.append("%s;"%(hex(cp)[2:]))
+ return (u"".join(res), exc.end)
+ else:
+ return xmlcharrefreplace_errors(exc)
+
+ register_error(unicode_encode_errors, htmlentityreplace_errors)
+
+ del register_error
+
+
+class HTMLSerializer(object):
+
+ # attribute quoting options
+ quote_attr_values = False
+ quote_char = u'"'
+ use_best_quote_char = True
+
+ # tag syntax options
+ omit_optional_tags = True
+ minimize_boolean_attributes = True
+ use_trailing_solidus = False
+ space_before_trailing_solidus = True
+
+ # escaping options
+ escape_lt_in_attrs = False
+ escape_rcdata = False
+ resolve_entities = True
+
+ # miscellaneous options
+ inject_meta_charset = True
+ strip_whitespace = False
+ sanitize = False
+
+ options = ("quote_attr_values", "quote_char", "use_best_quote_char",
+ "minimize_boolean_attributes", "use_trailing_solidus",
+ "space_before_trailing_solidus", "omit_optional_tags",
+ "strip_whitespace", "inject_meta_charset", "escape_lt_in_attrs",
+ "escape_rcdata", "resolve_entities", "sanitize")
+
+ def __init__(self, **kwargs):
+ """Initialize HTMLSerializer.
+
+ Keyword options (default given first unless specified) include:
+
+ inject_meta_charset=True|False
+ Whether it insert a meta element to define the character set of the
+ document.
+ quote_attr_values=True|False
+ Whether to quote attribute values that don't require quoting
+ per HTML5 parsing rules.
+ quote_char=u'"'|u"'"
+ Use given quote character for attribute quoting. Default is to
+ use double quote unless attribute value contains a double quote,
+ in which case single quotes are used instead.
+ escape_lt_in_attrs=False|True
+ Whether to escape < in attribute values.
+ escape_rcdata=False|True
+ Whether to escape characters that need to be escaped within normal
+ elements within rcdata elements such as style.
+ resolve_entities=True|False
+ Whether to resolve named character entities that appear in the
+ source tree. The XML predefined entities < > & " '
+ are unaffected by this setting.
+ strip_whitespace=False|True
+ Whether to remove semantically meaningless whitespace. (This
+ compresses all whitespace to a single space except within pre.)
+ minimize_boolean_attributes=True|False
+ Shortens boolean attributes to give just the attribute value,
+ for example becomes .
+ use_trailing_solidus=False|True
+ Includes a close-tag slash at the end of the start tag of void
+ elements (empty elements whose end tag is forbidden). E.g. .
+ space_before_trailing_solidus=True|False
+ Places a space immediately before the closing slash in a tag
+ using a trailing solidus. E.g. . Requires use_trailing_solidus.
+ sanitize=False|True
+ Strip all unsafe or unknown constructs from output.
+ See `html5lib user documentation`_
+ omit_optional_tags=True|False
+ Omit start/end tags that are optional.
+
+ .. _html5lib user documentation: http://code.google.com/p/html5lib/wiki/UserDocumentation
+ """
+ if kwargs.has_key('quote_char'):
+ self.use_best_quote_char = False
+ for attr in self.options:
+ setattr(self, attr, kwargs.get(attr, getattr(self, attr)))
+ self.errors = []
+ self.strict = False
+
+ def encode(self, string):
+ assert(isinstance(string, unicode))
+ if self.encoding:
+ return string.encode(self.encoding, unicode_encode_errors)
+ else:
+ return string
+
+ def encodeStrict(self, string):
+ assert(isinstance(string, unicode))
+ if self.encoding:
+ return string.encode(self.encoding, "strict")
+ else:
+ return string
+
+ def serialize(self, treewalker, encoding=None):
+ self.encoding = encoding
+ in_cdata = False
+ self.errors = []
+ if encoding and self.inject_meta_charset:
+ from html5lib.filters.inject_meta_charset import Filter
+ treewalker = Filter(treewalker, encoding)
+ # XXX: WhitespaceFilter should be used before OptionalTagFilter
+ # for maximum efficiently of this latter filter
+ if self.strip_whitespace:
+ from html5lib.filters.whitespace import Filter
+ treewalker = Filter(treewalker)
+ if self.sanitize:
+ from html5lib.filters.sanitizer import Filter
+ treewalker = Filter(treewalker)
+ if self.omit_optional_tags:
+ from html5lib.filters.optionaltags import Filter
+ treewalker = Filter(treewalker)
+ for token in treewalker:
+ type = token["type"]
+ if type == "Doctype":
+ doctype = u"= 0:
+ if token["systemId"].find(u"'") >= 0:
+ self.serializeError(_("System identifer contains both single and double quote characters"))
+ quote_char = u"'"
+ else:
+ quote_char = u'"'
+ doctype += u" %s%s%s" % (quote_char, token["systemId"], quote_char)
+
+ doctype += u">"
+ yield self.encodeStrict(doctype)
+
+ elif type in ("Characters", "SpaceCharacters"):
+ if type == "SpaceCharacters" or in_cdata:
+ if in_cdata and token["data"].find("") >= 0:
+ self.serializeError(_("Unexpected in CDATA"))
+ yield self.encode(token["data"])
+ else:
+ yield self.encode(escape(token["data"]))
+
+ elif type in ("StartTag", "EmptyTag"):
+ name = token["name"]
+ yield self.encodeStrict(u"<%s" % name)
+ if name in rcdataElements and not self.escape_rcdata:
+ in_cdata = True
+ elif in_cdata:
+ self.serializeError(_("Unexpected child element of a CDATA element"))
+ attributes = []
+ for (attr_namespace,attr_name),attr_value in sorted(token["data"].items()):
+ #TODO: Add namespace support here
+ k = attr_name
+ v = attr_value
+ yield self.encodeStrict(u' ')
+
+ yield self.encodeStrict(k)
+ if not self.minimize_boolean_attributes or \
+ (k not in booleanAttributes.get(name, tuple()) \
+ and k not in booleanAttributes.get("", tuple())):
+ yield self.encodeStrict(u"=")
+ if self.quote_attr_values or not v:
+ quote_attr = True
+ else:
+ quote_attr = reduce(lambda x,y: x or (y in v),
+ spaceCharacters + u">\"'=", False)
+ v = v.replace(u"&", u"&")
+ if self.escape_lt_in_attrs: v = v.replace(u"<", u"<")
+ if quote_attr:
+ quote_char = self.quote_char
+ if self.use_best_quote_char:
+ if u"'" in v and u'"' not in v:
+ quote_char = u'"'
+ elif u'"' in v and u"'" not in v:
+ quote_char = u"'"
+ if quote_char == u"'":
+ v = v.replace(u"'", u"'")
+ else:
+ v = v.replace(u'"', u""")
+ yield self.encodeStrict(quote_char)
+ yield self.encode(v)
+ yield self.encodeStrict(quote_char)
+ else:
+ yield self.encode(v)
+ if name in voidElements and self.use_trailing_solidus:
+ if self.space_before_trailing_solidus:
+ yield self.encodeStrict(u" /")
+ else:
+ yield self.encodeStrict(u"/")
+ yield self.encode(u">")
+
+ elif type == "EndTag":
+ name = token["name"]
+ if name in rcdataElements:
+ in_cdata = False
+ elif in_cdata:
+ self.serializeError(_("Unexpected child element of a CDATA element"))
+ yield self.encodeStrict(u"%s>" % name)
+
+ elif type == "Comment":
+ data = token["data"]
+ if data.find("--") >= 0:
+ self.serializeError(_("Comment contains --"))
+ yield self.encodeStrict(u"" % token["data"])
+
+ elif type == "Entity":
+ name = token["name"]
+ key = name + ";"
+ if not key in entities:
+ self.serializeError(_("Entity %s not recognized" % name))
+ if self.resolve_entities and key not in xmlEntities:
+ data = entities[key]
+ else:
+ data = u"&%s;" % name
+ yield self.encodeStrict(data)
+
+ else:
+ self.serializeError(token["data"])
+
+ def render(self, treewalker, encoding=None):
+ if encoding:
+ return "".join(list(self.serialize(treewalker, encoding)))
+ else:
+ return u"".join(list(self.serialize(treewalker)))
+
+ def serializeError(self, data="XXX ERROR MESSAGE NEEDED"):
+ # XXX The idea is to make data mandatory.
+ self.errors.append(data)
+ if self.strict:
+ raise SerializeError
+
+def SerializeError(Exception):
+ """Error in serialized tree"""
+ pass
diff --git a/libs/html5lib/serializer/xhtmlserializer.py b/libs/html5lib/serializer/xhtmlserializer.py
new file mode 100644
index 00000000..7fdce47b
--- /dev/null
+++ b/libs/html5lib/serializer/xhtmlserializer.py
@@ -0,0 +1,9 @@
+from htmlserializer import HTMLSerializer
+
+class XHTMLSerializer(HTMLSerializer):
+ quote_attr_values = True
+ minimize_boolean_attributes = False
+ use_trailing_solidus = True
+ escape_lt_in_attrs = True
+ omit_optional_tags = False
+ escape_rcdata = True
diff --git a/libs/html5lib/tokenizer.py b/libs/html5lib/tokenizer.py
new file mode 100644
index 00000000..7e9eca88
--- /dev/null
+++ b/libs/html5lib/tokenizer.py
@@ -0,0 +1,1744 @@
+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
+try:
+ from collections import deque
+except ImportError:
+ from utils import deque
+
+from constants import spaceCharacters
+from constants import entitiesWindows1252, entities
+from constants import asciiLowercase, asciiLetters, asciiUpper2Lower
+from constants import digits, hexDigits, EOF
+from constants import tokenTypes, tagTokenTypes
+from constants import replacementCharacters
+
+from inputstream import HTMLInputStream
+
+# Group entities by their first character, for faster lookups
+entitiesByFirstChar = {}
+for e in entities:
+ entitiesByFirstChar.setdefault(e[0], []).append(e)
+
+class HTMLTokenizer(object):
+ """ This class takes care of tokenizing HTML.
+
+ * self.currentToken
+ Holds the token that is currently being processed.
+
+ * self.state
+ Holds a reference to the method to be invoked... XXX
+
+ * self.stream
+ Points to HTMLInputStream object.
+ """
+
+ def __init__(self, stream, encoding=None, parseMeta=True, useChardet=True,
+ lowercaseElementName=True, lowercaseAttrName=True, parser=None):
+
+ self.stream = HTMLInputStream(stream, encoding, parseMeta, useChardet)
+ self.parser = parser
+
+ #Perform case conversions?
+ self.lowercaseElementName = lowercaseElementName
+ self.lowercaseAttrName = lowercaseAttrName
+
+ # Setup the initial tokenizer state
+ self.escapeFlag = False
+ self.lastFourChars = []
+ self.state = self.dataState
+ self.escape = False
+
+ # The current token being created
+ self.currentToken = None
+ super(HTMLTokenizer, self).__init__()
+
+ def __iter__(self):
+ """ This is where the magic happens.
+
+ We do our usually processing through the states and when we have a token
+ to return we yield the token which pauses processing until the next token
+ is requested.
+ """
+ self.tokenQueue = deque([])
+ # Start processing. When EOF is reached self.state will return False
+ # instead of True and the loop will terminate.
+ while self.state():
+ while self.stream.errors:
+ yield {"type": tokenTypes["ParseError"], "data": self.stream.errors.pop(0)}
+ while self.tokenQueue:
+ yield self.tokenQueue.popleft()
+
+ def consumeNumberEntity(self, isHex):
+ """This function returns either U+FFFD or the character based on the
+ decimal or hexadecimal representation. It also discards ";" if present.
+ If not present self.tokenQueue.append({"type": tokenTypes["ParseError"]}) is invoked.
+ """
+
+ allowed = digits
+ radix = 10
+ if isHex:
+ allowed = hexDigits
+ radix = 16
+
+ charStack = []
+
+ # Consume all the characters that are in range while making sure we
+ # don't hit an EOF.
+ c = self.stream.char()
+ while c in allowed and c is not EOF:
+ charStack.append(c)
+ c = self.stream.char()
+
+ # Convert the set of characters consumed to an int.
+ charAsInt = int("".join(charStack), radix)
+
+ # Certain characters get replaced with others
+ if charAsInt in replacementCharacters:
+ char = replacementCharacters[charAsInt]
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "illegal-codepoint-for-numeric-entity",
+ "datavars": {"charAsInt": charAsInt}})
+ elif ((0xD800 <= charAsInt <= 0xDFFF) or
+ (charAsInt > 0x10FFFF)):
+ char = u"\uFFFD"
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "illegal-codepoint-for-numeric-entity",
+ "datavars": {"charAsInt": charAsInt}})
+ else:
+ #Should speed up this check somehow (e.g. move the set to a constant)
+ if ((0x0001 <= charAsInt <= 0x0008) or
+ (0x000E <= charAsInt <= 0x001F) or
+ (0x007F <= charAsInt <= 0x009F) or
+ (0xFDD0 <= charAsInt <= 0xFDEF) or
+ charAsInt in frozenset([0x000B, 0xFFFE, 0xFFFF, 0x1FFFE,
+ 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE,
+ 0x3FFFF, 0x4FFFE, 0x4FFFF, 0x5FFFE,
+ 0x5FFFF, 0x6FFFE, 0x6FFFF, 0x7FFFE,
+ 0x7FFFF, 0x8FFFE, 0x8FFFF, 0x9FFFE,
+ 0x9FFFF, 0xAFFFE, 0xAFFFF, 0xBFFFE,
+ 0xBFFFF, 0xCFFFE, 0xCFFFF, 0xDFFFE,
+ 0xDFFFF, 0xEFFFE, 0xEFFFF, 0xFFFFE,
+ 0xFFFFF, 0x10FFFE, 0x10FFFF])):
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data":
+ "illegal-codepoint-for-numeric-entity",
+ "datavars": {"charAsInt": charAsInt}})
+ try:
+ # Try/except needed as UCS-2 Python builds' unichar only works
+ # within the BMP.
+ char = unichr(charAsInt)
+ except ValueError:
+ char = eval("u'\\U%08x'" % charAsInt)
+
+ # Discard the ; if present. Otherwise, put it back on the queue and
+ # invoke parseError on parser.
+ if c != u";":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "numeric-entity-without-semicolon"})
+ self.stream.unget(c)
+
+ return char
+
+ def consumeEntity(self, allowedChar=None, fromAttribute=False):
+ # Initialise to the default output for when no entity is matched
+ output = u"&"
+
+ charStack = [self.stream.char()]
+ if (charStack[0] in spaceCharacters or charStack[0] in (EOF, u"<", u"&")
+ or (allowedChar is not None and allowedChar == charStack[0])):
+ self.stream.unget(charStack[0])
+
+ elif charStack[0] == u"#":
+ # Read the next character to see if it's hex or decimal
+ hex = False
+ charStack.append(self.stream.char())
+ if charStack[-1] in (u"x", u"X"):
+ hex = True
+ charStack.append(self.stream.char())
+
+ # charStack[-1] should be the first digit
+ if (hex and charStack[-1] in hexDigits) \
+ or (not hex and charStack[-1] in digits):
+ # At least one digit found, so consume the whole number
+ self.stream.unget(charStack[-1])
+ output = self.consumeNumberEntity(hex)
+ else:
+ # No digits found
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "expected-numeric-entity"})
+ self.stream.unget(charStack.pop())
+ output = u"&" + u"".join(charStack)
+
+ else:
+ # At this point in the process might have named entity. Entities
+ # are stored in the global variable "entities".
+ #
+ # Consume characters and compare to these to a substring of the
+ # entity names in the list until the substring no longer matches.
+ filteredEntityList = entitiesByFirstChar.get(charStack[0], [])
+
+ def entitiesStartingWith(name):
+ return [e for e in filteredEntityList if e.startswith(name)]
+
+ while (charStack[-1] is not EOF and
+ entitiesStartingWith("".join(charStack))):
+ charStack.append(self.stream.char())
+
+ # At this point we have a string that starts with some characters
+ # that may match an entity
+ entityName = None
+
+ # Try to find the longest entity the string will match to take care
+ # of ¬i for instance.
+ for entityLength in xrange(len(charStack)-1, 1, -1):
+ possibleEntityName = "".join(charStack[:entityLength])
+ if possibleEntityName in entities:
+ entityName = possibleEntityName
+ break
+
+ if entityName is not None:
+ if entityName[-1] != ";":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "named-entity-without-semicolon"})
+ if (entityName[-1] != ";" and fromAttribute and
+ (charStack[entityLength] in asciiLetters or
+ charStack[entityLength] in digits or
+ charStack[entityLength] == "=")):
+ self.stream.unget(charStack.pop())
+ output = u"&" + u"".join(charStack)
+ else:
+ output = entities[entityName]
+ self.stream.unget(charStack.pop())
+ output += u"".join(charStack[entityLength:])
+ else:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-named-entity"})
+ self.stream.unget(charStack.pop())
+ output = u"&" + u"".join(charStack)
+
+ if fromAttribute:
+ self.currentToken["data"][-1][1] += output
+ else:
+ if output in spaceCharacters:
+ tokenType = "SpaceCharacters"
+ else:
+ tokenType = "Characters"
+ self.tokenQueue.append({"type": tokenTypes[tokenType], "data": output})
+
+ def processEntityInAttribute(self, allowedChar):
+ """This method replaces the need for "entityInAttributeValueState".
+ """
+ self.consumeEntity(allowedChar=allowedChar, fromAttribute=True)
+
+ def emitCurrentToken(self):
+ """This method is a generic handler for emitting the tags. It also sets
+ the state to "data" because that's what's needed after a token has been
+ emitted.
+ """
+ token = self.currentToken
+ # Add token to the queue to be yielded
+ if (token["type"] in tagTokenTypes):
+ if self.lowercaseElementName:
+ token["name"] = token["name"].translate(asciiUpper2Lower)
+ if token["type"] == tokenTypes["EndTag"]:
+ if token["data"]:
+ self.tokenQueue.append({"type":tokenTypes["ParseError"],
+ "data":"attributes-in-end-tag"})
+ if token["selfClosing"]:
+ self.tokenQueue.append({"type":tokenTypes["ParseError"],
+ "data":"self-closing-flag-on-end-tag"})
+ self.tokenQueue.append(token)
+ self.state = self.dataState
+
+
+ # Below are the various tokenizer states worked out.
+
+ def dataState(self):
+ data = self.stream.char()
+ if data == "&":
+ self.state = self.entityDataState
+ elif data == "<":
+ self.state = self.tagOpenState
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data":"invalid-codepoint"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": u"\u0000"})
+ elif data is EOF:
+ # Tokenization ends.
+ return False
+ elif data in spaceCharacters:
+ # Directly after emitting a token you switch back to the "data
+ # state". At that point spaceCharacters are important so they are
+ # emitted separately.
+ self.tokenQueue.append({"type": tokenTypes["SpaceCharacters"], "data":
+ data + self.stream.charsUntil(spaceCharacters, True)})
+ # No need to update lastFourChars here, since the first space will
+ # have already been appended to lastFourChars and will have broken
+ # any sequences
+ else:
+ chars = self.stream.charsUntil((u"&", u"<", u"\u0000"))
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data":
+ data + chars})
+ return True
+
+ def entityDataState(self):
+ self.consumeEntity()
+ self.state = self.dataState
+ return True
+
+ def rcdataState(self):
+ data = self.stream.char()
+ if data == "&":
+ self.state = self.characterReferenceInRcdata
+ elif data == "<":
+ self.state = self.rcdataLessThanSignState
+ elif data == EOF:
+ # Tokenization ends.
+ return False
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": u"\uFFFD"})
+ elif data in spaceCharacters:
+ # Directly after emitting a token you switch back to the "data
+ # state". At that point spaceCharacters are important so they are
+ # emitted separately.
+ self.tokenQueue.append({"type": tokenTypes["SpaceCharacters"], "data":
+ data + self.stream.charsUntil(spaceCharacters, True)})
+ # No need to update lastFourChars here, since the first space will
+ # have already been appended to lastFourChars and will have broken
+ # any sequences
+ else:
+ chars = self.stream.charsUntil((u"&", u"<"))
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data":
+ data + chars})
+ return True
+
+ def characterReferenceInRcdata(self):
+ self.consumeEntity()
+ self.state = self.rcdataState
+ return True
+
+ def rawtextState(self):
+ data = self.stream.char()
+ if data == "<":
+ self.state = self.rawtextLessThanSignState
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": u"\uFFFD"})
+ elif data == EOF:
+ # Tokenization ends.
+ return False
+ else:
+ chars = self.stream.charsUntil((u"<", u"\u0000"))
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data":
+ data + chars})
+ return True
+
+ def scriptDataState(self):
+ data = self.stream.char()
+ if data == "<":
+ self.state = self.scriptDataLessThanSignState
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": u"\uFFFD"})
+ elif data == EOF:
+ # Tokenization ends.
+ return False
+ else:
+ chars = self.stream.charsUntil((u"<", u"\u0000"))
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data":
+ data + chars})
+ return True
+
+ def plaintextState(self):
+ data = self.stream.char()
+ if data == EOF:
+ # Tokenization ends.
+ return False
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": u"\uFFFD"})
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data":
+ data + self.stream.charsUntil(u"\u0000")})
+ return True
+
+ def tagOpenState(self):
+ data = self.stream.char()
+ if data == u"!":
+ self.state = self.markupDeclarationOpenState
+ elif data == u"/":
+ self.state = self.closeTagOpenState
+ elif data in asciiLetters:
+ self.currentToken = {"type": tokenTypes["StartTag"],
+ "name": data, "data": [],
+ "selfClosing": False,
+ "selfClosingAcknowledged": False}
+ self.state = self.tagNameState
+ elif data == u">":
+ # XXX In theory it could be something besides a tag name. But
+ # do we really care?
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-tag-name-but-got-right-bracket"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<>"})
+ self.state = self.dataState
+ elif data == u"?":
+ # XXX In theory it could be something besides a tag name. But
+ # do we really care?
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-tag-name-but-got-question-mark"})
+ self.stream.unget(data)
+ self.state = self.bogusCommentState
+ else:
+ # XXX
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-tag-name"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"})
+ self.stream.unget(data)
+ self.state = self.dataState
+ return True
+
+ def closeTagOpenState(self):
+ data = self.stream.char()
+ if data in asciiLetters:
+ self.currentToken = {"type": tokenTypes["EndTag"], "name": data,
+ "data": [], "selfClosing":False}
+ self.state = self.tagNameState
+ elif data == u">":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-closing-tag-but-got-right-bracket"})
+ self.state = self.dataState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-closing-tag-but-got-eof"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u""})
+ self.state = self.dataState
+ else:
+ # XXX data can be _'_...
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-closing-tag-but-got-char",
+ "datavars": {"data": data}})
+ self.stream.unget(data)
+ self.state = self.bogusCommentState
+ return True
+
+ def tagNameState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ self.state = self.beforeAttributeNameState
+ elif data == u">":
+ self.emitCurrentToken()
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-tag-name"})
+ self.state = self.dataState
+ elif data == u"/":
+ self.state = self.selfClosingStartTagState
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["name"] += u"\uFFFD"
+ else:
+ self.currentToken["name"] += data
+ # (Don't use charsUntil here, because tag names are
+ # very short and it's faster to not do anything fancy)
+ return True
+
+ def rcdataLessThanSignState(self):
+ data = self.stream.char()
+ if data == "/":
+ self.temporaryBuffer = ""
+ self.state = self.rcdataEndTagOpenState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"})
+ self.stream.unget(data)
+ self.state = self.rcdataState
+ return True
+
+ def rcdataEndTagOpenState(self):
+ data = self.stream.char()
+ if data in asciiLetters:
+ self.temporaryBuffer += data
+ self.state = self.rcdataEndTagNameState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u""})
+ self.stream.unget(data)
+ self.state = self.rcdataState
+ return True
+
+ def rcdataEndTagNameState(self):
+ appropriate = self.currentToken and self.currentToken["name"].lower() == self.temporaryBuffer.lower()
+ data = self.stream.char()
+ if data in spaceCharacters and appropriate:
+ self.currentToken = {"type": tokenTypes["EndTag"],
+ "name": self.temporaryBuffer,
+ "data": [], "selfClosing":False}
+ self.state = self.beforeAttributeNameState
+ elif data == "/" and appropriate:
+ self.currentToken = {"type": tokenTypes["EndTag"],
+ "name": self.temporaryBuffer,
+ "data": [], "selfClosing":False}
+ self.state = self.selfClosingStartTagState
+ elif data == ">" and appropriate:
+ self.currentToken = {"type": tokenTypes["EndTag"],
+ "name": self.temporaryBuffer,
+ "data": [], "selfClosing":False}
+ self.emitCurrentToken()
+ self.state = self.dataState
+ elif data in asciiLetters:
+ self.temporaryBuffer += data
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": u"" + self.temporaryBuffer})
+ self.stream.unget(data)
+ self.state = self.rcdataState
+ return True
+
+ def rawtextLessThanSignState(self):
+ data = self.stream.char()
+ if data == "/":
+ self.temporaryBuffer = ""
+ self.state = self.rawtextEndTagOpenState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"})
+ self.stream.unget(data)
+ self.state = self.rawtextState
+ return True
+
+ def rawtextEndTagOpenState(self):
+ data = self.stream.char()
+ if data in asciiLetters:
+ self.temporaryBuffer += data
+ self.state = self.rawtextEndTagNameState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u""})
+ self.stream.unget(data)
+ self.state = self.rawtextState
+ return True
+
+ def rawtextEndTagNameState(self):
+ appropriate = self.currentToken and self.currentToken["name"].lower() == self.temporaryBuffer.lower()
+ data = self.stream.char()
+ if data in spaceCharacters and appropriate:
+ self.currentToken = {"type": tokenTypes["EndTag"],
+ "name": self.temporaryBuffer,
+ "data": [], "selfClosing":False}
+ self.state = self.beforeAttributeNameState
+ elif data == "/" and appropriate:
+ self.currentToken = {"type": tokenTypes["EndTag"],
+ "name": self.temporaryBuffer,
+ "data": [], "selfClosing":False}
+ self.state = self.selfClosingStartTagState
+ elif data == ">" and appropriate:
+ self.currentToken = {"type": tokenTypes["EndTag"],
+ "name": self.temporaryBuffer,
+ "data": [], "selfClosing":False}
+ self.emitCurrentToken()
+ self.state = self.dataState
+ elif data in asciiLetters:
+ self.temporaryBuffer += data
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": u"" + self.temporaryBuffer})
+ self.stream.unget(data)
+ self.state = self.rawtextState
+ return True
+
+ def scriptDataLessThanSignState(self):
+ data = self.stream.char()
+ if data == "/":
+ self.temporaryBuffer = ""
+ self.state = self.scriptDataEndTagOpenState
+ elif data == "!":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"" and appropriate:
+ self.currentToken = {"type": tokenTypes["EndTag"],
+ "name": self.temporaryBuffer,
+ "data": [], "selfClosing":False}
+ self.emitCurrentToken()
+ self.state = self.dataState
+ elif data in asciiLetters:
+ self.temporaryBuffer += data
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": u"" + self.temporaryBuffer})
+ self.stream.unget(data)
+ self.state = self.scriptDataState
+ return True
+
+ def scriptDataEscapeStartState(self):
+ data = self.stream.char()
+ if data == "-":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"})
+ self.state = self.scriptDataEscapeStartDashState
+ else:
+ self.stream.unget(data)
+ self.state = self.scriptDataState
+ return True
+
+ def scriptDataEscapeStartDashState(self):
+ data = self.stream.char()
+ if data == "-":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"})
+ self.state = self.scriptDataEscapedDashDashState
+ else:
+ self.stream.unget(data)
+ self.state = self.scriptDataState
+ return True
+
+ def scriptDataEscapedState(self):
+ data = self.stream.char()
+ if data == "-":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"})
+ self.state = self.scriptDataEscapedDashState
+ elif data == "<":
+ self.state = self.scriptDataEscapedLessThanSignState
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": u"\uFFFD"})
+ elif data == EOF:
+ self.state = self.dataState
+ else:
+ chars = self.stream.charsUntil((u"<", u"-", u"\u0000"))
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data":
+ data + chars})
+ return True
+
+ def scriptDataEscapedDashState(self):
+ data = self.stream.char()
+ if data == "-":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"})
+ self.state = self.scriptDataEscapedDashDashState
+ elif data == "<":
+ self.state = self.scriptDataEscapedLessThanSignState
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": u"\uFFFD"})
+ self.state = self.scriptDataEscapedState
+ elif data == EOF:
+ self.state = self.dataState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
+ self.state = self.scriptDataEscapedState
+ return True
+
+ def scriptDataEscapedDashDashState(self):
+ data = self.stream.char()
+ if data == "-":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"})
+ elif data == "<":
+ self.state = self.scriptDataEscapedLessThanSignState
+ elif data == ">":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u">"})
+ self.state = self.scriptDataState
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": u"\uFFFD"})
+ self.state = self.scriptDataEscapedState
+ elif data == EOF:
+ self.state = self.dataState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
+ self.state = self.scriptDataEscapedState
+ return True
+
+ def scriptDataEscapedLessThanSignState(self):
+ data = self.stream.char()
+ if data == "/":
+ self.temporaryBuffer = ""
+ self.state = self.scriptDataEscapedEndTagOpenState
+ elif data in asciiLetters:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<" + data})
+ self.temporaryBuffer = data
+ self.state = self.scriptDataDoubleEscapeStartState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"})
+ self.stream.unget(data)
+ self.state = self.scriptDataEscapedState
+ return True
+
+ def scriptDataEscapedEndTagOpenState(self):
+ data = self.stream.char()
+ if data in asciiLetters:
+ self.temporaryBuffer = data
+ self.state = self.scriptDataEscapedEndTagNameState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u""})
+ self.stream.unget(data)
+ self.state = self.scriptDataEscapedState
+ return True
+
+ def scriptDataEscapedEndTagNameState(self):
+ appropriate = self.currentToken and self.currentToken["name"].lower() == self.temporaryBuffer.lower()
+ data = self.stream.char()
+ if data in spaceCharacters and appropriate:
+ self.currentToken = {"type": tokenTypes["EndTag"],
+ "name": self.temporaryBuffer,
+ "data": [], "selfClosing":False}
+ self.state = self.beforeAttributeNameState
+ elif data == "/" and appropriate:
+ self.currentToken = {"type": tokenTypes["EndTag"],
+ "name": self.temporaryBuffer,
+ "data": [], "selfClosing":False}
+ self.state = self.selfClosingStartTagState
+ elif data == ">" and appropriate:
+ self.currentToken = {"type": tokenTypes["EndTag"],
+ "name": self.temporaryBuffer,
+ "data": [], "selfClosing":False}
+ self.emitCurrentToken()
+ self.state = self.dataState
+ elif data in asciiLetters:
+ self.temporaryBuffer += data
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": u"" + self.temporaryBuffer})
+ self.stream.unget(data)
+ self.state = self.scriptDataEscapedState
+ return True
+
+ def scriptDataDoubleEscapeStartState(self):
+ data = self.stream.char()
+ if data in (spaceCharacters | frozenset(("/", ">"))):
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
+ if self.temporaryBuffer.lower() == "script":
+ self.state = self.scriptDataDoubleEscapedState
+ else:
+ self.state = self.scriptDataEscapedState
+ elif data in asciiLetters:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
+ self.temporaryBuffer += data
+ else:
+ self.stream.unget(data)
+ self.state = self.scriptDataEscapedState
+ return True
+
+ def scriptDataDoubleEscapedState(self):
+ data = self.stream.char()
+ if data == "-":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"})
+ self.state = self.scriptDataDoubleEscapedDashState
+ elif data == "<":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"})
+ self.state = self.scriptDataDoubleEscapedLessThanSignState
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": u"\uFFFD"})
+ elif data == EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-script-in-script"})
+ self.state = self.dataState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
+ return True
+
+ def scriptDataDoubleEscapedDashState(self):
+ data = self.stream.char()
+ if data == "-":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"})
+ self.state = self.scriptDataDoubleEscapedDashDashState
+ elif data == "<":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"})
+ self.state = self.scriptDataDoubleEscapedLessThanSignState
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": u"\uFFFD"})
+ self.state = self.scriptDataDoubleEscapedState
+ elif data == EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-script-in-script"})
+ self.state = self.dataState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
+ self.state = self.scriptDataDoubleEscapedState
+ return True
+
+ def scriptDataDoubleEscapedDashState(self):
+ data = self.stream.char()
+ if data == "-":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"-"})
+ elif data == "<":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"<"})
+ self.state = self.scriptDataDoubleEscapedLessThanSignState
+ elif data == ">":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u">"})
+ self.state = self.scriptDataState
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": u"\uFFFD"})
+ self.state = self.scriptDataDoubleEscapedState
+ elif data == EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-script-in-script"})
+ self.state = self.dataState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
+ self.state = self.scriptDataDoubleEscapedState
+ return True
+
+ def scriptDataDoubleEscapedLessThanSignState(self):
+ data = self.stream.char()
+ if data == "/":
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": u"/"})
+ self.temporaryBuffer = ""
+ self.state = self.scriptDataDoubleEscapeEndState
+ else:
+ self.stream.unget(data)
+ self.state = self.scriptDataDoubleEscapedState
+ return True
+
+ def scriptDataDoubleEscapeEndState(self):
+ data = self.stream.char()
+ if data in (spaceCharacters | frozenset(("/", ">"))):
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
+ if self.temporaryBuffer.lower() == "script":
+ self.state = self.scriptDataEscapedState
+ else:
+ self.state = self.scriptDataDoubleEscapedState
+ elif data in asciiLetters:
+ self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
+ self.temporaryBuffer += data
+ else:
+ self.stream.unget(data)
+ self.state = self.scriptDataDoubleEscapedState
+ return True
+
+ def beforeAttributeNameState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ self.stream.charsUntil(spaceCharacters, True)
+ elif data in asciiLetters:
+ self.currentToken["data"].append([data, ""])
+ self.state = self.attributeNameState
+ elif data == u">":
+ self.emitCurrentToken()
+ elif data == u"/":
+ self.state = self.selfClosingStartTagState
+ elif data in (u"'", u'"', u"=", u"<"):
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "invalid-character-in-attribute-name"})
+ self.currentToken["data"].append([data, ""])
+ self.state = self.attributeNameState
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["data"].append([u"\uFFFD", ""])
+ self.state = self.attributeNameState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-attribute-name-but-got-eof"})
+ self.state = self.dataState
+ else:
+ self.currentToken["data"].append([data, ""])
+ self.state = self.attributeNameState
+ return True
+
+ def attributeNameState(self):
+ data = self.stream.char()
+ leavingThisState = True
+ emitToken = False
+ if data == u"=":
+ self.state = self.beforeAttributeValueState
+ elif data in asciiLetters:
+ self.currentToken["data"][-1][0] += data +\
+ self.stream.charsUntil(asciiLetters, True)
+ leavingThisState = False
+ elif data == u">":
+ # XXX If we emit here the attributes are converted to a dict
+ # without being checked and when the code below runs we error
+ # because data is a dict not a list
+ emitToken = True
+ elif data in spaceCharacters:
+ self.state = self.afterAttributeNameState
+ elif data == u"/":
+ self.state = self.selfClosingStartTagState
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["data"][-1][0] += u"\uFFFD"
+ leavingThisState = False
+ elif data in (u"'", u'"', u"<"):
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data":
+ "invalid-character-in-attribute-name"})
+ self.currentToken["data"][-1][0] += data
+ leavingThisState = False
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "eof-in-attribute-name"})
+ self.state = self.dataState
+ else:
+ self.currentToken["data"][-1][0] += data
+ leavingThisState = False
+
+ if leavingThisState:
+ # Attributes are not dropped at this stage. That happens when the
+ # start tag token is emitted so values can still be safely appended
+ # to attributes, but we do want to report the parse error in time.
+ if self.lowercaseAttrName:
+ self.currentToken["data"][-1][0] = (
+ self.currentToken["data"][-1][0].translate(asciiUpper2Lower))
+ for name, value in self.currentToken["data"][:-1]:
+ if self.currentToken["data"][-1][0] == name:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "duplicate-attribute"})
+ break
+ # XXX Fix for above XXX
+ if emitToken:
+ self.emitCurrentToken()
+ return True
+
+ def afterAttributeNameState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ self.stream.charsUntil(spaceCharacters, True)
+ elif data == u"=":
+ self.state = self.beforeAttributeValueState
+ elif data == u">":
+ self.emitCurrentToken()
+ elif data in asciiLetters:
+ self.currentToken["data"].append([data, ""])
+ self.state = self.attributeNameState
+ elif data == u"/":
+ self.state = self.selfClosingStartTagState
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["data"].append([u"\uFFFD", ""])
+ self.state = self.attributeNameState
+ elif data in (u"'", u'"', u"<"):
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "invalid-character-after-attribute-name"})
+ self.currentToken["data"].append([data, ""])
+ self.state = self.attributeNameState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-end-of-tag-but-got-eof"})
+ self.state = self.dataState
+ else:
+ self.currentToken["data"].append([data, ""])
+ self.state = self.attributeNameState
+ return True
+
+ def beforeAttributeValueState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ self.stream.charsUntil(spaceCharacters, True)
+ elif data == u"\"":
+ self.state = self.attributeValueDoubleQuotedState
+ elif data == u"&":
+ self.state = self.attributeValueUnQuotedState
+ self.stream.unget(data);
+ elif data == u"'":
+ self.state = self.attributeValueSingleQuotedState
+ elif data == u">":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-attribute-value-but-got-right-bracket"})
+ self.emitCurrentToken()
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["data"][-1][1] += u"\uFFFD"
+ self.state = self.attributeValueUnQuotedState
+ elif data in (u"=", u"<", u"`"):
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "equals-in-unquoted-attribute-value"})
+ self.currentToken["data"][-1][1] += data
+ self.state = self.attributeValueUnQuotedState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-attribute-value-but-got-eof"})
+ self.state = self.dataState
+ else:
+ self.currentToken["data"][-1][1] += data
+ self.state = self.attributeValueUnQuotedState
+ return True
+
+ def attributeValueDoubleQuotedState(self):
+ data = self.stream.char()
+ if data == "\"":
+ self.state = self.afterAttributeValueState
+ elif data == u"&":
+ self.processEntityInAttribute(u'"')
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["data"][-1][1] += u"\uFFFD"
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-attribute-value-double-quote"})
+ self.state = self.dataState
+ else:
+ self.currentToken["data"][-1][1] += data +\
+ self.stream.charsUntil(("\"", u"&"))
+ return True
+
+ def attributeValueSingleQuotedState(self):
+ data = self.stream.char()
+ if data == "'":
+ self.state = self.afterAttributeValueState
+ elif data == u"&":
+ self.processEntityInAttribute(u"'")
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["data"][-1][1] += u"\uFFFD"
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-attribute-value-single-quote"})
+ self.state = self.dataState
+ else:
+ self.currentToken["data"][-1][1] += data +\
+ self.stream.charsUntil(("'", u"&"))
+ return True
+
+ def attributeValueUnQuotedState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ self.state = self.beforeAttributeNameState
+ elif data == u"&":
+ self.processEntityInAttribute(">")
+ elif data == u">":
+ self.emitCurrentToken()
+ elif data in (u'"', u"'", u"=", u"<", u"`"):
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-character-in-unquoted-attribute-value"})
+ self.currentToken["data"][-1][1] += data
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["data"][-1][1] += u"\uFFFD"
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-attribute-value-no-quotes"})
+ self.state = self.dataState
+ else:
+ self.currentToken["data"][-1][1] += data + self.stream.charsUntil(
+ frozenset((u"&", u">", u'"', u"'", u"=", u"<", u"`")) | spaceCharacters)
+ return True
+
+ def afterAttributeValueState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ self.state = self.beforeAttributeNameState
+ elif data == u">":
+ self.emitCurrentToken()
+ elif data == u"/":
+ self.state = self.selfClosingStartTagState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-EOF-after-attribute-value"})
+ self.stream.unget(data)
+ self.state = self.dataState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-character-after-attribute-value"})
+ self.stream.unget(data)
+ self.state = self.beforeAttributeNameState
+ return True
+
+ def selfClosingStartTagState(self):
+ data = self.stream.char()
+ if data == ">":
+ self.currentToken["selfClosing"] = True
+ self.emitCurrentToken()
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data":
+ "unexpected-EOF-after-solidus-in-tag"})
+ self.stream.unget(data)
+ self.state = self.dataState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-character-after-soldius-in-tag"})
+ self.stream.unget(data)
+ self.state = self.beforeAttributeNameState
+ return True
+
+ def bogusCommentState(self):
+ # Make a new comment token and give it as value all the characters
+ # until the first > or EOF (charsUntil checks for EOF automatically)
+ # and emit it.
+ data = self.stream.charsUntil(u">")
+ data = data.replace(u"\u0000", u"\uFFFD")
+ self.tokenQueue.append(
+ {"type": tokenTypes["Comment"], "data": data})
+
+ # Eat the character directly after the bogus comment which is either a
+ # ">" or an EOF.
+ self.stream.char()
+ self.state = self.dataState
+ return True
+
+ def markupDeclarationOpenState(self):
+ charStack = [self.stream.char()]
+ if charStack[-1] == u"-":
+ charStack.append(self.stream.char())
+ if charStack[-1] == u"-":
+ self.currentToken = {"type": tokenTypes["Comment"], "data": u""}
+ self.state = self.commentStartState
+ return True
+ elif charStack[-1] in (u'd', u'D'):
+ matched = True
+ for expected in ((u'o', u'O'), (u'c', u'C'), (u't', u'T'),
+ (u'y', u'Y'), (u'p', u'P'), (u'e', u'E')):
+ charStack.append(self.stream.char())
+ if charStack[-1] not in expected:
+ matched = False
+ break
+ if matched:
+ self.currentToken = {"type": tokenTypes["Doctype"],
+ "name": u"",
+ "publicId": None, "systemId": None,
+ "correct": True}
+ self.state = self.doctypeState
+ return True
+ elif (charStack[-1] == "[" and
+ self.parser is not None and
+ self.parser.tree.openElements and
+ self.parser.tree.openElements[-1].namespace != self.parser.tree.defaultNamespace):
+ matched = True
+ for expected in ["C", "D", "A", "T", "A", "["]:
+ charStack.append(self.stream.char())
+ if charStack[-1] != expected:
+ matched = False
+ break
+ if matched:
+ self.state = self.cdataSectionState
+ return True
+
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-dashes-or-doctype"})
+
+ while charStack:
+ self.stream.unget(charStack.pop())
+ self.state = self.bogusCommentState
+ return True
+
+ def commentStartState(self):
+ data = self.stream.char()
+ if data == "-":
+ self.state = self.commentStartDashState
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["data"] += u"\uFFFD"
+ elif data == ">":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "incorrect-comment"})
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-comment"})
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.currentToken["data"] += data
+ self.state = self.commentState
+ return True
+
+ def commentStartDashState(self):
+ data = self.stream.char()
+ if data == "-":
+ self.state = self.commentEndState
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["data"] += u"-\uFFFD"
+ elif data == ">":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "incorrect-comment"})
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-comment"})
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.currentToken["data"] += "-" + data
+ self.state = self.commentState
+ return True
+
+
+ def commentState(self):
+ data = self.stream.char()
+ if data == u"-":
+ self.state = self.commentEndDashState
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["data"] += u"\uFFFD"
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "eof-in-comment"})
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.currentToken["data"] += data + \
+ self.stream.charsUntil((u"-", u"\u0000"))
+ return True
+
+ def commentEndDashState(self):
+ data = self.stream.char()
+ if data == u"-":
+ self.state = self.commentEndState
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["data"] += u"-\uFFFD"
+ self.state = self.commentState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-comment-end-dash"})
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.currentToken["data"] += u"-" + data
+ self.state = self.commentState
+ return True
+
+ def commentEndState(self):
+ data = self.stream.char()
+ if data == u">":
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["data"] += u"--\uFFFD"
+ self.state = self.commentState
+ elif data == "!":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-bang-after-double-dash-in-comment"})
+ self.state = self.commentEndBangState
+ elif data == u"-":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-dash-after-double-dash-in-comment"})
+ self.currentToken["data"] += data
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-comment-double-dash"})
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ # XXX
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-char-in-comment"})
+ self.currentToken["data"] += u"--" + data
+ self.state = self.commentState
+ return True
+
+ def commentEndBangState(self):
+ data = self.stream.char()
+ if data == u">":
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data == u"-":
+ self.currentToken["data"] += "--!"
+ self.state = self.commentEndDashState
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["data"] += u"--!\uFFFD"
+ self.state = self.commentState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-comment-end-bang-state"})
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.currentToken["data"] += u"--!" + data
+ self.state = self.commentState
+ return True
+
+ def doctypeState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ self.state = self.beforeDoctypeNameState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-doctype-name-but-got-eof"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "need-space-after-doctype"})
+ self.stream.unget(data)
+ self.state = self.beforeDoctypeNameState
+ return True
+
+ def beforeDoctypeNameState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ pass
+ elif data == u">":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-doctype-name-but-got-right-bracket"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["name"] = u"\uFFFD"
+ self.state = self.doctypeNameState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-doctype-name-but-got-eof"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.currentToken["name"] = data
+ self.state = self.doctypeNameState
+ return True
+
+ def doctypeNameState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ self.currentToken["name"] = self.currentToken["name"].translate(asciiUpper2Lower)
+ self.state = self.afterDoctypeNameState
+ elif data == u">":
+ self.currentToken["name"] = self.currentToken["name"].translate(asciiUpper2Lower)
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["name"] += u"\uFFFD"
+ self.state = self.doctypeNameState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-doctype-name"})
+ self.currentToken["correct"] = False
+ self.currentToken["name"] = self.currentToken["name"].translate(asciiUpper2Lower)
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.currentToken["name"] += data
+ return True
+
+ def afterDoctypeNameState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ pass
+ elif data == u">":
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data is EOF:
+ self.currentToken["correct"] = False
+ self.stream.unget(data)
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-doctype"})
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ if data in (u"p", u"P"):
+ matched = True
+ for expected in ((u"u", u"U"), (u"b", u"B"), (u"l", u"L"),
+ (u"i", u"I"), (u"c", u"C")):
+ data = self.stream.char()
+ if data not in expected:
+ matched = False
+ break
+ if matched:
+ self.state = self.afterDoctypePublicKeywordState
+ return True
+ elif data in (u"s", u"S"):
+ matched = True
+ for expected in ((u"y", u"Y"), (u"s", u"S"), (u"t", u"T"),
+ (u"e", u"E"), (u"m", u"M")):
+ data = self.stream.char()
+ if data not in expected:
+ matched = False
+ break
+ if matched:
+ self.state = self.afterDoctypeSystemKeywordState
+ return True
+
+ # All the characters read before the current 'data' will be
+ # [a-zA-Z], so they're garbage in the bogus doctype and can be
+ # discarded; only the latest character might be '>' or EOF
+ # and needs to be ungetted
+ self.stream.unget(data)
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "expected-space-or-right-bracket-in-doctype", "datavars":
+ {"data": data}})
+ self.currentToken["correct"] = False
+ self.state = self.bogusDoctypeState
+
+ return True
+
+ def afterDoctypePublicKeywordState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ self.state = self.beforeDoctypePublicIdentifierState
+ elif data in ("'", '"'):
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-char-in-doctype"})
+ self.stream.unget(data)
+ self.state = self.beforeDoctypePublicIdentifierState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.stream.unget(data)
+ self.state = self.beforeDoctypePublicIdentifierState
+ return True
+
+ def beforeDoctypePublicIdentifierState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ pass
+ elif data == "\"":
+ self.currentToken["publicId"] = u""
+ self.state = self.doctypePublicIdentifierDoubleQuotedState
+ elif data == "'":
+ self.currentToken["publicId"] = u""
+ self.state = self.doctypePublicIdentifierSingleQuotedState
+ elif data == ">":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-end-of-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-char-in-doctype"})
+ self.currentToken["correct"] = False
+ self.state = self.bogusDoctypeState
+ return True
+
+ def doctypePublicIdentifierDoubleQuotedState(self):
+ data = self.stream.char()
+ if data == "\"":
+ self.state = self.afterDoctypePublicIdentifierState
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["publicId"] += u"\uFFFD"
+ elif data == ">":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-end-of-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.currentToken["publicId"] += data
+ return True
+
+ def doctypePublicIdentifierSingleQuotedState(self):
+ data = self.stream.char()
+ if data == "'":
+ self.state = self.afterDoctypePublicIdentifierState
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["publicId"] += u"\uFFFD"
+ elif data == ">":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-end-of-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.currentToken["publicId"] += data
+ return True
+
+ def afterDoctypePublicIdentifierState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ self.state = self.betweenDoctypePublicAndSystemIdentifiersState
+ elif data == ">":
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data == '"':
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-char-in-doctype"})
+ self.currentToken["systemId"] = u""
+ self.state = self.doctypeSystemIdentifierDoubleQuotedState
+ elif data == "'":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-char-in-doctype"})
+ self.currentToken["systemId"] = u""
+ self.state = self.doctypeSystemIdentifierSingleQuotedState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-char-in-doctype"})
+ self.currentToken["correct"] = False
+ self.state = self.bogusDoctypeState
+ return True
+
+ def betweenDoctypePublicAndSystemIdentifiersState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ pass
+ elif data == ">":
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data == '"':
+ self.currentToken["systemId"] = u""
+ self.state = self.doctypeSystemIdentifierDoubleQuotedState
+ elif data == "'":
+ self.currentToken["systemId"] = u""
+ self.state = self.doctypeSystemIdentifierSingleQuotedState
+ elif data == EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-char-in-doctype"})
+ self.currentToken["correct"] = False
+ self.state = self.bogusDoctypeState
+ return True
+
+ def afterDoctypeSystemKeywordState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ self.state = self.beforeDoctypeSystemIdentifierState
+ elif data in ("'", '"'):
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-char-in-doctype"})
+ self.stream.unget(data)
+ self.state = self.beforeDoctypeSystemIdentifierState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.stream.unget(data)
+ self.state = self.beforeDoctypeSystemIdentifierState
+ return True
+
+ def beforeDoctypeSystemIdentifierState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ pass
+ elif data == "\"":
+ self.currentToken["systemId"] = u""
+ self.state = self.doctypeSystemIdentifierDoubleQuotedState
+ elif data == "'":
+ self.currentToken["systemId"] = u""
+ self.state = self.doctypeSystemIdentifierSingleQuotedState
+ elif data == ">":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-char-in-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-char-in-doctype"})
+ self.currentToken["correct"] = False
+ self.state = self.bogusDoctypeState
+ return True
+
+ def doctypeSystemIdentifierDoubleQuotedState(self):
+ data = self.stream.char()
+ if data == "\"":
+ self.state = self.afterDoctypeSystemIdentifierState
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["systemId"] += u"\uFFFD"
+ elif data == ">":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-end-of-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.currentToken["systemId"] += data
+ return True
+
+ def doctypeSystemIdentifierSingleQuotedState(self):
+ data = self.stream.char()
+ if data == "'":
+ self.state = self.afterDoctypeSystemIdentifierState
+ elif data == u"\u0000":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ self.currentToken["systemId"] += u"\uFFFD"
+ elif data == ">":
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-end-of-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.currentToken["systemId"] += data
+ return True
+
+ def afterDoctypeSystemIdentifierState(self):
+ data = self.stream.char()
+ if data in spaceCharacters:
+ pass
+ elif data == ">":
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data is EOF:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "eof-in-doctype"})
+ self.currentToken["correct"] = False
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
+ "unexpected-char-in-doctype"})
+ self.state = self.bogusDoctypeState
+ return True
+
+ def bogusDoctypeState(self):
+ data = self.stream.char()
+ if data == u">":
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ elif data is EOF:
+ # XXX EMIT
+ self.stream.unget(data)
+ self.tokenQueue.append(self.currentToken)
+ self.state = self.dataState
+ else:
+ pass
+ return True
+
+ def cdataSectionState(self):
+ data = []
+ while True:
+ data.append(self.stream.charsUntil(u"]"))
+ charStack = []
+
+ for expected in ["]", "]", ">"]:
+ charStack.append(self.stream.char())
+ matched = True
+ if charStack[-1] == EOF:
+ data.extend(charStack[:-1])
+ break
+ elif charStack[-1] != expected:
+ matched = False
+ data.extend(charStack)
+ break
+
+ if matched:
+ break
+ data = "".join(data)
+ #Deal with null here rather than in the parser
+ nullCount = data.count(u"\u0000")
+ if nullCount > 0:
+ for i in xrange(nullCount):
+ self.tokenQueue.append({"type": tokenTypes["ParseError"],
+ "data": "invalid-codepoint"})
+ data = data.replace(u"\u0000", u"\uFFFD")
+ if data:
+ self.tokenQueue.append({"type": tokenTypes["Characters"],
+ "data": data})
+ self.state = self.dataState
+ return True
diff --git a/libs/html5lib/treebuilders/__init__.py b/libs/html5lib/treebuilders/__init__.py
new file mode 100755
index 00000000..14f66d40
--- /dev/null
+++ b/libs/html5lib/treebuilders/__init__.py
@@ -0,0 +1,96 @@
+"""A collection of modules for building different kinds of tree from
+HTML documents.
+
+To create a treebuilder for a new type of tree, you need to do
+implement several things:
+
+1) A set of classes for various types of elements: Document, Doctype,
+Comment, Element. These must implement the interface of
+_base.treebuilders.Node (although comment nodes have a different
+signature for their constructor, see treebuilders.simpletree.Comment)
+Textual content may also be implemented as another node type, or not, as
+your tree implementation requires.
+
+2) A treebuilder object (called TreeBuilder by convention) that
+inherits from treebuilders._base.TreeBuilder. This has 4 required attributes:
+documentClass - the class to use for the bottommost node of a document
+elementClass - the class to use for HTML Elements
+commentClass - the class to use for comments
+doctypeClass - the class to use for doctypes
+It also has one required method:
+getDocument - Returns the root node of the complete document tree
+
+3) If you wish to run the unit tests, you must also create a
+testSerializer method on your treebuilder which accepts a node and
+returns a string containing Node and its children serialized according
+to the format used in the unittests
+
+The supplied simpletree module provides a python-only implementation
+of a full treebuilder and is a useful reference for the semantics of
+the various methods.
+"""
+
+treeBuilderCache = {}
+
+import sys
+
+def getTreeBuilder(treeType, implementation=None, **kwargs):
+ """Get a TreeBuilder class for various types of tree with built-in support
+
+ treeType - the name of the tree type required (case-insensitive). Supported
+ values are "simpletree", "dom", "etree" and "beautifulsoup"
+
+ "simpletree" - a built-in DOM-ish tree type with support for some
+ more pythonic idioms.
+ "dom" - A generic builder for DOM implementations, defaulting to
+ a xml.dom.minidom based implementation for the sake of
+ backwards compatibility (as releases up until 0.10 had a
+ builder called "dom" that was a minidom implemenation).
+ "etree" - A generic builder for tree implementations exposing an
+ elementtree-like interface (known to work with
+ ElementTree, cElementTree and lxml.etree).
+ "beautifulsoup" - Beautiful soup (if installed)
+
+ implementation - (Currently applies to the "etree" and "dom" tree types). A
+ module implementing the tree type e.g.
+ xml.etree.ElementTree or lxml.etree."""
+
+ treeType = treeType.lower()
+ if treeType not in treeBuilderCache:
+ if treeType == "dom":
+ import dom
+ # XXX: Keep backwards compatibility by using minidom if no implementation is given
+ if implementation == None:
+ from xml.dom import minidom
+ implementation = minidom
+ # XXX: NEVER cache here, caching is done in the dom submodule
+ return dom.getDomModule(implementation, **kwargs).TreeBuilder
+ elif treeType == "simpletree":
+ import simpletree
+ treeBuilderCache[treeType] = simpletree.TreeBuilder
+ elif treeType == "beautifulsoup":
+ import soup
+ treeBuilderCache[treeType] = soup.TreeBuilder
+ elif treeType == "lxml":
+ import etree_lxml
+ treeBuilderCache[treeType] = etree_lxml.TreeBuilder
+ elif treeType == "etree":
+ # Come up with a sane default
+ if implementation == None:
+ try:
+ import xml.etree.cElementTree as ET
+ except ImportError:
+ try:
+ import xml.etree.ElementTree as ET
+ except ImportError:
+ try:
+ import cElementTree as ET
+ except ImportError:
+ import elementtree.ElementTree as ET
+ implementation = ET
+ import etree
+ # NEVER cache here, caching is done in the etree submodule
+ return etree.getETreeModule(implementation, **kwargs).TreeBuilder
+ else:
+ raise ValueError("""Unrecognised treebuilder "%s" """%treeType)
+ return treeBuilderCache.get(treeType)
diff --git a/libs/html5lib/treebuilders/_base.py b/libs/html5lib/treebuilders/_base.py
new file mode 100755
index 00000000..f3782d28
--- /dev/null
+++ b/libs/html5lib/treebuilders/_base.py
@@ -0,0 +1,377 @@
+from html5lib.constants import scopingElements, tableInsertModeElements, namespaces
+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
+
+# The scope markers are inserted when entering object elements,
+# marquees, table cells, and table captions, and are used to prevent formatting
+# from "leaking" into tables, object elements, and marquees.
+Marker = None
+
+class Node(object):
+ def __init__(self, name):
+ """Node representing an item in the tree.
+ name - The tag name associated with the node
+ parent - The parent of the current node (or None for the document node)
+ value - The value of the current node (applies to text nodes and
+ comments
+ attributes - a dict holding name, value pairs for attributes of the node
+ childNodes - a list of child nodes of the current node. This must
+ include all elements but not necessarily other node types
+ _flags - A list of miscellaneous flags that can be set on the node
+ """
+ self.name = name
+ self.parent = None
+ self.value = None
+ self.attributes = {}
+ self.childNodes = []
+ self._flags = []
+
+ def __unicode__(self):
+ attributesStr = " ".join(["%s=\"%s\""%(name, value)
+ for name, value in
+ self.attributes.iteritems()])
+ if attributesStr:
+ return "<%s %s>"%(self.name,attributesStr)
+ else:
+ return "<%s>"%(self.name)
+
+ def __repr__(self):
+ return "<%s>" % (self.name)
+
+ def appendChild(self, node):
+ """Insert node as a child of the current node
+ """
+ raise NotImplementedError
+
+ def insertText(self, data, insertBefore=None):
+ """Insert data as text in the current node, positioned before the
+ start of node insertBefore or to the end of the node's text.
+ """
+ raise NotImplementedError
+
+ def insertBefore(self, node, refNode):
+ """Insert node as a child of the current node, before refNode in the
+ list of child nodes. Raises ValueError if refNode is not a child of
+ the current node"""
+ raise NotImplementedError
+
+ def removeChild(self, node):
+ """Remove node from the children of the current node
+ """
+ raise NotImplementedError
+
+ def reparentChildren(self, newParent):
+ """Move all the children of the current node to newParent.
+ This is needed so that trees that don't store text as nodes move the
+ text in the correct way
+ """
+ #XXX - should this method be made more general?
+ for child in self.childNodes:
+ newParent.appendChild(child)
+ self.childNodes = []
+
+ def cloneNode(self):
+ """Return a shallow copy of the current node i.e. a node with the same
+ name and attributes but with no parent or child nodes
+ """
+ raise NotImplementedError
+
+
+ def hasContent(self):
+ """Return true if the node has children or text, false otherwise
+ """
+ raise NotImplementedError
+
+class ActiveFormattingElements(list):
+ def append(self, node):
+ equalCount = 0
+ if node != Marker:
+ for element in self[::-1]:
+ if element == Marker:
+ break
+ if self.nodesEqual(element, node):
+ equalCount += 1
+ if equalCount == 3:
+ self.remove(element)
+ break
+ list.append(self, node)
+
+ def nodesEqual(self, node1, node2):
+ if not node1.nameTuple == node2.nameTuple:
+ return False
+
+ if not node1.attributes == node2.attributes:
+ return False
+
+ return True
+
+class TreeBuilder(object):
+ """Base treebuilder implementation
+ documentClass - the class to use for the bottommost node of a document
+ elementClass - the class to use for HTML Elements
+ commentClass - the class to use for comments
+ doctypeClass - the class to use for doctypes
+ """
+
+ #Document class
+ documentClass = None
+
+ #The class to use for creating a node
+ elementClass = None
+
+ #The class to use for creating comments
+ commentClass = None
+
+ #The class to use for creating doctypes
+ doctypeClass = None
+
+ #Fragment class
+ fragmentClass = None
+
+ def __init__(self, namespaceHTMLElements):
+ if namespaceHTMLElements:
+ self.defaultNamespace = "http://www.w3.org/1999/xhtml"
+ else:
+ self.defaultNamespace = None
+ self.reset()
+
+ def reset(self):
+ self.openElements = []
+ self.activeFormattingElements = ActiveFormattingElements()
+
+ #XXX - rename these to headElement, formElement
+ self.headPointer = None
+ self.formPointer = None
+
+ self.insertFromTable = False
+
+ self.document = self.documentClass()
+
+ def elementInScope(self, target, variant=None):
+
+ #If we pass a node in we match that. if we pass a string
+ #match any node with that name
+ exactNode = hasattr(target, "nameTuple")
+
+ listElementsMap = {
+ None:(scopingElements, False),
+ "button":(scopingElements | set([(namespaces["html"], "button")]), False),
+ "list":(scopingElements | set([(namespaces["html"], "ol"),
+ (namespaces["html"], "ul")]), False),
+ "table":(set([(namespaces["html"], "html"),
+ (namespaces["html"], "table")]), False),
+ "select":(set([(namespaces["html"], "optgroup"),
+ (namespaces["html"], "option")]), True)
+ }
+ listElements, invert = listElementsMap[variant]
+
+ for node in reversed(self.openElements):
+ if (node.name == target and not exactNode or
+ node == target and exactNode):
+ return True
+ elif (invert ^ (node.nameTuple in listElements)):
+ return False
+
+ assert False # We should never reach this point
+
+ def reconstructActiveFormattingElements(self):
+ # Within this algorithm the order of steps described in the
+ # specification is not quite the same as the order of steps in the
+ # code. It should still do the same though.
+
+ # Step 1: stop the algorithm when there's nothing to do.
+ if not self.activeFormattingElements:
+ return
+
+ # Step 2 and step 3: we start with the last element. So i is -1.
+ i = len(self.activeFormattingElements) - 1
+ entry = self.activeFormattingElements[i]
+ if entry == Marker or entry in self.openElements:
+ return
+
+ # Step 6
+ while entry != Marker and entry not in self.openElements:
+ if i == 0:
+ #This will be reset to 0 below
+ i = -1
+ break
+ i -= 1
+ # Step 5: let entry be one earlier in the list.
+ entry = self.activeFormattingElements[i]
+
+ while True:
+ # Step 7
+ i += 1
+
+ # Step 8
+ entry = self.activeFormattingElements[i]
+ clone = entry.cloneNode() #Mainly to get a new copy of the attributes
+
+ # Step 9
+ element = self.insertElement({"type":"StartTag",
+ "name":clone.name,
+ "namespace":clone.namespace,
+ "data":clone.attributes})
+
+ # Step 10
+ self.activeFormattingElements[i] = element
+
+ # Step 11
+ if element == self.activeFormattingElements[-1]:
+ break
+
+ def clearActiveFormattingElements(self):
+ entry = self.activeFormattingElements.pop()
+ while self.activeFormattingElements and entry != Marker:
+ entry = self.activeFormattingElements.pop()
+
+ def elementInActiveFormattingElements(self, name):
+ """Check if an element exists between the end of the active
+ formatting elements and the last marker. If it does, return it, else
+ return false"""
+
+ for item in self.activeFormattingElements[::-1]:
+ # Check for Marker first because if it's a Marker it doesn't have a
+ # name attribute.
+ if item == Marker:
+ break
+ elif item.name == name:
+ return item
+ return False
+
+ def insertRoot(self, token):
+ element = self.createElement(token)
+ self.openElements.append(element)
+ self.document.appendChild(element)
+
+ def insertDoctype(self, token):
+ name = token["name"]
+ publicId = token["publicId"]
+ systemId = token["systemId"]
+
+ doctype = self.doctypeClass(name, publicId, systemId)
+ self.document.appendChild(doctype)
+
+ def insertComment(self, token, parent=None):
+ if parent is None:
+ parent = self.openElements[-1]
+ parent.appendChild(self.commentClass(token["data"]))
+
+ def createElement(self, token):
+ """Create an element but don't insert it anywhere"""
+ name = token["name"]
+ namespace = token.get("namespace", self.defaultNamespace)
+ element = self.elementClass(name, namespace)
+ element.attributes = token["data"]
+ return element
+
+ def _getInsertFromTable(self):
+ return self._insertFromTable
+
+ def _setInsertFromTable(self, value):
+ """Switch the function used to insert an element from the
+ normal one to the misnested table one and back again"""
+ self._insertFromTable = value
+ if value:
+ self.insertElement = self.insertElementTable
+ else:
+ self.insertElement = self.insertElementNormal
+
+ insertFromTable = property(_getInsertFromTable, _setInsertFromTable)
+
+ def insertElementNormal(self, token):
+ name = token["name"]
+ assert type(name) == unicode, "Element %s not unicode"%name
+ namespace = token.get("namespace", self.defaultNamespace)
+ element = self.elementClass(name, namespace)
+ element.attributes = token["data"]
+ self.openElements[-1].appendChild(element)
+ self.openElements.append(element)
+ return element
+
+ def insertElementTable(self, token):
+ """Create an element and insert it into the tree"""
+ element = self.createElement(token)
+ if self.openElements[-1].name not in tableInsertModeElements:
+ return self.insertElementNormal(token)
+ else:
+ #We should be in the InTable mode. This means we want to do
+ #special magic element rearranging
+ parent, insertBefore = self.getTableMisnestedNodePosition()
+ if insertBefore is None:
+ parent.appendChild(element)
+ else:
+ parent.insertBefore(element, insertBefore)
+ self.openElements.append(element)
+ return element
+
+ def insertText(self, data, parent=None):
+ """Insert text data."""
+ if parent is None:
+ parent = self.openElements[-1]
+
+ if (not self.insertFromTable or (self.insertFromTable and
+ self.openElements[-1].name
+ not in tableInsertModeElements)):
+ parent.insertText(data)
+ else:
+ # We should be in the InTable mode. This means we want to do
+ # special magic element rearranging
+ parent, insertBefore = self.getTableMisnestedNodePosition()
+ parent.insertText(data, insertBefore)
+
+ def getTableMisnestedNodePosition(self):
+ """Get the foster parent element, and sibling to insert before
+ (or None) when inserting a misnested table node"""
+ # The foster parent element is the one which comes before the most
+ # recently opened table element
+ # XXX - this is really inelegant
+ lastTable=None
+ fosterParent = None
+ insertBefore = None
+ for elm in self.openElements[::-1]:
+ if elm.name == "table":
+ lastTable = elm
+ break
+ if lastTable:
+ # XXX - we should really check that this parent is actually a
+ # node here
+ if lastTable.parent:
+ fosterParent = lastTable.parent
+ insertBefore = lastTable
+ else:
+ fosterParent = self.openElements[
+ self.openElements.index(lastTable) - 1]
+ else:
+ fosterParent = self.openElements[0]
+ return fosterParent, insertBefore
+
+ def generateImpliedEndTags(self, exclude=None):
+ name = self.openElements[-1].name
+ # XXX td, th and tr are not actually needed
+ if (name in frozenset(("dd", "dt", "li", "option", "optgroup", "p", "rp", "rt"))
+ and name != exclude):
+ self.openElements.pop()
+ # XXX This is not entirely what the specification says. We should
+ # investigate it more closely.
+ self.generateImpliedEndTags(exclude)
+
+ def getDocument(self):
+ "Return the final tree"
+ return self.document
+
+ def getFragment(self):
+ "Return the final fragment"
+ #assert self.innerHTML
+ fragment = self.fragmentClass()
+ self.openElements[0].reparentChildren(fragment)
+ return fragment
+
+ def testSerializer(self, node):
+ """Serialize the subtree of node in the format required by unit tests
+ node - the node from which to start serializing"""
+ raise NotImplementedError
diff --git a/libs/html5lib/treebuilders/dom.py b/libs/html5lib/treebuilders/dom.py
new file mode 100644
index 00000000..9578da2b
--- /dev/null
+++ b/libs/html5lib/treebuilders/dom.py
@@ -0,0 +1,291 @@
+
+from xml.dom import minidom, Node, XML_NAMESPACE, XMLNS_NAMESPACE
+try:
+ from types import ModuleType
+except:
+ from new import module as ModuleType
+import re
+import weakref
+
+import _base
+from html5lib import constants, ihatexml
+from html5lib.constants import namespaces
+
+moduleCache = {}
+
+def getDomModule(DomImplementation):
+ name = "_" + DomImplementation.__name__+"builder"
+ if name in moduleCache:
+ return moduleCache[name]
+ else:
+ mod = ModuleType(name)
+ objs = getDomBuilder(DomImplementation)
+ mod.__dict__.update(objs)
+ moduleCache[name] = mod
+ return mod
+
+def getDomBuilder(DomImplementation):
+ Dom = DomImplementation
+ class AttrList(object):
+ def __init__(self, element):
+ self.element = element
+ def __iter__(self):
+ return self.element.attributes.items().__iter__()
+ def __setitem__(self, name, value):
+ self.element.setAttribute(name, value)
+ def __len__(self):
+ return len(self.element.attributes.items())
+ def items(self):
+ return [(item[0], item[1]) for item in
+ self.element.attributes.items()]
+ def keys(self):
+ return self.element.attributes.keys()
+ def __getitem__(self, name):
+ return self.element.getAttribute(name)
+
+ def __contains__(self, name):
+ if isinstance(name, tuple):
+ raise NotImplementedError
+ else:
+ return self.element.hasAttribute(name)
+
+ class NodeBuilder(_base.Node):
+ def __init__(self, element):
+ _base.Node.__init__(self, element.nodeName)
+ self.element = element
+
+ namespace = property(lambda self:hasattr(self.element, "namespaceURI")
+ and self.element.namespaceURI or None)
+
+ def appendChild(self, node):
+ node.parent = self
+ self.element.appendChild(node.element)
+
+ def insertText(self, data, insertBefore=None):
+ text = self.element.ownerDocument.createTextNode(data)
+ if insertBefore:
+ self.element.insertBefore(text, insertBefore.element)
+ else:
+ self.element.appendChild(text)
+
+ def insertBefore(self, node, refNode):
+ self.element.insertBefore(node.element, refNode.element)
+ node.parent = self
+
+ def removeChild(self, node):
+ if node.element.parentNode == self.element:
+ self.element.removeChild(node.element)
+ node.parent = None
+
+ def reparentChildren(self, newParent):
+ while self.element.hasChildNodes():
+ child = self.element.firstChild
+ self.element.removeChild(child)
+ newParent.element.appendChild(child)
+ self.childNodes = []
+
+ def getAttributes(self):
+ return AttrList(self.element)
+
+ def setAttributes(self, attributes):
+ if attributes:
+ for name, value in attributes.items():
+ if isinstance(name, tuple):
+ if name[0] is not None:
+ qualifiedName = (name[0] + ":" + name[1])
+ else:
+ qualifiedName = name[1]
+ self.element.setAttributeNS(name[2], qualifiedName,
+ value)
+ else:
+ self.element.setAttribute(
+ name, value)
+ attributes = property(getAttributes, setAttributes)
+
+ def cloneNode(self):
+ return NodeBuilder(self.element.cloneNode(False))
+
+ def hasContent(self):
+ return self.element.hasChildNodes()
+
+ def getNameTuple(self):
+ if self.namespace == None:
+ return namespaces["html"], self.name
+ else:
+ return self.namespace, self.name
+
+ nameTuple = property(getNameTuple)
+
+ class TreeBuilder(_base.TreeBuilder):
+ def documentClass(self):
+ self.dom = Dom.getDOMImplementation().createDocument(None,None,None)
+ return weakref.proxy(self)
+
+ def insertDoctype(self, token):
+ name = token["name"]
+ publicId = token["publicId"]
+ systemId = token["systemId"]
+
+ domimpl = Dom.getDOMImplementation()
+ doctype = domimpl.createDocumentType(name, publicId, systemId)
+ self.document.appendChild(NodeBuilder(doctype))
+ if Dom == minidom:
+ doctype.ownerDocument = self.dom
+
+ def elementClass(self, name, namespace=None):
+ if namespace is None and self.defaultNamespace is None:
+ node = self.dom.createElement(name)
+ else:
+ node = self.dom.createElementNS(namespace, name)
+
+ return NodeBuilder(node)
+
+ def commentClass(self, data):
+ return NodeBuilder(self.dom.createComment(data))
+
+ def fragmentClass(self):
+ return NodeBuilder(self.dom.createDocumentFragment())
+
+ def appendChild(self, node):
+ self.dom.appendChild(node.element)
+
+ def testSerializer(self, element):
+ return testSerializer(element)
+
+ def getDocument(self):
+ return self.dom
+
+ def getFragment(self):
+ return _base.TreeBuilder.getFragment(self).element
+
+ def insertText(self, data, parent=None):
+ data=data
+ if parent <> self:
+ _base.TreeBuilder.insertText(self, data, parent)
+ else:
+ # HACK: allow text nodes as children of the document node
+ if hasattr(self.dom, '_child_node_types'):
+ if not Node.TEXT_NODE in self.dom._child_node_types:
+ self.dom._child_node_types=list(self.dom._child_node_types)
+ self.dom._child_node_types.append(Node.TEXT_NODE)
+ self.dom.appendChild(self.dom.createTextNode(data))
+
+ name = None
+
+ def testSerializer(element):
+ element.normalize()
+ rv = []
+ def serializeElement(element, indent=0):
+ if element.nodeType == Node.DOCUMENT_TYPE_NODE:
+ if element.name:
+ if element.publicId or element.systemId:
+ publicId = element.publicId or ""
+ systemId = element.systemId or ""
+ rv.append( """|%s"""%(
+ ' '*indent, element.name, publicId, systemId))
+ else:
+ rv.append("|%s"%(' '*indent, element.name))
+ else:
+ rv.append("|%s"%(' '*indent,))
+ elif element.nodeType == Node.DOCUMENT_NODE:
+ rv.append("#document")
+ elif element.nodeType == Node.DOCUMENT_FRAGMENT_NODE:
+ rv.append("#document-fragment")
+ elif element.nodeType == Node.COMMENT_NODE:
+ rv.append("|%s"%(' '*indent, element.nodeValue))
+ elif element.nodeType == Node.TEXT_NODE:
+ rv.append("|%s\"%s\"" %(' '*indent, element.nodeValue))
+ else:
+ if (hasattr(element, "namespaceURI") and
+ element.namespaceURI != None):
+ name = "%s %s"%(constants.prefixes[element.namespaceURI],
+ element.nodeName)
+ else:
+ name = element.nodeName
+ rv.append("|%s<%s>"%(' '*indent, name))
+ if element.hasAttributes():
+ attributes = []
+ for i in range(len(element.attributes)):
+ attr = element.attributes.item(i)
+ name = attr.nodeName
+ value = attr.value
+ ns = attr.namespaceURI
+ if ns:
+ name = "%s %s"%(constants.prefixes[ns], attr.localName)
+ else:
+ name = attr.nodeName
+ attributes.append((name, value))
+
+ for name, value in sorted(attributes):
+ rv.append('|%s%s="%s"' % (' '*(indent+2), name, value))
+ indent += 2
+ for child in element.childNodes:
+ serializeElement(child, indent)
+ serializeElement(element, 0)
+
+ return "\n".join(rv)
+
+ def dom2sax(node, handler, nsmap={'xml':XML_NAMESPACE}):
+ if node.nodeType == Node.ELEMENT_NODE:
+ if not nsmap:
+ handler.startElement(node.nodeName, node.attributes)
+ for child in node.childNodes: dom2sax(child, handler, nsmap)
+ handler.endElement(node.nodeName)
+ else:
+ attributes = dict(node.attributes.itemsNS())
+
+ # gather namespace declarations
+ prefixes = []
+ for attrname in node.attributes.keys():
+ attr = node.getAttributeNode(attrname)
+ if (attr.namespaceURI == XMLNS_NAMESPACE or
+ (attr.namespaceURI == None and attr.nodeName.startswith('xmlns'))):
+ prefix = (attr.nodeName != 'xmlns' and attr.nodeName or None)
+ handler.startPrefixMapping(prefix, attr.nodeValue)
+ prefixes.append(prefix)
+ nsmap = nsmap.copy()
+ nsmap[prefix] = attr.nodeValue
+ del attributes[(attr.namespaceURI, attr.nodeName)]
+
+ # apply namespace declarations
+ for attrname in node.attributes.keys():
+ attr = node.getAttributeNode(attrname)
+ if attr.namespaceURI == None and ':' in attr.nodeName:
+ prefix = attr.nodeName.split(':')[0]
+ if nsmap.has_key(prefix):
+ del attributes[(attr.namespaceURI, attr.nodeName)]
+ attributes[(nsmap[prefix],attr.nodeName)]=attr.nodeValue
+
+ # SAX events
+ ns = node.namespaceURI or nsmap.get(None,None)
+ handler.startElementNS((ns,node.nodeName), node.nodeName, attributes)
+ for child in node.childNodes: dom2sax(child, handler, nsmap)
+ handler.endElementNS((ns, node.nodeName), node.nodeName)
+ for prefix in prefixes: handler.endPrefixMapping(prefix)
+
+ elif node.nodeType in [Node.TEXT_NODE, Node.CDATA_SECTION_NODE]:
+ handler.characters(node.nodeValue)
+
+ elif node.nodeType == Node.DOCUMENT_NODE:
+ handler.startDocument()
+ for child in node.childNodes: dom2sax(child, handler, nsmap)
+ handler.endDocument()
+
+ elif node.nodeType == Node.DOCUMENT_FRAGMENT_NODE:
+ for child in node.childNodes: dom2sax(child, handler, nsmap)
+
+ else:
+ # ATTRIBUTE_NODE
+ # ENTITY_NODE
+ # PROCESSING_INSTRUCTION_NODE
+ # COMMENT_NODE
+ # DOCUMENT_TYPE_NODE
+ # NOTATION_NODE
+ pass
+
+ return locals()
+
+# Keep backwards compatibility with things that directly load
+# classes/functions from this module
+for key, value in getDomModule(minidom).__dict__.items():
+ globals()[key] = value
diff --git a/libs/html5lib/treebuilders/etree.py b/libs/html5lib/treebuilders/etree.py
new file mode 100755
index 00000000..95be4755
--- /dev/null
+++ b/libs/html5lib/treebuilders/etree.py
@@ -0,0 +1,344 @@
+try:
+ from types import ModuleType
+except:
+ from new import module as ModuleType
+import re
+import types
+
+import _base
+from html5lib import ihatexml
+from html5lib import constants
+from html5lib.constants import namespaces
+
+tag_regexp = re.compile("{([^}]*)}(.*)")
+
+moduleCache = {}
+
+def getETreeModule(ElementTreeImplementation, fullTree=False):
+ name = "_" + ElementTreeImplementation.__name__+"builder"
+ if name in moduleCache:
+ return moduleCache[name]
+ else:
+ mod = ModuleType("_" + ElementTreeImplementation.__name__+"builder")
+ objs = getETreeBuilder(ElementTreeImplementation, fullTree)
+ mod.__dict__.update(objs)
+ moduleCache[name] = mod
+ return mod
+
+def getETreeBuilder(ElementTreeImplementation, fullTree=False):
+ ElementTree = ElementTreeImplementation
+ class Element(_base.Node):
+ def __init__(self, name, namespace=None):
+ self._name = name
+ self._namespace = namespace
+ self._element = ElementTree.Element(self._getETreeTag(name,
+ namespace))
+ if namespace is None:
+ self.nameTuple = namespaces["html"], self._name
+ else:
+ self.nameTuple = self._namespace, self._name
+ self.parent = None
+ self._childNodes = []
+ self._flags = []
+
+ def _getETreeTag(self, name, namespace):
+ if namespace is None:
+ etree_tag = name
+ else:
+ etree_tag = "{%s}%s"%(namespace, name)
+ return etree_tag
+
+ def _setName(self, name):
+ self._name = name
+ self._element.tag = self._getETreeTag(self._name, self._namespace)
+
+ def _getName(self):
+ return self._name
+
+ name = property(_getName, _setName)
+
+ def _setNamespace(self, namespace):
+ self._namespace = namespace
+ self._element.tag = self._getETreeTag(self._name, self._namespace)
+
+ def _getNamespace(self):
+ return self._namespace
+
+ namespace = property(_getNamespace, _setNamespace)
+
+ def _getAttributes(self):
+ return self._element.attrib
+
+ def _setAttributes(self, attributes):
+ #Delete existing attributes first
+ #XXX - there may be a better way to do this...
+ for key in self._element.attrib.keys():
+ del self._element.attrib[key]
+ for key, value in attributes.iteritems():
+ if isinstance(key, tuple):
+ name = "{%s}%s"%(key[2], key[1])
+ else:
+ name = key
+ self._element.set(name, value)
+
+ attributes = property(_getAttributes, _setAttributes)
+
+ def _getChildNodes(self):
+ return self._childNodes
+ def _setChildNodes(self, value):
+ del self._element[:]
+ self._childNodes = []
+ for element in value:
+ self.insertChild(element)
+
+ childNodes = property(_getChildNodes, _setChildNodes)
+
+ def hasContent(self):
+ """Return true if the node has children or text"""
+ return bool(self._element.text or len(self._element))
+
+ def appendChild(self, node):
+ self._childNodes.append(node)
+ self._element.append(node._element)
+ node.parent = self
+
+ def insertBefore(self, node, refNode):
+ index = list(self._element).index(refNode._element)
+ self._element.insert(index, node._element)
+ node.parent = self
+
+ def removeChild(self, node):
+ self._element.remove(node._element)
+ node.parent=None
+
+ def insertText(self, data, insertBefore=None):
+ if not(len(self._element)):
+ if not self._element.text:
+ self._element.text = ""
+ self._element.text += data
+ elif insertBefore is None:
+ #Insert the text as the tail of the last child element
+ if not self._element[-1].tail:
+ self._element[-1].tail = ""
+ self._element[-1].tail += data
+ else:
+ #Insert the text before the specified node
+ children = list(self._element)
+ index = children.index(insertBefore._element)
+ if index > 0:
+ if not self._element[index-1].tail:
+ self._element[index-1].tail = ""
+ self._element[index-1].tail += data
+ else:
+ if not self._element.text:
+ self._element.text = ""
+ self._element.text += data
+
+ def cloneNode(self):
+ element = type(self)(self.name, self.namespace)
+ for name, value in self.attributes.iteritems():
+ element.attributes[name] = value
+ return element
+
+ def reparentChildren(self, newParent):
+ if newParent.childNodes:
+ newParent.childNodes[-1]._element.tail += self._element.text
+ else:
+ if not newParent._element.text:
+ newParent._element.text = ""
+ if self._element.text is not None:
+ newParent._element.text += self._element.text
+ self._element.text = ""
+ _base.Node.reparentChildren(self, newParent)
+
+ class Comment(Element):
+ def __init__(self, data):
+ #Use the superclass constructor to set all properties on the
+ #wrapper element
+ self._element = ElementTree.Comment(data)
+ self.parent = None
+ self._childNodes = []
+ self._flags = []
+
+ def _getData(self):
+ return self._element.text
+
+ def _setData(self, value):
+ self._element.text = value
+
+ data = property(_getData, _setData)
+
+ class DocumentType(Element):
+ def __init__(self, name, publicId, systemId):
+ Element.__init__(self, "")
+ self._element.text = name
+ self.publicId = publicId
+ self.systemId = systemId
+
+ def _getPublicId(self):
+ return self._element.get(u"publicId", "")
+
+ def _setPublicId(self, value):
+ if value is not None:
+ self._element.set(u"publicId", value)
+
+ publicId = property(_getPublicId, _setPublicId)
+
+ def _getSystemId(self):
+ return self._element.get(u"systemId", "")
+
+ def _setSystemId(self, value):
+ if value is not None:
+ self._element.set(u"systemId", value)
+
+ systemId = property(_getSystemId, _setSystemId)
+
+ class Document(Element):
+ def __init__(self):
+ Element.__init__(self, "")
+
+ class DocumentFragment(Element):
+ def __init__(self):
+ Element.__init__(self, "")
+
+ def testSerializer(element):
+ rv = []
+ finalText = None
+ def serializeElement(element, indent=0):
+ if not(hasattr(element, "tag")):
+ element = element.getroot()
+ if element.tag == "":
+ if element.get("publicId") or element.get("systemId"):
+ publicId = element.get("publicId") or ""
+ systemId = element.get("systemId") or ""
+ rv.append( """"""%(
+ element.text, publicId, systemId))
+ else:
+ rv.append(""%(element.text,))
+ elif element.tag == "":
+ rv.append("#document")
+ if element.text:
+ rv.append("|%s\"%s\""%(' '*(indent+2), element.text))
+ if element.tail:
+ finalText = element.tail
+ elif element.tag == ElementTree.Comment:
+ rv.append("|%s"%(' '*indent, element.text))
+ else:
+ assert type(element.tag) in types.StringTypes, "Expected unicode, got %s"%type(element.tag)
+ nsmatch = tag_regexp.match(element.tag)
+
+ if nsmatch is None:
+ name = element.tag
+ else:
+ ns, name = nsmatch.groups()
+ prefix = constants.prefixes[ns]
+ name = "%s %s"%(prefix, name)
+ rv.append("|%s<%s>"%(' '*indent, name))
+
+ if hasattr(element, "attrib"):
+ attributes = []
+ for name, value in element.attrib.iteritems():
+ nsmatch = tag_regexp.match(name)
+ if nsmatch is not None:
+ ns, name = nsmatch.groups()
+ prefix = constants.prefixes[ns]
+ attr_string = "%s %s"%(prefix, name)
+ else:
+ attr_string = name
+ attributes.append((attr_string, value))
+
+ for name, value in sorted(attributes):
+ rv.append('|%s%s="%s"' % (' '*(indent+2), name, value))
+ if element.text:
+ rv.append("|%s\"%s\"" %(' '*(indent+2), element.text))
+ indent += 2
+ for child in element:
+ serializeElement(child, indent)
+ if element.tail:
+ rv.append("|%s\"%s\"" %(' '*(indent-2), element.tail))
+ serializeElement(element, 0)
+
+ if finalText is not None:
+ rv.append("|%s\"%s\""%(' '*2, finalText))
+
+ return "\n".join(rv)
+
+ def tostring(element):
+ """Serialize an element and its child nodes to a string"""
+ rv = []
+ finalText = None
+ filter = ihatexml.InfosetFilter()
+ def serializeElement(element):
+ if type(element) == type(ElementTree.ElementTree):
+ element = element.getroot()
+
+ if element.tag == "":
+ if element.get("publicId") or element.get("systemId"):
+ publicId = element.get("publicId") or ""
+ systemId = element.get("systemId") or ""
+ rv.append( """"""%(
+ element.text, publicId, systemId))
+ else:
+ rv.append(""%(element.text,))
+ elif element.tag == "":
+ if element.text:
+ rv.append(element.text)
+ if element.tail:
+ finalText = element.tail
+
+ for child in element:
+ serializeElement(child)
+
+ elif type(element.tag) == type(ElementTree.Comment):
+ rv.append(""%(element.text,))
+ else:
+ #This is assumed to be an ordinary element
+ if not element.attrib:
+ rv.append("<%s>"%(filter.fromXmlName(element.tag),))
+ else:
+ attr = " ".join(["%s=\"%s\""%(
+ filter.fromXmlName(name), value)
+ for name, value in element.attrib.iteritems()])
+ rv.append("<%s %s>"%(element.tag, attr))
+ if element.text:
+ rv.append(element.text)
+
+ for child in element:
+ serializeElement(child)
+
+ rv.append("%s>"%(element.tag,))
+
+ if element.tail:
+ rv.append(element.tail)
+
+ serializeElement(element)
+
+ if finalText is not None:
+ rv.append("%s\""%(' '*2, finalText))
+
+ return "".join(rv)
+
+ class TreeBuilder(_base.TreeBuilder):
+ documentClass = Document
+ doctypeClass = DocumentType
+ elementClass = Element
+ commentClass = Comment
+ fragmentClass = DocumentFragment
+
+ def testSerializer(self, element):
+ return testSerializer(element)
+
+ def getDocument(self):
+ if fullTree:
+ return self.document._element
+ else:
+ if self.defaultNamespace is not None:
+ return self.document._element.find(
+ "{%s}html"%self.defaultNamespace)
+ else:
+ return self.document._element.find("html")
+
+ def getFragment(self):
+ return _base.TreeBuilder.getFragment(self)._element
+
+ return locals()
diff --git a/libs/html5lib/treebuilders/etree_lxml.py b/libs/html5lib/treebuilders/etree_lxml.py
new file mode 100644
index 00000000..eee1e3b2
--- /dev/null
+++ b/libs/html5lib/treebuilders/etree_lxml.py
@@ -0,0 +1,336 @@
+import warnings
+import re
+
+import _base
+from html5lib.constants import DataLossWarning
+import html5lib.constants as constants
+import etree as etree_builders
+from html5lib import ihatexml
+
+try:
+ import lxml.etree as etree
+except ImportError:
+ pass
+
+fullTree = True
+tag_regexp = re.compile("{([^}]*)}(.*)")
+
+"""Module for supporting the lxml.etree library. The idea here is to use as much
+of the native library as possible, without using fragile hacks like custom element
+names that break between releases. The downside of this is that we cannot represent
+all possible trees; specifically the following are known to cause problems:
+
+Text or comments as siblings of the root element
+Docypes with no name
+
+When any of these things occur, we emit a DataLossWarning
+"""
+
+class DocumentType(object):
+ def __init__(self, name, publicId, systemId):
+ self.name = name
+ self.publicId = publicId
+ self.systemId = systemId
+
+class Document(object):
+ def __init__(self):
+ self._elementTree = None
+ self._childNodes = []
+
+ def appendChild(self, element):
+ self._elementTree.getroot().addnext(element._element)
+
+ def _getChildNodes(self):
+ return self._childNodes
+
+ childNodes = property(_getChildNodes)
+
+def testSerializer(element):
+ rv = []
+ finalText = None
+ filter = ihatexml.InfosetFilter()
+ def serializeElement(element, indent=0):
+ if not hasattr(element, "tag"):
+ if hasattr(element, "getroot"):
+ #Full tree case
+ rv.append("#document")
+ if element.docinfo.internalDTD:
+ if not (element.docinfo.public_id or
+ element.docinfo.system_url):
+ dtd_str = ""%element.docinfo.root_name
+ else:
+ dtd_str = """"""%(
+ element.docinfo.root_name,
+ element.docinfo.public_id,
+ element.docinfo.system_url)
+ rv.append("|%s%s"%(' '*(indent+2), dtd_str))
+ next_element = element.getroot()
+ while next_element.getprevious() is not None:
+ next_element = next_element.getprevious()
+ while next_element is not None:
+ serializeElement(next_element, indent+2)
+ next_element = next_element.getnext()
+ elif isinstance(element, basestring):
+ #Text in a fragment
+ rv.append("|%s\"%s\""%(' '*indent, element))
+ else:
+ #Fragment case
+ rv.append("#document-fragment")
+ for next_element in element:
+ serializeElement(next_element, indent+2)
+ elif type(element.tag) == type(etree.Comment):
+ rv.append("|%s"%(' '*indent, element.text))
+ else:
+ nsmatch = etree_builders.tag_regexp.match(element.tag)
+ if nsmatch is not None:
+ ns = nsmatch.group(1)
+ tag = nsmatch.group(2)
+ prefix = constants.prefixes[ns]
+ rv.append("|%s<%s %s>"%(' '*indent, prefix,
+ filter.fromXmlName(tag)))
+ else:
+ rv.append("|%s<%s>"%(' '*indent,
+ filter.fromXmlName(element.tag)))
+
+ if hasattr(element, "attrib"):
+ attributes = []
+ for name, value in element.attrib.iteritems():
+ nsmatch = tag_regexp.match(name)
+ if nsmatch is not None:
+ ns, name = nsmatch.groups()
+ name = filter.fromXmlName(name)
+ prefix = constants.prefixes[ns]
+ attr_string = "%s %s"%(prefix, name)
+ else:
+ attr_string = filter.fromXmlName(name)
+ attributes.append((attr_string, value))
+
+ for name, value in sorted(attributes):
+ rv.append('|%s%s="%s"' % (' '*(indent+2), name, value))
+
+ if element.text:
+ rv.append("|%s\"%s\"" %(' '*(indent+2), element.text))
+ indent += 2
+ for child in element.getchildren():
+ serializeElement(child, indent)
+ if hasattr(element, "tail") and element.tail:
+ rv.append("|%s\"%s\"" %(' '*(indent-2), element.tail))
+ serializeElement(element, 0)
+
+ if finalText is not None:
+ rv.append("|%s\"%s\""%(' '*2, finalText))
+
+ return "\n".join(rv)
+
+def tostring(element):
+ """Serialize an element and its child nodes to a string"""
+ rv = []
+ finalText = None
+ def serializeElement(element):
+ if not hasattr(element, "tag"):
+ if element.docinfo.internalDTD:
+ if element.docinfo.doctype:
+ dtd_str = element.docinfo.doctype
+ else:
+ dtd_str = ""%element.docinfo.root_name
+ rv.append(dtd_str)
+ serializeElement(element.getroot())
+
+ elif type(element.tag) == type(etree.Comment):
+ rv.append(""%(element.text,))
+
+ else:
+ #This is assumed to be an ordinary element
+ if not element.attrib:
+ rv.append("<%s>"%(element.tag,))
+ else:
+ attr = " ".join(["%s=\"%s\""%(name, value)
+ for name, value in element.attrib.iteritems()])
+ rv.append("<%s %s>"%(element.tag, attr))
+ if element.text:
+ rv.append(element.text)
+
+ for child in element.getchildren():
+ serializeElement(child)
+
+ rv.append("%s>"%(element.tag,))
+
+ if hasattr(element, "tail") and element.tail:
+ rv.append(element.tail)
+
+ serializeElement(element)
+
+ if finalText is not None:
+ rv.append("%s\""%(' '*2, finalText))
+
+ return "".join(rv)
+
+
+class TreeBuilder(_base.TreeBuilder):
+ documentClass = Document
+ doctypeClass = DocumentType
+ elementClass = None
+ commentClass = None
+ fragmentClass = Document
+
+ def __init__(self, namespaceHTMLElements, fullTree = False):
+ builder = etree_builders.getETreeModule(etree, fullTree=fullTree)
+ filter = self.filter = ihatexml.InfosetFilter()
+ self.namespaceHTMLElements = namespaceHTMLElements
+
+ class Attributes(dict):
+ def __init__(self, element, value={}):
+ self._element = element
+ dict.__init__(self, value)
+ for key, value in self.iteritems():
+ if isinstance(key, tuple):
+ name = "{%s}%s"%(key[2], filter.coerceAttribute(key[1]))
+ else:
+ name = filter.coerceAttribute(key)
+ self._element._element.attrib[name] = value
+
+ def __setitem__(self, key, value):
+ dict.__setitem__(self, key, value)
+ if isinstance(key, tuple):
+ name = "{%s}%s"%(key[2], filter.coerceAttribute(key[1]))
+ else:
+ name = filter.coerceAttribute(key)
+ self._element._element.attrib[name] = value
+
+ class Element(builder.Element):
+ def __init__(self, name, namespace):
+ name = filter.coerceElement(name)
+ builder.Element.__init__(self, name, namespace=namespace)
+ self._attributes = Attributes(self)
+
+ def _setName(self, name):
+ self._name = filter.coerceElement(name)
+ self._element.tag = self._getETreeTag(
+ self._name, self._namespace)
+
+ def _getName(self):
+ return filter.fromXmlName(self._name)
+
+ name = property(_getName, _setName)
+
+ def _getAttributes(self):
+ return self._attributes
+
+ def _setAttributes(self, attributes):
+ self._attributes = Attributes(self, attributes)
+
+ attributes = property(_getAttributes, _setAttributes)
+
+ def insertText(self, data, insertBefore=None):
+ data = filter.coerceCharacters(data)
+ builder.Element.insertText(self, data, insertBefore)
+
+ def appendChild(self, child):
+ builder.Element.appendChild(self, child)
+
+
+ class Comment(builder.Comment):
+ def __init__(self, data):
+ data = filter.coerceComment(data)
+ builder.Comment.__init__(self, data)
+
+ def _setData(self, data):
+ data = filter.coerceComment(data)
+ self._element.text = data
+
+ def _getData(self):
+ return self._element.text
+
+ data = property(_getData, _setData)
+
+ self.elementClass = Element
+ self.commentClass = builder.Comment
+ #self.fragmentClass = builder.DocumentFragment
+ _base.TreeBuilder.__init__(self, namespaceHTMLElements)
+
+ def reset(self):
+ _base.TreeBuilder.reset(self)
+ self.insertComment = self.insertCommentInitial
+ self.initial_comments = []
+ self.doctype = None
+
+ def testSerializer(self, element):
+ return testSerializer(element)
+
+ def getDocument(self):
+ if fullTree:
+ return self.document._elementTree
+ else:
+ return self.document._elementTree.getroot()
+
+ def getFragment(self):
+ fragment = []
+ element = self.openElements[0]._element
+ if element.text:
+ fragment.append(element.text)
+ fragment.extend(element.getchildren())
+ if element.tail:
+ fragment.append(element.tail)
+ return fragment
+
+ def insertDoctype(self, token):
+ name = token["name"]
+ publicId = token["publicId"]
+ systemId = token["systemId"]
+
+ if not name or ihatexml.nonXmlNameBMPRegexp.search(name) or name[0] == '"':
+ warnings.warn("lxml cannot represent null or non-xml doctype", DataLossWarning)
+
+ doctype = self.doctypeClass(name, publicId, systemId)
+ self.doctype = doctype
+
+ def insertCommentInitial(self, data, parent=None):
+ self.initial_comments.append(data)
+
+ def insertRoot(self, token):
+ """Create the document root"""
+ #Because of the way libxml2 works, it doesn't seem to be possible to
+ #alter information like the doctype after the tree has been parsed.
+ #Therefore we need to use the built-in parser to create our iniial
+ #tree, after which we can add elements like normal
+ docStr = ""
+ if self.doctype and self.doctype.name and not self.doctype.name.startswith('"'):
+ docStr += ""
+ docStr += ""
+
+ try:
+ root = etree.fromstring(docStr)
+ except etree.XMLSyntaxError:
+ print docStr
+ raise
+
+ #Append the initial comments:
+ for comment_token in self.initial_comments:
+ root.addprevious(etree.Comment(comment_token["data"]))
+
+ #Create the root document and add the ElementTree to it
+ self.document = self.documentClass()
+ self.document._elementTree = root.getroottree()
+
+ # Give the root element the right name
+ name = token["name"]
+ namespace = token.get("namespace", self.defaultNamespace)
+ if namespace is None:
+ etree_tag = name
+ else:
+ etree_tag = "{%s}%s"%(namespace, name)
+ root.tag = etree_tag
+
+ #Add the root element to the internal child/open data structures
+ root_element = self.elementClass(name, namespace)
+ root_element._element = root
+ self.document._childNodes.append(root_element)
+ self.openElements.append(root_element)
+
+ #Reset to the default insert comment function
+ self.insertComment = super(TreeBuilder, self).insertComment
diff --git a/libs/html5lib/treebuilders/simpletree.py b/libs/html5lib/treebuilders/simpletree.py
new file mode 100755
index 00000000..67fe7583
--- /dev/null
+++ b/libs/html5lib/treebuilders/simpletree.py
@@ -0,0 +1,256 @@
+import _base
+from html5lib.constants import voidElements, namespaces, prefixes
+from xml.sax.saxutils import escape
+
+# Really crappy basic implementation of a DOM-core like thing
+class Node(_base.Node):
+ type = -1
+ def __init__(self, name):
+ self.name = name
+ self.parent = None
+ self.value = None
+ self.childNodes = []
+ self._flags = []
+
+ def __iter__(self):
+ for node in self.childNodes:
+ yield node
+ for item in node:
+ yield item
+
+ def __unicode__(self):
+ return self.name
+
+ def toxml(self):
+ raise NotImplementedError
+
+ def printTree(self, indent=0):
+ tree = '\n|%s%s' % (' '* indent, unicode(self))
+ for child in self.childNodes:
+ tree += child.printTree(indent + 2)
+ return tree
+
+ def appendChild(self, node):
+ assert isinstance(node, Node)
+ if (isinstance(node, TextNode) and self.childNodes and
+ isinstance(self.childNodes[-1], TextNode)):
+ self.childNodes[-1].value += node.value
+ else:
+ self.childNodes.append(node)
+ node.parent = self
+
+ def insertText(self, data, insertBefore=None):
+ assert isinstance(data, unicode), "data %s is of type %s expected unicode"%(repr(data), type(data))
+ if insertBefore is None:
+ self.appendChild(TextNode(data))
+ else:
+ self.insertBefore(TextNode(data), insertBefore)
+
+ def insertBefore(self, node, refNode):
+ index = self.childNodes.index(refNode)
+ if (isinstance(node, TextNode) and index > 0 and
+ isinstance(self.childNodes[index - 1], TextNode)):
+ self.childNodes[index - 1].value += node.value
+ else:
+ self.childNodes.insert(index, node)
+ node.parent = self
+
+ def removeChild(self, node):
+ try:
+ self.childNodes.remove(node)
+ except:
+ # XXX
+ raise
+ node.parent = None
+
+ def cloneNode(self):
+ raise NotImplementedError
+
+ def hasContent(self):
+ """Return true if the node has children or text"""
+ return bool(self.childNodes)
+
+ def getNameTuple(self):
+ if self.namespace == None:
+ return namespaces["html"], self.name
+ else:
+ return self.namespace, self.name
+
+ nameTuple = property(getNameTuple)
+
+class Document(Node):
+ type = 1
+ def __init__(self):
+ Node.__init__(self, None)
+
+ def __str__(self):
+ return "#document"
+
+ def __unicode__(self):
+ return str(self)
+
+ def appendChild(self, child):
+ Node.appendChild(self, child)
+
+ def toxml(self, encoding="utf=8"):
+ result = ""
+ for child in self.childNodes:
+ result += child.toxml()
+ return result.encode(encoding)
+
+ def hilite(self, encoding="utf-8"):
+ result = "
"
+ for child in self.childNodes:
+ result += child.hilite()
+ return result.encode(encoding) + "
"
+
+ def printTree(self):
+ tree = unicode(self)
+ for child in self.childNodes:
+ tree += child.printTree(2)
+ return tree
+
+ def cloneNode(self):
+ return Document()
+
+class DocumentFragment(Document):
+ type = 2
+ def __str__(self):
+ return "#document-fragment"
+
+ def __unicode__(self):
+ return str(self)
+
+ def cloneNode(self):
+ return DocumentFragment()
+
+class DocumentType(Node):
+ type = 3
+ def __init__(self, name, publicId, systemId):
+ Node.__init__(self, name)
+ self.publicId = publicId
+ self.systemId = systemId
+
+ def __unicode__(self):
+ if self.publicId or self.systemId:
+ publicId = self.publicId or ""
+ systemId = self.systemId or ""
+ return """"""%(
+ self.name, publicId, systemId)
+
+ else:
+ return u"" % self.name
+
+
+ toxml = __unicode__
+
+ def hilite(self):
+ return '<!DOCTYPE %s>' % self.name
+
+ def cloneNode(self):
+ return DocumentType(self.name, self.publicId, self.systemId)
+
+class TextNode(Node):
+ type = 4
+ def __init__(self, value):
+ Node.__init__(self, None)
+ self.value = value
+
+ def __unicode__(self):
+ return u"\"%s\"" % self.value
+
+ def toxml(self):
+ return escape(self.value)
+
+ hilite = toxml
+
+ def cloneNode(self):
+ return TextNode(self.value)
+
+class Element(Node):
+ type = 5
+ def __init__(self, name, namespace=None):
+ Node.__init__(self, name)
+ self.namespace = namespace
+ self.attributes = {}
+
+ def __unicode__(self):
+ if self.namespace == None:
+ return u"<%s>" % self.name
+ else:
+ return u"<%s %s>"%(prefixes[self.namespace], self.name)
+
+ def toxml(self):
+ result = '<' + self.name
+ if self.attributes:
+ for name,value in self.attributes.iteritems():
+ result += u' %s="%s"' % (name, escape(value,{'"':'"'}))
+ if self.childNodes:
+ result += '>'
+ for child in self.childNodes:
+ result += child.toxml()
+ result += u'%s>' % self.name
+ else:
+ result += u'/>'
+ return result
+
+ def hilite(self):
+ result = '<%s' % self.name
+ if self.attributes:
+ for name, value in self.attributes.iteritems():
+ result += ' %s="%s"' % (name, escape(value, {'"':'"'}))
+ if self.childNodes:
+ result += ">"
+ for child in self.childNodes:
+ result += child.hilite()
+ elif self.name in voidElements:
+ return result + ">"
+ return result + '</%s>' % self.name
+
+ def printTree(self, indent):
+ tree = '\n|%s%s' % (' '*indent, unicode(self))
+ indent += 2
+ if self.attributes:
+ for name, value in sorted(self.attributes.iteritems()):
+ if isinstance(name, tuple):
+ name = "%s %s"%(name[0], name[1])
+ tree += '\n|%s%s="%s"' % (' ' * indent, name, value)
+ for child in self.childNodes:
+ tree += child.printTree(indent)
+ return tree
+
+ def cloneNode(self):
+ newNode = Element(self.name)
+ if hasattr(self, 'namespace'):
+ newNode.namespace = self.namespace
+ for attr, value in self.attributes.iteritems():
+ newNode.attributes[attr] = value
+ return newNode
+
+class CommentNode(Node):
+ type = 6
+ def __init__(self, data):
+ Node.__init__(self, None)
+ self.data = data
+
+ def __unicode__(self):
+ return "" % self.data
+
+ def toxml(self):
+ return "" % self.data
+
+ def hilite(self):
+ return '<!--%s-->' % escape(self.data)
+
+ def cloneNode(self):
+ return CommentNode(self.data)
+
+class TreeBuilder(_base.TreeBuilder):
+ documentClass = Document
+ doctypeClass = DocumentType
+ elementClass = Element
+ commentClass = CommentNode
+ fragmentClass = DocumentFragment
+
+ def testSerializer(self, node):
+ return node.printTree()
diff --git a/libs/html5lib/treebuilders/soup.py b/libs/html5lib/treebuilders/soup.py
new file mode 100644
index 00000000..9bc5ff0e
--- /dev/null
+++ b/libs/html5lib/treebuilders/soup.py
@@ -0,0 +1,236 @@
+import warnings
+
+warnings.warn("BeautifulSoup 3.x (as of 3.1) is not fully compatible with html5lib and support will be removed in the future", DeprecationWarning)
+
+from BeautifulSoup import BeautifulSoup, Tag, NavigableString, Comment, Declaration
+
+import _base
+from html5lib.constants import namespaces, DataLossWarning
+
+class AttrList(object):
+ def __init__(self, element):
+ self.element = element
+ self.attrs = dict(self.element.attrs)
+ def __iter__(self):
+ return self.attrs.items().__iter__()
+ def __setitem__(self, name, value):
+ "set attr", name, value
+ self.element[name] = value
+ def items(self):
+ return self.attrs.items()
+ def keys(self):
+ return self.attrs.keys()
+ def __getitem__(self, name):
+ return self.attrs[name]
+ def __contains__(self, name):
+ return name in self.attrs.keys()
+ def __eq__(self, other):
+ if len(self.keys()) != len(other.keys()):
+ return False
+ for item in self.keys():
+ if item not in other:
+ return False
+ if self[item] != other[item]:
+ return False
+ return True
+
+class Element(_base.Node):
+ def __init__(self, element, soup, namespace):
+ _base.Node.__init__(self, element.name)
+ self.element = element
+ self.soup = soup
+ self.namespace = namespace
+
+ def _nodeIndex(self, node, refNode):
+ # Finds a node by identity rather than equality
+ for index in range(len(self.element.contents)):
+ if id(self.element.contents[index]) == id(refNode.element):
+ return index
+ return None
+
+ 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
+ # (TODO: This has O(n^2) performance, for input like "aaa...")
+ newStr = NavigableString(self.element.contents[-1]+node.element)
+
+ # Remove the old text node
+ # (Can't simply use .extract() by itself, because it fails if
+ # an equal text node exists within the parent node)
+ oldElement = self.element.contents[-1]
+ del self.element.contents[-1]
+ oldElement.parent = None
+ oldElement.extract()
+
+ self.element.insert(len(self.element.contents), newStr)
+ else:
+ self.element.insert(len(self.element.contents), node.element)
+ node.parent = self
+
+ def getAttributes(self):
+ return AttrList(self.element)
+
+ def setAttributes(self, attributes):
+ if attributes:
+ for name, value in attributes.items():
+ self.element[name] = value
+
+ attributes = property(getAttributes, setAttributes)
+
+ def insertText(self, data, insertBefore=None):
+ text = TextNode(NavigableString(data), self.soup)
+ if insertBefore:
+ self.insertBefore(text, insertBefore)
+ else:
+ self.appendChild(text)
+
+ def insertBefore(self, node, refNode):
+ index = self._nodeIndex(node, refNode)
+ if (node.element.__class__ == NavigableString and self.element.contents
+ and self.element.contents[index-1].__class__ == NavigableString):
+ # (See comments in appendChild)
+ newStr = NavigableString(self.element.contents[index-1]+node.element)
+ oldNode = self.element.contents[index-1]
+ del self.element.contents[index-1]
+ oldNode.parent = None
+ oldNode.extract()
+
+ self.element.insert(index-1, newStr)
+ else:
+ self.element.insert(index, node.element)
+ node.parent = self
+
+ def removeChild(self, node):
+ index = self._nodeIndex(node.parent, node)
+ del node.parent.element.contents[index]
+ node.element.parent = None
+ node.element.extract()
+ node.parent = None
+
+ 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):
+ node = Element(Tag(self.soup, self.element.name), 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):
+ _base.Node.__init__(self, None)
+ self.element = element
+ self.soup = soup
+
+ def cloneNode(self):
+ raise NotImplementedError
+
+class TreeBuilder(_base.TreeBuilder):
+ def __init__(self, namespaceHTMLElements):
+ if namespaceHTMLElements:
+ warnings.warn("BeautifulSoup cannot represent elements in any namespace", DataLossWarning)
+ _base.TreeBuilder.__init__(self, namespaceHTMLElements)
+
+ def documentClass(self):
+ self.soup = BeautifulSoup("")
+ return Element(self.soup, self.soup, None)
+
+ def insertDoctype(self, token):
+ name = token["name"]
+ publicId = token["publicId"]
+ systemId = token["systemId"]
+
+ if publicId:
+ self.soup.insert(0, Declaration("DOCTYPE %s PUBLIC \"%s\" \"%s\""%(name, publicId, systemId or "")))
+ elif systemId:
+ self.soup.insert(0, Declaration("DOCTYPE %s SYSTEM \"%s\""%
+ (name, systemId)))
+ else:
+ self.soup.insert(0, Declaration("DOCTYPE %s"%name))
+
+ def elementClass(self, name, namespace):
+ if namespace is not None:
+ warnings.warn("BeautifulSoup cannot represent elements in any namespace", DataLossWarning)
+ return Element(Tag(self.soup, name), 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):
+ self.soup.insert(len(self.soup.contents), node.element)
+
+ def testSerializer(self, element):
+ return testSerializer(element)
+
+ def getDocument(self):
+ return self.soup
+
+ def getFragment(self):
+ return _base.TreeBuilder.getFragment(self).element
+
+def testSerializer(element):
+ import re
+ rv = []
+ def serializeElement(element, indent=0):
+ if isinstance(element, Declaration):
+ doctype_regexp = r'DOCTYPE\s+(?P[^\s]*)( PUBLIC "(?P.*)" "(?P.*)"| SYSTEM "(?P.*)")?'
+ m = re.compile(doctype_regexp).match(element.string)
+ assert m is not None, "DOCTYPE did not match expected format"
+ name = m.group('name')
+ publicId = m.group('publicId')
+ if publicId is not None:
+ systemId = m.group('systemId1') or ""
+ else:
+ systemId = m.group('systemId2')
+
+ if publicId is not None or systemId is not None:
+ rv.append("""|%s"""%
+ (' '*indent, name, publicId or "", systemId or ""))
+ else:
+ rv.append("|%s"%(' '*indent, name))
+
+ elif isinstance(element, BeautifulSoup):
+ if element.name == "[document_fragment]":
+ rv.append("#document-fragment")
+ else:
+ rv.append("#document")
+
+ elif isinstance(element, Comment):
+ rv.append("|%s"%(' '*indent, element.string))
+ elif isinstance(element, unicode):
+ rv.append("|%s\"%s\"" %(' '*indent, element))
+ else:
+ rv.append("|%s<%s>"%(' '*indent, element.name))
+ if element.attrs:
+ for name, value in sorted(element.attrs):
+ rv.append('|%s%s="%s"' % (' '*(indent+2), name, value))
+ indent += 2
+ if hasattr(element, "contents"):
+ for child in element.contents:
+ serializeElement(child, indent)
+ serializeElement(element, 0)
+
+ return "\n".join(rv)
diff --git a/libs/html5lib/treewalkers/__init__.py b/libs/html5lib/treewalkers/__init__.py
new file mode 100644
index 00000000..3a606a8b
--- /dev/null
+++ b/libs/html5lib/treewalkers/__init__.py
@@ -0,0 +1,52 @@
+"""A collection of modules for iterating through different kinds of
+tree, generating tokens identical to those produced by the tokenizer
+module.
+
+To create a tree walker for a new type of tree, you need to do
+implement a tree walker object (called TreeWalker by convention) that
+implements a 'serialize' method taking a tree as sole argument and
+returning an iterator generating tokens.
+"""
+
+treeWalkerCache = {}
+
+def getTreeWalker(treeType, implementation=None, **kwargs):
+ """Get a TreeWalker class for various types of tree with built-in support
+
+ treeType - the name of the tree type required (case-insensitive). Supported
+ values are "simpletree", "dom", "etree" and "beautifulsoup"
+
+ "simpletree" - a built-in DOM-ish tree type with support for some
+ more pythonic idioms.
+ "dom" - The xml.dom.minidom DOM implementation
+ "pulldom" - The xml.dom.pulldom event stream
+ "etree" - A generic walker for tree implementations exposing an
+ elementtree-like interface (known to work with
+ ElementTree, cElementTree and lxml.etree).
+ "lxml" - Optimized walker for lxml.etree
+ "beautifulsoup" - Beautiful soup (if installed)
+ "genshi" - a Genshi stream
+
+ implementation - (Currently applies to the "etree" tree type only). A module
+ implementing the tree type e.g. xml.etree.ElementTree or
+ cElementTree."""
+
+ treeType = treeType.lower()
+ if treeType not in treeWalkerCache:
+ if treeType in ("dom", "pulldom", "simpletree"):
+ mod = __import__(treeType, globals())
+ treeWalkerCache[treeType] = mod.TreeWalker
+ elif treeType == "genshi":
+ import genshistream
+ treeWalkerCache[treeType] = genshistream.TreeWalker
+ elif treeType == "beautifulsoup":
+ import soup
+ treeWalkerCache[treeType] = soup.TreeWalker
+ elif treeType == "lxml":
+ import lxmletree
+ treeWalkerCache[treeType] = lxmletree.TreeWalker
+ elif treeType == "etree":
+ import etree
+ # XXX: NEVER cache here, caching is done in the etree submodule
+ return etree.getETreeModule(implementation, **kwargs).TreeWalker
+ return treeWalkerCache.get(treeType)
diff --git a/libs/html5lib/treewalkers/_base.py b/libs/html5lib/treewalkers/_base.py
new file mode 100644
index 00000000..5929ba05
--- /dev/null
+++ b/libs/html5lib/treewalkers/_base.py
@@ -0,0 +1,176 @@
+import gettext
+_ = gettext.gettext
+
+from html5lib.constants import voidElements, spaceCharacters
+spaceCharacters = u"".join(spaceCharacters)
+
+class TreeWalker(object):
+ def __init__(self, tree):
+ self.tree = tree
+
+ def __iter__(self):
+ raise NotImplementedError
+
+ def error(self, msg):
+ return {"type": "SerializeError", "data": msg}
+
+ def normalizeAttrs(self, attrs):
+ newattrs = {}
+ if attrs:
+ #TODO: treewalkers should always have attrs
+ for (namespace,name),value in attrs.iteritems():
+ namespace = unicode(namespace) if namespace else None
+ name = unicode(name)
+ value = unicode(value)
+ newattrs[(namespace,name)] = value
+ return newattrs
+
+ def emptyTag(self, namespace, name, attrs, hasChildren=False):
+ yield {"type": "EmptyTag", "name": unicode(name),
+ "namespace":unicode(namespace),
+ "data": self.normalizeAttrs(attrs)}
+ if hasChildren:
+ yield self.error(_("Void element has children"))
+
+ def startTag(self, namespace, name, attrs):
+ return {"type": "StartTag",
+ "name": unicode(name),
+ "namespace":unicode(namespace),
+ "data": self.normalizeAttrs(attrs)}
+
+ def endTag(self, namespace, name):
+ return {"type": "EndTag",
+ "name": unicode(name),
+ "namespace":unicode(namespace),
+ "data": {}}
+
+ def text(self, data):
+ data = unicode(data)
+ middle = data.lstrip(spaceCharacters)
+ left = data[:len(data)-len(middle)]
+ if left:
+ yield {"type": "SpaceCharacters", "data": left}
+ data = middle
+ middle = data.rstrip(spaceCharacters)
+ right = data[len(middle):]
+ if middle:
+ yield {"type": "Characters", "data": middle}
+ if right:
+ yield {"type": "SpaceCharacters", "data": right}
+
+ def comment(self, data):
+ return {"type": "Comment", "data": unicode(data)}
+
+ def doctype(self, name, publicId=None, systemId=None, correct=True):
+ return {"type": "Doctype",
+ "name": name is not None and unicode(name) or u"",
+ "publicId": publicId,
+ "systemId": systemId,
+ "correct": correct}
+
+ def entity(self, name):
+ return {"type": "Entity", "name": unicode(name)}
+
+ def unknown(self, nodeType):
+ return self.error(_("Unknown node type: ") + nodeType)
+
+class RecursiveTreeWalker(TreeWalker):
+ def walkChildren(self, node):
+ raise NodeImplementedError
+
+ def element(self, node, namespace, name, attrs, hasChildren):
+ if name in voidElements:
+ for token in self.emptyTag(namespace, name, attrs, hasChildren):
+ yield token
+ else:
+ yield self.startTag(name, attrs)
+ if hasChildren:
+ for token in self.walkChildren(node):
+ yield token
+ yield self.endTag(name)
+
+from xml.dom import Node
+
+DOCUMENT = Node.DOCUMENT_NODE
+DOCTYPE = Node.DOCUMENT_TYPE_NODE
+TEXT = Node.TEXT_NODE
+ELEMENT = Node.ELEMENT_NODE
+COMMENT = Node.COMMENT_NODE
+ENTITY = Node.ENTITY_NODE
+UNKNOWN = "<#UNKNOWN#>"
+
+class NonRecursiveTreeWalker(TreeWalker):
+ def getNodeDetails(self, node):
+ raise NotImplementedError
+
+ def getFirstChild(self, node):
+ raise NotImplementedError
+
+ def getNextSibling(self, node):
+ raise NotImplementedError
+
+ def getParentNode(self, node):
+ raise NotImplementedError
+
+ def __iter__(self):
+ currentNode = self.tree
+ while currentNode is not None:
+ details = self.getNodeDetails(currentNode)
+ type, details = details[0], details[1:]
+ hasChildren = False
+ endTag = None
+
+ if type == DOCTYPE:
+ yield self.doctype(*details)
+
+ elif type == TEXT:
+ for token in self.text(*details):
+ yield token
+
+ elif type == ELEMENT:
+ namespace, name, attributes, hasChildren = details
+ if name in voidElements:
+ for token in self.emptyTag(namespace, name, attributes,
+ hasChildren):
+ yield token
+ hasChildren = False
+ else:
+ endTag = name
+ yield self.startTag(namespace, name, attributes)
+
+ elif type == COMMENT:
+ yield self.comment(details[0])
+
+ elif type == ENTITY:
+ yield self.entity(details[0])
+
+ elif type == DOCUMENT:
+ hasChildren = True
+
+ else:
+ yield self.unknown(details[0])
+
+ if hasChildren:
+ firstChild = self.getFirstChild(currentNode)
+ else:
+ firstChild = None
+
+ if firstChild is not None:
+ currentNode = firstChild
+ else:
+ while currentNode is not None:
+ details = self.getNodeDetails(currentNode)
+ type, details = details[0], details[1:]
+ if type == ELEMENT:
+ namespace, name, attributes, hasChildren = details
+ if name not in voidElements:
+ yield self.endTag(namespace, name)
+ if self.tree is currentNode:
+ currentNode = None
+ break
+ nextSibling = self.getNextSibling(currentNode)
+ if nextSibling is not None:
+ currentNode = nextSibling
+ break
+ else:
+ currentNode = self.getParentNode(currentNode)
diff --git a/libs/html5lib/treewalkers/dom.py b/libs/html5lib/treewalkers/dom.py
new file mode 100644
index 00000000..383b46cb
--- /dev/null
+++ b/libs/html5lib/treewalkers/dom.py
@@ -0,0 +1,41 @@
+from xml.dom import Node
+
+import gettext
+_ = gettext.gettext
+
+import _base
+from html5lib.constants import voidElements
+
+class TreeWalker(_base.NonRecursiveTreeWalker):
+ def getNodeDetails(self, node):
+ if node.nodeType == Node.DOCUMENT_TYPE_NODE:
+ return _base.DOCTYPE, node.name, node.publicId, node.systemId
+
+ elif node.nodeType in (Node.TEXT_NODE, Node.CDATA_SECTION_NODE):
+ return _base.TEXT, node.nodeValue
+
+ elif node.nodeType == Node.ELEMENT_NODE:
+ attrs = {}
+ for attr in node.attributes.keys():
+ attr = node.getAttributeNode(attr)
+ attrs[(attr.namespaceURI,attr.localName)] = attr.value
+ return (_base.ELEMENT, node.namespaceURI, node.nodeName,
+ attrs, node.hasChildNodes())
+
+ elif node.nodeType == Node.COMMENT_NODE:
+ return _base.COMMENT, node.nodeValue
+
+ elif node.nodeType in (Node.DOCUMENT_NODE, Node.DOCUMENT_FRAGMENT_NODE):
+ return (_base.DOCUMENT,)
+
+ else:
+ return _base.UNKNOWN, node.nodeType
+
+ def getFirstChild(self, node):
+ return node.firstChild
+
+ def getNextSibling(self, node):
+ return node.nextSibling
+
+ def getParentNode(self, node):
+ return node.parentNode
diff --git a/libs/html5lib/treewalkers/etree.py b/libs/html5lib/treewalkers/etree.py
new file mode 100644
index 00000000..13b03194
--- /dev/null
+++ b/libs/html5lib/treewalkers/etree.py
@@ -0,0 +1,141 @@
+import gettext
+_ = gettext.gettext
+
+try:
+ from types import ModuleType
+except:
+ from new import module as ModuleType
+import copy
+import re
+
+import _base
+from html5lib.constants import voidElements
+
+tag_regexp = re.compile("{([^}]*)}(.*)")
+
+moduleCache = {}
+
+def getETreeModule(ElementTreeImplementation):
+ name = "_" + ElementTreeImplementation.__name__+"builder"
+ if name in moduleCache:
+ return moduleCache[name]
+ else:
+ mod = ModuleType("_" + ElementTreeImplementation.__name__+"builder")
+ objs = getETreeBuilder(ElementTreeImplementation)
+ mod.__dict__.update(objs)
+ moduleCache[name] = mod
+ return mod
+
+def getETreeBuilder(ElementTreeImplementation):
+ ElementTree = ElementTreeImplementation
+
+ class TreeWalker(_base.NonRecursiveTreeWalker):
+ """Given the particular ElementTree representation, this implementation,
+ to avoid using recursion, returns "nodes" as tuples with the following
+ content:
+
+ 1. The current element
+
+ 2. The index of the element relative to its parent
+
+ 3. A stack of ancestor elements
+
+ 4. A flag "text", "tail" or None to indicate if the current node is a
+ text node; either the text or tail of the current element (1)
+ """
+ def getNodeDetails(self, node):
+ if isinstance(node, tuple): # It might be the root Element
+ elt, key, parents, flag = node
+ if flag in ("text", "tail"):
+ return _base.TEXT, getattr(elt, flag)
+ else:
+ node = elt
+
+ if not(hasattr(node, "tag")):
+ node = node.getroot()
+
+ if node.tag in ("", ""):
+ return (_base.DOCUMENT,)
+
+ elif node.tag == "":
+ return (_base.DOCTYPE, node.text,
+ node.get("publicId"), node.get("systemId"))
+
+ elif node.tag == ElementTree.Comment:
+ return _base.COMMENT, node.text
+
+ else:
+ assert type(node.tag) in (str, unicode), type(node.tag)
+ #This is assumed to be an ordinary element
+ match = tag_regexp.match(node.tag)
+ if match:
+ namespace, tag = match.groups()
+ else:
+ namespace = None
+ tag = node.tag
+ attrs = {}
+ for name, value in node.attrib.items():
+ match = tag_regexp.match(name)
+ if match:
+ attrs[(match.group(1),match.group(2))] = value
+ else:
+ attrs[(None,name)] = value
+ return (_base.ELEMENT, namespace, tag,
+ attrs, len(node) or node.text)
+
+ def getFirstChild(self, node):
+ if isinstance(node, tuple):
+ element, key, parents, flag = node
+ else:
+ element, key, parents, flag = node, None, [], None
+
+ if flag in ("text", "tail"):
+ return None
+ else:
+ if element.text:
+ return element, key, parents, "text"
+ elif len(element):
+ parents.append(element)
+ return element[0], 0, parents, None
+ else:
+ return None
+
+ def getNextSibling(self, node):
+ if isinstance(node, tuple):
+ element, key, parents, flag = node
+ else:
+ return None
+
+ if flag == "text":
+ if len(element):
+ parents.append(element)
+ return element[0], 0, parents, None
+ else:
+ return None
+ else:
+ if element.tail and flag != "tail":
+ return element, key, parents, "tail"
+ elif key < len(parents[-1]) - 1:
+ return parents[-1][key+1], key+1, parents, None
+ else:
+ return None
+
+ def getParentNode(self, node):
+ if isinstance(node, tuple):
+ element, key, parents, flag = node
+ else:
+ return None
+
+ if flag == "text":
+ if not parents:
+ return element
+ else:
+ return element, key, parents, None
+ else:
+ parent = parents.pop()
+ if not parents:
+ return parent
+ else:
+ return parent, list(parents[-1]).index(parent), parents, None
+
+ return locals()
diff --git a/libs/html5lib/treewalkers/genshistream.py b/libs/html5lib/treewalkers/genshistream.py
new file mode 100644
index 00000000..ef71a83e
--- /dev/null
+++ b/libs/html5lib/treewalkers/genshistream.py
@@ -0,0 +1,70 @@
+from genshi.core import START, END, XML_NAMESPACE, DOCTYPE, TEXT
+from genshi.core import START_NS, END_NS, START_CDATA, END_CDATA, PI, COMMENT
+from genshi.output import NamespaceFlattener
+
+import _base
+
+from html5lib.constants import voidElements
+
+class TreeWalker(_base.TreeWalker):
+ def __iter__(self):
+ depth = 0
+ ignore_until = None
+ previous = None
+ for event in self.tree:
+ if previous is not None:
+ if previous[0] == START:
+ depth += 1
+ if ignore_until <= depth:
+ ignore_until = None
+ if ignore_until is None:
+ for token in self.tokens(previous, event):
+ yield token
+ if token["type"] == "EmptyTag":
+ ignore_until = depth
+ if previous[0] == END:
+ depth -= 1
+ previous = event
+ if previous is not None:
+ if ignore_until is None or ignore_until <= depth:
+ for token in self.tokens(previous, None):
+ yield token
+ elif ignore_until is not None:
+ raise ValueError("Illformed DOM event stream: void element without END_ELEMENT")
+
+ def tokens(self, event, next):
+ kind, data, pos = event
+ if kind == START:
+ tag, attrib = data
+ name = tag.localname
+ namespace = tag.namespace
+ if tag in voidElements:
+ for token in self.emptyTag(namespace, name, list(attrib),
+ not next or next[0] != END
+ or next[1] != tag):
+ yield token
+ else:
+ yield self.startTag(namespace, name, list(attrib))
+
+ elif kind == END:
+ name = data.localname
+ namespace = data.namespace
+ if name not in voidElements:
+ yield self.endTag(namespace, name)
+
+ elif kind == COMMENT:
+ yield self.comment(data)
+
+ elif kind == TEXT:
+ for token in self.text(data):
+ yield token
+
+ elif kind == DOCTYPE:
+ yield self.doctype(*data)
+
+ elif kind in (XML_NAMESPACE, DOCTYPE, START_NS, END_NS, \
+ START_CDATA, END_CDATA, PI):
+ pass
+
+ else:
+ yield self.unknown(kind)
diff --git a/libs/html5lib/treewalkers/lxmletree.py b/libs/html5lib/treewalkers/lxmletree.py
new file mode 100644
index 00000000..46f4908c
--- /dev/null
+++ b/libs/html5lib/treewalkers/lxmletree.py
@@ -0,0 +1,186 @@
+from lxml import etree
+from html5lib.treebuilders.etree import tag_regexp
+
+from gettext import gettext
+_ = gettext
+
+import _base
+
+from html5lib.constants import voidElements
+from html5lib import ihatexml
+
+class Root(object):
+ def __init__(self, et):
+ self.elementtree = et
+ self.children = []
+ if et.docinfo.internalDTD:
+ self.children.append(Doctype(self, et.docinfo.root_name,
+ et.docinfo.public_id,
+ et.docinfo.system_url))
+ root = et.getroot()
+ node = root
+
+ while node.getprevious() is not None:
+ node = node.getprevious()
+ while node is not None:
+ self.children.append(node)
+ node = node.getnext()
+
+ self.text = None
+ self.tail = None
+
+ def __getitem__(self, key):
+ return self.children[key]
+
+ def getnext(self):
+ return None
+
+ def __len__(self):
+ return 1
+
+class Doctype(object):
+ def __init__(self, root_node, name, public_id, system_id):
+ self.root_node = root_node
+ self.name = name
+ self.public_id = public_id
+ self.system_id = system_id
+
+ self.text = None
+ self.tail = None
+
+ def getnext(self):
+ return self.root_node.children[1]
+
+class FragmentRoot(Root):
+ def __init__(self, children):
+ self.children = [FragmentWrapper(self, child) for child in children]
+ self.text = self.tail = None
+
+ def getnext(self):
+ return None
+
+class FragmentWrapper(object):
+ def __init__(self, fragment_root, obj):
+ self.root_node = fragment_root
+ self.obj = obj
+ if hasattr(self.obj, 'text'):
+ self.text = self.obj.text
+ else:
+ self.text = None
+ if hasattr(self.obj, 'tail'):
+ self.tail = self.obj.tail
+ else:
+ self.tail = None
+ self.isstring = isinstance(obj, basestring)
+
+ def __getattr__(self, name):
+ return getattr(self.obj, name)
+
+ def getnext(self):
+ siblings = self.root_node.children
+ idx = siblings.index(self)
+ if idx < len(siblings) - 1:
+ return siblings[idx + 1]
+ else:
+ return None
+
+ def __getitem__(self, key):
+ return self.obj[key]
+
+ def __nonzero__(self):
+ return bool(self.obj)
+
+ def getparent(self):
+ return None
+
+ def __str__(self):
+ return str(self.obj)
+
+ def __unicode__(self):
+ return unicode(self.obj)
+
+ def __len__(self):
+ return len(self.obj)
+
+
+class TreeWalker(_base.NonRecursiveTreeWalker):
+ def __init__(self, tree):
+ if hasattr(tree, "getroot"):
+ tree = Root(tree)
+ elif isinstance(tree, list):
+ tree = FragmentRoot(tree)
+ _base.NonRecursiveTreeWalker.__init__(self, tree)
+ self.filter = ihatexml.InfosetFilter()
+ def getNodeDetails(self, node):
+ if isinstance(node, tuple): # Text node
+ node, key = node
+ assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key
+ return _base.TEXT, getattr(node, key)
+
+ elif isinstance(node, Root):
+ return (_base.DOCUMENT,)
+
+ elif isinstance(node, Doctype):
+ return _base.DOCTYPE, node.name, node.public_id, node.system_id
+
+ elif isinstance(node, FragmentWrapper) and node.isstring:
+ return _base.TEXT, node
+
+ elif node.tag == etree.Comment:
+ return _base.COMMENT, node.text
+
+ elif node.tag == etree.Entity:
+ return _base.ENTITY, node.text[1:-1] # strip &;
+
+ else:
+ #This is assumed to be an ordinary element
+ match = tag_regexp.match(node.tag)
+ if match:
+ namespace, tag = match.groups()
+ else:
+ namespace = None
+ tag = node.tag
+ attrs = {}
+ for name, value in node.attrib.items():
+ match = tag_regexp.match(name)
+ if match:
+ attrs[(match.group(1),match.group(2))] = value
+ else:
+ attrs[(None,name)] = value
+ return (_base.ELEMENT, namespace, self.filter.fromXmlName(tag),
+ attrs, len(node) > 0 or node.text)
+
+ def getFirstChild(self, node):
+ assert not isinstance(node, tuple), _("Text nodes have no children")
+
+ assert len(node) or node.text, "Node has no children"
+ if node.text:
+ return (node, "text")
+ else:
+ return node[0]
+
+ def getNextSibling(self, node):
+ if isinstance(node, tuple): # Text node
+ node, key = node
+ assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key
+ if key == "text":
+ # XXX: we cannot use a "bool(node) and node[0] or None" construct here
+ # because node[0] might evaluate to False if it has no child element
+ if len(node):
+ return node[0]
+ else:
+ return None
+ else: # tail
+ return node.getnext()
+
+ return node.tail and (node, "tail") or node.getnext()
+
+ def getParentNode(self, node):
+ if isinstance(node, tuple): # Text node
+ node, key = node
+ assert key in ("text", "tail"), _("Text nodes are text or tail, found %s") % key
+ if key == "text":
+ return node
+ # else: fallback to "normal" processing
+
+ return node.getparent()
diff --git a/libs/html5lib/treewalkers/pulldom.py b/libs/html5lib/treewalkers/pulldom.py
new file mode 100644
index 00000000..1f8b95b8
--- /dev/null
+++ b/libs/html5lib/treewalkers/pulldom.py
@@ -0,0 +1,60 @@
+from xml.dom.pulldom import START_ELEMENT, END_ELEMENT, \
+ COMMENT, IGNORABLE_WHITESPACE, CHARACTERS
+
+import _base
+
+from html5lib.constants import voidElements
+
+class TreeWalker(_base.TreeWalker):
+ def __iter__(self):
+ ignore_until = None
+ previous = None
+ for event in self.tree:
+ if previous is not None and \
+ (ignore_until is None or previous[1] is ignore_until):
+ if previous[1] is ignore_until:
+ ignore_until = None
+ for token in self.tokens(previous, event):
+ yield token
+ if token["type"] == "EmptyTag":
+ ignore_until = previous[1]
+ previous = event
+ if ignore_until is None or previous[1] is ignore_until:
+ for token in self.tokens(previous, None):
+ yield token
+ elif ignore_until is not None:
+ raise ValueError("Illformed DOM event stream: void element without END_ELEMENT")
+
+ def tokens(self, event, next):
+ type, node = event
+ if type == START_ELEMENT:
+ name = node.nodeName
+ namespace = node.namespaceURI
+ attrs = {}
+ for attr in node.attributes.keys():
+ attr = node.getAttributeNode(attr)
+ attrs[(attr.namespaceURI,attr.localName)] = attr.value
+ if name in voidElements:
+ for token in self.emptyTag(namespace,
+ name,
+ attrs,
+ not next or next[1] is not node):
+ yield token
+ else:
+ yield self.startTag(namespace, name, attrs)
+
+ elif type == END_ELEMENT:
+ name = node.nodeName
+ namespace = node.namespaceURI
+ if name not in voidElements:
+ yield self.endTag(namespace, name)
+
+ elif type == COMMENT:
+ yield self.comment(node.nodeValue)
+
+ elif type in (IGNORABLE_WHITESPACE, CHARACTERS):
+ for token in self.text(node.nodeValue):
+ yield token
+
+ else:
+ yield self.unknown(type)
diff --git a/libs/html5lib/treewalkers/simpletree.py b/libs/html5lib/treewalkers/simpletree.py
new file mode 100644
index 00000000..9e6bd4c5
--- /dev/null
+++ b/libs/html5lib/treewalkers/simpletree.py
@@ -0,0 +1,78 @@
+import gettext
+_ = gettext.gettext
+
+import _base
+
+class TreeWalker(_base.NonRecursiveTreeWalker):
+ """Given that simpletree has no performant way of getting a node's
+ next sibling, this implementation returns "nodes" as tuples with the
+ following content:
+
+ 1. The parent Node (Element, Document or DocumentFragment)
+
+ 2. The child index of the current node in its parent's children list
+
+ 3. A list used as a stack of all ancestors. It is a pair tuple whose
+ first item is a parent Node and second item is a child index.
+ """
+
+ def getNodeDetails(self, node):
+ if isinstance(node, tuple): # It might be the root Node
+ parent, idx, parents = node
+ node = parent.childNodes[idx]
+
+ # testing node.type allows us not to import treebuilders.simpletree
+ if node.type in (1, 2): # Document or DocumentFragment
+ return (_base.DOCUMENT,)
+
+ elif node.type == 3: # DocumentType
+ return _base.DOCTYPE, node.name, node.publicId, node.systemId
+
+ elif node.type == 4: # TextNode
+ return _base.TEXT, node.value
+
+ elif node.type == 5: # Element
+ attrs = {}
+ for name, value in node.attributes.items():
+ if isinstance(name, tuple):
+ attrs[(name[2],name[1])] = value
+ else:
+ attrs[(None,name)] = value
+ return (_base.ELEMENT, node.namespace, node.name,
+ attrs, node.hasContent())
+
+ elif node.type == 6: # CommentNode
+ return _base.COMMENT, node.data
+
+ else:
+ return _node.UNKNOWN, node.type
+
+ def getFirstChild(self, node):
+ if isinstance(node, tuple): # It might be the root Node
+ parent, idx, parents = node
+ parents.append((parent, idx))
+ node = parent.childNodes[idx]
+ else:
+ parents = []
+
+ assert node.hasContent(), "Node has no children"
+ return (node, 0, parents)
+
+ def getNextSibling(self, node):
+ assert isinstance(node, tuple), "Node is not a tuple: " + str(node)
+ parent, idx, parents = node
+ idx += 1
+ if len(parent.childNodes) > idx:
+ return (parent, idx, parents)
+ else:
+ return None
+
+ def getParentNode(self, node):
+ assert isinstance(node, tuple)
+ parent, idx, parents = node
+ if parents:
+ parent, idx = parents.pop()
+ return parent, idx, parents
+ else:
+ # HACK: We could return ``parent`` but None will stop the algorithm the same way
+ return None
diff --git a/libs/html5lib/treewalkers/soup.py b/libs/html5lib/treewalkers/soup.py
new file mode 100644
index 00000000..fca65ecb
--- /dev/null
+++ b/libs/html5lib/treewalkers/soup.py
@@ -0,0 +1,60 @@
+import re
+import gettext
+_ = gettext.gettext
+
+from BeautifulSoup import BeautifulSoup, Declaration, Comment, Tag
+from html5lib.constants import namespaces
+import _base
+
+class TreeWalker(_base.NonRecursiveTreeWalker):
+ doctype_regexp = re.compile(
+ r'DOCTYPE\s+(?P[^\s]*)(\s*PUBLIC\s*"(?P.*)"\s*"(?P.*)"|\s*SYSTEM\s*"(?P.*)")?')
+ def getNodeDetails(self, node):
+ if isinstance(node, BeautifulSoup): # Document or DocumentFragment
+ return (_base.DOCUMENT,)
+
+ elif isinstance(node, Declaration): # DocumentType
+ string = unicode(node.string)
+ #Slice needed to remove markup added during unicode conversion,
+ #but only in some versions of BeautifulSoup/Python
+ if string.startswith(''):
+ string = string[2:-1]
+ m = self.doctype_regexp.match(string)
+ #This regexp approach seems wrong and fragile
+ #but beautiful soup stores the doctype as a single thing and we want the seperate bits
+ #It should work as long as the tree is created by html5lib itself but may be wrong if it's
+ #been modified at all
+ #We could just feed to it a html5lib tokenizer, I guess...
+ assert m is not None, "DOCTYPE did not match expected format"
+
+ name = m.group('name')
+ publicId = m.group('publicId')
+ if publicId is not None:
+ systemId = m.group('systemId1')
+ else:
+ systemId = m.group('systemId2')
+ return _base.DOCTYPE, name, publicId or "", systemId or ""
+
+ elif isinstance(node, Comment):
+ string = unicode(node.string)
+ if string.startswith(''):
+ string = string[4:-3]
+ return _base.COMMENT, string
+
+ elif isinstance(node, unicode): # TextNode
+ return _base.TEXT, node
+
+ elif isinstance(node, Tag): # Element
+ return (_base.ELEMENT, namespaces["html"], node.name,
+ dict(node.attrs).items(), node.contents)
+ else:
+ return _base.UNKNOWN, node.__class__.__name__
+
+ def getFirstChild(self, node):
+ return node.contents[0]
+
+ def getNextSibling(self, node):
+ return node.nextSibling
+
+ def getParentNode(self, node):
+ return node.parent
diff --git a/libs/html5lib/utils.py b/libs/html5lib/utils.py
new file mode 100644
index 00000000..d53f6788
--- /dev/null
+++ b/libs/html5lib/utils.py
@@ -0,0 +1,175 @@
+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
+
+class MethodDispatcher(dict):
+ """Dict with 2 special properties:
+
+ On initiation, keys that are lists, sets or tuples are converted to
+ multiple keys so accessing any one of the items in the original
+ list-like object returns the matching value
+
+ md = MethodDispatcher({("foo", "bar"):"baz"})
+ md["foo"] == "baz"
+
+ A default value which can be set through the default attribute.
+ """
+
+ def __init__(self, items=()):
+ # Using _dictEntries instead of directly assigning to self is about
+ # twice as fast. Please do careful performance testing before changing
+ # anything here.
+ _dictEntries = []
+ for name,value in items:
+ if type(name) in (list, tuple, frozenset, set):
+ for item in name:
+ _dictEntries.append((item, value))
+ else:
+ _dictEntries.append((name, value))
+ dict.__init__(self, _dictEntries)
+ self.default = None
+
+ def __getitem__(self, key):
+ return dict.get(self, key, self.default)
+
+#Pure python implementation of deque taken from the ASPN Python Cookbook
+#Original code by Raymond Hettinger
+
+class deque(object):
+
+ def __init__(self, iterable=(), maxsize=-1):
+ if not hasattr(self, 'data'):
+ self.left = self.right = 0
+ self.data = {}
+ self.maxsize = maxsize
+ self.extend(iterable)
+
+ def append(self, x):
+ self.data[self.right] = x
+ self.right += 1
+ if self.maxsize != -1 and len(self) > self.maxsize:
+ self.popleft()
+
+ def appendleft(self, x):
+ self.left -= 1
+ self.data[self.left] = x
+ if self.maxsize != -1 and len(self) > self.maxsize:
+ self.pop()
+
+ def pop(self):
+ if self.left == self.right:
+ raise IndexError('cannot pop from empty deque')
+ self.right -= 1
+ elem = self.data[self.right]
+ del self.data[self.right]
+ return elem
+
+ def popleft(self):
+ if self.left == self.right:
+ raise IndexError('cannot pop from empty deque')
+ elem = self.data[self.left]
+ del self.data[self.left]
+ self.left += 1
+ return elem
+
+ def clear(self):
+ self.data.clear()
+ self.left = self.right = 0
+
+ def extend(self, iterable):
+ for elem in iterable:
+ self.append(elem)
+
+ def extendleft(self, iterable):
+ for elem in iterable:
+ self.appendleft(elem)
+
+ def rotate(self, n=1):
+ if self:
+ n %= len(self)
+ for i in xrange(n):
+ self.appendleft(self.pop())
+
+ def __getitem__(self, i):
+ if i < 0:
+ i += len(self)
+ try:
+ return self.data[i + self.left]
+ except KeyError:
+ raise IndexError
+
+ def __setitem__(self, i, value):
+ if i < 0:
+ i += len(self)
+ try:
+ self.data[i + self.left] = value
+ except KeyError:
+ raise IndexError
+
+ def __delitem__(self, i):
+ size = len(self)
+ if not (-size <= i < size):
+ raise IndexError
+ data = self.data
+ if i < 0:
+ i += size
+ for j in xrange(self.left+i, self.right-1):
+ data[j] = data[j+1]
+ self.pop()
+
+ def __len__(self):
+ return self.right - self.left
+
+ def __cmp__(self, other):
+ if type(self) != type(other):
+ return cmp(type(self), type(other))
+ return cmp(list(self), list(other))
+
+ def __repr__(self, _track=[]):
+ if id(self) in _track:
+ return '...'
+ _track.append(id(self))
+ r = 'deque(%r)' % (list(self),)
+ _track.remove(id(self))
+ return r
+
+ def __getstate__(self):
+ return (tuple(self),)
+
+ def __setstate__(self, s):
+ self.__init__(s[0])
+
+ def __hash__(self):
+ raise TypeError
+
+ def __copy__(self):
+ return self.__class__(self)
+
+ def __deepcopy__(self, memo={}):
+ from copy import deepcopy
+ result = self.__class__()
+ memo[id(self)] = result
+ result.__init__(deepcopy(tuple(self), memo))
+ return result
+
+#Some utility functions to dal with weirdness around UCS2 vs UCS4
+#python builds
+
+def encodingType():
+ if len() == 2:
+ return "UCS2"
+ else:
+ return "UCS4"
+
+def isSurrogatePair(data):
+ return (len(data) == 2 and
+ ord(data[0]) >= 0xD800 and ord(data[0]) <= 0xDBFF and
+ ord(data[1]) >= 0xDC00 and ord(data[1]) <= 0xDFFF)
+
+def surrogatePairToCodepoint(data):
+ char_val = (0x10000 + (ord(data[0]) - 0xD800) * 0x400 +
+ (ord(data[1]) - 0xDC00))
+ return char_val
diff --git a/libs/oauth2/__init__.py b/libs/oauth2/__init__.py
index a965fc71..835270e3 100644
--- a/libs/oauth2/__init__.py
+++ b/libs/oauth2/__init__.py
@@ -1,7 +1,7 @@
"""
The MIT License
-Copyright (c) 2007 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel
+Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -22,6 +22,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
+import base64
import urllib
import time
import random
@@ -31,12 +32,24 @@ import binascii
import httplib2
try:
- from urlparse import parse_qs, parse_qsl
+ from urlparse import parse_qs
+ parse_qs # placate pyflakes
except ImportError:
- from cgi import parse_qs, parse_qsl
+ # fall back for Python 2.5
+ from cgi import parse_qs
+try:
+ from hashlib import sha1
+ sha = sha1
+except ImportError:
+ # hashlib was added in Python 2.5
+ import sha
-VERSION = '1.0' # Hi Blaine!
+import _version
+
+__version__ = _version.__version__
+
+OAUTH_VERSION = '1.0' # Hi Blaine!
HTTP_METHOD = 'GET'
SIGNATURE_METHOD = 'PLAINTEXT'
@@ -44,7 +57,7 @@ SIGNATURE_METHOD = 'PLAINTEXT'
class Error(RuntimeError):
"""Generic exception class."""
- def __init__(self, message='OAuth error occured.'):
+ def __init__(self, message='OAuth error occurred.'):
self._message = message
@property
@@ -55,18 +68,94 @@ class Error(RuntimeError):
def __str__(self):
return self._message
+
class MissingSignature(Error):
pass
+
def build_authenticate_header(realm=''):
"""Optional WWW-Authenticate header (401 error)"""
return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
+def build_xoauth_string(url, consumer, token=None):
+ """Build an XOAUTH string for use in SMTP/IMPA authentication."""
+ request = Request.from_consumer_and_token(consumer, token,
+ "GET", url)
+
+ signing_method = SignatureMethod_HMAC_SHA1()
+ request.sign_request(signing_method, consumer, token)
+
+ params = []
+ for k, v in sorted(request.iteritems()):
+ if v is not None:
+ params.append('%s="%s"' % (k, escape(v)))
+
+ return "%s %s %s" % ("GET", url, ','.join(params))
+
+
+def to_unicode(s):
+ """ Convert to unicode, raise exception with instructive error
+ message if s is not unicode, ascii, or utf-8. """
+ if not isinstance(s, unicode):
+ if not isinstance(s, str):
+ raise TypeError('You are required to pass either unicode or string here, not: %r (%s)' % (type(s), s))
+ try:
+ s = s.decode('utf-8')
+ except UnicodeDecodeError, le:
+ raise TypeError('You are required to pass either a unicode object or a utf-8 string here. You passed a Python string object which contained non-utf-8: %r. The UnicodeDecodeError that resulted from attempting to interpret it as utf-8 was: %s' % (s, le,))
+ return s
+
+def to_utf8(s):
+ return to_unicode(s).encode('utf-8')
+
+def to_unicode_if_string(s):
+ if isinstance(s, basestring):
+ return to_unicode(s)
+ else:
+ return s
+
+def to_utf8_if_string(s):
+ if isinstance(s, basestring):
+ return to_utf8(s)
+ else:
+ return s
+
+def to_unicode_optional_iterator(x):
+ """
+ Raise TypeError if x is a str containing non-utf8 bytes or if x is
+ an iterable which contains such a str.
+ """
+ if isinstance(x, basestring):
+ return to_unicode(x)
+
+ try:
+ l = list(x)
+ except TypeError, e:
+ assert 'is not iterable' in str(e)
+ return x
+ else:
+ return [ to_unicode(e) for e in l ]
+
+def to_utf8_optional_iterator(x):
+ """
+ Raise TypeError if x is a str or if x is an iterable which
+ contains a str.
+ """
+ if isinstance(x, basestring):
+ return to_utf8(x)
+
+ try:
+ l = list(x)
+ except TypeError, e:
+ assert 'is not iterable' in str(e)
+ return x
+ else:
+ return [ to_utf8_if_string(e) for e in l ]
+
def escape(s):
"""Escape a URL including any /."""
- return urllib.quote(s, safe='~')
-
+ return urllib.quote(s.encode('utf-8'), safe='~')
def generate_timestamp():
"""Get seconds since epoch (UTC)."""
@@ -114,10 +203,8 @@ class Consumer(object):
raise ValueError("Key and secret must be set.")
def __str__(self):
- data = {
- 'oauth_consumer_key': self.key,
- 'oauth_consumer_secret': self.secret
- }
+ data = {'oauth_consumer_key': self.key,
+ 'oauth_consumer_secret': self.secret}
return urllib.urlencode(data)
@@ -216,7 +303,7 @@ class Token(object):
try:
token.callback_confirmed = params['oauth_callback_confirmed'][0]
except KeyError:
- pass # 1.0, no callback confirmed.
+ pass # 1.0, no callback confirmed.
return token
def __str__(self):
@@ -250,36 +337,41 @@ class Request(dict):
"""
- http_method = HTTP_METHOD
- http_url = None
- version = VERSION
-
- def __init__(self, method=HTTP_METHOD, url=None, parameters=None):
- if method is not None:
- self.method = method
-
+ version = OAUTH_VERSION
+
+ def __init__(self, method=HTTP_METHOD, url=None, parameters=None,
+ body='', is_form_encoded=False):
if url is not None:
- self.url = url
-
+ self.url = to_unicode(url)
+ self.method = method
if parameters is not None:
- self.update(parameters)
-
+ for k, v in parameters.iteritems():
+ k = to_unicode(k)
+ v = to_unicode_optional_iterator(v)
+ self[k] = v
+ self.body = body
+ self.is_form_encoded = is_form_encoded
+
+
@setter
def url(self, value):
- parts = urlparse.urlparse(value)
- scheme, netloc, path = parts[:3]
-
- # Exclude default port numbers.
- if scheme == 'http' and netloc[-3:] == ':80':
- netloc = netloc[:-3]
- elif scheme == 'https' and netloc[-4:] == ':443':
- netloc = netloc[:-4]
-
- if scheme != 'http' and scheme != 'https':
- raise ValueError("Unsupported URL %s (%s)." % (value, scheme))
-
- value = '%s://%s%s' % (scheme, netloc, path)
self.__dict__['url'] = value
+ if value is not None:
+ scheme, netloc, path, params, query, fragment = urlparse.urlparse(value)
+
+ # Exclude default port numbers.
+ if scheme == 'http' and netloc[-3:] == ':80':
+ netloc = netloc[:-3]
+ elif scheme == 'https' and netloc[-4:] == ':443':
+ netloc = netloc[:-4]
+ if scheme not in ('http', 'https'):
+ raise ValueError("Unsupported URL %s (%s)." % (value, scheme))
+
+ # Normalized URL excludes params, query, and fragment.
+ self.normalized_url = urlparse.urlunparse((scheme, netloc, path, None, None, None))
+ else:
+ self.normalized_url = None
+ self.__dict__['url'] = None
@setter
def method(self, value):
@@ -309,17 +401,44 @@ class Request(dict):
def to_postdata(self):
"""Serialize as post data for a POST request."""
- return self.encode_postdata(self)
+ d = {}
+ for k, v in self.iteritems():
+ d[k.encode('utf-8')] = to_utf8_optional_iterator(v)
- def encode_postdata(self, data):
# tell urlencode to deal with sequence values and map them correctly
# to resulting querystring. for example self["k"] = ["v1", "v2"] will
# result in 'k=v1&k=v2' and not k=%5B%27v1%27%2C+%27v2%27%5D
- return urllib.urlencode(data, True)
-
+ return urllib.urlencode(d, True).replace('+', '%20')
+
def to_url(self):
"""Serialize as a URL for a GET request."""
- return '%s?%s' % (self.url, self.to_postdata())
+ base_url = urlparse.urlparse(self.url)
+ try:
+ query = base_url.query
+ except AttributeError:
+ # must be python <2.5
+ query = base_url[4]
+ query = parse_qs(query)
+ for k, v in self.items():
+ query.setdefault(k, []).append(v)
+
+ try:
+ scheme = base_url.scheme
+ netloc = base_url.netloc
+ path = base_url.path
+ params = base_url.params
+ fragment = base_url.fragment
+ except AttributeError:
+ # must be python <2.5
+ scheme = base_url[0]
+ netloc = base_url[1]
+ path = base_url[2]
+ params = base_url[3]
+ fragment = base_url[5]
+
+ url = (scheme, netloc, path, params,
+ urllib.urlencode(query, True), fragment)
+ return urlparse.urlunparse(url)
def get_parameter(self, parameter):
ret = self.get(parameter)
@@ -327,20 +446,52 @@ class Request(dict):
raise Error('Parameter not found: %s' % parameter)
return ret
-
+
def get_normalized_parameters(self):
"""Return a string that contains the parameters that must be signed."""
- items = [(k, v) for k, v in self.items() if k != 'oauth_signature']
- encoded_str = urllib.urlencode(sorted(items), True)
+ items = []
+ for key, value in self.iteritems():
+ if key == 'oauth_signature':
+ continue
+ # 1.0a/9.1.1 states that kvp must be sorted by key, then by value,
+ # so we unpack sequence values into multiple items for sorting.
+ if isinstance(value, basestring):
+ items.append((to_utf8_if_string(key), to_utf8(value)))
+ else:
+ try:
+ value = list(value)
+ except TypeError, e:
+ assert 'is not iterable' in str(e)
+ items.append((to_utf8_if_string(key), to_utf8_if_string(value)))
+ else:
+ items.extend((to_utf8_if_string(key), to_utf8_if_string(item)) for item in value)
+
+ # Include any query string parameters from the provided URL
+ query = urlparse.urlparse(self.url)[4]
+
+ url_items = self._split_url_string(query).items()
+ url_items = [(to_utf8(k), to_utf8(v)) for k, v in url_items if k != 'oauth_signature' ]
+ items.extend(url_items)
+
+ items.sort()
+ encoded_str = urllib.urlencode(items)
# Encode signature parameters per Oauth Core 1.0 protocol
# spec draft 7, section 3.6
# (http://tools.ietf.org/html/draft-hammer-oauth-07#section-3.6)
# Spaces must be encoded with "%20" instead of "+"
- return encoded_str.replace('+', '%20')
-
+ return encoded_str.replace('+', '%20').replace('%7E', '~')
+
def sign_request(self, signature_method, consumer, token):
"""Set the signature parameter to the result of sign."""
+ if not self.is_form_encoded:
+ # according to
+ # http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html
+ # section 4.1.1 "OAuth Consumers MUST NOT include an
+ # oauth_body_hash parameter on requests with form-encoded
+ # request bodies."
+ self['oauth_body_hash'] = base64.b64encode(sha(self.body).digest())
+
if 'oauth_consumer_key' not in self:
self['oauth_consumer_key'] = consumer.key
@@ -398,7 +549,8 @@ class Request(dict):
@classmethod
def from_consumer_and_token(cls, consumer, token=None,
- http_method=HTTP_METHOD, http_url=None, parameters=None):
+ http_method=HTTP_METHOD, http_url=None, parameters=None,
+ body='', is_form_encoded=False):
if not parameters:
parameters = {}
@@ -414,8 +566,11 @@ class Request(dict):
if token:
parameters['oauth_token'] = token.key
+ if token.verifier:
+ parameters['oauth_verifier'] = token.verifier
- return Request(http_method, http_url, parameters)
+ return Request(http_method, http_url, parameters, body=body,
+ is_form_encoded=is_form_encoded)
@classmethod
def from_token_and_callback(cls, token, callback=None,
@@ -451,12 +606,82 @@ class Request(dict):
@staticmethod
def _split_url_string(param_str):
"""Turn URL string into parameters."""
- parameters = parse_qs(param_str, keep_blank_values=False)
+ parameters = parse_qs(param_str.encode('utf-8'), keep_blank_values=True)
for k, v in parameters.iteritems():
parameters[k] = urllib.unquote(v[0])
return parameters
+class Client(httplib2.Http):
+ """OAuthClient is a worker to attempt to execute a request."""
+
+ def __init__(self, consumer, token=None, cache=None, timeout=None,
+ proxy_info=None):
+
+ if consumer is not None and not isinstance(consumer, Consumer):
+ raise ValueError("Invalid consumer.")
+
+ if token is not None and not isinstance(token, Token):
+ raise ValueError("Invalid token.")
+
+ self.consumer = consumer
+ self.token = token
+ self.method = SignatureMethod_HMAC_SHA1()
+
+ httplib2.Http.__init__(self, cache=cache, timeout=timeout, proxy_info=proxy_info)
+
+ def set_signature_method(self, method):
+ if not isinstance(method, SignatureMethod):
+ raise ValueError("Invalid signature method.")
+
+ self.method = method
+
+ def request(self, uri, method="GET", body='', headers=None,
+ redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None):
+ DEFAULT_POST_CONTENT_TYPE = 'application/x-www-form-urlencoded'
+
+ if not isinstance(headers, dict):
+ headers = {}
+
+ if method == "POST":
+ headers['Content-Type'] = headers.get('Content-Type',
+ DEFAULT_POST_CONTENT_TYPE)
+
+ is_form_encoded = \
+ headers.get('Content-Type') == 'application/x-www-form-urlencoded'
+
+ if is_form_encoded and body:
+ parameters = parse_qs(body)
+ else:
+ parameters = None
+
+ req = Request.from_consumer_and_token(self.consumer,
+ token=self.token, http_method=method, http_url=uri,
+ parameters=parameters, body=body, is_form_encoded=is_form_encoded)
+
+ req.sign_request(self.method, self.consumer, self.token)
+
+ schema, rest = urllib.splittype(uri)
+ if rest.startswith('//'):
+ hierpart = '//'
+ else:
+ hierpart = ''
+ host, rest = urllib.splithost(rest)
+
+ realm = schema + ':' + hierpart + host
+
+ if is_form_encoded:
+ body = req.to_postdata()
+ elif method == "GET":
+ uri = req.to_url()
+ else:
+ headers.update(req.to_header(realm=realm))
+
+ return httplib2.Http.request(self, uri, method=method, body=body,
+ headers=headers, redirections=redirections,
+ connection_type=connection_type)
+
+
class Server(object):
"""A skeletal implementation of a service provider, providing protected
resources to requests from authorized consumers.
@@ -467,7 +692,7 @@ class Server(object):
"""
timestamp_threshold = 300 # In seconds, five minutes.
- version = VERSION
+ version = OAUTH_VERSION
signature_methods = None
def __init__(self, signature_methods=None):
@@ -480,7 +705,7 @@ class Server(object):
def verify_request(self, request, consumer, token):
"""Verifies an api call and checks all the parameters."""
- version = self._get_version(request)
+ self._check_version(request)
self._check_signature(request, consumer, token)
parameters = request.get_nonoauth_parameters()
return parameters
@@ -489,15 +714,18 @@ class Server(object):
"""Optional support for the authenticate header."""
return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
+ def _check_version(self, request):
+ """Verify the correct version of the request for this server."""
+ version = self._get_version(request)
+ if version and version != self.version:
+ raise Error('OAuth version %s not supported.' % str(version))
+
def _get_version(self, request):
- """Verify the correct version request for this server."""
+ """Return the version of the request for this server."""
try:
version = request.get_parameter('oauth_version')
except:
- version = VERSION
-
- if version and version != self.version:
- raise Error('OAuth version %s not supported.' % str(version))
+ version = OAUTH_VERSION
return version
@@ -539,8 +767,6 @@ class Server(object):
raise Error('Invalid signature. Expected signature base '
'string: %s' % base)
- built = signature_method.sign(request, consumer, token)
-
def _check_timestamp(self, timestamp):
"""Verify that timestamp is recentish."""
timestamp = int(timestamp)
@@ -548,75 +774,8 @@ class Server(object):
lapsed = now - timestamp
if lapsed > self.timestamp_threshold:
raise Error('Expired timestamp: given %d and now %s has a '
- 'greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold))
-
-
-class Client(httplib2.Http):
- """OAuthClient is a worker to attempt to execute a request."""
-
- def __init__(self, consumer, token=None, cache=None, timeout=None,
- proxy_info=None):
-
- if consumer is not None and not isinstance(consumer, Consumer):
- raise ValueError("Invalid consumer.")
-
- if token is not None and not isinstance(token, Token):
- raise ValueError("Invalid token.")
-
- self.consumer = consumer
- self.token = token
- self.method = SignatureMethod_HMAC_SHA1()
-
- httplib2.Http.__init__(self, cache=cache, timeout=timeout,
- proxy_info=proxy_info)
-
- def set_signature_method(self, method):
- if not isinstance(method, SignatureMethod):
- raise ValueError("Invalid signature method.")
-
- self.method = method
-
- def request(self, uri, method="GET", body=None, headers=None,
- redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None,
- force_auth_header=False):
-
- if not isinstance(headers, dict):
- headers = {}
-
- if body and method == "POST":
- parameters = dict(parse_qsl(body))
- elif method == "GET":
- parsed = urlparse.urlparse(uri)
- parameters = parse_qs(parsed.query)
- else:
- parameters = None
-
- req = Request.from_consumer_and_token(self.consumer, token=self.token,
- http_method=method, http_url=uri, parameters=parameters)
-
- req.sign_request(self.method, self.consumer, self.token)
-
- if force_auth_header:
- # ensure we always send Authorization
- headers.update(req.to_header())
-
- if method == "POST":
- if not force_auth_header:
- body = req.to_postdata()
- else:
- body = req.encode_postdata(req.get_nonoauth_parameters())
- headers['Content-Type'] = 'application/x-www-form-urlencoded'
- elif method == "GET":
- if not force_auth_header:
- uri = req.to_url()
- else:
- if not force_auth_header:
- # don't call update twice.
- headers.update(req.to_header())
-
- return httplib2.Http.request(self, uri, method=method, body=body,
- headers=headers, redirections=redirections,
- connection_type=connection_type)
+ 'greater difference than threshold %d' % (timestamp, now,
+ self.timestamp_threshold))
class SignatureMethod(object):
@@ -657,11 +816,14 @@ class SignatureMethod(object):
class SignatureMethod_HMAC_SHA1(SignatureMethod):
name = 'HMAC-SHA1'
-
+
def signing_base(self, request, consumer, token):
+ if not hasattr(request, 'normalized_url') or request.normalized_url is None:
+ raise ValueError("Base URL for request is not set.")
+
sig = (
escape(request.method),
- escape(request.url),
+ escape(request.normalized_url),
escape(request.get_normalized_parameters()),
)
@@ -675,17 +837,12 @@ class SignatureMethod_HMAC_SHA1(SignatureMethod):
"""Builds the base signature string."""
key, raw = self.signing_base(request, consumer, token)
- # HMAC object.
- try:
- import hashlib # 2.5
- hashed = hmac.new(key, raw, hashlib.sha1)
- except ImportError:
- import sha # Deprecated
- hashed = hmac.new(key, raw, sha)
+ hashed = hmac.new(key, raw, sha)
# Calculate the digest base 64.
return binascii.b2a_base64(hashed.digest())[:-1]
+
class SignatureMethod_PLAINTEXT(SignatureMethod):
name = 'PLAINTEXT'
@@ -701,4 +858,3 @@ class SignatureMethod_PLAINTEXT(SignatureMethod):
def sign(self, request, consumer, token):
key, raw = self.signing_base(request, consumer, token)
return raw
-
diff --git a/libs/oauth2/_version.py b/libs/oauth2/_version.py
new file mode 100644
index 00000000..9d779eaa
--- /dev/null
+++ b/libs/oauth2/_version.py
@@ -0,0 +1,18 @@
+# This is the version of this source code.
+
+manual_verstr = "1.5"
+
+
+
+auto_build_num = "211"
+
+
+
+verstr = manual_verstr + "." + auto_build_num
+try:
+ from pyutil.version_class import Version as pyutil_Version
+ __version__ = pyutil_Version(verstr)
+except (ImportError, ValueError):
+ # Maybe there is no pyutil installed.
+ from distutils.version import LooseVersion as distutils_Version
+ __version__ = distutils_Version(verstr)
diff --git a/libs/oauth2/clients/__init__.py b/libs/oauth2/clients/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/libs/oauth2/clients/imap.py b/libs/oauth2/clients/imap.py
new file mode 100644
index 00000000..68b7cd8c
--- /dev/null
+++ b/libs/oauth2/clients/imap.py
@@ -0,0 +1,40 @@
+"""
+The MIT License
+
+Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+import oauth2
+import imaplib
+
+
+class IMAP4_SSL(imaplib.IMAP4_SSL):
+ """IMAP wrapper for imaplib.IMAP4_SSL that implements XOAUTH."""
+
+ def authenticate(self, url, consumer, token):
+ if consumer is not None and not isinstance(consumer, oauth2.Consumer):
+ raise ValueError("Invalid consumer.")
+
+ if token is not None and not isinstance(token, oauth2.Token):
+ raise ValueError("Invalid token.")
+
+ imaplib.IMAP4_SSL.authenticate(self, 'XOAUTH',
+ lambda x: oauth2.build_xoauth_string(url, consumer, token))
diff --git a/libs/oauth2/clients/smtp.py b/libs/oauth2/clients/smtp.py
new file mode 100644
index 00000000..3e7bf0b0
--- /dev/null
+++ b/libs/oauth2/clients/smtp.py
@@ -0,0 +1,41 @@
+"""
+The MIT License
+
+Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+import oauth2
+import smtplib
+import base64
+
+
+class SMTP(smtplib.SMTP):
+ """SMTP wrapper for smtplib.SMTP that implements XOAUTH."""
+
+ def authenticate(self, url, consumer, token):
+ if consumer is not None and not isinstance(consumer, oauth2.Consumer):
+ raise ValueError("Invalid consumer.")
+
+ if token is not None and not isinstance(token, oauth2.Token):
+ raise ValueError("Invalid token.")
+
+ self.docmd('AUTH', 'XOAUTH %s' % \
+ base64.b64encode(oauth2.build_xoauth_string(url, consumer, token)))
diff --git a/libs/oauthlib/__init__.py b/libs/oauthlib/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/libs/oauthlib/common.py b/libs/oauthlib/common.py
new file mode 100644
index 00000000..4cdfd0d4
--- /dev/null
+++ b/libs/oauthlib/common.py
@@ -0,0 +1,155 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+"""
+oauthlib.common
+~~~~~~~~~~~~~~
+
+This module provides data structures and utilities common
+to all implementations of OAuth.
+"""
+
+import re
+import urllib
+import urlparse
+
+
+always_safe = (u'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+ u'abcdefghijklmnopqrstuvwxyz'
+ u'0123456789' u'_.-')
+
+
+def quote(s, safe=u'/'):
+ encoded = s.encode("utf-8")
+ quoted = urllib.quote(encoded, safe)
+ return quoted.decode("utf-8")
+
+
+def unquote(s):
+ encoded = s.encode("utf-8")
+ unquoted = urllib.unquote(encoded)
+ return unquoted.decode("utf-8")
+
+
+def urlencode(params):
+ utf8_params = encode_params_utf8(params)
+ urlencoded = urllib.urlencode(utf8_params)
+ return urlencoded.decode("utf-8")
+
+
+def encode_params_utf8(params):
+ """Ensures that all parameters in a list of 2-element tuples are encoded to
+ bytestrings using UTF-8
+ """
+ encoded = []
+ for k, v in params:
+ encoded.append((
+ k.encode('utf-8') if isinstance(k, unicode) else k,
+ v.encode('utf-8') if isinstance(v, unicode) else v))
+ return encoded
+
+
+def decode_params_utf8(params):
+ """Ensures that all parameters in a list of 2-element tuples are decoded to
+ unicode using UTF-8.
+ """
+ decoded = []
+ for k, v in params:
+ decoded.append((
+ k.decode('utf-8') if isinstance(k, str) else k,
+ v.decode('utf-8') if isinstance(v, str) else v))
+ return decoded
+
+
+urlencoded = set(always_safe) | set(u'=&;%+~')
+
+
+def urldecode(query):
+ """Decode a query string in x-www-form-urlencoded format into a sequence
+ of two-element tuples.
+
+ Unlike urlparse.parse_qsl(..., strict_parsing=True) urldecode will enforce
+ correct formatting of the query string by validation. If validation fails
+ a ValueError will be raised. urllib.parse_qsl will only raise errors if
+ any of name-value pairs omits the equals sign.
+ """
+ # Check if query contains invalid characters
+ if query and not set(query) <= urlencoded:
+ raise ValueError('Invalid characters in query string.')
+
+ # Check for correctly hex encoded values using a regular expression
+ # All encoded values begin with % followed by two hex characters
+ # correct = %00, %A0, %0A, %FF
+ # invalid = %G0, %5H, %PO
+ invalid_hex = u'%[^0-9A-Fa-f]|%[0-9A-Fa-f][^0-9A-Fa-f]'
+ if len(re.findall(invalid_hex, query)):
+ raise ValueError('Invalid hex encoding in query string.')
+
+ query = query.decode('utf-8') if isinstance(query, str) else query
+ # We want to allow queries such as "c2" whereas urlparse.parse_qsl
+ # with the strict_parsing flag will not.
+ params = urlparse.parse_qsl(query, keep_blank_values=True)
+
+ # unicode all the things
+ return decode_params_utf8(params)
+
+
+def extract_params(raw):
+ """Extract parameters and return them as a list of 2-tuples.
+
+ Will successfully extract parameters from urlencoded query strings,
+ dicts, or lists of 2-tuples. Empty strings/dicts/lists will return an
+ empty list of parameters. Any other input will result in a return
+ value of None.
+ """
+ if isinstance(raw, basestring):
+ try:
+ params = urldecode(raw)
+ except ValueError:
+ params = None
+ elif hasattr(raw, '__iter__'):
+ try:
+ dict(raw)
+ except ValueError:
+ params = None
+ except TypeError:
+ params = None
+ else:
+ params = list(raw.items() if isinstance(raw, dict) else raw)
+ params = decode_params_utf8(params)
+ else:
+ params = None
+
+ return params
+
+
+class Request(object):
+ """A malleable representation of a signable HTTP request.
+
+ Body argument may contain any data, but parameters will only be decoded if
+ they are one of:
+
+ * urlencoded query string
+ * dict
+ * list of 2-tuples
+
+ Anything else will be treated as raw body data to be passed through
+ unmolested.
+ """
+
+ def __init__(self, uri, http_method=u'GET', body=None, headers=None):
+ self.uri = uri
+ self.http_method = http_method
+ self.headers = headers or {}
+ self.body = body
+ self.decoded_body = extract_params(body)
+ self.oauth_params = []
+
+ @property
+ def uri_query(self):
+ return urlparse.urlparse(self.uri).query
+
+ @property
+ def uri_query_params(self):
+ return urlparse.parse_qsl(self.uri_query, keep_blank_values=True,
+ strict_parsing=True)
diff --git a/libs/oauthlib/oauth1/__init__.py b/libs/oauthlib/oauth1/__init__.py
new file mode 100644
index 00000000..ef692b57
--- /dev/null
+++ b/libs/oauthlib/oauth1/__init__.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+"""
+oauthlib.oauth1
+~~~~~~~~~~~~~~
+
+This module is a wrapper for the most recent implementation of OAuth 1.0 Client
+and Server classes.
+"""
+
+from .rfc5849 import Client, Server
+
diff --git a/libs/oauthlib/oauth1/rfc5849/__init__.py b/libs/oauthlib/oauth1/rfc5849/__init__.py
new file mode 100644
index 00000000..03fb8b25
--- /dev/null
+++ b/libs/oauthlib/oauth1/rfc5849/__init__.py
@@ -0,0 +1,350 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+"""
+oauthlib.oauth1.rfc5849
+~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for signing and checking OAuth 1.0 RFC 5849 requests.
+"""
+
+import logging
+import urlparse
+
+from oauthlib.common import Request, urlencode
+from . import parameters, signature, utils
+
+SIGNATURE_HMAC = u"HMAC-SHA1"
+SIGNATURE_RSA = u"RSA-SHA1"
+SIGNATURE_PLAINTEXT = u"PLAINTEXT"
+SIGNATURE_METHODS = (SIGNATURE_HMAC, SIGNATURE_RSA, SIGNATURE_PLAINTEXT)
+
+SIGNATURE_TYPE_AUTH_HEADER = u'AUTH_HEADER'
+SIGNATURE_TYPE_QUERY = u'QUERY'
+SIGNATURE_TYPE_BODY = u'BODY'
+
+CONTENT_TYPE_FORM_URLENCODED = u'application/x-www-form-urlencoded'
+
+
+class Client(object):
+ """A client used to sign OAuth 1.0 RFC 5849 requests"""
+ def __init__(self, client_key,
+ client_secret=None,
+ resource_owner_key=None,
+ resource_owner_secret=None,
+ callback_uri=None,
+ signature_method=SIGNATURE_HMAC,
+ signature_type=SIGNATURE_TYPE_AUTH_HEADER,
+ rsa_key=None, verifier=None):
+ self.client_key = client_key
+ self.client_secret = client_secret
+ self.resource_owner_key = resource_owner_key
+ self.resource_owner_secret = resource_owner_secret
+ self.signature_method = signature_method
+ self.signature_type = signature_type
+ self.callback_uri = callback_uri
+ self.rsa_key = rsa_key
+ self.verifier = verifier
+
+ if self.signature_method == SIGNATURE_RSA and self.rsa_key is None:
+ raise ValueError('rsa_key is required when using RSA signature method.')
+
+ def get_oauth_signature(self, request):
+ """Get an OAuth signature to be used in signing a request
+ """
+ if self.signature_method == SIGNATURE_PLAINTEXT:
+ # fast-path
+ return signature.sign_plaintext(self.client_secret,
+ self.resource_owner_secret)
+
+ uri, headers, body = self._render(request)
+
+ collected_params = signature.collect_parameters(
+ uri_query=urlparse.urlparse(uri).query,
+ body=body,
+ headers=headers)
+ logging.debug("Collected params: {0}".format(collected_params))
+
+ normalized_params = signature.normalize_parameters(collected_params)
+ normalized_uri = signature.normalize_base_string_uri(request.uri)
+ logging.debug("Normalized params: {0}".format(normalized_params))
+ logging.debug("Normalized URI: {0}".format(normalized_uri))
+
+ base_string = signature.construct_base_string(request.http_method,
+ normalized_uri, normalized_params)
+
+ logging.debug("Base signing string: {0}".format(base_string))
+
+ if self.signature_method == SIGNATURE_HMAC:
+ sig = signature.sign_hmac_sha1(base_string, self.client_secret,
+ self.resource_owner_secret)
+ elif self.signature_method == SIGNATURE_RSA:
+ sig = signature.sign_rsa_sha1(base_string, self.rsa_key)
+ else:
+ sig = signature.sign_plaintext(self.client_secret,
+ self.resource_owner_secret)
+
+ logging.debug("Signature: {0}".format(sig))
+ return sig
+
+ def get_oauth_params(self):
+ """Get the basic OAuth parameters to be used in generating a signature.
+ """
+ params = [
+ (u'oauth_nonce', utils.generate_nonce()),
+ (u'oauth_timestamp', utils.generate_timestamp()),
+ (u'oauth_version', u'1.0'),
+ (u'oauth_signature_method', self.signature_method),
+ (u'oauth_consumer_key', self.client_key),
+ ]
+ if self.resource_owner_key:
+ params.append((u'oauth_token', self.resource_owner_key))
+ if self.callback_uri:
+ params.append((u'oauth_callback', self.callback_uri))
+ if self.verifier:
+ params.append((u'oauth_verifier', self.verifier))
+
+ return params
+
+ def _render(self, request, formencode=False):
+ """Render a signed request according to signature type
+
+ Returns a 3-tuple containing the request URI, headers, and body.
+
+ If the formencode argument is True and the body contains parameters, it
+ is escaped and returned as a valid formencoded string.
+ """
+ # TODO what if there are body params on a header-type auth?
+ # TODO what if there are query params on a body-type auth?
+
+ uri, headers, body = request.uri, request.headers, request.body
+
+ # TODO: right now these prepare_* methods are very narrow in scope--they
+ # only affect their little thing. In some cases (for example, with
+ # header auth) it might be advantageous to allow these methods to touch
+ # other parts of the request, like the headers—so the prepare_headers
+ # method could also set the Content-Type header to x-www-form-urlencoded
+ # like the spec requires. This would be a fundamental change though, and
+ # I'm not sure how I feel about it.
+ if self.signature_type == SIGNATURE_TYPE_AUTH_HEADER:
+ headers = parameters.prepare_headers(request.oauth_params, request.headers)
+ elif self.signature_type == SIGNATURE_TYPE_BODY and request.decoded_body is not None:
+ body = parameters.prepare_form_encoded_body(request.oauth_params, request.decoded_body)
+ if formencode:
+ body = urlencode(body)
+ headers['Content-Type'] = u'application/x-www-form-urlencoded'
+ elif self.signature_type == SIGNATURE_TYPE_QUERY:
+ uri = parameters.prepare_request_uri_query(request.oauth_params, request.uri)
+ else:
+ raise ValueError('Unknown signature type specified.')
+
+ return uri, headers, body
+
+ def sign(self, uri, http_method=u'GET', body=None, headers=None):
+ """Sign a request
+
+ Signs an HTTP request with the specified parts.
+
+ Returns a 3-tuple of the signed request's URI, headers, and body.
+ Note that http_method is not returned as it is unaffected by the OAuth
+ signing process.
+
+ The body argument may be a dict, a list of 2-tuples, or a formencoded
+ string. The Content-Type header must be 'application/x-www-form-urlencoded'
+ if it is present.
+
+ If the body argument is not one of the above, it will be returned
+ verbatim as it is unaffected by the OAuth signing process. Attempting to
+ sign a request with non-formencoded data using the OAuth body signature
+ type is invalid and will raise an exception.
+
+ If the body does contain parameters, it will be returned as a properly-
+ formatted formencoded string.
+
+ All string data MUST be unicode. This includes strings inside body
+ dicts, for example.
+ """
+ # normalize request data
+ request = Request(uri, http_method, body, headers)
+
+ # sanity check
+ content_type = request.headers.get('Content-Type', None)
+ multipart = content_type and content_type.startswith('multipart/')
+ should_have_params = content_type == CONTENT_TYPE_FORM_URLENCODED
+ has_params = request.decoded_body is not None
+ # 3.4.1.3.1. Parameter Sources
+ # [Parameters are collected from the HTTP request entity-body, but only
+ # if [...]:
+ # * The entity-body is single-part.
+ if multipart and has_params:
+ raise ValueError("Headers indicate a multipart body but body contains parameters.")
+ # * The entity-body follows the encoding requirements of the
+ # "application/x-www-form-urlencoded" content-type as defined by
+ # [W3C.REC-html40-19980424].
+ elif should_have_params and not has_params:
+ raise ValueError("Headers indicate a formencoded body but body was not decodable.")
+ # * The HTTP request entity-header includes the "Content-Type"
+ # header field set to "application/x-www-form-urlencoded".
+ elif not should_have_params and has_params:
+ raise ValueError("Body contains parameters but Content-Type header was not set.")
+
+ # 3.5.2. Form-Encoded Body
+ # Protocol parameters can be transmitted in the HTTP request entity-
+ # body, but only if the following REQUIRED conditions are met:
+ # o The entity-body is single-part.
+ # o The entity-body follows the encoding requirements of the
+ # "application/x-www-form-urlencoded" content-type as defined by
+ # [W3C.REC-html40-19980424].
+ # o The HTTP request entity-header includes the "Content-Type" header
+ # field set to "application/x-www-form-urlencoded".
+ elif self.signature_type == SIGNATURE_TYPE_BODY and not (
+ should_have_params and has_params and not multipart):
+ raise ValueError('Body signatures may only be used with form-urlencoded content')
+
+ # generate the basic OAuth parameters
+ request.oauth_params = self.get_oauth_params()
+
+ # generate the signature
+ request.oauth_params.append((u'oauth_signature', self.get_oauth_signature(request)))
+
+ # render the signed request and return it
+ return self._render(request, formencode=True)
+
+
+class Server(object):
+ """A server used to verify OAuth 1.0 RFC 5849 requests"""
+ def __init__(self, signature_method=SIGNATURE_HMAC, rsa_key=None):
+ self.signature_method = signature_method
+ self.rsa_key = rsa_key
+
+ def get_client_secret(self, client_key):
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ def get_resource_owner_secret(self, resource_owner_key):
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ def get_signature_type_and_params(self, uri_query, headers, body):
+ signature_types_with_oauth_params = filter(lambda s: s[1], (
+ (SIGNATURE_TYPE_AUTH_HEADER, utils.filter_oauth_params(
+ signature.collect_parameters(headers=headers,
+ exclude_oauth_signature=False))),
+ (SIGNATURE_TYPE_BODY, utils.filter_oauth_params(
+ signature.collect_parameters(body=body,
+ exclude_oauth_signature=False))),
+ (SIGNATURE_TYPE_QUERY, utils.filter_oauth_params(
+ signature.collect_parameters(uri_query=uri_query,
+ exclude_oauth_signature=False))),
+ ))
+
+ if len(signature_types_with_oauth_params) > 1:
+ raise ValueError('oauth_ params must come from only 1 signature type but were found in %s' % ', '.join(
+ [s[0] for s in signature_types_with_oauth_params]))
+ try:
+ signature_type, params = signature_types_with_oauth_params[0]
+ except IndexError:
+ raise ValueError('oauth_ params are missing. Could not determine signature type.')
+
+ return signature_type, dict(params)
+
+ def check_client_key(self, client_key):
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ def check_resource_owner_key(self, client_key, resource_owner_key):
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ def check_timestamp_and_nonce(self, timestamp, nonce):
+ raise NotImplementedError("Subclasses must implement this function.")
+
+ def check_request_signature(self, uri, http_method=u'GET', body='',
+ headers=None):
+ """Check a request's supplied signature to make sure the request is
+ valid.
+
+ Servers should return HTTP status 400 if a ValueError exception
+ is raised and HTTP status 401 on return value False.
+
+ Per `section 3.2`_ of the spec.
+
+ .. _`section 3.2`: http://tools.ietf.org/html/rfc5849#section-3.2
+ """
+ headers = headers or {}
+ signature_type = None
+ # FIXME: urlparse does not return unicode!
+ uri_query = urlparse.urlparse(uri).query
+
+ signature_type, params = self.get_signature_type_and_params(uri_query,
+ headers, body)
+
+ # the parameters may not include duplicate oauth entries
+ filtered_params = utils.filter_oauth_params(params)
+ if len(filtered_params) != len(params):
+ raise ValueError("Duplicate OAuth entries.")
+
+ params = dict(params)
+ request_signature = params.get(u'oauth_signature')
+ client_key = params.get(u'oauth_consumer_key')
+ resource_owner_key = params.get(u'oauth_token')
+ nonce = params.get(u'oauth_nonce')
+ timestamp = params.get(u'oauth_timestamp')
+ callback_uri = params.get(u'oauth_callback')
+ verifier = params.get(u'oauth_verifier')
+ signature_method = params.get(u'oauth_signature_method')
+
+ # ensure all mandatory parameters are present
+ if not all((request_signature, client_key, nonce,
+ timestamp, signature_method)):
+ raise ValueError("Missing OAuth parameters.")
+
+ # if version is supplied, it must be "1.0"
+ if u'oauth_version' in params and params[u'oauth_version'] != u'1.0':
+ raise ValueError("Invalid OAuth version.")
+
+ # signature method must be valid
+ if not signature_method in SIGNATURE_METHODS:
+ raise ValueError("Invalid signature method.")
+
+ # ensure client key is valid
+ if not self.check_client_key(client_key):
+ return False
+
+ # ensure resource owner key is valid and not expired
+ if not self.check_resource_owner_key(client_key, resource_owner_key):
+ return False
+
+ # ensure the nonce and timestamp haven't been used before
+ if not self.check_timestamp_and_nonce(timestamp, nonce):
+ return False
+
+ # FIXME: extract realm, then self.check_realm
+
+ # oauth_client parameters depend on client chosen signature method
+ # which may vary for each request, section 3.4
+ # HMAC-SHA1 and PLAINTEXT share parameters
+ if signature_method == SIGNATURE_RSA:
+ oauth_client = Client(client_key,
+ resource_owner_key=resource_owner_key,
+ callback_uri=callback_uri,
+ signature_method=signature_method,
+ signature_type=signature_type,
+ rsa_key=self.rsa_key, verifier=verifier)
+ else:
+ client_secret = self.get_client_secret(client_key)
+ resource_owner_secret = self.get_resource_owner_secret(
+ resource_owner_key)
+ oauth_client = Client(client_key,
+ client_secret=client_secret,
+ resource_owner_key=resource_owner_key,
+ resource_owner_secret=resource_owner_secret,
+ callback_uri=callback_uri,
+ signature_method=signature_method,
+ signature_type=signature_type,
+ verifier=verifier)
+
+ request = Request(uri, http_method, body, headers)
+ request.oauth_params = params
+
+ client_signature = oauth_client.get_oauth_signature(request)
+
+ # FIXME: use near constant time string compare to avoid timing attacks
+ return client_signature == request_signature
diff --git a/libs/oauthlib/oauth1/rfc5849/parameters.py b/libs/oauthlib/oauth1/rfc5849/parameters.py
new file mode 100644
index 00000000..dee23a43
--- /dev/null
+++ b/libs/oauthlib/oauth1/rfc5849/parameters.py
@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+"""
+oauthlib.parameters
+~~~~~~~~~~~~~~~~~~~
+
+This module contains methods related to `section 3.5`_ of the OAuth 1.0a spec.
+
+.. _`section 3.5`: http://tools.ietf.org/html/rfc5849#section-3.5
+"""
+
+from urlparse import urlparse, urlunparse
+from . import utils
+from oauthlib.common import extract_params, urlencode
+
+
+# TODO: do we need filter_params now that oauth_params are handled by Request?
+# We can easily pass in just oauth protocol params.
+@utils.filter_params
+def prepare_headers(oauth_params, headers=None, realm=None):
+ """**Prepare the Authorization header.**
+ Per `section 3.5.1`_ of the spec.
+
+ Protocol parameters can be transmitted using the HTTP "Authorization"
+ header field as defined by `RFC2617`_ with the auth-scheme name set to
+ "OAuth" (case insensitive).
+
+ For example::
+
+ Authorization: OAuth realm="Example",
+ oauth_consumer_key="0685bd9184jfhq22",
+ oauth_token="ad180jjd733klru7",
+ oauth_signature_method="HMAC-SHA1",
+ oauth_signature="wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
+ oauth_timestamp="137131200",
+ oauth_nonce="4572616e48616d6d65724c61686176",
+ oauth_version="1.0"
+
+
+ .. _`section 3.5.1`: http://tools.ietf.org/html/rfc5849#section-3.5.1
+ .. _`RFC2617`: http://tools.ietf.org/html/rfc2617
+ """
+ headers = headers or {}
+
+ # Protocol parameters SHALL be included in the "Authorization" header
+ # field as follows:
+ authorization_header_parameters_parts = []
+ for oauth_parameter_name, value in oauth_params:
+ # 1. Parameter names and values are encoded per Parameter Encoding
+ # (`Section 3.6`_)
+ #
+ # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+ escaped_name = utils.escape(oauth_parameter_name)
+ escaped_value = utils.escape(value)
+
+ # 2. Each parameter's name is immediately followed by an "=" character
+ # (ASCII code 61), a """ character (ASCII code 34), the parameter
+ # value (MAY be empty), and another """ character (ASCII code 34).
+ part = u'{0}="{1}"'.format(escaped_name, escaped_value)
+
+ authorization_header_parameters_parts.append(part)
+
+ # 3. Parameters are separated by a "," character (ASCII code 44) and
+ # OPTIONAL linear whitespace per `RFC2617`_.
+ #
+ # .. _`RFC2617`: http://tools.ietf.org/html/rfc2617
+ authorization_header_parameters = ', '.join(
+ authorization_header_parameters_parts)
+
+ # 4. The OPTIONAL "realm" parameter MAY be added and interpreted per
+ # `RFC2617 section 1.2`_.
+ #
+ # .. _`RFC2617 section 1.2`: http://tools.ietf.org/html/rfc2617#section-1.2
+ if realm:
+ # NOTE: realm should *not* be escaped
+ authorization_header_parameters = (u'realm="%s", ' % realm +
+ authorization_header_parameters)
+
+ # the auth-scheme name set to "OAuth" (case insensitive).
+ authorization_header = u'OAuth %s' % authorization_header_parameters
+
+ # contribute the Authorization header to the given headers
+ full_headers = {}
+ full_headers.update(headers)
+ full_headers[u'Authorization'] = authorization_header
+ return full_headers
+
+
+def _append_params(oauth_params, params):
+ """Append OAuth params to an existing set of parameters.
+
+ Both params and oauth_params is must be lists of 2-tuples.
+
+ Per `section 3.5.2`_ and `3.5.3`_ of the spec.
+
+ .. _`section 3.5.2`: http://tools.ietf.org/html/rfc5849#section-3.5.2
+ .. _`3.5.3`: http://tools.ietf.org/html/rfc5849#section-3.5.3
+
+ """
+ merged = list(params)
+ merged.extend(oauth_params)
+ # The request URI / entity-body MAY include other request-specific
+ # parameters, in which case, the protocol parameters SHOULD be appended
+ # following the request-specific parameters, properly separated by an "&"
+ # character (ASCII code 38)
+ merged.sort(key=lambda i: i[0].startswith('oauth_'))
+ return merged
+
+
+def prepare_form_encoded_body(oauth_params, body):
+ """Prepare the Form-Encoded Body.
+
+ Per `section 3.5.2`_ of the spec.
+
+ .. _`section 3.5.2`: http://tools.ietf.org/html/rfc5849#section-3.5.2
+
+ """
+ # append OAuth params to the existing body
+ return _append_params(oauth_params, body)
+
+
+def prepare_request_uri_query(oauth_params, uri):
+ """Prepare the Request URI Query.
+
+ Per `section 3.5.3`_ of the spec.
+
+ .. _`section 3.5.3`: http://tools.ietf.org/html/rfc5849#section-3.5.3
+
+ """
+ # append OAuth params to the existing set of query components
+ sch, net, path, par, query, fra = urlparse(uri)
+ query = urlencode(_append_params(oauth_params, extract_params(query) or []))
+ return urlunparse((sch, net, path, par, query, fra))
diff --git a/libs/oauthlib/oauth1/rfc5849/signature.py b/libs/oauthlib/oauth1/rfc5849/signature.py
new file mode 100644
index 00000000..99101d43
--- /dev/null
+++ b/libs/oauthlib/oauth1/rfc5849/signature.py
@@ -0,0 +1,501 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+"""
+oauthlib.oauth1.rfc5849.signature
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module represents a direct implementation of `section 3.4`_ of the spec.
+
+Terminology:
+ * Client: software interfacing with an OAuth API
+ * Server: the API provider
+ * Resource Owner: the user who is granting authorization to the client
+
+Steps for signing a request:
+
+1. Collect parameters from the uri query, auth header, & body
+2. Normalize those parameters
+3. Normalize the uri
+4. Pass the normalized uri, normalized parameters, and http method to
+ construct the base string
+5. Pass the base string and any keys needed to a signing function
+
+.. _`section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4
+"""
+import binascii
+import hashlib
+import hmac
+import urlparse
+from . import utils
+from oauthlib.common import extract_params
+
+
+def construct_base_string(http_method, base_string_uri,
+ normalized_encoded_request_parameters):
+ """**String Construction**
+ Per `section 3.4.1.1`_ of the spec.
+
+ For example, the HTTP request::
+
+ POST /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b HTTP/1.1
+ Host: example.com
+ Content-Type: application/x-www-form-urlencoded
+ Authorization: OAuth realm="Example",
+ oauth_consumer_key="9djdj82h48djs9d2",
+ oauth_token="kkk9d7dh3k39sjv7",
+ oauth_signature_method="HMAC-SHA1",
+ oauth_timestamp="137131201",
+ oauth_nonce="7d8f3e4a",
+ oauth_signature="bYT5CMsGcbgUdFHObYMEfcx6bsw%3D"
+
+ c2&a3=2+q
+
+ is represented by the following signature base string (line breaks
+ are for display purposes only)::
+
+ POST&http%3A%2F%2Fexample.com%2Frequest&a2%3Dr%2520b%26a3%3D2%2520q
+ %26a3%3Da%26b5%3D%253D%25253D%26c%2540%3D%26c2%3D%26oauth_consumer_
+ key%3D9djdj82h48djs9d2%26oauth_nonce%3D7d8f3e4a%26oauth_signature_m
+ ethod%3DHMAC-SHA1%26oauth_timestamp%3D137131201%26oauth_token%3Dkkk
+ 9d7dh3k39sjv7
+
+ .. _`section 3.4.1.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.1
+ """
+
+ # The signature base string is constructed by concatenating together,
+ # in order, the following HTTP request elements:
+
+ # 1. The HTTP request method in uppercase. For example: "HEAD",
+ # "GET", "POST", etc. If the request uses a custom HTTP method, it
+ # MUST be encoded (`Section 3.6`_).
+ #
+ # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+ base_string = utils.escape(http_method.upper())
+
+ # 2. An "&" character (ASCII code 38).
+ base_string += u'&'
+
+ # 3. The base string URI from `Section 3.4.1.2`_, after being encoded
+ # (`Section 3.6`_).
+ #
+ # .. _`Section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2
+ # .. _`Section 3.4.6`: http://tools.ietf.org/html/rfc5849#section-3.4.6
+ base_string += utils.escape(base_string_uri)
+
+ # 4. An "&" character (ASCII code 38).
+ base_string += u'&'
+
+ # 5. The request parameters as normalized in `Section 3.4.1.3.2`_, after
+ # being encoded (`Section 3.6`).
+ #
+ # .. _`Section 3.4.1.3.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
+ # .. _`Section 3.4.6`: http://tools.ietf.org/html/rfc5849#section-3.4.6
+ base_string += utils.escape(normalized_encoded_request_parameters)
+
+ return base_string
+
+
+def normalize_base_string_uri(uri):
+ """**Base String URI**
+ Per `section 3.4.1.2`_ of the spec.
+
+ For example, the HTTP request::
+
+ GET /r%20v/X?id=123 HTTP/1.1
+ Host: EXAMPLE.COM:80
+
+ is represented by the base string URI: "http://example.com/r%20v/X".
+
+ In another example, the HTTPS request::
+
+ GET /?q=1 HTTP/1.1
+ Host: www.example.net:8080
+
+ is represented by the base string URI: "https://www.example.net:8080/".
+
+ .. _`section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2
+ """
+ if not isinstance(uri, unicode):
+ raise ValueError('uri must be a unicode object.')
+
+ # FIXME: urlparse does not support unicode
+ scheme, netloc, path, params, query, fragment = urlparse.urlparse(uri)
+
+ # The scheme, authority, and path of the request resource URI `RFC3986`
+ # are included by constructing an "http" or "https" URI representing
+ # the request resource (without the query or fragment) as follows:
+ #
+ # .. _`RFC2616`: http://tools.ietf.org/html/rfc3986
+
+ # 1. The scheme and host MUST be in lowercase.
+ scheme = scheme.lower()
+ netloc = netloc.lower()
+
+ # 2. The host and port values MUST match the content of the HTTP
+ # request "Host" header field.
+ # TODO: enforce this constraint
+
+ # 3. The port MUST be included if it is not the default port for the
+ # scheme, and MUST be excluded if it is the default. Specifically,
+ # the port MUST be excluded when making an HTTP request `RFC2616`_
+ # to port 80 or when making an HTTPS request `RFC2818`_ to port 443.
+ # All other non-default port numbers MUST be included.
+ #
+ # .. _`RFC2616`: http://tools.ietf.org/html/rfc2616
+ # .. _`RFC2818`: http://tools.ietf.org/html/rfc2818
+ default_ports = (
+ (u'http', u'80'),
+ (u'https', u'443'),
+ )
+ if u':' in netloc:
+ host, port = netloc.split(u':', 1)
+ if (scheme, port) in default_ports:
+ netloc = host
+
+ return urlparse.urlunparse((scheme, netloc, path, u'', u'', u''))
+
+
+# ** Request Parameters **
+#
+# Per `section 3.4.1.3`_ of the spec.
+#
+# In order to guarantee a consistent and reproducible representation of
+# the request parameters, the parameters are collected and decoded to
+# their original decoded form. They are then sorted and encoded in a
+# particular manner that is often different from their original
+# encoding scheme, and concatenated into a single string.
+#
+# .. _`section 3.4.1.3`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3
+
+def collect_parameters(uri_query='', body=[], headers=None,
+ exclude_oauth_signature=True):
+ """**Parameter Sources**
+
+ Parameters starting with `oauth_` will be unescaped.
+
+ Body parameters must be supplied as a dict, a list of 2-tuples, or a
+ formencoded query string.
+
+ Headers must be supplied as a dict.
+
+ Per `section 3.4.1.3.1`_ of the spec.
+
+ For example, the HTTP request::
+
+ POST /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b HTTP/1.1
+ Host: example.com
+ Content-Type: application/x-www-form-urlencoded
+ Authorization: OAuth realm="Example",
+ oauth_consumer_key="9djdj82h48djs9d2",
+ oauth_token="kkk9d7dh3k39sjv7",
+ oauth_signature_method="HMAC-SHA1",
+ oauth_timestamp="137131201",
+ oauth_nonce="7d8f3e4a",
+ oauth_signature="djosJKDKJSD8743243%2Fjdk33klY%3D"
+
+ c2&a3=2+q
+
+ contains the following (fully decoded) parameters used in the
+ signature base sting::
+
+ +------------------------+------------------+
+ | Name | Value |
+ +------------------------+------------------+
+ | b5 | =%3D |
+ | a3 | a |
+ | c@ | |
+ | a2 | r b |
+ | oauth_consumer_key | 9djdj82h48djs9d2 |
+ | oauth_token | kkk9d7dh3k39sjv7 |
+ | oauth_signature_method | HMAC-SHA1 |
+ | oauth_timestamp | 137131201 |
+ | oauth_nonce | 7d8f3e4a |
+ | c2 | |
+ | a3 | 2 q |
+ +------------------------+------------------+
+
+ Note that the value of "b5" is "=%3D" and not "==". Both "c@" and
+ "c2" have empty values. While the encoding rules specified in this
+ specification for the purpose of constructing the signature base
+ string exclude the use of a "+" character (ASCII code 43) to
+ represent an encoded space character (ASCII code 32), this practice
+ is widely used in "application/x-www-form-urlencoded" encoded values,
+ and MUST be properly decoded, as demonstrated by one of the "a3"
+ parameter instances (the "a3" parameter is used twice in this
+ request).
+
+ .. _`section 3.4.1.3.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3.1
+ """
+ headers = headers or {}
+ params = []
+
+ # The parameters from the following sources are collected into a single
+ # list of name/value pairs:
+
+ # * The query component of the HTTP request URI as defined by
+ # `RFC3986, Section 3.4`_. The query component is parsed into a list
+ # of name/value pairs by treating it as an
+ # "application/x-www-form-urlencoded" string, separating the names
+ # and values and decoding them as defined by
+ # `W3C.REC-html40-19980424`_, Section 17.13.4.
+ #
+ # .. _`RFC3986, Section 3.4`: http://tools.ietf.org/html/rfc3986#section-3.4
+ # .. _`W3C.REC-html40-19980424`: http://tools.ietf.org/html/rfc5849#ref-W3C.REC-html40-19980424
+ if uri_query:
+ params.extend(urlparse.parse_qsl(uri_query, keep_blank_values=True))
+
+ # * The OAuth HTTP "Authorization" header field (`Section 3.5.1`_) if
+ # present. The header's content is parsed into a list of name/value
+ # pairs excluding the "realm" parameter if present. The parameter
+ # values are decoded as defined by `Section 3.5.1`_.
+ #
+ # .. _`Section 3.5.1`: http://tools.ietf.org/html/rfc5849#section-3.5.1
+ if headers:
+ headers_lower = dict((k.lower(), v) for k, v in headers.items())
+ authorization_header = headers_lower.get(u'authorization')
+ if authorization_header is not None:
+ params.extend([i for i in utils.parse_authorization_header(
+ authorization_header) if i[0] != u'realm'])
+
+ # * The HTTP request entity-body, but only if all of the following
+ # conditions are met:
+ # * The entity-body is single-part.
+ #
+ # * The entity-body follows the encoding requirements of the
+ # "application/x-www-form-urlencoded" content-type as defined by
+ # `W3C.REC-html40-19980424`_.
+
+ # * The HTTP request entity-header includes the "Content-Type"
+ # header field set to "application/x-www-form-urlencoded".
+ #
+ # .._`W3C.REC-html40-19980424`: http://tools.ietf.org/html/rfc5849#ref-W3C.REC-html40-19980424
+
+ # TODO: enforce header param inclusion conditions
+ bodyparams = extract_params(body) or []
+ params.extend(bodyparams)
+
+ # ensure all oauth params are unescaped
+ unescaped_params = []
+ for k, v in params:
+ if k.startswith(u'oauth_'):
+ v = utils.unescape(v)
+ unescaped_params.append((k, v))
+
+ # The "oauth_signature" parameter MUST be excluded from the signature
+ # base string if present.
+ if exclude_oauth_signature:
+ unescaped_params = filter(lambda i: i[0] != u'oauth_signature',
+ unescaped_params)
+
+ return unescaped_params
+
+
+def normalize_parameters(params):
+ """**Parameters Normalization**
+ Per `section 3.4.1.3.2`_ of the spec.
+
+ For example, the list of parameters from the previous section would
+ be normalized as follows:
+
+ Encoded::
+
+ +------------------------+------------------+
+ | Name | Value |
+ +------------------------+------------------+
+ | b5 | %3D%253D |
+ | a3 | a |
+ | c%40 | |
+ | a2 | r%20b |
+ | oauth_consumer_key | 9djdj82h48djs9d2 |
+ | oauth_token | kkk9d7dh3k39sjv7 |
+ | oauth_signature_method | HMAC-SHA1 |
+ | oauth_timestamp | 137131201 |
+ | oauth_nonce | 7d8f3e4a |
+ | c2 | |
+ | a3 | 2%20q |
+ +------------------------+------------------+
+
+ Sorted::
+
+ +------------------------+------------------+
+ | Name | Value |
+ +------------------------+------------------+
+ | a2 | r%20b |
+ | a3 | 2%20q |
+ | a3 | a |
+ | b5 | %3D%253D |
+ | c%40 | |
+ | c2 | |
+ | oauth_consumer_key | 9djdj82h48djs9d2 |
+ | oauth_nonce | 7d8f3e4a |
+ | oauth_signature_method | HMAC-SHA1 |
+ | oauth_timestamp | 137131201 |
+ | oauth_token | kkk9d7dh3k39sjv7 |
+ +------------------------+------------------+
+
+ Concatenated Pairs::
+
+ +-------------------------------------+
+ | Name=Value |
+ +-------------------------------------+
+ | a2=r%20b |
+ | a3=2%20q |
+ | a3=a |
+ | b5=%3D%253D |
+ | c%40= |
+ | c2= |
+ | oauth_consumer_key=9djdj82h48djs9d2 |
+ | oauth_nonce=7d8f3e4a |
+ | oauth_signature_method=HMAC-SHA1 |
+ | oauth_timestamp=137131201 |
+ | oauth_token=kkk9d7dh3k39sjv7 |
+ +-------------------------------------+
+
+ and concatenated together into a single string (line breaks are for
+ display purposes only)::
+
+ a2=r%20b&a3=2%20q&a3=a&b5=%3D%253D&c%40=&c2=&oauth_consumer_key=9dj
+ dj82h48djs9d2&oauth_nonce=7d8f3e4a&oauth_signature_method=HMAC-SHA1
+ &oauth_timestamp=137131201&oauth_token=kkk9d7dh3k39sjv7
+
+ .. _`section 3.4.1.3.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
+ """
+
+ # The parameters collected in `Section 3.4.1.3`_ are normalized into a
+ # single string as follows:
+ #
+ # .. _`Section 3.4.1.3`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3
+
+ # 1. First, the name and value of each parameter are encoded
+ # (`Section 3.6`_).
+ #
+ # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+ key_values = [(utils.escape(k), utils.escape(v)) for k, v in params]
+
+ # 2. The parameters are sorted by name, using ascending byte value
+ # ordering. If two or more parameters share the same name, they
+ # are sorted by their value.
+ key_values.sort()
+
+ # 3. The name of each parameter is concatenated to its corresponding
+ # value using an "=" character (ASCII code 61) as a separator, even
+ # if the value is empty.
+ parameter_parts = [u'{0}={1}'.format(k, v) for k, v in key_values]
+
+ # 4. The sorted name/value pairs are concatenated together into a
+ # single string by using an "&" character (ASCII code 38) as
+ # separator.
+ return u'&'.join(parameter_parts)
+
+
+def sign_hmac_sha1(base_string, client_secret, resource_owner_secret):
+ """**HMAC-SHA1**
+
+ The "HMAC-SHA1" signature method uses the HMAC-SHA1 signature
+ algorithm as defined in `RFC2104`_::
+
+ digest = HMAC-SHA1 (key, text)
+
+ Per `section 3.4.2`_ of the spec.
+
+ .. _`RFC2104`: http://tools.ietf.org/html/rfc2104
+ .. _`section 3.4.2`: http://tools.ietf.org/html/rfc5849#section-3.4.2
+ """
+
+ # The HMAC-SHA1 function variables are used in following way:
+
+ # text is set to the value of the signature base string from
+ # `Section 3.4.1.1`_.
+ #
+ # .. _`Section 3.4.1.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.1
+ text = base_string
+
+ # key is set to the concatenated values of:
+ # 1. The client shared-secret, after being encoded (`Section 3.6`_).
+ #
+ # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+ key = utils.escape(client_secret or u'')
+
+ # 2. An "&" character (ASCII code 38), which MUST be included
+ # even when either secret is empty.
+ key += u'&'
+
+ # 3. The token shared-secret, after being encoded (`Section 3.6`_).
+ #
+ # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+ key += utils.escape(resource_owner_secret or u'')
+
+ # FIXME: HMAC does not support unicode!
+ key_utf8 = key.encode('utf-8')
+ text_utf8 = text.encode('utf-8')
+ signature = hmac.new(key_utf8, text_utf8, hashlib.sha1)
+
+ # digest is used to set the value of the "oauth_signature" protocol
+ # parameter, after the result octet string is base64-encoded
+ # per `RFC2045, Section 6.8`.
+ #
+ # .. _`RFC2045, Section 6.8`: http://tools.ietf.org/html/rfc2045#section-6.8
+ return binascii.b2a_base64(signature.digest())[:-1].decode('utf-8')
+
+
+def sign_rsa_sha1(base_string, rsa_private_key):
+ """**RSA-SHA1**
+
+ Per `section 3.4.3`_ of the spec.
+
+ The "RSA-SHA1" signature method uses the RSASSA-PKCS1-v1_5 signature
+ algorithm as defined in `RFC3447, Section 8.2`_ (also known as
+ PKCS#1), using SHA-1 as the hash function for EMSA-PKCS1-v1_5. To
+ use this method, the client MUST have established client credentials
+ with the server that included its RSA public key (in a manner that is
+ beyond the scope of this specification).
+
+ NOTE: this method requires the python-rsa library.
+
+ .. _`section 3.4.3`: http://tools.ietf.org/html/rfc5849#section-3.4.3
+ .. _`RFC3447, Section 8.2`: http://tools.ietf.org/html/rfc3447#section-8.2
+
+ """
+
+ # TODO: finish RSA documentation
+
+ import rsa
+ key = rsa.PrivateKey.load_pkcs1(rsa_private_key)
+ sig = rsa.sign(base_string, key, 'SHA-1')
+ return binascii.b2a_base64(sig)[:-1]
+
+
+def sign_plaintext(client_secret, resource_owner_secret):
+ """Sign a request using plaintext.
+
+ Per `section 3.4.4`_ of the spec.
+
+ The "PLAINTEXT" method does not employ a signature algorithm. It
+ MUST be used with a transport-layer mechanism such as TLS or SSL (or
+ sent over a secure channel with equivalent protections). It does not
+ utilize the signature base string or the "oauth_timestamp" and
+ "oauth_nonce" parameters.
+
+ .. _`section 3.4.4`: http://tools.ietf.org/html/rfc5849#section-3.4.4
+
+ """
+
+ # The "oauth_signature" protocol parameter is set to the concatenated
+ # value of:
+
+ # 1. The client shared-secret, after being encoded (`Section 3.6`_).
+ #
+ # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+ signature = utils.escape(client_secret or u'')
+
+ # 2. An "&" character (ASCII code 38), which MUST be included even
+ # when either secret is empty.
+ signature += u'&'
+
+ # 3. The token shared-secret, after being encoded (`Section 3.6`_).
+ #
+ # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+ signature += utils.escape(resource_owner_secret or u'')
+
+ return signature
+
diff --git a/libs/oauthlib/oauth1/rfc5849/utils.py b/libs/oauthlib/oauth1/rfc5849/utils.py
new file mode 100644
index 00000000..6db446fd
--- /dev/null
+++ b/libs/oauthlib/oauth1/rfc5849/utils.py
@@ -0,0 +1,141 @@
+# -*- coding: utf-8 -*-
+
+"""
+oauthlib.utils
+~~~~~~~~~~~~~~
+
+This module contains utility methods used by various parts of the OAuth
+spec.
+"""
+
+import string
+import time
+import urllib2
+from random import getrandbits, choice
+
+from oauthlib.common import quote, unquote
+
+UNICODE_ASCII_CHARACTER_SET = (string.ascii_letters.decode('ascii') +
+ string.digits.decode('ascii'))
+
+
+def filter_params(target):
+ """Decorator which filters params to remove non-oauth_* parameters
+
+ Assumes the decorated method takes a params dict or list of tuples as its
+ first argument.
+ """
+ def wrapper(params, *args, **kwargs):
+ params = filter_oauth_params(params)
+ return target(params, *args, **kwargs)
+
+ wrapper.__doc__ = target.__doc__
+ return wrapper
+
+
+def filter_oauth_params(params):
+ """Removes all non oauth parameters from a dict or a list of params."""
+ is_oauth = lambda kv: kv[0].startswith(u"oauth_")
+ if isinstance(params, dict):
+ return filter(is_oauth, params.items())
+ else:
+ return filter(is_oauth, params)
+
+
+def generate_timestamp():
+ """Get seconds since epoch (UTC).
+
+ Per `section 3.3`_ of the spec.
+
+ .. _`section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3
+ """
+ return unicode(int(time.time()))
+
+
+def generate_nonce():
+ """Generate pseudorandom nonce that is unlikely to repeat.
+
+ Per `section 3.3`_ of the spec.
+
+ A random 64-bit number is appended to the epoch timestamp for both
+ randomness and to decrease the likelihood of collisions.
+
+ .. _`section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3
+ """
+ return unicode(getrandbits(64)) + generate_timestamp()
+
+
+def generate_token(length=20, chars=UNICODE_ASCII_CHARACTER_SET):
+ """Generates a generic OAuth token
+
+ According to `section 2`_ of the spec, the method of token
+ construction is undefined. This implementation is simply a random selection
+ of `length` choices from `chars`.
+
+ Credit to Ignacio Vazquez-Abrams for his excellent `Stackoverflow answer`_
+
+ .. _`Stackoverflow answer` : http://stackoverflow.com/questions/2257441/
+ python-random-string-generation-with-upper-case-letters-and-digits
+
+ """
+ return u''.join(choice(chars) for x in range(length))
+
+
+def escape(u):
+ """Escape a unicode string in an OAuth-compatible fashion.
+
+ Per `section 3.6`_ of the spec.
+
+ .. _`section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+
+ """
+ if not isinstance(u, unicode):
+ raise ValueError('Only unicode objects are escapable.')
+ # Letters, digits, and the characters '_.-' are already treated as safe
+ # by urllib.quote(). We need to add '~' to fully support rfc5849.
+ return quote(u, safe='~')
+
+
+def unescape(u):
+ if not isinstance(u, unicode):
+ raise ValueError('Only unicode objects are unescapable.')
+ return unquote(u)
+
+
+def urlencode(query):
+ """Encode a sequence of two-element tuples or dictionary into a URL query string.
+
+ Operates using an OAuth-safe escape() method, in contrast to urllib.urlencode.
+ """
+ # Convert dictionaries to list of tuples
+ if isinstance(query, dict):
+ query = query.items()
+ return u"&".join([u'='.join([escape(k), escape(v)]) for k, v in query])
+
+
+def parse_keqv_list(l):
+ """A unicode-safe version of urllib2.parse_keqv_list"""
+ encoded_list = [u.encode('utf-8') for u in l]
+ encoded_parsed = urllib2.parse_keqv_list(encoded_list)
+ return dict((k.decode('utf-8'),
+ v.decode('utf-8')) for k,v in encoded_parsed.items())
+
+
+def parse_http_list(u):
+ """A unicode-safe version of urllib2.parse_http_list"""
+ encoded_str = u.encode('utf-8')
+ encoded_list = urllib2.parse_http_list(encoded_str)
+ return [s.decode('utf-8') for s in encoded_list]
+
+
+def parse_authorization_header(authorization_header):
+ """Parse an OAuth authorization header into a list of 2-tuples"""
+ auth_scheme = u'OAuth '
+ if authorization_header.startswith(auth_scheme):
+ authorization_header = authorization_header.replace(auth_scheme, u'', 1)
+ items = parse_http_list(authorization_header)
+ try:
+ return parse_keqv_list(items).items()
+ except ValueError:
+ raise ValueError('Malformed authorization header')
+
diff --git a/libs/oauthlib/oauth2/__init__.py b/libs/oauthlib/oauth2/__init__.py
new file mode 100644
index 00000000..0e8933cf
--- /dev/null
+++ b/libs/oauthlib/oauth2/__init__.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+"""
+oauthlib.oauth2
+~~~~~~~~~~~~~~
+
+This module is a wrapper for the most recent implementation of OAuth 2.0 Client
+and Server classes.
+"""
+
+from .draft25 import Client, Server
+
diff --git a/libs/oauthlib/oauth2/draft25/__init__.py b/libs/oauthlib/oauth2/draft25/__init__.py
new file mode 100644
index 00000000..3e50a18f
--- /dev/null
+++ b/libs/oauthlib/oauth2/draft25/__init__.py
@@ -0,0 +1,14 @@
+"""
+oauthlib.oauth2.draft_25
+~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for signing and checking OAuth 2.0 draft 25 requests.
+"""
+
+class Client(object):
+ pass
+
+class Server(object):
+ pass
+
diff --git a/libs/oauthlib/oauth2/draft25/tokens.py b/libs/oauthlib/oauth2/draft25/tokens.py
new file mode 100644
index 00000000..9b5f5868
--- /dev/null
+++ b/libs/oauthlib/oauth2/draft25/tokens.py
@@ -0,0 +1,131 @@
+from __future__ import absolute_import
+"""
+oauthlib.oauth2.draft25.tokens
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module contains methods for adding two types of access tokens to requests.
+
+- Bearer http://tools.ietf.org/html/draft-ietf-oauth-saml2-bearer-08
+- MAC http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-00
+
+"""
+from binascii import b2a_base64
+import hashlib
+import hmac
+from urlparse import urlparse
+
+from . import utils
+
+
+def prepare_mac_header(token, uri, key, http_method, nonce=None, headers=None,
+ body=None, ext=u'', hash_algorithm=u'hmac-sha-1'):
+ """Add an `MAC Access Authentication`_ signature to headers.
+
+ Unlike OAuth 1, this HMAC signature does not require inclusion of the request
+ payload/body, neither does it use a combination of client_secret and
+ token_secret but rather a mac_key provided together with the access token.
+
+ Currently two algorithms are supported, "hmac-sha-1" and "hmac-sha-256",
+ `extension algorithms`_ are not supported.
+
+ Example MAC Authorization header, linebreaks added for clarity
+
+ Authorization: MAC id="h480djs93hd8",
+ nonce="1336363200:dj83hs9s",
+ mac="bhCQXTVyfj5cmA9uKkPFx1zeOXM="
+
+ .. _`MAC Access Authentication`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01
+ .. _`extension algorithms`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-7.1
+
+ :param uri: Request URI.
+ :param headers: Request headers as a dictionary.
+ :param http_method: HTTP Request method.
+ :param key: MAC given provided by token endpoint.
+ :param algorithm: HMAC algorithm provided by token endpoint.
+ :return: headers dictionary with the authorization field added.
+ """
+ http_method = http_method.upper()
+ host, port = utils.host_from_uri(uri)
+
+ if hash_algorithm.lower() == u'hmac-sha-1':
+ h = hashlib.sha1
+ else:
+ h = hashlib.sha256
+
+ nonce = nonce or u'{0}:{1}'.format(utils.generate_nonce(), utils.generate_timestamp())
+ sch, net, path, par, query, fra = urlparse(uri)
+
+ if query:
+ request_uri = path + u'?' + query
+ else:
+ request_uri = path
+
+ # Hash the body/payload
+ if body is not None:
+ bodyhash = b2a_base64(h(body).digest())[:-1].decode('utf-8')
+ else:
+ bodyhash = u''
+
+ # Create the normalized base string
+ base = []
+ base.append(nonce)
+ base.append(http_method.upper())
+ base.append(request_uri)
+ base.append(host)
+ base.append(port)
+ base.append(bodyhash)
+ base.append(ext)
+ base_string = '\n'.join(base) + u'\n'
+
+ # hmac struggles with unicode strings - http://bugs.python.org/issue5285
+ if isinstance(key, unicode):
+ key = key.encode('utf-8')
+ sign = hmac.new(key, base_string, h)
+ sign = b2a_base64(sign.digest())[:-1].decode('utf-8')
+
+ header = []
+ header.append(u'MAC id="%s"' % token)
+ header.append(u'nonce="%s"' % nonce)
+ if bodyhash:
+ header.append(u'bodyhash="%s"' % bodyhash)
+ if ext:
+ header.append(u'ext="%s"' % ext)
+ header.append(u'mac="%s"' % sign)
+
+ headers = headers or {}
+ headers[u'Authorization'] = u', '.join(header)
+ return headers
+
+
+def prepare_bearer_uri(token, uri):
+ """Add a `Bearer Token`_ to the request URI.
+ Not recommended, use only if client can't use authorization header or body.
+
+ http://www.example.com/path?access_token=h480djs93hd8
+
+ .. _`Bearer Token`: http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-18
+ """
+ return utils.add_params_to_uri(uri, [((u'access_token', token))])
+
+
+def prepare_bearer_headers(token, headers=None):
+ """Add a `Bearer Token`_ to the request URI.
+ Recommended method of passing bearer tokens.
+
+ Authorization: Bearer h480djs93hd8
+
+ .. _`Bearer Token`: http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-18
+ """
+ headers = headers or {}
+ headers[u'Authorization'] = u'Bearer %s' % token
+ return headers
+
+
+def prepare_bearer_body(token, body=u''):
+ """Add a `Bearer Token`_ to the request body.
+
+ access_token=h480djs93hd8
+
+ .. _`Bearer Token`: http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-18
+ """
+ return utils.add_params_to_qs(body, [((u'access_token', token))])
diff --git a/libs/oauthlib/oauth2/draft25/utils.py b/libs/oauthlib/oauth2/draft25/utils.py
new file mode 100644
index 00000000..48b4ea1d
--- /dev/null
+++ b/libs/oauthlib/oauth2/draft25/utils.py
@@ -0,0 +1,128 @@
+"""
+oauthlib.utils
+~~~~~~~~~~~~~~
+
+This module contains utility methods used by various parts of the OAuth 2 spec.
+"""
+
+import random
+import string
+import time
+import urllib
+from urlparse import urlparse, urlunparse, parse_qsl
+
+UNICODE_ASCII_CHARACTER_SET = (string.ascii_letters.decode('ascii') +
+ string.digits.decode('ascii'))
+
+def add_params_to_qs(query, params):
+ """Extend a query with a list of two-tuples.
+
+ :param query: Query string.
+ :param params: List of two-tuples.
+ :return: extended query
+ """
+ queryparams = parse_qsl(query, keep_blank_values=True)
+ queryparams.extend(params)
+ return urlencode(queryparams)
+
+
+def add_params_to_uri(uri, params):
+ """Add a list of two-tuples to the uri query components.
+
+ :param uri: Full URI.
+ :param params: List of two-tuples.
+ :return: uri with extended query
+ """
+ sch, net, path, par, query, fra = urlparse(uri)
+ query = add_params_to_qs(query, params)
+ return urlunparse((sch, net, path, par, query, fra))
+
+
+def escape(u):
+ """Escape a string in an OAuth-compatible fashion.
+
+ Per `section 3.6`_ of the spec.
+
+ .. _`section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+
+ """
+ if not isinstance(u, unicode):
+ raise ValueError('Only unicode objects are escapable.')
+ return urllib.quote(u.encode('utf-8'), safe='~')
+
+
+def generate_nonce():
+ """Generate pseudorandom nonce that is unlikely to repeat.
+
+ Per `section 3.2.1`_ of the MAC Access Authentication spec.
+
+ A random 64-bit number is appended to the epoch timestamp for both
+ randomness and to decrease the likelihood of collisions.
+
+ .. _`section 3.2.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1
+ """
+ return unicode(unicode(random.getrandbits(64)) + generate_timestamp())
+
+
+def generate_timestamp():
+ """Get seconds since epoch (UTC).
+
+ Per `section 3.2.1`_ of the MAC Access Authentication spec.
+
+ .. _`section 3.2.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1
+ """
+ return unicode(int(time.time()))
+
+
+def generate_token(length=20, chars=UNICODE_ASCII_CHARACTER_SET):
+ """Generates a generic OAuth 2 token
+
+ According to `section 1.4`_ and `section 1.5` of the spec, the method of token
+ construction is undefined. This implementation is simply a random selection
+ of `length` choices from `chars`. SystemRandom is used since it provides
+ higher entropy than random.choice.
+
+ .. _`section 1.4`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-1.4
+ .. _`section 1.5`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-1.5
+ """
+ rand = random.SystemRandom()
+ return u''.join(rand.choice(chars) for x in range(length))
+
+
+def host_from_uri(uri):
+ """Extract hostname and port from URI.
+
+ Will use default port for HTTP and HTTPS if none is present in the URI.
+
+ >>> host_from_uri(u'https://www.example.com/path?query')
+ u'www.example.com', u'443'
+ >>> host_from_uri(u'http://www.example.com:8080/path?query')
+ u'www.example.com', u'8080'
+
+ :param uri: Full URI.
+ :param http_method: HTTP request method.
+ :return: hostname, port
+ """
+ default_ports = {
+ u'HTTP' : u'80',
+ u'HTTPS' : u'443',
+ }
+
+ sch, netloc, path, par, query, fra = urlparse(uri)
+ if u':' in netloc:
+ netloc, port = netloc.split(u':', 1)
+ else:
+ port = default_ports.get(sch.upper())
+
+ return netloc, port
+
+
+def urlencode(query):
+ """Encode a sequence of two-element tuples or dictionary into a URL query string.
+
+ Operates using an OAuth-safe escape() method, in contrast to urllib.urlenocde.
+ """
+ # Convert dictionaries to list of tuples
+ if isinstance(query, dict):
+ query = query.items()
+ return "&".join(['='.join([escape(k), escape(v)]) for k, v in query])
diff --git a/libs/pyasn1/__init__.py b/libs/pyasn1/__init__.py
new file mode 100644
index 00000000..7de39fe5
--- /dev/null
+++ b/libs/pyasn1/__init__.py
@@ -0,0 +1 @@
+majorVersionId = '1'
diff --git a/libs/pyasn1/codec/__init__.py b/libs/pyasn1/codec/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/libs/pyasn1/codec/ber/__init__.py b/libs/pyasn1/codec/ber/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/libs/pyasn1/codec/ber/decoder.py b/libs/pyasn1/codec/ber/decoder.py
new file mode 100644
index 00000000..ae9311cb
--- /dev/null
+++ b/libs/pyasn1/codec/ber/decoder.py
@@ -0,0 +1,746 @@
+# BER decoder
+from pyasn1.type import tag, base, univ, char, useful, tagmap
+from pyasn1.codec.ber import eoo
+from pyasn1.compat.octets import oct2int, octs2ints
+from pyasn1 import error
+
+class AbstractDecoder:
+ protoComponent = None
+ def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet,
+ length, state, decodeFun):
+ raise error.PyAsn1Error('Decoder not implemented for %s' % tagSet)
+
+ def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet,
+ length, state, decodeFun):
+ raise error.PyAsn1Error('Indefinite length mode decoder not implemented for %s' % tagSet)
+
+class AbstractSimpleDecoder(AbstractDecoder):
+ def _createComponent(self, asn1Spec, tagSet, value=None):
+ if asn1Spec is None:
+ return self.protoComponent.clone(value, tagSet)
+ elif value is None:
+ return asn1Spec
+ else:
+ return asn1Spec.clone(value)
+
+class AbstractConstructedDecoder(AbstractDecoder):
+ def _createComponent(self, asn1Spec, tagSet, value=None):
+ if asn1Spec is None:
+ return self.protoComponent.clone(tagSet)
+ else:
+ return asn1Spec.clone()
+
+class EndOfOctetsDecoder(AbstractSimpleDecoder):
+ def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet,
+ length, state, decodeFun):
+ return eoo.endOfOctets, substrate[:length]
+
+class ExplicitTagDecoder(AbstractSimpleDecoder):
+ def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet,
+ length, state, decodeFun):
+ return decodeFun(substrate[:length], asn1Spec, tagSet, length)
+
+ def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet,
+ length, state, decodeFun):
+ value, substrate = decodeFun(substrate, asn1Spec, tagSet, length)
+ terminator, substrate = decodeFun(substrate)
+ if terminator == eoo.endOfOctets:
+ return value, substrate
+ else:
+ raise error.PyAsn1Error('Missing end-of-octets terminator')
+
+explicitTagDecoder = ExplicitTagDecoder()
+
+class IntegerDecoder(AbstractSimpleDecoder):
+ protoComponent = univ.Integer(0)
+ precomputedValues = {
+ '\x00': 0,
+ '\x01': 1,
+ '\x02': 2,
+ '\x03': 3,
+ '\x04': 4,
+ '\x05': 5,
+ '\x06': 6,
+ '\x07': 7,
+ '\x08': 8,
+ '\x09': 9,
+ '\xff': -1,
+ '\xfe': -2,
+ '\xfd': -3,
+ '\xfc': -4,
+ '\xfb': -5
+ }
+
+ def _valueFilter(self, value):
+ try:
+ return int(value)
+ except OverflowError:
+ return value
+
+ def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, length,
+ state, decodeFun):
+ substrate = substrate[:length]
+ if not substrate:
+ raise error.PyAsn1Error('Empty substrate')
+ if substrate in self.precomputedValues:
+ value = self.precomputedValues[substrate]
+ else:
+ firstOctet = oct2int(substrate[0])
+ if firstOctet & 0x80:
+ value = -1
+ else:
+ value = 0
+ for octet in substrate:
+ value = value << 8 | oct2int(octet)
+ value = self._valueFilter(value)
+ return self._createComponent(asn1Spec, tagSet, value), substrate
+
+class BooleanDecoder(IntegerDecoder):
+ protoComponent = univ.Boolean(0)
+ def _valueFilter(self, value):
+ if value:
+ return 1
+ else:
+ return 0
+
+class BitStringDecoder(AbstractSimpleDecoder):
+ protoComponent = univ.BitString(())
+ def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, length,
+ state, decodeFun):
+ substrate = substrate[:length]
+ if tagSet[0][1] == tag.tagFormatSimple: # XXX what tag to check?
+ if not substrate:
+ raise error.PyAsn1Error('Missing initial octet')
+ trailingBits = oct2int(substrate[0])
+ if trailingBits > 7:
+ raise error.PyAsn1Error(
+ 'Trailing bits overflow %s' % trailingBits
+ )
+ substrate = substrate[1:]
+ lsb = p = 0; l = len(substrate)-1; b = ()
+ while p <= l:
+ if p == l:
+ lsb = trailingBits
+ j = 7
+ o = oct2int(substrate[p])
+ while j >= lsb:
+ b = b + ((o>>j)&0x01,)
+ j = j - 1
+ p = p + 1
+ return self._createComponent(asn1Spec, tagSet, b), ''
+ r = self._createComponent(asn1Spec, tagSet, ())
+ if not decodeFun:
+ return r, substrate
+ while substrate:
+ component, substrate = decodeFun(substrate)
+ r = r + component
+ return r, substrate
+
+ def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet,
+ length, state, decodeFun):
+ r = self._createComponent(asn1Spec, tagSet, '')
+ if not decodeFun:
+ return r, substrate
+ while substrate:
+ component, substrate = decodeFun(substrate)
+ if component == eoo.endOfOctets:
+ break
+ r = r + component
+ else:
+ raise error.SubstrateUnderrunError(
+ 'No EOO seen before substrate ends'
+ )
+ return r, substrate
+
+class OctetStringDecoder(AbstractSimpleDecoder):
+ protoComponent = univ.OctetString('')
+ def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, length,
+ state, decodeFun):
+ substrate = substrate[:length]
+ if tagSet[0][1] == tag.tagFormatSimple: # XXX what tag to check?
+ return self._createComponent(asn1Spec, tagSet, substrate), ''
+ r = self._createComponent(asn1Spec, tagSet, '')
+ if not decodeFun:
+ return r, substrate
+ while substrate:
+ component, substrate = decodeFun(substrate)
+ r = r + component
+ return r, substrate
+
+ def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet,
+ length, state, decodeFun):
+ r = self._createComponent(asn1Spec, tagSet, '')
+ if not decodeFun:
+ return r, substrate
+ while substrate:
+ component, substrate = decodeFun(substrate)
+ if component == eoo.endOfOctets:
+ break
+ r = r + component
+ else:
+ raise error.SubstrateUnderrunError(
+ 'No EOO seen before substrate ends'
+ )
+ return r, substrate
+
+class NullDecoder(AbstractSimpleDecoder):
+ protoComponent = univ.Null('')
+ def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet,
+ length, state, decodeFun):
+ substrate = substrate[:length]
+ r = self._createComponent(asn1Spec, tagSet)
+ if substrate:
+ raise error.PyAsn1Error('Unexpected substrate for Null')
+ return r, substrate
+
+class ObjectIdentifierDecoder(AbstractSimpleDecoder):
+ protoComponent = univ.ObjectIdentifier(())
+ def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, length,
+ state, decodeFun):
+ substrate = substrate[:length]
+ if not substrate:
+ raise error.PyAsn1Error('Empty substrate')
+ oid = (); index = 0
+ # Get the first subid
+ subId = oct2int(substrate[index])
+ oid = oid + divmod(subId, 40)
+
+ index = index + 1
+ substrateLen = len(substrate)
+
+ while index < substrateLen:
+ subId = oct2int(substrate[index])
+ if subId < 128:
+ oid = oid + (subId,)
+ index = index + 1
+ else:
+ # Construct subid from a number of octets
+ nextSubId = subId
+ subId = 0
+ while nextSubId >= 128 and index < substrateLen:
+ subId = (subId << 7) + (nextSubId & 0x7F)
+ index = index + 1
+ nextSubId = oct2int(substrate[index])
+ if index == substrateLen:
+ raise error.SubstrateUnderrunError(
+ 'Short substrate for OID %s' % oid
+ )
+ subId = (subId << 7) + nextSubId
+ oid = oid + (subId,)
+ index = index + 1
+ return self._createComponent(asn1Spec, tagSet, oid), substrate[index:]
+
+class RealDecoder(AbstractSimpleDecoder):
+ protoComponent = univ.Real()
+ def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet,
+ length, state, decodeFun):
+ substrate = substrate[:length]
+ if not length:
+ raise error.SubstrateUnderrunError('Short substrate for Real')
+ fo = oct2int(substrate[0]); substrate = substrate[1:]
+ if fo & 0x40: # infinite value
+ value = fo & 0x01 and '-inf' or 'inf'
+ elif fo & 0x80: # binary enoding
+ if fo & 0x11 == 0:
+ n = 1
+ elif fo & 0x01:
+ n = 2
+ elif fo & 0x02:
+ n = 3
+ else:
+ n = oct2int(substrate[0])
+ eo, substrate = substrate[:n], substrate[n:]
+ if not eo or not substrate:
+ raise error.PyAsn1Error('Real exponent screwed')
+ e = 0
+ while eo: # exponent
+ e <<= 8
+ e |= oct2int(eo[0])
+ eo = eo[1:]
+ p = 0
+ while substrate: # value
+ p <<= 8
+ p |= oct2int(substrate[0])
+ substrate = substrate[1:]
+ if fo & 0x40: # sign bit
+ p = -p
+ value = (p, 2, e)
+ elif fo & 0xc0 == 0: # character encoding
+ try:
+ if fo & 0x3 == 0x1: # NR1
+ value = (int(substrate), 10, 0)
+ elif fo & 0x3 == 0x2: # NR2
+ value = float(substrate)
+ elif fo & 0x3 == 0x3: # NR3
+ value = float(substrate)
+ else:
+ raise error.SubstrateUnderrunError(
+ 'Unknown NR (tag %s)' % fo
+ )
+ except ValueError:
+ raise error.SubstrateUnderrunError(
+ 'Bad character Real syntax'
+ )
+ elif fo & 0xc0 == 0x40: # special real value
+ pass
+ else:
+ raise error.SubstrateUnderrunError(
+ 'Unknown encoding (tag %s)' % fo
+ )
+ return self._createComponent(asn1Spec, tagSet, value), substrate
+
+class SequenceDecoder(AbstractConstructedDecoder):
+ protoComponent = univ.Sequence()
+ def _getComponentTagMap(self, r, idx):
+ try:
+ return r.getComponentTagMapNearPosition(idx)
+ except error.PyAsn1Error:
+ return
+
+ def _getComponentPositionByType(self, r, t, idx):
+ return r.getComponentPositionNearType(t, idx)
+
+ def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet,
+ length, state, decodeFun):
+ substrate = substrate[:length]
+ r = self._createComponent(asn1Spec, tagSet)
+ idx = 0
+ if not decodeFun:
+ return r, substrate
+ while substrate:
+ asn1Spec = self._getComponentTagMap(r, idx)
+ component, substrate = decodeFun(
+ substrate, asn1Spec
+ )
+ idx = self._getComponentPositionByType(
+ r, component.getEffectiveTagSet(), idx
+ )
+ r.setComponentByPosition(idx, component, asn1Spec is None)
+ idx = idx + 1
+ r.setDefaultComponents()
+ r.verifySizeSpec()
+ return r, substrate
+
+ def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet,
+ length, state, decodeFun):
+ r = self._createComponent(asn1Spec, tagSet)
+ idx = 0
+ while substrate:
+ asn1Spec = self._getComponentTagMap(r, idx)
+ if not decodeFun:
+ return r, substrate
+ component, substrate = decodeFun(substrate, asn1Spec)
+ if component == eoo.endOfOctets:
+ break
+ idx = self._getComponentPositionByType(
+ r, component.getEffectiveTagSet(), idx
+ )
+ r.setComponentByPosition(idx, component, asn1Spec is None)
+ idx = idx + 1
+ else:
+ raise error.SubstrateUnderrunError(
+ 'No EOO seen before substrate ends'
+ )
+ r.setDefaultComponents()
+ r.verifySizeSpec()
+ return r, substrate
+
+class SequenceOfDecoder(AbstractConstructedDecoder):
+ protoComponent = univ.SequenceOf()
+ def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet,
+ length, state, decodeFun):
+ substrate = substrate[:length]
+ r = self._createComponent(asn1Spec, tagSet)
+ asn1Spec = r.getComponentType()
+ idx = 0
+ if not decodeFun:
+ return r, substrate
+ while substrate:
+ component, substrate = decodeFun(
+ substrate, asn1Spec
+ )
+ r.setComponentByPosition(idx, component, asn1Spec is None)
+ idx = idx + 1
+ r.verifySizeSpec()
+ return r, substrate
+
+ def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet,
+ length, state, decodeFun):
+ r = self._createComponent(asn1Spec, tagSet)
+ asn1Spec = r.getComponentType()
+ idx = 0
+ if not decodeFun:
+ return r, substrate
+ while substrate:
+ component, substrate = decodeFun(substrate, asn1Spec)
+ if component == eoo.endOfOctets:
+ break
+ r.setComponentByPosition(idx, component, asn1Spec is None)
+ idx = idx + 1
+ else:
+ raise error.SubstrateUnderrunError(
+ 'No EOO seen before substrate ends'
+ )
+ r.verifySizeSpec()
+ return r, substrate
+
+class SetDecoder(SequenceDecoder):
+ protoComponent = univ.Set()
+ def _getComponentTagMap(self, r, idx):
+ return r.getComponentTagMap()
+
+ def _getComponentPositionByType(self, r, t, idx):
+ nextIdx = r.getComponentPositionByType(t)
+ if nextIdx is None:
+ return idx
+ else:
+ return nextIdx
+
+class SetOfDecoder(SequenceOfDecoder):
+ protoComponent = univ.SetOf()
+
+class ChoiceDecoder(AbstractConstructedDecoder):
+ protoComponent = univ.Choice()
+ def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet,
+ length, state, decodeFun):
+ substrate = substrate[:length]
+ r = self._createComponent(asn1Spec, tagSet)
+ if not decodeFun:
+ return r, substrate
+ if r.getTagSet() == tagSet: # explicitly tagged Choice
+ component, substrate = decodeFun(
+ substrate, r.getComponentTagMap()
+ )
+ else:
+ component, substrate = decodeFun(
+ substrate, r.getComponentTagMap(), tagSet, length, state
+ )
+ if isinstance(component, univ.Choice):
+ effectiveTagSet = component.getEffectiveTagSet()
+ else:
+ effectiveTagSet = component.getTagSet()
+ r.setComponentByType(effectiveTagSet, component, 0, asn1Spec is None)
+ return r, substrate
+
+ indefLenValueDecoder = valueDecoder
+
+class AnyDecoder(AbstractSimpleDecoder):
+ protoComponent = univ.Any()
+ def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet,
+ length, state, decodeFun):
+ if asn1Spec is None or \
+ asn1Spec is not None and tagSet != asn1Spec.getTagSet():
+ # untagged Any container, recover inner header substrate
+ length = length + len(fullSubstrate) - len(substrate)
+ substrate = fullSubstrate
+ substrate = substrate[:length]
+ return self._createComponent(asn1Spec, tagSet, value=substrate), ''
+
+ def indefLenValueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet,
+ length, state, decodeFun):
+ if asn1Spec is not None and tagSet == asn1Spec.getTagSet():
+ # tagged Any type -- consume header substrate
+ header = ''
+ else:
+ # untagged Any, recover header substrate
+ header = fullSubstrate[:-len(substrate)]
+
+ r = self._createComponent(asn1Spec, tagSet, header)
+
+ # Any components do not inherit initial tag
+ asn1Spec = self.protoComponent
+
+ if not decodeFun:
+ return r, substrate
+ while substrate:
+ component, substrate = decodeFun(substrate, asn1Spec)
+ if component == eoo.endOfOctets:
+ break
+ r = r + component
+ else:
+ raise error.SubstrateUnderrunError(
+ 'No EOO seen before substrate ends'
+ )
+ return r, substrate
+
+# character string types
+class UTF8StringDecoder(OctetStringDecoder):
+ protoComponent = char.UTF8String()
+class NumericStringDecoder(OctetStringDecoder):
+ protoComponent = char.NumericString()
+class PrintableStringDecoder(OctetStringDecoder):
+ protoComponent = char.PrintableString()
+class TeletexStringDecoder(OctetStringDecoder):
+ protoComponent = char.TeletexString()
+class VideotexStringDecoder(OctetStringDecoder):
+ protoComponent = char.VideotexString()
+class IA5StringDecoder(OctetStringDecoder):
+ protoComponent = char.IA5String()
+class GraphicStringDecoder(OctetStringDecoder):
+ protoComponent = char.GraphicString()
+class VisibleStringDecoder(OctetStringDecoder):
+ protoComponent = char.VisibleString()
+class GeneralStringDecoder(OctetStringDecoder):
+ protoComponent = char.GeneralString()
+class UniversalStringDecoder(OctetStringDecoder):
+ protoComponent = char.UniversalString()
+class BMPStringDecoder(OctetStringDecoder):
+ protoComponent = char.BMPString()
+
+# "useful" types
+class GeneralizedTimeDecoder(OctetStringDecoder):
+ protoComponent = useful.GeneralizedTime()
+class UTCTimeDecoder(OctetStringDecoder):
+ protoComponent = useful.UTCTime()
+
+tagMap = {
+ eoo.endOfOctets.tagSet: EndOfOctetsDecoder(),
+ univ.Integer.tagSet: IntegerDecoder(),
+ univ.Boolean.tagSet: BooleanDecoder(),
+ univ.BitString.tagSet: BitStringDecoder(),
+ univ.OctetString.tagSet: OctetStringDecoder(),
+ univ.Null.tagSet: NullDecoder(),
+ univ.ObjectIdentifier.tagSet: ObjectIdentifierDecoder(),
+ univ.Enumerated.tagSet: IntegerDecoder(),
+ univ.Real.tagSet: RealDecoder(),
+ univ.Sequence.tagSet: SequenceDecoder(), # conflicts with SequenceOf
+ univ.Set.tagSet: SetDecoder(), # conflicts with SetOf
+ univ.Choice.tagSet: ChoiceDecoder(), # conflicts with Any
+ # character string types
+ char.UTF8String.tagSet: UTF8StringDecoder(),
+ char.NumericString.tagSet: NumericStringDecoder(),
+ char.PrintableString.tagSet: PrintableStringDecoder(),
+ char.TeletexString.tagSet: TeletexStringDecoder(),
+ char.VideotexString.tagSet: VideotexStringDecoder(),
+ char.IA5String.tagSet: IA5StringDecoder(),
+ char.GraphicString.tagSet: GraphicStringDecoder(),
+ char.VisibleString.tagSet: VisibleStringDecoder(),
+ char.GeneralString.tagSet: GeneralStringDecoder(),
+ char.UniversalString.tagSet: UniversalStringDecoder(),
+ char.BMPString.tagSet: BMPStringDecoder(),
+ # useful types
+ useful.GeneralizedTime.tagSet: GeneralizedTimeDecoder(),
+ useful.UTCTime.tagSet: UTCTimeDecoder()
+ }
+
+# Type-to-codec map for ambiguous ASN.1 types
+typeMap = {
+ univ.Set.typeId: SetDecoder(),
+ univ.SetOf.typeId: SetOfDecoder(),
+ univ.Sequence.typeId: SequenceDecoder(),
+ univ.SequenceOf.typeId: SequenceOfDecoder(),
+ univ.Choice.typeId: ChoiceDecoder(),
+ univ.Any.typeId: AnyDecoder()
+ }
+
+( stDecodeTag, stDecodeLength, stGetValueDecoder, stGetValueDecoderByAsn1Spec,
+ stGetValueDecoderByTag, stTryAsExplicitTag, stDecodeValue,
+ stDumpRawValue, stErrorCondition, stStop ) = [x for x in range(10)]
+
+class Decoder:
+ defaultErrorState = stErrorCondition
+# defaultErrorState = stDumpRawValue
+ defaultRawDecoder = AnyDecoder()
+ def __init__(self, tagMap, typeMap={}):
+ self.__tagMap = tagMap
+ self.__typeMap = typeMap
+ self.__endOfOctetsTagSet = eoo.endOfOctets.getTagSet()
+ # Tag & TagSet objects caches
+ self.__tagCache = {}
+ self.__tagSetCache = {}
+
+ def __call__(self, substrate, asn1Spec=None, tagSet=None,
+ length=None, state=stDecodeTag, recursiveFlag=1):
+ fullSubstrate = substrate
+ while state != stStop:
+ if state == stDecodeTag:
+ # Decode tag
+ if not substrate:
+ raise error.SubstrateUnderrunError(
+ 'Short octet stream on tag decoding'
+ )
+
+ firstOctet = substrate[0]
+ substrate = substrate[1:]
+ if firstOctet in self.__tagCache:
+ lastTag = self.__tagCache[firstOctet]
+ else:
+ t = oct2int(firstOctet)
+ tagClass = t&0xC0
+ tagFormat = t&0x20
+ tagId = t&0x1F
+ if tagId == 0x1F:
+ tagId = 0
+ while 1:
+ if not substrate:
+ raise error.SubstrateUnderrunError(
+ 'Short octet stream on long tag decoding'
+ )
+ t = oct2int(substrate[0])
+ tagId = tagId << 7 | (t&0x7F)
+ substrate = substrate[1:]
+ if not t&0x80:
+ break
+ lastTag = tag.Tag(
+ tagClass=tagClass, tagFormat=tagFormat, tagId=tagId
+ )
+ if tagId < 31:
+ # cache short tags
+ self.__tagCache[firstOctet] = lastTag
+ if tagSet is None:
+ if firstOctet in self.__tagSetCache:
+ tagSet = self.__tagSetCache[firstOctet]
+ else:
+ # base tag not recovered
+ tagSet = tag.TagSet((), lastTag)
+ if firstOctet in self.__tagCache:
+ self.__tagSetCache[firstOctet] = tagSet
+ else:
+ tagSet = lastTag + tagSet
+ state = stDecodeLength
+ if state == stDecodeLength:
+ # Decode length
+ if not substrate:
+ raise error.SubstrateUnderrunError(
+ 'Short octet stream on length decoding'
+ )
+ firstOctet = oct2int(substrate[0])
+ if firstOctet == 128:
+ size = 1
+ length = -1
+ elif firstOctet < 128:
+ length, size = firstOctet, 1
+ else:
+ size = firstOctet & 0x7F
+ # encoded in size bytes
+ length = 0
+ lengthString = substrate[1:size+1]
+ # missing check on maximum size, which shouldn't be a
+ # problem, we can handle more than is possible
+ if len(lengthString) != size:
+ raise error.SubstrateUnderrunError(
+ '%s<%s at %s' %
+ (size, len(lengthString), tagSet)
+ )
+ for char in lengthString:
+ length = (length << 8) | oct2int(char)
+ size = size + 1
+ state = stGetValueDecoder
+ substrate = substrate[size:]
+ if length != -1 and len(substrate) < length:
+ raise error.SubstrateUnderrunError(
+ '%d-octet short' % (length - len(substrate))
+ )
+ if state == stGetValueDecoder:
+ if asn1Spec is None:
+ state = stGetValueDecoderByTag
+ else:
+ state = stGetValueDecoderByAsn1Spec
+ #
+ # There're two ways of creating subtypes in ASN.1 what influences
+ # decoder operation. These methods are:
+ # 1) Either base types used in or no IMPLICIT tagging has been
+ # applied on subtyping.
+ # 2) Subtype syntax drops base type information (by means of
+ # IMPLICIT tagging.
+ # The first case allows for complete tag recovery from substrate
+ # while the second one requires original ASN.1 type spec for
+ # decoding.
+ #
+ # In either case a set of tags (tagSet) is coming from substrate
+ # in an incremental, tag-by-tag fashion (this is the case of
+ # EXPLICIT tag which is most basic). Outermost tag comes first
+ # from the wire.
+ #
+ if state == stGetValueDecoderByTag:
+ if tagSet in self.__tagMap:
+ concreteDecoder = self.__tagMap[tagSet]
+ else:
+ concreteDecoder = None
+ if concreteDecoder:
+ state = stDecodeValue
+ else:
+ _k = tagSet[:1]
+ if _k in self.__tagMap:
+ concreteDecoder = self.__tagMap[_k]
+ else:
+ concreteDecoder = None
+ if concreteDecoder:
+ state = stDecodeValue
+ else:
+ state = stTryAsExplicitTag
+ if state == stGetValueDecoderByAsn1Spec:
+ if isinstance(asn1Spec, (dict, tagmap.TagMap)):
+ if tagSet in asn1Spec:
+ __chosenSpec = asn1Spec[tagSet]
+ else:
+ __chosenSpec = None
+ else:
+ __chosenSpec = asn1Spec
+ if __chosenSpec is not None and (
+ tagSet == __chosenSpec.getTagSet() or \
+ tagSet in __chosenSpec.getTagMap()
+ ):
+ # use base type for codec lookup to recover untagged types
+ baseTagSet = __chosenSpec.baseTagSet
+ if __chosenSpec.typeId is not None and \
+ __chosenSpec.typeId in self.__typeMap:
+ # ambiguous type
+ concreteDecoder = self.__typeMap[__chosenSpec.typeId]
+ elif baseTagSet in self.__tagMap:
+ # base type or tagged subtype
+ concreteDecoder = self.__tagMap[baseTagSet]
+ else:
+ concreteDecoder = None
+ if concreteDecoder:
+ asn1Spec = __chosenSpec
+ state = stDecodeValue
+ else:
+ state = stTryAsExplicitTag
+ elif tagSet == self.__endOfOctetsTagSet:
+ concreteDecoder = self.__tagMap[tagSet]
+ state = stDecodeValue
+ else:
+ state = stTryAsExplicitTag
+ if state == stTryAsExplicitTag:
+ if tagSet and \
+ tagSet[0][1] == tag.tagFormatConstructed and \
+ tagSet[0][0] != tag.tagClassUniversal:
+ # Assume explicit tagging
+ concreteDecoder = explicitTagDecoder
+ state = stDecodeValue
+ else:
+ state = self.defaultErrorState
+ if state == stDumpRawValue:
+ concreteDecoder = self.defaultRawDecoder
+ state = stDecodeValue
+ if state == stDecodeValue:
+ if recursiveFlag:
+ decodeFun = self
+ else:
+ decodeFun = None
+ if length == -1: # indef length
+ value, substrate = concreteDecoder.indefLenValueDecoder(
+ fullSubstrate, substrate, asn1Spec, tagSet, length,
+ stGetValueDecoder, decodeFun
+ )
+ else:
+ value, _substrate = concreteDecoder.valueDecoder(
+ fullSubstrate, substrate, asn1Spec, tagSet, length,
+ stGetValueDecoder, decodeFun
+ )
+ if recursiveFlag:
+ substrate = substrate[length:]
+ else:
+ substrate = _substrate
+ state = stStop
+ if state == stErrorCondition:
+ raise error.PyAsn1Error(
+ '%r not in asn1Spec: %r' % (tagSet, asn1Spec)
+ )
+ return value, substrate
+
+decode = Decoder(tagMap, typeMap)
+
+# XXX
+# non-recursive decoding; return position rather than substrate
diff --git a/libs/pyasn1/codec/ber/encoder.py b/libs/pyasn1/codec/ber/encoder.py
new file mode 100644
index 00000000..2149b0ba
--- /dev/null
+++ b/libs/pyasn1/codec/ber/encoder.py
@@ -0,0 +1,334 @@
+# BER encoder
+from pyasn1.type import base, tag, univ, char, useful
+from pyasn1.codec.ber import eoo
+from pyasn1.compat.octets import int2oct, ints2octs, null, str2octs
+from pyasn1 import error
+
+class Error(Exception): pass
+
+class AbstractItemEncoder:
+ supportIndefLenMode = 1
+ def encodeTag(self, t, isConstructed):
+ tagClass, tagFormat, tagId = t.asTuple() # this is a hotspot
+ v = tagClass | tagFormat
+ if isConstructed:
+ v = v|tag.tagFormatConstructed
+ if tagId < 31:
+ return int2oct(v|tagId)
+ else:
+ s = int2oct(tagId&0x7f)
+ tagId = tagId >> 7
+ while tagId:
+ s = int2oct(0x80|(tagId&0x7f)) + s
+ tagId = tagId >> 7
+ return int2oct(v|0x1F) + s
+
+ def encodeLength(self, length, defMode):
+ if not defMode and self.supportIndefLenMode:
+ return int2oct(0x80)
+ if length < 0x80:
+ return int2oct(length)
+ else:
+ substrate = null
+ while length:
+ substrate = int2oct(length&0xff) + substrate
+ length = length >> 8
+ substrateLen = len(substrate)
+ if substrateLen > 126:
+ raise Error('Length octets overflow (%d)' % substrateLen)
+ return int2oct(0x80 | substrateLen) + substrate
+
+ def encodeValue(self, encodeFun, value, defMode, maxChunkSize):
+ raise Error('Not implemented')
+
+ def _encodeEndOfOctets(self, encodeFun, defMode):
+ if defMode or not self.supportIndefLenMode:
+ return null
+ else:
+ return encodeFun(eoo.endOfOctets, defMode)
+
+ def encode(self, encodeFun, value, defMode, maxChunkSize):
+ substrate, isConstructed = self.encodeValue(
+ encodeFun, value, defMode, maxChunkSize
+ )
+ tagSet = value.getTagSet()
+ if tagSet:
+ if not isConstructed: # primitive form implies definite mode
+ defMode = 1
+ return self.encodeTag(
+ tagSet[-1], isConstructed
+ ) + self.encodeLength(
+ len(substrate), defMode
+ ) + substrate + self._encodeEndOfOctets(encodeFun, defMode)
+ else:
+ return substrate # untagged value
+
+class EndOfOctetsEncoder(AbstractItemEncoder):
+ def encodeValue(self, encodeFun, value, defMode, maxChunkSize):
+ return null, 0
+
+class ExplicitlyTaggedItemEncoder(AbstractItemEncoder):
+ def encodeValue(self, encodeFun, value, defMode, maxChunkSize):
+ if isinstance(value, base.AbstractConstructedAsn1Item):
+ value = value.clone(tagSet=value.getTagSet()[:-1],
+ cloneValueFlag=1)
+ else:
+ value = value.clone(tagSet=value.getTagSet()[:-1])
+ return encodeFun(value, defMode, maxChunkSize), 1
+
+explicitlyTaggedItemEncoder = ExplicitlyTaggedItemEncoder()
+
+class IntegerEncoder(AbstractItemEncoder):
+ supportIndefLenMode = 0
+ def encodeValue(self, encodeFun, value, defMode, maxChunkSize):
+ octets = []
+ value = int(value) # to save on ops on asn1 type
+ while 1:
+ octets.insert(0, value & 0xff)
+ if value == 0 or value == -1:
+ break
+ value = value >> 8
+ if value == 0 and octets[0] & 0x80:
+ octets.insert(0, 0)
+ while len(octets) > 1 and \
+ (octets[0] == 0 and octets[1] & 0x80 == 0 or \
+ octets[0] == 0xff and octets[1] & 0x80 != 0):
+ del octets[0]
+ return ints2octs(octets), 0
+
+class BitStringEncoder(AbstractItemEncoder):
+ def encodeValue(self, encodeFun, value, defMode, maxChunkSize):
+ if not maxChunkSize or len(value) <= maxChunkSize*8:
+ r = {}; l = len(value); p = 0; j = 7
+ while p < l:
+ i, j = divmod(p, 8)
+ r[i] = r.get(i,0) | value[p]<<(7-j)
+ p = p + 1
+ keys = list(r); keys.sort()
+ return int2oct(7-j) + ints2octs([r[k] for k in keys]), 0
+ else:
+ pos = 0; substrate = null
+ while 1:
+ # count in octets
+ v = value.clone(value[pos*8:pos*8+maxChunkSize*8])
+ if not v:
+ break
+ substrate = substrate + encodeFun(v, defMode, maxChunkSize)
+ pos = pos + maxChunkSize
+ return substrate, 1
+
+class OctetStringEncoder(AbstractItemEncoder):
+ def encodeValue(self, encodeFun, value, defMode, maxChunkSize):
+ if not maxChunkSize or len(value) <= maxChunkSize:
+ return value.asOctets(), 0
+ else:
+ pos = 0; substrate = null
+ while 1:
+ v = value.clone(value[pos:pos+maxChunkSize])
+ if not v:
+ break
+ substrate = substrate + encodeFun(v, defMode, maxChunkSize)
+ pos = pos + maxChunkSize
+ return substrate, 1
+
+class NullEncoder(AbstractItemEncoder):
+ supportIndefLenMode = 0
+ def encodeValue(self, encodeFun, value, defMode, maxChunkSize):
+ return null, 0
+
+class ObjectIdentifierEncoder(AbstractItemEncoder):
+ supportIndefLenMode = 0
+ precomputedValues = {
+ (1, 3, 6, 1, 2): (43, 6, 1, 2),
+ (1, 3, 6, 1, 4): (43, 6, 1, 4)
+ }
+ def encodeValue(self, encodeFun, value, defMode, maxChunkSize):
+ oid = value.asTuple()
+ if oid[:5] in self.precomputedValues:
+ octets = self.precomputedValues[oid[:5]]
+ index = 5
+ else:
+ if len(oid) < 2:
+ raise error.PyAsn1Error('Short OID %s' % value)
+
+ # Build the first twos
+ index = 0
+ subid = oid[index] * 40
+ subid = subid + oid[index+1]
+ if subid < 0 or subid > 0xff:
+ raise error.PyAsn1Error(
+ 'Initial sub-ID overflow %s in OID %s' % (oid[index:], value)
+ )
+ octets = (subid,)
+ index = index + 2
+
+ # Cycle through subids
+ for subid in oid[index:]:
+ if subid > -1 and subid < 128:
+ # Optimize for the common case
+ octets = octets + (subid & 0x7f,)
+ elif subid < 0 or subid > 0xFFFFFFFF:
+ raise error.PyAsn1Error(
+ 'SubId overflow %s in %s' % (subid, value)
+ )
+ else:
+ # Pack large Sub-Object IDs
+ res = (subid & 0x7f,)
+ subid = subid >> 7
+ while subid > 0:
+ res = (0x80 | (subid & 0x7f),) + res
+ subid = subid >> 7
+ # Add packed Sub-Object ID to resulted Object ID
+ octets += res
+
+ return ints2octs(octets), 0
+
+class RealEncoder(AbstractItemEncoder):
+ def encodeValue(self, encodeFun, value, defMode, maxChunkSize):
+ if value.isPlusInfinity():
+ return int2oct(0x40), 0
+ if value.isMinusInfinity():
+ return int2oct(0x41), 0
+ m, b, e = value
+ if not m:
+ return null, 0
+ if b == 10:
+ return str2octs('\x03%dE%s%d' % (m, e == 0 and '+' or '', e)), 0
+ elif b == 2:
+ fo = 0x80 # binary enoding
+ if m < 0:
+ fo = fo | 0x40 # sign bit
+ m = -m
+ while int(m) != m: # drop floating point
+ m *= 2
+ e -= 1
+ while m & 0x1 == 0: # mantissa normalization
+ m >>= 1
+ e += 1
+ eo = null
+ while e:
+ eo = int2oct(e&0xff) + eo
+ e >>= 8
+ n = len(eo)
+ if n > 0xff:
+ raise error.PyAsn1Error('Real exponent overflow')
+ if n == 1:
+ pass
+ elif n == 2:
+ fo |= 1
+ elif n == 3:
+ fo |= 2
+ else:
+ fo |= 3
+ eo = int2oct(n//0xff+1) + eo
+ po = null
+ while m:
+ po = int2oct(m&0xff) + po
+ m >>= 8
+ substrate = int2oct(fo) + eo + po
+ return substrate, 0
+ else:
+ raise error.PyAsn1Error('Prohibited Real base %s' % b)
+
+class SequenceEncoder(AbstractItemEncoder):
+ def encodeValue(self, encodeFun, value, defMode, maxChunkSize):
+ value.setDefaultComponents()
+ value.verifySizeSpec()
+ substrate = null; idx = len(value)
+ while idx > 0:
+ idx = idx - 1
+ if value[idx] is None: # Optional component
+ continue
+ component = value.getDefaultComponentByPosition(idx)
+ if component is not None and component == value[idx]:
+ continue
+ substrate = encodeFun(
+ value[idx], defMode, maxChunkSize
+ ) + substrate
+ return substrate, 1
+
+class SequenceOfEncoder(AbstractItemEncoder):
+ def encodeValue(self, encodeFun, value, defMode, maxChunkSize):
+ value.verifySizeSpec()
+ substrate = null; idx = len(value)
+ while idx > 0:
+ idx = idx - 1
+ substrate = encodeFun(
+ value[idx], defMode, maxChunkSize
+ ) + substrate
+ return substrate, 1
+
+class ChoiceEncoder(AbstractItemEncoder):
+ def encodeValue(self, encodeFun, value, defMode, maxChunkSize):
+ return encodeFun(value.getComponent(), defMode, maxChunkSize), 1
+
+class AnyEncoder(OctetStringEncoder):
+ def encodeValue(self, encodeFun, value, defMode, maxChunkSize):
+ return value.asOctets(), defMode == 0
+
+tagMap = {
+ eoo.endOfOctets.tagSet: EndOfOctetsEncoder(),
+ univ.Boolean.tagSet: IntegerEncoder(),
+ univ.Integer.tagSet: IntegerEncoder(),
+ univ.BitString.tagSet: BitStringEncoder(),
+ univ.OctetString.tagSet: OctetStringEncoder(),
+ univ.Null.tagSet: NullEncoder(),
+ univ.ObjectIdentifier.tagSet: ObjectIdentifierEncoder(),
+ univ.Enumerated.tagSet: IntegerEncoder(),
+ univ.Real.tagSet: RealEncoder(),
+ # Sequence & Set have same tags as SequenceOf & SetOf
+ univ.SequenceOf.tagSet: SequenceOfEncoder(),
+ univ.SetOf.tagSet: SequenceOfEncoder(),
+ univ.Choice.tagSet: ChoiceEncoder(),
+ # character string types
+ char.UTF8String.tagSet: OctetStringEncoder(),
+ char.NumericString.tagSet: OctetStringEncoder(),
+ char.PrintableString.tagSet: OctetStringEncoder(),
+ char.TeletexString.tagSet: OctetStringEncoder(),
+ char.VideotexString.tagSet: OctetStringEncoder(),
+ char.IA5String.tagSet: OctetStringEncoder(),
+ char.GraphicString.tagSet: OctetStringEncoder(),
+ char.VisibleString.tagSet: OctetStringEncoder(),
+ char.GeneralString.tagSet: OctetStringEncoder(),
+ char.UniversalString.tagSet: OctetStringEncoder(),
+ char.BMPString.tagSet: OctetStringEncoder(),
+ # useful types
+ useful.GeneralizedTime.tagSet: OctetStringEncoder(),
+ useful.UTCTime.tagSet: OctetStringEncoder()
+ }
+
+# Type-to-codec map for ambiguous ASN.1 types
+typeMap = {
+ univ.Set.typeId: SequenceEncoder(),
+ univ.SetOf.typeId: SequenceOfEncoder(),
+ univ.Sequence.typeId: SequenceEncoder(),
+ univ.SequenceOf.typeId: SequenceOfEncoder(),
+ univ.Choice.typeId: ChoiceEncoder(),
+ univ.Any.typeId: AnyEncoder()
+ }
+
+class Encoder:
+ def __init__(self, tagMap, typeMap={}):
+ self.__tagMap = tagMap
+ self.__typeMap = typeMap
+
+ def __call__(self, value, defMode=1, maxChunkSize=0):
+ tagSet = value.getTagSet()
+ if len(tagSet) > 1:
+ concreteEncoder = explicitlyTaggedItemEncoder
+ else:
+ if value.typeId is not None and value.typeId in self.__typeMap:
+ concreteEncoder = self.__typeMap[value.typeId]
+ elif tagSet in self.__tagMap:
+ concreteEncoder = self.__tagMap[tagSet]
+ else:
+ baseTagSet = value.baseTagSet
+ if baseTagSet in self.__tagMap:
+ concreteEncoder = self.__tagMap[baseTagSet]
+ else:
+ raise Error('No encoder for %s' % value)
+ return concreteEncoder.encode(
+ self, value, defMode, maxChunkSize
+ )
+
+encode = Encoder(tagMap, typeMap)
diff --git a/libs/pyasn1/codec/ber/eoo.py b/libs/pyasn1/codec/ber/eoo.py
new file mode 100644
index 00000000..379be199
--- /dev/null
+++ b/libs/pyasn1/codec/ber/eoo.py
@@ -0,0 +1,8 @@
+from pyasn1.type import base, tag
+
+class EndOfOctets(base.AbstractSimpleAsn1Item):
+ defaultValue = 0
+ tagSet = tag.initTagSet(
+ tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 0x00)
+ )
+endOfOctets = EndOfOctets()
diff --git a/libs/pyasn1/codec/cer/__init__.py b/libs/pyasn1/codec/cer/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/libs/pyasn1/codec/cer/decoder.py b/libs/pyasn1/codec/cer/decoder.py
new file mode 100644
index 00000000..71395d22
--- /dev/null
+++ b/libs/pyasn1/codec/cer/decoder.py
@@ -0,0 +1,32 @@
+# CER decoder
+from pyasn1.type import univ
+from pyasn1.codec.ber import decoder
+from pyasn1.compat.octets import oct2int
+from pyasn1 import error
+
+class BooleanDecoder(decoder.AbstractSimpleDecoder):
+ protoComponent = univ.Boolean(0)
+ def valueDecoder(self, fullSubstrate, substrate, asn1Spec, tagSet, length,
+ state, decodeFun):
+ substrate = substrate[:length]
+ if not substrate:
+ raise error.PyAsn1Error('Empty substrate')
+ byte = oct2int(substrate[0])
+ if byte == 0xff:
+ value = 1
+ elif byte == 0x00:
+ value = 0
+ else:
+ raise error.PyAsn1Error('Boolean CER violation: %s' % byte)
+ return self._createComponent(asn1Spec, tagSet, value), substrate[1:]
+
+tagMap = decoder.tagMap.copy()
+tagMap.update({
+ univ.Boolean.tagSet: BooleanDecoder(),
+ })
+
+typeMap = decoder.typeMap
+
+class Decoder(decoder.Decoder): pass
+
+decode = Decoder(tagMap, decoder.typeMap)
diff --git a/libs/pyasn1/codec/cer/encoder.py b/libs/pyasn1/codec/cer/encoder.py
new file mode 100644
index 00000000..4c05130a
--- /dev/null
+++ b/libs/pyasn1/codec/cer/encoder.py
@@ -0,0 +1,87 @@
+# CER encoder
+from pyasn1.type import univ
+from pyasn1.codec.ber import encoder
+from pyasn1.compat.octets import int2oct, null
+
+class BooleanEncoder(encoder.IntegerEncoder):
+ def encodeValue(self, encodeFun, client, defMode, maxChunkSize):
+ if client == 0:
+ substrate = int2oct(0)
+ else:
+ substrate = int2oct(255)
+ return substrate, 0
+
+class BitStringEncoder(encoder.BitStringEncoder):
+ def encodeValue(self, encodeFun, client, defMode, maxChunkSize):
+ return encoder.BitStringEncoder.encodeValue(
+ self, encodeFun, client, defMode, 1000
+ )
+
+class OctetStringEncoder(encoder.OctetStringEncoder):
+ def encodeValue(self, encodeFun, client, defMode, maxChunkSize):
+ return encoder.OctetStringEncoder.encodeValue(
+ self, encodeFun, client, defMode, 1000
+ )
+
+# specialized RealEncoder here
+# specialized GeneralStringEncoder here
+# specialized GeneralizedTimeEncoder here
+# specialized UTCTimeEncoder here
+
+class SetOfEncoder(encoder.SequenceOfEncoder):
+ def encodeValue(self, encodeFun, client, defMode, maxChunkSize):
+ if isinstance(client, univ.SequenceAndSetBase):
+ client.setDefaultComponents()
+ client.verifySizeSpec()
+ substrate = null; idx = len(client)
+ # This is certainly a hack but how else do I distinguish SetOf
+ # from Set if they have the same tags&constraints?
+ if isinstance(client, univ.SequenceAndSetBase):
+ # Set
+ comps = []
+ while idx > 0:
+ idx = idx - 1
+ if client[idx] is None: # Optional component
+ continue
+ if client.getDefaultComponentByPosition(idx) == client[idx]:
+ continue
+ comps.append(client[idx])
+ comps.sort(key=lambda x: isinstance(x, univ.Choice) and \
+ x.getMinTagSet() or x.getTagSet())
+ for c in comps:
+ substrate += encodeFun(c, defMode, maxChunkSize)
+ else:
+ # SetOf
+ compSubs = []
+ while idx > 0:
+ idx = idx - 1
+ compSubs.append(
+ encodeFun(client[idx], defMode, maxChunkSize)
+ )
+ compSubs.sort() # perhaps padding's not needed
+ substrate = null
+ for compSub in compSubs:
+ substrate += compSub
+ return substrate, 1
+
+tagMap = encoder.tagMap.copy()
+tagMap.update({
+ univ.Boolean.tagSet: BooleanEncoder(),
+ univ.BitString.tagSet: BitStringEncoder(),
+ univ.OctetString.tagSet: OctetStringEncoder(),
+ univ.SetOf().tagSet: SetOfEncoder() # conflcts with Set
+ })
+
+typeMap = encoder.typeMap.copy()
+typeMap.update({
+ univ.Set.typeId: SetOfEncoder(),
+ univ.SetOf.typeId: SetOfEncoder()
+ })
+
+class Encoder(encoder.Encoder):
+ def __call__(self, client, defMode=0, maxChunkSize=0):
+ return encoder.Encoder.__call__(self, client, defMode, maxChunkSize)
+
+encode = Encoder(tagMap, typeMap)
+
+# EncoderFactory queries class instance and builds a map of tags -> encoders
diff --git a/libs/pyasn1/codec/der/__init__.py b/libs/pyasn1/codec/der/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/libs/pyasn1/codec/der/decoder.py b/libs/pyasn1/codec/der/decoder.py
new file mode 100644
index 00000000..0f5a24ca
--- /dev/null
+++ b/libs/pyasn1/codec/der/decoder.py
@@ -0,0 +1,5 @@
+# DER decoder
+from pyasn1.type import univ
+from pyasn1.codec.cer import decoder
+
+decode = decoder.Decoder(decoder.tagMap, decoder.typeMap)
diff --git a/libs/pyasn1/codec/der/encoder.py b/libs/pyasn1/codec/der/encoder.py
new file mode 100644
index 00000000..4e5faefa
--- /dev/null
+++ b/libs/pyasn1/codec/der/encoder.py
@@ -0,0 +1,28 @@
+# DER encoder
+from pyasn1.type import univ
+from pyasn1.codec.cer import encoder
+
+class SetOfEncoder(encoder.SetOfEncoder):
+ def _cmpSetComponents(self, c1, c2):
+ tagSet1 = isinstance(c1, univ.Choice) and \
+ c1.getEffectiveTagSet() or c1.getTagSet()
+ tagSet2 = isinstance(c2, univ.Choice) and \
+ c2.getEffectiveTagSet() or c2.getTagSet()
+ return cmp(tagSet1, tagSet2)
+
+tagMap = encoder.tagMap.copy()
+tagMap.update({
+ # Overload CER encodrs with BER ones (a bit hackerish XXX)
+ univ.BitString.tagSet: encoder.encoder.BitStringEncoder(),
+ univ.OctetString.tagSet: encoder.encoder.OctetStringEncoder(),
+ # Set & SetOf have same tags
+ univ.SetOf().tagSet: SetOfEncoder()
+ })
+
+typeMap = encoder.typeMap
+
+class Encoder(encoder.Encoder):
+ def __call__(self, client, defMode=1, maxChunkSize=0):
+ return encoder.Encoder.__call__(self, client, defMode, maxChunkSize)
+
+encode = Encoder(tagMap, typeMap)
diff --git a/libs/pyasn1/compat/__init__.py b/libs/pyasn1/compat/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/libs/pyasn1/compat/octets.py b/libs/pyasn1/compat/octets.py
new file mode 100644
index 00000000..d0303eaa
--- /dev/null
+++ b/libs/pyasn1/compat/octets.py
@@ -0,0 +1,18 @@
+from sys import version_info
+
+if version_info[0] <= 2:
+ int2oct = chr
+ ints2octs = lambda s: ''.join([ int2oct(x) for x in s ])
+ null = ''
+ oct2int = ord
+ octs2ints = lambda s: [ oct2int(x) for x in s ]
+ str2octs = lambda x: x
+ octs2str = lambda x: x
+else:
+ ints2octs = bytes
+ int2oct = lambda x: ints2octs((x,))
+ null = ints2octs()
+ oct2int = lambda x: x
+ octs2ints = lambda s: [ x for x in s ]
+ str2octs = lambda x: x.encode()
+ octs2str = lambda x: x.decode()
diff --git a/libs/pyasn1/error.py b/libs/pyasn1/error.py
new file mode 100644
index 00000000..716406ff
--- /dev/null
+++ b/libs/pyasn1/error.py
@@ -0,0 +1,3 @@
+class PyAsn1Error(Exception): pass
+class ValueConstraintError(PyAsn1Error): pass
+class SubstrateUnderrunError(PyAsn1Error): pass
diff --git a/libs/pyasn1/type/__init__.py b/libs/pyasn1/type/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/libs/pyasn1/type/base.py b/libs/pyasn1/type/base.py
new file mode 100644
index 00000000..db31671e
--- /dev/null
+++ b/libs/pyasn1/type/base.py
@@ -0,0 +1,244 @@
+# Base classes for ASN.1 types
+import sys
+from pyasn1.type import constraint, tagmap
+from pyasn1 import error
+
+class Asn1Item: pass
+
+class Asn1ItemBase(Asn1Item):
+ # Set of tags for this ASN.1 type
+ tagSet = ()
+
+ # A list of constraint.Constraint instances for checking values
+ subtypeSpec = constraint.ConstraintsIntersection()
+
+ # Used for ambiguous ASN.1 types identification
+ typeId = None
+
+ def __init__(self, tagSet=None, subtypeSpec=None):
+ if tagSet is None:
+ self._tagSet = self.tagSet
+ else:
+ self._tagSet = tagSet
+ if subtypeSpec is None:
+ self._subtypeSpec = self.subtypeSpec
+ else:
+ self._subtypeSpec = subtypeSpec
+
+ def _verifySubtypeSpec(self, value, idx=None):
+ try:
+ self._subtypeSpec(value, idx)
+ except error.PyAsn1Error:
+ c, i, t = sys.exc_info()
+ raise c('%s at %s' % (i, self.__class__.__name__))
+
+ def getSubtypeSpec(self): return self._subtypeSpec
+
+ def getTagSet(self): return self._tagSet
+ def getEffectiveTagSet(self): return self._tagSet # used by untagged types
+ def getTagMap(self): return tagmap.TagMap({self._tagSet: self})
+
+ def isSameTypeWith(self, other):
+ return self is other or \
+ self._tagSet == other.getTagSet() and \
+ self._subtypeSpec == other.getSubtypeSpec()
+ def isSuperTypeOf(self, other):
+ """Returns true if argument is a ASN1 subtype of ourselves"""
+ return self._tagSet.isSuperTagSetOf(other.getTagSet()) and \
+ self._subtypeSpec.isSuperTypeOf(other.getSubtypeSpec())
+
+class __NoValue:
+ def __getattr__(self, attr):
+ raise error.PyAsn1Error('No value for %s()' % attr)
+ def __getitem__(self, i):
+ raise error.PyAsn1Error('No value')
+
+noValue = __NoValue()
+
+# Base class for "simple" ASN.1 objects. These are immutable.
+class AbstractSimpleAsn1Item(Asn1ItemBase):
+ defaultValue = noValue
+ def __init__(self, value=None, tagSet=None, subtypeSpec=None):
+ Asn1ItemBase.__init__(self, tagSet, subtypeSpec)
+ if value is None or value is noValue:
+ value = self.defaultValue
+ if value is None or value is noValue:
+ self.__hashedValue = value = noValue
+ else:
+ value = self.prettyIn(value)
+ self._verifySubtypeSpec(value)
+ self.__hashedValue = hash(value)
+ self._value = value
+ self._len = None
+
+ def __repr__(self):
+ if self._value is noValue:
+ return self.__class__.__name__ + '()'
+ else:
+ return self.__class__.__name__ + '(%s)' % (self.prettyOut(self._value),)
+ def __str__(self): return str(self._value)
+ def __eq__(self, other):
+ return self is other and True or self._value == other
+ def __ne__(self, other): return self._value != other
+ def __lt__(self, other): return self._value < other
+ def __le__(self, other): return self._value <= other
+ def __gt__(self, other): return self._value > other
+ def __ge__(self, other): return self._value >= other
+ if sys.version_info[0] <= 2:
+ def __nonzero__(self): return bool(self._value)
+ else:
+ def __bool__(self): return bool(self._value)
+ def __hash__(self): return self.__hashedValue
+
+ def clone(self, value=None, tagSet=None, subtypeSpec=None):
+ if value is None and tagSet is None and subtypeSpec is None:
+ return self
+ if value is None:
+ value = self._value
+ if tagSet is None:
+ tagSet = self._tagSet
+ if subtypeSpec is None:
+ subtypeSpec = self._subtypeSpec
+ return self.__class__(value, tagSet, subtypeSpec)
+
+ def subtype(self, value=None, implicitTag=None, explicitTag=None,
+ subtypeSpec=None):
+ if value is None:
+ value = self._value
+ if implicitTag is not None:
+ tagSet = self._tagSet.tagImplicitly(implicitTag)
+ elif explicitTag is not None:
+ tagSet = self._tagSet.tagExplicitly(explicitTag)
+ else:
+ tagSet = self._tagSet
+ if subtypeSpec is None:
+ subtypeSpec = self._subtypeSpec
+ else:
+ subtypeSpec = subtypeSpec + self._subtypeSpec
+ return self.__class__(value, tagSet, subtypeSpec)
+
+ def prettyIn(self, value): return value
+ def prettyOut(self, value): return str(value)
+
+ def prettyPrint(self, scope=0): return self.prettyOut(self._value)
+ # XXX Compatibility stub
+ def prettyPrinter(self, scope=0): return self.prettyPrint(scope)
+
+#
+# Constructed types:
+# * There are five of them: Sequence, SequenceOf/SetOf, Set and Choice
+# * ASN1 types and values are represened by Python class instances
+# * Value initialization is made for defaulted components only
+# * Primary method of component addressing is by-position. Data model for base
+# type is Python sequence. Additional type-specific addressing methods
+# may be implemented for particular types.
+# * SequenceOf and SetOf types do not implement any additional methods
+# * Sequence, Set and Choice types also implement by-identifier addressing
+# * Sequence, Set and Choice types also implement by-asn1-type (tag) addressing
+# * Sequence and Set types may include optional and defaulted
+# components
+# * Constructed types hold a reference to component types used for value
+# verification and ordering.
+# * Component type is a scalar type for SequenceOf/SetOf types and a list
+# of types for Sequence/Set/Choice.
+#
+
+class AbstractConstructedAsn1Item(Asn1ItemBase):
+ componentType = None
+ sizeSpec = constraint.ConstraintsIntersection()
+ def __init__(self, componentType=None, tagSet=None,
+ subtypeSpec=None, sizeSpec=None):
+ Asn1ItemBase.__init__(self, tagSet, subtypeSpec)
+ if componentType is None:
+ self._componentType = self.componentType
+ else:
+ self._componentType = componentType
+ if sizeSpec is None:
+ self._sizeSpec = self.sizeSpec
+ else:
+ self._sizeSpec = sizeSpec
+ self._componentValues = []
+ self._componentValuesSet = 0
+
+ def __repr__(self):
+ r = self.__class__.__name__ + '()'
+ for idx in range(len(self._componentValues)):
+ if self._componentValues[idx] is None:
+ continue
+ r = r + '.setComponentByPosition(%s, %r)' % (
+ idx, self._componentValues[idx]
+ )
+ return r
+
+ def __eq__(self, other):
+ return self is other and True or self._componentValues == other
+ def __ne__(self, other): return self._componentValues != other
+ def __lt__(self, other): return self._componentValues < other
+ def __le__(self, other): return self._componentValues <= other
+ def __gt__(self, other): return self._componentValues > other
+ def __ge__(self, other): return self._componentValues >= other
+ if sys.version_info[0] <= 2:
+ def __nonzero__(self): return bool(self._componentValues)
+ else:
+ def __bool__(self): return bool(self._componentValues)
+
+ def getComponentTagMap(self):
+ raise error.PyAsn1Error('Method not implemented')
+
+ def _cloneComponentValues(self, myClone, cloneValueFlag): pass
+
+ def clone(self, tagSet=None, subtypeSpec=None, sizeSpec=None,
+ cloneValueFlag=None):
+ if tagSet is None:
+ tagSet = self._tagSet
+ if subtypeSpec is None:
+ subtypeSpec = self._subtypeSpec
+ if sizeSpec is None:
+ sizeSpec = self._sizeSpec
+ r = self.__class__(self._componentType, tagSet, subtypeSpec, sizeSpec)
+ if cloneValueFlag:
+ self._cloneComponentValues(r, cloneValueFlag)
+ return r
+
+ def subtype(self, implicitTag=None, explicitTag=None, subtypeSpec=None,
+ sizeSpec=None, cloneValueFlag=None):
+ if implicitTag is not None:
+ tagSet = self._tagSet.tagImplicitly(implicitTag)
+ elif explicitTag is not None:
+ tagSet = self._tagSet.tagExplicitly(explicitTag)
+ else:
+ tagSet = self._tagSet
+ if subtypeSpec is None:
+ subtypeSpec = self._subtypeSpec
+ else:
+ subtypeSpec = subtypeSpec + self._subtypeSpec
+ if sizeSpec is None:
+ sizeSpec = self._sizeSpec
+ else:
+ sizeSpec = sizeSpec + self._sizeSpec
+ r = self.__class__(self._componentType, tagSet, subtypeSpec, sizeSpec)
+ if cloneValueFlag:
+ self._cloneComponentValues(r, cloneValueFlag)
+ return r
+
+ def _verifyComponent(self, idx, value): pass
+
+ def verifySizeSpec(self): self._sizeSpec(self)
+
+ def getComponentByPosition(self, idx):
+ raise error.PyAsn1Error('Method not implemented')
+ def setComponentByPosition(self, idx, value, verifyConstraints=True):
+ raise error.PyAsn1Error('Method not implemented')
+
+ def getComponentType(self): return self._componentType
+
+ def __getitem__(self, idx): return self.getComponentByPosition(idx)
+ def __setitem__(self, idx, value): self.setComponentByPosition(idx, value)
+
+ def __len__(self): return len(self._componentValues)
+
+ def clear(self):
+ self._componentValues = []
+ self._componentValuesSet = 0
+
+ def setDefaultComponents(self): pass
diff --git a/libs/pyasn1/type/char.py b/libs/pyasn1/type/char.py
new file mode 100644
index 00000000..ae112f8b
--- /dev/null
+++ b/libs/pyasn1/type/char.py
@@ -0,0 +1,61 @@
+# ASN.1 "character string" types
+from pyasn1.type import univ, tag
+
+class UTF8String(univ.OctetString):
+ tagSet = univ.OctetString.tagSet.tagImplicitly(
+ tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 12)
+ )
+ encoding = "utf-8"
+
+class NumericString(univ.OctetString):
+ tagSet = univ.OctetString.tagSet.tagImplicitly(
+ tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 18)
+ )
+
+class PrintableString(univ.OctetString):
+ tagSet = univ.OctetString.tagSet.tagImplicitly(
+ tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 19)
+ )
+
+class TeletexString(univ.OctetString):
+ tagSet = univ.OctetString.tagSet.tagImplicitly(
+ tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 20)
+ )
+
+
+class VideotexString(univ.OctetString):
+ tagSet = univ.OctetString.tagSet.tagImplicitly(
+ tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 21)
+ )
+
+class IA5String(univ.OctetString):
+ tagSet = univ.OctetString.tagSet.tagImplicitly(
+ tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 22)
+ )
+
+class GraphicString(univ.OctetString):
+ tagSet = univ.OctetString.tagSet.tagImplicitly(
+ tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 25)
+ )
+
+class VisibleString(univ.OctetString):
+ tagSet = univ.OctetString.tagSet.tagImplicitly(
+ tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 26)
+ )
+
+class GeneralString(univ.OctetString):
+ tagSet = univ.OctetString.tagSet.tagImplicitly(
+ tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 27)
+ )
+
+class UniversalString(univ.OctetString):
+ tagSet = univ.OctetString.tagSet.tagImplicitly(
+ tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 28)
+ )
+ encoding = "utf-32-be"
+
+class BMPString(univ.OctetString):
+ tagSet = univ.OctetString.tagSet.tagImplicitly(
+ tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 30)
+ )
+ encoding = "utf-16-be"
diff --git a/libs/pyasn1/type/constraint.py b/libs/pyasn1/type/constraint.py
new file mode 100644
index 00000000..66873937
--- /dev/null
+++ b/libs/pyasn1/type/constraint.py
@@ -0,0 +1,200 @@
+#
+# ASN.1 subtype constraints classes.
+#
+# Constraints are relatively rare, but every ASN1 object
+# is doing checks all the time for whether they have any
+# constraints and whether they are applicable to the object.
+#
+# What we're going to do is define objects/functions that
+# can be called unconditionally if they are present, and that
+# are simply not present if there are no constraints.
+#
+# Original concept and code by Mike C. Fletcher.
+#
+import sys
+from pyasn1.type import error
+
+class AbstractConstraint:
+ """Abstract base-class for constraint objects
+
+ Constraints should be stored in a simple sequence in the
+ namespace of their client Asn1Item sub-classes.
+ """
+ def __init__(self, *values):
+ self._valueMap = {}
+ self._setValues(values)
+ self.__hashedValues = None
+ def __call__(self, value, idx=None):
+ try:
+ self._testValue(value, idx)
+ except error.ValueConstraintError:
+ raise error.ValueConstraintError(
+ '%s failed at: \"%s\"' % (self, sys.exc_info()[1])
+ )
+ def __repr__(self):
+ return '%s(%s)' % (
+ self.__class__.__name__,
+ ', '.join([repr(x) for x in self._values])
+ )
+ def __eq__(self, other):
+ return self is other and True or self._values == other
+ def __ne__(self, other): return self._values != other
+ def __lt__(self, other): return self._values < other
+ def __le__(self, other): return self._values <= other
+ def __gt__(self, other): return self._values > other
+ def __ge__(self, other): return self._values >= other
+ if sys.version_info[0] <= 2:
+ def __nonzero__(self): return bool(self._values)
+ else:
+ def __bool__(self): return bool(self._values)
+
+ def __hash__(self):
+ if self.__hashedValues is None:
+ self.__hashedValues = hash((self.__class__.__name__, self._values))
+ return self.__hashedValues
+
+ def _setValues(self, values): self._values = values
+ def _testValue(self, value, idx):
+ raise error.ValueConstraintError(value)
+
+ # Constraints derivation logic
+ def getValueMap(self): return self._valueMap
+ def isSuperTypeOf(self, otherConstraint):
+ return self in otherConstraint.getValueMap() or \
+ otherConstraint is self or otherConstraint == self
+ def isSubTypeOf(self, otherConstraint):
+ return otherConstraint in self._valueMap or \
+ otherConstraint is self or otherConstraint == self
+
+class SingleValueConstraint(AbstractConstraint):
+ """Value must be part of defined values constraint"""
+ def _testValue(self, value, idx):
+ # XXX index vals for performance?
+ if value not in self._values:
+ raise error.ValueConstraintError(value)
+
+class ContainedSubtypeConstraint(AbstractConstraint):
+ """Value must satisfy all of defined set of constraints"""
+ def _testValue(self, value, idx):
+ for c in self._values:
+ c(value, idx)
+
+class ValueRangeConstraint(AbstractConstraint):
+ """Value must be within start and stop values (inclusive)"""
+ def _testValue(self, value, idx):
+ if value < self.start or value > self.stop:
+ raise error.ValueConstraintError(value)
+
+ def _setValues(self, values):
+ if len(values) != 2:
+ raise error.PyAsn1Error(
+ '%s: bad constraint values' % (self.__class__.__name__,)
+ )
+ self.start, self.stop = values
+ if self.start > self.stop:
+ raise error.PyAsn1Error(
+ '%s: screwed constraint values (start > stop): %s > %s' % (
+ self.__class__.__name__,
+ self.start, self.stop
+ )
+ )
+ AbstractConstraint._setValues(self, values)
+
+class ValueSizeConstraint(ValueRangeConstraint):
+ """len(value) must be within start and stop values (inclusive)"""
+ def _testValue(self, value, idx):
+ l = len(value)
+ if l < self.start or l > self.stop:
+ raise error.ValueConstraintError(value)
+
+class PermittedAlphabetConstraint(SingleValueConstraint):
+ def _setValues(self, values):
+ self._values = ()
+ for v in values:
+ self._values = self._values + tuple(v)
+
+ def _testValue(self, value, idx):
+ for v in value:
+ if v not in self._values:
+ raise error.ValueConstraintError(value)
+
+# This is a bit kludgy, meaning two op modes within a single constraing
+class InnerTypeConstraint(AbstractConstraint):
+ """Value must satisfy type and presense constraints"""
+ def _testValue(self, value, idx):
+ if self.__singleTypeConstraint:
+ self.__singleTypeConstraint(value)
+ elif self.__multipleTypeConstraint:
+ if idx not in self.__multipleTypeConstraint:
+ raise error.ValueConstraintError(value)
+ constraint, status = self.__multipleTypeConstraint[idx]
+ if status == 'ABSENT': # XXX presense is not checked!
+ raise error.ValueConstraintError(value)
+ constraint(value)
+
+ def _setValues(self, values):
+ self.__multipleTypeConstraint = {}
+ self.__singleTypeConstraint = None
+ for v in values:
+ if isinstance(v, tuple):
+ self.__multipleTypeConstraint[v[0]] = v[1], v[2]
+ else:
+ self.__singleTypeConstraint = v
+ AbstractConstraint._setValues(self, values)
+
+# Boolean ops on constraints
+
+class ConstraintsExclusion(AbstractConstraint):
+ """Value must not fit the single constraint"""
+ def _testValue(self, value, idx):
+ try:
+ self._values[0](value, idx)
+ except error.ValueConstraintError:
+ return
+ else:
+ raise error.ValueConstraintError(value)
+
+ def _setValues(self, values):
+ if len(values) != 1:
+ raise error.PyAsn1Error('Single constraint expected')
+ AbstractConstraint._setValues(self, values)
+
+class AbstractConstraintSet(AbstractConstraint):
+ """Value must not satisfy the single constraint"""
+ def __getitem__(self, idx): return self._values[idx]
+
+ def __add__(self, value): return self.__class__(self, value)
+ def __radd__(self, value): return self.__class__(self, value)
+
+ def __len__(self): return len(self._values)
+
+ # Constraints inclusion in sets
+
+ def _setValues(self, values):
+ self._values = values
+ for v in values:
+ self._valueMap[v] = 1
+ self._valueMap.update(v.getValueMap())
+
+class ConstraintsIntersection(AbstractConstraintSet):
+ """Value must satisfy all constraints"""
+ def _testValue(self, value, idx):
+ for v in self._values:
+ v(value, idx)
+
+class ConstraintsUnion(AbstractConstraintSet):
+ """Value must satisfy at least one constraint"""
+ def _testValue(self, value, idx):
+ for v in self._values:
+ try:
+ v(value, idx)
+ except error.ValueConstraintError:
+ pass
+ else:
+ return
+ raise error.ValueConstraintError(
+ 'all of %s failed for \"%s\"' % (self._values, value)
+ )
+
+# XXX
+# add tests for type check
diff --git a/libs/pyasn1/type/error.py b/libs/pyasn1/type/error.py
new file mode 100644
index 00000000..3e684844
--- /dev/null
+++ b/libs/pyasn1/type/error.py
@@ -0,0 +1,3 @@
+from pyasn1.error import PyAsn1Error
+
+class ValueConstraintError(PyAsn1Error): pass
diff --git a/libs/pyasn1/type/namedtype.py b/libs/pyasn1/type/namedtype.py
new file mode 100644
index 00000000..aa9c5678
--- /dev/null
+++ b/libs/pyasn1/type/namedtype.py
@@ -0,0 +1,132 @@
+# NamedType specification for constructed types
+import sys
+from pyasn1.type import tagmap
+from pyasn1 import error
+
+class NamedType:
+ isOptional = 0
+ isDefaulted = 0
+ def __init__(self, name, t):
+ self.__name = name; self.__type = t
+ def __repr__(self): return '%s(%s, %s)' % (
+ self.__class__.__name__, self.__name, self.__type
+ )
+ def getType(self): return self.__type
+ def getName(self): return self.__name
+ def __getitem__(self, idx):
+ if idx == 0: return self.__name
+ if idx == 1: return self.__type
+ raise IndexError()
+
+class OptionalNamedType(NamedType):
+ isOptional = 1
+class DefaultedNamedType(NamedType):
+ isDefaulted = 1
+
+class NamedTypes:
+ def __init__(self, *namedTypes):
+ self.__namedTypes = namedTypes
+ self.__namedTypesLen = len(self.__namedTypes)
+ self.__minTagSet = None
+ self.__tagToPosIdx = {}; self.__nameToPosIdx = {}
+ self.__tagMap = { False: None, True: None }
+ self.__ambigiousTypes = {}
+
+ def __repr__(self):
+ r = '%s(' % self.__class__.__name__
+ for n in self.__namedTypes:
+ r = r + '%r, ' % (n,)
+ return r + ')'
+
+ def __getitem__(self, idx): return self.__namedTypes[idx]
+
+ if sys.version_info[0] <= 2:
+ def __nonzero__(self): return bool(self.__namedTypesLen)
+ else:
+ def __bool__(self): return bool(self.__namedTypesLen)
+ def __len__(self): return self.__namedTypesLen
+
+ def getTypeByPosition(self, idx):
+ if idx < 0 or idx >= self.__namedTypesLen:
+ raise error.PyAsn1Error('Type position out of range')
+ else:
+ return self.__namedTypes[idx].getType()
+
+ def getPositionByType(self, tagSet):
+ if not self.__tagToPosIdx:
+ idx = self.__namedTypesLen
+ while idx > 0:
+ idx = idx - 1
+ tagMap = self.__namedTypes[idx].getType().getTagMap()
+ for t in tagMap.getPosMap():
+ if t in self.__tagToPosIdx:
+ raise error.PyAsn1Error('Duplicate type %s' % t)
+ self.__tagToPosIdx[t] = idx
+ try:
+ return self.__tagToPosIdx[tagSet]
+ except KeyError:
+ raise error.PyAsn1Error('Type %s not found' % tagSet)
+
+ def getNameByPosition(self, idx):
+ try:
+ return self.__namedTypes[idx].getName()
+ except IndexError:
+ raise error.PyAsn1Error('Type position out of range')
+ def getPositionByName(self, name):
+ if not self.__nameToPosIdx:
+ idx = self.__namedTypesLen
+ while idx > 0:
+ idx = idx - 1
+ n = self.__namedTypes[idx].getName()
+ if n in self.__nameToPosIdx:
+ raise error.PyAsn1Error('Duplicate name %s' % n)
+ self.__nameToPosIdx[n] = idx
+ try:
+ return self.__nameToPosIdx[name]
+ except KeyError:
+ raise error.PyAsn1Error('Name %s not found' % name)
+
+ def __buildAmbigiousTagMap(self):
+ ambigiousTypes = ()
+ idx = self.__namedTypesLen
+ while idx > 0:
+ idx = idx - 1
+ t = self.__namedTypes[idx]
+ if t.isOptional or t.isDefaulted:
+ ambigiousTypes = (t, ) + ambigiousTypes
+ else:
+ ambigiousTypes = (t, )
+ self.__ambigiousTypes[idx] = NamedTypes(*ambigiousTypes)
+
+ def getTagMapNearPosition(self, idx):
+ if not self.__ambigiousTypes: self.__buildAmbigiousTagMap()
+ try:
+ return self.__ambigiousTypes[idx].getTagMap()
+ except KeyError:
+ raise error.PyAsn1Error('Type position out of range')
+
+ def getPositionNearType(self, tagSet, idx):
+ if not self.__ambigiousTypes: self.__buildAmbigiousTagMap()
+ try:
+ return idx+self.__ambigiousTypes[idx].getPositionByType(tagSet)
+ except KeyError:
+ raise error.PyAsn1Error('Type position out of range')
+
+ def genMinTagSet(self):
+ if self.__minTagSet is None:
+ for t in self.__namedTypes:
+ __type = t.getType()
+ tagSet = getattr(__type,'getMinTagSet',__type.getTagSet)()
+ if self.__minTagSet is None or tagSet < self.__minTagSet:
+ self.__minTagSet = tagSet
+ return self.__minTagSet
+
+ def getTagMap(self, uniq=False):
+ if self.__tagMap[uniq] is None:
+ tagMap = tagmap.TagMap()
+ for nt in self.__namedTypes:
+ tagMap = tagMap.clone(
+ nt.getType(), nt.getType().getTagMap(), uniq
+ )
+ self.__tagMap[uniq] = tagMap
+ return self.__tagMap[uniq]
diff --git a/libs/pyasn1/type/namedval.py b/libs/pyasn1/type/namedval.py
new file mode 100644
index 00000000..815e2d42
--- /dev/null
+++ b/libs/pyasn1/type/namedval.py
@@ -0,0 +1,46 @@
+# ASN.1 named integers
+from pyasn1 import error
+
+__all__ = [ 'NamedValues' ]
+
+class NamedValues:
+ def __init__(self, *namedValues):
+ self.nameToValIdx = {}; self.valToNameIdx = {}
+ self.namedValues = ()
+ automaticVal = 1
+ for namedValue in namedValues:
+ if isinstance(namedValue, tuple):
+ name, val = namedValue
+ else:
+ name = namedValue
+ val = automaticVal
+ if name in self.nameToValIdx:
+ raise error.PyAsn1Error('Duplicate name %s' % name)
+ self.nameToValIdx[name] = val
+ if val in self.valToNameIdx:
+ raise error.PyAsn1Error('Duplicate value %s' % name)
+ self.valToNameIdx[val] = name
+ self.namedValues = self.namedValues + ((name, val),)
+ automaticVal = automaticVal + 1
+ def __str__(self): return str(self.namedValues)
+
+ def getName(self, value):
+ if value in self.valToNameIdx:
+ return self.valToNameIdx[value]
+
+ def getValue(self, name):
+ if name in self.nameToValIdx:
+ return self.nameToValIdx[name]
+
+ def __getitem__(self, i): return self.namedValues[i]
+ def __len__(self): return len(self.namedValues)
+
+ def __add__(self, namedValues):
+ return self.__class__(*self.namedValues + namedValues)
+ def __radd__(self, namedValues):
+ return self.__class__(*namedValues + tuple(self))
+
+ def clone(self, *namedValues):
+ return self.__class__(*tuple(self) + namedValues)
+
+# XXX clone/subtype?
diff --git a/libs/pyasn1/type/tag.py b/libs/pyasn1/type/tag.py
new file mode 100644
index 00000000..0cf67ebd
--- /dev/null
+++ b/libs/pyasn1/type/tag.py
@@ -0,0 +1,122 @@
+# ASN.1 types tags
+from operator import getitem
+from pyasn1 import error
+
+tagClassUniversal = 0x00
+tagClassApplication = 0x40
+tagClassContext = 0x80
+tagClassPrivate = 0xC0
+
+tagFormatSimple = 0x00
+tagFormatConstructed = 0x20
+
+tagCategoryImplicit = 0x01
+tagCategoryExplicit = 0x02
+tagCategoryUntagged = 0x04
+
+class Tag:
+ def __init__(self, tagClass, tagFormat, tagId):
+ if tagId < 0:
+ raise error.PyAsn1Error(
+ 'Negative tag ID (%s) not allowed' % tagId
+ )
+ self.__tag = (tagClass, tagFormat, tagId)
+ self.uniq = (tagClass, tagId)
+ self.__hashedUniqTag = hash(self.uniq)
+
+ def __repr__(self):
+ return '%s(tagClass=%s, tagFormat=%s, tagId=%s)' % (
+ (self.__class__.__name__,) + self.__tag
+ )
+ # These is really a hotspot -- expose public "uniq" attribute to save on
+ # function calls
+ def __eq__(self, other): return self.uniq == other.uniq
+ def __ne__(self, other): return self.uniq != other.uniq
+ def __lt__(self, other): return self.uniq < other.uniq
+ def __le__(self, other): return self.uniq <= other.uniq
+ def __gt__(self, other): return self.uniq > other.uniq
+ def __ge__(self, other): return self.uniq >= other.uniq
+ def __hash__(self): return self.__hashedUniqTag
+ def __getitem__(self, idx): return self.__tag[idx]
+ def __and__(self, otherTag):
+ (tagClass, tagFormat, tagId) = otherTag
+ return self.__class__(
+ self.__tag&tagClass, self.__tag&tagFormat, self.__tag&tagId
+ )
+ def __or__(self, otherTag):
+ (tagClass, tagFormat, tagId) = otherTag
+ return self.__class__(
+ self.__tag[0]|tagClass,
+ self.__tag[1]|tagFormat,
+ self.__tag[2]|tagId
+ )
+ def asTuple(self): return self.__tag # __getitem__() is slow
+
+class TagSet:
+ def __init__(self, baseTag=(), *superTags):
+ self.__baseTag = baseTag
+ self.__superTags = superTags
+ self.__hashedSuperTags = hash(superTags)
+ _uniq = ()
+ for t in superTags:
+ _uniq = _uniq + t.uniq
+ self.uniq = _uniq
+ self.__lenOfSuperTags = len(superTags)
+
+ def __repr__(self):
+ return '%s(%s)' % (
+ self.__class__.__name__,
+ ', '.join([repr(x) for x in self.__superTags])
+ )
+
+ def __add__(self, superTag):
+ return self.__class__(
+ self.__baseTag, *self.__superTags + (superTag,)
+ )
+ def __radd__(self, superTag):
+ return self.__class__(
+ self.__baseTag, *(superTag,) + self.__superTags
+ )
+
+ def tagExplicitly(self, superTag):
+ tagClass, tagFormat, tagId = superTag
+ if tagClass == tagClassUniversal:
+ raise error.PyAsn1Error(
+ 'Can\'t tag with UNIVERSAL-class tag'
+ )
+ if tagFormat != tagFormatConstructed:
+ superTag = Tag(tagClass, tagFormatConstructed, tagId)
+ return self + superTag
+
+ def tagImplicitly(self, superTag):
+ tagClass, tagFormat, tagId = superTag
+ if self.__superTags:
+ superTag = Tag(tagClass, self.__superTags[-1][1], tagId)
+ return self[:-1] + superTag
+
+ def getBaseTag(self): return self.__baseTag
+ def __getitem__(self, idx):
+ if isinstance(idx, slice):
+ return self.__class__(
+ self.__baseTag, *getitem(self.__superTags, idx)
+ )
+ return self.__superTags[idx]
+ def __eq__(self, other): return self.uniq == other.uniq
+ def __ne__(self, other): return self.uniq != other.uniq
+ def __lt__(self, other): return self.uniq < other.uniq
+ def __le__(self, other): return self.uniq <= other.uniq
+ def __gt__(self, other): return self.uniq > other.uniq
+ def __ge__(self, other): return self.uniq >= other.uniq
+ def __hash__(self): return self.__hashedSuperTags
+ def __len__(self): return self.__lenOfSuperTags
+ def isSuperTagSetOf(self, tagSet):
+ if len(tagSet) < self.__lenOfSuperTags:
+ return
+ idx = self.__lenOfSuperTags - 1
+ while idx >= 0:
+ if self.__superTags[idx] != tagSet[idx]:
+ return
+ idx = idx - 1
+ return 1
+
+def initTagSet(tag): return TagSet(tag, tag)
diff --git a/libs/pyasn1/type/tagmap.py b/libs/pyasn1/type/tagmap.py
new file mode 100644
index 00000000..53e1791a
--- /dev/null
+++ b/libs/pyasn1/type/tagmap.py
@@ -0,0 +1,52 @@
+from pyasn1 import error
+
+class TagMap:
+ def __init__(self, posMap={}, negMap={}, defType=None):
+ self.__posMap = posMap.copy()
+ self.__negMap = negMap.copy()
+ self.__defType = defType
+
+ def __contains__(self, tagSet):
+ return tagSet in self.__posMap or \
+ self.__defType is not None and tagSet not in self.__negMap
+
+ def __getitem__(self, tagSet):
+ if tagSet in self.__posMap:
+ return self.__posMap[tagSet]
+ elif tagSet in self.__negMap:
+ raise error.PyAsn1Error('Key in negative map')
+ elif self.__defType is not None:
+ return self.__defType
+ else:
+ raise KeyError()
+
+ def __repr__(self):
+ s = '%r/%r' % (self.__posMap, self.__negMap)
+ if self.__defType is not None:
+ s = s + '/%r' % (self.__defType,)
+ return s
+
+ def clone(self, parentType, tagMap, uniq=False):
+ if self.__defType is not None and tagMap.getDef() is not None:
+ raise error.PyAsn1Error('Duplicate default value at %s' % self)
+ if tagMap.getDef() is not None:
+ defType = tagMap.getDef()
+ else:
+ defType = self.__defType
+
+ posMap = self.__posMap.copy()
+ for k in tagMap.getPosMap():
+ if uniq and k in posMap:
+ raise error.PyAsn1Error('Duplicate positive key %s' % k)
+ posMap[k] = parentType
+
+ negMap = self.__negMap.copy()
+ negMap.update(tagMap.getNegMap())
+
+ return self.__class__(
+ posMap, negMap, defType,
+ )
+
+ def getPosMap(self): return self.__posMap.copy()
+ def getNegMap(self): return self.__negMap.copy()
+ def getDef(self): return self.__defType
diff --git a/libs/pyasn1/type/univ.py b/libs/pyasn1/type/univ.py
new file mode 100644
index 00000000..cb4f49b7
--- /dev/null
+++ b/libs/pyasn1/type/univ.py
@@ -0,0 +1,1037 @@
+# ASN.1 "universal" data types
+import operator, sys
+from pyasn1.type import base, tag, constraint, namedtype, namedval, tagmap
+from pyasn1.codec.ber import eoo
+from pyasn1.compat import octets
+from pyasn1 import error
+
+# "Simple" ASN.1 types (yet incomplete)
+
+class Integer(base.AbstractSimpleAsn1Item):
+ tagSet = baseTagSet = tag.initTagSet(
+ tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 0x02)
+ )
+ namedValues = namedval.NamedValues()
+ def __init__(self, value=None, tagSet=None, subtypeSpec=None,
+ namedValues=None):
+ if namedValues is None:
+ self.__namedValues = self.namedValues
+ else:
+ self.__namedValues = namedValues
+ base.AbstractSimpleAsn1Item.__init__(
+ self, value, tagSet, subtypeSpec
+ )
+
+ def __and__(self, value): return self.clone(self._value & value)
+ def __rand__(self, value): return self.clone(value & self._value)
+ def __or__(self, value): return self.clone(self._value | value)
+ def __ror__(self, value): return self.clone(value | self._value)
+ def __xor__(self, value): return self.clone(self._value ^ value)
+ def __rxor__(self, value): return self.clone(value ^ self._value)
+ def __lshift__(self, value): return self.clone(self._value << value)
+ def __rshift__(self, value): return self.clone(self._value >> value)
+
+ def __add__(self, value): return self.clone(self._value + value)
+ def __radd__(self, value): return self.clone(value + self._value)
+ def __sub__(self, value): return self.clone(self._value - value)
+ def __rsub__(self, value): return self.clone(value - self._value)
+ def __mul__(self, value): return self.clone(self._value * value)
+ def __rmul__(self, value): return self.clone(value * self._value)
+ def __mod__(self, value): return self.clone(self._value % value)
+ def __rmod__(self, value): return self.clone(value % self._value)
+ def __pow__(self, value, modulo=None): return self.clone(pow(self._value, value, modulo))
+ def __rpow__(self, value): return self.clone(pow(value, self._value))
+
+ if sys.version_info[0] <= 2:
+ def __div__(self, value): return self.clone(self._value // value)
+ def __rdiv__(self, value): return self.clone(value // self._value)
+ else:
+ def __truediv__(self, value): return self.clone(self._value / value)
+ def __rtruediv__(self, value): return self.clone(value / self._value)
+ def __divmod__(self, value): return self.clone(self._value // value)
+ def __rdivmod__(self, value): return self.clone(value // self._value)
+
+ __hash__ = base.AbstractSimpleAsn1Item.__hash__
+
+ def __int__(self): return int(self._value)
+ if sys.version_info[0] <= 2:
+ def __long__(self): return long(self._value)
+ def __float__(self): return float(self._value)
+ def __abs__(self): return abs(self._value)
+ def __index__(self): return int(self._value)
+
+ def __lt__(self, value): return self._value < value
+ def __le__(self, value): return self._value <= value
+ def __eq__(self, value): return self._value == value
+ def __ne__(self, value): return self._value != value
+ def __gt__(self, value): return self._value > value
+ def __ge__(self, value): return self._value >= value
+
+ def prettyIn(self, value):
+ if not isinstance(value, str):
+ return int(value)
+ r = self.__namedValues.getValue(value)
+ if r is not None:
+ return r
+ try:
+ return int(value)
+ except ValueError:
+ raise error.PyAsn1Error(
+ 'Can\'t coerce %s into integer: %s' % (value, sys.exc_info()[1])
+ )
+
+ def prettyOut(self, value):
+ r = self.__namedValues.getName(value)
+ return r is None and str(value) or repr(r)
+
+ def getNamedValues(self): return self.__namedValues
+
+ def clone(self, value=None, tagSet=None, subtypeSpec=None,
+ namedValues=None):
+ if value is None and tagSet is None and subtypeSpec is None \
+ and namedValues is None:
+ return self
+ if value is None:
+ value = self._value
+ if tagSet is None:
+ tagSet = self._tagSet
+ if subtypeSpec is None:
+ subtypeSpec = self._subtypeSpec
+ if namedValues is None:
+ namedValues = self.__namedValues
+ return self.__class__(value, tagSet, subtypeSpec, namedValues)
+
+ def subtype(self, value=None, implicitTag=None, explicitTag=None,
+ subtypeSpec=None, namedValues=None):
+ if value is None:
+ value = self._value
+ if implicitTag is not None:
+ tagSet = self._tagSet.tagImplicitly(implicitTag)
+ elif explicitTag is not None:
+ tagSet = self._tagSet.tagExplicitly(explicitTag)
+ else:
+ tagSet = self._tagSet
+ if subtypeSpec is None:
+ subtypeSpec = self._subtypeSpec
+ else:
+ subtypeSpec = subtypeSpec + self._subtypeSpec
+ if namedValues is None:
+ namedValues = self.__namedValues
+ else:
+ namedValues = namedValues + self.__namedValues
+ return self.__class__(value, tagSet, subtypeSpec, namedValues)
+
+class Boolean(Integer):
+ tagSet = baseTagSet = tag.initTagSet(
+ tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 0x01),
+ )
+ subtypeSpec = Integer.subtypeSpec+constraint.SingleValueConstraint(0,1)
+ namedValues = Integer.namedValues.clone(('False', 0), ('True', 1))
+
+class BitString(base.AbstractSimpleAsn1Item):
+ tagSet = baseTagSet = tag.initTagSet(
+ tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 0x03)
+ )
+ namedValues = namedval.NamedValues()
+ def __init__(self, value=None, tagSet=None, subtypeSpec=None,
+ namedValues=None):
+ if namedValues is None:
+ self.__namedValues = self.namedValues
+ else:
+ self.__namedValues = namedValues
+ base.AbstractSimpleAsn1Item.__init__(
+ self, value, tagSet, subtypeSpec
+ )
+
+ def clone(self, value=None, tagSet=None, subtypeSpec=None,
+ namedValues=None):
+ if value is None and tagSet is None and subtypeSpec is None \
+ and namedValues is None:
+ return self
+ if value is None:
+ value = self._value
+ if tagSet is None:
+ tagSet = self._tagSet
+ if subtypeSpec is None:
+ subtypeSpec = self._subtypeSpec
+ if namedValues is None:
+ namedValues = self.__namedValues
+ return self.__class__(value, tagSet, subtypeSpec, namedValues)
+
+ def subtype(self, value=None, implicitTag=None, explicitTag=None,
+ subtypeSpec=None, namedValues=None):
+ if value is None:
+ value = self._value
+ if implicitTag is not None:
+ tagSet = self._tagSet.tagImplicitly(implicitTag)
+ elif explicitTag is not None:
+ tagSet = self._tagSet.tagExplicitly(explicitTag)
+ else:
+ tagSet = self._tagSet
+ if subtypeSpec is None:
+ subtypeSpec = self._subtypeSpec
+ else:
+ subtypeSpec = subtypeSpec + self._subtypeSpec
+ if namedValues is None:
+ namedValues = self.__namedValues
+ else:
+ namedValues = namedValues + self.__namedValues
+ return self.__class__(value, tagSet, subtypeSpec, namedValues)
+
+ def __str__(self): return str(tuple(self))
+
+ # Immutable sequence object protocol
+
+ def __len__(self):
+ if self._len is None:
+ self._len = len(self._value)
+ return self._len
+ def __getitem__(self, i):
+ if isinstance(i, slice):
+ return self.clone(operator.getitem(self._value, i))
+ else:
+ return self._value[i]
+
+ def __add__(self, value): return self.clone(self._value + value)
+ def __radd__(self, value): return self.clone(value + self._value)
+ def __mul__(self, value): return self.clone(self._value * value)
+ def __rmul__(self, value): return self * value
+
+ def prettyIn(self, value):
+ r = []
+ if not value:
+ return ()
+ elif isinstance(value, str):
+ if value[0] == '\'':
+ if value[-2:] == '\'B':
+ for v in value[1:-2]:
+ if v == '0':
+ r.append(0)
+ elif v == '1':
+ r.append(1)
+ else:
+ raise error.PyAsn1Error(
+ 'Non-binary BIT STRING initializer %s' % (v,)
+ )
+ return tuple(r)
+ elif value[-2:] == '\'H':
+ for v in value[1:-2]:
+ i = 4
+ v = int(v, 16)
+ while i:
+ i = i - 1
+ r.append((v>>i)&0x01)
+ return tuple(r)
+ else:
+ raise error.PyAsn1Error(
+ 'Bad BIT STRING value notation %s' % value
+ )
+ else:
+ for i in value.split(','):
+ j = self.__namedValues.getValue(i)
+ if j is None:
+ raise error.PyAsn1Error(
+ 'Unknown bit identifier \'%s\'' % i
+ )
+ if j >= len(r):
+ r.extend([0]*(j-len(r)+1))
+ r[j] = 1
+ return tuple(r)
+ elif isinstance(value, (tuple, list)):
+ r = tuple(value)
+ for b in r:
+ if b and b != 1:
+ raise error.PyAsn1Error(
+ 'Non-binary BitString initializer \'%s\'' % (r,)
+ )
+ return r
+ elif isinstance(value, BitString):
+ return tuple(value)
+ else:
+ raise error.PyAsn1Error(
+ 'Bad BitString initializer type \'%s\'' % (value,)
+ )
+
+ def prettyOut(self, value):
+ return '\"\'%s\'B\"' % ''.join([str(x) for x in value])
+
+class OctetString(base.AbstractSimpleAsn1Item):
+ tagSet = baseTagSet = tag.initTagSet(
+ tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 0x04)
+ )
+ defaultBinValue = defaultHexValue = base.noValue
+ encoding = 'us-ascii'
+ def __init__(self, value=None, tagSet=None, subtypeSpec=None,
+ encoding=None, binValue=None, hexValue=None):
+ if encoding is None:
+ self._encoding = self.encoding
+ else:
+ self._encoding = encoding
+ if binValue is not None:
+ value = self.fromBinaryString(binValue)
+ if hexValue is not None:
+ value = self.fromHexString(hexValue)
+ if value is None or value is base.noValue:
+ value = self.defaultHexValue
+ if value is None or value is base.noValue:
+ value = self.defaultBinValue
+ self.__intValue = None
+ base.AbstractSimpleAsn1Item.__init__(self, value, tagSet, subtypeSpec)
+
+ def clone(self, value=None, tagSet=None, subtypeSpec=None,
+ encoding=None, binValue=None, hexValue=None):
+ if value is None and tagSet is None and subtypeSpec is None and \
+ encoding is None and binValue is None and hexValue is None:
+ return self
+ if value is None and binValue is None and hexValue is None:
+ value = self._value
+ if tagSet is None:
+ tagSet = self._tagSet
+ if subtypeSpec is None:
+ subtypeSpec = self._subtypeSpec
+ if encoding is None:
+ encoding = self._encoding
+ return self.__class__(
+ value, tagSet, subtypeSpec, encoding, binValue, hexValue
+ )
+
+ if sys.version_info[0] <= 2:
+ def prettyIn(self, value):
+ if isinstance(value, str):
+ return value
+ elif isinstance(value, (tuple, list)):
+ try:
+ return ''.join([ chr(x) for x in value ])
+ except ValueError:
+ raise error.PyAsn1Error(
+ 'Bad OctetString initializer \'%s\'' % (value,)
+ )
+ else:
+ return str(value)
+ else:
+ def prettyIn(self, value):
+ if isinstance(value, bytes):
+ return value
+ elif isinstance(value, OctetString):
+ return value.asOctets()
+ elif isinstance(value, (tuple, list, map)):
+ try:
+ return bytes(value)
+ except ValueError:
+ raise error.PyAsn1Error(
+ 'Bad OctetString initializer \'%s\'' % (value,)
+ )
+ else:
+ try:
+ return str(value).encode(self._encoding)
+ except UnicodeEncodeError:
+ raise error.PyAsn1Error(
+ 'Can\'t encode string \'%s\' with \'%s\' codec' % (value, self._encoding)
+ )
+
+
+ def fromBinaryString(self, value):
+ bitNo = 8; byte = 0; r = ()
+ for v in value:
+ if bitNo:
+ bitNo = bitNo - 1
+ else:
+ bitNo = 7
+ r = r + (byte,)
+ byte = 0
+ if v == '0':
+ v = 0
+ elif v == '1':
+ v = 1
+ else:
+ raise error.PyAsn1Error(
+ 'Non-binary OCTET STRING initializer %s' % (v,)
+ )
+ byte = byte | (v << bitNo)
+ return octets.ints2octs(r + (byte,))
+
+ def fromHexString(self, value):
+ r = p = ()
+ for v in value:
+ if p:
+ r = r + (int(p+v, 16),)
+ p = ()
+ else:
+ p = v
+ if p:
+ r = r + (int(p+'0', 16),)
+ return octets.ints2octs(r)
+
+ def prettyOut(self, value):
+ if sys.version_info[0] <= 2:
+ numbers = tuple([ ord(x) for x in value ])
+ else:
+ numbers = tuple(value)
+ if [ x for x in numbers if x < 32 or x > 126 ]:
+ return '0x' + ''.join([ '%.2x' % x for x in numbers ])
+ else:
+ return str(value)
+
+ def __repr__(self):
+ if self._value is base.noValue:
+ return self.__class__.__name__ + '()'
+ if [ x for x in self.asNumbers() if x < 32 or x > 126 ]:
+ return self.__class__.__name__ + '(hexValue=\'' + ''.join([ '%.2x' % x for x in self.asNumbers() ])+'\')'
+ else:
+ return self.__class__.__name__ + '(\'' + self.prettyOut(self._value) + '\')'
+
+ if sys.version_info[0] <= 2:
+ def __str__(self): return str(self._value)
+ def __unicode__(self):
+ return self._value.decode(self._encoding, 'ignore')
+ def asOctets(self): return self._value
+ def asNumbers(self):
+ if self.__intValue is None:
+ self.__intValue = tuple([ ord(x) for x in self._value ])
+ return self.__intValue
+ else:
+ def __str__(self): return self._value.decode(self._encoding, 'ignore')
+ def __bytes__(self): return self._value
+ def asOctets(self): return self._value
+ def asNumbers(self):
+ if self.__intValue is None:
+ self.__intValue = tuple(self._value)
+ return self.__intValue
+
+ # Immutable sequence object protocol
+
+ def __len__(self):
+ if self._len is None:
+ self._len = len(self._value)
+ return self._len
+ def __getitem__(self, i):
+ if isinstance(i, slice):
+ return self.clone(operator.getitem(self._value, i))
+ else:
+ return self._value[i]
+
+ def __add__(self, value): return self.clone(self._value + self.prettyIn(value))
+ def __radd__(self, value): return self.clone(self.prettyIn(value) + self._value)
+ def __mul__(self, value): return self.clone(self._value * value)
+ def __rmul__(self, value): return self * value
+
+class Null(OctetString):
+ defaultValue = ''.encode() # This is tightly constrained
+ tagSet = baseTagSet = tag.initTagSet(
+ tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 0x05)
+ )
+ subtypeSpec = OctetString.subtypeSpec+constraint.SingleValueConstraint(''.encode())
+
+if sys.version_info[0] <= 2:
+ intTypes = (int, long)
+else:
+ intTypes = int
+
+class ObjectIdentifier(base.AbstractSimpleAsn1Item):
+ tagSet = baseTagSet = tag.initTagSet(
+ tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 0x06)
+ )
+ def __add__(self, other): return self.clone(self._value + other)
+ def __radd__(self, other): return self.clone(other + self._value)
+
+ def asTuple(self): return self._value
+
+ # Sequence object protocol
+
+ def __len__(self):
+ if self._len is None:
+ self._len = len(self._value)
+ return self._len
+ def __getitem__(self, i):
+ if isinstance(i, slice):
+ return self.clone(
+ operator.getitem(self._value, i)
+ )
+ else:
+ return self._value[i]
+
+ def __str__(self): return self.prettyPrint()
+
+ def index(self, suboid): return self._value.index(suboid)
+
+ def isPrefixOf(self, value):
+ """Returns true if argument OID resides deeper in the OID tree"""
+ l = len(self)
+ if l <= len(value):
+ if self._value[:l] == value[:l]:
+ return 1
+ return 0
+
+ def prettyIn(self, value):
+ """Dotted -> tuple of numerics OID converter"""
+ if isinstance(value, tuple):
+ pass
+ elif isinstance(value, ObjectIdentifier):
+ return tuple(value)
+ elif isinstance(value, str):
+ r = []
+ for element in [ x for x in value.split('.') if x != '' ]:
+ try:
+ r.append(int(element, 0))
+ except ValueError:
+ raise error.PyAsn1Error(
+ 'Malformed Object ID %s at %s: %s' %
+ (str(value), self.__class__.__name__, sys.exc_info()[1])
+ )
+ value = tuple(r)
+ else:
+ try:
+ value = tuple(value)
+ except TypeError:
+ raise error.PyAsn1Error(
+ 'Malformed Object ID %s at %s: %s' %
+ (str(value), self.__class__.__name__,sys.exc_info()[1])
+ )
+
+ for x in value:
+ if not isinstance(x, intTypes) or x < 0:
+ raise error.PyAsn1Error(
+ 'Invalid sub-ID in %s at %s' % (value, self.__class__.__name__)
+ )
+
+ return value
+
+ def prettyOut(self, value): return '.'.join([ str(x) for x in value ])
+
+class Real(base.AbstractSimpleAsn1Item):
+ try:
+ _plusInf = float('inf')
+ _minusInf = float('-inf')
+ _inf = (_plusInf, _minusInf)
+ except ValueError:
+ # Infinity support is platform and Python dependent
+ _plusInf = _minusInf = None
+ _inf = ()
+
+ tagSet = baseTagSet = tag.initTagSet(
+ tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 0x09)
+ )
+
+ def __normalizeBase10(self, value):
+ m, b, e = value
+ while m and m % 10 == 0:
+ m = m / 10
+ e = e + 1
+ return m, b, e
+
+ def prettyIn(self, value):
+ if isinstance(value, tuple) and len(value) == 3:
+ for d in value:
+ if not isinstance(d, intTypes):
+ raise error.PyAsn1Error(
+ 'Lame Real value syntax: %s' % (value,)
+ )
+ if value[1] not in (2, 10):
+ raise error.PyAsn1Error(
+ 'Prohibited base for Real value: %s' % value[1]
+ )
+ if value[1] == 10:
+ value = self.__normalizeBase10(value)
+ return value
+ elif isinstance(value, intTypes):
+ return self.__normalizeBase10((value, 10, 0))
+ elif isinstance(value, float):
+ if self._inf and value in self._inf:
+ return value
+ else:
+ e = 0
+ while int(value) != value:
+ value = value * 10
+ e = e - 1
+ return self.__normalizeBase10((int(value), 10, e))
+ elif isinstance(value, Real):
+ return tuple(value)
+ elif isinstance(value, str): # handle infinite literal
+ try:
+ return float(value)
+ except ValueError:
+ pass
+ raise error.PyAsn1Error(
+ 'Bad real value syntax: %s' % (value,)
+ )
+
+ def prettyOut(self, value):
+ if value in self._inf:
+ return '\'%s\'' % value
+ else:
+ return str(value)
+
+ def isPlusInfinity(self): return self._value == self._plusInf
+ def isMinusInfinity(self): return self._value == self._minusInf
+ def isInfinity(self): return self._value in self._inf
+
+ def __str__(self): return str(float(self))
+
+ def __add__(self, value): return self.clone(float(self) + value)
+ def __radd__(self, value): return self + value
+ def __mul__(self, value): return self.clone(float(self) * value)
+ def __rmul__(self, value): return self * value
+ def __sub__(self, value): return self.clone(float(self) - value)
+ def __rsub__(self, value): return self.clone(value - float(self))
+ def __mod__(self, value): return self.clone(float(self) % value)
+ def __rmod__(self, value): return self.clone(value % float(self))
+ def __pow__(self, value, modulo=None): return self.clone(pow(float(self), value, modulo))
+ def __rpow__(self, value): return self.clone(pow(value, float(self)))
+
+ if sys.version_info[0] <= 2:
+ def __div__(self, value): return self.clone(float(self) / value)
+ def __rdiv__(self, value): return self.clone(value / float(self))
+ else:
+ def __truediv__(self, value): return self.clone(float(self) / value)
+ def __rtruediv__(self, value): return self.clone(value / float(self))
+ def __divmod__(self, value): return self.clone(float(self) // value)
+ def __rdivmod__(self, value): return self.clone(value // float(self))
+
+ def __int__(self): return int(float(self))
+ if sys.version_info[0] <= 2:
+ def __long__(self): return long(float(self))
+ def __float__(self):
+ if self._value in self._inf:
+ return self._value
+ else:
+ return float(
+ self._value[0] * pow(self._value[1], self._value[2])
+ )
+ def __abs__(self): return abs(float(self))
+
+ def __lt__(self, value): return float(self) < value
+ def __le__(self, value): return float(self) <= value
+ def __eq__(self, value): return float(self) == value
+ def __ne__(self, value): return float(self) != value
+ def __gt__(self, value): return float(self) > value
+ def __ge__(self, value): return float(self) >= value
+
+ if sys.version_info[0] <= 2:
+ def __nonzero__(self): return bool(float(self))
+ else:
+ def __bool__(self): return bool(float(self))
+ __hash__ = base.AbstractSimpleAsn1Item.__hash__
+
+ def __getitem__(self, idx):
+ if self._value in self._inf:
+ raise error.PyAsn1Error('Invalid infinite value operation')
+ else:
+ return self._value[idx]
+
+class Enumerated(Integer):
+ tagSet = baseTagSet = tag.initTagSet(
+ tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 0x0A)
+ )
+
+# "Structured" ASN.1 types
+
+class SetOf(base.AbstractConstructedAsn1Item):
+ componentType = None
+ tagSet = baseTagSet = tag.initTagSet(
+ tag.Tag(tag.tagClassUniversal, tag.tagFormatConstructed, 0x11)
+ )
+ typeId = 1
+
+ def _cloneComponentValues(self, myClone, cloneValueFlag):
+ idx = 0; l = len(self._componentValues)
+ while idx < l:
+ c = self._componentValues[idx]
+ if c is not None:
+ if isinstance(c, base.AbstractConstructedAsn1Item):
+ myClone.setComponentByPosition(
+ idx, c.clone(cloneValueFlag=cloneValueFlag)
+ )
+ else:
+ myClone.setComponentByPosition(idx, c.clone())
+ idx = idx + 1
+
+ def _verifyComponent(self, idx, value):
+ if self._componentType is not None and \
+ not self._componentType.isSuperTypeOf(value):
+ raise error.PyAsn1Error('Component type error %s' % value)
+
+ def getComponentByPosition(self, idx): return self._componentValues[idx]
+ def setComponentByPosition(self, idx, value=None, verifyConstraints=True):
+ l = len(self._componentValues)
+ if idx >= l:
+ self._componentValues = self._componentValues + (idx-l+1)*[None]
+ if value is None:
+ if self._componentValues[idx] is None:
+ if self._componentType is None:
+ raise error.PyAsn1Error('Component type not defined')
+ self._componentValues[idx] = self._componentType.clone()
+ self._componentValuesSet = self._componentValuesSet + 1
+ return self
+ elif not isinstance(value, base.Asn1Item):
+ if self._componentType is None:
+ raise error.PyAsn1Error('Component type not defined')
+ if isinstance(self._componentType, base.AbstractSimpleAsn1Item):
+ value = self._componentType.clone(value=value)
+ else:
+ raise error.PyAsn1Error('Instance value required')
+ if verifyConstraints:
+ if self._componentType is not None:
+ self._verifyComponent(idx, value)
+ self._verifySubtypeSpec(value, idx)
+ if self._componentValues[idx] is None:
+ self._componentValuesSet = self._componentValuesSet + 1
+ self._componentValues[idx] = value
+ return self
+
+ def getComponentTagMap(self):
+ if self._componentType is not None:
+ return self._componentType.getTagMap()
+
+ def prettyPrint(self, scope=0):
+ scope = scope + 1
+ r = self.__class__.__name__ + ':\n'
+ for idx in range(len(self._componentValues)):
+ r = r + ' '*scope
+ if self._componentValues[idx] is None:
+ r = r + ''
+ else:
+ r = r + self._componentValues[idx].prettyPrint(scope)
+ return r
+
+class SequenceOf(SetOf):
+ tagSet = baseTagSet = tag.initTagSet(
+ tag.Tag(tag.tagClassUniversal, tag.tagFormatConstructed, 0x10)
+ )
+ typeId = 2
+
+class SequenceAndSetBase(base.AbstractConstructedAsn1Item):
+ componentType = namedtype.NamedTypes()
+ def __init__(self, componentType=None, tagSet=None,
+ subtypeSpec=None, sizeSpec=None):
+ base.AbstractConstructedAsn1Item.__init__(
+ self, componentType, tagSet, subtypeSpec, sizeSpec
+ )
+ if self._componentType is None:
+ self._componentTypeLen = 0
+ else:
+ self._componentTypeLen = len(self._componentType)
+
+ def __getitem__(self, idx):
+ if isinstance(idx, str):
+ return self.getComponentByName(idx)
+ else:
+ return base.AbstractConstructedAsn1Item.__getitem__(self, idx)
+
+ def __setitem__(self, idx, value):
+ if isinstance(idx, str):
+ self.setComponentByName(idx, value)
+ else:
+ base.AbstractConstructedAsn1Item.__setitem__(self, idx, value)
+
+ def _cloneComponentValues(self, myClone, cloneValueFlag):
+ idx = 0; l = len(self._componentValues)
+ while idx < l:
+ c = self._componentValues[idx]
+ if c is not None:
+ if isinstance(c, base.AbstractConstructedAsn1Item):
+ myClone.setComponentByPosition(
+ idx, c.clone(cloneValueFlag=cloneValueFlag)
+ )
+ else:
+ myClone.setComponentByPosition(idx, c.clone())
+ idx = idx + 1
+
+ def _verifyComponent(self, idx, value):
+ if idx >= self._componentTypeLen:
+ raise error.PyAsn1Error(
+ 'Component type error out of range'
+ )
+ t = self._componentType[idx].getType()
+ if not t.isSuperTypeOf(value):
+ raise error.PyAsn1Error('Component type error %r vs %r' % (t, value))
+
+ def getComponentByName(self, name):
+ return self.getComponentByPosition(
+ self._componentType.getPositionByName(name)
+ )
+ def setComponentByName(self, name, value=None, verifyConstraints=True):
+ return self.setComponentByPosition(
+ self._componentType.getPositionByName(name), value,
+ verifyConstraints
+ )
+
+ def getComponentByPosition(self, idx):
+ try:
+ return self._componentValues[idx]
+ except IndexError:
+ if idx < self._componentTypeLen:
+ return
+ raise
+ def setComponentByPosition(self, idx, value=None, verifyConstraints=True):
+ l = len(self._componentValues)
+ if idx >= l:
+ self._componentValues = self._componentValues + (idx-l+1)*[None]
+ if value is None:
+ if self._componentValues[idx] is None:
+ self._componentValues[idx] = self._componentType.getTypeByPosition(idx).clone()
+ self._componentValuesSet = self._componentValuesSet + 1
+ return self
+ elif not isinstance(value, base.Asn1Item):
+ t = self._componentType.getTypeByPosition(idx)
+ if isinstance(t, base.AbstractSimpleAsn1Item):
+ value = t.clone(value=value)
+ else:
+ raise error.PyAsn1Error('Instance value required')
+ if verifyConstraints:
+ if self._componentTypeLen:
+ self._verifyComponent(idx, value)
+ self._verifySubtypeSpec(value, idx)
+ if self._componentValues[idx] is None:
+ self._componentValuesSet = self._componentValuesSet + 1
+ self._componentValues[idx] = value
+ return self
+
+ def getNameByPosition(self, idx):
+ if self._componentTypeLen:
+ return self._componentType.getNameByPosition(idx)
+
+ def getDefaultComponentByPosition(self, idx):
+ if self._componentTypeLen and self._componentType[idx].isDefaulted:
+ return self._componentType[idx].getType()
+
+ def getComponentType(self):
+ if self._componentTypeLen:
+ return self._componentType
+
+ def setDefaultComponents(self):
+ if self._componentTypeLen == self._componentValuesSet:
+ return
+ idx = self._componentTypeLen
+ while idx:
+ idx = idx - 1
+ if self._componentType[idx].isDefaulted:
+ if self.getComponentByPosition(idx) is None:
+ self.setComponentByPosition(idx)
+ elif not self._componentType[idx].isOptional:
+ if self.getComponentByPosition(idx) is None:
+ raise error.PyAsn1Error(
+ 'Uninitialized component #%s at %r' % (idx, self)
+ )
+
+ def prettyPrint(self, scope=0):
+ scope = scope + 1
+ r = self.__class__.__name__ + ':\n'
+ for idx in range(len(self._componentValues)):
+ if self._componentValues[idx] is not None:
+ r = r + ' '*scope
+ componentType = self.getComponentType()
+ if componentType is None:
+ r = r + ''
+ else:
+ r = r + componentType.getNameByPosition(idx)
+ r = '%s=%s\n' % (
+ r, self._componentValues[idx].prettyPrint(scope)
+ )
+ return r
+
+class Sequence(SequenceAndSetBase):
+ tagSet = baseTagSet = tag.initTagSet(
+ tag.Tag(tag.tagClassUniversal, tag.tagFormatConstructed, 0x10)
+ )
+ typeId = 3
+
+ def getComponentTagMapNearPosition(self, idx):
+ if self._componentType:
+ return self._componentType.getTagMapNearPosition(idx)
+
+ def getComponentPositionNearType(self, tagSet, idx):
+ if self._componentType:
+ return self._componentType.getPositionNearType(tagSet, idx)
+ else:
+ return idx
+
+class Set(SequenceAndSetBase):
+ tagSet = baseTagSet = tag.initTagSet(
+ tag.Tag(tag.tagClassUniversal, tag.tagFormatConstructed, 0x11)
+ )
+ typeId = 4
+
+ def getComponent(self, innerFlag=0): return self
+
+ def getComponentByType(self, tagSet, innerFlag=0):
+ c = self.getComponentByPosition(
+ self._componentType.getPositionByType(tagSet)
+ )
+ if innerFlag and isinstance(c, Set):
+ # get inner component by inner tagSet
+ return c.getComponent(1)
+ else:
+ # get outer component by inner tagSet
+ return c
+
+ def setComponentByType(self, tagSet, value=None, innerFlag=0,
+ verifyConstraints=True):
+ idx = self._componentType.getPositionByType(tagSet)
+ t = self._componentType.getTypeByPosition(idx)
+ if innerFlag: # set inner component by inner tagSet
+ if t.getTagSet():
+ return self.setComponentByPosition(
+ idx, value, verifyConstraints
+ )
+ else:
+ t = self.setComponentByPosition(idx).getComponentByPosition(idx)
+ return t.setComponentByType(
+ tagSet, value, innerFlag, verifyConstraints
+ )
+ else: # set outer component by inner tagSet
+ return self.setComponentByPosition(
+ idx, value, verifyConstraints
+ )
+
+ def getComponentTagMap(self):
+ if self._componentType:
+ return self._componentType.getTagMap(True)
+
+ def getComponentPositionByType(self, tagSet):
+ if self._componentType:
+ return self._componentType.getPositionByType(tagSet)
+
+class Choice(Set):
+ tagSet = baseTagSet = tag.TagSet() # untagged
+ sizeSpec = constraint.ConstraintsIntersection(
+ constraint.ValueSizeConstraint(1, 1)
+ )
+ typeId = 5
+ _currentIdx = None
+
+ def __eq__(self, other):
+ if self._componentValues:
+ return self._componentValues[self._currentIdx] == other
+ return NotImplemented
+ def __ne__(self, other):
+ if self._componentValues:
+ return self._componentValues[self._currentIdx] != other
+ return NotImplemented
+ def __lt__(self, other):
+ if self._componentValues:
+ return self._componentValues[self._currentIdx] < other
+ return NotImplemented
+ def __le__(self, other):
+ if self._componentValues:
+ return self._componentValues[self._currentIdx] <= other
+ return NotImplemented
+ def __gt__(self, other):
+ if self._componentValues:
+ return self._componentValues[self._currentIdx] > other
+ return NotImplemented
+ def __ge__(self, other):
+ if self._componentValues:
+ return self._componentValues[self._currentIdx] >= other
+ return NotImplemented
+ if sys.version_info[0] <= 2:
+ def __nonzero__(self, other): return bool(self._componentValues)
+ else:
+ def __bool__(self, other): return bool(self._componentValues)
+
+ def __len__(self): return self._currentIdx is not None and 1 or 0
+
+ def verifySizeSpec(self):
+ if self._currentIdx is None:
+ raise error.PyAsn1Error('Component not chosen')
+ else:
+ self._sizeSpec(' ')
+
+ def _cloneComponentValues(self, myClone, cloneValueFlag):
+ try:
+ c = self.getComponent()
+ except error.PyAsn1Error:
+ pass
+ else:
+ if isinstance(c, Choice):
+ tagSet = c.getEffectiveTagSet()
+ else:
+ tagSet = c.getTagSet()
+ if isinstance(c, base.AbstractConstructedAsn1Item):
+ myClone.setComponentByType(
+ tagSet, c.clone(cloneValueFlag=cloneValueFlag)
+ )
+ else:
+ myClone.setComponentByType(tagSet, c.clone())
+
+ def setComponentByPosition(self, idx, value=None, verifyConstraints=True):
+ l = len(self._componentValues)
+ if idx >= l:
+ self._componentValues = self._componentValues + (idx-l+1)*[None]
+ if self._currentIdx is not None:
+ self._componentValues[self._currentIdx] = None
+ if value is None:
+ if self._componentValues[idx] is None:
+ self._componentValues[idx] = self._componentType.getTypeByPosition(idx).clone()
+ self._componentValuesSet = 1
+ self._currentIdx = idx
+ return self
+ elif not isinstance(value, base.Asn1Item):
+ value = self._componentType.getTypeByPosition(idx).clone(
+ value=value
+ )
+ if verifyConstraints:
+ if self._componentTypeLen:
+ self._verifyComponent(idx, value)
+ self._verifySubtypeSpec(value, idx)
+ self._componentValues[idx] = value
+ self._currentIdx = idx
+ self._componentValuesSet = 1
+ return self
+
+ def getMinTagSet(self):
+ if self._tagSet:
+ return self._tagSet
+ else:
+ return self._componentType.genMinTagSet()
+
+ def getEffectiveTagSet(self):
+ if self._tagSet:
+ return self._tagSet
+ else:
+ c = self.getComponent()
+ if isinstance(c, Choice):
+ return c.getEffectiveTagSet()
+ else:
+ return c.getTagSet()
+
+ def getTagMap(self):
+ if self._tagSet:
+ return Set.getTagMap(self)
+ else:
+ return Set.getComponentTagMap(self)
+
+ def getComponent(self, innerFlag=0):
+ if self._currentIdx is None:
+ raise error.PyAsn1Error('Component not chosen')
+ else:
+ c = self._componentValues[self._currentIdx]
+ if innerFlag and isinstance(c, Choice):
+ return c.getComponent(innerFlag)
+ else:
+ return c
+
+ def getName(self, innerFlag=0):
+ if self._currentIdx is None:
+ raise error.PyAsn1Error('Component not chosen')
+ else:
+ if innerFlag:
+ c = self._componentValues[self._currentIdx]
+ if isinstance(c, Choice):
+ return c.getName(innerFlag)
+ return self._componentType.getNameByPosition(self._currentIdx)
+
+ def setDefaultComponents(self): pass
+
+class Any(OctetString):
+ tagSet = baseTagSet = tag.TagSet() # untagged
+ typeId = 6
+
+ def getTagMap(self):
+ return tagmap.TagMap(
+ { self.getTagSet(): self },
+ { eoo.endOfOctets.getTagSet(): eoo.endOfOctets },
+ self
+ )
+
+# XXX
+# coercion rules?
diff --git a/libs/pyasn1/type/useful.py b/libs/pyasn1/type/useful.py
new file mode 100644
index 00000000..a7139c22
--- /dev/null
+++ b/libs/pyasn1/type/useful.py
@@ -0,0 +1,12 @@
+# ASN.1 "useful" types
+from pyasn1.type import char, tag
+
+class GeneralizedTime(char.VisibleString):
+ tagSet = char.VisibleString.tagSet.tagImplicitly(
+ tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 24)
+ )
+
+class UTCTime(char.VisibleString):
+ tagSet = char.VisibleString.tagSet.tagImplicitly(
+ tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 23)
+ )
diff --git a/libs/requests/__init__.py b/libs/requests/__init__.py
index 0ace12b6..10c9e7ac 100644
--- a/libs/requests/__init__.py
+++ b/libs/requests/__init__.py
@@ -15,8 +15,8 @@ requests
"""
__title__ = 'requests'
-__version__ = '0.11.1'
-__build__ = 0x001101
+__version__ = '0.13.1'
+__build__ = 0x001301
__author__ = 'Kenneth Reitz'
__license__ = 'ISC'
__copyright__ = 'Copyright 2012 Kenneth Reitz'
diff --git a/libs/requests/api.py b/libs/requests/api.py
index e0bf346c..9cea79af 100644
--- a/libs/requests/api.py
+++ b/libs/requests/api.py
@@ -12,7 +12,9 @@ This module implements the Requests API.
"""
from . import sessions
+from .safe_mode import catch_exceptions_if_in_safe_mode
+@catch_exceptions_if_in_safe_mode
def request(method, url, **kwargs):
"""Constructs and sends a :class:`Request `.
Returns :class:`Response ` object.
@@ -30,7 +32,7 @@ def request(method, url, **kwargs):
:param proxies: (optional) Dictionary mapping protocol to the URL of the proxy.
:param return_response: (optional) If False, an un-sent Request object will returned.
:param session: (optional) A :class:`Session` object to be used for the request.
- :param config: (optional) A configuration dictionary.
+ :param config: (optional) A configuration dictionary. See ``request.defaults`` for allowed keys and their default values.
:param verify: (optional) if ``True``, the SSL cert will be verified. A CA_BUNDLE path can also be provided.
:param prefetch: (optional) if ``True``, the response content will be immediately downloaded.
:param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
diff --git a/libs/requests/async.py b/libs/requests/async.py
index f12cf268..e69de29b 100644
--- a/libs/requests/async.py
+++ b/libs/requests/async.py
@@ -1,106 +0,0 @@
-# -*- coding: utf-8 -*-
-
-"""
-requests.async
-~~~~~~~~~~~~~~
-
-This module contains an asynchronous replica of ``requests.api``, powered
-by gevent. All API methods return a ``Request`` instance (as opposed to
-``Response``). A list of requests can be sent with ``map()``.
-"""
-
-try:
- import gevent
- from gevent import monkey as curious_george
- from gevent.pool import Pool
-except ImportError:
- raise RuntimeError('Gevent is required for requests.async.')
-
-# Monkey-patch.
-curious_george.patch_all(thread=False, select=False)
-
-from . import api
-
-
-__all__ = (
- 'map', 'imap',
- 'get', 'options', 'head', 'post', 'put', 'patch', 'delete', 'request'
-)
-
-
-def patched(f):
- """Patches a given API function to not send."""
-
- def wrapped(*args, **kwargs):
-
- kwargs['return_response'] = False
- kwargs['prefetch'] = True
-
- config = kwargs.get('config', {})
- config.update(safe_mode=True)
-
- kwargs['config'] = config
-
- return f(*args, **kwargs)
-
- return wrapped
-
-
-def send(r, pool=None, prefetch=False):
- """Sends the request object using the specified pool. If a pool isn't
- specified this method blocks. Pools are useful because you can specify size
- and can hence limit concurrency."""
-
- if pool != None:
- return pool.spawn(r.send, prefetch=prefetch)
-
- return gevent.spawn(r.send, prefetch=prefetch)
-
-
-# Patched requests.api functions.
-get = patched(api.get)
-options = patched(api.options)
-head = patched(api.head)
-post = patched(api.post)
-put = patched(api.put)
-patch = patched(api.patch)
-delete = patched(api.delete)
-request = patched(api.request)
-
-
-def map(requests, prefetch=True, size=None):
- """Concurrently converts a list of Requests to Responses.
-
- :param requests: a collection of Request objects.
- :param prefetch: If False, the content will not be downloaded immediately.
- :param size: Specifies the number of requests to make at a time. If None, no throttling occurs.
- """
-
- requests = list(requests)
-
- pool = Pool(size) if size else None
- jobs = [send(r, pool, prefetch=prefetch) for r in requests]
- gevent.joinall(jobs)
-
- return [r.response for r in requests]
-
-
-def imap(requests, prefetch=True, size=2):
- """Concurrently converts a generator object of Requests to
- a generator of Responses.
-
- :param requests: a generator of Request objects.
- :param prefetch: If False, the content will not be downloaded immediately.
- :param size: Specifies the number of requests to make at a time. default is 2
- """
-
- pool = Pool(size)
-
- def send(r):
- r.send(prefetch)
- return r.response
-
- for r in pool.imap_unordered(send, requests):
- yield r
-
- pool.join()
\ No newline at end of file
diff --git a/libs/requests/auth.py b/libs/requests/auth.py
index 385dd27d..cb851d2c 100644
--- a/libs/requests/auth.py
+++ b/libs/requests/auth.py
@@ -7,14 +7,25 @@ requests.auth
This module contains the authentication handlers for Requests.
"""
+import os
import time
import hashlib
from base64 import b64encode
+
from .compat import urlparse, str
-from .utils import randombytes, parse_dict_header
+from .utils import parse_dict_header
+try:
+ from oauthlib.oauth1.rfc5849 import (Client, SIGNATURE_HMAC, SIGNATURE_TYPE_AUTH_HEADER)
+ from oauthlib.common import extract_params
+ # hush pyflakes:
+ SIGNATURE_HMAC; SIGNATURE_TYPE_AUTH_HEADER
+except (ImportError, SyntaxError):
+ SIGNATURE_HMAC = None
+ SIGNATURE_TYPE_AUTH_HEADER = None
+CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'
def _basic_auth_str(username, password):
"""Returns a Basic Auth string."""
@@ -29,6 +40,74 @@ class AuthBase(object):
raise NotImplementedError('Auth hooks must be callable.')
+class OAuth1(AuthBase):
+ """Signs the request using OAuth 1 (RFC5849)"""
+ def __init__(self, client_key,
+ client_secret=None,
+ resource_owner_key=None,
+ resource_owner_secret=None,
+ callback_uri=None,
+ signature_method=SIGNATURE_HMAC,
+ signature_type=SIGNATURE_TYPE_AUTH_HEADER,
+ rsa_key=None, verifier=None):
+
+ try:
+ signature_type = signature_type.upper()
+ except AttributeError:
+ pass
+
+ self.client = Client(client_key, client_secret, resource_owner_key,
+ resource_owner_secret, callback_uri, signature_method,
+ signature_type, rsa_key, verifier)
+
+ def __call__(self, r):
+ """Add OAuth parameters to the request.
+
+ Parameters may be included from the body if the content-type is
+ urlencoded, if no content type is set an educated guess is made.
+ """
+ contenttype = r.headers.get('Content-Type', None)
+ # extract_params will not give params unless the body is a properly
+ # formatted string, a dictionary or a list of 2-tuples.
+ decoded_body = extract_params(r.data)
+ if contenttype == None and decoded_body != None:
+ # extract_params can only check the present r.data and does not know
+ # of r.files, thus an extra check is performed. We know that
+ # if files are present the request will not have
+ # Content-type: x-www-form-urlencoded. We guess it will have
+ # a mimetype of multipart/form-encoded and if this is not the case
+ # we assume the correct header will be set later.
+ if r.files:
+ # Omit body data in the signing and since it will always
+ # be empty (cant add paras to body if multipart) and we wish
+ # to preserve body.
+ r.headers['Content-Type'] = 'multipart/form-encoded'
+ r.url, r.headers, _ = self.client.sign(
+ unicode(r.url), unicode(r.method), None, r.headers)
+ else:
+ # Normal signing
+ r.headers['Content-Type'] = 'application/x-www-form-urlencoded'
+ r.url, r.headers, r.data = self.client.sign(
+ unicode(r.url), unicode(r.method), r.data, r.headers)
+
+ # Having the authorization header, key or value, in unicode will
+ # result in UnicodeDecodeErrors when the request is concatenated
+ # by httplib. This can easily be seen when attaching files.
+ # Note that simply encoding the value is not enough since Python
+ # saves the type of first key set. Thus we remove and re-add.
+ # >>> d = {u'a':u'foo'}
+ # >>> d['a'] = 'foo'
+ # >>> d
+ # { u'a' : 'foo' }
+ u_header = unicode('Authorization')
+ if u_header in r.headers:
+ auth_header = r.headers[u_header].encode('utf-8')
+ del r.headers[u_header]
+ r.headers['Authorization'] = auth_header
+
+ return r
+
+
class HTTPBasicAuth(AuthBase):
"""Attaches HTTP Basic Authentication to the given Request object."""
def __init__(self, username, password):
@@ -56,6 +135,8 @@ class HTTPDigestAuth(AuthBase):
def handle_401(self, r):
"""Takes the given response and tries digest-auth, if needed."""
+ r.request.deregister_hook('response', self.handle_401)
+
s_auth = r.headers.get('www-authenticate', '')
if 'digest' in s_auth.lower():
@@ -74,21 +155,21 @@ class HTTPDigestAuth(AuthBase):
algorithm = algorithm.upper()
# lambdas assume digest modules are imported at the top level
if algorithm == 'MD5':
- def h(x):
+ def md5_utf8(x):
if isinstance(x, str):
x = x.encode('utf-8')
return hashlib.md5(x).hexdigest()
- H = h
+ hash_utf8 = md5_utf8
elif algorithm == 'SHA':
- def h(x):
+ def sha_utf8(x):
if isinstance(x, str):
x = x.encode('utf-8')
return hashlib.sha1(x).hexdigest()
- H = h
+ hash_utf8 = sha_utf8
# XXX MD5-sess
- KD = lambda s, d: H("%s:%s" % (s, d))
+ KD = lambda s, d: hash_utf8("%s:%s" % (s, d))
- if H is None:
+ if hash_utf8 is None:
return None
# XXX not implemented yet
@@ -112,13 +193,13 @@ class HTTPDigestAuth(AuthBase):
s = str(nonce_count).encode('utf-8')
s += nonce.encode('utf-8')
s += time.ctime().encode('utf-8')
- s += randombytes(8)
+ s += os.urandom(8)
cnonce = (hashlib.sha1(s).hexdigest()[:16])
- noncebit = "%s:%s:%s:%s:%s" % (nonce, ncvalue, cnonce, qop, H(A2))
- respdig = KD(H(A1), noncebit)
+ noncebit = "%s:%s:%s:%s:%s" % (nonce, ncvalue, cnonce, qop, hash_utf8(A2))
+ respdig = KD(hash_utf8(A1), noncebit)
elif qop is None:
- respdig = KD(H(A1), "%s:%s" % (nonce, H(A2)))
+ respdig = KD(hash_utf8(A1), "%s:%s" % (nonce, hash_utf8(A2)))
else:
# XXX handle auth-int.
return None
diff --git a/libs/requests/compat.py b/libs/requests/compat.py
index fec7a01d..37063f58 100644
--- a/libs/requests/compat.py
+++ b/libs/requests/compat.py
@@ -83,7 +83,7 @@ if is_py2:
from urlparse import urlparse, urlunparse, urljoin, urlsplit
from urllib2 import parse_http_list
import cookielib
- from .packages.oreos.monkeys import SimpleCookie
+ from Cookie import Morsel
from StringIO import StringIO
bytes = str
@@ -96,7 +96,7 @@ elif is_py3:
from urllib.parse import urlparse, urlunparse, urljoin, urlsplit, urlencode, quote, unquote
from urllib.request import parse_http_list
from http import cookiejar as cookielib
- from http.cookies import SimpleCookie
+ from http.cookies import Morsel
from io import StringIO
str = str
diff --git a/libs/requests/cookies.py b/libs/requests/cookies.py
new file mode 100644
index 00000000..85726b06
--- /dev/null
+++ b/libs/requests/cookies.py
@@ -0,0 +1,363 @@
+"""
+Compatibility code to be able to use `cookielib.CookieJar` with requests.
+
+requests.utils imports from here, so be careful with imports.
+"""
+
+import collections
+from .compat import cookielib, urlparse, Morsel
+
+try:
+ import threading
+ # grr, pyflakes: this fixes "redefinition of unused 'threading'"
+ threading
+except ImportError:
+ import dummy_threading as threading
+
+class MockRequest(object):
+ """Wraps a `requests.Request` to mimic a `urllib2.Request`.
+
+ The code in `cookielib.CookieJar` expects this interface in order to correctly
+ manage cookie policies, i.e., determine whether a cookie can be set, given the
+ domains of the request and the cookie.
+
+ The original request object is read-only. The client is responsible for collecting
+ the new headers via `get_new_headers()` and interpreting them appropriately. You
+ probably want `get_cookie_header`, defined below.
+ """
+
+ def __init__(self, request):
+ self._r = request
+ self._new_headers = {}
+
+ def get_type(self):
+ return urlparse(self._r.full_url).scheme
+
+ def get_host(self):
+ return urlparse(self._r.full_url).netloc
+
+ def get_origin_req_host(self):
+ if self._r.response.history:
+ r = self._r.response.history[0]
+ return urlparse(r).netloc
+ else:
+ return self.get_host()
+
+ def get_full_url(self):
+ return self._r.full_url
+
+ def is_unverifiable(self):
+ # unverifiable == redirected
+ return bool(self._r.response.history)
+
+ def has_header(self, name):
+ return name in self._r.headers or name in self._new_headers
+
+ def get_header(self, name, default=None):
+ return self._r.headers.get(name, self._new_headers.get(name, default))
+
+ def add_header(self, key, val):
+ """cookielib has no legitimate use for this method; add it back if you find one."""
+ raise NotImplementedError("Cookie headers should be added with add_unredirected_header()")
+
+ def add_unredirected_header(self, name, value):
+ self._new_headers[name] = value
+
+ def get_new_headers(self):
+ return self._new_headers
+
+class MockResponse(object):
+ """Wraps a `httplib.HTTPMessage` to mimic a `urllib.addinfourl`.
+
+ ...what? Basically, expose the parsed HTTP headers from the server response
+ the way `cookielib` expects to see them.
+ """
+
+ def __init__(self, headers):
+ """Make a MockResponse for `cookielib` to read.
+
+ :param headers: a httplib.HTTPMessage or analogous carrying the headers
+ """
+ self._headers = headers
+
+ def info(self):
+ return self._headers
+
+ def getheaders(self, name):
+ self._headers.getheaders(name)
+
+def extract_cookies_to_jar(jar, request, response):
+ """Extract the cookies from the response into a CookieJar.
+
+ :param jar: cookielib.CookieJar (not necessarily a RequestsCookieJar)
+ :param request: our own requests.Request object
+ :param response: urllib3.HTTPResponse object
+ """
+ # the _original_response field is the wrapped httplib.HTTPResponse object,
+ req = MockRequest(request)
+ # pull out the HTTPMessage with the headers and put it in the mock:
+ res = MockResponse(response._original_response.msg)
+ jar.extract_cookies(res, req)
+
+def get_cookie_header(jar, request):
+ """Produce an appropriate Cookie header string to be sent with `request`, or None."""
+ r = MockRequest(request)
+ jar.add_cookie_header(r)
+ return r.get_new_headers().get('Cookie')
+
+def remove_cookie_by_name(cookiejar, name, domain=None, path=None):
+ """Unsets a cookie by name, by default over all domains and paths.
+
+ Wraps CookieJar.clear(), is O(n).
+ """
+ clearables = []
+ for cookie in cookiejar:
+ if cookie.name == name:
+ if domain is None or domain == cookie.domain:
+ if path is None or path == cookie.path:
+ clearables.append((cookie.domain, cookie.path, cookie.name))
+
+ for domain, path, name in clearables:
+ cookiejar.clear(domain, path, name)
+
+class CookieConflictError(RuntimeError):
+ """There are two cookies that meet the criteria specified in the cookie jar.
+ Use .get and .set and include domain and path args in order to be more specific."""
+
+class RequestsCookieJar(cookielib.CookieJar, collections.MutableMapping):
+ """Compatibility class; is a cookielib.CookieJar, but exposes a dict interface.
+
+ This is the CookieJar we create by default for requests and sessions that
+ don't specify one, since some clients may expect response.cookies and
+ session.cookies to support dict operations.
+
+ Don't use the dict interface internally; it's just for compatibility with
+ with external client code. All `requests` code should work out of the box
+ with externally provided instances of CookieJar, e.g., LWPCookieJar and
+ FileCookieJar.
+
+ Caution: dictionary operations that are normally O(1) may be O(n).
+
+ Unlike a regular CookieJar, this class is pickleable.
+ """
+
+ def get(self, name, default=None, domain=None, path=None):
+ """Dict-like get() that also supports optional domain and path args in
+ order to resolve naming collisions from using one cookie jar over
+ multiple domains. Caution: operation is O(n), not O(1)."""
+ try:
+ return self._find_no_duplicates(name, domain, path)
+ except KeyError:
+ return default
+
+ def set(self, name, value, **kwargs):
+ """Dict-like set() that also supports optional domain and path args in
+ order to resolve naming collisions from using one cookie jar over
+ multiple domains."""
+ # support client code that unsets cookies by assignment of a None value:
+ if value is None:
+ remove_cookie_by_name(self, name, domain=kwargs.get('domain'), path=kwargs.get('path'))
+ return
+
+ if isinstance(value, Morsel):
+ c = morsel_to_cookie(value)
+ else:
+ c = create_cookie(name, value, **kwargs)
+ self.set_cookie(c)
+ return c
+
+ def keys(self):
+ """Dict-like keys() that returns a list of names of cookies from the jar.
+ See values() and items()."""
+ keys = []
+ for cookie in iter(self):
+ keys.append(cookie.name)
+ return keys
+
+ def values(self):
+ """Dict-like values() that returns a list of values of cookies from the jar.
+ See keys() and items()."""
+ values = []
+ for cookie in iter(self):
+ values.append(cookie.value)
+ return values
+
+ def items(self):
+ """Dict-like items() that returns a list of name-value tuples from the jar.
+ See keys() and values(). Allows client-code to call "dict(RequestsCookieJar)
+ and get a vanilla python dict of key value pairs."""
+ items = []
+ for cookie in iter(self):
+ items.append((cookie.name, cookie.value))
+ return items
+
+ def list_domains(self):
+ """Utility method to list all the domains in the jar."""
+ domains = []
+ for cookie in iter(self):
+ if cookie.domain not in domains:
+ domains.append(cookie.domain)
+ return domains
+
+ def list_paths(self):
+ """Utility method to list all the paths in the jar."""
+ paths = []
+ for cookie in iter(self):
+ if cookie.path not in paths:
+ paths.append(cookie.path)
+ return paths
+
+ def multiple_domains(self):
+ """Returns True if there are multiple domains in the jar.
+ Returns False otherwise."""
+ domains = []
+ for cookie in iter(self):
+ if cookie.domain is not None and cookie.domain in domains:
+ return True
+ domains.append(cookie.domain)
+ return False # there is only one domain in jar
+
+ def get_dict(self, domain=None, path=None):
+ """Takes as an argument an optional domain and path and returns a plain old
+ Python dict of name-value pairs of cookies that meet the requirements."""
+ dictionary = {}
+ for cookie in iter(self):
+ if (domain == None or cookie.domain == domain) and (path == None
+ or cookie.path == path):
+ dictionary[cookie.name] = cookie.value
+ return dictionary
+
+ def __getitem__(self, name):
+ """Dict-like __getitem__() for compatibility with client code. Throws exception
+ if there are more than one cookie with name. In that case, use the more
+ explicit get() method instead. Caution: operation is O(n), not O(1)."""
+ return self._find_no_duplicates(name)
+
+ def __setitem__(self, name, value):
+ """Dict-like __setitem__ for compatibility with client code. Throws exception
+ if there is already a cookie of that name in the jar. In that case, use the more
+ explicit set() method instead."""
+ self.set(name, value)
+
+ def __delitem__(self, name):
+ """Deletes a cookie given a name. Wraps cookielib.CookieJar's remove_cookie_by_name()."""
+ remove_cookie_by_name(self, name)
+
+ def _find(self, name, domain=None, path=None):
+ """Requests uses this method internally to get cookie values. Takes as args name
+ and optional domain and path. Returns a cookie.value. If there are conflicting cookies,
+ _find arbitrarily chooses one. See _find_no_duplicates if you want an exception thrown
+ if there are conflicting cookies."""
+ for cookie in iter(self):
+ if cookie.name == name:
+ if domain is None or cookie.domain == domain:
+ if path is None or cookie.path == path:
+ return cookie.value
+
+ raise KeyError('name=%r, domain=%r, path=%r' % (name, domain, path))
+
+ def _find_no_duplicates(self, name, domain=None, path=None):
+ """__get_item__ and get call _find_no_duplicates -- never used in Requests internally.
+ Takes as args name and optional domain and path. Returns a cookie.value.
+ Throws KeyError if cookie is not found and CookieConflictError if there are
+ multiple cookies that match name and optionally domain and path."""
+ toReturn = None
+ for cookie in iter(self):
+ if cookie.name == name:
+ if domain is None or cookie.domain == domain:
+ if path is None or cookie.path == path:
+ if toReturn != None: # if there are multiple cookies that meet passed in criteria
+ raise CookieConflictError('There are multiple cookies with name, %r' % (name))
+ toReturn = cookie.value # we will eventually return this as long as no cookie conflict
+
+ if toReturn:
+ return toReturn
+ raise KeyError('name=%r, domain=%r, path=%r' % (name, domain, path))
+
+ def __getstate__(self):
+ """Unlike a normal CookieJar, this class is pickleable."""
+ state = self.__dict__.copy()
+ # remove the unpickleable RLock object
+ state.pop('_cookies_lock')
+ return state
+
+ def __setstate__(self, state):
+ """Unlike a normal CookieJar, this class is pickleable."""
+ self.__dict__.update(state)
+ if '_cookies_lock' not in self.__dict__:
+ self._cookies_lock = threading.RLock()
+
+ def copy(self):
+ """This is not implemented. Calling this will throw an exception."""
+ raise NotImplementedError
+
+def create_cookie(name, value, **kwargs):
+ """Make a cookie from underspecified parameters.
+
+ By default, the pair of `name` and `value` will be set for the domain ''
+ and sent on every request (this is sometimes called a "supercookie").
+ """
+ result = dict(
+ version=0,
+ name=name,
+ value=value,
+ port=None,
+ domain='',
+ path='/',
+ secure=False,
+ expires=None,
+ discard=True,
+ comment=None,
+ comment_url=None,
+ rest={'HttpOnly': None},
+ rfc2109=False,
+ )
+
+ badargs = set(kwargs) - set(result)
+ if badargs:
+ err = 'create_cookie() got unexpected keyword arguments: %s'
+ raise TypeError(err % list(badargs))
+
+ result.update(kwargs)
+ result['port_specified'] = bool(result['port'])
+ result['domain_specified'] = bool(result['domain'])
+ result['domain_initial_dot'] = result['domain'].startswith('.')
+ result['path_specified'] = bool(result['path'])
+
+ return cookielib.Cookie(**result)
+
+def morsel_to_cookie(morsel):
+ """Convert a Morsel object into a Cookie containing the one k/v pair."""
+ c = create_cookie(
+ name=morsel.key,
+ value=morsel.value,
+ version=morsel['version'] or 0,
+ port=None,
+ port_specified=False,
+ domain=morsel['domain'],
+ domain_specified=bool(morsel['domain']),
+ domain_initial_dot=morsel['domain'].startswith('.'),
+ path=morsel['path'],
+ path_specified=bool(morsel['path']),
+ secure=bool(morsel['secure']),
+ expires=morsel['max-age'] or morsel['expires'],
+ discard=False,
+ comment=morsel['comment'],
+ comment_url=bool(morsel['comment']),
+ rest={'HttpOnly': morsel['httponly']},
+ rfc2109=False,
+ )
+ return c
+
+def cookiejar_from_dict(cookie_dict, cookiejar=None):
+ """Returns a CookieJar from a key/value dictionary.
+
+ :param cookie_dict: Dict of key/values to insert into CookieJar.
+ """
+ if cookiejar is None:
+ cookiejar = RequestsCookieJar()
+
+ if cookie_dict is not None:
+ for name in cookie_dict:
+ cookiejar.set_cookie(create_cookie(name, cookie_dict[name]))
+ return cookiejar
diff --git a/libs/requests/defaults.py b/libs/requests/defaults.py
index 6a7ea270..41e279df 100644
--- a/libs/requests/defaults.py
+++ b/libs/requests/defaults.py
@@ -20,6 +20,7 @@ Configurations:
:pool_connections: The number of active HTTP connection pools to use.
:encode_uri: If true, URIs will automatically be percent-encoded.
:trust_env: If true, the surrouding environment will be trusted (environ, netrc).
+:param store_cookies: If false, the received cookies as part of the HTTP response would be ignored.
"""
@@ -47,5 +48,6 @@ defaults['strict_mode'] = False
defaults['keep_alive'] = True
defaults['encode_uri'] = True
defaults['trust_env'] = True
+defaults['store_cookies'] = True
diff --git a/libs/requests/exceptions.py b/libs/requests/exceptions.py
index 3c262e36..57f7b82d 100644
--- a/libs/requests/exceptions.py
+++ b/libs/requests/exceptions.py
@@ -35,4 +35,7 @@ class MissingSchema(RequestException, ValueError):
"""The URL schema (e.g. http or https) is missing."""
class InvalidSchema(RequestException, ValueError):
- """See defaults.py for valid schemas."""
\ No newline at end of file
+ """See defaults.py for valid schemas."""
+
+class InvalidURL(RequestException, ValueError):
+ """ The URL provided was somehow invalid. """
diff --git a/libs/requests/hooks.py b/libs/requests/hooks.py
index 3560b89d..272abb73 100644
--- a/libs/requests/hooks.py
+++ b/libs/requests/hooks.py
@@ -12,6 +12,9 @@ Available hooks:
A dictionary of the arguments being sent to Request().
``pre_request``:
+ The Request object, directly after being created.
+
+``pre_send``:
The Request object, directly before being sent.
``post_request``:
@@ -25,8 +28,7 @@ Available hooks:
import traceback
-HOOKS = ('args', 'pre_request', 'post_request', 'response')
-
+HOOKS = ('args', 'pre_request', 'pre_send', 'post_request', 'response')
def dispatch_hook(key, hooks, hook_data):
"""Dispatches a hook dictionary on a given piece of data."""
@@ -41,7 +43,10 @@ def dispatch_hook(key, hooks, hook_data):
for hook in hooks:
try:
- hook_data = hook(hook_data) or hook_data
+ _hook_data = hook(hook_data)
+ if _hook_data is not None:
+ hook_data = _hook_data
+
except Exception:
traceback.print_exc()
diff --git a/libs/requests/models.py b/libs/requests/models.py
index 70e35036..fbf7fc6e 100644
--- a/libs/requests/models.py
+++ b/libs/requests/models.py
@@ -7,6 +7,7 @@ requests.models
This module contains the primary objects that power Requests.
"""
+import json
import os
from datetime import datetime
@@ -15,8 +16,8 @@ from .structures import CaseInsensitiveDict
from .status_codes import codes
from .auth import HTTPBasicAuth, HTTPProxyAuth
-from .packages.urllib3.response import HTTPResponse
-from .packages.urllib3.exceptions import MaxRetryError
+from .cookies import cookiejar_from_dict, extract_cookies_to_jar, get_cookie_header
+from .packages.urllib3.exceptions import MaxRetryError, LocationParseError
from .packages.urllib3.exceptions import SSLError as _SSLError
from .packages.urllib3.exceptions import HTTPError as _HTTPError
from .packages.urllib3 import connectionpool, poolmanager
@@ -24,22 +25,25 @@ from .packages.urllib3.filepost import encode_multipart_formdata
from .defaults import SCHEMAS
from .exceptions import (
ConnectionError, HTTPError, RequestException, Timeout, TooManyRedirects,
- URLRequired, SSLError, MissingSchema, InvalidSchema)
+ URLRequired, SSLError, MissingSchema, InvalidSchema, InvalidURL)
from .utils import (
get_encoding_from_headers, stream_untransfer, guess_filename, requote_uri,
- dict_from_string, stream_decode_response_unicode, get_netrc_auth)
+ stream_decode_response_unicode, get_netrc_auth, get_environ_proxies,
+ DEFAULT_CA_BUNDLE_PATH)
from .compat import (
- urlparse, urlunparse, urljoin, urlsplit, urlencode, str, bytes,
- SimpleCookie, is_py2)
+ cookielib, urlparse, urlunparse, urljoin, urlsplit, urlencode, str, bytes,
+ StringIO, is_py2)
# Import chardet if it is available.
try:
import chardet
+ # hush pyflakes
+ chardet
except ImportError:
- pass
+ chardet = None
REDIRECT_STATI = (codes.moved, codes.found, codes.other, codes.temporary_moved)
-
+CONTENT_CHUNK_SIZE = 10 * 1024
class Request(object):
"""The :class:`Request ` object. It carries out all functionality of
@@ -61,6 +65,7 @@ class Request(object):
proxies=None,
hooks=None,
config=None,
+ prefetch=False,
_poolmanager=None,
verify=None,
session=None,
@@ -80,17 +85,18 @@ class Request(object):
self.headers = dict(headers or [])
#: Dictionary of files to multipart upload (``{filename: content}``).
- self.files = files
+ self.files = None
#: HTTP Method to use.
self.method = method
- #: Dictionary or byte of request body data to attach to the
+ #: Dictionary, bytes or file stream of request body data to attach to the
#: :class:`Request `.
self.data = None
#: Dictionary or byte of querystring data to attach to the
- #: :class:`Request `.
+ #: :class:`Request `. The dictionary values can be lists for representing
+ #: multivalued query parameters.
self.params = None
#: True if :class:`Request ` is part of a redirect chain (disables history
@@ -106,13 +112,11 @@ class Request(object):
# If no proxies are given, allow configuration by environment variables
# HTTP_PROXY and HTTPS_PROXY.
if not self.proxies and self.config.get('trust_env'):
- if 'HTTP_PROXY' in os.environ:
- self.proxies['http'] = os.environ['HTTP_PROXY']
- if 'HTTPS_PROXY' in os.environ:
- self.proxies['https'] = os.environ['HTTPS_PROXY']
+ self.proxies = get_environ_proxies()
- self.data, self._enc_data = self._encode_params(data)
- self.params, self._enc_params = self._encode_params(params)
+ self.data = data
+ self.params = params
+ self.files = files
#: :class:`Response ` instance, containing
#: content and metadata of HTTP Response, once :attr:`sent `.
@@ -122,7 +126,10 @@ class Request(object):
self.auth = auth
#: CookieJar to attach to :class:`Request `.
- self.cookies = dict(cookies or [])
+ if isinstance(cookies, cookielib.CookieJar):
+ self.cookies = cookies
+ else:
+ self.cookies = cookiejar_from_dict(cookies)
#: True if Request has been sent.
self.sent = False
@@ -147,6 +154,9 @@ class Request(object):
#: SSL Certificate
self.cert = cert
+ #: Prefetch response content
+ self.prefetch = prefetch
+
if headers:
headers = CaseInsensitiveDict(self.headers)
else:
@@ -186,16 +196,16 @@ class Request(object):
# Set encoding.
response.encoding = get_encoding_from_headers(response.headers)
- # Start off with our local cookies.
- cookies = self.cookies or dict()
-
- # Add new cookies from the server.
- if 'set-cookie' in response.headers:
- cookie_header = response.headers['set-cookie']
- cookies = dict_from_string(cookie_header)
+ # Add new cookies from the server. Don't if configured not to
+ if self.config.get('store_cookies'):
+ extract_cookies_to_jar(self.cookies, self, resp)
# Save cookies in Response.
- response.cookies = cookies
+ response.cookies = self.cookies
+
+ # Save cookies in Session.
+ for cookie in self.cookies:
+ self.session.cookies.set_cookie(cookie)
# No exceptions were harmed in the making of this request.
response.error = getattr(resp, 'error', None)
@@ -213,8 +223,6 @@ class Request(object):
r = build(resp)
- self.cookies.update(r.cookies)
-
if r.status_code in REDIRECT_STATI and not self.redirect:
while (('location' in r.headers) and
@@ -232,6 +240,7 @@ class Request(object):
url = r.headers['location']
data = self.data
+ files = self.files
# Handle redirection without scheme (see: RFC 1808 Section 4)
if url.startswith('//'):
@@ -250,6 +259,7 @@ class Request(object):
if r.status_code is codes.see_other:
method = 'GET'
data = None
+ files = None
else:
method = self.method
@@ -259,10 +269,12 @@ class Request(object):
if r.status_code in (codes.moved, codes.found) and self.method == 'POST':
method = 'GET'
data = None
+ files = None
if (r.status_code == 303) and self.method != 'HEAD':
method = 'GET'
data = None
+ files = None
# Remove the cookie headers that were sent.
headers = self.headers
@@ -274,7 +286,7 @@ class Request(object):
request = Request(
url=url,
headers=headers,
- files=self.files,
+ files=files,
method=method,
params=self.session.params,
auth=self.auth,
@@ -292,41 +304,68 @@ class Request(object):
request.send()
r = request.response
- self.cookies.update(r.cookies)
r.history = history
self.response = r
self.response.request = self
- self.response.cookies.update(self.cookies)
@staticmethod
def _encode_params(data):
"""Encode parameters in a piece of data.
- If the data supplied is a dictionary, encodes each parameter in it, and
- returns a list of tuples containing the encoded parameters, and a urlencoded
- version of that.
-
- Otherwise, assumes the data is already encoded appropriately, and
- returns it twice.
+ Will successfully encode parameters when passed as a dict or a list of
+ 2-tuples. Order is retained if data is a list of 2-tuples but abritrary
+ if parameters are supplied as a dict.
"""
if isinstance(data, bytes):
- return data, data
+ return data
+ if isinstance(data, str):
+ return data
+ elif hasattr(data, 'read'):
+ return data
+ elif hasattr(data, '__iter__'):
+ try:
+ dict(data)
+ except ValueError:
+ raise ValueError('Unable to encode lists with elements that are not 2-tuples.')
- if hasattr(data, '__iter__') and not isinstance(data, str):
- data = dict(data)
-
- if hasattr(data, 'items'):
+ params = list(data.items() if isinstance(data, dict) else data)
result = []
- for k, vs in list(data.items()):
+ for k, vs in params:
for v in isinstance(vs, list) and vs or [vs]:
- result.append((k.encode('utf-8') if isinstance(k, str) else k,
- v.encode('utf-8') if isinstance(v, str) else v))
- return result, urlencode(result, doseq=True)
+ result.append(
+ (k.encode('utf-8') if isinstance(k, str) else k,
+ v.encode('utf-8') if isinstance(v, str) else v))
+ return urlencode(result, doseq=True)
else:
- return data, data
+ return data
+
+ def _encode_files(self, files):
+
+ if (not files) or isinstance(self.data, str):
+ return None
+
+ try:
+ fields = self.data.copy()
+ except AttributeError:
+ fields = dict(self.data)
+
+ for (k, v) in list(files.items()):
+ # support for explicit filename
+ if isinstance(v, (tuple, list)):
+ fn, fp = v
+ else:
+ fn = guess_filename(v) or k
+ fp = v
+ if isinstance(fp, (bytes, str)):
+ fp = StringIO(fp)
+ fields.update({k: (fn, fp.read())})
+
+ (body, content_type) = encode_multipart_formdata(fields)
+
+ return (body, content_type)
@property
def full_url(self):
@@ -351,7 +390,6 @@ class Request(object):
if not path:
path = '/'
-
if is_py2:
if isinstance(scheme, str):
scheme = scheme.encode('utf-8')
@@ -368,11 +406,12 @@ class Request(object):
url = (urlunparse([scheme, netloc, path, params, query, fragment]))
- if self._enc_params:
+ enc_params = self._encode_params(self.params)
+ if enc_params:
if urlparse(url).query:
- url = '%s&%s' % (url, self._enc_params)
+ url = '%s&%s' % (url, enc_params)
else:
- url = '%s?%s' % (url, self._enc_params)
+ url = '%s?%s' % (url, enc_params)
if self.config.get('encode_uri', True):
url = requote_uri(url)
@@ -407,10 +446,21 @@ class Request(object):
def register_hook(self, event, hook):
"""Properly register a hook."""
- return self.hooks[event].append(hook)
+ self.hooks[event].append(hook)
+
+ def deregister_hook(self, event, hook):
+ """Deregister a previously registered hook.
+ Returns True if the hook existed, False if not.
+ """
+
+ try:
+ self.hooks[event].remove(hook)
+ return True
+ except ValueError:
+ return False
def send(self, anyway=False, prefetch=False):
- """Sends the request. Returns True of successful, False if not.
+ """Sends the request. Returns True if successful, False if not.
If there was an HTTPError during transmission,
self.response.status_code will contain the HTTPError code.
@@ -423,6 +473,10 @@ class Request(object):
# Build the URL
url = self.full_url
+ # Pre-request hook.
+ r = dispatch_hook('pre_request', self.hooks, self)
+ self.__dict__.update(r.__dict__)
+
# Logging
if self.config.get('verbose'):
self.config.get('verbose').write('%s %s %s\n' % (
@@ -433,41 +487,6 @@ class Request(object):
body = None
content_type = None
- # Multi-part file uploads.
- if self.files:
- if not isinstance(self.data, str):
-
- try:
- fields = self.data.copy()
- except AttributeError:
- fields = dict(self.data)
-
- for (k, v) in list(self.files.items()):
- # support for explicit filename
- if isinstance(v, (tuple, list)):
- fn, fp = v
- else:
- fn = guess_filename(v) or k
- fp = v
- fields.update({k: (fn, fp.read())})
-
- (body, content_type) = encode_multipart_formdata(fields)
- else:
- pass
- # TODO: Conflict?
- else:
- if self.data:
-
- body = self._enc_data
- if isinstance(self.data, str):
- content_type = None
- else:
- content_type = 'application/x-www-form-urlencoded'
-
- # Add content-type if it wasn't explicitly provided.
- if (content_type) and (not 'content-type' in self.headers):
- self.headers['Content-Type'] = content_type
-
# Use .netrc auth if none was provided.
if not self.auth and self.config.get('trust_env'):
self.auth = get_netrc_auth(url)
@@ -483,10 +502,27 @@ class Request(object):
# Update self to reflect the auth changes.
self.__dict__.update(r.__dict__)
+ # Multi-part file uploads.
+ if self.files:
+ (body, content_type) = self._encode_files(self.files)
+ else:
+ if self.data:
+
+ body = self._encode_params(self.data)
+ if isinstance(self.data, str) or hasattr(self.data, 'read'):
+ content_type = None
+ else:
+ content_type = 'application/x-www-form-urlencoded'
+
+ # Add content-type if it wasn't explicitly provided.
+ if (content_type) and (not 'content-type' in self.headers):
+ self.headers['Content-Type'] = content_type
+
_p = urlparse(url)
+ no_proxy = filter(lambda x:x.strip(), self.proxies.get('no', '').split(','))
proxy = self.proxies.get(_p.scheme)
- if proxy:
+ if proxy and not any(map(_p.netloc.endswith, no_proxy)):
conn = poolmanager.proxy_from_url(proxy)
_proxy = urlparse(proxy)
if '@' in _proxy.netloc:
@@ -496,10 +532,14 @@ class Request(object):
self.__dict__.update(r.__dict__)
else:
# Check to see if keep_alive is allowed.
- if self.config.get('keep_alive'):
- conn = self._poolmanager.connection_from_url(url)
- else:
- conn = connectionpool.connection_from_url(url)
+ try:
+ if self.config.get('keep_alive'):
+ conn = self._poolmanager.connection_from_url(url)
+ else:
+ conn = connectionpool.connection_from_url(url)
+ self.headers['Connection'] = 'close'
+ except LocationParseError as e:
+ raise InvalidURL(e)
if url.startswith('https') and self.verify:
@@ -513,13 +553,15 @@ class Request(object):
if not cert_loc and self.config.get('trust_env'):
cert_loc = os.environ.get('REQUESTS_CA_BUNDLE')
- # Curl compatiblity.
+ # Curl compatibility.
if not cert_loc and self.config.get('trust_env'):
cert_loc = os.environ.get('CURL_CA_BUNDLE')
- # Use the awesome certifi list.
if not cert_loc:
- cert_loc = __import__('certifi').where()
+ cert_loc = DEFAULT_CA_BUNDLE_PATH
+
+ if not cert_loc:
+ raise Exception("Could not find a suitable SSL CA certificate bundle.")
conn.cert_reqs = 'CERT_REQUIRED'
conn.ca_certs = cert_loc
@@ -536,64 +578,43 @@ class Request(object):
if not self.sent or anyway:
- if self.cookies:
-
- # Skip if 'cookie' header is explicitly set.
- if 'cookie' not in self.headers:
-
- # Simple cookie with our dict.
- c = SimpleCookie()
- for (k, v) in list(self.cookies.items()):
- c[k] = v
-
- # Turn it into a header.
- cookie_header = c.output(header='', sep='; ').strip()
-
- # Attach Cookie header to request.
+ # Skip if 'cookie' header is explicitly set.
+ if 'cookie' not in self.headers:
+ cookie_header = get_cookie_header(self.cookies, self)
+ if cookie_header is not None:
self.headers['Cookie'] = cookie_header
- # Pre-request hook.
- r = dispatch_hook('pre_request', self.hooks, self)
+ # Pre-send hook.
+ r = dispatch_hook('pre_send', self.hooks, self)
self.__dict__.update(r.__dict__)
+ # catch urllib3 exceptions and throw Requests exceptions
try:
- # The inner try .. except re-raises certain exceptions as
- # internal exception types; the outer suppresses exceptions
- # when safe mode is set.
- try:
- # Send the request.
- r = conn.urlopen(
- method=self.method,
- url=self.path_url,
- body=body,
- headers=self.headers,
- redirect=False,
- assert_same_host=False,
- preload_content=False,
- decode_content=False,
- retries=self.config.get('max_retries', 0),
- timeout=self.timeout,
- )
- self.sent = True
+ # Send the request.
+ r = conn.urlopen(
+ method=self.method,
+ url=self.path_url,
+ body=body,
+ headers=self.headers,
+ redirect=False,
+ assert_same_host=False,
+ preload_content=False,
+ decode_content=False,
+ retries=self.config.get('max_retries', 0),
+ timeout=self.timeout,
+ )
+ self.sent = True
- except MaxRetryError as e:
- raise ConnectionError(e)
+ except MaxRetryError as e:
+ raise ConnectionError(e)
- except (_SSLError, _HTTPError) as e:
- if self.verify and isinstance(e, _SSLError):
- raise SSLError(e)
+ except (_SSLError, _HTTPError) as e:
+ if self.verify and isinstance(e, _SSLError):
+ raise SSLError(e)
- raise Timeout('Request timed out.')
-
- except RequestException as e:
- if self.config.get('safe_mode', False):
- # In safe mode, catch the exception and attach it to
- # a blank urllib3.HTTPResponse object.
- r = HTTPResponse()
- r.error = e
- else:
- raise
+ raise Timeout('Request timed out.')
+ # build_response can throw TooManyRedirects
self._build_response(r)
# Response manipulation hook.
@@ -604,7 +625,7 @@ class Request(object):
self.__dict__.update(r.__dict__)
# If prefetch is True, mark content as consumed.
- if prefetch:
+ if prefetch or self.prefetch:
# Save the response.
self.response.content
@@ -623,7 +644,7 @@ class Response(object):
def __init__(self):
- self._content = None
+ self._content = False
self._content_consumed = False
#: Integer Code of responded HTTP Status.
@@ -643,7 +664,7 @@ class Response(object):
#: Resulting :class:`HTTPError` of request, if one occurred.
self.error = None
- #: Encoding to decode with when accessing r.content.
+ #: Encoding to decode with when accessing r.text.
self.encoding = None
#: A list of :class:`Response ` objects from
@@ -654,8 +675,8 @@ class Response(object):
#: The :class:`Request ` that created the Response.
self.request = None
- #: A dictionary of Cookies the server sent back.
- self.cookies = {}
+ #: A CookieJar of Cookies the server sent back.
+ self.cookies = None
#: Dictionary of configurations for this request.
self.config = {}
@@ -679,7 +700,7 @@ class Response(object):
return False
return True
- def iter_content(self, chunk_size=10 * 1024, decode_unicode=False):
+ def iter_content(self, chunk_size=1, decode_unicode=False):
"""Iterates over the response data. This avoids reading the content
at once into memory for large responses. The chunk size is the number
of bytes it should read into memory. This is not necessarily the
@@ -721,7 +742,7 @@ class Response(object):
chunk = pending + chunk
lines = chunk.splitlines()
- if lines[-1][-1] == chunk[-1]:
+ if lines and lines[-1] and chunk and lines[-1][-1] == chunk[-1]:
pending = lines.pop()
else:
pending = None
@@ -736,7 +757,7 @@ class Response(object):
def content(self):
"""Content of the response, in bytes."""
- if self._content is None:
+ if self._content is False:
# Read the contents.
try:
if self._content_consumed:
@@ -746,7 +767,7 @@ class Response(object):
if self.status_code is 0:
self._content = None
else:
- self._content = bytes().join(self.iter_content()) or bytes()
+ self._content = bytes().join(self.iter_content(CONTENT_CHUNK_SIZE)) or bytes()
except AttributeError:
self._content = None
@@ -754,16 +775,6 @@ class Response(object):
self._content_consumed = True
return self._content
- def _detected_encoding(self):
- try:
- detected = chardet.detect(self.content) or {}
- return detected.get('encoding')
-
- # Trust that chardet isn't available or something went terribly wrong.
- except Exception:
- pass
-
-
@property
def text(self):
"""Content of the response, in unicode.
@@ -776,9 +787,13 @@ class Response(object):
content = None
encoding = self.encoding
- # Fallback to auto-detected encoding if chardet is available.
+ if not self.content:
+ return str('')
+
+ # Fallback to auto-detected encoding.
if self.encoding is None:
- encoding = self._detected_encoding()
+ if chardet is not None:
+ encoding = chardet.detect(self.content)['encoding']
# Decode unicode from given encoding.
try:
@@ -789,11 +804,17 @@ class Response(object):
#
# So we try blindly encoding.
content = str(self.content, errors='replace')
- except (UnicodeError, TypeError):
- pass
return content
+ @property
+ def json(self):
+ """Returns the json-encoded content of a request, if any."""
+ try:
+ return json.loads(self.text or self.content)
+ except ValueError:
+ return None
+
def raise_for_status(self, allow_redirects=True):
"""Raises stored :class:`HTTPError` or :class:`URLError`, if one occurred."""
@@ -810,7 +831,6 @@ class Response(object):
http_error.response = self
raise http_error
-
elif (self.status_code >= 500) and (self.status_code < 600):
http_error = HTTPError('%s Server Error' % self.status_code)
http_error.response = self
diff --git a/libs/requests/packages/oreos/__init__.py b/libs/requests/packages/oreos/__init__.py
deleted file mode 100644
index d01340f2..00000000
--- a/libs/requests/packages/oreos/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from .core import dict_from_string
\ No newline at end of file
diff --git a/libs/requests/packages/oreos/core.py b/libs/requests/packages/oreos/core.py
deleted file mode 100644
index 359d7447..00000000
--- a/libs/requests/packages/oreos/core.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# -*- coding: utf-8 -*-
-
-"""
-oreos.core
-~~~~~~~~~~
-
-The creamy white center.
-"""
-
-from .monkeys import SimpleCookie
-
-
-def dict_from_string(s):
- """Returns a MultiDict with Cookies."""
-
- cookies = dict()
-
- c = SimpleCookie()
- c.load(s)
-
- for k,v in c.items():
- cookies.update({k: v.value})
-
- return cookies
\ No newline at end of file
diff --git a/libs/requests/packages/oreos/monkeys.py b/libs/requests/packages/oreos/monkeys.py
deleted file mode 100644
index 2cf90163..00000000
--- a/libs/requests/packages/oreos/monkeys.py
+++ /dev/null
@@ -1,773 +0,0 @@
-# -*- coding: utf-8 -*-
-
-"""
-oreos.monkeys
-~~~~~~~~~~~~~
-
-Monkeypatches.
-"""
-#!/usr/bin/env python
-#
-
-####
-# Copyright 2000 by Timothy O'Malley
-#
-# All Rights Reserved
-#
-# Permission to use, copy, modify, and distribute this software
-# and its documentation for any purpose and without fee is hereby
-# granted, provided that the above copyright notice appear in all
-# copies and that both that copyright notice and this permission
-# notice appear in supporting documentation, and that the name of
-# Timothy O'Malley not be used in advertising or publicity
-# pertaining to distribution of the software without specific, written
-# prior permission.
-#
-# Timothy O'Malley DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
-# SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-# AND FITNESS, IN NO EVENT SHALL Timothy O'Malley BE LIABLE FOR
-# ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
-# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
-# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-# PERFORMANCE OF THIS SOFTWARE.
-#
-####
-#
-# Id: Cookie.py,v 2.29 2000/08/23 05:28:49 timo Exp
-# by Timothy O'Malley
-#
-# Cookie.py is a Python module for the handling of HTTP
-# cookies as a Python dictionary. See RFC 2109 for more
-# information on cookies.
-#
-# The original idea to treat Cookies as a dictionary came from
-# Dave Mitchell (davem@magnet.com) in 1995, when he released the
-# first version of nscookie.py.
-#
-####
-
-r"""
-Here's a sample session to show how to use this module.
-At the moment, this is the only documentation.
-
-The Basics
-----------
-
-Importing is easy..
-
- >>> import Cookie
-
-Most of the time you start by creating a cookie. Cookies come in
-three flavors, each with slightly different encoding semantics, but
-more on that later.
-
- >>> C = Cookie.SimpleCookie()
- >>> C = Cookie.SerialCookie()
- >>> C = Cookie.SmartCookie()
-
-[Note: Long-time users of Cookie.py will remember using
-Cookie.Cookie() to create an Cookie object. Although deprecated, it
-is still supported by the code. See the Backward Compatibility notes
-for more information.]
-
-Once you've created your Cookie, you can add values just as if it were
-a dictionary.
-
- >>> C = Cookie.SmartCookie()
- >>> C["fig"] = "newton"
- >>> C["sugar"] = "wafer"
- >>> C.output()
- 'Set-Cookie: fig=newton\r\nSet-Cookie: sugar=wafer'
-
-Notice that the printable representation of a Cookie is the
-appropriate format for a Set-Cookie: header. This is the
-default behavior. You can change the header and printed
-attributes by using the .output() function
-
- >>> C = Cookie.SmartCookie()
- >>> C["rocky"] = "road"
- >>> C["rocky"]["path"] = "/cookie"
- >>> print C.output(header="Cookie:")
- Cookie: rocky=road; Path=/cookie
- >>> print C.output(attrs=[], header="Cookie:")
- Cookie: rocky=road
-
-The load() method of a Cookie extracts cookies from a string. In a
-CGI script, you would use this method to extract the cookies from the
-HTTP_COOKIE environment variable.
-
- >>> C = Cookie.SmartCookie()
- >>> C.load("chips=ahoy; vienna=finger")
- >>> C.output()
- 'Set-Cookie: chips=ahoy\r\nSet-Cookie: vienna=finger'
-
-The load() method is darn-tootin smart about identifying cookies
-within a string. Escaped quotation marks, nested semicolons, and other
-such trickeries do not confuse it.
-
- >>> C = Cookie.SmartCookie()
- >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=\\012;";')
- >>> print C
- Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=\012;"
-
-Each element of the Cookie also supports all of the RFC 2109
-Cookie attributes. Here's an example which sets the Path
-attribute.
-
- >>> C = Cookie.SmartCookie()
- >>> C["oreo"] = "doublestuff"
- >>> C["oreo"]["path"] = "/"
- >>> print C
- Set-Cookie: oreo=doublestuff; Path=/
-
-Each dictionary element has a 'value' attribute, which gives you
-back the value associated with the key.
-
- >>> C = Cookie.SmartCookie()
- >>> C["twix"] = "none for you"
- >>> C["twix"].value
- 'none for you'
-
-
-A Bit More Advanced
--------------------
-
-As mentioned before, there are three different flavors of Cookie
-objects, each with different encoding/decoding semantics. This
-section briefly discusses the differences.
-
-SimpleCookie
-
-The SimpleCookie expects that all values should be standard strings.
-Just to be sure, SimpleCookie invokes the str() builtin to convert
-the value to a string, when the values are set dictionary-style.
-
- >>> C = Cookie.SimpleCookie()
- >>> C["number"] = 7
- >>> C["string"] = "seven"
- >>> C["number"].value
- '7'
- >>> C["string"].value
- 'seven'
- >>> C.output()
- 'Set-Cookie: number=7\r\nSet-Cookie: string=seven'
-
-
-SerialCookie
-
-The SerialCookie expects that all values should be serialized using
-cPickle (or pickle, if cPickle isn't available). As a result of
-serializing, SerialCookie can save almost any Python object to a
-value, and recover the exact same object when the cookie has been
-returned. (SerialCookie can yield some strange-looking cookie
-values, however.)
-
- >>> C = Cookie.SerialCookie()
- >>> C["number"] = 7
- >>> C["string"] = "seven"
- >>> C["number"].value
- 7
- >>> C["string"].value
- 'seven'
- >>> C.output()
- 'Set-Cookie: number="I7\\012."\r\nSet-Cookie: string="S\'seven\'\\012p1\\012."'
-
-Be warned, however, if SerialCookie cannot de-serialize a value (because
-it isn't a valid pickle'd object), IT WILL RAISE AN EXCEPTION.
-
-
-SmartCookie
-
-The SmartCookie combines aspects of each of the other two flavors.
-When setting a value in a dictionary-fashion, the SmartCookie will
-serialize (ala cPickle) the value *if and only if* it isn't a
-Python string. String objects are *not* serialized. Similarly,
-when the load() method parses out values, it attempts to de-serialize
-the value. If it fails, then it fallsback to treating the value
-as a string.
-
- >>> C = Cookie.SmartCookie()
- >>> C["number"] = 7
- >>> C["string"] = "seven"
- >>> C["number"].value
- 7
- >>> C["string"].value
- 'seven'
- >>> C.output()
- 'Set-Cookie: number="I7\\012."\r\nSet-Cookie: string=seven'
-
-
-Backwards Compatibility
------------------------
-
-In order to keep compatibilty with earlier versions of Cookie.py,
-it is still possible to use Cookie.Cookie() to create a Cookie. In
-fact, this simply returns a SmartCookie.
-
- >>> C = Cookie.Cookie()
- >>> print C.__class__.__name__
- SmartCookie
-
-
-Finis.
-""" #"
-# ^
-# |----helps out font-lock
-
-#
-# Import our required modules
-#
-import string
-
-try:
- from cPickle import dumps, loads
-except ImportError:
- from pickle import dumps, loads
-
-import re, warnings
-
-__all__ = ["CookieError","BaseCookie","SimpleCookie","SerialCookie",
- "SmartCookie","Cookie"]
-
-_nulljoin = ''.join
-_semispacejoin = '; '.join
-_spacejoin = ' '.join
-
-#
-# Define an exception visible to External modules
-#
-class CookieError(Exception):
- pass
-
-
-# These quoting routines conform to the RFC2109 specification, which in
-# turn references the character definitions from RFC2068. They provide
-# a two-way quoting algorithm. Any non-text character is translated
-# into a 4 character sequence: a forward-slash followed by the
-# three-digit octal equivalent of the character. Any '\' or '"' is
-# quoted with a preceeding '\' slash.
-#
-# These are taken from RFC2068 and RFC2109.
-# _RFC2965Forbidden is the list of forbidden chars we accept anyway
-# _LegalChars is the list of chars which don't require "'s
-# _Translator hash-table for fast quoting
-#
-_RFC2965Forbidden = "[]:{}="
-_LegalChars = ( string.ascii_letters + string.digits +
- "!#$%&'*+-.^_`|~_@" + _RFC2965Forbidden )
-_Translator = {
- '\000' : '\\000', '\001' : '\\001', '\002' : '\\002',
- '\003' : '\\003', '\004' : '\\004', '\005' : '\\005',
- '\006' : '\\006', '\007' : '\\007', '\010' : '\\010',
- '\011' : '\\011', '\012' : '\\012', '\013' : '\\013',
- '\014' : '\\014', '\015' : '\\015', '\016' : '\\016',
- '\017' : '\\017', '\020' : '\\020', '\021' : '\\021',
- '\022' : '\\022', '\023' : '\\023', '\024' : '\\024',
- '\025' : '\\025', '\026' : '\\026', '\027' : '\\027',
- '\030' : '\\030', '\031' : '\\031', '\032' : '\\032',
- '\033' : '\\033', '\034' : '\\034', '\035' : '\\035',
- '\036' : '\\036', '\037' : '\\037',
-
- # Because of the way browsers really handle cookies (as opposed
- # to what the RFC says) we also encode , and ;
-
- ',' : '\\054', ';' : '\\073',
-
- '"' : '\\"', '\\' : '\\\\',
-
- '\177' : '\\177', '\200' : '\\200', '\201' : '\\201',
- '\202' : '\\202', '\203' : '\\203', '\204' : '\\204',
- '\205' : '\\205', '\206' : '\\206', '\207' : '\\207',
- '\210' : '\\210', '\211' : '\\211', '\212' : '\\212',
- '\213' : '\\213', '\214' : '\\214', '\215' : '\\215',
- '\216' : '\\216', '\217' : '\\217', '\220' : '\\220',
- '\221' : '\\221', '\222' : '\\222', '\223' : '\\223',
- '\224' : '\\224', '\225' : '\\225', '\226' : '\\226',
- '\227' : '\\227', '\230' : '\\230', '\231' : '\\231',
- '\232' : '\\232', '\233' : '\\233', '\234' : '\\234',
- '\235' : '\\235', '\236' : '\\236', '\237' : '\\237',
- '\240' : '\\240', '\241' : '\\241', '\242' : '\\242',
- '\243' : '\\243', '\244' : '\\244', '\245' : '\\245',
- '\246' : '\\246', '\247' : '\\247', '\250' : '\\250',
- '\251' : '\\251', '\252' : '\\252', '\253' : '\\253',
- '\254' : '\\254', '\255' : '\\255', '\256' : '\\256',
- '\257' : '\\257', '\260' : '\\260', '\261' : '\\261',
- '\262' : '\\262', '\263' : '\\263', '\264' : '\\264',
- '\265' : '\\265', '\266' : '\\266', '\267' : '\\267',
- '\270' : '\\270', '\271' : '\\271', '\272' : '\\272',
- '\273' : '\\273', '\274' : '\\274', '\275' : '\\275',
- '\276' : '\\276', '\277' : '\\277', '\300' : '\\300',
- '\301' : '\\301', '\302' : '\\302', '\303' : '\\303',
- '\304' : '\\304', '\305' : '\\305', '\306' : '\\306',
- '\307' : '\\307', '\310' : '\\310', '\311' : '\\311',
- '\312' : '\\312', '\313' : '\\313', '\314' : '\\314',
- '\315' : '\\315', '\316' : '\\316', '\317' : '\\317',
- '\320' : '\\320', '\321' : '\\321', '\322' : '\\322',
- '\323' : '\\323', '\324' : '\\324', '\325' : '\\325',
- '\326' : '\\326', '\327' : '\\327', '\330' : '\\330',
- '\331' : '\\331', '\332' : '\\332', '\333' : '\\333',
- '\334' : '\\334', '\335' : '\\335', '\336' : '\\336',
- '\337' : '\\337', '\340' : '\\340', '\341' : '\\341',
- '\342' : '\\342', '\343' : '\\343', '\344' : '\\344',
- '\345' : '\\345', '\346' : '\\346', '\347' : '\\347',
- '\350' : '\\350', '\351' : '\\351', '\352' : '\\352',
- '\353' : '\\353', '\354' : '\\354', '\355' : '\\355',
- '\356' : '\\356', '\357' : '\\357', '\360' : '\\360',
- '\361' : '\\361', '\362' : '\\362', '\363' : '\\363',
- '\364' : '\\364', '\365' : '\\365', '\366' : '\\366',
- '\367' : '\\367', '\370' : '\\370', '\371' : '\\371',
- '\372' : '\\372', '\373' : '\\373', '\374' : '\\374',
- '\375' : '\\375', '\376' : '\\376', '\377' : '\\377'
- }
-
-_idmap = ''.join(chr(x) for x in range(256))
-
-def _quote(str, LegalChars=_LegalChars,
- idmap=_idmap, translate=string.translate):
- #
- # If the string does not need to be double-quoted,
- # then just return the string. Otherwise, surround
- # the string in doublequotes and precede quote (with a \)
- # special characters.
- #
- if "" == translate(str, idmap, LegalChars):
- return str
- else:
- return '"' + _nulljoin( map(_Translator.get, str, str) ) + '"'
-# end _quote
-
-
-_OctalPatt = re.compile(r"\\[0-3][0-7][0-7]")
-_QuotePatt = re.compile(r"[\\].")
-
-def _unquote(str):
- # If there aren't any doublequotes,
- # then there can't be any special characters. See RFC 2109.
- if len(str) < 2:
- return str
- if str[0] != '"' or str[-1] != '"':
- return str
-
- # We have to assume that we must decode this string.
- # Down to work.
-
- # Remove the "s
- str = str[1:-1]
-
- # Check for special sequences. Examples:
- # \012 --> \n
- # \" --> "
- #
- i = 0
- n = len(str)
- res = []
- while 0 <= i < n:
- Omatch = _OctalPatt.search(str, i)
- Qmatch = _QuotePatt.search(str, i)
- if not Omatch and not Qmatch: # Neither matched
- res.append(str[i:])
- break
- # else:
- j = k = -1
- if Omatch: j = Omatch.start(0)
- if Qmatch: k = Qmatch.start(0)
- if Qmatch and ( not Omatch or k < j ): # QuotePatt matched
- res.append(str[i:k])
- res.append(str[k+1])
- i = k+2
- else: # OctalPatt matched
- res.append(str[i:j])
- res.append( chr( int(str[j+1:j+4], 8) ) )
- i = j+4
- return _nulljoin(res)
-# end _unquote
-
-# The _getdate() routine is used to set the expiration time in
-# the cookie's HTTP header. By default, _getdate() returns the
-# current time in the appropriate "expires" format for a
-# Set-Cookie header. The one optional argument is an offset from
-# now, in seconds. For example, an offset of -3600 means "one hour ago".
-# The offset may be a floating point number.
-#
-
-_weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
-
-_monthname = [None,
- 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
- 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
-
-def _getdate(future=0, weekdayname=_weekdayname, monthname=_monthname):
- from time import gmtime, time
- now = time()
- year, month, day, hh, mm, ss, wd, y, z = gmtime(now + future)
- return "%s, %02d-%3s-%4d %02d:%02d:%02d GMT" % \
- (weekdayname[wd], day, monthname[month], year, hh, mm, ss)
-
-
-#
-# A class to hold ONE key,value pair.
-# In a cookie, each such pair may have several attributes.
-# so this class is used to keep the attributes associated
-# with the appropriate key,value pair.
-# This class also includes a coded_value attribute, which
-# is used to hold the network representation of the
-# value. This is most useful when Python objects are
-# pickled for network transit.
-#
-
-class Morsel(dict):
- # RFC 2109 lists these attributes as reserved:
- # path comment domain
- # max-age secure version
- #
- # For historical reasons, these attributes are also reserved:
- # expires
- #
- # This is an extension from Microsoft:
- # httponly
- #
- # This dictionary provides a mapping from the lowercase
- # variant on the left to the appropriate traditional
- # formatting on the right.
- _reserved = { "expires" : "expires",
- "path" : "Path",
- "comment" : "Comment",
- "domain" : "Domain",
- "max-age" : "Max-Age",
- "secure" : "secure",
- "httponly" : "httponly",
- "version" : "Version",
- }
-
- def __init__(self):
- # Set defaults
- self.key = self.value = self.coded_value = None
-
- # Set default attributes
- for K in self._reserved:
- dict.__setitem__(self, K, "")
- # end __init__
-
- def __setitem__(self, K, V):
- K = K.lower()
- if not K in self._reserved:
- raise CookieError("Invalid Attribute %s" % K)
- dict.__setitem__(self, K, V)
- # end __setitem__
-
- def isReservedKey(self, K):
- return K.lower() in self._reserved
- # end isReservedKey
-
- def set(self, key, val, coded_val,
- LegalChars=_LegalChars,
- idmap=_idmap, translate=string.translate):
- # First we verify that the key isn't a reserved word
- # Second we make sure it only contains legal characters
- if key.lower() in self._reserved:
- raise CookieError("Attempt to set a reserved key: %s" % key)
- if "" != translate(key, idmap, LegalChars):
- raise CookieError("Illegal key value: %s" % key)
-
- # It's a good key, so save it.
- self.key = key
- self.value = val
- self.coded_value = coded_val
- # end set
-
- def output(self, attrs=None, header = "Set-Cookie:"):
- return "%s %s" % ( header, self.OutputString(attrs) )
-
- __str__ = output
-
- def __repr__(self):
- return '<%s: %s=%s>' % (self.__class__.__name__,
- self.key, repr(self.value) )
-
- def js_output(self, attrs=None):
- # Print javascript
- return """
-
- """ % ( self.OutputString(attrs).replace('"',r'\"'), )
- # end js_output()
-
- def OutputString(self, attrs=None):
- # Build up our result
- #
- result = []
- RA = result.append
-
- # First, the key=value pair
- RA("%s=%s" % (self.key, self.coded_value))
-
- # Now add any defined attributes
- if attrs is None:
- attrs = self._reserved
- items = self.items()
- items.sort()
- for K,V in items:
- if V == "": continue
- if K not in attrs: continue
- if K == "expires" and type(V) == type(1):
- RA("%s=%s" % (self._reserved[K], _getdate(V)))
- elif K == "max-age" and type(V) == type(1):
- RA("%s=%d" % (self._reserved[K], V))
- elif K == "secure":
- RA(str(self._reserved[K]))
- elif K == "httponly":
- RA(str(self._reserved[K]))
- else:
- RA("%s=%s" % (self._reserved[K], V))
-
- # Return the result
- return _semispacejoin(result)
- # end OutputString
-# end Morsel class
-
-
-
-#
-# Pattern for finding cookie
-#
-# This used to be strict parsing based on the RFC2109 and RFC2068
-# specifications. I have since discovered that MSIE 3.0x doesn't
-# follow the character rules outlined in those specs. As a
-# result, the parsing rules here are less strict.
-#
-
-_LegalCharsPatt = r"[\w\d!#%&'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\=\[\]\_]"
-
-_CookiePattern = re.compile(
- r"(?x)" # This is a Verbose pattern
- r"(?P" # Start of group 'key'
- ""+ _LegalCharsPatt +"+?" # Any word of at least one letter, nongreedy
- r")" # End of group 'key'
- r"\s*=\s*" # Equal Sign
- r"(?P" # Start of group 'val'
- r'"(?:[^\\"]|\\.)*"' # Any doublequoted string
- r"|" # or
- r"\w{3},\s[\w\d-]{9,11}\s[\d:]{8}\sGMT" # Special case for "expires" attr
- r"|" # or
- ""+ _LegalCharsPatt +"*" # Any word or empty string
- r")" # End of group 'val'
- r"\s*;?" # Probably ending in a semi-colon
- )
-
-
-# At long last, here is the cookie class.
-# Using this class is almost just like using a dictionary.
-# See this module's docstring for example usage.
-#
-class BaseCookie(dict):
- # A container class for a set of Morsels
- #
-
- def value_decode(self, val):
- """real_value, coded_value = value_decode(STRING)
- Called prior to setting a cookie's value from the network
- representation. The VALUE is the value read from HTTP
- header.
- Override this function to modify the behavior of cookies.
- """
- return val, val
- # end value_encode
-
- def value_encode(self, val):
- """real_value, coded_value = value_encode(VALUE)
- Called prior to setting a cookie's value from the dictionary
- representation. The VALUE is the value being assigned.
- Override this function to modify the behavior of cookies.
- """
- strval = str(val)
- return strval, strval
- # end value_encode
-
- def __init__(self, input=None):
- if input: self.load(input)
- # end __init__
-
- def __set(self, key, real_value, coded_value):
- """Private method for setting a cookie's value"""
- M = self.get(key, Morsel())
- M.set(key, real_value, coded_value)
- dict.__setitem__(self, key, M)
- # end __set
-
- def __setitem__(self, key, value):
- """Dictionary style assignment."""
- rval, cval = self.value_encode(value)
- self.__set(key, rval, cval)
- # end __setitem__
-
- def output(self, attrs=None, header="Set-Cookie:", sep="\015\012"):
- """Return a string suitable for HTTP."""
- result = []
- items = self.items()
- items.sort()
- for K,V in items:
- result.append( V.output(attrs, header) )
- return sep.join(result)
- # end output
-
- __str__ = output
-
- def __repr__(self):
- L = []
- items = self.items()
- items.sort()
- for K,V in items:
- L.append( '%s=%s' % (K,repr(V.value) ) )
- return '<%s: %s>' % (self.__class__.__name__, _spacejoin(L))
-
- def js_output(self, attrs=None):
- """Return a string suitable for JavaScript."""
- result = []
- items = self.items()
- items.sort()
- for K,V in items:
- result.append( V.js_output(attrs) )
- return _nulljoin(result)
- # end js_output
-
- def load(self, rawdata):
- """Load cookies from a string (presumably HTTP_COOKIE) or
- from a dictionary. Loading cookies from a dictionary 'd'
- is equivalent to calling:
- map(Cookie.__setitem__, d.keys(), d.values())
- """
- if type(rawdata) == type(""):
- self.__ParseString(rawdata)
- else:
- # self.update() wouldn't call our custom __setitem__
- for k, v in rawdata.items():
- self[k] = v
- return
- # end load()
-
- def __ParseString(self, str, patt=_CookiePattern):
- i = 0 # Our starting point
- n = len(str) # Length of string
- M = None # current morsel
-
- while 0 <= i < n:
- # Start looking for a cookie
- match = patt.search(str, i)
- if not match: break # No more cookies
-
- K,V = match.group("key"), match.group("val")
- i = match.end(0)
-
- # Parse the key, value in case it's metainfo
- if K[0] == "$":
- # We ignore attributes which pertain to the cookie
- # mechanism as a whole. See RFC 2109.
- # (Does anyone care?)
- if M:
- M[ K[1:] ] = V
- elif K.lower() in Morsel._reserved:
- if M:
- M[ K ] = _unquote(V)
- else:
- rval, cval = self.value_decode(V)
- self.__set(K, rval, cval)
- M = self[K]
- # end __ParseString
-# end BaseCookie class
-
-class SimpleCookie(BaseCookie):
- """SimpleCookie
- SimpleCookie supports strings as cookie values. When setting
- the value using the dictionary assignment notation, SimpleCookie
- calls the builtin str() to convert the value to a string. Values
- received from HTTP are kept as strings.
- """
- def value_decode(self, val):
- return _unquote( val ), val
- def value_encode(self, val):
- strval = str(val)
- return strval, _quote( strval )
-# end SimpleCookie
-
-class SerialCookie(BaseCookie):
- """SerialCookie
- SerialCookie supports arbitrary objects as cookie values. All
- values are serialized (using cPickle) before being sent to the
- client. All incoming values are assumed to be valid Pickle
- representations. IF AN INCOMING VALUE IS NOT IN A VALID PICKLE
- FORMAT, THEN AN EXCEPTION WILL BE RAISED.
-
- Note: Large cookie values add overhead because they must be
- retransmitted on every HTTP transaction.
-
- Note: HTTP has a 2k limit on the size of a cookie. This class
- does not check for this limit, so be careful!!!
- """
- def __init__(self, input=None):
- warnings.warn("SerialCookie class is insecure; do not use it",
- DeprecationWarning)
- BaseCookie.__init__(self, input)
- # end __init__
- def value_decode(self, val):
- # This could raise an exception!
- return loads( _unquote(val) ), val
- def value_encode(self, val):
- return val, _quote( dumps(val) )
-# end SerialCookie
-
-class SmartCookie(BaseCookie):
- """SmartCookie
- SmartCookie supports arbitrary objects as cookie values. If the
- object is a string, then it is quoted. If the object is not a
- string, however, then SmartCookie will use cPickle to serialize
- the object into a string representation.
-
- Note: Large cookie values add overhead because they must be
- retransmitted on every HTTP transaction.
-
- Note: HTTP has a 2k limit on the size of a cookie. This class
- does not check for this limit, so be careful!!!
- """
- def __init__(self, input=None):
- warnings.warn("Cookie/SmartCookie class is insecure; do not use it",
- DeprecationWarning)
- BaseCookie.__init__(self, input)
- # end __init__
- def value_decode(self, val):
- strval = _unquote(val)
- try:
- return loads(strval), val
- except:
- return strval, val
- def value_encode(self, val):
- if type(val) == type(""):
- return val, _quote(val)
- else:
- return val, _quote( dumps(val) )
-# end SmartCookie
-
-
-###########################################################
-# Backwards Compatibility: Don't break any existing code!
-
-# We provide Cookie() as an alias for SmartCookie()
-Cookie = SmartCookie
-
-#
-###########################################################
-
-def _test():
- import doctest, Cookie
- return doctest.testmod(Cookie)
-
-if __name__ == "__main__":
- _test()
-
-
-#Local Variables:
-#tab-width: 4
-#end:
diff --git a/libs/requests/packages/oreos/structures.py b/libs/requests/packages/oreos/structures.py
deleted file mode 100644
index 83292777..00000000
--- a/libs/requests/packages/oreos/structures.py
+++ /dev/null
@@ -1,399 +0,0 @@
-# -*- coding: utf-8 -*-
-
-"""
-oreos.structures
-~~~~~~~~~~~~~~~~
-
-The plastic blue packaging.
-
-This is mostly directly stolen from mitsuhiko/werkzeug.
-"""
-
-__all__ = ('MultiDict',)
-
-class _Missing(object):
-
- def __repr__(self):
- return 'no value'
-
- def __reduce__(self):
- return '_missing'
-
-_missing = _Missing()
-
-
-
-def iter_multi_items(mapping):
- """Iterates over the items of a mapping yielding keys and values
- without dropping any from more complex structures.
- """
- if isinstance(mapping, MultiDict):
- for item in mapping.iteritems(multi=True):
- yield item
- elif isinstance(mapping, dict):
- for key, value in mapping.iteritems():
- if isinstance(value, (tuple, list)):
- for value in value:
- yield key, value
- else:
- yield key, value
- else:
- for item in mapping:
- yield item
-
-
-
-class TypeConversionDict(dict):
- """Works like a regular dict but the :meth:`get` method can perform
- type conversions. :class:`MultiDict` and :class:`CombinedMultiDict`
- are subclasses of this class and provide the same feature.
-
- .. versionadded:: 0.5
- """
-
- def get(self, key, default=None, type=None):
- """Return the default value if the requested data doesn't exist.
- If `type` is provided and is a callable it should convert the value,
- return it or raise a :exc:`ValueError` if that is not possible. In
- this case the function will return the default as if the value was not
- found:
-
- >>> d = TypeConversionDict(foo='42', bar='blub')
- >>> d.get('foo', type=int)
- 42
- >>> d.get('bar', -1, type=int)
- -1
-
- :param key: The key to be looked up.
- :param default: The default value to be returned if the key can't
- be looked up. If not further specified `None` is
- returned.
- :param type: A callable that is used to cast the value in the
- :class:`MultiDict`. If a :exc:`ValueError` is raised
- by this callable the default value is returned.
- """
- try:
- rv = self[key]
- if type is not None:
- rv = type(rv)
- except (KeyError, ValueError):
- rv = default
- return rv
-
-
-class MultiDict(TypeConversionDict):
- """A :class:`MultiDict` is a dictionary subclass customized to deal with
- multiple values for the same key which is for example used by the parsing
- functions in the wrappers. This is necessary because some HTML form
- elements pass multiple values for the same key.
-
- :class:`MultiDict` implements all standard dictionary methods.
- Internally, it saves all values for a key as a list, but the standard dict
- access methods will only return the first value for a key. If you want to
- gain access to the other values, too, you have to use the `list` methods as
- explained below.
-
- Basic Usage:
-
- >>> d = MultiDict([('a', 'b'), ('a', 'c')])
- >>> d
- MultiDict([('a', 'b'), ('a', 'c')])
- >>> d['a']
- 'b'
- >>> d.getlist('a')
- ['b', 'c']
- >>> 'a' in d
- True
-
- It behaves like a normal dict thus all dict functions will only return the
- first value when multiple values for one key are found.
-
- From Werkzeug 0.3 onwards, the `KeyError` raised by this class is also a
- subclass of the :exc:`~exceptions.BadRequest` HTTP exception and will
- render a page for a ``400 BAD REQUEST`` if caught in a catch-all for HTTP
- exceptions.
-
- A :class:`MultiDict` can be constructed from an iterable of
- ``(key, value)`` tuples, a dict, a :class:`MultiDict` or from Werkzeug 0.2
- onwards some keyword parameters.
-
- :param mapping: the initial value for the :class:`MultiDict`. Either a
- regular dict, an iterable of ``(key, value)`` tuples
- or `None`.
- """
-
- def __init__(self, mapping=None):
- if isinstance(mapping, MultiDict):
- dict.__init__(self, ((k, l[:]) for k, l in mapping.iterlists()))
- elif isinstance(mapping, dict):
- tmp = {}
- for key, value in mapping.iteritems():
- if isinstance(value, (tuple, list)):
- value = list(value)
- else:
- value = [value]
- tmp[key] = value
- dict.__init__(self, tmp)
- else:
- tmp = {}
- for key, value in mapping or ():
- tmp.setdefault(key, []).append(value)
- dict.__init__(self, tmp)
-
- def __getstate__(self):
- return dict(self.lists())
-
- def __setstate__(self, value):
- dict.clear(self)
- dict.update(self, value)
-
- def __iter__(self):
- return self.iterkeys()
-
- def __getitem__(self, key):
- """Return the first data value for this key;
- raises KeyError if not found.
-
- :param key: The key to be looked up.
- :raise KeyError: if the key does not exist.
- """
- if key in self:
- return dict.__getitem__(self, key)[0]
- raise KeyError(key)
-
- def __setitem__(self, key, value):
- """Like :meth:`add` but removes an existing key first.
-
- :param key: the key for the value.
- :param value: the value to set.
- """
- dict.__setitem__(self, key, [value])
-
- def add(self, key, value):
- """Adds a new value for the key.
-
- .. versionadded:: 0.6
-
- :param key: the key for the value.
- :param value: the value to add.
- """
- dict.setdefault(self, key, []).append(value)
-
- def getlist(self, key, type=None):
- """Return the list of items for a given key. If that key is not in the
- `MultiDict`, the return value will be an empty list. Just as `get`
- `getlist` accepts a `type` parameter. All items will be converted
- with the callable defined there.
-
- :param key: The key to be looked up.
- :param type: A callable that is used to cast the value in the
- :class:`MultiDict`. If a :exc:`ValueError` is raised
- by this callable the value will be removed from the list.
- :return: a :class:`list` of all the values for the key.
- """
- try:
- rv = dict.__getitem__(self, key)
- except KeyError:
- return []
- if type is None:
- return list(rv)
- result = []
- for item in rv:
- try:
- result.append(type(item))
- except ValueError:
- pass
- return result
-
- def setlist(self, key, new_list):
- """Remove the old values for a key and add new ones. Note that the list
- you pass the values in will be shallow-copied before it is inserted in
- the dictionary.
-
- >>> d = MultiDict()
- >>> d.setlist('foo', ['1', '2'])
- >>> d['foo']
- '1'
- >>> d.getlist('foo')
- ['1', '2']
-
- :param key: The key for which the values are set.
- :param new_list: An iterable with the new values for the key. Old values
- are removed first.
- """
- dict.__setitem__(self, key, list(new_list))
-
- def setdefault(self, key, default=None):
- """Returns the value for the key if it is in the dict, otherwise it
- returns `default` and sets that value for `key`.
-
- :param key: The key to be looked up.
- :param default: The default value to be returned if the key is not
- in the dict. If not further specified it's `None`.
- """
- if key not in self:
- self[key] = default
- else:
- default = self[key]
- return default
-
- def setlistdefault(self, key, default_list=None):
- """Like `setdefault` but sets multiple values. The list returned
- is not a copy, but the list that is actually used internally. This
- means that you can put new values into the dict by appending items
- to the list:
-
- >>> d = MultiDict({"foo": 1})
- >>> d.setlistdefault("foo").extend([2, 3])
- >>> d.getlist("foo")
- [1, 2, 3]
-
- :param key: The key to be looked up.
- :param default: An iterable of default values. It is either copied
- (in case it was a list) or converted into a list
- before returned.
- :return: a :class:`list`
- """
- if key not in self:
- default_list = list(default_list or ())
- dict.__setitem__(self, key, default_list)
- else:
- default_list = dict.__getitem__(self, key)
- return default_list
-
- def items(self, multi=False):
- """Return a list of ``(key, value)`` pairs.
-
- :param multi: If set to `True` the list returned will have a
- pair for each value of each key. Otherwise it
- will only contain pairs for the first value of
- each key.
-
- :return: a :class:`list`
- """
- return list(self.iteritems(multi))
-
- def lists(self):
- """Return a list of ``(key, values)`` pairs, where values is the list of
- all values associated with the key.
-
- :return: a :class:`list`
- """
- return list(self.iterlists())
-
- def values(self):
- """Returns a list of the first value on every key's value list.
-
- :return: a :class:`list`.
- """
- return [self[key] for key in self.iterkeys()]
-
- def listvalues(self):
- """Return a list of all values associated with a key. Zipping
- :meth:`keys` and this is the same as calling :meth:`lists`:
-
- >>> d = MultiDict({"foo": [1, 2, 3]})
- >>> zip(d.keys(), d.listvalues()) == d.lists()
- True
-
- :return: a :class:`list`
- """
- return list(self.iterlistvalues())
-
- def iteritems(self, multi=False):
- """Like :meth:`items` but returns an iterator."""
- for key, values in dict.iteritems(self):
- if multi:
- for value in values:
- yield key, value
- else:
- yield key, values[0]
-
- def iterlists(self):
- """Like :meth:`items` but returns an iterator."""
- for key, values in dict.iteritems(self):
- yield key, list(values)
-
- def itervalues(self):
- """Like :meth:`values` but returns an iterator."""
- for values in dict.itervalues(self):
- yield values[0]
-
- def iterlistvalues(self):
- """Like :meth:`listvalues` but returns an iterator."""
- return dict.itervalues(self)
-
- def copy(self):
- """Return a shallow copy of this object."""
- return self.__class__(self)
-
- def to_dict(self, flat=True):
- """Return the contents as regular dict. If `flat` is `True` the
- returned dict will only have the first item present, if `flat` is
- `False` all values will be returned as lists.
-
- :param flat: If set to `False` the dict returned will have lists
- with all the values in it. Otherwise it will only
- contain the first value for each key.
- :return: a :class:`dict`
- """
- if flat:
- return dict(self.iteritems())
- return dict(self.lists())
-
- def update(self, other_dict):
- """update() extends rather than replaces existing key lists."""
- for key, value in iter_multi_items(other_dict):
- MultiDict.add(self, key, value)
-
- def pop(self, key, default=_missing):
- """Pop the first item for a list on the dict. Afterwards the
- key is removed from the dict, so additional values are discarded:
-
- >>> d = MultiDict({"foo": [1, 2, 3]})
- >>> d.pop("foo")
- 1
- >>> "foo" in d
- False
-
- :param key: the key to pop.
- :param default: if provided the value to return if the key was
- not in the dictionary.
- """
- try:
- return dict.pop(self, key)[0]
- except KeyError as e:
- if default is not _missing:
- return default
- raise KeyError(str(e))
-
- def popitem(self):
- """Pop an item from the dict."""
- try:
- item = dict.popitem(self)
- return (item[0], item[1][0])
- except KeyError as e:
- raise KeyError(str(e))
-
- def poplist(self, key):
- """Pop the list for a key from the dict. If the key is not in the dict
- an empty list is returned.
-
- .. versionchanged:: 0.5
- If the key does no longer exist a list is returned instead of
- raising an error.
- """
- return dict.pop(self, key, [])
-
- def popitemlist(self):
- """Pop a ``(key, list)`` tuple from the dict."""
- try:
- return dict.popitem(self)
- except KeyError as e:
- raise KeyError(str(e))
-
- def __copy__(self):
- return self.copy()
-
- def __repr__(self):
- return '%s(%r)' % (self.__class__.__name__, self.items(multi=True))
diff --git a/libs/requests/packages/urllib3/__init__.py b/libs/requests/packages/urllib3/__init__.py
index 2d6fecec..81e76f50 100644
--- a/libs/requests/packages/urllib3/__init__.py
+++ b/libs/requests/packages/urllib3/__init__.py
@@ -10,7 +10,7 @@ urllib3 - Thread-safe connection pooling and re-using.
__author__ = 'Andrey Petrov (andrey.petrov@shazow.net)'
__license__ = 'MIT'
-__version__ = '1.3'
+__version__ = 'dev'
from .connectionpool import (
diff --git a/libs/requests/packages/urllib3/connectionpool.py b/libs/requests/packages/urllib3/connectionpool.py
index c3cb3b12..336aa773 100644
--- a/libs/requests/packages/urllib3/connectionpool.py
+++ b/libs/requests/packages/urllib3/connectionpool.py
@@ -260,10 +260,11 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
httplib_response = conn.getresponse()
- log.debug("\"%s %s %s\" %s %s" %
- (method, url,
- conn._http_vsn_str, # pylint: disable-msg=W0212
- httplib_response.status, httplib_response.length))
+ # AppEngine doesn't have a version attr.
+ http_version = getattr(conn, '_http_vsn_str', 'HTTP/?'),
+ log.debug("\"%s %s %s\" %s %s" % (method, url, http_version,
+ httplib_response.status,
+ httplib_response.length))
return httplib_response
diff --git a/libs/requests/safe_mode.py b/libs/requests/safe_mode.py
new file mode 100644
index 00000000..619d368c
--- /dev/null
+++ b/libs/requests/safe_mode.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+
+"""
+requests.safe_mode
+~~~~~~~~~~~~
+
+This module contains a decorator that implements safe_mode.
+
+:copyright: (c) 2012 by Kenneth Reitz.
+:license: ISC, see LICENSE for more details.
+
+"""
+
+from .models import Response
+from .packages.urllib3.response import HTTPResponse
+from .exceptions import RequestException, ConnectionError, HTTPError
+import socket
+
+def catch_exceptions_if_in_safe_mode(function):
+ """New implementation of safe_mode. We catch all exceptions at the API level
+ and then return a blank Response object with the error field filled. This decorator
+ wraps request() in api.py.
+ """
+
+ def wrapped(method, url, **kwargs):
+ # if save_mode, we catch exceptions and fill error field
+ if (kwargs.get('config') and kwargs.get('config').get('safe_mode')) or (kwargs.get('session')
+ and kwargs.get('session').config.get('safe_mode')):
+ try:
+ return function(method, url, **kwargs)
+ except (RequestException, ConnectionError, HTTPError, socket.timeout) as e:
+ r = Response()
+ r.error = e
+ r.raw = HTTPResponse() # otherwise, tests fail
+ r.status_code = 0 # with this status_code, content returns None
+ return r
+ return function(method, url, **kwargs)
+ return wrapped
diff --git a/libs/requests/sessions.py b/libs/requests/sessions.py
index 94c94bf9..3113c787 100644
--- a/libs/requests/sessions.py
+++ b/libs/requests/sessions.py
@@ -9,13 +9,15 @@ requests (cookies, auth, proxies).
"""
+from copy import deepcopy
+from .compat import cookielib
+from .cookies import cookiejar_from_dict, remove_cookie_by_name
from .defaults import defaults
from .models import Request
from .hooks import dispatch_hook
from .utils import header_expand
from .packages.urllib3.poolmanager import PoolManager
-
def merge_kwargs(local_kwarg, default_kwarg):
"""Merges kwarg dictionaries.
@@ -52,7 +54,7 @@ class Session(object):
__attrs__ = [
'headers', 'cookies', 'auth', 'timeout', 'proxies', 'hooks',
- 'params', 'config', 'verify', 'cert']
+ 'params', 'config', 'verify', 'cert', 'prefetch']
def __init__(self,
@@ -69,7 +71,6 @@ class Session(object):
cert=None):
self.headers = headers or {}
- self.cookies = cookies or {}
self.auth = auth
self.timeout = timeout
self.proxies = proxies or {}
@@ -81,16 +82,15 @@ class Session(object):
self.cert = cert
for (k, v) in list(defaults.items()):
- self.config.setdefault(k, v)
+ self.config.setdefault(k, deepcopy(v))
self.init_poolmanager()
# Set up a CookieJar to be used by default
- self.cookies = {}
-
- # Add passed cookies in.
- if cookies is not None:
- self.cookies.update(cookies)
+ if isinstance(cookies, cookielib.CookieJar):
+ self.cookies = cookies
+ else:
+ self.cookies = cookiejar_from_dict(cookies)
def init_poolmanager(self):
self.poolmanager = PoolManager(
@@ -134,12 +134,12 @@ class Session(object):
:param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
:param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`.
:param files: (optional) Dictionary of 'filename': file-like-objects for multipart encoding upload.
- :param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth.
+ :param auth: (optional) Auth tuple or callable to enable Basic/Digest/Custom HTTP Auth.
:param timeout: (optional) Float describing the timeout of the request.
:param allow_redirects: (optional) Boolean. Set to True by default.
:param proxies: (optional) Dictionary mapping protocol to the URL of the proxy.
:param return_response: (optional) If False, an un-sent Request object will returned.
- :param config: (optional) A configuration dictionary.
+ :param config: (optional) A configuration dictionary. See ``request.defaults`` for allowed keys and their default values.
:param prefetch: (optional) if ``True``, the response content will be immediately downloaded.
:param verify: (optional) if ``True``, the SSL cert will be verified. A CA_BUNDLE path can also be provided.
:param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
@@ -148,7 +148,6 @@ class Session(object):
method = str(method).upper()
# Default empty dicts for dict params.
- cookies = {} if cookies is None else cookies
data = {} if data is None else data
files = {} if files is None else files
headers = {} if headers is None else headers
@@ -179,16 +178,39 @@ class Session(object):
allow_redirects=allow_redirects,
proxies=proxies,
config=config,
+ prefetch=prefetch,
verify=verify,
cert=cert,
_poolmanager=self.poolmanager
)
+ # merge session cookies into passed-in ones
+ dead_cookies = None
+ # passed-in cookies must become a CookieJar:
+ if not isinstance(cookies, cookielib.CookieJar):
+ args['cookies'] = cookiejar_from_dict(cookies)
+ # support unsetting cookies that have been passed in with None values
+ # this is only meaningful when `cookies` is a dict ---
+ # for a real CookieJar, the client should use session.cookies.clear()
+ if cookies is not None:
+ dead_cookies = [name for name in cookies if cookies[name] is None]
+ # merge the session's cookies into the passed-in cookies:
+ for cookie in self.cookies:
+ args['cookies'].set_cookie(cookie)
+ # remove the unset cookies from the jar we'll be using with the current request
+ # (but not from the session's own store of cookies):
+ if dead_cookies is not None:
+ for name in dead_cookies:
+ remove_cookie_by_name(args['cookies'], name)
+
# Merge local kwargs with session kwargs.
for attr in self.__attrs__:
+ # we already merged cookies:
+ if attr == 'cookies':
+ continue
+
session_val = getattr(self, attr, None)
local_val = args.get(attr)
-
args[attr] = merge_kwargs(local_val, session_val)
# Arguments manipulation hook.
@@ -207,9 +229,6 @@ class Session(object):
# Send the HTTP Request.
r.send(prefetch=prefetch)
- # Send any cookies back up the to the session.
- self.cookies.update(r.response.cookies)
-
# Return the response.
return r.response
diff --git a/libs/requests/structures.py b/libs/requests/structures.py
index 37467542..fd1051a8 100644
--- a/libs/requests/structures.py
+++ b/libs/requests/structures.py
@@ -30,7 +30,7 @@ class CaseInsensitiveDict(dict):
self._clear_lower_keys()
def __delitem__(self, key):
- dict.__delitem__(self, key)
+ dict.__delitem__(self, self.lower_keys.get(key.lower(), key))
self._lower_keys.clear()
def __contains__(self, key):
diff --git a/libs/requests/utils.py b/libs/requests/utils.py
index b722d990..8c445e7b 100644
--- a/libs/requests/utils.py
+++ b/libs/requests/utils.py
@@ -12,18 +12,46 @@ that are also useful for external consumption.
import cgi
import codecs
import os
-import random
import re
import zlib
from netrc import netrc, NetrcParseError
from .compat import parse_http_list as _parse_list_header
-from .compat import quote, cookielib, SimpleCookie, is_py2, urlparse
-from .compat import basestring, bytes, str
+from .compat import quote, urlparse, basestring, bytes, str
+from .cookies import RequestsCookieJar, cookiejar_from_dict
+_hush_pyflakes = (RequestsCookieJar,)
+
+CERTIFI_BUNDLE_PATH = None
+try:
+ # see if requests's own CA certificate bundle is installed
+ import certifi
+ CERTIFI_BUNDLE_PATH = certifi.where()
+except ImportError:
+ pass
NETRC_FILES = ('.netrc', '_netrc')
+# common paths for the OS's CA certificate bundle
+POSSIBLE_CA_BUNDLE_PATHS = [
+ # Red Hat, CentOS, Fedora and friends (provided by the ca-certificates package):
+ '/etc/pki/tls/certs/ca-bundle.crt',
+ # Ubuntu, Debian, and friends (provided by the ca-certificates package):
+ '/etc/ssl/certs/ca-certificates.crt',
+ # FreeBSD (provided by the ca_root_nss package):
+ '/usr/local/share/certs/ca-root-nss.crt',
+]
+
+def get_os_ca_bundle_path():
+ """Try to pick an available CA certificate bundle provided by the OS."""
+ for path in POSSIBLE_CA_BUNDLE_PATHS:
+ if os.path.exists(path):
+ return path
+ return None
+
+# if certifi is installed, use its CA bundle;
+# otherwise, try and use the OS bundle
+DEFAULT_CA_BUNDLE_PATH = CERTIFI_BUNDLE_PATH or get_os_ca_bundle_path()
def dict_to_sequence(d):
"""Returns an internal sequence dictionary update."""
@@ -37,52 +65,39 @@ def dict_to_sequence(d):
def get_netrc_auth(url):
"""Returns the Requests tuple auth for a given url from netrc."""
- locations = (os.path.expanduser('~/{0}'.format(f)) for f in NETRC_FILES)
- netrc_path = None
-
- for loc in locations:
- if os.path.exists(loc) and not netrc_path:
- netrc_path = loc
-
- # Abort early if there isn't one.
- if netrc_path is None:
- return netrc_path
-
- ri = urlparse(url)
-
- # Strip port numbers from netloc
- host = ri.netloc.split(':')[0]
-
try:
- _netrc = netrc(netrc_path).authenticators(host)
- if _netrc:
- # Return with login / password
- login_i = (0 if _netrc[0] else 1)
- return (_netrc[login_i], _netrc[2])
- except (NetrcParseError, IOError, AttributeError):
- # If there was a parsing error or a permissions issue reading the file,
- # we'll just skip netrc auth
+ locations = (os.path.expanduser('~/{0}'.format(f)) for f in NETRC_FILES)
+ netrc_path = None
+
+ for loc in locations:
+ if os.path.exists(loc) and not netrc_path:
+ netrc_path = loc
+
+ # Abort early if there isn't one.
+ if netrc_path is None:
+ return netrc_path
+
+ ri = urlparse(url)
+
+ # Strip port numbers from netloc
+ host = ri.netloc.split(':')[0]
+
+ try:
+ _netrc = netrc(netrc_path).authenticators(host)
+ if _netrc:
+ # Return with login / password
+ login_i = (0 if _netrc[0] else 1)
+ return (_netrc[login_i], _netrc[2])
+ except (NetrcParseError, IOError):
+ # If there was a parsing error or a permissions issue reading the file,
+ # we'll just skip netrc auth
+ pass
+
+ # AppEngine hackiness.
+ except AttributeError:
pass
-def dict_from_string(s):
- """Returns a MultiDict with Cookies."""
-
- cookies = dict()
-
- try:
- c = SimpleCookie()
- c.load(s)
-
- for k, v in list(c.items()):
- cookies.update({k: v.value})
- # This stuff is not to be trusted.
- except Exception:
- pass
-
- return cookies
-
-
def guess_filename(obj):
"""Tries to guess the filename of the given object."""
name = getattr(obj, 'name', None)
@@ -231,15 +246,6 @@ def header_expand(headers):
return ''.join(collector)
-def randombytes(n):
- """Return n random bytes."""
- if is_py2:
- L = [chr(random.randrange(0, 256)) for i in range(n)]
- else:
- L = [chr(random.randrange(0, 256)).encode('utf-8') for i in range(n)]
- return b"".join(L)
-
-
def dict_from_cookiejar(cj):
"""Returns a key/value dictionary from a CookieJar.
@@ -257,24 +263,6 @@ def dict_from_cookiejar(cj):
return cookie_dict
-def cookiejar_from_dict(cookie_dict):
- """Returns a CookieJar from a key/value dictionary.
-
- :param cookie_dict: Dict of key/values to insert into CookieJar.
- """
-
- # return cookiejar if one was passed in
- if isinstance(cookie_dict, cookielib.CookieJar):
- return cookie_dict
-
- # create cookiejar
- cj = cookielib.CookieJar()
-
- cj = add_dict_to_cookiejar(cj, cookie_dict)
-
- return cj
-
-
def add_dict_to_cookiejar(cj, cookie_dict):
"""Returns a CookieJar from a key/value dictionary.
@@ -282,31 +270,9 @@ def add_dict_to_cookiejar(cj, cookie_dict):
:param cookie_dict: Dict of key/values to insert into CookieJar.
"""
- for k, v in list(cookie_dict.items()):
-
- cookie = cookielib.Cookie(
- version=0,
- name=k,
- value=v,
- port=None,
- port_specified=False,
- domain='',
- domain_specified=False,
- domain_initial_dot=False,
- path='/',
- path_specified=True,
- secure=False,
- expires=None,
- discard=True,
- comment=None,
- comment_url=None,
- rest={'HttpOnly': None},
- rfc2109=False
- )
-
- # add cookie to cookiejar
+ cj2 = cookiejar_from_dict(cookie_dict)
+ for cookie in cj2:
cj.set_cookie(cookie)
-
return cj
@@ -442,21 +408,23 @@ UNRESERVED_SET = frozenset(
def unquote_unreserved(uri):
"""Un-escape any percent-escape sequences in a URI that are unreserved
- characters.
- This leaves all reserved, illegal and non-ASCII bytes encoded.
+ characters. This leaves all reserved, illegal and non-ASCII bytes encoded.
"""
- parts = uri.split('%')
- for i in range(1, len(parts)):
- h = parts[i][0:2]
- if len(h) == 2:
- c = chr(int(h, 16))
- if c in UNRESERVED_SET:
- parts[i] = c + parts[i][2:]
+ try:
+ parts = uri.split('%')
+ for i in range(1, len(parts)):
+ h = parts[i][0:2]
+ if len(h) == 2 and h.isalnum():
+ c = chr(int(h, 16))
+ if c in UNRESERVED_SET:
+ parts[i] = c + parts[i][2:]
+ else:
+ parts[i] = '%' + parts[i]
else:
parts[i] = '%' + parts[i]
- else:
- parts[i] = '%' + parts[i]
- return ''.join(parts)
+ return ''.join(parts)
+ except ValueError:
+ return uri
def requote_uri(uri):
@@ -469,3 +437,19 @@ def requote_uri(uri):
# Then quote only illegal characters (do not quote reserved, unreserved,
# or '%')
return quote(unquote_unreserved(uri), safe="!#$%&'()*+,/:;=?@[]~")
+
+def get_environ_proxies():
+ """Return a dict of environment proxies."""
+
+ proxy_keys = [
+ 'all',
+ 'http',
+ 'https',
+ 'ftp',
+ 'socks',
+ 'no'
+ ]
+
+ get_proxy = lambda k: os.environ.get(k) or os.environ.get(k.upper())
+ proxies = [(key, get_proxy(key + '_proxy')) for key in proxy_keys]
+ return dict([(key, val) for (key, val) in proxies if val])
diff --git a/libs/rsa/__init__.py b/libs/rsa/__init__.py
new file mode 100644
index 00000000..8fb5e00a
--- /dev/null
+++ b/libs/rsa/__init__.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Sybren A. Stüvel
+#
+# 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.
+"""RSA module
+
+Module for calculating large primes, and RSA encryption, decryption, signing
+and verification. Includes generating public and private keys.
+
+WARNING: this implementation does not use random padding, compression of the
+cleartext input to prevent repetitions, or other common security improvements.
+Use with care.
+
+If you want to have a more secure implementation, use the functions from the
+``rsa.pkcs1`` module.
+
+"""
+
+__author__ = "Sybren Stuvel, Barry Mead and Yesudeep Mangalapilly"
+__date__ = "2012-06-17"
+__version__ = '3.1.1'
+
+from rsa.key import newkeys, PrivateKey, PublicKey
+from rsa.pkcs1 import encrypt, decrypt, sign, verify, DecryptionError, \
+ VerificationError
+
+# Do doctest if we're run directly
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod()
+
+__all__ = ["newkeys", "encrypt", "decrypt", "sign", "verify", 'PublicKey',
+ 'PrivateKey', 'DecryptionError', 'VerificationError']
+
diff --git a/libs/rsa/_compat.py b/libs/rsa/_compat.py
new file mode 100644
index 00000000..3c4eb81b
--- /dev/null
+++ b/libs/rsa/_compat.py
@@ -0,0 +1,160 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Sybren A. Stüvel
+#
+# 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.
+
+"""Python compatibility wrappers."""
+
+
+from __future__ import absolute_import
+
+import sys
+from struct import pack
+
+try:
+ MAX_INT = sys.maxsize
+except AttributeError:
+ MAX_INT = sys.maxint
+
+MAX_INT64 = (1 << 63) - 1
+MAX_INT32 = (1 << 31) - 1
+MAX_INT16 = (1 << 15) - 1
+
+# Determine the word size of the processor.
+if MAX_INT == MAX_INT64:
+ # 64-bit processor.
+ MACHINE_WORD_SIZE = 64
+elif MAX_INT == MAX_INT32:
+ # 32-bit processor.
+ MACHINE_WORD_SIZE = 32
+else:
+ # Else we just assume 64-bit processor keeping up with modern times.
+ MACHINE_WORD_SIZE = 64
+
+
+try:
+ # < Python3
+ unicode_type = unicode
+ have_python3 = False
+except NameError:
+ # Python3.
+ unicode_type = str
+ have_python3 = True
+
+# Fake byte literals.
+if str is unicode_type:
+ def byte_literal(s):
+ return s.encode('latin1')
+else:
+ def byte_literal(s):
+ return s
+
+# ``long`` is no more. Do type detection using this instead.
+try:
+ integer_types = (int, long)
+except NameError:
+ integer_types = (int,)
+
+b = byte_literal
+
+try:
+ # Python 2.6 or higher.
+ bytes_type = bytes
+except NameError:
+ # Python 2.5
+ bytes_type = str
+
+
+# To avoid calling b() multiple times in tight loops.
+ZERO_BYTE = b('\x00')
+EMPTY_BYTE = b('')
+
+
+def is_bytes(obj):
+ """
+ Determines whether the given value is a byte string.
+
+ :param obj:
+ The value to test.
+ :returns:
+ ``True`` if ``value`` is a byte string; ``False`` otherwise.
+ """
+ return isinstance(obj, bytes_type)
+
+
+def is_integer(obj):
+ """
+ Determines whether the given value is an integer.
+
+ :param obj:
+ The value to test.
+ :returns:
+ ``True`` if ``value`` is an integer; ``False`` otherwise.
+ """
+ return isinstance(obj, integer_types)
+
+
+def byte(num):
+ """
+ Converts a number between 0 and 255 (both inclusive) to a base-256 (byte)
+ representation.
+
+ Use it as a replacement for ``chr`` where you are expecting a byte
+ because this will work on all current versions of Python::
+
+ :param num:
+ An unsigned integer between 0 and 255 (both inclusive).
+ :returns:
+ A single byte.
+ """
+ return pack("B", num)
+
+
+def get_word_alignment(num, force_arch=64,
+ _machine_word_size=MACHINE_WORD_SIZE):
+ """
+ Returns alignment details for the given number based on the platform
+ Python is running on.
+
+ :param num:
+ Unsigned integral number.
+ :param force_arch:
+ If you don't want to use 64-bit unsigned chunks, set this to
+ anything other than 64. 32-bit chunks will be preferred then.
+ Default 64 will be used when on a 64-bit machine.
+ :param _machine_word_size:
+ (Internal) The machine word size used for alignment.
+ :returns:
+ 4-tuple::
+
+ (word_bits, word_bytes,
+ max_uint, packing_format_type)
+ """
+ max_uint64 = 0xffffffffffffffff
+ max_uint32 = 0xffffffff
+ max_uint16 = 0xffff
+ max_uint8 = 0xff
+
+ if force_arch == 64 and _machine_word_size >= 64 and num > max_uint32:
+ # 64-bit unsigned integer.
+ return 64, 8, max_uint64, "Q"
+ elif num > max_uint16:
+ # 32-bit unsigned integer
+ return 32, 4, max_uint32, "L"
+ elif num > max_uint8:
+ # 16-bit unsigned integer.
+ return 16, 2, max_uint16, "H"
+ else:
+ # 8-bit unsigned integer.
+ return 8, 1, max_uint8, "B"
diff --git a/libs/rsa/_version133.py b/libs/rsa/_version133.py
new file mode 100644
index 00000000..230a03c8
--- /dev/null
+++ b/libs/rsa/_version133.py
@@ -0,0 +1,442 @@
+"""RSA module
+pri = k[1] //Private part of keys d,p,q
+
+Module for calculating large primes, and RSA encryption, decryption,
+signing and verification. Includes generating public and private keys.
+
+WARNING: this code implements the mathematics of RSA. It is not suitable for
+real-world secure cryptography purposes. It has not been reviewed by a security
+expert. It does not include padding of data. There are many ways in which the
+output of this module, when used without any modification, can be sucessfully
+attacked.
+"""
+
+__author__ = "Sybren Stuvel, Marloes de Boer and Ivo Tamboer"
+__date__ = "2010-02-05"
+__version__ = '1.3.3'
+
+# NOTE: Python's modulo can return negative numbers. We compensate for
+# this behaviour using the abs() function
+
+from cPickle import dumps, loads
+import base64
+import math
+import os
+import random
+import sys
+import types
+import zlib
+
+from rsa._compat import byte
+
+# Display a warning that this insecure version is imported.
+import warnings
+warnings.warn('Insecure version of the RSA module is imported as %s, be careful'
+ % __name__)
+
+def gcd(p, q):
+ """Returns the greatest common divisor of p and q
+
+
+ >>> gcd(42, 6)
+ 6
+ """
+ if p>> (128*256 + 64)*256 + + 15
+ 8405007
+ >>> l = [128, 64, 15]
+ >>> bytes2int(l)
+ 8405007
+ """
+
+ if not (type(bytes) is types.ListType or type(bytes) is types.StringType):
+ raise TypeError("You must pass a string or a list")
+
+ # Convert byte stream to integer
+ integer = 0
+ for byte in bytes:
+ integer *= 256
+ if type(byte) is types.StringType: byte = ord(byte)
+ integer += byte
+
+ return integer
+
+def int2bytes(number):
+ """Converts a number to a string of bytes
+
+ >>> bytes2int(int2bytes(123456789))
+ 123456789
+ """
+
+ if not (type(number) is types.LongType or type(number) is types.IntType):
+ raise TypeError("You must pass a long or an int")
+
+ string = ""
+
+ while number > 0:
+ string = "%s%s" % (byte(number & 0xFF), string)
+ number /= 256
+
+ return string
+
+def fast_exponentiation(a, p, n):
+ """Calculates r = a^p mod n
+ """
+ result = a % n
+ remainders = []
+ while p != 1:
+ remainders.append(p & 1)
+ p = p >> 1
+ while remainders:
+ rem = remainders.pop()
+ result = ((a ** rem) * result ** 2) % n
+ return result
+
+def read_random_int(nbits):
+ """Reads a random integer of approximately nbits bits rounded up
+ to whole bytes"""
+
+ nbytes = ceil(nbits/8.)
+ randomdata = os.urandom(nbytes)
+ return bytes2int(randomdata)
+
+def ceil(x):
+ """ceil(x) -> int(math.ceil(x))"""
+
+ return int(math.ceil(x))
+
+def randint(minvalue, maxvalue):
+ """Returns a random integer x with minvalue <= x <= maxvalue"""
+
+ # Safety - get a lot of random data even if the range is fairly
+ # small
+ min_nbits = 32
+
+ # The range of the random numbers we need to generate
+ range = maxvalue - minvalue
+
+ # Which is this number of bytes
+ rangebytes = ceil(math.log(range, 2) / 8.)
+
+ # Convert to bits, but make sure it's always at least min_nbits*2
+ rangebits = max(rangebytes * 8, min_nbits * 2)
+
+ # Take a random number of bits between min_nbits and rangebits
+ nbits = random.randint(min_nbits, rangebits)
+
+ return (read_random_int(nbits) % range) + minvalue
+
+def fermat_little_theorem(p):
+ """Returns 1 if p may be prime, and something else if p definitely
+ is not prime"""
+
+ a = randint(1, p-1)
+ return fast_exponentiation(a, p-1, p)
+
+def jacobi(a, b):
+ """Calculates the value of the Jacobi symbol (a/b)
+ """
+
+ if a % b == 0:
+ return 0
+ result = 1
+ while a > 1:
+ if a & 1:
+ if ((a-1)*(b-1) >> 2) & 1:
+ result = -result
+ b, a = a, b % a
+ else:
+ if ((b ** 2 - 1) >> 3) & 1:
+ result = -result
+ a = a >> 1
+ return result
+
+def jacobi_witness(x, n):
+ """Returns False if n is an Euler pseudo-prime with base x, and
+ True otherwise.
+ """
+
+ j = jacobi(x, n) % n
+ f = fast_exponentiation(x, (n-1)/2, n)
+
+ if j == f: return False
+ return True
+
+def randomized_primality_testing(n, k):
+ """Calculates whether n is composite (which is always correct) or
+ prime (which is incorrect with error probability 2**-k)
+
+ Returns False if the number if composite, and True if it's
+ probably prime.
+ """
+
+ q = 0.5 # Property of the jacobi_witness function
+
+ # t = int(math.ceil(k / math.log(1/q, 2)))
+ t = ceil(k / math.log(1/q, 2))
+ for i in range(t+1):
+ x = randint(1, n-1)
+ if jacobi_witness(x, n): return False
+
+ return True
+
+def is_prime(number):
+ """Returns True if the number is prime, and False otherwise.
+
+ >>> is_prime(42)
+ 0
+ >>> is_prime(41)
+ 1
+ """
+
+ """
+ if not fermat_little_theorem(number) == 1:
+ # Not prime, according to Fermat's little theorem
+ return False
+ """
+
+ if randomized_primality_testing(number, 5):
+ # Prime, according to Jacobi
+ return True
+
+ # Not prime
+ return False
+
+
+def getprime(nbits):
+ """Returns a prime number of max. 'math.ceil(nbits/8)*8' bits. In
+ other words: nbits is rounded up to whole bytes.
+
+ >>> p = getprime(8)
+ >>> is_prime(p-1)
+ 0
+ >>> is_prime(p)
+ 1
+ >>> is_prime(p+1)
+ 0
+ """
+
+ nbytes = int(math.ceil(nbits/8.))
+
+ while True:
+ integer = read_random_int(nbits)
+
+ # Make sure it's odd
+ integer |= 1
+
+ # Test for primeness
+ if is_prime(integer): break
+
+ # Retry if not prime
+
+ return integer
+
+def are_relatively_prime(a, b):
+ """Returns True if a and b are relatively prime, and False if they
+ are not.
+
+ >>> are_relatively_prime(2, 3)
+ 1
+ >>> are_relatively_prime(2, 4)
+ 0
+ """
+
+ d = gcd(a, b)
+ return (d == 1)
+
+def find_p_q(nbits):
+ """Returns a tuple of two different primes of nbits bits"""
+
+ p = getprime(nbits)
+ while True:
+ q = getprime(nbits)
+ if not q == p: break
+
+ return (p, q)
+
+def extended_euclid_gcd(a, b):
+ """Returns a tuple (d, i, j) such that d = gcd(a, b) = ia + jb
+ """
+
+ if b == 0:
+ return (a, 1, 0)
+
+ q = abs(a % b)
+ r = long(a / b)
+ (d, k, l) = extended_euclid_gcd(b, q)
+
+ return (d, l, k - l*r)
+
+# Main function: calculate encryption and decryption keys
+def calculate_keys(p, q, nbits):
+ """Calculates an encryption and a decryption key for p and q, and
+ returns them as a tuple (e, d)"""
+
+ n = p * q
+ phi_n = (p-1) * (q-1)
+
+ while True:
+ # Make sure e has enough bits so we ensure "wrapping" through
+ # modulo n
+ e = getprime(max(8, nbits/2))
+ if are_relatively_prime(e, n) and are_relatively_prime(e, phi_n): break
+
+ (d, i, j) = extended_euclid_gcd(e, phi_n)
+
+ if not d == 1:
+ raise Exception("e (%d) and phi_n (%d) are not relatively prime" % (e, phi_n))
+
+ if not (e * i) % phi_n == 1:
+ raise Exception("e (%d) and i (%d) are not mult. inv. modulo phi_n (%d)" % (e, i, phi_n))
+
+ return (e, i)
+
+
+def gen_keys(nbits):
+ """Generate RSA keys of nbits bits. Returns (p, q, e, d).
+
+ Note: this can take a long time, depending on the key size.
+ """
+
+ while True:
+ (p, q) = find_p_q(nbits)
+ (e, d) = calculate_keys(p, q, nbits)
+
+ # For some reason, d is sometimes negative. We don't know how
+ # to fix it (yet), so we keep trying until everything is shiny
+ if d > 0: break
+
+ return (p, q, e, d)
+
+def gen_pubpriv_keys(nbits):
+ """Generates public and private keys, and returns them as (pub,
+ priv).
+
+ The public key consists of a dict {e: ..., , n: ....). The private
+ key consists of a dict {d: ...., p: ...., q: ....).
+ """
+
+ (p, q, e, d) = gen_keys(nbits)
+
+ return ( {'e': e, 'n': p*q}, {'d': d, 'p': p, 'q': q} )
+
+def encrypt_int(message, ekey, n):
+ """Encrypts a message using encryption key 'ekey', working modulo
+ n"""
+
+ if type(message) is types.IntType:
+ return encrypt_int(long(message), ekey, n)
+
+ if not type(message) is types.LongType:
+ raise TypeError("You must pass a long or an int")
+
+ if message > 0 and \
+ math.floor(math.log(message, 2)) > math.floor(math.log(n, 2)):
+ raise OverflowError("The message is too long")
+
+ return fast_exponentiation(message, ekey, n)
+
+def decrypt_int(cyphertext, dkey, n):
+ """Decrypts a cypher text using the decryption key 'dkey', working
+ modulo n"""
+
+ return encrypt_int(cyphertext, dkey, n)
+
+def sign_int(message, dkey, n):
+ """Signs 'message' using key 'dkey', working modulo n"""
+
+ return decrypt_int(message, dkey, n)
+
+def verify_int(signed, ekey, n):
+ """verifies 'signed' using key 'ekey', working modulo n"""
+
+ return encrypt_int(signed, ekey, n)
+
+def picklechops(chops):
+ """Pickles and base64encodes it's argument chops"""
+
+ value = zlib.compress(dumps(chops))
+ encoded = base64.encodestring(value)
+ return encoded.strip()
+
+def unpicklechops(string):
+ """base64decodes and unpickes it's argument string into chops"""
+
+ return loads(zlib.decompress(base64.decodestring(string)))
+
+def chopstring(message, key, n, funcref):
+ """Splits 'message' into chops that are at most as long as n,
+ converts these into integers, and calls funcref(integer, key, n)
+ for each chop.
+
+ Used by 'encrypt' and 'sign'.
+ """
+
+ msglen = len(message)
+ mbits = msglen * 8
+ nbits = int(math.floor(math.log(n, 2)))
+ nbytes = nbits / 8
+ blocks = msglen / nbytes
+
+ if msglen % nbytes > 0:
+ blocks += 1
+
+ cypher = []
+
+ for bindex in range(blocks):
+ offset = bindex * nbytes
+ block = message[offset:offset+nbytes]
+ value = bytes2int(block)
+ cypher.append(funcref(value, key, n))
+
+ return picklechops(cypher)
+
+def gluechops(chops, key, n, funcref):
+ """Glues chops back together into a string. calls
+ funcref(integer, key, n) for each chop.
+
+ Used by 'decrypt' and 'verify'.
+ """
+ message = ""
+
+ chops = unpicklechops(chops)
+
+ for cpart in chops:
+ mpart = funcref(cpart, key, n)
+ message += int2bytes(mpart)
+
+ return message
+
+def encrypt(message, key):
+ """Encrypts a string 'message' with the public key 'key'"""
+
+ return chopstring(message, key['e'], key['n'], encrypt_int)
+
+def sign(message, key):
+ """Signs a string 'message' with the private key 'key'"""
+
+ return chopstring(message, key['d'], key['p']*key['q'], decrypt_int)
+
+def decrypt(cypher, key):
+ """Decrypts a cypher with the private key 'key'"""
+
+ return gluechops(cypher, key['d'], key['p']*key['q'], decrypt_int)
+
+def verify(cypher, key):
+ """Verifies a cypher with the public key 'key'"""
+
+ return gluechops(cypher, key['e'], key['n'], encrypt_int)
+
+# Do doctest if we're not imported
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod()
+
+__all__ = ["gen_pubpriv_keys", "encrypt", "decrypt", "sign", "verify"]
+
diff --git a/libs/rsa/_version200.py b/libs/rsa/_version200.py
new file mode 100644
index 00000000..f9156538
--- /dev/null
+++ b/libs/rsa/_version200.py
@@ -0,0 +1,529 @@
+"""RSA module
+
+Module for calculating large primes, and RSA encryption, decryption,
+signing and verification. Includes generating public and private keys.
+
+WARNING: this implementation does not use random padding, compression of the
+cleartext input to prevent repetitions, or other common security improvements.
+Use with care.
+
+"""
+
+__author__ = "Sybren Stuvel, Marloes de Boer, Ivo Tamboer, and Barry Mead"
+__date__ = "2010-02-08"
+__version__ = '2.0'
+
+import math
+import os
+import random
+import sys
+import types
+from rsa._compat import byte
+
+# Display a warning that this insecure version is imported.
+import warnings
+warnings.warn('Insecure version of the RSA module is imported as %s' % __name__)
+
+
+def bit_size(number):
+ """Returns the number of bits required to hold a specific long number"""
+
+ return int(math.ceil(math.log(number,2)))
+
+def gcd(p, q):
+ """Returns the greatest common divisor of p and q
+ >>> gcd(48, 180)
+ 12
+ """
+ # Iterateive Version is faster and uses much less stack space
+ while q != 0:
+ if p < q: (p,q) = (q,p)
+ (p,q) = (q, p % q)
+ return p
+
+
+def bytes2int(bytes):
+ """Converts a list of bytes or a string to an integer
+
+ >>> (((128 * 256) + 64) * 256) + 15
+ 8405007
+ >>> l = [128, 64, 15]
+ >>> bytes2int(l) #same as bytes2int('\x80@\x0f')
+ 8405007
+ """
+
+ if not (type(bytes) is types.ListType or type(bytes) is types.StringType):
+ raise TypeError("You must pass a string or a list")
+
+ # Convert byte stream to integer
+ integer = 0
+ for byte in bytes:
+ integer *= 256
+ if type(byte) is types.StringType: byte = ord(byte)
+ integer += byte
+
+ return integer
+
+def int2bytes(number):
+ """
+ Converts a number to a string of bytes
+ """
+
+ if not (type(number) is types.LongType or type(number) is types.IntType):
+ raise TypeError("You must pass a long or an int")
+
+ string = ""
+
+ while number > 0:
+ string = "%s%s" % (byte(number & 0xFF), string)
+ number /= 256
+
+ return string
+
+def to64(number):
+ """Converts a number in the range of 0 to 63 into base 64 digit
+ character in the range of '0'-'9', 'A'-'Z', 'a'-'z','-','_'.
+
+ >>> to64(10)
+ 'A'
+ """
+
+ if not (type(number) is types.LongType or type(number) is types.IntType):
+ raise TypeError("You must pass a long or an int")
+
+ if 0 <= number <= 9: #00-09 translates to '0' - '9'
+ return byte(number + 48)
+
+ if 10 <= number <= 35:
+ return byte(number + 55) #10-35 translates to 'A' - 'Z'
+
+ if 36 <= number <= 61:
+ return byte(number + 61) #36-61 translates to 'a' - 'z'
+
+ if number == 62: # 62 translates to '-' (minus)
+ return byte(45)
+
+ if number == 63: # 63 translates to '_' (underscore)
+ return byte(95)
+
+ raise ValueError('Invalid Base64 value: %i' % number)
+
+
+def from64(number):
+ """Converts an ordinal character value in the range of
+ 0-9,A-Z,a-z,-,_ to a number in the range of 0-63.
+
+ >>> from64(49)
+ 1
+ """
+
+ if not (type(number) is types.LongType or type(number) is types.IntType):
+ raise TypeError("You must pass a long or an int")
+
+ if 48 <= number <= 57: #ord('0') - ord('9') translates to 0-9
+ return(number - 48)
+
+ if 65 <= number <= 90: #ord('A') - ord('Z') translates to 10-35
+ return(number - 55)
+
+ if 97 <= number <= 122: #ord('a') - ord('z') translates to 36-61
+ return(number - 61)
+
+ if number == 45: #ord('-') translates to 62
+ return(62)
+
+ if number == 95: #ord('_') translates to 63
+ return(63)
+
+ raise ValueError('Invalid Base64 value: %i' % number)
+
+
+def int2str64(number):
+ """Converts a number to a string of base64 encoded characters in
+ the range of '0'-'9','A'-'Z,'a'-'z','-','_'.
+
+ >>> int2str64(123456789)
+ '7MyqL'
+ """
+
+ if not (type(number) is types.LongType or type(number) is types.IntType):
+ raise TypeError("You must pass a long or an int")
+
+ string = ""
+
+ while number > 0:
+ string = "%s%s" % (to64(number & 0x3F), string)
+ number /= 64
+
+ return string
+
+
+def str642int(string):
+ """Converts a base64 encoded string into an integer.
+ The chars of this string in in the range '0'-'9','A'-'Z','a'-'z','-','_'
+
+ >>> str642int('7MyqL')
+ 123456789
+ """
+
+ if not (type(string) is types.ListType or type(string) is types.StringType):
+ raise TypeError("You must pass a string or a list")
+
+ integer = 0
+ for byte in string:
+ integer *= 64
+ if type(byte) is types.StringType: byte = ord(byte)
+ integer += from64(byte)
+
+ return integer
+
+def read_random_int(nbits):
+ """Reads a random integer of approximately nbits bits rounded up
+ to whole bytes"""
+
+ nbytes = int(math.ceil(nbits/8.))
+ randomdata = os.urandom(nbytes)
+ return bytes2int(randomdata)
+
+def randint(minvalue, maxvalue):
+ """Returns a random integer x with minvalue <= x <= maxvalue"""
+
+ # Safety - get a lot of random data even if the range is fairly
+ # small
+ min_nbits = 32
+
+ # The range of the random numbers we need to generate
+ range = (maxvalue - minvalue) + 1
+
+ # Which is this number of bytes
+ rangebytes = ((bit_size(range) + 7) / 8)
+
+ # Convert to bits, but make sure it's always at least min_nbits*2
+ rangebits = max(rangebytes * 8, min_nbits * 2)
+
+ # Take a random number of bits between min_nbits and rangebits
+ nbits = random.randint(min_nbits, rangebits)
+
+ return (read_random_int(nbits) % range) + minvalue
+
+def jacobi(a, b):
+ """Calculates the value of the Jacobi symbol (a/b)
+ where both a and b are positive integers, and b is odd
+ """
+
+ if a == 0: return 0
+ result = 1
+ while a > 1:
+ if a & 1:
+ if ((a-1)*(b-1) >> 2) & 1:
+ result = -result
+ a, b = b % a, a
+ else:
+ if (((b * b) - 1) >> 3) & 1:
+ result = -result
+ a >>= 1
+ if a == 0: return 0
+ return result
+
+def jacobi_witness(x, n):
+ """Returns False if n is an Euler pseudo-prime with base x, and
+ True otherwise.
+ """
+
+ j = jacobi(x, n) % n
+ f = pow(x, (n-1)/2, n)
+
+ if j == f: return False
+ return True
+
+def randomized_primality_testing(n, k):
+ """Calculates whether n is composite (which is always correct) or
+ prime (which is incorrect with error probability 2**-k)
+
+ Returns False if the number is composite, and True if it's
+ probably prime.
+ """
+
+ # 50% of Jacobi-witnesses can report compositness of non-prime numbers
+
+ for i in range(k):
+ x = randint(1, n-1)
+ if jacobi_witness(x, n): return False
+
+ return True
+
+def is_prime(number):
+ """Returns True if the number is prime, and False otherwise.
+
+ >>> is_prime(42)
+ 0
+ >>> is_prime(41)
+ 1
+ """
+
+ if randomized_primality_testing(number, 6):
+ # Prime, according to Jacobi
+ return True
+
+ # Not prime
+ return False
+
+
+def getprime(nbits):
+ """Returns a prime number of max. 'math.ceil(nbits/8)*8' bits. In
+ other words: nbits is rounded up to whole bytes.
+
+ >>> p = getprime(8)
+ >>> is_prime(p-1)
+ 0
+ >>> is_prime(p)
+ 1
+ >>> is_prime(p+1)
+ 0
+ """
+
+ while True:
+ integer = read_random_int(nbits)
+
+ # Make sure it's odd
+ integer |= 1
+
+ # Test for primeness
+ if is_prime(integer): break
+
+ # Retry if not prime
+
+ return integer
+
+def are_relatively_prime(a, b):
+ """Returns True if a and b are relatively prime, and False if they
+ are not.
+
+ >>> are_relatively_prime(2, 3)
+ 1
+ >>> are_relatively_prime(2, 4)
+ 0
+ """
+
+ d = gcd(a, b)
+ return (d == 1)
+
+def find_p_q(nbits):
+ """Returns a tuple of two different primes of nbits bits"""
+ pbits = nbits + (nbits/16) #Make sure that p and q aren't too close
+ qbits = nbits - (nbits/16) #or the factoring programs can factor n
+ p = getprime(pbits)
+ while True:
+ q = getprime(qbits)
+ #Make sure p and q are different.
+ if not q == p: break
+ return (p, q)
+
+def extended_gcd(a, b):
+ """Returns a tuple (r, i, j) such that r = gcd(a, b) = ia + jb
+ """
+ # r = gcd(a,b) i = multiplicitive inverse of a mod b
+ # or j = multiplicitive inverse of b mod a
+ # Neg return values for i or j are made positive mod b or a respectively
+ # Iterateive Version is faster and uses much less stack space
+ x = 0
+ y = 1
+ lx = 1
+ ly = 0
+ oa = a #Remember original a/b to remove
+ ob = b #negative values from return results
+ while b != 0:
+ q = long(a/b)
+ (a, b) = (b, a % b)
+ (x, lx) = ((lx - (q * x)),x)
+ (y, ly) = ((ly - (q * y)),y)
+ if (lx < 0): lx += ob #If neg wrap modulo orignal b
+ if (ly < 0): ly += oa #If neg wrap modulo orignal a
+ return (a, lx, ly) #Return only positive values
+
+# Main function: calculate encryption and decryption keys
+def calculate_keys(p, q, nbits):
+ """Calculates an encryption and a decryption key for p and q, and
+ returns them as a tuple (e, d)"""
+
+ n = p * q
+ phi_n = (p-1) * (q-1)
+
+ while True:
+ # Make sure e has enough bits so we ensure "wrapping" through
+ # modulo n
+ e = max(65537,getprime(nbits/4))
+ if are_relatively_prime(e, n) and are_relatively_prime(e, phi_n): break
+
+ (d, i, j) = extended_gcd(e, phi_n)
+
+ if not d == 1:
+ raise Exception("e (%d) and phi_n (%d) are not relatively prime" % (e, phi_n))
+ if (i < 0):
+ raise Exception("New extended_gcd shouldn't return negative values")
+ if not (e * i) % phi_n == 1:
+ raise Exception("e (%d) and i (%d) are not mult. inv. modulo phi_n (%d)" % (e, i, phi_n))
+
+ return (e, i)
+
+
+def gen_keys(nbits):
+ """Generate RSA keys of nbits bits. Returns (p, q, e, d).
+
+ Note: this can take a long time, depending on the key size.
+ """
+
+ (p, q) = find_p_q(nbits)
+ (e, d) = calculate_keys(p, q, nbits)
+
+ return (p, q, e, d)
+
+def newkeys(nbits):
+ """Generates public and private keys, and returns them as (pub,
+ priv).
+
+ The public key consists of a dict {e: ..., , n: ....). The private
+ key consists of a dict {d: ...., p: ...., q: ....).
+ """
+ nbits = max(9,nbits) # Don't let nbits go below 9 bits
+ (p, q, e, d) = gen_keys(nbits)
+
+ return ( {'e': e, 'n': p*q}, {'d': d, 'p': p, 'q': q} )
+
+def encrypt_int(message, ekey, n):
+ """Encrypts a message using encryption key 'ekey', working modulo n"""
+
+ if type(message) is types.IntType:
+ message = long(message)
+
+ if not type(message) is types.LongType:
+ raise TypeError("You must pass a long or int")
+
+ if message < 0 or message > n:
+ raise OverflowError("The message is too long")
+
+ #Note: Bit exponents start at zero (bit counts start at 1) this is correct
+ safebit = bit_size(n) - 2 #compute safe bit (MSB - 1)
+ message += (1 << safebit) #add safebit to ensure folding
+
+ return pow(message, ekey, n)
+
+def decrypt_int(cyphertext, dkey, n):
+ """Decrypts a cypher text using the decryption key 'dkey', working
+ modulo n"""
+
+ message = pow(cyphertext, dkey, n)
+
+ safebit = bit_size(n) - 2 #compute safe bit (MSB - 1)
+ message -= (1 << safebit) #remove safebit before decode
+
+ return message
+
+def encode64chops(chops):
+ """base64encodes chops and combines them into a ',' delimited string"""
+
+ chips = [] #chips are character chops
+
+ for value in chops:
+ chips.append(int2str64(value))
+
+ #delimit chops with comma
+ encoded = ','.join(chips)
+
+ return encoded
+
+def decode64chops(string):
+ """base64decodes and makes a ',' delimited string into chops"""
+
+ chips = string.split(',') #split chops at commas
+
+ chops = []
+
+ for string in chips: #make char chops (chips) into chops
+ chops.append(str642int(string))
+
+ return chops
+
+def chopstring(message, key, n, funcref):
+ """Chops the 'message' into integers that fit into n,
+ leaving room for a safebit to be added to ensure that all
+ messages fold during exponentiation. The MSB of the number n
+ is not independant modulo n (setting it could cause overflow), so
+ use the next lower bit for the safebit. Therefore reserve 2-bits
+ in the number n for non-data bits. Calls specified encryption
+ function for each chop.
+
+ Used by 'encrypt' and 'sign'.
+ """
+
+ msglen = len(message)
+ mbits = msglen * 8
+ #Set aside 2-bits so setting of safebit won't overflow modulo n.
+ nbits = bit_size(n) - 2 # leave room for safebit
+ nbytes = nbits / 8
+ blocks = msglen / nbytes
+
+ if msglen % nbytes > 0:
+ blocks += 1
+
+ cypher = []
+
+ for bindex in range(blocks):
+ offset = bindex * nbytes
+ block = message[offset:offset+nbytes]
+ value = bytes2int(block)
+ cypher.append(funcref(value, key, n))
+
+ return encode64chops(cypher) #Encode encrypted ints to base64 strings
+
+def gluechops(string, key, n, funcref):
+ """Glues chops back together into a string. calls
+ funcref(integer, key, n) for each chop.
+
+ Used by 'decrypt' and 'verify'.
+ """
+ message = ""
+
+ chops = decode64chops(string) #Decode base64 strings into integer chops
+
+ for cpart in chops:
+ mpart = funcref(cpart, key, n) #Decrypt each chop
+ message += int2bytes(mpart) #Combine decrypted strings into a msg
+
+ return message
+
+def encrypt(message, key):
+ """Encrypts a string 'message' with the public key 'key'"""
+ if 'n' not in key:
+ raise Exception("You must use the public key with encrypt")
+
+ return chopstring(message, key['e'], key['n'], encrypt_int)
+
+def sign(message, key):
+ """Signs a string 'message' with the private key 'key'"""
+ if 'p' not in key:
+ raise Exception("You must use the private key with sign")
+
+ return chopstring(message, key['d'], key['p']*key['q'], encrypt_int)
+
+def decrypt(cypher, key):
+ """Decrypts a string 'cypher' with the private key 'key'"""
+ if 'p' not in key:
+ raise Exception("You must use the private key with decrypt")
+
+ return gluechops(cypher, key['d'], key['p']*key['q'], decrypt_int)
+
+def verify(cypher, key):
+ """Verifies a string 'cypher' with the public key 'key'"""
+ if 'n' not in key:
+ raise Exception("You must use the public key with verify")
+
+ return gluechops(cypher, key['e'], key['n'], decrypt_int)
+
+# Do doctest if we're not imported
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod()
+
+__all__ = ["newkeys", "encrypt", "decrypt", "sign", "verify"]
+
diff --git a/libs/rsa/bigfile.py b/libs/rsa/bigfile.py
new file mode 100644
index 00000000..516cf56b
--- /dev/null
+++ b/libs/rsa/bigfile.py
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Sybren A. Stüvel
+#
+# 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.
+
+'''Large file support
+
+ - break a file into smaller blocks, and encrypt them, and store the
+ encrypted blocks in another file.
+
+ - take such an encrypted files, decrypt its blocks, and reconstruct the
+ original file.
+
+The encrypted file format is as follows, where || denotes byte concatenation:
+
+ FILE := VERSION || BLOCK || BLOCK ...
+
+ BLOCK := LENGTH || DATA
+
+ LENGTH := varint-encoded length of the subsequent data. Varint comes from
+ Google Protobuf, and encodes an integer into a variable number of bytes.
+ Each byte uses the 7 lowest bits to encode the value. The highest bit set
+ to 1 indicates the next byte is also part of the varint. The last byte will
+ have this bit set to 0.
+
+This file format is called the VARBLOCK format, in line with the varint format
+used to denote the block sizes.
+
+'''
+
+from rsa import key, common, pkcs1, varblock
+from rsa._compat import byte
+
+def encrypt_bigfile(infile, outfile, pub_key):
+ '''Encrypts a file, writing it to 'outfile' in VARBLOCK format.
+
+ :param infile: file-like object to read the cleartext from
+ :param outfile: file-like object to write the crypto in VARBLOCK format to
+ :param pub_key: :py:class:`rsa.PublicKey` to encrypt with
+
+ '''
+
+ if not isinstance(pub_key, key.PublicKey):
+ raise TypeError('Public key required, but got %r' % pub_key)
+
+ key_bytes = common.bit_size(pub_key.n) // 8
+ blocksize = key_bytes - 11 # keep space for PKCS#1 padding
+
+ # Write the version number to the VARBLOCK file
+ outfile.write(byte(varblock.VARBLOCK_VERSION))
+
+ # Encrypt and write each block
+ for block in varblock.yield_fixedblocks(infile, blocksize):
+ crypto = pkcs1.encrypt(block, pub_key)
+
+ varblock.write_varint(outfile, len(crypto))
+ outfile.write(crypto)
+
+def decrypt_bigfile(infile, outfile, priv_key):
+ '''Decrypts an encrypted VARBLOCK file, writing it to 'outfile'
+
+ :param infile: file-like object to read the crypto in VARBLOCK format from
+ :param outfile: file-like object to write the cleartext to
+ :param priv_key: :py:class:`rsa.PrivateKey` to decrypt with
+
+ '''
+
+ if not isinstance(priv_key, key.PrivateKey):
+ raise TypeError('Private key required, but got %r' % priv_key)
+
+ for block in varblock.yield_varblocks(infile):
+ cleartext = pkcs1.decrypt(block, priv_key)
+ outfile.write(cleartext)
+
+__all__ = ['encrypt_bigfile', 'decrypt_bigfile']
+
diff --git a/libs/rsa/cli.py b/libs/rsa/cli.py
new file mode 100644
index 00000000..2441955a
--- /dev/null
+++ b/libs/rsa/cli.py
@@ -0,0 +1,379 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Sybren A. Stüvel
+#
+# 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.
+
+'''Commandline scripts.
+
+These scripts are called by the executables defined in setup.py.
+'''
+
+from __future__ import with_statement, print_function
+
+import abc
+import sys
+from optparse import OptionParser
+
+import rsa
+import rsa.bigfile
+import rsa.pkcs1
+
+HASH_METHODS = sorted(rsa.pkcs1.HASH_METHODS.keys())
+
+def keygen():
+ '''Key generator.'''
+
+ # Parse the CLI options
+ parser = OptionParser(usage='usage: %prog [options] keysize',
+ description='Generates a new RSA keypair of "keysize" bits.')
+
+ parser.add_option('--pubout', type='string',
+ help='Output filename for the public key. The public key is '
+ 'not saved if this option is not present. You can use '
+ 'pyrsa-priv2pub to create the public key file later.')
+
+ parser.add_option('-o', '--out', type='string',
+ help='Output filename for the private key. The key is '
+ 'written to stdout if this option is not present.')
+
+ parser.add_option('--form',
+ help='key format of the private and public keys - default PEM',
+ choices=('PEM', 'DER'), default='PEM')
+
+ (cli, cli_args) = parser.parse_args(sys.argv[1:])
+
+ if len(cli_args) != 1:
+ parser.print_help()
+ raise SystemExit(1)
+
+ try:
+ keysize = int(cli_args[0])
+ except ValueError:
+ parser.print_help()
+ print('Not a valid number: %s' % cli_args[0], file=sys.stderr)
+ raise SystemExit(1)
+
+ print('Generating %i-bit key' % keysize, file=sys.stderr)
+ (pub_key, priv_key) = rsa.newkeys(keysize)
+
+
+ # Save public key
+ if cli.pubout:
+ print('Writing public key to %s' % cli.pubout, file=sys.stderr)
+ data = pub_key.save_pkcs1(format=cli.form)
+ with open(cli.pubout, 'wb') as outfile:
+ outfile.write(data)
+
+ # Save private key
+ data = priv_key.save_pkcs1(format=cli.form)
+
+ if cli.out:
+ print('Writing private key to %s' % cli.out, file=sys.stderr)
+ with open(cli.out, 'wb') as outfile:
+ outfile.write(data)
+ else:
+ print('Writing private key to stdout', file=sys.stderr)
+ sys.stdout.write(data)
+
+
+class CryptoOperation(object):
+ '''CLI callable that operates with input, output, and a key.'''
+
+ __metaclass__ = abc.ABCMeta
+
+ keyname = 'public' # or 'private'
+ usage = 'usage: %%prog [options] %(keyname)s_key'
+ description = None
+ operation = 'decrypt'
+ operation_past = 'decrypted'
+ operation_progressive = 'decrypting'
+ input_help = 'Name of the file to %(operation)s. Reads from stdin if ' \
+ 'not specified.'
+ output_help = 'Name of the file to write the %(operation_past)s file ' \
+ 'to. Written to stdout if this option is not present.'
+ expected_cli_args = 1
+ has_output = True
+
+ key_class = rsa.PublicKey
+
+ def __init__(self):
+ self.usage = self.usage % self.__class__.__dict__
+ self.input_help = self.input_help % self.__class__.__dict__
+ self.output_help = self.output_help % self.__class__.__dict__
+
+ @abc.abstractmethod
+ def perform_operation(self, indata, key, cli_args=None):
+ '''Performs the program's operation.
+
+ Implement in a subclass.
+
+ :returns: the data to write to the output.
+ '''
+
+ def __call__(self):
+ '''Runs the program.'''
+
+ (cli, cli_args) = self.parse_cli()
+
+ key = self.read_key(cli_args[0], cli.keyform)
+
+ indata = self.read_infile(cli.input)
+
+ print(self.operation_progressive.title(), file=sys.stderr)
+ outdata = self.perform_operation(indata, key, cli_args)
+
+ if self.has_output:
+ self.write_outfile(outdata, cli.output)
+
+ def parse_cli(self):
+ '''Parse the CLI options
+
+ :returns: (cli_opts, cli_args)
+ '''
+
+ parser = OptionParser(usage=self.usage, description=self.description)
+
+ parser.add_option('-i', '--input', type='string', help=self.input_help)
+
+ if self.has_output:
+ parser.add_option('-o', '--output', type='string', help=self.output_help)
+
+ parser.add_option('--keyform',
+ help='Key format of the %s key - default PEM' % self.keyname,
+ choices=('PEM', 'DER'), default='PEM')
+
+ (cli, cli_args) = parser.parse_args(sys.argv[1:])
+
+ if len(cli_args) != self.expected_cli_args:
+ parser.print_help()
+ raise SystemExit(1)
+
+ return (cli, cli_args)
+
+ def read_key(self, filename, keyform):
+ '''Reads a public or private key.'''
+
+ print('Reading %s key from %s' % (self.keyname, filename), file=sys.stderr)
+ with open(filename, 'rb') as keyfile:
+ keydata = keyfile.read()
+
+ return self.key_class.load_pkcs1(keydata, keyform)
+
+ def read_infile(self, inname):
+ '''Read the input file'''
+
+ if inname:
+ print('Reading input from %s' % inname, file=sys.stderr)
+ with open(inname, 'rb') as infile:
+ return infile.read()
+
+ print('Reading input from stdin', file=sys.stderr)
+ return sys.stdin.read()
+
+ def write_outfile(self, outdata, outname):
+ '''Write the output file'''
+
+ if outname:
+ print('Writing output to %s' % outname, file=sys.stderr)
+ with open(outname, 'wb') as outfile:
+ outfile.write(outdata)
+ else:
+ print('Writing output to stdout', file=sys.stderr)
+ sys.stdout.write(outdata)
+
+class EncryptOperation(CryptoOperation):
+ '''Encrypts a file.'''
+
+ keyname = 'public'
+ description = ('Encrypts a file. The file must be shorter than the key '
+ 'length in order to be encrypted. For larger files, use the '
+ 'pyrsa-encrypt-bigfile command.')
+ operation = 'encrypt'
+ operation_past = 'encrypted'
+ operation_progressive = 'encrypting'
+
+
+ def perform_operation(self, indata, pub_key, cli_args=None):
+ '''Encrypts files.'''
+
+ return rsa.encrypt(indata, pub_key)
+
+class DecryptOperation(CryptoOperation):
+ '''Decrypts a file.'''
+
+ keyname = 'private'
+ description = ('Decrypts a file. The original file must be shorter than '
+ 'the key length in order to have been encrypted. For larger '
+ 'files, use the pyrsa-decrypt-bigfile command.')
+ operation = 'decrypt'
+ operation_past = 'decrypted'
+ operation_progressive = 'decrypting'
+ key_class = rsa.PrivateKey
+
+ def perform_operation(self, indata, priv_key, cli_args=None):
+ '''Decrypts files.'''
+
+ return rsa.decrypt(indata, priv_key)
+
+class SignOperation(CryptoOperation):
+ '''Signs a file.'''
+
+ keyname = 'private'
+ usage = 'usage: %%prog [options] private_key hash_method'
+ description = ('Signs a file, outputs the signature. Choose the hash '
+ 'method from %s' % ', '.join(HASH_METHODS))
+ operation = 'sign'
+ operation_past = 'signature'
+ operation_progressive = 'Signing'
+ key_class = rsa.PrivateKey
+ expected_cli_args = 2
+
+ output_help = ('Name of the file to write the signature to. Written '
+ 'to stdout if this option is not present.')
+
+ def perform_operation(self, indata, priv_key, cli_args):
+ '''Decrypts files.'''
+
+ hash_method = cli_args[1]
+ if hash_method not in HASH_METHODS:
+ raise SystemExit('Invalid hash method, choose one of %s' %
+ ', '.join(HASH_METHODS))
+
+ return rsa.sign(indata, priv_key, hash_method)
+
+class VerifyOperation(CryptoOperation):
+ '''Verify a signature.'''
+
+ keyname = 'public'
+ usage = 'usage: %%prog [options] private_key signature_file'
+ description = ('Verifies a signature, exits with status 0 upon success, '
+ 'prints an error message and exits with status 1 upon error.')
+ operation = 'verify'
+ operation_past = 'verified'
+ operation_progressive = 'Verifying'
+ key_class = rsa.PublicKey
+ expected_cli_args = 2
+ has_output = False
+
+ def perform_operation(self, indata, pub_key, cli_args):
+ '''Decrypts files.'''
+
+ signature_file = cli_args[1]
+
+ with open(signature_file, 'rb') as sigfile:
+ signature = sigfile.read()
+
+ try:
+ rsa.verify(indata, signature, pub_key)
+ except rsa.VerificationError:
+ raise SystemExit('Verification failed.')
+
+ print('Verification OK', file=sys.stderr)
+
+
+class BigfileOperation(CryptoOperation):
+ '''CryptoOperation that doesn't read the entire file into memory.'''
+
+ def __init__(self):
+ CryptoOperation.__init__(self)
+
+ self.file_objects = []
+
+ def __del__(self):
+ '''Closes any open file handles.'''
+
+ for fobj in self.file_objects:
+ fobj.close()
+
+ def __call__(self):
+ '''Runs the program.'''
+
+ (cli, cli_args) = self.parse_cli()
+
+ key = self.read_key(cli_args[0], cli.keyform)
+
+ # Get the file handles
+ infile = self.get_infile(cli.input)
+ outfile = self.get_outfile(cli.output)
+
+ # Call the operation
+ print(self.operation_progressive.title(), file=sys.stderr)
+ self.perform_operation(infile, outfile, key, cli_args)
+
+ def get_infile(self, inname):
+ '''Returns the input file object'''
+
+ if inname:
+ print('Reading input from %s' % inname, file=sys.stderr)
+ fobj = open(inname, 'rb')
+ self.file_objects.append(fobj)
+ else:
+ print('Reading input from stdin', file=sys.stderr)
+ fobj = sys.stdin
+
+ return fobj
+
+ def get_outfile(self, outname):
+ '''Returns the output file object'''
+
+ if outname:
+ print('Will write output to %s' % outname, file=sys.stderr)
+ fobj = open(outname, 'wb')
+ self.file_objects.append(fobj)
+ else:
+ print('Will write output to stdout', file=sys.stderr)
+ fobj = sys.stdout
+
+ return fobj
+
+class EncryptBigfileOperation(BigfileOperation):
+ '''Encrypts a file to VARBLOCK format.'''
+
+ keyname = 'public'
+ description = ('Encrypts a file to an encrypted VARBLOCK file. The file '
+ 'can be larger than the key length, but the output file is only '
+ 'compatible with Python-RSA.')
+ operation = 'encrypt'
+ operation_past = 'encrypted'
+ operation_progressive = 'encrypting'
+
+ def perform_operation(self, infile, outfile, pub_key, cli_args=None):
+ '''Encrypts files to VARBLOCK.'''
+
+ return rsa.bigfile.encrypt_bigfile(infile, outfile, pub_key)
+
+class DecryptBigfileOperation(BigfileOperation):
+ '''Decrypts a file in VARBLOCK format.'''
+
+ keyname = 'private'
+ description = ('Decrypts an encrypted VARBLOCK file that was encrypted '
+ 'with pyrsa-encrypt-bigfile')
+ operation = 'decrypt'
+ operation_past = 'decrypted'
+ operation_progressive = 'decrypting'
+ key_class = rsa.PrivateKey
+
+ def perform_operation(self, infile, outfile, priv_key, cli_args=None):
+ '''Decrypts a VARBLOCK file.'''
+
+ return rsa.bigfile.decrypt_bigfile(infile, outfile, priv_key)
+
+
+encrypt = EncryptOperation()
+decrypt = DecryptOperation()
+sign = SignOperation()
+verify = VerifyOperation()
+encrypt_bigfile = EncryptBigfileOperation()
+decrypt_bigfile = DecryptBigfileOperation()
+
diff --git a/libs/rsa/common.py b/libs/rsa/common.py
new file mode 100644
index 00000000..39feb8c2
--- /dev/null
+++ b/libs/rsa/common.py
@@ -0,0 +1,185 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Sybren A. Stüvel
+#
+# 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.
+
+'''Common functionality shared by several modules.'''
+
+
+def bit_size(num):
+ '''
+ Number of bits needed to represent a integer excluding any prefix
+ 0 bits.
+
+ As per definition from http://wiki.python.org/moin/BitManipulation and
+ to match the behavior of the Python 3 API.
+
+ Usage::
+
+ >>> bit_size(1023)
+ 10
+ >>> bit_size(1024)
+ 11
+ >>> bit_size(1025)
+ 11
+
+ :param num:
+ Integer value. If num is 0, returns 0. Only the absolute value of the
+ number is considered. Therefore, signed integers will be abs(num)
+ before the number's bit length is determined.
+ :returns:
+ Returns the number of bits in the integer.
+ '''
+ if num == 0:
+ return 0
+ if num < 0:
+ num = -num
+
+ # Make sure this is an int and not a float.
+ num & 1
+
+ hex_num = "%x" % num
+ return ((len(hex_num) - 1) * 4) + {
+ '0':0, '1':1, '2':2, '3':2,
+ '4':3, '5':3, '6':3, '7':3,
+ '8':4, '9':4, 'a':4, 'b':4,
+ 'c':4, 'd':4, 'e':4, 'f':4,
+ }[hex_num[0]]
+
+
+def _bit_size(number):
+ '''
+ Returns the number of bits required to hold a specific long number.
+ '''
+ if number < 0:
+ raise ValueError('Only nonnegative numbers possible: %s' % number)
+
+ if number == 0:
+ return 0
+
+ # This works, even with very large numbers. When using math.log(number, 2),
+ # you'll get rounding errors and it'll fail.
+ bits = 0
+ while number:
+ bits += 1
+ number >>= 1
+
+ return bits
+
+
+def byte_size(number):
+ '''
+ Returns the number of bytes required to hold a specific long number.
+
+ The number of bytes is rounded up.
+
+ Usage::
+
+ >>> byte_size(1 << 1023)
+ 128
+ >>> byte_size((1 << 1024) - 1)
+ 128
+ >>> byte_size(1 << 1024)
+ 129
+
+ :param number:
+ An unsigned integer
+ :returns:
+ The number of bytes required to hold a specific long number.
+ '''
+ quanta, mod = divmod(bit_size(number), 8)
+ if mod or number == 0:
+ quanta += 1
+ return quanta
+ #return int(math.ceil(bit_size(number) / 8.0))
+
+
+def extended_gcd(a, b):
+ '''Returns a tuple (r, i, j) such that r = gcd(a, b) = ia + jb
+ '''
+ # r = gcd(a,b) i = multiplicitive inverse of a mod b
+ # or j = multiplicitive inverse of b mod a
+ # Neg return values for i or j are made positive mod b or a respectively
+ # Iterateive Version is faster and uses much less stack space
+ x = 0
+ y = 1
+ lx = 1
+ ly = 0
+ oa = a #Remember original a/b to remove
+ ob = b #negative values from return results
+ while b != 0:
+ q = a // b
+ (a, b) = (b, a % b)
+ (x, lx) = ((lx - (q * x)),x)
+ (y, ly) = ((ly - (q * y)),y)
+ if (lx < 0): lx += ob #If neg wrap modulo orignal b
+ if (ly < 0): ly += oa #If neg wrap modulo orignal a
+ return (a, lx, ly) #Return only positive values
+
+
+def inverse(x, n):
+ '''Returns x^-1 (mod n)
+
+ >>> inverse(7, 4)
+ 3
+ >>> (inverse(143, 4) * 143) % 4
+ 1
+ '''
+
+ (divider, inv, _) = extended_gcd(x, n)
+
+ if divider != 1:
+ raise ValueError("x (%d) and n (%d) are not relatively prime" % (x, n))
+
+ return inv
+
+
+def crt(a_values, modulo_values):
+ '''Chinese Remainder Theorem.
+
+ Calculates x such that x = a[i] (mod m[i]) for each i.
+
+ :param a_values: the a-values of the above equation
+ :param modulo_values: the m-values of the above equation
+ :returns: x such that x = a[i] (mod m[i]) for each i
+
+
+ >>> crt([2, 3], [3, 5])
+ 8
+
+ >>> crt([2, 3, 2], [3, 5, 7])
+ 23
+
+ >>> crt([2, 3, 0], [7, 11, 15])
+ 135
+ '''
+
+ m = 1
+ x = 0
+
+ for modulo in modulo_values:
+ m *= modulo
+
+ for (m_i, a_i) in zip(modulo_values, a_values):
+ M_i = m // m_i
+ inv = inverse(M_i, m_i)
+
+ x = (x + a_i * M_i * inv) % m
+
+ return x
+
+if __name__ == '__main__':
+ import doctest
+ doctest.testmod()
+
diff --git a/libs/rsa/core.py b/libs/rsa/core.py
new file mode 100644
index 00000000..90dfee8e
--- /dev/null
+++ b/libs/rsa/core.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Sybren A. Stüvel
+#
+# 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.
+
+'''Core mathematical operations.
+
+This is the actual core RSA implementation, which is only defined
+mathematically on integers.
+'''
+
+
+from rsa._compat import is_integer
+
+def assert_int(var, name):
+
+ if is_integer(var):
+ return
+
+ raise TypeError('%s should be an integer, not %s' % (name, var.__class__))
+
+def encrypt_int(message, ekey, n):
+ '''Encrypts a message using encryption key 'ekey', working modulo n'''
+
+ assert_int(message, 'message')
+ assert_int(ekey, 'ekey')
+ assert_int(n, 'n')
+
+ if message < 0:
+ raise ValueError('Only non-negative numbers are supported')
+
+ if message > n:
+ raise OverflowError("The message %i is too long for n=%i" % (message, n))
+
+ return pow(message, ekey, n)
+
+def decrypt_int(cyphertext, dkey, n):
+ '''Decrypts a cypher text using the decryption key 'dkey', working
+ modulo n'''
+
+ assert_int(cyphertext, 'cyphertext')
+ assert_int(dkey, 'dkey')
+ assert_int(n, 'n')
+
+ message = pow(cyphertext, dkey, n)
+ return message
+
diff --git a/libs/rsa/key.py b/libs/rsa/key.py
new file mode 100644
index 00000000..3870541a
--- /dev/null
+++ b/libs/rsa/key.py
@@ -0,0 +1,581 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Sybren A. Stüvel
+#
+# 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.
+
+'''RSA key generation code.
+
+Create new keys with the newkeys() function. It will give you a PublicKey and a
+PrivateKey object.
+
+Loading and saving keys requires the pyasn1 module. This module is imported as
+late as possible, such that other functionality will remain working in absence
+of pyasn1.
+
+'''
+
+import logging
+from rsa._compat import b
+
+import rsa.prime
+import rsa.pem
+import rsa.common
+
+log = logging.getLogger(__name__)
+
+class AbstractKey(object):
+ '''Abstract superclass for private and public keys.'''
+
+ @classmethod
+ def load_pkcs1(cls, keyfile, format='PEM'):
+ r'''Loads a key in PKCS#1 DER or PEM format.
+
+ :param keyfile: contents of a DER- or PEM-encoded file that contains
+ the public key.
+ :param format: the format of the file to load; 'PEM' or 'DER'
+
+ :return: a PublicKey object
+
+ '''
+
+ methods = {
+ 'PEM': cls._load_pkcs1_pem,
+ 'DER': cls._load_pkcs1_der,
+ }
+
+ if format not in methods:
+ formats = ', '.join(sorted(methods.keys()))
+ raise ValueError('Unsupported format: %r, try one of %s' % (format,
+ formats))
+
+ method = methods[format]
+ return method(keyfile)
+
+ def save_pkcs1(self, format='PEM'):
+ '''Saves the public key in PKCS#1 DER or PEM format.
+
+ :param format: the format to save; 'PEM' or 'DER'
+ :returns: the DER- or PEM-encoded public key.
+
+ '''
+
+ methods = {
+ 'PEM': self._save_pkcs1_pem,
+ 'DER': self._save_pkcs1_der,
+ }
+
+ if format not in methods:
+ formats = ', '.join(sorted(methods.keys()))
+ raise ValueError('Unsupported format: %r, try one of %s' % (format,
+ formats))
+
+ method = methods[format]
+ return method()
+
+class PublicKey(AbstractKey):
+ '''Represents a public RSA key.
+
+ This key is also known as the 'encryption key'. It contains the 'n' and 'e'
+ values.
+
+ Supports attributes as well as dictionary-like access. Attribute accesss is
+ faster, though.
+
+ >>> PublicKey(5, 3)
+ PublicKey(5, 3)
+
+ >>> key = PublicKey(5, 3)
+ >>> key.n
+ 5
+ >>> key['n']
+ 5
+ >>> key.e
+ 3
+ >>> key['e']
+ 3
+
+ '''
+
+ __slots__ = ('n', 'e')
+
+ def __init__(self, n, e):
+ self.n = n
+ self.e = e
+
+ def __getitem__(self, key):
+ return getattr(self, key)
+
+ def __repr__(self):
+ return 'PublicKey(%i, %i)' % (self.n, self.e)
+
+ def __eq__(self, other):
+ if other is None:
+ return False
+
+ if not isinstance(other, PublicKey):
+ return False
+
+ return self.n == other.n and self.e == other.e
+
+ def __ne__(self, other):
+ return not (self == other)
+
+ @classmethod
+ def _load_pkcs1_der(cls, keyfile):
+ r'''Loads a key in PKCS#1 DER format.
+
+ @param keyfile: contents of a DER-encoded file that contains the public
+ key.
+ @return: a PublicKey object
+
+ First let's construct a DER encoded key:
+
+ >>> import base64
+ >>> b64der = 'MAwCBQCNGmYtAgMBAAE='
+ >>> der = base64.decodestring(b64der)
+
+ This loads the file:
+
+ >>> PublicKey._load_pkcs1_der(der)
+ PublicKey(2367317549, 65537)
+
+ '''
+
+ from pyasn1.codec.der import decoder
+ (priv, _) = decoder.decode(keyfile)
+
+ # ASN.1 contents of DER encoded public key:
+ #
+ # RSAPublicKey ::= SEQUENCE {
+ # modulus INTEGER, -- n
+ # publicExponent INTEGER, -- e
+
+ as_ints = tuple(int(x) for x in priv)
+ return cls(*as_ints)
+
+ def _save_pkcs1_der(self):
+ '''Saves the public key in PKCS#1 DER format.
+
+ @returns: the DER-encoded public key.
+ '''
+
+ from pyasn1.type import univ, namedtype
+ from pyasn1.codec.der import encoder
+
+ class AsnPubKey(univ.Sequence):
+ componentType = namedtype.NamedTypes(
+ namedtype.NamedType('modulus', univ.Integer()),
+ namedtype.NamedType('publicExponent', univ.Integer()),
+ )
+
+ # Create the ASN object
+ asn_key = AsnPubKey()
+ asn_key.setComponentByName('modulus', self.n)
+ asn_key.setComponentByName('publicExponent', self.e)
+
+ return encoder.encode(asn_key)
+
+ @classmethod
+ def _load_pkcs1_pem(cls, keyfile):
+ '''Loads a PKCS#1 PEM-encoded public key file.
+
+ The contents of the file before the "-----BEGIN RSA PUBLIC KEY-----" and
+ after the "-----END RSA PUBLIC KEY-----" lines is ignored.
+
+ @param keyfile: contents of a PEM-encoded file that contains the public
+ key.
+ @return: a PublicKey object
+ '''
+
+ der = rsa.pem.load_pem(keyfile, 'RSA PUBLIC KEY')
+ return cls._load_pkcs1_der(der)
+
+ def _save_pkcs1_pem(self):
+ '''Saves a PKCS#1 PEM-encoded public key file.
+
+ @return: contents of a PEM-encoded file that contains the public key.
+ '''
+
+ der = self._save_pkcs1_der()
+ return rsa.pem.save_pem(der, 'RSA PUBLIC KEY')
+
+class PrivateKey(AbstractKey):
+ '''Represents a private RSA key.
+
+ This key is also known as the 'decryption key'. It contains the 'n', 'e',
+ 'd', 'p', 'q' and other values.
+
+ Supports attributes as well as dictionary-like access. Attribute accesss is
+ faster, though.
+
+ >>> PrivateKey(3247, 65537, 833, 191, 17)
+ PrivateKey(3247, 65537, 833, 191, 17)
+
+ exp1, exp2 and coef don't have to be given, they will be calculated:
+
+ >>> pk = PrivateKey(3727264081, 65537, 3349121513, 65063, 57287)
+ >>> pk.exp1
+ 55063
+ >>> pk.exp2
+ 10095
+ >>> pk.coef
+ 50797
+
+ If you give exp1, exp2 or coef, they will be used as-is:
+
+ >>> pk = PrivateKey(1, 2, 3, 4, 5, 6, 7, 8)
+ >>> pk.exp1
+ 6
+ >>> pk.exp2
+ 7
+ >>> pk.coef
+ 8
+
+ '''
+
+ __slots__ = ('n', 'e', 'd', 'p', 'q', 'exp1', 'exp2', 'coef')
+
+ def __init__(self, n, e, d, p, q, exp1=None, exp2=None, coef=None):
+ self.n = n
+ self.e = e
+ self.d = d
+ self.p = p
+ self.q = q
+
+ # Calculate the other values if they aren't supplied
+ if exp1 is None:
+ self.exp1 = int(d % (p - 1))
+ else:
+ self.exp1 = exp1
+
+ if exp1 is None:
+ self.exp2 = int(d % (q - 1))
+ else:
+ self.exp2 = exp2
+
+ if coef is None:
+ self.coef = rsa.common.inverse(q, p)
+ else:
+ self.coef = coef
+
+ def __getitem__(self, key):
+ return getattr(self, key)
+
+ def __repr__(self):
+ return 'PrivateKey(%(n)i, %(e)i, %(d)i, %(p)i, %(q)i)' % self
+
+ def __eq__(self, other):
+ if other is None:
+ return False
+
+ if not isinstance(other, PrivateKey):
+ return False
+
+ return (self.n == other.n and
+ self.e == other.e and
+ self.d == other.d and
+ self.p == other.p and
+ self.q == other.q and
+ self.exp1 == other.exp1 and
+ self.exp2 == other.exp2 and
+ self.coef == other.coef)
+
+ def __ne__(self, other):
+ return not (self == other)
+
+ @classmethod
+ def _load_pkcs1_der(cls, keyfile):
+ r'''Loads a key in PKCS#1 DER format.
+
+ @param keyfile: contents of a DER-encoded file that contains the private
+ key.
+ @return: a PrivateKey object
+
+ First let's construct a DER encoded key:
+
+ >>> import base64
+ >>> b64der = 'MC4CAQACBQDeKYlRAgMBAAECBQDHn4npAgMA/icCAwDfxwIDANcXAgInbwIDAMZt'
+ >>> der = base64.decodestring(b64der)
+
+ This loads the file:
+
+ >>> PrivateKey._load_pkcs1_der(der)
+ PrivateKey(3727264081, 65537, 3349121513, 65063, 57287)
+
+ '''
+
+ from pyasn1.codec.der import decoder
+ (priv, _) = decoder.decode(keyfile)
+
+ # ASN.1 contents of DER encoded private key:
+ #
+ # RSAPrivateKey ::= SEQUENCE {
+ # version Version,
+ # modulus INTEGER, -- n
+ # publicExponent INTEGER, -- e
+ # privateExponent INTEGER, -- d
+ # prime1 INTEGER, -- p
+ # prime2 INTEGER, -- q
+ # exponent1 INTEGER, -- d mod (p-1)
+ # exponent2 INTEGER, -- d mod (q-1)
+ # coefficient INTEGER, -- (inverse of q) mod p
+ # otherPrimeInfos OtherPrimeInfos OPTIONAL
+ # }
+
+ if priv[0] != 0:
+ raise ValueError('Unable to read this file, version %s != 0' % priv[0])
+
+ as_ints = tuple(int(x) for x in priv[1:9])
+ return cls(*as_ints)
+
+ def _save_pkcs1_der(self):
+ '''Saves the private key in PKCS#1 DER format.
+
+ @returns: the DER-encoded private key.
+ '''
+
+ from pyasn1.type import univ, namedtype
+ from pyasn1.codec.der import encoder
+
+ class AsnPrivKey(univ.Sequence):
+ componentType = namedtype.NamedTypes(
+ namedtype.NamedType('version', univ.Integer()),
+ namedtype.NamedType('modulus', univ.Integer()),
+ namedtype.NamedType('publicExponent', univ.Integer()),
+ namedtype.NamedType('privateExponent', univ.Integer()),
+ namedtype.NamedType('prime1', univ.Integer()),
+ namedtype.NamedType('prime2', univ.Integer()),
+ namedtype.NamedType('exponent1', univ.Integer()),
+ namedtype.NamedType('exponent2', univ.Integer()),
+ namedtype.NamedType('coefficient', univ.Integer()),
+ )
+
+ # Create the ASN object
+ asn_key = AsnPrivKey()
+ asn_key.setComponentByName('version', 0)
+ asn_key.setComponentByName('modulus', self.n)
+ asn_key.setComponentByName('publicExponent', self.e)
+ asn_key.setComponentByName('privateExponent', self.d)
+ asn_key.setComponentByName('prime1', self.p)
+ asn_key.setComponentByName('prime2', self.q)
+ asn_key.setComponentByName('exponent1', self.exp1)
+ asn_key.setComponentByName('exponent2', self.exp2)
+ asn_key.setComponentByName('coefficient', self.coef)
+
+ return encoder.encode(asn_key)
+
+ @classmethod
+ def _load_pkcs1_pem(cls, keyfile):
+ '''Loads a PKCS#1 PEM-encoded private key file.
+
+ The contents of the file before the "-----BEGIN RSA PRIVATE KEY-----" and
+ after the "-----END RSA PRIVATE KEY-----" lines is ignored.
+
+ @param keyfile: contents of a PEM-encoded file that contains the private
+ key.
+ @return: a PrivateKey object
+ '''
+
+ der = rsa.pem.load_pem(keyfile, b('RSA PRIVATE KEY'))
+ return cls._load_pkcs1_der(der)
+
+ def _save_pkcs1_pem(self):
+ '''Saves a PKCS#1 PEM-encoded private key file.
+
+ @return: contents of a PEM-encoded file that contains the private key.
+ '''
+
+ der = self._save_pkcs1_der()
+ return rsa.pem.save_pem(der, b('RSA PRIVATE KEY'))
+
+def find_p_q(nbits, getprime_func=rsa.prime.getprime, accurate=True):
+ ''''Returns a tuple of two different primes of nbits bits each.
+
+ The resulting p * q has exacty 2 * nbits bits, and the returned p and q
+ will not be equal.
+
+ :param nbits: the number of bits in each of p and q.
+ :param getprime_func: the getprime function, defaults to
+ :py:func:`rsa.prime.getprime`.
+
+ *Introduced in Python-RSA 3.1*
+
+ :param accurate: whether to enable accurate mode or not.
+ :returns: (p, q), where p > q
+
+ >>> (p, q) = find_p_q(128)
+ >>> from rsa import common
+ >>> common.bit_size(p * q)
+ 256
+
+ When not in accurate mode, the number of bits can be slightly less
+
+ >>> (p, q) = find_p_q(128, accurate=False)
+ >>> from rsa import common
+ >>> common.bit_size(p * q) <= 256
+ True
+ >>> common.bit_size(p * q) > 240
+ True
+
+ '''
+
+ total_bits = nbits * 2
+
+ # Make sure that p and q aren't too close or the factoring programs can
+ # factor n.
+ shift = nbits // 16
+ pbits = nbits + shift
+ qbits = nbits - shift
+
+ # Choose the two initial primes
+ log.debug('find_p_q(%i): Finding p', nbits)
+ p = getprime_func(pbits)
+ log.debug('find_p_q(%i): Finding q', nbits)
+ q = getprime_func(qbits)
+
+ def is_acceptable(p, q):
+ '''Returns True iff p and q are acceptable:
+
+ - p and q differ
+ - (p * q) has the right nr of bits (when accurate=True)
+ '''
+
+ if p == q:
+ return False
+
+ if not accurate:
+ return True
+
+ # Make sure we have just the right amount of bits
+ found_size = rsa.common.bit_size(p * q)
+ return total_bits == found_size
+
+ # Keep choosing other primes until they match our requirements.
+ change_p = False
+ while not is_acceptable(p, q):
+ # Change p on one iteration and q on the other
+ if change_p:
+ p = getprime_func(pbits)
+ else:
+ q = getprime_func(qbits)
+
+ change_p = not change_p
+
+ # We want p > q as described on
+ # http://www.di-mgt.com.au/rsa_alg.html#crt
+ return (max(p, q), min(p, q))
+
+def calculate_keys(p, q, nbits):
+ '''Calculates an encryption and a decryption key given p and q, and
+ returns them as a tuple (e, d)
+
+ '''
+
+ phi_n = (p - 1) * (q - 1)
+
+ # A very common choice for e is 65537
+ e = 65537
+
+ try:
+ d = rsa.common.inverse(e, phi_n)
+ except ValueError:
+ raise ValueError("e (%d) and phi_n (%d) are not relatively prime" %
+ (e, phi_n))
+
+ if (e * d) % phi_n != 1:
+ raise ValueError("e (%d) and d (%d) are not mult. inv. modulo "
+ "phi_n (%d)" % (e, d, phi_n))
+
+ return (e, d)
+
+def gen_keys(nbits, getprime_func, accurate=True):
+ '''Generate RSA keys of nbits bits. Returns (p, q, e, d).
+
+ Note: this can take a long time, depending on the key size.
+
+ :param nbits: the total number of bits in ``p`` and ``q``. Both ``p`` and
+ ``q`` will use ``nbits/2`` bits.
+ :param getprime_func: either :py:func:`rsa.prime.getprime` or a function
+ with similar signature.
+ '''
+
+ (p, q) = find_p_q(nbits // 2, getprime_func, accurate)
+ (e, d) = calculate_keys(p, q, nbits // 2)
+
+ return (p, q, e, d)
+
+def newkeys(nbits, accurate=True, poolsize=1):
+ '''Generates public and private keys, and returns them as (pub, priv).
+
+ The public key is also known as the 'encryption key', and is a
+ :py:class:`rsa.PublicKey` object. The private key is also known as the
+ 'decryption key' and is a :py:class:`rsa.PrivateKey` object.
+
+ :param nbits: the number of bits required to store ``n = p*q``.
+ :param accurate: when True, ``n`` will have exactly the number of bits you
+ asked for. However, this makes key generation much slower. When False,
+ `n`` may have slightly less bits.
+ :param poolsize: the number of processes to use to generate the prime
+ numbers. If set to a number > 1, a parallel algorithm will be used.
+ This requires Python 2.6 or newer.
+
+ :returns: a tuple (:py:class:`rsa.PublicKey`, :py:class:`rsa.PrivateKey`)
+
+ The ``poolsize`` parameter was added in *Python-RSA 3.1* and requires
+ Python 2.6 or newer.
+
+ '''
+
+ if nbits < 16:
+ raise ValueError('Key too small')
+
+ if poolsize < 1:
+ raise ValueError('Pool size (%i) should be >= 1' % poolsize)
+
+ # Determine which getprime function to use
+ if poolsize > 1:
+ from rsa import parallel
+ import functools
+
+ getprime_func = functools.partial(parallel.getprime, poolsize=poolsize)
+ else: getprime_func = rsa.prime.getprime
+
+ # Generate the key components
+ (p, q, e, d) = gen_keys(nbits, getprime_func)
+
+ # Create the key objects
+ n = p * q
+
+ return (
+ PublicKey(n, e),
+ PrivateKey(n, e, d, p, q)
+ )
+
+__all__ = ['PublicKey', 'PrivateKey', 'newkeys']
+
+if __name__ == '__main__':
+ import doctest
+
+ try:
+ for count in range(100):
+ (failures, tests) = doctest.testmod()
+ if failures:
+ break
+
+ if (count and count % 10 == 0) or count == 1:
+ print('%i times' % count)
+ except KeyboardInterrupt:
+ print('Aborted')
+ else:
+ print('Doctests done')
diff --git a/libs/rsa/parallel.py b/libs/rsa/parallel.py
new file mode 100644
index 00000000..e5034ac7
--- /dev/null
+++ b/libs/rsa/parallel.py
@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Sybren A. Stüvel
+#
+# 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.
+
+'''Functions for parallel computation on multiple cores.
+
+Introduced in Python-RSA 3.1.
+
+.. note::
+
+ Requires Python 2.6 or newer.
+
+'''
+
+from __future__ import print_function
+
+import multiprocessing as mp
+
+import rsa.prime
+import rsa.randnum
+
+def _find_prime(nbits, pipe):
+ while True:
+ integer = rsa.randnum.read_random_int(nbits)
+
+ # Make sure it's odd
+ integer |= 1
+
+ # Test for primeness
+ if rsa.prime.is_prime(integer):
+ pipe.send(integer)
+ return
+
+def getprime(nbits, poolsize):
+ '''Returns a prime number that can be stored in 'nbits' bits.
+
+ Works in multiple threads at the same time.
+
+ >>> p = getprime(128, 3)
+ >>> rsa.prime.is_prime(p-1)
+ False
+ >>> rsa.prime.is_prime(p)
+ True
+ >>> rsa.prime.is_prime(p+1)
+ False
+
+ >>> from rsa import common
+ >>> common.bit_size(p) == 128
+ True
+
+ '''
+
+ (pipe_recv, pipe_send) = mp.Pipe(duplex=False)
+
+ # Create processes
+ procs = [mp.Process(target=_find_prime, args=(nbits, pipe_send))
+ for _ in range(poolsize)]
+ [p.start() for p in procs]
+
+ result = pipe_recv.recv()
+
+ [p.terminate() for p in procs]
+
+ return result
+
+__all__ = ['getprime']
+
+
+if __name__ == '__main__':
+ print('Running doctests 1000x or until failure')
+ import doctest
+
+ for count in range(100):
+ (failures, tests) = doctest.testmod()
+ if failures:
+ break
+
+ if count and count % 10 == 0:
+ print('%i times' % count)
+
+ print('Doctests done')
+
diff --git a/libs/rsa/pem.py b/libs/rsa/pem.py
new file mode 100644
index 00000000..b1c3a0ed
--- /dev/null
+++ b/libs/rsa/pem.py
@@ -0,0 +1,120 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Sybren A. Stüvel
+#
+# 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.
+
+'''Functions that load and write PEM-encoded files.'''
+
+import base64
+from rsa._compat import b, is_bytes
+
+def _markers(pem_marker):
+ '''
+ Returns the start and end PEM markers
+ '''
+
+ if is_bytes(pem_marker):
+ pem_marker = pem_marker.decode('utf-8')
+
+ return (b('-----BEGIN %s-----' % pem_marker),
+ b('-----END %s-----' % pem_marker))
+
+def load_pem(contents, pem_marker):
+ '''Loads a PEM file.
+
+ @param contents: the contents of the file to interpret
+ @param pem_marker: the marker of the PEM content, such as 'RSA PRIVATE KEY'
+ when your file has '-----BEGIN RSA PRIVATE KEY-----' and
+ '-----END RSA PRIVATE KEY-----' markers.
+
+ @return the base64-decoded content between the start and end markers.
+
+ @raise ValueError: when the content is invalid, for example when the start
+ marker cannot be found.
+
+ '''
+
+ (pem_start, pem_end) = _markers(pem_marker)
+
+ pem_lines = []
+ in_pem_part = False
+
+ for line in contents.splitlines():
+ line = line.strip()
+
+ # Skip empty lines
+ if not line:
+ continue
+
+ # Handle start marker
+ if line == pem_start:
+ if in_pem_part:
+ raise ValueError('Seen start marker "%s" twice' % pem_start)
+
+ in_pem_part = True
+ continue
+
+ # Skip stuff before first marker
+ if not in_pem_part:
+ continue
+
+ # Handle end marker
+ if in_pem_part and line == pem_end:
+ in_pem_part = False
+ break
+
+ # Load fields
+ if b(':') in line:
+ continue
+
+ pem_lines.append(line)
+
+ # Do some sanity checks
+ if not pem_lines:
+ raise ValueError('No PEM start marker "%s" found' % pem_start)
+
+ if in_pem_part:
+ raise ValueError('No PEM end marker "%s" found' % pem_end)
+
+ # Base64-decode the contents
+ pem = b('').join(pem_lines)
+ return base64.decodestring(pem)
+
+
+def save_pem(contents, pem_marker):
+ '''Saves a PEM file.
+
+ @param contents: the contents to encode in PEM format
+ @param pem_marker: the marker of the PEM content, such as 'RSA PRIVATE KEY'
+ when your file has '-----BEGIN RSA PRIVATE KEY-----' and
+ '-----END RSA PRIVATE KEY-----' markers.
+
+ @return the base64-encoded content between the start and end markers.
+
+ '''
+
+ (pem_start, pem_end) = _markers(pem_marker)
+
+ b64 = base64.encodestring(contents).replace(b('\n'), b(''))
+ pem_lines = [pem_start]
+
+ for block_start in range(0, len(b64), 64):
+ block = b64[block_start:block_start + 64]
+ pem_lines.append(block)
+
+ pem_lines.append(pem_end)
+ pem_lines.append(b(''))
+
+ return b('\n').join(pem_lines)
+
diff --git a/libs/rsa/pkcs1.py b/libs/rsa/pkcs1.py
new file mode 100644
index 00000000..1274fe39
--- /dev/null
+++ b/libs/rsa/pkcs1.py
@@ -0,0 +1,389 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Sybren A. Stüvel
+#
+# 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.
+
+'''Functions for PKCS#1 version 1.5 encryption and signing
+
+This module implements certain functionality from PKCS#1 version 1.5. For a
+very clear example, read http://www.di-mgt.com.au/rsa_alg.html#pkcs1schemes
+
+At least 8 bytes of random padding is used when encrypting a message. This makes
+these methods much more secure than the ones in the ``rsa`` module.
+
+WARNING: this module leaks information when decryption or verification fails.
+The exceptions that are raised contain the Python traceback information, which
+can be used to deduce where in the process the failure occurred. DO NOT PASS
+SUCH INFORMATION to your users.
+'''
+
+import hashlib
+import os
+
+from rsa._compat import b
+from rsa import common, transform, core, varblock
+
+# ASN.1 codes that describe the hash algorithm used.
+HASH_ASN1 = {
+ 'MD5': b('\x30\x20\x30\x0c\x06\x08\x2a\x86\x48\x86\xf7\x0d\x02\x05\x05\x00\x04\x10'),
+ 'SHA-1': b('\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14'),
+ 'SHA-256': b('\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20'),
+ 'SHA-384': b('\x30\x41\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x02\x05\x00\x04\x30'),
+ 'SHA-512': b('\x30\x51\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x03\x05\x00\x04\x40'),
+}
+
+HASH_METHODS = {
+ 'MD5': hashlib.md5,
+ 'SHA-1': hashlib.sha1,
+ 'SHA-256': hashlib.sha256,
+ 'SHA-384': hashlib.sha384,
+ 'SHA-512': hashlib.sha512,
+}
+
+class CryptoError(Exception):
+ '''Base class for all exceptions in this module.'''
+
+class DecryptionError(CryptoError):
+ '''Raised when decryption fails.'''
+
+class VerificationError(CryptoError):
+ '''Raised when verification fails.'''
+
+def _pad_for_encryption(message, target_length):
+ r'''Pads the message for encryption, returning the padded message.
+
+ :return: 00 02 RANDOM_DATA 00 MESSAGE
+
+ >>> block = _pad_for_encryption('hello', 16)
+ >>> len(block)
+ 16
+ >>> block[0:2]
+ '\x00\x02'
+ >>> block[-6:]
+ '\x00hello'
+
+ '''
+
+ max_msglength = target_length - 11
+ msglength = len(message)
+
+ if msglength > max_msglength:
+ raise OverflowError('%i bytes needed for message, but there is only'
+ ' space for %i' % (msglength, max_msglength))
+
+ # Get random padding
+ padding = b('')
+ padding_length = target_length - msglength - 3
+
+ # We remove 0-bytes, so we'll end up with less padding than we've asked for,
+ # so keep adding data until we're at the correct length.
+ while len(padding) < padding_length:
+ needed_bytes = padding_length - len(padding)
+
+ # Always read at least 8 bytes more than we need, and trim off the rest
+ # after removing the 0-bytes. This increases the chance of getting
+ # enough bytes, especially when needed_bytes is small
+ new_padding = os.urandom(needed_bytes + 5)
+ new_padding = new_padding.replace(b('\x00'), b(''))
+ padding = padding + new_padding[:needed_bytes]
+
+ assert len(padding) == padding_length
+
+ return b('').join([b('\x00\x02'),
+ padding,
+ b('\x00'),
+ message])
+
+
+def _pad_for_signing(message, target_length):
+ r'''Pads the message for signing, returning the padded message.
+
+ The padding is always a repetition of FF bytes.
+
+ :return: 00 01 PADDING 00 MESSAGE
+
+ >>> block = _pad_for_signing('hello', 16)
+ >>> len(block)
+ 16
+ >>> block[0:2]
+ '\x00\x01'
+ >>> block[-6:]
+ '\x00hello'
+ >>> block[2:-6]
+ '\xff\xff\xff\xff\xff\xff\xff\xff'
+
+ '''
+
+ max_msglength = target_length - 11
+ msglength = len(message)
+
+ if msglength > max_msglength:
+ raise OverflowError('%i bytes needed for message, but there is only'
+ ' space for %i' % (msglength, max_msglength))
+
+ padding_length = target_length - msglength - 3
+
+ return b('').join([b('\x00\x01'),
+ padding_length * b('\xff'),
+ b('\x00'),
+ message])
+
+
+def encrypt(message, pub_key):
+ '''Encrypts the given message using PKCS#1 v1.5
+
+ :param message: the message to encrypt. Must be a byte string no longer than
+ ``k-11`` bytes, where ``k`` is the number of bytes needed to encode
+ the ``n`` component of the public key.
+ :param pub_key: the :py:class:`rsa.PublicKey` to encrypt with.
+ :raise OverflowError: when the message is too large to fit in the padded
+ block.
+
+ >>> from rsa import key, common
+ >>> (pub_key, priv_key) = key.newkeys(256)
+ >>> message = 'hello'
+ >>> crypto = encrypt(message, pub_key)
+
+ The crypto text should be just as long as the public key 'n' component:
+
+ >>> len(crypto) == common.byte_size(pub_key.n)
+ True
+
+ '''
+
+ keylength = common.byte_size(pub_key.n)
+ padded = _pad_for_encryption(message, keylength)
+
+ payload = transform.bytes2int(padded)
+ encrypted = core.encrypt_int(payload, pub_key.e, pub_key.n)
+ block = transform.int2bytes(encrypted, keylength)
+
+ return block
+
+def decrypt(crypto, priv_key):
+ r'''Decrypts the given message using PKCS#1 v1.5
+
+ The decryption is considered 'failed' when the resulting cleartext doesn't
+ start with the bytes 00 02, or when the 00 byte between the padding and
+ the message cannot be found.
+
+ :param crypto: the crypto text as returned by :py:func:`rsa.encrypt`
+ :param priv_key: the :py:class:`rsa.PrivateKey` to decrypt with.
+ :raise DecryptionError: when the decryption fails. No details are given as
+ to why the code thinks the decryption fails, as this would leak
+ information about the private key.
+
+
+ >>> import rsa
+ >>> (pub_key, priv_key) = rsa.newkeys(256)
+
+ It works with strings:
+
+ >>> crypto = encrypt('hello', pub_key)
+ >>> decrypt(crypto, priv_key)
+ 'hello'
+
+ And with binary data:
+
+ >>> crypto = encrypt('\x00\x00\x00\x00\x01', pub_key)
+ >>> decrypt(crypto, priv_key)
+ '\x00\x00\x00\x00\x01'
+
+ Altering the encrypted information will *likely* cause a
+ :py:class:`rsa.pkcs1.DecryptionError`. If you want to be *sure*, use
+ :py:func:`rsa.sign`.
+
+
+ .. warning::
+
+ Never display the stack trace of a
+ :py:class:`rsa.pkcs1.DecryptionError` exception. It shows where in the
+ code the exception occurred, and thus leaks information about the key.
+ It's only a tiny bit of information, but every bit makes cracking the
+ keys easier.
+
+ >>> crypto = encrypt('hello', pub_key)
+ >>> crypto = crypto[0:5] + 'X' + crypto[6:] # change a byte
+ >>> decrypt(crypto, priv_key)
+ Traceback (most recent call last):
+ ...
+ DecryptionError: Decryption failed
+
+ '''
+
+ blocksize = common.byte_size(priv_key.n)
+ encrypted = transform.bytes2int(crypto)
+ decrypted = core.decrypt_int(encrypted, priv_key.d, priv_key.n)
+ cleartext = transform.int2bytes(decrypted, blocksize)
+
+ # If we can't find the cleartext marker, decryption failed.
+ if cleartext[0:2] != b('\x00\x02'):
+ raise DecryptionError('Decryption failed')
+
+ # Find the 00 separator between the padding and the message
+ try:
+ sep_idx = cleartext.index(b('\x00'), 2)
+ except ValueError:
+ raise DecryptionError('Decryption failed')
+
+ return cleartext[sep_idx+1:]
+
+def sign(message, priv_key, hash):
+ '''Signs the message with the private key.
+
+ Hashes the message, then signs the hash with the given key. This is known
+ as a "detached signature", because the message itself isn't altered.
+
+ :param message: the message to sign. Can be an 8-bit string or a file-like
+ object. If ``message`` has a ``read()`` method, it is assumed to be a
+ file-like object.
+ :param priv_key: the :py:class:`rsa.PrivateKey` to sign with
+ :param hash: the hash method used on the message. Use 'MD5', 'SHA-1',
+ 'SHA-256', 'SHA-384' or 'SHA-512'.
+ :return: a message signature block.
+ :raise OverflowError: if the private key is too small to contain the
+ requested hash.
+
+ '''
+
+ # Get the ASN1 code for this hash method
+ if hash not in HASH_ASN1:
+ raise ValueError('Invalid hash method: %s' % hash)
+ asn1code = HASH_ASN1[hash]
+
+ # Calculate the hash
+ hash = _hash(message, hash)
+
+ # Encrypt the hash with the private key
+ cleartext = asn1code + hash
+ keylength = common.byte_size(priv_key.n)
+ padded = _pad_for_signing(cleartext, keylength)
+
+ payload = transform.bytes2int(padded)
+ encrypted = core.encrypt_int(payload, priv_key.d, priv_key.n)
+ block = transform.int2bytes(encrypted, keylength)
+
+ return block
+
+def verify(message, signature, pub_key):
+ '''Verifies that the signature matches the message.
+
+ The hash method is detected automatically from the signature.
+
+ :param message: the signed message. Can be an 8-bit string or a file-like
+ object. If ``message`` has a ``read()`` method, it is assumed to be a
+ file-like object.
+ :param signature: the signature block, as created with :py:func:`rsa.sign`.
+ :param pub_key: the :py:class:`rsa.PublicKey` of the person signing the message.
+ :raise VerificationError: when the signature doesn't match the message.
+
+ .. warning::
+
+ Never display the stack trace of a
+ :py:class:`rsa.pkcs1.VerificationError` exception. It shows where in
+ the code the exception occurred, and thus leaks information about the
+ key. It's only a tiny bit of information, but every bit makes cracking
+ the keys easier.
+
+ '''
+
+ blocksize = common.byte_size(pub_key.n)
+ encrypted = transform.bytes2int(signature)
+ decrypted = core.decrypt_int(encrypted, pub_key.e, pub_key.n)
+ clearsig = transform.int2bytes(decrypted, blocksize)
+
+ # If we can't find the signature marker, verification failed.
+ if clearsig[0:2] != b('\x00\x01'):
+ raise VerificationError('Verification failed')
+
+ # Find the 00 separator between the padding and the payload
+ try:
+ sep_idx = clearsig.index(b('\x00'), 2)
+ except ValueError:
+ raise VerificationError('Verification failed')
+
+ # Get the hash and the hash method
+ (method_name, signature_hash) = _find_method_hash(clearsig[sep_idx+1:])
+ message_hash = _hash(message, method_name)
+
+ # Compare the real hash to the hash in the signature
+ if message_hash != signature_hash:
+ raise VerificationError('Verification failed')
+
+def _hash(message, method_name):
+ '''Returns the message digest.
+
+ :param message: the signed message. Can be an 8-bit string or a file-like
+ object. If ``message`` has a ``read()`` method, it is assumed to be a
+ file-like object.
+ :param method_name: the hash method, must be a key of
+ :py:const:`HASH_METHODS`.
+
+ '''
+
+ if method_name not in HASH_METHODS:
+ raise ValueError('Invalid hash method: %s' % method_name)
+
+ method = HASH_METHODS[method_name]
+ hasher = method()
+
+ if hasattr(message, 'read') and hasattr(message.read, '__call__'):
+ # read as 1K blocks
+ for block in varblock.yield_fixedblocks(message, 1024):
+ hasher.update(block)
+ else:
+ # hash the message object itself.
+ hasher.update(message)
+
+ return hasher.digest()
+
+
+def _find_method_hash(method_hash):
+ '''Finds the hash method and the hash itself.
+
+ :param method_hash: ASN1 code for the hash method concatenated with the
+ hash itself.
+
+ :return: tuple (method, hash) where ``method`` is the used hash method, and
+ ``hash`` is the hash itself.
+
+ :raise VerificationFailed: when the hash method cannot be found
+
+ '''
+
+ for (hashname, asn1code) in HASH_ASN1.items():
+ if not method_hash.startswith(asn1code):
+ continue
+
+ return (hashname, method_hash[len(asn1code):])
+
+ raise VerificationError('Verification failed')
+
+
+__all__ = ['encrypt', 'decrypt', 'sign', 'verify',
+ 'DecryptionError', 'VerificationError', 'CryptoError']
+
+if __name__ == '__main__':
+ print('Running doctests 1000x or until failure')
+ import doctest
+
+ for count in range(1000):
+ (failures, tests) = doctest.testmod()
+ if failures:
+ break
+
+ if count and count % 100 == 0:
+ print('%i times' % count)
+
+ print('Doctests done')
diff --git a/libs/rsa/prime.py b/libs/rsa/prime.py
new file mode 100644
index 00000000..7422eb1d
--- /dev/null
+++ b/libs/rsa/prime.py
@@ -0,0 +1,166 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Sybren A. Stüvel
+#
+# 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.
+
+'''Numerical functions related to primes.
+
+Implementation based on the book Algorithm Design by Michael T. Goodrich and
+Roberto Tamassia, 2002.
+'''
+
+__all__ = [ 'getprime', 'are_relatively_prime']
+
+import rsa.randnum
+
+def gcd(p, q):
+ '''Returns the greatest common divisor of p and q
+
+ >>> gcd(48, 180)
+ 12
+ '''
+
+ while q != 0:
+ if p < q: (p,q) = (q,p)
+ (p,q) = (q, p % q)
+ return p
+
+
+def jacobi(a, b):
+ '''Calculates the value of the Jacobi symbol (a/b) where both a and b are
+ positive integers, and b is odd
+
+ :returns: -1, 0 or 1
+ '''
+
+ assert a > 0
+ assert b > 0
+
+ if a == 0: return 0
+ result = 1
+ while a > 1:
+ if a & 1:
+ if ((a-1)*(b-1) >> 2) & 1:
+ result = -result
+ a, b = b % a, a
+ else:
+ if (((b * b) - 1) >> 3) & 1:
+ result = -result
+ a >>= 1
+ if a == 0: return 0
+ return result
+
+def jacobi_witness(x, n):
+ '''Returns False if n is an Euler pseudo-prime with base x, and
+ True otherwise.
+ '''
+
+ j = jacobi(x, n) % n
+
+ f = pow(x, n >> 1, n)
+
+ if j == f: return False
+ return True
+
+def randomized_primality_testing(n, k):
+ '''Calculates whether n is composite (which is always correct) or
+ prime (which is incorrect with error probability 2**-k)
+
+ Returns False if the number is composite, and True if it's
+ probably prime.
+ '''
+
+ # 50% of Jacobi-witnesses can report compositness of non-prime numbers
+
+ # The implemented algorithm using the Jacobi witness function has error
+ # probability q <= 0.5, according to Goodrich et. al
+ #
+ # q = 0.5
+ # t = int(math.ceil(k / log(1 / q, 2)))
+ # So t = k / log(2, 2) = k / 1 = k
+ # this means we can use range(k) rather than range(t)
+
+ for _ in range(k):
+ x = rsa.randnum.randint(n-1)
+ if jacobi_witness(x, n): return False
+
+ return True
+
+def is_prime(number):
+ '''Returns True if the number is prime, and False otherwise.
+
+ >>> is_prime(42)
+ False
+ >>> is_prime(41)
+ True
+ '''
+
+ return randomized_primality_testing(number, 6)
+
+def getprime(nbits):
+ '''Returns a prime number that can be stored in 'nbits' bits.
+
+ >>> p = getprime(128)
+ >>> is_prime(p-1)
+ False
+ >>> is_prime(p)
+ True
+ >>> is_prime(p+1)
+ False
+
+ >>> from rsa import common
+ >>> common.bit_size(p) == 128
+ True
+
+ '''
+
+ while True:
+ integer = rsa.randnum.read_random_int(nbits)
+
+ # Make sure it's odd
+ integer |= 1
+
+ # Test for primeness
+ if is_prime(integer):
+ return integer
+
+ # Retry if not prime
+
+
+def are_relatively_prime(a, b):
+ '''Returns True if a and b are relatively prime, and False if they
+ are not.
+
+ >>> are_relatively_prime(2, 3)
+ 1
+ >>> are_relatively_prime(2, 4)
+ 0
+ '''
+
+ d = gcd(a, b)
+ return (d == 1)
+
+if __name__ == '__main__':
+ print('Running doctests 1000x or until failure')
+ import doctest
+
+ for count in range(1000):
+ (failures, tests) = doctest.testmod()
+ if failures:
+ break
+
+ if count and count % 100 == 0:
+ print('%i times' % count)
+
+ print('Doctests done')
diff --git a/libs/rsa/randnum.py b/libs/rsa/randnum.py
new file mode 100644
index 00000000..0e782744
--- /dev/null
+++ b/libs/rsa/randnum.py
@@ -0,0 +1,85 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Sybren A. Stüvel
+#
+# 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.
+
+'''Functions for generating random numbers.'''
+
+# Source inspired by code by Yesudeep Mangalapilly
+
+import os
+
+from rsa import common, transform
+from rsa._compat import byte
+
+def read_random_bits(nbits):
+ '''Reads 'nbits' random bits.
+
+ If nbits isn't a whole number of bytes, an extra byte will be appended with
+ only the lower bits set.
+ '''
+
+ nbytes, rbits = divmod(nbits, 8)
+
+ # Get the random bytes
+ randomdata = os.urandom(nbytes)
+
+ # Add the remaining random bits
+ if rbits > 0:
+ randomvalue = ord(os.urandom(1))
+ randomvalue >>= (8 - rbits)
+ randomdata = byte(randomvalue) + randomdata
+
+ return randomdata
+
+
+def read_random_int(nbits):
+ '''Reads a random integer of approximately nbits bits.
+ '''
+
+ randomdata = read_random_bits(nbits)
+ value = transform.bytes2int(randomdata)
+
+ # Ensure that the number is large enough to just fill out the required
+ # number of bits.
+ value |= 1 << (nbits - 1)
+
+ return value
+
+def randint(maxvalue):
+ '''Returns a random integer x with 1 <= x <= maxvalue
+
+ May take a very long time in specific situations. If maxvalue needs N bits
+ to store, the closer maxvalue is to (2 ** N) - 1, the faster this function
+ is.
+ '''
+
+ bit_size = common.bit_size(maxvalue)
+
+ tries = 0
+ while True:
+ value = read_random_int(bit_size)
+ if value <= maxvalue:
+ break
+
+ if tries and tries % 10 == 0:
+ # After a lot of tries to get the right number of bits but still
+ # smaller than maxvalue, decrease the number of bits by 1. That'll
+ # dramatically increase the chances to get a large enough number.
+ bit_size -= 1
+ tries += 1
+
+ return value
+
+
diff --git a/libs/rsa/transform.py b/libs/rsa/transform.py
new file mode 100644
index 00000000..c740b2d2
--- /dev/null
+++ b/libs/rsa/transform.py
@@ -0,0 +1,220 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Sybren A. Stüvel
+#
+# 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.
+
+'''Data transformation functions.
+
+From bytes to a number, number to bytes, etc.
+'''
+
+from __future__ import absolute_import
+
+try:
+ # We'll use psyco if available on 32-bit architectures to speed up code.
+ # Using psyco (if available) cuts down the execution time on Python 2.5
+ # at least by half.
+ import psyco
+ psyco.full()
+except ImportError:
+ pass
+
+import binascii
+from struct import pack
+from rsa import common
+from rsa._compat import is_integer, b, byte, get_word_alignment, ZERO_BYTE, EMPTY_BYTE
+
+
+def bytes2int(raw_bytes):
+ r'''Converts a list of bytes or an 8-bit string to an integer.
+
+ When using unicode strings, encode it to some encoding like UTF8 first.
+
+ >>> (((128 * 256) + 64) * 256) + 15
+ 8405007
+ >>> bytes2int('\x80@\x0f')
+ 8405007
+
+ '''
+
+ return int(binascii.hexlify(raw_bytes), 16)
+
+
+def _int2bytes(number, block_size=None):
+ r'''Converts a number to a string of bytes.
+
+ Usage::
+
+ >>> _int2bytes(123456789)
+ '\x07[\xcd\x15'
+ >>> bytes2int(_int2bytes(123456789))
+ 123456789
+
+ >>> _int2bytes(123456789, 6)
+ '\x00\x00\x07[\xcd\x15'
+ >>> bytes2int(_int2bytes(123456789, 128))
+ 123456789
+
+ >>> _int2bytes(123456789, 3)
+ Traceback (most recent call last):
+ ...
+ OverflowError: Needed 4 bytes for number, but block size is 3
+
+ @param number: the number to convert
+ @param block_size: the number of bytes to output. If the number encoded to
+ bytes is less than this, the block will be zero-padded. When not given,
+ the returned block is not padded.
+
+ @throws OverflowError when block_size is given and the number takes up more
+ bytes than fit into the block.
+ '''
+ # Type checking
+ if not is_integer(number):
+ raise TypeError("You must pass an integer for 'number', not %s" %
+ number.__class__)
+
+ if number < 0:
+ raise ValueError('Negative numbers cannot be used: %i' % number)
+
+ # Do some bounds checking
+ if number == 0:
+ needed_bytes = 1
+ raw_bytes = [ZERO_BYTE]
+ else:
+ needed_bytes = common.byte_size(number)
+ raw_bytes = []
+
+ # You cannot compare None > 0 in Python 3x. It will fail with a TypeError.
+ if block_size and block_size > 0:
+ if needed_bytes > block_size:
+ raise OverflowError('Needed %i bytes for number, but block size '
+ 'is %i' % (needed_bytes, block_size))
+
+ # Convert the number to bytes.
+ while number > 0:
+ raw_bytes.insert(0, byte(number & 0xFF))
+ number >>= 8
+
+ # Pad with zeroes to fill the block
+ if block_size and block_size > 0:
+ padding = (block_size - needed_bytes) * ZERO_BYTE
+ else:
+ padding = EMPTY_BYTE
+
+ return padding + EMPTY_BYTE.join(raw_bytes)
+
+
+def bytes_leading(raw_bytes, needle=ZERO_BYTE):
+ '''
+ Finds the number of prefixed byte occurrences in the haystack.
+
+ Useful when you want to deal with padding.
+
+ :param raw_bytes:
+ Raw bytes.
+ :param needle:
+ The byte to count. Default \000.
+ :returns:
+ The number of leading needle bytes.
+ '''
+ leading = 0
+ # Indexing keeps compatibility between Python 2.x and Python 3.x
+ _byte = needle[0]
+ for x in raw_bytes:
+ if x == _byte:
+ leading += 1
+ else:
+ break
+ return leading
+
+
+def int2bytes(number, fill_size=None, chunk_size=None, overflow=False):
+ '''
+ Convert an unsigned integer to bytes (base-256 representation)::
+
+ Does not preserve leading zeros if you don't specify a chunk size or
+ fill size.
+
+ .. NOTE:
+ You must not specify both fill_size and chunk_size. Only one
+ of them is allowed.
+
+ :param number:
+ Integer value
+ :param fill_size:
+ If the optional fill size is given the length of the resulting
+ byte string is expected to be the fill size and will be padded
+ with prefix zero bytes to satisfy that length.
+ :param chunk_size:
+ If optional chunk size is given and greater than zero, pad the front of
+ the byte string with binary zeros so that the length is a multiple of
+ ``chunk_size``.
+ :param overflow:
+ ``False`` (default). If this is ``True``, no ``OverflowError``
+ will be raised when the fill_size is shorter than the length
+ of the generated byte sequence. Instead the byte sequence will
+ be returned as is.
+ :returns:
+ Raw bytes (base-256 representation).
+ :raises:
+ ``OverflowError`` when fill_size is given and the number takes up more
+ bytes than fit into the block. This requires the ``overflow``
+ argument to this function to be set to ``False`` otherwise, no
+ error will be raised.
+ '''
+ if number < 0:
+ raise ValueError("Number must be an unsigned integer: %d" % number)
+
+ if fill_size and chunk_size:
+ raise ValueError("You can either fill or pad chunks, but not both")
+
+ # Ensure these are integers.
+ number & 1
+
+ raw_bytes = b('')
+
+ # Pack the integer one machine word at a time into bytes.
+ num = number
+ word_bits, _, max_uint, pack_type = get_word_alignment(num)
+ pack_format = ">%s" % pack_type
+ while num > 0:
+ raw_bytes = pack(pack_format, num & max_uint) + raw_bytes
+ num >>= word_bits
+ # Obtain the index of the first non-zero byte.
+ zero_leading = bytes_leading(raw_bytes)
+ if number == 0:
+ raw_bytes = ZERO_BYTE
+ # De-padding.
+ raw_bytes = raw_bytes[zero_leading:]
+
+ length = len(raw_bytes)
+ if fill_size and fill_size > 0:
+ if not overflow and length > fill_size:
+ raise OverflowError(
+ "Need %d bytes for number, but fill size is %d" %
+ (length, fill_size)
+ )
+ raw_bytes = raw_bytes.rjust(fill_size, ZERO_BYTE)
+ elif chunk_size and chunk_size > 0:
+ remainder = length % chunk_size
+ if remainder:
+ padding_size = chunk_size - remainder
+ raw_bytes = raw_bytes.rjust(length + padding_size, ZERO_BYTE)
+ return raw_bytes
+
+
+if __name__ == '__main__':
+ import doctest
+ doctest.testmod()
+
diff --git a/libs/rsa/util.py b/libs/rsa/util.py
new file mode 100644
index 00000000..307bda5d
--- /dev/null
+++ b/libs/rsa/util.py
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Sybren A. Stüvel
+#
+# 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.
+
+'''Utility functions.'''
+
+from __future__ import with_statement
+
+import sys
+from optparse import OptionParser
+
+import rsa.key
+
+def private_to_public():
+ '''Reads a private key and outputs the corresponding public key.'''
+
+ # Parse the CLI options
+ parser = OptionParser(usage='usage: %prog [options]',
+ description='Reads a private key and outputs the '
+ 'corresponding public key. Both private and public keys use '
+ 'the format described in PKCS#1 v1.5')
+
+ parser.add_option('-i', '--input', dest='infilename', type='string',
+ help='Input filename. Reads from stdin if not specified')
+ parser.add_option('-o', '--output', dest='outfilename', type='string',
+ help='Output filename. Writes to stdout of not specified')
+
+ parser.add_option('--inform', dest='inform',
+ help='key format of input - default PEM',
+ choices=('PEM', 'DER'), default='PEM')
+
+ parser.add_option('--outform', dest='outform',
+ help='key format of output - default PEM',
+ choices=('PEM', 'DER'), default='PEM')
+
+ (cli, cli_args) = parser.parse_args(sys.argv)
+
+ # Read the input data
+ if cli.infilename:
+ print >>sys.stderr, 'Reading private key from %s in %s format' % \
+ (cli.infilename, cli.inform)
+ with open(cli.infilename) as infile:
+ in_data = infile.read()
+ else:
+ print >>sys.stderr, 'Reading private key from stdin in %s format' % \
+ cli.inform
+ in_data = sys.stdin.read()
+
+
+ # Take the public fields and create a public key
+ priv_key = rsa.key.PrivateKey.load_pkcs1(in_data, cli.inform)
+ pub_key = rsa.key.PublicKey(priv_key.n, priv_key.e)
+
+ # Save to the output file
+ out_data = pub_key.save_pkcs1(cli.outform)
+
+ if cli.outfilename:
+ print >>sys.stderr, 'Writing public key to %s in %s format' % \
+ (cli.outfilename, cli.outform)
+ with open(cli.outfilename, 'w') as outfile:
+ outfile.write(out_data)
+ else:
+ print >>sys.stderr, 'Writing public key to stdout in %s format' % \
+ cli.outform
+ sys.stdout.write(out_data)
+
+
diff --git a/libs/rsa/varblock.py b/libs/rsa/varblock.py
new file mode 100644
index 00000000..c7d96ae6
--- /dev/null
+++ b/libs/rsa/varblock.py
@@ -0,0 +1,155 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2011 Sybren A. Stüvel
+#
+# 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.
+
+'''VARBLOCK file support
+
+The VARBLOCK file format is as follows, where || denotes byte concatenation:
+
+ FILE := VERSION || BLOCK || BLOCK ...
+
+ BLOCK := LENGTH || DATA
+
+ LENGTH := varint-encoded length of the subsequent data. Varint comes from
+ Google Protobuf, and encodes an integer into a variable number of bytes.
+ Each byte uses the 7 lowest bits to encode the value. The highest bit set
+ to 1 indicates the next byte is also part of the varint. The last byte will
+ have this bit set to 0.
+
+This file format is called the VARBLOCK format, in line with the varint format
+used to denote the block sizes.
+
+'''
+
+from rsa._compat import byte, b
+
+
+ZERO_BYTE = b('\x00')
+VARBLOCK_VERSION = 1
+
+def read_varint(infile):
+ '''Reads a varint from the file.
+
+ When the first byte to be read indicates EOF, (0, 0) is returned. When an
+ EOF occurs when at least one byte has been read, an EOFError exception is
+ raised.
+
+ @param infile: the file-like object to read from. It should have a read()
+ method.
+ @returns (varint, length), the read varint and the number of read bytes.
+ '''
+
+ varint = 0
+ read_bytes = 0
+
+ while True:
+ char = infile.read(1)
+ if len(char) == 0:
+ if read_bytes == 0:
+ return (0, 0)
+ raise EOFError('EOF while reading varint, value is %i so far' %
+ varint)
+
+ byte = ord(char)
+ varint += (byte & 0x7F) << (7 * read_bytes)
+
+ read_bytes += 1
+
+ if not byte & 0x80:
+ return (varint, read_bytes)
+
+
+def write_varint(outfile, value):
+ '''Writes a varint to a file.
+
+ @param outfile: the file-like object to write to. It should have a write()
+ method.
+ @returns the number of written bytes.
+ '''
+
+ # there is a big difference between 'write the value 0' (this case) and
+ # 'there is nothing left to write' (the false-case of the while loop)
+
+ if value == 0:
+ outfile.write(ZERO_BYTE)
+ return 1
+
+ written_bytes = 0
+ while value > 0:
+ to_write = value & 0x7f
+ value = value >> 7
+
+ if value > 0:
+ to_write |= 0x80
+
+ outfile.write(byte(to_write))
+ written_bytes += 1
+
+ return written_bytes
+
+
+def yield_varblocks(infile):
+ '''Generator, yields each block in the input file.
+
+ @param infile: file to read, is expected to have the VARBLOCK format as
+ described in the module's docstring.
+ @yields the contents of each block.
+ '''
+
+ # Check the version number
+ first_char = infile.read(1)
+ if len(first_char) == 0:
+ raise EOFError('Unable to read VARBLOCK version number')
+
+ version = ord(first_char)
+ if version != VARBLOCK_VERSION:
+ raise ValueError('VARBLOCK version %i not supported' % version)
+
+ while True:
+ (block_size, read_bytes) = read_varint(infile)
+
+ # EOF at block boundary, that's fine.
+ if read_bytes == 0 and block_size == 0:
+ break
+
+ block = infile.read(block_size)
+
+ read_size = len(block)
+ if read_size != block_size:
+ raise EOFError('Block size is %i, but could read only %i bytes' %
+ (block_size, read_size))
+
+ yield block
+
+
+def yield_fixedblocks(infile, blocksize):
+ '''Generator, yields each block of ``blocksize`` bytes in the input file.
+
+ :param infile: file to read and separate in blocks.
+ :returns: a generator that yields the contents of each block
+ '''
+
+ while True:
+ block = infile.read(blocksize)
+
+ read_bytes = len(block)
+ if read_bytes == 0:
+ break
+
+ yield block
+
+ if read_bytes < blocksize:
+ break
+
diff --git a/libs/subliminal/__init__.py b/libs/subliminal/__init__.py
old mode 100755
new mode 100644
diff --git a/libs/subliminal/api.py b/libs/subliminal/api.py
old mode 100755
new mode 100644
index baff5f1a..51416f8c
--- a/libs/subliminal/api.py
+++ b/libs/subliminal/api.py
@@ -18,7 +18,7 @@
from .core import (SERVICES, LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE,
MATCHING_CONFIDENCE, create_list_tasks, consume_task, create_download_tasks,
group_by_video, key_subtitles)
-from .languages import list_languages
+from .language import language_set, language_list, LANGUAGES
import logging
@@ -26,30 +26,32 @@ __all__ = ['list_subtitles', 'download_subtitles']
logger = logging.getLogger(__name__)
-def list_subtitles(paths, languages=None, services=None, force=True, multi=False, cache_dir=None, max_depth=3):
+def list_subtitles(paths, languages=None, services=None, force=True, multi=False, cache_dir=None, max_depth=3, scan_filter=None):
"""List subtitles in given paths according to the criteria
:param paths: path(s) to video file or folder
:type paths: string or list
- :param list languages: languages to search for, in preferred order
+ :param languages: languages to search for, in preferred order
+ :type languages: list of :class:`~subliminal.language.Language` or string
:param list services: services to use for the search, in preferred order
:param bool force: force searching for subtitles even if some are detected
:param bool multi: search multiple languages for the same video
:param string cache_dir: path to the cache directory to use
:param int max_depth: maximum depth for scanning entries
+ :param function scan_filter: filter function that takes a path as argument and returns a boolean indicating whether it has to be filtered out (``True``) or not (``False``)
:return: found subtitles
:rtype: dict of :class:`~subliminal.videos.Video` => [:class:`~subliminal.subtitles.ResultSubtitle`]
"""
services = services or SERVICES
- languages = set(languages or list_languages(1))
+ languages = language_set(languages) if languages is not None else language_set(LANGUAGES)
if isinstance(paths, basestring):
paths = [paths]
if any([not isinstance(p, unicode) for p in paths]):
logger.warning(u'Not all entries are unicode')
results = []
service_instances = {}
- tasks = create_list_tasks(paths, languages, services, force, multi, cache_dir, max_depth)
+ tasks = create_list_tasks(paths, languages, services, force, multi, cache_dir, max_depth, scan_filter)
for task in tasks:
try:
result = consume_task(task, service_instances)
@@ -61,40 +63,47 @@ def list_subtitles(paths, languages=None, services=None, force=True, multi=False
return group_by_video(results)
-def download_subtitles(paths, languages=None, services=None, force=True, multi=False, cache_dir=None, max_depth=3, order=None):
+def download_subtitles(paths, languages=None, services=None, force=True, multi=False, cache_dir=None, max_depth=3, scan_filter=None, order=None):
"""Download subtitles in given paths according to the criteria
:param paths: path(s) to video file or folder
:type paths: string or list
- :param list languages: languages to search for, in preferred order
+ :param languages: languages to search for, in preferred order
+ :type languages: list of :class:`~subliminal.language.Language` or string
:param list services: services to use for the search, in preferred order
:param bool force: force searching for subtitles even if some are detected
:param bool multi: search multiple languages for the same video
:param string cache_dir: path to the cache directory to use
:param int max_depth: maximum depth for scanning entries
+ :param function scan_filter: filter function that takes a path as argument and returns a boolean indicating whether it has to be filtered out (``True``) or not (``False``)
:param order: preferred order for subtitles sorting
:type list: list of :data:`~subliminal.core.LANGUAGE_INDEX`, :data:`~subliminal.core.SERVICE_INDEX`, :data:`~subliminal.core.SERVICE_CONFIDENCE`, :data:`~subliminal.core.MATCHING_CONFIDENCE`
- :return: found subtitles
- :rtype: list of (:class:`~subliminal.videos.Video`, [:class:`~subliminal.subtitles.ResultSubtitle`])
+ :return: downloaded subtitles
+ :rtype: dict of :class:`~subliminal.videos.Video` => [:class:`~subliminal.subtitles.ResultSubtitle`]
+
+ .. note::
+
+ If you use ``multi=True``, :data:`~subliminal.core.LANGUAGE_INDEX` has to be the first item of the ``order`` list
+ or you might get unexpected results.
"""
services = services or SERVICES
- languages = languages or list_languages(1)
+ languages = language_list(languages) if languages is not None else language_list(LANGUAGES)
if isinstance(paths, basestring):
paths = [paths]
order = order or [LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE, MATCHING_CONFIDENCE]
- subtitles_by_video = list_subtitles(paths, set(languages), services, force, multi, cache_dir, max_depth)
+ subtitles_by_video = list_subtitles(paths, languages, services, force, multi, cache_dir, max_depth, scan_filter)
for video, subtitles in subtitles_by_video.iteritems():
subtitles.sort(key=lambda s: key_subtitles(s, video, languages, services, order), reverse=True)
results = []
service_instances = {}
- tasks = create_download_tasks(subtitles_by_video, multi)
+ tasks = create_download_tasks(subtitles_by_video, languages, multi)
for task in tasks:
try:
result = consume_task(task, service_instances)
- results.append(result)
+ results.append((task.video, result))
except:
logger.error(u'Error consuming task %r' % task, exc_info=True)
for service_instance in service_instances.itervalues():
service_instance.terminate()
- return results
+ return group_by_video(results)
diff --git a/libs/subliminal/async.py b/libs/subliminal/async.py
old mode 100755
new mode 100644
index ce18a278..6a69b766
--- a/libs/subliminal/async.py
+++ b/libs/subliminal/async.py
@@ -18,13 +18,14 @@
from .core import (consume_task, LANGUAGE_INDEX, SERVICE_INDEX,
SERVICE_CONFIDENCE, MATCHING_CONFIDENCE, SERVICES, create_list_tasks,
create_download_tasks, group_by_video, key_subtitles)
-from .languages import list_languages
+from .language import language_list, language_set, LANGUAGES
from .tasks import StopTask
import Queue
import logging
import threading
+__all__ = ['Worker', 'Pool']
logger = logging.getLogger(__name__)
@@ -108,34 +109,34 @@ class Pool(object):
break
return results
- def list_subtitles(self, paths, languages=None, services=None, force=True, multi=False, cache_dir=None, max_depth=3):
+ def list_subtitles(self, paths, languages=None, services=None, force=True, multi=False, cache_dir=None, max_depth=3, scan_filter=None):
"""See :meth:`subliminal.list_subtitles`"""
services = services or SERVICES
- languages = set(languages or list_languages(1))
+ languages = language_set(languages) if languages is not None else language_set(LANGUAGES)
if isinstance(paths, basestring):
paths = [paths]
if any([not isinstance(p, unicode) for p in paths]):
logger.warning(u'Not all entries are unicode')
- tasks = create_list_tasks(paths, languages, services, force, multi, cache_dir, max_depth)
+ tasks = create_list_tasks(paths, languages, services, force, multi, cache_dir, max_depth, scan_filter)
for task in tasks:
self.tasks.put(task)
self.join()
results = self.collect()
return group_by_video(results)
- def download_subtitles(self, paths, languages=None, services=None, cache_dir=None, max_depth=3, force=True, multi=False, order=None):
+ def download_subtitles(self, paths, languages=None, services=None, force=True, multi=False, cache_dir=None, max_depth=3, scan_filter=None, order=None):
"""See :meth:`subliminal.download_subtitles`"""
services = services or SERVICES
- languages = languages or list_languages(1)
+ languages = language_list(languages) if languages is not None else language_list(LANGUAGES)
if isinstance(paths, basestring):
paths = [paths]
order = order or [LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE, MATCHING_CONFIDENCE]
- subtitles_by_video = self.list_subtitles(paths, set(languages), services, force, multi, cache_dir, max_depth)
+ subtitles_by_video = self.list_subtitles(paths, languages, services, force, multi, cache_dir, max_depth, scan_filter)
for video, subtitles in subtitles_by_video.iteritems():
subtitles.sort(key=lambda s: key_subtitles(s, video, languages, services, order), reverse=True)
- tasks = create_download_tasks(subtitles_by_video, multi)
+ tasks = create_download_tasks(subtitles_by_video, languages, multi)
for task in tasks:
self.tasks.put(task)
self.join()
results = self.collect()
- return results
+ return group_by_video(results)
diff --git a/libs/subliminal/cache.py b/libs/subliminal/cache.py
new file mode 100644
index 00000000..9add007a
--- /dev/null
+++ b/libs/subliminal/cache.py
@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*-
+# Copyright 2012 Nicolas Wack
+#
+# This file is part of subliminal.
+#
+# subliminal is free software; you can redistribute it and/or modify it under
+# the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# subliminal is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with subliminal. If not, see .
+from collections import defaultdict
+from functools import wraps
+import logging
+import os.path
+import threading
+try:
+ import cPickle as pickle
+except ImportError:
+ import pickle
+
+
+__all__ = ['Cache', 'cachedmethod']
+logger = logging.getLogger(__name__)
+
+
+class Cache(object):
+ """A Cache object contains cached values for methods. It can have
+ separate internal caches, one for each service
+
+ """
+ def __init__(self, cache_dir):
+ self.cache_dir = cache_dir
+ self.cache = defaultdict(dict)
+ self.lock = threading.RLock()
+
+ def __del__(self):
+ for service_name in self.cache:
+ self.save(service_name)
+
+ def cache_location(self, service_name):
+ return os.path.join(self.cache_dir, 'subliminal_%s.cache' % service_name)
+
+ def load(self, service_name):
+ with self.lock:
+ if service_name in self.cache:
+ # already loaded
+ return
+
+ self.cache[service_name] = defaultdict(dict)
+ filename = self.cache_location(service_name)
+ logger.debug(u'Cache: loading cache from %s' % filename)
+ try:
+ self.cache[service_name] = pickle.load(open(filename, 'rb'))
+ except IOError:
+ logger.info('Cache: Cache file "%s" doesn\'t exist, creating it' % filename)
+ except EOFError:
+ logger.error('Cache: cache file "%s" is corrupted... Removing it.' % filename)
+ os.remove(filename)
+
+ def save(self, service_name):
+ filename = self.cache_location(service_name)
+ logger.debug(u'Cache: saving cache to %s' % filename)
+ with self.lock:
+ pickle.dump(self.cache[service_name], open(filename, 'wb'))
+
+ def clear(self, service_name):
+ try:
+ os.remove(self.cache_location(service_name))
+ except OSError:
+ pass
+ self.cache[service_name] = defaultdict(dict)
+
+ def cached_func_key(self, func, cls=None):
+ try:
+ cls = func.im_class
+ except:
+ pass
+ return ('%s.%s' % (cls.__module__, cls.__name__), func.__name__)
+
+ def function_cache(self, service_name, func):
+ func_key = self.cached_func_key(func)
+ return self.cache[service_name][func_key]
+
+ def cache_for(self, service_name, func, args, result):
+ # no need to lock here, dict ops are atomic
+ self.function_cache(service_name, func)[args] = result
+
+ def cached_value(self, service_name, func, args):
+ """Raises KeyError if not found"""
+ # no need to lock here, dict ops are atomic
+ return self.function_cache(service_name, func)[args]
+
+
+def cachedmethod(function):
+ """Decorator to make a method use the cache.
+
+ .. note::
+
+ This can NOT be used with static functions, it has to be used on
+ methods of some class
+
+ """
+ @wraps(function)
+ def cached(*args):
+ c = args[0].config.cache
+ service_name = args[0].__class__.__name__
+ func_key = c.cached_func_key(function, cls=args[0].__class__)
+ func_cache = c.cache[service_name][func_key]
+
+ # we need to remove the first element of args for the key, as it is the
+ # instance pointer and we don't want the cache to know which instance
+ # called it, it is shared among all instances of the same class
+ key = args[1:]
+
+ if key in func_cache:
+ result = func_cache[key]
+ logger.debug(u'Using cached value for %s(%s), returns: %s' % (func_key, key, result))
+ return result
+
+ result = function(*args)
+
+ # note: another thread could have already cached a value in the
+ # meantime, but that's ok as we prefer to keep the latest value in
+ # the cache
+ func_cache[key] = result
+ return result
+ return cached
diff --git a/libs/subliminal/core.py b/libs/subliminal/core.py
old mode 100755
new mode 100644
index 56f4347e..537fa655
--- a/libs/subliminal/core.py
+++ b/libs/subliminal/core.py
@@ -20,8 +20,10 @@ from .services import ServiceConfig
from .tasks import DownloadTask, ListTask
from .utils import get_keywords
from .videos import Episode, Movie, scan
+from .language import Language
from collections import defaultdict
from itertools import groupby
+import bs4
import guessit
import logging
@@ -30,11 +32,11 @@ __all__ = ['SERVICES', 'LANGUAGE_INDEX', 'SERVICE_INDEX', 'SERVICE_CONFIDENCE',
'create_list_tasks', 'create_download_tasks', 'consume_task', 'matching_confidence',
'key_subtitles', 'group_by_video']
logger = logging.getLogger(__name__)
-SERVICES = ['opensubtitles', 'bierdopje', 'subswiki', 'subtitulos', 'thesubdb']
+SERVICES = ['opensubtitles', 'bierdopje', 'subswiki', 'subtitulos', 'thesubdb', 'addic7ed', 'tvsubtitles']
LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE, MATCHING_CONFIDENCE = range(4)
-def create_list_tasks(paths, languages, services, force, multi, cache_dir, max_depth):
+def create_list_tasks(paths, languages, services, force, multi, cache_dir, max_depth, scan_filter):
"""Create a list of :class:`~subliminal.tasks.ListTask` from one or more paths using the given criteria
:param paths: path(s) to video file or folder
@@ -45,51 +47,48 @@ def create_list_tasks(paths, languages, services, force, multi, cache_dir, max_d
:param bool multi: search multiple languages for the same video
:param string cache_dir: path to the cache directory to use
:param int max_depth: maximum depth for scanning entries
+ :param function scan_filter: filter function that takes a path as argument and returns a boolean indicating whether it has to be filtered out (``True``) or not (``False``)
:return: the created tasks
:rtype: list of :class:`~subliminal.tasks.ListTask`
"""
scan_result = []
for p in paths:
- scan_result.extend(scan(p, max_depth))
+ scan_result.extend(scan(p, max_depth, scan_filter))
logger.debug(u'Found %d videos in %r with maximum depth %d' % (len(scan_result), paths, max_depth))
tasks = []
config = ServiceConfig(multi, cache_dir)
+ services = filter_services(services)
for video, detected_subtitles in scan_result:
- detected_languages = set([s.language for s in detected_subtitles])
+ detected_languages = set(s.language for s in detected_subtitles)
wanted_languages = languages.copy()
if not force and multi:
wanted_languages -= detected_languages
if not wanted_languages:
logger.debug(u'No need to list multi subtitles %r for %r because %r detected' % (languages, video, detected_languages))
continue
- if not force and not multi and None in detected_languages:
+ if not force and not multi and Language('Undetermined') in detected_languages:
logger.debug(u'No need to list single subtitles %r for %r because one detected' % (languages, video))
continue
logger.debug(u'Listing subtitles %r for %r with services %r' % (wanted_languages, video, services))
for service_name in services:
mod = __import__('services.' + service_name, globals=globals(), locals=locals(), fromlist=['Service'], level=-1)
service = mod.Service
- service_languages = wanted_languages & service.available_languages()
- if not service_languages:
- logger.debug(u'Skipping %r: none of wanted languages %r available for service %s' % (video, wanted_languages, service_name))
+ if not service.check_validity(video, wanted_languages):
continue
- if not service.is_valid_video(video):
- logger.debug(u'Skipping %r: not part of supported videos %r for service %s' % (video, service.videos, service_name))
- continue
- task = ListTask(video, service_languages, service_name, config)
+ task = ListTask(video, wanted_languages & service.languages, service_name, config)
logger.debug(u'Created task %r' % task)
tasks.append(task)
return tasks
-def create_download_tasks(subtitles_by_video, multi):
+def create_download_tasks(subtitles_by_video, languages, multi):
"""Create a list of :class:`~subliminal.tasks.DownloadTask` from a list results grouped by video
- :param subtitles_by_video: :class:`~subliminal.tasks.ListTask` results grouped by video and sorted
+ :param subtitles_by_video: :class:`~subliminal.tasks.ListTask` results with ordered subtitles
:type subtitles_by_video: dict of :class:`~subliminal.videos.Video` => [:class:`~subliminal.subtitles.Subtitle`]
- :param order: preferred order for subtitles sorting
- :type list: list of :data:`LANGUAGE_INDEX`, :data:`SERVICE_INDEX`, :data:`SERVICE_CONFIDENCE`, :data:`MATCHING_CONFIDENCE`
+ :param languages: languages in preferred order
+ :type languages: :class:`~subliminal.language.language_list`
:param bool multi: download multiple languages for the same video
:return: the created tasks
:rtype: list of :class:`~subliminal.tasks.DownloadTask`
@@ -104,7 +103,7 @@ def create_download_tasks(subtitles_by_video, multi):
logger.debug(u'Created task %r' % task)
tasks.append(task)
continue
- for _, by_language in groupby(subtitles, lambda s: s.language):
+ for _, by_language in groupby(subtitles, lambda s: languages.index(s.language)):
task = DownloadTask(video, list(by_language))
logger.debug(u'Created task %r' % task)
tasks.append(task)
@@ -120,7 +119,7 @@ def consume_task(task, services=None):
:type task: :class:`~subliminal.tasks.ListTask` or :class:`~subliminal.tasks.DownloadTask`
:param dict services: mapping between the service name and an instance of this service
:return: the result of the task
- :rtype: list of :class:`~subliminal.subtitles.ResultSubtitle` or :class:`~subliminal.subtitles.Subtitle`
+ :rtype: list of :class:`~subliminal.subtitles.ResultSubtitle`
"""
if services is None:
@@ -128,21 +127,14 @@ def consume_task(task, services=None):
logger.info(u'Consuming %r' % task)
result = None
if isinstance(task, ListTask):
- if task.service not in services:
- mod = __import__('services.' + task.service, globals=globals(), locals=locals(), fromlist=['Service'], level=-1)
- services[task.service] = mod.Service(task.config)
- services[task.service].init()
- subtitles = services[task.service].list(task.video, task.languages)
- result = subtitles
+ service = get_service(services, task.service, config=task.config)
+ result = service.list(task.video, task.languages)
elif isinstance(task, DownloadTask):
for subtitle in task.subtitles:
- if subtitle.service not in services:
- mod = __import__('services.' + subtitle.service, globals=globals(), locals=locals(), fromlist=['Service'], level=-1)
- services[subtitle.service] = mod.Service()
- services[subtitle.service].init()
+ service = get_service(services, subtitle.service)
try:
- services[subtitle.service].download(subtitle)
- result = subtitle
+ service.download(subtitle)
+ result = [subtitle]
break
except DownloadFailedError:
logger.warning(u'Could not download subtitle %r, trying next' % subtitle)
@@ -166,6 +158,7 @@ def matching_confidence(video, subtitle):
guess = guessit.guess_file_info(subtitle.release, 'autodetect')
video_keywords = get_keywords(video.guess)
subtitle_keywords = get_keywords(guess) | subtitle.keywords
+ logger.debug(u'Video keywords %r - Subtitle keywords %r' % (video_keywords, subtitle_keywords))
replacement = {'keywords': len(video_keywords & subtitle_keywords)}
if isinstance(video, Episode):
replacement.update({'series': 0, 'season': 0, 'episode': 0})
@@ -188,11 +181,34 @@ def matching_confidence(video, subtitle):
if 'year' in guess and guess['year'] == video.year:
replacement['year'] = 1
else:
- return 0
+ logger.debug(u'Not able to compute confidence for %r' % video)
+ return 0.0
+ logger.debug(u'Found %r' % replacement)
confidence = float(int(matching_format.format(**replacement), 2)) / float(int(best, 2))
+ logger.info(u'Computed confidence %.4f for %r and %r' % (confidence, video, subtitle))
return confidence
+def get_service(services, service_name, config=None):
+ """Get a service from its name in the service dict with the specified config.
+ If the service does not exist in the service dict, it is created and added to the dict.
+
+ :param dict services: dict where to get existing services or put created ones
+ :param string service_name: name of the service to get
+ :param config: config to use for the service
+ :type config: :class:`~subliminal.services.ServiceConfig` or None
+ :return: the corresponding service
+ :rtype: :class:`~subliminal.services.ServiceBase`
+
+ """
+ if service_name not in services:
+ mod = __import__('services.' + service_name, globals=globals(), locals=locals(), fromlist=['Service'], level=-1)
+ services[service_name] = mod.Service()
+ services[service_name].init()
+ services[service_name].config = config
+ return services[service_name]
+
+
def key_subtitles(subtitle, video, languages, services, order):
"""Create a key to sort subtitle using the given order
@@ -212,6 +228,7 @@ def key_subtitles(subtitle, video, languages, services, order):
for sort_item in order:
if sort_item == LANGUAGE_INDEX:
key += '{0:03d}'.format(len(languages) - languages.index(subtitle.language) - 1)
+ key += '{0:01d}'.format(subtitle.language == languages[languages.index(subtitle.language)])
elif sort_item == SERVICE_INDEX:
key += '{0:02d}'.format(len(services) - services.index(subtitle.service) - 1)
elif sort_item == SERVICE_CONFIDENCE:
@@ -236,5 +253,23 @@ def group_by_video(list_results):
"""
result = defaultdict(list)
for video, subtitles in list_results:
- result[video] += subtitles
+ result[video] += subtitles or []
return result
+
+
+def filter_services(services):
+ """Filter out services that are not available because of a missing feature
+
+ :param list services: service names to filter
+ :return: a copy of the initial list of service names without unavailable ones
+ :rtype: list
+
+ """
+ filtered_services = services[:]
+ for service_name in services:
+ mod = __import__('services.' + service_name, globals=globals(), locals=locals(), fromlist=['Service'], level=-1)
+ service = mod.Service
+ if service.required_features is not None and bs4.builder_registry.lookup(*service.required_features) is None:
+ logger.warning(u'Service %s not available: none of available features could be used. One of %r required' % (service_name, service.required_features))
+ filtered_services.remove(service_name)
+ return filtered_services
diff --git a/libs/subliminal/exceptions.py b/libs/subliminal/exceptions.py
old mode 100755
new mode 100644
index 93001110..66e3dd51
--- a/libs/subliminal/exceptions.py
+++ b/libs/subliminal/exceptions.py
@@ -22,60 +22,11 @@ class Error(Exception):
pass
-class InvalidLanguageError(Error):
- """Exception raised when invalid language is submitted
-
- Attributes:
- language -- language that cause the error
- """
- def __init__(self, language):
- self.language = language
-
- def __str__(self):
- return self.language
-
-
-class MissingLanguageError(Error):
- """Exception raised when a missing language is found
-
- Attributes:
- language -- the missing language
- """
- def __init__(self, language):
- self.language = language
-
- def __str__(self):
- return self.language
-
-
-class InvalidServiceError(Error):
- """Exception raised when invalid service is submitted
-
- :param string service: service that causes the error
-
- """
- def __init__(self, service):
- self.service = service
-
- def __str__(self):
- return self.service
-
-
class ServiceError(Error):
""""Exception raised by services"""
pass
-class WrongTaskError(Error):
- """"Exception raised when invalid task is submitted"""
- pass
-
-
class DownloadFailedError(Error):
""""Exception raised when a download task has failed in service"""
pass
-
-
-class UnknownVideoError(Error):
- """"Exception raised when a video could not be identified"""
- pass
diff --git a/libs/subliminal/infos.py b/libs/subliminal/infos.py
old mode 100755
new mode 100644
index b28fda00..46039ac3
--- a/libs/subliminal/infos.py
+++ b/libs/subliminal/infos.py
@@ -15,4 +15,4 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see .
-__version__ = '0.5.1'
+__version__ = '0.6.1'
diff --git a/libs/subliminal/language.py b/libs/subliminal/language.py
new file mode 100644
index 00000000..efc75bb4
--- /dev/null
+++ b/libs/subliminal/language.py
@@ -0,0 +1,1047 @@
+# -*- coding: utf-8 -*-
+# Copyright 2011-2012 Antoine Bertin
+#
+# This file is part of subliminal.
+#
+# subliminal is free software; you can redistribute it and/or modify it under
+# the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# subliminal is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with subliminal. If not, see .
+from .utils import to_unicode
+import re
+import logging
+
+
+logger = logging.getLogger(__name__)
+
+
+COUNTRIES = [('AF', 'AFG', '004', u'Afghanistan'),
+ ('AX', 'ALA', '248', u'Åland Islands'),
+ ('AL', 'ALB', '008', u'Albania'),
+ ('DZ', 'DZA', '012', u'Algeria'),
+ ('AS', 'ASM', '016', u'American Samoa'),
+ ('AD', 'AND', '020', u'Andorra'),
+ ('AO', 'AGO', '024', u'Angola'),
+ ('AI', 'AIA', '660', u'Anguilla'),
+ ('AQ', 'ATA', '010', u'Antarctica'),
+ ('AG', 'ATG', '028', u'Antigua and Barbuda'),
+ ('AR', 'ARG', '032', u'Argentina'),
+ ('AM', 'ARM', '051', u'Armenia'),
+ ('AW', 'ABW', '533', u'Aruba'),
+ ('AU', 'AUS', '036', u'Australia'),
+ ('AT', 'AUT', '040', u'Austria'),
+ ('AZ', 'AZE', '031', u'Azerbaijan'),
+ ('BS', 'BHS', '044', u'Bahamas'),
+ ('BH', 'BHR', '048', u'Bahrain'),
+ ('BD', 'BGD', '050', u'Bangladesh'),
+ ('BB', 'BRB', '052', u'Barbados'),
+ ('BY', 'BLR', '112', u'Belarus'),
+ ('BE', 'BEL', '056', u'Belgium'),
+ ('BZ', 'BLZ', '084', u'Belize'),
+ ('BJ', 'BEN', '204', u'Benin'),
+ ('BM', 'BMU', '060', u'Bermuda'),
+ ('BT', 'BTN', '064', u'Bhutan'),
+ ('BO', 'BOL', '068', u'Bolivia, Plurinational State of'),
+ ('BQ', 'BES', '535', u'Bonaire, Sint Eustatius and Saba'),
+ ('BA', 'BIH', '070', u'Bosnia and Herzegovina'),
+ ('BW', 'BWA', '072', u'Botswana'),
+ ('BV', 'BVT', '074', u'Bouvet Island'),
+ ('BR', 'BRA', '076', u'Brazil'),
+ ('IO', 'IOT', '086', u'British Indian Ocean Territory'),
+ ('BN', 'BRN', '096', u'Brunei Darussalam'),
+ ('BG', 'BGR', '100', u'Bulgaria'),
+ ('BF', 'BFA', '854', u'Burkina Faso'),
+ ('BI', 'BDI', '108', u'Burundi'),
+ ('KH', 'KHM', '116', u'Cambodia'),
+ ('CM', 'CMR', '120', u'Cameroon'),
+ ('CA', 'CAN', '124', u'Canada'),
+ ('CV', 'CPV', '132', u'Cape Verde'),
+ ('KY', 'CYM', '136', u'Cayman Islands'),
+ ('CF', 'CAF', '140', u'Central African Republic'),
+ ('TD', 'TCD', '148', u'Chad'),
+ ('CL', 'CHL', '152', u'Chile'),
+ ('CN', 'CHN', '156', u'China'),
+ ('CX', 'CXR', '162', u'Christmas Island'),
+ ('CC', 'CCK', '166', u'Cocos (Keeling) Islands'),
+ ('CO', 'COL', '170', u'Colombia'),
+ ('KM', 'COM', '174', u'Comoros'),
+ ('CG', 'COG', '178', u'Congo'),
+ ('CD', 'COD', '180', u'Congo, The Democratic Republic of the'),
+ ('CK', 'COK', '184', u'Cook Islands'),
+ ('CR', 'CRI', '188', u'Costa Rica'),
+ ('CI', 'CIV', '384', u'Côte d\'Ivoire'),
+ ('HR', 'HRV', '191', u'Croatia'),
+ ('CU', 'CUB', '192', u'Cuba'),
+ ('CW', 'CUW', '531', u'Curaçao'),
+ ('CY', 'CYP', '196', u'Cyprus'),
+ ('CZ', 'CZE', '203', u'Czech Republic'),
+ ('DK', 'DNK', '208', u'Denmark'),
+ ('DJ', 'DJI', '262', u'Djibouti'),
+ ('DM', 'DMA', '212', u'Dominica'),
+ ('DO', 'DOM', '214', u'Dominican Republic'),
+ ('EC', 'ECU', '218', u'Ecuador'),
+ ('EG', 'EGY', '818', u'Egypt'),
+ ('SV', 'SLV', '222', u'El Salvador'),
+ ('GQ', 'GNQ', '226', u'Equatorial Guinea'),
+ ('ER', 'ERI', '232', u'Eritrea'),
+ ('EE', 'EST', '233', u'Estonia'),
+ ('ET', 'ETH', '231', u'Ethiopia'),
+ ('FK', 'FLK', '238', u'Falkland Islands (Malvinas)'),
+ ('FO', 'FRO', '234', u'Faroe Islands'),
+ ('FJ', 'FJI', '242', u'Fiji'),
+ ('FI', 'FIN', '246', u'Finland'),
+ ('FR', 'FRA', '250', u'France'),
+ ('GF', 'GUF', '254', u'French Guiana'),
+ ('PF', 'PYF', '258', u'French Polynesia'),
+ ('TF', 'ATF', '260', u'French Southern Territories'),
+ ('GA', 'GAB', '266', u'Gabon'),
+ ('GM', 'GMB', '270', u'Gambia'),
+ ('GE', 'GEO', '268', u'Georgia'),
+ ('DE', 'DEU', '276', u'Germany'),
+ ('GH', 'GHA', '288', u'Ghana'),
+ ('GI', 'GIB', '292', u'Gibraltar'),
+ ('GR', 'GRC', '300', u'Greece'),
+ ('GL', 'GRL', '304', u'Greenland'),
+ ('GD', 'GRD', '308', u'Grenada'),
+ ('GP', 'GLP', '312', u'Guadeloupe'),
+ ('GU', 'GUM', '316', u'Guam'),
+ ('GT', 'GTM', '320', u'Guatemala'),
+ ('GG', 'GGY', '831', u'Guernsey'),
+ ('GN', 'GIN', '324', u'Guinea'),
+ ('GW', 'GNB', '624', u'Guinea-Bissau'),
+ ('GY', 'GUY', '328', u'Guyana'),
+ ('HT', 'HTI', '332', u'Haiti'),
+ ('HM', 'HMD', '334', u'Heard Island and McDonald Islands'),
+ ('VA', 'VAT', '336', u'Holy See (Vatican City State)'),
+ ('HN', 'HND', '340', u'Honduras'),
+ ('HK', 'HKG', '344', u'Hong Kong'),
+ ('HU', 'HUN', '348', u'Hungary'),
+ ('IS', 'ISL', '352', u'Iceland'),
+ ('IN', 'IND', '356', u'India'),
+ ('ID', 'IDN', '360', u'Indonesia'),
+ ('IR', 'IRN', '364', u'Iran, Islamic Republic of'),
+ ('IQ', 'IRQ', '368', u'Iraq'),
+ ('IE', 'IRL', '372', u'Ireland'),
+ ('IM', 'IMN', '833', u'Isle of Man'),
+ ('IL', 'ISR', '376', u'Israel'),
+ ('IT', 'ITA', '380', u'Italy'),
+ ('JM', 'JAM', '388', u'Jamaica'),
+ ('JP', 'JPN', '392', u'Japan'),
+ ('JE', 'JEY', '832', u'Jersey'),
+ ('JO', 'JOR', '400', u'Jordan'),
+ ('KZ', 'KAZ', '398', u'Kazakhstan'),
+ ('KE', 'KEN', '404', u'Kenya'),
+ ('KI', 'KIR', '296', u'Kiribati'),
+ ('KP', 'PRK', '408', u'Korea, Democratic People\'s Republic of'),
+ ('KR', 'KOR', '410', u'Korea, Republic of'),
+ ('KW', 'KWT', '414', u'Kuwait'),
+ ('KG', 'KGZ', '417', u'Kyrgyzstan'),
+ ('LA', 'LAO', '418', u'Lao People\'s Democratic Republic'),
+ ('LV', 'LVA', '428', u'Latvia'),
+ ('LB', 'LBN', '422', u'Lebanon'),
+ ('LS', 'LSO', '426', u'Lesotho'),
+ ('LR', 'LBR', '430', u'Liberia'),
+ ('LY', 'LBY', '434', u'Libya'),
+ ('LI', 'LIE', '438', u'Liechtenstein'),
+ ('LT', 'LTU', '440', u'Lithuania'),
+ ('LU', 'LUX', '442', u'Luxembourg'),
+ ('MO', 'MAC', '446', u'Macao'),
+ ('MK', 'MKD', '807', u'Macedonia, Republic of'),
+ ('MG', 'MDG', '450', u'Madagascar'),
+ ('MW', 'MWI', '454', u'Malawi'),
+ ('MY', 'MYS', '458', u'Malaysia'),
+ ('MV', 'MDV', '462', u'Maldives'),
+ ('ML', 'MLI', '466', u'Mali'),
+ ('MT', 'MLT', '470', u'Malta'),
+ ('MH', 'MHL', '584', u'Marshall Islands'),
+ ('MQ', 'MTQ', '474', u'Martinique'),
+ ('MR', 'MRT', '478', u'Mauritania'),
+ ('MU', 'MUS', '480', u'Mauritius'),
+ ('YT', 'MYT', '175', u'Mayotte'),
+ ('MX', 'MEX', '484', u'Mexico'),
+ ('FM', 'FSM', '583', u'Micronesia, Federated States of'),
+ ('MD', 'MDA', '498', u'Moldova, Republic of'),
+ ('MC', 'MCO', '492', u'Monaco'),
+ ('MN', 'MNG', '496', u'Mongolia'),
+ ('ME', 'MNE', '499', u'Montenegro'),
+ ('MS', 'MSR', '500', u'Montserrat'),
+ ('MA', 'MAR', '504', u'Morocco'),
+ ('MZ', 'MOZ', '508', u'Mozambique'),
+ ('MM', 'MMR', '104', u'Myanmar'),
+ ('NA', 'NAM', '516', u'Namibia'),
+ ('NR', 'NRU', '520', u'Nauru'),
+ ('NP', 'NPL', '524', u'Nepal'),
+ ('NL', 'NLD', '528', u'Netherlands'),
+ ('NC', 'NCL', '540', u'New Caledonia'),
+ ('NZ', 'NZL', '554', u'New Zealand'),
+ ('NI', 'NIC', '558', u'Nicaragua'),
+ ('NE', 'NER', '562', u'Niger'),
+ ('NG', 'NGA', '566', u'Nigeria'),
+ ('NU', 'NIU', '570', u'Niue'),
+ ('NF', 'NFK', '574', u'Norfolk Island'),
+ ('MP', 'MNP', '580', u'Northern Mariana Islands'),
+ ('NO', 'NOR', '578', u'Norway'),
+ ('OM', 'OMN', '512', u'Oman'),
+ ('PK', 'PAK', '586', u'Pakistan'),
+ ('PW', 'PLW', '585', u'Palau'),
+ ('PS', 'PSE', '275', u'Palestinian Territory, Occupied'),
+ ('PA', 'PAN', '591', u'Panama'),
+ ('PG', 'PNG', '598', u'Papua New Guinea'),
+ ('PY', 'PRY', '600', u'Paraguay'),
+ ('PE', 'PER', '604', u'Peru'),
+ ('PH', 'PHL', '608', u'Philippines'),
+ ('PN', 'PCN', '612', u'Pitcairn'),
+ ('PL', 'POL', '616', u'Poland'),
+ ('PT', 'PRT', '620', u'Portugal'),
+ ('PR', 'PRI', '630', u'Puerto Rico'),
+ ('QA', 'QAT', '634', u'Qatar'),
+ ('RE', 'REU', '638', u'Réunion'),
+ ('RO', 'ROU', '642', u'Romania'),
+ ('RU', 'RUS', '643', u'Russian Federation'),
+ ('RW', 'RWA', '646', u'Rwanda'),
+ ('BL', 'BLM', '652', u'Saint Barthélemy'),
+ ('SH', 'SHN', '654', u'Saint Helena, Ascension and Tristan da Cunha'),
+ ('KN', 'KNA', '659', u'Saint Kitts and Nevis'),
+ ('LC', 'LCA', '662', u'Saint Lucia'),
+ ('MF', 'MAF', '663', u'Saint Martin (French part)'),
+ ('PM', 'SPM', '666', u'Saint Pierre and Miquelon'),
+ ('VC', 'VCT', '670', u'Saint Vincent and the Grenadines'),
+ ('WS', 'WSM', '882', u'Samoa'),
+ ('SM', 'SMR', '674', u'San Marino'),
+ ('ST', 'STP', '678', u'Sao Tome and Principe'),
+ ('SA', 'SAU', '682', u'Saudi Arabia'),
+ ('SN', 'SEN', '686', u'Senegal'),
+ ('RS', 'SRB', '688', u'Serbia'),
+ ('SC', 'SYC', '690', u'Seychelles'),
+ ('SL', 'SLE', '694', u'Sierra Leone'),
+ ('SG', 'SGP', '702', u'Singapore'),
+ ('SX', 'SXM', '534', u'Sint Maarten (Dutch part)'),
+ ('SK', 'SVK', '703', u'Slovakia'),
+ ('SI', 'SVN', '705', u'Slovenia'),
+ ('SB', 'SLB', '090', u'Solomon Islands'),
+ ('SO', 'SOM', '706', u'Somalia'),
+ ('ZA', 'ZAF', '710', u'South Africa'),
+ ('GS', 'SGS', '239', u'South Georgia and the South Sandwich Islands'),
+ ('ES', 'ESP', '724', u'Spain'),
+ ('LK', 'LKA', '144', u'Sri Lanka'),
+ ('SD', 'SDN', '729', u'Sudan'),
+ ('SR', 'SUR', '740', u'Suriname'),
+ ('SS', 'SSD', '728', u'South Sudan'),
+ ('SJ', 'SJM', '744', u'Svalbard and Jan Mayen'),
+ ('SZ', 'SWZ', '748', u'Swaziland'),
+ ('SE', 'SWE', '752', u'Sweden'),
+ ('CH', 'CHE', '756', u'Switzerland'),
+ ('SY', 'SYR', '760', u'Syrian Arab Republic'),
+ ('TW', 'TWN', '158', u'Taiwan, Province of China'),
+ ('TJ', 'TJK', '762', u'Tajikistan'),
+ ('TZ', 'TZA', '834', u'Tanzania, United Republic of'),
+ ('TH', 'THA', '764', u'Thailand'),
+ ('TL', 'TLS', '626', u'Timor-Leste'),
+ ('TG', 'TGO', '768', u'Togo'),
+ ('TK', 'TKL', '772', u'Tokelau'),
+ ('TO', 'TON', '776', u'Tonga'),
+ ('TT', 'TTO', '780', u'Trinidad and Tobago'),
+ ('TN', 'TUN', '788', u'Tunisia'),
+ ('TR', 'TUR', '792', u'Turkey'),
+ ('TM', 'TKM', '795', u'Turkmenistan'),
+ ('TC', 'TCA', '796', u'Turks and Caicos Islands'),
+ ('TV', 'TUV', '798', u'Tuvalu'),
+ ('UG', 'UGA', '800', u'Uganda'),
+ ('UA', 'UKR', '804', u'Ukraine'),
+ ('AE', 'ARE', '784', u'United Arab Emirates'),
+ ('GB', 'GBR', '826', u'United Kingdom'),
+ ('US', 'USA', '840', u'United States'),
+ ('UM', 'UMI', '581', u'United States Minor Outlying Islands'),
+ ('UY', 'URY', '858', u'Uruguay'),
+ ('UZ', 'UZB', '860', u'Uzbekistan'),
+ ('VU', 'VUT', '548', u'Vanuatu'),
+ ('VE', 'VEN', '862', u'Venezuela, Bolivarian Republic of'),
+ ('VN', 'VNM', '704', u'Viet Nam'),
+ ('VG', 'VGB', '092', u'Virgin Islands, British'),
+ ('VI', 'VIR', '850', u'Virgin Islands, U.S.'),
+ ('WF', 'WLF', '876', u'Wallis and Futuna'),
+ ('EH', 'ESH', '732', u'Western Sahara'),
+ ('YE', 'YEM', '887', u'Yemen'),
+ ('ZM', 'ZMB', '894', u'Zambia'),
+ ('ZW', 'ZWE', '716', u'Zimbabwe')]
+
+
+LANGUAGES = [('aar', '', 'aa', u'Afar', u'afar'),
+ ('abk', '', 'ab', u'Abkhazian', u'abkhaze'),
+ ('ace', '', '', u'Achinese', u'aceh'),
+ ('ach', '', '', u'Acoli', u'acoli'),
+ ('ada', '', '', u'Adangme', u'adangme'),
+ ('ady', '', '', u'Adyghe; Adygei', u'adyghé'),
+ ('afa', '', '', u'Afro-Asiatic languages', u'afro-asiatiques, langues'),
+ ('afh', '', '', u'Afrihili', u'afrihili'),
+ ('afr', '', 'af', u'Afrikaans', u'afrikaans'),
+ ('ain', '', '', u'Ainu', u'aïnou'),
+ ('aka', '', 'ak', u'Akan', u'akan'),
+ ('akk', '', '', u'Akkadian', u'akkadien'),
+ ('alb', 'sqi', 'sq', u'Albanian', u'albanais'),
+ ('ale', '', '', u'Aleut', u'aléoute'),
+ ('alg', '', '', u'Algonquian languages', u'algonquines, langues'),
+ ('alt', '', '', u'Southern Altai', u'altai du Sud'),
+ ('amh', '', 'am', u'Amharic', u'amharique'),
+ ('ang', '', '', u'English, Old (ca.450-1100)', u'anglo-saxon (ca.450-1100)'),
+ ('anp', '', '', u'Angika', u'angika'),
+ ('apa', '', '', u'Apache languages', u'apaches, langues'),
+ ('ara', '', 'ar', u'Arabic', u'arabe'),
+ ('arc', '', '', u'Official Aramaic (700-300 BCE); Imperial Aramaic (700-300 BCE)', u'araméen d\'empire (700-300 BCE)'),
+ ('arg', '', 'an', u'Aragonese', u'aragonais'),
+ ('arm', 'hye', 'hy', u'Armenian', u'arménien'),
+ ('arn', '', '', u'Mapudungun; Mapuche', u'mapudungun; mapuche; mapuce'),
+ ('arp', '', '', u'Arapaho', u'arapaho'),
+ ('art', '', '', u'Artificial languages', u'artificielles, langues'),
+ ('arw', '', '', u'Arawak', u'arawak'),
+ ('asm', '', 'as', u'Assamese', u'assamais'),
+ ('ast', '', '', u'Asturian; Bable; Leonese; Asturleonese', u'asturien; bable; léonais; asturoléonais'),
+ ('ath', '', '', u'Athapascan languages', u'athapascanes, langues'),
+ ('aus', '', '', u'Australian languages', u'australiennes, langues'),
+ ('ava', '', 'av', u'Avaric', u'avar'),
+ ('ave', '', 'ae', u'Avestan', u'avestique'),
+ ('awa', '', '', u'Awadhi', u'awadhi'),
+ ('aym', '', 'ay', u'Aymara', u'aymara'),
+ ('aze', '', 'az', u'Azerbaijani', u'azéri'),
+ ('bad', '', '', u'Banda languages', u'banda, langues'),
+ ('bai', '', '', u'Bamileke languages', u'bamiléké, langues'),
+ ('bak', '', 'ba', u'Bashkir', u'bachkir'),
+ ('bal', '', '', u'Baluchi', u'baloutchi'),
+ ('bam', '', 'bm', u'Bambara', u'bambara'),
+ ('ban', '', '', u'Balinese', u'balinais'),
+ ('baq', 'eus', 'eu', u'Basque', u'basque'),
+ ('bas', '', '', u'Basa', u'basa'),
+ ('bat', '', '', u'Baltic languages', u'baltes, langues'),
+ ('bej', '', '', u'Beja; Bedawiyet', u'bedja'),
+ ('bel', '', 'be', u'Belarusian', u'biélorusse'),
+ ('bem', '', '', u'Bemba', u'bemba'),
+ ('ben', '', 'bn', u'Bengali', u'bengali'),
+ ('ber', '', '', u'Berber languages', u'berbères, langues'),
+ ('bho', '', '', u'Bhojpuri', u'bhojpuri'),
+ ('bih', '', 'bh', u'Bihari languages', u'langues biharis'),
+ ('bik', '', '', u'Bikol', u'bikol'),
+ ('bin', '', '', u'Bini; Edo', u'bini; edo'),
+ ('bis', '', 'bi', u'Bislama', u'bichlamar'),
+ ('bla', '', '', u'Siksika', u'blackfoot'),
+ ('bnt', '', '', u'Bantu (Other)', u'bantoues, autres langues'),
+ ('bos', '', 'bs', u'Bosnian', u'bosniaque'),
+ ('bra', '', '', u'Braj', u'braj'),
+ ('bre', '', 'br', u'Breton', u'breton'),
+ ('btk', '', '', u'Batak languages', u'batak, langues'),
+ ('bua', '', '', u'Buriat', u'bouriate'),
+ ('bug', '', '', u'Buginese', u'bugi'),
+ ('bul', '', 'bg', u'Bulgarian', u'bulgare'),
+ ('bur', 'mya', 'my', u'Burmese', u'birman'),
+ ('byn', '', '', u'Blin; Bilin', u'blin; bilen'),
+ ('cad', '', '', u'Caddo', u'caddo'),
+ ('cai', '', '', u'Central American Indian languages', u'amérindiennes de L\'Amérique centrale, langues'),
+ ('car', '', '', u'Galibi Carib', u'karib; galibi; carib'),
+ ('cat', '', 'ca', u'Catalan; Valencian', u'catalan; valencien'),
+ ('cau', '', '', u'Caucasian languages', u'caucasiennes, langues'),
+ ('ceb', '', '', u'Cebuano', u'cebuano'),
+ ('cel', '', '', u'Celtic languages', u'celtiques, langues; celtes, langues'),
+ ('cha', '', 'ch', u'Chamorro', u'chamorro'),
+ ('chb', '', '', u'Chibcha', u'chibcha'),
+ ('che', '', 'ce', u'Chechen', u'tchétchène'),
+ ('chg', '', '', u'Chagatai', u'djaghataï'),
+ ('chi', 'zho', 'zh', u'Chinese', u'chinois'),
+ ('chk', '', '', u'Chuukese', u'chuuk'),
+ ('chm', '', '', u'Mari', u'mari'),
+ ('chn', '', '', u'Chinook jargon', u'chinook, jargon'),
+ ('cho', '', '', u'Choctaw', u'choctaw'),
+ ('chp', '', '', u'Chipewyan; Dene Suline', u'chipewyan'),
+ ('chr', '', '', u'Cherokee', u'cherokee'),
+ ('chu', '', 'cu', u'Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic', u'slavon d\'église; vieux slave; slavon liturgique; vieux bulgare'),
+ ('chv', '', 'cv', u'Chuvash', u'tchouvache'),
+ ('chy', '', '', u'Cheyenne', u'cheyenne'),
+ ('cmc', '', '', u'Chamic languages', u'chames, langues'),
+ ('cop', '', '', u'Coptic', u'copte'),
+ ('cor', '', 'kw', u'Cornish', u'cornique'),
+ ('cos', '', 'co', u'Corsican', u'corse'),
+ ('cpe', '', '', u'Creoles and pidgins, English based', u'créoles et pidgins basés sur l\'anglais'),
+ ('cpf', '', '', u'Creoles and pidgins, French-based ', u'créoles et pidgins basés sur le français'),
+ ('cpp', '', '', u'Creoles and pidgins, Portuguese-based ', u'créoles et pidgins basés sur le portugais'),
+ ('cre', '', 'cr', u'Cree', u'cree'),
+ ('crh', '', '', u'Crimean Tatar; Crimean Turkish', u'tatar de Crimé'),
+ ('crp', '', '', u'Creoles and pidgins ', u'créoles et pidgins'),
+ ('csb', '', '', u'Kashubian', u'kachoube'),
+ ('cus', '', '', u'Cushitic languages', u'couchitiques, langues'),
+ ('cze', 'ces', 'cs', u'Czech', u'tchèque'),
+ ('dak', '', '', u'Dakota', u'dakota'),
+ ('dan', '', 'da', u'Danish', u'danois'),
+ ('dar', '', '', u'Dargwa', u'dargwa'),
+ ('day', '', '', u'Land Dayak languages', u'dayak, langues'),
+ ('del', '', '', u'Delaware', u'delaware'),
+ ('den', '', '', u'Slave (Athapascan)', u'esclave (athapascan)'),
+ ('dgr', '', '', u'Dogrib', u'dogrib'),
+ ('din', '', '', u'Dinka', u'dinka'),
+ ('div', '', 'dv', u'Divehi; Dhivehi; Maldivian', u'maldivien'),
+ ('doi', '', '', u'Dogri', u'dogri'),
+ ('dra', '', '', u'Dravidian languages', u'dravidiennes, langues'),
+ ('dsb', '', '', u'Lower Sorbian', u'bas-sorabe'),
+ ('dua', '', '', u'Duala', u'douala'),
+ ('dum', '', '', u'Dutch, Middle (ca.1050-1350)', u'néerlandais moyen (ca. 1050-1350)'),
+ ('dut', 'nld', 'nl', u'Dutch; Flemish', u'néerlandais; flamand'),
+ ('dyu', '', '', u'Dyula', u'dioula'),
+ ('dzo', '', 'dz', u'Dzongkha', u'dzongkha'),
+ ('efi', '', '', u'Efik', u'efik'),
+ ('egy', '', '', u'Egyptian (Ancient)', u'égyptien'),
+ ('eka', '', '', u'Ekajuk', u'ekajuk'),
+ ('elx', '', '', u'Elamite', u'élamite'),
+ ('eng', '', 'en', u'English', u'anglais'),
+ ('enm', '', '', u'English, Middle (1100-1500)', u'anglais moyen (1100-1500)'),
+ ('epo', '', 'eo', u'Esperanto', u'espéranto'),
+ ('est', '', 'et', u'Estonian', u'estonien'),
+ ('ewe', '', 'ee', u'Ewe', u'éwé'),
+ ('ewo', '', '', u'Ewondo', u'éwondo'),
+ ('fan', '', '', u'Fang', u'fang'),
+ ('fao', '', 'fo', u'Faroese', u'féroïen'),
+ ('fat', '', '', u'Fanti', u'fanti'),
+ ('fij', '', 'fj', u'Fijian', u'fidjien'),
+ ('fil', '', '', u'Filipino; Pilipino', u'filipino; pilipino'),
+ ('fin', '', 'fi', u'Finnish', u'finnois'),
+ ('fiu', '', '', u'Finno-Ugrian languages', u'finno-ougriennes, langues'),
+ ('fon', '', '', u'Fon', u'fon'),
+ ('fre', 'fra', 'fr', u'French', u'français'),
+ ('frm', '', '', u'French, Middle (ca.1400-1600)', u'français moyen (1400-1600)'),
+ ('fro', '', '', u'French, Old (842-ca.1400)', u'français ancien (842-ca.1400)'),
+ ('frr', '', '', u'Northern Frisian', u'frison septentrional'),
+ ('frs', '', '', u'Eastern Frisian', u'frison oriental'),
+ ('fry', '', 'fy', u'Western Frisian', u'frison occidental'),
+ ('ful', '', 'ff', u'Fulah', u'peul'),
+ ('fur', '', '', u'Friulian', u'frioulan'),
+ ('gaa', '', '', u'Ga', u'ga'),
+ ('gay', '', '', u'Gayo', u'gayo'),
+ ('gba', '', '', u'Gbaya', u'gbaya'),
+ ('gem', '', '', u'Germanic languages', u'germaniques, langues'),
+ ('geo', 'kat', 'ka', u'Georgian', u'géorgien'),
+ ('ger', 'deu', 'de', u'German', u'allemand'),
+ ('gez', '', '', u'Geez', u'guèze'),
+ ('gil', '', '', u'Gilbertese', u'kiribati'),
+ ('gla', '', 'gd', u'Gaelic; Scottish Gaelic', u'gaélique; gaélique écossais'),
+ ('gle', '', 'ga', u'Irish', u'irlandais'),
+ ('glg', '', 'gl', u'Galician', u'galicien'),
+ ('glv', '', 'gv', u'Manx', u'manx; mannois'),
+ ('gmh', '', '', u'German, Middle High (ca.1050-1500)', u'allemand, moyen haut (ca. 1050-1500)'),
+ ('goh', '', '', u'German, Old High (ca.750-1050)', u'allemand, vieux haut (ca. 750-1050)'),
+ ('gon', '', '', u'Gondi', u'gond'),
+ ('gor', '', '', u'Gorontalo', u'gorontalo'),
+ ('got', '', '', u'Gothic', u'gothique'),
+ ('grb', '', '', u'Grebo', u'grebo'),
+ ('grc', '', '', u'Greek, Ancient (to 1453)', u'grec ancien (jusqu\'à 1453)'),
+ ('gre', 'ell', 'el', u'Greek, Modern (1453-)', u'grec moderne (après 1453)'),
+ ('grn', '', 'gn', u'Guarani', u'guarani'),
+ ('gsw', '', '', u'Swiss German; Alemannic; Alsatian', u'suisse alémanique; alémanique; alsacien'),
+ ('guj', '', 'gu', u'Gujarati', u'goudjrati'),
+ ('gwi', '', '', u'Gwich\'in', u'gwich\'in'),
+ ('hai', '', '', u'Haida', u'haida'),
+ ('hat', '', 'ht', u'Haitian; Haitian Creole', u'haïtien; créole haïtien'),
+ ('hau', '', 'ha', u'Hausa', u'haoussa'),
+ ('haw', '', '', u'Hawaiian', u'hawaïen'),
+ ('heb', '', 'he', u'Hebrew', u'hébreu'),
+ ('her', '', 'hz', u'Herero', u'herero'),
+ ('hil', '', '', u'Hiligaynon', u'hiligaynon'),
+ ('him', '', '', u'Himachali languages; Western Pahari languages', u'langues himachalis; langues paharis occidentales'),
+ ('hin', '', 'hi', u'Hindi', u'hindi'),
+ ('hit', '', '', u'Hittite', u'hittite'),
+ ('hmn', '', '', u'Hmong; Mong', u'hmong'),
+ ('hmo', '', 'ho', u'Hiri Motu', u'hiri motu'),
+ ('hrv', '', 'hr', u'Croatian', u'croate'),
+ ('hsb', '', '', u'Upper Sorbian', u'haut-sorabe'),
+ ('hun', '', 'hu', u'Hungarian', u'hongrois'),
+ ('hup', '', '', u'Hupa', u'hupa'),
+ ('iba', '', '', u'Iban', u'iban'),
+ ('ibo', '', 'ig', u'Igbo', u'igbo'),
+ ('ice', 'isl', 'is', u'Icelandic', u'islandais'),
+ ('ido', '', 'io', u'Ido', u'ido'),
+ ('iii', '', 'ii', u'Sichuan Yi; Nuosu', u'yi de Sichuan'),
+ ('ijo', '', '', u'Ijo languages', u'ijo, langues'),
+ ('iku', '', 'iu', u'Inuktitut', u'inuktitut'),
+ ('ile', '', 'ie', u'Interlingue; Occidental', u'interlingue'),
+ ('ilo', '', '', u'Iloko', u'ilocano'),
+ ('ina', '', 'ia', u'Interlingua (International Auxiliary Language Association)', u'interlingua (langue auxiliaire internationale)'),
+ ('inc', '', '', u'Indic languages', u'indo-aryennes, langues'),
+ ('ind', '', 'id', u'Indonesian', u'indonésien'),
+ ('ine', '', '', u'Indo-European languages', u'indo-européennes, langues'),
+ ('inh', '', '', u'Ingush', u'ingouche'),
+ ('ipk', '', 'ik', u'Inupiaq', u'inupiaq'),
+ ('ira', '', '', u'Iranian languages', u'iraniennes, langues'),
+ ('iro', '', '', u'Iroquoian languages', u'iroquoises, langues'),
+ ('ita', '', 'it', u'Italian', u'italien'),
+ ('jav', '', 'jv', u'Javanese', u'javanais'),
+ ('jbo', '', '', u'Lojban', u'lojban'),
+ ('jpn', '', 'ja', u'Japanese', u'japonais'),
+ ('jpr', '', '', u'Judeo-Persian', u'judéo-persan'),
+ ('jrb', '', '', u'Judeo-Arabic', u'judéo-arabe'),
+ ('kaa', '', '', u'Kara-Kalpak', u'karakalpak'),
+ ('kab', '', '', u'Kabyle', u'kabyle'),
+ ('kac', '', '', u'Kachin; Jingpho', u'kachin; jingpho'),
+ ('kal', '', 'kl', u'Kalaallisut; Greenlandic', u'groenlandais'),
+ ('kam', '', '', u'Kamba', u'kamba'),
+ ('kan', '', 'kn', u'Kannada', u'kannada'),
+ ('kar', '', '', u'Karen languages', u'karen, langues'),
+ ('kas', '', 'ks', u'Kashmiri', u'kashmiri'),
+ ('kau', '', 'kr', u'Kanuri', u'kanouri'),
+ ('kaw', '', '', u'Kawi', u'kawi'),
+ ('kaz', '', 'kk', u'Kazakh', u'kazakh'),
+ ('kbd', '', '', u'Kabardian', u'kabardien'),
+ ('kha', '', '', u'Khasi', u'khasi'),
+ ('khi', '', '', u'Khoisan languages', u'khoïsan, langues'),
+ ('khm', '', 'km', u'Central Khmer', u'khmer central'),
+ ('kho', '', '', u'Khotanese; Sakan', u'khotanais; sakan'),
+ ('kik', '', 'ki', u'Kikuyu; Gikuyu', u'kikuyu'),
+ ('kin', '', 'rw', u'Kinyarwanda', u'rwanda'),
+ ('kir', '', 'ky', u'Kirghiz; Kyrgyz', u'kirghiz'),
+ ('kmb', '', '', u'Kimbundu', u'kimbundu'),
+ ('kok', '', '', u'Konkani', u'konkani'),
+ ('kom', '', 'kv', u'Komi', u'kom'),
+ ('kon', '', 'kg', u'Kongo', u'kongo'),
+ ('kor', '', 'ko', u'Korean', u'coréen'),
+ ('kos', '', '', u'Kosraean', u'kosrae'),
+ ('kpe', '', '', u'Kpelle', u'kpellé'),
+ ('krc', '', '', u'Karachay-Balkar', u'karatchai balkar'),
+ ('krl', '', '', u'Karelian', u'carélien'),
+ ('kro', '', '', u'Kru languages', u'krou, langues'),
+ ('kru', '', '', u'Kurukh', u'kurukh'),
+ ('kua', '', 'kj', u'Kuanyama; Kwanyama', u'kuanyama; kwanyama'),
+ ('kum', '', '', u'Kumyk', u'koumyk'),
+ ('kur', '', 'ku', u'Kurdish', u'kurde'),
+ ('kut', '', '', u'Kutenai', u'kutenai'),
+ ('lad', '', '', u'Ladino', u'judéo-espagnol'),
+ ('lah', '', '', u'Lahnda', u'lahnda'),
+ ('lam', '', '', u'Lamba', u'lamba'),
+ ('lao', '', 'lo', u'Lao', u'lao'),
+ ('lat', '', 'la', u'Latin', u'latin'),
+ ('lav', '', 'lv', u'Latvian', u'letton'),
+ ('lez', '', '', u'Lezghian', u'lezghien'),
+ ('lim', '', 'li', u'Limburgan; Limburger; Limburgish', u'limbourgeois'),
+ ('lin', '', 'ln', u'Lingala', u'lingala'),
+ ('lit', '', 'lt', u'Lithuanian', u'lituanien'),
+ ('lol', '', '', u'Mongo', u'mongo'),
+ ('loz', '', '', u'Lozi', u'lozi'),
+ ('ltz', '', 'lb', u'Luxembourgish; Letzeburgesch', u'luxembourgeois'),
+ ('lua', '', '', u'Luba-Lulua', u'luba-lulua'),
+ ('lub', '', 'lu', u'Luba-Katanga', u'luba-katanga'),
+ ('lug', '', 'lg', u'Ganda', u'ganda'),
+ ('lui', '', '', u'Luiseno', u'luiseno'),
+ ('lun', '', '', u'Lunda', u'lunda'),
+ ('luo', '', '', u'Luo (Kenya and Tanzania)', u'luo (Kenya et Tanzanie)'),
+ ('lus', '', '', u'Lushai', u'lushai'),
+ ('mac', 'mkd', 'mk', u'Macedonian', u'macédonien'),
+ ('mad', '', '', u'Madurese', u'madourais'),
+ ('mag', '', '', u'Magahi', u'magahi'),
+ ('mah', '', 'mh', u'Marshallese', u'marshall'),
+ ('mai', '', '', u'Maithili', u'maithili'),
+ ('mak', '', '', u'Makasar', u'makassar'),
+ ('mal', '', 'ml', u'Malayalam', u'malayalam'),
+ ('man', '', '', u'Mandingo', u'mandingue'),
+ ('mao', 'mri', 'mi', u'Maori', u'maori'),
+ ('map', '', '', u'Austronesian languages', u'austronésiennes, langues'),
+ ('mar', '', 'mr', u'Marathi', u'marathe'),
+ ('mas', '', '', u'Masai', u'massaï'),
+ ('may', 'msa', 'ms', u'Malay', u'malais'),
+ ('mdf', '', '', u'Moksha', u'moksa'),
+ ('mdr', '', '', u'Mandar', u'mandar'),
+ ('men', '', '', u'Mende', u'mendé'),
+ ('mga', '', '', u'Irish, Middle (900-1200)', u'irlandais moyen (900-1200)'),
+ ('mic', '', '', u'Mi\'kmaq; Micmac', u'mi\'kmaq; micmac'),
+ ('min', '', '', u'Minangkabau', u'minangkabau'),
+ ('mkh', '', '', u'Mon-Khmer languages', u'môn-khmer, langues'),
+ ('mlg', '', 'mg', u'Malagasy', u'malgache'),
+ ('mlt', '', 'mt', u'Maltese', u'maltais'),
+ ('mnc', '', '', u'Manchu', u'mandchou'),
+ ('mni', '', '', u'Manipuri', u'manipuri'),
+ ('mno', '', '', u'Manobo languages', u'manobo, langues'),
+ ('moh', '', '', u'Mohawk', u'mohawk'),
+ ('mon', '', 'mn', u'Mongolian', u'mongol'),
+ ('mos', '', '', u'Mossi', u'moré'),
+ ('mun', '', '', u'Munda languages', u'mounda, langues'),
+ ('mus', '', '', u'Creek', u'muskogee'),
+ ('mwl', '', '', u'Mirandese', u'mirandais'),
+ ('mwr', '', '', u'Marwari', u'marvari'),
+ ('myn', '', '', u'Mayan languages', u'maya, langues'),
+ ('myv', '', '', u'Erzya', u'erza'),
+ ('nah', '', '', u'Nahuatl languages', u'nahuatl, langues'),
+ ('nai', '', '', u'North American Indian languages', u'nord-amérindiennes, langues'),
+ ('nap', '', '', u'Neapolitan', u'napolitain'),
+ ('nau', '', 'na', u'Nauru', u'nauruan'),
+ ('nav', '', 'nv', u'Navajo; Navaho', u'navaho'),
+ ('nbl', '', 'nr', u'Ndebele, South; South Ndebele', u'ndébélé du Sud'),
+ ('nde', '', 'nd', u'Ndebele, North; North Ndebele', u'ndébélé du Nord'),
+ ('ndo', '', 'ng', u'Ndonga', u'ndonga'),
+ ('nds', '', '', u'Low German; Low Saxon; German, Low; Saxon, Low', u'bas allemand; bas saxon; allemand, bas; saxon, bas'),
+ ('nep', '', 'ne', u'Nepali', u'népalais'),
+ ('new', '', '', u'Nepal Bhasa; Newari', u'nepal bhasa; newari'),
+ ('nia', '', '', u'Nias', u'nias'),
+ ('nic', '', '', u'Niger-Kordofanian languages', u'nigéro-kordofaniennes, langues'),
+ ('niu', '', '', u'Niuean', u'niué'),
+ ('nno', '', 'nn', u'Norwegian Nynorsk; Nynorsk, Norwegian', u'norvégien nynorsk; nynorsk, norvégien'),
+ ('nob', '', 'nb', u'Bokmål, Norwegian; Norwegian Bokmål', u'norvégien bokmål'),
+ ('nog', '', '', u'Nogai', u'nogaï; nogay'),
+ ('non', '', '', u'Norse, Old', u'norrois, vieux'),
+ ('nor', '', 'no', u'Norwegian', u'norvégien'),
+ ('nqo', '', '', u'N\'Ko', u'n\'ko'),
+ ('nso', '', '', u'Pedi; Sepedi; Northern Sotho', u'pedi; sepedi; sotho du Nord'),
+ ('nub', '', '', u'Nubian languages', u'nubiennes, langues'),
+ ('nwc', '', '', u'Classical Newari; Old Newari; Classical Nepal Bhasa', u'newari classique'),
+ ('nya', '', 'ny', u'Chichewa; Chewa; Nyanja', u'chichewa; chewa; nyanja'),
+ ('nym', '', '', u'Nyamwezi', u'nyamwezi'),
+ ('nyn', '', '', u'Nyankole', u'nyankolé'),
+ ('nyo', '', '', u'Nyoro', u'nyoro'),
+ ('nzi', '', '', u'Nzima', u'nzema'),
+ ('oci', '', 'oc', u'Occitan (post 1500); Provençal', u'occitan (après 1500); provençal'),
+ ('oji', '', 'oj', u'Ojibwa', u'ojibwa'),
+ ('ori', '', 'or', u'Oriya', u'oriya'),
+ ('orm', '', 'om', u'Oromo', u'galla'),
+ ('osa', '', '', u'Osage', u'osage'),
+ ('oss', '', 'os', u'Ossetian; Ossetic', u'ossète'),
+ ('ota', '', '', u'Turkish, Ottoman (1500-1928)', u'turc ottoman (1500-1928)'),
+ ('oto', '', '', u'Otomian languages', u'otomi, langues'),
+ ('paa', '', '', u'Papuan languages', u'papoues, langues'),
+ ('pag', '', '', u'Pangasinan', u'pangasinan'),
+ ('pal', '', '', u'Pahlavi', u'pahlavi'),
+ ('pam', '', '', u'Pampanga; Kapampangan', u'pampangan'),
+ ('pan', '', 'pa', u'Panjabi; Punjabi', u'pendjabi'),
+ ('pap', '', '', u'Papiamento', u'papiamento'),
+ ('pau', '', '', u'Palauan', u'palau'),
+ ('peo', '', '', u'Persian, Old (ca.600-400 B.C.)', u'perse, vieux (ca. 600-400 av. J.-C.)'),
+ ('per', 'fas', 'fa', u'Persian', u'persan'),
+ ('phi', '', '', u'Philippine languages', u'philippines, langues'),
+ ('phn', '', '', u'Phoenician', u'phénicien'),
+ ('pli', '', 'pi', u'Pali', u'pali'),
+ ('pol', '', 'pl', u'Polish', u'polonais'),
+ ('pon', '', '', u'Pohnpeian', u'pohnpei'),
+ ('por', '', 'pt', u'Portuguese', u'portugais'),
+ ('pra', '', '', u'Prakrit languages', u'prâkrit, langues'),
+ ('pro', '', '', u'Provençal, Old (to 1500)', u'provençal ancien (jusqu\'à 1500)'),
+ ('pus', '', 'ps', u'Pushto; Pashto', u'pachto'),
+ ('que', '', 'qu', u'Quechua', u'quechua'),
+ ('raj', '', '', u'Rajasthani', u'rajasthani'),
+ ('rap', '', '', u'Rapanui', u'rapanui'),
+ ('rar', '', '', u'Rarotongan; Cook Islands Maori', u'rarotonga; maori des îles Cook'),
+ ('roa', '', '', u'Romance languages', u'romanes, langues'),
+ ('roh', '', 'rm', u'Romansh', u'romanche'),
+ ('rom', '', '', u'Romany', u'tsigane'),
+ ('rum', 'ron', 'ro', u'Romanian; Moldavian; Moldovan', u'roumain; moldave'),
+ ('run', '', 'rn', u'Rundi', u'rundi'),
+ ('rup', '', '', u'Aromanian; Arumanian; Macedo-Romanian', u'aroumain; macédo-roumain'),
+ ('rus', '', 'ru', u'Russian', u'russe'),
+ ('sad', '', '', u'Sandawe', u'sandawe'),
+ ('sag', '', 'sg', u'Sango', u'sango'),
+ ('sah', '', '', u'Yakut', u'iakoute'),
+ ('sai', '', '', u'South American Indian (Other)', u'indiennes d\'Amérique du Sud, autres langues'),
+ ('sal', '', '', u'Salishan languages', u'salishennes, langues'),
+ ('sam', '', '', u'Samaritan Aramaic', u'samaritain'),
+ ('san', '', 'sa', u'Sanskrit', u'sanskrit'),
+ ('sas', '', '', u'Sasak', u'sasak'),
+ ('sat', '', '', u'Santali', u'santal'),
+ ('scn', '', '', u'Sicilian', u'sicilien'),
+ ('sco', '', '', u'Scots', u'écossais'),
+ ('sel', '', '', u'Selkup', u'selkoupe'),
+ ('sem', '', '', u'Semitic languages', u'sémitiques, langues'),
+ ('sga', '', '', u'Irish, Old (to 900)', u'irlandais ancien (jusqu\'à 900)'),
+ ('sgn', '', '', u'Sign Languages', u'langues des signes'),
+ ('shn', '', '', u'Shan', u'chan'),
+ ('sid', '', '', u'Sidamo', u'sidamo'),
+ ('sin', '', 'si', u'Sinhala; Sinhalese', u'singhalais'),
+ ('sio', '', '', u'Siouan languages', u'sioux, langues'),
+ ('sit', '', '', u'Sino-Tibetan languages', u'sino-tibétaines, langues'),
+ ('sla', '', '', u'Slavic languages', u'slaves, langues'),
+ ('slo', 'slk', 'sk', u'Slovak', u'slovaque'),
+ ('slv', '', 'sl', u'Slovenian', u'slovène'),
+ ('sma', '', '', u'Southern Sami', u'sami du Sud'),
+ ('sme', '', 'se', u'Northern Sami', u'sami du Nord'),
+ ('smi', '', '', u'Sami languages', u'sames, langues'),
+ ('smj', '', '', u'Lule Sami', u'sami de Lule'),
+ ('smn', '', '', u'Inari Sami', u'sami d\'Inari'),
+ ('smo', '', 'sm', u'Samoan', u'samoan'),
+ ('sms', '', '', u'Skolt Sami', u'sami skolt'),
+ ('sna', '', 'sn', u'Shona', u'shona'),
+ ('snd', '', 'sd', u'Sindhi', u'sindhi'),
+ ('snk', '', '', u'Soninke', u'soninké'),
+ ('sog', '', '', u'Sogdian', u'sogdien'),
+ ('som', '', 'so', u'Somali', u'somali'),
+ ('son', '', '', u'Songhai languages', u'songhai, langues'),
+ ('sot', '', 'st', u'Sotho, Southern', u'sotho du Sud'),
+ ('spa', '', 'es', u'Spanish; Castilian', u'espagnol; castillan'),
+ ('srd', '', 'sc', u'Sardinian', u'sarde'),
+ ('srn', '', '', u'Sranan Tongo', u'sranan tongo'),
+ ('srp', '', 'sr', u'Serbian', u'serbe'),
+ ('srr', '', '', u'Serer', u'sérère'),
+ ('ssa', '', '', u'Nilo-Saharan languages', u'nilo-sahariennes, langues'),
+ ('ssw', '', 'ss', u'Swati', u'swati'),
+ ('suk', '', '', u'Sukuma', u'sukuma'),
+ ('sun', '', 'su', u'Sundanese', u'soundanais'),
+ ('sus', '', '', u'Susu', u'soussou'),
+ ('sux', '', '', u'Sumerian', u'sumérien'),
+ ('swa', '', 'sw', u'Swahili', u'swahili'),
+ ('swe', '', 'sv', u'Swedish', u'suédois'),
+ ('syc', '', '', u'Classical Syriac', u'syriaque classique'),
+ ('syr', '', '', u'Syriac', u'syriaque'),
+ ('tah', '', 'ty', u'Tahitian', u'tahitien'),
+ ('tai', '', '', u'Tai languages', u'tai, langues'),
+ ('tam', '', 'ta', u'Tamil', u'tamoul'),
+ ('tat', '', 'tt', u'Tatar', u'tatar'),
+ ('tel', '', 'te', u'Telugu', u'télougou'),
+ ('tem', '', '', u'Timne', u'temne'),
+ ('ter', '', '', u'Tereno', u'tereno'),
+ ('tet', '', '', u'Tetum', u'tetum'),
+ ('tgk', '', 'tg', u'Tajik', u'tadjik'),
+ ('tgl', '', 'tl', u'Tagalog', u'tagalog'),
+ ('tha', '', 'th', u'Thai', u'thaï'),
+ ('tib', 'bod', 'bo', u'Tibetan', u'tibétain'),
+ ('tig', '', '', u'Tigre', u'tigré'),
+ ('tir', '', 'ti', u'Tigrinya', u'tigrigna'),
+ ('tiv', '', '', u'Tiv', u'tiv'),
+ ('tkl', '', '', u'Tokelau', u'tokelau'),
+ ('tlh', '', '', u'Klingon; tlhIngan-Hol', u'klingon'),
+ ('tli', '', '', u'Tlingit', u'tlingit'),
+ ('tmh', '', '', u'Tamashek', u'tamacheq'),
+ ('tog', '', '', u'Tonga (Nyasa)', u'tonga (Nyasa)'),
+ ('ton', '', 'to', u'Tonga (Tonga Islands)', u'tongan (Îles Tonga)'),
+ ('tpi', '', '', u'Tok Pisin', u'tok pisin'),
+ ('tsi', '', '', u'Tsimshian', u'tsimshian'),
+ ('tsn', '', 'tn', u'Tswana', u'tswana'),
+ ('tso', '', 'ts', u'Tsonga', u'tsonga'),
+ ('tuk', '', 'tk', u'Turkmen', u'turkmène'),
+ ('tum', '', '', u'Tumbuka', u'tumbuka'),
+ ('tup', '', '', u'Tupi languages', u'tupi, langues'),
+ ('tur', '', 'tr', u'Turkish', u'turc'),
+ ('tut', '', '', u'Altaic languages', u'altaïques, langues'),
+ ('tvl', '', '', u'Tuvalu', u'tuvalu'),
+ ('twi', '', 'tw', u'Twi', u'twi'),
+ ('tyv', '', '', u'Tuvinian', u'touva'),
+ ('udm', '', '', u'Udmurt', u'oudmourte'),
+ ('uga', '', '', u'Ugaritic', u'ougaritique'),
+ ('uig', '', 'ug', u'Uighur; Uyghur', u'ouïgour'),
+ ('ukr', '', 'uk', u'Ukrainian', u'ukrainien'),
+ ('umb', '', '', u'Umbundu', u'umbundu'),
+ ('und', '', '', u'Undetermined', u'indéterminée'),
+ ('urd', '', 'ur', u'Urdu', u'ourdou'),
+ ('uzb', '', 'uz', u'Uzbek', u'ouszbek'),
+ ('vai', '', '', u'Vai', u'vaï'),
+ ('ven', '', 've', u'Venda', u'venda'),
+ ('vie', '', 'vi', u'Vietnamese', u'vietnamien'),
+ ('vol', '', 'vo', u'Volapük', u'volapük'),
+ ('vot', '', '', u'Votic', u'vote'),
+ ('wak', '', '', u'Wakashan languages', u'wakashanes, langues'),
+ ('wal', '', '', u'Walamo', u'walamo'),
+ ('war', '', '', u'Waray', u'waray'),
+ ('was', '', '', u'Washo', u'washo'),
+ ('wel', 'cym', 'cy', u'Welsh', u'gallois'),
+ ('wen', '', '', u'Sorbian languages', u'sorabes, langues'),
+ ('wln', '', 'wa', u'Walloon', u'wallon'),
+ ('wol', '', 'wo', u'Wolof', u'wolof'),
+ ('xal', '', '', u'Kalmyk; Oirat', u'kalmouk; oïrat'),
+ ('xho', '', 'xh', u'Xhosa', u'xhosa'),
+ ('yao', '', '', u'Yao', u'yao'),
+ ('yap', '', '', u'Yapese', u'yapois'),
+ ('yid', '', 'yi', u'Yiddish', u'yiddish'),
+ ('yor', '', 'yo', u'Yoruba', u'yoruba'),
+ ('ypk', '', '', u'Yupik languages', u'yupik, langues'),
+ ('zap', '', '', u'Zapotec', u'zapotèque'),
+ ('zbl', '', '', u'Blissymbols; Blissymbolics; Bliss', u'symboles Bliss; Bliss'),
+ ('zen', '', '', u'Zenaga', u'zenaga'),
+ ('zha', '', 'za', u'Zhuang; Chuang', u'zhuang; chuang'),
+ ('znd', '', '', u'Zande languages', u'zandé, langues'),
+ ('zul', '', 'zu', u'Zulu', u'zoulou'),
+ ('zun', '', '', u'Zuni', u'zuni'),
+ ('zza', '', '', u'Zaza; Dimili; Dimli; Kirdki; Kirmanjki; Zazaki', u'zaza; dimili; dimli; kirdki; kirmanjki; zazaki')]
+
+
+class Country(object):
+ """Country according to ISO-3166
+
+ :param string country: country name, alpha2 code, alpha3 code or numeric code
+ :param list countries: all countries
+ :type countries: see :data:`~subliminal.language.COUNTRIES`
+
+ """
+ def __init__(self, country, countries=None):
+ countries = countries or COUNTRIES
+ country = to_unicode(country.strip().lower())
+ country_tuple = None
+
+ # Try to find the country
+ if len(country) == 2:
+ country_tuple = dict((c[0].lower(), c) for c in countries).get(country)
+ elif len(country) == 3 and not country.isdigit():
+ country_tuple = dict((c[1].lower(), c) for c in countries).get(country)
+ elif len(country) == 3 and country.isdigit():
+ country_tuple = dict((c[2].lower(), c) for c in countries).get(country)
+ if country_tuple is None:
+ country_tuple = dict((c[3].lower(), c) for c in countries).get(country)
+
+ # Raise ValueError if nothing is found
+ if country_tuple is None:
+ raise ValueError('Country %s does not exist' % country)
+
+ # Set default attrs
+ self.alpha2 = country_tuple[0]
+ self.alpha3 = country_tuple[1]
+ self.numeric = country_tuple[2]
+ self.name = country_tuple[3]
+
+ def __hash__(self):
+ return hash(self.alpha3)
+
+ def __eq__(self, other):
+ if isinstance(other, Country):
+ return self.alpha3 == other.alpha3
+ return False
+
+ def __ne__(self, other):
+ return not self == other
+
+ def __unicode__(self):
+ return self.name
+
+ def __str__(self):
+ return unicode(self).encode('utf-8')
+
+ def __repr__(self):
+ return 'Country(%s)' % self
+
+
+class Language(object):
+ """Language according to ISO-639
+
+ :param string language: language name (english or french), alpha2 code, alpha3 code, terminologic code or numeric code, eventually with a country
+ :param country: country of the language
+ :type country: :class:`Country` or string
+ :param languages: all languages
+ :type languages: see :data:`~subliminal.language.LANGUAGES`
+ :param countries: all countries
+ :type countries: see :data:`~subliminal.language.COUNTRIES`
+ :param bool strict: whether to raise a ValueError on unknown language or not
+
+ :class:`Language` implements the inclusion test, with the ``in`` keyword::
+
+ >>> Language('pt-BR') in Language('pt') # Portuguese (Brazil) is included in Portuguese
+ True
+ >>> Language('pt') in Language('pt-BR') # Portuguese is not included in Portuguese (Brazil)
+ False
+
+ """
+ with_country_regexps = [re.compile('(.*)\((.*)\)'), re.compile('(.*)[-_](.*)')]
+
+ def __init__(self, language, country=None, languages=None, countries=None, strict=True):
+ languages = languages or LANGUAGES
+ countries = countries or COUNTRIES
+
+ # Get the country
+ self.country = None
+ if isinstance(country, Country):
+ self.country = country
+ elif isinstance(country, basestring):
+ try:
+ self.country = Country(country, countries)
+ except ValueError:
+ logger.warning(u'Country %s could not be identified' % country)
+ if strict:
+ raise
+
+ # Language + Country format
+ #TODO: Improve this part
+ if country is None:
+ for regexp in [r.match(language) for r in self.with_country_regexps]:
+ if regexp:
+ language = regexp.group(1)
+ try:
+ self.country = Country(regexp.group(2), countries)
+ except ValueError:
+ logger.warning(u'Country %s could not be identified' % country)
+ if strict:
+ raise
+ break
+
+ # Try to find the language
+ language = to_unicode(language.strip().lower())
+ language_tuple = None
+ if len(language) == 2:
+ language_tuple = dict((l[2].lower(), l) for l in languages).get(language)
+ elif len(language) == 3:
+ language_tuple = dict((l[0].lower(), l) for l in languages).get(language)
+ if language_tuple is None:
+ language_tuple = dict((l[1].lower(), l) for l in languages).get(language)
+ if language_tuple is None:
+ language_tuple = dict((l[3].split('; ')[0].lower(), l) for l in languages).get(language)
+ if language_tuple is None:
+ language_tuple = dict((l[4].split('; ')[0].lower(), l) for l in languages).get(language)
+
+ # Raise ValueError if strict or continue with Undetermined
+ if language_tuple is None:
+ if strict:
+ raise ValueError('Language %s does not exist' % language)
+ language_tuple = dict((l[0].lower(), l) for l in languages).get('und')
+
+ # Set attributes
+ self.alpha2 = language_tuple[2]
+ self.alpha3 = language_tuple[0]
+ self.terminologic = language_tuple[1]
+ self.name = language_tuple[3]
+ self.french_name = language_tuple[4]
+
+ def __hash__(self):
+ if self.country is None:
+ return hash(self.alpha3)
+ return hash(self.alpha3 + self.country.alpha3)
+
+ def __eq__(self, other):
+ if isinstance(other, Language):
+ return self.alpha3 == other.alpha3 and self.country == other.country
+ return False
+
+ def __contains__(self, item):
+ if isinstance(item, Language):
+ if self == item:
+ return True
+ if self.country is None:
+ return self.alpha3 == item.alpha3
+ return False
+
+ def __ne__(self, other):
+ return not self == other
+
+ def __nonzero__(self):
+ return self.alpha3 != 'und'
+
+ def __unicode__(self):
+ if self.country is None:
+ return self.name
+ return '%s (%s)' % (self.name, self.country)
+
+ def __str__(self):
+ return unicode(self).encode('utf-8')
+
+ def __repr__(self):
+ if self.country is None:
+ return 'Language(%s)' % self.name.encode('utf-8')
+ return 'Language(%s, country=%s)' % (self.name.encode('utf-8'), self.country)
+
+
+class language_set(set):
+ """Set of :class:`Language` with some specificities.
+
+ :param iterable: where to take elements from
+ :type iterable: iterable of :class:`Languages ` or string
+ :param languages: all languages
+ :type languages: see :data:`~subliminal.language.LANGUAGES`
+ :param bool strict: whether to raise a ValueError on invalid language or not
+
+ The following redefinitions are meant to reflect the inclusion logic in :class:`Language`
+
+ * Inclusion test, with the ``in`` keyword
+ * Intersection
+ * Substraction
+
+ Here is an illustration of the previous points::
+
+ >>> Language('en') in language_set(['en-US', 'en-CA'])
+ False
+ >>> Language('en-US') in language_set(['en', 'fr'])
+ True
+ >>> language_set(['en']) & language_set(['en-US', 'en-CA'])
+ language_set([Language(English, country=Canada), Language(English, country=United States)])
+ >>> language_set(['en-US', 'en-CA', 'fr']) - language_set(['en'])
+ language_set([Language(French)])
+
+ """
+ def __init__(self, iterable=None, languages=None, strict=True):
+ iterable = iterable or []
+ languages = languages or LANGUAGES
+ items = []
+ for i in iterable:
+ if isinstance(i, Language):
+ items.append(i)
+ continue
+ if isinstance(i, tuple):
+ items.append(Language(i[0], languages=languages, strict=strict))
+ continue
+ items.append(Language(i, languages=languages, strict=strict))
+ super(language_set, self).__init__(items)
+
+ def __contains__(self, item):
+ for i in self:
+ if item in i:
+ return True
+ return super(language_set, self).__contains__(item)
+
+ def __and__(self, other):
+ results = language_set()
+ for i in self:
+ for j in other:
+ if i in j:
+ results.add(i)
+ for i in other:
+ for j in self:
+ if i in j:
+ results.add(i)
+ return results
+
+ def __sub__(self, other):
+ results = language_set()
+ for i in self:
+ if i not in other:
+ results.add(i)
+ return results
+
+
+class language_list(list):
+ """List of :class:`Language` with some specificities.
+
+ :param iterable: where to take elements from
+ :type iterable: iterable of :class:`Languages ` or string
+ :param languages: all languages
+ :type languages: see :data:`~subliminal.language.LANGUAGES`
+ :param bool strict: whether to raise a ValueError on invalid language or not
+
+ The following redefinitions are meant to reflect the inclusion logic in :class:`Language`
+
+ * Inclusion test, with the ``in`` keyword
+ * Index
+
+ Here is an illustration of the previous points::
+
+ >>> Language('en') in language_list(['en-US', 'en-CA'])
+ False
+ >>> Language('en-US') in language_list(['en', 'fr-BE'])
+ True
+ >>> language_list(['en', 'fr-BE']).index(Language('en-US'))
+ 0
+
+ """
+ def __init__(self, iterable=None, languages=None, strict=True):
+ iterable = iterable or []
+ languages = languages or LANGUAGES
+ items = []
+ for i in iterable:
+ if isinstance(i, Language):
+ items.append(i)
+ continue
+ if isinstance(i, tuple):
+ items.append(Language(i[0], languages=languages, strict=strict))
+ continue
+ items.append(Language(i, languages=languages, strict=strict))
+ super(language_list, self).__init__(items)
+
+ def __contains__(self, item):
+ for i in self:
+ if item in i:
+ return True
+ return super(language_list, self).__contains__(item)
+
+ def index(self, x, strict=False):
+ if not strict:
+ for i in range(len(self)):
+ if x in self[i]:
+ return i
+ return super(language_list, self).index(x)
diff --git a/libs/subliminal/languages.py b/libs/subliminal/languages.py
deleted file mode 100755
index f743953f..00000000
--- a/libs/subliminal/languages.py
+++ /dev/null
@@ -1,547 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright 2011-2012 Antoine Bertin
-#
-# This file is part of subliminal.
-#
-# subliminal is free software; you can redistribute it and/or modify it under
-# the terms of the GNU Lesser General Public License as published by
-# the Free Software Foundation; either version 3 of the License, or
-# (at your option) any later version.
-#
-# subliminal is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with subliminal. If not, see .
-__all__ = ['convert_language', 'list_languages', 'LANGUAGES']
-
-
-def convert_language(language, to_iso, from_iso=None):
- """Convert a language into another format
-
- :param string language: language
- :param int to_iso: convert language to ISO-639-x
- :param int from_iso: convert language from ISO-639-x
- :return: converted language
- :rtype: string
-
- """
- if from_iso == None: # if no from_iso is given, try to guess it
- if language.startswith(language[:1].upper()):
- from_iso = 0
- elif len(language) == 2:
- from_iso = 1
- elif len(language) == 3:
- from_iso = 2
- else:
- raise ValueError('Invalid input language format')
- if isinstance(language, unicode):
- language = language.encode('utf-8')
- converted_language = None
- for language_tuple in LANGUAGES:
- if language_tuple[from_iso] == language and language_tuple[to_iso]:
- converted_language = language_tuple[to_iso]
- break
- return converted_language
-
-
-def list_languages(iso):
- """List languages in the given ISO-639-x format
-
- :param int iso: ISO-639-x format to list
- :return: languages in the requested format
- :rtype: list
-
- """
- return [l[iso] for l in LANGUAGES if l[iso]]
-
-#: ISO-639-2 languages list from http://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt
-#: + ('Brazilian', 'po', 'pob')
-LANGUAGES = [('Afar', 'aa', 'aar'),
- ('Abkhazian', 'ab', 'abk'),
- ('Achinese', '', 'ace'),
- ('Acoli', '', 'ach'),
- ('Adangme', '', 'ada'),
- ('Adyghe; Adygei', '', 'ady'),
- ('Afro-Asiatic languages', '', 'afa'),
- ('Afrihili', '', 'afh'),
- ('Afrikaans', 'af', 'afr'),
- ('Ainu', '', 'ain'),
- ('Akan', 'ak', 'aka'),
- ('Akkadian', '', 'akk'),
- ('Albanian', 'sq', 'alb'),
- ('Aleut', '', 'ale'),
- ('Algonquian languages', '', 'alg'),
- ('Southern Altai', '', 'alt'),
- ('Amharic', 'am', 'amh'),
- ('English, Old (ca.450-1100)', '', 'ang'),
- ('Angika', '', 'anp'),
- ('Apache languages', '', 'apa'),
- ('Arabic', 'ar', 'ara'),
- ('Official Aramaic (700-300 BCE); Imperial Aramaic (700-300 BCE)', '', 'arc'),
- ('Aragonese', 'an', 'arg'),
- ('Armenian', 'hy', 'arm'),
- ('Mapudungun; Mapuche', '', 'arn'),
- ('Arapaho', '', 'arp'),
- ('Artificial languages', '', 'art'),
- ('Arawak', '', 'arw'),
- ('Assamese', 'as', 'asm'),
- ('Asturian; Bable; Leonese; Asturleonese', '', 'ast'),
- ('Athapascan languages', '', 'ath'),
- ('Australian languages', '', 'aus'),
- ('Avaric', 'av', 'ava'),
- ('Avestan', 'ae', 'ave'),
- ('Awadhi', '', 'awa'),
- ('Aymara', 'ay', 'aym'),
- ('Azerbaijani', 'az', 'aze'),
- ('Banda languages', '', 'bad'),
- ('Bamileke languages', '', 'bai'),
- ('Bashkir', 'ba', 'bak'),
- ('Baluchi', '', 'bal'),
- ('Bambara', 'bm', 'bam'),
- ('Balinese', '', 'ban'),
- ('Basque', 'eu', 'baq'),
- ('Basa', '', 'bas'),
- ('Baltic languages', '', 'bat'),
- ('Beja; Bedawiyet', '', 'bej'),
- ('Belarusian', 'be', 'bel'),
- ('Bemba', '', 'bem'),
- ('Bengali', 'bn', 'ben'),
- ('Berber languages', '', 'ber'),
- ('Bhojpuri', '', 'bho'),
- ('Bihari languages', 'bh', 'bih'),
- ('Bikol', '', 'bik'),
- ('Bini; Edo', '', 'bin'),
- ('Bislama', 'bi', 'bis'),
- ('Siksika', '', 'bla'),
- ('Bantu (Other)', '', 'bnt'),
- ('Bosnian', 'bs', 'bos'),
- ('Braj', '', 'bra'),
- ('Breton', 'br', 'bre'),
- ('Batak languages', '', 'btk'),
- ('Buriat', '', 'bua'),
- ('Buginese', '', 'bug'),
- ('Bulgarian', 'bg', 'bul'),
- ('Burmese', 'my', 'bur'),
- ('Blin; Bilin', '', 'byn'),
- ('Caddo', '', 'cad'),
- ('Central American Indian languages', '', 'cai'),
- ('Galibi Carib', '', 'car'),
- ('Catalan; Valencian', 'ca', 'cat'),
- ('Caucasian languages', '', 'cau'),
- ('Cebuano', '', 'ceb'),
- ('Celtic languages', '', 'cel'),
- ('Chamorro', 'ch', 'cha'),
- ('Chibcha', '', 'chb'),
- ('Chechen', 'ce', 'che'),
- ('Chagatai', '', 'chg'),
- ('Chinese', 'zh', 'chi'),
- ('Chuukese', '', 'chk'),
- ('Mari', '', 'chm'),
- ('Chinook jargon', '', 'chn'),
- ('Choctaw', '', 'cho'),
- ('Chipewyan; Dene Suline', '', 'chp'),
- ('Cherokee', '', 'chr'),
- ('Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic', 'cu', 'chu'),
- ('Chuvash', 'cv', 'chv'),
- ('Cheyenne', '', 'chy'),
- ('Chamic languages', '', 'cmc'),
- ('Coptic', '', 'cop'),
- ('Cornish', 'kw', 'cor'),
- ('Corsican', 'co', 'cos'),
- ('Creoles and pidgins, English based', '', 'cpe'),
- ('Creoles and pidgins, French-based ', '', 'cpf'),
- ('Creoles and pidgins, Portuguese-based ', '', 'cpp'),
- ('Cree', 'cr', 'cre'),
- ('Crimean Tatar; Crimean Turkish', '', 'crh'),
- ('Creoles and pidgins ', '', 'crp'),
- ('Kashubian', '', 'csb'),
- ('Cushitic languages', '', 'cus'),
- ('Czech', 'cs', 'cze'),
- ('Dakota', '', 'dak'),
- ('Danish', 'da', 'dan'),
- ('Dargwa', '', 'dar'),
- ('Land Dayak languages', '', 'day'),
- ('Delaware', '', 'del'),
- ('Slave (Athapascan)', '', 'den'),
- ('Dogrib', '', 'dgr'),
- ('Dinka', '', 'din'),
- ('Divehi; Dhivehi; Maldivian', 'dv', 'div'),
- ('Dogri', '', 'doi'),
- ('Dravidian languages', '', 'dra'),
- ('Lower Sorbian', '', 'dsb'),
- ('Duala', '', 'dua'),
- ('Dutch, Middle (ca.1050-1350)', '', 'dum'),
- ('Dutch; Flemish', 'nl', 'dut'),
- ('Dyula', '', 'dyu'),
- ('Dzongkha', 'dz', 'dzo'),
- ('Efik', '', 'efi'),
- ('Egyptian (Ancient)', '', 'egy'),
- ('Ekajuk', '', 'eka'),
- ('Elamite', '', 'elx'),
- ('English', 'en', 'eng'),
- ('English, Middle (1100-1500)', '', 'enm'),
- ('Esperanto', 'eo', 'epo'),
- ('Estonian', 'et', 'est'),
- ('Ewe', 'ee', 'ewe'),
- ('Ewondo', '', 'ewo'),
- ('Fang', '', 'fan'),
- ('Faroese', 'fo', 'fao'),
- ('Fanti', '', 'fat'),
- ('Fijian', 'fj', 'fij'),
- ('Filipino; Pilipino', '', 'fil'),
- ('Finnish', 'fi', 'fin'),
- ('Finno-Ugrian languages', '', 'fiu'),
- ('Fon', '', 'fon'),
- ('French', 'fr', 'fre'),
- ('French, Middle (ca.1400-1600)', '', 'frm'),
- ('French, Old (842-ca.1400)', '', 'fro'),
- ('Northern Frisian', '', 'frr'),
- ('Eastern Frisian', '', 'frs'),
- ('Western Frisian', 'fy', 'fry'),
- ('Fulah', 'ff', 'ful'),
- ('Friulian', '', 'fur'),
- ('Ga', '', 'gaa'),
- ('Gayo', '', 'gay'),
- ('Gbaya', '', 'gba'),
- ('Germanic languages', '', 'gem'),
- ('Georgian', 'ka', 'geo'),
- ('German', 'de', 'ger'),
- ('Geez', '', 'gez'),
- ('Gilbertese', '', 'gil'),
- ('Gaelic; Scottish Gaelic', 'gd', 'gla'),
- ('Irish', 'ga', 'gle'),
- ('Galician', 'gl', 'glg'),
- ('Manx', 'gv', 'glv'),
- ('German, Middle High (ca.1050-1500)', '', 'gmh'),
- ('German, Old High (ca.750-1050)', '', 'goh'),
- ('Gondi', '', 'gon'),
- ('Gorontalo', '', 'gor'),
- ('Gothic', '', 'got'),
- ('Grebo', '', 'grb'),
- ('Greek, Ancient (to 1453)', '', 'grc'),
- ('Greek, Modern (1453-)', 'el', 'gre'),
- ('Guarani', 'gn', 'grn'),
- ('Swiss German; Alemannic; Alsatian', '', 'gsw'),
- ('Gujarati', 'gu', 'guj'),
- ('Gwich\'in', '', 'gwi'),
- ('Haida', '', 'hai'),
- ('Haitian; Haitian Creole', 'ht', 'hat'),
- ('Hausa', 'ha', 'hau'),
- ('Hawaiian', '', 'haw'),
- ('Hebrew', 'he', 'heb'),
- ('Herero', 'hz', 'her'),
- ('Hiligaynon', '', 'hil'),
- ('Himachali languages; Western Pahari languages', '', 'him'),
- ('Hindi', 'hi', 'hin'),
- ('Hittite', '', 'hit'),
- ('Hmong; Mong', '', 'hmn'),
- ('Hiri Motu', 'ho', 'hmo'),
- ('Croatian', 'hr', 'hrv'),
- ('Upper Sorbian', '', 'hsb'),
- ('Hungarian', 'hu', 'hun'),
- ('Hupa', '', 'hup'),
- ('Iban', '', 'iba'),
- ('Igbo', 'ig', 'ibo'),
- ('Icelandic', 'is', 'ice'),
- ('Ido', 'io', 'ido'),
- ('Sichuan Yi; Nuosu', 'ii', 'iii'),
- ('Ijo languages', '', 'ijo'),
- ('Inuktitut', 'iu', 'iku'),
- ('Interlingue; Occidental', 'ie', 'ile'),
- ('Iloko', '', 'ilo'),
- ('Interlingua (International Auxiliary Language Association)', 'ia', 'ina'),
- ('Indic languages', '', 'inc'),
- ('Indonesian', 'id', 'ind'),
- ('Indo-European languages', '', 'ine'),
- ('Ingush', '', 'inh'),
- ('Inupiaq', 'ik', 'ipk'),
- ('Iranian languages', '', 'ira'),
- ('Iroquoian languages', '', 'iro'),
- ('Italian', 'it', 'ita'),
- ('Javanese', 'jv', 'jav'),
- ('Lojban', '', 'jbo'),
- ('Japanese', 'ja', 'jpn'),
- ('Judeo-Persian', '', 'jpr'),
- ('Judeo-Arabic', '', 'jrb'),
- ('Kara-Kalpak', '', 'kaa'),
- ('Kabyle', '', 'kab'),
- ('Kachin; Jingpho', '', 'kac'),
- ('Kalaallisut; Greenlandic', 'kl', 'kal'),
- ('Kamba', '', 'kam'),
- ('Kannada', 'kn', 'kan'),
- ('Karen languages', '', 'kar'),
- ('Kashmiri', 'ks', 'kas'),
- ('Kanuri', 'kr', 'kau'),
- ('Kawi', '', 'kaw'),
- ('Kazakh', 'kk', 'kaz'),
- ('Kabardian', '', 'kbd'),
- ('Khasi', '', 'kha'),
- ('Khoisan languages', '', 'khi'),
- ('Central Khmer', 'km', 'khm'),
- ('Khotanese; Sakan', '', 'kho'),
- ('Kikuyu; Gikuyu', 'ki', 'kik'),
- ('Kinyarwanda', 'rw', 'kin'),
- ('Kirghiz; Kyrgyz', 'ky', 'kir'),
- ('Kimbundu', '', 'kmb'),
- ('Konkani', '', 'kok'),
- ('Komi', 'kv', 'kom'),
- ('Kongo', 'kg', 'kon'),
- ('Korean', 'ko', 'kor'),
- ('Kosraean', '', 'kos'),
- ('Kpelle', '', 'kpe'),
- ('Karachay-Balkar', '', 'krc'),
- ('Karelian', '', 'krl'),
- ('Kru languages', '', 'kro'),
- ('Kurukh', '', 'kru'),
- ('Kuanyama; Kwanyama', 'kj', 'kua'),
- ('Kumyk', '', 'kum'),
- ('Kurdish', 'ku', 'kur'),
- ('Kutenai', '', 'kut'),
- ('Ladino', '', 'lad'),
- ('Lahnda', '', 'lah'),
- ('Lamba', '', 'lam'),
- ('Lao', 'lo', 'lao'),
- ('Latin', 'la', 'lat'),
- ('Latvian', 'lv', 'lav'),
- ('Lezghian', '', 'lez'),
- ('Limburgan; Limburger; Limburgish', 'li', 'lim'),
- ('Lingala', 'ln', 'lin'),
- ('Lithuanian', 'lt', 'lit'),
- ('Mongo', '', 'lol'),
- ('Lozi', '', 'loz'),
- ('Luxembourgish; Letzeburgesch', 'lb', 'ltz'),
- ('Luba-Lulua', '', 'lua'),
- ('Luba-Katanga', 'lu', 'lub'),
- ('Ganda', 'lg', 'lug'),
- ('Luiseno', '', 'lui'),
- ('Lunda', '', 'lun'),
- ('Luo (Kenya and Tanzania)', '', 'luo'),
- ('Lushai', '', 'lus'),
- ('Macedonian', 'mk', 'mac'),
- ('Madurese', '', 'mad'),
- ('Magahi', '', 'mag'),
- ('Marshallese', 'mh', 'mah'),
- ('Maithili', '', 'mai'),
- ('Makasar', '', 'mak'),
- ('Malayalam', 'ml', 'mal'),
- ('Mandingo', '', 'man'),
- ('Maori', 'mi', 'mao'),
- ('Austronesian languages', '', 'map'),
- ('Marathi', 'mr', 'mar'),
- ('Masai', '', 'mas'),
- ('Malay', 'ms', 'may'),
- ('Moksha', '', 'mdf'),
- ('Mandar', '', 'mdr'),
- ('Mende', '', 'men'),
- ('Irish, Middle (900-1200)', '', 'mga'),
- ('Mi\'kmaq; Micmac', '', 'mic'),
- ('Minangkabau', '', 'min'),
- ('Uncoded languages', '', 'mis'),
- ('Mon-Khmer languages', '', 'mkh'),
- ('Malagasy', 'mg', 'mlg'),
- ('Maltese', 'mt', 'mlt'),
- ('Manchu', '', 'mnc'),
- ('Manipuri', '', 'mni'),
- ('Manobo languages', '', 'mno'),
- ('Mohawk', '', 'moh'),
- ('Mongolian', 'mn', 'mon'),
- ('Mossi', '', 'mos'),
- ('Multiple languages', '', 'mul'),
- ('Munda languages', '', 'mun'),
- ('Creek', '', 'mus'),
- ('Mirandese', '', 'mwl'),
- ('Marwari', '', 'mwr'),
- ('Mayan languages', '', 'myn'),
- ('Erzya', '', 'myv'),
- ('Nahuatl languages', '', 'nah'),
- ('North American Indian languages', '', 'nai'),
- ('Neapolitan', '', 'nap'),
- ('Nauru', 'na', 'nau'),
- ('Navajo; Navaho', 'nv', 'nav'),
- ('Ndebele, South; South Ndebele', 'nr', 'nbl'),
- ('Ndebele, North; North Ndebele', 'nd', 'nde'),
- ('Ndonga', 'ng', 'ndo'),
- ('Low German; Low Saxon; German, Low; Saxon, Low', '', 'nds'),
- ('Nepali', 'ne', 'nep'),
- ('Nepal Bhasa; Newari', '', 'new'),
- ('Nias', '', 'nia'),
- ('Niger-Kordofanian languages', '', 'nic'),
- ('Niuean', '', 'niu'),
- ('Norwegian Nynorsk; Nynorsk, Norwegian', 'nn', 'nno'),
- ('Bokmål, Norwegian; Norwegian Bokmål', 'nb', 'nob'),
- ('Nogai', '', 'nog'),
- ('Norse, Old', '', 'non'),
- ('Norwegian', 'no', 'nor'),
- ('N\'Ko', '', 'nqo'),
- ('Pedi; Sepedi; Northern Sotho', '', 'nso'),
- ('Nubian languages', '', 'nub'),
- ('Classical Newari; Old Newari; Classical Nepal Bhasa', '', 'nwc'),
- ('Chichewa; Chewa; Nyanja', 'ny', 'nya'),
- ('Nyamwezi', '', 'nym'),
- ('Nyankole', '', 'nyn'),
- ('Nyoro', '', 'nyo'),
- ('Nzima', '', 'nzi'),
- ('Occitan (post 1500); Provençal', 'oc', 'oci'),
- ('Ojibwa', 'oj', 'oji'),
- ('Oriya', 'or', 'ori'),
- ('Oromo', 'om', 'orm'),
- ('Osage', '', 'osa'),
- ('Ossetian; Ossetic', 'os', 'oss'),
- ('Turkish, Ottoman (1500-1928)', '', 'ota'),
- ('Otomian languages', '', 'oto'),
- ('Papuan languages', '', 'paa'),
- ('Pangasinan', '', 'pag'),
- ('Pahlavi', '', 'pal'),
- ('Pampanga; Kapampangan', '', 'pam'),
- ('Panjabi; Punjabi', 'pa', 'pan'),
- ('Papiamento', '', 'pap'),
- ('Palauan', '', 'pau'),
- ('Persian, Old (ca.600-400 B.C.)', '', 'peo'),
- ('Persian', 'fa', 'per'),
- ('Philippine languages', '', 'phi'),
- ('Phoenician', '', 'phn'),
- ('Pali', 'pi', 'pli'),
- ('Polish', 'pl', 'pol'),
- ('Pohnpeian', '', 'pon'),
- ('Portuguese', 'pt', 'por'),
- ('Prakrit languages', '', 'pra'),
- ('Provençal, Old (to 1500)', '', 'pro'),
- ('Pushto; Pashto', 'ps', 'pus'),
- ('Reserved for local use', '', 'qaa-qtz'),
- ('Quechua', 'qu', 'que'),
- ('Rajasthani', '', 'raj'),
- ('Rapanui', '', 'rap'),
- ('Rarotongan; Cook Islands Maori', '', 'rar'),
- ('Romance languages', '', 'roa'),
- ('Romansh', 'rm', 'roh'),
- ('Romany', '', 'rom'),
- ('Romanian; Moldavian; Moldovan', 'ro', 'rum'),
- ('Rundi', 'rn', 'run'),
- ('Aromanian; Arumanian; Macedo-Romanian', '', 'rup'),
- ('Russian', 'ru', 'rus'),
- ('Sandawe', '', 'sad'),
- ('Sango', 'sg', 'sag'),
- ('Yakut', '', 'sah'),
- ('South American Indian (Other)', '', 'sai'),
- ('Salishan languages', '', 'sal'),
- ('Samaritan Aramaic', '', 'sam'),
- ('Sanskrit', 'sa', 'san'),
- ('Sasak', '', 'sas'),
- ('Santali', '', 'sat'),
- ('Sicilian', '', 'scn'),
- ('Scots', '', 'sco'),
- ('Selkup', '', 'sel'),
- ('Semitic languages', '', 'sem'),
- ('Irish, Old (to 900)', '', 'sga'),
- ('Sign Languages', '', 'sgn'),
- ('Shan', '', 'shn'),
- ('Sidamo', '', 'sid'),
- ('Sinhala; Sinhalese', 'si', 'sin'),
- ('Siouan languages', '', 'sio'),
- ('Sino-Tibetan languages', '', 'sit'),
- ('Slavic languages', '', 'sla'),
- ('Slovak', 'sk', 'slo'),
- ('Slovenian', 'sl', 'slv'),
- ('Southern Sami', '', 'sma'),
- ('Northern Sami', 'se', 'sme'),
- ('Sami languages', '', 'smi'),
- ('Lule Sami', '', 'smj'),
- ('Inari Sami', '', 'smn'),
- ('Samoan', 'sm', 'smo'),
- ('Skolt Sami', '', 'sms'),
- ('Shona', 'sn', 'sna'),
- ('Sindhi', 'sd', 'snd'),
- ('Soninke', '', 'snk'),
- ('Sogdian', '', 'sog'),
- ('Somali', 'so', 'som'),
- ('Songhai languages', '', 'son'),
- ('Sotho, Southern', 'st', 'sot'),
- ('Spanish; Castilian', 'es', 'spa'),
- ('Sardinian', 'sc', 'srd'),
- ('Sranan Tongo', '', 'srn'),
- ('Serbian', 'sr', 'srp'),
- ('Serer', '', 'srr'),
- ('Nilo-Saharan languages', '', 'ssa'),
- ('Swati', 'ss', 'ssw'),
- ('Sukuma', '', 'suk'),
- ('Sundanese', 'su', 'sun'),
- ('Susu', '', 'sus'),
- ('Sumerian', '', 'sux'),
- ('Swahili', 'sw', 'swa'),
- ('Swedish', 'sv', 'swe'),
- ('Classical Syriac', '', 'syc'),
- ('Syriac', '', 'syr'),
- ('Tahitian', 'ty', 'tah'),
- ('Tai languages', '', 'tai'),
- ('Tamil', 'ta', 'tam'),
- ('Tatar', 'tt', 'tat'),
- ('Telugu', 'te', 'tel'),
- ('Timne', '', 'tem'),
- ('Tereno', '', 'ter'),
- ('Tetum', '', 'tet'),
- ('Tajik', 'tg', 'tgk'),
- ('Tagalog', 'tl', 'tgl'),
- ('Thai', 'th', 'tha'),
- ('Tibetan', 'bo', 'tib'),
- ('Tigre', '', 'tig'),
- ('Tigrinya', 'ti', 'tir'),
- ('Tiv', '', 'tiv'),
- ('Tokelau', '', 'tkl'),
- ('Klingon; tlhIngan-Hol', '', 'tlh'),
- ('Tlingit', '', 'tli'),
- ('Tamashek', '', 'tmh'),
- ('Tonga (Nyasa)', '', 'tog'),
- ('Tonga (Tonga Islands)', 'to', 'ton'),
- ('Tok Pisin', '', 'tpi'),
- ('Tsimshian', '', 'tsi'),
- ('Tswana', 'tn', 'tsn'),
- ('Tsonga', 'ts', 'tso'),
- ('Turkmen', 'tk', 'tuk'),
- ('Tumbuka', '', 'tum'),
- ('Tupi languages', '', 'tup'),
- ('Turkish', 'tr', 'tur'),
- ('Altaic languages', '', 'tut'),
- ('Tuvalu', '', 'tvl'),
- ('Twi', 'tw', 'twi'),
- ('Tuvinian', '', 'tyv'),
- ('Udmurt', '', 'udm'),
- ('Ugaritic', '', 'uga'),
- ('Uighur; Uyghur', 'ug', 'uig'),
- ('Ukrainian', 'uk', 'ukr'),
- ('Umbundu', '', 'umb'),
- ('Undetermined', '', 'und'),
- ('Urdu', 'ur', 'urd'),
- ('Uzbek', 'uz', 'uzb'),
- ('Vai', '', 'vai'),
- ('Venda', 've', 'ven'),
- ('Vietnamese', 'vi', 'vie'),
- ('Volapük', 'vo', 'vol'),
- ('Votic', '', 'vot'),
- ('Wakashan languages', '', 'wak'),
- ('Walamo', '', 'wal'),
- ('Waray', '', 'war'),
- ('Washo', '', 'was'),
- ('Welsh', 'cy', 'wel'),
- ('Sorbian languages', '', 'wen'),
- ('Walloon', 'wa', 'wln'),
- ('Wolof', 'wo', 'wol'),
- ('Kalmyk; Oirat', '', 'xal'),
- ('Xhosa', 'xh', 'xho'),
- ('Yao', '', 'yao'),
- ('Yapese', '', 'yap'),
- ('Yiddish', 'yi', 'yid'),
- ('Yoruba', 'yo', 'yor'),
- ('Yupik languages', '', 'ypk'),
- ('Zapotec', '', 'zap'),
- ('Blissymbols; Blissymbolics; Bliss', '', 'zbl'),
- ('Zenaga', '', 'zen'),
- ('Zhuang; Chuang', 'za', 'zha'),
- ('Zande languages', '', 'znd'),
- ('Zulu', 'zu', 'zul'),
- ('Zuni', '', 'zun'),
- ('No linguistic content; Not applicable', '', 'zxx'),
- ('Zaza; Dimili; Dimli; Kirdki; Kirmanjki; Zazaki', '', 'zza'),
- ('Brazilian', 'po', 'pob')]
diff --git a/libs/subliminal/services/__init__.py b/libs/subliminal/services/__init__.py
old mode 100755
new mode 100644
index 67e457fe..929c2163
--- a/libs/subliminal/services/__init__.py
+++ b/libs/subliminal/services/__init__.py
@@ -15,11 +15,15 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see .
-from ..exceptions import MissingLanguageError, DownloadFailedError
+from ..cache import Cache
+from ..exceptions import DownloadFailedError, ServiceError
+from ..language import language_set, Language
+from ..subtitles import EXTENSIONS
import logging
import os
import requests
import threading
+import zipfile
__all__ = ['ServiceBase', 'ServiceConfig']
@@ -37,7 +41,7 @@ class ServiceBase(object):
server_url = ''
#: User Agent for any HTTP-based requests
- user_agent = 'subliminal v0.5'
+ user_agent = 'subliminal v0.6'
#: Whether based on an API or not
api_based = False
@@ -45,14 +49,14 @@ class ServiceBase(object):
#: Timeout for web requests
timeout = 5
- #: Lock for cache interactions
- lock = threading.Lock()
+ #: :class:`~subliminal.language.language_set` of available languages
+ languages = language_set()
- #: Mapping to Service's language codes and subliminal's
- languages = {}
+ #: Map between language objects and language codes used in the service
+ language_map = {}
- #: Whether the mapping is reverted or not
- reverted_languages = False
+ #: Default attribute of a :class:`~subliminal.language.Language` to get with :meth:`get_code`
+ language_code = 'alpha2'
#: Accepted video classes (:class:`~subliminal.videos.Episode`, :class:`~subliminal.videos.Movie`, :class:`~subliminal.videos.UnknownVideo`)
videos = []
@@ -60,8 +64,12 @@ class ServiceBase(object):
#: Whether the video has to exist or not
require_video = False
+ #: List of required features for BeautifulSoup
+ required_features = None
+
def __init__(self, config=None):
self.config = config or ServiceConfig()
+ self.session = None
def __enter__(self):
self.init()
@@ -75,33 +83,84 @@ class ServiceBase(object):
logger.debug(u'Initializing %s' % self.__class__.__name__)
self.session = requests.session(timeout=10, headers={'User-Agent': self.user_agent})
+ def init_cache(self):
+ """Initialize cache, make sure it is loaded from disk"""
+ if not self.config or not self.config.cache:
+ raise ServiceError('Cache directory is required')
+ self.config.cache.load(self.__class__.__name__)
+
+ def save_cache(self):
+ self.config.cache.save(self.__class__.__name__)
+
+ def clear_cache(self):
+ self.config.cache.clear(self.__class__.__name__)
+
+ def cache_for(self, func, args, result):
+ return self.config.cache.cache_for(self.__class__.__name__, func, args, result)
+
+ def cached_value(self, func, args):
+ return self.config.cache.cached_value(self.__class__.__name__, func, args)
+
def terminate(self):
"""Terminate connection"""
logger.debug(u'Terminating %s' % self.__class__.__name__)
+ def get_code(self, language):
+ """Get the service code for a :class:`~subliminal.language.Language`
+
+ It uses the :data:`language_map` and if there's no match, falls back
+ on the :data:`language_code` attribute of the given :class:`~subliminal.language.Language`
+
+ """
+ if language in self.language_map:
+ return self.language_map[language]
+ if self.language_code is None:
+ raise ValueError('%r has no matching code' % language)
+ return getattr(language, self.language_code)
+
+ def get_language(self, code):
+ """Get a :class:`~subliminal.language.Language` from a service code
+
+ It uses the :data:`language_map` and if there's no match, uses the
+ given code as ``language`` parameter for the :class:`~subliminal.language.Language`
+ constructor
+
+ .. note::
+
+ A warning is emitted if the generated :class:`~subliminal.language.Language`
+ is "Undetermined"
+
+ """
+ if code in self.language_map:
+ return self.language_map[code]
+ language = Language(code, strict=False)
+ if language == Language('Undetermined'):
+ logger.warning(u'Code %s could not be identified as a language for %s' % (code, self.__class__.__name__))
+ return language
+
def query(self, *args):
"""Make the actual query"""
- pass
+ raise NotImplementedError()
def list(self, video, languages):
- """List subtitles"""
- pass
+ """List subtitles
+
+ As a service writer, you can either override this method or implement
+ :meth:`list_checked` instead to have the languages pre-filtered for you
+
+ """
+ if not self.check_validity(video, languages):
+ return []
+ return self.list_checked(video, languages)
+
+ def list_checked(self, video, languages):
+ """List subtitles without having to check parameters for validity"""
+ raise NotImplementedError()
def download(self, subtitle):
"""Download a subtitle"""
self.download_file(subtitle.link, subtitle.path)
-
- @classmethod
- def available_languages(cls):
- """Available languages in the Service
-
- :return: available languages
- :rtype: set
-
- """
- if not cls.reverted_languages:
- return set(cls.languages.keys())
- return set(cls.languages.values())
+ return subtitle
@classmethod
def check_validity(cls, video, languages):
@@ -109,76 +168,20 @@ class ServiceBase(object):
:param video: the video to check
:type video: :class:`~subliminal.videos.video`
- :param set languages: languages to check
+ :param languages: languages to check
+ :type languages: :class:`~subliminal.language.Language`
:rtype: bool
"""
- languages &= cls.available_languages()
+ languages = (languages & cls.languages) - language_set(['Undetermined'])
if not languages:
- logger.debug(u'No language available for service %s' % cls.__class__.__name__.lower())
+ logger.debug(u'No language available for service %s' % cls.__name__.lower())
return False
- if not cls.is_valid_video(video):
- logger.debug(u'%r is not valid for service %s' % (video, cls.__class__.__name__.lower()))
+ if cls.require_video and not video.exists or not isinstance(video, tuple(cls.videos)):
+ logger.debug(u'%r is not valid for service %s' % (video, cls.__name__.lower()))
return False
return True
- @classmethod
- def is_valid_video(cls, video):
- """Check if video is valid in the Service
-
- :param video: the video to check
- :type video: :class:`~subliminal.videos.Video`
- :rtype: bool
-
- """
- if cls.require_video and not video.exists:
- return False
- if not isinstance(video, tuple(cls.videos)):
- return False
- return True
-
- @classmethod
- def is_valid_language(cls, language):
- """Check if language is valid in the Service
-
- :param string language: the language to check
- :rtype: bool
-
- """
- if language in cls.available_languages():
- return True
- return False
-
- @classmethod
- def get_revert_language(cls, language):
- """ISO-639-1 language code from service language code
-
- :param string language: service language code
- :return: ISO-639-1 language code
- :rtype: string
-
- """
- if not cls.reverted_languages and language in cls.languages.values():
- return [k for k, v in cls.languages.iteritems() if v == language][0]
- if cls.reverted_languages and language in cls.languages.keys():
- return cls.languages[language]
- raise MissingLanguageError(language)
-
- @classmethod
- def get_language(cls, language):
- """Service language code from ISO-639-1 language code
-
- :param string language: ISO-639-1 language code
- :return: service language code
- :rtype: string
-
- """
- if not cls.reverted_languages and language in cls.languages.keys():
- return cls.languages[language]
- if cls.reverted_languages and language in cls.languages.values():
- return [k for k, v in cls.languages.iteritems() if v == language][0]
- raise MissingLanguageError(language)
-
def download_file(self, url, filepath):
"""Attempt to download a file and remove it in case of failure
@@ -186,17 +189,53 @@ class ServiceBase(object):
:param string filepath: destination path
"""
- logger.info(u'Downloading %s' % url)
+ logger.info(u'Downloading %s in %s' % (url, filepath))
try:
r = self.session.get(url, headers={'Referer': url, 'User-Agent': self.user_agent})
with open(filepath, 'wb') as f:
f.write(r.content)
except Exception as e:
- logger.error(u'Download %s failed: %s' % (url, e))
+ logger.error(u'Download failed: %s' % e)
if os.path.exists(filepath):
os.remove(filepath)
raise DownloadFailedError(str(e))
- logger.debug(u'Download finished for file %s. Size: %s' % (filepath, os.path.getsize(filepath)))
+ logger.debug(u'Download finished')
+
+ def download_zip_file(self, url, filepath):
+ """Attempt to download a zip file and extract any subtitle file from it, if any.
+ This cleans up after itself if anything fails.
+
+ :param string url: URL of the zip file to download
+ :param string filepath: destination path for the subtitle
+
+ """
+ logger.info(u'Downloading %s in %s' % (url, filepath))
+ try:
+ zippath = filepath + '.zip'
+ r = self.session.get(url, headers={'Referer': url, 'User-Agent': self.user_agent})
+ with open(zippath, 'wb') as f:
+ f.write(r.content)
+ if not zipfile.is_zipfile(zippath):
+ # TODO: could check if maybe we already have a text file and
+ # download it directly
+ raise DownloadFailedError('Downloaded file is not a zip file')
+ with zipfile.ZipFile(zippath) as zipsub:
+ for subfile in zipsub.namelist():
+ if os.path.splitext(subfile)[1] in EXTENSIONS:
+ with open(filepath, 'w') as f:
+ f.write(zipsub.open(subfile).read())
+ break
+ else:
+ raise DownloadFailedError('No subtitles found in zip file')
+ os.remove(zippath)
+ except Exception as e:
+ logger.error(u'Download %s failed: %s' % (url, e))
+ if os.path.exists(zippath):
+ os.remove(zippath)
+ if os.path.exists(filepath):
+ os.remove(filepath)
+ raise DownloadFailedError(str(e))
+ logger.debug(u'Download finished')
class ServiceConfig(object):
@@ -209,6 +248,9 @@ class ServiceConfig(object):
def __init__(self, multi=False, cache_dir=None):
self.multi = multi
self.cache_dir = cache_dir
+ self.cache = None
+ if cache_dir is not None:
+ self.cache = Cache(cache_dir)
def __repr__(self):
- return 'ServiceConfig(%r, %s)' % (self.multi, self.cache_dir)
+ return 'ServiceConfig(%r, %s)' % (self.multi, self.cache.cache_dir)
diff --git a/libs/subliminal/services/addic7ed.py b/libs/subliminal/services/addic7ed.py
new file mode 100644
index 00000000..de32dd51
--- /dev/null
+++ b/libs/subliminal/services/addic7ed.py
@@ -0,0 +1,173 @@
+# -*- coding: utf-8 -*-
+# Copyright 2012 Olivier Leveau
+#
+# This file is part of subliminal.
+#
+# subliminal is free software; you can redistribute it and/or modify it under
+# the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# subliminal is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with subliminal. If not, see .
+from . import ServiceBase
+from ..cache import cachedmethod
+from ..exceptions import DownloadFailedError
+from ..language import Language, language_set
+from ..subtitles import get_subtitle_path, ResultSubtitle
+from ..utils import get_keywords
+from ..videos import Episode
+from bs4 import BeautifulSoup
+import logging
+import os
+import re
+
+
+logger = logging.getLogger(__name__)
+
+
+def match(pattern, string):
+ try:
+ return re.search(pattern, string).group(1)
+ except AttributeError:
+ logger.debug(u'Could not match %r on %r' % (pattern, string))
+ return None
+
+
+def matches(pattern, string):
+ try:
+ return re.search(pattern, string).group(1, 2)
+ except AttributeError:
+ logger.debug(u'Could not match %r on %r' % (pattern, string))
+ return None
+
+
+class Addic7ed(ServiceBase):
+ server_url = 'http://www.addic7ed.com'
+ api_based = False
+ #TODO: Complete this
+ languages = language_set(['ar', 'ca', 'de', 'el', 'en', 'es', 'eu', 'fr', 'ga', 'he', 'hr', 'hu', 'it',
+ 'pl', 'pt', 'ro', 'ru', 'se', 'pt-br'])
+ language_map = {'Portuguese (Brazilian)': Language('por-BR'), 'Greek': Language('gre'),
+ 'Spanish (Latin America)': Language('spa'), }
+ videos = [Episode]
+ require_video = False
+ required_features = ['permissive']
+
+ @cachedmethod
+ def get_likely_series_id(self, name):
+ r = self.session.get('%s/shows.php' % self.server_url)
+ soup = BeautifulSoup(r.content, self.required_features)
+ for elem in soup.find_all('h3'):
+ show_name = elem.a.text.lower()
+ show_id = int(match('show/([0-9]+)', elem.a['href']))
+ # we could just return the id of the queried show, but as we
+ # already downloaded the whole page we might as well fill in the
+ # information for all the shows
+ self.cache_for(self.get_likely_series_id, args=(show_name,), result=show_id)
+ return self.cached_value(self.get_likely_series_id, args=(name,))
+
+ @cachedmethod
+ def get_episode_url(self, series_id, season, number):
+ """Get the Addic7ed id for the given episode. Raises KeyError if none
+ could be found
+
+ """
+ # download the page of the show, contains ids for all episodes all seasons
+ r = self.session.get('%s/show/%d' % (self.server_url, series_id))
+ soup = BeautifulSoup(r.content, self.required_features)
+ form = soup.find('form', attrs={'name': 'multidl'})
+ for table in form.find_all('table'):
+ for row in table.find_all('tr'):
+ cell = row.find('td', 'MultiDldS')
+ if not cell:
+ continue
+ m = matches('/serie/.+/([0-9]+)/([0-9]+)/', cell.a['href'])
+ if not m:
+ continue
+ episode_url = cell.a['href']
+ season_number = int(m[0])
+ episode_number = int(m[1])
+ # we could just return the url of the queried episode, but as we
+ # already downloaded the whole page we might as well fill in the
+ # information for all the episodes of the show
+ self.cache_for(self.get_episode_url, args=(series_id, season_number, episode_number), result=episode_url)
+ # raises KeyError if not found
+ return self.cached_value(self.get_episode_url, args=(series_id, season, number))
+
+ # Do not cache this method in order to always check for the most recent
+ # subtitles
+ def get_sub_urls(self, episode_url):
+ suburls = []
+ r = self.session.get('%s/%s' % (self.server_url, episode_url))
+ epsoup = BeautifulSoup(r.content, self.required_features)
+ for releaseTable in epsoup.find_all('table', 'tabel95'):
+ releaseRow = releaseTable.find('td', 'NewsTitle')
+ if not releaseRow:
+ continue
+ release = releaseRow.text.strip()
+ for row in releaseTable.find_all('tr'):
+ link = row.find('a', 'buttonDownload')
+ if not link:
+ continue
+ if 'href' not in link.attrs or not (link['href'].startswith('/original') or link['href'].startswith('/updated')):
+ continue
+ suburl = link['href']
+ lang = self.get_language(row.find('td', 'language').text.strip())
+ result = {'suburl': suburl, 'language': lang, 'release': release}
+ suburls.append(result)
+ return suburls
+
+ def list_checked(self, video, languages):
+ return self.query(video.path or video.release, languages, get_keywords(video.guess), video.series, video.season, video.episode)
+
+ def query(self, filepath, languages, keywords, series, season, episode):
+ logger.debug(u'Getting subtitles for %s season %d episode %d with languages %r' % (series, season, episode, languages))
+ self.init_cache()
+ try:
+ sid = self.get_likely_series_id(series.lower())
+ except KeyError:
+ logger.debug(u'Could not find series id for %s' % series)
+ return []
+ try:
+ ep_url = self.get_episode_url(sid, season, episode)
+ except KeyError:
+ logger.debug(u'Could not find episode id for %s season %d episode %d' % (series, season, episode))
+ return []
+ suburls = self.get_sub_urls(ep_url)
+ # filter the subtitles with our queried languages
+ subtitles = []
+ for suburl in suburls:
+ language = suburl['language']
+ if language not in languages:
+ continue
+ path = get_subtitle_path(filepath, language, self.config.multi)
+ subtitle = ResultSubtitle(path, language, self.__class__.__name__.lower(), '%s/%s' % (self.server_url, suburl['suburl']),
+ keywords=[suburl['release']])
+ subtitles.append(subtitle)
+ return subtitles
+
+ def download(self, subtitle):
+ logger.info(u'Downloading %s in %s' % (subtitle.link, subtitle.path))
+ try:
+ r = self.session.get(subtitle.link, headers={'Referer': subtitle.link, 'User-Agent': self.user_agent})
+ soup = BeautifulSoup(r.content, self.required_features)
+ if soup.title is not None and u'Addic7ed.com' in soup.title.text.strip():
+ raise DownloadFailedError('Download limit exceeded')
+ with open(subtitle.path, 'wb') as f:
+ f.write(r.content)
+ except Exception as e:
+ logger.error(u'Download failed: %s' % e)
+ if os.path.exists(subtitle.path):
+ os.remove(subtitle.path)
+ raise DownloadFailedError(str(e))
+ logger.debug(u'Download finished')
+ return subtitle
+
+
+Service = Addic7ed
diff --git a/libs/subliminal/services/bierdopje.py b/libs/subliminal/services/bierdopje.py
old mode 100755
new mode 100644
index 15401ada..66738fa6
--- a/libs/subliminal/services/bierdopje.py
+++ b/libs/subliminal/services/bierdopje.py
@@ -16,13 +16,14 @@
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see .
from . import ServiceBase
+from ..cache import cachedmethod
from ..exceptions import ServiceError
-from ..subtitles import get_subtitle_path, ResultSubtitle
-from ..videos import Episode
+from ..language import language_set
+from ..subtitles import get_subtitle_path, ResultSubtitle, EXTENSIONS
from ..utils import to_unicode
-import BeautifulSoup
+from ..videos import Episode
+from bs4 import BeautifulSoup
import logging
-import os.path
import urllib
try:
import cPickle as pickle
@@ -36,30 +37,22 @@ logger = logging.getLogger(__name__)
class BierDopje(ServiceBase):
server_url = 'http://api.bierdopje.com/A2B638AC5D804C2E/'
api_based = True
- languages = {'en': 'en', 'nl': 'nl'}
- reverted_languages = False
+ languages = language_set(['eng', 'dut'])
videos = [Episode]
require_video = False
+ required_features = ['xml']
- def __init__(self, config=None):
- super(BierDopje, self).__init__(config)
- self.showids = {}
- if self.config and self.config.cache_dir:
- self.init_cache()
-
- def init_cache(self):
- logger.debug(u'Initializing cache...')
- if not self.config or not self.config.cache_dir:
- raise ServiceError('Cache directory is required')
- self.showids_cache = os.path.join(self.config.cache_dir, 'bierdopje_showids.cache')
- if not os.path.exists(self.showids_cache):
- self.save_cache()
-
- def save_cache(self):
- logger.debug(u'Saving showids to cache...')
- with self.lock:
- with open(self.showids_cache, 'w') as f:
- pickle.dump(self.showids, f)
+ @cachedmethod
+ def get_show_id(self, series):
+ r = self.session.get('%sGetShowByName/%s' % (self.server_url, urllib.quote(series.lower())))
+ if r.status_code != 200:
+ logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
+ return None
+ soup = BeautifulSoup(r.content, self.required_features)
+ if soup.status.contents[0] == 'false':
+ logger.debug(u'Could not find show %s' % series)
+ return None
+ return int(soup.showid.contents[0])
def load_cache(self):
logger.debug(u'Loading showids from cache...')
@@ -67,25 +60,12 @@ class BierDopje(ServiceBase):
with open(self.showids_cache, 'r') as f:
self.showids = pickle.load(f)
- def query(self, season, episode, languages, filepath, tvdbid=None, series=None):
- self.load_cache()
+ def query(self, filepath, season, episode, languages, tvdbid=None, series=None):
+ self.init_cache()
if series:
- if series.lower() in self.showids: # from cache
- request_id = self.showids[series.lower()]
- logger.debug(u'Retreived showid %d for %s from cache' % (request_id, series))
- else: # query to get showid
- logger.debug(u'Getting showid from show name %s...' % series)
- r = self.session.get('%sGetShowByName/%s' % (self.server_url, urllib.quote(series.lower())))
- if r.status_code != 200:
- logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
- return []
- soup = BeautifulSoup.BeautifulStoneSoup(r.content)
- if soup.status.contents[0] == 'false':
- logger.debug(u'Could not find show %s' % series)
- return []
- request_id = int(soup.showid.contents[0])
- self.showids[series.lower()] = request_id
- self.save_cache()
+ request_id = self.get_show_id(series.lower())
+ if request_id is None:
+ return []
request_source = 'showid'
request_is_tvdbid = 'false'
elif tvdbid:
@@ -96,27 +76,27 @@ class BierDopje(ServiceBase):
raise ServiceError('One or more parameter missing')
subtitles = []
for language in languages:
- logger.debug(u'Getting subtitles for %s %d season %d episode %d with language %s' % (request_source, request_id, season, episode, language))
- r = self.session.get('%sGetAllSubsFor/%s/%s/%s/%s/%s' % (self.server_url, request_id, season, episode, language, request_is_tvdbid))
+ logger.debug(u'Getting subtitles for %s %d season %d episode %d with language %s' % (request_source, request_id, season, episode, language.alpha2))
+ r = self.session.get('%sGetAllSubsFor/%s/%s/%s/%s/%s' % (self.server_url, request_id, season, episode, language.alpha2, request_is_tvdbid))
if r.status_code != 200:
logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
return []
- soup = BeautifulSoup.BeautifulStoneSoup(r.content)
+ soup = BeautifulSoup(r.content, self.required_features)
if soup.status.contents[0] == 'false':
- logger.debug(u'Could not find subtitles for %s %d season %d episode %d with language %s' % (request_source, request_id, season, episode, language))
+ logger.debug(u'Could not find subtitles for %s %d season %d episode %d with language %s' % (request_source, request_id, season, episode, language.alpha2))
continue
path = get_subtitle_path(filepath, language, self.config.multi)
for result in soup.results('result'):
- subtitle = ResultSubtitle(path, language, service=self.__class__.__name__.lower(), link=result.downloadlink.contents[0],
- release=to_unicode(result.filename.contents[0]))
+ release = to_unicode(result.filename.contents[0])
+ if not release.endswith(tuple(EXTENSIONS)):
+ release += '.srt'
+ subtitle = ResultSubtitle(path, language, self.__class__.__name__.lower(), result.downloadlink.contents[0],
+ release=release)
subtitles.append(subtitle)
return subtitles
- def list(self, video, languages):
- if not self.check_validity(video, languages):
- return []
- results = self.query(video.season, video.episode, languages, video.path or video.release, video.tvdbid, video.series)
- return results
+ def list_checked(self, video, languages):
+ return self.query(video.path or video.release, video.season, video.episode, languages, video.tvdbid, video.series)
Service = BierDopje
diff --git a/libs/subliminal/services/opensubtitles.py b/libs/subliminal/services/opensubtitles.py
old mode 100755
new mode 100644
index 9dee27b9..1cee25ba
--- a/libs/subliminal/services/opensubtitles.py
+++ b/libs/subliminal/services/opensubtitles.py
@@ -17,9 +17,10 @@
# along with subliminal. If not, see .
from . import ServiceBase
from ..exceptions import ServiceError, DownloadFailedError
+from ..language import Language, language_set
from ..subtitles import get_subtitle_path, ResultSubtitle
-from ..videos import Episode, Movie
from ..utils import to_unicode
+from ..videos import Episode, Movie
import gzip
import logging
import os.path
@@ -32,34 +33,50 @@ logger = logging.getLogger(__name__)
class OpenSubtitles(ServiceBase):
server_url = 'http://api.opensubtitles.org/xml-rpc'
api_based = True
- languages = {'aa': 'aar', 'ab': 'abk', 'af': 'afr', 'ak': 'aka', 'sq': 'alb', 'am': 'amh', 'ar': 'ara',
- 'an': 'arg', 'hy': 'arm', 'as': 'asm', 'av': 'ava', 'ae': 'ave', 'ay': 'aym', 'az': 'aze',
- 'ba': 'bak', 'bm': 'bam', 'eu': 'baq', 'be': 'bel', 'bn': 'ben', 'bh': 'bih', 'bi': 'bis',
- 'bs': 'bos', 'br': 'bre', 'bg': 'bul', 'my': 'bur', 'ca': 'cat', 'ch': 'cha', 'ce': 'che',
- 'zh': 'chi', 'cu': 'chu', 'cv': 'chv', 'kw': 'cor', 'co': 'cos', 'cr': 'cre', 'cs': 'cze',
- 'da': 'dan', 'dv': 'div', 'nl': 'dut', 'dz': 'dzo', 'en': 'eng', 'eo': 'epo', 'et': 'est',
- 'ee': 'ewe', 'fo': 'fao', 'fj': 'fij', 'fi': 'fin', 'fr': 'fre', 'fy': 'fry', 'ff': 'ful',
- 'ka': 'geo', 'de': 'ger', 'gd': 'gla', 'ga': 'gle', 'gl': 'glg', 'gv': 'glv', 'el': 'ell',
- 'gn': 'grn', 'gu': 'guj', 'ht': 'hat', 'ha': 'hau', 'he': 'heb', 'hz': 'her', 'hi': 'hin',
- 'ho': 'hmo', 'hr': 'hrv', 'hu': 'hun', 'ig': 'ibo', 'is': 'ice', 'io': 'ido', 'ii': 'iii',
- 'iu': 'iku', 'ie': 'ile', 'ia': 'ina', 'id': 'ind', 'ik': 'ipk', 'it': 'ita', 'jv': 'jav',
- 'ja': 'jpn', 'kl': 'kal', 'kn': 'kan', 'ks': 'kas', 'kr': 'kau', 'kk': 'kaz', 'km': 'khm',
- 'ki': 'kik', 'rw': 'kin', 'ky': 'kir', 'kv': 'kom', 'kg': 'kon', 'ko': 'kor', 'kj': 'kua',
- 'ku': 'kur', 'lo': 'lao', 'la': 'lat', 'lv': 'lav', 'li': 'lim', 'ln': 'lin', 'lt': 'lit',
- 'lb': 'ltz', 'lu': 'lub', 'lg': 'lug', 'mk': 'mac', 'mh': 'mah', 'ml': 'mal', 'mi': 'mao',
- 'mr': 'mar', 'ms': 'may', 'mg': 'mlg', 'mt': 'mlt', 'mo': 'mol', 'mn': 'mon', 'na': 'nau',
- 'nv': 'nav', 'nr': 'nbl', 'nd': 'nde', 'ng': 'ndo', 'ne': 'nep', 'nn': 'nno', 'nb': 'nob',
- 'no': 'nor', 'ny': 'nya', 'oc': 'oci', 'oj': 'oji', 'or': 'ori', 'om': 'orm', 'os': 'oss',
- 'pa': 'pan', 'fa': 'per', 'pi': 'pli', 'pl': 'pol', 'pt': 'por', 'ps': 'pus', 'qu': 'que',
- 'rm': 'roh', 'rn': 'run', 'ru': 'rus', 'sg': 'sag', 'sa': 'san', 'sr': 'scc', 'si': 'sin',
- 'sk': 'slo', 'sl': 'slv', 'se': 'sme', 'sm': 'smo', 'sn': 'sna', 'sd': 'snd', 'so': 'som',
- 'st': 'sot', 'es': 'spa', 'sc': 'srd', 'ss': 'ssw', 'su': 'sun', 'sw': 'swa', 'sv': 'swe',
- 'ty': 'tah', 'ta': 'tam', 'tt': 'tat', 'te': 'tel', 'tg': 'tgk', 'tl': 'tgl', 'th': 'tha',
- 'bo': 'tib', 'ti': 'tir', 'to': 'ton', 'tn': 'tsn', 'ts': 'tso', 'tk': 'tuk', 'tr': 'tur',
- 'tw': 'twi', 'ug': 'uig', 'uk': 'ukr', 'ur': 'urd', 'uz': 'uzb', 've': 'ven', 'vi': 'vie',
- 'vo': 'vol', 'cy': 'wel', 'wa': 'wln', 'wo': 'wol', 'xh': 'xho', 'yi': 'yid', 'yo': 'yor',
- 'za': 'zha', 'zu': 'zul', 'ro': 'rum', 'po': 'pob', 'un': 'unk', 'ay': 'ass'}
- reverted_languages = False
+ # Source: http://www.opensubtitles.org/addons/export_languages.php
+ languages = language_set(['aar', 'abk', 'ace', 'ach', 'ada', 'ady', 'afa', 'afh', 'afr', 'ain', 'aka', 'akk',
+ 'alb', 'ale', 'alg', 'alt', 'amh', 'ang', 'apa', 'ara', 'arc', 'arg', 'arm', 'arn',
+ 'arp', 'art', 'arw', 'asm', 'ast', 'ath', 'aus', 'ava', 'ave', 'awa', 'aym', 'aze',
+ 'bad', 'bai', 'bak', 'bal', 'bam', 'ban', 'baq', 'bas', 'bat', 'bej', 'bel', 'bem',
+ 'ben', 'ber', 'bho', 'bih', 'bik', 'bin', 'bis', 'bla', 'bnt', 'bos', 'bra', 'bre',
+ 'btk', 'bua', 'bug', 'bul', 'bur', 'byn', 'cad', 'cai', 'car', 'cat', 'cau', 'ceb',
+ 'cel', 'cha', 'chb', 'che', 'chg', 'chi', 'chk', 'chm', 'chn', 'cho', 'chp', 'chr',
+ 'chu', 'chv', 'chy', 'cmc', 'cop', 'cor', 'cos', 'cpe', 'cpf', 'cpp', 'cre', 'crh',
+ 'crp', 'csb', 'cus', 'cze', 'dak', 'dan', 'dar', 'day', 'del', 'den', 'dgr', 'din',
+ 'div', 'doi', 'dra', 'dua', 'dum', 'dut', 'dyu', 'dzo', 'efi', 'egy', 'eka', 'ell',
+ 'elx', 'eng', 'enm', 'epo', 'est', 'ewe', 'ewo', 'fan', 'fao', 'fat', 'fij', 'fil',
+ 'fin', 'fiu', 'fon', 'fre', 'frm', 'fro', 'fry', 'ful', 'fur', 'gaa', 'gay', 'gba',
+ 'gem', 'geo', 'ger', 'gez', 'gil', 'gla', 'gle', 'glg', 'glv', 'gmh', 'goh', 'gon',
+ 'gor', 'got', 'grb', 'grc', 'grn', 'guj', 'gwi', 'hai', 'hat', 'hau', 'haw', 'heb',
+ 'her', 'hil', 'him', 'hin', 'hit', 'hmn', 'hmo', 'hrv', 'hun', 'hup', 'iba', 'ibo',
+ 'ice', 'ido', 'iii', 'ijo', 'iku', 'ile', 'ilo', 'ina', 'inc', 'ind', 'ine', 'inh',
+ 'ipk', 'ira', 'iro', 'ita', 'jav', 'jpn', 'jpr', 'jrb', 'kaa', 'kab', 'kac', 'kal',
+ 'kam', 'kan', 'kar', 'kas', 'kau', 'kaw', 'kaz', 'kbd', 'kha', 'khi', 'khm', 'kho',
+ 'kik', 'kin', 'kir', 'kmb', 'kok', 'kom', 'kon', 'kor', 'kos', 'kpe', 'krc', 'kro',
+ 'kru', 'kua', 'kum', 'kur', 'kut', 'lad', 'lah', 'lam', 'lao', 'lat', 'lav', 'lez',
+ 'lim', 'lin', 'lit', 'lol', 'loz', 'ltz', 'lua', 'lub', 'lug', 'lui', 'lun', 'luo',
+ 'lus', 'mac', 'mad', 'mag', 'mah', 'mai', 'mak', 'mal', 'man', 'mao', 'map', 'mar',
+ 'mas', 'may', 'mdf', 'mdr', 'men', 'mga', 'mic', 'min', 'mkh', 'mlg', 'mlt', 'mnc',
+ 'mni', 'mno', 'moh', 'mon', 'mos', 'mun', 'mus', 'mwl', 'mwr', 'myn', 'myv', 'nah',
+ 'nai', 'nap', 'nau', 'nav', 'nbl', 'nde', 'ndo', 'nds', 'nep', 'new', 'nia', 'nic',
+ 'niu', 'nno', 'nob', 'nog', 'non', 'nor', 'nso', 'nub', 'nwc', 'nya', 'nym', 'nyn',
+ 'nyo', 'nzi', 'oci', 'oji', 'ori', 'orm', 'osa', 'oss', 'ota', 'oto', 'paa', 'pag',
+ 'pal', 'pam', 'pan', 'pap', 'pau', 'peo', 'per', 'phi', 'phn', 'pli', 'pol', 'pon',
+ 'por', 'pra', 'pro', 'pus', 'que', 'raj', 'rap', 'rar', 'roa', 'roh', 'rom', 'rum',
+ 'run', 'rup', 'rus', 'sad', 'sag', 'sah', 'sai', 'sal', 'sam', 'san', 'sas', 'sat',
+ 'scn', 'sco', 'sel', 'sem', 'sga', 'sgn', 'shn', 'sid', 'sin', 'sio', 'sit', 'sla',
+ 'slo', 'slv', 'sma', 'sme', 'smi', 'smj', 'smn', 'smo', 'sms', 'sna', 'snd', 'snk',
+ 'sog', 'som', 'son', 'sot', 'spa', 'srd', 'srp', 'srr', 'ssa', 'ssw', 'suk', 'sun',
+ 'sus', 'sux', 'swa', 'swe', 'syr', 'tah', 'tai', 'tam', 'tat', 'tel', 'tem', 'ter',
+ 'tet', 'tgk', 'tgl', 'tha', 'tib', 'tig', 'tir', 'tiv', 'tkl', 'tlh', 'tli', 'tmh',
+ 'tog', 'ton', 'tpi', 'tsi', 'tsn', 'tso', 'tuk', 'tum', 'tup', 'tur', 'tut', 'tvl',
+ 'twi', 'tyv', 'udm', 'uga', 'uig', 'ukr', 'umb', 'urd', 'uzb', 'vai', 'ven', 'vie',
+ 'vol', 'vot', 'wak', 'wal', 'war', 'was', 'wel', 'wen', 'wln', 'wol', 'xal', 'xho',
+ 'yao', 'yap', 'yid', 'yor', 'ypk', 'zap', 'zen', 'zha', 'znd', 'zul', 'zun',
+ 'por-BR', 'rum-MD'])
+ language_map = {'mol': Language('rum-MD'), 'scc': Language('srp'), 'pob': Language('por-BR'),
+ Language('rum-MD'): 'mol', Language('srp'): 'scc', Language('por-BR'): 'pob'}
+ language_code = 'alpha3'
videos = [Episode, Movie]
require_video = False
confidence_order = ['moviehash', 'imdbid', 'fulltext']
@@ -92,7 +109,7 @@ class OpenSubtitles(ServiceBase):
if not searches:
raise ServiceError('One or more parameter missing')
for search in searches:
- search['sublanguageid'] = ','.join([self.get_language(l) for l in languages])
+ search['sublanguageid'] = ','.join(self.get_code(l) for l in languages)
logger.debug(u'Getting subtitles %r with token %s' % (searches, self.token))
results = self.server.SearchSubtitles(self.token, searches)
if not results['data']:
@@ -100,17 +117,15 @@ class OpenSubtitles(ServiceBase):
return []
subtitles = []
for result in results['data']:
- language = self.get_revert_language(result['SubLanguageID'])
+ language = self.get_language(result['SubLanguageID'])
path = get_subtitle_path(filepath, language, self.config.multi)
confidence = 1 - float(self.confidence_order.index(result['MatchedBy'])) / float(len(self.confidence_order))
- subtitle = ResultSubtitle(path, language, service=self.__class__.__name__.lower(), link=result['SubDownloadLink'],
+ subtitle = ResultSubtitle(path, language, self.__class__.__name__.lower(), result['SubDownloadLink'],
release=to_unicode(result['SubFileName']), confidence=confidence)
subtitles.append(subtitle)
return subtitles
- def list(self, video, languages):
- if not self.check_validity(video, languages):
- return []
+ def list_checked(self, video, languages):
results = []
if video.exists:
results = self.query(video.path or video.release, languages, moviehash=video.hashes['OpenSubtitles'], size=str(video.size))
diff --git a/libs/subliminal/services/podnapisi.py b/libs/subliminal/services/podnapisi.py
new file mode 100644
index 00000000..618c0e77
--- /dev/null
+++ b/libs/subliminal/services/podnapisi.py
@@ -0,0 +1,110 @@
+# -*- coding: utf-8 -*-
+# Copyright 2011-2012 Antoine Bertin
+#
+# This file is part of subliminal.
+#
+# subliminal is free software; you can redistribute it and/or modify it under
+# the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# subliminal is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with subliminal. If not, see .
+from . import ServiceBase
+from ..exceptions import ServiceError, DownloadFailedError
+from ..language import language_set, Language
+from ..subtitles import get_subtitle_path, ResultSubtitle
+from ..utils import to_unicode
+from ..videos import Episode, Movie
+from hashlib import md5, sha256
+import logging
+import xmlrpclib
+
+
+logger = logging.getLogger(__name__)
+
+
+class Podnapisi(ServiceBase):
+ server_url = 'http://ssp.podnapisi.net:8000'
+ api_based = True
+ languages = language_set(['ar', 'be', 'bg', 'bs', 'ca', 'ca', 'cs', 'da', 'de', 'el', 'en',
+ 'es', 'et', 'fa', 'fi', 'fr', 'ga', 'he', 'hi', 'hr', 'hu', 'id',
+ 'is', 'it', 'ja', 'ko', 'lt', 'lv', 'mk', 'ms', 'nl', 'nn', 'pl',
+ 'pt', 'ro', 'ru', 'sk', 'sl', 'sq', 'sr', 'sv', 'th', 'tr', 'uk',
+ 'vi', 'zh', 'es-ar', 'pt-br'])
+ language_map = {'jp': Language('jpn'), Language('jpn'): 'jp',
+ 'gr': Language('gre'), Language('gre'): 'gr',
+ 'pb': Language('por-BR'), Language('por-BR'): 'pb',
+ 'ag': Language('spa-AR'), Language('spa-AR'): 'ag',
+ 'cyr': Language('srp')}
+ videos = [Episode, Movie]
+ require_video = True
+
+ def __init__(self, config=None):
+ super(Podnapisi, self).__init__(config)
+ self.server = xmlrpclib.ServerProxy(self.server_url)
+ self.token = None
+
+ def init(self):
+ super(Podnapisi, self).init()
+ result = self.server.initiate(self.user_agent)
+ if result['status'] != 200:
+ raise ServiceError('Initiate failed')
+ username = 'python_subliminal'
+ password = sha256(md5('XWFXQ6gE5Oe12rv4qxXX').hexdigest() + result['nonce']).hexdigest()
+ self.token = result['session']
+ result = self.server.authenticate(self.token, username, password)
+ if result['status'] != 200:
+ raise ServiceError('Authenticate failed')
+
+ def terminate(self):
+ super(Podnapisi, self).terminate()
+
+ def query(self, filepath, languages, moviehash):
+ results = self.server.search(self.token, [moviehash])
+ if results['status'] != 200:
+ logger.error('Search failed with error code %d' % results['status'])
+ return []
+ if not results['results'] or not results['results'][moviehash]['subtitles']:
+ logger.debug(u'Could not find subtitles for %r with token %s' % (moviehash, self.token))
+ return []
+ subtitles = []
+ for result in results['results'][moviehash]['subtitles']:
+ language = self.get_language(result['lang'])
+ if language not in languages:
+ continue
+ path = get_subtitle_path(filepath, language, self.config.multi)
+ subtitle = ResultSubtitle(path, language, self.__class__.__name__.lower(), result['id'],
+ release=to_unicode(result['release']), confidence=result['weight'])
+ subtitles.append(subtitle)
+ if not subtitles:
+ return []
+ # Convert weight to confidence
+ max_weight = float(max([s.confidence for s in subtitles]))
+ min_weight = float(min([s.confidence for s in subtitles]))
+ for subtitle in subtitles:
+ if max_weight == 0 and min_weight == 0:
+ subtitle.confidence = 1.0
+ else:
+ subtitle.confidence = (subtitle.confidence - min_weight) / (max_weight - min_weight)
+ return subtitles
+
+ def list_checked(self, video, languages):
+ results = self.query(video.path, languages, video.hashes['OpenSubtitles'])
+ return results
+
+ def download(self, subtitle):
+ results = self.server.download(self.token, [subtitle.link])
+ if results['status'] != 200:
+ raise DownloadFailedError()
+ subtitle.link = 'http://www.podnapisi.net/static/podnapisi/' + results['names'][0]['filename']
+ self.download_file(subtitle.link, subtitle.path)
+ return subtitle
+
+
+Service = Podnapisi
diff --git a/libs/subliminal/services/subswiki.py b/libs/subliminal/services/subswiki.py
old mode 100755
new mode 100644
index c5670c1c..a7b98539
--- a/libs/subliminal/services/subswiki.py
+++ b/libs/subliminal/services/subswiki.py
@@ -17,10 +17,11 @@
# along with subliminal. If not, see .
from . import ServiceBase
from ..exceptions import ServiceError
+from ..language import language_set, Language
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..videos import Episode, Movie
+from bs4 import BeautifulSoup
from subliminal.utils import get_keywords, split_keyword
-import BeautifulSoup
import logging
import re
import urllib
@@ -32,17 +33,17 @@ logger = logging.getLogger(__name__)
class SubsWiki(ServiceBase):
server_url = 'http://www.subswiki.com'
api_based = False
- languages = {u'English (US)': 'en', u'English (UK)': 'en', u'English': 'en', u'French': 'fr', u'Brazilian': 'po',
- u'Portuguese': 'pt', u'Español (Latinoamérica)': 'es', u'Español (España)': 'es', u'Español': 'es',
- u'Italian': 'it', u'Català': 'ca'}
- reverted_languages = True
+ languages = language_set(['eng-US', 'eng-GB', 'eng', 'fre', 'por-BR', 'por', 'spa-ES', u'spa', u'ita', u'cat'])
+ language_map = {u'Español': Language('spa'), u'Español (España)': Language('spa'), u'Español (Latinoamérica)': Language('spa'),
+ u'Català': Language('cat'), u'Brazilian': Language('por-BR'), u'English (US)': Language('eng-US'),
+ u'English (UK)': Language('eng-GB')}
+ language_code = 'name'
videos = [Episode, Movie]
require_video = False
release_pattern = re.compile('\nVersion (.+), ([0-9]+).([0-9])+ MBs')
+ required_features = ['permissive']
- def list(self, video, languages):
- if not self.check_validity(video, languages):
- return []
+ def list_checked(self, video, languages):
results = []
if isinstance(video, Episode):
results = self.query(video.path or video.release, languages, get_keywords(video.guess), series=video.series, season=video.season, episode=video.episode)
@@ -74,7 +75,7 @@ class SubsWiki(ServiceBase):
if r.status_code != 200:
logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
return []
- soup = BeautifulSoup.BeautifulSoup(r.content)
+ soup = BeautifulSoup(r.content, self.required_features)
subtitles = []
for sub in soup('td', {'class': 'NewsTitle'}):
sub_keywords = split_keyword(self.release_pattern.search(sub.contents[1]).group(1).lower())
@@ -82,8 +83,8 @@ class SubsWiki(ServiceBase):
logger.debug(u'None of subtitle keywords %r in %r' % (sub_keywords, keywords))
continue
for html_language in sub.parent.parent.findAll('td', {'class': 'language'}):
- language = self.get_revert_language(html_language.string.strip())
- if not language in languages:
+ language = self.get_language(html_language.string.strip())
+ if language not in languages:
logger.debug(u'Language %r not in wanted languages %r' % (language, languages))
continue
html_status = html_language.findNextSibling('td')
@@ -92,8 +93,9 @@ class SubsWiki(ServiceBase):
logger.debug(u'Wrong subtitle status %s' % status)
continue
path = get_subtitle_path(filepath, language, self.config.multi)
- subtitle = ResultSubtitle(path, language, service=self.__class__.__name__.lower(), link='%s%s' % (self.server_url, html_status.findNext('td').find('a')['href']))
+ subtitle = ResultSubtitle(path, language, self.__class__.__name__.lower(), '%s%s' % (self.server_url, html_status.findNext('td').find('a')['href']))
subtitles.append(subtitle)
return subtitles
+
Service = SubsWiki
diff --git a/libs/subliminal/services/subtitulos.py b/libs/subliminal/services/subtitulos.py
old mode 100755
new mode 100644
index 44888e70..c40b76d2
--- a/libs/subliminal/services/subtitulos.py
+++ b/libs/subliminal/services/subtitulos.py
@@ -16,10 +16,11 @@
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see .
from . import ServiceBase
+from ..language import language_set, Language
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..videos import Episode
+from bs4 import BeautifulSoup
from subliminal.utils import get_keywords, split_keyword
-import BeautifulSoup
import logging
import re
import unicodedata
@@ -32,19 +33,21 @@ logger = logging.getLogger(__name__)
class Subtitulos(ServiceBase):
server_url = 'http://www.subtitulos.es'
api_based = False
- languages = {u'English (US)': 'en', u'English (UK)': 'en', u'English': 'en', u'French': 'fr', u'Brazilian': 'po',
- u'Portuguese': 'pt', u'Español (Latinoamérica)': 'es', u'Español (España)': 'es', u'Español': 'es',
- u'Italian': 'it', u'Català': 'ca'}
- reverted_languages = True
+ languages = language_set(['eng-US', 'eng-GB', 'eng', 'fre', 'por-BR', 'por', 'spa-ES', u'spa', u'ita', u'cat'])
+ language_map = {u'Español': Language('spa'), u'Español (España)': Language('spa'), u'Español (Latinoamérica)': Language('spa'),
+ u'Català': Language('cat'), u'Brazilian': Language('por-BR'), u'English (US)': Language('eng-US'),
+ u'English (UK)': Language('eng-GB'), 'Galego': Language('glg')}
+ language_code = 'name'
videos = [Episode]
require_video = False
- release_pattern = re.compile('Versión (.+) ([0-9]+).([0-9])+ megabytes')
+ required_features = ['permissive']
+ # the '.+' in the pattern for Version allows us to match both 'ó'
+ # and the 'ó' char directly. This is because now BS4 converts the html
+ # code chars into their equivalent unicode char
+ release_pattern = re.compile('Versi.+n (.+) ([0-9]+).([0-9])+ megabytes')
- def list(self, video, languages):
- if not self.check_validity(video, languages):
- return []
- results = self.query(video.path or video.release, languages, get_keywords(video.guess), video.series, video.season, video.episode)
- return results
+ def list_checked(self, video, languages):
+ return self.query(video.path or video.release, languages, get_keywords(video.guess), video.series, video.season, video.episode)
def query(self, filepath, languages, keywords, series, season, episode):
request_series = series.lower().replace(' ', '_')
@@ -58,7 +61,7 @@ class Subtitulos(ServiceBase):
if r.status_code != 200:
logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
return []
- soup = BeautifulSoup.BeautifulSoup(r.content)
+ soup = BeautifulSoup(r.content, self.required_features)
subtitles = []
for sub in soup('div', {'id': 'version'}):
sub_keywords = split_keyword(self.release_pattern.search(sub.find('p', {'class': 'title-sub'}).contents[1]).group(1).lower())
@@ -66,8 +69,8 @@ class Subtitulos(ServiceBase):
logger.debug(u'None of subtitle keywords %r in %r' % (sub_keywords, keywords))
continue
for html_language in sub.findAllNext('ul', {'class': 'sslist'}):
- language = self.get_revert_language(html_language.findNext('li', {'class': 'li-idioma'}).find('strong').contents[0].string.strip())
- if not language in languages:
+ language = self.get_language(html_language.findNext('li', {'class': 'li-idioma'}).find('strong').contents[0].string.strip())
+ if language not in languages:
logger.debug(u'Language %r not in wanted languages %r' % (language, languages))
continue
html_status = html_language.findNext('li', {'class': 'li-estado green'})
@@ -76,8 +79,10 @@ class Subtitulos(ServiceBase):
logger.debug(u'Wrong subtitle status %s' % status)
continue
path = get_subtitle_path(filepath, language, self.config.multi)
- subtitle = ResultSubtitle(path, language, service=self.__class__.__name__.lower(), link=html_status.findNext('span', {'class': 'descargar green'}).find('a')['href'], keywords=sub_keywords)
+ subtitle = ResultSubtitle(path, language, self.__class__.__name__.lower(), html_status.findNext('span', {'class': 'descargar green'}).find('a')['href'],
+ keywords=sub_keywords)
subtitles.append(subtitle)
return subtitles
+
Service = Subtitulos
diff --git a/libs/subliminal/services/thesubdb.py b/libs/subliminal/services/thesubdb.py
old mode 100755
new mode 100644
index cccddd40..274c775f
--- a/libs/subliminal/services/thesubdb.py
+++ b/libs/subliminal/services/thesubdb.py
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see .
from . import ServiceBase
+from ..language import language_set
from ..subtitles import get_subtitle_path, ResultSubtitle
from ..videos import Episode, Movie, UnknownVideo
import logging
@@ -25,22 +26,18 @@ logger = logging.getLogger(__name__)
class TheSubDB(ServiceBase):
- server_url = 'http://api.thesubdb.com/' # for testing purpose, use http://sandbox.thesubdb.com/ instead
- user_agent = 'SubDB/1.0 (subliminal/0.5; https://github.com/Diaoul/subliminal)' # defined by the API
+ server_url = 'http://api.thesubdb.com'
+ user_agent = 'SubDB/1.0 (subliminal/0.6; https://github.com/Diaoul/subliminal)'
api_based = True
- languages = {'af': 'af', 'cs': 'cs', 'da': 'da', 'de': 'de', 'en': 'en', 'es': 'es', 'fi': 'fi',
- 'fr': 'fr', 'hu': 'hu', 'id': 'id', 'it': 'it', 'la': 'la', 'nl': 'nl', 'no': 'no',
- 'oc': 'oc', 'pl': 'pl', 'pt': 'pt', 'ro': 'ro', 'ru': 'ru', 'sl': 'sl', 'sr': 'sr',
- 'sv': 'sv', 'tr': 'tr'} # list available with the API at http://sandbox.thesubdb.com/?action=languages
- reverted_languages = False
+ # Source: http://api.thesubdb.com/?action=languages
+ languages = language_set(['af', 'cs', 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'id', 'it',
+ 'la', 'nl', 'no', 'oc', 'pl', 'pt', 'ro', 'ru', 'sl', 'sr', 'sv',
+ 'tr'])
videos = [Movie, Episode, UnknownVideo]
require_video = True
- def list(self, video, languages):
- if not self.check_validity(video, languages):
- return []
- results = self.query(video.path, video.hashes['TheSubDB'], languages)
- return results
+ def list_checked(self, video, languages):
+ return self.query(video.path, video.hashes['TheSubDB'], languages)
def query(self, filepath, moviehash, languages):
r = self.session.get(self.server_url, params={'action': 'search', 'hash': moviehash})
@@ -50,7 +47,7 @@ class TheSubDB(ServiceBase):
if r.status_code != 200:
logger.error(u'Request %s returned status code %d' % (r.url, r.status_code))
return []
- available_languages = set([self.get_revert_language(l) for l in r.content.split(',')])
+ available_languages = language_set(r.content.split(','))
languages &= available_languages
if not languages:
logger.debug(u'Could not find subtitles for hash %s with languages %r (only %r available)' % (moviehash, languages, available_languages))
@@ -58,8 +55,9 @@ class TheSubDB(ServiceBase):
subtitles = []
for language in languages:
path = get_subtitle_path(filepath, language, self.config.multi)
- subtitle = ResultSubtitle(path, language, service=self.__class__.__name__.lower(), link='%s?action=download&hash=%s&language=%s' % (self.server_url, moviehash, self.get_language(language)))
+ subtitle = ResultSubtitle(path, language, self.__class__.__name__.lower(), '%s?action=download&hash=%s&language=%s' % (self.server_url, moviehash, language.alpha2))
subtitles.append(subtitle)
return subtitles
+
Service = TheSubDB
diff --git a/libs/subliminal/services/tvsubtitles.py b/libs/subliminal/services/tvsubtitles.py
new file mode 100644
index 00000000..a260f169
--- /dev/null
+++ b/libs/subliminal/services/tvsubtitles.py
@@ -0,0 +1,142 @@
+# -*- coding: utf-8 -*-
+# Copyright 2012 Nicolas Wack
+#
+# This file is part of subliminal.
+#
+# subliminal is free software; you can redistribute it and/or modify it under
+# the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# subliminal is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with subliminal. If not, see .
+from . import ServiceBase
+from ..cache import cachedmethod
+from ..language import language_set, Language
+from ..subtitles import get_subtitle_path, ResultSubtitle
+from ..utils import get_keywords
+from ..videos import Episode
+from bs4 import BeautifulSoup
+import logging
+import re
+
+
+logger = logging.getLogger(__name__)
+
+
+def match(pattern, string):
+ try:
+ return re.search(pattern, string).group(1)
+ except AttributeError:
+ logger.debug(u'Could not match %r on %r' % (pattern, string))
+ return None
+
+
+class TvSubtitles(ServiceBase):
+ server_url = 'http://www.tvsubtitles.net'
+ api_based = False
+ languages = language_set(['ar', 'bg', 'cs', 'da', 'de', 'el', 'en', 'es', 'fi', 'fr', 'hu',
+ 'it', 'ja', 'ko', 'nl', 'pl', 'pt', 'ro', 'ru', 'sv', 'tr', 'uk',
+ 'zh', 'pt-br'])
+ #TODO: Find more exceptions
+ language_map = {'gr': Language('gre'), 'cz': Language('cze'), 'ua': Language('ukr'),
+ 'cn': Language('chi')}
+ videos = [Episode]
+ require_video = False
+ required_features = ['permissive']
+
+ @cachedmethod
+ def get_likely_series_id(self, name):
+ r = self.session.post('%s/search.php' % self.server_url, data={'q': name})
+ soup = BeautifulSoup(r.content, self.required_features)
+ maindiv = soup.find('div', 'left')
+ results = []
+ for elem in maindiv.find_all('li'):
+ sid = int(match('tvshow-([0-9]+)\.html', elem.a['href']))
+ show_name = match('(.*) \(', elem.a.text)
+ results.append((show_name, sid))
+ #TODO: pick up the best one in a smart way
+ result = results[0]
+ return result[1]
+
+ @cachedmethod
+ def get_episode_id(self, series_id, season, number):
+ """Get the TvSubtitles id for the given episode. Raises KeyError if none
+ could be found."""
+ # download the page of the season, contains ids for all episodes
+ episode_id = None
+ r = self.session.get('%s/tvshow-%d-%d.html' % (self.server_url, series_id, season))
+ soup = BeautifulSoup(r.content, self.required_features)
+ table = soup.find('table', id='table5')
+ for row in table.find_all('tr'):
+ cells = row.find_all('td')
+ if not cells:
+ continue
+ episode_number = match('x([0-9]+)', cells[0].text)
+ if not episode_number:
+ continue
+ episode_number = int(episode_number)
+ episode_id = int(match('episode-([0-9]+)', cells[1].a['href']))
+ # we could just return the id of the queried episode, but as we
+ # already downloaded the whole page we might as well fill in the
+ # information for all the episodes of the season
+ self.cache_for(self.get_episode_id, args=(series_id, season, episode_number), result=episode_id)
+ # raises KeyError if not found
+ return self.cached_value(self.get_episode_id, args=(series_id, season, number))
+
+ # Do not cache this method in order to always check for the most recent
+ # subtitles
+ def get_sub_ids(self, episode_id):
+ subids = []
+ r = self.session.get('%s/episode-%d.html' % (self.server_url, episode_id))
+ epsoup = BeautifulSoup(r.content, self.required_features)
+ for subdiv in epsoup.find_all('a'):
+ if 'href' not in subdiv.attrs or not subdiv['href'].startswith('/subtitle'):
+ continue
+ subid = int(match('([0-9]+)', subdiv['href']))
+ lang = self.get_language(match('flags/(.*).gif', subdiv.img['src']))
+ result = {'subid': subid, 'language': lang}
+ for p in subdiv.find_all('p'):
+ if 'alt' in p.attrs and p['alt'] == 'rip':
+ result['rip'] = p.text.strip()
+ if 'alt' in p.attrs and p['alt'] == 'release':
+ result['release'] = p.text.strip()
+ subids.append(result)
+ return subids
+
+ def list_checked(self, video, languages):
+ return self.query(video.path or video.release, languages, get_keywords(video.guess), video.series, video.season, video.episode)
+
+ def query(self, filepath, languages, keywords, series, season, episode):
+ logger.debug(u'Getting subtitles for %s season %d episode %d with languages %r' % (series, season, episode, languages))
+ self.init_cache()
+ sid = self.get_likely_series_id(series.lower())
+ try:
+ ep_id = self.get_episode_id(sid, season, episode)
+ except KeyError:
+ logger.debug(u'Could not find episode id for %s season %d episode %d' % (series, season, episode))
+ return []
+ subids = self.get_sub_ids(ep_id)
+ # filter the subtitles with our queried languages
+ subtitles = []
+ for subid in subids:
+ language = subid['language']
+ if language not in languages:
+ continue
+ path = get_subtitle_path(filepath, language, self.config.multi)
+ subtitle = ResultSubtitle(path, language, self.__class__.__name__.lower(), '%s/download-%d.html' % (self.server_url, subid['subid']),
+ keywords=[subid['rip'], subid['release']])
+ subtitles.append(subtitle)
+ return subtitles
+
+ def download(self, subtitle):
+ self.download_zip_file(subtitle.link, subtitle.path)
+ return subtitle
+
+
+Service = TvSubtitles
diff --git a/libs/subliminal/subtitles.py b/libs/subliminal/subtitles.py
old mode 100755
new mode 100644
index 355046dc..a8664777
--- a/libs/subliminal/subtitles.py
+++ b/libs/subliminal/subtitles.py
@@ -15,13 +15,13 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see .
-from .languages import list_languages, convert_language
+from .language import Language
+from .utils import to_unicode
import os.path
__all__ = ['Subtitle', 'EmbeddedSubtitle', 'ExternalSubtitle', 'ResultSubtitle', 'get_subtitle_path']
-
#: Subtitles extensions
EXTENSIONS = ['.srt', '.sub', '.txt']
@@ -30,10 +30,13 @@ class Subtitle(object):
"""Base class for subtitles
:param string path: path to the subtitle
- :param string language: language of the subtitle (second element of :class:`~subliminal.languages.LANGUAGES`)
+ :param language: language of the subtitle
+ :type language: :class:`~subliminal.language.Language`
"""
def __init__(self, path, language):
+ if not isinstance(language, Language):
+ raise TypeError('%r is not an instance of Language')
self.path = path
self.language = language
@@ -44,12 +47,22 @@ class Subtitle(object):
return os.path.exists(self.path)
return False
+ def __unicode__(self):
+ return to_unicode(self.path)
+
+ def __str__(self):
+ return unicode(self).encode('utf-8')
+
+ def __repr__(self):
+ return '%s(%s, %s)' % (self.__class__.__name__, self, self.language)
+
class EmbeddedSubtitle(Subtitle):
"""Subtitle embedded in a container
:param string path: path to the subtitle
- :param string language: language of the subtitle (second element of :class:`~subliminal.languages.LANGUAGES`)
+ :param language: language of the subtitle
+ :type language: :class:`~subliminal.language.Language`
:param int track_id: id of the subtitle track in the container
"""
@@ -59,7 +72,7 @@ class EmbeddedSubtitle(Subtitle):
@classmethod
def from_enzyme(cls, path, subtitle):
- language = convert_language(subtitle.language, 1, 2)
+ language = Language(subtitle.language, strict=False)
return cls(path, language, subtitle.trackno)
@@ -68,16 +81,14 @@ class ExternalSubtitle(Subtitle):
@classmethod
def from_path(cls, path):
"""Create an :class:`ExternalSubtitle` from path"""
- extension = ''
+ extension = None
for e in EXTENSIONS:
if path.endswith(e):
extension = e
break
- if not extension:
+ if extension is None:
raise ValueError('Not a supported subtitle extension')
- language = os.path.splitext(path[:len(path) - len(extension)])[1][1:]
- if not language in list_languages(1):
- language = None
+ language = Language(os.path.splitext(path[:len(path) - len(extension)])[1][1:], strict=False)
return cls(path, language)
@@ -85,7 +96,8 @@ class ResultSubtitle(ExternalSubtitle):
"""Subtitle found using :mod:`~subliminal.services`
:param string path: path to the subtitle
- :param string language: language of the subtitle (second element of :class:`~subliminal.languages.LANGUAGES`)
+ :param language: language of the subtitle
+ :type language: :class:`~subliminal.language.Language`
:param string service: name of the service
:param string link: download link for the subtitle
:param string release: release name of the video
@@ -93,13 +105,13 @@ class ResultSubtitle(ExternalSubtitle):
:param set keywords: keywords that describe the subtitle
"""
- def __init__(self, path, language, service, link, release=None, confidence=1, keywords=set()):
+ def __init__(self, path, language, service, link, release=None, confidence=1, keywords=None):
super(ResultSubtitle, self).__init__(path, language)
self.service = service
self.link = link
self.release = release
self.confidence = confidence
- self.keywords = keywords
+ self.keywords = keywords or set()
@property
def single(self):
@@ -109,22 +121,29 @@ class ResultSubtitle(ExternalSubtitle):
:rtype: bool
"""
- extension = os.path.splitext(self.path)[0]
- language = os.path.splitext(self.path[:len(self.path) - len(extension)])[1][1:]
- if not language in list_languages(1):
- return True
- return False
+ return self.language == Language('Undetermined')
def __repr__(self):
- return 'ResultSubtitle(%s, %s, %.2f, %s)' % (self.language, self.service, self.confidence, self.release)
+ if not self.release:
+ return 'ResultSubtitle(%s, %s, %s, %.2f)' % (self.path, self.language, self.service, self.confidence)
+ return 'ResultSubtitle(%s, %s, %s, %.2f, release=%s)' % (self.path, self.language, self.service, self.confidence, self.release.encode('ascii', 'ignore'))
def get_subtitle_path(video_path, language, multi):
- """Create the subtitle path from the given video path using language if multi"""
+ """Create the subtitle path from the given video path using language if multi
+
+ :param string video_path: path to the video
+ :param language: language of the subtitle
+ :type language: :class:`~subliminal.language.Language`
+ :param bool multi: whether to use multi language naming or not
+ :return: path of the subtitle
+ :rtype: string
+
+ """
if not os.path.exists(video_path):
path = os.path.splitext(os.path.basename(video_path))[0]
else:
path = os.path.splitext(video_path)[0]
if multi and language:
- return path + '.%s%s' % (language, EXTENSIONS[0])
+ return path + '.%s%s' % (language.alpha2, EXTENSIONS[0])
return path + '%s' % EXTENSIONS[0]
diff --git a/libs/subliminal/tasks.py b/libs/subliminal/tasks.py
old mode 100755
new mode 100644
index 448a64f1..bccf9ab5
--- a/libs/subliminal/tasks.py
+++ b/libs/subliminal/tasks.py
@@ -35,6 +35,7 @@ class ListTask(Task):
"""
def __init__(self, video, languages, service, config):
+ super(ListTask, self).__init__()
self.video = video
self.service = service
self.languages = languages
@@ -54,6 +55,7 @@ class DownloadTask(Task):
"""
def __init__(self, video, subtitles):
+ super(DownloadTask, self).__init__()
self.video = video
self.subtitles = subtitles
diff --git a/libs/subliminal/utils.py b/libs/subliminal/utils.py
old mode 100755
new mode 100644
index 00cda322..e4fe4e85
--- a/libs/subliminal/utils.py
+++ b/libs/subliminal/utils.py
@@ -61,4 +61,9 @@ def to_unicode(data):
raise ValueError('Basestring expected')
if isinstance(data, unicode):
return data
+ for encoding in ('utf-8', 'latin-1'):
+ try:
+ return unicode(data, encoding)
+ except UnicodeDecodeError:
+ pass
return unicode(data, 'utf-8', 'replace')
diff --git a/libs/subliminal/videos.py b/libs/subliminal/videos.py
old mode 100755
new mode 100644
index 79276b14..a63bfee1
--- a/libs/subliminal/videos.py
+++ b/libs/subliminal/videos.py
@@ -16,7 +16,8 @@
# You should have received a copy of the GNU Lesser General Public License
# along with subliminal. If not, see .
from . import subtitles
-from .languages import list_languages
+from .language import Language
+from .utils import to_unicode
import enzyme
import guessit
import hashlib
@@ -130,18 +131,29 @@ class Video(object):
logger.debug(u'Failed parsing %s with enzyme' % self.path)
if isinstance(video_infos, enzyme.core.AVContainer):
results.extend([subtitles.EmbeddedSubtitle.from_enzyme(self.path, s) for s in video_infos.subtitles])
- for l in list_languages(1):
- for e in subtitles.EXTENSIONS:
- single_path = basepath + '%s' % e
- if os.path.exists(single_path):
- results.append(subtitles.ExternalSubtitle(single_path, None))
- multi_path = basepath + '.%s%s' % (l, e)
- if os.path.exists(multi_path):
- results.append(subtitles.ExternalSubtitle(multi_path, l))
+ # cannot use glob here because it chokes if there are any square
+ # brackets inside the filename, so we have to use basic string
+ # startswith/endswith comparisons
+ folder, basename = os.path.split(basepath)
+ existing = [f for f in os.listdir(folder) if f.startswith(basename)]
+ for path in existing:
+ for ext in subtitles.EXTENSIONS:
+ if path.endswith(ext):
+ language = Language(path[len(basename) + 1:-len(ext)], strict=False)
+ results.append(subtitles.ExternalSubtitle(path, language))
return results
+ def __unicode__(self):
+ return to_unicode(self.path or self.release)
+
+ def __str__(self):
+ return unicode(self).encode('utf-8')
+
def __repr__(self):
- return '%s(%r)' % (self.__class__.__name__, self.release)
+ return '%s(%s)' % (self.__class__.__name__, self)
+
+ def __hash__(self):
+ return hash(self.path or self.release)
class Episode(Video):
@@ -189,11 +201,12 @@ class UnknownVideo(Video):
pass
-def scan(entry, max_depth=3, depth=0):
+def scan(entry, max_depth=3, scan_filter=None, depth=0):
"""Scan a path for videos and subtitles
:param string entry: path
:param int max_depth: maximum folder depth
+ :param function scan_filter: filter function that takes a path as argument and returns a boolean indicating whether it has to be filtered out (``True``) or not (``False``)
:param int depth: starting depth
:return: found videos and subtitles
:rtype: list of (:class:`Video`, [:class:`~subliminal.subtitles.Subtitle`])
@@ -201,19 +214,19 @@ def scan(entry, max_depth=3, depth=0):
"""
if depth > max_depth and max_depth != 0: # we do not want to search the whole file system except if max_depth = 0
return []
- if depth == 0:
- entry = os.path.abspath(entry)
if os.path.isdir(entry): # a dir? recurse
logger.debug(u'Scanning directory %s with depth %d/%d' % (entry, depth, max_depth))
result = []
for e in os.listdir(entry):
- result.extend(scan(os.path.join(entry, e), max_depth, depth + 1))
+ result.extend(scan(os.path.join(entry, e), max_depth, scan_filter, depth + 1))
return result
if os.path.isfile(entry) or depth == 0:
logger.debug(u'Scanning file %s with depth %d/%d' % (entry, depth, max_depth))
if depth != 0: # trust the user: only check for valid format if recursing
if mimetypes.guess_type(entry)[0] not in MIMETYPES and os.path.splitext(entry)[1] not in EXTENSIONS:
return []
+ if scan_filter is not None and scan_filter(entry):
+ return []
video = Video.from_path(entry)
return [(video, video.scan())]
logger.warning(u'Scanning entry %s failed with depth %d/%d' % (entry, depth, max_depth))
@@ -260,6 +273,8 @@ def hash_thesubdb(path):
"""
readsize = 64 * 1024
+ if os.path.getsize(path) < readsize:
+ return None
with open(path, 'rb') as f:
data = f.read(readsize)
f.seek(-readsize, os.SEEK_END)
diff --git a/libs/tornado/__init__.py b/libs/tornado/__init__.py
new file mode 100755
index 00000000..61551208
--- /dev/null
+++ b/libs/tornado/__init__.py
@@ -0,0 +1,29 @@
+#!/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."""
+
+from __future__ import absolute_import, division, with_statement
+
+# 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.3.post1"
+version_info = (2, 3, 0, 1)
diff --git a/libs/tornado/auth.py b/libs/tornado/auth.py
new file mode 100755
index 00000000..a61e359a
--- /dev/null
+++ b/libs/tornado/auth.py
@@ -0,0 +1,1153 @@
+#!/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()
+"""
+
+from __future__ import absolute_import, division, with_statement
+
+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)
+
+ 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 100755
index 00000000..55a10d10
--- /dev/null
+++ b/libs/tornado/autoreload.py
@@ -0,0 +1,298 @@
+#!/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.
+
+Reloading loses any Python interpreter command-line arguments (e.g. ``-u``)
+because it re-executes Python using ``sys.executable`` and ``sys.argv``.
+Additionally, modifying these variables will cause reloading to behave
+incorrectly.
+"""
+
+from __future__ import absolute_import, division, with_statement
+
+import os
+import sys
+
+# sys.path handling
+# -----------------
+#
+# If a module is run with "python -m", the current directory (i.e. "")
+# is automatically prepended to sys.path, but not if it is run as
+# "path/to/file.py". The processing for "-m" rewrites the former to
+# the latter, so subsequent executions won't have the same path as the
+# original.
+#
+# Conversely, when run as path/to/file.py, the directory containing
+# file.py gets added to the path, which can cause confusion as imports
+# may become relative in spite of the future import.
+#
+# We address the former problem by setting the $PYTHONPATH environment
+# variable before re-execution so the new process will see the correct
+# path. We attempt to address the latter problem when tornado.autoreload
+# is run as __main__, although we can't fix the general case because
+# we cannot reliably reconstruct the original command line
+# (http://bugs.python.org/issue14208).
+
+if __name__ == "__main__":
+ # This sys.path manipulation must come before our imports (as much
+ # as possible - if we introduced a tornado.sys or tornado.os
+ # module we'd be in trouble), or else our imports would become
+ # relative again despite the future import.
+ #
+ # There is a separate __main__ block at the end of the file to call main().
+ if sys.path[0] == os.path.dirname(__file__):
+ del sys.path[0]
+
+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)
+ # sys.path fixes: see comments at top of file. If sys.path[0] is an empty
+ # string, we were (probably) invoked with -m and the effective path
+ # is about to change on re-exec. Add the current directory to $PYTHONPATH
+ # to ensure that the new process sees the same path we did.
+ 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", ""))
+ 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.
+ loader = pkgutil.get_loader(module)
+ if loader is not None:
+ watch(loader.get_filename())
+
+ wait()
+
+
+if __name__ == "__main__":
+ # See also the other __main__ block at the top of the file, which modifies
+ # sys.path before our imports
+ main()
diff --git a/libs/tornado/ca-certificates.crt b/libs/tornado/ca-certificates.crt
new file mode 100755
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-----
+MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
+BAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln
+biBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF
+MQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT
+d2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
+CgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8
+76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+
+bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c
+6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE
+emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd
+MmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt
+MDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y
+MszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y
+FGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi
+aG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM
+gI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB
+qTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7
+lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn
+8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov
+L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6
+45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO
+UYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5
+O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC
+bwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv
+GPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a
+77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC
+hdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3
+92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp
+Ld6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w
+ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt
+Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ
+-----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-----
+MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UE
+BhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWdu
+IFNpbHZlciBDQSAtIEcyMB4XDTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0Nlow
+RzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMY
+U3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
+MIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644N0Mv
+Fz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7br
+YT7QbNHm+/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieF
+nbAVlDLaYQ1HTWBCrpJH6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH
+6ATK72oxh9TAtvmUcXtnZLi2kUpCe2UuMGoM9ZDulebyzYLs2aFK7PayS+VFheZt
+eJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5hqAaEuSh6XzjZG6k4sIN/
+c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5FZGkECwJ
+MoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRH
+HTBsROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTf
+jNFusB3hB48IHpmccelM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb6
+5i/4z3GcRm25xBWNOHkDRUjvxF3XCO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOB
+rDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU
+F6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRBtjpbO8tFnb0c
+wpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0
+cDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIB
+AHPGgeAn0i0P4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShp
+WJHckRE1qTodvBqlYJ7YH39FkWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9
+xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L3XWgwF15kIwb4FDm3jH+mHtwX6WQ
+2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx/uNncqCxv1yL5PqZ
+IseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFaDGi8
+aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2X
+em1ZqSqPe97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQR
+dAtq/gsD/KNVV4n+SsuuWxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/
+OMpXEA29MC/HpeZBoNquBYeaoKRlbEwJDIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+
+hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ubDgEj8Z+7fNzcbBGXJbLy
+tGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u
+-----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 100755
index 00000000..95958c19
--- /dev/null
+++ b/libs/tornado/curl_httpclient.py
@@ -0,0 +1,441 @@
+#!/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 absolute_import, division, 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_cert is not None:
+ curl.setopt(pycurl.SSLCERT, request.client_cert)
+
+ if request.client_key is not None:
+ curl.setopt(pycurl.SSLKEY, request.client_key)
+
+ 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 100755
index 00000000..982c5db5
--- /dev/null
+++ b/libs/tornado/database.py
@@ -0,0 +1,238 @@
+#!/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."""
+
+from __future__ import absolute_import, division, with_statement
+
+import copy
+import itertools
+import logging
+import time
+
+try:
+ import MySQLdb.constants
+ import MySQLdb.converters
+ import MySQLdb.cursors
+except ImportError:
+ # If MySQLdb isn't available this module won't actually be useable,
+ # but we want it to at least be importable (mainly for readthedocs.org,
+ # which has limitations on third-party modules)
+ MySQLdb = None
+
+
+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)
+
+if MySQLdb is not None:
+ # 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 100755
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 100755
index 00000000..ed07c53d
--- /dev/null
+++ b/libs/tornado/escape.py
@@ -0,0 +1,351 @@
+#!/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.
+"""
+
+from __future__ import absolute_import, division, with_statement
+
+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("", "<\\/")
+
+
+def json_decode(value):
+ """Returns Python objects for the given JSON string."""
+ return _json_decode(to_basestring(value))
+
+
+def squeeze(value):
+ """Replace all sequences of whitespace chars with a single space."""
+ return re.sub(r"[\x00-\x20]+", " ", value).strip()
+
+
+def url_escape(value):
+ """Returns a valid URL-encoded version of the given value."""
+ return urllib.quote_plus(utf8(value))
+
+# python 3 changed things around enough that we need two separate
+# implementations of url_unescape. We also need our own implementation
+# of parse_qs since python 3's version insists on decoding everything.
+if sys.version_info[0] < 3:
+ def url_unescape(value, encoding='utf-8'):
+ """Decodes the given value from a URL.
+
+ The argument may be either a byte or unicode string.
+
+ If encoding is None, the result will be a byte string. Otherwise,
+ the result is a unicode string in the specified encoding.
+ """
+ if encoding is None:
+ return urllib.unquote_plus(utf8(value))
+ else:
+ return unicode(urllib.unquote_plus(utf8(value)), encoding)
+
+ parse_qs_bytes = parse_qs
+else:
+ def url_unescape(value, encoding='utf-8'):
+ """Decodes the given value from a URL.
+
+ The argument may be either a byte or unicode string.
+
+ If encoding is None, the result will be a byte string. Otherwise,
+ the result is a unicode string in the specified encoding.
+ """
+ if encoding is None:
+ return urllib.parse.unquote_to_bytes(value)
+ else:
+ return urllib.unquote_plus(to_basestring(value), encoding=encoding)
+
+ def parse_qs_bytes(qs, keep_blank_values=False, strict_parsing=False):
+ """Parses a query string like urlparse.parse_qs, but returns the
+ values as byte strings.
+
+ Keys still become type str (interpreted as latin1 in python3!)
+ because it's too painful to keep them as byte strings in
+ python3 and in practice they're nearly always ascii anyway.
+ """
+ # This is gross, but python3 doesn't give us another way.
+ # Latin1 is the universal donor of character encodings.
+ result = parse_qs(qs, keep_blank_values, strict_parsing,
+ encoding='latin1', errors='strict')
+ encoded = {}
+ for k, v in result.iteritems():
+ encoded[k] = [i.encode('latin1') for i in v]
+ return encoded
+
+
+_UTF8_TYPES = (bytes, type(None))
+
+
+def utf8(value):
+ """Converts a string argument to a byte string.
+
+ If the argument is already a byte string or None, it is returned unchanged.
+ Otherwise it must be a unicode string and is encoded as utf8.
+ """
+ if isinstance(value, _UTF8_TYPES):
+ return value
+ assert isinstance(value, unicode)
+ return value.encode("utf-8")
+
+_TO_UNICODE_TYPES = (unicode, type(None))
+
+
+def to_unicode(value):
+ """Converts a string argument to a unicode string.
+
+ If the argument is already a unicode string or None, it is returned
+ unchanged. Otherwise it must be a byte string and is decoded as utf8.
+ """
+ if isinstance(value, _TO_UNICODE_TYPES):
+ return value
+ assert isinstance(value, bytes)
+ return value.decode("utf-8")
+
+# to_unicode was previously named _unicode not because it was private,
+# but to avoid conflicts with the built-in unicode() function/type
+_unicode = to_unicode
+
+# When dealing with the standard library across python 2 and 3 it is
+# sometimes useful to have a direct conversion to the native string type
+if str is unicode:
+ native_str = to_unicode
+else:
+ native_str = utf8
+
+_BASESTRING_TYPES = (basestring, type(None))
+
+
+def to_basestring(value):
+ """Converts a string argument to a subclass of basestring.
+
+ In python2, byte and unicode strings are mostly interchangeable,
+ so functions that deal with a user-supplied argument in combination
+ with ascii string constants can use either and should return the type
+ the user supplied. In python3, the two types are not interchangeable,
+ so this method is needed to convert byte strings to unicode.
+ """
+ if isinstance(value, _BASESTRING_TYPES):
+ return value
+ assert isinstance(value, bytes)
+ return value.decode("utf-8")
+
+
+def recursive_unicode(obj):
+ """Walks a simple data structure, converting byte strings to unicode.
+
+ Supports lists, tuples, and dictionaries.
+ """
+ if isinstance(obj, dict):
+ return dict((recursive_unicode(k), recursive_unicode(v)) for (k, v) in obj.iteritems())
+ elif isinstance(obj, list):
+ return list(recursive_unicode(i) for i in obj)
+ elif isinstance(obj, tuple):
+ return tuple(recursive_unicode(i) for i in obj)
+ elif isinstance(obj, bytes):
+ return to_unicode(obj)
+ else:
+ return obj
+
+# I originally used the regex from
+# http://daringfireball.net/2010/07/improved_regex_for_matching_urls
+# but it gets all exponential on certain patterns (such as too many trailing
+# dots), causing the regex matcher to never return.
+# This regex should avoid those problems.
+_URL_RE = re.compile(ur"""\b((?:([\w-]+):(/{1,3})|www[.])(?:(?:(?:[^\s&()]|&|")*(?:[^!"#$%&'()*+,.:;<=>?@\[\]^`{|}~\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, or a callable
+ taking the link as an argument and returning the extra text
+ e.g. ``linkify(text, extra_params='rel="nofollow" class="external"')``,
+ or::
+
+ def extra_params_cb(url):
+ if url.startswith("http://example.com"):
+ return 'class="internal"'
+ else:
+ return 'class="external" rel="nofollow"'
+ linkify(text, extra_params=extra_params_cb)
+
+ 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 and not callable(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
+
+ if callable(extra_params):
+ params = " " + extra_params(href).strip()
+ else:
+ 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 100755
index 00000000..506697d7
--- /dev/null
+++ b/libs/tornado/gen.py
@@ -0,0 +1,409 @@
+"""``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 absolute_import, division, 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) as deactivate:
+ gen = func(*args, **kwargs)
+ if isinstance(gen, types.GeneratorType):
+ runner = Runner(gen, deactivate)
+ runner.run()
+ return
+ assert gen is None, gen
+ deactivate()
+ # 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, deactivate_stack_context):
+ self.gen = gen
+ self.deactivate_stack_context = deactivate_stack_context
+ 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)
+ self.deactivate_stack_context()
+ 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 100755
index 00000000..0fcc943f
--- /dev/null
+++ b/libs/tornado/httpclient.py
@@ -0,0 +1,442 @@
+"""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.
+"""
+
+from __future__ import absolute_import, division, with_statement
+
+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, **kwargs):
+ self._io_loop = IOLoop()
+ if async_client_class is None:
+ async_client_class = AsyncHTTPClient
+ self._async_client = async_client_class(self._io_loop, **kwargs)
+ 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
+
+ _DEFAULT_MAX_CLIENTS = 10
+
+ @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=None, 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)
+ if max_clients is not None:
+ # max_clients is special because it may be passed
+ # positionally instead of by keyword
+ args["max_clients"] = max_clients
+ elif "max_clients" not in args:
+ args["max_clients"] = AsyncHTTPClient._DEFAULT_MAX_CLIENTS
+ instance.initialize(io_loop, **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
+
+ @staticmethod
+ def _save_configuration():
+ return (AsyncHTTPClient._impl_class, AsyncHTTPClient._impl_kwargs)
+
+ @staticmethod
+ def _restore_configuration(saved):
+ AsyncHTTPClient._impl_class = saved[0]
+ AsyncHTTPClient._impl_kwargs = saved[1]
+
+
+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=None, buffer=None,
+ effective_url=None, error=None, request_time=None,
+ time_info=None):
+ self.request = request
+ self.code = code
+ if headers is not None:
+ self.headers = headers
+ else:
+ self.headers = httputil.HTTPHeaders()
+ 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 or {}
+
+ 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 100755
index 00000000..952a6a26
--- /dev/null
+++ b/libs/tornado/httpserver.py
@@ -0,0 +1,469 @@
+#!/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`.
+"""
+
+from __future__ import absolute_import, division, with_statement
+
+import Cookie
+import logging
+import socket
+import time
+
+from tornado.escape import 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
+ 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:])
+
+ # HTTPRequest wants an IP, not a full socket address
+ if getattr(self.stream.socket, 'family', socket.AF_INET) in (
+ socket.AF_INET, socket.AF_INET6):
+ # Jython 2.5.2 doesn't have the socket.family attribute,
+ # so just assume IP in that case.
+ remote_ip = self.address[0]
+ else:
+ # Unix (or other) socket; fake the remote address
+ remote_ip = '0.0.0.0'
+
+ self._request = HTTPRequest(
+ connection=self, method=method, uri=uri, version=version,
+ headers=headers, remote_ip=remote_ip)
+
+ 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
+ if self._request.method in ("POST", "PATCH", "PUT"):
+ httputil.parse_body_arguments(
+ self._request.headers.get("Content-Type", ""), data,
+ self._request.arguments, self._request.files)
+ 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
+
+ self.path, sep, self.query = uri.partition('?')
+ arguments = parse_qs_bytes(self.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 100755
index 00000000..6f5d07a6
--- /dev/null
+++ b/libs/tornado/httputil.py
@@ -0,0 +1,312 @@
+#!/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."""
+
+from __future__ import absolute_import, division, with_statement
+
+import logging
+import urllib
+import re
+
+from tornado.escape import native_str, parse_qs_bytes, utf8
+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
+ if (len(args) == 1 and len(kwargs) == 0 and
+ isinstance(args[0], HTTPHeaders)):
+ # Copy constructor
+ for k, v in args[0].get_all():
+ self.add(k, v)
+ else:
+ # Dict-style initialization
+ 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
+
+ def copy(self):
+ # default implementation returns dict(self), not the subclass
+ return HTTPHeaders(self)
+
+ _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_body_arguments(content_type, body, arguments, files):
+ if content_type.startswith("application/x-www-form-urlencoded"):
+ uri_arguments = parse_qs_bytes(native_str(body))
+ for name, values in uri_arguments.iteritems():
+ values = [v for v in values if v]
+ if values:
+ 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:
+ parse_multipart_form_data(utf8(v), body, arguments, files)
+ break
+ else:
+ logging.warning("Invalid multipart/form-data")
+
+
+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]
+ final_boundary_index = data.rfind(b("--") + boundary + b("--"))
+ if final_boundary_index == -1:
+ logging.warning("Invalid multipart/form-data: no final boundary")
+ return
+ parts = data[:final_boundary_index].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()
diff --git a/libs/tornado/ioloop.py b/libs/tornado/ioloop.py
new file mode 100755
index 00000000..70eb5564
--- /dev/null
+++ b/libs/tornado/ioloop.py
@@ -0,0 +1,672 @@
+#!/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 absolute_import, division, 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
+
+ # Global lock for creating global IOLoop instance
+ _instance_lock = threading.Lock()
+
+ 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"):
+ with IOLoop._instance_lock:
+ if not hasattr(IOLoop, "_instance"):
+ # New instance after double check
+ 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).
+
+ Many applications will only use a single IOLoop that runs for the
+ entire lifetime of the process. In that case closing the IOLoop
+ is not necessary since everything will be cleaned up when the
+ process exits. `IOLoop.close` is provided mainly for scenarios
+ such as unit tests, which create and destroy a large number of
+ IOLoops.
+
+ An IOLoop must be completely stopped before it can be closed. This
+ means that `IOLoop.stop()` must be called *and* `IOLoop.start()` must
+ be allowed to return before attempting to call `IOLoop.close()`.
+ Therefore the call to `close` will usually appear just after
+ the call to `start` rather than near the call to `stop`.
+ """
+ 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.
+
+ Note that even after `stop` has been called, the IOLoop is not
+ completely stopped until `IOLoop.start` has also returned.
+ """
+ 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):
+ if fd in self._active:
+ raise IOError("fd %d already registered" % fd)
+ 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 fd in self.read_fds or fd in self.write_fds or fd in self.error_fds:
+ raise IOError("fd %d already registered" % fd)
+ 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
+ from tornado 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 100755
index 00000000..cfe6b1ce
--- /dev/null
+++ b/libs/tornado/iostream.py
@@ -0,0 +1,764 @@
+#!/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 absolute_import, division, with_statement
+
+import collections
+import errno
+import logging
+import os
+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.
+
+ When a stream is closed due to an error, the IOStream's `error`
+ attribute contains the exception object.
+
+ 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.error = None
+ 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."""
+ self._set_read_callback(callback)
+ self._read_regex = re.compile(regex)
+ self._try_inline_read()
+
+ def read_until(self, delimiter, callback):
+ """Call callback when we read the given delimiter."""
+ self._set_read_callback(callback)
+ self._read_delimiter = delimiter
+ self._try_inline_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.
+ """
+ self._set_read_callback(callback)
+ assert isinstance(num_bytes, (int, long))
+ self._read_bytes = num_bytes
+ self._streaming_callback = stack_context.wrap(streaming_callback)
+ self._try_inline_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.
+ """
+ self._set_read_callback(callback)
+ if self.closed():
+ self._run_callback(callback, self._consume(self._read_buffer_size))
+ self._read_callback = None
+ return
+ self._read_until_close = True
+ 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()
+ # We use bool(_write_buffer) as a proxy for write_buffer_size>0,
+ # so never put empty strings in the buffer.
+ if data:
+ # Break up large contiguous strings before inserting them in the
+ # write buffer, so we don't have to recopy the entire thing
+ # as we slice off pieces to send to the socket.
+ WRITE_BUFFER_CHUNK_SIZE = 128 * 1024
+ if len(data) > WRITE_BUFFER_CHUNK_SIZE:
+ for i in range(0, len(data), WRITE_BUFFER_CHUNK_SIZE):
+ self._write_buffer.append(data[i:i + WRITE_BUFFER_CHUNK_SIZE])
+ else:
+ 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 any(sys.exc_info()):
+ self.error = sys.exc_info()[1]
+ 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
+ self._maybe_run_close_callback()
+
+ def _maybe_run_close_callback(self):
+ if (self.socket is None and 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:
+ errno = self.socket.getsockopt(socket.SOL_SOCKET,
+ socket.SO_ERROR)
+ self.error = socket.error(errno, os.strerror(errno))
+ # 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):
+ try:
+ try:
+ # Pretend to have a pending callback so that an EOF in
+ # _read_to_buffer doesn't trigger an immediate close
+ # callback. At the end of this method we'll either
+ # estabilsh a real pending callback via
+ # _read_from_buffer or run the close callback.
+ #
+ # We need two try statements here so that
+ # pending_callbacks is decremented before the `except`
+ # clause below (which calls `close` and does need to
+ # trigger the callback)
+ self._pending_callbacks += 1
+ while True:
+ # 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.
+ if self._read_to_buffer() == 0:
+ break
+ finally:
+ self._pending_callbacks -= 1
+ except Exception:
+ logging.warning("error on read", exc_info=True)
+ self.close()
+ return
+ if self._read_from_buffer():
+ return
+ else:
+ self._maybe_run_close_callback()
+
+ def _set_read_callback(self, callback):
+ assert not self._read_callback, "Already reading"
+ self._read_callback = stack_context.wrap(callback)
+
+ def _try_inline_read(self):
+ """Attempt to complete the current read operation from buffered data.
+
+ If the read can be completed without blocking, schedules the
+ read callback on the next IOLoop iteration; otherwise starts
+ listening for reads on the socket.
+ """
+ # See if we've already got the data from a previous read
+ if self._read_from_buffer():
+ return
+ self._check_closed()
+ try:
+ # See comments in _handle_read about incrementing _pending_callbacks
+ self._pending_callbacks += 1
+ while True:
+ if self._read_to_buffer() == 0:
+ break
+ self._check_closed()
+ finally:
+ self._pending_callbacks -= 1
+ if self._read_from_buffer():
+ return
+ self._add_io_state(self.io_loop.READ)
+
+ 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._streaming_callback is not None and self._read_buffer_size:
+ bytes_to_consume = self._read_buffer_size
+ if self._read_bytes is not None:
+ bytes_to_consume = min(self._read_bytes, bytes_to_consume)
+ self._read_bytes -= bytes_to_consume
+ self._run_callback(self._streaming_callback,
+ self._consume(bytes_to_consume))
+ if self._read_bytes is not None and 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().
+ if self._read_buffer:
+ while True:
+ 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
+ if len(self._read_buffer) == 1:
+ break
+ _double_prefix(self._read_buffer)
+ elif self._read_regex is not None:
+ if self._read_buffer:
+ while True:
+ m = self._read_regex.search(self._read_buffer[0])
+ if m is not None:
+ 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
+ if len(self._read_buffer) == 1:
+ break
+ _double_prefix(self._read_buffer)
+ return False
+
+ def _handle_connect(self):
+ err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
+ if err != 0:
+ self.error = socket.error(err, os.strerror(err))
+ # 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:
+ self._maybe_run_close_callback()
+ 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] in (errno.ECONNABORTED, errno.ECONNRESET):
+ 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 _double_prefix(deque):
+ """Grow by doubling, but don't split the second chunk just because the
+ first one is small.
+ """
+ new_len = max(len(deque[0]) * 2,
+ (len(deque[0]) + len(deque[1])))
+ _merge_prefix(deque, new_len)
+
+
+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 100755
index 00000000..9f8ee7ea
--- /dev/null
+++ b/libs/tornado/locale.py
@@ -0,0 +1,509 @@
+#!/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.
+"""
+
+from __future__ import absolute_import, division, with_statement
+
+import csv
+import datetime
+import logging
+import os
+import re
+
+from tornado import escape
+
+_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
+ full_path = os.path.join(directory, path)
+ try:
+ # python 3: csv.reader requires a file open in text mode.
+ # Force utf8 to avoid dependence on $LANG environment variable.
+ f = open(full_path, "r", encoding="utf-8")
+ except TypeError:
+ # python 2: files return byte strings, which are decoded below.
+ # Once we drop python 2.5, this could use io.open instead
+ # on both 2 and 3.
+ f = open(full_path, "r")
+ _translations[locale] = {}
+ for i, row in enumerate(csv.reader(f)):
+ if not row or len(row) < 2:
+ continue
+ row = [escape.to_unicode(c).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.debug("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.debug("Supported locales: %s", sorted(_supported_locales))
+
+
+def get_supported_locales():
+ """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 __init__(self, code, translations):
+ try:
+ # python 2
+ self.ngettext = translations.ungettext
+ self.gettext = translations.ugettext
+ except AttributeError:
+ # python 3
+ self.ngettext = translations.ngettext
+ self.gettext = translations.gettext
+ # self.gettext must exist before __init__ is called, since it
+ # calls into self.translate
+ super(GettextLocale, self).__init__(code, translations)
+
+ def translate(self, message, plural_message=None, count=None):
+ if plural_message is not None:
+ assert count is not None
+ return self.ngettext(message, plural_message, count)
+ else:
+ return self.gettext(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 100755
index 00000000..ba0b27d2
--- /dev/null
+++ b/libs/tornado/netutil.py
@@ -0,0 +1,343 @@
+#!/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."""
+
+from __future__ import absolute_import, division, with_statement
+
+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
+
+ # Verify the SSL options. Otherwise we don't get errors until clients
+ # connect. This doesn't verify that the keys are legitimate, but
+ # the SSL module doesn't do that until there is a connected socket
+ # which seems like too much work
+ if self.ssl_options is not None:
+ # Only certfile is required: it can contain both keys
+ if 'certfile' not in self.ssl_options:
+ raise KeyError('missing key "certfile" in ssl_options')
+
+ if not os.path.exists(self.ssl_options['certfile']):
+ raise ValueError('certfile "%s" does not exist' %
+ self.ssl_options['certfile'])
+ if ('keyfile' in self.ssl_options and
+ not os.path.exists(self.ssl_options['keyfile'])):
+ raise ValueError('keyfile "%s" does not exist' %
+ self.ssl_options['keyfile'])
+
+ 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())
+ if os.name != 'nt':
+ 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 100755
index 00000000..a3d74d68
--- /dev/null
+++ b/libs/tornado/options.py
@@ -0,0 +1,481 @@
+#!/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.
+"""
+
+from __future__ import absolute_import, division, with_statement
+
+import datetime
+import logging
+import logging.handlers
+import re
+import sys
+import os
+import time
+import textwrap
+
+from tornado.escape import _unicode
+
+# For pretty log messages, if available
+try:
+ import curses
+except ImportError:
+ curses = None
+
+
+class Error(Exception):
+ """Exception raised by errors in the options module."""
+ pass
+
+
+class _Options(dict):
+ """A collection of options, a dictionary with object-like access.
+
+ Normally accessed via static functions in the `tornado.options` module,
+ which reference a global instance.
+ """
+ def __getattr__(self, name):
+ if isinstance(self.get(name), _Option):
+ return self[name].value()
+ raise AttributeError("Unrecognized option %r" % name)
+
+ def __setattr__(self, name, value):
+ if isinstance(self.get(name), _Option):
+ return self[name].set(value)
+ raise AttributeError("Unrecognized option %r" % name)
+
+ def define(self, name, default=None, type=None, help=None, metavar=None,
+ multiple=False, group=None):
+ if name in self:
+ raise Error("Option %r already defined in %s", name,
+ self[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
+ self[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(self, args=None):
+ 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 self:
+ print_help()
+ raise Error('Unrecognized command line option: %r' % name)
+ option = self[name]
+ if not equals:
+ if option.type == bool:
+ value = "true"
+ else:
+ raise Error('Option %r requires a value' % name)
+ option.parse(value)
+ if self.help:
+ print_help()
+ sys.exit(0)
+
+ # Set up log level and pretty console logging by default
+ if self.logging != 'none':
+ logging.getLogger().setLevel(getattr(logging, self.logging.upper()))
+ enable_pretty_logging()
+
+ return remaining
+
+ def parse_config_file(self, path):
+ config = {}
+ execfile(path, config, config)
+ for name in config:
+ if name in self:
+ self[name].set(config[name])
+
+ def print_help(self, file=sys.stdout):
+ """Prints all the command line options to stdout."""
+ print >> file, "Usage: %s [OPTIONS]" % sys.argv[0]
+ print >> file, "\nOptions:\n"
+ by_group = {}
+ for option in self.itervalues():
+ by_group.setdefault(option.group_name, []).append(option)
+
+ for filename, o in sorted(by_group.items()):
+ if filename:
+ print >> file, "\n%s options:\n" % os.path.normpath(filename)
+ o.sort(key=lambda option: option.name)
+ for option in o:
+ prefix = option.name
+ if option.metavar:
+ prefix += "=" + option.metavar
+ description = option.help or ""
+ if option.default is not None and option.default != '':
+ description += " (default %s)" % option.default
+ lines = textwrap.wrap(description, 79 - 35)
+ if len(prefix) > 30 or len(lines) == 0:
+ lines.insert(0, '')
+ print >> file, " --%-30s %s" % (prefix, lines[0])
+ for line in lines[1:]:
+ print >> file, "%-34s %s" % (' ', line)
+ print >> file
+
+
+class _Option(object):
+ def __init__(self, name, default=None, type=basestring, 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,
+ basestring: self._parse_string,
+ }.get(self.type, self.type)
+ if self.multiple:
+ 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 (%s given)" %
+ (self.name, self.type.__name__, type(value)))
+ 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)
+
+
+options = _Options()
+"""Global options dictionary.
+
+Supports both attribute-style and dict-style access.
+"""
+
+
+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.
+ """
+ return options.define(name, default=default, type=type, help=help,
+ metavar=metavar, multiple=multiple, group=group)
+
+
+def parse_command_line(args=None):
+ """Parses all options given on the command line (defaults to sys.argv).
+
+ Note that args[0] is ignored since it is the program name in sys.argv.
+
+ We return a list of all arguments that are not parsed as options.
+ """
+ return options.parse_command_line(args)
+
+
+def parse_config_file(path):
+ """Parses and loads the Python config file at the given path."""
+ return options.parse_config_file(path)
+
+
+def print_help(file=sys.stdout):
+ """Prints all the command line options to stdout."""
+ return options.print_help(file)
+
+
+def enable_pretty_logging(options=options):
+ """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__)
+ assert isinstance(record.message, basestring) # guaranteed by logging
+ 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)
+
+ # Encoding notes: The logging module prefers to work with character
+ # strings, but only enforces that log messages are instances of
+ # basestring. In python 2, non-ascii bytestrings will make
+ # their way through the logging framework until they blow up with
+ # an unhelpful decoding error (with this formatter it happens
+ # when we attach the prefix, but there are other opportunities for
+ # exceptions further along in the framework).
+ #
+ # If a byte string makes it this far, convert it to unicode to
+ # ensure it will make it out to the logs. Use repr() as a fallback
+ # to ensure that all byte strings can be converted successfully,
+ # but don't do it by default so we don't add extra quotes to ascii
+ # bytestrings. This is a bit of a hacky place to do this, but
+ # it's worth it since the encoding errors that would otherwise
+ # result are so useless (and tornado is fond of using utf8-encoded
+ # byte strings whereever possible).
+ try:
+ message = _unicode(record.message)
+ except UnicodeDecodeError:
+ message = repr(record.message)
+
+ formatted = prefix + " " + 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 ")
+
+
+# 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 100755
index 00000000..e69de29b
diff --git a/libs/tornado/platform/auto.py b/libs/tornado/platform/auto.py
new file mode 100755
index 00000000..7bfec116
--- /dev/null
+++ b/libs/tornado/platform/auto.py
@@ -0,0 +1,34 @@
+#!/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
+"""
+
+from __future__ import absolute_import, division, with_statement
+
+import os
+
+if os.name == 'nt':
+ from tornado.platform.common import Waker
+ from tornado.platform.windows import set_close_exec
+else:
+ from tornado.platform.posix import set_close_exec, Waker
diff --git a/libs/tornado/platform/common.py b/libs/tornado/platform/common.py
new file mode 100755
index 00000000..176ce2e5
--- /dev/null
+++ b/libs/tornado/platform/common.py
@@ -0,0 +1,89 @@
+"""Lowest-common-denominator implementations of platform functionality."""
+from __future__ import absolute_import, division, with_statement
+
+import errno
+import socket
+
+from tornado.platform import interface
+from tornado.util import b
+
+
+class Waker(interface.Waker):
+ """Create an OS independent asynchronous pipe.
+
+ For use on platforms that don't have os.pipe() (or where pipes cannot
+ be passed to select()), but do have sockets. This includes Windows
+ and Jython.
+ """
+ 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))
+ a.listen(1)
+ connect_address = a.getsockname() # assigned (host, port) pair
+ try:
+ self.writer.connect(connect_address)
+ break # success
+ except socket.error, detail:
+ if (not hasattr(errno, 'WSAEADDRINUSE') or
+ 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, socket.error):
+ pass
+
+ def consume(self):
+ try:
+ while True:
+ result = self.reader.recv(1024)
+ if not result:
+ break
+ except (IOError, socket.error):
+ pass
+
+ def close(self):
+ self.reader.close()
+ self.writer.close()
diff --git a/libs/tornado/platform/interface.py b/libs/tornado/platform/interface.py
new file mode 100755
index 00000000..21e72cd9
--- /dev/null
+++ b/libs/tornado/platform/interface.py
@@ -0,0 +1,59 @@
+#!/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`.
+"""
+
+from __future__ import absolute_import, division, with_statement
+
+
+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 100755
index 00000000..8d674c0e
--- /dev/null
+++ b/libs/tornado/platform/posix.py
@@ -0,0 +1,68 @@
+#!/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."""
+
+from __future__ import absolute_import, division, with_statement
+
+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 100755
index 00000000..6474a478
--- /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, 11.1.0, and 12.0.0
+"""
+
+from __future__ import absolute_import, division, with_statement
+
+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 implementer
+
+import tornado
+import tornado.ioloop
+from tornado.stack_context import NullContext
+from tornado.ioloop import IOLoop
+
+
+class TornadoDelayedCall(object):
+ """DelayedCall object for Tornado."""
+ 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
+# Fake class decorator for python 2.5 compatibility
+TornadoDelayedCall = implementer(IDelayedCall)(TornadoDelayedCall)
+
+
+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`.
+ """
+ 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")
+TornadoReactor = implementer(IReactorTime, IReactorFDSet)(TornadoReactor)
+
+
+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 100755
index 00000000..80c8a6e2
--- /dev/null
+++ b/libs/tornado/platform/windows.py
@@ -0,0 +1,20 @@
+# NOTE: win32 support is currently experimental, and not recommended
+# for production use.
+
+
+from __future__ import absolute_import, division, with_statement
+import ctypes
+import ctypes.wintypes
+
+# 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()
diff --git a/libs/tornado/process.py b/libs/tornado/process.py
new file mode 100755
index 00000000..28a61bcd
--- /dev/null
+++ b/libs/tornado/process.py
@@ -0,0 +1,158 @@
+#!/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."""
+
+from __future__ import absolute_import, division, with_statement
+
+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 100755
index 00000000..c6e8a3a7
--- /dev/null
+++ b/libs/tornado/simple_httpclient.py
@@ -0,0 +1,534 @@
+#!/usr/bin/env python
+from __future__ import absolute_import, division, 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, GzipDecompressor
+
+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
+
+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)
+ # We're going to modify this (to add Host, Accept-Encoding, etc),
+ # so make sure we don't modify the caller's object. This is also
+ # where normal dicts get converted to HTTPHeaders objects.
+ 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", "PATCH", "OPTIONS"])
+
+ 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]
+ parsed_hostname = host # save final parsed host for _on_connect
+ 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,
+ stack_context.wrap(self._on_timeout))
+ self.stream.set_close_callback(self._on_close)
+ self.stream.connect(sockaddr,
+ functools.partial(self._on_connect, parsed,
+ parsed_hostname))
+
+ def _on_timeout(self):
+ self._timeout = None
+ if self.final_callback is not None:
+ raise HTTPError(599, "Timeout")
+
+ def _on_connect(self, parsed, parsed_hostname):
+ 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,
+ stack_context.wrap(self._on_timeout))
+ if (self.request.validate_cert and
+ isinstance(self.stream, SSLIOStream)):
+ match_hostname(self.stream.socket.getpeercert(),
+ # ipv6 addresses are broken (in
+ # parsed.hostname) until 2.7, here is
+ # correctly parsed value calculated in
+ # __init__
+ 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 "Connection" not in self.request.headers:
+ self.request.headers["Connection"] = "close"
+ if "Host" not in self.request.headers:
+ if '@' in parsed.netloc:
+ self.request.headers["Host"] = parsed.netloc.rpartition('@')[-1]
+ else:
+ 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", "PATCH", "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,
+ ))
+ if hasattr(self, "stream"):
+ self.stream.close()
+
+ def _on_close(self):
+ if self.final_callback is not None:
+ raise 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
+ if ("Transfer-Encoding" in self.headers or
+ content_length not in (None, 0)):
+ raise ValueError("Response with code %d should not have body" %
+ self.code)
+ self._on_body(b(""))
+ return
+
+ if (self.request.use_gzip and
+ self.headers.get("Content-Encoding") == "gzip"):
+ self._decompressor = GzipDecompressor()
+ 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) +
+ self._decompressor.flush())
+ 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:
+ if self._decompressor is not None:
+ tail = self._decompressor.flush()
+ if tail:
+ # I believe the tail will always be empty (i.e.
+ # decompress will return all it can). The purpose
+ # of the flush call is to detect errors such
+ # as truncated input. But in case it ever returns
+ # anything, treat it as an extra chunk
+ if self.request.streaming_callback is not None:
+ self.request.streaming_callback(tail)
+ else:
+ self.chunks.append(tail)
+ # 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 100755
index 00000000..afce681b
--- /dev/null
+++ b/libs/tornado/stack_context.py
@@ -0,0 +1,273 @@
+#!/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 absolute_import, division, with_statement
+
+import contextlib
+import functools
+import itertools
+import operator
+import sys
+import threading
+
+from tornado.util import raise_exc_info
+
+
+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):
+
+ The result of ``with StackContext() as cb:`` is a deactivation
+ callback. Run this callback when the StackContext is no longer
+ needed to ensure that it is not propagated any further (note that
+ deactivating a context does not affect any instances of that
+ context that are currently pending). This is an advanced feature
+ and not necessary in most applications.
+ '''
+ def __init__(self, context_factory, _active_cell=None):
+ self.context_factory = context_factory
+ self.active_cell = _active_cell or [True]
+
+ # 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, active_cell) tuples
+ _state.contexts = (self.old_contexts +
+ ((StackContext, self.context_factory, self.active_cell),))
+ try:
+ self.context = self.context_factory()
+ self.context.__enter__()
+ except Exception:
+ _state.contexts = self.old_contexts
+ raise
+ return lambda: operator.setitem(self.active_cell, 0, False)
+
+ 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, _active_cell=None):
+ self.exception_handler = exception_handler
+ self.active_cell = _active_cell or [True]
+
+ def __enter__(self):
+ self.old_contexts = _state.contexts
+ _state.contexts = (self.old_contexts +
+ ((ExceptionStackContext, self.exception_handler,
+ self.active_cell),))
+ return lambda: operator.setitem(self.active_cell, 0, False)
+
+ 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(*args, **kwargs):
+ callback, contexts, args = args[0], args[1], args[2:]
+
+ if contexts is _state.contexts or not contexts:
+ callback(*args, **kwargs)
+ return
+ if not _state.contexts:
+ new_contexts = [cls(arg, active_cell)
+ for (cls, arg, active_cell) in contexts
+ if active_cell[0]]
+ # 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, active_cell)
+ for (cls, arg, active_cell) in contexts
+ if active_cell[0]])
+ else:
+ new_contexts = [cls(arg, active_cell)
+ for (cls, arg, active_cell) in contexts[len(_state.contexts):]
+ if active_cell[0]]
+ 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_info(exc)
diff --git a/libs/tornado/template.py b/libs/tornado/template.py
new file mode 100755
index 00000000..13eb7808
--- /dev/null
+++ b/libs/tornado/template.py
@@ -0,0 +1,854 @@
+#!/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 %}
+
+ {% 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 %}
+
+ Note that as an implementation detail apply blocks are implemented
+ as nested functions and thus may interact strangely with variables
+ set via ``{% set %}``, or the use of ``{% break %}`` or ``{% continue %}``
+ within loops.
+
+``{% 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. ``{% break %}`` and
+ ``{% continue %}`` may be used inside the loop.
+
+``{% 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 %}...{% else %}...{% end %}``
+ Same as the python ``try`` statement.
+
+``{% while *condition* %}... {% end %}``
+ Same as the python ``while`` statement. ``{% break %}`` and
+ ``{% continue %}`` may be used inside the loop.
+"""
+
+from __future__ import absolute_import, division, 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, "rb")
+ 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, in_loop=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", "try"]),
+ "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
+ if operator in ("for", "while"):
+ block_body = _parse(reader, template, operator, operator)
+ elif operator == "apply":
+ # apply creates a nested function so syntactically it's not
+ # in the loop.
+ block_body = _parse(reader, template, operator, None)
+ else:
+ block_body = _parse(reader, template, operator, in_loop)
+
+ 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
+
+ elif operator in ("break", "continue"):
+ if not in_loop:
+ raise ParseError("%s outside %s block" % (operator, set(["for", "while"])))
+ body.chunks.append(_Statement(contents, line))
+ continue
+
+ else:
+ raise ParseError("unknown operator: %r" % operator)
diff --git a/libs/tornado/testing.py b/libs/tornado/testing.py
new file mode 100755
index 00000000..42fec8e7
--- /dev/null
+++ b/libs/tornado/testing.py
@@ -0,0 +1,446 @@
+#!/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 absolute_import, division, with_statement
+
+from cStringIO import StringIO
+try:
+ from tornado.httpclient import AsyncHTTPClient
+ from tornado.httpserver import HTTPServer
+ from tornado.simple_httpclient import SimpleAsyncHTTPClient
+ 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
+ SimpleAsyncHTTPClient = None
+from tornado.stack_context import StackContext, NullContext
+from tornado.util import raise_exc_info
+import contextlib
+import logging
+import os
+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
+ self.__timeout = 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 __rethrow(self):
+ if self.__failure is not None:
+ failure = self.__failure
+ self.__failure = None
+ raise_exc_info(failure)
+
+ def run(self, result=None):
+ with StackContext(self._stack_context):
+ super(AsyncTestCase, self).run(result)
+ # In case an exception escaped super.run or the StackContext caught
+ # an exception when there wasn't a wait() to re-raise it, do so here.
+ self.__rethrow()
+
+ 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 %s seconds' %
+ timeout)
+ except Exception:
+ self.__failure = sys.exc_info()
+ self.stop()
+ if self.__timeout is not None:
+ self.io_loop.remove_timeout(self.__timeout)
+ self.__timeout = 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
+ self.__rethrow()
+ 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 = self.get_http_client()
+ self._app = self.get_app()
+ self.http_server = self.get_http_server()
+ self.http_server.listen(self.get_http_port(), address="127.0.0.1")
+
+ def get_http_client(self):
+ return AsyncHTTPClient(io_loop=self.io_loop)
+
+ def get_http_server(self):
+ return HTTPServer(self._app, io_loop=self.io_loop,
+ **self.get_httpserver_options())
+
+
+ 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 the server.
+ """
+ return {}
+
+ def get_http_port(self):
+ """Returns the port used by the server.
+
+ A new port is chosen for each test.
+ """
+ if self.__port is None:
+ self.__port = get_unused_port()
+ return self.__port
+
+ def get_protocol(self):
+ return 'http'
+
+ def get_url(self, path):
+ """Returns an absolute url for the given path on the test server."""
+ return '%s://localhost:%s%s' % (self.get_protocol(),
+ self.get_http_port(), path)
+
+ def tearDown(self):
+ self.http_server.stop()
+ self.http_client.close()
+ super(AsyncHTTPTestCase, self).tearDown()
+
+
+class AsyncHTTPSTestCase(AsyncHTTPTestCase):
+ """A test case that starts an HTTPS server.
+
+ Interface is generally the same as `AsyncHTTPTestCase`.
+ """
+ def get_http_client(self):
+ # Some versions of libcurl have deadlock bugs with ssl,
+ # so always run these tests with SimpleAsyncHTTPClient.
+ return SimpleAsyncHTTPClient(io_loop=self.io_loop, force_instance=True)
+
+ def get_httpserver_options(self):
+ return dict(ssl_options=self.get_ssl_options())
+
+ def get_ssl_options(self):
+ """May be overridden by subclasses to select SSL options.
+
+ By default includes a self-signed testing certificate.
+ """
+ # Testing keys were generated with:
+ # openssl req -new -keyout tornado/test/test.key -out tornado/test/test.crt -nodes -days 3650 -x509
+ module_dir = os.path.dirname(__file__)
+ return dict(
+ certfile=os.path.join(module_dir, 'test', 'test.crt'),
+ keyfile=os.path.join(module_dir, 'test', 'test.key'))
+
+ def get_protocol(self):
+ return 'https'
+
+ def fetch(self, path, **kwargs):
+ return AsyncHTTPTestCase.fetch(self, path, validate_cert=False,
+ **kwargs)
+
+
+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(**kwargs):
+ """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
+ python -m tornado.test.runtests
+ # Runs one test
+ python -m tornado.test.runtests tornado.test.stack_context_test
+
+ Additional keyword arguments passed through to ``unittest.main()``.
+ For example, use ``tornado.testing.main(verbosity=2)``
+ to show many test details as they are run.
+ See http://docs.python.org/library/unittest.html#unittest.main
+ for full argument list.
+ """
+ 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, **kwargs)
+ else:
+ unittest.main(defaultTest="all", argv=argv, **kwargs)
+ 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 100755
index 00000000..80cab898
--- /dev/null
+++ b/libs/tornado/util.py
@@ -0,0 +1,101 @@
+"""Miscellaneous utility functions."""
+
+from __future__ import absolute_import, division, with_statement
+
+import zlib
+
+
+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
+
+
+class GzipDecompressor(object):
+ """Streaming gzip decompressor.
+
+ The interface is like that of `zlib.decompressobj` (without the
+ optional arguments, but it understands gzip headers and checksums.
+ """
+ def __init__(self):
+ # Magic parameter makes zlib module understand gzip header
+ # http://stackoverflow.com/questions/1838699/how-can-i-decompress-a-gzip-stream-with-zlib
+ # This works on cpython and pypy, but not jython.
+ self.decompressobj = zlib.decompressobj(16 + zlib.MAX_WBITS)
+
+ def decompress(self, value):
+ """Decompress a chunk, returning newly-available data.
+
+ Some data may be buffered for later processing; `flush` must
+ be called when there is no more input data to ensure that
+ all data was processed.
+ """
+ return self.decompressobj.decompress(value)
+
+ def flush(self):
+ """Return any remaining buffered data not yet returned by decompress.
+
+ Also checks for errors such as truncated input.
+ No other methods may be called on this object after `flush`.
+ """
+ return self.decompressobj.flush()
+
+
+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 raise_exc_info(exc_info):
+ """Re-raise an exception (with original traceback) from an exc_info tuple.
+
+ The argument is a ``(type, value, traceback)`` tuple as returned by
+ `sys.exc_info`.
+ """
+ # 2to3 isn't smart enough to convert three-argument raise
+ # statements correctly in some cases.
+ if isinstance(exc_info[1], exc_info[0]):
+ raise exc_info[1], None, exc_info[2]
+ # After 2to3: raise exc_info[1].with_traceback(exc_info[2])
+ else:
+ # I think this branch is only taken for string exceptions,
+ # which were removed in Python 2.6.
+ raise exc_info[0], exc_info[1], exc_info[2]
+ # After 2to3: raise exc_info[0](exc_info[1]).with_traceback(exc_info[2])
+
+
+def doctests():
+ import doctest
+ return doctest.DocTestSuite()
diff --git a/libs/tornado/web.py b/libs/tornado/web.py
new file mode 100755
index 00000000..99c6858d
--- /dev/null
+++ b/libs/tornado/web.py
@@ -0,0 +1,2060 @@
+#!/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 absolute_import, division, 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, raise_exc_info
+
+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", "PATCH", "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 getattr(self.request, "connection", None):
+ 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 patch(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 clear_header(self, name):
+ """Clears an outgoing header, undoing a previous `set_header` call.
+
+ Note that this method does not apply to multi-valued headers
+ set by `add_header`.
+ """
+ if name in self._headers:
+ del self._headers[name]
+
+ 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_cookie"):
+ self._new_cookie = Cookie.SimpleCookie()
+ if name in self._new_cookie:
+ del self._new_cookie[name]
+ self._new_cookie[name] = value
+ morsel = self._new_cookie[name]
+ if domain:
+ morsel["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())
+ morsel["expires"] = email.utils.formatdate(
+ timestamp, localtime=False, usegmt=True)
+ if path:
+ morsel["path"] = path
+ for k, v in kwargs.iteritems():
+ if k == 'max_age':
+ k = 'max-age'
+ morsel[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`.
+
+ Secure cookies may contain arbitrary byte values, not just unicode
+ strings (unlike regular cookies)
+ """
+ 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.
+
+ The decoded cookie value is returned as a byte string (unlike
+ `get_cookie`).
+ """
+ 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('