mirror of
https://github.com/wweir/sower.git
synced 2026-10-21 05:51:38 +00:00
feat(sowerd): reverse-proxy HTTP fake-site fallback with edge headers
Unmatched HTTP/1.x browsing is reverse-proxied to fake_site instead of TCP-spliced, preserving Host. Site routes and this path overwrite X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Proto, and X-Real-IP from the TLS peer and drop client-supplied forwarding headers. Non-HTTP fallback, including sower auth failure, still relays the original bytes.
This commit is contained in:
+53
-10
@@ -368,24 +368,35 @@ func handleConn(conn net.Conn, fakeSite string, router siteRouter, handlers []pr
|
||||
slog.Debug("protocol auth or decode failed, fallback", "protocol", handler.Name(), "error", err)
|
||||
_ = rereadConn.SetReadDeadline(time.Time{})
|
||||
rereadConn.Stop().Reread()
|
||||
dur, err = fallbackConn(rereadConn, conn, fakeSite, router, &hijacked)
|
||||
dur, err = fallbackConn(rereadConn, conn, fakeSite, router, &hijacked, probeBuf)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
probeBuf = extendHTTPRequestProbe(rereadConn, probeBuf)
|
||||
rereadConn.Stop().Reread()
|
||||
_ = rereadConn.SetReadDeadline(time.Time{})
|
||||
dur, err = fallbackConn(rereadConn, conn, fakeSite, router, &hijacked)
|
||||
dur, err = fallbackConn(rereadConn, conn, fakeSite, router, &hijacked, probeBuf)
|
||||
}
|
||||
|
||||
func fallbackConn(conn net.Conn, tlsConn net.Conn, fakeSite string, router siteRouter, hijacked *atomic.Bool) (time.Duration, error) {
|
||||
func fallbackConn(conn net.Conn, tlsConn net.Conn, fakeSite string, router siteRouter, hijacked *atomic.Bool, probe []byte) (time.Duration, error) {
|
||||
start := time.Now()
|
||||
if entry := router.lookup(sniFromConn(tlsConn)); entry != nil {
|
||||
return time.Since(start), reverseProxyConn(conn, entry, hijacked)
|
||||
}
|
||||
if httpRequestLineProbe(probe) {
|
||||
return time.Since(start), reverseProxyConn(conn, newFakeSiteEntry(fakeSite), hijacked)
|
||||
}
|
||||
return relay.RelayTo(conn, fakeSite)
|
||||
}
|
||||
|
||||
func newFakeSiteEntry(fakeSite string) *siteEntry {
|
||||
return &siteEntry{
|
||||
upstream: &url.URL{Scheme: "http", Host: fakeSite},
|
||||
preserveHost: true,
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizeConfig(cfg config.SowerdConfig) map[string]any {
|
||||
return map[string]any{
|
||||
"log_level": cfg.LogLevel.String(),
|
||||
@@ -481,8 +492,9 @@ type pathRoute struct {
|
||||
// siteEntry is the routing target for one site route: a default upstream plus
|
||||
// optional path-prefix overrides.
|
||||
type siteEntry struct {
|
||||
upstream *url.URL
|
||||
paths []pathRoute
|
||||
upstream *url.URL
|
||||
paths []pathRoute
|
||||
preserveHost bool // fake-site fallback keeps the original Host header
|
||||
}
|
||||
|
||||
// resolve returns the upstream URL for the given request path. A configured
|
||||
@@ -546,6 +558,31 @@ func sniFromConn(conn net.Conn) string {
|
||||
return tlsConn.ConnectionState().ServerName
|
||||
}
|
||||
|
||||
func stripResidualForwardingHeaders(h http.Header) {
|
||||
h.Del("X-Real-IP")
|
||||
for k := range h {
|
||||
if strings.HasPrefix(k, "X-Forwarded-") {
|
||||
h.Del(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setEdgeProxyHeaders overwrites forwarding headers from the TLS-edge peer.
|
||||
// ReverseProxy.Rewrite already deleted Forwarded / X-Forwarded-For /
|
||||
// X-Forwarded-Host / X-Forwarded-Proto and does not auto-populate them.
|
||||
// X-Forwarded-Proto is https because this path is the 443 listener after TLS
|
||||
// termination (req.TLS is nil on the decrypted conn).
|
||||
func setEdgeProxyHeaders(req *http.Request, forwardedHost, remoteAddr string) {
|
||||
if forwardedHost != "" {
|
||||
req.Header.Set("X-Forwarded-Host", forwardedHost)
|
||||
}
|
||||
req.Header.Set("X-Forwarded-Proto", "https")
|
||||
if clientIP, _, err := net.SplitHostPort(remoteAddr); err == nil {
|
||||
req.Header.Set("X-Forwarded-For", clientIP)
|
||||
req.Header.Set("X-Real-IP", clientIP)
|
||||
}
|
||||
}
|
||||
|
||||
// reverseProxyConn serves the decrypted HTTP connection through a reverse
|
||||
// proxy to the upstream selected per request by the site entry's path routes.
|
||||
// Reverse proxies and their transports are created lazily per upstream and
|
||||
@@ -559,11 +596,17 @@ func reverseProxyConn(conn net.Conn, entry *siteEntry, hijacked *atomic.Bool) er
|
||||
if proxy, ok := proxies[key]; ok {
|
||||
return proxy
|
||||
}
|
||||
proxy := httputil.NewSingleHostReverseProxy(upstream)
|
||||
baseDirector := proxy.Director
|
||||
proxy.Director = func(req *http.Request) {
|
||||
baseDirector(req)
|
||||
req.Host = upstream.Host
|
||||
proxy := &httputil.ReverseProxy{
|
||||
Rewrite: func(pr *httputil.ProxyRequest) {
|
||||
pr.SetURL(upstream)
|
||||
if entry.preserveHost {
|
||||
pr.Out.Host = pr.In.Host
|
||||
} else {
|
||||
pr.Out.Host = upstream.Host
|
||||
}
|
||||
stripResidualForwardingHeaders(pr.Out.Header)
|
||||
setEdgeProxyHeaders(pr.Out, pr.In.Host, pr.In.RemoteAddr)
|
||||
},
|
||||
}
|
||||
transport := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
|
||||
+339
-27
@@ -1,12 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/sha1"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -274,6 +272,125 @@ func TestReverseProxyConnHTTP(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type connWithRemoteAddr struct {
|
||||
net.Conn
|
||||
addr net.Addr
|
||||
}
|
||||
|
||||
func (c connWithRemoteAddr) RemoteAddr() net.Addr { return c.addr }
|
||||
|
||||
func TestReverseProxyConnForwardingHeaders(t *testing.T) {
|
||||
const clientIP = "203.0.113.10"
|
||||
gotCh := make(chan *http.Request, 1)
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotCh <- r.Clone(r.Context())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer upstream.Close()
|
||||
|
||||
upstreamURL, _ := url.Parse(upstream.URL)
|
||||
rawServer, clientConn := net.Pipe()
|
||||
serverConn := connWithRemoteAddr{
|
||||
Conn: rawServer,
|
||||
addr: &net.TCPAddr{IP: net.ParseIP(clientIP), Port: 54321},
|
||||
}
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
errCh <- reverseProxyConn(serverConn, &siteEntry{upstream: upstreamURL}, &atomic.Bool{})
|
||||
}()
|
||||
go func() {
|
||||
_, _ = clientConn.Write([]byte(
|
||||
"GET /test HTTP/1.1\r\n" +
|
||||
"Host: a.example.com\r\n" +
|
||||
"X-Forwarded-For: 1.2.3.4\r\n" +
|
||||
"X-Forwarded-Proto: http\r\n" +
|
||||
"X-Forwarded-Host: spoofed.example\r\n" +
|
||||
"X-Forwarded-Port: 80\r\n" +
|
||||
"X-Real-IP: 1.2.3.4\r\n\r\n"))
|
||||
}()
|
||||
|
||||
_ = clientConn.SetReadDeadline(time.Now().Add(2 * time.Second))
|
||||
buf := make([]byte, 4096)
|
||||
_, _ = clientConn.Read(buf)
|
||||
_ = clientConn.Close()
|
||||
|
||||
select {
|
||||
case got := <-gotCh:
|
||||
if got.Host != upstreamURL.Host {
|
||||
t.Errorf("Host = %q, want upstream %q", got.Host, upstreamURL.Host)
|
||||
}
|
||||
if got.Header.Get("X-Forwarded-For") != clientIP {
|
||||
t.Errorf("X-Forwarded-For = %q, want %q", got.Header.Get("X-Forwarded-For"), clientIP)
|
||||
}
|
||||
if got.Header.Get("X-Real-IP") != clientIP {
|
||||
t.Errorf("X-Real-IP = %q, want %q", got.Header.Get("X-Real-IP"), clientIP)
|
||||
}
|
||||
if got.Header.Get("X-Forwarded-Proto") != "https" {
|
||||
t.Errorf("X-Forwarded-Proto = %q, want https", got.Header.Get("X-Forwarded-Proto"))
|
||||
}
|
||||
if got.Header.Get("X-Forwarded-Host") != "a.example.com" {
|
||||
t.Errorf("X-Forwarded-Host = %q, want a.example.com", got.Header.Get("X-Forwarded-Host"))
|
||||
}
|
||||
if got.Header.Get("X-Forwarded-Port") != "" {
|
||||
t.Errorf("X-Forwarded-Port = %q, want empty", got.Header.Get("X-Forwarded-Port"))
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("upstream did not receive request")
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
t.Fatalf("reverseProxyConn error: %v", err)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("reverseProxyConn timed out")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReverseProxyConnPreserveHost(t *testing.T) {
|
||||
gotCh := make(chan string, 1)
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotCh <- r.Host
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer upstream.Close()
|
||||
|
||||
upstreamURL, _ := url.Parse(upstream.URL)
|
||||
serverConn, clientConn := net.Pipe()
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
errCh <- reverseProxyConn(serverConn, &siteEntry{upstream: upstreamURL, preserveHost: true}, &atomic.Bool{})
|
||||
}()
|
||||
go func() {
|
||||
_, _ = clientConn.Write([]byte("GET / HTTP/1.1\r\nHost: miss.example.com\r\n\r\n"))
|
||||
}()
|
||||
|
||||
_ = clientConn.SetReadDeadline(time.Now().Add(2 * time.Second))
|
||||
buf := make([]byte, 4096)
|
||||
_, _ = clientConn.Read(buf)
|
||||
_ = clientConn.Close()
|
||||
|
||||
select {
|
||||
case got := <-gotCh:
|
||||
if got != "miss.example.com" {
|
||||
t.Fatalf("Host = %q, want miss.example.com", got)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("upstream did not receive request")
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
t.Fatalf("reverseProxyConn error: %v", err)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("reverseProxyConn timed out")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleConnRoutesFallbackBySNI(t *testing.T) {
|
||||
var upstreamHost string
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -588,7 +705,13 @@ func websocketAcceptKey(key string) string {
|
||||
}
|
||||
|
||||
func TestHandleConnFallsBackToFakeSiteWhenSNIMisses(t *testing.T) {
|
||||
fakeSite := startHTTP1TCPServer(t, "fake-site-fallback")
|
||||
gotCh := make(chan *http.Request, 1)
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotCh <- r.Clone(r.Context())
|
||||
_, _ = w.Write([]byte("fake-site-fallback"))
|
||||
}))
|
||||
defer upstream.Close()
|
||||
upstreamURL, _ := url.Parse(upstream.URL)
|
||||
|
||||
certServer := httptest.NewTLSServer(http.NotFoundHandler())
|
||||
cert := certServer.TLS.Certificates[0]
|
||||
@@ -609,14 +732,21 @@ func TestHandleConnFallsBackToFakeSiteWhenSNIMisses(t *testing.T) {
|
||||
router := newSiteRouter([]config.SiteRoute{
|
||||
{Domains: []string{"route.example.com"}, Upstream: "http://127.0.0.1:1"},
|
||||
})
|
||||
go handleConn(serverTLS, fakeSite, router, []proxyProtocolHandler{newSowerProtocolHandler(transportSower.New("secret"))})
|
||||
go handleConn(serverTLS, upstreamURL.Host, router, []proxyProtocolHandler{newSowerProtocolHandler(transportSower.New("secret"))})
|
||||
|
||||
if err := clientTLS.Handshake(); err != nil {
|
||||
t.Fatalf("tls handshake: %v", err)
|
||||
}
|
||||
|
||||
padding := strings.Repeat("x", 512)
|
||||
_, _ = clientTLS.Write([]byte("GET /fallback HTTP/1.1\r\nHost: miss.example.com\r\nX-Pad: " + padding + "\r\n\r\n"))
|
||||
_, _ = clientTLS.Write([]byte(
|
||||
"GET /fallback HTTP/1.1\r\n" +
|
||||
"Host: miss.example.com\r\n" +
|
||||
"X-Forwarded-For: 1.2.3.4\r\n" +
|
||||
"X-Forwarded-Proto: http\r\n" +
|
||||
"X-Forwarded-Host: spoofed.example\r\n" +
|
||||
"X-Real-IP: 1.2.3.4\r\n" +
|
||||
"X-Pad: " + padding + "\r\n\r\n"))
|
||||
|
||||
_ = clientTLS.SetReadDeadline(time.Now().Add(2 * time.Second))
|
||||
buf := make([]byte, 4096)
|
||||
@@ -628,6 +758,123 @@ func TestHandleConnFallsBackToFakeSiteWhenSNIMisses(t *testing.T) {
|
||||
if !strings.Contains(resp, "fake-site-fallback") {
|
||||
t.Fatalf("response does not contain fake site body: %q", resp)
|
||||
}
|
||||
|
||||
select {
|
||||
case got := <-gotCh:
|
||||
if got.Host != "miss.example.com" {
|
||||
t.Errorf("Host = %q, want miss.example.com", got.Host)
|
||||
}
|
||||
if got.Header.Get("X-Forwarded-Proto") != "https" {
|
||||
t.Errorf("X-Forwarded-Proto = %q, want https", got.Header.Get("X-Forwarded-Proto"))
|
||||
}
|
||||
if got.Header.Get("X-Forwarded-Host") != "miss.example.com" {
|
||||
t.Errorf("X-Forwarded-Host = %q, want miss.example.com", got.Header.Get("X-Forwarded-Host"))
|
||||
}
|
||||
if got.Header.Get("X-Forwarded-For") == "1.2.3.4" {
|
||||
t.Errorf("X-Forwarded-For still spoofed: %q", got.Header.Get("X-Forwarded-For"))
|
||||
}
|
||||
if got.Header.Get("X-Real-IP") == "1.2.3.4" {
|
||||
t.Errorf("X-Real-IP still spoofed: %q", got.Header.Get("X-Real-IP"))
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("upstream did not receive request")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleConnFakeSiteHTTPSplitRequestLineSetsProxyHeaders(t *testing.T) {
|
||||
gotCh := make(chan *http.Request, 1)
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotCh <- r.Clone(r.Context())
|
||||
_, _ = w.Write([]byte("split-fallback"))
|
||||
}))
|
||||
defer upstream.Close()
|
||||
upstreamURL, _ := url.Parse(upstream.URL)
|
||||
|
||||
certServer := httptest.NewTLSServer(http.NotFoundHandler())
|
||||
cert := certServer.TLS.Certificates[0]
|
||||
certServer.Close()
|
||||
|
||||
serverRaw, clientRaw := net.Pipe()
|
||||
serverTLS := tls.Server(serverRaw, &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
NextProtos: []string{"http/1.1"},
|
||||
})
|
||||
clientTLS := tls.Client(clientRaw, &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: "miss.example.com",
|
||||
NextProtos: []string{"http/1.1"},
|
||||
})
|
||||
defer clientTLS.Close()
|
||||
|
||||
go handleConn(serverTLS, upstreamURL.Host, siteRouter{}, []proxyProtocolHandler{newSowerProtocolHandler(transportSower.New("secret"))})
|
||||
|
||||
if err := clientTLS.Handshake(); err != nil {
|
||||
t.Fatalf("tls handshake: %v", err)
|
||||
}
|
||||
|
||||
_, _ = clientTLS.Write([]byte("GET /fall"))
|
||||
_, _ = clientTLS.Write([]byte("back HTTP/1.1\r\nHost: miss.example.com\r\n\r\n"))
|
||||
|
||||
_ = clientTLS.SetReadDeadline(time.Now().Add(2 * time.Second))
|
||||
buf := make([]byte, 4096)
|
||||
n, err := clientTLS.Read(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("read response: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(buf[:n]), "split-fallback") {
|
||||
t.Fatalf("response does not contain fake site body: %q", buf[:n])
|
||||
}
|
||||
|
||||
select {
|
||||
case got := <-gotCh:
|
||||
if got.Header.Get("X-Forwarded-Proto") != "https" {
|
||||
t.Errorf("X-Forwarded-Proto = %q, want https", got.Header.Get("X-Forwarded-Proto"))
|
||||
}
|
||||
if got.Host != "miss.example.com" {
|
||||
t.Errorf("Host = %q, want miss.example.com", got.Host)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("upstream did not receive request")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleConnRelaysNonHTTPFakeSiteWhenProbeMisses(t *testing.T) {
|
||||
fakeSite := startRawTCPServer(t, "raw-fallback")
|
||||
|
||||
certServer := httptest.NewTLSServer(http.NotFoundHandler())
|
||||
cert := certServer.TLS.Certificates[0]
|
||||
certServer.Close()
|
||||
|
||||
serverRaw, clientRaw := net.Pipe()
|
||||
serverTLS := tls.Server(serverRaw, &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
NextProtos: []string{"http/1.1"},
|
||||
})
|
||||
clientTLS := tls.Client(clientRaw, &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: "miss.example.com",
|
||||
NextProtos: []string{"http/1.1"},
|
||||
})
|
||||
defer clientTLS.Close()
|
||||
|
||||
go handleConn(serverTLS, fakeSite, siteRouter{}, []proxyProtocolHandler{newSowerProtocolHandler(transportSower.New("secret"))})
|
||||
|
||||
if err := clientTLS.Handshake(); err != nil {
|
||||
t.Fatalf("tls handshake: %v", err)
|
||||
}
|
||||
_ = clientTLS.SetDeadline(time.Now().Add(2 * time.Second))
|
||||
if _, err := clientTLS.Write([]byte("not-http-payload")); err != nil {
|
||||
t.Fatalf("write non-http payload: %v", err)
|
||||
}
|
||||
|
||||
buf := make([]byte, 4096)
|
||||
n, err := clientTLS.Read(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("read fallback response: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(buf[:n]), "raw-fallback") {
|
||||
t.Fatalf("response does not contain raw fallback body: %q", buf[:n])
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleConnFallsBackAfterSowerAuthFailure(t *testing.T) {
|
||||
@@ -670,33 +917,98 @@ func TestHandleConnFallsBackAfterSowerAuthFailure(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func startHTTP1TCPServer(t *testing.T, body string) string {
|
||||
t.Helper()
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen fake site: %v", err)
|
||||
func TestHTTPRequestLineProbe(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want bool
|
||||
}{
|
||||
{"GET / HTTP/1.1\r\nHost: x\r\n\r\n", true},
|
||||
{"POST /submit HTTP/1.0\r\n\r\n", true},
|
||||
{"CONNECT example.com:443 HTTP/1.1\r\n", true},
|
||||
{"GET / HTTP/1.1\nHost: x\n\n", true},
|
||||
{"GET / HTTP/1.1", true},
|
||||
{"GET / HTTP/1.1\r", true},
|
||||
{"GET / HTTP/1.10\r\n", false},
|
||||
{"GET / HTTP/1.1 extra\r\n", false},
|
||||
{"GET /fall", false},
|
||||
{"hello", false},
|
||||
{"\x80xxxx", false},
|
||||
{"GETX / HTTP/1.1\r\n", false},
|
||||
}
|
||||
t.Cleanup(func() { _ = ln.Close() })
|
||||
for _, tt := range tests {
|
||||
if got := httpRequestLineProbe([]byte(tt.in)); got != tt.want {
|
||||
t.Errorf("httpRequestLineProbe(%q) = %v, want %v", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCouldBeHTTPRequestLine(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want bool
|
||||
}{
|
||||
{"G", true},
|
||||
{"GET", true},
|
||||
{"GET /fall", true},
|
||||
{"GET / HTTP/1.", true},
|
||||
{"GET / HTTP/1.1\r", true},
|
||||
{"GET / HTTP/1.1\r\n", false},
|
||||
{"hello", false},
|
||||
{"\x80", false},
|
||||
{"GET / HTTP/1.10", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := couldBeHTTPRequestLine([]byte(tt.in)); got != tt.want {
|
||||
t.Errorf("couldBeHTTPRequestLine(%q) = %v, want %v", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFakeSiteEntry(t *testing.T) {
|
||||
e := newFakeSiteEntry("127.0.0.1:9080")
|
||||
if e.upstream.String() != "http://127.0.0.1:9080" {
|
||||
t.Fatalf("upstream = %q, want http://127.0.0.1:9080", e.upstream.String())
|
||||
}
|
||||
if !e.preserveHost {
|
||||
t.Fatal("preserveHost = false, want true")
|
||||
}
|
||||
v6 := newFakeSiteEntry("[::1]:8080")
|
||||
if v6.upstream.Host != "[::1]:8080" {
|
||||
t.Fatalf("ipv6 host = %q, want [::1]:8080", v6.upstream.Host)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtendHTTPRequestProbeReadsSplitRequestLine(t *testing.T) {
|
||||
server, client := net.Pipe()
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
|
||||
go func() {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
r := bufio.NewReader(conn)
|
||||
for {
|
||||
line, err := r.ReadString('\n')
|
||||
if err != nil || line == "\r\n" || line == "\n" {
|
||||
break
|
||||
}
|
||||
}
|
||||
_, _ = conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Length: " + fmt.Sprint(len(body)) + "\r\n\r\n" + body))
|
||||
_, _ = client.Write([]byte("GET /fall"))
|
||||
_, _ = client.Write([]byte("back HTTP/1.1\r\nHost: x\r\n\r\n"))
|
||||
}()
|
||||
|
||||
return ln.Addr().String()
|
||||
_ = server.SetReadDeadline(time.Now().Add(2 * time.Second))
|
||||
first := make([]byte, protocolProbeMaxBytes)
|
||||
n, err := server.Read(first)
|
||||
if err != nil {
|
||||
t.Fatalf("first read: %v", err)
|
||||
}
|
||||
got := extendHTTPRequestProbe(server, first[:n])
|
||||
if !httpRequestLineProbe(got) {
|
||||
t.Fatalf("extended probe not HTTP request line: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtendHTTPRequestProbeSkipsNonHTTP(t *testing.T) {
|
||||
server, client := net.Pipe()
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
|
||||
got := extendHTTPRequestProbe(server, []byte("not-http"))
|
||||
if string(got) != "not-http" {
|
||||
t.Fatalf("probe = %q, want not-http", got)
|
||||
}
|
||||
}
|
||||
|
||||
func startRawTCPServer(t *testing.T, body string) string {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
@@ -57,6 +58,105 @@ func (h sowerProtocolHandler) Unwrap(conn net.Conn) (net.Addr, error) {
|
||||
return h.transport.Unwrap(conn)
|
||||
}
|
||||
|
||||
func httpRequestLine(buf []byte) (line []byte, complete bool) {
|
||||
i := bytes.IndexByte(buf, '\n')
|
||||
if i < 0 {
|
||||
return buf, false
|
||||
}
|
||||
line = buf[:i]
|
||||
if len(line) > 0 && line[len(line)-1] == '\r' {
|
||||
line = line[:len(line)-1]
|
||||
}
|
||||
return line, true
|
||||
}
|
||||
|
||||
func httpRequestLineProbe(buf []byte) bool {
|
||||
line, _ := httpRequestLine(buf)
|
||||
method, rest, ok := bytes.Cut(line, []byte{' '})
|
||||
if !ok || !isExactHTTPMethod(method) {
|
||||
return false
|
||||
}
|
||||
target, version, ok := bytes.Cut(rest, []byte{' '})
|
||||
if !ok || len(target) == 0 {
|
||||
return false
|
||||
}
|
||||
version = bytes.TrimSuffix(version, []byte{'\r'})
|
||||
if bytes.IndexByte(version, ' ') >= 0 {
|
||||
return false
|
||||
}
|
||||
return string(version) == "HTTP/1.1" || string(version) == "HTTP/1.0"
|
||||
}
|
||||
|
||||
func isExactHTTPMethod(m []byte) bool {
|
||||
switch string(m) {
|
||||
case "GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "TRACE", "CONNECT":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isHTTPMethodPrefix(b []byte) bool {
|
||||
if len(b) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, method := range []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "TRACE", "CONNECT"} {
|
||||
if bytes.HasPrefix([]byte(method), b) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isHTTPVersionPrefix(b []byte) bool {
|
||||
b = bytes.TrimSuffix(b, []byte{'\r'})
|
||||
return bytes.HasPrefix([]byte("HTTP/1.1"), b) || bytes.HasPrefix([]byte("HTTP/1.0"), b)
|
||||
}
|
||||
|
||||
func couldBeHTTPRequestLine(buf []byte) bool {
|
||||
line, complete := httpRequestLine(buf)
|
||||
if complete {
|
||||
return false
|
||||
}
|
||||
method, rest, hasSpace := bytes.Cut(line, []byte{' '})
|
||||
if !hasSpace {
|
||||
return isHTTPMethodPrefix(method)
|
||||
}
|
||||
if !isExactHTTPMethod(method) {
|
||||
return false
|
||||
}
|
||||
_, version, hasVersion := bytes.Cut(rest, []byte{' '})
|
||||
if !hasVersion {
|
||||
return true
|
||||
}
|
||||
if bytes.IndexByte(version, ' ') >= 0 {
|
||||
return false
|
||||
}
|
||||
return isHTTPVersionPrefix(version)
|
||||
}
|
||||
|
||||
func extendHTTPRequestProbe(conn net.Conn, probe []byte) []byte {
|
||||
if httpRequestLineProbe(probe) || !couldBeHTTPRequestLine(probe) {
|
||||
return probe
|
||||
}
|
||||
buf := make([]byte, protocolProbeMaxBytes)
|
||||
n := copy(buf, probe)
|
||||
for n < protocolProbeMaxBytes {
|
||||
nr, err := conn.Read(buf[n:])
|
||||
if nr > 0 {
|
||||
n += nr
|
||||
if httpRequestLineProbe(buf[:n]) || !couldBeHTTPRequestLine(buf[:n]) {
|
||||
return buf[:n]
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return buf[:n]
|
||||
}
|
||||
}
|
||||
return buf[:n]
|
||||
}
|
||||
|
||||
func readProtocolProbe(conn net.Conn) ([]byte, error) {
|
||||
buf := make([]byte, protocolProbeMaxBytes)
|
||||
for {
|
||||
|
||||
Reference in New Issue
Block a user