diff --git a/nodebuilder/core/config.go b/nodebuilder/core/config.go index 04321df54b..4d5c84df74 100644 --- a/nodebuilder/core/config.go +++ b/nodebuilder/core/config.go @@ -28,7 +28,8 @@ type EndpointConfig struct { // Must be set to true if XTokenPath is provided. TLSEnabled bool // XTokenPath specifies the path to the directory that contains a JSON file with the X-Token for gRPC authentication. - // The JSON file must contain a key "x-token" with the authentication token. + // The JSON file can be named either "xtoken.json" or "x-token.json". + // The JSON file must contain a key "x-token" (preferred) or "xtoken" with the authentication token. // If left empty, the client will not include the X-Token in its requests. XTokenPath string } diff --git a/nodebuilder/core/constructors.go b/nodebuilder/core/constructors.go index 663a372b15..2557b46d69 100644 --- a/nodebuilder/core/constructors.go +++ b/nodebuilder/core/constructors.go @@ -5,12 +5,14 @@ import ( "crypto/tls" "encoding/json" "errors" + "fmt" "net" "os" "path/filepath" "time" grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry" + logging "github.com/ipfs/go-log/v2" "go.uber.org/fx" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -22,6 +24,8 @@ import ( "github.com/celestiaorg/celestia-node/libs/utils" ) +var log = logging.Logger("nodebuilder/core") + const ( // gRPC client requires fetching a block on initialization that can be larger // than the default message size set in gRPC. Increasing defaults up to 64MB @@ -32,7 +36,8 @@ const ( // TODO(@vgonkivs): Revisit this constant once the block size reaches 64MB. defaultGRPCMessageSize = 64 * 1024 * 1024 // 64Mb - xtokenFileName = "xtoken.json" + xtokenFileName = "xtoken.json" + xtokenFileNameAlt = "x-token.json" //nolint:gosec ) type AdditionalCoreConns []*grpc.ClientConn @@ -144,28 +149,68 @@ func authStreamInterceptor(xtoken string) grpc.StreamClientInterceptor { } // parseTokenPath retrieves the authentication token from a JSON file at the specified path. +// It supports both "xtoken.json" and "x-token.json" filenames, and both "xtoken" and "x-token" JSON keys. func parseTokenPath(xtokenPath string) (string, error) { - xtokenPath = filepath.Join(xtokenPath, xtokenFileName) - exist := utils.Exists(xtokenPath) - if !exist { - return "", os.ErrNotExist + // Try both filename variants: xtoken.json and x-token.json + var tokenFilePath string + + primaryPath := filepath.Join(xtokenPath, xtokenFileName) + altPath := filepath.Join(xtokenPath, xtokenFileNameAlt) + + switch { + case utils.Exists(primaryPath): + tokenFilePath = primaryPath + case utils.Exists(altPath): + tokenFilePath = altPath + log.Warnf("Using alternate filename '%s'. Consider using '%s' for consistency.", xtokenFileNameAlt, xtokenFileName) + default: + return "", fmt.Errorf("authentication token file not found. Expected '%s' or '%s' in directory: %s", + xtokenFileName, xtokenFileNameAlt, xtokenPath) } - token, err := os.ReadFile(xtokenPath) + token, err := os.ReadFile(tokenFilePath) if err != nil { - return "", err + return "", fmt.Errorf("failed to read token file '%s': %w", tokenFilePath, err) } + // Support "x-token" (preferred), "xtoken", and "token" JSON keys for maximum compatibility auth := struct { - Token string `json:"x-token"` + XToken string `json:"x-token"` + XTokenAlt string `json:"xtoken"` + Token string `json:"token"` }{} err = json.Unmarshal(token, &auth) if err != nil { - return "", err + return "", fmt.Errorf( + "failed to parse token file '%s': %w. Expected JSON with 'x-token', 'xtoken', or 'token' key", + tokenFilePath, err) } - if auth.Token == "" { - return "", errors.New("x-token is empty. Please setup a token or cleanup xtokenPath") + + var tokenValue string + + switch { + case auth.XToken != "": + tokenValue = auth.XToken + case auth.XTokenAlt != "": + tokenValue = auth.XTokenAlt + log.Warnf("Using alternate JSON key 'xtoken' in file '%s'. Consider using 'x-token' for consistency.", tokenFilePath) + case auth.Token != "": + tokenValue = auth.Token + log.Warnf("Using alternate JSON key 'token' in file '%s'. Consider using 'x-token' for consistency.", tokenFilePath) + default: + return "", fmt.Errorf( + "authentication token is empty or missing in file '%s'. "+ + "Please provide a JSON file with 'x-token' (preferred), 'xtoken', or 'token' key containing the token value", + tokenFilePath) } - return auth.Token, nil + + if tokenValue == "" { + return "", fmt.Errorf( + "authentication token is empty in file '%s'. "+ + "Please setup a valid token or remove the xtokenPath configuration", + tokenFilePath) + } + + return tokenValue, nil } diff --git a/nodebuilder/core/constructors_test.go b/nodebuilder/core/constructors_test.go new file mode 100644 index 0000000000..a13a5ab224 --- /dev/null +++ b/nodebuilder/core/constructors_test.go @@ -0,0 +1,168 @@ +package core + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseTokenPath(t *testing.T) { + tests := []struct { + name string + filename string + jsonContent string + expectedToken string + expectError bool + errorContains string + }{ + { + name: "x-token key with xtoken.json filename", + filename: "xtoken.json", + jsonContent: `{"x-token": "test-token-123"}`, + expectedToken: "test-token-123", + expectError: false, + }, + { + name: "x-token key with x-token.json filename", + filename: "x-token.json", + jsonContent: `{"x-token": "test-token-456"}`, + expectedToken: "test-token-456", + expectError: false, + }, + { + name: "xtoken key with xtoken.json filename", + filename: "xtoken.json", + jsonContent: `{"xtoken": "test-token-789"}`, + expectedToken: "test-token-789", + expectError: false, + }, + { + name: "xtoken key with x-token.json filename", + filename: "x-token.json", + jsonContent: `{"xtoken": "test-token-abc"}`, + expectedToken: "test-token-abc", + expectError: false, + }, + { + name: "token key with xtoken.json filename (QuickNode style)", + filename: "xtoken.json", + jsonContent: `{"token": "hunter2"}`, + expectedToken: "hunter2", + expectError: false, + }, + { + name: "token key with x-token.json filename", + filename: "x-token.json", + jsonContent: `{"token": "hunter3"}`, + expectedToken: "hunter3", + expectError: false, + }, + { + name: "x-token key takes precedence over xtoken", + filename: "xtoken.json", + jsonContent: `{"x-token": "priority-token", "xtoken": "should-be-ignored"}`, + expectedToken: "priority-token", + expectError: false, + }, + { + name: "x-token key takes precedence over token", + filename: "xtoken.json", + jsonContent: `{"x-token": "priority-token", "token": "should-be-ignored"}`, + expectedToken: "priority-token", + expectError: false, + }, + { + name: "xtoken key takes precedence over token", + filename: "xtoken.json", + jsonContent: `{"xtoken": "priority-token", "token": "should-be-ignored"}`, + expectedToken: "priority-token", + expectError: false, + }, + { + name: "empty token value", + filename: "xtoken.json", + jsonContent: `{"x-token": ""}`, + expectedToken: "", + expectError: true, + errorContains: "authentication token is empty", + }, + { + name: "missing token key", + filename: "xtoken.json", + jsonContent: `{"other-key": "value"}`, + expectedToken: "", + expectError: true, + errorContains: "authentication token is empty or missing", + }, + { + name: "invalid JSON", + filename: "xtoken.json", + jsonContent: `invalid json`, + expectedToken: "", + expectError: true, + errorContains: "failed to parse token file", + }, + { + name: "file not found", + filename: "nonexistent.json", + jsonContent: ``, + expectedToken: "", + expectError: true, + errorContains: "authentication token file not found", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testDir := t.TempDir() + + if tt.jsonContent != "" { + tokenFile := filepath.Join(testDir, tt.filename) + err := os.WriteFile(tokenFile, []byte(tt.jsonContent), 0o644) + require.NoError(t, err) + } + + token, err := parseTokenPath(testDir) + + if tt.expectError { + require.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + require.NoError(t, err) + assert.Equal(t, tt.expectedToken, token) + } + }) + } +} + +func TestParseTokenPathFilenamePreference(t *testing.T) { + tmpDir := t.TempDir() + + // Create both files - xtoken.json should be preferred + xtokenFile := filepath.Join(tmpDir, "xtoken.json") + xTokenFile := filepath.Join(tmpDir, "x-token.json") + + err := os.WriteFile(xtokenFile, []byte(`{"x-token": "from-xtoken.json"}`), 0o644) + require.NoError(t, err) + + err = os.WriteFile(xTokenFile, []byte(`{"x-token": "from-x-token.json"}`), 0o644) + require.NoError(t, err) + + // Should prefer xtoken.json + token, err := parseTokenPath(tmpDir) + require.NoError(t, err) + assert.Equal(t, "from-xtoken.json", token) + + // Remove xtoken.json, should use x-token.json + err = os.Remove(xtokenFile) + require.NoError(t, err) + + token, err = parseTokenPath(tmpDir) + require.NoError(t, err) + assert.Equal(t, "from-x-token.json", token) +} diff --git a/nodebuilder/core/flags.go b/nodebuilder/core/flags.go index ca41e5cc08..ac3d978ee7 100644 --- a/nodebuilder/core/flags.go +++ b/nodebuilder/core/flags.go @@ -38,8 +38,10 @@ func Flags() *flag.FlagSet { flags.String( coreXTokenPathFlag, "", - "specifies the file path to the JSON file containing the X-Token for gRPC authentication. "+ - "The JSON file should have a key-value pair where the key is 'x-token' and the value is the authentication token. "+ + "specifies the directory path containing the JSON file with the X-Token for gRPC authentication. "+ + "The JSON file can be named either 'xtoken.json' or 'x-token.json'. "+ + "The JSON file should have a key-value pair where the key is 'x-token' (preferred) or 'xtoken', "+ + "and the value is the authentication token. "+ "NOTE: the path is parsed only if core.tls enabled. "+ "If left empty, the client will not include the X-Token in its requests.", )