-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathconnected_roundtripper.go
More file actions
132 lines (116 loc) · 4.15 KB
/
connected_roundtripper.go
File metadata and controls
132 lines (116 loc) · 4.15 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
package fronted
import (
"fmt"
"io"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/getlantern/ops"
)
type connectedRoundTripper struct {
front Front
net.Conn
provider *Provider
}
func newConnectedRoundTripper(fr Front, conn net.Conn, provider *Provider) connectedRoundTripper {
return connectedRoundTripper{
front: fr,
Conn: conn,
provider: provider,
}
}
// Also implements http.RoundTripper
func (crt connectedRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
op := ops.Begin("fronted_request")
defer op.End()
originHost := req.URL.Hostname()
frontedHost := crt.provider.Lookup(originHost)
if frontedHost == "" {
// this error is not the masquerade's fault in particular
// so it is returned as good.
crt.Conn.Close()
crt.front.markWithResult(true)
err := fmt.Errorf("no domain fronting mapping for '%s'. Please add it to provider_map.yaml or equivalent for %s",
crt.front.getProviderID(), originHost)
op.FailIf(err)
return nil, err
}
log.Debug("Host translated from origin to fronted", "origin", originHost, "fronted", frontedHost, "provider", crt.front.getProviderID())
reqi, err := withDomainFront(req, frontedHost, req.Body)
if err != nil {
log.Debug("Could not copy request with domain front", "error", err, "origin", originHost, "frontedHost", frontedHost, "provider", crt.front.getProviderID())
return nil, op.FailIf(fmt.Errorf("Failed to copy http request with origin translated to %v?: %w", frontedHost, err))
}
disableKeepAlives := true
if strings.EqualFold(reqi.Header.Get("Connection"), "upgrade") {
disableKeepAlives = false
}
tr := connectedConnHTTPTransport(crt.Conn, disableKeepAlives)
resp, err := tr.RoundTrip(reqi)
if err != nil {
log.Debug("Could not complete request", "error", err, "origin", originHost, "frontedHost", frontedHost, "provider", crt.front.getProviderID())
crt.front.markWithResult(false)
return nil, err
}
err = crt.provider.ValidateResponse(resp)
if err != nil {
log.Debug("Response validation failed", "error", err, "origin", originHost, "frontedHost", frontedHost, "provider", crt.front.getProviderID())
resp.Body.Close()
crt.front.markWithResult(false)
return nil, err
}
crt.front.markWithResult(true)
log.Debug("Request completed successfully")
return resp, nil
}
// connectedConnHTTPTransport uses a preconnected connection to the CDN to make HTTP requests.
// This uses the pre-established connection to the CDN on the fronting domain.
func connectedConnHTTPTransport(conn net.Conn, disableKeepAlives bool) http.RoundTripper {
return &connectedTransport{
Transport: http.Transport{
Dial: func(network, addr string) (net.Conn, error) {
return conn, nil
},
TLSHandshakeTimeout: 20 * time.Second,
DisableKeepAlives: disableKeepAlives,
IdleConnTimeout: 70 * time.Second,
},
}
}
// connectedTransport is a wrapper struct enabling us to modify the protocol of outgoing
// requests to make them all HTTP instead of potentially HTTPS, which breaks our particular
// implemenation of direct domain fronting.
type connectedTransport struct {
http.Transport
}
func (ct *connectedTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
defer func(op ops.Op) { op.End() }(ops.Begin("direct_transport_roundtrip"))
// The connection is already encrypted by domain fronting. We need to rewrite URLs starting
// with "https://" to "http://", lest we get an error for doubling up on TLS.
// The RoundTrip interface requires that we not modify the memory in the request, so we just
// create a copy.
norm := new(http.Request)
*norm = *req // includes shallow copies of maps, but okay
norm.URL = new(url.URL)
*norm.URL = *req.URL
norm.URL.Scheme = "http"
return ct.Transport.RoundTrip(norm)
}
func withDomainFront(req *http.Request, frontedHost string, body io.ReadCloser) (*http.Request, error) {
urlCopy := *req.URL
urlCopy.Host = frontedHost
r, err := http.NewRequestWithContext(req.Context(), req.Method, urlCopy.String(), body)
if err != nil {
return nil, err
}
for k, vs := range req.Header {
if !strings.EqualFold(k, "Host") {
v := make([]string, len(vs))
copy(v, vs)
r.Header[k] = v
}
}
return r, nil
}