Skip to content

Commit

Permalink
chore: update for latest repo changes
Browse files Browse the repository at this point in the history
  • Loading branch information
Cherry committed Dec 27, 2024
1 parent 496e598 commit 32b25ef
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 34 deletions.
2 changes: 2 additions & 0 deletions src/worker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.`);
}
Expand Down
2 changes: 1 addition & 1 deletion src/worker/services/mojang/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
92 changes: 60 additions & 32 deletions src/worker/services/mojang/service.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -66,54 +66,63 @@ 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<Response> {
private async retrieveTextureDirect(request: CraftheadRequest, gatherer: PromiseGatherer, kind: TextureKind): Promise<TextureResponse> {
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) {
const lookup = await this.mojangApi.fetchProfile(request.identity, gatherer);
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<Response> {
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, {
Expand All @@ -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<Response> {
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<TextureResponse | undefined> {
Expand All @@ -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,
};
}
}

Expand Down
77 changes: 76 additions & 1 deletion test/worker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import worker from '../src/worker/index';

const IncomingRequest = Request<unknown, IncomingRequestCfProperties>;

describe('worker', () => {
describe('worker requests', () => {
it('responds with HTML for index', async () => {
const request = new IncomingRequest('http://crafthead.net');
const ctx = createExecutionContext();
Expand Down Expand Up @@ -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');
});
});

0 comments on commit 32b25ef

Please sign in to comment.