Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ Validation class for objects in NicTool. Analgous to Nictool/../\*/Sanity in v2.
✔ rejects invalid: a.b d.com.
✔ rejects invalid: a.b
d.com.
export_type
type
✔ rejects missing
✔ accepts valid: bind
✔ accepts valid: djbdns
Expand Down
56 changes: 45 additions & 11 deletions lib/nameserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,42 @@ export const name = Joi.string()
.domain({ allowFullyQualified: true, tlds: false })
.pattern(/\.$/)

export const type = Joi.string().valid(
/**
* Which nameserver software this is — the `nt_nameserver_export_type` names,
* which is also what NameserverSupervisor dispatches on.
*/
export const BUILDABLE = [
'bind',
'djbdns',
'knot',
'nsd',
'maradns',
'powerdns',
'dynect',
'native',
)
'coredns',
]

/**
* 2.x export types that name how a nameserver is fed rather than a different
* nameserver, resolved to the type that implements them.
*
* bind-nsupdate BIND fed over the network (RFC 2136) instead of by file
* copy. 2.x made that an export type of its own; v3 makes it
* a transport choice, so the nameserver is just bind.
*/
export const ALIASES = { 'bind-nsupdate': 'bind' }

/**
* A type a 2.x install can hold that nothing here implements. Storing one is
* allowed — an adopted 2.x record must survive a round trip through the API —
* but the supervisor refuses to start it rather than inventing a substitute.
*/
export const UNBUILDABLE = ['dynect']

/** The canonical spelling for a stored type, which may be a 2.x alias. */
export const resolveType = (value) => ALIASES[value] ?? value

export const type = Joi.string().valid(...BUILDABLE, ...Object.keys(ALIASES), ...UNBUILDABLE)

export const remote_login = Joi.string().empty('').max(127)

Expand All @@ -34,19 +60,29 @@ export const listen = Joi.array().items(
)

export const publisher = Joi.object({
type: Joi.string().valid('memory', 'rfc1035', 'tinydns-cdb', 'powerdns-db').required(),
type: Joi.string()
.valid('memory', 'rfc1035', 'maradns', 'tinydns-cdb', 'powerdns-db', 'coredns-redis', 'none')
.required(),
path: Joi.string().empty('').max(1024),
database: Joi.string().empty('').max(255),
}).unknown(true)

// these objects are stored as JSON and read by the dns-nameserver classes
export const transport = Joi.object({
type: Joi.string().valid('noop', 'axfr', 'rsync', 'db-replication').required(),
type: Joi.string().valid('noop', 'axfr', 'rsync', 'db-replication', 'pull').required(),
// pull: free text naming how the far side fetches, e.g. "fetchzone from cron"
source: Joi.string().empty('').max(255),
interval: shared.uint32.default(300),
cooldown: shared.uint16.default(5),
// axfr: who to NOTIFY. "host", "host:port" and "[v6]:port" all parse.
notify: Joi.array().items(Joi.string().max(255)),
master: Joi.string().empty('').max(255),
tsig_key: Joi.string().empty('').max(255),
tsigKey: Joi.string().empty('').max(255),
port: shared.uint16.min(1),
timeoutMs: shared.uint32,
attempts: shared.uint16.min(1),
remote: Joi.string().empty('').max(255),
ssh_key: Joi.string().empty('').max(1024),
sshKey: Joi.string().empty('').max(1024),
}).unknown(true)

