github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/tequilapi/endpoints/proposals.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 "strconv" 22 23 "github.com/gin-gonic/gin" 24 "github.com/mysteriumnetwork/go-rest/apierror" 25 26 "github.com/mysteriumnetwork/node/core/discovery/proposal" 27 "github.com/mysteriumnetwork/node/core/location" 28 "github.com/mysteriumnetwork/node/core/quality" 29 "github.com/mysteriumnetwork/node/market" 30 "github.com/mysteriumnetwork/node/nat" 31 "github.com/mysteriumnetwork/node/services/datatransfer" 32 "github.com/mysteriumnetwork/node/services/dvpn" 33 "github.com/mysteriumnetwork/node/services/scraping" 34 "github.com/mysteriumnetwork/node/services/wireguard" 35 "github.com/mysteriumnetwork/node/tequilapi/contract" 36 "github.com/mysteriumnetwork/node/tequilapi/utils" 37 ) 38 39 // QualityFinder allows to fetch proposal quality data 40 type QualityFinder interface { 41 ProposalsQuality() []quality.ProposalQuality 42 } 43 44 type priceAPI interface { 45 GetCurrentPrice(nodeType string, country string, serviceType string) (market.Price, error) 46 } 47 48 type proposalsEndpoint struct { 49 proposalRepository proposalRepository 50 pricer priceAPI 51 locationResolver location.Resolver 52 filterPresets proposal.FilterPresetRepository 53 natProber natProber 54 } 55 56 // NewProposalsEndpoint creates and returns proposal creation endpoint 57 func NewProposalsEndpoint(proposalRepository proposalRepository, pricer priceAPI, locationResolver location.Resolver, filterPresetRepository proposal.FilterPresetRepository, natProber natProber) *proposalsEndpoint { 58 return &proposalsEndpoint{ 59 proposalRepository: proposalRepository, 60 pricer: pricer, 61 locationResolver: locationResolver, 62 filterPresets: filterPresetRepository, 63 natProber: natProber, 64 } 65 } 66 67 // swagger:operation GET /proposals Proposal listProposals 68 // 69 // --- 70 // summary: Returns proposals 71 // description: Returns list of proposals filtered by provider id 72 // parameters: 73 // - in: query 74 // name: provider_id 75 // description: id of provider proposals 76 // type: string 77 // - in: query 78 // name: service_type 79 // description: the service type of the proposal. Possible values are "openvpn", "wireguard" and "noop" 80 // type: string 81 // - in: query 82 // name: access_policy 83 // description: the access policy id to filter the proposals by 84 // type: string 85 // - in: query 86 // name: access_policy_source 87 // description: the access policy source to filter the proposals by 88 // type: string 89 // - in: query 90 // name: country 91 // description: If given will filter proposals by node location country. 92 // type: string 93 // - in: query 94 // name: ip_type 95 // description: IP Type (residential, datacenter, etc.). 96 // type: string 97 // - in: query 98 // name: compatibility_min 99 // description: Minimum compatibility level of the proposal. 100 // type: integer 101 // - in: query 102 // name: compatibility_max 103 // description: Maximum compatibility level of the proposal. 104 // type: integer 105 // - in: query 106 // name: quality_min 107 // description: Minimum quality of the provider. 108 // type: number 109 // - in: query 110 // name: nat_compatibility 111 // description: Pick nodes compatible with NAT of specified type. Specify "auto" to probe NAT. 112 // type: string 113 // responses: 114 // 200: 115 // description: List of proposals 116 // schema: 117 // "$ref": "#/definitions/ListProposalsResponse" 118 // 500: 119 // description: Internal server error 120 // schema: 121 // "$ref": "#/definitions/APIError" 122 func (pe *proposalsEndpoint) List(c *gin.Context) { 123 req := c.Request 124 presetID, _ := strconv.Atoi(req.URL.Query().Get("preset_id")) 125 compatibilityMinQuery := req.URL.Query().Get("compatibility_min") 126 compatibilityMin := 2 127 if compatibilityMinQuery != "" { 128 compatibilityMin, _ = strconv.Atoi(compatibilityMinQuery) 129 } 130 compatibilityMax, _ := strconv.Atoi(req.URL.Query().Get("compatibility_max")) 131 qualityMin := func() float32 { 132 f, err := strconv.ParseFloat(req.URL.Query().Get("quality_min"), 32) 133 if err != nil { 134 return 0 135 } 136 return float32(f) 137 }() 138 139 natCompatibility := nat.NATType(req.URL.Query().Get("nat_compatibility")) 140 if natCompatibility == contract.AutoNATType { 141 natType, err := pe.natProber.Probe(req.Context()) 142 if err != nil { 143 natCompatibility = "" 144 } else { 145 natCompatibility = natType 146 } 147 } 148 149 includeMonitoringFailed, _ := strconv.ParseBool(req.URL.Query().Get("include_monitoring_failed")) 150 proposals, err := pe.proposalRepository.Proposals(&proposal.Filter{ 151 PresetID: presetID, 152 ProviderID: req.URL.Query().Get("provider_id"), 153 ServiceType: req.URL.Query().Get("service_type"), 154 AccessPolicy: req.URL.Query().Get("access_policy"), 155 AccessPolicySource: req.URL.Query().Get("access_policy_source"), 156 LocationCountry: req.URL.Query().Get("location_country"), 157 IPType: req.URL.Query().Get("ip_type"), 158 NATCompatibility: natCompatibility, 159 CompatibilityMin: compatibilityMin, 160 CompatibilityMax: compatibilityMax, 161 QualityMin: qualityMin, 162 ExcludeUnsupported: true, 163 IncludeMonitoringFailed: includeMonitoringFailed, 164 }) 165 if err != nil { 166 c.Error(apierror.Internal("Proposal query failed: "+err.Error(), contract.ErrCodeProposalsQuery)) 167 return 168 } 169 170 proposalsRes := contract.ListProposalsResponse{Proposals: []contract.ProposalDTO{}} 171 for _, p := range proposals { 172 proposalsRes.Proposals = append(proposalsRes.Proposals, contract.NewProposalDTO(p)) 173 } 174 175 utils.WriteAsJSON(proposalsRes, c.Writer) 176 } 177 178 // swagger:operation GET /proposals/countries Countries listCountries 179 // 180 // --- 181 // summary: Returns number of proposals per country 182 // description: Returns a list of countries with a number of proposals 183 // parameters: 184 // - in: query 185 // name: provider_id 186 // description: id of provider proposals 187 // type: string 188 // - in: query 189 // name: service_type 190 // description: the service type of the proposal. Possible values are "openvpn", "wireguard" and "noop" 191 // type: string 192 // - in: query 193 // name: access_policy 194 // description: the access policy id to filter the proposals by 195 // type: string 196 // - in: query 197 // name: access_policy_source 198 // description: the access policy source to filter the proposals by 199 // type: string 200 // - in: query 201 // name: country 202 // description: If given will filter proposals by node location country. 203 // type: string 204 // - in: query 205 // name: ip_type 206 // description: IP Type (residential, datacenter, etc.). 207 // type: string 208 // - in: query 209 // name: compatibility_min 210 // description: Minimum compatibility level of the proposal. 211 // type: integer 212 // - in: query 213 // name: compatibility_max 214 // description: Maximum compatibility level of the proposal. 215 // type: integer 216 // - in: query 217 // name: quality_min 218 // description: Minimum quality of the provider. 219 // type: number 220 // - in: query 221 // name: nat_compatibility 222 // description: Pick nodes compatible with NAT of specified type. Specify "auto" to probe NAT. 223 // type: string 224 // responses: 225 // 200: 226 // description: List of countries 227 // schema: 228 // "$ref": "#/definitions/ListProposalsCountiesResponse" 229 // 500: 230 // description: Internal server error 231 // schema: 232 // "$ref": "#/definitions/APIError" 233 func (pe *proposalsEndpoint) Countries(c *gin.Context) { 234 req := c.Request 235 236 presetID, _ := strconv.Atoi(req.URL.Query().Get("preset_id")) 237 compatibilityMinQuery := req.URL.Query().Get("compatibility_min") 238 compatibilityMin := 2 239 if compatibilityMinQuery != "" { 240 compatibilityMin, _ = strconv.Atoi(compatibilityMinQuery) 241 } 242 compatibilityMax, _ := strconv.Atoi(req.URL.Query().Get("compatibility_max")) 243 qualityMin := func() float32 { 244 f, err := strconv.ParseFloat(req.URL.Query().Get("quality_min"), 32) 245 if err != nil { 246 return 0 247 } 248 return float32(f) 249 }() 250 251 natCompatibility := nat.NATType(req.URL.Query().Get("nat_compatibility")) 252 if natCompatibility == contract.AutoNATType { 253 natType, err := pe.natProber.Probe(req.Context()) 254 if err != nil { 255 natCompatibility = "" 256 } else { 257 natCompatibility = natType 258 } 259 } 260 261 includeMonitoringFailed, _ := strconv.ParseBool(req.URL.Query().Get("include_monitoring_failed")) 262 countries, err := pe.proposalRepository.Countries(&proposal.Filter{ 263 PresetID: presetID, 264 ProviderID: req.URL.Query().Get("provider_id"), 265 ServiceType: req.URL.Query().Get("service_type"), 266 AccessPolicy: req.URL.Query().Get("access_policy"), 267 AccessPolicySource: req.URL.Query().Get("access_policy_source"), 268 LocationCountry: req.URL.Query().Get("location_country"), 269 IPType: req.URL.Query().Get("ip_type"), 270 NATCompatibility: natCompatibility, 271 CompatibilityMin: compatibilityMin, 272 CompatibilityMax: compatibilityMax, 273 QualityMin: qualityMin, 274 ExcludeUnsupported: true, 275 IncludeMonitoringFailed: includeMonitoringFailed, 276 }) 277 if err != nil { 278 c.Error(apierror.Internal("Proposal country query failed: "+err.Error(), contract.ErrCodeProposalsCountryQuery)) 279 return 280 } 281 282 utils.WriteAsJSON(countries, c.Writer) 283 } 284 285 // swagger:operation GET /prices/current 286 // 287 // --- 288 // summary: Returns proposals 289 // description: Returns list of proposals filtered by provider id 290 // responses: 291 // 200: 292 // description: Current proposal price 293 // schema: 294 // "$ref": "#/definitions/CurrentPriceResponse" 295 // 500: 296 // description: Internal server error 297 // schema: 298 // "$ref": "#/definitions/APIError" 299 func (pe *proposalsEndpoint) CurrentPrice(c *gin.Context) { 300 allowedServiceTypes := map[string]struct{}{ 301 wireguard.ServiceType: {}, 302 scraping.ServiceType: {}, 303 datatransfer.ServiceType: {}, 304 dvpn.ServiceType: {}, 305 } 306 307 serviceType := c.Request.URL.Query().Get("service_type") 308 if len(serviceType) == 0 { 309 serviceType = wireguard.ServiceType 310 } else { 311 if _, ok := allowedServiceTypes[serviceType]; !ok { 312 c.Error(apierror.BadRequest("Invalid service type", contract.ErrCodeProposalsServiceType)) 313 return 314 } 315 } 316 317 loc, err := pe.locationResolver.DetectLocation() 318 if err != nil { 319 c.Error(apierror.Internal("Cannot detect location", contract.ErrCodeProposalsDetectLocation)) 320 return 321 } 322 323 price, err := pe.pricer.GetCurrentPrice(loc.IPType, loc.Country, serviceType) 324 if err != nil { 325 c.Error(apierror.Internal("Cannot retrieve current prices: "+err.Error(), contract.ErrCodeProposalsPrices)) 326 return 327 } 328 329 utils.WriteAsJSON(contract.CurrentPriceResponse{ 330 ServiceType: serviceType, 331 332 PricePerHour: price.PricePerHour, 333 PricePerGiB: price.PricePerGiB, 334 335 PricePerHourTokens: contract.NewTokens(price.PricePerHour), 336 PricePerGiBTokens: contract.NewTokens(price.PricePerGiB), 337 }, c.Writer) 338 } 339 340 // swagger:operation GET /v2/prices/current 341 // 342 // --- 343 // summary: Returns prices 344 // description: Returns prices for all service types 345 // responses: 346 // 200: 347 // description: Current price for service type 348 // schema: 349 // "$ref": "#/definitions/CurrentPriceResponse" 350 // 500: 351 // description: Internal server error 352 // schema: 353 // "$ref": "#/definitions/APIError" 354 func (pe *proposalsEndpoint) CurrentPrices(c *gin.Context) { 355 loc, err := pe.locationResolver.DetectLocation() 356 if err != nil { 357 c.Error(apierror.Internal("Cannot detect location", contract.ErrCodeProposalsDetectLocation)) 358 return 359 } 360 361 serviceTypes := []string{wireguard.ServiceType, scraping.ServiceType, datatransfer.ServiceType, dvpn.ServiceType} 362 result := make([]contract.CurrentPriceResponse, len(serviceTypes)) 363 364 for i, serviceType := range serviceTypes { 365 price, err := pe.pricer.GetCurrentPrice(loc.IPType, loc.Country, serviceType) 366 if err != nil { 367 c.Error(apierror.Internal("Cannot retrieve current prices: "+err.Error(), contract.ErrCodeProposalsPrices)) 368 return 369 } 370 371 result[i] = contract.CurrentPriceResponse{ 372 ServiceType: serviceType, 373 PricePerHour: price.PricePerHour, 374 PricePerGiB: price.PricePerGiB, 375 376 PricePerHourTokens: contract.NewTokens(price.PricePerHour), 377 PricePerGiBTokens: contract.NewTokens(price.PricePerGiB), 378 } 379 } 380 381 utils.WriteAsJSON(result, c.Writer) 382 } 383 384 // swagger:operation GET /proposals/filter-presets Proposal proposalFilterPresets 385 // 386 // --- 387 // summary: Returns proposal filter presets 388 // description: Returns proposal filter presets 389 // responses: 390 // 200: 391 // description: List of proposal filter presets 392 // schema: 393 // "$ref": "#/definitions/ListProposalFilterPresetsResponse" 394 // 500: 395 // description: Internal server error 396 // schema: 397 // "$ref": "#/definitions/APIError" 398 func (pe *proposalsEndpoint) FilterPresets(c *gin.Context) { 399 presets, err := pe.filterPresets.List() 400 if err != nil { 401 c.Error(apierror.Internal("Cannot list presets", contract.ErrCodeProposalsPresets)) 402 return 403 } 404 presetsRes := contract.ListProposalFilterPresetsResponse{Items: []contract.FilterPreset{}} 405 for _, p := range presets.Entries { 406 presetsRes.Items = append(presetsRes.Items, contract.NewFilterPreset(p)) 407 } 408 utils.WriteAsJSON(presetsRes, c.Writer) 409 } 410 411 // AddRoutesForProposals attaches proposals endpoints to router 412 func AddRoutesForProposals( 413 proposalRepository proposalRepository, 414 pricer priceAPI, 415 locationResolver location.Resolver, 416 filterPresetRepository proposal.FilterPresetRepository, 417 natProber natProber, 418 ) func(*gin.Engine) error { 419 pe := NewProposalsEndpoint(proposalRepository, pricer, locationResolver, filterPresetRepository, natProber) 420 return func(e *gin.Engine) error { 421 proposalGroup := e.Group("/proposals") 422 { 423 proposalGroup.GET("", pe.List) 424 proposalGroup.GET("/filter-presets", pe.FilterPresets) 425 proposalGroup.GET("/countries", pe.Countries) 426 } 427 428 e.GET("/prices/current", pe.CurrentPrice) 429 e.GET("/v2/prices/current", pe.CurrentPrices) 430 return nil 431 } 432 }