示例#1
0
 def _check_openssl_version(self):
     """
     Check that merchant server has PCI compliant version of TLS
     Print warning if it does not.
     """
     if self.ssl_major <= 0 and not util.older_than_27():
         log.warning(
             'SECURITY WARNING: openssl version ' + self.ssl_version + ' detected. Please upgrade to latest OpenSSL \
              version to enable TLSv1.2.')
示例#2
0
class Api(object):

    # User-Agent for HTTP request
    ssl_version = "" if util.older_than_27() else ssl.OPENSSL_VERSION
    ssl_version_info = None if util.older_than_27(
    ) else ssl.OPENSSL_VERSION_INFO
    library_details = "requests %s; python %s; %s" % (
        requests.__version__, platform.python_version(), ssl_version)
    user_agent = "PayPalSDK/PayPal-Python-SDK %s (%s)" % (__version__,
                                                          library_details)

    def __init__(self, options=None, **kwargs):
        """Create API object

        Usage::

            >>> import paypalrestsdk
            >>> api = paypalrestsdk.Api(mode="sandbox", client_id='CLIENT_ID', client_secret='CLIENT_SECRET',
             ssl_options={"cert": "/path/to/server.pem"})
        """
        kwargs = util.merge_dict(options or {}, kwargs)

        self.mode = kwargs.get("mode", "sandbox")
        self.endpoint = kwargs.get("endpoint", self.default_endpoint())
        self.token_endpoint = kwargs.get("token_endpoint", self.endpoint)
        # Mandatory parameter, so not using `dict.get`
        self.client_id = kwargs["client_id"]
        # Mandatory parameter, so not using `dict.get`
        self.client_secret = kwargs["client_secret"]
        self.proxies = kwargs.get("proxies", None)
        self.token_hash = None
        self.token_request_at = None
        # setup SSL certificate verification if private certificate provided
        ssl_options = kwargs.get("ssl_options", {})
        if "cert" in ssl_options:
            os.environ["REQUESTS_CA_BUNDLE"] = ssl_options["cert"]

        if kwargs.get("token"):
            self.token_hash = {
                "access_token": kwargs["token"],
                "token_type": "Bearer"
            }

        self.options = kwargs

    def default_endpoint(self):
        return __endpoint_map__.get(self.mode)

    def basic_auth(self):
        """Find basic auth, and returns base64 encoded
        """
        credentials = "%s:%s" % (self.client_id, self.client_secret)
        return base64.b64encode(
            credentials.encode('utf-8')).decode('utf-8').replace("\n", "")

    def get_token_hash(self,
                       authorization_code=None,
                       refresh_token=None,
                       headers=None):
        """Generate new token by making a POST request

            1. By using client credentials if validate_token_hash finds
            token to be invalid. This is useful during web flow so that an already
            authenticated user is not reprompted for login
            2. Exchange authorization_code from mobile device for a long living
            refresh token that can be used to charge user who has consented to future
            payments
            3. Exchange refresh_token for the user for a access_token of type Bearer
            which can be passed in to charge user

        """
        path = "/v1/oauth2/token"
        payload = "grant_type=client_credentials"

        if authorization_code is not None:
            payload = "grant_type=authorization_code&response_type=token&redirect_uri=urn:ietf:wg:oauth:2.0:oob&code=" + \
                authorization_code

        elif refresh_token is not None:
            payload = "grant_type=refresh_token&refresh_token=" + refresh_token

        else:
            self.validate_token_hash()
            if self.token_hash is not None:
                # return cached copy
                return self.token_hash

        token = self.http_call(
            util.join_url(self.token_endpoint, path),
            "POST",
            data=payload,
            headers=util.merge_dict(
                {
                    "Authorization": ("Basic %s" % self.basic_auth()),
                    "Content-Type": "application/x-www-form-urlencoded",
                    "Accept": "application/json",
                    "User-Agent": self.user_agent
                }, headers or {}))

        if refresh_token is None and authorization_code is None:
            # cache token for re-use in normal case
            self.token_request_at = datetime.datetime.now()
            self.token_hash = token
        return token

    def validate_token_hash(self):
        """Checks if token duration has expired and if so resets token
        """
        if self.token_request_at and self.token_hash and self.token_hash.get(
                "expires_in") is not None:
            delta = datetime.datetime.now() - self.token_request_at
            duration = (
                delta.microseconds +
                (delta.seconds + delta.days * 24 * 3600) * 10**6) / 10**6
            if duration > self.token_hash.get("expires_in"):
                self.token_hash = None

    def get_access_token(self,
                         authorization_code=None,
                         refresh_token=None,
                         headers=None):
        """Wraps get_token_hash for getting access token
        """
        return self.get_token_hash(authorization_code,
                                   refresh_token,
                                   headers=headers or {})['access_token']

    def get_refresh_token(self, authorization_code=None, headers=None):
        """Exchange authorization code for refresh token for future payments
        """
        if authorization_code is None:
            raise exceptions.MissingConfig(
                "Authorization code needed to get new refresh token. \
            Refer to https://developer.paypal.com/docs/integration/mobile/make-future-payment/#get-an-auth-code"
            )
        return self.get_token_hash(authorization_code, headers=headers
                                   or {})["refresh_token"]

    def _check_openssl_version(self):
        """
        Check that merchant server has PCI compliant version of TLS
        Print warning if it does not.
        """
        if self.ssl_version_info and self.ssl_version_info < (1, 0, 1, 0, 0):
            log.warning('WARNING: openssl version ' + self.ssl_version +
                        ' detected. Per PCI Security Council mandate \
                (https://github.com/paypal/TLS-update), you MUST update to the latest security library.'
                        )

    def request(self,
                url,
                method,
                body=None,
                headers=None,
                refresh_token=None):
        """Make HTTP call, formats response and does error handling. Uses http_call method in API class.

        Usage::

            >>> api.request("https://api.sandbox.paypal.com/v1/payments/payment?count=10", "GET", {})
            >>> api.request("https://api.sandbox.paypal.com/v1/payments/payment", "POST", "{}", {} )

        """

        http_headers = util.merge_dict(
            self.headers(refresh_token=refresh_token, headers=headers or {}),
            headers or {})

        if http_headers.get('PayPal-Request-Id'):
            log.info('PayPal-Request-Id: %s' %
                     (http_headers['PayPal-Request-Id']))

        self._check_openssl_version()

        try:
            return self.http_call(url,
                                  method,
                                  data=json.dumps(body),
                                  headers=http_headers)

        # Format Error message for bad request
        except exceptions.BadRequest as error:
            return {"error": json.loads(error.content)}

        # Handle Expired token
        except exceptions.UnauthorizedAccess as error:
            if (self.token_hash and self.client_id):
                self.token_hash = None
                return self.request(url, method, body, headers)
            else:
                raise error

    def http_call(self, url, method, **kwargs):
        """Makes a http call. Logs response information.
        """
        log.info('Request[%s]: %s' % (method, url))

        if self.mode.lower() != 'live':
            request_headers = kwargs.get("headers", {})
            request_body = kwargs.get("data", {})
            log.debug("Level: " + self.mode)
            log.debug('Request: \nHeaders: %s\nBody: %s' %
                      (str(request_headers), str(request_body)))
        else:
            log.info(
                'Not logging full request/response headers and body in live mode for compliance'
            )

        start_time = datetime.datetime.now()
        response = requests.request(method,
                                    url,
                                    proxies=self.proxies,
                                    **kwargs)
        duration = datetime.datetime.now() - start_time
        log.info('Response[%d]: %s, Duration: %s.%ss.' %
                 (response.status_code, response.reason, duration.seconds,
                  duration.microseconds))

        debug_id = response.headers.get('PayPal-Debug-Id')
        if debug_id:
            log.debug('debug_id: %s' % debug_id)
        if self.mode.lower() != 'live':
            log.debug('Headers: %s\nBody: %s' %
                      (str(response.headers), str(response.content)))

        return self.handle_response(response, response.content.decode('utf-8'))

    def handle_response(self, response, content):
        """Validate HTTP response
        """
        status = response.status_code
        if status in (301, 302, 303, 307):
            raise exceptions.Redirection(response, content)
        elif 200 <= status <= 299:
            return json.loads(content) if content else {}
        elif status == 400:
            raise exceptions.BadRequest(response, content)
        elif status == 401:
            raise exceptions.UnauthorizedAccess(response, content)
        elif status == 403:
            raise exceptions.ForbiddenAccess(response, content)
        elif status == 404:
            raise exceptions.ResourceNotFound(response, content)
        elif status == 405:
            raise exceptions.MethodNotAllowed(response, content)
        elif status == 409:
            raise exceptions.ResourceConflict(response, content)
        elif status == 410:
            raise exceptions.ResourceGone(response, content)
        elif status == 422:
            raise exceptions.ResourceInvalid(response, content)
        elif 401 <= status <= 499:
            raise exceptions.ClientError(response, content)
        elif 500 <= status <= 599:
            raise exceptions.ServerError(response, content)
        else:
            raise exceptions.ConnectionError(
                response, content, "Unknown response code: #{response.code}")

    def headers(self, refresh_token=None, headers=None):
        """Default HTTP headers
        """
        token_hash = self.get_token_hash(refresh_token=refresh_token,
                                         headers=headers or {})

        return {
            "Authorization":
            ("%s %s" % (token_hash['token_type'], token_hash['access_token'])),
            "Content-Type":
            "application/json",
            "Accept":
            "application/json",
            "User-Agent":
            self.user_agent
        }

    def get(self, action, headers=None, refresh_token=None):
        """Make GET request

        Usage::

            >>> api.get("v1/payments/payment?count=1")
            >>> api.get("v1/payments/payment/PAY-1234")
        """
        return self.request(util.join_url(self.endpoint, action),
                            'GET',
                            headers=headers or {},
                            refresh_token=refresh_token)

    def post(self, action, params=None, headers=None, refresh_token=None):
        """Make POST request

        Usage::

            >>> api.post("v1/payments/payment", { 'indent': 'sale' })
            >>> api.post("v1/payments/payment/PAY-1234/execute", { 'payer_id': '1234' })

        """
        return self.request(util.join_url(self.endpoint, action),
                            'POST',
                            body=params or {},
                            headers=headers or {},
                            refresh_token=refresh_token)

    def put(self, action, params=None, headers=None, refresh_token=None):
        """Make PUT request

        Usage::

            >>> api.put("v1/invoicing/invoices/INV2-RUVR-ADWQ", { 'id': 'INV2-RUVR-ADWQ', 'status': 'DRAFT'})
        """
        return self.request(util.join_url(self.endpoint, action),
                            'PUT',
                            body=params or {},
                            headers=headers or {},
                            refresh_token=refresh_token)

    def patch(self, action, params=None, headers=None, refresh_token=None):
        """Make PATCH request

        Usage::

            >>> api.patch("v1/payments/billing-plans/P-5VH69258TN786403SVUHBM6A", { 'op': 'replace', 'path': '/merchant-preferences'})
        """
        return self.request(util.join_url(self.endpoint, action),
                            'PATCH',
                            body=params or {},
                            headers=headers or {},
                            refresh_token=refresh_token)

    def delete(self, action, headers=None, refresh_token=None):
        """Make DELETE request
        """
        return self.request(util.join_url(self.endpoint, action),
                            'DELETE',
                            headers=headers or {},
                            refresh_token=refresh_token)