From 4fd71945e9bf81cf8cb6355b20abab0d3a2586b2 Mon Sep 17 00:00:00 2001 From: Tom Peltonen Date: Sun, 8 Jan 2023 22:59:06 +1100 Subject: [PATCH] Template files added --- src/api/authn51/authn51.go | 274 ++++++++++++++++++++++++++++++++++++ src/api/configuration.go | 185 ++++++++++++++++++++++++ src/api/errorh/errors.go | 27 ++++ src/api/errorh/httperror.go | 93 ++++++++++++ src/api/generalApi.gen.go | 20 +++ src/api/getHealth.go | 98 +++++++++++++ src/api/serverapi.go | 175 +++++++++++++++++++++++ src/api/utility.go | 30 ++++ src/build/server.cfg.yaml | 5 + src/build/types.cfg.yaml | 4 + src/servermain.go | 132 +++++++++++++++++ 11 files changed, 1043 insertions(+) create mode 100644 src/api/authn51/authn51.go create mode 100644 src/api/configuration.go create mode 100644 src/api/errorh/errors.go create mode 100644 src/api/errorh/httperror.go create mode 100644 src/api/generalApi.gen.go create mode 100644 src/api/getHealth.go create mode 100644 src/api/serverapi.go create mode 100644 src/api/utility.go create mode 100644 src/build/server.cfg.yaml create mode 100644 src/build/types.cfg.yaml create mode 100644 src/servermain.go diff --git a/src/api/authn51/authn51.go b/src/api/authn51/authn51.go new file mode 100644 index 0000000..f569369 --- /dev/null +++ b/src/api/authn51/authn51.go @@ -0,0 +1,274 @@ +package authn51 + + +import ( + "errors" + "log" + "os" + "fmt" + "time" + "context" + "net/http" + "strings" + + "gopkg.in/yaml.v2" + "github.com/dgrijalva/jwt-go" + "github.com/google/uuid" + "github.com/labstack/echo/v4" + "github.com/getkin/kin-openapi/openapi3filter" +) + + +type Authn51 struct { + Environment string + Providers []string + AuthnFunc openapi3filter.AuthenticationFunc + Bearer struct { + Secret string + DefaultUser string + } + Basic struct { + DefaultUser string + Password string + } + Apikey struct { + ApiValue string + } +} + + +type Configuration struct { + Environment string `yaml:"environment"` + Authn51 struct { + Bearer struct { + Secret string `yaml:"secret"` + DefaultUser string `yaml:"default_user"` + } `yaml:"bearer"` + Basic struct { + DefaultUser string `yaml:"default_user"` + Password string `yaml:"password"` + } `yaml:"basic"` + Apikey struct { + ApiValue string `yaml:"api_value"` + } `yaml:"api_key"` + } `yaml:"authn51"` +} + + +type AuthenticationToken struct { + Environment string `yaml:"environment"` + Token string `yaml:"token"` + Claims struct { + UserId string `yaml:"user_id"` + Exp string `yaml:"exp"` + } `yaml:"claims"` +} + +func NewAuthn51(configPath string) (*Authn51, error) { + + var authn51 Authn51 + var config Configuration + + file, err := os.Open(configPath) + if err != nil { + return nil, err + } + defer file.Close() + + d := yaml.NewDecoder(file) + + if err := d.Decode(&config); err != nil { + return nil, err + } + + authn51.Bearer.DefaultUser = config.Authn51.Bearer.DefaultUser + authn51.Bearer.Secret = config.Authn51.Bearer.Secret + authn51.AuthnFunc = authn51.AuthenticationFunc + if (authn51.Bearer.Secret == "%ACCESS_SECRET%") { + authn51.Bearer.Secret = os.Getenv("%ACCESS_SECRET%") + } + if (authn51.Bearer.Secret == "") { + log.Printf("No secret supplied. Generating random value") + authn51.Bearer.Secret = uuid.New().String() + } + + return &authn51, nil +} + + +func (self *Authn51) CreateToken(userId string) (AuthenticationToken, error) { + var err error + var authToken AuthenticationToken + + exp := time.Now().Add(time.Minute * 20) + + atClaims := jwt.MapClaims{} + atClaims["authorized"] = true + atClaims["organization"] = "any" + atClaims["user_id"] = userId + atClaims["exp"] = exp.Unix() + + at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims) + token, err := at.SignedString([]byte(self.Bearer.Secret)) + if err != nil { + return authToken, err + } + + authToken.Token = token + authToken.Claims.UserId = userId + authToken.Claims.Exp = exp.String() + + return authToken, nil +} + + + +func (self *Authn51) extractToken(r *http.Request) string { + bearToken := r.Header.Get("Authorization") + strArr := strings.Split(bearToken, " ") + if len(strArr) == 2 { + return strArr[1] + } + return "" +} + +func (self *Authn51) verifyToken(r *http.Request) (*jwt.Token, error) { + tokenString := self.extractToken(r) + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + //Make sure that the token method conform to "SigningMethodHMAC" + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(self.Bearer.Secret), nil + }) + if err != nil { + return nil, err + } + return token, nil +} + +func (self *Authn51) tokenValid(r *http.Request) error { + token, err := self.verifyToken(r) + if err != nil { + return err + } + if _, ok := token.Claims.(jwt.Claims); !ok && !token.Valid { + return err + } + return nil +} + +type AccessDetails struct { + Organization string + UserId string +} + +func (self *Authn51) ExtractTokenMetadata(r *http.Request) (*AccessDetails, error) { + token, err := self.verifyToken(r) + if err != nil { + return nil, err + } + claims, ok := token.Claims.(jwt.MapClaims) + if ok && token.Valid { + organization, ok := claims["organization"].(string) + if !ok { + return nil, fmt.Errorf("Extract error with missing '%s'", "organization") + } + userId, ok := claims["user_id"].(string) + if !ok { + return nil, fmt.Errorf("Extract error with missing '%s'", "user_id") + } + return &AccessDetails{ + Organization: organization, + UserId: userId, + }, nil + } + return nil, fmt.Errorf("Token extract error: %s", "unknown") +} + + +func (self *Authn51) AuthenticationFunc(ctx context.Context, ai *openapi3filter.AuthenticationInput) error { + + schemeN := ai.SecuritySchemeName + if (schemeN == "") { + log.Print("Scheme name is blank") + } + + req := ai.RequestValidationInput.Request + + if (ai.SecurityScheme.Scheme == "basic") { + userName, password, ok := req.BasicAuth() + if (ok){ + if (userName == "joe" && password == "secret") { + return nil + } + } + + err := errors.New("Basic authentication failed!") + msg := fmt.Errorf("security requirement '%q' failed", schemeN) + return &echo.HTTPError{ + Code: http.StatusUnauthorized, + Message: msg, + Internal: err, + } + + } + + if (ai.SecurityScheme.Scheme == "bearer") { + + ad, err := self.ExtractTokenMetadata(req) + if (err != nil || ad == nil) { + + log.Print("Bearer authentication failed extraction!") + err := errors.New("Bearer authentication failed!") + msg := fmt.Errorf("security requirement '%q' failed", schemeN) + return &echo.HTTPError{ + Code: http.StatusUnauthorized, + Message: msg, + Internal: err, + } + + } + + if (ad.UserId != "") { + return nil + } + + log.Print("Bearer authentication failed user id!") + err = errors.New("Bearer authentication failed!") + msg := fmt.Errorf("security requirement %q failed", schemeN) + return &echo.HTTPError{ + Code: http.StatusUnauthorized, + Message: msg, + Internal: err, + } + + } + + if (ai.SecurityScheme.Type == "apiKey") { + auth := req.Header.Get("X-ApiKey") + if auth == "secret" { + return nil + } + + err := errors.New("API key authentication failed!") + msg := fmt.Errorf("security requirement '%q' failed", schemeN) + return &echo.HTTPError{ + Code: http.StatusUnauthorized, + Message: msg, + Internal: err, + } + + } + + log.Printf("security requirement '%q' failed", schemeN) + err := errors.New("Authentication failed!") + msg := fmt.Errorf("security requirement '%q' failed", schemeN) + return &echo.HTTPError{ + Code: http.StatusUnauthorized, + Message: msg, + Internal: err, + } + +} + diff --git a/src/api/configuration.go b/src/api/configuration.go new file mode 100644 index 0000000..6cfdeb9 --- /dev/null +++ b/src/api/configuration.go @@ -0,0 +1,185 @@ +package api + + +import ( + "flag" + "fmt" + "os" + "log" + "time" + + "gopkg.in/yaml.v2" +) + + + +type SoftwareConfiguration struct { + configFile string + // Execution environment + // If not psecified or recognised then assume product + // for safey. Vales are: + // - production + // - staging + // - testing + // - development + Environment string `yaml:"environment"` + Server struct { + // IP Address to bind the HTTP Server to + Host string `yaml:"host"` + // TCP Port to bind the HTTP Server to + Port int `yaml:"port"` + // Service base path + BasePath string `yaml:"base_path"` + // Timeout vaues + Timeout struct { + // Server is the general server timeout to use + // for graceful shutdowns + Server time.Duration `yaml:"server"` + + // Write is the amount of time to wait until an HTTP server + // write opperation is cancelled + Write time.Duration `yaml:"write"` + + // Read is the amount of time to wait until an HTTP server + // read operation is cancelled + Read time.Duration `yaml:"read"` + + // Read is the amount of time to wait + // until an IDLE HTTP session is closed + Idle time.Duration `yaml:"idle"` + } `yaml:"timeout"` + } `yaml:"server"` + Folders struct { + // Data folder + Data string `yaml:"data"` + // Static HTLM, CSS folder + Static string `yaml:"static"` + // Configuration folder + Config string `yaml:"config"` + } `yaml:"folders"` + DataSource struct { + // Load products on start up + Load bool `yaml:"load"` + // Overwrte existing product data + Overwrite bool `yaml:"overwrite"` + } `yaml:"dataSource"` + Api struct { + // Deafult page size for lists + PageSize int `yaml:"page_size"` + } `yaml:"api"` + Hosts struct { + // Product protocol, host name and port, e.g.http://localhost:8075 + Product string `yaml:"product"` + // Portfolio, account protocol, host name and port, e.g.http://localhost:8076 + Protfolio string `yaml:"portfolio"` + // Quote protocol, host name and port, e.g.http://localhost:8077 + Quote string `yaml:"quote"` + // Order managemet protocol, host name and port, e.g.http://localhost:8077 + Order string `yaml:"order"` + // Search protocol, host name and port, e.g.http://localhost:8075 + Search string `yaml:"search"` + } `yaml:"hosts"` + Support struct { + // Message verbose level, 1=minimum message, 5=All messages + Level int `yaml:"level"` + } `yaml:"support"` +} + +func NewSoftwareConfiguration() *SoftwareConfiguration { + + var sc = SoftwareConfiguration{} + + sc.Environment = "production" + sc.Server.BasePath = "/" + + sc.DataSource.Load = true + sc.DataSource.Overwrite = false + + sc.Folders.Static = "./static" + sc.Folders.Data = "./data" + sc.Folders.Config = "./config" + + sc.Support.Level= 1 + + sc.Api.PageSize = 20 + + return &sc +} + +func (self SoftwareConfiguration) GetConfigurationFile() string { + return self.configFile +} + +func LoadConfig(configPath string) (*SoftwareConfiguration, error) { + + config := &SoftwareConfiguration{} + + file, err := os.Open(configPath) + if err != nil { + return nil, err + } + defer file.Close() + + d := yaml.NewDecoder(file) + + if err := d.Decode(&config); err != nil { + return nil, err + } + + config.configFile = configPath + + if (config.Environment == "") { + config.Environment = "production" + } + + // Perform some validation + if (config.Environment != "production" && + config.Environment != "staging" && + config.Environment != "testing" && + config.Environment != "development") { + log.Printf("Environment value '%s' not valid. Reverting to defauult", config.Environment) + config.Environment = "production" + } + + if (config.Api.PageSize < 1) { + config.Api.PageSize = 20 + } + + return config, nil +} + +func ParseFlags(configPath string) (*SoftwareConfiguration, error) { + + flag.StringVar(&configPath, "config", configPath, "path to config file") + var port = flag.Int("port", 0, "Port for HTTP server micro service") + var dataFolder = flag.String("data", "", "Data folder") + var staticFolder = flag.String("static", "", "Static folder") + + flag.Parse() + + fileStat, err := os.Stat(configPath) + if err != nil { + return nil, err + } + if fileStat.IsDir() { + return nil, fmt.Errorf("'%s' is a directory, not a normal file", configPath) + } + + config, err := LoadConfig(configPath) + if err != nil { + return nil, err + } + + if (*port > 0) { + config.Server.Port = *port + } + if (*dataFolder != "") { + config.Folders.Data = *dataFolder + } + if (*staticFolder != "") { + config.Folders.Static = *staticFolder + } + + return config, nil +} + diff --git a/src/api/errorh/errors.go b/src/api/errorh/errors.go new file mode 100644 index 0000000..f323f62 --- /dev/null +++ b/src/api/errorh/errors.go @@ -0,0 +1,27 @@ +package errorh + + + +// A common error payload returned +// when the response code is not 2xx +type ErrorModel struct { + // Error description, that shuld be less technical + // and more user orientated where possible + Message string `json:"message,omitempty"` + + // Information on how the error or issue may + // be resolved. + Resolution string `json:"resolution,omitempty"` + + // Status identifier that can be used to identify the + // cause of the error. + // + // It s not the HTTP status code (eg 4xx ot 5xx) + Status string `json:"status,omitempty"` + + // Technical information for the error. + // + // This must not contain sensitive information + Technical string `json:"technical,omitempty"` +} + diff --git a/src/api/errorh/httperror.go b/src/api/errorh/httperror.go new file mode 100644 index 0000000..fa60215 --- /dev/null +++ b/src/api/errorh/httperror.go @@ -0,0 +1,93 @@ +package errorh + +import ( + "errors" + "github.com/labstack/echo/v4" + "net/http" + "fmt" +) + +type ( + httpErrorHandler struct { + statusCodes map[error]int + } +) + + +func NewHttpErrorHandler() *httpErrorHandler { + return &httpErrorHandler{} +} + +func (self *httpErrorHandler) getStatusCode(err error) int { + for key, value := range self.statusCodes { + if errors.Is(err, key) { + return value + } + } + + return http.StatusInternalServerError +} + +func unwrapRecursive(err error) error { + var originalErr = err + + for originalErr != nil { + var internalErr = errors.Unwrap(originalErr) + + if internalErr == nil { + break + } + + originalErr = internalErr + } + + return originalErr +} + +func (self *httpErrorHandler) Handler(err error, c echo.Context) { + + technical := "" + he, ok := err.(*echo.HTTPError) + if ok { + if he.Internal != nil { + if herr, ok := he.Internal.(*echo.HTTPError); ok { + he = herr + } + technical = fmt.Sprintf("%v", he.Internal) + } + } else { + he = &echo.HTTPError{ + Code: self.getStatusCode(err), + Message: unwrapRecursive(err).Error(), + } + } + + code := he.Code + stdError := ErrorModel{ + Message: fmt.Sprintf("%v", he.Message), + Resolution: "", + Status: fmt.Sprintf("DM00%d", code), + Technical: technical, + } + + if (code == 401) { + stdError.Resolution = "Please provide authentication details. These can be API key, Basic Auth, Bearer Auth." + } + if (code == 400) { + if (stdError.Message == "no matching operation was found") { + stdError.Resolution = "Please check the Operation name and resource and correct to a valid name. Reference the Open API specification for valid names" + } + } + + // Send response + if !c.Response().Committed { + if c.Request().Method == http.MethodHead { + err = c.NoContent(he.Code) + } else { + err = c.JSON(code, stdError) + } + if err != nil { + c.Echo().Logger.Error(err) + } + } +} diff --git a/src/api/generalApi.gen.go b/src/api/generalApi.gen.go new file mode 100644 index 0000000..84cc10b --- /dev/null +++ b/src/api/generalApi.gen.go @@ -0,0 +1,20 @@ +package api + +import ( + "net/http" + + "github.com/labstack/echo/v4" + + "{{moduleName}}/api/gen" +) + + +func (self *BianApiService) {{servicesign}} { + + self.Lock.Lock() + defer self.Lock.Unlock() + + message := "{{funcName}} service API not implemented" + status := "DM00501" + return sendGeneralError(ctx, http.StatusNotImplemented, message, status, "Implement the API code", "") +} diff --git a/src/api/getHealth.go b/src/api/getHealth.go new file mode 100644 index 0000000..b24a380 --- /dev/null +++ b/src/api/getHealth.go @@ -0,0 +1,98 @@ +package api + +import ( + "time" + "net/http" + + "github.com/labstack/echo/v4" + + "{{moduleName}}/api/gen" +) + + +// Service domain health +// (GET /health) +func (self *BianApiService) GetHealth(ctx echo.Context) error { + + self.Lock.Lock() + defer self.Lock.Unlock() + + description := "API for XXX" + serviceName := "" + + health := gen.HealthModel{ + Description: &description, + ServiceId: &serviceName, + Status: gen.PASS, + } + + health.Links = []gen.LinksModel{} + + hmc := self.newHealthModelChecks() + + asAt := time.Now() + + { + var items1 []gen.HealthcheckModel + status1 := "PASS" + cid1 := "local-file-store" + cty1 := "datastore" + + item1 := gen.HealthcheckModel{ + AsAt: &asAt, + Status: &status1, + ComponentId: &cid1, + ComponentType: &cty1, + } + items1 = append(items1,item1) + + status2 := "PASS" + cid2 := "process-server" + cty2 := "system" + + item2 := gen.HealthcheckModel{ + AsAt: &asAt, + Status: &status2, + ComponentId: &cid2, + ComponentType: &cty2, + } + items1 = append(items1,item2) + + hmc.Set("dependencies", items1) + } + + { + var items []gen.HealthcheckModel + + status1 := "PASS" + cid1 := "process-server" + cty1 := "system" + observedValue := float32(asAt.Sub(self.StartTime).Seconds()) + observedUnit := "s" + + item := gen.HealthcheckModel{ + AsAt: &asAt, + Status: &status1, + ComponentId: &cid1, + ComponentType: &cty1, + ObservedValue: &observedValue, + ObservedUnit: &observedUnit, + } + items = append(items,item) + + hmc.Set("uptime", items) + } + + health.Checks = hmc + + return ctx.JSON(http.StatusOK, health) +} + + +func (self *BianApiService) newHealthModelChecks() gen.HealthModel_Checks { + + obj := gen.HealthModel_Checks{} + return obj +} + + diff --git a/src/api/serverapi.go b/src/api/serverapi.go new file mode 100644 index 0000000..86f57f5 --- /dev/null +++ b/src/api/serverapi.go @@ -0,0 +1,175 @@ +package api + +import ( + "log" + "sync" + "time" + "net/http" + + "github.com/labstack/echo/v4" + + "{{moduleName}}/api/gen" + "{{moduleName}}/api/authn51" +) + + +type BianApiService struct { + Lock sync.Mutex + StartTime time.Time + Configuration SoftwareConfiguration + Authn *authn51.Authn51 + DataFolder string + StatusCode int +} + +func NewMicroService(serverConfig SoftwareConfiguration) (*BianApiService, error) { + + var microservice = BianApiService{ + StartTime: time.Now(), + DataFolder: serverConfig.Folders.Data, + StatusCode: 200, + } + + microservice.Configuration = serverConfig + aut, err := authn51.NewAuthn51(serverConfig.GetConfigurationFile()) + if (err != nil) { + log.Printf("Error creating Authn51: %v", err) + } + microservice.Authn = aut + + if (microservice.Configuration.Support.Level > 2) { + log.Printf("Base path: %s", microservice.Configuration.Server.BasePath) + } + + // Prime the security + if (!microservice.IsProductionEnvironment()) { + token, _ := microservice.Authn.CreateToken(microservice.Authn.Bearer.DefaultUser) + log.Printf("token: %s", token) + } + + return µservice, nil +} + + +func sendGeneralErrorBrief(ctx echo.Context, code int, message string) error { + return sendGeneralError(ctx, code, message, "", "", "") +} + + +func sendGeneralError(ctx echo.Context, code int, message string, status string, resolution string, technical string) error { + + generalError := gen.ErrorModel{ + Message: &message, + Status: &status, + Resolution: &resolution, + Technical: &technical, + } + + return ctx.JSON(code, generalError) +} + +func setLinkBrief(href string, rel string) gen.LinksModel { + + mt := "application/json" + op := gen.GET + + return setLink(href, rel, mt, op, "") +} + +func setLink(href string, rel string, mediaType string, operation gen.LinksModelOperation, caption string) gen.LinksModel { + + linkHref := href + linkRel := rel + + mt := mediaType + if (mt == "") { + mt = "application/json" + } + op := operation + if (op == "") { + op = gen.GET + } + cap := caption + + var link = gen.LinksModel { + Href: linkHref, + MediaType: &mt, + Rel: linkRel, + Operation: &op, + Caption: &cap, + } + + return link +} + + +func (microservice *BianApiService) IsProductionEnvironment() bool { + + if (microservice.Configuration.Environment == "development" || microservice.Configuration.Environment == "testing") { + return false + } else { + return true + } + +} + +func (self *BianApiService) RegisterHandlers(e *echo.Echo) { + + gen.RegisterHandlersWithBaseURL(e, self, self.Configuration.Server.BasePath) + + if (!self.IsProductionEnvironment()) { + if (self.Configuration.Support.Level > 2) { + log.Print("Enabling non production routes for debugging") + } + e.Static((self.Configuration.Server.BasePath+ "/"), self.Configuration.Folders.Static) + e.GET((self.Configuration.Server.BasePath+ "/authn/new-token"), self.NewToken) + e.GET((self.Configuration.Server.BasePath+ "/authn/refresh-token"), self.RefreshToken) + } + + if (self.Configuration.Support.Level > 2) { + log.Print("Echo routes are:") + for _, route := range e.Routes() { + log.Printf(" Route: %s %s %s", route.Method, route.Path, route.Name) + } + } + + +} + +func (self *BianApiService) BrowseSkipper(path string) bool { + if (!self.IsProductionEnvironment()) { + return browseSkipper(self.Configuration.Server.BasePath, path) + } else { + return false + } +} + +func (self *BianApiService) NewToken(ctx echo.Context) error { + + if (self.IsProductionEnvironment()) { + return ctx.JSON(http.StatusForbidden, "") + } else { + token, _ := self.Authn.CreateToken(self.Authn.Bearer.DefaultUser) + log.Printf("New token: %s", token) + + return ctx.JSON(http.StatusOK, token) + } +} + +func (self *BianApiService) RefreshToken(ctx echo.Context) error { + + if (self.IsProductionEnvironment()) { + return ctx.JSON(http.StatusForbidden, "") + } else { + ad, err := self.Authn.ExtractTokenMetadata(ctx.Request()) + if (err != nil) { + return ctx.JSON(http.StatusBadRequest, "") + } else { + token, _ := self.Authn.CreateToken(ad.UserId) + log.Printf("Refresh token: %s \n %s", ad.UserId, token ) + + return ctx.JSON(http.StatusOK, token) + } + } +} + diff --git a/src/api/utility.go b/src/api/utility.go new file mode 100644 index 0000000..4845d4a --- /dev/null +++ b/src/api/utility.go @@ -0,0 +1,30 @@ +package api + + +import ( + "strings" +) + + + + +func browseSkipper(baseUrl string, path string) bool { + + if strings.HasSuffix(path, ".html") || strings.HasSuffix(path, ".json") { + return true + } + if strings.HasSuffix(path, ".ico") { + return true + } + if path == (baseUrl + "/") { + return true + } + if strings.HasPrefix(path, (baseUrl+"/authn/")) { + return true + } + if strings.HasPrefix(path, (baseUrl+"/assets/")) { + return true + } + + return false +} diff --git a/src/build/server.cfg.yaml b/src/build/server.cfg.yaml new file mode 100644 index 0000000..bf652aa --- /dev/null +++ b/src/build/server.cfg.yaml @@ -0,0 +1,5 @@ +package: gen +generate: + echo-server: true + embedded-spec: true +output: api/gen/{{appShort}}-server.gen.go diff --git a/src/build/types.cfg.yaml b/src/build/types.cfg.yaml new file mode 100644 index 0000000..703fe68 --- /dev/null +++ b/src/build/types.cfg.yaml @@ -0,0 +1,4 @@ +package: gen +generate: + models: true +output: api/gen/{{appShort}}-type.gen.go diff --git a/src/servermain.go b/src/servermain.go new file mode 100644 index 0000000..14e4e59 --- /dev/null +++ b/src/servermain.go @@ -0,0 +1,132 @@ +package main + +import ( + "fmt" + "os" + "time" + "os/signal" + "log" + "net/http" + "context" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/deepmap/oapi-codegen/pkg/middleware" + "github.com/labstack/echo/v4" + echomiddleware "github.com/labstack/echo/v4/middleware" + + "{{moduleName}}/api" + "{{moduleName}}/api/gen" + "{{moduleName}}/api/errorh" + +) + + +func Run(e *echo.Echo, addressPort string, timeOut time.Duration, msgLevel int) { + + var runChan = make(chan os.Signal, 1) + + // Set up a context to allow for graceful server shutdowns in the event + // of an OS interrupt (defers the cancel just in case) + ctx, cancel := context.WithTimeout( + context.Background(), + timeOut, + ) + defer cancel() + + // Handle ctrl+c/ctrl+x interrupt + signal.Notify(runChan, os.Interrupt) + + if (msgLevel > 0) { + log.Printf("Server is starting on %s\n", addressPort) + } + + go func() { + if err := e.Start(addressPort); err != nil { + if err == http.ErrServerClosed { + // Normal interrupt operation, ignore + } else { + log.Fatalf("Server failed to start due to err: %v", err) + } + } + }() + + interrupt := <-runChan + + if (msgLevel > 0) { + log.Printf("Server is shutting down due to %+v\n", interrupt) + } + if err := e.Server.Shutdown(ctx); err != nil { + if ("interrupt" != fmt.Sprintf("%v", interrupt)) { + log.Printf("Server was unable to gracefully shutdown due to err: '%+v'", err) + } + } + +} + + +func main() { + + configPath := "./config/{{appShort}}.yaml" + softwareConfig, err := api.ParseFlags(configPath) + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading configuration,flags\n") + fmt.Fprintf(os.Stderr, "Error detail: %v\n", err) + os.Exit(1) + } + if (softwareConfig.Support.Level > 2) { + log.Printf("Message verbosity level: %d", softwareConfig.Support.Level) + } + + swagger, err := gen.GetSwagger() + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading Open API spec\n: %s", err) + os.Exit(1) + } + + // Create an instance of our handler which satisfies the generated interface + var microService, _ = api.NewMicroService(*softwareConfig) + + // Adjust servers to fit configuration + swagger.Servers = nil + server := openapi3.Server{ + URL: microService.Configuration.Server.BasePath, + } + swagger.AddServer(&server) + + e := echo.New() + + // Log all requests + e.Use(echomiddleware.Logger()) + // Custom error handling + e.HTTPErrorHandler = errorh.NewHttpErrorHandler().Handler + + var options middleware.Options + if (!microService.IsProductionEnvironment()) { + options.Skipper = func(c echo.Context) bool { + return microService.BrowseSkipper(c.Request().URL.Path) + } + } + options.Options.AuthenticationFunc = microService.Authn.AuthnFunc + + // Use our validation middleware to check all requests against the + // OpenAPI schema. + e.Use(middleware.OapiRequestValidatorWithOptions(swagger, &options)) + e.Use(echomiddleware.GzipWithConfig(echomiddleware.GzipConfig{ + Level: 3, + })) + + microService.RegisterHandlers(e) + + if (microService.Configuration.Support.Level < 1) { + e.HideBanner = true + } + + e.Server.ReadTimeout = microService.Configuration.Server.Timeout.Read * time.Second + e.Server.WriteTimeout = microService.Configuration.Server.Timeout.Write * time.Second + e.Server.IdleTimeout = microService.Configuration.Server.Timeout.Idle * time.Second + + addressPort := fmt.Sprintf("%s:%d", microService.Configuration.Server.Host, microService.Configuration.Server.Port) + timeOut := microService.Configuration.Server.Timeout.Server * time.Second + // And we serve HTTP until the world ends. + Run(e, addressPort, timeOut, microService.Configuration.Support.Level) +}