447 lines
15 KiB
JavaScript
447 lines
15 KiB
JavaScript
'use strict';
|
|
|
|
const Os = require('os');
|
|
|
|
const Somever = require('@hapi/somever');
|
|
const Validate = require('@hapi/validate');
|
|
|
|
|
|
const internals = {};
|
|
|
|
|
|
exports.symbol = Symbol('hapi-response');
|
|
|
|
|
|
exports.apply = function (type, options, ...message) {
|
|
|
|
const result = internals[type].validate(options);
|
|
|
|
if (result.error) {
|
|
throw new Error(`Invalid ${type} options ${message.length ? '(' + message.join(' ') + ')' : ''} ${result.error.annotate()}`);
|
|
}
|
|
|
|
return result.value;
|
|
};
|
|
|
|
|
|
exports.enable = function (options) {
|
|
|
|
const settings = options ? Object.assign({}, options) : {}; // Shallow cloned
|
|
|
|
if (settings.security === true) {
|
|
settings.security = {};
|
|
}
|
|
|
|
if (settings.cors === true) {
|
|
settings.cors = {};
|
|
}
|
|
|
|
return settings;
|
|
};
|
|
|
|
exports.versionMatch = (version, range) => Somever.match(version, range, { includePrerelease: true });
|
|
|
|
internals.access = Validate.object({
|
|
entity: Validate.valid('user', 'app', 'any'),
|
|
scope: [false, Validate.array().items(Validate.string()).single().min(1)]
|
|
});
|
|
|
|
|
|
internals.auth = Validate.alternatives([
|
|
Validate.string(),
|
|
internals.access.keys({
|
|
mode: Validate.valid('required', 'optional', 'try'),
|
|
strategy: Validate.string(),
|
|
strategies: Validate.array().items(Validate.string()).min(1),
|
|
access: Validate.array().items(internals.access.min(1)).single().min(1),
|
|
payload: [
|
|
Validate.valid('required', 'optional'),
|
|
Validate.boolean()
|
|
]
|
|
})
|
|
.without('strategy', 'strategies')
|
|
.without('access', ['scope', 'entity'])
|
|
]);
|
|
|
|
|
|
internals.event = Validate.object({
|
|
method: Validate.array().items(Validate.function()).single(),
|
|
options: Validate.object({
|
|
before: Validate.array().items(Validate.string()).single(),
|
|
after: Validate.array().items(Validate.string()).single(),
|
|
bind: Validate.any(),
|
|
sandbox: Validate.valid('server', 'plugin'),
|
|
timeout: Validate.number().integer().min(1)
|
|
})
|
|
.default({})
|
|
});
|
|
|
|
|
|
internals.exts = Validate.array()
|
|
.items(internals.event.keys({ type: Validate.string().required() })).single();
|
|
|
|
|
|
internals.failAction = Validate.alternatives([
|
|
Validate.valid('error', 'log', 'ignore'),
|
|
Validate.function()
|
|
])
|
|
.default('error');
|
|
|
|
|
|
internals.routeBase = Validate.object({
|
|
app: Validate.object().allow(null),
|
|
auth: internals.auth.allow(false),
|
|
bind: Validate.object().allow(null),
|
|
cache: Validate.object({
|
|
expiresIn: Validate.number(),
|
|
expiresAt: Validate.string(),
|
|
privacy: Validate.valid('default', 'public', 'private'),
|
|
statuses: Validate.array().items(Validate.number().integer().min(200)).min(1).single().default([200, 204]),
|
|
otherwise: Validate.string().default('no-cache')
|
|
})
|
|
.allow(false)
|
|
.default(),
|
|
compression: Validate.object()
|
|
.pattern(/.+/, Validate.object())
|
|
.default(),
|
|
cors: Validate.object({
|
|
origin: Validate.array().min(1).allow('ignore').default(['*']),
|
|
maxAge: Validate.number().default(86400),
|
|
headers: Validate.array().items(Validate.string()).default(['Accept', 'Authorization', 'Content-Type', 'If-None-Match']),
|
|
additionalHeaders: Validate.array().items(Validate.string()).default([]),
|
|
exposedHeaders: Validate.array().items(Validate.string()).default(['WWW-Authenticate', 'Server-Authorization']),
|
|
additionalExposedHeaders: Validate.array().items(Validate.string()).default([]),
|
|
credentials: Validate.boolean().when('origin', { is: 'ignore', then: false }).default(false),
|
|
preflightStatusCode: Validate.valid(200, 204).default(200)
|
|
})
|
|
.allow(false, true)
|
|
.default(false),
|
|
ext: Validate.object({
|
|
onPreAuth: Validate.array().items(internals.event).single(),
|
|
onCredentials: Validate.array().items(internals.event).single(),
|
|
onPostAuth: Validate.array().items(internals.event).single(),
|
|
onPreHandler: Validate.array().items(internals.event).single(),
|
|
onPostHandler: Validate.array().items(internals.event).single(),
|
|
onPreResponse: Validate.array().items(internals.event).single(),
|
|
onPostResponse: Validate.array().items(internals.event).single()
|
|
})
|
|
.default({}),
|
|
files: Validate.object({
|
|
relativeTo: Validate.string().pattern(/^([\/\.])|([A-Za-z]:\\)|(\\\\)/).default('.')
|
|
})
|
|
.default(),
|
|
json: Validate.object({
|
|
replacer: Validate.alternatives(Validate.function(), Validate.array()).allow(null).default(null),
|
|
space: Validate.number().allow(null).default(null),
|
|
suffix: Validate.string().allow(null).default(null),
|
|
escape: Validate.boolean().default(false)
|
|
})
|
|
.default(),
|
|
log: Validate.object({
|
|
collect: Validate.boolean().default(false)
|
|
})
|
|
.default(),
|
|
payload: Validate.object({
|
|
output: Validate.valid('data', 'stream', 'file').default('data'),
|
|
parse: Validate.boolean().allow('gunzip').default(true),
|
|
multipart: Validate.object({
|
|
output: Validate.valid('data', 'stream', 'file', 'annotated').required()
|
|
})
|
|
.default(false)
|
|
.allow(true, false),
|
|
allow: Validate.array().items(Validate.string()).single(),
|
|
override: Validate.string(),
|
|
protoAction: Validate.valid('error', 'remove', 'ignore').default('error'),
|
|
maxBytes: Validate.number().integer().positive().default(1024 * 1024),
|
|
maxParts: Validate.number().integer().positive().default(1000),
|
|
uploads: Validate.string().default(Os.tmpdir()),
|
|
failAction: internals.failAction,
|
|
timeout: Validate.number().integer().positive().allow(false).default(10 * 1000),
|
|
defaultContentType: Validate.string().default('application/json'),
|
|
compression: Validate.object()
|
|
.pattern(/.+/, Validate.object())
|
|
.default()
|
|
})
|
|
.default(),
|
|
plugins: Validate.object(),
|
|
response: Validate.object({
|
|
disconnectStatusCode: Validate.number().integer().min(400).default(499),
|
|
emptyStatusCode: Validate.valid(200, 204).default(204),
|
|
failAction: internals.failAction,
|
|
modify: Validate.boolean(),
|
|
options: Validate.object(),
|
|
ranges: Validate.boolean().default(true),
|
|
sample: Validate.number().min(0).max(100).when('modify', { then: Validate.forbidden() }),
|
|
schema: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(true, false),
|
|
status: Validate.object().pattern(/\d\d\d/, Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(true, false))
|
|
})
|
|
.default(),
|
|
security: Validate.object({
|
|
hsts: Validate.alternatives([
|
|
Validate.object({
|
|
maxAge: Validate.number(),
|
|
includeSubdomains: Validate.boolean(),
|
|
includeSubDomains: Validate.boolean(),
|
|
preload: Validate.boolean()
|
|
}),
|
|
Validate.boolean(),
|
|
Validate.number()
|
|
])
|
|
.default(15768000),
|
|
xframe: Validate.alternatives([
|
|
Validate.boolean(),
|
|
Validate.valid('sameorigin', 'deny'),
|
|
Validate.object({
|
|
rule: Validate.valid('sameorigin', 'deny', 'allow-from'),
|
|
source: Validate.string()
|
|
})
|
|
])
|
|
.default('deny'),
|
|
xss: Validate.valid('enabled', 'disabled', false).default('disabled'),
|
|
noOpen: Validate.boolean().default(true),
|
|
noSniff: Validate.boolean().default(true),
|
|
referrer: Validate.alternatives([
|
|
Validate.boolean().valid(false),
|
|
Validate.valid('', 'no-referrer', 'no-referrer-when-downgrade',
|
|
'unsafe-url', 'same-origin', 'origin', 'strict-origin',
|
|
'origin-when-cross-origin', 'strict-origin-when-cross-origin')
|
|
])
|
|
.default(false)
|
|
})
|
|
.allow(null, false, true)
|
|
.default(false),
|
|
state: Validate.object({
|
|
parse: Validate.boolean().default(true),
|
|
failAction: internals.failAction
|
|
})
|
|
.default(),
|
|
timeout: Validate.object({
|
|
socket: Validate.number().integer().positive().allow(false),
|
|
server: Validate.number().integer().positive().allow(false).default(false)
|
|
})
|
|
.default(),
|
|
validate: Validate.object({
|
|
headers: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, true),
|
|
params: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, true),
|
|
query: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, false, true),
|
|
payload: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, false, true),
|
|
state: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, false, true),
|
|
failAction: internals.failAction,
|
|
errorFields: Validate.object(),
|
|
options: Validate.object().default(),
|
|
validator: Validate.object()
|
|
})
|
|
.default()
|
|
});
|
|
|
|
|
|
internals.server = Validate.object({
|
|
address: Validate.string().hostname(),
|
|
app: Validate.object().allow(null),
|
|
autoListen: Validate.boolean(),
|
|
cache: Validate.allow(null), // Validated elsewhere
|
|
compression: Validate.object({
|
|
minBytes: Validate.number().min(1).integer().default(1024)
|
|
})
|
|
.allow(false)
|
|
.default(),
|
|
debug: Validate.object({
|
|
request: Validate.array().items(Validate.string()).single().allow(false).default(['implementation']),
|
|
log: Validate.array().items(Validate.string()).single().allow(false)
|
|
})
|
|
.allow(false)
|
|
.default(),
|
|
host: Validate.string().hostname().allow(null),
|
|
info: Validate.object({
|
|
remote: Validate.boolean().default(false)
|
|
})
|
|
.default({}),
|
|
listener: Validate.any(),
|
|
load: Validate.object({
|
|
sampleInterval: Validate.number().integer().min(0).default(0)
|
|
})
|
|
.unknown()
|
|
.default(),
|
|
mime: Validate.object().empty(null).default(),
|
|
operations: Validate.object({
|
|
cleanStop: Validate.boolean().default(true)
|
|
})
|
|
.default(),
|
|
plugins: Validate.object(),
|
|
port: Validate.alternatives([
|
|
Validate.number().integer().min(0), // TCP port
|
|
Validate.string().pattern(/\//), // Unix domain socket
|
|
Validate.string().pattern(/^\\\\\.\\pipe\\/) // Windows named pipe
|
|
])
|
|
.allow(null),
|
|
query: Validate.object({
|
|
parser: Validate.function()
|
|
})
|
|
.default(),
|
|
router: Validate.object({
|
|
isCaseSensitive: Validate.boolean().default(true),
|
|
stripTrailingSlash: Validate.boolean().default(false)
|
|
})
|
|
.default(),
|
|
routes: internals.routeBase.default(),
|
|
state: Validate.object(), // Cookie defaults
|
|
tls: Validate.alternatives([
|
|
Validate.object().allow(null),
|
|
Validate.boolean()
|
|
]),
|
|
uri: Validate.string().pattern(/[^/]$/)
|
|
});
|
|
|
|
|
|
internals.vhost = Validate.alternatives([
|
|
Validate.string().hostname(),
|
|
Validate.array().items(Validate.string().hostname()).min(1)
|
|
]);
|
|
|
|
|
|
internals.handler = Validate.alternatives([
|
|
Validate.function(),
|
|
Validate.object().length(1)
|
|
]);
|
|
|
|
|
|
internals.route = Validate.object({
|
|
method: Validate.string().pattern(/^[a-zA-Z0-9!#\$%&'\*\+\-\.^_`\|~]+$/).required(),
|
|
path: Validate.string().required(),
|
|
rules: Validate.object(),
|
|
vhost: internals.vhost,
|
|
|
|
// Validated in route construction
|
|
|
|
handler: Validate.any(),
|
|
options: Validate.any(),
|
|
config: Validate.any() // Backwards compatibility
|
|
})
|
|
.without('config', 'options');
|
|
|
|
|
|
internals.pre = [
|
|
Validate.function(),
|
|
Validate.object({
|
|
method: Validate.alternatives(Validate.string(), Validate.function()).required(),
|
|
assign: Validate.string(),
|
|
mode: Validate.valid('serial', 'parallel'),
|
|
failAction: internals.failAction
|
|
})
|
|
];
|
|
|
|
|
|
internals.routeConfig = internals.routeBase.keys({
|
|
description: Validate.string(),
|
|
id: Validate.string(),
|
|
isInternal: Validate.boolean(),
|
|
notes: [
|
|
Validate.string(),
|
|
Validate.array().items(Validate.string())
|
|
],
|
|
pre: Validate.array().items(...internals.pre.concat(Validate.array().items(...internals.pre).min(1))),
|
|
tags: [
|
|
Validate.string(),
|
|
Validate.array().items(Validate.string())
|
|
]
|
|
});
|
|
|
|
|
|
internals.cacheConfig = Validate.alternatives([
|
|
Validate.function(),
|
|
Validate.object({
|
|
name: Validate.string().invalid('_default'),
|
|
shared: Validate.boolean(),
|
|
provider: [
|
|
Validate.function(),
|
|
{
|
|
constructor: Validate.function().required(),
|
|
options: Validate.object({
|
|
partition: Validate.string().default('hapi-cache')
|
|
})
|
|
.unknown() // Catbox client validates other keys
|
|
.default({})
|
|
}
|
|
],
|
|
engine: Validate.object()
|
|
})
|
|
.xor('provider', 'engine')
|
|
]);
|
|
|
|
|
|
internals.cache = Validate.array().items(internals.cacheConfig).min(1).single();
|
|
|
|
|
|
internals.cachePolicy = Validate.object({
|
|
cache: Validate.string().allow(null).allow(''),
|
|
segment: Validate.string(),
|
|
shared: Validate.boolean()
|
|
})
|
|
.unknown(); // Catbox policy validates other keys
|
|
|
|
|
|
internals.method = Validate.object({
|
|
bind: Validate.object().allow(null),
|
|
generateKey: Validate.function(),
|
|
cache: internals.cachePolicy
|
|
});
|
|
|
|
|
|
internals.methodObject = Validate.object({
|
|
name: Validate.string().required(),
|
|
method: Validate.function().required(),
|
|
options: Validate.object()
|
|
});
|
|
|
|
|
|
internals.register = Validate.object({
|
|
once: true,
|
|
routes: Validate.object({
|
|
prefix: Validate.string().pattern(/^\/.+/),
|
|
vhost: internals.vhost
|
|
})
|
|
.default({})
|
|
});
|
|
|
|
|
|
internals.semver = Validate.string();
|
|
|
|
|
|
internals.plugin = internals.register.keys({
|
|
options: Validate.any(),
|
|
plugin: Validate.object({
|
|
register: Validate.function().required(),
|
|
name: Validate.string().when('pkg.name', { is: Validate.exist(), otherwise: Validate.required() }),
|
|
version: Validate.string(),
|
|
multiple: Validate.boolean().default(false),
|
|
dependencies: [
|
|
Validate.array().items(Validate.string()).single(),
|
|
Validate.object().pattern(/.+/, internals.semver)
|
|
],
|
|
once: true,
|
|
requirements: Validate.object({
|
|
hapi: Validate.string(),
|
|
node: Validate.string()
|
|
})
|
|
.default(),
|
|
pkg: Validate.object({
|
|
name: Validate.string(),
|
|
version: Validate.string().default('0.0.0')
|
|
})
|
|
.unknown()
|
|
.default({})
|
|
})
|
|
.unknown()
|
|
})
|
|
.without('once', 'options')
|
|
.unknown();
|
|
|
|
|
|
internals.rules = Validate.object({
|
|
validate: Validate.object({
|
|
schema: Validate.alternatives(Validate.object(), Validate.array()).required(),
|
|
options: Validate.object()
|
|
.default({ allowUnknown: true })
|
|
})
|
|
});
|