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.')
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)