def get_access_token(username=None, password=None, ca_certs=None):
    """Get a goauth access token from nexus.

    Uses basic auth with the user's username and password to authenticate
    to nexus. If the username or password are not passed, they are prompted
    for on stdin.

    @param username: Globus Online username to authenticate as, or None
                     to prompt on stdin.
    @param password: Globus Online password to authenticate with, or None
                     to prompt on stdin (with echo disabled for security).
    @param ca_certs: Path to a ca certificate to verify nexus, or None
                     to use the default CA included in the package.

    @return: GOAuthResult object. Most applications will only care about the
             token field, but username/password may be useful for caching
             authentication information when using the prompt.
    """
    if ca_certs is None:
        from globusonline.transfer.api_client import get_ca
        ca_certs = get_ca(HOST)
    if username is None:
        print "Globus Online Username: "******"Globus Online Password: "******"%s:%s" % (username, password))
    headers = {
        "Content-type": "application/json; charset=UTF-8",
        "Hostname": HOST,
        "Accept": "application/json; charset=UTF-8",
        "Authorization": "Basic %s" % basic_auth
    }
    c = VerifiedHTTPSConnection(HOST, PORT, ca_certs=ca_certs)
    c.request("GET", GOAUTH_PATH, headers=headers)
    response = c.getresponse()
    if response.status == 403:
        raise GOCredentialsError()
    elif response.status > 299 or response.status < 200:
        raise GOAuthError("error response: %d %s" %
                          (response.status, response.reason))
    data = json.loads(response.read())
    token = data.get("access_token")
    if token is None:
        raise GOAuthError("no token in response")

    return GOAuthResult(username, password, token)
def get_access_token(username=None, password=None, ca_certs=None):
    """Get a goauth access token from nexus.

    Uses basic auth with the user's username and password to authenticate
    to nexus. If the username or password are not passed, they are prompted
    for on stdin.

    @param username: Globus Online username to authenticate as, or None
                     to prompt on stdin.
    @param password: Globus Online password to authenticate with, or None
                     to prompt on stdin (with echo disabled for security).
    @param ca_certs: Path to a ca certificate to verify nexus, or None
                     to use the default CA included in the package.

    @return: GOAuthResult object. Most applications will only care about the
             token field, but username/password may be useful for caching
             authentication information when using the prompt.
    """
    if ca_certs is None:
        from globusonline.transfer.api_client import get_ca

        ca_certs = get_ca(HOST)
    if username is None:
        print "Globus Online Username: "******"Globus Online Password: "******"%s:%s" % (username, password))
    headers = {
        "Content-type": "application/json; charset=UTF-8",
        "Hostname": HOST,
        "Accept": "application/json; charset=UTF-8",
        "Authorization": "Basic %s" % basic_auth,
    }
    c = VerifiedHTTPSConnection(HOST, PORT, ca_certs=ca_certs)
    c.request("GET", GOAUTH_PATH, headers=headers)
    response = c.getresponse()
    if response.status == 403:
        raise GOCredentialsError()
    elif response.status > 299 or response.status < 200:
        raise GOAuthError("error response: %d %s" % (response.status, response.reason))
    data = json.loads(response.read())
    token = data.get("access_token")
    if token is None:
        raise GOAuthError("no token in response")

    return GOAuthResult(username, password, token)
def get_go_auth(ca_certs, username=None, password=None):
    """
    POST the login form to www.globusonline.org to get the cookie,
    prompting for username and password on stdin if they were not
    passed as parameters.

    @return: a GOAuthResult instance. The cookie is what most clients will
             be interested in, but if the username is not passed as a
             parameter the caller may need that as well, and may want
             to cache the password.
    """
    if ca_certs is None:
        from globusonline.transfer.api_client import get_ca
        ca_certs = get_ca(HOST)
    if username is None:
        print "GO Username: "******"GO Password: "******"Content-type": "application/x-www-form-urlencoded",
                "Hostname": HOST }
    c = VerifiedHTTPSConnection(HOST, PORT, ca_certs=ca_certs)
    body = urllib.urlencode(dict(username=username,
                                 password=password))
    c.request("POST", PATH, body=body, headers=headers)
    response = c.getresponse()
    set_cookie_header = response.getheader("set-cookie")
    if not set_cookie_header:
        # TODO: more appropriate exc type
        raise ValueError("No cookies received")

    cookies = BaseCookie(set_cookie_header)
    morsel = cookies.get("saml")
    if not morsel:
        raise ValueError("No saml cookie received")

    return GOAuthResult(username, password, morsel.coded_value)
