Compare commits

...

5 commits
0.1.8 ... main

Author SHA1 Message Date
bohwaz 0a0ec2297f Update dependencies, remove recursive directory time 2023-12-31 16:59:19 +01:00
bohwaz 6e431043aa Make sure we accept user if password is not set 2023-07-23 00:52:01 +02:00
bohwaz 4bfef21755 Update README, fix #10 2023-05-09 14:17:07 +02:00
BohwaZ 18ba8ffb17
Merge pull request #9 from clach04/patch-1
clarify password hashing
2023-04-02 17:56:49 +02:00
clach04 dd83f184a9
clarify password hashing
Code hashes (a best practice) passwords rather than encrypting them.
2023-04-02 08:53:08 -07:00
4 changed files with 172 additions and 117 deletions

View file

@ -1,4 +1,5 @@
all: clean index.php
KD2FW_URL := https://fossil.kd2.org/kd2fw/doc/trunk/src/lib/KD2/
deps:
@-mkdir -p lib/KD2/WebDAV

View file

@ -35,9 +35,9 @@ You can use any WebDAV client, but we recommend these:
It's really as simple as it says: just upload the [`index.php`](https://github.com/kd2org/picodav/raw/main/index.php) file 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
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 provided `.htaccess` as a basis for your server configuration.
The only requirement is PHP 7.4, or more recent (8.0 and 8.1 are also supported, not tested with PHP 8.2).
The only requirement is PHP 7.4, or more recent (8.0-8.2 are also supported).
Note that by default, write access is disabled for security purposes. See below to enable write access.
@ -71,9 +71,9 @@ 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.
Note that PicoDAV will replace this password with a hashed 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:
Here is an example of the password once it has been hashed:
```
password = '$2y$10$fbdabTjNPN3gMAUlaSEoR.kKHLnh0yMGneuJ7P2AOhSSNr8gUaCPu'

231
index.php
View file

@ -25,6 +25,16 @@ namespace KD2\WebDAV
'DAV::quota-available-bytes',
];
const PROP_NAMESPACE_MICROSOFT = 'urn:schemas-microsoft-com:';
const MODIFICATION_TIME_PROPERTIES = [
'DAV::lastmodified',
'DAV::creationdate',
'DAV::getlastmodified',
'urn:schemas-microsoft-com::Win32LastModifiedTime',
'urn:schemas-microsoft-com::Win32CreationTime',
];
// Custom properties
const PROP_DIGEST_MD5 = 'urn:karadav:digest_md5';
@ -56,7 +66,8 @@ namespace KD2\WebDAV
public function setBaseURI(string $uri): void
{
$this->base_uri = rtrim($uri, '/') . '/';
$this->base_uri = '/' . ltrim($uri, '/');
$this->base_uri = rtrim($this->base_uri, '/') . '/';
}
protected function extendExecutionTime(): void
@ -104,7 +115,7 @@ namespace KD2\WebDAV
foreach ($list as $file => $props) {
if (null === $props) {
$props = $this->storage->properties(trim($uri . '/' . $file, '/'), self::BASIC_PROPERTIES, 0);
$props = $this->storage->propfind(trim($uri . '/' . $file, '/'), self::BASIC_PROPERTIES, 0);
}
$collection = !empty($props['DAV::resourcetype']) && $props['DAV::resourcetype'] == 'collection';
@ -209,7 +220,7 @@ namespace KD2\WebDAV
elseif (!empty($_SERVER['HTTP_OC_CHECKSUM'])
&& preg_match('/MD5:[a-f0-9]{32}|SHA1:[a-f0-9]{40}/', $_SERVER['HTTP_OC_CHECKSUM'], $match)) {
$hash_algo = strtok($match[0], ':');
$hash = strtok(false);
$hash = strtok('');
}
$uri = $this->_prefix($uri);
@ -218,7 +229,7 @@ namespace KD2\WebDAV
if (!empty($_SERVER['HTTP_IF_MATCH'])) {
$etag = trim($_SERVER['HTTP_IF_MATCH'], '" ');
$prop = $this->storage->properties($uri, ['DAV::getetag'], 0);
$prop = $this->storage->propfind($uri, ['DAV::getetag'], 0);
if (!empty($prop['DAV::getetag']) && $prop['DAV::getetag'] != $etag) {
throw new Exception('ETag did not match condition', 412);
@ -229,10 +240,6 @@ namespace KD2\WebDAV
// This expects a UNIX timestamp
$mtime = (int)($_SERVER['HTTP_X_OC_MTIME'] ?? 0) ?: null;
if ($mtime) {
header('X-OC-MTime: accepted');
}
$this->extendExecutionTime();
$stream = fopen('php://input', 'r');
@ -252,9 +259,17 @@ namespace KD2\WebDAV
fseek($stream, 0, SEEK_SET);
}
$created = $this->storage->put($uri, $stream, $hash_algo, $hash, $mtime);
$created = $this->storage->put($uri, $stream, $hash_algo, $hash);
$prop = $this->storage->properties($uri, ['DAV::getetag'], 0);
if ($mtime) {
$mtime = new \DateTime('@' . $mtime);
if ($this->storage->touch($uri, $mtime)) {
header('X-OC-MTime: accepted');
}
}
$prop = $this->storage->propfind($uri, ['DAV::getetag'], 0);
if (!empty($prop['DAV::getetag'])) {
$value = $prop['DAV::getetag'];
@ -282,7 +297,7 @@ namespace KD2\WebDAV
$requested_props[] = self::PROP_DIGEST_MD5;
}
$props = $this->storage->properties($uri, $requested_props, 0);
$props = $this->storage->propfind($uri, $requested_props, 0);
if (!$props) {
throw new Exception('Resource Not Found', 404);
@ -363,7 +378,7 @@ namespace KD2\WebDAV
}
if (!isset($file['content']) && !isset($file['resource']) && !isset($file['path'])) {
throw new \RuntimeException('Invalid file array returned by ::get()');
throw new \RuntimeException('Invalid file array returned by ::get(): ' . print_r($file, true));
}
$this->extendExecutionTime();
@ -573,7 +588,7 @@ namespace KD2\WebDAV
// should do just nothing, see 'depth_zero_copy' test in litmus
if ($depth == 0
&& $this->storage->exists($destination)
&& current($this->storage->properties($destination, ['DAV::resourcetype'], 0)) == 'collection') {
&& current($this->storage->propfind($destination, ['DAV::resourcetype'], 0)) == 'collection') {
$overwritten = $this->storage->exists($uri);
}
else {
@ -692,18 +707,24 @@ namespace KD2\WebDAV
$requested_keys = $requested ? array_keys($requested) : null;
// Find root element properties
$properties = $this->storage->properties($uri, $requested_keys, $depth);
$properties = $this->storage->propfind($uri, $requested_keys, $depth);
if (null === $properties) {
throw new Exception('This does not exist', 404);
}
if (isset($properties['DAV::getlastmodified'])) {
foreach (self::MODIFICATION_TIME_PROPERTIES as $name) {
$properties[$name] = $properties['DAV::getlastmodified'];
}
}
$items = [$uri => $properties];
if ($depth) {
foreach ($this->storage->list($uri, $requested) as $file => $properties) {
$path = trim($uri . '/' . $file, '/');
$properties = $properties ?? $this->storage->properties($path, $requested_keys, 0);
$properties = $properties ?? $this->storage->propfind($path, $requested_keys, 0);
if (!$properties) {
$this->log('!!! Cannot find "%s"', $path);
@ -947,19 +968,93 @@ namespace KD2\WebDAV
$uri = $this->_prefix($uri);
$this->checkLock($uri);
$prefix = '<?xml version="1.0" encoding="utf-8"?>' . "\n";
$prefix.= '<d:multistatus xmlns:d="DAV:"';
$suffix = "</d:multistatus>\n";
$body = file_get_contents('php://input');
$this->storage->setProperties($uri, $body);
$properties = $this->parsePropPatch($body);
$root_namespaces = [];
$i = 0;
$set_time = null;
$set_time_name = null;
foreach ($properties as $name => $value) {
$pos = strrpos($name, ':');
$ns = substr($name, 0, $pos);
if (!array_key_exists($ns, $root_namespaces)) {
$alias = 'rns' . $i++;
$root_namespaces[$ns] = $alias;
$prefix .= sprintf(' xmlns:%s="%s"', $alias, htmlspecialchars($ns, ENT_XML1));
}
}
// See if the client wants to set the modification time
foreach (self::MODIFICATION_TIME_PROPERTIES as $name) {
if (!array_key_exists($name, $properties) || $value['action'] !== 'set' || empty($value['content'])) {
continue;
}
$ts = $value['content'];
if (ctype_digit($ts)) {
$ts = '@' . $ts;
}
$set_time = new \DateTime($value['content']);
$set_time_name = $name;
}
$prefix .= sprintf(">\n<d:response>\n <d:href>%s</d:href>\n", htmlspecialchars($url, ENT_XML1));
// http_response_code doesn't know the 207 status code
header('HTTP/1.1 207 Multi-Status', true);
header('Content-Type: application/xml; charset=utf-8');
header('Content-Type: application/xml; charset=utf-8', true);
$out = '<?xml version="1.0" encoding="utf-8"?>' . "\n";
$out .= '<d:multistatus xmlns:d="DAV:">';
$out .= '</d:multistatus>';
if (!count($properties)) {
return $prefix . $suffix;
}
return $out;
if ($set_time) {
unset($properties[$set_time_name]);
}
$return = $this->storage->proppatch($uri, $properties);
if ($set_time && $this->touch($uri, $set_time)) {
$return[$set_time_name] = 200;
}
$out = '';
static $messages = [
200 => 'OK',
403 => 'Forbidden',
409 => 'Conflict',
427 => 'Failed Dependency',
507 => 'Insufficient Storage',
];
foreach ($return as $name => $status) {
$pos = strrpos($name, ':');
$ns = substr($name, 0, $pos);
$name = substr($name, $pos + 1);
$out .= " <d:propstat>\n <d:prop>";
$out .= sprintf("<%s:%s /></d:prop>\n <d:status>HTTP/1.1 %d %s</d:status>",
$root_namespaces[$ns],
$name,
$status,
$messages[$status] ?? ''
);
$out .= "\n </d:propstat>\n";
}
$out .= "</d:response>\n";
return $prefix . $out . $suffix;
}
public function http_lock(string $uri): ?string
@ -1110,7 +1205,7 @@ namespace KD2\WebDAV
&& preg_match('/\(<([^>]*)>\s+\["([^""]+)"\]\)/', $_SERVER['HTTP_IF'], $match)) {
$token = $match[1];
$request_etag = $match[2];
$etag = current($this->storage->properties($uri, ['DAV::getetag'], 0));
$etag = current($this->storage->propfind($uri, ['DAV::getetag'], 0));
if ($request_etag != $etag) {
throw new Exception('Resource is locked and etag does not match', 412);
@ -1163,9 +1258,10 @@ namespace KD2\WebDAV
{
$uri = parse_url($source, PHP_URL_PATH);
$uri = rawurldecode($uri);
$uri = rtrim($uri, '/');
$uri = trim($uri, '/');
$uri = '/' . $uri;
if ($uri . '/' == $this->base_uri) {
if ($uri . '/' === $this->base_uri) {
$uri .= '/';
}
@ -1190,6 +1286,7 @@ namespace KD2\WebDAV
$uri = $_SERVER['REQUEST_URI'] ?? '/';
}
$uri = '/' . ltrim($uri, '/');
$this->original_uri = $uri;
if ($uri . '/' == $this->base_uri) {
@ -1200,7 +1297,7 @@ namespace KD2\WebDAV
$uri = substr($uri, strlen($this->base_uri));
}
else {
$this->log('<= %s is not a managed URL', $uri);
$this->log('<= %s is not a managed URL (%s)', $uri, $this->base_uri);
return false;
}
@ -1293,14 +1390,14 @@ namespace KD2\WebDAV
abstract public function exists(string $uri): bool;
abstract public function properties(string $uri, ?array $requested_properties, int $depth): ?array;
abstract public function propfind(string $uri, ?array $requested_properties, int $depth): ?array;
public function setProperties(string $uri, string $body): void
public function proppatch(string $uri, array $properties): array
{
// By default, properties are not saved
}
abstract public function put(string $uri, $pointer, ?string $hash_algo, ?string $hash, ?int $mtime): bool;
abstract public function put(string $uri, $pointer, ?string $hash_algo, ?string $hash): bool;
abstract public function delete(string $uri): void;
@ -1312,6 +1409,8 @@ namespace KD2\WebDAV
abstract public function list(string $uri, array $properties): iterable;
abstract public function touch(string $uri, \DateTimeInterface $timestamp): bool;
public function lock(string $uri, string $token, string $scope): void
{
// By default locking is not implemented
@ -1367,13 +1466,15 @@ namespace PicoDAV
$user = $_SERVER['PHP_AUTH_USER'] ?? null;
$password = $_SERVER['PHP_AUTH_PW'] ?? null;
$hash = $this->users[$user]['password'] ?? null;
if (!$hash) {
if (!array_key_exists($user, $this->users)) {
return false;
}
if (!password_verify($password, $hash)) {
$hash = $this->users[$user]['password'] ?? null;
// If no password is set, we accept any password as we consider that a .htaccess/.htpasswd
// access has been granted
if (null !== $hash && !password_verify($password, $hash)) {
return false;
}
@ -1532,13 +1633,8 @@ namespace PicoDAV
case 'DAV::resourcetype':
return is_dir($target) ? 'collection' : '';
case 'DAV::getlastmodified':
if (!$uri && $depth == 0 && is_dir($target)) {
$mtime = self::getDirectoryMTime($target);
}
else {
$mtime = filemtime($target);
}
$mtime = filemtime($target);
if (!$mtime) {
return null;
}
@ -1585,7 +1681,7 @@ namespace PicoDAV
return null;
}
public function properties(string $uri, ?array $properties, int $depth): ?array
public function propfind(string $uri, ?array $properties, int $depth): ?array
{
$target = $this->path . $uri;
@ -1610,7 +1706,7 @@ namespace PicoDAV
return $out;
}
public function put(string $uri, $pointer, ?string $hash_algo, ?string $hash, ?int $mtime): bool
public function put(string $uri, $pointer, ?string $hash_algo, ?string $hash): bool
{
if (preg_match(self::PUT_IGNORE_PATTERN, basename($uri))) {
return false;
@ -1670,10 +1766,6 @@ namespace PicoDAV
rename($tmp_file, $target);
}
if ($mtime) {
@touch($target, $mtime);
}
return $new;
}
@ -1800,28 +1892,10 @@ namespace PicoDAV
mkdir($target, 0770);
}
static public function getDirectoryMTime(string $path): int
public function touch(string $uri, \DateTimeInterface $datetime): bool
{
$last = 0;
$path = rtrim($path, '/');
foreach (self::glob($path, '/*', GLOB_NOSORT) as $f) {
if (is_dir($f)) {
$m = self::getDirectoryMTime($f);
if ($m > $last) {
$last = $m;
}
}
$m = filemtime($f);
if ($m > $last) {
$last = $m;
}
}
return $last;
$target = $this->path . $uri;
return @touch($target, $datetime->getTimestamp());
}
}
@ -1940,11 +2014,11 @@ RewriteRule ^.*$ /index.php [END]
$fp = fopen(__FILE__, 'r');
if ($relative_uri == '.webdav/webdav.js') {
fseek($fp, 52608, SEEK_SET);
echo fread($fp, 27798);
fseek($fp, 55024, SEEK_SET);
echo fread($fp, 27891);
}
else {
fseek($fp, 52608 + 27798, SEEK_SET);
fseek($fp, 55024 + 27891, SEEK_SET);
echo fread($fp, 7004);
}
@ -2187,12 +2261,14 @@ const WebDAVNavigator = (url, options) => {
};
const wopi_init = async () => {
if (!wopi_discovery_url) {
try {
var d = await reqXML('GET', wopi_discovery_url);
}
catch (e) {
reloadListing();
return;
}
var d = await reqXML('GET', wopi_discovery_url);
d.querySelectorAll('app').forEach(app => {
var mime = (a = app.getAttribute('name').match(/^.*\/.*$/)) ? a[0] : null;
wopi_mimes[mime] = {};
@ -2381,6 +2457,10 @@ const WebDAVNavigator = (url, options) => {
};
const formatDate = (date) => {
if (isNaN(date)) {
return '';
}
var now = new Date;
var nb_hours = (+(now) - +(date)) / 3600 / 1000;
@ -2571,7 +2651,7 @@ const WebDAVNavigator = (url, options) => {
$('.download_all').onclick = download_all;
}
if (!root_permissions || root_permissions.indexOf('CK') != -1) {
if (!root_permissions || root_permissions.indexOf('C') != -1 || root_permissions.indexOf('K') != -1) {
$('.upload').insertAdjacentHTML('afterbegin', create_buttons);
$('.mkdir').onclick = () => {
@ -2806,11 +2886,10 @@ const WebDAVNavigator = (url, options) => {
document.querySelector('html').innerHTML = html_tpl;
// Wait for WOPI discovery before creating the list
if (wopi_discovery_url) {
// Wait for WOPI discovery before creating the list
wopi_init();
}
else {
} else {
reloadListing();
}

View file

@ -43,13 +43,15 @@ namespace PicoDAV
$user = $_SERVER['PHP_AUTH_USER'] ?? null;
$password = $_SERVER['PHP_AUTH_PW'] ?? null;
$hash = $this->users[$user]['password'] ?? null;
if (!$hash) {
if (!array_key_exists($user, $this->users)) {
return false;
}
if (!password_verify($password, $hash)) {
$hash = $this->users[$user]['password'] ?? null;
// If no password is set, we accept any password as we consider that a .htaccess/.htpasswd
// access has been granted
if (null !== $hash && !password_verify($password, $hash)) {
return false;
}
@ -208,12 +210,7 @@ namespace PicoDAV
case 'DAV::resourcetype':
return is_dir($target) ? 'collection' : '';
case 'DAV::getlastmodified':
if (!$uri && $depth == 0 && is_dir($target)) {
$mtime = self::getDirectoryMTime($target);
}
else {
$mtime = filemtime($target);
}
$mtime = filemtime($target);
if (!$mtime) {
return null;
@ -261,7 +258,7 @@ namespace PicoDAV
return null;
}
public function properties(string $uri, ?array $properties, int $depth): ?array
public function propfind(string $uri, ?array $properties, int $depth): ?array
{
$target = $this->path . $uri;
@ -286,7 +283,7 @@ namespace PicoDAV
return $out;
}
public function put(string $uri, $pointer, ?string $hash_algo, ?string $hash, ?int $mtime): bool
public function put(string $uri, $pointer, ?string $hash_algo, ?string $hash): bool
{
if (preg_match(self::PUT_IGNORE_PATTERN, basename($uri))) {
return false;
@ -346,10 +343,6 @@ namespace PicoDAV
rename($tmp_file, $target);
}
if ($mtime) {
@touch($target, $mtime);
}
return $new;
}
@ -476,28 +469,10 @@ namespace PicoDAV
mkdir($target, 0770);
}
static public function getDirectoryMTime(string $path): int
public function touch(string $uri, \DateTimeInterface $datetime): bool
{
$last = 0;
$path = rtrim($path, '/');
foreach (self::glob($path, '/*', GLOB_NOSORT) as $f) {
if (is_dir($f)) {
$m = self::getDirectoryMTime($f);
if ($m > $last) {
$last = $m;
}
}
$m = filemtime($f);
if ($m > $last) {
$last = $m;
}
}
return $last;
$target = $this->path . $uri;
return @touch($target, $datetime->getTimestamp());
}
}