2012-03-23 04:34:34 +04:00
|
|
|
import calendar
|
|
|
|
from datetime import datetime
|
|
|
|
import json
|
|
|
|
import sys
|
|
|
|
import time
|
|
|
|
|
2012-04-03 01:27:08 +04:00
|
|
|
from django import forms
|
2012-03-23 04:34:34 +04:00
|
|
|
from django.conf import settings
|
|
|
|
|
|
|
|
import jwt
|
|
|
|
from statsd import statsd
|
|
|
|
|
|
|
|
import amo
|
|
|
|
|
2012-04-03 01:27:08 +04:00
|
|
|
from .forms import PaymentForm, ContributionForm
|
|
|
|
from .models import InappConfig
|
2012-03-23 04:34:34 +04:00
|
|
|
|
|
|
|
|
|
|
|
class InappPaymentError(Exception):
|
|
|
|
"""An error occurred while processing an in-app payment."""
|
|
|
|
|
|
|
|
def __init__(self, msg, app_id=None):
|
|
|
|
self.app_id = app_id
|
|
|
|
if self.app_id:
|
|
|
|
msg = '%s (app ID=%r)' % (msg, self.app_id)
|
|
|
|
super(Exception, self).__init__(msg)
|
|
|
|
|
|
|
|
|
|
|
|
class UnknownAppError(InappPaymentError):
|
|
|
|
"""The application ID is not known."""
|
|
|
|
|
|
|
|
|
|
|
|
class RequestVerificationError(InappPaymentError):
|
|
|
|
"""The payment request could not be verified."""
|
|
|
|
|
|
|
|
|
|
|
|
class RequestExpired(InappPaymentError):
|
|
|
|
"""The payment request expired."""
|
|
|
|
|
|
|
|
|
|
|
|
class AppPaymentsDisabled(InappPaymentError):
|
|
|
|
"""In-app payment functionality for this app has been disabled."""
|
|
|
|
|
|
|
|
|
|
|
|
class AppPaymentsRevoked(InappPaymentError):
|
|
|
|
"""In-app payment functionality for this app has been revoked."""
|
|
|
|
|
|
|
|
|
|
|
|
class InvalidRequest(InappPaymentError):
|
|
|
|
"""The payment request has malformed or missing information."""
|
|
|
|
|
|
|
|
|
|
|
|
def _re_raise_as(NewExc, *args, **kw):
|
|
|
|
"""Raise a new exception using the preserved traceback of the last one."""
|
|
|
|
etype, val, tb = sys.exc_info()
|
|
|
|
raise NewExc(*args, **kw), None, tb
|
|
|
|
|
|
|
|
|
|
|
|
def verify_request(signed_request):
|
|
|
|
"""
|
|
|
|
Verifies a signed in-app payment request.
|
|
|
|
|
|
|
|
Returns the trusted JSON data from the original request.
|
|
|
|
JWT spec: http://openid.net/specs/draft-jones-json-web-token-07.html
|
|
|
|
|
|
|
|
One extra key, _config, is added to the returned JSON.
|
|
|
|
This is the InappConfig instance.
|
|
|
|
|
|
|
|
When there's an error, an exception derived from InappPaymentError
|
|
|
|
will be raised.
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
signed_request = str(signed_request) # must be base64 encoded bytes
|
|
|
|
except UnicodeEncodeError, exc:
|
|
|
|
_re_raise_as(RequestVerificationError,
|
|
|
|
'Non-ascii payment JWT: %s' % exc)
|
|
|
|
try:
|
|
|
|
app_req = jwt.decode(signed_request, verify=False)
|
|
|
|
except jwt.DecodeError, exc:
|
|
|
|
_re_raise_as(RequestVerificationError, 'Invalid payment JWT: %s' % exc)
|
|
|
|
try:
|
|
|
|
app_req = json.loads(app_req)
|
|
|
|
except ValueError, exc:
|
|
|
|
_re_raise_as(RequestVerificationError,
|
|
|
|
'Invalid JSON for payment JWT: %s' % exc)
|
|
|
|
|
|
|
|
app_id = app_req.get('iss')
|
|
|
|
|
|
|
|
# Verify the signature:
|
|
|
|
try:
|
|
|
|
cfg = InappConfig.objects.get(public_key=app_id)
|
|
|
|
except InappConfig.DoesNotExist:
|
|
|
|
_re_raise_as(UnknownAppError, 'App does not exist', app_id=app_id)
|
|
|
|
if cfg.status == amo.INAPP_STATUS_REVOKED:
|
|
|
|
raise AppPaymentsRevoked('Payments revoked', app_id=app_id)
|
|
|
|
elif cfg.status != amo.INAPP_STATUS_ACTIVE:
|
|
|
|
raise AppPaymentsDisabled('Payments disabled (status=%s)'
|
|
|
|
% (cfg.status), app_id=app_id)
|
|
|
|
app_req['_config'] = cfg
|
|
|
|
|
|
|
|
# TODO(Kumar) see bug 736573 for HSM integration
|
|
|
|
try:
|
|
|
|
with statsd.timer('inapp_pay.verify'):
|
|
|
|
jwt.decode(signed_request, cfg.private_key, verify=True)
|
|
|
|
except jwt.DecodeError, exc:
|
|
|
|
_re_raise_as(RequestVerificationError,
|
|
|
|
'Payment verification failed: %s' % exc,
|
|
|
|
app_id=app_id)
|
|
|
|
|
|
|
|
# Check timestamps:
|
|
|
|
try:
|
|
|
|
expires = float(str(app_req.get('exp')))
|
|
|
|
issued = float(str(app_req.get('iat')))
|
|
|
|
except ValueError:
|
|
|
|
_re_raise_as(RequestVerificationError,
|
|
|
|
'Payment JWT had an invalid exp (%r) or iat (%r) '
|
|
|
|
% (app_req.get('exp'), app_req.get('iat')),
|
|
|
|
app_id=app_id)
|
|
|
|
now = calendar.timegm(time.gmtime())
|
|
|
|
if expires < now:
|
|
|
|
raise RequestExpired('Payment JWT expired: %s UTC < %s UTC '
|
|
|
|
'(issued at %s UTC)'
|
|
|
|
% (datetime.utcfromtimestamp(expires),
|
|
|
|
datetime.utcfromtimestamp(now),
|
|
|
|
datetime.utcfromtimestamp(issued)),
|
|
|
|
app_id=app_id)
|
|
|
|
if issued < (now - 3600): # issued more than an hour ago
|
|
|
|
raise RequestExpired('Payment JWT iat expired: %s UTC < %s UTC '
|
|
|
|
% (datetime.utcfromtimestamp(issued),
|
|
|
|
datetime.utcfromtimestamp(now)),
|
|
|
|
app_id=app_id)
|
|
|
|
try:
|
|
|
|
not_before = float(str(app_req.get('nbf')))
|
|
|
|
except ValueError:
|
|
|
|
app_req['nbf'] = None # this field is optional
|
|
|
|
else:
|
|
|
|
about_now = now + 300 # pad 5 minutes for clock skew
|
|
|
|
if not_before >= about_now:
|
|
|
|
raise InvalidRequest('Payment JWT cannot be processed before '
|
|
|
|
'%s UTC (nbf must be < %s UTC)'
|
|
|
|
% (datetime.utcfromtimestamp(not_before),
|
|
|
|
datetime.utcfromtimestamp(about_now)),
|
|
|
|
app_id=app_id)
|
|
|
|
|
|
|
|
# Check JWT audience.
|
|
|
|
audience = app_req.get('aud', None)
|
|
|
|
if not audience:
|
|
|
|
raise InvalidRequest('Payment JWT is missing aud (audience)',
|
|
|
|
app_id=app_id)
|
2012-04-03 01:27:08 +04:00
|
|
|
if audience != settings.INAPP_MARKET_ID:
|
2012-03-23 04:34:34 +04:00
|
|
|
raise InvalidRequest('Payment JWT aud (audience) must be set to %r; '
|
2012-04-03 01:27:08 +04:00
|
|
|
'got: %r' % (settings.INAPP_MARKET_ID,
|
2012-03-23 04:34:34 +04:00
|
|
|
audience),
|
|
|
|
app_id=app_id)
|
|
|
|
|
|
|
|
request = app_req.get('request', None)
|
2012-04-03 01:27:08 +04:00
|
|
|
|
|
|
|
# Check payment details.
|
2012-03-23 04:34:34 +04:00
|
|
|
if not isinstance(request, dict):
|
|
|
|
raise InvalidRequest('Payment JWT is missing request dict: %r'
|
|
|
|
% request, app_id=app_id)
|
|
|
|
for key in ('price', 'currency', 'name', 'description'):
|
|
|
|
if key not in request:
|
|
|
|
raise InvalidRequest('Payment JWT is missing request[%r]'
|
|
|
|
% key, app_id=app_id)
|
|
|
|
|
2012-04-03 01:27:08 +04:00
|
|
|
# Validate values for model integrity.
|
|
|
|
key_trans = {'app_data': 'productdata', 'amount': 'price'}
|
|
|
|
for form in (PaymentForm(), ContributionForm()):
|
|
|
|
for name, field in form.fields.items():
|
|
|
|
req_field = key_trans.get(name, name)
|
|
|
|
value = request[req_field]
|
|
|
|
try:
|
|
|
|
field.clean(value)
|
|
|
|
except forms.ValidationError, exc:
|
|
|
|
_re_raise_as(InvalidRequest,
|
|
|
|
u'request[%r] is invalid: %s' % (req_field, exc))
|
|
|
|
|
2012-03-23 04:34:34 +04:00
|
|
|
# TODO(Kumar) Prevent repeat payments with nonce (bug 738776).
|
|
|
|
return app_req
|