很早之前写过一篇《使用 Nginx 反代 Mastodon》,但那篇主要指对没有站点管理权的情况下如何添加反代域名,而且年代久远一些内容也需要更新。

应嘎嘎的要求,今天特别写一篇博文讲一讲 Mastodon 域名设置相关的内容。

本文篇幅可能会比较长,前半部分将大致讲解 Mastodon 的架构以及域名相关设置,后半部分将讲解如何更换 WEB_DOMAIN、如何添加反代域名。

Mastodon 大体架构

/images/2022/full-explanation-of-mastodon-domain-name-settings/Mastodon架构图.thumbnail.png

Mastodon 大体架构图

Mastodon 是一个前后端分离的应用,前端是使用 React 写成的单页应用,负责交互与展示;后端由 Ruby 写成负表提供 API 。

当用户访问时,HTML、XHR 请求反代至 Puma 服务, websocket 请求反代至 Node 服务,其余静态资源由 nginx 响应。

因此,Mastodon 也可以像 Pleroma 那样使用替换前端,例如:Cuckoo+Pinafore

/images/2022/full-explanation-of-mastodon-domain-name-settings/pinafore.bgme.bid.thumbnail.png

pinafore.bgme.bid

Mastodon 服务发现流程

众所周知,Mastodon 是一个基于 ActivityPub 协议联邦式的去中心化平台。

那么问题来了,Mastodon 是如何发现远程实例的?或者说当你在编辑框中写下 @[email protected] 后,Mastodon 是如何将其翻译为 ActivityPub 协议所需的 HTTPS URI 的?

Mastodon 帐户的两个身份标识:

  • webfinger acct URI:跨实例的可验证的全局用户身份标识。

  • actor URI:用于 federation 过程的其他所有方面。

Mastodon 域名配置

在 Mastodon 配置文件中,与用户使用相关的域名选项有 LOCAL_DOMAINWEB_DOMAINALTERNATE_DOMAINSSTREAMING_API_BASE_URLCDN_HOSTS3_ALIAS_HOST

LOCAL_DOMAIN

服务器在 Fediverse 网络中唯一标识,用于生成帐户 acct URI,确认后无法更改,修改该项配置将导致帐户 acct URI 改变,远程服务器会将现有帐户视为不同于之前的全新帐户。

例如: @[email protected] 将实例的 LOCAL_DOMAINmastodon.qpomelo.app 改为 qpomelo.cc,其acct也从 acct:[email protected] 变更为 acct:[email protected]

/images/2022/full-explanation-of-mastodon-domain-name-settings/local_domain_mastodon.qpomelo.app.thumbnail.png

@[email protected]

/images/2022/full-explanation-of-mastodon-domain-name-settings/local_domain_qpomelo.cc.thumbnail.png

@[email protected]

虽然实际上是同一个帐户,但由于acct不同,被远程实例视为两个不同的帐户。

WEB_DOMAIN

可选配置项,默认与 LOCAL_DOMAIN 相同,用于生成网页内容,诸如:actorinbox 等。通过设置 WEB_DOMAIN,可以将 Mastodon 服务运行于另一域名。

例如:假设 bgme.me 已经存在其它服务,将 LOCAL_DOMAIN 设为 bgme.me,将 WEB_DOMAIN 设为 mastodon.bgme.me,便可以保证帐户 acct URI 以 bgme.me 结尾的情况下将 Mastodon 相关服务运行在 mastodon.bgme.me 域名下。

/images/2022/full-explanation-of-mastodon-domain-name-settings/web_domain_admin_ui.thumbnail.png

@[email protected]

http "https://naraku.cc/.well-known/host-meta"  -p HBhb
GET /.well-known/host-meta HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Host: naraku.cc
User-Agent: HTTPie/3.2.1



