Subresource Integrity exposed to plugins from Service Worker

merge-requests/6/merge
Michał "rysiek" Woźniak 2021-11-08 20:51:27 +00:00
rodzic 0d5db7e2a0
commit 4f528a4123
11 zmienionych plików z 345 dodań i 31 usunięć

Wyświetl plik

@ -134,6 +134,62 @@ describe("plugin: alt-fetch", () => {
expect(response.url).toEqual('https://resilient.is/test.json')
})
test("it should pass the Request() init data to fetch() for all used endpoints", async () => {
init = {
name: 'alt-fetch',
endpoints: [
'https://alt.resilient.is/test.json',
'https://error.resilientis/test.json',
'https://timeout.resilientis/test.json',
'https://alt2.resilient.is/test.json',
'https://alt3.resilient.is/test.json',
'https://alt4.resilient.is/test.json'
]}
require("../../plugins/alt-fetch.js");
global.fetch.mockImplementation((url, init) => {
const response = new Response(
new Blob(
[JSON.stringify({ test: "success" })],
{type: "application/json"}
),
{
status: 200,
statusText: "OK",
headers: {
'ETag': 'TestingETagHeader'
},
url: url
});
return Promise.resolve(response);
});
var initTest = {
method: "GET",
headers: new Headers({"x-stub": "STUB"}),
mode: "mode-stub",
credentials: "credentials-stub",
cache: "cache-stub",
referrer: "referrer-stub",
// these are not implemented by service-worker-mock
// https://github.com/zackargyle/service-workers/blob/master/packages/service-worker-mock/models/Request.js#L20
redirect: undefined,
integrity: undefined,
cache: undefined
}
const response = await LibResilientPluginConstructors.get('alt-fetch')(LR, init).fetch('https://resilient.is/test.json', initTest);
expect(fetch).toHaveBeenCalledTimes(3);
expect(fetch).toHaveBeenNthCalledWith(1, expect.stringContaining('/test.json'), initTest);
expect(fetch).toHaveBeenNthCalledWith(2, expect.stringContaining('/test.json'), initTest);
expect(fetch).toHaveBeenNthCalledWith(3, expect.stringContaining('/test.json'), initTest);
expect(await response.json()).toEqual({test: "success"})
expect(response.url).toEqual('https://resilient.is/test.json')
})
test("it should set the LibResilient headers", async () => {
require("../../plugins/alt-fetch.js");

Wyświetl plik

@ -93,6 +93,31 @@ describe("plugin: any-of", () => {
expect(response.url).toEqual('https://resilient.is/test.json')
});
test("it should return data from fetch()", async () => {
require("../../plugins/any-of.js");
var initTest = {
method: "GET",
headers: new Headers({"x-stub": "STUB"}),
mode: "mode-stub",
credentials: "credentials-stub",
cache: "cache-stub",
referrer: "referrer-stub",
// these are not implemented by service-worker-mock
// https://github.com/zackargyle/service-workers/blob/master/packages/service-worker-mock/models/Request.js#L20
redirect: undefined,
integrity: undefined,
cache: undefined
}
const response = await LibResilientPluginConstructors.get('any-of')(LR, init).fetch('https://resilient.is/test.json', initTest);
expect(fetch).toHaveBeenCalled();
expect(fetch).toHaveBeenCalledWith('https://resilient.is/test.json', initTest);
expect(await response.json()).toEqual({test: "success"})
expect(response.url).toEqual('https://resilient.is/test.json')
});
test("it should throw an error when HTTP status is >= 400", async () => {
global.fetch.mockImplementation((url, init) => {

Wyświetl plik

@ -3,25 +3,25 @@ const makeServiceWorkerEnv = require('service-worker-mock');
global.fetch = require('node-fetch');
jest.mock('node-fetch')
global.fetch.mockImplementation((url, init) => {
const response = new Response(
new Blob(
[JSON.stringify({ test: "success" })],
{type: "application/json"}
),
{
status: 200,
statusText: "OK",
headers: {
'ETag': 'TestingETagHeader'
},
url: url
});
return Promise.resolve(response);
});
describe("plugin: fetch", () => {
beforeEach(() => {
global.fetch.mockImplementation((url, init) => {
const response = new Response(
new Blob(
[JSON.stringify({ test: "success" })],
{type: "application/json"}
),
{
status: 200,
statusText: "OK",
headers: {
'ETag': 'TestingETagHeader'
},
url: url
});
return Promise.resolve(response);
});
Object.assign(global, makeServiceWorkerEnv());
jest.resetModules();
global.LibResilientPluginConstructors = new Map()
@ -31,6 +31,7 @@ describe("plugin: fetch", () => {
}
}
})
test("it should register in LibResilientPluginConstructors", () => {
require("../../plugins/fetch.js");
expect(LibResilientPluginConstructors.get('fetch')().name).toEqual('fetch');
@ -46,6 +47,30 @@ describe("plugin: fetch", () => {
expect(response.url).toEqual('https://resilient.is/test.json')
});
test("it should pass the Request() init data to fetch()", async () => {
require("../../plugins/fetch.js");
var initTest = {
method: "GET",
headers: new Headers({"x-stub": "STUB"}),
mode: "mode-stub",
credentials: "credentials-stub",
cache: "cache-stub",
referrer: "referrer-stub",
// these are not implemented by service-worker-mock
// https://github.com/zackargyle/service-workers/blob/master/packages/service-worker-mock/models/Request.js#L20
redirect: undefined,
integrity: undefined,
cache: undefined
}
const response = await LibResilientPluginConstructors.get('fetch')(LR).fetch('https://resilient.is/test.json', initTest);
expect(fetch).toHaveBeenCalledWith('https://resilient.is/test.json', initTest);
expect(await response.json()).toEqual({test: "success"})
expect(response.url).toEqual('https://resilient.is/test.json')
});
test("it should set the LibResilient headers", async () => {
require("../../plugins/fetch.js");

Wyświetl plik

@ -169,6 +169,76 @@ describe("service-worker", () => {
expect(await response.json()).toEqual({ test: "success" })
});
test("plugins should receive the Request() init data", async () => {
self.LibResilientConfig = {
plugins: [{
name: 'reject-all'
},{
name: 'resolve-all'
}],
loggedComponents: [
'service-worker'
]
}
let rejectingFetch = jest.fn((request, init)=>{ return Promise.reject(request); })
let resolvingFetch = jest.fn((request, init)=>{
return Promise.resolve(
new Response(
new Blob(
[JSON.stringify({ test: "success" })],
{type: "application/json"}
),
{
status: 200,
statusText: "OK",
headers: {
'ETag': 'TestingETagHeader'
},
url: self.location.origin + '/test.json'
})
)
})
global.LibResilientPluginConstructors.set('reject-all', ()=>{
return {
name: 'reject-all',
description: 'Reject all requests.',
version: '0.0.1',
fetch: rejectingFetch
}
})
global.LibResilientPluginConstructors.set('resolve-all', ()=>{
return {
name: 'resolve-all',
description: 'Resolve all requests.',
version: '0.0.1',
fetch: resolvingFetch
}
})
var initTest = {
method: "GET",
headers: new Headers({"x-stub": "STUB"}),
mode: "mode-stub",
credentials: "credentials-stub",
cache: "cache-stub",
referrer: "referrer-stub",
// these are not implemented by service-worker-mock
// https://github.com/zackargyle/service-workers/blob/master/packages/service-worker-mock/models/Request.js#L20
redirect: undefined,
integrity: undefined,
cache: undefined
}
require("../service-worker.js");
var response = await self.trigger('fetch', new Request('/test.json', initTest))
expect(rejectingFetch).toHaveBeenCalled();
expect(resolvingFetch).toHaveBeenCalled();
expect(await response.json()).toEqual({ test: "success" })
expect(rejectingFetch).toHaveBeenCalledWith('https://www.test.com/test.json', initTest)
expect(resolvingFetch).toHaveBeenCalledWith('https://www.test.com/test.json', initTest)
});
test("defaultPluginTimeout should be respected", async () => {
jest.useFakeTimers()
self.LibResilientConfig = {
@ -575,6 +645,110 @@ describe("service-worker", () => {
})).toEqual({ test: "success" })
});
test("after a retrieval from a stashing plugin, background plugin should receive the Request() init data", async () => {
self.LibResilientConfig = {
plugins: [{
name: 'stashing-test'
},{
name: 'resolve-all'
}],
loggedComponents: [
'service-worker'
]
}
let resolvingFetch = jest.fn((request, init)=>{
return Promise.resolve(
new Response(
new Blob(
[JSON.stringify({ test: "success" })],
{type: "application/json"}
),
{
status: 200,
statusText: "OK",
headers: {
'X-LibResilient-Method': 'resolve-all',
'X-LibResilient-ETag': 'TestingETagHeader'
},
url: self.location.origin + '/test.json'
})
)
})
let resolvingFetch2 = jest.fn((request, init)=>{
return Promise.resolve(
new Response(
new Blob(
[JSON.stringify({ test: "success2" })],
{type: "application/json"}
),
{
status: 200,
statusText: "OK",
headers: {
'ETag': 'NewTestingETagHeader'
},
url: self.location.origin + '/test.json'
})
)
})
let stashingStash = jest.fn(async (response, url)=>{
expect(await response.json()).toEqual({ test: "success2" })
expect(response.headers.get('ETag')).toEqual('NewTestingETagHeader')
})
global.LibResilientPluginConstructors.set('stashing-test', ()=>{
return {
name: 'stashing-test',
description: 'Mock stashing plugin.',
version: '0.0.1',
fetch: resolvingFetch,
stash: stashingStash
}
})
global.LibResilientPluginConstructors.set('resolve-all', ()=>{
return {
name: 'resolve-all',
description: 'Resolve all requests.',
version: '0.0.1',
fetch: resolvingFetch2
}
})
var testClient = new Client()
self.clients.clients.push(testClient)
var fetchedDiffersFound = false
testClient.addEventListener('message', event => {
if (event.data.fetchedDiffers) {
fetchedDiffersFound = true
}
})
require("../service-worker.js");
var initTest = {
method: "GET",
headers: new Headers({"x-stub": "STUB"}),
mode: "mode-stub",
credentials: "credentials-stub",
cache: "cache-stub",
referrer: "referrer-stub",
// these are not implemented by service-worker-mock
// https://github.com/zackargyle/service-workers/blob/master/packages/service-worker-mock/models/Request.js#L20
redirect: undefined,
integrity: undefined,
cache: undefined
}
var response = await self.trigger('fetch', {
request: new Request('/test.json', initTest),
clientId: testClient.id
})
expect(resolvingFetch).toHaveBeenCalled();
expect(await response.json()).toEqual({ test: "success" })
expect(resolvingFetch).toHaveBeenCalledWith('https://www.test.com/test.json', initTest);
expect(resolvingFetch2).toHaveBeenCalledWith('https://www.test.com/test.json', initTest);
});
test("unstashing content explicitly should work", async () => {
self.LibResilientConfig = {
plugins: [{

Wyświetl plik

@ -62,12 +62,16 @@
/**
* getting content using regular HTTP(S) fetch()
*/
let fetchContentFromAlternativeEndpoints = (url) => {
let fetchContentFromAlternativeEndpoints = (url, init={}) => {
// we're going to try a random endpoint and building an URL of the form:
// https://<endpoint_address>/<pubkey>/<rest_of_URL>
var path = url.replace(/https?:\/\/[^/]+\//, '')
// we really want to make fetch happen, Regina!
// TODO: this change should *probably* be handled on the Service Worker level
init.cache = 'reload'
// we don't want to modify the original endpoints array
var sourceEndpoints = [...config.endpoints]
@ -91,7 +95,7 @@
return Promise.any(
useEndpoints.map(
u=>fetch(u, {cache: "reload"})
u=>fetch(u, init)
))
.then((response) => {
// 4xx? 5xx? that's a paddlin'

Wyświetl plik

@ -32,10 +32,10 @@
/**
* getting content using regular HTTP(S) fetch()
*/
let fetchContent = (url) => {
let fetchContent = (url, init={}) => {
LR.log(pluginName, `using: [${config.uses.map(p=>p.name).join(', ')}]!`)
return Promise.any(
config.uses.map(p=>p.fetch(url))
config.uses.map(p=>p.fetch(url, init))
)
}

Wyświetl plik

@ -21,7 +21,7 @@
/**
* getting content from cache
*/
let getContentFromCache = (url) => {
let getContentFromCache = (url, init={}) => {
LR.log(pluginName, `getting from cache: ${url}`)
return caches.open('v1')
.then((cache) => {

Wyświetl plik

@ -24,9 +24,13 @@
/**
* getting content using regular HTTP(S) fetch()
*/
let fetchContent = (url) => {
let fetchContent = (url, init={}) => {
LR.log(pluginName, `regular fetch: ${url}`)
return fetch(url, {cache: "reload"})
// we really want to make fetch happen, Regina!
// TODO: this change should *probably* be handled on the Service Worker level
init.cache = 'reload'
// run built-in regular fetch()
return fetch(url, init)
.then((response) => {
// 4xx? 5xx? that's a paddlin'
if (response.status >= 400) {

Wyświetl plik

@ -165,7 +165,7 @@ if (typeof window === 'undefined') {
/**
* the workhorse of this plugin
*/
async function getContentFromGunAndIPFS(url) {
async function getContentFromGunAndIPFS(url, init={}) {
await setup_gun_ipfs();

Wyświetl plik

@ -96,7 +96,7 @@
/**
* the workhorse of this plugin
*/
async function getContentFromIPNSAndIPFS(url) {
async function getContentFromIPNSAndIPFS(url, init={}) {
return new Error("Not implemented yet.")
var urlArray = url.replace(/https?:\/\//, '').split('/')

Wyświetl plik

@ -398,15 +398,35 @@ let LibResilientResourceInfo = class {
|* === Main Brain of LibResilient === *|
\* ========================================================================= */
/**
* generate Request()-compatible init object from an existing Request
*
* req - the request to work off of
*/
let initFromRequest = (req) => {
return {
method: req.method,
headers: req.headers,
mode: req.mode,
credentials: req.credentials,
cache: req.cache,
redirect: req.redirect,
referrer: req.referrer,
integrity: req.integrity
}
}
/**
* run a plugin's fetch() method
* while handling all the auxiliary stuff like saving info in reqInfo
*
* plugin - the plugin to use
* url - string containing the URL to fetch
* init - Request() initialization parameters
* reqInfo - instance of LibResilientResourceInfo
*/
let libresilientFetch = (plugin, url, reqInfo) => {
let libresilientFetch = (plugin, url, init, reqInfo) => {
// status of the plugin
reqInfo.update({
@ -416,11 +436,13 @@ let libresilientFetch = (plugin, url, reqInfo) => {
// log stuff
self.log('service-worker', "LibResilient Service Worker handling URL:", url,
'\n+-- using method(s):', plugin.name)
'\n+-- init:', Object.getOwnPropertyNames(init).map(p=>`\n - ${p}: ${init[p]}`).join(''),
'\n+-- using method(s):', plugin.name
)
// race the plugin(s) vs. a timeout
return Promise.race([
plugin.fetch(url),
plugin.fetch(url, init),
promiseTimeout(
self.LibResilientConfig.defaultPluginTimeout,
false,
@ -464,6 +486,9 @@ let getResourceThroughLibResilient = (request, clientId, useStashed=true, doStas
// clean the URL, removing any fragment identifier
var url = request.url.replace(/#.+$/, '');
// get the init object from Request
var init = initFromRequest(request)
// set-up reqInfo for the fetch event
var reqInfo = new LibResilientResourceInfo(url, clientId)
@ -503,14 +528,14 @@ let getResourceThroughLibResilient = (request, clientId, useStashed=true, doStas
state: "error",
fetchError: error.toString()
})
return libresilientFetch(currentPlugin, url, reqInfo)
return libresilientFetch(currentPlugin, url, init, reqInfo)
})
},
// this libresilientFetch() will run first
// all other promises generated by LibResilientPlugins[] will be chained on it
// using the catch() in reduce() above
// skipping this very first plugin by way of slice(1)
libresilientFetch(LibResilientPluginsRun[0], url, reqInfo)
libresilientFetch(LibResilientPluginsRun[0], url, init, reqInfo)
)
.then((response)=>{
// we got a successful response
@ -532,6 +557,7 @@ let getResourceThroughLibResilient = (request, clientId, useStashed=true, doStas
self.log('service-worker', 'starting background no-stashed fetch for:', url);
// event.waitUntil?
// https://stackoverflow.com/questions/37902441/what-does-event-waituntil-do-in-service-worker-and-why-is-it-needed/37906330#37906330
// TODO: perhaps don't use the `request` again? some wrapper?
getResourceThroughLibResilient(request, clientId, false, true, response.clone()).catch((e)=>{
self.log('service-worker', 'background no-stashed fetch failed for:', url);
})