1# Licensed to the Apache Software Foundation (ASF) under one or more 2# contributor license agreements. See the NOTICE file distributed with 3# this work for additional information regarding copyright ownership. 4# The ASF licenses this file to You under the Apache License, Version 2.0 5# (the "License"); you may not use this file except in compliance with 6# the License. You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16""" 17Subclass for httplib.HTTPSConnection with optional certificate name 18verification, depending on libcloud.security settings. 19""" 20 21import os 22import warnings 23import requests 24from requests.adapters import HTTPAdapter 25from requests.packages.urllib3.poolmanager import PoolManager 26 27import libcloud.security 28from libcloud.utils.py3 import urlparse, PY3 29 30 31__all__ = [ 32 'LibcloudBaseConnection', 33 'LibcloudConnection' 34] 35 36ALLOW_REDIRECTS = 1 37 38# Default timeout for HTTP requests in seconds 39DEFAULT_REQUEST_TIMEOUT = 60 40 41HTTP_PROXY_ENV_VARIABLE_NAME = 'http_proxy' 42HTTPS_PROXY_ENV_VARIABLE_NAME = 'https_proxy' 43 44 45class SignedHTTPSAdapter(HTTPAdapter): 46 def __init__(self, cert_file, key_file): 47 self.cert_file = cert_file 48 self.key_file = key_file 49 super(SignedHTTPSAdapter, self).__init__() 50 51 def init_poolmanager(self, connections, maxsize, block=False): 52 self.poolmanager = PoolManager( 53 num_pools=connections, maxsize=maxsize, 54 block=block, 55 cert_file=self.cert_file, 56 key_file=self.key_file) 57 58 59class LibcloudBaseConnection(object): 60 """ 61 Base connection class to inherit from. 62 63 Note: This class should not be instantiated directly. 64 """ 65 66 session = None 67 68 proxy_scheme = None 69 proxy_host = None 70 proxy_port = None 71 72 proxy_username = None 73 proxy_password = None 74 75 http_proxy_used = False 76 77 ca_cert = None 78 79 def __init__(self): 80 self.session = requests.Session() 81 82 def set_http_proxy(self, proxy_url): 83 """ 84 Set a HTTP proxy which will be used with this connection. 85 86 :param proxy_url: Proxy URL (e.g. http://<hostname>:<port> without 87 authentication and 88 http://<username>:<password>@<hostname>:<port> for 89 basic auth authentication information. 90 :type proxy_url: ``str`` 91 """ 92 result = self._parse_proxy_url(proxy_url=proxy_url) 93 94 scheme = result[0] 95 host = result[1] 96 port = result[2] 97 username = result[3] 98 password = result[4] 99 100 self.proxy_scheme = scheme 101 self.proxy_host = host 102 self.proxy_port = port 103 self.proxy_username = username 104 self.proxy_password = password 105 self.http_proxy_used = True 106 107 self.session.proxies = { 108 'http': proxy_url, 109 'https': proxy_url, 110 } 111 112 def _parse_proxy_url(self, proxy_url): 113 """ 114 Parse and validate a proxy URL. 115 116 :param proxy_url: Proxy URL (e.g. http://hostname:3128) 117 :type proxy_url: ``str`` 118 119 :rtype: ``tuple`` (``scheme``, ``hostname``, ``port``) 120 """ 121 parsed = urlparse.urlparse(proxy_url) 122 123 if parsed.scheme not in ('http', 'https'): 124 raise ValueError('Only http and https proxies are supported') 125 126 if not parsed.hostname or not parsed.port: 127 raise ValueError('proxy_url must be in the following format: ' 128 '<scheme>://<proxy host>:<proxy port>') 129 130 proxy_scheme = parsed.scheme 131 proxy_host, proxy_port = parsed.hostname, parsed.port 132 133 netloc = parsed.netloc 134 135 if '@' in netloc: 136 username_password = netloc.split('@', 1)[0] 137 split = username_password.split(':', 1) 138 139 if len(split) < 2: 140 raise ValueError('URL is in an invalid format') 141 142 proxy_username, proxy_password = split[0], split[1] 143 else: 144 proxy_username = None 145 proxy_password = None 146 147 return (proxy_scheme, proxy_host, proxy_port, proxy_username, 148 proxy_password) 149 150 def _setup_verify(self): 151 self.verify = libcloud.security.VERIFY_SSL_CERT 152 153 def _setup_ca_cert(self, **kwargs): 154 # simulating keyword-only argument in Python 2 155 ca_certs_path = kwargs.get('ca_cert', libcloud.security.CA_CERTS_PATH) 156 157 if self.verify is False: 158 pass 159 else: 160 if isinstance(ca_certs_path, list): 161 msg = ( 162 'Providing a list of CA trusts is no longer supported ' 163 'since libcloud 2.0. Using the first element in the list. ' 164 'See http://libcloud.readthedocs.io/en/latest/other/' 165 'changes_in_2_0.html#providing-a-list-of-ca-trusts-is-no-' 166 'longer-supported') 167 warnings.warn(msg, DeprecationWarning) 168 self.ca_cert = ca_certs_path[0] 169 else: 170 self.ca_cert = ca_certs_path 171 172 def _setup_signing(self, cert_file=None, key_file=None): 173 """ 174 Setup request signing by mounting a signing 175 adapter to the session 176 """ 177 self.session.mount('https://', SignedHTTPSAdapter(cert_file, key_file)) 178 179 180class LibcloudConnection(LibcloudBaseConnection): 181 timeout = None 182 host = None 183 response = None 184 185 def __init__(self, host, port, secure=None, **kwargs): 186 scheme = 'https' if secure is not None and secure else 'http' 187 self.host = '{0}://{1}{2}'.format( 188 'https' if port == 443 else scheme, 189 host, 190 ":{0}".format(port) if port not in (80, 443) else "" 191 ) 192 193 # Support for HTTP(s) proxy 194 # NOTE: We always only use a single proxy (either HTTP or HTTPS) 195 https_proxy_url_env = os.environ.get(HTTPS_PROXY_ENV_VARIABLE_NAME, 196 None) 197 http_proxy_url_env = os.environ.get(HTTP_PROXY_ENV_VARIABLE_NAME, 198 https_proxy_url_env) 199 200 # Connection argument has precedence over environment variables 201 proxy_url = kwargs.pop('proxy_url', http_proxy_url_env) 202 203 self._setup_verify() 204 self._setup_ca_cert() 205 206 LibcloudBaseConnection.__init__(self) 207 208 self.session.timeout = kwargs.pop('timeout', DEFAULT_REQUEST_TIMEOUT) 209 210 if 'cert_file' in kwargs or 'key_file' in kwargs: 211 self._setup_signing(**kwargs) 212 213 if proxy_url: 214 self.set_http_proxy(proxy_url=proxy_url) 215 216 @property 217 def verification(self): 218 """ 219 The option for SSL verification given to underlying requests 220 """ 221 return self.ca_cert if self.ca_cert is not None else self.verify 222 223 def request(self, method, url, body=None, headers=None, raw=False, 224 stream=False, hooks=None): 225 url = urlparse.urljoin(self.host, url) 226 headers = self._normalize_headers(headers=headers) 227 228 self.response = self.session.request( 229 method=method.lower(), 230 url=url, 231 data=body, 232 headers=headers, 233 allow_redirects=ALLOW_REDIRECTS, 234 stream=stream, 235 verify=self.verification, 236 timeout=self.session.timeout, 237 hooks=hooks, 238 ) 239 240 def prepared_request(self, method, url, body=None, 241 headers=None, raw=False, stream=False): 242 headers = self._normalize_headers(headers=headers) 243 244 req = requests.Request(method, ''.join([self.host, url]), 245 data=body, headers=headers) 246 247 prepped = self.session.prepare_request(req) 248 249 self.response = self.session.send( 250 prepped, 251 stream=stream, 252 verify=self.ca_cert if self.ca_cert is not None else self.verify) 253 254 def getresponse(self): 255 return self.response 256 257 def getheaders(self): 258 # urlib decoded response body, libcloud has a bug 259 # and will not check if content is gzipped, so let's 260 # remove headers indicating compressed content. 261 if 'content-encoding' in self.response.headers: 262 del self.response.headers['content-encoding'] 263 return self.response.headers 264 265 @property 266 def status(self): 267 return self.response.status_code 268 269 @property 270 def reason(self): 271 return None if self.response.status_code > 400 else self.response.text 272 273 def connect(self): # pragma: no cover 274 pass 275 276 def read(self): 277 return self.response.content 278 279 def close(self): # pragma: no cover 280 # return connection back to pool 281 self.response.close() 282 283 def _normalize_headers(self, headers): 284 headers = headers or {} 285 286 # all headers should be strings 287 for key, value in headers.items(): 288 if isinstance(value, (int, float)): 289 headers[key] = str(value) 290 291 return headers 292 293 294class HttpLibResponseProxy(object): 295 """ 296 Provides a proxy pattern around the :class:`requests.Reponse` 297 object to a :class:`httplib.HTTPResponse` object 298 """ 299 def __init__(self, response): 300 self._response = response 301 302 def read(self, amt=None): 303 return self._response.text 304 305 def getheader(self, name, default=None): 306 """ 307 Get the contents of the header name, or default 308 if there is no matching header. 309 """ 310 if name in self._response.headers.keys(): 311 return self._response.headers[name] 312 else: 313 return default 314 315 def getheaders(self): 316 """ 317 Return a list of (header, value) tuples. 318 """ 319 if PY3: 320 return list(self._response.headers.items()) 321 else: 322 return self._response.headers.items() 323 324 @property 325 def status(self): 326 return self._response.status_code 327 328 @property 329 def reason(self): 330 return self._response.reason 331 332 @property 333 def version(self): 334 # requests doesn't expose this 335 return '11' 336 337 @property 338 def body(self): 339 # NOTE: We use property to avoid saving whole response body into RAM 340 # See https://github.com/apache/libcloud/pull/1132 for details 341 return self._response.content 342