From eb252defd52f175ddc5df6184463c18b70b19342 Mon Sep 17 00:00:00 2001 From: mellonnen Date: Wed, 22 Feb 2023 22:52:30 +0100 Subject: [PATCH 1/5] feat: add WithCopyFlag option --- ui/sender/sender.go | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/ui/sender/sender.go b/ui/sender/sender.go index 656a484..9771d11 100644 --- a/ui/sender/sender.go +++ b/ui/sender/sender.go @@ -67,6 +67,12 @@ func WithVersion(version semver.Version) Option { } } +func WithCopyFlags(flags map[string]string) Option { + return func(m *model) { + m.copyFlags = flags + } +} + type model struct { state uiState // defaults to 0 (showPassword) transferType transfer.Type // defaults to 0 (Unknown) @@ -91,6 +97,7 @@ type model struct { help help.Model keys ui.KeyMap copyMessageTimer timer.Model + copyFlags map[string]string } // New creates a new sender program. @@ -250,7 +257,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case key.Matches(msg, m.keys.Quit): return m, tea.Quit case key.Matches(msg, m.keys.CopyPassword): - err := clipboard.WriteAll(fmt.Sprintf("portal receive %s", m.password)) + err := clipboard.WriteAll(m.copyReceiverCommand()) if err != nil { return m, ui.ErrorCmd(errors.New("Failed to copy password to clipboard")) } else { @@ -319,7 +326,7 @@ func (m model) View() string { return ui.PadText + ui.LogSeparator(m.width) + ui.PadText + ui.InfoStyle(statusText) + "\n\n" + ui.PadText + ui.InfoStyle("On the receiving end, run:") + "\n" + - ui.PadText + ui.InfoStyle(fmt.Sprintf("portal receive %s", m.password)) + "\n\n" + + ui.PadText + ui.InfoStyle(m.copyReceiverCommand()) + "\n\n" + m.fileTable.View() + ui.PadText + m.help.View(m.keys) + "\n\n" @@ -429,7 +436,7 @@ func listenTransferCmd(msgs chan interface{}) tea.Cmd { } } -// -------------------- HELPER METHODS ------------------------- +// -------------------------------------------------- Helper Functions ------------------------------------------------- func (m *model) resetSpinner() { m.spinner = spinner.New() @@ -443,3 +450,16 @@ func (m *model) resetSpinner() { m.spinner.Spinner = ui.TransferSpinner } } + +func (m *model) copyReceiverCommand() string { + var builder strings.Builder + builder.WriteString("portal receive ") + builder.WriteString(m.password) + for flag, value := range m.copyFlags { + builder.WriteRune(' ') + builder.WriteString(flag) + builder.WriteRune(' ') + builder.WriteString(value) + } + return builder.String() +} From 8deae65d730f515b2fe3a94499d24b152fb65d2c Mon Sep 17 00:00:00 2001 From: mellonnen Date: Wed, 22 Feb 2023 22:53:00 +0100 Subject: [PATCH 2/5] chore, feat: cleanup in runner code, pass flags to sender ui --- cmd/portal/main.go | 33 +++++++++++++++++---------------- cmd/portal/receive.go | 30 +++++++++++++++--------------- cmd/portal/send.go | 39 ++++++++++++++++++++++----------------- 3 files changed, 54 insertions(+), 48 deletions(-) diff --git a/cmd/portal/main.go b/cmd/portal/main.go index 7aa0043..95b2538 100644 --- a/cmd/portal/main.go +++ b/cmd/portal/main.go @@ -20,6 +20,21 @@ import ( // injected at link time using -ldflags. var version string +// Initialization of cobra and viper. +func init() { + cobra.OnInitialize(initViperConfig) + + rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Specifes if portal logs debug information to a file on the format `.portal-[command].log` in the current directory") + // Setup viper config. + // Add cobra subcommands. + rootCmd.AddCommand(sendCmd) + rootCmd.AddCommand(receiveCmd) + rootCmd.AddCommand(serveCmd) + rootCmd.AddCommand(versionCmd) +} + +// ------------------------------------------------------ Command ------------------------------------------------------ + // rootCmd is the top level `portal` command on which the other subcommands are attached to. var rootCmd = &cobra.Command{ Use: "portal", @@ -45,20 +60,7 @@ func main() { } } -// Initialization of cobra and viper. -func init() { - cobra.OnInitialize(initViperConfig) - - rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Specifes if portal logs debug information to a file on the format `.portal-[command].log` in the current directory") - // Setup viper config. - // Add cobra subcommands. - rootCmd.AddCommand(sendCmd) - rootCmd.AddCommand(receiveCmd) - rootCmd.AddCommand(serveCmd) - rootCmd.AddCommand(versionCmd) -} - -// HELPER FUNCTIONS +// -------------------------------------------------- Helper Functions ------------------------------------------------- // initViperConfig initializes the viper config. // It creates a `.portal.yml` file at the home directory if it has not been created earlier @@ -67,8 +69,7 @@ func init() { func initViperConfig() { // Set default values viper.SetDefault("verbose", false) - viper.SetDefault("rendezvousPort", DEFAULT_RENDEZVOUS_PORT) - viper.SetDefault("rendezvousAddress", DEFAULT_RENDEZVOUS_ADDRESS) + viper.SetDefault("relay", fmt.Sprintf("%s:%d", DEFAULT_RENDEZVOUS_ADDRESS, DEFAULT_RENDEZVOUS_PORT)) // Find home directory. home, err := homedir.Dir() diff --git a/cmd/portal/receive.go b/cmd/portal/receive.go index b39d203..5694b73 100644 --- a/cmd/portal/receive.go +++ b/cmd/portal/receive.go @@ -16,6 +16,14 @@ import ( "golang.org/x/exp/slices" ) +// Setup flags. +func init() { + // Add subcommand flags (dummy default values as default values are handled through viper). + receiveCmd.Flags().StringP("relay", "r", "", "address of the relay server") +} + +// ------------------------------------------------------ Command ------------------------------------------------------ + // receiveCmd is the cobra command for `portal receive` var receiveCmd = &cobra.Command{ Use: "receive", @@ -23,12 +31,12 @@ var receiveCmd = &cobra.Command{ Long: "The receive command receives files from the sender with the matching password.", Args: cobra.ExactArgs(1), ValidArgsFunction: passwordCompletion, - PreRun: func(cmd *cobra.Command, args []string) { + PreRunE: func(cmd *cobra.Command, args []string) error { // Bind flags to viper - //nolint - viper.BindPFlag("rendezvousPort", cmd.Flags().Lookup("rendezvous-port")) - //nolint - viper.BindPFlag("rendezvousAddress", cmd.Flags().Lookup("rendezvous-address")) + if err := viper.BindPFlag("relay", cmd.Flags().Lookup("relay")); err != nil { + return fmt.Errorf("binding relay flag: %w", err) + } + return nil }, RunE: func(cmd *cobra.Command, args []string) error { file.RemoveTemporaryFiles(file.RECEIVE_TEMP_FILE_NAME_PREFIX) @@ -50,24 +58,16 @@ var receiveCmd = &cobra.Command{ }, } -// Setup flags. -func init() { - // Add subcommand flags (dummy default values as default values are handled through viper). - // TODO: recactor this into a single flag for providing a TCPAddr. - receiveCmd.Flags().IntP("rendezvous-port", "p", 0, "port on which the rendezvous server is running") - receiveCmd.Flags().StringP("rendezvous-address", "a", "", "host address for the rendezvous server") -} +// ------------------------------------------------------ Handler ------------------------------------------------------ // handleReceiveCommand is the receive application. func handleReceiveCommand(password string) { - addr := viper.GetString("rendezvousAddress") - port := viper.GetInt("rendezvousPort") var opts []receiver.Option ver, err := semver.Parse(version) if err == nil { opts = append(opts, receiver.WithVersion(ver)) } - receiver := receiver.New(fmt.Sprintf("%s:%d", addr, port), password, opts...) + receiver := receiver.New(viper.GetString("relay"), password, opts...) if _, err := receiver.Run(); err != nil { fmt.Println("Error initializing UI", err) diff --git a/cmd/portal/send.go b/cmd/portal/send.go index 277f561..83d1bbb 100644 --- a/cmd/portal/send.go +++ b/cmd/portal/send.go @@ -12,18 +12,27 @@ import ( "github.com/spf13/viper" ) +// Set flags. +func init() { + // Add subcommand flags (dummy default values as default values are handled through viper) + sendCmd.Flags().StringP("relay", "r", "", "address of the relay server") +} + +// ------------------------------------------------------ Command ------------------------------------------------------ + // sendCmd cobra command for `portal send`. var sendCmd = &cobra.Command{ Use: "send", Short: "Send one or more files", Long: "The send command adds one or more files to be sent. Files are archived and compressed before sending.", Args: cobra.MinimumNArgs(1), - PreRun: func(cmd *cobra.Command, args []string) { + PreRunE: func(cmd *cobra.Command, args []string) error { // Bind flags to viper - //nolint:errcheck - viper.BindPFlag("rendezvousPort", cmd.Flags().Lookup("rendezvous-port")) - //nolint:errcheck - viper.BindPFlag("rendezvousAddress", cmd.Flags().Lookup("rendezvous-address")) + if err := viper.BindPFlag("relay", cmd.Flags().Lookup("relay")); err != nil { + return fmt.Errorf("binding relay flag: %w", err) + } + return nil + }, RunE: func(cmd *cobra.Command, args []string) error { if err := sender.Init(); err != nil { @@ -40,30 +49,26 @@ var sendCmd = &cobra.Command{ } defer logFile.Close() - handleSendCommand(args) + handleSendCommand(args, cmd.Flag("relay").Changed) return nil }, } -// Set flags. -func init() { - // Add subcommand flags (dummy default values as default values are handled through viper) - //TODO: refactor into a single flag providing a string - sendCmd.Flags().IntP("rendezvous-port", "p", 0, "port on which the rendezvous server is running") - sendCmd.Flags().StringP("rendezvous-address", "a", "", "host address for the rendezvous server") -} +// ------------------------------------------------------ Handler ------------------------------------------------------ // handleSendCommand is the sender application. -func handleSendCommand(fileNames []string) { - addr := viper.GetString("rendezvousAddress") - port := viper.GetInt("rendezvousPort") +func handleSendCommand(fileNames []string, selfHostedRelay bool) { var opts []senderui.Option ver, err := semver.Parse(version) // Conditionally add option to sender ui if err == nil { opts = append(opts, senderui.WithVersion(ver)) } - sender := senderui.New(fileNames, fmt.Sprintf("%s:%d", addr, port), opts...) + relayAddr := viper.GetString("relay") + if selfHostedRelay { + opts = append(opts, senderui.WithCopyFlags(map[string]string{"--relay": relayAddr})) + } + sender := senderui.New(fileNames, relayAddr, opts...) if _, err := sender.Run(); err != nil { fmt.Println("Error initializing UI", err) os.Exit(1) From 2883d8d918bca049cc14c038a28e416a3cb88d27 Mon Sep 17 00:00:00 2001 From: mellonnen Date: Thu, 23 Feb 2023 11:41:12 +0100 Subject: [PATCH 3/5] feat: make verbose flag description more clear --- cmd/portal/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/portal/main.go b/cmd/portal/main.go index 95b2538..3505440 100644 --- a/cmd/portal/main.go +++ b/cmd/portal/main.go @@ -24,7 +24,7 @@ var version string func init() { cobra.OnInitialize(initViperConfig) - rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Specifes if portal logs debug information to a file on the format `.portal-[command].log` in the current directory") + rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Log debug information to a file on the format `.portal-[command].log` in the current directory") // Setup viper config. // Add cobra subcommands. rootCmd.AddCommand(sendCmd) From a095d4269f6c944fb5aaed0e4686e81c5ff303ba Mon Sep 17 00:00:00 2001 From: mellonnen Date: Thu, 23 Feb 2023 11:41:28 +0100 Subject: [PATCH 4/5] feat: rework relay validation --- cmd/portal/main.go | 69 ------------------------------------------ cmd/portal/receive.go | 5 ++- cmd/portal/send.go | 5 +-- cmd/portal/validate.go | 43 ++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 74 deletions(-) create mode 100644 cmd/portal/validate.go diff --git a/cmd/portal/main.go b/cmd/portal/main.go index 3505440..9ae9bd2 100644 --- a/cmd/portal/main.go +++ b/cmd/portal/main.go @@ -1,14 +1,11 @@ package main import ( - "errors" "fmt" "io" "log" - "net" "os" "path/filepath" - "unicode/utf8" tea "github.com/charmbracelet/bubbletea" homedir "github.com/mitchellh/go-homedir" @@ -106,17 +103,6 @@ func initViperConfig() { } } -// validateRendezvousAddressInViper validates that the `rendezvousAddress` value in viper is a valid hostname or IP -func validateRendezvousAddressInViper() error { - rendezvouzAdress := net.ParseIP(viper.GetString("rendezvousAddress")) - err := validateHostname(viper.GetString("rendezvousAddress")) - // neither a valid IP nor a valid hostname was provided - if (rendezvouzAdress == nil) && err != nil { - return errors.New("invalid IP or hostname provided") - } - return nil -} - func setupLoggingFromViper(cmd string) (*os.File, error) { if viper.GetBool("verbose") { f, err := tea.LogToFile(fmt.Sprintf(".portal-%s.log", cmd), fmt.Sprintf("portal-%s: \n", cmd)) @@ -128,58 +114,3 @@ func setupLoggingFromViper(cmd string) (*os.File, error) { log.SetOutput(io.Discard) return nil, nil } - -// validateHostname returns an error if the domain name is not valid -// See https://tools.ietf.org/html/rfc1034#section-3.5 and -// https://tools.ietf.org/html/rfc1123#section-2. -// source: https://gist.github.com/chmike/d4126a3247a6d9a70922fc0e8b4f4013 -func validateHostname(name string) error { - switch { - case len(name) == 0: - return nil - case len(name) > 255: - return fmt.Errorf("name length is %d, can't exceed 255", len(name)) - } - var l int - for i := 0; i < len(name); i++ { - b := name[i] - if b == '.' { - // check domain labels validity - switch { - case i == l: - return fmt.Errorf("invalid character '%c' at offset %d: label can't begin with a period", b, i) - case i-l > 63: - return fmt.Errorf("byte length of label '%s' is %d, can't exceed 63", name[l:i], i-l) - case name[l] == '-': - return fmt.Errorf("label '%s' at offset %d begins with a hyphen", name[l:i], l) - case name[i-1] == '-': - return fmt.Errorf("label '%s' at offset %d ends with a hyphen", name[l:i], l) - } - l = i + 1 - continue - } - // test label character validity, note: tests are ordered by decreasing validity frequency - if !(b >= 'a' && b <= 'z' || b >= '0' && b <= '9' || b == '-' || b >= 'A' && b <= 'Z') { - // show the printable unicode character starting at byte offset i - c, _ := utf8.DecodeRuneInString(name[i:]) - if c == utf8.RuneError { - return fmt.Errorf("invalid rune at offset %d", i) - } - return fmt.Errorf("invalid character '%c' at offset %d", c, i) - } - } - // check top level domain validity - switch { - case l == len(name): - return fmt.Errorf("missing top level domain, domain can't end with a period") - case len(name)-l > 63: - return fmt.Errorf("byte length of top level domain '%s' is %d, can't exceed 63", name[l:], len(name)-l) - case name[l] == '-': - return fmt.Errorf("top level domain '%s' at offset %d begins with a hyphen", name[l:], l) - case name[len(name)-1] == '-': - return fmt.Errorf("top level domain '%s' at offset %d ends with a hyphen", name[l:], l) - case name[l] >= '0' && name[l] <= '9': - return fmt.Errorf("top level domain '%s' at offset %d begins with a digit", name[l:], l) - } - return nil -} diff --git a/cmd/portal/receive.go b/cmd/portal/receive.go index 5694b73..36d247d 100644 --- a/cmd/portal/receive.go +++ b/cmd/portal/receive.go @@ -32,7 +32,7 @@ var receiveCmd = &cobra.Command{ Args: cobra.ExactArgs(1), ValidArgsFunction: passwordCompletion, PreRunE: func(cmd *cobra.Command, args []string) error { - // Bind flags to viper + // BindvalidateRelayInViper if err := viper.BindPFlag("relay", cmd.Flags().Lookup("relay")); err != nil { return fmt.Errorf("binding relay flag: %w", err) } @@ -40,8 +40,7 @@ var receiveCmd = &cobra.Command{ }, RunE: func(cmd *cobra.Command, args []string) error { file.RemoveTemporaryFiles(file.RECEIVE_TEMP_FILE_NAME_PREFIX) - err := validateRendezvousAddressInViper() - if err != nil { + if err := validateRelayInViper(); err != nil { return err } logFile, err := setupLoggingFromViper("receive") diff --git a/cmd/portal/send.go b/cmd/portal/send.go index 83d1bbb..011c39e 100644 --- a/cmd/portal/send.go +++ b/cmd/portal/send.go @@ -15,7 +15,7 @@ import ( // Set flags. func init() { // Add subcommand flags (dummy default values as default values are handled through viper) - sendCmd.Flags().StringP("relay", "r", "", "address of the relay server") + sendCmd.Flags().StringP("relay", "r", "", "Address of the relay server ()") } // ------------------------------------------------------ Command ------------------------------------------------------ @@ -39,7 +39,8 @@ var sendCmd = &cobra.Command{ return err } file.RemoveTemporaryFiles(file.SEND_TEMP_FILE_NAME_PREFIX) - if err := validateRendezvousAddressInViper(); err != nil { + + if err := validateRelayInViper(); err != nil { return err } diff --git a/cmd/portal/validate.go b/cmd/portal/validate.go new file mode 100644 index 0000000..3b34635 --- /dev/null +++ b/cmd/portal/validate.go @@ -0,0 +1,43 @@ +package main + +import ( + "errors" + "net" + "regexp" + "strings" + + "github.com/spf13/viper" + "golang.org/x/net/idna" +) + +var ErrInvalidRelay = errors.New("invalid relay provided") + +var ipv6Rex = regexp.MustCompile(`\[(.*?)\]`) + +func stripPort(addr string) string { + split := strings.Split(addr, ":") + if len(split) == 2 { + return split[0] + } + + matches := ipv6Rex.FindStringSubmatch(addr) + if len(matches) >= 2 { + return matches[1] + } + return addr +} + +// validateRelayInViper validates that the `rendezvousAddress` value in viper is a valid hostname or IP +func validateRelayInViper() error { + relayAddr := viper.GetString("relay") + + if ip := net.ParseIP(stripPort(relayAddr)); ip != nil { + return nil + } + + if _, err := idna.Lookup.ToASCII(relayAddr); err == nil { + return nil + } + + return ErrInvalidRelay +} From 7af990abec64243fcbaaecad4648479fd85f7ac1 Mon Sep 17 00:00:00 2001 From: mellonnen Date: Thu, 23 Feb 2023 11:51:30 +0100 Subject: [PATCH 5/5] feat: make relay flag description more clear --- cmd/portal/receive.go | 8 +++++++- cmd/portal/send.go | 7 ++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/cmd/portal/receive.go b/cmd/portal/receive.go index 36d247d..da4fdca 100644 --- a/cmd/portal/receive.go +++ b/cmd/portal/receive.go @@ -18,8 +18,14 @@ import ( // Setup flags. func init() { + // Add subcommand flags (dummy default values as default values are handled through viper) + desc := `Address of relay server. Can be provided as, + - ipv4: 127.0.0.1:8080 + - ipv6: [::1]:8080 + - domain: somedomain.com + ` + receiveCmd.Flags().StringP("relay", "r", "", desc) // Add subcommand flags (dummy default values as default values are handled through viper). - receiveCmd.Flags().StringP("relay", "r", "", "address of the relay server") } // ------------------------------------------------------ Command ------------------------------------------------------ diff --git a/cmd/portal/send.go b/cmd/portal/send.go index 011c39e..487d851 100644 --- a/cmd/portal/send.go +++ b/cmd/portal/send.go @@ -15,7 +15,12 @@ import ( // Set flags. func init() { // Add subcommand flags (dummy default values as default values are handled through viper) - sendCmd.Flags().StringP("relay", "r", "", "Address of the relay server ()") + desc := `Address of relay server. Can be provided as, + - ipv4: 127.0.0.1:8080 + - ipv6: [::1]:8080 + - domain: somedomain.com + ` + sendCmd.Flags().StringP("relay", "r", "", desc) } // ------------------------------------------------------ Command ------------------------------------------------------