diff --git a/cmd/crc/cmd/daemon.go b/cmd/crc/cmd/daemon.go index d9e9dc0cbb..2bb8b6964f 100644 --- a/cmd/crc/cmd/daemon.go +++ b/cmd/crc/cmd/daemon.go @@ -16,7 +16,7 @@ import ( "github.com/containers/gvisor-tap-vsock/pkg/virtualnetwork" "github.com/crc-org/crc/pkg/crc/adminhelper" "github.com/crc-org/crc/pkg/crc/api" - "github.com/crc-org/crc/pkg/crc/api/websocket" + "github.com/crc-org/crc/pkg/crc/api/events" crcConfig "github.com/crc-org/crc/pkg/crc/config" "github.com/crc-org/crc/pkg/crc/constants" "github.com/crc-org/crc/pkg/crc/daemonclient" @@ -145,7 +145,7 @@ func run(configuration *types.Configuration) error { mux.Handle("/network/", http.StripPrefix("/network", vn.Mux())) machineClient := newMachine() mux.Handle("/api/", http.StripPrefix("/api", api.NewMux(config, machineClient, logging.Memory, segmentClient))) - mux.Handle("/socket/", http.StripPrefix("/socket", websocket.NewWebsocketServer(machineClient))) + mux.Handle("/events", http.StripPrefix("/events", events.NewEventServer(machineClient))) s := &http.Server{ Handler: handlers.LoggingHandler(os.Stderr, mux), ReadHeaderTimeout: 10 * time.Second, diff --git a/cmd/crc/cmd/status.go b/cmd/crc/cmd/status.go index 7a5a182e9f..b0c967a4ff 100644 --- a/cmd/crc/cmd/status.go +++ b/cmd/crc/cmd/status.go @@ -81,20 +81,14 @@ func runWatchStatus(writer io.Writer, client *daemonclient.Client, cacheDir stri isPullInit := false - for { - loadResult, err := client.WebSocketClient.Status() - - if err != nil { - return err - } - + err := client.SSEClient.Status(func(loadResult *types.ClusterLoadResult) { if !isPullInit { ramBar, cpuBars = createBars(loadResult.CPUUse, writer) barPull = pb.NewPool(append([]*pb.ProgressBar{ramBar}, cpuBars...)...) isPullInit = true - err = barPull.Start() + err := barPull.Start() if err != nil { - return nil + return } } @@ -103,7 +97,8 @@ func runWatchStatus(writer io.Writer, client *daemonclient.Client, cacheDir stri for i, cpuLoad := range loadResult.CPUUse { cpuBars[i].SetCurrent(cpuLoad) } - } + }) + return err } func createBars(cpuUse []int64, writer io.Writer) (ramBar *pb.ProgressBar, cpuBars []*pb.ProgressBar) { diff --git a/go.mod b/go.mod index 486ed91487..a16b8d6d23 100644 --- a/go.mod +++ b/go.mod @@ -39,6 +39,7 @@ require ( github.com/pborman/uuid v1.2.1 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 github.com/pkg/errors v0.9.1 + github.com/r3labs/sse/v2 v2.10.0 github.com/segmentio/analytics-go/v3 v3.2.1 github.com/shirou/gopsutil/v3 v3.23.1 github.com/sirupsen/logrus v1.9.3 @@ -61,7 +62,6 @@ require ( k8s.io/apimachinery v0.25.6 k8s.io/client-go v0.25.6 libvirt.org/go/libvirtxml v1.9000.0 - nhooyr.io/websocket v1.8.7 ) require ( @@ -94,7 +94,6 @@ require ( github.com/felixge/httpsnoop v1.0.3 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect - github.com/gin-gonic/gin v1.9.1 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-openapi/analysis v0.21.4 // indirect @@ -194,6 +193,7 @@ require ( google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef // indirect google.golang.org/grpc v1.52.0 // indirect google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect diff --git a/go.sum b/go.sum index 44950aee07..6c1f5a7678 100644 --- a/go.sum +++ b/go.sum @@ -88,7 +88,6 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/cavaliergopher/grab/v3 v3.0.1 h1:4z7TkBfmPjmLAAmkkAZNX/6QJ1nNFdv3SdIHXju0Fr4= github.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -98,7 +97,6 @@ github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cb github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= github.com/cheggaaa/pb/v3 v3.1.0 h1:3uouEsl32RL7gTiQsuaXD4Bzbfl5tGztXGUvXbs4O04= github.com/cheggaaa/pb/v3 v3.1.0/go.mod h1:YjrevcBqadFDaGQKRdmZxTY42pXEqda48Ea3lt0K/BE= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -200,14 +198,8 @@ github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= -github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -256,13 +248,6 @@ github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+ github.com/go-openapi/validate v0.21.0/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= github.com/go-openapi/validate v0.22.0 h1:b0QecH6VslW/TxtpKgzpO1SNG7GU2FsaqKdP1E2T50Y= github.com/go-openapi/validate v0.22.0/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= -github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= @@ -292,13 +277,6 @@ github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWe github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= -github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= -github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= -github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= @@ -401,8 +379,6 @@ github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= @@ -473,7 +449,6 @@ github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGu github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -489,7 +464,6 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= @@ -511,8 +485,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/letsencrypt/boulder v0.0.0-20221109233200-85aa52084eaf h1:ndns1qx/5dL43g16EQkPV/i8+b3l5bYQwLeoSBe7tS8= github.com/letsencrypt/boulder v0.0.0-20221109233200-85aa52084eaf/go.mod h1:aGkAgvWY/IUcVFfuly53REpfv5edu25oij+qHRFaraA= github.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2 h1:DZMFueDbfz6PNc1GwDRA8+6lBx1TB9UnxDQliCqR73Y= @@ -588,7 +560,6 @@ github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6U github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= @@ -669,6 +640,8 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0= +github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= @@ -768,13 +741,8 @@ github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7Am github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/u-root/uio v0.0.0-20210528114334-82958018845c h1:BFvcl34IGnw8yvJi8hlqLFo9EshRInwWBs2M5fGWzQA= github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= -github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= @@ -828,7 +796,6 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -900,6 +867,7 @@ golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -1222,6 +1190,8 @@ google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cn google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/statsd.v2 v2.0.0 h1:FXkZSCZIH17vLCO5sO2UucTHsH9pc+17F6pl3JVCwMc= +gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= +gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= 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/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1282,8 +1252,6 @@ k8s.io/utils v0.0.0-20220823124924-e9cbc92d1a73 h1:H9TCJUUx+2VA0ZiD9lvtaX8fthFsM k8s.io/utils v0.0.0-20220823124924-e9cbc92d1a73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= libvirt.org/go/libvirtxml v1.9000.0 h1:Qu4rHf7lgsbWSdLapNzHAECwLlilyocn4R3v14mYIoU= libvirt.org/go/libvirtxml v1.9000.0/go.mod h1:7Oq2BLDstLr/XtoQD8Fr3mfDNrzlI3utYKySXF2xkng= -nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= -nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/pkg/crc/api/client/sse_client.go b/pkg/crc/api/client/sse_client.go new file mode 100644 index 0000000000..c0d95d8615 --- /dev/null +++ b/pkg/crc/api/client/sse_client.go @@ -0,0 +1,36 @@ +package client + +import ( + "encoding/json" + "net/http" + + "github.com/crc-org/crc/pkg/crc/logging" + "github.com/crc-org/crc/pkg/crc/machine/types" + "github.com/r3labs/sse/v2" +) + +type SSEClient struct { + client *sse.Client +} + +func NewSSEClient(transport *http.Transport) *SSEClient { + client := sse.NewClient("http://unix/events") + client.Connection.Transport = transport + return &SSEClient{ + client: client, + } +} + +func (c *SSEClient) Status(statusCallback func(*types.ClusterLoadResult)) error { + err := c.client.Subscribe("status", func(msg *sse.Event) { + wmState := &types.ClusterLoadResult{} + err := json.Unmarshal(msg.Data, wmState) + if err != nil { + logging.Errorf("Could not parse status event: %s", err) + return + } + statusCallback(wmState) + }) + + return err +} diff --git a/pkg/crc/api/client/websocket_client.go b/pkg/crc/api/client/websocket_client.go deleted file mode 100644 index abfe7ac62c..0000000000 --- a/pkg/crc/api/client/websocket_client.go +++ /dev/null @@ -1,39 +0,0 @@ -package client - -import ( - "context" - "net/http" - - "github.com/crc-org/crc/pkg/crc/machine/types" - "nhooyr.io/websocket" - "nhooyr.io/websocket/wsjson" -) - -type WebSocketClient struct { - client *http.Client - statusConnection *websocket.Conn -} - -func NewWebSocketClient(httpClient *http.Client) *WebSocketClient { - return &WebSocketClient{ - client: httpClient, - } -} - -func (c *WebSocketClient) Status() (*types.ClusterLoadResult, error) { - ctx := context.Background() - - if c.statusConnection == nil { - conn, _, err := websocket.Dial(ctx, "ws://unix/socket/status", &websocket.DialOptions{HTTPClient: c.client}) - - if err != nil { - return nil, err - } - c.statusConnection = conn - } - - wmState := &types.ClusterLoadResult{} - err := wsjson.Read(ctx, c.statusConnection, wmState) - return wmState, err - -} diff --git a/pkg/crc/api/events/event_server.go b/pkg/crc/api/events/event_server.go new file mode 100644 index 0000000000..3512f1428f --- /dev/null +++ b/pkg/crc/api/events/event_server.go @@ -0,0 +1,74 @@ +package events + +import ( + "net/http" + "sync" + + "github.com/crc-org/crc/pkg/crc/logging" + "github.com/crc-org/crc/pkg/crc/machine" + "github.com/r3labs/sse/v2" +) + +type EventServer struct { + sseServer *sse.Server + muStreams sync.RWMutex + streams map[string]EventStream + machine machine.Client +} + +func NewEventServer(machine machine.Client) *EventServer { + + var sseServer = sse.New() + sseServer.AutoReplay = false + + eventServer := &EventServer{ + sseServer: sseServer, + machine: machine, + streams: map[string]EventStream{}, + } + + sseServer.OnSubscribe = func(streamId string, sub *sse.Subscriber) { + logging.Debugf("OnSubscribe on channel: %s", streamId) + stream, ok := eventServer.streams[streamId] + eventServer.muStreams.Lock() + defer eventServer.muStreams.Unlock() + if !ok { + stream = createEventStream(eventServer, streamId) + if stream == nil { + logging.Errorf("Could not create EventStream for %s", streamId) + return + } + eventServer.streams[streamId] = stream + } + + stream.AddSubscriber(sub) + } + + sseServer.OnUnsubscribe = func(streamId string, sub *sse.Subscriber) { + logging.Debugf("OnUnsubscribe on channel: %s", streamId) + stream, ok := eventServer.streams[streamId] + if !ok { + logging.Debugf("Could not find stream:%s", streamId) + return + } + stream.RemoveSubscriber(sub) + } + + sseServer.CreateStream(LOGS) + sseServer.CreateStream(STATUS) + return eventServer +} + +func (es *EventServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + es.sseServer.ServeHTTP(w, r) +} + +func createEventStream(server *EventServer, streamID string) EventStream { + switch streamID { + case LOGS: + return newLogsStream(server) + case STATUS: + return newStatusStream(server) + } + return nil +} diff --git a/pkg/crc/api/events/event_stream.go b/pkg/crc/api/events/event_stream.go new file mode 100644 index 0000000000..121239a4c0 --- /dev/null +++ b/pkg/crc/api/events/event_stream.go @@ -0,0 +1,46 @@ +package events + +import ( + "sync" + + "github.com/r3labs/sse/v2" +) + +type EventStream interface { + AddSubscriber(subscriber *sse.Subscriber) + RemoveSubscriber(subscriber *sse.Subscriber) +} + +type eventStream struct { + subscribers map[*sse.Subscriber]interface{} + producer EventProducer + publisher EventPublisher + streamMutex sync.Mutex +} + +func newStream(producer EventProducer, publisher EventPublisher) EventStream { + return &eventStream{ + subscribers: map[*sse.Subscriber]interface{}{}, + producer: producer, + publisher: publisher, + } +} + +func (es *eventStream) AddSubscriber(subscriber *sse.Subscriber) { + es.streamMutex.Lock() + defer es.streamMutex.Unlock() + + es.subscribers[subscriber] = struct{}{} + if len(es.subscribers) == 1 { + es.producer.Start(es.publisher) + } +} +func (es *eventStream) RemoveSubscriber(subscriber *sse.Subscriber) { + es.streamMutex.Lock() + defer es.streamMutex.Unlock() + + delete(es.subscribers, subscriber) + if len(es.subscribers) == 0 { + es.producer.Stop() + } +} diff --git a/pkg/crc/api/events/events.go b/pkg/crc/api/events/events.go new file mode 100644 index 0000000000..012ad9dedb --- /dev/null +++ b/pkg/crc/api/events/events.go @@ -0,0 +1,33 @@ +package events + +import "github.com/r3labs/sse/v2" + +const ( + LOGS = "logs" // Logs event channel, contains daemon logs + STATUS = "status" // status event channel, contains VM load info +) + +type EventPublisher interface { + Publish(event *sse.Event) +} + +type EventProducer interface { + Start(publisher EventPublisher) + Stop() +} + +type eventPublisher struct { + streamID string + sseServer *sse.Server +} + +func newEventPublisher(streamID string, server *sse.Server) EventPublisher { + return &eventPublisher{ + streamID: streamID, + sseServer: server, + } +} + +func (ep *eventPublisher) Publish(event *sse.Event) { + ep.sseServer.Publish(ep.streamID, event) +} diff --git a/pkg/crc/api/events/log_stream.go b/pkg/crc/api/events/log_stream.go new file mode 100644 index 0000000000..b2fd053cd2 --- /dev/null +++ b/pkg/crc/api/events/log_stream.go @@ -0,0 +1,73 @@ +package events + +import ( + "github.com/crc-org/crc/pkg/crc/logging" + "github.com/r3labs/sse/v2" + "github.com/sirupsen/logrus" +) + +type streamHook struct { + server *sse.Server + formatter logrus.Formatter + level logrus.Level +} + +type logsStream struct { + hasInitialized bool + server *EventServer +} + +func newSSEStreamHook(server *sse.Server) *streamHook { + return &streamHook{ + server, + &logrus.JSONFormatter{ + TimestampFormat: "", + DisableTimestamp: false, + DisableHTMLEscape: false, + DataKey: "", + FieldMap: nil, + CallerPrettyfier: nil, + PrettyPrint: false, + }, + logging.DefaultLogLevel(), + } +} + +func newLogsStream(server *EventServer) EventStream { + return &logsStream{ + hasInitialized: false, + server: server, + } +} + +func (s *streamHook) Levels() []logrus.Level { + var levels []logrus.Level + for _, level := range logrus.AllLevels { + if level <= s.level { + levels = append(levels, level) + } + } + return levels +} + +func (s *streamHook) Fire(entry *logrus.Entry) error { + line, err := s.formatter.Format(entry) + if err != nil { + return err + } + + s.server.Publish(LOGS, &sse.Event{Event: []byte(LOGS), Data: line}) + return nil +} + +func (l *logsStream) AddSubscriber(_ *sse.Subscriber) { + if !l.hasInitialized { + logrus.AddHook(newSSEStreamHook(l.server.sseServer)) + l.hasInitialized = true + } + +} + +func (l *logsStream) RemoveSubscriber(_ *sse.Subscriber) { + // do nothing as we could not remove log listener +} diff --git a/pkg/crc/api/websocket/status.go b/pkg/crc/api/events/status.go similarity index 62% rename from pkg/crc/api/websocket/status.go rename to pkg/crc/api/events/status.go index e6cc9d0497..a942306500 100644 --- a/pkg/crc/api/websocket/status.go +++ b/pkg/crc/api/events/status.go @@ -1,17 +1,17 @@ -package websocket +package events import ( "encoding/json" - "io" "time" "github.com/crc-org/crc/pkg/crc/logging" crcMachine "github.com/crc-org/crc/pkg/crc/machine" + "github.com/r3labs/sse/v2" ) type genData func() (interface{}, error) -// A data generator for a websocket endpoint. It will fetch data at regular intervals, and +// TickListener a data generator for an event stream. It will fetch data at regular intervals, and // send it to all clients connected to the endpoint. type TickListener struct { done chan bool @@ -19,14 +19,18 @@ type TickListener struct { tickPeriod time.Duration } -func NewStatusListener(machine crcMachine.Client) ConnectionListener { +func newStatusStream(server *EventServer) EventStream { + return newStream(NewStatusListener(server.machine), newEventPublisher(STATUS, server.sseServer)) +} + +func NewStatusListener(machine crcMachine.Client) EventProducer { getStatus := func() (interface{}, error) { return machine.GetClusterLoad() } return NewTickListener(getStatus) } -func NewTickListener(generator genData) ConnectionListener { +func NewTickListener(generator genData) EventProducer { return &TickListener{ done: make(chan bool), generator: generator, @@ -34,8 +38,8 @@ func NewTickListener(generator genData) ConnectionListener { } } -func (s *TickListener) start(dataSender io.Writer) { - +func (s *TickListener) Start(publisher EventPublisher) { + logging.Debug("Start sending status events") ticker := time.NewTicker(s.tickPeriod) go func() { for { @@ -56,16 +60,13 @@ func (s *TickListener) start(dataSender io.Writer) { logging.Errorf("unexpected error during status object to JSON conversion: %v", err) continue } - _, err = dataSender.Write(bytes) - if err != nil { - logging.Errorf("unexpected error during writing data to WebSocket: %v", err) - } + publisher.Publish(&sse.Event{Event: []byte("status"), Data: bytes}) } } }() - } -func (s *TickListener) stop() { +func (s *TickListener) Stop() { + logging.Debug("Stop sending status events") s.done <- true } diff --git a/pkg/crc/api/websocket/endpoint.go b/pkg/crc/api/websocket/endpoint.go deleted file mode 100644 index 604d36d537..0000000000 --- a/pkg/crc/api/websocket/endpoint.go +++ /dev/null @@ -1,71 +0,0 @@ -package websocket - -import ( - "io" - "sync" -) - -// websocket endpoint such as "/status". It has a number of clients connected -// to it, and works together with an EndpointHandler to listen for the data to -// send to its clients. -type Endpoint interface { - addClient(client *wsClient) - deleteClient(client *wsClient) - setHandler(handler *EndpointHandler) - io.Writer -} - -type endpoint struct { - clients map[*wsClient]struct{} - clientsMutex sync.Mutex - handler *EndpointHandler -} - -func NewEndpoint() Endpoint { - return &endpoint{ - clients: make(map[*wsClient]struct{}), - } -} - -func (e *endpoint) addClient(client *wsClient) { - e.clientsMutex.Lock() - defer e.clientsMutex.Unlock() - - e.clients[client] = struct{}{} - if len(e.clients) == 1 { - e.handler.hasClient() - } - -} - -func (e *endpoint) deleteClient(client *wsClient) { - e.clientsMutex.Lock() - defer e.clientsMutex.Unlock() - - delete(e.clients, client) - // all clients disconnected - if len(e.clients) == 0 { - e.handler.noClient() - } -} - -// send data bytes to clients -func (e *endpoint) Write(data []byte) (int, error) { - e.clientsMutex.Lock() - defer e.clientsMutex.Unlock() - - // use 'msgs' client channel to pass data to client impl - for client := range e.clients { - select { - case client.msgs <- data: - default: - go client.closeSlow() - } - } - - return len(data), nil -} - -func (e *endpoint) setHandler(handler *EndpointHandler) { - e.handler = handler -} diff --git a/pkg/crc/api/websocket/handler.go b/pkg/crc/api/websocket/handler.go deleted file mode 100644 index e2ef195ef5..0000000000 --- a/pkg/crc/api/websocket/handler.go +++ /dev/null @@ -1,44 +0,0 @@ -package websocket - -import ( - "io" -) - -// An EndpointHandler tells a set of listeners when they should be generating -// data for sending to clients. -type EndpointHandler struct { - listeners []ConnectionListener - dataSender io.Writer -} - -type ConnectionListener interface { - // called when first client connected - start(dataSender io.Writer) - // called when all clients close connections - stop() -} - -func NewEndpointHandler(dataSender io.Writer) *EndpointHandler { - handler := &EndpointHandler{ - listeners: make([]ConnectionListener, 0), - dataSender: dataSender, - } - - return handler -} - -func (h *EndpointHandler) hasClient() { - for _, l := range h.listeners { - l.start(h.dataSender) - } -} - -func (h *EndpointHandler) noClient() { - for _, l := range h.listeners { - l.stop() - } -} - -func (h *EndpointHandler) addListener(listener ConnectionListener) { - h.listeners = append(h.listeners, listener) -} diff --git a/pkg/crc/api/websocket/server.go b/pkg/crc/api/websocket/server.go deleted file mode 100644 index b66090c78a..0000000000 --- a/pkg/crc/api/websocket/server.go +++ /dev/null @@ -1,128 +0,0 @@ -package websocket - -import ( - goContext "context" - "errors" - "net/http" - "time" - - "github.com/crc-org/crc/pkg/crc/logging" - "github.com/crc-org/crc/pkg/crc/machine" - "nhooyr.io/websocket" -) - -type wsClient struct { - msgs chan []byte - closeSlow func() -} - -type Server struct { - // clientMessageBuffer controls the max number - // of messages that can be queued for a subscriber - // before it is kicked. - // - // Defaults to 16. - clientMessageBuffer int - - // serveMux routes the various endpoints to the appropriate handler. - serveMux http.ServeMux - - endpoints map[string]Endpoint -} - -func NewWebsocketServer(machine machine.Client) *Server { - ws := &Server{ - clientMessageBuffer: 16, - endpoints: map[string]Endpoint{}, - } - - ws.endpoints["/status"] = createStatusEndpoint(machine) - - ws.serveMux.HandleFunc("/status", func(writer http.ResponseWriter, request *http.Request) { - ws.handleEndpoint("/status", writer, request) - }) - - return ws -} - -func (ws *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { - ws.serveMux.ServeHTTP(w, r) -} - -func (ws *Server) handleEndpoint(endpointPath string, w http.ResponseWriter, r *http.Request) { - logging.Debugf("handle %s request", endpointPath) - c, err := websocket.Accept(w, r, nil) - if err != nil { - logging.Infof("%v", err) - return - } - - defer c.Close(websocket.StatusInternalError, "") - - // TODO add reading messages from client - - err = ws.subscribe(endpointPath, r.Context(), c) - if errors.Is(err, goContext.Canceled) { - return - } - if websocket.CloseStatus(err) == websocket.StatusNormalClosure || - websocket.CloseStatus(err) == websocket.StatusGoingAway { - return - } - if err != nil { - logging.Infof("%v", err) - return - } -} - -func (ws *Server) subscribe(endpointPath string, ctx goContext.Context, c *websocket.Conn) error { - logging.Debugf("Subscribe: %s", endpointPath) - ctx = c.CloseRead(ctx) - client := &wsClient{ - msgs: make(chan []byte, ws.clientMessageBuffer), - closeSlow: func() { - c.Close(websocket.StatusPolicyViolation, "connection too slow to keep up with messages") - }, - } - - ws.addClient(endpointPath, client) - defer ws.deleteClient(endpointPath, client) - - // pull data from 'msgs' client channel and write to socket - for { - select { - case msg := <-client.msgs: - err := writeTimeout(ctx, time.Second*5, c, msg) - if err != nil { - return err - } - case <-ctx.Done(): - return ctx.Err() - } - } -} - -// addClient registers a client. -func (ws *Server) addClient(endpointPath string, s *wsClient) { - ws.endpoints[endpointPath].addClient(s) -} - -// deleteClient deletes the given client. -func (ws *Server) deleteClient(endpointPath string, s *wsClient) { - ws.endpoints[endpointPath].deleteClient(s) -} - -func writeTimeout(ctx goContext.Context, timeout time.Duration, c *websocket.Conn, msg []byte) error { - ctx, cancel := goContext.WithTimeout(ctx, timeout) - defer cancel() - - return c.Write(ctx, websocket.MessageText, msg) -} - -func createStatusEndpoint(machine machine.Client) Endpoint { - statusEndpoint := NewEndpoint() - statusEndpointHandler := NewEndpointHandler(statusEndpoint) - statusEndpoint.setHandler(statusEndpointHandler) - statusEndpointHandler.addListener(NewStatusListener(machine)) - return statusEndpoint -} diff --git a/pkg/crc/daemonclient/client.go b/pkg/crc/daemonclient/client.go index cf018b05a9..04ba1783d7 100644 --- a/pkg/crc/daemonclient/client.go +++ b/pkg/crc/daemonclient/client.go @@ -13,9 +13,9 @@ import ( const genericDaemonNotRunningMessage = "Is 'crc daemon' running? Cannot reach daemon API" type Client struct { - NetworkClient *networkclient.Client - APIClient client.Client - WebSocketClient *client.WebSocketClient + NetworkClient *networkclient.Client + APIClient client.Client + SSEClient *client.SSEClient } func New() *Client { @@ -26,9 +26,7 @@ func New() *Client { APIClient: client.New(&http.Client{ Transport: transport(), }, "http://unix/api"), - WebSocketClient: client.NewWebSocketClient(&http.Client{ - Transport: transport(), - }), + SSEClient: client.NewSSEClient(transport()), } } diff --git a/pkg/crc/logging/logging.go b/pkg/crc/logging/logging.go index 472733b184..3a971d6b08 100644 --- a/pkg/crc/logging/logging.go +++ b/pkg/crc/logging/logging.go @@ -68,6 +68,14 @@ func InitLogrus(logFilePath string) { } } +func DefaultLogLevel() logrus.Level { + level, err := logrus.ParseLevel(logLevel) + if err != nil { + level = logrus.InfoLevel + } + return level +} + func defaultLogLevel() string { defaultLevel := "info" envLogLevel := os.Getenv("CRC_LOG_LEVEL") diff --git a/vendor/github.com/r3labs/sse/v2/.gitignore b/vendor/github.com/r3labs/sse/v2/.gitignore new file mode 100644 index 0000000000..d48c759d6c --- /dev/null +++ b/vendor/github.com/r3labs/sse/v2/.gitignore @@ -0,0 +1,2 @@ +.idea +.vscode \ No newline at end of file diff --git a/vendor/github.com/r3labs/sse/v2/.golangci.yml b/vendor/github.com/r3labs/sse/v2/.golangci.yml new file mode 100644 index 0000000000..5a76e9a0fd --- /dev/null +++ b/vendor/github.com/r3labs/sse/v2/.golangci.yml @@ -0,0 +1,15 @@ +linters: + enable-all: true + disable: + - gofmt + - gofumpt + - goimports + - golint # deprecated + - interfacer # deprecated + - maligned # deprecated + - scopelint # deprecated + - varnamelen + +linters-settings: + govet: + enable-all: true diff --git a/vendor/github.com/r3labs/sse/v2/CONTRIBUTING.md b/vendor/github.com/r3labs/sse/v2/CONTRIBUTING.md new file mode 100644 index 0000000000..b9c7859d3c --- /dev/null +++ b/vendor/github.com/r3labs/sse/v2/CONTRIBUTING.md @@ -0,0 +1,80 @@ +# Contributing guidelines + +Looking to contribute something to this project? Here's how you can help: + +Please take a moment to review this document in order to make the contribution process easy and effective for everyone involved. + +Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue or assessing patches and features. + +We also have a [code of conduct](https://ernest.io/conduct). + +## Using the issue tracker + +The issue tracker is the preferred channel for [bug reports](#bug-reports), [features requests](#feature-requests) and [submitting pull requests](#pull-requests), but please respect the following restrictions: + +* Please **do not** use the issue tracker for personal support requests. + +* Please **do not** derail issues. Keep the discussion on topic and + respect the opinions of others. + + +## Bug reports + +A bug is a _demonstrable problem_ that is caused by the code in the repository. +Good bug reports are extremely helpful - thank you! + +Guidelines for bug reports: + +1. **Use the GitHub issue search** — check if the issue has already been + reported. + +2. **Check if the issue has been fixed** — try to reproduce it using the + latest `master` or `develop` branch in the repository. + +3. **Isolate the problem** — create a reduced test case and a live example. + +A good bug report shouldn't leave others needing to chase you up for more +information. Please try to be as detailed as possible in your report. What is +your environment? What steps will reproduce the issue? Which environment experience the problem? What would you expect to be the outcome? All these +details will help people to fix any potential bugs. + +Example: + +> Short and descriptive example bug report title +> +> A summary of the issue and the environment in which it occurs. If +> suitable, include the steps required to reproduce the bug. +> +> 1. This is the first step +> 2. This is the second step +> 3. Further steps, etc. +> +> `` - a link to the reduced test case +> +> Any other information you want to share that is relevant to the issue being +> reported. This might include the lines of code that you have identified as +> causing the bug, and potential solutions (and your opinions on their +> merits). + + +## Feature requests + +Feature requests are welcome. But take a moment to find out whether your idea +fits with the scope and aims of the project. It's up to *you* to make a strong +case to convince the project's developers of the merits of this feature. Please +provide as much detail and context as possible. + + +## Pull requests + +Good pull requests - patches, improvements, new features - are a fantastic +help. They should remain focused in scope and avoid containing unrelated +commits. + +[**Please ask first**](https://ernest.io/community) before embarking on any significant pull request (e.g. +implementing features, refactoring code, porting to a different language), +otherwise you risk spending a lot of time working on something that the +project's developers might not want to merge into the project. + +Please adhere to the coding conventions used throughout a project (indentation, +accurate comments, etc.) and any other requirements (such as test coverage). diff --git a/vendor/github.com/r3labs/sse/v2/LICENSE b/vendor/github.com/r3labs/sse/v2/LICENSE new file mode 100644 index 0000000000..a612ad9813 --- /dev/null +++ b/vendor/github.com/r3labs/sse/v2/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/vendor/github.com/r3labs/sse/v2/Makefile b/vendor/github.com/r3labs/sse/v2/Makefile new file mode 100644 index 0000000000..a63b7001e0 --- /dev/null +++ b/vendor/github.com/r3labs/sse/v2/Makefile @@ -0,0 +1,20 @@ +install: + go install -v + +build: + go build -v ./... + +lint: + golint ./... + go vet ./... + +test: + go test -v ./... --cover + +deps: + go get -u gopkg.in/cenkalti/backoff.v1 + go get -u github.com/golang/lint/golint + go get -u github.com/stretchr/testify + +clean: + go clean diff --git a/vendor/github.com/r3labs/sse/v2/README.md b/vendor/github.com/r3labs/sse/v2/README.md new file mode 100644 index 0000000000..c2201be698 --- /dev/null +++ b/vendor/github.com/r3labs/sse/v2/README.md @@ -0,0 +1,191 @@ +# SSE - Server Sent Events Client/Server Library for Go + +## Synopsis + +SSE is a client/server implementation for Server Sent Events for Golang. + +## Build status + +* Master: [![CircleCI Master](https://circleci.com/gh/r3labs/sse.svg?style=svg)](https://circleci.com/gh/r3labs/sse) + +## Quick start + +To install: +``` +go get github.com/r3labs/sse/v2 +``` + +To Test: + +```sh +$ make deps +$ make test +``` + +#### Example Server + +There are two parts of the server. It is comprised of the message scheduler and a http handler function. +The messaging system is started when running: + +```go +func main() { + server := sse.New() +} +``` + +To add a stream to this handler: + +```go +func main() { + server := sse.New() + server.CreateStream("messages") +} +``` + +This creates a new stream inside of the scheduler. Seeing as there are no consumers, publishing a message to this channel will do nothing. +Clients can connect to this stream once the http handler is started by specifying _stream_ as a url parameter, like so: + +``` +http://server/events?stream=messages +``` + + +In order to start the http server: + +```go +func main() { + server := sse.New() + + // Create a new Mux and set the handler + mux := http.NewServeMux() + mux.HandleFunc("/events", server.ServeHTTP) + + http.ListenAndServe(":8080", mux) +} +``` + +To publish messages to a stream: + +```go +func main() { + server := sse.New() + + // Publish a payload to the stream + server.Publish("messages", &sse.Event{ + Data: []byte("ping"), + }) +} +``` + +Please note there must be a stream with the name you specify and there must be subscribers to that stream + +A way to detect disconnected clients: + +```go +func main() { + server := sse.New() + + mux := http.NewServeMux() + mux.HandleFunc("/events", func(w http.ResponseWriter, r *http.Request) { + go func() { + // Received Browser Disconnection + <-r.Context().Done() + println("The client is disconnected here") + return + }() + + server.ServeHTTP(w, r) + }) + + http.ListenAndServe(":8080", mux) +} +``` + +#### Example Client + +The client exposes a way to connect to an SSE server. The client can also handle multiple events under the same url. + +To create a new client: + +```go +func main() { + client := sse.NewClient("http://server/events") +} +``` + +To subscribe to an event stream, please use the Subscribe function. This accepts the name of the stream and a handler function: + +```go +func main() { + client := sse.NewClient("http://server/events") + + client.Subscribe("messages", func(msg *sse.Event) { + // Got some data! + fmt.Println(msg.Data) + }) +} +``` + +Please note that this function will block the current thread. You can run this function in a go routine. + +If you wish to have events sent to a channel, you can use SubscribeChan: + +```go +func main() { + events := make(chan *sse.Event) + + client := sse.NewClient("http://server/events") + client.SubscribeChan("messages", events) +} +``` + +#### HTTP client parameters + +To add additional parameters to the http client, such as disabling ssl verification for self signed certs, you can override the http client or update its options: + +```go +func main() { + client := sse.NewClient("http://server/events") + client.Connection.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } +} +``` + +#### URL query parameters + +To set custom query parameters on the client or disable the stream parameter altogether: + +```go +func main() { + client := sse.NewClient("http://server/events?search=example") + + client.SubscribeRaw(func(msg *sse.Event) { + // Got some data! + fmt.Println(msg.Data) + }) +} +``` + + +## Contributing + +Please read through our +[contributing guidelines](CONTRIBUTING.md). +Included are directions for opening issues, coding standards, and notes on +development. + +Moreover, if your pull request contains patches or features, you must include +relevant unit tests. + +## Versioning + +For transparency into our release cycle and in striving to maintain backward +compatibility, this project is maintained under [the Semantic Versioning guidelines](http://semver.org/). + +## Copyright and License + +Code and documentation copyright since 2015 r3labs.io authors. + +Code released under +[the Mozilla Public License Version 2.0](LICENSE). diff --git a/vendor/github.com/r3labs/sse/v2/client.go b/vendor/github.com/r3labs/sse/v2/client.go new file mode 100644 index 0000000000..61772b624d --- /dev/null +++ b/vendor/github.com/r3labs/sse/v2/client.go @@ -0,0 +1,390 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package sse + +import ( + "bytes" + "context" + "encoding/base64" + "errors" + "fmt" + "io" + "net/http" + "sync" + "sync/atomic" + "time" + + "gopkg.in/cenkalti/backoff.v1" +) + +var ( + headerID = []byte("id:") + headerData = []byte("data:") + headerEvent = []byte("event:") + headerRetry = []byte("retry:") +) + +func ClientMaxBufferSize(s int) func(c *Client) { + return func(c *Client) { + c.maxBufferSize = s + } +} + +// ConnCallback defines a function to be called on a particular connection event +type ConnCallback func(c *Client) + +// ResponseValidator validates a response +type ResponseValidator func(c *Client, resp *http.Response) error + +// Client handles an incoming server stream +type Client struct { + Retry time.Time + ReconnectStrategy backoff.BackOff + disconnectcb ConnCallback + connectedcb ConnCallback + subscribed map[chan *Event]chan struct{} + Headers map[string]string + ReconnectNotify backoff.Notify + ResponseValidator ResponseValidator + Connection *http.Client + URL string + LastEventID atomic.Value // []byte + maxBufferSize int + mu sync.Mutex + EncodingBase64 bool + Connected bool +} + +// NewClient creates a new client +func NewClient(url string, opts ...func(c *Client)) *Client { + c := &Client{ + URL: url, + Connection: &http.Client{}, + Headers: make(map[string]string), + subscribed: make(map[chan *Event]chan struct{}), + maxBufferSize: 1 << 16, + } + + for _, opt := range opts { + opt(c) + } + + return c +} + +// Subscribe to a data stream +func (c *Client) Subscribe(stream string, handler func(msg *Event)) error { + return c.SubscribeWithContext(context.Background(), stream, handler) +} + +// SubscribeWithContext to a data stream with context +func (c *Client) SubscribeWithContext(ctx context.Context, stream string, handler func(msg *Event)) error { + operation := func() error { + resp, err := c.request(ctx, stream) + if err != nil { + return err + } + if validator := c.ResponseValidator; validator != nil { + err = validator(c, resp) + if err != nil { + return err + } + } else if resp.StatusCode != 200 { + resp.Body.Close() + return fmt.Errorf("could not connect to stream: %s", http.StatusText(resp.StatusCode)) + } + defer resp.Body.Close() + + reader := NewEventStreamReader(resp.Body, c.maxBufferSize) + eventChan, errorChan := c.startReadLoop(reader) + + for { + select { + case err = <-errorChan: + return err + case msg := <-eventChan: + handler(msg) + } + } + } + + // Apply user specified reconnection strategy or default to standard NewExponentialBackOff() reconnection method + var err error + if c.ReconnectStrategy != nil { + err = backoff.RetryNotify(operation, c.ReconnectStrategy, c.ReconnectNotify) + } else { + err = backoff.RetryNotify(operation, backoff.NewExponentialBackOff(), c.ReconnectNotify) + } + return err +} + +// SubscribeChan sends all events to the provided channel +func (c *Client) SubscribeChan(stream string, ch chan *Event) error { + return c.SubscribeChanWithContext(context.Background(), stream, ch) +} + +// SubscribeChanWithContext sends all events to the provided channel with context +func (c *Client) SubscribeChanWithContext(ctx context.Context, stream string, ch chan *Event) error { + var connected bool + errch := make(chan error) + c.mu.Lock() + c.subscribed[ch] = make(chan struct{}) + c.mu.Unlock() + + operation := func() error { + resp, err := c.request(ctx, stream) + if err != nil { + return err + } + if validator := c.ResponseValidator; validator != nil { + err = validator(c, resp) + if err != nil { + return err + } + } else if resp.StatusCode != 200 { + resp.Body.Close() + return fmt.Errorf("could not connect to stream: %s", http.StatusText(resp.StatusCode)) + } + defer resp.Body.Close() + + if !connected { + // Notify connect + errch <- nil + connected = true + } + + reader := NewEventStreamReader(resp.Body, c.maxBufferSize) + eventChan, errorChan := c.startReadLoop(reader) + + for { + var msg *Event + // Wait for message to arrive or exit + select { + case <-c.subscribed[ch]: + return nil + case err = <-errorChan: + return err + case msg = <-eventChan: + } + + // Wait for message to be sent or exit + if msg != nil { + select { + case <-c.subscribed[ch]: + return nil + case ch <- msg: + // message sent + } + } + } + } + + go func() { + defer c.cleanup(ch) + // Apply user specified reconnection strategy or default to standard NewExponentialBackOff() reconnection method + var err error + if c.ReconnectStrategy != nil { + err = backoff.RetryNotify(operation, c.ReconnectStrategy, c.ReconnectNotify) + } else { + err = backoff.RetryNotify(operation, backoff.NewExponentialBackOff(), c.ReconnectNotify) + } + + // channel closed once connected + if err != nil && !connected { + errch <- err + } + }() + err := <-errch + close(errch) + return err +} + +func (c *Client) startReadLoop(reader *EventStreamReader) (chan *Event, chan error) { + outCh := make(chan *Event) + erChan := make(chan error) + go c.readLoop(reader, outCh, erChan) + return outCh, erChan +} + +func (c *Client) readLoop(reader *EventStreamReader, outCh chan *Event, erChan chan error) { + for { + // Read each new line and process the type of event + event, err := reader.ReadEvent() + if err != nil { + if err == io.EOF { + erChan <- nil + return + } + // run user specified disconnect function + if c.disconnectcb != nil { + c.Connected = false + c.disconnectcb(c) + } + erChan <- err + return + } + + if !c.Connected && c.connectedcb != nil { + c.Connected = true + c.connectedcb(c) + } + + // If we get an error, ignore it. + var msg *Event + if msg, err = c.processEvent(event); err == nil { + if len(msg.ID) > 0 { + c.LastEventID.Store(msg.ID) + } else { + msg.ID, _ = c.LastEventID.Load().([]byte) + } + + // Send downstream if the event has something useful + if msg.hasContent() { + outCh <- msg + } + } + } +} + +// SubscribeRaw to an sse endpoint +func (c *Client) SubscribeRaw(handler func(msg *Event)) error { + return c.Subscribe("", handler) +} + +// SubscribeRawWithContext to an sse endpoint with context +func (c *Client) SubscribeRawWithContext(ctx context.Context, handler func(msg *Event)) error { + return c.SubscribeWithContext(ctx, "", handler) +} + +// SubscribeChanRaw sends all events to the provided channel +func (c *Client) SubscribeChanRaw(ch chan *Event) error { + return c.SubscribeChan("", ch) +} + +// SubscribeChanRawWithContext sends all events to the provided channel with context +func (c *Client) SubscribeChanRawWithContext(ctx context.Context, ch chan *Event) error { + return c.SubscribeChanWithContext(ctx, "", ch) +} + +// Unsubscribe unsubscribes a channel +func (c *Client) Unsubscribe(ch chan *Event) { + c.mu.Lock() + defer c.mu.Unlock() + + if c.subscribed[ch] != nil { + c.subscribed[ch] <- struct{}{} + } +} + +// OnDisconnect specifies the function to run when the connection disconnects +func (c *Client) OnDisconnect(fn ConnCallback) { + c.disconnectcb = fn +} + +// OnConnect specifies the function to run when the connection is successful +func (c *Client) OnConnect(fn ConnCallback) { + c.connectedcb = fn +} + +func (c *Client) request(ctx context.Context, stream string) (*http.Response, error) { + req, err := http.NewRequest("GET", c.URL, nil) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + + // Setup request, specify stream to connect to + if stream != "" { + query := req.URL.Query() + query.Add("stream", stream) + req.URL.RawQuery = query.Encode() + } + + req.Header.Set("Cache-Control", "no-cache") + req.Header.Set("Accept", "text/event-stream") + req.Header.Set("Connection", "keep-alive") + + lastID, exists := c.LastEventID.Load().([]byte) + if exists && lastID != nil { + req.Header.Set("Last-Event-ID", string(lastID)) + } + + // Add user specified headers + for k, v := range c.Headers { + req.Header.Set(k, v) + } + + return c.Connection.Do(req) +} + +func (c *Client) processEvent(msg []byte) (event *Event, err error) { + var e Event + + if len(msg) < 1 { + return nil, errors.New("event message was empty") + } + + // Normalize the crlf to lf to make it easier to split the lines. + // Split the line by "\n" or "\r", per the spec. + for _, line := range bytes.FieldsFunc(msg, func(r rune) bool { return r == '\n' || r == '\r' }) { + switch { + case bytes.HasPrefix(line, headerID): + e.ID = append([]byte(nil), trimHeader(len(headerID), line)...) + case bytes.HasPrefix(line, headerData): + // The spec allows for multiple data fields per event, concatenated them with "\n". + e.Data = append(e.Data[:], append(trimHeader(len(headerData), line), byte('\n'))...) + // The spec says that a line that simply contains the string "data" should be treated as a data field with an empty body. + case bytes.Equal(line, bytes.TrimSuffix(headerData, []byte(":"))): + e.Data = append(e.Data, byte('\n')) + case bytes.HasPrefix(line, headerEvent): + e.Event = append([]byte(nil), trimHeader(len(headerEvent), line)...) + case bytes.HasPrefix(line, headerRetry): + e.Retry = append([]byte(nil), trimHeader(len(headerRetry), line)...) + default: + // Ignore any garbage that doesn't match what we're looking for. + } + } + + // Trim the last "\n" per the spec. + e.Data = bytes.TrimSuffix(e.Data, []byte("\n")) + + if c.EncodingBase64 { + buf := make([]byte, base64.StdEncoding.DecodedLen(len(e.Data))) + + n, err := base64.StdEncoding.Decode(buf, e.Data) + if err != nil { + err = fmt.Errorf("failed to decode event message: %s", err) + } + e.Data = buf[:n] + } + return &e, err +} + +func (c *Client) cleanup(ch chan *Event) { + c.mu.Lock() + defer c.mu.Unlock() + + if c.subscribed[ch] != nil { + close(c.subscribed[ch]) + delete(c.subscribed, ch) + } +} + +func trimHeader(size int, data []byte) []byte { + if data == nil || len(data) < size { + return data + } + + data = data[size:] + // Remove optional leading whitespace + if len(data) > 0 && data[0] == 32 { + data = data[1:] + } + // Remove trailing new line + if len(data) > 0 && data[len(data)-1] == 10 { + data = data[:len(data)-1] + } + return data +} diff --git a/vendor/github.com/r3labs/sse/v2/event.go b/vendor/github.com/r3labs/sse/v2/event.go new file mode 100644 index 0000000000..1258038786 --- /dev/null +++ b/vendor/github.com/r3labs/sse/v2/event.go @@ -0,0 +1,114 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package sse + +import ( + "bufio" + "bytes" + "context" + "io" + "time" +) + +// Event holds all of the event source fields +type Event struct { + timestamp time.Time + ID []byte + Data []byte + Event []byte + Retry []byte + Comment []byte +} + +func (e *Event) hasContent() bool { + return len(e.ID) > 0 || len(e.Data) > 0 || len(e.Event) > 0 || len(e.Retry) > 0 +} + +// EventStreamReader scans an io.Reader looking for EventStream messages. +type EventStreamReader struct { + scanner *bufio.Scanner +} + +// NewEventStreamReader creates an instance of EventStreamReader. +func NewEventStreamReader(eventStream io.Reader, maxBufferSize int) *EventStreamReader { + scanner := bufio.NewScanner(eventStream) + initBufferSize := minPosInt(4096, maxBufferSize) + scanner.Buffer(make([]byte, initBufferSize), maxBufferSize) + + split := func(data []byte, atEOF bool) (int, []byte, error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + + // We have a full event payload to parse. + if i, nlen := containsDoubleNewline(data); i >= 0 { + return i + nlen, data[0:i], nil + } + // If we're at EOF, we have all of the data. + if atEOF { + return len(data), data, nil + } + // Request more data. + return 0, nil, nil + } + // Set the split function for the scanning operation. + scanner.Split(split) + + return &EventStreamReader{ + scanner: scanner, + } +} + +// Returns a tuple containing the index of a double newline, and the number of bytes +// represented by that sequence. If no double newline is present, the first value +// will be negative. +func containsDoubleNewline(data []byte) (int, int) { + // Search for each potentially valid sequence of newline characters + crcr := bytes.Index(data, []byte("\r\r")) + lflf := bytes.Index(data, []byte("\n\n")) + crlflf := bytes.Index(data, []byte("\r\n\n")) + lfcrlf := bytes.Index(data, []byte("\n\r\n")) + crlfcrlf := bytes.Index(data, []byte("\r\n\r\n")) + // Find the earliest position of a double newline combination + minPos := minPosInt(crcr, minPosInt(lflf, minPosInt(crlflf, minPosInt(lfcrlf, crlfcrlf)))) + // Detemine the length of the sequence + nlen := 2 + if minPos == crlfcrlf { + nlen = 4 + } else if minPos == crlflf || minPos == lfcrlf { + nlen = 3 + } + return minPos, nlen +} + +// Returns the minimum non-negative value out of the two values. If both +// are negative, a negative value is returned. +func minPosInt(a, b int) int { + if a < 0 { + return b + } + if b < 0 { + return a + } + if a > b { + return b + } + return a +} + +// ReadEvent scans the EventStream for events. +func (e *EventStreamReader) ReadEvent() ([]byte, error) { + if e.scanner.Scan() { + event := e.scanner.Bytes() + return event, nil + } + if err := e.scanner.Err(); err != nil { + if err == context.Canceled { + return nil, io.EOF + } + return nil, err + } + return nil, io.EOF +} diff --git a/vendor/github.com/r3labs/sse/v2/event_log.go b/vendor/github.com/r3labs/sse/v2/event_log.go new file mode 100644 index 0000000000..aa17dad058 --- /dev/null +++ b/vendor/github.com/r3labs/sse/v2/event_log.go @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package sse + +import ( + "strconv" + "time" +) + +// EventLog holds all of previous events +type EventLog []*Event + +// Add event to eventlog +func (e *EventLog) Add(ev *Event) { + if !ev.hasContent() { + return + } + + ev.ID = []byte(e.currentindex()) + ev.timestamp = time.Now() + *e = append(*e, ev) +} + +// Clear events from eventlog +func (e *EventLog) Clear() { + *e = nil +} + +// Replay events to a subscriber +func (e *EventLog) Replay(s *Subscriber) { + for i := 0; i < len(*e); i++ { + id, _ := strconv.Atoi(string((*e)[i].ID)) + if id >= s.eventid { + s.connection <- (*e)[i] + } + } +} + +func (e *EventLog) currentindex() string { + return strconv.Itoa(len(*e)) +} diff --git a/vendor/github.com/r3labs/sse/v2/http.go b/vendor/github.com/r3labs/sse/v2/http.go new file mode 100644 index 0000000000..c7a2b434a9 --- /dev/null +++ b/vendor/github.com/r3labs/sse/v2/http.go @@ -0,0 +1,120 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package sse + +import ( + "bytes" + "fmt" + "net/http" + "strconv" + "time" +) + +// ServeHTTP serves new connections with events for a given stream ... +func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + flusher, err := w.(http.Flusher) + if !err { + http.Error(w, "Streaming unsupported!", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + + for k, v := range s.Headers { + w.Header().Set(k, v) + } + + // Get the StreamID from the URL + streamID := r.URL.Query().Get("stream") + if streamID == "" { + http.Error(w, "Please specify a stream!", http.StatusInternalServerError) + return + } + + stream := s.getStream(streamID) + + if stream == nil { + if !s.AutoStream { + http.Error(w, "Stream not found!", http.StatusInternalServerError) + return + } + + stream = s.CreateStream(streamID) + } + + eventid := 0 + if id := r.Header.Get("Last-Event-ID"); id != "" { + var err error + eventid, err = strconv.Atoi(id) + if err != nil { + http.Error(w, "Last-Event-ID must be a number!", http.StatusBadRequest) + return + } + } + + // Create the stream subscriber + sub := stream.addSubscriber(eventid, r.URL) + + go func() { + <-r.Context().Done() + + sub.close() + + if s.AutoStream && !s.AutoReplay && stream.getSubscriberCount() == 0 { + s.RemoveStream(streamID) + } + }() + + w.WriteHeader(http.StatusOK) + flusher.Flush() + + // Push events to client + for ev := range sub.connection { + // If the data buffer is an empty string abort. + if len(ev.Data) == 0 && len(ev.Comment) == 0 { + break + } + + // if the event has expired, dont send it + if s.EventTTL != 0 && time.Now().After(ev.timestamp.Add(s.EventTTL)) { + continue + } + + if len(ev.Data) > 0 { + fmt.Fprintf(w, "id: %s\n", ev.ID) + + if s.SplitData { + sd := bytes.Split(ev.Data, []byte("\n")) + for i := range sd { + fmt.Fprintf(w, "data: %s\n", sd[i]) + } + } else { + if bytes.HasPrefix(ev.Data, []byte(":")) { + fmt.Fprintf(w, "%s\n", ev.Data) + } else { + fmt.Fprintf(w, "data: %s\n", ev.Data) + } + } + + if len(ev.Event) > 0 { + fmt.Fprintf(w, "event: %s\n", ev.Event) + } + + if len(ev.Retry) > 0 { + fmt.Fprintf(w, "retry: %s\n", ev.Retry) + } + } + + if len(ev.Comment) > 0 { + fmt.Fprintf(w, ": %s\n", ev.Comment) + } + + fmt.Fprint(w, "\n") + + flusher.Flush() + } +} diff --git a/vendor/github.com/r3labs/sse/v2/server.go b/vendor/github.com/r3labs/sse/v2/server.go new file mode 100644 index 0000000000..d1b27af325 --- /dev/null +++ b/vendor/github.com/r3labs/sse/v2/server.go @@ -0,0 +1,156 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package sse + +import ( + "encoding/base64" + "sync" + "time" +) + +// DefaultBufferSize size of the queue that holds the streams messages. +const DefaultBufferSize = 1024 + +// Server Is our main struct +type Server struct { + // Extra headers adding to the HTTP response to each client + Headers map[string]string + // Sets a ttl that prevents old events from being transmitted + EventTTL time.Duration + // Specifies the size of the message buffer for each stream + BufferSize int + // Encodes all data as base64 + EncodeBase64 bool + // Splits an events data into multiple data: entries + SplitData bool + // Enables creation of a stream when a client connects + AutoStream bool + // Enables automatic replay for each new subscriber that connects + AutoReplay bool + + // Specifies the function to run when client subscribe or un-subscribe + OnSubscribe func(streamID string, sub *Subscriber) + OnUnsubscribe func(streamID string, sub *Subscriber) + + streams map[string]*Stream + muStreams sync.RWMutex +} + +// New will create a server and setup defaults +func New() *Server { + return &Server{ + BufferSize: DefaultBufferSize, + AutoStream: false, + AutoReplay: true, + streams: make(map[string]*Stream), + Headers: map[string]string{}, + } +} + +// NewWithCallback will create a server and setup defaults with callback function +func NewWithCallback(onSubscribe, onUnsubscribe func(streamID string, sub *Subscriber)) *Server { + return &Server{ + BufferSize: DefaultBufferSize, + AutoStream: false, + AutoReplay: true, + streams: make(map[string]*Stream), + Headers: map[string]string{}, + OnSubscribe: onSubscribe, + OnUnsubscribe: onUnsubscribe, + } +} + +// Close shuts down the server, closes all of the streams and connections +func (s *Server) Close() { + s.muStreams.Lock() + defer s.muStreams.Unlock() + + for id := range s.streams { + s.streams[id].close() + delete(s.streams, id) + } +} + +// CreateStream will create a new stream and register it +func (s *Server) CreateStream(id string) *Stream { + s.muStreams.Lock() + defer s.muStreams.Unlock() + + if s.streams[id] != nil { + return s.streams[id] + } + + str := newStream(id, s.BufferSize, s.AutoReplay, s.AutoStream, s.OnSubscribe, s.OnUnsubscribe) + str.run() + + s.streams[id] = str + + return str +} + +// RemoveStream will remove a stream +func (s *Server) RemoveStream(id string) { + s.muStreams.Lock() + defer s.muStreams.Unlock() + + if s.streams[id] != nil { + s.streams[id].close() + delete(s.streams, id) + } +} + +// StreamExists checks whether a stream by a given id exists +func (s *Server) StreamExists(id string) bool { + return s.getStream(id) != nil +} + +// Publish sends a mesage to every client in a streamID. +// If the stream's buffer is full, it blocks until the message is sent out to +// all subscribers (but not necessarily arrived the clients), or when the +// stream is closed. +func (s *Server) Publish(id string, event *Event) { + stream := s.getStream(id) + if stream == nil { + return + } + + select { + case <-stream.quit: + case stream.event <- s.process(event): + } +} + +// TryPublish is the same as Publish except that when the operation would cause +// the call to be blocked, it simply drops the message and returns false. +// Together with a small BufferSize, it can be useful when publishing the +// latest message ASAP is more important than reliable delivery. +func (s *Server) TryPublish(id string, event *Event) bool { + stream := s.getStream(id) + if stream == nil { + return false + } + + select { + case stream.event <- s.process(event): + return true + default: + return false + } +} + +func (s *Server) getStream(id string) *Stream { + s.muStreams.RLock() + defer s.muStreams.RUnlock() + return s.streams[id] +} + +func (s *Server) process(event *Event) *Event { + if s.EncodeBase64 { + output := make([]byte, base64.StdEncoding.EncodedLen(len(event.Data))) + base64.StdEncoding.Encode(output, event.Data) + event.Data = output + } + return event +} diff --git a/vendor/github.com/r3labs/sse/v2/stream.go b/vendor/github.com/r3labs/sse/v2/stream.go new file mode 100644 index 0000000000..bfbcb9b523 --- /dev/null +++ b/vendor/github.com/r3labs/sse/v2/stream.go @@ -0,0 +1,153 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package sse + +import ( + "net/url" + "sync" + "sync/atomic" +) + +// Stream ... +type Stream struct { + ID string + event chan *Event + quit chan struct{} + quitOnce sync.Once + register chan *Subscriber + deregister chan *Subscriber + subscribers []*Subscriber + Eventlog EventLog + subscriberCount int32 + // Enables replaying of eventlog to newly added subscribers + AutoReplay bool + isAutoStream bool + + // Specifies the function to run when client subscribe or un-subscribe + OnSubscribe func(streamID string, sub *Subscriber) + OnUnsubscribe func(streamID string, sub *Subscriber) +} + +// newStream returns a new stream +func newStream(id string, buffSize int, replay, isAutoStream bool, onSubscribe, onUnsubscribe func(string, *Subscriber)) *Stream { + return &Stream{ + ID: id, + AutoReplay: replay, + subscribers: make([]*Subscriber, 0), + isAutoStream: isAutoStream, + register: make(chan *Subscriber), + deregister: make(chan *Subscriber), + event: make(chan *Event, buffSize), + quit: make(chan struct{}), + Eventlog: make(EventLog, 0), + OnSubscribe: onSubscribe, + OnUnsubscribe: onUnsubscribe, + } +} + +func (str *Stream) run() { + go func(str *Stream) { + for { + select { + // Add new subscriber + case subscriber := <-str.register: + str.subscribers = append(str.subscribers, subscriber) + if str.AutoReplay { + str.Eventlog.Replay(subscriber) + } + + // Remove closed subscriber + case subscriber := <-str.deregister: + i := str.getSubIndex(subscriber) + if i != -1 { + str.removeSubscriber(i) + } + + if str.OnUnsubscribe != nil { + go str.OnUnsubscribe(str.ID, subscriber) + } + + // Publish event to subscribers + case event := <-str.event: + if str.AutoReplay { + str.Eventlog.Add(event) + } + for i := range str.subscribers { + str.subscribers[i].connection <- event + } + + // Shutdown if the server closes + case <-str.quit: + // remove connections + str.removeAllSubscribers() + return + } + } + }(str) +} + +func (str *Stream) close() { + str.quitOnce.Do(func() { + close(str.quit) + }) +} + +func (str *Stream) getSubIndex(sub *Subscriber) int { + for i := range str.subscribers { + if str.subscribers[i] == sub { + return i + } + } + return -1 +} + +// addSubscriber will create a new subscriber on a stream +func (str *Stream) addSubscriber(eventid int, url *url.URL) *Subscriber { + atomic.AddInt32(&str.subscriberCount, 1) + sub := &Subscriber{ + eventid: eventid, + quit: str.deregister, + connection: make(chan *Event, 64), + URL: url, + } + + if str.isAutoStream { + sub.removed = make(chan struct{}, 1) + } + + str.register <- sub + + if str.OnSubscribe != nil { + go str.OnSubscribe(str.ID, sub) + } + + return sub +} + +func (str *Stream) removeSubscriber(i int) { + atomic.AddInt32(&str.subscriberCount, -1) + close(str.subscribers[i].connection) + if str.subscribers[i].removed != nil { + str.subscribers[i].removed <- struct{}{} + close(str.subscribers[i].removed) + } + str.subscribers = append(str.subscribers[:i], str.subscribers[i+1:]...) +} + +func (str *Stream) removeAllSubscribers() { + for i := 0; i < len(str.subscribers); i++ { + close(str.subscribers[i].connection) + if str.subscribers[i].removed != nil { + str.subscribers[i].removed <- struct{}{} + close(str.subscribers[i].removed) + } + } + atomic.StoreInt32(&str.subscriberCount, 0) + str.subscribers = str.subscribers[:0] +} + +func (str *Stream) getSubscriberCount() int { + return int(atomic.LoadInt32(&str.subscriberCount)) +} diff --git a/vendor/github.com/r3labs/sse/v2/subscriber.go b/vendor/github.com/r3labs/sse/v2/subscriber.go new file mode 100644 index 0000000000..4b54c204f3 --- /dev/null +++ b/vendor/github.com/r3labs/sse/v2/subscriber.go @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package sse + +import "net/url" + +// Subscriber ... +type Subscriber struct { + quit chan *Subscriber + connection chan *Event + removed chan struct{} + eventid int + URL *url.URL +} + +// Close will let the stream know that the clients connection has terminated +func (s *Subscriber) close() { + s.quit <- s + if s.removed != nil { + <-s.removed + } +} diff --git a/vendor/gopkg.in/cenkalti/backoff.v1/.gitignore b/vendor/gopkg.in/cenkalti/backoff.v1/.gitignore new file mode 100644 index 0000000000..00268614f0 --- /dev/null +++ b/vendor/gopkg.in/cenkalti/backoff.v1/.gitignore @@ -0,0 +1,22 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe diff --git a/vendor/gopkg.in/cenkalti/backoff.v1/.travis.yml b/vendor/gopkg.in/cenkalti/backoff.v1/.travis.yml new file mode 100644 index 0000000000..1040404bfb --- /dev/null +++ b/vendor/gopkg.in/cenkalti/backoff.v1/.travis.yml @@ -0,0 +1,9 @@ +language: go +go: + - 1.3.3 + - tip +before_install: + - go get github.com/mattn/goveralls + - go get golang.org/x/tools/cmd/cover +script: + - $HOME/gopath/bin/goveralls -service=travis-ci diff --git a/vendor/gopkg.in/cenkalti/backoff.v1/LICENSE b/vendor/gopkg.in/cenkalti/backoff.v1/LICENSE new file mode 100644 index 0000000000..89b8179965 --- /dev/null +++ b/vendor/gopkg.in/cenkalti/backoff.v1/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Cenk Altı + +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. diff --git a/vendor/gopkg.in/cenkalti/backoff.v1/README.md b/vendor/gopkg.in/cenkalti/backoff.v1/README.md new file mode 100644 index 0000000000..13b347fb95 --- /dev/null +++ b/vendor/gopkg.in/cenkalti/backoff.v1/README.md @@ -0,0 +1,30 @@ +# Exponential Backoff [![GoDoc][godoc image]][godoc] [![Build Status][travis image]][travis] [![Coverage Status][coveralls image]][coveralls] + +This is a Go port of the exponential backoff algorithm from [Google's HTTP Client Library for Java][google-http-java-client]. + +[Exponential backoff][exponential backoff wiki] +is an algorithm that uses feedback to multiplicatively decrease the rate of some process, +in order to gradually find an acceptable rate. +The retries exponentially increase and stop increasing when a certain threshold is met. + +## Usage + +See https://godoc.org/github.com/cenkalti/backoff#pkg-examples + +## Contributing + +* I would like to keep this library as small as possible. +* Please don't send a PR without opening an issue and discussing it first. +* If proposed change is not a common use case, I will probably not accept it. + +[godoc]: https://godoc.org/github.com/cenkalti/backoff +[godoc image]: https://godoc.org/github.com/cenkalti/backoff?status.png +[travis]: https://travis-ci.org/cenkalti/backoff +[travis image]: https://travis-ci.org/cenkalti/backoff.png?branch=master +[coveralls]: https://coveralls.io/github/cenkalti/backoff?branch=master +[coveralls image]: https://coveralls.io/repos/github/cenkalti/backoff/badge.svg?branch=master + +[google-http-java-client]: https://github.com/google/google-http-java-client +[exponential backoff wiki]: http://en.wikipedia.org/wiki/Exponential_backoff + +[advanced example]: https://godoc.org/github.com/cenkalti/backoff#example_ diff --git a/vendor/gopkg.in/cenkalti/backoff.v1/backoff.go b/vendor/gopkg.in/cenkalti/backoff.v1/backoff.go new file mode 100644 index 0000000000..2102c5f2de --- /dev/null +++ b/vendor/gopkg.in/cenkalti/backoff.v1/backoff.go @@ -0,0 +1,66 @@ +// Package backoff implements backoff algorithms for retrying operations. +// +// Use Retry function for retrying operations that may fail. +// If Retry does not meet your needs, +// copy/paste the function into your project and modify as you wish. +// +// There is also Ticker type similar to time.Ticker. +// You can use it if you need to work with channels. +// +// See Examples section below for usage examples. +package backoff + +import "time" + +// BackOff is a backoff policy for retrying an operation. +type BackOff interface { + // NextBackOff returns the duration to wait before retrying the operation, + // or backoff.Stop to indicate that no more retries should be made. + // + // Example usage: + // + // duration := backoff.NextBackOff(); + // if (duration == backoff.Stop) { + // // Do not retry operation. + // } else { + // // Sleep for duration and retry operation. + // } + // + NextBackOff() time.Duration + + // Reset to initial state. + Reset() +} + +// Stop indicates that no more retries should be made for use in NextBackOff(). +const Stop time.Duration = -1 + +// ZeroBackOff is a fixed backoff policy whose backoff time is always zero, +// meaning that the operation is retried immediately without waiting, indefinitely. +type ZeroBackOff struct{} + +func (b *ZeroBackOff) Reset() {} + +func (b *ZeroBackOff) NextBackOff() time.Duration { return 0 } + +// StopBackOff is a fixed backoff policy that always returns backoff.Stop for +// NextBackOff(), meaning that the operation should never be retried. +type StopBackOff struct{} + +func (b *StopBackOff) Reset() {} + +func (b *StopBackOff) NextBackOff() time.Duration { return Stop } + +// ConstantBackOff is a backoff policy that always returns the same backoff delay. +// This is in contrast to an exponential backoff policy, +// which returns a delay that grows longer as you call NextBackOff() over and over again. +type ConstantBackOff struct { + Interval time.Duration +} + +func (b *ConstantBackOff) Reset() {} +func (b *ConstantBackOff) NextBackOff() time.Duration { return b.Interval } + +func NewConstantBackOff(d time.Duration) *ConstantBackOff { + return &ConstantBackOff{Interval: d} +} diff --git a/vendor/gopkg.in/cenkalti/backoff.v1/context.go b/vendor/gopkg.in/cenkalti/backoff.v1/context.go new file mode 100644 index 0000000000..5d15709254 --- /dev/null +++ b/vendor/gopkg.in/cenkalti/backoff.v1/context.go @@ -0,0 +1,60 @@ +package backoff + +import ( + "time" + + "golang.org/x/net/context" +) + +// BackOffContext is a backoff policy that stops retrying after the context +// is canceled. +type BackOffContext interface { + BackOff + Context() context.Context +} + +type backOffContext struct { + BackOff + ctx context.Context +} + +// WithContext returns a BackOffContext with context ctx +// +// ctx must not be nil +func WithContext(b BackOff, ctx context.Context) BackOffContext { + if ctx == nil { + panic("nil context") + } + + if b, ok := b.(*backOffContext); ok { + return &backOffContext{ + BackOff: b.BackOff, + ctx: ctx, + } + } + + return &backOffContext{ + BackOff: b, + ctx: ctx, + } +} + +func ensureContext(b BackOff) BackOffContext { + if cb, ok := b.(BackOffContext); ok { + return cb + } + return WithContext(b, context.Background()) +} + +func (b *backOffContext) Context() context.Context { + return b.ctx +} + +func (b *backOffContext) NextBackOff() time.Duration { + select { + case <-b.Context().Done(): + return Stop + default: + return b.BackOff.NextBackOff() + } +} diff --git a/vendor/gopkg.in/cenkalti/backoff.v1/exponential.go b/vendor/gopkg.in/cenkalti/backoff.v1/exponential.go new file mode 100644 index 0000000000..9a6addf075 --- /dev/null +++ b/vendor/gopkg.in/cenkalti/backoff.v1/exponential.go @@ -0,0 +1,156 @@ +package backoff + +import ( + "math/rand" + "time" +) + +/* +ExponentialBackOff is a backoff implementation that increases the backoff +period for each retry attempt using a randomization function that grows exponentially. + +NextBackOff() is calculated using the following formula: + + randomized interval = + RetryInterval * (random value in range [1 - RandomizationFactor, 1 + RandomizationFactor]) + +In other words NextBackOff() will range between the randomization factor +percentage below and above the retry interval. + +For example, given the following parameters: + + RetryInterval = 2 + RandomizationFactor = 0.5 + Multiplier = 2 + +the actual backoff period used in the next retry attempt will range between 1 and 3 seconds, +multiplied by the exponential, that is, between 2 and 6 seconds. + +Note: MaxInterval caps the RetryInterval and not the randomized interval. + +If the time elapsed since an ExponentialBackOff instance is created goes past the +MaxElapsedTime, then the method NextBackOff() starts returning backoff.Stop. + +The elapsed time can be reset by calling Reset(). + +Example: Given the following default arguments, for 10 tries the sequence will be, +and assuming we go over the MaxElapsedTime on the 10th try: + + Request # RetryInterval (seconds) Randomized Interval (seconds) + + 1 0.5 [0.25, 0.75] + 2 0.75 [0.375, 1.125] + 3 1.125 [0.562, 1.687] + 4 1.687 [0.8435, 2.53] + 5 2.53 [1.265, 3.795] + 6 3.795 [1.897, 5.692] + 7 5.692 [2.846, 8.538] + 8 8.538 [4.269, 12.807] + 9 12.807 [6.403, 19.210] + 10 19.210 backoff.Stop + +Note: Implementation is not thread-safe. +*/ +type ExponentialBackOff struct { + InitialInterval time.Duration + RandomizationFactor float64 + Multiplier float64 + MaxInterval time.Duration + // After MaxElapsedTime the ExponentialBackOff stops. + // It never stops if MaxElapsedTime == 0. + MaxElapsedTime time.Duration + Clock Clock + + currentInterval time.Duration + startTime time.Time + random *rand.Rand +} + +// Clock is an interface that returns current time for BackOff. +type Clock interface { + Now() time.Time +} + +// Default values for ExponentialBackOff. +const ( + DefaultInitialInterval = 500 * time.Millisecond + DefaultRandomizationFactor = 0.5 + DefaultMultiplier = 1.5 + DefaultMaxInterval = 60 * time.Second + DefaultMaxElapsedTime = 15 * time.Minute +) + +// NewExponentialBackOff creates an instance of ExponentialBackOff using default values. +func NewExponentialBackOff() *ExponentialBackOff { + b := &ExponentialBackOff{ + InitialInterval: DefaultInitialInterval, + RandomizationFactor: DefaultRandomizationFactor, + Multiplier: DefaultMultiplier, + MaxInterval: DefaultMaxInterval, + MaxElapsedTime: DefaultMaxElapsedTime, + Clock: SystemClock, + random: rand.New(rand.NewSource(time.Now().UnixNano())), + } + b.Reset() + return b +} + +type systemClock struct{} + +func (t systemClock) Now() time.Time { + return time.Now() +} + +// SystemClock implements Clock interface that uses time.Now(). +var SystemClock = systemClock{} + +// Reset the interval back to the initial retry interval and restarts the timer. +func (b *ExponentialBackOff) Reset() { + b.currentInterval = b.InitialInterval + b.startTime = b.Clock.Now() +} + +// NextBackOff calculates the next backoff interval using the formula: +// Randomized interval = RetryInterval +/- (RandomizationFactor * RetryInterval) +func (b *ExponentialBackOff) NextBackOff() time.Duration { + // Make sure we have not gone over the maximum elapsed time. + if b.MaxElapsedTime != 0 && b.GetElapsedTime() > b.MaxElapsedTime { + return Stop + } + defer b.incrementCurrentInterval() + if b.random == nil { + b.random = rand.New(rand.NewSource(time.Now().UnixNano())) + } + return getRandomValueFromInterval(b.RandomizationFactor, b.random.Float64(), b.currentInterval) +} + +// GetElapsedTime returns the elapsed time since an ExponentialBackOff instance +// is created and is reset when Reset() is called. +// +// The elapsed time is computed using time.Now().UnixNano(). +func (b *ExponentialBackOff) GetElapsedTime() time.Duration { + return b.Clock.Now().Sub(b.startTime) +} + +// Increments the current interval by multiplying it with the multiplier. +func (b *ExponentialBackOff) incrementCurrentInterval() { + // Check for overflow, if overflow is detected set the current interval to the max interval. + if float64(b.currentInterval) >= float64(b.MaxInterval)/b.Multiplier { + b.currentInterval = b.MaxInterval + } else { + b.currentInterval = time.Duration(float64(b.currentInterval) * b.Multiplier) + } +} + +// Returns a random value from the following interval: +// [randomizationFactor * currentInterval, randomizationFactor * currentInterval]. +func getRandomValueFromInterval(randomizationFactor, random float64, currentInterval time.Duration) time.Duration { + var delta = randomizationFactor * float64(currentInterval) + var minInterval = float64(currentInterval) - delta + var maxInterval = float64(currentInterval) + delta + + // Get a random value from the range [minInterval, maxInterval]. + // The formula used below has a +1 because if the minInterval is 1 and the maxInterval is 3 then + // we want a 33% chance for selecting either 1, 2 or 3. + return time.Duration(minInterval + (random * (maxInterval - minInterval + 1))) +} diff --git a/vendor/gopkg.in/cenkalti/backoff.v1/retry.go b/vendor/gopkg.in/cenkalti/backoff.v1/retry.go new file mode 100644 index 0000000000..5dbd825b5c --- /dev/null +++ b/vendor/gopkg.in/cenkalti/backoff.v1/retry.go @@ -0,0 +1,78 @@ +package backoff + +import "time" + +// An Operation is executing by Retry() or RetryNotify(). +// The operation will be retried using a backoff policy if it returns an error. +type Operation func() error + +// Notify is a notify-on-error function. It receives an operation error and +// backoff delay if the operation failed (with an error). +// +// NOTE that if the backoff policy stated to stop retrying, +// the notify function isn't called. +type Notify func(error, time.Duration) + +// Retry the operation o until it does not return error or BackOff stops. +// o is guaranteed to be run at least once. +// It is the caller's responsibility to reset b after Retry returns. +// +// If o returns a *PermanentError, the operation is not retried, and the +// wrapped error is returned. +// +// Retry sleeps the goroutine for the duration returned by BackOff after a +// failed operation returns. +func Retry(o Operation, b BackOff) error { return RetryNotify(o, b, nil) } + +// RetryNotify calls notify function with the error and wait duration +// for each failed attempt before sleep. +func RetryNotify(operation Operation, b BackOff, notify Notify) error { + var err error + var next time.Duration + + cb := ensureContext(b) + + b.Reset() + for { + if err = operation(); err == nil { + return nil + } + + if permanent, ok := err.(*PermanentError); ok { + return permanent.Err + } + + if next = b.NextBackOff(); next == Stop { + return err + } + + if notify != nil { + notify(err, next) + } + + t := time.NewTimer(next) + + select { + case <-cb.Context().Done(): + t.Stop() + return err + case <-t.C: + } + } +} + +// PermanentError signals that the operation should not be retried. +type PermanentError struct { + Err error +} + +func (e *PermanentError) Error() string { + return e.Err.Error() +} + +// Permanent wraps the given err in a *PermanentError. +func Permanent(err error) *PermanentError { + return &PermanentError{ + Err: err, + } +} diff --git a/vendor/gopkg.in/cenkalti/backoff.v1/ticker.go b/vendor/gopkg.in/cenkalti/backoff.v1/ticker.go new file mode 100644 index 0000000000..49a99718d7 --- /dev/null +++ b/vendor/gopkg.in/cenkalti/backoff.v1/ticker.go @@ -0,0 +1,81 @@ +package backoff + +import ( + "runtime" + "sync" + "time" +) + +// Ticker holds a channel that delivers `ticks' of a clock at times reported by a BackOff. +// +// Ticks will continue to arrive when the previous operation is still running, +// so operations that take a while to fail could run in quick succession. +type Ticker struct { + C <-chan time.Time + c chan time.Time + b BackOffContext + stop chan struct{} + stopOnce sync.Once +} + +// NewTicker returns a new Ticker containing a channel that will send the time at times +// specified by the BackOff argument. Ticker is guaranteed to tick at least once. +// The channel is closed when Stop method is called or BackOff stops. +func NewTicker(b BackOff) *Ticker { + c := make(chan time.Time) + t := &Ticker{ + C: c, + c: c, + b: ensureContext(b), + stop: make(chan struct{}), + } + go t.run() + runtime.SetFinalizer(t, (*Ticker).Stop) + return t +} + +// Stop turns off a ticker. After Stop, no more ticks will be sent. +func (t *Ticker) Stop() { + t.stopOnce.Do(func() { close(t.stop) }) +} + +func (t *Ticker) run() { + c := t.c + defer close(c) + t.b.Reset() + + // Ticker is guaranteed to tick at least once. + afterC := t.send(time.Now()) + + for { + if afterC == nil { + return + } + + select { + case tick := <-afterC: + afterC = t.send(tick) + case <-t.stop: + t.c = nil // Prevent future ticks from being sent to the channel. + return + case <-t.b.Context().Done(): + return + } + } +} + +func (t *Ticker) send(tick time.Time) <-chan time.Time { + select { + case t.c <- tick: + case <-t.stop: + return nil + } + + next := t.b.NextBackOff() + if next == Stop { + t.Stop() + return nil + } + + return time.After(next) +} diff --git a/vendor/gopkg.in/cenkalti/backoff.v1/tries.go b/vendor/gopkg.in/cenkalti/backoff.v1/tries.go new file mode 100644 index 0000000000..d2da7308b6 --- /dev/null +++ b/vendor/gopkg.in/cenkalti/backoff.v1/tries.go @@ -0,0 +1,35 @@ +package backoff + +import "time" + +/* +WithMaxTries creates a wrapper around another BackOff, which will +return Stop if NextBackOff() has been called too many times since +the last time Reset() was called + +Note: Implementation is not thread-safe. +*/ +func WithMaxTries(b BackOff, max uint64) BackOff { + return &backOffTries{delegate: b, maxTries: max} +} + +type backOffTries struct { + delegate BackOff + maxTries uint64 + numTries uint64 +} + +func (b *backOffTries) NextBackOff() time.Duration { + if b.maxTries > 0 { + if b.maxTries <= b.numTries { + return Stop + } + b.numTries++ + } + return b.delegate.NextBackOff() +} + +func (b *backOffTries) Reset() { + b.numTries = 0 + b.delegate.Reset() +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 867cfba695..abe8d4af3b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -281,8 +281,6 @@ github.com/fsnotify/fsnotify # github.com/ghodss/yaml v1.0.0 ## explicit github.com/ghodss/yaml -# github.com/gin-gonic/gin v1.9.1 -## explicit; go 1.20 # github.com/go-logr/logr v1.2.4 ## explicit; go 1.16 github.com/go-logr/logr @@ -645,6 +643,9 @@ github.com/power-devops/perfstat github.com/proglottis/gpgme # github.com/prometheus/client_golang v1.14.0 ## explicit; go 1.17 +# github.com/r3labs/sse/v2 v2.10.0 +## explicit; go 1.13 +github.com/r3labs/sse/v2 # github.com/rivo/uniseg v0.4.3 ## explicit; go 1.18 github.com/rivo/uniseg @@ -992,6 +993,9 @@ google.golang.org/protobuf/types/known/anypb google.golang.org/protobuf/types/known/durationpb google.golang.org/protobuf/types/known/emptypb google.golang.org/protobuf/types/known/timestamppb +# gopkg.in/cenkalti/backoff.v1 v1.1.0 +## explicit +gopkg.in/cenkalti/backoff.v1 # gopkg.in/inf.v0 v0.9.1 ## explicit gopkg.in/inf.v0 @@ -1294,14 +1298,6 @@ k8s.io/utils/strings/slices # libvirt.org/go/libvirtxml v1.9000.0 ## explicit; go 1.11 libvirt.org/go/libvirtxml -# nhooyr.io/websocket v1.8.7 -## explicit; go 1.13 -nhooyr.io/websocket -nhooyr.io/websocket/internal/bpool -nhooyr.io/websocket/internal/errd -nhooyr.io/websocket/internal/wsjs -nhooyr.io/websocket/internal/xsync -nhooyr.io/websocket/wsjson # sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 ## explicit; go 1.18 sigs.k8s.io/json diff --git a/vendor/nhooyr.io/websocket/.gitignore b/vendor/nhooyr.io/websocket/.gitignore deleted file mode 100644 index 6961e5c894..0000000000 --- a/vendor/nhooyr.io/websocket/.gitignore +++ /dev/null @@ -1 +0,0 @@ -websocket.test diff --git a/vendor/nhooyr.io/websocket/LICENSE.txt b/vendor/nhooyr.io/websocket/LICENSE.txt deleted file mode 100644 index b5b5fef31f..0000000000 --- a/vendor/nhooyr.io/websocket/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2018 Anmol Sethi - -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. diff --git a/vendor/nhooyr.io/websocket/README.md b/vendor/nhooyr.io/websocket/README.md deleted file mode 100644 index df20c581a5..0000000000 --- a/vendor/nhooyr.io/websocket/README.md +++ /dev/null @@ -1,132 +0,0 @@ -# websocket - -[![godoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://pkg.go.dev/nhooyr.io/websocket) -[![coverage](https://img.shields.io/badge/coverage-88%25-success)](https://nhooyrio-websocket-coverage.netlify.app) - -websocket is a minimal and idiomatic WebSocket library for Go. - -## Install - -```bash -go get nhooyr.io/websocket -``` - -## Highlights - -- Minimal and idiomatic API -- First class [context.Context](https://blog.golang.org/context) support -- Fully passes the WebSocket [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite) -- [Single dependency](https://pkg.go.dev/nhooyr.io/websocket?tab=imports) -- JSON and protobuf helpers in the [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) and [wspb](https://pkg.go.dev/nhooyr.io/websocket/wspb) subpackages -- Zero alloc reads and writes -- Concurrent writes -- [Close handshake](https://pkg.go.dev/nhooyr.io/websocket#Conn.Close) -- [net.Conn](https://pkg.go.dev/nhooyr.io/websocket#NetConn) wrapper -- [Ping pong](https://pkg.go.dev/nhooyr.io/websocket#Conn.Ping) API -- [RFC 7692](https://tools.ietf.org/html/rfc7692) permessage-deflate compression -- Compile to [Wasm](https://pkg.go.dev/nhooyr.io/websocket#hdr-Wasm) - -## Roadmap - -- [ ] HTTP/2 [#4](https://github.com/nhooyr/websocket/issues/4) - -## Examples - -For a production quality example that demonstrates the complete API, see the -[echo example](./examples/echo). - -For a full stack example, see the [chat example](./examples/chat). - -### Server - -```go -http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) { - c, err := websocket.Accept(w, r, nil) - if err != nil { - // ... - } - defer c.Close(websocket.StatusInternalError, "the sky is falling") - - ctx, cancel := context.WithTimeout(r.Context(), time.Second*10) - defer cancel() - - var v interface{} - err = wsjson.Read(ctx, c, &v) - if err != nil { - // ... - } - - log.Printf("received: %v", v) - - c.Close(websocket.StatusNormalClosure, "") -}) -``` - -### Client - -```go -ctx, cancel := context.WithTimeout(context.Background(), time.Minute) -defer cancel() - -c, _, err := websocket.Dial(ctx, "ws://localhost:8080", nil) -if err != nil { - // ... -} -defer c.Close(websocket.StatusInternalError, "the sky is falling") - -err = wsjson.Write(ctx, c, "hi") -if err != nil { - // ... -} - -c.Close(websocket.StatusNormalClosure, "") -``` - -## Comparison - -### gorilla/websocket - -Advantages of [gorilla/websocket](https://github.com/gorilla/websocket): - -- Mature and widely used -- [Prepared writes](https://pkg.go.dev/github.com/gorilla/websocket#PreparedMessage) -- Configurable [buffer sizes](https://pkg.go.dev/github.com/gorilla/websocket#hdr-Buffers) - -Advantages of nhooyr.io/websocket: - -- Minimal and idiomatic API - - Compare godoc of [nhooyr.io/websocket](https://pkg.go.dev/nhooyr.io/websocket) with [gorilla/websocket](https://pkg.go.dev/github.com/gorilla/websocket) side by side. -- [net.Conn](https://pkg.go.dev/nhooyr.io/websocket#NetConn) wrapper -- Zero alloc reads and writes ([gorilla/websocket#535](https://github.com/gorilla/websocket/issues/535)) -- Full [context.Context](https://blog.golang.org/context) support -- Dial uses [net/http.Client](https://golang.org/pkg/net/http/#Client) - - Will enable easy HTTP/2 support in the future - - Gorilla writes directly to a net.Conn and so duplicates features of net/http.Client. -- Concurrent writes -- Close handshake ([gorilla/websocket#448](https://github.com/gorilla/websocket/issues/448)) -- Idiomatic [ping pong](https://pkg.go.dev/nhooyr.io/websocket#Conn.Ping) API - - Gorilla requires registering a pong callback before sending a Ping -- Can target Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432)) -- Transparent message buffer reuse with [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) and [wspb](https://pkg.go.dev/nhooyr.io/websocket/wspb) subpackages -- [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster WebSocket masking implementation in pure Go - - Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/). -- Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - - Gorilla only supports no context takeover mode - - We use [klauspost/compress](https://github.com/klauspost/compress) for much lower memory usage ([gorilla/websocket#203](https://github.com/gorilla/websocket/issues/203)) -- [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) -- Actively maintained ([gorilla/websocket#370](https://github.com/gorilla/websocket/issues/370)) - -#### golang.org/x/net/websocket - -[golang.org/x/net/websocket](https://pkg.go.dev/golang.org/x/net/websocket) is deprecated. -See [golang/go/issues/18152](https://github.com/golang/go/issues/18152). - -The [net.Conn](https://pkg.go.dev/nhooyr.io/websocket#NetConn) can help in transitioning -to nhooyr.io/websocket. - -#### gobwas/ws - -[gobwas/ws](https://github.com/gobwas/ws) has an extremely flexible API that allows it to be used -in an event driven style for performance. See the author's [blog post](https://medium.freecodecamp.org/million-websockets-and-go-cc58418460bb). - -However when writing idiomatic Go, nhooyr.io/websocket will be faster and easier to use. diff --git a/vendor/nhooyr.io/websocket/accept.go b/vendor/nhooyr.io/websocket/accept.go deleted file mode 100644 index 18536bdb2c..0000000000 --- a/vendor/nhooyr.io/websocket/accept.go +++ /dev/null @@ -1,370 +0,0 @@ -// +build !js - -package websocket - -import ( - "bytes" - "crypto/sha1" - "encoding/base64" - "errors" - "fmt" - "io" - "log" - "net/http" - "net/textproto" - "net/url" - "path/filepath" - "strings" - - "nhooyr.io/websocket/internal/errd" -) - -// AcceptOptions represents Accept's options. -type AcceptOptions struct { - // Subprotocols lists the WebSocket subprotocols that Accept will negotiate with the client. - // The empty subprotocol will always be negotiated as per RFC 6455. If you would like to - // reject it, close the connection when c.Subprotocol() == "". - Subprotocols []string - - // InsecureSkipVerify is used to disable Accept's origin verification behaviour. - // - // You probably want to use OriginPatterns instead. - InsecureSkipVerify bool - - // OriginPatterns lists the host patterns for authorized origins. - // The request host is always authorized. - // Use this to enable cross origin WebSockets. - // - // i.e javascript running on example.com wants to access a WebSocket server at chat.example.com. - // In such a case, example.com is the origin and chat.example.com is the request host. - // One would set this field to []string{"example.com"} to authorize example.com to connect. - // - // Each pattern is matched case insensitively against the request origin host - // with filepath.Match. - // See https://golang.org/pkg/path/filepath/#Match - // - // Please ensure you understand the ramifications of enabling this. - // If used incorrectly your WebSocket server will be open to CSRF attacks. - // - // Do not use * as a pattern to allow any origin, prefer to use InsecureSkipVerify instead - // to bring attention to the danger of such a setting. - OriginPatterns []string - - // CompressionMode controls the compression mode. - // Defaults to CompressionNoContextTakeover. - // - // See docs on CompressionMode for details. - CompressionMode CompressionMode - - // CompressionThreshold controls the minimum size of a message before compression is applied. - // - // Defaults to 512 bytes for CompressionNoContextTakeover and 128 bytes - // for CompressionContextTakeover. - CompressionThreshold int -} - -// Accept accepts a WebSocket handshake from a client and upgrades the -// the connection to a WebSocket. -// -// Accept will not allow cross origin requests by default. -// See the InsecureSkipVerify and OriginPatterns options to allow cross origin requests. -// -// Accept will write a response to w on all errors. -func Accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, error) { - return accept(w, r, opts) -} - -func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Conn, err error) { - defer errd.Wrap(&err, "failed to accept WebSocket connection") - - if opts == nil { - opts = &AcceptOptions{} - } - opts = &*opts - - errCode, err := verifyClientRequest(w, r) - if err != nil { - http.Error(w, err.Error(), errCode) - return nil, err - } - - if !opts.InsecureSkipVerify { - err = authenticateOrigin(r, opts.OriginPatterns) - if err != nil { - if errors.Is(err, filepath.ErrBadPattern) { - log.Printf("websocket: %v", err) - err = errors.New(http.StatusText(http.StatusForbidden)) - } - http.Error(w, err.Error(), http.StatusForbidden) - return nil, err - } - } - - hj, ok := w.(http.Hijacker) - if !ok { - err = errors.New("http.ResponseWriter does not implement http.Hijacker") - http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented) - return nil, err - } - - w.Header().Set("Upgrade", "websocket") - w.Header().Set("Connection", "Upgrade") - - key := r.Header.Get("Sec-WebSocket-Key") - w.Header().Set("Sec-WebSocket-Accept", secWebSocketAccept(key)) - - subproto := selectSubprotocol(r, opts.Subprotocols) - if subproto != "" { - w.Header().Set("Sec-WebSocket-Protocol", subproto) - } - - copts, err := acceptCompression(r, w, opts.CompressionMode) - if err != nil { - return nil, err - } - - w.WriteHeader(http.StatusSwitchingProtocols) - // See https://github.com/nhooyr/websocket/issues/166 - if ginWriter, ok := w.(interface { - WriteHeaderNow() - }); ok { - ginWriter.WriteHeaderNow() - } - - netConn, brw, err := hj.Hijack() - if err != nil { - err = fmt.Errorf("failed to hijack connection: %w", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return nil, err - } - - // https://github.com/golang/go/issues/32314 - b, _ := brw.Reader.Peek(brw.Reader.Buffered()) - brw.Reader.Reset(io.MultiReader(bytes.NewReader(b), netConn)) - - return newConn(connConfig{ - subprotocol: w.Header().Get("Sec-WebSocket-Protocol"), - rwc: netConn, - client: false, - copts: copts, - flateThreshold: opts.CompressionThreshold, - - br: brw.Reader, - bw: brw.Writer, - }), nil -} - -func verifyClientRequest(w http.ResponseWriter, r *http.Request) (errCode int, _ error) { - if !r.ProtoAtLeast(1, 1) { - return http.StatusUpgradeRequired, fmt.Errorf("WebSocket protocol violation: handshake request must be at least HTTP/1.1: %q", r.Proto) - } - - if !headerContainsTokenIgnoreCase(r.Header, "Connection", "Upgrade") { - w.Header().Set("Connection", "Upgrade") - w.Header().Set("Upgrade", "websocket") - return http.StatusUpgradeRequired, fmt.Errorf("WebSocket protocol violation: Connection header %q does not contain Upgrade", r.Header.Get("Connection")) - } - - if !headerContainsTokenIgnoreCase(r.Header, "Upgrade", "websocket") { - w.Header().Set("Connection", "Upgrade") - w.Header().Set("Upgrade", "websocket") - return http.StatusUpgradeRequired, fmt.Errorf("WebSocket protocol violation: Upgrade header %q does not contain websocket", r.Header.Get("Upgrade")) - } - - if r.Method != "GET" { - return http.StatusMethodNotAllowed, fmt.Errorf("WebSocket protocol violation: handshake request method is not GET but %q", r.Method) - } - - if r.Header.Get("Sec-WebSocket-Version") != "13" { - w.Header().Set("Sec-WebSocket-Version", "13") - return http.StatusBadRequest, fmt.Errorf("unsupported WebSocket protocol version (only 13 is supported): %q", r.Header.Get("Sec-WebSocket-Version")) - } - - if r.Header.Get("Sec-WebSocket-Key") == "" { - return http.StatusBadRequest, errors.New("WebSocket protocol violation: missing Sec-WebSocket-Key") - } - - return 0, nil -} - -func authenticateOrigin(r *http.Request, originHosts []string) error { - origin := r.Header.Get("Origin") - if origin == "" { - return nil - } - - u, err := url.Parse(origin) - if err != nil { - return fmt.Errorf("failed to parse Origin header %q: %w", origin, err) - } - - if strings.EqualFold(r.Host, u.Host) { - return nil - } - - for _, hostPattern := range originHosts { - matched, err := match(hostPattern, u.Host) - if err != nil { - return fmt.Errorf("failed to parse filepath pattern %q: %w", hostPattern, err) - } - if matched { - return nil - } - } - return fmt.Errorf("request Origin %q is not authorized for Host %q", origin, r.Host) -} - -func match(pattern, s string) (bool, error) { - return filepath.Match(strings.ToLower(pattern), strings.ToLower(s)) -} - -func selectSubprotocol(r *http.Request, subprotocols []string) string { - cps := headerTokens(r.Header, "Sec-WebSocket-Protocol") - for _, sp := range subprotocols { - for _, cp := range cps { - if strings.EqualFold(sp, cp) { - return cp - } - } - } - return "" -} - -func acceptCompression(r *http.Request, w http.ResponseWriter, mode CompressionMode) (*compressionOptions, error) { - if mode == CompressionDisabled { - return nil, nil - } - - for _, ext := range websocketExtensions(r.Header) { - switch ext.name { - case "permessage-deflate": - return acceptDeflate(w, ext, mode) - // Disabled for now, see https://github.com/nhooyr/websocket/issues/218 - // case "x-webkit-deflate-frame": - // return acceptWebkitDeflate(w, ext, mode) - } - } - return nil, nil -} - -func acceptDeflate(w http.ResponseWriter, ext websocketExtension, mode CompressionMode) (*compressionOptions, error) { - copts := mode.opts() - - for _, p := range ext.params { - switch p { - case "client_no_context_takeover": - copts.clientNoContextTakeover = true - continue - case "server_no_context_takeover": - copts.serverNoContextTakeover = true - continue - } - - if strings.HasPrefix(p, "client_max_window_bits") { - // We cannot adjust the read sliding window so cannot make use of this. - continue - } - - err := fmt.Errorf("unsupported permessage-deflate parameter: %q", p) - http.Error(w, err.Error(), http.StatusBadRequest) - return nil, err - } - - copts.setHeader(w.Header()) - - return copts, nil -} - -func acceptWebkitDeflate(w http.ResponseWriter, ext websocketExtension, mode CompressionMode) (*compressionOptions, error) { - copts := mode.opts() - // The peer must explicitly request it. - copts.serverNoContextTakeover = false - - for _, p := range ext.params { - if p == "no_context_takeover" { - copts.serverNoContextTakeover = true - continue - } - - // We explicitly fail on x-webkit-deflate-frame's max_window_bits parameter instead - // of ignoring it as the draft spec is unclear. It says the server can ignore it - // but the server has no way of signalling to the client it was ignored as the parameters - // are set one way. - // Thus us ignoring it would make the client think we understood it which would cause issues. - // See https://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate-06#section-4.1 - // - // Either way, we're only implementing this for webkit which never sends the max_window_bits - // parameter so we don't need to worry about it. - err := fmt.Errorf("unsupported x-webkit-deflate-frame parameter: %q", p) - http.Error(w, err.Error(), http.StatusBadRequest) - return nil, err - } - - s := "x-webkit-deflate-frame" - if copts.clientNoContextTakeover { - s += "; no_context_takeover" - } - w.Header().Set("Sec-WebSocket-Extensions", s) - - return copts, nil -} - -func headerContainsTokenIgnoreCase(h http.Header, key, token string) bool { - for _, t := range headerTokens(h, key) { - if strings.EqualFold(t, token) { - return true - } - } - return false -} - -type websocketExtension struct { - name string - params []string -} - -func websocketExtensions(h http.Header) []websocketExtension { - var exts []websocketExtension - extStrs := headerTokens(h, "Sec-WebSocket-Extensions") - for _, extStr := range extStrs { - if extStr == "" { - continue - } - - vals := strings.Split(extStr, ";") - for i := range vals { - vals[i] = strings.TrimSpace(vals[i]) - } - - e := websocketExtension{ - name: vals[0], - params: vals[1:], - } - - exts = append(exts, e) - } - return exts -} - -func headerTokens(h http.Header, key string) []string { - key = textproto.CanonicalMIMEHeaderKey(key) - var tokens []string - for _, v := range h[key] { - v = strings.TrimSpace(v) - for _, t := range strings.Split(v, ",") { - t = strings.TrimSpace(t) - tokens = append(tokens, t) - } - } - return tokens -} - -var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11") - -func secWebSocketAccept(secWebSocketKey string) string { - h := sha1.New() - h.Write([]byte(secWebSocketKey)) - h.Write(keyGUID) - - return base64.StdEncoding.EncodeToString(h.Sum(nil)) -} diff --git a/vendor/nhooyr.io/websocket/accept_js.go b/vendor/nhooyr.io/websocket/accept_js.go deleted file mode 100644 index daad4b79fe..0000000000 --- a/vendor/nhooyr.io/websocket/accept_js.go +++ /dev/null @@ -1,20 +0,0 @@ -package websocket - -import ( - "errors" - "net/http" -) - -// AcceptOptions represents Accept's options. -type AcceptOptions struct { - Subprotocols []string - InsecureSkipVerify bool - OriginPatterns []string - CompressionMode CompressionMode - CompressionThreshold int -} - -// Accept is stubbed out for Wasm. -func Accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, error) { - return nil, errors.New("unimplemented") -} diff --git a/vendor/nhooyr.io/websocket/close.go b/vendor/nhooyr.io/websocket/close.go deleted file mode 100644 index 7cbc19e9de..0000000000 --- a/vendor/nhooyr.io/websocket/close.go +++ /dev/null @@ -1,76 +0,0 @@ -package websocket - -import ( - "errors" - "fmt" -) - -// StatusCode represents a WebSocket status code. -// https://tools.ietf.org/html/rfc6455#section-7.4 -type StatusCode int - -// https://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number -// -// These are only the status codes defined by the protocol. -// -// You can define custom codes in the 3000-4999 range. -// The 3000-3999 range is reserved for use by libraries, frameworks and applications. -// The 4000-4999 range is reserved for private use. -const ( - StatusNormalClosure StatusCode = 1000 - StatusGoingAway StatusCode = 1001 - StatusProtocolError StatusCode = 1002 - StatusUnsupportedData StatusCode = 1003 - - // 1004 is reserved and so unexported. - statusReserved StatusCode = 1004 - - // StatusNoStatusRcvd cannot be sent in a close message. - // It is reserved for when a close message is received without - // a status code. - StatusNoStatusRcvd StatusCode = 1005 - - // StatusAbnormalClosure is exported for use only with Wasm. - // In non Wasm Go, the returned error will indicate whether the - // connection was closed abnormally. - StatusAbnormalClosure StatusCode = 1006 - - StatusInvalidFramePayloadData StatusCode = 1007 - StatusPolicyViolation StatusCode = 1008 - StatusMessageTooBig StatusCode = 1009 - StatusMandatoryExtension StatusCode = 1010 - StatusInternalError StatusCode = 1011 - StatusServiceRestart StatusCode = 1012 - StatusTryAgainLater StatusCode = 1013 - StatusBadGateway StatusCode = 1014 - - // StatusTLSHandshake is only exported for use with Wasm. - // In non Wasm Go, the returned error will indicate whether there was - // a TLS handshake failure. - StatusTLSHandshake StatusCode = 1015 -) - -// CloseError is returned when the connection is closed with a status and reason. -// -// Use Go 1.13's errors.As to check for this error. -// Also see the CloseStatus helper. -type CloseError struct { - Code StatusCode - Reason string -} - -func (ce CloseError) Error() string { - return fmt.Sprintf("status = %v and reason = %q", ce.Code, ce.Reason) -} - -// CloseStatus is a convenience wrapper around Go 1.13's errors.As to grab -// the status code from a CloseError. -// -// -1 will be returned if the passed error is nil or not a CloseError. -func CloseStatus(err error) StatusCode { - var ce CloseError - if errors.As(err, &ce) { - return ce.Code - } - return -1 -} diff --git a/vendor/nhooyr.io/websocket/close_notjs.go b/vendor/nhooyr.io/websocket/close_notjs.go deleted file mode 100644 index 4251311d2e..0000000000 --- a/vendor/nhooyr.io/websocket/close_notjs.go +++ /dev/null @@ -1,211 +0,0 @@ -// +build !js - -package websocket - -import ( - "context" - "encoding/binary" - "errors" - "fmt" - "log" - "time" - - "nhooyr.io/websocket/internal/errd" -) - -// Close performs the WebSocket close handshake with the given status code and reason. -// -// It will write a WebSocket close frame with a timeout of 5s and then wait 5s for -// the peer to send a close frame. -// All data messages received from the peer during the close handshake will be discarded. -// -// The connection can only be closed once. Additional calls to Close -// are no-ops. -// -// The maximum length of reason must be 125 bytes. Avoid -// sending a dynamic reason. -// -// Close will unblock all goroutines interacting with the connection once -// complete. -func (c *Conn) Close(code StatusCode, reason string) error { - return c.closeHandshake(code, reason) -} - -func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { - defer errd.Wrap(&err, "failed to close WebSocket") - - writeErr := c.writeClose(code, reason) - closeHandshakeErr := c.waitCloseHandshake() - - if writeErr != nil { - return writeErr - } - - if CloseStatus(closeHandshakeErr) == -1 { - return closeHandshakeErr - } - - return nil -} - -var errAlreadyWroteClose = errors.New("already wrote close") - -func (c *Conn) writeClose(code StatusCode, reason string) error { - c.closeMu.Lock() - wroteClose := c.wroteClose - c.wroteClose = true - c.closeMu.Unlock() - if wroteClose { - return errAlreadyWroteClose - } - - ce := CloseError{ - Code: code, - Reason: reason, - } - - var p []byte - var marshalErr error - if ce.Code != StatusNoStatusRcvd { - p, marshalErr = ce.bytes() - if marshalErr != nil { - log.Printf("websocket: %v", marshalErr) - } - } - - writeErr := c.writeControl(context.Background(), opClose, p) - if CloseStatus(writeErr) != -1 { - // Not a real error if it's due to a close frame being received. - writeErr = nil - } - - // We do this after in case there was an error writing the close frame. - c.setCloseErr(fmt.Errorf("sent close frame: %w", ce)) - - if marshalErr != nil { - return marshalErr - } - return writeErr -} - -func (c *Conn) waitCloseHandshake() error { - defer c.close(nil) - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - defer cancel() - - err := c.readMu.lock(ctx) - if err != nil { - return err - } - defer c.readMu.unlock() - - if c.readCloseFrameErr != nil { - return c.readCloseFrameErr - } - - for { - h, err := c.readLoop(ctx) - if err != nil { - return err - } - - for i := int64(0); i < h.payloadLength; i++ { - _, err := c.br.ReadByte() - if err != nil { - return err - } - } - } -} - -func parseClosePayload(p []byte) (CloseError, error) { - if len(p) == 0 { - return CloseError{ - Code: StatusNoStatusRcvd, - }, nil - } - - if len(p) < 2 { - return CloseError{}, fmt.Errorf("close payload %q too small, cannot even contain the 2 byte status code", p) - } - - ce := CloseError{ - Code: StatusCode(binary.BigEndian.Uint16(p)), - Reason: string(p[2:]), - } - - if !validWireCloseCode(ce.Code) { - return CloseError{}, fmt.Errorf("invalid status code %v", ce.Code) - } - - return ce, nil -} - -// See http://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number -// and https://tools.ietf.org/html/rfc6455#section-7.4.1 -func validWireCloseCode(code StatusCode) bool { - switch code { - case statusReserved, StatusNoStatusRcvd, StatusAbnormalClosure, StatusTLSHandshake: - return false - } - - if code >= StatusNormalClosure && code <= StatusBadGateway { - return true - } - if code >= 3000 && code <= 4999 { - return true - } - - return false -} - -func (ce CloseError) bytes() ([]byte, error) { - p, err := ce.bytesErr() - if err != nil { - err = fmt.Errorf("failed to marshal close frame: %w", err) - ce = CloseError{ - Code: StatusInternalError, - } - p, _ = ce.bytesErr() - } - return p, err -} - -const maxCloseReason = maxControlPayload - 2 - -func (ce CloseError) bytesErr() ([]byte, error) { - if len(ce.Reason) > maxCloseReason { - return nil, fmt.Errorf("reason string max is %v but got %q with length %v", maxCloseReason, ce.Reason, len(ce.Reason)) - } - - if !validWireCloseCode(ce.Code) { - return nil, fmt.Errorf("status code %v cannot be set", ce.Code) - } - - buf := make([]byte, 2+len(ce.Reason)) - binary.BigEndian.PutUint16(buf, uint16(ce.Code)) - copy(buf[2:], ce.Reason) - return buf, nil -} - -func (c *Conn) setCloseErr(err error) { - c.closeMu.Lock() - c.setCloseErrLocked(err) - c.closeMu.Unlock() -} - -func (c *Conn) setCloseErrLocked(err error) { - if c.closeErr == nil { - c.closeErr = fmt.Errorf("WebSocket closed: %w", err) - } -} - -func (c *Conn) isClosed() bool { - select { - case <-c.closed: - return true - default: - return false - } -} diff --git a/vendor/nhooyr.io/websocket/compress.go b/vendor/nhooyr.io/websocket/compress.go deleted file mode 100644 index 80b46d1c1d..0000000000 --- a/vendor/nhooyr.io/websocket/compress.go +++ /dev/null @@ -1,39 +0,0 @@ -package websocket - -// CompressionMode represents the modes available to the deflate extension. -// See https://tools.ietf.org/html/rfc7692 -// -// A compatibility layer is implemented for the older deflate-frame extension used -// by safari. See https://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate-06 -// It will work the same in every way except that we cannot signal to the peer we -// want to use no context takeover on our side, we can only signal that they should. -// It is however currently disabled due to Safari bugs. See https://github.com/nhooyr/websocket/issues/218 -type CompressionMode int - -const ( - // CompressionNoContextTakeover grabs a new flate.Reader and flate.Writer as needed - // for every message. This applies to both server and client side. - // - // This means less efficient compression as the sliding window from previous messages - // will not be used but the memory overhead will be lower if the connections - // are long lived and seldom used. - // - // The message will only be compressed if greater than 512 bytes. - CompressionNoContextTakeover CompressionMode = iota - - // CompressionContextTakeover uses a flate.Reader and flate.Writer per connection. - // This enables reusing the sliding window from previous messages. - // As most WebSocket protocols are repetitive, this can be very efficient. - // It carries an overhead of 8 kB for every connection compared to CompressionNoContextTakeover. - // - // If the peer negotiates NoContextTakeover on the client or server side, it will be - // used instead as this is required by the RFC. - CompressionContextTakeover - - // CompressionDisabled disables the deflate extension. - // - // Use this if you are using a predominantly binary protocol with very - // little duplication in between messages or CPU and memory are more - // important than bandwidth. - CompressionDisabled -) diff --git a/vendor/nhooyr.io/websocket/compress_notjs.go b/vendor/nhooyr.io/websocket/compress_notjs.go deleted file mode 100644 index 809a272c3d..0000000000 --- a/vendor/nhooyr.io/websocket/compress_notjs.go +++ /dev/null @@ -1,181 +0,0 @@ -// +build !js - -package websocket - -import ( - "io" - "net/http" - "sync" - - "github.com/klauspost/compress/flate" -) - -func (m CompressionMode) opts() *compressionOptions { - return &compressionOptions{ - clientNoContextTakeover: m == CompressionNoContextTakeover, - serverNoContextTakeover: m == CompressionNoContextTakeover, - } -} - -type compressionOptions struct { - clientNoContextTakeover bool - serverNoContextTakeover bool -} - -func (copts *compressionOptions) setHeader(h http.Header) { - s := "permessage-deflate" - if copts.clientNoContextTakeover { - s += "; client_no_context_takeover" - } - if copts.serverNoContextTakeover { - s += "; server_no_context_takeover" - } - h.Set("Sec-WebSocket-Extensions", s) -} - -// These bytes are required to get flate.Reader to return. -// They are removed when sending to avoid the overhead as -// WebSocket framing tell's when the message has ended but then -// we need to add them back otherwise flate.Reader keeps -// trying to return more bytes. -const deflateMessageTail = "\x00\x00\xff\xff" - -type trimLastFourBytesWriter struct { - w io.Writer - tail []byte -} - -func (tw *trimLastFourBytesWriter) reset() { - if tw != nil && tw.tail != nil { - tw.tail = tw.tail[:0] - } -} - -func (tw *trimLastFourBytesWriter) Write(p []byte) (int, error) { - if tw.tail == nil { - tw.tail = make([]byte, 0, 4) - } - - extra := len(tw.tail) + len(p) - 4 - - if extra <= 0 { - tw.tail = append(tw.tail, p...) - return len(p), nil - } - - // Now we need to write as many extra bytes as we can from the previous tail. - if extra > len(tw.tail) { - extra = len(tw.tail) - } - if extra > 0 { - _, err := tw.w.Write(tw.tail[:extra]) - if err != nil { - return 0, err - } - - // Shift remaining bytes in tail over. - n := copy(tw.tail, tw.tail[extra:]) - tw.tail = tw.tail[:n] - } - - // If p is less than or equal to 4 bytes, - // all of it is is part of the tail. - if len(p) <= 4 { - tw.tail = append(tw.tail, p...) - return len(p), nil - } - - // Otherwise, only the last 4 bytes are. - tw.tail = append(tw.tail, p[len(p)-4:]...) - - p = p[:len(p)-4] - n, err := tw.w.Write(p) - return n + 4, err -} - -var flateReaderPool sync.Pool - -func getFlateReader(r io.Reader, dict []byte) io.Reader { - fr, ok := flateReaderPool.Get().(io.Reader) - if !ok { - return flate.NewReaderDict(r, dict) - } - fr.(flate.Resetter).Reset(r, dict) - return fr -} - -func putFlateReader(fr io.Reader) { - flateReaderPool.Put(fr) -} - -type slidingWindow struct { - buf []byte -} - -var swPoolMu sync.RWMutex -var swPool = map[int]*sync.Pool{} - -func slidingWindowPool(n int) *sync.Pool { - swPoolMu.RLock() - p, ok := swPool[n] - swPoolMu.RUnlock() - if ok { - return p - } - - p = &sync.Pool{} - - swPoolMu.Lock() - swPool[n] = p - swPoolMu.Unlock() - - return p -} - -func (sw *slidingWindow) init(n int) { - if sw.buf != nil { - return - } - - if n == 0 { - n = 32768 - } - - p := slidingWindowPool(n) - buf, ok := p.Get().([]byte) - if ok { - sw.buf = buf[:0] - } else { - sw.buf = make([]byte, 0, n) - } -} - -func (sw *slidingWindow) close() { - if sw.buf == nil { - return - } - - swPoolMu.Lock() - swPool[cap(sw.buf)].Put(sw.buf) - swPoolMu.Unlock() - sw.buf = nil -} - -func (sw *slidingWindow) write(p []byte) { - if len(p) >= cap(sw.buf) { - sw.buf = sw.buf[:cap(sw.buf)] - p = p[len(p)-cap(sw.buf):] - copy(sw.buf, p) - return - } - - left := cap(sw.buf) - len(sw.buf) - if left < len(p) { - // We need to shift spaceNeeded bytes from the end to make room for p at the end. - spaceNeeded := len(p) - left - copy(sw.buf, sw.buf[spaceNeeded:]) - sw.buf = sw.buf[:len(sw.buf)-spaceNeeded] - } - - sw.buf = append(sw.buf, p...) -} diff --git a/vendor/nhooyr.io/websocket/conn.go b/vendor/nhooyr.io/websocket/conn.go deleted file mode 100644 index a41808be3f..0000000000 --- a/vendor/nhooyr.io/websocket/conn.go +++ /dev/null @@ -1,13 +0,0 @@ -package websocket - -// MessageType represents the type of a WebSocket message. -// See https://tools.ietf.org/html/rfc6455#section-5.6 -type MessageType int - -// MessageType constants. -const ( - // MessageText is for UTF-8 encoded text messages like JSON. - MessageText MessageType = iota + 1 - // MessageBinary is for binary messages like protobufs. - MessageBinary -) diff --git a/vendor/nhooyr.io/websocket/conn_notjs.go b/vendor/nhooyr.io/websocket/conn_notjs.go deleted file mode 100644 index 0c85ab7711..0000000000 --- a/vendor/nhooyr.io/websocket/conn_notjs.go +++ /dev/null @@ -1,265 +0,0 @@ -// +build !js - -package websocket - -import ( - "bufio" - "context" - "errors" - "fmt" - "io" - "runtime" - "strconv" - "sync" - "sync/atomic" -) - -// Conn represents a WebSocket connection. -// All methods may be called concurrently except for Reader and Read. -// -// You must always read from the connection. Otherwise control -// frames will not be handled. See Reader and CloseRead. -// -// Be sure to call Close on the connection when you -// are finished with it to release associated resources. -// -// On any error from any method, the connection is closed -// with an appropriate reason. -type Conn struct { - subprotocol string - rwc io.ReadWriteCloser - client bool - copts *compressionOptions - flateThreshold int - br *bufio.Reader - bw *bufio.Writer - - readTimeout chan context.Context - writeTimeout chan context.Context - - // Read state. - readMu *mu - readHeaderBuf [8]byte - readControlBuf [maxControlPayload]byte - msgReader *msgReader - readCloseFrameErr error - - // Write state. - msgWriterState *msgWriterState - writeFrameMu *mu - writeBuf []byte - writeHeaderBuf [8]byte - writeHeader header - - closed chan struct{} - closeMu sync.Mutex - closeErr error - wroteClose bool - - pingCounter int32 - activePingsMu sync.Mutex - activePings map[string]chan<- struct{} -} - -type connConfig struct { - subprotocol string - rwc io.ReadWriteCloser - client bool - copts *compressionOptions - flateThreshold int - - br *bufio.Reader - bw *bufio.Writer -} - -func newConn(cfg connConfig) *Conn { - c := &Conn{ - subprotocol: cfg.subprotocol, - rwc: cfg.rwc, - client: cfg.client, - copts: cfg.copts, - flateThreshold: cfg.flateThreshold, - - br: cfg.br, - bw: cfg.bw, - - readTimeout: make(chan context.Context), - writeTimeout: make(chan context.Context), - - closed: make(chan struct{}), - activePings: make(map[string]chan<- struct{}), - } - - c.readMu = newMu(c) - c.writeFrameMu = newMu(c) - - c.msgReader = newMsgReader(c) - - c.msgWriterState = newMsgWriterState(c) - if c.client { - c.writeBuf = extractBufioWriterBuf(c.bw, c.rwc) - } - - if c.flate() && c.flateThreshold == 0 { - c.flateThreshold = 128 - if !c.msgWriterState.flateContextTakeover() { - c.flateThreshold = 512 - } - } - - runtime.SetFinalizer(c, func(c *Conn) { - c.close(errors.New("connection garbage collected")) - }) - - go c.timeoutLoop() - - return c -} - -// Subprotocol returns the negotiated subprotocol. -// An empty string means the default protocol. -func (c *Conn) Subprotocol() string { - return c.subprotocol -} - -func (c *Conn) close(err error) { - c.closeMu.Lock() - defer c.closeMu.Unlock() - - if c.isClosed() { - return - } - c.setCloseErrLocked(err) - close(c.closed) - runtime.SetFinalizer(c, nil) - - // Have to close after c.closed is closed to ensure any goroutine that wakes up - // from the connection being closed also sees that c.closed is closed and returns - // closeErr. - c.rwc.Close() - - go func() { - c.msgWriterState.close() - - c.msgReader.close() - }() -} - -func (c *Conn) timeoutLoop() { - readCtx := context.Background() - writeCtx := context.Background() - - for { - select { - case <-c.closed: - return - - case writeCtx = <-c.writeTimeout: - case readCtx = <-c.readTimeout: - - case <-readCtx.Done(): - c.setCloseErr(fmt.Errorf("read timed out: %w", readCtx.Err())) - go c.writeError(StatusPolicyViolation, errors.New("timed out")) - case <-writeCtx.Done(): - c.close(fmt.Errorf("write timed out: %w", writeCtx.Err())) - return - } - } -} - -func (c *Conn) flate() bool { - return c.copts != nil -} - -// Ping sends a ping to the peer and waits for a pong. -// Use this to measure latency or ensure the peer is responsive. -// Ping must be called concurrently with Reader as it does -// not read from the connection but instead waits for a Reader call -// to read the pong. -// -// TCP Keepalives should suffice for most use cases. -func (c *Conn) Ping(ctx context.Context) error { - p := atomic.AddInt32(&c.pingCounter, 1) - - err := c.ping(ctx, strconv.Itoa(int(p))) - if err != nil { - return fmt.Errorf("failed to ping: %w", err) - } - return nil -} - -func (c *Conn) ping(ctx context.Context, p string) error { - pong := make(chan struct{}, 1) - - c.activePingsMu.Lock() - c.activePings[p] = pong - c.activePingsMu.Unlock() - - defer func() { - c.activePingsMu.Lock() - delete(c.activePings, p) - c.activePingsMu.Unlock() - }() - - err := c.writeControl(ctx, opPing, []byte(p)) - if err != nil { - return err - } - - select { - case <-c.closed: - return c.closeErr - case <-ctx.Done(): - err := fmt.Errorf("failed to wait for pong: %w", ctx.Err()) - c.close(err) - return err - case <-pong: - return nil - } -} - -type mu struct { - c *Conn - ch chan struct{} -} - -func newMu(c *Conn) *mu { - return &mu{ - c: c, - ch: make(chan struct{}, 1), - } -} - -func (m *mu) forceLock() { - m.ch <- struct{}{} -} - -func (m *mu) lock(ctx context.Context) error { - select { - case <-m.c.closed: - return m.c.closeErr - case <-ctx.Done(): - err := fmt.Errorf("failed to acquire lock: %w", ctx.Err()) - m.c.close(err) - return err - case m.ch <- struct{}{}: - // To make sure the connection is certainly alive. - // As it's possible the send on m.ch was selected - // over the receive on closed. - select { - case <-m.c.closed: - // Make sure to release. - m.unlock() - return m.c.closeErr - default: - } - return nil - } -} - -func (m *mu) unlock() { - select { - case <-m.ch: - default: - } -} diff --git a/vendor/nhooyr.io/websocket/dial.go b/vendor/nhooyr.io/websocket/dial.go deleted file mode 100644 index 7a7787ff71..0000000000 --- a/vendor/nhooyr.io/websocket/dial.go +++ /dev/null @@ -1,292 +0,0 @@ -// +build !js - -package websocket - -import ( - "bufio" - "bytes" - "context" - "crypto/rand" - "encoding/base64" - "fmt" - "io" - "io/ioutil" - "net/http" - "net/url" - "strings" - "sync" - "time" - - "nhooyr.io/websocket/internal/errd" -) - -// DialOptions represents Dial's options. -type DialOptions struct { - // HTTPClient is used for the connection. - // Its Transport must return writable bodies for WebSocket handshakes. - // http.Transport does beginning with Go 1.12. - HTTPClient *http.Client - - // HTTPHeader specifies the HTTP headers included in the handshake request. - HTTPHeader http.Header - - // Subprotocols lists the WebSocket subprotocols to negotiate with the server. - Subprotocols []string - - // CompressionMode controls the compression mode. - // Defaults to CompressionNoContextTakeover. - // - // See docs on CompressionMode for details. - CompressionMode CompressionMode - - // CompressionThreshold controls the minimum size of a message before compression is applied. - // - // Defaults to 512 bytes for CompressionNoContextTakeover and 128 bytes - // for CompressionContextTakeover. - CompressionThreshold int -} - -// Dial performs a WebSocket handshake on url. -// -// The response is the WebSocket handshake response from the server. -// You never need to close resp.Body yourself. -// -// If an error occurs, the returned response may be non nil. -// However, you can only read the first 1024 bytes of the body. -// -// This function requires at least Go 1.12 as it uses a new feature -// in net/http to perform WebSocket handshakes. -// See docs on the HTTPClient option and https://github.com/golang/go/issues/26937#issuecomment-415855861 -// -// URLs with http/https schemes will work and are interpreted as ws/wss. -func Dial(ctx context.Context, u string, opts *DialOptions) (*Conn, *http.Response, error) { - return dial(ctx, u, opts, nil) -} - -func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) (_ *Conn, _ *http.Response, err error) { - defer errd.Wrap(&err, "failed to WebSocket dial") - - if opts == nil { - opts = &DialOptions{} - } - - opts = &*opts - if opts.HTTPClient == nil { - opts.HTTPClient = http.DefaultClient - } else if opts.HTTPClient.Timeout > 0 { - var cancel context.CancelFunc - - ctx, cancel = context.WithTimeout(ctx, opts.HTTPClient.Timeout) - defer cancel() - - newClient := *opts.HTTPClient - newClient.Timeout = 0 - opts.HTTPClient = &newClient - } - - if opts.HTTPHeader == nil { - opts.HTTPHeader = http.Header{} - } - - secWebSocketKey, err := secWebSocketKey(rand) - if err != nil { - return nil, nil, fmt.Errorf("failed to generate Sec-WebSocket-Key: %w", err) - } - - var copts *compressionOptions - if opts.CompressionMode != CompressionDisabled { - copts = opts.CompressionMode.opts() - } - - resp, err := handshakeRequest(ctx, urls, opts, copts, secWebSocketKey) - if err != nil { - return nil, resp, err - } - respBody := resp.Body - resp.Body = nil - defer func() { - if err != nil { - // We read a bit of the body for easier debugging. - r := io.LimitReader(respBody, 1024) - - timer := time.AfterFunc(time.Second*3, func() { - respBody.Close() - }) - defer timer.Stop() - - b, _ := ioutil.ReadAll(r) - respBody.Close() - resp.Body = ioutil.NopCloser(bytes.NewReader(b)) - } - }() - - copts, err = verifyServerResponse(opts, copts, secWebSocketKey, resp) - if err != nil { - return nil, resp, err - } - - rwc, ok := respBody.(io.ReadWriteCloser) - if !ok { - return nil, resp, fmt.Errorf("response body is not a io.ReadWriteCloser: %T", respBody) - } - - return newConn(connConfig{ - subprotocol: resp.Header.Get("Sec-WebSocket-Protocol"), - rwc: rwc, - client: true, - copts: copts, - flateThreshold: opts.CompressionThreshold, - br: getBufioReader(rwc), - bw: getBufioWriter(rwc), - }), resp, nil -} - -func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, copts *compressionOptions, secWebSocketKey string) (*http.Response, error) { - u, err := url.Parse(urls) - if err != nil { - return nil, fmt.Errorf("failed to parse url: %w", err) - } - - switch u.Scheme { - case "ws": - u.Scheme = "http" - case "wss": - u.Scheme = "https" - case "http", "https": - default: - return nil, fmt.Errorf("unexpected url scheme: %q", u.Scheme) - } - - req, _ := http.NewRequestWithContext(ctx, "GET", u.String(), nil) - req.Header = opts.HTTPHeader.Clone() - req.Header.Set("Connection", "Upgrade") - req.Header.Set("Upgrade", "websocket") - req.Header.Set("Sec-WebSocket-Version", "13") - req.Header.Set("Sec-WebSocket-Key", secWebSocketKey) - if len(opts.Subprotocols) > 0 { - req.Header.Set("Sec-WebSocket-Protocol", strings.Join(opts.Subprotocols, ",")) - } - if copts != nil { - copts.setHeader(req.Header) - } - - resp, err := opts.HTTPClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to send handshake request: %w", err) - } - return resp, nil -} - -func secWebSocketKey(rr io.Reader) (string, error) { - if rr == nil { - rr = rand.Reader - } - b := make([]byte, 16) - _, err := io.ReadFull(rr, b) - if err != nil { - return "", fmt.Errorf("failed to read random data from rand.Reader: %w", err) - } - return base64.StdEncoding.EncodeToString(b), nil -} - -func verifyServerResponse(opts *DialOptions, copts *compressionOptions, secWebSocketKey string, resp *http.Response) (*compressionOptions, error) { - if resp.StatusCode != http.StatusSwitchingProtocols { - return nil, fmt.Errorf("expected handshake response status code %v but got %v", http.StatusSwitchingProtocols, resp.StatusCode) - } - - if !headerContainsTokenIgnoreCase(resp.Header, "Connection", "Upgrade") { - return nil, fmt.Errorf("WebSocket protocol violation: Connection header %q does not contain Upgrade", resp.Header.Get("Connection")) - } - - if !headerContainsTokenIgnoreCase(resp.Header, "Upgrade", "WebSocket") { - return nil, fmt.Errorf("WebSocket protocol violation: Upgrade header %q does not contain websocket", resp.Header.Get("Upgrade")) - } - - if resp.Header.Get("Sec-WebSocket-Accept") != secWebSocketAccept(secWebSocketKey) { - return nil, fmt.Errorf("WebSocket protocol violation: invalid Sec-WebSocket-Accept %q, key %q", - resp.Header.Get("Sec-WebSocket-Accept"), - secWebSocketKey, - ) - } - - err := verifySubprotocol(opts.Subprotocols, resp) - if err != nil { - return nil, err - } - - return verifyServerExtensions(copts, resp.Header) -} - -func verifySubprotocol(subprotos []string, resp *http.Response) error { - proto := resp.Header.Get("Sec-WebSocket-Protocol") - if proto == "" { - return nil - } - - for _, sp2 := range subprotos { - if strings.EqualFold(sp2, proto) { - return nil - } - } - - return fmt.Errorf("WebSocket protocol violation: unexpected Sec-WebSocket-Protocol from server: %q", proto) -} - -func verifyServerExtensions(copts *compressionOptions, h http.Header) (*compressionOptions, error) { - exts := websocketExtensions(h) - if len(exts) == 0 { - return nil, nil - } - - ext := exts[0] - if ext.name != "permessage-deflate" || len(exts) > 1 || copts == nil { - return nil, fmt.Errorf("WebSocket protcol violation: unsupported extensions from server: %+v", exts[1:]) - } - - copts = &*copts - - for _, p := range ext.params { - switch p { - case "client_no_context_takeover": - copts.clientNoContextTakeover = true - continue - case "server_no_context_takeover": - copts.serverNoContextTakeover = true - continue - } - - return nil, fmt.Errorf("unsupported permessage-deflate parameter: %q", p) - } - - return copts, nil -} - -var bufioReaderPool sync.Pool - -func getBufioReader(r io.Reader) *bufio.Reader { - br, ok := bufioReaderPool.Get().(*bufio.Reader) - if !ok { - return bufio.NewReader(r) - } - br.Reset(r) - return br -} - -func putBufioReader(br *bufio.Reader) { - bufioReaderPool.Put(br) -} - -var bufioWriterPool sync.Pool - -func getBufioWriter(w io.Writer) *bufio.Writer { - bw, ok := bufioWriterPool.Get().(*bufio.Writer) - if !ok { - return bufio.NewWriter(w) - } - bw.Reset(w) - return bw -} - -func putBufioWriter(bw *bufio.Writer) { - bufioWriterPool.Put(bw) -} diff --git a/vendor/nhooyr.io/websocket/doc.go b/vendor/nhooyr.io/websocket/doc.go deleted file mode 100644 index efa920e3b6..0000000000 --- a/vendor/nhooyr.io/websocket/doc.go +++ /dev/null @@ -1,32 +0,0 @@ -// +build !js - -// Package websocket implements the RFC 6455 WebSocket protocol. -// -// https://tools.ietf.org/html/rfc6455 -// -// Use Dial to dial a WebSocket server. -// -// Use Accept to accept a WebSocket client. -// -// Conn represents the resulting WebSocket connection. -// -// The examples are the best way to understand how to correctly use the library. -// -// The wsjson and wspb subpackages contain helpers for JSON and protobuf messages. -// -// More documentation at https://nhooyr.io/websocket. -// -// Wasm -// -// The client side supports compiling to Wasm. -// It wraps the WebSocket browser API. -// -// See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket -// -// Some important caveats to be aware of: -// -// - Accept always errors out -// - Conn.Ping is no-op -// - HTTPClient, HTTPHeader and CompressionMode in DialOptions are no-op -// - *http.Response from Dial is &http.Response{} with a 101 status code on success -package websocket // import "nhooyr.io/websocket" diff --git a/vendor/nhooyr.io/websocket/frame.go b/vendor/nhooyr.io/websocket/frame.go deleted file mode 100644 index 2a036f944a..0000000000 --- a/vendor/nhooyr.io/websocket/frame.go +++ /dev/null @@ -1,294 +0,0 @@ -package websocket - -import ( - "bufio" - "encoding/binary" - "fmt" - "io" - "math" - "math/bits" - - "nhooyr.io/websocket/internal/errd" -) - -// opcode represents a WebSocket opcode. -type opcode int - -// https://tools.ietf.org/html/rfc6455#section-11.8. -const ( - opContinuation opcode = iota - opText - opBinary - // 3 - 7 are reserved for further non-control frames. - _ - _ - _ - _ - _ - opClose - opPing - opPong - // 11-16 are reserved for further control frames. -) - -// header represents a WebSocket frame header. -// See https://tools.ietf.org/html/rfc6455#section-5.2. -type header struct { - fin bool - rsv1 bool - rsv2 bool - rsv3 bool - opcode opcode - - payloadLength int64 - - masked bool - maskKey uint32 -} - -// readFrameHeader reads a header from the reader. -// See https://tools.ietf.org/html/rfc6455#section-5.2. -func readFrameHeader(r *bufio.Reader, readBuf []byte) (h header, err error) { - defer errd.Wrap(&err, "failed to read frame header") - - b, err := r.ReadByte() - if err != nil { - return header{}, err - } - - h.fin = b&(1<<7) != 0 - h.rsv1 = b&(1<<6) != 0 - h.rsv2 = b&(1<<5) != 0 - h.rsv3 = b&(1<<4) != 0 - - h.opcode = opcode(b & 0xf) - - b, err = r.ReadByte() - if err != nil { - return header{}, err - } - - h.masked = b&(1<<7) != 0 - - payloadLength := b &^ (1 << 7) - switch { - case payloadLength < 126: - h.payloadLength = int64(payloadLength) - case payloadLength == 126: - _, err = io.ReadFull(r, readBuf[:2]) - h.payloadLength = int64(binary.BigEndian.Uint16(readBuf)) - case payloadLength == 127: - _, err = io.ReadFull(r, readBuf) - h.payloadLength = int64(binary.BigEndian.Uint64(readBuf)) - } - if err != nil { - return header{}, err - } - - if h.payloadLength < 0 { - return header{}, fmt.Errorf("received negative payload length: %v", h.payloadLength) - } - - if h.masked { - _, err = io.ReadFull(r, readBuf[:4]) - if err != nil { - return header{}, err - } - h.maskKey = binary.LittleEndian.Uint32(readBuf) - } - - return h, nil -} - -// maxControlPayload is the maximum length of a control frame payload. -// See https://tools.ietf.org/html/rfc6455#section-5.5. -const maxControlPayload = 125 - -// writeFrameHeader writes the bytes of the header to w. -// See https://tools.ietf.org/html/rfc6455#section-5.2 -func writeFrameHeader(h header, w *bufio.Writer, buf []byte) (err error) { - defer errd.Wrap(&err, "failed to write frame header") - - var b byte - if h.fin { - b |= 1 << 7 - } - if h.rsv1 { - b |= 1 << 6 - } - if h.rsv2 { - b |= 1 << 5 - } - if h.rsv3 { - b |= 1 << 4 - } - - b |= byte(h.opcode) - - err = w.WriteByte(b) - if err != nil { - return err - } - - lengthByte := byte(0) - if h.masked { - lengthByte |= 1 << 7 - } - - switch { - case h.payloadLength > math.MaxUint16: - lengthByte |= 127 - case h.payloadLength > 125: - lengthByte |= 126 - case h.payloadLength >= 0: - lengthByte |= byte(h.payloadLength) - } - err = w.WriteByte(lengthByte) - if err != nil { - return err - } - - switch { - case h.payloadLength > math.MaxUint16: - binary.BigEndian.PutUint64(buf, uint64(h.payloadLength)) - _, err = w.Write(buf) - case h.payloadLength > 125: - binary.BigEndian.PutUint16(buf, uint16(h.payloadLength)) - _, err = w.Write(buf[:2]) - } - if err != nil { - return err - } - - if h.masked { - binary.LittleEndian.PutUint32(buf, h.maskKey) - _, err = w.Write(buf[:4]) - if err != nil { - return err - } - } - - return nil -} - -// mask applies the WebSocket masking algorithm to p -// with the given key. -// See https://tools.ietf.org/html/rfc6455#section-5.3 -// -// The returned value is the correctly rotated key to -// to continue to mask/unmask the message. -// -// It is optimized for LittleEndian and expects the key -// to be in little endian. -// -// See https://github.com/golang/go/issues/31586 -func mask(key uint32, b []byte) uint32 { - if len(b) >= 8 { - key64 := uint64(key)<<32 | uint64(key) - - // At some point in the future we can clean these unrolled loops up. - // See https://github.com/golang/go/issues/31586#issuecomment-487436401 - - // Then we xor until b is less than 128 bytes. - for len(b) >= 128 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^key64) - v = binary.LittleEndian.Uint64(b[8:16]) - binary.LittleEndian.PutUint64(b[8:16], v^key64) - v = binary.LittleEndian.Uint64(b[16:24]) - binary.LittleEndian.PutUint64(b[16:24], v^key64) - v = binary.LittleEndian.Uint64(b[24:32]) - binary.LittleEndian.PutUint64(b[24:32], v^key64) - v = binary.LittleEndian.Uint64(b[32:40]) - binary.LittleEndian.PutUint64(b[32:40], v^key64) - v = binary.LittleEndian.Uint64(b[40:48]) - binary.LittleEndian.PutUint64(b[40:48], v^key64) - v = binary.LittleEndian.Uint64(b[48:56]) - binary.LittleEndian.PutUint64(b[48:56], v^key64) - v = binary.LittleEndian.Uint64(b[56:64]) - binary.LittleEndian.PutUint64(b[56:64], v^key64) - v = binary.LittleEndian.Uint64(b[64:72]) - binary.LittleEndian.PutUint64(b[64:72], v^key64) - v = binary.LittleEndian.Uint64(b[72:80]) - binary.LittleEndian.PutUint64(b[72:80], v^key64) - v = binary.LittleEndian.Uint64(b[80:88]) - binary.LittleEndian.PutUint64(b[80:88], v^key64) - v = binary.LittleEndian.Uint64(b[88:96]) - binary.LittleEndian.PutUint64(b[88:96], v^key64) - v = binary.LittleEndian.Uint64(b[96:104]) - binary.LittleEndian.PutUint64(b[96:104], v^key64) - v = binary.LittleEndian.Uint64(b[104:112]) - binary.LittleEndian.PutUint64(b[104:112], v^key64) - v = binary.LittleEndian.Uint64(b[112:120]) - binary.LittleEndian.PutUint64(b[112:120], v^key64) - v = binary.LittleEndian.Uint64(b[120:128]) - binary.LittleEndian.PutUint64(b[120:128], v^key64) - b = b[128:] - } - - // Then we xor until b is less than 64 bytes. - for len(b) >= 64 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^key64) - v = binary.LittleEndian.Uint64(b[8:16]) - binary.LittleEndian.PutUint64(b[8:16], v^key64) - v = binary.LittleEndian.Uint64(b[16:24]) - binary.LittleEndian.PutUint64(b[16:24], v^key64) - v = binary.LittleEndian.Uint64(b[24:32]) - binary.LittleEndian.PutUint64(b[24:32], v^key64) - v = binary.LittleEndian.Uint64(b[32:40]) - binary.LittleEndian.PutUint64(b[32:40], v^key64) - v = binary.LittleEndian.Uint64(b[40:48]) - binary.LittleEndian.PutUint64(b[40:48], v^key64) - v = binary.LittleEndian.Uint64(b[48:56]) - binary.LittleEndian.PutUint64(b[48:56], v^key64) - v = binary.LittleEndian.Uint64(b[56:64]) - binary.LittleEndian.PutUint64(b[56:64], v^key64) - b = b[64:] - } - - // Then we xor until b is less than 32 bytes. - for len(b) >= 32 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^key64) - v = binary.LittleEndian.Uint64(b[8:16]) - binary.LittleEndian.PutUint64(b[8:16], v^key64) - v = binary.LittleEndian.Uint64(b[16:24]) - binary.LittleEndian.PutUint64(b[16:24], v^key64) - v = binary.LittleEndian.Uint64(b[24:32]) - binary.LittleEndian.PutUint64(b[24:32], v^key64) - b = b[32:] - } - - // Then we xor until b is less than 16 bytes. - for len(b) >= 16 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^key64) - v = binary.LittleEndian.Uint64(b[8:16]) - binary.LittleEndian.PutUint64(b[8:16], v^key64) - b = b[16:] - } - - // Then we xor until b is less than 8 bytes. - for len(b) >= 8 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^key64) - b = b[8:] - } - } - - // Then we xor until b is less than 4 bytes. - for len(b) >= 4 { - v := binary.LittleEndian.Uint32(b) - binary.LittleEndian.PutUint32(b, v^key) - b = b[4:] - } - - // xor remaining bytes. - for i := range b { - b[i] ^= byte(key) - key = bits.RotateLeft32(key, -8) - } - - return key -} diff --git a/vendor/nhooyr.io/websocket/internal/bpool/bpool.go b/vendor/nhooyr.io/websocket/internal/bpool/bpool.go deleted file mode 100644 index aa826fba2b..0000000000 --- a/vendor/nhooyr.io/websocket/internal/bpool/bpool.go +++ /dev/null @@ -1,24 +0,0 @@ -package bpool - -import ( - "bytes" - "sync" -) - -var bpool sync.Pool - -// Get returns a buffer from the pool or creates a new one if -// the pool is empty. -func Get() *bytes.Buffer { - b := bpool.Get() - if b == nil { - return &bytes.Buffer{} - } - return b.(*bytes.Buffer) -} - -// Put returns a buffer into the pool. -func Put(b *bytes.Buffer) { - b.Reset() - bpool.Put(b) -} diff --git a/vendor/nhooyr.io/websocket/internal/errd/wrap.go b/vendor/nhooyr.io/websocket/internal/errd/wrap.go deleted file mode 100644 index 6e779131af..0000000000 --- a/vendor/nhooyr.io/websocket/internal/errd/wrap.go +++ /dev/null @@ -1,14 +0,0 @@ -package errd - -import ( - "fmt" -) - -// Wrap wraps err with fmt.Errorf if err is non nil. -// Intended for use with defer and a named error return. -// Inspired by https://github.com/golang/go/issues/32676. -func Wrap(err *error, f string, v ...interface{}) { - if *err != nil { - *err = fmt.Errorf(f+": %w", append(v, *err)...) - } -} diff --git a/vendor/nhooyr.io/websocket/internal/wsjs/wsjs_js.go b/vendor/nhooyr.io/websocket/internal/wsjs/wsjs_js.go deleted file mode 100644 index 26ffb45625..0000000000 --- a/vendor/nhooyr.io/websocket/internal/wsjs/wsjs_js.go +++ /dev/null @@ -1,170 +0,0 @@ -// +build js - -// Package wsjs implements typed access to the browser javascript WebSocket API. -// -// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket -package wsjs - -import ( - "syscall/js" -) - -func handleJSError(err *error, onErr func()) { - r := recover() - - if jsErr, ok := r.(js.Error); ok { - *err = jsErr - - if onErr != nil { - onErr() - } - return - } - - if r != nil { - panic(r) - } -} - -// New is a wrapper around the javascript WebSocket constructor. -func New(url string, protocols []string) (c WebSocket, err error) { - defer handleJSError(&err, func() { - c = WebSocket{} - }) - - jsProtocols := make([]interface{}, len(protocols)) - for i, p := range protocols { - jsProtocols[i] = p - } - - c = WebSocket{ - v: js.Global().Get("WebSocket").New(url, jsProtocols), - } - - c.setBinaryType("arraybuffer") - - return c, nil -} - -// WebSocket is a wrapper around a javascript WebSocket object. -type WebSocket struct { - v js.Value -} - -func (c WebSocket) setBinaryType(typ string) { - c.v.Set("binaryType", string(typ)) -} - -func (c WebSocket) addEventListener(eventType string, fn func(e js.Value)) func() { - f := js.FuncOf(func(this js.Value, args []js.Value) interface{} { - fn(args[0]) - return nil - }) - c.v.Call("addEventListener", eventType, f) - - return func() { - c.v.Call("removeEventListener", eventType, f) - f.Release() - } -} - -// CloseEvent is the type passed to a WebSocket close handler. -type CloseEvent struct { - Code uint16 - Reason string - WasClean bool -} - -// OnClose registers a function to be called when the WebSocket is closed. -func (c WebSocket) OnClose(fn func(CloseEvent)) (remove func()) { - return c.addEventListener("close", func(e js.Value) { - ce := CloseEvent{ - Code: uint16(e.Get("code").Int()), - Reason: e.Get("reason").String(), - WasClean: e.Get("wasClean").Bool(), - } - fn(ce) - }) -} - -// OnError registers a function to be called when there is an error -// with the WebSocket. -func (c WebSocket) OnError(fn func(e js.Value)) (remove func()) { - return c.addEventListener("error", fn) -} - -// MessageEvent is the type passed to a message handler. -type MessageEvent struct { - // string or []byte. - Data interface{} - - // There are more fields to the interface but we don't use them. - // See https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent -} - -// OnMessage registers a function to be called when the WebSocket receives a message. -func (c WebSocket) OnMessage(fn func(m MessageEvent)) (remove func()) { - return c.addEventListener("message", func(e js.Value) { - var data interface{} - - arrayBuffer := e.Get("data") - if arrayBuffer.Type() == js.TypeString { - data = arrayBuffer.String() - } else { - data = extractArrayBuffer(arrayBuffer) - } - - me := MessageEvent{ - Data: data, - } - fn(me) - - return - }) -} - -// Subprotocol returns the WebSocket subprotocol in use. -func (c WebSocket) Subprotocol() string { - return c.v.Get("protocol").String() -} - -// OnOpen registers a function to be called when the WebSocket is opened. -func (c WebSocket) OnOpen(fn func(e js.Value)) (remove func()) { - return c.addEventListener("open", fn) -} - -// Close closes the WebSocket with the given code and reason. -func (c WebSocket) Close(code int, reason string) (err error) { - defer handleJSError(&err, nil) - c.v.Call("close", code, reason) - return err -} - -// SendText sends the given string as a text message -// on the WebSocket. -func (c WebSocket) SendText(v string) (err error) { - defer handleJSError(&err, nil) - c.v.Call("send", v) - return err -} - -// SendBytes sends the given message as a binary message -// on the WebSocket. -func (c WebSocket) SendBytes(v []byte) (err error) { - defer handleJSError(&err, nil) - c.v.Call("send", uint8Array(v)) - return err -} - -func extractArrayBuffer(arrayBuffer js.Value) []byte { - uint8Array := js.Global().Get("Uint8Array").New(arrayBuffer) - dst := make([]byte, uint8Array.Length()) - js.CopyBytesToGo(dst, uint8Array) - return dst -} - -func uint8Array(src []byte) js.Value { - uint8Array := js.Global().Get("Uint8Array").New(len(src)) - js.CopyBytesToJS(uint8Array, src) - return uint8Array -} diff --git a/vendor/nhooyr.io/websocket/internal/xsync/go.go b/vendor/nhooyr.io/websocket/internal/xsync/go.go deleted file mode 100644 index 7a61f27fa2..0000000000 --- a/vendor/nhooyr.io/websocket/internal/xsync/go.go +++ /dev/null @@ -1,25 +0,0 @@ -package xsync - -import ( - "fmt" -) - -// Go allows running a function in another goroutine -// and waiting for its error. -func Go(fn func() error) <-chan error { - errs := make(chan error, 1) - go func() { - defer func() { - r := recover() - if r != nil { - select { - case errs <- fmt.Errorf("panic in go fn: %v", r): - default: - } - } - }() - errs <- fn() - }() - - return errs -} diff --git a/vendor/nhooyr.io/websocket/internal/xsync/int64.go b/vendor/nhooyr.io/websocket/internal/xsync/int64.go deleted file mode 100644 index a0c4020415..0000000000 --- a/vendor/nhooyr.io/websocket/internal/xsync/int64.go +++ /dev/null @@ -1,23 +0,0 @@ -package xsync - -import ( - "sync/atomic" -) - -// Int64 represents an atomic int64. -type Int64 struct { - // We do not use atomic.Load/StoreInt64 since it does not - // work on 32 bit computers but we need 64 bit integers. - i atomic.Value -} - -// Load loads the int64. -func (v *Int64) Load() int64 { - i, _ := v.i.Load().(int64) - return i -} - -// Store stores the int64. -func (v *Int64) Store(i int64) { - v.i.Store(i) -} diff --git a/vendor/nhooyr.io/websocket/netconn.go b/vendor/nhooyr.io/websocket/netconn.go deleted file mode 100644 index 64aadf0b99..0000000000 --- a/vendor/nhooyr.io/websocket/netconn.go +++ /dev/null @@ -1,166 +0,0 @@ -package websocket - -import ( - "context" - "fmt" - "io" - "math" - "net" - "sync" - "time" -) - -// NetConn converts a *websocket.Conn into a net.Conn. -// -// It's for tunneling arbitrary protocols over WebSockets. -// Few users of the library will need this but it's tricky to implement -// correctly and so provided in the library. -// See https://github.com/nhooyr/websocket/issues/100. -// -// Every Write to the net.Conn will correspond to a message write of -// the given type on *websocket.Conn. -// -// The passed ctx bounds the lifetime of the net.Conn. If cancelled, -// all reads and writes on the net.Conn will be cancelled. -// -// If a message is read that is not of the correct type, the connection -// will be closed with StatusUnsupportedData and an error will be returned. -// -// Close will close the *websocket.Conn with StatusNormalClosure. -// -// When a deadline is hit, the connection will be closed. This is -// different from most net.Conn implementations where only the -// reading/writing goroutines are interrupted but the connection is kept alive. -// -// The Addr methods will return a mock net.Addr that returns "websocket" for Network -// and "websocket/unknown-addr" for String. -// -// A received StatusNormalClosure or StatusGoingAway close frame will be translated to -// io.EOF when reading. -func NetConn(ctx context.Context, c *Conn, msgType MessageType) net.Conn { - nc := &netConn{ - c: c, - msgType: msgType, - } - - var cancel context.CancelFunc - nc.writeContext, cancel = context.WithCancel(ctx) - nc.writeTimer = time.AfterFunc(math.MaxInt64, cancel) - if !nc.writeTimer.Stop() { - <-nc.writeTimer.C - } - - nc.readContext, cancel = context.WithCancel(ctx) - nc.readTimer = time.AfterFunc(math.MaxInt64, cancel) - if !nc.readTimer.Stop() { - <-nc.readTimer.C - } - - return nc -} - -type netConn struct { - c *Conn - msgType MessageType - - writeTimer *time.Timer - writeContext context.Context - - readTimer *time.Timer - readContext context.Context - - readMu sync.Mutex - eofed bool - reader io.Reader -} - -var _ net.Conn = &netConn{} - -func (c *netConn) Close() error { - return c.c.Close(StatusNormalClosure, "") -} - -func (c *netConn) Write(p []byte) (int, error) { - err := c.c.Write(c.writeContext, c.msgType, p) - if err != nil { - return 0, err - } - return len(p), nil -} - -func (c *netConn) Read(p []byte) (int, error) { - c.readMu.Lock() - defer c.readMu.Unlock() - - if c.eofed { - return 0, io.EOF - } - - if c.reader == nil { - typ, r, err := c.c.Reader(c.readContext) - if err != nil { - switch CloseStatus(err) { - case StatusNormalClosure, StatusGoingAway: - c.eofed = true - return 0, io.EOF - } - return 0, err - } - if typ != c.msgType { - err := fmt.Errorf("unexpected frame type read (expected %v): %v", c.msgType, typ) - c.c.Close(StatusUnsupportedData, err.Error()) - return 0, err - } - c.reader = r - } - - n, err := c.reader.Read(p) - if err == io.EOF { - c.reader = nil - err = nil - } - return n, err -} - -type websocketAddr struct { -} - -func (a websocketAddr) Network() string { - return "websocket" -} - -func (a websocketAddr) String() string { - return "websocket/unknown-addr" -} - -func (c *netConn) RemoteAddr() net.Addr { - return websocketAddr{} -} - -func (c *netConn) LocalAddr() net.Addr { - return websocketAddr{} -} - -func (c *netConn) SetDeadline(t time.Time) error { - c.SetWriteDeadline(t) - c.SetReadDeadline(t) - return nil -} - -func (c *netConn) SetWriteDeadline(t time.Time) error { - if t.IsZero() { - c.writeTimer.Stop() - } else { - c.writeTimer.Reset(t.Sub(time.Now())) - } - return nil -} - -func (c *netConn) SetReadDeadline(t time.Time) error { - if t.IsZero() { - c.readTimer.Stop() - } else { - c.readTimer.Reset(t.Sub(time.Now())) - } - return nil -} diff --git a/vendor/nhooyr.io/websocket/read.go b/vendor/nhooyr.io/websocket/read.go deleted file mode 100644 index ae05cf93ed..0000000000 --- a/vendor/nhooyr.io/websocket/read.go +++ /dev/null @@ -1,474 +0,0 @@ -// +build !js - -package websocket - -import ( - "bufio" - "context" - "errors" - "fmt" - "io" - "io/ioutil" - "strings" - "time" - - "nhooyr.io/websocket/internal/errd" - "nhooyr.io/websocket/internal/xsync" -) - -// Reader reads from the connection until until there is a WebSocket -// data message to be read. It will handle ping, pong and close frames as appropriate. -// -// It returns the type of the message and an io.Reader to read it. -// The passed context will also bound the reader. -// Ensure you read to EOF otherwise the connection will hang. -// -// Call CloseRead if you do not expect any data messages from the peer. -// -// Only one Reader may be open at a time. -func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { - return c.reader(ctx) -} - -// Read is a convenience method around Reader to read a single message -// from the connection. -func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { - typ, r, err := c.Reader(ctx) - if err != nil { - return 0, nil, err - } - - b, err := ioutil.ReadAll(r) - return typ, b, err -} - -// CloseRead starts a goroutine to read from the connection until it is closed -// or a data message is received. -// -// Once CloseRead is called you cannot read any messages from the connection. -// The returned context will be cancelled when the connection is closed. -// -// If a data message is received, the connection will be closed with StatusPolicyViolation. -// -// Call CloseRead when you do not expect to read any more messages. -// Since it actively reads from the connection, it will ensure that ping, pong and close -// frames are responded to. This means c.Ping and c.Close will still work as expected. -func (c *Conn) CloseRead(ctx context.Context) context.Context { - ctx, cancel := context.WithCancel(ctx) - go func() { - defer cancel() - c.Reader(ctx) - c.Close(StatusPolicyViolation, "unexpected data message") - }() - return ctx -} - -// SetReadLimit sets the max number of bytes to read for a single message. -// It applies to the Reader and Read methods. -// -// By default, the connection has a message read limit of 32768 bytes. -// -// When the limit is hit, the connection will be closed with StatusMessageTooBig. -func (c *Conn) SetReadLimit(n int64) { - // We add read one more byte than the limit in case - // there is a fin frame that needs to be read. - c.msgReader.limitReader.limit.Store(n + 1) -} - -const defaultReadLimit = 32768 - -func newMsgReader(c *Conn) *msgReader { - mr := &msgReader{ - c: c, - fin: true, - } - mr.readFunc = mr.read - - mr.limitReader = newLimitReader(c, mr.readFunc, defaultReadLimit+1) - return mr -} - -func (mr *msgReader) resetFlate() { - if mr.flateContextTakeover() { - mr.dict.init(32768) - } - if mr.flateBufio == nil { - mr.flateBufio = getBufioReader(mr.readFunc) - } - - mr.flateReader = getFlateReader(mr.flateBufio, mr.dict.buf) - mr.limitReader.r = mr.flateReader - mr.flateTail.Reset(deflateMessageTail) -} - -func (mr *msgReader) putFlateReader() { - if mr.flateReader != nil { - putFlateReader(mr.flateReader) - mr.flateReader = nil - } -} - -func (mr *msgReader) close() { - mr.c.readMu.forceLock() - mr.putFlateReader() - mr.dict.close() - if mr.flateBufio != nil { - putBufioReader(mr.flateBufio) - } - - if mr.c.client { - putBufioReader(mr.c.br) - mr.c.br = nil - } -} - -func (mr *msgReader) flateContextTakeover() bool { - if mr.c.client { - return !mr.c.copts.serverNoContextTakeover - } - return !mr.c.copts.clientNoContextTakeover -} - -func (c *Conn) readRSV1Illegal(h header) bool { - // If compression is disabled, rsv1 is illegal. - if !c.flate() { - return true - } - // rsv1 is only allowed on data frames beginning messages. - if h.opcode != opText && h.opcode != opBinary { - return true - } - return false -} - -func (c *Conn) readLoop(ctx context.Context) (header, error) { - for { - h, err := c.readFrameHeader(ctx) - if err != nil { - return header{}, err - } - - if h.rsv1 && c.readRSV1Illegal(h) || h.rsv2 || h.rsv3 { - err := fmt.Errorf("received header with unexpected rsv bits set: %v:%v:%v", h.rsv1, h.rsv2, h.rsv3) - c.writeError(StatusProtocolError, err) - return header{}, err - } - - if !c.client && !h.masked { - return header{}, errors.New("received unmasked frame from client") - } - - switch h.opcode { - case opClose, opPing, opPong: - err = c.handleControl(ctx, h) - if err != nil { - // Pass through CloseErrors when receiving a close frame. - if h.opcode == opClose && CloseStatus(err) != -1 { - return header{}, err - } - return header{}, fmt.Errorf("failed to handle control frame %v: %w", h.opcode, err) - } - case opContinuation, opText, opBinary: - return h, nil - default: - err := fmt.Errorf("received unknown opcode %v", h.opcode) - c.writeError(StatusProtocolError, err) - return header{}, err - } - } -} - -func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { - select { - case <-c.closed: - return header{}, c.closeErr - case c.readTimeout <- ctx: - } - - h, err := readFrameHeader(c.br, c.readHeaderBuf[:]) - if err != nil { - select { - case <-c.closed: - return header{}, c.closeErr - case <-ctx.Done(): - return header{}, ctx.Err() - default: - c.close(err) - return header{}, err - } - } - - select { - case <-c.closed: - return header{}, c.closeErr - case c.readTimeout <- context.Background(): - } - - return h, nil -} - -func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { - select { - case <-c.closed: - return 0, c.closeErr - case c.readTimeout <- ctx: - } - - n, err := io.ReadFull(c.br, p) - if err != nil { - select { - case <-c.closed: - return n, c.closeErr - case <-ctx.Done(): - return n, ctx.Err() - default: - err = fmt.Errorf("failed to read frame payload: %w", err) - c.close(err) - return n, err - } - } - - select { - case <-c.closed: - return n, c.closeErr - case c.readTimeout <- context.Background(): - } - - return n, err -} - -func (c *Conn) handleControl(ctx context.Context, h header) (err error) { - if h.payloadLength < 0 || h.payloadLength > maxControlPayload { - err := fmt.Errorf("received control frame payload with invalid length: %d", h.payloadLength) - c.writeError(StatusProtocolError, err) - return err - } - - if !h.fin { - err := errors.New("received fragmented control frame") - c.writeError(StatusProtocolError, err) - return err - } - - ctx, cancel := context.WithTimeout(ctx, time.Second*5) - defer cancel() - - b := c.readControlBuf[:h.payloadLength] - _, err = c.readFramePayload(ctx, b) - if err != nil { - return err - } - - if h.masked { - mask(h.maskKey, b) - } - - switch h.opcode { - case opPing: - return c.writeControl(ctx, opPong, b) - case opPong: - c.activePingsMu.Lock() - pong, ok := c.activePings[string(b)] - c.activePingsMu.Unlock() - if ok { - select { - case pong <- struct{}{}: - default: - } - } - return nil - } - - defer func() { - c.readCloseFrameErr = err - }() - - ce, err := parseClosePayload(b) - if err != nil { - err = fmt.Errorf("received invalid close payload: %w", err) - c.writeError(StatusProtocolError, err) - return err - } - - err = fmt.Errorf("received close frame: %w", ce) - c.setCloseErr(err) - c.writeClose(ce.Code, ce.Reason) - c.close(err) - return err -} - -func (c *Conn) reader(ctx context.Context) (_ MessageType, _ io.Reader, err error) { - defer errd.Wrap(&err, "failed to get reader") - - err = c.readMu.lock(ctx) - if err != nil { - return 0, nil, err - } - defer c.readMu.unlock() - - if !c.msgReader.fin { - err = errors.New("previous message not read to completion") - c.close(fmt.Errorf("failed to get reader: %w", err)) - return 0, nil, err - } - - h, err := c.readLoop(ctx) - if err != nil { - return 0, nil, err - } - - if h.opcode == opContinuation { - err := errors.New("received continuation frame without text or binary frame") - c.writeError(StatusProtocolError, err) - return 0, nil, err - } - - c.msgReader.reset(ctx, h) - - return MessageType(h.opcode), c.msgReader, nil -} - -type msgReader struct { - c *Conn - - ctx context.Context - flate bool - flateReader io.Reader - flateBufio *bufio.Reader - flateTail strings.Reader - limitReader *limitReader - dict slidingWindow - - fin bool - payloadLength int64 - maskKey uint32 - - // readerFunc(mr.Read) to avoid continuous allocations. - readFunc readerFunc -} - -func (mr *msgReader) reset(ctx context.Context, h header) { - mr.ctx = ctx - mr.flate = h.rsv1 - mr.limitReader.reset(mr.readFunc) - - if mr.flate { - mr.resetFlate() - } - - mr.setFrame(h) -} - -func (mr *msgReader) setFrame(h header) { - mr.fin = h.fin - mr.payloadLength = h.payloadLength - mr.maskKey = h.maskKey -} - -func (mr *msgReader) Read(p []byte) (n int, err error) { - err = mr.c.readMu.lock(mr.ctx) - if err != nil { - return 0, fmt.Errorf("failed to read: %w", err) - } - defer mr.c.readMu.unlock() - - n, err = mr.limitReader.Read(p) - if mr.flate && mr.flateContextTakeover() { - p = p[:n] - mr.dict.write(p) - } - if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) && mr.fin && mr.flate { - mr.putFlateReader() - return n, io.EOF - } - if err != nil { - err = fmt.Errorf("failed to read: %w", err) - mr.c.close(err) - } - return n, err -} - -func (mr *msgReader) read(p []byte) (int, error) { - for { - if mr.payloadLength == 0 { - if mr.fin { - if mr.flate { - return mr.flateTail.Read(p) - } - return 0, io.EOF - } - - h, err := mr.c.readLoop(mr.ctx) - if err != nil { - return 0, err - } - if h.opcode != opContinuation { - err := errors.New("received new data message without finishing the previous message") - mr.c.writeError(StatusProtocolError, err) - return 0, err - } - mr.setFrame(h) - - continue - } - - if int64(len(p)) > mr.payloadLength { - p = p[:mr.payloadLength] - } - - n, err := mr.c.readFramePayload(mr.ctx, p) - if err != nil { - return n, err - } - - mr.payloadLength -= int64(n) - - if !mr.c.client { - mr.maskKey = mask(mr.maskKey, p) - } - - return n, nil - } -} - -type limitReader struct { - c *Conn - r io.Reader - limit xsync.Int64 - n int64 -} - -func newLimitReader(c *Conn, r io.Reader, limit int64) *limitReader { - lr := &limitReader{ - c: c, - } - lr.limit.Store(limit) - lr.reset(r) - return lr -} - -func (lr *limitReader) reset(r io.Reader) { - lr.n = lr.limit.Load() - lr.r = r -} - -func (lr *limitReader) Read(p []byte) (int, error) { - if lr.n <= 0 { - err := fmt.Errorf("read limited at %v bytes", lr.limit.Load()) - lr.c.writeError(StatusMessageTooBig, err) - return 0, err - } - - if int64(len(p)) > lr.n { - p = p[:lr.n] - } - n, err := lr.r.Read(p) - lr.n -= int64(n) - return n, err -} - -type readerFunc func(p []byte) (int, error) - -func (f readerFunc) Read(p []byte) (int, error) { - return f(p) -} diff --git a/vendor/nhooyr.io/websocket/stringer.go b/vendor/nhooyr.io/websocket/stringer.go deleted file mode 100644 index 5a66ba2907..0000000000 --- a/vendor/nhooyr.io/websocket/stringer.go +++ /dev/null @@ -1,91 +0,0 @@ -// Code generated by "stringer -type=opcode,MessageType,StatusCode -output=stringer.go"; DO NOT EDIT. - -package websocket - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[opContinuation-0] - _ = x[opText-1] - _ = x[opBinary-2] - _ = x[opClose-8] - _ = x[opPing-9] - _ = x[opPong-10] -} - -const ( - _opcode_name_0 = "opContinuationopTextopBinary" - _opcode_name_1 = "opCloseopPingopPong" -) - -var ( - _opcode_index_0 = [...]uint8{0, 14, 20, 28} - _opcode_index_1 = [...]uint8{0, 7, 13, 19} -) - -func (i opcode) String() string { - switch { - case 0 <= i && i <= 2: - return _opcode_name_0[_opcode_index_0[i]:_opcode_index_0[i+1]] - case 8 <= i && i <= 10: - i -= 8 - return _opcode_name_1[_opcode_index_1[i]:_opcode_index_1[i+1]] - default: - return "opcode(" + strconv.FormatInt(int64(i), 10) + ")" - } -} -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[MessageText-1] - _ = x[MessageBinary-2] -} - -const _MessageType_name = "MessageTextMessageBinary" - -var _MessageType_index = [...]uint8{0, 11, 24} - -func (i MessageType) String() string { - i -= 1 - if i < 0 || i >= MessageType(len(_MessageType_index)-1) { - return "MessageType(" + strconv.FormatInt(int64(i+1), 10) + ")" - } - return _MessageType_name[_MessageType_index[i]:_MessageType_index[i+1]] -} -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[StatusNormalClosure-1000] - _ = x[StatusGoingAway-1001] - _ = x[StatusProtocolError-1002] - _ = x[StatusUnsupportedData-1003] - _ = x[statusReserved-1004] - _ = x[StatusNoStatusRcvd-1005] - _ = x[StatusAbnormalClosure-1006] - _ = x[StatusInvalidFramePayloadData-1007] - _ = x[StatusPolicyViolation-1008] - _ = x[StatusMessageTooBig-1009] - _ = x[StatusMandatoryExtension-1010] - _ = x[StatusInternalError-1011] - _ = x[StatusServiceRestart-1012] - _ = x[StatusTryAgainLater-1013] - _ = x[StatusBadGateway-1014] - _ = x[StatusTLSHandshake-1015] -} - -const _StatusCode_name = "StatusNormalClosureStatusGoingAwayStatusProtocolErrorStatusUnsupportedDatastatusReservedStatusNoStatusRcvdStatusAbnormalClosureStatusInvalidFramePayloadDataStatusPolicyViolationStatusMessageTooBigStatusMandatoryExtensionStatusInternalErrorStatusServiceRestartStatusTryAgainLaterStatusBadGatewayStatusTLSHandshake" - -var _StatusCode_index = [...]uint16{0, 19, 34, 53, 74, 88, 106, 127, 156, 177, 196, 220, 239, 259, 278, 294, 312} - -func (i StatusCode) String() string { - i -= 1000 - if i < 0 || i >= StatusCode(len(_StatusCode_index)-1) { - return "StatusCode(" + strconv.FormatInt(int64(i+1000), 10) + ")" - } - return _StatusCode_name[_StatusCode_index[i]:_StatusCode_index[i+1]] -} diff --git a/vendor/nhooyr.io/websocket/write.go b/vendor/nhooyr.io/websocket/write.go deleted file mode 100644 index 2210cf817a..0000000000 --- a/vendor/nhooyr.io/websocket/write.go +++ /dev/null @@ -1,397 +0,0 @@ -// +build !js - -package websocket - -import ( - "bufio" - "context" - "crypto/rand" - "encoding/binary" - "errors" - "fmt" - "io" - "time" - - "github.com/klauspost/compress/flate" - - "nhooyr.io/websocket/internal/errd" -) - -// Writer returns a writer bounded by the context that will write -// a WebSocket message of type dataType to the connection. -// -// You must close the writer once you have written the entire message. -// -// Only one writer can be open at a time, multiple calls will block until the previous writer -// is closed. -func (c *Conn) Writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) { - w, err := c.writer(ctx, typ) - if err != nil { - return nil, fmt.Errorf("failed to get writer: %w", err) - } - return w, nil -} - -// Write writes a message to the connection. -// -// See the Writer method if you want to stream a message. -// -// If compression is disabled or the threshold is not met, then it -// will write the message in a single frame. -func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { - _, err := c.write(ctx, typ, p) - if err != nil { - return fmt.Errorf("failed to write msg: %w", err) - } - return nil -} - -type msgWriter struct { - mw *msgWriterState - closed bool -} - -func (mw *msgWriter) Write(p []byte) (int, error) { - if mw.closed { - return 0, errors.New("cannot use closed writer") - } - return mw.mw.Write(p) -} - -func (mw *msgWriter) Close() error { - if mw.closed { - return errors.New("cannot use closed writer") - } - mw.closed = true - return mw.mw.Close() -} - -type msgWriterState struct { - c *Conn - - mu *mu - writeMu *mu - - ctx context.Context - opcode opcode - flate bool - - trimWriter *trimLastFourBytesWriter - dict slidingWindow -} - -func newMsgWriterState(c *Conn) *msgWriterState { - mw := &msgWriterState{ - c: c, - mu: newMu(c), - writeMu: newMu(c), - } - return mw -} - -func (mw *msgWriterState) ensureFlate() { - if mw.trimWriter == nil { - mw.trimWriter = &trimLastFourBytesWriter{ - w: writerFunc(mw.write), - } - } - - mw.dict.init(8192) - mw.flate = true -} - -func (mw *msgWriterState) flateContextTakeover() bool { - if mw.c.client { - return !mw.c.copts.clientNoContextTakeover - } - return !mw.c.copts.serverNoContextTakeover -} - -func (c *Conn) writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) { - err := c.msgWriterState.reset(ctx, typ) - if err != nil { - return nil, err - } - return &msgWriter{ - mw: c.msgWriterState, - closed: false, - }, nil -} - -func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error) { - mw, err := c.writer(ctx, typ) - if err != nil { - return 0, err - } - - if !c.flate() { - defer c.msgWriterState.mu.unlock() - return c.writeFrame(ctx, true, false, c.msgWriterState.opcode, p) - } - - n, err := mw.Write(p) - if err != nil { - return n, err - } - - err = mw.Close() - return n, err -} - -func (mw *msgWriterState) reset(ctx context.Context, typ MessageType) error { - err := mw.mu.lock(ctx) - if err != nil { - return err - } - - mw.ctx = ctx - mw.opcode = opcode(typ) - mw.flate = false - - mw.trimWriter.reset() - - return nil -} - -// Write writes the given bytes to the WebSocket connection. -func (mw *msgWriterState) Write(p []byte) (_ int, err error) { - err = mw.writeMu.lock(mw.ctx) - if err != nil { - return 0, fmt.Errorf("failed to write: %w", err) - } - defer mw.writeMu.unlock() - - defer func() { - if err != nil { - err = fmt.Errorf("failed to write: %w", err) - mw.c.close(err) - } - }() - - if mw.c.flate() { - // Only enables flate if the length crosses the - // threshold on the first frame - if mw.opcode != opContinuation && len(p) >= mw.c.flateThreshold { - mw.ensureFlate() - } - } - - if mw.flate { - err = flate.StatelessDeflate(mw.trimWriter, p, false, mw.dict.buf) - if err != nil { - return 0, err - } - mw.dict.write(p) - return len(p), nil - } - - return mw.write(p) -} - -func (mw *msgWriterState) write(p []byte) (int, error) { - n, err := mw.c.writeFrame(mw.ctx, false, mw.flate, mw.opcode, p) - if err != nil { - return n, fmt.Errorf("failed to write data frame: %w", err) - } - mw.opcode = opContinuation - return n, nil -} - -// Close flushes the frame to the connection. -func (mw *msgWriterState) Close() (err error) { - defer errd.Wrap(&err, "failed to close writer") - - err = mw.writeMu.lock(mw.ctx) - if err != nil { - return err - } - defer mw.writeMu.unlock() - - _, err = mw.c.writeFrame(mw.ctx, true, mw.flate, mw.opcode, nil) - if err != nil { - return fmt.Errorf("failed to write fin frame: %w", err) - } - - if mw.flate && !mw.flateContextTakeover() { - mw.dict.close() - } - mw.mu.unlock() - return nil -} - -func (mw *msgWriterState) close() { - if mw.c.client { - mw.c.writeFrameMu.forceLock() - putBufioWriter(mw.c.bw) - } - - mw.writeMu.forceLock() - mw.dict.close() -} - -func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error { - ctx, cancel := context.WithTimeout(ctx, time.Second*5) - defer cancel() - - _, err := c.writeFrame(ctx, true, false, opcode, p) - if err != nil { - return fmt.Errorf("failed to write control frame %v: %w", opcode, err) - } - return nil -} - -// frame handles all writes to the connection. -func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opcode, p []byte) (_ int, err error) { - err = c.writeFrameMu.lock(ctx) - if err != nil { - return 0, err - } - defer c.writeFrameMu.unlock() - - // If the state says a close has already been written, we wait until - // the connection is closed and return that error. - // - // However, if the frame being written is a close, that means its the close from - // the state being set so we let it go through. - c.closeMu.Lock() - wroteClose := c.wroteClose - c.closeMu.Unlock() - if wroteClose && opcode != opClose { - select { - case <-ctx.Done(): - return 0, ctx.Err() - case <-c.closed: - return 0, c.closeErr - } - } - - select { - case <-c.closed: - return 0, c.closeErr - case c.writeTimeout <- ctx: - } - - defer func() { - if err != nil { - select { - case <-c.closed: - err = c.closeErr - case <-ctx.Done(): - err = ctx.Err() - } - c.close(err) - err = fmt.Errorf("failed to write frame: %w", err) - } - }() - - c.writeHeader.fin = fin - c.writeHeader.opcode = opcode - c.writeHeader.payloadLength = int64(len(p)) - - if c.client { - c.writeHeader.masked = true - _, err = io.ReadFull(rand.Reader, c.writeHeaderBuf[:4]) - if err != nil { - return 0, fmt.Errorf("failed to generate masking key: %w", err) - } - c.writeHeader.maskKey = binary.LittleEndian.Uint32(c.writeHeaderBuf[:]) - } - - c.writeHeader.rsv1 = false - if flate && (opcode == opText || opcode == opBinary) { - c.writeHeader.rsv1 = true - } - - err = writeFrameHeader(c.writeHeader, c.bw, c.writeHeaderBuf[:]) - if err != nil { - return 0, err - } - - n, err := c.writeFramePayload(p) - if err != nil { - return n, err - } - - if c.writeHeader.fin { - err = c.bw.Flush() - if err != nil { - return n, fmt.Errorf("failed to flush: %w", err) - } - } - - select { - case <-c.closed: - return n, c.closeErr - case c.writeTimeout <- context.Background(): - } - - return n, nil -} - -func (c *Conn) writeFramePayload(p []byte) (n int, err error) { - defer errd.Wrap(&err, "failed to write frame payload") - - if !c.writeHeader.masked { - return c.bw.Write(p) - } - - maskKey := c.writeHeader.maskKey - for len(p) > 0 { - // If the buffer is full, we need to flush. - if c.bw.Available() == 0 { - err = c.bw.Flush() - if err != nil { - return n, err - } - } - - // Start of next write in the buffer. - i := c.bw.Buffered() - - j := len(p) - if j > c.bw.Available() { - j = c.bw.Available() - } - - _, err := c.bw.Write(p[:j]) - if err != nil { - return n, err - } - - maskKey = mask(maskKey, c.writeBuf[i:c.bw.Buffered()]) - - p = p[j:] - n += j - } - - return n, nil -} - -type writerFunc func(p []byte) (int, error) - -func (f writerFunc) Write(p []byte) (int, error) { - return f(p) -} - -// extractBufioWriterBuf grabs the []byte backing a *bufio.Writer -// and returns it. -func extractBufioWriterBuf(bw *bufio.Writer, w io.Writer) []byte { - var writeBuf []byte - bw.Reset(writerFunc(func(p2 []byte) (int, error) { - writeBuf = p2[:cap(p2)] - return len(p2), nil - })) - - bw.WriteByte(0) - bw.Flush() - - bw.Reset(w) - - return writeBuf -} - -func (c *Conn) writeError(code StatusCode, err error) { - c.setCloseErr(err) - c.writeClose(code, err.Error()) - c.close(nil) -} diff --git a/vendor/nhooyr.io/websocket/ws_js.go b/vendor/nhooyr.io/websocket/ws_js.go deleted file mode 100644 index b87e32cdaf..0000000000 --- a/vendor/nhooyr.io/websocket/ws_js.go +++ /dev/null @@ -1,379 +0,0 @@ -package websocket // import "nhooyr.io/websocket" - -import ( - "bytes" - "context" - "errors" - "fmt" - "io" - "net/http" - "reflect" - "runtime" - "strings" - "sync" - "syscall/js" - - "nhooyr.io/websocket/internal/bpool" - "nhooyr.io/websocket/internal/wsjs" - "nhooyr.io/websocket/internal/xsync" -) - -// Conn provides a wrapper around the browser WebSocket API. -type Conn struct { - ws wsjs.WebSocket - - // read limit for a message in bytes. - msgReadLimit xsync.Int64 - - closingMu sync.Mutex - isReadClosed xsync.Int64 - closeOnce sync.Once - closed chan struct{} - closeErrOnce sync.Once - closeErr error - closeWasClean bool - - releaseOnClose func() - releaseOnMessage func() - - readSignal chan struct{} - readBufMu sync.Mutex - readBuf []wsjs.MessageEvent -} - -func (c *Conn) close(err error, wasClean bool) { - c.closeOnce.Do(func() { - runtime.SetFinalizer(c, nil) - - if !wasClean { - err = fmt.Errorf("unclean connection close: %w", err) - } - c.setCloseErr(err) - c.closeWasClean = wasClean - close(c.closed) - }) -} - -func (c *Conn) init() { - c.closed = make(chan struct{}) - c.readSignal = make(chan struct{}, 1) - - c.msgReadLimit.Store(32768) - - c.releaseOnClose = c.ws.OnClose(func(e wsjs.CloseEvent) { - err := CloseError{ - Code: StatusCode(e.Code), - Reason: e.Reason, - } - // We do not know if we sent or received this close as - // its possible the browser triggered it without us - // explicitly sending it. - c.close(err, e.WasClean) - - c.releaseOnClose() - c.releaseOnMessage() - }) - - c.releaseOnMessage = c.ws.OnMessage(func(e wsjs.MessageEvent) { - c.readBufMu.Lock() - defer c.readBufMu.Unlock() - - c.readBuf = append(c.readBuf, e) - - // Lets the read goroutine know there is definitely something in readBuf. - select { - case c.readSignal <- struct{}{}: - default: - } - }) - - runtime.SetFinalizer(c, func(c *Conn) { - c.setCloseErr(errors.New("connection garbage collected")) - c.closeWithInternal() - }) -} - -func (c *Conn) closeWithInternal() { - c.Close(StatusInternalError, "something went wrong") -} - -// Read attempts to read a message from the connection. -// The maximum time spent waiting is bounded by the context. -func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { - if c.isReadClosed.Load() == 1 { - return 0, nil, errors.New("WebSocket connection read closed") - } - - typ, p, err := c.read(ctx) - if err != nil { - return 0, nil, fmt.Errorf("failed to read: %w", err) - } - if int64(len(p)) > c.msgReadLimit.Load() { - err := fmt.Errorf("read limited at %v bytes", c.msgReadLimit.Load()) - c.Close(StatusMessageTooBig, err.Error()) - return 0, nil, err - } - return typ, p, nil -} - -func (c *Conn) read(ctx context.Context) (MessageType, []byte, error) { - select { - case <-ctx.Done(): - c.Close(StatusPolicyViolation, "read timed out") - return 0, nil, ctx.Err() - case <-c.readSignal: - case <-c.closed: - return 0, nil, c.closeErr - } - - c.readBufMu.Lock() - defer c.readBufMu.Unlock() - - me := c.readBuf[0] - // We copy the messages forward and decrease the size - // of the slice to avoid reallocating. - copy(c.readBuf, c.readBuf[1:]) - c.readBuf = c.readBuf[:len(c.readBuf)-1] - - if len(c.readBuf) > 0 { - // Next time we read, we'll grab the message. - select { - case c.readSignal <- struct{}{}: - default: - } - } - - switch p := me.Data.(type) { - case string: - return MessageText, []byte(p), nil - case []byte: - return MessageBinary, p, nil - default: - panic("websocket: unexpected data type from wsjs OnMessage: " + reflect.TypeOf(me.Data).String()) - } -} - -// Ping is mocked out for Wasm. -func (c *Conn) Ping(ctx context.Context) error { - return nil -} - -// Write writes a message of the given type to the connection. -// Always non blocking. -func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { - err := c.write(ctx, typ, p) - if err != nil { - // Have to ensure the WebSocket is closed after a write error - // to match the Go API. It can only error if the message type - // is unexpected or the passed bytes contain invalid UTF-8 for - // MessageText. - err := fmt.Errorf("failed to write: %w", err) - c.setCloseErr(err) - c.closeWithInternal() - return err - } - return nil -} - -func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) error { - if c.isClosed() { - return c.closeErr - } - switch typ { - case MessageBinary: - return c.ws.SendBytes(p) - case MessageText: - return c.ws.SendText(string(p)) - default: - return fmt.Errorf("unexpected message type: %v", typ) - } -} - -// Close closes the WebSocket with the given code and reason. -// It will wait until the peer responds with a close frame -// or the connection is closed. -// It thus performs the full WebSocket close handshake. -func (c *Conn) Close(code StatusCode, reason string) error { - err := c.exportedClose(code, reason) - if err != nil { - return fmt.Errorf("failed to close WebSocket: %w", err) - } - return nil -} - -func (c *Conn) exportedClose(code StatusCode, reason string) error { - c.closingMu.Lock() - defer c.closingMu.Unlock() - - ce := fmt.Errorf("sent close: %w", CloseError{ - Code: code, - Reason: reason, - }) - - if c.isClosed() { - return fmt.Errorf("tried to close with %q but connection already closed: %w", ce, c.closeErr) - } - - c.setCloseErr(ce) - err := c.ws.Close(int(code), reason) - if err != nil { - return err - } - - <-c.closed - if !c.closeWasClean { - return c.closeErr - } - return nil -} - -// Subprotocol returns the negotiated subprotocol. -// An empty string means the default protocol. -func (c *Conn) Subprotocol() string { - return c.ws.Subprotocol() -} - -// DialOptions represents the options available to pass to Dial. -type DialOptions struct { - // Subprotocols lists the subprotocols to negotiate with the server. - Subprotocols []string -} - -// Dial creates a new WebSocket connection to the given url with the given options. -// The passed context bounds the maximum time spent waiting for the connection to open. -// The returned *http.Response is always nil or a mock. It's only in the signature -// to match the core API. -func Dial(ctx context.Context, url string, opts *DialOptions) (*Conn, *http.Response, error) { - c, resp, err := dial(ctx, url, opts) - if err != nil { - return nil, nil, fmt.Errorf("failed to WebSocket dial %q: %w", url, err) - } - return c, resp, nil -} - -func dial(ctx context.Context, url string, opts *DialOptions) (*Conn, *http.Response, error) { - if opts == nil { - opts = &DialOptions{} - } - - url = strings.Replace(url, "http://", "ws://", 1) - url = strings.Replace(url, "https://", "wss://", 1) - - ws, err := wsjs.New(url, opts.Subprotocols) - if err != nil { - return nil, nil, err - } - - c := &Conn{ - ws: ws, - } - c.init() - - opench := make(chan struct{}) - releaseOpen := ws.OnOpen(func(e js.Value) { - close(opench) - }) - defer releaseOpen() - - select { - case <-ctx.Done(): - c.Close(StatusPolicyViolation, "dial timed out") - return nil, nil, ctx.Err() - case <-opench: - return c, &http.Response{ - StatusCode: http.StatusSwitchingProtocols, - }, nil - case <-c.closed: - return nil, nil, c.closeErr - } -} - -// Reader attempts to read a message from the connection. -// The maximum time spent waiting is bounded by the context. -func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { - typ, p, err := c.Read(ctx) - if err != nil { - return 0, nil, err - } - return typ, bytes.NewReader(p), nil -} - -// Writer returns a writer to write a WebSocket data message to the connection. -// It buffers the entire message in memory and then sends it when the writer -// is closed. -func (c *Conn) Writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) { - return writer{ - c: c, - ctx: ctx, - typ: typ, - b: bpool.Get(), - }, nil -} - -type writer struct { - closed bool - - c *Conn - ctx context.Context - typ MessageType - - b *bytes.Buffer -} - -func (w writer) Write(p []byte) (int, error) { - if w.closed { - return 0, errors.New("cannot write to closed writer") - } - n, err := w.b.Write(p) - if err != nil { - return n, fmt.Errorf("failed to write message: %w", err) - } - return n, nil -} - -func (w writer) Close() error { - if w.closed { - return errors.New("cannot close closed writer") - } - w.closed = true - defer bpool.Put(w.b) - - err := w.c.Write(w.ctx, w.typ, w.b.Bytes()) - if err != nil { - return fmt.Errorf("failed to close writer: %w", err) - } - return nil -} - -// CloseRead implements *Conn.CloseRead for wasm. -func (c *Conn) CloseRead(ctx context.Context) context.Context { - c.isReadClosed.Store(1) - - ctx, cancel := context.WithCancel(ctx) - go func() { - defer cancel() - c.read(ctx) - c.Close(StatusPolicyViolation, "unexpected data message") - }() - return ctx -} - -// SetReadLimit implements *Conn.SetReadLimit for wasm. -func (c *Conn) SetReadLimit(n int64) { - c.msgReadLimit.Store(n) -} - -func (c *Conn) setCloseErr(err error) { - c.closeErrOnce.Do(func() { - c.closeErr = fmt.Errorf("WebSocket closed: %w", err) - }) -} - -func (c *Conn) isClosed() bool { - select { - case <-c.closed: - return true - default: - return false - } -} diff --git a/vendor/nhooyr.io/websocket/wsjson/wsjson.go b/vendor/nhooyr.io/websocket/wsjson/wsjson.go deleted file mode 100644 index 2000a77af8..0000000000 --- a/vendor/nhooyr.io/websocket/wsjson/wsjson.go +++ /dev/null @@ -1,67 +0,0 @@ -// Package wsjson provides helpers for reading and writing JSON messages. -package wsjson // import "nhooyr.io/websocket/wsjson" - -import ( - "context" - "encoding/json" - "fmt" - - "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/bpool" - "nhooyr.io/websocket/internal/errd" -) - -// Read reads a JSON message from c into v. -// It will reuse buffers in between calls to avoid allocations. -func Read(ctx context.Context, c *websocket.Conn, v interface{}) error { - return read(ctx, c, v) -} - -func read(ctx context.Context, c *websocket.Conn, v interface{}) (err error) { - defer errd.Wrap(&err, "failed to read JSON message") - - _, r, err := c.Reader(ctx) - if err != nil { - return err - } - - b := bpool.Get() - defer bpool.Put(b) - - _, err = b.ReadFrom(r) - if err != nil { - return err - } - - err = json.Unmarshal(b.Bytes(), v) - if err != nil { - c.Close(websocket.StatusInvalidFramePayloadData, "failed to unmarshal JSON") - return fmt.Errorf("failed to unmarshal JSON: %w", err) - } - - return nil -} - -// Write writes the JSON message v to c. -// It will reuse buffers in between calls to avoid allocations. -func Write(ctx context.Context, c *websocket.Conn, v interface{}) error { - return write(ctx, c, v) -} - -func write(ctx context.Context, c *websocket.Conn, v interface{}) (err error) { - defer errd.Wrap(&err, "failed to write JSON message") - - w, err := c.Writer(ctx, websocket.MessageText) - if err != nil { - return err - } - - // json.Marshal cannot reuse buffers between calls as it has to return - // a copy of the byte slice but Encoder does as it directly writes to w. - err = json.NewEncoder(w).Encode(v) - if err != nil { - return fmt.Errorf("failed to marshal JSON: %w", err) - } - - return w.Close() -}