addons-server/mkt/inapp_pay/verify.py

200 строки
7.2 KiB
Python

import calendar
from datetime import datetime
import json
import sys
import time
from django import forms
from django.conf import settings
import jwt
from statsd import statsd
import amo
from market.models import Price
from .forms import PaymentForm, ContributionForm
from .models import InappConfig
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,
addon__status=amo.STATUS_PUBLIC)
except InappConfig.DoesNotExist:
_re_raise_as(UnknownAppError, 'App does not exist or is not public',
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
try:
with statsd.timer('inapp_pay.verify'):
jwt.decode(signed_request, cfg.get_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)
if audience != settings.INAPP_MARKET_ID:
raise InvalidRequest('Payment JWT aud (audience) must be set to %r; '
'got: %r' % (settings.INAPP_MARKET_ID,
audience),
app_id=app_id)
request = app_req.get('request', None)
# Check payment details.
if not isinstance(request, dict):
raise InvalidRequest('Payment JWT is missing request dict: %r'
% request, app_id=app_id)
for key in ('priceTier', 'name', 'description'):
if key not in request:
raise InvalidRequest('Payment JWT is missing request[%r]'
% key, app_id=app_id)
# Validate values for model integrity.
key_trans = {'app_data': 'productdata'}
for form in (PaymentForm(), ContributionForm()):
for name, field in form.fields.items():
if name in ('amount', 'currency'):
# Since we're using price tiers we don't need to complain
# about missing amount (which is price in the request)
# or currency.
continue
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))
# Validate the price tier.
try:
if not Price.objects.filter(pk=request['priceTier']).exists():
raise InvalidRequest(
u'priceTier:%s is not a supported price tier. Consult the '
u'docs for all supported tiers: '
u'https://developer.mozilla.org/en/Apps/In-app_payments'
% request['priceTier'])
except ValueError:
_re_raise_as(InvalidRequest,
u'priceTier:%r is not a valid number'
% request['priceTier'])
return app_req