From de8cbc00333770302e9c7767ab8dca532b735c42 Mon Sep 17 00:00:00 2001 From: Vladislav Yarmak Date: Wed, 7 Sep 2022 00:08:06 +0300 Subject: [PATCH 1/2] htpasswd-alike util --- go.sum | 2 ++ main.go | 21 +++++++++++----- utils.go | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 6 deletions(-) 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 +} From fe4294e3c2091eecd0b8a5ab7e866289d570e483 Mon Sep 17 00:00:00 2001 From: Vladislav Yarmak Date: Wed, 7 Sep 2022 00:25:38 +0300 Subject: [PATCH 2/2] upd doc --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6750902..d84d6ba 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ Authentication parameters are passed as URI via `-auth` parameter. Scheme of URI * `username` - login. * `password` - password. * `hidden_domain` - if specified and is not an empty string, proxy will respond with "407 Proxy Authentication Required" only on specified domain. All unauthenticated clients will receive "400 Bad Request" status. This option is useful to prevent DPI active probing from discovering that service is a proxy, hiding proxy authentication prompt when no valid auth header was provided. Hidden domain is used for generating 407 response code to trigger browser authorization request in cases when browser has no prior knowledge proxy authentication is required. In such cases user has to navigate to any hidden domain page via plaintext HTTP, authenticate themselves and then browser will remember authentication. -* `basicfile` - use htpasswd-like file with login and password pairs for authentication. Such file can be created/updated like this: `touch /etc/dumbproxy.htpasswd && htpasswd -bBC 4 /etc/dumbproxy.htpasswd username password`. `path` parameter in URL for this provider must point to a local file with login and bcrypt-hashed password lines. Example: `basicfile://?path=/etc/dumbproxy.htpasswd`. +* `basicfile` - use htpasswd-like file with login and password pairs for authentication. Such file can be created/updated with command like this: `dumbproxy -passwd /etc/dumbproxy.htpasswd username password` or with `htpasswd` utility from Apache HTTPD utils. `path` parameter in URL for this provider must point to a local file with login and bcrypt-hashed password lines. Example: `basicfile://?path=/etc/dumbproxy.htpasswd`. * `path` - location of file with login and password pairs. File format is similar to htpasswd files. Each line must be in form `:`. Empty lines and lines starting with `#` are ignored. * `hidden_domain` - same as in `static` provider * `reload` - interval for conditional password file reload, if it was modified since last load. Use negative duration to disable autoreload. Default: `15s`. @@ -195,6 +195,10 @@ $ ~/go/bin/dumbproxy -h key for TLS certificate -list-ciphers list ciphersuites + -passwd string + update given htpasswd file and add/set password for username. Username and password can be passed as positional arguments or requested interactively + -passwd-cost int + bcrypt password cost (for -passwd mode) (default 4) -timeout duration timeout for network operations (default 10s) -verbosity int