github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/cmd/commands/connection/command.go (about)

     1  /*
     2   * Copyright (C) 2020 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 connection
    19  
    20  import (
    21  	"errors"
    22  	"fmt"
    23  	"os"
    24  	"strings"
    25  	"text/tabwriter"
    26  	"time"
    27  
    28  	"github.com/urfave/cli/v2"
    29  
    30  	"github.com/mysteriumnetwork/node/cmd/commands/cli/clio"
    31  	"github.com/mysteriumnetwork/node/config"
    32  	"github.com/mysteriumnetwork/node/config/remote"
    33  	"github.com/mysteriumnetwork/node/core/connection"
    34  	"github.com/mysteriumnetwork/node/core/connection/connectionstate"
    35  	"github.com/mysteriumnetwork/node/datasize"
    36  	"github.com/mysteriumnetwork/node/identity/registry"
    37  	"github.com/mysteriumnetwork/node/money"
    38  	tequilapi_client "github.com/mysteriumnetwork/node/tequilapi/client"
    39  	"github.com/mysteriumnetwork/node/tequilapi/contract"
    40  	"github.com/mysteriumnetwork/terms/terms-go"
    41  )
    42  
    43  // CommandName is the name of this command
    44  const CommandName = "connection"
    45  
    46  var (
    47  	flagProxyPort = cli.IntFlag{
    48  		Name:  "proxy",
    49  		Usage: "Proxy port",
    50  	}
    51  
    52  	flagCountry = cli.StringFlag{
    53  		Name:  "country",
    54  		Usage: "Two letter (ISO 3166-1 alpha-2) country code to filter proposals.",
    55  	}
    56  
    57  	flagLocationType = cli.StringFlag{
    58  		Name:  "location-type",
    59  		Usage: "Node location types to filter by eg.'hosting', 'residential', 'mobile' etc.",
    60  	}
    61  
    62  	flagSortType = cli.StringFlag{
    63  		Name:  "sort",
    64  		Usage: "Proposal sorting type. One of: quality, bandwidth, latency, uptime or price",
    65  		Value: "quality",
    66  	}
    67  
    68  	flagIncludeFailed = cli.BoolFlag{
    69  		Name:  "include-failed",
    70  		Usage: "Include proposals marked as test failed by monitoring agent",
    71  		Value: false,
    72  	}
    73  )
    74  
    75  const serviceWireguard = "wireguard"
    76  
    77  // NewCommand function creates license command.
    78  func NewCommand() *cli.Command {
    79  	var cmd *command
    80  
    81  	return &cli.Command{
    82  		Name:        CommandName,
    83  		Usage:       "Manage your connection",
    84  		Description: "Using the connection subcommands you can manage your connection or get additional information about it",
    85  		Flags:       []cli.Flag{&config.FlagTequilapiAddress, &config.FlagTequilapiPort},
    86  		Before: func(ctx *cli.Context) error {
    87  			tc, err := clio.NewTequilApiClient(ctx)
    88  			if err != nil {
    89  				return err
    90  			}
    91  
    92  			cfg, err := remote.NewConfig(tc)
    93  			if err != nil {
    94  				return err
    95  			}
    96  
    97  			cmd = &command{
    98  				tequilapi: tc,
    99  				cfg:       cfg,
   100  			}
   101  			return nil
   102  		},
   103  		Subcommands: []*cli.Command{
   104  			{
   105  				Name:  "proposals",
   106  				Usage: "List all possible proposals to which you can connect",
   107  				Flags: []cli.Flag{&flagCountry, &flagLocationType},
   108  				Action: func(ctx *cli.Context) error {
   109  					cmd.proposals(ctx)
   110  					return nil
   111  				},
   112  			},
   113  			{
   114  				Name:      "up",
   115  				ArgsUsage: "[ProviderIdentityAddress]",
   116  				Usage:     "Create a new connection",
   117  				Flags:     []cli.Flag{&config.FlagAgreedTermsConditions, &flagCountry, &flagLocationType, &flagSortType, &flagIncludeFailed, &flagProxyPort},
   118  				Action: func(ctx *cli.Context) error {
   119  					cmd.up(ctx)
   120  					return nil
   121  				},
   122  			},
   123  			{
   124  				Name:  "down",
   125  				Usage: "Disconnect from your current connection",
   126  				Flags: []cli.Flag{&flagProxyPort},
   127  				Action: func(ctx *cli.Context) error {
   128  					cmd.down(ctx)
   129  					return nil
   130  				},
   131  			},
   132  			{
   133  				Name:  "info",
   134  				Usage: "Show information about your connection",
   135  				Flags: []cli.Flag{&flagProxyPort},
   136  				Action: func(ctx *cli.Context) error {
   137  					cmd.info(ctx)
   138  					return nil
   139  				},
   140  			},
   141  		},
   142  	}
   143  }
   144  
   145  type command struct {
   146  	tequilapi *tequilapi_client.Client
   147  	cfg       *remote.Config
   148  }
   149  
   150  func (c *command) proposals(ctx *cli.Context) {
   151  	locationType := ctx.String(flagLocationType.Name)
   152  	locationCountry := ctx.String(flagCountry.Name)
   153  	if locationCountry != "" && len(locationCountry) != 2 {
   154  		clio.Warn("Country code must be in ISO 3166-1 alpha-2 format. Example: 'UK', 'US'")
   155  		return
   156  	}
   157  
   158  	proposals, err := c.tequilapi.ProposalsByLocationAndService(serviceWireguard, locationType, locationCountry)
   159  	if err != nil {
   160  		clio.Warn("Failed to fetch proposal list")
   161  		return
   162  	}
   163  
   164  	if len(proposals) == 0 {
   165  		clio.Info("No proposals found")
   166  		return
   167  	}
   168  
   169  	clio.Info("Found proposals:")
   170  	w := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0)
   171  	for _, p := range proposals {
   172  		fmt.Fprintln(w, proposalFormatted(&p))
   173  	}
   174  	w.Flush()
   175  }
   176  
   177  func (c *command) down(ctx *cli.Context) {
   178  	status, err := c.tequilapi.ConnectionStatus(ctx.Int(flagProxyPort.Name))
   179  	if err != nil {
   180  		clio.Warn("Could not get connection status")
   181  		return
   182  	}
   183  
   184  	if status.Status != string(connectionstate.NotConnected) {
   185  		if err := c.tequilapi.ConnectionDestroy(ctx.Int(flagProxyPort.Name)); err != nil {
   186  			clio.Warn(err)
   187  			return
   188  		}
   189  	}
   190  
   191  	clio.Success("Disconnected")
   192  }
   193  
   194  func (c *command) handleTOS(ctx *cli.Context) error {
   195  	if ctx.Bool(config.FlagAgreedTermsConditions.Name) {
   196  		c.acceptTOS()
   197  		return nil
   198  	}
   199  
   200  	agreed := c.cfg.GetBool(contract.TermsConsumerAgreed)
   201  	if !agreed {
   202  		return errors.New("you must agree with consumer terms of use in order to use this command")
   203  	}
   204  
   205  	version := c.cfg.GetString(contract.TermsVersion)
   206  	if version != terms.TermsVersion {
   207  		return fmt.Errorf("you've agreed to terms of use version %s, but version %s is required", version, terms.TermsVersion)
   208  	}
   209  
   210  	return nil
   211  }
   212  
   213  func (c *command) acceptTOS() {
   214  	t := true
   215  	if err := c.tequilapi.UpdateTerms(contract.TermsRequest{
   216  		AgreedConsumer: &t,
   217  		AgreedVersion:  terms.TermsVersion,
   218  	}); err != nil {
   219  		clio.Info("Failed to save terms of use agreement, you will have to re-agree on next launch")
   220  	}
   221  }
   222  
   223  func (c *command) up(ctx *cli.Context) {
   224  	if err := c.handleTOS(ctx); err != nil {
   225  		clio.PrintTOSError(err)
   226  		return
   227  	}
   228  
   229  	status, err := c.tequilapi.ConnectionStatus(ctx.Int(flagProxyPort.Name))
   230  	if err != nil {
   231  		clio.Warn("Could not get connection status")
   232  		return
   233  	}
   234  
   235  	switch connectionstate.State(status.Status) {
   236  	case
   237  		// connectionstate.Connected,
   238  		connectionstate.Connecting,
   239  		connectionstate.Disconnecting,
   240  		connectionstate.Reconnecting:
   241  
   242  		msg := fmt.Sprintf("You can't create a new connection, you're in state '%s'", status.Status)
   243  		clio.Warn(msg)
   244  		return
   245  	}
   246  
   247  	providers := strings.Split(ctx.Args().First(), ",")
   248  	providerIDs := []string{}
   249  
   250  	for _, p := range providers {
   251  		if len(p) > 0 {
   252  			providerIDs = append(providerIDs, p)
   253  		}
   254  	}
   255  
   256  	id, err := c.tequilapi.CurrentIdentity("", "")
   257  	if err != nil {
   258  		clio.Error("Failed to get your identity")
   259  		return
   260  	}
   261  
   262  	identityStatus, err := c.tequilapi.Identity(id.Address)
   263  	if err != nil {
   264  		clio.Warn("Failed to get identity status")
   265  		return
   266  	}
   267  
   268  	if identityStatus.RegistrationStatus != registry.Registered.String() {
   269  		clio.Warn("Your identity is not registered, please execute `myst account register` first")
   270  		return
   271  	}
   272  
   273  	clio.Status("CONNECTING", "Creating connection from:", id.Address, "to:", providers)
   274  
   275  	connectOptions := contract.ConnectOptions{
   276  		DNS:               connection.DNSOptionAuto,
   277  		DisableKillSwitch: false,
   278  		ProxyPort:         ctx.Int(flagProxyPort.Name),
   279  	}
   280  	hermesID, err := c.cfg.GetHermesID()
   281  	if err != nil {
   282  		clio.Error(err)
   283  		return
   284  	}
   285  
   286  	filter := contract.ConnectionCreateFilter{
   287  		Providers:               providerIDs,
   288  		CountryCode:             ctx.String(flagCountry.Name),
   289  		IPType:                  ctx.String(flagLocationType.Name),
   290  		SortBy:                  ctx.String(flagSortType.Name),
   291  		IncludeMonitoringFailed: ctx.Bool(flagIncludeFailed.Name),
   292  	}
   293  
   294  	_, err = c.tequilapi.SmartConnectionCreate(id.Address, hermesID, serviceWireguard, filter, connectOptions)
   295  	if err != nil {
   296  		clio.Error("Failed to create a new connection: ", err)
   297  		return
   298  	}
   299  
   300  	clio.Success("Connected")
   301  }
   302  
   303  func (c *command) info(ctx *cli.Context) {
   304  	inf := newConnInfo()
   305  
   306  	id, err := c.tequilapi.CurrentIdentity("", "")
   307  	if err == nil {
   308  		inf.set(infIdentity, id.Address)
   309  	}
   310  
   311  	status, err := c.tequilapi.ConnectionStatus(ctx.Int(flagProxyPort.Name))
   312  	if err == nil {
   313  		if status.Status == string(connectionstate.Connected) {
   314  			inf.isConnected = true
   315  			inf.set(infProposal, status.Proposal.String())
   316  		}
   317  
   318  		inf.set(infStatus, status.Status)
   319  		inf.set(infSessionID, status.SessionID)
   320  	}
   321  
   322  	ip, err := c.tequilapi.ProxyIP(ctx.Int(flagProxyPort.Name))
   323  	if err == nil {
   324  		inf.set(infIP, ip.IP)
   325  	}
   326  
   327  	location, err := c.tequilapi.ProxyLocation(ctx.Int(flagProxyPort.Name))
   328  	if err == nil {
   329  		inf.set(infLocation, fmt.Sprintf("%s, %s (%s - %s)", location.City, location.Country, location.IPType, location.ISP))
   330  	}
   331  
   332  	if status.Status != string(connectionstate.Connected) {
   333  		inf.printAll()
   334  		return
   335  	}
   336  
   337  	statistics, err := c.tequilapi.ConnectionStatistics(status.SessionID)
   338  	if err == nil {
   339  		inf.set(infDuration, fmt.Sprint(time.Duration(statistics.Duration)*time.Second))
   340  		inf.set(infTransferred, fmt.Sprintf("%s/%s", datasize.FromBytes(statistics.BytesReceived), datasize.FromBytes(statistics.BytesSent)))
   341  		inf.set(infThroughput, fmt.Sprintf("%s/%s", datasize.BitSpeed(statistics.ThroughputReceived), datasize.BitSpeed(statistics.ThroughputSent)))
   342  		inf.set(infSpent, money.New(statistics.TokensSpent).String())
   343  	}
   344  
   345  	inf.printAll()
   346  }