Proxy: Add MASQUE inbound (IETF CONNECT-IP server, RFC 9484) (#6844)
Completes https://github.com/XTLS/Xray-core/pull/6807 and https://github.com/XTLS/Xray-core/pull/6810
This commit is contained in:
1 parent
65e853ed84
commit
7a018833ec
22 files changed
+3339
-91
No files matched your search
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/apernet/quic-go"
|
||||
)
|
||||
|
||||
const maxBufferedRequestBody = 32 << 10
|
||||
const maxStreamBuffer = 32 << 10
|
||||
|
||||
type HTTP2ClientConn struct {
|
||||
roundTripper http.RoundTripper
|
||||
@@ -37,7 +37,7 @@ func (c *HTTP2ClientConn) Dial(req *Request) (*Conn, *http.Response, error) {
|
||||
ctx := httpReq.Context()
|
||||
streamCtx, cancel := context.WithCancel(context.WithoutCancel(ctx))
|
||||
stop := context.AfterFunc(ctx, cancel)
|
||||
body := newRequestBody()
|
||||
body := NewStreamBuffer()
|
||||
r := httpReq.Clone(streamCtx)
|
||||
r.Header[":protocol"] = []string{requestProtocol}
|
||||
r.Body = body
|
||||
@@ -67,7 +67,7 @@ func (c *HTTP2ClientConn) Dial(req *Request) (*Conn, *http.Response, error) {
|
||||
|
||||
type http2Stream struct {
|
||||
reader *bufio.Reader
|
||||
body *requestBody
|
||||
body *StreamBuffer
|
||||
rsp io.Closer
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
@@ -86,7 +86,7 @@ func (s *http2Stream) abort() {
|
||||
s.rsp.Close()
|
||||
}
|
||||
|
||||
type requestBody struct {
|
||||
type StreamBuffer struct {
|
||||
mu sync.Mutex
|
||||
cond sync.Cond
|
||||
buf []byte
|
||||
@@ -95,13 +95,13 @@ type requestBody struct {
|
||||
deadline time.Time
|
||||
}
|
||||
|
||||
func newRequestBody() *requestBody {
|
||||
b := &requestBody{}
|
||||
func NewStreamBuffer() *StreamBuffer {
|
||||
b := &StreamBuffer{}
|
||||
b.cond.L = &b.mu
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *requestBody) Read(p []byte) (int, error) {
|
||||
func (b *StreamBuffer) Read(p []byte) (int, error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
for len(b.buf) == 0 && !b.closed && b.err == nil {
|
||||
@@ -119,7 +119,7 @@ func (b *requestBody) Read(p []byte) (int, error) {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (b *requestBody) Write(p []byte) (int, error) {
|
||||
func (b *StreamBuffer) Write(p []byte) (int, error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
for {
|
||||
@@ -130,7 +130,7 @@ func (b *requestBody) Write(p []byte) (int, error) {
|
||||
return 0, io.ErrClosedPipe
|
||||
case !b.deadline.IsZero() && !time.Now().Before(b.deadline):
|
||||
return 0, os.ErrDeadlineExceeded
|
||||
case len(b.buf) < maxBufferedRequestBody:
|
||||
case len(b.buf) < maxStreamBuffer:
|
||||
b.buf = append(b.buf, p...)
|
||||
b.cond.Broadcast()
|
||||
return len(p), nil
|
||||
@@ -139,7 +139,7 @@ func (b *requestBody) Write(p []byte) (int, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (b *requestBody) Close() error {
|
||||
func (b *StreamBuffer) Close() error {
|
||||
b.mu.Lock()
|
||||
b.closed = true
|
||||
b.cond.Broadcast()
|
||||
@@ -147,7 +147,7 @@ func (b *requestBody) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *requestBody) CloseWithError(err error) {
|
||||
func (b *StreamBuffer) CloseWithError(err error) {
|
||||
b.mu.Lock()
|
||||
if b.err == nil {
|
||||
b.err = err
|
||||
@@ -157,7 +157,7 @@ func (b *requestBody) CloseWithError(err error) {
|
||||
b.mu.Unlock()
|
||||
}
|
||||
|
||||
func (b *requestBody) SetWriteDeadline(t time.Time) error {
|
||||
func (b *StreamBuffer) SetWriteDeadline(t time.Time) error {
|
||||
b.mu.Lock()
|
||||
b.deadline = t
|
||||
b.cond.Broadcast()
|
||||
|
||||
@@ -109,7 +109,7 @@ func setupHTTP2Conns(t *testing.T) (client, server *Conn) {
|
||||
|
||||
func newTestHTTP2Stream() (*http2Stream, *io.PipeWriter) {
|
||||
pr, pw := io.Pipe()
|
||||
return &http2Stream{reader: bufio.NewReader(pr), body: newRequestBody(), rsp: pr, cancel: func() {}}, pw
|
||||
return &http2Stream{reader: bufio.NewReader(pr), body: NewStreamBuffer(), rsp: pr, cancel: func() {}}, pw
|
||||
}
|
||||
|
||||
func TestHTTP2Request(t *testing.T) {
|
||||
@@ -343,7 +343,7 @@ func TestHTTP2CloseUnblocksWrites(t *testing.T) {
|
||||
require.Eventually(t, func() bool {
|
||||
str.body.mu.Lock()
|
||||
defer str.body.mu.Unlock()
|
||||
return len(str.body.buf) >= maxBufferedRequestBody
|
||||
return len(str.body.buf) >= maxStreamBuffer
|
||||
}, 5*time.Second, time.Millisecond)
|
||||
|
||||
closed := make(chan error, 1)
|
||||
@@ -416,7 +416,7 @@ func TestHTTP2WritesDatagramCapsules(t *testing.T) {
|
||||
|
||||
func TestRequestBody(t *testing.T) {
|
||||
t.Run("coalesces writes", func(t *testing.T) {
|
||||
b := newRequestBody()
|
||||
b := NewStreamBuffer()
|
||||
for _, s := range []string{"foo", "bar", "baz"} {
|
||||
_, err := b.Write([]byte(s))
|
||||
require.NoError(t, err)
|
||||
@@ -428,8 +428,8 @@ func TestRequestBody(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("blocks writes while full", func(t *testing.T) {
|
||||
b := newRequestBody()
|
||||
_, err := b.Write(make([]byte, maxBufferedRequestBody))
|
||||
b := NewStreamBuffer()
|
||||
_, err := b.Write(make([]byte, maxStreamBuffer))
|
||||
require.NoError(t, err)
|
||||
written := make(chan struct{})
|
||||
go func() {
|
||||
@@ -441,7 +441,7 @@ func TestRequestBody(t *testing.T) {
|
||||
t.Fatal("write did not block")
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
_, err = b.Read(make([]byte, maxBufferedRequestBody))
|
||||
_, err = b.Read(make([]byte, maxStreamBuffer))
|
||||
require.NoError(t, err)
|
||||
select {
|
||||
case <-written:
|
||||
@@ -451,7 +451,7 @@ func TestRequestBody(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("close", func(t *testing.T) {
|
||||
b := newRequestBody()
|
||||
b := NewStreamBuffer()
|
||||
_, err := b.Write([]byte("foo"))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, b.Close())
|
||||
@@ -463,8 +463,8 @@ func TestRequestBody(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("write deadline", func(t *testing.T) {
|
||||
b := newRequestBody()
|
||||
_, err := b.Write(make([]byte, maxBufferedRequestBody))
|
||||
b := NewStreamBuffer()
|
||||
_, err := b.Write(make([]byte, maxStreamBuffer))
|
||||
require.NoError(t, err)
|
||||
writeErr := make(chan error, 1)
|
||||
go func() {
|
||||
@@ -481,11 +481,11 @@ func TestRequestBody(t *testing.T) {
|
||||
require.NoError(t, b.Close())
|
||||
data, err := io.ReadAll(b)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, data, maxBufferedRequestBody)
|
||||
require.Len(t, data, maxStreamBuffer)
|
||||
})
|
||||
|
||||
t.Run("close with error", func(t *testing.T) {
|
||||
b := newRequestBody()
|
||||
b := NewStreamBuffer()
|
||||
_, err := b.Write([]byte("foo"))
|
||||
require.NoError(t, err)
|
||||
b.CloseWithError(net.ErrClosed)
|
||||
|
||||
@@ -0,0 +1,680 @@
|
||||
package masque
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
go_errors "errors"
|
||||
"io"
|
||||
"maps"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/transport/internet/masque/connectip"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/hpack"
|
||||
)
|
||||
|
||||
const (
|
||||
http2MaxConcurrentStreams = 100
|
||||
http2HandshakeTimeout = 10 * time.Second
|
||||
http2DefaultHeaderTable = 4096
|
||||
)
|
||||
|
||||
var (
|
||||
errHTTP2BadPreface = go_errors.New("http2: invalid connection preface")
|
||||
errHTTP2BadRequest = go_errors.New("http2: malformed request")
|
||||
errHTTP2StreamClosed = go_errors.New("http2: stream closed")
|
||||
errHTTP2RequestBodyClosed = go_errors.New("http2: request body closed")
|
||||
)
|
||||
|
||||
type connAddrsKey struct{}
|
||||
|
||||
type connAddrs struct {
|
||||
local net.Addr
|
||||
remote net.Addr
|
||||
}
|
||||
|
||||
type http2ServerConn struct {
|
||||
conn net.Conn
|
||||
handler http.Handler
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
wmu sync.Mutex
|
||||
bw *bufio.Writer
|
||||
fr *http2.Framer
|
||||
hbuf bytes.Buffer
|
||||
henc *hpack.Encoder
|
||||
|
||||
lastFrame atomic.Int64
|
||||
|
||||
mu sync.Mutex
|
||||
cond sync.Cond
|
||||
err error
|
||||
maxFrameSize uint32
|
||||
initialWindow int64
|
||||
connSendWindow int64
|
||||
connRecvWindow int64
|
||||
connRecvUnacked int64
|
||||
streams map[uint32]*http2ServerStream
|
||||
lastStreamID uint32
|
||||
}
|
||||
|
||||
type http2ServerStream struct {
|
||||
c *http2ServerConn
|
||||
id uint32
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
header http.Header
|
||||
out *connectip.StreamBuffer
|
||||
sent chan struct{}
|
||||
|
||||
sendWindow int64
|
||||
recvWindow int64
|
||||
recvUnacked int64
|
||||
recv bytes.Buffer
|
||||
recvEnd bool
|
||||
wroteHeader bool
|
||||
sentEnd bool
|
||||
resetErr error
|
||||
}
|
||||
|
||||
func serveHTTP2(ctx context.Context, conn net.Conn, handler http.Handler) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
c := &http2ServerConn{
|
||||
conn: conn,
|
||||
handler: handler,
|
||||
ctx: context.WithValue(ctx, connAddrsKey{}, connAddrs{local: conn.LocalAddr(), remote: conn.RemoteAddr()}),
|
||||
cancel: cancel,
|
||||
bw: bufio.NewWriter(conn),
|
||||
maxFrameSize: http2DefaultFrameSize,
|
||||
initialWindow: http2DefaultWindow,
|
||||
connSendWindow: http2DefaultWindow,
|
||||
connRecvWindow: http2ConnectionWindow,
|
||||
streams: make(map[uint32]*http2ServerStream),
|
||||
}
|
||||
c.cond.L = &c.mu
|
||||
c.henc = hpack.NewEncoder(&c.hbuf)
|
||||
c.henc.SetMaxDynamicTableSizeLimit(0)
|
||||
c.lastFrame.Store(time.Now().UnixNano())
|
||||
|
||||
br := bufio.NewReader(conn)
|
||||
preface := make([]byte, len(http2.ClientPreface))
|
||||
conn.SetReadDeadline(time.Now().Add(http2HandshakeTimeout))
|
||||
if _, err := io.ReadFull(br, preface); err != nil || string(preface) != http2.ClientPreface {
|
||||
c.fail(errHTTP2BadPreface)
|
||||
return
|
||||
}
|
||||
conn.SetReadDeadline(time.Time{})
|
||||
|
||||
c.fr = http2.NewFramer(c.bw, br)
|
||||
c.fr.SetMaxReadFrameSize(http2DefaultFrameSize)
|
||||
c.fr.ReadMetaHeaders = hpack.NewDecoder(http2DefaultHeaderTable, nil)
|
||||
c.fr.MaxHeaderListSize = http2MaxHeaderListSize
|
||||
if err := c.write(func(fr *http2.Framer) error {
|
||||
if err := fr.WriteSettings(
|
||||
http2.Setting{ID: http2.SettingMaxConcurrentStreams, Val: http2MaxConcurrentStreams},
|
||||
http2.Setting{ID: http2.SettingInitialWindowSize, Val: http2StreamWindow},
|
||||
http2.Setting{ID: http2.SettingMaxHeaderListSize, Val: http2MaxHeaderListSize},
|
||||
http2.Setting{ID: http2.SettingEnableConnectProtocol, Val: 1},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return fr.WriteWindowUpdate(0, http2ConnectionWindow-http2DefaultWindow)
|
||||
}); err != nil {
|
||||
c.fail(err)
|
||||
return
|
||||
}
|
||||
go c.keepAlive()
|
||||
c.readLoop()
|
||||
}
|
||||
|
||||
func (c *http2ServerConn) write(f func(*http2.Framer) error) error {
|
||||
c.wmu.Lock()
|
||||
defer c.wmu.Unlock()
|
||||
if err := f(c.fr); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.bw.Flush()
|
||||
}
|
||||
|
||||
func (c *http2ServerConn) fail(err error) {
|
||||
c.mu.Lock()
|
||||
if c.err == nil {
|
||||
c.err = err
|
||||
}
|
||||
streams := slices.Collect(maps.Values(c.streams))
|
||||
c.cond.Broadcast()
|
||||
c.mu.Unlock()
|
||||
for _, st := range streams {
|
||||
st.out.CloseWithError(err)
|
||||
st.cancel()
|
||||
}
|
||||
c.cancel()
|
||||
c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *http2ServerConn) keepAlive() {
|
||||
ticker := time.NewTicker(http2KeepAlivePeriod)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
}
|
||||
idle := time.Since(time.Unix(0, c.lastFrame.Load()))
|
||||
if idle >= http2IdleTimeout {
|
||||
c.fail(errHTTP2IdleTimeout)
|
||||
return
|
||||
}
|
||||
if idle >= http2KeepAlivePeriod {
|
||||
go c.write(func(fr *http2.Framer) error {
|
||||
return fr.WritePing(false, [8]byte{})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *http2ServerConn) readLoop() {
|
||||
for {
|
||||
f, err := c.fr.ReadFrame()
|
||||
if err != nil {
|
||||
var streamErr http2.StreamError
|
||||
if go_errors.As(err, &streamErr) {
|
||||
if err := c.resetStream(streamErr); err != nil {
|
||||
c.fail(err)
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
c.fail(err)
|
||||
return
|
||||
}
|
||||
c.lastFrame.Store(time.Now().UnixNano())
|
||||
if err := c.handleFrame(f); err != nil {
|
||||
c.fail(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *http2ServerConn) handleFrame(f http2.Frame) error {
|
||||
switch f := f.(type) {
|
||||
case *http2.SettingsFrame:
|
||||
if f.IsAck() {
|
||||
return nil
|
||||
}
|
||||
if err := c.applySettings(f); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.write((*http2.Framer).WriteSettingsAck)
|
||||
case *http2.PingFrame:
|
||||
if f.IsAck() {
|
||||
return nil
|
||||
}
|
||||
return c.write(func(fr *http2.Framer) error {
|
||||
return fr.WritePing(true, f.Data)
|
||||
})
|
||||
case *http2.WindowUpdateFrame:
|
||||
return c.handleWindowUpdate(f)
|
||||
case *http2.MetaHeadersFrame:
|
||||
return c.handleHeaders(f)
|
||||
case *http2.DataFrame:
|
||||
return c.handleData(f)
|
||||
case *http2.RSTStreamFrame:
|
||||
c.abortStream(f.StreamID, http2.StreamError{StreamID: f.StreamID, Code: f.ErrCode}, false)
|
||||
case *http2.PushPromiseFrame:
|
||||
return http2.ConnectionError(http2.ErrCodeProtocol)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *http2ServerConn) applySettings(f *http2.SettingsFrame) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
defer c.cond.Broadcast()
|
||||
return f.ForeachSetting(func(s http2.Setting) error {
|
||||
if err := s.Valid(); err != nil {
|
||||
return err
|
||||
}
|
||||
switch s.ID {
|
||||
case http2.SettingMaxFrameSize:
|
||||
c.maxFrameSize = s.Val
|
||||
case http2.SettingInitialWindowSize:
|
||||
delta := int64(s.Val) - c.initialWindow
|
||||
c.initialWindow = int64(s.Val)
|
||||
for _, st := range c.streams {
|
||||
st.sendWindow += delta
|
||||
if st.sendWindow > math.MaxInt32 {
|
||||
return http2.ConnectionError(http2.ErrCodeFlowControl)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (c *http2ServerConn) handleWindowUpdate(f *http2.WindowUpdateFrame) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if f.StreamID == 0 {
|
||||
c.connSendWindow += int64(f.Increment)
|
||||
if c.connSendWindow > math.MaxInt32 {
|
||||
return http2.ConnectionError(http2.ErrCodeFlowControl)
|
||||
}
|
||||
} else if st := c.streams[f.StreamID]; st != nil {
|
||||
st.sendWindow += int64(f.Increment)
|
||||
if st.sendWindow > math.MaxInt32 {
|
||||
return http2.ConnectionError(http2.ErrCodeFlowControl)
|
||||
}
|
||||
}
|
||||
c.cond.Broadcast()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *http2ServerConn) handleHeaders(f *http2.MetaHeadersFrame) error {
|
||||
c.mu.Lock()
|
||||
if st := c.streams[f.StreamID]; st != nil {
|
||||
if !f.StreamEnded() {
|
||||
c.mu.Unlock()
|
||||
return http2.ConnectionError(http2.ErrCodeProtocol)
|
||||
}
|
||||
st.recvEnd = true
|
||||
c.cond.Broadcast()
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
if f.StreamID%2 == 0 || f.StreamID <= c.lastStreamID {
|
||||
c.mu.Unlock()
|
||||
return http2.ConnectionError(http2.ErrCodeProtocol)
|
||||
}
|
||||
c.lastStreamID = f.StreamID
|
||||
refused := len(c.streams) >= http2MaxConcurrentStreams
|
||||
c.mu.Unlock()
|
||||
|
||||
if refused {
|
||||
return c.write(func(fr *http2.Framer) error {
|
||||
return fr.WriteRSTStream(f.StreamID, http2.ErrCodeRefusedStream)
|
||||
})
|
||||
}
|
||||
req, err := newHTTP2Request(f)
|
||||
if err != nil {
|
||||
return c.write(func(fr *http2.Framer) error {
|
||||
return fr.WriteRSTStream(f.StreamID, http2.ErrCodeProtocol)
|
||||
})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(c.ctx)
|
||||
st := &http2ServerStream{
|
||||
c: c,
|
||||
id: f.StreamID,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
header: make(http.Header),
|
||||
out: connectip.NewStreamBuffer(),
|
||||
sent: make(chan struct{}),
|
||||
recvWindow: http2StreamWindow,
|
||||
recvEnd: f.StreamEnded(),
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
req.RemoteAddr = c.conn.RemoteAddr().String()
|
||||
if st.recvEnd {
|
||||
req.Body = http.NoBody
|
||||
req.ContentLength = 0
|
||||
} else {
|
||||
req.Body = &http2RequestBody{st: st}
|
||||
req.ContentLength = -1
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
if c.err != nil {
|
||||
err := c.err
|
||||
c.mu.Unlock()
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
st.sendWindow = c.initialWindow
|
||||
c.streams[f.StreamID] = st
|
||||
c.mu.Unlock()
|
||||
|
||||
go c.serveStream(st, req)
|
||||
return nil
|
||||
}
|
||||
|
||||
func newHTTP2Request(f *http2.MetaHeadersFrame) (*http.Request, error) {
|
||||
method := f.PseudoValue("method")
|
||||
scheme := f.PseudoValue("scheme")
|
||||
authority := f.PseudoValue("authority")
|
||||
path := f.PseudoValue("path")
|
||||
protocol := f.PseudoValue("protocol")
|
||||
if method == "" || (protocol != "" && method != http.MethodConnect) {
|
||||
return nil, errHTTP2BadRequest
|
||||
}
|
||||
var u *url.URL
|
||||
if method == http.MethodConnect && protocol == "" {
|
||||
if authority == "" || path != "" || scheme != "" {
|
||||
return nil, errHTTP2BadRequest
|
||||
}
|
||||
u = &url.URL{Host: authority}
|
||||
} else {
|
||||
if path == "" || scheme == "" {
|
||||
return nil, errHTTP2BadRequest
|
||||
}
|
||||
var err error
|
||||
if u, err = url.ParseRequestURI(path); err != nil {
|
||||
return nil, errHTTP2BadRequest
|
||||
}
|
||||
}
|
||||
header := make(http.Header)
|
||||
for _, hf := range f.RegularFields() {
|
||||
header.Add(hf.Name, hf.Value)
|
||||
}
|
||||
if protocol != "" {
|
||||
header.Set(":protocol", protocol)
|
||||
}
|
||||
if authority == "" {
|
||||
authority = header.Get("Host")
|
||||
}
|
||||
return &http.Request{
|
||||
Method: method,
|
||||
URL: u,
|
||||
Proto: "HTTP/2.0",
|
||||
ProtoMajor: 2,
|
||||
Header: header,
|
||||
Host: authority,
|
||||
RequestURI: path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *http2ServerConn) handleData(f *http2.DataFrame) error {
|
||||
size := int64(f.Length)
|
||||
c.mu.Lock()
|
||||
c.connRecvWindow -= size
|
||||
if c.connRecvWindow < 0 {
|
||||
c.mu.Unlock()
|
||||
return http2.ConnectionError(http2.ErrCodeFlowControl)
|
||||
}
|
||||
st := c.streams[f.StreamID]
|
||||
if st == nil || st.resetErr != nil || st.recvEnd {
|
||||
idle := st == nil && f.StreamID > c.lastStreamID
|
||||
c.connRecvWindow += size
|
||||
c.mu.Unlock()
|
||||
if idle {
|
||||
return http2.ConnectionError(http2.ErrCodeProtocol)
|
||||
}
|
||||
if size == 0 {
|
||||
return nil
|
||||
}
|
||||
return c.write(func(fr *http2.Framer) error {
|
||||
return fr.WriteWindowUpdate(0, uint32(size))
|
||||
})
|
||||
}
|
||||
st.recvWindow -= size
|
||||
if st.recvWindow < 0 {
|
||||
c.mu.Unlock()
|
||||
return http2.ConnectionError(http2.ErrCodeFlowControl)
|
||||
}
|
||||
st.recv.Write(f.Data())
|
||||
padding := size - int64(len(f.Data()))
|
||||
st.recvUnacked += padding
|
||||
c.connRecvUnacked += padding
|
||||
if f.StreamEnded() {
|
||||
st.recvEnd = true
|
||||
}
|
||||
c.cond.Broadcast()
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *http2ServerConn) resetStream(streamErr http2.StreamError) error {
|
||||
c.mu.Lock()
|
||||
if streamErr.StreamID%2 == 1 && streamErr.StreamID > c.lastStreamID {
|
||||
c.lastStreamID = streamErr.StreamID
|
||||
}
|
||||
c.mu.Unlock()
|
||||
c.abortStream(streamErr.StreamID, streamErr, false)
|
||||
return c.write(func(fr *http2.Framer) error {
|
||||
return fr.WriteRSTStream(streamErr.StreamID, streamErr.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *http2ServerConn) abortStream(id uint32, err error, reset bool) {
|
||||
c.mu.Lock()
|
||||
st := c.streams[id]
|
||||
if st == nil || st.resetErr != nil {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
st.resetErr = err
|
||||
c.cond.Broadcast()
|
||||
c.mu.Unlock()
|
||||
st.out.CloseWithError(err)
|
||||
st.cancel()
|
||||
if reset {
|
||||
go c.write(func(fr *http2.Framer) error {
|
||||
return fr.WriteRSTStream(id, http2.ErrCodeCancel)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (c *http2ServerConn) serveStream(st *http2ServerStream, req *http.Request) {
|
||||
go st.sendLoop()
|
||||
c.handler.ServeHTTP(&http2ResponseWriter{st: st}, req)
|
||||
st.writeHeader(http.StatusOK)
|
||||
st.out.Close()
|
||||
<-st.sent
|
||||
|
||||
c.mu.Lock()
|
||||
finish := st.resetErr == nil && !st.sentEnd && c.err == nil
|
||||
refuse := finish && !st.recvEnd
|
||||
st.sentEnd = true
|
||||
delete(c.streams, st.id)
|
||||
c.cond.Broadcast()
|
||||
c.mu.Unlock()
|
||||
st.cancel()
|
||||
|
||||
if finish {
|
||||
if err := c.write(func(fr *http2.Framer) error {
|
||||
if err := fr.WriteData(st.id, true, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if refuse {
|
||||
return fr.WriteRSTStream(st.id, http2.ErrCodeNo)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
c.fail(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (st *http2ServerStream) writeHeader(code int) {
|
||||
c := st.c
|
||||
c.mu.Lock()
|
||||
if st.wroteHeader || st.resetErr != nil || c.err != nil {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
st.wroteHeader = true
|
||||
header := st.header.Clone()
|
||||
maxFrameSize := int(c.maxFrameSize)
|
||||
c.mu.Unlock()
|
||||
|
||||
if err := c.write(func(fr *http2.Framer) error {
|
||||
c.hbuf.Reset()
|
||||
c.henc.WriteField(hpack.HeaderField{Name: ":status", Value: strconv.Itoa(code)})
|
||||
for _, k := range slices.Sorted(maps.Keys(header)) {
|
||||
name := strings.ToLower(k)
|
||||
switch name {
|
||||
case "connection", "proxy-connection", "keep-alive", "transfer-encoding", "upgrade":
|
||||
continue
|
||||
}
|
||||
for _, v := range header[k] {
|
||||
c.henc.WriteField(hpack.HeaderField{Name: name, Value: v})
|
||||
}
|
||||
}
|
||||
block := c.hbuf.Bytes()
|
||||
for first := true; first || len(block) > 0; first = false {
|
||||
chunk := block[:min(len(block), maxFrameSize)]
|
||||
block = block[len(chunk):]
|
||||
var err error
|
||||
if first {
|
||||
err = fr.WriteHeaders(http2.HeadersFrameParam{StreamID: st.id, BlockFragment: chunk, EndHeaders: len(block) == 0})
|
||||
} else {
|
||||
err = fr.WriteContinuation(st.id, len(block) == 0, chunk)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
c.fail(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (st *http2ServerStream) sendLoop() {
|
||||
defer close(st.sent)
|
||||
c := st.c
|
||||
buf := make([]byte, http2DefaultFrameSize)
|
||||
for {
|
||||
n, err := st.out.Read(buf)
|
||||
for data := buf[:n]; len(data) > 0; {
|
||||
allowed, err := st.awaitSendWindow(len(data))
|
||||
if err != nil {
|
||||
st.out.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
if err := c.write(func(fr *http2.Framer) error {
|
||||
return fr.WriteData(st.id, false, data[:allowed])
|
||||
}); err != nil {
|
||||
c.fail(err)
|
||||
return
|
||||
}
|
||||
data = data[allowed:]
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (st *http2ServerStream) awaitSendWindow(n int) (int, error) {
|
||||
c := st.c
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
for {
|
||||
switch {
|
||||
case st.resetErr != nil:
|
||||
return 0, st.resetErr
|
||||
case c.err != nil:
|
||||
return 0, c.err
|
||||
case st.sentEnd:
|
||||
return 0, errHTTP2StreamClosed
|
||||
}
|
||||
if window := min(c.connSendWindow, st.sendWindow); window > 0 {
|
||||
n = int(min(int64(n), window, int64(c.maxFrameSize)))
|
||||
c.connSendWindow -= int64(n)
|
||||
st.sendWindow -= int64(n)
|
||||
return n, nil
|
||||
}
|
||||
c.cond.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
type http2ResponseWriter struct {
|
||||
st *http2ServerStream
|
||||
}
|
||||
|
||||
func (w *http2ResponseWriter) Header() http.Header { return w.st.header }
|
||||
|
||||
func (w *http2ResponseWriter) WriteHeader(code int) { w.st.writeHeader(code) }
|
||||
|
||||
func (w *http2ResponseWriter) Write(p []byte) (int, error) {
|
||||
w.st.writeHeader(http.StatusOK)
|
||||
return w.st.out.Write(p)
|
||||
}
|
||||
|
||||
func (w *http2ResponseWriter) Flush() { w.st.writeHeader(http.StatusOK) }
|
||||
|
||||
func (w *http2ResponseWriter) SetWriteDeadline(t time.Time) error {
|
||||
return w.st.out.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
type http2RequestBody struct {
|
||||
st *http2ServerStream
|
||||
}
|
||||
|
||||
func (b *http2RequestBody) Read(p []byte) (int, error) {
|
||||
st := b.st
|
||||
c := st.c
|
||||
c.mu.Lock()
|
||||
for st.recv.Len() == 0 && !st.recvEnd && st.resetErr == nil && c.err == nil {
|
||||
c.cond.Wait()
|
||||
}
|
||||
if st.recv.Len() == 0 {
|
||||
err := io.EOF
|
||||
switch {
|
||||
case st.resetErr != nil:
|
||||
err = st.resetErr
|
||||
case c.err != nil && !st.recvEnd:
|
||||
err = c.err
|
||||
}
|
||||
c.mu.Unlock()
|
||||
return 0, err
|
||||
}
|
||||
n, _ := st.recv.Read(p)
|
||||
st.recvUnacked += int64(n)
|
||||
c.connRecvUnacked += int64(n)
|
||||
var streamUpdate, connUpdate int64
|
||||
if st.recvUnacked >= http2WindowUpdateSize && !st.recvEnd {
|
||||
streamUpdate = st.recvUnacked
|
||||
st.recvUnacked = 0
|
||||
st.recvWindow += streamUpdate
|
||||
}
|
||||
if c.connRecvUnacked >= http2WindowUpdateSize {
|
||||
connUpdate = c.connRecvUnacked
|
||||
c.connRecvUnacked = 0
|
||||
c.connRecvWindow += connUpdate
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
if streamUpdate > 0 || connUpdate > 0 {
|
||||
if err := c.write(func(fr *http2.Framer) error {
|
||||
if connUpdate > 0 {
|
||||
if err := fr.WriteWindowUpdate(0, uint32(connUpdate)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if streamUpdate > 0 {
|
||||
return fr.WriteWindowUpdate(st.id, uint32(streamUpdate))
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
c.fail(err)
|
||||
}
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (b *http2RequestBody) Close() error {
|
||||
st := b.st
|
||||
c := st.c
|
||||
c.mu.Lock()
|
||||
done := st.recvEnd
|
||||
c.mu.Unlock()
|
||||
if !done {
|
||||
c.abortStream(st.id, errHTTP2RequestBodyClosed, true)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
package masque
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/hpack"
|
||||
)
|
||||
|
||||
func serveHTTP2Pipe(t *testing.T, handler http.Handler) net.Conn {
|
||||
t.Helper()
|
||||
client, server := tcpPipe(t)
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
serveHTTP2(context.Background(), server, handler)
|
||||
close(done)
|
||||
}()
|
||||
t.Cleanup(func() {
|
||||
client.Close()
|
||||
server.Close()
|
||||
<-done
|
||||
})
|
||||
return client
|
||||
}
|
||||
|
||||
type http2ClientPeer struct {
|
||||
t *testing.T
|
||||
conn net.Conn
|
||||
fr *http2.Framer
|
||||
hbuf bytes.Buffer
|
||||
henc *hpack.Encoder
|
||||
}
|
||||
|
||||
func newHTTP2ClientPeer(t *testing.T, handler http.Handler) (*http2ClientPeer, []http2.Setting) {
|
||||
t.Helper()
|
||||
conn := serveHTTP2Pipe(t, handler)
|
||||
p := &http2ClientPeer{t: t, conn: conn, fr: http2.NewFramer(conn, conn)}
|
||||
p.henc = hpack.NewEncoder(&p.hbuf)
|
||||
p.fr.ReadMetaHeaders = hpack.NewDecoder(4096, nil)
|
||||
_, err := io.WriteString(conn, http2.ClientPreface)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, p.fr.WriteSettings())
|
||||
|
||||
f := p.readFrame()
|
||||
require.IsType(t, &http2.SettingsFrame{}, f)
|
||||
var settings []http2.Setting
|
||||
f.(*http2.SettingsFrame).ForeachSetting(func(s http2.Setting) error {
|
||||
settings = append(settings, s)
|
||||
return nil
|
||||
})
|
||||
f = p.readFrame()
|
||||
require.IsType(t, &http2.WindowUpdateFrame{}, f)
|
||||
require.Equal(t, uint32(http2ConnectionWindow-http2DefaultWindow), f.(*http2.WindowUpdateFrame).Increment)
|
||||
f = p.readFrame()
|
||||
require.True(t, f.(*http2.SettingsFrame).IsAck())
|
||||
require.NoError(t, p.fr.WriteSettingsAck())
|
||||
return p, settings
|
||||
}
|
||||
|
||||
func (p *http2ClientPeer) readFrame() http2.Frame {
|
||||
p.t.Helper()
|
||||
p.conn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
f, err := p.fr.ReadFrame()
|
||||
require.NoError(p.t, err)
|
||||
return f
|
||||
}
|
||||
|
||||
func (p *http2ClientPeer) writeHeaders(streamID uint32, endStream bool, fields ...string) {
|
||||
p.t.Helper()
|
||||
p.hbuf.Reset()
|
||||
for i := 0; i < len(fields); i += 2 {
|
||||
require.NoError(p.t, p.henc.WriteField(hpack.HeaderField{Name: fields[i], Value: fields[i+1]}))
|
||||
}
|
||||
require.NoError(p.t, p.fr.WriteHeaders(http2.HeadersFrameParam{
|
||||
StreamID: streamID,
|
||||
BlockFragment: p.hbuf.Bytes(),
|
||||
EndHeaders: true,
|
||||
EndStream: endStream,
|
||||
}))
|
||||
}
|
||||
|
||||
func (p *http2ClientPeer) writeConnect(streamID uint32) {
|
||||
p.t.Helper()
|
||||
p.writeHeaders(streamID, false,
|
||||
":method", "CONNECT",
|
||||
":protocol", "connect-ip",
|
||||
":scheme", "https",
|
||||
":authority", "proxy.example",
|
||||
":path", "/.well-known/masque/ip/*/*/",
|
||||
"capsule-protocol", "?1",
|
||||
)
|
||||
}
|
||||
|
||||
func TestHTTP2ServerSettings(t *testing.T) {
|
||||
_, settings := newHTTP2ClientPeer(t, http.NotFoundHandler())
|
||||
require.Equal(t, []http2.Setting{
|
||||
{ID: http2.SettingMaxConcurrentStreams, Val: http2MaxConcurrentStreams},
|
||||
{ID: http2.SettingInitialWindowSize, Val: http2StreamWindow},
|
||||
{ID: http2.SettingMaxHeaderListSize, Val: http2MaxHeaderListSize},
|
||||
{ID: http2.SettingEnableConnectProtocol, Val: 1},
|
||||
}, settings)
|
||||
}
|
||||
|
||||
func TestHTTP2ServerRoundTrip(t *testing.T) {
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, http.MethodConnect, r.Method)
|
||||
assert.Equal(t, "connect-ip", r.Header.Get(":protocol"))
|
||||
assert.Equal(t, "?1", r.Header.Get("Capsule-Protocol"))
|
||||
assert.Equal(t, "Basic dTpw", r.Header.Get("Authorization"))
|
||||
assert.Equal(t, 2, r.ProtoMajor)
|
||||
assert.Equal(t, "proxy.example", r.Host)
|
||||
assert.Equal(t, "/.well-known/masque/ip/*/*/", r.URL.Path)
|
||||
w.Header().Set("Capsule-Protocol", "?1")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
assert.NoError(t, http.NewResponseController(w).Flush())
|
||||
_, err := io.Copy(w, r.Body)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
cc, err := newHTTP2ClientConn(serveHTTP2Pipe(t, handler))
|
||||
require.NoError(t, err)
|
||||
defer cc.Close()
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
rsp, err := cc.RoundTrip(connectRequest(t, context.Background(), pr))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, rsp.StatusCode)
|
||||
require.Equal(t, "?1", rsp.Header.Get("Capsule-Protocol"))
|
||||
|
||||
payload := make([]byte, 3*http2ConnectionWindow/2)
|
||||
rand.Read(payload)
|
||||
go func() {
|
||||
pw.Write(payload)
|
||||
pw.Close()
|
||||
}()
|
||||
echoed := sha256.New()
|
||||
n, err := io.Copy(echoed, rsp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(len(payload)), n)
|
||||
require.Equal(t, sha256.Sum256(payload), [32]byte(echoed.Sum(nil)))
|
||||
}
|
||||
|
||||
func TestHTTP2ServerStatus(t *testing.T) {
|
||||
p, _ := newHTTP2ClientPeer(t, http.NotFoundHandler())
|
||||
p.writeConnect(1)
|
||||
f := p.readFrame()
|
||||
require.IsType(t, &http2.MetaHeadersFrame{}, f)
|
||||
require.Equal(t, "404", f.(*http2.MetaHeadersFrame).PseudoValue("status"))
|
||||
var body []byte
|
||||
for {
|
||||
f = p.readFrame()
|
||||
require.IsType(t, &http2.DataFrame{}, f)
|
||||
body = append(body, f.(*http2.DataFrame).Data()...)
|
||||
if f.(*http2.DataFrame).StreamEnded() {
|
||||
break
|
||||
}
|
||||
}
|
||||
require.Equal(t, "404 page not found\n", string(body))
|
||||
f = p.readFrame()
|
||||
require.IsType(t, &http2.RSTStreamFrame{}, f)
|
||||
require.Equal(t, http2.ErrCodeNo, f.(*http2.RSTStreamFrame).ErrCode)
|
||||
}
|
||||
|
||||
func TestHTTP2ServerMalformedRequests(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
fields []string
|
||||
}{
|
||||
{"no method", []string{":scheme", "https", ":path", "/", ":authority", "proxy.example"}},
|
||||
{"no path", []string{":method", "GET", ":scheme", "https", ":authority", "proxy.example"}},
|
||||
{"protocol without CONNECT", []string{":method", "GET", ":protocol", "connect-ip", ":scheme", "https", ":path", "/", ":authority", "proxy.example"}},
|
||||
{"plain CONNECT with a path", []string{":method", "CONNECT", ":path", "/", ":authority", "proxy.example"}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
p, _ := newHTTP2ClientPeer(t, http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
||||
t.Error("the handler saw a malformed request")
|
||||
}))
|
||||
p.writeHeaders(1, false, tc.fields...)
|
||||
f := p.readFrame()
|
||||
require.IsType(t, &http2.RSTStreamFrame{}, f)
|
||||
require.Equal(t, http2.ErrCodeProtocol, f.(*http2.RSTStreamFrame).ErrCode)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTP2ServerRefusesExtraStreams(t *testing.T) {
|
||||
release := make(chan struct{})
|
||||
var started sync.WaitGroup
|
||||
started.Add(http2MaxConcurrentStreams)
|
||||
p, _ := newHTTP2ClientPeer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
started.Done()
|
||||
<-release
|
||||
}))
|
||||
defer close(release)
|
||||
for i := range http2MaxConcurrentStreams {
|
||||
p.writeConnect(uint32(2*i + 1))
|
||||
}
|
||||
started.Wait()
|
||||
p.writeConnect(2*http2MaxConcurrentStreams + 1)
|
||||
f := p.readFrame()
|
||||
require.IsType(t, &http2.RSTStreamFrame{}, f)
|
||||
require.Equal(t, uint32(2*http2MaxConcurrentStreams+1), f.Header().StreamID)
|
||||
require.Equal(t, http2.ErrCodeRefusedStream, f.(*http2.RSTStreamFrame).ErrCode)
|
||||
}
|
||||
|
||||
func TestHTTP2ServerClientReset(t *testing.T) {
|
||||
readErr := make(chan error, 1)
|
||||
canceled := make(chan struct{})
|
||||
p, _ := newHTTP2ClientPeer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, err := r.Body.Read(make([]byte, 1))
|
||||
readErr <- err
|
||||
<-r.Context().Done()
|
||||
close(canceled)
|
||||
}))
|
||||
p.writeConnect(1)
|
||||
f := p.readFrame()
|
||||
require.Equal(t, "200", f.(*http2.MetaHeadersFrame).PseudoValue("status"))
|
||||
require.NoError(t, p.fr.WriteRSTStream(1, http2.ErrCodeCancel))
|
||||
select {
|
||||
case err := <-readErr:
|
||||
require.Equal(t, http2.StreamError{StreamID: 1, Code: http2.ErrCodeCancel}, err)
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("the body read did not fail after RST_STREAM")
|
||||
}
|
||||
select {
|
||||
case <-canceled:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("the request context was not canceled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTP2ServerAnswersPings(t *testing.T) {
|
||||
p, _ := newHTTP2ClientPeer(t, http.NotFoundHandler())
|
||||
data := [8]byte{8, 7, 6, 5, 4, 3, 2, 1}
|
||||
require.NoError(t, p.fr.WritePing(false, data))
|
||||
f := p.readFrame()
|
||||
require.IsType(t, &http2.PingFrame{}, f)
|
||||
require.True(t, f.(*http2.PingFrame).IsAck())
|
||||
require.Equal(t, data, f.(*http2.PingFrame).Data)
|
||||
}
|
||||
|
||||
func TestHTTP2ServerRejectsOverflow(t *testing.T) {
|
||||
p, _ := newHTTP2ClientPeer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
<-r.Context().Done()
|
||||
}))
|
||||
p.writeConnect(1)
|
||||
p.readFrame()
|
||||
chunk := make([]byte, http2DefaultFrameSize)
|
||||
go func() {
|
||||
for range http2StreamWindow/len(chunk) + 1 {
|
||||
if p.fr.WriteData(1, false, chunk) != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
p.conn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
_, err := io.Copy(io.Discard, p.conn)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestHTTP2ServerBadPreface(t *testing.T) {
|
||||
conn := serveHTTP2Pipe(t, http.NotFoundHandler())
|
||||
_, err := io.WriteString(conn, "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
|
||||
require.NoError(t, err)
|
||||
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
n, err := io.Copy(io.Discard, conn)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, n)
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
package masque
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
gotls "crypto/tls"
|
||||
go_errors "errors"
|
||||
"io"
|
||||
"maps"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/quic-go"
|
||||
"github.com/apernet/quic-go/http3"
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"github.com/xtls/xray-core/transport/internet/hysteria/congestion"
|
||||
"github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr"
|
||||
"github.com/xtls/xray-core/transport/internet/masque/connectip"
|
||||
"github.com/xtls/xray-core/transport/internet/tls"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
type Listener struct {
|
||||
path pathMatcher
|
||||
addConn internet.ConnHandler
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
quicServer *http3.Server
|
||||
quicListener *quic.Listener
|
||||
transport *quic.Transport
|
||||
pktConn net.PacketConn
|
||||
tcpListener net.Listener
|
||||
|
||||
mu sync.Mutex
|
||||
conns map[net.Conn]struct{}
|
||||
}
|
||||
|
||||
func serverVersions(config *tls.Config) (h2, h3 bool) {
|
||||
h2 = slices.Contains(config.NextProtocol, http2.NextProtoTLS)
|
||||
h3 = slices.Contains(config.NextProtocol, http3.NextProtoH3) || !h2
|
||||
return h2, h3
|
||||
}
|
||||
|
||||
func Listen(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, handler internet.ConnHandler) (internet.Listener, error) {
|
||||
if address.Family().IsDomain() {
|
||||
return nil, errors.New("address is domain")
|
||||
}
|
||||
tlsConfig := tls.ConfigFromStreamSettings(streamSettings)
|
||||
if tlsConfig == nil {
|
||||
return nil, errors.New("tls config is nil")
|
||||
}
|
||||
config := streamSettings.ProtocolSettings.(*Config)
|
||||
path, err := newPathMatcher(config.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l := &Listener{
|
||||
path: path,
|
||||
addConn: handler,
|
||||
conns: make(map[net.Conn]struct{}),
|
||||
}
|
||||
l.ctx, l.cancel = context.WithCancel(context.Background())
|
||||
h2, h3 := serverVersions(tlsConfig)
|
||||
if h3 {
|
||||
if err := l.listenHTTP3(address, port, streamSettings, tlsConfig); err != nil {
|
||||
l.Close()
|
||||
return nil, err
|
||||
}
|
||||
errors.LogInfo(ctx, "listening UDP for MASQUE over HTTP/3 on ", address, ":", port)
|
||||
}
|
||||
if h2 {
|
||||
if err := l.listenHTTP2(ctx, address, port, streamSettings, tlsConfig); err != nil {
|
||||
l.Close()
|
||||
return nil, err
|
||||
}
|
||||
errors.LogInfo(ctx, "listening TCP for MASQUE over HTTP/2 on ", address, ":", port)
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (l *Listener) listenHTTP3(address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, tlsConfig *tls.Config) error {
|
||||
quicParams := streamSettings.QuicParams
|
||||
if quicParams == nil {
|
||||
quicParams = &internet.QuicParams{
|
||||
BbrProfile: string(bbr.ProfileStandard),
|
||||
}
|
||||
}
|
||||
switch quicParams.Congestion {
|
||||
case "", "reno", "bbr", "brutal", "force-brutal":
|
||||
default:
|
||||
return errors.New("unknown congestion control: ", quicParams.Congestion)
|
||||
}
|
||||
quicConfig := &quic.Config{
|
||||
InitialStreamReceiveWindow: quicParams.InitStreamReceiveWindow,
|
||||
MaxStreamReceiveWindow: quicParams.MaxStreamReceiveWindow,
|
||||
InitialConnectionReceiveWindow: quicParams.InitConnReceiveWindow,
|
||||
MaxConnectionReceiveWindow: quicParams.MaxConnReceiveWindow,
|
||||
MaxIdleTimeout: time.Duration(quicParams.MaxIdleTimeout) * time.Second,
|
||||
KeepAlivePeriod: time.Duration(quicParams.KeepAlivePeriod) * time.Second,
|
||||
MaxIncomingStreams: quicParams.MaxIncomingStreams,
|
||||
InitialPacketSize: initialPacketSize,
|
||||
DisablePathMTUDiscovery: quicParams.DisablePathMtuDiscovery || (runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin"),
|
||||
EnableDatagrams: true,
|
||||
DisablePathManager: true,
|
||||
}
|
||||
if quicParams.MaxIdleTimeout == 0 {
|
||||
quicConfig.MaxIdleTimeout = 30 * time.Second
|
||||
}
|
||||
|
||||
udpAddr := &net.UDPAddr{IP: address.IP(), Port: int(port)}
|
||||
var err error
|
||||
if streamSettings.FinalMask != nil {
|
||||
l.pktConn, err = streamSettings.FinalMask.ListenPacket(context.Background(), udpAddr)
|
||||
} else {
|
||||
l.pktConn, err = internet.ListenSystemPacket(context.Background(), udpAddr, streamSettings.SocketSettings)
|
||||
}
|
||||
if err != nil {
|
||||
return errors.New("failed to listen UDP on ", address, ":", port).Base(err)
|
||||
}
|
||||
var resetKey *quic.StatelessResetKey
|
||||
if !quicParams.DisableStatelessReset {
|
||||
resetKey = &quic.StatelessResetKey{}
|
||||
common.Must2(rand.Read(resetKey[:]))
|
||||
}
|
||||
l.transport = &quic.Transport{Conn: l.pktConn, DisableGSO: quicParams.DisableGSO, StatelessResetKey: resetKey}
|
||||
|
||||
gotlsConfig := tlsConfig.GetTLSConfig()
|
||||
gotlsConfig.NextProtos = []string{http3.NextProtoH3}
|
||||
l.quicListener, err = l.transport.Listen(gotlsConfig, quicConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.quicServer = &http3.Server{
|
||||
Handler: l,
|
||||
EnableDatagrams: true,
|
||||
ConnContext: func(ctx context.Context, conn *quic.Conn) context.Context {
|
||||
switch quicParams.Congestion {
|
||||
case "reno":
|
||||
case "", "bbr", "brutal":
|
||||
congestion.UseBBR(conn, bbr.Profile(quicParams.BbrProfile))
|
||||
case "force-brutal":
|
||||
congestion.UseBrutal(conn, quicParams.BrutalUp, quicParams.BrutalDisableLossCompensation)
|
||||
}
|
||||
return context.WithValue(ctx, connAddrsKey{}, connAddrs{local: conn.LocalAddr(), remote: conn.RemoteAddr()})
|
||||
},
|
||||
}
|
||||
go func() {
|
||||
if err := l.quicServer.ServeListener(l.quicListener); err != nil && !go_errors.Is(err, quic.ErrServerClosed) && !go_errors.Is(err, http.ErrServerClosed) {
|
||||
errors.LogErrorInner(context.Background(), err, "failed to serve MASQUE over HTTP/3")
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Listener) listenHTTP2(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, tlsConfig *tls.Config) error {
|
||||
tcpAddr := &net.TCPAddr{IP: address.IP(), Port: int(port)}
|
||||
var err error
|
||||
if streamSettings.FinalMask != nil {
|
||||
l.tcpListener, err = streamSettings.FinalMask.Listen(ctx, tcpAddr)
|
||||
} else {
|
||||
l.tcpListener, err = internet.ListenSystem(ctx, tcpAddr, streamSettings.SocketSettings)
|
||||
}
|
||||
if err != nil {
|
||||
return errors.New("failed to listen TCP on ", address, ":", port).Base(err)
|
||||
}
|
||||
gotlsConfig := tlsConfig.GetTLSConfig()
|
||||
gotlsConfig.NextProtos = []string{http2.NextProtoTLS}
|
||||
go l.acceptHTTP2(gotlsConfig)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Listener) acceptHTTP2(config *gotls.Config) {
|
||||
for {
|
||||
conn, err := l.tcpListener.Accept()
|
||||
if err != nil {
|
||||
if l.ctx.Err() != nil || strings.Contains(err.Error(), "closed") {
|
||||
return
|
||||
}
|
||||
errors.LogWarningInner(context.Background(), err, "failed to accept MASQUE connections")
|
||||
if strings.Contains(err.Error(), "too many") {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
continue
|
||||
}
|
||||
go l.serveHTTP2Conn(conn, config)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Listener) serveHTTP2Conn(conn net.Conn, config *gotls.Config) {
|
||||
tlsConn := tls.Server(conn, config).(*tls.Conn)
|
||||
if !l.track(tlsConn, true) {
|
||||
tlsConn.Close()
|
||||
return
|
||||
}
|
||||
defer l.track(tlsConn, false)
|
||||
ctx, cancel := context.WithTimeout(l.ctx, http2HandshakeTimeout)
|
||||
err := tlsConn.HandshakeContext(ctx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
errors.LogDebugInner(context.Background(), err, "MASQUE: TLS handshake failed")
|
||||
tlsConn.Close()
|
||||
return
|
||||
}
|
||||
if protocol := tlsConn.NegotiatedProtocol(); protocol != http2.NextProtoTLS {
|
||||
errors.LogDebug(context.Background(), "MASQUE: the client negotiated ", protocol, " instead of h2")
|
||||
tlsConn.Close()
|
||||
return
|
||||
}
|
||||
serveHTTP2(l.ctx, tlsConn, l)
|
||||
}
|
||||
|
||||
func (l *Listener) track(conn net.Conn, add bool) bool {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if !add {
|
||||
delete(l.conns, conn)
|
||||
return true
|
||||
}
|
||||
if l.ctx.Err() != nil {
|
||||
return false
|
||||
}
|
||||
l.conns[conn] = struct{}{}
|
||||
return true
|
||||
}
|
||||
|
||||
func (l *Listener) Addr() net.Addr {
|
||||
if l.tcpListener != nil {
|
||||
return l.tcpListener.Addr()
|
||||
}
|
||||
return l.quicListener.Addr()
|
||||
}
|
||||
|
||||
func (l *Listener) Close() error {
|
||||
l.cancel()
|
||||
var errs []error
|
||||
if l.quicServer != nil {
|
||||
errs = append(errs, l.quicServer.Close())
|
||||
}
|
||||
if l.quicListener != nil {
|
||||
errs = append(errs, l.quicListener.Close())
|
||||
}
|
||||
if l.transport != nil {
|
||||
errs = append(errs, l.transport.Close())
|
||||
}
|
||||
if l.pktConn != nil {
|
||||
errs = append(errs, l.pktConn.Close())
|
||||
}
|
||||
if l.tcpListener != nil {
|
||||
errs = append(errs, l.tcpListener.Close())
|
||||
}
|
||||
l.mu.Lock()
|
||||
for conn := range l.conns {
|
||||
conn.Close()
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return errors.Combine(errs...)
|
||||
}
|
||||
|
||||
func (l *Listener) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if !l.path.match(r.URL) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
request, err := connectip.ParseProxyRequest(r)
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
if perr, ok := go_errors.AsType[*connectip.ProxyRequestParseError](err); ok {
|
||||
status = perr.HTTPStatus
|
||||
}
|
||||
w.WriteHeader(status)
|
||||
return
|
||||
}
|
||||
addrs, _ := r.Context().Value(connAddrsKey{}).(connAddrs)
|
||||
conn := &ServerConn{
|
||||
w: w,
|
||||
request: r,
|
||||
proxyRequest: request,
|
||||
local: addrs.local,
|
||||
remote: addrs.remote,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
l.addConn(conn)
|
||||
select {
|
||||
case <-conn.done:
|
||||
case <-l.ctx.Done():
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
type pathMatcher struct {
|
||||
path string
|
||||
query url.Values
|
||||
}
|
||||
|
||||
func newPathMatcher(path string) (pathMatcher, error) {
|
||||
u, err := url.ParseRequestURI(path)
|
||||
if err != nil || !strings.HasPrefix(u.Path, "/") {
|
||||
return pathMatcher{}, errors.New("invalid path: ", path)
|
||||
}
|
||||
return pathMatcher{path: u.Path, query: u.Query()}, nil
|
||||
}
|
||||
|
||||
func (m pathMatcher) match(u *url.URL) bool {
|
||||
return u.Path == m.path && maps.EqualFunc(u.Query(), m.query, slices.Equal[[]string])
|
||||
}
|
||||
|
||||
type ServerConn struct {
|
||||
w http.ResponseWriter
|
||||
request *http.Request
|
||||
proxyRequest *connectip.ProxyRequest
|
||||
local net.Addr
|
||||
remote net.Addr
|
||||
answer sync.Once
|
||||
mu sync.Mutex
|
||||
ipConn *connectip.Conn
|
||||
done chan struct{}
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func (c *ServerConn) Request() *http.Request {
|
||||
return c.request
|
||||
}
|
||||
|
||||
func (c *ServerConn) Accept() (*connectip.Conn, error) {
|
||||
var err error = errors.New("the request was already answered")
|
||||
c.answer.Do(func() {
|
||||
var ipConn *connectip.Conn
|
||||
ipConn, err = (&connectip.Proxy{}).Proxy(c.w, c.proxyRequest)
|
||||
c.mu.Lock()
|
||||
c.ipConn = ipConn
|
||||
c.mu.Unlock()
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.ipConn, nil
|
||||
}
|
||||
|
||||
func (c *ServerConn) Reject(status int, header http.Header) {
|
||||
c.answer.Do(func() {
|
||||
for k, vv := range header {
|
||||
for _, v := range vv {
|
||||
c.w.Header().Add(k, v)
|
||||
}
|
||||
}
|
||||
c.w.WriteHeader(status)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *ServerConn) tunnel() *connectip.Conn {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.ipConn
|
||||
}
|
||||
|
||||
func (c *ServerConn) Read(b []byte) (int, error) {
|
||||
ipConn := c.tunnel()
|
||||
if ipConn == nil {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
return ipConn.ReadPacket(b)
|
||||
}
|
||||
|
||||
func (c *ServerConn) Write(b []byte) (int, error) {
|
||||
ipConn := c.tunnel()
|
||||
if ipConn == nil {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
icmp, err := ipConn.WritePacket(b)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(icmp) > 0 {
|
||||
return 0, &PacketTooBigError{ICMP: icmp}
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (c *ServerConn) Close() error {
|
||||
c.closeOnce.Do(func() {
|
||||
c.Reject(http.StatusInternalServerError, nil)
|
||||
if ipConn := c.tunnel(); ipConn != nil {
|
||||
ipConn.Close()
|
||||
}
|
||||
close(c.done)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ServerConn) LocalAddr() net.Addr {
|
||||
return c.local
|
||||
}
|
||||
|
||||
func (c *ServerConn) RemoteAddr() net.Addr {
|
||||
return c.remote
|
||||
}
|
||||
|
||||
func (c *ServerConn) SetDeadline(time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ServerConn) SetReadDeadline(time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ServerConn) SetWriteDeadline(time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
common.Must(internet.RegisterTransportListener(protocolName, Listen))
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package masque
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/xtls/xray-core/transport/internet/stat"
|
||||
"github.com/xtls/xray-core/transport/internet/tls"
|
||||
)
|
||||
|
||||
func TestServerVersions(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
alpn []string
|
||||
h2, h3 bool
|
||||
}{
|
||||
{alpn: nil, h3: true},
|
||||
{alpn: []string{"h3"}, h3: true},
|
||||
{alpn: []string{"h2"}, h2: true},
|
||||
{alpn: []string{"h2", "http/1.1"}, h2: true},
|
||||
{alpn: []string{"h3", "h2"}, h2: true, h3: true},
|
||||
{alpn: []string{"http/1.1"}, h3: true},
|
||||
} {
|
||||
h2, h3 := serverVersions(&tls.Config{NextProtocol: c.alpn})
|
||||
if h2 != c.h2 || h3 != c.h3 {
|
||||
t.Errorf("serverVersions(%q) = %v, %v, want %v, %v", c.alpn, h2, h3, c.h2, c.h3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathMatcher(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
path string
|
||||
request string
|
||||
want bool
|
||||
}{
|
||||
{DefaultPath, "/.well-known/masque/ip/*/*/", true},
|
||||
{DefaultPath, "/.well-known/masque/ip/%2A/%2A/", true},
|
||||
{DefaultPath, "/.well-known/masque/ip/*/*", false},
|
||||
{DefaultPath, "/.well-known/masque/ip/*/*/?x=1", false},
|
||||
{DefaultPath, "/.well-known/masque/ip/192.0.2.1/6/", false},
|
||||
{"/masque?target=*&ipproto=*", "/masque?ipproto=*&target=*", true},
|
||||
{"/masque?target=*&ipproto=*", "/masque?target=*", false},
|
||||
} {
|
||||
m, err := newPathMatcher(c.path)
|
||||
require.NoError(t, err)
|
||||
u, err := url.ParseRequestURI(c.request)
|
||||
require.NoError(t, err)
|
||||
if got := m.match(u); got != c.want {
|
||||
t.Errorf("path %q matching %q = %v, want %v", c.path, c.request, got, c.want)
|
||||
}
|
||||
}
|
||||
_, err := newPathMatcher("masque")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func connectIPRequest(target string) *http.Request {
|
||||
r := httptest.NewRequest(http.MethodGet, "https://proxy.example"+target, nil)
|
||||
r.Method = http.MethodConnect
|
||||
r.Proto, r.ProtoMajor, r.ProtoMinor = "HTTP/2.0", 2, 0
|
||||
r.Header.Set(":protocol", "connect-ip")
|
||||
r.Header.Set("Capsule-Protocol", "?1")
|
||||
r.Body = io.NopCloser(&blockingReader{})
|
||||
return r
|
||||
}
|
||||
|
||||
type blockingReader struct{}
|
||||
|
||||
func (*blockingReader) Read([]byte) (int, error) {
|
||||
time.Sleep(time.Hour)
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
func serve(t *testing.T, r *http.Request, handle func(*ServerConn)) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
path, err := newPathMatcher(DefaultPath)
|
||||
require.NoError(t, err)
|
||||
l := &Listener{path: path, addConn: func(conn stat.Connection) {
|
||||
go func() {
|
||||
handle(conn.(*ServerConn))
|
||||
conn.Close()
|
||||
}()
|
||||
}}
|
||||
l.ctx, l.cancel = context.WithCancel(context.Background())
|
||||
defer l.cancel()
|
||||
w := httptest.NewRecorder()
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
l.ServeHTTP(w, r)
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("ServeHTTP did not return")
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
func TestListenerRejectsOtherRequests(t *testing.T) {
|
||||
unexpected := func(*ServerConn) { t.Error("an invalid request reached the proxy") }
|
||||
|
||||
require.Equal(t, http.StatusNotFound, serve(t, connectIPRequest("/other"), unexpected).Code)
|
||||
|
||||
get := connectIPRequest(DefaultPath)
|
||||
get.Method = http.MethodGet
|
||||
require.Equal(t, http.StatusMethodNotAllowed, serve(t, get, unexpected).Code)
|
||||
|
||||
websocket := connectIPRequest(DefaultPath)
|
||||
websocket.Header.Set(":protocol", "websocket")
|
||||
require.Equal(t, http.StatusNotImplemented, serve(t, websocket, unexpected).Code)
|
||||
|
||||
noCapsules := connectIPRequest(DefaultPath)
|
||||
noCapsules.Header.Del("Capsule-Protocol")
|
||||
require.Equal(t, http.StatusBadRequest, serve(t, noCapsules, unexpected).Code)
|
||||
}
|
||||
|
||||
func TestServerConnAnswers(t *testing.T) {
|
||||
t.Run("reject", func(t *testing.T) {
|
||||
w := serve(t, connectIPRequest(DefaultPath), func(conn *ServerConn) {
|
||||
require.Equal(t, "connect-ip", conn.Request().Header.Get(":protocol"))
|
||||
conn.Reject(http.StatusUnauthorized, http.Header{"WWW-Authenticate": {"Basic"}})
|
||||
})
|
||||
require.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
require.Equal(t, "Basic", w.Header().Get("WWW-Authenticate"))
|
||||
})
|
||||
|
||||
t.Run("no answer", func(t *testing.T) {
|
||||
w := serve(t, connectIPRequest(DefaultPath), func(*ServerConn) {})
|
||||
require.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
})
|
||||
|
||||
t.Run("accept", func(t *testing.T) {
|
||||
w := serve(t, connectIPRequest(DefaultPath), func(conn *ServerConn) {
|
||||
ipConn, err := conn.Accept()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ipConn)
|
||||
_, err = conn.Accept()
|
||||
require.Error(t, err)
|
||||
conn.Reject(http.StatusForbidden, nil)
|
||||
})
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
require.Equal(t, "?1", w.Header().Get("Capsule-Protocol"))
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user