Skip to content

add Digest authentication for http proxy server #79

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 93 additions & 1 deletion common/auth/auth.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
package auth

import "github.com/sagernet/sing/common"
import (
"crypto/md5"
"crypto/sha256"
"encoding/hex"
"fmt"

"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/param"
)

const Realm = "sing-box"

type Challenge struct {
Username string
Nonce string
Algorithm string
Uri string
CNonce string
Nc string
Response string
}

type User struct {
Username string
Expand Down Expand Up @@ -28,3 +48,75 @@ func (au *Authenticator) Verify(username string, password string) bool {
passwordList, ok := au.userMap[username]
return ok && common.Contains(passwordList, password)
}

func (au *Authenticator) VerifyDigest(method string, uri string, s string) (string, bool) {
c, err := ParseChallenge(s)
if err != nil {
return "", false
}
if c.Username == "" || c.Nonce == "" || c.Nc == "" || c.CNonce == "" || c.Response == "" {
return "", false
}
if c.Uri != "" {
uri = c.Uri
}
passwordList, ok := au.userMap[c.Username]
if ok {
for _, password := range passwordList {
resp := ""
if c.Algorithm == "SHA-256" {
ha1 := sha256str(c.Username + ":" + Realm + ":" + password)
ha2 := sha256str(method + ":" + uri)
resp = sha256str(ha1 + ":" + c.Nonce + ":" + c.Nc + ":" + c.CNonce + ":auth:" + ha2)
} else {
ha1 := md5str(c.Username + ":" + Realm + ":" + password)
ha2 := md5str(method + ":" + uri)
resp = md5str(ha1 + ":" + c.Nonce + ":" + c.Nc + ":" + c.CNonce + ":auth:" + ha2)
}
if resp != "" && resp == c.Response {
return c.Username, true
}
}
}
return "", false
}

func ParseChallenge(s string) (*Challenge, error) {
pp, err := param.Parse(s)
if err != nil {
return nil, fmt.Errorf("digest: invalid challenge: %w", err)
}
var c Challenge

for _, p := range pp {
switch p.Key {
case "username":
c.Username = p.Value
case "nonce":
c.Nonce = p.Value
case "algorithm":
c.Algorithm = p.Value
case "uri":
c.Uri = p.Value
case "cnonce":
c.CNonce = p.Value
case "nc":
c.Nc = p.Value
case "response":
c.Response = p.Value
}
}
return &c, nil
}

func md5str(str string) string {
h := md5.New()
h.Write([]byte(str))
return hex.EncodeToString(h.Sum(nil))
}

func sha256str(str string) string {
h := sha256.New()
h.Write([]byte(str))
return hex.EncodeToString(h.Sum(nil))
}
10 changes: 4 additions & 6 deletions common/json/badjson/merge_objects.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package badjson

import (
"context"
"reflect"

E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
cJSON "github.com/sagernet/sing/common/json/internal/contextjson"
)

func MarshallObjects(objects ...any) ([]byte, error) {
Expand All @@ -31,16 +33,12 @@ func UnmarshallExcluded(inputContent []byte, parentObject any, object any) error
}

func UnmarshallExcludedContext(ctx context.Context, inputContent []byte, parentObject any, object any) error {
parentContent, err := newJSONObject(ctx, parentObject)
if err != nil {
return err
}
var content JSONObject
err = content.UnmarshalJSONContext(ctx, inputContent)
err := content.UnmarshalJSONContext(ctx, inputContent)
if err != nil {
return err
}
for _, key := range parentContent.Keys() {
for _, key := range cJSON.ObjectKeys(reflect.TypeOf(parentObject)) {
content.Remove(key)
}
if object == nil {
Expand Down
20 changes: 20 additions & 0 deletions common/json/internal/contextjson/keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package json

import (
"reflect"

"github.com/sagernet/sing/common"
)

func ObjectKeys(object reflect.Type) []string {
switch object.Kind() {
case reflect.Pointer:
return ObjectKeys(object.Elem())
case reflect.Struct:
default:
panic("invalid non-struct input")
}
return common.Map(cachedTypeFields(object).list, func(field field) string {
return field.name
})
}
25 changes: 25 additions & 0 deletions common/json/internal/contextjson/keys_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package json_test

import (
"reflect"
"testing"

json "github.com/sagernet/sing/common/json/internal/contextjson"

"github.com/stretchr/testify/require"
)

type MyObject struct {
Hello string `json:"hello,omitempty"`
MyWorld
MyWorld2 string `json:"-"`
}

type MyWorld struct {
World string `json:"world,omitempty"`
}

func TestObjectKeys(t *testing.T) {
keys := json.ObjectKeys(reflect.TypeOf(&MyObject{}))
require.Equal(t, []string{"hello", "world"}, keys)
}
189 changes: 189 additions & 0 deletions common/param/param.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package param

// code retrieve from https://github.com/icholy/digest/tree/master/internal/param

import (
"bufio"
"fmt"
"io"
"strconv"
"strings"
)

// Param is a key/value header parameter
type Param struct {
Key string
Value string
Quote bool
}

// String returns the formatted parameter
func (p Param) String() string {
if p.Quote {
return p.Key + "=" + strconv.Quote(p.Value)
}
return p.Key + "=" + p.Value
}

// Format formats the parameters to be included in the header
func Format(pp ...Param) string {
var b strings.Builder
for i, p := range pp {
if i > 0 {
b.WriteString(", ")
}
b.WriteString(p.String())
}
return b.String()
}

// Parse parses the header parameters
func Parse(s string) ([]Param, error) {
var pp []Param
br := bufio.NewReader(strings.NewReader(s))
for i := 0; true; i++ {
// skip whitespace
if err := skipWhite(br); err != nil {
return nil, err
}
// see if there's more to read
if _, err := br.Peek(1); err == io.EOF {
break
}
// read key/value pair
p, err := parseParam(br, i == 0)
if err != nil {
return nil, fmt.Errorf("param: %w", err)
}
pp = append(pp, p)
}
return pp, nil
}

func parseIdent(br *bufio.Reader) (string, error) {
var ident []byte
for {
b, err := br.ReadByte()
if err == io.EOF {
break
}
if err != nil {
return "", err
}
if !(('a' <= b && b <= 'z') || ('A' <= b && b <= 'Z') || '0' <= b && b <= '9' || b == '-') {
if err := br.UnreadByte(); err != nil {
return "", err
}
break
}
ident = append(ident, b)
}
return string(ident), nil
}

func parseByte(br *bufio.Reader, expect byte) error {
b, err := br.ReadByte()
if err != nil {
if err == io.EOF {
return fmt.Errorf("expected '%c', got EOF", expect)
}
return err
}
if b != expect {
return fmt.Errorf("expected '%c', got '%c'", expect, b)
}
return nil
}

func parseString(br *bufio.Reader) (string, error) {
var s []rune
// read the open quote
if err := parseByte(br, '"'); err != nil {
return "", err
}
// read the string
var escaped bool
for {
r, _, err := br.ReadRune()
if err != nil {
return "", err
}
if escaped {
s = append(s, r)
escaped = false
continue
}
if r == '\\' {
escaped = true
continue
}
// closing quote
if r == '"' {
break
}
s = append(s, r)
}
return string(s), nil
}

func skipWhite(br *bufio.Reader) error {
for {
b, err := br.ReadByte()
if err != nil {
if err == io.EOF {
return nil
}
return err
}
if b != ' ' {
return br.UnreadByte()
}
}
}

func parseParam(br *bufio.Reader, first bool) (Param, error) {
// skip whitespace
if err := skipWhite(br); err != nil {
return Param{}, err
}
if !first {
// read the comma separator
if err := parseByte(br, ','); err != nil {
return Param{}, err
}
// skip whitespace
if err := skipWhite(br); err != nil {
return Param{}, err
}
}
// read the key
key, err := parseIdent(br)
if err != nil {
return Param{}, err
}
// skip whitespace
if err := skipWhite(br); err != nil {
return Param{}, err
}
// read the equals sign
if err := parseByte(br, '='); err != nil {
return Param{}, err
}
// skip whitespace
if err := skipWhite(br); err != nil {
return Param{}, err
}
// read the value
var value string
var quote bool
if b, _ := br.Peek(1); len(b) == 1 && b[0] == '"' {
quote = true
value, err = parseString(br)
} else {
value, err = parseIdent(br)
}
if err != nil {
return Param{}, err
}
return Param{Key: key, Value: value, Quote: quote}, nil
}
6 changes: 2 additions & 4 deletions common/windnsapi/dnsapi_test.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
//go:build windows

package windnsapi

import (
"runtime"
"testing"

"github.com/stretchr/testify/require"
)

func TestDNSAPI(t *testing.T) {
if runtime.GOOS != "windows" {
t.SkipNow()
}
t.Parallel()
require.NoError(t, FlushResolverCache())
}
Loading