diff --git a/backend/netstorage/netstorage.go b/backend/netstorage/netstorage.go
index b47eb40f2..ee6ebcc63 100755
--- a/backend/netstorage/netstorage.go
+++ b/backend/netstorage/netstorage.go
@@ -573,10 +573,12 @@ func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) (
fs.Errorf(nil, "Unable to parse URL %q: %v", f.endpointURL, err)
return fs.ErrorDirNotFound
}
- resumeURL, err := rest.URLJoin(u, rest.URLPathEscape(resumeStart))
+ // A bad resume start must fail the listing rather than
+ // return fs.ErrorDirNotFound which sync treats as an empty
+ // directory
+ resumeURL, err := rest.URLJoinRoot(u, resumeStart)
if err != nil {
- fs.Errorf(nil, "Unable to join URL %q for resumeStart %s: %v", f.endpointURL, resumeStart, err)
- return fs.ErrorDirNotFound
+ return fmt.Errorf("unable to join URL %q for resumeStart %q: %w", f.endpointURL, resumeStart, err)
}
URL = resumeURL.String()
}
diff --git a/backend/netstorage/netstorage_internal_test.go b/backend/netstorage/netstorage_internal_test.go
new file mode 100644
index 000000000..cf568a16f
--- /dev/null
+++ b/backend/netstorage/netstorage_internal_test.go
@@ -0,0 +1,61 @@
+package netstorage
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "github.com/rclone/rclone/fs"
+ "github.com/rclone/rclone/fs/config/configmap"
+ "github.com/rclone/rclone/fs/config/obscure"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestListResumeCannotChangeHost(t *testing.T) {
+ // The resume start returned by a list request comes from the
+ // server. It must not be able to move the signed continuation
+ // request to another host.
+ var requests int
+ other := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ requests++
+ http.Error(w, "not found", http.StatusNotFound)
+ }))
+ defer other.Close()
+ otherHost := strings.TrimPrefix(other.URL, "http://")
+
+ var listRequests int
+ configured := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ action := r.Header.Get("X-Akamai-ACS-Action")
+ switch {
+ case strings.Contains(action, "action=stat"):
+ _, _ = w.Write([]byte(``))
+ case strings.Contains(action, "action=list"):
+ listRequests++
+ _, _ = w.Write([]byte(`
`))
+ default:
+ http.Error(w, "unexpected action "+action, http.StatusBadRequest)
+ }
+ }))
+ defer configured.Close()
+
+ m := configmap.Simple{
+ "type": "netstorage",
+ "protocol": "http",
+ "host": strings.TrimPrefix(configured.URL, "http://") + "/123456/",
+ "account": "account",
+ "secret": obscure.MustObscure("secret"),
+ }
+ f, err := NewFs(context.Background(), "TestNetStorage", "dir", m)
+ require.NoError(t, err)
+
+ // The listing must fail loudly rather than with
+ // fs.ErrorDirNotFound which sync treats as an empty directory
+ err = f.(*Fs).ListR(context.Background(), "", func(entries fs.DirEntries) error { return nil })
+ assert.ErrorContains(t, err, "must not change the scheme, host or user")
+ assert.NotErrorIs(t, err, fs.ErrorDirNotFound)
+ assert.Equal(t, 1, listRequests, "list request sent to the configured host")
+ assert.Equal(t, 0, requests, "request sent to the other host")
+}