Blake Smith

create. code. learn.

»

Apex Domains for ActivityPub and Mastodon / GotoSocial with S3 and Cloudfront

If you are setting up an ActivityPub (Mastodon or GotoSocial) server, and want your domain to be at the “Apex” of your domain, and your domain is already hosting an existing website, you’ll need to do some additional setup. GotoSocial calls this a “Split Domain deployment”.

In my case, I wanted my ActivityPub user to be @blake@blakesmith.me, but my website is already hosted at blakesmith.me, meaning my ActivityPub server (GotoSocial in this case) needs a little extra configuration.

The setup is:

  • Host domain: social.blakesmith.me
  • Account domain: blakesmith.me

Here’s my NixOS module that configures GotoSocial (a minimal ActivityPub server, great for small and single user instances) with the split domain on my server. Notice the host and account-domain configuration:

{  pkgs, ... }:

{
  services.gotosocial = {
    enable = true;
    settings = {
      host = "social.blakesmith.me";
      account-domain = "blakesmith.me";
    };
  };

  services.nginx = {
    virtualHosts."social.blakesmith.me" = {
      forceSSL = true;
      enableACME = true;
      locations."/" = {
        proxyPass = "http://127.0.0.1:8080";
        extraConfig =
          "proxy_ssl_server_name on;" +
          "proxy_pass_header Authorization;" +
          "client_max_body_size 0;" +
          "proxy_http_version 1.1;" +
          "proxy_request_buffering off;" +
          "proxy_set_header Host $host;" +
          "proxy_set_header Upgrade $http_upgrade;" +
          "proxy_set_header Connection \"upgrade\";" +
          "proxy_set_header X-Forwarded-For $remote_addr;" +
          "proxy_set_header X-Forwarded-Proto $scheme;"
        ;
      };
    };
  };

  security.acme = {
    certs."social.blakesmith.me".email = "blakesmith0@gmail.com";
  };
}

We put nginx in front of GotoSocial, and setup a LetsEncrypt TLS cert as well (required for ActivityPub). This should all work great once DNS for social.blakesmith.me is pointed at the NixOS server. The GotoSocial daemon is reachable at social.blakesmith.me, but is configured to have usernames with blakesmith.me.

My website, blakesmith.me is hosted via an S3 bucket, with Cloudfront in front of it. We need to configure the S3 bucket for blakesmith.me to redirect all requests to the /.well-known route prefix to social.blakesmith.me. Here’s the terraform necessary:

resource "aws_s3_bucket" "origin_blakesmith_me" {
  bucket = "origin.blakesmith.me"
  # ...SNIP...
  website {
    index_document = "index.html"
    routing_rules = <<EOF
[{
    "Condition": {
        "KeyPrefixEquals": ".well-known"
    },
    "Redirect": {
        "HostName": "social.blakesmith.me",
        "HttpRedirectCode": "301",
        "Protocol": "https"
    }
}]
EOF
  }
}

The routing_rules is the most important part: Any route to the bucket that is prefixed with .well-known will be redirected to social.blakesmith.me. ActivityPub / Mastodon uses the WebFinger protocol as a way to resolve profile and server information for a given Mastodon handle.

Let’s test it:

$ curl -v "https://blakesmith.me/.well-known/webfinger?resource=acct:blake@blakesmith.me"
> GET /.well-known/webfinger?resource=acct:blake@blakesmith.me HTTP/2
> Host: blakesmith.me
> User-Agent: curl/8.4.0
> Accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
< HTTP/2 301
< content-length: 0
< location: https://social.blakesmith.me/.well-known/webfinger?resource=acct:blake@blakesmith.me
< date: Sat, 17 Feb 2024 18:02:11 GMT
< server: AmazonS3
< x-cache: Hit from cloudfront
< via: 1.1 4076c139caa3b19374b9d2d1784ca5e0.cloudfront.net (CloudFront)
< x-amz-cf-pop: ORD56-P4
< x-amz-cf-id: XN5GXTM2fhACc4ZWmRbs2PMKmxEMeI-3yj0GZJ_orlLAp655feXVZQ==
< age: 3
<
* Connection #0 to host blakesmith.me left intact

