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 }