-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathproxy.go
More file actions
179 lines (169 loc) · 6.57 KB
/
proxy.go
File metadata and controls
179 lines (169 loc) · 6.57 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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
package httprelay
import (
"bufio"
"bytes"
"io"
"net"
"net/http"
"sync"
"time"
"github.com/cobratbq/goutils/std/errors"
io_ "github.com/cobratbq/goutils/std/io"
"github.com/cobratbq/goutils/std/log"
http_ "github.com/cobratbq/goutils/std/net/http"
"golang.org/x/net/proxy"
)
func DirectDialer() net.Dialer {
return net.Dialer{
Timeout: 0,
Deadline: time.Time{},
KeepAlive: -1,
FallbackDelay: -1,
}
}
// mnot's blog: https://www.mnot.net/blog/2011/07/11/what_proxies_must_do
// rfc: http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-14#section-3.3
//
// 0. Advertise HTTP/1.1 Correctly - this is covered by starting a normal HTTP
// client connection through the SOCKS proxy which establishes a (sane)
// connection according to its own parameters, instead of blindly copying
// parameters from the original requesting client's connection.
//
// 1. Remove Hop-by-hop Headers - this is covered in the copyHeaders function
// which explicitly skips known hop-by-hop headers and checks 'Connection'
// header for additional headers we need to skip.
//
// Remarks from the blog post not covered explicitly are tested in the tests
// assumptions_test.go
// HTTPProxyHandler is a proxy handler that passes on request to a SOCKS5 proxy server.
type HTTPProxyHandler struct {
// Dialer is the dialer for connecting to the SOCKS5 proxy.
Dialer proxy.Dialer
UserAgent string
}
func (h *HTTPProxyHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
var err error
switch req.Method {
case http.MethodConnect:
// TODO Go 1.20 added an OnProxyConnect callback for use by proxies. This probably voids the use for connection hijacking. Investigate and possibly use.
err = processConnect(resp, req, h.Dialer.Dial)
default:
err = h.processRequest(resp, req)
}
if err != nil {
log.Warnln("Error serving request:", err.Error())
}
}
// TODO append body that explains the error as is expected from 5xx http status codes
func (h *HTTPProxyHandler) processRequest(resp http.ResponseWriter, req *http.Request) error {
// TODO what to do when body of request is very large?
body, err := io.ReadAll(req.Body)
if err != nil {
resp.WriteHeader(http.StatusInternalServerError)
return err
}
io_.CloseLogged(req.Body, "Failed to close request body: %+v")
// The request body is only closed in certain error cases. In other cases, we
// let body be closed by during processing of request to remote host.
log.Infoln(req.Proto, req.Method, req.URL.Host)
// Verification of requests is already handled by net/http library.
// Establish connection with socks proxy
conn, err := h.Dialer.Dial("tcp", fullHost(req.URL.Host))
if err == ErrBlockedHost {
resp.WriteHeader(http.StatusForbidden)
return errors.Context(err, "host '"+req.URL.Host+"'")
} else if err != nil {
resp.WriteHeader(http.StatusInternalServerError)
return errors.Context(err, "failed to connect to host")
}
defer io_.CloseLoggedWithIgnores(conn, "Error closing connection to socks proxy: %+v", io.ErrClosedPipe)
// Prepare request for socks proxy
proxyReq, err := http.NewRequest(req.Method, req.RequestURI, bytes.NewReader(body))
if err != nil {
resp.WriteHeader(http.StatusInternalServerError)
return err
}
// Transfer headers to proxy request
copyHeaders(proxyReq.Header, req.Header)
if h.UserAgent != "" {
// Add specified user agent as header.
proxyReq.Header.Add("User-Agent", h.UserAgent)
}
// Send request to socks proxy
if err = proxyReq.Write(conn); err != nil {
resp.WriteHeader(http.StatusInternalServerError)
return err
}
// Read proxy response
proxyRespReader := bufio.NewReader(conn)
proxyResp, err := http.ReadResponse(proxyRespReader, proxyReq)
if err != nil {
resp.WriteHeader(http.StatusInternalServerError)
return err
}
// Transfer headers to client response
copyHeaders(resp.Header(), proxyResp.Header)
resp.Header().Set("Connection", "close")
// Verification of response is already handled by net/http library.
resp.WriteHeader(proxyResp.StatusCode)
_, err = io.Copy(resp, proxyResp.Body)
io_.CloseLoggedWithIgnores(proxyResp.Body, "Error closing response body: %+v", io.ErrClosedPipe)
return err
}
// "CONNECT"-only proxy, i.e. only establish tunneled connections through CONNECT-method.
type HTTPConnectHandler struct {
// Dialer is the dialer for connecting to the SOCKS5 proxy.
Dialer proxy.Dialer
UserAgent string
}
func (h *HTTPConnectHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
var err error
switch req.Method {
case http.MethodConnect:
// TODO Go 1.20 added an OnProxyConnect callback for use by proxies. This probably voids the use for connection hijacking. Investigate and possibly use.
err = processConnect(resp, req, h.Dialer.Dial)
case http.MethodHead, http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodOptions, http.MethodTrace, http.MethodPatch:
_, err = http_.RespondMethodNotAllowed(resp, []string{http.MethodConnect}, nil)
default:
resp.WriteHeader(http.StatusBadRequest)
_, err = resp.Write([]byte("Bad or unsupported request."))
}
if err != nil {
log.Warnln("Error serving request:", err.Error())
}
}
func processConnect(resp http.ResponseWriter, req *http.Request, dial func(string, string) (net.Conn, error)) error {
defer io_.CloseLoggedWithIgnores(req.Body, "Error while closing request body: %+v", io.ErrClosedPipe)
log.Infoln(req.Proto, req.Method, req.URL.Host)
// Establish connection with socks proxy
proxyConn, err := dial("tcp", req.Host)
if err == ErrBlockedHost {
resp.WriteHeader(http.StatusForbidden)
return err
} else if err != nil {
resp.WriteHeader(http.StatusInternalServerError)
return err
}
defer io_.CloseLoggedWithIgnores(proxyConn, "Failed to close connection to remote location: %+v", io.ErrClosedPipe)
// Acquire raw connection to the client
clientInput, clientConn, err := http_.HijackConnection(resp)
if err != nil {
resp.WriteHeader(http.StatusInternalServerError)
return err
}
defer io_.CloseLoggedWithIgnores(clientConn, "Failed to close connection to local client: %+v", io.ErrClosedPipe)
// Send 200 Connection established to client to signal tunnel ready
// Responses to CONNECT requests MUST NOT contain any body payload.
// TODO add additional headers to proxy server's response? (Via)
if _, err = clientConn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n")); err != nil {
resp.WriteHeader(http.StatusInternalServerError)
return err
}
// Start copying data from one connection to the other
var wg sync.WaitGroup
wg.Add(2)
go io_.Transfer(&wg, proxyConn, clientInput)
go io_.Transfer(&wg, clientConn, proxyConn)
wg.Wait()
return nil
}