github.com/decred/dcrlnd@v0.7.6/cmd/dcrlncli/profile.go (about) 1 package main 2 3 import ( 4 "bytes" 5 "crypto/x509" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "io/ioutil" 10 "path" 11 "strings" 12 13 "github.com/decred/dcrlnd/lncfg" 14 "github.com/decred/dcrlnd/lnrpc" 15 "github.com/decred/dcrlnd/walletunlocker" 16 "github.com/urfave/cli" 17 "gopkg.in/macaroon.v2" 18 ) 19 20 var ( 21 errNoProfileFile = errors.New("no profile file found") 22 ) 23 24 // profileEntry is a struct that represents all settings for one specific 25 // profile. 26 type profileEntry struct { 27 Name string `json:"name"` 28 RPCServer string `json:"rpcserver"` 29 LndDir string `json:"lnddir"` 30 Chain string `json:"chain"` 31 Network string `json:"network"` 32 NoMacaroons bool `json:"no-macaroons,omitempty"` 33 TLSCert string `json:"tlscert"` 34 Macaroons *macaroonJar `json:"macaroons"` 35 } 36 37 // cert returns the profile's TLS certificate as a x509 certificate pool. 38 func (e *profileEntry) cert() (*x509.CertPool, error) { 39 if e.TLSCert == "" { 40 return nil, nil 41 } 42 43 cp := x509.NewCertPool() 44 if !cp.AppendCertsFromPEM([]byte(e.TLSCert)) { 45 return nil, fmt.Errorf("credentials: failed to append " + 46 "certificate") 47 } 48 return cp, nil 49 } 50 51 // getGlobalOptions returns the global connection options. If a profile file 52 // exists, these global options might be read from a predefined profile. If no 53 // profile exists, the global options from the command line are returned as an 54 // ephemeral profile entry. 55 func getGlobalOptions(ctx *cli.Context, skipMacaroons bool) (*profileEntry, 56 error) { 57 58 var profileName string 59 60 // Try to load the default profile file and depending on its existence 61 // what profile to use. 62 f, err := loadProfileFile(defaultProfileFile) 63 switch { 64 // The legacy case where no profile file exists and the user also didn't 65 // request to use one. We only consider the global options here. 66 case err == errNoProfileFile && !ctx.GlobalIsSet("profile"): 67 return profileFromContext(ctx, false, skipMacaroons) 68 69 // The file doesn't exist but the user specified an explicit profile. 70 case err == errNoProfileFile && ctx.GlobalIsSet("profile"): 71 return nil, fmt.Errorf("profile file %s does not exist", 72 defaultProfileFile) 73 74 // There is a file but we couldn't read/parse it. 75 case err != nil: 76 return nil, fmt.Errorf("could not read profile file %s: "+ 77 "%v", defaultProfileFile, err) 78 79 // The user explicitly disabled the use of profiles for this command by 80 // setting the flag to an empty string. We fall back to the default/old 81 // behavior. 82 case ctx.GlobalIsSet("profile") && ctx.GlobalString("profile") == "": 83 return profileFromContext(ctx, false, skipMacaroons) 84 85 // There is a file, but no default profile is specified. The user also 86 // didn't specify a profile to use so we fall back to the default/old 87 // behavior. 88 case !ctx.GlobalIsSet("profile") && len(f.Default) == 0: 89 return profileFromContext(ctx, false, skipMacaroons) 90 91 // The user didn't specify a profile but there is a default one defined. 92 case !ctx.GlobalIsSet("profile") && len(f.Default) > 0: 93 profileName = f.Default 94 95 // The user specified a specific profile to use. 96 case ctx.GlobalIsSet("profile"): 97 profileName = ctx.GlobalString("profile") 98 } 99 100 // If we got to here, we do have a profile file and know the name of the 101 // profile to use. Now we just need to make sure it does exist. 102 for _, prof := range f.Profiles { 103 if prof.Name == profileName { 104 return prof, nil 105 } 106 } 107 108 return nil, fmt.Errorf("profile '%s' not found in file %s", profileName, 109 defaultProfileFile) 110 } 111 112 // profileFromContext creates an ephemeral profile entry from the global options 113 // set in the CLI context. 114 func profileFromContext(ctx *cli.Context, store, skipMacaroons bool) ( 115 *profileEntry, error) { 116 117 // Parse the paths of the cert and macaroon. This will validate the 118 // chain and network value as well. 119 tlsCertPath, macPath, err := extractPathArgs(ctx) 120 if err != nil { 121 return nil, err 122 } 123 124 // Load the certificate file now, if specified. We store it as plain PEM 125 // directly. 126 var tlsCert []byte 127 if tlsCertPath != "" { 128 var err error 129 tlsCert, err = ioutil.ReadFile(tlsCertPath) 130 if err != nil { 131 return nil, fmt.Errorf("could not load TLS cert file: %v", err) 132 } 133 } 134 135 entry := &profileEntry{ 136 RPCServer: ctx.GlobalString("rpcserver"), 137 LndDir: lncfg.CleanAndExpandPath(ctx.GlobalString("lnddir")), 138 Chain: ctx.GlobalString("chain"), 139 Network: ctx.GlobalString("network"), 140 NoMacaroons: ctx.GlobalBool("no-macaroons"), 141 TLSCert: string(tlsCert), 142 } 143 144 // If we aren't using macaroons in general (flag --no-macaroons) or 145 // don't need macaroons for this command (wallet unlocker), we can now 146 // return already. 147 if skipMacaroons || ctx.GlobalBool("no-macaroons") { 148 return entry, nil 149 } 150 151 // Now load and possibly encrypt the macaroon file. 152 macBytes, err := ioutil.ReadFile(macPath) 153 if err != nil { 154 return nil, fmt.Errorf("unable to read macaroon path (check "+ 155 "the network setting!): %v", err) 156 } 157 mac := &macaroon.Macaroon{} 158 if err = mac.UnmarshalBinary(macBytes); err != nil { 159 return nil, fmt.Errorf("unable to decode macaroon: %v", err) 160 } 161 162 var pw []byte 163 if store { 164 // Read a password from the terminal. If it's empty, we won't 165 // encrypt the macaroon and store it plaintext. 166 pw, err = capturePassword( 167 "Enter password to encrypt macaroon with or leave "+ 168 "blank to store in plaintext: ", true, 169 walletunlocker.ValidatePassword, 170 ) 171 if err != nil { 172 return nil, fmt.Errorf("unable to get encryption "+ 173 "password: %v", err) 174 } 175 } 176 macEntry := &macaroonEntry{} 177 if err = macEntry.storeMacaroon(mac, pw); err != nil { 178 return nil, fmt.Errorf("unable to store macaroon: %v", err) 179 } 180 181 // We determine the name of the macaroon from the file itself but cut 182 // off the ".macaroon" at the end. 183 macEntry.Name = path.Base(macPath) 184 if path.Ext(macEntry.Name) == "macaroon" { 185 macEntry.Name = strings.TrimSuffix(macEntry.Name, ".macaroon") 186 } 187 188 // Now that we have the macaroon jar as well, let's return the entry 189 // with all the values populated. 190 entry.Macaroons = &macaroonJar{ 191 Default: macEntry.Name, 192 Timeout: ctx.GlobalInt64("macaroontimeout"), 193 IP: ctx.GlobalString("macaroonip"), 194 Jar: []*macaroonEntry{macEntry}, 195 } 196 197 return entry, nil 198 } 199 200 // loadProfileFile tries to load the file specified and JSON deserialize it into 201 // the profile file struct. 202 func loadProfileFile(file string) (*profileFile, error) { 203 if !lnrpc.FileExists(file) { 204 return nil, errNoProfileFile 205 } 206 207 content, err := ioutil.ReadFile(file) 208 if err != nil { 209 return nil, fmt.Errorf("could not load profile file %s: %v", 210 file, err) 211 } 212 f := &profileFile{} 213 err = f.unmarshalJSON(content) 214 if err != nil { 215 return nil, fmt.Errorf("could not unmarshal profile file %s: "+ 216 "%v", file, err) 217 } 218 return f, nil 219 } 220 221 // saveProfileFile stores the given profile file struct in the specified file, 222 // overwriting it if it already existed. 223 func saveProfileFile(file string, f *profileFile) error { 224 content, err := f.marshalJSON() 225 if err != nil { 226 return fmt.Errorf("could not marshal profile: %v", err) 227 } 228 return ioutil.WriteFile(file, content, 0644) 229 } 230 231 // profileFile is a struct that represents the whole content of a profile file. 232 type profileFile struct { 233 Default string `json:"default,omitempty"` 234 Profiles []*profileEntry `json:"profiles"` 235 } 236 237 // unmarshalJSON tries to parse the given JSON and unmarshal it into the 238 // receiving instance. 239 func (f *profileFile) unmarshalJSON(content []byte) error { 240 return json.Unmarshal(content, f) 241 } 242 243 // marshalJSON serializes the receiving instance to formatted/indented JSON. 244 func (f *profileFile) marshalJSON() ([]byte, error) { 245 b, err := json.Marshal(f) 246 if err != nil { 247 return nil, fmt.Errorf("error JSON marshalling profile: %v", 248 err) 249 } 250 251 var out bytes.Buffer 252 err = json.Indent(&out, b, "", " ") 253 if err != nil { 254 return nil, fmt.Errorf("error indenting profile JSON: %v", err) 255 } 256 out.WriteString("\n") 257 return out.Bytes(), nil 258 }