It’s working if you get a valid redirect to your host domain server! If we follow redirects, we’ll hit the ActivityPub server hosted at social.blakesmith.me:

$ curl -L "https://blakesmith.me/.well-known/webfinger?resource=acct:blake@blakesmith.me" | jq .
{
  "subject": "acct:blake@blakesmith.me",
  "aliases": [
    "https://social.blakesmith.me/users/blake",
    "https://social.blakesmith.me/@blake"
  ],
  "links": [
    {
      "rel": "http://webfinger.net/rel/profile-page",
      "type": "text/html",
      "href": "https://social.blakesmith.me/@blake"
    },
    {
      "rel": "self",
      "type": "application/activity+json",
      "href": "https://social.blakesmith.me/users/blake"
    }
  ]
}

If all is well, other users should be able to follow you using the account domain as your username (In my case: @blake@blakesmith.me).

One pitfall that tripped me up for awhile: if you have Cloudfront in front of your S3 bucket, like I do, you have to configure your Cloudfront origin using a custom origin and the S3 website endpoint, NOT the bucket endpoint. Otherwise, you’ll get “The specified key does not exist” errors when trying to access the webfinger resource at your account domain.

The relevant terraform setup for the Cloudfront distribution looks like this (notice how the bucket origin is configured with a custom_origin_config):

resource "aws_cloudfront_distribution" "blakesmith_distribution" {
  origin {
    domain_name = aws_s3_bucket.origin_blakesmith_me.website_endpoint
    origin_id = "blakesmith_origin"
    custom_origin_config {
      http_port = 80
      https_port = 443
      origin_protocol_policy = "http-only"
      origin_ssl_protocols = [ "TLSv1.1", "TLSv1.2", "SSLv3" ]
    }
  }

  enabled = true
  is_ipv6_enabled = true
  default_root_object = "index.html"
  aliases = ["blakesmith.me", "www.blakesmith.me"]
  price_class = "PriceClass_200"
  retain_on_delete = true
  default_cache_behavior {
    allowed_methods = [ "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT" ]
    cached_methods = [ "GET", "HEAD" ]
    target_origin_id = "blakesmith_origin"
    forwarded_values {
      query_string = true
      cookies {
        forward = "none"
      }
    }
    viewer_protocol_policy = "redirect-to-https"
    min_ttl = 0
    default_ttl = 3600
    max_ttl = 86400
  }
  viewer_certificate {
    acm_certificate_arn = aws_acm_certificate_validation.blakesmith_me.certificate_arn
    ssl_support_method = "sni-only"
  }
  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }
}

Feel free to follow me at @blake@blakesmith.me on ActivityPub!


»

Learning Homebrew Game Boy Game Development in Assembly

A few weeks ago, I shipped a mostly complete Breakout / Arkanoid clone called “Brickbasher”, written for the original Game Boy. You can download the ROM here.


The limitations of designing for small and constrained hardware proved to be really fun. You can’t yak-shave too much when you only have 4 bit colors, and 4 basic audio oscillators to work with! I played so many old-school Game Boy games growing up, so it’s been a rewarding nostalgia trip to dive in head first and do what my younger self could only dream of.

At a high level, the tools and building blocks for the project are:

  1. Game Boy flavored 6502 assembly for all the code, assembled with RGBDS.
  2. PNG tilemaps for game assets, layed out in Aseprite, and converted into ROM data with my own custom tool called gbtile.
  3. Music tracked with hUGETracker and played in game with hUGEDriver.
  4. Testing / emulation with the BGB Game Boy emulator (Ironically, ran via Wine, since I’m a Linux user, and BGB seems to have the best debugging tools I could find)

