6ccaae0573
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.
183 lines
5.7 KiB
Go
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"))
|
|
}
|
|
}
|