package hibp import ( "encoding/json" "errors" "fmt" "io/ioutil" "net/http" "time" ) const ( apiURL = "https://haveibeenpwned.com/api/v3/breachedaccount/" clientTimeoutSecs = 2 userAgent = "WTFUtil" ) type hibpError struct { StatusCode int `json:"statusCode"` Message string `json:"message"` } func (widget *Widget) fullURL(account string, truncated bool) string { truncStr := "false" if truncated { truncStr = "true" } return apiURL + account + fmt.Sprintf("?truncateResponse=%s", truncStr) } func (widget *Widget) fetchForAccount(account string, since string) (*Status, error) { if account == "" { return nil, nil } hibpClient := http.Client{ Timeout: time.Second * clientTimeoutSecs, } asTruncated := true if since != "" { asTruncated = false } request, err := http.NewRequest(http.MethodGet, widget.fullURL(account, asTruncated), nil) if err != nil { return nil, err } request.Header.Set("User-Agent", userAgent) request.Header.Set("hibp-api-key", widget.settings.apiKey) response, getErr := hibpClient.Do(request) if getErr != nil { return nil, err } body, readErr := ioutil.ReadAll(response.Body) if readErr != nil { return nil, err } hibpErr := widget.validateHTTPResponse(response.StatusCode, body) if hibpErr != nil { return nil, errors.New(hibpErr.Message) } stat, err := widget.parseResponseBody(account, body) if err != nil { return nil, err } return stat, nil } func (widget *Widget) parseResponseBody(account string, body []byte) (*Status, error) { breaches := []Breach{} stat := NewStatus(account, breaches) if len(body) == 0 { // If the body is empty then there's no breaches return stat, nil } jsonErr := json.Unmarshal(body, &breaches) if jsonErr != nil { return stat, jsonErr } breaches = widget.filterBreaches(breaches) stat.Breaches = breaches return stat, nil } func (widget *Widget) filterBreaches(breaches []Breach) []Breach { // If there's no valid since value in the settings, there's no point in trying to filter // the breaches on that value, they'll all pass if !widget.settings.HasSince() { return breaches } sinceDate, err := widget.settings.SinceDate() if err != nil { return breaches } latestBreaches := []Breach{} for _, breach := range breaches { breachDate, err := breach.BreachDate() if err != nil { // Append the erring breach here because a failing breach date doesn't mean that // the breach itself isn't applicable. The date could be missing or malformed, // in which case we err on the side of caution and assume that the breach is valid latestBreaches = append(latestBreaches, breach) continue } if breachDate.After(sinceDate) { latestBreaches = append(latestBreaches, breach) } } return latestBreaches } func (widget *Widget) validateHTTPResponse(responseCode int, body []byte) *hibpError { hibpErr := &hibpError{} switch responseCode { case 401, 402: err := json.Unmarshal(body, hibpErr) if err != nil { return nil } default: hibpErr = nil } return hibpErr }