diff --git a/Gopkg.lock b/Gopkg.lock index 65c9b2a..b8c07e1 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,28 +1,5 @@ -memo = "85a9af901e8e5abbab7be176652cf5619eae1955e8e11be0db6e4dbc449c6b57" +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. -[[projects]] - name = "github.com/gin-gonic/gin" - packages = [".","binding","render"] - revision = "e2212d40c62a98b388a5eb48ecbdcf88534688ba" - version = "v1.1.4" - -[[projects]] - branch = "master" - name = "github.com/golang/protobuf" - packages = ["proto"] - revision = "2bba0603135d7d7f5cb73b2125beeda19c09f4ef" - -[[projects]] - branch = "master" - name = "github.com/manucorporat/sse" - packages = ["."] - revision = "ee05b128a739a0fb76c7ebd3ae4810c1de808d6d" - -[[projects]] - name = "github.com/mattn/go-isatty" - packages = ["."] - revision = "fc9e8d8ef48496124e79ae0df75490096eccf6fe" - version = "v0.0.2" [[projects]] name = "github.com/oschwald/geoip2-golang" @@ -42,10 +19,10 @@ memo = "85a9af901e8e5abbab7be176652cf5619eae1955e8e11be0db6e4dbc449c6b57" revision = "fae40520f4ba0a112874d1f0deb9498ebb2198cb" [[projects]] - branch = "master" - name = "golang.org/x/net" - packages = ["context"] - revision = "5602c733f70afc6dcec6766be0d5034d4c4f14de" + name = "github.com/yanc0/beeping" + packages = ["sslcheck"] + revision = "964570feba06594fc570ec4e08bfffe20f5ca805" + version = "v0.5.0" [[projects]] branch = "master" @@ -53,14 +30,9 @@ memo = "85a9af901e8e5abbab7be176652cf5619eae1955e8e11be0db6e4dbc449c6b57" packages = ["unix","windows"] revision = "f3918c30c5c2cb527c0b071a27c35120a6c0719a" -[[projects]] - name = "gopkg.in/go-playground/validator.v8" - packages = ["."] - revision = "5f57d2222ad794d0dffb07e664ea05e2ee07d60c" - version = "v8.18.1" - -[[projects]] - branch = "v2" - name = "gopkg.in/yaml.v2" - packages = ["."] - revision = "cd8b52f8269e0feb286dfeef29f8fe4d5b397e0b" +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "c2420b8e195d681f876e58304a699cc47c4ff446fbae426f42272180f4b8d933" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/README.md b/README.md index b011c8d..d8e1c99 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,25 @@ beeping returns HTTP 500 when check fail. The body contains the reason of the fa ## HTTP Basic Auth -Just add the 'auth' option in your JSON. +### Protect your BeePing server + +Add the auth-* arguments : + +- **auth-user** The auth username +- **auth-secret** The auth user secret +- **auth-method** The auth secret digest mechanism + +**For now, only the 'clear' method is supported.** + +For example : + +``` +$ go run beeping.go -auth-user "john" -auth-password "passw0rd" +``` + +### Authenticated request to the client + +Add the 'auth' field in your JSON. ``` $ curl -XPOST http://localhost:8080/check -d '{"url":"http://127.0.0.1:3000","auth":"john:secret"}' diff --git a/beeping.go b/beeping.go index 60d9408..5525bb4 100644 --- a/beeping.go +++ b/beeping.go @@ -3,6 +3,8 @@ package main import ( "crypto/tls" "encoding/base64" + "encoding/json" + "errors" "flag" "fmt" "io/ioutil" @@ -15,7 +17,6 @@ import ( "strings" "time" - "github.com/gin-gonic/gin" "github.com/oschwald/geoip2-golang" "github.com/tcnksm/go-httpstat" "github.com/yanc0/beeping/sslcheck" @@ -32,6 +33,20 @@ var port *string var tlsmode *bool var validatetarget *bool +var authUser *string +var authSecret *string +var authMethod *string + +type HTTPAuth struct { + User, Secret, Method string +} + +var auth *HTTPAuth + +type ErrorMessage struct { + Message string `json:"message"` +} + type Beeping struct { Version string `json:"version"` Message string `json:"message"` @@ -81,6 +96,16 @@ type Response struct { SSL *sslcheck.CheckSSL `json:"ssl,omitempty"` } +func NewErrorMessage(message string) *ErrorMessage { + var response = ErrorMessage{} + response.Message = message + return &response +} + +func InvalidJSONResponse() *ErrorMessage { + return NewErrorMessage("Invalid JSON sent") +} + func NewResponse() *Response { var response = Response{} response.Timeline = &Timeline{} @@ -143,61 +168,116 @@ func main() { port = flag.String("port", "8080", "The port to bind the server to") tlsmode = flag.Bool("tlsmode", false, "Activate SSL/TLS versions and Cipher support checks (slow)") validatetarget = flag.Bool("validatetarget", true, "Perform some security checks on the target provided") + authUser = flag.String("auth-user", "", "HTTP Auth User e.g. 'admin'") + authSecret = flag.String("auth-secret", "", "HTTP Auth Secret e.g. 'passw0rd'") + authMethod = flag.String("auth-method", "clear", "HTTP Auth Method (only 'clear' for now)") flag.Parse() - gin.SetMode("release") + instantiateAuthMechanism() - router := gin.New() - router.POST("/check", handlerCheck) - router.GET("/", handlerDefault) + http.HandleFunc("/check", handlerCheck) + http.HandleFunc("/", handlerDefault) log.Println("[INFO] Listening on", *listen, *port) - router.Run(*listen + ":" + *port) + http.ListenAndServe(*listen+":"+*port, nil) +} + +func instantiateAuthMechanism() { + if *authUser == "" { + return + } + if *authSecret == "" { + panic(errors.New("Auth secret can not be empty.")) + } + switch strings.ToLower(*authMethod) { + case "clear": + break + default: + panic(errors.New("Unsupported auth method.")) + } + auth = &HTTPAuth{*authUser, *authSecret, *authMethod} + log.Printf("[INFO] HTTP Auth enabled: %v\n", auth) } -func handlerDefault(c *gin.Context) { +func checkAuth(w http.ResponseWriter, r *http.Request) bool { + if auth == nil { + return true + } + user, pass, _ := r.BasicAuth() + fmt.Println(user + " " + pass) + //TODO depending the auth method, transform the pass + if user == auth.User && pass == auth.Secret { + return true + } + log.Println("[INFO] Unauthorized (", user, ")") + w.Header().Add("WWW-Authenticate", "Basic realm=\"Access Denied\"") + http.Error(w, "401, Unauthorized", 401) + return false +} + +func handlerDefault(w http.ResponseWriter, r *http.Request) { + if !checkAuth(w, r) { + return + } var beeping Beeping beeping.Version = VERSION beeping.Message = MESSAGE log.Println("[INFO] Beeping version", beeping.Version) - c.JSON(http.StatusOK, beeping) + jsonRes, _ := json.Marshal(beeping) + w.Header().Set("Content-Type", "application/json") + w.Write(jsonRes) } -func handlerCheck(c *gin.Context) { +func handlerCheck(w http.ResponseWriter, r *http.Request) { + if !checkAuth(w, r) { + return + } + var check = NewCheck() - if c.BindJSON(&check) != nil { + + w.Header().Set("Content-Type", "application/json") + + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&check) + if err != nil { log.Println("[WARN] Invalid JSON sent") - c.JSON(http.StatusBadRequest, gin.H{"message": "invalid json sent"}) + jsonRes, _ := json.Marshal(InvalidJSONResponse()) + w.Write(jsonRes) return } - // with security checks - if *validatetarget { - if err := check.validateTarget(); err != nil { - log.Println("[WARN] Invalid target:", err.Error()) - c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) - return - } + if !*validatetarget { response, err := CheckHTTP(check) if err != nil { log.Println("[WARN] Check failed:", err.Error()) - c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) + jsonRes, _ := json.Marshal(NewErrorMessage(err.Error())) + w.Write(jsonRes) return } log.Println("[INFO] Successful check:", check.URL, "-", response.HTTPRequestTime, "ms") - c.JSON(http.StatusOK, response) + jsonRes, _ := json.Marshal(response) + w.Write(jsonRes) + return + } + + if err := check.validateTarget(); err != nil { + log.Println("[WARN] Invalid target:", err.Error()) + jsonRes, _ := json.Marshal(NewErrorMessage(err.Error())) + w.Write(jsonRes) return } - // without security checks response, err := CheckHTTP(check) if err != nil { log.Println("[WARN] Check failed:", err.Error()) - c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) + jsonRes, _ := json.Marshal(NewErrorMessage(err.Error())) + w.Write(jsonRes) return } + log.Println("[INFO] Successful check:", check.URL, "-", response.HTTPRequestTime, "ms") - c.JSON(http.StatusOK, response) + jsonRes, _ := json.Marshal(response) + w.Write(jsonRes) } // CheckHTTP do HTTP check and return a beeping response