diff --git a/Gopkg.lock b/Gopkg.lock index 3900ed7a2f..6c95f81ff2 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -607,6 +607,9 @@ "github.com/ethereum/go-ethereum/p2p/nat", "github.com/ethereum/go-ethereum/params", "github.com/gofrs/uuid", + "github.com/huin/goupnp", + "github.com/huin/goupnp/httpu", + "github.com/huin/goupnp/ssdp", "github.com/jackpal/gateway", "github.com/julienschmidt/httprouter", "github.com/mdlayher/wireguardctrl", diff --git a/cmd/di.go b/cmd/di.go index 5f44b4a128..f9c0bb0fd4 100644 --- a/cmd/di.go +++ b/cmd/di.go @@ -50,6 +50,7 @@ import ( "github.com/mysteriumnetwork/node/metrics" "github.com/mysteriumnetwork/node/money" "github.com/mysteriumnetwork/node/nat" + "github.com/mysteriumnetwork/node/nat/event" "github.com/mysteriumnetwork/node/nat/mapping" "github.com/mysteriumnetwork/node/nat/traversal" "github.com/mysteriumnetwork/node/nat/traversal/config" @@ -95,14 +96,20 @@ type NatPinger interface { // NatEventTracker is responsible for tracking NAT events type NatEventTracker interface { - ConsumeNATEvent(event traversal.Event) - LastEvent() *traversal.Event - WaitForEvent() traversal.Event + ConsumeNATEvent(event event.Event) + LastEvent() *event.Event + WaitForEvent() event.Event } // NatEventSender is responsible for sending NAT events to metrics server type NatEventSender interface { - ConsumeNATEvent(event traversal.Event) + ConsumeNATEvent(event event.Event) +} + +// NATStatusTracker tracks status of NAT traversal by consuming NAT events +type NATStatusTracker interface { + Status() nat.Status + ConsumeNATEvent(event event.Event) } // Dependencies is DI container for top level components which is reused in several places @@ -140,9 +147,10 @@ type Dependencies struct { ServiceRegistry *service.Registry ServiceSessionStorage *session.StorageMemory - NATPinger NatPinger - NATTracker NatEventTracker - NATEventSender NatEventSender + NATPinger NatPinger + NATTracker NatEventTracker + NATEventSender NatEventSender + NATStatusTracker NATStatusTracker PortPool *port.Pool @@ -299,11 +307,15 @@ func (di *Dependencies) subscribeEventConsumers() error { } // NAT events - err = di.EventBus.Subscribe(traversal.EventTopic, di.NATEventSender.ConsumeNATEvent) + err = di.EventBus.Subscribe(event.Topic, di.NATEventSender.ConsumeNATEvent) + if err != nil { + return err + } + err = di.EventBus.Subscribe(event.Topic, di.NATTracker.ConsumeNATEvent) if err != nil { return err } - return di.EventBus.Subscribe(traversal.EventTopic, di.NATTracker.ConsumeNATEvent) + return di.EventBus.Subscribe(event.Topic, di.NATStatusTracker.ConsumeNATEvent) } func (di *Dependencies) bootstrapNodeComponents(nodeOptions node.Options) { @@ -344,6 +356,7 @@ func (di *Dependencies) bootstrapNodeComponents(nodeOptions node.Options) { tequilapi_endpoints.AddRoutesForServiceSessions(router, di.ServiceSessionStorage) tequilapi_endpoints.AddRoutesForPayout(router, di.IdentityManager, di.SignerFactory, di.MysteriumAPI) tequilapi_endpoints.AddRoutesForAccessPolicies(router, nodeOptions.AccessPolicyEndpointAddress) + tequilapi_endpoints.AddRoutesForNAT(router, di.NATStatusTracker.Status) identity_registry.AddIdentityRegistrationEndpoint(router, di.IdentityRegistration, di.IdentityRegistry) corsPolicy := tequilapi.NewMysteriumCorsPolicy() @@ -485,8 +498,7 @@ func (di *Dependencies) bootstrapMetrics(options node.Options) { } func (di *Dependencies) bootstrapNATComponents(options node.Options) { - di.NATTracker = traversal.NewEventsTracker() - di.NATEventSender = traversal.NewEventsSender(di.MetricsSender, di.IPResolver.GetPublicIP) + di.NATTracker = event.NewTracker() if options.ExperimentNATPunching { di.NATPinger = traversal.NewPingerFactory( di.NATTracker, @@ -499,4 +511,14 @@ func (di *Dependencies) bootstrapNATComponents(options node.Options) { } else { di.NATPinger = &traversal.NoopPinger{} } + + di.NATEventSender = event.NewSender(di.MetricsSender, di.IPResolver.GetPublicIP) + + var lastStageName string + if options.ExperimentNATPunching { + lastStageName = traversal.StageName + } else { + lastStageName = mapping.StageName + } + di.NATStatusTracker = nat.NewStatusTracker(lastStageName) } diff --git a/nat/traversal/events_sender.go b/nat/event/sender.go similarity index 69% rename from nat/traversal/events_sender.go rename to nat/event/sender.go index eec2267b0b..51a5294685 100644 --- a/nat/traversal/events_sender.go +++ b/nat/event/sender.go @@ -15,16 +15,16 @@ * along with this program. If not, see . */ -package traversal +package event import ( log "github.com/cihub/seelog" ) -const eventsSenderLogPrefix = "[traversal-events-sender] " +const senderLogPrefix = "[traversal-events-sender] " -// EventsSender allows subscribing to NAT events and sends them to server -type EventsSender struct { +// Sender allows subscribing to NAT events and sends them to server +type Sender struct { metricsSender metricsSender ipResolver ipResolver lastIp string @@ -38,16 +38,16 @@ type metricsSender interface { type ipResolver func() (string, error) -// NewEventsSender returns a new instance of events sender -func NewEventsSender(metricsSender metricsSender, ipResolver ipResolver) *EventsSender { - return &EventsSender{metricsSender: metricsSender, ipResolver: ipResolver, lastIp: ""} +// NewSender returns a new instance of events sender +func NewSender(metricsSender metricsSender, ipResolver ipResolver) *Sender { + return &Sender{metricsSender: metricsSender, ipResolver: ipResolver, lastIp: ""} } // ConsumeNATEvent sends received event to server -func (es *EventsSender) ConsumeNATEvent(event Event) { +func (es *Sender) ConsumeNATEvent(event Event) { publicIP, err := es.ipResolver() if err != nil { - log.Warnf(eventsSenderLogPrefix, "resolving public ip failed: ", err) + log.Warnf(senderLogPrefix, "resolving public ip failed: ", err) return } if publicIP == es.lastIp && es.matchesLastEvent(event) { @@ -56,14 +56,14 @@ func (es *EventsSender) ConsumeNATEvent(event Event) { err = es.sendNATEvent(event) if err != nil { - log.Warnf(eventsSenderLogPrefix, "sending event failed: ", err) + log.Warnf(senderLogPrefix, "sending event failed: ", err) } es.lastIp = publicIP es.lastEvent = &event } -func (es *EventsSender) sendNATEvent(event Event) error { +func (es *Sender) sendNATEvent(event Event) error { if event.Successful { return es.metricsSender.SendNATMappingSuccessEvent(event.Stage) } @@ -71,7 +71,7 @@ func (es *EventsSender) sendNATEvent(event Event) error { return es.metricsSender.SendNATMappingFailEvent(event.Stage, event.Error) } -func (es *EventsSender) matchesLastEvent(event Event) bool { +func (es *Sender) matchesLastEvent(event Event) bool { if es.lastEvent == nil { return false } diff --git a/nat/traversal/events_sender_test.go b/nat/event/sender_test.go similarity index 89% rename from nat/traversal/events_sender_test.go rename to nat/event/sender_test.go index 9b819b79dc..913545dc15 100644 --- a/nat/traversal/events_sender_test.go +++ b/nat/event/sender_test.go @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package traversal +package event import ( "errors" @@ -60,7 +60,7 @@ func (resolver *mockIPResolver) GetPublicIP() (string, error) { func Test_EventsSender_ConsumeNATEvent_SendsSuccessEvent(t *testing.T) { mockMetricsSender := buildMockMetricsSender(nil) mockIPResolver := mockIPResolver{mockIp: "1st ip"} - sender := NewEventsSender(mockMetricsSender, mockIPResolver.GetPublicIP) + sender := NewSender(mockMetricsSender, mockIPResolver.GetPublicIP) sender.ConsumeNATEvent(Event{Stage: "hole_punching", Successful: true}) @@ -71,7 +71,7 @@ func Test_EventsSender_ConsumeNATEvent_SendsSuccessEvent(t *testing.T) { func Test_EventsSender_ConsumeNATEvent_WithSameIp_DoesNotSendSuccessEventAgain(t *testing.T) { mockMetricsSender := buildMockMetricsSender(nil) mockIPResolver := mockIPResolver{mockIp: "1st ip"} - sender := NewEventsSender(mockMetricsSender, mockIPResolver.GetPublicIP) + sender := NewSender(mockMetricsSender, mockIPResolver.GetPublicIP) sender.ConsumeNATEvent(Event{Successful: true}) @@ -84,7 +84,7 @@ func Test_EventsSender_ConsumeNATEvent_WithSameIp_DoesNotSendSuccessEventAgain(t func Test_EventsSender_ConsumeNATEvent_WithDifferentIP_SendsSuccessEventAgain(t *testing.T) { mockMetricsSender := buildMockMetricsSender(nil) mockIPResolver := &mockIPResolver{mockIp: "1st ip"} - sender := NewEventsSender(mockMetricsSender, mockIPResolver.GetPublicIP) + sender := NewSender(mockMetricsSender, mockIPResolver.GetPublicIP) sender.ConsumeNATEvent(Event{Successful: true}) @@ -98,7 +98,7 @@ func Test_EventsSender_ConsumeNATEvent_WithDifferentIP_SendsSuccessEventAgain(t func Test_EventsSender_ConsumeNATEvent_WhenIPResolverFails_DoesNotSendEvent(t *testing.T) { mockMetricsSender := buildMockMetricsSender(nil) mockIPResolver := &mockIPResolver{mockErr: errors.New("mock error")} - sender := NewEventsSender(mockMetricsSender, mockIPResolver.GetPublicIP) + sender := NewSender(mockMetricsSender, mockIPResolver.GetPublicIP) sender.ConsumeNATEvent(Event{Successful: true}) @@ -108,7 +108,7 @@ func Test_EventsSender_ConsumeNATEvent_WhenIPResolverFails_DoesNotSendEvent(t *t func Test_EventsSender_ConsumeNATEvent_SendsFailureEvent(t *testing.T) { mockMetricsSender := buildMockMetricsSender(nil) mockIPResolver := mockIPResolver{mockIp: "1st ip"} - sender := NewEventsSender(mockMetricsSender, mockIPResolver.GetPublicIP) + sender := NewSender(mockMetricsSender, mockIPResolver.GetPublicIP) testErr := errors.New("test error") sender.ConsumeNATEvent(Event{Stage: "hole_punching", Successful: false, Error: testErr}) @@ -120,7 +120,7 @@ func Test_EventsSender_ConsumeNATEvent_SendsFailureEvent(t *testing.T) { func Test_EventsSender_ConsumeNATEvent_WithFailuresOfDifferentStages_SendsBothEvents(t *testing.T) { mockMetricsSender := buildMockMetricsSender(nil) mockIPResolver := &mockIPResolver{mockIp: "1st ip"} - sender := NewEventsSender(mockMetricsSender, mockIPResolver.GetPublicIP) + sender := NewSender(mockMetricsSender, mockIPResolver.GetPublicIP) testErr1 := errors.New("test error 1") sender.ConsumeNATEvent(Event{Successful: false, Error: testErr1, Stage: "test 1"}) @@ -133,7 +133,7 @@ func Test_EventsSender_ConsumeNATEvent_WithFailuresOfDifferentStages_SendsBothEv func Test_EventsSender_ConsumeNATEvent_WithSuccessAndFailureOnSameIp_SendsBothEvents(t *testing.T) { mockMetricsSender := buildMockMetricsSender(nil) mockIPResolver := &mockIPResolver{mockIp: "1st ip"} - sender := NewEventsSender(mockMetricsSender, mockIPResolver.GetPublicIP) + sender := NewSender(mockMetricsSender, mockIPResolver.GetPublicIP) sender.ConsumeNATEvent(Event{Successful: true}) testErr := errors.New("test error") diff --git a/nat/traversal/events_tracker.go b/nat/event/tracker.go similarity index 74% rename from nat/traversal/events_tracker.go rename to nat/event/tracker.go index 8c9d153cb0..a9504a1acd 100644 --- a/nat/traversal/events_tracker.go +++ b/nat/event/tracker.go @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package traversal +package event import ( "time" @@ -23,19 +23,19 @@ import ( log "github.com/cihub/seelog" ) -// EventTopic the topic that traversal events are published on -const EventTopic = "Traversal" +// Topic the topic that traversal events are published on +const Topic = "Traversal" const eventsTrackerLogPrefix = "[traversal-events-tracker] " -// EventsTracker is able to track NAT traversal events -type EventsTracker struct { +// Tracker is able to track NAT traversal events +type Tracker struct { lastEvent *Event eventChan chan Event } -// BuildSuccessEvent returns new event for successful NAT traversal -func BuildSuccessEvent(stage string) Event { +// BuildSuccessfulEvent returns new event for successful NAT traversal +func BuildSuccessfulEvent(stage string) Event { return Event{Stage: stage, Successful: true} } @@ -44,13 +44,13 @@ func BuildFailureEvent(stage string, err error) Event { return Event{Stage: stage, Successful: false, Error: err} } -// NewEventsTracker returns a new instance of event tracker -func NewEventsTracker() *EventsTracker { - return &EventsTracker{eventChan: make(chan Event, 1)} +// NewTracker returns a new instance of event tracker +func NewTracker() *Tracker { + return &Tracker{eventChan: make(chan Event, 1)} } // ConsumeNATEvent consumes a NAT event -func (et *EventsTracker) ConsumeNATEvent(event Event) { +func (et *Tracker) ConsumeNATEvent(event Event) { log.Info(eventsTrackerLogPrefix, "got NAT event: ", event) et.lastEvent = &event @@ -61,13 +61,13 @@ func (et *EventsTracker) ConsumeNATEvent(event Event) { } // LastEvent returns the last known event and boolean flag, indicating if such event exists -func (et *EventsTracker) LastEvent() *Event { +func (et *Tracker) LastEvent() *Event { log.Info(eventsTrackerLogPrefix, "getting last NAT event: ", et.lastEvent) return et.lastEvent } // WaitForEvent waits for event to occur -func (et *EventsTracker) WaitForEvent() Event { +func (et *Tracker) WaitForEvent() Event { if et.lastEvent != nil { return *et.lastEvent } diff --git a/nat/mapping/port_mapping.go b/nat/mapping/port_mapping.go index e0a7e7c49f..d4f2ceac34 100644 --- a/nat/mapping/port_mapping.go +++ b/nat/mapping/port_mapping.go @@ -22,7 +22,7 @@ import ( log "github.com/cihub/seelog" portmap "github.com/ethereum/go-ethereum/p2p/nat" - "github.com/mysteriumnetwork/node/nat/traversal" + "github.com/mysteriumnetwork/node/nat/event" ) const logPrefix = "[port mapping] " @@ -88,11 +88,11 @@ func addMapping(m portmap.Interface, protocol string, extPort, intPort int, name log.Debugf("%s Couldn't add port mapping for port %d: %v, retrying with permanent lease", logPrefix, extPort, err) if err := m.AddMapping(protocol, extPort, intPort, name, 0); err != nil { // some gateways support only permanent leases - publisher.Publish(traversal.EventTopic, traversal.BuildFailureEvent(StageName, err)) + publisher.Publish(event.Topic, event.BuildFailureEvent(StageName, err)) log.Debugf("%s Couldn't add port mapping for port %d: %v", logPrefix, extPort, err) return } } - publisher.Publish(traversal.EventTopic, traversal.BuildSuccessEvent(StageName)) + publisher.Publish(event.Topic, event.BuildSuccessfulEvent(StageName)) log.Info(logPrefix, "Mapped network port: ", extPort) } diff --git a/nat/status_tracker.go b/nat/status_tracker.go new file mode 100644 index 0000000000..35e5f890c2 --- /dev/null +++ b/nat/status_tracker.go @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2019 The "MysteriumNetwork/node" Authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package nat + +import ( + "github.com/mysteriumnetwork/node/nat/event" +) + +// StatusTracker keeps status of NAT traversal by consuming NAT events - whether if finished and was it successful. +// It can finish either by successful event from any stage, or by a failure of the last stage. +type StatusTracker struct { + lastStageName string + status Status +} + +const ( + statusNotFinished = "not_finished" + statusSuccessful = "successful" + statusFailure = "failure" +) + +// Status represents NAT traversal status (either "not_finished", "successful" or "failure") and an optional error. +type Status struct { + Status string + Error error +} + +// Status returns NAT traversal status +func (t *StatusTracker) Status() Status { + return t.status +} + +// ConsumeNATEvent processes NAT event to determine NAT traversal status +func (t *StatusTracker) ConsumeNATEvent(event event.Event) { + if event.Stage == t.lastStageName && event.Successful == false { + t.status = Status{Status: statusFailure, Error: event.Error} + return + } + + if event.Successful { + t.status = Status{Status: statusSuccessful} + return + } +} + +// NewStatusTracker returns new instance of status tracker +func NewStatusTracker(lastStageName string) *StatusTracker { + return &StatusTracker{ + lastStageName: lastStageName, + status: Status{Status: statusNotFinished}, + } +} diff --git a/nat/status_tracker_test.go b/nat/status_tracker_test.go new file mode 100644 index 0000000000..4ac9600490 --- /dev/null +++ b/nat/status_tracker_test.go @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2019 The "MysteriumNetwork/node" Authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package nat + +import ( + "testing" + + "github.com/mysteriumnetwork/node/nat/event" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func Test_StatusTracker_Status_ReturnsNotFinishedInitially(t *testing.T) { + tracker := NewStatusTracker("last stage") + status := tracker.Status() + + assert.Equal(t, "not_finished", status.Status) + assert.Nil(t, status.Error) +} + +func Test_StatusTracker_Status_ReturnsSuccessful_WithSuccessfulEvent(t *testing.T) { + tracker := NewStatusTracker("last stage") + tracker.ConsumeNATEvent(event.Event{Successful: true, Stage: "any stage"}) + status := tracker.Status() + + assert.Equal(t, "successful", status.Status) + assert.Nil(t, status.Error) +} + +func Test_StatusTracker_Status_ReturnsFailure_WithHolepunchingFailureEvent(t *testing.T) { + tracker := NewStatusTracker("last stage") + tracker.ConsumeNATEvent(event.Event{Successful: false, Stage: "last stage", Error: errors.New("test error")}) + status := tracker.Status() + + assert.Equal(t, "failure", status.Status) + assert.EqualError(t, status.Error, "test error") +} + +func Test_StatusTracker_Status_ReturnsNotFinished_WithPortMappingFailureEvent(t *testing.T) { + tracker := NewStatusTracker("last stage") + tracker.ConsumeNATEvent(event.Event{Successful: false, Stage: "first stage"}) + status := tracker.Status() + + assert.Equal(t, "not_finished", status.Status) + assert.Nil(t, status.Error) +} diff --git a/nat/traversal/pinger.go b/nat/traversal/pinger.go index 8fe341e95a..dd057f8763 100644 --- a/nat/traversal/pinger.go +++ b/nat/traversal/pinger.go @@ -26,15 +26,17 @@ import ( log "github.com/cihub/seelog" "github.com/mysteriumnetwork/node/core/port" + "github.com/mysteriumnetwork/node/nat/event" "github.com/mysteriumnetwork/node/services" "github.com/pkg/errors" "golang.org/x/net/ipv4" ) +// StageName represents hole-punching stage of NAT traversal +const StageName = "hole_punching" const prefix = "[NATPinger] " const pingInterval = 200 const pingTimeout = 10000 -const stageName = "hole_punching" // Pinger represents NAT pinger structure type Pinger struct { @@ -53,7 +55,7 @@ type Pinger struct { // NatEventWaiter is responsible for waiting for nat events type NatEventWaiter interface { - WaitForEvent() Event + WaitForEvent() event.Event } // ConfigParser is able to parse a config from given raw json @@ -225,11 +227,11 @@ func (p *Pinger) ping(conn *net.UDPConn) error { err := p.sendPingRequest(conn, n) if err != nil { - p.eventPublisher.Publish(EventTopic, BuildFailureEvent(stageName, err)) + p.eventPublisher.Publish(event.Topic, event.BuildFailureEvent(StageName, err)) return err } - p.eventPublisher.Publish(EventTopic, BuildSuccessEvent(stageName)) + p.eventPublisher.Publish(event.Topic, event.BuildSuccessfulEvent(StageName)) n++ } diff --git a/services/openvpn/service/manager.go b/services/openvpn/service/manager.go index 7d45588c41..11ffde9ef7 100644 --- a/services/openvpn/service/manager.go +++ b/services/openvpn/service/manager.go @@ -28,8 +28,8 @@ import ( "github.com/mysteriumnetwork/node/identity" "github.com/mysteriumnetwork/node/market" "github.com/mysteriumnetwork/node/nat" + "github.com/mysteriumnetwork/node/nat/event" "github.com/mysteriumnetwork/node/nat/mapping" - "github.com/mysteriumnetwork/node/nat/traversal" "github.com/mysteriumnetwork/node/services" openvpn_service "github.com/mysteriumnetwork/node/services/openvpn" "github.com/mysteriumnetwork/node/session" @@ -58,7 +58,7 @@ type NATPinger interface { // NATEventGetter allows us to fetch the last known NAT event type NATEventGetter interface { - LastEvent() *traversal.Event + LastEvent() *event.Event } // Manager represents entrypoint for Openvpn service with top level components diff --git a/session/manager.go b/session/manager.go index 9c240102f8..cc83058c5b 100644 --- a/session/manager.go +++ b/session/manager.go @@ -26,6 +26,7 @@ import ( log "github.com/cihub/seelog" "github.com/mysteriumnetwork/node/identity" "github.com/mysteriumnetwork/node/market" + "github.com/mysteriumnetwork/node/nat/event" "github.com/mysteriumnetwork/node/nat/traversal" ) @@ -74,7 +75,7 @@ type BalanceTrackerFactory func(consumer, provider, issuer identity.Identity) (B // NATEventGetter lets us access the last known traversal event type NATEventGetter interface { - LastEvent() *traversal.Event + LastEvent() *event.Event } // NewManager returns new session Manager diff --git a/session/manager_test.go b/session/manager_test.go index 3ac560968a..d2c4003d1f 100644 --- a/session/manager_test.go +++ b/session/manager_test.go @@ -22,6 +22,7 @@ import ( "github.com/mysteriumnetwork/node/identity" "github.com/mysteriumnetwork/node/market" + "github.com/mysteriumnetwork/node/nat/event" "github.com/mysteriumnetwork/node/nat/traversal" "github.com/stretchr/testify/assert" ) @@ -98,6 +99,6 @@ func TestManager_Create_RejectsUnknownProposal(t *testing.T) { type MockNatEventTracker struct { } -func (mnet *MockNatEventTracker) LastEvent() *traversal.Event { - return &traversal.Event{} +func (mnet *MockNatEventTracker) LastEvent() *event.Event { + return &event.Event{} } diff --git a/tequilapi/endpoints/nat.go b/tequilapi/endpoints/nat.go new file mode 100644 index 0000000000..6ecfede5e9 --- /dev/null +++ b/tequilapi/endpoints/nat.go @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2019 The "MysteriumNetwork/node" Authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package endpoints + +import ( + "net/http" + + "github.com/julienschmidt/httprouter" + "github.com/mysteriumnetwork/node/nat" + "github.com/mysteriumnetwork/node/nat/event" + "github.com/mysteriumnetwork/node/tequilapi/utils" +) + +const ( + statusNotFinished = "not_finished" + statusSuccessful = "successful" + statusFailure = "failure" +) + +// NATStatusDTO gives information about NAT traversal success or failure +// swagger:model NATStatusDTO +type NATStatusDTO struct { + Status string `json:"status"` + Error string `json:"error,omitempty"` +} + +type natStatusProvider func() nat.Status + +// NATEvents allows retrieving last traversal event +type NATEvents interface { + LastEvent() *event.Event +} + +// NATEndpoint struct represents endpoints about NAT traversal +type NATEndpoint struct { + statusProvider natStatusProvider +} + +// NewNATEndpoint creates and returns nat endpoint +func NewNATEndpoint(statusProvider natStatusProvider) *NATEndpoint { + return &NATEndpoint{ + statusProvider: statusProvider, + } +} + +// NATStatus provides NAT configuration info +// swagger:operation GET /nat/status NAT NATStatusDTO +// --- +// summary: Shows NAT status +// description: NAT status returns the last known NAT traversal status +// responses: +// 200: +// description: NAT status ("not_finished"/"successful"/"failed") and optionally error if status is "failed" +// schema: +// "$ref": "#/definitions/NATStatusDTO" +func (ne *NATEndpoint) NATStatus(resp http.ResponseWriter, _ *http.Request, _ httprouter.Params) { + status := ne.statusProvider() + statusResponse := toNATStatusResponse(status) + utils.WriteAsJSON(statusResponse, resp) +} + +// AddRoutesForNAT adds nat routes to given router +func AddRoutesForNAT(router *httprouter.Router, statusProvider natStatusProvider) { + natEndpoint := NewNATEndpoint(statusProvider) + + router.GET("/nat/status", natEndpoint.NATStatus) +} + +func toNATStatusResponse(status nat.Status) NATStatusDTO { + if status.Error == nil { + return NATStatusDTO{Status: status.Status} + } + error := status.Error.Error() + return NATStatusDTO{Status: status.Status, Error: error} +} diff --git a/tequilapi/endpoints/nat_test.go b/tequilapi/endpoints/nat_test.go new file mode 100644 index 0000000000..8cc1d4a131 --- /dev/null +++ b/tequilapi/endpoints/nat_test.go @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2019 The "MysteriumNetwork/node" Authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package endpoints + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/julienschmidt/httprouter" + "github.com/mysteriumnetwork/node/nat" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +type mockNATStatusProvider struct { + mockStatus nat.Status +} + +func (mockProvider *mockNATStatusProvider) Status() nat.Status { + return mockProvider.mockStatus +} + +func Test_NATStatus_ReturnsStatusSuccessful_WithSuccessfulEvent(t *testing.T) { + testResponse( + t, + nat.Status{Status: statusSuccessful}, + `{ + "status": "successful" + }`, + ) +} + +func Test_NATStatus_ReturnsStatusFailureAndError_WithFailureEvent(t *testing.T) { + testResponse( + t, + nat.Status{Status: statusFailure, Error: errors.New("mock error")}, + `{ + "status": "failure", + "error": "mock error" + }`, + ) +} + +func Test_NATStatus_ReturnsStatusNotFinished_WhenEventIsNotAvailable(t *testing.T) { + testResponse( + t, + nat.Status{Status: statusNotFinished}, + `{ + "status": "not_finished" + }`, + ) +} + +func testResponse(t *testing.T, mockStatus nat.Status, expectedJson string) { + provider := mockNATStatusProvider{mockStatus: mockStatus} + + req, err := http.NewRequest(http.MethodGet, "/nat/status", nil) + assert.Nil(t, err) + resp := httptest.NewRecorder() + router := httprouter.New() + AddRoutesForNAT(router, provider.Status) + + router.ServeHTTP(resp, req) + + assert.Equal(t, http.StatusOK, resp.Code) + assert.JSONEq(t, expectedJson, resp.Body.String()) +}