Hacking iframe badges into auto-updating Github profile images with AWS Lambda and Golang

Zack Proser
Level Up Coding
Published in
14 min readFeb 22, 2021

--

I finally got an internet badge I was proud of, but I couldn’t embed it in my Github profile!

Wren.co is a startup that empowers you, your friends and families to offset your carbon footprint by funding a climate fund portfolio of restorative projects. The team at Wren.co are doing profoundly critical work and you should support them, whether it be by offsetting your own footprint, if you can afford to, or by introducing Wren to your friends and co-workers.

They are not just tackling climate related damage, but also the interlinked problem of building social consensus and cooperation around fighting climate change by allowing you to gift Wren, offset someone else’s footprint, create and join teams, and compete in friendly leaderboards.

SVG Badges are easy to include anywhere — except Github

Recently, Wren released a feature that allows you to display a slick badge that shows your overall climate impact, based on your contributions.

It looks like this, though you can choose from a couple styles. Note that the number of tons of carbon offset is updated in real-time:

My immediate thought was that I wanted to place this badge on my Github profile, via the recently introduced Github personal Readme feature. This feature allows you to edit a README.md file in a special repository that will be in-lined into your Github profile, so that you can personalize it a bit:

This is Github giving its users more control over styling their own profile, as long as you render things in Markdown. In my case, the block to the right of my image that contains my badge, a short blurb about my interests and a table with links to my other content is all rendered in Markdown that lives in this special repo.

Markdown is fast and easy, but restrictive

There are good security reasons why Github’s markdown parser for the profile READMEs only allows certain HTML elements to be rendered (a chief concern is that someone could inject malicious scripts into the HTML, so that anyone who visits the profile is susceptible to scripting attacks).

However, this also necessarily introduces limitations to what you can render and display in your profile README. Wren.co released their initial iteration of badges implemented as iframes that you can quickly embed in any site you control to render your badge, and you can even modify the styling to some degree via a couple parameters:

<iframe src="http://wren.co/badge/simple/mims" style="width: 300px; height: 128px; border: 0" />

Unfortunately, iframes are one of the no-no elements for Github’s markdown parser, but I still wanted to display my badge and have it updated regularly, without having to do it myself manually. So, I wrote a quick app to do it for me.

Isn’t this excessive and unnecessary?

Won’t Wren.co likely update their badges to support returning images later? Definitely, yes and maybe. But it’s still good practice for working with AWS serverless tech.

What we’re building

Although I decided to use this approach for my Wren.co badge, you could use the same technique and code outlined here to do the same for any other badge that is only available as an iframe. The code for this solution is also available at https://github.com/zackproser/wren-badge-rotator.

In this walk-through I’ll share my problem-solving approach and all the code for setting up a system that will:

  1. Automatically fetch my latest iframe badge every month
  2. Convert it to a static image
  3. Publish it on my Github profile via a Pull request

If you want to skip ahead to the code for this app, you can find it at https://github.com/zackproser/wren-badge-rotator.

Problem statement

I knew I wanted my badge to be displayed on my Github profile README, and I couldn’t use the iframe directly in my markdown, because Github would sanitize it out prior to rendering my profile.

I could take a screenshot of my own badge and then add it as a local image in my special profile README repo, and refer to it via a local image href like so:

![Climate Neutral Human](img/wren-carbon.png)

Then I could wrap the image in a link, either my Wren referral link or to to my Wren profile. That works initially, but because I have a monthly offset subscription to Wren, and because I occasionally make one-off offsets or buy them for others, I know that my stats are going to be changing regularly, that is, assuming the Wren badges support real-time updates?

Reconnaissance

I read through the Wren badge announcement and examined the URL the suggested iframes were pointing to. I found an HTML page that looked like this:

So — the underlying HTML page itself renders a badge that fills the width of the viewport. It’s worth noting that I tried appending “.png” to the end of the URL to see if the server would return an image I could directly link to, but that functionality was not currently supported. Instead, it returned an application error that tells me Wren is using Heroku :)

