diff --git a/go.sum b/go.sum index 205e35f..e9164bd 100644 --- a/go.sum +++ b/go.sum @@ -16,8 +16,10 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= diff --git a/main.go b/main.go index f009c1c..2059da5 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ import ( "golang.org/x/crypto/acme" "golang.org/x/crypto/acme/autocert" + "golang.org/x/crypto/bcrypt" ) var ( @@ -65,12 +66,9 @@ type CLIArgs struct { autocertACME string autocertEmail string autocertHTTP string -} - -func list_ciphers() { - for _, cipher := range tls.CipherSuites() { - fmt.Println(cipher.Name) - } + passwd string + passwdCost int + positionalArgs []string } func parse_args() CLIArgs { @@ -93,7 +91,11 @@ func parse_args() CLIArgs { flag.StringVar(&args.autocertACME, "autocert-acme", autocert.DefaultACMEDirectory, "custom ACME endpoint") flag.StringVar(&args.autocertEmail, "autocert-email", "", "email used for ACME registration") flag.StringVar(&args.autocertHTTP, "autocert-http", "", "listen address for HTTP-01 challenges handler of ACME") + flag.StringVar(&args.passwd, "passwd", "", "update given htpasswd file and add/set password for username. "+ + "Username and password can be passed as positional arguments or requested interactively") + flag.IntVar(&args.passwdCost, "passwd-cost", bcrypt.MinCost, "bcrypt password cost (for -passwd mode)") flag.Parse() + args.positionalArgs = flag.Args() return args } @@ -110,6 +112,13 @@ func run() int { return 0 } + if args.passwd != "" { + if err := passwd(args.passwd, args.passwdCost, args.positionalArgs...); err != nil { + log.Fatalf("can't set password: %v", err) + } + return 0 + } + logWriter := NewLogWriter(os.Stderr) defer logWriter.Close() diff --git a/utils.go b/utils.go index 96a2667..a692361 100644 --- a/utils.go +++ b/utils.go @@ -16,6 +16,9 @@ import ( "strings" "sync" "time" + + "golang.org/x/crypto/bcrypt" + "golang.org/x/crypto/ssh/terminal" ) const COPY_BUF = 128 * 1024 @@ -192,6 +195,62 @@ func makeCipherList(ciphers string) []uint16 { return cipherIDList } +func list_ciphers() { + for _, cipher := range tls.CipherSuites() { + fmt.Println(cipher.Name) + } +} + +func passwd(filename string, cost int, args ...string) error { + var ( + username, password, password2 string + err error + ) + + if len(args) > 0 { + username = args[0] + } else { + username, err = prompt("Enter username: ", false) + if err != nil { + return fmt.Errorf("can't get username: %w", err) + } + } + + if len(args) > 1 { + password = args[1] + } else { + password, err = prompt("Enter password: ", true) + if err != nil { + return fmt.Errorf("can't get password: %w", err) + } + password2, err = prompt("Repeat password: ", true) + if err != nil { + return fmt.Errorf("can't get password (repeat): %w", err) + } + if password != password2 { + return fmt.Errorf("passwords do not match") + } + } + + hash, err := bcrypt.GenerateFromPassword([]byte(password), cost) + if err != nil { + return fmt.Errorf("can't generate password hash: %w", err) + } + + f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + return fmt.Errorf("can't open file: %w", err) + } + defer f.Close() + + _, err = f.WriteString(fmt.Sprintf("%s:%s\n", username, hash)) + if err != nil { + return fmt.Errorf("can't write to file: %w", err) + } + + return nil +} + func fileModTime(filename string) (time.Time, error) { f, err := os.Open(filename) if err != nil { @@ -206,3 +265,20 @@ func fileModTime(filename string) (time.Time, error) { return fi.ModTime(), nil } + +func prompt(prompt string, secure bool) (string, error) { + var input string + fmt.Print(prompt) + + if secure { + b, err := terminal.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return "", err + } + input = string(b) + fmt.Println() + } else { + fmt.Scanln(&input) + } + return input, nil +}