From 4f37589d88396b32947b5e6a0a98d0a003e12614 Mon Sep 17 00:00:00 2001 From: wasim-nihal Date: Sun, 3 Mar 2024 19:52:30 +0530 Subject: [PATCH] support for yaml based configurations for mock endpoints Signed-off-by: wasim-nihal --- examples/example-1.yaml | 24 ++++++++ go.mod | 9 ++- go.sum | 23 +++---- http/auth.go | 27 +++++++++ http/server.go | 129 ++++++++++++++++++++++++++++++++++++++++ main.go | 67 +++++---------------- 6 files changed, 209 insertions(+), 70 deletions(-) create mode 100644 examples/example-1.yaml create mode 100644 http/auth.go create mode 100644 http/server.go diff --git a/examples/example-1.yaml b/examples/example-1.yaml new file mode 100644 index 0000000..0359ab1 --- /dev/null +++ b/examples/example-1.yaml @@ -0,0 +1,24 @@ +endpoints: + /hello: + - method: GET + content: text/plain + body: Hello, World! (GET) + status: 200 + - method: POST + content: text/plain + body: Hello, World! (POST) + status: 200 + /bye: + - method: GET + content: text/plain + body: Goodbye! + status: 400 + +## WIP (currently not available) +# fileServer: +# enable: +# serverDirectory: +# serverFiles: +# - +# - +# - \ No newline at end of file diff --git a/go.mod b/go.mod index a7a8c28..197e86d 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,16 @@ module mock-http-server -go 1.20 +go 1.19 -require github.com/prometheus/client_golang v1.18.0 +require ( + github.com/prometheus/client_golang v1.18.0 + gopkg.in/yaml.v2 v2.4.0 +) require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/kr/text v0.2.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.46.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect diff --git a/go.sum b/go.sum index 62900bf..6d86670 100644 --- a/go.sum +++ b/go.sum @@ -2,31 +2,26 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= -github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= -github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y= github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/http/auth.go b/http/auth.go new file mode 100644 index 0000000..fc2276a --- /dev/null +++ b/http/auth.go @@ -0,0 +1,27 @@ +package httpserver + +import ( + "flag" + "net/http" +) + +var ( + basicAuthUsername = flag.String("basicauth.username", "", "username for basic authentication") + basicAuthPassword = flag.String("basicauth.password", "", "password for basic authentication") +) + +func CheckBasicAuth(w http.ResponseWriter, r *http.Request) bool { + if len(*basicAuthUsername) == 0 { + // HTTP Basic Auth is disabled. + return true + } + username, password, ok := r.BasicAuth() + if ok { + if username == *basicAuthUsername && password == *basicAuthPassword { + return true + } + } + w.Header().Set("WWW-Authenticate", `Basic realm="MockServer"`) + http.Error(w, "", http.StatusUnauthorized) + return false +} diff --git a/http/server.go b/http/server.go new file mode 100644 index 0000000..97a2c80 --- /dev/null +++ b/http/server.go @@ -0,0 +1,129 @@ +package httpserver + +import ( + "crypto/tls" + "flag" + "fmt" + "log" + "net" + "net/http" + "os" + + "github.com/prometheus/client_golang/prometheus/promhttp" + "gopkg.in/yaml.v2" +) + +var ( + ConfigFile = flag.String("server.config", "", "path to server configuration file") + tlsEnable = flag.Bool("tls", false, "Whether to enable TLS for incoming HTTP requests at configured endpoints. -tlsCertFile and -tlsKeyFile must be set if -tls is set. ") + tlsCertFile = flag.String("tlsCertFile", "", "Path to file with TLS certificate if -tls is set.") + tlsKeyFile = flag.String("tlsKeyFile", "", "Path to file with TLS key if -tls is set.") +) + +// Config represents the YAML configuration structure +type Config struct { + Endpoints map[string][]EndpointConfig `yaml:"endpoints"` + TlsConfig TlsConfig `yaml:"tls"` + FileServer FileServer `yaml:"fileServer"` +} + +type FileServer struct { + Enable bool `yaml:"enable"` + ServeDir string `yaml:"serveDirectory"` + ServeFiles []string `yaml:"serveFiles"` +} +type TlsConfig struct { + ServerCert string `yaml:"serverCert"` + ServerKey string `yaml:"serverKey"` + CaCert string `yaml:"caCert"` + EnableMtls bool `yaml:"enableMtls"` +} + +// EndpointConfig represents the configuration for each endpoint +type EndpointConfig struct { + Method string `yaml:"method"` + Content string `yaml:"content"` + Body string `yaml:"body"` + Status int `yaml:"status"` +} + +// MockServer is a mock HTTP server based on the YAML configuration +type MockServer struct { + config Config +} + +// NewMockServer creates a new instance of MockServer with the provided YAML configuration +func NewMockServer() (*MockServer, error) { + var cfg Config + + // Read YAML configuration file + data, err := os.ReadFile(*ConfigFile) + if err != nil { + return nil, err + } + // Unmarshal YAML data into Config struct + if err := yaml.Unmarshal(data, &cfg); err != nil { + return nil, err + } + return &MockServer{ + config: cfg, + }, nil +} + +// MetricsHandler handles requests for Prometheus metrics +func MetricsHandler() http.Handler { + return promhttp.Handler() +} + +// Handler handles incoming HTTP requests and returns mock responses based on the configuration +func (s *MockServer) Handler(w http.ResponseWriter, r *http.Request) { + if !CheckBasicAuth(w, r) { + return + } + endpoint := r.URL.Path + method := r.Method + + if endpointConfigs, ok := s.config.Endpoints[endpoint]; ok { + var config *EndpointConfig + for _, cfg := range endpointConfigs { + if method == cfg.Method { + config = &cfg + } + } + if config != nil { + w.Header().Set("Content-Type", config.Content) + w.WriteHeader(config.Status) + fmt.Fprintf(w, config.Body) + return + } + } + fmt.Fprintf(w, "endpoint not configured\n") + // Return 404 if the endpoint or method is not configured + http.NotFound(w, r) +} + +func GetListener(addr string) (*net.Listener, error) { + var tlsConfig *tls.Config + var cert tls.Certificate + var err error + if *tlsEnable { + cert, err = tls.LoadX509KeyPair(*tlsCertFile, *tlsKeyFile) + if err != nil { + log.Printf("unable to load the tls certificates. reason: %s", err.Error()) + return nil, err + } + tlsConfig = &tls.Config{ + GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { + return &cert, nil + }, + } + } + ln, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + if tlsConfig != nil { + ln = tls.NewListener(ln, tlsConfig) + } + return &ln, err +} diff --git a/main.go b/main.go index 0bbd34c..277919e 100644 --- a/main.go +++ b/main.go @@ -4,67 +4,28 @@ import ( "flag" "fmt" "log" + httpserver "mock-http-server/http" "net/http" - - "github.com/prometheus/client_golang/prometheus/promhttp" ) var ( - username = flag.String("username", "", "Username for basic authentication") - password = flag.String("password", "", "Password for basic authentication") - port = flag.Int("port", 8080, "http port for server") + port = flag.String("port", "8080", "http port for server") ) func main() { flag.Parse() - var handler, metricsHandler func(w http.ResponseWriter, r *http.Request) - if *username != "" && *password != "" { - log.Println("Started Http Server with basic auth enabled") - handler = handleWithBasicAuth(serverHandler, *username, *password) - metricsHandler = handleWithBasicAuth(promhttp.Handler().ServeHTTP, *username, *password) - } else { - log.Println("Started Http Server with basic auth disabled") - handler = serverHandler - metricsHandler = promhttp.Handler().ServeHTTP + // Create a new mock server with the provided YAML configuration + mockServer, err := httpserver.NewMockServer() + if err != nil { + log.Fatalf("Failed to create mock server: %v", err) } - http.HandleFunc("/metrics", metricsHandler) - http.HandleFunc("/", handler) - port := *port - addr := fmt.Sprintf(":%d", port) - fmt.Printf("Server started at http://localhost:%d\n", port) - log.Fatal(http.ListenAndServe(addr, nil)) -} - -func serverHandler(w http.ResponseWriter, r *http.Request) { - // Log request details - log.Printf("Received %s request from %s with payload: %s\n", r.Method, r.RemoteAddr, extractPayload(r)) - - // Dummy response - response := "Hello, this is a dummy response!" - w.WriteHeader(http.StatusOK) - w.Write([]byte(response)) -} - -func handleWithBasicAuth(handler http.HandlerFunc, username, password string) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - user, pass, ok := r.BasicAuth() - - if !ok || user != username || pass != password { - log.Printf("Unauthorized access attempt from %s with incorrect credentials\n", r.RemoteAddr) - w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - handler(w, r) - } -} - -func extractPayload(r *http.Request) string { - if r.Method == http.MethodPost { - body := make([]byte, r.ContentLength) - r.Body.Read(body) - return string(body) + http.HandleFunc("/", mockServer.Handler) + http.HandleFunc("/metrics", httpserver.MetricsHandler().ServeHTTP) + // Start the mock server on port 8080 + log.Printf("Mock server is running on port %s...", *port) + ln, err := httpserver.GetListener(fmt.Sprintf("127.0.0.1:%s", *port)) + if err != nil { + log.Fatal(err) } - return "" + http.Serve(*ln, nil) }