From f0441a6a42d1fef4e0b324106cbd3afa33f62ca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 23 Jun 2023 11:09:59 +0200 Subject: [PATCH 1/4] feat: protect HTTP API using access token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new configuration option to enable access-token-based authorization for all incoming HTTP requests. Example use: ``` ❯ go run ./cmd/lassie -- daemon --access-token supersecret Lassie daemon listening on address 127.0.0.1:56251 Hit CTRL-C to stop the daemon ``` Unauthorized request: ``` ❯ curl -i 'http://127.0.0.1:56251/debug/pprof/cmdline' HTTP/1.1 401 Unauthorized Date: Fri, 23 Jun 2023 09:13:57 GMT Content-Length: 13 Content-Type: text/plain; charset=utf-8 Unauthorized ``` Authorized request: ``` ❯ curl -i 'http://127.0.0.1:56251/debug/pprof/cmdline' -H 'Authorization: Bearer supersecret' HTTP/1.1 200 OK Content-Type: text/plain; charset=utf-8 X-Content-Type-Options: nosniff Date: Fri, 23 Jun 2023 09:13:30 GMT Content-Length: 120 Warning: Binary output can mess up your terminal. Use "--output -" to tell Warning: curl to output it to your terminal anyway, or consider "--output Warning: " to save to a file. ``` Signed-off-by: Miroslav Bajtoš --- cmd/lassie/daemon.go | 11 +++++++++-- pkg/server/http/server.go | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/cmd/lassie/daemon.go b/cmd/lassie/daemon.go index e6443760..b102ad1a 100644 --- a/cmd/lassie/daemon.go +++ b/cmd/lassie/daemon.go @@ -74,6 +74,11 @@ var daemonFlags = []cli.Flag{ FlagBitswapConcurrency, FlagGlobalTimeout, FlagProviderTimeout, + &cli.StringFlag{ + Name: "access-token", + Usage: "require HTTP clients to authorize using Bearer scheme and the configured access token", + Value: "", + }, } var daemonCmd = &cli.Command{ @@ -119,7 +124,8 @@ func daemonAction(cctx *cli.Context) error { port := cctx.Uint("port") tempDir := cctx.String("tempdir") maxBlocks := cctx.Uint64("maxblocks") - httpServerCfg := getHttpServerConfigForDaemon(address, port, tempDir, maxBlocks) + accessToken := cctx.String("access-token") + httpServerCfg := getHttpServerConfigForDaemon(address, port, tempDir, maxBlocks, accessToken) // event recorder config eventRecorderURL := cctx.String("event-recorder-url") @@ -200,11 +206,12 @@ func defaultDaemonRun( } // getHttpServerConfigForDaemon returns a HttpServerConfig for the daemon command. -func getHttpServerConfigForDaemon(address string, port uint, tempDir string, maxBlocks uint64) httpserver.HttpServerConfig { +func getHttpServerConfigForDaemon(address string, port uint, tempDir string, maxBlocks uint64, accessToken string) httpserver.HttpServerConfig { return httpserver.HttpServerConfig{ Address: address, Port: port, TempDir: tempDir, MaxBlocksPerRequest: maxBlocks, + AccessToken: accessToken, } } diff --git a/pkg/server/http/server.go b/pkg/server/http/server.go index 4ca04c94..dec4e196 100644 --- a/pkg/server/http/server.go +++ b/pkg/server/http/server.go @@ -27,6 +27,7 @@ type HttpServerConfig struct { Port uint TempDir string MaxBlocksPerRequest uint64 + AccessToken string } type contextKey struct { @@ -52,6 +53,11 @@ func NewHttpServer(ctx context.Context, lassie *lassie.Lassie, cfg HttpServerCon // create server mux := http.NewServeMux() handler := servertiming.Middleware(mux, nil) + + if cfg.AccessToken != "" { + handler = authorizationMiddleware(handler, cfg.AccessToken) + } + server := &http.Server{ Addr: fmt.Sprintf(":%d", cfg.Port), BaseContext: func(listener net.Listener) context.Context { return ctx }, @@ -102,3 +108,17 @@ func (s *HttpServer) Close() error { s.cancel() return s.server.Shutdown(context.Background()) } + +func authorizationMiddleware(next http.Handler, accessToken string) http.Handler { + requiredHeaderValue := "Bearer " + accessToken + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") == requiredHeaderValue { + next.ServeHTTP(w, r) + return + } + + // Unauthorized + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintln(w, "Unauthorized") + }) +} From 94caaee49de781fa8c1ed8e468d488f3579bb834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 26 Jun 2023 15:48:44 +0200 Subject: [PATCH 2/4] fixup! add tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miroslav Bajtoš --- pkg/internal/itest/http_fetch_test.go | 59 ++++++++++++++++++++------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/pkg/internal/itest/http_fetch_test.go b/pkg/internal/itest/http_fetch_test.go index 5de882bf..1a9774a3 100644 --- a/pkg/internal/itest/http_fetch_test.go +++ b/pkg/internal/itest/http_fetch_test.go @@ -64,20 +64,21 @@ func TestHttpFetch(t *testing.T) { wrapPath := "/want2/want1/want0" testCases := []struct { - name string - graphsyncRemotes int - bitswapRemotes int - httpRemotes int - disableGraphsync bool - expectFail bool - expectUncleanEnd bool - modifyHttpConfig func(httpserver.HttpServerConfig) httpserver.HttpServerConfig - generate func(*testing.T, io.Reader, []testpeer.TestPeer) []unixfs.DirEntry - paths []string - setHeader headerSetter - modifyQueries []queryModifier - validateBodies []bodyValidator - lassieOpts lassieOptsGen + name string + graphsyncRemotes int + bitswapRemotes int + httpRemotes int + disableGraphsync bool + expectFail bool + expectUncleanEnd bool + expectUnauthorized bool + modifyHttpConfig func(httpserver.HttpServerConfig) httpserver.HttpServerConfig + generate func(*testing.T, io.Reader, []testpeer.TestPeer) []unixfs.DirEntry + paths []string + setHeader headerSetter + modifyQueries []queryModifier + validateBodies []bodyValidator + lassieOpts lassieOptsGen }{ { name: "graphsync large sharded file", @@ -918,6 +919,34 @@ func TestHttpFetch(t *testing.T) { validateCarBody(t, body, srcData.Root, wantCids, true) }}, }, + { + name: "with access token - rejects anonymous requests", + httpRemotes: 1, + generate: func(t *testing.T, rndReader io.Reader, remotes []testpeer.TestPeer) []unixfs.DirEntry { + return []unixfs.DirEntry{unixfs.GenerateFile(t, remotes[0].LinkSystem, rndReader, 1024)} + }, + modifyHttpConfig: func(cfg httpserver.HttpServerConfig) httpserver.HttpServerConfig { + cfg.AccessToken = "super-secret" + return cfg + }, + expectUnauthorized: true, + }, + { + name: "with access token - allows requests with authorization header", + httpRemotes: 1, + generate: func(t *testing.T, rndReader io.Reader, remotes []testpeer.TestPeer) []unixfs.DirEntry { + return []unixfs.DirEntry{unixfs.GenerateFile(t, remotes[0].LinkSystem, rndReader, 1024)} + }, + modifyHttpConfig: func(cfg httpserver.HttpServerConfig) httpserver.HttpServerConfig { + cfg.AccessToken = "super-secret" + return cfg + }, + setHeader: func(header http.Header) { + header.Set("Authorization", "Bearer super-secret") + header.Add("Accept", "application/vnd.ipld.car") + }, + expectUnauthorized: false, + }, } for _, testCase := range testCases { @@ -1031,6 +1060,8 @@ func TestHttpFetch(t *testing.T) { for i, resp := range responses { if testCase.expectFail { req.Equal(http.StatusGatewayTimeout, resp.StatusCode) + } else if testCase.expectUnauthorized { + req.Equal(http.StatusUnauthorized, resp.StatusCode) } else { if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) From 8a0f3a691caff1f0ffba85c3d36a66750d7a45d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 26 Jun 2023 15:49:15 +0200 Subject: [PATCH 3/4] Apply suggestions from code review Co-authored-by: Julian Gruber --- cmd/lassie/daemon.go | 2 +- pkg/server/http/server.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/lassie/daemon.go b/cmd/lassie/daemon.go index b102ad1a..b7f31d9b 100644 --- a/cmd/lassie/daemon.go +++ b/cmd/lassie/daemon.go @@ -76,7 +76,7 @@ var daemonFlags = []cli.Flag{ FlagProviderTimeout, &cli.StringFlag{ Name: "access-token", - Usage: "require HTTP clients to authorize using Bearer scheme and the configured access token", + Usage: "require HTTP clients to authorize using Bearer scheme and given access token", Value: "", }, } diff --git a/pkg/server/http/server.go b/pkg/server/http/server.go index dec4e196..421d55b4 100644 --- a/pkg/server/http/server.go +++ b/pkg/server/http/server.go @@ -110,7 +110,7 @@ func (s *HttpServer) Close() error { } func authorizationMiddleware(next http.Handler, accessToken string) http.Handler { - requiredHeaderValue := "Bearer " + accessToken + requiredHeaderValue := fmt.Sprintf("Bearer %s", accessToken) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Authorization") == requiredHeaderValue { next.ServeHTTP(w, r) From d78e9bc8184173fd24e4961b575d76b7eb7d1e38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 26 Jun 2023 15:54:16 +0200 Subject: [PATCH 4/4] fixup! add tests for daemon CLI args MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miroslav Bajtoš --- cmd/lassie/daemon_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cmd/lassie/daemon_test.go b/cmd/lassie/daemon_test.go index e73007a6..ae7e874e 100644 --- a/cmd/lassie/daemon_test.go +++ b/cmd/lassie/daemon_test.go @@ -41,6 +41,7 @@ func TestDaemonCommandFlags(t *testing.T) { require.Equal(t, "127.0.0.1", hCfg.Address) require.Equal(t, uint(0), hCfg.Port) require.Equal(t, uint64(0), hCfg.MaxBlocksPerRequest) + require.Equal(t, "", hCfg.AccessToken) // event recorder config require.Equal(t, "", erCfg.EndpointURL) @@ -172,6 +173,14 @@ func TestDaemonCommandFlags(t *testing.T) { return nil }, }, + { + name: "with access token", + args: []string{"daemon", "--access-token", "super-secret"}, + assert: func(ctx context.Context, lCfg *l.LassieConfig, hCfg h.HttpServerConfig, erCfg *a.EventRecorderConfig) error { + require.Equal(t, "super-secret", hCfg.AccessToken) + return nil + }, + }, } for _, test := range tests {