-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdial.go
More file actions
160 lines (133 loc) · 4.43 KB
/
Copy pathdial.go
File metadata and controls
160 lines (133 loc) · 4.43 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
package cooper
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"strings"
"time"
)
var (
// ErrNilRequest is returned when the provided *http.Request is nil.
ErrNilRequest = errors.New("request cannot be nil")
// ErrInvalidHost is returned when the request URL does not contain a valid hostname.
ErrInvalidHost = errors.New("request is missing a valid host")
// ErrProtocolConflict is returned when a protocol is supplied via WithProtocol
// and the request already carries a different Upgrade header value.
ErrProtocolConflict = errors.New("protocol conflict: WithProtocol and Upgrade header disagree")
// ErrDialFailed is returned when the initial network connection (TCP)
// to the remote host cannot be established. This usually indicates
// a timeout, an unreachable host, or a refused connection.
ErrDialFailed = errors.New("dial failed")
// ErrTLSHandshakeFailed is returned when the TCP connection is successful
// but the subsequent TLS/SSL cryptographic handshake fails. This can
// happen due to expired certificates, hostname mismatches (SNI),
// or unsupported protocol versions.
ErrTLSHandshakeFailed = errors.New("tls handshake failed")
)
// DialOption configures the behaviour of Dial.
type DialOption func(*dialConfig)
type dialConfig struct {
dialer *net.Dialer
tlsConfig *tls.Config
upgradeOptions []UpgradeOption
protocol string
}
// WithDialer sets a custom net.Dialer for establishing the TCP connection.
func WithDialer(d *net.Dialer) DialOption {
return func(c *dialConfig) {
c.dialer = d
}
}
// WithTLSConfig enables TLS for the connection using the provided configuration.
// Pass &tls.Config{} to use default TLS settings; the server name is derived
// from the request URL when not explicitly set.
func WithTLSConfig(tlsConfig *tls.Config) DialOption {
return func(c *dialConfig) {
c.tlsConfig = tlsConfig
}
}
// WithUpgradeOptions passes additional options to the underlying Upgrade call.
func WithUpgradeOptions(opts ...UpgradeOption) DialOption {
return func(c *dialConfig) {
c.upgradeOptions = append(c.upgradeOptions, opts...)
}
}
// WithProtocol sets the Upgrade protocol when the request does not already
// carry an Upgrade header. If the request header is already set to a different
// value, Dial returns ErrProtocolConflict.
func WithProtocol(proto string) DialOption {
return func(c *dialConfig) {
c.protocol = proto
}
}
// DialContext establishes a connection to the host specified in r, performs
// an HTTP/1.1 protocol upgrade, and returns the established connection using the provided context.
func DialContext(ctx context.Context, r *http.Request, opts ...DialOption) (net.Conn, error) {
if r == nil {
return nil, ErrNilRequest
}
cfg := &dialConfig{}
for _, o := range opts {
o(cfg)
}
headerProto := r.Header.Get("Upgrade")
if cfg.protocol != "" && headerProto != "" && !strings.EqualFold(cfg.protocol, headerProto) {
return nil, fmt.Errorf("%w: option %q, header %q", ErrProtocolConflict, cfg.protocol, headerProto)
}
if cfg.protocol != "" && headerProto == "" {
r.Header.Set("Upgrade", cfg.protocol)
}
host := r.URL.Hostname()
if host == "" {
return nil, ErrInvalidHost
}
port := r.URL.Port()
if port == "" {
if cfg.tlsConfig != nil {
port = "443"
} else {
port = "80"
}
}
addr := net.JoinHostPort(host, port)
dialer := cfg.dialer
if dialer == nil {
dialer = &net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
}
}
conn, err := dialer.DialContext(ctx, "tcp", addr)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrDialFailed, err)
}
if cfg.tlsConfig != nil {
tlsConfig := cfg.tlsConfig
if tlsConfig.ServerName == "" {
tlsConfig = tlsConfig.Clone()
tlsConfig.ServerName = host
}
tlsConn := tls.Client(conn, tlsConfig)
if err := tlsConn.HandshakeContext(ctx); err != nil {
conn.Close()
return nil, fmt.Errorf("%w: %w", ErrTLSHandshakeFailed, err)
}
conn = tlsConn
}
upgradedConn, err := Upgrade(conn, r, cfg.upgradeOptions...)
if err != nil {
conn.Close()
return nil, err
}
return upgradedConn, nil
}
// Dial establishes a connection to the host specified in r, performs
// an HTTP/1.1 protocol upgrade, and returns the established connection using the request context.
//
// In case you want to use an alternate context for dialing use DialContext instead.
func Dial(r *http.Request, opts ...DialOption) (net.Conn, error) {
return DialContext(r.Context(), r, opts...)
}