Skip to content
Open
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
14 changes: 7 additions & 7 deletions sdk/go/go.mod
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
module github.com/NVIDIA/OpenShell/sdk/go

go 1.25.0
go 1.24.0

require (
github.com/coder/websocket v1.8.15
github.com/stretchr/testify v1.11.1
golang.org/x/oauth2 v0.36.0
google.golang.org/grpc v1.81.1
golang.org/x/oauth2 v0.35.0
google.golang.org/grpc v1.80.0
google.golang.org/protobuf v1.36.11
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.33.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
44 changes: 22 additions & 22 deletions sdk/go/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,30 +20,30 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
Expand Down
29 changes: 29 additions & 0 deletions sdk/go/openshell/v1/edge/cloudflare.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package edge

import (
"errors"
"fmt"

v1 "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1"
)

// CloudflareAccess returns an AuthProvider that adds Cloudflare Access
// headers to every RPC. It sets:
// - cf-access-jwt-assertion: the edge JWT token
// - cookie: CF_Authorization=<token>
//
// The edgeToken authenticates with the Cloudflare Access edge proxy.
// Returns an error if baseAuth is nil or edgeToken is empty.
func CloudflareAccess(baseAuth v1.AuthProvider, edgeToken string) (v1.AuthProvider, error) {
if edgeToken == "" {
return nil, errors.New("edge token must not be empty")
}

return v1.WithExtraHeaders(baseAuth, map[string]string{
"cf-access-jwt-assertion": edgeToken,
"cookie": fmt.Sprintf("CF_Authorization=%s", edgeToken),
})
}
88 changes: 88 additions & 0 deletions sdk/go/openshell/v1/edge/cloudflare_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package edge

import (
"context"
"testing"

v1 "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestCloudflareAccess_ValidToken(t *testing.T) {
base := v1.StaticToken("my-token")
auth, err := CloudflareAccess(base, "cf-edge-jwt-xxx")
require.NoError(t, err)

md, err := auth.GetRequestMetadata(context.Background())
require.NoError(t, err)

// Base auth header preserved.
assert.Equal(t, "Bearer my-token", md["authorization"])

// Cloudflare-specific headers present.
assert.Equal(t, "cf-edge-jwt-xxx", md["cf-access-jwt-assertion"])
assert.Equal(t, "CF_Authorization=cf-edge-jwt-xxx", md["cookie"])
}

func TestCloudflareAccess_EmptyToken(t *testing.T) {
base := v1.StaticToken("my-token")
_, err := CloudflareAccess(base, "")
require.Error(t, err)
assert.Contains(t, err.Error(), "edge token")
}

func TestCloudflareAccess_NilBase(t *testing.T) {
_, err := CloudflareAccess(nil, "cf-edge-jwt-xxx")
require.Error(t, err)
assert.Contains(t, err.Error(), "base")
}

func TestCloudflareAccess_WithNoAuth(t *testing.T) {
auth, err := CloudflareAccess(v1.NoAuth(), "cf-edge-jwt-xxx")
require.NoError(t, err)

md, err := auth.GetRequestMetadata(context.Background())
require.NoError(t, err)

// NoAuth provides no base metadata; only CF headers should appear.
assert.Equal(t, "cf-edge-jwt-xxx", md["cf-access-jwt-assertion"])
assert.Equal(t, "CF_Authorization=cf-edge-jwt-xxx", md["cookie"])
}

func TestCloudflareAccess_RequireTransportSecurity_Delegates(t *testing.T) {
tests := []struct {
name string
base v1.AuthProvider
expected bool
}{
{
name: "delegates to NoAuth (false)",
base: v1.NoAuth(),
expected: false,
},
{
name: "delegates to StaticToken (true)",
base: v1.StaticToken("tok"),
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
auth, err := CloudflareAccess(tt.base, "cf-edge-jwt-xxx")
require.NoError(t, err)
assert.Equal(t, tt.expected, auth.RequireTransportSecurity())
})
}
}

func TestCloudflareAccess_TokenNotInError(t *testing.T) {
// Verify the error for empty token does not leak actual token values.
_, err := CloudflareAccess(v1.StaticToken("s3cr3t-val"), "")
require.Error(t, err)
// The error should mention the parameter name, not any token value.
assert.NotContains(t, err.Error(), "s3cr3t-val")
}
88 changes: 88 additions & 0 deletions sdk/go/openshell/v1/edge/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

// Package edge provides utilities for connecting to OpenShell gateways
// through edge proxies such as Cloudflare Access. It includes convenience
// constructors for common edge auth patterns and a WebSocket tunnel proxy
// for gRPC transport through HTTP/1.1-only proxies.
//
// # Cloudflare Access
//
// CloudflareAccess wraps any AuthProvider with the headers required by
// Cloudflare Access (cf-access-jwt-assertion and CF_Authorization cookie).
// The edge token is typically a service token or application token obtained
// from Cloudflare:
//
// base := v1.StaticToken("my-gateway-token")
// auth, err := edge.CloudflareAccess(base, os.Getenv("CF_ACCESS_TOKEN"))
// if err != nil {
// log.Fatal(err)
// }
// client, err := v1.NewClient(v1.Config{
// Address: "gateway.example.com:443",
// Auth: auth,
// })
// if err != nil {
// log.Fatal(err)
// }
// defer client.Close()
//
// CloudflareAccess composes with any auth provider, including RefreshableToken
// for automatic token refresh:
//
// tokenSource := oauth2Config.TokenSource(ctx, initialToken)
// refreshAuth, err := v1.RefreshableToken(tokenSource)
// if err != nil {
// log.Fatal(err)
// }
// auth, err := edge.CloudflareAccess(refreshAuth, cfToken)
// if err != nil {
// log.Fatal(err)
// }
//
// # WebSocket Tunnel
//
// TunnelProxy bridges gRPC connections over a WebSocket tunnel for edge
// proxies that reject standard HTTP/2 POST requests. The tunnel carries
// its own edge token for proxy authentication, independent of the
// application-level auth provider.
//
// Create a tunnel proxy pointed at the gateway, then dial the proxy's
// local address from the gRPC client:
//
// tunnel, err := edge.NewTunnelProxy(
// "wss://gateway.example.com/ws",
// os.Getenv("CF_ACCESS_TOKEN"),
// )
// if err != nil {
// log.Fatal(err)
// }
// defer tunnel.Close()
//
// auth := v1.StaticToken("my-gateway-token")
// client, err := v1.NewClient(v1.Config{
// Address: tunnel.Addr(),
// Auth: auth,
// TLS: &v1.TLSConfig{Insecure: true}, // local tunnel
// })
// if err != nil {
// log.Fatal(err)
// }
// defer client.Close()
//
// Use functional options to configure TLS, logging, and close timeout:
//
// tunnel, err := edge.NewTunnelProxy(
// "wss://gateway.example.com/ws",
// cfToken,
// edge.WithTunnelTLS(&tls.Config{RootCAs: customCertPool}),
// edge.WithTunnelLogger(myLogger),
// edge.WithCloseTimeout(10*time.Second),
// )
//
// Close drains in-flight connections gracefully. If draining exceeds the
// configured timeout (default 5 seconds), remaining connections are
// force-closed:
//
// err := tunnel.Close() // safe to call multiple times
package edge
Loading