HTTP/1.1 301 Moved Permanently
CF-Cache-Status: DYNAMIC
CF-RAY: 77ef38a2ef452ac1-LAX
Connection: keep-alive
Content-Type: text/html
Date: Sun, 25 Dec 2022 05:36:49 GMT
NEL: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
Report-To: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=iZw4%2BWLYieZU2hKsOvMH4bRt1nJSgQkvZgaiWgY7GsvaBWsDvOB8W0sLcPo7LHzmungp42AkWLkEfcU%2BTG2hrK%2F6jSOTLMlOkNkNNJVna7CjhUaukV%2FOvBrf6Mu5izCV0HKz9BfZq%2FA%3D"}],"group":"cf-nel","max_age":604800}
Server: cloudflare
Strict-Transport-Security: max-age=15552000; includeSubDomains; preload
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
access-control-allow-origin: *
alt-svc: h3=":443"; ma=86400, h3-29=":443"; ma=86400
location: https://mtd.naraku.cc/.well-known/host-meta

<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx</center>
</body>
</html>
http "https://mtd.naraku.cc/.well-known/host-meta"  -p HBhb
GET /.well-known/host-meta HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Host: mtd.naraku.cc
User-Agent: HTTPie/3.2.1



HTTP/1.1 200 OK
CF-Cache-Status: DYNAMIC
CF-RAY: 77ef36c5efab527b-LAX
Cache-Control: max-age=259200, public
Connection: keep-alive
Content-Type: application/xrd+xml; charset=utf-8
Date: Sun, 25 Dec 2022 05:35:33 GMT
NEL: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
Report-To: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=oXXheIvbL6bzP4E2Kp%2F6wgvMn8cvr2bXJgXNl8bO%2F86r4Gdy4z7fp3y7SMwVAAORKiy5Qh5rjVx6TKaLhpnHT7D2Xwh58g1r1W4HU6n%2BnJgnZZWaeqqnyMhZKEpHXTkJMkTwKT4FUtkefGU%2F"}],"group":"cf-nel","max_age":604800}
Server: cloudflare
Transfer-Encoding: chunked
alt-svc: h3=":443"; ma=86400, h3-29=":443"; ma=86400
content-security-policy: base-uri 'none'; default-src 'none'; frame-ancestors 'none'; font-src 'self' https://mtd.naraku.cc; img-src 'self' https: data: blob: https://mtd.naraku.cc; style-src 'self' https://mtd.naraku.cc 'nonce-PXsk8y8bu+tcBwEB5toFkw=='; media-src 'self' https: data: https://mtd.naraku.cc; frame-src 'self' https:; manifest-src 'self' https://mtd.naraku.cc; connect-src 'self' data: blob: https://mtd.naraku.cc https://mtd.naraku.cc wss://mtd.naraku.cc; script-src 'self' https://mtd.naraku.cc 'wasm-unsafe-eval'; child-src 'self' blob: https://mtd.naraku.cc; worker-src 'self' blob: https://mtd.naraku.cc
etag: W/"5205d754e2b6177b4be99ecc2e1413a7"
permissions-policy: interest-cohort=()
strict-transport-security: max-age=15552000; includeSubDomains; preload
vary: Accept, Origin
x-content-type-options: nosniff
x-frame-options: DENY
x-request-id: cd29d011-08e4-4117-b31b-f1f5c0f04c4b
x-runtime: 0.004306
x-xss-protection: 0

<?xml version="1.0" encoding="UTF-8"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
    <Link rel="lrdd" template="https://mtd.naraku.cc/.well-known/webfinger?resource={uri}"/>
</XRD>
http "https://mtd.naraku.cc/.well-known/webfinger?resource=acct:[email protected]"  -p HBhb
GET /.well-known/webfinger?resource=acct:[email protected] HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Host: mtd.naraku.cc
User-Agent: HTTPie/3.2.1



