diff --git a/Dockerfile b/Dockerfile index 6d79d33..2f90c56 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ FROM golang:1.15 WORKDIR /app -COPY go.* . +COPY go.* ./ RUN go mod download COPY . . CMD ["go", "run", "main.go"] # $ docker build -t passdb-server . -# $ docker run --env-file .env passdb-server +# $ docker run --env-file .env -p 3000:3000 passdb-server diff --git a/go.mod b/go.mod index 963966d..64bda6d 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.15 require ( cloud.google.com/go/bigquery v1.10.0 - github.com/audibleblink/haveibeenpwned v0.0.0-20200818214456-56e4be30fb8d github.com/go-chi/chi v4.1.2+incompatible golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc // indirect google.golang.org/api v0.29.0 diff --git a/go.sum b/go.sum index ef4e42e..1f6fe03 100644 --- a/go.sum +++ b/go.sum @@ -36,8 +36,6 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/audibleblink/haveibeenpwned v0.0.0-20200818214456-56e4be30fb8d h1:uyZwWOznDwSaK2dnhNcxp9Y71TNMxQ56u/lRCM1tGpo= -github.com/audibleblink/haveibeenpwned v0.0.0-20200818214456-56e4be30fb8d/go.mod h1:UnV/rC9/gIWgDPaLj/4qEOiVtwSS8xgofXGyvKcSuCg= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= diff --git a/hibp/hibp.go b/hibp/hibp.go new file mode 100644 index 0000000..2978561 --- /dev/null +++ b/hibp/hibp.go @@ -0,0 +1,102 @@ +package hibp + +import ( + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "net/url" + "os" +) + +//API URL of haveibeenpwned.com +const API = "https://haveibeenpwned.com/api/v3/" + +//BreachModel Each breach contains a number of attributes describing the incident. In the future, these attributes may expand without the API being versioned. +type BreachModel struct { + Name string `json:"Name,omitempty"` + Title string `json:"Title,omitempty"` + Domain string `json:"Domain,omitempty"` + BreachDate string `json:"BreachDate,omitempty"` + AddedDate string `json:"AddedDate,omitempty"` + ModifiedDate string `json:"ModifiedDate,omitempty"` + PwnCount int `json:"PwnCount,omitempty"` + Description string `json:"Description,omitempty"` + DataClasses []string `json:"DataClasses,omitempty"` + IsVerified bool `json:"IsVerified,omitempty"` + IsFabricated bool `json:"IsFabricated,omitempty"` + IsSensitive bool `json:"IsSensitive,omitempty"` + IsRetired bool `json:"IsRetired,omitempty"` + IsSpamList bool `json:"IsSpamList,omitempty"` + LogoPath string `json:"LogoPath,omitempty"` +} + +//BreachedAccount The most common use of the API is to return a list of all breaches a particular account has been involved in. The API takes a single parameter which is the account to be searched for. The account is not case sensitive and will be trimmed of leading or trailing white spaces. The account should always be URL encoded. +func BreachedAccount(account, domainFilter string, truncate, unverified bool) ([]BreachModel, error) { + + res, err := callService("breachedaccount", account, domainFilter, truncate, unverified) + if err != nil { + return nil, err + } + if res.StatusCode == http.StatusNotFound { + return nil, nil + } + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, err + } + defer res.Body.Close() + + breaches := make([]BreachModel, 0) + if err := json.Unmarshal(body, &breaches); err != nil { + return nil, err + } + + return breaches, nil +} + +func callService(service, account, domainFilter string, truncate, unverified bool) (*http.Response, error) { + client := &http.Client{} + + u, err := url.Parse(API) + if err != nil { + return nil, err + } + + u.Path += service + "/" + account + parameters := url.Values{} + if domainFilter != "" { + parameters.Add("domain", domainFilter) + } + if truncate == false { + parameters.Add("truncateResponse", "false") + } + if unverified { + parameters.Add("includeUnverified", "true") + } + u.RawQuery = parameters.Encode() + + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, err + } + + req.Header.Set("User-Agent", "Go/1.15") + req.Header.Set("hibp-api-key", os.Getenv("HIBP_API_KEY")) + res, err := client.Do(req) + + switch res.StatusCode { + case http.StatusBadRequest: + return nil, errors.New("the account does not comply with an acceptable format") + case http.StatusTooManyRequests: + return nil, errors.New("too many requests — the rate limit has been exceeded") + case http.StatusUnauthorized: + return nil, errors.New("valid header `hibp-api-key` required") + } + + if err != nil { + return nil, err + } + return res, nil +} diff --git a/main.go b/main.go index 502b4f5..9363ffb 100644 --- a/main.go +++ b/main.go @@ -9,12 +9,10 @@ import ( "os" "strings" - hibp "github.com/audibleblink/haveibeenpwned" - - "google.golang.org/api/iterator" - "cloud.google.com/go/bigquery" + "google.golang.org/api/iterator" + "github.com/audibleblink/passdb/hibp" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" ) @@ -25,6 +23,8 @@ var ( googleCred = os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") hibpKey = os.Getenv("HIBP_API_KEY") + port = "3000" + bq *bigquery.Client ) @@ -35,6 +35,10 @@ func init() { log.Fatal(err) } + if len(os.Args) > 1 { + port = os.Args[1] + } + ctx := context.Background() bq, err = bigquery.NewClient(ctx, projectID) if err != nil { @@ -55,19 +59,21 @@ func main() { r.Get("/emails/{email}", handleEmail) r.Get("/breaches/{email}", handleBreaches) - err := http.ListenAndServe(":3000", r) + listen := fmt.Sprintf("127.0.0.1:%s", port) + log.Printf("Starting server on %s\n", listen) + err := http.ListenAndServe(listen, r) if err != nil { log.Fatal(err) } } -type Record struct { +type record struct { Username string Domain string Password string } -type Breach struct { +type breach struct { Title string Domain string Date string @@ -124,9 +130,9 @@ func handleBreaches(w http.ResponseWriter, r *http.Request) { return } - var breaches []*Breach + var breaches []*breach for _, hibpBreach := range hibpBreaches { - breach := &Breach{ + breach := &breach{ Title: hibpBreach.Title, Domain: hibpBreach.Domain, Date: hibpBreach.BreachDate, @@ -145,19 +151,19 @@ func handleBreaches(w http.ResponseWriter, r *http.Request) { w.Write(data) } -func recordsByUsername(username string) (records []*Record, err error) { +func recordsByUsername(username string) (records []*record, err error) { return recordsBy("username", username) } -func recordsByPassword(password string) (records []*Record, err error) { +func recordsByPassword(password string) (records []*record, err error) { return recordsBy("password", password) } -func recordsByDomain(domain string) (records []*Record, err error) { +func recordsByDomain(domain string) (records []*record, err error) { return recordsBy("domain", domain) } -func recordsByEmail(email string) (records []*Record, err error) { +func recordsByEmail(email string) (records []*record, err error) { usernameAndDomain := strings.Split(email, "@") if len(usernameAndDomain) != 2 { err = fmt.Errorf("invalid email format") @@ -169,7 +175,12 @@ func recordsByEmail(email string) (records []*Record, err error) { return queryRecords(queryString) } -func queryRecords(queryString string) (records []*Record, err error) { +func recordsBy(column, value string) (records []*record, err error) { + queryString := fmt.Sprintf(`SELECT DISTINCT * FROM %s WHERE %s = "%s"`, bigQueryTable, column, value) + return queryRecords(queryString) +} + +func queryRecords(queryString string) (records []*record, err error) { query := bq.Query(queryString) ctx := context.Background() results, err := query.Read(ctx) @@ -178,7 +189,7 @@ func queryRecords(queryString string) (records []*Record, err error) { } for { - var r Record + var r record err = results.Next(&r) if err == iterator.Done { err = nil @@ -192,12 +203,7 @@ func queryRecords(queryString string) (records []*Record, err error) { return } -func recordsBy(column, value string) (records []*Record, err error) { - queryString := fmt.Sprintf(`SELECT DISTINCT * FROM %s WHERE %s = "%s"`, bigQueryTable, column, value) - return queryRecords(queryString) -} - -func resultWriter(w http.ResponseWriter, records *[]*Record) { +func resultWriter(w http.ResponseWriter, records *[]*record) { resultJson, err := json.Marshal(records) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError)