✨ Add pihole integration (#860)
* ✨ Add pihole integration * Update src/widgets/adhole/AdHoleControls.tsx Co-authored-by: Larvey <39219859+LarveyOfficial@users.noreply.github.com> * Update src/tools/client/math.ts Co-authored-by: Meier Lukas <meierschlumpf@gmail.com> * Update src/widgets/dnshole/DnsHoleSummary.tsx Co-authored-by: Meier Lukas <meierschlumpf@gmail.com> --------- Co-authored-by: Larvey <39219859+LarveyOfficial@users.noreply.github.com> Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
@@ -6,9 +6,8 @@ import Consola from 'consola';
|
||||
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import { BackendConfigType, ConfigType } from '../../../types/config';
|
||||
import { getConfig } from '../../../tools/config/getConfig';
|
||||
import widgets from '../../../widgets';
|
||||
import { BackendConfigType, ConfigType } from '../../../types/config';
|
||||
import { IRssWidget } from '../../../widgets/rss/RssWidgetTile';
|
||||
|
||||
function Put(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
||||
53
src/pages/api/modules/dns-hole/control.ts
Normal file
53
src/pages/api/modules/dns-hole/control.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { z } from 'zod';
|
||||
import { getCookie } from 'cookies-next';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { getConfig } from '../../../../tools/config/getConfig';
|
||||
import { findAppProperty } from '../../../../tools/client/app-properties';
|
||||
import { PiHoleClient } from '../../../../tools/server/sdk/pihole/piHole';
|
||||
|
||||
const getQuerySchema = z.object({
|
||||
status: z.enum(['enabled', 'disabled']),
|
||||
});
|
||||
|
||||
export const Post = async (request: NextApiRequest, response: NextApiResponse) => {
|
||||
const configName = getCookie('config-name', { req: request });
|
||||
const config = getConfig(configName?.toString() ?? 'default');
|
||||
|
||||
const parseResult = getQuerySchema.safeParse(request.query);
|
||||
|
||||
if (!parseResult.success) {
|
||||
response.status(400).json({ message: 'invalid query parameters, please specify the status' });
|
||||
return;
|
||||
}
|
||||
|
||||
const applicableApps = config.apps.filter((x) => x.integration?.type === 'pihole');
|
||||
|
||||
for (let i = 0; i < applicableApps.length; i += 1) {
|
||||
const app = applicableApps[i];
|
||||
|
||||
const pihole = new PiHoleClient(
|
||||
app.url,
|
||||
findAppProperty(app, 'password')
|
||||
);
|
||||
|
||||
switch (parseResult.data.status) {
|
||||
case 'enabled':
|
||||
await pihole.enable();
|
||||
break;
|
||||
case 'disabled':
|
||||
await pihole.disable();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
response.status(200).json({});
|
||||
};
|
||||
|
||||
export default async (request: NextApiRequest, response: NextApiResponse) => {
|
||||
if (request.method === 'POST') {
|
||||
return Post(request, response);
|
||||
}
|
||||
|
||||
return response.status(405).json({});
|
||||
};
|
||||
273
src/pages/api/modules/dns-hole/summary.spec.ts
Normal file
273
src/pages/api/modules/dns-hole/summary.spec.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
import Consola from 'consola';
|
||||
import { describe, it, vi, expect } from 'vitest';
|
||||
import { createMocks } from 'node-mocks-http';
|
||||
|
||||
import GetSummary from './summary';
|
||||
import { ConfigType } from '../../../../types/config';
|
||||
|
||||
const mockedGetConfig = vi.fn();
|
||||
|
||||
describe('DNS hole', () => {
|
||||
it('combine and return aggregated data', async () => {
|
||||
// arrange
|
||||
const { req, res } = createMocks({
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
vi.mock('./../../../../tools/config/getConfig.ts', () => ({
|
||||
get getConfig() {
|
||||
return mockedGetConfig;
|
||||
},
|
||||
}));
|
||||
|
||||
mockedGetConfig.mockReturnValue({
|
||||
apps: [
|
||||
{
|
||||
url: 'http://pi.hole',
|
||||
integration: {
|
||||
type: 'pihole',
|
||||
properties: [
|
||||
{
|
||||
field: 'password',
|
||||
type: 'private',
|
||||
value: 'hf3829fj238g8',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as ConfigType);
|
||||
const errorLogSpy = vi.spyOn(Consola, 'error');
|
||||
const warningLogSpy = vi.spyOn(Consola, 'warn');
|
||||
|
||||
fetchMock.mockResponse((request) => {
|
||||
if (request.url === 'http://pi.hole/admin/api.php?summaryRaw&auth=hf3829fj238g8') {
|
||||
return JSON.stringify({
|
||||
domains_being_blocked: 780348,
|
||||
dns_queries_today: 36910,
|
||||
ads_blocked_today: 9700,
|
||||
ads_percentage_today: 26.280142,
|
||||
unique_domains: 6217,
|
||||
queries_forwarded: 12943,
|
||||
queries_cached: 13573,
|
||||
clients_ever_seen: 20,
|
||||
unique_clients: 17,
|
||||
dns_queries_all_types: 36910,
|
||||
reply_UNKNOWN: 947,
|
||||
reply_NODATA: 3313,
|
||||
reply_NXDOMAIN: 1244,
|
||||
reply_CNAME: 5265,
|
||||
reply_IP: 25635,
|
||||
reply_DOMAIN: 97,
|
||||
reply_RRNAME: 4,
|
||||
reply_SERVFAIL: 28,
|
||||
reply_REFUSED: 0,
|
||||
reply_NOTIMP: 0,
|
||||
reply_OTHER: 0,
|
||||
reply_DNSSEC: 0,
|
||||
reply_NONE: 0,
|
||||
reply_BLOB: 377,
|
||||
dns_queries_all_replies: 36910,
|
||||
privacy_level: 0,
|
||||
status: 'enabled',
|
||||
gravity_last_updated: {
|
||||
file_exists: true,
|
||||
absolute: 1682216493,
|
||||
relative: {
|
||||
days: 5,
|
||||
hours: 17,
|
||||
minutes: 52,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(`Bad url: ${request.url}`));
|
||||
});
|
||||
|
||||
// Act
|
||||
await GetSummary(req, res);
|
||||
|
||||
// Assert
|
||||
expect(res._getStatusCode()).toBe(200);
|
||||
expect(res.finished).toBe(true);
|
||||
expect(JSON.parse(res._getData())).toEqual({
|
||||
adsBlockedToday: 9700,
|
||||
adsBlockedTodayPercentage: 0.26280140883229475,
|
||||
dnsQueriesToday: 36910,
|
||||
domainsBeingBlocked: 780348,
|
||||
status: [
|
||||
{
|
||||
status: 'enabled',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(errorLogSpy).not.toHaveBeenCalled();
|
||||
expect(warningLogSpy).not.toHaveBeenCalled();
|
||||
|
||||
errorLogSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('combine and return aggregated data when multiple instances', async () => {
|
||||
// arrange
|
||||
const { req, res } = createMocks({
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
vi.mock('./../../../../tools/config/getConfig.ts', () => ({
|
||||
get getConfig() {
|
||||
return mockedGetConfig;
|
||||
},
|
||||
}));
|
||||
|
||||
mockedGetConfig.mockReturnValue({
|
||||
apps: [
|
||||
{
|
||||
id: 'app1',
|
||||
url: 'http://pi.hole',
|
||||
integration: {
|
||||
type: 'pihole',
|
||||
properties: [
|
||||
{
|
||||
field: 'password',
|
||||
type: 'private',
|
||||
value: 'hf3829fj238g8',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'app2',
|
||||
url: 'http://pi2.hole',
|
||||
integration: {
|
||||
type: 'pihole',
|
||||
properties: [
|
||||
{
|
||||
field: 'password',
|
||||
type: 'private',
|
||||
value: 'ayaka',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as ConfigType);
|
||||
const errorLogSpy = vi.spyOn(Consola, 'error');
|
||||
const warningLogSpy = vi.spyOn(Consola, 'warn');
|
||||
|
||||
fetchMock.mockResponse((request) => {
|
||||
if (request.url === 'http://pi.hole/admin/api.php?summaryRaw&auth=hf3829fj238g8') {
|
||||
return JSON.stringify({
|
||||
domains_being_blocked: 3,
|
||||
dns_queries_today: 8,
|
||||
ads_blocked_today: 5,
|
||||
ads_percentage_today: 26,
|
||||
unique_domains: 4,
|
||||
queries_forwarded: 2,
|
||||
queries_cached: 2,
|
||||
clients_ever_seen: 2,
|
||||
unique_clients: 3,
|
||||
dns_queries_all_types: 3,
|
||||
reply_UNKNOWN: 2,
|
||||
reply_NODATA: 3,
|
||||
reply_NXDOMAIN: 5,
|
||||
reply_CNAME: 6,
|
||||
reply_IP: 5,
|
||||
reply_DOMAIN: 3,
|
||||
reply_RRNAME: 2,
|
||||
reply_SERVFAIL: 2,
|
||||
reply_REFUSED: 0,
|
||||
reply_NOTIMP: 0,
|
||||
reply_OTHER: 0,
|
||||
reply_DNSSEC: 0,
|
||||
reply_NONE: 0,
|
||||
reply_BLOB: 1,
|
||||
dns_queries_all_replies: 36910,
|
||||
privacy_level: 0,
|
||||
status: 'enabled',
|
||||
gravity_last_updated: {
|
||||
file_exists: true,
|
||||
absolute: 1682216493,
|
||||
relative: {
|
||||
days: 5,
|
||||
hours: 17,
|
||||
minutes: 52,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (request.url === 'http://pi2.hole/admin/api.php?summaryRaw&auth=ayaka') {
|
||||
return JSON.stringify({
|
||||
domains_being_blocked: 1,
|
||||
dns_queries_today: 3,
|
||||
ads_blocked_today: 2,
|
||||
ads_percentage_today: 47,
|
||||
unique_domains: 4,
|
||||
queries_forwarded: 4,
|
||||
queries_cached: 2,
|
||||
clients_ever_seen: 2,
|
||||
unique_clients: 2,
|
||||
dns_queries_all_types: 1,
|
||||
reply_UNKNOWN: 3,
|
||||
reply_NODATA: 2,
|
||||
reply_NXDOMAIN: 1,
|
||||
reply_CNAME: 3,
|
||||
reply_IP: 2,
|
||||
reply_DOMAIN: 97,
|
||||
reply_RRNAME: 4,
|
||||
reply_SERVFAIL: 28,
|
||||
reply_REFUSED: 0,
|
||||
reply_NOTIMP: 0,
|
||||
reply_OTHER: 0,
|
||||
reply_DNSSEC: 0,
|
||||
reply_NONE: 0,
|
||||
reply_BLOB: 2,
|
||||
dns_queries_all_replies: 4,
|
||||
privacy_level: 0,
|
||||
status: 'disabled',
|
||||
gravity_last_updated: {
|
||||
file_exists: true,
|
||||
absolute: 1682216493,
|
||||
relative: {
|
||||
days: 5,
|
||||
hours: 17,
|
||||
minutes: 52,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(`Bad url: ${request.url}`));
|
||||
});
|
||||
|
||||
// Act
|
||||
await GetSummary(req, res);
|
||||
|
||||
// Assert
|
||||
expect(res._getStatusCode()).toBe(200);
|
||||
expect(res.finished).toBe(true);
|
||||
expect(JSON.parse(res._getData())).toStrictEqual({
|
||||
adsBlockedToday: 7,
|
||||
adsBlockedTodayPercentage: 0.6363636363636364,
|
||||
dnsQueriesToday: 11,
|
||||
domainsBeingBlocked: 4,
|
||||
status: [
|
||||
{
|
||||
appId: 'app1',
|
||||
status: 'enabled',
|
||||
},
|
||||
{
|
||||
appId: 'app2',
|
||||
status: 'disabled',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(errorLogSpy).not.toHaveBeenCalled();
|
||||
expect(warningLogSpy).not.toHaveBeenCalled();
|
||||
|
||||
errorLogSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
60
src/pages/api/modules/dns-hole/summary.ts
Normal file
60
src/pages/api/modules/dns-hole/summary.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import Consola from 'consola';
|
||||
import { getCookie } from 'cookies-next';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { findAppProperty } from '../../../../tools/client/app-properties';
|
||||
import { getConfig } from '../../../../tools/config/getConfig';
|
||||
import { PiHoleClient } from '../../../../tools/server/sdk/pihole/piHole';
|
||||
import { AdStatistics } from '../../../../widgets/dnshole/type';
|
||||
|
||||
export const Get = async (request: NextApiRequest, response: NextApiResponse) => {
|
||||
const configName = getCookie('config-name', { req: request });
|
||||
const config = getConfig(configName?.toString() ?? 'default');
|
||||
|
||||
const applicableApps = config.apps.filter((x) => x.integration?.type === 'pihole');
|
||||
|
||||
const data: AdStatistics = {
|
||||
domainsBeingBlocked: 0,
|
||||
adsBlockedToday: 0,
|
||||
adsBlockedTodayPercentage: 0,
|
||||
dnsQueriesToday: 0,
|
||||
status: [],
|
||||
};
|
||||
|
||||
const adsBlockedTodayPercentageArr: number[] = [];
|
||||
|
||||
for (let i = 0; i < applicableApps.length; i += 1) {
|
||||
const app = applicableApps[i];
|
||||
|
||||
try {
|
||||
const piHole = new PiHoleClient(app.url, findAppProperty(app, 'password'));
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const summary = await piHole.getSummary();
|
||||
|
||||
data.domainsBeingBlocked += summary.domains_being_blocked;
|
||||
data.adsBlockedToday += summary.ads_blocked_today;
|
||||
data.dnsQueriesToday += summary.dns_queries_today;
|
||||
data.status.push({
|
||||
status: summary.status,
|
||||
appId: app.id,
|
||||
});
|
||||
adsBlockedTodayPercentageArr.push(summary.ads_percentage_today);
|
||||
} catch (err) {
|
||||
Consola.error(`Failed to communicate with PiHole at ${app.url}: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
data.adsBlockedTodayPercentage = data.adsBlockedToday / data.dnsQueriesToday;
|
||||
if (Number.isNaN(data.adsBlockedTodayPercentage)) {
|
||||
data.adsBlockedTodayPercentage = 0;
|
||||
}
|
||||
return response.status(200).json(data);
|
||||
};
|
||||
|
||||
export default async (request: NextApiRequest, response: NextApiResponse) => {
|
||||
if (request.method === 'GET') {
|
||||
return Get(request, response);
|
||||
}
|
||||
|
||||
return response.status(405);
|
||||
};
|
||||
Reference in New Issue
Block a user