From 4c6866773598b9be3a40d925f376099743d5509f Mon Sep 17 00:00:00 2001
From: Timm Fitschen <t.fitschen@indiscale.com>
Date: Wed, 2 Nov 2022 15:34:24 +0100
Subject: [PATCH] WIP: http proxy

---
 setup.py                               |  7 ++-
 src/caosdb/connection/connection.py    | 66 ++++++++++++++++++++++++--
 src/caosdb/connection/streaminghttp.py | 17 +++++--
 src/caosdb/schema-pycaosdb-ini.yml     |  4 ++
 tox.ini                                |  1 -
 5 files changed, 86 insertions(+), 9 deletions(-)

diff --git a/setup.py b/setup.py
index e044d1f5..50c5b89e 100755
--- a/setup.py
+++ b/setup.py
@@ -171,7 +171,12 @@ def setup_package():
         python_requires='>=3.8',
         package_dir={'': 'src'},
         install_requires=['lxml>=4.6.3',
-                          'PyYAML>=5.4.1', 'future', 'PySocks>=1.6.7'],
+                          "requests>=2.28.1",
+                          "python-dateutil>=2.8.2",
+                          'PyYAML>=5.4.1',
+                          'future',
+                          'PySocks>=1.6.7',
+                         ],
         extras_require={'keyring': ['keyring>=13.0.0'],
                         'jsonschema': ['jsonschema>=4.4.0']},
         setup_requires=["pytest-runner>=2.0,<3dev"],
diff --git a/src/caosdb/connection/connection.py b/src/caosdb/connection/connection.py
index 43eb3410..3fff3b7d 100644
--- a/src/caosdb/connection/connection.py
+++ b/src/caosdb/connection/connection.py
@@ -31,6 +31,9 @@ import sys
 from builtins import str  # pylint: disable=redefined-builtin
 from errno import EPIPE as BrokenPipe
 from socket import error as SocketError
