mirror of
https://github.com/wneessen/go-hibp.git
synced 2024-11-22 21:00:51 +01:00
Overhauling error handling of the different APIs as part of #24
- More error generalization - Fixed PwnedPasswords API errors - Added SHA1 hash validation with corresponding error - More tests for error handling
This commit is contained in:
parent
c5ea330401
commit
f143794341
3 changed files with 107 additions and 28 deletions
16
hibp.go
16
hibp.go
|
@ -16,9 +16,12 @@ import (
|
||||||
// Version represents the version of this package
|
// Version represents the version of this package
|
||||||
const Version = "1.0.5"
|
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
|
||||||
|
@ -37,6 +40,17 @@ var (
|
||||||
|
|
||||||
// ErrNonPositiveResponse should be returned if a HTTP request failed with a non HTTP-200 status
|
// 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")
|
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
|
||||||
|
|
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()
|
||||||
|
|
Loading…
Reference in a new issue