diff --git a/.gitignore b/.gitignore index cbd3a20..98cdc73 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /node_modules/ /keys.json *.zone +/dist diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cd0c640 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM alpine:latest + +RUN apk add --no-cache nodejs npm bind bind-tools +WORKDIR /icydns + +COPY package*.json ./ +RUN npm install --only production +COPY dist/ ./dist/ + +RUN mkdir /rndc +RUN mkdir /zones + +ENV RNDC_KEYFILE="/rndc/rndc.key" +ENV ZONEFILES="/zones" + +VOLUME /zones +VOLUME /rndc + +EXPOSE 9129 +CMD [ "npm", "start" ] diff --git a/README.md b/README.md new file mode 100644 index 0000000..203d784 --- /dev/null +++ b/README.md @@ -0,0 +1,176 @@ +# IcyDNS HTTP API +HTTP API for managing BIND zone files. + +## Running +This application is intended to be run behind a proxy. Requires node v14+ for `fs/promises`. Also requires `bind-tools` for checking zone files. + +* `npm install` +* `npm run build` +* `npm start` + +### Environment variables +* `PORT` - server port +* `ZONEFILES` - path to zone files +* `CACHE_TTL` - internal zone cache time-to-live +* `RNDC_SERVER` - RNDC host +* `RNDC_PORT` - RNDC port +* `RNDC_KEYFILE` - location of RNDC's key file + +Zones are automatically reloaded using `rndc` after updates. If you do not have rndc configured, you will need to reload the zones manually, but the files still get updated. + +## API + +**All requests are prefixed with `/api/v1`.** Authorization is by bearer token, i.e. `-H 'Authorization: Bearer '`. `?` denotes optional parameter. + +### `GET /zone/:domain` +Returns all of `:domain`'s DNS records. + +**Query:** None + +**Response:** +```typescript +{ + ttl: number; + records: [ + [index]: { + name: string; + type: string; + value: string; + } + ] +} +``` + +### `GET /zone/:domain/download` +Provides `:domain`'s records as a file. + +**Query:** None + +**Response:** BIND zone file + +### `POST /zone/:domain` +Reloads `:domain`'s zone file. Optionally changes the zone file's TTL value. + +**Body:** +```typescript +{ + ttl?: number; +} +``` + +**Response:** +```typescript +{ + success: boolean; + message: string; + ttl?: number; +} +``` + +### `GET /zone/records/:domain` +Returns all of `:domain`'s DNS records or performs a search based on provided query parameters. + +**Query:** +* `name?` +* `type?` +* `value?` + +**Response:** +```typescript +[ + [index]: { + name: string; + type: string; + value: string; + index?: number; // when searching only + } +] +``` + +### `POST /zone/records/:domain` +Updates a DNS record of `:domain` at `index`. + +**Body:** +```typescript +{ + index: number; + record: { + name?: string; + type?: string; + value?: string; + } +} +``` + +**Response:** +```typescript +{ + success: boolean; + message: string; + record: DNSRecord; +} +``` + +### `PUT /zone/records/:domain` +Creates a new DNS record for `:domain`. + +**Body:** +```typescript +{ + record: { + name: string; + type: string; + value: string; + } +} +``` + +**Response:** +```typescript +{ + success: boolean; + message: string; + record: DNSRecord; +} +``` + +### `DELETE /zone/records/:domain` +Deletes a DNS record from `:domain` at `index`. + +**Body:** +```typescript +{ + index: number; +} +``` + +**Response:** +```typescript +{ + success: boolean; + message: string; + record: DNSRecord; +} +``` + +### `POST /set-ip/:domain` +Quickly updates the `:domain`'s IP address (first occurences of `A` and `AAAA` records of `@` or `subdomain`). One of the IP addresses is taken from the request, so it's a good idea to use curl with `-4` to automatically set the IPv4 address and provide the IPv6 address with a body parameter. + +**Body:** +```typescript +{ + ipv4?: string; + ipv6?: string; + subdomain?: string; + dualRequest?: boolean; +} +``` + +**Response:** +```typescript +{ + success: boolean; + message: string; + actions: string[]; // detailed descriptions of what was actually done +} +``` diff --git a/package.json b/package.json index 098210f..b1d316d 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "test": "echo \"Error: no test specified\" && exit 1", "build": "tsc", "watch": "tsc -w", - "start": "node src/index.js" + "start": "node dist/index.js" }, "keywords": [], "author": "", diff --git a/src/dns/cache.ts b/src/dns/cache.ts index b3824b3..f5af0f3 100644 --- a/src/dns/cache.ts +++ b/src/dns/cache.ts @@ -84,7 +84,15 @@ export class DNSCache { throw new Error('No such cached zone file!'); } - await this.validator.validateAndSave(name, zone); + try { + await this.validator.validateAndSave(name, zone); + } catch (e) { + // Reload previous state + if (e.message.contains('Validation')) { + await this.load(name, zone.file); + } + throw e; + } } async update(name: string, newZone?: CachedZone, skipReload = false): Promise { diff --git a/src/dns/reader.ts b/src/dns/reader.ts index 32cf8fc..d35119c 100644 --- a/src/dns/reader.ts +++ b/src/dns/reader.ts @@ -37,7 +37,7 @@ function parseRecordLine(line: string, index: number, lines: string[]): DNSRecor if (split[2] === 'SOA') { return { name: split[0], - type: DNSRecordType[split[2]], + type: DNSRecordType.SOA, value: split.slice(3).join(' '), nameserver: split[3], email: split[4], diff --git a/tsconfig.json b/tsconfig.json index 0eeaea8..d275478 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,7 @@ // "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. */ + "outDir": "./dist", /* 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 */