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

     1  /*
     2   * Copyright (C) 2019 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 mmn
    19  
    20  import (
    21  	"bufio"
    22  	"encoding/base64"
    23  	"fmt"
    24  	"net/url"
    25  	"os"
    26  	"os/exec"
    27  	"runtime"
    28  	"strings"
    29  
    30  	"github.com/mysteriumnetwork/node/tequilapi/pkce"
    31  
    32  	"github.com/mysteriumnetwork/node/tequilapi/sso"
    33  
    34  	"github.com/rs/zerolog/log"
    35  
    36  	"github.com/mysteriumnetwork/node/config"
    37  	"github.com/mysteriumnetwork/node/core/ip"
    38  	nodevent "github.com/mysteriumnetwork/node/core/node/event"
    39  	"github.com/mysteriumnetwork/node/core/service/servicestate"
    40  	"github.com/mysteriumnetwork/node/eventbus"
    41  	"github.com/mysteriumnetwork/node/identity"
    42  	"github.com/mysteriumnetwork/node/metadata"
    43  )
    44  
    45  // MMN struct
    46  type MMN struct {
    47  	client     *client
    48  	ipResolver ip.Resolver
    49  
    50  	lastIP       string
    51  	lastIdentity identity.Identity
    52  
    53  	mystnodesURL   string
    54  	claimPath      string
    55  	onboardingPath string
    56  }
    57  
    58  // NewMMN creates new instance of MMN
    59  func NewMMN(resolver ip.Resolver, client *client) *MMN {
    60  	return &MMN{client: client, ipResolver: resolver, mystnodesURL: config.GetString(config.FlagMMNAddress), claimPath: "/node-claim", onboardingPath: "/clickboarding"}
    61  }
    62  
    63  // Subscribe subscribes to node events and reports them to MMN
    64  func (m *MMN) Subscribe(eventBus eventbus.EventBus) error {
    65  	if err := eventBus.SubscribeAsync(nodevent.AppTopicNode, m.handleNodeStart); err != nil {
    66  		return err
    67  	}
    68  	if err := eventBus.SubscribeAsync(identity.AppTopicIdentityUnlock, m.handleIdentityUnlock); err != nil {
    69  		return err
    70  	}
    71  	return eventBus.SubscribeAsync(servicestate.AppTopicServiceStatus, m.handleServiceStart)
    72  }
    73  
    74  // handleNodeStart handles node state change and fetches the IP accordingly.
    75  func (m *MMN) handleNodeStart(e nodevent.Payload) {
    76  	if e.Status != nodevent.StatusStarted {
    77  		return
    78  	}
    79  
    80  	var err error
    81  	m.lastIP, err = m.ipResolver.GetOutboundIP()
    82  	if err != nil {
    83  		log.Error().Msgf("Failed to get get Outbound IP for MMN: %v", err)
    84  	}
    85  }
    86  
    87  func (m *MMN) handleIdentityUnlock(ev identity.AppEventIdentityUnlock) {
    88  	m.lastIdentity = ev.ID
    89  }
    90  
    91  // handleServiceStart does auto-register to MMN, but only for providers.
    92  func (m *MMN) handleServiceStart(e servicestate.AppEventServiceStatus) {
    93  	if e.Status != string(servicestate.Running) {
    94  		return
    95  	}
    96  
    97  	// TODO Turn off auto-register then WEB UI will have possibility to configure API key
    98  	isRegistrationEnabled := len(config.Current.GetString(config.FlagMMNAPIKey.Name)) != 0
    99  	if !isRegistrationEnabled {
   100  		log.Debug().Msg("Identity unlocked, registration to MMN disabled because the API key missing in config.")
   101  		return
   102  	}
   103  
   104  	if err := m.ClaimNode(); err != nil {
   105  		log.Error().Msgf("Failed to register identity to MMN: %v", err)
   106  	}
   107  }
   108  
   109  func (m *MMN) claimRequestNoRedirect() NodeClaimRequest {
   110  	return m.claimRequest(nil)
   111  }
   112  
   113  func (m *MMN) claimRequest(redirectURL *url.URL) NodeClaimRequest {
   114  	rru := ""
   115  	if redirectURL != nil {
   116  		rru = fmt.Sprint(redirectURL)
   117  	}
   118  	return NodeClaimRequest{
   119  		LocalIP:     m.lastIP,
   120  		Identity:    m.lastIdentity.Address,
   121  		APIKey:      config.GetString(config.FlagMMNAPIKey),
   122  		VendorID:    config.GetString(config.FlagVendorID),
   123  		Arch:        runtime.GOOS + docker() + "/" + runtime.GOARCH,
   124  		OS:          getOS(),
   125  		NodeVersion: metadata.VersionAsString(),
   126  		RedirectURL: rru,
   127  	}
   128  }
   129  
   130  func (m *MMN) onboardingRequest(info pkce.Info, redirectURL *url.URL) sso.MystnodesMessage {
   131  	return sso.MystnodesMessage{
   132  		CodeChallenge: info.CodeChallenge,
   133  		Identity:      m.lastIdentity.Address,
   134  		RedirectURL:   fmt.Sprint(redirectURL),
   135  	}
   136  }
   137  
   138  // ClaimNode registers node to MMN
   139  func (m *MMN) ClaimNode() error {
   140  	return m.client.ClaimNode(m.claimRequestNoRedirect())
   141  }
   142  
   143  // ClaimLink generate claim link
   144  func (m *MMN) ClaimLink(redirectURL *url.URL) (*url.URL, error) {
   145  	claimRequestJson, err := m.claimRequest(redirectURL).json()
   146  	if err != nil {
   147  		return nil, err
   148  	}
   149  
   150  	signature, err := m.client.signer(m.lastIdentity).Sign(claimRequestJson)
   151  	if err != nil {
   152  		return nil, err
   153  	}
   154  
   155  	link, err := url.Parse(m.mystnodesURL)
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  
   160  	link = link.JoinPath(m.claimPath)
   161  
   162  	q := link.Query()
   163  	q.Set("message", base64.RawURLEncoding.EncodeToString(claimRequestJson))
   164  	q.Set("signature", base64.RawURLEncoding.EncodeToString(signature.Bytes()))
   165  	link.RawQuery = q.Encode()
   166  
   167  	return link, nil
   168  }
   169  
   170  func getOS() string {
   171  	switch runtime.GOOS {
   172  	case "darwin":
   173  		output, err := exec.Command("sw_vers", "-productVersion").Output()
   174  		if err != nil {
   175  			log.Error().Err(err).Msg("Failed to get OS information")
   176  			return "macOS (unknown)"
   177  		}
   178  		return "macOS " + strings.TrimSpace(string(output))
   179  	case "linux":
   180  		distro, err := parseLinuxOS()
   181  		if err != nil {
   182  			log.Error().Err(err).Msg("Failed to get OS information")
   183  			return "linux (unknown)" + docker()
   184  		}
   185  		return distro + docker()
   186  	case "windows":
   187  		output, err := exec.Command("wmic", "os", "get", "Caption", "/value").Output()
   188  		if err != nil {
   189  			log.Error().Err(err).Msg("Failed to get OS information")
   190  			return "windows (unknown)"
   191  		}
   192  		return strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(string(output)), "Caption="))
   193  	}
   194  	return runtime.GOOS
   195  }
   196  
   197  func parseLinuxOS() (string, error) {
   198  	output, err := exec.Command("lsb_release", "-d").Output()
   199  	if err == nil {
   200  		return strings.TrimSpace(strings.TrimPrefix(string(output), "Description:")), nil
   201  	}
   202  
   203  	const etcOsRelease = "/etc/os-release"
   204  	const altOsRelease = "/usr/lib/os-release"
   205  	osReleaseFile, err := os.Open(etcOsRelease)
   206  	if err != nil {
   207  		if !os.IsNotExist(err) {
   208  			return "", fmt.Errorf("error opening %s: %w", etcOsRelease, err)
   209  		}
   210  		osReleaseFile, err = os.Open(altOsRelease)
   211  		if err != nil {
   212  			return "", fmt.Errorf("error opening %s: %w", altOsRelease, err)
   213  		}
   214  	}
   215  	defer osReleaseFile.Close()
   216  
   217  	var prettyName string
   218  	scanner := bufio.NewScanner(osReleaseFile)
   219  	for scanner.Scan() {
   220  		line := scanner.Text()
   221  		if strings.HasPrefix(line, "PRETTY_NAME") {
   222  			tokens := strings.SplitN(line, "=", 2)
   223  			if len(tokens) == 2 {
   224  				prettyName = strings.Trim(tokens[1], "\"")
   225  			}
   226  		}
   227  	}
   228  	if prettyName != "" {
   229  		return prettyName, nil
   230  	}
   231  
   232  	return "linux (unknown)", nil
   233  }
   234  
   235  func docker() string {
   236  	if _, err := os.Stat("/.dockerenv"); err == nil {
   237  		return "(docker)"
   238  	}
   239  
   240  	return ""
   241  }