// DEBUG MODE var SW_MAIN = ";;;/*\n * run in service worker\n */\n//\n// utils\n//\nfunction hexToBytes(inStr) {\n const outLen = inStr.length / 2;\n const outBuf = new Uint8Array(outLen);\n\n for (let i = 0; i < outLen; i++) {\n let byte = parseInt(inStr.substr(i * 2, 2), 16);\n console.assert(!isNaN(byte));\n outBuf[i] = byte;\n }\n return outBuf;\n}\n\nfunction bytesToHex(bytes) {\n let str = '';\n for (let i = 0; i < bytes.length; i++) {\n const val = bytes[i];\n let hex = val.toString(16);\n if (val < 16) {\n hex = '0' + hex;\n }\n str += hex;\n }\n return str;\n}\n\nfunction strHash(s) {\n let sum = 0;\n for (let i = 0, n = s.length; i < n; i++) {\n sum = (sum << 5) - sum + s.charCodeAt(i);\n sum = sum >>> 0;\n }\n return sum;\n}\n\nfunction memcmp(b1, b2, size) {\n // TODO: u32 optimize\n for (let i = 0; i < size; i++) {\n if (b1[i] !== b2[i]) {\n return false;\n }\n }\n return true;\n}\n\nasync function hashVerify(buf, hash) {\n const subtle = crypto.subtle;\n if (!subtle) {\n return true;\n }\n const sha256 = await subtle.digest('SHA-256', buf);\n const u8 = new Uint8Array(sha256);\n return memcmp(u8, hash, hash.length);\n}\nclass RC4 {\n constructor(key) {\n const sbox = new Uint8Array(256);\n let i, j = 0;\n\n for (i = 0; i < 256; i++) {\n sbox[i] = i;\n }\n for (i = 0; i < 256; i++) {\n j = (j + sbox[i] + key[i % key.length]) & 0xff;\n\n let tmp = sbox[i];\n sbox[i] = sbox[j];\n sbox[j] = tmp;\n }\n this.sbox = sbox;\n this.pos1 = 0;\n this.pos2 = 0;\n }\n\n crypt(data) {\n const sbox = this.sbox;\n let pos1 = this.pos1;\n let pos2 = this.pos2;\n\n for (let i = 0; i < data.length; i++) {\n pos1 = (pos1 + 1) & 0xff;\n pos2 = (pos2 + sbox[pos1]) & 0xff;\n\n const tmp = sbox[pos1];\n sbox[pos1] = sbox[pos2];\n sbox[pos2] = tmp;\n\n const j = (sbox[pos1] + sbox[pos2]) & 0xff;\n data[i] ^= sbox[j];\n }\n this.pos1 = pos1;\n this.pos2 = pos2;\n }\n}const _STUB_LEN = 433;\n\nclass Img {\n async load(url, key) {\n const opt = {\n referrerPolicy: 'no-referrer',\n };\n\n const res = await fetch(url, opt);\n const rsr = res.body.getReader();\n const reader = new MyReader(rsr);\n\n await reader.aReadBytes(_STUB_LEN);\n\n this._reader = reader;\n this._rsr = rsr;\n this._rc4 = new RC4(key);\n }\n\n async read() {\n let ret;\n let avail = this._reader.avail;\n if (avail > 0) {\n ret = {\n done: this._reader.eof,\n value: this._reader.readBytes(avail),\n };\n } else {\n ret = await this._rsr.read();\n }\n if (ret.value) {\n this._rc4.crypt(ret.value);\n }\n return ret;\n }\n\n cancel() {\n return this._reader.cancel();\n }\n}const __txtDec = new TextDecoder();\n\nclass MyReader {\n constructor(source) {\n this.avail = 0;\n this.eof = false;\n this._source = source;\n this._queue = [];\n this._offset = 0;\n }\n\n readUint32() {\n const buf = this.readBytes(4);\n const int = buf[3] << 24 | buf[2] << 16 | buf[1] << 8 | buf[0];\n return int >>> 0;\n }\n\n readUint16() {\n const buf = this.readBytes(2);\n return buf[1] << 8 | buf[0];\n }\n\n readTinyText() {\n const lenBuf = this.readBytes(1);\n const strBuf = this.readBytes(lenBuf[0]);\n return __txtDec.decode(strBuf);\n }\n\n readBytes(size, discard = true) {\n if (size > this.avail) {\n throw 'avail data not enough';\n }\n return this._readFromBuf(size, discard);\n }\n\n\n async aReadUint32() {\n const buf = await this.aReadBytes(4);\n const int = buf[3] << 24 | buf[2] << 16 | buf[1] << 8 | buf[0];\n return int >>> 0;\n }\n\n async aReadBytes(size, discard = true) {\n if (this.eof) {\n throw Error('EOF');\n }\n while (this.avail < size) {\n await this._load();\n if (this.eof) {\n break;\n }\n }\n return this.readBytes(size, discard);\n }\n\n // ...\n\n async _load() {\n const r = await this._source.read();\n if (r.done) {\n this.eof = true;\n return;\n }\n const chunk = r.value;\n this._queue.push(chunk);\n this.avail += chunk.length;\n }\n\n close() {\n this._source.cancel();\n }\n\n _readFromBuf(size, discard) {\n // first chunk\n let buf = this._queue[0];\n let len = buf.length;\n\n let beg = this._offset;\n let end = beg + size;\n\n // enough? (in most cases)\n if (end <= len) {\n if (discard) {\n this.avail -= size;\n this._offset = end;\n }\n return buf.subarray(beg, end);\n }\n\n // concat small chunks\n let dstBuf = new Uint8Array(size);\n let dstPos = 0;\n let i = 0;\n let stop;\n\n for (;;) {\n end = len;\n\n let srcBuf = buf.subarray(beg, end);\n dstBuf.set(srcBuf, dstPos);\n dstPos += (end - beg);\n\n if (stop) {\n break;\n }\n\n buf = this._queue[++i];\n len = buf.length;\n\n let remain = size - dstPos;\n if (len >= remain) {\n len = remain;\n stop = true;\n }\n beg = 0;\n }\n\n if (discard) {\n this._queue.splice(0, i); // unshift i counts\n this.avail -= size;\n this._offset = end;\n }\n return dstBuf;\n }\n}const HASH_SIZE = 10;\nconst BLK_SIZE = 1024 * 16;\n\nclass VFile {\n constructor(url, urlKeyLen, hash) {\n this._url = url;\n this._urlKeyLen = urlKeyLen;\n this._hash = hash;\n this._remain = 0;\n this._reader = null;\n this._partNum = 0;\n this._partUrlArr = [];\n this._partHashArr = [];\n this._part1BlkNum = 0;\n }\n\n async open() {\n const partUrlKeyArr = [];\n const partHashArr = [];\n\n try {\n const img = new Img();\n await img.load(this._url, this._hash);\n\n const reader = new MyReader(img);\n\n const headLen = await reader.aReadUint32();\n const headBuf = await reader.aReadBytes(headLen, false);\n\n if (!hashVerify(headBuf, this._hash)) {\n throw 'head hash incorrect';\n }\n\n const mime = reader.readTinyText();\n const size = reader.readUint32();\n const partNum = reader.readUint16();\n\n for (let i = 0; i < partNum; i++) {\n this._partUrlArr[i] = await reader.readBytes(this._urlKeyLen);\n }\n for (let i = 0; i < partNum; i++) {\n this._partHashArr[i] = await reader.readBytes(HASH_SIZE);\n }\n\n this._part1BlkNum = reader.readUint16();\n this._hash = reader.readBytes(HASH_SIZE);\n\n this._remain = size;\n this._reader = reader;\n this._partNum = partNum;\n\n return {size, mime};\n\n } catch (err) {\n console.warn('read head err:', err);\n return null;\n }\n }\n\n\n async pull() {\n const blkLen = Math.min(this._remain, BLK_SIZE);\n this._remain -= blkLen;\n\n const isLast = (this._remain === 0);\n const bufLen = blkLen + (isLast ? 0 : HASH_SIZE);\n //\n // if i < last: buf = blk[i] + Hash[i+1]\n // if i = last: buf = blk[i]\n //\n try {\n var buf = await this._reader.aReadBytes(bufLen);\n } catch (err) {\n }\n\n if (!buf || buf.length !== bufLen) {\n throw 'bad size';\n }\n\n // verify\n if (!hashVerify(buf, this._hash)) {\n throw 'block hash incorrect';\n }\n\n // next hash\n if (!isLast) {\n this._hash = buf.subarray(-HASH_SIZE);\n }\n\n // body output\n return {\n data: buf.subarray(0, BLK_SIZE),\n done: isLast,\n };\n }\n\n close() {\n if (this._reader) {\n this._reader.close();\n }\n }\n}const NODES_CONFIG = [\n// zhihu\n{\n URL_KEY_LEN: 16,\n WAY_NUM: 8,\n\n genUrl(id, item) {\n const keyHex = bytesToHex(item.url);\n return `pic${id+1}.zhimg.com/80/v2-${keyHex}.gif`;\n }\n},\n\n// sm.ms\n{\n URL_KEY_LEN: 8,\n WAY_NUM: 2,\n\n genUrl(id, item) {\n // +new Date('2018/03/13 GMT')\n const DAY_BASE = 1520899200000,\n\n domain = ['i.loli.net', 'ooo.0o0.ooo'][id],\n urlKey = item.url,\n\n dayNum = (urlKey[0] << 4) | (urlKey[1] >> 4),\n dayObj = new Date(DAY_BASE + dayNum * 86400000),\n dayStr = dayObj.toISOString().substr(0, 10).replace(/-/g, '/'),\n\n fileHex = bytesToHex(urlKey),\n fileStr = fileHex.substr(3);\n\n return `${domain}/${dayStr}/${fileStr}.gif`;\n }\n},\n];\n\n\nclass Node {\n constructor(config) {\n this.urlKeyLen = config.URL_KEY_LEN;\n this.wayNum = config.WAY_NUM;\n this._genUrl = config.genUrl;\n \n this._ready = true;\n this._priority = 0;\n\n // this._speed = 0;\n // this._delay = 0;\n this._pathMap = null;\n\n this._totalNum = 0;\n this._errorNum = 0;\n\n this._lastErrTick = 0;\n }\n\n async _loadMani(info) {\n this._ready = false;\n\n const url = this.genUrl(info);\n console.log('load manifest:', url);\n\n const hash = info.hash;\n\n const img = new Img();\n await img.load(url, hash);\n\n const reader = new MyReader(img);\n const nFile = await reader.aReadUint32();\n\n const size = nFile * (4 + this.urlKeyLen + HASH_SIZE);\n const bytes = await reader.aReadBytes(size, false);\n\n if (!hashVerify(bytes, hash)) {\n throw 'manifest hash incorrect';\n }\n const map = {};\n\n for (let i = 0; i < nFile; i++) {\n const pathHash = reader.readUint32();\n const urlKey = reader.readBytes(this.urlKeyLen);\n const fileHash = reader.readBytes(HASH_SIZE);\n\n map[pathHash] = {\n url: urlKey,\n hash: fileHash,\n };\n }\n\n this._pathMap = map;\n this._ready = true;\n }\n\n genUrl(info) {\n return 'https://' + this._genUrl(0, info);\n }\n\n getFileInfo(path) {\n return this._pathMap[strHash(path)];\n }\n\n getPathInfo(path) {\n // TODO: custom rewrite\n let ret;\n if (/^$|\\/$/.test(path)) {\n ret =\n this.getFileInfo(path + 'index.html');\n } else {\n ret =\n this.getFileInfo(path) ||\n this.getFileInfo(path + '.html') ||\n this.getFileInfo(path + '/index.html');\n }\n return ret;\n }\n\n use() {\n this._totalNum++;\n }\n\n error() {\n this._errorNum++;\n this._lastErrTick = Date.now();\n }\n\n static choose() {\n const now = Date.now();\n\n for (let i = 0; i < nodeList.length; i++) {\n const node = nodeList[i];\n if (!node._ready) {\n continue;\n }\n if (now - node._lastErrTick < 1000 * 60) {\n continue;\n }\n return node;\n }\n return null;\n }\n\n // async\n static loadManiList(listBuf, count) {\n return new Promise(y => {\n let fails = 0;\n let pos = 0;\n let count = 0;\n\n do {\n const node = nodeList[count++];\n const urlKeyLen = node.urlKeyLen;\n\n const urlKeyBuf = listBuf.subarray(pos, pos + urlKeyLen);\n pos += urlKeyLen;\n\n const hashBuf = listBuf.subarray(pos, pos + HASH_SIZE);\n pos += HASH_SIZE;\n\n const info = {\n url: urlKeyBuf, \n hash: hashBuf\n };\n\n node._loadMani(info)\n .then(y)\n .catch(err => {\n console.warn('load manifest err:', err);\n node.error();\n\n if (++fails === count) {\n console.log('no available node!');\n y();\n }\n });\n } while (pos < listBuf.length);\n });\n }\n}\n\nconst nodeList = NODES_CONFIG.map(i => new Node(i));class Task {\n constructor(path) {\n this.done = false;\n this.timeout = false;\n\n this._path = path;\n this._node = null;\n this._file = null;\n this._rxMax = 0;\n this._rxPos = 0;\n }\n\n async readHead() {\n let status = 200;\n\n for (;;) {\n const node = Node.choose();\n if (!node) {\n return null;\n }\n\n let info = node.getPathInfo(this._path);\n if (!info) {\n status = 404;\n info = node.getFileInfo('404.html');\n if (!info) {\n return null;\n }\n }\n\n const url = node.genUrl(info);\n const file = new VFile(url, node.urlKeyLen, info.hash);\n\n // node.use();\n\n const head = await file.open();\n if (head) {\n this._node = node;\n this._file = file;\n return {status, head};\n }\n\n node.error();\n file.close();\n }\n }\n\n async readBody() {\n console.assert(!this.done);\n\n for (;;) {\n let ret = await this._file.pull();\n if (!ret) {\n this._file.close();\n this._node.error();\n\n // choose a new node\n const head = await this.readHead();\n if (!head) {\n console.warn('node switch fail');\n this.done = true;\n return null;\n }\n\n // reset progress\n this._rxPos = 0;\n continue;\n }\n\n const {data, done} = ret;\n\n this._rxPos += data.length;\n if (this._rxPos <= this._rxMax) {\n // skip the data that has been downloaded\n continue;\n }\n this._rxMax = this._rxPos;\n\n if (done) {\n this.done = true;\n }\n return data;\n }\n }\n\n stop() {\n this._file.close();\n }\n}\n\nconst DEFAULT_HEADERS = new Headers({\n 'x-xss-protection': '1',\n 'content-type': 'text/html',\n});\n\nfunction makeErrRes(code, text) {\n const html = `