mirror of
https://github.com/ncarlier/webhookd.git
synced 2025-04-08 22:10:55 +00:00
feat(): use htpasswd to manage basic auth
This commit is contained in:
parent
0a096c2763
commit
aab844cee7
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
release/
|
release/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.htpasswd
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -1,5 +1,7 @@
|
||||||
.SILENT :
|
.SILENT :
|
||||||
|
|
||||||
|
export GO111MODULE=on
|
||||||
|
|
||||||
# Author
|
# Author
|
||||||
AUTHOR=github.com/ncarlier
|
AUTHOR=github.com/ncarlier
|
||||||
|
|
||||||
|
|
30
README.md
30
README.md
|
@ -46,6 +46,7 @@ You can configure the daemon by:
|
||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `APP_LISTEN_ADDR` | `:8080` | HTTP service address |
|
| `APP_LISTEN_ADDR` | `:8080` | HTTP service address |
|
||||||
|
| `APP_PASSWD_FILE` | `.htpasswd` | Password file for HTTP basic authentication |
|
||||||
| `APP_NB_WORKERS` | `2` | The number of workers to start |
|
| `APP_NB_WORKERS` | `2` | The number of workers to start |
|
||||||
| `APP_HOOK_TIMEOUT` | `10` | Hook maximum delay before timeout (in second) |
|
| `APP_HOOK_TIMEOUT` | `10` | Hook maximum delay before timeout (in second) |
|
||||||
| `APP_SCRIPTS_DIR` | `./scripts` | Scripts directory |
|
| `APP_SCRIPTS_DIR` | `./scripts` | Scripts directory |
|
||||||
|
@ -64,6 +65,7 @@ You can configure the daemon by:
|
||||||
| Parameter | Default | Description |
|
| Parameter | Default | Description |
|
||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `-l <address> or --listen <address>` | `:8080` | HTTP service address |
|
| `-l <address> or --listen <address>` | `:8080` | HTTP service address |
|
||||||
|
| `-p or --passwd <htpasswd file>` | `.htpasswd` | Password file for HTTP basic authentication
|
||||||
| `-d or --debug` | false | Output debug logs |
|
| `-d or --debug` | false | Output debug logs |
|
||||||
| `--nb-workers <workers>` | `2` | The number of workers to start |
|
| `--nb-workers <workers>` | `2` | The number of workers to start |
|
||||||
| `--scripts <dir>` | `./scripts` | Scripts directory |
|
| `--scripts <dir>` | `./scripts` | Scripts directory |
|
||||||
|
@ -199,6 +201,34 @@ SMTP notification configuration:
|
||||||
|
|
||||||
The log file will be sent as an GZIP attachment.
|
The log file will be sent as an GZIP attachment.
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
You can restrict access to webhooks using HTTP basic authentication.
|
||||||
|
|
||||||
|
To activate basic authentication, you have to create a `htpasswd` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ # create passwd file the user 'api'
|
||||||
|
$ htpasswd -B -c .htpasswd api
|
||||||
|
```
|
||||||
|
This command will ask for a password and store it in the htpawsswd file.
|
||||||
|
|
||||||
|
Please note that by default, the daemon will try to load the `.htpasswd` file.
|
||||||
|
|
||||||
|
But you can override this behavior by specifying the location of the file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ APP_PASSWD_FILE=/etc/webhookd/users.htpasswd
|
||||||
|
$ # or
|
||||||
|
$ webhookd -p /etc/webhookd/users.htpasswd
|
||||||
|
```
|
||||||
|
|
||||||
|
Once configured, you must call webhooks using basic authentication:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ curl -u api:test -XPOST "http://localhost:8080/echo?msg=hello"
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
70
config.go
70
config.go
|
@ -1,70 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"flag"
|
|
||||||
"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
|
|
||||||
Authentication *string
|
|
||||||
AuthenticationParam *string
|
|
||||||
}
|
|
||||||
|
|
||||||
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"),
|
|
||||||
Authentication: flag.String("auth", getEnv("AUTH", "none"), ""),
|
|
||||||
AuthenticationParam: flag.String("auth-param", 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 {
|
|
||||||
if value, ok := os.LookupEnv("APP_" + key); ok {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
func getIntEnv(key string, fallback int) int {
|
|
||||||
strValue := getEnv(key, strconv.Itoa(fallback))
|
|
||||||
if value, err := strconv.Atoi(strValue); err == nil {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
func getBoolEnv(key string, fallback bool) bool {
|
|
||||||
strValue := getEnv(key, strconv.FormatBool(fallback))
|
|
||||||
if value, err := strconv.ParseBool(strValue); err == nil {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return fallback
|
|
||||||
}
|
|
3
go.mod
Normal file
3
go.mod
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module github.com/ncarlier/webhookd
|
||||||
|
|
||||||
|
require golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9
|
2
go.sum
Normal file
2
go.sum
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
|
||||||
|
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
96
main.go
96
main.go
|
@ -3,30 +3,17 @@ package main
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ncarlier/webhookd/pkg/api"
|
"github.com/ncarlier/webhookd/pkg/api"
|
||||||
"github.com/ncarlier/webhookd/pkg/auth"
|
"github.com/ncarlier/webhookd/pkg/config"
|
||||||
"github.com/ncarlier/webhookd/pkg/logger"
|
"github.com/ncarlier/webhookd/pkg/logger"
|
||||||
"github.com/ncarlier/webhookd/pkg/worker"
|
"github.com/ncarlier/webhookd/pkg/worker"
|
||||||
)
|
)
|
||||||
|
|
||||||
type key int
|
|
||||||
|
|
||||||
const (
|
|
||||||
requestIDKey key = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
healthy int32
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
@ -35,47 +22,25 @@ func main() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var authmethod auth.Method
|
conf := config.Get()
|
||||||
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"
|
level := "info"
|
||||||
if *config.Debug {
|
if *conf.Debug {
|
||||||
level = "debug"
|
level = "debug"
|
||||||
}
|
}
|
||||||
logger.Init(level)
|
logger.Init(level)
|
||||||
|
|
||||||
logger.Debug.Println("Starting webhookd server...")
|
logger.Debug.Println("Starting webhookd server...")
|
||||||
logger.Debug.Println("Using Authentication:", name)
|
|
||||||
authmethod.Init(*config.Debug)
|
|
||||||
|
|
||||||
router := http.NewServeMux()
|
|
||||||
router.Handle("/", api.Index(*config.Timeout, *config.ScriptDir))
|
|
||||||
router.Handle("/healthz", healthz())
|
|
||||||
|
|
||||||
nextRequestID := func() string {
|
|
||||||
return fmt.Sprintf("%d", time.Now().UnixNano())
|
|
||||||
}
|
|
||||||
|
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: *config.ListenAddr,
|
Addr: *conf.ListenAddr,
|
||||||
Handler: authmethod.Middleware()(tracing(nextRequestID)(logging(logger.Debug)(router))),
|
Handler: api.NewRouter(config.Get()),
|
||||||
ErrorLog: logger.Error,
|
ErrorLog: logger.Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the dispatcher.
|
// Start the dispatcher.
|
||||||
logger.Debug.Printf("Starting the dispatcher (%d workers)...\n", *config.NbWorkers)
|
logger.Debug.Printf("Starting the dispatcher (%d workers)...\n", *conf.NbWorkers)
|
||||||
worker.StartDispatcher(*config.NbWorkers)
|
worker.StartDispatcher(*conf.NbWorkers)
|
||||||
|
|
||||||
done := make(chan bool)
|
done := make(chan bool)
|
||||||
quit := make(chan os.Signal, 1)
|
quit := make(chan os.Signal, 1)
|
||||||
|
@ -84,7 +49,7 @@ func main() {
|
||||||
go func() {
|
go func() {
|
||||||
<-quit
|
<-quit
|
||||||
logger.Debug.Println("Server is shutting down...")
|
logger.Debug.Println("Server is shutting down...")
|
||||||
atomic.StoreInt32(&healthy, 0)
|
api.Shutdown()
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
@ -96,51 +61,12 @@ func main() {
|
||||||
close(done)
|
close(done)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
logger.Info.Println("Server is ready to handle requests at", *config.ListenAddr)
|
logger.Info.Println("Server is ready to handle requests at", *conf.ListenAddr)
|
||||||
atomic.StoreInt32(&healthy, 1)
|
api.Start()
|
||||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
logger.Error.Fatalf("Could not listen on %s: %v\n", *config.ListenAddr, err)
|
logger.Error.Fatalf("Could not listen on %s: %v\n", *conf.ListenAddr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
<-done
|
<-done
|
||||||
logger.Debug.Println("Server stopped")
|
logger.Debug.Println("Server stopped")
|
||||||
}
|
}
|
||||||
|
|
||||||
func healthz() http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if atomic.LoadInt32(&healthy) == 1 {
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusServiceUnavailable)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func logging(logger *log.Logger) func(http.Handler) http.Handler {
|
|
||||||
return func(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
defer func() {
|
|
||||||
requestID, ok := r.Context().Value(requestIDKey).(string)
|
|
||||||
if !ok {
|
|
||||||
requestID = "unknown"
|
|
||||||
}
|
|
||||||
logger.Println(requestID, r.Method, r.URL.Path, r.RemoteAddr, r.UserAgent())
|
|
||||||
}()
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func tracing(nextRequestID func() string) func(http.Handler) http.Handler {
|
|
||||||
return func(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
requestID := r.Header.Get("X-Request-Id")
|
|
||||||
if requestID == "" {
|
|
||||||
requestID = nextRequestID()
|
|
||||||
}
|
|
||||||
ctx := context.WithValue(r.Context(), requestIDKey, requestID)
|
|
||||||
w.Header().Set("X-Request-Id", requestID)
|
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
32
pkg/api/healthz.go
Normal file
32
pkg/api/healthz.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/ncarlier/webhookd/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
healthy int32
|
||||||
|
)
|
||||||
|
|
||||||
|
// Shutdown set API as stopped
|
||||||
|
func Shutdown() {
|
||||||
|
atomic.StoreInt32(&healthy, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start set API as started
|
||||||
|
func Start() {
|
||||||
|
atomic.StoreInt32(&healthy, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func healthz(conf *config.Config) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if atomic.LoadInt32(&healthy) == 1 {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
})
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ncarlier/webhookd/pkg/config"
|
||||||
"github.com/ncarlier/webhookd/pkg/logger"
|
"github.com/ncarlier/webhookd/pkg/logger"
|
||||||
"github.com/ncarlier/webhookd/pkg/tools"
|
"github.com/ncarlier/webhookd/pkg/tools"
|
||||||
"github.com/ncarlier/webhookd/pkg/worker"
|
"github.com/ncarlier/webhookd/pkg/worker"
|
||||||
|
@ -24,10 +25,10 @@ func atoiFallback(str string, fallback int) int {
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
// Index is the main handler of the API.
|
// index is the main handler of the API.
|
||||||
func Index(timeout int, scrDir string) http.Handler {
|
func index(conf *config.Config) http.Handler {
|
||||||
defaultTimeout = timeout
|
defaultTimeout = *conf.Timeout
|
||||||
scriptDir = scrDir
|
scriptDir = *conf.ScriptDir
|
||||||
return http.HandlerFunc(webhookHandler)
|
return http.HandlerFunc(webhookHandler)
|
||||||
}
|
}
|
||||||
|
|
36
pkg/api/router.go
Normal file
36
pkg/api/router.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ncarlier/webhookd/pkg/auth"
|
||||||
|
"github.com/ncarlier/webhookd/pkg/config"
|
||||||
|
"github.com/ncarlier/webhookd/pkg/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewRouter creates router with declared routes
|
||||||
|
func NewRouter(conf *config.Config) *http.ServeMux {
|
||||||
|
router := http.NewServeMux()
|
||||||
|
authenticator := auth.NewAuthenticator(conf)
|
||||||
|
|
||||||
|
nextRequestID := func() string {
|
||||||
|
return fmt.Sprintf("%d", time.Now().UnixNano())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, route := range routes {
|
||||||
|
var handler http.Handler
|
||||||
|
|
||||||
|
handler = route.HandlerFunc(conf)
|
||||||
|
handler = middleware.Logger(handler)
|
||||||
|
handler = middleware.Tracing(nextRequestID)(handler)
|
||||||
|
|
||||||
|
if authenticator != nil {
|
||||||
|
handler = middleware.Auth(handler, authenticator)
|
||||||
|
}
|
||||||
|
router.Handle(route.Path, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
33
pkg/api/routes.go
Normal file
33
pkg/api/routes.go
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/ncarlier/webhookd/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandlerFunc custom function handler
|
||||||
|
type HandlerFunc func(conf *config.Config) http.Handler
|
||||||
|
|
||||||
|
// Route is the structure of an HTTP route definition
|
||||||
|
type Route struct {
|
||||||
|
Method string
|
||||||
|
Path string
|
||||||
|
HandlerFunc HandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Routes is a list of Route
|
||||||
|
type Routes []Route
|
||||||
|
|
||||||
|
var routes = Routes{
|
||||||
|
Route{
|
||||||
|
"GET",
|
||||||
|
"/",
|
||||||
|
index,
|
||||||
|
},
|
||||||
|
Route{
|
||||||
|
"GET",
|
||||||
|
"/healtz",
|
||||||
|
healthz,
|
||||||
|
},
|
||||||
|
}
|
23
pkg/auth/auth.go
Normal file
23
pkg/auth/auth.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/ncarlier/webhookd/pkg/config"
|
||||||
|
"github.com/ncarlier/webhookd/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Authenticator is a generic interface to validate an HTTP request
|
||||||
|
type Authenticator interface {
|
||||||
|
Validate(r *http.Request) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuthenticator creates new authenticator form the configuration
|
||||||
|
func NewAuthenticator(conf *config.Config) Authenticator {
|
||||||
|
authenticator, err := NewHtpasswdFromFile(*conf.PasswdFile)
|
||||||
|
if err != nil {
|
||||||
|
logger.Debug.Printf("unable to load htpasswd file: \"%s\" (%s)\n", *conf.PasswdFile, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return authenticator
|
||||||
|
}
|
|
@ -1,27 +0,0 @@
|
||||||
package auth
|
|
||||||
|
|
||||||
import "net/http"
|
|
||||||
|
|
||||||
// Method an interface describing an authentication method
|
|
||||||
type Method interface {
|
|
||||||
// Called after ParseParam method.
|
|
||||||
// auth.Method should initialize itself here and get ready to receive requests.
|
|
||||||
// Logger has been initialized so it is safe to call logger methods here.
|
|
||||||
Init(debug bool)
|
|
||||||
// Return Method Usage Info
|
|
||||||
Usage() string
|
|
||||||
// Parse the parameter passed through the -authparam flag
|
|
||||||
// Logger is not initialized at this state so do NOT call logger methods
|
|
||||||
// If the parameter is unacceptable, return an error and main should exit
|
|
||||||
ParseParam(string) error
|
|
||||||
// Return a middleware to handle connections.
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
)
|
|
|
@ -1,59 +0,0 @@
|
||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ncarlier/webhookd/pkg/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
type basicAuth struct {
|
|
||||||
username string
|
|
||||||
password string
|
|
||||||
authheader string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *basicAuth) Init(_ bool) {}
|
|
||||||
|
|
||||||
func (c *basicAuth) Usage() string {
|
|
||||||
return "HTTP Basic Auth. Usage: -auth basic -authparam <username>:<password>[:<realm>] (example: -auth basic -auth-param 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 {
|
|
||||||
logger.Info.Printf("HTTP Basic Auth: %s PASSED\n", username)
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
} else if !ok {
|
|
||||||
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 {
|
|
||||||
logger.Warning.Printf("HTTP Basic Auth: Invalid credentials for username %s\n", username)
|
|
||||||
w.WriteHeader(403)
|
|
||||||
w.Write([]byte("Forbidden."))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
95
pkg/auth/htpasswd-file.go
Normal file
95
pkg/auth/htpasswd-file.go
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/csv"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
shaRe = regexp.MustCompile(`^{SHA}`)
|
||||||
|
bcrRe = regexp.MustCompile(`^\$2b\$|^\$2a\$|^\$2y\$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// HtpasswdFile is a map for usernames to passwords.
|
||||||
|
type HtpasswdFile struct {
|
||||||
|
path string
|
||||||
|
users map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHtpasswdFromFile reads the users and passwords from a htpasswd file and returns them.
|
||||||
|
func NewHtpasswdFromFile(path string) (*HtpasswdFile, error) {
|
||||||
|
r, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
cr := csv.NewReader(r)
|
||||||
|
cr.Comma = ':'
|
||||||
|
cr.Comment = '#'
|
||||||
|
cr.TrimLeadingSpace = true
|
||||||
|
|
||||||
|
records, err := cr.ReadAll()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
users := make(map[string]string)
|
||||||
|
for _, record := range records {
|
||||||
|
users[record[0]] = record[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HtpasswdFile{
|
||||||
|
path: path,
|
||||||
|
users: users,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate HTTP request credentials
|
||||||
|
func (h *HtpasswdFile) Validate(r *http.Request) bool {
|
||||||
|
s := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
|
||||||
|
if len(s) != 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := base64.StdEncoding.DecodeString(s[1])
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
pair := strings.SplitN(string(b), ":", 2)
|
||||||
|
if len(pair) != 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.validateCredentials(pair[0], pair[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HtpasswdFile) validateCredentials(user string, password string) bool {
|
||||||
|
pwd, exists := h.users[user]
|
||||||
|
if !exists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case shaRe.MatchString(pwd):
|
||||||
|
d := sha1.New()
|
||||||
|
_, _ = d.Write([]byte(password))
|
||||||
|
if pwd[5:] == base64.StdEncoding.EncodeToString(d.Sum(nil)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case bcrRe.MatchString(pwd):
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(pwd), []byte(password))
|
||||||
|
if err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
15
pkg/auth/htpasswd-file_test.go
Normal file
15
pkg/auth/htpasswd-file_test.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ncarlier/webhookd/pkg/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateCredentials(t *testing.T) {
|
||||||
|
htpasswdFile, err := NewHtpasswdFromFile("test.htpasswd")
|
||||||
|
assert.Nil(t, err, ".htpasswd file should be loaded")
|
||||||
|
assert.NotNil(t, htpasswdFile, ".htpasswd file should be loaded")
|
||||||
|
assert.Equal(t, true, htpasswdFile.validateCredentials("foo", "bar"), "credentials should be valid")
|
||||||
|
assert.Equal(t, false, htpasswdFile.validateCredentials("foo", "bir"), "credentials should not be valid")
|
||||||
|
}
|
|
@ -1,25 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
2
pkg/auth/test.htpasswd
Normal file
2
pkg/auth/test.htpasswd
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# htpasswd -B -c test.htpasswd foo
|
||||||
|
foo:$2y$05$068L1J0kA3FEh8jHSlnluut4gYleWd47Ig/AWztz8/8bQS6tHvtd.
|
60
pkg/config/config.go
Normal file
60
pkg/config/config.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config contain global configuration
|
||||||
|
type Config struct {
|
||||||
|
ListenAddr *string
|
||||||
|
NbWorkers *int
|
||||||
|
Debug *bool
|
||||||
|
Timeout *int
|
||||||
|
ScriptDir *string
|
||||||
|
PasswdFile *string
|
||||||
|
}
|
||||||
|
|
||||||
|
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"),
|
||||||
|
PasswdFile: flag.String("passwd", getEnv("PASSWD_FILE", ".htpasswd"), "Password file (encoded with htpasswd)"),
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
flag.StringVar(config.PasswdFile, "p", *config.PasswdFile, "Password file (encoded with htpasswd)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get global configuration
|
||||||
|
func Get() *Config {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnv(key, fallback string) string {
|
||||||
|
if value, ok := os.LookupEnv("APP_" + key); ok {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIntEnv(key string, fallback int) int {
|
||||||
|
strValue := getEnv(key, strconv.Itoa(fallback))
|
||||||
|
if value, err := strconv.Atoi(strValue); err == nil {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBoolEnv(key string, fallback bool) bool {
|
||||||
|
strValue := getEnv(key, strconv.FormatBool(fallback))
|
||||||
|
if value, err := strconv.ParseBool(strValue); err == nil {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
20
pkg/middleware/auth.go
Normal file
20
pkg/middleware/auth.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/ncarlier/webhookd/pkg/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Auth is a middleware to checks HTTP request credentials
|
||||||
|
func Auth(inner http.Handler, authn auth.Authenticator) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if authn.Validate(r) {
|
||||||
|
inner.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("WWW-Authenticate", `Basic realm="Ah ah ah, you didn't say the magic word"`)
|
||||||
|
w.WriteHeader(401)
|
||||||
|
w.Write([]byte("401 Unauthorized\n"))
|
||||||
|
})
|
||||||
|
}
|
29
pkg/middleware/logger.go
Normal file
29
pkg/middleware/logger.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ncarlier/webhookd/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type key int
|
||||||
|
|
||||||
|
const (
|
||||||
|
requestIDKey key = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger is a middleware to log HTTP request
|
||||||
|
func Logger(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
start := time.Now()
|
||||||
|
defer func() {
|
||||||
|
requestID, ok := r.Context().Value(requestIDKey).(string)
|
||||||
|
if !ok {
|
||||||
|
requestID = "unknown"
|
||||||
|
}
|
||||||
|
logger.Info.Println(requestID, r.Method, r.URL.Path, r.RemoteAddr, r.UserAgent(), time.Since(start))
|
||||||
|
}()
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
21
pkg/middleware/tracing.go
Normal file
21
pkg/middleware/tracing.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tracing is a middleware to trace HTTP request
|
||||||
|
func Tracing(nextRequestID func() string) func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
requestID := r.Header.Get("X-Request-Id")
|
||||||
|
if requestID == "" {
|
||||||
|
requestID = nextRequestID()
|
||||||
|
}
|
||||||
|
ctx := context.WithValue(r.Context(), requestIDKey, requestID)
|
||||||
|
w.Header().Set("X-Request-Id", requestID)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user