Skip to content

Commit 6253deb

Browse files
authored
Merge pull request #163 from SenseUnit/auth_enhancements
Auth enhancements
2 parents fc13a4d + 52849af commit 6253deb

File tree

10 files changed

+185
-54
lines changed

10 files changed

+185
-54
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Simple, scriptable, secure HTTP/SOCKS5 forward proxy.
1717
* Via static login and password
1818
* Via HMAC signatures provisioned by central authority (e.g. some webservice)
1919
* Via Redis or Redis Cluster database
20+
* Chaining of all above in order to lookup multiple sources or provide custom rejection response.
2021
* Supports TLS operation mode (HTTP(S) proxy over TLS)
2122
* Supports client authentication with client TLS certificates
2223
* Native ACME support (can issue TLS certificates automatically using Let's Encrypt or BuyPass)
@@ -255,13 +256,16 @@ Authentication parameters are passed as URI via `-auth` parameter. Scheme of URI
255256
* `username` - login.
256257
* `password` - password.
257258
* `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.
259+
* `else` - optional URL specifying the next auth provider to chain to, if authentication failed. Example: `-auth 'static://?username=root&password=mycoolpass&else=static%3A%2F%2F%3Fusername%3Dadmin%26password%3D123456'`.
258260
* `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`. Parameters:
259261
* `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.
260262
* `hidden_domain` - same as in `static` provider.
261263
* `reload` - interval for conditional password file reload, if it was modified since last load. Use negative duration to disable autoreload. Default: `15s`.
264+
* `else` - optional URL specifying the next auth provider to chain to, if authentication failed. Example: `-auth 'basicfile://?path=/etc/dumbproxy.htpasswd&else=static%3A%2F%2F%3Fusername%3Dadmin%26password%3D123456'`.
262265
* `hmac` - authentication with HMAC-signatures passed as username and password via basic authentication scheme. In that scheme username represents user login as usual and password should be constructed as follows: *password := urlsafe\_base64\_without\_padding(expire\_timestamp || hmac\_sha256(secret, "dumbproxy grant token v1" || username || expire\_timestamp))*, where *expire_timestamp* is 64-bit big-endian UNIX timestamp and *||* is a concatenation operator. [This Python script](https://gist.github.com/Snawoot/2b5acc232680d830f0f308f14e540f1d) can be used as a reference implementation of signing. Dumbproxy itself also provides built-in signer: `dumbproxy -hmac-sign <HMAC key> <username> <validity duration>`. Parameters of this auth scheme are:
263266
* `secret` - hex-encoded HMAC secret key. Alternatively it can be specified by `DUMBPROXY_HMAC_SECRET` environment variable. Secret key can be generated with command like this: `openssl rand -hex 32` or `dumbproxy -hmac-genkey`.
264267
* `hidden_domain` - same as in `static` provider.
268+
* `else` - optional URL specifying the next auth provider to chain to, if authentication failed.
265269
* `cert` - use mutual TLS authentication with client certificates. In order to use this auth provider server must listen sockert in TLS mode (`-cert` and `-key` options) and client CA file must be specified (`-cacert`). Example: `cert://`. Parameters of this scheme are:
266270
* `blacklist` - location of file with list of serial numbers of blocked certificates, one per each line in form of hex-encoded colon-separated bytes. Example: `ab:01:02:03`. Empty lines and comments starting with `#` are ignored.
267271
* `reload` - interval for certificate blacklist file reload, if it was modified since last load. Use negative duration to disable autoreload. Default: `15s`.
@@ -270,10 +274,16 @@ Authentication parameters are passed as URI via `-auth` parameter. Scheme of URI
270274
* `url` - URL specifying Redis instance to connect to. See [ParseURL](https://pkg.go.dev/github.com/redis/go-redis/v9#ParseURL) documentation for the complete specification of Redis URL format.
271275
* `key_prefix` - prefix to prepend to each key before lookup. Helps isolate keys under common prefix. Default is empty string (`""`).
272276
* `hidden_domain` - same as in `static` provider.
277+
* `else` - optional URL specifying the next auth provider to chain to, if authentication failed.
273278
* `redis-cluster` - same as Redis, but uses Redis Cluster client instead.
274279
* `url` - URL specifying Redis instance to connect to. See [ParseClusterURL](https://pkg.go.dev/github.com/redis/go-redis/v9#ParseClusterURL) documentation for the complete specification of Redis URL format.
275280
* `key_prefix` - prefix to prepend to each key before lookup. Helps isolate keys under common prefix. Default is empty string (`""`).
276281
* `hidden_domain` - same as in `static` provider.
282+
* `else` - optional URL specifying the next auth provider to chain to, if authentication failed.
283+
* `reject-http`, `reject-https` - auth provider which always rejects auth and returns response fetched from specified URL. Useful as a last auth chain element with other providers in order to masquerade proxy endpoint or return custom rejection response from another webserver. Example: `-auth reject-https://www.google.com`. Parameters:
284+
* `method` - override HTTP request method.
285+
* `qs` - provide query string to the URL in request.
286+
* `x-forwarded` - boolean parameter specifying if X-Forwarded headers should be populated.
277287
278288
## Scripting
279289

auth/auth.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ func NewAuth(paramstr string, logger *clog.CondLogger) (Auth, error) {
3636
return NewRedisAuth(url, true, logger)
3737
case "none":
3838
return NoAuth{}, nil
39+
case "reject-http", "reject-https":
40+
return NewRejectHTTPAuth(url, logger)
3941
default:
4042
return nil, errors.New("Unknown auth scheme")
4143
}

auth/basic.go

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type BasicAuth struct {
2929
hiddenDomain string
3030
stopOnce sync.Once
3131
stopChan chan struct{}
32+
next Auth
3233
}
3334

3435
func NewBasicFileAuth(param_url *url.URL, logger *clog.CondLogger) (*BasicAuth, error) {
@@ -64,6 +65,14 @@ func NewBasicFileAuth(param_url *url.URL, logger *clog.CondLogger) (*BasicAuth,
6465
go auth.reloadLoop(reloadInterval)
6566
}
6667

68+
if nextAuth := values.Get("else"); nextAuth != "" {
69+
nap, err := NewAuth(nextAuth, logger)
70+
if err != nil {
71+
return nil, fmt.Errorf("chained auth provider construction failed: %w", err)
72+
}
73+
auth.next = nap
74+
}
75+
6776
return auth, nil
6877
}
6978

@@ -117,32 +126,28 @@ func (auth *BasicAuth) reloadLoop(interval time.Duration) {
117126

118127
func (auth *BasicAuth) Valid(user, password, userAddr string) bool {
119128
pwFile := auth.pw.Load().file
120-
return pwFile.Match(user, password)
129+
return pwFile.Match(user, password) || tryValid(auth.next, auth.logger, user, password, userAddr)
121130
}
122131

123-
func (auth *BasicAuth) Validate(_ context.Context, wr http.ResponseWriter, req *http.Request) (string, bool) {
132+
func (auth *BasicAuth) Validate(ctx context.Context, wr http.ResponseWriter, req *http.Request) (string, bool) {
124133
hdr := req.Header.Get("Proxy-Authorization")
125134
if hdr == "" {
126-
requireBasicAuth(wr, req, auth.hiddenDomain)
127-
return "", false
135+
return requireBasicAuth(ctx, wr, req, auth.hiddenDomain, auth.next)
128136
}
129137
hdr_parts := strings.SplitN(hdr, " ", 2)
130138
if len(hdr_parts) != 2 || strings.ToLower(hdr_parts[0]) != "basic" {
131-
requireBasicAuth(wr, req, auth.hiddenDomain)
132-
return "", false
139+
return requireBasicAuth(ctx, wr, req, auth.hiddenDomain, auth.next)
133140
}
134141

135142
token := hdr_parts[1]
136143
data, err := base64.StdEncoding.DecodeString(token)
137144
if err != nil {
138-
requireBasicAuth(wr, req, auth.hiddenDomain)
139-
return "", false
145+
return requireBasicAuth(ctx, wr, req, auth.hiddenDomain, auth.next)
140146
}
141147

142148
pair := strings.SplitN(string(data), ":", 2)
143149
if len(pair) != 2 {
144-
requireBasicAuth(wr, req, auth.hiddenDomain)
145-
return "", false
150+
return requireBasicAuth(ctx, wr, req, auth.hiddenDomain, auth.next)
146151
}
147152

148153
login := pair[0]
@@ -165,12 +170,14 @@ func (auth *BasicAuth) Validate(_ context.Context, wr http.ResponseWriter, req *
165170
return login, true
166171
}
167172
}
168-
requireBasicAuth(wr, req, auth.hiddenDomain)
169-
return "", false
173+
return requireBasicAuth(ctx, wr, req, auth.hiddenDomain, auth.next)
170174
}
171175

172176
func (auth *BasicAuth) Stop() {
173177
auth.stopOnce.Do(func() {
178+
if auth.next != nil {
179+
auth.next.Stop()
180+
}
174181
close(auth.stopChan)
175182
})
176183
}

auth/cert.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ func (auth *CertAuth) Validate(ctx context.Context, wr http.ResponseWriter, req
9696

9797
func (auth *CertAuth) Stop() {
9898
auth.stopOnce.Do(func() {
99+
if auth.next != nil {
100+
auth.next.Stop()
101+
}
99102
close(auth.stopChan)
100103
})
101104
}

auth/common.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package auth
22

33
import (
4+
"context"
45
"crypto/subtle"
56
"errors"
67
"net"
78
"net/http"
89
"strconv"
910
"strings"
1011

12+
clog "github.com/SenseUnit/dumbproxy/log"
1113
"github.com/tg123/go-htpasswd"
1214
)
1315

@@ -19,7 +21,10 @@ func matchHiddenDomain(host, hidden_domain string) bool {
1921
return subtle.ConstantTimeCompare([]byte(host), []byte(hidden_domain)) == 1
2022
}
2123

22-
func requireBasicAuth(wr http.ResponseWriter, req *http.Request, hidden_domain string) {
24+
func requireBasicAuth(ctx context.Context, wr http.ResponseWriter, req *http.Request, hidden_domain string, next Auth) (string, bool) {
25+
if next != nil {
26+
return next.Validate(ctx, wr, req)
27+
}
2328
if hidden_domain != "" &&
2429
!matchHiddenDomain(req.URL.Host, hidden_domain) &&
2530
!matchHiddenDomain(req.Host, hidden_domain) {
@@ -30,6 +35,17 @@ func requireBasicAuth(wr http.ResponseWriter, req *http.Request, hidden_domain s
3035
wr.WriteHeader(407)
3136
wr.Write([]byte(AUTH_REQUIRED_MSG))
3237
}
38+
return "", false
39+
}
40+
41+
func tryValid(auth Auth, logger *clog.CondLogger, user, password, userAddr string) bool {
42+
if validator, ok := auth.(interface {
43+
Valid(string, string, string) bool
44+
}); ok {
45+
return validator.Valid(user, password, userAddr)
46+
}
47+
logger.Warning("chained auth provider does not have Valid() method!")
48+
return false
3349
}
3450

3551
func makePasswdMatcher(encoded string) (htpasswd.EncodedPasswd, error) {

auth/hmac.go

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"os"
1515
"strconv"
1616
"strings"
17+
"sync"
1718
"time"
1819

1920
clog "github.com/SenseUnit/dumbproxy/log"
@@ -30,6 +31,8 @@ type HMACAuth struct {
3031
secret []byte
3132
hiddenDomain string
3233
logger *clog.CondLogger
34+
stopOnce sync.Once
35+
next Auth
3336
}
3437

3538
func NewHMACAuth(param_url *url.URL, logger *clog.CondLogger) (*HMACAuth, error) {
@@ -52,11 +55,21 @@ func NewHMACAuth(param_url *url.URL, logger *clog.CondLogger) (*HMACAuth, error)
5255
return nil, fmt.Errorf("can't hex-decode HMAC secret: %w", err)
5356
}
5457

55-
return &HMACAuth{
58+
auth := &HMACAuth{
5659
secret: secret,
5760
logger: logger,
5861
hiddenDomain: strings.ToLower(values.Get("hidden_domain")),
59-
}, nil
62+
}
63+
64+
if nextAuth := values.Get("else"); nextAuth != "" {
65+
nap, err := NewAuth(nextAuth, logger)
66+
if err != nil {
67+
return nil, fmt.Errorf("chained auth provider construction failed: %w", err)
68+
}
69+
auth.next = nap
70+
}
71+
72+
return auth, nil
6073
}
6174

6275
type HMACToken struct {
@@ -85,32 +98,28 @@ func VerifyHMACLoginAndPassword(secret []byte, login, password string) bool {
8598
}
8699

87100
func (auth *HMACAuth) Valid(user, password, userAddr string) bool {
88-
return VerifyHMACLoginAndPassword(auth.secret, user, password)
101+
return VerifyHMACLoginAndPassword(auth.secret, user, password) || tryValid(auth.next, auth.logger, user, password, userAddr)
89102
}
90103

91-
func (auth *HMACAuth) Validate(_ context.Context, wr http.ResponseWriter, req *http.Request) (string, bool) {
104+
func (auth *HMACAuth) Validate(ctx context.Context, wr http.ResponseWriter, req *http.Request) (string, bool) {
92105
hdr := req.Header.Get("Proxy-Authorization")
93106
if hdr == "" {
94-
requireBasicAuth(wr, req, auth.hiddenDomain)
95-
return "", false
107+
return requireBasicAuth(ctx, wr, req, auth.hiddenDomain, auth.next)
96108
}
97109
hdr_parts := strings.SplitN(hdr, " ", 2)
98110
if len(hdr_parts) != 2 || strings.ToLower(hdr_parts[0]) != "basic" {
99-
requireBasicAuth(wr, req, auth.hiddenDomain)
100-
return "", false
111+
return requireBasicAuth(ctx, wr, req, auth.hiddenDomain, auth.next)
101112
}
102113

103114
token := hdr_parts[1]
104115
data, err := base64.StdEncoding.DecodeString(token)
105116
if err != nil {
106-
requireBasicAuth(wr, req, auth.hiddenDomain)
107-
return "", false
117+
return requireBasicAuth(ctx, wr, req, auth.hiddenDomain, auth.next)
108118
}
109119

110120
pair := strings.SplitN(string(data), ":", 2)
111121
if len(pair) != 2 {
112-
requireBasicAuth(wr, req, auth.hiddenDomain)
113-
return "", false
122+
return requireBasicAuth(ctx, wr, req, auth.hiddenDomain, auth.next)
114123
}
115124

116125
login := pair[0]
@@ -131,11 +140,15 @@ func (auth *HMACAuth) Validate(_ context.Context, wr http.ResponseWriter, req *h
131140
return login, true
132141
}
133142
}
134-
requireBasicAuth(wr, req, auth.hiddenDomain)
135-
return "", false
143+
return requireBasicAuth(ctx, wr, req, auth.hiddenDomain, auth.next)
136144
}
137145

138146
func (auth *HMACAuth) Stop() {
147+
auth.stopOnce.Do(func() {
148+
if auth.next != nil {
149+
auth.next.Stop()
150+
}
151+
})
139152
}
140153

141154
func CalculateHMACSignature(secret []byte, username string, expire int64) []byte {

0 commit comments

Comments
 (0)