Validates and activates the key like PUT /api/v1/license (same service call, audited as the system actor) and prints the license state without the key. Exit codes: 2 bad input, 3 key rejected, 4 license server unreachable or untrusted (nothing stored). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
172 lines
5.2 KiB
Go
172 lines
5.2 KiB
Go
// SPDX-License-Identifier: LicenseRef-DDI-Proprietary
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.beugsiud.de/beug.k/DDI/internal/brand"
|
|
"git.beugsiud.de/beug.k/DDI/server/internal/app"
|
|
"git.beugsiud.de/beug.k/DDI/server/internal/config"
|
|
"git.beugsiud.de/beug.k/DDI/server/internal/service"
|
|
)
|
|
|
|
// Exit codes of `license set-key` besides 0 (stored) and 1 (other errors,
|
|
// e.g. configuration or database). The installer uses them to decide
|
|
// whether to warn or to fail.
|
|
const (
|
|
exitUsage = 2 // bad arguments, empty or oversized input
|
|
exitKeyRejected = 3 // the license service rejected the key
|
|
exitLicenseSvc = 4 // the license service is unreachable or untrusted
|
|
)
|
|
|
|
// maxKeyInput bounds what is read from stdin; maxKeyLength matches the
|
|
// limit of the license manager.
|
|
const (
|
|
maxKeyInput = 4096
|
|
maxKeyLength = 512
|
|
)
|
|
|
|
// exitError carries a specific exit code.
|
|
type exitError struct {
|
|
code int
|
|
err error
|
|
}
|
|
|
|
func (e *exitError) Error() string { return e.err.Error() }
|
|
func (e *exitError) Unwrap() error { return e.err }
|
|
|
|
func licenseUsage(w io.Writer) {
|
|
_, _ = fmt.Fprintf(w, `Usage: %[1]s license set-key < keyfile
|
|
|
|
set-key read a license key from stdin, validate and activate it online
|
|
(like PUT /api/v1/license) and print the resulting license state
|
|
|
|
Exit codes: 0 activated, 1 other error, 2 empty or oversized input,
|
|
3 key rejected, 4 license server unreachable (nothing was changed).
|
|
`, brand.ServerBinary)
|
|
}
|
|
|
|
func cmdLicense(ctx context.Context, args []string, lookup config.LookupFunc, stdin io.Reader, stdout, logOut io.Writer) error {
|
|
if len(args) == 0 || args[0] == "help" || args[0] == "-h" || args[0] == "--help" {
|
|
if len(args) == 0 {
|
|
licenseUsage(logOut)
|
|
return &exitError{code: exitUsage, err: errors.New("license: missing subcommand")}
|
|
}
|
|
licenseUsage(stdout)
|
|
return nil
|
|
}
|
|
switch args[0] {
|
|
case "set-key":
|
|
if len(args) > 1 {
|
|
return &exitError{code: exitUsage, err: errors.New("license set-key takes no arguments; pass the key on stdin")}
|
|
}
|
|
return cmdLicenseSetKey(ctx, lookup, stdin, stdout, logOut)
|
|
default:
|
|
licenseUsage(logOut)
|
|
return &exitError{code: exitUsage, err: fmt.Errorf("license: unknown subcommand %q", args[0])}
|
|
}
|
|
}
|
|
|
|
// readKey reads the license key from r: trimmed, not empty, bounded.
|
|
func readKey(r io.Reader) (string, error) {
|
|
data, err := io.ReadAll(io.LimitReader(r, maxKeyInput+1))
|
|
if err != nil {
|
|
return "", fmt.Errorf("read license key from stdin: %w", err)
|
|
}
|
|
if len(data) > maxKeyInput {
|
|
return "", &exitError{code: exitUsage, err: fmt.Errorf("input on stdin is larger than %d bytes; expected a single license key", maxKeyInput)}
|
|
}
|
|
key := string(bytes.TrimSpace(data))
|
|
switch {
|
|
case key == "":
|
|
return "", &exitError{code: exitUsage, err: errors.New("no license key on stdin")}
|
|
case len(key) > maxKeyLength:
|
|
return "", &exitError{code: exitUsage, err: fmt.Errorf("license key is longer than %d characters", maxKeyLength)}
|
|
case strings.ContainsAny(key, "\r\n"):
|
|
return "", &exitError{code: exitUsage, err: errors.New("expected a single license key, got several lines")}
|
|
}
|
|
return key, nil
|
|
}
|
|
|
|
func cmdLicenseSetKey(ctx context.Context, lookup config.LookupFunc, stdin io.Reader, stdout, logOut io.Writer) error {
|
|
if f, ok := stdin.(*os.File); ok {
|
|
if fi, err := f.Stat(); err == nil && fi.Mode()&os.ModeCharDevice != 0 {
|
|
_, _ = fmt.Fprintln(logOut, "reading the license key from stdin (finish with Ctrl-D)")
|
|
}
|
|
}
|
|
key, err := readKey(stdin)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cfg, log, err := setup(lookup, logOut)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
core, err := app.Open(ctx, cfg, log, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer core.Close()
|
|
st, err := core.Service.SetLicenseKey(ctx, service.SystemActor, key)
|
|
if err != nil {
|
|
return licenseCLIError(err)
|
|
}
|
|
return printLicense(stdout, st)
|
|
}
|
|
|
|
// licenseCLIError maps service errors to messages and exit codes.
|
|
func licenseCLIError(err error) error {
|
|
var se *service.Error
|
|
if !errors.As(err, &se) {
|
|
return err
|
|
}
|
|
msg := se.Message
|
|
if pc := se.Params["provider_code"]; pc != "" {
|
|
msg += " (" + pc + ")"
|
|
}
|
|
switch se.Kind {
|
|
case service.KindUnavailable:
|
|
return &exitError{code: exitLicenseSvc, err: fmt.Errorf("license key not stored: %s [%s]", msg, se.Code)}
|
|
case service.KindInvalid, service.KindConflict:
|
|
return &exitError{code: exitKeyRejected, err: fmt.Errorf("license key rejected: %s [%s]", msg, se.Code)}
|
|
}
|
|
return err
|
|
}
|
|
|
|
func printLicense(w io.Writer, st service.LicenseStatus) error {
|
|
limit := func(v *int) string {
|
|
if v == nil {
|
|
return "unlimited"
|
|
}
|
|
return strconv.Itoa(*v)
|
|
}
|
|
day := func(t *time.Time) string {
|
|
if t == nil {
|
|
return "never"
|
|
}
|
|
return t.UTC().Format(time.RFC3339)
|
|
}
|
|
features := strings.Join(st.Features, ", ")
|
|
if features == "" {
|
|
features = "-"
|
|
}
|
|
name := st.Name
|
|
if st.LicenseID != "" {
|
|
name = strings.TrimSpace(name + " (" + st.LicenseID + ")")
|
|
}
|
|
_, err := fmt.Fprintf(w, "status: %s\nreason: %s\nlicense: %s\nexpires: %s\n"+
|
|
"max agents: %s\nmax addresses: %s (in use: %d)\nfeatures: %s\n",
|
|
st.State, st.Reason, name, day(st.ExpiresAt), limit(st.Limits.MaxAgents), limit(st.Limits.MaxAddresses),
|
|
st.Usage.Addresses, features)
|
|
return err
|
|
}
|