From 88c9cb3eefe0d1ba2b8b33cdb52d999edc439beb Mon Sep 17 00:00:00 2001 From: Nathan Coad Date: Wed, 11 Sep 2024 21:24:34 +1000 Subject: [PATCH] more file structure --- .gitignore | 6 +- api/resource/common/handler.go | 43 +++++++++ api/resource/vm/handler.go | 32 +++++++ api/resource/vm/model.go | 58 ++++++++++++ api/router/router.go | 25 +++++ cert.pem | 30 ++++++ cmd/api/main.go | 157 +++++++++++++++++++++++++++++++ data.sqlite-journal | Bin 8720 -> 0 bytes go.mod | 13 ++- go.sum | 10 ++ internal/utils/certOperations.go | 139 +++++++++++++++++++++++++++ internal/utils/utils.go | 55 +++++++++++ privkey.pem | 52 ++++++++++ vm/db/models.go | 7 -- vm/db/query.sql.go | 51 +++++----- vm/query.sql | 4 +- vm/schema.sql | 7 -- 17 files changed, 649 insertions(+), 40 deletions(-) create mode 100644 api/resource/common/handler.go create mode 100644 api/resource/vm/handler.go create mode 100644 api/resource/vm/model.go create mode 100644 api/router/router.go create mode 100644 cert.pem delete mode 100644 data.sqlite-journal create mode 100644 go.sum create mode 100644 internal/utils/certOperations.go create mode 100644 internal/utils/utils.go create mode 100644 privkey.pem diff --git a/.gitignore b/.gitignore index 6d79957..386168d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -data.sqlite \ No newline at end of file +data.sqlite +data.sqlite-journal +.env +vctp +vctp.log \ No newline at end of file diff --git a/api/resource/common/handler.go b/api/resource/common/handler.go new file mode 100644 index 0000000..e732f25 --- /dev/null +++ b/api/resource/common/handler.go @@ -0,0 +1,43 @@ +package common + +import ( + "fmt" + "net" + "net/http" +) + +// Display some information about this API for the default page +// TODO - static fileserver with docs +func HomeLink(w http.ResponseWriter, r *http.Request) { + //w.WriteHeader(http.StatusNotImplemented) + fmt.Fprintf(w, "VM Chargeback Tracking Program. API interface only. See Nathan Coad (nathan.coad@dell.com) for further details. ") +} + +// Display error message for invalid requests +func HandleNotFound(w http.ResponseWriter, r *http.Request) { + ip, err := IPFromRequest(r) + if err != nil { + fmt.Println("Error", err) + http.Error(w, "VM Chargeback Tracking Program.. Invalid Path Specified.", http.StatusNotFound) + return + } + + fmt.Printf("Request from IP %s\n", ip.String()) + http.Error(w, "VM Chargeback Tracking Program. Invalid Path Specified.", http.StatusNotFound) + // TODO - investigate rate limiting for invalid requests +} + +// IPFromRequest extracts the user IP address from req, if present. +// @see https://blog.golang.org/context/userip/userip.go +func IPFromRequest(req *http.Request) (net.IP, error) { + ip, _, err := net.SplitHostPort(req.RemoteAddr) + if err != nil { + return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr) + } + + userIP := net.ParseIP(ip) + if userIP == nil { + return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr) + } + return userIP, nil +} diff --git a/api/resource/vm/handler.go b/api/resource/vm/handler.go new file mode 100644 index 0000000..0e894f6 --- /dev/null +++ b/api/resource/vm/handler.go @@ -0,0 +1,32 @@ +package vm + +import ( + "fmt" + "io" + "log/slog" + "net/http" + + "github.com/gorilla/mux" +) + +// TODO godoc +func VmCreateHandler(w http.ResponseWriter, r *http.Request) { + + reqBody, err := io.ReadAll(r.Body) + if err != nil { + fmt.Fprintf(w, "Invalid data received") + w.WriteHeader(http.StatusInternalServerError) + return + } + + slog.Debug("received create request", "body", string(reqBody)) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "Create Request (%d): %v\n", len(reqBody), string(reqBody)) +} + +// TODO godoc +func VmRemoveHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "Remove Request: %v\n", vars) +} diff --git a/api/resource/vm/model.go b/api/resource/vm/model.go new file mode 100644 index 0000000..02642f9 --- /dev/null +++ b/api/resource/vm/model.go @@ -0,0 +1,58 @@ +package vm + +import "time" + +type CloudEventReceived struct { + ID string `json:"id"` + Specversion string `json:"specversion"` + Source string `json:"source"` + Type string `json:"type"` + Time time.Time `json:"time"` + Data struct { + ChainID int `json:"ChainId"` + ChangeTag string `json:"ChangeTag"` + ComputeResource struct { + ComputeResource struct { + Type string `json:"Type"` + Value string `json:"Value"` + } `json:"ComputeResource"` + Name string `json:"Name"` + } `json:"ComputeResource"` + CreatedTime time.Time `json:"CreatedTime"` + Datacenter struct { + Datacenter struct { + Type string `json:"Type"` + Value string `json:"Value"` + } `json:"Datacenter"` + Name string `json:"Name"` + } `json:"Datacenter"` + Ds interface{} `json:"Ds"` + Dvs interface{} `json:"Dvs"` + FullFormattedMessage string `json:"FullFormattedMessage"` + Host struct { + Host struct { + Type string `json:"Type"` + Value string `json:"Value"` + } `json:"Host"` + Name string `json:"Name"` + } `json:"Host"` + Key int `json:"Key"` + Net interface{} `json:"Net"` + SrcTemplate struct { + Name string `json:"Name"` + VM struct { + Type string `json:"Type"` + Value string `json:"Value"` + } `json:"Vm"` + } `json:"SrcTemplate"` + Template bool `json:"Template"` + UserName string `json:"UserName"` + VM struct { + Name string `json:"Name"` + VM struct { + Type string `json:"Type"` + Value string `json:"Value"` + } `json:"Vm"` + } `json:"Vm"` + } `json:"data"` +} diff --git a/api/router/router.go b/api/router/router.go new file mode 100644 index 0000000..a045075 --- /dev/null +++ b/api/router/router.go @@ -0,0 +1,25 @@ +package router + +import ( + "net/http" + "vm-ctp/api/resource/common" + "vm-ctp/api/resource/vm" + + "github.com/gorilla/mux" +) + +func New() *mux.Router { + r := mux.NewRouter() + + // If nothing more specific is requested then just display some version information + r.HandleFunc("/", common.HomeLink) + + s := r.PathPrefix("/api").Subrouter() + s.HandleFunc("/event/vm/create", vm.VmCreateHandler).Methods("POST") // receive VM creation event from Direktiv + s.HandleFunc("/event/vm/remove", vm.VmRemoveHandler).Methods("POST") // receive VM creation event from Direktiv + + // Not found handler + r.NotFoundHandler = http.HandlerFunc(common.HandleNotFound) + + return r +} diff --git a/cert.pem b/cert.pem new file mode 100644 index 0000000..2152cf1 --- /dev/null +++ b/cert.pem @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFIjCCAwqgAwIBAgIRALRk49JcstjX/i+y8wHc4KAwDQYJKoZIhvcNAQELBQAw +DzENMAsGA1UEChMERFRNUzAeFw0yNDA5MTExMDU0NDBaFw0yNTA5MTExMDU0NDBa +MA8xDTALBgNVBAoTBERUTVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQDVVlFCBCo5uJ4+D33Y9iJjz5PbW76ubMSWf+ndBTkUF9Psw+eyzkR1b7EVT2/c +eJfAK2rE6vTAajf9JItRMLCQXPVRf8ecmg/NcVhwuNSKG86TDqGfbNBmBnSZQhEu +JSfMoZvUiYtKd8JY9o8Y7n9BaU3cM0lg57+2TvLsF1WEY4DKr2P46AmhoMnqgC2n +1hokgNmPIuxfWk1N+zWkw0Kec2KiroB4hKiwSIcidR6YaR5/8Iiuorg2O4rhNx1D +lFGHUAIPTpCrpQjoMD3rHcnpGjK6NksO8gFOgjjd9XKpznGk2+bhPxTA9mwoL+qx +E/MdTAjKn+fJB2FYduT8ZlkFSu98WGjZr5/UT72fDemNVMNGfzWVEGgWQCRSHvvI +KzPb8A8GLaSj0CH/jww1KyXFmu8adU7Spg/ZHU9rStgdrGfGmtx6MXLE1a87mEQl +M/suEP7/ikK570FSJh1kop1MdDNPaMxtsRXfGws1JVWMhPzlmReC9jkAbfW5axfd +Nh4jYJm18rqlQR7JDPjusD4zdI+mesoiI45vaG0/b3F63pKYLuREuE0RIpimQlKV +A0+X2RXk8+h9Xpc3Z1pVoHHcoQ3Jw+JM0KScFncaBxczqSo/EQzw4LE9XYyz7Rks +qpU3WzvcVimEpj6flpqobGI63kf1rSkeCaIDLdgUlUJTxQIDAQABo3kwdzAOBgNV +HQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUAa1rnwMtehNK2bVPXKivXzNHIKYwIAYDVR0RBBkwF4IPbmF0 +aG1icDE0LmxvY2FshwQKAAGhMA0GCSqGSIb3DQEBCwUAA4ICAQBenuJYnqTcELUP +scHkrVhH0zpmejRk55qBAxIpfR4XFklKr1lt3aHTs3dDbfS9t3ni/S2RVVhVtgWm +pXt3AteYCkA8CbuhSBirtVrDWi/NT6ClZHRgCcfoZzE5uh9HuQOQpGg2iRby/tnG +FU2o8KpYETcCEEuvhlusIg7FTueqCMCvbjsZ8j9PY5nZ0JZmAAqS2g4MR0zqFBfj +wamx3gXLupMdOdcAnqlVkc6UxkWyLvhcy1kpl+E+xzFoOsC0XCLSwFdM2gIB21yd +HcVMOnP38t+ijrErv5prsczuUhP0T3q/XBr5wzc4OBIQYbqjzhP67JaVZ6HU8UWj +GTuUuuZDwSTBoIJ0sDxmoMt9z2j7xFvSeMVKf13CRTR2s5QLPX/1AIyANg88xjQM +jJPGyiaBCYZZJbjH2SwwKuyqOaph8yz140QgxMNi6yymbruFBk9Gj9b6jDklFSM5 +kHpmC4Gs8apL57odLnwl/Bf6A51z6BtZe6vzKl33ED5Zq0AlgIh3cICsXMwBTB+1 +6xGFVpyqjKLe4p0laDrJh26O2wCjZdztU71pTIKU+UNhxCssXmy8Dw7qm82xgPxK +EENv6jK8jArVAyCJcmLxzhT2cvaN0NnqVRc6zDvY3K+KzG1Q7AcpJGeaM0BZym7l +9so3Qdv2RdPWtgdNX4EMDwxTUaeD6g== +-----END CERTIFICATE----- diff --git a/cmd/api/main.go b/cmd/api/main.go index 06ab7d0..e78ca70 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -1 +1,158 @@ package main + +import ( + "context" + "crypto/tls" + "fmt" + "log" + "log/slog" + "net/http" + "os" + "os/signal" + "time" + "vm-ctp/api/router" + utils "vm-ctp/internal/utils" + + "github.com/gorilla/handlers" + "github.com/joho/godotenv" +) + +// For build numbers, from https://blog.kowalczyk.info/article/vEja/embedding-build-number-in-go-executable.html +var sha1ver string // sha1 revision used to build the program +var buildTime string // when the executable was built +var wait time.Duration +var bindDisableTls bool + +func main() { + // Load data from environment file + envFilename := utils.GetFilePath(".env") + err := godotenv.Load(envFilename) + if err != nil { + panic("Error loading .env file") + } + + // Open connection to logfile + // From https://ispycode.com/GO/Logging/Logging-to-multiple-destinations + logFile := os.Getenv("LOG_FILE") + if logFile == "" { + logFile = "./vctp.log" + } + logfileWriter, err := os.OpenFile(logFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + fmt.Printf("Unable to write logfile '%s' : '%s'\n", logFile, err) + os.Exit(1) + } + + h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true}) + slog.SetDefault(slog.New(h)) + + //log.SetOutput(logfileWriter) + //log.Printf("vCTP starting execution. Built on %s from sha1 %s. Runtime %s\n", buildTime, sha1ver, runtime.Version()) + + r := router.New() + + // Log everything to stdout in Apache Common Log Format + var loggedRouter http.Handler + loggedRouter = handlers.LoggingHandler(logfileWriter, r) + + // Set some options for TLS + tlsConfig := &tls.Config{ + MinVersion: tls.VersionTLS12, + CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256}, + PreferServerCipherSuites: true, + InsecureSkipVerify: true, + CipherSuites: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_RSA_WITH_AES_256_CBC_SHA, + }, + } + + // Determine bind IP + bindIP := os.Getenv("BIND_IP") + if bindIP == "" { + bindIP = utils.GetOutboundIP().String() + } + // Determine bind port + bindPort := os.Getenv("BIND_PORT") + if bindPort == "" { + bindPort = "9443" + } + bindAddress := fmt.Sprint(bindIP, ":", bindPort) + slog.Info("Will listen on address", "ip", bindIP, "port", bindPort) + + // Determine bind disable TLS + bindDisableTlsEnv := os.Getenv("BIND_DISABLE_TLS") + if bindDisableTlsEnv == "true" { + bindDisableTls = true + } + + // Get file names for TLS cert/key + tlsCertFilename := os.Getenv("TLS_CERT_FILE") + if tlsCertFilename != "" { + tlsCertFilename = utils.GetFilePath(tlsCertFilename) + } else { + tlsCertFilename = "./cert.pem" + } + + tlsKeyFilename := os.Getenv("TLS_KEY_FILE") + if tlsKeyFilename != "" { + tlsKeyFilename = utils.GetFilePath(tlsKeyFilename) + } else { + tlsKeyFilename = "./privkey.pem" + } + + // Generate certificate if required + if !(utils.FileExists(tlsCertFilename) && utils.FileExists(tlsKeyFilename)) { + slog.Warn("Specified TLS certificate or private key do not exist", "certificate", tlsCertFilename, "tls-key", tlsKeyFilename) + utils.GenerateCerts(tlsCertFilename, tlsKeyFilename) + } + + // Configure the http server + srv := &http.Server{ + Addr: bindAddress, + // Good practice to set timeouts to avoid Slowloris attacks. + WriteTimeout: time.Second * 15, + ReadTimeout: time.Second * 15, + IdleTimeout: time.Second * 60, + Handler: loggedRouter, // Pass our instance of gorilla/mux in. + TLSConfig: tlsConfig, + TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)), + } + slog.Info("Started listening on", "bindAddress", bindAddress) + + // Run our server in a goroutine so that it doesn't block + // Also decide whether to start a TLS or plain http server + go func() { + if bindDisableTls { + if err := srv.ListenAndServe(); err != nil { + log.Println(err) + } + } else { + if err := srv.ListenAndServeTLS(tlsCertFilename, tlsKeyFilename); err != nil { + log.Println(err) + } + } + }() + + c := make(chan os.Signal, 1) + // We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C) + // SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught. + signal.Notify(c, os.Interrupt) + + // Block until we receive our signal. + <-c + + // Create a deadline to wait for. + ctx, cancel := context.WithTimeout(context.Background(), wait) + defer cancel() + // Doesn't block if no connections, but will otherwise wait + // until the timeout deadline. + srv.Shutdown(ctx) + // Optionally, you could run srv.Shutdown in a goroutine and block on + // <-ctx.Done() if your application should wait for other services + // to finalize based on context cancellation. + slog.Info("shutting down") + os.Exit(0) +} diff --git a/data.sqlite-journal b/data.sqlite-journal deleted file mode 100644 index d090de216a2a270103f70a2b5b6f2583f1a47bc5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8720 zcmeI$y9&ZE6oBER=qUOEg){2n;tN=%i*)e<&K08s?S+!1tIy$x2tZZLr+L z(Jq!