Repository navigation
Expand file tree
/
Copy pathhttps_head_test.go
More file actions
163 lines (143 loc) · 4.63 KB
/
Copy pathhttps_head_test.go
File metadata and controls
163 lines (143 loc) · 4.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
package goproxy_test
import (
"context"
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"net/url"
"sync/atomic"
"testing"
"time"
"github.com/elazarl/goproxy"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type countingConn struct {
net.Conn
writes *atomic.Int32
}
func (c *countingConn) Write(p []byte) (int, error) {
c.writes.Add(1)
return c.Conn.Write(p)
}
type countingListener struct {
net.Listener
writes *atomic.Int32
}
func (l *countingListener) Accept() (net.Conn, error) {
c, err := l.Listener.Accept()
if err != nil {
return c, err
}
return &countingConn{Conn: c, writes: l.writes}, nil
}
func TestMitmResponseStreamsSSE(t *testing.T) {
const event = "data: ready\n\n"
release := make(chan struct{})
upstream := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.WriteHeader(http.StatusOK)
_, _ = io.WriteString(w, event)
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
<-release
}))
defer upstream.Close()
proxy := goproxy.NewProxyHttpServer()
proxy.Tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm)
proxyServer := httptest.NewServer(proxy)
defer proxyServer.Close()
defer close(release)
proxyURL, err := url.Parse(proxyServer.URL)
require.NoError(t, err)
client := &http.Client{Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}}
result := make(chan error, 1)
go func() {
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, upstream.URL, nil)
if err != nil {
result <- err
return
}
resp, err := client.Do(req)
if err != nil {
result <- err
return
}
defer resp.Body.Close()
body := make([]byte, len(event))
_, err = io.ReadFull(resp.Body, body)
if err == nil && string(body) != event {
err = fmt.Errorf("received event %q, want %q", body, event)
}
result <- err
}()
select {
case err := <-result:
require.NoError(t, err)
case <-time.After(2 * time.Second):
t.Fatal("SSE event was buffered while the upstream response remained open")
}
}
// TestMitmResponseHeadIsNotFragmented ensures the MITM response head (status line
// + headers + terminator) is written to the client as a single buffered flush
// rather than one tiny TLS record per header field. Fragmenting it into dozens of
// tiny records breaks strict clients (e.g. tungstenite's WebSocket handshake
// rejects a response delivered as too many small packets).
func TestMitmResponseHeadIsNotFragmented(t *testing.T) {
upstream := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for _, kv := range [][]string{
{"Cache-Control", "no-cache"},
{"Content-Language", "en"},
{"Referrer-Policy", "strict-origin-when-cross-origin"},
{"Strict-Transport-Security", "max-age=31536000; includeSubDomains"},
{"Vary", "Accept-Encoding"},
{"X-Content-Type-Options", "nosniff"},
{"X-Frame-Options", "DENY"},
{"X-One", "1"},
{"X-Two", "2"},
{"X-Three", "3"},
{"X-Four", "4"},
{"X-Five", "5"},
} {
w.Header().Set(kv[0], kv[1])
}
_, _ = io.WriteString(w, "ok")
}))
defer upstream.Close()
proxy := goproxy.NewProxyHttpServer()
proxy.Tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
proxy.OnRequest(goproxy.ReqHostIs(upstream.Listener.Addr().String())).HandleConnect(goproxy.AlwaysMitm)
var writes atomic.Int32
ln, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", "127.0.0.1:0")
require.NoError(t, err)
server := &http.Server{Handler: proxy, ReadHeaderTimeout: 10 * time.Second}
go func() {
_ = server.Serve(&countingListener{Listener: ln, writes: &writes})
}()
defer server.Close()
proxyURL := &url.URL{Scheme: "http", Host: ln.Addr().String()}
client := &http.Client{Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}}
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, upstream.URL+"/", nil)
require.NoError(t, err)
resp, err := client.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, "DENY", resp.Header.Get("X-Frame-Options"))
// Buffered head flush → a handful of records; unbuffered, the 12-header head alone is dozens.
n := int(writes.Load())
t.Logf("client-facing TLS record writes: %d", n)
assert.Less(t, n, 20,
"response head should be coalesced into one record, not fragmented per header (got %d writes)", n)
}