Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/rtb-buddy
/.idea/
/coverage.out
/rtblab
1 change: 1 addition & 0 deletions cmd/rtb-buddy/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -266,3 +266,4 @@ Chain verification: OK
----

include::rbfs.adoc[]
include::voucher.adoc[]
32 changes: 32 additions & 0 deletions cmd/rtb-buddy/cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package cmd

import (
"fmt"
"log/slog"
"os"

"github.com/rtbrick/tools/cmd/rtb-buddy/config"
"github.com/rtbrick/tools/pkg/cmd/apigw"
"github.com/rtbrick/tools/pkg/cmd/lix1"
"github.com/rtbrick/tools/pkg/cmd/rbfs"
"github.com/rtbrick/tools/pkg/cmd/voucher"
"github.com/spf13/cobra"
)

var logLevel string

func NewRootCmd(silenceErrors, silenceUsage bool) *cobra.Command {
cmd := &cobra.Command{
Use: "rtb-buddy",
Expand All @@ -19,9 +24,36 @@ func NewRootCmd(silenceErrors, silenceUsage bool) *cobra.Command {
}
cmd.SetOut(os.Stdout)
cmd.Version = config.VERSION
cmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "Log level (debug, info, warn, error)")
if err := cmd.RegisterFlagCompletionFunc("log-level", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"debug", "info", "warn", "error"}, cobra.ShellCompDirectiveNoFileComp
}); err != nil {
panic(err)
}
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
var level slog.Level
switch logLevel {
case "debug":
level = slog.LevelDebug
case "info":
level = slog.LevelInfo
case "warn":
level = slog.LevelWarn
case "error":
level = slog.LevelError
default:
return fmt.Errorf("unknown log level %q", logLevel)
}
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
AddSource: false,
Level: level,
})))
return nil
}
cmd.AddCommand(apigw.NewApigwCmd())
cmd.AddCommand(lix1.NewLix1Cmd())
cmd.AddCommand(rbfs.NewRbfsCmd())
cmd.AddCommand(voucher.NewVoucherCmd())
cmd.AddCommand(newVersionCmd())
return cmd
}
5 changes: 0 additions & 5 deletions cmd/rtb-buddy/main.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
package main

import (
"log/slog"
"os"

"github.com/rtbrick/tools/cmd/rtb-buddy/cmd"
)

func main() {
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
AddSource: false,
Level: slog.LevelDebug,
})))
if err := cmd.NewRootCmd(false, false).Execute(); err != nil {
os.Exit(1)
}
Expand Down
49 changes: 49 additions & 0 deletions cmd/rtb-buddy/voucher.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
=== voucher

Manages vouchers and licenses.

==== voucher request license

Requests a license from a voucher.

* `--voucher` — Voucher JWT string (required)
* `--fingerprint` — Device fingerprint (required)
* `--role` — Device role (required)
* `--model` — Hardware model (required)
* `--image-id` — Image ID (optional)
* `-o, --output-jwks` — Output path for JWKS file (only written if set)

[source,sh]
----
# Request a license
rtb-buddy voucher request license \
--voucher <jwt> \
--fingerprint SERIAL-123456 \
--role multiservice-edge \
--model "ufispace_S9510-28DC" \
--image-id my-image-id

# Request a license and write JWKS to a file
rtb-buddy voucher request license \
--voucher <jwt> \
--fingerprint SERIAL-123456 \
--role multiservice-edge \
--model "ufispace_S9510-28DC" \
-o jwks.json
----

==== voucher request jwks

Requests JWKS from a voucher.

* `--voucher` — Voucher JWT string (required)
* `-o, --output-jwks` — Output path for JWKS file (only written if set)

[source,sh]
----
# Print JWKS to stdout
rtb-buddy voucher request jwks --voucher <jwt>

# Write JWKS to a file
rtb-buddy voucher request jwks --voucher <jwt> -o jwks.json
----
42 changes: 19 additions & 23 deletions pkg/cmd/rbfs/lix1_ping.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ package rbfs

