Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

api: Add support for Private Network Access header preflight requests #1620

Merged
merged 7 commits into from
Sep 19, 2024
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
36 changes: 36 additions & 0 deletions api/handlers_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2613,3 +2613,39 @@ func TestAccounts(t *testing.T) {
assert.Equal(t, uint64(1000000000000), (*acctA.Account.CreatedAssets)[0].Params.Total)
assert.Equal(t, "bogo", *(*acctA.Account.CreatedAssets)[0].Params.UnitName)
}

func TestPNAHeader(t *testing.T) {
db, shutdownFunc := setupIdb(t, test.MakeGenesis())
defer shutdownFunc()

//////////
// When // We preflight an endpoint with the PNA "Request" header set
//////////

serverCtx, serverCancel := context.WithCancel(context.Background())
defer serverCancel()
opts := defaultOpts
opts.EnablePrivateNetworkAccessHeader = true
listenAddr := "localhost:8894"
go Serve(serverCtx, listenAddr, db, nil, logrus.New(), opts)

waitForServer(t, listenAddr)

path := "/health"
client := &http.Client{}
req, err := http.NewRequest("OPTIONS", "http://"+listenAddr+path, nil)
require.NoError(t, err)
req.Header.Add("Access-Control-Request-Private-Network", "true")

t.Log("making HTTP request path", req.URL)

resp, err := client.Do(req)
require.NoError(t, err)

//////////
// Then // We expect the PNA "Allow" header to be set
//////////

require.Equal(t, resp.Header.Get("Access-Control-Allow-Private-Network"), "true")
defer resp.Body.Close()
}
20 changes: 20 additions & 0 deletions api/middlewares/pna.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package middlewares

import (
"net/http"

"github.com/labstack/echo/v4"
)

// MakePNA constructs the Private Network Access middleware function
func MakePNA() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
req := ctx.Request()
if req.Method == http.MethodOptions && req.Header.Get("Access-Control-Request-Private-Network") == "true" {
ctx.Response().Header().Set("Access-Control-Allow-Private-Network", "true")
}
return next(ctx)
}
}
}
69 changes: 69 additions & 0 deletions api/middlewares/pna_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package middlewares

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
)

func TestMakePNA(t *testing.T) {
// Create a new Echo instance
e := echo.New()

// Create a handler to be wrapped by the middleware
handler := func(c echo.Context) error {
return c.String(http.StatusOK, "OK")
}

// Create the middleware
middleware := MakePNA()

// Test case 1: OPTIONS request with Access-Control-Request-Private-Network header
t.Run("OPTIONS request with PNA header", func(t *testing.T) {
// Create a new HTTP request and response recorder
req := httptest.NewRequest(http.MethodOptions, "/", nil)
rec := httptest.NewRecorder()

// Set the expected PNA header
req.Header.Set("Access-Control-Request-Private-Network", "true")

// Create Echo context
c := e.NewContext(req, rec)

// Call our MakePNA middleware
err := middleware(handler)(c)

// Assert there's no error and check the PNA header was set correctly
assert.NoError(t, err)
assert.Equal(t, "true", rec.Header().Get("Access-Control-Allow-Private-Network"))
})

// Test case 2: Non-OPTIONS request
t.Run("Non-OPTIONS request", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

err := middleware(handler)(c)

// Assert there's no error and check the PNA header wasn't set
assert.NoError(t, err)
assert.Empty(t, rec.Header().Get("Access-Control-Allow-Private-Network"))
})

// Test case 3: OPTIONS request without Access-Control-Request-Private-Network header
t.Run("OPTIONS request without Private Network header", func(t *testing.T) {
req := httptest.NewRequest(http.MethodOptions, "/", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

err := middleware(handler)(c)

// Assert there's no error and check the PNA header wasn't set
assert.NoError(t, err)
assert.Empty(t, rec.Header().Get("Access-Control-Allow-Private-Network"))
})
}
8 changes: 7 additions & 1 deletion api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ import (
"github.com/algorand/indexer/v3/idb"
)

