UNPKG

@ppwcode/terraform-ppwcode-modules

Version:
403 lines (382 loc) 14.9 kB
/** * Copyright 2017 - 2018 PeopleWare n.v. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const moment = require('moment') const pad = require('pad') const Q = require('q') const dns = require('dns') const Contract = require('@toryt/contracts-iv') const PromiseContract = require('@toryt/contracts-iv/lib/IV/PromiseContract') const util = require('./_util') class SoaSerial { get invariants () { return ( moment.isMoment(this.at) && this.at.utcOffset() === 0 && this.at.utc().hours() === 0 && this.at.utc().minutes() === 0 && this.at.utc().seconds() === 0 && this.at.utc().milliseconds() === 0 && typeof this.year === 'string' && SoaSerial.yearRegExp.test(this.year) && this.year === this.at.format(SoaSerial.yearPattern) && typeof this.month === 'string' && SoaSerial.monthRegExp.test(this.month) && Number.parseInt(this.month) >= 1 && Number.parseInt(this.month) <= SoaSerial.maxMonth && this.month === this.at.format(SoaSerial.monthPattern) && typeof this.day === 'string' && SoaSerial.dayRegExp.test(this.day) && Number.parseInt(this.day) >= 1 && Number.parseInt(this.day) <= moment( this.year + this.month, SoaSerial.yearPattern + SoaSerial.monthPattern ).daysInMonth() && this.day === this.at.format(SoaSerial.dayPattern) && typeof this.serialStart === 'string' && this.serialStart === this.year + this.month + this.day && typeof this.sequenceNumber === 'number' && this.sequenceNumber >= 0 && this.sequenceNumber <= SoaSerial.maxSequenceNumber && typeof this.serial === 'string' && this.serial === this.serialStart + pad(2, this.sequenceNumber, '0') && JSON.parse(JSON.stringify(this)).at === JSON.parse(JSON.stringify(this.at)) && JSON.parse(JSON.stringify(this)).year === this.year && JSON.parse(JSON.stringify(this)).month === this.month && JSON.parse(JSON.stringify(this)).day === this.day && JSON.parse(JSON.stringify(this)).sequenceNumber === this.sequenceNumber && JSON.parse(JSON.stringify(this)).serial === this.serial ) } /** * Create a new {@code SoaSerial}, given the date of {@code at} and the {@sequenceNumber}. * * @param {Date|moment.Moment} at - the date at which this instance should be used * @param {Number} sequenceNumber - the sequence number of the SOA serial within the day represented by {@code at} */ constructor (at, sequenceNumber) { this._at = moment.utc(at).startOf('day') this.sequenceNumber = sequenceNumber } /** * @return {moment.Moment} */ get at () { return moment.utc(this._at).startOf('day') } /** * @return {string} 4-digit year of {@link #at} */ get year () { return this._at.format(SoaSerial.yearPattern) } /** * @return {string} 2-digit month of {@link #at} */ get month () { return this._at.format(SoaSerial.monthPattern) } /** * @return {string} 2-digit day of {@link #at} */ get day () { return this._at.format(SoaSerial.dayPattern) } /** * The first 8 digits of the SOA serial * * @return {string} {@link #year} + {@link #month} + {@link #day} */ get serialStart () { return this._at.format(SoaSerial.isoDateWithoutDashesPattern) } /** * 10-digit SOA serial, to be used at {@code at}, with sequence number {@code sequenceNumber}. * * @return {string} {@link #serialStart} + {@link #sequenceNumber} */ get serial () { return this.serialStart + pad(2, this.sequenceNumber, '0') } // noinspection JSUnusedGlobalSymbols toJSON () { return { at: this.at, year: this.year, month: this.month, day: this.day, sequenceNumber: this.sequenceNumber, serial: this.serial } } } SoaSerial.constructorContract = new Contract({ pre: [ (at, sequenceNumber) => at instanceof Date || moment.isMoment(at), (at, sequenceNumber) => typeof sequenceNumber === 'number', (at, sequenceNumber) => sequenceNumber >= 0, (at, sequenceNumber) => sequenceNumber <= SoaSerial.maxSequenceNumber ], post: [ (at, sequenceNumber, result) => result.at.format(SoaSerial.isoDateWithoutDashesPattern) === moment(at) .utc() .format(SoaSerial.isoDateWithoutDashesPattern), (at, sequenceNumber, result) => result.sequenceNumber === sequenceNumber ], exception: Contract.mustNotHappen }) SoaSerial.yearPattern = 'YYYY' SoaSerial.yearRegExp = /^\d{4}$/ SoaSerial.monthPattern = 'MM' SoaSerial.monthRegExp = /^\d{2}$/ SoaSerial.dayPattern = 'DD' SoaSerial.dayRegExp = /^\d{2}$/ SoaSerial.isoDateWithoutDashesPattern = SoaSerial.yearPattern + SoaSerial.monthPattern + SoaSerial.dayPattern SoaSerial.serialRegExp = /^(\d{8})(\d{2})$/ SoaSerial.detailedSerialRegExp = /^(\d{4})(\d{2})(\d{2})(\d{2})$/ // noinspection MagicNumberJS SoaSerial.maxSequenceNumber = 99 // noinspection MagicNumberJS SoaSerial.maxMonth = 12 SoaSerial.isASerial = new Contract({ post: [ (candidate, result) => typeof result === 'boolean', (candidate, result) => !result || typeof candidate === 'string', (candidate, result) => !result || SoaSerial.detailedSerialRegExp.test(candidate), (candidate, result) => !result || Number.parseInt(SoaSerial.detailedSerialRegExp.exec(candidate)[2]) >= 1, (candidate, result) => !result || Number.parseInt(SoaSerial.detailedSerialRegExp.exec(candidate)[2]) <= SoaSerial.maxMonth, (candidate, result) => !result || Number.parseInt(SoaSerial.detailedSerialRegExp.exec(candidate)[3]) >= 1, (candidate, result) => !result || Number.parseInt(SoaSerial.detailedSerialRegExp.exec(candidate)[3]) <= moment( SoaSerial.detailedSerialRegExp.exec(candidate)[1] + SoaSerial.detailedSerialRegExp.exec(candidate)[2], SoaSerial.yearPattern + SoaSerial.monthPattern ).daysInMonth(), (candidate, result) => !result || Number.parseInt(SoaSerial.detailedSerialRegExp.exec(candidate)[4]) >= 0, (candidate, result) => !result || Number.parseInt(SoaSerial.detailedSerialRegExp.exec(candidate)[4]) <= SoaSerial.maxSequenceNumber ], exception: Contract.mustNotHappen }).implementation(function (candidate) { if ( typeof candidate !== 'string' || !SoaSerial.detailedSerialRegExp.test(candidate) ) { return false } const parts = SoaSerial.detailedSerialRegExp.exec(candidate) const month = Number.parseInt(parts[2]) const day = Number.parseInt(parts[3]) const sequenceNumber = Number.parseInt(parts[4]) return ( month >= 1 && month <= SoaSerial.maxMonth && day >= 1 && day <= moment( parts[1] + parts[2], SoaSerial.yearPattern + SoaSerial.monthPattern ).daysInMonth() && sequenceNumber >= 0 && sequenceNumber <= SoaSerial.maxSequenceNumber ) }) /** * Construct an {@link SoaSerial} instance from the serial string as expected in a DNS SOA record. * * @param {string} serial - string in the format YYYYMMDDnn, expected of an SOA serial * @return {object} object with the parts of the serial ({@code year}, {@code month}, {@code day}, * {@code sequenceNumber}) as properties of type {@code string} */ SoaSerial.parse = new Contract({ pre: [serial => SoaSerial.isASerial(serial)], post: [ (serial, result) => result instanceof SoaSerial, (serial, result) => result.serial === serial ], exception: Contract.mustNotHappen }).implementation(function parse (serial) { const parts = SoaSerial.serialRegExp.exec(serial) return new SoaSerial( moment.utc(parts[1], SoaSerial.isoDateWithoutDashesPattern), Number.parseInt(parts[2]) ) }) /** * Return a Promise for the serial in the SOA record of {@code domain}, retrieved via DNS. * The promise is rejected if the record does not exists, or the network is not available. * The result is not necessary in the format YYYYMMDDnn. This is recommendation, not an obligation. * * @param {string} domain - FQDN of the domain to get the SOA record of * @return {Promise<string>} Promise for the serial string in the SOA record of {@code domain}, retrieved via DNS */ SoaSerial.currentSoaSerialString = new PromiseContract({ pre: [domain => typeof domain === 'string'], post: [ (domain, result) => typeof result === 'string', (domain, result) => /^\d+$/.test(result) /* Contracts does not offer support for Promises yet */ ], fastException: PromiseContract.mustNotHappen, /* domain does not exist, or there is no SOA record, or there is no internet connection, or no DNS server can be contacted, … */ exception: util.exceptionIsAnError }).implementation(function currentSoaSerialString (domain) { return util .realPromise(Q.denodeify(dns.resolveSoa)(domain)) .then(soa => '' + soa.serial) }) /** * Return a Promise for the SoaSerial, based on the serial in the SOA record of {@code domain}, retrieved via DNS. * The promise is rejected if the record does not exists, or the network is not available, or the serial is not in * the format YYYYMMDDnn. * * @param {string} domain - FQDN of the domain to get the SOA record of * @return {Promise<SoaSerial>} Promise for the SoaSerial, based on the serial string in the SOA record of * {@code domain}, retrieved via DNS */ SoaSerial.currentSoaSerial = new PromiseContract({ pre: [domain => typeof domain === 'string'], post: [(domain, result) => result instanceof SoaSerial], fastException: PromiseContract.mustNotHappen, /* domain does not exist, or there is no SOA record, or there is no internet connection, or no DNS server can be contacted, … */ exception: util.exceptionIsAnError }).implementation(function currentSoaSerial (domain) { return SoaSerial.currentSoaSerialString(domain).then(serialString => { if (!SoaSerial.isASerial(serialString)) { throw new Error('The serial of the domain is not in the form YYYYMMDDnn') } return SoaSerial.parse(serialString) }) }) /** * Get the current SOA serial via DNS (which might not exists), and create the next one, for use * at {@code at}. If no SOA record exists, the network is not available, or the serial is not in * the format YYYYMMDDnn, an SoaSerial is created with sequence number 0. * The promise is rejected if the current sequence number is already at 99, and {@code at} is at the same * day in the UTC time zone as reflected by the current SOA serial, or if {@code at} is before the * day of the current SOA serial in the UTC timezone. * * In principle, {@code at} must be today, or in the future. * * @param {string} domain - FQDN of the domain to get the current SOA record of * @param {Date|moment.Moment} at - the time at which the resulting instance would be used * @return {Promise<SoaSerial>} Promise for an SoaSerial instance, that can be used at {@code at}, * give the current state of the {@code domain} SOA in DNS */ SoaSerial.nextSoaSerial = new PromiseContract({ pre: [ (domain, at) => typeof domain === 'string', (domain, at) => at instanceof Date || moment.isMoment(at) ], post: [(domain, at, result) => result instanceof SoaSerial], fastException: PromiseContract.mustNotHappen, /* current sequence number is already at 99, and {@code at} is at the same day in the UTC time zone as reflected by the current SOA serial, or {@code at} is before the day reflected by the current SOA serial. */ exception: util.exceptionIsAnError }).implementation(function (domain, at) { return SoaSerial.currentSoaSerialString(domain).then( serial => SoaSerial.isASerial(serial) ? SoaSerial.parse(serial).next(at) : new SoaSerial(at, 0), ignore => new SoaSerial(at, 0) ) }) /** * Return a new instance of {@link SoaSerial}, that can be used as the next SOA serial after {@code this}, at * {@code useAt}. * - If the UTC day-date part of {@link #at} is before the UTC day-date part of {@code useAt}, * the result has {@link at} === {@code useAt}, and its {@link #sequenceNumber} is 0. * - If {@link #at} has the same UTC date as {@code useAt}, the result has the same {@link at} as {@code this} * (and thus the same {@link year()}, {@link month()}, and {@link #day()}, but its {@link #sequenceNumber} is the * successor of this' {@link #sequenceNumber}. * * Fails if the day of {@code useAt} is before the day of {@link at} in the UTC timezone, or the sequence numbers are * depleted. * In principle, {@code useAt} must be today, or in the future. * * @param {Date|Moment} useAt - the time at which the resulting instance would be used * @return {SoaSerial} Next SOA serial, to be used at {@code useAt}, given {@code this} as the current SOA serial */ SoaSerial.prototype.next = new Contract({ pre: [useAt => useAt instanceof Date || moment.isMoment(useAt)], post: [ (useAt, result) => result instanceof SoaSerial, (useAt, result) => result.serialStart === moment(useAt) .utc() .format(SoaSerial.isoDateWithoutDashesPattern), function (useAt, result) { return ( result.serialStart === this.serialStart || result.sequenceNumber === 0 ) }, function (useAt, result) { return ( result.serialStart !== this.serialStart || result.sequenceNumber === this.sequenceNumber + 1 ) } ], exception: [ function (useAt, err) { return ( err instanceof Error && (this.at.isAfter(useAt, 'day') || SoaSerial.maxSequenceNumber <= this.sequenceNumber) ) } ] }).implementation(function next (useAt) { if (SoaSerial.maxSequenceNumber <= this.sequenceNumber) { throw new Error( 'Cannot create a next serial on useAt, ' + 'because this serial already has the maximum sequence number for that day' ) } if (this._at.isAfter(useAt, 'day')) { throw new Error( 'Cannot create a next serial for useAt earlier than the day in the current serial' ) } return new SoaSerial( useAt, this._at.isSame(useAt, 'day') ? this.sequenceNumber + 1 : 0 ) }) module.exports = SoaSerial