Skip to content

Commit 710a918

Browse files
committed
deep review: fixes
1 parent beee905 commit 710a918

10 files changed

Lines changed: 981 additions & 54 deletions

File tree

http.go

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -169,12 +169,22 @@ func (self *HttpProxy) handleHttp(w http.ResponseWriter, r *http.Request) {
169169
// r.Header.Del("Proxy-Authorization")
170170

171171
b := bytes.NewBuffer(nil)
172-
_, err := copyBufferWithTimeout(b, io.LimitReader(r.Body, self.MaxHttpBodyBytes), nil, self.ProxyReadTimeout, self.ProxyWriteTimeout)
173-
r.Body.Close()
172+
bodyReader := io.Reader(r.Body)
173+
if 0 < self.MaxHttpBodyBytes {
174+
bodyReader = io.LimitReader(r.Body, self.MaxHttpBodyBytes+1)
175+
}
176+
_, err := copyBufferWithTimeout(b, bodyReader, nil, self.ProxyReadTimeout, self.ProxyWriteTimeout)
177+
if closeErr := r.Body.Close(); err == nil {
178+
err = closeErr
179+
}
174180
if err != nil {
175181
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
176182
return
177183
}
184+
if 0 < self.MaxHttpBodyBytes && self.MaxHttpBodyBytes < int64(b.Len()) {
185+
http.Error(w, http.StatusText(http.StatusRequestEntityTooLarge), http.StatusRequestEntityTooLarge)
186+
return
187+
}
178188
bodyBytes := b.Bytes()
179189

180190
handleCtx, handleCancel := context.WithCancel(r.Context())
@@ -195,16 +205,7 @@ func (self *HttpProxy) handleHttp(w http.ResponseWriter, r *http.Request) {
195205

196206
var response *http.Response
197207
for {
198-
r2, err := http.NewRequestWithContext(
199-
r.Context(),
200-
r.Method,
201-
r.URL.String(),
202-
bytes.NewReader(bodyBytes),
203-
)
204-
if err != nil {
205-
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
206-
return
207-
}
208+
r2 := cloneProxyRequest(handleCtx, r, bodyBytes)
208209
reconnect := connect.NewReconnect(self.ProxyConnectTimeout)
209210
response, err = tr.RoundTrip(r2)
210211
if err == nil {
@@ -265,8 +266,9 @@ func (self *HttpProxy) handleHttp(w http.ResponseWriter, r *http.Request) {
265266
chunked = true
266267
}
267268
if chunked {
268-
f := w.(http.Flusher)
269-
flush = f.Flush
269+
if f, ok := w.(http.Flusher); ok {
270+
flush = f.Flush
271+
}
270272
}
271273
_, err := copyBufferWithTimeoutAndFlush(w, response.Body, nil, self.ProxyReadTimeout, self.ProxyWriteTimeout, flush)
272274
if err != nil {
@@ -275,6 +277,33 @@ func (self *HttpProxy) handleHttp(w http.ResponseWriter, r *http.Request) {
275277
}
276278
}
277279

280+
func cloneProxyRequest(ctx context.Context, r *http.Request, bodyBytes []byte) *http.Request {
281+
r2 := r.Clone(ctx)
282+
r2.RequestURI = ""
283+
r2.Header = r.Header.Clone()
284+
removeProxyRequestHeaders(r2.Header)
285+
if len(bodyBytes) == 0 {
286+
r2.Body = http.NoBody
287+
r2.GetBody = func() (io.ReadCloser, error) {
288+
return http.NoBody, nil
289+
}
290+
r2.ContentLength = 0
291+
} else {
292+
r2.Body = io.NopCloser(bytes.NewReader(bodyBytes))
293+
r2.GetBody = func() (io.ReadCloser, error) {
294+
return io.NopCloser(bytes.NewReader(bodyBytes)), nil
295+
}
296+
r2.ContentLength = int64(len(bodyBytes))
297+
}
298+
return r2
299+
}
300+
301+
func removeProxyRequestHeaders(h http.Header) {
302+
h.Del("Proxy-Authenticate")
303+
h.Del("Proxy-Authorization")
304+
h.Del("Proxy-Connection")
305+
}
306+
278307
// for a hijacked connection
279308
func httpError(w io.Writer, statusCode int, err error) error {
280309
errorMessage := err.Error()

http_test.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package proxy
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"io"
7+
"net"
8+
"net/http"
9+
"net/http/httptest"
10+
"strings"
11+
"testing"
12+
"time"
13+
)
14+
15+
func TestHttpProxyForwardsRequestMetadataAndBody(t *testing.T) {
16+
type receivedRequest struct {
17+
host string
18+
authorization string
19+
contentType string
20+
proxyAuthorization string
21+
body string
22+
}
23+
24+
received := make(chan receivedRequest, 1)
25+
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
26+
body, err := io.ReadAll(r.Body)
27+
if err != nil {
28+
t.Errorf("backend read body: %v", err)
29+
return
30+
}
31+
received <- receivedRequest{
32+
host: r.Host,
33+
authorization: r.Header.Get("Authorization"),
34+
contentType: r.Header.Get("Content-Type"),
35+
proxyAuthorization: r.Header.Get("Proxy-Authorization"),
36+
body: string(body),
37+
}
38+
w.Header().Set("X-Backend", "ok")
39+
w.WriteHeader(http.StatusCreated)
40+
_, _ = w.Write([]byte("created"))
41+
}))
42+
defer backend.Close()
43+
44+
proxy := NewHttpProxy()
45+
proxy.ConnectDialWithRequest = func(r *http.Request, network string, addr string) (net.Conn, error) {
46+
var d net.Dialer
47+
return d.DialContext(r.Context(), network, addr)
48+
}
49+
50+
req := httptest.NewRequest(http.MethodPost, backend.URL+"/resource", strings.NewReader("request-body"))
51+
req.Host = "forwarded.example"
52+
req.Header.Set("Authorization", "Bearer token")
53+
req.Header.Set("Content-Type", "text/plain")
54+
req.Header.Set("Proxy-Authorization", "proxy-secret")
55+
56+
rr := httptest.NewRecorder()
57+
proxy.ServeHTTP(rr, req)
58+
59+
if rr.Code != http.StatusCreated {
60+
t.Fatalf("status = %d, want %d; body=%q", rr.Code, http.StatusCreated, rr.Body.String())
61+
}
62+
if rr.Header().Get("X-Backend") != "ok" {
63+
t.Fatalf("missing backend response header")
64+
}
65+
66+
got := <-received
67+
if got.host != "forwarded.example" {
68+
t.Fatalf("host = %q, want forwarded.example", got.host)
69+
}
70+
if got.authorization != "Bearer token" {
71+
t.Fatalf("authorization = %q", got.authorization)
72+
}
73+
if got.contentType != "text/plain" {
74+
t.Fatalf("content-type = %q", got.contentType)
75+
}
76+
if got.proxyAuthorization != "" {
77+
t.Fatalf("proxy authorization was forwarded: %q", got.proxyAuthorization)
78+
}
79+
if got.body != "request-body" {
80+
t.Fatalf("body = %q", got.body)
81+
}
82+
}
83+
84+
func TestHttpProxyRejectsOversizedRequestBody(t *testing.T) {
85+
proxy := NewHttpProxy()
86+
proxy.MaxHttpBodyBytes = 3
87+
proxy.ConnectDialWithRequest = func(r *http.Request, network string, addr string) (net.Conn, error) {
88+
t.Fatalf("dial should not be called for oversized request")
89+
return nil, context.Canceled
90+
}
91+
92+
req := httptest.NewRequest(http.MethodPost, "http://example.test/upload", strings.NewReader("four"))
93+
rr := httptest.NewRecorder()
94+
95+
proxy.ServeHTTP(rr, req)
96+
97+
if rr.Code != http.StatusRequestEntityTooLarge {
98+
t.Fatalf("status = %d, want %d", rr.Code, http.StatusRequestEntityTooLarge)
99+
}
100+
}
101+
102+
func TestHttpProxyConnectTunnel(t *testing.T) {
103+
backendAddr := startTCPBackend(t, func(conn net.Conn) {
104+
defer conn.Close()
105+
buf := make([]byte, 4)
106+
if _, err := io.ReadFull(conn, buf); err != nil {
107+
t.Errorf("backend read: %v", err)
108+
return
109+
}
110+
if string(buf) != "ping" {
111+
t.Errorf("backend got %q, want ping", string(buf))
112+
return
113+
}
114+
_, _ = conn.Write([]byte("pong"))
115+
})
116+
117+
proxy := NewHttpProxy()
118+
proxy.ConnectDialWithRequest = func(r *http.Request, network string, addr string) (net.Conn, error) {
119+
var d net.Dialer
120+
return d.DialContext(r.Context(), "tcp", backendAddr)
121+
}
122+
123+
ctx, cancel := context.WithCancel(context.Background())
124+
defer cancel()
125+
proxyAddr := freeTCPAddr(t)
126+
errCh := make(chan error, 1)
127+
go func() {
128+
errCh <- proxy.ListenAndServe(ctx, "tcp", proxyAddr)
129+
}()
130+
waitForTCP(t, proxyAddr)
131+
defer func() {
132+
cancel()
133+
select {
134+
case err := <-errCh:
135+
if err != nil && err != http.ErrServerClosed {
136+
t.Fatalf("http proxy returned error: %v", err)
137+
}
138+
case <-time.After(2 * time.Second):
139+
t.Fatalf("http proxy did not stop")
140+
}
141+
}()
142+
143+
conn, err := net.DialTimeout("tcp", proxyAddr, 2*time.Second)
144+
if err != nil {
145+
t.Fatalf("dial proxy: %v", err)
146+
}
147+
defer conn.Close()
148+
if err := conn.SetDeadline(time.Now().Add(3 * time.Second)); err != nil {
149+
t.Fatalf("set deadline: %v", err)
150+
}
151+
152+
if _, err := io.WriteString(conn, "CONNECT example.test:443 HTTP/1.1\r\nHost: example.test:443\r\n\r\n"); err != nil {
153+
t.Fatalf("write connect request: %v", err)
154+
}
155+
reader := bufio.NewReader(conn)
156+
status, err := reader.ReadString('\n')
157+
if err != nil {
158+
t.Fatalf("read connect status: %v", err)
159+
}
160+
if !strings.Contains(status, "200") {
161+
t.Fatalf("connect status = %q", status)
162+
}
163+
for {
164+
line, err := reader.ReadString('\n')
165+
if err != nil {
166+
t.Fatalf("read connect headers: %v", err)
167+
}
168+
if line == "\r\n" {
169+
break
170+
}
171+
}
172+
173+
if _, err := conn.Write([]byte("ping")); err != nil {
174+
t.Fatalf("write tunnel: %v", err)
175+
}
176+
buf := make([]byte, 4)
177+
if _, err := io.ReadFull(reader, buf); err != nil {
178+
t.Fatalf("read tunnel: %v", err)
179+
}
180+
if string(buf) != "pong" {
181+
t.Fatalf("tunnel got %q, want pong", string(buf))
182+
}
183+
}

profile/cpu

2.95 KB
Binary file not shown.

profile/memory

1.69 KB
Binary file not shown.

socks.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ func (self *SocksProxy) ListenAndServe(ctx context.Context, network string, addr
9393
}
9494

9595
func (self *SocksProxy) connectHandle(ctx context.Context, writer io.Writer, r SocksRequest) error {
96+
clientConn, _ := writer.(net.Conn)
9697
proxyConn, err := self.ConnectDialWithRequest(ctx, r, "tcp", r.DestAddr.String())
9798
if err != nil {
9899
resp := mapDialErrorToSocksReply(err)
@@ -103,6 +104,9 @@ func (self *SocksProxy) connectHandle(ctx context.Context, writer io.Writer, r S
103104
defer handleCancel()
104105
go connect.HandleError(func() {
105106
defer proxyConn.Close()
107+
if clientConn != nil {
108+
defer clientConn.Close()
109+
}
106110
select {
107111
case <-handleCtx.Done():
108112
}
@@ -131,8 +135,9 @@ func (self *SocksProxy) Valid(username string, password string, userAddr string)
131135

132136
// socks.NameResolver
133137
func (self *SocksProxy) Resolve(ctx context.Context, name string) (context.Context, net.IP, error) {
134-
// names are not resolved locally
135-
return ctx, net.ParseIP("0.0.0.0").To4(), nil
138+
// Names are resolved by the proxied dialer. Returning nil preserves the FQDN
139+
// in the request address instead of replacing it with a local resolver result.
140+
return ctx, nil, nil
136141
}
137142

138143
func mapDialErrorToSocksReply(err error) uint8 {

0 commit comments

Comments
 (0)