7

2023-35: 从飞书导出日历到 Fastmail

 1 year ago
source link: https://xuanwo.io/reports/2023-35/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

2023-35: 从飞书导出日历到 Fastmail

在 Databend Labs,我们主要通过飞书进行日常沟通和任务协调。对于需要语音交流的会议,我们通过日历功能来统一安排时间。我个人则更倾向于使用 Fastmail,用其服务管理邮件和日程。因此,我开始考虑能否将飞书的日历同步到 Fastmail,以便在一个平台上统一管理所有日程。

理论上,这并不复杂:

  • 飞书提供了 CalDAV 来支持日历同步
  • Fastmail 允许订阅 CalDAV 服务

看似只需在 Fastmail 上适当配置 CalDAV 即可。然而,现实是每次尝试配置飞书的 CalDAV 到 Fastmail 时都会遇到报错。以前我对此不以为意,但这个周末,我决定彻底解决这个问题。

TL; DR

飞书暴露的 CalDAV 实现并不标准,对客户端的 Auto Discovery 行为有依赖。解决方案是手动请求飞书 CalDAV 服务,获取到真实的地址再配置。

首先发送 PROPFIND 到 /<username> 获取飞书生成的随机日历 ID

curl -v -X PROPFIND -H "Depth: 1" -H "Content-Type: application/xml" -u "<username>:<password>" https://caldav.feishu.cn/calendars/

在响应中会有形如 <D:response><D:href>/calendars/<uuid>/</D:href></D:response> 的输出,这里的 /calendars/<uuid>/ 就是飞书为个人日历生成的随机路径。

然后在 CalDAV 客户端配置如下即可:

  • Username: <username>
  • Password: <password>
  • Server URL: https://caldav.feishu.cn/calendars/<uuid>/

在日历同步领域我们主要遇到以下标准:

CalDAV

CalDAV 是基于 WebDAV 的扩展,是 HTTP 扩展的一部分,允许用户或应用程序读取和写入存储在远程服务器上的日历数据。CalDAV 的主要应用场合是在多个设备或多个应用之间同步日历信息,包括事件、提醒和其他相关数据。常见的服务如 Google Calendar 和 Apple iCloud 都支持 CalDAV。

iCal 是一个文件格式标准,正式名为 iCalendar(.ics 文件),用于存储日历事件、提醒和其他时间相关信息。这种文件格式被广泛接受和支持,可以用于导入、导出和共享日历数据。在日历服务中我们最常用的订阅日历的方式就是通过 iCal

漫长的调试

根据飞书提供的文档 个人用户如何设置本地系统日历到飞书日历的单向同步,并根据一些最基础的信任,我们可以先假定飞书这个功能的实现是正确的,它没有在文档里面说瞎话,也就是飞书的 CalDAV 功能在 macOS 和 Windows 10 上是正常的,那 Fastmail 不正常只能是 CalDAV Client 具体实现上的一些差异。

通过查询文档和 ChatGPT,我们能够知道 CalDAV 最简单的使用方式是通过 PROPFIND 来查询指定条目的属性:

飞书的实际输出是压缩后的,这里进行了格式化

> curl -X PROPFIND -H "Depth: 1" -H "Content-Type: application/xml" -u "<username>:<password>" https://caldav.feishu.cn/
<?xml version="1.0" encoding="UTF-8"?>
<D:multistatus
    xmlns:D="DAV:"
    xmlns:C="urn:ietf:params:xml:ns:caldav"
    xmlns:CS="http://calendarserver.org/ns/"
    xmlns:ICAL="http://apple.com/ns/ical/"
    xmlns:ME="http://me.com/_namespace/">
    <D:response>
        <D:href>/</D:href>
    </D:response>
</D:multistatus>

我们预期这里输出的应该是根路径的属性,但是这里只有一个 href。虽然还不知道该怎么继续,但是我们已经能够知道请求是正常响应的,只不过内容不符合预期。根据已有的输出,我猜想飞书使用了 calendarserver 进行了一些魔改。再加上飞书这个 CalDAV 应该是多租户的,所以我问 ChatGPT,如果有多个用户的话,该如何请求?

> calendarserver 如果有多个用户的话,他们的用户路径是什么样的?

在多用户环境中使用CalendarServer,一种常用的开源日历服务器,通常涉及为每个用户设置单独的访问路径和账户。这通常通过一种叫做路径路由的机制来实现,该机制允许每个用户通过特定的URL路径来访问其各自的日历数据。

假设CalendarServer的基本URL是 `https://calendar.example.com/`,多个用户(比如 `Alice` 和 `Bob`)可能会有如下的用户路径:

- Alice: `https://calendar.example.com/calendars/users/alice/`
- Bob: `https://calendar.example.com/calendars/users/bob/`

在这些路径下,各用户通常会有多个子路径,用于表示个人日历、工作日历、假期日历等。例如,Alice 的个人日历可能位于 `https://calendar.example.com/calendars/users/alice/personal/`。

这样的路径结构有助于实现多用户环境下的访问控制、数据隔离和个性化设置。这通常通过与后端数据库或者身份验证服务(如LDAP或OAuth)的集成来进一步强化。

综上所述,多用户的CalendarServer通常会为每个用户提供一个专有的URL路径,用于访问他们各自的日历数据和设置。这样的设计有助于在多用户环境中实现安全和数据管理。

好,现在我们大胆猜想飞书沿用了同样的设计:

