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;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
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 App\Services\Dav\LaravelSabreAuthBackend;
|
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
|
class DavController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
public function handle()
|
||||||
* Single-action controller: handles every WebDAV request.
|
|
||||||
*/
|
|
||||||
public function __invoke(Request $laravelRequest): Response
|
|
||||||
{
|
{
|
||||||
//\Log::debug('DavController reached', ['path' => $larevelRequest->path()]);
|
$authBackend = new LaravelSabreAuthBackend();
|
||||||
//\Log::debug('[DAV] raw headers', getallheaders());
|
$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 = [
|
$nodes = [
|
||||||
new \Sabre\DAVACL\PrincipalCollection($principalBackend),
|
new PrincipalCollection($principalBackend),
|
||||||
new \Sabre\CalDAV\CalendarRoot($principalBackend, $calBackend),
|
// Add your Calendars or Addressbooks here
|
||||||
new \Sabre\CardDAV\AddressBookRoot($principalBackend, $cardBackend),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
$server = new DAV\Server($nodes);
|
||||||
* create the server
|
|
||||||
*/
|
|
||||||
$server = new Server($nodes);
|
|
||||||
$server->setBaseUri('/dav/');
|
$server->setBaseUri('/dav/');
|
||||||
|
|
||||||
/**
|
$server->addPlugin(new AuthPlugin($authBackend, 'WebDAV'));
|
||||||
* add plugins (order matters!)
|
$server->addPlugin(new ACLPlugin());
|
||||||
*/
|
|
||||||
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->on('beforeMethod', function () {
|
||||||
* execute (sabre sends the response and exits)
|
\Log::info('SabreDAV beforeMethod triggered');
|
||||||
*/
|
|
||||||
// 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('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();
|
ob_start();
|
||||||
$server->exec();
|
$server->exec();
|
||||||
$body = ob_get_clean();
|
|
||||||
$sabre = $server->httpResponse;
|
$status = $server->httpResponse->getStatus();
|
||||||
return new Response($body, $sabre->getStatus(), $sabre->getHeaders());
|
$content = ob_get_contents();
|
||||||
|
ob_end_clean();
|
||||||
|
$server->exec();
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,59 +1,40 @@
|
|||||||
<?php
|
<?php
|
||||||
// app/Services/Dav/LaravelSabreAuthBackend.php
|
|
||||||
|
|
||||||
namespace App\Services\Dav;
|
namespace App\Services\Dav;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
use Sabre\DAV\Auth\Backend\AbstractBasic;
|
use Sabre\DAV\Auth\Backend\AbstractBasic;
|
||||||
|
use Sabre\DAV\Auth\Backend\BasicCallBack;
|
||||||
|
|
||||||
class LaravelSabreAuthBackend extends AbstractBasic
|
class LaravelSabreAuthBackend extends AbstractBasic
|
||||||
{
|
{
|
||||||
/** Sabre stores the authenticated principal URI here */
|
public function __construct()
|
||||||
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
|
|
||||||
{
|
{
|
||||||
//\Log::debug('[DAV] auth called', ['u'=>$u]);
|
\Log::info('LaravelSabreAuthBackend instantiated');
|
||||||
//$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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
protected function validateUserPass($username, $password)
|
||||||
* Optional — Sabre may call this when it needs to know
|
|
||||||
* who is currently authenticated.
|
|
||||||
*/
|
|
||||||
public function getCurrentUser(): ?string
|
|
||||||
{
|
{
|
||||||
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
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('users', function (Blueprint $table) {
|
Schema::create('users', function (Blueprint $table) {
|
||||||
$table->ulid('id')->primary(); // ULID PK
|
$table->ulid('id')->primary(); // ulid primary key
|
||||||
$table->string('uri')->unique()->nullable(); // from Sabre principals table
|
$table->string('uri')->unique()->nullable()->after('email'); // formerly from sabre principals table
|
||||||
$table->string('displayname')->nullable(); // from Sabre principals table
|
$table->string('displayname')->nullable(); // formerly from sabre principals table
|
||||||
$table->string('name')->nullable(); // custom name if necessary
|
$table->string('name')->nullable(); // custom name if necessary
|
||||||
$table->string('email')->unique();
|
$table->string('email')->unique();
|
||||||
$table->timestamp('email_verified_at')->nullable();
|
$table->timestamp('email_verified_at')->nullable();
|
||||||
@ -30,7 +30,6 @@ return new class extends Migration
|
|||||||
$table->string('id')->primary();
|
$table->string('id')->primary();
|
||||||
$table->ulid('user_id')->nullable()->index(); // ULID FK
|
$table->ulid('user_id')->nullable()->index(); // ULID FK
|
||||||
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
|
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
|
||||||
|
|
||||||
$table->string('ip_address', 45)->nullable();
|
$table->string('ip_address', 45)->nullable();
|
||||||
$table->text('user_agent')->nullable();
|
$table->text('user_agent')->nullable();
|
||||||
$table->longText('payload');
|
$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([
|
$user->update([
|
||||||
'uri' => 'principals/'.$user->id,
|
'uri' => 'principals/'.$user->email,
|
||||||
'displayname' => $user->name,
|
'displayname' => $user->name,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -55,14 +55,10 @@ require __DIR__.'/auth.php';
|
|||||||
| Leave this outside the auth middleware; the SabreLaravelAuthPlugin handles
|
| Leave this outside the auth middleware; the SabreLaravelAuthPlugin handles
|
||||||
| authentication for DAV clients.
|
| authentication for DAV clients.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Route::match(
|
Route::match(
|
||||||
[
|
['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD', 'PROPFIND', 'PROPPATCH', 'MKCOL', 'COPY', 'MOVE', 'REPORT', 'LOCK', 'UNLOCK'],
|
||||||
'GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS',
|
'/dav/{any?}',
|
||||||
'PROPFIND', 'PROPPATCH', 'REPORT', 'MKCOL',
|
[DavController::class, 'handle']
|
||||||
'COPY', 'MOVE', 'LOCK', 'UNLOCK',
|
)->where('any', '.*')
|
||||||
],
|
->withoutMiddleware([VerifyCsrfToken::class]);
|
||||||
'/dav/{path?}',
|
|
||||||
DavController::class
|
|
||||||
)
|
|
||||||
->withoutMiddleware([VerifyCsrfToken::class]) // disable CSRF check
|
|
||||||
->where('path', '.*');
|
|
||||||
|
Loading…
Reference in New Issue
Block a user