HTTP/1.1 200 OK
CF-Cache-Status: DYNAMIC
CF-RAY: 77ef33764ff15214-LAX
Cache-Control: max-age=259200, public
Connection: keep-alive
Content-Type: application/jrd+json; charset=utf-8
Date: Sun, 25 Dec 2022 05:33:17 GMT
NEL: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
Report-To: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=vQy9au8mhZlm8cgjGpTkPxU%2BOODjpwPh1jRwdMGLJZWblTWLidQyqgfQclB7%2BnFwA08aJYS2sQhoSoww7scjtJ6FQ2BVzUQan9TWZ4TIg%2BVHWx9oKDudox6VWThj9cDg9uyqp%2FU6C%2BsxqBmx"}],"group":"cf-nel","max_age":604800}
Server: cloudflare
Transfer-Encoding: chunked
alt-svc: h3=":443"; ma=86400, h3-29=":443"; ma=86400
content-security-policy: base-uri 'none'; default-src 'none'; frame-ancestors 'none'; font-src 'self' https://mtd.naraku.cc; img-src 'self' https: data: blob: https://mtd.naraku.cc; style-src 'self' https://mtd.naraku.cc 'nonce-HtWXbWc6yrKVW32Aqzv4Cw=='; media-src 'self' https: data: https://mtd.naraku.cc; frame-src 'self' https:; manifest-src 'self' https://mtd.naraku.cc; connect-src 'self' data: blob: https://mtd.naraku.cc https://mtd.naraku.cc wss://mtd.naraku.cc; script-src 'self' https://mtd.naraku.cc 'wasm-unsafe-eval'; child-src 'self' blob: https://mtd.naraku.cc; worker-src 'self' blob: https://mtd.naraku.cc
etag: W/"b465d414f933c0013f1e193073fabba4"
permissions-policy: interest-cohort=()
strict-transport-security: max-age=15552000; includeSubDomains; preload
vary: Accept, Origin
x-content-type-options: nosniff
x-frame-options: DENY
x-request-id: f6aa211d-bab6-47a1-87a7-fdd97b511e8b
x-runtime: 0.008764
x-xss-protection: 0

{
    "aliases": [
        "https://mtd.naraku.cc/@naraku",
        "https://mtd.naraku.cc/users/naraku"
    ],
    "links": [
        {
            "href": "https://mtd.naraku.cc/@naraku",
            "rel": "http://webfinger.net/rel/profile-page",
            "type": "text/html"
        },
        {
            "href": "https://mtd.naraku.cc/users/naraku",
            "rel": "self",
            "type": "application/activity+json"
        },
        {
            "rel": "http://ostatus.org/schema/1.0/subscribe",
            "template": "https://mtd.naraku.cc/authorize_interaction?uri={uri}"
        }
    ],
    "subject": "acct:[email protected]"
}
http "https://mtd.naraku.cc/users/naraku"  "Accept: application/activity+json" -p HBhb
GET /users/naraku HTTP/1.1
Accept: application/activity+json
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Host: mtd.naraku.cc
User-Agent: HTTPie/3.2.1



HTTP/1.1 200 OK
CF-Cache-Status: DYNAMIC
CF-RAY: 77ef47fb4c7f2b50-LAX
Cache-Control: max-age=180, public
Connection: keep-alive
Content-Type: application/activity+json; charset=utf-8
Date: Sun, 25 Dec 2022 05:47:18 GMT
NEL: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
Report-To: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=p9ppDYwQMI6z5Sc%2BY1FlfE2BxwxXfd%2F7yQSwwrQdyRHcKXvwUSF%2BJTug1i%2BQ7QAIz0nKix6XTVTH5fMrLNoIixkf4%2Bex29%2FavM6659cwkh8z6YN4W5A0%2B3onYsrQI%2F2pw9oWJx1Nd399QZRT"}],"group":"cf-nel","max_age":604800}
Server: cloudflare
Transfer-Encoding: chunked
alt-svc: h3=":443"; ma=86400, h3-29=":443"; ma=86400
content-security-policy: base-uri 'none'; default-src 'none'; frame-ancestors 'none'; font-src 'self' https://mtd.naraku.cc; img-src 'self' https: data: blob: https://mtd.naraku.cc; style-src 'self' https://mtd.naraku.cc 'nonce-0qN1dzer3t4zyTyWue1qqw=='; media-src 'self' https: data: https://mtd.naraku.cc; frame-src 'self' https:; manifest-src 'self' https://mtd.naraku.cc; connect-src 'self' data: blob: https://mtd.naraku.cc https://mtd.naraku.cc wss://mtd.naraku.cc; script-src 'self' https://mtd.naraku.cc 'wasm-unsafe-eval'; child-src 'self' blob: https://mtd.naraku.cc; worker-src 'self' blob: https://mtd.naraku.cc
etag: W/"416f589000111e76eae38f5f7eb69471"
permissions-policy: interest-cohort=()
referrer-policy: origin
strict-transport-security: max-age=15552000; includeSubDomains; preload
vary: Accept, Origin
x-content-type-options: nosniff
x-frame-options: DENY
x-request-id: 52b64673-7371-4292-b508-ef26d617f344
x-runtime: 0.023334
x-xss-protection: 0