import (
"bytes"
"crypto/tls"
"encoding/xml"
"fmt"
"io"
"net/http"
"net/url"
"time"

"github.com/spf13/cobra"

"github.com/rtbrick/tools/pkg/httpclient"
)

// buildLix1dPingBody builds the XML PingRequest body for rbfs lix1 ping
Expand Down Expand Up @@ -73,34 +74,29 @@ func NewLix1PingCmd() *cobra.Command {
}

// Build HTTP client
tlsConfig := &tls.Config{
InsecureSkipVerify: insecure,
}
if cert != "" && certKey != "" {
// Load client certificate for mTLS
certData, err := tls.LoadX509KeyPair(cert, certKey)
if err != nil {
return fmt.Errorf("load client cert/key: %w", err)
}
tlsConfig.Certificates = append(tlsConfig.Certificates, certData)
element := path
baseURL, err := url.Parse("https://" + element + ":12322")
if err != nil {
return fmt.Errorf("parse base URL: %w", err)
}

// Parse element from path (could be hostname or include more details)
element := path
url := fmt.Sprintf("https://%s:12322/X1/NE", element)
opts := []httpclient.Option{
httpclient.WithClientCert(cert, certKey),
}
if insecure {
opts = append(opts, httpclient.WithInsecureSkipVerify())
}
hc := httpclient.NewClient(baseURL, opts...)

req, err := http.NewRequest("POST", url, bytes.NewReader(body))
req, err := hc.NewRequestWithContext(cmd.Context(), "POST", "/X1/NE",
bytes.NewReader(body),
httpclient.WithHeader("Content-Type", "application/xml"),
)
if err != nil {
return fmt.Errorf("create request: %w", err)
}
req.Header.Set("Content-Type", "application/xml")

client := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{TLSClientConfig: tlsConfig},
}

resp, err := client.Do(req)
resp, err := hc.Do(req)
if err != nil {
return fmt.Errorf("do request: %w", err)
}
Expand Down
10 changes: 10 additions & 0 deletions pkg/cmd/voucher/flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package voucher

const (
flagVoucher = "voucher"
flagFingerprint = "fingerprint"
flagRole = "role"
flagModel = "model"
flagImageID = "image-id"
flagOutput = "output"
)
73 changes: 73 additions & 0 deletions pkg/cmd/voucher/jwks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package voucher

import (
"context"
"encoding/json"
"fmt"
"net/url"
"os"
"time"

"github.com/rtbrick/tools/pkg/license"
"github.com/spf13/cobra"
)

func NewJWKSCmd() *cobra.Command {
var voucher string
var output string

cmd := &cobra.Command{
Use: "jwks",
Short: "Request JWKS from a voucher",
RunE: func(cmd *cobra.Command, args []string) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

status, err := license.DecodeVoucher(voucher)
if err != nil {
return fmt.Errorf("failed to decode voucher: %w", err)
}

if status.CustomClaims == nil || status.CustomClaims.LicenseServerURL == nil {
return fmt.Errorf("voucher does not contain a license server URL")
}

ls, err := url.Parse(*status.CustomClaims.LicenseServerURL)
if err != nil {
return fmt.Errorf("failed to parse license server URL: %w", err)
}

client := license.NewDefaultLicenseClient(voucher, ls)

jwks, err := client.RequestJWKS(ctx)
if err != nil {
return fmt.Errorf("failed to request JWKS: %w", err)
}

jwksJSON, err := json.MarshalIndent(jwks, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal JWKS: %w", err)
}

if output != "" {
if err := os.WriteFile(output, jwksJSON, 0644); err != nil {
return fmt.Errorf("failed to write JWKS to %s: %w", output, err)
}
cmd.Printf("JWKS written to %s\n", output)
} else {
cmd.Println(string(jwksJSON))
}

return nil
},
}

cmd.Flags().StringVar(&voucher, flagVoucher, "", "Voucher JWT string")
cmd.Flags().StringVar(&output, flagOutput, "", "Output path for JWKS file (only written if set)")

if err := cmd.MarkFlagRequired(flagVoucher); err != nil {
panic(err)
}

return cmd
}
Loading
Loading