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 }