forked from TrueCloudLab/frostfs-node
759421ebbf
Currently have static priority of what key is used irregardless of whether a flag was provided via CLI or in config. This makes it impossible to override some of the config settings. While we could try to check if the key is provided by CLI by binding CLI flag under to viper under a different name the same problem would occur for config/environment variables. Fixing all of this with current set of keys is too complicate. In this commit we revert changes from #610 and use a single flag for all types of keys. Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
537 lines
14 KiB
Go
537 lines
14 KiB
Go
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/nspcc-dev/neofs-node/pkg/util/keyer"
|
|
locodedb "github.com/nspcc-dev/neofs-node/pkg/util/locode/db"
|
|
airportsdb "github.com/nspcc-dev/neofs-node/pkg/util/locode/db/airports"
|
|
locodebolt "github.com/nspcc-dev/neofs-node/pkg/util/locode/db/boltdb"
|
|
continentsdb "github.com/nspcc-dev/neofs-node/pkg/util/locode/db/continents/geojson"
|
|
csvlocode "github.com/nspcc-dev/neofs-node/pkg/util/locode/table/csv"
|
|
sdkstatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
|
|
"github.com/nspcc-dev/neofs-sdk-go/token"
|
|
"github.com/nspcc-dev/neofs-sdk-go/version"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
var errKeyerSingleArgument = errors.New("pass only one argument at a time")
|
|
|
|
var (
|
|
utilCmd = &cobra.Command{
|
|
Use: "util",
|
|
Short: "Utility operations",
|
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
|
flags := cmd.Flags()
|
|
|
|
_ = viper.BindPFlag(generateKey, flags.Lookup(generateKey))
|
|
_ = viper.BindPFlag(walletPath, flags.Lookup(walletPath))
|
|
_ = viper.BindPFlag(address, flags.Lookup(address))
|
|
_ = viper.BindPFlag(verbose, flags.Lookup(verbose))
|
|
},
|
|
}
|
|
|
|
signCmd = &cobra.Command{
|
|
Use: "sign",
|
|
Short: "Sign NeoFS structure",
|
|
}
|
|
|
|
signBearerCmd = &cobra.Command{
|
|
Use: "bearer-token",
|
|
Short: "Sign bearer token to use it in requests",
|
|
Run: signBearerToken,
|
|
}
|
|
|
|
signSessionCmd = &cobra.Command{
|
|
Use: "session-token",
|
|
Short: "Sign session token to use it in requests",
|
|
Run: signSessionToken,
|
|
}
|
|
|
|
convertCmd = &cobra.Command{
|
|
Use: "convert",
|
|
Short: "Convert representation of NeoFS structures",
|
|
}
|
|
|
|
convertEACLCmd = &cobra.Command{
|
|
Use: "eacl",
|
|
Short: "Convert representation of extended ACL table",
|
|
Run: convertEACLTable,
|
|
}
|
|
|
|
keyerCmd = &cobra.Command{
|
|
Use: "keyer",
|
|
Short: "Generate or print information about keys",
|
|
Run: processKeyer,
|
|
}
|
|
)
|
|
|
|
// locode section
|
|
var locodeCmd = &cobra.Command{
|
|
Use: "locode",
|
|
Short: "Working with NeoFS UN/LOCODE database",
|
|
}
|
|
|
|
const (
|
|
locodeGenerateInputFlag = "in"
|
|
locodeGenerateSubDivFlag = "subdiv"
|
|
locodeGenerateAirportsFlag = "airports"
|
|
locodeGenerateCountriesFlag = "countries"
|
|
locodeGenerateContinentsFlag = "continents"
|
|
locodeGenerateOutputFlag = "out"
|
|
)
|
|
|
|
type namesDB struct {
|
|
*airportsdb.DB
|
|
*csvlocode.Table
|
|
}
|
|
|
|
var (
|
|
locodeGenerateInPaths []string
|
|
locodeGenerateSubDivPath string
|
|
locodeGenerateAirportsPath string
|
|
locodeGenerateCountriesPath string
|
|
locodeGenerateContinentsPath string
|
|
locodeGenerateOutPath string
|
|
|
|
locodeGenerateCmd = &cobra.Command{
|
|
Use: "generate",
|
|
Short: "generate UN/LOCODE database for NeoFS",
|
|
Run: func(cmd *cobra.Command, _ []string) {
|
|
locodeDB := csvlocode.New(
|
|
csvlocode.Prm{
|
|
Path: locodeGenerateInPaths[0],
|
|
SubDivPath: locodeGenerateSubDivPath,
|
|
},
|
|
csvlocode.WithExtraPaths(locodeGenerateInPaths[1:]...),
|
|
)
|
|
|
|
airportDB := airportsdb.New(airportsdb.Prm{
|
|
AirportsPath: locodeGenerateAirportsPath,
|
|
CountriesPath: locodeGenerateCountriesPath,
|
|
})
|
|
|
|
continentsDB := continentsdb.New(continentsdb.Prm{
|
|
Path: locodeGenerateContinentsPath,
|
|
})
|
|
|
|
targetDB := locodebolt.New(locodebolt.Prm{
|
|
Path: locodeGenerateOutPath,
|
|
})
|
|
|
|
err := targetDB.Open()
|
|
exitOnErr(cmd, err)
|
|
|
|
defer targetDB.Close()
|
|
|
|
names := &namesDB{
|
|
DB: airportDB,
|
|
Table: locodeDB,
|
|
}
|
|
|
|
err = locodedb.FillDatabase(locodeDB, airportDB, continentsDB, names, targetDB)
|
|
exitOnErr(cmd, err)
|
|
},
|
|
}
|
|
)
|
|
|
|
const (
|
|
locodeInfoDBFlag = "db"
|
|
locodeInfoCodeFlag = "locode"
|
|
)
|
|
|
|
var (
|
|
locodeInfoDBPath string
|
|
locodeInfoCode string
|
|
|
|
locodeInfoCmd = &cobra.Command{
|
|
Use: "info",
|
|
Short: "print information about UN/LOCODE from NeoFS database",
|
|
Run: func(cmd *cobra.Command, _ []string) {
|
|
targetDB := locodebolt.New(locodebolt.Prm{
|
|
Path: locodeInfoDBPath,
|
|
}, locodebolt.ReadOnly())
|
|
|
|
err := targetDB.Open()
|
|
exitOnErr(cmd, err)
|
|
|
|
defer targetDB.Close()
|
|
|
|
record, err := locodedb.LocodeRecord(targetDB, locodeInfoCode)
|
|
exitOnErr(cmd, err)
|
|
|
|
cmd.Printf("Country: %s\n", record.CountryName())
|
|
cmd.Printf("Location: %s\n", record.LocationName())
|
|
cmd.Printf("Continent: %s\n", record.Continent())
|
|
if subDivCode := record.SubDivCode(); subDivCode != "" {
|
|
cmd.Printf("Subdivision: [%s] %s\n", subDivCode, record.SubDivName())
|
|
}
|
|
|
|
geoPoint := record.GeoPoint()
|
|
cmd.Printf("Coordinates: %0.2f, %0.2f\n", geoPoint.Latitude(), geoPoint.Longitude())
|
|
},
|
|
}
|
|
)
|
|
|
|
func initUtilKeyerCmd() {
|
|
keyerCmd.Flags().BoolP("generate", "g", false, "generate new private key")
|
|
keyerCmd.Flags().Bool("hex", false, "print all values in hex encoding")
|
|
keyerCmd.Flags().BoolP("uncompressed", "u", false, "use uncompressed public key format")
|
|
keyerCmd.Flags().BoolP("multisig", "m", false, "calculate multisig address from public keys")
|
|
}
|
|
|
|
func initCommonFlagsWithoutRPC(cmd *cobra.Command) {
|
|
flags := cmd.Flags()
|
|
|
|
flags.BoolP(generateKey, generateKeyShorthand, generateKeyDefault, generateKeyUsage)
|
|
flags.StringP(walletPath, walletPathShorthand, walletPathDefault, walletPathUsage)
|
|
flags.StringP(address, addressShorthand, addressDefault, addressUsage)
|
|
flags.BoolP(verbose, verboseShorthand, verboseDefault, verboseUsage)
|
|
}
|
|
|
|
func initUtilSignBearerCmd() {
|
|
initCommonFlagsWithoutRPC(signBearerCmd)
|
|
|
|
flags := signBearerCmd.Flags()
|
|
|
|
flags.String("from", "", "File with JSON or binary encoded bearer token to sign")
|
|
_ = signBearerCmd.MarkFlagFilename("from")
|
|
_ = signBearerCmd.MarkFlagRequired("from")
|
|
|
|
flags.String("to", "", "File to dump signed bearer token (default: binary encoded)")
|
|
flags.Bool("json", false, "Dump bearer token in JSON encoding")
|
|
}
|
|
|
|
func initUtilSignSessionCmd() {
|
|
initCommonFlagsWithoutRPC(signSessionCmd)
|
|
|
|
flags := signSessionCmd.Flags()
|
|
|
|
flags.String("from", "", "File with JSON encoded session token to sign")
|
|
_ = signSessionCmd.MarkFlagFilename("from")
|
|
_ = signSessionCmd.MarkFlagRequired("from")
|
|
|
|
flags.String("to", "", "File to save signed session token (optional)")
|
|
}
|
|
|
|
func initUtilConvertEACLCmd() {
|
|
flags := convertEACLCmd.Flags()
|
|
|
|
flags.String("from", "", "File with JSON or binary encoded extended ACL table")
|
|
_ = convertEACLCmd.MarkFlagFilename("from")
|
|
_ = convertEACLCmd.MarkFlagRequired("from")
|
|
|
|
flags.String("to", "", "File to dump extended ACL table (default: binary encoded)")
|
|
flags.Bool("json", false, "Dump extended ACL table in JSON encoding")
|
|
}
|
|
|
|
func initUtilLocodeGenerateCmd() {
|
|
flags := locodeGenerateCmd.Flags()
|
|
|
|
flags.StringSliceVar(&locodeGenerateInPaths, locodeGenerateInputFlag, nil, "List of paths to UN/LOCODE tables (csv)")
|
|
_ = locodeGenerateCmd.MarkFlagRequired(locodeGenerateInputFlag)
|
|
|
|
flags.StringVar(&locodeGenerateSubDivPath, locodeGenerateSubDivFlag, "", "Path to UN/LOCODE subdivision database (csv)")
|
|
_ = locodeGenerateCmd.MarkFlagRequired(locodeGenerateSubDivFlag)
|
|
|
|
flags.StringVar(&locodeGenerateAirportsPath, locodeGenerateAirportsFlag, "", "Path to OpenFlights airport database (csv)")
|
|
_ = locodeGenerateCmd.MarkFlagRequired(locodeGenerateAirportsFlag)
|
|
|
|
flags.StringVar(&locodeGenerateCountriesPath, locodeGenerateCountriesFlag, "", "Path to OpenFlights country database (csv)")
|
|
_ = locodeGenerateCmd.MarkFlagRequired(locodeGenerateCountriesFlag)
|
|
|
|
flags.StringVar(&locodeGenerateContinentsPath, locodeGenerateContinentsFlag, "", "Path to continent polygons (GeoJSON)")
|
|
_ = locodeGenerateCmd.MarkFlagRequired(locodeGenerateContinentsFlag)
|
|
|
|
flags.StringVar(&locodeGenerateOutPath, locodeGenerateOutputFlag, "", "Target path for generated database")
|
|
_ = locodeGenerateCmd.MarkFlagRequired(locodeGenerateOutputFlag)
|
|
}
|
|
|
|
func initUtilLocodeInfoCmd() {
|
|
flags := locodeInfoCmd.Flags()
|
|
|
|
flags.StringVar(&locodeInfoDBPath, locodeInfoDBFlag, "", "Path to NeoFS UN/LOCODE database")
|
|
_ = locodeInfoCmd.MarkFlagRequired(locodeInfoDBFlag)
|
|
|
|
flags.StringVar(&locodeInfoCode, locodeInfoCodeFlag, "", "UN/LOCODE")
|
|
_ = locodeInfoCmd.MarkFlagRequired(locodeInfoCodeFlag)
|
|
}
|
|
|
|
func init() {
|
|
rootCmd.AddCommand(utilCmd)
|
|
|
|
utilCmd.AddCommand(
|
|
signCmd,
|
|
convertCmd,
|
|
keyerCmd,
|
|
locodeCmd,
|
|
)
|
|
|
|
signCmd.AddCommand(signBearerCmd, signSessionCmd)
|
|
convertCmd.AddCommand(convertEACLCmd)
|
|
locodeCmd.AddCommand(locodeGenerateCmd, locodeInfoCmd)
|
|
|
|
initUtilKeyerCmd()
|
|
|
|
initUtilSignBearerCmd()
|
|
initUtilSignSessionCmd()
|
|
|
|
initUtilConvertEACLCmd()
|
|
|
|
initUtilLocodeInfoCmd()
|
|
initUtilLocodeGenerateCmd()
|
|
}
|
|
|
|
func signBearerToken(cmd *cobra.Command, _ []string) {
|
|
btok, err := getBearerToken(cmd, "from")
|
|
exitOnErr(cmd, err)
|
|
|
|
key, err := getKey()
|
|
exitOnErr(cmd, err)
|
|
|
|
err = completeBearerToken(btok)
|
|
exitOnErr(cmd, err)
|
|
|
|
err = btok.SignToken(key)
|
|
exitOnErr(cmd, err)
|
|
|
|
to := cmd.Flag("to").Value.String()
|
|
jsonFlag, _ := cmd.Flags().GetBool("json")
|
|
|
|
var data []byte
|
|
if jsonFlag || len(to) == 0 {
|
|
data, err = btok.MarshalJSON()
|
|
exitOnErr(cmd, errf("can't JSON encode bearer token: %w", err))
|
|
} else {
|
|
data, err = btok.Marshal()
|
|
exitOnErr(cmd, errf("can't binary encode bearer token: %w", err))
|
|
}
|
|
|
|
if len(to) == 0 {
|
|
prettyPrintJSON(cmd, data)
|
|
|
|
return
|
|
}
|
|
|
|
err = os.WriteFile(to, data, 0644)
|
|
exitOnErr(cmd, errf("can't write signed bearer token to file: %w", err))
|
|
|
|
cmd.Printf("signed bearer token was successfully dumped to %s\n", to)
|
|
}
|
|
|
|
func signSessionToken(cmd *cobra.Command, _ []string) {
|
|
path, err := cmd.Flags().GetString("from")
|
|
exitOnErr(cmd, err)
|
|
|
|
stok, err := getSessionToken(path)
|
|
if err != nil {
|
|
exitOnErr(cmd, fmt.Errorf("can't read session token from %s: %w", path, err))
|
|
}
|
|
|
|
key, err := getKey()
|
|
exitOnErr(cmd, errf("can't get private key, make sure it is provided: %w", err))
|
|
|
|
err = stok.Sign(key)
|
|
exitOnErr(cmd, errf("can't sign token: %w", err))
|
|
|
|
data, err := stok.MarshalJSON()
|
|
exitOnErr(cmd, errf("can't encode session token: %w", err))
|
|
|
|
to := cmd.Flag("to").Value.String()
|
|
if len(to) == 0 {
|
|
prettyPrintJSON(cmd, data)
|
|
return
|
|
}
|
|
|
|
err = os.WriteFile(to, data, 0644)
|
|
if err != nil {
|
|
exitOnErr(cmd, fmt.Errorf("can't write signed session token to %s: %w", to, err))
|
|
}
|
|
|
|
cmd.Printf("signed session token saved in %s\n", to)
|
|
}
|
|
|
|
func convertEACLTable(cmd *cobra.Command, _ []string) {
|
|
pathFrom := cmd.Flag("from").Value.String()
|
|
to := cmd.Flag("to").Value.String()
|
|
jsonFlag, _ := cmd.Flags().GetBool("json")
|
|
|
|
table, err := parseEACL(pathFrom)
|
|
exitOnErr(cmd, err)
|
|
|
|
var data []byte
|
|
if jsonFlag || len(to) == 0 {
|
|
data, err = table.MarshalJSON()
|
|
exitOnErr(cmd, errf("can't JSON encode extended ACL table: %w", err))
|
|
} else {
|
|
data, err = table.Marshal()
|
|
exitOnErr(cmd, errf("can't binary encode extended ACL table: %w", err))
|
|
}
|
|
|
|
if len(to) == 0 {
|
|
prettyPrintJSON(cmd, data)
|
|
return
|
|
}
|
|
|
|
err = os.WriteFile(to, data, 0644)
|
|
exitOnErr(cmd, errf("can't write exteded ACL table to file: %w", err))
|
|
|
|
cmd.Printf("extended ACL table was successfully dumped to %s\n", to)
|
|
}
|
|
|
|
func processKeyer(cmd *cobra.Command, args []string) {
|
|
var (
|
|
err error
|
|
|
|
result = new(keyer.Dashboard)
|
|
generate, _ = cmd.Flags().GetBool("generate")
|
|
useHex, _ = cmd.Flags().GetBool("hex")
|
|
uncompressed, _ = cmd.Flags().GetBool("uncompressed")
|
|
multisig, _ = cmd.Flags().GetBool("multisig")
|
|
)
|
|
|
|
if multisig {
|
|
err = result.ParseMultiSig(args)
|
|
} else {
|
|
if len(args) > 1 {
|
|
exitOnErr(cmd, errKeyerSingleArgument)
|
|
}
|
|
|
|
var argument string
|
|
if len(args) > 0 {
|
|
argument = args[0]
|
|
}
|
|
|
|
switch {
|
|
case generate:
|
|
err = keyerGenerate(argument, result)
|
|
case fileExists(argument):
|
|
err = keyerParseFile(argument, result)
|
|
default:
|
|
err = result.ParseString(argument)
|
|
}
|
|
}
|
|
|
|
exitOnErr(cmd, err)
|
|
|
|
result.PrettyPrint(uncompressed, useHex)
|
|
}
|
|
|
|
func completeBearerToken(btok *token.BearerToken) error {
|
|
if v2 := btok.ToV2(); v2 != nil {
|
|
// set eACL table version, because it usually omitted
|
|
table := v2.GetBody().GetEACL()
|
|
table.SetVersion(version.Current().ToV2())
|
|
} else {
|
|
return errors.New("unsupported bearer token version")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func prettyPrintJSON(cmd *cobra.Command, data []byte) {
|
|
buf := new(bytes.Buffer)
|
|
if err := json.Indent(buf, data, "", " "); err != nil {
|
|
printVerbose("Can't pretty print json: %w", err)
|
|
}
|
|
|
|
cmd.Println(buf)
|
|
}
|
|
|
|
func prettyPrintUnixTime(s string) string {
|
|
unixTime, err := strconv.ParseInt(s, 10, 64)
|
|
if err != nil {
|
|
return "malformed"
|
|
}
|
|
|
|
timestamp := time.Unix(unixTime, 0)
|
|
|
|
return timestamp.String()
|
|
}
|
|
|
|
func keyerGenerate(filename string, d *keyer.Dashboard) error {
|
|
key := make([]byte, keyer.NeoPrivateKeySize)
|
|
|
|
_, err := rand.Read(key)
|
|
if err != nil {
|
|
return fmt.Errorf("can't get random source: %w", err)
|
|
}
|
|
|
|
err = d.ParseBinary(key)
|
|
if err != nil {
|
|
return fmt.Errorf("can't parse key: %w", err)
|
|
}
|
|
|
|
if filename != "" {
|
|
return os.WriteFile(filename, key, 0600)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func fileExists(filename string) bool {
|
|
info, err := os.Stat(filename)
|
|
if os.IsNotExist(err) {
|
|
return false
|
|
}
|
|
|
|
return !info.IsDir()
|
|
}
|
|
|
|
func keyerParseFile(filename string, d *keyer.Dashboard) error {
|
|
data, err := os.ReadFile(filename)
|
|
if err != nil {
|
|
return fmt.Errorf("can't open %v file: %w", filename, err)
|
|
}
|
|
|
|
return d.ParseBinary(data)
|
|
}
|
|
|
|
// errf returns formatted error in errFmt format
|
|
// if err is not nil.
|
|
func errf(errFmt string, err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf(errFmt, err)
|
|
}
|
|
|
|
// exitOnErr prints error and exits with a code that matches
|
|
// one of the common errors from sdk library. If no errors
|
|
// found, exits with 1 code.
|
|
// Does nothing if passed error in nil.
|
|
func exitOnErr(cmd *cobra.Command, err error) {
|
|
if err == nil {
|
|
return
|
|
}
|
|
|
|
const (
|
|
_ = iota
|
|
internal
|
|
)
|
|
|
|
var (
|
|
code int
|
|
|
|
internalErr = new(sdkstatus.ServerInternal)
|
|
)
|
|
|
|
switch {
|
|
case errors.As(err, &internalErr):
|
|
code = internal
|
|
default:
|
|
code = internal
|
|
}
|
|
|
|
cmd.PrintErrln(err)
|
|
os.Exit(code)
|
|
}
|