diff --git a/backend/internal/PiHoleDNSPlugin.js b/backend/internal/PiHoleDNSPlugin.js new file mode 100644 index 00000000..2aac300d --- /dev/null +++ b/backend/internal/PiHoleDNSPlugin.js @@ -0,0 +1,90 @@ +const axios = require('axios'); +const cheerio = require('cheerio'); +const qs = require('querystring'); + +const PIHOLE_PLUGIN_ENABLED = process.env.PIHOLE_PLUGIN_ENABLED === 'true'; +const PIHOLE_PASSWORD = process.env.PIHOLE_PASSWORD; +const PIHOLE_LOGIN_URL = 'http://'+process.env.PIHOLE_IP+'/admin/index.php'; +const PIHOLE_CUSTOMDNS_URL = 'http://'+process.env.PIHOLE_IP+'/admin/scripts/pi-hole/php/customdns.php'; + +// IP to entry in pihole dns table +const DNS_TABLE_IP = process.env.DNS_TABLE_IP; + +// Function to update Pi-hole with domain and IP +async function updatePihole(domain, action) { + // Check if the Pi-hole plugin is enabled + if (!PIHOLE_PLUGIN_ENABLED) { + return; + } + try { + // Step 1: Login to Pi-hole to get session cookie + const loginResponse = await axios.post(PIHOLE_LOGIN_URL, qs.stringify({ + pw: PIHOLE_PASSWORD, + }), { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'User-Agent': 'Mozilla/5.0' // Pretend to be a browser + }, + withCredentials: true // Send cookies with the request + }); + + if (loginResponse.status === 200) { + // Extract session cookie (PHPSESSID) + const cookies = loginResponse.headers['set-cookie']; + const sessionCookie = cookies.find((cookie) => cookie.startsWith('PHPSESSID')); + + if (!sessionCookie) { + throw new Error('PHP session cookie not found'); + } + + // Step 2: Fetch HTML content of index.php after login + const indexHtmlResponse = await axios.get(PIHOLE_LOGIN_URL, { + headers: { + Cookie: sessionCookie.split(';')[0] // Send only the PHPSESSID part of the cookie + } + }); + + // Load HTML content into cheerio for DOM manipulation + const $ = cheerio.load(indexHtmlResponse.data); + + // Extract token value from element with ID "token" + const token = $('#token').text().trim(); + + + // Step 3: Add custom DNS record with explicit session cookie and token + const headers = { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'Pragma': 'no-cache', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'en-GB,en;q=0.9', + 'Accept-Encoding': 'gzip, deflate', + 'Connection': 'keep-alive', + 'X-Requested-With': 'XMLHttpRequest', + 'Cookie': sessionCookie.split(';')[0] // Send only the PHPSESSID part of the cookie + }; + + // Request data including token + const requestData = { + action: action, + ip: DNS_TABLE_IP, + domain: domain, + token: token // Use the token retrieved from the HTML page + }; + + // Make the POST request to add custom DNS record + const addRecordResponse = await axios.post(PIHOLE_CUSTOMDNS_URL, qs.stringify(requestData), { + headers: headers + }); + + console.log('PiHole API:', addRecordResponse.data); + } else { + console.error('Login failed:', loginResponse.statusText); + } + } catch (error) { + console.error('Error logging in or adding custom DNS record:', error.message); + } +} + +module.exports = { + updatePihole: updatePihole +}; diff --git a/backend/internal/proxy-host.js b/backend/internal/proxy-host.js index dbff1147..8324e0dd 100644 --- a/backend/internal/proxy-host.js +++ b/backend/internal/proxy-host.js @@ -6,6 +6,7 @@ const internalHost = require('./host'); const internalNginx = require('./nginx'); const internalAuditLog = require('./audit-log'); const internalCertificate = require('./certificate'); +const piHole = require('./PiHoleDNSPlugin'); function omissions () { return ['is_deleted']; @@ -64,9 +65,21 @@ const internalProxyHost = { }); }) .then(() => { + + // Update PiHole + for (let i = 0; i < row.domain_names.length; i++) { + piHole.updatePihole(row.domain_names[i], 'add').then(); + } + return row; }); } else { + + // Update PiHole + for (let i = 0; i < row.domain_names.length; i++) { + piHole.updatePihole(row.domain_names[i], 'add').then(); + } + return row; } }) @@ -153,9 +166,18 @@ const internalProxyHost = { data.certificate_id = cert.id; }) .then(() => { + // Update PiHole + for (let i = 0; i < row.domain_names.length; i++) { + piHole.updatePihole(row.domain_names[i], 'delete').then(); + } return row; }); } else { + // Update PiHole + for (let i = 0; i < row.domain_names.length; i++) { + piHole.updatePihole(row.domain_names[i], 'delete').then(); + + } return row; } }) @@ -181,6 +203,7 @@ const internalProxyHost = { meta: data }) .then(() => { + return saved_row; }); }); @@ -200,6 +223,9 @@ const internalProxyHost = { .then((new_meta) => { row.meta = new_meta; row = internalHost.cleanRowCertificateMeta(row); + for (let i = 0; i < row.domain_names.length; i++) { + piHole.updatePihole(row.domain_names[i], 'add').then(); + } return _.omit(row, omissions()); }); }); @@ -275,6 +301,11 @@ const internalProxyHost = { is_deleted: 1 }) .then(() => { + // Update PiHole + + for (let i = 0; i < row.domain_names.length; i++) { + piHole.updatePihole(row.domain_names[i], 'delete').then(); + } // Delete Nginx Config return internalNginx.deleteConfig('proxy_host', row) .then(() => { diff --git a/backend/package.json b/backend/package.json index b938c9a9..3ff0c321 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,9 +6,11 @@ "dependencies": { "ajv": "^6.12.0", "archiver": "^5.3.0", + "axios": "^1.7.2", "batchflow": "^0.4.0", "bcrypt": "^5.0.0", "body-parser": "^1.19.0", + "cheerio": "^1.0.0-rc.12", "compression": "^1.7.4", "express": "^4.19.2", "express-fileupload": "^1.1.9", diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 14ca2f7a..6621ce7a 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -29,6 +29,12 @@ services: DB_MYSQL_NAME: 'npm' # DB_SQLITE_FILE: "/data/database.sqlite" # DISABLE_IPV6: "true" + # pihole: + # DNS_TABLE_IP: '192.168.10.10' + # PIHOLE_PLUGIN_ENABLED: 'true' + # PIHOLE_PASSWORD: 'password' + # PIHOLE_IP: '192.168.10.2' + volumes: - npm_data:/data - le_data:/etc/letsencrypt