Skip to content

Commit

Permalink
Merge pull request #10 from Snawoot/htpasswd_util
Browse files Browse the repository at this point in the history
htpasswd-alike util
  • Loading branch information
Snawoot authored Sep 6, 2022
2 parents a546f76 + fe4294e commit b049886
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 7 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<username>:<bcrypt hash of password>`. 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`.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
21 changes: 15 additions & 6 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
"golang.org/x/crypto/bcrypt"
)

var (
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}

Expand All @@ -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()

Expand Down
76 changes: 76 additions & 0 deletions utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import (
"strings"
"sync"
"time"

"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/ssh/terminal"
)

const COPY_BUF = 128 * 1024
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}

0 comments on commit b049886

Please sign in to comment.