github.com/wtfutil/wtf@v0.43.0/modules/pihole/client.go (about) 1 package pihole 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "net/http" 8 url2 "net/url" 9 "regexp" 10 "strconv" 11 "strings" 12 "time" 13 ) 14 15 type Status struct { 16 DomainsBeingBlocked string `json:"domains_being_blocked"` 17 DNSQueriesToday string `json:"dns_queries_today"` 18 AdsBlockedToday string `json:"ads_blocked_today"` 19 AdsPercentageToday string `json:"ads_percentage_today"` 20 UniqueDomains string `json:"unique_domains"` 21 QueriesForwarded string `json:"queries_forwarded"` 22 QueriesCached string `json:"queries_cached"` 23 Status string `json:"status"` 24 GravityLastUpdated struct { 25 Relative struct { 26 Days FlexInt `json:"days"` 27 Hours FlexInt `json:"hours"` 28 Minutes FlexInt `json:"minutes"` 29 } 30 } `json:"gravity_last_updated"` 31 } 32 33 func getStatus(c http.Client, apiURL string) (status Status, err error) { 34 var req *http.Request 35 36 var url *url2.URL 37 38 if url, err = url2.Parse(apiURL); err != nil { 39 return status, fmt.Errorf(" failed to parse API URL\n %s", parseError(err)) 40 } 41 42 var query url2.Values 43 44 if query, err = url2.ParseQuery(url.RawQuery); err != nil { 45 return status, fmt.Errorf(" failed to parse query\n %s", parseError(err)) 46 } 47 48 query.Add("summary", "") 49 50 url.RawQuery = query.Encode() 51 if req, err = http.NewRequest("GET", url.String(), http.NoBody); err != nil { 52 return status, fmt.Errorf(" failed to create request\n %s", parseError(err)) 53 } 54 55 var resp *http.Response 56 57 if resp, err = c.Do(req); err != nil || resp == nil { 58 return status, fmt.Errorf(" failed to connect to Pi-hole server\n %s", parseError(err)) 59 } 60 61 defer func() { 62 if closeErr := resp.Body.Close(); closeErr != nil { 63 return 64 } 65 }() 66 67 if resp.StatusCode >= http.StatusBadRequest { 68 return status, fmt.Errorf(" failed to retrieve version from Pi-hole server\n http status code: %d", 69 resp.StatusCode) 70 } 71 72 var rBody []byte 73 74 if rBody, err = io.ReadAll(resp.Body); err != nil { 75 return status, fmt.Errorf(" failed to read status response") 76 } 77 78 if err = json.Unmarshal(rBody, &status); err != nil { 79 return status, fmt.Errorf(" failed to retrieve status: check provided api URL and token\n %s", 80 parseError(err)) 81 } 82 83 return status, err 84 } 85 86 type FlexInt int 87 88 func (fi *FlexInt) UnmarshalJSON(b []byte) error { 89 if b[0] != '"' { 90 return json.Unmarshal(b, (*int)(fi)) 91 } 92 93 var s string 94 95 if err := json.Unmarshal(b, &s); err != nil { 96 return err 97 } 98 99 i, err := strconv.Atoi(s) 100 if err != nil { 101 return err 102 } 103 104 *fi = FlexInt(i) 105 106 return nil 107 } 108 109 type TopItems struct { 110 TopQueries map[string]int `json:"top_queries"` 111 TopAds map[string]int `json:"top_ads"` 112 } 113 114 func getTopItems(c http.Client, settings *Settings) (ti TopItems, err error) { 115 var req *http.Request 116 117 var url *url2.URL 118 119 if url, err = url2.Parse(settings.apiUrl); err != nil { 120 return ti, fmt.Errorf(" failed to parse API URL\n %s", parseError(err)) 121 } 122 123 var query url2.Values 124 125 if query, err = url2.ParseQuery(url.RawQuery); err != nil { 126 return ti, fmt.Errorf(" failed to parse query\n %s", parseError(err)) 127 } 128 129 query.Add("auth", settings.token) 130 query.Add("topItems", strconv.Itoa(settings.showTopItems)) 131 132 url.RawQuery = query.Encode() 133 134 req, err = http.NewRequest("GET", url.String(), http.NoBody) 135 if err != nil { 136 return ti, fmt.Errorf(" failed to create request\n %s", parseError(err)) 137 } 138 139 var resp *http.Response 140 141 if resp, err = c.Do(req); err != nil || resp == nil { 142 return ti, fmt.Errorf(" failed to connect to Pi-hole server\n %s", parseError(err)) 143 } 144 145 defer func() { 146 if closeErr := resp.Body.Close(); closeErr != nil { 147 return 148 } 149 }() 150 151 if resp.StatusCode >= http.StatusBadRequest { 152 return ti, fmt.Errorf(" failed to retrieve version from Pi-hole server\n http status code: %d", 153 resp.StatusCode) 154 } 155 156 var rBody []byte 157 158 rBody, err = io.ReadAll(resp.Body) 159 if err = json.Unmarshal(rBody, &ti); err != nil { 160 return ti, fmt.Errorf(" failed to retrieve top items: check provided api URL and token\n %s", 161 parseError(err)) 162 } 163 164 return ti, err 165 } 166 167 type TopClients struct { 168 TopSources map[string]int `json:"top_sources"` 169 } 170 171 // parseError removes any token from output and ensures a non-nil response 172 func parseError(err error) string { 173 if err == nil { 174 return "unknown error" 175 } 176 177 var re = regexp.MustCompile(`auth=[a-zA-Z0-9]*`) 178 179 return re.ReplaceAllString(err.Error(), "auth=<token>") 180 } 181 182 func getTopClients(c http.Client, settings *Settings) (tc TopClients, err error) { 183 var req *http.Request 184 185 var url *url2.URL 186 187 if url, err = url2.Parse(settings.apiUrl); err != nil { 188 return tc, fmt.Errorf(" failed to parse API URL\n %s", parseError(err)) 189 } 190 191 var query url2.Values 192 193 if query, err = url2.ParseQuery(url.RawQuery); err != nil { 194 return tc, fmt.Errorf(" failed to parse query\n %s", parseError(err)) 195 } 196 197 query.Add("topClients", strconv.Itoa(settings.showTopClients)) 198 query.Add("auth", settings.token) 199 url.RawQuery = query.Encode() 200 201 if req, err = http.NewRequest("GET", url.String(), http.NoBody); err != nil { 202 return tc, fmt.Errorf(" failed to create request\n %s", parseError(err)) 203 } 204 205 var resp *http.Response 206 207 if resp, err = c.Do(req); err != nil || resp == nil { 208 return tc, fmt.Errorf(" failed to connect to Pi-hole server\n %s", parseError(err)) 209 } 210 211 defer func() { 212 if closeErr := resp.Body.Close(); closeErr != nil { 213 return 214 } 215 }() 216 217 if resp.StatusCode >= http.StatusBadRequest { 218 return tc, fmt.Errorf(" failed to retrieve version from Pi-hole server\n http status code: %d", 219 resp.StatusCode) 220 } 221 222 var rBody []byte 223 224 if rBody, err = io.ReadAll(resp.Body); err != nil { 225 return tc, fmt.Errorf(" failed to read top clients response\n %s", parseError(err)) 226 } 227 228 if err = json.Unmarshal(rBody, &tc); err != nil { 229 return tc, fmt.Errorf(" failed to retrieve top clients: check provided api URL and token\n %s", 230 parseError(err)) 231 } 232 233 return tc, err 234 } 235 236 type QueryTypes struct { 237 QueryTypes map[string]float32 `json:"querytypes"` 238 } 239 240 func getQueryTypes(c http.Client, settings *Settings) (qt QueryTypes, err error) { 241 var req *http.Request 242 243 var url *url2.URL 244 245 if url, err = url2.Parse(settings.apiUrl); err != nil { 246 return qt, fmt.Errorf(" failed to parse API URL\n %s", parseError(err)) 247 } 248 249 var query url2.Values 250 251 if query, err = url2.ParseQuery(url.RawQuery); err != nil { 252 return qt, fmt.Errorf(" failed to parse query\n %s", parseError(err)) 253 } 254 255 query.Add("getQueryTypes", strconv.Itoa(settings.showTopClients)) 256 query.Add("auth", settings.token) 257 258 url.RawQuery = query.Encode() 259 260 if req, err = http.NewRequest("GET", url.String(), http.NoBody); err != nil { 261 return qt, fmt.Errorf(" failed to create request\n %s", parseError(err)) 262 } 263 264 var resp *http.Response 265 266 if resp, err = c.Do(req); err != nil || resp == nil { 267 return qt, fmt.Errorf(" failed to connect to Pi-hole server\n %s", parseError(err)) 268 } 269 270 defer func() { 271 if closeErr := resp.Body.Close(); closeErr != nil { 272 return 273 } 274 }() 275 276 if resp.StatusCode >= http.StatusBadRequest { 277 return qt, fmt.Errorf(" failed to retrieve version from Pi-hole server\n http status code: %d", 278 resp.StatusCode) 279 } 280 281 var rBody []byte 282 283 if rBody, err = io.ReadAll(resp.Body); err != nil { 284 return qt, fmt.Errorf(" failed to read top clients response\n %s", parseError(err)) 285 } 286 287 if err = json.Unmarshal(rBody, &qt); err != nil { 288 return qt, fmt.Errorf(" failed to parse query types response\n %s", parseError(err)) 289 } 290 291 return qt, err 292 } 293 294 func checkServer(c http.Client, apiURL string) error { 295 var err error 296 297 var req *http.Request 298 299 var url *url2.URL 300 301 if url, err = url2.Parse(apiURL); err != nil { 302 return fmt.Errorf(" failed to parse API URL\n %s", parseError(err)) 303 } 304 305 if url.Host == "" { 306 return fmt.Errorf(" please specify 'apiUrl' in Pi-hole settings, e.g.\n apiUrl: http://<server>:<port>/admin/api.php") 307 } 308 309 if req, err = http.NewRequest("GET", fmt.Sprintf("%s?version", 310 url.String()), http.NoBody); err != nil { 311 return fmt.Errorf("invalid request: %s", parseError(err)) 312 } 313 314 var resp *http.Response 315 316 if resp, err = c.Do(req); err != nil { 317 return fmt.Errorf(" failed to connect to Pi-hole server\n %s", parseError(err)) 318 } 319 320 defer func() { 321 _ = resp.Body.Close() 322 }() 323 324 if resp.StatusCode >= http.StatusBadRequest { 325 return fmt.Errorf(" failed to retrieve version from Pi-hole server\n http status code: %d", 326 resp.StatusCode) 327 } 328 329 var vResp struct { 330 Version int `json:"version"` 331 } 332 333 var rBody []byte 334 335 if rBody, err = io.ReadAll(resp.Body); err != nil { 336 return fmt.Errorf(" Pi-hole server failed to respond\n %s", parseError(err)) 337 } 338 339 if err = json.Unmarshal(rBody, &vResp); err != nil { 340 return fmt.Errorf(" invalid response returned from Pi-hole Server\n %s", parseError(err)) 341 } 342 343 if vResp.Version != 3 { 344 return fmt.Errorf(" only Pi-hole API version 3 is supported\n version %d was detected", vResp.Version) 345 } 346 347 return err 348 } 349 350 func (widget *Widget) adblockSwitch(action string) { 351 var req *http.Request 352 353 var url *url2.URL 354 url, _ = url2.Parse(widget.settings.apiUrl) 355 356 var query url2.Values 357 query, _ = url2.ParseQuery(url.RawQuery) 358 359 query.Add(strings.ToLower(action), "") 360 query.Add("auth", widget.settings.token) 361 362 url.RawQuery = query.Encode() 363 364 req, _ = http.NewRequest("GET", url.String(), http.NoBody) 365 366 c := getClient() 367 resp, _ := c.Do(req) 368 369 defer func() { 370 _ = resp.Body.Close() 371 }() 372 373 widget.Refresh() 374 } 375 376 func getClient() http.Client { 377 return http.Client{ 378 Transport: &http.Transport{ 379 TLSHandshakeTimeout: 5 * time.Second, 380 DisableKeepAlives: false, 381 DisableCompression: false, 382 ResponseHeaderTimeout: 20 * time.Second, 383 }, 384 Timeout: 21 * time.Second, 385 } 386 }