{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://w3id.org/security/v1",
        {
            "Curve25519Key": "toot:Curve25519Key",
            "Device": "toot:Device",
            "Ed25519Key": "toot:Ed25519Key",
            "Ed25519Signature": "toot:Ed25519Signature",
            "Emoji": "toot:Emoji",
            "EncryptedMessage": "toot:EncryptedMessage",
            "PropertyValue": "schema:PropertyValue",
            "alsoKnownAs": {
                "@id": "as:alsoKnownAs",
                "@type": "@id"
            },
            "cipherText": "toot:cipherText",
            "claim": {
                "@id": "toot:claim",
                "@type": "@id"
            },
            "deviceId": "toot:deviceId",
            "devices": {
                "@id": "toot:devices",
                "@type": "@id"
            },
            "discoverable": "toot:discoverable",
            "featured": {
                "@id": "toot:featured",
                "@type": "@id"
            },
            "featuredTags": {
                "@id": "toot:featuredTags",
                "@type": "@id"
            },
            "fingerprintKey": {
                "@id": "toot:fingerprintKey",
                "@type": "@id"
            },
            "focalPoint": {
                "@container": "@list",
                "@id": "toot:focalPoint"
            },
            "identityKey": {
                "@id": "toot:identityKey",
                "@type": "@id"
            },
            "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
            "messageFranking": "toot:messageFranking",
            "messageType": "toot:messageType",
            "movedTo": {
                "@id": "as:movedTo",
                "@type": "@id"
            },
            "publicKeyBase64": "toot:publicKeyBase64",
            "schema": "http://schema.org#",
            "suspended": "toot:suspended",
            "toot": "http://joinmastodon.org/ns#",
            "value": "schema:value"
        }
    ],
    "attachment": [
        {
            "name": "Matrix",
            "type": "PropertyValue",
            "value": "<span class=\"h-card\"><a href=\"https://mtd.naraku.cc/@naraku\" class=\"u-url mention\">@<span>naraku</span></a></span>:naraku.cc"
        }
    ],
    "devices": "https://mtd.naraku.cc/users/naraku/collections/devices",
    "discoverable": true,
    "endpoints": {
        "sharedInbox": "https://mtd.naraku.cc/inbox"
    },
    "featured": "https://mtd.naraku.cc/users/naraku/collections/featured",
    "featuredTags": "https://mtd.naraku.cc/users/naraku/collections/tags",
    "followers": "https://mtd.naraku.cc/users/naraku/followers",
    "following": "https://mtd.naraku.cc/users/naraku/following",
    "icon": {
        "mediaType": "image/jpeg",
        "type": "Image",
        "url": "https://mtd.naraku.cc/system/accounts/avatars/109/513/842/758/850/375/original/355c3a2e588b4fba.jpeg"
    },
    "id": "https://mtd.naraku.cc/users/naraku",
    "image": {
        "mediaType": "image/jpeg",
        "type": "Image",
        "url": "https://mtd.naraku.cc/system/accounts/headers/109/513/842/758/850/375/original/156276bb27f3591e.jpeg"
    },
    "inbox": "https://mtd.naraku.cc/users/naraku/inbox",
    "manuallyApprovesFollowers": false,
    "name": "Naraku :mastodon:",
    "outbox": "https://mtd.naraku.cc/users/naraku/outbox",
    "preferredUsername": "naraku",
    "publicKey": {
        "id": "https://mtd.naraku.cc/users/naraku#main-key",
        "owner": "https://mtd.naraku.cc/users/naraku",
        "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArYihLR7VFAiMymGUwkxo\nmMQCaHd9FD59oUfGWGKVCsJ/IkeIbi0+jM5LyeC/QdwCC/ZRgMNvS4zIQgakSRoq\nkiRN3KL3Nv9Uqo9bjC21/H6bgPiZ1aEmck9sZgN0Polxwa3SPeJ08wY8AOWPnGrg\n0kin8+7D1pkemFSfJSJSjPvc9PrKOBCVdbF74haHA99LeHo6GO75P4iLHQnlw565\nVNrUnbtH52Bcoeavrt0SDdQX239z1YqmRxAuApYlg1l4Hy/+hpmUAoCQJs3ogsjF\nTCjo1jpHgcnKRHn3gLL3o7m4+SpeUuXtFOnsXVnYujpnY0p1ejPx8gwAXcI8kwNI\ngQIDAQAB\n-----END PUBLIC KEY-----\n"
    },
    "published": "2022-12-14T00:00:00Z",
    "summary": "<p>naraku.cc Admin :verify:</p><p>Stand with Ukraine! 🇺🇦 <br />Stand with Democracy!</p>",
    "tag": [
        {
            "icon": {
                "mediaType": "image/gif",
                "type": "Image",
                "url": "https://mtd.naraku.cc/system/custom_emojis/images/2022/000/000/973/original/20775f75cba35af7.gif"
            },
            "id": "https://mtd.naraku.cc/emojis/973",
            "name": ":verify:",
            "type": "Emoji",
            "updated": "2022-12-17T17:26:50Z"
        },
        {
            "icon": {
                "mediaType": "image/png",
                "type": "Image",
                "url": "https://mtd.naraku.cc/system/custom_emojis/images/2022/000/000/099/original/56fceeecb032309c.png"
            },
            "id": "https://mtd.naraku.cc/emojis/99",
            "name": ":mastodon:",
            "type": "Emoji",
            "updated": "2022-12-17T10:45:48Z"
        }
    ],
    "type": "Person",
    "url": "https://mtd.naraku.cc/@naraku"
}

ALTERNATE_DOMAINS

指向该服务器的其它域名。如果有多个域名指向 Mastodon 服务器,配置 ALTERNATE_DOMAINS 允许 Fediverse 服务通过其它域名发现帐户。可配置多个域名,域名之间使用逗号隔开,如 foo.com,bar.com

从实现上讲对于列入 ALTERNATE_DOMAINS 的域名,Mastodon 将响应来自这些域名的 WebFinger 查询请求,故其它实例可通过相应的后缀查找到原始帐户。

/images/2022/full-explanation-of-mastodon-domain-name-settings/webfinger_bgme_bgme.bid.thumbnail.png

通过 WebFinger 协议查询 acct:[email protected]

Mastodon v3.4.0 以后版本 [1] ,出于安全性考量,Puma 只响应 HostLOCAL_DOMAINWEB_DOMAINALTERNATE_DOMAINS 的 HTTP 请求。对于其它 Host 一律近回 403 Forbidden 响应。

STREAMING_API_BASE_URL

设置 STREAMING_API_BASE_URL 可将 streaming API 部署于不同域名或不同子域名。这可能有助于提高 streaming API 的性能。

示例值:wss://streaming.example.com

/images/2022/full-explanation-of-mastodon-domain-name-settings/stream_api_api_v1_instance.thumbnail.png

GET /api/v1/instance

/images/2022/full-explanation-of-mastodon-domain-name-settings/stream_api_initial_state.thumbnail.png

initial-state

CDN_HOST

你可以通过设置 CDN_HOST 将静态文件(logos,emojis,CSS,JS 等等等)托管于独立域名,如CDN(内容分发网络,Content Delivery Network),这将降低用户加载时间。

示例值:https://assets.example.com

/images/2022/full-explanation-of-mastodon-domain-name-settings/cdn_host.thumbnail.png

CDN_HOST

S3_ALIAS_HOST

类似于 CDN_HOST,设置 S3_ALIAS_HOST 可以将用户上传内容托管至一独立域名。

示例值: files.example.com

/images/2022/full-explanation-of-mastodon-domain-name-settings/s3_alias_host_search.thumbnail.png

Search

/images/2022/full-explanation-of-mastodon-domain-name-settings/s3_alias_host_xhr.thumbnail.png

XHR

/images/2022/full-explanation-of-mastodon-domain-name-settings/s3_alias_host_ws.thumbnail.png

websocket

域名注意事项

如前所述,

Mastodon域名不但是你的用户访问你服务器的方式,更是你的实例和你的用户在联邦宇宙中的身份标识。是后者而不是前者决定了权威域名无法更改。

如果你使用 masto.host 这类的全托管服务,其可能会为你的实例提供 masto.host 子域。但千万注意,实例的域名一定要自行注册,不要使用全托管服务商为你提供的下属子域。 现在设想这样一个场景,masto.host 宣布下个月要大幅提高托管服务收费,因为太贵了,你不再想使用masto.host托管服务了。

如果你使用的是自己的域名,那么很简单,只需要导出数据库、导出媒体文件、导出应用密钥,然后使用这些东西转移到另一家托管服务商或自己托管服务器,然后把域名指向新托管商或新服务器就OK了。

但如果你使用的是 masto.host 的域名,那么你就面临这样一个窘境,你的实例域名是属于masto.host所有,而不是你自己所有,你现在不使用masto.host的服务了,masto.host自然没有义务为你提供域名。由于Mastodon域名一旦确定便不能被更改,如果masto.host不为你提供域名,那你的实例就只能下线。这时如果你实例已经积累了相当用户,又同时希望能继续运行,那你就只有忍受masto.host提价这一个选择,即使价格再贵。

实际操作篇

在不同域名托管 Mastodon 服务

添加反代域名

大致步骤:

  1. 组网(同一台机器的情况下可省略)

  2. 修改环境变量,将 Puma 监听地址改为 0.0.0.0 (同一台机器的情况下可省略)

  3. 同步 /home/mastodon/live/public (同一台机器的情况下可省略)

  4. 将反代域名添加至 ALTERNATE_DOMAINS

  5. 创建 MITM 代理,修改 websocket 流中的 S3_ALIAS_HOST (可选)

  6. 修改 nginx 配置
    1. 未设置 STREAMING_API_BASE_URLCDN_HOSTS3_ALIAS_HOST
      1. 将反代域名添加至 server_name

      2. 重新生成证书

    2. 需修改 STREAMING_API_BASE_URLCDN_HOSTS3_ALIAS_HOST
      1. 复制创建新一份配置文件

      2. 修改 proxy_cache_path

      3. 添加 proxy_set_header Accept-Encoding identity;

      4. 替换 STREAMING_API_BASE_URL (可选)

      5. 替换 CDN_HOST (可选)

      6. 替换 S3_ALIAS_HOST (可选)

      7. 重新生成证书

  7. 修改 nginx 配置的 /api/v1/streaming 部分(可选)

  8. 重载 nginx 配置

# 同一机器需删去该部分
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

# 同一机器需修改名称
upstream backend {
    server 127.0.0.1:3000 fail_timeout=0;
}

# 同一机器需修改名称,根据需求将 stream API 上游地址修改为 MITM  代理
upstream streaming {
    server 127.0.0.1:4000 fail_timeout=0;
}

# 同一机器需删去该部分
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=CACHE:10m inactive=7d max_size=1g;

