forked from wtfutil/wtf
/
client.go
143 lines (114 loc) · 3.02 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
package hibp
import (
"encoding/json"
"errors"
"fmt"
"io"
"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), http.NoBody)
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 := io.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
}