github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/tequilapi/endpoints/connection.go (about)

     1  /*
     2   * Copyright (C) 2017 The "MysteriumNetwork/node" Authors.
     3   *
     4   * This program is free software: you can redistribute it and/or modify
     5   * it under the terms of the GNU General Public License as published by
     6   * the Free Software Foundation, either version 3 of the License, or
     7   * (at your option) any later version.
     8   *
     9   * This program is distributed in the hope that it will be useful,
    10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12   * GNU General Public License for more details.
    13   *
    14   * You should have received a copy of the GNU General Public License
    15   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16   */
    17  
    18  package endpoints
    19  
    20  import (
    21  	"encoding/json"
    22  	"fmt"
    23  	"net/http"
    24  	"strconv"
    25  
    26  	"github.com/ethereum/go-ethereum/common"
    27  	"github.com/gin-gonic/gin"
    28  	"github.com/rs/zerolog/log"
    29  
    30  	"github.com/mysteriumnetwork/go-rest/apierror"
    31  	"github.com/mysteriumnetwork/node/config"
    32  	"github.com/mysteriumnetwork/node/core/connection"
    33  	"github.com/mysteriumnetwork/node/core/discovery/proposal"
    34  	"github.com/mysteriumnetwork/node/core/quality"
    35  	"github.com/mysteriumnetwork/node/eventbus"
    36  	"github.com/mysteriumnetwork/node/identity"
    37  	"github.com/mysteriumnetwork/node/identity/registry"
    38  	"github.com/mysteriumnetwork/node/market"
    39  	"github.com/mysteriumnetwork/node/tequilapi/contract"
    40  	"github.com/mysteriumnetwork/node/tequilapi/utils"
    41  )
    42  
    43  const (
    44  	// statusConnectCancelled indicates that connect request was cancelled by user. Since there is no such concept in REST
    45  	// operations, custom client error code is defined. Maybe in later times a better idea will come how to handle these situations
    46  	statusConnectCancelled = 499
    47  )
    48  
    49  // ProposalGetter defines interface to fetch currently active service proposal by id
    50  type ProposalGetter interface {
    51  	GetProposal(id market.ProposalID) (*market.ServiceProposal, error)
    52  }
    53  
    54  type identityRegistry interface {
    55  	GetRegistrationStatus(int64, identity.Identity) (registry.RegistrationStatus, error)
    56  }
    57  
    58  // ConnectionEndpoint struct represents /connection resource and it's subresources
    59  type ConnectionEndpoint struct {
    60  	manager       connection.MultiManager
    61  	publisher     eventbus.Publisher
    62  	stateProvider stateProvider
    63  	// TODO connection should use concrete proposal from connection params and avoid going to marketplace
    64  	proposalRepository proposalRepository
    65  	identityRegistry   identityRegistry
    66  	addressProvider    addressProvider
    67  }
    68  
    69  // NewConnectionEndpoint creates and returns connection endpoint
    70  func NewConnectionEndpoint(manager connection.MultiManager, stateProvider stateProvider, proposalRepository proposalRepository, identityRegistry identityRegistry, publisher eventbus.Publisher, addressProvider addressProvider) *ConnectionEndpoint {
    71  	return &ConnectionEndpoint{
    72  		manager:            manager,
    73  		publisher:          publisher,
    74  		stateProvider:      stateProvider,
    75  		proposalRepository: proposalRepository,
    76  		identityRegistry:   identityRegistry,
    77  		addressProvider:    addressProvider,
    78  	}
    79  }
    80  
    81  // Status returns status of connection
    82  // swagger:operation GET /connection Connection connectionStatus
    83  //
    84  //	---
    85  //	summary: Returns connection status
    86  //	description: Returns status of current connection
    87  //	responses:
    88  //	  200:
    89  //	    description: Status
    90  //	    schema:
    91  //	      "$ref": "#/definitions/ConnectionInfoDTO"
    92  //	  400:
    93  //	    description: Failed to parse or request validation failed
    94  //	    schema:
    95  //	      "$ref": "#/definitions/APIError"
    96  //	  500:
    97  //	    description: Internal server error
    98  //	    schema:
    99  //	      "$ref": "#/definitions/APIError"
   100  func (ce *ConnectionEndpoint) Status(c *gin.Context) {
   101  	n := 0
   102  	id := c.Query("id")
   103  	if len(id) > 0 {
   104  		var err error
   105  		n, err = strconv.Atoi(id)
   106  		if err != nil {
   107  			c.Error(apierror.ParseFailed())
   108  			return
   109  		}
   110  	}
   111  	status := ce.manager.Status(n)
   112  	statusResponse := contract.NewConnectionInfoDTO(status)
   113  	utils.WriteAsJSON(statusResponse, c.Writer)
   114  }
   115  
   116  // Create starts new connection
   117  // swagger:operation PUT /connection Connection connectionCreate
   118  //
   119  //	---
   120  //	summary: Starts new connection
   121  //	description: Consumer opens connection to provider
   122  //	parameters:
   123  //	  - in: body
   124  //	    name: body
   125  //	    description: Parameters in body (consumer_id, provider_id, service_type) required for creating new connection
   126  //	    schema:
   127  //	      $ref: "#/definitions/ConnectionCreateRequestDTO"
   128  //	responses:
   129  //	  201:
   130  //	    description: Connection started
   131  //	    schema:
   132  //	      "$ref": "#/definitions/ConnectionInfoDTO"
   133  //	  400:
   134  //	    description: Failed to parse or request validation failed
   135  //	    schema:
   136  //	      "$ref": "#/definitions/APIError"
   137  //	  422:
   138  //	    description: Unable to process the request at this point
   139  //	    schema:
   140  //	      "$ref": "#/definitions/APIError"
   141  //	  500:
   142  //	    description: Internal server error
   143  //	    schema:
   144  //	      "$ref": "#/definitions/APIError"
   145  func (ce *ConnectionEndpoint) Create(c *gin.Context) {
   146  	hermes, err := ce.addressProvider.GetActiveHermes(config.GetInt64(config.FlagChainID))
   147  	if err != nil {
   148  		c.Error(apierror.Internal("Failed to get active hermes", contract.ErrCodeActiveHermes))
   149  		return
   150  	}
   151  
   152  	cr, err := toConnectionRequest(c.Request, hermes.Hex())
   153  	if err != nil {
   154  		ce.publisher.Publish(quality.AppTopicConnectionEvents, (&contract.ConnectionCreateRequest{}).Event(quality.StagePraseRequest, err.Error()))
   155  		c.Error(apierror.ParseFailed())
   156  		return
   157  	}
   158  
   159  	if err := cr.Validate(); err != nil {
   160  		ce.publisher.Publish(quality.AppTopicConnectionEvents, cr.Event(quality.StageValidateRequest, err.Detail()))
   161  		c.Error(err)
   162  		return
   163  	}
   164  
   165  	consumerID := identity.FromAddress(cr.ConsumerID)
   166  	status, err := ce.identityRegistry.GetRegistrationStatus(config.GetInt64(config.FlagChainID), consumerID)
   167  	if err != nil {
   168  		ce.publisher.Publish(quality.AppTopicConnectionEvents, cr.Event(quality.StageRegistrationGetStatus, err.Error()))
   169  		log.Error().Err(err).Stack().Msg("Could not check registration status")
   170  		c.Error(apierror.Internal("Failed to check ID registration status: "+err.Error(), contract.ErrCodeIDRegistrationCheck))
   171  		return
   172  	}
   173  
   174  	switch status {
   175  	case registry.Unregistered, registry.RegistrationError, registry.Unknown:
   176  		ce.publisher.Publish(quality.AppTopicConnectionEvents, cr.Event(quality.StageRegistrationUnregistered, ""))
   177  		log.Error().Msgf("Identity %q is not registered, aborting...", cr.ConsumerID)
   178  		c.Error(apierror.Unprocessable(fmt.Sprintf("Identity %q is not registered. Please register the identity first", cr.ConsumerID), contract.ErrCodeIDNotRegistered))
   179  		return
   180  	case registry.InProgress:
   181  		ce.publisher.Publish(quality.AppTopicConnectionEvents, cr.Event(quality.StageRegistrationInProgress, ""))
   182  		log.Info().Msgf("identity %q registration is in progress, continuing...", cr.ConsumerID)
   183  	case registry.Registered:
   184  		ce.publisher.Publish(quality.AppTopicConnectionEvents, cr.Event(quality.StageRegistrationRegistered, ""))
   185  		log.Info().Msgf("identity %q is registered, continuing...", cr.ConsumerID)
   186  	default:
   187  		ce.publisher.Publish(quality.AppTopicConnectionEvents, cr.Event(quality.StageRegistrationUnknown, ""))
   188  		log.Error().Msgf("identity %q has unknown status, aborting...", cr.ConsumerID)
   189  		c.Error(apierror.Unprocessable(fmt.Sprintf("Identity %q has unknown status. Aborting", cr.ConsumerID), contract.ErrCodeIDStatusUnknown))
   190  		return
   191  	}
   192  
   193  	if len(cr.ProviderID) > 0 {
   194  		cr.Filter.Providers = append(cr.Filter.Providers, cr.ProviderID)
   195  	}
   196  
   197  	f := &proposal.Filter{
   198  		ServiceType:             cr.ServiceType,
   199  		LocationCountry:         cr.Filter.CountryCode,
   200  		ProviderIDs:             cr.Filter.Providers,
   201  		IPType:                  cr.Filter.IPType,
   202  		IncludeMonitoringFailed: cr.Filter.IncludeMonitoringFailed,
   203  		AccessPolicy:            "all",
   204  	}
   205  	proposalLookup := connection.FilteredProposals(f, cr.Filter.SortBy, ce.proposalRepository)
   206  
   207  	err = ce.manager.Connect(consumerID, common.HexToAddress(cr.HermesID), proposalLookup, getConnectOptions(cr))
   208  	if err != nil {
   209  		switch err {
   210  		case connection.ErrAlreadyExists:
   211  			ce.publisher.Publish(quality.AppTopicConnectionEvents, cr.Event(quality.StageConnectionAlreadyExists, err.Error()))
   212  			c.Error(apierror.Unprocessable("Connection already exists", contract.ErrCodeConnectionAlreadyExists))
   213  		case connection.ErrConnectionCancelled:
   214  			ce.publisher.Publish(quality.AppTopicConnectionEvents, cr.Event(quality.StageConnectionCanceled, err.Error()))
   215  			c.Error(apierror.Unprocessable("Connection cancelled", contract.ErrCodeConnectionCancelled))
   216  		default:
   217  			ce.publisher.Publish(quality.AppTopicConnectionEvents, cr.Event(quality.StageConnectionUnknownError, err.Error()))
   218  			log.Error().Err(err).Msg("Failed to connect")
   219  			c.Error(apierror.Internal("Failed to connect: "+err.Error(), contract.ErrCodeConnect))
   220  		}
   221  		return
   222  	}
   223  
   224  	ce.publisher.Publish(quality.AppTopicConnectionEvents, cr.Event(quality.StageConnectionOK, ""))
   225  	c.Status(http.StatusCreated)
   226  
   227  	statusResp := ce.manager.Status(cr.ConnectOptions.ProxyPort)
   228  	statusResponse := contract.NewConnectionInfoDTO(statusResp)
   229  	utils.WriteAsJSON(statusResponse, c.Writer)
   230  }
   231  
   232  // Kill stops connection
   233  // swagger:operation DELETE /connection Connection connectionCancel
   234  //
   235  //	---
   236  //	summary: Stops connection
   237  //	description: Stops current connection
   238  //	responses:
   239  //	  202:
   240  //	    description: Connection stopped
   241  //	  400:
   242  //	    description: Failed to parse or request validation failed
   243  //	    schema:
   244  //	      "$ref": "#/definitions/APIError"
   245  //	  422:
   246  //	    description: Unable to process the request at this point (e.g. no active connection exists)
   247  //	    schema:
   248  //	      "$ref": "#/definitions/APIError"
   249  //	  500:
   250  //	    description: Internal server error
   251  //	    schema:
   252  //	      "$ref": "#/definitions/APIError"
   253  func (ce *ConnectionEndpoint) Kill(c *gin.Context) {
   254  	n := 0
   255  	id := c.Query("id")
   256  	if len(id) > 0 {
   257  		var err error
   258  		n, err = strconv.Atoi(id)
   259  		if err != nil {
   260  			c.Error(apierror.ParseFailed())
   261  			return
   262  		}
   263  	}
   264  
   265  	err := ce.manager.Disconnect(n)
   266  	if err != nil {
   267  		switch err {
   268  		case connection.ErrNoConnection:
   269  			c.Error(apierror.Unprocessable("No connection exists", contract.ErrCodeNoConnectionExists))
   270  		default:
   271  			c.Error(apierror.Internal("Could not disconnect: "+err.Error(), contract.ErrCodeDisconnect))
   272  		}
   273  		return
   274  	}
   275  	c.Status(http.StatusAccepted)
   276  }
   277  
   278  // GetStatistics returns statistics about current connection
   279  // swagger:operation GET /connection/statistics Connection connectionStatistics
   280  //
   281  //	---
   282  //	summary: Returns connection statistics
   283  //	description: Returns statistics about current connection
   284  //	responses:
   285  //	  200:
   286  //	    description: Connection statistics
   287  //	    schema:
   288  //	      "$ref": "#/definitions/ConnectionStatisticsDTO"
   289  func (ce *ConnectionEndpoint) GetStatistics(c *gin.Context) {
   290  	id := c.Query("id")
   291  	conn := ce.stateProvider.GetConnection(id)
   292  
   293  	response := contract.NewConnectionStatisticsDTO(conn.Session, conn.Statistics, conn.Throughput, conn.Invoice)
   294  	utils.WriteAsJSON(response, c.Writer)
   295  }
   296  
   297  // GetTraffic returns traffic information about requested connection
   298  // swagger:operation GET /connection/traffic Connection connectionTraffic
   299  //
   300  //	---
   301  //	summary: Returns connection traffic information
   302  //	description: Returns traffic information about requested connection
   303  //	responses:
   304  //	  200:
   305  //	    description: Connection traffic
   306  //	    schema:
   307  //	      "$ref": "#/definitions/ConnectionTrafficDTO"
   308  //	  400:
   309  //	    description: Failed to parse or request validation failed
   310  //	    schema:
   311  //	      "$ref": "#/definitions/APIError"
   312  func (ce *ConnectionEndpoint) GetTraffic(c *gin.Context) {
   313  	n := 0
   314  	id := c.Query("id")
   315  	if len(id) > 0 {
   316  		var err error
   317  		n, err = strconv.Atoi(id)
   318  		if err != nil {
   319  			c.Error(apierror.ParseFailed())
   320  			return
   321  		}
   322  	}
   323  
   324  	traffic := ce.manager.Stats(n)
   325  
   326  	response := contract.ConnectionTrafficDTO{
   327  		BytesSent:     traffic.BytesSent,
   328  		BytesReceived: traffic.BytesReceived,
   329  	}
   330  	utils.WriteAsJSON(response, c.Writer)
   331  }
   332  
   333  type proposalRepository interface {
   334  	Proposal(id market.ProposalID) (*proposal.PricedServiceProposal, error)
   335  	Proposals(filter *proposal.Filter) ([]proposal.PricedServiceProposal, error)
   336  	Countries(filter *proposal.Filter) (map[string]int, error)
   337  	EnrichProposalWithPrice(in market.ServiceProposal) (proposal.PricedServiceProposal, error)
   338  }
   339  
   340  // AddRoutesForConnection adds connections routes to given router
   341  func AddRoutesForConnection(
   342  	manager connection.MultiManager,
   343  	stateProvider stateProvider,
   344  	proposalRepository proposalRepository,
   345  	identityRegistry identityRegistry,
   346  	publisher eventbus.Publisher,
   347  	addressProvider addressProvider,
   348  ) func(*gin.Engine) error {
   349  	connectionEndpoint := NewConnectionEndpoint(manager, stateProvider, proposalRepository, identityRegistry, publisher, addressProvider)
   350  	return func(e *gin.Engine) error {
   351  		connGroup := e.Group("")
   352  		{
   353  			connGroup.GET("/connection", connectionEndpoint.Status)
   354  			connGroup.PUT("/connection", connectionEndpoint.Create)
   355  			connGroup.DELETE("/connection", connectionEndpoint.Kill)
   356  			connGroup.GET("/connection/statistics", connectionEndpoint.GetStatistics)
   357  			connGroup.GET("/connection/traffic", connectionEndpoint.GetTraffic)
   358  		}
   359  		return nil
   360  	}
   361  }
   362  
   363  func toConnectionRequest(req *http.Request, defaultHermes string) (*contract.ConnectionCreateRequest, error) {
   364  	connectionRequest := contract.ConnectionCreateRequest{
   365  		ConnectOptions: contract.ConnectOptions{
   366  			DisableKillSwitch: false,
   367  			DNS:               connection.DNSOptionAuto,
   368  		},
   369  		HermesID: defaultHermes,
   370  	}
   371  	err := json.NewDecoder(req.Body).Decode(&connectionRequest)
   372  	if err != nil {
   373  		return nil, err
   374  	}
   375  	return &connectionRequest, nil
   376  }
   377  
   378  func getConnectOptions(cr *contract.ConnectionCreateRequest) connection.ConnectParams {
   379  	dns := connection.DNSOptionAuto
   380  	if cr.ConnectOptions.DNS != "" {
   381  		dns = cr.ConnectOptions.DNS
   382  	}
   383  
   384  	return connection.ConnectParams{
   385  		DisableKillSwitch: cr.ConnectOptions.DisableKillSwitch,
   386  		DNS:               dns,
   387  		ProxyPort:         cr.ConnectOptions.ProxyPort,
   388  	}
   389  }