{
  "type": "aside",
  "draft": false,
  "title": "GoToSocial split-domain redirects for a static website on AWS CloudFront",
  "slug": "gotosocial-cloudfront-redirect",
  "tags": [
    "gotosocial",
    "social",
    "aws",
    "webdev"
  ],
  "time": "13:19:27-07:00",
  "attachments": [],
  "year": "2025",
  "month": "10",
  "day": "25",
  "isDir": false,
  "date": "2025-10-25T20:19:27.000Z",
  "postName": "2025-10-25",
  "html": "<p>For awhile now, I've wanted to set up an installation of <a href=\"https://gotosocial.org/\">GoToSocial</a> for my <a href=\"https://lmorchard.com\">lmorchard.com</a> domain and run my own tiny fediverse outpost. And what I <em>really</em> wanted to do was to be able to host handles like <a href=\"https://gts.lmorchard.com/@links\">@links@lmorchard.com</a>, <a href=\"https://gts.lmorchard.com/@blog\">@blog@lmorchard.com</a>, and <a href=\"https://gts.lmorchard.com/@lmorchard\">@lmorchard@lmorchard.com</a>. (<a href=\"https://masto.hackers.town/@lmorchard/115413464705080292\">I was thinking of doing @me@lmorchard.com</a>, just like my email address. But, that could be confusing, because I might look like my name is \"me\" everywhere.)</p>\n<p>Per the GoToSocial documentation, <a href=\"https://docs.gotosocial.org/en/latest/advanced/host-account-domain/\">Split-domain deployments</a> are supported by way of a few server-side redirects on the vanity account domain:</p>\n<blockquote>\n<p>The way ActivityPub implementations discover how to map your account domain to your host domain is through a protocol called <a href=\"https://www.rfc-editor.org/rfc/rfc7033\">webfinger</a>. This mapping is typically cached by servers and hence why you can't change it after the fact.</p>\n<p>It works by doing a request to <code>https://&lt;account domain&gt;/.well-known/webfinger?resource=acct:@me@example.org</code>. At this point, a server can return a redirect to where the actual webfinger endpoint is, <code>https://&lt;host domain&gt;/.well-known/webfinger?resource=acct:@me@example.org</code> or may respond directly. The JSON document that is returned informs you what the endpoint to query is for the user</p>\n</blockquote>\n<p>So, I need <code>lmorchard.com/.well-known/webfinger</code> to redirect to <code>gts.lmorchard.com/.well-known/webfinger</code> with query parameters intact to make the magic happen.</p>\n<p>There's a wrinkle, though: lmorchard.com points at a statically-generated site, uploaded to Amazon S3, hosted behind a CloudFront CDN. That's been low-hassle to keep running for years now, as opposed to say a full-featured nginx server. The trade-off has been that this hosting arrangement didn't support any smarts on the server side. So, I thought the redirects would be infeasible.</p>\n<p>However, I'd missed that <a href=\"https://aws.amazon.com/blogs/aws/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/\">CloudFront added support for edge functions</a> a few years ago. That means redirects are <em>entirely</em> feasible these days!</p>\n<p>Long story short, here's the edge function I came up with to do the needful for GoToSocial. Nothing super-special, just that a) it works and b) it took me a few rounds of mistakes before I got it working. So, this might be handy for someone else trying to do something similar! (Or me, if I ever lose it and need to set this up again.)</p>\n<pre><code class=\"language-javascript\">function handler(event) {\n    var request = event.request;\n    var uri = request.uri;\n    \n    // Check if the request is for one of the well-known endpoints\n    if (uri === '/.well-known/webfinger' || \n        uri === '/.well-known/host-meta' || \n        uri === '/.well-known/nodeinfo') {\n        \n        // Build redirect URL\n        var redirectUrl = 'https://gts.lmorchard.com' + uri;\n        \n        // Manually build query string from querystring object\n        var queryString = request.querystring;\n        if (queryString &amp;&amp; Object.keys(queryString).length &gt; 0) {\n            var params = [];\n            for (var key in queryString) {\n                if (queryString.hasOwnProperty(key)) {\n                    var value = queryString[key].value;\n                    // Don't re-encode - values are already URL-encoded\n                    params.push(key + '=' + value);\n                }\n            }\n            if (params.length &gt; 0) {\n                redirectUrl += '?' + params.join('&amp;');\n            }\n        }\n        \n        return {\n            statusCode: 301,\n            statusDescription: 'Moved Permanently',\n            headers: {\n                'location': { value: redirectUrl },\n                'access-control-allow-origin': { value: '*' },\n                'access-control-allow-methods': { value: 'GET, HEAD, OPTIONS' },\n                'access-control-allow-headers': { value: 'Content-Type' }\n            }\n        };\n    }\n    \n    // Return the request unchanged for all other paths\n    return request;\n}\n</code></pre>\n",
  "body": "\nFor awhile now, I've wanted to set up an installation of [GoToSocial](https://gotosocial.org/) for my [lmorchard.com](https://lmorchard.com) domain and run my own tiny fediverse outpost. And what I *really* wanted to do was to be able to host handles like [@links@lmorchard.com](https://gts.lmorchard.com/@links), [@blog@lmorchard.com](https://gts.lmorchard.com/@blog), and [@lmorchard@lmorchard.com](https://gts.lmorchard.com/@lmorchard). ([I was thinking of doing @me@lmorchard.com](https://masto.hackers.town/@lmorchard/115413464705080292), just like my email address. But, that could be confusing, because I might look like my name is \"me\" everywhere.)\n\nPer the GoToSocial documentation, [Split-domain deployments](https://docs.gotosocial.org/en/latest/advanced/host-account-domain/) are supported by way of a few server-side redirects on the vanity account domain:\n\n> The way ActivityPub implementations discover how to map your account domain to your host domain is through a protocol called [webfinger](https://www.rfc-editor.org/rfc/rfc7033). This mapping is typically cached by servers and hence why you can't change it after the fact.\n>\n> It works by doing a request to `https://<account domain>/.well-known/webfinger?resource=acct:@me@example.org`. At this point, a server can return a redirect to where the actual webfinger endpoint is, `https://<host domain>/.well-known/webfinger?resource=acct:@me@example.org` or may respond directly. The JSON document that is returned informs you what the endpoint to query is for the user\n\nSo, I need `lmorchard.com/.well-known/webfinger` to redirect to `gts.lmorchard.com/.well-known/webfinger` with query parameters intact to make the magic happen.\n\nThere's a wrinkle, though: lmorchard.com points at a statically-generated site, uploaded to Amazon S3, hosted behind a CloudFront CDN. That's been low-hassle to keep running for years now, as opposed to say a full-featured nginx server. The trade-off has been that this hosting arrangement didn't support any smarts on the server side. So, I thought the redirects would be infeasible.\n\nHowever, I'd missed that [CloudFront added support for edge functions](https://aws.amazon.com/blogs/aws/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/) a few years ago. That means redirects are *entirely* feasible these days!\n\nLong story short, here's the edge function I came up with to do the needful for GoToSocial. Nothing super-special, just that a) it works and b) it took me a few rounds of mistakes before I got it working. So, this might be handy for someone else trying to do something similar! (Or me, if I ever lose it and need to set this up again.)\n\n```javascript\nfunction handler(event) {\n    var request = event.request;\n    var uri = request.uri;\n    \n    // Check if the request is for one of the well-known endpoints\n    if (uri === '/.well-known/webfinger' || \n        uri === '/.well-known/host-meta' || \n        uri === '/.well-known/nodeinfo') {\n        \n        // Build redirect URL\n        var redirectUrl = 'https://gts.lmorchard.com' + uri;\n        \n        // Manually build query string from querystring object\n        var queryString = request.querystring;\n        if (queryString && Object.keys(queryString).length > 0) {\n            var params = [];\n            for (var key in queryString) {\n                if (queryString.hasOwnProperty(key)) {\n                    var value = queryString[key].value;\n                    // Don't re-encode - values are already URL-encoded\n                    params.push(key + '=' + value);\n                }\n            }\n            if (params.length > 0) {\n                redirectUrl += '?' + params.join('&');\n            }\n        }\n        \n        return {\n            statusCode: 301,\n            statusDescription: 'Moved Permanently',\n            headers: {\n                'location': { value: redirectUrl },\n                'access-control-allow-origin': { value: '*' },\n                'access-control-allow-methods': { value: 'GET, HEAD, OPTIONS' },\n                'access-control-allow-headers': { value: 'Content-Type' }\n            }\n        };\n    }\n    \n    // Return the request unchanged for all other paths\n    return request;\n}\n```",
  "parentPath": "./content/posts/2025",
  "path": "2025/10/25/gotosocial-cloudfront-redirect",
  "needsBuild": true,
  "prevPostPath": "2025/10/23/w43",
  "prevPostTitle": "2025 Week 43",
  "nextPostPath": "2025/10/25/miscellanea",
  "nextPostTitle": "Miscellanea for 2025-10-25"
}