Skip to content

Commit

Permalink
feat: overwrite prompt, config file + commands, add relay flag when c…
Browse files Browse the repository at this point in the history
…opying password (#53)
  • Loading branch information
ZinoKader committed Feb 24, 2023
1 parent c3276cc commit 09abbee
Show file tree
Hide file tree
Showing 15 changed files with 583 additions and 228 deletions.
145 changes: 145 additions & 0 deletions cmd/portal/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package main

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/SpatiumPortae/portal/internal/config"
"github.com/alecthomas/chroma/quick"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

func init() {
configCmd.AddCommand(configPathCmd)
configCmd.AddCommand(configViewCmd)
configCmd.AddCommand(configEditCmd)
configCmd.AddCommand(configResetCmd)
}

var configCmd = &cobra.Command{
Use: "config",
Short: "View and configure options",
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
ValidArgs: []string{configPathCmd.Name(), configViewCmd.Name(), configEditCmd.Name(), configResetCmd.Name()},
Run: func(cmd *cobra.Command, args []string) {},
}

var configPathCmd = &cobra.Command{
Use: "path",
Short: "Output the path of the config file",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(viper.ConfigFileUsed())
},
}

var configViewCmd = &cobra.Command{
Use: "view",
Short: "View the configured options",
RunE: func(cmd *cobra.Command, args []string) error {
configPath := viper.ConfigFileUsed()
config, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("config file (%s) could not be read: %w", configPath, err)
}
if err := quick.Highlight(os.Stdout, string(config), "yaml", "terminal256", "onedark"); err != nil {
// Failed to highlight output, output un-highlighted config file contents.
fmt.Println(string(config))
}
return nil
},
}

var configEditCmd = &cobra.Command{
Use: "edit",
Short: "Edit the configuration file",
RunE: func(cmd *cobra.Command, args []string) error {
configPath := viper.ConfigFileUsed()
// Strip arguments from editor variable -- allows exec.Command to lookup the editor executable correctly.
editor, _, _ := strings.Cut(os.Getenv("EDITOR"), " ")
if len(editor) == 0 {
//lint:ignore ST1005 error string is command output
return fmt.Errorf(
"Could not find default editor (is the $EDITOR variable set?)\nOptionally you can open the file (%s) manually", configPath,
)
}

editorCmd := exec.Command(editor, configPath)
editorCmd.Stdin = os.Stdin
editorCmd.Stdout = os.Stdout
editorCmd.Stderr = os.Stderr
if err := editorCmd.Run(); err != nil {
return fmt.Errorf("failed to open file (%s) in editor (%s): %w", configPath, editor, err)
}
return nil
},
}

var configResetCmd = &cobra.Command{
Use: "reset",
Short: "Reset to the default configuration",
RunE: func(cmd *cobra.Command, args []string) error {
configPath := viper.ConfigFileUsed()
err := os.WriteFile(configPath, config.ToYaml(config.GetDefault()), 0)
if err != nil {
return fmt.Errorf("config file (%s) could not be read/written to: %w", configPath, err)
}
return nil
},
}

// -------------------------------------------------- Helper Functions -------------------------------------------------

// initConfig initializes the viper config.
// `config.yml` is created in $HOME/.config/portal if not already existing.
// NOTE: The precedence levels of viper are the following: flags -> config file -> defaults.
func initConfig() {
home, err := homedir.Dir()
if err != nil {
fmt.Println(err)
os.Exit(1)
}

configPath := filepath.Join(home, config.CONFIGS_DIR_NAME, config.PORTAL_CONFIG_DIR_NAME)
viper.AddConfigPath(configPath)
viper.SetConfigName(config.CONFIG_FILE_NAME)
viper.SetConfigType(config.CONFIG_FILE_EXT)

if err := viper.ReadInConfig(); err != nil {
// Create config file if not found.
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
err := os.MkdirAll(configPath, os.ModePerm)
if err != nil {
fmt.Println("Could not create config directory:", err)
os.Exit(1)
}

configFile, err := os.Create(filepath.Join(configPath, fmt.Sprintf("%s.%s", config.CONFIG_FILE_NAME, config.CONFIG_FILE_EXT)))
if err != nil {
fmt.Println("Could not create config file:", err)
os.Exit(1)
}
defer configFile.Close()

_, err = configFile.Write(config.ToYaml(config.GetDefault()))
if err != nil {
fmt.Println("Could not write defaults to config file:", err)
os.Exit(1)
}
} else {
fmt.Println("Could not read config file:", err)
os.Exit(1)
}
}
}

