diff --git a/README.md b/README.md index 82f0a5a..674b1b9 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,21 @@ If you drop the [`index.php`](./index.php) file in a directory of your web-serve ![Web UI screenshot](https://raw.githubusercontent.com/kd2org/webdav-manager.js/main/scr_desktop.png) +* Single-file WebDAV server! +* No database! +* Very fast and lightweight! +* Compatible with tons of apps! +* Manage files and directories from a web browser: + * Upload directly from browser, using paste or drag and drop + * Rename + * Delete + * Create and edit text files + * Create directories + * MarkDown live preview + * Preview of images, text, MarkDown and PDF +* Manage users and password with only a text file! +* Restrict users to some directories, control where they can write! + ## WebDAV clients You can use any WebDAV client, but we recommend these: @@ -70,19 +85,38 @@ All users have read access to everything by default. #### Restricting users to some directories -You can also limit users in which directories and files they can access by using the `restrict` and `restrict_write` configuration directives: +If you want something more detailed, you can also limit users in which directories and files they can access by using the `restrict[]` and `restrict_write[]` configuration directives. + +These are tables, so you can have more than one directory restriction, don't forget the `[]`! + +In the following example, the user will only be able to read the `constitution` directory and not write anything: ``` -[emusk] -password = youSuck +[olympe] +password = abcd write = false -restrict[] = 'kill-twitter/' +restrict[] = 'constitution/' +``` -[pouyane] -password = youArePaidWayTooMuch -write = false -restrict[] = 'total/' -restrict_write[] = 'total/kill-the-planet/' +Here the user will be able to only read and write in the `constitution` and `images` directories: + +``` +[olympe] +password = abcd +write = true +restrict[] = 'constitution/' +restrict[] = 'images/' +``` + +And here, she will be able to only read from the `constitution` directory and write in the `constitution/book` and `constitution/summary` directories: + +``` +[olympe] +password = abcd +write = true +restrict[] = 'constitution/' +restrict_write[] = 'constitution/book/' +restrict_write[] = 'constitution/summary/' ``` ### Allow unrestricted access to everyone diff --git a/index.php b/index.php index ac74c1d..98b1e73 100644 --- a/index.php +++ b/index.php @@ -725,7 +725,7 @@ namespace KD2\WebDAV $uri = trim(rtrim($this->base_uri, '/') . '/' . ltrim($uri, '/'), '/'); $path = '/' . str_replace('%2F', '/', rawurlencode($uri)); - if (($item['DAV::resourcetype'] ?? null) == 'collection') { + if (($item['DAV::resourcetype'] ?? null) == 'collection' && $path != '/') { $path .= '/'; } @@ -1353,16 +1353,28 @@ namespace PicoDAV return true; } - if ($this->auth()) { + if (!$this->auth()) { + return false; + } + + $restrict = $this->users[$this->user]['restrict'] ?? []; + + if (!is_array($restrict) || empty($restrict)) { return true; } + foreach ($restrict as $match) { + if (0 === strpos($uri, $match)) { + return true; + } + } + return false; } public function canWrite(string $uri): bool { - if (!$this->user && !ANONYMOUS_WRITE) { + if (!$this->auth() && !ANONYMOUS_WRITE) { return false; } @@ -1374,7 +1386,36 @@ namespace PicoDAV return true; } - if (!empty($this->users[$this->user]['write'])) { + if (!$this->auth() || empty($this->users[$this->user]['write'])) { + return false; + } + + $restrict = $this->users[$this->user]['restrict_write'] ?? []; + + if (!is_array($restrict) || empty($restrict)) { + return true; + } + + foreach ($restrict as $match) { + if (0 === strpos($uri, $match)) { + return true; + } + } + + return false; + } + + public function canOnlyCreate(string $uri): bool + { + $restrict = $this->users[$this->user]['restrict_write'] ?? []; + + if (in_array($uri, $restrict, true)) { + return true; + } + + $restrict = $this->users[$this->user]['restrict'] ?? []; + + if (in_array($uri, $restrict, true)) { return true; } @@ -1383,13 +1424,13 @@ namespace PicoDAV public function list(string $uri, ?array $properties): iterable { - if (!$this->canRead($uri)) { - throw new WebDAV_Exception('Access forbidden', 403); + if (!$this->canRead($uri . '/')) { + //throw new WebDAV_Exception('Access forbidden', 403); } $dirs = self::glob($this->path . $uri, '/*', \GLOB_ONLYDIR); $dirs = array_map('basename', $dirs); - $dirs = array_filter($dirs, fn($a) => $this->canRead(ltrim($uri . '/' . $a, '/'))); + $dirs = array_filter($dirs, fn($a) => $this->canRead(ltrim($uri . '/' . $a, '/') . '/')); natcasesort($dirs); $files = self::glob($this->path . $uri, '/*'); @@ -1403,6 +1444,7 @@ namespace PicoDAV $files = array_flip(array_merge($dirs, $files)); $files = array_map(fn($a) => null, $files); + return $files; } @@ -1452,8 +1494,6 @@ namespace PicoDAV } return new \DateTime('@' . $mtime); - case 'DAV::displayname': - return basename($target); case 'DAV::ishidden': return basename($target)[0] == '.'; case 'DAV::getetag': @@ -1466,12 +1506,23 @@ namespace PicoDAV case 'http://owncloud.org/ns:permissions': $permissions = 'G'; + if (is_dir($target)) { + $uri .= '/'; + } + if (is_writeable($target) && $this->canWrite($uri)) { - $permissions .= 'DNVWCK'; + // If the directory is one of the restricted paths, + // then we can only do stuff INSIDE, and not delete/rename the directory itself + if ($this->canOnlyCreate($uri)) { + $permissions .= 'CK'; + } + else { + $permissions .= 'DNVWCK'; + } } return $permissions; - case WebDAV::PROP_DIGEST_MD5: + case Server::PROP_DIGEST_MD5: if (!is_file($target)) { return null; } @@ -1578,6 +1629,10 @@ namespace PicoDAV throw new WebDAV_Exception('Access forbidden', 403); } + if ($this->canOnlyCreate($uri)) { + throw new WebDAV_Exception('Access forbidden', 403); + } + $target = $this->path . $uri; if (!file_exists($target)) { @@ -1602,11 +1657,9 @@ namespace PicoDAV public function copymove(bool $move, string $uri, string $destination): bool { - if (!$this->canWrite($uri)) { - throw new WebDAV_Exception('Access forbidden', 403); - } - - if (!$this->canWrite($destination)) { + if (!$this->canWrite($uri) + || !$this->canWrite($destination) + || $this->canOnlyCreate($uri)) { throw new WebDAV_Exception('Access forbidden', 403); } @@ -1794,11 +1847,11 @@ RewriteRule ^.*$ /index.php [END] $fp = fopen(__FILE__, 'r'); if ($relative_uri == 'webdav.js') { - fseek($fp, 48399, SEEK_SET); + fseek($fp, 49575, SEEK_SET); echo fread($fp, 25889); } else { - fseek($fp, 48399 + 25889, SEEK_SET); + fseek($fp, 49575 + 25889, SEEK_SET); echo fread($fp, 6760); } diff --git a/server.php b/server.php index fd81372..74a937c 100644 --- a/server.php +++ b/server.php @@ -77,16 +77,28 @@ namespace PicoDAV return true; } - if ($this->auth()) { + if (!$this->auth()) { + return false; + } + + $restrict = $this->users[$this->user]['restrict'] ?? []; + + if (!is_array($restrict) || empty($restrict)) { return true; } + foreach ($restrict as $match) { + if (0 === strpos($uri, $match)) { + return true; + } + } + return false; } public function canWrite(string $uri): bool { - if (!$this->user && !ANONYMOUS_WRITE) { + if (!$this->auth() && !ANONYMOUS_WRITE) { return false; } @@ -98,7 +110,36 @@ namespace PicoDAV return true; } - if (!empty($this->users[$this->user]['write'])) { + if (!$this->auth() || empty($this->users[$this->user]['write'])) { + return false; + } + + $restrict = $this->users[$this->user]['restrict_write'] ?? []; + + if (!is_array($restrict) || empty($restrict)) { + return true; + } + + foreach ($restrict as $match) { + if (0 === strpos($uri, $match)) { + return true; + } + } + + return false; + } + + public function canOnlyCreate(string $uri): bool + { + $restrict = $this->users[$this->user]['restrict_write'] ?? []; + + if (in_array($uri, $restrict, true)) { + return true; + } + + $restrict = $this->users[$this->user]['restrict'] ?? []; + + if (in_array($uri, $restrict, true)) { return true; } @@ -107,13 +148,13 @@ namespace PicoDAV public function list(string $uri, ?array $properties): iterable { - if (!$this->canRead($uri)) { - throw new WebDAV_Exception('Access forbidden', 403); + if (!$this->canRead($uri . '/')) { + //throw new WebDAV_Exception('Access forbidden', 403); } $dirs = self::glob($this->path . $uri, '/*', \GLOB_ONLYDIR); $dirs = array_map('basename', $dirs); - $dirs = array_filter($dirs, fn($a) => $this->canRead(ltrim($uri . '/' . $a, '/'))); + $dirs = array_filter($dirs, fn($a) => $this->canRead(ltrim($uri . '/' . $a, '/') . '/')); natcasesort($dirs); $files = self::glob($this->path . $uri, '/*'); @@ -127,6 +168,7 @@ namespace PicoDAV $files = array_flip(array_merge($dirs, $files)); $files = array_map(fn($a) => null, $files); + return $files; } @@ -176,8 +218,6 @@ namespace PicoDAV } return new \DateTime('@' . $mtime); - case 'DAV::displayname': - return basename($target); case 'DAV::ishidden': return basename($target)[0] == '.'; case 'DAV::getetag': @@ -190,12 +230,23 @@ namespace PicoDAV case 'http://owncloud.org/ns:permissions': $permissions = 'G'; + if (is_dir($target)) { + $uri .= '/'; + } + if (is_writeable($target) && $this->canWrite($uri)) { - $permissions .= 'DNVWCK'; + // If the directory is one of the restricted paths, + // then we can only do stuff INSIDE, and not delete/rename the directory itself + if ($this->canOnlyCreate($uri)) { + $permissions .= 'CK'; + } + else { + $permissions .= 'DNVWCK'; + } } return $permissions; - case WebDAV::PROP_DIGEST_MD5: + case Server::PROP_DIGEST_MD5: if (!is_file($target)) { return null; } @@ -302,6 +353,10 @@ namespace PicoDAV throw new WebDAV_Exception('Access forbidden', 403); } + if ($this->canOnlyCreate($uri)) { + throw new WebDAV_Exception('Access forbidden', 403); + } + $target = $this->path . $uri; if (!file_exists($target)) { @@ -326,11 +381,9 @@ namespace PicoDAV public function copymove(bool $move, string $uri, string $destination): bool { - if (!$this->canWrite($uri)) { - throw new WebDAV_Exception('Access forbidden', 403); - } - - if (!$this->canWrite($destination)) { + if (!$this->canWrite($uri) + || !$this->canWrite($destination) + || $this->canOnlyCreate($uri)) { throw new WebDAV_Exception('Access forbidden', 403); }