Skip to content

Commit 67c6781

Browse files
committed
add Digest authentication for http proxy server
https://datatracker.ietf.org/doc/html/rfc2617 server will send both Basic and Digest header to client client can use either Basic or Digest for authentication Change-Id: Iaa6629c143551770c836af3ead823bd148b244c6
1 parent 23af22f commit 67c6781

File tree

3 files changed

+322
-6
lines changed

3 files changed

+322
-6
lines changed

common/auth/auth.go

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
11
package auth
22

3-
import "github.com/sagernet/sing/common"
3+
import (
4+
"crypto/md5"
5+
"encoding/hex"
6+
"fmt"
7+
8+
"github.com/sagernet/sing/common"
9+
"github.com/sagernet/sing/common/param"
10+
)
11+
12+
const Realm = "sing-box"
13+
14+
type Challenge struct {
15+
Username string
16+
Nonce string
17+
CNonce string
18+
Nc string
19+
Response string
20+
}
421

522
type User struct {
623
Username string
@@ -28,3 +45,55 @@ func (au *Authenticator) Verify(username string, password string) bool {
2845
passwordList, ok := au.userMap[username]
2946
return ok && common.Contains(passwordList, password)
3047
}
48+
49+
func (au *Authenticator) VerifyDigest(method string, uri string, s string) (string, bool) {
50+
c, err := ParseChallenge(s)
51+
if err != nil {
52+
return "", false
53+
}
54+
if c.Username == "" || c.Nonce == "" || c.Nc == "" || c.CNonce == "" || c.Response == "" {
55+
return "", false
56+
}
57+
passwordList, ok := au.userMap[c.Username]
58+
if ok {
59+
for _, password := range passwordList {
60+
ha1 := md5str(c.Username + ":" + Realm + ":" + password)
61+
ha2 := md5str(method + ":" + uri)
62+
resp := md5str(ha1 + ":" + c.Nonce + ":" + c.Nc + ":" + c.CNonce + ":auth:" + ha2)
63+
if resp == c.Response {
64+
return c.Username, true
65+
}
66+
}
67+
}
68+
return "", false
69+
}
70+
71+
func ParseChallenge(s string) (*Challenge, error) {
72+
pp, err := param.Parse(s)
73+
if err != nil {
74+
return nil, fmt.Errorf("digest: invalid challenge: %w", err)
75+
}
76+
var c Challenge
77+
78+
for _, p := range pp {
79+
switch p.Key {
80+
case "username":
81+
c.Username = p.Value
82+
case "nonce":
83+
c.Nonce = p.Value
84+
case "cnonce":
85+
c.CNonce = p.Value
86+
case "nc":
87+
c.Nc = p.Value
88+
case "response":
89+
c.Response = p.Value
90+
}
91+
}
92+
return &c, nil
93+
}
94+
95+
func md5str(str string) string {
96+
h := md5.New()
97+
h.Write([]byte(str))
98+
return hex.EncodeToString(h.Sum(nil))
99+
}

common/param/param.go

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package param
2+
3+
// code retrieve from https://github.com/icholy/digest/tree/master/internal/param
4+
5+
import (
6+
"bufio"
7+
"fmt"
8+
"io"
9+
"strconv"
10+
"strings"
11+
)
12+
13+
// Param is a key/value header parameter
14+
type Param struct {
15+
Key string
16+
Value string
17+
Quote bool
18+
}
19+
20+
// String returns the formatted parameter
21+
func (p Param) String() string {
22+
if p.Quote {
23+
return p.Key + "=" + strconv.Quote(p.Value)
24+
}
25+
return p.Key + "=" + p.Value
26+
}
27+
28+
// Format formats the parameters to be included in the header
29+
func Format(pp ...Param) string {
30+
var b strings.Builder
31+
for i, p := range pp {
32+
if i > 0 {
33+
b.WriteString(", ")
34+
}
35+
b.WriteString(p.String())
36+
}
37+
return b.String()
38+
}
39+
40+
// Parse parses the header parameters
41+
func Parse(s string) ([]Param, error) {
42+
var pp []Param
43+
br := bufio.NewReader(strings.NewReader(s))
44+
for i := 0; true; i++ {
45+
// skip whitespace
46+
if err := skipWhite(br); err != nil {
47+
return nil, err
48+
}
49+
// see if there's more to read
50+
if _, err := br.Peek(1); err == io.EOF {
51+
break
52+
}
53+
// read key/value pair
54+
p, err := parseParam(br, i == 0)
55+
if err != nil {
56+
return nil, fmt.Errorf("param: %w", err)
57+
}
58+
pp = append(pp, p)
59+
}
60+
return pp, nil
61+
}
62+
63+
func parseIdent(br *bufio.Reader) (string, error) {
64+
var ident []byte
65+
for {
66+
b, err := br.ReadByte()
67+
if err == io.EOF {
68+
break
69+
}
70+
if err != nil {
71+
return "", err
72+
}
73+
if !(('a' <= b && b <= 'z') || ('A' <= b && b <= 'Z') || '0' <= b && b <= '9' || b == '-') {
74+
if err := br.UnreadByte(); err != nil {
75+
return "", err
76+
}
77+
break
78+
}
79+
ident = append(ident, b)
80+
}
81+
return string(ident), nil
82+
}
83+
84+
func parseByte(br *bufio.Reader, expect byte) error {
85+
b, err := br.ReadByte()
86+
if err != nil {
87+
if err == io.EOF {
88+
return fmt.Errorf("expected '%c', got EOF", expect)
89+
}
90+
return err
91+
}
92+
if b != expect {
93+
return fmt.Errorf("expected '%c', got '%c'", expect, b)
94+
}
95+
return nil
96+
}
97+
98+
func parseString(br *bufio.Reader) (string, error) {
99+
var s []rune
100+
// read the open quote
101+
if err := parseByte(br, '"'); err != nil {
102+
return "", err
103+
}
104+
// read the string
105+
var escaped bool
106+
for {
107+
r, _, err := br.ReadRune()
108+
if err != nil {
109+
return "", err
110+
}
111+
if escaped {
112+
s = append(s, r)
113+
escaped = false
114+
continue
115+
}
116+
if r == '\\' {
117+
escaped = true
118+
continue
119+
}
120+
// closing quote
121+
if r == '"' {
122+
break
123+
}
124+
s = append(s, r)
125+
}
126+
return string(s), nil
127+
}
128+
129+
func skipWhite(br *bufio.Reader) error {
130+
for {
131+
b, err := br.ReadByte()
132+
if err != nil {
133+
if err == io.EOF {
134+
return nil
135+
}
136+
return err
137+
}
138+
if b != ' ' {
139+
return br.UnreadByte()
140+
}
141+
}
142+
}
143+
144+
func parseParam(br *bufio.Reader, first bool) (Param, error) {
145+
// skip whitespace
146+
if err := skipWhite(br); err != nil {
147+
return Param{}, err
148+
}
149+
if !first {
150+
// read the comma separator
151+
if err := parseByte(br, ','); err != nil {
152+
return Param{}, err
153+
}
154+
// skip whitespace
155+
if err := skipWhite(br); err != nil {
156+
return Param{}, err
157+
}
158+
}
159+
// read the key
160+
key, err := parseIdent(br)
161+
if err != nil {
162+
return Param{}, err
163+
}
164+
// skip whitespace
165+
if err := skipWhite(br); err != nil {
166+
return Param{}, err
167+
}
168+
// read the equals sign
169+
if err := parseByte(br, '='); err != nil {
170+
return Param{}, err
171+
}
172+
// skip whitespace
173+
if err := skipWhite(br); err != nil {
174+
return Param{}, err
175+
}
176+
// read the value
177+
var value string
178+
var quote bool
179+
if b, _ := br.Peek(1); len(b) == 1 && b[0] == '"' {
180+
quote = true
181+
value, err = parseString(br)
182+
} else {
183+
value, err = parseIdent(br)
184+
}
185+
if err != nil {
186+
return Param{}, err
187+
}
188+
return Param{Key: key, Value: value, Quote: quote}, nil
189+
}

protocol/http/handshake.go

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package http
33
import (
44
std_bufio "bufio"
55
"context"
6+
"crypto/rand"
67
"encoding/base64"
8+
"encoding/hex"
79
"io"
810
"net"
911
"net/http"
@@ -42,6 +44,12 @@ func HandleConnectionEx(
4244
authOk bool
4345
)
4446
authorization := request.Header.Get("Proxy-Authorization")
47+
if strings.HasPrefix(authorization, "Digest ") {
48+
username, authOk = authenticator.VerifyDigest(request.Method, request.RequestURI, authorization[7:])
49+
if authOk {
50+
ctx = auth.ContextWithUser(ctx, username)
51+
}
52+
}
4553
if strings.HasPrefix(authorization, "Basic ") {
4654
userPassword, _ := base64.URLEncoding.DecodeString(authorization[6:])
4755
userPswdArr := strings.SplitN(string(userPassword), ":", 2)
@@ -56,10 +64,31 @@ func HandleConnectionEx(
5664
}
5765
if !authOk {
5866
// Since no one else is using the library, use a fixed realm until rewritten
59-
err = responseWith(
60-
request, http.StatusProxyAuthRequired,
61-
"Proxy-Authenticate", `Basic realm="sing-box" charset="UTF-8"`,
62-
).Write(conn)
67+
// define realm in common/auth package, still "sing-box" now
68+
nonce := "";
69+
randomBytes := make([]byte, 16)
70+
_, err = rand.Read(randomBytes)
71+
if err == nil {
72+
nonce = hex.EncodeToString(randomBytes)
73+
}
74+
if nonce == "" {
75+
err = responseWithBody(
76+
request, http.StatusProxyAuthRequired,
77+
"Proxy authentication required",
78+
"Content-Type", "text/plain; charset=utf-8",
79+
"Proxy-Authenticate", "Basic realm=\"" + auth.Realm + "\"",
80+
"Connection", "close",
81+
).Write(conn)
82+
} else {
83+
err = responseWithBody(
84+
request, http.StatusProxyAuthRequired,
85+
"Proxy authentication required",
86+
"Content-Type", "text/plain; charset=utf-8",
87+
"Proxy-Authenticate", "Basic realm=\"" + auth.Realm + "\"",
88+
"Proxy-Authenticate", "Digest realm=\"" + auth.Realm + "\", nonce=\"" + nonce + "\", qop=\"auth\", stale=false",
89+
"Connection", "close",
90+
).Write(conn)
91+
}
6392
if err != nil {
6493
return err
6594
}
@@ -68,7 +97,8 @@ func HandleConnectionEx(
6897
} else if authorization != "" {
6998
return E.New("http: authentication failed, Proxy-Authorization=", authorization)
7099
} else {
71-
return E.New("http: authentication failed, no Proxy-Authorization header")
100+
//return E.New("http: authentication failed, no Proxy-Authorization header")
101+
continue
72102
}
73103
}
74104
}
@@ -270,3 +300,31 @@ func responseWith(request *http.Request, statusCode int, headers ...string) *htt
270300
Header: header,
271301
}
272302
}
303+
304+
func responseWithBody(request *http.Request, statusCode int, body string, headers ...string) *http.Response {
305+
var header http.Header
306+
if len(headers) > 0 {
307+
header = make(http.Header)
308+
for i := 0; i < len(headers); i += 2 {
309+
header.Add(headers[i], headers[i+1])
310+
}
311+
}
312+
var bodyReadCloser io.ReadCloser
313+
var bodyContentLength = int64(0)
314+
if body != "" {
315+
bodyReadCloser = io.NopCloser(strings.NewReader(body))
316+
bodyContentLength = int64(len(body))
317+
}
318+
return &http.Response{
319+
StatusCode: statusCode,
320+
Status: http.StatusText(statusCode),
321+
Proto: request.Proto,
322+
ProtoMajor: request.ProtoMajor,
323+
ProtoMinor: request.ProtoMinor,
324+
Header: header,
325+
Body: bodyReadCloser,
326+
ContentLength: bodyContentLength,
327+
Close: true,
328+
}
329+
}
330+

0 commit comments

Comments
 (0)