diff --git a/caddyfile.go b/caddyfile.go index 80e0ab2..239b5c7 100644 --- a/caddyfile.go +++ b/caddyfile.go @@ -1,6 +1,7 @@ package forwardproxy import ( + "encoding/base64" "log" "strconv" "strings" @@ -21,6 +22,14 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) return &fp, err } +// EncodeAuthCredentials base64-encode credentials +func EncodeAuthCredentials(user, pass string) (result []byte) { + raw := []byte(user + ":" + pass) + result = make([]byte, base64.StdEncoding.EncodedLen(len(raw))) + base64.StdEncoding.Encode(result, raw) + return +} + // UnmarshalCaddyfile unmarshals Caddyfile tokens into h. func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if !d.Next() { @@ -45,13 +54,10 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if strings.Contains(args[0], ":") { return d.Err("character ':' in usernames is not allowed") } - // TODO: Support multiple basicauths. - // TODO: Actually, just try to use Caddy 2's existing basicauth module. - if h.BasicauthUser != "" || h.BasicauthPass != "" { - return d.Err("Multi-user basicauth is not supported") + if h.AuthCredentials == nil { + h.AuthCredentials = [][]byte{} } - h.BasicauthUser = args[0] - h.BasicauthPass = args[1] + h.AuthCredentials = append(h.AuthCredentials, EncodeAuthCredentials(args[0], args[1])) case "hosts": if len(args) == 0 { return d.ArgErr() diff --git a/common_test.go b/common_test.go index 2e32284..4880d3b 100644 --- a/common_test.go +++ b/common_test.go @@ -201,10 +201,9 @@ func TestMain(m *testing.M) { root: "./test/forwardproxy", tls: true, proxyHandler: &Handler{ - PACPath: defaultPACPath, - ACL: []ACLRule{{Subjects: []string{"all"}, Allow: true}}, - BasicauthUser: "test", - BasicauthPass: "pass", + PACPath: defaultPACPath, + ACL: []ACLRule{{Subjects: []string{"all"}, Allow: true}}, + AuthCredentials: [][]byte{EncodeAuthCredentials("test", "pass")}, }, } @@ -212,10 +211,9 @@ func TestMain(m *testing.M) { addr: "127.0.69.73:6973", root: "./test/forwardproxy", proxyHandler: &Handler{ - PACPath: defaultPACPath, - ACL: []ACLRule{{Subjects: []string{"all"}, Allow: true}}, - BasicauthUser: "test", - BasicauthPass: "pass", + PACPath: defaultPACPath, + ACL: []ACLRule{{Subjects: []string{"all"}, Allow: true}}, + AuthCredentials: [][]byte{EncodeAuthCredentials("test", "pass")}, }, } @@ -227,8 +225,7 @@ func TestMain(m *testing.M) { PACPath: "/superhiddenfile.pac", ACL: []ACLRule{{Subjects: []string{"all"}, Allow: true}}, ProbeResistance: &ProbeResistance{Domain: "test.localhost"}, - BasicauthUser: "test", - BasicauthPass: "pass", + AuthCredentials: [][]byte{EncodeAuthCredentials("test", "pass")}, }, httpRedirPort: "8880", } @@ -255,9 +252,8 @@ func TestMain(m *testing.M) { root: "./test/upstreamingproxy", tls: true, proxyHandler: &Handler{ - Upstream: "https://test:pass@127.0.0.1:4891", - BasicauthUser: "upstreamtest", - BasicauthPass: "upstreampass", + Upstream: "https://test:pass@127.0.0.1:4891", + AuthCredentials: [][]byte{EncodeAuthCredentials("upstreamtest", "upstreampass")}, }, } diff --git a/forwardproxy.go b/forwardproxy.go index 8198cea..218f205 100644 --- a/forwardproxy.go +++ b/forwardproxy.go @@ -35,6 +35,7 @@ import ( "strings" "sync" "time" + "unicode/utf8" caddy "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" @@ -91,10 +92,7 @@ type Handler struct { aclRules []aclRule // TODO: temporary/deprecated - we should try to reuse existing authentication modules instead! - BasicauthUser string `json:"auth_user_deprecated,omitempty"` - BasicauthPass string `json:"auth_pass_deprecated,omitempty"` - authRequired bool - authCredentials [][]byte // slice with base64-encoded credentials + AuthCredentials [][]byte `json:"auth_credentials,omitempty"` // slice with base64-encoded credentials } // CaddyModule returns the Caddy module information. @@ -120,14 +118,6 @@ func (h *Handler) Provision(ctx caddy.Context) error { TLSHandshakeTimeout: 10 * time.Second, } - // TODO: temporary, in an effort to get the tests to pass - if h.BasicauthUser != "" && h.BasicauthPass != "" { - basicAuthBuf := make([]byte, base64.StdEncoding.EncodedLen(len(h.BasicauthUser)+1+len(h.BasicauthPass))) - base64.StdEncoding.Encode(basicAuthBuf, []byte(h.BasicauthUser+":"+h.BasicauthPass)) - h.authRequired = true - h.authCredentials = [][]byte{basicAuthBuf} - } - // access control lists for _, rule := range h.ACL { for _, subj := range rule.Subjects { @@ -155,7 +145,7 @@ func (h *Handler) Provision(ctx caddy.Context) error { h.aclRules = append(h.aclRules, &aclAllRule{allow: true}) if h.ProbeResistance != nil { - if !h.authRequired { + if h.AuthCredentials == nil { return fmt.Errorf("probe resistance requires authentication") } if len(h.ProbeResistance.Domain) > 0 { @@ -237,7 +227,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht } var authErr error - if h.authRequired { + if h.AuthCredentials != nil { authErr = h.checkCredentials(r) } if h.ProbeResistance != nil && len(h.ProbeResistance.Domain) > 0 && reqHost == h.ProbeResistance.Domain { @@ -421,14 +411,37 @@ func (h Handler) checkCredentials(r *http.Request) error { if strings.ToLower(pa[0]) != "basic" { return errors.New("auth type is not supported") } - for _, creds := range h.authCredentials { + for _, creds := range h.AuthCredentials { if subtle.ConstantTimeCompare(creds, []byte(pa[1])) == 1 { + repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + buf := make([]byte, base64.StdEncoding.DecodedLen(len(creds))) + _, _ = base64.StdEncoding.Decode(buf, creds) // should not err ever since we are decoding a known good input + cred := string(buf) + repl.Set("http.auth.user.id", cred[:strings.IndexByte(cred, ':')]) // Please do not consider this to be timing-attack-safe code. Simple equality is almost // mindlessly substituted with constant time algo and there ARE known issues with this code, // e.g. size of smallest credentials is guessable. TODO: protect from all the attacks! Hash? return nil } } + repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + buf := make([]byte, base64.StdEncoding.DecodedLen(len([]byte(pa[1])))) + n, err := base64.StdEncoding.Decode(buf, []byte(pa[1])) + if err != nil { + repl.Set("http.auth.user.id", "invalidbase64:"+err.Error()) + return err + } + if utf8.Valid(buf[:n]) { + cred := string(buf[:n]) + i := strings.IndexByte(cred, ':') + if i >= 0 { + repl.Set("http.auth.user.id", "invalid:"+cred[:i]) + } else { + repl.Set("http.auth.user.id", "invalidformat:"+cred) + } + } else { + repl.Set("http.auth.user.id", "invalid::") + } return errors.New("invalid credentials") }