Add validateGQLRequest method in PaymentsManager (#426)

* Refactor GQL validation for payment

* Export payment util const PAYMENT_HEADER_KEY

* Check payment header only for paid queries
This commit is contained in:
Nabarun Gogoi 2023-10-12 10:58:13 +05:30 committed by GitHub
parent caa8da7090
commit 8515630f49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -24,7 +24,7 @@ const log = debug('laconic:payments');
const IntrospectionQuery = 'IntrospectionQuery'; const IntrospectionQuery = 'IntrospectionQuery';
const IntrospectionQuerySelection = '__schema'; const IntrospectionQuerySelection = '__schema';
const PAYMENT_HEADER_KEY = 'x-payment'; export const PAYMENT_HEADER_KEY = 'x-payment';
const PAYMENT_HEADER_REGEX = /vhash:(.*),vsig:(.*)/; const PAYMENT_HEADER_REGEX = /vhash:(.*),vsig:(.*)/;
const ERR_FREE_QUOTA_EXHUASTED = 'Free quota exhausted'; const ERR_FREE_QUOTA_EXHUASTED = 'Free quota exhausted';
@ -355,25 +355,85 @@ export const paymentsPlugin = (paymentsManager?: PaymentsManager): ApolloServerP
const querySelections = requestContext.operation?.selectionSet.selections const querySelections = requestContext.operation?.selectionSet.selections
.map((selection: any) => (selection as FieldNode).name.value); .map((selection: any) => (selection as FieldNode).name.value);
// Continue if it's an introspection query for schema try {
await validateGQLRequest(
paymentsManager,
{
querySelections,
operationName: requestContext.operationName,
paymentHeader: requestContext.request.http?.headers.get(PAYMENT_HEADER_KEY)
}
);
return null;
} catch (error) {
if (error instanceof GQLPaymentError) {
return {
errors: [{ message: error.message }],
http: new HTTPResponse(undefined, {
headers: requestContext.response?.http?.headers,
status: error.status
})
};
}
throw error;
}
}
};
}
};
};
class GQLPaymentError extends Error {
status: number;
constructor (message: string, status: number) {
super(message);
this.status = status;
}
}
export const validateGQLRequest = async (
paymentsManager: PaymentsManager,
{ operationName, querySelections, paymentHeader }: {
operationName?: string | null;
querySelections?: string[];
paymentHeader?: string | null;
}
): Promise<boolean> => {
// Return true if it's an introspection query for schema
// (made by ApolloServer playground / default landing page) // (made by ApolloServer playground / default landing page)
if ( if (
requestContext.operationName === IntrospectionQuery && operationName === IntrospectionQuery &&
querySelections && querySelections.length === 1 && querySelections && querySelections.length === 1 &&
querySelections[0] === IntrospectionQuerySelection querySelections[0] === IntrospectionQuerySelection
) { ) {
return null; return true;
} }
const paymentHeader = requestContext.request.http?.headers.get(PAYMENT_HEADER_KEY); const paidQuerySelections = (querySelections ?? []).filter(querySelection => {
if (paymentHeader == null) { if (paymentsManager.freeQueriesList.includes(querySelection)) {
return { return false;
errors: [{ message: ERR_HEADER_MISSING }], }
http: new HTTPResponse(undefined, {
headers: requestContext.response?.http?.headers, // Serve a query for free if rate is not configured
status: HTTP_CODE_BAD_REQUEST const configuredQueryCost = paymentsManager.queryRates[querySelection];
}) if (configuredQueryCost === undefined) {
}; log(`Query rate not configured for "${querySelection}", serving free query`);
return false;
}
return true;
});
// Return true if no paid queries exist
if (!paidQuerySelections.length) {
return true;
}
if (!paymentHeader) {
throw new GQLPaymentError(ERR_HEADER_MISSING, HTTP_CODE_BAD_REQUEST);
} }
let vhash: string, vsig: string; let vhash: string, vsig: string;
@ -382,49 +442,19 @@ export const paymentsPlugin = (paymentsManager?: PaymentsManager): ApolloServerP
if (match) { if (match) {
[, vhash, vsig] = match; [, vhash, vsig] = match;
} else { } else {
return { throw new GQLPaymentError(ERR_INVALID_PAYMENT_HEADER, HTTP_CODE_BAD_REQUEST);
errors: [{ message: ERR_INVALID_PAYMENT_HEADER }],
http: new HTTPResponse(undefined, {
headers: requestContext.response?.http?.headers,
status: HTTP_CODE_BAD_REQUEST
})
};
} }
const signerAddress = nitroUtils.getSignerAddress(vhash, vsig); const signerAddress = nitroUtils.getSignerAddress(vhash, vsig);
// eslint-disable-next-line @typescript-eslint/no-unused-vars for await (const querySelection of paidQuerySelections) {
for await (const querySelection of querySelections ?? []) {
if (paymentsManager.freeQueriesList.includes(querySelection)) {
continue;
}
// Serve a query for free if rate is not configured
const configuredQueryCost = paymentsManager.queryRates[querySelection];
if (configuredQueryCost === undefined) {
log(`Query rate not configured for "${querySelection}", serving a free query to ${signerAddress}`);
continue;
}
const [allowRequest, rejectionMessage] = await paymentsManager.allowRequest(vhash, signerAddress, querySelection); const [allowRequest, rejectionMessage] = await paymentsManager.allowRequest(vhash, signerAddress, querySelection);
if (!allowRequest) { if (!allowRequest) {
const failResponse: GraphQLResponse = { throw new GQLPaymentError(rejectionMessage, HTTP_CODE_PAYMENT_REQUIRED);
errors: [{ message: rejectionMessage }],
http: new HTTPResponse(undefined, {
headers: requestContext.response?.http?.headers,
status: HTTP_CODE_PAYMENT_REQUIRED
})
};
return failResponse;
} }
} }
return null; return true;
}
};
}
};
}; };
// Helper method to modify a given JsonRpcProvider to make payment for required methods // Helper method to modify a given JsonRpcProvider to make payment for required methods