Reverts authentication back to email for compatibility, seeing correct 207s, updates database migrations and seed for new URI scheme; note that I think the DavController is outdated or needs cleanup.

This commit is contained in:
Andrew Gioia 2025-07-18 09:58:28 -04:00
parent dbfc474105
commit 8d0738019f
Signed by: andrew
GPG Key ID: FC09694A000800C8
8 changed files with 146 additions and 213 deletions

View File

@ -2,101 +2,45 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Sabre\DAV\Server;
use Sabre\DAV\Auth\Plugin as AuthPlugin;
use Sabre\DAV\Browser\Plugin as BrowserPlugin;
use Sabre\CalDAV\Plugin as CalDAVPlugin;
use Sabre\CardDAV\Plugin as CardDAVPlugin;
use Sabre\CalDAV\Backend\PDO as CalDavPDO;
use Sabre\CardDAV\Backend\PDO as CardDavPDO;
use Symfony\Component\HttpFoundation\Response;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
use App\Services\Dav\PrincipalBackend;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use App\Services\Dav\LaravelSabreAuthBackend;
use App\Services\Dav\LaravelSabrePrincipalBackend;
use Sabre\DAV;
use Sabre\DAV\Auth\Plugin as AuthPlugin;
use Sabre\DAVACL\Plugin as ACLPlugin;
use Sabre\DAVACL\PrincipalCollection;
class DavController extends Controller
{
/**
* Single-action controller: handles every WebDAV request.
*/
public function __invoke(Request $laravelRequest): Response
public function handle()
{
//\Log::debug('DavController reached', ['path' => $larevelRequest->path()]);
//\Log::debug('[DAV] raw headers', getallheaders());
$authBackend = new LaravelSabreAuthBackend();
$principalBackend = new LaravelSabrePrincipalBackend();
\Log::info('SabreDAV DavController');
/**
* sabre back-ends tied to laravel's current db connection
*/
$pdo = DB::connection()->getPdo();
$principalBackend = new PrincipalBackend();
$calBackend = new CalDavPDO($pdo);
$cardBackend = new CardDavPDO($pdo);
/**
* resource tree
*/
$nodes = [
new \Sabre\DAVACL\PrincipalCollection($principalBackend),
new \Sabre\CalDAV\CalendarRoot($principalBackend, $calBackend),
new \Sabre\CardDAV\AddressBookRoot($principalBackend, $cardBackend),
new PrincipalCollection($principalBackend),
// Add your Calendars or Addressbooks here
];
/**
* create the server
*/
$server = new Server($nodes);
$server = new DAV\Server($nodes);
$server->setBaseUri('/dav/');
/**
* add plugins (order matters!)
*/
if ($existing = $server->getPlugin('auth')) {
$server->removePlugin($existing);
}
$server->addPlugin(
new AuthPlugin(new LaravelSabreAuthBackend(), 'KithkinDAV')
);
$server->addPlugin(new \Sabre\DAVACL\Plugin());
//$server->addPlugin(new \Sabre\CalDAV\Plugin());
//$server->addPlugin(new \Sabre\CardDAV\Plugin());
//$server->addPlugin(new \Sabre\DAV\Browser\Plugin());
$server->addPlugin(new AuthPlugin($authBackend, 'WebDAV'));
$server->addPlugin(new ACLPlugin());
/**
* execute (sabre sends the response and exits)
*/
// logging
\Log::debug('[DAV] listener attached to', ['hash' => spl_object_hash($server)]);
$server->on('beforeMethod', function ($req) {
\Log::debug('[DAV] beforeMethod', [
'verb' => $req->getMethod(),
'url' => $req->getUrl(),
'auth' => $req->getHeader('Authorization'),
]);
$server->on('beforeMethod', function () {
\Log::info('SabreDAV beforeMethod triggered');
});
$server->on('method:PROPFIND', function ($req) {
\Log::debug('[DAV] method:PROPFIND', [
'url' => $req->getUrl(),
'auth' => $req->getHeader('Authorization'),
]);
});
\Log::debug('[DAV] about to exec', ['hash' => spl_object_hash($server)]);
ob_start();
$server->exec();
$body = ob_get_clean();
$sabre = $server->httpResponse;
return new Response($body, $sabre->getStatus(), $sabre->getHeaders());
$status = $server->httpResponse->getStatus();
$content = ob_get_contents();
ob_end_clean();
$server->exec();
exit;
}
}

View File

@ -1,59 +1,40 @@
<?php
// app/Services/Dav/LaravelSabreAuthBackend.php
namespace App\Services\Dav;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Sabre\DAV\Auth\Backend\AbstractBasic;
use Sabre\DAV\Auth\Backend\BasicCallBack;
class LaravelSabreAuthBackend extends AbstractBasic
{
/** Sabre stores the authenticated principal URI here */
protected ?string $currentUser = null;
/**
* Sabre calls this after extracting Basic-Auth credentials.
* Return TRUE when the credentials are valid; FALSE otherwise.
*
* @param string|null $username
* @param string|null $password
*/
protected function validateUserPass($username, $password): bool
public function __construct()
{
//\Log::debug('[DAV] auth called', ['u'=>$u]);
//$this->currentUser = 'principals/' . (User::first()->id ?? 'dummy');
//zreturn true;
if (!$username) {
return false; // no credentials supplied
}
// Allow login via e-mail OR the "short" user name
$user = User::where('email', $username)
->orWhere('name', $username)
->first();
if (!$user || !Hash::check($password, $user->password)) {
return false; // invalid creds
}
// Log the user into Laravel so policies / Auth::user() work
Auth::setUser($user);
// Tell Sabre which principal this login maps to (ULID-based)
$this->currentUser = 'principals/' . $user->id;
return true;
\Log::info('LaravelSabreAuthBackend instantiated');
}
/**
* Optional Sabre may call this when it needs to know
* who is currently authenticated.
*/
public function getCurrentUser(): ?string
protected function validateUserPass($username, $password)
{
return $this->currentUser;
$user = User::where('email', $username)->first();
if ($user && Hash::check($password, $user->password)) {
// THIS is the new required step
$this->currentPrincipal = 'principals/' . $user->email;
\Log::info('SabreDAV auth success', ['principal' => $this->currentPrincipal]);
return true;
}
\Log::warning('SabreDAV auth failed', ['username' => $username]);
return false;
}
public function getCurrentPrincipal()
{
\Log::debug('SabreDAV getCurrentPrincipal', ['current' => $$this->currentPrincipal]);
return $this->currentPrincipal;
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace App\Services\Dav;
use Sabre\DAV\PropPatch;
use Sabre\DAVACL\PrincipalBackend\AbstractBackend;
use App\Models\User;
use Illuminate\Support\Facades\Log;
class LaravelSabrePrincipalBackend extends AbstractBackend
{
public function getPrincipalsByPrefix($prefixPath)
{
return User::all()->map(function ($user) {
return [
'uri' => 'principals/' . $user->id,
'{DAV:}displayname' => $user->name,
];
})->toArray();
}
public function getPrincipalByPath($path)
{
\Log::info('getPrincipalByPath', ['path' => $path]);
$email = basename($path);
$user = User::where('email', $email)->first();
if ($user) {
return [
'uri' => 'principals/' . $user->email,
'{DAV:}displayname' => $user->name,
];
}
\Log::warning('No user found for principal path', ['key' => $key]);
return null;
}
public function updatePrincipal($path, PropPatch $propPatch)
{
// Not implementing updates to principal properties
$propPatch->handle([], fn () => true);
return false;
}
public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof')
{
// No search functionality
return [];
}
public function findByUri($uri, $principalPrefix)
{
// No group lookup support
return null;
}
public function getGroupMemberSet($principal)
{
// Not supporting group principals
return [];
}
public function getGroupMembership($principal)
{
// Not supporting group memberships
return [];
}
public function setGroupMemberSet($principal, array $members)
{
// Not supporting modifying groups
throw new \Sabre\DAV\Exception\MethodNotAllowed('Setting group members not supported.');
}
}

View File

@ -1,74 +0,0 @@
<?php
namespace App\Services\Dav;
use Sabre\DAVACL\PrincipalBackend\AbstractBackend;
use App\Models\User;
/**
* Minimal principal backend pulls principals straight from `users`.
*/
class PrincipalBackend extends AbstractBackend
{
/* ---------- mandatory look-ups ---------- */
public function getPrincipalByPath($path)
{
// "principals/<ulid>"
if (! str_starts_with($path, 'principals/')) {
return null;
}
$id = substr($path, strlen('principals/'));
$user = User::find($id);
return $user ? $this->format($user) : null;
}
public function getPrincipalsByPrefix($prefixPath)
{
// Sabre passes "principals" as the prefix for /principals/*
return User::all()->map(fn ($u) => $this->format($u))->all();
}
/* ---------- the five extra interface methods ---------- */
public function updatePrincipal($path, $mutations)
{
// not writable for now
return false;
}
public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof', $limit = 100)
{
// not implemented yet
return [];
}
public function getGroupMemberSet($principal)
{
return [];
}
public function getGroupMembership($principal)
{
return [];
}
public function setGroupMemberSet($principal, array $members)
{
return false; // read-only
}
/* ---------- helper ---------- */
private function format(User $u)
{
return [
'id' => $u->id,
'uri' => $u->uri, // "principals/<ULID>"
'{DAV:}displayname' => $u->displayname ?? $u->name,
'{http://sabredav.org/ns}email-address' => $u->email,
];
}
}

11
curl.html Normal file
View File

@ -0,0 +1,11 @@
HTTP/2 207
server: nginx/1.27.5
date: Fri, 18 Jul 2025 13:51:55 GMT
content-type: application/xml; charset=utf-8
x-powered-by: PHP/8.4.8
x-sabre-version: 4.7.0
vary: Brief,Prefer
dav: 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search
<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns"><d:response><d:href>/dav/principals/andrew@kithkin.lan/</d:href><d:propstat><d:prop><d:resourcetype><d:principal/></d:resourcetype></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat></d:response></d:multistatus>

View File

@ -9,9 +9,9 @@ return new class extends Migration
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->ulid('id')->primary(); // ULID PK
$table->string('uri')->unique()->nullable(); // from Sabre principals table
$table->string('displayname')->nullable(); // from Sabre principals table
$table->ulid('id')->primary(); // ulid primary key
$table->string('uri')->unique()->nullable()->after('email'); // formerly from sabre principals table
$table->string('displayname')->nullable(); // formerly from sabre principals table
$table->string('name')->nullable(); // custom name if necessary
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
@ -30,7 +30,6 @@ return new class extends Migration
$table->string('id')->primary();
$table->ulid('user_id')->nullable()->index(); // ULID FK
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');

View File

@ -27,9 +27,9 @@ class DatabaseSeeder extends Seeder
]
);
/** fill the Sabre-friendly columns once we know the ULID */
/** fill the Sabre-friendly columns */
$user->update([
'uri' => 'principals/'.$user->id,
'uri' => 'principals/'.$user->email,
'displayname' => $user->name,
]);

View File

@ -55,14 +55,10 @@ require __DIR__.'/auth.php';
| Leave this outside the auth middleware; the SabreLaravelAuthPlugin handles
| authentication for DAV clients.
*/
Route::match(
[
'GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS',
'PROPFIND', 'PROPPATCH', 'REPORT', 'MKCOL',
'COPY', 'MOVE', 'LOCK', 'UNLOCK',
],
'/dav/{path?}',
DavController::class
)
->withoutMiddleware([VerifyCsrfToken::class]) // disable CSRF check
->where('path', '.*');
['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD', 'PROPFIND', 'PROPPATCH', 'MKCOL', 'COPY', 'MOVE', 'REPORT', 'LOCK', 'UNLOCK'],
'/dav/{any?}',
[DavController::class, 'handle']
)->where('any', '.*')
->withoutMiddleware([VerifyCsrfToken::class]);