mirror of
https://github.com/wneessen/go-hibp.git
synced 2024-11-14 01:12:54 +01:00
Added BreachedAccount() to breaches API
Also added WithUserAgent() to the HIBP client for custom UA configuration
This commit is contained in:
parent
0b3734da33
commit
ed7f680919
5 changed files with 213 additions and 39 deletions
105
breach.go
105
breach.go
|
@ -100,25 +100,10 @@ func (b *BreachApi) Breaches(options ...BreachOption) ([]*Breach, *http.Response
|
||||||
queryParams := b.setBreachOpts(options...)
|
queryParams := b.setBreachOpts(options...)
|
||||||
apiUrl := fmt.Sprintf("%s/breaches", BaseUrl)
|
apiUrl := fmt.Sprintf("%s/breaches", BaseUrl)
|
||||||
|
|
||||||
hreq, err := b.hibp.HttpReq(http.MethodGet, apiUrl, queryParams)
|
hb, hr, err := b.apiCall(http.MethodGet, apiUrl, queryParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
hr, err := b.hibp.hc.Do(hreq)
|
|
||||||
if err != nil {
|
|
||||||
return nil, hr, err
|
|
||||||
}
|
|
||||||
if hr.StatusCode != 200 {
|
|
||||||
return nil, hr, fmt.Errorf("API responded with non HTTP-200: %s", hr.Status)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = hr.Body.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
hb, err := io.ReadAll(hr.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, hr, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var breachList []*Breach
|
var breachList []*Breach
|
||||||
if err := json.Unmarshal(hb, &breachList); err != nil {
|
if err := json.Unmarshal(hb, &breachList); err != nil {
|
||||||
|
@ -137,26 +122,10 @@ func (b *BreachApi) BreachByName(n string, options ...BreachOption) (*Breach, *h
|
||||||
}
|
}
|
||||||
|
|
||||||
apiUrl := fmt.Sprintf("%s/breach/%s", BaseUrl, n)
|
apiUrl := fmt.Sprintf("%s/breach/%s", BaseUrl, n)
|
||||||
|
hb, hr, err := b.apiCall(http.MethodGet, apiUrl, queryParams)
|
||||||
hreq, err := b.hibp.HttpReq(http.MethodGet, apiUrl, queryParams)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
hr, err := b.hibp.hc.Do(hreq)
|
|
||||||
if err != nil {
|
|
||||||
return nil, hr, err
|
|
||||||
}
|
|
||||||
if hr.StatusCode != 200 {
|
|
||||||
return nil, hr, fmt.Errorf("API responded with non HTTP-200: %s", hr.Status)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = hr.Body.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
hb, err := io.ReadAll(hr.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, hr, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var breachDetails *Breach
|
var breachDetails *Breach
|
||||||
if err := json.Unmarshal(hb, &breachDetails); err != nil {
|
if err := json.Unmarshal(hb, &breachDetails); err != nil {
|
||||||
|
@ -166,6 +135,45 @@ func (b *BreachApi) BreachByName(n string, options ...BreachOption) (*Breach, *h
|
||||||
return breachDetails, hr, nil
|
return breachDetails, hr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DataClasses are attribute of a record compromised in a breach. This method returns a list of strings
|
||||||
|
// with all registered data classes known to HIBP
|
||||||
|
func (b *BreachApi) DataClasses() ([]string, *http.Response, error) {
|
||||||
|
apiUrl := fmt.Sprintf("%s/dataclasses", BaseUrl)
|
||||||
|
hb, hr, err := b.apiCall(http.MethodGet, apiUrl, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var dataClasses []string
|
||||||
|
if err := json.Unmarshal(hb, &dataClasses); err != nil {
|
||||||
|
return nil, hr, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataClasses, hr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BreachedAccount returns a single breached site based on its name
|
||||||
|
func (b *BreachApi) BreachedAccount(a string, options ...BreachOption) ([]*Breach, *http.Response, error) {
|
||||||
|
queryParams := b.setBreachOpts(options...)
|
||||||
|
|
||||||
|
if a == "" {
|
||||||
|
return nil, nil, fmt.Errorf("no account id given")
|
||||||
|
}
|
||||||
|
|
||||||
|
apiUrl := fmt.Sprintf("%s/breachedaccount/%s", BaseUrl, a)
|
||||||
|
hb, hr, err := b.apiCall(http.MethodGet, apiUrl, queryParams)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var breachDetails []*Breach
|
||||||
|
if err := json.Unmarshal(hb, &breachDetails); err != nil {
|
||||||
|
return nil, hr, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return breachDetails, hr, nil
|
||||||
|
}
|
||||||
|
|
||||||
// WithDomain sets the domain filter for the breaches API
|
// WithDomain sets the domain filter for the breaches API
|
||||||
func WithDomain(d string) BreachOption {
|
func WithDomain(d string) BreachOption {
|
||||||
return func(b *BreachApi) {
|
return func(b *BreachApi) {
|
||||||
|
@ -174,6 +182,7 @@ func WithDomain(d string) BreachOption {
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithoutTruncate disables the truncateResponse parameter in the breaches API
|
// WithoutTruncate disables the truncateResponse parameter in the breaches API
|
||||||
|
// This option only influences the BreachedAccount method
|
||||||
func WithoutTruncate() BreachOption {
|
func WithoutTruncate() BreachOption {
|
||||||
return func(b *BreachApi) {
|
return func(b *BreachApi) {
|
||||||
b.disableTrunc = true
|
b.disableTrunc = true
|
||||||
|
@ -217,6 +226,9 @@ func (b *BreachApi) setBreachOpts(options ...BreachOption) map[string]string {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, opt := range options {
|
for _, opt := range options {
|
||||||
|
if opt == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
opt(b)
|
opt(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,3 +246,30 @@ func (b *BreachApi) setBreachOpts(options ...BreachOption) map[string]string {
|
||||||
|
|
||||||
return queryParams
|
return queryParams
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// apiCall performs the API call to the breaches API and returns the HTTP response body JSON as
|
||||||
|
// byte array
|
||||||
|
func (b *BreachApi) apiCall(m string, p string, q map[string]string) ([]byte, *http.Response, error) {
|
||||||
|
hreq, err := b.hibp.HttpReq(m, p, q)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
hr, err := b.hibp.hc.Do(hreq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, hr, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = hr.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
hb, err := io.ReadAll(hr.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, hr, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if hr.StatusCode != 200 {
|
||||||
|
return nil, hr, fmt.Errorf("API responded with non HTTP-200: %s - %s", hr.Status, hb)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hb, hr, nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package hibp
|
package hibp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,6 +23,23 @@ func TestBreaches(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestBreachesWithNil tests the Breaches() method of the breaches API with a nil option
|
||||||
|
func TestBreachesWithNil(t *testing.T) {
|
||||||
|
hc := New()
|
||||||
|
if hc == nil {
|
||||||
|
t.Errorf("hibp client creation failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
breachList, _, err := hc.BreachApi.Breaches(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if breachList != nil && len(breachList) <= 0 {
|
||||||
|
t.Error("breaches list returned 0 results")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestBreachesWithDomain tests the Breaches() method of the breaches API for a specific domain
|
// TestBreachesWithDomain tests the Breaches() method of the breaches API for a specific domain
|
||||||
func TestBreachesWithDomain(t *testing.T) {
|
func TestBreachesWithDomain(t *testing.T) {
|
||||||
testTable := []struct {
|
testTable := []struct {
|
||||||
|
@ -133,3 +152,69 @@ func TestBreachByName(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestDataClasses tests the DataClasses() method of the breaches API
|
||||||
|
func TestDataClasses(t *testing.T) {
|
||||||
|
hc := New()
|
||||||
|
if hc == nil {
|
||||||
|
t.Errorf("hibp client creation failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
classList, _, err := hc.BreachApi.DataClasses()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if classList != nil && len(classList) <= 0 {
|
||||||
|
t.Error("breaches list returned 0 results")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBreachedAccount tests the BreachedAccount() method of the breaches API
|
||||||
|
func TestBreachedAccount(t *testing.T) {
|
||||||
|
testTable := []struct {
|
||||||
|
testName string
|
||||||
|
accountName string
|
||||||
|
isBreached bool
|
||||||
|
moreThanOneBreach bool
|
||||||
|
}{
|
||||||
|
{"account-exists is breached once", "account-exists", true,
|
||||||
|
false},
|
||||||
|
{"multiple-breaches is breached multiple times", "multiple-breaches",
|
||||||
|
true, true},
|
||||||
|
{"opt-out is not breached", "opt-out", false, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
hc := New(WithApiKey(os.Getenv("HIBP_API_KEY")))
|
||||||
|
if hc == nil {
|
||||||
|
t.Error("failed to create HIBP client")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testTable {
|
||||||
|
t.Run(tc.testName, func(t *testing.T) {
|
||||||
|
breachDetails, _, err := hc.BreachApi.BreachedAccount(
|
||||||
|
fmt.Sprintf("%s@hibp-integration-tests.com", tc.accountName))
|
||||||
|
if err != nil && tc.isBreached {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if breachDetails == nil && tc.isBreached {
|
||||||
|
t.Errorf("breach for the account %q is expected, but returned 0 results.",
|
||||||
|
tc.accountName)
|
||||||
|
}
|
||||||
|
if breachDetails != nil && !tc.isBreached {
|
||||||
|
t.Errorf("breach for the account %q is expected to be not breached, but returned breach details.",
|
||||||
|
tc.accountName)
|
||||||
|
}
|
||||||
|
if breachDetails != nil && tc.moreThanOneBreach && len(breachDetails) <= 1 {
|
||||||
|
t.Errorf("breach for the account %q is expected to be breached multiple, but returned %d breaches.",
|
||||||
|
tc.accountName, len(breachDetails))
|
||||||
|
}
|
||||||
|
if breachDetails != nil && !tc.moreThanOneBreach && len(breachDetails) > 1 {
|
||||||
|
t.Errorf("breach for the account %q is expected to be breached once, but returned %d breaches.",
|
||||||
|
tc.accountName, len(breachDetails))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
hibp "github.com/wneessen/go-hibp"
|
"github.com/wneessen/go-hibp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
27
hibp.go
27
hibp.go
|
@ -3,7 +3,6 @@ package hibp
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -11,16 +10,22 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Version represents the version of this package
|
// Version represents the version of this package
|
||||||
const Version = "0.1.2"
|
const Version = "0.1.3"
|
||||||
|
|
||||||
// BaseUrl is the base URL for the majority of API calls
|
// BaseUrl is the base URL for the majority of API calls
|
||||||
const BaseUrl = "https://haveibeenpwned.com/api/v3"
|
const BaseUrl = "https://haveibeenpwned.com/api/v3"
|
||||||
|
|
||||||
|
// DefaultUserAgent defines the default UA string for the HTTP client
|
||||||
|
// Currently the URL in the UA string is comment out, as there is a bug in the HIBP API
|
||||||
|
// not allowing multiple slashes
|
||||||
|
const DefaultUserAgent = `go-hibp v` + Version // + ` - https://github.com/wneessen/go-hibp`
|
||||||
|
|
||||||
// Client is the HIBP client object
|
// Client is the HIBP client object
|
||||||
type Client struct {
|
type Client struct {
|
||||||
hc *http.Client // HTTP client to perform the API requests
|
hc *http.Client // HTTP client to perform the API requests
|
||||||
to time.Duration // HTTP client timeout
|
to time.Duration // HTTP client timeout
|
||||||
ak string // HIBP API key
|
ak string // HIBP API key
|
||||||
|
ua string // User agent string for the HTTP client
|
||||||
|
|
||||||
PwnedPassApi *PwnedPassApi // Reference to the PwnedPassApi API
|
PwnedPassApi *PwnedPassApi // Reference to the PwnedPassApi API
|
||||||
PwnedPassApiOpts *PwnedPasswordOptions // Additional options for the PwnedPassApi API
|
PwnedPassApiOpts *PwnedPasswordOptions // Additional options for the PwnedPassApi API
|
||||||
|
@ -38,9 +43,13 @@ func New(options ...Option) *Client {
|
||||||
// Set defaults
|
// Set defaults
|
||||||
c.to = time.Second * 5
|
c.to = time.Second * 5
|
||||||
c.PwnedPassApiOpts = &PwnedPasswordOptions{}
|
c.PwnedPassApiOpts = &PwnedPasswordOptions{}
|
||||||
|
c.ua = DefaultUserAgent
|
||||||
|
|
||||||
// Set additional options
|
// Set additional options
|
||||||
for _, opt := range options {
|
for _, opt := range options {
|
||||||
|
if opt == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
opt(c)
|
opt(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,6 +84,16 @@ func WithPwnedPadding() Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithUserAgent sets a custom user agent string for the HTTP client
|
||||||
|
func WithUserAgent(a string) Option {
|
||||||
|
if a == "" {
|
||||||
|
return func(c *Client) {}
|
||||||
|
}
|
||||||
|
return func(c *Client) {
|
||||||
|
c.ua = a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// HttpReq performs an HTTP request to the corresponding API
|
// HttpReq performs an HTTP request to the corresponding API
|
||||||
func (c *Client) HttpReq(m, p string, q map[string]string) (*http.Request, error) {
|
func (c *Client) HttpReq(m, p string, q map[string]string) (*http.Request, error) {
|
||||||
u, err := url.Parse(p)
|
u, err := url.Parse(p)
|
||||||
|
@ -106,12 +125,10 @@ func (c *Client) HttpReq(m, p string, q map[string]string) (*http.Request, error
|
||||||
}
|
}
|
||||||
|
|
||||||
hr.Header.Set("Accept", "application/json")
|
hr.Header.Set("Accept", "application/json")
|
||||||
hr.Header.Set("User-Agent", fmt.Sprintf("go-hibp v%s - https://github.com/wneessen/go-hibp", Version))
|
hr.Header.Set("user-agent", c.ua)
|
||||||
|
|
||||||
if c.ak != "" {
|
if c.ak != "" {
|
||||||
hr.Header.Set("hibp-api-key", c.ak)
|
hr.Header.Set("hibp-api-key", c.ak)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.PwnedPassApiOpts.WithPadding {
|
if c.PwnedPassApiOpts.WithPadding {
|
||||||
hr.Header.Set("Add-Padding", "true")
|
hr.Header.Set("Add-Padding", "true")
|
||||||
}
|
}
|
||||||
|
|
33
hibp_test.go
33
hibp_test.go
|
@ -1,6 +1,7 @@
|
||||||
package hibp
|
package hibp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -14,6 +15,14 @@ func TestNew(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestNewWithNil tests the New() function with a nil option
|
||||||
|
func TestNewWithNil(t *testing.T) {
|
||||||
|
hc := New(nil)
|
||||||
|
if hc == nil {
|
||||||
|
t.Errorf("hibp client creation failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestNewWithHttpTimeout tests the New() function with the http timeout option
|
// TestNewWithHttpTimeout tests the New() function with the http timeout option
|
||||||
func TestNewWithHttpTimeout(t *testing.T) {
|
func TestNewWithHttpTimeout(t *testing.T) {
|
||||||
hc := New(WithHttpTimeout(time.Second * 10))
|
hc := New(WithHttpTimeout(time.Second * 10))
|
||||||
|
@ -53,3 +62,27 @@ func TestNewWithApiKey(t *testing.T) {
|
||||||
apiKey, hc.ak)
|
apiKey, hc.ak)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestNewWithUserAgent tests the New() function with a custom user agent
|
||||||
|
func TestNewWithUserAgent(t *testing.T) {
|
||||||
|
hc := New()
|
||||||
|
if hc == nil {
|
||||||
|
t.Errorf("hibp client creation failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if hc.ua != DefaultUserAgent {
|
||||||
|
t.Errorf("hibp client default user agent was not set properly. Expected %s, got: %s",
|
||||||
|
DefaultUserAgent, hc.ua)
|
||||||
|
}
|
||||||
|
|
||||||
|
custUA := fmt.Sprintf("customUA v%s", Version)
|
||||||
|
hc = New(WithUserAgent(custUA))
|
||||||
|
if hc == nil {
|
||||||
|
t.Errorf("hibp client creation failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if hc.ua != custUA {
|
||||||
|
t.Errorf("hibp client custom user agent was not set properly. Expected %s, got: %s",
|
||||||
|
custUA, hc.ua)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue