Copy of tornado from another project, mainly for templates.
diff --git a/tornado/__init__.py b/tornado/__init__.py
new file mode 100644
index 0000000..f13041e
--- /dev/null
+++ b/tornado/__init__.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""The Tornado web server and tools."""
+
+# version is a human-readable version number.
+
+# version_info is a four-tuple for programmatic comparison. The first
+# three numbers are the components of the version number.  The fourth
+# is zero for an official release, positive for a development branch,
+# or negative for a release candidate (after the base version number
+# has been incremented)
+version = "2.2"
+version_info = (2, 2, 0, 0)
diff --git a/tornado/auth.py b/tornado/auth.py
new file mode 100644
index 0000000..a716210
--- /dev/null
+++ b/tornado/auth.py
@@ -0,0 +1,1134 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Implementations of various third-party authentication schemes.
+
+All the classes in this file are class Mixins designed to be used with
+web.py RequestHandler classes. The primary methods for each service are
+authenticate_redirect(), authorize_redirect(), and get_authenticated_user().
+The former should be called to redirect the user to, e.g., the OpenID
+authentication page on the third party service, and the latter should
+be called upon return to get the user data from the data returned by
+the third party service.
+
+They all take slightly different arguments due to the fact all these
+services implement authentication and authorization slightly differently.
+See the individual service classes below for complete documentation.
+
+Example usage for Google OpenID::
+
+    class GoogleHandler(tornado.web.RequestHandler, tornado.auth.GoogleMixin):
+        @tornado.web.asynchronous
+        def get(self):
+            if self.get_argument("openid.mode", None):
+                self.get_authenticated_user(self.async_callback(self._on_auth))
+                return
+            self.authenticate_redirect()
+
+        def _on_auth(self, user):
+            if not user:
+                raise tornado.web.HTTPError(500, "Google auth failed")
+            # Save the user with, e.g., set_secure_cookie()
+"""
+
+import base64
+import binascii
+import hashlib
+import hmac
+import logging
+import time
+import urllib
+import urlparse
+import uuid
+
+from tornado import httpclient
+from tornado import escape
+from tornado.httputil import url_concat
+from tornado.util import bytes_type, b
+
+class OpenIdMixin(object):
+    """Abstract implementation of OpenID and Attribute Exchange.
+
+    See GoogleMixin below for example implementations.
+    """
+    def authenticate_redirect(self, callback_uri=None,
+                              ax_attrs=["name","email","language","username"]):
+        """Returns the authentication URL for this service.
+
+        After authentication, the service will redirect back to the given
+        callback URI.
+
+        We request the given attributes for the authenticated user by
+        default (name, email, language, and username). If you don't need
+        all those attributes for your app, you can request fewer with
+        the ax_attrs keyword argument.
+        """
+        callback_uri = callback_uri or self.request.uri
+        args = self._openid_args(callback_uri, ax_attrs=ax_attrs)
+        self.redirect(self._OPENID_ENDPOINT + "?" + urllib.urlencode(args))
+
+    def get_authenticated_user(self, callback, http_client=None):
+        """Fetches the authenticated user data upon redirect.
+
+        This method should be called by the handler that receives the
+        redirect from the authenticate_redirect() or authorize_redirect()
+        methods.
+        """
+        # Verify the OpenID response via direct request to the OP
+        args = dict((k, v[-1]) for k, v in self.request.arguments.iteritems())
+        args["openid.mode"] = u"check_authentication"
+        url = self._OPENID_ENDPOINT
+        if http_client is None: http_client = httpclient.AsyncHTTPClient()
+        http_client.fetch(url, self.async_callback(
+            self._on_authentication_verified, callback),
+            method="POST", body=urllib.urlencode(args))
+
+    def _openid_args(self, callback_uri, ax_attrs=[], oauth_scope=None):
+        url = urlparse.urljoin(self.request.full_url(), callback_uri)
+        args = {
+            "openid.ns": "http://specs.openid.net/auth/2.0",
+            "openid.claimed_id":
+                "http://specs.openid.net/auth/2.0/identifier_select",
+            "openid.identity":
+                "http://specs.openid.net/auth/2.0/identifier_select",
+            "openid.return_to": url,
+            "openid.realm": urlparse.urljoin(url, '/'),
+            "openid.mode": "checkid_setup",
+        }
+        if ax_attrs:
+            args.update({
+                "openid.ns.ax": "http://openid.net/srv/ax/1.0",
+                "openid.ax.mode": "fetch_request",
+            })
+            ax_attrs = set(ax_attrs)
+            required = []
+            if "name" in ax_attrs:
+                ax_attrs -= set(["name", "firstname", "fullname", "lastname"])
+                required += ["firstname", "fullname", "lastname"]
+                args.update({
+                    "openid.ax.type.firstname":
+                        "http://axschema.org/namePerson/first",
+                    "openid.ax.type.fullname":
+                        "http://axschema.org/namePerson",
+                    "openid.ax.type.lastname":
+                        "http://axschema.org/namePerson/last",
+                })
+            known_attrs = {
+                "email": "http://axschema.org/contact/email",
+                "language": "http://axschema.org/pref/language",
+                "username": "http://axschema.org/namePerson/friendly",
+            }
+            for name in ax_attrs:
+                args["openid.ax.type." + name] = known_attrs[name]
+                required.append(name)
+            args["openid.ax.required"] = ",".join(required)
+        if oauth_scope:
+            args.update({
+                "openid.ns.oauth":
+                    "http://specs.openid.net/extensions/oauth/1.0",
+                "openid.oauth.consumer": self.request.host.split(":")[0],
+                "openid.oauth.scope": oauth_scope,
+            })
+        return args
+
+    def _on_authentication_verified(self, callback, response):
+        if response.error or b("is_valid:true") not in response.body:
+            logging.warning("Invalid OpenID response: %s", response.error or
+                            response.body)
+            callback(None)
+            return
+
+        # Make sure we got back at least an email from attribute exchange
+        ax_ns = None
+        for name in self.request.arguments.iterkeys():
+            if name.startswith("openid.ns.") and \
+               self.get_argument(name) == u"http://openid.net/srv/ax/1.0":
+                ax_ns = name[10:]
+                break
+        def get_ax_arg(uri):
+            if not ax_ns: return u""
+            prefix = "openid." + ax_ns + ".type."
+            ax_name = None
+            for name in self.request.arguments.iterkeys():
+                if self.get_argument(name) == uri and name.startswith(prefix):
+                    part = name[len(prefix):]
+                    ax_name = "openid." + ax_ns + ".value." + part
+                    break
+            if not ax_name: return u""
+            return self.get_argument(ax_name, u"")
+
+        email = get_ax_arg("http://axschema.org/contact/email")
+        name = get_ax_arg("http://axschema.org/namePerson")
+        first_name = get_ax_arg("http://axschema.org/namePerson/first")
+        last_name = get_ax_arg("http://axschema.org/namePerson/last")
+        username = get_ax_arg("http://axschema.org/namePerson/friendly")
+        locale = get_ax_arg("http://axschema.org/pref/language").lower()
+        user = dict()
+        name_parts = []
+        if first_name:
+            user["first_name"] = first_name
+            name_parts.append(first_name)
+        if last_name:
+            user["last_name"] = last_name
+            name_parts.append(last_name)
+        if name:
+            user["name"] = name
+        elif name_parts:
+            user["name"] = u" ".join(name_parts)
+        elif email:
+            user["name"] = email.split("@")[0]
+        if email: user["email"] = email
+        if locale: user["locale"] = locale
+        if username: user["username"] = username
+        callback(user)
+
+
+class OAuthMixin(object):
+    """Abstract implementation of OAuth.
+
+    See TwitterMixin and FriendFeedMixin below for example implementations.
+    """
+
+    def authorize_redirect(self, callback_uri=None, extra_params=None,
+                           http_client=None):
+        """Redirects the user to obtain OAuth authorization for this service.
+
+        Twitter and FriendFeed both require that you register a Callback
+        URL with your application. You should call this method to log the
+        user in, and then call get_authenticated_user() in the handler
+        you registered as your Callback URL to complete the authorization
+        process.
+
+        This method sets a cookie called _oauth_request_token which is
+        subsequently used (and cleared) in get_authenticated_user for
+        security purposes.
+        """
+        if callback_uri and getattr(self, "_OAUTH_NO_CALLBACKS", False):
+            raise Exception("This service does not support oauth_callback")
+        if http_client is None:
+            http_client = httpclient.AsyncHTTPClient()
+        if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
+            http_client.fetch(
+                self._oauth_request_token_url(callback_uri=callback_uri,
+                                              extra_params=extra_params),
+                self.async_callback(
+                    self._on_request_token,
+                    self._OAUTH_AUTHORIZE_URL,
+                callback_uri))
+        else:
+            http_client.fetch(
+                self._oauth_request_token_url(),
+                self.async_callback(
+                    self._on_request_token, self._OAUTH_AUTHORIZE_URL,
+                    callback_uri))
+
+
+    def get_authenticated_user(self, callback, http_client=None):
+        """Gets the OAuth authorized user and access token on callback.
+
+        This method should be called from the handler for your registered
+        OAuth Callback URL to complete the registration process. We call
+        callback with the authenticated user, which in addition to standard
+        attributes like 'name' includes the 'access_key' attribute, which
+        contains the OAuth access you can use to make authorized requests
+        to this service on behalf of the user.
+
+        """
+        request_key = escape.utf8(self.get_argument("oauth_token"))
+        oauth_verifier = self.get_argument("oauth_verifier", None)
+        request_cookie = self.get_cookie("_oauth_request_token")
+        if not request_cookie:
+            logging.warning("Missing OAuth request token cookie")
+            callback(None)
+            return
+        self.clear_cookie("_oauth_request_token")
+        cookie_key, cookie_secret = [base64.b64decode(escape.utf8(i)) for i in request_cookie.split("|")]
+        if cookie_key != request_key:
+            logging.info((cookie_key, request_key, request_cookie))
+            logging.warning("Request token does not match cookie")
+            callback(None)
+            return
+        token = dict(key=cookie_key, secret=cookie_secret)
+        if oauth_verifier:
+            token["verifier"] = oauth_verifier
+        if http_client is None:
+            http_client = httpclient.AsyncHTTPClient()
+        http_client.fetch(self._oauth_access_token_url(token),
+                          self.async_callback(self._on_access_token, callback))
+
+    def _oauth_request_token_url(self, callback_uri= None, extra_params=None):
+        consumer_token = self._oauth_consumer_token()
+        url = self._OAUTH_REQUEST_TOKEN_URL
+        args = dict(
+            oauth_consumer_key=consumer_token["key"],
+            oauth_signature_method="HMAC-SHA1",
+            oauth_timestamp=str(int(time.time())),
+            oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes),
+            oauth_version=getattr(self, "_OAUTH_VERSION", "1.0a"),
+        )
+        if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
+            if callback_uri:
+                args["oauth_callback"] = urlparse.urljoin(
+                    self.request.full_url(), callback_uri)
+            if extra_params: args.update(extra_params)
+            signature = _oauth10a_signature(consumer_token, "GET", url, args)
+        else:
+            signature = _oauth_signature(consumer_token, "GET", url, args)
+
+        args["oauth_signature"] = signature
+        return url + "?" + urllib.urlencode(args)
+
+    def _on_request_token(self, authorize_url, callback_uri, response):
+        if response.error:
+            raise Exception("Could not get request token")
+        request_token = _oauth_parse_response(response.body)
+        data = (base64.b64encode(request_token["key"]) + b("|") +
+                base64.b64encode(request_token["secret"]))
+        self.set_cookie("_oauth_request_token", data)
+        args = dict(oauth_token=request_token["key"])
+        if callback_uri:
+            args["oauth_callback"] = urlparse.urljoin(
+                self.request.full_url(), callback_uri)
+        self.redirect(authorize_url + "?" + urllib.urlencode(args))
+
+    def _oauth_access_token_url(self, request_token):
+        consumer_token = self._oauth_consumer_token()
+        url = self._OAUTH_ACCESS_TOKEN_URL
+        args = dict(
+            oauth_consumer_key=consumer_token["key"],
+            oauth_token=request_token["key"],
+            oauth_signature_method="HMAC-SHA1",
+            oauth_timestamp=str(int(time.time())),
+            oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes),
+            oauth_version=getattr(self, "_OAUTH_VERSION", "1.0a"),
+        )
+        if "verifier" in request_token:
+          args["oauth_verifier"]=request_token["verifier"]
+
+        if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
+            signature = _oauth10a_signature(consumer_token, "GET", url, args,
+                                            request_token)
+        else:
+            signature = _oauth_signature(consumer_token, "GET", url, args,
+                                         request_token)
+
+        args["oauth_signature"] = signature
+        return url + "?" + urllib.urlencode(args)
+
+    def _on_access_token(self, callback, response):
+        if response.error:
+            logging.warning("Could not fetch access token")
+            callback(None)
+            return
+
+        access_token = _oauth_parse_response(response.body)
+        self._oauth_get_user(access_token, self.async_callback(
+             self._on_oauth_get_user, access_token, callback))
+
+    def _oauth_get_user(self, access_token, callback):
+        raise NotImplementedError()
+
+    def _on_oauth_get_user(self, access_token, callback, user):
+        if not user:
+            callback(None)
+            return
+        user["access_token"] = access_token
+        callback(user)
+
+    def _oauth_request_parameters(self, url, access_token, parameters={},
+                                  method="GET"):
+        """Returns the OAuth parameters as a dict for the given request.
+
+        parameters should include all POST arguments and query string arguments
+        that will be sent with the request.
+        """
+        consumer_token = self._oauth_consumer_token()
+        base_args = dict(
+            oauth_consumer_key=consumer_token["key"],
+            oauth_token=access_token["key"],
+            oauth_signature_method="HMAC-SHA1",
+            oauth_timestamp=str(int(time.time())),
+            oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes),
+            oauth_version=getattr(self, "_OAUTH_VERSION", "1.0a"),
+        )
+        args = {}
+        args.update(base_args)
+        args.update(parameters)
+        if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
+            signature = _oauth10a_signature(consumer_token, method, url, args,
+                                         access_token)
+        else:
+            signature = _oauth_signature(consumer_token, method, url, args,
+                                         access_token)
+        base_args["oauth_signature"] = signature
+        return base_args
+
+class OAuth2Mixin(object):
+    """Abstract implementation of OAuth v 2."""
+
+    def authorize_redirect(self, redirect_uri=None, client_id=None,
+                           client_secret=None, extra_params=None ):
+        """Redirects the user to obtain OAuth authorization for this service.
+
+        Some providers require that you register a Callback
+        URL with your application. You should call this method to log the
+        user in, and then call get_authenticated_user() in the handler
+        you registered as your Callback URL to complete the authorization
+        process.
+        """
+        args = {
+          "redirect_uri": redirect_uri,
+          "client_id": client_id
+        }
+        if extra_params: args.update(extra_params)
+        self.redirect(
+                url_concat(self._OAUTH_AUTHORIZE_URL, args))
+
+    def _oauth_request_token_url(self, redirect_uri= None, client_id = None,
+                                 client_secret=None, code=None,
+                                 extra_params=None):
+        url = self._OAUTH_ACCESS_TOKEN_URL
+        args = dict(
+            redirect_uri=redirect_uri,
+            code=code,
+            client_id=client_id,
+            client_secret=client_secret,
+            )
+        if extra_params: args.update(extra_params)
+        return url_concat(url, args)
+
+class TwitterMixin(OAuthMixin):
+    """Twitter OAuth authentication.
+
+    To authenticate with Twitter, register your application with
+    Twitter at http://twitter.com/apps. Then copy your Consumer Key and
+    Consumer Secret to the application settings 'twitter_consumer_key' and
+    'twitter_consumer_secret'. Use this Mixin on the handler for the URL
+    you registered as your application's Callback URL.
+
+    When your application is set up, you can use this Mixin like this
+    to authenticate the user with Twitter and get access to their stream::
+
+        class TwitterHandler(tornado.web.RequestHandler,
+                             tornado.auth.TwitterMixin):
+            @tornado.web.asynchronous
+            def get(self):
+                if self.get_argument("oauth_token", None):
+                    self.get_authenticated_user(self.async_callback(self._on_auth))
+                    return
+                self.authorize_redirect()
+
+            def _on_auth(self, user):
+                if not user:
+                    raise tornado.web.HTTPError(500, "Twitter auth failed")
+                # Save the user using, e.g., set_secure_cookie()
+
+    The user object returned by get_authenticated_user() includes the
+    attributes 'username', 'name', and all of the custom Twitter user
+    attributes describe at
+    http://apiwiki.twitter.com/Twitter-REST-API-Method%3A-users%C2%A0show
+    in addition to 'access_token'. You should save the access token with
+    the user; it is required to make requests on behalf of the user later
+    with twitter_request().
+    """
+    _OAUTH_REQUEST_TOKEN_URL = "http://api.twitter.com/oauth/request_token"
+    _OAUTH_ACCESS_TOKEN_URL = "http://api.twitter.com/oauth/access_token"
+    _OAUTH_AUTHORIZE_URL = "http://api.twitter.com/oauth/authorize"
+    _OAUTH_AUTHENTICATE_URL = "http://api.twitter.com/oauth/authenticate"
+    _OAUTH_NO_CALLBACKS = False
+
+
+    def authenticate_redirect(self, callback_uri = None):
+        """Just like authorize_redirect(), but auto-redirects if authorized.
+
+        This is generally the right interface to use if you are using
+        Twitter for single-sign on.
+        """
+        http = httpclient.AsyncHTTPClient()
+        http.fetch(self._oauth_request_token_url(callback_uri = callback_uri), self.async_callback(
+            self._on_request_token, self._OAUTH_AUTHENTICATE_URL, None))
+
+    def twitter_request(self, path, callback, access_token=None,
+                           post_args=None, **args):
+        """Fetches the given API path, e.g., "/statuses/user_timeline/btaylor"
+
+        The path should not include the format (we automatically append
+        ".json" and parse the JSON output).
+
+        If the request is a POST, post_args should be provided. Query
+        string arguments should be given as keyword arguments.
+
+        All the Twitter methods are documented at
+        http://apiwiki.twitter.com/Twitter-API-Documentation.
+
+        Many methods require an OAuth access token which you can obtain
+        through authorize_redirect() and get_authenticated_user(). The
+        user returned through that process includes an 'access_token'
+        attribute that can be used to make authenticated requests via
+        this method. Example usage::
+
+            class MainHandler(tornado.web.RequestHandler,
+                              tornado.auth.TwitterMixin):
+                @tornado.web.authenticated
+                @tornado.web.asynchronous
+                def get(self):
+                    self.twitter_request(
+                        "/statuses/update",
+                        post_args={"status": "Testing Tornado Web Server"},
+                        access_token=user["access_token"],
+                        callback=self.async_callback(self._on_post))
+
+                def _on_post(self, new_entry):
+                    if not new_entry:
+                        # Call failed; perhaps missing permission?
+                        self.authorize_redirect()
+                        return
+                    self.finish("Posted a message!")
+
+        """
+        if path.startswith('http:') or path.startswith('https:'):
+            # Raw urls are useful for e.g. search which doesn't follow the
+            # usual pattern: http://search.twitter.com/search.json
+            url = path
+        else:
+            url = "http://api.twitter.com/1" + path + ".json"
+        # Add the OAuth resource request signature if we have credentials
+        if access_token:
+            all_args = {}
+            all_args.update(args)
+            all_args.update(post_args or {})
+            method = "POST" if post_args is not None else "GET"
+            oauth = self._oauth_request_parameters(
+                url, access_token, all_args, method=method)
+            args.update(oauth)
+        if args: url += "?" + urllib.urlencode(args)
+        callback = self.async_callback(self._on_twitter_request, callback)
+        http = httpclient.AsyncHTTPClient()
+        if post_args is not None:
+            http.fetch(url, method="POST", body=urllib.urlencode(post_args),
+                       callback=callback)
+        else:
+            http.fetch(url, callback=callback)
+
+    def _on_twitter_request(self, callback, response):
+        if response.error:
+            logging.warning("Error response %s fetching %s", response.error,
+                            response.request.url)
+            callback(None)
+            return
+        callback(escape.json_decode(response.body))
+
+    def _oauth_consumer_token(self):
+        self.require_setting("twitter_consumer_key", "Twitter OAuth")
+        self.require_setting("twitter_consumer_secret", "Twitter OAuth")
+        return dict(
+            key=self.settings["twitter_consumer_key"],
+            secret=self.settings["twitter_consumer_secret"])
+
+    def _oauth_get_user(self, access_token, callback):
+        callback = self.async_callback(self._parse_user_response, callback)
+        self.twitter_request(
+            "/users/show/" + access_token["screen_name"],
+            access_token=access_token, callback=callback)
+
+    def _parse_user_response(self, callback, user):
+        if user:
+            user["username"] = user["screen_name"]
+        callback(user)
+
+
+class FriendFeedMixin(OAuthMixin):
+    """FriendFeed OAuth authentication.
+
+    To authenticate with FriendFeed, register your application with
+    FriendFeed at http://friendfeed.com/api/applications. Then
+    copy your Consumer Key and Consumer Secret to the application settings
+    'friendfeed_consumer_key' and 'friendfeed_consumer_secret'. Use
+    this Mixin on the handler for the URL you registered as your
+    application's Callback URL.
+
+    When your application is set up, you can use this Mixin like this
+    to authenticate the user with FriendFeed and get access to their feed::
+
+        class FriendFeedHandler(tornado.web.RequestHandler,
+                                tornado.auth.FriendFeedMixin):
+            @tornado.web.asynchronous
+            def get(self):
+                if self.get_argument("oauth_token", None):
+                    self.get_authenticated_user(self.async_callback(self._on_auth))
+                    return
+                self.authorize_redirect()
+
+            def _on_auth(self, user):
+                if not user:
+                    raise tornado.web.HTTPError(500, "FriendFeed auth failed")
+                # Save the user using, e.g., set_secure_cookie()
+
+    The user object returned by get_authenticated_user() includes the
+    attributes 'username', 'name', and 'description' in addition to
+    'access_token'. You should save the access token with the user;
+    it is required to make requests on behalf of the user later with
+    friendfeed_request().
+    """
+    _OAUTH_VERSION = "1.0"
+    _OAUTH_REQUEST_TOKEN_URL = "https://friendfeed.com/account/oauth/request_token"
+    _OAUTH_ACCESS_TOKEN_URL = "https://friendfeed.com/account/oauth/access_token"
+    _OAUTH_AUTHORIZE_URL = "https://friendfeed.com/account/oauth/authorize"
+    _OAUTH_NO_CALLBACKS = True
+    _OAUTH_VERSION = "1.0"
+
+
+    def friendfeed_request(self, path, callback, access_token=None,
+                           post_args=None, **args):
+        """Fetches the given relative API path, e.g., "/bret/friends"
+
+        If the request is a POST, post_args should be provided. Query
+        string arguments should be given as keyword arguments.
+
+        All the FriendFeed methods are documented at
+        http://friendfeed.com/api/documentation.
+
+        Many methods require an OAuth access token which you can obtain
+        through authorize_redirect() and get_authenticated_user(). The
+        user returned through that process includes an 'access_token'
+        attribute that can be used to make authenticated requests via
+        this method. Example usage::
+
+            class MainHandler(tornado.web.RequestHandler,
+                              tornado.auth.FriendFeedMixin):
+                @tornado.web.authenticated
+                @tornado.web.asynchronous
+                def get(self):
+                    self.friendfeed_request(
+                        "/entry",
+                        post_args={"body": "Testing Tornado Web Server"},
+                        access_token=self.current_user["access_token"],
+                        callback=self.async_callback(self._on_post))
+
+                def _on_post(self, new_entry):
+                    if not new_entry:
+                        # Call failed; perhaps missing permission?
+                        self.authorize_redirect()
+                        return
+                    self.finish("Posted a message!")
+
+        """
+        # Add the OAuth resource request signature if we have credentials
+        url = "http://friendfeed-api.com/v2" + path
+        if access_token:
+            all_args = {}
+            all_args.update(args)
+            all_args.update(post_args or {})
+            method = "POST" if post_args is not None else "GET"
+            oauth = self._oauth_request_parameters(
+                url, access_token, all_args, method=method)
+            args.update(oauth)
+        if args: url += "?" + urllib.urlencode(args)
+        callback = self.async_callback(self._on_friendfeed_request, callback)
+        http = httpclient.AsyncHTTPClient()
+        if post_args is not None:
+            http.fetch(url, method="POST", body=urllib.urlencode(post_args),
+                       callback=callback)
+        else:
+            http.fetch(url, callback=callback)
+
+    def _on_friendfeed_request(self, callback, response):
+        if response.error:
+            logging.warning("Error response %s fetching %s", response.error,
+                            response.request.url)
+            callback(None)
+            return
+        callback(escape.json_decode(response.body))
+
+    def _oauth_consumer_token(self):
+        self.require_setting("friendfeed_consumer_key", "FriendFeed OAuth")
+        self.require_setting("friendfeed_consumer_secret", "FriendFeed OAuth")
+        return dict(
+            key=self.settings["friendfeed_consumer_key"],
+            secret=self.settings["friendfeed_consumer_secret"])
+
+    def _oauth_get_user(self, access_token, callback):
+        callback = self.async_callback(self._parse_user_response, callback)
+        self.friendfeed_request(
+            "/feedinfo/" + access_token["username"],
+            include="id,name,description", access_token=access_token,
+            callback=callback)
+
+    def _parse_user_response(self, callback, user):
+        if user:
+            user["username"] = user["id"]
+        callback(user)
+
+
+class GoogleMixin(OpenIdMixin, OAuthMixin):
+    """Google Open ID / OAuth authentication.
+
+    No application registration is necessary to use Google for authentication
+    or to access Google resources on behalf of a user. To authenticate with
+    Google, redirect with authenticate_redirect(). On return, parse the
+    response with get_authenticated_user(). We send a dict containing the
+    values for the user, including 'email', 'name', and 'locale'.
+    Example usage::
+
+        class GoogleHandler(tornado.web.RequestHandler, tornado.auth.GoogleMixin):
+           @tornado.web.asynchronous
+           def get(self):
+               if self.get_argument("openid.mode", None):
+                   self.get_authenticated_user(self.async_callback(self._on_auth))
+                   return
+            self.authenticate_redirect()
+
+            def _on_auth(self, user):
+                if not user:
+                    raise tornado.web.HTTPError(500, "Google auth failed")
+                # Save the user with, e.g., set_secure_cookie()
+
+    """
+    _OPENID_ENDPOINT = "https://www.google.com/accounts/o8/ud"
+    _OAUTH_ACCESS_TOKEN_URL = "https://www.google.com/accounts/OAuthGetAccessToken"
+
+    def authorize_redirect(self, oauth_scope, callback_uri=None,
+                           ax_attrs=["name","email","language","username"]):
+        """Authenticates and authorizes for the given Google resource.
+
+        Some of the available resources are:
+
+        * Gmail Contacts - http://www.google.com/m8/feeds/
+        * Calendar - http://www.google.com/calendar/feeds/
+        * Finance - http://finance.google.com/finance/feeds/
+
+        You can authorize multiple resources by separating the resource
+        URLs with a space.
+        """
+        callback_uri = callback_uri or self.request.uri
+        args = self._openid_args(callback_uri, ax_attrs=ax_attrs,
+                                 oauth_scope=oauth_scope)
+        self.redirect(self._OPENID_ENDPOINT + "?" + urllib.urlencode(args))
+
+    def get_authenticated_user(self, callback):
+        """Fetches the authenticated user data upon redirect."""
+        # Look to see if we are doing combined OpenID/OAuth
+        oauth_ns = ""
+        for name, values in self.request.arguments.iteritems():
+            if name.startswith("openid.ns.") and \
+               values[-1] == u"http://specs.openid.net/extensions/oauth/1.0":
+                oauth_ns = name[10:]
+                break
+        token = self.get_argument("openid." + oauth_ns + ".request_token", "")
+        if token:
+            http = httpclient.AsyncHTTPClient()
+            token = dict(key=token, secret="")
+            http.fetch(self._oauth_access_token_url(token),
+                       self.async_callback(self._on_access_token, callback))
+        else:
+            OpenIdMixin.get_authenticated_user(self, callback)
+
+    def _oauth_consumer_token(self):
+        self.require_setting("google_consumer_key", "Google OAuth")
+        self.require_setting("google_consumer_secret", "Google OAuth")
+        return dict(
+            key=self.settings["google_consumer_key"],
+            secret=self.settings["google_consumer_secret"])
+
+    def _oauth_get_user(self, access_token, callback):
+        OpenIdMixin.get_authenticated_user(self, callback)
+
+class FacebookMixin(object):
+    """Facebook Connect authentication.
+
+    New applications should consider using `FacebookGraphMixin` below instead
+    of this class.
+
+    To authenticate with Facebook, register your application with
+    Facebook at http://www.facebook.com/developers/apps.php. Then
+    copy your API Key and Application Secret to the application settings
+    'facebook_api_key' and 'facebook_secret'.
+
+    When your application is set up, you can use this Mixin like this
+    to authenticate the user with Facebook::
+
+        class FacebookHandler(tornado.web.RequestHandler,
+                              tornado.auth.FacebookMixin):
+            @tornado.web.asynchronous
+            def get(self):
+                if self.get_argument("session", None):
+                    self.get_authenticated_user(self.async_callback(self._on_auth))
+                    return
+                self.authenticate_redirect()
+
+            def _on_auth(self, user):
+                if not user:
+                    raise tornado.web.HTTPError(500, "Facebook auth failed")
+                # Save the user using, e.g., set_secure_cookie()
+
+    The user object returned by get_authenticated_user() includes the
+    attributes 'facebook_uid' and 'name' in addition to session attributes
+    like 'session_key'. You should save the session key with the user; it is
+    required to make requests on behalf of the user later with
+    facebook_request().
+    """
+    def authenticate_redirect(self, callback_uri=None, cancel_uri=None,
+                              extended_permissions=None):
+        """Authenticates/installs this app for the current user."""
+        self.require_setting("facebook_api_key", "Facebook Connect")
+        callback_uri = callback_uri or self.request.uri
+        args = {
+            "api_key": self.settings["facebook_api_key"],
+            "v": "1.0",
+            "fbconnect": "true",
+            "display": "page",
+            "next": urlparse.urljoin(self.request.full_url(), callback_uri),
+            "return_session": "true",
+        }
+        if cancel_uri:
+            args["cancel_url"] = urlparse.urljoin(
+                self.request.full_url(), cancel_uri)
+        if extended_permissions:
+            if isinstance(extended_permissions, (unicode, bytes_type)):
+                extended_permissions = [extended_permissions]
+            args["req_perms"] = ",".join(extended_permissions)
+        self.redirect("http://www.facebook.com/login.php?" +
+                      urllib.urlencode(args))
+
+    def authorize_redirect(self, extended_permissions, callback_uri=None,
+                           cancel_uri=None):
+        """Redirects to an authorization request for the given FB resource.
+
+        The available resource names are listed at
+        http://wiki.developers.facebook.com/index.php/Extended_permission.
+        The most common resource types include:
+
+        * publish_stream
+        * read_stream
+        * email
+        * sms
+
+        extended_permissions can be a single permission name or a list of
+        names. To get the session secret and session key, call
+        get_authenticated_user() just as you would with
+        authenticate_redirect().
+        """
+        self.authenticate_redirect(callback_uri, cancel_uri,
+                                   extended_permissions)
+
+    def get_authenticated_user(self, callback):
+        """Fetches the authenticated Facebook user.
+
+        The authenticated user includes the special Facebook attributes
+        'session_key' and 'facebook_uid' in addition to the standard
+        user attributes like 'name'.
+        """
+        self.require_setting("facebook_api_key", "Facebook Connect")
+        session = escape.json_decode(self.get_argument("session"))
+        self.facebook_request(
+            method="facebook.users.getInfo",
+            callback=self.async_callback(
+                self._on_get_user_info, callback, session),
+            session_key=session["session_key"],
+            uids=session["uid"],
+            fields="uid,first_name,last_name,name,locale,pic_square," \
+                   "profile_url,username")
+
+    def facebook_request(self, method, callback, **args):
+        """Makes a Facebook API REST request.
+
+        We automatically include the Facebook API key and signature, but
+        it is the callers responsibility to include 'session_key' and any
+        other required arguments to the method.
+
+        The available Facebook methods are documented here:
+        http://wiki.developers.facebook.com/index.php/API
+
+        Here is an example for the stream.get() method::
+
+            class MainHandler(tornado.web.RequestHandler,
+                              tornado.auth.FacebookMixin):
+                @tornado.web.authenticated
+                @tornado.web.asynchronous
+                def get(self):
+                    self.facebook_request(
+                        method="stream.get",
+                        callback=self.async_callback(self._on_stream),
+                        session_key=self.current_user["session_key"])
+
+                def _on_stream(self, stream):
+                    if stream is None:
+                       # Not authorized to read the stream yet?
+                       self.redirect(self.authorize_redirect("read_stream"))
+                       return
+                    self.render("stream.html", stream=stream)
+
+        """
+        self.require_setting("facebook_api_key", "Facebook Connect")
+        self.require_setting("facebook_secret", "Facebook Connect")
+        if not method.startswith("facebook."):
+            method = "facebook." + method
+        args["api_key"] = self.settings["facebook_api_key"]
+        args["v"] = "1.0"
+        args["method"] = method
+        args["call_id"] = str(long(time.time() * 1e6))
+        args["format"] = "json"
+        args["sig"] = self._signature(args)
+        url = "http://api.facebook.com/restserver.php?" + \
+            urllib.urlencode(args)
+        http = httpclient.AsyncHTTPClient()
+        http.fetch(url, callback=self.async_callback(
+            self._parse_response, callback))
+
+    def _on_get_user_info(self, callback, session, users):
+        if users is None:
+            callback(None)
+            return
+        callback({
+            "name": users[0]["name"],
+            "first_name": users[0]["first_name"],
+            "last_name": users[0]["last_name"],
+            "uid": users[0]["uid"],
+            "locale": users[0]["locale"],
+            "pic_square": users[0]["pic_square"],
+            "profile_url": users[0]["profile_url"],
+            "username": users[0].get("username"),
+            "session_key": session["session_key"],
+            "session_expires": session.get("expires"),
+        })
+
+    def _parse_response(self, callback, response):
+        if response.error:
+            logging.warning("HTTP error from Facebook: %s", response.error)
+            callback(None)
+            return
+        try:
+            json = escape.json_decode(response.body)
+        except Exception:
+            logging.warning("Invalid JSON from Facebook: %r", response.body)
+            callback(None)
+            return
+        if isinstance(json, dict) and json.get("error_code"):
+            logging.warning("Facebook error: %d: %r", json["error_code"],
+                            json.get("error_msg"))
+            callback(None)
+            return
+        callback(json)
+
+    def _signature(self, args):
+        parts = ["%s=%s" % (n, args[n]) for n in sorted(args.keys())]
+        body = "".join(parts) + self.settings["facebook_secret"]
+        if isinstance(body, unicode): body = body.encode("utf-8")
+        return hashlib.md5(body).hexdigest()
+
+class FacebookGraphMixin(OAuth2Mixin):
+    """Facebook authentication using the new Graph API and OAuth2."""
+    _OAUTH_ACCESS_TOKEN_URL = "https://graph.facebook.com/oauth/access_token?"
+    _OAUTH_AUTHORIZE_URL = "https://graph.facebook.com/oauth/authorize?"
+    _OAUTH_NO_CALLBACKS = False
+
+    def get_authenticated_user(self, redirect_uri, client_id, client_secret,
+                              code, callback, extra_fields=None):
+      """Handles the login for the Facebook user, returning a user object.
+
+      Example usage::
+
+          class FacebookGraphLoginHandler(LoginHandler, tornado.auth.FacebookGraphMixin):
+            @tornado.web.asynchronous
+            def get(self):
+                if self.get_argument("code", False):
+                    self.get_authenticated_user(
+                      redirect_uri='/auth/facebookgraph/',
+                      client_id=self.settings["facebook_api_key"],
+                      client_secret=self.settings["facebook_secret"],
+                      code=self.get_argument("code"),
+                      callback=self.async_callback(
+                        self._on_login))
+                    return
+                self.authorize_redirect(redirect_uri='/auth/facebookgraph/',
+                                        client_id=self.settings["facebook_api_key"],
+                                        extra_params={"scope": "read_stream,offline_access"})
+
+            def _on_login(self, user):
+              logging.error(user)
+              self.finish()
+
+      """
+      http = httpclient.AsyncHTTPClient()
+      args = {
+        "redirect_uri": redirect_uri,
+        "code": code,
+        "client_id": client_id,
+        "client_secret": client_secret,
+      }
+
+      fields = set(['id', 'name', 'first_name', 'last_name',
+                    'locale', 'picture', 'link'])
+      if extra_fields: fields.update(extra_fields)
+
+      http.fetch(self._oauth_request_token_url(**args),
+          self.async_callback(self._on_access_token, redirect_uri, client_id,
+                              client_secret, callback, fields))
+
+    def _on_access_token(self, redirect_uri, client_id, client_secret,
+                        callback, fields, response):
+      if response.error:
+          logging.warning('Facebook auth error: %s' % str(response))
+          callback(None)
+          return
+
+      args = escape.parse_qs_bytes(escape.native_str(response.body))
+      session = {
+          "access_token": args["access_token"][-1],
+          "expires": args.get("expires")
+      }
+
+      self.facebook_request(
+          path="/me",
+          callback=self.async_callback(
+              self._on_get_user_info, callback, session, fields),
+          access_token=session["access_token"],
+          fields=",".join(fields)
+          )
+
+
+    def _on_get_user_info(self, callback, session, fields, user):
+        if user is None:
+            callback(None)
+            return
+
+        fieldmap = {}
+        for field in fields:
+            fieldmap[field] = user.get(field)
+
+        fieldmap.update({"access_token": session["access_token"], "session_expires": session.get("expires")})
+        callback(fieldmap)
+
+    def facebook_request(self, path, callback, access_token=None,
+                           post_args=None, **args):
+        """Fetches the given relative API path, e.g., "/btaylor/picture"
+
+        If the request is a POST, post_args should be provided. Query
+        string arguments should be given as keyword arguments.
+
+        An introduction to the Facebook Graph API can be found at
+        http://developers.facebook.com/docs/api
+
+        Many methods require an OAuth access token which you can obtain
+        through authorize_redirect() and get_authenticated_user(). The
+        user returned through that process includes an 'access_token'
+        attribute that can be used to make authenticated requests via
+        this method. Example usage::
+
+            class MainHandler(tornado.web.RequestHandler,
+                              tornado.auth.FacebookGraphMixin):
+                @tornado.web.authenticated
+                @tornado.web.asynchronous
+                def get(self):
+                    self.facebook_request(
+                        "/me/feed",
+                        post_args={"message": "I am posting from my Tornado application!"},
+                        access_token=self.current_user["access_token"],
+                        callback=self.async_callback(self._on_post))
+
+                def _on_post(self, new_entry):
+                    if not new_entry:
+                        # Call failed; perhaps missing permission?
+                        self.authorize_redirect()
+                        return
+                    self.finish("Posted a message!")
+
+        """
+        url = "https://graph.facebook.com" + path
+        all_args = {}
+        if access_token:
+            all_args["access_token"] = access_token
+            all_args.update(args)
+            all_args.update(post_args or {})
+        if all_args: url += "?" + urllib.urlencode(all_args)
+        callback = self.async_callback(self._on_facebook_request, callback)
+        http = httpclient.AsyncHTTPClient()
+        if post_args is not None:
+            http.fetch(url, method="POST", body=urllib.urlencode(post_args),
+                       callback=callback)
+        else:
+            http.fetch(url, callback=callback)
+
+    def _on_facebook_request(self, callback, response):
+        if response.error:
+            logging.warning("Error response %s fetching %s", response.error,
+                            response.request.url)
+            callback(None)
+            return
+        callback(escape.json_decode(response.body))
+
+def _oauth_signature(consumer_token, method, url, parameters={}, token=None):
+    """Calculates the HMAC-SHA1 OAuth signature for the given request.
+
+    See http://oauth.net/core/1.0/#signing_process
+    """
+    parts = urlparse.urlparse(url)
+    scheme, netloc, path = parts[:3]
+    normalized_url = scheme.lower() + "://" + netloc.lower() + path
+
+    base_elems = []
+    base_elems.append(method.upper())
+    base_elems.append(normalized_url)
+    base_elems.append("&".join("%s=%s" % (k, _oauth_escape(str(v)))
+                               for k, v in sorted(parameters.items())))
+    base_string =  "&".join(_oauth_escape(e) for e in base_elems)
+
+    key_elems = [escape.utf8(consumer_token["secret"])]
+    key_elems.append(escape.utf8(token["secret"] if token else ""))
+    key = b("&").join(key_elems)
+
+    hash = hmac.new(key, escape.utf8(base_string), hashlib.sha1)
+    return binascii.b2a_base64(hash.digest())[:-1]
+
+def _oauth10a_signature(consumer_token, method, url, parameters={}, token=None):
+    """Calculates the HMAC-SHA1 OAuth 1.0a signature for the given request.
+
+    See http://oauth.net/core/1.0a/#signing_process
+    """
+    parts = urlparse.urlparse(url)
+    scheme, netloc, path = parts[:3]
+    normalized_url = scheme.lower() + "://" + netloc.lower() + path
+
+    base_elems = []
+    base_elems.append(method.upper())
+    base_elems.append(normalized_url)
+    base_elems.append("&".join("%s=%s" % (k, _oauth_escape(str(v)))
+                               for k, v in sorted(parameters.items())))
+
+    base_string =  "&".join(_oauth_escape(e) for e in base_elems)
+    key_elems = [escape.utf8(urllib.quote(consumer_token["secret"], safe='~'))]
+    key_elems.append(escape.utf8(urllib.quote(token["secret"], safe='~') if token else ""))
+    key = b("&").join(key_elems)
+
+    hash = hmac.new(key, escape.utf8(base_string), hashlib.sha1)
+    return binascii.b2a_base64(hash.digest())[:-1]
+
+def _oauth_escape(val):
+    if isinstance(val, unicode):
+        val = val.encode("utf-8")
+    return urllib.quote(val, safe="~")
+
+
+def _oauth_parse_response(body):
+    p = escape.parse_qs(body, keep_blank_values=False)
+    token = dict(key=p[b("oauth_token")][0], secret=p[b("oauth_token_secret")][0])
+
+    # Add the extra parameters the Provider included to the token
+    special = (b("oauth_token"), b("oauth_token_secret"))
+    token.update((k, p[k][0]) for k in p if k not in special)
+    return token
+
+
diff --git a/tornado/autoreload.py b/tornado/autoreload.py
new file mode 100644
index 0000000..7e3a3d7
--- /dev/null
+++ b/tornado/autoreload.py
@@ -0,0 +1,250 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""A module to automatically restart the server when a module is modified.
+
+Most applications should not call this module directly.  Instead, pass the
+keyword argument ``debug=True`` to the `tornado.web.Application` constructor.
+This will enable autoreload mode as well as checking for changes to templates
+and static resources.
+
+This module depends on IOLoop, so it will not work in WSGI applications
+and Google AppEngine.  It also will not work correctly when HTTPServer's
+multi-process mode is used.
+"""
+
+from __future__ import with_statement
+
+import functools
+import logging
+import os
+import pkgutil
+import sys
+import types
+import subprocess
+
+from tornado import ioloop
+from tornado import process
+
+try:
+    import signal
+except ImportError:
+    signal = None
+
+def start(io_loop=None, check_time=500):
+    """Restarts the process automatically when a module is modified.
+
+    We run on the I/O loop, and restarting is a destructive operation,
+    so will terminate any pending requests.
+    """
+    io_loop = io_loop or ioloop.IOLoop.instance()
+    add_reload_hook(functools.partial(_close_all_fds, io_loop))
+    modify_times = {}
+    callback = functools.partial(_reload_on_update, modify_times)
+    scheduler = ioloop.PeriodicCallback(callback, check_time, io_loop=io_loop)
+    scheduler.start()
+
+def wait():
+    """Wait for a watched file to change, then restart the process.
+
+    Intended to be used at the end of scripts like unit test runners,
+    to run the tests again after any source file changes (but see also
+    the command-line interface in `main`)
+    """
+    io_loop = ioloop.IOLoop()
+    start(io_loop)
+    io_loop.start()
+
+_watched_files = set()
+
+def watch(filename):
+    """Add a file to the watch list.
+
+    All imported modules are watched by default.
+    """
+    _watched_files.add(filename)
+
+_reload_hooks = []
+
+def add_reload_hook(fn):
+    """Add a function to be called before reloading the process.
+
+    Note that for open file and socket handles it is generally
+    preferable to set the ``FD_CLOEXEC`` flag (using `fcntl` or
+    `tornado.platform.auto.set_close_exec`) instead of using a reload
+    hook to close them.
+    """
+    _reload_hooks.append(fn)
+
+def _close_all_fds(io_loop):
+    for fd in io_loop._handlers.keys():
+        try:
+            os.close(fd)
+        except Exception:
+            pass
+
+_reload_attempted = False
+
+def _reload_on_update(modify_times):
+    if _reload_attempted:
+        # We already tried to reload and it didn't work, so don't try again.
+        return
+    if process.task_id() is not None:
+        # We're in a child process created by fork_processes.  If child
+        # processes restarted themselves, they'd all restart and then
+        # all call fork_processes again.
+        return
+    for module in sys.modules.values():
+        # Some modules play games with sys.modules (e.g. email/__init__.py
+        # in the standard library), and occasionally this can cause strange
+        # failures in getattr.  Just ignore anything that's not an ordinary
+        # module.
+        if not isinstance(module, types.ModuleType): continue
+        path = getattr(module, "__file__", None)
+        if not path: continue
+        if path.endswith(".pyc") or path.endswith(".pyo"):
+            path = path[:-1]
+        _check_file(modify_times, path)
+    for path in _watched_files:
+        _check_file(modify_times, path)
+
+def _check_file(modify_times, path):
+    try:
+        modified = os.stat(path).st_mtime
+    except Exception:
+        return
+    if path not in modify_times:
+        modify_times[path] = modified
+        return
+    if modify_times[path] != modified:
+        logging.info("%s modified; restarting server", path)
+        _reload()
+
+def _reload():
+    global _reload_attempted
+    _reload_attempted = True
+    for fn in _reload_hooks:
+        fn()
+    if hasattr(signal, "setitimer"):
+        # Clear the alarm signal set by
+        # ioloop.set_blocking_log_threshold so it doesn't fire
+        # after the exec.
+        signal.setitimer(signal.ITIMER_REAL, 0, 0)
+    if sys.platform == 'win32':
+        # os.execv is broken on Windows and can't properly parse command line
+        # arguments and executable name if they contain whitespaces. subprocess
+        # fixes that behavior.
+        subprocess.Popen([sys.executable] + sys.argv)
+        sys.exit(0)
+    else:
+        try:
+            os.execv(sys.executable, [sys.executable] + sys.argv)
+        except OSError:
+            # Mac OS X versions prior to 10.6 do not support execv in
+            # a process that contains multiple threads.  Instead of
+            # re-executing in the current process, start a new one
+            # and cause the current process to exit.  This isn't
+            # ideal since the new process is detached from the parent
+            # terminal and thus cannot easily be killed with ctrl-C,
+            # but it's better than not being able to autoreload at
+            # all.
+            # Unfortunately the errno returned in this case does not
+            # appear to be consistent, so we can't easily check for
+            # this error specifically.
+            os.spawnv(os.P_NOWAIT, sys.executable,
+                      [sys.executable] + sys.argv)
+            sys.exit(0)
+
+_USAGE = """\
+Usage:
+  python -m tornado.autoreload -m module.to.run [args...]
+  python -m tornado.autoreload path/to/script.py [args...]
+"""
+def main():
+    """Command-line wrapper to re-run a script whenever its source changes.
+    
+    Scripts may be specified by filename or module name::
+
+        python -m tornado.autoreload -m tornado.test.runtests
+        python -m tornado.autoreload tornado/test/runtests.py
+
+    Running a script with this wrapper is similar to calling
+    `tornado.autoreload.wait` at the end of the script, but this wrapper
+    can catch import-time problems like syntax errors that would otherwise
+    prevent the script from reaching its call to `wait`.
+    """
+    original_argv = sys.argv
+    sys.argv = sys.argv[:]
+    if len(sys.argv) >= 3 and sys.argv[1] == "-m":
+        mode = "module"
+        module = sys.argv[2]
+        del sys.argv[1:3]
+    elif len(sys.argv) >= 2:
+        mode = "script"
+        script = sys.argv[1]
+        sys.argv = sys.argv[1:]
+    else:
+        print >>sys.stderr, _USAGE
+        sys.exit(1)
+
+    try:
+        if mode == "module":
+            import runpy
+            runpy.run_module(module, run_name="__main__", alter_sys=True)
+        elif mode == "script":
+            with open(script) as f:
+                global __file__
+                __file__ = script
+                # Use globals as our "locals" dictionary so that
+                # something that tries to import __main__ (e.g. the unittest
+                # module) will see the right things.
+                exec f.read() in globals(), globals()
+    except SystemExit, e:
+        logging.info("Script exited with status %s", e.code)
+    except Exception, e:
+        logging.warning("Script exited with uncaught exception", exc_info=True)
+        if isinstance(e, SyntaxError):
+            watch(e.filename)
+    else:
+        logging.info("Script exited normally")
+    # restore sys.argv so subsequent executions will include autoreload
+    sys.argv = original_argv
+
+    if mode == 'module':
+        # runpy did a fake import of the module as __main__, but now it's
+        # no longer in sys.modules.  Figure out where it is and watch it.
+        watch(pkgutil.get_loader(module).get_filename())
+
+    wait()
+    
+
+if __name__ == "__main__":
+    # If this module is run with "python -m tornado.autoreload", the current
+    # directory is automatically prepended to sys.path, but not if it is
+    # run as "path/to/tornado/autoreload.py".  The processing for "-m" rewrites
+    # the former to the latter, so subsequent executions won't have the same
+    # path as the original.  Modify os.environ here to ensure that the
+    # re-executed process will have the same path.
+    # Conversely, when run as path/to/tornado/autoreload.py, the directory
+    # containing autoreload.py gets added to the path, but we don't want
+    # tornado modules importable at top level, so remove it.
+    path_prefix = '.' + os.pathsep
+    if (sys.path[0] == '' and
+        not os.environ.get("PYTHONPATH", "").startswith(path_prefix)):
+        os.environ["PYTHONPATH"] = path_prefix + os.environ.get("PYTHONPATH", "")
+    elif sys.path[0] == os.path.dirname(__file__):
+        del sys.path[0]
+    main()
diff --git a/tornado/ca-certificates.crt b/tornado/ca-certificates.crt
new file mode 100644
index 0000000..26971c8
--- /dev/null
+++ b/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/tornado/curl_httpclient.py b/tornado/curl_httpclient.py
new file mode 100644
index 0000000..a338cb8
--- /dev/null
+++ b/tornado/curl_httpclient.py
@@ -0,0 +1,435 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Blocking and non-blocking HTTP client implementations using pycurl."""
+
+from __future__ import with_statement
+
+import cStringIO
+import collections
+import logging
+import pycurl
+import threading
+import time
+
+from tornado import httputil
+from tornado import ioloop
+from tornado import stack_context
+
+from tornado.escape import utf8
+from tornado.httpclient import HTTPRequest, HTTPResponse, HTTPError, AsyncHTTPClient, main
+
+class CurlAsyncHTTPClient(AsyncHTTPClient):
+    def initialize(self, io_loop=None, max_clients=10,
+                   max_simultaneous_connections=None):
+        self.io_loop = io_loop
+        self._multi = pycurl.CurlMulti()
+        self._multi.setopt(pycurl.M_TIMERFUNCTION, self._set_timeout)
+        self._multi.setopt(pycurl.M_SOCKETFUNCTION, self._handle_socket)
+        self._curls = [_curl_create(max_simultaneous_connections)
+                       for i in xrange(max_clients)]
+        self._free_list = self._curls[:]
+        self._requests = collections.deque()
+        self._fds = {}
+        self._timeout = None
+
+        try:
+            self._socket_action = self._multi.socket_action
+        except AttributeError:
+            # socket_action is found in pycurl since 7.18.2 (it's been
+            # in libcurl longer than that but wasn't accessible to
+            # python).
+            logging.warning("socket_action method missing from pycurl; "
+                            "falling back to socket_all. Upgrading "
+                            "libcurl and pycurl will improve performance")
+            self._socket_action = \
+                lambda fd, action: self._multi.socket_all()
+
+        # libcurl has bugs that sometimes cause it to not report all
+        # relevant file descriptors and timeouts to TIMERFUNCTION/
+        # SOCKETFUNCTION.  Mitigate the effects of such bugs by
+        # forcing a periodic scan of all active requests.
+        self._force_timeout_callback = ioloop.PeriodicCallback(
+            self._handle_force_timeout, 1000, io_loop=io_loop)
+        self._force_timeout_callback.start()
+
+    def close(self):
+        self._force_timeout_callback.stop()
+        for curl in self._curls:
+            curl.close()
+        self._multi.close()
+        self._closed = True
+        super(CurlAsyncHTTPClient, self).close()
+
+    def fetch(self, request, callback, **kwargs):
+        if not isinstance(request, HTTPRequest):
+            request = HTTPRequest(url=request, **kwargs)
+        self._requests.append((request, stack_context.wrap(callback)))
+        self._process_queue()
+        self._set_timeout(0)
+
+    def _handle_socket(self, event, fd, multi, data):
+        """Called by libcurl when it wants to change the file descriptors
+        it cares about.
+        """
+        event_map = {
+            pycurl.POLL_NONE: ioloop.IOLoop.NONE,
+            pycurl.POLL_IN: ioloop.IOLoop.READ,
+            pycurl.POLL_OUT: ioloop.IOLoop.WRITE,
+            pycurl.POLL_INOUT: ioloop.IOLoop.READ | ioloop.IOLoop.WRITE
+        }
+        if event == pycurl.POLL_REMOVE:
+            self.io_loop.remove_handler(fd)
+            del self._fds[fd]
+        else:
+            ioloop_event = event_map[event]
+            if fd not in self._fds:
+                self._fds[fd] = ioloop_event
+                self.io_loop.add_handler(fd, self._handle_events,
+                                         ioloop_event)
+            else:
+                self._fds[fd] = ioloop_event
+                self.io_loop.update_handler(fd, ioloop_event)
+
+    def _set_timeout(self, msecs):
+        """Called by libcurl to schedule a timeout."""
+        if self._timeout is not None:
+            self.io_loop.remove_timeout(self._timeout)
+        self._timeout = self.io_loop.add_timeout(
+            time.time() + msecs/1000.0, self._handle_timeout)
+
+    def _handle_events(self, fd, events):
+        """Called by IOLoop when there is activity on one of our
+        file descriptors.
+        """
+        action = 0
+        if events & ioloop.IOLoop.READ: action |= pycurl.CSELECT_IN
+        if events & ioloop.IOLoop.WRITE: action |= pycurl.CSELECT_OUT
+        while True:
+            try:
+                ret, num_handles = self._socket_action(fd, action)
+            except pycurl.error, e:
+                ret = e.args[0]
+            if ret != pycurl.E_CALL_MULTI_PERFORM:
+                break
+        self._finish_pending_requests()
+
+    def _handle_timeout(self):
+        """Called by IOLoop when the requested timeout has passed."""
+        with stack_context.NullContext():
+            self._timeout = None
+            while True:
+                try:
+                    ret, num_handles = self._socket_action(
+                        pycurl.SOCKET_TIMEOUT, 0)
+                except pycurl.error, e:
+                    ret = e.args[0]
+                if ret != pycurl.E_CALL_MULTI_PERFORM:
+                    break
+            self._finish_pending_requests()
+
+        # In theory, we shouldn't have to do this because curl will
+        # call _set_timeout whenever the timeout changes.  However,
+        # sometimes after _handle_timeout we will need to reschedule
+        # immediately even though nothing has changed from curl's
+        # perspective.  This is because when socket_action is
+        # called with SOCKET_TIMEOUT, libcurl decides internally which
+        # timeouts need to be processed by using a monotonic clock
+        # (where available) while tornado uses python's time.time()
+        # to decide when timeouts have occurred.  When those clocks
+        # disagree on elapsed time (as they will whenever there is an
+        # NTP adjustment), tornado might call _handle_timeout before
+        # libcurl is ready.  After each timeout, resync the scheduled
+        # timeout with libcurl's current state.
+        new_timeout = self._multi.timeout()
+        if new_timeout != -1:
+            self._set_timeout(new_timeout)
+
+    def _handle_force_timeout(self):
+        """Called by IOLoop periodically to ask libcurl to process any
+        events it may have forgotten about.
+        """
+        with stack_context.NullContext():
+            while True:
+                try:
+                    ret, num_handles = self._multi.socket_all()
+                except pycurl.error, e:
+                    ret = e.args[0]
+                if ret != pycurl.E_CALL_MULTI_PERFORM:
+                    break
+            self._finish_pending_requests()
+
+    def _finish_pending_requests(self):
+        """Process any requests that were completed by the last
+        call to multi.socket_action.
+        """
+        while True:
+            num_q, ok_list, err_list = self._multi.info_read()
+            for curl in ok_list:
+                self._finish(curl)
+            for curl, errnum, errmsg in err_list:
+                self._finish(curl, errnum, errmsg)
+            if num_q == 0:
+                break
+        self._process_queue()
+
+    def _process_queue(self):
+        with stack_context.NullContext():
+            while True:
+                started = 0
+                while self._free_list and self._requests:
+                    started += 1
+                    curl = self._free_list.pop()
+                    (request, callback) = self._requests.popleft()
+                    curl.info = {
+                        "headers": httputil.HTTPHeaders(),
+                        "buffer": cStringIO.StringIO(),
+                        "request": request,
+                        "callback": callback,
+                        "curl_start_time": time.time(),
+                    }
+                    # Disable IPv6 to mitigate the effects of this bug
+                    # on curl versions <= 7.21.0
+                    # http://sourceforge.net/tracker/?func=detail&aid=3017819&group_id=976&atid=100976
+                    if pycurl.version_info()[2] <= 0x71500:  # 7.21.0
+                        curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_V4)
+                    _curl_setup_request(curl, request, curl.info["buffer"],
+                                        curl.info["headers"])
+                    self._multi.add_handle(curl)
+
+                if not started:
+                    break
+
+    def _finish(self, curl, curl_error=None, curl_message=None):
+        info = curl.info
+        curl.info = None
+        self._multi.remove_handle(curl)
+        self._free_list.append(curl)
+        buffer = info["buffer"]
+        if curl_error:
+            error = CurlError(curl_error, curl_message)
+            code = error.code
+            effective_url = None
+            buffer.close()
+            buffer = None
+        else:
+            error = None
+            code = curl.getinfo(pycurl.HTTP_CODE)
+            effective_url = curl.getinfo(pycurl.EFFECTIVE_URL)
+            buffer.seek(0)
+        # the various curl timings are documented at
+        # http://curl.haxx.se/libcurl/c/curl_easy_getinfo.html
+        time_info = dict(
+            queue=info["curl_start_time"] - info["request"].start_time,
+            namelookup=curl.getinfo(pycurl.NAMELOOKUP_TIME),
+            connect=curl.getinfo(pycurl.CONNECT_TIME),
+            pretransfer=curl.getinfo(pycurl.PRETRANSFER_TIME),
+            starttransfer=curl.getinfo(pycurl.STARTTRANSFER_TIME),
+            total=curl.getinfo(pycurl.TOTAL_TIME),
+            redirect=curl.getinfo(pycurl.REDIRECT_TIME),
+            )
+        try:
+            info["callback"](HTTPResponse(
+                request=info["request"], code=code, headers=info["headers"],
+                buffer=buffer, effective_url=effective_url, error=error,
+                request_time=time.time() - info["curl_start_time"],
+                time_info=time_info))
+        except Exception:
+            self.handle_callback_exception(info["callback"])
+
+
+    def handle_callback_exception(self, callback):
+        self.io_loop.handle_callback_exception(callback)
+
+
+class CurlError(HTTPError):
+    def __init__(self, errno, message):
+        HTTPError.__init__(self, 599, message)
+        self.errno = errno
+
+
+def _curl_create(max_simultaneous_connections=None):
+    curl = pycurl.Curl()
+    if logging.getLogger().isEnabledFor(logging.DEBUG):
+        curl.setopt(pycurl.VERBOSE, 1)
+        curl.setopt(pycurl.DEBUGFUNCTION, _curl_debug)
+    curl.setopt(pycurl.MAXCONNECTS, max_simultaneous_connections or 5)
+    return curl
+
+
+def _curl_setup_request(curl, request, buffer, headers):
+    curl.setopt(pycurl.URL, utf8(request.url))
+
+    # libcurl's magic "Expect: 100-continue" behavior causes delays
+    # with servers that don't support it (which include, among others,
+    # Google's OpenID endpoint).  Additionally, this behavior has
+    # a bug in conjunction with the curl_multi_socket_action API
+    # (https://sourceforge.net/tracker/?func=detail&atid=100976&aid=3039744&group_id=976),
+    # which increases the delays.  It's more trouble than it's worth,
+    # so just turn off the feature (yes, setting Expect: to an empty
+    # value is the official way to disable this)
+    if "Expect" not in request.headers:
+        request.headers["Expect"] = ""
+
+    # libcurl adds Pragma: no-cache by default; disable that too
+    if "Pragma" not in request.headers:
+        request.headers["Pragma"] = ""
+
+    # Request headers may be either a regular dict or HTTPHeaders object
+    if isinstance(request.headers, httputil.HTTPHeaders):
+        curl.setopt(pycurl.HTTPHEADER,
+                    [utf8("%s: %s" % i) for i in request.headers.get_all()])
+    else:
+        curl.setopt(pycurl.HTTPHEADER,
+                    [utf8("%s: %s" % i) for i in request.headers.iteritems()])
+
+    if request.header_callback:
+        curl.setopt(pycurl.HEADERFUNCTION, request.header_callback)
+    else:
+        curl.setopt(pycurl.HEADERFUNCTION,
+                    lambda line: _curl_header_callback(headers, line))
+    if request.streaming_callback:
+        curl.setopt(pycurl.WRITEFUNCTION, request.streaming_callback)
+    else:
+        curl.setopt(pycurl.WRITEFUNCTION, buffer.write)
+    curl.setopt(pycurl.FOLLOWLOCATION, request.follow_redirects)
+    curl.setopt(pycurl.MAXREDIRS, request.max_redirects)
+    curl.setopt(pycurl.CONNECTTIMEOUT_MS, int(1000 * request.connect_timeout))
+    curl.setopt(pycurl.TIMEOUT_MS, int(1000 * request.request_timeout))
+    if request.user_agent:
+        curl.setopt(pycurl.USERAGENT, utf8(request.user_agent))
+    else:
+        curl.setopt(pycurl.USERAGENT, "Mozilla/5.0 (compatible; pycurl)")
+    if request.network_interface:
+        curl.setopt(pycurl.INTERFACE, request.network_interface)
+    if request.use_gzip:
+        curl.setopt(pycurl.ENCODING, "gzip,deflate")
+    else:
+        curl.setopt(pycurl.ENCODING, "none")
+    if request.proxy_host and request.proxy_port:
+        curl.setopt(pycurl.PROXY, request.proxy_host)
+        curl.setopt(pycurl.PROXYPORT, request.proxy_port)
+        if request.proxy_username:
+            credentials = '%s:%s' % (request.proxy_username,
+                    request.proxy_password)
+            curl.setopt(pycurl.PROXYUSERPWD, credentials)
+    else:
+        curl.setopt(pycurl.PROXY, '')
+    if request.validate_cert:
+        curl.setopt(pycurl.SSL_VERIFYPEER, 1)
+        curl.setopt(pycurl.SSL_VERIFYHOST, 2)
+    else:
+        curl.setopt(pycurl.SSL_VERIFYPEER, 0)
+        curl.setopt(pycurl.SSL_VERIFYHOST, 0)
+    if request.ca_certs is not None:
+        curl.setopt(pycurl.CAINFO, request.ca_certs)
+    else:
+        # There is no way to restore pycurl.CAINFO to its default value
+        # (Using unsetopt makes it reject all certificates).
+        # I don't see any way to read the default value from python so it
+        # can be restored later.  We'll have to just leave CAINFO untouched
+        # if no ca_certs file was specified, and require that if any
+        # request uses a custom ca_certs file, they all must.
+        pass
+
+    if request.allow_ipv6 is False:
+        # Curl behaves reasonably when DNS resolution gives an ipv6 address
+        # that we can't reach, so allow ipv6 unless the user asks to disable.
+        # (but see version check in _process_queue above)
+        curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_V4)
+
+    # Set the request method through curl's irritating interface which makes
+    # up names for almost every single method
+    curl_options = {
+        "GET": pycurl.HTTPGET,
+        "POST": pycurl.POST,
+        "PUT": pycurl.UPLOAD,
+        "HEAD": pycurl.NOBODY,
+    }
+    custom_methods = set(["DELETE"])
+    for o in curl_options.values():
+        curl.setopt(o, False)
+    if request.method in curl_options:
+        curl.unsetopt(pycurl.CUSTOMREQUEST)
+        curl.setopt(curl_options[request.method], True)
+    elif request.allow_nonstandard_methods or request.method in custom_methods:
+        curl.setopt(pycurl.CUSTOMREQUEST, request.method)
+    else:
+        raise KeyError('unknown method ' + request.method)
+
+    # Handle curl's cryptic options for every individual HTTP method
+    if request.method in ("POST", "PUT"):
+        request_buffer =  cStringIO.StringIO(utf8(request.body))
+        curl.setopt(pycurl.READFUNCTION, request_buffer.read)
+        if request.method == "POST":
+            def ioctl(cmd):
+                if cmd == curl.IOCMD_RESTARTREAD:
+                    request_buffer.seek(0)
+            curl.setopt(pycurl.IOCTLFUNCTION, ioctl)
+            curl.setopt(pycurl.POSTFIELDSIZE, len(request.body))
+        else:
+            curl.setopt(pycurl.INFILESIZE, len(request.body))
+
+    if request.auth_username is not None:
+        userpwd = "%s:%s" % (request.auth_username, request.auth_password or '')
+        curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
+        curl.setopt(pycurl.USERPWD, utf8(userpwd))
+        logging.debug("%s %s (username: %r)", request.method, request.url,
+                      request.auth_username)
+    else:
+        curl.unsetopt(pycurl.USERPWD)
+        logging.debug("%s %s", request.method, request.url)
+
+    if request.client_key is not None or request.client_cert is not None:
+        raise ValueError("Client certificate not supported with curl_httpclient")
+
+    if threading.activeCount() > 1:
+        # libcurl/pycurl is not thread-safe by default.  When multiple threads
+        # are used, signals should be disabled.  This has the side effect
+        # of disabling DNS timeouts in some environments (when libcurl is
+        # not linked against ares), so we don't do it when there is only one
+        # thread.  Applications that use many short-lived threads may need
+        # to set NOSIGNAL manually in a prepare_curl_callback since
+        # there may not be any other threads running at the time we call
+        # threading.activeCount.
+        curl.setopt(pycurl.NOSIGNAL, 1)
+    if request.prepare_curl_callback is not None:
+        request.prepare_curl_callback(curl)
+
+
+def _curl_header_callback(headers, header_line):
+    # header_line as returned by curl includes the end-of-line characters.
+    header_line = header_line.strip()
+    if header_line.startswith("HTTP/"):
+        headers.clear()
+        return
+    if not header_line:
+        return
+    headers.parse_line(header_line)
+
+def _curl_debug(debug_type, debug_msg):
+    debug_types = ('I', '<', '>', '<', '>')
+    if debug_type == 0:
+        logging.debug('%s', debug_msg.strip())
+    elif debug_type in (1, 2):
+        for line in debug_msg.splitlines():
+            logging.debug('%s %s', debug_types[debug_type], line)
+    elif debug_type == 4:
+        logging.debug('%s %r', debug_types[debug_type], debug_msg)
+
+if __name__ == "__main__":
+    AsyncHTTPClient.configure(CurlAsyncHTTPClient)
+    main()
diff --git a/tornado/database.py b/tornado/database.py
new file mode 100644
index 0000000..9771713
--- /dev/null
+++ b/tornado/database.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""A lightweight wrapper around MySQLdb."""
+
+import copy
+import MySQLdb.constants
+import MySQLdb.converters
+import MySQLdb.cursors
+import itertools
+import logging
+import time
+
+class Connection(object):
+    """A lightweight wrapper around MySQLdb DB-API connections.
+
+    The main value we provide is wrapping rows in a dict/object so that
+    columns can be accessed by name. Typical usage::
+
+        db = database.Connection("localhost", "mydatabase")
+        for article in db.query("SELECT * FROM articles"):
+            print article.title
+
+    Cursors are hidden by the implementation, but other than that, the methods
+    are very similar to the DB-API.
+
+    We explicitly set the timezone to UTC and the character encoding to
+    UTF-8 on all connections to avoid time zone and encoding errors.
+    """
+    def __init__(self, host, database, user=None, password=None,
+                 max_idle_time=7*3600):
+        self.host = host
+        self.database = database
+        self.max_idle_time = max_idle_time
+
+        args = dict(conv=CONVERSIONS, use_unicode=True, charset="utf8",
+                    db=database, init_command='SET time_zone = "+0:00"',
+                    sql_mode="TRADITIONAL")
+        if user is not None:
+            args["user"] = user
+        if password is not None:
+            args["passwd"] = password
+
+        # We accept a path to a MySQL socket file or a host(:port) string
+        if "/" in host:
+            args["unix_socket"] = host
+        else:
+            self.socket = None
+            pair = host.split(":")
+            if len(pair) == 2:
+                args["host"] = pair[0]
+                args["port"] = int(pair[1])
+            else:
+                args["host"] = host
+                args["port"] = 3306
+
+        self._db = None
+        self._db_args = args
+        self._last_use_time = time.time()
+        try:
+            self.reconnect()
+        except Exception:
+            logging.error("Cannot connect to MySQL on %s", self.host,
+                          exc_info=True)
+
+    def __del__(self):
+        self.close()
+
+    def close(self):
+        """Closes this database connection."""
+        if getattr(self, "_db", None) is not None:
+            self._db.close()
+            self._db = None
+
+    def reconnect(self):
+        """Closes the existing database connection and re-opens it."""
+        self.close()
+        self._db = MySQLdb.connect(**self._db_args)
+        self._db.autocommit(True)
+
+    def iter(self, query, *parameters):
+        """Returns an iterator for the given query and parameters."""
+        self._ensure_connected()
+        cursor = MySQLdb.cursors.SSCursor(self._db)
+        try:
+            self._execute(cursor, query, parameters)
+            column_names = [d[0] for d in cursor.description]
+            for row in cursor:
+                yield Row(zip(column_names, row))
+        finally:
+            cursor.close()
+
+    def query(self, query, *parameters):
+        """Returns a row list for the given query and parameters."""
+        cursor = self._cursor()
+        try:
+            self._execute(cursor, query, parameters)
+            column_names = [d[0] for d in cursor.description]
+            return [Row(itertools.izip(column_names, row)) for row in cursor]
+        finally:
+            cursor.close()
+
+    def get(self, query, *parameters):
+        """Returns the first row returned for the given query."""
+        rows = self.query(query, *parameters)
+        if not rows:
+            return None
+        elif len(rows) > 1:
+            raise Exception("Multiple rows returned for Database.get() query")
+        else:
+            return rows[0]
+
+    # rowcount is a more reasonable default return value than lastrowid,
+    # but for historical compatibility execute() must return lastrowid.
+    def execute(self, query, *parameters):
+        """Executes the given query, returning the lastrowid from the query."""
+        return self.execute_lastrowid(query, *parameters)
+
+    def execute_lastrowid(self, query, *parameters):
+        """Executes the given query, returning the lastrowid from the query."""
+        cursor = self._cursor()
+        try:
+            self._execute(cursor, query, parameters)
+            return cursor.lastrowid
+        finally:
+            cursor.close()
+
+    def execute_rowcount(self, query, *parameters):
+        """Executes the given query, returning the rowcount from the query."""
+        cursor = self._cursor()
+        try:
+            self._execute(cursor, query, parameters)
+            return cursor.rowcount
+        finally:
+            cursor.close()
+
+    def executemany(self, query, parameters):
+        """Executes the given query against all the given param sequences.
+
+        We return the lastrowid from the query.
+        """
+        return self.executemany_lastrowid(query, parameters)
+
+    def executemany_lastrowid(self, query, parameters):
+        """Executes the given query against all the given param sequences.
+
+        We return the lastrowid from the query.
+        """
+        cursor = self._cursor()
+        try:
+            cursor.executemany(query, parameters)
+            return cursor.lastrowid
+        finally:
+            cursor.close()
+
+    def executemany_rowcount(self, query, parameters):
+        """Executes the given query against all the given param sequences.
+
+        We return the rowcount from the query.
+        """
+        cursor = self._cursor()
+        try:
+            cursor.executemany(query, parameters)
+            return cursor.rowcount
+        finally:
+            cursor.close()
+
+    def _ensure_connected(self):
+        # Mysql by default closes client connections that are idle for
+        # 8 hours, but the client library does not report this fact until
+        # you try to perform a query and it fails.  Protect against this
+        # case by preemptively closing and reopening the connection
+        # if it has been idle for too long (7 hours by default).
+        if (self._db is None or
+            (time.time() - self._last_use_time > self.max_idle_time)):
+            self.reconnect()
+        self._last_use_time = time.time()
+
+    def _cursor(self):
+        self._ensure_connected()
+        return self._db.cursor()
+
+    def _execute(self, cursor, query, parameters):
+        try:
+            return cursor.execute(query, parameters)
+        except OperationalError:
+            logging.error("Error connecting to MySQL on %s", self.host)
+            self.close()
+            raise
+
+
+class Row(dict):
+    """A dict that allows for object-like property access syntax."""
+    def __getattr__(self, name):
+        try:
+            return self[name]
+        except KeyError:
+            raise AttributeError(name)
+
+
+# Fix the access conversions to properly recognize unicode/binary
+FIELD_TYPE = MySQLdb.constants.FIELD_TYPE
+FLAG = MySQLdb.constants.FLAG
+CONVERSIONS = copy.copy(MySQLdb.converters.conversions)
+
+field_types = [FIELD_TYPE.BLOB, FIELD_TYPE.STRING, FIELD_TYPE.VAR_STRING]
+if 'VARCHAR' in vars(FIELD_TYPE):
+    field_types.append(FIELD_TYPE.VARCHAR)
+
+for field_type in field_types:
+    CONVERSIONS[field_type] = [(FLAG.BINARY, str)] + CONVERSIONS[field_type]
+
+
+# Alias some common MySQL exceptions
+IntegrityError = MySQLdb.IntegrityError
+OperationalError = MySQLdb.OperationalError
diff --git a/tornado/epoll.c b/tornado/epoll.c
new file mode 100644
index 0000000..9a2e3a3
--- /dev/null
+++ b/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 <string.h>
+#include <sys/epoll.h>
+
+#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/tornado/escape.py b/tornado/escape.py
new file mode 100644
index 0000000..4010b1c
--- /dev/null
+++ b/tornado/escape.py
@@ -0,0 +1,327 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Escaping/unescaping methods for HTML, JSON, URLs, and others.
+
+Also includes a few other miscellaneous string manipulation functions that
+have crept in over time.
+"""
+
+import htmlentitydefs
+import re
+import sys
+import urllib
+
+# Python3 compatibility:  On python2.5, introduce the bytes alias from 2.6
+try: bytes
+except Exception: bytes = str
+
+try:
+    from urlparse import parse_qs  # Python 2.6+
+except ImportError:
+    from cgi import parse_qs
+
+# json module is in the standard library as of python 2.6; fall back to
+# simplejson if present for older versions.
+try:
+    import json
+    assert hasattr(json, "loads") and hasattr(json, "dumps")
+    _json_decode = json.loads
+    _json_encode = json.dumps
+except Exception:
+    try:
+        import simplejson
+        _json_decode = lambda s: simplejson.loads(_unicode(s))
+        _json_encode = lambda v: simplejson.dumps(v)
+    except ImportError:
+        try:
+            # For Google AppEngine
+            from django.utils import simplejson
+            _json_decode = lambda s: simplejson.loads(_unicode(s))
+            _json_encode = lambda v: simplejson.dumps(v)
+        except ImportError:
+            def _json_decode(s):
+                raise NotImplementedError(
+                    "A JSON parser is required, e.g., simplejson at "
+                    "http://pypi.python.org/pypi/simplejson/")
+            _json_encode = _json_decode
+
+
+_XHTML_ESCAPE_RE = re.compile('[&<>"]')
+_XHTML_ESCAPE_DICT = {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;'}
+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 <script> tag
+    # in HTML, as it prevents </script> 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&()]|&amp;|&quot;)*(?:[^!"#$%&'()*+,.:;<=>?@\[\]^`{|}~\s]))|(?:\((?:[^\s&()]|&amp;|&quot;)*\)))+)""")
+
+
+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 <a href="http://tornadoweb.org">http://tornadoweb.org</a>!``
+
+    Parameters:
+
+    shorten: Long urls will be shortened for display.
+
+    extra_params: Extra text to include in the link tag,
+        e.g. linkify(text, extra_params='rel="nofollow" class="external"')
+
+    require_protocol: Only linkify urls which include a protocol. If this is
+        False, urls such as www.facebook.com will also be linkified.
+
+    permitted_protocols: List (or set) of protocols which should be linkified,
+        e.g. linkify(text, permitted_protocols=["http", "ftp", "mailto"]).
+        It is very unsafe to include protocols such as "javascript".
+    """
+    if extra_params:
+        extra_params = " " + extra_params.strip()
+
+    def make_link(m):
+        url = m.group(1)
+        proto = m.group(2)
+        if require_protocol and not proto:
+            return url  # not protocol, no linkify
+
+        if proto and proto not in permitted_protocols:
+            return url  # bad protocol, no linkify
+
+        href = m.group(1)
+        if not proto:
+            href = "http://" + href   # no proto specified, use http
+
+        params = extra_params
+
+        # clip long urls. max_len is just an approximation
+        max_len = 30
+        if shorten and len(url) > max_len:
+            before_clip = url
+            if proto:
+                proto_len = len(proto) + 1 + len(m.group(3) or "")  # +1 for :
+            else:
+                proto_len = 0
+
+            parts = url[proto_len:].split("/")
+            if len(parts) > 1:
+                # Grab the whole host part plus the first bit of the path
+                # The path is usually not that interesting once shortened
+                # (no more slug, etc), so it really just provides a little
+                # extra indication of shortening.
+                url = url[:proto_len] + parts[0] + "/" + \
+                        parts[1][:8].split('?')[0].split('.')[0]
+
+            if len(url) > max_len * 1.5:  # still too long
+                url = url[:max_len]
+
+            if url != before_clip:
+                amp = url.rfind('&')
+                # avoid splitting html char entities
+                if amp > max_len - 5:
+                    url = url[:amp]
+                url += "..."
+
+                if len(url) >= len(before_clip):
+                    url = before_clip
+                else:
+                    # full url is visible on mouse-over (for those who don't
+                    # have a status bar, such as Safari by default)
+                    params += ' title="%s"' % href
+
+        return u'<a href="%s"%s>%s</a>' % (href, params, url)
+
+    # First HTML-escape so that our strings are all safe.
+    # The regex is modified to avoid character entites other than &amp; so
+    # that we won't pick up &quot;, 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/tornado/gen.py b/tornado/gen.py
new file mode 100644
index 0000000..51be537
--- /dev/null
+++ b/tornado/gen.py
@@ -0,0 +1,382 @@
+"""``tornado.gen`` is a generator-based interface to make it easier to
+work in an asynchronous environment.  Code using the ``gen`` module
+is technically asynchronous, but it is written as a single generator
+instead of a collection of separate functions.
+
+For example, the following asynchronous handler::
+
+    class AsyncHandler(RequestHandler):
+        @asynchronous
+        def get(self):
+            http_client = AsyncHTTPClient()
+            http_client.fetch("http://example.com",
+                              callback=self.on_fetch)
+
+        def on_fetch(self, response):
+            do_something_with_response(response)
+            self.render("template.html")
+
+could be written with ``gen`` as::
+
+    class GenAsyncHandler(RequestHandler):
+        @asynchronous
+        @gen.engine
+        def get(self):
+            http_client = AsyncHTTPClient()
+            response = yield gen.Task(http_client.fetch, "http://example.com")
+            do_something_with_response(response)
+            self.render("template.html")
+
+`Task` works with any function that takes a ``callback`` keyword
+argument.  You can also yield a list of ``Tasks``, which will be
+started at the same time and run in parallel; a list of results will
+be returned when they are all finished::
+
+    def get(self):
+        http_client = AsyncHTTPClient()
+        response1, response2 = yield [gen.Task(http_client.fetch, url1),
+                                      gen.Task(http_client.fetch, url2)]
+
+For more complicated interfaces, `Task` can be split into two parts:
+`Callback` and `Wait`::
+
+    class GenAsyncHandler2(RequestHandler):
+        @asynchronous
+        @gen.engine
+        def get(self):
+            http_client = AsyncHTTPClient()
+            http_client.fetch("http://example.com",
+                              callback=(yield gen.Callback("key"))
+            response = yield gen.Wait("key")
+            do_something_with_response(response)
+            self.render("template.html")
+
+The ``key`` argument to `Callback` and `Wait` allows for multiple
+asynchronous operations to be started at different times and proceed
+in parallel: yield several callbacks with different keys, then wait
+for them once all the async operations have started.
+
+The result of a `Wait` or `Task` yield expression depends on how the callback
+was run.  If it was called with no arguments, the result is ``None``.  If
+it was called with one argument, the result is that argument.  If it was
+called with more than one argument or any keyword arguments, the result
+is an `Arguments` object, which is a named tuple ``(args, kwargs)``.
+"""
+from __future__ import with_statement
+
+import functools
+import operator
+import sys
+import types
+
+from tornado.stack_context import ExceptionStackContext
+
+class KeyReuseError(Exception): pass
+class UnknownKeyError(Exception): pass
+class LeakedCallbackError(Exception): pass
+class BadYieldError(Exception): pass
+
+def engine(func):
+    """Decorator for asynchronous generators.
+
+    Any generator that yields objects from this module must be wrapped
+    in this decorator.  The decorator only works on functions that are
+    already asynchronous.  For `~tornado.web.RequestHandler`
+    ``get``/``post``/etc methods, this means that both the
+    `tornado.web.asynchronous` and `tornado.gen.engine` decorators
+    must be used (for proper exception handling, ``asynchronous``
+    should come before ``gen.engine``).  In most other cases, it means
+    that it doesn't make sense to use ``gen.engine`` on functions that
+    don't already take a callback argument.
+    """
+    @functools.wraps(func)
+    def wrapper(*args, **kwargs):
+        runner = None
+        def handle_exception(typ, value, tb):
+            # if the function throws an exception before its first "yield"
+            # (or is not a generator at all), the Runner won't exist yet.
+            # However, in that case we haven't reached anything asynchronous
+            # yet, so we can just let the exception propagate.
+            if runner is not None:
+                return runner.handle_exception(typ, value, tb)
+            return False
+        with ExceptionStackContext(handle_exception):
+            gen = func(*args, **kwargs)
+            if isinstance(gen, types.GeneratorType):
+                runner = Runner(gen)
+                runner.run()
+                return
+            assert gen is None, gen
+            # no yield, so we're done
+    return wrapper
+
+class YieldPoint(object):
+    """Base class for objects that may be yielded from the generator."""
+    def start(self, runner):
+        """Called by the runner after the generator has yielded.
+        
+        No other methods will be called on this object before ``start``.
+        """
+        raise NotImplementedError()
+
+    def is_ready(self):
+        """Called by the runner to determine whether to resume the generator.
+
+        Returns a boolean; may be called more than once.
+        """
+        raise NotImplementedError()
+
+    def get_result(self):
+        """Returns the value to use as the result of the yield expression.
+        
+        This method will only be called once, and only after `is_ready`
+        has returned true.
+        """
+        raise NotImplementedError()
+
+class Callback(YieldPoint):
+    """Returns a callable object that will allow a matching `Wait` to proceed.
+
+    The key may be any value suitable for use as a dictionary key, and is
+    used to match ``Callbacks`` to their corresponding ``Waits``.  The key
+    must be unique among outstanding callbacks within a single run of the
+    generator function, but may be reused across different runs of the same
+    function (so constants generally work fine).
+
+    The callback may be called with zero or one arguments; if an argument
+    is given it will be returned by `Wait`.
+    """
+    def __init__(self, key):
+        self.key = key
+
+    def start(self, runner):
+        self.runner = runner
+        runner.register_callback(self.key)
+
+    def is_ready(self):
+        return True
+
+    def get_result(self):
+        return self.runner.result_callback(self.key)
+
+class Wait(YieldPoint):
+    """Returns the argument passed to the result of a previous `Callback`."""
+    def __init__(self, key):
+        self.key = key
+
+    def start(self, runner):
+        self.runner = runner
+
+    def is_ready(self):
+        return self.runner.is_ready(self.key)
+
+    def get_result(self):
+        return self.runner.pop_result(self.key)
+
+class WaitAll(YieldPoint):
+    """Returns the results of multiple previous `Callbacks`.
+
+    The argument is a sequence of `Callback` keys, and the result is
+    a list of results in the same order.
+
+    `WaitAll` is equivalent to yielding a list of `Wait` objects.
+    """
+    def __init__(self, keys):
+        self.keys = keys
+
+    def start(self, runner):
+        self.runner = runner
+
+    def is_ready(self):
+        return all(self.runner.is_ready(key) for key in self.keys)
+        
+    def get_result(self):
+        return [self.runner.pop_result(key) for key in self.keys]
+            
+
+class Task(YieldPoint):
+    """Runs a single asynchronous operation.
+
+    Takes a function (and optional additional arguments) and runs it with
+    those arguments plus a ``callback`` keyword argument.  The argument passed
+    to the callback is returned as the result of the yield expression.
+
+    A `Task` is equivalent to a `Callback`/`Wait` pair (with a unique
+    key generated automatically)::
+    
+        result = yield gen.Task(func, args)
+        
+        func(args, callback=(yield gen.Callback(key)))
+        result = yield gen.Wait(key)
+    """
+    def __init__(self, func, *args, **kwargs):
+        assert "callback" not in kwargs
+        self.args = args
+        self.kwargs = kwargs
+        self.func = func
+
+    def start(self, runner):
+        self.runner = runner
+        self.key = object()
+        runner.register_callback(self.key)
+        self.kwargs["callback"] = runner.result_callback(self.key)
+        self.func(*self.args, **self.kwargs)
+    
+    def is_ready(self):
+        return self.runner.is_ready(self.key)
+
+    def get_result(self):
+        return self.runner.pop_result(self.key)
+
+class Multi(YieldPoint):
+    """Runs multiple asynchronous operations in parallel.
+
+    Takes a list of ``Tasks`` or other ``YieldPoints`` and returns a list of
+    their responses.  It is not necessary to call `Multi` explicitly,
+    since the engine will do so automatically when the generator yields
+    a list of ``YieldPoints``.
+    """
+    def __init__(self, children):
+        assert all(isinstance(i, YieldPoint) for i in children)
+        self.children = children
+    
+    def start(self, runner):
+        for i in self.children:
+            i.start(runner)
+
+    def is_ready(self):
+        return all(i.is_ready() for i in self.children)
+
+    def get_result(self):
+        return [i.get_result() for i in self.children]
+
+class _NullYieldPoint(YieldPoint):
+    def start(self, runner):
+        pass
+    def is_ready(self):
+        return True
+    def get_result(self):
+        return None
+
+class Runner(object):
+    """Internal implementation of `tornado.gen.engine`.
+
+    Maintains information about pending callbacks and their results.
+    """
+    def __init__(self, gen):
+        self.gen = gen
+        self.yield_point = _NullYieldPoint()
+        self.pending_callbacks = set()
+        self.results = {}
+        self.running = False
+        self.finished = False
+        self.exc_info = None
+        self.had_exception = False
+
+    def register_callback(self, key):
+        """Adds ``key`` to the list of callbacks."""
+        if key in self.pending_callbacks:
+            raise KeyReuseError("key %r is already pending" % key)
+        self.pending_callbacks.add(key)
+
+    def is_ready(self, key):
+        """Returns true if a result is available for ``key``."""
+        if key not in self.pending_callbacks:
+            raise UnknownKeyError("key %r is not pending" % key)
+        return key in self.results
+
+    def set_result(self, key, result):
+        """Sets the result for ``key`` and attempts to resume the generator."""
+        self.results[key] = result
+        self.run()
+
+    def pop_result(self, key):
+        """Returns the result for ``key`` and unregisters it."""
+        self.pending_callbacks.remove(key)
+        return self.results.pop(key)
+
+    def run(self):
+        """Starts or resumes the generator, running until it reaches a
+        yield point that is not ready.
+        """
+        if self.running or self.finished:
+            return
+        try:
+            self.running = True
+            while True:
+                if self.exc_info is None:
+                    try:
+                        if not self.yield_point.is_ready():
+                            return
+                        next = self.yield_point.get_result()
+                    except Exception:
+                        self.exc_info = sys.exc_info()
+                try:
+                    if self.exc_info is not None:
+                        self.had_exception = True
+                        exc_info = self.exc_info
+                        self.exc_info = None
+                        yielded = self.gen.throw(*exc_info)
+                    else:
+                        yielded = self.gen.send(next)
+                except StopIteration:
+                    self.finished = True
+                    if self.pending_callbacks and not self.had_exception:
+                        # If we ran cleanly without waiting on all callbacks
+                        # raise an error (really more of a warning).  If we
+                        # had an exception then some callbacks may have been
+                        # orphaned, so skip the check in that case.
+                        raise LeakedCallbackError(
+                            "finished without waiting for callbacks %r" %
+                            self.pending_callbacks)
+                    return
+                except Exception:
+                    self.finished = True
+                    raise
+                if isinstance(yielded, list):
+                    yielded = Multi(yielded)
+                if isinstance(yielded, YieldPoint):
+                    self.yield_point = yielded
+                    try:
+                        self.yield_point.start(self)
+                    except Exception:
+                        self.exc_info = sys.exc_info()
+                else:
+                    self.exc_info = (BadYieldError("yielded unknown object %r" % yielded),)
+        finally:
+            self.running = False
+
+    def result_callback(self, key):
+        def inner(*args, **kwargs):
+            if kwargs or len(args) > 1:
+                result = Arguments(args, kwargs)
+            elif args:
+                result = args[0]
+            else:
+                result = None
+            self.set_result(key, result)
+        return inner
+
+    def handle_exception(self, typ, value, tb):
+        if not self.running and not self.finished:
+            self.exc_info = (typ, value, tb)
+            self.run()
+            return True
+        else:
+            return False
+
+# in python 2.6+ this could be a collections.namedtuple
+class Arguments(tuple):
+    """The result of a yield expression whose callback had more than one
+    argument (or keyword arguments).
+
+    The `Arguments` object can be used as a tuple ``(args, kwargs)``
+    or an object with attributes ``args`` and ``kwargs``.
+    """
+    __slots__ = ()
+
+    def __new__(cls, args, kwargs):
+        return tuple.__new__(cls, (args, kwargs))
+
+    args = property(operator.itemgetter(0))
+    kwargs = property(operator.itemgetter(1))
diff --git a/tornado/httpclient.py b/tornado/httpclient.py
new file mode 100644
index 0000000..354d907
--- /dev/null
+++ b/tornado/httpclient.py
@@ -0,0 +1,417 @@
+"""Blocking and non-blocking HTTP client interfaces.
+
+This module defines a common interface shared by two implementations,
+`simple_httpclient` and `curl_httpclient`.  Applications may either
+instantiate their chosen implementation class directly or use the
+`AsyncHTTPClient` class from this module, which selects an implementation
+that can be overridden with the `AsyncHTTPClient.configure` method.
+
+The default implementation is `simple_httpclient`, and this is expected
+to be suitable for most users' needs.  However, some applications may wish
+to switch to `curl_httpclient` for reasons such as the following:
+
+* `curl_httpclient` has some features not found in `simple_httpclient`,
+  including support for HTTP proxies and the ability to use a specified
+  network interface.
+
+* `curl_httpclient` is more likely to be compatible with sites that are
+  not-quite-compliant with the HTTP spec, or sites that use little-exercised
+  features of HTTP.
+
+* `simple_httpclient` only supports SSL on Python 2.6 and above.
+
+* `curl_httpclient` is faster
+
+* `curl_httpclient` was the default prior to Tornado 2.0.
+
+Note that if you are using `curl_httpclient`, it is highly recommended that
+you use a recent version of ``libcurl`` and ``pycurl``.  Currently the minimum
+supported version is 7.18.2, and the recommended version is 7.21.1 or newer.
+"""
+
+import calendar
+import email.utils
+import httplib
+import time
+import weakref
+
+from tornado.escape import utf8
+from tornado import httputil
+from tornado.ioloop import IOLoop
+from tornado.util import import_object, bytes_type
+
+class HTTPClient(object):
+    """A blocking HTTP client.
+
+    This interface is provided for convenience and testing; most applications
+    that are running an IOLoop will want to use `AsyncHTTPClient` instead.
+    Typical usage looks like this::
+
+        http_client = httpclient.HTTPClient()
+        try:
+            response = http_client.fetch("http://www.google.com/")
+            print response.body
+        except httpclient.HTTPError, e:
+            print "Error:", e
+    """
+    def __init__(self, async_client_class=None):
+        self._io_loop = IOLoop()
+        if async_client_class is None:
+            async_client_class = AsyncHTTPClient
+        self._async_client = async_client_class(self._io_loop)
+        self._response = None
+        self._closed = False
+
+    def __del__(self):
+        self.close()
+
+    def close(self):
+        """Closes the HTTPClient, freeing any resources used."""
+        if not self._closed:
+            self._async_client.close()
+            self._io_loop.close()
+            self._closed = True
+
+    def fetch(self, request, **kwargs):
+        """Executes a request, returning an `HTTPResponse`.
+        
+        The request may be either a string URL or an `HTTPRequest` object.
+        If it is a string, we construct an `HTTPRequest` using any additional
+        kwargs: ``HTTPRequest(request, **kwargs)``
+
+        If an error occurs during the fetch, we raise an `HTTPError`.
+        """
+        def callback(response):
+            self._response = response
+            self._io_loop.stop()
+        self._async_client.fetch(request, callback, **kwargs)
+        self._io_loop.start()
+        response = self._response
+        self._response = None
+        response.rethrow()
+        return response
+
+class AsyncHTTPClient(object):
+    """An non-blocking HTTP client.
+
+    Example usage::
+
+        import ioloop
+
+        def handle_request(response):
+            if response.error:
+                print "Error:", response.error
+            else:
+                print response.body
+            ioloop.IOLoop.instance().stop()
+
+        http_client = httpclient.AsyncHTTPClient()
+        http_client.fetch("http://www.google.com/", handle_request)
+        ioloop.IOLoop.instance().start()
+
+    The constructor for this class is magic in several respects:  It actually
+    creates an instance of an implementation-specific subclass, and instances
+    are reused as a kind of pseudo-singleton (one per IOLoop).  The keyword
+    argument force_instance=True can be used to suppress this singleton
+    behavior.  Constructor arguments other than io_loop and force_instance
+    are deprecated.  The implementation subclass as well as arguments to
+    its constructor can be set with the static method configure()
+    """
+    _impl_class = None
+    _impl_kwargs = None
+
+    @classmethod
+    def _async_clients(cls):
+        assert cls is not AsyncHTTPClient, "should only be called on subclasses"
+        if not hasattr(cls, '_async_client_dict'):
+            cls._async_client_dict = weakref.WeakKeyDictionary()
+        return cls._async_client_dict
+
+    def __new__(cls, io_loop=None, max_clients=10, force_instance=False, 
+                **kwargs):
+        io_loop = io_loop or IOLoop.instance()
+        if cls is AsyncHTTPClient:
+            if cls._impl_class is None:
+                from tornado.simple_httpclient import SimpleAsyncHTTPClient
+                AsyncHTTPClient._impl_class = SimpleAsyncHTTPClient
+            impl = AsyncHTTPClient._impl_class
+        else:
+            impl = cls
+        if io_loop in impl._async_clients() and not force_instance:
+            return impl._async_clients()[io_loop]
+        else:
+            instance = super(AsyncHTTPClient, cls).__new__(impl)
+            args = {}
+            if cls._impl_kwargs:
+                args.update(cls._impl_kwargs)
+            args.update(kwargs)
+            instance.initialize(io_loop, max_clients, **args)
+            if not force_instance:
+                impl._async_clients()[io_loop] = instance
+            return instance
+
+    def close(self):
+        """Destroys this http client, freeing any file descriptors used.
+        Not needed in normal use, but may be helpful in unittests that
+        create and destroy http clients.  No other methods may be called
+        on the AsyncHTTPClient after close().
+        """
+        if self._async_clients().get(self.io_loop) is self:
+            del self._async_clients()[self.io_loop]
+
+    def fetch(self, request, callback, **kwargs):
+        """Executes a request, calling callback with an `HTTPResponse`.
+
+        The request may be either a string URL or an `HTTPRequest` object.
+        If it is a string, we construct an `HTTPRequest` using any additional
+        kwargs: ``HTTPRequest(request, **kwargs)``
+
+        If an error occurs during the fetch, the HTTPResponse given to the
+        callback has a non-None error attribute that contains the exception
+        encountered during the request. You can call response.rethrow() to
+        throw the exception (if any) in the callback.
+        """
+        raise NotImplementedError()
+
+    @staticmethod
+    def configure(impl, **kwargs):
+        """Configures the AsyncHTTPClient subclass to use.
+
+        AsyncHTTPClient() actually creates an instance of a subclass.
+        This method may be called with either a class object or the
+        fully-qualified name of such a class (or None to use the default,
+        SimpleAsyncHTTPClient)
+
+        If additional keyword arguments are given, they will be passed
+        to the constructor of each subclass instance created.  The
+        keyword argument max_clients determines the maximum number of
+        simultaneous fetch() operations that can execute in parallel
+        on each IOLoop.  Additional arguments may be supported depending
+        on the implementation class in use.
+
+        Example::
+
+           AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient")
+        """
+        if isinstance(impl, (unicode, bytes_type)):
+            impl = import_object(impl)
+        if impl is not None and not issubclass(impl, AsyncHTTPClient):
+            raise ValueError("Invalid AsyncHTTPClient implementation")
+        AsyncHTTPClient._impl_class = impl
+        AsyncHTTPClient._impl_kwargs = kwargs
+
+class HTTPRequest(object):
+    """HTTP client request object."""
+    def __init__(self, url, method="GET", headers=None, body=None,
+                 auth_username=None, auth_password=None,
+                 connect_timeout=20.0, request_timeout=20.0,
+                 if_modified_since=None, follow_redirects=True,
+                 max_redirects=5, user_agent=None, use_gzip=True,
+                 network_interface=None, streaming_callback=None,
+                 header_callback=None, prepare_curl_callback=None,
+                 proxy_host=None, proxy_port=None, proxy_username=None,
+                 proxy_password='', allow_nonstandard_methods=False,
+                 validate_cert=True, ca_certs=None,
+                 allow_ipv6=None,
+                 client_key=None, client_cert=None):
+        """Creates an `HTTPRequest`.
+
+        All parameters except `url` are optional.
+
+        :arg string url: URL to fetch
+        :arg string method: HTTP method, e.g. "GET" or "POST"
+        :arg headers: Additional HTTP headers to pass on the request
+        :type headers: `~tornado.httputil.HTTPHeaders` or `dict`
+        :arg string auth_username: Username for HTTP "Basic" authentication
+        :arg string auth_password: Password for HTTP "Basic" authentication
+        :arg float connect_timeout: Timeout for initial connection in seconds
+        :arg float request_timeout: Timeout for entire request in seconds
+        :arg datetime if_modified_since: Timestamp for ``If-Modified-Since``
+           header
+        :arg bool follow_redirects: Should redirects be followed automatically
+           or return the 3xx response?
+        :arg int max_redirects: Limit for `follow_redirects`
+        :arg string user_agent: String to send as ``User-Agent`` header
+        :arg bool use_gzip: Request gzip encoding from the server
+        :arg string network_interface: Network interface to use for request
+        :arg callable streaming_callback: If set, `streaming_callback` will
+           be run with each chunk of data as it is received, and 
+           `~HTTPResponse.body` and `~HTTPResponse.buffer` will be empty in 
+           the final response.
+        :arg callable header_callback: If set, `header_callback` will
+           be run with each header line as it is received, and 
+           `~HTTPResponse.headers` will be empty in the final response.
+        :arg callable prepare_curl_callback: If set, will be called with
+           a `pycurl.Curl` object to allow the application to make additional
+           `setopt` calls.
+        :arg string proxy_host: HTTP proxy hostname.  To use proxies, 
+           `proxy_host` and `proxy_port` must be set; `proxy_username` and 
+           `proxy_pass` are optional.  Proxies are currently only support 
+           with `curl_httpclient`.
+        :arg int proxy_port: HTTP proxy port
+        :arg string proxy_username: HTTP proxy username
+        :arg string proxy_password: HTTP proxy password
+        :arg bool allow_nonstandard_methods: Allow unknown values for `method` 
+           argument?
+        :arg bool validate_cert: For HTTPS requests, validate the server's
+           certificate?
+        :arg string ca_certs: filename of CA certificates in PEM format,
+           or None to use defaults.  Note that in `curl_httpclient`, if
+           any request uses a custom `ca_certs` file, they all must (they
+           don't have to all use the same `ca_certs`, but it's not possible
+           to mix requests with ca_certs and requests that use the defaults.
+        :arg bool allow_ipv6: Use IPv6 when available?  Default is false in 
+           `simple_httpclient` and true in `curl_httpclient`
+        :arg string client_key: Filename for client SSL key, if any
+        :arg string client_cert: Filename for client SSL certificate, if any
+        """
+        if headers is None:
+            headers = httputil.HTTPHeaders()
+        if if_modified_since:
+            timestamp = calendar.timegm(if_modified_since.utctimetuple())
+            headers["If-Modified-Since"] = email.utils.formatdate(
+                timestamp, localtime=False, usegmt=True)
+        self.proxy_host = proxy_host
+        self.proxy_port = proxy_port
+        self.proxy_username = proxy_username
+        self.proxy_password = proxy_password
+        self.url = url
+        self.method = method
+        self.headers = headers
+        self.body = utf8(body)
+        self.auth_username = auth_username
+        self.auth_password = auth_password
+        self.connect_timeout = connect_timeout
+        self.request_timeout = request_timeout
+        self.follow_redirects = follow_redirects
+        self.max_redirects = max_redirects
+        self.user_agent = user_agent
+        self.use_gzip = use_gzip
+        self.network_interface = network_interface
+        self.streaming_callback = streaming_callback
+        self.header_callback = header_callback
+        self.prepare_curl_callback = prepare_curl_callback
+        self.allow_nonstandard_methods = allow_nonstandard_methods
+        self.validate_cert = validate_cert
+        self.ca_certs = ca_certs
+        self.allow_ipv6 = allow_ipv6
+        self.client_key = client_key
+        self.client_cert = client_cert
+        self.start_time = time.time()
+
+
+class HTTPResponse(object):
+    """HTTP Response object.
+
+    Attributes:
+
+    * request: HTTPRequest object
+
+    * code: numeric HTTP status code, e.g. 200 or 404
+
+    * headers: httputil.HTTPHeaders object
+
+    * buffer: cStringIO object for response body
+
+    * body: respose body as string (created on demand from self.buffer)
+
+    * error: Exception object, if any
+
+    * request_time: seconds from request start to finish
+
+    * time_info: dictionary of diagnostic timing information from the request.
+        Available data are subject to change, but currently uses timings
+        available from http://curl.haxx.se/libcurl/c/curl_easy_getinfo.html,
+        plus 'queue', which is the delay (if any) introduced by waiting for
+        a slot under AsyncHTTPClient's max_clients setting.
+    """
+    def __init__(self, request, code, headers={}, buffer=None,
+                 effective_url=None, error=None, request_time=None,
+                 time_info={}):
+        self.request = request
+        self.code = code
+        self.headers = headers
+        self.buffer = buffer
+        self._body = None
+        if effective_url is None:
+            self.effective_url = request.url
+        else:
+            self.effective_url = effective_url
+        if error is None:
+            if self.code < 200 or self.code >= 300:
+                self.error = HTTPError(self.code, response=self)
+            else:
+                self.error = None
+        else:
+            self.error = error
+        self.request_time = request_time
+        self.time_info = time_info
+
+    def _get_body(self):
+        if self.buffer is None:
+            return None
+        elif self._body is None:
+            self._body = self.buffer.getvalue()
+
+        return self._body
+
+    body = property(_get_body)
+
+    def rethrow(self):
+        """If there was an error on the request, raise an `HTTPError`."""
+        if self.error:
+            raise self.error
+
+    def __repr__(self):
+        args = ",".join("%s=%r" % i for i in self.__dict__.iteritems())
+        return "%s(%s)" % (self.__class__.__name__, args)
+
+
+class HTTPError(Exception):
+    """Exception thrown for an unsuccessful HTTP request.
+
+    Attributes:
+
+    code - HTTP error integer error code, e.g. 404.  Error code 599 is
+           used when no HTTP response was received, e.g. for a timeout.
+
+    response - HTTPResponse object, if any.
+
+    Note that if follow_redirects is False, redirects become HTTPErrors,
+    and you can look at error.response.headers['Location'] to see the
+    destination of the redirect.
+    """
+    def __init__(self, code, message=None, response=None):
+        self.code = code
+        message = message or httplib.responses.get(code, "Unknown")
+        self.response = response
+        Exception.__init__(self, "HTTP %d: %s" % (self.code, message))
+
+
+def main():
+    from tornado.options import define, options, parse_command_line
+    define("print_headers", type=bool, default=False)
+    define("print_body", type=bool, default=True)
+    define("follow_redirects", type=bool, default=True)
+    define("validate_cert", type=bool, default=True)
+    args = parse_command_line()
+    client = HTTPClient()
+    for arg in args:
+        try:
+            response = client.fetch(arg,
+                                    follow_redirects=options.follow_redirects,
+                                    validate_cert=options.validate_cert,
+                                    )
+        except HTTPError, e:
+            if e.response is not None:
+                response = e.response
+            else:
+                raise
+        if options.print_headers:
+            print response.headers
+        if options.print_body:
+            print response.body
+    client.close()
+
+if __name__ == "__main__":
+    main()
diff --git a/tornado/httpserver.py b/tornado/httpserver.py
new file mode 100644
index 0000000..e24c376
--- /dev/null
+++ b/tornado/httpserver.py
@@ -0,0 +1,476 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""A non-blocking, single-threaded HTTP server.
+
+Typical applications have little direct interaction with the `HTTPServer`
+class except to start a server at the beginning of the process
+(and even that is often done indirectly via `tornado.web.Application.listen`).
+
+This module also defines the `HTTPRequest` class which is exposed via
+`tornado.web.RequestHandler.request`.
+"""
+
+import Cookie
+import logging
+import socket
+import time
+import urlparse
+
+from tornado.escape import utf8, native_str, parse_qs_bytes
+from tornado import httputil
+from tornado import iostream
+from tornado.netutil import TCPServer
+from tornado import stack_context
+from tornado.util import b, bytes_type
+
+try:
+    import ssl # Python 2.6+
+except ImportError:
+    ssl = None
+
+class HTTPServer(TCPServer):
+    r"""A non-blocking, single-threaded HTTP server.
+
+    A server is defined by a request callback that takes an HTTPRequest
+    instance as an argument and writes a valid HTTP response with
+    `HTTPRequest.write`. `HTTPRequest.finish` finishes the request (but does
+    not necessarily close the connection in the case of HTTP/1.1 keep-alive
+    requests). A simple example server that echoes back the URI you
+    requested::
+
+        import httpserver
+        import ioloop
+
+        def handle_request(request):
+           message = "You requested %s\n" % request.uri
+           request.write("HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\n%s" % (
+                         len(message), message))
+           request.finish()
+
+        http_server = httpserver.HTTPServer(handle_request)
+        http_server.listen(8888)
+        ioloop.IOLoop.instance().start()
+
+    `HTTPServer` is a very basic connection handler. Beyond parsing the
+    HTTP request body and headers, the only HTTP semantics implemented
+    in `HTTPServer` is HTTP/1.1 keep-alive connections. We do not, however,
+    implement chunked encoding, so the request callback must provide a
+    ``Content-Length`` header or implement chunked encoding for HTTP/1.1
+    requests for the server to run correctly for HTTP/1.1 clients. If
+    the request handler is unable to do this, you can provide the
+    ``no_keep_alive`` argument to the `HTTPServer` constructor, which will
+    ensure the connection is closed on every request no matter what HTTP
+    version the client is using.
+
+    If ``xheaders`` is ``True``, we support the ``X-Real-Ip`` and ``X-Scheme``
+    headers, which override the remote IP and HTTP scheme for all requests.
+    These headers are useful when running Tornado behind a reverse proxy or
+    load balancer.
+
+    `HTTPServer` can serve SSL traffic with Python 2.6+ and OpenSSL.
+    To make this server serve SSL traffic, send the ssl_options dictionary
+    argument with the arguments required for the `ssl.wrap_socket` method,
+    including "certfile" and "keyfile"::
+
+       HTTPServer(applicaton, ssl_options={
+           "certfile": os.path.join(data_dir, "mydomain.crt"),
+           "keyfile": os.path.join(data_dir, "mydomain.key"),
+       })
+
+    `HTTPServer` initialization follows one of three patterns (the
+    initialization methods are defined on `tornado.netutil.TCPServer`):
+
+    1. `~tornado.netutil.TCPServer.listen`: simple single-process::
+
+            server = HTTPServer(app)
+            server.listen(8888)
+            IOLoop.instance().start()
+
+       In many cases, `tornado.web.Application.listen` can be used to avoid
+       the need to explicitly create the `HTTPServer`.
+
+    2. `~tornado.netutil.TCPServer.bind`/`~tornado.netutil.TCPServer.start`: 
+       simple multi-process::
+
+            server = HTTPServer(app)
+            server.bind(8888)
+            server.start(0)  # Forks multiple sub-processes
+            IOLoop.instance().start()
+
+       When using this interface, an `IOLoop` must *not* be passed
+       to the `HTTPServer` constructor.  `start` will always start
+       the server on the default singleton `IOLoop`.
+
+    3. `~tornado.netutil.TCPServer.add_sockets`: advanced multi-process::
+
+            sockets = tornado.netutil.bind_sockets(8888)
+            tornado.process.fork_processes(0)
+            server = HTTPServer(app)
+            server.add_sockets(sockets)
+            IOLoop.instance().start()
+
+       The `add_sockets` interface is more complicated, but it can be
+       used with `tornado.process.fork_processes` to give you more
+       flexibility in when the fork happens.  `add_sockets` can
+       also be used in single-process servers if you want to create
+       your listening sockets in some way other than
+       `tornado.netutil.bind_sockets`.
+
+    """
+    def __init__(self, request_callback, no_keep_alive=False, io_loop=None,
+                 xheaders=False, ssl_options=None, **kwargs):
+        self.request_callback = request_callback
+        self.no_keep_alive = no_keep_alive
+        self.xheaders = xheaders
+        TCPServer.__init__(self, io_loop=io_loop, ssl_options=ssl_options,
+                           **kwargs)
+
+    def handle_stream(self, stream, address):
+        HTTPConnection(stream, address, self.request_callback,
+                       self.no_keep_alive, self.xheaders)
+
+class _BadRequestException(Exception):
+    """Exception class for malformed HTTP requests."""
+    pass
+
+class HTTPConnection(object):
+    """Handles a connection to an HTTP client, executing HTTP requests.
+
+    We parse HTTP headers and bodies, and execute the request callback
+    until the HTTP conection is closed.
+    """
+    def __init__(self, stream, address, request_callback, no_keep_alive=False,
+                 xheaders=False):
+        self.stream = stream
+        if self.stream.socket.family not in (socket.AF_INET, socket.AF_INET6):
+            # Unix (or other) socket; fake the remote address
+            address = ('0.0.0.0', 0)
+        self.address = address
+        self.request_callback = request_callback
+        self.no_keep_alive = no_keep_alive
+        self.xheaders = xheaders
+        self._request = None
+        self._request_finished = False
+        # Save stack context here, outside of any request.  This keeps
+        # contexts from one request from leaking into the next.
+        self._header_callback = stack_context.wrap(self._on_headers)
+        self.stream.read_until(b("\r\n\r\n"), self._header_callback)
+        self._write_callback = None
+
+    def write(self, chunk, callback=None):
+        """Writes a chunk of output to the stream."""
+        assert self._request, "Request closed"
+        if not self.stream.closed():
+            self._write_callback = stack_context.wrap(callback)
+            self.stream.write(chunk, self._on_write_complete)
+
+    def finish(self):
+        """Finishes the request."""
+        assert self._request, "Request closed"
+        self._request_finished = True
+        if not self.stream.writing():
+            self._finish_request()
+
+    def _on_write_complete(self):
+        if self._write_callback is not None:
+            callback = self._write_callback
+            self._write_callback = None
+            callback()            
+        # _on_write_complete is enqueued on the IOLoop whenever the
+        # IOStream's write buffer becomes empty, but it's possible for
+        # another callback that runs on the IOLoop before it to
+        # simultaneously write more data and finish the request.  If
+        # there is still data in the IOStream, a future
+        # _on_write_complete will be responsible for calling
+        # _finish_request.
+        if self._request_finished and not self.stream.writing():
+            self._finish_request()
+
+    def _finish_request(self):
+        if self.no_keep_alive:
+            disconnect = True
+        else:
+            connection_header = self._request.headers.get("Connection")
+            if connection_header is not None:
+                connection_header = connection_header.lower()
+            if self._request.supports_http_1_1():
+                disconnect = connection_header == "close"
+            elif ("Content-Length" in self._request.headers
+                    or self._request.method in ("HEAD", "GET")):
+                disconnect = connection_header != "keep-alive"
+            else:
+                disconnect = True
+        self._request = None
+        self._request_finished = False
+        if disconnect:
+            self.stream.close()
+            return
+        self.stream.read_until(b("\r\n\r\n"), self._header_callback)
+
+    def _on_headers(self, data):
+        try:
+            data = native_str(data.decode('latin1'))
+            eol = data.find("\r\n")
+            start_line = data[:eol]
+            try:
+                method, uri, version = start_line.split(" ")
+            except ValueError:
+                raise _BadRequestException("Malformed HTTP request line")
+            if not version.startswith("HTTP/"):
+                raise _BadRequestException("Malformed HTTP version in HTTP Request-Line")
+            headers = httputil.HTTPHeaders.parse(data[eol:])
+            self._request = HTTPRequest(
+                connection=self, method=method, uri=uri, version=version,
+                headers=headers, remote_ip=self.address[0])
+
+            content_length = headers.get("Content-Length")
+            if content_length:
+                content_length = int(content_length)
+                if content_length > self.stream.max_buffer_size:
+                    raise _BadRequestException("Content-Length too long")
+                if headers.get("Expect") == "100-continue":
+                    self.stream.write(b("HTTP/1.1 100 (Continue)\r\n\r\n"))
+                self.stream.read_bytes(content_length, self._on_request_body)
+                return
+
+            self.request_callback(self._request)
+        except _BadRequestException, e:
+            logging.info("Malformed HTTP request from %s: %s",
+                         self.address[0], e)
+            self.stream.close()
+            return
+
+    def _on_request_body(self, data):
+        self._request.body = data
+        content_type = self._request.headers.get("Content-Type", "")
+        if self._request.method in ("POST", "PUT"):
+            if content_type.startswith("application/x-www-form-urlencoded"):
+                arguments = parse_qs_bytes(native_str(self._request.body))
+                for name, values in arguments.iteritems():
+                    values = [v for v in values if v]
+                    if values:
+                        self._request.arguments.setdefault(name, []).extend(
+                            values)
+            elif content_type.startswith("multipart/form-data"):
+                fields = content_type.split(";")
+                for field in fields:
+                    k, sep, v = field.strip().partition("=")
+                    if k == "boundary" and v:
+                        httputil.parse_multipart_form_data(
+                            utf8(v), data,
+                            self._request.arguments,
+                            self._request.files)
+                        break
+                else:
+                    logging.warning("Invalid multipart/form-data")
+        self.request_callback(self._request)
+
+
+class HTTPRequest(object):
+    """A single HTTP request.
+
+    All attributes are type `str` unless otherwise noted.
+
+    .. attribute:: method
+
+       HTTP request method, e.g. "GET" or "POST"
+
+    .. attribute:: uri
+
+       The requested uri.
+
+    .. attribute:: path
+
+       The path portion of `uri`
+
+    .. attribute:: query
+
+       The query portion of `uri`
+
+    .. attribute:: version
+
+       HTTP version specified in request, e.g. "HTTP/1.1"
+
+    .. attribute:: headers
+
+       `HTTPHeader` dictionary-like object for request headers.  Acts like
+       a case-insensitive dictionary with additional methods for repeated
+       headers.
+
+    .. attribute:: body
+
+       Request body, if present, as a byte string.
+
+    .. attribute:: remote_ip
+
+       Client's IP address as a string.  If `HTTPServer.xheaders` is set,
+       will pass along the real IP address provided by a load balancer
+       in the ``X-Real-Ip`` header
+
+    .. attribute:: protocol
+
+       The protocol used, either "http" or "https".  If `HTTPServer.xheaders`
+       is set, will pass along the protocol used by a load balancer if
+       reported via an ``X-Scheme`` header.
+
+    .. attribute:: host
+
+       The requested hostname, usually taken from the ``Host`` header.
+
+    .. attribute:: arguments
+
+       GET/POST arguments are available in the arguments property, which
+       maps arguments names to lists of values (to support multiple values
+       for individual names). Names are of type `str`, while arguments
+       are byte strings.  Note that this is different from 
+       `RequestHandler.get_argument`, which returns argument values as 
+       unicode strings.
+
+    .. attribute:: files
+
+       File uploads are available in the files property, which maps file
+       names to lists of :class:`HTTPFile`.
+
+    .. attribute:: connection
+
+       An HTTP request is attached to a single HTTP connection, which can
+       be accessed through the "connection" attribute. Since connections
+       are typically kept open in HTTP/1.1, multiple requests can be handled
+       sequentially on a single connection.
+    """
+    def __init__(self, method, uri, version="HTTP/1.0", headers=None,
+                 body=None, remote_ip=None, protocol=None, host=None,
+                 files=None, connection=None):
+        self.method = method
+        self.uri = uri
+        self.version = version
+        self.headers = headers or httputil.HTTPHeaders()
+        self.body = body or ""
+        if connection and connection.xheaders:
+            # Squid uses X-Forwarded-For, others use X-Real-Ip
+            self.remote_ip = self.headers.get(
+                "X-Real-Ip", self.headers.get("X-Forwarded-For", remote_ip))
+            if not self._valid_ip(self.remote_ip):
+                self.remote_ip = remote_ip
+            # AWS uses X-Forwarded-Proto
+            self.protocol = self.headers.get(
+                "X-Scheme", self.headers.get("X-Forwarded-Proto", protocol))
+            if self.protocol not in ("http", "https"):
+                self.protocol = "http"
+        else:
+            self.remote_ip = remote_ip
+            if protocol:
+                self.protocol = protocol
+            elif connection and isinstance(connection.stream, 
+                                           iostream.SSLIOStream):
+                self.protocol = "https"
+            else:
+                self.protocol = "http"
+        self.host = host or self.headers.get("Host") or "127.0.0.1"
+        self.files = files or {}
+        self.connection = connection
+        self._start_time = time.time()
+        self._finish_time = None
+
+        scheme, netloc, path, query, fragment = urlparse.urlsplit(native_str(uri))
+        self.path = path
+        self.query = query
+        arguments = parse_qs_bytes(query)
+        self.arguments = {}
+        for name, values in arguments.iteritems():
+            values = [v for v in values if v]
+            if values: self.arguments[name] = values
+
+    def supports_http_1_1(self):
+        """Returns True if this request supports HTTP/1.1 semantics"""
+        return self.version == "HTTP/1.1"
+
+    @property
+    def cookies(self):
+        """A dictionary of Cookie.Morsel objects."""
+        if not hasattr(self, "_cookies"):
+            self._cookies = Cookie.SimpleCookie()
+            if "Cookie" in self.headers:
+                try:
+                    self._cookies.load(
+                        native_str(self.headers["Cookie"]))
+                except Exception:
+                    self._cookies = {}
+        return self._cookies
+
+    def write(self, chunk, callback=None):
+        """Writes the given chunk to the response stream."""
+        assert isinstance(chunk, bytes_type)
+        self.connection.write(chunk, callback=callback)
+
+    def finish(self):
+        """Finishes this HTTP request on the open connection."""
+        self.connection.finish()
+        self._finish_time = time.time()
+
+    def full_url(self):
+        """Reconstructs the full URL for this request."""
+        return self.protocol + "://" + self.host + self.uri
+
+    def request_time(self):
+        """Returns the amount of time it took for this request to execute."""
+        if self._finish_time is None:
+            return time.time() - self._start_time
+        else:
+            return self._finish_time - self._start_time
+
+    def get_ssl_certificate(self):
+        """Returns the client's SSL certificate, if any.
+
+        To use client certificates, the HTTPServer must have been constructed
+        with cert_reqs set in ssl_options, e.g.::
+
+            server = HTTPServer(app,
+                ssl_options=dict(
+                    certfile="foo.crt",
+                    keyfile="foo.key",
+                    cert_reqs=ssl.CERT_REQUIRED,
+                    ca_certs="cacert.crt"))
+
+        The return value is a dictionary, see SSLSocket.getpeercert() in
+        the standard library for more details.
+        http://docs.python.org/library/ssl.html#sslsocket-objects
+        """
+        try:
+            return self.connection.stream.socket.getpeercert()
+        except ssl.SSLError:
+            return None
+
+    def __repr__(self):
+        attrs = ("protocol", "host", "method", "uri", "version", "remote_ip",
+                 "body")
+        args = ", ".join(["%s=%r" % (n, getattr(self, n)) for n in attrs])
+        return "%s(%s, headers=%s)" % (
+            self.__class__.__name__, args, dict(self.headers))
+
+    def _valid_ip(self, ip):
+        try:
+            res = socket.getaddrinfo(ip, 0, socket.AF_UNSPEC,
+                                     socket.SOCK_STREAM,
+                                     0, socket.AI_NUMERICHOST)
+            return bool(res)
+        except socket.gaierror, e:
+            if e.args[0] == socket.EAI_NONAME:
+                return False
+            raise
+        return True
+
diff --git a/tornado/httputil.py b/tornado/httputil.py
new file mode 100644
index 0000000..8aec4b4
--- /dev/null
+++ b/tornado/httputil.py
@@ -0,0 +1,280 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""HTTP utility code shared by clients and servers."""
+
+import logging
+import urllib
+import re
+
+from tornado.util import b, ObjectDict
+
+class HTTPHeaders(dict):
+    """A dictionary that maintains Http-Header-Case for all keys.
+
+    Supports multiple values per key via a pair of new methods,
+    add() and get_list().  The regular dictionary interface returns a single
+    value per key, with multiple values joined by a comma.
+
+    >>> h = HTTPHeaders({"content-type": "text/html"})
+    >>> h.keys()
+    ['Content-Type']
+    >>> h["Content-Type"]
+    'text/html'
+
+    >>> h.add("Set-Cookie", "A=B")
+    >>> h.add("Set-Cookie", "C=D")
+    >>> h["set-cookie"]
+    'A=B,C=D'
+    >>> h.get_list("set-cookie")
+    ['A=B', 'C=D']
+
+    >>> for (k,v) in sorted(h.get_all()):
+    ...    print '%s: %s' % (k,v)
+    ...
+    Content-Type: text/html
+    Set-Cookie: A=B
+    Set-Cookie: C=D
+    """
+    def __init__(self, *args, **kwargs):
+        # Don't pass args or kwargs to dict.__init__, as it will bypass
+        # our __setitem__
+        dict.__init__(self)
+        self._as_list = {}
+        self._last_key = None
+        self.update(*args, **kwargs)
+
+    # new public methods
+
+    def add(self, name, value):
+        """Adds a new value for the given key."""
+        norm_name = HTTPHeaders._normalize_name(name)
+        self._last_key = norm_name
+        if norm_name in self:
+            # bypass our override of __setitem__ since it modifies _as_list
+            dict.__setitem__(self, norm_name, self[norm_name] + ',' + value)
+            self._as_list[norm_name].append(value)
+        else:
+            self[norm_name] = value
+
+    def get_list(self, name):
+        """Returns all values for the given header as a list."""
+        norm_name = HTTPHeaders._normalize_name(name)
+        return self._as_list.get(norm_name, [])
+
+    def get_all(self):
+        """Returns an iterable of all (name, value) pairs.
+
+        If a header has multiple values, multiple pairs will be
+        returned with the same name.
+        """
+        for name, list in self._as_list.iteritems():
+            for value in list:
+                yield (name, value)
+
+    def parse_line(self, line):
+        """Updates the dictionary with a single header line.
+
+        >>> h = HTTPHeaders()
+        >>> h.parse_line("Content-Type: text/html")
+        >>> h.get('content-type')
+        'text/html'
+        """
+        if line[0].isspace():
+            # continuation of a multi-line header
+            new_part = ' ' + line.lstrip()
+            self._as_list[self._last_key][-1] += new_part
+            dict.__setitem__(self, self._last_key,
+                             self[self._last_key] + new_part)
+        else:
+            name, value = line.split(":", 1)
+            self.add(name, value.strip())
+
+    @classmethod
+    def parse(cls, headers):
+        """Returns a dictionary from HTTP header text.
+
+        >>> h = HTTPHeaders.parse("Content-Type: text/html\\r\\nContent-Length: 42\\r\\n")
+        >>> sorted(h.iteritems())
+        [('Content-Length', '42'), ('Content-Type', 'text/html')]
+        """
+        h = cls()
+        for line in headers.splitlines():
+            if line:
+                h.parse_line(line)
+        return h
+
+    # dict implementation overrides
+
+    def __setitem__(self, name, value):
+        norm_name = HTTPHeaders._normalize_name(name)
+        dict.__setitem__(self, norm_name, value)
+        self._as_list[norm_name] = [value]
+
+    def __getitem__(self, name):
+        return dict.__getitem__(self, HTTPHeaders._normalize_name(name))
+
+    def __delitem__(self, name):
+        norm_name = HTTPHeaders._normalize_name(name)
+        dict.__delitem__(self, norm_name)
+        del self._as_list[norm_name]
+
+    def __contains__(self, name):
+        norm_name = HTTPHeaders._normalize_name(name)
+        return dict.__contains__(self, norm_name)
+
+    def get(self, name, default=None):
+        return dict.get(self, HTTPHeaders._normalize_name(name), default)
+
+    def update(self, *args, **kwargs):
+        # dict.update bypasses our __setitem__
+        for k, v in dict(*args, **kwargs).iteritems():
+            self[k] = v
+
+    _NORMALIZED_HEADER_RE = re.compile(r'^[A-Z0-9][a-z0-9]*(-[A-Z0-9][a-z0-9]*)*$')
+    _normalized_headers = {}
+
+    @staticmethod
+    def _normalize_name(name):
+        """Converts a name to Http-Header-Case.
+
+        >>> HTTPHeaders._normalize_name("coNtent-TYPE")
+        'Content-Type'
+        """
+        try:
+            return HTTPHeaders._normalized_headers[name]
+        except KeyError:
+            if HTTPHeaders._NORMALIZED_HEADER_RE.match(name):
+                normalized = name
+            else:
+                normalized = "-".join([w.capitalize() for w in name.split("-")])
+            HTTPHeaders._normalized_headers[name] = normalized
+            return normalized
+
+
+def url_concat(url, args):
+    """Concatenate url and argument dictionary regardless of whether
+    url has existing query parameters.
+
+    >>> url_concat("http://example.com/foo?a=b", dict(c="d"))
+    'http://example.com/foo?a=b&c=d'
+    """
+    if not args: return url
+    if url[-1] not in ('?', '&'):
+        url += '&' if ('?' in url) else '?'
+    return url + urllib.urlencode(args)
+
+
+class HTTPFile(ObjectDict):
+    """Represents an HTTP file. For backwards compatibility, its instance
+    attributes are also accessible as dictionary keys.
+
+    :ivar filename:
+    :ivar body:
+    :ivar content_type: The content_type comes from the provided HTTP header
+        and should not be trusted outright given that it can be easily forged.
+    """
+    pass
+
+
+def parse_multipart_form_data(boundary, data, arguments, files):
+    """Parses a multipart/form-data body.
+
+    The boundary and data parameters are both byte strings.
+    The dictionaries given in the arguments and files parameters
+    will be updated with the contents of the body.
+    """
+    # The standard allows for the boundary to be quoted in the header,
+    # although it's rare (it happens at least for google app engine
+    # xmpp).  I think we're also supposed to handle backslash-escapes
+    # here but I'll save that until we see a client that uses them
+    # in the wild.
+    if boundary.startswith(b('"')) and boundary.endswith(b('"')):
+        boundary = boundary[1:-1]
+    if data.endswith(b("\r\n")):
+        footer_length = len(boundary) + 6
+    else:
+        footer_length = len(boundary) + 4
+    parts = data[:-footer_length].split(b("--") + boundary + b("\r\n"))
+    for part in parts:
+        if not part: continue
+        eoh = part.find(b("\r\n\r\n"))
+        if eoh == -1:
+            logging.warning("multipart/form-data missing headers")
+            continue
+        headers = HTTPHeaders.parse(part[:eoh].decode("utf-8"))
+        disp_header = headers.get("Content-Disposition", "")
+        disposition, disp_params = _parse_header(disp_header)
+        if disposition != "form-data" or not part.endswith(b("\r\n")):
+            logging.warning("Invalid multipart/form-data")
+            continue
+        value = part[eoh + 4:-2]
+        if not disp_params.get("name"):
+            logging.warning("multipart/form-data value missing name")
+            continue
+        name = disp_params["name"]
+        if disp_params.get("filename"):
+            ctype = headers.get("Content-Type", "application/unknown")
+            files.setdefault(name, []).append(HTTPFile(
+                filename=disp_params["filename"], body=value,
+                content_type=ctype))
+        else:
+            arguments.setdefault(name, []).append(value)
+
+
+# _parseparam and _parse_header are copied and modified from python2.7's cgi.py
+# The original 2.7 version of this code did not correctly support some
+# combinations of semicolons and double quotes.
+def _parseparam(s):
+    while s[:1] == ';':
+        s = s[1:]
+        end = s.find(';')
+        while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2:
+            end = s.find(';', end + 1)
+        if end < 0:
+            end = len(s)
+        f = s[:end]
+        yield f.strip()
+        s = s[end:]
+
+def _parse_header(line):
+    """Parse a Content-type like header.
+
+    Return the main content-type and a dictionary of options.
+
+    """
+    parts = _parseparam(';' + line)
+    key = parts.next()
+    pdict = {}
+    for p in parts:
+        i = p.find('=')
+        if i >= 0:
+            name = p[:i].strip().lower()
+            value = p[i+1:].strip()
+            if len(value) >= 2 and value[0] == value[-1] == '"':
+                value = value[1:-1]
+                value = value.replace('\\\\', '\\').replace('\\"', '"')
+            pdict[name] = value
+    return key, pdict
+
+
+def doctests():
+    import doctest
+    return doctest.DocTestSuite()
+
+if __name__ == "__main__":
+    import doctest
+    doctest.testmod()
diff --git a/tornado/ioloop.py b/tornado/ioloop.py
new file mode 100644
index 0000000..edd2fec
--- /dev/null
+++ b/tornado/ioloop.py
@@ -0,0 +1,643 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""An I/O event loop for non-blocking sockets.
+
+Typical applications will use a single `IOLoop` object, in the
+`IOLoop.instance` singleton.  The `IOLoop.start` method should usually
+be called at the end of the ``main()`` function.  Atypical applications may
+use more than one `IOLoop`, such as one `IOLoop` per thread, or per `unittest`
+case.
+
+In addition to I/O events, the `IOLoop` can also schedule time-based events.
+`IOLoop.add_timeout` is a non-blocking alternative to `time.sleep`.
+"""
+
+from __future__ import with_statement
+
+import datetime
+import errno
+import heapq
+import os
+import logging
+import select
+import thread
+import threading
+import time
+import traceback
+
+from tornado import stack_context
+
+try:
+    import signal
+except ImportError:
+    signal = None
+
+from tornado.platform.auto import set_close_exec, Waker
+
+
+class IOLoop(object):
+    """A level-triggered I/O loop.
+
+    We use epoll (Linux) or kqueue (BSD and Mac OS X; requires python
+    2.6+) if they are available, or else we fall back on select(). If
+    you are implementing a system that needs to handle thousands of
+    simultaneous connections, you should use a system that supports either
+    epoll or queue.
+
+    Example usage for a simple TCP server::
+
+        import errno
+        import functools
+        import ioloop
+        import socket
+
+        def connection_ready(sock, fd, events):
+            while True:
+                try:
+                    connection, address = sock.accept()
+                except socket.error, e:
+                    if e.args[0] not in (errno.EWOULDBLOCK, errno.EAGAIN):
+                        raise
+                    return
+                connection.setblocking(0)
+                handle_connection(connection, address)
+
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
+        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        sock.setblocking(0)
+        sock.bind(("", port))
+        sock.listen(128)
+
+        io_loop = ioloop.IOLoop.instance()
+        callback = functools.partial(connection_ready, sock)
+        io_loop.add_handler(sock.fileno(), callback, io_loop.READ)
+        io_loop.start()
+
+    """
+    # Constants from the epoll module
+    _EPOLLIN = 0x001
+    _EPOLLPRI = 0x002
+    _EPOLLOUT = 0x004
+    _EPOLLERR = 0x008
+    _EPOLLHUP = 0x010
+    _EPOLLRDHUP = 0x2000
+    _EPOLLONESHOT = (1 << 30)
+    _EPOLLET = (1 << 31)
+
+    # Our events map exactly to the epoll events
+    NONE = 0
+    READ = _EPOLLIN
+    WRITE = _EPOLLOUT
+    ERROR = _EPOLLERR | _EPOLLHUP
+
+    def __init__(self, impl=None):
+        self._impl = impl or _poll()
+        if hasattr(self._impl, 'fileno'):
+            set_close_exec(self._impl.fileno())
+        self._handlers = {}
+        self._events = {}
+        self._callbacks = []
+        self._callback_lock = threading.Lock()
+        self._timeouts = []
+        self._running = False
+        self._stopped = False
+        self._thread_ident = None
+        self._blocking_signal_threshold = None
+
+        # Create a pipe that we send bogus data to when we want to wake
+        # the I/O loop when it is idle
+        self._waker = Waker()
+        self.add_handler(self._waker.fileno(),
+                         lambda fd, events: self._waker.consume(),
+                         self.READ)
+
+    @staticmethod
+    def instance():
+        """Returns a global IOLoop instance.
+
+        Most single-threaded applications have a single, global IOLoop.
+        Use this method instead of passing around IOLoop instances
+        throughout your code.
+
+        A common pattern for classes that depend on IOLoops is to use
+        a default argument to enable programs with multiple IOLoops
+        but not require the argument for simpler applications::
+
+            class MyClass(object):
+                def __init__(self, io_loop=None):
+                    self.io_loop = io_loop or IOLoop.instance()
+        """
+        if not hasattr(IOLoop, "_instance"):
+            IOLoop._instance = IOLoop()
+        return IOLoop._instance
+
+    @staticmethod
+    def initialized():
+        """Returns true if the singleton instance has been created."""
+        return hasattr(IOLoop, "_instance")
+
+    def install(self):
+        """Installs this IOloop object as the singleton instance.
+
+        This is normally not necessary as `instance()` will create
+        an IOLoop on demand, but you may want to call `install` to use
+        a custom subclass of IOLoop.
+        """
+        assert not IOLoop.initialized()
+        IOLoop._instance = self
+
+    def close(self, all_fds=False):
+        """Closes the IOLoop, freeing any resources used.
+
+        If ``all_fds`` is true, all file descriptors registered on the
+        IOLoop will be closed (not just the ones created by the IOLoop itself.
+        """
+        self.remove_handler(self._waker.fileno())
+        if all_fds:
+            for fd in self._handlers.keys()[:]:
+                try:
+                    os.close(fd)
+                except Exception:
+                    logging.debug("error closing fd %s", fd, exc_info=True)
+        self._waker.close()
+        self._impl.close()
+
+    def add_handler(self, fd, handler, events):
+        """Registers the given handler to receive the given events for fd."""
+        self._handlers[fd] = stack_context.wrap(handler)
+        self._impl.register(fd, events | self.ERROR)
+
+    def update_handler(self, fd, events):
+        """Changes the events we listen for fd."""
+        self._impl.modify(fd, events | self.ERROR)
+
+    def remove_handler(self, fd):
+        """Stop listening for events on fd."""
+        self._handlers.pop(fd, None)
+        self._events.pop(fd, None)
+        try:
+            self._impl.unregister(fd)
+        except (OSError, IOError):
+            logging.debug("Error deleting fd from IOLoop", exc_info=True)
+
+    def set_blocking_signal_threshold(self, seconds, action):
+        """Sends a signal if the ioloop is blocked for more than s seconds.
+
+        Pass seconds=None to disable.  Requires python 2.6 on a unixy
+        platform.
+
+        The action parameter is a python signal handler.  Read the
+        documentation for the python 'signal' module for more information.
+        If action is None, the process will be killed if it is blocked for
+        too long.
+        """
+        if not hasattr(signal, "setitimer"):
+            logging.error("set_blocking_signal_threshold requires a signal module "
+                       "with the setitimer method")
+            return
+        self._blocking_signal_threshold = seconds
+        if seconds is not None:
+            signal.signal(signal.SIGALRM,
+                          action if action is not None else signal.SIG_DFL)
+
+    def set_blocking_log_threshold(self, seconds):
+        """Logs a stack trace if the ioloop is blocked for more than s seconds.
+        Equivalent to set_blocking_signal_threshold(seconds, self.log_stack)
+        """
+        self.set_blocking_signal_threshold(seconds, self.log_stack)
+
+    def log_stack(self, signal, frame):
+        """Signal handler to log the stack trace of the current thread.
+
+        For use with set_blocking_signal_threshold.
+        """
+        logging.warning('IOLoop blocked for %f seconds in\n%s',
+                        self._blocking_signal_threshold,
+                        ''.join(traceback.format_stack(frame)))
+
+    def start(self):
+        """Starts the I/O loop.
+
+        The loop will run until one of the I/O handlers calls stop(), which
+        will make the loop stop after the current event iteration completes.
+        """
+        if self._stopped:
+            self._stopped = False
+            return
+        self._thread_ident = thread.get_ident()
+        self._running = True
+        while True:
+            poll_timeout = 3600.0
+
+            # Prevent IO event starvation by delaying new callbacks
+            # to the next iteration of the event loop.
+            with self._callback_lock:
+                callbacks = self._callbacks
+                self._callbacks = []
+            for callback in callbacks:
+                self._run_callback(callback)
+
+            if self._timeouts:
+                now = time.time()
+                while self._timeouts:
+                    if self._timeouts[0].callback is None:
+                        # the timeout was cancelled
+                        heapq.heappop(self._timeouts)
+                    elif self._timeouts[0].deadline <= now:
+                        timeout = heapq.heappop(self._timeouts)
+                        self._run_callback(timeout.callback)
+                    else:
+                        seconds = self._timeouts[0].deadline - now
+                        poll_timeout = min(seconds, poll_timeout)
+                        break
+
+            if self._callbacks:
+                # If any callbacks or timeouts called add_callback,
+                # we don't want to wait in poll() before we run them.
+                poll_timeout = 0.0
+
+            if not self._running:
+                break
+
+            if self._blocking_signal_threshold is not None:
+                # clear alarm so it doesn't fire while poll is waiting for
+                # events.
+                signal.setitimer(signal.ITIMER_REAL, 0, 0)
+
+            try:
+                event_pairs = self._impl.poll(poll_timeout)
+            except Exception, e:
+                # Depending on python version and IOLoop implementation,
+                # different exception types may be thrown and there are
+                # two ways EINTR might be signaled:
+                # * e.errno == errno.EINTR
+                # * e.args is like (errno.EINTR, 'Interrupted system call')
+                if (getattr(e, 'errno', None) == errno.EINTR or
+                    (isinstance(getattr(e, 'args', None), tuple) and
+                     len(e.args) == 2 and e.args[0] == errno.EINTR)):
+                    continue
+                else:
+                    raise
+
+            if self._blocking_signal_threshold is not None:
+                signal.setitimer(signal.ITIMER_REAL,
+                                 self._blocking_signal_threshold, 0)
+
+            # Pop one fd at a time from the set of pending fds and run
+            # its handler. Since that handler may perform actions on
+            # other file descriptors, there may be reentrant calls to
+            # this IOLoop that update self._events
+            self._events.update(event_pairs)
+            while self._events:
+                fd, events = self._events.popitem()
+                try:
+                    self._handlers[fd](fd, events)
+                except (OSError, IOError), e:
+                    if e.args[0] == errno.EPIPE:
+                        # Happens when the client closes the connection
+                        pass
+                    else:
+                        logging.error("Exception in I/O handler for fd %s",
+                                      fd, exc_info=True)
+                except Exception:
+                    logging.error("Exception in I/O handler for fd %s",
+                                  fd, exc_info=True)
+        # reset the stopped flag so another start/stop pair can be issued
+        self._stopped = False
+        if self._blocking_signal_threshold is not None:
+            signal.setitimer(signal.ITIMER_REAL, 0, 0)
+
+    def stop(self):
+        """Stop the loop after the current event loop iteration is complete.
+        If the event loop is not currently running, the next call to start()
+        will return immediately.
+
+        To use asynchronous methods from otherwise-synchronous code (such as
+        unit tests), you can start and stop the event loop like this::
+
+          ioloop = IOLoop()
+          async_method(ioloop=ioloop, callback=ioloop.stop)
+          ioloop.start()
+
+        ioloop.start() will return after async_method has run its callback,
+        whether that callback was invoked before or after ioloop.start.
+        """
+        self._running = False
+        self._stopped = True
+        self._waker.wake()
+
+    def running(self):
+        """Returns true if this IOLoop is currently running."""
+        return self._running
+
+    def add_timeout(self, deadline, callback):
+        """Calls the given callback at the time deadline from the I/O loop.
+
+        Returns a handle that may be passed to remove_timeout to cancel.
+
+        ``deadline`` may be a number denoting a unix timestamp (as returned
+        by ``time.time()`` or a ``datetime.timedelta`` object for a deadline
+        relative to the current time.
+
+        Note that it is not safe to call `add_timeout` from other threads.
+        Instead, you must use `add_callback` to transfer control to the
+        IOLoop's thread, and then call `add_timeout` from there.
+        """
+        timeout = _Timeout(deadline, stack_context.wrap(callback))
+        heapq.heappush(self._timeouts, timeout)
+        return timeout
+
+    def remove_timeout(self, timeout):
+        """Cancels a pending timeout.
+
+        The argument is a handle as returned by add_timeout.
+        """
+        # Removing from a heap is complicated, so just leave the defunct
+        # timeout object in the queue (see discussion in
+        # http://docs.python.org/library/heapq.html).
+        # If this turns out to be a problem, we could add a garbage
+        # collection pass whenever there are too many dead timeouts.
+        timeout.callback = None
+
+    def add_callback(self, callback):
+        """Calls the given callback on the next I/O loop iteration.
+
+        It is safe to call this method from any thread at any time.
+        Note that this is the *only* method in IOLoop that makes this
+        guarantee; all other interaction with the IOLoop must be done
+        from that IOLoop's thread.  add_callback() may be used to transfer
+        control from other threads to the IOLoop's thread.
+        """
+        with self._callback_lock:
+            list_empty = not self._callbacks
+            self._callbacks.append(stack_context.wrap(callback))
+        if list_empty and thread.get_ident() != self._thread_ident:
+            # If we're in the IOLoop's thread, we know it's not currently
+            # polling.  If we're not, and we added the first callback to an
+            # empty list, we may need to wake it up (it may wake up on its
+            # own, but an occasional extra wake is harmless).  Waking
+            # up a polling IOLoop is relatively expensive, so we try to
+            # avoid it when we can.
+            self._waker.wake()
+
+    def _run_callback(self, callback):
+        try:
+            callback()
+        except Exception:
+            self.handle_callback_exception(callback)
+
+    def handle_callback_exception(self, callback):
+        """This method is called whenever a callback run by the IOLoop
+        throws an exception.
+
+        By default simply logs the exception as an error.  Subclasses
+        may override this method to customize reporting of exceptions.
+
+        The exception itself is not passed explicitly, but is available
+        in sys.exc_info.
+        """
+        logging.error("Exception in callback %r", callback, exc_info=True)
+
+
+class _Timeout(object):
+    """An IOLoop timeout, a UNIX timestamp and a callback"""
+
+    # Reduce memory overhead when there are lots of pending callbacks
+    __slots__ = ['deadline', 'callback']
+
+    def __init__(self, deadline, callback):
+        if isinstance(deadline, (int, long, float)):
+            self.deadline = deadline
+        elif isinstance(deadline, datetime.timedelta):
+            self.deadline = time.time() + _Timeout.timedelta_to_seconds(deadline)
+        else:
+            raise TypeError("Unsupported deadline %r" % deadline)
+        self.callback = callback
+
+    @staticmethod
+    def timedelta_to_seconds(td):
+        """Equivalent to td.total_seconds() (introduced in python 2.7)."""
+        return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / float(10**6)
+
+    # Comparison methods to sort by deadline, with object id as a tiebreaker
+    # to guarantee a consistent ordering.  The heapq module uses __le__
+    # in python2.5, and __lt__ in 2.6+ (sort() and most other comparisons
+    # use __lt__).
+    def __lt__(self, other):
+        return ((self.deadline, id(self)) <
+                (other.deadline, id(other)))
+
+    def __le__(self, other):
+        return ((self.deadline, id(self)) <=
+                (other.deadline, id(other)))
+
+
+class PeriodicCallback(object):
+    """Schedules the given callback to be called periodically.
+
+    The callback is called every callback_time milliseconds.
+
+    `start` must be called after the PeriodicCallback is created.
+    """
+    def __init__(self, callback, callback_time, io_loop=None):
+        self.callback = callback
+        self.callback_time = callback_time
+        self.io_loop = io_loop or IOLoop.instance()
+        self._running = False
+        self._timeout = None
+
+    def start(self):
+        """Starts the timer."""
+        self._running = True
+        self._next_timeout = time.time()
+        self._schedule_next()
+
+    def stop(self):
+        """Stops the timer."""
+        self._running = False
+        if self._timeout is not None:
+            self.io_loop.remove_timeout(self._timeout)
+            self._timeout = None
+
+    def _run(self):
+        if not self._running: return
+        try:
+            self.callback()
+        except Exception:
+            logging.error("Error in periodic callback", exc_info=True)
+        self._schedule_next()
+
+    def _schedule_next(self):
+        if self._running:
+            current_time = time.time()
+            while self._next_timeout <= current_time:
+                self._next_timeout += self.callback_time / 1000.0
+            self._timeout = self.io_loop.add_timeout(self._next_timeout, self._run)
+
+
+class _EPoll(object):
+    """An epoll-based event loop using our C module for Python 2.5 systems"""
+    _EPOLL_CTL_ADD = 1
+    _EPOLL_CTL_DEL = 2
+    _EPOLL_CTL_MOD = 3
+
+    def __init__(self):
+        self._epoll_fd = epoll.epoll_create()
+
+    def fileno(self):
+        return self._epoll_fd
+
+    def close(self):
+        os.close(self._epoll_fd)
+
+    def register(self, fd, events):
+        epoll.epoll_ctl(self._epoll_fd, self._EPOLL_CTL_ADD, fd, events)
+
+    def modify(self, fd, events):
+        epoll.epoll_ctl(self._epoll_fd, self._EPOLL_CTL_MOD, fd, events)
+
+    def unregister(self, fd):
+        epoll.epoll_ctl(self._epoll_fd, self._EPOLL_CTL_DEL, fd, 0)
+
+    def poll(self, timeout):
+        return epoll.epoll_wait(self._epoll_fd, int(timeout * 1000))
+
+
+class _KQueue(object):
+    """A kqueue-based event loop for BSD/Mac systems."""
+    def __init__(self):
+        self._kqueue = select.kqueue()
+        self._active = {}
+
+    def fileno(self):
+        return self._kqueue.fileno()
+
+    def close(self):
+        self._kqueue.close()
+
+    def register(self, fd, events):
+        self._control(fd, events, select.KQ_EV_ADD)
+        self._active[fd] = events
+
+    def modify(self, fd, events):
+        self.unregister(fd)
+        self.register(fd, events)
+
+    def unregister(self, fd):
+        events = self._active.pop(fd)
+        self._control(fd, events, select.KQ_EV_DELETE)
+
+    def _control(self, fd, events, flags):
+        kevents = []
+        if events & IOLoop.WRITE:
+            kevents.append(select.kevent(
+                    fd, filter=select.KQ_FILTER_WRITE, flags=flags))
+        if events & IOLoop.READ or not kevents:
+            # Always read when there is not a write
+            kevents.append(select.kevent(
+                    fd, filter=select.KQ_FILTER_READ, flags=flags))
+        # Even though control() takes a list, it seems to return EINVAL
+        # on Mac OS X (10.6) when there is more than one event in the list.
+        for kevent in kevents:
+            self._kqueue.control([kevent], 0)
+
+    def poll(self, timeout):
+        kevents = self._kqueue.control(None, 1000, timeout)
+        events = {}
+        for kevent in kevents:
+            fd = kevent.ident
+            if kevent.filter == select.KQ_FILTER_READ:
+                events[fd] = events.get(fd, 0) | IOLoop.READ
+            if kevent.filter == select.KQ_FILTER_WRITE:
+                if kevent.flags & select.KQ_EV_EOF:
+                    # If an asynchronous connection is refused, kqueue
+                    # returns a write event with the EOF flag set.
+                    # Turn this into an error for consistency with the
+                    # other IOLoop implementations.
+                    # Note that for read events, EOF may be returned before
+                    # all data has been consumed from the socket buffer,
+                    # so we only check for EOF on write events.
+                    events[fd] = IOLoop.ERROR
+                else:
+                    events[fd] = events.get(fd, 0) | IOLoop.WRITE
+            if kevent.flags & select.KQ_EV_ERROR:
+                events[fd] = events.get(fd, 0) | IOLoop.ERROR
+        return events.items()
+
+
+class _Select(object):
+    """A simple, select()-based IOLoop implementation for non-Linux systems"""
+    def __init__(self):
+        self.read_fds = set()
+        self.write_fds = set()
+        self.error_fds = set()
+        self.fd_sets = (self.read_fds, self.write_fds, self.error_fds)
+
+    def close(self):
+        pass
+
+    def register(self, fd, events):
+        if events & IOLoop.READ: self.read_fds.add(fd)
+        if events & IOLoop.WRITE: self.write_fds.add(fd)
+        if events & IOLoop.ERROR:
+            self.error_fds.add(fd)
+            # Closed connections are reported as errors by epoll and kqueue,
+            # but as zero-byte reads by select, so when errors are requested
+            # we need to listen for both read and error.
+            self.read_fds.add(fd)
+
+    def modify(self, fd, events):
+        self.unregister(fd)
+        self.register(fd, events)
+
+    def unregister(self, fd):
+        self.read_fds.discard(fd)
+        self.write_fds.discard(fd)
+        self.error_fds.discard(fd)
+
+    def poll(self, timeout):
+        readable, writeable, errors = select.select(
+            self.read_fds, self.write_fds, self.error_fds, timeout)
+        events = {}
+        for fd in readable:
+            events[fd] = events.get(fd, 0) | IOLoop.READ
+        for fd in writeable:
+            events[fd] = events.get(fd, 0) | IOLoop.WRITE
+        for fd in errors:
+            events[fd] = events.get(fd, 0) | IOLoop.ERROR
+        return events.items()
+
+
+# Choose a poll implementation. Use epoll if it is available, fall back to
+# select() for non-Linux platforms
+if hasattr(select, "epoll"):
+    # Python 2.6+ on Linux
+    _poll = select.epoll
+elif hasattr(select, "kqueue"):
+    # Python 2.6+ on BSD or Mac
+    _poll = _KQueue
+else:
+    try:
+        # Linux systems with our C module installed
+        import epoll
+        _poll = _EPoll
+    except Exception:
+        # All other systems
+        import sys
+        if "linux" in sys.platform:
+            logging.warning("epoll module not found; using select()")
+        _poll = _Select
diff --git a/tornado/iostream.py b/tornado/iostream.py
new file mode 100644
index 0000000..db7895f
--- /dev/null
+++ b/tornado/iostream.py
@@ -0,0 +1,728 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""A utility class to write to and read from a non-blocking socket."""
+
+from __future__ import with_statement
+
+import collections
+import errno
+import logging
+import socket
+import sys
+import re
+
+from tornado import ioloop
+from tornado import stack_context
+from tornado.util import b, bytes_type
+
+try:
+    import ssl # Python 2.6+
+except ImportError:
+    ssl = None
+
+class IOStream(object):
+    r"""A utility class to write to and read from a non-blocking socket.
+
+    We support a non-blocking ``write()`` and a family of ``read_*()`` methods.
+    All of the methods take callbacks (since writing and reading are
+    non-blocking and asynchronous). 
+
+    The socket parameter may either be connected or unconnected.  For
+    server operations the socket is the result of calling socket.accept().
+    For client operations the socket is created with socket.socket(),
+    and may either be connected before passing it to the IOStream or
+    connected with IOStream.connect.
+
+    A very simple (and broken) HTTP client using this class::
+
+        from tornado import ioloop
+        from tornado import iostream
+        import socket
+
+        def send_request():
+            stream.write("GET / HTTP/1.0\r\nHost: friendfeed.com\r\n\r\n")
+            stream.read_until("\r\n\r\n", on_headers)
+
+        def on_headers(data):
+            headers = {}
+            for line in data.split("\r\n"):
+               parts = line.split(":")
+               if len(parts) == 2:
+                   headers[parts[0].strip()] = parts[1].strip()
+            stream.read_bytes(int(headers["Content-Length"]), on_body)
+
+        def on_body(data):
+            print data
+            stream.close()
+            ioloop.IOLoop.instance().stop()
+
+        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
+        stream = iostream.IOStream(s)
+        stream.connect(("friendfeed.com", 80), send_request)
+        ioloop.IOLoop.instance().start()
+
+    """
+    def __init__(self, socket, io_loop=None, max_buffer_size=104857600,
+                 read_chunk_size=4096):
+        self.socket = socket
+        self.socket.setblocking(False)
+        self.io_loop = io_loop or ioloop.IOLoop.instance()
+        self.max_buffer_size = max_buffer_size
+        self.read_chunk_size = read_chunk_size
+        self._read_buffer = collections.deque()
+        self._write_buffer = collections.deque()
+        self._read_buffer_size = 0
+        self._write_buffer_frozen = False
+        self._read_delimiter = None
+        self._read_regex = None
+        self._read_bytes = None
+        self._read_until_close = False
+        self._read_callback = None
+        self._streaming_callback = None
+        self._write_callback = None
+        self._close_callback = None
+        self._connect_callback = None
+        self._connecting = False
+        self._state = None
+        self._pending_callbacks = 0
+
+    def connect(self, address, callback=None):
+        """Connects the socket to a remote address without blocking.
+
+        May only be called if the socket passed to the constructor was
+        not previously connected.  The address parameter is in the
+        same format as for socket.connect, i.e. a (host, port) tuple.
+        If callback is specified, it will be called when the
+        connection is completed.
+
+        Note that it is safe to call IOStream.write while the
+        connection is pending, in which case the data will be written
+        as soon as the connection is ready.  Calling IOStream read
+        methods before the socket is connected works on some platforms
+        but is non-portable.
+        """
+        self._connecting = True
+        try:
+            self.socket.connect(address)
+        except socket.error, e:
+            # In non-blocking mode we expect connect() to raise an
+            # exception with EINPROGRESS or EWOULDBLOCK.
+            #
+            # On freebsd, other errors such as ECONNREFUSED may be
+            # returned immediately when attempting to connect to
+            # localhost, so handle them the same way as an error
+            # reported later in _handle_connect.
+            if e.args[0] not in (errno.EINPROGRESS, errno.EWOULDBLOCK):
+                logging.warning("Connect error on fd %d: %s",
+                                self.socket.fileno(), e)
+                self.close()
+                return
+        self._connect_callback = stack_context.wrap(callback)
+        self._add_io_state(self.io_loop.WRITE)
+
+    def read_until_regex(self, regex, callback):
+        """Call callback when we read the given regex pattern."""
+        assert not self._read_callback, "Already reading"
+        self._read_regex = re.compile(regex)
+        self._read_callback = stack_context.wrap(callback)
+        while True:
+            # See if we've already got the data from a previous read
+            if self._read_from_buffer():
+                return
+            self._check_closed()
+            if self._read_to_buffer() == 0:
+                break
+        self._add_io_state(self.io_loop.READ)
+        
+    def read_until(self, delimiter, callback):
+        """Call callback when we read the given delimiter."""
+        assert not self._read_callback, "Already reading"
+        self._read_delimiter = delimiter
+        self._read_callback = stack_context.wrap(callback)
+        while True:
+            # See if we've already got the data from a previous read
+            if self._read_from_buffer():
+                return
+            self._check_closed()
+            if self._read_to_buffer() == 0:
+                break
+        self._add_io_state(self.io_loop.READ)
+
+    def read_bytes(self, num_bytes, callback, streaming_callback=None):
+        """Call callback when we read the given number of bytes.
+
+        If a ``streaming_callback`` is given, it will be called with chunks
+        of data as they become available, and the argument to the final
+        ``callback`` will be empty.
+        """
+        assert not self._read_callback, "Already reading"
+        assert isinstance(num_bytes, (int, long))
+        self._read_bytes = num_bytes
+        self._read_callback = stack_context.wrap(callback)
+        self._streaming_callback = stack_context.wrap(streaming_callback)
+        while True:
+            if self._read_from_buffer():
+                return
+            self._check_closed()
+            if self._read_to_buffer() == 0:
+                break
+        self._add_io_state(self.io_loop.READ)
+
+    def read_until_close(self, callback, streaming_callback=None):
+        """Reads all data from the socket until it is closed.
+
+        If a ``streaming_callback`` is given, it will be called with chunks
+        of data as they become available, and the argument to the final
+        ``callback`` will be empty.
+
+        Subject to ``max_buffer_size`` limit from `IOStream` constructor if
+        a ``streaming_callback`` is not used.
+        """
+        assert not self._read_callback, "Already reading"
+        if self.closed():
+            self._run_callback(callback, self._consume(self._read_buffer_size))
+            return
+        self._read_until_close = True
+        self._read_callback = stack_context.wrap(callback)
+        self._streaming_callback = stack_context.wrap(streaming_callback)
+        self._add_io_state(self.io_loop.READ)
+
+    def write(self, data, callback=None):
+        """Write the given data to this stream.
+
+        If callback is given, we call it when all of the buffered write
+        data has been successfully written to the stream. If there was
+        previously buffered write data and an old write callback, that
+        callback is simply overwritten with this new callback.
+        """
+        assert isinstance(data, bytes_type)
+        self._check_closed()
+        if data:
+            # We use bool(_write_buffer) as a proxy for write_buffer_size>0,
+            # so never put empty strings in the buffer.
+            self._write_buffer.append(data)
+        self._write_callback = stack_context.wrap(callback)
+        self._handle_write()
+        if self._write_buffer:
+            self._add_io_state(self.io_loop.WRITE)
+        self._maybe_add_error_listener()
+
+    def set_close_callback(self, callback):
+        """Call the given callback when the stream is closed."""
+        self._close_callback = stack_context.wrap(callback)
+
+    def close(self):
+        """Close this stream."""
+        if self.socket is not None:
+            if self._read_until_close:
+                callback = self._read_callback
+                self._read_callback = None
+                self._read_until_close = False
+                self._run_callback(callback,
+                                   self._consume(self._read_buffer_size))
+            if self._state is not None:
+                self.io_loop.remove_handler(self.socket.fileno())
+                self._state = None
+            self.socket.close()
+            self.socket = None
+            if self._close_callback and self._pending_callbacks == 0:
+                # if there are pending callbacks, don't run the close callback
+                # until they're done (see _maybe_add_error_handler)
+                cb = self._close_callback
+                self._close_callback = None
+                self._run_callback(cb)
+
+    def reading(self):
+        """Returns true if we are currently reading from the stream."""
+        return self._read_callback is not None
+
+    def writing(self):
+        """Returns true if we are currently writing to the stream."""
+        return bool(self._write_buffer)
+
+    def closed(self):
+        """Returns true if the stream has been closed."""
+        return self.socket is None
+
+    def _handle_events(self, fd, events):
+        if not self.socket:
+            logging.warning("Got events for closed stream %d", fd)
+            return
+        try:
+            if events & self.io_loop.READ:
+                self._handle_read()
+            if not self.socket:
+                return
+            if events & self.io_loop.WRITE:
+                if self._connecting:
+                    self._handle_connect()
+                self._handle_write()
+            if not self.socket:
+                return
+            if events & self.io_loop.ERROR:
+                # We may have queued up a user callback in _handle_read or
+                # _handle_write, so don't close the IOStream until those
+                # callbacks have had a chance to run.
+                self.io_loop.add_callback(self.close)
+                return
+            state = self.io_loop.ERROR
+            if self.reading():
+                state |= self.io_loop.READ
+            if self.writing():
+                state |= self.io_loop.WRITE
+            if state == self.io_loop.ERROR:
+                state |= self.io_loop.READ
+            if state != self._state:
+                assert self._state is not None, \
+                    "shouldn't happen: _handle_events without self._state"
+                self._state = state
+                self.io_loop.update_handler(self.socket.fileno(), self._state)
+        except Exception:
+            logging.error("Uncaught exception, closing connection.",
+                          exc_info=True)
+            self.close()
+            raise
+
+    def _run_callback(self, callback, *args):
+        def wrapper():
+            self._pending_callbacks -= 1
+            try:
+                callback(*args)
+            except Exception:
+                logging.error("Uncaught exception, closing connection.",
+                              exc_info=True)
+                # Close the socket on an uncaught exception from a user callback
+                # (It would eventually get closed when the socket object is
+                # gc'd, but we don't want to rely on gc happening before we
+                # run out of file descriptors)
+                self.close()
+                # Re-raise the exception so that IOLoop.handle_callback_exception
+                # can see it and log the error
+                raise
+            self._maybe_add_error_listener()
+        # We schedule callbacks to be run on the next IOLoop iteration
+        # rather than running them directly for several reasons:
+        # * Prevents unbounded stack growth when a callback calls an
+        #   IOLoop operation that immediately runs another callback
+        # * Provides a predictable execution context for e.g.
+        #   non-reentrant mutexes
+        # * Ensures that the try/except in wrapper() is run outside
+        #   of the application's StackContexts
+        with stack_context.NullContext():
+            # stack_context was already captured in callback, we don't need to
+            # capture it again for IOStream's wrapper.  This is especially
+            # important if the callback was pre-wrapped before entry to
+            # IOStream (as in HTTPConnection._header_callback), as we could
+            # capture and leak the wrong context here.
+            self._pending_callbacks += 1
+            self.io_loop.add_callback(wrapper)
+
+    def _handle_read(self):
+        while True:
+            try:
+                # Read from the socket until we get EWOULDBLOCK or equivalent.
+                # SSL sockets do some internal buffering, and if the data is
+                # sitting in the SSL object's buffer select() and friends
+                # can't see it; the only way to find out if it's there is to
+                # try to read it.
+                result = self._read_to_buffer()
+            except Exception:
+                self.close()
+                return
+            if result == 0:
+                break
+            else:
+                if self._read_from_buffer():
+                    return
+
+    def _read_from_socket(self):
+        """Attempts to read from the socket.
+
+        Returns the data read or None if there is nothing to read.
+        May be overridden in subclasses.
+        """
+        try:
+            chunk = self.socket.recv(self.read_chunk_size)
+        except socket.error, e:
+            if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
+                return None
+            else:
+                raise
+        if not chunk:
+            self.close()
+            return None
+        return chunk
+
+    def _read_to_buffer(self):
+        """Reads from the socket and appends the result to the read buffer.
+
+        Returns the number of bytes read.  Returns 0 if there is nothing
+        to read (i.e. the read returns EWOULDBLOCK or equivalent).  On
+        error closes the socket and raises an exception.
+        """
+        try:
+            chunk = self._read_from_socket()
+        except socket.error, e:
+            # ssl.SSLError is a subclass of socket.error
+            logging.warning("Read error on %d: %s",
+                            self.socket.fileno(), e)
+            self.close()
+            raise
+        if chunk is None:
+            return 0
+        self._read_buffer.append(chunk)
+        self._read_buffer_size += len(chunk)
+        if self._read_buffer_size >= self.max_buffer_size:
+            logging.error("Reached maximum read buffer size")
+            self.close()
+            raise IOError("Reached maximum read buffer size")
+        return len(chunk)
+
+    def _read_from_buffer(self):
+        """Attempts to complete the currently-pending read from the buffer.
+
+        Returns True if the read was completed.
+        """
+        if self._read_bytes is not None:
+            if self._streaming_callback is not None and self._read_buffer_size:
+                bytes_to_consume = min(self._read_bytes, self._read_buffer_size)
+                self._read_bytes -= bytes_to_consume
+                self._run_callback(self._streaming_callback,
+                                   self._consume(bytes_to_consume))
+            if self._read_buffer_size >= self._read_bytes:
+                num_bytes = self._read_bytes
+                callback = self._read_callback
+                self._read_callback = None
+                self._streaming_callback = None
+                self._read_bytes = None
+                self._run_callback(callback, self._consume(num_bytes))
+                return True
+        elif self._read_delimiter is not None:
+            # Multi-byte delimiters (e.g. '\r\n') may straddle two
+            # chunks in the read buffer, so we can't easily find them
+            # without collapsing the buffer.  However, since protocols
+            # using delimited reads (as opposed to reads of a known
+            # length) tend to be "line" oriented, the delimiter is likely
+            # to be in the first few chunks.  Merge the buffer gradually
+            # since large merges are relatively expensive and get undone in
+            # consume().
+            loc = -1
+            if self._read_buffer:
+                loc = self._read_buffer[0].find(self._read_delimiter)
+            while loc == -1 and len(self._read_buffer) > 1:
+                # Grow by doubling, but don't split the second chunk just
+                # because the first one is small.
+                new_len = max(len(self._read_buffer[0]) * 2,
+                              (len(self._read_buffer[0]) +
+                               len(self._read_buffer[1])))
+                _merge_prefix(self._read_buffer, new_len)
+                loc = self._read_buffer[0].find(self._read_delimiter)
+            if loc != -1:
+                callback = self._read_callback
+                delimiter_len = len(self._read_delimiter)
+                self._read_callback = None
+                self._streaming_callback = None
+                self._read_delimiter = None
+                self._run_callback(callback,
+                                   self._consume(loc + delimiter_len))
+                return True
+        elif self._read_regex is not None:
+            m = None
+            if self._read_buffer:
+                m = self._read_regex.search(self._read_buffer[0])
+            while m is None and len(self._read_buffer) > 1:
+                # Grow by doubling, but don't split the second chunk just
+                # because the first one is small.
+                new_len = max(len(self._read_buffer[0]) * 2,
+                              (len(self._read_buffer[0]) +
+                               len(self._read_buffer[1])))
+                _merge_prefix(self._read_buffer, new_len)
+                m = self._read_regex.search(self._read_buffer[0])
+            _merge_prefix(self._read_buffer, sys.maxint)
+            m = self._read_regex.search(self._read_buffer[0])
+            if m:
+                callback = self._read_callback
+                self._read_callback = None
+                self._streaming_callback = None
+                self._read_regex = None
+                self._run_callback(callback, self._consume(m.end()))
+                return True
+        elif self._read_until_close:
+            if self._streaming_callback is not None and self._read_buffer_size:
+                self._run_callback(self._streaming_callback,
+                                   self._consume(self._read_buffer_size))
+        return False
+
+    def _handle_connect(self):
+        err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
+        if err != 0:
+            # IOLoop implementations may vary: some of them return
+            # an error state before the socket becomes writable, so
+            # in that case a connection failure would be handled by the
+            # error path in _handle_events instead of here.
+            logging.warning("Connect error on fd %d: %s",
+                            self.socket.fileno(), errno.errorcode[err])
+            self.close()
+            return
+        if self._connect_callback is not None:
+            callback = self._connect_callback
+            self._connect_callback = None
+            self._run_callback(callback)
+        self._connecting = False
+
+    def _handle_write(self):
+        while self._write_buffer:
+            try:
+                if not self._write_buffer_frozen:
+                    # On windows, socket.send blows up if given a
+                    # write buffer that's too large, instead of just
+                    # returning the number of bytes it was able to
+                    # process.  Therefore we must not call socket.send
+                    # with more than 128KB at a time.
+                    _merge_prefix(self._write_buffer, 128 * 1024)
+                num_bytes = self.socket.send(self._write_buffer[0])
+                if num_bytes == 0:
+                    # With OpenSSL, if we couldn't write the entire buffer,
+                    # the very same string object must be used on the
+                    # next call to send.  Therefore we suppress
+                    # merging the write buffer after an incomplete send.
+                    # A cleaner solution would be to set
+                    # SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER, but this is
+                    # not yet accessible from python
+                    # (http://bugs.python.org/issue8240)
+                    self._write_buffer_frozen = True
+                    break
+                self._write_buffer_frozen = False
+                _merge_prefix(self._write_buffer, num_bytes)
+                self._write_buffer.popleft()
+            except socket.error, e:
+                if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
+                    self._write_buffer_frozen = True
+                    break
+                else:
+                    logging.warning("Write error on %d: %s",
+                                    self.socket.fileno(), e)
+                    self.close()
+                    return
+        if not self._write_buffer and self._write_callback:
+            callback = self._write_callback
+            self._write_callback = None
+            self._run_callback(callback)
+
+    def _consume(self, loc):
+        if loc == 0:
+            return b("")
+        _merge_prefix(self._read_buffer, loc)
+        self._read_buffer_size -= loc
+        return self._read_buffer.popleft()
+
+    def _check_closed(self):
+        if not self.socket:
+            raise IOError("Stream is closed")
+
+    def _maybe_add_error_listener(self):
+        if self._state is None and self._pending_callbacks == 0:
+            if self.socket is None:
+                cb = self._close_callback
+                if cb is not None:
+                    self._close_callback = None
+                    self._run_callback(cb)
+            else:
+                self._add_io_state(ioloop.IOLoop.READ)
+
+    def _add_io_state(self, state):
+        """Adds `state` (IOLoop.{READ,WRITE} flags) to our event handler.
+
+        Implementation notes: Reads and writes have a fast path and a
+        slow path.  The fast path reads synchronously from socket
+        buffers, while the slow path uses `_add_io_state` to schedule
+        an IOLoop callback.  Note that in both cases, the callback is
+        run asynchronously with `_run_callback`.
+
+        To detect closed connections, we must have called
+        `_add_io_state` at some point, but we want to delay this as
+        much as possible so we don't have to set an `IOLoop.ERROR`
+        listener that will be overwritten by the next slow-path
+        operation.  As long as there are callbacks scheduled for
+        fast-path ops, those callbacks may do more reads.
+        If a sequence of fast-path ops do not end in a slow-path op,
+        (e.g. for an @asynchronous long-poll request), we must add
+        the error handler.  This is done in `_run_callback` and `write`
+        (since the write callback is optional so we can have a
+        fast-path write with no `_run_callback`)
+        """
+        if self.socket is None:
+            # connection has been closed, so there can be no future events
+            return
+        if self._state is None:
+            self._state = ioloop.IOLoop.ERROR | state
+            with stack_context.NullContext():
+                self.io_loop.add_handler(
+                    self.socket.fileno(), self._handle_events, self._state)
+        elif not self._state & state:
+            self._state = self._state | state
+            self.io_loop.update_handler(self.socket.fileno(), self._state)
+
+
+class SSLIOStream(IOStream):
+    """A utility class to write to and read from a non-blocking SSL socket.
+
+    If the socket passed to the constructor is already connected,
+    it should be wrapped with::
+
+        ssl.wrap_socket(sock, do_handshake_on_connect=False, **kwargs)
+
+    before constructing the SSLIOStream.  Unconnected sockets will be
+    wrapped when IOStream.connect is finished.
+    """
+    def __init__(self, *args, **kwargs):
+        """Creates an SSLIOStream.
+
+        If a dictionary is provided as keyword argument ssl_options,
+        it will be used as additional keyword arguments to ssl.wrap_socket.
+        """
+        self._ssl_options = kwargs.pop('ssl_options', {})
+        super(SSLIOStream, self).__init__(*args, **kwargs)
+        self._ssl_accepting = True
+        self._handshake_reading = False
+        self._handshake_writing = False
+
+    def reading(self):
+        return self._handshake_reading or super(SSLIOStream, self).reading()
+
+    def writing(self):
+        return self._handshake_writing or super(SSLIOStream, self).writing()
+
+    def _do_ssl_handshake(self):
+        # Based on code from test_ssl.py in the python stdlib
+        try:
+            self._handshake_reading = False
+            self._handshake_writing = False
+            self.socket.do_handshake()
+        except ssl.SSLError, err:
+            if err.args[0] == ssl.SSL_ERROR_WANT_READ:
+                self._handshake_reading = True
+                return
+            elif err.args[0] == ssl.SSL_ERROR_WANT_WRITE:
+                self._handshake_writing = True
+                return
+            elif err.args[0] in (ssl.SSL_ERROR_EOF,
+                                 ssl.SSL_ERROR_ZERO_RETURN):
+                return self.close()
+            elif err.args[0] == ssl.SSL_ERROR_SSL:
+                logging.warning("SSL Error on %d: %s", self.socket.fileno(), err)
+                return self.close()
+            raise
+        except socket.error, err:
+            if err.args[0] == errno.ECONNABORTED:
+                return self.close()
+        else:
+            self._ssl_accepting = False
+            super(SSLIOStream, self)._handle_connect()
+
+    def _handle_read(self):
+        if self._ssl_accepting:
+            self._do_ssl_handshake()
+            return
+        super(SSLIOStream, self)._handle_read()
+
+    def _handle_write(self):
+        if self._ssl_accepting:
+            self._do_ssl_handshake()
+            return
+        super(SSLIOStream, self)._handle_write()
+
+    def _handle_connect(self):
+        self.socket = ssl.wrap_socket(self.socket,
+                                      do_handshake_on_connect=False,
+                                      **self._ssl_options)
+        # Don't call the superclass's _handle_connect (which is responsible
+        # for telling the application that the connection is complete)
+        # until we've completed the SSL handshake (so certificates are
+        # available, etc).
+
+
+    def _read_from_socket(self):
+        if self._ssl_accepting:
+            # If the handshake hasn't finished yet, there can't be anything
+            # to read (attempting to read may or may not raise an exception
+            # depending on the SSL version)
+            return None
+        try:
+            # SSLSocket objects have both a read() and recv() method,
+            # while regular sockets only have recv().
+            # The recv() method blocks (at least in python 2.6) if it is
+            # called when there is nothing to read, so we have to use
+            # read() instead.
+            chunk = self.socket.read(self.read_chunk_size)
+        except ssl.SSLError, e:
+            # SSLError is a subclass of socket.error, so this except
+            # block must come first.
+            if e.args[0] == ssl.SSL_ERROR_WANT_READ:
+                return None
+            else:
+                raise
+        except socket.error, e:
+            if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
+                return None
+            else:
+                raise
+        if not chunk:
+            self.close()
+            return None
+        return chunk
+
+def _merge_prefix(deque, size):
+    """Replace the first entries in a deque of strings with a single
+    string of up to size bytes.
+
+    >>> d = collections.deque(['abc', 'de', 'fghi', 'j'])
+    >>> _merge_prefix(d, 5); print d
+    deque(['abcde', 'fghi', 'j'])
+
+    Strings will be split as necessary to reach the desired size.
+    >>> _merge_prefix(d, 7); print d
+    deque(['abcdefg', 'hi', 'j'])
+
+    >>> _merge_prefix(d, 3); print d
+    deque(['abc', 'defg', 'hi', 'j'])
+
+    >>> _merge_prefix(d, 100); print d
+    deque(['abcdefghij'])
+    """
+    if len(deque) == 1 and len(deque[0]) <= size:
+        return
+    prefix = []
+    remaining = size
+    while deque and remaining > 0:
+        chunk = deque.popleft()
+        if len(chunk) > remaining:
+            deque.appendleft(chunk[remaining:])
+            chunk = chunk[:remaining]
+        prefix.append(chunk)
+        remaining -= len(chunk)
+    # This data structure normally just contains byte strings, but
+    # the unittest gets messy if it doesn't use the default str() type,
+    # so do the merge based on the type of data that's actually present.
+    if prefix:
+        deque.appendleft(type(prefix[0])().join(prefix))
+    if not deque:
+        deque.appendleft(b(""))
+
+def doctests():
+    import doctest
+    return doctest.DocTestSuite()
diff --git a/tornado/locale.py b/tornado/locale.py
new file mode 100644
index 0000000..61cdb7e
--- /dev/null
+++ b/tornado/locale.py
@@ -0,0 +1,472 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Translation methods for generating localized strings.
+
+To load a locale and generate a translated string::
+
+    user_locale = locale.get("es_LA")
+    print user_locale.translate("Sign out")
+
+locale.get() returns the closest matching locale, not necessarily the
+specific locale you requested. You can support pluralization with
+additional arguments to translate(), e.g.::
+
+    people = [...]
+    message = user_locale.translate(
+        "%(list)s is online", "%(list)s are online", len(people))
+    print message % {"list": user_locale.list(people)}
+
+The first string is chosen if len(people) == 1, otherwise the second
+string is chosen.
+
+Applications should call one of load_translations (which uses a simple
+CSV format) or load_gettext_translations (which uses the .mo format
+supported by gettext and related tools).  If neither method is called,
+the locale.translate method will simply return the original string.
+"""
+
+import csv
+import datetime
+import logging
+import os
+import re
+
+_default_locale = "en_US"
+_translations = {}
+_supported_locales = frozenset([_default_locale])
+_use_gettext = False
+
+def get(*locale_codes):
+    """Returns the closest match for the given locale codes.
+
+    We iterate over all given locale codes in order. If we have a tight
+    or a loose match for the code (e.g., "en" for "en_US"), we return
+    the locale. Otherwise we move to the next code in the list.
+
+    By default we return en_US if no translations are found for any of
+    the specified locales. You can change the default locale with
+    set_default_locale() below.
+    """
+    return Locale.get_closest(*locale_codes)
+
+
+def set_default_locale(code):
+    """Sets the default locale, used in get_closest_locale().
+
+    The default locale is assumed to be the language used for all strings
+    in the system. The translations loaded from disk are mappings from
+    the default locale to the destination locale. Consequently, you don't
+    need to create a translation file for the default locale.
+    """
+    global _default_locale
+    global _supported_locales
+    _default_locale = code
+    _supported_locales = frozenset(_translations.keys() + [_default_locale])
+
+
+def load_translations(directory):
+    u"""Loads translations from CSV files in a directory.
+
+    Translations are strings with optional Python-style named placeholders
+    (e.g., "My name is %(name)s") and their associated translations.
+
+    The directory should have translation files of the form LOCALE.csv,
+    e.g. es_GT.csv. The CSV files should have two or three columns: string,
+    translation, and an optional plural indicator. Plural indicators should
+    be one of "plural" or "singular". A given string can have both singular
+    and plural forms. For example "%(name)s liked this" may have a
+    different verb conjugation depending on whether %(name)s is one
+    name or a list of names. There should be two rows in the CSV file for
+    that string, one with plural indicator "singular", and one "plural".
+    For strings with no verbs that would change on translation, simply
+    use "unknown" or the empty string (or don't include the column at all).
+
+    The file is read using the csv module in the default "excel" dialect.
+    In this format there should not be spaces after the commas.
+
+    Example translation es_LA.csv:
+
+        "I love you","Te amo"
+        "%(name)s liked this","A %(name)s les gust\u00f3 esto","plural"
+        "%(name)s liked this","A %(name)s le gust\u00f3 esto","singular"
+
+    """
+    global _translations
+    global _supported_locales
+    _translations = {}
+    for path in os.listdir(directory):
+        if not path.endswith(".csv"): continue
+        locale, extension = path.split(".")
+        if not re.match("[a-z]+(_[A-Z]+)?$", locale):
+            logging.error("Unrecognized locale %r (path: %s)", locale,
+                          os.path.join(directory, path))
+            continue
+        f = open(os.path.join(directory, path), "r")
+        _translations[locale] = {}
+        for i, row in enumerate(csv.reader(f)):
+            if not row or len(row) < 2: continue
+            row = [c.decode("utf-8").strip() for c in row]
+            english, translation = row[:2]
+            if len(row) > 2:
+                plural = row[2] or "unknown"
+            else:
+                plural = "unknown"
+            if plural not in ("plural", "singular", "unknown"):
+                logging.error("Unrecognized plural indicator %r in %s line %d",
+                              plural, path, i + 1)
+                continue
+            _translations[locale].setdefault(plural, {})[english] = translation
+        f.close()
+    _supported_locales = frozenset(_translations.keys() + [_default_locale])
+    logging.info("Supported locales: %s", sorted(_supported_locales))
+
+def load_gettext_translations(directory, domain):
+    """Loads translations from gettext's locale tree
+
+    Locale tree is similar to system's /usr/share/locale, like:
+
+    {directory}/{lang}/LC_MESSAGES/{domain}.mo
+
+    Three steps are required to have you app translated:
+
+    1. Generate POT translation file
+        xgettext --language=Python --keyword=_:1,2 -d cyclone file1.py file2.html etc
+
+    2. Merge against existing POT file:
+        msgmerge old.po cyclone.po > new.po
+
+    3. Compile:
+        msgfmt cyclone.po -o {directory}/pt_BR/LC_MESSAGES/cyclone.mo
+    """
+    import gettext
+    global _translations
+    global _supported_locales
+    global _use_gettext
+    _translations = {}
+    for lang in os.listdir(directory):
+        if lang.startswith('.'): continue  # skip .svn, etc
+        if os.path.isfile(os.path.join(directory, lang)): continue
+        try:
+            os.stat(os.path.join(directory, lang, "LC_MESSAGES", domain+".mo"))
+            _translations[lang] = gettext.translation(domain, directory,
+                                                      languages=[lang])
+        except Exception, e:
+            logging.error("Cannot load translation for '%s': %s", lang, str(e))
+            continue
+    _supported_locales = frozenset(_translations.keys() + [_default_locale])
+    _use_gettext = True
+    logging.info("Supported locales: %s", sorted(_supported_locales))
+
+
+def get_supported_locales(cls):
+    """Returns a list of all the supported locale codes."""
+    return _supported_locales
+
+
+class Locale(object):
+    """Object representing a locale.
+
+    After calling one of `load_translations` or `load_gettext_translations`,
+    call `get` or `get_closest` to get a Locale object.
+    """
+    @classmethod
+    def get_closest(cls, *locale_codes):
+        """Returns the closest match for the given locale code."""
+        for code in locale_codes:
+            if not code: continue
+            code = code.replace("-", "_")
+            parts = code.split("_")
+            if len(parts) > 2:
+                continue
+            elif len(parts) == 2:
+                code = parts[0].lower() + "_" + parts[1].upper()
+            if code in _supported_locales:
+                return cls.get(code)
+            if parts[0].lower() in _supported_locales:
+                return cls.get(parts[0].lower())
+        return cls.get(_default_locale)
+
+    @classmethod
+    def get(cls, code):
+        """Returns the Locale for the given locale code.
+
+        If it is not supported, we raise an exception.
+        """
+        if not hasattr(cls, "_cache"):
+            cls._cache = {}
+        if code not in cls._cache:
+            assert code in _supported_locales
+            translations = _translations.get(code, None)
+            if translations is None:
+                locale = CSVLocale(code, {})
+            elif _use_gettext:
+                locale = GettextLocale(code, translations)
+            else:
+                locale = CSVLocale(code, translations)
+            cls._cache[code] = locale
+        return cls._cache[code]
+
+    def __init__(self, code, translations):
+        self.code = code
+        self.name = LOCALE_NAMES.get(code, {}).get("name", u"Unknown")
+        self.rtl = False
+        for prefix in ["fa", "ar", "he"]:
+            if self.code.startswith(prefix):
+                self.rtl = True
+                break
+        self.translations = translations
+
+        # Initialize strings for date formatting
+        _ = self.translate
+        self._months = [
+            _("January"), _("February"), _("March"), _("April"),
+            _("May"), _("June"), _("July"), _("August"),
+            _("September"), _("October"), _("November"), _("December")]
+        self._weekdays = [
+            _("Monday"), _("Tuesday"), _("Wednesday"), _("Thursday"),
+            _("Friday"), _("Saturday"), _("Sunday")]
+
+    def translate(self, message, plural_message=None, count=None):
+        """Returns the translation for the given message for this locale.
+
+        If plural_message is given, you must also provide count. We return
+        plural_message when count != 1, and we return the singular form
+        for the given message when count == 1.
+        """
+        raise NotImplementedError()
+
+    def format_date(self, date, gmt_offset=0, relative=True, shorter=False,
+                    full_format=False):
+        """Formats the given date (which should be GMT).
+
+        By default, we return a relative time (e.g., "2 minutes ago"). You
+        can return an absolute date string with relative=False.
+
+        You can force a full format date ("July 10, 1980") with
+        full_format=True.
+
+        This method is primarily intended for dates in the past.
+        For dates in the future, we fall back to full format.
+        """
+        if self.code.startswith("ru"):
+            relative = False
+        if type(date) in (int, long, float):
+            date = datetime.datetime.utcfromtimestamp(date)
+        now = datetime.datetime.utcnow()
+        if date > now:
+            if relative and (date - now).seconds < 60:
+                # Due to click skew, things are some things slightly
+                # in the future. Round timestamps in the immediate
+                # future down to now in relative mode.
+                date = now
+            else:
+                # Otherwise, future dates always use the full format.
+                full_format = True
+        local_date = date - datetime.timedelta(minutes=gmt_offset)
+        local_now = now - datetime.timedelta(minutes=gmt_offset)
+        local_yesterday = local_now - datetime.timedelta(hours=24)
+        difference = now - date
+        seconds = difference.seconds
+        days = difference.days
+
+        _ = self.translate
+        format = None
+        if not full_format:
+            if relative and days == 0:
+                if seconds < 50:
+                    return _("1 second ago", "%(seconds)d seconds ago",
+                             seconds) % { "seconds": seconds }
+
+                if seconds < 50 * 60:
+                    minutes = round(seconds / 60.0)
+                    return _("1 minute ago", "%(minutes)d minutes ago",
+                             minutes) % { "minutes": minutes }
+
+                hours = round(seconds / (60.0 * 60))
+                return _("1 hour ago", "%(hours)d hours ago",
+                         hours) % { "hours": hours }
+
+            if days == 0:
+                format = _("%(time)s")
+            elif days == 1 and local_date.day == local_yesterday.day and \
+                 relative:
+                format = _("yesterday") if shorter else \
+                         _("yesterday at %(time)s")
+            elif days < 5:
+                format = _("%(weekday)s") if shorter else \
+                         _("%(weekday)s at %(time)s")
+            elif days < 334:  # 11mo, since confusing for same month last year
+                format = _("%(month_name)s %(day)s") if shorter else \
+                         _("%(month_name)s %(day)s at %(time)s")
+
+        if format is None:
+            format = _("%(month_name)s %(day)s, %(year)s") if shorter else \
+                     _("%(month_name)s %(day)s, %(year)s at %(time)s")
+
+        tfhour_clock = self.code not in ("en", "en_US", "zh_CN")
+        if tfhour_clock:
+            str_time = "%d:%02d" % (local_date.hour, local_date.minute)
+        elif self.code == "zh_CN":
+            str_time = "%s%d:%02d" % (
+                (u'\u4e0a\u5348', u'\u4e0b\u5348')[local_date.hour >= 12],
+                local_date.hour % 12 or 12, local_date.minute)
+        else:
+            str_time = "%d:%02d %s" % (
+                local_date.hour % 12 or 12, local_date.minute,
+                ("am", "pm")[local_date.hour >= 12])
+
+        return format % {
+            "month_name": self._months[local_date.month - 1],
+            "weekday": self._weekdays[local_date.weekday()],
+            "day": str(local_date.day),
+            "year": str(local_date.year),
+            "time": str_time
+        }
+
+    def format_day(self, date, gmt_offset=0, dow=True):
+        """Formats the given date as a day of week.
+
+        Example: "Monday, January 22". You can remove the day of week with
+        dow=False.
+        """
+        local_date = date - datetime.timedelta(minutes=gmt_offset)
+        _ = self.translate
+        if dow:
+            return _("%(weekday)s, %(month_name)s %(day)s") % {
+                "month_name": self._months[local_date.month - 1],
+                "weekday": self._weekdays[local_date.weekday()],
+                "day": str(local_date.day),
+            }
+        else:
+            return _("%(month_name)s %(day)s") % {
+                "month_name": self._months[local_date.month - 1],
+                "day": str(local_date.day),
+            }
+
+    def list(self, parts):
+        """Returns a comma-separated list for the given list of parts.
+
+        The format is, e.g., "A, B and C", "A and B" or just "A" for lists
+        of size 1.
+        """
+        _ = self.translate
+        if len(parts) == 0: return ""
+        if len(parts) == 1: return parts[0]
+        comma = u' \u0648 ' if self.code.startswith("fa") else u", "
+        return _("%(commas)s and %(last)s") % {
+            "commas": comma.join(parts[:-1]),
+            "last": parts[len(parts) - 1],
+        }
+
+    def friendly_number(self, value):
+        """Returns a comma-separated number for the given integer."""
+        if self.code not in ("en", "en_US"):
+            return str(value)
+        value = str(value)
+        parts = []
+        while value:
+            parts.append(value[-3:])
+            value = value[:-3]
+        return ",".join(reversed(parts))
+
+class CSVLocale(Locale):
+    """Locale implementation using tornado's CSV translation format."""
+    def translate(self, message, plural_message=None, count=None):
+        if plural_message is not None:
+            assert count is not None
+            if count != 1:
+                message = plural_message
+                message_dict = self.translations.get("plural", {})
+            else:
+                message_dict = self.translations.get("singular", {})
+        else:
+            message_dict = self.translations.get("unknown", {})
+        return message_dict.get(message, message)
+
+class GettextLocale(Locale):
+    """Locale implementation using the gettext module."""
+    def translate(self, message, plural_message=None, count=None):
+        if plural_message is not None:
+            assert count is not None
+            return self.translations.ungettext(message, plural_message, count)
+        else:
+            return self.translations.ugettext(message)
+
+LOCALE_NAMES = {
+    "af_ZA": {"name_en": u"Afrikaans", "name": u"Afrikaans"},
+    "am_ET": {"name_en": u"Amharic", "name": u'\u12a0\u121b\u122d\u129b'},
+    "ar_AR": {"name_en": u"Arabic", "name": u"\u0627\u0644\u0639\u0631\u0628\u064a\u0629"},
+    "bg_BG": {"name_en": u"Bulgarian", "name": u"\u0411\u044a\u043b\u0433\u0430\u0440\u0441\u043a\u0438"},
+    "bn_IN": {"name_en": u"Bengali", "name": u"\u09ac\u09be\u0982\u09b2\u09be"},
+    "bs_BA": {"name_en": u"Bosnian", "name": u"Bosanski"},
+    "ca_ES": {"name_en": u"Catalan", "name": u"Catal\xe0"},
+    "cs_CZ": {"name_en": u"Czech", "name": u"\u010ce\u0161tina"},
+    "cy_GB": {"name_en": u"Welsh", "name": u"Cymraeg"},
+    "da_DK": {"name_en": u"Danish", "name": u"Dansk"},
+    "de_DE": {"name_en": u"German", "name": u"Deutsch"},
+    "el_GR": {"name_en": u"Greek", "name": u"\u0395\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac"},
+    "en_GB": {"name_en": u"English (UK)", "name": u"English (UK)"},
+    "en_US": {"name_en": u"English (US)", "name": u"English (US)"},
+    "es_ES": {"name_en": u"Spanish (Spain)", "name": u"Espa\xf1ol (Espa\xf1a)"},
+    "es_LA": {"name_en": u"Spanish", "name": u"Espa\xf1ol"},
+    "et_EE": {"name_en": u"Estonian", "name": u"Eesti"},
+    "eu_ES": {"name_en": u"Basque", "name": u"Euskara"},
+    "fa_IR": {"name_en": u"Persian", "name": u"\u0641\u0627\u0631\u0633\u06cc"},
+    "fi_FI": {"name_en": u"Finnish", "name": u"Suomi"},
+    "fr_CA": {"name_en": u"French (Canada)", "name": u"Fran\xe7ais (Canada)"},
+    "fr_FR": {"name_en": u"French", "name": u"Fran\xe7ais"},
+    "ga_IE": {"name_en": u"Irish", "name": u"Gaeilge"},
+    "gl_ES": {"name_en": u"Galician", "name": u"Galego"},
+    "he_IL": {"name_en": u"Hebrew", "name": u"\u05e2\u05d1\u05e8\u05d9\u05ea"},
+    "hi_IN": {"name_en": u"Hindi", "name": u"\u0939\u093f\u0928\u094d\u0926\u0940"},
+    "hr_HR": {"name_en": u"Croatian", "name": u"Hrvatski"},
+    "hu_HU": {"name_en": u"Hungarian", "name": u"Magyar"},
+    "id_ID": {"name_en": u"Indonesian", "name": u"Bahasa Indonesia"},
+    "is_IS": {"name_en": u"Icelandic", "name": u"\xcdslenska"},
+    "it_IT": {"name_en": u"Italian", "name": u"Italiano"},
+    "ja_JP": {"name_en": u"Japanese", "name": u"\u65e5\u672c\u8a9e"},
+    "ko_KR": {"name_en": u"Korean", "name": u"\ud55c\uad6d\uc5b4"},
+    "lt_LT": {"name_en": u"Lithuanian", "name": u"Lietuvi\u0173"},
+    "lv_LV": {"name_en": u"Latvian", "name": u"Latvie\u0161u"},
+    "mk_MK": {"name_en": u"Macedonian", "name": u"\u041c\u0430\u043a\u0435\u0434\u043e\u043d\u0441\u043a\u0438"},
+    "ml_IN": {"name_en": u"Malayalam", "name": u"\u0d2e\u0d32\u0d2f\u0d3e\u0d33\u0d02"},
+    "ms_MY": {"name_en": u"Malay", "name": u"Bahasa Melayu"},
+    "nb_NO": {"name_en": u"Norwegian (bokmal)", "name": u"Norsk (bokm\xe5l)"},
+    "nl_NL": {"name_en": u"Dutch", "name": u"Nederlands"},
+    "nn_NO": {"name_en": u"Norwegian (nynorsk)", "name": u"Norsk (nynorsk)"},
+    "pa_IN": {"name_en": u"Punjabi", "name": u"\u0a2a\u0a70\u0a1c\u0a3e\u0a2c\u0a40"},
+    "pl_PL": {"name_en": u"Polish", "name": u"Polski"},
+    "pt_BR": {"name_en": u"Portuguese (Brazil)", "name": u"Portugu\xeas (Brasil)"},
+    "pt_PT": {"name_en": u"Portuguese (Portugal)", "name": u"Portugu\xeas (Portugal)"},
+    "ro_RO": {"name_en": u"Romanian", "name": u"Rom\xe2n\u0103"},
+    "ru_RU": {"name_en": u"Russian", "name": u"\u0420\u0443\u0441\u0441\u043a\u0438\u0439"},
+    "sk_SK": {"name_en": u"Slovak", "name": u"Sloven\u010dina"},
+    "sl_SI": {"name_en": u"Slovenian", "name": u"Sloven\u0161\u010dina"},
+    "sq_AL": {"name_en": u"Albanian", "name": u"Shqip"},
+    "sr_RS": {"name_en": u"Serbian", "name": u"\u0421\u0440\u043f\u0441\u043a\u0438"},
+    "sv_SE": {"name_en": u"Swedish", "name": u"Svenska"},
+    "sw_KE": {"name_en": u"Swahili", "name": u"Kiswahili"},
+    "ta_IN": {"name_en": u"Tamil", "name": u"\u0ba4\u0bae\u0bbf\u0bb4\u0bcd"},
+    "te_IN": {"name_en": u"Telugu", "name": u"\u0c24\u0c46\u0c32\u0c41\u0c17\u0c41"},
+    "th_TH": {"name_en": u"Thai", "name": u"\u0e20\u0e32\u0e29\u0e32\u0e44\u0e17\u0e22"},
+    "tl_PH": {"name_en": u"Filipino", "name": u"Filipino"},
+    "tr_TR": {"name_en": u"Turkish", "name": u"T\xfcrk\xe7e"},
+    "uk_UA": {"name_en": u"Ukraini ", "name": u"\u0423\u043a\u0440\u0430\u0457\u043d\u0441\u044c\u043a\u0430"},
+    "vi_VN": {"name_en": u"Vietnamese", "name": u"Ti\u1ebfng Vi\u1ec7t"},
+    "zh_CN": {"name_en": u"Chinese (Simplified)", "name": u"\u4e2d\u6587(\u7b80\u4f53)"},
+    "zh_TW": {"name_en": u"Chinese (Traditional)", "name": u"\u4e2d\u6587(\u7e41\u9ad4)"},
+}
diff --git a/tornado/netutil.py b/tornado/netutil.py
new file mode 100644
index 0000000..a7745ab
--- /dev/null
+++ b/tornado/netutil.py
@@ -0,0 +1,320 @@
+#!/usr/bin/env python
+#
+# Copyright 2011 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Miscellaneous network utility code."""
+
+import errno
+import logging
+import os
+import socket
+import stat
+
+from tornado import process
+from tornado.ioloop import IOLoop
+from tornado.iostream import IOStream, SSLIOStream
+from tornado.platform.auto import set_close_exec
+
+try:
+    import ssl # Python 2.6+
+except ImportError:
+    ssl = None
+
+class TCPServer(object):
+    r"""A non-blocking, single-threaded TCP server.
+
+    To use `TCPServer`, define a subclass which overrides the `handle_stream`
+    method.
+
+    `TCPServer` can serve SSL traffic with Python 2.6+ and OpenSSL.
+    To make this server serve SSL traffic, send the ssl_options dictionary
+    argument with the arguments required for the `ssl.wrap_socket` method,
+    including "certfile" and "keyfile"::
+
+       TCPServer(ssl_options={
+           "certfile": os.path.join(data_dir, "mydomain.crt"),
+           "keyfile": os.path.join(data_dir, "mydomain.key"),
+       })
+
+    `TCPServer` initialization follows one of three patterns:
+
+    1. `listen`: simple single-process::
+
+            server = TCPServer()
+            server.listen(8888)
+            IOLoop.instance().start()
+
+    2. `bind`/`start`: simple multi-process::
+
+            server = TCPServer()
+            server.bind(8888)
+            server.start(0)  # Forks multiple sub-processes
+            IOLoop.instance().start()
+
+       When using this interface, an `IOLoop` must *not* be passed
+       to the `TCPServer` constructor.  `start` will always start
+       the server on the default singleton `IOLoop`.
+
+    3. `add_sockets`: advanced multi-process::
+
+            sockets = bind_sockets(8888)
+            tornado.process.fork_processes(0)
+            server = TCPServer()
+            server.add_sockets(sockets)
+            IOLoop.instance().start()
+
+       The `add_sockets` interface is more complicated, but it can be
+       used with `tornado.process.fork_processes` to give you more
+       flexibility in when the fork happens.  `add_sockets` can
+       also be used in single-process servers if you want to create
+       your listening sockets in some way other than
+       `bind_sockets`.
+    """
+    def __init__(self, io_loop=None, ssl_options=None):
+        self.io_loop = io_loop
+        self.ssl_options = ssl_options
+        self._sockets = {}  # fd -> socket object
+        self._pending_sockets = []
+        self._started = False
+
+    def listen(self, port, address=""):
+        """Starts accepting connections on the given port.
+
+        This method may be called more than once to listen on multiple ports.
+        `listen` takes effect immediately; it is not necessary to call
+        `TCPServer.start` afterwards.  It is, however, necessary to start
+        the `IOLoop`.
+        """
+        sockets = bind_sockets(port, address=address)
+        self.add_sockets(sockets)
+
+    def add_sockets(self, sockets):
+        """Makes this server start accepting connections on the given sockets.
+
+        The ``sockets`` parameter is a list of socket objects such as
+        those returned by `bind_sockets`.
+        `add_sockets` is typically used in combination with that
+        method and `tornado.process.fork_processes` to provide greater
+        control over the initialization of a multi-process server.
+        """
+        if self.io_loop is None:
+            self.io_loop = IOLoop.instance()
+
+        for sock in sockets:
+            self._sockets[sock.fileno()] = sock
+            add_accept_handler(sock, self._handle_connection,
+                               io_loop=self.io_loop)
+
+    def add_socket(self, socket):
+        """Singular version of `add_sockets`.  Takes a single socket object."""
+        self.add_sockets([socket])
+
+    def bind(self, port, address=None, family=0, 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=0, backlog=128):
+    """Creates listening sockets bound to the given port and address.
+
+    Returns a list of socket objects (multiple sockets are returned if
+    the given address maps to multiple IP addresses, which is most common
+    for mixed IPv4 and IPv6 use).
+
+    Address may be either an IP address or hostname.  If it's a hostname,
+    the server will listen on all IP addresses associated with the
+    name.  Address may be an empty string or None to listen on all
+    available interfaces.  Family may be set to either socket.AF_INET
+    or socket.AF_INET6 to restrict to ipv4 or ipv6 addresses, otherwise
+    both will be used if available.
+
+    The ``backlog`` argument has the same meaning as for 
+    ``socket.listen()``.
+    """
+    sockets = []
+    if address == "":
+        address = None
+    flags = socket.AI_PASSIVE
+    if hasattr(socket, "AI_ADDRCONFIG"):
+        # AI_ADDRCONFIG ensures that we only try to bind on ipv6
+        # if the system is configured for it, but the flag doesn't
+        # exist on some platforms (specifically WinXP, although
+        # newer versions of windows have it)
+        flags |= socket.AI_ADDRCONFIG
+    for res in set(socket.getaddrinfo(address, port, family, socket.SOCK_STREAM,
+                                  0, flags)):
+        af, socktype, proto, canonname, sockaddr = res
+        sock = socket.socket(af, socktype, proto)
+        set_close_exec(sock.fileno())
+        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        if af == socket.AF_INET6:
+            # On linux, ipv6 sockets accept ipv4 too by default,
+            # but this makes it impossible to bind to both
+            # 0.0.0.0 in ipv4 and :: in ipv6.  On other systems,
+            # separate sockets *must* be used to listen for both ipv4
+            # and ipv6.  For consistency, always disable ipv4 on our
+            # ipv6 sockets and use a separate ipv4 socket when needed.
+            #
+            # Python 2.x on windows doesn't have IPPROTO_IPV6.
+            if hasattr(socket, "IPPROTO_IPV6"):
+                sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
+        sock.setblocking(0)
+        sock.bind(sockaddr)
+        sock.listen(backlog)
+        sockets.append(sock)
+    return sockets
+
+if hasattr(socket, 'AF_UNIX'):
+    def bind_unix_socket(file, mode=0600, backlog=128):
+        """Creates a listening unix socket.
+
+        If a socket with the given name already exists, it will be deleted.
+        If any other file with that name exists, an exception will be
+        raised.
+
+        Returns a socket object (not a list of socket objects like 
+        `bind_sockets`)
+        """
+        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+        set_close_exec(sock.fileno())
+        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        sock.setblocking(0)
+        try:
+            st = os.stat(file)
+        except OSError, err:
+            if err.errno != errno.ENOENT:
+                raise
+        else:
+            if stat.S_ISSOCK(st.st_mode):
+                os.remove(file)
+            else:
+                raise ValueError("File %s exists and is not a socket", file)
+        sock.bind(file)
+        os.chmod(file, mode)
+        sock.listen(backlog)
+        return sock
+
+def add_accept_handler(sock, callback, io_loop=None):
+    """Adds an ``IOLoop`` event handler to accept new connections on ``sock``.
+
+    When a connection is accepted, ``callback(connection, address)`` will
+    be run (``connection`` is a socket object, and ``address`` is the
+    address of the other end of the connection).  Note that this signature
+    is different from the ``callback(fd, events)`` signature used for
+    ``IOLoop`` handlers.
+    """
+    if io_loop is None:
+        io_loop = IOLoop.instance()
+    def accept_handler(fd, events):
+        while True:
+            try:
+                connection, address = sock.accept()
+            except socket.error, e:
+                if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
+                    return
+                raise
+            callback(connection, address)
+    io_loop.add_handler(sock.fileno(), accept_handler, IOLoop.READ)
diff --git a/tornado/options.py b/tornado/options.py
new file mode 100644
index 0000000..f9f472f
--- /dev/null
+++ b/tornado/options.py
@@ -0,0 +1,417 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""A command line parsing module that lets modules define their own options.
+
+Each module defines its own options, e.g.::
+
+    from tornado.options import define, options
+
+    define("mysql_host", default="127.0.0.1:3306", help="Main user DB")
+    define("memcache_hosts", default="127.0.0.1:11011", multiple=True,
+           help="Main user memcache servers")
+
+    def connect():
+        db = database.Connection(options.mysql_host)
+        ...
+
+The main() method of your application does not need to be aware of all of
+the options used throughout your program; they are all automatically loaded
+when the modules are loaded. Your main() method can parse the command line
+or parse a config file with::
+
+    import tornado.options
+    tornado.options.parse_config_file("/etc/server.conf")
+    tornado.options.parse_command_line()
+
+Command line formats are what you would expect ("--myoption=myvalue").
+Config files are just Python files. Global names become options, e.g.::
+
+    myoption = "myvalue"
+    myotheroption = "myothervalue"
+
+We support datetimes, timedeltas, ints, and floats (just pass a 'type'
+kwarg to define). We also accept multi-value options. See the documentation
+for define() below.
+"""
+
+import datetime
+import logging
+import logging.handlers
+import re
+import sys
+import time
+
+from tornado.escape import _unicode
+
+# For pretty log messages, if available
+try:
+    import curses
+except ImportError:
+    curses = None
+
+
+def define(name, default=None, type=None, help=None, metavar=None,
+           multiple=False, group=None):
+    """Defines a new command line option.
+
+    If type is given (one of str, float, int, datetime, or timedelta)
+    or can be inferred from the default, we parse the command line
+    arguments based on the given type. If multiple is True, we accept
+    comma-separated values, and the option value is always a list.
+
+    For multi-value integers, we also accept the syntax x:y, which
+    turns into range(x, y) - very useful for long integer ranges.
+
+    help and metavar are used to construct the automatically generated
+    command line help string. The help message is formatted like::
+
+       --name=METAVAR      help string
+
+    group is used to group the defined options in logical groups. By default,
+    command line options are grouped by the defined file.
+
+    Command line option names must be unique globally. They can be parsed
+    from the command line with parse_command_line() or parsed from a
+    config file with parse_config_file.
+    """
+    if name in options:
+        raise Error("Option %r already defined in %s", name,
+                    options[name].file_name)
+    frame = sys._getframe(0)
+    options_file = frame.f_code.co_filename
+    file_name = frame.f_back.f_code.co_filename
+    if file_name == options_file: file_name = ""
+    if type is None:
+        if not multiple and default is not None:
+            type = default.__class__
+        else:
+            type = str
+    if group:
+        group_name = group
+    else:
+        group_name = file_name
+    options[name] = _Option(name, file_name=file_name, default=default,
+                            type=type, help=help, metavar=metavar,
+                            multiple=multiple, group_name=group_name)
+
+
+def parse_command_line(args=None):
+    """Parses all options given on the command line.
+
+    We return all command line arguments that are not options as a list.
+    """
+    if args is None: args = sys.argv
+    remaining = []
+    for i in xrange(1, len(args)):
+        # All things after the last option are command line arguments
+        if not args[i].startswith("-"):
+            remaining = args[i:]
+            break
+        if args[i] == "--":
+            remaining = args[i+1:]
+            break
+        arg = args[i].lstrip("-")
+        name, equals, value = arg.partition("=")
+        name = name.replace('-', '_')
+        if not name in options:
+            print_help()
+            raise Error('Unrecognized command line option: %r' % name)
+        option = options[name]
+        if not equals:
+            if option.type == bool:
+                value = "true"
+            else:
+                raise Error('Option %r requires a value' % name)
+        option.parse(value)
+    if options.help:
+        print_help()
+        sys.exit(0)
+
+    # Set up log level and pretty console logging by default
+    if options.logging != 'none':
+        logging.getLogger().setLevel(getattr(logging, options.logging.upper()))
+        enable_pretty_logging()
+
+    return remaining
+
+
+def parse_config_file(path):
+    """Parses and loads the Python config file at the given path."""
+    config = {}
+    execfile(path, config, config)
+    for name in config:
+        if name in options:
+            options[name].set(config[name])
+
+
+def print_help(file=sys.stdout):
+    """Prints all the command line options to stdout."""
+    print >> file, "Usage: %s [OPTIONS]" % sys.argv[0]
+    print >> file, ""
+    print >> file, "Options:"
+    by_group = {}
+    for option in options.itervalues():
+        by_group.setdefault(option.group_name, []).append(option)
+
+    for filename, o in sorted(by_group.items()):
+        if filename: print >> file, filename
+        o.sort(key=lambda option: option.name)
+        for option in o:
+            prefix = option.name
+            if option.metavar:
+                prefix += "=" + option.metavar
+            print >> file, "  --%-30s %s" % (prefix, option.help or "")
+    print >> file
+
+
+class _Options(dict):
+    """Our global program options, an dictionary with object-like access."""
+    @classmethod
+    def instance(cls):
+        if not hasattr(cls, "_instance"):
+            cls._instance = cls()
+        return cls._instance
+
+    def __getattr__(self, name):
+        if isinstance(self.get(name), _Option):
+            return self[name].value()
+        raise AttributeError("Unrecognized option %r" % name)
+
+
+class _Option(object):
+    def __init__(self, name, default=None, type=str, help=None, metavar=None,
+                 multiple=False, file_name=None, group_name=None):
+        if default is None and multiple:
+            default = []
+        self.name = name
+        self.type = type
+        self.help = help
+        self.metavar = metavar
+        self.multiple = multiple
+        self.file_name = file_name
+        self.group_name = group_name
+        self.default = default
+        self._value = None
+
+    def value(self):
+        return self.default if self._value is None else self._value
+
+    def parse(self, value):
+        _parse = {
+            datetime.datetime: self._parse_datetime,
+            datetime.timedelta: self._parse_timedelta,
+            bool: self._parse_bool,
+            str: self._parse_string,
+        }.get(self.type, self.type)
+        if self.multiple:
+            if self._value is None:
+                self._value = []
+            for part in value.split(","):
+                if self.type in (int, long):
+                    # allow ranges of the form X:Y (inclusive at both ends)
+                    lo, _, hi = part.partition(":")
+                    lo = _parse(lo)
+                    hi = _parse(hi) if hi else lo
+                    self._value.extend(range(lo, hi+1))
+                else:
+                    self._value.append(_parse(part))
+        else:
+            self._value = _parse(value)
+        return self.value()
+
+    def set(self, value):
+        if self.multiple:
+            if not isinstance(value, list):
+                raise Error("Option %r is required to be a list of %s" %
+                            (self.name, self.type.__name__))
+            for item in value:
+                if item != None and not isinstance(item, self.type):
+                    raise Error("Option %r is required to be a list of %s" %
+                                (self.name, self.type.__name__))
+        else:
+            if value != None and not isinstance(value, self.type):
+                raise Error("Option %r is required to be a %s" %
+                            (self.name, self.type.__name__))
+        self._value = value
+
+    # Supported date/time formats in our options
+    _DATETIME_FORMATS = [
+        "%a %b %d %H:%M:%S %Y",
+        "%Y-%m-%d %H:%M:%S",
+        "%Y-%m-%d %H:%M",
+        "%Y-%m-%dT%H:%M",
+        "%Y%m%d %H:%M:%S",
+        "%Y%m%d %H:%M",
+        "%Y-%m-%d",
+        "%Y%m%d",
+        "%H:%M:%S",
+        "%H:%M",
+    ]
+
+    def _parse_datetime(self, value):
+        for format in self._DATETIME_FORMATS:
+            try:
+                return datetime.datetime.strptime(value, format)
+            except ValueError:
+                pass
+        raise Error('Unrecognized date/time format: %r' % value)
+
+    _TIMEDELTA_ABBREVS = [
+        ('hours', ['h']),
+        ('minutes', ['m', 'min']),
+        ('seconds', ['s', 'sec']),
+        ('milliseconds', ['ms']),
+        ('microseconds', ['us']),
+        ('days', ['d']),
+        ('weeks', ['w']),
+    ]
+
+    _TIMEDELTA_ABBREV_DICT = dict(
+        (abbrev, full) for full, abbrevs in _TIMEDELTA_ABBREVS
+        for abbrev in abbrevs)
+
+    _FLOAT_PATTERN = r'[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?'
+
+    _TIMEDELTA_PATTERN = re.compile(
+        r'\s*(%s)\s*(\w*)\s*' % _FLOAT_PATTERN, re.IGNORECASE)
+
+    def _parse_timedelta(self, value):
+        try:
+            sum = datetime.timedelta()
+            start = 0
+            while start < len(value):
+                m = self._TIMEDELTA_PATTERN.match(value, start)
+                if not m:
+                    raise Exception()
+                num = float(m.group(1))
+                units = m.group(2) or 'seconds'
+                units = self._TIMEDELTA_ABBREV_DICT.get(units, units)
+                sum += datetime.timedelta(**{units: num})
+                start = m.end()
+            return sum
+        except Exception:
+            raise
+
+    def _parse_bool(self, value):
+        return value.lower() not in ("false", "0", "f")
+
+    def _parse_string(self, value):
+        return _unicode(value)
+
+
+class Error(Exception):
+    """Exception raised by errors in the options module."""
+    pass
+
+
+def enable_pretty_logging():
+    """Turns on formatted logging output as configured.
+    
+    This is called automatically by `parse_command_line`.
+    """
+    root_logger = logging.getLogger()
+    if options.log_file_prefix:
+        channel = logging.handlers.RotatingFileHandler(
+            filename=options.log_file_prefix,
+            maxBytes=options.log_file_max_size,
+            backupCount=options.log_file_num_backups)
+        channel.setFormatter(_LogFormatter(color=False))
+        root_logger.addHandler(channel)
+
+    if (options.log_to_stderr or
+        (options.log_to_stderr is None and not root_logger.handlers)):
+        # Set up color if we are in a tty and curses is installed
+        color = False
+        if curses and sys.stderr.isatty():
+            try:
+                curses.setupterm()
+                if curses.tigetnum("colors") > 0:
+                    color = True
+            except Exception:
+                pass
+        channel = logging.StreamHandler()
+        channel.setFormatter(_LogFormatter(color=color))
+        root_logger.addHandler(channel)
+
+
+
+class _LogFormatter(logging.Formatter):
+    def __init__(self, color, *args, **kwargs):
+        logging.Formatter.__init__(self, *args, **kwargs)
+        self._color = color
+        if color:
+            # The curses module has some str/bytes confusion in python3.
+            # Most methods return bytes, but only accept strings.
+            # The explict calls to unicode() below are harmless in python2,
+            # but will do the right conversion in python3.
+            fg_color = unicode(curses.tigetstr("setaf") or 
+                               curses.tigetstr("setf") or "", "ascii")
+            self._colors = {
+                logging.DEBUG: unicode(curses.tparm(fg_color, 4), # Blue
+                                       "ascii"),
+                logging.INFO: unicode(curses.tparm(fg_color, 2), # Green
+                                      "ascii"),
+                logging.WARNING: unicode(curses.tparm(fg_color, 3), # Yellow
+                                         "ascii"),
+                logging.ERROR: unicode(curses.tparm(fg_color, 1), # Red
+                                       "ascii"),
+            }
+            self._normal = unicode(curses.tigetstr("sgr0"), "ascii")
+
+    def format(self, record):
+        try:
+            record.message = record.getMessage()
+        except Exception, e:
+            record.message = "Bad message (%r): %r" % (e, record.__dict__)
+        record.asctime = time.strftime(
+            "%y%m%d %H:%M:%S", self.converter(record.created))
+        prefix = '[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]' % \
+            record.__dict__
+        if self._color:
+            prefix = (self._colors.get(record.levelno, self._normal) +
+                      prefix + self._normal)
+        formatted = prefix + " " + record.message
+        if record.exc_info:
+            if not record.exc_text:
+                record.exc_text = self.formatException(record.exc_info)
+        if record.exc_text:
+            formatted = formatted.rstrip() + "\n" + record.exc_text
+        return formatted.replace("\n", "\n    ")
+
+
+options = _Options.instance()
+
+
+# Default options
+define("help", type=bool, help="show this help information")
+define("logging", default="info",
+       help=("Set the Python log level. If 'none', tornado won't touch the "
+             "logging configuration."),
+       metavar="debug|info|warning|error|none")
+define("log_to_stderr", type=bool, default=None,
+       help=("Send log output to stderr (colorized if possible). "
+             "By default use stderr if --log_file_prefix is not set and "
+             "no other logging is configured."))
+define("log_file_prefix", type=str, default=None, metavar="PATH",
+       help=("Path prefix for log files. "
+             "Note that if you are running multiple tornado processes, "
+             "log_file_prefix must be different for each of them (e.g. "
+             "include the port number)"))
+define("log_file_max_size", type=int, default=100 * 1000 * 1000,
+       help="max size of log files before rollover")
+define("log_file_num_backups", type=int, default=10,
+       help="number of log files to keep")
diff --git a/tornado/platform/__init__.py b/tornado/platform/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tornado/platform/__init__.py
diff --git a/tornado/platform/auto.py b/tornado/platform/auto.py
new file mode 100644
index 0000000..e76d731
--- /dev/null
+++ b/tornado/platform/auto.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+#
+# Copyright 2011 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Implementation of platform-specific functionality.
+
+For each function or class described in `tornado.platform.interface`,
+the appropriate platform-specific implementation exists in this module.
+Most code that needs access to this functionality should do e.g.::
+
+    from tornado.platform.auto import set_close_exec
+"""
+
+import os
+
+if os.name == 'nt':
+    from tornado.platform.windows import set_close_exec, Waker
+else:
+    from tornado.platform.posix import set_close_exec, Waker
diff --git a/tornado/platform/interface.py b/tornado/platform/interface.py
new file mode 100644
index 0000000..20f0f71
--- /dev/null
+++ b/tornado/platform/interface.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+#
+# Copyright 2011 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Interfaces for platform-specific functionality.
+
+This module exists primarily for documentation purposes and as base classes
+for other tornado.platform modules.  Most code should import the appropriate
+implementation from `tornado.platform.auto`.
+"""
+
+def set_close_exec(fd):
+    """Sets the close-on-exec bit (``FD_CLOEXEC``)for a file descriptor."""
+    raise NotImplementedError()
+
+class Waker(object):
+    """A socket-like object that can wake another thread from ``select()``.
+
+    The `~tornado.ioloop.IOLoop` will add the Waker's `fileno()` to
+    its ``select`` (or ``epoll`` or ``kqueue``) calls.  When another
+    thread wants to wake up the loop, it calls `wake`.  Once it has woken
+    up, it will call `consume` to do any necessary per-wake cleanup.  When
+    the ``IOLoop`` is closed, it closes its waker too.
+    """
+    def fileno(self):
+        """Returns a file descriptor for this waker.
+        
+        Must be suitable for use with ``select()`` or equivalent on the
+        local platform.
+        """
+        raise NotImplementedError()
+
+    def wake(self):
+        """Triggers activity on the waker's file descriptor."""
+        raise NotImplementedError()
+
+    def consume(self):
+        """Called after the listen has woken up to do any necessary cleanup."""
+        raise NotImplementedError()
+
+    def close(self):
+        """Closes the waker's file descriptor(s)."""
+        raise NotImplementedError()
+
+    
diff --git a/tornado/platform/posix.py b/tornado/platform/posix.py
new file mode 100644
index 0000000..252d929
--- /dev/null
+++ b/tornado/platform/posix.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python
+#
+# Copyright 2011 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Posix implementations of platform-specific functionality."""
+
+#import fcntl
+import os
+
+from tornado.platform import interface
+from tornado.util import b
+
+def set_close_exec(fd):
+    #flags = fcntl.fcntl(fd, fcntl.F_GETFD)
+    #fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
+    pass
+
+def _set_nonblocking(fd):
+    #flags = fcntl.fcntl(fd, fcntl.F_GETFL)
+    #fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
+    pass
+    
+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/tornado/platform/twisted.py b/tornado/platform/twisted.py
new file mode 100644
index 0000000..5d406d3
--- /dev/null
+++ b/tornado/platform/twisted.py
@@ -0,0 +1,330 @@
+# Author: Ovidiu Predescu
+# Date: July 2011
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+# Note:  This module's docs are not currently extracted automatically,
+# so changes must be made manually to twisted.rst
+# TODO: refactor doc build process to use an appropriate virtualenv
+"""A Twisted reactor built on the Tornado IOLoop.
+
+This module lets you run applications and libraries written for
+Twisted in a Tornado application.  To use it, simply call `install` at
+the beginning of the application::
+
+    import tornado.platform.twisted
+    tornado.platform.twisted.install()
+    from twisted.internet import reactor
+
+When the app is ready to start, call `IOLoop.instance().start()`
+instead of `reactor.run()`.  This will allow you to use a mixture of
+Twisted and Tornado code in the same process.
+
+It is also possible to create a non-global reactor by calling
+`tornado.platform.twisted.TornadoReactor(io_loop)`.  However, if
+the `IOLoop` and reactor are to be short-lived (such as those used in
+unit tests), additional cleanup may be required.  Specifically, it is
+recommended to call::
+
+    reactor.fireSystemEvent('shutdown')
+    reactor.disconnectAll()
+
+before closing the `IOLoop`.
+
+This module has been tested with Twisted versions 11.0.0 and 11.1.0.
+"""
+
+from __future__ import with_statement, absolute_import
+
+import functools
+import logging
+import time
+
+from twisted.internet.posixbase import PosixReactorBase
+from twisted.internet.interfaces import \
+    IReactorFDSet, IDelayedCall, IReactorTime
+from twisted.python import failure, log
+from twisted.internet import error
+
+from zope.interface import implements
+
+import tornado
+import tornado.ioloop
+from tornado.stack_context import NullContext
+from tornado.ioloop import IOLoop
+
+
+class TornadoDelayedCall(object):
+    """DelayedCall object for Tornado."""
+    implements(IDelayedCall)
+
+    def __init__(self, reactor, seconds, f, *args, **kw):
+        self._reactor = reactor
+        self._func = functools.partial(f, *args, **kw)
+        self._time = self._reactor.seconds() + seconds
+        self._timeout = self._reactor._io_loop.add_timeout(self._time,
+                                                           self._called)
+        self._active = True
+
+    def _called(self):
+        self._active = False
+        self._reactor._removeDelayedCall(self)
+        try:
+            self._func()
+        except:
+            logging.error("_called caught exception", exc_info=True)
+
+    def getTime(self):
+        return self._time
+
+    def cancel(self):
+        self._active = False
+        self._reactor._io_loop.remove_timeout(self._timeout)
+        self._reactor._removeDelayedCall(self)
+
+    def delay(self, seconds):
+        self._reactor._io_loop.remove_timeout(self._timeout)
+        self._time += seconds
+        self._timeout = self._reactor._io_loop.add_timeout(self._time,
+                                                           self._called)
+
+    def reset(self, seconds):
+        self._reactor._io_loop.remove_timeout(self._timeout)
+        self._time = self._reactor.seconds() + seconds
+        self._timeout = self._reactor._io_loop.add_timeout(self._time,
+                                                           self._called)
+
+    def active(self):
+        return self._active
+
+class TornadoReactor(PosixReactorBase):
+    """Twisted reactor built on the Tornado IOLoop.
+
+    Since it is intented to be used in applications where the top-level
+    event loop is ``io_loop.start()`` rather than ``reactor.run()``,
+    it is implemented a little differently than other Twisted reactors.
+    We override `mainLoop` instead of `doIteration` and must implement
+    timed call functionality on top of `IOLoop.add_timeout` rather than
+    using the implementation in `PosixReactorBase`.
+    """
+    implements(IReactorTime, IReactorFDSet)
+
+    def __init__(self, io_loop=None):
+        if not io_loop:
+            io_loop = tornado.ioloop.IOLoop.instance()
+        self._io_loop = io_loop
+        self._readers = {}  # map of reader objects to fd
+        self._writers = {}  # map of writer objects to fd
+        self._fds = {} # a map of fd to a (reader, writer) tuple
+        self._delayedCalls = {}
+        PosixReactorBase.__init__(self)
+
+        # IOLoop.start() bypasses some of the reactor initialization.
+        # Fire off the necessary events if they weren't already triggered
+        # by reactor.run().
+        def start_if_necessary():
+            if not self._started:
+                self.fireSystemEvent('startup')
+        self._io_loop.add_callback(start_if_necessary)
+
+    # IReactorTime
+    def seconds(self):
+        return time.time()
+
+    def callLater(self, seconds, f, *args, **kw):
+        dc = TornadoDelayedCall(self, seconds, f, *args, **kw)
+        self._delayedCalls[dc] = True
+        return dc
+
+    def getDelayedCalls(self):
+        return [x for x in self._delayedCalls if x._active]
+
+    def _removeDelayedCall(self, dc):
+        if dc in self._delayedCalls:
+            del self._delayedCalls[dc]
+
+    # IReactorThreads
+    def callFromThread(self, f, *args, **kw):
+        """See `twisted.internet.interfaces.IReactorThreads.callFromThread`"""
+        assert callable(f), "%s is not callable" % f
+        p = functools.partial(f, *args, **kw)
+        self._io_loop.add_callback(p)
+
+    # We don't need the waker code from the super class, Tornado uses
+    # its own waker.
+    def installWaker(self):
+        pass
+
+    def wakeUp(self):
+        pass
+
+    # IReactorFDSet
+    def _invoke_callback(self, fd, events):
+        (reader, writer) = self._fds[fd]
+        if reader:
+            err = None
+            if reader.fileno() == -1:
+                err = error.ConnectionLost()
+            elif events & IOLoop.READ:
+                err = log.callWithLogger(reader, reader.doRead)
+            if err is None and events & IOLoop.ERROR:
+                err = error.ConnectionLost()
+            if err is not None:
+                self.removeReader(reader)
+                reader.readConnectionLost(failure.Failure(err))
+        if writer:
+            err = None
+            if writer.fileno() == -1:
+                err = error.ConnectionLost()
+            elif events & IOLoop.WRITE:
+                err = log.callWithLogger(writer, writer.doWrite)
+            if err is None and events & IOLoop.ERROR:
+                err = error.ConnectionLost()
+            if err is not None:
+                self.removeWriter(writer)
+                writer.writeConnectionLost(failure.Failure(err))
+
+    def addReader(self, reader):
+        """Add a FileDescriptor for notification of data available to read."""
+        if reader in self._readers:
+            # Don't add the reader if it's already there
+            return
+        fd = reader.fileno()
+        self._readers[reader] = fd
+        if fd in self._fds:
+            (_, writer) = self._fds[fd]
+            self._fds[fd] = (reader, writer)
+            if writer:
+                # We already registered this fd for write events,
+                # update it for read events as well.
+                self._io_loop.update_handler(fd, IOLoop.READ | IOLoop.WRITE)
+        else:
+            with NullContext():
+                self._fds[fd] = (reader, None)
+                self._io_loop.add_handler(fd, self._invoke_callback,
+                                         IOLoop.READ)
+
+    def addWriter(self, writer):
+        """Add a FileDescriptor for notification of data available to write."""
+        if writer in self._writers:
+            return
+        fd = writer.fileno()
+        self._writers[writer] = fd
+        if fd in self._fds:
+            (reader, _) = self._fds[fd]
+            self._fds[fd] = (reader, writer)
+            if reader:
+                # We already registered this fd for read events,
+                # update it for write events as well.
+                self._io_loop.update_handler(fd, IOLoop.READ | IOLoop.WRITE)
+        else:
+            with NullContext():
+                self._fds[fd] = (None, writer)
+                self._io_loop.add_handler(fd, self._invoke_callback,
+                                         IOLoop.WRITE)
+
+    def removeReader(self, reader):
+        """Remove a Selectable for notification of data available to read."""
+        if reader in self._readers:
+            fd = self._readers.pop(reader)
+            (_, writer) = self._fds[fd]
+            if writer:
+                # We have a writer so we need to update the IOLoop for
+                # write events only.
+                self._fds[fd] = (None, writer)
+                self._io_loop.update_handler(fd, IOLoop.WRITE)
+            else:
+                # Since we have no writer registered, we remove the
+                # entry from _fds and unregister the handler from the
+                # IOLoop
+                del self._fds[fd]
+                self._io_loop.remove_handler(fd)
+
+    def removeWriter(self, writer):
+        """Remove a Selectable for notification of data available to write."""
+        if writer in self._writers:
+            fd = self._writers.pop(writer)
+            (reader, _) = self._fds[fd]
+            if reader:
+                # We have a reader so we need to update the IOLoop for
+                # read events only.
+                self._fds[fd] = (reader, None)
+                self._io_loop.update_handler(fd, IOLoop.READ)
+            else:
+                # Since we have no reader registered, we remove the
+                # entry from the _fds and unregister the handler from
+                # the IOLoop.
+                del self._fds[fd]
+                self._io_loop.remove_handler(fd)
+
+    def removeAll(self):
+        return self._removeAll(self._readers, self._writers)
+
+    def getReaders(self):
+        return self._readers.keys()
+
+    def getWriters(self):
+        return self._writers.keys()
+
+    # The following functions are mainly used in twisted-style test cases;
+    # it is expected that most users of the TornadoReactor will call
+    # IOLoop.start() instead of Reactor.run().
+    def stop(self):
+        PosixReactorBase.stop(self)
+        self._io_loop.stop()
+
+    def crash(self):
+        PosixReactorBase.crash(self)
+        self._io_loop.stop()
+
+    def doIteration(self, delay):
+        raise NotImplementedError("doIteration")
+
+    def mainLoop(self):
+        self._io_loop.start()
+        if self._stopped:
+            self.fireSystemEvent("shutdown")
+
+class _TestReactor(TornadoReactor):
+    """Subclass of TornadoReactor for use in unittests.
+
+    This can't go in the test.py file because of import-order dependencies
+    with the Twisted reactor test builder.
+    """
+    def __init__(self):
+        # always use a new ioloop
+        super(_TestReactor, self).__init__(IOLoop())
+
+    def listenTCP(self, port, factory, backlog=50, interface=''):
+        # default to localhost to avoid firewall prompts on the mac
+        if not interface:
+            interface = '127.0.0.1'
+        return super(_TestReactor, self).listenTCP(
+            port, factory, backlog=backlog, interface=interface)
+
+    def listenUDP(self, port, protocol, interface='', maxPacketSize=8192):
+        if not interface:
+            interface = '127.0.0.1'
+        return super(_TestReactor, self).listenUDP(
+            port, protocol, interface=interface, maxPacketSize=maxPacketSize)
+
+
+
+def install(io_loop=None):
+    """Install this package as the default Twisted reactor."""
+    if not io_loop:
+        io_loop = tornado.ioloop.IOLoop.instance()
+    reactor = TornadoReactor(io_loop)
+    from twisted.internet.main import installReactor
+    installReactor(reactor)
+    return reactor
diff --git a/tornado/platform/windows.py b/tornado/platform/windows.py
new file mode 100644
index 0000000..1735f1b
--- /dev/null
+++ b/tornado/platform/windows.py
@@ -0,0 +1,97 @@
+# NOTE: win32 support is currently experimental, and not recommended
+# for production use.
+
+import ctypes
+import ctypes.wintypes
+import socket
+import errno
+
+from tornado.platform import interface
+from tornado.util import b
+
+# See: http://msdn.microsoft.com/en-us/library/ms724935(VS.85).aspx
+SetHandleInformation = ctypes.windll.kernel32.SetHandleInformation
+SetHandleInformation.argtypes = (ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD)
+SetHandleInformation.restype = ctypes.wintypes.BOOL
+
+HANDLE_FLAG_INHERIT = 0x00000001
+
+
+def set_close_exec(fd):
+    success = SetHandleInformation(fd, HANDLE_FLAG_INHERIT, 0)
+    if not success:
+        raise ctypes.GetLastError()
+
+
+class Waker(interface.Waker):
+    """Create an OS independent asynchronous pipe"""
+    def __init__(self):
+        # Based on Zope async.py: http://svn.zope.org/zc.ngi/trunk/src/zc/ngi/async.py
+
+        self.writer = socket.socket()
+        # Disable buffering -- pulling the trigger sends 1 byte,
+        # and we want that sent immediately, to wake up ASAP.
+        self.writer.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+
+        count = 0
+        while 1:
+            count += 1
+            # Bind to a local port; for efficiency, let the OS pick
+            # a free port for us.
+            # Unfortunately, stress tests showed that we may not
+            # be able to connect to that port ("Address already in
+            # use") despite that the OS picked it.  This appears
+            # to be a race bug in the Windows socket implementation.
+            # So we loop until a connect() succeeds (almost always
+            # on the first try).  See the long thread at
+            # http://mail.zope.org/pipermail/zope/2005-July/160433.html
+            # for hideous details.
+            a = socket.socket()
+            a.bind(("127.0.0.1", 0))
+            connect_address = a.getsockname()  # assigned (host, port) pair
+            a.listen(1)
+            try:
+                self.writer.connect(connect_address)
+                break    # success
+            except socket.error, detail:
+                if detail[0] != errno.WSAEADDRINUSE:
+                    # "Address already in use" is the only error
+                    # I've seen on two WinXP Pro SP2 boxes, under
+                    # Pythons 2.3.5 and 2.4.1.
+                    raise
+                # (10048, 'Address already in use')
+                # assert count <= 2 # never triggered in Tim's tests
+                if count >= 10:  # I've never seen it go above 2
+                    a.close()
+                    self.writer.close()
+                    raise socket.error("Cannot bind trigger!")
+                # Close `a` and try again.  Note:  I originally put a short
+                # sleep() here, but it didn't appear to help or hurt.
+                a.close()
+
+        self.reader, addr = a.accept()
+        self.reader.setblocking(0)
+        self.writer.setblocking(0)
+        a.close()
+        self.reader_fd = self.reader.fileno()
+
+    def fileno(self):
+        return self.reader.fileno()
+
+    def wake(self):
+        try:
+            self.writer.send(b("x"))
+        except IOError:
+            pass
+
+    def consume(self):
+        try:
+            while True:
+                result = self.reader.recv(1024)
+                if not result: break
+        except IOError:
+            pass
+
+    def close(self):
+        self.reader.close()
+        self.writer.close()
diff --git a/tornado/process.py b/tornado/process.py
new file mode 100644
index 0000000..06f6aa9
--- /dev/null
+++ b/tornado/process.py
@@ -0,0 +1,149 @@
+#!/usr/bin/env python
+#
+# Copyright 2011 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Utilities for working with multiple processes."""
+
+import errno
+import logging
+import os
+import sys
+import time
+
+from binascii import hexlify
+
+from tornado import ioloop
+
+try:
+    import multiprocessing # Python 2.6+
+except ImportError:
+    multiprocessing = None
+
+def cpu_count():
+    """Returns the number of processors on this machine."""
+    if multiprocessing is not None:
+        try:
+            return multiprocessing.cpu_count()
+        except NotImplementedError:
+            pass
+    try:
+        return os.sysconf("SC_NPROCESSORS_CONF")
+    except ValueError:
+        pass
+    logging.error("Could not detect number of processors; assuming 1")
+    return 1
+
+def _reseed_random():
+    if 'random' not in sys.modules:
+        return
+    import random
+    # If os.urandom is available, this method does the same thing as
+    # random.seed (at least as of python 2.6).  If os.urandom is not
+    # available, we mix in the pid in addition to a timestamp.
+    try:
+        seed = long(hexlify(os.urandom(16)), 16)
+    except NotImplementedError:
+        seed = int(time.time() * 1000) ^ os.getpid()
+    random.seed(seed)
+
+
+_task_id = None
+
+def fork_processes(num_processes, max_restarts=100):
+    """Starts multiple worker processes.
+
+    If ``num_processes`` is None or <= 0, we detect the number of cores
+    available on this machine and fork that number of child
+    processes. If ``num_processes`` is given and > 0, we fork that
+    specific number of sub-processes.
+
+    Since we use processes and not threads, there is no shared memory
+    between any server code.
+
+    Note that multiple processes are not compatible with the autoreload
+    module (or the debug=True option to `tornado.web.Application`).
+    When using multiple processes, no IOLoops can be created or
+    referenced until after the call to ``fork_processes``.
+
+    In each child process, ``fork_processes`` returns its *task id*, a
+    number between 0 and ``num_processes``.  Processes that exit
+    abnormally (due to a signal or non-zero exit status) are restarted
+    with the same id (up to ``max_restarts`` times).  In the parent
+    process, ``fork_processes`` returns None if all child processes
+    have exited normally, but will otherwise only exit by throwing an
+    exception.
+    """
+    global _task_id
+    assert _task_id is None
+    if num_processes is None or num_processes <= 0:
+        num_processes = cpu_count()
+    if ioloop.IOLoop.initialized():
+        raise RuntimeError("Cannot run in multiple processes: IOLoop instance "
+                           "has already been initialized. You cannot call "
+                           "IOLoop.instance() before calling start_processes()")
+    logging.info("Starting %d processes", num_processes)
+    children = {}
+    def start_child(i):
+        pid = os.fork()
+        if pid == 0:
+            # child process
+            _reseed_random()
+            global _task_id
+            _task_id = i
+            return i
+        else:
+            children[pid] = i
+            return None
+    for i in range(num_processes):
+        id = start_child(i)
+        if id is not None: return id
+    num_restarts = 0
+    while children:
+        try:
+            pid, status = os.wait()
+        except OSError, e:
+            if e.errno == errno.EINTR:
+                continue
+            raise
+        if pid not in children:
+            continue
+        id = children.pop(pid)
+        if os.WIFSIGNALED(status):
+            logging.warning("child %d (pid %d) killed by signal %d, restarting",
+                            id, pid, os.WTERMSIG(status))
+        elif os.WEXITSTATUS(status) != 0:
+            logging.warning("child %d (pid %d) exited with status %d, restarting",
+                            id, pid, os.WEXITSTATUS(status))
+        else:
+            logging.info("child %d (pid %d) exited normally", id, pid)
+            continue
+        num_restarts += 1
+        if num_restarts > max_restarts:
+            raise RuntimeError("Too many child restarts, giving up")
+        new_id = start_child(id)
+        if new_id is not None: return new_id
+    # All child processes exited cleanly, so exit the master process
+    # instead of just returning to right after the call to
+    # fork_processes (which will probably just start up another IOLoop
+    # unless the caller checks the return value).
+    sys.exit(0)
+
+def task_id():
+    """Returns the current task id, if any.
+
+    Returns None if this process was not created by `fork_processes`.
+    """
+    global _task_id
+    return _task_id
diff --git a/tornado/simple_httpclient.py b/tornado/simple_httpclient.py
new file mode 100644
index 0000000..376d410
--- /dev/null
+++ b/tornado/simple_httpclient.py
@@ -0,0 +1,509 @@
+#!/usr/bin/env python
+from __future__ import with_statement
+
+from tornado.escape import utf8, _unicode, native_str
+from tornado.httpclient import HTTPRequest, HTTPResponse, HTTPError, AsyncHTTPClient, main
+from tornado.httputil import HTTPHeaders
+from tornado.iostream import IOStream, SSLIOStream
+from tornado import stack_context
+from tornado.util import b
+
+import base64
+import collections
+import contextlib
+import copy
+import functools
+import logging
+import os.path
+import re
+import socket
+import sys
+import time
+import urlparse
+import zlib
+
+try:
+    from io import BytesIO  # python 3
+except ImportError:
+    from cStringIO import StringIO as BytesIO  # python 2
+
+try:
+    import ssl # python 2.6+
+except ImportError:
+    ssl = None
+
+_DEFAULT_CA_CERTS = os.path.dirname(__file__) + '/ca-certificates.crt'
+
+class SimpleAsyncHTTPClient(AsyncHTTPClient):
+    """Non-blocking HTTP client with no external dependencies.
+
+    This class implements an HTTP 1.1 client on top of Tornado's IOStreams.
+    It does not currently implement all applicable parts of the HTTP
+    specification, but it does enough to work with major web service APIs
+    (mostly tested against the Twitter API so far).
+
+    This class has not been tested extensively in production and
+    should be considered somewhat experimental as of the release of
+    tornado 1.2.  It is intended to become the default AsyncHTTPClient
+    implementation in a future release.  It may either be used
+    directly, or to facilitate testing of this class with an existing
+    application, setting the environment variable
+    USE_SIMPLE_HTTPCLIENT=1 will cause this class to transparently
+    replace tornado.httpclient.AsyncHTTPClient.
+
+    Some features found in the curl-based AsyncHTTPClient are not yet
+    supported.  In particular, proxies are not supported, connections
+    are not reused, and callers cannot select the network interface to be
+    used.
+
+    Python 2.6 or higher is required for HTTPS support.  Users of Python 2.5
+    should use the curl-based AsyncHTTPClient if HTTPS support is required.
+
+    """
+    def initialize(self, io_loop=None, max_clients=10,
+                   max_simultaneous_connections=None,
+                   hostname_mapping=None, max_buffer_size=104857600):
+        """Creates a AsyncHTTPClient.
+
+        Only a single AsyncHTTPClient instance exists per IOLoop
+        in order to provide limitations on the number of pending connections.
+        force_instance=True may be used to suppress this behavior.
+
+        max_clients is the number of concurrent requests that can be in
+        progress.  max_simultaneous_connections has no effect and is accepted
+        only for compatibility with the curl-based AsyncHTTPClient.  Note
+        that these arguments are only used when the client is first created,
+        and will be ignored when an existing client is reused.
+
+        hostname_mapping is a dictionary mapping hostnames to IP addresses.
+        It can be used to make local DNS changes when modifying system-wide
+        settings like /etc/hosts is not possible or desirable (e.g. in
+        unittests).
+
+        max_buffer_size is the number of bytes that can be read by IOStream. It
+        defaults to 100mb.
+        """
+        self.io_loop = io_loop
+        self.max_clients = max_clients
+        self.queue = collections.deque()
+        self.active = {}
+        self.hostname_mapping = hostname_mapping
+        self.max_buffer_size = max_buffer_size
+
+    def fetch(self, request, callback, **kwargs):
+        if not isinstance(request, HTTPRequest):
+            request = HTTPRequest(url=request, **kwargs)
+        if not isinstance(request.headers, HTTPHeaders):
+            request.headers = HTTPHeaders(request.headers)
+        callback = stack_context.wrap(callback)
+        self.queue.append((request, callback))
+        self._process_queue()
+        if self.queue:
+            logging.debug("max_clients limit reached, request queued. "
+                          "%d active, %d queued requests." % (
+                    len(self.active), len(self.queue)))
+
+    def _process_queue(self):
+        with stack_context.NullContext():
+            while self.queue and len(self.active) < self.max_clients:
+                request, callback = self.queue.popleft()
+                key = object()
+                self.active[key] = (request, callback)
+                _HTTPConnection(self.io_loop, self, request,
+                                functools.partial(self._release_fetch, key),
+                                callback,
+                                self.max_buffer_size)
+
+    def _release_fetch(self, key):
+        del self.active[key]
+        self._process_queue()
+
+
+
+class _HTTPConnection(object):
+    _SUPPORTED_METHODS = set(["GET", "HEAD", "POST", "PUT", "DELETE"])
+
+    def __init__(self, io_loop, client, request, release_callback,
+                 final_callback, max_buffer_size):
+        self.start_time = time.time()
+        self.io_loop = io_loop
+        self.client = client
+        self.request = request
+        self.release_callback = release_callback
+        self.final_callback = final_callback
+        self.code = None
+        self.headers = None
+        self.chunks = None
+        self._decompressor = None
+        # Timeout handle returned by IOLoop.add_timeout
+        self._timeout = None
+        with stack_context.StackContext(self.cleanup):
+            parsed = urlparse.urlsplit(_unicode(self.request.url))
+            if ssl is None and parsed.scheme == "https":
+                raise ValueError("HTTPS requires either python2.6+ or "
+                                 "curl_httpclient")
+            if parsed.scheme not in ("http", "https"):
+                raise ValueError("Unsupported url scheme: %s" %
+                                 self.request.url)
+            # urlsplit results have hostname and port results, but they
+            # didn't support ipv6 literals until python 2.7.
+            netloc = parsed.netloc
+            if "@" in netloc:
+                userpass, _, netloc = netloc.rpartition("@")
+            match = re.match(r'^(.+):(\d+)$', netloc)
+            if match:
+                host = match.group(1)
+                port = int(match.group(2))
+            else:
+                host = netloc
+                port = 443 if parsed.scheme == "https" else 80
+            if re.match(r'^\[.*\]$', host):
+                # raw ipv6 addresses in urls are enclosed in brackets
+                host = host[1:-1]
+            if self.client.hostname_mapping is not None:
+                host = self.client.hostname_mapping.get(host, host)
+
+            if request.allow_ipv6:
+                af = socket.AF_UNSPEC
+            else:
+                # We only try the first IP we get from getaddrinfo,
+                # so restrict to ipv4 by default.
+                af = socket.AF_INET
+
+            addrinfo = socket.getaddrinfo(host, port, af, socket.SOCK_STREAM,
+                                          0, 0)
+            af, socktype, proto, canonname, sockaddr = addrinfo[0]
+
+            if parsed.scheme == "https":
+                ssl_options = {}
+                if request.validate_cert:
+                    ssl_options["cert_reqs"] = ssl.CERT_REQUIRED
+                if request.ca_certs is not None:
+                    ssl_options["ca_certs"] = request.ca_certs
+                else:
+                    ssl_options["ca_certs"] = _DEFAULT_CA_CERTS
+                if request.client_key is not None:
+                    ssl_options["keyfile"] = request.client_key
+                if request.client_cert is not None:
+                    ssl_options["certfile"] = request.client_cert
+
+                # SSL interoperability is tricky.  We want to disable
+                # SSLv2 for security reasons; it wasn't disabled by default
+                # until openssl 1.0.  The best way to do this is to use
+                # the SSL_OP_NO_SSLv2, but that wasn't exposed to python
+                # until 3.2.  Python 2.7 adds the ciphers argument, which
+                # can also be used to disable SSLv2.  As a last resort
+                # on python 2.6, we set ssl_version to SSLv3.  This is
+                # more narrow than we'd like since it also breaks
+                # compatibility with servers configured for TLSv1 only,
+                # but nearly all servers support SSLv3:
+                # http://blog.ivanristic.com/2011/09/ssl-survey-protocol-support.html
+                if sys.version_info >= (2,7):
+                    ssl_options["ciphers"] = "DEFAULT:!SSLv2"
+                else:
+                    # This is really only necessary for pre-1.0 versions
+                    # of openssl, but python 2.6 doesn't expose version
+                    # information.
+                    ssl_options["ssl_version"] = ssl.PROTOCOL_SSLv3
+
+                self.stream = SSLIOStream(socket.socket(af, socktype, proto),
+                                          io_loop=self.io_loop,
+                                          ssl_options=ssl_options,
+                                          max_buffer_size=max_buffer_size)
+            else:
+                self.stream = IOStream(socket.socket(af, socktype, proto),
+                                       io_loop=self.io_loop,
+                                       max_buffer_size=max_buffer_size)
+            timeout = min(request.connect_timeout, request.request_timeout)
+            if timeout:
+                self._timeout = self.io_loop.add_timeout(
+                    self.start_time + timeout,
+                    self._on_timeout)
+            self.stream.set_close_callback(self._on_close)
+            self.stream.connect(sockaddr,
+                                functools.partial(self._on_connect, parsed))
+
+    def _on_timeout(self):
+        self._timeout = None
+        self._run_callback(HTTPResponse(self.request, 599,
+                                        request_time=time.time() - self.start_time,
+                                        error=HTTPError(599, "Timeout")))
+        self.stream.close()
+
+    def _on_connect(self, parsed):
+        if self._timeout is not None:
+            self.io_loop.remove_timeout(self._timeout)
+            self._timeout = None
+        if self.request.request_timeout:
+            self._timeout = self.io_loop.add_timeout(
+                self.start_time + self.request.request_timeout,
+                self._on_timeout)
+        if (self.request.validate_cert and
+            isinstance(self.stream, SSLIOStream)):
+            match_hostname(self.stream.socket.getpeercert(),
+                           parsed.hostname)
+        if (self.request.method not in self._SUPPORTED_METHODS and
+            not self.request.allow_nonstandard_methods):
+            raise KeyError("unknown method %s" % self.request.method)
+        for key in ('network_interface',
+                    'proxy_host', 'proxy_port',
+                    'proxy_username', 'proxy_password'):
+            if getattr(self.request, key, None):
+                raise NotImplementedError('%s not supported' % key)
+        if "Host" not in self.request.headers:
+            self.request.headers["Host"] = parsed.netloc
+        username, password = None, None
+        if parsed.username is not None:
+            username, password = parsed.username, parsed.password
+        elif self.request.auth_username is not None:
+            username = self.request.auth_username
+            password = self.request.auth_password or ''
+        if username is not None:
+            auth = utf8(username) + b(":") + utf8(password)
+            self.request.headers["Authorization"] = (b("Basic ") +
+                                                     base64.b64encode(auth))
+        if self.request.user_agent:
+            self.request.headers["User-Agent"] = self.request.user_agent
+        if not self.request.allow_nonstandard_methods:
+            if self.request.method in ("POST", "PUT"):
+                assert self.request.body is not None
+            else:
+                assert self.request.body is None
+        if self.request.body is not None:
+            self.request.headers["Content-Length"] = str(len(
+                    self.request.body))
+        if (self.request.method == "POST" and
+            "Content-Type" not in self.request.headers):
+            self.request.headers["Content-Type"] = "application/x-www-form-urlencoded"
+        if self.request.use_gzip:
+            self.request.headers["Accept-Encoding"] = "gzip"
+        req_path = ((parsed.path or '/') +
+                (('?' + parsed.query) if parsed.query else ''))
+        request_lines = [utf8("%s %s HTTP/1.1" % (self.request.method,
+                                                  req_path))]
+        for k, v in self.request.headers.get_all():
+            line = utf8(k) + b(": ") + utf8(v)
+            if b('\n') in line:
+                raise ValueError('Newline in header: ' + repr(line))
+            request_lines.append(line)
+        self.stream.write(b("\r\n").join(request_lines) + b("\r\n\r\n"))
+        if self.request.body is not None:
+            self.stream.write(self.request.body)
+        self.stream.read_until_regex(b("\r?\n\r?\n"), self._on_headers)
+
+    def _release(self):
+        if self.release_callback is not None:
+            release_callback = self.release_callback
+            self.release_callback = None
+            release_callback()
+
+    def _run_callback(self, response):
+        self._release()
+        if self.final_callback is not None:
+            final_callback = self.final_callback
+            self.final_callback = None
+            final_callback(response)
+
+    @contextlib.contextmanager
+    def cleanup(self):
+        try:
+            yield
+        except Exception, e:
+            logging.warning("uncaught exception", exc_info=True)
+            self._run_callback(HTTPResponse(self.request, 599, error=e,
+                                request_time=time.time() - self.start_time,
+                                ))
+
+    def _on_close(self):
+        self._run_callback(HTTPResponse(
+                self.request, 599,
+                request_time=time.time() - self.start_time,
+                error=HTTPError(599, "Connection closed")))
+
+    def _on_headers(self, data):
+        data = native_str(data.decode("latin1"))
+        first_line, _, header_data = data.partition("\n")
+        match = re.match("HTTP/1.[01] ([0-9]+)", first_line)
+        assert match
+        self.code = int(match.group(1))
+        self.headers = HTTPHeaders.parse(header_data)
+
+        if "Content-Length" in self.headers:
+            if "," in self.headers["Content-Length"]:
+                # Proxies sometimes cause Content-Length headers to get
+                # duplicated.  If all the values are identical then we can
+                # use them but if they differ it's an error.
+                pieces = re.split(r',\s*', self.headers["Content-Length"])
+                if any(i != pieces[0] for i in pieces):
+                    raise ValueError("Multiple unequal Content-Lengths: %r" %
+                                     self.headers["Content-Length"])
+                self.headers["Content-Length"] = pieces[0]
+            content_length = int(self.headers["Content-Length"])
+        else:
+            content_length = None
+
+        if self.request.header_callback is not None:
+            for k, v in self.headers.get_all():
+                self.request.header_callback("%s: %s\r\n" % (k, v))
+
+        if self.request.method == "HEAD":
+            # HEAD requests never have content, even though they may have
+            # content-length headers
+            self._on_body(b(""))
+            return
+        if 100 <= self.code < 200 or self.code in (204, 304):
+            # These response codes never have bodies
+            # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3
+            assert "Transfer-Encoding" not in self.headers
+            assert content_length in (None, 0)
+            self._on_body(b(""))
+            return
+
+        if (self.request.use_gzip and
+            self.headers.get("Content-Encoding") == "gzip"):
+            # Magic parameter makes zlib module understand gzip header
+            # http://stackoverflow.com/questions/1838699/how-can-i-decompress-a-gzip-stream-with-zlib
+            self._decompressor = zlib.decompressobj(16+zlib.MAX_WBITS)
+        if self.headers.get("Transfer-Encoding") == "chunked":
+            self.chunks = []
+            self.stream.read_until(b("\r\n"), self._on_chunk_length)
+        elif content_length is not None:
+            self.stream.read_bytes(content_length, self._on_body)
+        else:
+            self.stream.read_until_close(self._on_body)
+
+    def _on_body(self, data):
+        if self._timeout is not None:
+            self.io_loop.remove_timeout(self._timeout)
+            self._timeout = None
+        original_request = getattr(self.request, "original_request",
+                                   self.request)
+        if (self.request.follow_redirects and
+            self.request.max_redirects > 0 and
+            self.code in (301, 302, 303, 307)):
+            new_request = copy.copy(self.request)
+            new_request.url = urlparse.urljoin(self.request.url,
+                                               self.headers["Location"])
+            new_request.max_redirects -= 1
+            del new_request.headers["Host"]
+            # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4
+            # client SHOULD make a GET request
+            if self.code == 303:
+                new_request.method = "GET"
+                new_request.body = None
+                for h in ["Content-Length", "Content-Type",
+                          "Content-Encoding", "Transfer-Encoding"]:
+                    try:
+                        del self.request.headers[h]
+                    except KeyError:
+                        pass
+            new_request.original_request = original_request
+            final_callback = self.final_callback
+            self.final_callback = None
+            self._release()
+            self.client.fetch(new_request, final_callback)
+            self.stream.close()
+            return
+        if self._decompressor:
+            data = self._decompressor.decompress(data)
+        if self.request.streaming_callback:
+            if self.chunks is None:
+                # if chunks is not None, we already called streaming_callback
+                # in _on_chunk_data
+                self.request.streaming_callback(data)
+            buffer = BytesIO()
+        else:
+            buffer = BytesIO(data) # TODO: don't require one big string?
+        response = HTTPResponse(original_request,
+                                self.code, headers=self.headers,
+                                request_time=time.time() - self.start_time,
+                                buffer=buffer,
+                                effective_url=self.request.url)
+        self._run_callback(response)
+        self.stream.close()
+
+    def _on_chunk_length(self, data):
+        # TODO: "chunk extensions" http://tools.ietf.org/html/rfc2616#section-3.6.1
+        length = int(data.strip(), 16)
+        if length == 0:
+            # all the data has been decompressed, so we don't need to
+            # decompress again in _on_body
+            self._decompressor = None
+            self._on_body(b('').join(self.chunks))
+        else:
+            self.stream.read_bytes(length + 2,  # chunk ends with \r\n
+                              self._on_chunk_data)
+
+    def _on_chunk_data(self, data):
+        assert data[-2:] == b("\r\n")
+        chunk = data[:-2]
+        if self._decompressor:
+            chunk = self._decompressor.decompress(chunk)
+        if self.request.streaming_callback is not None:
+            self.request.streaming_callback(chunk)
+        else:
+            self.chunks.append(chunk)
+        self.stream.read_until(b("\r\n"), self._on_chunk_length)
+
+
+# match_hostname was added to the standard library ssl module in python 3.2.
+# The following code was backported for older releases and copied from
+# https://bitbucket.org/brandon/backports.ssl_match_hostname
+class CertificateError(ValueError):
+    pass
+
+def _dnsname_to_pat(dn):
+    pats = []
+    for frag in dn.split(r'.'):
+        if frag == '*':
+            # When '*' is a fragment by itself, it matches a non-empty dotless
+            # fragment.
+            pats.append('[^.]+')
+        else:
+            # Otherwise, '*' matches any dotless fragment.
+            frag = re.escape(frag)
+            pats.append(frag.replace(r'\*', '[^.]*'))
+    return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
+
+def match_hostname(cert, hostname):
+    """Verify that *cert* (in decoded format as returned by
+    SSLSocket.getpeercert()) matches the *hostname*.  RFC 2818 rules
+    are mostly followed, but IP addresses are not accepted for *hostname*.
+
+    CertificateError is raised on failure. On success, the function
+    returns nothing.
+    """
+    if not cert:
+        raise ValueError("empty or no certificate")
+    dnsnames = []
+    san = cert.get('subjectAltName', ())
+    for key, value in san:
+        if key == 'DNS':
+            if _dnsname_to_pat(value).match(hostname):
+                return
+            dnsnames.append(value)
+    if not san:
+        # The subject is only checked when subjectAltName is empty
+        for sub in cert.get('subject', ()):
+            for key, value in sub:
+                # XXX according to RFC 2818, the most specific Common Name
+                # must be used.
+                if key == 'commonName':
+                    if _dnsname_to_pat(value).match(hostname):
+                        return
+                    dnsnames.append(value)
+    if len(dnsnames) > 1:
+        raise CertificateError("hostname %r "
+            "doesn't match either of %s"
+            % (hostname, ', '.join(map(repr, dnsnames))))
+    elif len(dnsnames) == 1:
+        raise CertificateError("hostname %r "
+            "doesn't match %r"
+            % (hostname, dnsnames[0]))
+    else:
+        raise CertificateError("no appropriate commonName or "
+            "subjectAltName fields were found")
+
+if __name__ == "__main__":
+    AsyncHTTPClient.configure(SimpleAsyncHTTPClient)
+    main()
diff --git a/tornado/stack_context.py b/tornado/stack_context.py
new file mode 100644
index 0000000..1ba3730
--- /dev/null
+++ b/tornado/stack_context.py
@@ -0,0 +1,244 @@
+#!/usr/bin/env python
+#
+# Copyright 2010 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+'''StackContext allows applications to maintain threadlocal-like state
+that follows execution as it moves to other execution contexts.
+
+The motivating examples are to eliminate the need for explicit
+async_callback wrappers (as in tornado.web.RequestHandler), and to
+allow some additional context to be kept for logging.
+
+This is slightly magic, but it's an extension of the idea that an exception
+handler is a kind of stack-local state and when that stack is suspended
+and resumed in a new context that state needs to be preserved.  StackContext
+shifts the burden of restoring that state from each call site (e.g.
+wrapping each AsyncHTTPClient callback in async_callback) to the mechanisms
+that transfer control from one context to another (e.g. AsyncHTTPClient
+itself, IOLoop, thread pools, etc).
+
+Example usage::
+
+    @contextlib.contextmanager
+    def die_on_error():
+        try:
+            yield
+        except Exception:
+            logging.error("exception in asynchronous operation",exc_info=True)
+            sys.exit(1)
+
+    with StackContext(die_on_error):
+        # Any exception thrown here *or in callback and its desendents*
+        # will cause the process to exit instead of spinning endlessly
+        # in the ioloop.
+        http_client.fetch(url, callback)
+    ioloop.start()
+
+Most applications shouln't have to work with `StackContext` directly.
+Here are a few rules of thumb for when it's necessary:
+
+* If you're writing an asynchronous library that doesn't rely on a
+  stack_context-aware library like `tornado.ioloop` or `tornado.iostream`
+  (for example, if you're writing a thread pool), use
+  `stack_context.wrap()` before any asynchronous operations to capture the
+  stack context from where the operation was started.
+
+* If you're writing an asynchronous library that has some shared
+  resources (such as a connection pool), create those shared resources
+  within a ``with stack_context.NullContext():`` block.  This will prevent
+  ``StackContexts`` from leaking from one request to another.
+
+* If you want to write something like an exception handler that will
+  persist across asynchronous calls, create a new `StackContext` (or
+  `ExceptionStackContext`), and make your asynchronous calls in a ``with``
+  block that references your `StackContext`.
+'''
+
+from __future__ import with_statement
+
+import contextlib
+import functools
+import itertools
+import sys
+import threading
+
+class _State(threading.local):
+    def __init__(self):
+        self.contexts = ()
+_state = _State()
+
+class StackContext(object):
+    '''Establishes the given context as a StackContext that will be transferred.
+
+    Note that the parameter is a callable that returns a context
+    manager, not the context itself.  That is, where for a
+    non-transferable context manager you would say::
+
+      with my_context():
+
+    StackContext takes the function itself rather than its result::
+
+      with StackContext(my_context):
+    '''
+    def __init__(self, context_factory):
+        self.context_factory = context_factory
+
+    # Note that some of this code is duplicated in ExceptionStackContext
+    # below.  ExceptionStackContext is more common and doesn't need
+    # the full generality of this class.
+    def __enter__(self):
+        self.old_contexts = _state.contexts
+        # _state.contexts is a tuple of (class, arg) pairs
+        _state.contexts = (self.old_contexts +
+                           ((StackContext, self.context_factory),))
+        try:
+            self.context = self.context_factory()
+            self.context.__enter__()
+        except Exception:
+            _state.contexts = self.old_contexts
+            raise
+
+    def __exit__(self, type, value, traceback):
+        try:
+            return self.context.__exit__(type, value, traceback)
+        finally:
+            _state.contexts = self.old_contexts
+
+class ExceptionStackContext(object):
+    '''Specialization of StackContext for exception handling.
+
+    The supplied exception_handler function will be called in the
+    event of an uncaught exception in this context.  The semantics are
+    similar to a try/finally clause, and intended use cases are to log
+    an error, close a socket, or similar cleanup actions.  The
+    exc_info triple (type, value, traceback) will be passed to the
+    exception_handler function.
+
+    If the exception handler returns true, the exception will be
+    consumed and will not be propagated to other exception handlers.
+    '''
+    def __init__(self, exception_handler):
+        self.exception_handler = exception_handler
+
+    def __enter__(self):
+        self.old_contexts = _state.contexts
+        _state.contexts = (self.old_contexts +
+                           ((ExceptionStackContext, self.exception_handler),))
+
+    def __exit__(self, type, value, traceback):
+        try:
+            if type is not None:
+                return self.exception_handler(type, value, traceback)
+        finally:
+            _state.contexts = self.old_contexts
+
+class NullContext(object):
+    '''Resets the StackContext.
+
+    Useful when creating a shared resource on demand (e.g. an AsyncHTTPClient)
+    where the stack that caused the creating is not relevant to future
+    operations.
+    '''
+    def __enter__(self):
+        self.old_contexts = _state.contexts
+        _state.contexts = ()
+
+    def __exit__(self, type, value, traceback):
+        _state.contexts = self.old_contexts
+
+class _StackContextWrapper(functools.partial):
+    pass
+
+def wrap(fn):
+    '''Returns a callable object that will restore the current StackContext
+    when executed.
+
+    Use this whenever saving a callback to be executed later in a
+    different execution context (either in a different thread or
+    asynchronously in the same thread).
+    '''
+    if fn is None or fn.__class__ is _StackContextWrapper:
+        return fn
+    # functools.wraps doesn't appear to work on functools.partial objects
+    #@functools.wraps(fn)
+    def wrapped(callback, contexts, *args, **kwargs):
+        if contexts is _state.contexts or not contexts:
+            callback(*args, **kwargs)
+            return
+        if not _state.contexts:
+            new_contexts = [cls(arg) for (cls, arg) in contexts]
+        # If we're moving down the stack, _state.contexts is a prefix
+        # of contexts.  For each element of contexts not in that prefix,
+        # create a new StackContext object.
+        # If we're moving up the stack (or to an entirely different stack),
+        # _state.contexts will have elements not in contexts.  Use
+        # NullContext to clear the state and then recreate from contexts.
+        elif (len(_state.contexts) > len(contexts) or
+            any(a[1] is not b[1]
+                for a, b in itertools.izip(_state.contexts, contexts))):
+            # contexts have been removed or changed, so start over
+            new_contexts = ([NullContext()] +
+                            [cls(arg) for (cls,arg) in contexts])
+        else:
+            new_contexts = [cls(arg)
+                            for (cls, arg) in contexts[len(_state.contexts):]]
+        if len(new_contexts) > 1:
+            with _nested(*new_contexts):
+                callback(*args, **kwargs)
+        elif new_contexts:
+            with new_contexts[0]:
+                callback(*args, **kwargs)
+        else:
+            callback(*args, **kwargs)
+    if _state.contexts:
+        return _StackContextWrapper(wrapped, fn, _state.contexts)
+    else:
+        return _StackContextWrapper(fn)
+
+@contextlib.contextmanager
+def _nested(*managers):
+    """Support multiple context managers in a single with-statement.
+
+    Copied from the python 2.6 standard library.  It's no longer present
+    in python 3 because the with statement natively supports multiple
+    context managers, but that doesn't help if the list of context
+    managers is not known until runtime.
+    """
+    exits = []
+    vars = []
+    exc = (None, None, None)
+    try:
+        for mgr in managers:
+            exit = mgr.__exit__
+            enter = mgr.__enter__
+            vars.append(enter())
+            exits.append(exit)
+        yield vars
+    except:
+        exc = sys.exc_info()
+    finally:
+        while exits:
+            exit = exits.pop()
+            try:
+                if exit(*exc):
+                    exc = (None, None, None)
+            except:
+                exc = sys.exc_info()
+        if exc != (None, None, None):
+            # Don't rely on sys.exc_info() still containing
+            # the right information. Another exception may
+            # have been raised and caught by an exit method
+            raise exc[0], exc[1], exc[2]
+
diff --git a/tornado/template.py b/tornado/template.py
new file mode 100644
index 0000000..139667d
--- /dev/null
+++ b/tornado/template.py
@@ -0,0 +1,826 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""A simple template system that compiles templates to Python code.
+
+Basic usage looks like::
+
+    t = template.Template("<html>{{ myvalue }}</html>")
+    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
+    <html>
+      <head>
+        <title>{% block title %}Default title{% end %}</title>
+      </head>
+      <body>
+        <ul>
+          {% for student in students %}
+            {% block student %}
+              <li>{{ escape(student.name) }}</li>
+            {% end %}
+          {% end %}
+        </ul>
+      </body>
+    </html>
+
+    ### bold.html
+    {% extends "base.html" %}
+
+    {% block title %}A bolder title{% end %}
+
+    {% block student %}
+      <li><span style="bold">{{ escape(student.name) }}</span></li>
+    {% 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] %}
+     <li>{{ escape(student.name) }}</li>
+   {% end %}
+
+Translating directly to Python means you can apply functions to expressions
+easily, like the escape() function in the examples above. You can pass
+functions in to your template just like any other variable::
+
+   ### Python code
+   def add(x, y):
+      return x + y
+   template.execute(add=add)
+
+   ### The template
+   {{ add(1, 2) }}
+
+We provide the functions escape(), url_escape(), json_encode(), and squeeze()
+to all templates by default.
+
+Typical applications do not create `Template` or `Loader` instances by
+hand, but instead use the `render` and `render_string` methods of
+`tornado.web.RequestHandler`, which load templates automatically based
+on the ``template_path`` `Application` setting.
+
+Syntax Reference
+----------------
+
+Template expressions are surrounded by double curly braces: ``{{ ... }}``.
+The contents may be any python expression, which will be escaped according
+to the current autoescape setting and inserted into the output.  Other
+template directives use ``{% %}``.  These tags may be escaped as ``{{!``
+and ``{%!`` if you need to include a literal ``{{`` or ``{%`` in the output.
+
+To comment out a section so that it is omitted from the output, surround it
+with ``{# ... #}``.
+
+``{% apply *function* %}...{% end %}``
+    Applies a function to the output of all template code between ``apply``
+    and ``end``::
+
+        {% apply linkify %}{{name}} said: {{message}}{% end %}
+
+``{% autoescape *function* %}``
+    Sets the autoescape mode for the current file.  This does not affect
+    other files, even those referenced by ``{% include %}``.  Note that
+    autoescaping can also be configured globally, at the `Application`
+    or `Loader`.::
+
+        {% autoescape xhtml_escape %}
+        {% autoescape None %}
+
+``{% block *name* %}...{% end %}``
+    Indicates a named, replaceable block for use with ``{% extends %}``.
+    Blocks in the parent template will be replaced with the contents of
+    the same-named block in a child template.::
+
+        <!-- base.html -->
+        <title>{% block title %}Default title{% end %}</title>
+
+        <!-- mypage.html -->
+        {% extends "base.html" %}
+        {% block title %}My page title{% end %}
+
+``{% comment ... %}``
+    A comment which will be removed from the template output.  Note that
+    there is no ``{% end %}`` tag; the comment goes from the word ``comment``
+    to the closing ``%}`` tag.
+
+``{% extends *filename* %}``
+    Inherit from another template.  Templates that use ``extends`` should
+    contain one or more ``block`` tags to replace content from the parent
+    template.  Anything in the child template not contained in a ``block``
+    tag will be ignored.  For an example, see the ``{% block %}`` tag.
+
+``{% for *var* in *expr* %}...{% end %}``
+    Same as the python ``for`` statement.
+    
+``{% from *x* import *y* %}``
+    Same as the python ``import`` statement.
+
+``{% if *condition* %}...{% elif *condition* %}...{% else %}...{% end %}``
+    Conditional statement - outputs the first section whose condition is
+    true.  (The ``elif`` and ``else`` sections are optional)
+
+``{% import *module* %}``
+    Same as the python ``import`` statement.
+
+``{% include *filename* %}``
+    Includes another template file.  The included file can see all the local
+    variables as if it were copied directly to the point of the ``include``
+    directive (the ``{% autoescape %}`` directive is an exception).
+    Alternately, ``{% module Template(filename, **kwargs) %}`` may be used
+    to include another template with an isolated namespace.
+
+``{% module *expr* %}``
+    Renders a `~tornado.web.UIModule`.  The output of the ``UIModule`` is
+    not escaped::
+
+        {% module Template("foo.html", arg=42) %}
+
+``{% raw *expr* %}``
+    Outputs the result of the given expression without autoescaping.
+
+``{% set *x* = *y* %}``
+    Sets a local variable.
+
+``{% try %}...{% except %}...{% finally %}...{% end %}``
+    Same as the python ``try`` statement.
+
+``{% while *condition* %}... {% end %}``
+    Same as the python ``while`` statement.
+"""
+
+from __future__ import with_statement
+
+import cStringIO
+import datetime
+import linecache
+import logging
+import os.path
+import posixpath
+import re
+import threading
+
+from tornado import escape
+from tornado.util import bytes_type, ObjectDict
+
+_DEFAULT_AUTOESCAPE = "xhtml_escape"
+_UNSET = object()
+
+class Template(object):
+    """A compiled template.
+
+    We compile into Python from the given template_string. You can generate
+    the template from variables with generate().
+    """
+    def __init__(self, template_string, name="<string>", loader=None,
+                 compress_whitespace=None, autoescape=_UNSET):
+        self.name = name
+        if compress_whitespace is None:
+            compress_whitespace = name.endswith(".html") or \
+                name.endswith(".js")
+        if autoescape is not _UNSET:
+            self.autoescape = autoescape
+        elif loader:
+            self.autoescape = loader.autoescape
+        else:
+            self.autoescape = _DEFAULT_AUTOESCAPE
+        self.namespace = loader.namespace if loader else {}
+        reader = _TemplateReader(name, escape.native_str(template_string))
+        self.file = _File(self, _parse(reader, self))
+        self.code = self._generate_python(loader, compress_whitespace)
+        self.loader = loader
+        try:
+            # Under python2.5, the fake filename used here must match
+            # the module name used in __name__ below.
+            self.compiled = compile(
+                escape.to_unicode(self.code),
+                "%s.generated.py" % self.name.replace('.','_'),
+                "exec")
+        except Exception:
+            formatted_code = _format_code(self.code).rstrip()
+            logging.error("%s code:\n%s", self.name, formatted_code)
+            raise
+
+    def generate(self, **kwargs):
+        """Generate this template with the given arguments."""
+        namespace = {
+            "escape": escape.xhtml_escape,
+            "xhtml_escape": escape.xhtml_escape,
+            "url_escape": escape.url_escape,
+            "json_encode": escape.json_encode,
+            "squeeze": escape.squeeze,
+            "linkify": escape.linkify,
+            "datetime": datetime,
+            "_utf8": escape.utf8,  # for internal use
+            "_string_types": (unicode, bytes_type),
+            # __name__ and __loader__ allow the traceback mechanism to find
+            # the generated source code.
+            "__name__": self.name.replace('.', '_'),
+            "__loader__": ObjectDict(get_source=lambda name: self.code),
+        }
+        namespace.update(self.namespace)
+        namespace.update(kwargs)
+        exec self.compiled in namespace
+        execute = namespace["_execute"]
+        # Clear the traceback module's cache of source data now that
+        # we've generated a new template (mainly for this module's
+        # unittests, where different tests reuse the same name).
+        linecache.clearcache()
+        try:
+            return execute()
+        except Exception:
+            formatted_code = _format_code(self.code).rstrip()
+            logging.error("%s code:\n%s", self.name, formatted_code)
+            raise
+
+    def _generate_python(self, loader, compress_whitespace):
+        buffer = cStringIO.StringIO()
+        try:
+            # named_blocks maps from names to _NamedBlock objects
+            named_blocks = {}
+            ancestors = self._get_ancestors(loader)
+            ancestors.reverse()
+            for ancestor in ancestors:
+                ancestor.find_named_blocks(loader, named_blocks)
+            self.file.find_named_blocks(loader, named_blocks)
+            writer = _CodeWriter(buffer, named_blocks, loader, ancestors[0].template,
+                                 compress_whitespace)
+            ancestors[0].generate(writer)
+            return buffer.getvalue()
+        finally:
+            buffer.close()
+
+    def _get_ancestors(self, loader):
+        ancestors = [self.file]
+        for chunk in self.file.body.chunks:
+            if isinstance(chunk, _ExtendsBlock):
+                if not loader:
+                    raise ParseError("{% extends %} block found, but no "
+                                     "template loader")
+                template = loader.load(chunk.name, self.name)
+                ancestors.extend(template._get_ancestors(loader))
+        return ancestors
+
+
+class BaseLoader(object):
+    """Base class for template loaders."""
+    def __init__(self, autoescape=_DEFAULT_AUTOESCAPE, namespace=None):
+        """Creates a template loader.
+
+        root_directory may be the empty string if this loader does not
+        use the filesystem.
+
+        autoescape must be either None or a string naming a function
+        in the template namespace, such as "xhtml_escape".
+        """
+        self.autoescape = autoescape
+        self.namespace = namespace or {}
+        self.templates = {}
+        # self.lock protects self.templates.  It's a reentrant lock
+        # because templates may load other templates via `include` or
+        # `extends`.  Note that thanks to the GIL this code would be safe
+        # even without the lock, but could lead to wasted work as multiple
+        # threads tried to compile the same template simultaneously.
+        self.lock = threading.RLock()
+
+    def reset(self):
+        """Resets the cache of compiled templates."""
+        with self.lock:
+            self.templates = {}
+
+    def resolve_path(self, name, parent_path=None):
+        """Converts a possibly-relative path to absolute (used internally)."""
+        raise NotImplementedError()
+
+    def load(self, name, parent_path=None):
+        """Loads a template."""
+        name = self.resolve_path(name, parent_path=parent_path)
+        with self.lock:
+            if name not in self.templates:
+                self.templates[name] = self._create_template(name)
+            return self.templates[name]
+
+    def _create_template(self, name):
+        raise NotImplementedError()
+
+class Loader(BaseLoader):
+    """A template loader that loads from a single root directory.
+
+    You must use a template loader to use template constructs like
+    {% extends %} and {% include %}. Loader caches all templates after
+    they are loaded the first time.
+    """
+    def __init__(self, root_directory, **kwargs):
+        super(Loader, self).__init__(**kwargs)
+        self.root = os.path.abspath(root_directory)
+
+    def resolve_path(self, name, parent_path=None):
+        if parent_path and not parent_path.startswith("<") and \
+           not parent_path.startswith("/") and \
+           not name.startswith("/"):
+            current_path = os.path.join(self.root, parent_path)
+            file_dir = os.path.dirname(os.path.abspath(current_path))
+            relative_path = os.path.abspath(os.path.join(file_dir, name))
+            if relative_path.startswith(self.root):
+                name = relative_path[len(self.root) + 1:]
+        return name
+
+    def _create_template(self, name):
+        path = os.path.join(self.root, name)
+        f = open(path, "r")
+        template = Template(f.read(), name=name, loader=self)
+        f.close()
+        return template
+
+
+class DictLoader(BaseLoader):
+    """A template loader that loads from a dictionary."""
+    def __init__(self, dict, **kwargs):
+        super(DictLoader, self).__init__(**kwargs)
+        self.dict = dict
+
+    def resolve_path(self, name, parent_path=None):
+        if parent_path and not parent_path.startswith("<") and \
+           not parent_path.startswith("/") and \
+           not name.startswith("/"):
+            file_dir = posixpath.dirname(parent_path)
+            name = posixpath.normpath(posixpath.join(file_dir, name))
+        return name
+
+    def _create_template(self, name):
+        return Template(self.dict[name], name=name, loader=self)
+
+
+class _Node(object):
+    def each_child(self):
+        return ()
+
+    def generate(self, writer):
+        raise NotImplementedError()
+
+    def find_named_blocks(self, loader, named_blocks):
+        for child in self.each_child():
+            child.find_named_blocks(loader, named_blocks)
+
+
+class _File(_Node):
+    def __init__(self, template, body):
+        self.template = template
+        self.body = body
+        self.line = 0
+
+    def generate(self, writer):
+        writer.write_line("def _execute():", self.line)
+        with writer.indent():
+            writer.write_line("_buffer = []", self.line)
+            writer.write_line("_append = _buffer.append", self.line)
+            self.body.generate(writer)
+            writer.write_line("return _utf8('').join(_buffer)", self.line)
+
+    def each_child(self):
+        return (self.body,)
+
+
+
+class _ChunkList(_Node):
+    def __init__(self, chunks):
+        self.chunks = chunks
+
+    def generate(self, writer):
+        for chunk in self.chunks:
+            chunk.generate(writer)
+
+    def each_child(self):
+        return self.chunks
+
+
+class _NamedBlock(_Node):
+    def __init__(self, name, body, template, line):
+        self.name = name
+        self.body = body
+        self.template = template
+        self.line = line
+
+    def each_child(self):
+        return (self.body,)
+
+    def generate(self, writer):
+        block = writer.named_blocks[self.name]
+        with writer.include(block.template, self.line):
+            block.body.generate(writer)
+
+    def find_named_blocks(self, loader, named_blocks):
+        named_blocks[self.name] = self
+        _Node.find_named_blocks(self, loader, named_blocks)
+
+
+class _ExtendsBlock(_Node):
+    def __init__(self, name):
+        self.name = name
+
+
+class _IncludeBlock(_Node):
+    def __init__(self, name, reader, line):
+        self.name = name
+        self.template_name = reader.name
+        self.line = line
+
+    def find_named_blocks(self, loader, named_blocks):
+        included = loader.load(self.name, self.template_name)
+        included.file.find_named_blocks(loader, named_blocks)
+
+    def generate(self, writer):
+        included = writer.loader.load(self.name, self.template_name)
+        with writer.include(included, self.line):
+            included.file.body.generate(writer)
+
+
+class _ApplyBlock(_Node):
+    def __init__(self, method, line, body=None):
+        self.method = method
+        self.line = line
+        self.body = body
+
+    def each_child(self):
+        return (self.body,)
+
+    def generate(self, writer):
+        method_name = "apply%d" % writer.apply_counter
+        writer.apply_counter += 1
+        writer.write_line("def %s():" % method_name, self.line)
+        with writer.indent():
+            writer.write_line("_buffer = []", self.line)
+            writer.write_line("_append = _buffer.append", self.line)
+            self.body.generate(writer)
+            writer.write_line("return _utf8('').join(_buffer)", self.line)
+        writer.write_line("_append(%s(%s()))" % (
+            self.method, method_name), self.line)
+
+
+class _ControlBlock(_Node):
+    def __init__(self, statement, line, body=None):
+        self.statement = statement
+        self.line = line
+        self.body = body
+
+    def each_child(self):
+        return (self.body,)
+
+    def generate(self, writer):
+        writer.write_line("%s:" % self.statement, self.line)
+        with writer.indent():
+            self.body.generate(writer)
+
+
+class _IntermediateControlBlock(_Node):
+    def __init__(self, statement, line):
+        self.statement = statement
+        self.line = line
+
+    def generate(self, writer):
+        writer.write_line("%s:" % self.statement, self.line, writer.indent_size() - 1)
+
+
+class _Statement(_Node):
+    def __init__(self, statement, line):
+        self.statement = statement
+        self.line = line
+
+    def generate(self, writer):
+        writer.write_line(self.statement, self.line)
+
+
+class _Expression(_Node):
+    def __init__(self, expression, line, raw=False):
+        self.expression = expression
+        self.line = line
+        self.raw = raw
+
+    def generate(self, writer):
+        writer.write_line("_tmp = %s" % self.expression, self.line)
+        writer.write_line("if isinstance(_tmp, _string_types):"
+                          " _tmp = _utf8(_tmp)", self.line)
+        writer.write_line("else: _tmp = _utf8(str(_tmp))", self.line)
+        if not self.raw and writer.current_template.autoescape is not None:
+            # In python3 functions like xhtml_escape return unicode,
+            # so we have to convert to utf8 again.
+            writer.write_line("_tmp = _utf8(%s(_tmp))" %
+                              writer.current_template.autoescape, self.line)
+        writer.write_line("_append(_tmp)", self.line)
+
+class _Module(_Expression):
+    def __init__(self, expression, line):
+        super(_Module, self).__init__("_modules." + expression, line,
+                                      raw=True)
+
+class _Text(_Node):
+    def __init__(self, value, line):
+        self.value = value
+        self.line = line
+
+    def generate(self, writer):
+        value = self.value
+
+        # Compress lots of white space to a single character. If the whitespace
+        # breaks a line, have it continue to break a line, but just with a
+        # single \n character
+        if writer.compress_whitespace and "<pre>" not in value:
+            value = re.sub(r"([\t ]+)", " ", value)
+            value = re.sub(r"(\s*\n\s*)", "\n", value)
+
+        if value:
+            writer.write_line('_append(%r)' % escape.utf8(value), self.line)
+
+
+class ParseError(Exception):
+    """Raised for template syntax errors."""
+    pass
+
+
+class _CodeWriter(object):
+    def __init__(self, file, named_blocks, loader, current_template,
+                 compress_whitespace):
+        self.file = file
+        self.named_blocks = named_blocks
+        self.loader = loader
+        self.current_template = current_template
+        self.compress_whitespace = compress_whitespace
+        self.apply_counter = 0
+        self.include_stack = []
+        self._indent = 0
+
+    def indent_size(self):
+        return self._indent
+
+    def indent(self):
+        class Indenter(object):
+            def __enter__(_):
+                self._indent += 1
+                return self
+
+            def __exit__(_, *args):
+                assert self._indent > 0
+                self._indent -= 1
+
+        return Indenter()
+
+    def include(self, template, line):
+        self.include_stack.append((self.current_template, line))
+        self.current_template = template
+
+        class IncludeTemplate(object):
+            def __enter__(_):
+                return self
+
+            def __exit__(_, *args):
+                self.current_template = self.include_stack.pop()[0]
+
+        return IncludeTemplate()
+
+    def write_line(self, line, line_number, indent=None):
+        if indent == None:
+            indent = self._indent
+        line_comment = '  # %s:%d' % (self.current_template.name, line_number)
+        if self.include_stack:
+            ancestors = ["%s:%d" % (tmpl.name, lineno)
+                         for (tmpl, lineno) in self.include_stack]
+            line_comment += ' (via %s)' % ', '.join(reversed(ancestors))
+        print >> self.file, "    "*indent + line + line_comment
+
+
+class _TemplateReader(object):
+    def __init__(self, name, text):
+        self.name = name
+        self.text = text
+        self.line = 1
+        self.pos = 0
+
+    def find(self, needle, start=0, end=None):
+        assert start >= 0, start
+        pos = self.pos
+        start += pos
+        if end is None:
+            index = self.text.find(needle, start)
+        else:
+            end += pos
+            assert end >= start
+            index = self.text.find(needle, start, end)
+        if index != -1:
+            index -= pos
+        return index
+
+    def consume(self, count=None):
+        if count is None:
+            count = len(self.text) - self.pos
+        newpos = self.pos + count
+        self.line += self.text.count("\n", self.pos, newpos)
+        s = self.text[self.pos:newpos]
+        self.pos = newpos
+        return s
+
+    def remaining(self):
+        return len(self.text) - self.pos
+
+    def __len__(self):
+        return self.remaining()
+
+    def __getitem__(self, key):
+        if type(key) is slice:
+            size = len(self)
+            start, stop, step = key.indices(size)
+            if start is None: start = self.pos
+            else: start += self.pos
+            if stop is not None: stop += self.pos
+            return self.text[slice(start, stop, step)]
+        elif key < 0:
+            return self.text[key]
+        else:
+            return self.text[self.pos + key]
+
+    def __str__(self):
+        return self.text[self.pos:]
+
+
+def _format_code(code):
+    lines = code.splitlines()
+    format = "%%%dd  %%s\n" % len(repr(len(lines) + 1))
+    return "".join([format % (i + 1, line) for (i, line) in enumerate(lines)])
+
+
+def _parse(reader, template, in_block=None):
+    body = _ChunkList([])
+    while True:
+        # Find next template directive
+        curly = 0
+        while True:
+            curly = reader.find("{", curly)
+            if curly == -1 or curly + 1 == reader.remaining():
+                # EOF
+                if in_block:
+                    raise ParseError("Missing {%% end %%} block for %s" %
+                                     in_block)
+                body.chunks.append(_Text(reader.consume(), reader.line))
+                return body
+            # If the first curly brace is not the start of a special token,
+            # start searching from the character after it
+            if reader[curly + 1] not in ("{", "%", "#"):
+                curly += 1
+                continue
+            # When there are more than 2 curlies in a row, use the
+            # innermost ones.  This is useful when generating languages
+            # like latex where curlies are also meaningful
+            if (curly + 2 < reader.remaining() and
+                reader[curly + 1] == '{' and reader[curly + 2] == '{'):
+                curly += 1
+                continue
+            break
+
+        # Append any text before the special token
+        if curly > 0:
+            cons = reader.consume(curly)
+            body.chunks.append(_Text(cons, reader.line))
+
+        start_brace = reader.consume(2)
+        line = reader.line
+
+        # Template directives may be escaped as "{{!" or "{%!".
+        # In this case output the braces and consume the "!".
+        # This is especially useful in conjunction with jquery templates,
+        # which also use double braces.
+        if reader.remaining() and reader[0] == "!":
+            reader.consume(1)
+            body.chunks.append(_Text(start_brace, line))
+            continue
+
+        # Comment
+        if start_brace == "{#":
+            end = reader.find("#}")
+            if end == -1:
+                raise ParseError("Missing end expression #} on line %d" % line)
+            contents = reader.consume(end).strip()
+            reader.consume(2)
+            continue
+
+        # Expression
+        if start_brace == "{{":
+            end = reader.find("}}")
+            if end == -1:
+                raise ParseError("Missing end expression }} on line %d" % line)
+            contents = reader.consume(end).strip()
+            reader.consume(2)
+            if not contents:
+                raise ParseError("Empty expression on line %d" % line)
+            body.chunks.append(_Expression(contents, line))
+            continue
+
+        # Block
+        assert start_brace == "{%", start_brace
+        end = reader.find("%}")
+        if end == -1:
+            raise ParseError("Missing end block %%} on line %d" % line)
+        contents = reader.consume(end).strip()
+        reader.consume(2)
+        if not contents:
+            raise ParseError("Empty block tag ({%% %%}) on line %d" % line)
+
+        operator, space, suffix = contents.partition(" ")
+        suffix = suffix.strip()
+
+        # Intermediate ("else", "elif", etc) blocks
+        intermediate_blocks = {
+            "else": set(["if", "for", "while"]),
+            "elif": set(["if"]),
+            "except": set(["try"]),
+            "finally": set(["try"]),
+        }
+        allowed_parents = intermediate_blocks.get(operator)
+        if allowed_parents is not None:
+            if not in_block:
+                raise ParseError("%s outside %s block" %
+                            (operator, allowed_parents))
+            if in_block not in allowed_parents:
+                raise ParseError("%s block cannot be attached to %s block" % (operator, in_block))
+            body.chunks.append(_IntermediateControlBlock(contents, line))
+            continue
+
+        # End tag
+        elif operator == "end":
+            if not in_block:
+                raise ParseError("Extra {%% end %%} block on line %d" % line)
+            return body
+
+        elif operator in ("extends", "include", "set", "import", "from",
+                          "comment", "autoescape", "raw", "module"):
+            if operator == "comment":
+                continue
+            if operator == "extends":
+                suffix = suffix.strip('"').strip("'")
+                if not suffix:
+                    raise ParseError("extends missing file path on line %d" % line)
+                block = _ExtendsBlock(suffix)
+            elif operator in ("import", "from"):
+                if not suffix:
+                    raise ParseError("import missing statement on line %d" % line)
+                block = _Statement(contents, line)
+            elif operator == "include":
+                suffix = suffix.strip('"').strip("'")
+                if not suffix:
+                    raise ParseError("include missing file path on line %d" % line)
+                block = _IncludeBlock(suffix, reader, line)
+            elif operator == "set":
+                if not suffix:
+                    raise ParseError("set missing statement on line %d" % line)
+                block = _Statement(suffix, line)
+            elif operator == "autoescape":
+                fn = suffix.strip()
+                if fn == "None": fn = None
+                template.autoescape = fn
+                continue
+            elif operator == "raw":
+                block = _Expression(suffix, line, raw=True)
+            elif operator == "module":
+                block = _Module(suffix, line)
+            body.chunks.append(block)
+            continue
+
+        elif operator in ("apply", "block", "try", "if", "for", "while"):
+            # parse inner body recursively
+            block_body = _parse(reader, template, operator)
+            if operator == "apply":
+                if not suffix:
+                    raise ParseError("apply missing method name on line %d" % line)
+                block = _ApplyBlock(suffix, line, block_body)
+            elif operator == "block":
+                if not suffix:
+                    raise ParseError("block missing name on line %d" % line)
+                block = _NamedBlock(suffix, block_body, template, line)
+            else:
+                block = _ControlBlock(contents, line, block_body)
+            body.chunks.append(block)
+            continue
+
+        else:
+            raise ParseError("unknown operator: %r" % operator)
diff --git a/tornado/test/README b/tornado/test/README
new file mode 100644
index 0000000..2d6195d
--- /dev/null
+++ b/tornado/test/README
@@ -0,0 +1,4 @@
+Test coverage is almost non-existent, but it's a start.  Be sure to
+set PYTHONPATH apprioriately (generally to the root directory of your
+tornado checkout) when running tests to make sure you're getting the
+version of the tornado package that you expect.
\ No newline at end of file
diff --git a/tornado/test/__init__.py b/tornado/test/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tornado/test/__init__.py
diff --git a/tornado/test/auth_test.py b/tornado/test/auth_test.py
new file mode 100644
index 0000000..2047904
--- /dev/null
+++ b/tornado/test/auth_test.py
@@ -0,0 +1,186 @@
+# These tests do not currently do much to verify the correct implementation
+# of the openid/oauth protocols, they just exercise the major code paths
+# and ensure that it doesn't blow up (e.g. with unicode/bytes issues in
+# python 3)
+
+from tornado.auth import OpenIdMixin, OAuthMixin, OAuth2Mixin
+from tornado.escape import json_decode
+from tornado.testing import AsyncHTTPTestCase, LogTrapTestCase
+from tornado.util import b
+from tornado.web import RequestHandler, Application, asynchronous
+
+class OpenIdClientLoginHandler(RequestHandler, OpenIdMixin):
+    def initialize(self, test):
+        self._OPENID_ENDPOINT = test.get_url('/openid/server/authenticate')
+
+    @asynchronous
+    def get(self):
+        if self.get_argument('openid.mode', None):
+            self.get_authenticated_user(
+                self.on_user, http_client=self.settings['http_client'])
+            return
+        self.authenticate_redirect()
+
+    def on_user(self, user):
+        assert user is not None
+        self.finish(user)
+
+class OpenIdServerAuthenticateHandler(RequestHandler):
+    def post(self):
+        assert self.get_argument('openid.mode') == 'check_authentication'
+        self.write('is_valid:true')
+
+class OAuth1ClientLoginHandler(RequestHandler, OAuthMixin):
+    def initialize(self, test, version):
+        self._OAUTH_VERSION = version
+        self._OAUTH_REQUEST_TOKEN_URL = test.get_url('/oauth1/server/request_token')
+        self._OAUTH_AUTHORIZE_URL = test.get_url('/oauth1/server/authorize')
+        self._OAUTH_ACCESS_TOKEN_URL = test.get_url('/oauth1/server/access_token')
+
+    def _oauth_consumer_token(self):
+        return dict(key='asdf', secret='qwer')
+
+    @asynchronous
+    def get(self):
+        if self.get_argument('oauth_token', None):
+            self.get_authenticated_user(
+                self.on_user, http_client=self.settings['http_client'])
+            return
+        self.authorize_redirect(http_client=self.settings['http_client'])
+
+    def on_user(self, user):
+        assert user is not None
+        self.finish(user)
+
+    def _oauth_get_user(self, access_token, callback):
+        assert access_token == dict(key=b('uiop'), secret=b('5678')), access_token
+        callback(dict(email='foo@example.com'))
+
+class OAuth1ClientRequestParametersHandler(RequestHandler, OAuthMixin):
+    def initialize(self, version):
+        self._OAUTH_VERSION = version
+
+    def _oauth_consumer_token(self):
+        return dict(key='asdf', secret='qwer')
+
+    def get(self):
+        params = self._oauth_request_parameters(
+            'http://www.example.com/api/asdf',
+            dict(key='uiop', secret='5678'),
+            parameters=dict(foo='bar'))
+        import urllib; urllib.urlencode(params)
+        self.write(params)
+
+class OAuth1ServerRequestTokenHandler(RequestHandler):
+    def get(self):
+        self.write('oauth_token=zxcv&oauth_token_secret=1234')
+
+class OAuth1ServerAccessTokenHandler(RequestHandler):
+    def get(self):
+        self.write('oauth_token=uiop&oauth_token_secret=5678')
+
+class OAuth2ClientLoginHandler(RequestHandler, OAuth2Mixin):
+    def initialize(self, test):
+        self._OAUTH_AUTHORIZE_URL = test.get_url('/oauth2/server/authorize')
+
+    def get(self):
+        self.authorize_redirect()
+
+
+class AuthTest(AsyncHTTPTestCase, LogTrapTestCase):
+    def get_app(self):
+        return Application(
+            [
+                # test endpoints
+                ('/openid/client/login', OpenIdClientLoginHandler, dict(test=self)),
+                ('/oauth10/client/login', OAuth1ClientLoginHandler,
+                 dict(test=self, version='1.0')),
+                ('/oauth10/client/request_params',
+                 OAuth1ClientRequestParametersHandler,
+                 dict(version='1.0')),
+                ('/oauth10a/client/login', OAuth1ClientLoginHandler,
+                 dict(test=self, version='1.0a')),
+                ('/oauth10a/client/request_params',
+                 OAuth1ClientRequestParametersHandler,
+                 dict(version='1.0a')),
+                ('/oauth2/client/login', OAuth2ClientLoginHandler, dict(test=self)),
+
+                # simulated servers
+                ('/openid/server/authenticate', OpenIdServerAuthenticateHandler),
+                ('/oauth1/server/request_token', OAuth1ServerRequestTokenHandler),
+                ('/oauth1/server/access_token', OAuth1ServerAccessTokenHandler),
+                ],
+            http_client=self.http_client)
+
+    def test_openid_redirect(self):
+        response = self.fetch('/openid/client/login', follow_redirects=False)
+        self.assertEqual(response.code, 302)
+        self.assertTrue(
+            '/openid/server/authenticate?' in response.headers['Location'])
+
+    def test_openid_get_user(self):
+        response = self.fetch('/openid/client/login?openid.mode=blah&openid.ns.ax=http://openid.net/srv/ax/1.0&openid.ax.type.email=http://axschema.org/contact/email&openid.ax.value.email=foo@example.com')
+        response.rethrow()
+        parsed = json_decode(response.body)
+        self.assertEqual(parsed["email"], "foo@example.com")
+
+    def test_oauth10_redirect(self):
+        response = self.fetch('/oauth10/client/login', follow_redirects=False)
+        self.assertEqual(response.code, 302)
+        self.assertTrue(response.headers['Location'].endswith(
+            '/oauth1/server/authorize?oauth_token=zxcv'))
+        # the cookie is base64('zxcv')|base64('1234')
+        self.assertTrue(
+            '_oauth_request_token="enhjdg==|MTIzNA=="' in response.headers['Set-Cookie'],
+            response.headers['Set-Cookie'])
+
+    def test_oauth10_get_user(self):
+        response = self.fetch(
+            '/oauth10/client/login?oauth_token=zxcv',
+            headers={'Cookie':'_oauth_request_token=enhjdg==|MTIzNA=='})
+        response.rethrow()
+        parsed = json_decode(response.body)
+        self.assertEqual(parsed['email'], 'foo@example.com')
+        self.assertEqual(parsed['access_token'], dict(key='uiop', secret='5678'))
+
+    def test_oauth10_request_parameters(self):
+        response = self.fetch('/oauth10/client/request_params')
+        response.rethrow()
+        parsed = json_decode(response.body)
+        self.assertEqual(parsed['oauth_consumer_key'], 'asdf')
+        self.assertEqual(parsed['oauth_token'], 'uiop')
+        self.assertTrue('oauth_nonce' in parsed)
+        self.assertTrue('oauth_signature' in parsed)
+
+    def test_oauth10a_redirect(self):
+        response = self.fetch('/oauth10a/client/login', follow_redirects=False)
+        self.assertEqual(response.code, 302)
+        self.assertTrue(response.headers['Location'].endswith(
+            '/oauth1/server/authorize?oauth_token=zxcv'))
+        # the cookie is base64('zxcv')|base64('1234')
+        self.assertTrue(
+            '_oauth_request_token="enhjdg==|MTIzNA=="' in response.headers['Set-Cookie'],
+            response.headers['Set-Cookie'])
+
+    def test_oauth10a_get_user(self):
+        response = self.fetch(
+            '/oauth10a/client/login?oauth_token=zxcv',
+            headers={'Cookie':'_oauth_request_token=enhjdg==|MTIzNA=='})
+        response.rethrow()
+        parsed = json_decode(response.body)
+        self.assertEqual(parsed['email'], 'foo@example.com')
+        self.assertEqual(parsed['access_token'], dict(key='uiop', secret='5678'))
+
+    def test_oauth10a_request_parameters(self):
+        response = self.fetch('/oauth10a/client/request_params')
+        response.rethrow()
+        parsed = json_decode(response.body)
+        self.assertEqual(parsed['oauth_consumer_key'], 'asdf')
+        self.assertEqual(parsed['oauth_token'], 'uiop')
+        self.assertTrue('oauth_nonce' in parsed)
+        self.assertTrue('oauth_signature' in parsed)
+
+    def test_oauth2_redirect(self):
+        response = self.fetch('/oauth2/client/login', follow_redirects=False)
+        self.assertEqual(response.code, 302)
+        self.assertTrue('/oauth2/server/authorize?' in response.headers['Location'])
diff --git a/tornado/test/curl_httpclient_test.py b/tornado/test/curl_httpclient_test.py
new file mode 100644
index 0000000..afa56f8
--- /dev/null
+++ b/tornado/test/curl_httpclient_test.py
@@ -0,0 +1,23 @@
+from tornado.test.httpclient_test import HTTPClientCommonTestCase
+
+try:
+    import pycurl
+except ImportError:
+    pycurl = None
+
+if pycurl is not None:
+    from tornado.curl_httpclient import CurlAsyncHTTPClient
+
+class CurlHTTPClientCommonTestCase(HTTPClientCommonTestCase):
+    def get_http_client(self):
+        client = CurlAsyncHTTPClient(io_loop=self.io_loop)
+        # make sure AsyncHTTPClient magic doesn't give us the wrong class
+        self.assertTrue(isinstance(client, CurlAsyncHTTPClient))
+        return client
+
+# Remove the base class from our namespace so the unittest module doesn't
+# try to run it again.
+del HTTPClientCommonTestCase
+
+if pycurl is None:
+    del CurlHTTPClientCommonTestCase
diff --git a/tornado/test/escape_test.py b/tornado/test/escape_test.py
new file mode 100644
index 0000000..42ba50b
--- /dev/null
+++ b/tornado/test/escape_test.py
@@ -0,0 +1,189 @@
+#!/usr/bin/env python
+
+import tornado.escape
+import unittest
+
+from tornado.escape import utf8, xhtml_escape, xhtml_unescape, url_escape, url_unescape, to_unicode, json_decode, json_encode
+from tornado.util import b
+
+linkify_tests = [
+    # (input, linkify_kwargs, expected_output)
+
+    ("hello http://world.com/!", {},
+     u'hello <a href="http://world.com/">http://world.com/</a>!'),
+
+    ("hello http://world.com/with?param=true&stuff=yes", {},
+     u'hello <a href="http://world.com/with?param=true&amp;stuff=yes">http://world.com/with?param=true&amp;stuff=yes</a>'),
+
+    # an opened paren followed by many chars killed Gruber's regex
+    ("http://url.com/w(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", {},
+     u'<a href="http://url.com/w">http://url.com/w</a>(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'),
+
+    # as did too many dots at the end
+    ("http://url.com/withmany.......................................", {},
+     u'<a href="http://url.com/withmany">http://url.com/withmany</a>.......................................'),
+
+    ("http://url.com/withmany((((((((((((((((((((((((((((((((((a)", {},
+     u'<a href="http://url.com/withmany">http://url.com/withmany</a>((((((((((((((((((((((((((((((((((a)'),
+
+    # some examples from http://daringfireball.net/2009/11/liberal_regex_for_matching_urls
+    # plus a fex extras (such as multiple parentheses).
+    ("http://foo.com/blah_blah", {},
+     u'<a href="http://foo.com/blah_blah">http://foo.com/blah_blah</a>'),
+
+    ("http://foo.com/blah_blah/", {},
+     u'<a href="http://foo.com/blah_blah/">http://foo.com/blah_blah/</a>'),
+
+    ("(Something like http://foo.com/blah_blah)", {},
+     u'(Something like <a href="http://foo.com/blah_blah">http://foo.com/blah_blah</a>)'),
+
+    ("http://foo.com/blah_blah_(wikipedia)", {},
+     u'<a href="http://foo.com/blah_blah_(wikipedia)">http://foo.com/blah_blah_(wikipedia)</a>'),
+
+    ("http://foo.com/blah_(blah)_(wikipedia)_blah", {},
+     u'<a href="http://foo.com/blah_(blah)_(wikipedia)_blah">http://foo.com/blah_(blah)_(wikipedia)_blah</a>'),
+
+    ("(Something like http://foo.com/blah_blah_(wikipedia))", {},
+     u'(Something like <a href="http://foo.com/blah_blah_(wikipedia)">http://foo.com/blah_blah_(wikipedia)</a>)'),
+
+    ("http://foo.com/blah_blah.", {},
+     u'<a href="http://foo.com/blah_blah">http://foo.com/blah_blah</a>.'),
+
+    ("http://foo.com/blah_blah/.", {},
+     u'<a href="http://foo.com/blah_blah/">http://foo.com/blah_blah/</a>.'),
+
+    ("<http://foo.com/blah_blah>", {},
+     u'&lt;<a href="http://foo.com/blah_blah">http://foo.com/blah_blah</a>&gt;'),
+
+    ("<http://foo.com/blah_blah/>", {},
+     u'&lt;<a href="http://foo.com/blah_blah/">http://foo.com/blah_blah/</a>&gt;'),
+
+    ("http://foo.com/blah_blah,", {},
+     u'<a href="http://foo.com/blah_blah">http://foo.com/blah_blah</a>,'),
+
+    ("http://www.example.com/wpstyle/?p=364.", {},
+     u'<a href="http://www.example.com/wpstyle/?p=364">http://www.example.com/wpstyle/?p=364</a>.'),
+
+    ("rdar://1234", 
+     {"permitted_protocols": ["http", "rdar"]},
+     u'<a href="rdar://1234">rdar://1234</a>'),
+
+    ("rdar:/1234", 
+     {"permitted_protocols": ["rdar"]},
+     u'<a href="rdar:/1234">rdar:/1234</a>'),
+
+    ("http://userid:password@example.com:8080", {},
+     u'<a href="http://userid:password@example.com:8080">http://userid:password@example.com:8080</a>'),
+
+    ("http://userid@example.com", {},
+     u'<a href="http://userid@example.com">http://userid@example.com</a>'),
+
+    ("http://userid@example.com:8080", {},
+     u'<a href="http://userid@example.com:8080">http://userid@example.com:8080</a>'),
+
+    ("http://userid:password@example.com", {},
+     u'<a href="http://userid:password@example.com">http://userid:password@example.com</a>'),
+
+    ("message://%3c330e7f8409726r6a4ba78dkf1fd71420c1bf6ff@mail.gmail.com%3e",
+     {"permitted_protocols": ["http", "message"]},
+     u'<a href="message://%3c330e7f8409726r6a4ba78dkf1fd71420c1bf6ff@mail.gmail.com%3e">message://%3c330e7f8409726r6a4ba78dkf1fd71420c1bf6ff@mail.gmail.com%3e</a>'),
+
+    (u"http://\u27a1.ws/\u4a39", {},
+     u'<a href="http://\u27a1.ws/\u4a39">http://\u27a1.ws/\u4a39</a>'),
+
+    ("<tag>http://example.com</tag>", {},
+     u'&lt;tag&gt;<a href="http://example.com">http://example.com</a>&lt;/tag&gt;'),
+
+    ("Just a www.example.com link.", {},
+     u'Just a <a href="http://www.example.com">www.example.com</a> link.'),
+
+    ("Just a www.example.com link.", 
+     {"require_protocol": True},
+     u'Just a www.example.com link.'),
+
+    ("A http://reallylong.com/link/that/exceedsthelenglimit.html",
+     {"require_protocol": True, "shorten": True},
+     u'A <a href="http://reallylong.com/link/that/exceedsthelenglimit.html" title="http://reallylong.com/link/that/exceedsthelenglimit.html">http://reallylong.com/link...</a>'),
+
+    ("A http://reallylongdomainnamethatwillbetoolong.com/hi!",
+     {"shorten": True},
+     u'A <a href="http://reallylongdomainnamethatwillbetoolong.com/hi" title="http://reallylongdomainnamethatwillbetoolong.com/hi">http://reallylongdomainnametha...</a>!'),
+
+    ("A file:///passwords.txt and http://web.com link", {},
+     u'A file:///passwords.txt and <a href="http://web.com">http://web.com</a> link'),
+
+    ("A file:///passwords.txt and http://web.com link",
+     {"permitted_protocols": ["file"]},
+     u'A <a href="file:///passwords.txt">file:///passwords.txt</a> and http://web.com link'),
+
+    ("www.external-link.com",
+     {"extra_params": 'rel="nofollow" class="external"'},
+     u'<a href="http://www.external-link.com" rel="nofollow" class="external">www.external-link.com</a>'),
+]
+
+
+class EscapeTestCase(unittest.TestCase):
+    def test_linkify(self):
+        for text, kwargs, html in linkify_tests:
+            linked = tornado.escape.linkify(text, **kwargs)
+            self.assertEqual(linked, html)
+
+    def test_xhtml_escape(self):
+        tests = [
+            ("<foo>", "&lt;foo&gt;"),
+            (u"<foo>", u"&lt;foo&gt;"),
+            (b("<foo>"), b("&lt;foo&gt;")),
+
+            ("<>&\"", "&lt;&gt;&amp;&quot;"),
+            ("&amp;", "&amp;amp;"),
+            ]
+        for unescaped, escaped in tests:
+            self.assertEqual(utf8(xhtml_escape(unescaped)), utf8(escaped))
+            self.assertEqual(utf8(unescaped), utf8(xhtml_unescape(escaped)))
+
+    def test_url_escape(self):
+        tests = [
+            # byte strings are passed through as-is
+            (u'\u00e9'.encode('utf8'), '%C3%A9'),
+            (u'\u00e9'.encode('latin1'), '%E9'),
+
+            # unicode strings become utf8
+            (u'\u00e9', '%C3%A9'),
+            ]
+        for unescaped, escaped in tests:
+            self.assertEqual(url_escape(unescaped), escaped)
+
+    def test_url_unescape(self):
+        tests = [
+            ('%C3%A9', u'\u00e9', 'utf8'),
+            ('%C3%A9', u'\u00c3\u00a9', 'latin1'),
+            ('%C3%A9', utf8(u'\u00e9'), None),
+            ]
+        for escaped, unescaped, encoding in tests:
+            # input strings to url_unescape should only contain ascii
+            # characters, but make sure the function accepts both byte
+            # and unicode strings.
+            self.assertEqual(url_unescape(to_unicode(escaped), encoding), unescaped)
+            self.assertEqual(url_unescape(utf8(escaped), encoding), unescaped)
+
+    def test_escape_return_types(self):
+        # On python2 the escape methods should generally return the same
+        # type as their argument
+        self.assertEqual(type(xhtml_escape("foo")), str)
+        self.assertEqual(type(xhtml_escape(u"foo")), unicode)
+
+    def test_json_decode(self):
+        # json_decode accepts both bytes and unicode, but strings it returns
+        # are always unicode.
+        self.assertEqual(json_decode(b('"foo"')), u"foo")
+        self.assertEqual(json_decode(u'"foo"'), u"foo")
+
+        # Non-ascii bytes are interpreted as utf8
+        self.assertEqual(json_decode(utf8(u'"\u00e9"')), u"\u00e9")
+
+    def test_json_encode(self):
+        # json deals with strings, not bytes, but our encoding function should
+        # accept bytes as well as long as they are utf8.
+        self.assertEqual(json_decode(json_encode(u"\u00e9")), u"\u00e9")
+        self.assertEqual(json_decode(json_encode(utf8(u"\u00e9"))), u"\u00e9")
+        self.assertRaises(UnicodeDecodeError, json_encode, b("\xe9"))
diff --git a/tornado/test/gen_test.py b/tornado/test/gen_test.py
new file mode 100644
index 0000000..935b409
--- /dev/null
+++ b/tornado/test/gen_test.py
@@ -0,0 +1,324 @@
+import functools
+from tornado.escape import url_escape
+from tornado.httpclient import AsyncHTTPClient
+from tornado.testing import AsyncHTTPTestCase, AsyncTestCase, LogTrapTestCase
+from tornado.util import b
+from tornado.web import Application, RequestHandler, asynchronous
+
+from tornado import gen
+
+class GenTest(AsyncTestCase):
+    def run_gen(self, f):
+        f()
+        self.wait()
+
+    def delay_callback(self, iterations, callback, arg):
+        """Runs callback(arg) after a number of IOLoop iterations."""
+        if iterations == 0:
+            callback(arg)
+        else:
+            self.io_loop.add_callback(functools.partial(
+                    self.delay_callback, iterations - 1, callback, arg))
+
+    def test_no_yield(self):
+        @gen.engine
+        def f():
+            self.stop()
+        self.run_gen(f)
+
+    def test_inline_cb(self):
+        @gen.engine
+        def f():
+            (yield gen.Callback("k1"))()
+            res = yield gen.Wait("k1")
+            assert res is None
+            self.stop()
+        self.run_gen(f)
+
+    def test_ioloop_cb(self):
+        @gen.engine
+        def f():
+            self.io_loop.add_callback((yield gen.Callback("k1")))
+            yield gen.Wait("k1")
+            self.stop()
+        self.run_gen(f)
+
+    def test_exception_phase1(self):
+        @gen.engine
+        def f():
+            1/0
+        self.assertRaises(ZeroDivisionError, self.run_gen, f)
+
+    def test_exception_phase2(self):
+        @gen.engine
+        def f():
+            self.io_loop.add_callback((yield gen.Callback("k1")))
+            yield gen.Wait("k1")
+            1/0
+        self.assertRaises(ZeroDivisionError, self.run_gen, f)
+
+    def test_exception_in_task_phase1(self):
+        def fail_task(callback):
+            1/0
+
+        @gen.engine
+        def f():
+            try:
+                yield gen.Task(fail_task)
+                raise Exception("did not get expected exception")
+            except ZeroDivisionError:
+                self.stop()
+        self.run_gen(f)
+
+    def test_exception_in_task_phase2(self):
+        # This is the case that requires the use of stack_context in gen.engine
+        def fail_task(callback):
+            self.io_loop.add_callback(lambda: 1/0)
+
+        @gen.engine
+        def f():
+            try:
+                yield gen.Task(fail_task)
+                raise Exception("did not get expected exception")
+            except ZeroDivisionError:
+                self.stop()
+        self.run_gen(f)
+
+    def test_with_arg(self):
+        @gen.engine
+        def f():
+            (yield gen.Callback("k1"))(42)
+            res = yield gen.Wait("k1")
+            self.assertEqual(42, res)
+            self.stop()
+        self.run_gen(f)
+
+    def test_key_reuse(self):
+        @gen.engine
+        def f():
+            yield gen.Callback("k1")
+            yield gen.Callback("k1")
+            self.stop()
+        self.assertRaises(gen.KeyReuseError, self.run_gen, f)
+
+    def test_key_mismatch(self):
+        @gen.engine
+        def f():
+            yield gen.Callback("k1")
+            yield gen.Wait("k2")
+            self.stop()
+        self.assertRaises(gen.UnknownKeyError, self.run_gen, f)
+
+    def test_leaked_callback(self):
+        @gen.engine
+        def f():
+            yield gen.Callback("k1")
+            self.stop()
+        self.assertRaises(gen.LeakedCallbackError, self.run_gen, f)
+
+    def test_parallel_callback(self):
+        @gen.engine
+        def f():
+            for k in range(3):
+                self.io_loop.add_callback((yield gen.Callback(k)))
+            yield gen.Wait(1)
+            self.io_loop.add_callback((yield gen.Callback(3)))
+            yield gen.Wait(0)
+            yield gen.Wait(3)
+            yield gen.Wait(2)
+            self.stop()
+        self.run_gen(f)
+
+    def test_bogus_yield(self):
+        @gen.engine
+        def f():
+            yield 42
+        self.assertRaises(gen.BadYieldError, self.run_gen, f)
+
+    def test_reuse(self):
+        @gen.engine
+        def f():
+            self.io_loop.add_callback((yield gen.Callback(0)))
+            yield gen.Wait(0)
+            self.stop()
+        self.run_gen(f)
+        self.run_gen(f)
+
+    def test_task(self):
+        @gen.engine
+        def f():
+            yield gen.Task(self.io_loop.add_callback)
+            self.stop()
+        self.run_gen(f)
+
+    def test_wait_all(self):
+        @gen.engine
+        def f():
+            (yield gen.Callback("k1"))("v1")
+            (yield gen.Callback("k2"))("v2")
+            results = yield gen.WaitAll(["k1", "k2"])
+            self.assertEqual(results, ["v1", "v2"])
+            self.stop()
+        self.run_gen(f)
+
+    def test_exception_in_yield(self):
+        @gen.engine
+        def f():
+            try:
+                yield gen.Wait("k1")
+                raise "did not get expected exception"
+            except gen.UnknownKeyError:
+                pass
+            self.stop()
+        self.run_gen(f)
+
+    def test_resume_after_exception_in_yield(self):
+        @gen.engine
+        def f():
+            try:
+                yield gen.Wait("k1")
+                raise "did not get expected exception"
+            except gen.UnknownKeyError:
+                pass
+            (yield gen.Callback("k2"))("v2")
+            self.assertEqual((yield gen.Wait("k2")), "v2")
+            self.stop()
+        self.run_gen(f)
+
+    def test_orphaned_callback(self):
+        @gen.engine
+        def f():
+            self.orphaned_callback = yield gen.Callback(1)
+        try:
+            self.run_gen(f)
+            raise "did not get expected exception"
+        except gen.LeakedCallbackError:
+            pass
+        self.orphaned_callback()
+
+    def test_multi(self):
+        @gen.engine
+        def f():
+            (yield gen.Callback("k1"))("v1")
+            (yield gen.Callback("k2"))("v2")
+            results = yield [gen.Wait("k1"), gen.Wait("k2")]
+            self.assertEqual(results, ["v1", "v2"])
+            self.stop()
+        self.run_gen(f)
+
+    def test_multi_delayed(self):
+        @gen.engine
+        def f():
+            # callbacks run at different times
+            responses = yield [
+                gen.Task(self.delay_callback, 3, arg="v1"),
+                gen.Task(self.delay_callback, 1, arg="v2"),
+                ]
+            self.assertEqual(responses, ["v1", "v2"])
+            self.stop()
+        self.run_gen(f)
+
+    def test_arguments(self):
+        @gen.engine
+        def f():
+            (yield gen.Callback("noargs"))()
+            self.assertEqual((yield gen.Wait("noargs")), None)
+            (yield gen.Callback("1arg"))(42)
+            self.assertEqual((yield gen.Wait("1arg")), 42)
+
+            (yield gen.Callback("kwargs"))(value=42)
+            result = yield gen.Wait("kwargs")
+            self.assertTrue(isinstance(result, gen.Arguments))
+            self.assertEqual(((), dict(value=42)), result)
+            self.assertEqual(dict(value=42), result.kwargs)
+
+            (yield gen.Callback("2args"))(42, 43)
+            result = yield gen.Wait("2args")
+            self.assertTrue(isinstance(result, gen.Arguments))
+            self.assertEqual(((42, 43), {}), result)
+            self.assertEqual((42, 43), result.args)
+
+            def task_func(callback):
+                callback(None, error="foo")
+            result = yield gen.Task(task_func)
+            self.assertTrue(isinstance(result, gen.Arguments))
+            self.assertEqual(((None,), dict(error="foo")), result)
+
+            self.stop()
+        self.run_gen(f)
+
+
+class GenSequenceHandler(RequestHandler):
+    @asynchronous
+    @gen.engine
+    def get(self):
+        self.io_loop = self.request.connection.stream.io_loop
+        self.io_loop.add_callback((yield gen.Callback("k1")))
+        yield gen.Wait("k1")
+        self.write("1")
+        self.io_loop.add_callback((yield gen.Callback("k2")))
+        yield gen.Wait("k2")
+        self.write("2")
+        # reuse an old key
+        self.io_loop.add_callback((yield gen.Callback("k1")))
+        yield gen.Wait("k1")
+        self.finish("3")
+
+class GenTaskHandler(RequestHandler):
+    @asynchronous
+    @gen.engine
+    def get(self):
+        io_loop = self.request.connection.stream.io_loop
+        client = AsyncHTTPClient(io_loop=io_loop)
+        response = yield gen.Task(client.fetch, self.get_argument('url'))
+        response.rethrow()
+        self.finish(b("got response: ") + response.body)
+
+class GenExceptionHandler(RequestHandler):
+    @asynchronous
+    @gen.engine
+    def get(self):
+        # This test depends on the order of the two decorators.
+        io_loop = self.request.connection.stream.io_loop
+        yield gen.Task(io_loop.add_callback)
+        raise Exception("oops")
+
+class GenYieldExceptionHandler(RequestHandler):
+    @asynchronous
+    @gen.engine
+    def get(self):
+        io_loop = self.request.connection.stream.io_loop
+        # Test the interaction of the two stack_contexts.
+        def fail_task(callback):
+            io_loop.add_callback(lambda: 1/0)
+        try:
+            yield gen.Task(fail_task)
+            raise Exception("did not get expected exception")
+        except ZeroDivisionError:
+            self.finish('ok')
+
+class GenWebTest(AsyncHTTPTestCase, LogTrapTestCase):
+    def get_app(self):
+        return Application([
+                ('/sequence', GenSequenceHandler),
+                ('/task', GenTaskHandler),
+                ('/exception', GenExceptionHandler),
+                ('/yield_exception', GenYieldExceptionHandler),
+                ])
+
+    def test_sequence_handler(self):
+        response = self.fetch('/sequence')
+        self.assertEqual(response.body, b("123"))
+
+    def test_task_handler(self):
+        response = self.fetch('/task?url=%s' % url_escape(self.get_url('/sequence')))
+        self.assertEqual(response.body, b("got response: 123"))
+
+    def test_exception_handler(self):
+        # Make sure we get an error and not a timeout
+        response = self.fetch('/exception')
+        self.assertEqual(500, response.code)
+
+    def test_yield_exception_handler(self):
+        response = self.fetch('/yield_exception')
+        self.assertEqual(response.body, b('ok'))
diff --git a/tornado/test/httpclient_test.py b/tornado/test/httpclient_test.py
new file mode 100644
index 0000000..8388338
--- /dev/null
+++ b/tornado/test/httpclient_test.py
@@ -0,0 +1,192 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+
+import base64
+import binascii
+from contextlib import closing
+import functools
+
+from tornado.escape import utf8
+from tornado.httpclient import AsyncHTTPClient
+from tornado.iostream import IOStream
+from tornado import netutil
+from tornado.testing import AsyncHTTPTestCase, LogTrapTestCase, get_unused_port
+from tornado.util import b, bytes_type
+from tornado.web import Application, RequestHandler, url
+
+class HelloWorldHandler(RequestHandler):
+    def get(self):
+        name = self.get_argument("name", "world")
+        self.set_header("Content-Type", "text/plain")
+        self.finish("Hello %s!" % name)
+
+class PostHandler(RequestHandler):
+    def post(self):
+        self.finish("Post arg1: %s, arg2: %s" % (
+            self.get_argument("arg1"), self.get_argument("arg2")))
+
+class ChunkHandler(RequestHandler):
+    def get(self):
+        self.write("asdf")
+        self.flush()
+        self.write("qwer")
+
+class AuthHandler(RequestHandler):
+    def get(self):
+        self.finish(self.request.headers["Authorization"])
+
+class CountdownHandler(RequestHandler):
+    def get(self, count):
+        count = int(count)
+        if count > 0:
+            self.redirect(self.reverse_url("countdown", count - 1))
+        else:
+            self.write("Zero")
+
+class EchoPostHandler(RequestHandler):
+    def post(self):
+        self.write(self.request.body)
+
+# These tests end up getting run redundantly: once here with the default
+# HTTPClient implementation, and then again in each implementation's own
+# test suite.
+class HTTPClientCommonTestCase(AsyncHTTPTestCase, LogTrapTestCase):
+    def get_http_client(self):
+        """Returns AsyncHTTPClient instance.  May be overridden in subclass."""
+        return AsyncHTTPClient(io_loop=self.io_loop)
+
+    def get_app(self):
+        return Application([
+            url("/hello", HelloWorldHandler),
+            url("/post", PostHandler),
+            url("/chunk", ChunkHandler),
+            url("/auth", AuthHandler),
+            url("/countdown/([0-9]+)", CountdownHandler, name="countdown"),
+            url("/echopost", EchoPostHandler),
+            ], gzip=True)
+
+    def setUp(self):
+        super(HTTPClientCommonTestCase, self).setUp()
+        # replace the client defined in the parent class
+        self.http_client = self.get_http_client()
+
+    def test_hello_world(self):
+        response = self.fetch("/hello")
+        self.assertEqual(response.code, 200)
+        self.assertEqual(response.headers["Content-Type"], "text/plain")
+        self.assertEqual(response.body, b("Hello world!"))
+        self.assertEqual(int(response.request_time), 0)
+
+        response = self.fetch("/hello?name=Ben")
+        self.assertEqual(response.body, b("Hello Ben!"))
+
+    def test_streaming_callback(self):
+        # streaming_callback is also tested in test_chunked
+        chunks = []
+        response = self.fetch("/hello",
+                              streaming_callback=chunks.append)
+        # with streaming_callback, data goes to the callback and not response.body
+        self.assertEqual(chunks, [b("Hello world!")])
+        self.assertFalse(response.body)
+
+    def test_post(self):
+        response = self.fetch("/post", method="POST",
+                              body="arg1=foo&arg2=bar")
+        self.assertEqual(response.code, 200)
+        self.assertEqual(response.body, b("Post arg1: foo, arg2: bar"))
+
+    def test_chunked(self):
+        response = self.fetch("/chunk")
+        self.assertEqual(response.body, b("asdfqwer"))
+
+        chunks = []
+        response = self.fetch("/chunk",
+                              streaming_callback=chunks.append)
+        self.assertEqual(chunks, [b("asdf"), b("qwer")])
+        self.assertFalse(response.body)
+
+    def test_chunked_close(self):
+        # test case in which chunks spread read-callback processing
+        # over several ioloop iterations, but the connection is already closed.
+        port = get_unused_port()
+        (sock,) = netutil.bind_sockets(port, address="127.0.0.1")
+        with closing(sock):
+            def write_response(stream, request_data):
+                stream.write(b("""\
+HTTP/1.1 200 OK
+Transfer-Encoding: chunked
+
+1
+1
+1
+2
+0
+
+""").replace(b("\n"), b("\r\n")), callback=stream.close)
+            def accept_callback(conn, address):
+                # fake an HTTP server using chunked encoding where the final chunks
+                # and connection close all happen at once
+                stream = IOStream(conn, io_loop=self.io_loop)
+                stream.read_until(b("\r\n\r\n"),
+                                  functools.partial(write_response, stream))
+            netutil.add_accept_handler(sock, accept_callback, self.io_loop)
+            self.http_client.fetch("http://127.0.0.1:%d/" % port, self.stop)
+            resp = self.wait()
+            resp.rethrow()
+            self.assertEqual(resp.body, b("12"))
+        
+
+    def test_basic_auth(self):
+        self.assertEqual(self.fetch("/auth", auth_username="Aladdin",
+                                    auth_password="open sesame").body,
+                         b("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="))
+
+    def test_follow_redirect(self):
+        response = self.fetch("/countdown/2", follow_redirects=False)
+        self.assertEqual(302, response.code)
+        self.assertTrue(response.headers["Location"].endswith("/countdown/1"))
+
+        response = self.fetch("/countdown/2")
+        self.assertEqual(200, response.code)
+        self.assertTrue(response.effective_url.endswith("/countdown/0"))
+        self.assertEqual(b("Zero"), response.body)
+
+    def test_credentials_in_url(self):
+        url = self.get_url("/auth").replace("http://", "http://me:secret@")
+        self.http_client.fetch(url, self.stop)
+        response = self.wait()
+        self.assertEqual(b("Basic ") + base64.b64encode(b("me:secret")),
+                         response.body)
+
+    def test_body_encoding(self):
+        unicode_body = u"\xe9"
+        byte_body = binascii.a2b_hex(b("e9"))
+
+        # unicode string in body gets converted to utf8
+        response = self.fetch("/echopost", method="POST", body=unicode_body,
+                              headers={"Content-Type": "application/blah"})
+        self.assertEqual(response.headers["Content-Length"], "2")
+        self.assertEqual(response.body, utf8(unicode_body))
+
+        # byte strings pass through directly
+        response = self.fetch("/echopost", method="POST",
+                              body=byte_body,
+                              headers={"Content-Type": "application/blah"})
+        self.assertEqual(response.headers["Content-Length"], "1")
+        self.assertEqual(response.body, byte_body)
+
+        # Mixing unicode in headers and byte string bodies shouldn't
+        # break anything
+        response = self.fetch("/echopost", method="POST", body=byte_body,
+                              headers={"Content-Type": "application/blah"},
+                              user_agent=u"foo")
+        self.assertEqual(response.headers["Content-Length"], "1")
+        self.assertEqual(response.body, byte_body)
+
+    def test_types(self):
+        response = self.fetch("/hello")
+        self.assertEqual(type(response.body), bytes_type)
+        self.assertEqual(type(response.headers["Content-Type"]), str)
+        self.assertEqual(type(response.code), int)
+        self.assertEqual(type(response.effective_url), str)
diff --git a/tornado/test/httpserver_test.py b/tornado/test/httpserver_test.py
new file mode 100644
index 0000000..1a53a34
--- /dev/null
+++ b/tornado/test/httpserver_test.py
@@ -0,0 +1,373 @@
+#!/usr/bin/env python
+
+from tornado import httpclient, simple_httpclient, netutil
+from tornado.escape import json_decode, utf8, _unicode, recursive_unicode, native_str
+from tornado.httpserver import HTTPServer
+from tornado.httputil import HTTPHeaders
+from tornado.iostream import IOStream
+from tornado.simple_httpclient import SimpleAsyncHTTPClient
+from tornado.testing import AsyncHTTPTestCase, LogTrapTestCase, AsyncTestCase
+from tornado.util import b, bytes_type
+from tornado.web import Application, RequestHandler
+import os
+import shutil
+import socket
+import sys
+import tempfile
+
+try:
+    import ssl
+except ImportError:
+    ssl = None
+
+class HandlerBaseTestCase(AsyncHTTPTestCase, LogTrapTestCase):
+    def get_app(self):
+        return Application([('/', self.__class__.Handler)])
+
+    def fetch_json(self, *args, **kwargs):
+        response = self.fetch(*args, **kwargs)
+        response.rethrow()
+        return json_decode(response.body)
+
+class HelloWorldRequestHandler(RequestHandler):
+    def initialize(self, protocol="http"):
+        self.expected_protocol = protocol
+
+    def get(self):
+        assert self.request.protocol == self.expected_protocol
+        self.finish("Hello world")
+
+    def post(self):
+        self.finish("Got %d bytes in POST" % len(self.request.body))
+
+class BaseSSLTest(AsyncHTTPTestCase, LogTrapTestCase):
+    def get_ssl_version(self):
+        raise NotImplementedError()
+
+    def setUp(self):
+        super(BaseSSLTest, self).setUp()
+        # Replace the client defined in the parent class.
+        # Some versions of libcurl have deadlock bugs with ssl,
+        # so always run these tests with SimpleAsyncHTTPClient.
+        self.http_client = SimpleAsyncHTTPClient(io_loop=self.io_loop,
+                                                 force_instance=True)
+
+    def get_app(self):
+        return Application([('/', HelloWorldRequestHandler, 
+                             dict(protocol="https"))])
+
+    def get_httpserver_options(self):
+        # Testing keys were generated with:
+        # openssl req -new -keyout tornado/test/test.key -out tornado/test/test.crt -nodes -days 3650 -x509
+        test_dir = os.path.dirname(__file__)
+        return dict(ssl_options=dict(
+                certfile=os.path.join(test_dir, 'test.crt'),
+                keyfile=os.path.join(test_dir, 'test.key'),
+                ssl_version=self.get_ssl_version()))
+
+    def fetch(self, path, **kwargs):
+        self.http_client.fetch(self.get_url(path).replace('http', 'https'),
+                               self.stop,
+                               validate_cert=False,
+                               **kwargs)
+        return self.wait()
+
+class SSLTestMixin(object):
+    def test_ssl(self):
+        response = self.fetch('/')
+        self.assertEqual(response.body, b("Hello world"))
+
+    def test_large_post(self):
+        response = self.fetch('/',
+                              method='POST',
+                              body='A'*5000)
+        self.assertEqual(response.body, b("Got 5000 bytes in POST"))
+
+    def test_non_ssl_request(self):
+        # Make sure the server closes the connection when it gets a non-ssl
+        # connection, rather than waiting for a timeout or otherwise
+        # misbehaving.
+        self.http_client.fetch(self.get_url("/"), self.stop,
+                               request_timeout=3600,
+                               connect_timeout=3600)
+        response = self.wait()
+        self.assertEqual(response.code, 599)
+
+# Python's SSL implementation differs significantly between versions.
+# For example, SSLv3 and TLSv1 throw an exception if you try to read
+# from the socket before the handshake is complete, but the default
+# of SSLv23 allows it.
+class SSLv23Test(BaseSSLTest, SSLTestMixin):
+    def get_ssl_version(self): return ssl.PROTOCOL_SSLv23
+class SSLv3Test(BaseSSLTest, SSLTestMixin):
+    def get_ssl_version(self): return ssl.PROTOCOL_SSLv3
+class TLSv1Test(BaseSSLTest, SSLTestMixin):
+    def get_ssl_version(self): return ssl.PROTOCOL_TLSv1
+
+if hasattr(ssl, 'PROTOCOL_SSLv2'):
+    class SSLv2Test(BaseSSLTest):
+        def get_ssl_version(self): return ssl.PROTOCOL_SSLv2
+
+        def test_sslv2_fail(self):
+            # This is really more of a client test, but run it here since
+            # we've got all the other ssl version tests here.
+            # Clients should have SSLv2 disabled by default.
+            try:
+                # The server simply closes the connection when it gets
+                # an SSLv2 ClientHello packet.
+                # request_timeout is needed here because on some platforms
+                # (cygwin, but not native windows python), the close is not
+                # detected promptly.
+                response = self.fetch('/', request_timeout=1)
+            except ssl.SSLError:
+                # In some python/ssl builds the PROTOCOL_SSLv2 constant
+                # exists but SSLv2 support is still compiled out, which
+                # would result in an SSLError here (details vary depending
+                # on python version).  The important thing is that
+                # SSLv2 request's don't succeed, so we can just ignore
+                # the errors here.
+                return
+            self.assertEqual(response.code, 599)
+
+if ssl is None:
+    del BaseSSLTest
+    del SSLv23Test
+    del SSLv3Test
+    del TLSv1Test
+elif getattr(ssl, 'OPENSSL_VERSION_INFO', (0,0)) < (1,0):
+    # In pre-1.0 versions of openssl, SSLv23 clients always send SSLv2
+    # ClientHello messages, which are rejected by SSLv3 and TLSv1
+    # servers.  Note that while the OPENSSL_VERSION_INFO was formally
+    # introduced in python3.2, it was present but undocumented in
+    # python 2.7
+    del SSLv3Test
+    del TLSv1Test
+
+class MultipartTestHandler(RequestHandler):
+    def post(self):
+        self.finish({"header": self.request.headers["X-Header-Encoding-Test"],
+                     "argument": self.get_argument("argument"),
+                     "filename": self.request.files["files"][0].filename,
+                     "filebody": _unicode(self.request.files["files"][0]["body"]),
+                     })
+
+class RawRequestHTTPConnection(simple_httpclient._HTTPConnection):
+    def set_request(self, request):
+        self.__next_request = request
+
+    def _on_connect(self, parsed):
+        self.stream.write(self.__next_request)
+        self.__next_request = None
+        self.stream.read_until(b("\r\n\r\n"), self._on_headers)
+
+# This test is also called from wsgi_test
+class HTTPConnectionTest(AsyncHTTPTestCase, LogTrapTestCase):
+    def get_handlers(self):
+        return [("/multipart", MultipartTestHandler),
+                ("/hello", HelloWorldRequestHandler)]
+
+    def get_app(self):
+        return Application(self.get_handlers())
+
+    def raw_fetch(self, headers, body):
+        client = SimpleAsyncHTTPClient(self.io_loop)
+        conn = RawRequestHTTPConnection(self.io_loop, client,
+                                        httpclient.HTTPRequest(self.get_url("/")),
+                                        None, self.stop,
+                                        1024*1024)
+        conn.set_request(
+            b("\r\n").join(headers +
+                           [utf8("Content-Length: %d\r\n" % len(body))]) +
+            b("\r\n") + body)
+        response = self.wait()
+        client.close()
+        response.rethrow()
+        return response
+
+    def test_multipart_form(self):
+        # Encodings here are tricky:  Headers are latin1, bodies can be
+        # anything (we use utf8 by default).
+        response = self.raw_fetch([
+                b("POST /multipart HTTP/1.0"),
+                b("Content-Type: multipart/form-data; boundary=1234567890"),
+                b("X-Header-encoding-test: \xe9"),
+                ],
+                                  b("\r\n").join([
+                    b("Content-Disposition: form-data; name=argument"),
+                    b(""),
+                    u"\u00e1".encode("utf-8"),
+                    b("--1234567890"),
+                    u'Content-Disposition: form-data; name="files"; filename="\u00f3"'.encode("utf8"),
+                    b(""),
+                    u"\u00fa".encode("utf-8"),
+                    b("--1234567890--"),
+                    b(""),
+                    ]))
+        data = json_decode(response.body)
+        self.assertEqual(u"\u00e9", data["header"])
+        self.assertEqual(u"\u00e1", data["argument"])
+        self.assertEqual(u"\u00f3", data["filename"])
+        self.assertEqual(u"\u00fa", data["filebody"])
+
+    def test_100_continue(self):
+        # Run through a 100-continue interaction by hand:
+        # When given Expect: 100-continue, we get a 100 response after the
+        # headers, and then the real response after the body.
+        stream = IOStream(socket.socket(), io_loop=self.io_loop)
+        stream.connect(("localhost", self.get_http_port()), callback=self.stop)
+        self.wait()
+        stream.write(b("\r\n").join([b("POST /hello HTTP/1.1"),
+                                  b("Content-Length: 1024"),
+                                  b("Expect: 100-continue"),
+                                  b("\r\n")]), callback=self.stop)
+        self.wait()
+        stream.read_until(b("\r\n\r\n"), self.stop)
+        data = self.wait()
+        self.assertTrue(data.startswith(b("HTTP/1.1 100 ")), data)
+        stream.write(b("a") * 1024)
+        stream.read_until(b("\r\n"), self.stop)
+        first_line = self.wait()
+        self.assertTrue(first_line.startswith(b("HTTP/1.1 200")), first_line)
+        stream.read_until(b("\r\n\r\n"), self.stop)
+        header_data = self.wait()
+        headers = HTTPHeaders.parse(native_str(header_data.decode('latin1')))
+        stream.read_bytes(int(headers["Content-Length"]), self.stop)
+        body = self.wait()
+        self.assertEqual(body, b("Got 1024 bytes in POST"))
+
+class EchoHandler(RequestHandler):
+    def get(self):
+        self.write(recursive_unicode(self.request.arguments))
+
+class TypeCheckHandler(RequestHandler):
+    def prepare(self):
+        self.errors = {}
+        fields = [
+            ('method', str),
+            ('uri', str),
+            ('version', str),
+            ('remote_ip', str),
+            ('protocol', str),
+            ('host', str),
+            ('path', str),
+            ('query', str),
+            ]
+        for field, expected_type in fields:
+            self.check_type(field, getattr(self.request, field), expected_type)
+
+        self.check_type('header_key', self.request.headers.keys()[0], str)
+        self.check_type('header_value', self.request.headers.values()[0], str)
+
+        self.check_type('cookie_key', self.request.cookies.keys()[0], str)
+        self.check_type('cookie_value', self.request.cookies.values()[0].value, str)
+        # secure cookies
+
+        self.check_type('arg_key', self.request.arguments.keys()[0], str)
+        self.check_type('arg_value', self.request.arguments.values()[0][0], bytes_type)
+
+    def post(self):
+        self.check_type('body', self.request.body, bytes_type)
+        self.write(self.errors)
+
+    def get(self):
+        self.write(self.errors)
+
+    def check_type(self, name, obj, expected_type):
+        actual_type = type(obj)
+        if expected_type != actual_type:
+            self.errors[name] = "expected %s, got %s" % (expected_type, 
+                                                         actual_type)
+
+class HTTPServerTest(AsyncHTTPTestCase, LogTrapTestCase):
+    def get_app(self):
+        return Application([("/echo", EchoHandler),
+                            ("/typecheck", TypeCheckHandler),
+                            ])
+
+    def test_query_string_encoding(self):
+        response = self.fetch("/echo?foo=%C3%A9")
+        data = json_decode(response.body)
+        self.assertEqual(data, {u"foo": [u"\u00e9"]})
+
+    def test_types(self):
+        headers = {"Cookie": "foo=bar"}
+        response = self.fetch("/typecheck?foo=bar", headers=headers)
+        data = json_decode(response.body)
+        self.assertEqual(data, {})
+
+        response = self.fetch("/typecheck", method="POST", body="foo=bar", headers=headers)
+        data = json_decode(response.body)
+        self.assertEqual(data, {})
+
+class XHeaderTest(HandlerBaseTestCase):
+    class Handler(RequestHandler):
+        def get(self):
+            self.write(dict(remote_ip=self.request.remote_ip))
+
+    def get_httpserver_options(self):
+        return dict(xheaders=True)
+
+    def test_ip_headers(self):
+        self.assertEqual(self.fetch_json("/")["remote_ip"],
+                         "127.0.0.1")
+
+        valid_ipv4 = {"X-Real-IP": "4.4.4.4"}
+        self.assertEqual(
+            self.fetch_json("/", headers=valid_ipv4)["remote_ip"],
+            "4.4.4.4")
+
+        valid_ipv6 = {"X-Real-IP": "2620:0:1cfe:face:b00c::3"}
+        self.assertEqual(
+            self.fetch_json("/", headers=valid_ipv6)["remote_ip"],
+            "2620:0:1cfe:face:b00c::3")
+
+        invalid_chars = {"X-Real-IP": "4.4.4.4<script>"}
+        self.assertEqual(
+            self.fetch_json("/", headers=invalid_chars)["remote_ip"],
+            "127.0.0.1")
+
+        invalid_host = {"X-Real-IP": "www.google.com"}
+        self.assertEqual(
+            self.fetch_json("/", headers=invalid_host)["remote_ip"],
+            "127.0.0.1")
+
+
+class UnixSocketTest(AsyncTestCase, LogTrapTestCase):
+    """HTTPServers can listen on Unix sockets too.
+
+    Why would you want to do this?  Nginx can proxy to backends listening
+    on unix sockets, for one thing (and managing a namespace for unix
+    sockets can be easier than managing a bunch of TCP port numbers).
+
+    Unfortunately, there's no way to specify a unix socket in a url for
+    an HTTP client, so we have to test this by hand.
+    """
+    def setUp(self):
+        super(UnixSocketTest, self).setUp()
+        self.tmpdir = tempfile.mkdtemp()
+
+    def tearDown(self):
+        shutil.rmtree(self.tmpdir)
+        super(UnixSocketTest, self).tearDown()
+
+    def test_unix_socket(self):
+        sockfile = os.path.join(self.tmpdir, "test.sock")
+        sock = netutil.bind_unix_socket(sockfile)
+        app = Application([("/hello", HelloWorldRequestHandler)])
+        server = HTTPServer(app, io_loop=self.io_loop)
+        server.add_socket(sock)
+        stream = IOStream(socket.socket(socket.AF_UNIX), io_loop=self.io_loop)
+        stream.connect(sockfile, self.stop)
+        self.wait()
+        stream.write(b("GET /hello HTTP/1.0\r\n\r\n"))
+        stream.read_until(b("\r\n"), self.stop)
+        response = self.wait()
+        self.assertEqual(response, b("HTTP/1.0 200 OK\r\n"))
+        stream.read_until(b("\r\n\r\n"), self.stop)
+        headers = HTTPHeaders.parse(self.wait().decode('latin1'))
+        stream.read_bytes(int(headers["Content-Length"]), self.stop)
+        body = self.wait()
+        self.assertEqual(body, b("Hello world"))
+
+if not hasattr(socket, 'AF_UNIX') or sys.platform == 'cygwin':
+    del UnixSocketTest
diff --git a/tornado/test/httputil_test.py b/tornado/test/httputil_test.py
new file mode 100644
index 0000000..0566b6e
--- /dev/null
+++ b/tornado/test/httputil_test.py
@@ -0,0 +1,140 @@
+#!/usr/bin/env python
+
+from tornado.httputil import url_concat, parse_multipart_form_data, HTTPHeaders
+from tornado.escape import utf8
+from tornado.testing import LogTrapTestCase
+from tornado.util import b
+import logging
+import unittest
+
+
+class TestUrlConcat(unittest.TestCase):
+
+    def test_url_concat_no_query_params(self):
+        url = url_concat(
+                "https://localhost/path",
+                [('y','y'), ('z','z')],
+                )
+        self.assertEqual(url, "https://localhost/path?y=y&z=z")
+
+    def test_url_concat_encode_args(self):
+        url = url_concat(
+                "https://localhost/path",
+                [('y','/y'), ('z','z')],
+                )
+        self.assertEqual(url, "https://localhost/path?y=%2Fy&z=z")
+
+    def test_url_concat_trailing_q(self):
+        url = url_concat(
+                "https://localhost/path?",
+                [('y','y'), ('z','z')],
+                )
+        self.assertEqual(url, "https://localhost/path?y=y&z=z")
+
+    def test_url_concat_q_with_no_trailing_amp(self):
+        url = url_concat(
+                "https://localhost/path?x",
+                [('y','y'), ('z','z')],
+                )
+        self.assertEqual(url, "https://localhost/path?x&y=y&z=z")
+
+    def test_url_concat_trailing_amp(self):
+        url = url_concat(
+                "https://localhost/path?x&",
+                [('y','y'), ('z','z')],
+                )
+        self.assertEqual(url, "https://localhost/path?x&y=y&z=z")
+
+    def test_url_concat_mult_params(self):
+        url = url_concat(
+                "https://localhost/path?a=1&b=2",
+                [('y','y'), ('z','z')],
+                )
+        self.assertEqual(url, "https://localhost/path?a=1&b=2&y=y&z=z")
+
+    def test_url_concat_no_params(self):
+        url = url_concat(
+            "https://localhost/path?r=1&t=2",
+            [],
+            )
+        self.assertEqual(url, "https://localhost/path?r=1&t=2")
+
+class MultipartFormDataTest(LogTrapTestCase):
+    def test_file_upload(self):
+        data = b("""\
+--1234
+Content-Disposition: form-data; name="files"; filename="ab.txt"
+
+Foo
+--1234--""").replace(b("\n"), b("\r\n"))
+        args = {}
+        files = {}
+        parse_multipart_form_data(b("1234"), data, args, files)
+        file = files["files"][0]
+        self.assertEqual(file["filename"], "ab.txt")
+        self.assertEqual(file["body"], b("Foo"))
+        
+    def test_unquoted_names(self):
+        # quotes are optional unless special characters are present
+        data = b("""\
+--1234
+Content-Disposition: form-data; name=files; filename=ab.txt
+
+Foo
+--1234--""").replace(b("\n"), b("\r\n"))
+        args = {}
+        files = {}
+        parse_multipart_form_data(b("1234"), data, args, files)
+        file = files["files"][0]
+        self.assertEqual(file["filename"], "ab.txt")
+        self.assertEqual(file["body"], b("Foo"))
+        
+    def test_special_filenames(self):
+        filenames = ['a;b.txt',
+                     'a"b.txt',
+                     'a";b.txt',
+                     'a;"b.txt',
+                     'a";";.txt',
+                     'a\\"b.txt',
+                     'a\\b.txt',
+                     ]
+        for filename in filenames:
+            logging.info("trying filename %r", filename)
+            data = """\
+--1234
+Content-Disposition: form-data; name="files"; filename="%s"
+
+Foo
+--1234--""" % filename.replace('\\', '\\\\').replace('"', '\\"')
+            data = utf8(data.replace("\n", "\r\n"))
+            args = {}
+            files = {}
+            parse_multipart_form_data(b("1234"), data, args, files)
+            file = files["files"][0]
+            self.assertEqual(file["filename"], filename)
+            self.assertEqual(file["body"], b("Foo"))
+
+class HTTPHeadersTest(unittest.TestCase):
+    def test_multi_line(self):
+        # Lines beginning with whitespace are appended to the previous line
+        # with any leading whitespace replaced by a single space.
+        # Note that while multi-line headers are a part of the HTTP spec,
+        # their use is strongly discouraged.
+        data = """\
+Foo: bar
+ baz
+Asdf: qwer
+\tzxcv
+Foo: even
+     more
+     lines
+""".replace("\n", "\r\n")
+        headers = HTTPHeaders.parse(data)
+        self.assertEqual(headers["asdf"], "qwer zxcv")
+        self.assertEqual(headers.get_list("asdf"), ["qwer zxcv"])
+        self.assertEqual(headers["Foo"], "bar baz,even more lines")
+        self.assertEqual(headers.get_list("foo"), ["bar baz", "even more lines"])
+        self.assertEqual(sorted(list(headers.get_all())),
+                         [("Asdf", "qwer zxcv"),
+                          ("Foo", "bar baz"),
+                          ("Foo", "even more lines")])
diff --git a/tornado/test/import_test.py b/tornado/test/import_test.py
new file mode 100644
index 0000000..7da1a1e
--- /dev/null
+++ b/tornado/test/import_test.py
@@ -0,0 +1,57 @@
+import unittest
+
+class ImportTest(unittest.TestCase):
+    def test_import_everything(self):
+        # Some of our modules are not otherwise tested.  Import them
+        # all (unless they have external dependencies) here to at
+        # least ensure that there are no syntax errors.
+        import tornado.auth
+        import tornado.autoreload
+        # import tornado.curl_httpclient  # depends on pycurl
+        # import tornado.database  # depends on MySQLdb
+        import tornado.escape
+        import tornado.httpclient
+        import tornado.httpserver
+        import tornado.httputil
+        import tornado.ioloop
+        import tornado.iostream
+        import tornado.locale
+        import tornado.options
+        import tornado.netutil
+        # import tornado.platform.twisted # depends on twisted
+        import tornado.process
+        import tornado.simple_httpclient
+        import tornado.stack_context
+        import tornado.template
+        import tornado.testing
+        import tornado.util
+        import tornado.web
+        import tornado.websocket
+        import tornado.wsgi
+
+    # for modules with dependencies, if those dependencies can be loaded,
+    # load them too.
+
+    def test_import_pycurl(self):
+        try:
+            import pycurl
+        except ImportError:
+            pass
+        else:
+            import tornado.curl_httpclient
+
+    def test_import_mysqldb(self):
+        try:
+            import MySQLdb
+        except ImportError:
+            pass
+        else:
+            import tornado.database
+
+    def test_import_twisted(self):
+        try:
+            import twisted
+        except ImportError:
+            pass
+        else:
+            import tornado.platform.twisted
diff --git a/tornado/test/ioloop_test.py b/tornado/test/ioloop_test.py
new file mode 100644
index 0000000..74bb602
--- /dev/null
+++ b/tornado/test/ioloop_test.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python
+
+import datetime
+import unittest
+import time
+
+from tornado.testing import AsyncTestCase, LogTrapTestCase
+
+class TestIOLoop(AsyncTestCase, LogTrapTestCase):
+    def test_add_callback_wakeup(self):
+        # Make sure that add_callback from inside a running IOLoop
+        # wakes up the IOLoop immediately instead of waiting for a timeout.
+        def callback():
+            self.called = True
+            self.stop()
+
+        def schedule_callback():
+            self.called = False
+            self.io_loop.add_callback(callback)
+            # Store away the time so we can check if we woke up immediately
+            self.start_time = time.time()
+        self.io_loop.add_timeout(time.time(), schedule_callback)
+        self.wait()
+        self.assertAlmostEqual(time.time(), self.start_time, places=2)
+        self.assertTrue(self.called)
+
+    def test_add_timeout_timedelta(self):
+        self.io_loop.add_timeout(datetime.timedelta(microseconds=1), self.stop)
+        self.wait()
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tornado/test/iostream_test.py b/tornado/test/iostream_test.py
new file mode 100644
index 0000000..43b2e17
--- /dev/null
+++ b/tornado/test/iostream_test.py
@@ -0,0 +1,203 @@
+from tornado import netutil
+from tornado.ioloop import IOLoop
+from tornado.iostream import IOStream
+from tornado.testing import AsyncHTTPTestCase, LogTrapTestCase, get_unused_port
+from tornado.util import b
+from tornado.web import RequestHandler, Application
+import socket
+import time
+
+class HelloHandler(RequestHandler):
+    def get(self):
+        self.write("Hello")
+
+class TestIOStream(AsyncHTTPTestCase, LogTrapTestCase):
+    def get_app(self):
+        return Application([('/', HelloHandler)])
+
+    def make_iostream_pair(self, **kwargs):
+        port = get_unused_port()
+        [listener] = netutil.bind_sockets(port, '127.0.0.1',
+                                          family=socket.AF_INET)
+        streams = [None, None]
+        def accept_callback(connection, address):
+            streams[0] = IOStream(connection, io_loop=self.io_loop, **kwargs)
+            self.stop()
+        def connect_callback():
+            streams[1] = client_stream
+            self.stop()
+        netutil.add_accept_handler(listener, accept_callback,
+                                   io_loop=self.io_loop)
+        client_stream = IOStream(socket.socket(), io_loop=self.io_loop,
+                                 **kwargs)
+        client_stream.connect(('127.0.0.1', port),
+                              callback=connect_callback)
+        self.wait(condition=lambda: all(streams))
+        self.io_loop.remove_handler(listener.fileno())
+        listener.close()
+        return streams
+
+    def test_read_zero_bytes(self):
+        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
+        s.connect(("localhost", self.get_http_port()))
+        self.stream = IOStream(s, io_loop=self.io_loop)
+        self.stream.write(b("GET / HTTP/1.0\r\n\r\n"))
+
+        # normal read
+        self.stream.read_bytes(9, self.stop)
+        data = self.wait()
+        self.assertEqual(data, b("HTTP/1.0 "))
+
+        # zero bytes
+        self.stream.read_bytes(0, self.stop)
+        data = self.wait()
+        self.assertEqual(data, b(""))
+
+        # another normal read
+        self.stream.read_bytes(3, self.stop)
+        data = self.wait()
+        self.assertEqual(data, b("200"))
+
+    def test_write_zero_bytes(self):
+        # Attempting to write zero bytes should run the callback without
+        # going into an infinite loop.
+        server, client = self.make_iostream_pair()
+        server.write(b(''), callback=self.stop)
+        self.wait()
+        # As a side effect, the stream is now listening for connection
+        # close (if it wasn't already), but is not listening for writes
+        self.assertEqual(server._state, IOLoop.READ|IOLoop.ERROR)
+
+    def test_connection_refused(self):
+        # When a connection is refused, the connect callback should not
+        # be run.  (The kqueue IOLoop used to behave differently from the
+        # epoll IOLoop in this respect)
+        port = get_unused_port()
+        stream = IOStream(socket.socket(), self.io_loop)
+        self.connect_called = False
+        def connect_callback():
+            self.connect_called = True
+        stream.set_close_callback(self.stop)
+        stream.connect(("localhost", port), connect_callback)
+        self.wait()
+        self.assertFalse(self.connect_called)
+
+    def test_connection_closed(self):
+        # When a server sends a response and then closes the connection,
+        # the client must be allowed to read the data before the IOStream
+        # closes itself.  Epoll reports closed connections with a separate
+        # EPOLLRDHUP event delivered at the same time as the read event,
+        # while kqueue reports them as a second read/write event with an EOF
+        # flag.
+        response = self.fetch("/", headers={"Connection": "close"})
+        response.rethrow()
+
+    def test_read_until_close(self):
+        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
+        s.connect(("localhost", self.get_http_port()))
+        stream = IOStream(s, io_loop=self.io_loop)
+        stream.write(b("GET / HTTP/1.0\r\n\r\n"))
+        
+        stream.read_until_close(self.stop)
+        data = self.wait()
+        self.assertTrue(data.startswith(b("HTTP/1.0 200")))
+        self.assertTrue(data.endswith(b("Hello")))
+
+    def test_streaming_callback(self):
+        server, client = self.make_iostream_pair()
+        try:
+            chunks = []
+            final_called = []
+            def streaming_callback(data):
+                chunks.append(data)
+                self.stop()
+            def final_callback(data):
+                assert not data
+                final_called.append(True)
+                self.stop()
+            server.read_bytes(6, callback=final_callback,
+                              streaming_callback=streaming_callback)
+            client.write(b("1234"))
+            self.wait(condition=lambda: chunks)
+            client.write(b("5678"))
+            self.wait(condition=lambda: final_called)
+            self.assertEqual(chunks, [b("1234"), b("56")])
+
+            # the rest of the last chunk is still in the buffer
+            server.read_bytes(2, callback=self.stop)
+            data = self.wait()
+            self.assertEqual(data, b("78"))
+        finally:
+            server.close()
+            client.close()
+
+    def test_streaming_until_close(self):
+        server, client = self.make_iostream_pair()
+        try:
+            chunks = []
+            def callback(data):
+                chunks.append(data)
+                self.stop()
+            client.read_until_close(callback=callback,
+                                    streaming_callback=callback)
+            server.write(b("1234"))
+            self.wait()
+            server.write(b("5678"))
+            self.wait()
+            server.close()
+            self.wait()
+            self.assertEqual(chunks, [b("1234"), b("5678"), b("")])
+        finally:
+            server.close()
+            client.close()
+
+    def test_delayed_close_callback(self):
+        # The scenario:  Server closes the connection while there is a pending
+        # read that can be served out of buffered data.  The client does not
+        # run the close_callback as soon as it detects the close, but rather
+        # defers it until after the buffered read has finished.
+        server, client = self.make_iostream_pair()
+        try:
+            client.set_close_callback(self.stop)
+            server.write(b("12"))
+            chunks = []
+            def callback1(data):
+                chunks.append(data)
+                client.read_bytes(1, callback2)
+                server.close()
+            def callback2(data):
+                chunks.append(data)
+            client.read_bytes(1, callback1)
+            self.wait()  # stopped by close_callback
+            self.assertEqual(chunks, [b("1"), b("2")])
+        finally:
+            server.close()
+            client.close()
+
+    def test_close_buffered_data(self):
+        # Similar to the previous test, but with data stored in the OS's
+        # socket buffers instead of the IOStream's read buffer.  Out-of-band
+        # close notifications must be delayed until all data has been
+        # drained into the IOStream buffer. (epoll used to use out-of-band
+        # close events with EPOLLRDHUP, but no longer)
+        #
+        # This depends on the read_chunk_size being smaller than the
+        # OS socket buffer, so make it small.
+        server, client = self.make_iostream_pair(read_chunk_size=256)
+        try:
+            server.write(b("A") * 512)
+            client.read_bytes(256, self.stop)
+            data = self.wait()
+            self.assertEqual(b("A") * 256, data)
+            server.close()
+            # Allow the close to propagate to the client side of the
+            # connection.  Using add_callback instead of add_timeout
+            # doesn't seem to work, even with multiple iterations
+            self.io_loop.add_timeout(time.time() + 0.01, self.stop)
+            self.wait()
+            client.read_bytes(256, self.stop)
+            data = self.wait()
+            self.assertEqual(b("A") * 256, data)
+        finally:
+            server.close()
+            client.close()
diff --git a/tornado/test/process_test.py b/tornado/test/process_test.py
new file mode 100644
index 0000000..de9ae52
--- /dev/null
+++ b/tornado/test/process_test.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python
+
+import logging
+import os
+import signal
+import sys
+from tornado.httpclient import HTTPClient, HTTPError
+from tornado.httpserver import HTTPServer
+from tornado.ioloop import IOLoop
+from tornado.netutil import bind_sockets
+from tornado.process import fork_processes, task_id
+from tornado.simple_httpclient import SimpleAsyncHTTPClient
+from tornado.testing import LogTrapTestCase, get_unused_port
+from tornado.web import RequestHandler, Application
+
+# Not using AsyncHTTPTestCase because we need control over the IOLoop.
+# Logging is tricky here so you may want to replace LogTrapTestCase
+# with unittest.TestCase when debugging.
+class ProcessTest(LogTrapTestCase):
+    def get_app(self):
+        class ProcessHandler(RequestHandler):
+            def get(self):
+                if self.get_argument("exit", None):
+                    # must use os._exit instead of sys.exit so unittest's
+                    # exception handler doesn't catch it
+                    os._exit(int(self.get_argument("exit")))
+                if self.get_argument("signal", None):
+                    os.kill(os.getpid(),
+                            int(self.get_argument("signal")))
+                self.write(str(os.getpid()))
+        return Application([("/", ProcessHandler)])
+
+    def tearDown(self):
+        if task_id() is not None:
+            # We're in a child process, and probably got to this point
+            # via an uncaught exception.  If we return now, both
+            # processes will continue with the rest of the test suite.
+            # Exit now so the parent process will restart the child
+            # (since we don't have a clean way to signal failure to
+            # the parent that won't restart)
+            logging.error("aborting child process from tearDown")
+            logging.shutdown()
+            os._exit(1)
+        super(ProcessTest, self).tearDown()
+
+    def test_multi_process(self):
+        self.assertFalse(IOLoop.initialized())
+        port = get_unused_port()
+        def get_url(path):
+            return "http://127.0.0.1:%d%s" % (port, path)
+        sockets = bind_sockets(port, "127.0.0.1")
+        # ensure that none of these processes live too long
+        signal.alarm(5)  # master process
+        try:
+            id = fork_processes(3, max_restarts=3)
+        except SystemExit, e:
+            # if we exit cleanly from fork_processes, all the child processes
+            # finished with status 0
+            self.assertEqual(e.code, 0)
+            self.assertTrue(task_id() is None)
+            for sock in sockets: sock.close()
+            signal.alarm(0)
+            return
+        signal.alarm(5)  # child process
+        try:
+            if id in (0, 1):
+                signal.alarm(5)
+                self.assertEqual(id, task_id())
+                server = HTTPServer(self.get_app())
+                server.add_sockets(sockets)
+                IOLoop.instance().start()
+            elif id == 2:
+                signal.alarm(5)
+                self.assertEqual(id, task_id())
+                for sock in sockets: sock.close()
+                # Always use SimpleAsyncHTTPClient here; the curl
+                # version appears to get confused sometimes if the
+                # connection gets closed before it's had a chance to
+                # switch from writing mode to reading mode.
+                client = HTTPClient(SimpleAsyncHTTPClient)
+
+                def fetch(url, fail_ok=False):
+                    try:
+                        return client.fetch(get_url(url))
+                    except HTTPError, e:
+                        if not (fail_ok and e.code == 599):
+                            raise
+
+                # Make two processes exit abnormally
+                fetch("/?exit=2", fail_ok=True)
+                fetch("/?exit=3", fail_ok=True)
+
+                # They've been restarted, so a new fetch will work
+                int(fetch("/").body)
+
+                # Now the same with signals
+                # Disabled because on the mac a process dying with a signal
+                # can trigger an "Application exited abnormally; send error
+                # report to Apple?" prompt.
+                #fetch("/?signal=%d" % signal.SIGTERM, fail_ok=True)
+                #fetch("/?signal=%d" % signal.SIGABRT, fail_ok=True)
+                #int(fetch("/").body)
+
+                # Now kill them normally so they won't be restarted
+                fetch("/?exit=0", fail_ok=True)
+                # One process left; watch it's pid change
+                pid = int(fetch("/").body)
+                fetch("/?exit=4", fail_ok=True)
+                pid2 = int(fetch("/").body)
+                self.assertNotEqual(pid, pid2)
+
+                # Kill the last one so we shut down cleanly
+                fetch("/?exit=0", fail_ok=True)
+
+                os._exit(0)
+        except Exception:
+            logging.error("exception in child process %d", id, exc_info=True)
+            raise
+            
+
+if os.name != 'posix' or sys.platform == 'cygwin':
+    # All sorts of unixisms here
+    del ProcessTest
diff --git a/tornado/test/run_pyversion_tests.py b/tornado/test/run_pyversion_tests.py
new file mode 100755
index 0000000..8c4e967
--- /dev/null
+++ b/tornado/test/run_pyversion_tests.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+"""Runs the tornado test suite with all supported python interpreters."""
+
+import os
+import subprocess
+import sys
+
+INTERPRETERS = [
+    "python2.5",
+    "python2.6",
+    "python2.7",
+    "auto2to3",
+    "pypy",
+    ]
+
+def exists_on_path(filename):
+    for dir in os.environ["PATH"].split(":"):
+        if os.path.exists(os.path.join(dir, filename)):
+            return True
+    return False
+
+def main():
+    for interpreter in INTERPRETERS:
+        print "=================== %s =======================" % interpreter
+        if not exists_on_path(interpreter):
+            print "Interpreter not found, skipping..."
+            continue
+        args = [interpreter, "-m", "tornado.test.runtests"] + sys.argv[1:]
+        ret = subprocess.call(args)
+        if ret != 0:
+            print "Tests on %s failed with exit code %d" % (interpreter, ret)
+            sys.exit(ret)
+    print "All tests passed"
+
+if __name__ == "__main__":
+    main()
diff --git a/tornado/test/runtests.py b/tornado/test/runtests.py
new file mode 100755
index 0000000..188aee8
--- /dev/null
+++ b/tornado/test/runtests.py
@@ -0,0 +1,33 @@
+#!/usr/bin/env python
+import unittest
+
+TEST_MODULES = [
+    'tornado.httputil.doctests',
+    'tornado.iostream.doctests',
+    'tornado.util.doctests',
+    'tornado.test.auth_test',
+    'tornado.test.curl_httpclient_test',
+    'tornado.test.escape_test',
+    'tornado.test.gen_test',
+    'tornado.test.httpclient_test',
+    'tornado.test.httpserver_test',
+    'tornado.test.httputil_test',
+    'tornado.test.import_test',
+    'tornado.test.ioloop_test',
+    'tornado.test.iostream_test',
+    'tornado.test.process_test',
+    'tornado.test.simple_httpclient_test',
+    'tornado.test.stack_context_test',
+    'tornado.test.template_test',
+    'tornado.test.testing_test',
+    'tornado.test.twisted_test',
+    'tornado.test.web_test',
+    'tornado.test.wsgi_test',
+]
+
+def all():
+    return unittest.defaultTestLoader.loadTestsFromNames(TEST_MODULES)
+
+if __name__ == '__main__':
+    import tornado.testing
+    tornado.testing.main()
diff --git a/tornado/test/simple_httpclient_test.py b/tornado/test/simple_httpclient_test.py
new file mode 100644
index 0000000..ebb265b
--- /dev/null
+++ b/tornado/test/simple_httpclient_test.py
@@ -0,0 +1,230 @@
+from __future__ import with_statement
+
+import collections
+import gzip
+import logging
+import socket
+
+from tornado.ioloop import IOLoop
+from tornado.simple_httpclient import SimpleAsyncHTTPClient, _DEFAULT_CA_CERTS
+from tornado.test.httpclient_test import HTTPClientCommonTestCase, ChunkHandler, CountdownHandler, HelloWorldHandler
+from tornado.testing import AsyncHTTPTestCase, LogTrapTestCase
+from tornado.util import b
+from tornado.web import RequestHandler, Application, asynchronous, url
+
+class SimpleHTTPClientCommonTestCase(HTTPClientCommonTestCase):
+    def get_http_client(self):
+        client = SimpleAsyncHTTPClient(io_loop=self.io_loop,
+                                       force_instance=True)
+        self.assertTrue(isinstance(client, SimpleAsyncHTTPClient))
+        return client
+
+# Remove the base class from our namespace so the unittest module doesn't
+# try to run it again.
+del HTTPClientCommonTestCase
+
+class TriggerHandler(RequestHandler):
+    def initialize(self, queue, wake_callback):
+        self.queue = queue
+        self.wake_callback = wake_callback
+
+    @asynchronous
+    def get(self):
+        logging.info("queuing trigger")
+        self.queue.append(self.finish)
+        self.wake_callback()
+
+class HangHandler(RequestHandler):
+    @asynchronous
+    def get(self):
+        pass
+
+class ContentLengthHandler(RequestHandler):
+    def get(self):
+        self.set_header("Content-Length", self.get_argument("value"))
+        self.write("ok")
+
+class HeadHandler(RequestHandler):
+    def head(self):
+        self.set_header("Content-Length", "7")
+
+class NoContentHandler(RequestHandler):
+    def get(self):
+        if self.get_argument("error", None):
+            self.set_header("Content-Length", "7")
+        self.set_status(204)
+
+class SeeOther303PostHandler(RequestHandler):
+    def post(self):
+        assert self.request.body == b("blah")
+        self.set_header("Location", "/303_get")
+        self.set_status(303)
+
+class SeeOther303GetHandler(RequestHandler):
+    def get(self):
+        assert not self.request.body
+        self.write("ok")
+
+
+class SimpleHTTPClientTestCase(AsyncHTTPTestCase, LogTrapTestCase):
+    def setUp(self):
+        super(SimpleHTTPClientTestCase, self).setUp()
+        self.http_client = SimpleAsyncHTTPClient(self.io_loop)
+
+    def get_app(self):
+        # callable objects to finish pending /trigger requests
+        self.triggers = collections.deque()
+        return Application([
+            url("/trigger", TriggerHandler, dict(queue=self.triggers,
+                                                 wake_callback=self.stop)),
+            url("/chunk", ChunkHandler),
+            url("/countdown/([0-9]+)", CountdownHandler, name="countdown"),
+            url("/hang", HangHandler),
+            url("/hello", HelloWorldHandler),
+            url("/content_length", ContentLengthHandler),
+            url("/head", HeadHandler),
+            url("/no_content", NoContentHandler),
+            url("/303_post", SeeOther303PostHandler),
+            url("/303_get", SeeOther303GetHandler),
+            ], gzip=True)
+
+    def test_singleton(self):
+        # Class "constructor" reuses objects on the same IOLoop
+        self.assertTrue(SimpleAsyncHTTPClient(self.io_loop) is
+                        SimpleAsyncHTTPClient(self.io_loop))
+        # unless force_instance is used
+        self.assertTrue(SimpleAsyncHTTPClient(self.io_loop) is not
+                        SimpleAsyncHTTPClient(self.io_loop,
+                                              force_instance=True))
+        # different IOLoops use different objects
+        io_loop2 = IOLoop()
+        self.assertTrue(SimpleAsyncHTTPClient(self.io_loop) is not
+                        SimpleAsyncHTTPClient(io_loop2))
+
+    def test_connection_limit(self):
+        client = SimpleAsyncHTTPClient(self.io_loop, max_clients=2,
+                                       force_instance=True)
+        self.assertEqual(client.max_clients, 2)
+        seen = []
+        # Send 4 requests.  Two can be sent immediately, while the others
+        # will be queued
+        for i in range(4):
+            client.fetch(self.get_url("/trigger"),
+                         lambda response, i=i: (seen.append(i), self.stop()))
+        self.wait(condition=lambda: len(self.triggers) == 2)
+        self.assertEqual(len(client.queue), 2)
+
+        # Finish the first two requests and let the next two through
+        self.triggers.popleft()()
+        self.triggers.popleft()()
+        self.wait(condition=lambda: (len(self.triggers) == 2 and
+                                     len(seen) == 2))
+        self.assertEqual(set(seen), set([0, 1]))
+        self.assertEqual(len(client.queue), 0)
+
+        # Finish all the pending requests
+        self.triggers.popleft()()
+        self.triggers.popleft()()
+        self.wait(condition=lambda: len(seen) == 4)
+        self.assertEqual(set(seen), set([0, 1, 2, 3]))
+        self.assertEqual(len(self.triggers), 0)
+
+    def test_redirect_connection_limit(self):
+        # following redirects should not consume additional connections
+        client = SimpleAsyncHTTPClient(self.io_loop, max_clients=1,
+                                       force_instance=True)
+        client.fetch(self.get_url('/countdown/3'), self.stop,
+                     max_redirects=3)
+        response = self.wait()
+        response.rethrow()
+
+    def test_default_certificates_exist(self):
+        open(_DEFAULT_CA_CERTS).close()
+
+    def test_gzip(self):
+        # All the tests in this file should be using gzip, but this test
+        # ensures that it is in fact getting compressed.
+        # Setting Accept-Encoding manually bypasses the client's
+        # decompression so we can see the raw data.
+        response = self.fetch("/chunk", use_gzip=False,
+                              headers={"Accept-Encoding": "gzip"})
+        self.assertEqual(response.headers["Content-Encoding"], "gzip")
+        self.assertNotEqual(response.body, b("asdfqwer"))
+        # Our test data gets bigger when gzipped.  Oops.  :)
+        self.assertEqual(len(response.body), 34)
+        f = gzip.GzipFile(mode="r", fileobj=response.buffer)
+        self.assertEqual(f.read(), b("asdfqwer"))
+
+    def test_max_redirects(self):
+        response = self.fetch("/countdown/5", max_redirects=3)
+        self.assertEqual(302, response.code)
+        # We requested 5, followed three redirects for 4, 3, 2, then the last
+        # unfollowed redirect is to 1.
+        self.assertTrue(response.request.url.endswith("/countdown/5"))
+        self.assertTrue(response.effective_url.endswith("/countdown/2"))
+        self.assertTrue(response.headers["Location"].endswith("/countdown/1"))
+
+    def test_303_redirect(self):
+       response = self.fetch("/303_post", method="POST", body="blah")
+       self.assertEqual(200, response.code)
+       self.assertTrue(response.request.url.endswith("/303_post"))
+       self.assertTrue(response.effective_url.endswith("/303_get"))
+       #request is the original request, is a POST still
+       self.assertEqual("POST", response.request.method)
+
+    def test_request_timeout(self):
+        response = self.fetch('/hang', request_timeout=0.1)
+        self.assertEqual(response.code, 599)
+        self.assertTrue(0.099 < response.request_time < 0.11, response.request_time)
+        self.assertEqual(str(response.error), "HTTP 599: Timeout")
+
+    def test_ipv6(self):
+        if not socket.has_ipv6:
+            # python compiled without ipv6 support, so skip this test
+            return
+        try:
+            self.http_server.listen(self.get_http_port(), address='::1')
+        except socket.gaierror, e:
+            if e.args[0] == socket.EAI_ADDRFAMILY:
+                # python supports ipv6, but it's not configured on the network
+                # interface, so skip this test.
+                return
+            raise
+        url = self.get_url("/hello").replace("localhost", "[::1]")
+
+        # ipv6 is currently disabled by default and must be explicitly requested
+        self.http_client.fetch(url, self.stop)
+        response = self.wait()
+        self.assertEqual(response.code, 599)
+
+        self.http_client.fetch(url, self.stop, allow_ipv6=True)
+        response = self.wait()
+        self.assertEqual(response.body, b("Hello world!"))
+
+    def test_multiple_content_length_accepted(self):
+        response = self.fetch("/content_length?value=2,2")
+        self.assertEqual(response.body, b("ok"))
+        response = self.fetch("/content_length?value=2,%202,2")
+        self.assertEqual(response.body, b("ok"))
+
+        response = self.fetch("/content_length?value=2,4")
+        self.assertEqual(response.code, 599)
+        response = self.fetch("/content_length?value=2,%202,3")
+        self.assertEqual(response.code, 599)
+
+    def test_head_request(self):
+        response = self.fetch("/head", method="HEAD")
+        self.assertEqual(response.code, 200)
+        self.assertEqual(response.headers["content-length"], "7")
+        self.assertFalse(response.body)
+
+    def test_no_content(self):
+        response = self.fetch("/no_content")
+        self.assertEqual(response.code, 204)
+        # 204 status doesn't need a content-length, but tornado will
+        # add a zero content-length anyway.
+        self.assertEqual(response.headers["Content-length"], "0")
+
+        # 204 status with non-zero content length is malformed
+        response = self.fetch("/no_content?error=1")
+        self.assertEqual(response.code, 599)
diff --git a/tornado/test/stack_context_test.py b/tornado/test/stack_context_test.py
new file mode 100644
index 0000000..b79f1e3
--- /dev/null
+++ b/tornado/test/stack_context_test.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python
+from __future__ import with_statement
+
+from tornado.stack_context import StackContext, wrap
+from tornado.testing import AsyncHTTPTestCase, AsyncTestCase, LogTrapTestCase
+from tornado.util import b
+from tornado.web import asynchronous, Application, RequestHandler
+import contextlib
+import functools
+import logging
+import unittest
+
+class TestRequestHandler(RequestHandler):
+    def __init__(self, app, request, io_loop):
+        super(TestRequestHandler, self).__init__(app, request)
+        self.io_loop = io_loop
+
+    @asynchronous
+    def get(self):
+        logging.info('in get()')
+        # call self.part2 without a self.async_callback wrapper.  Its
+        # exception should still get thrown
+        self.io_loop.add_callback(self.part2)
+
+    def part2(self):
+        logging.info('in part2()')
+        # Go through a third layer to make sure that contexts once restored
+        # are again passed on to future callbacks
+        self.io_loop.add_callback(self.part3)
+
+    def part3(self):
+        logging.info('in part3()')
+        raise Exception('test exception')
+
+    def get_error_html(self, status_code, **kwargs):
+        if 'exception' in kwargs and str(kwargs['exception']) == 'test exception':
+            return 'got expected exception'
+        else:
+            return 'unexpected failure'
+
+class HTTPStackContextTest(AsyncHTTPTestCase, LogTrapTestCase):
+    def get_app(self):
+        return Application([('/', TestRequestHandler,
+                             dict(io_loop=self.io_loop))])
+
+    def test_stack_context(self):
+        self.http_client.fetch(self.get_url('/'), self.handle_response)
+        self.wait()
+        self.assertEqual(self.response.code, 500)
+        self.assertTrue(b('got expected exception') in self.response.body)
+
+    def handle_response(self, response):
+        self.response = response
+        self.stop()
+
+class StackContextTest(AsyncTestCase, LogTrapTestCase):
+    def setUp(self):
+        super(StackContextTest, self).setUp()
+        self.active_contexts = []
+
+    @contextlib.contextmanager
+    def context(self, name):
+        self.active_contexts.append(name)
+        yield
+        self.assertEqual(self.active_contexts.pop(), name)
+
+    # Simulates the effect of an asynchronous library that uses its own
+    # StackContext internally and then returns control to the application.
+    def test_exit_library_context(self):
+        def library_function(callback):
+            # capture the caller's context before introducing our own
+            callback = wrap(callback)
+            with StackContext(functools.partial(self.context, 'library')):
+                self.io_loop.add_callback(
+                  functools.partial(library_inner_callback, callback))
+        def library_inner_callback(callback):
+            self.assertEqual(self.active_contexts[-2:],
+                             ['application', 'library'])
+            callback()
+        def final_callback():
+            # implementation detail:  the full context stack at this point
+            # is ['application', 'library', 'application'].  The 'library'
+            # context was not removed, but is no longer innermost so
+            # the application context takes precedence.
+            self.assertEqual(self.active_contexts[-1], 'application')
+            self.stop()
+        with StackContext(functools.partial(self.context, 'application')):
+            library_function(final_callback)
+        self.wait()
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tornado/test/static/robots.txt b/tornado/test/static/robots.txt
new file mode 100644
index 0000000..1f53798
--- /dev/null
+++ b/tornado/test/static/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow: /
diff --git a/tornado/test/template_test.py b/tornado/test/template_test.py
new file mode 100644
index 0000000..c2a0533
--- /dev/null
+++ b/tornado/test/template_test.py
@@ -0,0 +1,304 @@
+from __future__ import with_statement
+
+import traceback
+
+from tornado.escape import utf8, native_str
+from tornado.template import Template, DictLoader, ParseError
+from tornado.testing import LogTrapTestCase
+from tornado.util import b, bytes_type, ObjectDict
+
+class TemplateTest(LogTrapTestCase):
+    def test_simple(self):
+        template = Template("Hello {{ name }}!")
+        self.assertEqual(template.generate(name="Ben"),
+                         b("Hello Ben!"))
+
+    def test_bytes(self):
+        template = Template("Hello {{ name }}!")
+        self.assertEqual(template.generate(name=utf8("Ben")),
+                         b("Hello Ben!"))
+
+    def test_expressions(self):
+        template = Template("2 + 2 = {{ 2 + 2 }}")
+        self.assertEqual(template.generate(), b("2 + 2 = 4"))
+
+    def test_comment(self):
+        template = Template("Hello{# TODO i18n #} {{ name }}!")
+        self.assertEqual(template.generate(name=utf8("Ben")),
+                         b("Hello Ben!"))
+
+    def test_include(self):
+        loader = DictLoader({
+                "index.html": '{% include "header.html" %}\nbody text',
+                "header.html": "header text",
+                })
+        self.assertEqual(loader.load("index.html").generate(),
+                         b("header text\nbody text"))
+
+    def test_extends(self):
+        loader = DictLoader({
+                "base.html": """\
+<title>{% block title %}default title{% end %}</title>
+<body>{% block body %}default body{% end %}</body>
+""",
+                "page.html": """\
+{% extends "base.html" %}
+{% block title %}page title{% end %}
+{% block body %}page body{% end %}
+""",
+                })
+        self.assertEqual(loader.load("page.html").generate(),
+                         b("<title>page title</title>\n<body>page body</body>\n"))
+
+    def test_relative_load(self):
+        loader = DictLoader({
+                "a/1.html": "{% include '2.html' %}",
+                "a/2.html": "{% include '../b/3.html' %}",
+                "b/3.html": "ok",
+                })
+        self.assertEqual(loader.load("a/1.html").generate(),
+                         b("ok"))
+
+    def test_escaping(self):
+        self.assertRaises(ParseError, lambda: Template("{{"))
+        self.assertRaises(ParseError, lambda: Template("{%"))
+        self.assertEqual(Template("{{!").generate(), b("{{"))
+        self.assertEqual(Template("{%!").generate(), b("{%"))
+        self.assertEqual(Template("{{ 'expr' }} {{!jquery expr}}").generate(),
+                         b("expr {{jquery expr}}"))
+
+    def test_unicode_template(self):
+        template = Template(utf8(u"\u00e9"))
+        self.assertEqual(template.generate(), utf8(u"\u00e9"))
+
+    def test_unicode_literal_expression(self):
+        # Unicode literals should be usable in templates.  Note that this
+        # test simulates unicode characters appearing directly in the
+        # template file (with utf8 encoding), i.e. \u escapes would not
+        # be used in the template file itself.
+        if str is unicode:
+            # python 3 needs a different version of this test since
+            # 2to3 doesn't run on template internals
+            template = Template(utf8(u'{{ "\u00e9" }}'))
+        else:
+            template = Template(utf8(u'{{ u"\u00e9" }}'))
+        self.assertEqual(template.generate(), utf8(u"\u00e9"))
+
+    def test_custom_namespace(self):
+        loader = DictLoader({"test.html": "{{ inc(5) }}"}, namespace={"inc": lambda x: x+1})
+        self.assertEqual(loader.load("test.html").generate(), b("6"))
+
+    def test_apply(self):
+        def upper(s): return s.upper()
+        template = Template(utf8("{% apply upper %}foo{% end %}"))
+        self.assertEqual(template.generate(upper=upper), b("FOO"))
+
+    def test_if(self):
+        template = Template(utf8("{% if x > 4 %}yes{% else %}no{% end %}"))
+        self.assertEqual(template.generate(x=5), b("yes"))
+        self.assertEqual(template.generate(x=3), b("no"))
+
+    def test_comment_directive(self):
+        template = Template(utf8("{% comment blah blah %}foo"))
+        self.assertEqual(template.generate(), b("foo"))
+
+class StackTraceTest(LogTrapTestCase):
+    def test_error_line_number_expression(self):
+        loader = DictLoader({"test.html": """one
+two{{1/0}}
+three
+        """})
+        try:
+            loader.load("test.html").generate()
+        except ZeroDivisionError:
+            self.assertTrue("# test.html:2" in traceback.format_exc())
+
+    def test_error_line_number_directive(self):
+        loader = DictLoader({"test.html": """one
+two{%if 1/0%}
+three{%end%}
+        """})
+        try:
+            loader.load("test.html").generate()
+        except ZeroDivisionError:
+            self.assertTrue("# test.html:2" in traceback.format_exc())
+
+    def test_error_line_number_module(self):
+        loader = DictLoader({
+            "base.html": "{% module Template('sub.html') %}",
+            "sub.html": "{{1/0}}",
+        }, namespace={"_modules": ObjectDict({"Template": lambda path, **kwargs: loader.load(path).generate(**kwargs)})})
+        try:
+            loader.load("base.html").generate()
+        except ZeroDivisionError:
+            exc_stack = traceback.format_exc()
+            self.assertTrue('# base.html:1' in exc_stack)
+            self.assertTrue('# sub.html:1' in exc_stack)
+
+    def test_error_line_number_include(self):
+        loader = DictLoader({
+            "base.html": "{% include 'sub.html' %}",
+            "sub.html": "{{1/0}}",
+        })
+        try:
+            loader.load("base.html").generate()
+        except ZeroDivisionError:
+            self.assertTrue("# sub.html:1 (via base.html:1)" in
+                            traceback.format_exc())
+
+    def test_error_line_number_extends_base_error(self):
+        loader = DictLoader({
+            "base.html": "{{1/0}}",
+            "sub.html": "{% extends 'base.html' %}",
+        })
+        try:
+            loader.load("sub.html").generate()
+        except ZeroDivisionError:
+            exc_stack = traceback.format_exc()
+        self.assertTrue("# base.html:1" in exc_stack)
+
+
+    def test_error_line_number_extends_sub_error(self):
+        loader = DictLoader({
+            "base.html": "{% block 'block' %}{% end %}",
+            "sub.html": """
+{% extends 'base.html' %}
+{% block 'block' %}
+{{1/0}}
+{% end %}
+            """})
+        try:
+            loader.load("sub.html").generate()
+        except ZeroDivisionError:
+            self.assertTrue("# sub.html:4 (via base.html:1)" in
+                            traceback.format_exc())
+
+    def test_multi_includes(self):
+        loader = DictLoader({
+                "a.html": "{% include 'b.html' %}",
+                "b.html": "{% include 'c.html' %}",
+                "c.html": "{{1/0}}",
+                })
+        try:
+            loader.load("a.html").generate()
+        except ZeroDivisionError:
+            self.assertTrue("# c.html:1 (via b.html:1, a.html:1)" in
+                            traceback.format_exc())
+
+
+class AutoEscapeTest(LogTrapTestCase):
+    def setUp(self):
+        self.templates = {
+            "escaped.html": "{% autoescape xhtml_escape %}{{ name }}",
+            "unescaped.html": "{% autoescape None %}{{ name }}",
+            "default.html": "{{ name }}",
+
+            "include.html": """\
+escaped: {% include 'escaped.html' %}
+unescaped: {% include 'unescaped.html' %}
+default: {% include 'default.html' %}
+""",
+
+            "escaped_block.html": """\
+{% autoescape xhtml_escape %}\
+{% block name %}base: {{ name }}{% end %}""",
+            "unescaped_block.html": """\
+{% autoescape None %}\
+{% block name %}base: {{ name }}{% end %}""",
+
+            # Extend a base template with different autoescape policy,
+            # with and without overriding the base's blocks
+            "escaped_extends_unescaped.html": """\
+{% autoescape xhtml_escape %}\
+{% extends "unescaped_block.html" %}""",
+            "escaped_overrides_unescaped.html": """\
+{% autoescape xhtml_escape %}\
+{% extends "unescaped_block.html" %}\
+{% block name %}extended: {{ name }}{% end %}""",
+            "unescaped_extends_escaped.html": """\
+{% autoescape None %}\
+{% extends "escaped_block.html" %}""",
+            "unescaped_overrides_escaped.html": """\
+{% autoescape None %}\
+{% extends "escaped_block.html" %}\
+{% block name %}extended: {{ name }}{% end %}""",
+
+            "raw_expression.html": """\
+{% autoescape xhtml_escape %}\
+expr: {{ name }}
+raw: {% raw name %}""",
+            }
+    
+    def test_default_off(self):
+        loader = DictLoader(self.templates, autoescape=None)
+        name = "Bobby <table>s"
+        self.assertEqual(loader.load("escaped.html").generate(name=name),
+                         b("Bobby &lt;table&gt;s"))
+        self.assertEqual(loader.load("unescaped.html").generate(name=name),
+                         b("Bobby <table>s"))
+        self.assertEqual(loader.load("default.html").generate(name=name),
+                         b("Bobby <table>s"))
+
+        self.assertEqual(loader.load("include.html").generate(name=name),
+                         b("escaped: Bobby &lt;table&gt;s\n"
+                           "unescaped: Bobby <table>s\n"
+                           "default: Bobby <table>s\n"))
+        
+    def test_default_on(self):
+        loader = DictLoader(self.templates, autoescape="xhtml_escape")
+        name = "Bobby <table>s"
+        self.assertEqual(loader.load("escaped.html").generate(name=name),
+                         b("Bobby &lt;table&gt;s"))
+        self.assertEqual(loader.load("unescaped.html").generate(name=name),
+                         b("Bobby <table>s"))
+        self.assertEqual(loader.load("default.html").generate(name=name),
+                         b("Bobby &lt;table&gt;s"))
+        
+        self.assertEqual(loader.load("include.html").generate(name=name),
+                         b("escaped: Bobby &lt;table&gt;s\n"
+                           "unescaped: Bobby <table>s\n"
+                           "default: Bobby &lt;table&gt;s\n"))
+
+    def test_unextended_block(self):
+        loader = DictLoader(self.templates)
+        name = "<script>"
+        self.assertEqual(loader.load("escaped_block.html").generate(name=name),
+                         b("base: &lt;script&gt;"))
+        self.assertEqual(loader.load("unescaped_block.html").generate(name=name),
+                         b("base: <script>"))
+
+    def test_extended_block(self):
+        loader = DictLoader(self.templates)
+        def render(name): return loader.load(name).generate(name="<script>")
+        self.assertEqual(render("escaped_extends_unescaped.html"),
+                         b("base: <script>"))
+        self.assertEqual(render("escaped_overrides_unescaped.html"),
+                         b("extended: &lt;script&gt;"))
+
+        self.assertEqual(render("unescaped_extends_escaped.html"),
+                         b("base: &lt;script&gt;"))
+        self.assertEqual(render("unescaped_overrides_escaped.html"),
+                         b("extended: <script>"))
+
+    def test_raw_expression(self):
+        loader = DictLoader(self.templates)
+        def render(name): return loader.load(name).generate(name='<>&"')
+        self.assertEqual(render("raw_expression.html"),
+                         b("expr: &lt;&gt;&amp;&quot;\n"
+                           "raw: <>&\""))
+
+    def test_custom_escape(self):
+        loader = DictLoader({"foo.py": 
+                             "{% autoescape py_escape %}s = {{ name }}\n"})
+        def py_escape(s):
+            self.assertEqual(type(s), bytes_type)
+            return repr(native_str(s))
+        def render(template, name):
+            return loader.load(template).generate(py_escape=py_escape, 
+                                                  name=name)
+        self.assertEqual(render("foo.py", "<html>"),
+                         b("s = '<html>'\n"))
+        self.assertEqual(render("foo.py", "';sys.exit()"),
+                         b("""s = "';sys.exit()"\n"""))
+        self.assertEqual(render("foo.py", ["not a string"]),
+                         b("""s = "['not a string']"\n"""))
diff --git a/tornado/test/test.crt b/tornado/test/test.crt
new file mode 100644
index 0000000..25538c8
--- /dev/null
+++ b/tornado/test/test.crt
@@ -0,0 +1,15 @@
+-----BEGIN CERTIFICATE-----
+MIICSDCCAbGgAwIBAgIJAN1oTowzMbkzMA0GCSqGSIb3DQEBBQUAMD0xCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRkwFwYDVQQKDBBUb3JuYWRvIFdl
+YiBUZXN0MB4XDTEwMDgyNTE4MjQ0NFoXDTIwMDgyMjE4MjQ0NFowPTELMAkGA1UE
+BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExGTAXBgNVBAoMEFRvcm5hZG8gV2Vi
+IFRlc3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALirW3mX4jbdFse2aZwW
+zszCJ1IsRDrzALpbvMYLLbIZqo+Z8v5aERKTRQpXFqGaZyY+tdwYy7X7YXcLtKqv
+jnw/MSeIaqkw5pROKz5aR0nkPLvcTmhJVLVPCLc8dFnIlu8aC9TrDhr90P+PzU39
+UG7zLweA9zXKBuW3Tjo5dMP3AgMBAAGjUDBOMB0GA1UdDgQWBBRhJjMBYrzddCFr
+/0vvPyHMeqgo0TAfBgNVHSMEGDAWgBRhJjMBYrzddCFr/0vvPyHMeqgo0TAMBgNV
+HRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4GBAGP6GaxSfb21bikcqaK3ZKCC1sRJ
+tiCuvJZbBUFUCAzl05dYUfJZim/oWK+GqyUkUB8ciYivUNnn9OtS7DnlTgT2ws2e
+lNgn5cuFXoAGcHXzVlHG3yoywYBf3y0Dn20uzrlLXUWJAzoSLOt2LTaXvwlgm7hF
+W1q8SQ6UBshRw2X0
+-----END CERTIFICATE-----
diff --git a/tornado/test/test.key b/tornado/test/test.key
new file mode 100644
index 0000000..577d518
--- /dev/null
+++ b/tornado/test/test.key
@@ -0,0 +1,16 @@
+-----BEGIN PRIVATE KEY-----
+MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALirW3mX4jbdFse2
+aZwWzszCJ1IsRDrzALpbvMYLLbIZqo+Z8v5aERKTRQpXFqGaZyY+tdwYy7X7YXcL
+tKqvjnw/MSeIaqkw5pROKz5aR0nkPLvcTmhJVLVPCLc8dFnIlu8aC9TrDhr90P+P
+zU39UG7zLweA9zXKBuW3Tjo5dMP3AgMBAAECgYEAiygNaWYrf95AcUQi9w00zpUr
+nj9fNvCwxr2kVbRMvd2balS/CC4EmXPCXdVcZ3B7dBVjYzSIJV0Fh/iZLtnVysD9
+fcNMZ+Cz71b/T0ItsNYOsJk0qUVyP52uqsqkNppIPJsD19C+ZeMLZj6iEiylZyl8
+2U16c/kVIjER63mUEGkCQQDayQOTGPJrKHqPAkUqzeJkfvHH2yCf+cySU+w6ezyr
+j9yxcq8aZoLusCebDVT+kz7RqnD5JePFvB38cMuepYBLAkEA2BTFdZx30f4moPNv
+JlXlPNJMUTUzsXG7n4vNc+18O5ous0NGQII8jZWrIcTrP8wiP9fF3JwUsKrJhcBn
+xRs3hQJBAIDUgz1YIE+HW3vgi1gkOh6RPdBAsVpiXtr/fggFz3j60qrO7FswaAMj
+SX8c/6KUlBYkNjgP3qruFf4zcUNvEzcCQQCaioCPFVE9ByBpjLG6IUTKsz2R9xL5
+nfYqrbpLZ1aq6iLsYvkjugHE4X57sHLwNfdo4dHJbnf9wqhO2MVe25BhAkBdKYpY
+7OKc/2mmMbJDhVBgoixz/muN/5VjdfbvVY48naZkJF1p1tmogqPC5F1jPCS4rM+S
+FfPJIHRNEn2oktw5
+-----END PRIVATE KEY-----
diff --git a/tornado/test/testing_test.py b/tornado/test/testing_test.py
new file mode 100644
index 0000000..bdca031
--- /dev/null
+++ b/tornado/test/testing_test.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+import unittest
+from tornado.testing import AsyncTestCase, LogTrapTestCase
+
+class AsyncTestCaseTest(AsyncTestCase, LogTrapTestCase):
+    def test_exception_in_callback(self):
+        self.io_loop.add_callback(lambda: 1/0)
+        try:
+            self.wait()
+            self.fail("did not get expected exception")
+        except ZeroDivisionError:
+            pass
+
+class SetUpTearDownTest(unittest.TestCase):
+    def test_set_up_tear_down(self):
+        """
+        This test makes sure that AsyncTestCase calls super methods for
+        setUp and tearDown.
+
+        InheritBoth is a subclass of both AsyncTestCase and
+        SetUpTearDown, with the ordering so that the super of
+        AsyncTestCase will be SetUpTearDown.
+        """
+        events = []
+        result = unittest.TestResult()
+
+        class SetUpTearDown(unittest.TestCase):
+            def setUp(self):
+                events.append('setUp')
+
+            def tearDown(self):
+                events.append('tearDown')
+
+        class InheritBoth(AsyncTestCase, SetUpTearDown):
+            def test(self):
+                events.append('test')
+
+        InheritBoth('test').run(result)
+        expected = ['setUp', 'test', 'tearDown']
+        self.assertEqual(expected, events)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tornado/test/twisted_test.py b/tornado/test/twisted_test.py
new file mode 100644
index 0000000..ba53c78
--- /dev/null
+++ b/tornado/test/twisted_test.py
@@ -0,0 +1,474 @@
+# 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.
+
+"""
+Unittest for the twisted-style reactor.
+"""
+
+import os
+import thread
+import threading
+import unittest
+
+try:
+    import fcntl
+    import twisted
+    from twisted.internet.defer import Deferred
+    from twisted.internet.interfaces import IReadDescriptor, IWriteDescriptor
+    from twisted.internet.protocol import Protocol
+    from twisted.web.client import Agent
+    from twisted.web.resource import Resource
+    from twisted.web.server import Site
+    from twisted.python import log
+    from tornado.platform.twisted import TornadoReactor
+    from zope.interface import implements
+except ImportError:
+    fcntl = None
+    twisted = None
+    IReadDescriptor = IWriteDescriptor = None
+    def implements(f): pass
+
+from tornado.httpclient import AsyncHTTPClient
+from tornado.ioloop import IOLoop
+from tornado.platform.auto import set_close_exec
+from tornado.testing import get_unused_port
+from tornado.util import import_object
+from tornado.web import RequestHandler, Application
+
+class ReactorTestCase(unittest.TestCase):
+    def setUp(self):
+        self._io_loop = IOLoop()
+        self._reactor = TornadoReactor(self._io_loop)
+
+    def tearDown(self):
+        self._io_loop.close(all_fds=True)
+
+class ReactorWhenRunningTest(ReactorTestCase):
+    def test_whenRunning(self):
+        self._whenRunningCalled = False
+        self._anotherWhenRunningCalled = False
+        self._reactor.callWhenRunning(self.whenRunningCallback)
+        self._reactor.run()
+        self.assertTrue(self._whenRunningCalled)
+        self.assertTrue(self._anotherWhenRunningCalled)
+
+    def whenRunningCallback(self):
+        self._whenRunningCalled = True
+        self._reactor.callWhenRunning(self.anotherWhenRunningCallback)
+        self._reactor.stop()
+
+    def anotherWhenRunningCallback(self):
+        self._anotherWhenRunningCalled = True
+
+class ReactorCallLaterTest(ReactorTestCase):
+    def test_callLater(self):
+        self._laterCalled = False
+        self._now = self._reactor.seconds()
+        self._timeout = 0.001
+        dc = self._reactor.callLater(self._timeout, self.callLaterCallback)
+        self.assertEqual(self._reactor.getDelayedCalls(), [dc])
+        self._reactor.run()
+        self.assertTrue(self._laterCalled)
+        self.assertTrue(self._called - self._now > self._timeout)
+        self.assertEqual(self._reactor.getDelayedCalls(), [])
+
+    def callLaterCallback(self):
+        self._laterCalled = True
+        self._called = self._reactor.seconds()
+        self._reactor.stop()
+
+class ReactorTwoCallLaterTest(ReactorTestCase):
+    def test_callLater(self):
+        self._later1Called = False
+        self._later2Called = False
+        self._now = self._reactor.seconds()
+        self._timeout1 = 0.0005
+        dc1 = self._reactor.callLater(self._timeout1, self.callLaterCallback1)
+        self._timeout2 = 0.001
+        dc2 = self._reactor.callLater(self._timeout2, self.callLaterCallback2)
+        self.assertTrue(self._reactor.getDelayedCalls() == [dc1, dc2] or
+                        self._reactor.getDelayedCalls() == [dc2, dc1])
+        self._reactor.run()
+        self.assertTrue(self._later1Called)
+        self.assertTrue(self._later2Called)
+        self.assertTrue(self._called1 - self._now > self._timeout1)
+        self.assertTrue(self._called2 - self._now > self._timeout2)
+        self.assertEqual(self._reactor.getDelayedCalls(), [])
+
+    def callLaterCallback1(self):
+        self._later1Called = True
+        self._called1 = self._reactor.seconds()
+
+    def callLaterCallback2(self):
+        self._later2Called = True
+        self._called2 = self._reactor.seconds()
+        self._reactor.stop()
+
+class ReactorCallFromThreadTest(ReactorTestCase):
+    def setUp(self):
+        super(ReactorCallFromThreadTest, self).setUp()
+        self._mainThread = thread.get_ident()
+
+    def tearDown(self):
+        self._thread.join()
+        super(ReactorCallFromThreadTest, self).tearDown()
+
+    def _newThreadRun(self):
+        self.assertNotEqual(self._mainThread, thread.get_ident())
+        if hasattr(self._thread, 'ident'):  # new in python 2.6
+            self.assertEqual(self._thread.ident, thread.get_ident())
+        self._reactor.callFromThread(self._fnCalledFromThread)
+
+    def _fnCalledFromThread(self):
+        self.assertEqual(self._mainThread, thread.get_ident())
+        self._reactor.stop()
+
+    def _whenRunningCallback(self):
+        self._thread = threading.Thread(target=self._newThreadRun)
+        self._thread.start()
+
+    def testCallFromThread(self):
+        self._reactor.callWhenRunning(self._whenRunningCallback)
+        self._reactor.run()
+
+class ReactorCallInThread(ReactorTestCase):
+    def setUp(self):
+        super(ReactorCallInThread, self).setUp()
+        self._mainThread = thread.get_ident()
+
+    def _fnCalledInThread(self, *args, **kwargs):
+        self.assertNotEqual(thread.get_ident(), self._mainThread)
+        self._reactor.callFromThread(lambda: self._reactor.stop())
+
+    def _whenRunningCallback(self):
+        self._reactor.callInThread(self._fnCalledInThread)
+
+    def testCallInThread(self):
+        self._reactor.callWhenRunning(self._whenRunningCallback)
+        self._reactor.run()
+
+class Reader:
+    implements(IReadDescriptor)
+
+    def __init__(self, fd, callback):
+        self._fd = fd
+        self._callback = callback
+
+    def logPrefix(self): return "Reader"
+
+    def close(self):
+        self._fd.close()
+
+    def fileno(self):
+        return self._fd.fileno()
+
+    def connectionLost(self, reason):
+        self.close()
+
+    def doRead(self):
+        self._callback(self._fd)
+
+class Writer:
+    implements(IWriteDescriptor)
+
+    def __init__(self, fd, callback):
+        self._fd = fd
+        self._callback = callback
+
+    def logPrefix(self): return "Writer"
+
+    def close(self):
+        self._fd.close()
+
+    def fileno(self):
+        return self._fd.fileno()
+
+    def connectionLost(self, reason):
+        self.close()
+
+    def doWrite(self):
+        self._callback(self._fd)
+
+class ReactorReaderWriterTest(ReactorTestCase):
+    def _set_nonblocking(self, fd):
+        flags = fcntl.fcntl(fd, fcntl.F_GETFL)
+        fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
+
+    def setUp(self):
+        super(ReactorReaderWriterTest, self).setUp()
+        r, w = os.pipe()
+        self._set_nonblocking(r)
+        self._set_nonblocking(w)
+        set_close_exec(r)
+        set_close_exec(w)
+        self._p1 = os.fdopen(r, "rb", 0)
+        self._p2 = os.fdopen(w, "wb", 0)
+
+    def tearDown(self):
+        super(ReactorReaderWriterTest, self).tearDown()
+        self._p1.close()
+        self._p2.close()
+
+    def _testReadWrite(self):
+        """
+        In this test the writer writes an 'x' to its fd. The reader
+        reads it, check the value and ends the test.
+        """
+        self.shouldWrite = True
+        def checkReadInput(fd):
+            self.assertEquals(fd.read(), 'x')
+            self._reactor.stop()
+        def writeOnce(fd):
+            if self.shouldWrite:
+                self.shouldWrite = False
+                fd.write('x')
+        self._reader = Reader(self._p1, checkReadInput)
+        self._writer = Writer(self._p2, writeOnce)
+
+        self._reactor.addWriter(self._writer)
+
+        # Test that adding the reader twice adds it only once to
+        # IOLoop.
+        self._reactor.addReader(self._reader)
+        self._reactor.addReader(self._reader)
+
+    def testReadWrite(self):
+        self._reactor.callWhenRunning(self._testReadWrite)
+        self._reactor.run()
+
+    def _testNoWriter(self):
+        """
+        In this test we have no writer. Make sure the reader doesn't
+        read anything.
+        """
+        def checkReadInput(fd):
+            self.fail("Must not be called.")
+
+        def stopTest():
+            # Close the writer here since the IOLoop doesn't know
+            # about it.
+            self._writer.close()
+            self._reactor.stop()
+        self._reader = Reader(self._p1, checkReadInput)
+
+        # We create a writer, but it should never be invoked.
+        self._writer = Writer(self._p2, lambda fd: fd.write('x'))
+
+        # Test that adding and removing the writer leaves us with no writer.
+        self._reactor.addWriter(self._writer)
+        self._reactor.removeWriter(self._writer)
+
+        # Test that adding and removing the reader doesn't cause
+        # unintended effects.
+        self._reactor.addReader(self._reader)
+
+        # Wake up after a moment and stop the test
+        self._reactor.callLater(0.001, stopTest)
+
+    def testNoWriter(self):
+        self._reactor.callWhenRunning(self._testNoWriter)
+        self._reactor.run()
+
+# Test various combinations of twisted and tornado http servers,
+# http clients, and event loop interfaces.
+class CompatibilityTests(unittest.TestCase):
+    def setUp(self):
+        self.io_loop = IOLoop()
+        self.reactor = TornadoReactor(self.io_loop)
+
+    def tearDown(self):
+        self.reactor.disconnectAll()
+        self.io_loop.close(all_fds=True)
+
+    def start_twisted_server(self):
+        class HelloResource(Resource):
+            isLeaf = True
+            def render_GET(self, request):
+                return "Hello from twisted!"
+        site = Site(HelloResource())
+        self.twisted_port = get_unused_port()
+        self.reactor.listenTCP(self.twisted_port, site, interface='127.0.0.1')
+
+    def start_tornado_server(self):
+        class HelloHandler(RequestHandler):
+            def get(self):
+                self.write("Hello from tornado!")
+        app = Application([('/', HelloHandler)],
+                          log_function=lambda x: None)
+        self.tornado_port = get_unused_port()
+        app.listen(self.tornado_port, address='127.0.0.1', io_loop=self.io_loop)
+
+    def run_ioloop(self):
+        self.stop_loop = self.io_loop.stop
+        self.io_loop.start()
+        self.reactor.fireSystemEvent('shutdown')
+
+    def run_reactor(self):
+        self.stop_loop = self.reactor.stop
+        self.stop = self.reactor.stop
+        self.reactor.run()
+
+    def tornado_fetch(self, url, runner):
+        responses = []
+        client = AsyncHTTPClient(self.io_loop)
+        def callback(response):
+            responses.append(response)
+            self.stop_loop()
+        client.fetch(url, callback=callback)
+        runner()
+        self.assertEqual(len(responses), 1)
+        responses[0].rethrow()
+        return responses[0]
+
+    def twisted_fetch(self, url, runner):
+        # http://twistedmatrix.com/documents/current/web/howto/client.html
+        chunks = []
+        client = Agent(self.reactor)
+        d = client.request('GET', url)
+        class Accumulator(Protocol):
+            def __init__(self, finished):
+                self.finished = finished
+            def dataReceived(self, data):
+                chunks.append(data)
+            def connectionLost(self, reason):
+                self.finished.callback(None)
+        def callback(response):
+            finished = Deferred()
+            response.deliverBody(Accumulator(finished))
+            return finished
+        d.addCallback(callback)
+        def shutdown(ignored):
+            self.stop_loop()
+        d.addBoth(shutdown)
+        runner()
+        self.assertTrue(chunks)
+        return ''.join(chunks)
+
+    def testTwistedServerTornadoClientIOLoop(self):
+        self.start_twisted_server()
+        response = self.tornado_fetch(
+            'http://localhost:%d' % self.twisted_port, self.run_ioloop)
+        self.assertEqual(response.body, 'Hello from twisted!')
+
+    def testTwistedServerTornadoClientReactor(self):
+        self.start_twisted_server()
+        response = self.tornado_fetch(
+            'http://localhost:%d' % self.twisted_port, self.run_reactor)
+        self.assertEqual(response.body, 'Hello from twisted!')
+
+    def testTornadoServerTwistedClientIOLoop(self):
+        self.start_tornado_server()
+        response = self.twisted_fetch(
+            'http://localhost:%d' % self.tornado_port, self.run_ioloop)
+        self.assertEqual(response, 'Hello from tornado!')
+
+    def testTornadoServerTwistedClientReactor(self):
+        self.start_tornado_server()
+        response = self.twisted_fetch(
+            'http://localhost:%d' % self.tornado_port, self.run_reactor)
+        self.assertEqual(response, 'Hello from tornado!')
+
+
+if twisted is None:
+    del ReactorWhenRunningTest
+    del ReactorCallLaterTest
+    del ReactorTwoCallLaterTest
+    del ReactorCallFromThreadTest
+    del ReactorCallInThread
+    del ReactorReaderWriterTest
+    del CompatibilityTests
+else:
+    # Import and run as much of twisted's test suite as possible.
+    # This is unfortunately rather dependent on implementation details,
+    # but there doesn't appear to be a clean all-in-one conformance test
+    # suite for reactors.
+    #
+    # This is a list of all test suites using the ReactorBuilder
+    # available in Twisted 11.0.0 and 11.1.0 (and a blacklist of
+    # specific test methods to be disabled).
+    twisted_tests = {
+        'twisted.internet.test.test_core.ObjectModelIntegrationTest': [],
+        'twisted.internet.test.test_core.SystemEventTestsBuilder': [
+            'test_iterate',  # deliberately not supported
+            ],
+        'twisted.internet.test.test_fdset.ReactorFDSetTestsBuilder': [
+            "test_lostFileDescriptor",  # incompatible with epoll and kqueue
+            ],
+        'twisted.internet.test.test_process.ProcessTestsBuilder': [
+            # Doesn't work on python 2.5
+            'test_systemCallUninterruptedByChildExit',
+            # Doesn't clean up its temp files
+            'test_shebang',
+            ],
+        'twisted.internet.test.test_process.PTYProcessTestsBuilder': [
+            'test_systemCallUninterruptedByChildExit',
+            ],
+        'twisted.internet.test.test_tcp.TCPClientTestsBuilder': [],
+        'twisted.internet.test.test_tcp.TCPPortTestsBuilder': [],
+        'twisted.internet.test.test_tcp.TCPConnectionTestsBuilder': [],
+        'twisted.internet.test.test_tcp.WriteSequenceTests': [],
+        'twisted.internet.test.test_tcp.AbortConnectionTestCase': [],
+        'twisted.internet.test.test_threads.ThreadTestsBuilder': [],
+        'twisted.internet.test.test_time.TimeTestsBuilder': [],
+        # Extra third-party dependencies (pyOpenSSL)
+        #'twisted.internet.test.test_tls.SSLClientTestsMixin': [],
+        'twisted.internet.test.test_udp.UDPServerTestsBuilder': [],
+        'twisted.internet.test.test_unix.UNIXTestsBuilder': [
+            # Platform-specific.  These tests would be skipped automatically
+            # if we were running twisted's own test runner.
+            'test_connectToLinuxAbstractNamespace',
+            'test_listenOnLinuxAbstractNamespace',
+            ],
+        'twisted.internet.test.test_unix.UNIXDatagramTestsBuilder': [
+            'test_listenOnLinuxAbstractNamespace',
+            ],
+        'twisted.internet.test.test_unix.UNIXPortTestsBuilder': [],
+        }
+    for test_name, blacklist in twisted_tests.iteritems():
+        try:
+            test_class = import_object(test_name)
+        except (ImportError, AttributeError):
+            continue
+        for test_func in blacklist:
+            if hasattr(test_class, test_func):
+                # The test_func may be defined in a mixin, so clobber
+                # it instead of delattr()
+                setattr(test_class, test_func, lambda self: None)
+        def make_test_subclass(test_class):
+            class TornadoTest(test_class):
+                _reactors = ["tornado.platform.twisted._TestReactor"]
+                def unbuildReactor(self, reactor):
+                    test_class.unbuildReactor(self, reactor)
+                    # Clean up file descriptors (especially epoll/kqueue
+                    # objects) eagerly instead of leaving them for the
+                    # GC.  Unfortunately we can't do this in reactor.stop
+                    # since twisted expects to be able to unregister
+                    # connections in a post-shutdown hook.
+                    reactor._io_loop.close(all_fds=True)
+            TornadoTest.__name__ = test_class.__name__
+            return TornadoTest
+        test_subclass = make_test_subclass(test_class)
+        globals().update(test_subclass.makeTestCaseClasses())
+
+    # Since we're not using twisted's test runner, it's tricky to get
+    # logging set up well.  Most of the time it's easiest to just
+    # leave it turned off, but while working on these tests you may want
+    # to uncomment one of the other lines instead.
+    log.defaultObserver.stop()
+    #import sys; log.startLogging(sys.stderr, setStdout=0)
+    #log.startLoggingWithObserver(log.PythonLoggingObserver().emit, setStdout=0)
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tornado/test/web_test.py b/tornado/test/web_test.py
new file mode 100644
index 0000000..9f4c860
--- /dev/null
+++ b/tornado/test/web_test.py
@@ -0,0 +1,638 @@
+from tornado.escape import json_decode, utf8, to_unicode, recursive_unicode, native_str
+from tornado.iostream import IOStream
+from tornado.template import DictLoader
+from tornado.testing import LogTrapTestCase, AsyncHTTPTestCase
+from tornado.util import b, bytes_type, ObjectDict
+from tornado.web import RequestHandler, authenticated, Application, asynchronous, url, HTTPError, StaticFileHandler, _create_signature
+
+import binascii
+import logging
+import os
+import re
+import socket
+import sys
+
+class CookieTestRequestHandler(RequestHandler):
+    # stub out enough methods to make the secure_cookie functions work
+    def __init__(self):
+        # don't call super.__init__
+        self._cookies = {}
+        self.application = ObjectDict(settings=dict(cookie_secret='0123456789'))
+
+    def get_cookie(self, name):
+        return self._cookies.get(name)
+
+    def set_cookie(self, name, value, expires_days=None):
+        self._cookies[name] = value
+
+class SecureCookieTest(LogTrapTestCase):
+    def test_round_trip(self):
+        handler = CookieTestRequestHandler()
+        handler.set_secure_cookie('foo', b('bar'))
+        self.assertEqual(handler.get_secure_cookie('foo'), b('bar'))
+
+    def test_cookie_tampering_future_timestamp(self):
+        handler = CookieTestRequestHandler()
+        # this string base64-encodes to '12345678'
+        handler.set_secure_cookie('foo', binascii.a2b_hex(b('d76df8e7aefc')))
+        cookie = handler._cookies['foo']
+        match = re.match(b(r'12345678\|([0-9]+)\|([0-9a-f]+)'), cookie)
+        assert match
+        timestamp = match.group(1)
+        sig = match.group(2)
+        self.assertEqual(
+            _create_signature(handler.application.settings["cookie_secret"],
+                              'foo', '12345678', timestamp),
+            sig)
+        # shifting digits from payload to timestamp doesn't alter signature
+        # (this is not desirable behavior, just confirming that that's how it
+        # works)
+        self.assertEqual(
+            _create_signature(handler.application.settings["cookie_secret"],
+                              'foo', '1234', b('5678') + timestamp),
+            sig)
+        # tamper with the cookie
+        handler._cookies['foo'] = utf8('1234|5678%s|%s' % (timestamp, sig))
+        # it gets rejected
+        assert handler.get_secure_cookie('foo') is None
+
+class CookieTest(AsyncHTTPTestCase, LogTrapTestCase):
+    def get_app(self):
+        class SetCookieHandler(RequestHandler):
+            def get(self):
+                # Try setting cookies with different argument types
+                # to ensure that everything gets encoded correctly
+                self.set_cookie("str", "asdf")
+                self.set_cookie("unicode", u"qwer")
+                self.set_cookie("bytes", b("zxcv"))
+
+        class GetCookieHandler(RequestHandler):
+            def get(self):
+                self.write(self.get_cookie("foo", "default"))
+
+        class SetCookieDomainHandler(RequestHandler):
+            def get(self):
+                # unicode domain and path arguments shouldn't break things
+                # either (see bug #285)
+                self.set_cookie("unicode_args", "blah", domain=u"foo.com",
+                                path=u"/foo")
+
+        class SetCookieSpecialCharHandler(RequestHandler):
+            def get(self):
+                self.set_cookie("equals", "a=b")
+                self.set_cookie("semicolon", "a;b")
+                self.set_cookie("quote", 'a"b')
+
+
+        return Application([
+                ("/set", SetCookieHandler),
+                ("/get", GetCookieHandler),
+                ("/set_domain", SetCookieDomainHandler),
+                ("/special_char", SetCookieSpecialCharHandler),
+                ])
+
+    def test_set_cookie(self):
+        response = self.fetch("/set")
+        self.assertEqual(response.headers.get_list("Set-Cookie"),
+                         ["str=asdf; Path=/",
+                          "unicode=qwer; Path=/",
+                          "bytes=zxcv; Path=/"])
+
+    def test_get_cookie(self):
+        response = self.fetch("/get", headers={"Cookie": "foo=bar"})
+        self.assertEqual(response.body, b("bar"))
+
+        response = self.fetch("/get", headers={"Cookie": 'foo="bar"'})
+        self.assertEqual(response.body, b("bar"))
+
+        response = self.fetch("/get", headers={"Cookie": "/=exception;"})
+        self.assertEqual(response.body, b("default"))
+
+    def test_set_cookie_domain(self):
+        response = self.fetch("/set_domain")
+        self.assertEqual(response.headers.get_list("Set-Cookie"),
+                         ["unicode_args=blah; Domain=foo.com; Path=/foo"])
+
+    def test_cookie_special_char(self):
+        response = self.fetch("/special_char")
+        headers = response.headers.get_list("Set-Cookie")
+        self.assertEqual(len(headers), 3)
+        self.assertEqual(headers[0], 'equals="a=b"; Path=/')
+        # python 2.7 octal-escapes the semicolon; older versions leave it alone
+        self.assertTrue(headers[1] in ('semicolon="a;b"; Path=/',
+                                       'semicolon="a\\073b"; Path=/'),
+                        headers[1])
+        self.assertEqual(headers[2], 'quote="a\\"b"; Path=/')
+
+        data = [('foo=a=b', 'a=b'),
+                ('foo="a=b"', 'a=b'),
+                ('foo="a;b"', 'a;b'),
+                #('foo=a\\073b', 'a;b'),  # even encoded, ";" is a delimiter
+                ('foo="a\\073b"', 'a;b'),
+                ('foo="a\\"b"', 'a"b'),
+                ]
+        for header, expected in data:
+            logging.info("trying %r", header)
+            response = self.fetch("/get", headers={"Cookie": header})
+            self.assertEqual(response.body, utf8(expected))
+
+class AuthRedirectRequestHandler(RequestHandler):
+    def initialize(self, login_url):
+        self.login_url = login_url
+
+    def get_login_url(self):
+        return self.login_url
+
+    @authenticated
+    def get(self):
+        # we'll never actually get here because the test doesn't follow redirects
+        self.send_error(500)
+
+class AuthRedirectTest(AsyncHTTPTestCase, LogTrapTestCase):
+    def get_app(self):
+        return Application([('/relative', AuthRedirectRequestHandler,
+                             dict(login_url='/login')),
+                            ('/absolute', AuthRedirectRequestHandler,
+                             dict(login_url='http://example.com/login'))])
+
+    def test_relative_auth_redirect(self):
+        self.http_client.fetch(self.get_url('/relative'), self.stop,
+                               follow_redirects=False)
+        response = self.wait()
+        self.assertEqual(response.code, 302)
+        self.assertEqual(response.headers['Location'], '/login?next=%2Frelative')
+
+    def test_absolute_auth_redirect(self):
+        self.http_client.fetch(self.get_url('/absolute'), self.stop,
+                               follow_redirects=False)
+        response = self.wait()
+        self.assertEqual(response.code, 302)
+        self.assertTrue(re.match(
+            'http://example.com/login\?next=http%3A%2F%2Flocalhost%3A[0-9]+%2Fabsolute',
+            response.headers['Location']), response.headers['Location'])
+
+
+class ConnectionCloseHandler(RequestHandler):
+    def initialize(self, test):
+        self.test = test
+
+    @asynchronous
+    def get(self):
+        self.test.on_handler_waiting()
+
+    def on_connection_close(self):
+        self.test.on_connection_close()
+
+class ConnectionCloseTest(AsyncHTTPTestCase, LogTrapTestCase):
+    def get_app(self):
+        return Application([('/', ConnectionCloseHandler, dict(test=self))])
+
+    def test_connection_close(self):
+        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
+        s.connect(("localhost", self.get_http_port()))
+        self.stream = IOStream(s, io_loop=self.io_loop)
+        self.stream.write(b("GET / HTTP/1.0\r\n\r\n"))
+        self.wait()
+
+    def on_handler_waiting(self):
+        logging.info('handler waiting')
+        self.stream.close()
+
+    def on_connection_close(self):
+        logging.info('connection closed')
+        self.stop()
+
+class EchoHandler(RequestHandler):
+    def get(self, path):
+        # Type checks: web.py interfaces convert argument values to
+        # unicode strings (by default, but see also decode_argument).
+        # In httpserver.py (i.e. self.request.arguments), they're left
+        # as bytes.  Keys are always native strings.
+        for key in self.request.arguments:
+            assert type(key) == str, repr(key)
+            for value in self.request.arguments[key]:
+                assert type(value) == bytes_type, repr(value)
+            for value in self.get_arguments(key):
+                assert type(value) == unicode, repr(value)
+        assert type(path) == unicode, repr(path)
+        self.write(dict(path=path,
+                        args=recursive_unicode(self.request.arguments)))
+
+class RequestEncodingTest(AsyncHTTPTestCase, LogTrapTestCase):
+    def get_app(self):
+        return Application([("/(.*)", EchoHandler)])
+
+    def test_question_mark(self):
+        # Ensure that url-encoded question marks are handled properly
+        self.assertEqual(json_decode(self.fetch('/%3F').body),
+                         dict(path='?', args={}))
+        self.assertEqual(json_decode(self.fetch('/%3F?%3F=%3F').body),
+                         dict(path='?', args={'?': ['?']}))
+
+    def test_path_encoding(self):
+        # Path components and query arguments should be decoded the same way
+        self.assertEqual(json_decode(self.fetch('/%C3%A9?arg=%C3%A9').body),
+                         {u"path":u"\u00e9",
+                          u"args": {u"arg": [u"\u00e9"]}})
+
+class TypeCheckHandler(RequestHandler):
+    def prepare(self):
+        self.errors = {}
+
+        self.check_type('status', self.get_status(), int)
+
+        # get_argument is an exception from the general rule of using
+        # type str for non-body data mainly for historical reasons.
+        self.check_type('argument', self.get_argument('foo'), unicode)
+        self.check_type('cookie_key', self.cookies.keys()[0], str)
+        self.check_type('cookie_value', self.cookies.values()[0].value, str)
+
+        self.check_type('xsrf_token', self.xsrf_token, bytes_type)
+        self.check_type('xsrf_form_html', self.xsrf_form_html(), str)
+
+        self.check_type('reverse_url', self.reverse_url('typecheck', 'foo'), str)
+
+        self.check_type('request_summary', self._request_summary(), str)
+
+    def get(self, path_component):
+        # path_component uses type unicode instead of str for consistency
+        # with get_argument()
+        self.check_type('path_component', path_component, unicode)
+        self.write(self.errors)
+
+    def post(self, path_component):
+        self.check_type('path_component', path_component, unicode)
+        self.write(self.errors)
+
+    def check_type(self, name, obj, expected_type):
+        actual_type = type(obj)
+        if expected_type != actual_type:
+            self.errors[name] = "expected %s, got %s" % (expected_type,
+                                                         actual_type)
+
+class DecodeArgHandler(RequestHandler):
+    def decode_argument(self, value, name=None):
+        assert type(value) == bytes_type, repr(value)
+        # use self.request.arguments directly to avoid recursion
+        if 'encoding' in self.request.arguments:
+            return value.decode(to_unicode(self.request.arguments['encoding'][0]))
+        else:
+            return value
+
+    def get(self, arg):
+        def describe(s):
+            if type(s) == bytes_type:
+                return ["bytes", native_str(binascii.b2a_hex(s))]
+            elif type(s) == unicode:
+                return ["unicode", s]
+            raise Exception("unknown type")
+        self.write({'path': describe(arg),
+                    'query': describe(self.get_argument("foo")),
+                    })
+
+class LinkifyHandler(RequestHandler):
+    def get(self):
+        self.render("linkify.html", message="http://example.com")
+
+class UIModuleResourceHandler(RequestHandler):
+    def get(self):
+        self.render("page.html", entries=[1,2])
+
+class OptionalPathHandler(RequestHandler):
+    def get(self, path):
+        self.write({"path": path})
+
+class FlowControlHandler(RequestHandler):
+    # These writes are too small to demonstrate real flow control,
+    # but at least it shows that the callbacks get run.
+    @asynchronous
+    def get(self):
+        self.write("1")
+        self.flush(callback=self.step2)
+
+    def step2(self):
+        self.write("2")
+        self.flush(callback=self.step3)
+
+    def step3(self):
+        self.write("3")
+        self.finish()
+
+class MultiHeaderHandler(RequestHandler):
+    def get(self):
+        self.set_header("x-overwrite", "1")
+        self.set_header("x-overwrite", 2)
+        self.add_header("x-multi", 3)
+        self.add_header("x-multi", "4")
+
+class RedirectHandler(RequestHandler):
+    def get(self):
+        if self.get_argument('permanent', None) is not None:
+            self.redirect('/', permanent=int(self.get_argument('permanent')))
+        elif self.get_argument('status', None) is not None:
+            self.redirect('/', status=int(self.get_argument('status')))
+        else:
+            raise Exception("didn't get permanent or status arguments")
+
+
+class WebTest(AsyncHTTPTestCase, LogTrapTestCase):
+    def get_app(self):
+        loader = DictLoader({
+                "linkify.html": "{% module linkify(message) %}",
+                "page.html": """\
+<html><head></head><body>
+{% for e in entries %}
+{% module Template("entry.html", entry=e) %}
+{% end %}
+</body></html>""",
+                "entry.html": """\
+{{ set_resources(embedded_css=".entry { margin-bottom: 1em; }", embedded_javascript="js_embed()", css_files=["/base.css", "/foo.css"], javascript_files="/common.js", html_head="<meta>", html_body='<script src="/analytics.js"/>') }}
+<div class="entry">...</div>""",
+                })
+        urls = [
+            url("/typecheck/(.*)", TypeCheckHandler, name='typecheck'),
+            url("/decode_arg/(.*)", DecodeArgHandler),
+            url("/decode_arg_kw/(?P<arg>.*)", DecodeArgHandler),
+            url("/linkify", LinkifyHandler),
+            url("/uimodule_resources", UIModuleResourceHandler),
+            url("/optional_path/(.+)?", OptionalPathHandler),
+            url("/flow_control", FlowControlHandler),
+            url("/multi_header", MultiHeaderHandler),
+            url("/redirect", RedirectHandler),
+            ]
+        return Application(urls,
+                           template_loader=loader,
+                           autoescape="xhtml_escape")
+
+    def fetch_json(self, *args, **kwargs):
+        response = self.fetch(*args, **kwargs)
+        response.rethrow()
+        return json_decode(response.body)
+
+    def test_types(self):
+        response = self.fetch("/typecheck/asdf?foo=bar",
+                              headers={"Cookie": "cook=ie"})
+        data = json_decode(response.body)
+        self.assertEqual(data, {})
+
+        response = self.fetch("/typecheck/asdf?foo=bar", method="POST",
+                              headers={"Cookie": "cook=ie"},
+                              body="foo=bar")
+
+    def test_decode_argument(self):
+        # These urls all decode to the same thing
+        urls = ["/decode_arg/%C3%A9?foo=%C3%A9&encoding=utf-8",
+                "/decode_arg/%E9?foo=%E9&encoding=latin1",
+                "/decode_arg_kw/%E9?foo=%E9&encoding=latin1",
+                ]
+        for url in urls:
+            response = self.fetch(url)
+            response.rethrow()
+            data = json_decode(response.body)
+            self.assertEqual(data, {u'path': [u'unicode', u'\u00e9'],
+                                    u'query': [u'unicode', u'\u00e9'],
+                                    })
+
+        response = self.fetch("/decode_arg/%C3%A9?foo=%C3%A9")
+        response.rethrow()
+        data = json_decode(response.body)
+        self.assertEqual(data, {u'path': [u'bytes', u'c3a9'],
+                                u'query': [u'bytes', u'c3a9'],
+                                })
+
+    def test_uimodule_unescaped(self):
+        response = self.fetch("/linkify")
+        self.assertEqual(response.body,
+                         b("<a href=\"http://example.com\">http://example.com</a>"))
+
+    def test_uimodule_resources(self):
+        response = self.fetch("/uimodule_resources")
+        self.assertEqual(response.body, b("""\
+<html><head><link href="/base.css" type="text/css" rel="stylesheet"/><link href="/foo.css" type="text/css" rel="stylesheet"/>
+<style type="text/css">
+.entry { margin-bottom: 1em; }
+</style>
+<meta>
+</head><body>
+
+
+<div class="entry">...</div>
+
+
+<div class="entry">...</div>
+
+<script src="/common.js" type="text/javascript"></script>
+<script type="text/javascript">
+//<![CDATA[
+js_embed()
+//]]>
+</script>
+<script src="/analytics.js"/>
+</body></html>"""))
+
+    def test_optional_path(self):
+        self.assertEqual(self.fetch_json("/optional_path/foo"),
+                         {u"path": u"foo"})
+        self.assertEqual(self.fetch_json("/optional_path/"),
+                         {u"path": None})
+
+    def test_flow_control(self):
+        self.assertEqual(self.fetch("/flow_control").body, b("123"))
+
+    def test_multi_header(self):
+        response = self.fetch("/multi_header")
+        self.assertEqual(response.headers["x-overwrite"], "2")
+        self.assertEqual(response.headers.get_list("x-multi"), ["3", "4"])
+
+    def test_redirect(self):
+        response = self.fetch("/redirect?permanent=1", follow_redirects=False)
+        self.assertEqual(response.code, 301)
+        response = self.fetch("/redirect?permanent=0", follow_redirects=False)
+        self.assertEqual(response.code, 302)
+        response = self.fetch("/redirect?status=307", follow_redirects=False)
+        self.assertEqual(response.code, 307)
+
+
+class ErrorResponseTest(AsyncHTTPTestCase, LogTrapTestCase):
+    def get_app(self):
+        class DefaultHandler(RequestHandler):
+            def get(self):
+                if self.get_argument("status", None):
+                    raise HTTPError(int(self.get_argument("status")))
+                1/0
+
+        class WriteErrorHandler(RequestHandler):
+            def get(self):
+                if self.get_argument("status", None):
+                    self.send_error(int(self.get_argument("status")))
+                else:
+                    1/0
+
+            def write_error(self, status_code, **kwargs):
+                self.set_header("Content-Type", "text/plain")
+                if "exc_info" in kwargs:
+                    self.write("Exception: %s" % kwargs["exc_info"][0].__name__)
+                else:
+                    self.write("Status: %d" % status_code)
+
+        class GetErrorHtmlHandler(RequestHandler):
+            def get(self):
+                if self.get_argument("status", None):
+                    self.send_error(int(self.get_argument("status")))
+                else:
+                    1/0
+
+            def get_error_html(self, status_code, **kwargs):
+                self.set_header("Content-Type", "text/plain")
+                if "exception" in kwargs:
+                    self.write("Exception: %s" % sys.exc_info()[0].__name__)
+                else:
+                    self.write("Status: %d" % status_code)
+
+        class FailedWriteErrorHandler(RequestHandler):
+            def get(self):
+                1/0
+
+            def write_error(self, status_code, **kwargs):
+                raise Exception("exception in write_error")
+
+
+        return Application([
+                url("/default", DefaultHandler),
+                url("/write_error", WriteErrorHandler),
+                url("/get_error_html", GetErrorHtmlHandler),
+                url("/failed_write_error", FailedWriteErrorHandler),
+                ])
+
+    def test_default(self):
+        response = self.fetch("/default")
+        self.assertEqual(response.code, 500)
+        self.assertTrue(b("500: Internal Server Error") in response.body)
+
+        response = self.fetch("/default?status=503")
+        self.assertEqual(response.code, 503)
+        self.assertTrue(b("503: Service Unavailable") in response.body)
+
+    def test_write_error(self):
+        response = self.fetch("/write_error")
+        self.assertEqual(response.code, 500)
+        self.assertEqual(b("Exception: ZeroDivisionError"), response.body)
+
+        response = self.fetch("/write_error?status=503")
+        self.assertEqual(response.code, 503)
+        self.assertEqual(b("Status: 503"), response.body)
+
+    def test_get_error_html(self):
+        response = self.fetch("/get_error_html")
+        self.assertEqual(response.code, 500)
+        self.assertEqual(b("Exception: ZeroDivisionError"), response.body)
+
+        response = self.fetch("/get_error_html?status=503")
+        self.assertEqual(response.code, 503)
+        self.assertEqual(b("Status: 503"), response.body)
+
+    def test_failed_write_error(self):
+        response = self.fetch("/failed_write_error")
+        self.assertEqual(response.code, 500)
+        self.assertEqual(b(""), response.body)
+
+class StaticFileTest(AsyncHTTPTestCase, LogTrapTestCase):
+    def get_app(self):
+        class StaticUrlHandler(RequestHandler):
+            def get(self, path):
+                self.write(self.static_url(path))
+
+        class AbsoluteStaticUrlHandler(RequestHandler):
+            include_host = True
+            def get(self, path):
+                self.write(self.static_url(path))
+
+        class OverrideStaticUrlHandler(RequestHandler):
+            def get(self, path):
+                do_include = bool(self.get_argument("include_host"))
+                self.include_host = not do_include
+
+                regular_url = self.static_url(path)
+                override_url = self.static_url(path, include_host=do_include)
+                if override_url == regular_url:
+                    return self.write(str(False))
+
+                protocol = self.request.protocol + "://"
+                protocol_length = len(protocol)
+                check_regular = regular_url.find(protocol, 0, protocol_length)
+                check_override = override_url.find(protocol, 0, protocol_length)
+
+                if do_include:
+                    result = (check_override == 0 and check_regular == -1)
+                else:
+                    result = (check_override == -1 and check_regular == 0)
+                self.write(str(result))
+
+        return Application([('/static_url/(.*)', StaticUrlHandler),
+                            ('/abs_static_url/(.*)', AbsoluteStaticUrlHandler),
+                            ('/override_static_url/(.*)', OverrideStaticUrlHandler)],
+                           static_path=os.path.join(os.path.dirname(__file__), 'static'))
+
+    def test_static_files(self):
+        response = self.fetch('/robots.txt')
+        assert b("Disallow: /") in response.body
+
+        response = self.fetch('/static/robots.txt')
+        assert b("Disallow: /") in response.body
+
+    def test_static_url(self):
+        response = self.fetch("/static_url/robots.txt")
+        self.assertEqual(response.body, b("/static/robots.txt?v=f71d2"))
+
+    def test_absolute_static_url(self):
+        response = self.fetch("/abs_static_url/robots.txt")
+        self.assertEqual(response.body,
+                         utf8(self.get_url("/") + "static/robots.txt?v=f71d2"))
+
+    def test_include_host_override(self):
+        self._trigger_include_host_check(False)
+        self._trigger_include_host_check(True)
+
+    def _trigger_include_host_check(self, include_host):
+        path = "/override_static_url/robots.txt?include_host=%s"
+        response = self.fetch(path % int(include_host))
+        self.assertEqual(response.body, utf8(str(True)))
+
+class CustomStaticFileTest(AsyncHTTPTestCase, LogTrapTestCase):
+    def get_app(self):
+        class MyStaticFileHandler(StaticFileHandler):
+            def get(self, path):
+                path = self.parse_url_path(path)
+                assert path == "foo.txt"
+                self.write("bar")
+
+            @classmethod
+            def make_static_url(cls, settings, path):
+                version_hash = cls.get_version(settings, path)
+                extension_index = path.rindex('.')
+                before_version = path[:extension_index]
+                after_version = path[(extension_index + 1):]
+                return '/static/%s.%s.%s' % (before_version, 42, after_version)
+
+            @classmethod
+            def parse_url_path(cls, url_path):
+                extension_index = url_path.rindex('.')
+                version_index = url_path.rindex('.', 0, extension_index)
+                return '%s%s' % (url_path[:version_index],
+                                 url_path[extension_index:])
+
+        class StaticUrlHandler(RequestHandler):
+            def get(self, path):
+                self.write(self.static_url(path))
+
+        return Application([("/static_url/(.*)", StaticUrlHandler)],
+                           static_path="dummy",
+                           static_handler_class=MyStaticFileHandler)
+
+    def test_serve(self):
+        response = self.fetch("/static/foo.42.txt")
+        self.assertEqual(response.body, b("bar"))
+
+    def test_static_url(self):
+        response = self.fetch("/static_url/foo.txt")
+        self.assertEqual(response.body, b("/static/foo.42.txt"))
diff --git a/tornado/test/wsgi_test.py b/tornado/test/wsgi_test.py
new file mode 100644
index 0000000..9c3ff7f
--- /dev/null
+++ b/tornado/test/wsgi_test.py
@@ -0,0 +1,58 @@
+from wsgiref.validate import validator
+
+from tornado.testing import AsyncHTTPTestCase, LogTrapTestCase
+from tornado.util import b
+from tornado.web import RequestHandler
+from tornado.wsgi import WSGIApplication, WSGIContainer
+
+class WSGIContainerTest(AsyncHTTPTestCase, LogTrapTestCase):
+    def wsgi_app(self, environ, start_response):
+        status = "200 OK"
+        response_headers = [("Content-Type", "text/plain")]
+        start_response(status, response_headers)
+        return [b("Hello world!")]
+
+    def get_app(self):
+        return WSGIContainer(validator(self.wsgi_app))
+
+    def test_simple(self):
+        response = self.fetch("/")
+        self.assertEqual(response.body, b("Hello world!"))
+
+class WSGIApplicationTest(AsyncHTTPTestCase, LogTrapTestCase):
+    def get_app(self):
+        class HelloHandler(RequestHandler):
+            def get(self):
+                self.write("Hello world!")
+
+        class PathQuotingHandler(RequestHandler):
+            def get(self, path):
+                self.write(path)
+
+        # It would be better to run the wsgiref server implementation in
+        # another thread instead of using our own WSGIContainer, but this
+        # fits better in our async testing framework and the wsgiref
+        # validator should keep us honest
+        return WSGIContainer(validator(WSGIApplication([
+                        ("/", HelloHandler),
+                        ("/path/(.*)", PathQuotingHandler),
+                        ])))
+
+    def test_simple(self):
+        response = self.fetch("/")
+        self.assertEqual(response.body, b("Hello world!"))
+
+    def test_path_quoting(self):
+        response = self.fetch("/path/foo%20bar%C3%A9")
+        self.assertEqual(response.body, u"foo bar\u00e9".encode("utf-8"))
+
+# This is kind of hacky, but run some of the HTTPServer tests through
+# WSGIContainer and WSGIApplication to make sure everything survives
+# repeated disassembly and reassembly.
+from tornado.test.httpserver_test import HTTPConnectionTest
+
+class WSGIConnectionTest(HTTPConnectionTest):
+    def get_app(self):
+        return WSGIContainer(validator(WSGIApplication(self.get_handlers())))
+
+del HTTPConnectionTest
diff --git a/tornado/testing.py b/tornado/testing.py
new file mode 100644
index 0000000..b2b983d
--- /dev/null
+++ b/tornado/testing.py
@@ -0,0 +1,382 @@
+#!/usr/bin/env python
+"""Support classes for automated testing.
+
+This module contains three parts:
+
+* `AsyncTestCase`/`AsyncHTTPTestCase`:  Subclasses of unittest.TestCase
+  with additional support for testing asynchronous (IOLoop-based) code.
+
+* `LogTrapTestCase`:  Subclass of unittest.TestCase that discards log output
+  from tests that pass and only produces output for failing tests.
+
+* `main()`: A simple test runner (wrapper around unittest.main()) with support
+  for the tornado.autoreload module to rerun the tests when code changes.
+
+These components may be used together or independently.  In particular,
+it is safe to combine AsyncTestCase and LogTrapTestCase via multiple
+inheritance.  See the docstrings for each class/function below for more
+information.
+"""
+
+from __future__ import with_statement
+
+from cStringIO import StringIO
+try:
+    from tornado.httpclient import AsyncHTTPClient
+    from tornado.httpserver import HTTPServer
+    from tornado.ioloop import IOLoop
+except ImportError:
+    # These modules are not importable on app engine.  Parts of this module
+    # won't work, but e.g. LogTrapTestCase and main() will.
+    AsyncHTTPClient = None
+    HTTPServer = None
+    IOLoop = None
+from tornado.stack_context import StackContext, NullContext
+import contextlib
+import logging
+import signal
+import sys
+import time
+import unittest
+
+_next_port = 10000
+def get_unused_port():
+    """Returns a (hopefully) unused port number."""
+    global _next_port
+    port = _next_port
+    _next_port = _next_port + 1
+    return port
+
+class AsyncTestCase(unittest.TestCase):
+    """TestCase subclass for testing IOLoop-based asynchronous code.
+
+    The unittest framework is synchronous, so the test must be complete
+    by the time the test method returns.  This method provides the stop()
+    and wait() methods for this purpose.  The test method itself must call
+    self.wait(), and asynchronous callbacks should call self.stop() to signal
+    completion.
+
+    By default, a new IOLoop is constructed for each test and is available
+    as self.io_loop.  This IOLoop should be used in the construction of
+    HTTP clients/servers, etc.  If the code being tested requires a
+    global IOLoop, subclasses should override get_new_ioloop to return it.
+
+    The IOLoop's start and stop methods should not be called directly.
+    Instead, use self.stop self.wait.  Arguments passed to self.stop are
+    returned from self.wait.  It is possible to have multiple
+    wait/stop cycles in the same test.
+
+    Example::
+
+        # This test uses an asynchronous style similar to most async
+        # application code.
+        class MyTestCase(AsyncTestCase):
+            def test_http_fetch(self):
+                client = AsyncHTTPClient(self.io_loop)
+                client.fetch("http://www.tornadoweb.org/", self.handle_fetch)
+                self.wait()
+
+            def handle_fetch(self, response):
+                # Test contents of response (failures and exceptions here
+                # will cause self.wait() to throw an exception and end the
+                # test).
+                # Exceptions thrown here are magically propagated to
+                # self.wait() in test_http_fetch() via stack_context.
+                self.assertIn("FriendFeed", response.body)
+                self.stop()
+
+        # This test uses the argument passing between self.stop and self.wait
+        # for a simpler, more synchronous style.
+        # This style is recommended over the preceding example because it
+        # keeps the assertions in the test method itself, and is therefore
+        # less sensitive to the subtleties of stack_context.
+        class MyTestCase2(AsyncTestCase):
+            def test_http_fetch(self):
+                client = AsyncHTTPClient(self.io_loop)
+                client.fetch("http://www.tornadoweb.org/", self.stop)
+                response = self.wait()
+                # Test contents of response
+                self.assertIn("FriendFeed", response.body)
+    """
+    def __init__(self, *args, **kwargs):
+        super(AsyncTestCase, self).__init__(*args, **kwargs)
+        self.__stopped = False
+        self.__running = False
+        self.__failure = None
+        self.__stop_args = None
+
+    def setUp(self):
+        super(AsyncTestCase, self).setUp()
+        self.io_loop = self.get_new_ioloop()
+
+    def tearDown(self):
+        if (not IOLoop.initialized() or
+            self.io_loop is not IOLoop.instance()):
+            # Try to clean up any file descriptors left open in the ioloop.
+            # This avoids leaks, especially when tests are run repeatedly
+            # in the same process with autoreload (because curl does not
+            # set FD_CLOEXEC on its file descriptors)
+            self.io_loop.close(all_fds=True)
+        super(AsyncTestCase, self).tearDown()
+
+    def get_new_ioloop(self):
+        '''Creates a new IOLoop for this test.  May be overridden in
+        subclasses for tests that require a specific IOLoop (usually
+        the singleton).
+        '''
+        return IOLoop()
+
+    @contextlib.contextmanager
+    def _stack_context(self):
+        try:
+            yield
+        except Exception:
+            self.__failure = sys.exc_info()
+            self.stop()
+
+    def run(self, result=None):
+        with StackContext(self._stack_context):
+            super(AsyncTestCase, self).run(result)
+
+    def stop(self, _arg=None, **kwargs):
+        '''Stops the ioloop, causing one pending (or future) call to wait()
+        to return.
+
+        Keyword arguments or a single positional argument passed to stop() are
+        saved and will be returned by wait().
+        '''
+        assert _arg is None or not kwargs
+        self.__stop_args = kwargs or _arg
+        if self.__running:
+            self.io_loop.stop()
+            self.__running = False
+        self.__stopped = True
+
+    def wait(self, condition=None, timeout=5):
+        """Runs the IOLoop until stop is called or timeout has passed.
+
+        In the event of a timeout, an exception will be thrown.
+
+        If condition is not None, the IOLoop will be restarted after stop()
+        until condition() returns true.
+        """
+        if not self.__stopped:
+            if timeout:
+                def timeout_func():
+                    try:
+                        raise self.failureException(
+                          'Async operation timed out after %d seconds' %
+                          timeout)
+                    except Exception:
+                        self.__failure = sys.exc_info()
+                    self.stop()
+                self.io_loop.add_timeout(time.time() + timeout, timeout_func)
+            while True:
+                self.__running = True
+                with NullContext():
+                    # Wipe out the StackContext that was established in
+                    # self.run() so that all callbacks executed inside the
+                    # IOLoop will re-run it.
+                    self.io_loop.start()
+                if (self.__failure is not None or
+                    condition is None or condition()):
+                    break
+        assert self.__stopped
+        self.__stopped = False
+        if self.__failure is not None:
+            # 2to3 isn't smart enough to convert three-argument raise
+            # statements correctly in some cases.
+            if isinstance(self.__failure[1], self.__failure[0]):
+                raise self.__failure[1], None, self.__failure[2]
+            else:
+                raise self.__failure[0], self.__failure[1], self.__failure[2]
+        result = self.__stop_args
+        self.__stop_args = None
+        return result
+
+
+class AsyncHTTPTestCase(AsyncTestCase):
+    '''A test case that starts up an HTTP server.
+
+    Subclasses must override get_app(), which returns the
+    tornado.web.Application (or other HTTPServer callback) to be tested.
+    Tests will typically use the provided self.http_client to fetch
+    URLs from this server.
+
+    Example::
+
+        class MyHTTPTest(AsyncHTTPTestCase):
+            def get_app(self):
+                return Application([('/', MyHandler)...])
+
+            def test_homepage(self):
+                # The following two lines are equivalent to
+                #   response = self.fetch('/')
+                # but are shown in full here to demonstrate explicit use
+                # of self.stop and self.wait.
+                self.http_client.fetch(self.get_url('/'), self.stop)
+                response = self.wait()
+                # test contents of response
+    '''
+    def setUp(self):
+        super(AsyncHTTPTestCase, self).setUp()
+        self.__port = None
+
+        self.http_client = AsyncHTTPClient(io_loop=self.io_loop)
+        self._app = self.get_app()
+        self.http_server = HTTPServer(self._app, io_loop=self.io_loop,
+                                      **self.get_httpserver_options())
+        self.http_server.listen(self.get_http_port(), address="127.0.0.1")
+
+    def get_app(self):
+        """Should be overridden by subclasses to return a
+        tornado.web.Application or other HTTPServer callback.
+        """
+        raise NotImplementedError()
+
+    def fetch(self, path, **kwargs):
+        """Convenience method to synchronously fetch a url.
+
+        The given path will be appended to the local server's host and port.
+        Any additional kwargs will be passed directly to
+        AsyncHTTPClient.fetch (and so could be used to pass method="POST",
+        body="...", etc).
+        """
+        self.http_client.fetch(self.get_url(path), self.stop, **kwargs)
+        return self.wait()
+
+    def get_httpserver_options(self):
+        """May be overridden by subclasses to return additional
+        keyword arguments for HTTPServer.
+        """
+        return {}
+
+    def get_http_port(self):
+        """Returns the port used by the HTTPServer.
+
+        A new port is chosen for each test.
+        """
+        if self.__port is None:
+            self.__port = get_unused_port()
+        return self.__port
+
+    def get_url(self, path):
+        """Returns an absolute url for the given path on the test server."""
+        return 'http://localhost:%s%s' % (self.get_http_port(), path)
+
+    def tearDown(self):
+        self.http_server.stop()
+        self.http_client.close()
+        super(AsyncHTTPTestCase, self).tearDown()
+
+class LogTrapTestCase(unittest.TestCase):
+    """A test case that captures and discards all logging output
+    if the test passes.
+
+    Some libraries can produce a lot of logging output even when
+    the test succeeds, so this class can be useful to minimize the noise.
+    Simply use it as a base class for your test case.  It is safe to combine
+    with AsyncTestCase via multiple inheritance
+    ("class MyTestCase(AsyncHTTPTestCase, LogTrapTestCase):")
+
+    This class assumes that only one log handler is configured and that
+    it is a StreamHandler.  This is true for both logging.basicConfig
+    and the "pretty logging" configured by tornado.options.
+    """
+    def run(self, result=None):
+        logger = logging.getLogger()
+        if len(logger.handlers) > 1:
+            # Multiple handlers have been defined.  It gets messy to handle
+            # this, especially since the handlers may have different
+            # formatters.  Just leave the logging alone in this case.
+            super(LogTrapTestCase, self).run(result)
+            return
+        if not logger.handlers:
+            logging.basicConfig()
+        self.assertEqual(len(logger.handlers), 1)
+        handler = logger.handlers[0]
+        assert isinstance(handler, logging.StreamHandler)
+        old_stream = handler.stream
+        try:
+            handler.stream = StringIO()
+            logging.info("RUNNING TEST: " + str(self))
+            old_error_count = len(result.failures) + len(result.errors)
+            super(LogTrapTestCase, self).run(result)
+            new_error_count = len(result.failures) + len(result.errors)
+            if new_error_count != old_error_count:
+                old_stream.write(handler.stream.getvalue())
+        finally:
+            handler.stream = old_stream
+
+def main():
+    """A simple test runner.
+
+    This test runner is essentially equivalent to `unittest.main` from
+    the standard library, but adds support for tornado-style option
+    parsing and log formatting.
+
+    The easiest way to run a test is via the command line::
+
+        python -m tornado.testing tornado.test.stack_context_test
+
+    See the standard library unittest module for ways in which tests can
+    be specified.
+
+    Projects with many tests may wish to define a test script like
+    tornado/test/runtests.py.  This script should define a method all()
+    which returns a test suite and then call tornado.testing.main().
+    Note that even when a test script is used, the all() test suite may
+    be overridden by naming a single test on the command line::
+
+        # Runs all tests
+        tornado/test/runtests.py
+        # Runs one test
+        tornado/test/runtests.py tornado.test.stack_context_test
+
+    """
+    from tornado.options import define, options, parse_command_line
+
+    define('autoreload', type=bool, default=False,
+           help="DEPRECATED: use tornado.autoreload.main instead")
+    define('httpclient', type=str, default=None)
+    define('exception_on_interrupt', type=bool, default=True,
+           help=("If true (default), ctrl-c raises a KeyboardInterrupt "
+                 "exception.  This prints a stack trace but cannot interrupt "
+                 "certain operations.  If false, the process is more reliably "
+                 "killed, but does not print a stack trace."))
+    argv = [sys.argv[0]] + parse_command_line(sys.argv)
+
+    if options.httpclient:
+        from tornado.httpclient import AsyncHTTPClient
+        AsyncHTTPClient.configure(options.httpclient)
+
+    if not options.exception_on_interrupt:
+        signal.signal(signal.SIGINT, signal.SIG_DFL)
+
+    if __name__ == '__main__' and len(argv) == 1:
+        print >> sys.stderr, "No tests specified"
+        sys.exit(1)
+    try:
+        # In order to be able to run tests by their fully-qualified name
+        # on the command line without importing all tests here,
+        # module must be set to None.  Python 3.2's unittest.main ignores
+        # defaultTest if no module is given (it tries to do its own
+        # test discovery, which is incompatible with auto2to3), so don't
+        # set module if we're not asking for a specific test.
+        if len(argv) > 1:
+            unittest.main(module=None, argv=argv)
+        else:
+            unittest.main(defaultTest="all", argv=argv)
+    except SystemExit, e:
+        if e.code == 0:
+            logging.info('PASS')
+        else:
+            logging.error('FAIL')
+        if not options.autoreload:
+            raise
+    if options.autoreload:
+        import tornado.autoreload
+        tornado.autoreload.wait()
+
+if __name__ == '__main__':
+    main()
diff --git a/tornado/util.py b/tornado/util.py
new file mode 100644
index 0000000..6752401
--- /dev/null
+++ b/tornado/util.py
@@ -0,0 +1,47 @@
+"""Miscellaneous utility functions."""
+
+class ObjectDict(dict):
+    """Makes a dictionary behave like an object."""
+    def __getattr__(self, name):
+        try:
+            return self[name]
+        except KeyError:
+            raise AttributeError(name)
+
+    def __setattr__(self, name, value):
+        self[name] = value
+
+
+def import_object(name):
+    """Imports an object by name.
+
+    import_object('x.y.z') is equivalent to 'from x.y import z'.
+
+    >>> import tornado.escape
+    >>> import_object('tornado.escape') is tornado.escape
+    True
+    >>> import_object('tornado.escape.utf8') is tornado.escape.utf8
+    True
+    """
+    parts = name.split('.')
+    obj = __import__('.'.join(parts[:-1]), None, None, [parts[-1]], 0)
+    return getattr(obj, parts[-1])
+
+# Fake byte literal support:  In python 2.6+, you can say b"foo" to get
+# a byte literal (str in 2.x, bytes in 3.x).  There's no way to do this
+# in a way that supports 2.5, though, so we need a function wrapper
+# to convert our string literals.  b() should only be applied to literal
+# latin1 strings.  Once we drop support for 2.5, we can remove this function
+# and just use byte literals.
+if str is unicode:
+    def b(s):
+        return s.encode('latin1')
+    bytes_type = bytes
+else:
+    def b(s):
+        return s
+    bytes_type = str
+
+def doctests():
+    import doctest
+    return doctest.DocTestSuite()
diff --git a/tornado/web.py b/tornado/web.py
new file mode 100644
index 0000000..c31eb67
--- /dev/null
+++ b/tornado/web.py
@@ -0,0 +1,1985 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""
+The Tornado web framework looks a bit like web.py (http://webpy.org/) or
+Google's webapp (http://code.google.com/appengine/docs/python/tools/webapp/),
+but with additional tools and optimizations to take advantage of the
+Tornado non-blocking web server and tools.
+
+Here is the canonical "Hello, world" example app::
+
+    import tornado.ioloop
+    import tornado.web
+
+    class MainHandler(tornado.web.RequestHandler):
+        def get(self):
+            self.write("Hello, world")
+
+    if __name__ == "__main__":
+        application = tornado.web.Application([
+            (r"/", MainHandler),
+        ])
+        application.listen(8888)
+        tornado.ioloop.IOLoop.instance().start()
+
+See the Tornado walkthrough on http://tornadoweb.org for more details
+and a good getting started guide.
+
+Thread-safety notes
+-------------------
+
+In general, methods on RequestHandler and elsewhere in tornado are not
+thread-safe.  In particular, methods such as write(), finish(), and
+flush() must only be called from the main thread.  If you use multiple
+threads it is important to use IOLoop.add_callback to transfer control
+back to the main thread before finishing the request.
+"""
+
+from __future__ import with_statement
+
+import Cookie
+import base64
+import binascii
+import calendar
+import datetime
+import email.utils
+import functools
+import gzip
+import hashlib
+import hmac
+import httplib
+import itertools
+import logging
+import mimetypes
+import os.path
+import re
+import stat
+import sys
+import threading
+import time
+import tornado
+import traceback
+import types
+import urllib
+import urlparse
+import uuid
+
+from tornado import escape
+from tornado import locale
+from tornado import stack_context
+from tornado import template
+from tornado.escape import utf8, _unicode
+from tornado.util import b, bytes_type, import_object, ObjectDict
+
+try:
+    from io import BytesIO  # python 3
+except ImportError:
+    from cStringIO import StringIO as BytesIO  # python 2
+
+class RequestHandler(object):
+    """Subclass this class and define get() or post() to make a handler.
+
+    If you want to support more methods than the standard GET/HEAD/POST, you
+    should override the class variable SUPPORTED_METHODS in your
+    RequestHandler class.
+    """
+    SUPPORTED_METHODS = ("GET", "HEAD", "POST", "DELETE", "PUT", "OPTIONS")
+
+    _template_loaders = {}  # {path: template.BaseLoader}
+    _template_loader_lock = threading.Lock()
+
+    def __init__(self, application, request, **kwargs):
+        self.application = application
+        self.request = request
+        self._headers_written = False
+        self._finished = False
+        self._auto_finish = True
+        self._transforms = None  # will be set in _execute
+        self.ui = ObjectDict((n, self._ui_method(m)) for n, m in
+                     application.ui_methods.iteritems())
+        # UIModules are available as both `modules` and `_modules` in the
+        # template namespace.  Historically only `modules` was available
+        # but could be clobbered by user additions to the namespace.
+        # The template {% module %} directive looks in `_modules` to avoid
+        # possible conflicts.
+        self.ui["_modules"] = ObjectDict((n, self._ui_module(n, m)) for n, m in
+                                 application.ui_modules.iteritems())
+        self.ui["modules"] = self.ui["_modules"]
+        self.clear()
+        # Check since connection is not available in WSGI
+        if hasattr(self.request, "connection"):
+            self.request.connection.stream.set_close_callback(
+                self.on_connection_close)
+        self.initialize(**kwargs)
+
+    def initialize(self):
+        """Hook for subclass initialization.
+
+        A dictionary passed as the third argument of a url spec will be
+        supplied as keyword arguments to initialize().
+
+        Example::
+
+            class ProfileHandler(RequestHandler):
+                def initialize(self, database):
+                    self.database = database
+
+                def get(self, username):
+                    ...
+
+            app = Application([
+                (r'/user/(.*)', ProfileHandler, dict(database=database)),
+                ])
+        """
+        pass
+
+    @property
+    def settings(self):
+        """An alias for `self.application.settings`."""
+        return self.application.settings
+
+    def head(self, *args, **kwargs):
+        raise HTTPError(405)
+
+    def get(self, *args, **kwargs):
+        raise HTTPError(405)
+
+    def post(self, *args, **kwargs):
+        raise HTTPError(405)
+
+    def delete(self, *args, **kwargs):
+        raise HTTPError(405)
+
+    def put(self, *args, **kwargs):
+        raise HTTPError(405)
+
+    def options(self, *args, **kwargs):
+        raise HTTPError(405)
+
+    def prepare(self):
+        """Called at the beginning of a request before `get`/`post`/etc.
+
+        Override this method to perform common initialization regardless
+        of the request method.
+        """
+        pass
+
+    def on_finish(self):
+        """Called after the end of a request.
+
+        Override this method to perform cleanup, logging, etc.
+        This method is a counterpart to `prepare`.  ``on_finish`` may
+        not produce any output, as it is called after the response
+        has been sent to the client.
+        """
+        pass
+
+    def on_connection_close(self):
+        """Called in async handlers if the client closed the connection.
+
+        Override this to clean up resources associated with
+        long-lived connections.  Note that this method is called only if
+        the connection was closed during asynchronous processing; if you
+        need to do cleanup after every request override `on_finish`
+        instead.
+
+        Proxies may keep a connection open for a time (perhaps
+        indefinitely) after the client has gone away, so this method
+        may not be called promptly after the end user closes their
+        connection.
+        """
+        pass
+
+    def clear(self):
+        """Resets all headers and content for this response."""
+        # The performance cost of tornado.httputil.HTTPHeaders is significant
+        # (slowing down a benchmark with a trivial handler by more than 10%),
+        # and its case-normalization is not generally necessary for 
+        # headers we generate on the server side, so use a plain dict
+        # and list instead.
+        self._headers = {
+            "Server": "TornadoServer/%s" % tornado.version,
+            "Content-Type": "text/html; charset=UTF-8",
+        }
+        self._list_headers = []
+        self.set_default_headers()
+        if not self.request.supports_http_1_1():
+            if self.request.headers.get("Connection") == "Keep-Alive":
+                self.set_header("Connection", "Keep-Alive")
+        self._write_buffer = []
+        self._status_code = 200
+
+    def set_default_headers(self):
+        """Override this to set HTTP headers at the beginning of the request.
+
+        For example, this is the place to set a custom ``Server`` header.
+        Note that setting such headers in the normal flow of request
+        processing may not do what you want, since headers may be reset
+        during error handling.
+        """
+        pass
+
+    def set_status(self, status_code):
+        """Sets the status code for our response."""
+        assert status_code in httplib.responses
+        self._status_code = status_code
+
+    def get_status(self):
+        """Returns the status code for our response."""
+        return self._status_code
+
+    def set_header(self, name, value):
+        """Sets the given response header name and value.
+
+        If a datetime is given, we automatically format it according to the
+        HTTP specification. If the value is not a string, we convert it to
+        a string. All header values are then encoded as UTF-8.
+        """
+        self._headers[name] = self._convert_header_value(value)
+
+    def add_header(self, name, value):
+        """Adds the given response header and value.
+
+        Unlike `set_header`, `add_header` may be called multiple times
+        to return multiple values for the same header.
+        """
+        self._list_headers.append((name, self._convert_header_value(value)))
+
+    def _convert_header_value(self, value):
+        if isinstance(value, bytes_type):
+            pass
+        elif isinstance(value, unicode):
+            value = value.encode('utf-8')
+        elif isinstance(value, (int, long)):
+            # return immediately since we know the converted value will be safe
+            return str(value)
+        elif isinstance(value, datetime.datetime):
+            t = calendar.timegm(value.utctimetuple())
+            return email.utils.formatdate(t, localtime=False, usegmt=True)
+        else:
+            raise TypeError("Unsupported header value %r" % value)
+        # If \n is allowed into the header, it is possible to inject
+        # additional headers or split the request. Also cap length to
+        # prevent obviously erroneous values.
+        if len(value) > 4000 or re.match(b(r"[\x00-\x1f]"), value):
+            raise ValueError("Unsafe header value %r", value)
+        return value
+
+
+    _ARG_DEFAULT = []
+    def get_argument(self, name, default=_ARG_DEFAULT, strip=True):
+        """Returns the value of the argument with the given name.
+
+        If default is not provided, the argument is considered to be
+        required, and we throw an HTTP 400 exception if it is missing.
+
+        If the argument appears in the url more than once, we return the
+        last value.
+
+        The returned value is always unicode.
+        """
+        args = self.get_arguments(name, strip=strip)
+        if not args:
+            if default is self._ARG_DEFAULT:
+                raise HTTPError(400, "Missing argument %s" % name)
+            return default
+        return args[-1]
+
+    def get_arguments(self, name, strip=True):
+        """Returns a list of the arguments with the given name.
+
+        If the argument is not present, returns an empty list.
+
+        The returned values are always unicode.
+        """
+        values = []
+        for v in self.request.arguments.get(name, []):
+            v = self.decode_argument(v, name=name)
+            if isinstance(v, unicode):
+                # Get rid of any weird control chars (unless decoding gave
+                # us bytes, in which case leave it alone)
+                v = re.sub(r"[\x00-\x08\x0e-\x1f]", " ", v)
+            if strip:
+                v = v.strip()
+            values.append(v)
+        return values
+
+    def decode_argument(self, value, name=None):
+        """Decodes an argument from the request.
+
+        The argument has been percent-decoded and is now a byte string.
+        By default, this method decodes the argument as utf-8 and returns
+        a unicode string, but this may be overridden in subclasses.
+
+        This method is used as a filter for both get_argument() and for
+        values extracted from the url and passed to get()/post()/etc.
+
+        The name of the argument is provided if known, but may be None
+        (e.g. for unnamed groups in the url regex).
+        """
+        return _unicode(value)
+
+    @property
+    def cookies(self):
+        return self.request.cookies
+
+    def get_cookie(self, name, default=None):
+        """Gets the value of the cookie with the given name, else default."""
+        if self.request.cookies is not None and name in self.request.cookies:
+            return self.request.cookies[name].value
+        return default
+
+    def set_cookie(self, name, value, domain=None, expires=None, path="/",
+                   expires_days=None, **kwargs):
+        """Sets the given cookie name/value with the given options.
+
+        Additional keyword arguments are set on the Cookie.Morsel
+        directly.
+        See http://docs.python.org/library/cookie.html#morsel-objects
+        for available attributes.
+        """
+        # The cookie library only accepts type str, in both python 2 and 3
+        name = escape.native_str(name)
+        value = escape.native_str(value)
+        if re.search(r"[\x00-\x20]", name + value):
+            # Don't let us accidentally inject bad stuff
+            raise ValueError("Invalid cookie %r: %r" % (name, value))
+        if not hasattr(self, "_new_cookies"):
+            self._new_cookies = []
+        new_cookie = Cookie.SimpleCookie()
+        self._new_cookies.append(new_cookie)
+        new_cookie[name] = value
+        if domain:
+            new_cookie[name]["domain"] = domain
+        if expires_days is not None and not expires:
+            expires = datetime.datetime.utcnow() + datetime.timedelta(
+                days=expires_days)
+        if expires:
+            timestamp = calendar.timegm(expires.utctimetuple())
+            new_cookie[name]["expires"] = email.utils.formatdate(
+                timestamp, localtime=False, usegmt=True)
+        if path:
+            new_cookie[name]["path"] = path
+        for k, v in kwargs.iteritems():
+            if k == 'max_age': k = 'max-age'
+            new_cookie[name][k] = v
+
+    def clear_cookie(self, name, path="/", domain=None):
+        """Deletes the cookie with the given name."""
+        expires = datetime.datetime.utcnow() - datetime.timedelta(days=365)
+        self.set_cookie(name, value="", path=path, expires=expires,
+                        domain=domain)
+
+    def clear_all_cookies(self):
+        """Deletes all the cookies the user sent with this request."""
+        for name in self.request.cookies.iterkeys():
+            self.clear_cookie(name)
+
+    def set_secure_cookie(self, name, value, expires_days=30, **kwargs):
+        """Signs and timestamps a cookie so it cannot be forged.
+
+        You must specify the ``cookie_secret`` setting in your Application
+        to use this method. It should be a long, random sequence of bytes
+        to be used as the HMAC secret for the signature.
+
+        To read a cookie set with this method, use `get_secure_cookie()`.
+
+        Note that the ``expires_days`` parameter sets the lifetime of the
+        cookie in the browser, but is independent of the ``max_age_days``
+        parameter to `get_secure_cookie`.
+        """
+        self.set_cookie(name, self.create_signed_value(name, value),
+                        expires_days=expires_days, **kwargs)
+
+    def create_signed_value(self, name, value):
+        """Signs and timestamps a string so it cannot be forged.
+
+        Normally used via set_secure_cookie, but provided as a separate
+        method for non-cookie uses.  To decode a value not stored
+        as a cookie use the optional value argument to get_secure_cookie.
+        """
+        self.require_setting("cookie_secret", "secure cookies")
+        return create_signed_value(self.application.settings["cookie_secret"],
+                                   name, value)
+
+    def get_secure_cookie(self, name, value=None, max_age_days=31):
+        """Returns the given signed cookie if it validates, or None."""
+        self.require_setting("cookie_secret", "secure cookies")
+        if value is None: value = self.get_cookie(name)
+        return decode_signed_value(self.application.settings["cookie_secret"],
+                                   name, value, max_age_days=max_age_days)
+
+    def redirect(self, url, permanent=False, status=None):
+        """Sends a redirect to the given (optionally relative) URL.
+
+        If the ``status`` argument is specified, that value is used as the
+        HTTP status code; otherwise either 301 (permanent) or 302
+        (temporary) is chosen based on the ``permanent`` argument.
+        The default is 302 (temporary).
+        """
+        if self._headers_written:
+            raise Exception("Cannot redirect after headers have been written")
+        if status is None:
+            status = 301 if permanent else 302
+        else:
+            assert isinstance(status, int) and 300 <= status <= 399
+        self.set_status(status)
+        # Remove whitespace
+        url = re.sub(b(r"[\x00-\x20]+"), "", utf8(url))
+        self.set_header("Location", urlparse.urljoin(utf8(self.request.uri),
+                                                     url))
+        self.finish()
+
+    def write(self, chunk):
+        """Writes the given chunk to the output buffer.
+
+        To write the output to the network, use the flush() method below.
+
+        If the given chunk is a dictionary, we write it as JSON and set
+        the Content-Type of the response to be application/json.
+        (if you want to send JSON as a different Content-Type, call
+        set_header *after* calling write()).
+
+        Note that lists are not converted to JSON because of a potential
+        cross-site security vulnerability.  All JSON output should be
+        wrapped in a dictionary.  More details at
+        http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx
+        """
+        if self._finished:
+            raise RuntimeError("Cannot write() after finish().  May be caused "
+                               "by using async operations without the "
+                               "@asynchronous decorator.")
+        if isinstance(chunk, dict):
+            chunk = escape.json_encode(chunk)
+            self.set_header("Content-Type", "application/json; charset=UTF-8")
+        chunk = utf8(chunk)
+        self._write_buffer.append(chunk)
+
+    def render(self, template_name, **kwargs):
+        """Renders the template with the given arguments as the response."""
+        html = self.render_string(template_name, **kwargs)
+
+        # Insert the additional JS and CSS added by the modules on the page
+        js_embed = []
+        js_files = []
+        css_embed = []
+        css_files = []
+        html_heads = []
+        html_bodies = []
+        for module in getattr(self, "_active_modules", {}).itervalues():
+            embed_part = module.embedded_javascript()
+            if embed_part: js_embed.append(utf8(embed_part))
+            file_part = module.javascript_files()
+            if file_part:
+                if isinstance(file_part, (unicode, bytes_type)):
+                    js_files.append(file_part)
+                else:
+                    js_files.extend(file_part)
+            embed_part = module.embedded_css()
+            if embed_part: css_embed.append(utf8(embed_part))
+            file_part = module.css_files()
+            if file_part:
+                if isinstance(file_part, (unicode, bytes_type)):
+                    css_files.append(file_part)
+                else:
+                    css_files.extend(file_part)
+            head_part = module.html_head()
+            if head_part: html_heads.append(utf8(head_part))
+            body_part = module.html_body()
+            if body_part: html_bodies.append(utf8(body_part))
+        def is_absolute(path):
+            return any(path.startswith(x) for x in ["/", "http:", "https:"])
+        if js_files:
+            # Maintain order of JavaScript files given by modules
+            paths = []
+            unique_paths = set()
+            for path in js_files:
+                if not is_absolute(path):
+                    path = self.static_url(path)
+                if path not in unique_paths:
+                    paths.append(path)
+                    unique_paths.add(path)
+            js = ''.join('<script src="' + escape.xhtml_escape(p) +
+                         '" type="text/javascript"></script>'
+                         for p in paths)
+            sloc = html.rindex(b('</body>'))
+            html = html[:sloc] + utf8(js) + b('\n') + html[sloc:]
+        if js_embed:
+            js = b('<script type="text/javascript">\n//<![CDATA[\n') + \
+                b('\n').join(js_embed) + b('\n//]]>\n</script>')
+            sloc = html.rindex(b('</body>'))
+            html = html[:sloc] + js + b('\n') + html[sloc:]
+        if css_files:
+            paths = []
+            unique_paths = set()
+            for path in css_files:
+                if not is_absolute(path):
+                    path = self.static_url(path)
+                if path not in unique_paths:
+                    paths.append(path)
+                    unique_paths.add(path)
+            css = ''.join('<link href="' + escape.xhtml_escape(p) + '" '
+                          'type="text/css" rel="stylesheet"/>'
+                          for p in paths)
+            hloc = html.index(b('</head>'))
+            html = html[:hloc] + utf8(css) + b('\n') + html[hloc:]
+        if css_embed:
+            css = b('<style type="text/css">\n') + b('\n').join(css_embed) + \
+                b('\n</style>')
+            hloc = html.index(b('</head>'))
+            html = html[:hloc] + css + b('\n') + html[hloc:]
+        if html_heads:
+            hloc = html.index(b('</head>'))
+            html = html[:hloc] + b('').join(html_heads) + b('\n') + html[hloc:]
+        if html_bodies:
+            hloc = html.index(b('</body>'))
+            html = html[:hloc] + b('').join(html_bodies) + b('\n') + html[hloc:]
+        self.finish(html)
+
+    def render_string(self, template_name, **kwargs):
+        """Generate the given template with the given arguments.
+
+        We return the generated string. To generate and write a template
+        as a response, use render() above.
+        """
+        # If no template_path is specified, use the path of the calling file
+        template_path = self.get_template_path()
+        if not template_path:
+            frame = sys._getframe(0)
+            web_file = frame.f_code.co_filename
+            while frame.f_code.co_filename == web_file:
+                frame = frame.f_back
+            template_path = os.path.dirname(frame.f_code.co_filename)
+        with RequestHandler._template_loader_lock:
+            if template_path not in RequestHandler._template_loaders:
+                loader = self.create_template_loader(template_path)
+                RequestHandler._template_loaders[template_path] = loader
+            else:
+                loader = RequestHandler._template_loaders[template_path]
+        t = loader.load(template_name)
+        args = dict(
+            handler=self,
+            request=self.request,
+            current_user=self.current_user,
+            locale=self.locale,
+            _=self.locale.translate,
+            static_url=self.static_url,
+            xsrf_form_html=self.xsrf_form_html,
+            reverse_url=self.application.reverse_url
+        )
+        args.update(self.ui)
+        args.update(kwargs)
+        return t.generate(**args)
+
+    def create_template_loader(self, template_path):
+        settings = self.application.settings
+        if "template_loader" in settings:
+            return settings["template_loader"]
+        kwargs = {}
+        if "autoescape" in settings:
+            # autoescape=None means "no escaping", so we have to be sure
+            # to only pass this kwarg if the user asked for it.
+            kwargs["autoescape"] = settings["autoescape"]
+        return template.Loader(template_path, **kwargs)
+
+
+    def flush(self, include_footers=False, callback=None):
+        """Flushes the current output buffer to the network.
+        
+        The ``callback`` argument, if given, can be used for flow control:
+        it will be run when all flushed data has been written to the socket.
+        Note that only one flush callback can be outstanding at a time;
+        if another flush occurs before the previous flush's callback
+        has been run, the previous callback will be discarded.
+        """
+        if self.application._wsgi:
+            raise Exception("WSGI applications do not support flush()")
+
+        chunk = b("").join(self._write_buffer)
+        self._write_buffer = []
+        if not self._headers_written:
+            self._headers_written = True
+            for transform in self._transforms:
+                self._headers, chunk = transform.transform_first_chunk(
+                    self._headers, chunk, include_footers)
+            headers = self._generate_headers()
+        else:
+            for transform in self._transforms:
+                chunk = transform.transform_chunk(chunk, include_footers)
+            headers = b("")
+
+        # Ignore the chunk and only write the headers for HEAD requests
+        if self.request.method == "HEAD":
+            if headers: self.request.write(headers, callback=callback)
+            return
+
+        if headers or chunk:
+            self.request.write(headers + chunk, callback=callback)
+
+    def finish(self, chunk=None):
+        """Finishes this response, ending the HTTP request."""
+        if self._finished:
+            raise RuntimeError("finish() called twice.  May be caused "
+                               "by using async operations without the "
+                               "@asynchronous decorator.")
+
+        if chunk is not None: self.write(chunk)
+
+        # Automatically support ETags and add the Content-Length header if
+        # we have not flushed any content yet.
+        if not self._headers_written:
+            if (self._status_code == 200 and
+                self.request.method in ("GET", "HEAD") and
+                "Etag" not in self._headers):
+                etag = self.compute_etag()
+                if etag is not None:
+                    inm = self.request.headers.get("If-None-Match")
+                    if inm and inm.find(etag) != -1:
+                        self._write_buffer = []
+                        self.set_status(304)
+                    else:
+                        self.set_header("Etag", etag)
+            if "Content-Length" not in self._headers:
+                content_length = sum(len(part) for part in self._write_buffer)
+                self.set_header("Content-Length", content_length)
+
+        if hasattr(self.request, "connection"):
+            # Now that the request is finished, clear the callback we
+            # set on the IOStream (which would otherwise prevent the
+            # garbage collection of the RequestHandler when there
+            # are keepalive connections)
+            self.request.connection.stream.set_close_callback(None)
+
+        if not self.application._wsgi:
+            self.flush(include_footers=True)
+            self.request.finish()
+            self._log()
+        self._finished = True
+        self.on_finish()
+
+    def send_error(self, status_code=500, **kwargs):
+        """Sends the given HTTP error code to the browser.
+
+        If `flush()` has already been called, it is not possible to send
+        an error, so this method will simply terminate the response.
+        If output has been written but not yet flushed, it will be discarded
+        and replaced with the error page.
+
+        Override `write_error()` to customize the error page that is returned.
+        Additional keyword arguments are passed through to `write_error`.
+        """
+        if self._headers_written:
+            logging.error("Cannot send error response after headers written")
+            if not self._finished:
+                self.finish()
+            return
+        self.clear()
+        self.set_status(status_code)
+        try:
+            self.write_error(status_code, **kwargs)
+        except Exception:
+            logging.error("Uncaught exception in write_error", exc_info=True)
+        if not self._finished:
+            self.finish()
+
+    def write_error(self, status_code, **kwargs):
+        """Override to implement custom error pages.
+
+        ``write_error`` may call `write`, `render`, `set_header`, etc
+        to produce output as usual.
+
+        If this error was caused by an uncaught exception, an ``exc_info``
+        triple will be available as ``kwargs["exc_info"]``.  Note that this
+        exception may not be the "current" exception for purposes of
+        methods like ``sys.exc_info()`` or ``traceback.format_exc``.
+
+        For historical reasons, if a method ``get_error_html`` exists,
+        it will be used instead of the default ``write_error`` implementation.
+        ``get_error_html`` returned a string instead of producing output
+        normally, and had different semantics for exception handling.
+        Users of ``get_error_html`` are encouraged to convert their code
+        to override ``write_error`` instead.
+        """
+        if hasattr(self, 'get_error_html'):
+            if 'exc_info' in kwargs:
+                exc_info = kwargs.pop('exc_info')
+                kwargs['exception'] = exc_info[1]
+                try:
+                    # Put the traceback into sys.exc_info()
+                    raise exc_info[0], exc_info[1], exc_info[2]
+                except Exception:
+                    self.finish(self.get_error_html(status_code, **kwargs))
+            else:
+                self.finish(self.get_error_html(status_code, **kwargs))
+            return
+        if self.settings.get("debug") and "exc_info" in kwargs:
+            # in debug mode, try to send a traceback
+            self.set_header('Content-Type', 'text/plain')
+            for line in traceback.format_exception(*kwargs["exc_info"]):
+                self.write(line)
+            self.finish()
+        else:
+            self.finish("<html><title>%(code)d: %(message)s</title>" 
+                        "<body>%(code)d: %(message)s</body></html>" % {
+                    "code": status_code,
+                    "message": httplib.responses[status_code],
+                    })
+
+    @property
+    def locale(self):
+        """The local for the current session.
+
+        Determined by either get_user_locale, which you can override to
+        set the locale based on, e.g., a user preference stored in a
+        database, or get_browser_locale, which uses the Accept-Language
+        header.
+        """
+        if not hasattr(self, "_locale"):
+            self._locale = self.get_user_locale()
+            if not self._locale:
+                self._locale = self.get_browser_locale()
+                assert self._locale
+        return self._locale
+
+    def get_user_locale(self):
+        """Override to determine the locale from the authenticated user.
+
+        If None is returned, we fall back to get_browser_locale().
+
+        This method should return a tornado.locale.Locale object,
+        most likely obtained via a call like tornado.locale.get("en")
+        """
+        return None
+
+    def get_browser_locale(self, default="en_US"):
+        """Determines the user's locale from Accept-Language header.
+
+        See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
+        """
+        if "Accept-Language" in self.request.headers:
+            languages = self.request.headers["Accept-Language"].split(",")
+            locales = []
+            for language in languages:
+                parts = language.strip().split(";")
+                if len(parts) > 1 and parts[1].startswith("q="):
+                    try:
+                        score = float(parts[1][2:])
+                    except (ValueError, TypeError):
+                        score = 0.0
+                else:
+                    score = 1.0
+                locales.append((parts[0], score))
+            if locales:
+                locales.sort(key=lambda (l, s): s, reverse=True)
+                codes = [l[0] for l in locales]
+                return locale.get(*codes)
+        return locale.get(default)
+
+    @property
+    def current_user(self):
+        """The authenticated user for this request.
+
+        Determined by either get_current_user, which you can override to
+        set the user based on, e.g., a cookie. If that method is not
+        overridden, this method always returns None.
+
+        We lazy-load the current user the first time this method is called
+        and cache the result after that.
+        """
+        if not hasattr(self, "_current_user"):
+            self._current_user = self.get_current_user()
+        return self._current_user
+
+    def get_current_user(self):
+        """Override to determine the current user from, e.g., a cookie."""
+        return None
+
+    def get_login_url(self):
+        """Override to customize the login URL based on the request.
+
+        By default, we use the 'login_url' application setting.
+        """
+        self.require_setting("login_url", "@tornado.web.authenticated")
+        return self.application.settings["login_url"]
+
+    def get_template_path(self):
+        """Override to customize template path for each handler.
+
+        By default, we use the 'template_path' application setting.
+        Return None to load templates relative to the calling file.
+        """
+        return self.application.settings.get("template_path")
+
+    @property
+    def xsrf_token(self):
+        """The XSRF-prevention token for the current user/session.
+
+        To prevent cross-site request forgery, we set an '_xsrf' cookie
+        and include the same '_xsrf' value as an argument with all POST
+        requests. If the two do not match, we reject the form submission
+        as a potential forgery.
+
+        See http://en.wikipedia.org/wiki/Cross-site_request_forgery
+        """
+        if not hasattr(self, "_xsrf_token"):
+            token = self.get_cookie("_xsrf")
+            if not token:
+                token = binascii.b2a_hex(uuid.uuid4().bytes)
+                expires_days = 30 if self.current_user else None
+                self.set_cookie("_xsrf", token, expires_days=expires_days)
+            self._xsrf_token = token
+        return self._xsrf_token
+
+    def check_xsrf_cookie(self):
+        """Verifies that the '_xsrf' cookie matches the '_xsrf' argument.
+
+        To prevent cross-site request forgery, we set an '_xsrf'
+        cookie and include the same value as a non-cookie
+        field with all POST requests. If the two do not match, we
+        reject the form submission as a potential forgery.
+
+        The _xsrf value may be set as either a form field named _xsrf
+        or in a custom HTTP header named X-XSRFToken or X-CSRFToken
+        (the latter is accepted for compatibility with Django).
+
+        See http://en.wikipedia.org/wiki/Cross-site_request_forgery
+
+        Prior to release 1.1.1, this check was ignored if the HTTP header
+        "X-Requested-With: XMLHTTPRequest" was present.  This exception
+        has been shown to be insecure and has been removed.  For more
+        information please see
+        http://www.djangoproject.com/weblog/2011/feb/08/security/
+        http://weblog.rubyonrails.org/2011/2/8/csrf-protection-bypass-in-ruby-on-rails
+        """
+        token = (self.get_argument("_xsrf", None) or
+                 self.request.headers.get("X-Xsrftoken") or
+                 self.request.headers.get("X-Csrftoken"))
+        if not token:
+            raise HTTPError(403, "'_xsrf' argument missing from POST")
+        if self.xsrf_token != token:
+            raise HTTPError(403, "XSRF cookie does not match POST argument")
+
+    def xsrf_form_html(self):
+        """An HTML <input/> element to be included with all POST forms.
+
+        It defines the _xsrf input value, which we check on all POST
+        requests to prevent cross-site request forgery. If you have set
+        the 'xsrf_cookies' application setting, you must include this
+        HTML within all of your HTML forms.
+
+        See check_xsrf_cookie() above for more information.
+        """
+        return '<input type="hidden" name="_xsrf" value="' + \
+            escape.xhtml_escape(self.xsrf_token) + '"/>'
+
+    def static_url(self, path, include_host=None):
+        """Returns a static URL for the given relative static file path.
+
+        This method requires you set the 'static_path' setting in your
+        application (which specifies the root directory of your static
+        files).
+
+        We append ?v=<signature> to the returned URL, which makes our
+        static file handler set an infinite expiration header on the
+        returned content. The signature is based on the content of the
+        file.
+
+        By default this method returns URLs relative to the current
+        host, but if ``include_host`` is true the URL returned will be
+        absolute.  If this handler has an ``include_host`` attribute,
+        that value will be used as the default for all `static_url`
+        calls that do not pass ``include_host`` as a keyword argument.
+        """
+        self.require_setting("static_path", "static_url")
+        static_handler_class = self.settings.get(
+            "static_handler_class", StaticFileHandler)
+
+        if include_host is None:
+            include_host = getattr(self, "include_host", False)
+
+        if include_host:
+            base = self.request.protocol + "://" + self.request.host
+        else:
+            base = ""
+        return base + static_handler_class.make_static_url(self.settings, path)
+
+    def async_callback(self, callback, *args, **kwargs):
+        """Obsolete - catches exceptions from the wrapped function.
+
+        This function is unnecessary since Tornado 1.1.
+        """
+        if callback is None:
+            return None
+        if args or kwargs:
+            callback = functools.partial(callback, *args, **kwargs)
+        def wrapper(*args, **kwargs):
+            try:
+                return callback(*args, **kwargs)
+            except Exception, e:
+                if self._headers_written:
+                    logging.error("Exception after headers written",
+                                  exc_info=True)
+                else:
+                    self._handle_request_exception(e)
+        return wrapper
+
+    def require_setting(self, name, feature="this feature"):
+        """Raises an exception if the given app setting is not defined."""
+        if not self.application.settings.get(name):
+            raise Exception("You must define the '%s' setting in your "
+                            "application to use %s" % (name, feature))
+
+    def reverse_url(self, name, *args):
+        """Alias for `Application.reverse_url`."""
+        return self.application.reverse_url(name, *args)
+
+    def compute_etag(self):
+        """Computes the etag header to be used for this request.
+
+        May be overridden to provide custom etag implementations,
+        or may return None to disable tornado's default etag support.
+        """
+        hasher = hashlib.sha1()
+        for part in self._write_buffer:
+            hasher.update(part)
+        return '"%s"' % hasher.hexdigest()
+
+    def _stack_context_handle_exception(self, type, value, traceback):
+        try:
+            # For historical reasons _handle_request_exception only takes
+            # the exception value instead of the full triple,
+            # so re-raise the exception to ensure that it's in
+            # sys.exc_info()
+            raise type, value, traceback
+        except Exception:
+            self._handle_request_exception(value)
+        return True
+
+    def _execute(self, transforms, *args, **kwargs):
+        """Executes this request with the given output transforms."""
+        self._transforms = transforms
+        try:
+            if self.request.method not in self.SUPPORTED_METHODS:
+                raise HTTPError(405)
+            # If XSRF cookies are turned on, reject form submissions without
+            # the proper cookie
+            if self.request.method not in ("GET", "HEAD", "OPTIONS") and \
+               self.application.settings.get("xsrf_cookies"):
+                self.check_xsrf_cookie()
+            self.prepare()
+            if not self._finished:
+                args = [self.decode_argument(arg) for arg in args]
+                kwargs = dict((k, self.decode_argument(v, name=k))
+                              for (k,v) in kwargs.iteritems())
+                getattr(self, self.request.method.lower())(*args, **kwargs)
+                if self._auto_finish and not self._finished:
+                    self.finish()
+        except Exception, e:
+            self._handle_request_exception(e)
+
+    def _generate_headers(self):
+        lines = [utf8(self.request.version + " " +
+                      str(self._status_code) +
+                      " " + httplib.responses[self._status_code])]
+        lines.extend([(utf8(n) + b(": ") + utf8(v)) for n, v in 
+                      itertools.chain(self._headers.iteritems(), self._list_headers)])
+        for cookie_dict in getattr(self, "_new_cookies", []):
+            for cookie in cookie_dict.values():
+                lines.append(utf8("Set-Cookie: " + cookie.OutputString(None)))
+        return b("\r\n").join(lines) + b("\r\n\r\n")
+
+    def _log(self):
+        """Logs the current request.
+
+        Sort of deprecated since this functionality was moved to the
+        Application, but left in place for the benefit of existing apps
+        that have overridden this method.
+        """
+        self.application.log_request(self)
+
+    def _request_summary(self):
+        return self.request.method + " " + self.request.uri + \
+            " (" + self.request.remote_ip + ")"
+
+    def _handle_request_exception(self, e):
+        if isinstance(e, HTTPError):
+            if e.log_message:
+                format = "%d %s: " + e.log_message
+                args = [e.status_code, self._request_summary()] + list(e.args)
+                logging.warning(format, *args)
+            if e.status_code not in httplib.responses:
+                logging.error("Bad HTTP status code: %d", e.status_code)
+                self.send_error(500, exc_info=sys.exc_info())
+            else:
+                self.send_error(e.status_code, exc_info=sys.exc_info())
+        else:
+            logging.error("Uncaught exception %s\n%r", self._request_summary(),
+                          self.request, exc_info=True)
+            self.send_error(500, exc_info=sys.exc_info())
+
+    def _ui_module(self, name, module):
+        def render(*args, **kwargs):
+            if not hasattr(self, "_active_modules"):
+                self._active_modules = {}
+            if name not in self._active_modules:
+                self._active_modules[name] = module(self)
+            rendered = self._active_modules[name].render(*args, **kwargs)
+            return rendered
+        return render
+
+    def _ui_method(self, method):
+        return lambda *args, **kwargs: method(self, *args, **kwargs)
+
+
+def asynchronous(method):
+    """Wrap request handler methods with this if they are asynchronous.
+
+    If this decorator is given, the response is not finished when the
+    method returns. It is up to the request handler to call self.finish()
+    to finish the HTTP request. Without this decorator, the request is
+    automatically finished when the get() or post() method returns. ::
+
+       class MyRequestHandler(web.RequestHandler):
+           @web.asynchronous
+           def get(self):
+              http = httpclient.AsyncHTTPClient()
+              http.fetch("http://friendfeed.com/", self._on_download)
+
+           def _on_download(self, response):
+              self.write("Downloaded!")
+              self.finish()
+
+    """
+    @functools.wraps(method)
+    def wrapper(self, *args, **kwargs):
+        if self.application._wsgi:
+            raise Exception("@asynchronous is not supported for WSGI apps")
+        self._auto_finish = False
+        with stack_context.ExceptionStackContext(
+            self._stack_context_handle_exception):
+            return method(self, *args, **kwargs)
+    return wrapper
+
+
+def removeslash(method):
+    """Use this decorator to remove trailing slashes from the request path.
+
+    For example, a request to ``'/foo/'`` would redirect to ``'/foo'`` with this
+    decorator. Your request handler mapping should use a regular expression
+    like ``r'/foo/*'`` in conjunction with using the decorator.
+    """
+    @functools.wraps(method)
+    def wrapper(self, *args, **kwargs):
+        if self.request.path.endswith("/"):
+            if self.request.method in ("GET", "HEAD"):
+                uri = self.request.path.rstrip("/")
+                if uri:  # don't try to redirect '/' to ''
+                    if self.request.query: uri += "?" + self.request.query
+                    self.redirect(uri)
+                    return
+            else:
+                raise HTTPError(404)
+        return method(self, *args, **kwargs)
+    return wrapper
+
+
+def addslash(method):
+    """Use this decorator to add a missing trailing slash to the request path.
+
+    For example, a request to '/foo' would redirect to '/foo/' with this
+    decorator. Your request handler mapping should use a regular expression
+    like r'/foo/?' in conjunction with using the decorator.
+    """
+    @functools.wraps(method)
+    def wrapper(self, *args, **kwargs):
+        if not self.request.path.endswith("/"):
+            if self.request.method in ("GET", "HEAD"):
+                uri = self.request.path + "/"
+                if self.request.query: uri += "?" + self.request.query
+                self.redirect(uri)
+                return
+            raise HTTPError(404)
+        return method(self, *args, **kwargs)
+    return wrapper
+
+
+class Application(object):
+    """A collection of request handlers that make up a web application.
+
+    Instances of this class are callable and can be passed directly to
+    HTTPServer to serve the application::
+
+        application = web.Application([
+            (r"/", MainPageHandler),
+        ])
+        http_server = httpserver.HTTPServer(application)
+        http_server.listen(8080)
+        ioloop.IOLoop.instance().start()
+
+    The constructor for this class takes in a list of URLSpec objects
+    or (regexp, request_class) tuples. When we receive requests, we
+    iterate over the list in order and instantiate an instance of the
+    first request class whose regexp matches the request path.
+
+    Each tuple can contain an optional third element, which should be a
+    dictionary if it is present. That dictionary is passed as keyword
+    arguments to the contructor of the handler. This pattern is used
+    for the StaticFileHandler below (note that a StaticFileHandler
+    can be installed automatically with the static_path setting described
+    below)::
+
+        application = web.Application([
+            (r"/static/(.*)", web.StaticFileHandler, {"path": "/var/www"}),
+        ])
+
+    We support virtual hosts with the add_handlers method, which takes in
+    a host regular expression as the first argument::
+
+        application.add_handlers(r"www\.myhost\.com", [
+            (r"/article/([0-9]+)", ArticleHandler),
+        ])
+
+    You can serve static files by sending the static_path setting as a
+    keyword argument. We will serve those files from the /static/ URI
+    (this is configurable with the static_url_prefix setting),
+    and we will serve /favicon.ico and /robots.txt from the same directory.
+    A custom subclass of StaticFileHandler can be specified with the
+    static_handler_class setting.
+
+    .. attribute:: settings
+
+       Additonal keyword arguments passed to the constructor are saved in the
+       `settings` dictionary, and are often referred to in documentation as
+       "application settings".
+    """
+    def __init__(self, handlers=None, default_host="", transforms=None,
+                 wsgi=False, **settings):
+        if transforms is None:
+            self.transforms = []
+            if settings.get("gzip"):
+                self.transforms.append(GZipContentEncoding)
+            self.transforms.append(ChunkedTransferEncoding)
+        else:
+            self.transforms = transforms
+        self.handlers = []
+        self.named_handlers = {}
+        self.default_host = default_host
+        self.settings = settings
+        self.ui_modules = {'linkify': _linkify,
+                           'xsrf_form_html': _xsrf_form_html,
+                           'Template': TemplateModule,
+                           }
+        self.ui_methods = {}
+        self._wsgi = wsgi
+        self._load_ui_modules(settings.get("ui_modules", {}))
+        self._load_ui_methods(settings.get("ui_methods", {}))
+        if self.settings.get("static_path"):
+            path = self.settings["static_path"]
+            handlers = list(handlers or [])
+            static_url_prefix = settings.get("static_url_prefix",
+                                             "/static/")
+            static_handler_class = settings.get("static_handler_class",
+                                                StaticFileHandler)
+            static_handler_args = settings.get("static_handler_args", {})
+            static_handler_args['path'] = path
+            for pattern in [re.escape(static_url_prefix) + r"(.*)",
+                            r"/(favicon\.ico)", r"/(robots\.txt)"]:
+                handlers.insert(0, (pattern, static_handler_class,
+                                    static_handler_args))
+        if handlers: self.add_handlers(".*$", handlers)
+
+        # Automatically reload modified modules
+        if self.settings.get("debug") and not wsgi:
+            from tornado import autoreload
+            autoreload.start()
+
+    def listen(self, port, address="", **kwargs):
+        """Starts an HTTP server for this application on the given port.
+
+        This is a convenience alias for creating an HTTPServer object
+        and calling its listen method.  Keyword arguments not
+        supported by HTTPServer.listen are passed to the HTTPServer
+        constructor.  For advanced uses (e.g. preforking), do not use
+        this method; create an HTTPServer and call its bind/start
+        methods directly.
+
+        Note that after calling this method you still need to call
+        IOLoop.instance().start() to start the server.
+        """
+        # import is here rather than top level because HTTPServer
+        # is not importable on appengine
+        from tornado.httpserver import HTTPServer
+        server = HTTPServer(self, **kwargs)
+        server.listen(port, address)
+
+    def add_handlers(self, host_pattern, host_handlers):
+        """Appends the given handlers to our handler list.
+
+        Note that host patterns are processed sequentially in the
+        order they were added, and only the first matching pattern is
+        used.  This means that all handlers for a given host must be
+        added in a single add_handlers call.
+        """
+        if not host_pattern.endswith("$"):
+            host_pattern += "$"
+        handlers = []
+        # The handlers with the wildcard host_pattern are a special
+        # case - they're added in the constructor but should have lower
+        # precedence than the more-precise handlers added later.
+        # If a wildcard handler group exists, it should always be last
+        # in the list, so insert new groups just before it.
+        if self.handlers and self.handlers[-1][0].pattern == '.*$':
+            self.handlers.insert(-1, (re.compile(host_pattern), handlers))
+        else:
+            self.handlers.append((re.compile(host_pattern), handlers))
+
+        for spec in host_handlers:
+            if type(spec) is type(()):
+                assert len(spec) in (2, 3)
+                pattern = spec[0]
+                handler = spec[1]
+
+                if isinstance(handler, str):
+                    # import the Module and instantiate the class
+                    # Must be a fully qualified name (module.ClassName)
+                    handler = import_object(handler)
+
+                if len(spec) == 3:
+                    kwargs = spec[2]
+                else:
+                    kwargs = {}
+                spec = URLSpec(pattern, handler, kwargs)
+            handlers.append(spec)
+            if spec.name:
+                if spec.name in self.named_handlers:
+                    logging.warning(
+                        "Multiple handlers named %s; replacing previous value",
+                        spec.name)
+                self.named_handlers[spec.name] = spec
+
+    def add_transform(self, transform_class):
+        """Adds the given OutputTransform to our transform list."""
+        self.transforms.append(transform_class)
+
+    def _get_host_handlers(self, request):
+        host = request.host.lower().split(':')[0]
+        for pattern, handlers in self.handlers:
+            if pattern.match(host):
+                return handlers
+        # Look for default host if not behind load balancer (for debugging)
+        if "X-Real-Ip" not in request.headers:
+            for pattern, handlers in self.handlers:
+                if pattern.match(self.default_host):
+                    return handlers
+        return None
+
+    def _load_ui_methods(self, methods):
+        if type(methods) is types.ModuleType:
+            self._load_ui_methods(dict((n, getattr(methods, n))
+                                       for n in dir(methods)))
+        elif isinstance(methods, list):
+            for m in methods: self._load_ui_methods(m)
+        else:
+            for name, fn in methods.iteritems():
+                if not name.startswith("_") and hasattr(fn, "__call__") \
+                   and name[0].lower() == name[0]:
+                    self.ui_methods[name] = fn
+
+    def _load_ui_modules(self, modules):
+        if type(modules) is types.ModuleType:
+            self._load_ui_modules(dict((n, getattr(modules, n))
+                                       for n in dir(modules)))
+        elif isinstance(modules, list):
+            for m in modules: self._load_ui_modules(m)
+        else:
+            assert isinstance(modules, dict)
+            for name, cls in modules.iteritems():
+                try:
+                    if issubclass(cls, UIModule):
+                        self.ui_modules[name] = cls
+                except TypeError:
+                    pass
+
+    def __call__(self, request):
+        """Called by HTTPServer to execute the request."""
+        transforms = [t(request) for t in self.transforms]
+        handler = None
+        args = []
+        kwargs = {}
+        handlers = self._get_host_handlers(request)
+        if not handlers:
+            handler = RedirectHandler(
+                self, request, url="http://" + self.default_host + "/")
+        else:
+            for spec in handlers:
+                match = spec.regex.match(request.path)
+                if match:
+                    handler = spec.handler_class(self, request, **spec.kwargs)
+                    if spec.regex.groups:
+                        # None-safe wrapper around url_unescape to handle
+                        # unmatched optional groups correctly
+                        def unquote(s):
+                            if s is None: return s
+                            return escape.url_unescape(s, encoding=None)
+                        # Pass matched groups to the handler.  Since
+                        # match.groups() includes both named and unnamed groups,
+                        # we want to use either groups or groupdict but not both.
+                        # Note that args are passed as bytes so the handler can
+                        # decide what encoding to use.
+
+                        if spec.regex.groupindex:
+                            kwargs = dict(
+                                (k, unquote(v))
+                                for (k, v) in match.groupdict().iteritems())
+                        else:
+                            args = [unquote(s) for s in match.groups()]
+                    break
+            if not handler:
+                handler = ErrorHandler(self, request, status_code=404)
+
+        # In debug mode, re-compile templates and reload static files on every
+        # request so you don't need to restart to see changes
+        if self.settings.get("debug"):
+            with RequestHandler._template_loader_lock:
+                for loader in RequestHandler._template_loaders.values():
+                    loader.reset()
+            StaticFileHandler.reset()
+
+        handler._execute(transforms, *args, **kwargs)
+        return handler
+
+    def reverse_url(self, name, *args):
+        """Returns a URL path for handler named `name`
+
+        The handler must be added to the application as a named URLSpec
+        """
+        if name in self.named_handlers:
+            return self.named_handlers[name].reverse(*args)
+        raise KeyError("%s not found in named urls" % name)
+
+    def log_request(self, handler):
+        """Writes a completed HTTP request to the logs.
+
+        By default writes to the python root logger.  To change
+        this behavior either subclass Application and override this method,
+        or pass a function in the application settings dictionary as
+        'log_function'.
+        """
+        if "log_function" in self.settings:
+            self.settings["log_function"](handler)
+            return
+        if handler.get_status() < 400:
+            log_method = logging.info
+        elif handler.get_status() < 500:
+            log_method = logging.warning
+        else:
+            log_method = logging.error
+        request_time = 1000.0 * handler.request.request_time()
+        log_method("%d %s %.2fms", handler.get_status(),
+                   handler._request_summary(), request_time)
+
+
+
+class HTTPError(Exception):
+    """An exception that will turn into an HTTP error response."""
+    def __init__(self, status_code, log_message=None, *args):
+        self.status_code = status_code
+        self.log_message = log_message
+        self.args = args
+
+    def __str__(self):
+        message = "HTTP %d: %s" % (
+            self.status_code, httplib.responses[self.status_code])
+        if self.log_message:
+            return message + " (" + (self.log_message % self.args) + ")"
+        else:
+            return message
+
+
+class ErrorHandler(RequestHandler):
+    """Generates an error response with status_code for all requests."""
+    def initialize(self, status_code):
+        self.set_status(status_code)
+
+    def prepare(self):
+        raise HTTPError(self._status_code)
+
+
+class RedirectHandler(RequestHandler):
+    """Redirects the client to the given URL for all GET requests.
+
+    You should provide the keyword argument "url" to the handler, e.g.::
+
+        application = web.Application([
+            (r"/oldpath", web.RedirectHandler, {"url": "/newpath"}),
+        ])
+    """
+    def initialize(self, url, permanent=True):
+        self._url = url
+        self._permanent = permanent
+
+    def get(self):
+        self.redirect(self._url, permanent=self._permanent)
+
+
+class StaticFileHandler(RequestHandler):
+    """A simple handler that can serve static content from a directory.
+
+    To map a path to this handler for a static data directory /var/www,
+    you would add a line to your application like::
+
+        application = web.Application([
+            (r"/static/(.*)", web.StaticFileHandler, {"path": "/var/www"}),
+        ])
+
+    The local root directory of the content should be passed as the "path"
+    argument to the handler.
+
+    To support aggressive browser caching, if the argument "v" is given
+    with the path, we set an infinite HTTP expiration header. So, if you
+    want browsers to cache a file indefinitely, send them to, e.g.,
+    /static/images/myimage.png?v=xxx. Override ``get_cache_time`` method for
+    more fine-grained cache control.
+    """
+    CACHE_MAX_AGE = 86400*365*10 #10 years
+
+    _static_hashes = {}
+    _lock = threading.Lock()  # protects _static_hashes
+
+    def initialize(self, path, default_filename=None):
+        self.root = os.path.abspath(path) + os.path.sep
+        self.default_filename = default_filename
+
+    @classmethod
+    def reset(cls):
+        with cls._lock:
+            cls._static_hashes = {}
+
+    def head(self, path):
+        self.get(path, include_body=False)
+
+    def get(self, path, include_body=True):
+        path = self.parse_url_path(path)
+        abspath = os.path.abspath(os.path.join(self.root, path))
+        # os.path.abspath strips a trailing /
+        # it needs to be temporarily added back for requests to root/
+        if not (abspath + os.path.sep).startswith(self.root):
+            raise HTTPError(403, "%s is not in root static directory", path)
+        if os.path.isdir(abspath) and self.default_filename is not None:
+            # need to look at the request.path here for when path is empty
+            # but there is some prefix to the path that was already
+            # trimmed by the routing
+            if not self.request.path.endswith("/"):
+                self.redirect(self.request.path + "/")
+                return
+            abspath = os.path.join(abspath, self.default_filename)
+        if not os.path.exists(abspath):
+            raise HTTPError(404)
+        if not os.path.isfile(abspath):
+            raise HTTPError(403, "%s is not a file", path)
+
+        stat_result = os.stat(abspath)
+        modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME])
+
+        self.set_header("Last-Modified", modified)
+
+        mime_type, encoding = mimetypes.guess_type(abspath)
+        if mime_type:
+            self.set_header("Content-Type", mime_type)
+
+        cache_time = self.get_cache_time(path, modified, mime_type)
+
+        if cache_time > 0:
+            self.set_header("Expires", datetime.datetime.utcnow() + \
+                                       datetime.timedelta(seconds=cache_time))
+            self.set_header("Cache-Control", "max-age=" + str(cache_time))
+        else:
+            self.set_header("Cache-Control", "public")
+
+        self.set_extra_headers(path)
+
+        # Check the If-Modified-Since, and don't send the result if the
+        # content has not been modified
+        ims_value = self.request.headers.get("If-Modified-Since")
+        if ims_value is not None:
+            date_tuple = email.utils.parsedate(ims_value)
+            if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple))
+            if if_since >= modified:
+                self.set_status(304)
+                return
+
+        with open(abspath, "rb") as file:
+            data = file.read()
+            hasher = hashlib.sha1()
+            hasher.update(data)
+            self.set_header("Etag", '"%s"' % hasher.hexdigest())
+            if include_body:
+                self.write(data)
+            else:
+                assert self.request.method == "HEAD"
+                self.set_header("Content-Length", len(data))
+
+    def set_extra_headers(self, path):
+        """For subclass to add extra headers to the response"""
+        pass
+
+    def get_cache_time(self, path, modified, mime_type):
+        """Override to customize cache control behavior.
+
+        Return a positive number of seconds to trigger aggressive caching or 0
+        to mark resource as cacheable, only.
+
+        By default returns cache expiry of 10 years for resources requested
+        with "v" argument.
+        """
+        return self.CACHE_MAX_AGE if "v" in self.request.arguments else 0
+
+    @classmethod
+    def make_static_url(cls, settings, path):
+        """Constructs a versioned url for the given path.
+
+        This method may be overridden in subclasses (but note that it is
+        a class method rather than an instance method).
+        
+        ``settings`` is the `Application.settings` dictionary.  ``path``
+        is the static path being requested.  The url returned should be
+        relative to the current host.
+        """
+        static_url_prefix = settings.get('static_url_prefix', '/static/')
+        version_hash = cls.get_version(settings, path)
+        if version_hash:
+            return static_url_prefix + path + "?v=" + version_hash
+        return static_url_prefix + path
+
+    @classmethod
+    def get_version(cls, settings, path):
+        """Generate the version string to be used in static URLs.
+
+        This method may be overridden in subclasses (but note that it
+        is a class method rather than a static method).  The default
+        implementation uses a hash of the file's contents.
+
+        ``settings`` is the `Application.settings` dictionary and ``path``
+        is the relative location of the requested asset on the filesystem.
+        The returned value should be a string, or ``None`` if no version
+        could be determined.
+        """
+        abs_path = os.path.join(settings["static_path"], path)
+        with cls._lock:
+            hashes = cls._static_hashes
+            if abs_path not in hashes:
+                try:
+                    f = open(abs_path, "rb")
+                    hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
+                    f.close()
+                except Exception:
+                    logging.error("Could not open static file %r", path)
+                    hashes[abs_path] = None
+            hsh = hashes.get(abs_path)
+            if hsh:
+                return hsh[:5]
+        return None
+
+    def parse_url_path(self, url_path):
+        """Converts a static URL path into a filesystem path.
+
+        ``url_path`` is the path component of the URL with
+        ``static_url_prefix`` removed.  The return value should be
+        filesystem path relative to ``static_path``.
+        """
+        if os.path.sep != "/":
+            url_path = url_path.replace("/", os.path.sep)
+        return url_path
+
+
+class FallbackHandler(RequestHandler):
+    """A RequestHandler that wraps another HTTP server callback.
+
+    The fallback is a callable object that accepts an HTTPRequest,
+    such as an Application or tornado.wsgi.WSGIContainer.  This is most
+    useful to use both tornado RequestHandlers and WSGI in the same server.
+    Typical usage::
+
+        wsgi_app = tornado.wsgi.WSGIContainer(
+            django.core.handlers.wsgi.WSGIHandler())
+        application = tornado.web.Application([
+            (r"/foo", FooHandler),
+            (r".*", FallbackHandler, dict(fallback=wsgi_app),
+        ])
+    """
+    def initialize(self, fallback):
+        self.fallback = fallback
+
+    def prepare(self):
+        self.fallback(self.request)
+        self._finished = True
+
+
+class OutputTransform(object):
+    """A transform modifies the result of an HTTP request (e.g., GZip encoding)
+
+    A new transform instance is created for every request. See the
+    ChunkedTransferEncoding example below if you want to implement a
+    new Transform.
+    """
+    def __init__(self, request):
+        pass
+
+    def transform_first_chunk(self, headers, chunk, finishing):
+        return headers, chunk
+
+    def transform_chunk(self, chunk, finishing):
+        return chunk
+
+
+class GZipContentEncoding(OutputTransform):
+    """Applies the gzip content encoding to the response.
+
+    See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
+    """
+    CONTENT_TYPES = set([
+        "text/plain", "text/html", "text/css", "text/xml", "application/javascript", 
+        "application/x-javascript", "application/xml", "application/atom+xml",
+        "text/javascript", "application/json", "application/xhtml+xml"])
+    MIN_LENGTH = 5
+
+    def __init__(self, request):
+        self._gzipping = request.supports_http_1_1() and \
+            "gzip" in request.headers.get("Accept-Encoding", "")
+
+    def transform_first_chunk(self, headers, chunk, finishing):
+        if self._gzipping:
+            ctype = _unicode(headers.get("Content-Type", "")).split(";")[0]
+            self._gzipping = (ctype in self.CONTENT_TYPES) and \
+                (not finishing or len(chunk) >= self.MIN_LENGTH) and \
+                (finishing or "Content-Length" not in headers) and \
+                ("Content-Encoding" not in headers)
+        if self._gzipping:
+            headers["Content-Encoding"] = "gzip"
+            self._gzip_value = BytesIO()
+            self._gzip_file = gzip.GzipFile(mode="w", fileobj=self._gzip_value)
+            chunk = self.transform_chunk(chunk, finishing)
+            if "Content-Length" in headers:
+                headers["Content-Length"] = str(len(chunk))
+        return headers, chunk
+
+    def transform_chunk(self, chunk, finishing):
+        if self._gzipping:
+            self._gzip_file.write(chunk)
+            if finishing:
+                self._gzip_file.close()
+            else:
+                self._gzip_file.flush()
+            chunk = self._gzip_value.getvalue()
+            self._gzip_value.truncate(0)
+            self._gzip_value.seek(0)
+        return chunk
+
+
+class ChunkedTransferEncoding(OutputTransform):
+    """Applies the chunked transfer encoding to the response.
+
+    See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1
+    """
+    def __init__(self, request):
+        self._chunking = request.supports_http_1_1()
+
+    def transform_first_chunk(self, headers, chunk, finishing):
+        if self._chunking:
+            # No need to chunk the output if a Content-Length is specified
+            if "Content-Length" in headers or "Transfer-Encoding" in headers:
+                self._chunking = False
+            else:
+                headers["Transfer-Encoding"] = "chunked"
+                chunk = self.transform_chunk(chunk, finishing)
+        return headers, chunk
+
+    def transform_chunk(self, block, finishing):
+        if self._chunking:
+            # Don't write out empty chunks because that means END-OF-STREAM
+            # with chunked encoding
+            if block:
+                block = utf8("%x" % len(block)) + b("\r\n") + block + b("\r\n")
+            if finishing:
+                block += b("0\r\n\r\n")
+        return block
+
+
+def authenticated(method):
+    """Decorate methods with this to require that the user be logged in."""
+    @functools.wraps(method)
+    def wrapper(self, *args, **kwargs):
+        if not self.current_user:
+            if self.request.method in ("GET", "HEAD"):
+                url = self.get_login_url()
+                if "?" not in url:
+                    if urlparse.urlsplit(url).scheme:
+                        # if login url is absolute, make next absolute too
+                        next_url = self.request.full_url()
+                    else:
+                        next_url = self.request.uri
+                    url += "?" + urllib.urlencode(dict(next=next_url))
+                self.redirect(url)
+                return
+            raise HTTPError(403)
+        return method(self, *args, **kwargs)
+    return wrapper
+
+
+class UIModule(object):
+    """A UI re-usable, modular unit on a page.
+
+    UI modules often execute additional queries, and they can include
+    additional CSS and JavaScript that will be included in the output
+    page, which is automatically inserted on page render.
+    """
+    def __init__(self, handler):
+        self.handler = handler
+        self.request = handler.request
+        self.ui = handler.ui
+        self.current_user = handler.current_user
+        self.locale = handler.locale
+
+    def render(self, *args, **kwargs):
+        """Overridden in subclasses to return this module's output."""
+        raise NotImplementedError()
+
+    def embedded_javascript(self):
+        """Returns a JavaScript string that will be embedded in the page."""
+        return None
+
+    def javascript_files(self):
+        """Returns a list of JavaScript files required by this module."""
+        return None
+
+    def embedded_css(self):
+        """Returns a CSS string that will be embedded in the page."""
+        return None
+
+    def css_files(self):
+        """Returns a list of CSS files required by this module."""
+        return None
+
+    def html_head(self):
+        """Returns a CSS string that will be put in the <head/> element"""
+        return None
+
+    def html_body(self):
+        """Returns an HTML string that will be put in the <body/> element"""
+        return None
+
+    def render_string(self, path, **kwargs):
+        """Renders a template and returns it as a string."""
+        return self.handler.render_string(path, **kwargs)
+
+class _linkify(UIModule):
+    def render(self, text, **kwargs):
+        return escape.linkify(text, **kwargs)
+
+class _xsrf_form_html(UIModule):
+    def render(self):
+        return self.handler.xsrf_form_html()
+
+class TemplateModule(UIModule):
+    """UIModule that simply renders the given template.
+
+    {% module Template("foo.html") %} is similar to {% include "foo.html" %},
+    but the module version gets its own namespace (with kwargs passed to
+    Template()) instead of inheriting the outer template's namespace.
+
+    Templates rendered through this module also get access to UIModule's
+    automatic javascript/css features.  Simply call set_resources
+    inside the template and give it keyword arguments corresponding to
+    the methods on UIModule: {{ set_resources(js_files=static_url("my.js")) }}
+    Note that these resources are output once per template file, not once
+    per instantiation of the template, so they must not depend on 
+    any arguments to the template.
+    """
+    def __init__(self, handler):
+        super(TemplateModule, self).__init__(handler)
+        # keep resources in both a list and a dict to preserve order
+        self._resource_list = []
+        self._resource_dict = {}
+
+    def render(self, path, **kwargs):
+        def set_resources(**kwargs):
+            if path not in self._resource_dict:
+                self._resource_list.append(kwargs)
+                self._resource_dict[path] = kwargs
+            else:
+                if self._resource_dict[path] != kwargs:
+                    raise ValueError("set_resources called with different "
+                                     "resources for the same template")
+            return ""
+        return self.render_string(path, set_resources=set_resources,
+                                  **kwargs)
+
+    def _get_resources(self, key):
+        return (r[key] for r in self._resource_list if key in r)
+
+    def embedded_javascript(self):
+        return "\n".join(self._get_resources("embedded_javascript"))
+
+    def javascript_files(self):
+        result = []
+        for f in self._get_resources("javascript_files"):
+            if isinstance(f, (unicode, bytes_type)):
+                result.append(f)
+            else:
+                result.extend(f)
+        return result
+
+    def embedded_css(self):
+        return "\n".join(self._get_resources("embedded_css"))
+
+    def css_files(self):
+        result = []
+        for f in self._get_resources("css_files"):
+            if isinstance(f, (unicode, bytes_type)):
+                result.append(f)
+            else:
+                result.extend(f)
+        return result
+
+    def html_head(self):
+        return "".join(self._get_resources("html_head"))
+
+    def html_body(self):
+        return "".join(self._get_resources("html_body"))
+
+
+
+class URLSpec(object):
+    """Specifies mappings between URLs and handlers."""
+    def __init__(self, pattern, handler_class, kwargs={}, name=None):
+        """Creates a URLSpec.
+
+        Parameters:
+
+        pattern: Regular expression to be matched.  Any groups in the regex
+            will be passed in to the handler's get/post/etc methods as
+            arguments.
+
+        handler_class: RequestHandler subclass to be invoked.
+
+        kwargs (optional): A dictionary of additional arguments to be passed
+            to the handler's constructor.
+
+        name (optional): A name for this handler.  Used by
+            Application.reverse_url.
+        """
+        if not pattern.endswith('$'):
+            pattern += '$'
+        self.regex = re.compile(pattern)
+        assert len(self.regex.groupindex) in (0, self.regex.groups), \
+            ("groups in url regexes must either be all named or all "
+             "positional: %r" % self.regex.pattern)
+        self.handler_class = handler_class
+        self.kwargs = kwargs
+        self.name = name
+        self._path, self._group_count = self._find_groups()
+
+    def _find_groups(self):
+        """Returns a tuple (reverse string, group count) for a url.
+
+        For example: Given the url pattern /([0-9]{4})/([a-z-]+)/, this method
+        would return ('/%s/%s/', 2).
+        """
+        pattern = self.regex.pattern
+        if pattern.startswith('^'):
+            pattern = pattern[1:]
+        if pattern.endswith('$'):
+            pattern = pattern[:-1]
+
+        if self.regex.groups != pattern.count('('):
+            # The pattern is too complicated for our simplistic matching,
+            # so we can't support reversing it.
+            return (None, None)
+
+        pieces = []
+        for fragment in pattern.split('('):
+            if ')' in fragment:
+                paren_loc = fragment.index(')')
+                if paren_loc >= 0:
+                    pieces.append('%s' + fragment[paren_loc + 1:])
+            else:
+                pieces.append(fragment)
+
+        return (''.join(pieces), self.regex.groups)
+
+    def reverse(self, *args):
+        assert self._path is not None, \
+            "Cannot reverse url regex " + self.regex.pattern
+        assert len(args) == self._group_count, "required number of arguments "\
+            "not found"
+        if not len(args):
+            return self._path
+        return self._path % tuple([str(a) for a in args])
+
+url = URLSpec
+
+
+def _time_independent_equals(a, b):
+    if len(a) != len(b):
+        return False
+    result = 0
+    if type(a[0]) is int:  # python3 byte strings
+        for x, y in zip(a,b):
+            result |= x ^ y
+    else:  # python2
+        for x, y in zip(a, b):
+            result |= ord(x) ^ ord(y)
+    return result == 0
+
+def create_signed_value(secret, name, value):
+    timestamp = utf8(str(int(time.time())))
+    value = base64.b64encode(utf8(value))
+    signature = _create_signature(secret, name, value, timestamp)
+    value = b("|").join([value, timestamp, signature])
+    return value
+
+def decode_signed_value(secret, name, value, max_age_days=31):
+    if not value: return None
+    parts = utf8(value).split(b("|"))
+    if len(parts) != 3: return None
+    signature = _create_signature(secret, name, parts[0], parts[1])
+    if not _time_independent_equals(parts[2], signature):
+        logging.warning("Invalid cookie signature %r", value)
+        return None
+    timestamp = int(parts[1])
+    if timestamp < time.time() - max_age_days * 86400:
+        logging.warning("Expired cookie %r", value)
+        return None
+    if timestamp > time.time() + 31 * 86400:
+        # _cookie_signature does not hash a delimiter between the
+        # parts of the cookie, so an attacker could transfer trailing
+        # digits from the payload to the timestamp without altering the
+        # signature.  For backwards compatibility, sanity-check timestamp
+        # here instead of modifying _cookie_signature.
+        logging.warning("Cookie timestamp in future; possible tampering %r", value)
+        return None
+    if parts[1].startswith(b("0")):
+        logging.warning("Tampered cookie %r", value)
+    try:
+        return base64.b64decode(parts[0])
+    except Exception:
+        return None
+
+def _create_signature(secret, *parts):
+    hash = hmac.new(utf8(secret), digestmod=hashlib.sha1)
+    for part in parts: hash.update(utf8(part))
+    return utf8(hash.hexdigest())
diff --git a/tornado/websocket.py b/tornado/websocket.py
new file mode 100644
index 0000000..8aa7777
--- /dev/null
+++ b/tornado/websocket.py
@@ -0,0 +1,650 @@
+"""Server-side implementation of the WebSocket protocol.
+
+`WebSockets <http://dev.w3.org/html5/websockets/>`_ allow for bidirectional
+communication between the browser and server.
+
+.. warning::
+
+   The WebSocket protocol was recently finalized as `RFC 6455
+   <http://tools.ietf.org/html/rfc6455>`_ and is not yet supported in
+   all browsers.  Refer to http://caniuse.com/websockets for details
+   on compatibility.  In addition, during development the protocol
+   went through several incompatible versions, and some browsers only
+   support older versions.  By default this module only supports the
+   latest version of the protocol, but optional support for an older
+   version (known as "draft 76" or "hixie-76") can be enabled by
+   overriding `WebSocketHandler.allow_draft76` (see that method's
+   documentation for caveats).
+"""
+# Author: Jacob Kristhammar, 2010
+
+import array
+import functools
+import hashlib
+import logging
+import struct
+import time
+import base64
+import tornado.escape
+import tornado.web
+
+from tornado.util import bytes_type, b
+
+class WebSocketHandler(tornado.web.RequestHandler):
+    """Subclass this class to create a basic WebSocket handler.
+
+    Override on_message to handle incoming messages. You can also override
+    open and on_close to handle opened and closed connections.
+
+    See http://dev.w3.org/html5/websockets/ for details on the
+    JavaScript interface.  The protocol is specified at
+    http://tools.ietf.org/html/rfc6455.
+
+    Here is an example Web Socket handler that echos back all received messages
+    back to the client::
+
+      class EchoWebSocket(websocket.WebSocketHandler):
+          def open(self):
+              print "WebSocket opened"
+
+          def on_message(self, message):
+              self.write_message(u"You said: " + message)
+
+          def on_close(self):
+              print "WebSocket closed"
+
+    Web Sockets are not standard HTTP connections. The "handshake" is HTTP,
+    but after the handshake, the protocol is message-based. Consequently,
+    most of the Tornado HTTP facilities are not available in handlers of this
+    type. The only communication methods available to you are write_message()
+    and close(). Likewise, your request handler class should
+    implement open() method rather than get() or post().
+
+    If you map the handler above to "/websocket" in your application, you can
+    invoke it in JavaScript with::
+
+      var ws = new WebSocket("ws://localhost:8888/websocket");
+      ws.onopen = function() {
+         ws.send("Hello, world");
+      };
+      ws.onmessage = function (evt) {
+         alert(evt.data);
+      };
+
+    This script pops up an alert box that says "You said: Hello, world".
+    """
+    def __init__(self, application, request, **kwargs):
+        tornado.web.RequestHandler.__init__(self, application, request,
+                                            **kwargs)
+        self.stream = request.connection.stream
+        self.ws_connection = None
+
+    def _execute(self, transforms, *args, **kwargs):
+        self.open_args = args
+        self.open_kwargs = kwargs
+
+        # Websocket only supports GET method
+        if self.request.method != 'GET':
+            self.stream.write(tornado.escape.utf8(
+                "HTTP/1.1 405 Method Not Allowed\r\n\r\n"
+            ))
+            self.stream.close()
+            return
+
+        # Upgrade header should be present and should be equal to WebSocket
+        if self.request.headers.get("Upgrade", "").lower() != 'websocket':
+            self.stream.write(tornado.escape.utf8(
+                "HTTP/1.1 400 Bad Request\r\n\r\n"
+                "Can \"Upgrade\" only to \"WebSocket\"."
+            ))
+            self.stream.close()
+            return
+
+        # Connection header should be upgrade. Some proxy servers/load balancers
+        # might mess with it.
+        headers = self.request.headers
+        connection = map(lambda s: s.strip().lower(), headers.get("Connection", "").split(","))
+        if 'upgrade' not in connection:
+            self.stream.write(tornado.escape.utf8(
+                "HTTP/1.1 400 Bad Request\r\n\r\n"
+                "\"Connection\" must be \"Upgrade\"."
+            ))
+            self.stream.close()
+            return
+
+        # The difference between version 8 and 13 is that in 8 the
+        # client sends a "Sec-Websocket-Origin" header and in 13 it's
+        # simply "Origin".
+        if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
+            self.ws_connection = WebSocketProtocol13(self)
+            self.ws_connection.accept_connection()
+        elif (self.allow_draft76() and
+              "Sec-WebSocket-Version" not in self.request.headers):
+            self.ws_connection = WebSocketProtocol76(self)
+            self.ws_connection.accept_connection()
+        else:
+            self.stream.write(tornado.escape.utf8(
+                "HTTP/1.1 426 Upgrade Required\r\n"
+                "Sec-WebSocket-Version: 8\r\n\r\n"))
+            self.stream.close()
+
+    def write_message(self, message, binary=False):
+        """Sends the given message to the client of this Web Socket.
+
+        The message may be either a string or a dict (which will be
+        encoded as json).  If the ``binary`` argument is false, the
+        message will be sent as utf8; in binary mode any byte string
+        is allowed.
+        """
+        if isinstance(message, dict):
+            message = tornado.escape.json_encode(message)
+        self.ws_connection.write_message(message, binary=binary)
+
+    def select_subprotocol(self, subprotocols):
+        """Invoked when a new WebSocket requests specific subprotocols.
+
+        ``subprotocols`` is a list of strings identifying the
+        subprotocols proposed by the client.  This method may be
+        overridden to return one of those strings to select it, or
+        ``None`` to not select a subprotocol.  Failure to select a
+        subprotocol does not automatically abort the connection,
+        although clients may close the connection if none of their
+        proposed subprotocols was selected.
+        """
+        return None
+
+    def open(self):
+        """Invoked when a new WebSocket is opened.
+
+        The arguments to `open` are extracted from the `tornado.web.URLSpec`
+        regular expression, just like the arguments to
+        `tornado.web.RequestHandler.get`.
+        """
+        pass
+
+    def on_message(self, message):
+        """Handle incoming messages on the WebSocket
+
+        This method must be overridden.
+        """
+        raise NotImplementedError
+
+    def on_close(self):
+        """Invoked when the WebSocket is closed."""
+        pass
+
+    def close(self):
+        """Closes this Web Socket.
+
+        Once the close handshake is successful the socket will be closed.
+        """
+        self.ws_connection.close()
+
+    def allow_draft76(self):
+        """Override to enable support for the older "draft76" protocol.
+
+        The draft76 version of the websocket protocol is disabled by
+        default due to security concerns, but it can be enabled by
+        overriding this method to return True.
+
+        Connections using the draft76 protocol do not support the
+        ``binary=True`` flag to `write_message`.
+
+        Support for the draft76 protocol is deprecated and will be
+        removed in a future version of Tornado.
+        """
+        return False
+
+    def get_websocket_scheme(self):
+        """Return the url scheme used for this request, either "ws" or "wss".
+
+        This is normally decided by HTTPServer, but applications
+        may wish to override this if they are using an SSL proxy
+        that does not provide the X-Scheme header as understood
+        by HTTPServer.
+        
+        Note that this is only used by the draft76 protocol.
+        """
+        return "wss" if self.request.protocol == "https" else "ws"
+
+    def async_callback(self, callback, *args, **kwargs):
+        """Wrap callbacks with this if they are used on asynchronous requests.
+
+        Catches exceptions properly and closes this WebSocket if an exception
+        is uncaught.  (Note that this is usually unnecessary thanks to
+        `tornado.stack_context`)
+        """
+        return self.ws_connection.async_callback(callback, *args, **kwargs)
+
+    def _not_supported(self, *args, **kwargs):
+        raise Exception("Method not supported for Web Sockets")
+
+    def on_connection_close(self):
+        if self.ws_connection:
+            self.ws_connection.on_connection_close()
+            self.ws_connection = None
+            self.on_close()
+
+
+for method in ["write", "redirect", "set_header", "send_error", "set_cookie",
+               "set_status", "flush", "finish"]:
+    setattr(WebSocketHandler, method, WebSocketHandler._not_supported)
+
+
+class WebSocketProtocol(object):
+    """Base class for WebSocket protocol versions.
+    """
+    def __init__(self, handler):
+        self.handler = handler
+        self.request = handler.request
+        self.stream = handler.stream
+        self.client_terminated = False
+        self.server_terminated = False
+
+    def async_callback(self, callback, *args, **kwargs):
+        """Wrap callbacks with this if they are used on asynchronous requests.
+
+        Catches exceptions properly and closes this WebSocket if an exception
+        is uncaught.
+        """
+        if args or kwargs:
+            callback = functools.partial(callback, *args, **kwargs)
+        def wrapper(*args, **kwargs):
+            try:
+                return callback(*args, **kwargs)
+            except Exception:
+                logging.error("Uncaught exception in %s",
+                              self.request.path, exc_info=True)
+                self._abort()
+        return wrapper
+
+    def on_connection_close(self):
+        self._abort()
+
+    def _abort(self):
+        """Instantly aborts the WebSocket connection by closing the socket"""
+        self.client_terminated = True
+        self.server_terminated = True
+        self.stream.close()  # forcibly tear down the connection
+        self.close()  # let the subclass cleanup
+
+
+class WebSocketProtocol76(WebSocketProtocol):
+    """Implementation of the WebSockets protocol, version hixie-76.
+
+    This class provides basic functionality to process WebSockets requests as
+    specified in
+    http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
+    """
+    def __init__(self, handler):
+        WebSocketProtocol.__init__(self, handler)
+        self.challenge = None
+        self._waiting = None
+
+    def accept_connection(self):
+        try:
+            self._handle_websocket_headers()
+        except ValueError:
+            logging.debug("Malformed WebSocket request received")
+            self._abort()
+            return
+
+        scheme = self.handler.get_websocket_scheme()
+
+        # draft76 only allows a single subprotocol
+        subprotocol_header = ''
+        subprotocol = self.request.headers.get("Sec-WebSocket-Protocol", None)
+        if subprotocol:
+            selected = self.handler.select_subprotocol([subprotocol])
+            if selected:
+                assert selected == subprotocol
+                subprotocol_header = "Sec-WebSocket-Protocol: %s\r\n" % selected
+
+        # Write the initial headers before attempting to read the challenge.
+        # This is necessary when using proxies (such as HAProxy), which
+        # need to see the Upgrade headers before passing through the
+        # non-HTTP traffic that follows.
+        self.stream.write(tornado.escape.utf8(
+            "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
+            "Upgrade: WebSocket\r\n"
+            "Connection: Upgrade\r\n"
+            "Server: TornadoServer/%(version)s\r\n"
+            "Sec-WebSocket-Origin: %(origin)s\r\n"
+            "Sec-WebSocket-Location: %(scheme)s://%(host)s%(uri)s\r\n"
+            "%(subprotocol)s"
+            "\r\n" % (dict(
+                    version=tornado.version,
+                    origin=self.request.headers["Origin"],
+                    scheme=scheme,
+                    host=self.request.host,
+                    uri=self.request.uri,
+                    subprotocol=subprotocol_header))))
+        self.stream.read_bytes(8, self._handle_challenge)
+
+    def challenge_response(self, challenge):
+        """Generates the challenge response that's needed in the handshake
+
+        The challenge parameter should be the raw bytes as sent from the
+        client.
+        """
+        key_1 = self.request.headers.get("Sec-Websocket-Key1")
+        key_2 = self.request.headers.get("Sec-Websocket-Key2")
+        try:
+            part_1 = self._calculate_part(key_1)
+            part_2 = self._calculate_part(key_2)
+        except ValueError:
+            raise ValueError("Invalid Keys/Challenge")
+        return self._generate_challenge_response(part_1, part_2, challenge)
+
+    def _handle_challenge(self, challenge):
+        try:
+            challenge_response = self.challenge_response(challenge)
+        except ValueError:
+            logging.debug("Malformed key data in WebSocket request")
+            self._abort()
+            return
+        self._write_response(challenge_response)
+
+    def _write_response(self, challenge):
+        self.stream.write(challenge)
+        self.async_callback(self.handler.open)(*self.handler.open_args, **self.handler.open_kwargs)
+        self._receive_message()
+
+    def _handle_websocket_headers(self):
+        """Verifies all invariant- and required headers
+
+        If a header is missing or have an incorrect value ValueError will be
+        raised
+        """
+        fields = ("Origin", "Host", "Sec-Websocket-Key1",
+                  "Sec-Websocket-Key2")
+        if not all(map(lambda f: self.request.headers.get(f), fields)):
+            raise ValueError("Missing/Invalid WebSocket headers")
+
+    def _calculate_part(self, key):
+        """Processes the key headers and calculates their key value.
+
+        Raises ValueError when feed invalid key."""
+        number = int(''.join(c for c in key if c.isdigit()))
+        spaces = len([c for c in key if c.isspace()])
+        try:
+            key_number = number // spaces
+        except (ValueError, ZeroDivisionError):
+            raise ValueError
+        return struct.pack(">I", key_number)
+
+    def _generate_challenge_response(self, part_1, part_2, part_3):
+        m = hashlib.md5()
+        m.update(part_1)
+        m.update(part_2)
+        m.update(part_3)
+        return m.digest()
+
+    def _receive_message(self):
+        self.stream.read_bytes(1, self._on_frame_type)
+
+    def _on_frame_type(self, byte):
+        frame_type = ord(byte)
+        if frame_type == 0x00:
+            self.stream.read_until(b("\xff"), self._on_end_delimiter)
+        elif frame_type == 0xff:
+            self.stream.read_bytes(1, self._on_length_indicator)
+        else:
+            self._abort()
+
+    def _on_end_delimiter(self, frame):
+        if not self.client_terminated:
+            self.async_callback(self.handler.on_message)(
+                    frame[:-1].decode("utf-8", "replace"))
+        if not self.client_terminated:
+            self._receive_message()
+
+    def _on_length_indicator(self, byte):
+        if ord(byte) != 0x00:
+            self._abort()
+            return
+        self.client_terminated = True
+        self.close()
+
+    def write_message(self, message, binary=False):
+        """Sends the given message to the client of this Web Socket."""
+        if binary:
+            raise ValueError(
+                "Binary messages not supported by this version of websockets")
+        if isinstance(message, unicode):
+            message = message.encode("utf-8")
+        assert isinstance(message, bytes_type)
+        self.stream.write(b("\x00") + message + b("\xff"))
+
+    def close(self):
+        """Closes the WebSocket connection."""
+        if not self.server_terminated:
+            if not self.stream.closed():
+                self.stream.write("\xff\x00")
+            self.server_terminated = True
+        if self.client_terminated:
+            if self._waiting is not None:
+                self.stream.io_loop.remove_timeout(self._waiting)
+            self._waiting = None
+            self.stream.close()
+        elif self._waiting is None:
+            self._waiting = self.stream.io_loop.add_timeout(
+                time.time() + 5, self._abort)
+
+
+class WebSocketProtocol13(WebSocketProtocol):
+    """Implementation of the WebSocket protocol from RFC 6455.
+
+    This class supports versions 7 and 8 of the protocol in addition to the
+    final version 13.
+    """
+    def __init__(self, handler):
+        WebSocketProtocol.__init__(self, handler)
+        self._final_frame = False
+        self._frame_opcode = None
+        self._frame_mask = None
+        self._frame_length = None
+        self._fragmented_message_buffer = None
+        self._fragmented_message_opcode = None
+        self._waiting = None
+
+    def accept_connection(self):
+        try:
+            self._handle_websocket_headers()
+            self._accept_connection()
+        except ValueError:
+            logging.debug("Malformed WebSocket request received")
+            self._abort()
+            return
+
+    def _handle_websocket_headers(self):
+        """Verifies all invariant- and required headers
+
+        If a header is missing or have an incorrect value ValueError will be
+        raised
+        """
+        fields = ("Host", "Sec-Websocket-Key", "Sec-Websocket-Version")
+        if not all(map(lambda f: self.request.headers.get(f), fields)):
+            raise ValueError("Missing/Invalid WebSocket headers")
+
+    def _challenge_response(self):
+        sha1 = hashlib.sha1()
+        sha1.update(tornado.escape.utf8(
+                self.request.headers.get("Sec-Websocket-Key")))
+        sha1.update(b("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")) # Magic value
+        return tornado.escape.native_str(base64.b64encode(sha1.digest()))
+
+    def _accept_connection(self):
+        subprotocol_header = ''
+        subprotocols = self.request.headers.get("Sec-WebSocket-Protocol", '')
+        subprotocols = [s.strip() for s in subprotocols.split(',')]
+        if subprotocols:
+            selected = self.handler.select_subprotocol(subprotocols)
+            if selected:
+                assert selected in subprotocols
+                subprotocol_header = "Sec-WebSocket-Protocol: %s\r\n" % selected
+
+        self.stream.write(tornado.escape.utf8(
+            "HTTP/1.1 101 Switching Protocols\r\n"
+            "Upgrade: websocket\r\n"
+            "Connection: Upgrade\r\n"
+            "Sec-WebSocket-Accept: %s\r\n"
+            "%s"
+            "\r\n" % (self._challenge_response(), subprotocol_header)))
+
+        self.async_callback(self.handler.open)(*self.handler.open_args, **self.handler.open_kwargs)
+        self._receive_frame()
+
+    def _write_frame(self, fin, opcode, data):
+        if fin:
+            finbit = 0x80
+        else:
+            finbit = 0
+        frame = struct.pack("B", finbit | opcode)
+        l = len(data)
+        if l < 126:
+            frame += struct.pack("B", l)
+        elif l <= 0xFFFF:
+            frame += struct.pack("!BH", 126, l)
+        else:
+            frame += struct.pack("!BQ", 127, l)
+        frame += data
+        self.stream.write(frame)
+
+    def write_message(self, message, binary=False):
+        """Sends the given message to the client of this Web Socket."""
+        if binary:
+            opcode = 0x2
+        else:
+            opcode = 0x1
+        message = tornado.escape.utf8(message)
+        assert isinstance(message, bytes_type)
+        self._write_frame(True, opcode, message)
+
+    def _receive_frame(self):
+        self.stream.read_bytes(2, self._on_frame_start)
+
+    def _on_frame_start(self, data):
+        header, payloadlen = struct.unpack("BB", data)
+        self._final_frame = header & 0x80
+        reserved_bits = header & 0x70
+        self._frame_opcode = header & 0xf
+        self._frame_opcode_is_control = self._frame_opcode & 0x8
+        if reserved_bits:
+            # client is using as-yet-undefined extensions; abort
+            self._abort()
+            return
+        if not (payloadlen & 0x80):
+            # Unmasked frame -> abort connection
+            self._abort()
+            return
+        payloadlen = payloadlen & 0x7f
+        if self._frame_opcode_is_control and payloadlen >= 126:
+            # control frames must have payload < 126
+            self._abort()
+            return
+        if payloadlen < 126:
+            self._frame_length = payloadlen
+            self.stream.read_bytes(4, self._on_masking_key)
+        elif payloadlen == 126:
+            self.stream.read_bytes(2, self._on_frame_length_16)
+        elif payloadlen == 127:
+            self.stream.read_bytes(8, self._on_frame_length_64)
+
+    def _on_frame_length_16(self, data):
+        self._frame_length = struct.unpack("!H", data)[0];
+        self.stream.read_bytes(4, self._on_masking_key);
+
+    def _on_frame_length_64(self, data):
+        self._frame_length = struct.unpack("!Q", data)[0];
+        self.stream.read_bytes(4, self._on_masking_key);
+
+    def _on_masking_key(self, data):
+        self._frame_mask = array.array("B", data)
+        self.stream.read_bytes(self._frame_length, self._on_frame_data)
+
+    def _on_frame_data(self, data):
+        unmasked = array.array("B", data)
+        for i in xrange(len(data)):
+            unmasked[i] = unmasked[i] ^ self._frame_mask[i % 4]
+
+        if self._frame_opcode_is_control:
+            # control frames may be interleaved with a series of fragmented
+            # data frames, so control frames must not interact with
+            # self._fragmented_*
+            if not self._final_frame:
+                # control frames must not be fragmented
+                self._abort()
+                return
+            opcode = self._frame_opcode
+        elif self._frame_opcode == 0:  # continuation frame
+            if self._fragmented_message_buffer is None:
+                # nothing to continue
+                self._abort()
+                return
+            self._fragmented_message_buffer += unmasked
+            if self._final_frame:
+                opcode = self._fragmented_message_opcode
+                unmasked = self._fragmented_message_buffer
+                self._fragmented_message_buffer = None
+        else:  # start of new data message
+            if self._fragmented_message_buffer is not None:
+                # can't start new message until the old one is finished
+                self._abort()
+                return
+            if self._final_frame:
+                opcode = self._frame_opcode
+            else:
+                self._fragmented_message_opcode = self._frame_opcode
+                self._fragmented_message_buffer = unmasked
+
+        if self._final_frame:
+            self._handle_message(opcode, unmasked.tostring())
+
+        if not self.client_terminated:
+            self._receive_frame()
+
+
+    def _handle_message(self, opcode, data):
+        if self.client_terminated: return
+
+        if opcode == 0x1:
+            # UTF-8 data
+            try:
+                decoded = data.decode("utf-8")
+            except UnicodeDecodeError:
+                self._abort()
+                return
+            self.async_callback(self.handler.on_message)(decoded)
+        elif opcode == 0x2:
+            # Binary data
+            self.async_callback(self.handler.on_message)(data)
+        elif opcode == 0x8:
+            # Close
+            self.client_terminated = True
+            self.close()
+        elif opcode == 0x9:
+            # Ping
+            self._write_frame(True, 0xA, data)
+        elif opcode == 0xA:
+            # Pong
+            pass
+        else:
+            self._abort()
+
+    def close(self):
+        """Closes the WebSocket connection."""
+        if not self.server_terminated:
+            if not self.stream.closed():
+                self._write_frame(True, 0x8, b(""))
+            self.server_terminated = True
+        if self.client_terminated:
+            if self._waiting is not None:
+                self.stream.io_loop.remove_timeout(self._waiting)
+                self._waiting = None
+            self.stream.close()
+        elif self._waiting is None:
+            # Give the client a few seconds to complete a clean shutdown,
+            # otherwise just close the connection.
+            self._waiting = self.stream.io_loop.add_timeout(
+                time.time() + 5, self._abort)
diff --git a/tornado/wsgi.py b/tornado/wsgi.py
new file mode 100644
index 0000000..e8f878b
--- /dev/null
+++ b/tornado/wsgi.py
@@ -0,0 +1,296 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""WSGI support for the Tornado web framework.
+
+WSGI is the Python standard for web servers, and allows for interoperability
+between Tornado and other Python web frameworks and servers.  This module
+provides WSGI support in two ways:
+
+* `WSGIApplication` is a version of `tornado.web.Application` that can run 
+  inside a WSGI server.  This is useful for running a Tornado app on another
+  HTTP server, such as Google App Engine.  See the `WSGIApplication` class
+  documentation for limitations that apply.
+* `WSGIContainer` lets you run other WSGI applications and frameworks on the
+  Tornado HTTP server.  For example, with this class you can mix Django
+  and Tornado handlers in a single server.
+"""
+
+import Cookie
+import cgi
+import httplib
+import logging
+import sys
+import time
+import tornado
+import urllib
+
+from tornado import escape
+from tornado import httputil
+from tornado import web
+from tornado.escape import native_str, utf8
+from tornado.util import b
+
+try:
+    from io import BytesIO  # python 3
+except ImportError:
+    from cStringIO import StringIO as BytesIO  # python 2
+
+class WSGIApplication(web.Application):
+    """A WSGI equivalent of `tornado.web.Application`.
+
+    WSGIApplication is very similar to web.Application, except no
+    asynchronous methods are supported (since WSGI does not support
+    non-blocking requests properly). If you call self.flush() or other
+    asynchronous methods in your request handlers running in a
+    WSGIApplication, we throw an exception.
+
+    Example usage::
+
+        import tornado.web
+        import tornado.wsgi
+        import wsgiref.simple_server
+
+        class MainHandler(tornado.web.RequestHandler):
+            def get(self):
+                self.write("Hello, world")
+
+        if __name__ == "__main__":
+            application = tornado.wsgi.WSGIApplication([
+                (r"/", MainHandler),
+            ])
+            server = wsgiref.simple_server.make_server('', 8888, application)
+            server.serve_forever()
+
+    See the 'appengine' demo for an example of using this module to run
+    a Tornado app on Google AppEngine.
+
+    Since no asynchronous methods are available for WSGI applications, the
+    httpclient and auth modules are both not available for WSGI applications.
+    We support the same interface, but handlers running in a WSGIApplication
+    do not support flush() or asynchronous methods. 
+    """
+    def __init__(self, handlers=None, default_host="", **settings):
+        web.Application.__init__(self, handlers, default_host, transforms=[],
+                                 wsgi=True, **settings)
+
+    def __call__(self, environ, start_response):
+        handler = web.Application.__call__(self, HTTPRequest(environ))
+        assert handler._finished
+        status = str(handler._status_code) + " " + \
+            httplib.responses[handler._status_code]
+        headers = handler._headers.items()
+        for cookie_dict in getattr(handler, "_new_cookies", []):
+            for cookie in cookie_dict.values():
+                headers.append(("Set-Cookie", cookie.OutputString(None)))
+        start_response(status,
+                       [(native_str(k), native_str(v)) for (k,v) in headers])
+        return handler._write_buffer
+
+
+class HTTPRequest(object):
+    """Mimics `tornado.httpserver.HTTPRequest` for WSGI applications."""
+    def __init__(self, environ):
+        """Parses the given WSGI environ to construct the request."""
+        self.method = environ["REQUEST_METHOD"]
+        self.path = urllib.quote(environ.get("SCRIPT_NAME", ""))
+        self.path += urllib.quote(environ.get("PATH_INFO", ""))
+        self.uri = self.path
+        self.arguments = {}
+        self.query = environ.get("QUERY_STRING", "")
+        if self.query:
+            self.uri += "?" + self.query
+            arguments = cgi.parse_qs(self.query)
+            for name, values in arguments.iteritems():
+                values = [v for v in values if v]
+                if values: self.arguments[name] = values
+        self.version = "HTTP/1.1"
+        self.headers = httputil.HTTPHeaders()
+        if environ.get("CONTENT_TYPE"):
+            self.headers["Content-Type"] = environ["CONTENT_TYPE"]
+        if environ.get("CONTENT_LENGTH"):
+            self.headers["Content-Length"] = environ["CONTENT_LENGTH"]
+        for key in environ:
+            if key.startswith("HTTP_"):
+                self.headers[key[5:].replace("_", "-")] = environ[key]
+        if self.headers.get("Content-Length"):
+            self.body = environ["wsgi.input"].read(
+                int(self.headers["Content-Length"]))
+        else:
+            self.body = ""
+        self.protocol = environ["wsgi.url_scheme"]
+        self.remote_ip = environ.get("REMOTE_ADDR", "")
+        if environ.get("HTTP_HOST"):
+            self.host = environ["HTTP_HOST"]
+        else:
+            self.host = environ["SERVER_NAME"]
+
+        # Parse request body
+        self.files = {}
+        content_type = self.headers.get("Content-Type", "")
+        if content_type.startswith("application/x-www-form-urlencoded"):
+            for name, values in cgi.parse_qs(self.body).iteritems():
+                self.arguments.setdefault(name, []).extend(values)
+        elif content_type.startswith("multipart/form-data"):
+            if 'boundary=' in content_type:
+                boundary = content_type.split('boundary=',1)[1]
+                if boundary:
+                    httputil.parse_multipart_form_data(
+                        utf8(boundary), self.body, self.arguments, self.files)
+            else:
+                logging.warning("Invalid multipart/form-data")
+
+        self._start_time = time.time()
+        self._finish_time = None
+
+    def supports_http_1_1(self):
+        """Returns True if this request supports HTTP/1.1 semantics"""
+        return self.version == "HTTP/1.1"
+
+    @property
+    def cookies(self):
+        """A dictionary of Cookie.Morsel objects."""
+        if not hasattr(self, "_cookies"):
+            self._cookies = Cookie.SimpleCookie()
+            if "Cookie" in self.headers:
+                try:
+                    self._cookies.load(
+                        native_str(self.headers["Cookie"]))
+                except Exception:
+                    self._cookies = None
+        return self._cookies
+
+    def full_url(self):
+        """Reconstructs the full URL for this request."""
+        return self.protocol + "://" + self.host + self.uri
+
+    def request_time(self):
+        """Returns the amount of time it took for this request to execute."""
+        if self._finish_time is None:
+            return time.time() - self._start_time
+        else:
+            return self._finish_time - self._start_time
+
+
+class WSGIContainer(object):
+    r"""Makes a WSGI-compatible function runnable on Tornado's HTTP server.
+
+    Wrap a WSGI function in a WSGIContainer and pass it to HTTPServer to
+    run it. For example::
+
+        def simple_app(environ, start_response):
+            status = "200 OK"
+            response_headers = [("Content-type", "text/plain")]
+            start_response(status, response_headers)
+            return ["Hello world!\n"]
+
+        container = tornado.wsgi.WSGIContainer(simple_app)
+        http_server = tornado.httpserver.HTTPServer(container)
+        http_server.listen(8888)
+        tornado.ioloop.IOLoop.instance().start()
+
+    This class is intended to let other frameworks (Django, web.py, etc)
+    run on the Tornado HTTP server and I/O loop.
+
+    The `tornado.web.FallbackHandler` class is often useful for mixing
+    Tornado and WSGI apps in the same server.  See
+    https://github.com/bdarnell/django-tornado-demo for a complete example.
+    """
+    def __init__(self, wsgi_application):
+        self.wsgi_application = wsgi_application
+
+    def __call__(self, request):
+        data = {}
+        response = []
+        def start_response(status, response_headers, exc_info=None):
+            data["status"] = status
+            data["headers"] = response_headers
+            return response.append
+        app_response = self.wsgi_application(
+            WSGIContainer.environ(request), start_response)
+        response.extend(app_response)
+        body = b("").join(response)
+        if hasattr(app_response, "close"):
+            app_response.close()
+        if not data: raise Exception("WSGI app did not call start_response")
+
+        status_code = int(data["status"].split()[0])
+        headers = data["headers"]
+        header_set = set(k.lower() for (k,v) in headers)
+        body = escape.utf8(body)
+        if "content-length" not in header_set:
+            headers.append(("Content-Length", str(len(body))))
+        if "content-type" not in header_set:
+            headers.append(("Content-Type", "text/html; charset=UTF-8"))
+        if "server" not in header_set:
+            headers.append(("Server", "TornadoServer/%s" % tornado.version))
+
+        parts = [escape.utf8("HTTP/1.1 " + data["status"] + "\r\n")]
+        for key, value in headers:
+            parts.append(escape.utf8(key) + b(": ") + escape.utf8(value) + b("\r\n"))
+        parts.append(b("\r\n"))
+        parts.append(body)
+        request.write(b("").join(parts))
+        request.finish()
+        self._log(status_code, request)
+
+    @staticmethod
+    def environ(request):
+        """Converts a `tornado.httpserver.HTTPRequest` to a WSGI environment.
+        """
+        hostport = request.host.split(":")
+        if len(hostport) == 2:
+            host = hostport[0]
+            port = int(hostport[1])
+        else:
+            host = request.host
+            port = 443 if request.protocol == "https" else 80
+        environ = {
+            "REQUEST_METHOD": request.method,
+            "SCRIPT_NAME": "",
+            "PATH_INFO": urllib.unquote(request.path),
+            "QUERY_STRING": request.query,
+            "REMOTE_ADDR": request.remote_ip,
+            "SERVER_NAME": host,
+            "SERVER_PORT": str(port),
+            "SERVER_PROTOCOL": request.version,
+            "wsgi.version": (1, 0),
+            "wsgi.url_scheme": request.protocol,
+            "wsgi.input": BytesIO(escape.utf8(request.body)),
+            "wsgi.errors": sys.stderr,
+            "wsgi.multithread": False,
+            "wsgi.multiprocess": True,
+            "wsgi.run_once": False,
+        }
+        if "Content-Type" in request.headers:
+            environ["CONTENT_TYPE"] = request.headers.pop("Content-Type")
+        if "Content-Length" in request.headers:
+            environ["CONTENT_LENGTH"] = request.headers.pop("Content-Length")
+        for key, value in request.headers.iteritems():
+            environ["HTTP_" + key.replace("-", "_").upper()] = value
+        return environ
+
+    def _log(self, status_code, request):
+        if status_code < 400:
+            log_method = logging.info
+        elif status_code < 500:
+            log_method = logging.warning
+        else:
+            log_method = logging.error
+        request_time = 1000.0 * request.request_time()
+        summary = request.method + " " + request.uri + " (" + \
+            request.remote_ip + ")"
+        log_method("%d %s %.2fms", status_code, summary, request_time)