import requests from requests.auth import AuthBase from six.moves.urllib.parse import urlparse from dcos import config, util from dcos.errors import (DCOSAuthenticationException, DCOSAuthorizationException, DCOSBadRequest, DCOSConnectionError, DCOSException, DCOSHTTPException, DCOSUnprocessableException) logger = util.get_logger(__name__) DEFAULT_TIMEOUT = 5 def _default_is_success(status_code): """Returns true if the success status is between [200, 300). :param response_status: the http response status :type response_status: int :returns: True for success status; False otherwise :rtype: bool """ return 200 <= status_code < 300 def _verify_ssl(verify=None, toml_config=None): """Returns whether to verify ssl :param verify: whether to verify SSL certs or path to cert(s) :type verify: bool | str :param toml_config: cluster config to use :type toml_config: Toml :return: whether to verify SSL certs or path to cert(s) :rtype: bool | str """ if toml_config is None: toml_config = config.get_config() if verify is None: verify = config.get_config_val("core.ssl_verify", toml_config) if verify and verify.lower() == "true": verify = True elif verify and verify.lower() == "false": verify = False return verify @util.duration def _request(method, url, is_success=_default_is_success, timeout=DEFAULT_TIMEOUT, auth=None, verify=None, toml_config=None, **kwargs): """Sends an HTTP request. :param method: method for the new Request object :type method: str :param url: URL for the new Request object :type url: str :param is_success: Defines successful status codes for the request :type is_success: Function from int to bool :param timeout: request timeout :type timeout: int :param auth: authentication :type auth: AuthBase :param verify: whether to verify SSL certs or path to cert(s) :type verify: bool | str :param toml_config: cluster config to use :type toml_config: Toml :param kwargs: Additional arguments to requests.request (see http://docs.python-requests.org/en/latest/api/#requests.request) :type kwargs: dict :rtype: Response """ if 'headers' not in kwargs: kwargs['headers'] = {'Accept': 'application/json'} verify = _verify_ssl(verify, toml_config) # Silence 'Unverified HTTPS request' and 'SecurityWarning' for bad certs if verify is not None: silence_requests_warnings() logger.info( 'Sending HTTP [%r] to [%r]: %r', method, url, kwargs.get('headers')) try: response = requests.request( method=method, url=url, timeout=timeout, auth=auth, verify=verify, **kwargs) except requests.exceptions.SSLError as e: logger.exception("HTTP SSL Error") msg = ("An SSL error occurred. To configure your SSL settings, " "please run: `dcos config set core.ssl_verify `") description = config.get_property_description("core", "ssl_verify") if description is not None: msg += "\n: {}".format(description) raise DCOSException(msg) except requests.exceptions.ConnectionError as e: logger.exception("HTTP Connection Error") raise DCOSConnectionError(url) except requests.exceptions.Timeout as e: logger.exception("HTTP Timeout") raise DCOSException('Request to URL [{0}] timed out.'.format(url)) except requests.exceptions.RequestException as e: logger.exception("HTTP Exception") raise DCOSException('HTTP Exception: {}'.format(e)) logger.info('Received HTTP response [%r]: %r', response.status_code, response.headers) return response def request(method, url, is_success=_default_is_success, timeout=None, verify=None, toml_config=None, **kwargs): """Sends an HTTP request. If the server responds with a 401, ask the user for their credentials, and try request again (up to 3 times). :param method: method for the new Request object :type method: str :param url: URL for the new Request object :type url: str :param is_success: Defines successful status codes for the request :type is_success: Function from int to bool :param timeout: request timeout :type timeout: int :param verify: whether to verify SSL certs or path to cert(s) :type verify: bool | str :param toml_config: cluster config to use :type toml_config: Toml :param kwargs: Additional arguments to requests.request (see http://docs.python-requests.org/en/latest/api/#requests.request) :type kwargs: dict :rtype: Response """ if toml_config is None: toml_config = config.get_config() auth_token = config.get_config_val("core.dcos_acs_token", toml_config) prompt_login = config.get_config_val("core.prompt_login", toml_config) dcos_url = urlparse(config.get_config_val("core.dcos_url", toml_config)) cosmos_url = urlparse( config.get_config_val("package.cosmos_url", toml_config)) parsed_url = urlparse(url) # only request with DC/OS Auth if request is to DC/OS cluster # request should match scheme + netloc def _request_match(expected_url, actual_url): return expected_url.scheme == actual_url.scheme and \ expected_url.netloc == actual_url.netloc request_to_cluster = _request_match(dcos_url, parsed_url) or \ _request_match(cosmos_url, parsed_url) if auth_token and request_to_cluster: auth = DCOSAcsAuth(auth_token) else: auth = None response = _request(method, url, is_success, timeout, auth=auth, verify=verify, toml_config=toml_config, **kwargs) if is_success(response.status_code): return response elif response.status_code == 401: if prompt_login: # I don't like having imports that aren't at the top level, but # this is to resolve a circular import issue between dcos.http and # dcos.auth from dcos.auth import header_challenge_auth header_challenge_auth(dcos_url.geturl()) # if header_challenge_auth succeeded, then we auth-ed correctly and # thus can safely recursively call ourselves and not have to worry # about an infinite loop return request(method=method, url=url, is_success=is_success, timeout=timeout, verify=verify, **kwargs) else: if auth_token is not None: msg = ("Your core.dcos_acs_token is invalid. " "Please run: `dcos auth login`") raise DCOSAuthenticationException(msg) else: raise DCOSAuthenticationException(response) elif response.status_code == 422: raise DCOSUnprocessableException(response) elif response.status_code == 403: raise DCOSAuthorizationException(response) elif response.status_code == 400: raise DCOSBadRequest(response) else: raise DCOSHTTPException(response) def head(url, **kwargs): """Sends a HEAD request. :param url: URL for the new Request object :type url: str :param kwargs: Additional arguments to requests.request (see py:func:`request`) :type kwargs: dict :rtype: Response """ return request('head', url, **kwargs) def get(url, **kwargs): """Sends a GET request. :param url: URL for the new Request object :type url: str :param kwargs: Additional arguments to requests.request (see py:func:`request`) :type kwargs: dict :rtype: Response """ return request('get', url, **kwargs) def post(url, data=None, json=None, **kwargs): """Sends a POST request. :param url: URL for the new Request object :type url: str :param data: Request body :type data: dict, bytes, or file-like object :param json: JSON request body :type data: dict :param kwargs: Additional arguments to requests.request (see py:func:`request`) :type kwargs: dict :rtype: Response """ return request('post', url, data=data, json=json, **kwargs) def put(url, data=None, **kwargs): """Sends a PUT request. :param url: URL for the new Request object :type url: str :param data: Request body :type data: dict, bytes, or file-like object :param kwargs: Additional arguments to requests.request (see py:func:`request`) :type kwargs: dict :rtype: Response """ return request('put', url, data=data, **kwargs) def patch(url, data=None, **kwargs): """Sends a PATCH request. :param url: URL for the new Request object :type url: str :param data: Request body :type data: dict, bytes, or file-like object :param kwargs: Additional arguments to requests.request (see py:func:`request`) :type kwargs: dict :rtype: Response """ return request('patch', url, data=data, **kwargs) def delete(url, **kwargs): """Sends a DELETE request. :param url: URL for the new Request object :type url: str :param kwargs: Additional arguments to requests.request (see py:func:`request`) :type kwargs: dict :rtype: Response """ return request('delete', url, **kwargs) def silence_requests_warnings(): """Silence warnings from requests.packages.urllib3. See DCOS-1007.""" requests.packages.urllib3.disable_warnings() class DCOSAcsAuth(AuthBase): """Invokes DCOS Authentication flow for given Request object.""" def __init__(self, token): self.token = token def __call__(self, r): r.headers['Authorization'] = "token={}".format(self.token) return r