Adds receipt validation endpoint

This commit is contained in:
Florent Vilmart
2016-02-19 13:06:02 -05:00
parent 02566f0906
commit 9c477907bf
6 changed files with 358 additions and 5 deletions

View File

@@ -105,9 +105,9 @@ ExportAdapter.prototype.redirectClassNameForKey = function(className, key) {
// Returns a promise that resolves to the new schema.
// This does not update this.schema, because in a situation like a
// batch request, that could confuse other users of the schema.
ExportAdapter.prototype.validateObject = function(className, object) {
ExportAdapter.prototype.validateObject = function(className, object, query) {
return this.loadSchema().then((schema) => {
return schema.validateObject(className, object);
return schema.validateObject(className, object, query);
});
};

View File

@@ -90,7 +90,7 @@ RestWrite.prototype.execute = function() {
// Validates this operation against the schema.
RestWrite.prototype.validateSchema = function() {
return this.config.database.validateObject(this.className, this.data);
return this.config.database.validateObject(this.className, this.data, this.query);
};
// Runs any beforeSave triggers against this operation.

View File

@@ -59,9 +59,23 @@ var defaultColumns = {
"sessionToken": {type:'String'},
"expiresAt": {type:'Date'},
"createdWith": {type:'Object'}
},
_Product: {
"productIdentifier": {type:'String'},
"download": {type:'File'},
"downloadName": {type:'String'},
"icon": {type:'File'},
"order": {type:'Number'},
"title": {type:'String'},
"subtile": {type:'String'},
}
};
var requiredColumns = {
_Product: ["productIdentifier", "icon", "order", "title", "subtitle"]
}
// Valid classes must:
// Be one of _User, _Installation, _Role, _Session OR
// Be a join table OR
@@ -75,6 +89,7 @@ function classNameIsValid(className) {
className === '_Session' ||
className === '_SCHEMA' || //TODO: remove this, as _SCHEMA is not a valid class name for storing Parse Objects.
className === '_Role' ||
className === '_Product' ||
joinClassRegex.test(className) ||
//Class names have the same constraints as field names, but also allow the previous additional names.
fieldNameIsValid(className)
@@ -565,7 +580,7 @@ function thenValidateField(schemaPromise, className, key, type) {
// Validates an object provided in REST format.
// Returns a promise that resolves to the new schema if this object is
// valid.
Schema.prototype.validateObject = function(className, object) {
Schema.prototype.validateObject = function(className, object, query) {
var geocount = 0;
var promise = this.validateClassName(className);
for (var key in object) {
@@ -586,9 +601,48 @@ Schema.prototype.validateObject = function(className, object) {
}
promise = thenValidateField(promise, className, key, expected);
}
promise = thenValidateRequiredColumns(promise, className, object, query);
return promise;
};
// Given a schema promise, construct another schema promise that
// validates this field once the schema loads.
function thenValidateRequiredColumns(schemaPromise, className, object, query) {
return schemaPromise.then((schema) => {
return schema.validateRequiredColumns(className, object, query);
});
}
// Validates that all the properties are set for the object
Schema.prototype.validateRequiredColumns = function(className, object, query) {
var columns = requiredColumns[className];
if (!columns || columns.length == 0) {
return Promise.resolve(this);
}
var missingColumns = columns.filter(function(column){
if (query && query.objectId) {
if (object[column] && typeof object[column] === "object") {
// Trying to delete a required column
return object[column].__op == 'Delete';
}
// Not trying to do anything there
return false;
}
return !object[column]
});
if (missingColumns.length > 0) {
throw new Parse.Error(
Parse.Error.INCORRECT_TYPE,
missingColumns[0]+' is required.');
}
return Promise.resolve(this);
}
// Validates an operation passes class-level-permissions set in the schema
Schema.prototype.validatePermission = function(className, aclGroup, operation) {
if (!this.perms[className] || !this.perms[className][operation]) {

View File

@@ -148,7 +148,8 @@ function ParseServer(args) {
require('./functions'),
require('./schemas'),
new PushController(pushAdapter).getExpressRouter(),
new LoggerController(loggerAdapter).getExpressRouter()
new LoggerController(loggerAdapter).getExpressRouter(),
require('./validate_purchase')
];
if (process.env.PARSE_EXPERIMENTAL_CONFIG_ENABLED || process.env.TESTING) {
routers.push(require('./global_config'));

90
src/validate_purchase.js Normal file
View File

@@ -0,0 +1,90 @@
var PromiseRouter = require("./PromiseRouter");
var request = require("request");
var rest = require("./rest");
var Auth = require("./Auth");
const IAP_SANDBOX_URL = "https://sandbox.itunes.apple.com/verifyReceipt";
const IAP_PRODUCTION_URL = "https://buy.itunes.apple.com/verifyReceipt";
const APP_STORE_ERRORS = {
21000: "The App Store could not read the JSON object you provided.",
21002: "The data in the receipt-data property was malformed or missing.",
21003: "The receipt could not be authenticated.",
21004: "The shared secret you provided does not match the shared secret on file for your account.",
21005: "The receipt server is not currently available.",
21006: "This receipt is valid but the subscription has expired.",
21007: "This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead.",
21008: "This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead."
}
function appStoreError(status) {
status = parseInt(status);
var errorString = APP_STORE_ERRORS[status] || "unknown error.";
return { status: status, error: errorString }
}
function validateWithAppStore(url, receipt) {
return new Promise(function(fulfill, reject) {
request.post({
url: url,
body: { "receipt-data": receipt },
json: true,
}, function(err, res, body) {
var status = body.status;
if (status == 0) {
// No need to pass anything, status is OK
return fulfill();
}
// receipt is from test and should go to test
if (status == 21007) {
return validateWithAppStore(IAP_SANDBOX_URL);
}
return reject(body);
});
});
}
function getFileForProductIdentifier(productIdentifier, req) {
return rest.find(req.config, req.auth, '_Product', { productIdentifier: productIdentifier }).then(function(result){
const products = result.results;
if (!products || products.length != 1) {
// Error not found or too many
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.')
}
var download = products[0].download;
return Promise.resolve({response: download});
});
}
function handleRequest(req) {
let receipt = req.body.receipt;
const productIdentifier = req.body.productIdentifier;
if (!receipt || ! productIdentifier) {
// TODO: Error, malformed request
throw new Parse.Error(Parse.Error.INVALID_JSON, "missing receipt or productIdentifier");
}
// Transform the object if there
// otherwise assume it's in Base64 already
if (typeof receipt == "object") {
if (receipt["__type"] == "Bytes") {
receipt = receipt.base64;
}
}
if (process.env.NODE_ENV == "test" && req.body.bypassAppStoreValidation) {
return getFileForProductIdentifier(productIdentifier, req);
}
return validateWithAppStore(IAP_PRODUCTION_URL, receipt).then( () => {
return getFileForProductIdentifier(productIdentifier, req);
}, (error) => {
return Promise.resolve({response: appStoreError(error.status) });
});
}
var router = new PromiseRouter();
router.route("POST","/validate_purchase", handleRequest);
module.exports = router;