github.com/nats-io/nsc/v2@v2.8.7-0.20240307184528-efd7023c6896/cmd/common.go (about) 1 /* 2 * Copyright 2018-2022 The NATS Authors 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16 package cmd 17 18 import ( 19 "bytes" 20 "encoding/json" 21 "fmt" 22 "io" 23 "net/http" 24 "net/url" 25 "os" 26 "path" 27 "path/filepath" 28 "strconv" 29 "strings" 30 "sync" 31 "time" 32 33 "github.com/dustin/go-humanize" 34 "github.com/mitchellh/go-homedir" 35 cli "github.com/nats-io/cliprompts/v2" 36 "github.com/nats-io/jwt/v2" 37 "github.com/nats-io/nkeys" 38 "github.com/spf13/cobra" 39 "golang.org/x/text/cases" 40 "golang.org/x/text/language" 41 42 "github.com/nats-io/nsc/v2/cmd/store" 43 ) 44 45 // ResolvePath resolves a directory/file from an environment variable 46 // if not set defaultPath is returned 47 func ResolvePath(defaultPath string, varName string) string { 48 v := os.Getenv(varName) 49 if v != "" { 50 return v 51 } 52 return defaultPath 53 } 54 55 func GetOutput(fp string) (*os.File, error) { 56 var f *os.File 57 58 if fp == "--" { 59 f = os.Stdout 60 } else { 61 afp, err := filepath.Abs(fp) 62 if err != nil { 63 return nil, fmt.Errorf("error calculating abs %#q: %v", fp, err) 64 } 65 _, err = os.Stat(afp) 66 if err == nil { 67 return nil, fmt.Errorf("%#q already exists", afp) 68 } 69 if !os.IsNotExist(err) { 70 return nil, err 71 } 72 73 f, err = os.Create(afp) 74 if err != nil { 75 return nil, fmt.Errorf("error creating output file %#q: %v", afp, err) 76 } 77 } 78 return f, nil 79 } 80 81 func IsStdOut(fp string) bool { 82 return fp == "--" 83 } 84 85 func WriteJson(fp string, v interface{}) error { 86 data, err := json.Marshal(v) 87 if err != nil { 88 return fmt.Errorf("error marshaling: %v", err) 89 } 90 91 parent := filepath.Dir(fp) 92 if parent != "" { 93 if err := os.MkdirAll(parent, 0700); err != nil { 94 return fmt.Errorf("error creating dirs %#q: %v", fp, err) 95 } 96 } 97 if err := os.WriteFile(fp, data, 0600); err != nil { 98 return fmt.Errorf("error writing %#q: %v", fp, err) 99 } 100 101 return nil 102 } 103 104 func Write(fp string, data []byte) error { 105 var err error 106 var f *os.File 107 108 f, err = GetOutput(fp) 109 if err != nil { 110 return err 111 } 112 if !IsStdOut(fp) { 113 defer f.Close() 114 } 115 _, err = f.Write(data) 116 if err != nil { 117 return fmt.Errorf("error writing %#q: %v", fp, err) 118 } 119 120 if !IsStdOut(fp) { 121 if err := f.Sync(); err != nil { 122 return err 123 } 124 } 125 return nil 126 } 127 128 func ReadJson(fp string, v interface{}) error { 129 data, err := Read(fp) 130 if err != nil { 131 return err 132 } 133 err = json.Unmarshal(data, &v) 134 if err != nil { 135 return err 136 } 137 return nil 138 } 139 140 func Read(fp string) ([]byte, error) { 141 fp, err := Expand(fp) 142 if err != nil { 143 return nil, err 144 } 145 return os.ReadFile(fp) 146 } 147 148 func ParseNumber(s string) (int64, error) { 149 if s == "" { 150 return 0, nil 151 } 152 isNeg := strings.HasPrefix(s, "-") 153 if isNeg { 154 s = strings.TrimPrefix(s, "-") 155 } 156 i, err := humanize.ParseBytes(s) 157 if err != nil { 158 return 0, err 159 } 160 if isNeg { 161 return -(int64)(i), nil 162 } 163 return (int64)(i), nil 164 } 165 166 func UnixToDate(d int64) string { 167 if d == 0 { 168 return "" 169 } 170 171 return strings.Replace(time.Unix(d, 0).UTC().String(), " +0000", "", -1) 172 } 173 174 func HumanizedDate(d int64) string { 175 if d == 0 { 176 return "" 177 } 178 now := time.Now() 179 when := time.Unix(d, 0).UTC() 180 181 if now.After(when) { 182 return strings.TrimSpace(TitleCase(humanize.RelTime(when, now, "ago", ""))) 183 } else { 184 return strings.TrimSpace(TitleCase("in " + humanize.RelTime(now, when, "", ""))) 185 } 186 } 187 188 func RenderDate(d int64) string { 189 if d == 0 { 190 return "" 191 } 192 193 return UnixToDate(d) 194 } 195 196 func NKeyValidator(kind nkeys.PrefixByte) cli.Validator { 197 return func(v string) error { 198 if v == "" { 199 return fmt.Errorf("value cannot be empty") 200 } 201 nk, err := store.ResolveKey(v) 202 if err != nil { 203 return err 204 } 205 if nk == nil { 206 // if it looks like a file, provide a better message 207 if strings.Contains(v, string(os.PathSeparator)) { 208 _, err := os.Stat(v) 209 if err != nil { 210 return err 211 } 212 } 213 return fmt.Errorf("%q is not a valid nkey", v) 214 } 215 t, err := store.KeyType(nk) 216 if err != nil { 217 return err 218 } 219 if t != kind { 220 return fmt.Errorf("specified key is not valid for an %s", kind.String()) 221 } 222 return nil 223 } 224 } 225 226 func SeedNKeyValidatorMatching(pukeys []string, kinds ...nkeys.PrefixByte) cli.Validator { 227 return func(v string) error { 228 if v == "" { 229 return fmt.Errorf("value cannot be empty") 230 } 231 nk, err := store.ResolveKey(v) 232 if err != nil { 233 return err 234 } 235 if nk == nil { 236 // if it looks like a file, provide a better message 237 if strings.Contains(v, string(os.PathSeparator)) { 238 _, err := os.Stat(v) 239 if err != nil { 240 return err 241 } 242 } 243 return fmt.Errorf("%q is not a valid nkey", v) 244 } 245 t, err := store.KeyType(nk) 246 if err != nil { 247 return err 248 } 249 foundKind := false 250 kindNames := []string{} 251 for _, kind := range kinds { 252 if t == kind { 253 foundKind = true 254 break 255 } 256 kindNames = append(kindNames, kind.String()) 257 } 258 if !foundKind { 259 return fmt.Errorf("specified key is not valid for any of %v", kindNames) 260 } 261 262 pk, err := nk.PublicKey() 263 if err != nil { 264 return fmt.Errorf("error extracting public key: %v", err) 265 } 266 267 found := false 268 for _, k := range pukeys { 269 if k == pk { 270 found = true 271 break 272 } 273 } 274 if !found { 275 return fmt.Errorf("%q is not an expected signing key %v", v, pukeys) 276 } 277 278 _, err = nk.Seed() 279 if err != nil { 280 return err 281 } 282 283 return nil 284 } 285 } 286 287 func IsURL(v string) bool { 288 if u, err := url.Parse(v); err == nil { 289 s := strings.ToLower(u.Scheme) 290 return s == "http" || s == "https" 291 } 292 return false 293 } 294 295 func LoadFromFileOrURL(v string) ([]byte, error) { 296 // we expect either a file or url 297 if IsURL(v) { 298 return LoadFromURL(v) 299 } 300 v, err := Expand(v) 301 if err != nil { 302 return nil, err 303 } 304 _, err = os.Stat(v) 305 if err != nil { 306 return nil, err 307 } 308 return Read(v) 309 } 310 311 func LoadFromURL(url string) ([]byte, error) { 312 c := &http.Client{Timeout: time.Second * 5} 313 r, err := c.Get(url) 314 if err != nil { 315 return nil, fmt.Errorf("error loading %q: %v", url, err) 316 } 317 defer r.Body.Close() 318 if r.StatusCode != http.StatusOK { 319 return nil, fmt.Errorf("error reading response from %q: %v", url, r.Status) 320 } 321 var buf bytes.Buffer 322 _, err = io.Copy(&buf, r.Body) 323 if err != nil { 324 return nil, fmt.Errorf("error reading response from %q: %v", url, err) 325 } 326 data := buf.Bytes() 327 return data, nil 328 } 329 330 func IsValidDir(dir string) error { 331 fi, err := os.Stat(dir) 332 if err != nil { 333 return err 334 } 335 if !fi.IsDir() { 336 return fmt.Errorf("not a directory") 337 } 338 return nil 339 } 340 341 func MaybeMakeDir(dir string) error { 342 fi, err := os.Stat(dir) 343 if err != nil && os.IsNotExist(err) { 344 if err := os.MkdirAll(dir, 0700); err != nil { 345 return fmt.Errorf("error creating %#q: %v", dir, err) 346 } 347 } else if err != nil { 348 return fmt.Errorf("error stat'ing %#q: %v", dir, err) 349 } else if !fi.IsDir() { 350 return fmt.Errorf("%#q already exists and it is not a dir", dir) 351 } 352 return nil 353 } 354 355 func AbbrevHomePaths(fp string) string { 356 h, err := homedir.Dir() 357 if err != nil { 358 return fp 359 } 360 if strings.HasPrefix(fp, h) { 361 return strings.Replace(fp, h, "~", 1) 362 } 363 return fp 364 } 365 366 func NameFlagOrArgument(name string, ctx ActionCtx) string { 367 return nameFlagOrArgument(name, ctx.Args()) 368 } 369 370 func nameFlagOrArgument(name string, args []string) string { 371 if name == "" && len(args) > 0 { 372 return args[0] 373 } 374 return name 375 } 376 377 func MaxArgs(max int) cobra.PositionalArgs { 378 // if we are running in a test, remove the limit 379 if strings.Contains(strings.Join(os.Args, " "), "-test.v") { 380 return nil 381 } 382 return cobra.MaximumNArgs(max) 383 } 384 385 // ExpandPath expands the specified path calls. Resolves ~/ and ./.. paths. 386 func Expand(s string) (string, error) { 387 var err error 388 s, err = homedir.Expand(s) 389 if err != nil { 390 return "", err 391 } 392 return filepath.Abs(s) 393 } 394 395 func PushAccount(u string, accountjwt []byte) (int, []byte, error) { 396 resp, err := http.Post(u, "application/text", bytes.NewReader(accountjwt)) 397 if err != nil { 398 return 0, nil, err 399 } 400 defer resp.Body.Close() 401 data, err := io.ReadAll(resp.Body) 402 return resp.StatusCode, data, err 403 } 404 405 func IsAccountAvailable(status int) bool { 406 return status == http.StatusOK 407 } 408 409 func IsAccountPending(status int) bool { 410 return status > http.StatusOK && status < 300 411 } 412 413 // Validate an operator name 414 func OperatorNameValidator(v string) error { 415 operators := GetConfig().ListOperators() 416 for _, o := range operators { 417 if o == v { 418 r := GetConfig().StoreRoot 419 return fmt.Errorf("an operator named %#q already exists in %#q - specify a different directory with --dir", v, r) 420 } 421 } 422 return nil 423 } 424 425 func AccountJwtURLFromString(asu string, accountSubject string) (string, error) { 426 u, err := url.Parse(asu) 427 if err != nil { 428 return "", err 429 } 430 u.Path = path.Join(u.Path, "accounts", accountSubject) 431 return u.String(), nil 432 } 433 434 func AccountJwtURL(oc *jwt.OperatorClaims, ac *jwt.AccountClaims) (string, error) { 435 if oc.AccountServerURL == "" { 436 return "", fmt.Errorf("error: operator %q doesn't set an account server url", oc.Name) 437 } 438 return AccountJwtURLFromString(oc.AccountServerURL, ac.Subject) 439 } 440 441 func OperatorJwtURLFromString(asu string) (string, error) { 442 u, err := url.Parse(asu) 443 if err != nil { 444 return "", err 445 } 446 u.Path = path.Join(u.Path, "operator") 447 return u.String(), nil 448 } 449 450 func OperatorJwtURL(oc *jwt.OperatorClaims) (string, error) { 451 if oc.AccountServerURL == "" { 452 return "", fmt.Errorf("error: operator %q doesn't set an account server url", oc.Name) 453 } 454 return OperatorJwtURLFromString(oc.AccountServerURL) 455 } 456 457 func IsNatsUrl(url string) bool { 458 return store.IsNatsUrl(url) 459 } 460 461 func IsAccountServerURL(u string) bool { 462 return store.IsAccountServerURL(u) 463 } 464 465 func IsResolverURL(u string) bool { 466 return store.IsResolverURL(u) 467 } 468 469 func ValidSigner(kp nkeys.KeyPair, signers []string) (bool, error) { 470 pk, err := kp.PublicKey() 471 if err != nil { 472 return false, err 473 } 474 ok := false 475 for _, v := range signers { 476 if pk == v { 477 ok = true 478 break 479 } 480 } 481 return ok, nil 482 } 483 484 func GetOperatorSigners(ctx ActionCtx) ([]string, error) { 485 oc, err := ctx.StoreCtx().Store.ReadOperatorClaim() 486 if err != nil { 487 return nil, err 488 } 489 var signers []string 490 if !oc.StrictSigningKeyUsage { 491 signers = append(signers, oc.Subject) 492 } 493 signers = append(signers, oc.SigningKeys...) 494 return signers, nil 495 } 496 497 func diffDates(format string, a, b int64) store.Status { 498 if a != b { 499 as := "always" 500 if a != 0 { 501 as = UnixToDate(a) 502 } 503 bs := "always" 504 if b != 0 { 505 bs = UnixToDate(b) 506 } 507 v := fmt.Sprintf("from %s to %s", as, bs) 508 return store.NewServerMessage(format, v) 509 } 510 return nil 511 } 512 513 func limitToString(v int64) string { 514 switch v { 515 case -1: 516 return "unlimited" 517 default: 518 return fmt.Sprintf("%d", v) 519 } 520 } 521 522 func diffNumber(format string, a, b int64) store.Status { 523 if a != b { 524 as := limitToString(a) 525 bs := limitToString(b) 526 v := fmt.Sprintf("from %s to %s", as, bs) 527 return store.NewServerMessage(format, v) 528 } 529 return nil 530 } 531 532 func diffBool(format string, a, b bool) store.Status { 533 if a != b { 534 v := fmt.Sprintf("from %t to %t", a, b) 535 return store.NewServerMessage(format, v) 536 } 537 return nil 538 } 539 540 func DiffAccountLimits(a *jwt.AccountClaims, b *jwt.AccountClaims) store.Status { 541 r := store.NewReport(store.WARN, "account server modifications") 542 r.Add(diffDates("jwt start changed %s", a.NotBefore, b.NotBefore)) 543 r.Add(diffDates("jwt expiry changed %s", a.NotBefore, b.NotBefore)) 544 r.Add(diffNumber("max subscriptions changed %s", a.Limits.Subs, b.Limits.Subs)) 545 r.Add(diffNumber("max connections changed %s", a.Limits.Conn, b.Limits.Conn)) 546 r.Add(diffNumber("max leaf node connections changed %s", a.Limits.LeafNodeConn, b.Limits.LeafNodeConn)) 547 r.Add(diffNumber("max imports changed %s", a.Limits.Imports, b.Limits.Imports)) 548 r.Add(diffNumber("max exports changed %s", a.Limits.Exports, b.Limits.Exports)) 549 r.Add(diffNumber("max data changed %s", a.Limits.Data, b.Limits.Data)) 550 r.Add(diffNumber("max message payload changed %s", a.Limits.Payload, b.Limits.Payload)) 551 r.Add(diffBool("allow wildcard exports changed %s", a.Limits.WildcardExports, b.Limits.WildcardExports)) 552 if len(r.Details) == 0 { 553 return nil 554 } 555 return r 556 } 557 558 func StoreAccountAndUpdateStatus(ctx ActionCtx, token string, status *store.Report) { 559 rs, err := ctx.StoreCtx().Store.StoreClaim([]byte(token)) 560 // the order of the messages benefits from adding the status first 561 if rs != nil { 562 status.Add(rs) 563 } 564 if err != nil { 565 status.AddFromError(err) 566 } 567 } 568 569 func promptDuration(label string, defaultValue time.Duration) (time.Duration, error) { 570 value, err := cli.Prompt(label, defaultValue.String()) 571 if err != nil { 572 return time.Duration(0), err 573 } 574 if value == "" { 575 return time.Duration(0), nil 576 } 577 return time.ParseDuration(value) 578 } 579 580 type dateTime int64 581 582 func (t *dateTime) Set(val string) error { 583 if strings.TrimSpace(val) == "0" { 584 *t = 0 585 return nil 586 } 587 if v, err := time.Parse(time.RFC3339, val); err == nil { 588 *t = dateTime(v.Unix()) 589 return nil 590 } 591 num, err := strconv.ParseInt(val, 10, 64) 592 if err != nil { 593 return fmt.Errorf("provided value %q is not a number nor parsable as RFC3339", val) 594 } 595 *t = dateTime(num) 596 return nil 597 } 598 599 func (t *dateTime) String() string { 600 v := time.Unix(int64(*t), 0) 601 return v.Format(time.RFC3339) 602 } 603 604 func (t *dateTime) Type() string { 605 return "date-time" 606 } 607 608 func Debug(label string, v interface{}) { 609 d, err := json.MarshalIndent(v, "", " ") 610 if err != nil { 611 panic(err) 612 } 613 fmt.Println(label, string(d)) 614 } 615 616 var ( 617 englishCaser cases.Caser 618 caserOnce sync.Once 619 ) 620 621 func TitleCase(s string) string { 622 caserOnce.Do(func() { 623 englishCaser = cases.Title(language.English) 624 }) 625 return englishCaser.String(s) 626 }