Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ require (
github.com/golang/mock v1.6.0
github.com/google/go-cmp v0.5.9
github.com/google/uuid v1.3.0
github.com/gorilla/websocket v1.5.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.14.0
github.com/hashicorp/go-hclog v1.3.1
github.com/hashicorp/go-plugin v1.4.6
Expand All @@ -32,7 +33,6 @@ require (
github.com/prometheus/client_model v0.3.0
github.com/prometheus/common v0.37.0
github.com/rs/zerolog v1.28.0
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75
go.buf.build/grpc/go/conduitio/conduit-connector-protocol v1.4.3
go.buf.build/protocolbuffers/go/grpc-ecosystem/grpc-gateway v1.3.50
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91
Expand Down Expand Up @@ -84,7 +84,6 @@ require (
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/flatbuffers v2.0.0+incompatible // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.13.0 // indirect
Expand All @@ -109,7 +108,6 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/segmentio/kafka-go v0.4.35 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/xdg/scram v1.0.5 // indirect
github.com/xdg/stringprep v1.0.3 // indirect
github.com/xitongsys/parquet-go v1.6.2 // indirect
Expand Down
9 changes: 2 additions & 7 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
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/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.14.0 h1:t7uX3JBHdVwAi3G7sSSdbsk8NfgA+LnUS88V/2EKaA0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.14.0/go.mod h1:4OGVnY4qf2+gw+ssiHbW+pq4mo2yko94YxxMmXZ7jCA=
Expand Down Expand Up @@ -532,8 +532,6 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
Expand All @@ -559,8 +557,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/xdg/scram v1.0.5 h1:TuS0RFmt5Is5qm9Tm2SoD89OPqe4IRiFtyFY4iwWXsw=
github.com/xdg/scram v1.0.5/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
Expand Down Expand Up @@ -710,7 +706,6 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
Expand Down
1 change: 1 addition & 0 deletions pkg/conduit/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ func (r *Runtime) serveHTTPAPI(
grpcutil.WithDefaultGatewayMiddleware(
r.logger, allowCORS(gwmux, "http://localhost:4200"),
),
r.logger,
)

return r.serveHTTP(
Expand Down
5 changes: 2 additions & 3 deletions pkg/foundation/grpcutil/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"github.com/conduitio/conduit/pkg/foundation/log"
"github.com/google/uuid"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/tmc/grpc-websocket-proxy/wsproxy"
"google.golang.org/protobuf/encoding/protojson"
)

Expand Down Expand Up @@ -110,8 +109,8 @@ func WithHTTPEndpointHeader(h http.Handler) http.Handler {
})
}

func WithWebsockets(h http.Handler) http.Handler {
return wsproxy.WebsocketProxy(h)
func WithWebsockets(h http.Handler, l log.CtxLogger) http.Handler {
return newWebSocketProxy(h, l)
}

func extractEndpoint(r *http.Request) string {
Expand Down
132 changes: 132 additions & 0 deletions pkg/foundation/grpcutil/websocket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright © 2022 Meroxa, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package grpcutil

import (
"bufio"
"context"
"io"
"net/http"

"github.com/conduitio/conduit/pkg/foundation/log"
"github.com/gorilla/websocket"
)

type inMemoryResponseWriter struct {
io.Writer
header http.Header
closed chan bool
}

func newInMemoryResponseWriter(writer io.Writer) *inMemoryResponseWriter {
return &inMemoryResponseWriter{
Writer: writer,
header: http.Header{},
closed: make(chan bool, 1),
}
}

func (w *inMemoryResponseWriter) Write(b []byte) (int, error) {
return w.Writer.Write(b)
}
func (w *inMemoryResponseWriter) Header() http.Header {
return w.header
}
func (w *inMemoryResponseWriter) WriteHeader(int) {
// we don't have a use for the code
}
func (w *inMemoryResponseWriter) CloseNotify() <-chan bool {
return w.closed
}
func (w *inMemoryResponseWriter) Flush() {}

// wsProxy is a proxy around a http.Handler which
// redirects the response data from the http.Handler
// to a WebSocket connection.
type wsProxy struct {
handler http.Handler
logger log.CtxLogger
upgrader websocket.Upgrader
}

func newWebSocketProxy(handler http.Handler, logger log.CtxLogger) *wsProxy {
return &wsProxy{
handler: handler,
logger: logger.WithComponent("grpcutil.websocket"),
Comment thread
lovromazgon marked this conversation as resolved.
Outdated
upgrader: websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
},
Comment thread
lovromazgon marked this conversation as resolved.
}
}

func (p *wsProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !websocket.IsWebSocketUpgrade(r) {
p.handler.ServeHTTP(w, r)
return
}
p.proxy(w, r)
}

func (p *wsProxy) proxy(w http.ResponseWriter, r *http.Request) {
ctx, cancelFn := context.WithCancel(r.Context())
defer cancelFn()

// upgrade connection to WebSocket
conn, err := p.upgrader.Upgrade(w, r, http.Header{})
if err != nil {
p.logger.Err(ctx, err).Msg("error upgrading websocket")
return
}
defer conn.Close()

// We use a pipe to read the data being written to the underlying http.Handler
// and then write it to the WebSocket connection.
responseR, responseW := io.Pipe()
response := newInMemoryResponseWriter(responseW)
go func() {
<-ctx.Done()
p.logger.Debug(ctx).Err(ctx.Err()).Msg("closing pipes")
responseW.CloseWithError(io.EOF)
response.closed <- true
}()

go func() {
defer cancelFn()
p.handler.ServeHTTP(response, r)
}()

scanner := bufio.NewScanner(responseR)

for scanner.Scan() {
if len(scanner.Bytes()) == 0 {
p.logger.Warn(ctx).Err(scanner.Err()).Msg("[write] empty scan")
continue
}

p.logger.Trace(ctx).Msgf("[write] scanned %v", scanner.Text())
if err := conn.WriteMessage(websocket.TextMessage, scanner.Bytes()); err != nil {
p.logger.Warn(ctx).Err(err).Msg("[write] error writing websocket message")
return
}
}

if sErr := scanner.Err(); sErr != nil {
p.logger.Err(ctx, sErr).Msg("failed reading data from original response")
if err := conn.WriteMessage(websocket.TextMessage, []byte(sErr.Error())); err != nil {
p.logger.Warn(ctx).Err(err).Msg("[write] failed writing scanner error")
}
}
}
88 changes: 88 additions & 0 deletions pkg/foundation/grpcutil/websocket_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright © 2022 Meroxa, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package grpcutil

import (
"context"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/conduitio/conduit/pkg/foundation/log"
"github.com/gorilla/websocket"
"github.com/matryer/is"
)

type testHandler struct {
is *is.I
response string
}

func (h *testHandler) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
_, err := w.Write([]byte(h.response))
h.is.NoErr(err)
}

func TestWebSocket_NoUpgradeToWebSocket(t *testing.T) {
Comment thread
hariso marked this conversation as resolved.
is := is.New(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

h := &testHandler{
is: is,
response: "hi there",
}
s := httptest.NewServer(newWebSocketProxy(h, log.Nop()))
defer s.Close()

req, err := http.NewRequestWithContext(ctx, "GET", s.URL, nil)
is.NoErr(err)

resp, err := http.DefaultClient.Do(req)
is.NoErr(err)
is.True(resp.Body != nil) // expected response to have a body
defer resp.Body.Close()

bytes, err := io.ReadAll(resp.Body)
is.NoErr(err)
is.Equal(h.response, string(bytes))
}

func TestWebSocket_UpgradeToWebSocket(t *testing.T) {
is := is.New(t)

h := &testHandler{
is: is,
response: "hi there",
}
s := httptest.NewServer(newWebSocketProxy(h, log.Nop()))
defer s.Close()

// Convert http to ws
wsURL := "ws" + strings.TrimPrefix(s.URL, "http")

// Connect to the server
ws, resp, err := websocket.DefaultDialer.Dial(wsURL, nil)
is.NoErr(err)
defer ws.Close()
defer resp.Body.Close()

msgType, bytes, err := ws.ReadMessage()
is.NoErr(err)
is.Equal(h.response, string(bytes))
is.Equal(websocket.TextMessage, msgType)
}