The game runs on any standard Game Boy emulator, and should theoretically run on real hardware too.

Tools and Hardware Ramp-Up

In today’s modern homebrew world, believe it or not, you can get your hands on a C compiler that targets the Game Boy. C might be considered “low level” these days, but it’s luxurious compared to raw 6502 asm! I wanted to learn by diving down “one more layer of abstraction”, so I chucked the C compiler aside and went straight for emacs and an assembler.

After tool setup, the next big step was getting familiar with the Game Boy hardware and how it works. There are great manuals floating around that go through the important details of the Game Boy hardware. About a year before Brickbasher, I built a very rough webasm Game Boy emulator in Rust, which was another great way to get familiar with the inner workings of the system.

Tutorials

I used this gbdev.io tutorial as a starting point for mapping out the rudimentary game structure. The tutorial walks you through a lot of the basics of Game Boy 6502 assembly, and basic concepts of the Game Boy hardware. You draw tilemaps, take some basic user input, and move the paddle around back and forth. By the end of the tutorial, you wind up with something that looks like this:


You get basic tile drawing, and a small paddle that you can move around, but that’s it. Unfortunately, the rest of the tutorial beyond these basics is incomplete. So now I needed to go off on my own and figure out the rest.

Level Data

The original tutorial showed how to draw the background tiles with the bricks, but it was all hardcoded into the ROM tilemap. I wanted to implement a “level draw” routine that could take input level data, and draw the bricks in the correct locations. Here’s what the level data looks like in assembly ROM:

Level0:
        db 0,0,0,0,0,0
        db 0,0,0,0,0,0
        db 0,0,0,0,0,0
        db 2,2,2,2,2,2
        db 2,2,2,2,2,2

The level data format I came up with is a bit wasteful on space, but gets the job done. Having bricks all be “byte aligned” avoids doing a bunch of bit-level manipulation later on in tight loops (trading data for CPU time). I use a single byte to represent each brick, with a maximum of 5 rows of bricks per level. Each byte counts how many “hits” an individual brick can take, with a ‘0’ representing “no brick”.

From this level data, I wrote a routine that reads through the level data and writes out the brick tiles in the correct locations.

Sprites and Tiles

I wanted to make the original paddle sprite larger, and add a basic bouncing ball. The original tutorial did not include a sprite or tile map, other than the raw converted assembly data for the game. So I recreated the sprites in a PNG based tilemap so I could change things around to my liking:

Background Tilemap

Background Tilesheet. Individual tiles in Game Boy hardware are each 8x8 pixels wide, which is why Game Boy games usually follow similar “block size” layouts.

Ball sprite

Ball sprite that bounces around the level.

Paddle sprite

The Paddle Sprite. In the game, this actually uses a total of three 8x8 pixel Game Boy sprites.

I run these PNG images through a small utility I wrote that converts the PNG images into Game Boy sprite and tile data that can be loaded directly into the assembler. The RGBDS toolkit comes with a more feature rich utility to accomplish a similar task, but writing my own simplified tool was a really great way to learn about how the Game Boy represents pixel data down to the bit level.

Collision Detection

By far the hardest part of the entire project was doing collision detection on the limited Game Boy hardware. The Game Boy has no built-in multiplication instructions (read: multiplication in a tight loop is going to be expensive).

I bashed my head against the wall a lot, and settled on precomputing collision lookup tables that store the upper X and Y coordinates of each brick. In retrospect, because there are only a maximum of 30 bricks on the screen, I probably could have just computed the tables “offline” with a calculator, and hardcoded them into the game, but I wanted to figure out how to compute the tables with the Game Boy hardware itself.

I ended up needing to borrow and modify the multiplication routines from the Game Boy C compiler, in order to precompute these tables when the level loads.

Like I said, next time I’m just going to break out the calculator, or a Python script and slap the output directly into ROM data!

Music

