From 77a89461158c0850b3e56b2b09a10cfbe399e07b Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Mon, 3 Sep 2018 20:27:02 +0800 Subject: [PATCH] feat(security): add http basic auth --- .env | 8 +++++ config.go | 53 ++++++++++++++++++++++++++------ main.go | 19 +++++++++++- pkg/auth/authmethod.go | 19 ++++++++++++ pkg/auth/basic.go | 69 ++++++++++++++++++++++++++++++++++++++++++ pkg/auth/none.go | 25 +++++++++++++++ 6 files changed, 182 insertions(+), 11 deletions(-) create mode 100644 pkg/auth/authmethod.go create mode 100644 pkg/auth/basic.go create mode 100644 pkg/auth/none.go diff --git a/.env b/.env index dd7ba71..ac4c6f2 100644 --- a/.env +++ b/.env @@ -37,4 +37,12 @@ APP_HTTP_NOTIFIER_USER=api:key-xxxxxxxxxxxxxxxxxxxxxxxxxx # Defaults: localhost:25 APP_SMTP_NOTIFIER_HOST=localhost:25 +# Authentication Method +# Defaults: none +# Values: +# - "none" : No authentication (defaults). +# - "basic": HTTP Basic authentication. +AUTH=none +# Authentication Parameter +AUTH_PARAM=username:password \ No newline at end of file diff --git a/config.go b/config.go index c101f24..da7b1cd 100644 --- a/config.go +++ b/config.go @@ -1,31 +1,64 @@ package main import ( + "bytes" + "errors" "flag" + "fmt" "os" "strconv" + + "github.com/ncarlier/webhookd/pkg/auth" ) // Config contain global configuration type Config struct { - ListenAddr *string - NbWorkers *int - Debug *bool - Timeout *int - ScriptDir *string + ListenAddr *string + NbWorkers *int + Debug *bool + Timeout *int + ScriptDir *string + Authentication *string + AuthenticationParam *string +} + +type authFlag struct { + selectedMethod auth.Method +} + +func (c authFlag) Set(arg string) error { + fmt.Println(arg) + return errors.New("fall") +} + +func (c authFlag) String() string { + return "test" } var config = &Config{ - ListenAddr: flag.String("listen", getEnv("LISTEN_ADDR", ":8080"), "HTTP service address (e.g.address, ':8080')"), - NbWorkers: flag.Int("nb-workers", getIntEnv("NB_WORKERS", 2), "The number of workers to start"), - Debug: flag.Bool("debug", getBoolEnv("DEBUG", false), "Output debug logs"), - Timeout: flag.Int("timeout", getIntEnv("HOOK_TIMEOUT", 10), "Hook maximum delay before timeout (in second)"), - ScriptDir: flag.String("scripts", getEnv("SCRIPTS_DIR", "scripts"), "Scripts directory"), + ListenAddr: flag.String("listen", getEnv("LISTEN_ADDR", ":8080"), "HTTP service address (e.g.address, ':8080')"), + NbWorkers: flag.Int("nb-workers", getIntEnv("NB_WORKERS", 2), "The number of workers to start"), + Debug: flag.Bool("debug", getBoolEnv("DEBUG", false), "Output debug logs"), + Timeout: flag.Int("timeout", getIntEnv("HOOK_TIMEOUT", 10), "Hook maximum delay before timeout (in second)"), + ScriptDir: flag.String("scripts", getEnv("SCRIPTS_DIR", "scripts"), "Scripts directory"), + Authentication: flag.String("auth", getEnv("AUTH", "none"), ""), + AuthenticationParam: flag.String("authparam", getEnv("AUTH_PARAM", ""), func() string { + authdocwriter := bytes.NewBufferString("Authentication method. Available methods: ") + + for key, method := range auth.AvailableMethods { + authdocwriter.WriteRune('\n') + authdocwriter.WriteString(key) + authdocwriter.WriteRune(':') + authdocwriter.WriteString(method.Usage()) + } + return authdocwriter.String() + }()), } func init() { flag.StringVar(config.ListenAddr, "l", *config.ListenAddr, "HTTP service (e.g address: ':8080')") flag.BoolVar(config.Debug, "d", *config.Debug, "Output debug logs") + } func getEnv(key, fallback string) string { diff --git a/main.go b/main.go index d03343c..1c7d3ca 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( "time" "github.com/ncarlier/webhookd/pkg/api" + "github.com/ncarlier/webhookd/pkg/auth" "github.com/ncarlier/webhookd/pkg/logger" "github.com/ncarlier/webhookd/pkg/worker" ) @@ -34,6 +35,20 @@ func main() { return } + var authmethod auth.Method + name := *config.Authentication + if _, ok := auth.AvailableMethods[name]; ok { + authmethod = auth.AvailableMethods[name] + if err := authmethod.ParseParam(*config.AuthenticationParam); err != nil { + fmt.Println("Authentication parameter is not valid:", err.Error()) + fmt.Println(authmethod.Usage()) + os.Exit(2) + } + } else { + fmt.Println("Authentication name is not valid:", name) + os.Exit(2) + } + level := "info" if *config.Debug { level = "debug" @@ -41,6 +56,8 @@ func main() { logger.Init(level) logger.Debug.Println("Starting webhookd server...") + logger.Info.Println("Using Authentication:", name) + authmethod.Init(*config.Debug) router := http.NewServeMux() router.Handle("/", api.Index(*config.Timeout, *config.ScriptDir)) @@ -52,7 +69,7 @@ func main() { server := &http.Server{ Addr: *config.ListenAddr, - Handler: tracing(nextRequestID)(logging(logger.Debug)(router)), + Handler: authmethod.Middleware()(tracing(nextRequestID)(logging(logger.Debug)(router))), ErrorLog: logger.Error, } diff --git a/pkg/auth/authmethod.go b/pkg/auth/authmethod.go new file mode 100644 index 0000000..50d7dd1 --- /dev/null +++ b/pkg/auth/authmethod.go @@ -0,0 +1,19 @@ +package auth + +import "net/http" + +// Method an interface describing an authentication method +type Method interface { + Init(debug bool) + Usage() string + ParseParam(string) error + Middleware() func(http.Handler) http.Handler +} + +var ( + // AvailableMethods Returns a map of available auth methods + AvailableMethods = map[string]Method{ + "none": new(noAuth), + "basic": new(basicAuth), + } +) diff --git a/pkg/auth/basic.go b/pkg/auth/basic.go new file mode 100644 index 0000000..9adc2ba --- /dev/null +++ b/pkg/auth/basic.go @@ -0,0 +1,69 @@ +package auth + +import ( + "errors" + "fmt" + "net/http" + "strings" + + "github.com/ncarlier/webhookd/pkg/logger" +) + +type basicAuth struct { + username string + password string + debug bool + authheader string +} + +func (c *basicAuth) Init(debug bool) { + c.debug = debug + logger.Warning.Println("\u001B[33mBasic Auth: Debug mode enabled. Might Leak sentitive information in log output.\u001B[0m") +} + +func (c *basicAuth) Usage() string { + return "HTTP Basic Auth. Usage: -auth basic -authparam :[:] (example: -auth basic -authparam foo:bar)" +} + +func (c *basicAuth) ParseParam(param string) error { + res := strings.Split(param, ":") + realm := "Authentication required." + switch len(res) { + case 3: + realm = res[2] + fallthrough + case 2: + c.username, c.password = res[0], res[1] + c.authheader = fmt.Sprintf("Basic realm=\"%s\"", realm) + return nil + } + return errors.New("Invalid Auth param") + +} + +// BasicAuth HTTP Basic Auth implementation +func (c *basicAuth) Middleware() func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if username, password, ok := r.BasicAuth(); ok && username == c.username && password == c.password { + if c.debug { + logger.Debug.Printf("HTTP Basic Auth: %s:%s PASSED\n", username, password) + } + next.ServeHTTP(w, r) + } else if !ok { + if c.debug { + logger.Debug.Println("HTTP Basic Auth: Auth header not present.") + } + w.Header().Add("WWW-Authenticate", c.authheader) + w.WriteHeader(401) + w.Write([]byte("Authentication required.")) + } else { + if c.debug { + logger.Debug.Printf("HTTP Basic Auth: Invalid credentials: %s:%s \n", username, password) + } + w.WriteHeader(403) + w.Write([]byte("Forbidden.")) + } + }) + } +} diff --git a/pkg/auth/none.go b/pkg/auth/none.go new file mode 100644 index 0000000..257af25 --- /dev/null +++ b/pkg/auth/none.go @@ -0,0 +1,25 @@ +package auth + +import ( + "net/http" +) + +type noAuth struct { +} + +func (c *noAuth) Usage() string { + return "No Auth. Usage: --auth none" +} + +func (c *noAuth) Init(_ bool) {} + +func (c *noAuth) ParseParam(_ string) error { + return nil +} + +// NoAuth A Nop Auth middleware +func (c *noAuth) Middleware() func(http.Handler) http.Handler { + return func(h http.Handler) http.Handler { + return h + } +}