> curl -X PROPFIND -H "Depth: 1" -H "Content-Type: application/xml" -u "<username>:<password>" https://caldav.feishu.cn/calendars/users/<username>
<?xml version="1.0" encoding="UTF-8"?>
<D:multistatus
    xmlns:D="DAV:"
    xmlns:C="urn:ietf:params:xml:ns:caldav"
    xmlns:CS="http://calendarserver.org/ns/"
    xmlns:ICAL="http://apple.com/ns/ical/"
    xmlns:ME="http://me.com/_namespace/">
</D:multistatus>

很遗憾,没有猜对。访问 /calendars/users/<username> 是一个完全空的结果,那 /calendars/users/ 呢?

> curl -X PROPFIND -H "Depth: 1" -H "Content-Type: application/xml" -u "<username>:<password>" https://caldav.feishu.cn/calendars/users/
< HTTP/1.1 400 Bad Request
< Server: TLB
< Content-Length: 0
< Connection: keep-alive

有趣,直接报错了,那更进一步,访问 /calendars/ 会输出什么呢?

> curl -X PROPFIND -H "Depth: 1" -H "Content-Type: application/xml" -u "<username>:<password>" https://caldav.feishu.cn/calendars/
<?xml version="1.0" encoding="UTF-8"?>
<D:multistatus
    xmlns:D="DAV:"
    xmlns:C="urn:ietf:params:xml:ns:caldav"
    xmlns:CS="http://calendarserver.org/ns/"
    xmlns:ICAL="http://apple.com/ns/ical/"
    xmlns:ME="http://me.com/_namespace/">
    <D:response>
        <D:href>/calendars/</D:href>
    </D:response>
    <D:response>
        <D:href>/calendars/<uuid>/</D:href>
    </D:response>
</D:multistatus>

有变化了!我们拿到了一个新的 href,指向了一个 uuid,我们延续这个思路继续请求:

> curl -X PROPFIND -H "Depth: 1" -H "Content-Type: application/xml" -u "<username>:<password>" https://caldav.feishu.cn/calendars/<uuid>/
<?xml version="1.0" encoding="UTF-8"?>
<D:multistatus
    xmlns:D="DAV:"
    xmlns:C="urn:ietf:params:xml:ns:caldav"
    xmlns:CS="http://calendarserver.org/ns/"
    xmlns:ICAL="http://apple.com/ns/ical/"
    xmlns:ME="http://me.com/_namespace/">
    <D:response>
        <D:href>/calendars/<uuid>/d0f47ac4-a047-4589-b7c1-9af6e3cc471b.ics</D:href>
    </D:response>
    <D:response>
        <D:href>/calendars/<uuid>/bb010e10-cd76-4213-9d47-28a9bf48417e.ics</D:href>
    </D:response>
    <D:response>
        <D:href>/calendars/<uuid>/c0e28572-ed2e-447c-88ed-251dd437eb72.ics</D:href>
    </D:response>
    <D:response>
        <D:href>/calendars/<uuid>/2fb352a3-37d3-4487-aea2-dbc1b18ec371.ics</D:href>
    </D:response>
    <D:response>
        <D:href>/calendars/<uuid>/c987311c-3fee-4be5-881c-7d8e5113deea.ics</D:href>
    </D:response>
</D:multistatus>

好,我们现在拿到了一系列指向 ics 的路径,看起来每一个 ics 指向了一个具体的事件。我使用 https://caldav.feishu.cn/calendars/<uuid>/ 作为 Server URL 尝试连接,发现 Fastmail 成功连上了飞书的 CalDAV 并正确的获取到了事件!

万物皆草台班子

好,飞书已经成功连上了,但是我还是非常好奇,为什么刚刚好是 /calendars/<uuid> 呢?看着 PROPFIND 返回的结果,我尝试了一下访问 /

> curl -X PROPFIND -H "Depth: 1" -H "Content-Type: application/xml" -u "<username>:<password>" https://caldav.feishu.cn//
<?xml version="1.0" encoding="UTF-8"?>
<D:multistatus
    xmlns:D="DAV:"
    xmlns:C="urn:ietf:params:xml:ns:caldav"
    xmlns:CS="http://calendarserver.org/ns/"
    xmlns:ICAL="http://apple.com/ns/ical/"
    xmlns:ME="http://me.com/_namespace/">
    <D:response>
        <D:href>//</D:href>
    </D:response>
    <D:response>
        <D:href>//<uuid>/</D:href>
    </D:response>
</D:multistatus>

蛤?我简直不敢相信自己的眼睛,随后我尝试了一些其他的可能:

> curl -X PROPFIND -H "Depth: 1" -H "Content-Type: application/xml" -u "<username>:<password>" https://caldav.feishu.cn/feishu_is_really_cool/
<?xml version="1.0" encoding="UTF-8"?>
<D:multistatus
    xmlns:D="DAV:"
    xmlns:C="urn:ietf:params:xml:ns:caldav"
    xmlns:CS="http://calendarserver.org/ns/"
    xmlns:ICAL="http://apple.com/ns/ical/"
    xmlns:ME="http://me.com/_namespace/">
    <D:response>
        <D:href>/feishu_is_really_cool/</D:href>
    </D:response>
    <D:response>
        <D:href>/feishu_is_really_cool/61AEE45F-1E58-401C-61AE-E45F1E58401C/</D:href>
    </D:response>
</D:multistatus>

我懂了:飞书的 CalDAV 实现是如此的草台,以至于它只有在根路径下工作不正确。

本文分享了我调试飞书 CalDAV 的全过程,感谢 ChatGPT 的大力支持和飞书团队给予我的惊喜~


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK