Skip to content

Commit d72bbb5

Browse files
committed
Merge pull request #43 from liggitt/ignore_whitespace
Make header matching case-insensitive, tolerate multiple headers, internal whitespace
2 parents f59cbbb + 3f60c1b commit d72bbb5

File tree

2 files changed

+155
-15
lines changed

2 files changed

+155
-15
lines changed

spnego/spnego_transport.go

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@ import (
1111
"github.com/apcera/gssapi"
1212
)
1313

14+
const negotiateScheme = "Negotiate"
15+
1416
// AddSPNEGONegotiate adds a Negotiate header with the value of a serialized
1517
// token to an http header.
1618
func AddSPNEGONegotiate(h http.Header, name string, token *gssapi.Buffer) {
1719
if name == "" {
1820
return
1921
}
2022

21-
v := "Negotiate"
23+
v := negotiateScheme
2224
if token.Length() != 0 {
2325
data := token.Bytes()
2426
v = v + " " + base64.StdEncoding.EncodeToString(data)
@@ -28,31 +30,48 @@ func AddSPNEGONegotiate(h http.Header, name string, token *gssapi.Buffer) {
2830

2931
// CheckSPNEGONegotiate checks for the presence of a Negotiate header. If
3032
// present, we return a gssapi Token created from the header value sent to us.
31-
func CheckSPNEGONegotiate(lib *gssapi.Lib, h http.Header, name string) (present bool, token *gssapi.Buffer) {
33+
func CheckSPNEGONegotiate(lib *gssapi.Lib, h http.Header, name string) (bool, *gssapi.Buffer) {
3234
var err error
3335
defer func() {
3436
if err != nil {
3537
lib.Debug(fmt.Sprintf("CheckSPNEGONegotiate: %v", err))
3638
}
3739
}()
3840

39-
v := h.Get(name)
40-
if len(v) == 0 || !strings.HasPrefix(v, "Negotiate") {
41-
return false, nil
42-
}
41+
for _, header := range h[http.CanonicalHeaderKey(name)] {
42+
if len(header) < len(negotiateScheme) {
43+
continue
44+
}
45+
if !strings.EqualFold(header[:len(negotiateScheme)], negotiateScheme) {
46+
continue
47+
}
4348

44-
present = true
45-
tbytes, err := base64.StdEncoding.DecodeString(strings.TrimSpace(v[len("Negotiate"):]))
46-
if err != nil {
47-
return false, nil
48-
}
49+
// Remove the "Negotiate" prefix
50+
normalizedToken := header[len(negotiateScheme):]
51+
// Trim leading and trailing whitespace
52+
normalizedToken = strings.TrimSpace(normalizedToken)
53+
// Remove internal whitespace (some servers insert whitespace every 76 chars)
54+
normalizedToken = strings.Replace(normalizedToken, " ", "", -1)
55+
// Pad to a multiple of 4 chars for base64 (some servers strip trailing padding)
56+
if unpaddedChars := len(normalizedToken) % 4; unpaddedChars != 0 {
57+
normalizedToken += strings.Repeat("=", 4-unpaddedChars)
58+
}
59+
60+
tbytes, err := base64.StdEncoding.DecodeString(normalizedToken)
61+
if err != nil {
62+
continue
63+
}
64+
65+
if len(tbytes) == 0 {
66+
return true, nil
67+
}
4968

50-
if len(tbytes) > 0 {
51-
token, err = lib.MakeBufferBytes(tbytes)
69+
token, err := lib.MakeBufferBytes(tbytes)
5270
if err != nil {
53-
return false, nil
71+
continue
5472
}
73+
return true, token
5574
}
5675

57-
return present, token
76+
return false, nil
5877
}

spnego/spnego_transport_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package spnego
2+
3+
import (
4+
"net/http"
5+
"testing"
6+
7+
"github.com/apcera/gssapi"
8+
)
9+
10+
func TestCheckSPNEGONegotiate(t *testing.T) {
11+
lib, err := gssapi.Load(nil)
12+
if err != nil {
13+
t.Fatalf("unexpected error: %v", err)
14+
}
15+
16+
name := "WWW-Authenticate"
17+
canonicalName := http.CanonicalHeaderKey(name)
18+
19+
testcases := map[string]struct {
20+
Headers http.Header
21+
Name string
22+
ExpectedPresent bool
23+
ExpectedToken string
24+
}{
25+
"empty": {
26+
Headers: http.Header{},
27+
Name: name,
28+
ExpectedPresent: false,
29+
ExpectedToken: "",
30+
},
31+
32+
"non-negotiate": {
33+
Headers: http.Header{canonicalName: []string{"Basic"}},
34+
Name: name,
35+
ExpectedPresent: false,
36+
ExpectedToken: "",
37+
},
38+
39+
"negotiate, no token": {
40+
Headers: http.Header{canonicalName: []string{"Negotiate"}},
41+
Name: name,
42+
ExpectedPresent: true,
43+
ExpectedToken: "",
44+
},
45+
"negotiate, case-insensitive": {
46+
Headers: http.Header{canonicalName: []string{"negotiate"}},
47+
Name: name,
48+
ExpectedPresent: true,
49+
ExpectedToken: "",
50+
},
51+
"negotiate, fallback from basic-auth": {
52+
Headers: http.Header{canonicalName: []string{"Basic", "Negotiate"}},
53+
Name: name,
54+
ExpectedPresent: true,
55+
ExpectedToken: "",
56+
},
57+
58+
"negotiate, with token": {
59+
Headers: http.Header{canonicalName: []string{"Negotiate aGVsbG8="}},
60+
Name: name,
61+
ExpectedPresent: true,
62+
ExpectedToken: "hello",
63+
},
64+
"negotiate, with token with whitespace": {
65+
Headers: http.Header{canonicalName: []string{"Negotiate aGVs bG8="}},
66+
Name: name,
67+
ExpectedPresent: true,
68+
ExpectedToken: "hello",
69+
},
70+
71+
"negotiate, with token needing no padding": {
72+
Headers: http.Header{canonicalName: []string{"Negotiate cGFk"}},
73+
Name: name,
74+
ExpectedPresent: true,
75+
ExpectedToken: "pad",
76+
},
77+
"negotiate, with token with 1 end-padding =": {
78+
Headers: http.Header{canonicalName: []string{"Negotiate cGFkXzE="}},
79+
Name: name,
80+
ExpectedPresent: true,
81+
ExpectedToken: "pad_1",
82+
},
83+
"negotiate, with token missing 1 end-padding =": {
84+
Headers: http.Header{canonicalName: []string{"Negotiate cGFkXzE"}},
85+
Name: name,
86+
ExpectedPresent: true,
87+
ExpectedToken: "pad_1",
88+
},
89+
"negotiate, with token with 2 end-padding =": {
90+
Headers: http.Header{canonicalName: []string{"Negotiate cGFkX19fMg=="}},
91+
Name: name,
92+
ExpectedPresent: true,
93+
ExpectedToken: "pad___2",
94+
},
95+
"negotiate, with token missing 2 end-padding =": {
96+
Headers: http.Header{canonicalName: []string{"Negotiate cGFkX19fMg"}},
97+
Name: name,
98+
ExpectedPresent: true,
99+
ExpectedToken: "pad___2",
100+
},
101+
102+
"negotiate, with invalid token": {
103+
Headers: http.Header{canonicalName: []string{"Negotiate !@#$%"}},
104+
Name: name,
105+
ExpectedPresent: false,
106+
ExpectedToken: "",
107+
},
108+
}
109+
110+
for k, tc := range testcases {
111+
present, token := CheckSPNEGONegotiate(lib, tc.Headers, tc.Name)
112+
if present != tc.ExpectedPresent {
113+
t.Errorf("%s: expected present=%v, got %v", k, tc.ExpectedPresent, present)
114+
continue
115+
}
116+
if token.String() != tc.ExpectedToken {
117+
t.Errorf("%s: expected token=%q, got %q", k, tc.ExpectedToken, token)
118+
continue
119+
}
120+
}
121+
}

0 commit comments

Comments
 (0)