This content originally appeared on DEV Community and was authored by Joel Hans
All good things must come to an end, including my explorations of how ngrok engineers use ngrok for play. While the rest of this series has been about homelabs, the last entry is something completely different: a serverless URL shortener that runs entirely on ngrok.
No upstream services, you say? Everyone’s a fan of maintaining less.
—
Euan (Principal Software Engineer): Serverless URL shortener for ngrokkers
I used to run a URL shortener in college. That was a mistake, but you know, once you start running something you can’t stop running it, so I’ve been running it forever, ever since then. It would be nice to run it with no servers involved because maintaining servers is fun, but not maintaining them is also fun.
Since I started working at ngrok, I noticed that many work-related URLs are long and hard to read, like https://www.notion.so/ngrok/$PAGE-TITLE-$super-long-uuid-$super-long-hash-uuid
. go.ngrok.pizza/page-title
is way shorter.
It seemed like a fun challenge to try and make an fully functioning URL shorterner with no external dependencies, and so I ended up with the following policy.
This uses ngrok’s Internal Endpoints as the sorta key/value database, which is written to with the http-request
action, and read with the forward-internal
action.
I won’t exactly recommend using anything like this, but I do think it’s a neat hack!
Gateway config + shortener logic with a Cloud Endpoint:
on_http_request:
- actions:
- type: "oauth"
config:
provider: "google"
- name: ngrok-only
expressions:
- "!actions.ngrok.oauth.identity.email.endsWith('@ngrok.com')"
actions:
- type: "deny"
- name: create-new
expressions: [ "req.method == 'POST'" ]
actions:
- type: http-request
config:
url: "https://api.ngrok.com/endpoints"
method: "POST"
headers:
Authorization: "Bearer <secret goes here>"
Content-Type: "application/json"
Ngrok-Version: "2"
body: |-
{
"description": "go places target",
"metadata": "Created by ${actions.ngrok.oauth.identity.email}",
"bindings": ["internal"],
"url": "https://${req.url.query_params['slug'][0]}.go.internal",
"traffic_policy": "{\"on_http_request\": [{\"actions\": [{\"type\": \"redirect\",\"config\": {\"to\": \"${req.url.query_params['url'][0]}\"}}]}]}"
}
- type: custom-response
config:
status_code: 200
headers:
content-type: "text/html"
body: |-
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>URL Shortener</title>
</head>
<body>
URL: <a href="https://${endpoint.host}/${req.url.query_params['slug'][0]}">https://${endpoint.host}/${req.url.query_params['slug'][0]}</a>
</body>
</html>
- name: redirect
expressions: ["req.url.path != '/'"]
actions:
- type: forward-internal
config:
url: "https://${req.url.path.substring(1)}.go.internal"
on_error: continue
- type: custom-response
config:
status_code: 404
headers:
content-type: "text/html"
body: |-
not found
- name: default
actions:
- type: custom-response
config:
status_code: 200
headers:
content-type: "text/html"
body: |-
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>URL Shortener - ngrok Internal</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; line-height: 1.6; color: #333; background-color: #f4f4f4; padding: 20px; } .container { max-width: 600px; margin: 0 auto; background-color: #fff; padding: 40px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } h1 { color: #2c3e50; margin-bottom: 10px; text-align: center; } .subtitle { text-align: center; color: #7f8c8d; margin-bottom: 30px; font-size: 14px; } .info-box { background-color: #e8f4f8; border-left: 4px solid #3498db; padding: 15px; margin-bottom: 30px; border-radius: 4px; } .info-box h3 { margin-bottom: 8px; color: #2c3e50; } .info-box p { color: #555; font-size: 14px; line-height: 1.5; } form { display: flex; flex-direction: column; gap: 20px; } .form-group { display: flex; flex-direction: column; gap: 8px; } label { font-weight: 600; color: #2c3e50; font-size: 14px; } .label-hint { font-weight: normal; color: #7f8c8d; font-size: 12px; } input { padding: 12px 16px; border: 2px solid #e0e0e0; border-radius: 6px; font-size: 16px; transition: all 0.3s ease; } input:focus { outline: none; border-color: #3498db; box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1); } button { background-color: #3498db; color: white; padding: 12px 24px; border: none; border-radius: 6px; font-size: 16px; font-weight: 600; cursor: pointer; transition: all 0.3s ease; } button:hover { background-color: #2980b9; transform: translateY(-1px); box-shadow: 0 2px 5px rgba(0,0,0,0.2); } button:active { transform: translateY(0); box-shadow: 0 1px 2px rgba(0,0,0,0.2); } #out { margin-top: 20px; padding: 15px; border-radius: 6px; font-size: 14px; word-break: break-all; } #out.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; } #out.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
</style>
</head>
<body>
<div class="container">
<h1>URL Shortener</h1>
<p class="subtitle">ngrok Internal Tool</p>
<div class="info-box">
<h3>How it works</h3>
<p>Create short, memorable links that redirect to any URL. Perfect for sharing long or complex URLs in a more user-friendly format.</p>
</div>
<form id="form" action="/create" method="POST">
<div class="form-group">
<label for="slug">
Short Link Name
<span class="label-hint">(e.g., "team-meeting" or "project-docs")</span>
</label>
<input type="text" id="slug" name="slug" placeholder="Enter a memorable name" required>
</div>
<div class="form-group">
<label for="url">
Destination URL
<span class="label-hint">(The full URL where the short link will redirect)</span>
</label>
<input type="url" id="url" name="url" placeholder="https://example.com/very/long/url" required>
</div>
<button type="submit">Create Short Link</button>
</form>
<div id="out"></div>
</div>
<script>
document.querySelector('#form').addEventListener('submit', async function(event) {
event.preventDefault(); // Prevent the default form submission
const slug = document.querySelector('#slug').value;
const url = document.querySelector('#url').value;
let resp = await fetch(window.location.href + '?slug=' + encodeURIComponent(slug) + '&url=' + encodeURIComponent(url), {
method: 'POST',
});
if (resp.ok) {
document.querySelector("#out").innerHTML = await resp.text();
document.querySelector("#out").className = 'success';
} else {
document.querySelector("#out").innerHTML = await resp.error();
document.querySelector("#out").className = 'error';
}
});
</script>
</body>
</html>
What’s happening here? Well, a lot, but in short—on every HTTP request, this policy:
- Enforces OAuth-based authentication using Google.
- Filters out all logins from email accounts that do not end with
ngrok.com
and denies them. -
If the request is a
POST
, sends an HTTP request to the ngrok API itself to create a new internal endpoint, which has its own policy to redirect the request from a long URL to a shortened one, and then responds with a shorttext/html
body containing the shortened URL. - Forwards all requests not to the root path (
/
) to one of the internal endpoints. - Sends a default HTML response to all requests on the root path with a small webpage that lets you add a short link name and destination URL.
—
Unfortunately, the shortener server itself is ngrok-only at the moment, but there’s nothing stopping you from signing up for an account, copy-pasting the configuration below into a Cloud Endpoint of your own, and either getting rid of the OAuth action or changing the email it filters for and trying it out yourself.
As before, some docs to get you started:
- Traffic Policy
oauth
Traffic Policy actionhttp-request
Traffic Policy actioncustom-response
Traffic Policy actionforward-internal
Traffic Policy action
Well, that wraps things up for this five-part series. I appreciate anyone who read even one of these playful use cases of gateways and policies! I hope they’ve given you some inspiration of how you might—with both security and simplicity in mind—play around with the public internet at the services you hold closest to heart.
This content originally appeared on DEV Community and was authored by Joel Hans