Figuring out how the badges work

I was also able to confirm that badge stats update in real-time. I started by looking at my current badge, then going into Wren and doing a one-time offset of 1 ton of carbon, and then refreshing my badge, which immediately reflected this by incrementing my count of total tons offset by 1.

There are a few URL parameters you can pass to control the rendering of the badge, such as the `USERNAME` of the Wren user and the `STYLE` “{simple,logo}” of the badge itself. See the badge feature announcement for more info.

It’s also worth noting that the CSS of the page is written in-line with the page itself and not sourced from a separate script.

Modifying the full-width badge in dev tools

Still in Firefox dev tools, I tinker around with the HTML and CSS until I get something that starts to look halfway decent as a medium-sized badge.

I found the badge itself was an <a> tag wrapping an SVG (the Wren bird logo) and some other elements. There’s a container element — and by adding a width: 25% rule to it I was able to get the badge sized generally the way I wanted. More experimentation showed I should also constrain the HTML element’s width to 300px and its height to 117px.

At this point, I’m thinking of scraping this HTML page for my badge, but there’s an obstacle in that the badge will naturally fill the width of the viewport, so I can’t just grab the page wholesale and use it as is. There’s going to need to be some modification and translation performed after fetching it.

Modifying the HTML page, and handling remaining CSS issues

Modifying the CSS directly on the wren badge page via Dev tools is great for figuring out what changes to be made, but how can I then apply my CSS changes in a permanent way to style my badge?

I need to own the HTML for my own badge if I want to make my CSS modifications permanent. In other words, I’ll need to scrape my badge, re-write the CSS rules on the fly, host it somewhere at a URL that I control, and then snapshot that badge in order to get the final output I’m looking for.

Finding an HTML page snappshotter

I start thinking through various snapshotting solutions. Puppeteer comes to mind, but ultimately I just want an API, so I settle on https://htmlcsstoimage.com/ after experimenting with their free demo convinces me their API works well.

There’s another wrinkle I notice now as well, in that even once I modify the CSS on my version of my badge’s HTML page, anything that looks up that page is going to see a small green badge swimming in an ocean of a white page.

The HCTI API allows you to specify the viewport they use when generating snapshots. That can solve my cropping issue, but then there’s still a remaining CSS issue: the Wren badges have a mild “border-radius” rule applied to them, making their corners rounded, so if you take a perfectly square screenshot of them, they’ll show a few pixels of white background around their rounded corners.

This might not seem like a big deal, but I and many other people run Github in dark mode, so on a black background this would look gross (as a quick test on my Github profile README confirms).

I decide my modified badge page will also drop the border-radius so that I can display my perfectly squarely screenshotted badge on either a white or black background without any of the extraneous pixels around the corners glaring back.

Finally, HCTI lets me also pass along a CSS selector that they’ll use when screenshotting the page to only retrieve that element. I want a perfectly cropped badge, so I can provide the “.container” selector via their API, and as testing confirms, it works perfectly — only my updated, squared badge itself is extracted as an image.

Thinking through the requirements for automating this end to end

At this point it’s clear I’ll need to take a couple of steps to make this work the way I want. We can now review the same system flowchart with a little more context:

  1. I need something to kick this process off once per month, because the stats represented by my badge are updated at least that often.
  2. I’ll need to directly capture the original HTML of the badge page as it is, for my username.
  3. Next, I’ll need to make some modifications to the CSS itself, but I otherwise want the same overall HTML page structure to remain the same.
  4. I’ll need to do this HTML and CSS rewriting at runtime.
  5. I’ll then need to write my modified version of my badge’s HTML page, containing my tweaked CSS rules, somewhere locally.
  6. Next, I’ll need to immediately publish this modified HTML page at a public URL so that I can feed it to the HCTI API for scraping.
  7. I’ll then call the HCTI API and give them the URL to my modified page, along with the aforementioned “viewport_width”, “viewport_height” and “css_selector” params.
  8. This API call will result in a response from HCTI that contains a URL to their perfectly snapshotted version of my badge image, with my latest carbon offset stats.
  9. I’ll then need to read the image from this URL and save it somewhere locally and or remotely for later use.
  10. I’ll then need to clone my special Github profile repository at runtime.
  11. I’ll then need to overwrite the existing “img/wren-badge.png” that my README.md file is currently linking to, with this freshly updated badge image.
  12. I’ll then need to programmatically commit and push these changes to my remote repo, leveraging HTTP basic auth using my username and Github personal access token .
  13. Finally I can call the Github API with my token to open a Pull Request so that I can review that everything looks good prior to merging my updated badge.

