1package hibp 2 3import ( 4 "bytes" 5 "crypto/tls" 6 "fmt" 7 "io" 8 "log" 9 "net/http" 10 "net/url" 11 "time" 12) 13 14// Version represents the version of this package 15const Version = "1.0.0" 16 17// BaseUrl is the base URL for the majority of API calls 18const BaseUrl = "https://haveibeenpwned.com/api/v3" 19 20// DefaultUserAgent defines the default UA string for the HTTP client 21// Currently the URL in the UA string is comment out, as there is a bug in the HIBP API 22// not allowing multiple slashes 23const DefaultUserAgent = `go-hibp v` + Version // + ` - https://github.com/wneessen/go-hibp` 24 25// Client is the HIBP client object 26type Client struct { 27 hc *http.Client // HTTP client to perform the API requests 28 to time.Duration // HTTP client timeout 29 ak string // HIBP API key 30 ua string // User agent string for the HTTP client 31 32 // If set to true, the HTTP client will sleep instead of failing in case the HTTP 429 33 // rate limit hits a request 34 rlSleep bool 35 36 PwnedPassApi *PwnedPassApi // Reference to the PwnedPassApi API 37 PwnedPassApiOpts *PwnedPasswordOptions // Additional options for the PwnedPassApi API 38 39 BreachApi *BreachApi // Reference to the BreachApi 40 PasteApi *PasteApi // Reference to the PasteApi 41} 42 43// Option is a function that is used for grouping of Client options. 44type Option func(*Client) 45 46// New creates and returns a new HIBP client object 47func New(options ...Option) Client { 48 c := Client{} 49 50 // Set defaults 51 c.to = time.Second * 5 52 c.PwnedPassApiOpts = &PwnedPasswordOptions{} 53 c.ua = DefaultUserAgent 54 55 // Set additional options 56 for _, opt := range options { 57 if opt == nil { 58 continue 59 } 60 opt(&c) 61 } 62 63 // Add a http client to the Client object 64 c.hc = httpClient(c.to) 65 66 // Associate the different HIBP service APIs with the Client 67 c.PwnedPassApi = &PwnedPassApi{hibp: &c} 68 c.BreachApi = &BreachApi{hibp: &c} 69 c.PasteApi = &PasteApi{hibp: &c} 70 71 return c 72} 73 74// WithHttpTimeout overrides the default http client timeout 75func WithHttpTimeout(t time.Duration) Option { 76 return func(c *Client) { 77 c.to = t 78 } 79} 80 81// WithApiKey set the optional API key to the Client object 82func WithApiKey(k string) Option { 83 return func(c *Client) { 84 c.ak = k 85 } 86} 87 88// WithPwnedPadding enables padding-mode for the PwnedPasswords API client 89func WithPwnedPadding() Option { 90 return func(c *Client) { 91 c.PwnedPassApiOpts.WithPadding = true 92 } 93} 94 95// WithUserAgent sets a custom user agent string for the HTTP client 96func WithUserAgent(a string) Option { 97 if a == "" { 98 return func(c *Client) {} 99 } 100 return func(c *Client) { 101 c.ua = a 102 } 103} 104 105// WithRateLimitSleep let's the HTTP client sleep in case the API rate limiting hits (Defaults to fail) 106func WithRateLimitSleep() Option { 107 return func(c *Client) { 108 c.rlSleep = true 109 } 110} 111 112// HttpReq performs an HTTP request to the corresponding API 113func (c *Client) HttpReq(m, p string, q map[string]string) (*http.Request, error) { 114 u, err := url.Parse(p) 115 if err != nil { 116 return nil, err 117 } 118 119 if m == http.MethodGet { 120 uq := u.Query() 121 for k, v := range q { 122 uq.Add(k, v) 123 } 124 u.RawQuery = uq.Encode() 125 } 126 127 hr, err := http.NewRequest(m, u.String(), nil) 128 if err != nil { 129 return nil, err 130 } 131 132 if m == http.MethodPost { 133 pd := url.Values{} 134 for k, v := range q { 135 pd.Add(k, v) 136 } 137 138 rb := io.NopCloser(bytes.NewBufferString(pd.Encode())) 139 hr.Body = rb 140 } 141 142 hr.Header.Set("Accept", "application/json") 143 hr.Header.Set("user-agent", c.ua) 144 if c.ak != "" { 145 hr.Header.Set("hibp-api-key", c.ak) 146 } 147 if c.PwnedPassApiOpts.WithPadding { 148 hr.Header.Set("Add-Padding", "true") 149 } 150 151 return hr, nil 152} 153 154// HttpResBody performs the API call to the given path and returns the response body as byte array 155func (c *Client) HttpResBody(m string, p string, q map[string]string) ([]byte, *http.Response, error) { 156 hreq, err := c.HttpReq(m, p, q) 157 if err != nil { 158 return nil, nil, err 159 } 160 hr, err := c.hc.Do(hreq) 161 if err != nil { 162 return nil, hr, err 163 } 164 defer func() { 165 _ = hr.Body.Close() 166 }() 167 168 hb, err := io.ReadAll(hr.Body) 169 if err != nil { 170 return nil, hr, err 171 } 172 173 if hr.StatusCode == 429 && c.rlSleep { 174 headerDelay := hr.Header.Get("Retry-After") 175 delayTime, err := time.ParseDuration(headerDelay + "s") 176 if err != nil { 177 return nil, hr, err 178 } 179 log.Printf("API rate limit hit. Retrying request in %s", delayTime.String()) 180 time.Sleep(delayTime) 181 return c.HttpResBody(m, p, q) 182 } 183 184 if hr.StatusCode != 200 { 185 return nil, hr, fmt.Errorf("API responded with non HTTP-200: %s - %s", hr.Status, hb) 186 } 187 188 return hb, hr, nil 189} 190 191// httpClient returns a custom http client for the HIBP Client object 192func httpClient(to time.Duration) *http.Client { 193 tlsConfig := &tls.Config{ 194 MaxVersion: tls.VersionTLS13, 195 MinVersion: tls.VersionTLS12, 196 } 197 httpTransport := &http.Transport{TLSClientConfig: tlsConfig} 198 httpClient := &http.Client{ 199 Transport: httpTransport, 200 Timeout: 5 * time.Second, 201 } 202 if to.Nanoseconds() > 0 { 203 httpClient.Timeout = to 204 } 205 206 return httpClient 207} 208