Skip to content
This repository has been archived by the owner on Jan 25, 2018. It is now read-only.

Commit

Permalink
allow solitude to use and be a proxy (bug 777133)
Browse files Browse the repository at this point in the history
  • Loading branch information
Andy McKay committed Jul 27, 2012
1 parent b47e8af commit a0f9a5a
Show file tree
Hide file tree
Showing 22 changed files with 360 additions and 159 deletions.
4 changes: 2 additions & 2 deletions lib/paypal/check.py
Expand Up @@ -2,7 +2,7 @@

from django.conf import settings

from .client import Client
from .client import get_client
from .errors import PaypalError

log = logging.getLogger('s.paypal')
Expand Down Expand Up @@ -30,7 +30,7 @@ def __init__(self, paypal_id=None, paypal_permissions_token=None,
self.paypal_id = paypal_id
self.paypal_permissions_token = paypal_permissions_token
self.prices = prices
self.paypal = Client()
self.paypal = get_client()

def all(self):
self.check_id()
Expand Down
99 changes: 68 additions & 31 deletions lib/paypal/client.py
Expand Up @@ -2,6 +2,7 @@
from decimal import Decimal, InvalidOperation
import hashlib
import re
import urllib
import urlparse
import uuid

Expand All @@ -13,8 +14,8 @@
import requests

from .header import get_auth_header
from .constants import (PAYPAL_PERSONAL, PAYPAL_PERSONAL_LOOKUP,
REFUND_OK_STATUSES)
from .constants import (HEADERS_URL, HEADERS_TOKEN, PAYPAL_PERSONAL,
PAYPAL_PERSONAL_LOOKUP, REFUND_OK_STATUSES)
from .errors import errors, AuthError, PaypalDataError, PaypalError
from .urls import urls

Expand Down Expand Up @@ -46,10 +47,12 @@ def whitelist(self, urls, whitelist=None):
def nvp(self, data):
"""
Dumps a dict out into NVP pairs suitable for PayPal to consume.
Note: a urlencode will not work with chained payments. It must be
sorted and the key must not be escaped.
"""
out = []
escape = lambda k, v: '%s=%s' % (k, urlquote(v))
# This must be sorted for chained payments to work correctly.
for k, v in sorted(data.items()):
if isinstance(v, (list, tuple)):
out.extend([escape('%s(%s)' % (k, x), v_)
Expand All @@ -59,6 +62,12 @@ def nvp(self, data):

return '&'.join(out)

def prepare_data(self, data):
"""Anything else that needs to be done to prepare data for PayPal."""
if 'requestEnvelope.errorLanguage' not in data:
data['requestEnvelope.errorLanguage'] = 'en_US'
return self.nvp(data)

def receivers(self, seller_email, amount, preapproval, chains=None):
"""
Split a payment down into multiple receivers using the chains
Expand Down Expand Up @@ -110,9 +119,12 @@ def call(self, service, data, auth_token=None):
# Lookup the URL given the service.
url = urls[service]
errs = errors.get(service, errors['default'])

with statsd.timer('solitude.paypal.%s' % service):
log.info('Calling service: %s' % service)
return self._call(url, data, errs, auth_token=auth_token)
headers = self.headers(url, auth_token=auth_token)
data = self.prepare_data(data)
return self._call(url, data, headers, errs, verify=True)

def headers(self, url, auth_token=None):
"""
Expand Down Expand Up @@ -141,46 +153,36 @@ def headers(self, url, auth_token=None):

return headers

def _call(self, url, data, errs, auth_token=None):
if 'requestEnvelope.errorLanguage' not in data:
data['requestEnvelope.errorLanguage'] = 'en_US'

# Figure out the headers using the token.
headers = self.headers(url, auth_token=auth_token)

# Warning, a urlencode will not work with chained payments, it must
# be sorted and the key should not be escaped.
nvp = self.nvp(data)
def _call(self, url, data, headers, errs, verify=True):
try:
# This will check certs if settings.PAYPAL_CERT is specified.
result = requests.post(url, cert=settings.PAYPAL_CERT, data=nvp,
headers=headers, timeout=timeout,
verify=True)
result = requests.post(url, data=data, headers=headers,
verify=verify)
except AuthError, error:
log.error('Authentication error: %s' % error)
raise
except Exception, error:
log.error('HTTP Error: %s' % error)
# We'll log the actual error and then raise a Paypal error.
# That way all the calling methods only have catch a Paypal error,
# the fact that there may be say, a http error, is internal to this
# method.
raise PaypalError

response = dict(urlparse.parse_qsl(result.text))
if result.status_code > 299:
log.error('HTTP Status: %s' % result.status_code)
raise PaypalError(message='HTTP Status: %s' % result.status_code)

response = dict(urlparse.parse_qsl(result.text))
if 'error(0).errorId' in response:
id_, msg = (response['error(0).errorId'],
response['error(0).message'])
# We want some data to produce a nice error. However
# we do not want to pass everything back since this will go back in
# the REST response and that might leak data.
data = {'currency': data.get('currencyCode')}
log.error('Paypal Error (%s): %s, %s' % (id_, msg, data))
raise errs.get(id_, PaypalError)(id=id_, message=msg, data=data)
raise self.error(response, errs)

return response

def error(self, res, errs):
id_, msg = (res['error(0).errorId'], res['error(0).message'])
# We want some data to produce a nice error. However
# we do not want to pass everything back since this will go back in
# the REST response and that might leak data.
data = {'currency': res.get('currencyCode')}
log.error('Paypal Error (%s): %s' % (id_, msg))
return errs.get(id_, PaypalError)(id=id_, message=msg, data=data)

def get_permission_url(self, url, scope):
"""
Send permissions request to PayPal for privileges on
Expand Down Expand Up @@ -350,3 +352,38 @@ def get_verified(self, paypal_id):
res = self.call('get-verified', {'emailAddress': paypal_id,
'matchCriteria': 'NONE'})
return {'type': res['userInfo.accountType']}


class ClientProxy(Client):

def call(self, service, data, auth_token=None):
"""
When used as a proxy, will send slightly different data
and log differently.
"""
errs = errors.get(service, errors['default'])
with statsd.timer('solitude.proxy.paypal.%s' % service):
log.info('Calling proxy: %s' % service)
headers = self.headers(service, auth_token)
data = self.prepare_data(data)
return self._call(settings.PAYPAL_PROXY, data, headers,
errs, verify=False)

def headers(self, url, auth_token):
"""
When being used as a proxy, this will return a set of headers
that the proxy can understand.
"""
headers = {HEADERS_URL: url}
if auth_token:
headers[HEADERS_TOKEN] = urllib.urlencode(auth_token)
return headers


def get_client():
"""
Use this to get the right client and communicate with PayPal.
"""
if settings.PAYPAL_PROXY and not settings.SOLITUDE_PROXY:
return ClientProxy()
return Client()
4 changes: 4 additions & 0 deletions lib/paypal/constants.py
Expand Up @@ -87,3 +87,7 @@
'TRANSACTION_DETAILS',
'TRANSACTION_SEARCH'
]

# Header values that the proxy server will use.
HEADERS_URL = 'X_SOLITUDE_URL'
HEADERS_TOKEN = 'X_SOLITUDE_TOKEN'
4 changes: 2 additions & 2 deletions lib/paypal/resources/cached.py
Expand Up @@ -6,7 +6,7 @@
from tastypie import http
from tastypie.exceptions import ImmediateHttpResponse

from lib.paypal.client import Client
from lib.paypal.client import get_client
from lib.paypal.signals import create
from solitude.base import Resource as BaseResource

Expand Down Expand Up @@ -53,7 +53,7 @@ def obj_create(self, bundle, request, **kwargs):
if not form.is_valid():
raise self.form_errors(form)

paypal = Client()
paypal = get_client()
bundle.data = getattr(paypal, self._meta.method)(*form.args())
create.send(sender=self, bundle=bundle)
return bundle
1 change: 1 addition & 0 deletions lib/paypal/resources/ipn.py
Expand Up @@ -3,6 +3,7 @@
from lib.paypal.ipn import IPN
from lib.paypal.forms import IPNForm


class IPNResource(Resource):

class Meta(Resource.Meta):
Expand Down
4 changes: 2 additions & 2 deletions lib/paypal/resources/pay.py
@@ -1,6 +1,6 @@
from cached import Resource

from lib.paypal.client import Client
from lib.paypal.client import get_client
from lib.paypal.forms import KeyValidation, PayValidation
from lib.paypal.signals import create

Expand All @@ -16,7 +16,7 @@ def obj_create(self, bundle, request, **kwargs):
if not form.is_valid():
raise self.form_errors(form)

paypal = Client()
paypal = get_client()
# TODO: there might be a lot more we can do here.
bundle.data = paypal.get_pay_key(*form.args(), **form.kwargs())
create.send(sender=self, bundle=bundle, form=form.cleaned_data)
Expand Down
4 changes: 2 additions & 2 deletions lib/paypal/resources/permission.py
@@ -1,6 +1,6 @@
from cached import Resource

from lib.paypal.client import Client
from lib.paypal.client import get_client
from lib.paypal.forms import (CheckPermission, GetPermissionToken,
GetPermissionURL)

Expand Down Expand Up @@ -34,7 +34,7 @@ def obj_create(self, bundle, request, **kwargs):
if not form.is_valid():
raise self.form_errors(form)

paypal = Client()
paypal = get_client()
result = paypal.get_permission_token(*form.args())
seller = form.cleaned_data['seller']
seller.token = result['token']
Expand Down
4 changes: 2 additions & 2 deletions lib/paypal/resources/personal.py
@@ -1,6 +1,6 @@
from cached import Resource

from lib.paypal.client import Client
from lib.paypal.client import get_client
from lib.paypal.errors import PaypalError
from lib.paypal.forms import GetPersonal

Expand All @@ -12,7 +12,7 @@ def obj_create(self, bundle, request, **kwargs):
if not form.is_valid():
raise self.form_errors(form)

paypal = Client()
paypal = get_client()
result = getattr(paypal, self._meta.method)(*form.args())
if 'email' in result:
if form.cleaned_data['seller'].paypal_id != result['email']:
Expand Down
4 changes: 2 additions & 2 deletions lib/paypal/resources/preapproval.py
Expand Up @@ -3,7 +3,7 @@
from cached import Resource

from lib.buyers.models import Buyer, BuyerPaypal
from lib.paypal.client import Client
from lib.paypal.client import get_client
from lib.paypal.forms import PreapprovalValidation
from lib.paypal.urls import urls

Expand All @@ -20,7 +20,7 @@ def obj_create(self, bundle, request, **kwargs):
if not form.is_valid():
raise self.form_errors(form)

paypal = Client()
paypal = get_client()
bundle.data = {'key': paypal.get_preapproval_key(*form.args())['key'],
'uuid': form.cleaned_data['uuid'].uuid}
bundle.obj = self.obj()
Expand Down

0 comments on commit a0f9a5a

Please sign in to comment.