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.
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1 +1,2 @@ | ||||
| *DS_Store* | ||||
| data/* | ||||
| @@ -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)) | ||||
| } | ||||
|   | ||||
| @@ -33,6 +33,7 @@ func main() { | ||||
| 	go func() { | ||||
| 		<-signalHandler | ||||
| 		httpserver.Shutdown() | ||||
| 		configuration.Shutdown() | ||||
| 		shutdownDone <- true | ||||
| 	}() | ||||
|  | ||||
|   | ||||
							
								
								
									
										38
									
								
								domains/client/v1/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								domains/client/v1/config.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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)) | ||||
| } | ||||
| @@ -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)) | ||||
| } | ||||
							
								
								
									
										66
									
								
								domains/client/v1/packages.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								domains/client/v1/packages.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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)) | ||||
| } | ||||
| @@ -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}) | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
| } | ||||
|   | ||||
							
								
								
									
										13
									
								
								domains/server/v1/goimports.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								domains/server/v1/goimports.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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") | ||||
| } | ||||
							
								
								
									
										72
									
								
								domains/server/v1/packagesapi.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								domains/server/v1/packagesapi.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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}) | ||||
| } | ||||
| @@ -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) | ||||
| } | ||||
							
								
								
									
										33
									
								
								internal/configuration/envconfig.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								internal/configuration/envconfig.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| } | ||||
| @@ -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() | ||||
| } | ||||
|   | ||||
							
								
								
									
										204
									
								
								internal/configuration/fileconfig.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										204
									
								
								internal/configuration/fileconfig.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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() | ||||
| } | ||||
							
								
								
									
										65
									
								
								internal/httpserver/checkallowedips.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								internal/httpserver/checkallowedips.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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}}) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -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) | ||||
| } | ||||
|   | ||||
							
								
								
									
										38
									
								
								internal/httpserver/strictjson.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								internal/httpserver/strictjson.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
| @@ -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) | ||||
| } | ||||
|   | ||||
							
								
								
									
										5
									
								
								internal/structs/allowedips.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								internal/structs/allowedips.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| package structs | ||||
|  | ||||
| type AllowedIPsSetRequest struct { | ||||
| 	AllowedIPs []string | ||||
| } | ||||
							
								
								
									
										12
									
								
								internal/structs/error.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								internal/structs/error.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										37
									
								
								internal/structs/package.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								internal/structs/package.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
							
								
								
									
										9
									
								
								internal/structs/reply.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								internal/structs/reply.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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{} | ||||
| } | ||||
							
								
								
									
										8
									
								
								internal/structs/status.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								internal/structs/status.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| package structs | ||||
|  | ||||
| const ( | ||||
| 	StatusFailure Status = "failure" | ||||
| 	StatusSuccess Status = "success" | ||||
| ) | ||||
|  | ||||
| type Status string | ||||
		Reference in New Issue
	
	Block a user