diff --git a/app/Http/Controllers/DavController.php b/app/Http/Controllers/DavController.php index aa6fbc7..01b5681 100644 --- a/app/Http/Controllers/DavController.php +++ b/app/Http/Controllers/DavController.php @@ -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; } } - diff --git a/app/Services/Dav/LaravelSabreAuthBackend.php b/app/Services/Dav/LaravelSabreAuthBackend.php index 5a4e510..b35b410 100644 --- a/app/Services/Dav/LaravelSabreAuthBackend.php +++ b/app/Services/Dav/LaravelSabreAuthBackend.php @@ -1,59 +1,40 @@ $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; } } diff --git a/app/Services/Dav/LaravelSabrePrincipalBackend.php b/app/Services/Dav/LaravelSabrePrincipalBackend.php new file mode 100644 index 0000000..fc613b9 --- /dev/null +++ b/app/Services/Dav/LaravelSabrePrincipalBackend.php @@ -0,0 +1,76 @@ +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.'); + } +} diff --git a/app/Services/Dav/PrincipalBackend.php b/app/Services/Dav/PrincipalBackend.php deleted file mode 100644 index bc915cc..0000000 --- a/app/Services/Dav/PrincipalBackend.php +++ /dev/null @@ -1,74 +0,0 @@ -" - 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/" - '{DAV:}displayname' => $u->displayname ?? $u->name, - '{http://sabredav.org/ns}email-address' => $u->email, - ]; - } -} diff --git a/curl.html b/curl.html new file mode 100644 index 0000000..0e70e41 --- /dev/null +++ b/curl.html @@ -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 + + +/dav/principals/andrew@kithkin.lan/HTTP/1.1 200 OK diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index abe117c..811be65 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -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'); diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index a87739f..227109b 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -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, ]); diff --git a/routes/web.php b/routes/web.php index 77ddfa4..8b937ae 100644 --- a/routes/web.php +++ b/routes/web.php @@ -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]);