const CUSTOM_BLOCK_VALUE_LENGTH = 10240;
const APK_SIGNING_BLOCK_MAGIC_LENGTH = 16;
const APK_SIGNING_BLOCK_OFFSET_LENGTH = 8;
const APK_COMMENT_LENGTH = 512;
class EdgePack {
totalSize;
signVersion;
centralDirectoryOffset;
customBlockValueStart;
customBlockValueEnd;
rangeRelativeOffset;
customInfo;
constructor() {
this.totalSize = null;
this.signVersion = null;
this.centralDirectoryOffset = null;
this.customBlockValueStart = null;
this.customBlockValueEnd = null;
this.rangeRelativeOffset = null;
this.customInfo = null;
}
async handle(event) {
const { request } = event;
if (!this.checkRequest(request)) {
return;
}
let response = null;
try {
response = await fetch(request);
} catch (err) {
const error = {
code: 'FETCH_ORIGIN_ERROR',
message: err?.message,
};
response = new Response(JSON.stringify(error), {
status: 590,
});
}
if (!this.checkResponse(response)) {
return event.respondWith(response);
}
const { readable, writable } = new TransformStream();
this.handleStream(response, writable);
response.headers.set('Cache-Control', 'max-age=0');
const streamResponse = new Response(readable, response);
event.respondWith(streamResponse);
}
checkRequest(request) {
if (request.method !== 'GET') {
return false;
}
const { pathname, searchParams } = new URL(request.url);
const comment = searchParams?.get('comment');
if (!pathname.endsWith('.apk') || !comment) {
return false;
}
this.customInfo = comment;
return true;
}
checkResponse(response) {
if (response.status !== 200 && response.status !== 206) {
return false;
}
const contentLength = response.headers.get('Content-Length');
if (response.body === null || contentLength === null) {
return false;
}
this.totalSize = Number(contentLength);
const cosOffsetHeader = response.headers.get('x-cos-meta-edgepack-offset');
const cosTypeHeader = response.headers.get('x-cos-meta-edgepack-type');
if (!cosOffsetHeader || !cosTypeHeader) {
return false;
}
this.signVersion = cosTypeHeader;
this.centralDirectoryOffset = Number(cosOffsetHeader);
if (this.signVersion === 'v1') {
this.customBlockValueStart = this.totalSize - APK_COMMENT_LENGTH;
this.customBlockValueEnd = this.totalSize;
} else {
this.customBlockValueStart =
this.centralDirectoryOffset -
CUSTOM_BLOCK_VALUE_LENGTH -
APK_SIGNING_BLOCK_MAGIC_LENGTH -
APK_SIGNING_BLOCK_OFFSET_LENGTH;
this.customBlockValueEnd = this.centralDirectoryOffset;
}
this.rangeRelativeOffset = this.getRelativeOffset(response);
if (this.rangeRelativeOffset === null) {
return false;
}
return true;
}
getRelativeOffset(response) {
const start = this.customBlockValueStart;
const end = this.customBlockValueEnd;
const range = response.headers.get('Content-Range');
if (!range) return start;
const match = range.match(/bytes\\s*(\\d*)-(\\d*)/i);
if (!match || match?.length < 2) {
return start;
}
if (+match[2] < start || +match[1] > end) {
return null;
}
return start - +match[1];
}
async handleStream(response, writable) {
const comment = this.customInfo;
const relativeOffset = this.rangeRelativeOffset;
const responseBody = response.body;
const encoder = new TextEncoder();
const section = encoder.encode(comment);
const writer = writable.getWriter();
const reader = responseBody.getReader();
try {
let handledBytes = 0;
while (true) {
const result = await reader.read();
if (result.done) {
console.log('WRITE_COMMENT_DONE');
break;
}
const startByteOffset = handledBytes;
const buffer = result.value;
handledBytes += buffer.byteLength;
const min = Math.max(startByteOffset, relativeOffset);
const max = Math.min(relativeOffset + section.byteLength, handledBytes);
if (min < max) {
const bufferStart = min - startByteOffset;
const sectionStart = min - relativeOffset;
const sectionEnd = max - relativeOffset;
const replacement = section.subarray(sectionStart, sectionEnd);
new Uint8Array(buffer).set(replacement, bufferStart);
}
await writer.ready;
await writer.write(buffer);
}
} catch (err) {
console.error('WRITE_COMMENT_ERROR: ', err);
}
try {
await writer.ready;
await writer.close();
} catch (err) {
console.error('CLOSE_WRITER_ERROR: ', err);
} finally {
writer.releaseLock();
}
}
}
async function handleEvent(event) {
const edgepack = new EdgePack();
await edgepack.handle(event);
}
addEventListener('fetch', handleEvent);
Was this page helpful?