From 20668603c869687e1bfa14d32187e7274dfec2e9 Mon Sep 17 00:00:00 2001 From: Matt Webb Date: Fri, 30 Sep 2022 18:42:32 -0700 Subject: [PATCH] Add gravity update --- README.md | 1 + src/client.test.ts | 99 +++++++++++++++++++++++++++++++++++++++++++++- src/client.ts | 41 +++++++++++++++---- src/config.test.ts | 4 ++ src/config.ts | 4 ++ 5 files changed, 141 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 93927cb0..80ae4e5d 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ It is recommended you run this service with Docker. | `SECONDARY_HOST_(#)_BASE_URL` | Yes | N/A | `http://192.168.1.3` or `https://pihole2.example.com` | The base URL of your secondary Pi-hole, including the scheme (HTTP or HTTPS) and port but not including a following slash. Replace `(#)` with a number, starting at `1`, to add multiple secondary Pi-holes. | | `SECONDARY_HOST_(#)_PASSWORD` | Yes | N/A | `mypassword2` | The password used to log in to the admin interface. | | `INTERVAL_MINUTES` | No | 30 | Any non-zero positive integer, for example `5`, `30`, or `1440` | How long to wait between synchronizations. Defaults to five minutes. Remember that the DNS server on your secondary servers restarts everytime a sync is performed. | +| `UPDATE_GRAVITY` | No | `true` | `true`/`false` | Triggers a gravity update after a backup has been uploaded to a secondary Pi-hole. This updates adlists and restarts gravity. | | `SYNC_WHITELIST` | No | `true` | `true`/`false` | Copies the whitelist | | `SYNC_REGEX_WHITELIST` | No | `true` | `true`/`false` | Copies the regex whitelist | | `SYNC_BLACKLIST` | No | `true` | `true`/`false` | Copies the blacklist | diff --git a/src/client.test.ts b/src/client.test.ts index 9a088968..711e6d83 100644 --- a/src/client.test.ts +++ b/src/client.test.ts @@ -231,8 +231,104 @@ describe('Client', () => { }); }); - test('should upload backup successfully', async () => { + test('should throw error if gravity update fails', async () => { + teleporter + .post('/admin/scripts/pi-hole/php/teleporter.php') + .reply( + 200, + 'Processed adlist (14 entries)
\n' + + 'Processed adlist group assignments (13 entries)
\n' + + 'Processed blacklist (exact) (0 entries)
\n' + + 'Processed blacklist (regex) (3 entries)
\n' + + 'Processed client (8 entries)
\n' + + 'Processed client group assignments (16 entries)
\n' + + 'Processed local DNS records (41 entries)
\n' + + 'Processed domain_audit (0 entries)
\n' + + 'Processed black-/whitelist group assignments (10 entries)
\n' + + 'Processed group (3 entries)
\n' + + 'Processed whitelist (exact) (4 entries)
\n' + + 'Processed whitelist (regex) (0 entries)
\n' + + 'OK' + ); + teleporter + .get('/admin/scripts/pi-hole/php/gravity.sh.php', undefined) + .reply(200, '\ndata: \n\ndata: [✓] TCP (IPv6)\ndata: \ndata: \n\ndata:'); + + const expectError = expect(client.uploadBackup(backup)).rejects; + + await expectError.toBeInstanceOf(ErrorNotification); + await expectError.toMatchObject({ + message: 'Failed updating gravity on "http://10.0.0.2".', + verbose: { + host: 'http://10.0.0.2', + status: 200, + eventStream: '[✓] TCP (IPv6)' + } + }); + }); + + test('should upload backup and update gravity successfully', async () => { + const syncOptions = jest.spyOn(Config, 'syncOptions', 'get'); + + let requestBody = ''; + teleporter + .post('/admin/scripts/pi-hole/php/teleporter.php', (body) => (requestBody = body)) + .reply( + 200, + 'Processed adlist (14 entries)
\n' + + 'Processed adlist group assignments (13 entries)
\n' + + 'Processed blacklist (exact) (0 entries)
\n' + + 'Processed blacklist (regex) (3 entries)
\n' + + 'Processed client (8 entries)
\n' + + 'Processed client group assignments (16 entries)
\n' + + 'Processed local DNS records (41 entries)
\n' + + 'Processed domain_audit (0 entries)
\n' + + 'Processed black-/whitelist group assignments (10 entries)
\n' + + 'Processed group (3 entries)
\n' + + 'Processed whitelist (exact) (4 entries)
\n' + + 'Processed whitelist (regex) (0 entries)
\n' + + 'OK' + ); + teleporter + .get('/admin/scripts/pi-hole/php/gravity.sh.php', undefined) + .reply( + 200, + '\ndata: \n\ndata: [✓] TCP (IPv6)\ndata: \ndata: \n\ndata: [✓] Pi-hole blocking is enabled\ndata: \n\ndata:' + ); + + const result = await client.uploadBackup(backup); + + expect(result).toStrictEqual(true); + expect(syncOptions).toHaveBeenCalled(); + expect(requestBody).toContain( + 'name="token"\r\n\r\nabcdefgijklmnopqrstuvwxyzabcdefgijklmnopqrst' + ); + expect(requestBody).toContain('name="whitelist"\r\n\r\ntrue'); + expect(requestBody).toContain('name="regex_whitelist"\r\n\r\ntrue'); + expect(requestBody).toContain('name="blacklist"\r\n\r\ntrue'); + expect(requestBody).toContain('name="regexlist"\r\n\r\ntrue'); + expect(requestBody).toContain('name="adlist"\r\n\r\ntrue'); + expect(requestBody).toContain('name="client"\r\n\r\ntrue'); + expect(requestBody).toContain('name="group"\r\n\r\ntrue'); + expect(requestBody).toContain('name="auditlog"\r\n\r\nfalse'); + expect(requestBody).toContain('name="staticdhcpleases"\r\n\r\nfalse'); + expect(requestBody).toContain('name="localdnsrecords"\r\n\r\ntrue'); + expect(requestBody).toContain('name="localcnamerecords"\r\n\r\ntrue'); + expect(requestBody).toContain('name="flushtables"\r\n\r\ntrue'); + expect(requestBody).toContain('name="action"\r\n\r\nin'); + expect(requestBody).toContain( + 'name="zip_file"; filename="backup.tar.gz"\r\nContent-Type: application/octet-stream' + ); + expect(requestBody.match(/Content-Disposition: form-data; name=/g)).toHaveLength( + 15 + ); + }); + + test('should not update gravity if `updateGravity` is disabled', async () => { const syncOptions = jest.spyOn(Config, 'syncOptions', 'get'); + const updateGravity = jest + .spyOn(Config, 'updateGravity', 'get') + .mockReturnValue(false); let requestBody = ''; teleporter @@ -280,6 +376,7 @@ describe('Client', () => { expect(requestBody.match(/Content-Disposition: form-data; name=/g)).toHaveLength( 15 ); + updateGravity.mockRestore(); }); }); }); diff --git a/src/client.ts b/src/client.ts index c2623859..88290b19 100644 --- a/src/client.ts +++ b/src/client.ts @@ -110,26 +110,53 @@ export class Client { form.append('action', 'in'); form.append('zip_file', backup, 'backup.tar.gz'); - const response = await this.fetch( + const uploadResponse = await this.fetch( `${this.host.baseUrl}/admin/scripts/pi-hole/php/teleporter.php`, { body: form, method: 'POST' } ); - const text = await response.text(); - if (response.status !== 200 || !text.endsWith('OK')) + const uploadText = await uploadResponse.text(); + if (uploadResponse.status !== 200 || !uploadText.endsWith('OK')) throw new ErrorNotification({ - message: `Error: failed to upload backup to "${this.host.baseUrl}".`, + message: `Failed to upload backup to "${this.host.baseUrl}".`, verbose: { host: this.host.baseUrl, - status: response.status, - responseBody: text + status: uploadResponse.status, + responseBody: uploadText } }); Log.info(chalk.green(`✔️ Backup uploaded to ${this.host.baseUrl}!`)); - Log.verbose(`Result:\n${chalk.blue(text)}`); + Log.verbose(`Result:\n${chalk.blue(uploadText)}`); + + if (Config.updateGravity) { + Log.info(chalk.yellow(`➡️ Updating gravity on ${this.host.baseUrl}...`)); + const gravityUpdateResponse = await this.fetch( + `${this.host.baseUrl}/admin/scripts/pi-hole/php/gravity.sh.php`, + { method: 'GET' } + ); + + const updateText = (await gravityUpdateResponse.text()) + .replaceAll('\ndata:', '') + .trim(); + if ( + gravityUpdateResponse.status !== 200 || + !updateText.endsWith('Pi-hole blocking is enabled') + ) + throw new ErrorNotification({ + message: `Failed updating gravity on "${this.host.baseUrl}".`, + verbose: { + host: this.host.baseUrl, + status: gravityUpdateResponse.status, + eventStream: updateText + } + }); + + Log.info(chalk.green(`✔️ Gravity updated on ${this.host.baseUrl}!`)); + Log.verbose(`Result:\n${chalk.blue(updateText)}`); + } return true; } diff --git a/src/config.test.ts b/src/config.test.ts index 7941fa6c..9295877f 100644 --- a/src/config.test.ts +++ b/src/config.test.ts @@ -238,6 +238,10 @@ describe('Config', () => { }); }); + describe('updateGravity', () => { + testToHaveDefaultAndOverride('updateGravity', true, 'UPDATE_GRAVITY'); + }); + describe('verboseMode', () => { testToHaveDefaultAndOverride('verboseMode', false, 'VERBOSE'); }); diff --git a/src/config.ts b/src/config.ts index 2d2cdcb0..de45ad16 100644 --- a/src/config.ts +++ b/src/config.ts @@ -64,6 +64,10 @@ export class Config { return this._syncOptions; } + static get updateGravity(): boolean { + return process.env['UPDATE_GRAVITY'] !== 'false'; + } + static get verboseMode(): boolean { return process.env['VERBOSE'] === 'true'; }