Browse Source

update to 0.9.1

liuyuqi-dellpc 1 year ago
parent
commit
053b749ae0
15 changed files with 528 additions and 118 deletions
  1. 67 0
      cmd/completion.go
  2. 1 1
      cmd/config.go
  3. 11 0
      cmd/qrcp.go
  4. 37 4
      cmd/receive.go
  5. 35 2
      cmd/send.go
  6. 194 39
      config/config.go
  7. 2 0
      go.mod
  8. 8 0
      go.sum
  9. 1 1
      logger/logger.go
  10. 2 1
      pages/pages.go
  11. 14 8
      qr/qr.go
  12. 113 39
      server/server.go
  13. 6 2
      server/tcpkeepalivelistener.go
  14. 1 1
      util/net.go
  15. 36 20
      util/util.go

+ 67 - 0
cmd/completion.go

@@ -0,0 +1,67 @@
+package cmd
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+// completionCmd represents the completion command
+var completionCmd = &cobra.Command{
+	Use:   "completion [bash|zsh|fish|powershell]",
+	Short: "Generate completion script",
+	Long: `To load completions:
+
+Bash:
+
+$ source <(qrcp completion bash)
+
+# To load completions for each session, execute once:
+Linux:
+  $ qrcp completion bash > /etc/bash_completion.d/qrcp
+MacOS:
+  $ qrcp completion bash > /usr/local/etc/bash_completion.d/qrcp
+
+Zsh:
+
+# If shell completion is not already enabled in your environment you will need
+# to enable it.  You can execute the following once:
+
+$ echo "autoload -U compinit; compinit" >> ~/.zshrc
+
+# To load completions for each session, execute once:
+$ qrcp completion zsh > "${fpath[1]}/_qrcp"
+
+# You will need to start a new shell for this setup to take effect.
+
+Fish:
+
+$ qrcp completion fish | source
+
+# To load completions for each session, execute once:
+$ qrcp completion fish > ~/.config/fish/completions/qrcp.fish
+`,
+	DisableFlagsInUseLine: true,
+	ValidArgs:             []string{"bash", "zsh", "fish", "powershell"},
+	Args:                  cobra.ExactValidArgs(1),
+	Run: func(cmd *cobra.Command, args []string) {
+		switch args[0] {
+		case "bash":
+			if err := cmd.Root().GenBashCompletion(os.Stdout); err != nil {
+				panic(err)
+			}
+		case "zsh":
+			if err := cmd.Root().GenZshCompletion(os.Stdout); err != nil {
+				panic(err)
+			}
+		case "fish":
+			if err := cmd.Root().GenFishCompletion(os.Stdout, true); err != nil {
+				panic(err)
+			}
+		case "powershell":
+			if err := cmd.Root().GenPowerShellCompletion(os.Stdout); err != nil {
+				panic(err)
+			}
+		}
+	},
+}

+ 1 - 1
cmd/config.go

