mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-08-07 23:31:56 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c76283a2b4 | |||
| ae374c0463 | |||
| 42246f986c | |||
| 80f387214c | |||
| a368fbbe9b | |||
| 32df01a9da | |||
| 6dcf3a4cbc | |||
| 96f11de7ab | |||
| d03469b6d3 | |||
| 0cf6ce9aeb | |||
| e8f55cc911 | |||
| 35cee13175 | |||
| 5376df5439 | |||
| db9b6541d0 | |||
| 5bd4028ea7 | |||
| 1b2a96f96b | |||
| d50fee4fa0 | |||
| 6b3e2b020b | |||
| 6624a24040 | |||
| 7b81cf8aa6 | |||
| 26f5f80811 | |||
| 29f4650e25 | |||
| a14aa0322c |
@@ -1,3 +1,28 @@
|
||||
2020.3.1
|
||||
- 2020-03-27 TUN-2846: Trigger debug reconnects from stdin commands, not SIGUSR1
|
||||
|
||||
2020.3.0
|
||||
- 2020-03-23 AUTH-2394 fixed header for websockets. Added TCP alias
|
||||
- 2020-03-10 TUN-2797: Fix panic in SetConnDigest by making mutexes values.
|
||||
- 2020-03-13 TUN-2807: cloudflared hello-world shouldn't assume it's my first tunnel
|
||||
- 2020-03-13 TUN-2756: Set connection digest after reconnect.
|
||||
- 2020-03-16 TUN-2812: Tunnel proxies and RPCs can share an edge address
|
||||
- 2020-03-18 TUN-2816: cloudflared metrics server should be more discoverable
|
||||
- 2020-03-19 TUN-2820: Serialized headers for Websockets
|
||||
- 2020-03-19 TUN-2819: cloudflared should close its connections when a signal is sent
|
||||
- 2020-03-19 TUN-2823: Bugfix. cloudflared would hang forever if error occurred.
|
||||
- 2020-03-10 TUN-2796: Implement HTTP2 CONTINUATION headers correctly
|
||||
- 2020-03-02 TUN-2779: update sample HTML pages
|
||||
- 2020-03-04 TUN-2785: Use reconnect token by default
|
||||
- 2020-03-05 TUN-2754: Add ConnDigest to cloudflared and its RPCs
|
||||
- 2020-03-06 TUN-2755: ReconnectTunnel RPC now transmits ConnectionDigest
|
||||
- 2020-03-06 TUN-2761: Use the new header management functions in cloudflared
|
||||
- 2020-03-06 TUN-2788: cloudflared should store one ConnDigest per HA connection
|
||||
- 2020-02-26 TUN-2767: Test for large headers
|
||||
- 2020-02-28 do not terminate tunnel if origin is not reachable on start-up (#177)
|
||||
- 2020-02-28 TUN-2776: Add header serialization feature in cloudflared
|
||||
- 2020-02-21 TUN-2748: Insecure randomness vulnerability in github.com/miekg/dns
|
||||
|
||||
2020.2.1
|
||||
- 2020-02-20 TUN-2745: Rename existing header management functions
|
||||
- 2020-02-21 TUN-2746: Add the new header management functions
|
||||
|
||||
@@ -119,7 +119,7 @@ func Commands() []*cli.Command {
|
||||
{
|
||||
Name: "ssh",
|
||||
Action: ssh,
|
||||
Aliases: []string{"rdp"},
|
||||
Aliases: []string{"rdp", "tcp"},
|
||||
Usage: "",
|
||||
ArgsUsage: "",
|
||||
Description: `The ssh subcommand sends data over a proxy to the Cloudflare edge.`,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@@ -399,10 +400,16 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||
return err
|
||||
}
|
||||
|
||||
reconnectCh := make(chan struct{}, 1)
|
||||
if c.IsSet("stdin-control") {
|
||||
logger.Warn("Enabling control through stdin")
|
||||
go stdinControl(reconnectCh)
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errC <- origin.StartTunnelDaemon(ctx, tunnelConfig, connectedSignal, cloudflaredID)
|
||||
errC <- origin.StartTunnelDaemon(ctx, tunnelConfig, connectedSignal, cloudflaredID, reconnectCh)
|
||||
}()
|
||||
|
||||
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, c.Duration("grace-period"))
|
||||
@@ -596,6 +603,8 @@ func hostnameFromURI(uri string) string {
|
||||
return addPortIfMissing(u, 22)
|
||||
case "rdp":
|
||||
return addPortIfMissing(u, 3389)
|
||||
case "tcp":
|
||||
return addPortIfMissing(u, 7864) // just a random port since there isn't a default in this case
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -980,9 +989,16 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "use-reconnect-token",
|
||||
Usage: "Test reestablishing connections with the new 'reconnect token' flow.",
|
||||
Value: true,
|
||||
EnvVars: []string{"TUNNEL_USE_RECONNECT_TOKEN"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "use-quick-reconnects",
|
||||
Usage: "Test reestablishing connections with the new 'connection digest' flow.",
|
||||
EnvVars: []string{"TUNNEL_USE_QUICK_RECONNECTS"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: "dial-edge-timeout",
|
||||
Usage: "Maximum wait time to set up a connection with the edge",
|
||||
@@ -1051,5 +1067,28 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
EnvVars: []string{"HOST_KEY_PATH"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "stdin-control",
|
||||
Usage: "Control the process using commands sent through stdin",
|
||||
EnvVars: []string{"STDIN-CONTROL"},
|
||||
Hidden: true,
|
||||
Value: false,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func stdinControl(reconnectCh chan struct{}) {
|
||||
for {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
command := scanner.Text()
|
||||
|
||||
switch command {
|
||||
case "reconnect":
|
||||
reconnectCh <- struct{}{}
|
||||
default:
|
||||
logger.Warn("Unknown command: ", command)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,7 +237,6 @@ func prepareTunnelConfig(
|
||||
err = validation.ValidateHTTPService(originURL, hostname, httpTransport)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("unable to connect to the origin")
|
||||
return nil, errors.Wrap(err, "unable to connect to the origin")
|
||||
}
|
||||
|
||||
toEdgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c)
|
||||
@@ -277,6 +276,7 @@ func prepareTunnelConfig(
|
||||
TransportLogger: transportLogger,
|
||||
UseDeclarativeTunnel: c.Bool("use-declarative-tunnels"),
|
||||
UseReconnectToken: c.Bool("use-reconnect-token"),
|
||||
UseQuickReconnects: c.Bool("use-quick-reconnects"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package connection
|
||||
|
||||
const (
|
||||
FeatureSerializedHeaders = "serialized_headers"
|
||||
)
|
||||
|
||||
var SupportedFeatures = []string{
|
||||
FeatureSerializedHeaders,
|
||||
}
|
||||
@@ -68,8 +68,8 @@ func NewNoResolve(addrs []*net.TCPAddr) *Regions {
|
||||
|
||||
// GetAnyAddress returns an arbitrary address from the larger region.
|
||||
func (rs *Regions) GetAnyAddress() *net.TCPAddr {
|
||||
if rs.region1.AvailableAddrs() > rs.region2.AvailableAddrs() {
|
||||
return rs.region1.GetAnyAddress()
|
||||
if addr := rs.region1.GetAnyAddress(); addr != nil {
|
||||
return addr
|
||||
}
|
||||
return rs.region2.GetAnyAddress()
|
||||
}
|
||||
|
||||
@@ -47,6 +47,24 @@ func TestGiveBack(t *testing.T) {
|
||||
edge.GiveBack(addr)
|
||||
assert.Equal(t, 4, edge.AvailableAddrs())
|
||||
}
|
||||
|
||||
func TestRPCAndProxyShareSingleEdgeIP(t *testing.T) {
|
||||
l := logrus.New()
|
||||
|
||||
// Make an edge with a single IP
|
||||
edge := MockEdge(l, []*net.TCPAddr{&addr0})
|
||||
tunnelConnID := 0
|
||||
|
||||
// Use the IP for a tunnel
|
||||
addrTunnel, err := edge.GetAddr(tunnelConnID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Ensure the IP can be used for RPC too
|
||||
addrRPC, err := edge.GetAddrForRPC()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, addrTunnel, addrRPC)
|
||||
}
|
||||
|
||||
func TestGetAddrForRPC(t *testing.T) {
|
||||
l := logrus.New()
|
||||
edge := MockEdge(l, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
|
||||
@@ -25,7 +25,7 @@ require (
|
||||
github.com/gliderlabs/ssh v0.0.0-20191009160644-63518b5243e0
|
||||
github.com/go-sql-driver/mysql v1.4.1
|
||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
|
||||
github.com/google/certificate-transparency-go v1.1.0
|
||||
github.com/google/certificate-transparency-go v1.1.0 // indirect
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/gorilla/websocket v1.4.0
|
||||
@@ -38,7 +38,7 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.10 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.11.0
|
||||
github.com/mholt/caddy v0.0.0-20180807230124-d3b731e9255b // indirect
|
||||
github.com/miekg/dns v1.1.8
|
||||
github.com/miekg/dns v1.1.27
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/opentracing/opentracing-go v1.1.0 // indirect
|
||||
github.com/philhofer/fwd v1.0.0 // indirect
|
||||
@@ -53,7 +53,7 @@ require (
|
||||
github.com/stretchr/testify v1.3.0
|
||||
github.com/tinylib/msgp v1.1.0 // indirect
|
||||
github.com/xo/dburl v0.0.0-20191005012637-293c3298d6c0
|
||||
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
|
||||
golang.org/x/net v0.0.0-20191007182048-72f939374954
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be
|
||||
|
||||
@@ -215,6 +215,8 @@ github.com/mholt/caddy v0.0.0-20180807230124-d3b731e9255b h1:/BbY4n99iMazlr2igip
|
||||
github.com/mholt/caddy v0.0.0-20180807230124-d3b731e9255b/go.mod h1:Wb1PlT4DAYSqOEd03MsqkdkXnTxA8v9pKjdpxbqM1kY=
|
||||
github.com/miekg/dns v1.1.8 h1:1QYRAKU3lN5cRfLCkPU08hwvLJFhvjP6MqNMmQz6ZVI=
|
||||
github.com/miekg/dns v1.1.8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM=
|
||||
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
@@ -319,12 +321,15 @@ golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc h1:c0o/qxkaO2LF5t6fQrT4b5hzyggAkLLlCUjqfRxd8Q4=
|
||||
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -340,6 +345,7 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191007182048-72f939374954 h1:JGZucVF/L/TotR719NbujzadOZ2AgnYlqphQGHDCKaU=
|
||||
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
|
||||
@@ -366,6 +372,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -390,13 +397,16 @@ golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBn
|
||||
golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190909030654-5b82db07426d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.6.0/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
|
||||
+17
-69
@@ -3,11 +3,12 @@ package h2mux
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Header struct {
|
||||
@@ -71,6 +72,9 @@ func H2RequestHeadersToH1Request(h2 []Header, h1 *http.Request) error {
|
||||
return fmt.Errorf("unparseable content length")
|
||||
}
|
||||
h1.ContentLength = contentLength
|
||||
case "connection", "upgrade":
|
||||
// for websocket header support
|
||||
h1.Header.Add(http.CanonicalHeaderKey(header.Name), header.Value)
|
||||
default:
|
||||
// Ignore any other header;
|
||||
// User headers will be read from `RequestUserHeadersField`
|
||||
@@ -103,11 +107,21 @@ func ParseUserHeaders(headerNameToParseFrom string, headers []Header) ([]Header,
|
||||
func IsControlHeader(headerName string) bool {
|
||||
headerName = strings.ToLower(headerName)
|
||||
|
||||
return strings.ToLower(headerName) == "content-length" ||
|
||||
return headerName == "content-length" ||
|
||||
headerName == "connection" || headerName == "upgrade" || // Websocket headers
|
||||
strings.HasPrefix(headerName, ":") ||
|
||||
strings.HasPrefix(headerName, "cf-")
|
||||
}
|
||||
|
||||
// IsWebsocketClientHeader returns true if the header name is required by the client to upgrade properly
|
||||
func IsWebsocketClientHeader(headerName string) bool {
|
||||
headerName = strings.ToLower(headerName)
|
||||
|
||||
return headerName == "sec-websocket-accept" ||
|
||||
headerName == "connection" ||
|
||||
headerName == "upgrade"
|
||||
}
|
||||
|
||||
func H1ResponseToH2ResponseHeaders(h1 *http.Response) (h2 []Header) {
|
||||
h2 = []Header{
|
||||
{Name: ":status", Value: strconv.Itoa(h1.StatusCode)},
|
||||
@@ -121,7 +135,7 @@ func H1ResponseToH2ResponseHeaders(h1 *http.Response) (h2 []Header) {
|
||||
|
||||
// Since these are http2 headers, they're required to be lowercase
|
||||
h2 = append(h2, Header{Name: strings.ToLower(header), Value: value})
|
||||
} else if !IsControlHeader(header) {
|
||||
} else if !IsControlHeader(header) || IsWebsocketClientHeader(header) {
|
||||
// User headers, on the other hand, must all be serialized so that
|
||||
// HTTP/2 header validation won't be applied to HTTP/1 header values
|
||||
if _, ok := userHeaders[header]; ok {
|
||||
@@ -139,72 +153,6 @@ func H1ResponseToH2ResponseHeaders(h1 *http.Response) (h2 []Header) {
|
||||
return h2
|
||||
}
|
||||
|
||||
// Obsolete version of H2RequestHeadersToH1Request
|
||||
func OldH2RequestHeadersToH1Request(h2 []Header, h1 *http.Request) error {
|
||||
for _, header := range h2 {
|
||||
switch header.Name {
|
||||
case ":method":
|
||||
h1.Method = header.Value
|
||||
case ":scheme":
|
||||
// noop - use the preexisting scheme from h1.URL
|
||||
case ":authority":
|
||||
// Otherwise the host header will be based on the origin URL
|
||||
h1.Host = header.Value
|
||||
case ":path":
|
||||
// We don't want to be an "opinionated" proxy, so ideally we would use :path as-is.
|
||||
// However, this HTTP/1 Request object belongs to the Go standard library,
|
||||
// whose URL package makes some opinionated decisions about the encoding of
|
||||
// URL characters: see the docs of https://godoc.org/net/url#URL,
|
||||
// in particular the EscapedPath method https://godoc.org/net/url#URL.EscapedPath,
|
||||
// which is always used when computing url.URL.String(), whether we'd like it or not.
|
||||
//
|
||||
// Well, not *always*. We could circumvent this by using url.URL.Opaque. But
|
||||
// that would present unusual difficulties when using an HTTP proxy: url.URL.Opaque
|
||||
// is treated differently when HTTP_PROXY is set!
|
||||
// See https://github.com/golang/go/issues/5684#issuecomment-66080888
|
||||
//
|
||||
// This means we are subject to the behavior of net/url's function `shouldEscape`
|
||||
// (as invoked with mode=encodePath): https://github.com/golang/go/blob/go1.12.7/src/net/url/url.go#L101
|
||||
|
||||
if header.Value == "*" {
|
||||
h1.URL.Path = "*"
|
||||
continue
|
||||
}
|
||||
// Due to the behavior of validation.ValidateUrl, h1.URL may
|
||||
// already have a partial value, with or without a trailing slash.
|
||||
base := h1.URL.String()
|
||||
base = strings.TrimRight(base, "/")
|
||||
// But we know :path begins with '/', because we handled '*' above - see RFC7540
|
||||
url, err := url.Parse(base + header.Value)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("invalid path '%v'", header.Value))
|
||||
}
|
||||
h1.URL = url
|
||||
case "content-length":
|
||||
contentLength, err := strconv.ParseInt(header.Value, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unparseable content length")
|
||||
}
|
||||
h1.ContentLength = contentLength
|
||||
default:
|
||||
h1.Header.Add(http.CanonicalHeaderKey(header.Name), header.Value)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Obsolete version of H1ResponseToH2ResponseHeaders
|
||||
func OldH1ResponseToH2ResponseHeaders(h1 *http.Response) (h2 []Header) {
|
||||
h2 = []Header{{Name: ":status", Value: fmt.Sprintf("%d", h1.StatusCode)}}
|
||||
for headerName, headerValues := range h1.Header {
|
||||
for _, headerValue := range headerValues {
|
||||
h2 = append(h2, Header{Name: strings.ToLower(headerName), Value: headerValue})
|
||||
}
|
||||
}
|
||||
|
||||
return h2
|
||||
}
|
||||
|
||||
// Serialize HTTP1.x headers by base64-encoding each header name and value,
|
||||
// and then joining them in the format of [key:value;]
|
||||
func SerializeHeaders(h1Headers http.Header) string {
|
||||
|
||||
@@ -610,3 +610,43 @@ func TestH1ResponseToH2ResponseHeaders(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, expectedUserHeaders, actualUserHeaders)
|
||||
}
|
||||
|
||||
// The purpose of this test is to check that our code and the http.Header
|
||||
// implementation don't throw validation errors about header size
|
||||
func TestHeaderSize(t *testing.T) {
|
||||
largeValue := randSeq(5 * 1024 * 1024) // 5Mb
|
||||
largeHeaders := http.Header{
|
||||
"User-header": {largeValue},
|
||||
}
|
||||
mockResponse := http.Response{
|
||||
StatusCode: 200,
|
||||
Header: largeHeaders,
|
||||
}
|
||||
|
||||
serializedHeaders := H1ResponseToH2ResponseHeaders(&mockResponse)
|
||||
request, err := http.NewRequest(http.MethodGet, "https://example.com/", nil)
|
||||
assert.NoError(t, err)
|
||||
for _, header := range serializedHeaders {
|
||||
request.Header.Set(header.Name, header.Value)
|
||||
}
|
||||
|
||||
for _, header := range serializedHeaders {
|
||||
if header.Name != ResponseUserHeadersField {
|
||||
continue
|
||||
}
|
||||
|
||||
deserializedHeaders, err := DeserializeHeaders(header.Value)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, largeValue, deserializedHeaders[0].Value)
|
||||
}
|
||||
}
|
||||
|
||||
func randSeq(n int) string {
|
||||
randomizer := rand.New(rand.NewSource(17))
|
||||
var letters = []rune(":;,+/=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = letters[randomizer.Intn(len(letters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
+41
-17
@@ -250,28 +250,52 @@ func (w *MuxWriter) encodeHeaders(headers []Header) ([]byte, error) {
|
||||
// writeHeaders writes a block of encoded headers, splitting it into multiple frames if necessary.
|
||||
func (w *MuxWriter) writeHeaders(streamID uint32, headers []Header) error {
|
||||
encodedHeaders, err := w.encodeHeaders(headers)
|
||||
if err != nil {
|
||||
if err != nil || len(encodedHeaders) == 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
blockSize := int(w.maxFrameSize)
|
||||
endHeaders := len(encodedHeaders) == 0
|
||||
for !endHeaders && err == nil {
|
||||
blockFragment := encodedHeaders
|
||||
if len(encodedHeaders) > blockSize {
|
||||
blockFragment = blockFragment[:blockSize]
|
||||
encodedHeaders = encodedHeaders[blockSize:]
|
||||
// Send CONTINUATION frame if the headers can't be fit into 1 frame
|
||||
err = w.f.WriteContinuation(streamID, endHeaders, blockFragment)
|
||||
} else {
|
||||
endHeaders = true
|
||||
err = w.f.WriteHeaders(http2.HeadersFrameParam{
|
||||
StreamID: streamID,
|
||||
EndHeaders: endHeaders,
|
||||
BlockFragment: blockFragment,
|
||||
})
|
||||
// CONTINUATION is unnecessary; the headers fit within the blockSize
|
||||
if len(encodedHeaders) < blockSize {
|
||||
return w.f.WriteHeaders(http2.HeadersFrameParam{
|
||||
StreamID: streamID,
|
||||
EndHeaders: true,
|
||||
BlockFragment: encodedHeaders,
|
||||
})
|
||||
}
|
||||
|
||||
choppedHeaders := chopEncodedHeaders(encodedHeaders, blockSize)
|
||||
// len(choppedHeaders) is at least 2
|
||||
if err := w.f.WriteHeaders(http2.HeadersFrameParam{StreamID: streamID, EndHeaders: false, BlockFragment: choppedHeaders[0]}); err != nil {
|
||||
return err
|
||||
}
|
||||
for i := 1; i < len(choppedHeaders)-1; i++ {
|
||||
if err := w.f.WriteContinuation(streamID, false, choppedHeaders[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
if err := w.f.WriteContinuation(streamID, true, choppedHeaders[len(choppedHeaders)-1]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Partition a slice of bytes into `len(slice) / blockSize` slices of length `blockSize`
|
||||
func chopEncodedHeaders(headers []byte, chunkSize int) [][]byte {
|
||||
var divided [][]byte
|
||||
|
||||
for i := 0; i < len(headers); i += chunkSize {
|
||||
end := i + chunkSize
|
||||
|
||||
if end > len(headers) {
|
||||
end = len(headers)
|
||||
}
|
||||
|
||||
divided = append(divided, headers[i:end])
|
||||
}
|
||||
|
||||
return divided
|
||||
}
|
||||
|
||||
func (w *MuxWriter) writeUseDictionary(dictRequest useDictRequest) error {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package h2mux
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestChopEncodedHeaders(t *testing.T) {
|
||||
mockEncodedHeaders := make([]byte, 5)
|
||||
for i := range mockEncodedHeaders {
|
||||
mockEncodedHeaders[i] = byte(i)
|
||||
}
|
||||
chopped := chopEncodedHeaders(mockEncodedHeaders, 4)
|
||||
|
||||
assert.Equal(t, 2, len(chopped))
|
||||
assert.Equal(t, []byte{0, 1, 2, 3}, chopped[0])
|
||||
assert.Equal(t, []byte{4}, chopped[1])
|
||||
}
|
||||
|
||||
func TestChopEncodedEmptyHeaders(t *testing.T) {
|
||||
mockEncodedHeaders := make([]byte, 0)
|
||||
chopped := chopEncodedHeaders(mockEncodedHeaders, 3)
|
||||
|
||||
assert.Equal(t, 0, len(chopped))
|
||||
}
|
||||
@@ -80,7 +80,7 @@ ghost.init({
|
||||
<link rel="alternate" type="application/rss+xml" title="Cloudflare Blog" href="https://blog.cloudflare.com/rss/" />
|
||||
<meta name="msvalidate.01" content="CF295E1604697F9CAD18B5A232E871F6" />
|
||||
<meta class="swiftype" name="language" data-type="string" content="en" />
|
||||
<script src="https://s3-us-west-1.amazonaws.com/cf-ghost-assets-hotfix/js/index.js"></script>
|
||||
<script src="https://blog.cloudflare.com/assets/js/index.js"></script>
|
||||
<script type="text/javascript" src="//cdn.bizible.com/scripts/bizible.js" async=""></script>
|
||||
<script>
|
||||
var trackRecruitingLink = function(role, url) {
|
||||
@@ -128,7 +128,7 @@ HTMLAttrToAdd.setAttribute("lang", "en");
|
||||
overflow-y: scroll;
|
||||
}
|
||||
</style>
|
||||
<link href="https://s3-us-west-1.amazonaws.com/cf-ghost-assets-hotfix/css/screen.css" rel="stylesheet">
|
||||
<link href="https://blog.cloudflare.com/assets/css/screen.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.8.1/themes/prism.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
|
||||
@@ -113,7 +113,7 @@ ghost.init({
|
||||
<link rel="alternate" type="application/rss+xml" title="Cloudflare Blog" href="https://blog.cloudflare.com/rss/" />
|
||||
<meta name="msvalidate.01" content="CF295E1604697F9CAD18B5A232E871F6" />
|
||||
<meta class="swiftype" name="language" data-type="string" content="en" />
|
||||
<script src="https://s3-us-west-1.amazonaws.com/cf-ghost-assets-hotfix/js/index.js"></script>
|
||||
<script src="https://blog.cloudflare.com/assets/js/index.js"></script>
|
||||
<script type="text/javascript" src="//cdn.bizible.com/scripts/bizible.js" async=""></script>
|
||||
<script>
|
||||
var trackRecruitingLink = function(role, url) {
|
||||
@@ -161,7 +161,7 @@ HTMLAttrToAdd.setAttribute("lang", "en");
|
||||
overflow-y: scroll;
|
||||
}
|
||||
</style>
|
||||
<link href="https://s3-us-west-1.amazonaws.com/cf-ghost-assets-hotfix/css/screen.css" rel="stylesheet">
|
||||
<link href="https://blog.cloudflare.com/assets/css/screen.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.8.1/themes/prism.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
|
||||
@@ -109,7 +109,7 @@ ghost.init({
|
||||
<link rel="alternate" type="application/rss+xml" title="Cloudflare Blog" href="https://blog.cloudflare.com/rss/" />
|
||||
<meta name="msvalidate.01" content="CF295E1604697F9CAD18B5A232E871F6" />
|
||||
<meta class="swiftype" name="language" data-type="string" content="en" />
|
||||
<script src="https://s3-us-west-1.amazonaws.com/cf-ghost-assets-hotfix/js/index.js"></script>
|
||||
<script src="https://blog.cloudflare.com/assets/js/index.js"></script>
|
||||
<script type="text/javascript" src="//cdn.bizible.com/scripts/bizible.js" async=""></script>
|
||||
<script>
|
||||
var trackRecruitingLink = function(role, url) {
|
||||
@@ -157,7 +157,7 @@ HTMLAttrToAdd.setAttribute("lang", "en");
|
||||
overflow-y: scroll;
|
||||
}
|
||||
</style>
|
||||
<link href="https://s3-us-west-1.amazonaws.com/cf-ghost-assets-hotfix/css/screen.css" rel="stylesheet">
|
||||
<link href="https://blog.cloudflare.com/assets/css/screen.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.8.1/themes/prism.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ const indexTemplate = `
|
||||
<path class="st2" d="M93.6 12.8h-.5c-.1 0-.2.1-.3.2l-.7 2.4c-.3 1-.2 2 .3 2.6.5.6 1.2 1 2.1 1.1l3.7.2c.1 0 .2.1.3.1.1.1.1.2 0 .3-.1.2-.2.3-.4.3l-3.8.2c-2.1.1-4.3 1.8-5.1 3.8l-.2.9c-.1.1 0 .3.2.3h13.2c.2 0 .3-.1.3-.3.2-.8.4-1.7.4-2.6 0-5.2-4.3-9.5-9.5-9.5"/>
|
||||
<path class="st3" d="M104.4 30.8c-.5 0-.9-.4-.9-.9s.4-.9.9-.9.9.4.9.9-.4.9-.9.9m0-1.6c-.4 0-.7.3-.7.7 0 .4.3.7.7.7.4 0 .7-.3.7-.7 0-.4-.3-.7-.7-.7m.4 1.2h-.2l-.2-.3h-.2v.3h-.2v-.9h.5c.2 0 .3.1.3.3 0 .1-.1.2-.2.3l.2.3zm-.3-.5c.1 0 .1 0 .1-.1s-.1-.1-.1-.1h-.3v.3h.3zM14.8 29H17v6h3.8v1.9h-6zM23.1 32.9c0-2.3 1.8-4.1 4.3-4.1s4.2 1.8 4.2 4.1-1.8 4.1-4.3 4.1c-2.4 0-4.2-1.8-4.2-4.1m6.3 0c0-1.2-.8-2.2-2-2.2s-2 1-2 2.1.8 2.1 2 2.1c1.2.2 2-.8 2-2M34.3 33.4V29h2.2v4.4c0 1.1.6 1.7 1.5 1.7s1.5-.5 1.5-1.6V29h2.2v4.4c0 2.6-1.5 3.7-3.7 3.7-2.3-.1-3.7-1.2-3.7-3.7M45 29h3.1c2.8 0 4.5 1.6 4.5 3.9s-1.7 4-4.5 4h-3V29zm3.1 5.9c1.3 0 2.2-.7 2.2-2s-.9-2-2.2-2h-.9v4h.9zM55.7 29H62v1.9h-4.1v1.3h3.7V34h-3.7v2.9h-2.2zM65.1 29h2.2v6h3.8v1.9h-6zM76.8 28.9H79l3.4 8H80l-.6-1.4h-3.1l-.6 1.4h-2.3l3.4-8zm2 4.9l-.9-2.2-.9 2.2h1.8zM85.2 29h3.7c1.2 0 2 .3 2.6.9.5.5.7 1.1.7 1.8 0 1.2-.6 2-1.6 2.4l1.9 2.8H90l-1.6-2.4h-1v2.4h-2.2V29zm3.6 3.8c.7 0 1.2-.4 1.2-.9 0-.6-.5-.9-1.2-.9h-1.4v1.9h1.4zM95.3 29h6.4v1.8h-4.2V32h3.8v1.8h-3.8V35h4.3v1.9h-6.5zM10 33.9c-.3.7-1 1.2-1.8 1.2-1.2 0-2-1-2-2.1s.8-2.1 2-2.1c.9 0 1.6.6 1.9 1.3h2.3c-.4-1.9-2-3.3-4.2-3.3-2.4 0-4.3 1.8-4.3 4.1s1.8 4.1 4.2 4.1c2.1 0 3.7-1.4 4.2-3.2H10z"/>
|
||||
</svg>
|
||||
<h1 class="f4 f2-ns mt5 fw5">Congrats! You created your first tunnel!</h1>
|
||||
<h1 class="f4 f2-ns mt5 fw5">Congrats! You created a tunnel!</h1>
|
||||
<p class="f6 f5-m f4-l measure lh-copy fw3">
|
||||
Argo Tunnel exposes locally running applications to the internet by
|
||||
running an encrypted, virtual tunnel from your laptop or server to
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ func ServeMetrics(l net.Listener, shutdownC <-chan struct{}, logger *logrus.Logg
|
||||
defer wg.Done()
|
||||
err = server.Serve(l)
|
||||
}()
|
||||
logger.WithField("addr", l.Addr()).Info("Starting metrics server")
|
||||
logger.WithField("addr", fmt.Sprintf("%v/metrics", l.Addr())).Info("Starting metrics server")
|
||||
// server.Serve will hang if server.Shutdown is called before the server is
|
||||
// fully started up. So add artificial delay.
|
||||
time.Sleep(startupTime)
|
||||
|
||||
+33
-15
@@ -58,12 +58,15 @@ type Supervisor struct {
|
||||
|
||||
logger *logrus.Entry
|
||||
|
||||
jwtLock *sync.RWMutex
|
||||
jwtLock sync.RWMutex
|
||||
jwt []byte
|
||||
|
||||
eventDigestLock *sync.RWMutex
|
||||
eventDigestLock sync.RWMutex
|
||||
eventDigest []byte
|
||||
|
||||
connDigestLock sync.RWMutex
|
||||
connDigest map[uint8][]byte
|
||||
|
||||
bufferPool *buffer.Pool
|
||||
}
|
||||
|
||||
@@ -97,15 +100,14 @@ func NewSupervisor(config *TunnelConfig, u uuid.UUID) (*Supervisor, error) {
|
||||
tunnelErrors: make(chan tunnelError),
|
||||
tunnelsConnecting: map[int]chan struct{}{},
|
||||
logger: config.Logger.WithField("subsystem", "supervisor"),
|
||||
jwtLock: &sync.RWMutex{},
|
||||
eventDigestLock: &sync.RWMutex{},
|
||||
connDigest: make(map[uint8][]byte),
|
||||
bufferPool: buffer.NewPool(512 * 1024),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal) error {
|
||||
func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal, reconnectCh chan struct{}) error {
|
||||
logger := s.config.Logger
|
||||
if err := s.initialize(ctx, connectedSignal); err != nil {
|
||||
if err := s.initialize(ctx, connectedSignal, reconnectCh); err != nil {
|
||||
return err
|
||||
}
|
||||
var tunnelsWaiting []int
|
||||
@@ -155,7 +157,7 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal) er
|
||||
case <-backoffTimer:
|
||||
backoffTimer = nil
|
||||
for _, index := range tunnelsWaiting {
|
||||
go s.startTunnel(ctx, index, s.newConnectedTunnelSignal(index))
|
||||
go s.startTunnel(ctx, index, s.newConnectedTunnelSignal(index), reconnectCh)
|
||||
}
|
||||
tunnelsActive += len(tunnelsWaiting)
|
||||
tunnelsWaiting = nil
|
||||
@@ -189,7 +191,7 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal) er
|
||||
}
|
||||
|
||||
// Returns nil if initialization succeeded, else the initialization error.
|
||||
func (s *Supervisor) initialize(ctx context.Context, connectedSignal *signal.Signal) error {
|
||||
func (s *Supervisor) initialize(ctx context.Context, connectedSignal *signal.Signal, reconnectCh chan struct{}) error {
|
||||
logger := s.logger
|
||||
|
||||
s.lastResolve = time.Now()
|
||||
@@ -199,7 +201,7 @@ func (s *Supervisor) initialize(ctx context.Context, connectedSignal *signal.Sig
|
||||
s.config.HAConnections = availableAddrs
|
||||
}
|
||||
|
||||
go s.startFirstTunnel(ctx, connectedSignal)
|
||||
go s.startFirstTunnel(ctx, connectedSignal, reconnectCh)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
<-s.tunnelErrors
|
||||
@@ -211,7 +213,7 @@ func (s *Supervisor) initialize(ctx context.Context, connectedSignal *signal.Sig
|
||||
// At least one successful connection, so start the rest
|
||||
for i := 1; i < s.config.HAConnections; i++ {
|
||||
ch := signal.New(make(chan struct{}))
|
||||
go s.startTunnel(ctx, i, ch)
|
||||
go s.startTunnel(ctx, i, ch, reconnectCh)
|
||||
time.Sleep(registrationInterval)
|
||||
}
|
||||
return nil
|
||||
@@ -219,7 +221,7 @@ func (s *Supervisor) initialize(ctx context.Context, connectedSignal *signal.Sig
|
||||
|
||||
// startTunnel starts the first tunnel connection. The resulting error will be sent on
|
||||
// s.tunnelErrors. It will send a signal via connectedSignal if registration succeed
|
||||
func (s *Supervisor) startFirstTunnel(ctx context.Context, connectedSignal *signal.Signal) {
|
||||
func (s *Supervisor) startFirstTunnel(ctx context.Context, connectedSignal *signal.Signal, reconnectCh chan struct{}) {
|
||||
var (
|
||||
addr *net.TCPAddr
|
||||
err error
|
||||
@@ -234,7 +236,7 @@ func (s *Supervisor) startFirstTunnel(ctx context.Context, connectedSignal *sign
|
||||
return
|
||||
}
|
||||
|
||||
err = ServeTunnelLoop(ctx, s, s.config, addr, thisConnID, connectedSignal, s.cloudflaredUUID, s.bufferPool)
|
||||
err = ServeTunnelLoop(ctx, s, s.config, addr, thisConnID, connectedSignal, s.cloudflaredUUID, s.bufferPool, reconnectCh)
|
||||
// If the first tunnel disconnects, keep restarting it.
|
||||
edgeErrors := 0
|
||||
for s.unusedIPs() {
|
||||
@@ -257,13 +259,13 @@ func (s *Supervisor) startFirstTunnel(ctx context.Context, connectedSignal *sign
|
||||
return
|
||||
}
|
||||
}
|
||||
err = ServeTunnelLoop(ctx, s, s.config, addr, thisConnID, connectedSignal, s.cloudflaredUUID, s.bufferPool)
|
||||
err = ServeTunnelLoop(ctx, s, s.config, addr, thisConnID, connectedSignal, s.cloudflaredUUID, s.bufferPool, reconnectCh)
|
||||
}
|
||||
}
|
||||
|
||||
// startTunnel starts a new tunnel connection. The resulting error will be sent on
|
||||
// s.tunnelErrors.
|
||||
func (s *Supervisor) startTunnel(ctx context.Context, index int, connectedSignal *signal.Signal) {
|
||||
func (s *Supervisor) startTunnel(ctx context.Context, index int, connectedSignal *signal.Signal, reconnectCh chan struct{}) {
|
||||
var (
|
||||
addr *net.TCPAddr
|
||||
err error
|
||||
@@ -276,7 +278,7 @@ func (s *Supervisor) startTunnel(ctx context.Context, index int, connectedSignal
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = ServeTunnelLoop(ctx, s, s.config, addr, uint8(index), connectedSignal, s.cloudflaredUUID, s.bufferPool)
|
||||
err = ServeTunnelLoop(ctx, s, s.config, addr, uint8(index), connectedSignal, s.cloudflaredUUID, s.bufferPool, reconnectCh)
|
||||
}
|
||||
|
||||
func (s *Supervisor) newConnectedTunnelSignal(index int) *signal.Signal {
|
||||
@@ -332,6 +334,22 @@ func (s *Supervisor) SetEventDigest(eventDigest []byte) {
|
||||
s.eventDigest = eventDigest
|
||||
}
|
||||
|
||||
func (s *Supervisor) ConnDigest(connID uint8) ([]byte, error) {
|
||||
s.connDigestLock.RLock()
|
||||
defer s.connDigestLock.RUnlock()
|
||||
digest, ok := s.connDigest[connID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no connection digest for connection %v", connID)
|
||||
}
|
||||
return digest, nil
|
||||
}
|
||||
|
||||
func (s *Supervisor) SetConnDigest(connID uint8, connDigest []byte) {
|
||||
s.connDigestLock.Lock()
|
||||
defer s.connDigestLock.Unlock()
|
||||
s.connDigest[connID] = connDigest
|
||||
}
|
||||
|
||||
func (s *Supervisor) refreshAuth(
|
||||
ctx context.Context,
|
||||
backoff *BackoffHandler,
|
||||
|
||||
+46
-10
@@ -85,6 +85,8 @@ type TunnelConfig struct {
|
||||
|
||||
// feature-flag to use new edge reconnect tokens
|
||||
UseReconnectToken bool
|
||||
// feature-flag for using ConnectionDigest
|
||||
UseQuickReconnects bool
|
||||
}
|
||||
|
||||
// ReconnectTunnelCredentialManager is invoked by functions in this file to
|
||||
@@ -93,6 +95,8 @@ type ReconnectTunnelCredentialManager interface {
|
||||
ReconnectToken() ([]byte, error)
|
||||
EventDigest() ([]byte, error)
|
||||
SetEventDigest(eventDigest []byte)
|
||||
ConnDigest(connID uint8) ([]byte, error)
|
||||
SetConnDigest(connID uint8, connDigest []byte)
|
||||
}
|
||||
|
||||
type dupConnRegisterTunnelError struct{}
|
||||
@@ -161,15 +165,16 @@ func (c *TunnelConfig) RegistrationOptions(connectionID uint8, OriginLocalIP str
|
||||
RunFromTerminal: c.RunFromTerminal,
|
||||
CompressionQuality: c.CompressionQuality,
|
||||
UUID: uuid.String(),
|
||||
Features: connection.SupportedFeatures,
|
||||
}
|
||||
}
|
||||
|
||||
func StartTunnelDaemon(ctx context.Context, config *TunnelConfig, connectedSignal *signal.Signal, cloudflaredID uuid.UUID) error {
|
||||
func StartTunnelDaemon(ctx context.Context, config *TunnelConfig, connectedSignal *signal.Signal, cloudflaredID uuid.UUID, reconnectCh chan struct{}) error {
|
||||
s, err := NewSupervisor(config, cloudflaredID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.Run(ctx, connectedSignal)
|
||||
return s.Run(ctx, connectedSignal, reconnectCh)
|
||||
}
|
||||
|
||||
func ServeTunnelLoop(ctx context.Context,
|
||||
@@ -180,6 +185,7 @@ func ServeTunnelLoop(ctx context.Context,
|
||||
connectedSignal *signal.Signal,
|
||||
u uuid.UUID,
|
||||
bufferPool *buffer.Pool,
|
||||
reconnectCh chan struct{},
|
||||
) error {
|
||||
connectionLogger := config.Logger.WithField("connectionID", connectionID)
|
||||
config.Metrics.incrementHaConnections()
|
||||
@@ -204,6 +210,7 @@ func ServeTunnelLoop(ctx context.Context,
|
||||
&backoff,
|
||||
u,
|
||||
bufferPool,
|
||||
reconnectCh,
|
||||
)
|
||||
if recoverable {
|
||||
if duration, ok := backoff.GetBackoffDuration(ctx); ok {
|
||||
@@ -227,6 +234,7 @@ func ServeTunnel(
|
||||
backoff *BackoffHandler,
|
||||
u uuid.UUID,
|
||||
bufferPool *buffer.Pool,
|
||||
reconnectCh chan struct{},
|
||||
) (err error, recoverable bool) {
|
||||
// Treat panics as recoverable errors
|
||||
defer func() {
|
||||
@@ -277,7 +285,15 @@ func ServeTunnel(
|
||||
eventDigest, eventDigestErr := credentialManager.EventDigest()
|
||||
// if we have both credentials, we can reconnect
|
||||
if tokenErr == nil && eventDigestErr == nil {
|
||||
return ReconnectTunnel(serveCtx, token, eventDigest, handler.muxer, config, logger, connectionID, originLocalIP, u)
|
||||
var connDigest []byte
|
||||
|
||||
// check if we can use Quick Reconnects
|
||||
if config.UseQuickReconnects {
|
||||
if digest, connDigestErr := credentialManager.ConnDigest(connectionID); connDigestErr == nil {
|
||||
connDigest = digest
|
||||
}
|
||||
}
|
||||
return ReconnectTunnel(serveCtx, token, eventDigest, connDigest, handler.muxer, config, logger, connectionID, originLocalIP, u, credentialManager)
|
||||
}
|
||||
// log errors and proceed to RegisterTunnel
|
||||
if tokenErr != nil {
|
||||
@@ -305,6 +321,16 @@ func ServeTunnel(
|
||||
}
|
||||
})
|
||||
|
||||
errGroup.Go(func() error {
|
||||
select {
|
||||
case <-reconnectCh:
|
||||
return fmt.Errorf("received disconnect signal")
|
||||
case <-serveCtx.Done():
|
||||
return nil
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
errGroup.Go(func() error {
|
||||
// All routines should stop when muxer finish serving. When muxer is shutdown
|
||||
// gracefully, it doesn't return an error, so we need to return errMuxerShutdown
|
||||
@@ -379,19 +405,20 @@ func RegisterTunnel(
|
||||
return processRegisterTunnelError(registrationErr, config.Metrics, register)
|
||||
}
|
||||
credentialManager.SetEventDigest(registration.EventDigest)
|
||||
return processRegistrationSuccess(config, logger, connectionID, registration, register)
|
||||
return processRegistrationSuccess(config, logger, connectionID, registration, register, credentialManager)
|
||||
}
|
||||
|
||||
func ReconnectTunnel(
|
||||
ctx context.Context,
|
||||
token []byte,
|
||||
eventDigest []byte,
|
||||
eventDigest, connDigest []byte,
|
||||
muxer *h2mux.Muxer,
|
||||
config *TunnelConfig,
|
||||
logger *log.Entry,
|
||||
connectionID uint8,
|
||||
originLocalIP string,
|
||||
uuid uuid.UUID,
|
||||
credentialManager ReconnectTunnelCredentialManager,
|
||||
) error {
|
||||
config.TransportLogger.Debug("initiating RPC stream to reconnect")
|
||||
tunnelServer, err := connection.NewRPCClient(ctx, muxer, config.TransportLogger.WithField("subsystem", "rpc-reconnect"), openStreamTimeout)
|
||||
@@ -409,6 +436,7 @@ func ReconnectTunnel(
|
||||
ctx,
|
||||
token,
|
||||
eventDigest,
|
||||
connDigest,
|
||||
config.Hostname,
|
||||
config.RegistrationOptions(connectionID, originLocalIP, uuid),
|
||||
)
|
||||
@@ -416,10 +444,17 @@ func ReconnectTunnel(
|
||||
// ReconnectTunnel RPC failure
|
||||
return processRegisterTunnelError(registrationErr, config.Metrics, reconnect)
|
||||
}
|
||||
return processRegistrationSuccess(config, logger, connectionID, registration, reconnect)
|
||||
return processRegistrationSuccess(config, logger, connectionID, registration, reconnect, credentialManager)
|
||||
}
|
||||
|
||||
func processRegistrationSuccess(config *TunnelConfig, logger *log.Entry, connectionID uint8, registration *tunnelpogs.TunnelRegistration, name registerRPCName) error {
|
||||
func processRegistrationSuccess(
|
||||
config *TunnelConfig,
|
||||
logger *log.Entry,
|
||||
connectionID uint8,
|
||||
registration *tunnelpogs.TunnelRegistration,
|
||||
name registerRPCName,
|
||||
credentialManager ReconnectTunnelCredentialManager,
|
||||
) error {
|
||||
for _, logLine := range registration.LogLines {
|
||||
logger.Info(logLine)
|
||||
}
|
||||
@@ -441,6 +476,7 @@ func processRegistrationSuccess(config *TunnelConfig, logger *log.Entry, connect
|
||||
}
|
||||
}
|
||||
|
||||
credentialManager.SetConnDigest(connectionID, registration.ConnDigest)
|
||||
config.Metrics.userHostnamesCounts.WithLabelValues(registration.Url).Inc()
|
||||
|
||||
logger.Infof("Route propagating, it may take up to 1 minute for your new route to become functional")
|
||||
@@ -588,7 +624,7 @@ func (h *TunnelHandler) createRequest(stream *h2mux.MuxedStream) (*http.Request,
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unexpected error from http.NewRequest")
|
||||
}
|
||||
err = h2mux.OldH2RequestHeadersToH1Request(stream.Headers, req)
|
||||
err = h2mux.H2RequestHeadersToH1Request(stream.Headers, req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "invalid request received")
|
||||
}
|
||||
@@ -607,7 +643,7 @@ func (h *TunnelHandler) serveWebsocket(stream *h2mux.MuxedStream, req *http.Requ
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
err = stream.WriteHeaders(h2mux.OldH1ResponseToH2ResponseHeaders(response))
|
||||
err = stream.WriteHeaders(h2mux.H1ResponseToH2ResponseHeaders(response))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error writing response header")
|
||||
}
|
||||
@@ -641,7 +677,7 @@ func (h *TunnelHandler) serveHTTP(stream *h2mux.MuxedStream, req *http.Request)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
err = stream.WriteHeaders(h2mux.OldH1ResponseToH2ResponseHeaders(response))
|
||||
err = stream.WriteHeaders(h2mux.H1ResponseToH2ResponseHeaders(response))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error writing response header")
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func createRequest(stream *h2mux.MuxedStream, url *url.URL) (*http.Request, erro
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unexpected error from http.NewRequest")
|
||||
}
|
||||
err = h2mux.OldH2RequestHeadersToH1Request(stream.Headers, req)
|
||||
err = h2mux.H2RequestHeadersToH1Request(stream.Headers, req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "invalid request received")
|
||||
}
|
||||
|
||||
@@ -31,11 +31,11 @@ var (
|
||||
{Name: ":scheme", Value: "http"},
|
||||
{Name: ":authority", Value: "example.com"},
|
||||
{Name: ":path", Value: "/"},
|
||||
|
||||
// Regular headers must always come after the pseudoheaders
|
||||
{Name: h2mux.RequestUserHeadersField, Value: ""},
|
||||
}
|
||||
tunnelHostnameHeader = h2mux.Header{
|
||||
Name: h2mux.CloudflaredProxyTunnelHostnameHeader,
|
||||
Value: testTunnelHostname.String(),
|
||||
}
|
||||
tunnelHostnameHeader = h2mux.Header{Name: h2mux.CloudflaredProxyTunnelHostnameHeader, Value: testTunnelHostname.String()}
|
||||
)
|
||||
|
||||
func TestServeRequest(t *testing.T) {
|
||||
@@ -69,29 +69,73 @@ func TestServeRequest(t *testing.T) {
|
||||
assertRespBody(t, message, stream)
|
||||
}
|
||||
|
||||
func TestServeBadRequest(t *testing.T) {
|
||||
func createStreamHandler() *StreamHandler {
|
||||
configChan := make(chan *pogs.ClientConfig)
|
||||
useConfigResultChan := make(chan *pogs.UseConfigurationResult)
|
||||
streamHandler := NewStreamHandler(configChan, useConfigResultChan, logrus.New())
|
||||
|
||||
return NewStreamHandler(configChan, useConfigResultChan, logrus.New())
|
||||
}
|
||||
|
||||
func createRequestMuxPair(t *testing.T, streamHandler *StreamHandler) *DefaultMuxerPair {
|
||||
muxPair := NewDefaultMuxerPair(t, streamHandler)
|
||||
muxPair.Serve(t)
|
||||
|
||||
return muxPair
|
||||
}
|
||||
|
||||
func TestServeStatusBadRequest(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testOpenStreamTimeout)
|
||||
defer cancel()
|
||||
|
||||
// No tunnel hostname header, expect to get 400 Bad Request
|
||||
stream, err := muxPair.EdgeMux.OpenStream(ctx, baseHeaders, nil)
|
||||
stream, err := createRequestMuxPair(t, createStreamHandler()).EdgeMux.OpenStream(ctx, baseHeaders, nil)
|
||||
assert.NoError(t, err)
|
||||
assertStatusHeader(t, http.StatusBadRequest, stream.Headers)
|
||||
assertRespBody(t, statusBadRequest.text, stream)
|
||||
}
|
||||
|
||||
func TestServeInvalidContentLength(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testOpenStreamTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Invalid content-length, wouldn't be able to create a request
|
||||
// Expect to get 400 Bad Request
|
||||
headers := append(baseHeaders, tunnelHostnameHeader)
|
||||
headers = append(headers, h2mux.Header{
|
||||
Name: "content-length",
|
||||
Value: "x",
|
||||
})
|
||||
streamHandler := createStreamHandler()
|
||||
streamHandler.UpdateConfig([]*pogs.ReverseProxyConfig{
|
||||
{
|
||||
TunnelHostname: testTunnelHostname,
|
||||
OriginConfig: &pogs.HTTPOriginConfig{
|
||||
URLString: "",
|
||||
},
|
||||
},
|
||||
})
|
||||
mux := createRequestMuxPair(t, streamHandler).EdgeMux
|
||||
stream, err := mux.OpenStream(ctx, headers, nil)
|
||||
assert.NoError(t, err)
|
||||
assertStatusHeader(t, http.StatusBadRequest, stream.Headers)
|
||||
assertRespBody(t, statusBadRequest.text, stream)
|
||||
}
|
||||
|
||||
func TestServeStatusNotFound(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testOpenStreamTimeout)
|
||||
defer cancel()
|
||||
|
||||
// No mapping for the tunnel hostname, expect to get 404 Not Found
|
||||
headers := append(baseHeaders, tunnelHostnameHeader)
|
||||
stream, err = muxPair.EdgeMux.OpenStream(ctx, headers, nil)
|
||||
stream, err := createRequestMuxPair(t, createStreamHandler()).EdgeMux.OpenStream(ctx, headers, nil)
|
||||
assert.NoError(t, err)
|
||||
assertStatusHeader(t, http.StatusNotFound, stream.Headers)
|
||||
assertRespBody(t, statusNotFound.text, stream)
|
||||
}
|
||||
|
||||
func TestServeStatusBadGateway(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testOpenStreamTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Nothing listening on empty url, so proxy would fail. Expect to get 502 Bad Gateway
|
||||
reverseProxyConfigs := []*pogs.ReverseProxyConfig{
|
||||
@@ -102,21 +146,13 @@ func TestServeBadRequest(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
streamHandler := createStreamHandler()
|
||||
streamHandler.UpdateConfig(reverseProxyConfigs)
|
||||
stream, err = muxPair.EdgeMux.OpenStream(ctx, headers, nil)
|
||||
headers := append(baseHeaders, tunnelHostnameHeader)
|
||||
stream, err := createRequestMuxPair(t, streamHandler).EdgeMux.OpenStream(ctx, headers, nil)
|
||||
assert.NoError(t, err)
|
||||
assertStatusHeader(t, http.StatusBadGateway, stream.Headers)
|
||||
assertRespBody(t, statusBadGateway.text, stream)
|
||||
|
||||
// Invalid content-length, wouldn't not be able to create a request. Expect to get 400 Bad Request
|
||||
headers = append(headers, h2mux.Header{
|
||||
Name: "content-length",
|
||||
Value: "x",
|
||||
})
|
||||
stream, err = muxPair.EdgeMux.OpenStream(ctx, headers, nil)
|
||||
assert.NoError(t, err)
|
||||
assertStatusHeader(t, http.StatusBadRequest, stream.Headers)
|
||||
assertRespBody(t, statusBadRequest.text, stream)
|
||||
}
|
||||
|
||||
func assertStatusHeader(t *testing.T, expectedStatus int, headers []h2mux.Header) {
|
||||
@@ -218,6 +254,6 @@ type mockHTTPHandler struct {
|
||||
message []byte
|
||||
}
|
||||
|
||||
func (mth *mockHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(mth.message)
|
||||
func (mth *mockHTTPHandler) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write(mth.message)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ func (i TunnelServer_PogsImpl) ReconnectTunnel(p tunnelrpc.TunnelServer_reconnec
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
connDigest, err := p.Params.ConnDigest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hostname, err := p.Params.Hostname()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -29,7 +33,7 @@ func (i TunnelServer_PogsImpl) ReconnectTunnel(p tunnelrpc.TunnelServer_reconnec
|
||||
return err
|
||||
}
|
||||
server.Ack(p.Options)
|
||||
registration, err := i.impl.ReconnectTunnel(p.Ctx, jwt, eventDigest, hostname, pogsOptions)
|
||||
registration, err := i.impl.ReconnectTunnel(p.Ctx, jwt, eventDigest, connDigest, hostname, pogsOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -44,6 +48,7 @@ func (c TunnelServer_PogsClient) ReconnectTunnel(
|
||||
ctx context.Context,
|
||||
jwt,
|
||||
eventDigest []byte,
|
||||
connDigest []byte,
|
||||
hostname string,
|
||||
options *RegistrationOptions,
|
||||
) *TunnelRegistration {
|
||||
@@ -57,6 +62,10 @@ func (c TunnelServer_PogsClient) ReconnectTunnel(
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = p.SetConnDigest(connDigest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = p.SetHostname(hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -47,6 +47,7 @@ type SuccessfulTunnelRegistration struct {
|
||||
LogLines []string
|
||||
TunnelID string `capnp:"tunnelID"`
|
||||
EventDigest []byte
|
||||
ConnDigest []byte
|
||||
}
|
||||
|
||||
func NewSuccessfulTunnelRegistration(
|
||||
@@ -54,6 +55,7 @@ func NewSuccessfulTunnelRegistration(
|
||||
logLines []string,
|
||||
tunnelID string,
|
||||
eventDigest []byte,
|
||||
connDigest []byte,
|
||||
) *TunnelRegistration {
|
||||
// Marshal nil will result in an error
|
||||
if logLines == nil {
|
||||
@@ -65,6 +67,7 @@ func NewSuccessfulTunnelRegistration(
|
||||
LogLines: logLines,
|
||||
TunnelID: tunnelID,
|
||||
EventDigest: eventDigest,
|
||||
ConnDigest: connDigest,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -168,6 +171,7 @@ type RegistrationOptions struct {
|
||||
CompressionQuality uint64 `capnp:"compressionQuality"`
|
||||
UUID string `capnp:"uuid"`
|
||||
NumPreviousAttempts uint8
|
||||
Features []string
|
||||
}
|
||||
|
||||
func MarshalRegistrationOptions(s tunnelrpc.RegistrationOptions, p *RegistrationOptions) error {
|
||||
@@ -433,7 +437,7 @@ type TunnelServer interface {
|
||||
UnregisterTunnel(ctx context.Context, gracePeriodNanoSec int64) error
|
||||
Connect(ctx context.Context, parameters *ConnectParameters) (ConnectResult, error)
|
||||
Authenticate(ctx context.Context, originCert []byte, hostname string, options *RegistrationOptions) (*AuthenticateResponse, error)
|
||||
ReconnectTunnel(ctx context.Context, jwt, eventDigest []byte, hostname string, options *RegistrationOptions) (*TunnelRegistration, error)
|
||||
ReconnectTunnel(ctx context.Context, jwt, eventDigest, connDigest []byte, hostname string, options *RegistrationOptions) (*TunnelRegistration, error)
|
||||
}
|
||||
|
||||
func TunnelServer_ServerToClient(s TunnelServer) tunnelrpc.TunnelServer {
|
||||
|
||||
@@ -22,6 +22,7 @@ var (
|
||||
testErr = fmt.Errorf("Invalid credential")
|
||||
testLogLines = []string{"all", "working"}
|
||||
testEventDigest = []byte("asdf")
|
||||
testConnDigest = []byte("lkjh")
|
||||
)
|
||||
|
||||
// *PermanentRegistrationError implements TunnelRegistrationError
|
||||
@@ -32,8 +33,8 @@ var _ TunnelRegistrationError = (*RetryableRegistrationError)(nil)
|
||||
|
||||
func TestTunnelRegistration(t *testing.T) {
|
||||
testCases := []*TunnelRegistration{
|
||||
NewSuccessfulTunnelRegistration(testURL, testLogLines, testTunnelID, testEventDigest),
|
||||
NewSuccessfulTunnelRegistration(testURL, nil, testTunnelID, testEventDigest),
|
||||
NewSuccessfulTunnelRegistration(testURL, testLogLines, testTunnelID, testEventDigest, testConnDigest),
|
||||
NewSuccessfulTunnelRegistration(testURL, nil, testTunnelID, testEventDigest, testConnDigest),
|
||||
NewPermanentRegistrationError(testErr).Serialize(),
|
||||
NewRetryableRegistrationError(testErr, testRetryAfterSeconds).Serialize(),
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ struct TunnelRegistration {
|
||||
retryAfterSeconds @5 :UInt16;
|
||||
# A unique ID used to reconnect this tunnel.
|
||||
eventDigest @6 :Data;
|
||||
# A unique ID used to prove this tunnel was previously connected to a given metal.
|
||||
connDigest @7 :Data;
|
||||
}
|
||||
|
||||
struct RegistrationOptions {
|
||||
@@ -295,7 +297,7 @@ interface TunnelServer {
|
||||
unregisterTunnel @2 (gracePeriodNanoSec :Int64) -> ();
|
||||
connect @3 (parameters :CapnpConnectParameters) -> (result :ConnectResult);
|
||||
authenticate @4 (originCert :Data, hostname :Text, options :RegistrationOptions) -> (result :AuthenticateResponse);
|
||||
reconnectTunnel @5 (jwt :Data, eventDigest :Data, hostname :Text, options :RegistrationOptions) -> (result :TunnelRegistration);
|
||||
reconnectTunnel @5 (jwt :Data, eventDigest :Data, connDigest :Data, hostname :Text, options :RegistrationOptions) -> (result :TunnelRegistration);
|
||||
}
|
||||
|
||||
interface ClientService {
|
||||
|
||||
+492
-264
File diff suppressed because it is too large
Load Diff
+17
-4
@@ -56,7 +56,7 @@ Other supported formats are listed below.
|
||||
* `hostNameInCertificate` - Specifies the Common Name (CN) in the server certificate. Default value is the server host.
|
||||
* `ServerSPN` - The kerberos SPN (Service Principal Name) for the server. Default is MSSQLSvc/host:port.
|
||||
* `Workstation ID` - The workstation name (default is the host name)
|
||||
* `ApplicationIntent` - Can be given the value `ReadOnly` to initiate a read-only connection to an Availability Group listener.
|
||||
* `ApplicationIntent` - Can be given the value `ReadOnly` to initiate a read-only connection to an Availability Group listener. The `database` must be specified when connecting with `Application Intent` set to `ReadOnly`.
|
||||
|
||||
### The connection string can be specified in one of three formats:
|
||||
|
||||
@@ -187,6 +187,19 @@ _, err := db.ExecContext(ctx, "theproc", &rs)
|
||||
log.Printf("status=%d", rs)
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
var rs mssql.ReturnStatus
|
||||
_, err := db.QueryContext(ctx, "theproc", &rs)
|
||||
for rows.Next() {
|
||||
err = rows.Scan(&val)
|
||||
}
|
||||
log.Printf("status=%d", rs)
|
||||
```
|
||||
|
||||
Limitation: ReturnStatus cannot be retrieved using `QueryRow`.
|
||||
|
||||
## Parameters
|
||||
|
||||
The `sqlserver` driver uses normal MS SQL Server syntax and expects parameters in
|
||||
@@ -207,9 +220,9 @@ are supported:
|
||||
* time.Time -> datetimeoffset or datetime (TDS version dependent)
|
||||
* mssql.DateTime1 -> datetime
|
||||
* mssql.DateTimeOffset -> datetimeoffset
|
||||
* "cloud.google.com/go/civil".Date -> date
|
||||
* "cloud.google.com/go/civil".DateTime -> datetime2
|
||||
* "cloud.google.com/go/civil".Time -> time
|
||||
* "github.com/golang-sql/civil".Date -> date
|
||||
* "github.com/golang-sql/civil".DateTime -> datetime2
|
||||
* "github.com/golang-sql/civil".Time -> time
|
||||
* mssql.TVP -> Table Value Parameter (TDS version dependent)
|
||||
|
||||
## Important Notes
|
||||
|
||||
+4
-2
@@ -10,7 +10,7 @@ environment:
|
||||
SQLUSER: sa
|
||||
SQLPASSWORD: Password12!
|
||||
DATABASE: test
|
||||
GOVERSION: 110
|
||||
GOVERSION: 111
|
||||
matrix:
|
||||
- GOVERSION: 18
|
||||
SQLINSTANCE: SQL2016
|
||||
@@ -18,6 +18,8 @@ environment:
|
||||
SQLINSTANCE: SQL2016
|
||||
- GOVERSION: 110
|
||||
SQLINSTANCE: SQL2016
|
||||
- GOVERSION: 111
|
||||
SQLINSTANCE: SQL2016
|
||||
- SQLINSTANCE: SQL2014
|
||||
- SQLINSTANCE: SQL2012SP1
|
||||
- SQLINSTANCE: SQL2008R2SP2
|
||||
@@ -27,7 +29,7 @@ install:
|
||||
- set PATH=%GOPATH%\bin;%GOROOT%\bin;%PATH%
|
||||
- go version
|
||||
- go env
|
||||
- go get -u cloud.google.com/go/civil
|
||||
- go get -u github.com/golang-sql/civil
|
||||
|
||||
build_script:
|
||||
- go build
|
||||
|
||||
+16
-12
@@ -221,23 +221,27 @@ func (r *tdsBuffer) uint16() uint16 {
|
||||
}
|
||||
|
||||
func (r *tdsBuffer) BVarChar() string {
|
||||
l := int(r.byte())
|
||||
return r.readUcs2(l)
|
||||
return readBVarCharOrPanic(r)
|
||||
}
|
||||
|
||||
func (r *tdsBuffer) UsVarChar() string {
|
||||
l := int(r.uint16())
|
||||
return r.readUcs2(l)
|
||||
}
|
||||
|
||||
func (r *tdsBuffer) readUcs2(numchars int) string {
|
||||
b := make([]byte, numchars*2)
|
||||
r.ReadFull(b)
|
||||
res, err := ucs22str(b)
|
||||
func readBVarCharOrPanic(r io.Reader) string {
|
||||
s, err := readBVarChar(r)
|
||||
if err != nil {
|
||||
badStreamPanic(err)
|
||||
}
|
||||
return res
|
||||
return s
|
||||
}
|
||||
|
||||
func readUsVarCharOrPanic(r io.Reader) string {
|
||||
s, err := readUsVarChar(r)
|
||||
if err != nil {
|
||||
badStreamPanic(err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (r *tdsBuffer) UsVarChar() string {
|
||||
return readUsVarCharOrPanic(r)
|
||||
}
|
||||
|
||||
func (r *tdsBuffer) Read(buf []byte) (copied int, err error) {
|
||||
|
||||
+73
-44
@@ -7,9 +7,10 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/denisenkom/go-mssqldb/internal/decimal"
|
||||
)
|
||||
|
||||
type Bulk struct {
|
||||
@@ -42,6 +43,11 @@ type BulkOptions struct {
|
||||
|
||||
type DataValue interface{}
|
||||
|
||||
const (
|
||||
sqlDateFormat = "2006-01-02"
|
||||
sqlTimeFormat = "2006-01-02 15:04:05.999999999Z07:00"
|
||||
)
|
||||
|
||||
func (cn *Conn) CreateBulk(table string, columns []string) (_ *Bulk) {
|
||||
b := Bulk{ctx: context.Background(), cn: cn, tablename: table, headerSent: false, columnsName: columns}
|
||||
b.Debug = false
|
||||
@@ -334,7 +340,7 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error)
|
||||
case int64:
|
||||
intvalue = val
|
||||
default:
|
||||
err = fmt.Errorf("mssql: invalid type for int column")
|
||||
err = fmt.Errorf("mssql: invalid type for int column: %T", val)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -361,7 +367,7 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error)
|
||||
case int64:
|
||||
floatvalue = float64(val)
|
||||
default:
|
||||
err = fmt.Errorf("mssql: invalid type for float column: %s", val)
|
||||
err = fmt.Errorf("mssql: invalid type for float column: %T %s", val, val)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -380,7 +386,7 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error)
|
||||
case []byte:
|
||||
res.buffer = val
|
||||
default:
|
||||
err = fmt.Errorf("mssql: invalid type for nvarchar column: %s", val)
|
||||
err = fmt.Errorf("mssql: invalid type for nvarchar column: %T %s", val, val)
|
||||
return
|
||||
}
|
||||
res.ti.Size = len(res.buffer)
|
||||
@@ -392,14 +398,14 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error)
|
||||
case []byte:
|
||||
res.buffer = val
|
||||
default:
|
||||
err = fmt.Errorf("mssql: invalid type for varchar column: %s", val)
|
||||
err = fmt.Errorf("mssql: invalid type for varchar column: %T %s", val, val)
|
||||
return
|
||||
}
|
||||
res.ti.Size = len(res.buffer)
|
||||
|
||||
case typeBit, typeBitN:
|
||||
if reflect.TypeOf(val).Kind() != reflect.Bool {
|
||||
err = fmt.Errorf("mssql: invalid type for bit column: %s", val)
|
||||
err = fmt.Errorf("mssql: invalid type for bit column: %T %s", val, val)
|
||||
return
|
||||
}
|
||||
res.ti.TypeId = typeBitN
|
||||
@@ -413,18 +419,31 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error)
|
||||
case time.Time:
|
||||
res.buffer = encodeDateTime2(val, int(col.ti.Scale))
|
||||
res.ti.Size = len(res.buffer)
|
||||
case string:
|
||||
var t time.Time
|
||||
if t, err = time.Parse(sqlTimeFormat, val); err != nil {
|
||||
return res, fmt.Errorf("bulk: unable to convert string to date: %v", err)
|
||||
}
|
||||
res.buffer = encodeDateTime2(t, int(col.ti.Scale))
|
||||
res.ti.Size = len(res.buffer)
|
||||
default:
|
||||
err = fmt.Errorf("mssql: invalid type for datetime2 column: %s", val)
|
||||
err = fmt.Errorf("mssql: invalid type for datetime2 column: %T %s", val, val)
|
||||
return
|
||||
}
|
||||
case typeDateTimeOffsetN:
|
||||
switch val := val.(type) {
|
||||
case time.Time:
|
||||
res.buffer = encodeDateTimeOffset(val, int(res.ti.Scale))
|
||||
res.buffer = encodeDateTimeOffset(val, int(col.ti.Scale))
|
||||
res.ti.Size = len(res.buffer)
|
||||
case string:
|
||||
var t time.Time
|
||||
if t, err = time.Parse(sqlTimeFormat, val); err != nil {
|
||||
return res, fmt.Errorf("bulk: unable to convert string to date: %v", err)
|
||||
}
|
||||
res.buffer = encodeDateTimeOffset(t, int(col.ti.Scale))
|
||||
res.ti.Size = len(res.buffer)
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("mssql: invalid type for datetimeoffset column: %s", val)
|
||||
err = fmt.Errorf("mssql: invalid type for datetimeoffset column: %T %s", val, val)
|
||||
return
|
||||
}
|
||||
case typeDateN:
|
||||
@@ -432,69 +451,79 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error)
|
||||
case time.Time:
|
||||
res.buffer = encodeDate(val)
|
||||
res.ti.Size = len(res.buffer)
|
||||
case string:
|
||||
var t time.Time
|
||||
if t, err = time.ParseInLocation(sqlDateFormat, val, time.UTC); err != nil {
|
||||
return res, fmt.Errorf("bulk: unable to convert string to date: %v", err)
|
||||
}
|
||||
res.buffer = encodeDate(t)
|
||||
res.ti.Size = len(res.buffer)
|
||||
default:
|
||||
err = fmt.Errorf("mssql: invalid type for date column: %s", val)
|
||||
err = fmt.Errorf("mssql: invalid type for date column: %T %s", val, val)
|
||||
return
|
||||
}
|
||||
case typeDateTime, typeDateTimeN, typeDateTim4:
|
||||
var t time.Time
|
||||
switch val := val.(type) {
|
||||
case time.Time:
|
||||
if col.ti.Size == 4 {
|
||||
res.buffer = encodeDateTim4(val)
|
||||
res.ti.Size = len(res.buffer)
|
||||
} else if col.ti.Size == 8 {
|
||||
res.buffer = encodeDateTime(val)
|
||||
res.ti.Size = len(res.buffer)
|
||||
} else {
|
||||
err = fmt.Errorf("mssql: invalid size of column")
|
||||
t = val
|
||||
case string:
|
||||
if t, err = time.Parse(sqlTimeFormat, val); err != nil {
|
||||
return res, fmt.Errorf("bulk: unable to convert string to date: %v", err)
|
||||
}
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("mssql: invalid type for datetime column: %s", val)
|
||||
err = fmt.Errorf("mssql: invalid type for datetime column: %T %s", val, val)
|
||||
return
|
||||
}
|
||||
|
||||
if col.ti.Size == 4 {
|
||||
res.buffer = encodeDateTim4(t)
|
||||
res.ti.Size = len(res.buffer)
|
||||
} else if col.ti.Size == 8 {
|
||||
res.buffer = encodeDateTime(t)
|
||||
res.ti.Size = len(res.buffer)
|
||||
} else {
|
||||
err = fmt.Errorf("mssql: invalid size of column %d", col.ti.Size)
|
||||
}
|
||||
|
||||
// case typeMoney, typeMoney4, typeMoneyN:
|
||||
case typeDecimal, typeDecimalN, typeNumeric, typeNumericN:
|
||||
var value float64
|
||||
prec := col.ti.Prec
|
||||
scale := col.ti.Scale
|
||||
var dec decimal.Decimal
|
||||
switch v := val.(type) {
|
||||
case int:
|
||||
value = float64(v)
|
||||
dec = decimal.Int64ToDecimalScale(int64(v), 0)
|
||||
case int8:
|
||||
value = float64(v)
|
||||
dec = decimal.Int64ToDecimalScale(int64(v), 0)
|
||||
case int16:
|
||||
value = float64(v)
|
||||
dec = decimal.Int64ToDecimalScale(int64(v), 0)
|
||||
case int32:
|
||||
value = float64(v)
|
||||
dec = decimal.Int64ToDecimalScale(int64(v), 0)
|
||||
case int64:
|
||||
value = float64(v)
|
||||
dec = decimal.Int64ToDecimalScale(int64(v), 0)
|
||||
case float32:
|
||||
value = float64(v)
|
||||
dec, err = decimal.Float64ToDecimalScale(float64(v), scale)
|
||||
case float64:
|
||||
value = v
|
||||
dec, err = decimal.Float64ToDecimalScale(float64(v), scale)
|
||||
case string:
|
||||
if value, err = strconv.ParseFloat(v, 64); err != nil {
|
||||
return res, fmt.Errorf("bulk: unable to convert string to float: %v", err)
|
||||
}
|
||||
dec, err = decimal.StringToDecimalScale(v, scale)
|
||||
default:
|
||||
return res, fmt.Errorf("unknown value for decimal: %#v", v)
|
||||
return res, fmt.Errorf("unknown value for decimal: %T %#v", v, v)
|
||||
}
|
||||
|
||||
perc := col.ti.Prec
|
||||
scale := col.ti.Scale
|
||||
var dec Decimal
|
||||
dec, err = Float64ToDecimalScale(value, scale)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
dec.prec = perc
|
||||
dec.SetPrec(prec)
|
||||
|
||||
var length byte
|
||||
switch {
|
||||
case perc <= 9:
|
||||
case prec <= 9:
|
||||
length = 4
|
||||
case perc <= 19:
|
||||
case prec <= 19:
|
||||
length = 8
|
||||
case perc <= 28:
|
||||
case prec <= 28:
|
||||
length = 12
|
||||
default:
|
||||
length = 16
|
||||
@@ -504,7 +533,7 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error)
|
||||
// first byte length written by typeInfo.writer
|
||||
res.ti.Size = int(length) + 1
|
||||
// second byte sign
|
||||
if value < 0 {
|
||||
if !dec.IsPositive() {
|
||||
buf[0] = 0
|
||||
} else {
|
||||
buf[0] = 1
|
||||
@@ -527,7 +556,7 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error)
|
||||
res.ti.Size = len(val)
|
||||
res.buffer = val
|
||||
default:
|
||||
err = fmt.Errorf("mssql: invalid type for Binary column: %s", val)
|
||||
err = fmt.Errorf("mssql: invalid type for Binary column: %T %s", val, val)
|
||||
return
|
||||
}
|
||||
case typeGuid:
|
||||
@@ -536,7 +565,7 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error)
|
||||
res.ti.Size = len(val)
|
||||
res.buffer = val
|
||||
default:
|
||||
err = fmt.Errorf("mssql: invalid type for Guid column: %s", val)
|
||||
err = fmt.Errorf("mssql: invalid type for Guid column: %T %s", val, val)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+456
@@ -0,0 +1,456 @@
|
||||
package mssql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type connectParams struct {
|
||||
logFlags uint64
|
||||
port uint64
|
||||
host string
|
||||
instance string
|
||||
database string
|
||||
user string
|
||||
password string
|
||||
dial_timeout time.Duration
|
||||
conn_timeout time.Duration
|
||||
keepAlive time.Duration
|
||||
encrypt bool
|
||||
disableEncryption bool
|
||||
trustServerCertificate bool
|
||||
certificate string
|
||||
hostInCertificate string
|
||||
hostInCertificateProvided bool
|
||||
serverSPN string
|
||||
workstation string
|
||||
appname string
|
||||
typeFlags uint8
|
||||
failOverPartner string
|
||||
failOverPort uint64
|
||||
packetSize uint16
|
||||
}
|
||||
|
||||
func parseConnectParams(dsn string) (connectParams, error) {
|
||||
var p connectParams
|
||||
|
||||
var params map[string]string
|
||||
if strings.HasPrefix(dsn, "odbc:") {
|
||||
parameters, err := splitConnectionStringOdbc(dsn[len("odbc:"):])
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
params = parameters
|
||||
} else if strings.HasPrefix(dsn, "sqlserver://") {
|
||||
parameters, err := splitConnectionStringURL(dsn)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
params = parameters
|
||||
} else {
|
||||
params = splitConnectionString(dsn)
|
||||
}
|
||||
|
||||
strlog, ok := params["log"]
|
||||
if ok {
|
||||
var err error
|
||||
p.logFlags, err = strconv.ParseUint(strlog, 10, 64)
|
||||
if err != nil {
|
||||
return p, fmt.Errorf("Invalid log parameter '%s': %s", strlog, err.Error())
|
||||
}
|
||||
}
|
||||
server := params["server"]
|
||||
parts := strings.SplitN(server, `\`, 2)
|
||||
p.host = parts[0]
|
||||
if p.host == "." || strings.ToUpper(p.host) == "(LOCAL)" || p.host == "" {
|
||||
p.host = "localhost"
|
||||
}
|
||||
if len(parts) > 1 {
|
||||
p.instance = parts[1]
|
||||
}
|
||||
p.database = params["database"]
|
||||
p.user = params["user id"]
|
||||
p.password = params["password"]
|
||||
|
||||
p.port = 1433
|
||||
strport, ok := params["port"]
|
||||
if ok {
|
||||
var err error
|
||||
p.port, err = strconv.ParseUint(strport, 10, 16)
|
||||
if err != nil {
|
||||
f := "Invalid tcp port '%v': %v"
|
||||
return p, fmt.Errorf(f, strport, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/configure-the-network-packet-size-server-configuration-option
|
||||
// Default packet size remains at 4096 bytes
|
||||
p.packetSize = 4096
|
||||
strpsize, ok := params["packet size"]
|
||||
if ok {
|
||||
var err error
|
||||
psize, err := strconv.ParseUint(strpsize, 0, 16)
|
||||
if err != nil {
|
||||
f := "Invalid packet size '%v': %v"
|
||||
return p, fmt.Errorf(f, strpsize, err.Error())
|
||||
}
|
||||
|
||||
// Ensure packet size falls within the TDS protocol range of 512 to 32767 bytes
|
||||
// NOTE: Encrypted connections have a maximum size of 16383 bytes. If you request
|
||||
// a higher packet size, the server will respond with an ENVCHANGE request to
|
||||
// alter the packet size to 16383 bytes.
|
||||
p.packetSize = uint16(psize)
|
||||
if p.packetSize < 512 {
|
||||
p.packetSize = 512
|
||||
} else if p.packetSize > 32767 {
|
||||
p.packetSize = 32767
|
||||
}
|
||||
}
|
||||
|
||||
// https://msdn.microsoft.com/en-us/library/dd341108.aspx
|
||||
//
|
||||
// Do not set a connection timeout. Use Context to manage such things.
|
||||
// Default to zero, but still allow it to be set.
|
||||
if strconntimeout, ok := params["connection timeout"]; ok {
|
||||
timeout, err := strconv.ParseUint(strconntimeout, 10, 64)
|
||||
if err != nil {
|
||||
f := "Invalid connection timeout '%v': %v"
|
||||
return p, fmt.Errorf(f, strconntimeout, err.Error())
|
||||
}
|
||||
p.conn_timeout = time.Duration(timeout) * time.Second
|
||||
}
|
||||
p.dial_timeout = 15 * time.Second
|
||||
if strdialtimeout, ok := params["dial timeout"]; ok {
|
||||
timeout, err := strconv.ParseUint(strdialtimeout, 10, 64)
|
||||
if err != nil {
|
||||
f := "Invalid dial timeout '%v': %v"
|
||||
return p, fmt.Errorf(f, strdialtimeout, err.Error())
|
||||
}
|
||||
p.dial_timeout = time.Duration(timeout) * time.Second
|
||||
}
|
||||
|
||||
// default keep alive should be 30 seconds according to spec:
|
||||
// https://msdn.microsoft.com/en-us/library/dd341108.aspx
|
||||
p.keepAlive = 30 * time.Second
|
||||
if keepAlive, ok := params["keepalive"]; ok {
|
||||
timeout, err := strconv.ParseUint(keepAlive, 10, 64)
|
||||
if err != nil {
|
||||
f := "Invalid keepAlive value '%s': %s"
|
||||
return p, fmt.Errorf(f, keepAlive, err.Error())
|
||||
}
|
||||
p.keepAlive = time.Duration(timeout) * time.Second
|
||||
}
|
||||
encrypt, ok := params["encrypt"]
|
||||
if ok {
|
||||
if strings.EqualFold(encrypt, "DISABLE") {
|
||||
p.disableEncryption = true
|
||||
} else {
|
||||
var err error
|
||||
p.encrypt, err = strconv.ParseBool(encrypt)
|
||||
if err != nil {
|
||||
f := "Invalid encrypt '%s': %s"
|
||||
return p, fmt.Errorf(f, encrypt, err.Error())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
p.trustServerCertificate = true
|
||||
}
|
||||
trust, ok := params["trustservercertificate"]
|
||||
if ok {
|
||||
var err error
|
||||
p.trustServerCertificate, err = strconv.ParseBool(trust)
|
||||
if err != nil {
|
||||
f := "Invalid trust server certificate '%s': %s"
|
||||
return p, fmt.Errorf(f, trust, err.Error())
|
||||
}
|
||||
}
|
||||
p.certificate = params["certificate"]
|
||||
p.hostInCertificate, ok = params["hostnameincertificate"]
|
||||
if ok {
|
||||
p.hostInCertificateProvided = true
|
||||
} else {
|
||||
p.hostInCertificate = p.host
|
||||
p.hostInCertificateProvided = false
|
||||
}
|
||||
|
||||
serverSPN, ok := params["serverspn"]
|
||||
if ok {
|
||||
p.serverSPN = serverSPN
|
||||
} else {
|
||||
p.serverSPN = fmt.Sprintf("MSSQLSvc/%s:%d", p.host, p.port)
|
||||
}
|
||||
|
||||
workstation, ok := params["workstation id"]
|
||||
if ok {
|
||||
p.workstation = workstation
|
||||
} else {
|
||||
workstation, err := os.Hostname()
|
||||
if err == nil {
|
||||
p.workstation = workstation
|
||||
}
|
||||
}
|
||||
|
||||
appname, ok := params["app name"]
|
||||
if !ok {
|
||||
appname = "go-mssqldb"
|
||||
}
|
||||
p.appname = appname
|
||||
|
||||
appintent, ok := params["applicationintent"]
|
||||
if ok {
|
||||
if appintent == "ReadOnly" {
|
||||
if p.database == "" {
|
||||
return p, fmt.Errorf("Database must be specified when ApplicationIntent is ReadOnly")
|
||||
}
|
||||
p.typeFlags |= fReadOnlyIntent
|
||||
}
|
||||
}
|
||||
|
||||
failOverPartner, ok := params["failoverpartner"]
|
||||
if ok {
|
||||
p.failOverPartner = failOverPartner
|
||||
}
|
||||
|
||||
failOverPort, ok := params["failoverport"]
|
||||
if ok {
|
||||
var err error
|
||||
p.failOverPort, err = strconv.ParseUint(failOverPort, 0, 16)
|
||||
if err != nil {
|
||||
f := "Invalid tcp port '%v': %v"
|
||||
return p, fmt.Errorf(f, failOverPort, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func splitConnectionString(dsn string) (res map[string]string) {
|
||||
res = map[string]string{}
|
||||
parts := strings.Split(dsn, ";")
|
||||
for _, part := range parts {
|
||||
if len(part) == 0 {
|
||||
continue
|
||||
}
|
||||
lst := strings.SplitN(part, "=", 2)
|
||||
name := strings.TrimSpace(strings.ToLower(lst[0]))
|
||||
if len(name) == 0 {
|
||||
continue
|
||||
}
|
||||
var value string = ""
|
||||
if len(lst) > 1 {
|
||||
value = strings.TrimSpace(lst[1])
|
||||
}
|
||||
res[name] = value
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Splits a URL of the form sqlserver://username:password@host/instance?param1=value¶m2=value
|
||||
func splitConnectionStringURL(dsn string) (map[string]string, error) {
|
||||
res := map[string]string{}
|
||||
|
||||
u, err := url.Parse(dsn)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
if u.Scheme != "sqlserver" {
|
||||
return res, fmt.Errorf("scheme %s is not recognized", u.Scheme)
|
||||
}
|
||||
|
||||
if u.User != nil {
|
||||
res["user id"] = u.User.Username()
|
||||
p, exists := u.User.Password()
|
||||
if exists {
|
||||
res["password"] = p
|
||||
}
|
||||
}
|
||||
|
||||
host, port, err := net.SplitHostPort(u.Host)
|
||||
if err != nil {
|
||||
host = u.Host
|
||||
}
|
||||
|
||||
if len(u.Path) > 0 {
|
||||
res["server"] = host + "\\" + u.Path[1:]
|
||||
} else {
|
||||
res["server"] = host
|
||||
}
|
||||
|
||||
if len(port) > 0 {
|
||||
res["port"] = port
|
||||
}
|
||||
|
||||
query := u.Query()
|
||||
for k, v := range query {
|
||||
if len(v) > 1 {
|
||||
return res, fmt.Errorf("key %s provided more than once", k)
|
||||
}
|
||||
res[strings.ToLower(k)] = v[0]
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Splits a URL in the ODBC format
|
||||
func splitConnectionStringOdbc(dsn string) (map[string]string, error) {
|
||||
res := map[string]string{}
|
||||
|
||||
type parserState int
|
||||
const (
|
||||
// Before the start of a key
|
||||
parserStateBeforeKey parserState = iota
|
||||
|
||||
// Inside a key
|
||||
parserStateKey
|
||||
|
||||
// Beginning of a value. May be bare or braced
|
||||
parserStateBeginValue
|
||||
|
||||
// Inside a bare value
|
||||
parserStateBareValue
|
||||
|
||||
// Inside a braced value
|
||||
parserStateBracedValue
|
||||
|
||||
// A closing brace inside a braced value.
|
||||
// May be the end of the value or an escaped closing brace, depending on the next character
|
||||
parserStateBracedValueClosingBrace
|
||||
|
||||
// After a value. Next character should be a semicolon or whitespace.
|
||||
parserStateEndValue
|
||||
)
|
||||
|
||||
var state = parserStateBeforeKey
|
||||
|
||||
var key string
|
||||
var value string
|
||||
|
||||
for i, c := range dsn {
|
||||
switch state {
|
||||
case parserStateBeforeKey:
|
||||
switch {
|
||||
case c == '=':
|
||||
return res, fmt.Errorf("Unexpected character = at index %d. Expected start of key or semi-colon or whitespace.", i)
|
||||
case !unicode.IsSpace(c) && c != ';':
|
||||
state = parserStateKey
|
||||
key += string(c)
|
||||
}
|
||||
|
||||
case parserStateKey:
|
||||
switch c {
|
||||
case '=':
|
||||
key = normalizeOdbcKey(key)
|
||||
state = parserStateBeginValue
|
||||
|
||||
case ';':
|
||||
// Key without value
|
||||
key = normalizeOdbcKey(key)
|
||||
res[key] = value
|
||||
key = ""
|
||||
value = ""
|
||||
state = parserStateBeforeKey
|
||||
|
||||
default:
|
||||
key += string(c)
|
||||
}
|
||||
|
||||
case parserStateBeginValue:
|
||||
switch {
|
||||
case c == '{':
|
||||
state = parserStateBracedValue
|
||||
case c == ';':
|
||||
// Empty value
|
||||
res[key] = value
|
||||
key = ""
|
||||
state = parserStateBeforeKey
|
||||
case unicode.IsSpace(c):
|
||||
// Ignore whitespace
|
||||
default:
|
||||
state = parserStateBareValue
|
||||
value += string(c)
|
||||
}
|
||||
|
||||
case parserStateBareValue:
|
||||
if c == ';' {
|
||||
res[key] = strings.TrimRightFunc(value, unicode.IsSpace)
|
||||
key = ""
|
||||
value = ""
|
||||
state = parserStateBeforeKey
|
||||
} else {
|
||||
value += string(c)
|
||||
}
|
||||
|
||||
case parserStateBracedValue:
|
||||
if c == '}' {
|
||||
state = parserStateBracedValueClosingBrace
|
||||
} else {
|
||||
value += string(c)
|
||||
}
|
||||
|
||||
case parserStateBracedValueClosingBrace:
|
||||
if c == '}' {
|
||||
// Escaped closing brace
|
||||
value += string(c)
|
||||
state = parserStateBracedValue
|
||||
continue
|
||||
}
|
||||
|
||||
// End of braced value
|
||||
res[key] = value
|
||||
key = ""
|
||||
value = ""
|
||||
|
||||
// This character is the first character past the end,
|
||||
// so it needs to be parsed like the parserStateEndValue state.
|
||||
state = parserStateEndValue
|
||||
switch {
|
||||
case c == ';':
|
||||
state = parserStateBeforeKey
|
||||
case unicode.IsSpace(c):
|
||||
// Ignore whitespace
|
||||
default:
|
||||
return res, fmt.Errorf("Unexpected character %c at index %d. Expected semi-colon or whitespace.", c, i)
|
||||
}
|
||||
|
||||
case parserStateEndValue:
|
||||
switch {
|
||||
case c == ';':
|
||||
state = parserStateBeforeKey
|
||||
case unicode.IsSpace(c):
|
||||
// Ignore whitespace
|
||||
default:
|
||||
return res, fmt.Errorf("Unexpected character %c at index %d. Expected semi-colon or whitespace.", c, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch state {
|
||||
case parserStateBeforeKey: // Okay
|
||||
case parserStateKey: // Unfinished key. Treat as key without value.
|
||||
key = normalizeOdbcKey(key)
|
||||
res[key] = value
|
||||
case parserStateBeginValue: // Empty value
|
||||
res[key] = value
|
||||
case parserStateBareValue:
|
||||
res[key] = strings.TrimRightFunc(value, unicode.IsSpace)
|
||||
case parserStateBracedValue:
|
||||
return res, fmt.Errorf("Unexpected end of braced value at index %d.", len(dsn))
|
||||
case parserStateBracedValueClosingBrace: // End of braced value
|
||||
res[key] = value
|
||||
case parserStateEndValue: // Okay
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Normalizes the given string as an ODBC-format key
|
||||
func normalizeOdbcKey(s string) string {
|
||||
return strings.ToLower(strings.TrimRightFunc(s, unicode.IsSpace))
|
||||
}
|
||||
-131
@@ -1,131 +0,0 @@
|
||||
package mssql
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"math"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// http://msdn.microsoft.com/en-us/library/ee780893.aspx
|
||||
type Decimal struct {
|
||||
integer [4]uint32
|
||||
positive bool
|
||||
prec uint8
|
||||
scale uint8
|
||||
}
|
||||
|
||||
var scaletblflt64 [39]float64
|
||||
|
||||
func (d Decimal) ToFloat64() float64 {
|
||||
val := float64(0)
|
||||
for i := 3; i >= 0; i-- {
|
||||
val *= 0x100000000
|
||||
val += float64(d.integer[i])
|
||||
}
|
||||
if !d.positive {
|
||||
val = -val
|
||||
}
|
||||
if d.scale != 0 {
|
||||
val /= scaletblflt64[d.scale]
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
const autoScale = 100
|
||||
|
||||
func Float64ToDecimal(f float64) (Decimal, error) {
|
||||
return Float64ToDecimalScale(f, autoScale)
|
||||
}
|
||||
|
||||
func Float64ToDecimalScale(f float64, scale uint8) (Decimal, error) {
|
||||
var dec Decimal
|
||||
if math.IsNaN(f) {
|
||||
return dec, errors.New("NaN")
|
||||
}
|
||||
if math.IsInf(f, 0) {
|
||||
return dec, errors.New("Infinity can't be converted to decimal")
|
||||
}
|
||||
dec.positive = f >= 0
|
||||
if !dec.positive {
|
||||
f = math.Abs(f)
|
||||
}
|
||||
if f > 3.402823669209385e+38 {
|
||||
return dec, errors.New("Float value is out of range")
|
||||
}
|
||||
dec.prec = 20
|
||||
var integer float64
|
||||
for dec.scale = 0; dec.scale <= scale; dec.scale++ {
|
||||
integer = f * scaletblflt64[dec.scale]
|
||||
_, frac := math.Modf(integer)
|
||||
if frac == 0 && scale == autoScale {
|
||||
break
|
||||
}
|
||||
}
|
||||
for i := 0; i < 4; i++ {
|
||||
mod := math.Mod(integer, 0x100000000)
|
||||
integer -= mod
|
||||
integer /= 0x100000000
|
||||
dec.integer[i] = uint32(mod)
|
||||
}
|
||||
return dec, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
var acc float64 = 1
|
||||
for i := 0; i <= 38; i++ {
|
||||
scaletblflt64[i] = acc
|
||||
acc *= 10
|
||||
}
|
||||
}
|
||||
|
||||
func (d Decimal) BigInt() big.Int {
|
||||
bytes := make([]byte, 16)
|
||||
binary.BigEndian.PutUint32(bytes[0:4], d.integer[3])
|
||||
binary.BigEndian.PutUint32(bytes[4:8], d.integer[2])
|
||||
binary.BigEndian.PutUint32(bytes[8:12], d.integer[1])
|
||||
binary.BigEndian.PutUint32(bytes[12:16], d.integer[0])
|
||||
var x big.Int
|
||||
x.SetBytes(bytes)
|
||||
if !d.positive {
|
||||
x.Neg(&x)
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
func (d Decimal) Bytes() []byte {
|
||||
x := d.BigInt()
|
||||
return scaleBytes(x.String(), d.scale)
|
||||
}
|
||||
|
||||
func (d Decimal) UnscaledBytes() []byte {
|
||||
x := d.BigInt()
|
||||
return x.Bytes()
|
||||
}
|
||||
|
||||
func scaleBytes(s string, scale uint8) []byte {
|
||||
z := make([]byte, 0, len(s)+1)
|
||||
if s[0] == '-' || s[0] == '+' {
|
||||
z = append(z, byte(s[0]))
|
||||
s = s[1:]
|
||||
}
|
||||
pos := len(s) - int(scale)
|
||||
if pos <= 0 {
|
||||
z = append(z, byte('0'))
|
||||
} else if pos > 0 {
|
||||
z = append(z, []byte(s[:pos])...)
|
||||
}
|
||||
if scale > 0 {
|
||||
z = append(z, byte('.'))
|
||||
for pos < 0 {
|
||||
z = append(z, byte('0'))
|
||||
pos++
|
||||
}
|
||||
z = append(z, []byte(s[pos:])...)
|
||||
}
|
||||
return z
|
||||
}
|
||||
|
||||
func (d Decimal) String() string {
|
||||
return string(d.Bytes())
|
||||
}
|
||||
+1
-3
@@ -3,8 +3,6 @@ module github.com/denisenkom/go-mssqldb
|
||||
go 1.11
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.37.4
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.2 // indirect
|
||||
)
|
||||
|
||||
+2
-165
@@ -1,168 +1,5 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.37.2 h1:4y4L7BdHenTfZL0HervofNTHh9Ad6mNX72cQvl+5eH0=
|
||||
cloud.google.com/go v0.37.2/go.mod h1:H8IAquKe2L30IxoupDgqTaQvKSwF/c8prYHynGIWQbA=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.6.2/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/openzipkin/zipkin-go v0.1.3/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.opencensus.io v0.19.1/go.mod h1:gug0GbSHa8Pafr0d2urOSgoXHZ6x/RUlaiT0d9pqb4A=
|
||||
go.opencensus.io v0.19.2/go.mod h1:NO/8qkisMZLZ1FCsKNqtJPwc8/TaclWyY0B6wcYNg9M=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
golang.org/x/build v0.0.0-20190314133821-5284462c4bec/go.mod h1:atTaCNAy0f16Ah5aV1gMSwgiKVHwu/JncqDpuRr7lS4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181218192612-074acd46bca6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181219222714-6e267b5cc78e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.0.0-20181220000619-583d854617af/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.2.0/go.mod h1:IfRCZScioGtypHNTlz3gFk67J8uePVW7uDTBzXuIkhU=
|
||||
google.golang.org/api v0.3.0/go.mod h1:IuvZyQh8jgscv8qWfQ4ABd8m7hEudgBFM/EdhA3BnXw=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181219182458-5a97ab628bfb/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
package decimal
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Decimal represents decimal type in the Microsoft Open Specifications: http://msdn.microsoft.com/en-us/library/ee780893.aspx
|
||||
type Decimal struct {
|
||||
integer [4]uint32 // Little-endian
|
||||
positive bool
|
||||
prec uint8
|
||||
scale uint8
|
||||
}
|
||||
|
||||
var (
|
||||
scaletblflt64 [39]float64
|
||||
int10 big.Int
|
||||
int1e5 big.Int
|
||||
)
|
||||
|
||||
func init() {
|
||||
var acc float64 = 1
|
||||
for i := 0; i <= 38; i++ {
|
||||
scaletblflt64[i] = acc
|
||||
acc *= 10
|
||||
}
|
||||
|
||||
int10.SetInt64(10)
|
||||
int1e5.SetInt64(1e5)
|
||||
}
|
||||
|
||||
const autoScale = 100
|
||||
|
||||
// SetInteger sets the ind'th element in the integer array
|
||||
func (d *Decimal) SetInteger(integer uint32, ind uint8) {
|
||||
d.integer[ind] = integer
|
||||
}
|
||||
|
||||
// SetPositive sets the positive member
|
||||
func (d *Decimal) SetPositive(positive bool) {
|
||||
d.positive = positive
|
||||
}
|
||||
|
||||
// SetPrec sets the prec member
|
||||
func (d *Decimal) SetPrec(prec uint8) {
|
||||
d.prec = prec
|
||||
}
|
||||
|
||||
// SetScale sets the scale member
|
||||
func (d *Decimal) SetScale(scale uint8) {
|
||||
d.scale = scale
|
||||
}
|
||||
|
||||
// IsPositive returns true if the Decimal is positive
|
||||
func (d *Decimal) IsPositive() bool {
|
||||
return d.positive
|
||||
}
|
||||
|
||||
// ToFloat64 converts decimal to a float64
|
||||
func (d Decimal) ToFloat64() float64 {
|
||||
val := float64(0)
|
||||
for i := 3; i >= 0; i-- {
|
||||
val *= 0x100000000
|
||||
val += float64(d.integer[i])
|
||||
}
|
||||
if !d.positive {
|
||||
val = -val
|
||||
}
|
||||
if d.scale != 0 {
|
||||
val /= scaletblflt64[d.scale]
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// BigInt converts decimal to a bigint
|
||||
func (d Decimal) BigInt() big.Int {
|
||||
bytes := make([]byte, 16)
|
||||
binary.BigEndian.PutUint32(bytes[0:4], d.integer[3])
|
||||
binary.BigEndian.PutUint32(bytes[4:8], d.integer[2])
|
||||
binary.BigEndian.PutUint32(bytes[8:12], d.integer[1])
|
||||
binary.BigEndian.PutUint32(bytes[12:16], d.integer[0])
|
||||
var x big.Int
|
||||
x.SetBytes(bytes)
|
||||
if !d.positive {
|
||||
x.Neg(&x)
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// Bytes converts decimal to a scaled byte slice
|
||||
func (d Decimal) Bytes() []byte {
|
||||
x := d.BigInt()
|
||||
return ScaleBytes(x.String(), d.scale)
|
||||
}
|
||||
|
||||
// UnscaledBytes converts decimal to a unscaled byte slice
|
||||
func (d Decimal) UnscaledBytes() []byte {
|
||||
x := d.BigInt()
|
||||
return x.Bytes()
|
||||
}
|
||||
|
||||
// String converts decimal to a string
|
||||
func (d Decimal) String() string {
|
||||
return string(d.Bytes())
|
||||
}
|
||||
|
||||
// Float64ToDecimal converts float64 to decimal
|
||||
func Float64ToDecimal(f float64) (Decimal, error) {
|
||||
return Float64ToDecimalScale(f, autoScale)
|
||||
}
|
||||
|
||||
// Float64ToDecimalScale converts float64 to decimal; user can specify the scale
|
||||
func Float64ToDecimalScale(f float64, scale uint8) (Decimal, error) {
|
||||
var dec Decimal
|
||||
if math.IsNaN(f) {
|
||||
return dec, errors.New("NaN")
|
||||
}
|
||||
if math.IsInf(f, 0) {
|
||||
return dec, errors.New("Infinity can't be converted to decimal")
|
||||
}
|
||||
dec.positive = f >= 0
|
||||
if !dec.positive {
|
||||
f = math.Abs(f)
|
||||
}
|
||||
if f > 3.402823669209385e+38 {
|
||||
return dec, errors.New("Float value is out of range")
|
||||
}
|
||||
dec.prec = 20
|
||||
var integer float64
|
||||
for dec.scale = 0; dec.scale <= scale; dec.scale++ {
|
||||
integer = f * scaletblflt64[dec.scale]
|
||||
_, frac := math.Modf(integer)
|
||||
if frac == 0 && scale == autoScale {
|
||||
break
|
||||
}
|
||||
}
|
||||
for i := 0; i < 4; i++ {
|
||||
mod := math.Mod(integer, 0x100000000)
|
||||
integer -= mod
|
||||
integer /= 0x100000000
|
||||
dec.integer[i] = uint32(mod)
|
||||
if mod-math.Trunc(mod) >= 0.5 {
|
||||
dec.integer[i] = uint32(mod) + 1
|
||||
}
|
||||
}
|
||||
return dec, nil
|
||||
}
|
||||
|
||||
// Int64ToDecimalScale converts float64 to decimal; user can specify the scale
|
||||
func Int64ToDecimalScale(v int64, scale uint8) Decimal {
|
||||
positive := v >= 0
|
||||
if !positive {
|
||||
if v == math.MinInt64 {
|
||||
// Special case - can't negate
|
||||
return Decimal{
|
||||
integer: [4]uint32{0, 0x80000000, 0, 0},
|
||||
positive: false,
|
||||
prec: 20,
|
||||
scale: 0,
|
||||
}
|
||||
}
|
||||
v = -v
|
||||
}
|
||||
return Decimal{
|
||||
integer: [4]uint32{uint32(v), uint32(v >> 32), 0, 0},
|
||||
positive: positive,
|
||||
prec: 20,
|
||||
scale: scale,
|
||||
}
|
||||
}
|
||||
|
||||
// StringToDecimalScale converts string to decimal
|
||||
func StringToDecimalScale(v string, outScale uint8) (Decimal, error) {
|
||||
var r big.Int
|
||||
var unscaled string
|
||||
var inScale int
|
||||
|
||||
point := strings.LastIndexByte(v, '.')
|
||||
if point == -1 {
|
||||
inScale = 0
|
||||
unscaled = v
|
||||
} else {
|
||||
inScale = len(v) - point - 1
|
||||
unscaled = v[:point] + v[point+1:]
|
||||
}
|
||||
if inScale > math.MaxUint8 {
|
||||
return Decimal{}, fmt.Errorf("can't parse %q as a decimal number: scale too large", v)
|
||||
}
|
||||
|
||||
_, ok := r.SetString(unscaled, 10)
|
||||
if !ok {
|
||||
return Decimal{}, fmt.Errorf("can't parse %q as a decimal number", v)
|
||||
}
|
||||
|
||||
if inScale > int(outScale) {
|
||||
return Decimal{}, fmt.Errorf("can't parse %q as a decimal number: scale %d is larger than the scale %d of the target column", v, inScale, outScale)
|
||||
}
|
||||
for inScale < int(outScale) {
|
||||
if int(outScale)-inScale >= 5 {
|
||||
r.Mul(&r, &int1e5)
|
||||
inScale += 5
|
||||
} else {
|
||||
r.Mul(&r, &int10)
|
||||
inScale++
|
||||
}
|
||||
}
|
||||
|
||||
bytes := r.Bytes()
|
||||
if len(bytes) > 16 {
|
||||
return Decimal{}, fmt.Errorf("can't parse %q as a decimal number: precision too large", v)
|
||||
}
|
||||
var out [4]uint32
|
||||
for i, b := range bytes {
|
||||
pos := len(bytes) - i - 1
|
||||
out[pos/4] += uint32(b) << uint(pos%4*8)
|
||||
}
|
||||
return Decimal{
|
||||
integer: out,
|
||||
positive: r.Sign() >= 0,
|
||||
prec: 20,
|
||||
scale: uint8(inScale),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ScaleBytes converts a stringified decimal to a scaled byte slice
|
||||
func ScaleBytes(s string, scale uint8) []byte {
|
||||
z := make([]byte, 0, len(s)+1)
|
||||
if s[0] == '-' || s[0] == '+' {
|
||||
z = append(z, byte(s[0]))
|
||||
s = s[1:]
|
||||
}
|
||||
pos := len(s) - int(scale)
|
||||
if pos <= 0 {
|
||||
z = append(z, byte('0'))
|
||||
} else if pos > 0 {
|
||||
z = append(z, []byte(s[:pos])...)
|
||||
}
|
||||
if scale > 0 {
|
||||
z = append(z, byte('.'))
|
||||
for pos < 0 {
|
||||
z = append(z, byte('0'))
|
||||
pos++
|
||||
}
|
||||
z = append(z, []byte(s[pos:])...)
|
||||
}
|
||||
return z
|
||||
}
|
||||
+2
-23
@@ -716,6 +716,8 @@ func (rc *Rows) Next(dest []driver.Value) error {
|
||||
if tokdata.isError() {
|
||||
return rc.stmt.c.checkBadConn(tokdata.getError())
|
||||
}
|
||||
case ReturnStatus:
|
||||
rc.stmt.c.setReturnStatus(tokdata)
|
||||
case error:
|
||||
return rc.stmt.c.checkBadConn(tokdata)
|
||||
}
|
||||
@@ -874,29 +876,6 @@ func (r *Result) RowsAffected() (int64, error) {
|
||||
return r.rowsAffected, nil
|
||||
}
|
||||
|
||||
func (r *Result) LastInsertId() (int64, error) {
|
||||
s, err := r.c.Prepare("select cast(@@identity as bigint)")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer s.Close()
|
||||
rows, err := s.Query(nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
dest := make([]driver.Value, 1)
|
||||
err = rows.Next(dest)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if dest[0] == nil {
|
||||
return -1, errors.New("There is no generated identity value")
|
||||
}
|
||||
lastInsertId := dest[0].(int64)
|
||||
return lastInsertId, nil
|
||||
}
|
||||
|
||||
var _ driver.Pinger = &Conn{}
|
||||
|
||||
// Ping is used to check if the remote server is available and satisfies the Pinger interface.
|
||||
|
||||
+5
@@ -5,6 +5,7 @@ package mssql
|
||||
import (
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var _ driver.Connector = &Connector{}
|
||||
@@ -45,3 +46,7 @@ func (c *Connector) Connect(ctx context.Context) (driver.Conn, error) {
|
||||
func (c *Connector) Driver() driver.Driver {
|
||||
return c.driver
|
||||
}
|
||||
|
||||
func (r *Result) LastInsertId() (int64, error) {
|
||||
return -1, errors.New("LastInsertId is not supported. Please use the OUTPUT clause or add `select ID = convert(bigint, SCOPE_IDENTITY())` to the end of your query.")
|
||||
}
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// +build !go1.10
|
||||
|
||||
package mssql
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
)
|
||||
|
||||
func (r *Result) LastInsertId() (int64, error) {
|
||||
s, err := r.c.Prepare("select cast(@@identity as bigint)")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer s.Close()
|
||||
rows, err := s.Query(nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
dest := make([]driver.Value, 1)
|
||||
err = rows.Next(dest)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if dest[0] == nil {
|
||||
return -1, errors.New("There is no generated identity value")
|
||||
}
|
||||
lastInsertId := dest[0].(int64)
|
||||
return lastInsertId, nil
|
||||
}
|
||||
+1
-1
@@ -11,7 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
// "github.com/cockroachdb/apd"
|
||||
"cloud.google.com/go/civil"
|
||||
"github.com/golang-sql/civil"
|
||||
)
|
||||
|
||||
// Type alias provided for compatibility.
|
||||
|
||||
+105
-40
@@ -9,9 +9,6 @@ import (
|
||||
type timeoutConn struct {
|
||||
c net.Conn
|
||||
timeout time.Duration
|
||||
buf *tdsBuffer
|
||||
packetPending bool
|
||||
continueRead bool
|
||||
}
|
||||
|
||||
func newTimeoutConn(conn net.Conn, timeout time.Duration) *timeoutConn {
|
||||
@@ -22,32 +19,6 @@ func newTimeoutConn(conn net.Conn, timeout time.Duration) *timeoutConn {
|
||||
}
|
||||
|
||||
func (c *timeoutConn) Read(b []byte) (n int, err error) {
|
||||
if c.buf != nil {
|
||||
if c.packetPending {
|
||||
c.packetPending = false
|
||||
err = c.buf.FinishPacket()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Cannot send handshake packet: %s", err.Error())
|
||||
return
|
||||
}
|
||||
c.continueRead = false
|
||||
}
|
||||
if !c.continueRead {
|
||||
var packet packetType
|
||||
packet, err = c.buf.BeginRead()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Cannot read handshake packet: %s", err.Error())
|
||||
return
|
||||
}
|
||||
if packet != packPrelogin {
|
||||
err = fmt.Errorf("unexpected packet %d, expecting prelogin", packet)
|
||||
return
|
||||
}
|
||||
c.continueRead = true
|
||||
}
|
||||
n, err = c.buf.Read(b)
|
||||
return
|
||||
}
|
||||
if c.timeout > 0 {
|
||||
err = c.c.SetDeadline(time.Now().Add(c.timeout))
|
||||
if err != nil {
|
||||
@@ -58,17 +29,6 @@ func (c *timeoutConn) Read(b []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
func (c *timeoutConn) Write(b []byte) (n int, err error) {
|
||||
if c.buf != nil {
|
||||
if !c.packetPending {
|
||||
c.buf.BeginPacket(packPrelogin, false)
|
||||
c.packetPending = true
|
||||
}
|
||||
n, err = c.buf.Write(b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
if c.timeout > 0 {
|
||||
err = c.c.SetDeadline(time.Now().Add(c.timeout))
|
||||
if err != nil {
|
||||
@@ -101,3 +61,108 @@ func (c timeoutConn) SetReadDeadline(t time.Time) error {
|
||||
func (c timeoutConn) SetWriteDeadline(t time.Time) error {
|
||||
panic("Not implemented")
|
||||
}
|
||||
|
||||
// this connection is used during TLS Handshake
|
||||
// TDS protocol requires TLS handshake messages to be sent inside TDS packets
|
||||
type tlsHandshakeConn struct {
|
||||
buf *tdsBuffer
|
||||
packetPending bool
|
||||
continueRead bool
|
||||
}
|
||||
|
||||
func (c *tlsHandshakeConn) Read(b []byte) (n int, err error) {
|
||||
if c.packetPending {
|
||||
c.packetPending = false
|
||||
err = c.buf.FinishPacket()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Cannot send handshake packet: %s", err.Error())
|
||||
return
|
||||
}
|
||||
c.continueRead = false
|
||||
}
|
||||
if !c.continueRead {
|
||||
var packet packetType
|
||||
packet, err = c.buf.BeginRead()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Cannot read handshake packet: %s", err.Error())
|
||||
return
|
||||
}
|
||||
if packet != packPrelogin {
|
||||
err = fmt.Errorf("unexpected packet %d, expecting prelogin", packet)
|
||||
return
|
||||
}
|
||||
c.continueRead = true
|
||||
}
|
||||
return c.buf.Read(b)
|
||||
}
|
||||
|
||||
func (c *tlsHandshakeConn) Write(b []byte) (n int, err error) {
|
||||
if !c.packetPending {
|
||||
c.buf.BeginPacket(packPrelogin, false)
|
||||
c.packetPending = true
|
||||
}
|
||||
return c.buf.Write(b)
|
||||
}
|
||||
|
||||
func (c *tlsHandshakeConn) Close() error {
|
||||
panic("Not implemented")
|
||||
}
|
||||
|
||||
func (c *tlsHandshakeConn) LocalAddr() net.Addr {
|
||||
panic("Not implemented")
|
||||
}
|
||||
|
||||
func (c *tlsHandshakeConn) RemoteAddr() net.Addr {
|
||||
panic("Not implemented")
|
||||
}
|
||||
|
||||
func (c *tlsHandshakeConn) SetDeadline(t time.Time) error {
|
||||
panic("Not implemented")
|
||||
}
|
||||
|
||||
func (c *tlsHandshakeConn) SetReadDeadline(t time.Time) error {
|
||||
panic("Not implemented")
|
||||
}
|
||||
|
||||
func (c *tlsHandshakeConn) SetWriteDeadline(t time.Time) error {
|
||||
panic("Not implemented")
|
||||
}
|
||||
|
||||
// this connection just delegates all methods to it's wrapped connection
|
||||
// it also allows switching underlying connection on the fly
|
||||
// it is needed because tls.Conn does not allow switching underlying connection
|
||||
type passthroughConn struct {
|
||||
c net.Conn
|
||||
}
|
||||
|
||||
func (c passthroughConn) Read(b []byte) (n int, err error) {
|
||||
return c.c.Read(b)
|
||||
}
|
||||
|
||||
func (c passthroughConn) Write(b []byte) (n int, err error) {
|
||||
return c.c.Write(b)
|
||||
}
|
||||
|
||||
func (c passthroughConn) Close() error {
|
||||
return c.c.Close()
|
||||
}
|
||||
|
||||
func (c passthroughConn) LocalAddr() net.Addr {
|
||||
panic("Not implemented")
|
||||
}
|
||||
|
||||
func (c passthroughConn) RemoteAddr() net.Addr {
|
||||
panic("Not implemented")
|
||||
}
|
||||
|
||||
func (c passthroughConn) SetDeadline(t time.Time) error {
|
||||
panic("Not implemented")
|
||||
}
|
||||
|
||||
func (c passthroughConn) SetReadDeadline(t time.Time) error {
|
||||
panic("Not implemented")
|
||||
}
|
||||
|
||||
func (c passthroughConn) SetWriteDeadline(t time.Time) error {
|
||||
panic("Not implemented")
|
||||
}
|
||||
|
||||
+11
-472
@@ -10,13 +10,9 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf16"
|
||||
"unicode/utf8"
|
||||
)
|
||||
@@ -51,15 +47,13 @@ func parseInstances(msg []byte) map[string]map[string]string {
|
||||
}
|
||||
|
||||
func getInstances(ctx context.Context, d Dialer, address string) (map[string]map[string]string, error) {
|
||||
maxTime := 5 * time.Second
|
||||
ctx, cancel := context.WithTimeout(ctx, maxTime)
|
||||
defer cancel()
|
||||
conn, err := d.DialContext(ctx, "udp", address+":1434")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
conn.SetDeadline(time.Now().Add(maxTime))
|
||||
deadline, _ := ctx.Deadline()
|
||||
conn.SetDeadline(deadline)
|
||||
_, err = conn.Write([]byte{3})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -474,10 +468,9 @@ func readUcs2(r io.Reader, numchars int) (res string, err error) {
|
||||
}
|
||||
|
||||
func readUsVarChar(r io.Reader) (res string, err error) {
|
||||
var numchars uint16
|
||||
err = binary.Read(r, binary.LittleEndian, &numchars)
|
||||
numchars, err := readUshort(r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return
|
||||
}
|
||||
return readUcs2(r, int(numchars))
|
||||
}
|
||||
@@ -497,8 +490,7 @@ func writeUsVarChar(w io.Writer, s string) (err error) {
|
||||
}
|
||||
|
||||
func readBVarChar(r io.Reader) (res string, err error) {
|
||||
var numchars uint8
|
||||
err = binary.Read(r, binary.LittleEndian, &numchars)
|
||||
numchars, err := readByte(r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -525,8 +517,7 @@ func writeBVarChar(w io.Writer, s string) (err error) {
|
||||
}
|
||||
|
||||
func readBVarByte(r io.Reader) (res []byte, err error) {
|
||||
var length uint8
|
||||
err = binary.Read(r, binary.LittleEndian, &length)
|
||||
length, err := readByte(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -654,458 +645,6 @@ func sendAttention(buf *tdsBuffer) error {
|
||||
return buf.FinishPacket()
|
||||
}
|
||||
|
||||
type connectParams struct {
|
||||
logFlags uint64
|
||||
port uint64
|
||||
host string
|
||||
instance string
|
||||
database string
|
||||
user string
|
||||
password string
|
||||
dial_timeout time.Duration
|
||||
conn_timeout time.Duration
|
||||
keepAlive time.Duration
|
||||
encrypt bool
|
||||
disableEncryption bool
|
||||
trustServerCertificate bool
|
||||
certificate string
|
||||
hostInCertificate string
|
||||
hostInCertificateProvided bool
|
||||
serverSPN string
|
||||
workstation string
|
||||
appname string
|
||||
typeFlags uint8
|
||||
failOverPartner string
|
||||
failOverPort uint64
|
||||
packetSize uint16
|
||||
}
|
||||
|
||||
func splitConnectionString(dsn string) (res map[string]string) {
|
||||
res = map[string]string{}
|
||||
parts := strings.Split(dsn, ";")
|
||||
for _, part := range parts {
|
||||
if len(part) == 0 {
|
||||
continue
|
||||
}
|
||||
lst := strings.SplitN(part, "=", 2)
|
||||
name := strings.TrimSpace(strings.ToLower(lst[0]))
|
||||
if len(name) == 0 {
|
||||
continue
|
||||
}
|
||||
var value string = ""
|
||||
if len(lst) > 1 {
|
||||
value = strings.TrimSpace(lst[1])
|
||||
}
|
||||
res[name] = value
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Splits a URL in the ODBC format
|
||||
func splitConnectionStringOdbc(dsn string) (map[string]string, error) {
|
||||
res := map[string]string{}
|
||||
|
||||
type parserState int
|
||||
const (
|
||||
// Before the start of a key
|
||||
parserStateBeforeKey parserState = iota
|
||||
|
||||
// Inside a key
|
||||
parserStateKey
|
||||
|
||||
// Beginning of a value. May be bare or braced
|
||||
parserStateBeginValue
|
||||
|
||||
// Inside a bare value
|
||||
parserStateBareValue
|
||||
|
||||
// Inside a braced value
|
||||
parserStateBracedValue
|
||||
|
||||
// A closing brace inside a braced value.
|
||||
// May be the end of the value or an escaped closing brace, depending on the next character
|
||||
parserStateBracedValueClosingBrace
|
||||
|
||||
// After a value. Next character should be a semicolon or whitespace.
|
||||
parserStateEndValue
|
||||
)
|
||||
|
||||
var state = parserStateBeforeKey
|
||||
|
||||
var key string
|
||||
var value string
|
||||
|
||||
for i, c := range dsn {
|
||||
switch state {
|
||||
case parserStateBeforeKey:
|
||||
switch {
|
||||
case c == '=':
|
||||
return res, fmt.Errorf("Unexpected character = at index %d. Expected start of key or semi-colon or whitespace.", i)
|
||||
case !unicode.IsSpace(c) && c != ';':
|
||||
state = parserStateKey
|
||||
key += string(c)
|
||||
}
|
||||
|
||||
case parserStateKey:
|
||||
switch c {
|
||||
case '=':
|
||||
key = normalizeOdbcKey(key)
|
||||
if len(key) == 0 {
|
||||
return res, fmt.Errorf("Unexpected end of key at index %d.", i)
|
||||
}
|
||||
|
||||
state = parserStateBeginValue
|
||||
|
||||
case ';':
|
||||
// Key without value
|
||||
key = normalizeOdbcKey(key)
|
||||
if len(key) == 0 {
|
||||
return res, fmt.Errorf("Unexpected end of key at index %d.", i)
|
||||
}
|
||||
|
||||
res[key] = value
|
||||
key = ""
|
||||
value = ""
|
||||
state = parserStateBeforeKey
|
||||
|
||||
default:
|
||||
key += string(c)
|
||||
}
|
||||
|
||||
case parserStateBeginValue:
|
||||
switch {
|
||||
case c == '{':
|
||||
state = parserStateBracedValue
|
||||
case c == ';':
|
||||
// Empty value
|
||||
res[key] = value
|
||||
key = ""
|
||||
state = parserStateBeforeKey
|
||||
case unicode.IsSpace(c):
|
||||
// Ignore whitespace
|
||||
default:
|
||||
state = parserStateBareValue
|
||||
value += string(c)
|
||||
}
|
||||
|
||||
case parserStateBareValue:
|
||||
if c == ';' {
|
||||
res[key] = strings.TrimRightFunc(value, unicode.IsSpace)
|
||||
key = ""
|
||||
value = ""
|
||||
state = parserStateBeforeKey
|
||||
} else {
|
||||
value += string(c)
|
||||
}
|
||||
|
||||
case parserStateBracedValue:
|
||||
if c == '}' {
|
||||
state = parserStateBracedValueClosingBrace
|
||||
} else {
|
||||
value += string(c)
|
||||
}
|
||||
|
||||
case parserStateBracedValueClosingBrace:
|
||||
if c == '}' {
|
||||
// Escaped closing brace
|
||||
value += string(c)
|
||||
state = parserStateBracedValue
|
||||
continue
|
||||
}
|
||||
|
||||
// End of braced value
|
||||
res[key] = value
|
||||
key = ""
|
||||
value = ""
|
||||
|
||||
// This character is the first character past the end,
|
||||
// so it needs to be parsed like the parserStateEndValue state.
|
||||
state = parserStateEndValue
|
||||
switch {
|
||||
case c == ';':
|
||||
state = parserStateBeforeKey
|
||||
case unicode.IsSpace(c):
|
||||
// Ignore whitespace
|
||||
default:
|
||||
return res, fmt.Errorf("Unexpected character %c at index %d. Expected semi-colon or whitespace.", c, i)
|
||||
}
|
||||
|
||||
case parserStateEndValue:
|
||||
switch {
|
||||
case c == ';':
|
||||
state = parserStateBeforeKey
|
||||
case unicode.IsSpace(c):
|
||||
// Ignore whitespace
|
||||
default:
|
||||
return res, fmt.Errorf("Unexpected character %c at index %d. Expected semi-colon or whitespace.", c, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch state {
|
||||
case parserStateBeforeKey: // Okay
|
||||
case parserStateKey: // Unfinished key. Treat as key without value.
|
||||
key = normalizeOdbcKey(key)
|
||||
if len(key) == 0 {
|
||||
return res, fmt.Errorf("Unexpected end of key at index %d.", len(dsn))
|
||||
}
|
||||
res[key] = value
|
||||
case parserStateBeginValue: // Empty value
|
||||
res[key] = value
|
||||
case parserStateBareValue:
|
||||
res[key] = strings.TrimRightFunc(value, unicode.IsSpace)
|
||||
case parserStateBracedValue:
|
||||
return res, fmt.Errorf("Unexpected end of braced value at index %d.", len(dsn))
|
||||
case parserStateBracedValueClosingBrace: // End of braced value
|
||||
res[key] = value
|
||||
case parserStateEndValue: // Okay
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Normalizes the given string as an ODBC-format key
|
||||
func normalizeOdbcKey(s string) string {
|
||||
return strings.ToLower(strings.TrimRightFunc(s, unicode.IsSpace))
|
||||
}
|
||||
|
||||
// Splits a URL of the form sqlserver://username:password@host/instance?param1=value¶m2=value
|
||||
func splitConnectionStringURL(dsn string) (map[string]string, error) {
|
||||
res := map[string]string{}
|
||||
|
||||
u, err := url.Parse(dsn)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
if u.Scheme != "sqlserver" {
|
||||
return res, fmt.Errorf("scheme %s is not recognized", u.Scheme)
|
||||
}
|
||||
|
||||
if u.User != nil {
|
||||
res["user id"] = u.User.Username()
|
||||
p, exists := u.User.Password()
|
||||
if exists {
|
||||
res["password"] = p
|
||||
}
|
||||
}
|
||||
|
||||
host, port, err := net.SplitHostPort(u.Host)
|
||||
if err != nil {
|
||||
host = u.Host
|
||||
}
|
||||
|
||||
if len(u.Path) > 0 {
|
||||
res["server"] = host + "\\" + u.Path[1:]
|
||||
} else {
|
||||
res["server"] = host
|
||||
}
|
||||
|
||||
if len(port) > 0 {
|
||||
res["port"] = port
|
||||
}
|
||||
|
||||
query := u.Query()
|
||||
for k, v := range query {
|
||||
if len(v) > 1 {
|
||||
return res, fmt.Errorf("key %s provided more than once", k)
|
||||
}
|
||||
res[strings.ToLower(k)] = v[0]
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func parseConnectParams(dsn string) (connectParams, error) {
|
||||
var p connectParams
|
||||
|
||||
var params map[string]string
|
||||
if strings.HasPrefix(dsn, "odbc:") {
|
||||
parameters, err := splitConnectionStringOdbc(dsn[len("odbc:"):])
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
params = parameters
|
||||
} else if strings.HasPrefix(dsn, "sqlserver://") {
|
||||
parameters, err := splitConnectionStringURL(dsn)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
params = parameters
|
||||
} else {
|
||||
params = splitConnectionString(dsn)
|
||||
}
|
||||
|
||||
strlog, ok := params["log"]
|
||||
if ok {
|
||||
var err error
|
||||
p.logFlags, err = strconv.ParseUint(strlog, 10, 64)
|
||||
if err != nil {
|
||||
return p, fmt.Errorf("Invalid log parameter '%s': %s", strlog, err.Error())
|
||||
}
|
||||
}
|
||||
server := params["server"]
|
||||
parts := strings.SplitN(server, `\`, 2)
|
||||
p.host = parts[0]
|
||||
if p.host == "." || strings.ToUpper(p.host) == "(LOCAL)" || p.host == "" {
|
||||
p.host = "localhost"
|
||||
}
|
||||
if len(parts) > 1 {
|
||||
p.instance = parts[1]
|
||||
}
|
||||
p.database = params["database"]
|
||||
p.user = params["user id"]
|
||||
p.password = params["password"]
|
||||
|
||||
p.port = 1433
|
||||
strport, ok := params["port"]
|
||||
if ok {
|
||||
var err error
|
||||
p.port, err = strconv.ParseUint(strport, 10, 16)
|
||||
if err != nil {
|
||||
f := "Invalid tcp port '%v': %v"
|
||||
return p, fmt.Errorf(f, strport, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/configure-the-network-packet-size-server-configuration-option
|
||||
// Default packet size remains at 4096 bytes
|
||||
p.packetSize = 4096
|
||||
strpsize, ok := params["packet size"]
|
||||
if ok {
|
||||
var err error
|
||||
psize, err := strconv.ParseUint(strpsize, 0, 16)
|
||||
if err != nil {
|
||||
f := "Invalid packet size '%v': %v"
|
||||
return p, fmt.Errorf(f, strpsize, err.Error())
|
||||
}
|
||||
|
||||
// Ensure packet size falls within the TDS protocol range of 512 to 32767 bytes
|
||||
// NOTE: Encrypted connections have a maximum size of 16383 bytes. If you request
|
||||
// a higher packet size, the server will respond with an ENVCHANGE request to
|
||||
// alter the packet size to 16383 bytes.
|
||||
p.packetSize = uint16(psize)
|
||||
if p.packetSize < 512 {
|
||||
p.packetSize = 512
|
||||
} else if p.packetSize > 32767 {
|
||||
p.packetSize = 32767
|
||||
}
|
||||
}
|
||||
|
||||
// https://msdn.microsoft.com/en-us/library/dd341108.aspx
|
||||
//
|
||||
// Do not set a connection timeout. Use Context to manage such things.
|
||||
// Default to zero, but still allow it to be set.
|
||||
if strconntimeout, ok := params["connection timeout"]; ok {
|
||||
timeout, err := strconv.ParseUint(strconntimeout, 10, 64)
|
||||
if err != nil {
|
||||
f := "Invalid connection timeout '%v': %v"
|
||||
return p, fmt.Errorf(f, strconntimeout, err.Error())
|
||||
}
|
||||
p.conn_timeout = time.Duration(timeout) * time.Second
|
||||
}
|
||||
p.dial_timeout = 15 * time.Second
|
||||
if strdialtimeout, ok := params["dial timeout"]; ok {
|
||||
timeout, err := strconv.ParseUint(strdialtimeout, 10, 64)
|
||||
if err != nil {
|
||||
f := "Invalid dial timeout '%v': %v"
|
||||
return p, fmt.Errorf(f, strdialtimeout, err.Error())
|
||||
}
|
||||
p.dial_timeout = time.Duration(timeout) * time.Second
|
||||
}
|
||||
|
||||
// default keep alive should be 30 seconds according to spec:
|
||||
// https://msdn.microsoft.com/en-us/library/dd341108.aspx
|
||||
p.keepAlive = 30 * time.Second
|
||||
if keepAlive, ok := params["keepalive"]; ok {
|
||||
timeout, err := strconv.ParseUint(keepAlive, 10, 64)
|
||||
if err != nil {
|
||||
f := "Invalid keepAlive value '%s': %s"
|
||||
return p, fmt.Errorf(f, keepAlive, err.Error())
|
||||
}
|
||||
p.keepAlive = time.Duration(timeout) * time.Second
|
||||
}
|
||||
encrypt, ok := params["encrypt"]
|
||||
if ok {
|
||||
if strings.EqualFold(encrypt, "DISABLE") {
|
||||
p.disableEncryption = true
|
||||
} else {
|
||||
var err error
|
||||
p.encrypt, err = strconv.ParseBool(encrypt)
|
||||
if err != nil {
|
||||
f := "Invalid encrypt '%s': %s"
|
||||
return p, fmt.Errorf(f, encrypt, err.Error())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
p.trustServerCertificate = true
|
||||
}
|
||||
trust, ok := params["trustservercertificate"]
|
||||
if ok {
|
||||
var err error
|
||||
p.trustServerCertificate, err = strconv.ParseBool(trust)
|
||||
if err != nil {
|
||||
f := "Invalid trust server certificate '%s': %s"
|
||||
return p, fmt.Errorf(f, trust, err.Error())
|
||||
}
|
||||
}
|
||||
p.certificate = params["certificate"]
|
||||
p.hostInCertificate, ok = params["hostnameincertificate"]
|
||||
if ok {
|
||||
p.hostInCertificateProvided = true
|
||||
} else {
|
||||
p.hostInCertificate = p.host
|
||||
p.hostInCertificateProvided = false
|
||||
}
|
||||
|
||||
serverSPN, ok := params["serverspn"]
|
||||
if ok {
|
||||
p.serverSPN = serverSPN
|
||||
} else {
|
||||
p.serverSPN = fmt.Sprintf("MSSQLSvc/%s:%d", p.host, p.port)
|
||||
}
|
||||
|
||||
workstation, ok := params["workstation id"]
|
||||
if ok {
|
||||
p.workstation = workstation
|
||||
} else {
|
||||
workstation, err := os.Hostname()
|
||||
if err == nil {
|
||||
p.workstation = workstation
|
||||
}
|
||||
}
|
||||
|
||||
appname, ok := params["app name"]
|
||||
if !ok {
|
||||
appname = "go-mssqldb"
|
||||
}
|
||||
p.appname = appname
|
||||
|
||||
appintent, ok := params["applicationintent"]
|
||||
if ok {
|
||||
if appintent == "ReadOnly" {
|
||||
p.typeFlags |= fReadOnlyIntent
|
||||
}
|
||||
}
|
||||
|
||||
failOverPartner, ok := params["failoverpartner"]
|
||||
if ok {
|
||||
p.failOverPartner = failOverPartner
|
||||
}
|
||||
|
||||
failOverPort, ok := params["failoverport"]
|
||||
if ok {
|
||||
var err error
|
||||
p.failOverPort, err = strconv.ParseUint(failOverPort, 0, 16)
|
||||
if err != nil {
|
||||
f := "Invalid tcp port '%v': %v"
|
||||
return p, fmt.Errorf(f, failOverPort, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
type auth interface {
|
||||
InitialBytes() ([]byte, error)
|
||||
NextBytes([]byte) ([]byte, error)
|
||||
@@ -1277,12 +816,12 @@ initiate_connection:
|
||||
// while SQL Server seems to expect one TCP segment per encrypted TDS package.
|
||||
// Setting DynamicRecordSizingDisabled to true disables that algorithm and uses 16384 bytes per TLS package
|
||||
config.DynamicRecordSizingDisabled = true
|
||||
outbuf.transport = conn
|
||||
toconn.buf = outbuf
|
||||
tlsConn := tls.Client(toconn, &config)
|
||||
// setting up connection handler which will allow wrapping of TLS handshake packets inside TDS stream
|
||||
handshakeConn := tlsHandshakeConn{buf: outbuf}
|
||||
passthrough := passthroughConn{c: &handshakeConn}
|
||||
tlsConn := tls.Client(&passthrough, &config)
|
||||
err = tlsConn.Handshake()
|
||||
|
||||
toconn.buf = nil
|
||||
passthrough.c = toconn
|
||||
outbuf.transport = tlsConn
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("TLS Handshake failed: %v", err)
|
||||
|
||||
+9
-9
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/denisenkom/go-mssqldb/internal/cp"
|
||||
"github.com/denisenkom/go-mssqldb/internal/decimal"
|
||||
)
|
||||
|
||||
// fixed-length data types
|
||||
@@ -818,12 +819,12 @@ func decodeMoney(buf []byte) []byte {
|
||||
uint64(buf[1])<<40 |
|
||||
uint64(buf[2])<<48 |
|
||||
uint64(buf[3])<<56)
|
||||
return scaleBytes(strconv.FormatInt(money, 10), 4)
|
||||
return decimal.ScaleBytes(strconv.FormatInt(money, 10), 4)
|
||||
}
|
||||
|
||||
func decodeMoney4(buf []byte) []byte {
|
||||
money := int32(binary.LittleEndian.Uint32(buf[0:4]))
|
||||
return scaleBytes(strconv.FormatInt(int64(money), 10), 4)
|
||||
return decimal.ScaleBytes(strconv.FormatInt(int64(money), 10), 4)
|
||||
}
|
||||
|
||||
func decodeGuid(buf []byte) []byte {
|
||||
@@ -835,15 +836,14 @@ func decodeGuid(buf []byte) []byte {
|
||||
func decodeDecimal(prec uint8, scale uint8, buf []byte) []byte {
|
||||
var sign uint8
|
||||
sign = buf[0]
|
||||
dec := Decimal{
|
||||
positive: sign != 0,
|
||||
prec: prec,
|
||||
scale: scale,
|
||||
}
|
||||
var dec decimal.Decimal
|
||||
dec.SetPositive(sign != 0)
|
||||
dec.SetPrec(prec)
|
||||
dec.SetScale(scale)
|
||||
buf = buf[1:]
|
||||
l := len(buf) / 4
|
||||
for i := 0; i < l; i++ {
|
||||
dec.integer[i] = binary.LittleEndian.Uint32(buf[0:4])
|
||||
dec.SetInteger(binary.LittleEndian.Uint32(buf[0:4]), uint8(i))
|
||||
buf = buf[4:]
|
||||
}
|
||||
return dec.Bytes()
|
||||
@@ -1186,7 +1186,7 @@ func makeDecl(ti typeInfo) string {
|
||||
case typeBigChar, typeChar:
|
||||
return fmt.Sprintf("char(%d)", ti.Size)
|
||||
case typeBigVarChar, typeVarChar:
|
||||
if ti.Size > 4000 || ti.Size == 0 {
|
||||
if ti.Size > 8000 || ti.Size == 0 {
|
||||
return fmt.Sprintf("varchar(max)")
|
||||
} else {
|
||||
return fmt.Sprintf("varchar(%d)", ti.Size)
|
||||
|
||||
+6
@@ -72,3 +72,9 @@ func (u UniqueIdentifier) Value() (driver.Value, error) {
|
||||
func (u UniqueIdentifier) String() string {
|
||||
return fmt.Sprintf("%X-%X-%X-%X-%X", u[0:4], u[4:6], u[6:8], u[8:10], u[10:])
|
||||
}
|
||||
|
||||
// MarshalText converts Uniqueidentifier to bytes corresponding to the stringified hexadecimal representation of the Uniqueidentifier
|
||||
// e.g., "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA" -> [65 65 65 65 65 65 65 65 45 65 65 65 65 45 65 65 65 65 45 65 65 65 65 65 65 65 65 65 65 65 65]
|
||||
func (u UniqueIdentifier) MarshalText() []byte {
|
||||
return []byte(u.String())
|
||||
}
|
||||
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.6
|
||||
- 1.7
|
||||
- 1.8
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
Copyright (c) 2016 Felix Geisendörfer (felix@debuggable.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
.PHONY: ci generate clean
|
||||
|
||||
ci: clean generate
|
||||
go test -v ./...
|
||||
|
||||
generate:
|
||||
go generate .
|
||||
|
||||
clean:
|
||||
rm -rf *_generated*.go
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
# httpsnoop
|
||||
|
||||
Package httpsnoop provides an easy way to capture http related metrics (i.e.
|
||||
response time, bytes written, and http status code) from your application's
|
||||
http.Handlers.
|
||||
|
||||
Doing this requires non-trivial wrapping of the http.ResponseWriter interface,
|
||||
which is also exposed for users interested in a more low-level API.
|
||||
|
||||
[](https://godoc.org/github.com/felixge/httpsnoop)
|
||||
[](https://travis-ci.org/felixge/httpsnoop)
|
||||
|
||||
## Usage Example
|
||||
|
||||
```go
|
||||
// myH is your app's http handler, perhaps a http.ServeMux or similar.
|
||||
var myH http.Handler
|
||||
// wrappedH wraps myH in order to log every request.
|
||||
wrappedH := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
m := httpsnoop.CaptureMetrics(myH, w, r)
|
||||
log.Printf(
|
||||
"%s %s (code=%d dt=%s written=%d)",
|
||||
r.Method,
|
||||
r.URL,
|
||||
m.Code,
|
||||
m.Duration,
|
||||
m.Written,
|
||||
)
|
||||
})
|
||||
http.ListenAndServe(":8080", wrappedH)
|
||||
```
|
||||
|
||||
## Why this package exists
|
||||
|
||||
Instrumenting an application's http.Handler is surprisingly difficult.
|
||||
|
||||
However if you google for e.g. "capture ResponseWriter status code" you'll find
|
||||
lots of advise and code examples that suggest it to be a fairly trivial
|
||||
undertaking. Unfortunately everything I've seen so far has a high chance of
|
||||
breaking your application.
|
||||
|
||||
The main problem is that a `http.ResponseWriter` often implements additional
|
||||
interfaces such as `http.Flusher`, `http.CloseNotifier`, `http.Hijacker`, `http.Pusher`, and
|
||||
`io.ReaderFrom`. So the naive approach of just wrapping `http.ResponseWriter`
|
||||
in your own struct that also implements the `http.ResponseWriter` interface
|
||||
will hide the additional interfaces mentioned above. This has a high change of
|
||||
introducing subtle bugs into any non-trivial application.
|
||||
|
||||
Another approach I've seen people take is to return a struct that implements
|
||||
all of the interfaces above. However, that's also problematic, because it's
|
||||
difficult to fake some of these interfaces behaviors when the underlying
|
||||
`http.ResponseWriter` doesn't have an implementation. It's also dangerous,
|
||||
because an application may choose to operate differently, merely because it
|
||||
detects the presence of these additional interfaces.
|
||||
|
||||
This package solves this problem by checking which additional interfaces a
|
||||
`http.ResponseWriter` implements, returning a wrapped version implementing the
|
||||
exact same set of interfaces.
|
||||
|
||||
Additionally this package properly handles edge cases such as `WriteHeader` not
|
||||
being called, or called more than once, as well as concurrent calls to
|
||||
`http.ResponseWriter` methods, and even calls happening after the wrapped
|
||||
`ServeHTTP` has already returned.
|
||||
|
||||
Unfortunately this package is not perfect either. It's possible that it is
|
||||
still missing some interfaces provided by the go core (let me know if you find
|
||||
one), and it won't work for applications adding their own interfaces into the
|
||||
mix.
|
||||
|
||||
However, hopefully the explanation above has sufficiently scared you of rolling
|
||||
your own solution to this problem. httpsnoop may still break your application,
|
||||
but at least it tries to avoid it as much as possible.
|
||||
|
||||
Anyway, the real problem here is that smuggling additional interfaces inside
|
||||
`http.ResponseWriter` is a problematic design choice, but it probably goes as
|
||||
deep as the Go language specification itself. But that's okay, I still prefer
|
||||
Go over the alternatives ;).
|
||||
|
||||
## Performance
|
||||
|
||||
```
|
||||
BenchmarkBaseline-8 20000 94912 ns/op
|
||||
BenchmarkCaptureMetrics-8 20000 95461 ns/op
|
||||
```
|
||||
|
||||
As you can see, using `CaptureMetrics` on a vanilla http.Handler introduces an
|
||||
overhead of ~500 ns per http request on my machine. However, the margin of
|
||||
error appears to be larger than that, therefor it should be reasonable to
|
||||
assume that the overhead introduced by `CaptureMetrics` is absolutely
|
||||
negligible.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
-84
@@ -1,84 +0,0 @@
|
||||
package httpsnoop
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Metrics holds metrics captured from CaptureMetrics.
|
||||
type Metrics struct {
|
||||
// Code is the first http response code passed to the WriteHeader func of
|
||||
// the ResponseWriter. If no such call is made, a default code of 200 is
|
||||
// assumed instead.
|
||||
Code int
|
||||
// Duration is the time it took to execute the handler.
|
||||
Duration time.Duration
|
||||
// Written is the number of bytes successfully written by the Write or
|
||||
// ReadFrom function of the ResponseWriter. ResponseWriters may also write
|
||||
// data to their underlaying connection directly (e.g. headers), but those
|
||||
// are not tracked. Therefor the number of Written bytes will usually match
|
||||
// the size of the response body.
|
||||
Written int64
|
||||
}
|
||||
|
||||
// CaptureMetrics wraps the given hnd, executes it with the given w and r, and
|
||||
// returns the metrics it captured from it.
|
||||
func CaptureMetrics(hnd http.Handler, w http.ResponseWriter, r *http.Request) Metrics {
|
||||
return CaptureMetricsFn(w, func(ww http.ResponseWriter) {
|
||||
hnd.ServeHTTP(ww, r)
|
||||
})
|
||||
}
|
||||
|
||||
// CaptureMetricsFn wraps w and calls fn with the wrapped w and returns the
|
||||
// resulting metrics. This is very similar to CaptureMetrics (which is just
|
||||
// sugar on top of this func), but is a more usable interface if your
|
||||
// application doesn't use the Go http.Handler interface.
|
||||
func CaptureMetricsFn(w http.ResponseWriter, fn func(http.ResponseWriter)) Metrics {
|
||||
var (
|
||||
start = time.Now()
|
||||
m = Metrics{Code: http.StatusOK}
|
||||
headerWritten bool
|
||||
lock sync.Mutex
|
||||
hooks = Hooks{
|
||||
WriteHeader: func(next WriteHeaderFunc) WriteHeaderFunc {
|
||||
return func(code int) {
|
||||
next(code)
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
if !headerWritten {
|
||||
m.Code = code
|
||||
headerWritten = true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Write: func(next WriteFunc) WriteFunc {
|
||||
return func(p []byte) (int, error) {
|
||||
n, err := next(p)
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
m.Written += int64(n)
|
||||
headerWritten = true
|
||||
return n, err
|
||||
}
|
||||
},
|
||||
|
||||
ReadFrom: func(next ReadFromFunc) ReadFromFunc {
|
||||
return func(src io.Reader) (int64, error) {
|
||||
n, err := next(src)
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
headerWritten = true
|
||||
m.Written += n
|
||||
return n, err
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
fn(Wrap(w, hooks))
|
||||
m.Duration = time.Since(start)
|
||||
return m
|
||||
}
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
// Package httpsnoop provides an easy way to capture http related metrics (i.e.
|
||||
// response time, bytes written, and http status code) from your application's
|
||||
// http.Handlers.
|
||||
//
|
||||
// Doing this requires non-trivial wrapping of the http.ResponseWriter
|
||||
// interface, which is also exposed for users interested in a more low-level
|
||||
// API.
|
||||
package httpsnoop
|
||||
|
||||
//go:generate go run codegen/main.go
|
||||
-385
@@ -1,385 +0,0 @@
|
||||
// +build go1.8
|
||||
// Code generated by "httpsnoop/codegen"; DO NOT EDIT
|
||||
|
||||
package httpsnoop
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// HeaderFunc is part of the http.ResponseWriter interface.
|
||||
type HeaderFunc func() http.Header
|
||||
|
||||
// WriteHeaderFunc is part of the http.ResponseWriter interface.
|
||||
type WriteHeaderFunc func(code int)
|
||||
|
||||
// WriteFunc is part of the http.ResponseWriter interface.
|
||||
type WriteFunc func(b []byte) (int, error)
|
||||
|
||||
// FlushFunc is part of the http.Flusher interface.
|
||||
type FlushFunc func()
|
||||
|
||||
// CloseNotifyFunc is part of the http.CloseNotifier interface.
|
||||
type CloseNotifyFunc func() <-chan bool
|
||||
|
||||
// HijackFunc is part of the http.Hijacker interface.
|
||||
type HijackFunc func() (net.Conn, *bufio.ReadWriter, error)
|
||||
|
||||
// ReadFromFunc is part of the io.ReaderFrom interface.
|
||||
type ReadFromFunc func(src io.Reader) (int64, error)
|
||||
|
||||
// PushFunc is part of the http.Pusher interface.
|
||||
type PushFunc func(target string, opts *http.PushOptions) error
|
||||
|
||||
// Hooks defines a set of method interceptors for methods included in
|
||||
// http.ResponseWriter as well as some others. You can think of them as
|
||||
// middleware for the function calls they target. See Wrap for more details.
|
||||
type Hooks struct {
|
||||
Header func(HeaderFunc) HeaderFunc
|
||||
WriteHeader func(WriteHeaderFunc) WriteHeaderFunc
|
||||
Write func(WriteFunc) WriteFunc
|
||||
Flush func(FlushFunc) FlushFunc
|
||||
CloseNotify func(CloseNotifyFunc) CloseNotifyFunc
|
||||
Hijack func(HijackFunc) HijackFunc
|
||||
ReadFrom func(ReadFromFunc) ReadFromFunc
|
||||
Push func(PushFunc) PushFunc
|
||||
}
|
||||
|
||||
// Wrap returns a wrapped version of w that provides the exact same interface
|
||||
// as w. Specifically if w implements any combination of:
|
||||
//
|
||||
// - http.Flusher
|
||||
// - http.CloseNotifier
|
||||
// - http.Hijacker
|
||||
// - io.ReaderFrom
|
||||
// - http.Pusher
|
||||
//
|
||||
// The wrapped version will implement the exact same combination. If no hooks
|
||||
// are set, the wrapped version also behaves exactly as w. Hooks targeting
|
||||
// methods not supported by w are ignored. Any other hooks will intercept the
|
||||
// method they target and may modify the call's arguments and/or return values.
|
||||
// The CaptureMetrics implementation serves as a working example for how the
|
||||
// hooks can be used.
|
||||
func Wrap(w http.ResponseWriter, hooks Hooks) http.ResponseWriter {
|
||||
rw := &rw{w: w, h: hooks}
|
||||
_, i0 := w.(http.Flusher)
|
||||
_, i1 := w.(http.CloseNotifier)
|
||||
_, i2 := w.(http.Hijacker)
|
||||
_, i3 := w.(io.ReaderFrom)
|
||||
_, i4 := w.(http.Pusher)
|
||||
switch {
|
||||
// combination 1/32
|
||||
case !i0 && !i1 && !i2 && !i3 && !i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
}{rw}
|
||||
// combination 2/32
|
||||
case !i0 && !i1 && !i2 && !i3 && i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Pusher
|
||||
}{rw, rw}
|
||||
// combination 3/32
|
||||
case !i0 && !i1 && !i2 && i3 && !i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
io.ReaderFrom
|
||||
}{rw, rw}
|
||||
// combination 4/32
|
||||
case !i0 && !i1 && !i2 && i3 && i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
io.ReaderFrom
|
||||
http.Pusher
|
||||
}{rw, rw, rw}
|
||||
// combination 5/32
|
||||
case !i0 && !i1 && i2 && !i3 && !i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Hijacker
|
||||
}{rw, rw}
|
||||
// combination 6/32
|
||||
case !i0 && !i1 && i2 && !i3 && i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Hijacker
|
||||
http.Pusher
|
||||
}{rw, rw, rw}
|
||||
// combination 7/32
|
||||
case !i0 && !i1 && i2 && i3 && !i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Hijacker
|
||||
io.ReaderFrom
|
||||
}{rw, rw, rw}
|
||||
// combination 8/32
|
||||
case !i0 && !i1 && i2 && i3 && i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Hijacker
|
||||
io.ReaderFrom
|
||||
http.Pusher
|
||||
}{rw, rw, rw, rw}
|
||||
// combination 9/32
|
||||
case !i0 && i1 && !i2 && !i3 && !i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.CloseNotifier
|
||||
}{rw, rw}
|
||||
// combination 10/32
|
||||
case !i0 && i1 && !i2 && !i3 && i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.CloseNotifier
|
||||
http.Pusher
|
||||
}{rw, rw, rw}
|
||||
// combination 11/32
|
||||
case !i0 && i1 && !i2 && i3 && !i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.CloseNotifier
|
||||
io.ReaderFrom
|
||||
}{rw, rw, rw}
|
||||
// combination 12/32
|
||||
case !i0 && i1 && !i2 && i3 && i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.CloseNotifier
|
||||
io.ReaderFrom
|
||||
http.Pusher
|
||||
}{rw, rw, rw, rw}
|
||||
// combination 13/32
|
||||
case !i0 && i1 && i2 && !i3 && !i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.CloseNotifier
|
||||
http.Hijacker
|
||||
}{rw, rw, rw}
|
||||
// combination 14/32
|
||||
case !i0 && i1 && i2 && !i3 && i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.CloseNotifier
|
||||
http.Hijacker
|
||||
http.Pusher
|
||||
}{rw, rw, rw, rw}
|
||||
// combination 15/32
|
||||
case !i0 && i1 && i2 && i3 && !i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.CloseNotifier
|
||||
http.Hijacker
|
||||
io.ReaderFrom
|
||||
}{rw, rw, rw, rw}
|
||||
// combination 16/32
|
||||
case !i0 && i1 && i2 && i3 && i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.CloseNotifier
|
||||
http.Hijacker
|
||||
io.ReaderFrom
|
||||
http.Pusher
|
||||
}{rw, rw, rw, rw, rw}
|
||||
// combination 17/32
|
||||
case i0 && !i1 && !i2 && !i3 && !i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
}{rw, rw}
|
||||
// combination 18/32
|
||||
case i0 && !i1 && !i2 && !i3 && i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
http.Pusher
|
||||
}{rw, rw, rw}
|
||||
// combination 19/32
|
||||
case i0 && !i1 && !i2 && i3 && !i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
io.ReaderFrom
|
||||
}{rw, rw, rw}
|
||||
// combination 20/32
|
||||
case i0 && !i1 && !i2 && i3 && i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
io.ReaderFrom
|
||||
http.Pusher
|
||||
}{rw, rw, rw, rw}
|
||||
// combination 21/32
|
||||
case i0 && !i1 && i2 && !i3 && !i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
http.Hijacker
|
||||
}{rw, rw, rw}
|
||||
// combination 22/32
|
||||
case i0 && !i1 && i2 && !i3 && i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
http.Hijacker
|
||||
http.Pusher
|
||||
}{rw, rw, rw, rw}
|
||||
// combination 23/32
|
||||
case i0 && !i1 && i2 && i3 && !i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
http.Hijacker
|
||||
io.ReaderFrom
|
||||
}{rw, rw, rw, rw}
|
||||
// combination 24/32
|
||||
case i0 && !i1 && i2 && i3 && i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
http.Hijacker
|
||||
io.ReaderFrom
|
||||
http.Pusher
|
||||
}{rw, rw, rw, rw, rw}
|
||||
// combination 25/32
|
||||
case i0 && i1 && !i2 && !i3 && !i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
http.CloseNotifier
|
||||
}{rw, rw, rw}
|
||||
// combination 26/32
|
||||
case i0 && i1 && !i2 && !i3 && i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
http.CloseNotifier
|
||||
http.Pusher
|
||||
}{rw, rw, rw, rw}
|
||||
// combination 27/32
|
||||
case i0 && i1 && !i2 && i3 && !i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
http.CloseNotifier
|
||||
io.ReaderFrom
|
||||
}{rw, rw, rw, rw}
|
||||
// combination 28/32
|
||||
case i0 && i1 && !i2 && i3 && i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
http.CloseNotifier
|
||||
io.ReaderFrom
|
||||
http.Pusher
|
||||
}{rw, rw, rw, rw, rw}
|
||||
// combination 29/32
|
||||
case i0 && i1 && i2 && !i3 && !i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
http.CloseNotifier
|
||||
http.Hijacker
|
||||
}{rw, rw, rw, rw}
|
||||
// combination 30/32
|
||||
case i0 && i1 && i2 && !i3 && i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
http.CloseNotifier
|
||||
http.Hijacker
|
||||
http.Pusher
|
||||
}{rw, rw, rw, rw, rw}
|
||||
// combination 31/32
|
||||
case i0 && i1 && i2 && i3 && !i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
http.CloseNotifier
|
||||
http.Hijacker
|
||||
io.ReaderFrom
|
||||
}{rw, rw, rw, rw, rw}
|
||||
// combination 32/32
|
||||
case i0 && i1 && i2 && i3 && i4:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
http.CloseNotifier
|
||||
http.Hijacker
|
||||
io.ReaderFrom
|
||||
http.Pusher
|
||||
}{rw, rw, rw, rw, rw, rw}
|
||||
}
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
type rw struct {
|
||||
w http.ResponseWriter
|
||||
h Hooks
|
||||
}
|
||||
|
||||
func (w *rw) Header() http.Header {
|
||||
f := w.w.(http.ResponseWriter).Header
|
||||
if w.h.Header != nil {
|
||||
f = w.h.Header(f)
|
||||
}
|
||||
return f()
|
||||
}
|
||||
|
||||
func (w *rw) WriteHeader(code int) {
|
||||
f := w.w.(http.ResponseWriter).WriteHeader
|
||||
if w.h.WriteHeader != nil {
|
||||
f = w.h.WriteHeader(f)
|
||||
}
|
||||
f(code)
|
||||
}
|
||||
|
||||
func (w *rw) Write(b []byte) (int, error) {
|
||||
f := w.w.(http.ResponseWriter).Write
|
||||
if w.h.Write != nil {
|
||||
f = w.h.Write(f)
|
||||
}
|
||||
return f(b)
|
||||
}
|
||||
|
||||
func (w *rw) Flush() {
|
||||
f := w.w.(http.Flusher).Flush
|
||||
if w.h.Flush != nil {
|
||||
f = w.h.Flush(f)
|
||||
}
|
||||
f()
|
||||
}
|
||||
|
||||
func (w *rw) CloseNotify() <-chan bool {
|
||||
f := w.w.(http.CloseNotifier).CloseNotify
|
||||
if w.h.CloseNotify != nil {
|
||||
f = w.h.CloseNotify(f)
|
||||
}
|
||||
return f()
|
||||
}
|
||||
|
||||
func (w *rw) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
f := w.w.(http.Hijacker).Hijack
|
||||
if w.h.Hijack != nil {
|
||||
f = w.h.Hijack(f)
|
||||
}
|
||||
return f()
|
||||
}
|
||||
|
||||
func (w *rw) ReadFrom(src io.Reader) (int64, error) {
|
||||
f := w.w.(io.ReaderFrom).ReadFrom
|
||||
if w.h.ReadFrom != nil {
|
||||
f = w.h.ReadFrom(f)
|
||||
}
|
||||
return f(src)
|
||||
}
|
||||
|
||||
func (w *rw) Push(target string, opts *http.PushOptions) error {
|
||||
f := w.w.(http.Pusher).Push
|
||||
if w.h.Push != nil {
|
||||
f = w.h.Push(f)
|
||||
}
|
||||
return f(target, opts)
|
||||
}
|
||||
-243
@@ -1,243 +0,0 @@
|
||||
// +build !go1.8
|
||||
// Code generated by "httpsnoop/codegen"; DO NOT EDIT
|
||||
|
||||
package httpsnoop
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// HeaderFunc is part of the http.ResponseWriter interface.
|
||||
type HeaderFunc func() http.Header
|
||||
|
||||
// WriteHeaderFunc is part of the http.ResponseWriter interface.
|
||||
type WriteHeaderFunc func(code int)
|
||||
|
||||
// WriteFunc is part of the http.ResponseWriter interface.
|
||||
type WriteFunc func(b []byte) (int, error)
|
||||
|
||||
// FlushFunc is part of the http.Flusher interface.
|
||||
type FlushFunc func()
|
||||
|
||||
// CloseNotifyFunc is part of the http.CloseNotifier interface.
|
||||
type CloseNotifyFunc func() <-chan bool
|
||||
|
||||
// HijackFunc is part of the http.Hijacker interface.
|
||||
type HijackFunc func() (net.Conn, *bufio.ReadWriter, error)
|
||||
|
||||
// ReadFromFunc is part of the io.ReaderFrom interface.
|
||||
type ReadFromFunc func(src io.Reader) (int64, error)
|
||||
|
||||
// Hooks defines a set of method interceptors for methods included in
|
||||
// http.ResponseWriter as well as some others. You can think of them as
|
||||
// middleware for the function calls they target. See Wrap for more details.
|
||||
type Hooks struct {
|
||||
Header func(HeaderFunc) HeaderFunc
|
||||
WriteHeader func(WriteHeaderFunc) WriteHeaderFunc
|
||||
Write func(WriteFunc) WriteFunc
|
||||
Flush func(FlushFunc) FlushFunc
|
||||
CloseNotify func(CloseNotifyFunc) CloseNotifyFunc
|
||||
Hijack func(HijackFunc) HijackFunc
|
||||
ReadFrom func(ReadFromFunc) ReadFromFunc
|
||||
}
|
||||
|
||||
// Wrap returns a wrapped version of w that provides the exact same interface
|
||||
// as w. Specifically if w implements any combination of:
|
||||
//
|
||||
// - http.Flusher
|
||||
// - http.CloseNotifier
|
||||
// - http.Hijacker
|
||||
// - io.ReaderFrom
|
||||
//
|
||||
// The wrapped version will implement the exact same combination. If no hooks
|
||||
// are set, the wrapped version also behaves exactly as w. Hooks targeting
|
||||
// methods not supported by w are ignored. Any other hooks will intercept the
|
||||
// method they target and may modify the call's arguments and/or return values.
|
||||
// The CaptureMetrics implementation serves as a working example for how the
|
||||
// hooks can be used.
|
||||
func Wrap(w http.ResponseWriter, hooks Hooks) http.ResponseWriter {
|
||||
rw := &rw{w: w, h: hooks}
|
||||
_, i0 := w.(http.Flusher)
|
||||
_, i1 := w.(http.CloseNotifier)
|
||||
_, i2 := w.(http.Hijacker)
|
||||
_, i3 := w.(io.ReaderFrom)
|
||||
switch {
|
||||
// combination 1/16
|
||||
case !i0 && !i1 && !i2 && !i3:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
}{rw}
|
||||
// combination 2/16
|
||||
case !i0 && !i1 && !i2 && i3:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
io.ReaderFrom
|
||||
}{rw, rw}
|
||||
// combination 3/16
|
||||
case !i0 && !i1 && i2 && !i3:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Hijacker
|
||||
}{rw, rw}
|
||||
// combination 4/16
|
||||
case !i0 && !i1 && i2 && i3:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Hijacker
|
||||
io.ReaderFrom
|
||||
}{rw, rw, rw}
|
||||
// combination 5/16
|
||||
case !i0 && i1 && !i2 && !i3:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.CloseNotifier
|
||||
}{rw, rw}
|
||||
// combination 6/16
|
||||
case !i0 && i1 && !i2 && i3:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.CloseNotifier
|
||||
io.ReaderFrom
|
||||
}{rw, rw, rw}
|
||||
// combination 7/16
|
||||
case !i0 && i1 && i2 && !i3:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.CloseNotifier
|
||||
http.Hijacker
|
||||
}{rw, rw, rw}
|
||||
// combination 8/16
|
||||
case !i0 && i1 && i2 && i3:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.CloseNotifier
|
||||
http.Hijacker
|
||||
io.ReaderFrom
|
||||
}{rw, rw, rw, rw}
|
||||
// combination 9/16
|
||||
case i0 && !i1 && !i2 && !i3:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
}{rw, rw}
|
||||
// combination 10/16
|
||||
case i0 && !i1 && !i2 && i3:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
io.ReaderFrom
|
||||
}{rw, rw, rw}
|
||||
// combination 11/16
|
||||
case i0 && !i1 && i2 && !i3:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
http.Hijacker
|
||||
}{rw, rw, rw}
|
||||
// combination 12/16
|
||||
case i0 && !i1 && i2 && i3:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
http.Hijacker
|
||||
io.ReaderFrom
|
||||
}{rw, rw, rw, rw}
|
||||
// combination 13/16
|
||||
case i0 && i1 && !i2 && !i3:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
http.CloseNotifier
|
||||
}{rw, rw, rw}
|
||||
// combination 14/16
|
||||
case i0 && i1 && !i2 && i3:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
http.CloseNotifier
|
||||
io.ReaderFrom
|
||||
}{rw, rw, rw, rw}
|
||||
// combination 15/16
|
||||
case i0 && i1 && i2 && !i3:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
http.CloseNotifier
|
||||
http.Hijacker
|
||||
}{rw, rw, rw, rw}
|
||||
// combination 16/16
|
||||
case i0 && i1 && i2 && i3:
|
||||
return struct {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
http.CloseNotifier
|
||||
http.Hijacker
|
||||
io.ReaderFrom
|
||||
}{rw, rw, rw, rw, rw}
|
||||
}
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
type rw struct {
|
||||
w http.ResponseWriter
|
||||
h Hooks
|
||||
}
|
||||
|
||||
func (w *rw) Header() http.Header {
|
||||
f := w.w.(http.ResponseWriter).Header
|
||||
if w.h.Header != nil {
|
||||
f = w.h.Header(f)
|
||||
}
|
||||
return f()
|
||||
}
|
||||
|
||||
func (w *rw) WriteHeader(code int) {
|
||||
f := w.w.(http.ResponseWriter).WriteHeader
|
||||
if w.h.WriteHeader != nil {
|
||||
f = w.h.WriteHeader(f)
|
||||
}
|
||||
f(code)
|
||||
}
|
||||
|
||||
func (w *rw) Write(b []byte) (int, error) {
|
||||
f := w.w.(http.ResponseWriter).Write
|
||||
if w.h.Write != nil {
|
||||
f = w.h.Write(f)
|
||||
}
|
||||
return f(b)
|
||||
}
|
||||
|
||||
func (w *rw) Flush() {
|
||||
f := w.w.(http.Flusher).Flush
|
||||
if w.h.Flush != nil {
|
||||
f = w.h.Flush(f)
|
||||
}
|
||||
f()
|
||||
}
|
||||
|
||||
func (w *rw) CloseNotify() <-chan bool {
|
||||
f := w.w.(http.CloseNotifier).CloseNotify
|
||||
if w.h.CloseNotify != nil {
|
||||
f = w.h.CloseNotify(f)
|
||||
}
|
||||
return f()
|
||||
}
|
||||
|
||||
func (w *rw) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
f := w.w.(http.Hijacker).Hijack
|
||||
if w.h.Hijack != nil {
|
||||
f = w.h.Hijack(f)
|
||||
}
|
||||
return f()
|
||||
}
|
||||
|
||||
func (w *rw) ReadFrom(src io.Reader) (int64, error) {
|
||||
f := w.w.(io.ReaderFrom).ReadFrom
|
||||
if w.h.ReadFrom != nil {
|
||||
f = w.h.ReadFrom(f)
|
||||
}
|
||||
return f(src)
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
# Contributing
|
||||
|
||||
1. Sign one of the contributor license agreements below.
|
||||
|
||||
#### Running
|
||||
|
||||
Once you've done the necessary setup, you can run the integration tests by
|
||||
running:
|
||||
|
||||
``` sh
|
||||
$ go test -v github.com/golang-sql/civil
|
||||
```
|
||||
|
||||
## Contributor License Agreements
|
||||
|
||||
Before we can accept your pull requests you'll need to sign a Contributor
|
||||
License Agreement (CLA):
|
||||
|
||||
- **If you are an individual writing original source code** and **you own the
|
||||
intellectual property**, then you'll need to sign an [individual CLA][indvcla].
|
||||
- **If you work for a company that wants to allow you to contribute your
|
||||
work**, then you'll need to sign a [corporate CLA][corpcla].
|
||||
|
||||
You can sign these electronically (just scroll to the bottom). After that,
|
||||
we'll be able to accept your pull requests.
|
||||
|
||||
## Contributor Code of Conduct
|
||||
|
||||
As contributors and maintainers of this project,
|
||||
and in the interest of fostering an open and welcoming community,
|
||||
we pledge to respect all people who contribute through reporting issues,
|
||||
posting feature requests, updating documentation,
|
||||
submitting pull requests or patches, and other activities.
|
||||
|
||||
We are committed to making participation in this project
|
||||
a harassment-free experience for everyone,
|
||||
regardless of level of experience, gender, gender identity and expression,
|
||||
sexual orientation, disability, personal appearance,
|
||||
body size, race, ethnicity, age, religion, or nationality.
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery
|
||||
* Personal attacks
|
||||
* Trolling or insulting/derogatory comments
|
||||
* Public or private harassment
|
||||
* Publishing other's private information,
|
||||
such as physical or electronic
|
||||
addresses, without explicit permission
|
||||
* Other unethical or unprofessional conduct.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct.
|
||||
By adopting this Code of Conduct,
|
||||
project maintainers commit themselves to fairly and consistently
|
||||
applying these principles to every aspect of managing this project.
|
||||
Project maintainers who do not follow or enforce the Code of Conduct
|
||||
may be permanently removed from the project team.
|
||||
|
||||
This code of conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior
|
||||
may be reported by opening an issue
|
||||
or contacting one or more of the project maintainers.
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0,
|
||||
available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/)
|
||||
|
||||
[gcloudcli]: https://developers.google.com/cloud/sdk/gcloud/
|
||||
[indvcla]: https://developers.google.com/open-source/cla/individual
|
||||
[corpcla]: https://developers.google.com/open-source/cla/corporate
|
||||
+1
-1
@@ -199,4 +199,4 @@
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
limitations under the License.
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
# Civil Date and Time
|
||||
|
||||
[](https://godoc.org/github.com/golang-sql/civil)
|
||||
|
||||
Civil provides Date, Time of Day, and DateTime data types.
|
||||
|
||||
While there are many uses, using specific types when working
|
||||
with databases make is conceptually eaiser to understand what value
|
||||
is set in the remote system.
|
||||
|
||||
## Source
|
||||
|
||||
This civil package was extracted and forked from `cloud.google.com/go/civil`.
|
||||
As such the license and contributing requirements remain the same as that
|
||||
module.
|
||||
Generated
Vendored
-27
@@ -1,27 +0,0 @@
|
||||
Copyright (c) 2013 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
Copyright (c) 2013 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
-298
@@ -1,298 +0,0 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd.
|
||||
|
||||
// Package header provides functions for parsing HTTP headers.
|
||||
package header
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Octet types from RFC 2616.
|
||||
var octetTypes [256]octetType
|
||||
|
||||
type octetType byte
|
||||
|
||||
const (
|
||||
isToken octetType = 1 << iota
|
||||
isSpace
|
||||
)
|
||||
|
||||
func init() {
|
||||
// OCTET = <any 8-bit sequence of data>
|
||||
// CHAR = <any US-ASCII character (octets 0 - 127)>
|
||||
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
|
||||
// CR = <US-ASCII CR, carriage return (13)>
|
||||
// LF = <US-ASCII LF, linefeed (10)>
|
||||
// SP = <US-ASCII SP, space (32)>
|
||||
// HT = <US-ASCII HT, horizontal-tab (9)>
|
||||
// <"> = <US-ASCII double-quote mark (34)>
|
||||
// CRLF = CR LF
|
||||
// LWS = [CRLF] 1*( SP | HT )
|
||||
// TEXT = <any OCTET except CTLs, but including LWS>
|
||||
// separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
|
||||
// | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
|
||||
// token = 1*<any CHAR except CTLs or separators>
|
||||
// qdtext = <any TEXT except <">>
|
||||
|
||||
for c := 0; c < 256; c++ {
|
||||
var t octetType
|
||||
isCtl := c <= 31 || c == 127
|
||||
isChar := 0 <= c && c <= 127
|
||||
isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0
|
||||
if strings.IndexRune(" \t\r\n", rune(c)) >= 0 {
|
||||
t |= isSpace
|
||||
}
|
||||
if isChar && !isCtl && !isSeparator {
|
||||
t |= isToken
|
||||
}
|
||||
octetTypes[c] = t
|
||||
}
|
||||
}
|
||||
|
||||
// Copy returns a shallow copy of the header.
|
||||
func Copy(header http.Header) http.Header {
|
||||
h := make(http.Header)
|
||||
for k, vs := range header {
|
||||
h[k] = vs
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
var timeLayouts = []string{"Mon, 02 Jan 2006 15:04:05 GMT", time.RFC850, time.ANSIC}
|
||||
|
||||
// ParseTime parses the header as time. The zero value is returned if the
|
||||
// header is not present or there is an error parsing the
|
||||
// header.
|
||||
func ParseTime(header http.Header, key string) time.Time {
|
||||
if s := header.Get(key); s != "" {
|
||||
for _, layout := range timeLayouts {
|
||||
if t, err := time.Parse(layout, s); err == nil {
|
||||
return t.UTC()
|
||||
}
|
||||
}
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// ParseList parses a comma separated list of values. Commas are ignored in
|
||||
// quoted strings. Quoted values are not unescaped or unquoted. Whitespace is
|
||||
// trimmed.
|
||||
func ParseList(header http.Header, key string) []string {
|
||||
var result []string
|
||||
for _, s := range header[http.CanonicalHeaderKey(key)] {
|
||||
begin := 0
|
||||
end := 0
|
||||
escape := false
|
||||
quote := false
|
||||
for i := 0; i < len(s); i++ {
|
||||
b := s[i]
|
||||
switch {
|
||||
case escape:
|
||||
escape = false
|
||||
end = i + 1
|
||||
case quote:
|
||||
switch b {
|
||||
case '\\':
|
||||
escape = true
|
||||
case '"':
|
||||
quote = false
|
||||
}
|
||||
end = i + 1
|
||||
case b == '"':
|
||||
quote = true
|
||||
end = i + 1
|
||||
case octetTypes[b]&isSpace != 0:
|
||||
if begin == end {
|
||||
begin = i + 1
|
||||
end = begin
|
||||
}
|
||||
case b == ',':
|
||||
if begin < end {
|
||||
result = append(result, s[begin:end])
|
||||
}
|
||||
begin = i + 1
|
||||
end = begin
|
||||
default:
|
||||
end = i + 1
|
||||
}
|
||||
}
|
||||
if begin < end {
|
||||
result = append(result, s[begin:end])
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ParseValueAndParams parses a comma separated list of values with optional
|
||||
// semicolon separated name-value pairs. Content-Type and Content-Disposition
|
||||
// headers are in this format.
|
||||
func ParseValueAndParams(header http.Header, key string) (value string, params map[string]string) {
|
||||
params = make(map[string]string)
|
||||
s := header.Get(key)
|
||||
value, s = expectTokenSlash(s)
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
value = strings.ToLower(value)
|
||||
s = skipSpace(s)
|
||||
for strings.HasPrefix(s, ";") {
|
||||
var pkey string
|
||||
pkey, s = expectToken(skipSpace(s[1:]))
|
||||
if pkey == "" {
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(s, "=") {
|
||||
return
|
||||
}
|
||||
var pvalue string
|
||||
pvalue, s = expectTokenOrQuoted(s[1:])
|
||||
if pvalue == "" {
|
||||
return
|
||||
}
|
||||
pkey = strings.ToLower(pkey)
|
||||
params[pkey] = pvalue
|
||||
s = skipSpace(s)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// AcceptSpec describes an Accept* header.
|
||||
type AcceptSpec struct {
|
||||
Value string
|
||||
Q float64
|
||||
}
|
||||
|
||||
// ParseAccept parses Accept* headers.
|
||||
func ParseAccept(header http.Header, key string) (specs []AcceptSpec) {
|
||||
loop:
|
||||
for _, s := range header[key] {
|
||||
for {
|
||||
var spec AcceptSpec
|
||||
spec.Value, s = expectTokenSlash(s)
|
||||
if spec.Value == "" {
|
||||
continue loop
|
||||
}
|
||||
spec.Q = 1.0
|
||||
s = skipSpace(s)
|
||||
if strings.HasPrefix(s, ";") {
|
||||
s = skipSpace(s[1:])
|
||||
if !strings.HasPrefix(s, "q=") {
|
||||
continue loop
|
||||
}
|
||||
spec.Q, s = expectQuality(s[2:])
|
||||
if spec.Q < 0.0 {
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
specs = append(specs, spec)
|
||||
s = skipSpace(s)
|
||||
if !strings.HasPrefix(s, ",") {
|
||||
continue loop
|
||||
}
|
||||
s = skipSpace(s[1:])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func skipSpace(s string) (rest string) {
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
if octetTypes[s[i]]&isSpace == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return s[i:]
|
||||
}
|
||||
|
||||
func expectToken(s string) (token, rest string) {
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
if octetTypes[s[i]]&isToken == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return s[:i], s[i:]
|
||||
}
|
||||
|
||||
func expectTokenSlash(s string) (token, rest string) {
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
b := s[i]
|
||||
if (octetTypes[b]&isToken == 0) && b != '/' {
|
||||
break
|
||||
}
|
||||
}
|
||||
return s[:i], s[i:]
|
||||
}
|
||||
|
||||
func expectQuality(s string) (q float64, rest string) {
|
||||
switch {
|
||||
case len(s) == 0:
|
||||
return -1, ""
|
||||
case s[0] == '0':
|
||||
q = 0
|
||||
case s[0] == '1':
|
||||
q = 1
|
||||
default:
|
||||
return -1, ""
|
||||
}
|
||||
s = s[1:]
|
||||
if !strings.HasPrefix(s, ".") {
|
||||
return q, s
|
||||
}
|
||||
s = s[1:]
|
||||
i := 0
|
||||
n := 0
|
||||
d := 1
|
||||
for ; i < len(s); i++ {
|
||||
b := s[i]
|
||||
if b < '0' || b > '9' {
|
||||
break
|
||||
}
|
||||
n = n*10 + int(b) - '0'
|
||||
d *= 10
|
||||
}
|
||||
return q + float64(n)/float64(d), s[i:]
|
||||
}
|
||||
|
||||
func expectTokenOrQuoted(s string) (value string, rest string) {
|
||||
if !strings.HasPrefix(s, "\"") {
|
||||
return expectToken(s)
|
||||
}
|
||||
s = s[1:]
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '"':
|
||||
return s[:i], s[i+1:]
|
||||
case '\\':
|
||||
p := make([]byte, len(s)-1)
|
||||
j := copy(p, s[:i])
|
||||
escape := true
|
||||
for i = i + 1; i < len(s); i++ {
|
||||
b := s[i]
|
||||
switch {
|
||||
case escape:
|
||||
escape = false
|
||||
p[j] = b
|
||||
j++
|
||||
case b == '\\':
|
||||
escape = true
|
||||
case b == '"':
|
||||
return string(p[:j]), s[i+1:]
|
||||
default:
|
||||
p[j] = b
|
||||
j++
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
+1
-1
@@ -22,4 +22,4 @@ _testmain.go
|
||||
*.exe
|
||||
|
||||
.idea/
|
||||
*.iml
|
||||
*.iml
|
||||
|
||||
+5
-5
@@ -3,11 +3,11 @@ sudo: false
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- go: 1.4
|
||||
- go: 1.5
|
||||
- go: 1.6
|
||||
- go: 1.7
|
||||
- go: 1.8
|
||||
- go: 1.7.x
|
||||
- go: 1.8.x
|
||||
- go: 1.9.x
|
||||
- go: 1.10.x
|
||||
- go: 1.11.x
|
||||
- go: tip
|
||||
allow_failures:
|
||||
- go: tip
|
||||
|
||||
+1
@@ -4,5 +4,6 @@
|
||||
# Please keep the list sorted.
|
||||
|
||||
Gary Burd <gary@beagledreams.com>
|
||||
Google LLC (https://opensource.google.com/)
|
||||
Joachim Bauch <mail@joachim-bauch.de>
|
||||
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn
|
||||
<tr><td>Write message using io.WriteCloser</td><td><a href="http://godoc.org/github.com/gorilla/websocket#Conn.NextWriter">Yes</a></td><td>No, see note 3</td></tr>
|
||||
</table>
|
||||
|
||||
Notes:
|
||||
Notes:
|
||||
|
||||
1. Large messages are fragmented in [Chrome's new WebSocket implementation](http://www.ietf.org/mail-archive/web/hybi/current/msg10503.html).
|
||||
2. The application can get the type of a received data message by implementing
|
||||
|
||||
+128
-125
@@ -5,15 +5,15 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -53,6 +53,10 @@ type Dialer struct {
|
||||
// NetDial is nil, net.Dial is used.
|
||||
NetDial func(network, addr string) (net.Conn, error)
|
||||
|
||||
// NetDialContext specifies the dial function for creating TCP connections. If
|
||||
// NetDialContext is nil, net.DialContext is used.
|
||||
NetDialContext func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
|
||||
// Proxy specifies a function to return a proxy for a given
|
||||
// Request. If the function returns a non-nil error, the
|
||||
// request is aborted with the provided error.
|
||||
@@ -71,6 +75,17 @@ type Dialer struct {
|
||||
// do not limit the size of the messages that can be sent or received.
|
||||
ReadBufferSize, WriteBufferSize int
|
||||
|
||||
// WriteBufferPool is a pool of buffers for write operations. If the value
|
||||
// is not set, then write buffers are allocated to the connection for the
|
||||
// lifetime of the connection.
|
||||
//
|
||||
// A pool is most useful when the application has a modest volume of writes
|
||||
// across a large number of connections.
|
||||
//
|
||||
// Applications should use a single pool for each unique value of
|
||||
// WriteBufferSize.
|
||||
WriteBufferPool BufferPool
|
||||
|
||||
// Subprotocols specifies the client's requested subprotocols.
|
||||
Subprotocols []string
|
||||
|
||||
@@ -86,52 +101,13 @@ type Dialer struct {
|
||||
Jar http.CookieJar
|
||||
}
|
||||
|
||||
var errMalformedURL = errors.New("malformed ws or wss URL")
|
||||
|
||||
// parseURL parses the URL.
|
||||
//
|
||||
// This function is a replacement for the standard library url.Parse function.
|
||||
// In Go 1.4 and earlier, url.Parse loses information from the path.
|
||||
func parseURL(s string) (*url.URL, error) {
|
||||
// From the RFC:
|
||||
//
|
||||
// ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
|
||||
// wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]
|
||||
var u url.URL
|
||||
switch {
|
||||
case strings.HasPrefix(s, "ws://"):
|
||||
u.Scheme = "ws"
|
||||
s = s[len("ws://"):]
|
||||
case strings.HasPrefix(s, "wss://"):
|
||||
u.Scheme = "wss"
|
||||
s = s[len("wss://"):]
|
||||
default:
|
||||
return nil, errMalformedURL
|
||||
}
|
||||
|
||||
if i := strings.Index(s, "?"); i >= 0 {
|
||||
u.RawQuery = s[i+1:]
|
||||
s = s[:i]
|
||||
}
|
||||
|
||||
if i := strings.Index(s, "/"); i >= 0 {
|
||||
u.Opaque = s[i:]
|
||||
s = s[:i]
|
||||
} else {
|
||||
u.Opaque = "/"
|
||||
}
|
||||
|
||||
u.Host = s
|
||||
|
||||
if strings.Contains(u.Host, "@") {
|
||||
// Don't bother parsing user information because user information is
|
||||
// not allowed in websocket URIs.
|
||||
return nil, errMalformedURL
|
||||
}
|
||||
|
||||
return &u, nil
|
||||
// Dial creates a new client connection by calling DialContext with a background context.
|
||||
func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
|
||||
return d.DialContext(context.Background(), urlStr, requestHeader)
|
||||
}
|
||||
|
||||
var errMalformedURL = errors.New("malformed ws or wss URL")
|
||||
|
||||
func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) {
|
||||
hostPort = u.Host
|
||||
hostNoPort = u.Host
|
||||
@@ -150,26 +126,29 @@ func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) {
|
||||
return hostPort, hostNoPort
|
||||
}
|
||||
|
||||
// DefaultDialer is a dialer with all fields set to the default zero values.
|
||||
// DefaultDialer is a dialer with all fields set to the default values.
|
||||
var DefaultDialer = &Dialer{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
HandshakeTimeout: 45 * time.Second,
|
||||
}
|
||||
|
||||
// Dial creates a new client connection. Use requestHeader to specify the
|
||||
// nilDialer is dialer to use when receiver is nil.
|
||||
var nilDialer = *DefaultDialer
|
||||
|
||||
// DialContext creates a new client connection. Use requestHeader to specify the
|
||||
// origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie).
|
||||
// Use the response.Header to get the selected subprotocol
|
||||
// (Sec-WebSocket-Protocol) and cookies (Set-Cookie).
|
||||
//
|
||||
// The context will be used in the request and in the Dialer
|
||||
//
|
||||
// If the WebSocket handshake fails, ErrBadHandshake is returned along with a
|
||||
// non-nil *http.Response so that callers can handle redirects, authentication,
|
||||
// etcetera. The response body may not contain the entire response and does not
|
||||
// need to be closed by the application.
|
||||
func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
|
||||
|
||||
func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
|
||||
if d == nil {
|
||||
d = &Dialer{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
d = &nilDialer
|
||||
}
|
||||
|
||||
challengeKey, err := generateChallengeKey()
|
||||
@@ -177,7 +156,7 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
u, err := parseURL(urlStr)
|
||||
u, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -205,6 +184,7 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
|
||||
Header: make(http.Header),
|
||||
Host: u.Host,
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
// Set the cookies present in the cookie jar of the dialer
|
||||
if d.Jar != nil {
|
||||
@@ -237,45 +217,83 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
|
||||
k == "Sec-Websocket-Extensions" ||
|
||||
(k == "Sec-Websocket-Protocol" && len(d.Subprotocols) > 0):
|
||||
return nil, nil, errors.New("websocket: duplicate header not allowed: " + k)
|
||||
case k == "Sec-Websocket-Protocol":
|
||||
req.Header["Sec-WebSocket-Protocol"] = vs
|
||||
default:
|
||||
req.Header[k] = vs
|
||||
}
|
||||
}
|
||||
|
||||
if d.EnableCompression {
|
||||
req.Header.Set("Sec-Websocket-Extensions", "permessage-deflate; server_no_context_takeover; client_no_context_takeover")
|
||||
req.Header["Sec-WebSocket-Extensions"] = []string{"permessage-deflate; server_no_context_takeover; client_no_context_takeover"}
|
||||
}
|
||||
|
||||
if d.HandshakeTimeout != 0 {
|
||||
var cancel func()
|
||||
ctx, cancel = context.WithTimeout(ctx, d.HandshakeTimeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
// Get network dial function.
|
||||
var netDial func(network, add string) (net.Conn, error)
|
||||
|
||||
if d.NetDialContext != nil {
|
||||
netDial = func(network, addr string) (net.Conn, error) {
|
||||
return d.NetDialContext(ctx, network, addr)
|
||||
}
|
||||
} else if d.NetDial != nil {
|
||||
netDial = d.NetDial
|
||||
} else {
|
||||
netDialer := &net.Dialer{}
|
||||
netDial = func(network, addr string) (net.Conn, error) {
|
||||
return netDialer.DialContext(ctx, network, addr)
|
||||
}
|
||||
}
|
||||
|
||||
// If needed, wrap the dial function to set the connection deadline.
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
forwardDial := netDial
|
||||
netDial = func(network, addr string) (net.Conn, error) {
|
||||
c, err := forwardDial(network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = c.SetDeadline(deadline)
|
||||
if err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If needed, wrap the dial function to connect through a proxy.
|
||||
if d.Proxy != nil {
|
||||
proxyURL, err := d.Proxy(req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if proxyURL != nil {
|
||||
dialer, err := proxy_FromURL(proxyURL, netDialerFunc(netDial))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
netDial = dialer.Dial
|
||||
}
|
||||
}
|
||||
|
||||
hostPort, hostNoPort := hostPortNoPort(u)
|
||||
|
||||
var proxyURL *url.URL
|
||||
// Check wether the proxy method has been configured
|
||||
if d.Proxy != nil {
|
||||
proxyURL, err = d.Proxy(req)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
trace := httptrace.ContextClientTrace(ctx)
|
||||
if trace != nil && trace.GetConn != nil {
|
||||
trace.GetConn(hostPort)
|
||||
}
|
||||
|
||||
var targetHostPort string
|
||||
if proxyURL != nil {
|
||||
targetHostPort, _ = hostPortNoPort(proxyURL)
|
||||
} else {
|
||||
targetHostPort = hostPort
|
||||
netConn, err := netDial("tcp", hostPort)
|
||||
if trace != nil && trace.GotConn != nil {
|
||||
trace.GotConn(httptrace.GotConnInfo{
|
||||
Conn: netConn,
|
||||
})
|
||||
}
|
||||
|
||||
var deadline time.Time
|
||||
if d.HandshakeTimeout != 0 {
|
||||
deadline = time.Now().Add(d.HandshakeTimeout)
|
||||
}
|
||||
|
||||
netDial := d.NetDial
|
||||
if netDial == nil {
|
||||
netDialer := &net.Dialer{Deadline: deadline}
|
||||
netDial = netDialer.Dial
|
||||
}
|
||||
|
||||
netConn, err := netDial("tcp", targetHostPort)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -286,42 +304,6 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
|
||||
}
|
||||
}()
|
||||
|
||||
if err := netConn.SetDeadline(deadline); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if proxyURL != nil {
|
||||
connectHeader := make(http.Header)
|
||||
if user := proxyURL.User; user != nil {
|
||||
proxyUser := user.Username()
|
||||
if proxyPassword, passwordSet := user.Password(); passwordSet {
|
||||
credential := base64.StdEncoding.EncodeToString([]byte(proxyUser + ":" + proxyPassword))
|
||||
connectHeader.Set("Proxy-Authorization", "Basic "+credential)
|
||||
}
|
||||
}
|
||||
connectReq := &http.Request{
|
||||
Method: "CONNECT",
|
||||
URL: &url.URL{Opaque: hostPort},
|
||||
Host: hostPort,
|
||||
Header: connectHeader,
|
||||
}
|
||||
|
||||
connectReq.Write(netConn)
|
||||
|
||||
// Read response.
|
||||
// Okay to use and discard buffered reader here, because
|
||||
// TLS server will not speak until spoken to.
|
||||
br := bufio.NewReader(netConn)
|
||||
resp, err := http.ReadResponse(br, connectReq)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
f := strings.SplitN(resp.Status, " ", 2)
|
||||
return nil, nil, errors.New(f[1])
|
||||
}
|
||||
}
|
||||
|
||||
if u.Scheme == "https" {
|
||||
cfg := cloneTLSConfig(d.TLSClientConfig)
|
||||
if cfg.ServerName == "" {
|
||||
@@ -329,22 +311,31 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
|
||||
}
|
||||
tlsConn := tls.Client(netConn, cfg)
|
||||
netConn = tlsConn
|
||||
if err := tlsConn.Handshake(); err != nil {
|
||||
return nil, nil, err
|
||||
|
||||
var err error
|
||||
if trace != nil {
|
||||
err = doHandshakeWithTrace(trace, tlsConn, cfg)
|
||||
} else {
|
||||
err = doHandshake(tlsConn, cfg)
|
||||
}
|
||||
if !cfg.InsecureSkipVerify {
|
||||
if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize)
|
||||
conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize, d.WriteBufferPool, nil, nil)
|
||||
|
||||
if err := req.Write(netConn); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if trace != nil && trace.GotFirstResponseByte != nil {
|
||||
if peek, err := conn.br.Peek(1); err == nil && len(peek) == 1 {
|
||||
trace.GotFirstResponseByte()
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := http.ReadResponse(conn.br, req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -390,3 +381,15 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
|
||||
netConn = nil // to avoid close in defer.
|
||||
return conn, resp, nil
|
||||
}
|
||||
|
||||
func doHandshake(tlsConn *tls.Conn, cfg *tls.Config) error {
|
||||
if err := tlsConn.Handshake(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !cfg.InsecureSkipVerify {
|
||||
if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+95
-79
@@ -76,7 +76,7 @@ const (
|
||||
// is UTF-8 encoded text.
|
||||
PingMessage = 9
|
||||
|
||||
// PongMessage denotes a ping control message. The optional message payload
|
||||
// PongMessage denotes a pong control message. The optional message payload
|
||||
// is UTF-8 encoded text.
|
||||
PongMessage = 10
|
||||
)
|
||||
@@ -100,9 +100,8 @@ func (e *netError) Error() string { return e.msg }
|
||||
func (e *netError) Temporary() bool { return e.temporary }
|
||||
func (e *netError) Timeout() bool { return e.timeout }
|
||||
|
||||
// CloseError represents close frame.
|
||||
// CloseError represents a close message.
|
||||
type CloseError struct {
|
||||
|
||||
// Code is defined in RFC 6455, section 11.7.
|
||||
Code int
|
||||
|
||||
@@ -224,6 +223,20 @@ func isValidReceivedCloseCode(code int) bool {
|
||||
return validReceivedCloseCodes[code] || (code >= 3000 && code <= 4999)
|
||||
}
|
||||
|
||||
// BufferPool represents a pool of buffers. The *sync.Pool type satisfies this
|
||||
// interface. The type of the value stored in a pool is not specified.
|
||||
type BufferPool interface {
|
||||
// Get gets a value from the pool or returns nil if the pool is empty.
|
||||
Get() interface{}
|
||||
// Put adds a value to the pool.
|
||||
Put(interface{})
|
||||
}
|
||||
|
||||
// writePoolData is the type added to the write buffer pool. This wrapper is
|
||||
// used to prevent applications from peeking at and depending on the values
|
||||
// added to the pool.
|
||||
type writePoolData struct{ buf []byte }
|
||||
|
||||
// The Conn type represents a WebSocket connection.
|
||||
type Conn struct {
|
||||
conn net.Conn
|
||||
@@ -233,6 +246,8 @@ type Conn struct {
|
||||
// Write fields
|
||||
mu chan bool // used as mutex to protect write to conn
|
||||
writeBuf []byte // frame is constructed in this buffer.
|
||||
writePool BufferPool
|
||||
writeBufSize int
|
||||
writeDeadline time.Time
|
||||
writer io.WriteCloser // the current writer returned to the application
|
||||
isWriting bool // for best-effort concurrent write detection
|
||||
@@ -264,64 +279,29 @@ type Conn struct {
|
||||
newDecompressionReader func(io.Reader) io.ReadCloser
|
||||
}
|
||||
|
||||
func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int) *Conn {
|
||||
return newConnBRW(conn, isServer, readBufferSize, writeBufferSize, nil)
|
||||
}
|
||||
func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int, writeBufferPool BufferPool, br *bufio.Reader, writeBuf []byte) *Conn {
|
||||
|
||||
type writeHook struct {
|
||||
p []byte
|
||||
}
|
||||
|
||||
func (wh *writeHook) Write(p []byte) (int, error) {
|
||||
wh.p = p
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func newConnBRW(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int, brw *bufio.ReadWriter) *Conn {
|
||||
mu := make(chan bool, 1)
|
||||
mu <- true
|
||||
|
||||
var br *bufio.Reader
|
||||
if readBufferSize == 0 && brw != nil && brw.Reader != nil {
|
||||
// Reuse the supplied bufio.Reader if the buffer has a useful size.
|
||||
// This code assumes that peek on a reader returns
|
||||
// bufio.Reader.buf[:0].
|
||||
brw.Reader.Reset(conn)
|
||||
if p, err := brw.Reader.Peek(0); err == nil && cap(p) >= 256 {
|
||||
br = brw.Reader
|
||||
}
|
||||
}
|
||||
if br == nil {
|
||||
if readBufferSize == 0 {
|
||||
readBufferSize = defaultReadBufferSize
|
||||
}
|
||||
if readBufferSize < maxControlFramePayloadSize {
|
||||
} else if readBufferSize < maxControlFramePayloadSize {
|
||||
// must be large enough for control frame
|
||||
readBufferSize = maxControlFramePayloadSize
|
||||
}
|
||||
br = bufio.NewReaderSize(conn, readBufferSize)
|
||||
}
|
||||
|
||||
var writeBuf []byte
|
||||
if writeBufferSize == 0 && brw != nil && brw.Writer != nil {
|
||||
// Use the bufio.Writer's buffer if the buffer has a useful size. This
|
||||
// code assumes that bufio.Writer.buf[:1] is passed to the
|
||||
// bufio.Writer's underlying writer.
|
||||
var wh writeHook
|
||||
brw.Writer.Reset(&wh)
|
||||
brw.Writer.WriteByte(0)
|
||||
brw.Flush()
|
||||
if cap(wh.p) >= maxFrameHeaderSize+256 {
|
||||
writeBuf = wh.p[:cap(wh.p)]
|
||||
}
|
||||
}
|
||||
|
||||
if writeBuf == nil {
|
||||
if writeBufferSize == 0 {
|
||||
writeBufferSize = defaultWriteBufferSize
|
||||
}
|
||||
writeBuf = make([]byte, writeBufferSize+maxFrameHeaderSize)
|
||||
if writeBufferSize <= 0 {
|
||||
writeBufferSize = defaultWriteBufferSize
|
||||
}
|
||||
writeBufferSize += maxFrameHeaderSize
|
||||
|
||||
if writeBuf == nil && writeBufferPool == nil {
|
||||
writeBuf = make([]byte, writeBufferSize)
|
||||
}
|
||||
|
||||
mu := make(chan bool, 1)
|
||||
mu <- true
|
||||
c := &Conn{
|
||||
isServer: isServer,
|
||||
br: br,
|
||||
@@ -329,6 +309,8 @@ func newConnBRW(conn net.Conn, isServer bool, readBufferSize, writeBufferSize in
|
||||
mu: mu,
|
||||
readFinal: true,
|
||||
writeBuf: writeBuf,
|
||||
writePool: writeBufferPool,
|
||||
writeBufSize: writeBufferSize,
|
||||
enableWriteCompression: true,
|
||||
compressionLevel: defaultCompressionLevel,
|
||||
}
|
||||
@@ -343,7 +325,8 @@ func (c *Conn) Subprotocol() string {
|
||||
return c.subprotocol
|
||||
}
|
||||
|
||||
// Close closes the underlying network connection without sending or waiting for a close frame.
|
||||
// Close closes the underlying network connection without sending or waiting
|
||||
// for a close message.
|
||||
func (c *Conn) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
@@ -370,7 +353,16 @@ func (c *Conn) writeFatal(err error) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Conn) write(frameType int, deadline time.Time, bufs ...[]byte) error {
|
||||
func (c *Conn) read(n int) ([]byte, error) {
|
||||
p, err := c.br.Peek(n)
|
||||
if err == io.EOF {
|
||||
err = errUnexpectedEOF
|
||||
}
|
||||
c.br.Discard(len(p))
|
||||
return p, err
|
||||
}
|
||||
|
||||
func (c *Conn) write(frameType int, deadline time.Time, buf0, buf1 []byte) error {
|
||||
<-c.mu
|
||||
defer func() { c.mu <- true }()
|
||||
|
||||
@@ -382,15 +374,14 @@ func (c *Conn) write(frameType int, deadline time.Time, bufs ...[]byte) error {
|
||||
}
|
||||
|
||||
c.conn.SetWriteDeadline(deadline)
|
||||
for _, buf := range bufs {
|
||||
if len(buf) > 0 {
|
||||
_, err := c.conn.Write(buf)
|
||||
if err != nil {
|
||||
return c.writeFatal(err)
|
||||
}
|
||||
}
|
||||
if len(buf1) == 0 {
|
||||
_, err = c.conn.Write(buf0)
|
||||
} else {
|
||||
err = c.writeBufs(buf0, buf1)
|
||||
}
|
||||
if err != nil {
|
||||
return c.writeFatal(err)
|
||||
}
|
||||
|
||||
if frameType == CloseMessage {
|
||||
c.writeFatal(ErrCloseSent)
|
||||
}
|
||||
@@ -476,7 +467,19 @@ func (c *Conn) prepWrite(messageType int) error {
|
||||
c.writeErrMu.Lock()
|
||||
err := c.writeErr
|
||||
c.writeErrMu.Unlock()
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.writeBuf == nil {
|
||||
wpd, ok := c.writePool.Get().(writePoolData)
|
||||
if ok {
|
||||
c.writeBuf = wpd.buf
|
||||
} else {
|
||||
c.writeBuf = make([]byte, c.writeBufSize)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NextWriter returns a writer for the next message to send. The writer's Close
|
||||
@@ -484,6 +487,9 @@ func (c *Conn) prepWrite(messageType int) error {
|
||||
//
|
||||
// There can be at most one open writer on a connection. NextWriter closes the
|
||||
// previous writer if the application has not already done so.
|
||||
//
|
||||
// All message types (TextMessage, BinaryMessage, CloseMessage, PingMessage and
|
||||
// PongMessage) are supported.
|
||||
func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error) {
|
||||
if err := c.prepWrite(messageType); err != nil {
|
||||
return nil, err
|
||||
@@ -599,6 +605,10 @@ func (w *messageWriter) flushFrame(final bool, extra []byte) error {
|
||||
|
||||
if final {
|
||||
c.writer = nil
|
||||
if c.writePool != nil {
|
||||
c.writePool.Put(writePoolData{buf: c.writeBuf})
|
||||
c.writeBuf = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -764,7 +774,6 @@ func (c *Conn) SetWriteDeadline(t time.Time) error {
|
||||
// Read methods
|
||||
|
||||
func (c *Conn) advanceFrame() (int, error) {
|
||||
|
||||
// 1. Skip remainder of previous frame.
|
||||
|
||||
if c.readRemaining > 0 {
|
||||
@@ -1033,7 +1042,7 @@ func (c *Conn) SetReadDeadline(t time.Time) error {
|
||||
}
|
||||
|
||||
// SetReadLimit sets the maximum size for a message read from the peer. If a
|
||||
// message exceeds the limit, the connection sends a close frame to the peer
|
||||
// message exceeds the limit, the connection sends a close message to the peer
|
||||
// and returns ErrReadLimit to the application.
|
||||
func (c *Conn) SetReadLimit(limit int64) {
|
||||
c.readLimit = limit
|
||||
@@ -1046,24 +1055,22 @@ func (c *Conn) CloseHandler() func(code int, text string) error {
|
||||
|
||||
// SetCloseHandler sets the handler for close messages received from the peer.
|
||||
// The code argument to h is the received close code or CloseNoStatusReceived
|
||||
// if the close message is empty. The default close handler sends a close frame
|
||||
// back to the peer.
|
||||
// if the close message is empty. The default close handler sends a close
|
||||
// message back to the peer.
|
||||
//
|
||||
// The application must read the connection to process close messages as
|
||||
// described in the section on Control Frames above.
|
||||
// The handler function is called from the NextReader, ReadMessage and message
|
||||
// reader Read methods. The application must read the connection to process
|
||||
// close messages as described in the section on Control Messages above.
|
||||
//
|
||||
// The connection read methods return a CloseError when a close frame is
|
||||
// The connection read methods return a CloseError when a close message is
|
||||
// received. Most applications should handle close messages as part of their
|
||||
// normal error handling. Applications should only set a close handler when the
|
||||
// application must perform some action before sending a close frame back to
|
||||
// application must perform some action before sending a close message back to
|
||||
// the peer.
|
||||
func (c *Conn) SetCloseHandler(h func(code int, text string) error) {
|
||||
if h == nil {
|
||||
h = func(code int, text string) error {
|
||||
message := []byte{}
|
||||
if code != CloseNoStatusReceived {
|
||||
message = FormatCloseMessage(code, "")
|
||||
}
|
||||
message := FormatCloseMessage(code, "")
|
||||
c.WriteControl(CloseMessage, message, time.Now().Add(writeWait))
|
||||
return nil
|
||||
}
|
||||
@@ -1077,11 +1084,12 @@ func (c *Conn) PingHandler() func(appData string) error {
|
||||
}
|
||||
|
||||
// SetPingHandler sets the handler for ping messages received from the peer.
|
||||
// The appData argument to h is the PING frame application data. The default
|
||||
// The appData argument to h is the PING message application data. The default
|
||||
// ping handler sends a pong to the peer.
|
||||
//
|
||||
// The application must read the connection to process ping messages as
|
||||
// described in the section on Control Frames above.
|
||||
// The handler function is called from the NextReader, ReadMessage and message
|
||||
// reader Read methods. The application must read the connection to process
|
||||
// ping messages as described in the section on Control Messages above.
|
||||
func (c *Conn) SetPingHandler(h func(appData string) error) {
|
||||
if h == nil {
|
||||
h = func(message string) error {
|
||||
@@ -1103,11 +1111,12 @@ func (c *Conn) PongHandler() func(appData string) error {
|
||||
}
|
||||
|
||||
// SetPongHandler sets the handler for pong messages received from the peer.
|
||||
// The appData argument to h is the PONG frame application data. The default
|
||||
// The appData argument to h is the PONG message application data. The default
|
||||
// pong handler does nothing.
|
||||
//
|
||||
// The application must read the connection to process ping messages as
|
||||
// described in the section on Control Frames above.
|
||||
// The handler function is called from the NextReader, ReadMessage and message
|
||||
// reader Read methods. The application must read the connection to process
|
||||
// pong messages as described in the section on Control Messages above.
|
||||
func (c *Conn) SetPongHandler(h func(appData string) error) {
|
||||
if h == nil {
|
||||
h = func(string) error { return nil }
|
||||
@@ -1141,7 +1150,14 @@ func (c *Conn) SetCompressionLevel(level int) error {
|
||||
}
|
||||
|
||||
// FormatCloseMessage formats closeCode and text as a WebSocket close message.
|
||||
// An empty message is returned for code CloseNoStatusReceived.
|
||||
func FormatCloseMessage(closeCode int, text string) []byte {
|
||||
if closeCode == CloseNoStatusReceived {
|
||||
// Return empty message because it's illegal to send
|
||||
// CloseNoStatusReceived. Return non-nil value in case application
|
||||
// checks for nil.
|
||||
return []byte{}
|
||||
}
|
||||
buf := make([]byte, 2+len(text))
|
||||
binary.BigEndian.PutUint16(buf, uint16(closeCode))
|
||||
copy(buf[2:], text)
|
||||
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !go1.5
|
||||
|
||||
package websocket
|
||||
|
||||
import "io"
|
||||
|
||||
func (c *Conn) read(n int) ([]byte, error) {
|
||||
p, err := c.br.Peek(n)
|
||||
if err == io.EOF {
|
||||
err = errUnexpectedEOF
|
||||
}
|
||||
if len(p) > 0 {
|
||||
// advance over the bytes just read
|
||||
io.ReadFull(c.br, p)
|
||||
}
|
||||
return p, err
|
||||
}
|
||||
vendor/github.com/gorilla/websocket/conn_read.go → vendor/github.com/gorilla/websocket/conn_write.go
Generated
Vendored
+6
-9
@@ -2,17 +2,14 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build go1.5
|
||||
// +build go1.8
|
||||
|
||||
package websocket
|
||||
|
||||
import "io"
|
||||
import "net"
|
||||
|
||||
func (c *Conn) read(n int) ([]byte, error) {
|
||||
p, err := c.br.Peek(n)
|
||||
if err == io.EOF {
|
||||
err = errUnexpectedEOF
|
||||
}
|
||||
c.br.Discard(len(p))
|
||||
return p, err
|
||||
func (c *Conn) writeBufs(bufs ...[]byte) error {
|
||||
b := net.Buffers(bufs)
|
||||
_, err := b.WriteTo(c.conn)
|
||||
return err
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !go1.8
|
||||
|
||||
package websocket
|
||||
|
||||
func (c *Conn) writeBufs(bufs ...[]byte) error {
|
||||
for _, buf := range bufs {
|
||||
if len(buf) > 0 {
|
||||
if _, err := c.conn.Write(buf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+28
-28
@@ -6,9 +6,8 @@
|
||||
//
|
||||
// Overview
|
||||
//
|
||||
// The Conn type represents a WebSocket connection. A server application uses
|
||||
// the Upgrade function from an Upgrader object with a HTTP request handler
|
||||
// to get a pointer to a Conn:
|
||||
// The Conn type represents a WebSocket connection. A server application calls
|
||||
// the Upgrader.Upgrade method from an HTTP request handler to get a *Conn:
|
||||
//
|
||||
// var upgrader = websocket.Upgrader{
|
||||
// ReadBufferSize: 1024,
|
||||
@@ -31,10 +30,12 @@
|
||||
// for {
|
||||
// messageType, p, err := conn.ReadMessage()
|
||||
// if err != nil {
|
||||
// log.Println(err)
|
||||
// return
|
||||
// }
|
||||
// if err = conn.WriteMessage(messageType, p); err != nil {
|
||||
// return err
|
||||
// if err := conn.WriteMessage(messageType, p); err != nil {
|
||||
// log.Println(err)
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
//
|
||||
@@ -85,20 +86,26 @@
|
||||
// and pong. Call the connection WriteControl, WriteMessage or NextWriter
|
||||
// methods to send a control message to the peer.
|
||||
//
|
||||
// Connections handle received close messages by sending a close message to the
|
||||
// peer and returning a *CloseError from the the NextReader, ReadMessage or the
|
||||
// message Read method.
|
||||
// Connections handle received close messages by calling the handler function
|
||||
// set with the SetCloseHandler method and by returning a *CloseError from the
|
||||
// NextReader, ReadMessage or the message Read method. The default close
|
||||
// handler sends a close message to the peer.
|
||||
//
|
||||
// Connections handle received ping and pong messages by invoking callback
|
||||
// functions set with SetPingHandler and SetPongHandler methods. The callback
|
||||
// functions are called from the NextReader, ReadMessage and the message Read
|
||||
// methods.
|
||||
// Connections handle received ping messages by calling the handler function
|
||||
// set with the SetPingHandler method. The default ping handler sends a pong
|
||||
// message to the peer.
|
||||
//
|
||||
// The default ping handler sends a pong to the peer. The application's reading
|
||||
// goroutine can block for a short time while the handler writes the pong data
|
||||
// to the connection.
|
||||
// Connections handle received pong messages by calling the handler function
|
||||
// set with the SetPongHandler method. The default pong handler does nothing.
|
||||
// If an application sends ping messages, then the application should set a
|
||||
// pong handler to receive the corresponding pong.
|
||||
//
|
||||
// The application must read the connection to process ping, pong and close
|
||||
// The control message handler functions are called from the NextReader,
|
||||
// ReadMessage and message reader Read methods. The default close and ping
|
||||
// handlers can block these methods for a short time when the handler writes to
|
||||
// the connection.
|
||||
//
|
||||
// The application must read the connection to process close, ping and pong
|
||||
// messages sent from the peer. If the application is not otherwise interested
|
||||
// in messages from the peer, then the application should start a goroutine to
|
||||
// read and discard messages from the peer. A simple example is:
|
||||
@@ -137,19 +144,12 @@
|
||||
// method fails the WebSocket handshake with HTTP status 403.
|
||||
//
|
||||
// If the CheckOrigin field is nil, then the Upgrader uses a safe default: fail
|
||||
// the handshake if the Origin request header is present and not equal to the
|
||||
// Host request header.
|
||||
// the handshake if the Origin request header is present and the Origin host is
|
||||
// not equal to the Host request header.
|
||||
//
|
||||
// An application can allow connections from any origin by specifying a
|
||||
// function that always returns true:
|
||||
//
|
||||
// var upgrader = websocket.Upgrader{
|
||||
// CheckOrigin: func(r *http.Request) bool { return true },
|
||||
// }
|
||||
//
|
||||
// The deprecated Upgrade function does not enforce an origin policy. It's the
|
||||
// application's responsibility to check the Origin header before calling
|
||||
// Upgrade.
|
||||
// The deprecated package-level Upgrade function does not perform origin
|
||||
// checking. The application is responsible for checking the Origin header
|
||||
// before calling the Upgrade function.
|
||||
//
|
||||
// Compression EXPERIMENTAL
|
||||
//
|
||||
|
||||
+8
-3
@@ -9,12 +9,14 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// WriteJSON is deprecated, use c.WriteJSON instead.
|
||||
// WriteJSON writes the JSON encoding of v as a message.
|
||||
//
|
||||
// Deprecated: Use c.WriteJSON instead.
|
||||
func WriteJSON(c *Conn, v interface{}) error {
|
||||
return c.WriteJSON(v)
|
||||
}
|
||||
|
||||
// WriteJSON writes the JSON encoding of v to the connection.
|
||||
// WriteJSON writes the JSON encoding of v as a message.
|
||||
//
|
||||
// See the documentation for encoding/json Marshal for details about the
|
||||
// conversion of Go values to JSON.
|
||||
@@ -31,7 +33,10 @@ func (c *Conn) WriteJSON(v interface{}) error {
|
||||
return err2
|
||||
}
|
||||
|
||||
// ReadJSON is deprecated, use c.ReadJSON instead.
|
||||
// ReadJSON reads the next JSON-encoded message from the connection and stores
|
||||
// it in the value pointed to by v.
|
||||
//
|
||||
// Deprecated: Use c.ReadJSON instead.
|
||||
func ReadJSON(c *Conn, v interface{}) error {
|
||||
return c.ReadJSON(v)
|
||||
}
|
||||
|
||||
-1
@@ -11,7 +11,6 @@ import "unsafe"
|
||||
const wordSize = int(unsafe.Sizeof(uintptr(0)))
|
||||
|
||||
func maskBytes(key [4]byte, pos int, b []byte) int {
|
||||
|
||||
// Mask one byte at a time for small buffers.
|
||||
if len(b) < 2*wordSize {
|
||||
for i := range b {
|
||||
|
||||
-1
@@ -19,7 +19,6 @@ import (
|
||||
type PreparedMessage struct {
|
||||
messageType int
|
||||
data []byte
|
||||
err error
|
||||
mu sync.Mutex
|
||||
frames map[prepareKey]*preparedFrame
|
||||
}
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type netDialerFunc func(network, addr string) (net.Conn, error)
|
||||
|
||||
func (fn netDialerFunc) Dial(network, addr string) (net.Conn, error) {
|
||||
return fn(network, addr)
|
||||
}
|
||||
|
||||
func init() {
|
||||
proxy_RegisterDialerType("http", func(proxyURL *url.URL, forwardDialer proxy_Dialer) (proxy_Dialer, error) {
|
||||
return &httpProxyDialer{proxyURL: proxyURL, fowardDial: forwardDialer.Dial}, nil
|
||||
})
|
||||
}
|
||||
|
||||
type httpProxyDialer struct {
|
||||
proxyURL *url.URL
|
||||
fowardDial func(network, addr string) (net.Conn, error)
|
||||
}
|
||||
|
||||
func (hpd *httpProxyDialer) Dial(network string, addr string) (net.Conn, error) {
|
||||
hostPort, _ := hostPortNoPort(hpd.proxyURL)
|
||||
conn, err := hpd.fowardDial(network, hostPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
connectHeader := make(http.Header)
|
||||
if user := hpd.proxyURL.User; user != nil {
|
||||
proxyUser := user.Username()
|
||||
if proxyPassword, passwordSet := user.Password(); passwordSet {
|
||||
credential := base64.StdEncoding.EncodeToString([]byte(proxyUser + ":" + proxyPassword))
|
||||
connectHeader.Set("Proxy-Authorization", "Basic "+credential)
|
||||
}
|
||||
}
|
||||
|
||||
connectReq := &http.Request{
|
||||
Method: "CONNECT",
|
||||
URL: &url.URL{Opaque: addr},
|
||||
Host: addr,
|
||||
Header: connectHeader,
|
||||
}
|
||||
|
||||
if err := connectReq.Write(conn); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read response. It's OK to use and discard buffered reader here becaue
|
||||
// the remote server does not speak until spoken to.
|
||||
br := bufio.NewReader(conn)
|
||||
resp, err := http.ReadResponse(br, connectReq)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
conn.Close()
|
||||
f := strings.SplitN(resp.Status, " ", 2)
|
||||
return nil, errors.New(f[1])
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
+104
-32
@@ -7,7 +7,7 @@ package websocket
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"net"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -33,10 +33,23 @@ type Upgrader struct {
|
||||
// or received.
|
||||
ReadBufferSize, WriteBufferSize int
|
||||
|
||||
// WriteBufferPool is a pool of buffers for write operations. If the value
|
||||
// is not set, then write buffers are allocated to the connection for the
|
||||
// lifetime of the connection.
|
||||
//
|
||||
// A pool is most useful when the application has a modest volume of writes
|
||||
// across a large number of connections.
|
||||
//
|
||||
// Applications should use a single pool for each unique value of
|
||||
// WriteBufferSize.
|
||||
WriteBufferPool BufferPool
|
||||
|
||||
// Subprotocols specifies the server's supported protocols in order of
|
||||
// preference. If this field is set, then the Upgrade method negotiates a
|
||||
// preference. If this field is not nil, then the Upgrade method negotiates a
|
||||
// subprotocol by selecting the first match in this list with a protocol
|
||||
// requested by the client.
|
||||
// requested by the client. If there's no match, then no protocol is
|
||||
// negotiated (the Sec-Websocket-Protocol header is not included in the
|
||||
// handshake response).
|
||||
Subprotocols []string
|
||||
|
||||
// Error specifies the function for generating HTTP error responses. If Error
|
||||
@@ -44,8 +57,12 @@ type Upgrader struct {
|
||||
Error func(w http.ResponseWriter, r *http.Request, status int, reason error)
|
||||
|
||||
// CheckOrigin returns true if the request Origin header is acceptable. If
|
||||
// CheckOrigin is nil, the host in the Origin header must not be set or
|
||||
// must match the host of the request.
|
||||
// CheckOrigin is nil, then a safe default is used: return false if the
|
||||
// Origin request header is present and the origin host is not equal to
|
||||
// request Host header.
|
||||
//
|
||||
// A CheckOrigin function should carefully validate the request origin to
|
||||
// prevent cross-site request forgery.
|
||||
CheckOrigin func(r *http.Request) bool
|
||||
|
||||
// EnableCompression specify if the server should attempt to negotiate per
|
||||
@@ -76,7 +93,7 @@ func checkSameOrigin(r *http.Request) bool {
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return u.Host == r.Host
|
||||
return equalASCIIFold(u.Host, r.Host)
|
||||
}
|
||||
|
||||
func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header) string {
|
||||
@@ -99,42 +116,44 @@ func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header
|
||||
//
|
||||
// The responseHeader is included in the response to the client's upgrade
|
||||
// request. Use the responseHeader to specify cookies (Set-Cookie) and the
|
||||
// application negotiated subprotocol (Sec-Websocket-Protocol).
|
||||
// application negotiated subprotocol (Sec-WebSocket-Protocol).
|
||||
//
|
||||
// If the upgrade fails, then Upgrade replies to the client with an HTTP error
|
||||
// response.
|
||||
func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) {
|
||||
if r.Method != "GET" {
|
||||
return u.returnError(w, r, http.StatusMethodNotAllowed, "websocket: not a websocket handshake: request method is not GET")
|
||||
}
|
||||
|
||||
if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok {
|
||||
return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-Websocket-Extensions' headers are unsupported")
|
||||
}
|
||||
const badHandshake = "websocket: the client is not using the websocket protocol: "
|
||||
|
||||
if !tokenListContainsValue(r.Header, "Connection", "upgrade") {
|
||||
return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'upgrade' token not found in 'Connection' header")
|
||||
return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'upgrade' token not found in 'Connection' header")
|
||||
}
|
||||
|
||||
if !tokenListContainsValue(r.Header, "Upgrade", "websocket") {
|
||||
return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'websocket' token not found in 'Upgrade' header")
|
||||
return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'websocket' token not found in 'Upgrade' header")
|
||||
}
|
||||
|
||||
if r.Method != "GET" {
|
||||
return u.returnError(w, r, http.StatusMethodNotAllowed, badHandshake+"request method is not GET")
|
||||
}
|
||||
|
||||
if !tokenListContainsValue(r.Header, "Sec-Websocket-Version", "13") {
|
||||
return u.returnError(w, r, http.StatusBadRequest, "websocket: unsupported version: 13 not found in 'Sec-Websocket-Version' header")
|
||||
}
|
||||
|
||||
if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok {
|
||||
return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-WebSocket-Extensions' headers are unsupported")
|
||||
}
|
||||
|
||||
checkOrigin := u.CheckOrigin
|
||||
if checkOrigin == nil {
|
||||
checkOrigin = checkSameOrigin
|
||||
}
|
||||
if !checkOrigin(r) {
|
||||
return u.returnError(w, r, http.StatusForbidden, "websocket: 'Origin' header value not allowed")
|
||||
return u.returnError(w, r, http.StatusForbidden, "websocket: request origin not allowed by Upgrader.CheckOrigin")
|
||||
}
|
||||
|
||||
challengeKey := r.Header.Get("Sec-Websocket-Key")
|
||||
if challengeKey == "" {
|
||||
return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: `Sec-Websocket-Key' header is missing or blank")
|
||||
return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: `Sec-WebSocket-Key' header is missing or blank")
|
||||
}
|
||||
|
||||
subprotocol := u.selectSubprotocol(r, responseHeader)
|
||||
@@ -151,17 +170,12 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
netConn net.Conn
|
||||
err error
|
||||
)
|
||||
|
||||
h, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker")
|
||||
}
|
||||
var brw *bufio.ReadWriter
|
||||
netConn, brw, err = h.Hijack()
|
||||
netConn, brw, err := h.Hijack()
|
||||
if err != nil {
|
||||
return u.returnError(w, r, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
@@ -171,7 +185,21 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
|
||||
return nil, errors.New("websocket: client sent data before handshake is complete")
|
||||
}
|
||||
|
||||
c := newConnBRW(netConn, true, u.ReadBufferSize, u.WriteBufferSize, brw)
|
||||
var br *bufio.Reader
|
||||
if u.ReadBufferSize == 0 && bufioReaderSize(netConn, brw.Reader) > 256 {
|
||||
// Reuse hijacked buffered reader as connection reader.
|
||||
br = brw.Reader
|
||||
}
|
||||
|
||||
buf := bufioWriterBuffer(netConn, brw.Writer)
|
||||
|
||||
var writeBuf []byte
|
||||
if u.WriteBufferPool == nil && u.WriteBufferSize == 0 && len(buf) >= maxFrameHeaderSize+256 {
|
||||
// Reuse hijacked write buffer as connection buffer.
|
||||
writeBuf = buf
|
||||
}
|
||||
|
||||
c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize, u.WriteBufferPool, br, writeBuf)
|
||||
c.subprotocol = subprotocol
|
||||
|
||||
if compress {
|
||||
@@ -179,17 +207,23 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
|
||||
c.newDecompressionReader = decompressNoContextTakeover
|
||||
}
|
||||
|
||||
p := c.writeBuf[:0]
|
||||
// Use larger of hijacked buffer and connection write buffer for header.
|
||||
p := buf
|
||||
if len(c.writeBuf) > len(p) {
|
||||
p = c.writeBuf
|
||||
}
|
||||
p = p[:0]
|
||||
|
||||
p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...)
|
||||
p = append(p, computeAcceptKey(challengeKey)...)
|
||||
p = append(p, "\r\n"...)
|
||||
if c.subprotocol != "" {
|
||||
p = append(p, "Sec-Websocket-Protocol: "...)
|
||||
p = append(p, "Sec-WebSocket-Protocol: "...)
|
||||
p = append(p, c.subprotocol...)
|
||||
p = append(p, "\r\n"...)
|
||||
}
|
||||
if compress {
|
||||
p = append(p, "Sec-Websocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...)
|
||||
p = append(p, "Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...)
|
||||
}
|
||||
for k, vs := range responseHeader {
|
||||
if k == "Sec-Websocket-Protocol" {
|
||||
@@ -230,13 +264,14 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
|
||||
|
||||
// Upgrade upgrades the HTTP server connection to the WebSocket protocol.
|
||||
//
|
||||
// This function is deprecated, use websocket.Upgrader instead.
|
||||
// Deprecated: Use websocket.Upgrader instead.
|
||||
//
|
||||
// The application is responsible for checking the request origin before
|
||||
// calling Upgrade. An example implementation of the same origin policy is:
|
||||
// Upgrade does not perform origin checking. The application is responsible for
|
||||
// checking the Origin header before calling Upgrade. An example implementation
|
||||
// of the same origin policy check is:
|
||||
//
|
||||
// if req.Header.Get("Origin") != "http://"+req.Host {
|
||||
// http.Error(w, "Origin not allowed", 403)
|
||||
// http.Error(w, "Origin not allowed", http.StatusForbidden)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
@@ -289,3 +324,40 @@ func IsWebSocketUpgrade(r *http.Request) bool {
|
||||
return tokenListContainsValue(r.Header, "Connection", "upgrade") &&
|
||||
tokenListContainsValue(r.Header, "Upgrade", "websocket")
|
||||
}
|
||||
|
||||
// bufioReaderSize size returns the size of a bufio.Reader.
|
||||
func bufioReaderSize(originalReader io.Reader, br *bufio.Reader) int {
|
||||
// This code assumes that peek on a reset reader returns
|
||||
// bufio.Reader.buf[:0].
|
||||
// TODO: Use bufio.Reader.Size() after Go 1.10
|
||||
br.Reset(originalReader)
|
||||
if p, err := br.Peek(0); err == nil {
|
||||
return cap(p)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// writeHook is an io.Writer that records the last slice passed to it vio
|
||||
// io.Writer.Write.
|
||||
type writeHook struct {
|
||||
p []byte
|
||||
}
|
||||
|
||||
func (wh *writeHook) Write(p []byte) (int, error) {
|
||||
wh.p = p
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// bufioWriterBuffer grabs the buffer from a bufio.Writer.
|
||||
func bufioWriterBuffer(originalWriter io.Writer, bw *bufio.Writer) []byte {
|
||||
// This code assumes that bufio.Writer.buf[:1] is passed to the
|
||||
// bufio.Writer's underlying writer.
|
||||
var wh writeHook
|
||||
bw.Reset(&wh)
|
||||
bw.WriteByte(0)
|
||||
bw.Flush()
|
||||
|
||||
bw.Reset(originalWriter)
|
||||
|
||||
return wh.p[:cap(wh.p)]
|
||||
}
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// +build go1.8
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http/httptrace"
|
||||
)
|
||||
|
||||
func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error {
|
||||
if trace.TLSHandshakeStart != nil {
|
||||
trace.TLSHandshakeStart()
|
||||
}
|
||||
err := doHandshake(tlsConn, cfg)
|
||||
if trace.TLSHandshakeDone != nil {
|
||||
trace.TLSHandshakeDone(tlsConn.ConnectionState(), err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// +build !go1.8
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http/httptrace"
|
||||
)
|
||||
|
||||
func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error {
|
||||
return doHandshake(tlsConn, cfg)
|
||||
}
|
||||
+29
-6
@@ -11,6 +11,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
|
||||
@@ -111,14 +112,14 @@ func nextTokenOrQuoted(s string) (value string, rest string) {
|
||||
case escape:
|
||||
escape = false
|
||||
p[j] = b
|
||||
j += 1
|
||||
j++
|
||||
case b == '\\':
|
||||
escape = true
|
||||
case b == '"':
|
||||
return string(p[:j]), s[i+1:]
|
||||
default:
|
||||
p[j] = b
|
||||
j += 1
|
||||
j++
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
@@ -127,8 +128,31 @@ func nextTokenOrQuoted(s string) (value string, rest string) {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// equalASCIIFold returns true if s is equal to t with ASCII case folding.
|
||||
func equalASCIIFold(s, t string) bool {
|
||||
for s != "" && t != "" {
|
||||
sr, size := utf8.DecodeRuneInString(s)
|
||||
s = s[size:]
|
||||
tr, size := utf8.DecodeRuneInString(t)
|
||||
t = t[size:]
|
||||
if sr == tr {
|
||||
continue
|
||||
}
|
||||
if 'A' <= sr && sr <= 'Z' {
|
||||
sr = sr + 'a' - 'A'
|
||||
}
|
||||
if 'A' <= tr && tr <= 'Z' {
|
||||
tr = tr + 'a' - 'A'
|
||||
}
|
||||
if sr != tr {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return s == t
|
||||
}
|
||||
|
||||
// tokenListContainsValue returns true if the 1#token header with the given
|
||||
// name contains token.
|
||||
// name contains a token equal to value with ASCII case folding.
|
||||
func tokenListContainsValue(header http.Header, name string, value string) bool {
|
||||
headers:
|
||||
for _, s := range header[name] {
|
||||
@@ -142,7 +166,7 @@ headers:
|
||||
if s != "" && s[0] != ',' {
|
||||
continue headers
|
||||
}
|
||||
if strings.EqualFold(t, value) {
|
||||
if equalASCIIFold(t, value) {
|
||||
return true
|
||||
}
|
||||
if s == "" {
|
||||
@@ -154,9 +178,8 @@ headers:
|
||||
return false
|
||||
}
|
||||
|
||||
// parseExtensiosn parses WebSocket extensions from a header.
|
||||
// parseExtensions parses WebSocket extensions from a header.
|
||||
func parseExtensions(header http.Header) []map[string]string {
|
||||
|
||||
// From RFC 6455:
|
||||
//
|
||||
// Sec-WebSocket-Extensions = extension-list
|
||||
|
||||
+473
@@ -0,0 +1,473 @@
|
||||
// Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT.
|
||||
//go:generate bundle -o x_net_proxy.go golang.org/x/net/proxy
|
||||
|
||||
// Package proxy provides support for a variety of protocols to proxy network
|
||||
// data.
|
||||
//
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type proxy_direct struct{}
|
||||
|
||||
// Direct is a direct proxy: one that makes network connections directly.
|
||||
var proxy_Direct = proxy_direct{}
|
||||
|
||||
func (proxy_direct) Dial(network, addr string) (net.Conn, error) {
|
||||
return net.Dial(network, addr)
|
||||
}
|
||||
|
||||
// A PerHost directs connections to a default Dialer unless the host name
|
||||
// requested matches one of a number of exceptions.
|
||||
type proxy_PerHost struct {
|
||||
def, bypass proxy_Dialer
|
||||
|
||||
bypassNetworks []*net.IPNet
|
||||
bypassIPs []net.IP
|
||||
bypassZones []string
|
||||
bypassHosts []string
|
||||
}
|
||||
|
||||
// NewPerHost returns a PerHost Dialer that directs connections to either
|
||||
// defaultDialer or bypass, depending on whether the connection matches one of
|
||||
// the configured rules.
|
||||
func proxy_NewPerHost(defaultDialer, bypass proxy_Dialer) *proxy_PerHost {
|
||||
return &proxy_PerHost{
|
||||
def: defaultDialer,
|
||||
bypass: bypass,
|
||||
}
|
||||
}
|
||||
|
||||
// Dial connects to the address addr on the given network through either
|
||||
// defaultDialer or bypass.
|
||||
func (p *proxy_PerHost) Dial(network, addr string) (c net.Conn, err error) {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p.dialerForRequest(host).Dial(network, addr)
|
||||
}
|
||||
|
||||
func (p *proxy_PerHost) dialerForRequest(host string) proxy_Dialer {
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
for _, net := range p.bypassNetworks {
|
||||
if net.Contains(ip) {
|
||||
return p.bypass
|
||||
}
|
||||
}
|
||||
for _, bypassIP := range p.bypassIPs {
|
||||
if bypassIP.Equal(ip) {
|
||||
return p.bypass
|
||||
}
|
||||
}
|
||||
return p.def
|
||||
}
|
||||
|
||||
for _, zone := range p.bypassZones {
|
||||
if strings.HasSuffix(host, zone) {
|
||||
return p.bypass
|
||||
}
|
||||
if host == zone[1:] {
|
||||
// For a zone ".example.com", we match "example.com"
|
||||
// too.
|
||||
return p.bypass
|
||||
}
|
||||
}
|
||||
for _, bypassHost := range p.bypassHosts {
|
||||
if bypassHost == host {
|
||||
return p.bypass
|
||||
}
|
||||
}
|
||||
return p.def
|
||||
}
|
||||
|
||||
// AddFromString parses a string that contains comma-separated values
|
||||
// specifying hosts that should use the bypass proxy. Each value is either an
|
||||
// IP address, a CIDR range, a zone (*.example.com) or a host name
|
||||
// (localhost). A best effort is made to parse the string and errors are
|
||||
// ignored.
|
||||
func (p *proxy_PerHost) AddFromString(s string) {
|
||||
hosts := strings.Split(s, ",")
|
||||
for _, host := range hosts {
|
||||
host = strings.TrimSpace(host)
|
||||
if len(host) == 0 {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(host, "/") {
|
||||
// We assume that it's a CIDR address like 127.0.0.0/8
|
||||
if _, net, err := net.ParseCIDR(host); err == nil {
|
||||
p.AddNetwork(net)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
p.AddIP(ip)
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(host, "*.") {
|
||||
p.AddZone(host[1:])
|
||||
continue
|
||||
}
|
||||
p.AddHost(host)
|
||||
}
|
||||
}
|
||||
|
||||
// AddIP specifies an IP address that will use the bypass proxy. Note that
|
||||
// this will only take effect if a literal IP address is dialed. A connection
|
||||
// to a named host will never match an IP.
|
||||
func (p *proxy_PerHost) AddIP(ip net.IP) {
|
||||
p.bypassIPs = append(p.bypassIPs, ip)
|
||||
}
|
||||
|
||||
// AddNetwork specifies an IP range that will use the bypass proxy. Note that
|
||||
// this will only take effect if a literal IP address is dialed. A connection
|
||||
// to a named host will never match.
|
||||
func (p *proxy_PerHost) AddNetwork(net *net.IPNet) {
|
||||
p.bypassNetworks = append(p.bypassNetworks, net)
|
||||
}
|
||||
|
||||
// AddZone specifies a DNS suffix that will use the bypass proxy. A zone of
|
||||
// "example.com" matches "example.com" and all of its subdomains.
|
||||
func (p *proxy_PerHost) AddZone(zone string) {
|
||||
if strings.HasSuffix(zone, ".") {
|
||||
zone = zone[:len(zone)-1]
|
||||
}
|
||||
if !strings.HasPrefix(zone, ".") {
|
||||
zone = "." + zone
|
||||
}
|
||||
p.bypassZones = append(p.bypassZones, zone)
|
||||
}
|
||||
|
||||
// AddHost specifies a host name that will use the bypass proxy.
|
||||
func (p *proxy_PerHost) AddHost(host string) {
|
||||
if strings.HasSuffix(host, ".") {
|
||||
host = host[:len(host)-1]
|
||||
}
|
||||
p.bypassHosts = append(p.bypassHosts, host)
|
||||
}
|
||||
|
||||
// A Dialer is a means to establish a connection.
|
||||
type proxy_Dialer interface {
|
||||
// Dial connects to the given address via the proxy.
|
||||
Dial(network, addr string) (c net.Conn, err error)
|
||||
}
|
||||
|
||||
// Auth contains authentication parameters that specific Dialers may require.
|
||||
type proxy_Auth struct {
|
||||
User, Password string
|
||||
}
|
||||
|
||||
// FromEnvironment returns the dialer specified by the proxy related variables in
|
||||
// the environment.
|
||||
func proxy_FromEnvironment() proxy_Dialer {
|
||||
allProxy := proxy_allProxyEnv.Get()
|
||||
if len(allProxy) == 0 {
|
||||
return proxy_Direct
|
||||
}
|
||||
|
||||
proxyURL, err := url.Parse(allProxy)
|
||||
if err != nil {
|
||||
return proxy_Direct
|
||||
}
|
||||
proxy, err := proxy_FromURL(proxyURL, proxy_Direct)
|
||||
if err != nil {
|
||||
return proxy_Direct
|
||||
}
|
||||
|
||||
noProxy := proxy_noProxyEnv.Get()
|
||||
if len(noProxy) == 0 {
|
||||
return proxy
|
||||
}
|
||||
|
||||
perHost := proxy_NewPerHost(proxy, proxy_Direct)
|
||||
perHost.AddFromString(noProxy)
|
||||
return perHost
|
||||
}
|
||||
|
||||
// proxySchemes is a map from URL schemes to a function that creates a Dialer
|
||||
// from a URL with such a scheme.
|
||||
var proxy_proxySchemes map[string]func(*url.URL, proxy_Dialer) (proxy_Dialer, error)
|
||||
|
||||
// RegisterDialerType takes a URL scheme and a function to generate Dialers from
|
||||
// a URL with that scheme and a forwarding Dialer. Registered schemes are used
|
||||
// by FromURL.
|
||||
func proxy_RegisterDialerType(scheme string, f func(*url.URL, proxy_Dialer) (proxy_Dialer, error)) {
|
||||
if proxy_proxySchemes == nil {
|
||||
proxy_proxySchemes = make(map[string]func(*url.URL, proxy_Dialer) (proxy_Dialer, error))
|
||||
}
|
||||
proxy_proxySchemes[scheme] = f
|
||||
}
|
||||
|
||||
// FromURL returns a Dialer given a URL specification and an underlying
|
||||
// Dialer for it to make network requests.
|
||||
func proxy_FromURL(u *url.URL, forward proxy_Dialer) (proxy_Dialer, error) {
|
||||
var auth *proxy_Auth
|
||||
if u.User != nil {
|
||||
auth = new(proxy_Auth)
|
||||
auth.User = u.User.Username()
|
||||
if p, ok := u.User.Password(); ok {
|
||||
auth.Password = p
|
||||
}
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "socks5":
|
||||
return proxy_SOCKS5("tcp", u.Host, auth, forward)
|
||||
}
|
||||
|
||||
// If the scheme doesn't match any of the built-in schemes, see if it
|
||||
// was registered by another package.
|
||||
if proxy_proxySchemes != nil {
|
||||
if f, ok := proxy_proxySchemes[u.Scheme]; ok {
|
||||
return f(u, forward)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("proxy: unknown scheme: " + u.Scheme)
|
||||
}
|
||||
|
||||
var (
|
||||
proxy_allProxyEnv = &proxy_envOnce{
|
||||
names: []string{"ALL_PROXY", "all_proxy"},
|
||||
}
|
||||
proxy_noProxyEnv = &proxy_envOnce{
|
||||
names: []string{"NO_PROXY", "no_proxy"},
|
||||
}
|
||||
)
|
||||
|
||||
// envOnce looks up an environment variable (optionally by multiple
|
||||
// names) once. It mitigates expensive lookups on some platforms
|
||||
// (e.g. Windows).
|
||||
// (Borrowed from net/http/transport.go)
|
||||
type proxy_envOnce struct {
|
||||
names []string
|
||||
once sync.Once
|
||||
val string
|
||||
}
|
||||
|
||||
func (e *proxy_envOnce) Get() string {
|
||||
e.once.Do(e.init)
|
||||
return e.val
|
||||
}
|
||||
|
||||
func (e *proxy_envOnce) init() {
|
||||
for _, n := range e.names {
|
||||
e.val = os.Getenv(n)
|
||||
if e.val != "" {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SOCKS5 returns a Dialer that makes SOCKSv5 connections to the given address
|
||||
// with an optional username and password. See RFC 1928 and RFC 1929.
|
||||
func proxy_SOCKS5(network, addr string, auth *proxy_Auth, forward proxy_Dialer) (proxy_Dialer, error) {
|
||||
s := &proxy_socks5{
|
||||
network: network,
|
||||
addr: addr,
|
||||
forward: forward,
|
||||
}
|
||||
if auth != nil {
|
||||
s.user = auth.User
|
||||
s.password = auth.Password
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
type proxy_socks5 struct {
|
||||
user, password string
|
||||
network, addr string
|
||||
forward proxy_Dialer
|
||||
}
|
||||
|
||||
const proxy_socks5Version = 5
|
||||
|
||||
const (
|
||||
proxy_socks5AuthNone = 0
|
||||
proxy_socks5AuthPassword = 2
|
||||
)
|
||||
|
||||
const proxy_socks5Connect = 1
|
||||
|
||||
const (
|
||||
proxy_socks5IP4 = 1
|
||||
proxy_socks5Domain = 3
|
||||
proxy_socks5IP6 = 4
|
||||
)
|
||||
|
||||
var proxy_socks5Errors = []string{
|
||||
"",
|
||||
"general failure",
|
||||
"connection forbidden",
|
||||
"network unreachable",
|
||||
"host unreachable",
|
||||
"connection refused",
|
||||
"TTL expired",
|
||||
"command not supported",
|
||||
"address type not supported",
|
||||
}
|
||||
|
||||
// Dial connects to the address addr on the given network via the SOCKS5 proxy.
|
||||
func (s *proxy_socks5) Dial(network, addr string) (net.Conn, error) {
|
||||
switch network {
|
||||
case "tcp", "tcp6", "tcp4":
|
||||
default:
|
||||
return nil, errors.New("proxy: no support for SOCKS5 proxy connections of type " + network)
|
||||
}
|
||||
|
||||
conn, err := s.forward.Dial(s.network, s.addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.connect(conn, addr); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// connect takes an existing connection to a socks5 proxy server,
|
||||
// and commands the server to extend that connection to target,
|
||||
// which must be a canonical address with a host and port.
|
||||
func (s *proxy_socks5) connect(conn net.Conn, target string) error {
|
||||
host, portStr, err := net.SplitHostPort(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return errors.New("proxy: failed to parse port number: " + portStr)
|
||||
}
|
||||
if port < 1 || port > 0xffff {
|
||||
return errors.New("proxy: port number out of range: " + portStr)
|
||||
}
|
||||
|
||||
// the size here is just an estimate
|
||||
buf := make([]byte, 0, 6+len(host))
|
||||
|
||||
buf = append(buf, proxy_socks5Version)
|
||||
if len(s.user) > 0 && len(s.user) < 256 && len(s.password) < 256 {
|
||||
buf = append(buf, 2 /* num auth methods */, proxy_socks5AuthNone, proxy_socks5AuthPassword)
|
||||
} else {
|
||||
buf = append(buf, 1 /* num auth methods */, proxy_socks5AuthNone)
|
||||
}
|
||||
|
||||
if _, err := conn.Write(buf); err != nil {
|
||||
return errors.New("proxy: failed to write greeting to SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
|
||||
return errors.New("proxy: failed to read greeting from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
if buf[0] != 5 {
|
||||
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " has unexpected version " + strconv.Itoa(int(buf[0])))
|
||||
}
|
||||
if buf[1] == 0xff {
|
||||
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " requires authentication")
|
||||
}
|
||||
|
||||
// See RFC 1929
|
||||
if buf[1] == proxy_socks5AuthPassword {
|
||||
buf = buf[:0]
|
||||
buf = append(buf, 1 /* password protocol version */)
|
||||
buf = append(buf, uint8(len(s.user)))
|
||||
buf = append(buf, s.user...)
|
||||
buf = append(buf, uint8(len(s.password)))
|
||||
buf = append(buf, s.password...)
|
||||
|
||||
if _, err := conn.Write(buf); err != nil {
|
||||
return errors.New("proxy: failed to write authentication request to SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
|
||||
return errors.New("proxy: failed to read authentication reply from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
if buf[1] != 0 {
|
||||
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " rejected username/password")
|
||||
}
|
||||
}
|
||||
|
||||
buf = buf[:0]
|
||||
buf = append(buf, proxy_socks5Version, proxy_socks5Connect, 0 /* reserved */)
|
||||
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
buf = append(buf, proxy_socks5IP4)
|
||||
ip = ip4
|
||||
} else {
|
||||
buf = append(buf, proxy_socks5IP6)
|
||||
}
|
||||
buf = append(buf, ip...)
|
||||
} else {
|
||||
if len(host) > 255 {
|
||||
return errors.New("proxy: destination host name too long: " + host)
|
||||
}
|
||||
buf = append(buf, proxy_socks5Domain)
|
||||
buf = append(buf, byte(len(host)))
|
||||
buf = append(buf, host...)
|
||||
}
|
||||
buf = append(buf, byte(port>>8), byte(port))
|
||||
|
||||
if _, err := conn.Write(buf); err != nil {
|
||||
return errors.New("proxy: failed to write connect request to SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(conn, buf[:4]); err != nil {
|
||||
return errors.New("proxy: failed to read connect reply from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
failure := "unknown error"
|
||||
if int(buf[1]) < len(proxy_socks5Errors) {
|
||||
failure = proxy_socks5Errors[buf[1]]
|
||||
}
|
||||
|
||||
if len(failure) > 0 {
|
||||
return errors.New("proxy: SOCKS5 proxy at " + s.addr + " failed to connect: " + failure)
|
||||
}
|
||||
|
||||
bytesToDiscard := 0
|
||||
switch buf[3] {
|
||||
case proxy_socks5IP4:
|
||||
bytesToDiscard = net.IPv4len
|
||||
case proxy_socks5IP6:
|
||||
bytesToDiscard = net.IPv6len
|
||||
case proxy_socks5Domain:
|
||||
_, err := io.ReadFull(conn, buf[:1])
|
||||
if err != nil {
|
||||
return errors.New("proxy: failed to read domain length from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
bytesToDiscard = int(buf[0])
|
||||
default:
|
||||
return errors.New("proxy: got unknown address type " + strconv.Itoa(int(buf[3])) + " from SOCKS5 proxy at " + s.addr)
|
||||
}
|
||||
|
||||
if cap(buf) < bytesToDiscard {
|
||||
buf = make([]byte, bytesToDiscard)
|
||||
} else {
|
||||
buf = buf[:bytesToDiscard]
|
||||
}
|
||||
if _, err := io.ReadFull(conn, buf); err != nil {
|
||||
return errors.New("proxy: failed to read address from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
// Also need to discard the port number
|
||||
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
|
||||
return errors.New("proxy: failed to read port from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+5
-7
@@ -2,17 +2,15 @@ language: go
|
||||
sudo: false
|
||||
|
||||
go:
|
||||
- 1.10.x
|
||||
- 1.11.x
|
||||
- 1.12.x
|
||||
- "1.12.x"
|
||||
- "1.13.x"
|
||||
- tip
|
||||
|
||||
before_install:
|
||||
# don't use the miekg/dns when testing forks
|
||||
- mkdir -p $GOPATH/src/github.com/miekg
|
||||
- ln -s $TRAVIS_BUILD_DIR $GOPATH/src/github.com/miekg/ || true
|
||||
env:
|
||||
- GO111MODULE=on
|
||||
|
||||
script:
|
||||
- go generate ./... && test `git ls-files --modified | wc -l` = 0
|
||||
- go test -race -v -bench=. -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
after_success:
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
* @miekg @tmthrgd
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:6914c49eed986dfb8dffb33516fa129c49929d4d873f41e073c83c11c372b870"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"ed25519",
|
||||
"ed25519/internal/edwards25519",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "e3636079e1a4c1f337f212cc5cd2aca108f6c900"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:08e41d63f8dac84d83797368b56cf0b339e42d0224e5e56668963c28aec95685"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"bpf",
|
||||
"context",
|
||||
"internal/iana",
|
||||
"internal/socket",
|
||||
"ipv4",
|
||||
"ipv6",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "4dfa2610cdf3b287375bbba5b8f2a14d3b01d8de"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:b2ea75de0ccb2db2ac79356407f8a4cd8f798fe15d41b381c00abf3ae8e55ed1"
|
||||
name = "golang.org/x/sync"
|
||||
packages = ["errgroup"]
|
||||
pruneopts = ""
|
||||
revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:149a432fabebb8221a80f77731b1cd63597197ded4f14af606ebe3a0959004ec"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix"]
|
||||
pruneopts = ""
|
||||
revision = "e4b3c5e9061176387e7cea65e4dc5853801f3fb7"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
input-imports = [
|
||||
"golang.org/x/crypto/ed25519",
|
||||
"golang.org/x/net/ipv4",
|
||||
"golang.org/x/net/ipv6",
|
||||
"golang.org/x/sync/errgroup",
|
||||
"golang.org/x/sys/unix",
|
||||
]
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
|
||||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sync"
|
||||
+2
-4
@@ -1,7 +1,3 @@
|
||||
Extensions of the original work are copyright (c) 2011 Miek Gieben
|
||||
|
||||
As this is fork of the official Go code the same license applies:
|
||||
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
@@ -30,3 +26,5 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
As this is fork of the official Go code the same license applies.
|
||||
Extensions of the original work are copyright (c) 2011 Miek Gieben
|
||||
|
||||
+5
-2
@@ -69,6 +69,7 @@ A not-so-up-to-date-list-that-may-be-actually-current:
|
||||
* https://github.com/semihalev/sdns
|
||||
* https://render.com
|
||||
* https://github.com/peterzen/goresolver
|
||||
* https://github.com/folbricht/routedns
|
||||
|
||||
Send pull request if you want to be listed here.
|
||||
|
||||
@@ -93,8 +94,8 @@ DNS Authors 2012-
|
||||
|
||||
# Building
|
||||
|
||||
Building is done with the `go` tool. If you have setup your GOPATH correctly, the following should
|
||||
work:
|
||||
This library uses Go modules and uses semantic versioning. Building is done with the `go` tool, so
|
||||
the following should work:
|
||||
|
||||
go get github.com/miekg/dns
|
||||
go build github.com/miekg/dns
|
||||
@@ -126,6 +127,7 @@ Example programs can be found in the `github.com/miekg/exdns` repository.
|
||||
* 2915 - NAPTR record
|
||||
* 2929 - DNS IANA Considerations
|
||||
* 3110 - RSASHA1 DNS keys
|
||||
* 3123 - APL record
|
||||
* 3225 - DO bit (DNSSEC OK)
|
||||
* 340{1,2,3} - NAPTR record
|
||||
* 3445 - Limiting the scope of (DNS)KEY
|
||||
@@ -152,6 +154,7 @@ Example programs can be found in the `github.com/miekg/exdns` repository.
|
||||
* 6844 - CAA record
|
||||
* 6891 - EDNS0 update
|
||||
* 6895 - DNS IANA considerations
|
||||
* 6944 - DNSSEC DNSKEY Algorithm Status
|
||||
* 6975 - Algorithm Understanding in DNSSEC
|
||||
* 7043 - EUI48/EUI64 records
|
||||
* 7314 - DNS (EDNS) EXPIRE Option
|
||||
|
||||
+13
-8
@@ -6,22 +6,30 @@ type MsgAcceptFunc func(dh Header) MsgAcceptAction
|
||||
|
||||
// DefaultMsgAcceptFunc checks the request and will reject if:
|
||||
//
|
||||
// * isn't a request (don't respond in that case).
|
||||
// * isn't a request (don't respond in that case)
|
||||
//
|
||||
// * opcode isn't OpcodeQuery or OpcodeNotify
|
||||
//
|
||||
// * Zero bit isn't zero
|
||||
//
|
||||
// * has more than 1 question in the question section
|
||||
//
|
||||
// * has more than 1 RR in the Answer section
|
||||
//
|
||||
// * has more than 0 RRs in the Authority section
|
||||
//
|
||||
// * has more than 2 RRs in the Additional section
|
||||
//
|
||||
var DefaultMsgAcceptFunc MsgAcceptFunc = defaultMsgAcceptFunc
|
||||
|
||||
// MsgAcceptAction represents the action to be taken.
|
||||
type MsgAcceptAction int
|
||||
|
||||
const (
|
||||
MsgAccept MsgAcceptAction = iota // Accept the message
|
||||
MsgReject // Reject the message with a RcodeFormatError
|
||||
MsgIgnore // Ignore the error and send nothing back.
|
||||
MsgAccept MsgAcceptAction = iota // Accept the message
|
||||
MsgReject // Reject the message with a RcodeFormatError
|
||||
MsgIgnore // Ignore the error and send nothing back.
|
||||
MsgRejectNotImplemented // Reject the message with a RcodeNotImplemented
|
||||
)
|
||||
|
||||
func defaultMsgAcceptFunc(dh Header) MsgAcceptAction {
|
||||
@@ -32,12 +40,9 @@ func defaultMsgAcceptFunc(dh Header) MsgAcceptAction {
|
||||
// Don't allow dynamic updates, because then the sections can contain a whole bunch of RRs.
|
||||
opcode := int(dh.Bits>>11) & 0xF
|
||||
if opcode != OpcodeQuery && opcode != OpcodeNotify {
|
||||
return MsgReject
|
||||
return MsgRejectNotImplemented
|
||||
}
|
||||
|
||||
if isZero := dh.Bits&_Z != 0; isZero {
|
||||
return MsgReject
|
||||
}
|
||||
if dh.Qdcount != 1 {
|
||||
return MsgReject
|
||||
}
|
||||
|
||||
+38
-44
@@ -6,6 +6,7 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
@@ -128,20 +129,15 @@ func (c *Client) Exchange(m *Msg, address string) (r *Msg, rtt time.Duration, er
|
||||
return c.exchange(m, address)
|
||||
}
|
||||
|
||||
t := "nop"
|
||||
if t1, ok := TypeToString[m.Question[0].Qtype]; ok {
|
||||
t = t1
|
||||
}
|
||||
cl := "nop"
|
||||
if cl1, ok := ClassToString[m.Question[0].Qclass]; ok {
|
||||
cl = cl1
|
||||
}
|
||||
r, rtt, err, shared := c.group.Do(m.Question[0].Name+t+cl, func() (*Msg, time.Duration, error) {
|
||||
q := m.Question[0]
|
||||
key := fmt.Sprintf("%s:%d:%d", q.Name, q.Qtype, q.Qclass)
|
||||
r, rtt, err, shared := c.group.Do(key, func() (*Msg, time.Duration, error) {
|
||||
return c.exchange(m, address)
|
||||
})
|
||||
if r != nil && shared {
|
||||
r = r.Copy()
|
||||
}
|
||||
|
||||
return r, rtt, err
|
||||
}
|
||||
|
||||
@@ -219,8 +215,15 @@ func (co *Conn) ReadMsgHeader(hdr *Header) ([]byte, error) {
|
||||
n int
|
||||
err error
|
||||
)
|
||||
switch co.Conn.(type) {
|
||||
case *net.TCPConn, *tls.Conn:
|
||||
|
||||
if _, ok := co.Conn.(net.PacketConn); ok {
|
||||
if co.UDPSize > MinMsgSize {
|
||||
p = make([]byte, co.UDPSize)
|
||||
} else {
|
||||
p = make([]byte, MinMsgSize)
|
||||
}
|
||||
n, err = co.Read(p)
|
||||
} else {
|
||||
var length uint16
|
||||
if err := binary.Read(co.Conn, binary.BigEndian, &length); err != nil {
|
||||
return nil, err
|
||||
@@ -228,13 +231,6 @@ func (co *Conn) ReadMsgHeader(hdr *Header) ([]byte, error) {
|
||||
|
||||
p = make([]byte, length)
|
||||
n, err = io.ReadFull(co.Conn, p)
|
||||
default:
|
||||
if co.UDPSize > MinMsgSize {
|
||||
p = make([]byte, co.UDPSize)
|
||||
} else {
|
||||
p = make([]byte, MinMsgSize)
|
||||
}
|
||||
n, err = co.Read(p)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -260,21 +256,20 @@ func (co *Conn) Read(p []byte) (n int, err error) {
|
||||
return 0, ErrConnEmpty
|
||||
}
|
||||
|
||||
switch co.Conn.(type) {
|
||||
case *net.TCPConn, *tls.Conn:
|
||||
var length uint16
|
||||
if err := binary.Read(co.Conn, binary.BigEndian, &length); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if int(length) > len(p) {
|
||||
return 0, io.ErrShortBuffer
|
||||
}
|
||||
|
||||
return io.ReadFull(co.Conn, p[:length])
|
||||
if _, ok := co.Conn.(net.PacketConn); ok {
|
||||
// UDP connection
|
||||
return co.Conn.Read(p)
|
||||
}
|
||||
|
||||
// UDP connection
|
||||
return co.Conn.Read(p)
|
||||
var length uint16
|
||||
if err := binary.Read(co.Conn, binary.BigEndian, &length); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if int(length) > len(p) {
|
||||
return 0, io.ErrShortBuffer
|
||||
}
|
||||
|
||||
return io.ReadFull(co.Conn, p[:length])
|
||||
}
|
||||
|
||||
// WriteMsg sends a message through the connection co.
|
||||
@@ -301,21 +296,20 @@ func (co *Conn) WriteMsg(m *Msg) (err error) {
|
||||
}
|
||||
|
||||
// Write implements the net.Conn Write method.
|
||||
func (co *Conn) Write(p []byte) (n int, err error) {
|
||||
switch co.Conn.(type) {
|
||||
case *net.TCPConn, *tls.Conn:
|
||||
if len(p) > MaxMsgSize {
|
||||
return 0, &Error{err: "message too large"}
|
||||
}
|
||||
|
||||
l := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(l, uint16(len(p)))
|
||||
|
||||
n, err := (&net.Buffers{l, p}).WriteTo(co.Conn)
|
||||
return int(n), err
|
||||
func (co *Conn) Write(p []byte) (int, error) {
|
||||
if len(p) > MaxMsgSize {
|
||||
return 0, &Error{err: "message too large"}
|
||||
}
|
||||
|
||||
return co.Conn.Write(p)
|
||||
if _, ok := co.Conn.(net.PacketConn); ok {
|
||||
return co.Conn.Write(p)
|
||||
}
|
||||
|
||||
l := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(l, uint16(len(p)))
|
||||
|
||||
n, err := (&net.Buffers{l, p}).WriteTo(co.Conn)
|
||||
return int(n), err
|
||||
}
|
||||
|
||||
// Return the appropriate timeout for a specific request
|
||||
|
||||
+2
-2
@@ -54,7 +54,7 @@ type RR interface {
|
||||
// parse parses an RR from zone file format.
|
||||
//
|
||||
// This will only be called on a new and empty RR type with only the header populated.
|
||||
parse(c *zlexer, origin, file string) *ParseError
|
||||
parse(c *zlexer, origin string) *ParseError
|
||||
|
||||
// isDuplicate returns whether the two RRs are duplicates.
|
||||
isDuplicate(r2 RR) bool
|
||||
@@ -105,7 +105,7 @@ func (h *RR_Header) unpack(msg []byte, off int) (int, error) {
|
||||
panic("dns: internal error: unpack should never be called on RR_Header")
|
||||
}
|
||||
|
||||
func (h *RR_Header) parse(c *zlexer, origin, file string) *ParseError {
|
||||
func (h *RR_Header) parse(c *zlexer, origin string) *ParseError {
|
||||
panic("dns: internal error: parse should never be called on RR_Header")
|
||||
}
|
||||
|
||||
|
||||
+5
-2
@@ -141,8 +141,8 @@ func (k *DNSKEY) KeyTag() uint16 {
|
||||
switch k.Algorithm {
|
||||
case RSAMD5:
|
||||
// Look at the bottom two bytes of the modules, which the last
|
||||
// item in the pubkey. We could do this faster by looking directly
|
||||
// at the base64 values. But I'm lazy.
|
||||
// item in the pubkey.
|
||||
// This algorithm has been deprecated, but keep this key-tag calculation.
|
||||
modulus, _ := fromBase64([]byte(k.PublicKey))
|
||||
if len(modulus) > 1 {
|
||||
x := binary.BigEndian.Uint16(modulus[len(modulus)-2:])
|
||||
@@ -318,6 +318,9 @@ func (rr *RRSIG) Sign(k crypto.Signer, rrset []RR) error {
|
||||
}
|
||||
|
||||
rr.Signature = toBase64(signature)
|
||||
case RSAMD5, DSA, DSANSEC3SHA1:
|
||||
// See RFC 6944.
|
||||
return ErrAlg
|
||||
default:
|
||||
h := hash.New()
|
||||
h.Write(signdata)
|
||||
|
||||
+4
-42
@@ -2,7 +2,6 @@ package dns
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/dsa"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
@@ -20,11 +19,9 @@ import (
|
||||
// bits should be set to the size of the algorithm.
|
||||
func (k *DNSKEY) Generate(bits int) (crypto.PrivateKey, error) {
|
||||
switch k.Algorithm {
|
||||
case DSA, DSANSEC3SHA1:
|
||||
if bits != 1024 {
|
||||
return nil, ErrKeySize
|
||||
}
|
||||
case RSAMD5, RSASHA1, RSASHA256, RSASHA1NSEC3SHA1:
|
||||
case RSAMD5, DSA, DSANSEC3SHA1:
|
||||
return nil, ErrAlg
|
||||
case RSASHA1, RSASHA256, RSASHA1NSEC3SHA1:
|
||||
if bits < 512 || bits > 4096 {
|
||||
return nil, ErrKeySize
|
||||
}
|
||||
@@ -47,20 +44,7 @@ func (k *DNSKEY) Generate(bits int) (crypto.PrivateKey, error) {
|
||||
}
|
||||
|
||||
switch k.Algorithm {
|
||||
case DSA, DSANSEC3SHA1:
|
||||
params := new(dsa.Parameters)
|
||||
if err := dsa.GenerateParameters(params, rand.Reader, dsa.L1024N160); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
priv := new(dsa.PrivateKey)
|
||||
priv.PublicKey.Parameters = *params
|
||||
err := dsa.GenerateKey(priv, rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
k.setPublicKeyDSA(params.Q, params.P, params.G, priv.PublicKey.Y)
|
||||
return priv, nil
|
||||
case RSAMD5, RSASHA1, RSASHA256, RSASHA512, RSASHA1NSEC3SHA1:
|
||||
case RSASHA1, RSASHA256, RSASHA512, RSASHA1NSEC3SHA1:
|
||||
priv, err := rsa.GenerateKey(rand.Reader, bits)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -120,16 +104,6 @@ func (k *DNSKEY) setPublicKeyECDSA(_X, _Y *big.Int) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Set the public key for DSA
|
||||
func (k *DNSKEY) setPublicKeyDSA(_Q, _P, _G, _Y *big.Int) bool {
|
||||
if _Q == nil || _P == nil || _G == nil || _Y == nil {
|
||||
return false
|
||||
}
|
||||
buf := dsaToBuf(_Q, _P, _G, _Y)
|
||||
k.PublicKey = toBase64(buf)
|
||||
return true
|
||||
}
|
||||
|
||||
// Set the public key for Ed25519
|
||||
func (k *DNSKEY) setPublicKeyED25519(_K ed25519.PublicKey) bool {
|
||||
if _K == nil {
|
||||
@@ -164,15 +138,3 @@ func curveToBuf(_X, _Y *big.Int, intlen int) []byte {
|
||||
buf = append(buf, intToBytes(_Y, intlen)...)
|
||||
return buf
|
||||
}
|
||||
|
||||
// Set the public key for X and Y for Curve. The two
|
||||
// values are just concatenated.
|
||||
func dsaToBuf(_Q, _P, _G, _Y *big.Int) []byte {
|
||||
t := divRoundUp(divRoundUp(_G.BitLen(), 8)-64, 8)
|
||||
buf := []byte{byte(t)}
|
||||
buf = append(buf, intToBytes(_Q, 20)...)
|
||||
buf = append(buf, intToBytes(_P, 64+t*8)...)
|
||||
buf = append(buf, intToBytes(_G, 64+t*8)...)
|
||||
buf = append(buf, intToBytes(_Y, 64+t*8)...)
|
||||
return buf
|
||||
}
|
||||
|
||||
+2
-32
@@ -3,7 +3,6 @@ package dns
|
||||
import (
|
||||
"bufio"
|
||||
"crypto"
|
||||
"crypto/dsa"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"io"
|
||||
@@ -44,19 +43,8 @@ func (k *DNSKEY) ReadPrivateKey(q io.Reader, file string) (crypto.PrivateKey, er
|
||||
return nil, ErrPrivKey
|
||||
}
|
||||
switch uint8(algo) {
|
||||
case DSA:
|
||||
priv, err := readPrivateKeyDSA(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pub := k.publicKeyDSA()
|
||||
if pub == nil {
|
||||
return nil, ErrKey
|
||||
}
|
||||
priv.PublicKey = *pub
|
||||
return priv, nil
|
||||
case RSAMD5:
|
||||
fallthrough
|
||||
case RSAMD5, DSA, DSANSEC3SHA1:
|
||||
return nil, ErrAlg
|
||||
case RSASHA1:
|
||||
fallthrough
|
||||
case RSASHA1NSEC3SHA1:
|
||||
@@ -129,24 +117,6 @@ func readPrivateKeyRSA(m map[string]string) (*rsa.PrivateKey, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func readPrivateKeyDSA(m map[string]string) (*dsa.PrivateKey, error) {
|
||||
p := new(dsa.PrivateKey)
|
||||
p.X = new(big.Int)
|
||||
for k, v := range m {
|
||||
switch k {
|
||||
case "private_value(x)":
|
||||
v1, err := fromBase64([]byte(v))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.X.SetBytes(v1)
|
||||
case "created", "publish", "activate":
|
||||
/* not used in Go (yet) */
|
||||
}
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func readPrivateKeyECDSA(m map[string]string) (*ecdsa.PrivateKey, error) {
|
||||
p := new(ecdsa.PrivateKey)
|
||||
p.D = new(big.Int)
|
||||
|
||||
+4
-5
@@ -83,7 +83,7 @@ with:
|
||||
|
||||
in, err := dns.Exchange(m1, "127.0.0.1:53")
|
||||
|
||||
When this functions returns you will get dns message. A dns message consists
|
||||
When this functions returns you will get DNS message. A DNS message consists
|
||||
out of four sections.
|
||||
The question section: in.Question, the answer section: in.Answer,
|
||||
the authority section: in.Ns and the additional section: in.Extra.
|
||||
@@ -221,7 +221,7 @@ RFC 6895 sets aside a range of type codes for private use. This range is 65,280
|
||||
- 65,534 (0xFF00 - 0xFFFE). When experimenting with new Resource Records these
|
||||
can be used, before requesting an official type code from IANA.
|
||||
|
||||
See https://miek.nl/2014/September/21/idn-and-private-rr-in-go-dns/ for more
|
||||
See https://miek.nl/2014/september/21/idn-and-private-rr-in-go-dns/ for more
|
||||
information.
|
||||
|
||||
EDNS0
|
||||
@@ -238,9 +238,8 @@ Basic use pattern for creating an (empty) OPT RR:
|
||||
|
||||
The rdata of an OPT RR consists out of a slice of EDNS0 (RFC 6891) interfaces.
|
||||
Currently only a few have been standardized: EDNS0_NSID (RFC 5001) and
|
||||
EDNS0_SUBNET (draft-vandergaast-edns-client-subnet-02). Note that these options
|
||||
may be combined in an OPT RR. Basic use pattern for a server to check if (and
|
||||
which) options are set:
|
||||
EDNS0_SUBNET (RFC 7871). Note that these options may be combined in an OPT RR.
|
||||
Basic use pattern for a server to check if (and which) options are set:
|
||||
|
||||
// o is a dns.OPT
|
||||
for _, s := range o.Option {
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ package dns
|
||||
// IsDuplicate checks of r1 and r2 are duplicates of each other, excluding the TTL.
|
||||
// So this means the header data is equal *and* the RDATA is the same. Return true
|
||||
// is so, otherwise false.
|
||||
// It's is a protocol violation to have identical RRs in a message.
|
||||
// It's a protocol violation to have identical RRs in a message.
|
||||
func IsDuplicate(r1, r2 RR) bool {
|
||||
// Check whether the record header is identical.
|
||||
if !r1.Header().isDuplicate(r2.Header()) {
|
||||
|
||||
+24
-8
@@ -88,7 +88,7 @@ func (rr *OPT) len(off int, compression map[string]struct{}) int {
|
||||
return l
|
||||
}
|
||||
|
||||
func (rr *OPT) parse(c *zlexer, origin, file string) *ParseError {
|
||||
func (rr *OPT) parse(c *zlexer, origin string) *ParseError {
|
||||
panic("dns: internal error: parse should never be called on OPT")
|
||||
}
|
||||
|
||||
@@ -360,7 +360,7 @@ func (e *EDNS0_COOKIE) copy() EDNS0 { return &EDNS0_COOKIE{e.Code, e.C
|
||||
// The EDNS0_UL (Update Lease) (draft RFC) option is used to tell the server to set
|
||||
// an expiration on an update RR. This is helpful for clients that cannot clean
|
||||
// up after themselves. This is a draft RFC and more information can be found at
|
||||
// http://files.dns-sd.org/draft-sekar-dns-ul.txt
|
||||
// https://tools.ietf.org/html/draft-sekar-dns-ul-02
|
||||
//
|
||||
// o := new(dns.OPT)
|
||||
// o.Hdr.Name = "."
|
||||
@@ -370,24 +370,36 @@ func (e *EDNS0_COOKIE) copy() EDNS0 { return &EDNS0_COOKIE{e.Code, e.C
|
||||
// e.Lease = 120 // in seconds
|
||||
// o.Option = append(o.Option, e)
|
||||
type EDNS0_UL struct {
|
||||
Code uint16 // Always EDNS0UL
|
||||
Lease uint32
|
||||
Code uint16 // Always EDNS0UL
|
||||
Lease uint32
|
||||
KeyLease uint32
|
||||
}
|
||||
|
||||
// Option implements the EDNS0 interface.
|
||||
func (e *EDNS0_UL) Option() uint16 { return EDNS0UL }
|
||||
func (e *EDNS0_UL) String() string { return strconv.FormatUint(uint64(e.Lease), 10) }
|
||||
func (e *EDNS0_UL) copy() EDNS0 { return &EDNS0_UL{e.Code, e.Lease} }
|
||||
func (e *EDNS0_UL) String() string { return fmt.Sprintf("%d %d", e.Lease, e.KeyLease) }
|
||||
func (e *EDNS0_UL) copy() EDNS0 { return &EDNS0_UL{e.Code, e.Lease, e.KeyLease} }
|
||||
|
||||
// Copied: http://golang.org/src/pkg/net/dnsmsg.go
|
||||
func (e *EDNS0_UL) pack() ([]byte, error) {
|
||||
b := make([]byte, 4)
|
||||
var b []byte
|
||||
if e.KeyLease == 0 {
|
||||
b = make([]byte, 4)
|
||||
} else {
|
||||
b = make([]byte, 8)
|
||||
binary.BigEndian.PutUint32(b[4:], e.KeyLease)
|
||||
}
|
||||
binary.BigEndian.PutUint32(b, e.Lease)
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (e *EDNS0_UL) unpack(b []byte) error {
|
||||
if len(b) < 4 {
|
||||
switch len(b) {
|
||||
case 4:
|
||||
e.KeyLease = 0
|
||||
case 8:
|
||||
e.KeyLease = binary.BigEndian.Uint32(b[4:])
|
||||
default:
|
||||
return ErrBuf
|
||||
}
|
||||
e.Lease = binary.BigEndian.Uint32(b)
|
||||
@@ -531,6 +543,10 @@ func (e *EDNS0_EXPIRE) pack() ([]byte, error) {
|
||||
}
|
||||
|
||||
func (e *EDNS0_EXPIRE) unpack(b []byte) error {
|
||||
if len(b) == 0 {
|
||||
// zero-length EXPIRE query, see RFC 7314 Section 2
|
||||
return nil
|
||||
}
|
||||
if len(b) < 4 {
|
||||
return ErrBuf
|
||||
}
|
||||
|
||||
+10
-1
@@ -2,6 +2,8 @@
|
||||
|
||||
package dns
|
||||
|
||||
import "strings"
|
||||
|
||||
func Fuzz(data []byte) int {
|
||||
msg := new(Msg)
|
||||
|
||||
@@ -16,7 +18,14 @@ func Fuzz(data []byte) int {
|
||||
}
|
||||
|
||||
func FuzzNewRR(data []byte) int {
|
||||
if _, err := NewRR(string(data)); err != nil {
|
||||
str := string(data)
|
||||
// Do not fuzz lines that include the $INCLUDE keyword and hint the fuzzer
|
||||
// at avoiding them.
|
||||
// See GH#1025 for context.
|
||||
if strings.Contains(strings.ToUpper(str), "$INCLUDE") {
|
||||
return -1
|
||||
}
|
||||
if _, err := NewRR(str); err != nil {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
|
||||
+7
-2
@@ -49,11 +49,15 @@ func (zp *ZoneParser) generate(l lex) (RR, bool) {
|
||||
if err != nil {
|
||||
return zp.setParseError("bad stop in $GENERATE range", l)
|
||||
}
|
||||
if end < 0 || start < 0 || end < start {
|
||||
if end < 0 || start < 0 || end < start || (end-start)/step > 65535 {
|
||||
return zp.setParseError("bad range in $GENERATE range", l)
|
||||
}
|
||||
|
||||
zp.c.Next() // _BLANK
|
||||
// _BLANK
|
||||
l, ok := zp.c.Next()
|
||||
if !ok || l.value != zBlank {
|
||||
return zp.setParseError("garbage after $GENERATE range", l)
|
||||
}
|
||||
|
||||
// Create a complete new string, which we then parse again.
|
||||
var s string
|
||||
@@ -81,6 +85,7 @@ func (zp *ZoneParser) generate(l lex) (RR, bool) {
|
||||
}
|
||||
zp.sub = NewZoneParser(r, zp.origin, zp.file)
|
||||
zp.sub.includeDepth, zp.sub.includeAllowed = zp.includeDepth, zp.includeAllowed
|
||||
zp.sub.generateDisallowed = true
|
||||
zp.sub.SetDefaultTTL(defaultTtl)
|
||||
return zp.subNext()
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
module github.com/miekg/dns
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 // indirect
|
||||
)
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4 h1:Vk3wNqEZwyGyei9yq5ekj7frek2u7HUfffJ1/opblzc=
|
||||
golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472 h1:Gv7RPwsi3eZ2Fgewe3CBsuOebPwO27PoXzRpJPsvSSM=
|
||||
golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20180926154720-4dfa2610cdf3 h1:dgd4x4kJt7G4k4m93AYLzM8Ni6h2qLTfh9n9vXJT3/0=
|
||||
golang.org/x/net v0.0.0-20180926154720-4dfa2610cdf3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611 h1:O33LKL7WyJgjN9CvxfTIomjIClbd/Kq86/iipowHQU0=
|
||||
golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd h1:DBH9mDw0zluJT/R+nGuV3jWFWLFaHyYZWD4tOT+cjn0=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
+42
-18
@@ -126,20 +126,23 @@ func Split(s string) []int {
|
||||
// The bool end is true when the end of the string has been reached.
|
||||
// Also see PrevLabel.
|
||||
func NextLabel(s string, offset int) (i int, end bool) {
|
||||
quote := false
|
||||
if s == "" {
|
||||
return 0, true
|
||||
}
|
||||
for i = offset; i < len(s)-1; i++ {
|
||||
switch s[i] {
|
||||
case '\\':
|
||||
quote = !quote
|
||||
default:
|
||||
quote = false
|
||||
case '.':
|
||||
if quote {
|
||||
quote = !quote
|
||||
continue
|
||||
}
|
||||
return i + 1, false
|
||||
if s[i] != '.' {
|
||||
continue
|
||||
}
|
||||
j := i - 1
|
||||
for j >= 0 && s[j] == '\\' {
|
||||
j--
|
||||
}
|
||||
|
||||
if (j-i)%2 == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
return i + 1, false
|
||||
}
|
||||
return i + 1, true
|
||||
}
|
||||
@@ -149,17 +152,38 @@ func NextLabel(s string, offset int) (i int, end bool) {
|
||||
// The bool start is true when the start of the string has been overshot.
|
||||
// Also see NextLabel.
|
||||
func PrevLabel(s string, n int) (i int, start bool) {
|
||||
if s == "" {
|
||||
return 0, true
|
||||
}
|
||||
if n == 0 {
|
||||
return len(s), false
|
||||
}
|
||||
lab := Split(s)
|
||||
if lab == nil {
|
||||
return 0, true
|
||||
|
||||
l := len(s) - 1
|
||||
if s[l] == '.' {
|
||||
l--
|
||||
}
|
||||
if n > len(lab) {
|
||||
return 0, true
|
||||
|
||||
for ; l >= 0 && n > 0; l-- {
|
||||
if s[l] != '.' {
|
||||
continue
|
||||
}
|
||||
j := l - 1
|
||||
for j >= 0 && s[j] == '\\' {
|
||||
j--
|
||||
}
|
||||
|
||||
if (j-l)%2 == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
n--
|
||||
if n == 0 {
|
||||
return l + 1, false
|
||||
}
|
||||
}
|
||||
return lab[len(lab)-n], false
|
||||
|
||||
return 0, n > 1
|
||||
}
|
||||
|
||||
// equal compares a and b while ignoring case. It returns true when equal otherwise false.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user