-
-
Notifications
You must be signed in to change notification settings - Fork 153
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Websocket notifier implementation in the relay proxy (#802)
- Loading branch information
1 parent
65a4062
commit f568822
Showing
20 changed files
with
1,025 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package middleware | ||
|
||
import ( | ||
"github.com/labstack/echo/v4" | ||
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/config" | ||
) | ||
|
||
// WebsocketAuthorizer is a middleware that checks in the params if we have the needed parameter for authorization | ||
func WebsocketAuthorizer(config *config.Config) echo.MiddlewareFunc { | ||
return func(next echo.HandlerFunc) echo.HandlerFunc { | ||
return func(c echo.Context) error { | ||
if len(config.APIKeys) > 0 { | ||
apiKey := c.QueryParam("apiKey") | ||
if !config.APIKeyExists(apiKey) { | ||
return echo.ErrUnauthorized | ||
} | ||
} | ||
return next(c) | ||
} | ||
} | ||
} |
65 changes: 65 additions & 0 deletions
65
cmd/relayproxy/api/middleware/websocket_authorizer_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
package middleware_test | ||
|
||
import ( | ||
"fmt" | ||
middleware2 "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/api/middleware" | ||
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/config" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
"github.com/labstack/echo/v4" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestWebsocketAuthorizer(t *testing.T) { | ||
type args struct { | ||
confAPIKey string | ||
urlAPIKey string | ||
} | ||
tests := []struct { | ||
name string | ||
args args | ||
want int | ||
wantErr assert.ErrorAssertionFunc | ||
}{ | ||
{ | ||
name: "valid apiKey", | ||
args: args{ | ||
confAPIKey: "valid-api-key", | ||
urlAPIKey: "valid-api-key", | ||
}, | ||
want: http.StatusOK, | ||
wantErr: assert.NoError, | ||
}, | ||
{ | ||
name: "invalid apiKey", | ||
args: args{ | ||
confAPIKey: "valid-api-key", | ||
urlAPIKey: "invalid-api-key", | ||
}, | ||
want: http.StatusUnauthorized, | ||
wantErr: assert.Error, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
e := echo.New() | ||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/websocket?apiKey=%s", tt.args.urlAPIKey), nil) | ||
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) | ||
rec := httptest.NewRecorder() | ||
c := e.NewContext(req, rec) | ||
conf := &config.Config{ | ||
APIKeys: []string{tt.args.confAPIKey}, | ||
} | ||
middleware := middleware2.WebsocketAuthorizer(conf) | ||
handler := middleware(func(c echo.Context) error { | ||
return c.String(http.StatusOK, "Authorized") | ||
}) | ||
|
||
err := handler(c) | ||
tt.wantErr(t, err) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
package controller | ||
|
||
import ( | ||
"github.com/gorilla/websocket" | ||
"github.com/labstack/echo/v4" | ||
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/service" | ||
"go.uber.org/zap" | ||
"net/http" | ||
"time" | ||
) | ||
|
||
// NewWsFlagChange is the constructor to create a new controller to handle websocket | ||
// request to be notified about flag changes. | ||
func NewWsFlagChange(websocketService service.WebsocketService, logger *zap.Logger) Controller { | ||
return &wsFlagChange{ | ||
websocketService: websocketService, | ||
upgrader: websocket.Upgrader{ | ||
CheckOrigin: func(r *http.Request) bool { | ||
return true | ||
}, | ||
}, | ||
logger: logger, | ||
} | ||
} | ||
|
||
// wsFlagChange is the implementation of the controller | ||
type wsFlagChange struct { | ||
websocketService service.WebsocketService | ||
upgrader websocket.Upgrader | ||
logger *zap.Logger | ||
} | ||
|
||
// Handler is the entry point for the websocket endpoint to get notified when a flag has been edited | ||
// @Summary Websocket endpoint to be notified about flag changes | ||
// @Description This endpoint is a websocket endpoint to be notified about flag changes, every change | ||
// @Description will send a request to the client with a model.DiffCache format. | ||
// @Description | ||
// @Produce json | ||
// @Accept json | ||
// @Param apiKey query string false "apiKey use authorize the connection to the relay proxy" | ||
// @Success 200 {object} notifier.DiffCache "Success" | ||
// @Failure 400 {object} modeldocs.HTTPErrorDoc "Bad Request" | ||
// @Failure 500 {object} modeldocs.HTTPErrorDoc "Internal server error" | ||
// @Router /ws/v1/flag/change [post] | ||
func (f *wsFlagChange) Handler(c echo.Context) error { | ||
conn, err := f.upgrader.Upgrade(c.Response(), c.Request(), nil) | ||
if err != nil { | ||
return err | ||
} | ||
defer func() { _ = conn.Close() }() | ||
f.websocketService.Register(conn) | ||
defer f.websocketService.Deregister(conn) | ||
f.logger.Debug("registering new websocket connection", zap.Any("connection", conn)) | ||
|
||
// Start the ping pong loop | ||
go f.pingPongLoop(conn) | ||
isOpen := true | ||
for isOpen { | ||
_, _, err := conn.ReadMessage() | ||
if err != nil { | ||
f.websocketService.Deregister(conn) | ||
f.logger.Debug("closing websocket connection", zap.Error(err), zap.Any("connection", conn)) | ||
isOpen = false | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// pingPongLoop is a keep-alive call to the client. | ||
// It calls the client to ensure that the connection is still active. | ||
// If the ping is not working we are closing the session. | ||
func (f *wsFlagChange) pingPongLoop(conn *websocket.Conn) { | ||
// Ping interval duration | ||
pingInterval := 1 * time.Second | ||
// Create a ticker to send pings at regular intervals | ||
ticker := time.NewTicker(pingInterval) | ||
defer ticker.Stop() | ||
|
||
// nolint: gosimple | ||
for { | ||
select { | ||
case <-ticker.C: | ||
// Send a ping message to the client | ||
err := conn.WriteMessage(websocket.PingMessage, nil) | ||
if err != nil { | ||
f.websocketService.Deregister(conn) | ||
f.logger.Debug("closing websocket connection", zap.Error(err), zap.Any("connection", conn)) | ||
return | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.