From b9f4630c8b6731e2f8742a0bb421d754ed3dbc8a Mon Sep 17 00:00:00 2001 From: tibs Date: Wed, 27 Apr 2022 22:29:47 -0400 Subject: [PATCH 1/3] Only include `X-Crafthead-Skin-Model` header on relevant endpoints --- worker/index.ts | 9 ---- worker/services/mojang/service.ts | 69 ++++++++++++++++++------------- 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/worker/index.ts b/worker/index.ts index 9652e12..0ad778f 100644 --- a/worker/index.ts +++ b/worker/index.ts @@ -7,7 +7,6 @@ import {getRenderer} from './wasm'; import PromiseGatherer from "./promise_gather"; import {CachedMojangApiService, DirectMojangApiService} from "./services/mojang/api"; import { default as CACHE_BUST } from './util/cache-bust'; -import { EMPTY } from "./data"; self.addEventListener('fetch', (event: FetchEvent) => { event.respondWith(handleRequest(event)); @@ -109,14 +108,6 @@ async function processRequest(skinService: MojangRequestService, interpreted: Cr } case RequestedKind.Cape: { const cape = await skinService.retrieveCape(interpreted, gatherer); - if (cape.status === 404) { - return new Response(EMPTY, { - status: 404, - headers: { - 'X-Crafthead-Profile-Cache-Hit': 'invalid-profile' - } - }); - } return renderImage(cape, interpreted); } default: diff --git a/worker/services/mojang/service.ts b/worker/services/mojang/service.ts index 482e636..befa344 100644 --- a/worker/services/mojang/service.ts +++ b/worker/services/mojang/service.ts @@ -1,8 +1,8 @@ /// import PromiseGatherer from '../../promise_gather'; -import { IdentityKind, CraftheadRequest, TextureKind } from '../../request'; -import { ALEX_SKIN, STEVE_SKIN } from '../../data'; +import { IdentityKind, CraftheadRequest, RequestedKind, TextureKind } from '../../request'; +import { ALEX_SKIN, EMPTY, STEVE_SKIN } from '../../data'; import { MojangApiService, MojangProfile, MojangProfileProperty } from "./api"; import { CacheComputeResult } from '../../util/cache-helper'; import { fromHex, javaHashCode, offlinePlayerUuid, toHex, uuidVersion } from '../../util/uuid'; @@ -60,7 +60,7 @@ export default class MojangRequestService { /** * Fetches a texture directly from the Mojang servers. Assumes the request has been normalized already. */ - private async retrieveTextureDirect(request: CraftheadRequest, gatherer: PromiseGatherer, kind: TextureKind): Promise { + private async retrieveTextureDirect(request: CraftheadRequest, gatherer: PromiseGatherer, kind: TextureKind): Promise { const rawUuid = fromHex(request.identity); if (uuidVersion(rawUuid) === 4) { const lookup = await this.mojangApi.fetchProfile(request.identity, gatherer); @@ -69,39 +69,38 @@ export default class MojangRequestService { if (textureResponse) { const buff = await textureResponse.texture.arrayBuffer(); if (buff && buff.byteLength > 0) { - return new Response(buff, { - status: 200, - headers: { - 'X-Crafthead-Profile-Cache-Hit': lookup.source, - 'X-Crafthead-Skin-Model': request.model || textureResponse.model || 'default' - } - }); + return { + texture: new Response(buff, { + status: 200, + headers: { + 'X-Crafthead-Profile-Cache-Hit': lookup.source + } + }), + model: textureResponse.model + }; } } - return new Response(STEVE_SKIN, { + return { texture: new Response(STEVE_SKIN, { status: 404, headers: { - 'X-Crafthead-Profile-Cache-Hit': 'not-found', - 'X-Crafthead-Skin-Model': 'default' + 'X-Crafthead-Profile-Cache-Hit': 'not-found' } - }); + }) }; } - return new Response(STEVE_SKIN, { + return { texture: new Response(STEVE_SKIN, { status: 404, headers: { - 'X-Crafthead-Profile-Cache-Hit': 'not-found', - 'X-Crafthead-Skin-Model': 'default' + 'X-Crafthead-Profile-Cache-Hit': 'not-found' } - }); + }) }; } - return new Response(STEVE_SKIN, { + return { texture: new Response(STEVE_SKIN, { status: 404, headers: { - 'X-Crafthead-Profile-Cache-Hit': 'offline-mode', - 'X-Crafthead-Skin-Model': 'default' + 'X-Crafthead-Profile-Cache-Hit': 'offline-mode' } - }); + }) }; } async retrieveSkin(request: CraftheadRequest, gatherer: PromiseGatherer): Promise { @@ -112,31 +111,43 @@ export default class MojangRequestService { const normalized = await this.normalizeRequest(request, gatherer); const skin = await this.retrieveTextureDirect(normalized, gatherer, TextureKind.SKIN); - if (skin.status === 404) { + if (skin.texture.status === 404) { // Offline mode ID (usually when we have a username and the username isn't valid) const rawUuid = fromHex(normalized.identity); if (Math.abs(javaHashCode(rawUuid)) % 2 == 0) { return new Response(STEVE_SKIN, { headers: { - 'X-Crafthead-Profile-Cache-Hit': 'invalid-profile', - 'X-Crafthead-Skin-Model': 'default' + 'X-Crafthead-Profile-Cache-Hit': 'invalid-profile' } }); } else { return new Response(ALEX_SKIN, { headers: { - 'X-Crafthead-Profile-Cache-Hit': 'invalid-profile', - 'X-Crafthead-Skin-Model': 'slim' + 'X-Crafthead-Profile-Cache-Hit': 'invalid-profile' } }); } } - return skin; + + if ([RequestedKind.Skin, RequestedKind.Body, RequestedKind.Bust].includes(normalized.requested)) { + skin.texture.headers.set('X-Crafthead-Skin-Model', request.model || skin.model || 'default'); + } + + return skin.texture; } async retrieveCape(request: CraftheadRequest, gatherer: PromiseGatherer): Promise { const normalized = await this.normalizeRequest(request, gatherer); - return this.retrieveTextureDirect(normalized, gatherer, TextureKind.CAPE); + const cape = await this.retrieveTextureDirect(normalized, gatherer, TextureKind.CAPE); + if (cape.texture.status === 404) { + return new Response(EMPTY, { + status: 404, + headers: { + 'X-Crafthead-Profile-Cache-Hit': 'invalid-profile' + } + }); + } + return cape.texture; } private static async fetchTextureFromProfile(profile: MojangProfile, type: TextureKind): Promise { From cecc24e2e95e22535782845913580865478b9c30 Mon Sep 17 00:00:00 2001 From: tibs Date: Wed, 27 Apr 2022 23:02:08 -0400 Subject: [PATCH 2/3] Add `X-Crafthead-Texture-ID` header --- worker/services/mojang/service.ts | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/worker/services/mojang/service.ts b/worker/services/mojang/service.ts index befa344..b5c7643 100644 --- a/worker/services/mojang/service.ts +++ b/worker/services/mojang/service.ts @@ -69,13 +69,14 @@ export default class MojangRequestService { if (textureResponse) { const buff = await textureResponse.texture.arrayBuffer(); if (buff && buff.byteLength > 0) { + const response = new Response(buff, { + status: 200, + headers: textureResponse.texture.headers + }); + response.headers.set('X-Crafthead-Profile-Cache-Hit', lookup.source); + return { - texture: new Response(buff, { - status: 200, - headers: { - 'X-Crafthead-Profile-Cache-Hit': lookup.source - } - }), + texture: response, model: textureResponse.model }; } @@ -171,7 +172,14 @@ export default class MojangRequestService { } console.log("Successfully retrieved texture"); - return { texture: textureResponse, model: texturesData?.SKIN?.metadata?.model }; + + const response = new Response(textureResponse.body); + const textureID = textureUrl.split('/').pop(); + if (textureID) { + response.headers.set("X-Crafthead-Texture-ID", textureID) + } + + return { texture: response, model: texturesData?.SKIN?.metadata?.model }; } } From 32b25ef7259ea5136a4482c88b5c268767e03d1c Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 27 Dec 2024 18:13:40 +0000 Subject: [PATCH 3/3] chore: update for latest repo changes --- src/worker/index.ts | 2 + src/worker/services/mojang/api.ts | 2 +- src/worker/services/mojang/service.ts | 92 +++++++++++++++++---------- test/worker.test.ts | 77 +++++++++++++++++++++- 4 files changed, 139 insertions(+), 34 deletions(-) diff --git a/src/worker/index.ts b/src/worker/index.ts index 0ffa655..c875d20 100644 --- a/src/worker/index.ts +++ b/src/worker/index.ts @@ -19,6 +19,8 @@ function decorateHeaders(interpreted: CraftheadRequest, headers: Headers, hitCac copiedHeaders.set('X-Crafthead-Request-Cache-Hit', hitCache ? 'yes' : 'no'); if (!copiedHeaders.has('Content-Type')) { copiedHeaders.set('Content-Type', interpreted.requested === RequestedKind.Profile ? 'application/json' : 'image/png'); + } else if (copiedHeaders.get('Content-Type') !== 'application/json' && copiedHeaders.get('Content-Type')?.includes?.('text/plain') && interpreted.requested === RequestedKind.Profile) { + copiedHeaders.set('Content-Type', 'application/json'); } else { console.log(`Content-Type header already on response: ${copiedHeaders.get('Content-Type')}, not overriding.`); } diff --git a/src/worker/services/mojang/api.ts b/src/worker/services/mojang/api.ts index 84d60dd..887b4d9 100644 --- a/src/worker/services/mojang/api.ts +++ b/src/worker/services/mojang/api.ts @@ -129,7 +129,7 @@ export class DirectMojangApiService implements MojangApiService { }; return { result: data, - source: 'miss', + source: returnedProfile?.meta?.cached_at ? 'hit' : 'miss', }; } else if (profileResponse.status === 206 || profileResponse.status === 204) { return { diff --git a/src/worker/services/mojang/service.ts b/src/worker/services/mojang/service.ts index 3710156..2c63de7 100644 --- a/src/worker/services/mojang/service.ts +++ b/src/worker/services/mojang/service.ts @@ -1,5 +1,5 @@ -import { ALEX_SKIN, STEVE_SKIN } from '../../data'; -import { IdentityKind, TextureKind } from '../../request'; +import { ALEX_SKIN, EMPTY, STEVE_SKIN } from '../../data'; +import { IdentityKind, RequestedKind, TextureKind } from '../../request'; import { fromHex, javaHashCode, @@ -66,10 +66,12 @@ export default class MojangRequestService { /** * Fetches a texture directly from the Mojang servers. Assumes the request has been normalized already. */ - private async retrieveTextureDirect(request: CraftheadRequest, gatherer: PromiseGatherer, kind: TextureKind): Promise { + private async retrieveTextureDirect(request: CraftheadRequest, gatherer: PromiseGatherer, kind: TextureKind): Promise { if (request.identityType === IdentityKind.TextureID) { const textureResponse = await MojangRequestService.fetchTextureFromId(request.identity); - return MojangRequestService.constructTextureResponse(textureResponse, request); + return { + texture: await MojangRequestService.constructTextureResponse(textureResponse, request), + }; } const rawUuid = fromHex(request.identity); if (uuidVersion(rawUuid) === 4) { @@ -77,43 +79,50 @@ export default class MojangRequestService { if (lookup.result) { const textureResponse = await MojangRequestService.fetchTextureFromProfile(lookup.result, kind); if (textureResponse) { - return MojangRequestService.constructTextureResponse(textureResponse, request, lookup.source); + return { + texture: await MojangRequestService.constructTextureResponse(textureResponse, request, lookup.source), + model: textureResponse.model, + }; } - return new Response(STEVE_SKIN, { + return { + texture: new Response(STEVE_SKIN, { + status: 404, + headers: { + 'X-Crafthead-Profile-Cache-Hit': 'not-found', + }, + }), + }; + } + return { + texture: new Response(STEVE_SKIN, { status: 404, headers: { 'X-Crafthead-Profile-Cache-Hit': 'not-found', - 'X-Crafthead-Skin-Model': 'default', }, - }); - } - return new Response(STEVE_SKIN, { + }), + }; + } + + return { + texture: new Response(STEVE_SKIN, { status: 404, headers: { - 'X-Crafthead-Profile-Cache-Hit': 'not-found', - 'X-Crafthead-Skin-Model': 'default', + 'X-Crafthead-Profile-Cache-Hit': 'offline-mode', }, - }); - } - - return new Response(STEVE_SKIN, { - status: 404, - headers: { - 'X-Crafthead-Profile-Cache-Hit': 'offline-mode', - 'X-Crafthead-Skin-Model': 'default', - }, - }); + }), + }; } private static async constructTextureResponse(textureResponse: TextureResponse, request: CraftheadRequest, source?: string): Promise { const buff = await textureResponse.texture.arrayBuffer(); if (buff && buff.byteLength > 0) { + const headers = new Headers(textureResponse.texture.headers); + if (!headers.has('X-Crafthead-Profile-Cache-Hit')) { + headers.set('X-Crafthead-Profile-Cache-Hit', source || 'miss'); + } return new Response(buff, { status: 200, - headers: { - 'X-Crafthead-Profile-Cache-Hit': source || 'miss', - 'X-Crafthead-Skin-Model': request.model || textureResponse.model || 'default', - }, + headers, }); } return new Response(STEVE_SKIN, { @@ -133,30 +142,41 @@ export default class MojangRequestService { const normalized = await this.normalizeRequest(request, gatherer); const skin = await this.retrieveTextureDirect(normalized, gatherer, TextureKind.SKIN); - if (skin.status === 404) { + if (skin.texture.status === 404) { // Offline mode ID (usually when we have a username and the username isn't valid) const rawUuid = fromHex(normalized.identity); if (Math.abs(javaHashCode(rawUuid)) % 2 === 0) { return new Response(STEVE_SKIN, { headers: { 'X-Crafthead-Profile-Cache-Hit': 'invalid-profile', - 'X-Crafthead-Skin-Model': 'default', }, }); } return new Response(ALEX_SKIN, { headers: { 'X-Crafthead-Profile-Cache-Hit': 'invalid-profile', - 'X-Crafthead-Skin-Model': 'slim', }, }); } - return skin; + if ([RequestedKind.Skin, RequestedKind.Body, RequestedKind.Bust].includes(normalized.requested)) { + skin.texture.headers.set('X-Crafthead-Skin-Model', request.model || skin.model || 'default'); + } + + return skin.texture; } async retrieveCape(request: CraftheadRequest, gatherer: PromiseGatherer): Promise { const normalized = await this.normalizeRequest(request, gatherer); - return this.retrieveTextureDirect(normalized, gatherer, TextureKind.CAPE); + const cape = await this.retrieveTextureDirect(normalized, gatherer, TextureKind.CAPE); + if (cape.texture.status === 404) { + return new Response(EMPTY, { + status: 404, + headers: { + 'X-Crafthead-Profile-Cache-Hit': 'invalid-profile', + }, + }); + } + return cape.texture; } private static async fetchTextureFromProfile(profile: MojangProfile, type: TextureKind): Promise { @@ -180,8 +200,16 @@ export default class MojangRequestService { throw new Error(`Unable to retrieve texture from Mojang, http status ${textureResponse.status}`); } + const response = new Response(textureResponse.body); + const textureID = textureUrl.split('/').pop(); + if (textureID) { + response.headers.set('X-Crafthead-Texture-ID', textureID); + } //console.log('Successfully retrieved texture'); - return { texture: textureResponse, model: texturesData?.SKIN?.metadata?.model }; + return { + texture: response, + model: texturesData?.SKIN?.metadata?.model, + }; } } diff --git a/test/worker.test.ts b/test/worker.test.ts index 9e87482..d763e67 100644 --- a/test/worker.test.ts +++ b/test/worker.test.ts @@ -5,7 +5,7 @@ import worker from '../src/worker/index'; const IncomingRequest = Request; -describe('worker', () => { +describe('worker requests', () => { it('responds with HTML for index', async () => { const request = new IncomingRequest('http://crafthead.net'); const ctx = createExecutionContext(); @@ -202,3 +202,78 @@ describe('worker', () => { expect(json.properties[0].name).toBe('textures'); }); }); + +describe('worker headers', () => { + it('responds with expected headers', async () => { + const request = new IncomingRequest('http://crafthead.net/avatar/ef6134805b6244e4a4467fbe85d65513'); + const ctx = createExecutionContext(); + const response = await worker.fetch(request, env, ctx); + await waitOnExecutionContext(ctx); + expect(response.headers.get('access-control-allow-origin')).toBe('*'); + expect(response.headers.get('cache-control')).toBe('max-age=14400'); + expect(response.headers.get('content-type')).toBe('image/png'); + expect(response.headers.get('x-crafthead-request-cache-hit')).toBe('no'); + expect(response.headers.get('x-crafthead-texture-id')).toBe('9d2e80355eed693e3f0485893ef04ff6a507f3aab33f2bedb48cef56e30f67d0'); + expect(response.headers.get('x-crafthead-skin-model')).toBeNull(); + + // make second response to check cache hit + const ctx2 = createExecutionContext(); + const response2 = await worker.fetch(request, env, ctx2); + await waitOnExecutionContext(ctx2); + expect(response2.headers.get('x-crafthead-request-cache-hit')).toBe('yes'); + }); + + it('responds with expected headers for profile', async () => { + const request = new IncomingRequest('http://crafthead.net/profile/CherryJimbo'); + const ctx = createExecutionContext(); + const response = await worker.fetch(request, env, ctx); + await waitOnExecutionContext(ctx); + expect(response.headers.get('access-control-allow-origin')).toBe('*'); + expect(response.headers.get('cache-control')).toBe('max-age=14400'); + expect(response.headers.get('content-type')).toBe('application/json'); + expect(response.headers.get('x-crafthead-request-cache-hit')).toBe('no'); + + // make second response to check cache hit + const ctx2 = createExecutionContext(); + const response2 = await worker.fetch(request, env, ctx2); + await waitOnExecutionContext(ctx2); + expect(response2.headers.get('x-crafthead-request-cache-hit')).toBe('yes'); + }); + + it('responds with expected headers for body', async () => { + const request = new IncomingRequest('http://crafthead.net/body/CherryJimbo'); + const ctx = createExecutionContext(); + const response = await worker.fetch(request, env, ctx); + await waitOnExecutionContext(ctx); + expect(response.headers.get('access-control-allow-origin')).toBe('*'); + expect(response.headers.get('cache-control')).toBe('max-age=14400'); + expect(response.headers.get('content-type')).toBe('image/png'); + expect(response.headers.get('x-crafthead-request-cache-hit')).toBe('no'); + expect(response.headers.get('x-crafthead-skin-model')).toBe('default'); + expect(response.headers.get('x-crafthead-texture-id')).toBe('9d2e80355eed693e3f0485893ef04ff6a507f3aab33f2bedb48cef56e30f67d0'); + + // make second response to check cache hit + const ctx2 = createExecutionContext(); + const response2 = await worker.fetch(request, env, ctx2); + await waitOnExecutionContext(ctx2); + expect(response2.headers.get('x-crafthead-request-cache-hit')).toBe('yes'); + }); + + it('responds with expected headers for body (slim)', async () => { + const request = new IncomingRequest('http://crafthead.net/body/Alex'); + const ctx = createExecutionContext(); + const response = await worker.fetch(request, env, ctx); + await waitOnExecutionContext(ctx); + expect(response.headers.get('access-control-allow-origin')).toBe('*'); + expect(response.headers.get('cache-control')).toBe('max-age=14400'); + expect(response.headers.get('content-type')).toBe('image/png'); + expect(response.headers.get('x-crafthead-request-cache-hit')).toBe('no'); + expect(response.headers.get('x-crafthead-skin-model')).toBe('slim'); + + // make second response to check cache hit + const ctx2 = createExecutionContext(); + const response2 = await worker.fetch(request, env, ctx2); + await waitOnExecutionContext(ctx2); + expect(response2.headers.get('x-crafthead-request-cache-hit')).toBe('yes'); + }); +});