Add CardDav and CalDav servers (#80)
* Add CardDav and CalDav servers This implements the servers, but not the clients (yet). The implementation is essentially a proxy to Radicale (as a container in `docker-compose.yml`), with certain security assurances. If you're upgrading, basically you'll need to create a new `data-radicale` directory, and everything else should just work. This will also release v2.3.0 with those enabled by default. Tested with Thunderbird and Apple Calendar + Contacts. To disable these, simply add the new config details and comment out or don't add the new `radicale` service from `docker-compose.yml`. Related to #56
This commit is contained in:
9
routes/.well-known/caldav.tsx
Normal file
9
routes/.well-known/caldav.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Handler } from 'fresh/server.ts';
|
||||
|
||||
import { FreshContextState } from '/lib/types.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
export const handler: Handler<Data, FreshContextState> = () => {
|
||||
return new Response('Redirecting...', { status: 301, headers: { 'Location': '/caldav' } });
|
||||
};
|
||||
9
routes/.well-known/carddav.tsx
Normal file
9
routes/.well-known/carddav.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Handler } from 'fresh/server.ts';
|
||||
|
||||
import { FreshContextState } from '/lib/types.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
export const handler: Handler<Data, FreshContextState> = () => {
|
||||
return new Response('Redirecting...', { status: 301, headers: { 'Location': '/carddav' } });
|
||||
};
|
||||
@@ -7,7 +7,10 @@ export const handler = [
|
||||
async function handleCors(request: Request, context: FreshContext<FreshContextState>) {
|
||||
const path = new URL(request.url).pathname;
|
||||
|
||||
if (request.method == 'OPTIONS' && path !== '/dav' && !path.startsWith('/dav/')) {
|
||||
if (
|
||||
request.method == 'OPTIONS' && path !== '/dav' && !path.startsWith('/dav/') && path !== '/carddav' &&
|
||||
!path.startsWith('/carddav/') && path !== '/caldav' && !path.startsWith('/caldav/')
|
||||
) {
|
||||
const response = new Response(null, {
|
||||
status: 204,
|
||||
});
|
||||
@@ -68,10 +71,14 @@ export const handler = [
|
||||
const response = await context.next();
|
||||
|
||||
console.info(`${new Date().toISOString()} - [${response.status}] ${request.method} ${request.url}`);
|
||||
// NOTE: Uncomment when debugging WebDav stuff
|
||||
// if (request.url.includes('/dav')) {
|
||||
// NOTE: Uncomment when debugging WebDav/CardDav/CalDav stuff
|
||||
// if (request.url.includes('/dav') || request.url.includes('/carddav') || request.url.includes('/caldav')) {
|
||||
// console.info(`Request`, request.headers);
|
||||
// console.info((await request.clone().text()) || '<No Body>');
|
||||
// try {
|
||||
// console.info((await request.clone().text()) || '<No Body>');
|
||||
// } catch (_error) {
|
||||
// console.info('<No Body>');
|
||||
// }
|
||||
// console.info(`Response`, response.headers);
|
||||
// console.info(`Status`, response.status);
|
||||
// }
|
||||
|
||||
69
routes/caldav.tsx
Normal file
69
routes/caldav.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Handler, RouteConfig } from 'fresh/server.ts';
|
||||
|
||||
import { FreshContextState } from '/lib/types.ts';
|
||||
import { AppConfig } from '/lib/config.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
export const config: RouteConfig = {
|
||||
routeOverride: '/caldav/:path*',
|
||||
};
|
||||
|
||||
export const handler: Handler<Data, FreshContextState> = async (request, context) => {
|
||||
const calendarConfig = await AppConfig.getCalendarConfig();
|
||||
|
||||
if (!calendarConfig.enableCalDavServer) {
|
||||
return new Response('Not Found', { status: 404 });
|
||||
}
|
||||
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', {
|
||||
status: 401,
|
||||
headers: { 'www-authenticate': 'Basic realm="bewCloud", charset="UTF-8"' },
|
||||
});
|
||||
}
|
||||
|
||||
const { path } = context.params;
|
||||
|
||||
const userId = context.state.user.id;
|
||||
|
||||
try {
|
||||
const requestBodyText = await request.clone().text();
|
||||
|
||||
// Remove the `/caldav/` prefix from the hrefs in the request
|
||||
const parsedRequestBodyText = requestBodyText.replaceAll('<href>/caldav/', `<href>/`).replaceAll(
|
||||
':href>/caldav/',
|
||||
`:href>/`,
|
||||
);
|
||||
|
||||
const response = await fetch(`${calendarConfig.calDavUrl}/${path}`, {
|
||||
headers: {
|
||||
...Object.fromEntries(request.headers.entries()),
|
||||
'X-Remote-User': `${userId}`,
|
||||
},
|
||||
method: request.method,
|
||||
body: parsedRequestBodyText,
|
||||
});
|
||||
|
||||
if (response.status === 204) {
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
||||
const responseBodyText = await response.clone().text();
|
||||
|
||||
// Add the `/caldav/` prefix to the hrefs in the response
|
||||
const parsedBodyResponseText = responseBodyText.replaceAll('<href>/', `<href>/caldav/`).replaceAll(
|
||||
':href>/',
|
||||
`:href>/caldav/`,
|
||||
);
|
||||
|
||||
return new Response(parsedBodyResponseText, {
|
||||
status: response.status,
|
||||
headers: response.headers,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return new Response(null, { status: 405 });
|
||||
};
|
||||
69
routes/carddav.tsx
Normal file
69
routes/carddav.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Handler, RouteConfig } from 'fresh/server.ts';
|
||||
|
||||
import { FreshContextState } from '/lib/types.ts';
|
||||
import { AppConfig } from '/lib/config.ts';
|
||||
|
||||
interface Data {}
|
||||
|
||||
export const config: RouteConfig = {
|
||||
routeOverride: '/carddav/:path*',
|
||||
};
|
||||
|
||||
export const handler: Handler<Data, FreshContextState> = async (request, context) => {
|
||||
const contactsConfig = await AppConfig.getContactsConfig();
|
||||
|
||||
if (!contactsConfig.enableCardDavServer) {
|
||||
return new Response('Not Found', { status: 404 });
|
||||
}
|
||||
|
||||
if (!context.state.user) {
|
||||
return new Response('Unauthorized', {
|
||||
status: 401,
|
||||
headers: { 'www-authenticate': 'Basic realm="bewCloud", charset="UTF-8"' },
|
||||
});
|
||||
}
|
||||
|
||||
const { path } = context.params;
|
||||
|
||||
const userId = context.state.user.id;
|
||||
|
||||
try {
|
||||
const requestBodyText = await request.clone().text();
|
||||
|
||||
// Remove the `/carddav/` prefix from the hrefs in the request
|
||||
const parsedRequestBodyText = requestBodyText.replaceAll('<href>/carddav/', `<href>/`).replaceAll(
|
||||
':href>/carddav/',
|
||||
`:href>/`,
|
||||
);
|
||||
|
||||
const response = await fetch(`${contactsConfig.cardDavUrl}/${path}`, {
|
||||
headers: {
|
||||
...Object.fromEntries(request.headers.entries()),
|
||||
'X-Remote-User': `${userId}`,
|
||||
},
|
||||
method: request.method,
|
||||
body: parsedRequestBodyText,
|
||||
});
|
||||
|
||||
if (response.status === 204) {
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
||||
const responseBodyText = await response.clone().text();
|
||||
|
||||
// Add the `/carddav/` prefix to the hrefs in the response
|
||||
const parsedBodyResponseText = responseBodyText.replaceAll('<href>/', `<href>/carddav/`).replaceAll(
|
||||
':href>/',
|
||||
`:href>/carddav/`,
|
||||
);
|
||||
|
||||
return new Response(parsedBodyResponseText, {
|
||||
status: response.status,
|
||||
headers: response.headers,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return new Response(null, { status: 405 });
|
||||
};
|
||||
@@ -11,6 +11,8 @@ interface Data {
|
||||
currentPath: string;
|
||||
baseUrl: string;
|
||||
isFileSharingAllowed: boolean;
|
||||
isCardDavEnabled: boolean;
|
||||
isCalDavEnabled: boolean;
|
||||
}
|
||||
|
||||
export const handler: Handlers<Data, FreshContextState> = {
|
||||
@@ -40,6 +42,11 @@ export const handler: Handlers<Data, FreshContextState> = {
|
||||
const userFiles = await FileModel.list(context.state.user.id, currentPath);
|
||||
|
||||
const isPublicFileSharingAllowed = await AppConfig.isPublicFileSharingAllowed();
|
||||
const contactsConfig = await AppConfig.getContactsConfig();
|
||||
const calendarConfig = await AppConfig.getCalendarConfig();
|
||||
|
||||
const isCardDavEnabled = contactsConfig.enableCardDavServer;
|
||||
const isCalDavEnabled = calendarConfig.enableCalDavServer;
|
||||
|
||||
return await context.render({
|
||||
userDirectories,
|
||||
@@ -47,6 +54,8 @@ export const handler: Handlers<Data, FreshContextState> = {
|
||||
currentPath,
|
||||
baseUrl,
|
||||
isFileSharingAllowed: isPublicFileSharingAllowed,
|
||||
isCardDavEnabled,
|
||||
isCalDavEnabled,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -60,6 +69,8 @@ export default function FilesPage({ data }: PageProps<Data, FreshContextState>)
|
||||
initialPath={data.currentPath}
|
||||
baseUrl={data.baseUrl}
|
||||
isFileSharingAllowed={data.isFileSharingAllowed}
|
||||
isCardDavEnabled={data.isCardDavEnabled}
|
||||
isCalDavEnabled={data.isCalDavEnabled}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user