From 5bde8d705b9c9cae6751ccf95b7b8acfc1bbb7f5 Mon Sep 17 00:00:00 2001
From: Andrew Zhou <andrewfzhou@gmail.com>
Date: Mon, 11 May 2020 15:10:47 -0500
Subject: [PATCH] cmd: hash-password: Support reading from stdin (#3373)

Closes #3365

* http: Add support in hash-password for reading from terminals/stdin

* FIXUP: Run gofmt -s

* FIXUP

* FIXUP: Apply suggestions from code review

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

* FIXUP

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
---
 modules/caddyhttp/caddyauth/command.go | 45 ++++++++++++++++++++++++--
 1 file changed, 42 insertions(+), 3 deletions(-)

diff --git a/modules/caddyhttp/caddyauth/command.go b/modules/caddyhttp/caddyauth/command.go
index 24f6c5a1f..2c0881571 100644
--- a/modules/caddyhttp/caddyauth/command.go
+++ b/modules/caddyhttp/caddyauth/command.go
@@ -15,26 +15,34 @@
 package caddyauth
 
 import (
+	"bufio"
+	"bytes"
 	"encoding/base64"
 	"flag"
 	"fmt"
+	"os"
 
 	"github.com/caddyserver/caddy/v2"
 	caddycmd "github.com/caddyserver/caddy/v2/cmd"
 	"golang.org/x/crypto/bcrypt"
 	"golang.org/x/crypto/scrypt"
+	"golang.org/x/crypto/ssh/terminal"
 )
 
 func init() {
 	caddycmd.RegisterCommand(caddycmd.Command{
 		Name:  "hash-password",
 		Func:  cmdHashPassword,
-		Usage: "--plaintext <password> [--salt <string>] [--algorithm <name>]",
+		Usage: "[--algorithm <name>] [--salt <string>] [--plaintext <password>]",
 		Short: "Hashes a password and writes base64",
 		Long: `
 Convenient way to hash a plaintext password. The resulting
 hash is written to stdout as a base64 string.
 
+--plaintext, when omitted, will be read from stdin. If
+Caddy is attached to a controlling tty, the plaintext will
+not be echoed.
+
 --algorithm may be bcrypt or scrypt. If script, the default
 parameters are used.
 
@@ -52,16 +60,47 @@ be provided (scrypt).
 }
 
 func cmdHashPassword(fs caddycmd.Flags) (int, error) {
+	var err error
+
 	algorithm := fs.String("algorithm")
 	plaintext := []byte(fs.String("plaintext"))
 	salt := []byte(fs.String("salt"))
 
 	if len(plaintext) == 0 {
-		return caddy.ExitCodeFailedStartup, fmt.Errorf("password is required")
+		if terminal.IsTerminal(int(os.Stdin.Fd())) {
+			fmt.Print("Enter password: ")
+			plaintext, err = terminal.ReadPassword(int(os.Stdin.Fd()))
+			fmt.Println()
+			if err != nil {
+				return caddy.ExitCodeFailedStartup, err
+			}
+
+			fmt.Print("Confirm password: ")
+			confirmation, err := terminal.ReadPassword(int(os.Stdin.Fd()))
+			fmt.Println()
+			if err != nil {
+				return caddy.ExitCodeFailedStartup, err
+			}
+
+			if !bytes.Equal(plaintext, confirmation) {
+				return caddy.ExitCodeFailedStartup, fmt.Errorf("password does not match")
+			}
+		} else {
+			rd := bufio.NewReader(os.Stdin)
+			plaintext, err = rd.ReadBytes('\n')
+			if err != nil {
+				return caddy.ExitCodeFailedStartup, err
+			}
+
+			plaintext = plaintext[:len(plaintext)-1] // Trailing newline
+		}
+
+		if len(plaintext) == 0 {
+			return caddy.ExitCodeFailedStartup, fmt.Errorf("plaintext is required")
+		}
 	}
 
 	var hash []byte
-	var err error
 	switch algorithm {
 	case "bcrypt":
 		hash, err = bcrypt.GenerateFromPassword(plaintext, bcrypt.DefaultCost)