mirror of
https://github.com/openziti/zrok.git
synced 2026-08-24 10:14:56 -05:00
Merge branch 'main' into v2.0.3_python_sdk_security
This commit is contained in:
@@ -6,6 +6,8 @@ FIX: The Python SDK `ProxyShare` now rejects absolute proxy request paths before
|
||||
|
||||
FIX: Updated Python SDK unit tests to patch `zrok2.*` modules instead of the legacy `zrok.*` package path, allowing the non-integration test suite to pass against the v2 Python package layout.
|
||||
|
||||
FIX: The `zrok2 copy` drive sync path now rejects unsafe WebDAV and zrok drive paths before writing to a local filesystem target. Local drive sync operations are root-confined to prevent attacker-controlled paths or symlinks from writing, removing, moving, or timestamping files outside the selected destination while still allowing symlinks that resolve within the destination tree.
|
||||
|
||||
FIX: Frontends configured with `interstitial.user_agent_prefixes` no longer suppress the interstitial page for all requests. The prefix list is now correctly evaluated as an allow-list of User-Agents that should receive the page; if the list is empty all User-Agents receive it, matching the documented behavior.
|
||||
|
||||
FIX: Updated `github.com/shoenig/go-m1cpu` to v0.2.1 to correct segmentation violation on M5 macos systems.
|
||||
|
||||
+127
-12
@@ -3,12 +3,13 @@ package sync
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/openziti/zrok/v2/drives/davServer"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/openziti/zrok/v2/drives/davServer"
|
||||
)
|
||||
|
||||
type FilesystemTargetConfig struct {
|
||||
@@ -26,6 +27,31 @@ func NewFilesystemTarget(cfg *FilesystemTargetConfig) *FilesystemTarget {
|
||||
return &FilesystemTarget{cfg: cfg, root: root}
|
||||
}
|
||||
|
||||
type filesystemReadCloser struct {
|
||||
io.ReadCloser
|
||||
root *os.Root
|
||||
}
|
||||
|
||||
func (rc *filesystemReadCloser) Close() error {
|
||||
err := rc.ReadCloser.Close()
|
||||
rootErr := rc.root.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return rootErr
|
||||
}
|
||||
|
||||
func (t *FilesystemTarget) openRoot() (*os.Root, error) {
|
||||
return os.OpenRoot(t.cfg.Root)
|
||||
}
|
||||
|
||||
func (t *FilesystemTarget) ensureRoot(mode os.FileMode) (*os.Root, error) {
|
||||
if err := os.MkdirAll(t.cfg.Root, mode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.OpenRoot(t.cfg.Root)
|
||||
}
|
||||
|
||||
func (t *FilesystemTarget) Inventory() ([]*Object, error) {
|
||||
fi, err := os.Stat(t.cfg.Root)
|
||||
if os.IsNotExist(err) {
|
||||
@@ -74,7 +100,18 @@ func (t *FilesystemTarget) Dir(path string) ([]*Object, error) {
|
||||
}
|
||||
|
||||
func (t *FilesystemTarget) Mkdir(path string) error {
|
||||
return os.MkdirAll(filepath.Join(t.cfg.Root, path), os.ModePerm)
|
||||
localName, err := localNameFromVirtualPath(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
root, err := t.ensureRoot(os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer root.Close()
|
||||
|
||||
return root.MkdirAll(localName, os.ModePerm)
|
||||
}
|
||||
|
||||
func (t *FilesystemTarget) recurse(path string, d fs.DirEntry, err error) error {
|
||||
@@ -111,16 +148,39 @@ func (t *FilesystemTarget) recurse(path string, d fs.DirEntry, err error) error
|
||||
}
|
||||
|
||||
func (t *FilesystemTarget) ReadStream(path string) (io.ReadCloser, error) {
|
||||
return os.Open(filepath.Join(t.cfg.Root, path))
|
||||
localName, err := localNameFromVirtualPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
root, err := t.openRoot()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f, err := root.Open(localName)
|
||||
if err != nil {
|
||||
root.Close()
|
||||
return nil, err
|
||||
}
|
||||
return &filesystemReadCloser{ReadCloser: f, root: root}, nil
|
||||
}
|
||||
|
||||
func (t *FilesystemTarget) WriteStream(path string, stream io.Reader, mode os.FileMode) error {
|
||||
targetPath := filepath.Join(t.cfg.Root, path)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), mode); err != nil {
|
||||
localName, err := localNameFromVirtualPath(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.Create(targetPath)
|
||||
|
||||
root, err := t.ensureRoot(mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer root.Close()
|
||||
|
||||
if err := root.MkdirAll(filepath.Dir(localName), mode); err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := root.Create(localName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -133,20 +193,75 @@ func (t *FilesystemTarget) WriteStream(path string, stream io.Reader, mode os.Fi
|
||||
}
|
||||
|
||||
func (t *FilesystemTarget) WriteStreamWithModTime(path string, stream io.Reader, mode os.FileMode, modTime time.Time) error {
|
||||
return t.WriteStream(path, stream, mode)
|
||||
if err := t.WriteStream(path, stream, mode); err != nil {
|
||||
return err
|
||||
}
|
||||
return t.SetModificationTime(path, modTime)
|
||||
}
|
||||
|
||||
func (t *FilesystemTarget) Move(src, dest string) error {
|
||||
return os.Rename(filepath.Join(t.cfg.Root, src), filepath.Join(filepath.Dir(t.cfg.Root), dest))
|
||||
srcName, err := localNameFromVirtualPath(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
destName, err := localNameFromVirtualPath(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if srcName == "." {
|
||||
parent := filepath.Dir(t.cfg.Root)
|
||||
parentRoot, err := os.OpenRoot(parent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer parentRoot.Close()
|
||||
|
||||
rootName, err := filepath.Localize(filepath.Base(t.cfg.Root))
|
||||
if err != nil {
|
||||
return unsafePathError(t.cfg.Root)
|
||||
}
|
||||
return parentRoot.Rename(rootName, destName)
|
||||
}
|
||||
|
||||
root, err := t.openRoot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer root.Close()
|
||||
return root.Rename(srcName, destName)
|
||||
}
|
||||
|
||||
func (t *FilesystemTarget) Rm(path string) error {
|
||||
return os.RemoveAll(filepath.Join(t.cfg.Root, path))
|
||||
localName, err := localNameFromVirtualPath(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if localName == "." {
|
||||
return os.RemoveAll(t.cfg.Root)
|
||||
}
|
||||
|
||||
root, err := t.openRoot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer root.Close()
|
||||
return root.RemoveAll(localName)
|
||||
}
|
||||
|
||||
func (t *FilesystemTarget) SetModificationTime(path string, mtime time.Time) error {
|
||||
targetPath := filepath.Join(t.cfg.Root, path)
|
||||
if err := os.Chtimes(targetPath, time.Now(), mtime); err != nil {
|
||||
localName, err := localNameFromVirtualPath(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
root, err := t.openRoot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer root.Close()
|
||||
|
||||
if err := root.Chtimes(localName, time.Now(), mtime); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package sync
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func unsafePathError(p string) error {
|
||||
return fmt.Errorf("unsafe path '%s'", p)
|
||||
}
|
||||
|
||||
func cleanVirtualPath(p string) (string, error) {
|
||||
if p == "" {
|
||||
p = "/"
|
||||
}
|
||||
if strings.Contains(p, "\x00") {
|
||||
return "", unsafePathError(p)
|
||||
}
|
||||
for _, segment := range strings.Split(p, "/") {
|
||||
if segment == ".." {
|
||||
return "", unsafePathError(p)
|
||||
}
|
||||
}
|
||||
return path.Clean("/" + p), nil
|
||||
}
|
||||
|
||||
func objectPathFromVirtualPath(p string, isDir bool) (string, error) {
|
||||
clean, err := cleanVirtualPath(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if clean == "/" {
|
||||
return "/", nil
|
||||
}
|
||||
|
||||
rel := strings.TrimPrefix(clean, "/")
|
||||
if _, err := filepath.Localize(rel); err != nil {
|
||||
return "", unsafePathError(p)
|
||||
}
|
||||
|
||||
if isDir {
|
||||
return clean + "/", nil
|
||||
}
|
||||
return clean, nil
|
||||
}
|
||||
|
||||
func localNameFromVirtualPath(p string) (string, error) {
|
||||
clean, err := cleanVirtualPath(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if clean == "/" {
|
||||
return ".", nil
|
||||
}
|
||||
|
||||
localName, err := filepath.Localize(strings.TrimPrefix(clean, "/"))
|
||||
if err != nil {
|
||||
return "", unsafePathError(p)
|
||||
}
|
||||
return localName, nil
|
||||
}
|
||||
|
||||
func remoteObjectPath(rootPath, hrefPath string, isDir bool) (string, error) {
|
||||
root, err := cleanVirtualPath(rootPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
href, err := cleanVirtualPath(hrefPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if root != "/" && href != root && !strings.HasPrefix(href, root+"/") {
|
||||
return "", unsafePathError(hrefPath)
|
||||
}
|
||||
|
||||
rel := href
|
||||
if root != "/" {
|
||||
if href == root {
|
||||
rel = "/"
|
||||
} else {
|
||||
rel = "/" + strings.TrimPrefix(href, root+"/")
|
||||
}
|
||||
}
|
||||
|
||||
return objectPathFromVirtualPath(rel, isDir)
|
||||
}
|
||||
|
||||
func remoteFileObjectPath(requestPath, hrefPath string) (string, error) {
|
||||
request, err := cleanVirtualPath(requestPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
href, err := cleanVirtualPath(hrefPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if href != request {
|
||||
return "", unsafePathError(hrefPath)
|
||||
}
|
||||
return remoteObjectPath(path.Dir(request), href, false)
|
||||
}
|
||||
|
||||
func joinRemotePath(rootPath, objectPath string) (string, error) {
|
||||
root, err := cleanVirtualPath(rootPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
object, err := cleanVirtualPath(objectPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if object == "/" {
|
||||
return root, nil
|
||||
}
|
||||
return path.Join(root, object), nil
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
package sync
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type davTestEntry struct {
|
||||
href string
|
||||
isDir bool
|
||||
content string
|
||||
}
|
||||
|
||||
func newDAVTestServer(rootHref string, entries []davTestEntry) *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "PROPFIND":
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
w.WriteHeader(http.StatusMultiStatus)
|
||||
if r.Header.Get("Depth") == "0" {
|
||||
writeDAVMultiStatus(w, []davTestEntry{{href: rootHref, isDir: true}})
|
||||
return
|
||||
}
|
||||
writeDAVMultiStatus(w, append([]davTestEntry{{href: rootHref, isDir: true}}, entries...))
|
||||
|
||||
case "GET":
|
||||
for _, entry := range entries {
|
||||
if !entry.isDir && r.URL.Path == entry.href {
|
||||
_, _ = w.Write([]byte(entry.content))
|
||||
return
|
||||
}
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
|
||||
default:
|
||||
http.Error(w, "unexpected method", http.StatusInternalServerError)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func writeDAVMultiStatus(w http.ResponseWriter, entries []davTestEntry) {
|
||||
_, _ = fmt.Fprint(w, `<?xml version="1.0" encoding="utf-8"?><d:multistatus xmlns:d="DAV:">`)
|
||||
for _, entry := range entries {
|
||||
if entry.isDir {
|
||||
_, _ = fmt.Fprintf(w, `<d:response><d:href>%s</d:href><d:propstat><d:prop><d:resourcetype><d:collection/></d:resourcetype><d:getlastmodified>Mon, 01 Jan 2024 00:00:00 GMT</d:getlastmodified></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat></d:response>`, entry.href)
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(w, `<d:response><d:href>%s</d:href><d:propstat><d:prop><d:resourcetype/><d:getcontentlength>%d</d:getcontentlength><d:getlastmodified>Mon, 01 Jan 2024 00:00:00 GMT</d:getlastmodified></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat></d:response>`, entry.href, len(entry.content))
|
||||
}
|
||||
}
|
||||
_, _ = fmt.Fprint(w, `</d:multistatus>`)
|
||||
}
|
||||
|
||||
func TestWebDAVInventoryRejectsUnsafePaths(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
urlPath string
|
||||
rootHref string
|
||||
entry davTestEntry
|
||||
}{
|
||||
{
|
||||
name: "parent segment",
|
||||
urlPath: "/",
|
||||
rootHref: "/",
|
||||
entry: davTestEntry{href: "/../outside.txt", content: "owned"},
|
||||
},
|
||||
{
|
||||
name: "encoded parent segment",
|
||||
urlPath: "/",
|
||||
rootHref: "/",
|
||||
entry: davTestEntry{href: "/%2e%2e/outside.txt", content: "owned"},
|
||||
},
|
||||
{
|
||||
name: "nested parent segment",
|
||||
urlPath: "/",
|
||||
rootHref: "/",
|
||||
entry: davTestEntry{href: "/a/../outside.txt", content: "owned"},
|
||||
},
|
||||
{
|
||||
name: "unsafe directory",
|
||||
urlPath: "/",
|
||||
rootHref: "/",
|
||||
entry: davTestEntry{href: "/../outside/", isDir: true},
|
||||
},
|
||||
{
|
||||
name: "outside requested root",
|
||||
urlPath: "/base",
|
||||
rootHref: "/base/",
|
||||
entry: davTestEntry{href: "/other/outside.txt", content: "owned"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
srv := newDAVTestServer(tt.rootHref, []davTestEntry{tt.entry})
|
||||
defer srv.Close()
|
||||
|
||||
u, err := url.Parse(srv.URL + tt.urlPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
src, err := NewWebDAVTarget(&WebDAVTargetConfig{URL: u})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
base := t.TempDir()
|
||||
root := filepath.Join(base, "victim-root")
|
||||
dst := NewFilesystemTarget(&FilesystemTargetConfig{Root: root})
|
||||
|
||||
if err := OneWay(src, dst, false); err == nil {
|
||||
t.Fatal("OneWay succeeded, want unsafe path error")
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(base, "outside.txt")); !os.IsNotExist(err) {
|
||||
t.Fatalf("outside file: got %v, want not exist", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(base, "outside")); !os.IsNotExist(err) {
|
||||
t.Fatalf("outside directory: got %v, want not exist", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebDAVInventoryCopiesSafePathsUnderRequestedRoot(t *testing.T) {
|
||||
srv := newDAVTestServer("/base/", []davTestEntry{
|
||||
{href: "/base/file.txt", content: "safe"},
|
||||
})
|
||||
defer srv.Close()
|
||||
|
||||
u, err := url.Parse(srv.URL + "/base")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
src, err := NewWebDAVTarget(&WebDAVTargetConfig{URL: u})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
base := t.TempDir()
|
||||
root := filepath.Join(base, "dest")
|
||||
dst := NewFilesystemTarget(&FilesystemTargetConfig{Root: root})
|
||||
if err := OneWay(src, dst, false); err != nil {
|
||||
t.Fatalf("OneWay failed: %v", err)
|
||||
}
|
||||
|
||||
b, err := os.ReadFile(filepath.Join(root, "file.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile: %v", err)
|
||||
}
|
||||
if got := string(b); got != "safe" {
|
||||
t.Fatalf("ReadFile: got %q, want %q", got, "safe")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilesystemTargetRejectsTraversalPaths(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
root := filepath.Join(base, "root")
|
||||
if err := os.Mkdir(root, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
target := NewFilesystemTarget(&FilesystemTargetConfig{Root: root})
|
||||
|
||||
victim := filepath.Join(base, "victim.txt")
|
||||
if err := os.WriteFile(victim, []byte("victim"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "source.txt"), []byte("source"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := target.WriteStream("/../outside.txt", strings.NewReader("owned"), 0755); err == nil {
|
||||
t.Fatal("WriteStream succeeded, want error")
|
||||
}
|
||||
if err := target.Mkdir("/../outside-dir"); err == nil {
|
||||
t.Fatal("Mkdir succeeded, want error")
|
||||
}
|
||||
if err := target.Rm("/../victim.txt"); err == nil {
|
||||
t.Fatal("Rm succeeded, want error")
|
||||
}
|
||||
if err := target.SetModificationTime("/../victim.txt", time.Unix(123, 0)); err == nil {
|
||||
t.Fatal("SetModificationTime succeeded, want error")
|
||||
}
|
||||
if err := target.Move("/source.txt", "../moved.txt"); err == nil {
|
||||
t.Fatal("Move child succeeded, want error")
|
||||
}
|
||||
if err := target.Move("/", "../moved-root"); err == nil {
|
||||
t.Fatal("Move root succeeded, want error")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(base, "outside.txt")); !os.IsNotExist(err) {
|
||||
t.Fatalf("outside file: got %v, want not exist", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(base, "outside-dir")); !os.IsNotExist(err) {
|
||||
t.Fatalf("outside dir: got %v, want not exist", err)
|
||||
}
|
||||
b, err := os.ReadFile(victim)
|
||||
if err != nil {
|
||||
t.Fatalf("victim retained: %v", err)
|
||||
}
|
||||
if got := string(b); got != "victim" {
|
||||
t.Fatalf("victim content: got %q, want %q", got, "victim")
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(root, "source.txt")); err != nil {
|
||||
t.Fatalf("source retained: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilesystemTargetMkdirCreatesRoot(t *testing.T) {
|
||||
root := filepath.Join(t.TempDir(), "new-root")
|
||||
target := NewFilesystemTarget(&FilesystemTargetConfig{Root: root})
|
||||
|
||||
if err := target.Mkdir("/"); err != nil {
|
||||
t.Fatalf("Mkdir root: %v", err)
|
||||
}
|
||||
if fi, err := os.Stat(root); err != nil || !fi.IsDir() {
|
||||
t.Fatalf("root directory: info=%v err=%v, want directory", fi, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilesystemTargetRejectsSymlinkEscape(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
outside := t.TempDir()
|
||||
target := NewFilesystemTarget(&FilesystemTargetConfig{Root: root})
|
||||
|
||||
if err := os.Symlink(outside, filepath.Join(root, "escape")); err != nil {
|
||||
t.Skipf("symlinks unavailable: %v", err)
|
||||
}
|
||||
|
||||
if err := target.WriteStream("/escape/created.txt", strings.NewReader("owned"), 0755); err == nil {
|
||||
t.Fatal("WriteStream succeeded, want error")
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(outside, "created.txt")); !os.IsNotExist(err) {
|
||||
t.Fatalf("outside create: got %v, want not exist", err)
|
||||
}
|
||||
|
||||
victim := filepath.Join(outside, "victim.txt")
|
||||
if err := os.WriteFile(victim, []byte("victim"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := target.Rm("/escape/victim.txt"); err == nil {
|
||||
t.Fatal("Rm succeeded, want error")
|
||||
}
|
||||
if _, err := os.Stat(victim); err != nil {
|
||||
t.Fatalf("victim retained: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilesystemTargetAllowsInternalSymlink(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
actual := filepath.Join(root, "actual")
|
||||
if err := os.Mkdir(actual, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink("actual", filepath.Join(root, "nested")); err != nil {
|
||||
t.Skipf("symlinks unavailable: %v", err)
|
||||
}
|
||||
|
||||
target := NewFilesystemTarget(&FilesystemTargetConfig{Root: root})
|
||||
if err := target.WriteStream("/nested/created.txt", strings.NewReader("safe"), 0755); err != nil {
|
||||
t.Fatalf("WriteStream: %v", err)
|
||||
}
|
||||
|
||||
b, err := os.ReadFile(filepath.Join(actual, "created.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile: %v", err)
|
||||
}
|
||||
if got := string(b); got != "safe" {
|
||||
t.Fatalf("ReadFile: got %q, want %q", got, "safe")
|
||||
}
|
||||
}
|
||||
@@ -49,8 +49,13 @@ func OneWay(src, dst Target, sync bool) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dst.WriteStreamWithModTime(copyPath.Path, ss, os.ModePerm, copyPath.Modified); err != nil {
|
||||
return err
|
||||
writeErr := dst.WriteStreamWithModTime(copyPath.Path, ss, os.ModePerm, copyPath.Modified)
|
||||
closeErr := ss.Close()
|
||||
if writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
if closeErr != nil {
|
||||
return closeErr
|
||||
}
|
||||
}
|
||||
dl.Infof("=> %v", copyPath.Path)
|
||||
|
||||
+67
-19
@@ -2,14 +2,15 @@ package sync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/openziti/zrok/v2/drives/davClient"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
pathpkg "path"
|
||||
"time"
|
||||
|
||||
"github.com/openziti/zrok/v2/drives/davClient"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type WebDAVTargetConfig struct {
|
||||
@@ -37,31 +38,47 @@ func NewWebDAVTarget(cfg *WebDAVTargetConfig) (*WebDAVTarget, error) {
|
||||
}
|
||||
|
||||
func (t *WebDAVTarget) Inventory() ([]*Object, error) {
|
||||
rootFi, err := t.dc.Stat(context.Background(), t.cfg.URL.Path)
|
||||
rootPath, err := cleanVirtualPath(t.cfg.URL.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rootFi, err := t.dc.Stat(context.Background(), rootPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !rootFi.IsDir {
|
||||
base := filepath.Base(t.cfg.URL.Path)
|
||||
t.cfg.URL.Path = filepath.Dir(t.cfg.URL.Path)
|
||||
objectPath, err := remoteFileObjectPath(rootPath, rootFi.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.cfg.URL.Path = pathpkg.Dir(rootPath)
|
||||
return []*Object{{
|
||||
Path: "/" + base,
|
||||
Path: objectPath,
|
||||
IsDir: false,
|
||||
Size: rootFi.Size,
|
||||
Modified: rootFi.ModTime,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
fis, err := t.dc.Readdir(context.Background(), "", true)
|
||||
if _, err := remoteObjectPath(rootPath, rootFi.Path, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fis, err := t.dc.Readdir(context.Background(), rootPath, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var objects []*Object
|
||||
for _, fi := range fis {
|
||||
if fi.Path != "/" {
|
||||
objectPath, err := remoteObjectPath(rootPath, fi.Path, fi.IsDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if objectPath != "/" {
|
||||
objects = append(objects, &Object{
|
||||
Path: fi.Path,
|
||||
Path: objectPath,
|
||||
IsDir: fi.IsDir,
|
||||
Size: fi.Size,
|
||||
Modified: fi.ModTime,
|
||||
@@ -80,7 +97,7 @@ func (t *WebDAVTarget) Dir(path string) ([]*Object, error) {
|
||||
for _, fi := range fis {
|
||||
if fi.Path != "/" && fi.Path != t.cfg.URL.Path+"/" {
|
||||
objects = append(objects, &Object{
|
||||
Path: filepath.Base(fi.Path),
|
||||
Path: pathpkg.Base(fi.Path),
|
||||
IsDir: fi.IsDir,
|
||||
Size: fi.Size,
|
||||
Modified: fi.ModTime,
|
||||
@@ -91,22 +108,36 @@ func (t *WebDAVTarget) Dir(path string) ([]*Object, error) {
|
||||
}
|
||||
|
||||
func (t *WebDAVTarget) Mkdir(path string) error {
|
||||
fi, err := t.dc.Stat(context.Background(), filepath.Join(t.cfg.URL.Path, path))
|
||||
targetPath, err := joinRemotePath(t.cfg.URL.Path, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fi, err := t.dc.Stat(context.Background(), targetPath)
|
||||
if err == nil {
|
||||
if fi.IsDir {
|
||||
return nil
|
||||
}
|
||||
return errors.Errorf("'%v' already exists; not directory", path)
|
||||
}
|
||||
return t.dc.Mkdir(context.Background(), filepath.Join(t.cfg.URL.Path, path))
|
||||
return t.dc.Mkdir(context.Background(), targetPath)
|
||||
}
|
||||
|
||||
func (t *WebDAVTarget) ReadStream(path string) (io.ReadCloser, error) {
|
||||
return t.dc.Open(context.Background(), filepath.Join(t.cfg.URL.Path, path))
|
||||
targetPath, err := joinRemotePath(t.cfg.URL.Path, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t.dc.Open(context.Background(), targetPath)
|
||||
}
|
||||
|
||||
func (t *WebDAVTarget) WriteStream(path string, rs io.Reader, _ os.FileMode) error {
|
||||
ws, err := t.dc.Create(context.Background(), filepath.Join(t.cfg.URL.Path, path))
|
||||
targetPath, err := joinRemotePath(t.cfg.URL.Path, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ws, err := t.dc.Create(context.Background(), targetPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -119,7 +150,12 @@ func (t *WebDAVTarget) WriteStream(path string, rs io.Reader, _ os.FileMode) err
|
||||
}
|
||||
|
||||
func (t *WebDAVTarget) WriteStreamWithModTime(path string, rs io.Reader, _ os.FileMode, modTime time.Time) error {
|
||||
ws, err := t.dc.CreateWithModTime(context.Background(), filepath.Join(t.cfg.URL.Path, path), modTime)
|
||||
targetPath, err := joinRemotePath(t.cfg.URL.Path, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ws, err := t.dc.CreateWithModTime(context.Background(), targetPath, modTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -132,13 +168,25 @@ func (t *WebDAVTarget) WriteStreamWithModTime(path string, rs io.Reader, _ os.Fi
|
||||
}
|
||||
|
||||
func (t *WebDAVTarget) Move(src, dest string) error {
|
||||
return t.dc.MoveAll(context.Background(), filepath.Join(t.cfg.URL.Path, src), dest, true)
|
||||
sourcePath, err := joinRemotePath(t.cfg.URL.Path, src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return t.dc.MoveAll(context.Background(), sourcePath, dest, true)
|
||||
}
|
||||
|
||||
func (t *WebDAVTarget) Rm(path string) error {
|
||||
return t.dc.RemoveAll(context.Background(), filepath.Join(t.cfg.URL.Path, path))
|
||||
targetPath, err := joinRemotePath(t.cfg.URL.Path, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return t.dc.RemoveAll(context.Background(), targetPath)
|
||||
}
|
||||
|
||||
func (t *WebDAVTarget) SetModificationTime(path string, mtime time.Time) error {
|
||||
return t.dc.Touch(context.Background(), filepath.Join(t.cfg.URL.Path, path), mtime)
|
||||
targetPath, err := joinRemotePath(t.cfg.URL.Path, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return t.dc.Touch(context.Background(), targetPath, mtime)
|
||||
}
|
||||
|
||||
+69
-21
@@ -2,18 +2,19 @@ package sync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/openziti/zrok/v2/drives/davClient"
|
||||
"github.com/openziti/zrok/v2/environment/env_core"
|
||||
"github.com/openziti/zrok/v2/sdk/golang/sdk"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
pathpkg "path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/openziti/zrok/v2/drives/davClient"
|
||||
"github.com/openziti/zrok/v2/environment/env_core"
|
||||
"github.com/openziti/zrok/v2/sdk/golang/sdk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type ZrokTargetConfig struct {
|
||||
@@ -48,31 +49,47 @@ func NewZrokTarget(cfg *ZrokTargetConfig) (*ZrokTarget, error) {
|
||||
}
|
||||
|
||||
func (t *ZrokTarget) Inventory() ([]*Object, error) {
|
||||
rootFi, err := t.dc.Stat(context.Background(), t.cfg.URL.Path)
|
||||
rootPath, err := cleanVirtualPath(t.cfg.URL.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rootFi, err := t.dc.Stat(context.Background(), rootPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !rootFi.IsDir {
|
||||
base := filepath.Base(t.cfg.URL.Path)
|
||||
t.cfg.URL.Path = filepath.Dir(t.cfg.URL.Path)
|
||||
objectPath, err := remoteFileObjectPath(rootPath, rootFi.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.cfg.URL.Path = pathpkg.Dir(rootPath)
|
||||
return []*Object{{
|
||||
Path: "/" + base,
|
||||
Path: objectPath,
|
||||
IsDir: false,
|
||||
Size: rootFi.Size,
|
||||
Modified: rootFi.ModTime,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
fis, err := t.dc.Readdir(context.Background(), t.cfg.URL.Path, true)
|
||||
if _, err := remoteObjectPath(rootPath, rootFi.Path, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fis, err := t.dc.Readdir(context.Background(), rootPath, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var objects []*Object
|
||||
for _, fi := range fis {
|
||||
if fi.Path != "/" {
|
||||
objectPath, err := remoteObjectPath(rootPath, fi.Path, fi.IsDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if objectPath != "/" {
|
||||
objects = append(objects, &Object{
|
||||
Path: fi.Path,
|
||||
Path: objectPath,
|
||||
IsDir: fi.IsDir,
|
||||
Size: fi.Size,
|
||||
Modified: fi.ModTime,
|
||||
@@ -92,7 +109,7 @@ func (t *ZrokTarget) Dir(path string) ([]*Object, error) {
|
||||
for _, fi := range fis {
|
||||
if fi.Path != "/" && fi.Path != t.cfg.URL.Path+"/" {
|
||||
objects = append(objects, &Object{
|
||||
Path: filepath.Base(fi.Path),
|
||||
Path: pathpkg.Base(fi.Path),
|
||||
IsDir: fi.IsDir,
|
||||
Size: fi.Size,
|
||||
Modified: fi.ModTime,
|
||||
@@ -103,22 +120,36 @@ func (t *ZrokTarget) Dir(path string) ([]*Object, error) {
|
||||
}
|
||||
|
||||
func (t *ZrokTarget) Mkdir(path string) error {
|
||||
fi, err := t.dc.Stat(context.Background(), filepath.Join(t.cfg.URL.Path, path))
|
||||
targetPath, err := joinRemotePath(t.cfg.URL.Path, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fi, err := t.dc.Stat(context.Background(), targetPath)
|
||||
if err == nil {
|
||||
if fi.IsDir {
|
||||
return nil
|
||||
}
|
||||
return errors.Errorf("'%v' already exists; not directory", path)
|
||||
}
|
||||
return t.dc.Mkdir(context.Background(), filepath.Join(t.cfg.URL.Path, path))
|
||||
return t.dc.Mkdir(context.Background(), targetPath)
|
||||
}
|
||||
|
||||
func (t *ZrokTarget) ReadStream(path string) (io.ReadCloser, error) {
|
||||
return t.dc.Open(context.Background(), filepath.Join(t.cfg.URL.Path, path))
|
||||
targetPath, err := joinRemotePath(t.cfg.URL.Path, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t.dc.Open(context.Background(), targetPath)
|
||||
}
|
||||
|
||||
func (t *ZrokTarget) WriteStream(path string, rs io.Reader, _ os.FileMode) error {
|
||||
ws, err := t.dc.Create(context.Background(), filepath.Join(t.cfg.URL.Path, path))
|
||||
targetPath, err := joinRemotePath(t.cfg.URL.Path, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ws, err := t.dc.Create(context.Background(), targetPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -131,7 +162,12 @@ func (t *ZrokTarget) WriteStream(path string, rs io.Reader, _ os.FileMode) error
|
||||
}
|
||||
|
||||
func (t *ZrokTarget) WriteStreamWithModTime(path string, rs io.Reader, _ os.FileMode, modTime time.Time) error {
|
||||
ws, err := t.dc.CreateWithModTime(context.Background(), filepath.Join(t.cfg.URL.Path, path), modTime)
|
||||
targetPath, err := joinRemotePath(t.cfg.URL.Path, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ws, err := t.dc.CreateWithModTime(context.Background(), targetPath, modTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -144,13 +180,25 @@ func (t *ZrokTarget) WriteStreamWithModTime(path string, rs io.Reader, _ os.File
|
||||
}
|
||||
|
||||
func (t *ZrokTarget) Move(src, dest string) error {
|
||||
return t.dc.MoveAll(context.Background(), filepath.Join(t.cfg.URL.Path, src), dest, true)
|
||||
sourcePath, err := joinRemotePath(t.cfg.URL.Path, src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return t.dc.MoveAll(context.Background(), sourcePath, dest, true)
|
||||
}
|
||||
|
||||
func (t *ZrokTarget) Rm(path string) error {
|
||||
return t.dc.RemoveAll(context.Background(), filepath.Join(t.cfg.URL.Path, path))
|
||||
targetPath, err := joinRemotePath(t.cfg.URL.Path, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return t.dc.RemoveAll(context.Background(), targetPath)
|
||||
}
|
||||
|
||||
func (t *ZrokTarget) SetModificationTime(path string, mtime time.Time) error {
|
||||
return t.dc.Touch(context.Background(), filepath.Join(t.cfg.URL.Path, path), mtime)
|
||||
targetPath, err := joinRemotePath(t.cfg.URL.Path, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return t.dc.Touch(context.Background(), targetPath, mtime)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user