GoToSocial split-domain redirects for a static website on AWS CloudFront

For awhile now, I've wanted to set up an installation of GoToSocial for my 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, @blog@lmorchard.com, and @lmorchard@lmorchard.com. (I was thinking of doing @me@lmorchard.com, just like my email address. But, that could be confusing, because I might look like my name is "me" everywhere.)

Per the GoToSocial documentation, Split-domain deployments are supported by way of a few server-side redirects on the vanity account domain:

The way ActivityPub implementations discover how to map your account domain to your host domain is through a protocol called webfinger. This mapping is typically cached by servers and hence why you can't change it after the fact.

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

So, 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.

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.

However, I'd missed that CloudFront added support for edge functions a few years ago. That means redirects are entirely feasible these days!

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.)

function handler(event) {
    var request = event.request;
    var uri = request.uri;
    
    // Check if the request is for one of the well-known endpoints
    if (uri === '/.well-known/webfinger' || 
        uri === '/.well-known/host-meta' || 
        uri === '/.well-known/nodeinfo') {
        
        // Build redirect URL
        var redirectUrl = 'https://gts.lmorchard.com' + uri;
        
        // Manually build query string from querystring object
        var queryString = request.querystring;
        if (queryString && Object.keys(queryString).length > 0) {
            var params = [];
            for (var key in queryString) {
                if (queryString.hasOwnProperty(key)) {
                    var value = queryString[key].value;
                    // Don't re-encode - values are already URL-encoded
                    params.push(key + '=' + value);
                }
            }
            if (params.length > 0) {
                redirectUrl += '?' + params.join('&');
            }
        }
        
        return {
            statusCode: 301,
            statusDescription: 'Moved Permanently',
            headers: {
                'location': { value: redirectUrl },
                'access-control-allow-origin': { value: '*' },
                'access-control-allow-methods': { value: 'GET, HEAD, OPTIONS' },
                'access-control-allow-headers': { value: 'Content-Type' }
            }
        };
    }
    
    // Return the request unchanged for all other paths
    return request;
}
blog comments powered by Disqus
Miscellanea for 2025-10-25  Previous Developers shouldn't have to be data landlords Next