go(hibp): add AllBreachesForAccount + amend tests
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
* also automatically use hibp api key with direnv and in CI * check for rate-limit * don't interpret rate-limit in tests as a failure * report errors properly
This commit is contained in:
parent
ec7a8ca61a
commit
9fb9cc2735
@ -59,6 +59,9 @@ steps:
|
||||
volumes:
|
||||
- name: gopath
|
||||
path: /go
|
||||
environment:
|
||||
PCMT_HIBP_API_KEY:
|
||||
from_secret: hibp_api_key
|
||||
commands:
|
||||
- go test -cover ./...
|
||||
|
||||
@ -123,6 +126,9 @@ steps:
|
||||
volumes:
|
||||
- name: gopath
|
||||
path: /go
|
||||
environment:
|
||||
PCMT_HIBP_API_KEY:
|
||||
from_secret: hibp_api_key
|
||||
commands:
|
||||
- go test -cover ./...
|
||||
|
||||
|
1
.envrc
1
.envrc
@ -3,3 +3,4 @@ source_url "https://raw.githubusercontent.com/cachix/devenv/d1f7b48e35e6dee421cf
|
||||
use devenv
|
||||
|
||||
t=${XDG_RUNTIME_DIR}/secrets/pcmt_gitea_token; test -f ${t} && source_env ${t} && unset ${t}
|
||||
t=${XDG_RUNTIME_DIR}/secrets/hibp_api_key; test -f ${t} && source_env ${t} && unset ${t}
|
||||
|
@ -5,6 +5,7 @@ package hibp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
@ -29,6 +30,15 @@ type Subscription struct {
|
||||
DomainSearchMaxBreachedAccounts int
|
||||
}
|
||||
|
||||
// BreachName is used to represent a HIBP breach name object.
|
||||
type BreachName struct {
|
||||
// Name holds the actual breach name, which in HIBP is permanently unique.
|
||||
Name string `json:"Name" validate:"required,Name"`
|
||||
}
|
||||
|
||||
// BreachNames is a slice of BreachName objects.
|
||||
type BreachNames []BreachName
|
||||
|
||||
const (
|
||||
api = "https://haveibeenpwned.com/api/v3"
|
||||
appID = "pcmt (https://git.dotya.ml/mirre-mt/pcmt)"
|
||||
@ -37,11 +47,16 @@ const (
|
||||
|
||||
headerUA = "user-agent"
|
||||
headerHIBP = "hibp-api-key"
|
||||
|
||||
authKeyCheckValue = `Your request to the API couldn't be authorised. Check you have the right value in the "hibp-api-key" header, refer to the documentation for more: https://haveibeenpwned.com/API/v3#Authorisation`
|
||||
)
|
||||
|
||||
var (
|
||||
apiKey = os.Getenv("PCMT_HIBP_API_KEY")
|
||||
client = &http.Client{Timeout: reqTmOut}
|
||||
|
||||
ErrAuthKeyCheckValue = errors.New(authKeyCheckValue)
|
||||
ErrRateLimited = errors.New("We have been rate limited")
|
||||
)
|
||||
|
||||
// SubscriptionStatus models https://haveibeenpwned.com/API/v3#SubscriptionStatus.
|
||||
@ -78,7 +93,8 @@ func SubscriptionStatus() (*Subscription, error) {
|
||||
}
|
||||
|
||||
// GetAllBreaches retrieves all breaches available in HIBP, as per
|
||||
// https://haveibeenpwned.com/API/v3#AllBreaches.
|
||||
// https://haveibeenpwned.com/API/v3#AllBreaches. This should be run at
|
||||
// start-up to populate the cache.
|
||||
func GetAllBreaches() (*[]schema.HIBPSchema, error) {
|
||||
u := api + "/breaches"
|
||||
|
||||
@ -117,6 +133,57 @@ func GetAllBreaches() (*[]schema.HIBPSchema, error) {
|
||||
return &ab, nil
|
||||
}
|
||||
|
||||
// GetAllBreachesForAccount retrieves a list of breach names for a given
|
||||
// account.
|
||||
func GetAllBreachesForAccount(account string) ([]BreachName, error) {
|
||||
u := api + "/breachedaccount/" + account
|
||||
|
||||
req, err := http.NewRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
respCh, errCh := rChans()
|
||||
|
||||
setUA(req)
|
||||
setAuthHeader(req)
|
||||
scheduleReq(req, &respCh, &errCh)
|
||||
|
||||
resp := <-respCh
|
||||
err = <-errCh
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
if sc := resp.StatusCode; sc != 200 {
|
||||
// this is brittle...
|
||||
if string(body) == authKeyCheckValue {
|
||||
return nil, ErrAuthKeyCheckValue
|
||||
}
|
||||
|
||||
if sc == 429 {
|
||||
return nil, ErrRateLimited
|
||||
}
|
||||
}
|
||||
|
||||
bn := make([]BreachName, 0)
|
||||
|
||||
if len(body) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(body, &bn); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bn, nil
|
||||
}
|
||||
|
||||
func setUA(r *http.Request) {
|
||||
r.Header.Set(headerUA, appID)
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
package hibp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
@ -52,6 +53,7 @@ func TestMultipleBreaches(t *testing.T) {
|
||||
respCh, errCh := rChans()
|
||||
|
||||
setUA(req)
|
||||
setAuthHeader(req)
|
||||
scheduleReq(req, &respCh, &errCh)
|
||||
|
||||
resp := <-respCh
|
||||
@ -67,17 +69,63 @@ func TestMultipleBreaches(t *testing.T) {
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
b := string(body)
|
||||
|
||||
t.Log(b)
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
if apiKey == "" {
|
||||
if resp.StatusCode == 401 {
|
||||
t.Logf("apiKey is empty, expected a 401")
|
||||
if apiKey == "" {
|
||||
switch {
|
||||
case resp.StatusCode != 200:
|
||||
switch {
|
||||
case resp.StatusCode == 401:
|
||||
t.Logf("apiKey is empty, a 401 is expected")
|
||||
case resp.StatusCode == 429:
|
||||
t.Log("skipping due to rate-limit")
|
||||
t.SkipNow()
|
||||
default:
|
||||
t.Errorf("apiKey is empty, expected 401, got: %q, body: %q", resp.Status, b)
|
||||
}
|
||||
|
||||
default:
|
||||
t.Errorf("unexpected 200 without apiKey, got: %q, body: %q", resp.Status, b)
|
||||
}
|
||||
} else {
|
||||
if resp.StatusCode != 200 {
|
||||
if resp.StatusCode == 429 {
|
||||
t.Log("skipping due to rate-limit")
|
||||
t.SkipNow()
|
||||
} else {
|
||||
t.Errorf("apiKey is empty, expected 401, got: %q", resp.Status)
|
||||
t.Errorf("expected 200, got: %q body: %q", resp.Status, b)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("wanted 200, got: %q", resp.Status)
|
||||
want := `[{"Name":"Adobe"},{"Name":"Gawker"},{"Name":"Stratfor"}]`
|
||||
if want != b {
|
||||
t.Errorf("unexpected body, want: %q, got: %q", want, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
account := a + hibpTestDomain
|
||||
if b, err := GetAllBreachesForAccount(account); err != nil {
|
||||
if errors.Is(err, ErrRateLimited) {
|
||||
t.Log("skipping due to rate-limit")
|
||||
t.SkipNow()
|
||||
} else {
|
||||
t.Errorf("error getting all breaches for account: %q", err)
|
||||
}
|
||||
} else {
|
||||
if b != nil {
|
||||
t.Logf("breach names for account %q: %#v", account, b)
|
||||
|
||||
if len(b) != 1 {
|
||||
t.Errorf("expected 1 breach name for account %q, got: %d: %#v", account, len(b), b)
|
||||
} else {
|
||||
want := "Dubsmash"
|
||||
if b[0].Name != want {
|
||||
t.Errorf("expected breach name %q for account %q, got %q", want, account, b[0].Name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
t.Errorf("expected breach names for account %q, got nil", account)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -88,3 +136,31 @@ func TestGetAllBreaches(t *testing.T) {
|
||||
t.Errorf("error: %q", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAllBreachesForAccount(t *testing.T) {
|
||||
account := "dumb@test.com"
|
||||
|
||||
if b, err := GetAllBreachesForAccount(account); err != nil {
|
||||
if errors.Is(err, ErrRateLimited) {
|
||||
t.Log("skipping due to rate-limit")
|
||||
t.SkipNow()
|
||||
} else {
|
||||
t.Errorf("error getting all breaches for account: %q", err)
|
||||
}
|
||||
} else {
|
||||
if b != nil {
|
||||
t.Logf("breach names for account %q: %#v", account, b)
|
||||
|
||||
if len(b) != 1 {
|
||||
t.Errorf("expected 1 breach name for account %q, got: %d: %#v", account, len(b), b)
|
||||
} else {
|
||||
want := "Dubsmash"
|
||||
if b[0].Name != want {
|
||||
t.Errorf("expected breach name %q for account %q, got %q", want, account, b[0].Name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
t.Errorf("expected breach names for account %q, got nil", account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user