|
@@ -5,10 +5,11 @@ import (
|
|
"errors"
|
|
"errors"
|
|
"fmt"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"io/ioutil"
|
|
- "os/user"
|
|
|
|
|
|
+ "os"
|
|
"path/filepath"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strconv"
|
|
|
|
|
|
|
|
+ "github.com/adrg/xdg"
|
|
"github.com/asaskevich/govalidator"
|
|
"github.com/asaskevich/govalidator"
|
|
"github.com/claudiodangelis/qrcp/util"
|
|
"github.com/claudiodangelis/qrcp/util"
|
|
"github.com/manifoldco/promptui"
|
|
"github.com/manifoldco/promptui"
|
|
@@ -21,23 +22,31 @@ type Config struct {
|
|
Port int `json:"port"`
|
|
Port int `json:"port"`
|
|
KeepAlive bool `json:"keepAlive"`
|
|
KeepAlive bool `json:"keepAlive"`
|
|
Path string `json:"path"`
|
|
Path string `json:"path"`
|
|
|
|
+ Secure bool `json:"secure"`
|
|
|
|
+ TLSKey string `json:"tls-key"`
|
|
|
|
+ TLSCert string `json:"tls-cert"`
|
|
|
|
+ Output string `json:"output"`
|
|
}
|
|
}
|
|
|
|
|
|
-func configFile() string {
|
|
|
|
- currentUser, err := user.Current()
|
|
|
|
- if err != nil {
|
|
|
|
- panic(err)
|
|
|
|
- }
|
|
|
|
- return filepath.Join(currentUser.HomeDir, ".qrcp.json")
|
|
|
|
-}
|
|
|
|
|
|
+var configFile string
|
|
|
|
|
|
-type configOptions struct {
|
|
|
|
- interactive bool
|
|
|
|
- listAllInterfaces bool
|
|
|
|
|
|
+// Options of the qrcp configuration
|
|
|
|
+type Options struct {
|
|
|
|
+ Interface string
|
|
|
|
+ Port int
|
|
|
|
+ Path string
|
|
|
|
+ FQDN string
|
|
|
|
+ KeepAlive bool
|
|
|
|
+ Interactive bool
|
|
|
|
+ ListAllInterfaces bool
|
|
|
|
+ Secure bool
|
|
|
|
+ TLSCert string
|
|
|
|
+ TLSKey string
|
|
|
|
+ Output string
|
|
}
|
|
}
|
|
|
|
|
|
-func chooseInterface(opts configOptions) (string, error) {
|
|
|
|
- interfaces, err := util.Interfaces(opts.listAllInterfaces)
|
|
|
|
|
|
+func chooseInterface(opts Options) (string, error) {
|
|
|
|
+ interfaces, err := util.Interfaces(opts.ListAllInterfaces)
|
|
if err != nil {
|
|
if err != nil {
|
|
return "", err
|
|
return "", err
|
|
}
|
|
}
|
|
@@ -45,7 +54,7 @@ func chooseInterface(opts configOptions) (string, error) {
|
|
return "", errors.New("no interfaces found")
|
|
return "", errors.New("no interfaces found")
|
|
}
|
|
}
|
|
|
|
|
|
- if len(interfaces) == 1 && opts.interactive == false {
|
|
|
|
|
|
+ if len(interfaces) == 1 && !opts.Interactive {
|
|
for name := range interfaces {
|
|
for name := range interfaces {
|
|
fmt.Printf("only one interface found: %s, using this one\n", name)
|
|
fmt.Printf("only one interface found: %s, using this one\n", name)
|
|
return name, nil
|
|
return name, nil
|
|
@@ -77,10 +86,10 @@ func chooseInterface(opts configOptions) (string, error) {
|
|
}
|
|
}
|
|
|
|
|
|
// Load a new configuration
|
|
// Load a new configuration
|
|
-func Load(opts configOptions) (Config, error) {
|
|
|
|
|
|
+func Load(opts Options) (Config, error) {
|
|
var cfg Config
|
|
var cfg Config
|
|
// Read the configuration file, if it exists
|
|
// Read the configuration file, if it exists
|
|
- if file, err := ioutil.ReadFile(configFile()); err == nil {
|
|
|
|
|
|
+ if file, err := ioutil.ReadFile(configFile); err == nil {
|
|
// Read the config
|
|
// Read the config
|
|
if err := json.Unmarshal(file, &cfg); err != nil {
|
|
if err := json.Unmarshal(file, &cfg); err != nil {
|
|
return cfg, err
|
|
return cfg, err
|
|
@@ -102,17 +111,21 @@ func Load(opts configOptions) (Config, error) {
|
|
}
|
|
}
|
|
|
|
|
|
// Wizard starts an interactive configuration managements
|
|
// Wizard starts an interactive configuration managements
|
|
-func Wizard() error {
|
|
|
|
|
|
+func Wizard(path string, listAllInterfaces bool) error {
|
|
|
|
+ if err := setConfigFile(path); err != nil {
|
|
|
|
+ return err
|
|
|
|
+ }
|
|
var cfg Config
|
|
var cfg Config
|
|
- if file, err := ioutil.ReadFile(configFile()); err == nil {
|
|
|
|
|
|
+ if file, err := ioutil.ReadFile(configFile); err == nil {
|
|
// Read the config
|
|
// Read the config
|
|
if err := json.Unmarshal(file, &cfg); err != nil {
|
|
if err := json.Unmarshal(file, &cfg); err != nil {
|
|
return err
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Ask for interface
|
|
// Ask for interface
|
|
- opts := configOptions{
|
|
|
|
- interactive: true,
|
|
|
|
|
|
+ opts := Options{
|
|
|
|
+ Interactive: true,
|
|
|
|
+ ListAllInterfaces: listAllInterfaces,
|
|
}
|
|
}
|
|
iface, err := chooseInterface(opts)
|
|
iface, err := chooseInterface(opts)
|
|
if err != nil {
|
|
if err != nil {
|
|
@@ -121,7 +134,7 @@ func Wizard() error {
|
|
cfg.Interface = iface
|
|
cfg.Interface = iface
|
|
// Ask for fully qualified domain name
|
|
// Ask for fully qualified domain name
|
|
validateFqdn := func(input string) error {
|
|
validateFqdn := func(input string) error {
|
|
- if input != "" && govalidator.IsDNSName(input) == false {
|
|
|
|
|
|
+ if input != "" && !govalidator.IsDNSName(input) {
|
|
return errors.New("invalid domain")
|
|
return errors.New("invalid domain")
|
|
}
|
|
}
|
|
return nil
|
|
return nil
|
|
@@ -136,9 +149,9 @@ func Wizard() error {
|
|
}
|
|
}
|
|
// Ask for port
|
|
// Ask for port
|
|
validatePort := func(input string) error {
|
|
validatePort := func(input string) error {
|
|
- _, err := strconv.ParseInt(input, 10, 16)
|
|
|
|
|
|
+ _, err := strconv.ParseUint(input, 10, 16)
|
|
if err != nil {
|
|
if err != nil {
|
|
- return errors.New("Invalid number")
|
|
|
|
|
|
+ return errors.New("invalid number")
|
|
}
|
|
}
|
|
return nil
|
|
return nil
|
|
}
|
|
}
|
|
@@ -149,10 +162,39 @@ func Wizard() error {
|
|
Default: fmt.Sprintf("%d", cfg.Port),
|
|
Default: fmt.Sprintf("%d", cfg.Port),
|
|
}
|
|
}
|
|
if promptPortResultString, err := promptPort.Run(); err == nil {
|
|
if promptPortResultString, err := promptPort.Run(); err == nil {
|
|
- if port, err := strconv.ParseInt(promptPortResultString, 10, 16); err == nil {
|
|
|
|
|
|
+ if port, err := strconv.ParseUint(promptPortResultString, 10, 16); err == nil {
|
|
cfg.Port = int(port)
|
|
cfg.Port = int(port)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
+ validateIsDir := func(input string) error {
|
|
|
|
+ if input == "" {
|
|
|
|
+ return nil
|
|
|
|
+ }
|
|
|
|
+ path, err := filepath.Abs(input)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return err
|
|
|
|
+ }
|
|
|
|
+ f, err := os.Stat(path)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return err
|
|
|
|
+ }
|
|
|
|
+ if !f.IsDir() {
|
|
|
|
+ return errors.New("path is not a directory")
|
|
|
|
+ }
|
|
|
|
+ return nil
|
|
|
|
+ }
|
|
|
|
+ promptOutput := promptui.Prompt{
|
|
|
|
+ Label: "Choose default output directory for received files, empty does not set a default",
|
|
|
|
+ Default: cfg.Output,
|
|
|
|
+ Validate: validateIsDir,
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if promptOutputResultString, err := promptOutput.Run(); err == nil {
|
|
|
|
+ if promptOutputResultString != "" {
|
|
|
|
+ p, _ := filepath.Abs(promptOutputResultString)
|
|
|
|
+ cfg.Output = p
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
|
|
// Ask for path
|
|
// Ask for path
|
|
promptPath := promptui.Prompt{
|
|
promptPath := promptui.Prompt{
|
|
@@ -176,6 +218,55 @@ func Wizard() error {
|
|
} else {
|
|
} else {
|
|
cfg.KeepAlive = false
|
|
cfg.KeepAlive = false
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+ // TLS
|
|
|
|
+ promptSecure := promptui.Select{
|
|
|
|
+ Items: []string{"No", "Yes"},
|
|
|
|
+ Label: "Should files be securely transferred with HTTPS?",
|
|
|
|
+ }
|
|
|
|
+ if _, promptSecureResultString, err := promptSecure.Run(); err == nil {
|
|
|
|
+ if promptSecureResultString == "Yes" {
|
|
|
|
+ cfg.Secure = true
|
|
|
|
+ } else {
|
|
|
|
+ cfg.Secure = false
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ pathIsReadable := func(input string) error {
|
|
|
|
+ if input == "" {
|
|
|
|
+ return nil
|
|
|
|
+ }
|
|
|
|
+ path, err := filepath.Abs(util.Expand(input))
|
|
|
|
+ if err != nil {
|
|
|
|
+ return err
|
|
|
|
+ }
|
|
|
|
+ fmt.Println(path)
|
|
|
|
+ fileinfo, err := os.Stat(path)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return err
|
|
|
|
+ }
|
|
|
|
+ if fileinfo.Mode().IsDir() {
|
|
|
|
+ return fmt.Errorf(fmt.Sprintf("%s is a directory", input))
|
|
|
|
+ }
|
|
|
|
+ return nil
|
|
|
|
+ }
|
|
|
|
+ // TLS Cert
|
|
|
|
+ promptTLSCert := promptui.Prompt{
|
|
|
|
+ Label: "Choose TLS certificate path. Empty if not using HTTPS.",
|
|
|
|
+ Default: cfg.TLSCert,
|
|
|
|
+ Validate: pathIsReadable,
|
|
|
|
+ }
|
|
|
|
+ if promptTLSCertString, err := promptTLSCert.Run(); err == nil {
|
|
|
|
+ cfg.TLSCert = util.Expand(promptTLSCertString)
|
|
|
|
+ }
|
|
|
|
+ // TLS key
|
|
|
|
+ promptTLSKey := promptui.Prompt{
|
|
|
|
+ Label: "Choose TLS certificate key. Empty if not using HTTPS.",
|
|
|
|
+ Default: cfg.TLSKey,
|
|
|
|
+ Validate: pathIsReadable,
|
|
|
|
+ }
|
|
|
|
+ if promptTLSKeyString, err := promptTLSKey.Run(); err == nil {
|
|
|
|
+ cfg.TLSKey = util.Expand(promptTLSKeyString)
|
|
}
|
|
}
|
|
// Write it down
|
|
// Write it down
|
|
if err := write(cfg); err != nil {
|
|
if err := write(cfg); err != nil {
|
|
@@ -195,40 +286,104 @@ func write(cfg Config) error {
|
|
if err != nil {
|
|
if err != nil {
|
|
return err
|
|
return err
|
|
}
|
|
}
|
|
- if err := ioutil.WriteFile(configFile(), j, 0644); err != nil {
|
|
|
|
|
|
+ if err := ioutil.WriteFile(configFile, j, 0644); err != nil {
|
|
|
|
+ return err
|
|
|
|
+ }
|
|
|
|
+ return nil
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func pathExists(path string) bool {
|
|
|
|
+ _, err := os.Stat(path)
|
|
|
|
+ return !os.IsNotExist(err)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func setConfigFile(path string) error {
|
|
|
|
+ // If not explicitly set then use the default
|
|
|
|
+ if path == "" {
|
|
|
|
+ // First try legacy location
|
|
|
|
+ var legacyConfigFile = filepath.Join(xdg.Home, ".qrcp.json")
|
|
|
|
+ if pathExists(legacyConfigFile) {
|
|
|
|
+ configFile = legacyConfigFile
|
|
|
|
+ return nil
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Else use modern location, first ensuring that the directory
|
|
|
|
+ // exists
|
|
|
|
+ var configDir = filepath.Join(xdg.ConfigHome, "qrcp")
|
|
|
|
+ if !pathExists(configDir) {
|
|
|
|
+ if err := os.Mkdir(configDir, 0744); err != nil {
|
|
|
|
+ panic(err)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ configFile = filepath.Join(configDir, "config.json")
|
|
|
|
+ return nil
|
|
|
|
+ }
|
|
|
|
+ absolutepath, err := filepath.Abs(path)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return err
|
|
|
|
+ }
|
|
|
|
+ fileinfo, err := os.Stat(absolutepath)
|
|
|
|
+ if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
return err
|
|
}
|
|
}
|
|
|
|
+ if fileinfo != nil && fileinfo.IsDir() {
|
|
|
|
+ return fmt.Errorf("%s is not a file", absolutepath)
|
|
|
|
+ }
|
|
|
|
+ configFile = absolutepath
|
|
return nil
|
|
return nil
|
|
}
|
|
}
|
|
|
|
|
|
// New returns a new configuration struct. It loads defaults, then overrides
|
|
// New returns a new configuration struct. It loads defaults, then overrides
|
|
// values if any.
|
|
// values if any.
|
|
-func New(iface string, port int, path string, fqdn string, keepAlive bool, listAllInterfaces bool) (Config, error) {
|
|
|
|
- // Load saved file / defults
|
|
|
|
- cfg, err := Load(configOptions{listAllInterfaces: listAllInterfaces})
|
|
|
|
|
|
+func New(path string, opts Options) (Config, error) {
|
|
|
|
+ var cfg Config
|
|
|
|
+ // Set configFile
|
|
|
|
+ if err := setConfigFile(path); err != nil {
|
|
|
|
+ return cfg, err
|
|
|
|
+ }
|
|
|
|
+ // Load saved file / defaults
|
|
|
|
+ cfg, err := Load(opts)
|
|
if err != nil {
|
|
if err != nil {
|
|
return cfg, err
|
|
return cfg, err
|
|
}
|
|
}
|
|
- if iface != "" {
|
|
|
|
- cfg.Interface = iface
|
|
|
|
|
|
+ if opts.Interface != "" {
|
|
|
|
+ cfg.Interface = opts.Interface
|
|
}
|
|
}
|
|
- if fqdn != "" {
|
|
|
|
- if govalidator.IsDNSName(fqdn) == false {
|
|
|
|
|
|
+ if opts.FQDN != "" {
|
|
|
|
+ if !govalidator.IsDNSName(opts.FQDN) {
|
|
return cfg, errors.New("invalid value for fully-qualified domain name")
|
|
return cfg, errors.New("invalid value for fully-qualified domain name")
|
|
}
|
|
}
|
|
- cfg.FQDN = fqdn
|
|
|
|
|
|
+ cfg.FQDN = opts.FQDN
|
|
}
|
|
}
|
|
- if port != 0 {
|
|
|
|
- if port > 65535 {
|
|
|
|
- return cfg, errors.New("invalid value for port")
|
|
|
|
|
|
+ if opts.Port != 0 {
|
|
|
|
+ cfg.Port = opts.Port
|
|
|
|
+ } else if portVal, ok := os.LookupEnv("QRCP_PORT"); ok {
|
|
|
|
+ port, err := strconv.Atoi(portVal)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return cfg, errors.New("could not parse port from environment variable QRCP_PORT")
|
|
}
|
|
}
|
|
cfg.Port = port
|
|
cfg.Port = port
|
|
}
|
|
}
|
|
- if keepAlive {
|
|
|
|
|
|
+ if cfg.Port != 0 && !govalidator.IsPort(fmt.Sprintf("%d", cfg.Port)) {
|
|
|
|
+ return cfg, fmt.Errorf("%d is not a valid port", cfg.Port)
|
|
|
|
+ }
|
|
|
|
+ if opts.KeepAlive {
|
|
cfg.KeepAlive = true
|
|
cfg.KeepAlive = true
|
|
}
|
|
}
|
|
- if path != "" {
|
|
|
|
- cfg.Path = path
|
|
|
|
|
|
+ if opts.Path != "" {
|
|
|
|
+ cfg.Path = opts.Path
|
|
|
|
+ }
|
|
|
|
+ if opts.Secure {
|
|
|
|
+ cfg.Secure = true
|
|
|
|
+ }
|
|
|
|
+ if opts.TLSCert != "" {
|
|
|
|
+ cfg.TLSCert = opts.TLSCert
|
|
|
|
+ }
|
|
|
|
+ if opts.TLSKey != "" {
|
|
|
|
+ cfg.TLSKey = opts.TLSKey
|
|
|
|
+ }
|
|
|
|
+ if opts.Output != "" {
|
|
|
|
+ cfg.Output = opts.Output
|
|
}
|
|
}
|
|
return cfg, nil
|
|
return cfg, nil
|
|
}
|
|
}
|