diff --git a/cmd/serve/http/favicon.png b/cmd/serve/http/favicon.png new file mode 100644 index 000000000..346e03785 Binary files /dev/null and b/cmd/serve/http/favicon.png differ diff --git a/cmd/serve/http/http.go b/cmd/serve/http/http.go index e45c0ab85..1c7dbaba2 100644 --- a/cmd/serve/http/http.go +++ b/cmd/serve/http/http.go @@ -3,6 +3,7 @@ package http import ( "context" + _ "embed" "errors" "fmt" "io" @@ -57,6 +58,9 @@ var DefaultOpt = Options{ // Opt is options set by command line flags var Opt = DefaultOpt +//go:embed favicon.png +var faviconData []byte + func init() { fs.RegisterGlobalOptions(fs.OptionsInfo{Name: "http", Opt: &Opt, Options: OptionsInfo}) } @@ -201,6 +205,7 @@ func newServer(ctx context.Context, f fs.Fs, opt *Options, vfsOpt *vfscommon.Opt middleware.SetHeader("Accept-Ranges", "bytes"), middleware.SetHeader("Server", "rclone/"+fs.Version), ) + router.Get("/favicon.ico", s.serveFavicon) router.Get("/*", s.handler) router.Head("/*", s.handler) @@ -225,6 +230,27 @@ func (s *HTTP) Shutdown() error { return s.server.Shutdown() } +// serveFavicon serves the remote's favicon.ico if it exists, otherwise +// the rclone favicon +func (s *HTTP) serveFavicon(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + VFS, err := s.getVFS(ctx) + if err == nil { + node, err := VFS.Stat("favicon.ico") + if err == nil && node.IsFile() { + // Remote has favicon.ico, serve it as a regular file + s.serveFile(w, r, "favicon.ico") + return + } + } + // Serve the embedded rclone favicon + w.Header().Set("Content-Type", "image/png") + w.Header().Set("Cache-Control", "max-age=86400") + if _, err := w.Write(faviconData); err != nil { + fs.Debugf(nil, "Failed to write favicon: %v", err) + } +} + // handler reads incoming requests and dispatches them func (s *HTTP) handler(w http.ResponseWriter, r *http.Request) { isDir := strings.HasSuffix(r.URL.Path, "/") diff --git a/cmd/serve/http/http_test.go b/cmd/serve/http/http_test.go index 5d8ccdc06..79ac7e151 100644 --- a/cmd/serve/http/http_test.go +++ b/cmd/serve/http/http_test.go @@ -297,6 +297,56 @@ func TestAuthProxy(t *testing.T) { testGET(t, true) } +func TestFavicon(t *testing.T) { + ctx := context.Background() + + doGet := func(testURL, path string) *http.Response { + req, err := http.NewRequest("GET", testURL+path, nil) + require.NoError(t, err) + req.SetBasicAuth(testUser, testPass) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + return resp + } + + t.Run("fallback", func(t *testing.T) { + // testdata/files has no favicon.ico, so the embedded fallback is served + f, err := fs.NewFs(ctx, "testdata/files") + require.NoError(t, err) + s, testURL := start(ctx, t, f) + defer func() { assert.NoError(t, s.server.Shutdown()) }() + + resp := doGet(testURL, "favicon.ico") + defer func() { _ = resp.Body.Close() }() + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "image/png", resp.Header.Get("Content-Type")) + assert.Equal(t, "max-age=86400", resp.Header.Get("Cache-Control")) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Equal(t, faviconData, body) + }) + + t.Run("remote override", func(t *testing.T) { + // Start a server on a temp dir that already contains a custom favicon.ico, + // so the VFS sees it at init time and serves it instead of the fallback. + dir := t.TempDir() + customFavicon := []byte("custom favicon data") + require.NoError(t, os.WriteFile(filepath.Join(dir, "favicon.ico"), customFavicon, 0666)) + + f, err := fs.NewFs(ctx, dir) + require.NoError(t, err) + s, testURL := start(ctx, t, f) + defer func() { assert.NoError(t, s.server.Shutdown()) }() + + resp := doGet(testURL, "favicon.ico") + defer func() { _ = resp.Body.Close() }() + assert.Equal(t, http.StatusOK, resp.StatusCode) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Equal(t, customFavicon, body) + }) +} + func TestRc(t *testing.T) { servetest.TestRc(t, rc.Params{ "type": "http",