export const dnssec = Joi.object({
Expand Down Expand Up @@ -74,7 +110,7 @@ export const v3 = Joi.object({
remote_login: remote_login,
logdir: Joi.string().empty('').max(255),
datadir: Joi.string().empty('').min(2).max(255),
engine: type,
type: type.required(),
listen: listen,
publisher: publisher,
transport: transport,
Expand All @@ -83,7 +119,6 @@ export const v3 = Joi.object({
interval: shared.uint16,
serials: Joi.boolean(),
status: Joi.string().empty('').max(255),
type: type.required(),
}),
deleted: Joi.boolean(),
})
Expand All @@ -104,7 +139,7 @@ export const PUT = Joi.object({
address: shared.ipv4,
address6: shared.ipv6.empty(''),
remote_login: remote_login,
engine: type,
type: type,
listen: listen,
publisher: publisher,
transport: transport,
Expand All @@ -113,7 +148,6 @@ export const PUT = Joi.object({
interval: shared.uint16,
serials: Joi.boolean(),
status: Joi.string().empty('').max(255),
type: type,
}),
deleted: Joi.boolean(),
})
Expand Down
56 changes: 46 additions & 10 deletions lib/nameserver.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,21 +67,25 @@ describe('nameserver', function () {
}
})

describe('export.type', function () {
describe('type', function () {
it(`rejects missing`, () => {
const testCase = JSON.parse(JSON.stringify(testNS))
delete testCase.export.type
delete testCase.type

const { error, value } = schema.validate(testCase)

assert.strictEqual(error.message, '"export.type" is required')
assert.strictEqual(error.message, '"type" is required')
assert.deepEqual(value, testCase)
})

for (const n of ['bind', 'djbdns', 'knot', 'nsd', 'maradns', 'powerdns', 'dynect', 'native']) {
for (const n of [
...nameserver.BUILDABLE,
...Object.keys(nameserver.ALIASES),
...nameserver.UNBUILDABLE,
]) {
it(`accepts valid: ${n}`, () => {
const testCase = JSON.parse(JSON.stringify(testNS))
testCase.export.type = n
testCase.type = n

const { error, value } = schema.validate(testCase)

Expand All @@ -90,17 +94,49 @@ describe('nameserver', function () {
})
}

it('accepts coredns, which only v3 can build', () => {
const testCase = JSON.parse(JSON.stringify(testNS))
testCase.type = 'coredns'

assert.ifError(schema.validate(testCase).error)
})

// dynect has no v3 nameserver, but a 2.x install can hold one and the
// record has to survive a round trip. The supervisor refuses to start it.
it('accepts a 2.x type nothing here implements', () => {
const testCase = JSON.parse(JSON.stringify(testNS))
testCase.type = 'dynect'

assert.ifError(schema.validate(testCase).error)
})

// bind-nsupdate is BIND fed by RFC 2136 rather than by file copy, which is
// a transport choice here, not a separate nameserver.
it('resolves a 2.x alias to the type that implements it', () => {
assert.equal(nameserver.resolveType('bind-nsupdate'), 'bind')
})

it('leaves a canonical type alone', () => {
for (const t of nameserver.BUILDABLE) {
assert.equal(nameserver.resolveType(t), t)
}
})

it('does not claim an alias is buildable', () => {
for (const alias of Object.keys(nameserver.ALIASES)) {
assert.ok(!nameserver.BUILDABLE.includes(alias), alias)
assert.ok(nameserver.BUILDABLE.includes(nameserver.resolveType(alias)), alias)
}
})

for (const n of ['cryptic', 'fuzzy', 'yitizg', 'bin', 'djbs', 'DJB', 'BIND', 'NT']) {
it(`rejects invalid: ${n}`, () => {
const testCase = JSON.parse(JSON.stringify(testNS))
testCase.export.type = n
testCase.type = n

const { error, value } = schema.validate(testCase)

assert.strictEqual(
error.message,
'"export.type" must be one of [bind, djbdns, knot, nsd, maradns, powerdns, dynect, native]',
)
assert.match(error.message, /^"type" must be one of \[/)
assert.deepEqual(value, testCase)
})
}
Expand Down
2 changes: 1 addition & 1 deletion lib/test/nameserver.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"remote_login": "nsd",
"logdir": "/foo",
"datadir": "/bar",
"type": "nsd",
"export": {
"type": "nsd",
"interval": 0,
"serials": true,
"status": "last run:03-05 15:25<br>last cp :09-20 12:59"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
},
"homepage": "https://github.com/NicTool/validate#readme",
"devDependencies": {
"eslint": "^10.7.0",
"eslint": "^10.8.0",
"@eslint/js": "^10.0.1",
"eslint-config-prettier": "^10.1.8",
"globals": "^17.7.0"
Expand Down
Loading