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  }