diff --git a/client_connection/manager.go b/client_connection/manager.go index 9598416d0..63a3c51cd 100644 --- a/client_connection/manager.go +++ b/client_connection/manager.go @@ -123,7 +123,8 @@ func statusDisconnecting() ConnectionStatus { return ConnectionStatus{Disconnecting, "", nil} } -func ConfigureVpnClientFactory(mysteriumAPIClient server.Client, vpnClientRuntimeDirectory string, signerFactory identity.SignerFactory) VpnClientFactory { +func ConfigureVpnClientFactory(mysteriumAPIClient server.Client, vpnClientRuntimeDirectory string, + signerFactory identity.SignerFactory, statsKeeper *bytescount.SessionStatsKeeper) VpnClientFactory { return func(vpnSession session.SessionDto, id identity.Identity) (openvpn.Client, error) { vpnConfig, err := openvpn.NewClientConfigFromString( vpnSession.Config, @@ -133,10 +134,13 @@ func ConfigureVpnClientFactory(mysteriumAPIClient server.Client, vpnClientRuntim return nil, err } + statsSaver := bytescount.NewSessionStatsSaver(statsKeeper) statsSender := bytescount.NewSessionStatsSender(mysteriumAPIClient, vpnSession.ID, signerFactory(id)) + statsHandler := bytescount.NewCompositeStatsHandler(statsSaver, statsSender) + authenticator := auth.NewAuthenticatorFake() vpnMiddlewares := []openvpn.ManagementMiddleware{ - bytescount.NewMiddleware(statsSender, 1*time.Minute), + bytescount.NewMiddleware(statsHandler, 1*time.Minute), auth.NewMiddleware(authenticator), } return openvpn.NewClient( diff --git a/cmd/mysterium_client/run/command.go b/cmd/mysterium_client/run/command.go index ada445c75..0f00187eb 100644 --- a/cmd/mysterium_client/run/command.go +++ b/cmd/mysterium_client/run/command.go @@ -10,6 +10,7 @@ import ( "github.com/mysterium/node/identity" "github.com/mysterium/node/ip" "github.com/mysterium/node/openvpn" + "github.com/mysterium/node/openvpn/middlewares/client/bytescount" "github.com/mysterium/node/server" "github.com/mysterium/node/tequilapi" tequilapi_endpoints "github.com/mysterium/node/tequilapi/endpoints" @@ -43,14 +44,16 @@ func NewCommandWith( return identity.NewSigner(keystoreInstance, id) } - vpnClientFactory := client_connection.ConfigureVpnClientFactory(mysteriumClient, options.DirectoryRuntime, signerFactory) + statsKeeper := &bytescount.SessionStatsKeeper{} + + vpnClientFactory := client_connection.ConfigureVpnClientFactory(mysteriumClient, options.DirectoryRuntime, signerFactory, statsKeeper) connectionManager := client_connection.NewManager(mysteriumClient, dialogEstablisherFactory, vpnClientFactory) router := tequilapi.NewAPIRouter() tequilapi_endpoints.AddRoutesForIdentities(router, identityManager, mysteriumClient, signerFactory) ipResolver := ip.NewResolver() - tequilapi_endpoints.AddRoutesForConnection(router, connectionManager, ipResolver) + tequilapi_endpoints.AddRoutesForConnection(router, connectionManager, ipResolver, statsKeeper) tequilapi_endpoints.AddRoutesForProposals(router, mysteriumClient) httpAPIServer := tequilapi.NewServer(options.TequilapiAddress, options.TequilapiPort, router) diff --git a/openvpn/middlewares/client/bytescount/composite_stats_handler.go b/openvpn/middlewares/client/bytescount/composite_stats_handler.go new file mode 100644 index 000000000..f52038938 --- /dev/null +++ b/openvpn/middlewares/client/bytescount/composite_stats_handler.go @@ -0,0 +1,14 @@ +package bytescount + +// NewCompositeStatsHandler composes multiple stats handlers into single one, which executes all handlers sequentially +func NewCompositeStatsHandler(statsHandlers ...SessionStatsHandler) SessionStatsHandler { + return func(stats SessionStats) error { + for _, handler := range statsHandlers { + err := handler(stats) + if err != nil { + return err + } + } + return nil + } +} diff --git a/openvpn/middlewares/client/bytescount/composite_stats_handler_test.go b/openvpn/middlewares/client/bytescount/composite_stats_handler_test.go new file mode 100644 index 000000000..8074d9126 --- /dev/null +++ b/openvpn/middlewares/client/bytescount/composite_stats_handler_test.go @@ -0,0 +1,40 @@ +package bytescount + +import ( + "errors" + "github.com/stretchr/testify/assert" + "testing" +) + +var stats = SessionStats{BytesSent: 1, BytesReceived: 1} + +func TestCompositeHandlerWithNoHandlers(t *testing.T) { + stats := SessionStats{BytesSent: 1, BytesReceived: 1} + + compositeHandler := NewCompositeStatsHandler() + assert.NoError(t, compositeHandler(stats)) +} + +func TestCompositeHandlerWithSuccessfulHandler(t *testing.T) { + statsRecorder := fakeStatsRecorder{} + compositeHandler := NewCompositeStatsHandler(statsRecorder.record) + assert.NoError(t, compositeHandler(stats)) + assert.Equal(t, stats, statsRecorder.LastSessionStats) +} + +func TestCompositeHandlerWithFailingHandler(t *testing.T) { + failingHandler := func(stats SessionStats) error { return errors.New("fake error") } + compositeHandler := NewCompositeStatsHandler(failingHandler) + assert.Error(t, compositeHandler(stats), "fake error") +} + +func TestCompositeHandlerWithMultipleHandlers(t *testing.T) { + recorder1 := fakeStatsRecorder{} + recorder2 := fakeStatsRecorder{} + + compositeHandler := NewCompositeStatsHandler(recorder1.record, recorder2.record) + assert.NoError(t, compositeHandler(stats)) + + assert.Equal(t, stats, recorder1.LastSessionStats) + assert.Equal(t, stats, recorder2.LastSessionStats) +} diff --git a/openvpn/middlewares/client/bytescount/dto.go b/openvpn/middlewares/client/bytescount/dto.go new file mode 100644 index 000000000..902e807f1 --- /dev/null +++ b/openvpn/middlewares/client/bytescount/dto.go @@ -0,0 +1,6 @@ +package bytescount + +// SessionStats represents statistics, generated by bytescount middleware +type SessionStats struct { + BytesSent, BytesReceived int +} diff --git a/openvpn/middlewares/client/bytescount/fake_stats_handler.go b/openvpn/middlewares/client/bytescount/fake_stats_handler.go new file mode 100644 index 000000000..c7eeb9b17 --- /dev/null +++ b/openvpn/middlewares/client/bytescount/fake_stats_handler.go @@ -0,0 +1,10 @@ +package bytescount + +type fakeStatsRecorder struct { + LastSessionStats SessionStats +} + +func (sender *fakeStatsRecorder) record(sessionStats SessionStats) error { + sender.LastSessionStats = sessionStats + return nil +} diff --git a/openvpn/middlewares/client/bytescount/middleware.go b/openvpn/middlewares/client/bytescount/middleware.go index 4e8166bdb..8c28d5986 100644 --- a/openvpn/middlewares/client/bytescount/middleware.go +++ b/openvpn/middlewares/client/bytescount/middleware.go @@ -9,18 +9,22 @@ import ( "time" ) +// SessionStatsHandler is invoked when middleware receives statistics +type SessionStatsHandler func(SessionStats) error + type middleware struct { - sessionStatsSender SessionStatsSender - interval time.Duration + sessionStatsHandler SessionStatsHandler + interval time.Duration state openvpn.State connection net.Conn } -func NewMiddleware(sessionStatsSender SessionStatsSender, interval time.Duration) openvpn.ManagementMiddleware { +// NewMiddleware returns new bytescount middleware +func NewMiddleware(sessionStatsHandler SessionStatsHandler, interval time.Duration) openvpn.ManagementMiddleware { return &middleware{ - sessionStatsSender: sessionStatsSender, - interval: interval, + sessionStatsHandler: sessionStatsHandler, + interval: interval, connection: nil, } @@ -62,7 +66,8 @@ func (middleware *middleware) ConsumeLine(line string) (consumed bool, err error return } - err = middleware.sessionStatsSender(bytesOut, bytesIn) + stats := SessionStats{BytesSent: bytesOut, BytesReceived: bytesIn} + err = middleware.sessionStatsHandler(stats) return } diff --git a/openvpn/middlewares/client/bytescount/middleware_test.go b/openvpn/middlewares/client/bytescount/middleware_test.go index 594db0521..58e7936be 100644 --- a/openvpn/middlewares/client/bytescount/middleware_test.go +++ b/openvpn/middlewares/client/bytescount/middleware_test.go @@ -45,25 +45,15 @@ func (conn *fakeConnection) SetWriteDeadline(t time.Time) error { return nil } -type fakeStatsSender struct { - lastBytesSent, lastBytesReceived int -} - -func (sender *fakeStatsSender) send(bytesSent, bytesReceived int) error { - sender.lastBytesSent = bytesSent - sender.lastBytesReceived = bytesReceived - return nil -} - func Test_Factory(t *testing.T) { - statsSender := fakeStatsSender{} - middleware := NewMiddleware(statsSender.send, 1*time.Minute) + statsRecorder := fakeStatsRecorder{} + middleware := NewMiddleware(statsRecorder.record, 1*time.Minute) assert.NotNil(t, middleware) } func Test_Start(t *testing.T) { - statsSender := fakeStatsSender{} - middleware := NewMiddleware(statsSender.send, 1*time.Minute) + statsRecorder := fakeStatsRecorder{} + middleware := NewMiddleware(statsRecorder.record, 1*time.Minute) connection := &fakeConnection{} middleware.Start(connection) assert.Equal(t, []byte("bytecount 60\n"), connection.lastDataWritten) @@ -89,8 +79,8 @@ func Test_ConsumeLine(t *testing.T) { } for _, test := range tests { - statsSender := &fakeStatsSender{} - middleware := NewMiddleware(statsSender.send, 1*time.Minute) + statsRecorder := &fakeStatsRecorder{} + middleware := NewMiddleware(statsRecorder.record, 1*time.Minute) consumed, err := middleware.ConsumeLine(test.line) if test.expectedError != nil { assert.Error(t, test.expectedError, err.Error(), test.line) @@ -98,7 +88,7 @@ func Test_ConsumeLine(t *testing.T) { assert.NoError(t, err, test.line) } assert.Equal(t, test.expectedConsumed, consumed, test.line) - assert.Equal(t, test.expectedBytesReceived, statsSender.lastBytesReceived) - assert.Equal(t, test.expectedBytesSent, statsSender.lastBytesSent) + assert.Equal(t, test.expectedBytesReceived, statsRecorder.LastSessionStats.BytesReceived) + assert.Equal(t, test.expectedBytesSent, statsRecorder.LastSessionStats.BytesSent) } } diff --git a/openvpn/middlewares/client/bytescount/stats_keeper.go b/openvpn/middlewares/client/bytescount/stats_keeper.go new file mode 100644 index 000000000..ccd363ed9 --- /dev/null +++ b/openvpn/middlewares/client/bytescount/stats_keeper.go @@ -0,0 +1,16 @@ +package bytescount + +// SessionStatsKeeper keeps session stats +type SessionStatsKeeper struct { + sessionStats SessionStats +} + +// Save saves session stats to keeper +func (keeper *SessionStatsKeeper) Save(stats SessionStats) { + keeper.sessionStats = stats +} + +// Retrieve retrieves session stats from keeper +func (keeper *SessionStatsKeeper) Retrieve() SessionStats { + return keeper.sessionStats +} diff --git a/openvpn/middlewares/client/bytescount/stats_keeper_test.go b/openvpn/middlewares/client/bytescount/stats_keeper_test.go new file mode 100644 index 000000000..1a5019292 --- /dev/null +++ b/openvpn/middlewares/client/bytescount/stats_keeper_test.go @@ -0,0 +1,14 @@ +package bytescount + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestSessionStatsStoreWorks(t *testing.T) { + statsKeeper := &SessionStatsKeeper{} + stats := SessionStats{BytesSent: 1, BytesReceived: 2} + + statsKeeper.Save(stats) + assert.Equal(t, stats, statsKeeper.Retrieve()) +} diff --git a/openvpn/middlewares/client/bytescount/stats_saver.go b/openvpn/middlewares/client/bytescount/stats_saver.go new file mode 100644 index 000000000..37853a62a --- /dev/null +++ b/openvpn/middlewares/client/bytescount/stats_saver.go @@ -0,0 +1,9 @@ +package bytescount + +// NewSessionStatsSaver returns stats handler, which saves stats stats keeper +func NewSessionStatsSaver(statsKeeper *SessionStatsKeeper) SessionStatsHandler { + return func(sessionStats SessionStats) error { + statsKeeper.Save(sessionStats) + return nil + } +} diff --git a/openvpn/middlewares/client/bytescount/stats_saver_test.go b/openvpn/middlewares/client/bytescount/stats_saver_test.go new file mode 100644 index 000000000..6f5b1c06d --- /dev/null +++ b/openvpn/middlewares/client/bytescount/stats_saver_test.go @@ -0,0 +1,15 @@ +package bytescount + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestNewSessionStatsSaver(t *testing.T) { + statsKeeper := &SessionStatsKeeper{} + + saver := NewSessionStatsSaver(statsKeeper) + stats := SessionStats{BytesSent: 1, BytesReceived: 2} + saver(stats) + assert.Equal(t, stats, statsKeeper.Retrieve()) +} diff --git a/openvpn/middlewares/client/bytescount/stats_sender.go b/openvpn/middlewares/client/bytescount/stats_sender.go index deb7973a4..d034a959e 100644 --- a/openvpn/middlewares/client/bytescount/stats_sender.go +++ b/openvpn/middlewares/client/bytescount/stats_sender.go @@ -7,16 +7,18 @@ import ( "github.com/mysterium/node/session" ) +// SessionStatsSender sends statistics to server type SessionStatsSender func(bytesSent, bytesReceived int) error -func NewSessionStatsSender(mysteriumClient server.Client, sessionID session.SessionID, signer identity.Signer) SessionStatsSender { +// NewSessionStatsSender returns new session stats handler, which sends statistics to server +func NewSessionStatsSender(mysteriumClient server.Client, sessionID session.SessionID, signer identity.Signer) SessionStatsHandler { sessionIDString := string(sessionID) - return func(bytesSent, bytesReceived int) error { + return func(sessionStats SessionStats) error { return mysteriumClient.SendSessionStats( sessionIDString, dto.SessionStats{ - BytesSent: bytesSent, - BytesReceived: bytesReceived, + BytesSent: sessionStats.BytesSent, + BytesReceived: sessionStats.BytesReceived, }, signer, ) diff --git a/tequilapi/endpoints/connection.go b/tequilapi/endpoints/connection.go index 18079e7db..a66c9884b 100644 --- a/tequilapi/endpoints/connection.go +++ b/tequilapi/endpoints/connection.go @@ -6,6 +6,7 @@ import ( "github.com/mysterium/node/client_connection" "github.com/mysterium/node/identity" "github.com/mysterium/node/ip" + "github.com/mysterium/node/openvpn/middlewares/client/bytescount" "github.com/mysterium/node/tequilapi/utils" "github.com/mysterium/node/tequilapi/validation" "net/http" @@ -22,14 +23,16 @@ type statusResponse struct { } type connectionEndpoint struct { - manager client_connection.Manager - ipResolver ip.Resolver + manager client_connection.Manager + ipResolver ip.Resolver + statsKeeper *bytescount.SessionStatsKeeper } -func NewConnectionEndpoint(manager client_connection.Manager, ipResolver ip.Resolver) *connectionEndpoint { +func NewConnectionEndpoint(manager client_connection.Manager, ipResolver ip.Resolver, statsKeeper *bytescount.SessionStatsKeeper) *connectionEndpoint { return &connectionEndpoint{ - manager: manager, - ipResolver: ipResolver, + manager: manager, + ipResolver: ipResolver, + statsKeeper: statsKeeper, } } @@ -81,13 +84,28 @@ func (ce *connectionEndpoint) GetIP(writer http.ResponseWriter, request *http.Re utils.WriteAsJSON(response, writer) } -// TODO: Uppercase IPResolver? -func AddRoutesForConnection(router *httprouter.Router, manager client_connection.Manager, ipResolver ip.Resolver) { - connectionEndpoint := NewConnectionEndpoint(manager, ipResolver) +// GetStatistics returns statistics about current connection +func (ce *connectionEndpoint) GetStatistics(writer http.ResponseWriter, request *http.Request, params httprouter.Params) { + stats := ce.statsKeeper.Retrieve() + response := struct { + BytesSent int `json:"bytesSent"` + BytesReceived int `json:"bytesReceived"` + }{ + BytesSent: stats.BytesSent, + BytesReceived: stats.BytesReceived, + } + utils.WriteAsJSON(response, writer) +} + +// AddRoutesForConnection adds connections routes to given router +func AddRoutesForConnection(router *httprouter.Router, manager client_connection.Manager, ipResolver ip.Resolver, + statsKeeper *bytescount.SessionStatsKeeper) { + connectionEndpoint := NewConnectionEndpoint(manager, ipResolver, statsKeeper) router.GET("/connection", connectionEndpoint.Status) router.PUT("/connection", connectionEndpoint.Create) router.DELETE("/connection", connectionEndpoint.Kill) router.GET("/connection/ip", connectionEndpoint.GetIP) + router.GET("/connection/statistics", connectionEndpoint.GetStatistics) } func toConnectionRequest(req *http.Request) (*connectionRequest, error) { diff --git a/tequilapi/endpoints/connection_test.go b/tequilapi/endpoints/connection_test.go index 67ea74fbe..8d3965766 100644 --- a/tequilapi/endpoints/connection_test.go +++ b/tequilapi/endpoints/connection_test.go @@ -6,6 +6,7 @@ import ( "github.com/mysterium/node/client_connection" "github.com/mysterium/node/identity" "github.com/mysterium/node/ip" + "github.com/mysterium/node/openvpn/middlewares/client/bytescount" "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" @@ -46,8 +47,9 @@ func TestAddRoutesForConnectionAddsRoutes(t *testing.T) { router := httprouter.New() fakeManager := fakeManager{} ipResolver := ip.NewFakeResolver("123.123.123.123") + statsKeeper := &bytescount.SessionStatsKeeper{} - AddRoutesForConnection(router, &fakeManager, ipResolver) + AddRoutesForConnection(router, &fakeManager, ipResolver, statsKeeper) tests := []struct { method string @@ -72,6 +74,10 @@ func TestAddRoutesForConnectionAddsRoutes(t *testing.T) { http.MethodGet, "/connection/ip", "", http.StatusOK, `{"ip": "123.123.123.123"}`, }, + { + http.MethodGet, "/connection/statistics", "", + http.StatusOK, `{"bytesSent": 0, "bytesReceived": 0}`, + }, } for _, test := range tests { @@ -94,7 +100,7 @@ func TestDisconnectingState(t *testing.T) { SessionID: "", } - connEndpoint := NewConnectionEndpoint(&fakeManager, nil) + connEndpoint := NewConnectionEndpoint(&fakeManager, nil, nil) req := httptest.NewRequest(http.MethodGet, "/irrelevant", nil) resp := httptest.NewRecorder() @@ -116,7 +122,7 @@ func TestNotConnectedStateIsReturnedWhenNoConnection(t *testing.T) { SessionID: "", } - connEndpoint := NewConnectionEndpoint(&fakeManager, nil) + connEndpoint := NewConnectionEndpoint(&fakeManager, nil, nil) req := httptest.NewRequest(http.MethodGet, "/irrelevant", nil) resp := httptest.NewRecorder() @@ -138,7 +144,7 @@ func TestStateConnectingIsReturnedWhenIsConnectionInProgress(t *testing.T) { State: client_connection.Connecting, } - connEndpoint := NewConnectionEndpoint(&fakeManager, nil) + connEndpoint := NewConnectionEndpoint(&fakeManager, nil, nil) req := httptest.NewRequest(http.MethodGet, "/irrelevant", nil) resp := httptest.NewRecorder() @@ -161,7 +167,7 @@ func TestConnectedStateAndSessionIdIsReturnedWhenIsConnected(t *testing.T) { SessionID: "My-super-session", } - connEndpoint := NewConnectionEndpoint(&fakeManager, nil) + connEndpoint := NewConnectionEndpoint(&fakeManager, nil, nil) req := httptest.NewRequest(http.MethodGet, "/irrelevant", nil) resp := httptest.NewRecorder() @@ -181,7 +187,7 @@ func TestConnectedStateAndSessionIdIsReturnedWhenIsConnected(t *testing.T) { func TestPutReturns400ErrorIfRequestBodyIsNotJson(t *testing.T) { fakeManager := fakeManager{} - connEndpoint := NewConnectionEndpoint(&fakeManager, nil) + connEndpoint := NewConnectionEndpoint(&fakeManager, nil, nil) req := httptest.NewRequest(http.MethodPut, "/irrelevant", strings.NewReader("a")) resp := httptest.NewRecorder() @@ -200,7 +206,7 @@ func TestPutReturns400ErrorIfRequestBodyIsNotJson(t *testing.T) { func TestPutReturns422ErrorIfRequestBodyIsMissingFieldValues(t *testing.T) { fakeManager := fakeManager{} - connEndpoint := NewConnectionEndpoint(&fakeManager, nil) + connEndpoint := NewConnectionEndpoint(&fakeManager, nil, nil) req := httptest.NewRequest(http.MethodPut, "/irrelevant", strings.NewReader("{}")) resp := httptest.NewRecorder() @@ -222,7 +228,7 @@ func TestPutReturns422ErrorIfRequestBodyIsMissingFieldValues(t *testing.T) { func TestPutWithValidBodyCreatesConnection(t *testing.T) { fakeManager := fakeManager{} - connEndpoint := NewConnectionEndpoint(&fakeManager, nil) + connEndpoint := NewConnectionEndpoint(&fakeManager, nil, nil) req := httptest.NewRequest( http.MethodPut, "/irrelevant", @@ -245,7 +251,7 @@ func TestPutWithValidBodyCreatesConnection(t *testing.T) { func TestDeleteCallsDisconnect(t *testing.T) { fakeManager := fakeManager{} - connEndpoint := NewConnectionEndpoint(&fakeManager, nil) + connEndpoint := NewConnectionEndpoint(&fakeManager, nil, nil) req := httptest.NewRequest(http.MethodDelete, "/irrelevant", nil) resp := httptest.NewRecorder() @@ -259,7 +265,7 @@ func TestDeleteCallsDisconnect(t *testing.T) { func TestGetIPEndpointSucceeds(t *testing.T) { manager := fakeManager{} ipResolver := ip.NewFakeResolver("123.123.123.123") - connEndpoint := NewConnectionEndpoint(&manager, ipResolver) + connEndpoint := NewConnectionEndpoint(&manager, ipResolver, nil) resp := httptest.NewRecorder() connEndpoint.GetIP(resp, nil, nil) @@ -277,7 +283,7 @@ func TestGetIPEndpointSucceeds(t *testing.T) { func TestGetIPEndpointReturnsErrorWhenIPDetectionFails(t *testing.T) { manager := fakeManager{} ipResolver := ip.NewFailingFakeResolver(errors.New("fake error")) - connEndpoint := NewConnectionEndpoint(&manager, ipResolver) + connEndpoint := NewConnectionEndpoint(&manager, ipResolver, nil) resp := httptest.NewRecorder() connEndpoint.GetIP(resp, nil, nil) @@ -291,3 +297,23 @@ func TestGetIPEndpointReturnsErrorWhenIPDetectionFails(t *testing.T) { resp.Body.String(), ) } + +func TestGetStatisticsEndpointReturnsStatistics(t *testing.T) { + statsKeeper := &bytescount.SessionStatsKeeper{} + stats := bytescount.SessionStats{BytesSent: 1, BytesReceived: 2} + statsKeeper.Save(stats) + + manager := fakeManager{} + connEndpoint := NewConnectionEndpoint(&manager, nil, statsKeeper) + + resp := httptest.NewRecorder() + connEndpoint.GetStatistics(resp, nil, nil) + assert.JSONEq( + t, + `{ + "bytesSent": 1, + "bytesReceived": 2 + }`, + resp.Body.String(), + ) +}