-
Notifications
You must be signed in to change notification settings - Fork 0
/
hibpclient.go
156 lines (133 loc) · 3.77 KB
/
hibpclient.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
144
145
146
147
148
149
150
151
152
153
154
155
156
package main
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"sort"
"strconv"
"time"
)
// returns haveibeenpwned breaches and pastes for the given user or error upon failure
func (api *HIBPClient) GetHIBPLeaks(account string) ([]HIBPBreach, []HIBPPaste, error) {
breaches, err := api.GetHIBPBreaches(account)
if err != nil {
return nil, nil, err
}
pastes, err := api.GetHIBPPastes(account)
if err != nil {
return nil, nil, err
}
return breaches, pastes, nil
}
const (
HIBPBaseURL = "https://haveibeenpwned.com"
HIBPGetAccountBreachesURL = "/api/v2/breachedaccount/%s"
HIBPGetAccountPastesURL = "/api/v2/pasteaccount/%s"
DefaultMaxRetries = 10
DefaultRequestDelay = 10 * time.Second
DefaultHTTPTimeout = 10 * time.Second
)
var logger *log.Logger
type HIBPBreach struct {
Name string
Title string
Domain string
BreachDate string
AddedDate time.Time
ModifiedDate time.Time
PwnCount uint64
Description string
DataClasses []string
IsVerified bool
IsFabricated bool
IsSensitive bool
IsRetired bool
IsSpamList bool
LogoPath string
}
type HIBPPaste struct {
Source string
Id string
Title string
Date time.Time
EmailCount uint64
}
type HIBPClient struct {
client *http.Client
MaxRetries uint
RequestDelay time.Duration
baseURL string
nextSleep time.Duration // TODO: sleep once per both pastes / breaches
}
func NewHIBPClient() *HIBPClient {
return &HIBPClient{
&http.Client{Timeout: DefaultHTTPTimeout},
DefaultMaxRetries,
DefaultRequestDelay,
HIBPBaseURL,
0,
}
}
func (api *HIBPClient) getHIBPResp(urlTemplate, account string, respObject interface{}) error {
url := api.baseURL + fmt.Sprintf(urlTemplate, account)
for retries := uint(0); retries <= api.MaxRetries; retries++ {
if api.nextSleep > 0 {
logger.Printf("sleeping %s", api.nextSleep.String())
time.Sleep(api.nextSleep)
}
logger.Printf("requesting %s\n", url)
resp, err := api.client.Get(url)
// HIBP will block our IP if we do too many requests in short time
api.nextSleep = api.RequestDelay
if err != nil {
logger.Printf("network error %s", err.Error())
continue
}
if resp.StatusCode == 404 {
return nil
}
if resp.StatusCode != 200 {
// read Retry After header if exists and sleep that many seconds
retryAfter := resp.Header.Get("Retry-After")
backoffSeconds, err := strconv.ParseUint(retryAfter, 10, 64)
if err == nil {
// add 1 second for safety
api.nextSleep = time.Duration(backoffSeconds+1) * time.Second
}
logger.Printf("got http error %d for url %s (Retry-After %s)", resp.StatusCode, url, retryAfter)
continue
}
buf, err := ioutil.ReadAll(resp.Body)
if err != nil {
logger.Printf("error reading server response: %s", err.Error())
continue
}
return json.Unmarshal(buf, &respObject)
}
return errors.New(fmt.Sprintf("max retries exceeded (%d) for %s", api.MaxRetries, url))
}
// returns haveibeenpwned breaches for the given user or error upon failure
func (api *HIBPClient) GetHIBPBreaches(account string) ([]HIBPBreach, error) {
var breaches []HIBPBreach
if err := api.getHIBPResp(HIBPGetAccountBreachesURL, account, &breaches); err != nil {
return nil, err
}
sort.Slice(breaches, func(i, j int) bool {
return breaches[i].BreachDate > breaches[j].BreachDate
})
return breaches, nil
}
// returns haveibeenpwned pastes for the given user or error upon failure
func (api *HIBPClient) GetHIBPPastes(account string) ([]HIBPPaste, error) {
var pastes []HIBPPaste
if err := api.getHIBPResp(HIBPGetAccountPastesURL, account, &pastes); err != nil {
return nil, err
}
sort.Slice(pastes, func(i, j int) bool {
return pastes[i].Date.After(pastes[j].Date)
})
return pastes, nil
}