mirror of https://github.com/bastienwirtz/homer
feat(pihole): Support Pi-hole v6 API with session management (#875)
Add support for Pi-hole v6 API while maintaining v5 compatibility. The component now handles both API versions with proper session-based authentication for v6 and legacy authentication for v5. Includes automated retries, session caching, and proper cleanup. - Add new apiVersion config option (default: 5, accepts: 5,6) - Implement session-based auth with caching for v6 API - Add auto-retry mechanism with configurable attempts - Add periodic status polling for v6 API - Separate v5/v6 API handling logic for better maintenance - Improve error handling and status display - Update documentation with v6 API support details Breaking changes: None. V5 remains default for backward compatibility. V6 mode must be explicitly enabled by setting apiVersion: 6 in config.pull/923/head
parent
1f6e6e7cce
commit
9307f5a926
|
@ -471,10 +471,12 @@ The following configuration is available for the PiHole service.
|
|||
url: "http://192.168.0.151/admin"
|
||||
apikey: "<---insert-api-key-here--->" # optional, needed if web interface is password protected
|
||||
type: "PiHole"
|
||||
apiVersion: 5 # optional, defaults to 5. Use 6 if your PiHole instance uses API v6
|
||||
```
|
||||
|
||||
**Remarks:**
|
||||
If PiHole web interface is password protected, obtain the `apikey` from Settings > API/Web interface > Show API token.
|
||||
- If PiHole web interface is password protected, obtain the `apikey` from Settings > API/Web interface > Show API token.
|
||||
- For PiHole instances using API v6, set `apiVersion: 6` in your configuration. This enables session management and proper authentication handling.
|
||||
|
||||
## Ping
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"domains_being_blocked": 152588,
|
||||
"dns_queries_today": 0,
|
||||
"ads_blocked_today": 0,
|
||||
"ads_percentage_today": 42,
|
||||
"percent_blocked": 42,
|
||||
"unique_domains": 0,
|
||||
"queries_forwarded": 0,
|
||||
"queries_cached": 0,
|
||||
|
|
|
@ -26,36 +26,195 @@ export default {
|
|||
name: "PiHole",
|
||||
mixins: [service],
|
||||
props: {
|
||||
item: Object,
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
status: "",
|
||||
ads_percentage_today: 0,
|
||||
percent_blocked: 0,
|
||||
sessionId: null,
|
||||
sessionExpiry: null,
|
||||
retryCount: 0,
|
||||
maxRetries: 3,
|
||||
retryDelay: 5000,
|
||||
}),
|
||||
computed: {
|
||||
percentage: function () {
|
||||
if (this.ads_percentage_today) {
|
||||
return this.ads_percentage_today.toFixed(1);
|
||||
if (this.percent_blocked) {
|
||||
return this.percent_blocked.toFixed(1);
|
||||
}
|
||||
return "";
|
||||
},
|
||||
isAuthenticated() {
|
||||
return this.sessionId && this.sessionExpiry && Date.now() < this.sessionExpiry;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchStatus();
|
||||
if (parseInt(this.item.apiVersion, 10) === 6) {
|
||||
this.loadCachedSession();
|
||||
this.startStatusPolling();
|
||||
} else {
|
||||
this.fetchStatus_v5();
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (parseInt(this.item.apiVersion, 10) === 6) {
|
||||
this.stopStatusPolling();
|
||||
this.deleteSession();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchStatus: async function () {
|
||||
const authQueryParams = this.item.apikey
|
||||
? `?summaryRaw&auth=${this.item.apikey}`
|
||||
: "";
|
||||
const result = await this.fetch(`/api.php${authQueryParams}`).catch((e) =>
|
||||
console.log(e),
|
||||
);
|
||||
|
||||
this.status = result.status;
|
||||
this.ads_percentage_today = result.ads_percentage_today;
|
||||
startStatusPolling: function () {
|
||||
this.fetchStatus();
|
||||
// Poll every 5 minutes
|
||||
this.pollInterval = setInterval(this.fetchStatus, 300000);
|
||||
},
|
||||
},
|
||||
stopStatusPolling: function () {
|
||||
if (this.pollInterval) {
|
||||
clearInterval(this.pollInterval);
|
||||
}
|
||||
},
|
||||
loadCachedSession: function () {
|
||||
try {
|
||||
const cachedSession = localStorage.getItem(`pihole_session_${this.item.url}`);
|
||||
if (cachedSession) {
|
||||
const session = JSON.parse(cachedSession);
|
||||
if (session.expiry > Date.now()) {
|
||||
this.sessionId = session.sid;
|
||||
this.sessionExpiry = session.expiry;
|
||||
} else {
|
||||
this.removeCacheSession();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load cached session:", e);
|
||||
this.removeCacheSession();
|
||||
}
|
||||
},
|
||||
removeCacheSession: function () {
|
||||
localStorage.removeItem(`pihole_session_${this.item.url}`);
|
||||
this.sessionId = null;
|
||||
this.sessionExpiry = null;
|
||||
},
|
||||
deleteSession: async function () {
|
||||
if (!this.sessionId) return;
|
||||
|
||||
try {
|
||||
await this.fetch(`/api/auth/session/${encodeURIComponent(this.sessionId)}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Failed to delete session:", e);
|
||||
} finally {
|
||||
this.removeCacheSession();
|
||||
}
|
||||
},
|
||||
authenticate: async function () {
|
||||
if (!this.item.apikey) {
|
||||
console.error("API key is required for PiHole authentication");
|
||||
this.status = "disabled";
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const authResponse = await this.fetch("/api/auth", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ password: this.item.apikey }),
|
||||
});
|
||||
|
||||
if (authResponse?.session?.sid) {
|
||||
this.sessionId = authResponse.session.sid;
|
||||
this.sessionExpiry = Date.now() + (authResponse.session.validity * 1000);
|
||||
|
||||
localStorage.setItem(`pihole_session_${this.item.url}`, JSON.stringify({
|
||||
sid: this.sessionId,
|
||||
expiry: this.sessionExpiry
|
||||
}));
|
||||
|
||||
this.retryCount = 0;
|
||||
return true;
|
||||
}
|
||||
throw new Error("Invalid authentication response");
|
||||
} catch (e) {
|
||||
console.error("Authentication failed:", e);
|
||||
this.status = "disabled";
|
||||
return false;
|
||||
}
|
||||
},
|
||||
retryWithDelay: async function () {
|
||||
if (this.retryCount < this.maxRetries) {
|
||||
this.retryCount++;
|
||||
await new Promise(resolve => setTimeout(resolve, this.retryDelay));
|
||||
return this.fetchStatus();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
fetchStatus: async function () {
|
||||
try {
|
||||
if (!this.isAuthenticated) {
|
||||
const authenticated = await this.authenticate();
|
||||
if (!authenticated) return;
|
||||
}
|
||||
|
||||
const url = `${this.endpoint}/${`api/stats/summary?sid=${encodeURIComponent(this.sessionId)}`.replace(/^\/+/, '')}`;
|
||||
const response = await fetch(url);
|
||||
this.status = "enabled";
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
if (result?.queries?.percent_blocked !== undefined) {
|
||||
this.percent_blocked = result.queries.percent_blocked;
|
||||
this.retryCount = 0;
|
||||
} else {
|
||||
throw new Error("Invalid response format");
|
||||
}
|
||||
} else if (response.status === 401) {
|
||||
this.removeCacheSession();
|
||||
return this.retryWithDelay();
|
||||
} else {
|
||||
throw new Error(`HTTP error: ${response.status}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch status:", e);
|
||||
if (e.message.includes("HTTP error: 401") || e.message.includes("HTTP error: 403")) {
|
||||
this.removeCacheSession();
|
||||
return this.retryWithDelay();
|
||||
}
|
||||
this.status = "disabled";
|
||||
this.removeCacheSession();
|
||||
}
|
||||
},
|
||||
async fetchStatus_v5() {
|
||||
try {
|
||||
const params = {};
|
||||
if (this.item.apikey) {
|
||||
params.auth = this.item.apikey;
|
||||
}
|
||||
const url = new URL(`${this.endpoint}/api.php`);
|
||||
Object.keys(params).forEach(key => url.searchParams.append(key, params[key]));
|
||||
|
||||
const response = await fetch(url);
|
||||
this.status = response.status.toString();
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
if (result?.ads_percentage_today !== undefined) {
|
||||
this.percent_blocked = result.ads_percentage_today;
|
||||
}
|
||||
} else {
|
||||
throw new Error(`HTTP error: ${response.status}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch v5 status:", e);
|
||||
this.status = "error";
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
Loading…
Reference in New Issue