@@ -6,7 +6,7 @@ import (
 )
 
 func configCmdFunc(command *cobra.Command, args []string) error {
-	return config.Wizard()
+	return config.Wizard(configFlag, listallinterfacesFlag)
 }
 
 var configCmd = &cobra.Command{

+ 11 - 0
cmd/qrcp.go

@@ -9,6 +9,7 @@ func init() {
 	rootCmd.AddCommand(receiveCmd)
 	rootCmd.AddCommand(configCmd)
 	rootCmd.AddCommand(versionCmd)
+	rootCmd.AddCommand(completionCmd)
 	// Global command flags
 	rootCmd.PersistentFlags().BoolVarP(&quietFlag, "quiet", "q", false, "only print errors")
 	rootCmd.PersistentFlags().BoolVarP(&keepaliveFlag, "keep-alive", "k", false, "keep server alive after transferring")
@@ -18,6 +19,11 @@ func init() {
 	rootCmd.PersistentFlags().StringVarP(&interfaceFlag, "interface", "i", "", "network interface to use for the server")
 	rootCmd.PersistentFlags().StringVarP(&fqdnFlag, "fqdn", "d", "", "fully-qualified domain name to use for the resulting URLs")
 	rootCmd.PersistentFlags().BoolVarP(&zipFlag, "zip", "z", false, "zip content before transferring")
+	rootCmd.PersistentFlags().StringVarP(&configFlag, "config", "c", "", "path to the config file, defaults to $XDG_CONFIG_HOME/qrcp/config.json")
+	rootCmd.PersistentFlags().BoolVarP(&browserFlag, "browser", "b", false, "display the QR code in a browser window")
+	rootCmd.PersistentFlags().BoolVarP(&secureFlag, "secure", "s", false, "use https connection")
+	rootCmd.PersistentFlags().StringVar(&tlscertFlag, "tls-cert", "", "path to TLS certificate to use with HTTPS")
+	rootCmd.PersistentFlags().StringVar(&tlskeyFlag, "tls-key", "", "path to TLS private key to use with HTTPS")
 	// Receive command flags
 	receiveCmd.PersistentFlags().StringVarP(&outputFlag, "output", "o", "", "output directory for receiving files")
 }
@@ -32,6 +38,11 @@ var quietFlag bool
 var fqdnFlag string
 var pathFlag string
 var listallinterfacesFlag bool
+var configFlag string
+var browserFlag bool
+var secureFlag bool
+var tlscertFlag string
+var tlskeyFlag string
 
 // The root command (`qrcp`) is like a shortcut of the `send` command
 var rootCmd = &cobra.Command{

+ 37 - 4
cmd/receive.go

@@ -1,17 +1,32 @@
 package cmd
 
 import (
+	"fmt"
+
 	"github.com/claudiodangelis/qrcp/config"
 	"github.com/claudiodangelis/qrcp/logger"
 	"github.com/claudiodangelis/qrcp/qr"
 	"github.com/claudiodangelis/qrcp/server"
+	"github.com/eiannone/keyboard"
 	"github.com/spf13/cobra"
 )
 
 func receiveCmdFunc(command *cobra.Command, args []string) error {
 	log := logger.New(quietFlag)
 	// Load configuration
-	cfg, err := config.New(interfaceFlag, portFlag, pathFlag, fqdnFlag, keepaliveFlag, listallinterfacesFlag)
+	configOptions := config.Options{
+		Interface:         interfaceFlag,
+		Port:              portFlag,
+		Path:              pathFlag,
+		FQDN:              fqdnFlag,
+		KeepAlive:         keepaliveFlag,
+		ListAllInterfaces: listallinterfacesFlag,
+		Secure:            secureFlag,
+		TLSCert:           tlscertFlag,
+		TLSKey:            tlskeyFlag,
+		Output:            outputFlag,
+	}
+	cfg, err := config.New(configFlag, configOptions)
 	if err != nil {
 		return err
 	}
@@ -21,14 +36,32 @@ func receiveCmdFunc(command *cobra.Command, args []string) error {
 		return err
 	}
 	// Sets the output directory
-	if err := srv.ReceiveTo(outputFlag); err != nil {
+	if err := srv.ReceiveTo(cfg.Output); err != nil {
 		return err
 	}
 	// Prints the URL to scan to screen
-	log.Print("Scan the following URL with a QR reader to start the file transfer:")
+	log.Print(`Scan the following URL with a QR reader to start the file transfer, press CTRL+C or "q" to exit:`)
 	log.Print(srv.ReceiveURL)
 	// Renders the QR
 	qr.RenderString(srv.ReceiveURL)
+	if browserFlag {
+		srv.DisplayQR(srv.ReceiveURL)
+	}
+	if err := keyboard.Open(); err == nil {
+		defer func() {
+			keyboard.Close()
+		}()
+		go func() {
+			for {
+				char, key, _ := keyboard.GetKey()
+				if string(char) == "q" || key == keyboard.KeyCtrlC {
+					srv.Shutdown()
+				}
+			}
+		}()
+	} else {
+		log.Print(fmt.Sprintf("Warning: keyboard not detected: %v", err))
+	}
 	if err := srv.Wait(); err != nil {
 		return err
 	}
@@ -39,7 +72,7 @@ var receiveCmd = &cobra.Command{
 	Use:     "receive",
 	Aliases: []string{"r"},
 	Short:   "Receive one or more files",
-	Long:    "Receive one or more files. If not specified with the --output flag, the current working directory will be used as a destination.",
+	Long:    "Receive one or more files. The destination directory can be set with the config wizard, or by passing the --output flag. If none of the above are set, the current working directory will be used as a destination directory.",
 	Example: `# Receive files in the current directory
 qrcp receive
 # Receive files in a specific directory

+ 35 - 2
cmd/send.go

@@ -1,10 +1,13 @@
 package cmd
 
 import (
+	"fmt"
+
 	"github.com/claudiodangelis/qrcp/config"
 	"github.com/claudiodangelis/qrcp/logger"
 	"github.com/claudiodangelis/qrcp/payload"
 	"github.com/claudiodangelis/qrcp/qr"
+	"github.com/eiannone/keyboard"
 
 	"github.com/claudiodangelis/qrcp/server"
 	"github.com/spf13/cobra"
@@ -16,7 +19,19 @@ func sendCmdFunc(command *cobra.Command, args []string) error {
 	if err != nil {
 		return err
 	}
-	cfg, err := config.New(interfaceFlag, portFlag, pathFlag, fqdnFlag, keepaliveFlag, listallinterfacesFlag)
+	// Load configuration
+	configOptions := config.Options{
+		Interface:         interfaceFlag,
+		Port:              portFlag,
+		Path:              pathFlag,
+		FQDN:              fqdnFlag,
+		KeepAlive:         keepaliveFlag,
+		ListAllInterfaces: listallinterfacesFlag,
+		Secure:            secureFlag,
+		TLSCert:           tlscertFlag,
+		TLSKey:            tlskeyFlag,
+	}
+	cfg, err := config.New(configFlag, configOptions)
 	if err != nil {
 		return err
 	}
@@ -26,9 +41,27 @@ func sendCmdFunc(command *cobra.Command, args []string) error {
 	}
 	// Sets the payload
 	srv.Send(payload)
-	log.Print("Scan the following URL with a QR reader to start the file transfer:")
+	log.Print(`Scan the following URL with a QR reader to start the file transfer, press CTRL+C or "q" to exit:`)
 	log.Print(srv.SendURL)
 	qr.RenderString(srv.SendURL)
+	if browserFlag {
+		srv.DisplayQR(srv.SendURL)
+	}
+	if err := keyboard.Open(); err == nil {
+		defer func() {
+			keyboard.Close()
+		}()
+		go func() {
+			for {
+				char, key, _ := keyboard.GetKey()
+				if string(char) == "q" || key == keyboard.KeyCtrlC {
+					srv.Shutdown()
+				}
+			}
+		}()
+	} else {
+		log.Print(fmt.Sprintf("Warning: keyboard not detected: %v", err))
+	}
 	if err := srv.Wait(); err != nil {
 		return err
 	}

+ 194 - 39
config/config.go

@@ -5,10 +5,11 @@ import (
 	"errors"
 	"fmt"
 	"io/ioutil"
-	"os/user"
+	"os"
 	"path/filepath"
 	"strconv"
 
+	"github.com/adrg/xdg"
 	"github.com/asaskevich/govalidator"
 	"github.com/claudiodangelis/qrcp/util"
 	"github.com/manifoldco/promptui"
@@ -21,23 +22,31 @@ type Config struct {
 	Port      int    `json:"port"`
 	KeepAlive bool   `json:"keepAlive"`
 	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 {
 		return "", err
 	}
@@ -45,7 +54,7 @@ func chooseInterface(opts configOptions) (string, error) {
 		return "", errors.New("no interfaces found")
 	}
 
-	if len(interfaces) == 1 && opts.interactive == false {
+	if len(interfaces) == 1 && !opts.Interactive {
 		for name := range interfaces {
 			fmt.Printf("only one interface found: %s, using this one\n", name)
 			return name, nil
@@ -77,10 +86,10 @@ func chooseInterface(opts configOptions) (string, error) {
 }
 
 // Load a new configuration
-func Load(opts configOptions) (Config, error) {
+func Load(opts Options) (Config, error) {
 	var cfg Config
 	// 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
 		if err := json.Unmarshal(file, &cfg); err != nil {
 			return cfg, err
@@ -102,17 +111,21 @@ func Load(opts configOptions) (Config, error) {
 }
 
 // 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
-	if file, err := ioutil.ReadFile(configFile()); err == nil {
+	if file, err := ioutil.ReadFile(configFile); err == nil {
 		// Read the config
 		if err := json.Unmarshal(file, &cfg); err != nil {
 			return err
 		}
 	}
 	// Ask for interface
-	opts := configOptions{
-		interactive: true,
+	opts := Options{
+		Interactive:       true,
+		ListAllInterfaces: listAllInterfaces,
 	}
 	iface, err := chooseInterface(opts)
 	if err != nil {
@@ -121,7 +134,7 @@ func Wizard() error {
 	cfg.Interface = iface
 	// Ask for fully qualified domain name
 	validateFqdn := func(input string) error {
-		if input != "" && govalidator.IsDNSName(input) == false {
+		if input != "" && !govalidator.IsDNSName(input) {
 			return errors.New("invalid domain")
 		}
 		return nil
@@ -136,9 +149,9 @@ func Wizard() error {
 	}
 	// Ask for port
 	validatePort := func(input string) error {
-		_, err := strconv.ParseInt(input, 10, 16)
+		_, err := strconv.ParseUint(input, 10, 16)
 		if err != nil {
-			return errors.New("Invalid number")
+			return errors.New("invalid number")
 		}
 		return nil
 	}
@@ -149,10 +162,39 @@ func Wizard() error {
 		Default:  fmt.Sprintf("%d", cfg.Port),
 	}
 	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)
 		}
 	}
+	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
 	promptPath := promptui.Prompt{
@@ -176,6 +218,55 @@ func Wizard() error {
 		} else {
 			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
 	if err := write(cfg); err != nil {
@@ -195,40 +286,104 @@ func write(cfg Config) error {
 	if err != nil {
 		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
 	}
+	if fileinfo != nil && fileinfo.IsDir() {
+		return fmt.Errorf("%s is not a file", absolutepath)
+	}
+	configFile = absolutepath
 	return nil
 }
 
 // New returns a new configuration struct. It loads defaults, then overrides
 // 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 {
 		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")
 		}
-		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
 	}
-	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
 	}
-	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
 }

+ 2 - 0
go.mod

@@ -3,7 +3,9 @@ module github.com/claudiodangelis/qrcp
 go 1.14
 
 require (
+	github.com/adrg/xdg v0.3.2
 	github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496
+	github.com/eiannone/keyboard v0.0.0-20200508000154-caf4b762e807
 	github.com/fatih/color v1.9.0 // indirect
 	github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd
 	github.com/jhoonb/archivex v0.0.0-20180718040744-0488e4ce1681

+ 8 - 0
go.sum

@@ -1,6 +1,8 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/adrg/xdg v0.3.2 h1:GUSGQ5pHdev83AYhDSS1A/CX+0JIsxbiWtow2DSA+RU=
+github.com/adrg/xdg v0.3.2/go.mod h1:7I2hH/IT30IsupOpKZ5ue7/qNi3CoKzD6tL3HwpaRMQ=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
@@ -22,9 +24,12 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
 github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/eiannone/keyboard v0.0.0-20200508000154-caf4b762e807 h1:jdjd5e68T4R/j4PWxfZqcKY8KtT9oo8IPNVuV4bSXDQ=
+github.com/eiannone/keyboard v0.0.0-20200508000154-caf4b762e807/go.mod h1:Xoiu5VdKMvbRgHuY7+z64lhu/7lvax/22nzASF6GrO8=
 github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
 github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -115,8 +120,10 @@ github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb6
 github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
@@ -168,4 +175,5 @@ gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
 gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

+ 1 - 1
logger/logger.go

@@ -6,7 +6,7 @@ import (
 
 // Print prints its argument if the --quiet flag is not passed
 func (l Logger) Print(args ...interface{}) {
-	if l.quiet == false {
+	if !l.quiet {
 		fmt.Println(args...)
 	}
 }

+ 2 - 1
pages/pages.go

@@ -138,7 +138,8 @@ var Upload = `
             </svg>
         </div>
         <div class="row">
-            <form id="upload-form">
+            <form id="upload-form" onsubmit="submit.value = 'Transferring file, please wait.';
+                submit.disabled = true; return true;">
                 <h3>Send files or text</h3>
                 <div class="form-group">
                     <label for="files">

+ 14 - 8
qr/qr.go

@@ -1,21 +1,27 @@
 package qr
 
 import (
+	"fmt"
+	"image"
 	"log"
-	"os"
-	"path/filepath"
 
 	"github.com/skip2/go-qrcode"
 )
 
 // RenderString as a QR code
 func RenderString(s string) {
-	dir, err1 := filepath.Abs(filepath.Dir(os.Args[0]))
-	if err1 != nil {
-		log.Fatal(err1)
+	q, err := qrcode.New(s, qrcode.Medium)
+	if err != nil {
+		log.Fatal(err)
 	}
-	err2 := qrcode.WriteFile(s, qrcode.Medium, 200, dir+"/tmp.png")
-	if err2 != nil {
-		panic(err2)
+	fmt.Println(q.ToSmallString(false))
+}
+
+// RenderImage returns a QR code as an image.Image
+func RenderImage(s string) image.Image {
+	q, err := qrcode.New(s, qrcode.Medium)
+	if err != nil {
+		log.Fatal(err)
 	}
+	return q.Image(256)
 }

+ 113 - 39
server/server.go

@@ -2,17 +2,23 @@ package server
 
 import (
 	"context"
+	"crypto/tls"
 	"fmt"
+	"image/jpeg"
 	"io"
 	"log"
 	"net"
 	"net/http"
 	"os"
+	"os/exec"
 	"os/signal"
 	"path/filepath"
+	"runtime"
 	"strings"
 	"sync"
 
+	"github.com/claudiodangelis/qrcp/qr"
+
 	"github.com/claudiodangelis/qrcp/config"
 	"github.com/claudiodangelis/qrcp/pages"
 	"github.com/claudiodangelis/qrcp/payload"
@@ -22,6 +28,7 @@ import (
 
 // Server is the server
 type Server struct {
+	BaseURL string
 	// SendURL is the URL used to send the file
 	SendURL string
 	// ReceiveURL is the URL used to Receive the file
@@ -59,6 +66,19 @@ func (s *Server) Send(p payload.Payload) {
 	s.expectParallelRequests = true
 }
 
+// DisplayQR creates a handler for serving the QR code in the browser
+func (s *Server) DisplayQR(url string) {
+	const PATH = "/qr"
+	qrImg := qr.RenderImage(url)
+	http.HandleFunc(PATH, func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "image/jpeg")
+		if err := jpeg.Encode(w, qrImg, nil); err != nil {
+			panic(err)
+		}
+	})
+	openBrowser(s.BaseURL + PATH)
+}
+
 // Wait for transfer to be completed, it waits forever if kept awlive
 func (s Server) Wait() error {
 	<-s.stopChannel
@@ -66,13 +86,21 @@ func (s Server) Wait() error {
 		log.Println(err)
 	}
 	if s.payload.DeleteAfterTransfer {
-		s.payload.Delete()
+		if err := s.payload.Delete(); err != nil {
+			panic(err)
+		}
 	}
 	return nil
 }
 
+// Shutdown the server
+func (s Server) Shutdown() {
+	s.stopChannel <- true
+}
+
 // New instance of the server
 func New(cfg *config.Config) (*Server, error) {
+
 	app := &Server{}
 	// Get the address of the configured interface to bind the server to
 	bind, err := util.GetInterfaceAddress(cfg.Interface)
@@ -108,18 +136,37 @@ func New(cfg *config.Config) (*Server, error) {
 	if cfg.FQDN != "" {
 		hostname = fmt.Sprintf("%s:%d", cfg.FQDN, port)
 	}
-	// Set send and receive URLs
-	app.SendURL = fmt.Sprintf("http://%s/send/%s",
-		hostname, path)
-	app.ReceiveURL = fmt.Sprintf("http://%s/receive/%s",
-		hostname, path)
+	// Set URLs
+	protocol := "http"
+	if cfg.Secure {
+		protocol = "https"
+	}
+	app.BaseURL = fmt.Sprintf("%s://%s", protocol, hostname)
+	app.SendURL = fmt.Sprintf("%s/send/%s",
+		app.BaseURL, path)
+	app.ReceiveURL = fmt.Sprintf("%s/receive/%s",
+		app.BaseURL, path)
 	// Create a server
-	httpserver := &http.Server{Addr: host}
+	httpserver := &http.Server{
+		Addr: host,
+		TLSConfig: &tls.Config{
+			MinVersion:               tls.VersionTLS12,
+			CurvePreferences:         []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
+			PreferServerCipherSuites: true,
+			CipherSuites: []uint16{
+				tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+				tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
+				tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
+				tls.TLS_RSA_WITH_AES_256_CBC_SHA,
+			},
+		},
+		TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
+	}
 	// Create channel to send message to stop server
 	app.stopChannel = make(chan bool)
 	// Create cookie used to verify request is coming from first client to connect
 	cookie := http.Cookie{Name: "qrcp", Value: ""}
-	// Gracefully shutdown when an OS signal is received
+	// Gracefully shutdown when an OS signal is received or when "q" is pressed
 	sig := make(chan os.Signal, 1)
 	signal.Notify(sig, os.Interrupt)
 	go func() {
@@ -135,37 +182,40 @@ func New(cfg *config.Config) (*Server, error) {
 	// Create handlers
 	// Send handler (sends file to caller)
 	http.HandleFunc("/send/"+path, func(w http.ResponseWriter, r *http.Request) {
-		if cookie.Value == "" {
-			if !strings.HasPrefix(r.Header.Get("User-Agent"), "Mozilla") {
-				http.Error(w, "", http.StatusOK)
-				return
-			}
-			initCookie.Do(func() {
-				value, err := util.GetSessionID()
+		if !cfg.KeepAlive && strings.HasPrefix(r.Header.Get("User-Agent"), "Mozilla") {
+			fmt.Println("new req...")
+			if cookie.Value == "" {
+				initCookie.Do(func() {
+					value, err := util.GetSessionID()
+					if err != nil {
+						log.Println("Unable to generate session ID", err)
+						app.stopChannel <- true
+						return
+					}
+					cookie.Value = value
+					http.SetCookie(w, &cookie)
+				})
+			} else {
+				// Check for the expected cookie and value
+				// If it is missing or doesn't match
+				// return a 400 status
+				rcookie, err := r.Cookie(cookie.Name)
 				if err != nil {
-					log.Println("Unable to generate session ID", err)
-					app.stopChannel <- true
+					http.Error(w, err.Error(), http.StatusBadRequest)
 					return
 				}
-				cookie.Value = value
-				http.SetCookie(w, &cookie)
-			})
-		} else {
-			// Check for the expected cookie and value
-			// If it is missing or doesn't match
-			// return a 404 status
-			rcookie, err := r.Cookie(cookie.Name)
-			if err != nil || rcookie.Value != cookie.Value {
-				http.Error(w, "", http.StatusNotFound)
-				return
+				if rcookie.Value != cookie.Value {
+					http.Error(w, "mismatching cookie", http.StatusBadRequest)
+					return
+				}
+				// If the cookie exits and matches
+				// this is an aadditional request.
+				// Increment the waitgroup
+				waitgroup.Add(1)
 			}
-			// If the cookie exits and matches
-			// this is an aadditional request.
-			// Increment the waitgroup
-			waitgroup.Add(1)
+			// Remove connection from the waitgroup when done
+			defer waitgroup.Done()
 		}
-		// Remove connection from the waitfroup when done
-		defer waitgroup.Done()
 		w.Header().Set("Content-Disposition", "attachment; filename="+
 			app.payload.Filename)
 		http.ServeFile(w, r, app.payload.Path)
@@ -200,7 +250,7 @@ func New(cfg *config.Config) (*Server, error) {
 					continue
 				}
 				// Prepare the destination
-				fileName := getFileName(part.FileName(), filenames)
+				fileName := getFileName(filepath.Base(part.FileName()), filenames)
 				out, err := os.Create(filepath.Join(app.outputDir, fileName))
 				if err != nil {
 					// Output to server
@@ -252,7 +302,7 @@ func New(cfg *config.Config) (*Server, error) {
 			// Set the value of the variable to the actually transferred files
 			htmlVariables.File = strings.Join(transferredFiles, ", ")
 			serveTemplate("done", pages.Done, w, htmlVariables)
-			if cfg.KeepAlive == false {
+			if !cfg.KeepAlive {
 				app.stopChannel <- true
 			}
 		case "GET":
@@ -267,12 +317,36 @@ func New(cfg *config.Config) (*Server, error) {
 		}
 		app.stopChannel <- true
 	}()
-	// Receive handler (receives file from caller)
 	go func() {
-		if err := (httpserver.Serve(tcpKeepAliveListener{listener.(*net.TCPListener)})); err != http.ErrServerClosed {
-			log.Fatalln(err)
+		netListener := tcpKeepAliveListener{listener.(*net.TCPListener)}
+		if cfg.Secure {
+			if err := httpserver.ServeTLS(netListener, cfg.TLSCert, cfg.TLSKey); err != http.ErrServerClosed {
+				log.Fatalln("error starting the server:", err)
+			}
+		} else {
+			if err := httpserver.Serve(netListener); err != http.ErrServerClosed {
+				log.Fatalln("error starting the server", err)
+			}
 		}
 	}()
 	app.instance = httpserver
 	return app, nil
 }
+
+// openBrowser navigates to a url using the default system browser
+func openBrowser(url string) {
+	var err error
+	switch runtime.GOOS {
+	case "linux":
+		err = exec.Command("xdg-open", url).Start()
+	case "windows":
+		err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
+	case "darwin":
+		err = exec.Command("open", url).Start()
+	default:
+		err = fmt.Errorf("failed to open browser on platform: %s", runtime.GOOS)
+	}
+	if err != nil {
+		log.Fatal(err)
+	}
+}

+ 6 - 2
server/tcpkeepalivelistener.go

@@ -44,7 +44,11 @@ func (ln tcpKeepAliveListener) Accept() (net.Conn, error) {
 	if err != nil {
 		return nil, err
 	}
-	tc.SetKeepAlive(true)
-	tc.SetKeepAlivePeriod(3 * time.Minute)
+	if err := tc.SetKeepAlive(true); err != nil {
+		panic(err)
+	}
+	if err := tc.SetKeepAlivePeriod(3 * time.Minute); err != nil {
+		panic(err)
+	}
 	return tc, nil
 }

+ 1 - 1
util/net.go

@@ -16,7 +16,7 @@ func Interfaces(listAll bool) (map[string]string, error) {
 	}
 	var re = regexp.MustCompile(`^(veth|br\-|docker|lo|EHC|XHC|bridge|gif|stf|p2p|awdl|utun|tun|tap)`)
 	for _, iface := range ifaces {
-		if listAll == false && re.MatchString(iface.Name) {
+		if !listAll && re.MatchString(iface.Name) {
 			continue
 		}
 		if iface.Flags&net.FlagUp == 0 {

+ 36 - 20
util/util.go

@@ -8,13 +8,31 @@ import (
 	"io/ioutil"
 	"net"
 	"os"
-	"regexp"
+	"os/user"
+	"path/filepath"
+	"runtime"
 	"strconv"
+	"strings"
 	"time"
 
 	"github.com/jhoonb/archivex"
 )
 
+// Expand tilde in paths
+func Expand(input string) string {
+	if runtime.GOOS == "windows" {
+		return input
+	}
+	usr, _ := user.Current()
+	dir := usr.HomeDir
+	if input == "~" {
+		input = dir
+	} else if strings.HasPrefix(input, "~/") {
+		input = filepath.Join(dir, input[2:])
+	}
+	return input
+}
+
 // ZipFiles and return the resulting zip's filename
 func ZipFiles(files []string) (string, error) {
 	zip := new(archivex.ZipFile)
@@ -26,14 +44,18 @@ func ZipFiles(files []string) (string, error) {
 	if err := os.Rename(tmpfile.Name(), tmpfile.Name()+".zip"); err != nil {
 		return "", err
 	}
-	zip.Create(tmpfile.Name() + ".zip")
+	if err := zip.Create(tmpfile.Name() + ".zip"); err != nil {
+		return "", err
+	}
 	for _, filename := range files {
 		fileinfo, err := os.Stat(filename)
 		if err != nil {
 			return "", err
 		}
 		if fileinfo.IsDir() {
-			zip.AddAll(filename, true)
+			if err := zip.AddAll(filename, true); err != nil {
+				return "", err
+			}
 		} else {
 			file, err := os.Open(filename)
 			if err != nil {
@@ -97,6 +119,7 @@ func GetInterfaceAddress(ifaceString string) (string, error) {
 
 // FindIP returns the IP address of the passed interface, and an error
 func FindIP(iface net.Interface) (string, error) {
+	var ip string
 	addrs, err := iface.Addrs()
 	if err != nil {
 		return "", err
@@ -107,27 +130,20 @@ func FindIP(iface net.Interface) (string, error) {
 				continue
 			}
 			if ipnet.IP.To4() != nil {
-				return ipnet.IP.String(), nil
+				ip = ipnet.IP.String()
+				continue
+			}
+			// Use IPv6 only if an IPv4 hasn't been found yet.
+			// This is eventually overwritten with an IPv4, if found (see above)
+			if ip == "" {
+				ip = "[" + ipnet.IP.String() + "]"
 			}
-			return "[" + ipnet.IP.String() + "]", nil
 		}
 	}
-	return "", errors.New("Unable to find an IP for this interface")
-}
-
-func filterInterfaces(ifaces []net.Interface) []net.Interface {
-	filtered := []net.Interface{}
-	var re = regexp.MustCompile(`^(veth|br\-|docker|lo|EHC|XHC|bridge|gif|stf|p2p|awdl|utun|tun|tap)`)
-	for _, iface := range ifaces {
-		if re.MatchString(iface.Name) {
-			continue
-		}
-		if iface.Flags&net.FlagUp == 0 {
-			continue
-		}
-		filtered = append(filtered, iface)
+	if ip == "" {
+		return "", errors.New("unable to find an IP for this interface")
 	}
-	return filtered
+	return ip, nil
 }
 
 // ReadFilenames from dir