github.com/wtfutil/wtf@v0.43.0/modules/hibp/client.go (about)

     1  package hibp
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"time"
    10  )
    11  
    12  const (
    13  	apiURL            = "https://haveibeenpwned.com/api/v3/breachedaccount/"
    14  	clientTimeoutSecs = 2
    15  	userAgent         = "WTFUtil"
    16  )
    17  
    18  type hibpError struct {
    19  	StatusCode int    `json:"statusCode"`
    20  	Message    string `json:"message"`
    21  }
    22  
    23  func (widget *Widget) fullURL(account string, truncated bool) string {
    24  	truncStr := "false"
    25  	if truncated {
    26  		truncStr = "true"
    27  	}
    28  
    29  	return apiURL + account + fmt.Sprintf("?truncateResponse=%s", truncStr)
    30  }
    31  
    32  func (widget *Widget) fetchForAccount(account string, since string) (*Status, error) {
    33  	if account == "" {
    34  		return nil, nil
    35  	}
    36  
    37  	hibpClient := http.Client{
    38  		Timeout: time.Second * clientTimeoutSecs,
    39  	}
    40  
    41  	asTruncated := true
    42  	if since != "" {
    43  		asTruncated = false
    44  	}
    45  
    46  	request, err := http.NewRequest(http.MethodGet, widget.fullURL(account, asTruncated), http.NoBody)
    47  	if err != nil {
    48  		return nil, err
    49  	}
    50  
    51  	request.Header.Set("User-Agent", userAgent)
    52  	request.Header.Set("hibp-api-key", widget.settings.apiKey)
    53  
    54  	response, getErr := hibpClient.Do(request)
    55  	if getErr != nil {
    56  		return nil, err
    57  	}
    58  
    59  	body, readErr := io.ReadAll(response.Body)
    60  	if readErr != nil {
    61  		return nil, err
    62  	}
    63  
    64  	hibpErr := widget.validateHTTPResponse(response.StatusCode, body)
    65  	if hibpErr != nil {
    66  		return nil, errors.New(hibpErr.Message)
    67  	}
    68  
    69  	stat, err := widget.parseResponseBody(account, body)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  
    74  	return stat, nil
    75  }
    76  
    77  func (widget *Widget) parseResponseBody(account string, body []byte) (*Status, error) {
    78  	breaches := []Breach{}
    79  	stat := NewStatus(account, breaches)
    80  
    81  	if len(body) == 0 {
    82  		// If the body is empty then there's no breaches
    83  		return stat, nil
    84  	}
    85  
    86  	jsonErr := json.Unmarshal(body, &breaches)
    87  	if jsonErr != nil {
    88  		return stat, jsonErr
    89  	}
    90  
    91  	breaches = widget.filterBreaches(breaches)
    92  	stat.Breaches = breaches
    93  
    94  	return stat, nil
    95  }
    96  
    97  func (widget *Widget) filterBreaches(breaches []Breach) []Breach {
    98  	// If there's no valid since value in the settings, there's no point in trying to filter
    99  	// the breaches on that value, they'll all pass
   100  	if !widget.settings.HasSince() {
   101  		return breaches
   102  	}
   103  
   104  	sinceDate, err := widget.settings.SinceDate()
   105  	if err != nil {
   106  		return breaches
   107  	}
   108  
   109  	latestBreaches := []Breach{}
   110  
   111  	for _, breach := range breaches {
   112  		breachDate, err := breach.BreachDate()
   113  		if err != nil {
   114  			// Append the erring breach here because a failing breach date doesn't mean that
   115  			// the breach itself isn't applicable. The date could be missing or malformed,
   116  			// in which case we err on the side of caution and assume that the breach is valid
   117  			latestBreaches = append(latestBreaches, breach)
   118  			continue
   119  		}
   120  
   121  		if breachDate.After(sinceDate) {
   122  			latestBreaches = append(latestBreaches, breach)
   123  		}
   124  	}
   125  
   126  	return latestBreaches
   127  }
   128  
   129  func (widget *Widget) validateHTTPResponse(responseCode int, body []byte) *hibpError {
   130  	hibpErr := &hibpError{}
   131  
   132  	switch responseCode {
   133  	case 401, 402:
   134  		err := json.Unmarshal(body, hibpErr)
   135  		if err != nil {
   136  			return nil
   137  		}
   138  	default:
   139  		hibpErr = nil
   140  	}
   141  
   142  	return hibpErr
   143  }