2021-05-26 21:19:29 +01:00
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
2022-01-25 23:39:26 +00:00
"bytes"
2022-05-18 15:37:37 +01:00
"context"
2022-01-25 23:39:26 +00:00
"errors"
2021-05-26 21:19:29 +01:00
"fmt"
2022-01-25 23:39:26 +00:00
"io"
2021-05-26 21:19:29 +01:00
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
2022-05-18 15:37:37 +01:00
"github.com/spacemonkeygo/monkit/v3"
2021-05-26 21:19:29 +01:00
"github.com/zeebo/clingy"
"github.com/zeebo/errs"
2022-05-24 21:48:50 +01:00
"go.uber.org/zap"
2021-08-02 17:54:30 +01:00
"golang.org/x/term"
2022-05-18 15:37:37 +01:00
"storj.io/common/rpc/rpctracing"
2022-05-24 21:48:50 +01:00
jaeger "storj.io/monkit-jaeger"
"storj.io/private/version"
2021-05-26 21:19:29 +01:00
)
type external struct {
interactive bool // controls if interactive input is allowed
2022-02-07 20:21:55 +00:00
quic bool // if set, use the quic transport
2021-05-26 21:19:29 +01:00
dirs struct {
loaded bool // true if Setup has been called
current string // current config directory
legacy string // old config directory
}
migration struct {
migrated bool // true if a migration has been attempted
err error // any error from the migration attempt
}
config struct {
loaded bool // true if the existing config file is successfully loaded
values map [ string ] [ ] string // the existing configuration
}
access struct {
loaded bool // true if we've successfully loaded access.json
defaultName string // default access name to use from accesses
accesses map [ string ] string // map of all of the stored accesses
}
2022-05-18 15:37:37 +01:00
tracing struct {
2022-08-31 10:12:46 +01:00
traceID int64 // if non-zero, sets outgoing traces to the given id
traceAddress string // if non-zero, sampled spans are sent to this trace collector address.
sample float64 // the chance (number between 0 and 1.0) to send samples to the server.
verbose bool // flag to print out tracing information (like the used trace id)
2022-05-18 15:37:37 +01:00
}
2021-05-26 21:19:29 +01:00
}
func newExternal ( ) * external {
return & external { }
}
func ( ex * external ) Setup ( f clingy . Flags ) {
ex . interactive = f . Flag (
"interactive" , "Controls if interactive input is allowed" , true ,
2021-12-09 19:21:52 +00:00
clingy . Transform ( strconv . ParseBool ) , clingy . Boolean ,
2021-05-26 21:19:29 +01:00
clingy . Advanced ,
) . ( bool )
2022-02-07 20:21:55 +00:00
ex . quic = f . Flag (
"quic" , "If set, uses the quic transport" , false ,
clingy . Transform ( strconv . ParseBool ) , clingy . Boolean ,
clingy . Advanced ,
) . ( bool )
2021-05-26 21:19:29 +01:00
ex . dirs . current = f . Flag (
"config-dir" , "Directory that stores the configuration" ,
2022-03-30 12:06:53 +01:00
appDir ( false , defaultUplinkSubdir ( ) ... ) ,
2021-05-26 21:19:29 +01:00
) . ( string )
ex . dirs . legacy = f . Flag (
"legacy-config-dir" , "Directory that stores legacy configuration. Only used during migration" ,
2022-03-30 12:06:53 +01:00
appDir ( true , defaultUplinkSubdir ( ) ... ) ,
2021-05-26 21:19:29 +01:00
clingy . Advanced ,
) . ( string )
2022-05-18 15:37:37 +01:00
ex . tracing . traceID = f . Flag (
2022-08-31 10:12:46 +01:00
"trace-id" , "Specify a trace id manually. This should be globally unique. " +
"Usually you don't need to set it, and it will be automatically generated." , int64 ( 0 ) ,
2022-05-18 15:37:37 +01:00
clingy . Transform ( transformInt64 ) ,
clingy . Advanced ,
) . ( int64 )
2022-08-31 10:12:46 +01:00
ex . tracing . sample = f . Flag (
"trace-sample" , "The chance (between 0 and 1.0) to report tracing information. Set to 1 to always send it." , float64 ( 0 ) ,
clingy . Transform ( transformFloat64 ) ,
clingy . Advanced ,
) . ( float64 )
ex . tracing . verbose = f . Flag (
"trace-verbose" , "Flag to print out used trace ID" , false ,
clingy . Transform ( strconv . ParseBool ) ,
clingy . Advanced ,
) . ( bool )
2022-05-24 21:48:50 +01:00
ex . tracing . traceAddress = f . Flag (
"trace-addr" , "Specify where to send traces" , "agent.tracing.datasci.storj.io:5775" ,
clingy . Advanced ,
) . ( string )
2021-05-26 21:19:29 +01:00
ex . dirs . loaded = true
}
2022-05-18 15:37:37 +01:00
func transformInt64 ( x string ) ( int64 , error ) {
return strconv . ParseInt ( x , 0 , 64 )
}
2022-08-31 10:12:46 +01:00
func transformFloat64 ( x string ) ( float64 , error ) {
return strconv . ParseFloat ( x , 64 )
}
2021-06-22 23:41:22 +01:00
func ( ex * external ) AccessInfoFile ( ) string { return filepath . Join ( ex . dirs . current , "access.json" ) }
func ( ex * external ) ConfigFile ( ) string { return filepath . Join ( ex . dirs . current , "config.ini" ) }
2021-05-26 21:19:29 +01:00
func ( ex * external ) legacyConfigFile ( ) string { return filepath . Join ( ex . dirs . legacy , "config.yaml" ) }
// Dynamic is called by clingy to look up values for global flags not specified on the command
// line. This call lets us fill in values from config files or environment variables.
func ( ex * external ) Dynamic ( name string ) ( vals [ ] string , err error ) {
key := "UPLINK_" + strings . ToUpper ( strings . ReplaceAll ( name , "-" , "_" ) )
if val , ok := os . LookupEnv ( key ) ; ok {
return [ ] string { val } , nil
}
// if we have not yet loaded the directories, we should not try to migrate
// and load the current config.
if ! ex . dirs . loaded {
return nil , nil
}
// allow errors from migration and configuration loading so that calls to
// `uplink setup` can happen and write out a new configuration.
if err := ex . migrate ( ) ; err != nil {
return nil , nil //nolint
}
if err := ex . loadConfig ( ) ; err != nil {
return nil , nil //nolint
}
return ex . config . values [ name ] , nil
}
// Wrap is called by clingy with the command to be executed.
2022-08-30 10:51:31 +01:00
func ( ex * external ) Wrap ( ctx context . Context , cmd clingy . Command ) ( err error ) {
2021-05-26 21:19:29 +01:00
if err := ex . migrate ( ) ; err != nil {
2021-08-13 20:31:04 +01:00
return err
}
if err := ex . loadConfig ( ) ; err != nil {
2021-05-26 21:19:29 +01:00
return err
}
if ! ex . config . loaded {
2021-08-13 20:31:04 +01:00
if err := saveInitialConfig ( ctx , ex ) ; err != nil {
return err
}
2021-05-26 21:19:29 +01:00
}
2022-05-18 15:37:37 +01:00
2022-08-31 10:12:46 +01:00
if ex . tracing . traceAddress != "" && ( ex . tracing . sample > 0 || ex . tracing . traceID > 0 ) {
2022-05-24 21:48:50 +01:00
versionName := fmt . Sprintf ( "uplink-release-%s" , version . Build . Version . String ( ) )
if ! version . Build . Release {
2022-08-31 10:12:46 +01:00
versionName = "uplink-dev"
2022-05-24 21:48:50 +01:00
}
collector , err := jaeger . NewUDPCollector ( zap . L ( ) , ex . tracing . traceAddress , versionName , nil , 0 , 0 , 0 )
if err != nil {
return err
}
2022-08-31 10:12:46 +01:00
collectorCtx , cancelCollector := context . WithCancel ( ctx )
go collector . Run ( collectorCtx )
2022-05-24 21:48:50 +01:00
defer func ( ) {
2022-08-31 10:12:46 +01:00
// this will drain remaining messages
cancelCollector ( )
_ = collector . Close ( )
2022-05-24 21:48:50 +01:00
} ( )
2022-08-31 10:12:46 +01:00
cancel := jaeger . RegisterJaeger ( monkit . Default , collector , jaeger . Options { Fraction : ex . tracing . sample } )
2022-05-24 21:48:50 +01:00
defer cancel ( )
2022-05-18 15:37:37 +01:00
2022-08-31 10:12:46 +01:00
if ex . tracing . traceID == 0 {
if ex . tracing . verbose {
var printedFirst bool
monkit . Default . ObserveTraces ( func ( trace * monkit . Trace ) {
// workaround to hide the traceID of tlsopts.verifyIndentity called from a separated goroutine
if ! printedFirst {
_ , _ = fmt . Fprintf ( clingy . Stdout ( ctx ) , "New traceID %x\n" , trace . Id ( ) )
printedFirst = true
}
} )
}
} else {
trace := monkit . NewTrace ( ex . tracing . traceID )
trace . Set ( rpctracing . Sampled , true )
defer mon . Func ( ) . RemoteTrace ( & ctx , monkit . NewId ( ) , trace ) ( & err )
}
2022-05-18 15:37:37 +01:00
}
2022-08-31 10:12:46 +01:00
defer mon . Task ( ) ( & ctx ) ( & err )
2022-08-30 10:51:31 +01:00
return cmd . Execute ( ctx )
2022-05-18 15:37:37 +01:00
}
2021-05-26 21:19:29 +01:00
// PromptInput gets a line of input text from the user and returns an error if
// interactive mode is disabled.
2022-08-30 10:51:31 +01:00
func ( ex * external ) PromptInput ( ctx context . Context , prompt string ) ( input string , err error ) {
2021-05-26 21:19:29 +01:00
if ! ex . interactive {
return "" , errs . New ( "required user input in non-interactive setting" )
}
2022-08-30 10:51:31 +01:00
fmt . Fprint ( clingy . Stdout ( ctx ) , prompt , " " )
2022-01-25 23:39:26 +00:00
var buf [ ] byte
var tmp [ 1 ] byte
for {
2022-08-30 10:51:31 +01:00
_ , err := clingy . Stdin ( ctx ) . Read ( tmp [ : ] )
2022-01-25 23:39:26 +00:00
if errors . Is ( err , io . EOF ) {
break
} else if err != nil {
return "" , errs . Wrap ( err )
} else if tmp [ 0 ] == '\n' {
break
}
buf = append ( buf , tmp [ 0 ] )
}
return string ( bytes . TrimSpace ( buf ) ) , nil
2021-05-26 21:19:29 +01:00
}
2021-08-02 17:54:30 +01:00
// PromptInput gets a line of secret input from the user twice to ensure that
// it is the same value, and returns an error if interactive mode is disabled
// or if the prompt cannot be put into a mode where the typing is not echoed.
2022-08-30 10:51:31 +01:00
func ( ex * external ) PromptSecret ( ctx context . Context , prompt string ) ( secret string , err error ) {
2021-08-02 17:54:30 +01:00
if ! ex . interactive {
return "" , errs . New ( "required secret input in non-interactive setting" )
}
2022-08-30 10:51:31 +01:00
fh , ok := clingy . Stdin ( ctx ) . ( interface { Fd ( ) uintptr } )
2021-08-02 17:54:30 +01:00
if ! ok {
return "" , errs . New ( "unable to request secret from stdin" )
}
fd := int ( fh . Fd ( ) )
for {
2022-08-30 10:51:31 +01:00
fmt . Fprint ( clingy . Stdout ( ctx ) , prompt , " " )
2021-08-02 17:54:30 +01:00
first , err := term . ReadPassword ( fd )
if err != nil {
return "" , errs . New ( "unable to request secret from stdin: %w" , err )
}
2022-08-30 10:51:31 +01:00
fmt . Fprintln ( clingy . Stdout ( ctx ) )
2021-08-02 17:54:30 +01:00
2022-08-30 10:51:31 +01:00
fmt . Fprint ( clingy . Stdout ( ctx ) , "Again: " )
2021-08-02 17:54:30 +01:00
second , err := term . ReadPassword ( fd )
if err != nil {
return "" , errs . New ( "unable to request secret from stdin: %w" , err )
}
2022-08-30 10:51:31 +01:00
fmt . Fprintln ( clingy . Stdout ( ctx ) )
2021-08-02 17:54:30 +01:00
if string ( first ) != string ( second ) {
2022-08-30 10:51:31 +01:00
fmt . Fprintln ( clingy . Stdout ( ctx ) , "Values did not match. Try again." )
fmt . Fprintln ( clingy . Stdout ( ctx ) )
2021-08-02 17:54:30 +01:00
continue
}
return string ( first ) , nil
}
}
2022-03-30 12:06:53 +01:00
func defaultUplinkSubdir ( ) [ ] string {
switch runtime . GOOS {
case "windows" , "darwin" :
return [ ] string { "Storj" , "Uplink" }
default :
return [ ] string { "storj" , "uplink" }
}
}
2021-05-26 21:19:29 +01:00
// appDir returns best base directory for the currently running operating system. It
// has a legacy bool to have it return the same values that storj.io/common/fpath.ApplicationDir
// would have returned.
func appDir ( legacy bool , subdir ... string ) string {
var appdir string
home := os . Getenv ( "HOME" )
switch runtime . GOOS {
case "windows" :
// Windows standards: https://msdn.microsoft.com/en-us/library/windows/apps/hh465094.aspx?f=255&MSPPError=-2147217396
for _ , env := range [ ] string { "AppData" , "AppDataLocal" , "UserProfile" , "Home" } {
val := os . Getenv ( env )
if val != "" {
appdir = val
break
}
}
case "darwin" :
// Mac standards: https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/MacOSXDirectories/MacOSXDirectories.html
appdir = filepath . Join ( home , "Library" , "Application Support" )
case "linux" :
fallthrough
default :
// Linux standards: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
if legacy {
appdir = os . Getenv ( "XDG_DATA_HOME" )
if appdir == "" && home != "" {
appdir = filepath . Join ( home , ".local" , "share" )
}
} else {
appdir = os . Getenv ( "XDG_CONFIG_HOME" )
if appdir == "" && home != "" {
appdir = filepath . Join ( home , ".config" )
}
}
}
return filepath . Join ( append ( [ ] string { appdir } , subdir ... ) ... )
}