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 }