-
Notifications
You must be signed in to change notification settings - Fork 63
/
connect.go
148 lines (137 loc) · 3.84 KB
/
connect.go
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
// Copyright (c) 2017 Yandex LLC. All rights reserved.
// Use of this source code is governed by a MPL 2.0
// license that can be found in the LICENSE file.
// Author: Vladimir Skipor <skipor@yandex-team.ru>
package phttp
import (
"bufio"
"context"
"crypto/tls"
"net"
"net/http"
"net/http/httputil"
"net/url"
"github.com/pkg/errors"
"github.com/yandex/pandora/lib/netutil"
)
type ConnectGunConfig struct {
Target string `validate:"endpoint,required"`
ConnectSSL bool `config:"connect-ssl"` // Defines if tunnel encrypted.
SSL bool // As in HTTP gun, defines scheme for http requests.
Client ClientConfig `config:",squash"`
BaseGunConfig `config:",squash"`
}
func NewConnectGun(conf ConnectGunConfig) *ConnectGun {
scheme := "http"
if conf.SSL {
scheme = "https"
}
client := newConnectClient(conf)
var g ConnectGun
g = ConnectGun{
BaseGun: BaseGun{
Config: conf.BaseGunConfig,
Do: g.Do,
OnClose: func() error {
client.CloseIdleConnections()
return nil
},
},
scheme: scheme,
client: client,
}
return &g
}
type ConnectGun struct {
BaseGun
scheme string
client Client
}
var _ Gun = (*ConnectGun)(nil)
func (g *ConnectGun) Do(req *http.Request) (*http.Response, error) {
req.URL.Scheme = g.scheme
return g.client.Do(req)
}
func DefaultConnectGunConfig() ConnectGunConfig {
return ConnectGunConfig{
SSL: false,
ConnectSSL: false,
Client: DefaultClientConfig(),
}
}
func newConnectClient(conf ConnectGunConfig) Client {
transport := NewTransport(conf.Client.Transport,
newConnectDialFunc(
conf.Target,
conf.ConnectSSL,
NewDialer(conf.Client.Dialer),
))
return newClient(transport, conf.Client.Redirect)
}
func newConnectDialFunc(target string, connectSSL bool, dialer netutil.Dialer) netutil.DialerFunc {
return func(ctx context.Context, network, address string) (conn net.Conn, err error) {
// TODO(skipor): make connect sample.
// TODO(skipor): make httptrace callbacks called correctly.
if network != "tcp" {
panic("unsupported network " + network)
}
defer func() {
if err != nil && conn != nil {
_ = conn.Close()
conn = nil
}
}()
conn, err = dialer.DialContext(ctx, "tcp", target)
if err != nil {
err = errors.WithStack(err)
return
}
if connectSSL {
conn = tls.Client(conn, &tls.Config{InsecureSkipVerify: true})
}
req := &http.Request{
Method: "CONNECT",
URL: &url.URL{},
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(http.Header),
Body: nil,
Host: address,
}
// NOTE(skipor): any logic for CONNECT request can be easily added via hooks.
err = req.Write(conn)
if err != nil {
err = errors.WithStack(err)
return
}
// NOTE(skipor): according to RFC 2817 we can send origin at that moment and not wait
// for request. That requires to wrap conn and do following logic at first read.
r := bufio.NewReader(conn)
res, err := http.ReadResponse(r, req)
if err != nil {
err = errors.WithStack(err)
return
}
// RFC 7230 3.3.3.2: Any 2xx (Successful) response to a CONNECT request implies that
// the connection will become a tunnel immediately after the empty
// line that concludes the header fields. A client MUST ignore any
// Content-Length or Transfer-Encoding header fields received in
// such a message.
if res.StatusCode != http.StatusOK {
dump, dumpErr := httputil.DumpResponse(res, false)
err = errors.Errorf("Unexpected status code. Dumped response:\n%s\n Dump error: %s",
dump, dumpErr)
return
}
// No need to close body.
if r.Buffered() != 0 {
// Already receive something non HTTP from proxy or dialed server.
// Anyway it is incorrect situation.
peek, _ := r.Peek(r.Buffered())
err = errors.Errorf("Unexpected extra data after connect: %q", peek)
return
}
return
}
}