fix dav autodiscovery: well-known needs every method, /dav needs no cors
two bugs, both of which iOS reports as "Cannot connect using SSL" — a message about TLS for a problem that has nothing to do with TLS. the certificate was never involved. the well-known routes were registered with .get. RFC 6764 §6 has the client probe the well-known URI with the method it actually intends to use, and iOS sends PROPFIND — which fell through to the SPA catch-all and 404'd. they answered correctly in a browser, which is why they looked healthy. hono's cors() answers every OPTIONS itself as a preflight and never calls the route beneath it, so OPTIONS /dav/ returned a bare 204 with no DAV header. OPTIONS is not a preflight to a DAV client — it is how the client asks what the server can do, and iOS refuses an account whose server does not advertise calendar-access. cors now skips the DAV paths entirely; a CalDAV client is not a browser and has no origin to check. verified from the public internet: PROPFIND on both well-known paths 301s to /dav/, and an authenticated OPTIONS now returns `DAV: 1, 2, 3, calendar-access, addressbook, extended-mkcol`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+21
-6
@@ -59,8 +59,7 @@ export type { HonoVariables };
|
||||
|
||||
export const honoServer = new Hono<{ Variables: HonoVariables }>();
|
||||
|
||||
honoServer.use(
|
||||
cors({
|
||||
const corsMiddleware = cors({
|
||||
origin: (origin, c) => {
|
||||
const host = c.req.header('host');
|
||||
// TEMPORARY: see isMusicOriginExempt — echoes any Origin back for /api/music when enabled, so a
|
||||
@@ -70,8 +69,21 @@ honoServer.use(
|
||||
},
|
||||
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
|
||||
allowHeaders: ['Content-Type', 'Authorization'],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// The DAV doors are the one place CORS must NOT run, because hono's cors() answers every OPTIONS itself
|
||||
// as a preflight — 204, no body, no DAV headers — and never calls the route beneath it.
|
||||
//
|
||||
// OPTIONS is not a preflight to a DAV client. It is how the client asks what the server can do, and the
|
||||
// answer it needs is the `DAV: 1, 2, 3, calendar-access, addressbook` header. iOS sends it during account
|
||||
// setup and refuses the account when it is missing, reporting the failure as "Cannot connect using SSL" —
|
||||
// a message about TLS for a problem that has nothing to do with TLS, which is how this cost an evening.
|
||||
//
|
||||
// Nothing is lost by skipping it: a CalDAV client is not a browser and has no origin to check.
|
||||
const isDavPath = (path: string) =>
|
||||
path === '/dav' || path.startsWith('/dav/') || path === '/.well-known/caldav' || path === '/.well-known/carddav';
|
||||
|
||||
honoServer.use((ctx, next) => (isDavPath(ctx.req.path) ? next() : corsMiddleware(ctx, next)));
|
||||
|
||||
// Scoped-origin gate: restrict app origins (e.g. the music app) to their allowed path prefixes
|
||||
// (/api/auth + /api/music). No-ops for the main web origin and while OFFICER_MUSIC_ORIGIN is unset.
|
||||
@@ -99,8 +111,11 @@ honoServer.route('/dav', davSyncRouter);
|
||||
// credential, so they must sit above every auth gate. Without them iOS in particular degrades to
|
||||
// demanding a full collection URL, which is exactly the sort of thing that makes self-hosting feel
|
||||
// worse than the commercial product it is replacing.
|
||||
honoServer.get('/.well-known/caldav', (ctx) => ctx.redirect('/dav/', 301));
|
||||
honoServer.get('/.well-known/carddav', (ctx) => ctx.redirect('/dav/', 301));
|
||||
// `.all`, not `.get`: RFC 6764 §6 has the client probe the well-known URI with the method it actually
|
||||
// wants to use, and iOS sends PROPFIND, not GET. Registered as GET-only these answered 404 to every real
|
||||
// client while looking perfectly healthy in a browser.
|
||||
honoServer.all('/.well-known/caldav', (ctx) => ctx.redirect('/dav/', 301));
|
||||
honoServer.all('/.well-known/carddav', (ctx) => ctx.redirect('/dav/', 301));
|
||||
|
||||
const protectedRouter = createRouter();
|
||||
protectedRouter.use(bodyParser());
|
||||
|
||||
Reference in New Issue
Block a user