Files
x/handler/http2/auth_test.go
T
ginuerzh 6ccaae0573 refactor(handler/http2): split handler into 3 focused files with 52 tests
Extract authenticate/probe-resistance to auth.go and roundTrip/forwarding
to proxy.go, following the handler/http/ pattern. Export sentinel errors
(ErrAuthFailed, ErrWrongConnType) for precise test assertions. Fix
flushWriter panic recovery to handle non-string/non-error panics. Default
HTTPS port to 443 when no port is specified in the Host header.
2026-05-30 18:01:19 +08:00

183 lines
5.7 KiB
Go

package http2
import (
"context"
"encoding/base64"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/go-gost/core/auth"
"github.com/go-gost/core/handler"
)
func TestKnockMatch(t *testing.T) {
tests := []struct {
hostname string
knock string
want bool
}{
{"", "", false},
{"example.com", "", false},
{"example.com", "example.com", true},
{"EXAMPLE.COM", "example.com", true},
{"example.com", "foo,example.com,bar", true},
{"other.com", "foo, bar , baz", false},
{"other.com", "foo,bar,baz", false},
{"bar", "foo, bar , baz", true},
{"BAR", "foo, bar , baz", true},
}
for _, tt := range tests {
got := knockMatch(tt.hostname, tt.knock)
if got != tt.want {
t.Errorf("knockMatch(%q, %q) = %v, want %v", tt.hostname, tt.knock, got, tt.want)
}
}
}
func TestBasicProxyAuth(t *testing.T) {
h := newTestHandler()
tests := []struct {
name string
auth string
wantUser string
wantPass string
wantOK bool
}{
{"empty", "", "", "", false},
{"not basic", "Bearer token", "", "", false},
{"no colon", "Basic " + base64.StdEncoding.EncodeToString([]byte("nocolon")), "", "", false},
{"valid", "Basic " + base64.StdEncoding.EncodeToString([]byte("user:pass")), "user", "pass", true},
{"password with colon", "Basic " + base64.StdEncoding.EncodeToString([]byte("user:pass:word")), "user", "pass:word", true},
{"invalid base64", "Basic not-valid-base64!!!", "", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
user, pass, ok := h.basicProxyAuth(tt.auth)
if ok != tt.wantOK {
t.Errorf("ok = %v, want %v", ok, tt.wantOK)
}
if user != tt.wantUser {
t.Errorf("user = %q, want %q", user, tt.wantUser)
}
if pass != tt.wantPass {
t.Errorf("pass = %q, want %q", pass, tt.wantPass)
}
})
}
}
type testAuther struct {
ok bool
id string
}
func (a *testAuther) Authenticate(ctx context.Context, user, password string, opts ...auth.Option) (string, bool) {
return a.id, a.ok
}
func TestAuthenticate(t *testing.T) {
t.Run("no auther", func(t *testing.T) {
h := newTestHandler()
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
w := httptest.NewRecorder()
resp := &http.Response{Header: w.Header(), Body: io.NopCloser(strings.NewReader(""))}
id, ok := h.authenticate(context.Background(), w, req, resp, &testLogger{})
if !ok {
t.Error("expected ok without auther")
}
if id != "" {
t.Errorf("id = %q, want empty", id)
}
})
t.Run("auth succeeds", func(t *testing.T) {
h := newTestHandler(handler.AutherOption(&testAuther{ok: true, id: "client1"}))
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
req.Header.Set("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("user:pass")))
w := httptest.NewRecorder()
resp := &http.Response{Header: w.Header(), Body: io.NopCloser(strings.NewReader(""))}
id, ok := h.authenticate(context.Background(), w, req, resp, &testLogger{})
if !ok {
t.Error("expected ok with valid auth")
}
if id != "client1" {
t.Errorf("id = %q, want %q", id, "client1")
}
})
t.Run("auth fails - returns 407", func(t *testing.T) {
h := newTestHandler(handler.AutherOption(&testAuther{ok: false}))
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
w := httptest.NewRecorder()
resp := &http.Response{Header: w.Header(), Body: io.NopCloser(strings.NewReader(""))}
_, ok := h.authenticate(context.Background(), w, req, resp, &testLogger{})
if ok {
t.Error("expected !ok with failed auth")
}
if resp.StatusCode != http.StatusProxyAuthRequired {
t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusProxyAuthRequired)
}
if !strings.Contains(resp.Header.Get("Proxy-Authenticate"), "Basic") {
t.Error("missing Proxy-Authenticate header")
}
})
}
func TestAuthenticate_ProbeResistanceCode(t *testing.T) {
h := newTestHandler(handler.AutherOption(&testAuther{ok: false}))
h.md.probeResistance = &probeResistance{Type: "code", Value: "503"}
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
w := httptest.NewRecorder()
resp := &http.Response{Header: http.Header{}, Body: io.NopCloser(strings.NewReader(""))}
_, ok := h.authenticate(context.Background(), w, req, resp, &testLogger{})
if ok {
t.Error("expected !ok")
}
if resp.StatusCode != 503 {
t.Errorf("status = %d, want 503", resp.StatusCode)
}
}
func TestAuthenticate_ProbeResistanceKnockBypass(t *testing.T) {
h := newTestHandler(handler.AutherOption(&testAuther{ok: false}))
h.md.probeResistance = &probeResistance{Type: "code", Value: "403", Knock: "safe.example.com"}
req := httptest.NewRequest(http.MethodGet, "http://safe.example.com/", nil)
w := httptest.NewRecorder()
resp := &http.Response{Header: http.Header{}, Body: io.NopCloser(strings.NewReader(""))}
_, ok := h.authenticate(context.Background(), w, req, resp, &testLogger{})
if ok {
t.Error("expected !ok")
}
if resp.StatusCode != http.StatusProxyAuthRequired {
t.Errorf("status = %d, want %d (knock matched, probe resistance skipped)", resp.StatusCode, http.StatusProxyAuthRequired)
}
}
func TestAuthenticate_CustomRealm(t *testing.T) {
h := newTestHandler(handler.AutherOption(&testAuther{ok: false}))
h.md.authBasicRealm = "testrealm"
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
w := httptest.NewRecorder()
resp := &http.Response{Header: w.Header(), Body: io.NopCloser(strings.NewReader(""))}
_, ok := h.authenticate(context.Background(), w, req, resp, &testLogger{})
if ok {
t.Error("expected !ok")
}
if !strings.Contains(resp.Header.Get("Proxy-Authenticate"), `realm="testrealm"`) {
t.Errorf("Proxy-Authenticate = %q, want realm=testrealm", resp.Header.Get("Proxy-Authenticate"))
}
}