+from urllib.parse import urlparse
+from requests import Session as HTTPSession
+from requests.exceptions import ConnectionError as HTTPConnectionError
 
 from caosdb.configuration import get_config
 from caosdb.exceptions import (CaosDBException, HTTPClientError,
@@ -63,6 +66,32 @@ except ImportError:
 _LOGGER = logging.getLogger(__name__)
 
 
+class _WrappedHTTPResponse2(CaosDBHTTPResponse):
+
+    def __init__(self, response):
+        self.response = response
+
+    @property
+    def reason(self):
+        return self.response.reason
+
+    @property
+    def status(self):
+        return self.response.status_code
+
+    def read(self, size=None):
+        return self.response.raw.read(size)
+
+    def getheader(self, name, default=None):
+        return self.response.headers[name] if name in self.response.headers else default
+
+    def getheaders(self):
+        return self.response.headers.items()
+
+    def close(self):
+        self.response.close()
+
+
 class _WrappedHTTPResponse(CaosDBHTTPResponse):
 
     def __init__(self, response):
@@ -101,7 +130,6 @@ class _DefaultCaosDBServerConnection(CaosDBServerConnection):
     def __init__(self):
         self._useragent = ("caosdb-pylib/{version} - {implementation}".format(
             version=version, implementation=type(self).__name__))
-        self._http_con = None
         self._base_path = None
 
     def request(self, method, path, headers=None, body=None, **kwargs):
@@ -133,8 +161,25 @@ class _DefaultCaosDBServerConnection(CaosDBServerConnection):
         if headers is None:
             headers = {}
         headers["User-Agent"] = self._useragent
+
         try:
-            self._http_con = StreamingHTTPSConnection(
+            if self.setup_fields["https_proxy"] is not None:
+                session = HTTPSession()
+                session.proxies = {
+                    "https": self.setup_fields["https_proxy"]
+                }
+                response = session.request(method=method,
+                                           url=self.setup_fields["url_base_path"] + path,
+                                           headers=headers, data=body, stream=True)
+                return _WrappedHTTPResponse2(response)
+        except HTTPConnectionError as conn_err:
+            raise CaosDBConnectionError(
+                "Connection failed. Network or server down? " + str(conn_err)
+            )
+
+
+        try:
+            self._http_con = ProxyConnection(
                 # TODO looks as if configure needs to be done first.
                 # That is however not assured.
                 host=self.setup_fields["host"],
@@ -213,14 +258,24 @@ class _DefaultCaosDBServerConnection(CaosDBServerConnection):
                 "file.")
 
         socket_proxy = None
-
         if "socket_proxy" in config:
             socket_proxy = config["socket_proxy"]
 
+        https_proxy = None
+        if "https_proxy" in config:
+            result = urlparse(config["https_proxy"], scheme="http")
+            if result.scheme != "http":
+                raise ValueError(
+                    "The `https_proxy` parameter or config option must "
+                    "contain a valid http uri or None")
+            https_proxy = result.scheme + "://" + result.netloc
+
         self.setup_fields = {
+            "url_base_path": config["url"] + "/",
             "host": host,
             "timeout": int(config.get("timeout")),
             "context": context,
+            "https_proxy": https_proxy,
             "socket_proxy": socket_proxy}
 
 
@@ -342,6 +397,11 @@ def configure_connection(**kwargs):
         An authentication token which has been issued by the CaosDB Server.
         Implies `password_method="auth_token"` if set.  An example token string would be `["O","OneTimeAuthenticationToken","anonymous",["administration"],[],1592995200000,604800000,"3ZZ4WKRB-5I7DG2Q6-ZZE6T64P-VQ","197d0d081615c52dc18fb323c300d7be077beaad4020773bb58920b55023fa6ee49355e35754a4277b9ac525c882bcd3a22e7227ba36dfcbbdbf8f15f19d1ee9",1,30000]`.
 
+    https_proxy : str
+        Define a https proxy, e.g. `https://localhost:8888`. Currently,
+        authentication against the proxy and non-TLS connections are not
+        supported.  (Default: None)
+
     implementation : CaosDBServerConnection
         The class which implements the connection. (Default:
         _DefaultCaosDBServerConnection)
diff --git a/src/caosdb/connection/streaminghttp.py b/src/caosdb/connection/streaminghttp.py
index 01774301..e3442795 100644
--- a/src/caosdb/connection/streaminghttp.py
+++ b/src/caosdb/connection/streaminghttp.py
@@ -70,13 +70,22 @@ class StreamingHTTPSConnection(client.HTTPSConnection, object):
     that overrides the `send()` method to support iterable body objects."""
     # pylint: disable=unused-argument, arguments-differ
 
-    def __init__(self, socket_proxy=None, **kwargs):
+    def __init__(self, socket_proxy=None, https_proxy=None, **kwargs):
+        host = kwargs["host"]
+        port = int(kwargs["port"]) if "port" in kwargs else None
         if socket_proxy is not None:
-            host, port = socket_proxy.split(":")
-            socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, host,
-                                  int(port))
+            proxy_host, proxy_port = socket_proxy.split(":")
+            socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, proxy_host,
+                                  int(proxy_port))
             socket.socket = socks.socksocket
+        if https_proxy is not None:
+            tunnel_host = host
+            tunnel_port = port
+            host, port = https_proxy.split(":")
+            port = int(port)
         super(StreamingHTTPSConnection, self).__init__(**kwargs)
+        if tunnel_host is not None:
+            self.set_tunnel(host=host, port=port)
 
     def _send_output(self, body, **kwargs):
         """Send the currently buffered request and clear the buffer.
diff --git a/src/caosdb/schema-pycaosdb-ini.yml b/src/caosdb/schema-pycaosdb-ini.yml
index a81bf006..bd795e8a 100644
--- a/src/caosdb/schema-pycaosdb-ini.yml
+++ b/src/caosdb/schema-pycaosdb-ini.yml
@@ -55,6 +55,10 @@ schema-pycaosdb-ini:
           examples: ["localhost:12345"]
           type: string
           description: You can define a socket proxy to be used. This is for the case that the server sits behind a firewall which is being tunnelled with a socket proxy (SOCKS4 or SOCKS5) (e.g. via ssh's -D option or a dedicated proxy server).
+        https_proxy:
+          examples: ["https://localhost:8888"]
+          type: string
+          description: Define a HTTPS Proxy. Currently, authentication against the proxy and non-TLS HTTP connections are not supported.
         implementation:
           description: This option is used internally and for testing. Do not override.
           examples: [_DefaultCaosDBServerConnection]
diff --git a/tox.ini b/tox.ini
index e3218918..50c22d57 100644
--- a/tox.ini
+++ b/tox.ini
@@ -7,7 +7,6 @@ deps = .
     nose
     pytest
     pytest-cov
-    python-dateutil
     jsonschema==4.0.1
 commands=py.test --cov=caosdb -vv {posargs}
 
-- 
GitLab