Skip to content

Instantly share code, notes, and snippets.

@ryancdotorg
Last active October 14, 2020 16:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ryancdotorg/49a43e63d349d3a250a5a9f6a7618fa6 to your computer and use it in GitHub Desktop.
Save ryancdotorg/49a43e63d349d3a250a5a9f6a7618fa6 to your computer and use it in GitHub Desktop.
Self-extracting Service Worker Cache PoC code. The resulting file *MUST* be served with content-type: application/javascript; charset=x-user-defined
var ArchiveReader = function(ab) {
if (!(this instanceof ArchiveReader)) {
throw new Error('ArchiveReader must be instantiated with `new`!');
}
this.ab = ab;
this.dv = new DataView(ab);
this.offset = 0;
this.td = typeof TextDecoder === 'function' ? new TextDecoder() : null;
}
ArchiveReader.prototype = {
getUint8: function() {
var t = this.dv.getUint8(this.offset);
this.offset += 1;
return t;
},
getUint16: function() {
var t = this.dv.getUint16(this.offset);
this.offset += 2;
return t;
},
getUint32: function() {
var t = this.dv.getUint32(this.offset);
this.offset += 4;
return t;
},
getBytes: function(len) {
var t = new Uint8Array(this.ab, this.offset, len);
this.offset += len;
return t;
},
getString: function(len) {
if (len > 255 && this.td) {
return this.td.decode(this.getBytes(len));
} else {
for (var str = '', i = 0, n, c; i < len; ++i) {
// sloppy utf-8 decode
c = this.getUint8();
if (c & 0x80) {
if (c < 0xe0) { c &= 0x1f; n = 1; }
else if (c < 0xf0) { c &= 0x0f; n = 2; }
else { c &= 0x03; n = 3; }
while (n) { c = (c << 6) | (this.getUint8() & 0x3f); ++i; --n; }
}
if (c < 0x10000) {
str += String.fromCharCode(c);
} else {
c -= 0x10000;
str += String.fromCharCode(0xd800 | (c >> 10), 0xdc00 | (c & 0x3ff));
}
}
return str;
}
},
getItemMetadata: function() {
var ib = this.getUint8(), mt = ib >> 5, ai = ib & 0x1f, len;
if (ai < 24) {
len = ai;
} else {
switch (ai) {
case 24:
len = this.getUint8(); break;
case 25:
len = this.getUint16(); break;
case 26:
len = this.getUint32(); break;
default:
throw new Error('cannot parse metadata');
}
}
return {'majorType':mt,'length':len};
}
};
var fileSet = function() {
if (self.cacheId) {
self.cachedFiles = new Set();
caches.open(self.cacheId).then(function(cache) {
cache.keys().then(function(files) {
files.forEach(function(req) { self.cachedFiles.add(req.url); });
});
});
}
};
var fileAdd = function(name) {
};
var loadArchive = function() {
var type = {
'js': 'application/javascript; charset=utf-8',
'css': 'text/css; charset=utf-8',
'html': 'text/html; charset=utf-8',
'jpg': 'image/jpeg', 'jpeg': 'image/jpeg',
'png': 'image/png', 'gif': 'image/gif'
};
var defaultHeaders = {
'X-Frame-Options': 'SAMEORIGIN',
'X-Content-Type-Options': 'nosniff'
}
var data = SWAR();
return self.crypto.subtle.digest({name:'SHA-256'},data).then(function(hash) {
var id = [].slice.call(new Uint8Array(hash)).map(c => ('0'+c.toString(16)).substr(-2)).join('');
console.log('cache id ' + id);
return caches.open(id).then(function(cache) {
var ar = new ArchiveReader(data);
var meta = ar.getItemMetadata();
if (meta.majorType === 5) {
self.cachedFiles = new Set();
for (var i = meta.length; i > 0; --i) {
meta = ar.getItemMetadata();
var arName = ar.getString(meta.length);
meta = ar.getItemMetadata();
var arData = ar.getBytes(meta.length);
console.log('adding ' + arName + ' (' + arData.byteLength + ' bytes) to cache...');
var arHeaders = new Headers(Object.assign(
{'Content-Type': type[arName.substr(-1) === '/' ? 'html' : arName.split('.').pop()] || 'application/octet-stream'},
defaultHeaders
));
cache.put(self.origin + arName, new Response(arData, {headers: arHeaders}));
self.cachedFiles.add(self.origin + arName);
}
return Promise.resolve(id);
} else {
return Promise.reject(new Error('Parsing cache object failed!'));
}
});
});
};
self.addEventListener('install', function(event) {
console.log('Installing service worker...');
self.skipWaiting();
event.waitUntil(loadArchive().then(function(id) {
self.cacheId = id;
return caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(n => n != self.cacheId ? caches.delete(n) : void(0))
);
});
}));
});
self.addEventListener('activate', function(event) {
console.log('activate ' + self.cacheId);
event.waitUntil(self.clients.claim());
});
self.addEventListener('fetch', function(event) {
var url = event.request.url.replace(/\/[0-9a-f]{16}\/([^/]+)$/, '/$1');
if (!self.cachedFiles) {
fileSet();
} else if (self.cachedFiles.has(url)) {
event.respondWith(caches.match(event.request).then(function(response) {
if (response) {
return response;
} else {
cachedFiles.delete(url);
return response || fetch(event.request);
}
}));
}
});
// navigator.serviceWorker.register('cache.swar', {scope:'/'});
#!/usr/bin/env python3
import io, re, os, sys
from struct import pack
from hashlib import sha256
if len(sys.argv) < 4:
print('USAGE: swar.py OUTPUT WORKER FILE1 [FILE2 ... FILEN]');
sys.exit(1);
outputFile = sys.argv[1]
workerFile = sys.argv[2]
inputFiles = sys.argv[3:]
class CBORWriter:
def __init__(self, f):
self.f = f
# type 0: unsigned integer
# type 1: negative integer
# type 2: byte string
# type 3: utf-8 string
# type 4: array of items
# type 5: map of item pairs
# type 6: tagged type
# type 7: floats, simple, break
def writeTL(self, t, l):
if l < 24:
self.write(pack('>B', t << 5 | l))
elif l <= 0xff:
self.write(pack('>BB', t << 5 | 24, l))
elif l <= 0xffff:
self.write(pack('>BH', t << 5 | 25, l))
elif l <= 0xffffffff:
self.write(pack('>BL', t << 5 | 26, l))
elif l <= 0xffffffffffffffff:
self.write(pack('>BQ', t << 5 | 27, l))
def writeBytes(self, b):
self.writeTL(2, len(b))
self.write(b)
def writeString(self, s):
b = s.encode('utf-8')
self.writeTL(3, len(b))
self.write(b)
def write(self, b):
self.f.write(b)
def close(self):
self.f.close()
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.close()
TEMPLATE_HEADER = b'const SWAR=_=>{for(var r=((_=>{/*'
TEMPLATE_FOOTER = b'*/})+""),r=r.substring(r.indexOf("/*")+2,r.lastIndexOf('+\
b'"*/")).replace(/\*#[#/]/gm,r=>"*"+r[2]),e=0,a=r.length,'+\
b'n=new Uint8Array(a);e<a;++e)n[e]=r.charCodeAt(e);return'+\
b' n.buffer};\n\n'
def mapName(arg):
arName, _, fsName = arg.partition('=')
fsName = fsName if fsName else arName
arName = re.sub(r'^[./]*/', '/', arName)
print(arName)
return (arName, fsName)
with io.BytesIO() as blob:
with CBORWriter(blob) as cbor:
cbor.writeTL(5, len(inputFiles))
for arName, fsName in map(mapName, inputFiles):
cbor.writeString(arName)
with open(fsName, 'rb') as f:
cbor.writeTL(2, os.stat(f.fileno()).st_size)
cbor.write(f.read())
blob.seek(0)
with io.BytesIO() as swar:
swar.write(TEMPLATE_HEADER);
swar.write(re.sub(b'[*]([/#])', b'*#\\1', blob.read()))
swar.write(TEMPLATE_FOOTER);
swar.write(open(workerFile, 'rb').read())
swar.seek(0)
data = swar.read()
with open(outputFile, 'wb') as f:
f.write(data)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment