Add http.client transport (#10)

diff --git a/docs/reference/google.auth.exceptions.rst b/docs/reference/google.auth.exceptions.rst
new file mode 100644
index 0000000..c34e973
--- /dev/null
+++ b/docs/reference/google.auth.exceptions.rst
@@ -0,0 +1,7 @@
+google.auth.exceptions module
+=============================
+
+.. automodule:: google.auth.exceptions
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/reference/google.auth.rst b/docs/reference/google.auth.rst
index 57a16b4..0f147e9 100644
--- a/docs/reference/google.auth.rst
+++ b/docs/reference/google.auth.rst
@@ -6,11 +6,19 @@
     :undoc-members:
     :show-inheritance:
 
+Subpackages
+-----------
+
+.. toctree::
+
+    google.auth.transport
+
 Submodules
 ----------
 
 .. toctree::
 
    google.auth.crypt
+   google.auth.exceptions
    google.auth.jwt
 
diff --git a/docs/reference/google.auth.transport.rst b/docs/reference/google.auth.transport.rst
new file mode 100644
index 0000000..88b427c
--- /dev/null
+++ b/docs/reference/google.auth.transport.rst
@@ -0,0 +1,8 @@
+google.auth.transport package
+=============================
+
+.. automodule:: google.auth.transport
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
diff --git a/google/auth/transport/_http_client.py b/google/auth/transport/_http_client.py
new file mode 100644
index 0000000..e50a3b5
--- /dev/null
+++ b/google/auth/transport/_http_client.py
@@ -0,0 +1,104 @@
+# Copyright 2016 Google Inc.
+#
+# 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.
+
+"""Transport adapter for http.client, for internal use only."""
+
+import socket
+
+from six.moves import http_client
+from six.moves import urllib
+
+from google.auth import exceptions
+from google.auth import transport
+
+
+class Response(transport.Response):
+    """http.client transport request adapter.
+
+    Args:
+        response (http.client.HTTPResponse): The raw http client response.
+    """
+    def __init__(self, response):
+        self._status = response.status
+        self._headers = {
+            key.lower(): value for key, value in response.getheaders()}
+        self._data = response.read()
+
+    @property
+    def status(self):
+        return self._status
+
+    @property
+    def headers(self):
+        return self._headers
+
+    @property
+    def data(self):
+        return self._data
+
+
+class Request(transport.Request):
+    """http.client transport request adapter."""
+
+    def __call__(self, url, method='GET', body=None, headers=None,
+                 timeout=None, **kwargs):
+        """Make an HTTP request using http.client.
+
+        Args:
+            url (str): The URI to be requested.
+            method (str): The HTTP method to use for the request. Defaults
+                to 'GET'.
+            body (bytes): The payload / body in HTTP request.
+            headers (Mapping): Request headers.
+            timeout (Optional(int)): The number of seconds to wait for a
+                response from the server. If not specified or if None, the
+                socket global default timeout will be used.
+            kwargs: Additional arguments passed throught to the underlying
+                :meth:`~http.client.HTTPConnection.request` method.
+
+        Returns:
+            Response: The HTTP response.
+
+        Raises:
+            google.auth.exceptions.TransportError: If any exception occurred.
+        """
+        # socket._GLOBAL_DEFAULT_TIMEOUT is the default in http.client.
+        if timeout is None:
+            timeout = socket._GLOBAL_DEFAULT_TIMEOUT
+
+        # http.client doesn't allow None as the headers argument.
+        if headers is None:
+            headers = {}
+
+        # http.client needs the host and path parts specified separately.
+        parts = urllib.parse.urlsplit(url)
+        path = urllib.parse.urlunsplit(
+            ('', '', parts.path, parts.query, parts.fragment))
+
+        if parts.scheme != 'http':
+            raise exceptions.TransportError(
+                'http.client transport only supports the http scheme, {}'
+                'was specified'.format(parts.scheme))
+
+        connection = http_client.HTTPConnection(parts.netloc)
+
+        try:
+            connection.request(
+                method, path, body=body, headers=headers, **kwargs)
+            response = connection.getresponse()
+            return Response(response)
+        except (http_client.HTTPException, socket.error) as exc:
+            raise exceptions.TransportError(exc)
+        finally:
+            connection.close()
diff --git a/pylintrc b/pylintrc
index 1a47c02..1f01cfa 100644
--- a/pylintrc
+++ b/pylintrc
@@ -115,6 +115,7 @@
     wrong-import-position,
     no-name-in-module,
     locally-disabled,
+    locally-enabled,
     fixme
 
 
diff --git a/pylintrc.tests b/pylintrc.tests
index de1964f..09772c5 100644
--- a/pylintrc.tests
+++ b/pylintrc.tests
@@ -105,6 +105,7 @@
     wrong-import-position,
     no-name-in-module,
     locally-disabled,
+    locally-enabled,
     missing-docstring,
     redefined-outer-name,
     no-self-use,
diff --git a/tests/transport/__init__.py b/tests/transport/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/transport/__init__.py
diff --git a/tests/transport/compliance.py b/tests/transport/compliance.py
new file mode 100644
index 0000000..ad4e491
--- /dev/null
+++ b/tests/transport/compliance.py
@@ -0,0 +1,93 @@
+# Copyright 2016 Google Inc.
+#
+# 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.
+
+import flask
+import pytest
+from pytest_localserver.http import WSGIServer
+from six.moves import http_client
+
+from google.auth import exceptions
+
+# .invalid will never resolve, see https://tools.ietf.org/html/rfc2606
+NXDOMAIN = 'test.invalid'
+
+
+class RequestResponseTests(object):
+
+    @pytest.fixture
+    def server(self):
+        """Provides a test HTTP server.
+
+        The test server is automatically created before
+        a test and destroyed at the end. The server is serving a test
+        application that can be used to verify requests.
+        """
+        app = flask.Flask(__name__)
+        app.debug = True
+
+        # pylint: disable=unused-variable
+        # (pylint thinks the flask routes are unusued.)
+        @app.route('/basic')
+        def index():
+            header_value = flask.request.headers.get('x-test-header', 'value')
+            headers = {'X-Test-Header': header_value}
+            return 'Basic Content', http_client.OK, headers
+
+        @app.route('/server_error')
+        def server_error():
+            return 'Error', http_client.INTERNAL_SERVER_ERROR
+        # pylint: enable=unused-variable
+
+        server = WSGIServer(application=app.wsgi_app)
+        server.start()
+        yield server
+        server.stop()
+
+    def test_request_basic(self, server):
+        request = self.make_request()
+        response = request(url=server.url + '/basic', method='GET')
+
+        assert response.status == http_client.OK
+        assert response.headers['x-test-header'] == 'value'
+        assert response.data == b'Basic Content'
+
+    def test_request_timeout(self, server):
+        request = self.make_request()
+        response = request(url=server.url + '/basic', method='GET', timeout=2)
+
+        assert response.status == http_client.OK
+        assert response.headers['x-test-header'] == 'value'
+        assert response.data == b'Basic Content'
+
+    def test_request_headers(self, server):
+        request = self.make_request()
+        response = request(
+            url=server.url + '/basic', method='GET', headers={
+                'x-test-header': 'hello world'})
+
+        assert response.status == http_client.OK
+        assert response.headers['x-test-header'] == 'hello world'
+        assert response.data == b'Basic Content'
+
+    def test_request_error(self, server):
+        request = self.make_request()
+        response = request(url=server.url + '/server_error', method='GET')
+
+        assert response.status == http_client.INTERNAL_SERVER_ERROR
+        assert response.data == b'Error'
+
+    def test_connection_error(self):
+        request = self.make_request()
+        with pytest.raises(exceptions.TransportError):
+            request(url='http://{}'.format(NXDOMAIN), method='GET')
diff --git a/tests/transport/test__http_client.py b/tests/transport/test__http_client.py
new file mode 100644
index 0000000..b66e8a4
--- /dev/null
+++ b/tests/transport/test__http_client.py
@@ -0,0 +1,32 @@
+# Copyright 2016 Google Inc.
+#
+# 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.
+
+import pytest
+
+from google.auth import exceptions
+import google.auth.transport._http_client
+
+from tests.transport import compliance
+
+
+class TestRequestResponse(compliance.RequestResponseTests):
+    def make_request(self):
+        return google.auth.transport._http_client.Request()
+
+    def test_non_http(self):
+        request = self.make_request()
+        with pytest.raises(exceptions.TransportError) as excinfo:
+            request(url='https://{}'.format(compliance.NXDOMAIN), method='GET')
+
+        assert excinfo.match('https')