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