initial commit
This commit is contained in:
commit
97d0a13165
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/node_modules/
|
||||
*.zone
|
1049
package-lock.json
generated
Normal file
1049
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
package.json
Normal file
21
package.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "icy-dyndns",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^4.17.1",
|
||||
"express-async-errors": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.11",
|
||||
"@types/node": "^15.3.0",
|
||||
"typescript": "^4.2.4"
|
||||
}
|
||||
}
|
2
src/.gitignore
vendored
Normal file
2
src/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
**/*.js
|
||||
**/*.d.ts
|
72
src/dns/cache.ts
Normal file
72
src/dns/cache.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { CachedZone, DNSRecord, DNSZone, SOARecord } from "../models/interfaces";
|
||||
import { readZoneFile } from "./reader";
|
||||
import { DNSRecordType } from "./records";
|
||||
import { writeZoneFile } from "./writer";
|
||||
|
||||
export class DNSCache {
|
||||
private cached: Record<string, CachedZone> = {};
|
||||
|
||||
constructor(private ttl = 1600) {}
|
||||
|
||||
has(name: string): boolean {
|
||||
return this.cached[name] != null;
|
||||
}
|
||||
|
||||
async get(name: string): Promise<CachedZone | null> {
|
||||
const cached = this.cached[name];
|
||||
if (!cached) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cached.changed.getTime() < new Date().getTime() - this.ttl * 1000) {
|
||||
return this.load(name, cached.file);
|
||||
}
|
||||
|
||||
return this.cached[name];
|
||||
}
|
||||
|
||||
async set(name: string, zone: CachedZone): Promise<void> {
|
||||
this.cached[name] = zone;
|
||||
}
|
||||
|
||||
async load(name: string, file: string): Promise<CachedZone> {
|
||||
const zoneFile = await readZoneFile(file);
|
||||
const cache = {
|
||||
name,
|
||||
file,
|
||||
zone: zoneFile,
|
||||
added: new Date(),
|
||||
changed: new Date()
|
||||
}
|
||||
this.cached[name] = cache;
|
||||
return cache;
|
||||
}
|
||||
|
||||
async save(name: string): Promise<void> {
|
||||
const zone = await this.get(name);
|
||||
if (!zone) {
|
||||
throw new Error('No such cached zone file!');
|
||||
}
|
||||
await writeZoneFile(zone.zone, zone.file);
|
||||
}
|
||||
|
||||
async update(name: string, newZone?: CachedZone): Promise<void> {
|
||||
let zone: CachedZone | null;
|
||||
if (newZone) {
|
||||
zone = newZone;
|
||||
} else {
|
||||
zone = await this.get(name);
|
||||
}
|
||||
|
||||
if (!zone) {
|
||||
throw new Error('No such cached zone file!');
|
||||
}
|
||||
|
||||
zone.changed = new Date();
|
||||
const soa = zone.zone.records.find((record) => record.type === DNSRecordType.SOA) as SOARecord;
|
||||
soa.serial = Math.floor(Date.now() / 1000);
|
||||
|
||||
this.set(name, zone);
|
||||
return this.save(name);
|
||||
}
|
||||
}
|
95
src/dns/reader.ts
Normal file
95
src/dns/reader.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import { DNSRecord, DNSZone, SOARecord } from '../models/interfaces';
|
||||
import { DNSRecordType } from './records';
|
||||
|
||||
function cleanString(str: string): string {
|
||||
return str.replace(/;[^"]+$/, '') // Remove comments from the end
|
||||
.replace(/\s+/g,' ') // Remove duplicate whitespace
|
||||
.trim(); // Remove whitespace from start and end
|
||||
}
|
||||
|
||||
function parseRecordLine(line: string, index: number, lines: string[]): DNSRecord | SOARecord | null {
|
||||
let actualLine = '';
|
||||
let clean = cleanString(line);
|
||||
if (clean.includes('(')) {
|
||||
actualLine += clean;
|
||||
const trimLines = lines.slice(index + 1);
|
||||
for (const trimLine of trimLines) {
|
||||
actualLine += ' ' + cleanString(trimLine);
|
||||
if (trimLine.includes(')')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
actualLine = cleanString(actualLine.replace(/\(|\)/g, ''));
|
||||
} else {
|
||||
actualLine = clean;
|
||||
}
|
||||
|
||||
const split = actualLine.split(' ');
|
||||
if (split[0] === 'IN' && split[1] === 'NS') {
|
||||
return {
|
||||
name: '',
|
||||
type: DNSRecordType.NS,
|
||||
value: split.slice(2).join(' '),
|
||||
}
|
||||
}
|
||||
|
||||
if (split[2] === 'SOA') {
|
||||
return {
|
||||
name: split[0],
|
||||
type: DNSRecordType[split[2]],
|
||||
value: split.slice(3).join(' '),
|
||||
nameserver: split[3],
|
||||
email: split[4],
|
||||
serial: parseInt(split[5], 10),
|
||||
refresh: parseInt(split[6], 10),
|
||||
retry: parseInt(split[7], 10),
|
||||
expire: parseInt(split[8], 10),
|
||||
minimum: parseInt(split[9], 10),
|
||||
};
|
||||
}
|
||||
|
||||
if (!actualLine.includes('IN')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
name: split[0],
|
||||
type: DNSRecordType[<keyof typeof DNSRecordType>split[2]],
|
||||
value: split.slice(3).join(' ')
|
||||
}
|
||||
}
|
||||
|
||||
export function parseZoneFile(lines: string[]): DNSZone {
|
||||
let ttl = 0;
|
||||
const includes = [];
|
||||
const records: DNSRecord[] = [];
|
||||
|
||||
for (const index in lines) {
|
||||
const line = lines[index];
|
||||
if (line.startsWith('$TTL')) {
|
||||
ttl = parseInt(line.split(' ')[1], 10);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith('$INCLUDE')) {
|
||||
includes.push(...line.split(' ').slice(1));
|
||||
continue;
|
||||
}
|
||||
|
||||
const record = parseRecordLine(line, parseInt(index, 10), lines);
|
||||
if (record) {
|
||||
records.push(record);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ttl, includes, records
|
||||
}
|
||||
}
|
||||
|
||||
export async function readZoneFile(file: string): Promise<DNSZone> {
|
||||
const lines = await fs.readFile(file, { encoding: 'utf-8' });
|
||||
const splitLines = lines.split('\n');
|
||||
return parseZoneFile(splitLines);
|
||||
}
|
9
src/dns/records.ts
Normal file
9
src/dns/records.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export enum DNSRecordType {
|
||||
A = 'A', AAAA = 'AAAA', AFSDB = 'AFSDB', APL = 'APL', CAA = 'CAA', CDNSKEY = 'CDNSKEY', CDS = 'CDS', CERT = 'CERT',
|
||||
CNAME = 'CNAME', CSYNC = 'CSYNC', DHCID = 'DHCID', DLV = 'DLV', DNAME = 'DNAME', DNSKEY = 'DNSKEY', DS = 'DS',
|
||||
EUI48 = 'EUI48', EUI64 = 'EUI64', HINFO = 'HINFO', IPSECKEY = 'IPSECKEY', KEY = 'KEY', KX = 'KX', LOC = 'LOC',
|
||||
MX = 'MX', NAPTR = 'NAPTR', NS = 'NS', NSEC = 'NSEC', NSEC3 = 'NSEC3', NSEC3PARAM = 'NSEC3PARAM',
|
||||
OPENPGPKEY = 'OPENPGPKEY', PTR = 'PTR', RRSIG = 'RRSIG', RP = 'RP', SIG = 'SIG', SMIMEA = 'SMIMEA', SOA = 'SOA',
|
||||
SRV = 'SRV', SSHFP = 'SSHFP', TA = 'TA', TKEY = 'TKEY', TLSA = 'TLSA', TSIG = 'TSIG', TXT = 'TXT', URI = 'URI',
|
||||
ZONEMD = 'ZONEMD', SVCB = 'SVCB', HTTPS = 'HTTPS', AXFR = 'AXFR', IXFR = 'IXFR', OPT = 'OPT', Wildcard = '*'
|
||||
}
|
67
src/dns/writer.ts
Normal file
67
src/dns/writer.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import { DNSZone, SOARecord } from '../models/interfaces';
|
||||
import { DNSRecordType } from './records';
|
||||
|
||||
function createSOAString(record: SOARecord, padI: number, padJ: number): string[] {
|
||||
const name = record.name.padEnd(padI, ' ');
|
||||
const type = record.type.toString().padEnd(padJ, ' ');
|
||||
const padK = ' '.padStart(
|
||||
padI + 3
|
||||
);
|
||||
const padL = ['serial', 'refresh', 'retry', 'expire', 'minimum']
|
||||
.reduce((previous, current) => {
|
||||
const len = `${record[current]}`.length;
|
||||
return previous > len ? previous : len;
|
||||
}, 0) + 1;
|
||||
return [
|
||||
`${name} IN ${type} ${record.nameserver} ${record.email} (`,
|
||||
`${padK} ${record.serial.toString().padEnd(padL, ' ')} ; Serial`,
|
||||
`${padK} ${record.refresh.toString().padEnd(padL, ' ')} ; Refresh`,
|
||||
`${padK} ${record.retry.toString().padEnd(padL, ' ')} ; Retry`,
|
||||
`${padK} ${record.expire.toString().padEnd(padL, ' ')} ; Expire`,
|
||||
`${padK} ${record.minimum.toString().padEnd(padL, ' ')} ; Minimum`,
|
||||
`)`
|
||||
];
|
||||
}
|
||||
|
||||
export function createZoneFile(zone: DNSZone, preferredLineLength = 120): string[] {
|
||||
const file: string[] = [];
|
||||
file.push(`$TTL ${zone.ttl}`);
|
||||
|
||||
let longestName = 0;
|
||||
let longestType = 0;
|
||||
|
||||
// First pass: for nice alignments
|
||||
zone.records.forEach((record) => {
|
||||
if (record.name.length > longestName) {
|
||||
longestName = record.name.length;
|
||||
}
|
||||
|
||||
if (record.type.toString().length > longestType) {
|
||||
longestType = record.type.toString().length;
|
||||
}
|
||||
});
|
||||
|
||||
zone.records.forEach((record) => {
|
||||
if (record.type === DNSRecordType.SOA) {
|
||||
file.push(...createSOAString(record as SOARecord, longestName, longestType));
|
||||
return;
|
||||
}
|
||||
|
||||
const name = record.name.padEnd(longestName, ' ');
|
||||
const type = record.type.toString().padEnd(longestType, ' ');
|
||||
file.push(`${name} IN ${type} ${record.value}`);
|
||||
});
|
||||
|
||||
zone.includes.forEach((include) => {
|
||||
file.push(`$INCLUDE ${include}`);
|
||||
});
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
export async function writeZoneFile(zone: DNSZone, file: string): Promise<string[]> {
|
||||
const fullText = createZoneFile(zone);
|
||||
await fs.writeFile(file, fullText.join('\n'));
|
||||
return fullText;
|
||||
}
|
272
src/index.ts
Normal file
272
src/index.ts
Normal file
@ -0,0 +1,272 @@
|
||||
import express, { ErrorRequestHandler, NextFunction, Request, Response } from 'express';
|
||||
import 'express-async-errors';
|
||||
import path from 'path';
|
||||
import { DNSCache } from './dns/cache';
|
||||
import { DNSRecordType } from './dns/records';
|
||||
import { createZoneFile } from './dns/writer';
|
||||
import { fromRequest } from './ip/from-request';
|
||||
import { CachedZone, DNSRecord } from './models/interfaces';
|
||||
|
||||
const port = parseInt(process.env.PORT || '9129', 10);
|
||||
const dir = process.env.ZONEFILES || '.';
|
||||
|
||||
const app = express();
|
||||
const api = express.Router();
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
const cache = new DNSCache();
|
||||
const weHave = ['lunasqu.ee'];
|
||||
|
||||
async function getOrLoad(domain: string): Promise<CachedZone> {
|
||||
if (!cache.has(domain)) {
|
||||
if (!weHave.includes(domain)) {
|
||||
throw new Error('Invalid domain.');
|
||||
}
|
||||
return cache.load(domain, path.resolve(dir, `${domain}.zone`));
|
||||
}
|
||||
|
||||
const get = await cache.get(domain);
|
||||
|
||||
if (!get) {
|
||||
throw new Error('Misconfigured domain zone file.');
|
||||
}
|
||||
|
||||
return get;
|
||||
}
|
||||
|
||||
api.get('/records/:domain/download', async (req, res) => {
|
||||
const domain = req.params.domain;
|
||||
const cached = await getOrLoad(domain);
|
||||
res.send(createZoneFile(cached.zone).join('\n'));
|
||||
});
|
||||
|
||||
api.get('/records/:domain', async (req, res) => {
|
||||
const domain = req.params.domain;
|
||||
const cached = await getOrLoad(domain);
|
||||
|
||||
const type = req.query.type;
|
||||
const name = req.query.name;
|
||||
const value = req.query.value;
|
||||
|
||||
if (type || name || value) {
|
||||
const results = cached.zone.records.filter((zone) => {
|
||||
if (type && zone.type !== type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (name && zone.name !== name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value && !zone.value.includes(value as string)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}).map((record) => {
|
||||
const inx = cached.zone.records.indexOf(record);
|
||||
return {
|
||||
...record,
|
||||
index: inx
|
||||
}
|
||||
});
|
||||
|
||||
res.json({
|
||||
records: results,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
res.json(cached.zone);
|
||||
});
|
||||
|
||||
api.post('/records/:domain', async (req, res) => {
|
||||
const domain = req.params.domain;
|
||||
const index = parseInt(req.body.index, 10);
|
||||
const setters = req.body.record;
|
||||
|
||||
const cached = await getOrLoad(domain);
|
||||
const { zone } = cached;
|
||||
if (index == null || isNaN(index) || !zone.records[index]) {
|
||||
throw new Error('Invalid record index.');
|
||||
}
|
||||
|
||||
const keys = Object.keys(setters);
|
||||
const record = zone.records[index];
|
||||
if (!setters || keys.length === 0) {
|
||||
res.json({ success: true, message: 'Nothing was changed.', record });
|
||||
return;
|
||||
}
|
||||
|
||||
if (setters.type) {
|
||||
const upperType = setters.type.toUpperCase();
|
||||
if (upperType === 'SOA' && record.type !== DNSRecordType.SOA) {
|
||||
throw new Error('Cannot change type to Start Of Authority.');
|
||||
}
|
||||
|
||||
if (!DNSRecordType[<keyof typeof DNSRecordType>upperType] && upperType !== '*') {
|
||||
throw new Error('Unsupported record type.');
|
||||
}
|
||||
}
|
||||
|
||||
keys.forEach((key) => {
|
||||
if (record[key]) {
|
||||
record[key] = setters[key];
|
||||
}
|
||||
});
|
||||
|
||||
await cache.update(domain, cached);
|
||||
|
||||
res.json({ success: true, message: 'Record changed successfully.', record });
|
||||
});
|
||||
|
||||
api.delete('/records/:domain', async (req, res) => {
|
||||
const domain = req.params.domain;
|
||||
const index = parseInt(req.body.index, 10);
|
||||
|
||||
const cached = await getOrLoad(domain);
|
||||
const { zone } = cached;
|
||||
if (!index || isNaN(index) || !zone.records[index]) {
|
||||
throw new Error('Invalid record index.');
|
||||
}
|
||||
|
||||
const record = zone.records[index];
|
||||
if (record.type === DNSRecordType.SOA) {
|
||||
throw new Error('Cannot delete the Start Of Authority record.');
|
||||
}
|
||||
|
||||
zone.records.splice(index, 1);
|
||||
await cache.update(domain, cached);
|
||||
|
||||
res.json({ success: true, message: 'Record deleted successfully.', record });
|
||||
});
|
||||
|
||||
api.put('/records/:domain', async (req, res) => {
|
||||
const domain = req.params.domain;
|
||||
const setter = req.body.record;
|
||||
|
||||
if (!setter) {
|
||||
throw new Error('New record is missing!');
|
||||
}
|
||||
|
||||
const missing = ['name', 'type', 'value'].reduce<string[]>(
|
||||
(list, entry) => (setter[entry] == null ? [...list, entry] : list)
|
||||
, []);
|
||||
|
||||
if (missing.length) {
|
||||
throw new Error(`${missing.join(', ')} ${missing.length > 1 ? 'are' : 'is'} required.`);
|
||||
}
|
||||
|
||||
const { name, type, value } = setter;
|
||||
const upperType = type.toUpperCase();
|
||||
if (upperType === 'SOA') {
|
||||
throw new Error('Cannot add another Start Of Authority record. Please use POST method to modify the existing record.');
|
||||
}
|
||||
|
||||
if (!DNSRecordType[<keyof typeof DNSRecordType>upperType] && upperType !== '*') {
|
||||
throw new Error('Unsupported record type.');
|
||||
}
|
||||
|
||||
const cached = await getOrLoad(domain);
|
||||
const { zone } = cached;
|
||||
const newRecord = { name, type: upperType, value };
|
||||
zone.records.push(newRecord);
|
||||
|
||||
await cache.update(domain, cached);
|
||||
res.status(201).json({ success: true, message: 'Record added.', record: newRecord });
|
||||
});
|
||||
|
||||
api.post('/dyndns/:domain', async (req, res) => {
|
||||
const domain = req.params.domain;
|
||||
const subdomain = req.body.subdomain || '@';
|
||||
const waitPartial = req.body.dualRequest === true;
|
||||
const { v4, v6 } = fromRequest(req);
|
||||
|
||||
if (!v4 && !v6) {
|
||||
res.json({ success: true, message: 'Nothing to do.' });
|
||||
}
|
||||
|
||||
const cached = await getOrLoad(domain);
|
||||
const { zone } = cached;
|
||||
const actions: string[] = [];
|
||||
|
||||
if (v4) {
|
||||
const findFirstA = zone.records.find((record) =>
|
||||
record.type === DNSRecordType.A && record.name === subdomain
|
||||
);
|
||||
|
||||
if (!findFirstA) {
|
||||
zone.records.push({
|
||||
name: subdomain,
|
||||
type: DNSRecordType.A,
|
||||
value: v4
|
||||
});
|
||||
actions.push(`created A record ${v4}`);
|
||||
} else {
|
||||
if (findFirstA.value !== v4) {
|
||||
findFirstA.value = v4;
|
||||
actions.push(`updated A record with ${v4}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (v6) {
|
||||
const findFirstAAAA = zone.records.find((record) =>
|
||||
record.type === DNSRecordType.AAAA && record.name === subdomain
|
||||
);
|
||||
|
||||
if (!findFirstAAAA) {
|
||||
zone.records.push({
|
||||
name: subdomain,
|
||||
type: DNSRecordType.AAAA,
|
||||
value: v6
|
||||
});
|
||||
actions.push(`created AAAA record ${v6}`);
|
||||
} else {
|
||||
if (findFirstAAAA.value !== v6) {
|
||||
findFirstAAAA.value = v6;
|
||||
actions.push(`updated AAAA record with ${v6}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!actions.length) {
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Up to date.',
|
||||
actions
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (waitPartial && ((v4 && !v6) || (!v4 && v6))) {
|
||||
res.status(202).json({
|
||||
success: true,
|
||||
message: 'Waiting for next request..',
|
||||
actions
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await cache.update(domain, cached);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Successfully updated zone file.',
|
||||
actions
|
||||
});
|
||||
});
|
||||
|
||||
const errorHandler: ErrorRequestHandler = (err: any, req: Request, res: Response, next: NextFunction) => {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: err.message
|
||||
});
|
||||
}
|
||||
|
||||
api.use(errorHandler);
|
||||
app.use('/api/v1', api);
|
||||
|
||||
app.listen(port, () => console.log(`listening on ${port}`));
|
51
src/ip/from-request.ts
Normal file
51
src/ip/from-request.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { Request } from "express";
|
||||
import { validv4, validv6 } from "./validators";
|
||||
|
||||
export function fromRequest(req: Request): { v4: string | null, v6: string | null } {
|
||||
let v4 = null;
|
||||
const qv4 = req.query.ipv4 || req.body.ipv4;
|
||||
|
||||
let v6 = null;
|
||||
const qv6 = req.query.ipv6 || req.body.ipv6;
|
||||
|
||||
// Lets begin our trials
|
||||
// Determine Address from request headers
|
||||
if (req.header('x-forwarded-for')) {
|
||||
v4 = req.header('x-forwarded-for');
|
||||
} else {
|
||||
v4 = req.socket.remoteAddress;
|
||||
}
|
||||
|
||||
if (v4 && !validv4(v4)) {
|
||||
v6 = v4
|
||||
v4 = null
|
||||
}
|
||||
|
||||
// IPv4
|
||||
if (qv4 && validv4(qv4)) {
|
||||
v4 = qv4
|
||||
}
|
||||
|
||||
if (qv4 === 'ignore') {
|
||||
v4 = null
|
||||
}
|
||||
|
||||
// IPv6
|
||||
if (qv6 && validv6(qv6)) {
|
||||
v6 = qv6
|
||||
}
|
||||
|
||||
if (qv6 === 'ignore') {
|
||||
v6 = null
|
||||
}
|
||||
|
||||
if (v4 === null && v6 === null) {
|
||||
return { v4, v6 };
|
||||
}
|
||||
|
||||
// Remove subnet mask and prefix
|
||||
if (v6) v6 = v6.replace(/\/(\d+)$/, '')
|
||||
if (v4) v4 = v4.replace(/\/(\d+)$/, '')
|
||||
|
||||
return { v4, v6 };
|
||||
}
|
42
src/ip/validators.ts
Normal file
42
src/ip/validators.ts
Normal file
@ -0,0 +1,42 @@
|
||||
export function validv4(ipaddress: string): boolean {
|
||||
if (/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(ipaddress)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export function validv6(value: string): boolean {
|
||||
// See https://blogs.msdn.microsoft.com/oldnewthing/20060522-08/?p=31113 and
|
||||
// https://4sysops.com/archives/ipv6-tutorial-part-4-ipv6-address-syntax/
|
||||
const components = value.split(':')
|
||||
if (components.length < 2 || components.length > 8) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (components[0] !== '' || components[1] !== '') {
|
||||
// Address does not begin with a zero compression ("::")
|
||||
if (!components[0].match(/^[\da-f]{1,4}/i)) {
|
||||
// Component must contain 1-4 hex characters
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
let numberOfZeroCompressions = 0
|
||||
for (let i = 1; i < components.length; ++i) {
|
||||
if (components[i] === '') {
|
||||
// We're inside a zero compression ("::")
|
||||
++numberOfZeroCompressions
|
||||
if (numberOfZeroCompressions > 1) {
|
||||
// Zero compression can only occur once in an address
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (!components[i].match(/^[\da-f]{1,4}/i)) {
|
||||
// Component must contain 1-4 hex characters
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
32
src/models/interfaces.ts
Normal file
32
src/models/interfaces.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { DNSRecordType } from "../dns/records";
|
||||
|
||||
export interface DNSRecord {
|
||||
[key: string]: string | number;
|
||||
name: string;
|
||||
type: DNSRecordType;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface SOARecord extends DNSRecord {
|
||||
nameserver: string;
|
||||
email: string;
|
||||
serial: number;
|
||||
refresh: number;
|
||||
retry: number;
|
||||
expire: number;
|
||||
minimum: number;
|
||||
}
|
||||
|
||||
export interface DNSZone {
|
||||
ttl: number;
|
||||
records: DNSRecord[];
|
||||
includes: string[];
|
||||
}
|
||||
|
||||
export interface CachedZone {
|
||||
name: string;
|
||||
file: string;
|
||||
zone: DNSZone;
|
||||
added: Date;
|
||||
changed: Date;
|
||||
}
|
71
tsconfig.json
Normal file
71
tsconfig.json
Normal file
@ -0,0 +1,71 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||
|
||||
/* Basic Options */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
|
||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
// "outDir": "./", /* Redirect output structure to the directory. */
|
||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
// "composite": true, /* Enable project compilation */
|
||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||
// "removeComments": true, /* Do not emit comments to output. */
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
|
||||
/* Additional Checks */
|
||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
|
||||
|
||||
/* Module Resolution Options */
|
||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
|
||||
/* Advanced Options */
|
||||
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user