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"
|
url: "http://192.168.0.151/admin"
|
||||||
apikey: "<---insert-api-key-here--->" # optional, needed if web interface is password protected
|
apikey: "<---insert-api-key-here--->" # optional, needed if web interface is password protected
|
||||||
type: "PiHole"
|
type: "PiHole"
|
||||||
|
apiVersion: 5 # optional, defaults to 5. Use 6 if your PiHole instance uses API v6
|
||||||
```
|
```
|
||||||
|
|
||||||
**Remarks:**
|
**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
|
## Ping
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"domains_being_blocked": 152588,
|
"domains_being_blocked": 152588,
|
||||||
"dns_queries_today": 0,
|
"dns_queries_today": 0,
|
||||||
"ads_blocked_today": 0,
|
"ads_blocked_today": 0,
|
||||||
"ads_percentage_today": 42,
|
"percent_blocked": 42,
|
||||||
"unique_domains": 0,
|
"unique_domains": 0,
|
||||||
"queries_forwarded": 0,
|
"queries_forwarded": 0,
|
||||||
"queries_cached": 0,
|
"queries_cached": 0,
|
||||||
|
|
|
@ -26,36 +26,195 @@ export default {
|
||||||
name: "PiHole",
|
name: "PiHole",
|
||||||
mixins: [service],
|
mixins: [service],
|
||||||
props: {
|
props: {
|
||||||
item: Object,
|
item: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
status: "",
|
status: "",
|
||||||
ads_percentage_today: 0,
|
percent_blocked: 0,
|
||||||
|
sessionId: null,
|
||||||
|
sessionExpiry: null,
|
||||||
|
retryCount: 0,
|
||||||
|
maxRetries: 3,
|
||||||
|
retryDelay: 5000,
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
percentage: function () {
|
percentage: function () {
|
||||||
if (this.ads_percentage_today) {
|
if (this.percent_blocked) {
|
||||||
return this.ads_percentage_today.toFixed(1);
|
return this.percent_blocked.toFixed(1);
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
},
|
},
|
||||||
|
isAuthenticated() {
|
||||||
|
return this.sessionId && this.sessionExpiry && Date.now() < this.sessionExpiry;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
created() {
|
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: {
|
methods: {
|
||||||
fetchStatus: async function () {
|
startStatusPolling: function () {
|
||||||
const authQueryParams = this.item.apikey
|
this.fetchStatus();
|
||||||
? `?summaryRaw&auth=${this.item.apikey}`
|
// Poll every 5 minutes
|
||||||
: "";
|
this.pollInterval = setInterval(this.fetchStatus, 300000);
|
||||||
const result = await this.fetch(`/api.php${authQueryParams}`).catch((e) =>
|
},
|
||||||
console.log(e),
|
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;
|
||||||
|
|
||||||
this.status = result.status;
|
try {
|
||||||
this.ads_percentage_today = result.ads_percentage_today;
|
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>
|
</script>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue