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