Blocking share tracking with a Cloudflare Worker and Apple Shortcuts
26/12/2024
I've been doing a fairly primitive version of this for a little while. Instagram upped their game, so I had to match pace.
I am not quiet about how much I hate share tracking - it's a great way to build up a network of your contacts as part of your shadow profile, just using your passive behaviour. It's far more decisive than cookies and it's totally platform agnostic; it works on iMessage, WhatsApp, Signal, whichever messaging platform you use.
You're probably wondering why I've chosen this hill to die on, of all of them. There's two reasons: number 1 is fuck social networks. They already have more information than they need. They already have a huge amount of a huge number of people's attention, and now they want to be sure that they know about your true social network as well as the one you've chosen to tell them about? No. And 2 is because I can. This is something I can do something about (for now), and it annoys me. And if something annoys you, and you can do something about it but you don't, then you're going to stay annoyed and that's what you deserve.
The premise is fairly simple. This needs to work on my phone since that's where I usually click shared links, and from there my choices are limited to: either making my own app, or using Apple's Shortcuts app. Shortcuts can do a lot of things, and I was doing this whole thing there when it was just removing query strings, but since Instagram started totally obfuscating the original URL, Shortcuts alone became inviable. For some reason, Apple has not implemented for
or while
loops in Shortcuts, which would make my chosen approach super dirty. So part two of this approach is to use a simple Cloudflare worker.
export default {
async fetch(request, env, ctx) {
// firstly we're going to do a bit of authentication. it's not exactly bulletproof
// but it's unlikely that people are going to be able to guess my token so I'm
// alright with it
const url = new URL(request.url)
const token = url.searchParams.get('X-TOKEN')
if (token !== env.TOKEN) {
return new Response('no')
}
// this approach does violate SOLID, which isn't exactly cool from the
// perspective of a Cloudflare worker, but these functions are going
// to be fairly lightweight for the most part and I wanted to encapsulate
// anything I might need in Shortcuts into one worker. This is unlikely to
// become a monolith in the true sense
const fns = {
'/notrack': notrack
}
const fn = fns[url.pathname]
if (fn) {
return fn(request, env, ctx, url)
} else {
return new Response('Nothing')
}
},
};
// this approach for this function might seem a little weird, but Instagram has
// these new URLs that are like https://instagram.com/share/ABCDEF which totally
// obfuscate the original URL, meaning we need to do redirects to find the true URL
//
// We, therefore, start with the original URL and as soon as we're not redirecting
// any more, we're satisfied that that's the _actual_ URL and we can return it
//
// I fully expect Meta to have some in-page rendered redirect next, which will mean
// I need to revisit this approach but we'll cross that bridge when we come to it
// I'm not using any of the original `fetch` stuff at this point but I wanted to
// keep it in there, just in case.
const notrack = async (_request, _env, _ctx, url) => {
let source = url.searchParams.get('u')
// I keep the checked URLs for debugging purposes, but I don't actually use them anywhere
const checked = [source]
let searching = true
while (searching) {
const response = await fetch(
source, {
method: 'HEAD',
headers: {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:133.0) Gecko/20100101 Firefox/133.0',
// this sec-fetch-mode is completely new to me. I couldn't figure out why `fetch` wasn't working
// but `curl` was so Charlie helped me but using mitmproxy to sniff out the hidden headers that
// curl and fetch use to play spot the difference. It never occurred to me to use an mitm this way
'sec-fetch-mode': 'navigate',
}
}
)
// if this request was redirected, we go again
if (response.redirected) {
source = response.url
checked.push(source)
// otherwise we're done
} else {
searching = false
}
}
return new Response(removeQueryString(source))
}
const removeQueryString = (url) => {
// just to make this a bit less aggressive, we want to make sure that we only remove tracking
// parameters. In my old shortcut I was removing the entire query string, which breaks
// most pagination and forums and I didn't want to port that nuance to this
const blocklist = [
'igsh', // instagram
'igshid', // instagram
'si', // youtube, spotify
'fbclid', //facebook
'feature', // youtube but just the domain you came from
'gclid', // google
'rdid', // facebook
'share_url', // facebook
]
// we just take the passed URL, and iterate over my blocked parameters to remove them
const uri = new URL(url)
for (const param of blocklist) {
uri.searchParams.delete(param)
}
return uri.toString()
}
And my Shortcut that implements this. This is the one that pulls in the URL from my clipboard - I have a homescreen widget for this so if someone sends me a share-tracking URL I can copy it to clipboard and hit this widget and it'll clean up the URL for me. Most people don't care about this, weirdly. I also have a share-sheet widget so I can share, then run the widget straight from there so the URL never goes into my clipboard. Just saves a step. I rarely use this one now as most social networks forego the share sheet for their own copy link (for this exact reason yay!), so the clipboard one is the one I use most.
And that's it. It's working for now, but I also have a platform to try and build something a bit more intense on if I need to. I'm not sure if it's worth rendering an entire URL just to do this redirect, but I wouldn't put it past Meta at this point. I guess this data is very valuable to them so they'll probably go to some lengths to build it.
LITE - Strata