The Golang lambda function I ended up writing is able to accomplish all of this in around 6 seconds.

The solution takes shape

I landed on AWS lambda with a Golang runtime, since I can set up a CloudWatch alert to trigger my lambda once every month.

Rather than start from scratch, I leveraged AWS Serverless Application Model (SAM) to quickly scaffold a lambda function that connects up to an API Gateway instance, Xray for tracing and CloudWatch logs.

Leveraging Cloudformation for fast and easy deployments

Once I had the default serverless API template and my Lambda working as I wanted, I modified the Cloudformation template to drop the unnecessary API Gateway and its endpoints and replace it with a CloudWatch events trigger configured with a cron schedule to run on the second day of every month.

I did this because there’s no external API calls required to this system, it instead just needs to run itself once a month.

Cloudformation makes it easy to tear down and bring back up the entire stack quickly, and to enable others who may want to deploy the app themselves to quickly get up and running with it in their own account. Here’s what my final Cloudformation template looks like.

Note that this template describes every AWS resource required to run this app, and their relationships:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
wren-badge-rotator
Resources:
WrenBadgeImageResizeBucket:
Type: AWS::S3::Bucket
# Attach a bucket policy that allows all objects uploaded to it to be read by anonymous principals (such as the HCTI API's screenshotting / scraping bots)
WrenBadgeImageResizeBucketAllowPublicReadPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref WrenBadgeImageResizeBucket
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "s3:GetObject"
Resource:
- !Join
- ''
- - 'arn:aws:s3:::'
- !Ref WrenBadgeImageResizeBucket
- /*
Principal: "*"
WrenBadgeRotatorFunction:
Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
Properties:
CodeUri: wren-badge-rotator/
Handler: wren-badge-rotator
Runtime: go1.x
Description: A lambda function that scrapes, processes, and updates my Wren.co badge on my Github profile
# Allow a timeout of 15 minutes, the maximum supported by lambda, even though we won't need that much time
Timeout: 900
Events:
SimpleCWEEvent:
Type: Schedule
Properties:
# Run the lambda function once, on the second day of every month, at 8am
Schedule: cron(0 8 2 * ? *)
Environment:
Variables:
S3_BUCKET: !Ref WrenBadgeImageResizeBucket
WREN_USERNAME: zackproser
REPO_OWNER: zackproser
WrenBadgeRotatorFunctionS3BucketPolicy:
Type: AWS::IAM::Policy
Properties:
PolicyName: ManageImageResizeS3Bucket
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- 's3:*'
# Grant the Lambda function access to every file in the S3 bucket used for hosting the modified badge HTML and the final processed badge image
Resource:
- !Join
- ''
- - 'arn:aws:s3:::'
- !Ref WrenBadgeImageResizeBucket
- /*
Roles:
- !Ref WrenBadgeRotatorFunctionRole
Outputs:
WrenBadgeRotatorFunction:
Description: "Lambda function ARN"
Value: !GetAtt WrenBadgeRotatorFunction.Arn
WrenbadgeRotatorFunctionIamRole:
Description: "Implicit IAM Role created for the badge rotator function"
Value: !GetAtt WrenBadgeRotatorFunctionRole.Arn

Handling secrets

We always want to keep our secrets out of our code and version control, and Lambda makes it easy to quickly store sensitive environment variables such as my API keys and my Github personal access token via the Lambda dashboard.

They are encrypted within AWS, and are automatically injected into my lambda context at runtime so my Go code only has to read secrets from the environment.

This way, I can keep all secrets out of my code so that I can open-source the app.

Note that unlike other AWS-native solutions for secrets management such as AWS Secrets Manager and AWS Systems Manager Parameter Store, Lambda will display your env vars in plaintext (without having to click anything to reveal the values) when you’re logged into the dashboard and viewing the function, but this is fine if you’re a solo developer running this in your own account.

Once you start working with a team, opt for the increased security (and increased management overhead and code complexity) of Secrets Manager or SSM parameter store.

Rewriting the HTML via Golang templates

The most interesting piece of this app is extracting the existing badge from Wren.co’s original HTML page, and rewriting its styles on the fly.

Since I noticed that the CSS styles for Wren’s badge page were in-lined into the page, it was easier to create a Golang template that was almost identical to their HTML page, but contained my modified rules.

const wrapper = `
<!doctype html>
<html>
<head>
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">
<style>
html {
# Here are some of my modifications
width: 300px;
height: 117px;
}
 .wrapper-link {
text-decoration: none;
}
# ... (more css rules elided here for brevity)
</head>
<body>
{{ .Contents }}
</body>

With this template, we need only the HTML of the badge itself at runtime, to feed into the Contents variable.

This is accomplished by using recursion to find the original badge’s wrapping <a> tag in the HTML document fetched from Wren. You can see the whole solution in the html-rewriter.go file in the repo:

// Badge recursively searches the HTML document to find the badge node, which is wrapped in an "a" tag, or link
func Badge(doc *html.Node) (*html.Node, error) {
var badge *html.Node
var crawler func(*html.Node)
crawler = func(node *html.Node) {
if node.Type == html.ElementNode && node.Data == "a" {
badge = node
return
}
for child := node.FirstChild; child != nil; child = child.NextSibling {
crawler(child)
}
}
crawler(doc)
if badge != nil {
return badge, nil
}
return nil, errors.New("Missing <a> in the node tree")
}

This all gets rendered into the body of the HTML document. The resulting modified HTML is then written locally to the Lambda execution context and then pushed to S3 so that it can be reached publicly by anyone including the HCTI API’s scraping and screenshotting bots.

Check out the code and use it yourself

I have pushed my solution to the following repo, which I’ve named wren-badge-rotator.

Since everything is parameterized via environment variables, if you are familiar with AWS / Cloudformation / SAM you can use it to deploy your own instance of the same app, populating your own Github and Wren usernames, as well as your Github personal access token and your HCTI API key and user ID.

Project structure at a glance

Taking a quick look at the overall structure of the project, the img directory just holds a picture used by the README.md to render an example image.

Makefile is used by the SAM cli to run builds when you excecute the sam build command.

The entirety of the Golang Lambda function is contained within wren-badge-rotator/.

$ tree
.
├── img
│ └── github-profile.png
├── Makefile
├── README.md
├── template.yaml
└── wren-badge-rotator
├── git.go
├── go.mod
├── go.sum
├── html-rewriter.go
├── html-to-img.go
├── main.go
├── s3.go
└── startup.go
2 directories, 12 files

If you want to explore the code in-depth, you can view the repository here: https://github.com/zackproser/wren-badge-rotator . It is reasonably well documented via comments.

Deploying your own instance

If you want to run it for yourself, you can fork it, modifying the environment variables to point to your own repos and Wren account. The README includes instructions for deploying it via the included Cloudformation template.yml.

Conclusion

With this solution in place, I can now have my Wren.co badge dislayed on my Github profile and auto-updated for me once per month, so that it will always reflect my latest stats. In the meantime, this was a fun experiment leveraging AWS Lambda, CloudWatch events and Cloudformation to create a no-maintenance, cheap to operate app using purely serverless AWS offerings.

--

--