mirror of
https://github.com/wneessen/go-hibp.git
synced 2024-11-22 12:50:50 +01:00
Merge pull request #25 from wneessen/fix/24-verify-and-overhaul-the-error-handling-of-the-different-apis
Fix/24 verify and overhaul the error handling of the different apis
This commit is contained in:
commit
2b0b51ae17
6 changed files with 254 additions and 107 deletions
86
breach.go
86
breach.go
|
@ -96,81 +96,81 @@ type APIDate time.Time
|
||||||
|
|
||||||
// Breaches returns a list of all breaches in the HIBP system
|
// Breaches returns a list of all breaches in the HIBP system
|
||||||
func (b *BreachAPI) Breaches(options ...BreachOption) ([]*Breach, *http.Response, error) {
|
func (b *BreachAPI) Breaches(options ...BreachOption) ([]*Breach, *http.Response, error) {
|
||||||
queryParams := b.setBreachOpts(options...)
|
qp := b.setBreachOpts(options...)
|
||||||
apiURL := fmt.Sprintf("%s/breaches", BaseURL)
|
au := fmt.Sprintf("%s/breaches", BaseURL)
|
||||||
|
|
||||||
hb, hr, err := b.hibp.HTTPResBody(http.MethodGet, apiURL, queryParams)
|
hb, hr, err := b.hibp.HTTPResBody(http.MethodGet, au, qp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var breachList []*Breach
|
|
||||||
if err := json.Unmarshal(hb, &breachList); err != nil {
|
|
||||||
return nil, hr, err
|
return nil, hr, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return breachList, hr, nil
|
var bl []*Breach
|
||||||
|
if err := json.Unmarshal(hb, &bl); err != nil {
|
||||||
|
return nil, hr, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bl, hr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BreachByName returns a single breached site based on its name
|
// BreachByName returns a single breached site based on its name
|
||||||
func (b *BreachAPI) BreachByName(n string, options ...BreachOption) (*Breach, *http.Response, error) {
|
func (b *BreachAPI) BreachByName(n string, options ...BreachOption) (*Breach, *http.Response, error) {
|
||||||
queryParams := b.setBreachOpts(options...)
|
qp := b.setBreachOpts(options...)
|
||||||
|
|
||||||
if n == "" {
|
if n == "" {
|
||||||
return nil, nil, fmt.Errorf("no breach name given")
|
return nil, nil, ErrNoName
|
||||||
}
|
}
|
||||||
|
|
||||||
apiURL := fmt.Sprintf("%s/breach/%s", BaseURL, n)
|
au := fmt.Sprintf("%s/breach/%s", BaseURL, n)
|
||||||
hb, hr, err := b.hibp.HTTPResBody(http.MethodGet, apiURL, queryParams)
|
hb, hr, err := b.hibp.HTTPResBody(http.MethodGet, au, qp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var breachDetails *Breach
|
|
||||||
if err := json.Unmarshal(hb, &breachDetails); err != nil {
|
|
||||||
return nil, hr, err
|
return nil, hr, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return breachDetails, hr, nil
|
var bd *Breach
|
||||||
|
if err := json.Unmarshal(hb, &bd); err != nil {
|
||||||
|
return nil, hr, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bd, hr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DataClasses are attribute of a record compromised in a breach. This method returns a list of strings
|
// 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
|
// with all registered data classes known to HIBP
|
||||||
func (b *BreachAPI) DataClasses() ([]string, *http.Response, error) {
|
func (b *BreachAPI) DataClasses() ([]string, *http.Response, error) {
|
||||||
apiURL := fmt.Sprintf("%s/dataclasses", BaseURL)
|
au := fmt.Sprintf("%s/dataclasses", BaseURL)
|
||||||
hb, hr, err := b.hibp.HTTPResBody(http.MethodGet, apiURL, nil)
|
hb, hr, err := b.hibp.HTTPResBody(http.MethodGet, au, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var dataClasses []string
|
|
||||||
if err := json.Unmarshal(hb, &dataClasses); err != nil {
|
|
||||||
return nil, hr, err
|
return nil, hr, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return dataClasses, hr, nil
|
var dc []string
|
||||||
|
if err := json.Unmarshal(hb, &dc); err != nil {
|
||||||
|
return nil, hr, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dc, hr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BreachedAccount returns a single breached site based on its name
|
// BreachedAccount returns a single breached site based on its name
|
||||||
func (b *BreachAPI) BreachedAccount(a string, options ...BreachOption) ([]*Breach, *http.Response, error) {
|
func (b *BreachAPI) BreachedAccount(a string, options ...BreachOption) ([]*Breach, *http.Response, error) {
|
||||||
queryParams := b.setBreachOpts(options...)
|
qp := b.setBreachOpts(options...)
|
||||||
|
|
||||||
if a == "" {
|
if a == "" {
|
||||||
return nil, nil, fmt.Errorf("no account id given")
|
return nil, nil, ErrNoAccountID
|
||||||
}
|
}
|
||||||
|
|
||||||
apiURL := fmt.Sprintf("%s/breachedaccount/%s", BaseURL, a)
|
au := fmt.Sprintf("%s/breachedaccount/%s", BaseURL, a)
|
||||||
hb, hr, err := b.hibp.HTTPResBody(http.MethodGet, apiURL, queryParams)
|
hb, hr, err := b.hibp.HTTPResBody(http.MethodGet, au, qp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var breachDetails []*Breach
|
|
||||||
if err := json.Unmarshal(hb, &breachDetails); err != nil {
|
|
||||||
return nil, hr, err
|
return nil, hr, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return breachDetails, hr, nil
|
var bd []*Breach
|
||||||
|
if err := json.Unmarshal(hb, &bd); err != nil {
|
||||||
|
return nil, hr, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bd, hr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithDomain sets the domain filter for the breaches API
|
// WithDomain sets the domain filter for the breaches API
|
||||||
|
@ -205,7 +205,7 @@ func (d *APIDate) UnmarshalJSON(s []byte) error {
|
||||||
|
|
||||||
pd, err := time.Parse("2006-01-02", ds)
|
pd, err := time.Parse("2006-01-02", ds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to convert API date string to time.Time type: %w", err)
|
return fmt.Errorf("convert API date string to time.Time type: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
*(*time.Time)(d) = pd
|
*(*time.Time)(d) = pd
|
||||||
|
@ -220,7 +220,7 @@ func (d *APIDate) Time() time.Time {
|
||||||
|
|
||||||
// setBreachOpts returns a map of default settings and overridden values from different BreachOption
|
// setBreachOpts returns a map of default settings and overridden values from different BreachOption
|
||||||
func (b *BreachAPI) setBreachOpts(options ...BreachOption) map[string]string {
|
func (b *BreachAPI) setBreachOpts(options ...BreachOption) map[string]string {
|
||||||
queryParams := map[string]string{
|
qp := map[string]string{
|
||||||
"truncateResponse": "true",
|
"truncateResponse": "true",
|
||||||
"includeUnverified": "true",
|
"includeUnverified": "true",
|
||||||
}
|
}
|
||||||
|
@ -233,16 +233,16 @@ func (b *BreachAPI) setBreachOpts(options ...BreachOption) map[string]string {
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.domain != "" {
|
if b.domain != "" {
|
||||||
queryParams["domain"] = b.domain
|
qp["domain"] = b.domain
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.disableTrunc {
|
if b.disableTrunc {
|
||||||
queryParams["truncateResponse"] = "false"
|
qp["truncateResponse"] = "false"
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.noUnverified {
|
if b.noUnverified {
|
||||||
queryParams["includeUnverified"] = "false"
|
qp["includeUnverified"] = "false"
|
||||||
}
|
}
|
||||||
|
|
||||||
return queryParams
|
return qp
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package hibp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -14,8 +15,8 @@ const (
|
||||||
invalidDateJSON = `{"date": "202299-10-01"}`
|
invalidDateJSON = `{"date": "202299-10-01"}`
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestBreaches tests the Breaches() method of the breaches API
|
// TestBreachAPI_Breaches tests the Breaches() method of the breaches API
|
||||||
func TestBreaches(t *testing.T) {
|
func TestBreachAPI_Breaches(t *testing.T) {
|
||||||
hc := New()
|
hc := New()
|
||||||
breachList, _, err := hc.BreachAPI.Breaches()
|
breachList, _, err := hc.BreachAPI.Breaches()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -26,20 +27,21 @@ func TestBreaches(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestBreachesWithNil tests the Breaches() method of the breaches API with a nil option
|
// TestBreachAPI_Breaches_WithNIL tests the Breaches() method of the breaches API with a nil option
|
||||||
func TestBreachesWithNil(t *testing.T) {
|
func TestBreachAPI_Breaches_WithNIL(t *testing.T) {
|
||||||
hc := New()
|
hc := New()
|
||||||
breachList, _, err := hc.BreachAPI.Breaches(nil)
|
breachList, _, err := hc.BreachAPI.Breaches(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if breachList != nil && len(breachList) <= 0 {
|
if breachList != nil && len(breachList) <= 0 {
|
||||||
t.Error("breaches list returned 0 results")
|
t.Error("breaches list returned 0 results")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestBreachesWithDomain tests the Breaches() method of the breaches API for a specific domain
|
// TestBreachAPI_Breaches_WithDomain tests the Breaches() method of the breaches API for a specific domain
|
||||||
func TestBreachesWithDomain(t *testing.T) {
|
func TestBreachAPI_Breaches_WithDomain(t *testing.T) {
|
||||||
testTable := []struct {
|
testTable := []struct {
|
||||||
testName string
|
testName string
|
||||||
domain string
|
domain string
|
||||||
|
@ -55,6 +57,7 @@ func TestBreachesWithDomain(t *testing.T) {
|
||||||
breachList, _, err := hc.BreachAPI.Breaches(WithDomain(tc.domain))
|
breachList, _, err := hc.BreachAPI.Breaches(WithDomain(tc.domain))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if breachList == nil && tc.isBreached {
|
if breachList == nil && tc.isBreached {
|
||||||
|
@ -75,8 +78,8 @@ func TestBreachesWithDomain(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestBreachesWithoutUnverified tests the Breaches() method of the breaches API with the unverified parameter
|
// TestBreachAPI_Breaches_WithoutUnverified tests the Breaches() method of the breaches API with the unverified parameter
|
||||||
func TestBreachesWithoutUnverified(t *testing.T) {
|
func TestBreachAPI_Breaches_WithoutUnverified(t *testing.T) {
|
||||||
testTable := []struct {
|
testTable := []struct {
|
||||||
testName string
|
testName string
|
||||||
domain string
|
domain string
|
||||||
|
@ -94,6 +97,7 @@ func TestBreachesWithoutUnverified(t *testing.T) {
|
||||||
breachList, _, err := hc.BreachAPI.Breaches(WithDomain(tc.domain), WithoutUnverified())
|
breachList, _, err := hc.BreachAPI.Breaches(WithDomain(tc.domain), WithoutUnverified())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if breachList == nil && tc.isVerified && tc.isBreached {
|
if breachList == nil && tc.isVerified && tc.isBreached {
|
||||||
|
@ -104,8 +108,8 @@ func TestBreachesWithoutUnverified(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestBreachByName tests the BreachByName() method of the breaches API for a specific domain
|
// TestBreachAPI_BreachByName tests the BreachByName() method of the breaches API for a specific domain
|
||||||
func TestBreachByName(t *testing.T) {
|
func TestBreachAPI_BreachByName(t *testing.T) {
|
||||||
testTable := []struct {
|
testTable := []struct {
|
||||||
testName string
|
testName string
|
||||||
breachName string
|
breachName string
|
||||||
|
@ -122,6 +126,7 @@ func TestBreachByName(t *testing.T) {
|
||||||
breachDetails, _, err := hc.BreachAPI.BreachByName(tc.breachName)
|
breachDetails, _, err := hc.BreachAPI.BreachByName(tc.breachName)
|
||||||
if err != nil && !tc.shouldFail {
|
if err != nil && !tc.shouldFail {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if breachDetails == nil && tc.isBreached {
|
if breachDetails == nil && tc.isBreached {
|
||||||
|
@ -136,20 +141,42 @@ func TestBreachByName(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestDataClasses tests the DataClasses() method of the breaches API
|
// TestBreachAPI_BreachByName_FailedHTTP tests the BreachByName() method with a failing HTTP request
|
||||||
func TestDataClasses(t *testing.T) {
|
func TestBreachAPI_BreachByName_FailedHTTP(t *testing.T) {
|
||||||
|
hc := New(WithRateLimitSleep())
|
||||||
|
_, res, err := hc.BreachAPI.BreachByName("fäiled_invalid")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("HTTP request was supposed to fail but didn't")
|
||||||
|
}
|
||||||
|
if res == nil {
|
||||||
|
t.Errorf("expected HTTP response but got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBreachAPI_BreachByName_Errors tests the errors for the BreachByName() method
|
||||||
|
func TestBreachAPI_BreachByName_Errors(t *testing.T) {
|
||||||
|
hc := New(WithRateLimitSleep())
|
||||||
|
_, _, err := hc.BreachAPI.BreachByName("")
|
||||||
|
if !errors.Is(err, ErrNoName) {
|
||||||
|
t.Errorf("expected to receive ErrNoName error but didn't")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBreachAPI_DataClasses tests the DataClasses() method of the breaches API
|
||||||
|
func TestBreachAPI_DataClasses(t *testing.T) {
|
||||||
hc := New()
|
hc := New()
|
||||||
classList, _, err := hc.BreachAPI.DataClasses()
|
classList, _, err := hc.BreachAPI.DataClasses()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if classList != nil && len(classList) <= 0 {
|
if classList != nil && len(classList) <= 0 {
|
||||||
t.Error("breaches list returned 0 results")
|
t.Error("breaches list returned 0 results")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestBreachedAccount tests the BreachedAccount() method of the breaches API
|
// TestBreachAPI_BreachedAccount tests the BreachedAccount() method of the breaches API
|
||||||
func TestBreachedAccount(t *testing.T) {
|
func TestBreachAPI_BreachedAccount(t *testing.T) {
|
||||||
testTable := []struct {
|
testTable := []struct {
|
||||||
testName string
|
testName string
|
||||||
accountName string
|
accountName string
|
||||||
|
@ -200,9 +227,35 @@ func TestBreachedAccount(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestBreachedAccountWithoutTruncate tests the BreachedAccount() method of the breaches API with the
|
// TestBreachAPI_BreachedAccount_FailedHTTP tests the BreachedAccount() method of the breaches API with a failing
|
||||||
|
// HTTP request
|
||||||
|
func TestBreachAPI_BreachedAccount_FailedHTTP(t *testing.T) {
|
||||||
|
apiKey := os.Getenv("HIBP_API_KEY")
|
||||||
|
if apiKey == "" {
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
hc := New(WithAPIKey(apiKey), WithRateLimitSleep())
|
||||||
|
_, res, err := hc.BreachAPI.BreachedAccount("bröken@invalid_domain.tld")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("HTTP request was supposed to fail, but didn't")
|
||||||
|
}
|
||||||
|
if res == nil {
|
||||||
|
t.Errorf("expected HTTP response but got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBreachAPI_BreachedAccount_Errors tests the errors for the BreachedAccount() method
|
||||||
|
func TestBreachAPI_BreachedAccount_Errors(t *testing.T) {
|
||||||
|
hc := New(WithRateLimitSleep())
|
||||||
|
_, _, err := hc.BreachAPI.BreachedAccount("")
|
||||||
|
if !errors.Is(err, ErrNoAccountID) {
|
||||||
|
t.Errorf("expected to receive ErrNoAccountID error but didn't")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBreachAPI_BreachedAccount_WithoutTruncate tests the BreachedAccount() method of the breaches API with the
|
||||||
// truncateResponse option set to false
|
// truncateResponse option set to false
|
||||||
func TestBreachedAccountWithoutTruncate(t *testing.T) {
|
func TestBreachAPI_BreachedAccount_WithoutTruncate(t *testing.T) {
|
||||||
testTable := []struct {
|
testTable := []struct {
|
||||||
testName string
|
testName string
|
||||||
accountName string
|
accountName string
|
||||||
|
@ -211,14 +264,17 @@ func TestBreachedAccountWithoutTruncate(t *testing.T) {
|
||||||
shouldFail bool
|
shouldFail bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"account-exists is breached once", "account-exists@hibp-integration-tests.com", "Adobe",
|
"account-exists is breached once", "account-exists@hibp-integration-tests.com",
|
||||||
"adobe.com", false,
|
"Adobe", "adobe.com", false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"multiple-breaches is breached multiple times", "multiple-breaches@hibp-integration-tests.com", "Adobe",
|
"multiple-breaches is breached multiple times", "multiple-breaches@hibp-integration-tests.com",
|
||||||
"adobe.com", false,
|
"Adobe", "adobe.com", false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"opt-out is not breached", "opt-out@hibp-integration-tests.com", "",
|
||||||
|
"", true,
|
||||||
},
|
},
|
||||||
{"opt-out is not breached", "opt-out@hibp-integration-tests.com", "", "", true},
|
|
||||||
{"empty string should fail", "", "", "", true},
|
{"empty string should fail", "", "", "", true},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
54
hibp.go
54
hibp.go
|
@ -4,6 +4,7 @@ package hibp
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
@ -13,15 +14,44 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Version represents the version of this package
|
// Version represents the version of this package
|
||||||
const Version = "1.0.2"
|
const Version = "1.0.5"
|
||||||
|
|
||||||
// BaseURL is the base URL for the majority of API calls
|
// BaseURL is the base URL for the majority of API endpoints
|
||||||
const BaseURL = "https://haveibeenpwned.com/api/v3"
|
const BaseURL = "https://haveibeenpwned.com/api/v3"
|
||||||
|
|
||||||
|
// PasswdBaseURL is the base URL for the pwned passwords API endpoints
|
||||||
|
const PasswdBaseURL = "https://api.pwnedpasswords.com"
|
||||||
|
|
||||||
// DefaultUserAgent defines the default UA string for the HTTP client
|
// 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
|
// Currently the URL in the UA string is comment out, as there is a bug in the HIBP API
|
||||||
// not allowing multiple slashes
|
// not allowing multiple slashes
|
||||||
const DefaultUserAgent = `go-hibp v` + Version + ` (https://github.com/wneessen/go-hibp)`
|
const DefaultUserAgent = `go-hibp/` + Version + ` (+https://github.com/wneessen/go-hibp)`
|
||||||
|
|
||||||
|
// DefaultTimeout is the default timeout value for the HTTP client
|
||||||
|
const DefaultTimeout = time.Second * 5
|
||||||
|
|
||||||
|
// List of common errors
|
||||||
|
var (
|
||||||
|
// ErrNoAccountID is returned if no account ID is given to the corresponding API method
|
||||||
|
ErrNoAccountID = errors.New("no account ID given")
|
||||||
|
|
||||||
|
// ErrNoName is returned if no name is given to the corresponding API method
|
||||||
|
ErrNoName = errors.New("no name given")
|
||||||
|
|
||||||
|
// ErrNonPositiveResponse should be returned if a HTTP request failed with a non HTTP-200 status
|
||||||
|
ErrNonPositiveResponse = errors.New("non HTTP-200 response for HTTP request")
|
||||||
|
|
||||||
|
// ErrPrefixLengthMismatch should be used if a given prefix does not match the
|
||||||
|
// expected length
|
||||||
|
ErrPrefixLengthMismatch = errors.New("password hash prefix must be 5 characters long")
|
||||||
|
|
||||||
|
// ErrSHA1LengthMismatch should be used if a given SHA1 checksum does not match the
|
||||||
|
// expected length
|
||||||
|
ErrSHA1LengthMismatch = errors.New("SHA1 hash size needs to be 160 bits")
|
||||||
|
|
||||||
|
// ErrSHA1Invalid should be used if a given string does not represent a valid SHA1 hash
|
||||||
|
ErrSHA1Invalid = errors.New("not a valid SHA1 hash")
|
||||||
|
)
|
||||||
|
|
||||||
// Client is the HIBP client object
|
// Client is the HIBP client object
|
||||||
type Client struct {
|
type Client struct {
|
||||||
|
@ -49,7 +79,7 @@ func New(options ...Option) Client {
|
||||||
c := Client{}
|
c := Client{}
|
||||||
|
|
||||||
// Set defaults
|
// Set defaults
|
||||||
c.to = time.Second * 5
|
c.to = DefaultTimeout
|
||||||
c.PwnedPassAPIOpts = &PwnedPasswordOptions{}
|
c.PwnedPassAPIOpts = &PwnedPasswordOptions{}
|
||||||
c.ua = DefaultUserAgent
|
c.ua = DefaultUserAgent
|
||||||
|
|
||||||
|
@ -183,7 +213,7 @@ func (c *Client) HTTPResBody(m string, p string, q map[string]string) ([]byte, *
|
||||||
}
|
}
|
||||||
|
|
||||||
if hr.StatusCode != 200 {
|
if hr.StatusCode != 200 {
|
||||||
return nil, hr, fmt.Errorf("API responded with non HTTP-200: %s - %s", hr.Status, hb)
|
return nil, hr, fmt.Errorf("HTTP %s: %w", hr.Status, ErrNonPositiveResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
return hb, hr, nil
|
return hb, hr, nil
|
||||||
|
@ -191,18 +221,18 @@ func (c *Client) HTTPResBody(m string, p string, q map[string]string) ([]byte, *
|
||||||
|
|
||||||
// httpClient returns a custom http client for the HIBP Client object
|
// httpClient returns a custom http client for the HIBP Client object
|
||||||
func httpClient(to time.Duration) *http.Client {
|
func httpClient(to time.Duration) *http.Client {
|
||||||
tlsConfig := &tls.Config{
|
tc := &tls.Config{
|
||||||
MaxVersion: tls.VersionTLS13,
|
MaxVersion: tls.VersionTLS13,
|
||||||
MinVersion: tls.VersionTLS12,
|
MinVersion: tls.VersionTLS12,
|
||||||
}
|
}
|
||||||
httpTransport := &http.Transport{TLSClientConfig: tlsConfig}
|
ht := &http.Transport{TLSClientConfig: tc}
|
||||||
httpClient := &http.Client{
|
hc := &http.Client{
|
||||||
Transport: httpTransport,
|
Transport: ht,
|
||||||
Timeout: 5 * time.Second,
|
Timeout: DefaultTimeout,
|
||||||
}
|
}
|
||||||
if to.Nanoseconds() > 0 {
|
if to.Nanoseconds() > 0 {
|
||||||
httpClient.Timeout = to
|
hc.Timeout = to
|
||||||
}
|
}
|
||||||
|
|
||||||
return httpClient
|
return hc
|
||||||
}
|
}
|
||||||
|
|
50
password.go
50
password.go
|
@ -3,22 +3,13 @@ package hibp
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// ErrPrefixLengthMismatch should be used if a given prefix does not match the
|
|
||||||
// expected length
|
|
||||||
ErrPrefixLengthMismatch = "password hash prefix must be 5 characters long"
|
|
||||||
|
|
||||||
// ErrSHA1LengthMismatch should be used if a given SHA1 checksum does not match the
|
|
||||||
// expected length
|
|
||||||
ErrSHA1LengthMismatch = "SHA1 hash size needs to be 160 bits"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PwnedPassAPI is a HIBP Pwned Passwords API client
|
// PwnedPassAPI is a HIBP Pwned Passwords API client
|
||||||
type PwnedPassAPI struct {
|
type PwnedPassAPI struct {
|
||||||
hibp *Client // References back to the parent HIBP client
|
hibp *Client // References back to the parent HIBP client
|
||||||
|
@ -46,7 +37,7 @@ func (p *PwnedPassAPI) CheckPassword(pw string) (*Match, *http.Response, error)
|
||||||
// CheckSHA1 checks the Pwned Passwords database against a given SHA1 checksum of a password string
|
// CheckSHA1 checks the Pwned Passwords database against a given SHA1 checksum of a password string
|
||||||
func (p *PwnedPassAPI) CheckSHA1(h string) (*Match, *http.Response, error) {
|
func (p *PwnedPassAPI) CheckSHA1(h string) (*Match, *http.Response, error) {
|
||||||
if len(h) != 40 {
|
if len(h) != 40 {
|
||||||
return nil, nil, fmt.Errorf(ErrSHA1LengthMismatch)
|
return nil, nil, ErrSHA1LengthMismatch
|
||||||
}
|
}
|
||||||
|
|
||||||
pwMatches, hr, err := p.ListHashesPrefix(h[:5])
|
pwMatches, hr, err := p.ListHashesPrefix(h[:5])
|
||||||
|
@ -79,7 +70,11 @@ func (p *PwnedPassAPI) ListHashesPassword(pw string) ([]Match, *http.Response, e
|
||||||
// contain junk data
|
// contain junk data
|
||||||
func (p *PwnedPassAPI) ListHashesSHA1(h string) ([]Match, *http.Response, error) {
|
func (p *PwnedPassAPI) ListHashesSHA1(h string) ([]Match, *http.Response, error) {
|
||||||
if len(h) != 40 {
|
if len(h) != 40 {
|
||||||
return nil, nil, fmt.Errorf(ErrSHA1LengthMismatch)
|
return nil, nil, ErrSHA1LengthMismatch
|
||||||
|
}
|
||||||
|
dst := make([]byte, hex.DecodedLen(len(h)))
|
||||||
|
if _, err := hex.Decode(dst, []byte(h)); err != nil {
|
||||||
|
return nil, nil, ErrSHA1Invalid
|
||||||
}
|
}
|
||||||
return p.ListHashesPrefix(h[:5])
|
return p.ListHashesPrefix(h[:5])
|
||||||
}
|
}
|
||||||
|
@ -91,28 +86,29 @@ func (p *PwnedPassAPI) ListHashesSHA1(h string) ([]Match, *http.Response, error)
|
||||||
// contain junk data
|
// contain junk data
|
||||||
func (p *PwnedPassAPI) ListHashesPrefix(pf string) ([]Match, *http.Response, error) {
|
func (p *PwnedPassAPI) ListHashesPrefix(pf string) ([]Match, *http.Response, error) {
|
||||||
if len(pf) != 5 {
|
if len(pf) != 5 {
|
||||||
return nil, nil, fmt.Errorf(ErrPrefixLengthMismatch)
|
return nil, nil, ErrPrefixLengthMismatch
|
||||||
}
|
}
|
||||||
hreq, err := p.hibp.HTTPReq(http.MethodGet, fmt.Sprintf("https://api.pwnedpasswords.com/range/%s", pf),
|
|
||||||
nil)
|
au := fmt.Sprintf("%s/range/%s", PasswdBaseURL, pf)
|
||||||
|
hreq, err := p.hibp.HTTPReq(http.MethodGet, au, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
hr, err := p.hibp.hc.Do(hreq)
|
hr, err := p.hibp.hc.Do(hreq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, hr, err
|
||||||
}
|
|
||||||
if hr.StatusCode != 200 {
|
|
||||||
return nil, hr, fmt.Errorf("API responded with non HTTP-200: %s", hr.Status)
|
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = hr.Body.Close()
|
_ = hr.Body.Close()
|
||||||
}()
|
}()
|
||||||
|
if hr.StatusCode != 200 {
|
||||||
|
return nil, hr, fmt.Errorf("HTTP %s: %w", hr.Status, ErrNonPositiveResponse)
|
||||||
|
}
|
||||||
|
|
||||||
var pwMatches []Match
|
var pm []Match
|
||||||
scanObj := bufio.NewScanner(hr.Body)
|
so := bufio.NewScanner(hr.Body)
|
||||||
for scanObj.Scan() {
|
for so.Scan() {
|
||||||
hp := strings.SplitN(scanObj.Text(), ":", 2)
|
hp := strings.SplitN(so.Text(), ":", 2)
|
||||||
fh := fmt.Sprintf("%s%s", strings.ToLower(pf), strings.ToLower(hp[0]))
|
fh := fmt.Sprintf("%s%s", strings.ToLower(pf), strings.ToLower(hp[0]))
|
||||||
hc, err := strconv.ParseInt(hp[1], 10, 64)
|
hc, err := strconv.ParseInt(hp[1], 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -121,15 +117,15 @@ func (p *PwnedPassAPI) ListHashesPrefix(pf string) ([]Match, *http.Response, err
|
||||||
if hc == 0 {
|
if hc == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
pwMatches = append(pwMatches, Match{
|
pm = append(pm, Match{
|
||||||
Hash: fh,
|
Hash: fh,
|
||||||
Count: hc,
|
Count: hc,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := scanObj.Err(); err != nil {
|
if err := so.Err(); err != nil {
|
||||||
return nil, nil, err
|
return nil, hr, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return pwMatches, hr, nil
|
return pm, hr, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package hibp
|
package hibp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
@ -119,6 +120,74 @@ func TestPwnedPassAPI_ListHashesPrefix(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestPwnedPassAPI_ListHashesPrefix_Errors tests the ListHashesPrefix method's errors
|
||||||
|
func TestPwnedPassAPI_ListHashesPrefix_Errors(t *testing.T) {
|
||||||
|
hc := New()
|
||||||
|
|
||||||
|
// Empty prefix
|
||||||
|
t.Run("empty prefix", func(t *testing.T) {
|
||||||
|
_, _, err := hc.PwnedPassAPI.ListHashesPrefix("")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("ListHashesPrefix with empty prefix should fail but didn't")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrPrefixLengthMismatch) {
|
||||||
|
t.Errorf("ListHashesPrefix with empty prefix should return ErrPrefixLengthMismatch error but didn't")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Too long prefix
|
||||||
|
t.Run("too long prefix", func(t *testing.T) {
|
||||||
|
_, _, err := hc.PwnedPassAPI.ListHashesPrefix("abcdefg12345")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("ListHashesPrefix with too long prefix should fail but didn't")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrPrefixLengthMismatch) {
|
||||||
|
t.Errorf("ListHashesPrefix with too long prefix should return ErrPrefixLengthMismatch error but didn't")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPwnedPassAPI_ListHashesSHA1_Errors tests the ListHashesSHA1 method's errors
|
||||||
|
func TestPwnedPassAPI_ListHashesSHA1_Errors(t *testing.T) {
|
||||||
|
hc := New()
|
||||||
|
|
||||||
|
// Empty hash
|
||||||
|
t.Run("empty hash", func(t *testing.T) {
|
||||||
|
_, _, err := hc.PwnedPassAPI.ListHashesSHA1("")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("ListHashesSHA1 with empty hash should fail but didn't")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrSHA1LengthMismatch) {
|
||||||
|
t.Errorf("ListHashesSHA1 with empty hash should return ErrSHA1LengthMismatch error but didn't")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Too long hash
|
||||||
|
t.Run("too long hash", func(t *testing.T) {
|
||||||
|
_, _, err := hc.PwnedPassAPI.ListHashesSHA1("FF36DC7D3284A39991ADA90CAF20D1E3C0DADEFAB")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("ListHashesSHA1 with too long hash should fail but didn't")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrSHA1LengthMismatch) {
|
||||||
|
t.Errorf("ListHashesSHA1 with too long hash should return ErrSHA1LengthMismatch error but didn't")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Invalid hash
|
||||||
|
t.Run("invalid hash", func(t *testing.T) {
|
||||||
|
_, _, err := hc.PwnedPassAPI.ListHashesSHA1("FF36DC7D3284A39991ADA90CAF20D1E3C0DADEFZ")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("ListHashesSHA1 with invalid hash should fail but didn't")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrSHA1Invalid) {
|
||||||
|
t.Errorf("ListHashesSHA1 with invalid hash should return ErrSHA1Invalid error but didn't")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// TestPwnedPassApi_ListHashesSHA1 tests the PwnedPassAPI.ListHashesSHA1 metethod
|
// TestPwnedPassApi_ListHashesSHA1 tests the PwnedPassAPI.ListHashesSHA1 metethod
|
||||||
func TestPwnedPassAPI_ListHashesSHA1(t *testing.T) {
|
func TestPwnedPassAPI_ListHashesSHA1(t *testing.T) {
|
||||||
hc := New()
|
hc := New()
|
||||||
|
|
4
paste.go
4
paste.go
|
@ -2,15 +2,11 @@ package hibp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrNoAccountID is returned if no account ID is given to the PastedAccount method
|
|
||||||
var ErrNoAccountID = errors.New("no account ID given")
|
|
||||||
|
|
||||||
// PasteAPI is a HIBP pastes API client
|
// PasteAPI is a HIBP pastes API client
|
||||||
type PasteAPI struct {
|
type PasteAPI struct {
|
||||||
hibp *Client // References back to the parent HIBP client
|
hibp *Client // References back to the parent HIBP client
|
||||||
|
|
Loading…
Reference in a new issue