Exemple #4
0
def get_go_auth(ca_certs, username=None, password=None):
    """
    POST the login form to www.globusonline.org to get the cookie,
    prompting for username and password on stdin if they were not
    passed as parameters.

    @return: a GOAuthResult instance. The cookie is what most clients will
             be interested in, but if the username is not passed as a
             parameter the caller may need that as well, and may want
             to cache the password.
    """
    if username is None:
        print "GO Username: "******"GO Password: "******"Content-type": "application/x-www-form-urlencoded",
        "Hostname": HOST
    }
    c = VerifiedHTTPSConnection(HOST, PORT, ca_certs=ca_certs)
    body = urllib.urlencode(dict(username=username, password=password))
    c.request("POST", PATH, body=body, headers=headers)
    response = c.getresponse()
    set_cookie_header = response.getheader("set-cookie")
    if not set_cookie_header:
        # TODO: more appropriate exc type
        raise ValueError("No cookies received")

    cookies = BaseCookie(set_cookie_header)
    morsel = cookies.get("saml")
    if not morsel:
        raise ValueError("No saml cookie received")

    return GOAuthResult(username, password, morsel.coded_value)
class TransferAPIClient(object):
    """
    Maintains a connection to the server as a specific user. Not thread
    safe. Uses the JSON representations.

    Convenience api methods return a triple:
      (status_code, status_message, data)

    data is either the JSON response loaded as a python dictionary,
    or None if the reponse was empty, or a conveninience wrapper around
    the JSON data if the data itself is hard to use directly.

    Endpoint names can be full canonical names of the form
    ausername#epname, or simply epname, in which case the API looks at
    the logged in user's endpoints.
    """

    def __init__(self, username, server_ca_file=None,
                 cert_file=None, key_file=None,
                 base_url=DEFAULT_BASE_URL,
                 timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
                 httplib_debuglevel=0, max_attempts=1,
                 header_auth=None, goauth=None):
        """
        Initialize a client with the client credential and optional alternate
        base URL.

        For authentication, use either x509 or goauth; header_auth is
        deprecated, and cookie auth is no longer supported.

        x509 requires configuring your Globus Online account with the x509
        certificate and having the private key available. Requires @cert_file
        and @key_file, or just one if both are in the same file.

        goauth requires fetching an access token from nexus, which supports
        several authentication methods including basic auth - see the
        goauth module and the nexus documentation.

        @param username: username to connect to the service with.
        @param server_ca_file: path to file containing one or more x509
                               certificates, used to verify the server
                               certificate. If not specified tries to choose
                               the appropriate CA based on the hostname in
                               base_url.
        @param cert_file: path to file containing the x509 client certificate
                          for authentication.
        @param key_file: path to file containg the RSA key for client
                         authentication. If blank and cert_file passed,
                         uses cert_file.
        @param header_auth: contents of the saml cookie, but used for header
                            authentication, not cookie auth.
        @param goauth: goauth access token retrieved from nexus.
        @param base_url: optionally specify an alternate base url, if testing
                         out an unreleased or alternatively hosted version of
                         the API.
        @param timeout: timeout to set on the underlying TCP socket.
        @param max_attempts: Retry every API call on network
                             errors and ServiceUnavailable up to this many
                             times. Sleeps for 30 seconds between each attempt.
                             Note that a socket timeout will be treated as
                             a network error and retried. When max_attempts
                             is exceeded, the exception from the last attempt
                             will be raised. max_attempts=1 implies no
                             retrying.
        """
        if server_ca_file is None:
            server_ca_file = get_ca(base_url)
            if server_ca_file is None:
                raise InterfaceError("no CA found for base URL '%s'"
                                     % base_url)
        if not os.path.isfile(server_ca_file):
            raise InterfaceError("server_ca_file not found: '%s'"
                                 % server_ca_file)

        self.headers = {}

        if header_auth:
            if goauth or cert_file or key_file:
                raise InterfaceError("pass only one auth method")
        elif goauth:
            if cert_file or key_file:
                raise InterfaceError("pass only one auth method")
        elif cert_file or key_file:
            if not key_file:
                key_file = cert_file
            if not cert_file:
                cert_file = key_file
            if not os.path.isfile(cert_file):
                raise InterfaceError("cert_file not found: %s" % cert_file)
            if not os.path.isfile(key_file):
                raise InterfaceError("key_file not found: %s" % key_file)
            self.headers["X-Transfer-API-X509-User"] = username
        else:
            raise InterfaceError("pass one auth method")

        if max_attempts is not None:
            max_attempts = int(max_attempts)
            if max_attempts < 1:
                raise InterfaceError(
                    "max_attempts must be None or a positive integer")
        self.max_attempts = max_attempts

        self.goauth = goauth
        self.cert_file = cert_file
        self.key_file = key_file
        self.header_auth = header_auth

        self.username = username
        self.server_ca_file = server_ca_file
        self.httplib_debuglevel = httplib_debuglevel

        self.base_url = base_url
        self.host, self.port = _get_host_port(base_url)
        self.timeout = timeout

        self.print_request = False
        self.print_response = False
        self.c = None

        self.user_agent = "Python-httplib/%s (%s)" \
                          % (platform.python_version(), platform.system())
        self.client_info = "globusonline.transfer.api_client/%s" % __version__

    def connect(self):
        """
        Create an HTTPS connection to the server. Run automatically by
        request methods.
        """
        kwargs = dict(ca_certs=self.server_ca_file, strict=False,
                      timeout=self.timeout)
        if self.cert_file:
            kwargs["cert_file"] = self.cert_file
            kwargs["key_file"] = self.key_file
        self.c = VerifiedHTTPSConnection(self.host, self.port, **kwargs)

        self.c.set_debuglevel(self.httplib_debuglevel)

    def set_http_connection_debug(self, value):
        """
        Turn debugging of the underlying VerifiedHTTPSConnection on or
        off. Note: this may print sensative information, like auth tokens,
        to standard out.
        """
        if value:
            level = 1
        else:
            level = 0
        self.httplib_debuglevel = level
        if self.c:
            self.c.set_debuglevel(level)

    def set_debug_print(self, print_request, print_response):
        self.print_request = print_request
        self.print_response = print_response

    def close(self):
        """
        Close the wrapped VerifiedHTTPSConnection.
        """
        if self.c:
            self.c.close()
        self.c = None

    def _request(self, method, path, body=None, content_type=None):
        if not path.startswith("/"):
            path = "/" + path
        url = self.base_url + path

        headers = self.headers.copy()
        if content_type:
            headers["Content-Type"] = content_type

        if self.print_request:
            print
            print ">>>REQUEST>>>:"
            print "%s %s" % (method, url)
            for h in headers.iteritems():
                print "%s: %s" % h
            print
            if body:
                print body

        if self.goauth:
            headers["Authorization"] = "Globus-Goauthtoken %s" % self.goauth
        elif self.header_auth:
            headers["Authorization"] = "Bearer %s" % self.header_auth

        headers["User-Agent"] = self.user_agent
        headers["X-Transfer-API-Client"] = self.client_info

        def do_request():
            if self.c is None:
                self.connect()
            self.c.request(method, url, body=body, headers=headers)
            r = self.c.getresponse()
            response_body = r.read()
            return r, response_body

        for attempt in xrange(self.max_attempts):
            r = None
            try:
                try:
                    r, response_body = do_request()
                except BadStatusLine:
                    # This happens when the connection is closed by the server
                    # in between request, which is very likely when using
                    # interactively, in a client that waits for user input
                    # between requests, or after a retry wait. This does not
                    # count as an attempt - it just means the old connection
                    # has gone stale and we need a new one.
                    # TODO: find a more elegant way to re-use the connection
                    #       on closely spaced requests. Can we tell that the
                    #       connection is dead without making a request?
                    self.close()
                    r, response_body = do_request()
            except ssl.SSLError:
                # This probably has to do with failed authentication, so
                # retrying is not useful.
                self.close()
                raise
            except socket.error:
                # Network error. If the last attempt failed, raise,
                # otherwise do nothing and go on to next attempt.
                self.close()
                if attempt == self.max_attempts - 1:
                    raise

            # Check for 503 ServiceUnavailable, which is treated just like
            # network errors.
            if (r is not None and r.status == 503
            and attempt < self.max_attempts - 1):
                # Force sleep below and continue loop, unless we are on
                # the last attempt in which case skip this and return
                # the 503 error.
                self.close()
                r = None

            if r is not None:
                break
            else:
                time.sleep(RETRY_WAIT_SECONDS)

        if self.print_response:
            print
            print "<<<RESPONSE<<<:"
            print r.status, r.reason
            for h in r.getheaders():
                print "%s: %s" % h
            print
            print response_body

        return r, response_body

    def _request_json(self, method, path, body=None, content_type=None):
        """
        Make a request and load the response body as JSON, if the response
        is not empty.
        """
        r, response_body = self._request(method, path, body, content_type)
        if response_body:
            try:
                data = json.loads(response_body)
            except Exception as e:
                raise InterfaceError(
                    ("Unable to parse JSON in response: err='%s', "
                     +"body len='%d', status='%d %s'")
                    % (e, len(response_body), r.status, r.reason))
        else:
            data = None
        return api_result(r, data)

    # Generic API methods:
    def get(self, path):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self._request_json("GET", path)

    def put(self, path, body):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self._request_json("PUT", path, body, "application/json")

    def post(self, path, body):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self._request_json("POST", path, body, "application/json")

    def _delete(self, path):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError

        TODO: this conflicts with the method for submitting delete
              jobs, so it's named inconsistently from the other HTTP method
              functions. Maybe they should all be _ prefixed?
        """
        return self._request_json("DELETE", path)

    # Convenience API methods:
    def tasksummary(self, **kw):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.get("/tasksummary" + encode_qs(kw))

    def task_list(self, **kw):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.get("/task_list" + encode_qs(kw))

    def task(self, task_id, **kw):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.get("/task/%s" % task_id + encode_qs(kw))

    def task_update(self, task_id, task_data, **kw):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.post("/task/%s" % task_id + encode_qs(kw))

    def task_cancel(self, task_id, **kw):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.post("/task/%s/cancel" % task_id + encode_qs(kw),
                         body=None)

    def subtask_list(self, parent_task_id, **kw):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.get("/task/%s/subtask_list"
                        % parent_task_id + encode_qs(kw))

    def subtask(self, task_id, **kw):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.get("/subtask/%s" % task_id + encode_qs(kw))

    def task_event_list(self, parent_task_id, **kw):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.get("/task/%s/event_list" % parent_task_id + encode_qs(kw))

    def subtask_event_list(self, task_id, **kw):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.get("/subtask/%s/event_list" % task_id + encode_qs(kw))

    def endpoint_list(self, **kw):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.get("/endpoint_list" + encode_qs(kw))

    def endpoint(self, endpoint_name, **kw):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.get(_endpoint_path(endpoint_name) + encode_qs(kw))

    def endpoint_activation_requirements(self, endpoint_name, **kw):
        """
        @return: (code, reason, data), where data is an
                 ActivationRequirements instance instead of a plain
                 dictionary.
        @raise TransferAPIError
        """
        code, reason, data = self.get(_endpoint_path(endpoint_name,
                                                 "/activation_requirements")
                                      + encode_qs(kw))
        if code == 200 and data:
            data = ActivationRequirementList(data)
        return code, reason, data

    def endpoint_activate(self, endpoint_name, filled_requirements,
                          if_expires_in="", timeout=30):
        """
        @param endpoint_name: partial or canonical name of endpoint to
                              activate.
        @param filled_requirements: ActivationRequirementList instance with
                                    required values set for one activation
                                    type.
        @type filled_requirements: ActivationRequirementList
        @param if_expires_in: don't re-activate endpoint if it doesn't expire
                              for this many minutes. If not passed, always
                              activate, even if already activated.
        @param timeout: timeout in seconds to attempt contacting external
                        servers to get the credential.
        @return: (code, reason, data), where data is an ActivationRequirements
                 instance.
        @raise TransferAPIError
        """
        if filled_requirements:
            body = json.dumps(filled_requirements.json_data)
        else:
            raise InterfaceError("Use autoactivate instead; using activate "
                "with an empty request body to auto activate is "
                "deprecated.")
        # Note: blank query parameters are ignored, so we can pass blank
        # values to use the default behavior.
        qs = encode_qs(dict(if_expires_in=str(if_expires_in),
                            timeout=str(timeout)))
        code, reason, data = self.post(
            _endpoint_path(endpoint_name, "/activate" + qs), body=body)
        if code == 200 and data:
            data = ActivationRequirementList(data)
        return code, reason, data

    def endpoint_autoactivate(self, endpoint_name, if_expires_in="",
                              timeout=30):
        """
        @param endpoint_name: partial or canonical name of endpoint to
                              activate.
        @param if_expires_in: don't re-activate endpoint if it doesn't expire
                              for this many minutes. If not passed, always
                              activate, even if already activated.
        @param timeout: timeout in seconds to attempt contacting external
                        servers to get the credential.
        @return: (code, reason, data), where data is an ActivationRequirements
                 instance.
        @raise TransferAPIError
        """
        # Note: blank query parameters are ignored, so we can pass blank
        # values to use the default behavior.
        qs = encode_qs(dict(if_expires_in=str(if_expires_in),
                            timeout=str(timeout)))
        code, reason, data = self.post(
            _endpoint_path(endpoint_name, "/autoactivate" + qs), body=None)
        if code == 200 and data:
            data = ActivationRequirementList(data)
        return code, reason, data

    def endpoint_deactivate(self, endpoint_name, **kw):
        """
        @param endpoint_name: partial or canonical name of endpoint to
                              activate.
        @return: (code, reason, data)
        @raise TransferAPIError
        """
        # Note: blank query parameters are ignored, so we can pass blank
        # values to use the default behavior.
        code, reason, data = self.post(
            _endpoint_path(endpoint_name, "/deactivate") + encode_qs(kw),
            body=None)
        return code, reason, data

    def endpoint_ls(self, endpoint_name, path="", **kw):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        kw["path"] = path
        return self.get(_endpoint_path(endpoint_name, "/ls")
                        + encode_qs(kw))

    def endpoint_mkdir(self, endpoint_name, path, **kw):
        data = dict(path=path, DATA_TYPE="mkdir")
        return self.post(_endpoint_path(endpoint_name, "/mkdir")
                         + encode_qs(kw), json.dumps(data))

    def endpoint_create(self, endpoint_name, hostname=None, description="",
                        scheme="gsiftp", port=2811, subject=None,
                        myproxy_server=None, myproxy_dn=None,
                        public=False, is_globus_connect=False,
                        default_directory=None, oauth_server=None):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        data = { "DATA_TYPE": "endpoint",
                 "myproxy_server": myproxy_server,
                 "myproxy_dn": myproxy_dn,
                 "description": description,
                 "canonical_name": endpoint_name,
                 "public": public,
                 "is_globus_connect": is_globus_connect,
                 "default_directory": default_directory,
                 "oauth_server": oauth_server, }
        if not is_globus_connect:
            data["DATA"] = [dict(DATA_TYPE="server",
                                 hostname=hostname,
                                 scheme=scheme,
                                 port=port,
                                 subject=subject)]

        return self.post("/endpoint", json.dumps(data))

    def endpoint_update(self, endpoint_name, endpoint_data):
        """
        Call endpoint to get the data, modify as needed, then pass the
        modified data to this method.

        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.put(_endpoint_path(endpoint_name),
                        json.dumps(endpoint_data))

    def endpoint_rename(self, endpoint_name, new_endpoint_name):
        _, _, endpoint_data = self.endpoint(endpoint_name)
        endpoint_data["canonical_name"] = new_endpoint_name
        del endpoint_data["name"]
        return self.endpoint_update(endpoint_name, endpoint_data)

    def endpoint_delete(self, endpoint_name):
        """
        Delete the specified endpoint. Existing transfers using the endpoint
        will continue to work, but you will not be able to use the endpoint
        in any new operations, and it will be gone from the endpoint_list.

        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self._delete(_endpoint_path(endpoint_name))

    def submission_id(self):
        """
        @return: (status_code, status_reason, data)
        @raise: TransferAPIError
        """
        return self.get("/submission_id")

    # backward compatibility
    transfer_submission_id = submission_id

    def transfer(self, transfer):
        """
        @type transfer: Transfer object
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.post("/transfer", transfer.as_json())

    def delete(self, delete):
        """
        @type delete: Delete object
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.post("/delete", delete.as_json())
Exemple #6
0
class TransferAPIClient(object):
    """
    Maintains a connection to the server as a specific users. Not thread
    safe. Uses the JSON representations.

    Convenience api methods return a triple:
      (status_code, status_message, data)

    data is either the JSON response loaded as a python dictionary,
    or None if the reponse was empty, or a conveninience wrapper around
    the JSON data if the data itself is hard to use directly.

    Endpoint names can be full canonical names of the form
    ausername#epname, or simply epname, in which case the API looks at
    the logged in user's endpoints.
    """
    def __init__(self,
                 username,
                 server_ca_file,
                 cert_file=None,
                 key_file=None,
                 saml_cookie=None,
                 base_url=DEFAULT_BASE_URL,
                 timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
                 httplib_debuglevel=0,
                 max_attempts=1):
        """
        Initialize a client with the client credential and optional alternate
        base URL.

        The main authentication method is using an x509 certificate,
        in which case cert_file and key_file are required. A signed cookie
        can also be used, but that is mainly used for internal testing;
        however it is possible to copy the contents of the 'saml' cookie
        from the browser after signing in to www.globusonline.org and use
        that, until it expires.

        @param username: username to connect to the service with.
        @param server_ca_file: path to file containing one or more x509
                               certificates, used to verify the server
                               certificate.
        @param cert_file: path to file containing the x509 client certificate
                          for authentication.
        @param key_file: path to file containg the RSA key for client
                         authentication. If blank and cert_file passed,
                         uses cert_file.
        @param saml_cookie: contents of 'saml' cookie from
                            www.globusonline.org.
        @param base_url: optionally specify an alternate base url, if testing
                         out an unreleased or alternatively hosted version of
                         the API.
        @param timeout: timeout to set on the underlying TCP socket.
        @param max_attempts: Retry every API call on network
                             errors and ServiceUnavailable up to this many
                             times. Sleeps for 30 seconds between each attempt.
                             Note that a socket timeout will be treated as
                             a network error and retried. When max_attempts
                             is exceeded, the exception from the last attempt
                             will be raised. max_attempts=1 implies no
                             retrying.
        """
        if not os.path.isfile(server_ca_file):
            raise InterfaceError("server_ca_file not found: %s" %
                                 server_ca_file)

        if saml_cookie and (cert_file or key_file):
            raise InterfaceError("pass either cooie or cert and key"
                                 " files, not both.")
        if cert_file:
            if not os.path.isfile(cert_file):
                raise InterfaceError("cert_file not found: %s" % cert_file)
            if not key_file:
                key_file = cert_file
            else:
                if not os.path.isfile(key_file):
                    raise InterfaceError("key_file not found: %s" % key_file)

        if max_attempts is not None:
            max_attempts = int(max_attempts)
            if max_attempts < 1:
                raise InterfaceError(
                    "max_attempts must be None or a positive integer")
        self.max_attempts = max_attempts

        self.saml_cookie = saml_cookie
        self.cert_file = cert_file
        self.key_file = key_file

        self.username = username
        self.server_ca_file = server_ca_file
        self.httplib_debuglevel = httplib_debuglevel

        self.base_url = base_url
        self.host, self.port = _get_host_port(base_url)
        self.timeout = timeout

        if saml_cookie:
            unquoted = urllib.unquote(saml_cookie)
            if unquoted.find("un=%s|" % username) == -1:
                raise InterfaceError("saml cookie username does not match "
                                     "username argument")
            self.headers = {}
        else:
            self.headers = {"X-Transfer-API-X509-User": username}

        self.print_request = False
        self.print_response = False
        self.c = None

        self.user_agent = "Python-httplib/%s (%s)" \
                          % (platform.python_version(), platform.system())
        self.client_info = "globusonline.transfer.api_client/%s" % __version__

    def connect(self):
        """
        Create an HTTPS connection to the server. Run automatically by
        request methods.
        """
        kwargs = dict(ca_certs=self.server_ca_file,
                      strict=False,
                      timeout=self.timeout)
        if self.cert_file:
            kwargs["cert_file"] = self.cert_file
            kwargs["key_file"] = self.key_file
        self.c = VerifiedHTTPSConnection(self.host, self.port, **kwargs)

        self.c.set_debuglevel(self.httplib_debuglevel)

    def set_http_connection_debug(self, value):
        """
        Turn debugging of the underlying VerifiedHTTPSConnection on or
        off. Note: this may print sensative information, like saml cookie,
        to standard out.
        """
        if value:
            level = 1
        else:
            level = 0
        self.httplib_debuglevel = level
        if self.c:
            self.c.set_debuglevel(level)

    def set_debug_print(self, print_request, print_response):
        self.print_request = print_request
        self.print_response = print_response

    def close(self):
        """
        Close the wrapped VerifiedHTTPSConnection.
        """
        if self.c:
            self.c.close()
        self.c = None

    def _request(self, method, path, body=None, content_type=None):
        if not path.startswith("/"):
            path = "/" + path
        url = self.base_url + path

        headers = self.headers.copy()
        if content_type:
            headers["Content-Type"] = content_type

        if self.print_request:
            print
            print ">>>REQUEST>>>:"
            print "%s %s" % (method, url)
            if self.saml_cookie:
                # Should be enough to show the username and still hide the
                # signature.
                headers["Cookie"] = "saml=%s..." % self.saml_cookie[:31]
            for h in headers.iteritems():
                print "%s: %s" % h
            print
            if body:
                print body

        if self.saml_cookie:
            headers["Cookie"] = "saml=%s" % self.saml_cookie

        headers["User-Agent"] = self.user_agent
        headers["X-Transfer-API-Client"] = self.client_info

        def do_request():
            if self.c is None:
                self.connect()
            self.c.request(method, url, body=body, headers=headers)
            r = self.c.getresponse()
            response_body = r.read()
            return r, response_body

        for attempt in xrange(self.max_attempts):
            #print "attempt:", attempt
            r = None
            try:
                try:
                    r, response_body = do_request()
                except BadStatusLine:
                    # This happens when the connection is closed by the server
                    # in between request, which is very likely when using
                    # interactively, in a client that waits for user input
                    # between requests, or after a retry wait. This does not
                    # count as an attempt - it just means the old connection
                    # has gone stale and we need a new one.
                    # TODO: find a more elegant way to re-use the connection
                    #       on closely spaced requests. Can we tell that the
                    #       connection is dead without making a request?
                    self.close()
                    r, response_body = do_request()
            except ssl.SSLError:
                # This probably has to do with failed authentication, so
                # retrying is not useful.
                traceback.print_exc()
                self.close()
                raise
            except socket.error:
                # Network error. If the last attempt failed, raise,
                # otherwise do nothing and go on to next attempt.
                traceback.print_exc()
                self.close()
                if attempt == self.max_attempts - 1:
                    raise

            # Check for ServiceUnavailable, which is treated just like
            # network errors.
            if r is not None and attempt < self.max_attempts - 1:
                error_code = r.getheader("X-Transfer-API-Error", None)
                if error_code is not None \
                and error_code.startswith("ServiceUnavailable"):
                    # Force sleep below and continue loop
                    self.close()
                    r = None

            if r is not None:
                break
            else:
                time.sleep(RETRY_WAIT_SECONDS)

        if self.print_response:
            print
            print "<<<RESPONSE<<<:"
            print r.status, r.reason
            for h in r.getheaders():
                print "%s: %s" % h
            print
            print response_body

        return r, response_body

    def _request_json(self, method, path, body=None, content_type=None):
        """
        Make a request and load the response body as JSON, if the response
        is not empty.
        """
        r, response_body = self._request(method, path, body, content_type)
        if response_body:
            try:
                data = json.loads(response_body)
            except Exception as e:
                raise InterfaceError("Unable to parse JSON in response: " +
                                     str(e))
        else:
            data = None
        return api_result(r, data)

    # Generic API methods:
    def get(self, path):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self._request_json("GET", path)

    def put(self, path, body):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self._request_json("PUT", path, body, "application/json")

    def post(self, path, body):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self._request_json("POST", path, body, "application/json")

    def _delete(self, path):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError

        TODO: this conflicts with the method for submitting delete
              jobs, so it's named inconsistently from the other HTTP method
              functions. Maybe they should all be _ prefixed?
        """
        return self._request_json("DELETE", path)

    # Convenience API methods:
    def tasksummary(self, **kw):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.get("/tasksummary" + encode_qs(kw))

    def task_list(self, **kw):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.get("/task_list" + encode_qs(kw))

    def task(self, task_id, **kw):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.get("/task/%s" % task_id + encode_qs(kw))

    def task_update(self, task_id, task_data):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.post("/task/%s" % task_id + encode_qs(kw))

    def task_cancel(self, task_id, **kw):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.post("/task/%s/cancel" % task_id + encode_qs(kw),
                         body=None)

    def subtask_list(self, parent_task_id, **kw):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.get("/task/%s/subtask_list" % parent_task_id +
                        encode_qs(kw))

    def subtask(self, task_id, **kw):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.get("/subtask/%s" % task_id + encode_qs(kw))

    def task_event_list(self, parent_task_id, **kw):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.get("/task/%s/event_list" % parent_task_id + encode_qs(kw))

    def subtask_event_list(self, task_id, **kw):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.get("/subtask/%s/event_list" % task_id + encode_qs(kw))

    def endpoint_list(self, **kw):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.get("/endpoint_list" + encode_qs(kw))

    def endpoint(self, endpoint_name, **kw):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.get(_endpoint_path(endpoint_name) + encode_qs(kw))

    def endpoint_activation_requirements(self, endpoint_name, **kw):
        """
        @return: (code, reason, data), where data is an
                 ActivationRequirements instance instead of a plain
                 dictionary.
        @raise TransferAPIError
        """
        code, reason, data = self.get(
            _endpoint_path(endpoint_name, "/activation_requirements") +
            encode_qs(kw))
        if code == 200 and data:
            data = ActivationRequirementList(data)
        return code, reason, data

    def endpoint_activate(self,
                          endpoint_name,
                          filled_requirements,
                          if_expires_in="",
                          timeout=30):
        """
        @param endpoint_name: partial or canonical name of endpoint to
                              activate.
        @param filled_requirements: ActivationRequirementList instance,
                                    or None to attempt auto-activation.
        @type filled_requirements: ActivationRequirementList
        @param if_expires_in: don't re-activate endpoint if it doesn't expire
                              for this many minutes. If not passed, always
                              activate, even if already activated.
        @param timeout: timeout in seconds to attempt contacting external
                        servers to get the credential.
        @return: (code, reason, data), where data is an ActivationRequirements
                 instance.
        @raise TransferAPIError
        """
        if filled_requirements:
            body = json.dumps(filled_requirements.json_data)
        else:
            raise InterfaceError(
                "Use autoactivate instead; using activate "
                "with an empty request body to auto activate is "
                "deprecated.")
        # Note: blank query parameters are ignored, so we can pass blank
        # values to use the default behavior.
        qs = encode_qs(
            dict(if_expires_in=str(if_expires_in), timeout=str(timeout)))
        code, reason, data = self.post(_endpoint_path(endpoint_name,
                                                      "/activate" + qs),
                                       body=body)
        if code == 200 and data:
            data = ActivationRequirementList(data)
        return code, reason, data

    def endpoint_autoactivate(self,
                              endpoint_name,
                              if_expires_in="",
                              timeout=30):
        """
        @param endpoint_name: partial or canonical name of endpoint to
                              activate.
        @param if_expires_in: don't re-activate endpoint if it doesn't expire
                              for this many minutes. If not passed, always
                              activate, even if already activated.
        @param timeout: timeout in seconds to attempt contacting external
                        servers to get the credential.
        @return: (code, reason, data), where data is an ActivationRequirements
                 instance.
        @raise TransferAPIError
        """
        # Note: blank query parameters are ignored, so we can pass blank
        # values to use the default behavior.
        qs = encode_qs(
            dict(if_expires_in=str(if_expires_in), timeout=str(timeout)))
        code, reason, data = self.post(_endpoint_path(endpoint_name,
                                                      "/autoactivate" + qs),
                                       body=None)
        if code == 200 and data:
            data = ActivationRequirementList(data)
        return code, reason, data

    def endpoint_deactivate(self, endpoint_name, **kw):
        """
        @param endpoint_name: partial or canonical name of endpoint to
                              activate.
        @return: (code, reason, data)
        @raise TransferAPIError
        """
        # Note: blank query parameters are ignored, so we can pass blank
        # values to use the default behavior.
        code, reason, data = self.post(
            _endpoint_path(endpoint_name, "/deactivate") + encode_qs(kw),
            body=None)
        return code, reason, data

    def endpoint_ls(self, endpoint_name, path="", **kw):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        kw["path"] = path
        return self.get(_endpoint_path(endpoint_name, "/ls") + encode_qs(kw))

    def endpoint_mkdir(self, endpoint_name, path, **kw):
        data = dict(path=path, DATA_TYPE="mkdir")
        return self.post(
            _endpoint_path(endpoint_name, "/mkdir") + encode_qs(kw),
            json.dumps(data))

    def endpoint_create(self,
                        endpoint_name,
                        hostname=None,
                        description="",
                        scheme="gsiftp",
                        port=2811,
                        subject=None,
                        myproxy_server=None,
                        public=False,
                        is_globus_connect=False):
        """
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        data = {
            "DATA_TYPE": "endpoint",
            "myproxy_server": myproxy_server,
            "description": description,
            "canonical_name": endpoint_name,
            "public": public,
            "is_globus_connect": is_globus_connect,
        }
        if not is_globus_connect:
            data["DATA"] = [
                dict(DATA_TYPE="server",
                     hostname=hostname,
                     scheme=scheme,
                     port=port,
                     subject=subject)
            ]

        return self.post("/endpoint", json.dumps(data))

    def endpoint_update(self, endpoint_name, endpoint_data):
        """
        Call endpoint to get the data, modify as needed, then pass the
        modified data to this method.

        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.put(_endpoint_path(endpoint_name),
                        json.dumps(endpoint_data))

    def endpoint_rename(self, endpoint_name, new_endpoint_name):
        _, _, endpoint_data = self.endpoint(endpoint_name)
        endpoint_data["canonical_name"] = new_endpoint_name
        del endpoint_data["name"]
        return self.endpoint_update(endpoint_name, endpoint_data)

    def endpoint_delete(self, endpoint_name):
        """
        Delete the specified endpoint. Existing transfers using the endpoint
        will continue to work, but you will not be able to use the endpoint
        in any new operations, and it will be gone from the endpoint_list.

        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self._delete(_endpoint_path(endpoint_name))

    def submission_id(self):
        """
        @return: (status_code, status_reason, data)
        @raise: TransferAPIError
        """
        return self.get("/submission_id")

    # backward compatibility
    transfer_submission_id = submission_id

    def transfer(self, transfer):
        """
        @type transfer: Transfer object
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.post("/transfer", transfer.as_json())

    def delete(self, delete):
        """
        @type delete: Delete object
        @return: (status_code, status_reason, data)
        @raise TransferAPIError
        """
        return self.post("/delete", delete.as_json())