CDN hashes¶
Some endpoints, like the imageUrl provided by thumbnails.roblox.com/v1/users/avatar-3d?userId=1
, don't provide a full
CDN URL and only provide raw hashes, like this: bbdb80c2b573bf222da3e92f5f148330
.
We need to turn this into a full CDN URL.
A CDN URL looks like t[X].rbxcdn.com/bbdb80c2b573bf222da3e92f5f148330
where X is the CDN number.
The CDN number ranges from 0 to 7, so you might be tempted to send a request to t0, then t1, and keep going until you
reach the one containing the object. This works, but it's quite wasteful as you send up to 8 requests for one object.
A common implementation is as follows:
Create a function that takes in a string hash
. In this function, define x
to 31
.
Next, we loop through the first 32 characters in hash
, and in each iteration set the x
variable to itself bitwise
XORed against the integer representation of that character (x ^= chr
). Then we return x
.
We can then use the x
variable to compose the final URL with the pattern https://t{x}.rbxcdn.com/{hash}
.
Here are some test cases for this function:
func("bbdb80c2b573bf222da3e92f5f148330") -> 5
func("139602eb7c640c43833470e07caada4a") -> 7
func("b717c50234c3d91b0be7dbfc9c588ed4") -> 0
Examples¶
def get_cdn_url(hash):
i = 31
for char in hash[:32]:
i ^= ord(char) # i ^= int(char, 16) also works
return f"https://t{i%8}.rbxcdn.com/{hash}"
# alternatively:
from functools import reduce
def get_cdn_url(hash):
t = reduce(lambda last_code, char: last_code ^ ord(char), hash, 31)
return f"https://t{t % 8}.rbxcdn.com/{hash}"
package pkg
import "fmt"
func GetCdnUrl(hash string) string {
if hash == "" {
panic("hash is empty")
}
var i int = 31
for _, char := range hash {
i = i ^ int(char)
}
return fmt.Sprintf("https://t%d.rbxcdn.com/%s", i%8, hash)
}
defmodule CDN do
@spec get_cdn_url(String.t()) :: String.t()
def get_cdn_url(hash) do
t =
hash
|> String.to_charlist()
|> Enum.reduce(31, fn char, last_code -> Bitwise.bxor(last_code, char) end)
"https://t#{rem(t, 8)}.rbxcdn.com/#{hash}"
end
end
Commands to run, tested on an amd64 Arch Linux installation:
nasm -felf64 cdn_hash.asm -o cdn_hash.o
gcc -m64 -o cdn_hash cdn_hash.o
./cdn_hash
extern printf, snprintf
section .text
global main
get_cdn_url:
push rdi
push rsi
push rdx
push rcx
push r8
push rax
; rdi is the accumulator
mov rdi, 31
jmp .is_at_end
.xor_t:
; zero-extend the character (1 byte) so the other 7 bytes of rdx do not contain garbage data, from my testing it works
; even with the garbage but it's better to be safe
movzx rdx, byte [rax]
xor rdi, rdx
; increment hash pointer
inc rax
.is_at_end:
cmp byte [rax], 0
jne .xor_t
.fmt_cdn_url:
mov rax, rdi
xor rdx, rdx
mov rsi, 8
div rsi
; t
mov rcx, rdx
; buffer
lea rdi, [rel cdn_url]
; buffer size
mov rsi, 55
; format
lea rdx, [rel url_fmt]
; hash
pop rax
mov r8, rax
push rax
xor rax, rax
call [rel snprintf wrt ..got]
pop rax
pop r8
pop rcx
pop rdx
pop rsi
pop rdi
ret
main:
lea rax, [rel hash]
call get_cdn_url
lea rdi, [rel s_fmt]
lea rsi, [rel cdn_url]
xor rax, rax
call [rel printf wrt ..got]
mov rax, 60
xor rdi, rdi
syscall
section .data
cdn_url: times 55 db 0
s_fmt: db "%s", 10, 0
url_fmt: db "https://t%d.rbxcdn.com/%s", 0
hash: db "bbdb80c2b573bf222da3e92f5f148330", 0
const getCdnUrl = (hash) => {
const t = [...hash].reduce((lastCode, char) => lastCode ^ char.charCodeAt(0), 31);
return `https://t${t % 8}.rbxcdn.com/${hash}`;
}
using System;
using System.Linq;
string GetCdnUrl(string hash) {
int t = hash.ToCharArray().Aggregate(31, (lastCode, character) => lastCode ^ (int)character);
return $"https://t{t % 8}.rbxcdn.com/{hash}";
}
def get_cdn_url(hash)
t = hash.codepoints.reduce(31) { |last_code, code| last_code ^ code }
"https://t#{t % 8}.rbxcdn.com/#{hash}"
end
std::string getCdnUrl(const std::string& hash)
{
if (hash.empty()) throw std::exception("Hash cannot be empty");
int i = 31;
for (char const& c : hash)
{
i ^= (int)c;
}
char buff[100];
snprintf(buff, sizeof(buff), "https://t%d.rbxcdn.com/%s", i % 8, hash.c_str());
return std::string(buff);
}
void getCdnUrl(char *hash, char *buffer) {
int i = 31;
for (int j = 0; j < strlen(hash); j++) {
i ^= (int)hash[j];
}
snprintf(buffer, 55, "https://t%d.rbxcdn.com/%s", i % 8, hash);
}
fn get_cdn_url(hash: &str) -> String {
let t = hash.as_bytes().iter().fold(31, |last_code, code| {
last_code ^ code
});
format!("https://t{}.rbxcdn.com/{}", t % 8, hash)
}
local function getCdnUrl(hash)
local i = 31
for _, code in utf8.codes(hash) do
i = i ~ code
end
return string.format("https://t%d.rbxcdn.com/%s", i % 8, hash)
end
local function getCdnUrl(hash)
local i = 31
for _, code in utf8.codes(hash) do
i = bit32.bxor(i, code)
end
return string.format("https://t%d.rbxcdn.com/%s", i % 8, hash)
end
String getCdnUrl(String hash) {
int i = 31;
for (char character : hash.toCharArray()) {
i ^= (int) character;
}
return String.format("https://t%d.rbxcdn.com/%s", i % 8, hash);
}
fun getCdnUrl(hash: String): String {
val t = hash.toByteArray().fold(31) {acc, code -> acc xor code.toInt()}
return "https://t${t % 8}.rbxcdn.com/${hash}"
}
def get_cdn_url(hash : String): String
t = hash.codepoints.reduce(31) { |last_code, code| last_code ^ code }
"https://t#{t % 8}.rbxcdn.com/#{hash}"
end
let getCdnUrl (hash: string) =
let t = hash |> Seq.fold (fun lastCode char -> lastCode ^^^ (int)char) 31
$"https://t{t % 8}.rbxcdn.com/{hash}"
function get-cdn-url {
param (
[string] $hash
)
if ([string]::IsNullOrEmpty($hash)) { throw [System.ArgumentNullException]::new("hash"); }
[int] $i = 31;
foreach ($c in $hash.ToCharArray()) {
$i = $i -bxor $c;
}
return "https://t$($i % 8).rbxcdn.com/$($hash)"
}