Add users management

This commit is contained in:
bohwaz 2022-11-21 22:37:52 +01:00
parent be2f8415c6
commit 3adcc56962
5 changed files with 488 additions and 97 deletions

10
.htaccess Normal file
View File

@ -0,0 +1,10 @@
DirectoryIndex disabled
RedirectMatch 404 \.picodav\.ini
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f [OR]
RewriteCond %{REQUEST_URI} \.picodav\.ini [OR]
RewriteCond %{REQUEST_METHOD} !GET
RewriteRule ^.*$ /index.php [END]

124
README.md
View File

@ -1,5 +1,123 @@
# Nano-KaraDAV
# PicoDAV
## Single-file WebDAV server, just drop it in a directory!
## Single-file WebDAV server in PHP, just drop it in a directory!
If you drop the `index.php` file in a directory, and
If you drop the [`index.php`](./index.php) file in a directory of your web-server, it will make the contents of this directory available via WebDAV, and will also provide a nice web UI to manage the files, using [WebDAV Manager.js](https://github.com/kd2org/webdav-manager.js).
![Web UI screenshot](https://raw.githubusercontent.com/kd2org/webdav-manager.js/main/scr_desktop.png)
## WebDAV clients
You can use any WebDAV client, but we recommend these:
* Windows/OSX: [CyberDuck](https://cyberduck.io/download/)
* Linux: Any file manager should be able to connect to WebDAV (Dolphin, Thunar, Nautilus, etc.), but you can also use [FUSE webdavfs](https://github.com/miquels/webdavfs)
* Android: [RCX](https://f-droid.org/en/packages/io.github.x0b.rcx/) and [DAVx⁵](https://www.davx5.com/), see [the manual](https://manual.davx5.com/webdav_mounts.html)
## Install
It's really as simple as it says: just upload the `index.php` to a directory on your web-server, and it will now be available via WebDAV.
If you are using Apache (version 2.3.9 or later is required), a .htaccess file will be created if it does not exist, to redirect requests to `index.php`. If not, you can use
The only requirement is PHP 7.4, or more recent (8.0 and 8.1 are also supported, not tested with PHP 8.2).
Note that by default, write access is disabled for security purposes. See below to enable write access.
### Other web servers than Apache
This is designed to work best with Apache web servers. If you are using another web server, you'll have to adapt the rules described in `.htaccess` to your own server.
### Configuration
PicoDAV accepts a configuration file named `.picodav.ini`.
It should be in the same directory as `index.php`.
### Users and passwords
By default, the WebDAV server is accessible to everyone.
You can disable anonymous access by writing the following line inside `.picodav.ini`:
```
ANONYMOUS_READ = false
```
Then you need to create user accounts, for that add a new section to `.picodav.ini` for each user. For example if we want to give write access to a user named `greta`:
```
[greta]
password = verySecret
write = true
```
Note that PicoDAV will replace this password with an encrypted version the next time it is accessed, don't worry about that, this is for extra safety, just in case the `.picodav.ini` is accessed by a hacker if you made mistake in your web server configuration.
Here is an example of the password once it has been encrypted:
```
password = '$2y$10$fbdabTjNPN3gMAUlaSEoR.kKHLnh0yMGneuJ7P2AOhSSNr8gUaCPu'
```
Of course you can also only give a read access to this user by changing the `write` line to:
```
write = false
```
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:
```
[emusk]
password = youSuck
write = false
restrict[] = 'kill-twitter/'
[pouyane]
password = youArePaidWayTooMuch
write = false
restrict[] = 'total/'
restrict_write[] = 'total/kill-the-planet/'
```
### Allow unrestricted access to everyone
This will allow anonymous visitors to read and write to all the files:
```
ANONYMOUS_READ = true
ANONYMOUS_WRITE = true
```
Please note: if you do this, **EVERYONE** visiting your PicoDAV URL will be able to edit, delete or create files!
### Other notes
If you don't want to use the included users feature, you can also restrict access by using a [`.htpasswd` Apache file](https://www.cyberciti.biz/faq/create-update-user-authentication-files/).
For security purposes, the WebDAV server will not allow to read or delete UNIX hidden files (with the file name beginning with a dot, eg. `.htaccess` etc.).
Access to PHP files is also disabled for the same reasons.
## Dependencies
This software includes the KD2\WebDAV class from the [KD2FW package](https://fossil.kd2.org/kd2fw/).
It is a lightweight and easy to use library, designed to to add support for WebDAV and NextCloud clients to your software.
Contact us for a commercial license.
## Author
BohwaZ. Contact me on: Web: https://bohwaz.net / IRC = bohwaz@irc.libera.chat / Mastodon = https://mamot.fr/@bohwaz
## License
This software and its dependencies are available in open source with the AGPL v3 license. This requires you to share all your source code if you include this in your software. This is voluntary.
For entities wishing to use this software or libraries in a project where you don't want to have to publish all your source code, we can also sell this software with a commercial license, contact me at bohwaz at kd2 dot org. We can do that as we wrote and own 100% of the source code, dependencies included, there is no third-party code here.

222
index.php
View File

@ -1297,34 +1297,107 @@ namespace PicoDAV
const PUT_IGNORE_PATTERN = '!^~(?:lock\.|^\._)|^(?:\.DS_Store|Thumbs\.db|desktop\.ini)$!';
protected string $path;
protected ?string $user = null;
public array $users = [];
public function __construct()
{
$this->path = __DIR__ . '/';
}
public function auth(): bool
{
if (ANONYMOUS_WRITE && ANONYMOUS_READ) {
return true;
}
if ($this->user) {
return true;
}
$user = $_SERVER['PHP_AUTH_USER'] ?? null;
$password = $_SERVER['PHP_AUTH_PW'] ?? null;
$hash = $this->users[$user]['password'] ?? null;
if (!$hash) {
return false;
}
if (!password_verify($password, $hash)) {
return false;
}
$this->user = $user;
return true;
}
static protected function glob(string $path, string $pattern = '', int $flags = 0): array
{
$path = preg_replace('/[\*\?\[\]]/', '\\\\$0', $path);
return glob($path . $pattern, $flags);
}
public function canRead(string $uri): bool
{
if (in_array($uri, INTERNAL_FILES)) {
return false;
}
if (preg_match('/\.(?:php\d?|phtml|phps)$|^\./i', $uri)) {
return false;
}
if (ANONYMOUS_READ) {
return true;
}
if ($this->auth()) {
return true;
}
return false;
}
public function canWrite(string $uri): bool
{
if (!$this->user && !ANONYMOUS_WRITE) {
return false;
}
if (!$this->canRead($uri)) {
return false;
}
if (ANONYMOUS_WRITE) {
return true;
}
if (!empty($this->users[$this->user]['write'])) {
return true;
}
return false;
}
public function list(string $uri, ?array $properties): iterable
{
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, '/')));
natcasesort($dirs);
$files = self::glob($this->path . $uri, '/*');
$files = array_map('basename', $files);
$files = array_diff($files, $dirs);
// Remove PHP files from listings
$files = array_filter($files, fn($a) => !preg_match('/\.(?:php\d?|phtml|phps)$|^\./i', $a));
if (!$uri) {
$files = array_diff($files, ['webdav.js', 'webdav.css']);
}
// Remove PHP files and dot-files from listings
$files = array_filter($files, fn($a) => $this->canRead(ltrim($uri . '/' . $a, '/')));
natcasesort($files);
@ -1335,6 +1408,10 @@ namespace PicoDAV
public function get(string $uri): ?array
{
if (!$this->canRead($uri)) {
throw new WebDAV_Exception('Access forbidden', 403);
}
$path = $this->path . $uri;
if (!file_exists($path)) {
@ -1389,7 +1466,7 @@ namespace PicoDAV
case 'http://owncloud.org/ns:permissions':
$permissions = 'G';
if (is_writeable($target) && !FORCE_READONLY) {
if (is_writeable($target) && $this->canWrite($uri)) {
$permissions .= 'DNVWCK';
}
@ -1438,8 +1515,8 @@ namespace PicoDAV
return false;
}
if (FORCE_READONLY) {
throw new WebDAV_Exception('Write access is disabled', 403);
if (!$this->canWrite($uri)) {
throw new WebDAV_Exception('Access forbidden', 403);
}
$target = $this->path . $uri;
@ -1497,8 +1574,8 @@ namespace PicoDAV
public function delete(string $uri): void
{
if (FORCE_READONLY) {
throw new WebDAV_Exception('Write access is disabled', 403);
if (!$this->canWrite($uri)) {
throw new WebDAV_Exception('Access forbidden', 403);
}
$target = $this->path . $uri;
@ -1525,8 +1602,12 @@ namespace PicoDAV
public function copymove(bool $move, string $uri, string $destination): bool
{
if (FORCE_READONLY) {
throw new WebDAV_Exception('Write access is disabled', 403);
if (!$this->canWrite($uri)) {
throw new WebDAV_Exception('Access forbidden', 403);
}
if (!$this->canWrite($destination)) {
throw new WebDAV_Exception('Access forbidden', 403);
}
$source = $this->path . $uri;
@ -1590,8 +1671,8 @@ namespace PicoDAV
public function mkcol(string $uri): void
{
if (FORCE_READONLY) {
throw new WebDAV_Exception('Write access is disabled', 403);
if (!$this->canWrite($uri)) {
throw new WebDAV_Exception('Access forbidden', 403);
}
if (!disk_free_space($this->path)) {
@ -1649,6 +1730,20 @@ namespace PicoDAV
return $out;
}
function error(WebDAV_Exception $e)
{
if ($e->getCode() == 403 && !$this->storage->auth() && count($this->storage->users)) {
$user = $_SERVER['PHP_AUTH_USER'] ?? null;
http_response_code(401);
header('WWW-Authenticate: Basic realm="Please login"');
echo '<h2>Error 401</h2><h1>You need to login to access this.</h1>';
return;
}
parent::error($e);
}
}
}
@ -1666,27 +1761,18 @@ namespace {
$relative_uri = ltrim(substr($uri, strlen($root)), '/');
const DEFAULT_CONFIG = [
'FORCE_READONLY' => false,
];
if (!empty($_SERVER['SERVER_SOFTWARE']) && stristr($_SERVER['SERVER_SOFTWARE'], 'apache') && !file_exists(__DIR__ . '/.htaccess')) {
file_put_contents(__DIR__ . '/.htaccess', 'DirectoryIndex disabled
$config = [];
RedirectMatch 404 \\.picodav\\.ini
if (file_exists(__DIR__ . '/.picodav.ini')) {
$config = parse_ini_file(__DIR__ . '/.picodav.ini');
$config = array_change_key_case($config, \CASE_UPPER);
}
foreach (DEFAULT_CONFIG as $key => $value) {
if (array_key_exists($key, $config)) {
$value = $config[$key];
}
if (is_bool(DEFAULT_CONFIG[$key])) {
$value = boolval($value);
}
define('PicoDAV\\' . $key, $value);
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f [OR]
RewriteCond %{REQUEST_URI} \\.picodav\\.ini [OR]
RewriteCond %{REQUEST_METHOD} !GET
RewriteRule ^.*$ /index.php [END]
');
}
if ($relative_uri == 'webdav.js' || $relative_uri == 'webdav.css') {
@ -1708,11 +1794,11 @@ namespace {
$fp = fopen(__FILE__, 'r');
if ($relative_uri == 'webdav.js') {
fseek($fp, 45051, SEEK_SET);
fseek($fp, 48399, SEEK_SET);
echo fread($fp, 25889);
}
else {
fseek($fp, 45051 + 25889, SEEK_SET);
fseek($fp, 48399 + 25889, SEEK_SET);
echo fread($fp, 6760);
}
@ -1721,8 +1807,70 @@ namespace {
exit;
}
const CONFIG_FILE = __DIR__ . '/.picodav.ini';
const INTERNAL_FILES = ['.picodav.ini', 'index.php', 'webdav.js', 'webdav.css'];
const DEFAULT_CONFIG = [
'ANONYMOUS_READ' => false,
'ANONYMOUS_WRITE' => false,
];
$config = [];
$storage = new Storage;
if (file_exists(CONFIG_FILE)) {
$config = parse_ini_file(CONFIG_FILE, true);
$users = array_filter($config, 'is_array');
$config = array_diff_key($config, $users);
$config = array_change_key_case($config, \CASE_UPPER);
$replace = [];
// Encrypt plaintext passwords
foreach ($users as $name => $properties) {
if (isset($properties['password']) && substr($properties['password'], 0, 1) != '$') {
$users[$name]['password'] = $replace[$name] = password_hash($properties['password'], null);
}
}
if (count($replace)) {
$lines = file(CONFIG_FILE);
$current = null;
foreach ($lines as &$line) {
if (preg_match('/^\s*\[(\w+)\]\s*$/', $line, $match)) {
$current = $match[1];
continue;
}
if ($current && isset($replace[$current]) && preg_match('/^\s*password\s*=/', $line)) {
$line = sprintf("password = %s\n", var_export($replace[$current], true));
}
}
unset($line, $current);
file_put_contents(CONFIG_FILE, implode('', $lines));
}
$storage->users = $users;
}
foreach (DEFAULT_CONFIG as $key => $value) {
if (array_key_exists($key, $config)) {
$value = $config[$key];
}
if (is_bool(DEFAULT_CONFIG[$key])) {
$value = boolval($value);
}
define('PicoDAV\\' . $key, $value);
}
$dav = new Server;
$dav->setStorage(new Storage);
$dav->setStorage($storage);
$dav->setBaseURI($root);

View File

@ -17,6 +17,7 @@ $php = file_get_contents('server.php');
$php = strtr($php, [
'//__KD2\WebDAV\Server__' => clean_php_source('lib/KD2/WebDAV/Server.php'),
'//__KD2\WebDAV\AbstractStorage__' => clean_php_source('lib/KD2/WebDAV/AbstractStorage.php'),
'/*__HTACCESS__*/' => var_export(file_get_contents('.htaccess'), true),
'__JS_SIZE__' => filesize('webdav.js'),
'__CSS_SIZE__' => filesize('webdav.css'),
]);

View File

@ -21,34 +21,107 @@ namespace PicoDAV
const PUT_IGNORE_PATTERN = '!^~(?:lock\.|^\._)|^(?:\.DS_Store|Thumbs\.db|desktop\.ini)$!';
protected string $path;
protected ?string $user = null;
public array $users = [];
public function __construct()
{
$this->path = __DIR__ . '/';
}
public function auth(): bool
{
if (ANONYMOUS_WRITE && ANONYMOUS_READ) {
return true;
}
if ($this->user) {
return true;
}
$user = $_SERVER['PHP_AUTH_USER'] ?? null;
$password = $_SERVER['PHP_AUTH_PW'] ?? null;
$hash = $this->users[$user]['password'] ?? null;
if (!$hash) {
return false;
}
if (!password_verify($password, $hash)) {
return false;
}
$this->user = $user;
return true;
}
static protected function glob(string $path, string $pattern = '', int $flags = 0): array
{
$path = preg_replace('/[\*\?\[\]]/', '\\\\$0', $path);
return glob($path . $pattern, $flags);
}
public function canRead(string $uri): bool
{
if (in_array($uri, INTERNAL_FILES)) {
return false;
}
if (preg_match('/\.(?:php\d?|phtml|phps)$|^\./i', $uri)) {
return false;
}
if (ANONYMOUS_READ) {
return true;
}
if ($this->auth()) {
return true;
}
return false;
}
public function canWrite(string $uri): bool
{
if (!$this->user && !ANONYMOUS_WRITE) {
return false;
}
if (!$this->canRead($uri)) {
return false;
}
if (ANONYMOUS_WRITE) {
return true;
}
if (!empty($this->users[$this->user]['write'])) {
return true;
}
return false;
}
public function list(string $uri, ?array $properties): iterable
{
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, '/')));
natcasesort($dirs);
$files = self::glob($this->path . $uri, '/*');
$files = array_map('basename', $files);
$files = array_diff($files, $dirs);
// Remove PHP files from listings
$files = array_filter($files, fn($a) => !preg_match('/\.(?:php\d?|phtml|phps)$|^\./i', $a));
if (!$uri) {
$files = array_diff($files, ['webdav.js', 'webdav.css']);
}
// Remove PHP files and dot-files from listings
$files = array_filter($files, fn($a) => $this->canRead(ltrim($uri . '/' . $a, '/')));
natcasesort($files);
@ -59,8 +132,8 @@ namespace PicoDAV
public function get(string $uri): ?array
{
if (substr(basename($uri), 0, 1) == '.') {
throw new WebDAV_Exception('Invalid filename', 403);
if (!$this->canRead($uri)) {
throw new WebDAV_Exception('Access forbidden', 403);
}
$path = $this->path . $uri;
@ -117,7 +190,7 @@ namespace PicoDAV
case 'http://owncloud.org/ns:permissions':
$permissions = 'G';
if (is_writeable($target) && !FORCE_READONLY) {
if (is_writeable($target) && $this->canWrite($uri)) {
$permissions .= 'DNVWCK';
}
@ -166,12 +239,8 @@ namespace PicoDAV
return false;
}
if (FORCE_READONLY) {
throw new WebDAV_Exception('Write access is disabled', 403);
}
if (substr(basename($uri), 0, 1) == '.') {
throw new WebDAV_Exception('Invalid filename', 403);
if (!$this->canWrite($uri)) {
throw new WebDAV_Exception('Access forbidden', 403);
}
$target = $this->path . $uri;
@ -229,12 +298,8 @@ namespace PicoDAV
public function delete(string $uri): void
{
if (FORCE_READONLY) {
throw new WebDAV_Exception('Write access is disabled', 403);
}
if (substr(basename($uri), 0, 1) == '.') {
throw new WebDAV_Exception('Invalid filename', 403);
if (!$this->canWrite($uri)) {
throw new WebDAV_Exception('Access forbidden', 403);
}
$target = $this->path . $uri;
@ -261,16 +326,12 @@ namespace PicoDAV
public function copymove(bool $move, string $uri, string $destination): bool
{
if (FORCE_READONLY) {
throw new WebDAV_Exception('Write access is disabled', 403);
if (!$this->canWrite($uri)) {
throw new WebDAV_Exception('Access forbidden', 403);
}
if (substr(basename($uri), 0, 1) == '.') {
throw new WebDAV_Exception('Invalid filename', 403);
}
if (substr(basename($destination), 0, 1) == '.') {
throw new WebDAV_Exception('Invalid filename', 403);
if (!$this->canWrite($destination)) {
throw new WebDAV_Exception('Access forbidden', 403);
}
$source = $this->path . $uri;
@ -334,12 +395,8 @@ namespace PicoDAV
public function mkcol(string $uri): void
{
if (FORCE_READONLY) {
throw new WebDAV_Exception('Write access is disabled', 403);
}
if (substr(basename($uri), 0, 1) == '.') {
throw new WebDAV_Exception('Invalid filename', 403);
if (!$this->canWrite($uri)) {
throw new WebDAV_Exception('Access forbidden', 403);
}
if (!disk_free_space($this->path)) {
@ -397,6 +454,20 @@ namespace PicoDAV
return $out;
}
function error(WebDAV_Exception $e)
{
if ($e->getCode() == 403 && !$this->storage->auth() && count($this->storage->users)) {
$user = $_SERVER['PHP_AUTH_USER'] ?? null;
http_response_code(401);
header('WWW-Authenticate: Basic realm="Please login"');
echo '<h2>Error 401</h2><h1>You need to login to access this.</h1>';
return;
}
parent::error($e);
}
}
}
@ -414,27 +485,8 @@ namespace {
$relative_uri = ltrim(substr($uri, strlen($root)), '/');
const DEFAULT_CONFIG = [
'FORCE_READONLY' => false,
];
$config = [];
if (file_exists(__DIR__ . '/.picodav.ini')) {
$config = parse_ini_file(__DIR__ . '/.picodav.ini');
$config = array_change_key_case($config, \CASE_UPPER);
}
foreach (DEFAULT_CONFIG as $key => $value) {
if (array_key_exists($key, $config)) {
$value = $config[$key];
}
if (is_bool(DEFAULT_CONFIG[$key])) {
$value = boolval($value);
}
define('PicoDAV\\' . $key, $value);
if (!empty($_SERVER['SERVER_SOFTWARE']) && stristr($_SERVER['SERVER_SOFTWARE'], 'apache') && !file_exists(__DIR__ . '/.htaccess')) {
file_put_contents(__DIR__ . '/.htaccess', /*__HTACCESS__*/);
}
if ($relative_uri == 'webdav.js' || $relative_uri == 'webdav.css') {
@ -469,8 +521,70 @@ namespace {
exit;
}
const CONFIG_FILE = __DIR__ . '/.picodav.ini';
const INTERNAL_FILES = ['.picodav.ini', 'index.php', 'webdav.js', 'webdav.css'];
const DEFAULT_CONFIG = [
'ANONYMOUS_READ' => false,
'ANONYMOUS_WRITE' => false,
];
$config = [];
$storage = new Storage;
if (file_exists(CONFIG_FILE)) {
$config = parse_ini_file(CONFIG_FILE, true);
$users = array_filter($config, 'is_array');
$config = array_diff_key($config, $users);
$config = array_change_key_case($config, \CASE_UPPER);
$replace = [];
// Encrypt plaintext passwords
foreach ($users as $name => $properties) {
if (isset($properties['password']) && substr($properties['password'], 0, 1) != '$') {
$users[$name]['password'] = $replace[$name] = password_hash($properties['password'], null);
}
}
if (count($replace)) {
$lines = file(CONFIG_FILE);
$current = null;
foreach ($lines as &$line) {
if (preg_match('/^\s*\[(\w+)\]\s*$/', $line, $match)) {
$current = $match[1];
continue;
}
if ($current && isset($replace[$current]) && preg_match('/^\s*password\s*=/', $line)) {
$line = sprintf("password = %s\n", var_export($replace[$current], true));
}
}
unset($line, $current);
file_put_contents(CONFIG_FILE, implode('', $lines));
}
$storage->users = $users;
}
foreach (DEFAULT_CONFIG as $key => $value) {
if (array_key_exists($key, $config)) {
$value = $config[$key];
}
if (is_bool(DEFAULT_CONFIG[$key])) {
$value = boolval($value);
}
define('PicoDAV\\' . $key, $value);
}
$dav = new Server;
$dav->setStorage(new Storage);
$dav->setStorage($storage);
$dav->setBaseURI($root);