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;
const headers = new Headers(request.headers);
const modifiedRequest = new Request(request, { headers });
if (!this.checkRequest(modifiedRequest)) {
return;
}
let response = null;
try {
const headRequest = new Request(modifiedRequest.url, {
method: 'HEAD',
headers: modifiedRequest.headers,
});
response = await fetch(headRequest);
} 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);
}
response.headers.set('Cache-Control', 'max-age=0');
const streamResponse = new Response(
await this.combineStreams(modifiedRequest),
response
);
event.respondWith(streamResponse);
}
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];
}
checkRequest(request) {
if (request.method !== 'GET') {
return false;
}
if (request.headers.has('Range')) {
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;
}
async combineStreams(request) {
const { readable, writable } = new TransformStream();
this.handleStream(request, writable);
return readable;
}
async handleStream(request, writable) {
const comment = this.customInfo;
const relativeOffset = this.rangeRelativeOffset;
const encoder = new TextEncoder();
const section = encoder.encode(comment);
try {
const apkHeader = await this.apkHeaderStream(request);
const apkBody = await this.apkBodyStream(
request,
section,
relativeOffset
);
const apkBodyStream = apkBody.stream();
const apkTail = await this.apkTailStream(request);
const sources = [apkHeader, apkBodyStream, apkTail];
for (const stream of sources) {
try {
await stream.pipeTo(writable, {
preventClose: true,
});
} catch (e) {
console.error('STREAM_ERROR: ', e);
}
}
} catch (err) {
console.error('HANDLE_STREAM_ERROR: ', err);
} finally {
let writer = writable.getWriter();
writer.close();
writer.releaseLock();
}
}
async apkHeaderStream(request) {
const headers = new Headers(request.headers);
headers.set('Range', `bytes=0-${this.customBlockValueStart - 1}`);
const headResponse = await fetch(request, {
headers: headers,
});
return headResponse.body;
}
async apkBodyStream(request, section = null, relativeOffset = 0) {
const headers = new Headers(request.headers);
headers.set(
'Range',
`bytes=${this.customBlockValueStart}-${this.customBlockValueEnd - 1}`
);
const middleResponse = await fetch(request, {
headers: headers,
});
const reader = middleResponse.body.getReader();
let outputBuffers = [];
try {
let handledBytes = this.customBlockValueStart;
while (true) {
const result = await reader.read();
if (result.done) {
console.log('APK_BODY_STREAM_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);
}
outputBuffers.push(buffer);
}
} catch (err) {
console.error('APK_BODY_STREAM_ERROR: ', err);
}
return new Blob(outputBuffers);
}
async apkTailStream(request) {
const headers = new Headers(request.headers);
headers.set(
'Range',
`bytes=${this.customBlockValueEnd}-${this.totalSize - 1}`
);
const tailResponse = await fetch(request, {
headers: headers,
});
return tailResponse.body;
}
}
async function handleEvent(event) {
const edgepack = new EdgePack();
await edgepack.handle(event);
}
addEventListener('fetch', handleEvent);
Was this page helpful?