From 6ce7747dd588a55d3f63b4940cb5c0d7f895dc27 Mon Sep 17 00:00:00 2001 From: "Stanislav N. aka pztrn" Date: Mon, 7 Oct 2019 18:21:26 +0500 Subject: [PATCH] Working with packages and allowed IPs. giredorectl now able to interact with giredored about: * Setting package data. There is no such thing as "create" or "update", just set. * Deleting package data. * Setting allowed IP addresses. This is the only authorization method ATM, more may come in future. --- .gitignore | 1 + README.md | 6 +- cmd/giredorectl/main.go | 55 ++++++- cmd/giredored/main.go | 1 + domains/client/v1/config.go | 38 +++++ domains/client/v1/config_get.go | 18 --- domains/client/v1/packages.go | 66 ++++++++ domains/server/v1/configapi.go | 18 +++ domains/server/v1/exported.go | 10 ++ domains/server/v1/goimports.go | 13 ++ domains/server/v1/packagesapi.go | 72 +++++++++ internal/configuration/config.go | 22 --- internal/configuration/envconfig.go | 33 ++++ internal/configuration/exported.go | 15 +- internal/configuration/fileconfig.go | 204 +++++++++++++++++++++++++ internal/httpserver/checkallowedips.go | 65 ++++++++ internal/httpserver/exported.go | 2 + internal/httpserver/strictjson.go | 38 +++++ internal/requester/exported.go | 24 ++- internal/structs/allowedips.go | 5 + internal/structs/error.go | 12 ++ internal/structs/package.go | 37 +++++ internal/structs/reply.go | 9 ++ internal/structs/status.go | 8 + 24 files changed, 725 insertions(+), 47 deletions(-) create mode 100644 domains/client/v1/config.go delete mode 100644 domains/client/v1/config_get.go create mode 100644 domains/client/v1/packages.go create mode 100644 domains/server/v1/goimports.go create mode 100644 domains/server/v1/packagesapi.go delete mode 100644 internal/configuration/config.go create mode 100644 internal/configuration/envconfig.go create mode 100644 internal/configuration/fileconfig.go create mode 100644 internal/httpserver/checkallowedips.go create mode 100644 internal/httpserver/strictjson.go create mode 100644 internal/structs/allowedips.go create mode 100644 internal/structs/error.go create mode 100644 internal/structs/package.go create mode 100644 internal/structs/reply.go create mode 100644 internal/structs/status.go diff --git a/.gitignore b/.gitignore index 16c1a1f..be0a5f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ *DS_Store* +data/* \ No newline at end of file diff --git a/README.md b/README.md index 0f15ebc..aacac8f 100644 --- a/README.md +++ b/README.md @@ -17,4 +17,8 @@ ## Usage -*TBW* \ No newline at end of file +*TBW* + +## Developing + +*TBW* diff --git a/cmd/giredorectl/main.go b/cmd/giredorectl/main.go index b945333..35217af 100644 --- a/cmd/giredorectl/main.go +++ b/cmd/giredorectl/main.go @@ -22,6 +22,58 @@ func main() { clientv1.Initialize() clientv1.GetConfiguration(options) + return 0 + }), + ). + WithCommand( + cli.NewCommand("set", "sets configuration value"). + WithCommand( + cli.NewCommand("allowedips", "sets list of allowed IPs for interacting with configuration API"). + WithArg(cli.NewArg("allowed_ips_list", "list of allowed IP addresses delimited by comma. Subnets are also fine.")). + WithAction(func(args []string, options map[string]string) int { + logger.Initialize() + clientv1.Initialize() + clientv1.SetAllowedIPs(args, options) + return 0 + }), + ), + ) + + packages := cli.NewCommand("packages", "work with packages giredore will serve"). + WithShortcut("pkg"). + WithCommand( + cli.NewCommand("get", "gets and prints out list of packages that is served by giredore"). + WithArg(cli.NewArg("pkgnames", "one or more packages to get info about, delimited with comma, e.g. '/path/pkg1,/path/pkg2'. Say 'all' here to get info about all known packages.")). + WithAction(func(args []string, options map[string]string) int { + logger.Initialize() + clientv1.Initialize() + clientv1.GetPackages(args, options) + + return 0 + }), + ). + WithCommand( + cli.NewCommand("set", "creates or updates package data"). + WithArg(cli.NewArg("description", "optional package description that will be shown on package serving page")). + WithArg(cli.NewArg("origpath", "original path of package without domain, e.g. '/group/pkg' instead of 'github.com/group/pkg'")). + WithArg(cli.NewArg("realpath", "real path for package sources, e.g. 'github.com/group/pkg.git'")). + WithArg(cli.NewArg("vcs", "VCS used for package sources getting. See https://github.com/golang/tools/blob/master/go/vcs/vcs.go for list of supported VCS.")). + WithAction(func(args []string, options map[string]string) int { + logger.Initialize() + clientv1.Initialize() + clientv1.SetPackage(args, options) + + return 0 + }), + ). + WithCommand( + cli.NewCommand("delete", "deletes package data"). + WithArg(cli.NewArg("origpath", "original path of package without domain, e.g. '/group/pkg' instead of 'github.com/group/pkg'")). + WithAction(func(args []string, options map[string]string) int { + logger.Initialize() + clientv1.Initialize() + clientv1.DeletePackage(args, options) + return 0 }), ) @@ -30,7 +82,8 @@ func main() { WithOption( cli.NewOption("server", "giredore server address"), ). - WithCommand(config) + WithCommand(config). + WithCommand(packages) os.Exit(app.Run(os.Args, os.Stdout)) } diff --git a/cmd/giredored/main.go b/cmd/giredored/main.go index ea79f12..306401f 100644 --- a/cmd/giredored/main.go +++ b/cmd/giredored/main.go @@ -33,6 +33,7 @@ func main() { go func() { <-signalHandler httpserver.Shutdown() + configuration.Shutdown() shutdownDone <- true }() diff --git a/domains/client/v1/config.go b/domains/client/v1/config.go new file mode 100644 index 0000000..71721ea --- /dev/null +++ b/domains/client/v1/config.go @@ -0,0 +1,38 @@ +package clientv1 + +import ( + // stdlib + "strings" + + // local + "sources.dev.pztrn.name/pztrn/giredore/internal/requester" + "sources.dev.pztrn.name/pztrn/giredore/internal/structs" +) + +func GetConfiguration(options map[string]string) { + url := "http://" + options["server"] + "/_api/configuration" + log.Info().Msg("Getting configuration from giredore server...") + + data, err := requester.Get(url) + if err != nil { + log.Fatal().Err(err).Msg("Failed to get configuration from giredore server!") + } + + log.Debug().Msg("Got data: " + string(data)) +} + +func SetAllowedIPs(args []string, options map[string]string) { + url := "http://" + options["server"] + "/_api/configuration/allowedips" + log.Info().Str("allowed IPs", args[0]).Msg("Setting allowed IPs for API interaction...") + + req := &structs.AllowedIPsSetRequest{ + AllowedIPs: strings.Split(args[0], ","), + } + + data, err := requester.Post(url, req) + if err != nil { + log.Fatal().Err(err).Msg("Failed to set allowed IPs in giredore server configuration!") + } + + log.Debug().Msg("Got data: " + string(data)) +} diff --git a/domains/client/v1/config_get.go b/domains/client/v1/config_get.go deleted file mode 100644 index 7936fab..0000000 --- a/domains/client/v1/config_get.go +++ /dev/null @@ -1,18 +0,0 @@ -package clientv1 - -import ( - // local - "sources.dev.pztrn.name/pztrn/giredore/internal/requester" -) - -func GetConfiguration(options map[string]string) { - url := "http://" + options["server"] + "/_api/configuration" - log.Info().Msg("Getting configuration from giredore server...") - - data, err := requester.Get(url) - if err != nil { - log.Fatal().Err(err).Msg("Failed to get configuration from giredore server!") - } - - log.Debug().Msg("Got data: " + string(data)) -} diff --git a/domains/client/v1/packages.go b/domains/client/v1/packages.go new file mode 100644 index 0000000..263fcd4 --- /dev/null +++ b/domains/client/v1/packages.go @@ -0,0 +1,66 @@ +package clientv1 + +import ( + // stdlib + "strings" + + // local + "sources.dev.pztrn.name/pztrn/giredore/internal/requester" + "sources.dev.pztrn.name/pztrn/giredore/internal/structs" +) + +func DeletePackage(args []string, options map[string]string) { + req := &structs.PackageDeleteRequest{ + OriginalPath: args[0], + } + + log.Info().Str("original path", req.OriginalPath).Msg("Sending package deletion request to giredored...") + + url := "http://" + options["server"] + "/_api/packages" + data, err := requester.Delete(url, req) + if err != nil { + log.Fatal().Err(err).Msg("Failed to send package deletion request to giredored") + } + + log.Debug().Msg("Got data: " + string(data)) +} + +func GetPackages(args []string, options map[string]string) { + pkgs := strings.Split(args[0], ",") + + req := &structs.PackageGetRequest{} + if pkgs[0] == "all" { + req.All = true + } else { + req.PackageNames = pkgs + } + + url := "http://" + options["server"] + "/_api/packages" + log.Info().Msg("Getting packages data from giredore server...") + + data, err := requester.Post(url, req) + if err != nil { + log.Fatal().Err(err).Msg("Failed to get packages data from giredore server!") + } + + log.Debug().Msg("Got data: " + string(data)) +} + +func SetPackage(args []string, options map[string]string) { + pkg := &structs.Package{ + Description: args[0], + OriginalPath: args[1], + RealPath: args[2], + VCS: args[3], + } + + log.Info().Str("description", pkg.Description).Str("original path", pkg.OriginalPath).Str("real path", pkg.RealPath).Str("VCS", pkg.VCS).Msg("Sending set/update request to giredored...") + + url := "http://" + options["server"] + "/_api/packages" + data, err := requester.Put(url, pkg) + if err != nil { + log.Fatal().Err(err).Msg("Failed to send package update/set request to giredored") + } + + log.Debug().Msg("Got data: " + string(data)) +} diff --git a/domains/server/v1/configapi.go b/domains/server/v1/configapi.go index d7d0798..ee2529a 100644 --- a/domains/server/v1/configapi.go +++ b/domains/server/v1/configapi.go @@ -4,6 +4,10 @@ import ( // stdlib "net/http" + // local + "sources.dev.pztrn.name/pztrn/giredore/internal/configuration" + "sources.dev.pztrn.name/pztrn/giredore/internal/structs" + // other "github.com/labstack/echo" ) @@ -12,3 +16,17 @@ import ( func configurationGET(ec echo.Context) error { return ec.JSON(http.StatusOK, map[string]string{"result": "success"}) } + +func configurationAllowedIPsSET(ec echo.Context) error { + req := &structs.AllowedIPsSetRequest{} + if err := ec.Bind(req); err != nil { + log.Error().Err(err).Msg("Failed to parse allowed IPs set request") + return ec.JSON(http.StatusBadRequest, &structs.Reply{Status: structs.StatusFailure, Errors: []structs.Error{structs.ErrParsingAllowedIPsSetRequest}}) + } + + log.Debug().Msgf("Got set allowed IPs request: %+v", req) + + configuration.Cfg.SetAllowedIPs(req.AllowedIPs) + + return ec.JSON(http.StatusOK, &structs.Reply{Status: structs.StatusSuccess}) +} diff --git a/domains/server/v1/exported.go b/domains/server/v1/exported.go index 9eea932..38a2f64 100644 --- a/domains/server/v1/exported.go +++ b/domains/server/v1/exported.go @@ -17,5 +17,15 @@ func Initialize() { log = logger.Logger.With().Str("type", "domain").Str("package", "server").Int("version", 1).Logger() log.Info().Msg("Initializing...") + // Configuration-related. httpserver.Srv.GET("/_api/configuration", configurationGET) + httpserver.Srv.POST("/_api/configuration/allowedips", configurationAllowedIPsSET) + + // Packages-related. + httpserver.Srv.POST("/_api/packages", packagesGET) + httpserver.Srv.PUT("/_api/packages", packagesSET) + httpserver.Srv.DELETE("/_api/packages", packagesDELETE) + + // goimports serving. + httpserver.Srv.GET("/*", throwGoImports) } diff --git a/domains/server/v1/goimports.go b/domains/server/v1/goimports.go new file mode 100644 index 0000000..c426489 --- /dev/null +++ b/domains/server/v1/goimports.go @@ -0,0 +1,13 @@ +package serverv1 + +import ( + // stdlib + "net/http" + + // other + "github.com/labstack/echo" +) + +func throwGoImports(ec echo.Context) error { + return ec.String(http.StatusOK, "All OK here") +} diff --git a/domains/server/v1/packagesapi.go b/domains/server/v1/packagesapi.go new file mode 100644 index 0000000..eaf8273 --- /dev/null +++ b/domains/server/v1/packagesapi.go @@ -0,0 +1,72 @@ +package serverv1 + +import ( + // stdlib + "net/http" + + // local + "sources.dev.pztrn.name/pztrn/giredore/internal/configuration" + "sources.dev.pztrn.name/pztrn/giredore/internal/structs" + + // other + "github.com/labstack/echo" +) + +// This function responsible for getting packages configuration. +func packagesGET(ec echo.Context) error { + req := &structs.PackageGetRequest{} + if err := ec.Bind(req); err != nil { + log.Error().Err(err).Msg("Failed to parse package get request") + return ec.JSON(http.StatusBadRequest, &structs.Reply{Status: structs.StatusFailure, Errors: []structs.Error{structs.ErrParsingPackagesGetRequest}}) + } + + log.Info().Msgf("Received package(s) info get request: %+v", req) + + var pkgs map[string]*structs.Package + var errors []structs.Error + if req.All { + pkgs = configuration.Cfg.GetAllPackagesInfo() + } else { + pkgs, errors = configuration.Cfg.GetPackagesInfo(req.PackageNames) + } + + if errors != nil && len(errors) > 0 { + return ec.JSON(http.StatusBadRequest, &structs.Reply{Status: structs.StatusFailure, Errors: errors, Data: pkgs}) + } + + return ec.JSON(http.StatusOK, &structs.Reply{Status: structs.StatusSuccess, Data: pkgs}) +} + +// This function responsible for deleting package. +func packagesDELETE(ec echo.Context) error { + req := &structs.PackageDeleteRequest{} + if err := ec.Bind(req); err != nil { + log.Error().Err(err).Msg("Failed to parse package delete request") + return ec.JSON(http.StatusBadRequest, &structs.Reply{Status: structs.StatusFailure, Errors: []structs.Error{structs.ErrParsingDeleteRequest}}) + } + + log.Info().Msgf("Received package delete request: %+v", req) + + errs := configuration.Cfg.DeletePackage(req) + + if len(errs) > 0 { + return ec.JSON(http.StatusBadRequest, &structs.Reply{Status: structs.StatusFailure, Errors: errs}) + } + + return ec.JSON(http.StatusOK, &structs.Reply{Status: structs.StatusSuccess}) +} + +// This function responsible for setting or updating packages. +func packagesSET(ec echo.Context) error { + req := &structs.Package{} + if err := ec.Bind(req); err != nil { + log.Error().Err(err).Msg("Failed to parse package data") + return ec.JSON(http.StatusBadRequest, nil) + } + + log.Info().Msgf("Received package set/update request: %+v", req) + + configuration.Cfg.AddOrUpdatePackage(req) + + return ec.JSON(http.StatusOK, &structs.Reply{Status: structs.StatusSuccess}) +} diff --git a/internal/configuration/config.go b/internal/configuration/config.go deleted file mode 100644 index 89f3ddb..0000000 --- a/internal/configuration/config.go +++ /dev/null @@ -1,22 +0,0 @@ -package configuration - -import ( - // other - "github.com/vrischmann/envconfig" -) - -type config struct { - HTTP struct { - Listen string `envconfig:"default=127.0.0.1:62222"` - WaitForSeconds int `envconfig:"default=10"` - } -} - -// Initialize loads configuration into memory. -func (cf *config) Initialize() { - log.Info().Msg("Loading configuration...") - - _ = envconfig.Init(cf) - - log.Info().Msgf("Configuration parsed: %+v", cf) -} diff --git a/internal/configuration/envconfig.go b/internal/configuration/envconfig.go new file mode 100644 index 0000000..be928f8 --- /dev/null +++ b/internal/configuration/envconfig.go @@ -0,0 +1,33 @@ +package configuration + +import ( + // other + "github.com/vrischmann/envconfig" +) + +// This structure represents configuration that will be parsed via +// environment variables. This configuration has higher priority +// than configuration loaded from file. +type envConfig struct { + // DataDir is a directory where giredore will store it's data + // like dynamic configuration file. + DataDir string `envconfig:"default=/var/lib/giredore"` + // HTTP describes HTTP server configuration. + HTTP struct { + // Listen is an address on which HTTP server will listen. + Listen string `envconfig:"default=127.0.0.1:62222"` + // WaitForSeconds is a timeout during which we will wait for + // HTTP server be up. If timeout will pass and HTTP server won't + // start processing requests - giredore will exit. + WaitForSeconds int `envconfig:"default=10"` + } +} + +// Initialize parses environment variables into structure. +func (cf *envConfig) Initialize() { + log.Info().Msg("Loading configuration...") + + _ = envconfig.Init(cf) + + log.Info().Msgf("Environment parsed: %+v", cf) +} diff --git a/internal/configuration/exported.go b/internal/configuration/exported.go index 9118ecd..b743e40 100644 --- a/internal/configuration/exported.go +++ b/internal/configuration/exported.go @@ -12,7 +12,8 @@ var ( log zerolog.Logger loggerInitialized bool - Cfg *config + envCfg *envConfig + Cfg *fileConfig ) func Initialize() { @@ -20,6 +21,16 @@ func Initialize() { loggerInitialized = true log.Info().Msg("Initializing...") - Cfg = &config{} + envCfg = &envConfig{} + envCfg.Initialize() + + Cfg = &fileConfig{} Cfg.Initialize() + + Cfg.HTTP.Listen = envCfg.HTTP.Listen + Cfg.HTTP.WaitForSeconds = envCfg.HTTP.WaitForSeconds +} + +func Shutdown() { + Cfg.Save() } diff --git a/internal/configuration/fileconfig.go b/internal/configuration/fileconfig.go new file mode 100644 index 0000000..ecb5d1a --- /dev/null +++ b/internal/configuration/fileconfig.go @@ -0,0 +1,204 @@ +package configuration + +import ( + // stdlib + "encoding/json" + "io/ioutil" + "os" + "path/filepath" + "strings" + "sync" + + // local + "sources.dev.pztrn.name/pztrn/giredore/internal/structs" +) + +// This structure represents configuration that will be parsed via file. +// Despite on exporting fields there are setters and getters defined because +// data from configuration file will be parsed in exported fields and they +// may be accesses concurrently. In other words DO NOT USE EXPORTED FIELDS +// DIRECTLY! +type fileConfig struct { + // HTTP describes HTTP server configuration. + HTTP struct { + // AllowedIPs is a list of IPs that allowed to access API. + // There might be other authentication implemented in future. + AllowedIPs []string + allowedipsmutex sync.RWMutex + // Listen is an address on which HTTP server will listen. + Listen string `envconfig:"default=127.0.0.1:62222"` + // WaitForSeconds is a timeout during which we will wait for + // HTTP server be up. If timeout will pass and HTTP server won't + // start processing requests - giredore will exit. + WaitForSeconds int `envconfig:"default=10"` + } + // Packages describes packages mapping. + Packages map[string]*structs.Package + packagesMutex sync.RWMutex +} + +func (fc *fileConfig) AddOrUpdatePackage(pkg *structs.Package) { + fc.packagesMutex.Lock() + fc.Packages[pkg.OriginalPath] = pkg + fc.packagesMutex.Unlock() +} + +func (fc *fileConfig) DeletePackage(req *structs.PackageDeleteRequest) []structs.Error { + var errors []structs.Error + + fc.packagesMutex.Lock() + defer fc.packagesMutex.Unlock() + _, found := fc.Packages[req.OriginalPath] + + if !found { + errors = append(errors, structs.ErrPackageWasntDefined) + return errors + } + + delete(fc.Packages, req.OriginalPath) + + return errors +} + +func (fc *fileConfig) GetAllowedIPs() []string { + var allowedIPs []string + fc.HTTP.allowedipsmutex.RLock() + allowedIPs = append(allowedIPs, fc.HTTP.AllowedIPs...) + fc.HTTP.allowedipsmutex.RUnlock() + + return allowedIPs +} + +func (fc *fileConfig) GetAllPackagesInfo() map[string]*structs.Package { + pkgs := make(map[string]*structs.Package) + + fc.packagesMutex.Lock() + for name, pkg := range fc.Packages { + pkgs[name] = pkg + } + fc.packagesMutex.Unlock() + + return pkgs +} + +func (fc *fileConfig) GetPackagesInfo(packages []string) (map[string]*structs.Package, []structs.Error) { + pkgs := make(map[string]*structs.Package) + var errors []structs.Error + + fc.packagesMutex.Lock() + for _, neededPkg := range packages { + pkgData, found := fc.Packages[neededPkg] + if !found { + errors = append(errors, structs.ErrPackageWasntDefined+structs.Error(" Package was: "+neededPkg)) + } else { + pkgs[neededPkg] = pkgData + } + } + fc.packagesMutex.Unlock() + + return pkgs, errors +} + +// Initialize parses file contents into structure. +func (fc *fileConfig) Initialize() { + configPath := filepath.Join(envCfg.DataDir, "config.json") + cfgLoadLog := log.With().Str("configuration path", configPath).Logger() + cfgLoadLog.Info().Msg("Loading configuration file...") + + configPath, err := fc.normalizePath(configPath) + if err != nil { + cfgLoadLog.Fatal().Err(err).Msg("Failed to normalize configuration file path.") + } + + // Check if file "config.json" specified in envConfig.DataDir field + // exists. + if _, err2 := os.Stat(configPath); os.IsNotExist(err2) { + cfgLoadLog.Error().Msg("Unable to load configuration from filesystem.") + return + } + + // Try to load file into memory. + fileData, err3 := ioutil.ReadFile(configPath) + if err3 != nil { + cfgLoadLog.Fatal().Err(err3).Msg("Failed to read configuration file data into memory.") + } + + // ...and parse it. + err4 := json.Unmarshal(fileData, fc) + if err4 != nil { + cfgLoadLog.Fatal().Err(err4).Msg("Failed to parse configuration file.") + } + + if fc.Packages == nil { + fc.Packages = make(map[string]*structs.Package) + } + + // Ensure that localhost (127.0.0.1) are defined in AllowedIPs. + var localhostIsAllowed bool + for _, ip := range fc.HTTP.AllowedIPs { + if strings.Contains(ip, "127.0.0.1") { + localhostIsAllowed = true + break + } + } + + if !localhostIsAllowed { + cfgLoadLog.Warn().Msg("Localhost (127.0.0.1) wasn't allowed to access configuration API, adding it to list of allowed IP addresses") + fc.HTTP.AllowedIPs = append(fc.HTTP.AllowedIPs, "127.0.0.1") + } else { + cfgLoadLog.Debug().Msg("Localhost (127.0.0.1) is allowed to access configuration API") + } + + cfgLoadLog.Debug().Msgf("Configuration parsed: %+v", fc) + cfgLoadLog.Info().Int("packages count", len(fc.Packages)).Msg("Packages list loaded") +} + +// Normalizes passed configuration file path. +func (fc *fileConfig) normalizePath(configPath string) (string, error) { + // Normalize configuration file path. + if strings.Contains(configPath, "~") { + homeDir, err := os.UserHomeDir() + if err != nil { + return "", err + } + configPath = strings.Replace(configPath, "~", homeDir, 1) + } + + absPath, err1 := filepath.Abs(configPath) + if err1 != nil { + return "", err1 + } + + return absPath, nil +} + +// Save saves configuration into file. +func (fc *fileConfig) Save() { + configPath := filepath.Join(envCfg.DataDir, "config.json") + cfgSaveLog := log.With().Str("configuration path", configPath).Logger() + cfgSaveLog.Info().Msg("Saving configuration file...") + + data, err := json.Marshal(fc) + if err != nil { + cfgSaveLog.Fatal().Err(err).Msg("Failed to encode data into JSON. Configuration file won't be saved!") + return + } + + configPath, err1 := fc.normalizePath(configPath) + if err1 != nil { + cfgSaveLog.Fatal().Err(err1).Msg("Failed to normalize configuration file path.") + } + + err2 := ioutil.WriteFile(configPath, data, os.ModePerm) + if err2 != nil { + cfgSaveLog.Fatal().Err(err2).Msg("Failed to write configuration file data to file.") + } + + cfgSaveLog.Info().Msg("Configuration file saved.") +} + +func (fc *fileConfig) SetAllowedIPs(allowedIPs []string) { + fc.HTTP.allowedipsmutex.Lock() + fc.HTTP.AllowedIPs = allowedIPs + fc.HTTP.allowedipsmutex.Unlock() +} diff --git a/internal/httpserver/checkallowedips.go b/internal/httpserver/checkallowedips.go new file mode 100644 index 0000000..5df83a8 --- /dev/null +++ b/internal/httpserver/checkallowedips.go @@ -0,0 +1,65 @@ +package httpserver + +import ( + // stdlib + "net" + "net/http" + "strings" + + // local + "sources.dev.pztrn.name/pztrn/giredore/internal/configuration" + "sources.dev.pztrn.name/pztrn/giredore/internal/structs" + + // other + "github.com/labstack/echo" +) + +func checkAllowedIPs() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ec echo.Context) error { + // Do nothing if request came not in "/_api" namespace. + if !strings.HasPrefix(ec.Request().RequestURI, "/_api") { + _ = next(ec) + return nil + } + + // Get IPs and subnets from configuration and parse them + // into comparable things. + // If IP address was specified without network mask - assume /32. + var subnets []*net.IPNet + allowedIPs := configuration.Cfg.GetAllowedIPs() + for _, ip := range allowedIPs { + ipToParse := ip + if !strings.Contains(ip, "/") { + ipToParse = ip + "/32" + } + + _, net, err := net.ParseCIDR(ipToParse) + if err != nil { + log.Error().Err(err).Str("subnet", ipToParse).Msg("Failed to parse CIDR. /_api/ endpoint won't be accessible, this should be fixed manually in configuration file!") + return ec.JSON(http.StatusInternalServerError, &structs.Reply{Status: structs.StatusFailure, Errors: []structs.Error{structs.ErrInvalidAllowedIPDefined}}) + } + + subnets = append(subnets, net) + } + + // Check if requester's IP address are within allowed IP + // subnets. + ipToCheck := net.ParseIP(ec.RealIP()) + var allowed bool + for _, subnet := range subnets { + if subnet.Contains(ipToCheck) { + allowed = true + break + } + } + + if allowed { + _ = next(ec) + return nil + } + + return ec.JSON(http.StatusBadRequest, &structs.Reply{Status: structs.StatusFailure, Errors: []structs.Error{structs.ErrIPAddressNotAllowed}}) + } + } +} diff --git a/internal/httpserver/exported.go b/internal/httpserver/exported.go index d21a4ac..ca1c2ae 100644 --- a/internal/httpserver/exported.go +++ b/internal/httpserver/exported.go @@ -32,9 +32,11 @@ func Initialize() { Srv = echo.New() Srv.Use(middleware.Recover()) Srv.Use(requestLogger()) + Srv.Use(checkAllowedIPs()) Srv.DisableHTTP2 = true Srv.HideBanner = true Srv.HidePort = true + Srv.Binder = echo.Binder(&StrictJSONBinder{}) Srv.GET("/_internal/waitForOnline", waitForHTTPServerToBeUpHandler) } diff --git a/internal/httpserver/strictjson.go b/internal/httpserver/strictjson.go new file mode 100644 index 0000000..b5c8eef --- /dev/null +++ b/internal/httpserver/strictjson.go @@ -0,0 +1,38 @@ +package httpserver + +import ( + // stdlib + "encoding/json" + "fmt" + "net/http" + + // other + "github.com/labstack/echo" +) + +// StrictJSONBinder implements Binder interface for Echo. It will parse +// JSON in strict mode throwing errors on schema mismatches. +type StrictJSONBinder struct{} + +// Bind parses JSON input. +func (sjb *StrictJSONBinder) Bind(i interface{}, c echo.Context) error { + req := c.Request() + if req.ContentLength == 0 { + return echo.NewHTTPError(http.StatusBadRequest, "Request body can't be empty") + } + + // Decode it. + decoder := json.NewDecoder(req.Body) + decoder.DisallowUnknownFields() + if err := decoder.Decode(i); err != nil { + if ute, ok := err.(*json.UnmarshalTypeError); ok { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unmarshal type error: expected=%v, got=%v, field=%v, offset=%v", ute.Type, ute.Value, ute.Field, ute.Offset)) + } else if se, ok := err.(*json.SyntaxError); ok { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Syntax error: offset=%v, error=%v", se.Offset, se.Error())) + } else { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + } + + return nil +} diff --git a/internal/requester/exported.go b/internal/requester/exported.go index 24bfe00..00bf6ff 100644 --- a/internal/requester/exported.go +++ b/internal/requester/exported.go @@ -2,6 +2,8 @@ package requester import ( // stdlib + "bytes" + "encoding/json" "io/ioutil" "net/http" @@ -21,14 +23,22 @@ func Initialize() { log.Info().Msg("Initializing...") } -func execRequest(method string, url string, data map[string]string) ([]byte, error) { +func Delete(url string, data interface{}) ([]byte, error) { + return execRequest("DELETE", url, data) +} + +func execRequest(method string, url string, data interface{}) ([]byte, error) { log.Debug().Str("method", method).Str("URL", url).Msg("Trying to execute HTTP request...") httpClient := getHTTPClient() + var dataToSend []byte + if data != nil { + dataToSend, _ = json.Marshal(data) + } + // Compose HTTP request. - // ToDo: POST/PUT/other methods that require body. - httpReq, err := http.NewRequest(method, url, nil) + httpReq, err := http.NewRequest(method, url, bytes.NewReader(dataToSend)) if err != nil { return nil, err } @@ -52,3 +62,11 @@ func execRequest(method string, url string, data map[string]string) ([]byte, err func Get(url string) ([]byte, error) { return execRequest("GET", url, nil) } + +func Post(url string, data interface{}) ([]byte, error) { + return execRequest("POST", url, data) +} + +func Put(url string, data interface{}) ([]byte, error) { + return execRequest("PUT", url, data) +} diff --git a/internal/structs/allowedips.go b/internal/structs/allowedips.go new file mode 100644 index 0000000..5d4552e --- /dev/null +++ b/internal/structs/allowedips.go @@ -0,0 +1,5 @@ +package structs + +type AllowedIPsSetRequest struct { + AllowedIPs []string +} diff --git a/internal/structs/error.go b/internal/structs/error.go new file mode 100644 index 0000000..56aff11 --- /dev/null +++ b/internal/structs/error.go @@ -0,0 +1,12 @@ +package structs + +const ( + ErrInvalidAllowedIPDefined Error = "Invalid allowed IP address defined." + ErrIPAddressNotAllowed Error = "IP address not allowed to access configuration API." + ErrPackageWasntDefined Error = "Passed package wasn't defined." + ErrParsingAllowedIPsSetRequest Error = "Error parsing allowed IPs request." + ErrParsingDeleteRequest Error = "Delete request parsing failed" + ErrParsingPackagesGetRequest Error = "Error parsing package(s) info get request" +) + +type Error string diff --git a/internal/structs/package.go b/internal/structs/package.go new file mode 100644 index 0000000..481373f --- /dev/null +++ b/internal/structs/package.go @@ -0,0 +1,37 @@ +package structs + +// Package defines structure for 'pkg set' request and for storing it's +// data in configuration. +type Package struct { + // Description is an additional and optional description that + // can be show on package's page. + Description string + // OriginalPath is a package original path without domain part. + // E.g. for package "go.example.tld/group/pkgname" you should + // put here "/group/pkgname". + OriginalPath string + // RealPath is a path where package will be found. It should + // contain VCS path, e.g. "https://github.com/user/project.git". + RealPath string + // VCS is a versioning control system used for package. Everything + // that is supported by "go get" is applicable. + VCS string +} + +// PackageDeleteRequest defines structure for package deleting request. +type PackageDeleteRequest struct { + // OriginalPath is a package original path without domain part. + // E.g. for package "go.example.tld/group/pkgname" you should + // put here "/group/pkgname". + OriginalPath string +} + +// PackageGetRequest defined structure for package information getting +// request. +type PackageGetRequest struct { + // Should all packages be returned? + All bool + // If All = false, then what package name (or names) to return? + // They should be delimited with comma in CLI. + PackageNames []string +} diff --git a/internal/structs/reply.go b/internal/structs/reply.go new file mode 100644 index 0000000..f0df9e0 --- /dev/null +++ b/internal/structs/reply.go @@ -0,0 +1,9 @@ +package structs + +// Reply defined reply data structure that giredored and giredorectl +// will use. +type Reply struct { + Status Status + Errors []Error + Data interface{} +} diff --git a/internal/structs/status.go b/internal/structs/status.go new file mode 100644 index 0000000..1af1fea --- /dev/null +++ b/internal/structs/status.go @@ -0,0 +1,8 @@ +package structs + +const ( + StatusFailure Status = "failure" + StatusSuccess Status = "success" +) + +type Status string