I stumbled upon hUGETracker, an old-school style music tracker that can export to a format that’s readable on Game Boy hardware.

hUGETracker Logo

My game only has a very basic 4 bar repeating track, but it was enough to learn how to go through the process of getting music into a game via the hUGEDriver asm routines.

After compiling hUGEDriver.asm along with the game code, the routine to initialize the audio track looks like:

InitAudio:
        ;; Turn audio on
        ld a, $80
        ld [rAUDENA], a
        ld a, $FF
        ld [rAUDTERM], a
        ld a, $77
        ld [rAUDVOL], a

        ;; Start playing audio track
        ld hl, first_track
        call hUGE_init
        ei
        ret

And then we just need to tick the audio on a fixed interrupt cycle:

SECTION "Timer overflow interrupt", ROM0[$0050]
    nop
    jp isr_wrapper

isr_wrapper:
    push af
    push hl
    push bc
    push de
    ;; Call into hUGE_dosound to 'tick' the audio
    call hUGE_dosound
    pop de
    pop bc
    pop hl
    pop af
    reti

I didn’t implement any sound effects, but I could reserve one oscillator for effects if I ever get there.

Polish and Finish

Beyond that, I added a lot of the bare bones features necessary to make the game actually playable:

  • A life count, and “Game Over” state.
  • Multiple levels, and level advancement.
  • A “win” state when you get through all the levels.
  • Ready screens with delays to give you time to orient before the ball starts bouncing around the screen.

Brickbasher Ready Screen

Final Thoughts

There’s a lot more polish that’d be fun to add at some point. Logo splash screens, final credits, game sound effects, more music tracks, multiple balls, all would make great additions.

This was a “Big Magic” project: I did it just for fun, to learn, and because creating new things is its own reward. I enjoyed building the game up from raw assembly, and making other tools along the way. The constraints of limited hardware made for new problems that are solvable with trivial one-liners in a modern programming language on modern hardware, but stretched me creatively in unique new ways in 8-bit land.

Feel free to check out the source code on GitHub, and give the ROM a spin in a Game Boy emulator!


»

Micropad: Final Prototype Update

I’ve recently started building the final prototype revision of micropad. Since the last post, I’ve been able to:

  • Finish the transition to a USB-C connector
  • Get the case in good alignment, including other minor mechanical changes
  • Fix top-plate buckling

Micropad Final Prototype 1.

Micropad Final Prototype 2.

Micropad Final Prototype 3.

Mechanical Fixes

I fixed the top-plate buckling issue, by including 2 different standoff sizes that the PCB is mounted to. This increased the overall macropad height, but gives the board enough clearance for a completely level top-plate mount.

I also changed the revision 1 screw fasteners from M2 to M3. M3 fasteners are much easier to find and purchase, and I wanted these to be easily repairable if fasteners are ever lost.

Part of what delayed this project’s finalization was waiting for my Prusa Mini+ 3D printer to arrive:

Prusa Mini+ 3D Printer.

Having a 3D printer at home allowed me to iterate on case tweaks really fast, and get all the case tolerances worked out. It was also a fun multi-week assembly that my oldest son helped with, which made for great fun!

Firmware

On the firmware side: There’s still a few blocks of unsafe Rust in the firmware. Most of this is caused by needing static mutable references to peripherals for interrupt handling, but I was able to slim down the unsafe blocks by wrapping more global peripherals in Mutex<RefCell<T>> types than before. I ended up not using RTIC, since my firmware size was already at the upper limit of the onboard 32K flash memory size, and adding another dependency sent it over the edge. A few unsafe blocks in a small firmware like this seems worth it to not have to buy a chip with more onboard memory, or reduce other user-visible functionality.

I’ve made 4 of these micropads in my (really messy) workshop, to give to friends, and am excited with how they’ve turned out!

PCB Assembly.

Workshop.

Micropad Final Prototype 4.

As always, the full source for micropad is available from GitHub.


» all posts