snapshot of https://github.com/centrifugal/centrifugo.git (master, 2026-09-16; blobs > 131072B omitted)
This commit is contained in:
48
internal/cli/checkconfig.go
Normal file
48
internal/cli/checkconfig.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/config"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func CheckConfig() *cobra.Command {
|
||||
var checkConfigFile string
|
||||
var checkConfigStrict bool
|
||||
var checkConfigCmd = &cobra.Command{
|
||||
Use: "checkconfig",
|
||||
Short: "Check configuration file",
|
||||
Long: `Check Centrifugo configuration file`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
checkConfig(cmd, checkConfigFile, checkConfigStrict)
|
||||
},
|
||||
}
|
||||
checkConfigCmd.Flags().StringVarP(&checkConfigFile, "config", "c", "config.json", "path to config file to check")
|
||||
checkConfigCmd.Flags().BoolVarP(&checkConfigStrict, "strict", "s", false, "strict check - fail on unknown fields")
|
||||
return checkConfigCmd
|
||||
}
|
||||
|
||||
func checkConfig(cmd *cobra.Command, checkConfigFile string, strict bool) {
|
||||
cfg, cfgMeta, err := config.GetConfig(cmd, checkConfigFile)
|
||||
if err != nil {
|
||||
fmt.Printf("error getting config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if cfgMeta.FileNotFound {
|
||||
fmt.Println("config file not found")
|
||||
os.Exit(1)
|
||||
}
|
||||
err = cfg.Validate()
|
||||
if err != nil {
|
||||
fmt.Printf("error validating config: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if strict && len(cfgMeta.UnknownKeys) > 0 {
|
||||
fmt.Printf("unknown keys in config: %v\n", strings.Join(cfgMeta.UnknownKeys, ", "))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
95
internal/cli/checksubtoken.go
Normal file
95
internal/cli/checksubtoken.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/config"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/confighelpers"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/jwtverify"
|
||||
|
||||
"github.com/cristalhq/jwt/v5"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func CheckSubToken() *cobra.Command {
|
||||
var checkSubTokenConfigFile string
|
||||
var checkSubTokenCmd = &cobra.Command{
|
||||
Use: "checksubtoken [TOKEN]",
|
||||
Short: "Check subscription JWT",
|
||||
Long: `Check subscription JWT`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
checkSubToken(cmd, checkSubTokenConfigFile, args)
|
||||
},
|
||||
}
|
||||
checkSubTokenCmd.Flags().StringVarP(&checkSubTokenConfigFile, "config", "c", "config.json", "path to config file")
|
||||
return checkSubTokenCmd
|
||||
}
|
||||
|
||||
func checkSubToken(cmd *cobra.Command, checkSubTokenConfigFile string, args []string) {
|
||||
cfg, _, err := config.GetConfig(cmd, checkSubTokenConfigFile)
|
||||
if err != nil {
|
||||
fmt.Printf("error getting config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
verifierConfig, err := confighelpers.MakeVerifierConfig(cfg.Client.Token)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if cfg.Client.SubscriptionToken.Enabled {
|
||||
verifierConfig, err = confighelpers.MakeVerifierConfig(cfg.Client.SubscriptionToken.Token)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
if len(args) != 1 {
|
||||
fmt.Printf("error: provide token to check [centrifugo checksubtoken <TOKEN>]\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
subject, channel, claims, err := parseAndVerifySubToken(verifierConfig, cfg, args[0])
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
var user = fmt.Sprintf("user \"%s\"", subject)
|
||||
if subject == "" {
|
||||
user = "anonymous user"
|
||||
}
|
||||
fmt.Printf("valid subscription token for %s and channel \"%s\"\npayload: %s\n", user, channel, string(claims))
|
||||
}
|
||||
|
||||
// parseAndVerifySubToken checks subscription JWT for user.
|
||||
func parseAndVerifySubToken(config jwtverify.VerifierConfig, cfg config.Config, t string) (string, string, []byte, error) {
|
||||
token, err := jwt.ParseNoVerify([]byte(t)) // Will be verified later.
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
var claims jwtverify.SubscribeTokenClaims
|
||||
err = json.Unmarshal(token.Claims(), &claims)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
ct, err := verifySub(config, cfg, t)
|
||||
if err != nil {
|
||||
return "", "", nil, fmt.Errorf("token with algorithm %s and claims %s has error: %v", token.Header().Algorithm, string(token.Claims()), err)
|
||||
}
|
||||
|
||||
return ct.UserID, ct.Channel, token.Claims(), nil
|
||||
}
|
||||
|
||||
func verifySub(verifierConf jwtverify.VerifierConfig, cfg config.Config, token string) (jwtverify.SubscribeToken, error) {
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
if err != nil {
|
||||
return jwtverify.SubscribeToken{}, err
|
||||
}
|
||||
verifier, err := jwtverify.NewTokenVerifierJWT(verifierConf, cfgContainer)
|
||||
if err != nil {
|
||||
return jwtverify.SubscribeToken{}, err
|
||||
}
|
||||
return verifier.VerifySubscribeToken(token, false)
|
||||
}
|
||||
88
internal/cli/checktoken.go
Normal file
88
internal/cli/checktoken.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/config"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/confighelpers"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/jwtverify"
|
||||
|
||||
"github.com/cristalhq/jwt/v5"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func CheckToken() *cobra.Command {
|
||||
var checkTokenConfigFile string
|
||||
var checkTokenCmd = &cobra.Command{
|
||||
Use: "checktoken [TOKEN]",
|
||||
Short: "Check connection JWT",
|
||||
Long: `Check connection JWT`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
checkToken(cmd, checkTokenConfigFile, args)
|
||||
},
|
||||
}
|
||||
checkTokenCmd.Flags().StringVarP(&checkTokenConfigFile, "config", "c", "config.json", "path to config file")
|
||||
return checkTokenCmd
|
||||
}
|
||||
|
||||
func checkToken(cmd *cobra.Command, checkTokenConfigFile string, args []string) {
|
||||
cfg, _, err := config.GetConfig(cmd, checkTokenConfigFile)
|
||||
if err != nil {
|
||||
fmt.Printf("error getting config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
verifierConfig, err := confighelpers.MakeVerifierConfig(cfg.Client.Token)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if len(args) != 1 {
|
||||
fmt.Printf("error: provide token to check [centrifugo checktoken <TOKEN>]\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
subject, claims, err := parseAndVerifyToken(verifierConfig, cfg, args[0])
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
var user = fmt.Sprintf("user %s", subject)
|
||||
if subject == "" {
|
||||
user = "anonymous user"
|
||||
}
|
||||
fmt.Printf("valid token for %s\npayload: %s\n", user, string(claims))
|
||||
}
|
||||
|
||||
// parseAndVerifyToken checks JWT for user.
|
||||
func parseAndVerifyToken(config jwtverify.VerifierConfig, ruleConfig config.Config, t string) (string, []byte, error) {
|
||||
token, err := jwt.ParseNoVerify([]byte(t)) // Will be verified later.
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
claims := &jwt.RegisteredClaims{}
|
||||
err = json.Unmarshal(token.Claims(), claims)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
ct, err := verify(config, ruleConfig, t)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("token with algorithm %s and claims %s has error: %v", token.Header().Algorithm, string(token.Claims()), err)
|
||||
}
|
||||
|
||||
return ct.UserID, token.Claims(), nil
|
||||
}
|
||||
|
||||
func verify(verifierConf jwtverify.VerifierConfig, cfg config.Config, token string) (jwtverify.ConnectToken, error) {
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
if err != nil {
|
||||
return jwtverify.ConnectToken{}, err
|
||||
}
|
||||
verifier, err := jwtverify.NewTokenVerifierJWT(verifierConf, cfgContainer)
|
||||
if err != nil {
|
||||
return jwtverify.ConnectToken{}, err
|
||||
}
|
||||
return verifier.VerifyConnectToken(token, false)
|
||||
}
|
||||
352
internal/cli/configdoc.go
Normal file
352
internal/cli/configdoc.go
Normal file
@@ -0,0 +1,352 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/build"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/config"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/configtypes"
|
||||
|
||||
"github.com/pkg/browser"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/yuin/goldmark"
|
||||
)
|
||||
|
||||
//go:embed configdoc/template.html
|
||||
var htmlTemplate string
|
||||
|
||||
func ConfigDoc() *cobra.Command {
|
||||
var mdOutput bool
|
||||
var section string
|
||||
var port int
|
||||
var baseLevel int
|
||||
var configDocCmd = &cobra.Command{
|
||||
Use: "configdoc",
|
||||
Short: "Show Centrifugo configuration documentation generated from source code",
|
||||
Long: `Show Centrifugo configuration documentation generated from source code`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
configDoc(port, mdOutput, section, baseLevel)
|
||||
},
|
||||
}
|
||||
configDocCmd.Flags().BoolVarP(&mdOutput, "markdown", "m", false, "output markdown to stdout")
|
||||
configDocCmd.Flags().StringVarP(§ion, "section", "s", "", "filter by top-level section name")
|
||||
configDocCmd.Flags().IntVarP(&port, "port", "p", 6060, "port to run server on")
|
||||
configDocCmd.Flags().IntVarP(&baseLevel, "base-level", "l", 0, "base level to use")
|
||||
return configDocCmd
|
||||
}
|
||||
|
||||
func configDoc(port int, mdOutput bool, section string, baseLevel int) {
|
||||
// Documentation is built from the config structs at runtime — descriptions
|
||||
// come from the `doc` struct tags, types and defaults from reflection.
|
||||
docs := documentStruct(config.Config{}, "", 1)
|
||||
|
||||
if mdOutput {
|
||||
mdContent := convertDocsToMarkdown(docs, section, baseLevel, mdOutput)
|
||||
fmt.Println(mdContent)
|
||||
return
|
||||
}
|
||||
|
||||
// Filter docs by section if specified
|
||||
filteredDocs := docs
|
||||
if section != "" {
|
||||
filteredDocs = make([]FieldDoc, 0)
|
||||
for _, doc := range docs {
|
||||
if strings.HasPrefix(doc.Field, section) {
|
||||
filteredDocs = append(filteredDocs, doc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
url := "http://localhost:" + strconv.Itoa(port)
|
||||
// OpenURL will open the default browser.
|
||||
if err := browser.OpenURL(url); err != nil {
|
||||
fmt.Printf("Failed to open browser automatically: %v, see %s\n", err, url)
|
||||
}
|
||||
}()
|
||||
|
||||
http.HandleFunc("/", makeMarkdownHandler(filteredDocs))
|
||||
if err := http.ListenAndServe(":"+strconv.Itoa(port), nil); err != nil && !errors.Is(err, http.ErrServerClosed) { //nolint:gosec // Only for development use.
|
||||
fmt.Println("Failed to start server:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// FieldDoc represents the JSON documentation for a configuration field.
|
||||
type FieldDoc struct {
|
||||
Field string `json:"field"`
|
||||
Name string `json:"name"`
|
||||
GoName string `json:"go_name"`
|
||||
Level int `json:"level"`
|
||||
Type string `json:"type"`
|
||||
Default string `json:"default"`
|
||||
Comment string `json:"comment"`
|
||||
IsComplexType bool `json:"is_complex_type"`
|
||||
Children []FieldDoc `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
// ConvertDocsToMarkdown recursively converts a slice of FieldDoc entries into Markdown.
|
||||
func convertDocsToMarkdown(docs []FieldDoc, section string, baseLevel int, mdOutput bool) string {
|
||||
var sb strings.Builder
|
||||
for _, doc := range docs {
|
||||
if section != "" && !strings.HasPrefix(doc.Field, section) {
|
||||
continue
|
||||
}
|
||||
// Generate a header with the appropriate Markdown level.
|
||||
fieldLevel := doc.Level + baseLevel
|
||||
if fieldLevel > 6 {
|
||||
fieldLevel = 6
|
||||
}
|
||||
header := strings.Repeat("#", fieldLevel)
|
||||
|
||||
typeDesc := fmt.Sprintf("Type: `%s`", doc.Type)
|
||||
if doc.IsComplexType {
|
||||
typeDesc = fmt.Sprintf("Type: `%s` object", doc.Type)
|
||||
}
|
||||
if doc.Default != "" {
|
||||
typeDesc += fmt.Sprintf(". Default: `%s`", doc.Default)
|
||||
}
|
||||
|
||||
var envVar string
|
||||
if !strings.Contains(doc.Field, "[]") {
|
||||
if !doc.IsComplexType {
|
||||
envVar = "CENTRIFUGO_" + strings.ToUpper(strings.ReplaceAll(doc.Field, `.`, "_"))
|
||||
} else {
|
||||
if strings.Contains(doc.Field, "namespaces") || strings.Contains(doc.Field, "proxies") || strings.Contains(doc.Field, "consumers") {
|
||||
envVar = "CENTRIFUGO_" + strings.ToUpper(strings.ReplaceAll(doc.Field, `.`, "_"))
|
||||
} else if doc.Default == "[]" {
|
||||
envVar = "CENTRIFUGO_" + strings.ToUpper(strings.ReplaceAll(doc.Field, `.`, "_"))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !strings.HasSuffix(doc.Field, ".name") &&
|
||||
(strings.HasPrefix(doc.Field, "namespaces") ||
|
||||
strings.HasPrefix(doc.Field, "proxies") ||
|
||||
strings.HasPrefix(doc.Field, "consumers")) &&
|
||||
(!doc.IsComplexType || doc.Default == "[]") {
|
||||
field := strings.Replace(doc.Field, `[]`, "_<NAME>", 1)
|
||||
envVar = "CENTRIFUGO_" + strings.ToUpper(strings.ReplaceAll(field, `.`, "_"))
|
||||
}
|
||||
if strings.Contains(envVar, "[]") {
|
||||
envVar = ""
|
||||
}
|
||||
}
|
||||
|
||||
if envVar != "" {
|
||||
typeDesc += fmt.Sprintf("\n\nEnv: `%s`", envVar)
|
||||
}
|
||||
|
||||
comment := doc.Comment
|
||||
if comment == "" {
|
||||
comment = "No documentation available."
|
||||
}
|
||||
if strings.HasPrefix(comment, doc.GoName) {
|
||||
comment = fmt.Sprintf("`%s`%s", doc.Name, comment[len(doc.GoName):])
|
||||
}
|
||||
|
||||
field := fmt.Sprintf("`%s`", doc.Field)
|
||||
if !mdOutput && doc.IsComplexType {
|
||||
if doc.Default == "[]" {
|
||||
field += " `[...]`"
|
||||
} else {
|
||||
field += " `{...}`"
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("%s %s\n\n%s\n\n%s\n\n", header, field, typeDesc, comment))
|
||||
if len(doc.Children) > 0 {
|
||||
sb.WriteString(convertDocsToMarkdown(doc.Children, section, baseLevel, mdOutput))
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func makeMarkdownHandler(docs []FieldDoc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if simple markdown view is requested
|
||||
if r.URL.Query().Get("simple") == "1" {
|
||||
serveSimpleMarkdownView(w, docs)
|
||||
return
|
||||
}
|
||||
|
||||
// Convert docs to JSON for JavaScript consumption
|
||||
configJSON, err := json.Marshal(docs)
|
||||
if err != nil {
|
||||
http.Error(w, "Error marshaling config data: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the HTML template
|
||||
tmpl, err := template.New("config").Parse(htmlTemplate)
|
||||
if err != nil {
|
||||
http.Error(w, "Error parsing template: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Prepare template data
|
||||
data := struct {
|
||||
Version string
|
||||
Content template.HTML
|
||||
ConfigJSON template.JS
|
||||
}{
|
||||
Version: build.Version,
|
||||
Content: "", // Content is now generated by JavaScript.
|
||||
ConfigJSON: template.JS(configJSON), //nolint:gosec // Trusted data.
|
||||
}
|
||||
|
||||
// Execute template
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := tmpl.Execute(w, data); err != nil {
|
||||
http.Error(w, "Error executing template: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func serveSimpleMarkdownView(w http.ResponseWriter, docs []FieldDoc) {
|
||||
mdContent := convertDocsToMarkdown(docs, "", 0, false)
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := goldmark.Convert([]byte(mdContent), &buf); err != nil {
|
||||
http.Error(w, "Error converting markdown: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
simpleHTML := fmt.Sprintf(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Centrifugo %s Configuration Options (Simple View)</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.0/dist/materia/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body { padding: 2rem; }
|
||||
.markdown-body { max-width: 1024px; margin: auto; }
|
||||
code { color: #139f87; background-color: #f8f9fa; padding: 0.2rem 0.4rem; border-radius: 0.25rem; }
|
||||
.controls { text-align: center; margin-bottom: 2rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container markdown-body">
|
||||
<div class="controls">
|
||||
<a href="/" class="btn btn-primary">Switch to Enhanced View</a>
|
||||
</div>
|
||||
%s
|
||||
</div>
|
||||
</body>
|
||||
</html>`, build.Version, buf.String())
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_, _ = w.Write([]byte(simpleHTML))
|
||||
}
|
||||
|
||||
var docCodeRe = regexp.MustCompile(`<<(.+?)>>`)
|
||||
|
||||
// convertDocCodes turns <<value>> markers in doc text into Markdown inline code.
|
||||
func convertDocCodes(s string) string {
|
||||
return docCodeRe.ReplaceAllString(s, "`$1`")
|
||||
}
|
||||
|
||||
// getDisplayType returns the base type name and whether it's a complex (object)
|
||||
// type. Both the type label and the complex/object classification are shared via
|
||||
// configtypes so configdoc and the admin config UI (Pro) stay consistent.
|
||||
func getDisplayType(t reflect.Type) (string, bool) {
|
||||
return configtypes.TypeLabel(t), configtypes.IsComplexType(t)
|
||||
}
|
||||
|
||||
// documentStruct walks a config struct via reflection and builds the field
|
||||
// documentation tree at runtime — descriptions from the `doc` tag, types and
|
||||
// defaults from reflection. Fields tagged `expose:"-"` are excluded.
|
||||
func documentStruct(cfg interface{}, parentKey string, level int) []FieldDoc {
|
||||
var docs []FieldDoc
|
||||
t := reflect.TypeOf(cfg)
|
||||
if t.Kind() == reflect.Pointer {
|
||||
t = t.Elem()
|
||||
}
|
||||
if t.Kind() != reflect.Struct {
|
||||
return docs
|
||||
}
|
||||
|
||||
fieldLevel := level
|
||||
if parentKey != "" {
|
||||
fieldLevel = level + 1
|
||||
}
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
if field.Tag.Get("json") == "-" {
|
||||
continue
|
||||
}
|
||||
// Flatten squashed/embedded structs.
|
||||
if configtypes.IsSquash(field) {
|
||||
var nested interface{}
|
||||
if field.Type.Kind() == reflect.Pointer && field.Type.Elem().Kind() == reflect.Struct {
|
||||
nested = reflect.New(field.Type.Elem()).Interface()
|
||||
} else if field.Type.Kind() == reflect.Struct {
|
||||
nested = reflect.New(field.Type).Interface()
|
||||
}
|
||||
if nested != nil {
|
||||
docs = append(docs, documentStruct(nested, parentKey, level)...)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if field.Tag.Get("expose") == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
keyTag := configtypes.JSONKey(field)
|
||||
fullKey := keyTag
|
||||
if parentKey != "" {
|
||||
fullKey = parentKey + "." + keyTag
|
||||
}
|
||||
|
||||
displayType, isComplex := getDisplayType(field.Type)
|
||||
docEntry := FieldDoc{
|
||||
Field: fullKey,
|
||||
Name: keyTag,
|
||||
GoName: field.Name,
|
||||
Level: fieldLevel,
|
||||
Type: displayType,
|
||||
Default: field.Tag.Get("default"),
|
||||
IsComplexType: isComplex,
|
||||
Comment: convertDocCodes(field.Tag.Get("doc")),
|
||||
}
|
||||
|
||||
var children []FieldDoc
|
||||
switch field.Type.Kind() {
|
||||
case reflect.Struct:
|
||||
if field.Type.PkgPath() != "time" || field.Type.Name() != "Time" {
|
||||
children = documentStruct(reflect.New(field.Type).Interface(), fullKey, fieldLevel)
|
||||
}
|
||||
case reflect.Pointer:
|
||||
if field.Type.Elem().Kind() == reflect.Struct {
|
||||
children = documentStruct(reflect.New(field.Type.Elem()).Interface(), fullKey, fieldLevel)
|
||||
}
|
||||
case reflect.Slice:
|
||||
elem := field.Type.Elem()
|
||||
if elem.Kind() == reflect.Pointer {
|
||||
elem = elem.Elem()
|
||||
}
|
||||
if elem.Kind() == reflect.Struct {
|
||||
children = documentStruct(reflect.New(elem).Interface(), fullKey+"[]", fieldLevel)
|
||||
}
|
||||
}
|
||||
if len(children) > 0 {
|
||||
docEntry.Children = children
|
||||
}
|
||||
docs = append(docs, docEntry)
|
||||
}
|
||||
return docs
|
||||
}
|
||||
1502
internal/cli/configdoc/template.html
Normal file
1502
internal/cli/configdoc/template.html
Normal file
File diff suppressed because it is too large
Load Diff
103
internal/cli/defaultconfig.go
Normal file
103
internal/cli/defaultconfig.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/config"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/tools"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func DefaultConfig() *cobra.Command {
|
||||
var defaultConfigFile string
|
||||
var baseConfigFile string
|
||||
var dryRun bool
|
||||
var defaultConfigCmd = &cobra.Command{
|
||||
Use: "defaultconfig",
|
||||
Short: "Generate full configuration file with defaults",
|
||||
Long: `Generate full Centrifugo configuration file with defaults`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
defaultConfig(defaultConfigFile, baseConfigFile, dryRun)
|
||||
},
|
||||
}
|
||||
defaultConfigCmd.Flags().StringVarP(&defaultConfigFile, "config", "c", "config.json", "path to default config file to generate")
|
||||
defaultConfigCmd.Flags().StringVarP(&baseConfigFile, "base", "b", "", "path to the base config file to use")
|
||||
defaultConfigCmd.Flags().BoolVarP(&dryRun, "dry-run", "d", false, "dry run mode (do not write file and just print generated config to stdout)")
|
||||
return defaultConfigCmd
|
||||
}
|
||||
|
||||
func defaultConfig(configFile string, baseFile string, dryRun bool) {
|
||||
if !dryRun {
|
||||
exists, err := tools.PathExists(configFile)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if exists {
|
||||
fmt.Printf("error: target file already exists\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
conf, _, err := config.GetConfig(nil, baseFile)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err = conf.Validate(); err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var b []byte
|
||||
|
||||
ext := filepath.Ext(configFile)
|
||||
if len(ext) > 1 {
|
||||
ext = ext[1:]
|
||||
}
|
||||
|
||||
supportedExtensions := []string{"json", "toml", "yaml", "yml"}
|
||||
|
||||
// This is an unreleased feature so we remove RedisNats from generated config.
|
||||
conf.Broker.RedisNats = nil
|
||||
|
||||
jsonBytes, err := json.MarshalIndent(conf, "", " ")
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
switch ext {
|
||||
case "json":
|
||||
b = jsonBytes
|
||||
case "toml":
|
||||
b, err = toml.Marshal(conf)
|
||||
case "yaml", "yml":
|
||||
b, err = yaml.Marshal(conf)
|
||||
default:
|
||||
err = errors.New("output config file must have one of supported extensions: " + strings.Join(supportedExtensions, ", "))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
fmt.Println(string(b))
|
||||
return
|
||||
}
|
||||
|
||||
err = os.WriteFile(configFile, b, 0600)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
140
internal/cli/defaultenv.go
Normal file
140
internal/cli/defaultenv.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/config"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/config/envconfig"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func DefaultEnv() *cobra.Command {
|
||||
var baseConfigFile string
|
||||
var baseNonZeroOnly bool
|
||||
var defaultEnvCmd = &cobra.Command{
|
||||
Use: "defaultenv",
|
||||
Short: "Generate full environment var list with defaults",
|
||||
Long: `Generate full Centrifugo environment var list with defaults`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
defaultEnv(baseConfigFile, baseNonZeroOnly)
|
||||
},
|
||||
}
|
||||
defaultEnvCmd.Flags().StringVarP(&baseConfigFile, "base", "b", "", "path to the base config file to use")
|
||||
defaultEnvCmd.Flags().BoolVarP(&baseNonZeroOnly, "base-non-zero-only", "", false, "only output environment variables for values which were non zero in base config file")
|
||||
return defaultEnvCmd
|
||||
}
|
||||
|
||||
func defaultEnv(baseFile string, baseNonZeroOnly bool) {
|
||||
conf, meta, err := config.GetConfig(nil, baseFile)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err = conf.Validate(); err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
printSortedEnvVars(meta.KnownEnvVars, baseNonZeroOnly)
|
||||
}
|
||||
|
||||
var namedArrayEnvVars = []string{
|
||||
"CENTRIFUGO_CHANNEL_NAMESPACES",
|
||||
"CENTRIFUGO_RPC_NAMESPACES",
|
||||
"CENTRIFUGO_CONSUMERS",
|
||||
"CENTRIFUGO_PROXIES",
|
||||
}
|
||||
|
||||
func printSortedEnvVars(knownEnvVars map[string]envconfig.VarInfo, baseNonZeroOnly bool) {
|
||||
var envKeys []string
|
||||
for env := range knownEnvVars {
|
||||
envKeys = append(envKeys, env)
|
||||
}
|
||||
sort.Strings(envKeys)
|
||||
for _, env := range envKeys {
|
||||
if strings.HasSuffix(env, "-") {
|
||||
// Hacky way to skip unnecessary struct field, can be based on struct tag.
|
||||
continue
|
||||
}
|
||||
varInfo := knownEnvVars[env]
|
||||
if baseNonZeroOnly && !varInfo.NonZeroInFile {
|
||||
continue
|
||||
}
|
||||
value := valueToStringReflect(knownEnvVars[env].Field)
|
||||
if slices.Contains(namedArrayEnvVars, env) { // Special handling for named array env vars.
|
||||
var items []struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
s, err := strconv.Unquote(value)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err = json.Unmarshal([]byte(s), &items)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
names := make([]string, len(items))
|
||||
for i, item := range items {
|
||||
names[i] = item.Name
|
||||
}
|
||||
value = strconv.Quote(strings.Join(names, " "))
|
||||
}
|
||||
fmt.Printf("%s=%s\n", env, value)
|
||||
}
|
||||
}
|
||||
|
||||
// valueToStringReflect converts a reflect.Value to a string in a way suitable for environment variables.
|
||||
func valueToStringReflect(v reflect.Value) string {
|
||||
switch v.Kind() {
|
||||
case reflect.Slice, reflect.Array:
|
||||
// Check if the element type of the slice/array is a struct
|
||||
if v.Type().Elem().Kind() == reflect.Struct {
|
||||
// Marshal the entire array/slice of structs to JSON
|
||||
jsonValue, err := json.Marshal(v.Interface())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Escape double quotes to make the value suitable for environment variables
|
||||
return fmt.Sprintf("\"%v\"", strings.ReplaceAll(string(jsonValue), `"`, `\"`))
|
||||
}
|
||||
var elements []string
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
elements = append(elements, valueToStringReflect(v.Index(i)))
|
||||
}
|
||||
if len(elements) == 0 {
|
||||
return "\"\""
|
||||
}
|
||||
return fmt.Sprintf("%v", strings.Join(elements, " "))
|
||||
case reflect.Struct:
|
||||
// You can customize how structs should be serialized if needed
|
||||
return fmt.Sprintf("%v", v.Interface()) // Fallback to default formatting (customize if necessary)
|
||||
case reflect.Map:
|
||||
jsonValue, err := json.Marshal(v.Interface())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Escape double quotes to make the value suitable for environment variables
|
||||
return fmt.Sprintf("\"%v\"", strings.ReplaceAll(string(jsonValue), `"`, `\"`))
|
||||
case reflect.Pointer:
|
||||
if v.IsNil() {
|
||||
return ""
|
||||
}
|
||||
return valueToStringReflect(v.Elem()) // Dereference the pointer and recursively process
|
||||
case reflect.Invalid:
|
||||
return "" // Handle zero/nil values
|
||||
case reflect.String:
|
||||
return fmt.Sprintf("\"%v\"", v.Interface())
|
||||
default:
|
||||
// Fallback for other types (int, bool, etc.)
|
||||
return fmt.Sprintf("%v", v.Interface())
|
||||
}
|
||||
}
|
||||
45
internal/cli/genconfig.go
Normal file
45
internal/cli/genconfig.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/config"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/tools"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func GenConfig() *cobra.Command {
|
||||
var outputConfigFile string
|
||||
var genConfigCmd = &cobra.Command{
|
||||
Use: "genconfig",
|
||||
Short: "Generate minimal configuration file to start with",
|
||||
Long: `Generate minimal configuration file to start with`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
genConfig(cmd, outputConfigFile)
|
||||
},
|
||||
}
|
||||
genConfigCmd.Flags().StringVarP(&outputConfigFile, "config", "c", "config.json", "path to output config file")
|
||||
return genConfigCmd
|
||||
}
|
||||
|
||||
func genConfig(cmd *cobra.Command, outputConfigFile string) {
|
||||
err := tools.GenerateConfig(outputConfigFile)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cfg, _, err := config.GetConfig(cmd, outputConfigFile)
|
||||
if err != nil {
|
||||
_ = os.Remove(outputConfigFile)
|
||||
fmt.Printf("error getting config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err = cfg.Validate()
|
||||
if err != nil {
|
||||
_ = os.Remove(outputConfigFile)
|
||||
fmt.Printf("error validating config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
123
internal/cli/gensubtoken.go
Normal file
123
internal/cli/gensubtoken.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/config"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/confighelpers"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/jwtverify"
|
||||
|
||||
"github.com/cristalhq/jwt/v5"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
func GenSubToken() *cobra.Command {
|
||||
var genSubTokenConfigFile string
|
||||
var genSubTokenUser string
|
||||
var genSubTokenChannel string
|
||||
var genSubTokenTTL int64
|
||||
var genSubTokenQuiet bool
|
||||
var genSubTokenCmd = &cobra.Command{
|
||||
Use: "gensubtoken",
|
||||
Short: "Generate sample subscription JWT for user",
|
||||
Long: `Generate sample subscription JWT for user`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
genSubToken(cmd, genSubTokenConfigFile, genSubTokenUser, genSubTokenChannel, genSubTokenTTL, genSubTokenQuiet)
|
||||
},
|
||||
}
|
||||
genSubTokenCmd.Flags().StringVarP(&genSubTokenConfigFile, "config", "c", "config.json", "path to config file")
|
||||
genSubTokenCmd.Flags().StringVarP(&genSubTokenUser, "user", "u", "", "user ID")
|
||||
genSubTokenCmd.Flags().StringVarP(&genSubTokenChannel, "channel", "s", "", "channel")
|
||||
genSubTokenCmd.Flags().Int64VarP(&genSubTokenTTL, "ttl", "t", 3600*24*7, "token TTL in seconds, use -1 for token without expiration")
|
||||
genSubTokenCmd.Flags().BoolVarP(&genSubTokenQuiet, "quiet", "q", false, "only output the token without anything else")
|
||||
return genSubTokenCmd
|
||||
}
|
||||
|
||||
func genSubToken(
|
||||
cmd *cobra.Command, genSubTokenConfigFile string, genSubTokenUser string,
|
||||
genSubTokenChannel string, genSubTokenTTL int64, genSubTokenQuiet bool,
|
||||
) {
|
||||
cfg, _, err := config.GetConfig(cmd, genSubTokenConfigFile)
|
||||
if err != nil {
|
||||
fmt.Printf("error getting config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if genSubTokenChannel == "" {
|
||||
fmt.Println("channel is required")
|
||||
os.Exit(1)
|
||||
}
|
||||
verifierConfig, err := confighelpers.MakeVerifierConfig(cfg.Client.Token)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if cfg.Client.SubscriptionToken.Enabled {
|
||||
verifierConfig, err = confighelpers.MakeVerifierConfig(cfg.Client.SubscriptionToken.Token)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
token, err := generateSubToken(verifierConfig, genSubTokenUser, genSubTokenChannel, genSubTokenTTL)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
var user = fmt.Sprintf("user \"%s\"", genSubTokenUser)
|
||||
if genSubTokenUser == "" {
|
||||
user = "anonymous user"
|
||||
}
|
||||
exp := "without expiration"
|
||||
if genSubTokenTTL >= 0 {
|
||||
exp = fmt.Sprintf("with expiration TTL %s", time.Duration(genSubTokenTTL)*time.Second)
|
||||
}
|
||||
if genSubTokenQuiet {
|
||||
fmt.Print(token)
|
||||
return
|
||||
}
|
||||
fmt.Printf("HMAC SHA-256 JWT for %s and channel \"%s\" %s:\n%s\n", user, genSubTokenChannel, exp, token)
|
||||
}
|
||||
|
||||
// generateSubToken generates sample subscription JWT for user.
|
||||
func generateSubToken(config jwtverify.VerifierConfig, user string, channel string, ttlSeconds int64) (string, error) {
|
||||
if config.HMACSecretKey == "" {
|
||||
return "", errors.New("gensubtoken only supports generating HMAC-signed tokens, but no HMAC secret key is set in the configuration (client.subscription_token.token.hmac_secret_key, or client.token.hmac_secret_key when subscription token is not separately configured) – if you use RSA/ECDSA/JWKS token verification you need to generate the JWT with your own tooling")
|
||||
}
|
||||
signer, err := jwt.NewSignerHS(jwt.HS256, []byte(config.HMACSecretKey))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating HMAC signer: %w", err)
|
||||
}
|
||||
builder := jwt.NewBuilder(signer)
|
||||
claims := jwtverify.SubscribeTokenClaims{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
Subject: user,
|
||||
},
|
||||
Channel: channel,
|
||||
}
|
||||
if ttlSeconds > 0 {
|
||||
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Duration(ttlSeconds) * time.Second))
|
||||
}
|
||||
|
||||
encodedClaims, err := json.Marshal(claims)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if config.UserIDClaim != "" {
|
||||
encodedClaims, err = sjson.SetBytes(encodedClaims, config.UserIDClaim, user)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
token, err := builder.Build(encodedClaims)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return token.String(), nil
|
||||
}
|
||||
106
internal/cli/gentoken.go
Normal file
106
internal/cli/gentoken.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/config"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/confighelpers"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/jwtverify"
|
||||
|
||||
"github.com/cristalhq/jwt/v5"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
func GenToken() *cobra.Command {
|
||||
var genTokenConfigFile string
|
||||
var genTokenUser string
|
||||
var genTokenTTL int64
|
||||
var genTokenQuiet bool
|
||||
var genTokenCmd = &cobra.Command{
|
||||
Use: "gentoken",
|
||||
Short: "Generate sample connection JWT for user",
|
||||
Long: `Generate sample connection JWT for user`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
genToken(cmd, genTokenConfigFile, genTokenUser, genTokenTTL, genTokenQuiet)
|
||||
},
|
||||
}
|
||||
genTokenCmd.Flags().StringVarP(&genTokenConfigFile, "config", "c", "config.json", "path to config file")
|
||||
genTokenCmd.Flags().StringVarP(&genTokenUser, "user", "u", "", "user ID, by default anonymous")
|
||||
genTokenCmd.Flags().Int64VarP(&genTokenTTL, "ttl", "t", 3600*24*7, "token TTL in seconds, use -1 for token without expiration")
|
||||
genTokenCmd.Flags().BoolVarP(&genTokenQuiet, "quiet", "q", false, "only output the token without anything else")
|
||||
return genTokenCmd
|
||||
}
|
||||
|
||||
func genToken(cmd *cobra.Command, genTokenConfigFile string, genTokenUser string, genTokenTTL int64, genTokenQuiet bool) {
|
||||
cfg, _, err := config.GetConfig(cmd, genTokenConfigFile)
|
||||
if err != nil {
|
||||
fmt.Printf("error getting config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
verifierConfig, err := confighelpers.MakeVerifierConfig(cfg.Client.Token)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
token, err := generateToken(verifierConfig, genTokenUser, genTokenTTL)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
var user = fmt.Sprintf("user \"%s\"", genTokenUser)
|
||||
if genTokenUser == "" {
|
||||
user = "anonymous user"
|
||||
}
|
||||
exp := "without expiration"
|
||||
if genTokenTTL >= 0 {
|
||||
exp = fmt.Sprintf("with expiration TTL %s", time.Duration(genTokenTTL)*time.Second)
|
||||
}
|
||||
if genTokenQuiet {
|
||||
fmt.Print(token)
|
||||
return
|
||||
}
|
||||
fmt.Printf("HMAC SHA-256 JWT for %s %s:\n%s\n", user, exp, token)
|
||||
}
|
||||
|
||||
// generateToken generates sample JWT for user.
|
||||
func generateToken(config jwtverify.VerifierConfig, user string, ttlSeconds int64) (string, error) {
|
||||
if config.HMACSecretKey == "" {
|
||||
return "", errors.New("gentoken only supports generating HMAC-signed tokens, but no HMAC secret key is set in the configuration (client.token.hmac_secret_key) – if you use RSA/ECDSA/JWKS token verification you need to generate the JWT with your own tooling")
|
||||
}
|
||||
signer, err := jwt.NewSignerHS(jwt.HS256, []byte(config.HMACSecretKey))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating HMAC signer: %w", err)
|
||||
}
|
||||
builder := jwt.NewBuilder(signer)
|
||||
claims := jwtverify.ConnectTokenClaims{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
Subject: user,
|
||||
},
|
||||
}
|
||||
if ttlSeconds > 0 {
|
||||
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Duration(ttlSeconds) * time.Second))
|
||||
}
|
||||
|
||||
encodedClaims, err := json.Marshal(claims)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if config.UserIDClaim != "" {
|
||||
encodedClaims, err = sjson.SetBytes(encodedClaims, config.UserIDClaim, user)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
token, err := builder.Build(encodedClaims)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return token.String(), nil
|
||||
}
|
||||
38
internal/cli/serve.go
Normal file
38
internal/cli/serve.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func Serve() *cobra.Command {
|
||||
var serveDir string
|
||||
var servePort int
|
||||
var serveAddr string
|
||||
var serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "Run static file server (for development only)",
|
||||
Long: `Run static file server (for development only)`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
serve(serveAddr, servePort, serveDir)
|
||||
},
|
||||
}
|
||||
serveCmd.Flags().StringVarP(&serveDir, "dir", "d", "./", "path to directory")
|
||||
serveCmd.Flags().IntVarP(&servePort, "port", "p", 3000, "port to serve on")
|
||||
serveCmd.Flags().StringVarP(&serveAddr, "address", "a", "", "interface to serve on (default: all interfaces)")
|
||||
return serveCmd
|
||||
}
|
||||
|
||||
func serve(serveAddr string, servePort int, serveDir string) {
|
||||
address := net.JoinHostPort(serveAddr, strconv.Itoa(servePort))
|
||||
fmt.Printf("start serving %s on %s\n", serveDir, address)
|
||||
if err := http.ListenAndServe(address, http.FileServer(http.Dir(serveDir))); err != nil { //nolint:gosec // Only for development use.
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
25
internal/cli/version.go
Normal file
25
internal/cli/version.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/build"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func Version() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Centrifugo version information",
|
||||
Long: `Print the version information of Centrifugo`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
version()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func version() {
|
||||
fmt.Printf("Centrifugo v%s (Go version: %s)\n", build.Version, runtime.Version())
|
||||
}
|
||||
Reference in New Issue
Block a user