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:
		
							parent
							
								
									dbfc474105
								
							
						
					
					
						commit
						8d0738019f
					
				@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										76
									
								
								app/Services/Dav/LaravelSabrePrincipalBackend.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								app/Services/Dav/LaravelSabrePrincipalBackend.php
									
									
									
									
									
										Normal 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.');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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
									
								
							
							
						
						
									
										11
									
								
								curl.html
									
									
									
									
									
										Normal 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>
 | 
			
		||||
@ -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');
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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]);
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user