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
3 changes: 2 additions & 1 deletion nodebuilder/core/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
70 changes: 54 additions & 16 deletions nodebuilder/core/constructors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -31,8 +35,11 @@ const (
// actual response size = 10,85mb
// TODO(@vgonkivs): Revisit this constant once the block size reaches 64MB.
defaultGRPCMessageSize = 64 * 1024 * 1024 // 64Mb
)

xtokenFileName = "xtoken.json"
var (
xtokenFileNames = []string{"xtoken.json", "x-token.json"}
xtokenJSONKeys = []string{"x-token", "xtoken", "token"}
)

type AdditionalCoreConns []*grpc.ClientConn
Expand Down Expand Up @@ -144,28 +151,59 @@ 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
for i, fileName := range xtokenFileNames {
path := filepath.Join(xtokenPath, fileName)
if utils.Exists(path) {
tokenFilePath = path
if i > 0 {
log.Warnf("Using alternate filename '%s'. Consider using '%s' for consistency.", fileName, xtokenFileNames[0])
}
break
}
}

token, err := os.ReadFile(xtokenPath)
if err != nil {
return "", err
if tokenFilePath == "" {
return "", fmt.Errorf("authentication token file not found. Expected one of %v in directory: %s",
xtokenFileNames, xtokenPath)
}

auth := struct {
Token string `json:"x-token"`
}{}
token, err := os.ReadFile(tokenFilePath)
if err != nil {
return "", fmt.Errorf("failed to read token file '%s': %w", tokenFilePath, err)
}

err = json.Unmarshal(token, &auth)
// Parse JSON into a map to support multiple key variants
var tokenData map[string]string
err = json.Unmarshal(token, &tokenData)
if err != nil {
return "", err
return "", fmt.Errorf(
"failed to parse token file '%s': %w. Expected JSON with one of %v key",
tokenFilePath, err, xtokenJSONKeys)
}
if auth.Token == "" {
return "", errors.New("x-token is empty. Please setup a token or cleanup xtokenPath")

// Try each JSON key in order of preference
var tokenValue string
for i, key := range xtokenJSONKeys {
if val, ok := tokenData[key]; ok && val != "" {
tokenValue = val
if i > 0 {
log.Warnf("Using alternate JSON key '%s' in file '%s'. Consider using '%s' for consistency.",
key, tokenFilePath, xtokenJSONKeys[0])
}
break
}
}
return auth.Token, nil

if tokenValue == "" {
return "", fmt.Errorf(
"authentication token is empty or missing in file '%s'. "+
"Please provide a JSON file with one of %v key containing the token value",
tokenFilePath, xtokenJSONKeys)
}

return tokenValue, nil
}
168 changes: 168 additions & 0 deletions nodebuilder/core/constructors_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
6 changes: 4 additions & 2 deletions nodebuilder/core/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
)
Expand Down
Loading