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';
}