// Sets default viper values.
func setDefaults() {
for k, v := range config.ToMap(config.GetDefault()) {
viper.SetDefault(k, v)
}
}
11 changes: 0 additions & 11 deletions cmd/portal/constants.go

This file was deleted.

152 changes: 20 additions & 132 deletions cmd/portal/main.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
package main

import (
"errors"
"fmt"
"io"
"log"
"net"
"os"
"path/filepath"
"unicode/utf8"

tea "github.com/charmbracelet/bubbletea"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
Expand All @@ -20,6 +15,22 @@ import (
// injected at link time using -ldflags.
var version string

// Initialization of cobra and viper.
func init() {
initConfig()
setDefaults()

rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Log debug information to a file on the format `.portal-[command].log` in the current directory")
// Add cobra subcommands.
rootCmd.AddCommand(sendCmd)
rootCmd.AddCommand(receiveCmd)
rootCmd.AddCommand(serveCmd)
rootCmd.AddCommand(versionCmd)
rootCmd.AddCommand(configCmd)
}

// ------------------------------------------------------ Command ------------------------------------------------------

// rootCmd is the top level `portal` command on which the other subcommands are attached to.
var rootCmd = &cobra.Command{
Use: "portal",
Expand All @@ -31,7 +42,8 @@ var rootCmd = &cobra.Command{
}

var versionCmd = &cobra.Command{
Use: "version",
Use: "version",
Short: "Display the installed version of portal",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(version)
os.Exit(0)
Expand All @@ -45,140 +57,16 @@ 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

// initViperConfig initializes the viper config.
// It creates a `.portal.yml` file at the home directory if it has not been created earlier
// NOTE: The precedence levels of viper are the following: flags -> config file -> defaults
// See https://github.com/spf13/viper#why-viper
func initViperConfig() {
// Set default values
viper.SetDefault("verbose", false)
viper.SetDefault("rendezvousPort", DEFAULT_RENDEZVOUS_PORT)
viper.SetDefault("rendezvousAddress", DEFAULT_RENDEZVOUS_ADDRESS)

// Find home directory.
home, err := homedir.Dir()
if err != nil {
fmt.Println(err)
os.Exit(1)
}

// Search for config in home directory.
viper.AddConfigPath(home)
viper.SetConfigName(CONFIG_FILE_NAME)
viper.SetConfigType("yaml")

if err := viper.ReadInConfig(); err != nil {
// Create config file if not found
// NOTE: perhaps should be an empty file initially, as we would not want default IP to be written to a file on the user host
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
configPath := filepath.Join(home, CONFIG_FILE_NAME)
configFile, err := os.Create(configPath)
if err != nil {
fmt.Println("Could not create config file:", err)
os.Exit(1)
}
defer configFile.Close()
_, err = configFile.Write([]byte(DEFAULT_CONFIG_YAML))
if err != nil {
fmt.Println("Could not write defaults to config file:", err)
os.Exit(1)
}
} else {
fmt.Println("Could not read config file:", err)
os.Exit(1)
}
}
}

// 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
}
// -------------------------------------------------- Helper Functions -------------------------------------------------

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))
if err != nil {
return nil, fmt.Errorf("could not log to the provided file")
return nil, fmt.Errorf("could not log to the provided file: %w", err)
}
return f, nil
}
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
}
Loading

0 comments on commit 09abbee

Please sign in to comment.