server {
    listen 80;
    listen [::]:80;
    # 修改或添加 server_name
    server_name example.com;
    # 根据需求修改 root,同一机器保持原状即可
    root /home/mastodon/live/public;
    location /.well-known/acme-challenge/ { allow all; }
    location / { return 301 https://$host$request_uri; }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    # 修改或添加 server_name
    server_name example.com;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off;

    # Uncomment these lines once you acquire a certificate:
    # ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    # ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    keepalive_timeout    70;
    sendfile             on;
    client_max_body_size 80m;

    # 根据需求修改 root,同一机器保持原状即可
    root /home/mastodon/live/public;

    gzip on;
    gzip_disable "msie6";
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml image/x-icon;

    location / {
        try_files $uri @proxy;
    }

    # If Docker is used for deployment and Rails serves static files,
    # then needed must replace line `try_files $uri =404;` with `try_files $uri @proxy;`.
    location = /sw.js {
        add_header Cache-Control "public, max-age=604800, must-revalidate";
        add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
        try_files $uri =404;
    }

    location ~ ^/assets/ {
        add_header Cache-Control "public, max-age=2419200, must-revalidate";
        add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
        try_files $uri =404;
    }

    location ~ ^/avatars/ {
        add_header Cache-Control "public, max-age=2419200, must-revalidate";
        add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
        try_files $uri =404;
    }

    location ~ ^/emoji/ {
        add_header Cache-Control "public, max-age=2419200, must-revalidate";
        add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
        try_files $uri =404;
    }

    location ~ ^/headers/ {
        add_header Cache-Control "public, max-age=2419200, must-revalidate";
        add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
        try_files $uri =404;
    }

    location ~ ^/packs/ {
        add_header Cache-Control "public, max-age=2419200, must-revalidate";
        add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
        try_files $uri =404;
    }

    location ~ ^/shortcuts/ {
        add_header Cache-Control "public, max-age=2419200, must-revalidate";
        add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
        try_files $uri =404;
    }

    location ~ ^/sounds/ {
        add_header Cache-Control "public, max-age=2419200, must-revalidate";
        add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
        try_files $uri =404;
    }

    location ~ ^/system/ {
        add_header Cache-Control "public, max-age=2419200, immutable";
        add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
        try_files $uri =404;
    }

    location ^~ /api/v1/streaming {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Proxy "";

        proxy_pass http://streaming;
        proxy_buffering off;
        proxy_redirect off;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";

        tcp_nodelay on;
    }

    location @proxy {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Proxy "";
        proxy_pass_header Server;

        proxy_pass http://backend;
        proxy_buffering on;
        proxy_redirect off;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;


        #### 新增部分开始 ###

        # 向上游请求明文,nginx 无法替换压缩内容
        proxy_set_header Accept-Encoding identity;

        # 字符串只进行一次替换,即只替换第一个被匹配的字符串。这里关闭。
        sub_filter_once off;
        #替换的请求类型,增加 application/json 。
        sub_filter_types application/json;

        # 替换 STREAMING_API_BASE_URL (按需)
        sub_filter wss://example.com wss://example.org;
        # 替换 CDN_HOST (按需)
        sub_filter https://cdn.example.com https://cdn.example.org;
        # 替换 S3_ALIAS_HOST (按需)
        sub_filter https://img.example.com https://img.example.org;
        # 替捣 missing.png (按需)
        sub_filter https://example.com/avatars/original/missing.png https://example.org/avatars/original/missing.png;
        sub_filter https://example.com/headers/original/missing.png https://example.org/headers/original/missing.png;

        ### 新增部分结束 ###

        proxy_cache CACHE;
        proxy_cache_valid 200 7d;
        proxy_cache_valid 410 24h;
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        add_header X-Cached $upstream_cache_status;

        tcp_nodelay on;
    }

    error_page 404 500 501 502 503 504 /500.html;
}

websocket MITM 代理

安装 mitmproxy

import re
from mitmproxy import ctx


def websocket_message(flow):
    # get the latest message
    message = flow.messages[-1]

    if message.from_client:
        ctx.log.info("Client sent a message: {}".format(message.content))
    else:
        ctx.log.info("Server sent a message: {}".format(message.content))

    # manipulate the message content
    message.content = re.sub("https://img\.example\.com", "https://img.example.org", message.content)

    if 'FOOBAR' in message.content:
        # kill the message and not send it to the other endpoint
        message.kill()
# /etc/systemd/system/mitm-mastodon-websocket.service
[Unit]
Description=Mastodon Mitm push websocket
After=network.target
Wants=network.target

[Service]
Type=simple
User=www-data
Slice=system-mitm.slice
ExecStart=/usr/bin/mitmdump --listen-host 127.0.0.1 -p 4444 -s /opt/mastodon_websocket_messages.py --mode reverse:http://127.0.0.1:4000 --set keep_host_header --quiet
Restart=on-failure

[Install]
WantedBy=multi-user.target

参考资料