From c94201a4b2baa615ab586e317c33d51934128860 Mon Sep 17 00:00:00 2001 From: Amir Keibi Date: Fri, 11 Jan 2019 10:14:21 -0800 Subject: [PATCH 1/3] #22 added 2 optional parameters for username and password to be used whilst calling Traefik's /health endpoint. This is useful when Traefik's API endpoint (/health included) is protected with Basic Auth. --- .gitignore | 1 + health/health.go | 24 ++++++++++-- health/health_test.go | 88 +++++++++++++++++++++++++++++++++++++++---- types/config.go | 2 + 4 files changed, 104 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index d5cd777..26dd07d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ traefik-appinsights-watchdog debug bin **/debug.test +.vscode diff --git a/health/health.go b/health/health.go index ca63497..ccde97e 100644 --- a/health/health.go +++ b/health/health.go @@ -3,10 +3,12 @@ package health import ( "context" "crypto/tls" + "encoding/base64" "encoding/json" "fmt" "io/ioutil" "net/http" + "strings" "time" "github.com/lawrencegripper/traefik-appinsights-watchdog/types" @@ -24,14 +26,14 @@ func StartCheck(ctx context.Context, config types.Configuration, healthChannel c case <-ctx.Done(): return default: - ev := getStatsEvent(config.TraefikHealthEndpoint, tlsConfig) + ev := getStatsEvent(config.TraefikHealthEndpoint, config.APIEndpointUsername, config.APIEndpointPassword, tlsConfig) healthChannel <- ev time.Sleep(intervalDuration) } } } -func getStatsEvent(endpoint string, tlsConfig *tls.Config) types.StatsEvent { +func getStatsEvent(endpoint string, username string, password string, tlsConfig *tls.Config) types.StatsEvent { event := types.StatsEvent{ Source: "HealthCheck", SourceTime: time.Now(), @@ -46,7 +48,18 @@ func getStatsEvent(endpoint string, tlsConfig *tls.Config) types.StatsEvent { }, } - resp, err := client.Get(endpoint) + req, err := http.NewRequest("GET", endpoint, nil) + if err != nil { + event.IsSuccess = false + event.ErrorDetails = err.Error() + return event + } + + // credentials are optional, but if provided, only username is mandatory + if len(strings.TrimSpace(username)) != 0 { + req.Header.Add("Authorization", "Basic "+basicAuth(username, password)) + } + resp, err := client.Do(req) elapsed := time.Since(start) event.RequestDuration = elapsed if err != nil { @@ -76,3 +89,8 @@ func getStatsEvent(endpoint string, tlsConfig *tls.Config) types.StatsEvent { event.Data = data return event } + +func basicAuth(username, password string) string { + auth := username + ":" + password + return base64.StdEncoding.EncodeToString([]byte(auth)) +} diff --git a/health/health_test.go b/health/health_test.go index dce47d6..fedb630 100644 --- a/health/health_test.go +++ b/health/health_test.go @@ -2,6 +2,8 @@ package health import ( "context" + "encoding/base64" + "errors" "io/ioutil" "log" "net/http" @@ -94,13 +96,48 @@ func TestHealthRetreiveMetrics_Timeout(t *testing.T) { } } +func TestHealthRetreiveMetrics_Authorized(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(handleHealthWithCredentials)) + defer server.Close() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + config := types.Configuration{TraefikHealthEndpoint: server.URL + "/health", APIEndpointUsername: "User", APIEndpointPassword: "Sup3rSecr3t"} + channel := make(chan types.StatsEvent) + + go StartCheck(ctx, config, channel) + + timeout := time.After(time.Second * 3) + + select { + case statEvent := <-channel: + if !statEvent.IsSuccess { + t.Error("Stats event was a failure") + } + t.Log(statEvent) + return + case <-timeout: + t.Error("Timeout occurred") + return + } +} + func handleHealthSuceed(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/health" { http.NotFound(w, r) return } - body, err := ioutil.ReadFile("testdata/healthresponse_normal.json") + writeOkResponse(w) +} + +func handleHealthInvalid(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/health" { + http.NotFound(w, r) + return + } + + body, err := ioutil.ReadFile("testdata/healthresponse_invalid.json") if err != nil { w.WriteHeader(http.StatusInternalServerError) _, err = w.Write([]byte(err.Error())) @@ -117,13 +154,33 @@ func handleHealthSuceed(w http.ResponseWriter, r *http.Request) { } } -func handleHealthInvalid(w http.ResponseWriter, r *http.Request) { +func handleHealthTimeout(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/health" { http.NotFound(w, r) return } - body, err := ioutil.ReadFile("testdata/healthresponse_invalid.json") + time.Sleep(time.Second * 5) +} + +func handleHealthWithCredentials(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/health" { + http.NotFound(w, r) + return + } + + err := authenticate(r) + + if err != nil { + log.Fatal(err) + w.WriteHeader(http.StatusUnauthorized) + } else { + writeOkResponse(w) + } +} + +func writeOkResponse(w http.ResponseWriter) { + body, err := ioutil.ReadFile("testdata/healthresponse_normal.json") if err != nil { w.WriteHeader(http.StatusInternalServerError) _, err = w.Write([]byte(err.Error())) @@ -140,11 +197,26 @@ func handleHealthInvalid(w http.ResponseWriter, r *http.Request) { } } -func handleHealthTimeout(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/health" { - http.NotFound(w, r) - return +func authenticate(r *http.Request) error { + auth := strings.SplitN(r.Header.Get("Authorization"), " ", 2) + + if len(auth) != 2 || auth[0] != "Basic" { + return errors.New("Invalid or missing Authorization header") } - time.Sleep(time.Second * 5) + payload, _ := base64.StdEncoding.DecodeString(auth[1]) + pair := strings.SplitN(string(payload), ":", 2) + + if len(pair) != 2 || !validate(pair[0], pair[1]) { + return errors.New("Invalid credentials") + } + + return nil +} + +func validate(username, password string) bool { + if username == "User" && password == "Sup3rSecr3t" { + return true + } + return false } diff --git a/types/config.go b/types/config.go index b84ff49..70d041d 100644 --- a/types/config.go +++ b/types/config.go @@ -11,4 +11,6 @@ type Configuration struct { TraefikHealthEndpoint string `description:"The traeifk health endpoint http://localhost:port/health"` PollIntervalSec int `description:"The time waited between requests to the health endpoint"` AllowInvalidCert bool `description:"Allow invalid certificates when performing routing checks on localhost"` + APIEndpointUsername string `description:"Stores username required to call APIs including healthcheck"` + APIEndpointPassword string `description:"Stores password required to call APIs including healthcheck"` } From 5b069f777bebdc15129586d72d2b6d6f960b5ac5 Mon Sep 17 00:00:00 2001 From: Amir Keibi Date: Fri, 11 Jan 2019 14:51:50 -0800 Subject: [PATCH 2/3] #22 extracting the common string literals into untyped string constants --- health/health_test.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/health/health_test.go b/health/health_test.go index fedb630..372460d 100644 --- a/health/health_test.go +++ b/health/health_test.go @@ -15,6 +15,11 @@ import ( "github.com/lawrencegripper/traefik-appinsights-watchdog/types" ) +const ( + testUserName = "sp3cialUs3r" + testPassword = "Sup3rSecr3t" +) + func TestHealthRetreiveMetrics(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(handleHealthSuceed)) defer server.Close() @@ -102,7 +107,7 @@ func TestHealthRetreiveMetrics_Authorized(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - config := types.Configuration{TraefikHealthEndpoint: server.URL + "/health", APIEndpointUsername: "User", APIEndpointPassword: "Sup3rSecr3t"} + config := types.Configuration{TraefikHealthEndpoint: server.URL + "/health", APIEndpointUsername: testUserName, APIEndpointPassword: testPassword} channel := make(chan types.StatsEvent) go StartCheck(ctx, config, channel) @@ -215,7 +220,7 @@ func authenticate(r *http.Request) error { } func validate(username, password string) bool { - if username == "User" && password == "Sup3rSecr3t" { + if username == testUserName && password == testPassword { return true } return false From fb15857f8f89aea100fb71712a73fbb6bc9d177c Mon Sep 17 00:00:00 2001 From: Amir Keibi Date: Fri, 11 Jan 2019 16:24:57 -0800 Subject: [PATCH 3/3] #22 upgrade the base docker image --- .dockerignore | 3 ++- Dockerfile | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.dockerignore b/.dockerignore index cc74855..6eec620 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,3 @@ vendor -bin \ No newline at end of file +bin +.vscode diff --git a/Dockerfile b/Dockerfile index a3d77f2..64cfb18 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # build stage -FROM golang:1.9.2-alpine +FROM golang:1.11.4-alpine ENV GOBIN /go/bin RUN apk add --update --no-progress openssl git wget bash gcc musl-dev && \ rm -rf /var/cache/apk/* && \