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:
parent
83a8694061
commit
6ce7747dd5
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
*DS_Store*
|
*DS_Store*
|
||||||
|
data/*
|
@ -22,6 +22,58 @@ func main() {
|
|||||||
clientv1.Initialize()
|
clientv1.Initialize()
|
||||||
clientv1.GetConfiguration(options)
|
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
|
return 0
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ -30,7 +82,8 @@ func main() {
|
|||||||
WithOption(
|
WithOption(
|
||||||
cli.NewOption("server", "giredore server address"),
|
cli.NewOption("server", "giredore server address"),
|
||||||
).
|
).
|
||||||
WithCommand(config)
|
WithCommand(config).
|
||||||
|
WithCommand(packages)
|
||||||
|
|
||||||
os.Exit(app.Run(os.Args, os.Stdout))
|
os.Exit(app.Run(os.Args, os.Stdout))
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,7 @@ func main() {
|
|||||||
go func() {
|
go func() {
|
||||||
<-signalHandler
|
<-signalHandler
|
||||||
httpserver.Shutdown()
|
httpserver.Shutdown()
|
||||||
|
configuration.Shutdown()
|
||||||
shutdownDone <- true
|
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
|
// stdlib
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
// local
|
||||||
|
"sources.dev.pztrn.name/pztrn/giredore/internal/configuration"
|
||||||
|
"sources.dev.pztrn.name/pztrn/giredore/internal/structs"
|
||||||
|
|
||||||
// other
|
// other
|
||||||
"github.com/labstack/echo"
|
"github.com/labstack/echo"
|
||||||
)
|
)
|
||||||
@ -12,3 +16,17 @@ import (
|
|||||||
func configurationGET(ec echo.Context) error {
|
func configurationGET(ec echo.Context) error {
|
||||||
return ec.JSON(http.StatusOK, map[string]string{"result": "success"})
|
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 = logger.Logger.With().Str("type", "domain").Str("package", "server").Int("version", 1).Logger()
|
||||||
log.Info().Msg("Initializing...")
|
log.Info().Msg("Initializing...")
|
||||||
|
|
||||||
|
// Configuration-related.
|
||||||
httpserver.Srv.GET("/_api/configuration", configurationGET)
|
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
|
log zerolog.Logger
|
||||||
loggerInitialized bool
|
loggerInitialized bool
|
||||||
|
|
||||||
Cfg *config
|
envCfg *envConfig
|
||||||
|
Cfg *fileConfig
|
||||||
)
|
)
|
||||||
|
|
||||||
func Initialize() {
|
func Initialize() {
|
||||||
@ -20,6 +21,16 @@ func Initialize() {
|
|||||||
loggerInitialized = true
|
loggerInitialized = true
|
||||||
log.Info().Msg("Initializing...")
|
log.Info().Msg("Initializing...")
|
||||||
|
|
||||||
Cfg = &config{}
|
envCfg = &envConfig{}
|
||||||
|
envCfg.Initialize()
|
||||||
|
|
||||||
|
Cfg = &fileConfig{}
|
||||||
Cfg.Initialize()
|
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 = echo.New()
|
||||||
Srv.Use(middleware.Recover())
|
Srv.Use(middleware.Recover())
|
||||||
Srv.Use(requestLogger())
|
Srv.Use(requestLogger())
|
||||||
|
Srv.Use(checkAllowedIPs())
|
||||||
Srv.DisableHTTP2 = true
|
Srv.DisableHTTP2 = true
|
||||||
Srv.HideBanner = true
|
Srv.HideBanner = true
|
||||||
Srv.HidePort = true
|
Srv.HidePort = true
|
||||||
|
Srv.Binder = echo.Binder(&StrictJSONBinder{})
|
||||||
|
|
||||||
Srv.GET("/_internal/waitForOnline", waitForHTTPServerToBeUpHandler)
|
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 (
|
import (
|
||||||
// stdlib
|
// stdlib
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
@ -21,14 +23,22 @@ func Initialize() {
|
|||||||
log.Info().Msg("Initializing...")
|
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...")
|
log.Debug().Str("method", method).Str("URL", url).Msg("Trying to execute HTTP request...")
|
||||||
|
|
||||||
httpClient := getHTTPClient()
|
httpClient := getHTTPClient()
|
||||||
|
|
||||||
|
var dataToSend []byte
|
||||||
|
if data != nil {
|
||||||
|
dataToSend, _ = json.Marshal(data)
|
||||||
|
}
|
||||||
|
|
||||||
// Compose HTTP request.
|
// Compose HTTP request.
|
||||||
// ToDo: POST/PUT/other methods that require body.
|
httpReq, err := http.NewRequest(method, url, bytes.NewReader(dataToSend))
|
||||||
httpReq, err := http.NewRequest(method, url, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
func Get(url string) ([]byte, error) {
|
||||||
return execRequest("GET", url, nil)
|
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
|
Loading…
Reference in New Issue
Block a user