// ExtraOptions are options which change the behavior or the HTTP server.
// ExtraOptions are options which change the behavior of the HTTP server.
type ExtraOptions struct {
// Tokens are the access tokens which can access the API.
Tokens []string

// Respond to Private Network Access preflight requests sent to the indexer.
EnablePrivateNetworkAccessHeader bool

// MetricsEndpoint turns on the /metrics endpoint for prometheus metrics.
MetricsEndpoint bool

Expand Down Expand Up @@ -101,6 +104,9 @@ func Serve(ctx context.Context, serveAddr string, db idb.IndexerDb, dataError fu
}

e.Use(middlewares.MakeLogger(log))
if options.EnablePrivateNetworkAccessHeader {
e.Use(middlewares.MakePNA())
}
e.Use(middleware.CORS())
e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
// we currently support compressed result only for GET /v2/blocks/ API
Expand Down
57 changes: 30 additions & 27 deletions cmd/algorand-indexer/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,33 +23,34 @@ import (
)

type daemonConfig struct {
flags *pflag.FlagSet
daemonServerAddr string
developerMode bool
metricsMode string
tokenString string
writeTimeout time.Duration
readTimeout time.Duration
maxConn uint32
maxAPIResourcesPerAccount uint32
maxTransactionsLimit uint32
defaultTransactionsLimit uint32
maxAccountsLimit uint32
defaultAccountsLimit uint32
maxAssetsLimit uint32
defaultAssetsLimit uint32
maxBoxesLimit uint32
defaultBoxesLimit uint32
maxBalancesLimit uint32
defaultBalancesLimit uint32
maxApplicationsLimit uint32
defaultApplicationsLimit uint32
enableAllParameters bool
indexerDataDir string
cpuProfile string
pidFilePath string
configFile string
suppliedAPIConfigFile string
flags *pflag.FlagSet
daemonServerAddr string
developerMode bool
enablePrivateNetworkAccessHeader bool
metricsMode string
tokenString string
writeTimeout time.Duration
readTimeout time.Duration
maxConn uint32
maxAPIResourcesPerAccount uint32
maxTransactionsLimit uint32
defaultTransactionsLimit uint32
maxAccountsLimit uint32
defaultAccountsLimit uint32
maxAssetsLimit uint32
defaultAssetsLimit uint32
maxBoxesLimit uint32
defaultBoxesLimit uint32
maxBalancesLimit uint32
defaultBalancesLimit uint32
maxApplicationsLimit uint32
defaultApplicationsLimit uint32
enableAllParameters bool
indexerDataDir string
cpuProfile string
pidFilePath string
configFile string
suppliedAPIConfigFile string
}

// DaemonCmd creates the main cobra command, initializes flags, and viper aliases
Expand All @@ -71,6 +72,7 @@ func DaemonCmd() *cobra.Command {
cfg.flags.StringVarP(&cfg.daemonServerAddr, "server", "S", ":8980", "host:port to serve API on (default :8980)")
cfg.flags.StringVarP(&cfg.tokenString, "token", "t", "", "an optional auth token, when set REST calls must use this token in a bearer format, or in a 'X-Indexer-API-Token' header")
cfg.flags.BoolVarP(&cfg.developerMode, "dev-mode", "", false, "has no effect currently, reserved for future performance intensive operations")
cfg.flags.BoolVarP(&cfg.enablePrivateNetworkAccessHeader, "enable-private-network-access-header", "", false, "respond to Private Network Access preflight requests")
cfg.flags.StringVarP(&cfg.metricsMode, "metrics-mode", "", "OFF", "configure the /metrics endpoint to [ON, OFF, VERBOSE]")
cfg.flags.DurationVarP(&cfg.writeTimeout, "write-timeout", "", 30*time.Second, "set the maximum duration to wait before timing out writes to a http response, breaking connection")
cfg.flags.DurationVarP(&cfg.readTimeout, "read-timeout", "", 5*time.Second, "set the maximum duration for reading the entire request")
Expand Down Expand Up @@ -300,6 +302,7 @@ func runDaemon(daemonConfig *daemonConfig) error {

// makeOptions converts CLI options to server options
func makeOptions(daemonConfig *daemonConfig) (options api.ExtraOptions) {
options.EnablePrivateNetworkAccessHeader = daemonConfig.enablePrivateNetworkAccessHeader
if daemonConfig.tokenString != "" {
options.Tokens = append(options.Tokens, daemonConfig.tokenString)
}
Expand Down
Loading