@mojaloop/central-services-shared
Version:
Shared code for mojaloop central services
154 lines (137 loc) • 5.86 kB
JavaScript
// Motivation for this module is that hapi-openapi does not correctly validate headers on routes
// where the headers are specified at the path level, instead of the method level. And does not
// _appear_ to correctly validate the content of `string` + `pattern` headers at all, although the
// accuracy of this statement has not been thoroughly tested.
const { Factory: { createFSPIOPError }, Enums } = require('@mojaloop/central-services-error-handling')
const RootJoi = require('joi')
const DateExtension = require('@hapi/joi-date')
const Joi = RootJoi.extend(DateExtension)
const { API_TYPES, MAX_CONTENT_LENGTH } = require('../../../constants')
const {
checkApiType,
parseAcceptHeader,
parseContentTypeHeader,
protocolVersions,
convertSupportedVersionToExtensionList
} = require('../../headerValidation')
// Some defaults
const defaultProtocolResources = [
'parties',
'participants',
'quotes',
'transfers',
'bulkTransfers',
'bulkQuotes',
'transactionRequests',
'authorizations',
'fxQuotes',
'fxTransfers'
]
const defaultProtocolVersions = [
...protocolVersions.ONE,
...protocolVersions.TWO,
protocolVersions.anyVersion
]
const errorMessages = {
REQUESTED_VERSION_NOT_SUPPORTED: 'The Client requested an unsupported version, see extension list for supported version(s).',
INVALID_ACCEPT_HEADER: 'Invalid accept header',
INVALID_CONTENT_TYPE_HEADER: 'Invalid content-type header',
REQUIRE_ACCEPT_HEADER: 'Accept is required',
REQUIRE_CONTENT_TYPE_HEADER: 'Content-type is required',
SUPPLIED_VERSION_NOT_SUPPORTED: 'Client supplied a protocol version which is not supported by the server'
}
/**
* HAPI plugin to validate request headers per FSPIOP-API spec 1.0
*
* @param {[Object]} supportedProtocolVersions - an array of numerical protocol version strings
* supported by your implementation of the FSPIOP API e.g. ['1', '1.1']. Can
* also contain the anyVersion symbol: ['1', '1.1', anyVersion] found
* elsewhere in this module
* @param {[string]} resources - the API resources you wish to be validated. See
* defaultProtocolResources for an example.
*/
const plugin = {
name: 'fspiop-api-protocol-version-header-validator',
register: function (server, /* options: */ {
resources = defaultProtocolResources,
supportedProtocolContentVersions = defaultProtocolVersions,
supportedProtocolAcceptVersions = defaultProtocolVersions,
apiType = API_TYPES.fspiop
}) {
checkApiType(apiType)
server.ext('onPostAuth', (request, h) => {
// First, extract the resource type from the path
const resource = request.path.replace(/^\//, '').split('/')[0]
// Only validate requests for the requested resources
if (!resources.includes(resource)) {
return h.continue
}
// Always validate the accept header for a get request, or optionally if it has been
// supplied
if (request.method.toLowerCase() === 'get' || request.headers.accept) {
if (request.headers.accept === undefined) {
throw createFSPIOPError(Enums.FSPIOPErrorCodes.MISSING_ELEMENT, errorMessages.REQUIRE_ACCEPT_HEADER)
}
const accept = parseAcceptHeader(resource, request.headers.accept, apiType)
if (!accept.valid) {
throw createFSPIOPError(
Enums.FSPIOPErrorCodes.MALFORMED_SYNTAX,
errorMessages.INVALID_ACCEPT_HEADER
)
}
if (!supportedProtocolAcceptVersions.some(supportedVer => accept.versions.has(supportedVer))) {
const supportedVersionExtensionListMap = convertSupportedVersionToExtensionList(supportedProtocolAcceptVersions)
throw createFSPIOPError(
Enums.FSPIOPErrorCodes.UNACCEPTABLE_VERSION,
errorMessages.REQUESTED_VERSION_NOT_SUPPORTED,
null,
null,
supportedVersionExtensionListMap
)
}
}
const dateSchema = Joi.date().format('ddd, DD MMM YYYY HH:mm:ss [GMT]').required()
const dateHeader = request.headers.date
if (dateHeader === undefined) {
throw createFSPIOPError(Enums.FSPIOPErrorCodes.MISSING_ELEMENT, 'Missing required date header')
}
const { error } = dateSchema.validate(dateHeader)
if (error) {
throw createFSPIOPError(Enums.FSPIOPErrorCodes.MALFORMED_SYNTAX, 'Invalid date header')
}
if (request.headers['content-length'] > MAX_CONTENT_LENGTH) {
throw createFSPIOPError(Enums.FSPIOPErrorCodes.TOO_LARGE_PAYLOAD, 'Payload size is too large')
}
// Always validate the content-type header
if (request.headers['content-type'] === undefined) {
throw createFSPIOPError(Enums.FSPIOPErrorCodes.MISSING_ELEMENT, errorMessages.REQUIRE_CONTENT_TYPE_HEADER)
}
const contentType = parseContentTypeHeader(resource, request.headers['content-type'], apiType)
if (!contentType.valid) {
throw createFSPIOPError(
Enums.FSPIOPErrorCodes.MALFORMED_SYNTAX,
errorMessages.INVALID_CONTENT_TYPE_HEADER
)
}
// if (!supportedProtocolContentVersions.includes(contentType.version)) {
if (!supportedProtocolContentVersions.some(supportedVer => contentType.version === supportedVer)) {
const supportedVersionExtensionListMap = convertSupportedVersionToExtensionList(supportedProtocolContentVersions)
throw createFSPIOPError(
Enums.FSPIOPErrorCodes.UNACCEPTABLE_VERSION,
errorMessages.SUPPLIED_VERSION_NOT_SUPPORTED,
null,
null,
supportedVersionExtensionListMap
)
}
return h.continue
})
}
}
module.exports = {
plugin,
errorMessages,
defaultProtocolResources,
defaultProtocolVersions
}