diff --git a/config.js b/config.js new file mode 100644 index 0000000000000000000000000000000000000000..aa05fbec83bdeea8478d583367fb53a16cd73932 --- /dev/null +++ b/config.js @@ -0,0 +1,22 @@ +/* + * Samizdat config + * + * This is the config for Samizdat as deployed on the https://samizdat.is/ site + * + * When deploying Samizdat on your website you will need to create your own config, + * using this one as a template. + */ + +// plugins config +self.SamizdatConfig.plugins["gateway-ipns"] = { + // the pubkey of the preconfigured IPNS node + ipnsPubkey: 'QmYGVgGGfD5N4Xcc78CcMJ99dKcH6K6myhd4Uenv5yJwiJ' +} +self.SamizdatConfig.plugins["gun+ipfs"] = { + // the pubkey of the preconfigured Gun user + gunPubkey: 'WUK5ylwqqgUorceQRa84qfbBFhk7eNRDUoPbGK05SyE.-yohFhTzWPpDT-UDMuKGgemOUrw_cMMYWpy6plILqrg' +} +self.SamizdatConfig.plugins["ipns+ipfs"] = { + // the pubkey of the preconfigured Gun user + ipnsPubkey: 'QmYGVgGGfD5N4Xcc78CcMJ99dKcH6K6myhd4Uenv5yJwiJ' +} diff --git a/plugins/cache.js b/plugins/cache.js index dc70dc0c83c41cc75de87f55f3804e3c18dd63f4..9975dce11cf0543e07c9edb42ee00f37c56d12ef 100644 --- a/plugins/cache.js +++ b/plugins/cache.js @@ -2,129 +2,139 @@ |* === Stashing plugin using the Cache API === *| \* ========================================================================= */ -/** - * getting content from cache - */ -let getContentFromCache = (url) => { - console.log('Samizdat: getting from cache!') - return caches.open('v1') - .then((cache) => { - return cache.match(url) - }) - .then((response) => { - if (typeof response === 'undefined') { - throw new Error('Resource not found in cache'); - } else { - response.headers.forEach(function(v, k){ - console.log('+-- Retrieved cached header: ', k, ' :: ', v) - }); - // return the response - return response - } - }) -} +// no polluting of the global namespace please +(function () { -/** - * add resources to cache - * - * implements the stash() Samizdat plugin method - * - * accepts either a Response - * or a string containing a URL - * or an Array of string URLs - */ -let cacheContent = (resource, key) => { - return caches.open('v1') - .then((cache) => { - if (typeof resource === 'string') { - // assume URL - console.log("(COMMIT_UNKNOWN) caching an URL") - return cache.add(resource) - } else if (Array.isArray(resource)) { - // assume array of URLs - console.log("(COMMIT_UNKNOWN) caching an Array of URLs") - return cache.addAll(resource) - } else { - // assume a Response - // which means we either have a Request in key, a string URL in key, - // or we can use the URL in resource.url - if ( (typeof key !== 'object') && ( (typeof key !== 'string') || (key === '') ) ) { - if (typeof resource.url !== 'string' || resource.url === '') { - throw new Error('No URL to work with!') + /* + * this plugin has no config settings + */ + + /** + * getting content from cache + */ + let getContentFromCache = (url) => { + console.log('Samizdat: getting from cache!') + return caches.open('v1') + .then((cache) => { + return cache.match(url) + }) + .then((response) => { + if (typeof response === 'undefined') { + throw new Error('Resource not found in cache'); + } else { + response.headers.forEach(function(v, k){ + console.log('+-- Retrieved cached header: ', k, ' :: ', v) + }); + // return the response + return response + } + }) + } + + /** + * add resources to cache + * + * implements the stash() Samizdat plugin method + * + * accepts either a Response + * or a string containing a URL + * or an Array of string URLs + */ + let cacheContent = (resource, key) => { + return caches.open('v1') + .then((cache) => { + if (typeof resource === 'string') { + // assume URL + console.log("(COMMIT_UNKNOWN) caching an URL") + return cache.add(resource) + } else if (Array.isArray(resource)) { + // assume array of URLs + console.log("(COMMIT_UNKNOWN) caching an Array of URLs") + return cache.addAll(resource) + } else { + // assume a Response + // which means we either have a Request in key, a string URL in key, + // or we can use the URL in resource.url + if ( (typeof key !== 'object') && ( (typeof key !== 'string') || (key === '') ) ) { + if (typeof resource.url !== 'string' || resource.url === '') { + throw new Error('No URL to work with!') + } + key = resource.url } - key = resource.url + + // we need to create a new Response object + // with all the headers added explicitly + // otherwise the x-samizdat-* headers get ignored + var init = { + status: resource.status, + statusText: resource.statusText, + headers: {} + }; + resource.headers.forEach(function(val, header){ + init.headers[header] = val; + }); + return resource + .blob() + .then((blob) => { + console.log("(COMMIT_UNKNOWN) caching a Response to: " + key) + return cache.put(key, new Response( + blob, + init + )) + }) } - - // we need to create a new Response object - // with all the headers added explicitly - // otherwise the x-samizdat-* headers get ignored - var init = { - status: resource.status, - statusText: resource.statusText, - headers: {} - }; - resource.headers.forEach(function(val, header){ - init.headers[header] = val; - }); - return resource - .blob() - .then((blob) => { - console.log("(COMMIT_UNKNOWN) caching a Response to: " + key) - return cache.put(key, new Response( - blob, - init - )) + }) + } + + /** + * remove resources from cache + * + * implements the unstash() Samizdat plugin method + * + * accepts either a Response + * or a string containing a URL + * or an Array of string URLs + */ + let clearCachedContent = (resource) => { + return caches.open('v1') + .then((cache) => { + if (typeof resource === 'string') { + // assume URL + console.log("(COMMIT_UNKNOWN) deleting a cached URL") + return cache.delete(resource) + } else if (Array.isArray(resource)) { + // assume array of URLs + console.log("(COMMIT_UNKNOWN) deleting an Array of cached URLs") + return Promise.all( + resource.map((res)=>{ + return cache.delete(res) }) - } - }) -} + ) + } else { + // assume a Response + // which means we have an URL in resource.url + console.log("(COMMIT_UNKNOWN) removing a Response from cache: " + resource.url) + return cache.delete(resource.url) + } + }) + } -/** - * remove resources from cache - * - * implements the unstash() Samizdat plugin method - * - * accepts either a Response - * or a string containing a URL - * or an Array of string URLs - */ -let clearCachedContent = (resource) => { - return caches.open('v1') - .then((cache) => { - if (typeof resource === 'string') { - // assume URL - console.log("(COMMIT_UNKNOWN) deleting a cached URL") - return cache.delete(resource) - } else if (Array.isArray(resource)) { - // assume array of URLs - console.log("(COMMIT_UNKNOWN) deleting an Array of cached URLs") - return Promise.all( - resource.map((res)=>{ - return cache.delete(res) - }) - ) - } else { - // assume a Response - // which means we have an URL in resource.url - console.log("(COMMIT_UNKNOWN) removing a Response from cache: " + resource.url) - return cache.delete(resource.url) - } - }) -} + + // initialize the SamizdatPlugins array + if (!Array.isArray(self.SamizdatPlugins)) { + self.SamizdatPlugins = new Array() + } - -// initialize the SamizdatPlugins array -if (!Array.isArray(self.SamizdatPlugins)) { - self.SamizdatPlugins = new Array() -} + // and add ourselves to it + // with some additional metadata + self.SamizdatPlugins.push({ + name: 'cache', + description: 'Locally cached responses, using the Cache API.', + version: 'COMMIT_UNKNOWN', + fetch: getContentFromCache, + stash: cacheContent, + unstash: clearCachedContent + }) -// and add ourselves to it -// with some additional metadata -self.SamizdatPlugins.push({ - name: 'cache', - description: 'Locally cached responses, using the Cache API.', - version: 'COMMIT_UNKNOWN', - fetch: getContentFromCache, - stash: cacheContent, - unstash: clearCachedContent -}) +// done with not poluting the global namespace +})() diff --git a/plugins/fetch.js b/plugins/fetch.js index 79212157ad8394ddf53271db16767895c7ce80e2..ac4c1946eda5aef2b05900b9573d6ae0735da312 100644 --- a/plugins/fetch.js +++ b/plugins/fetch.js @@ -6,59 +6,69 @@ * this plugin does not implement any push method */ -/** - * getting content using regular HTTP(S) fetch() - */ -let fetchContent = (url) => { - console.log('Samizdat: regular fetch!') - return fetch(url, {cache: "reload"}) - .then((response) => { - // 4xx? 5xx? that's a paddlin' - if (response.status >= 400) { - // throw an Error to fall back to Samizdat: - throw new Error('HTTP Error: ' + response.status + ' ' + response.statusText); - } - // all good, it seems - console.log("(COMMIT_UNKNOWN) Fetched:", response.url); - - // we need to create a new Response object - // with all the headers added explicitly, - // since response.headers is immutable - var init = { - status: response.status, - statusText: response.statusText, - headers: {} - }; - response.headers.forEach(function(val, header){ - init.headers[header] = val; - }); - - // add the X-Samizdat-* headers to the mix - init.headers['X-Samizdat-Method'] = 'fetch' - init.headers['X-Samizdat-ETag'] = response.headers.get('ETag') - - // return the new response, using the Blob from the original one - return response - .blob() - .then((blob) => { - return new Response( - blob, - init - ) - }) - }) -} - -// initialize the SamizdatPlugins array -if (!Array.isArray(self.SamizdatPlugins)) { - self.SamizdatPlugins = new Array() -} +// no polluting of the global namespace please +(function () { + + /* + * this plugin has no config settings + */ + + /** + * getting content using regular HTTP(S) fetch() + */ + let fetchContent = (url) => { + console.log('Samizdat: regular fetch!') + return fetch(url, {cache: "reload"}) + .then((response) => { + // 4xx? 5xx? that's a paddlin' + if (response.status >= 400) { + // throw an Error to fall back to Samizdat: + throw new Error('HTTP Error: ' + response.status + ' ' + response.statusText); + } + // all good, it seems + console.log("(COMMIT_UNKNOWN) Fetched:", response.url); + + // we need to create a new Response object + // with all the headers added explicitly, + // since response.headers is immutable + var init = { + status: response.status, + statusText: response.statusText, + headers: {} + }; + response.headers.forEach(function(val, header){ + init.headers[header] = val; + }); + + // add the X-Samizdat-* headers to the mix + init.headers['X-Samizdat-Method'] = 'fetch' + init.headers['X-Samizdat-ETag'] = response.headers.get('ETag') + + // return the new response, using the Blob from the original one + return response + .blob() + .then((blob) => { + return new Response( + blob, + init + ) + }) + }) + } + + // initialize the SamizdatPlugins array + if (!Array.isArray(self.SamizdatPlugins)) { + self.SamizdatPlugins = new Array() + } + + // and add ourselves to it + // with some additional metadata + self.SamizdatPlugins.push({ + name: 'fetch', + description: 'Just a regular HTTP(S) fetch()', + version: 'COMMIT_UNKNOWN', + fetch: fetchContent + }) -// and add ourselves to it -// with some additional metadata -self.SamizdatPlugins.push({ - name: 'fetch', - description: 'Just a regular HTTP(S) fetch()', - version: 'COMMIT_UNKNOWN', - fetch: fetchContent -}) +// done with not poluting the global namespace +})() diff --git a/plugins/gateway-ipns.js b/plugins/gateway-ipns.js index 9241a7d9199a4052dd2ed63119acee8128df22e4..01e2fb2ece25c742ca018ed4c2b366f1662a2877 100644 --- a/plugins/gateway-ipns.js +++ b/plugins/gateway-ipns.js @@ -6,129 +6,148 @@ * this plugin does not implement any push method */ -// the pubkey of the preconfigured IPNS node -const ipnsPubKey = 'QmYGVgGGfD5N4Xcc78CcMJ99dKcH6K6myhd4Uenv5yJwiJ' +// no polluting of the global namespace please +(function () { -// this will become useful later -//const gatewaysJSONUrl = "https://ipfs.github.io/public-gateway-checker/gateways.json"; -// important: -// we cannot use gateways that use hash directly in the (sub)domain: -// https://github.com/node-fetch/node-fetch/issues/260 -const gateways = [ - 'https://ninetailed.ninja/ipns/', // Russia - 'https://10.via0.com/ipns/', // USA - 'https://ipfs.sloppyta.co/ipns/', // UK - 'https://gateway.temporal.cloud/ipns/', // Germany - 'https://ipfs.best-practice.se/ipns/' // Sweden -] + /* + * plugin config settings + */ + + // sane defaults + let defaultConfig = { + // the pubkey of the preconfigured IPNS node; always needs to be set in config.js + ipnsPubkey: null, + // some default IPFS gateways to use + // + // important: + // we cannot use gateways that use hash directly in the (sub)domain: + // https://github.com/node-fetch/node-fetch/issues/260 + ipfsGateways: [ + 'https://ninetailed.ninja/ipns/', // Russia + 'https://10.via0.com/ipns/', // USA + 'https://ipfs.sloppyta.co/ipns/', // UK + 'https://gateway.temporal.cloud/ipns/', // Germany + 'https://ipfs.best-practice.se/ipns/' // Sweden + ] + } + // merge the defaults with settings from SamizdatConfig + let config = {...defaultConfig, ...self.SamizdatConfig.plugins["gateway-ipns"]} -/* - * to do this right we need a Promise.any() polyfill - * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any - */ -Promise.any = async (promises) => { - // Promise.all() is the polar opposite of Promise.any() - // in that it returns as soon as there is a first rejection - // but without it, it returns an array of resolved results - return Promise.all( - promises.map(p => { - return new Promise((resolve, reject) => - // swap reject and resolve, so that we can use Promise.all() - // and get the result we need - Promise.resolve(p).then(reject, resolve) - ); - }) - // now, swap errors and values back - ).then( - err => Promise.reject(err), - val => Promise.resolve(val) - ); -}; + // reality check: Gun pubkey needs to be set to a non-empty string + if (typeof(config.ipnsPubkey) !== "string" || config.ipnsPubkey === "") { + let err = new Error("ipnsPubkey not confgured") + console.error(err) + throw err + } + /* + * to do this right we need a Promise.any() polyfill + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any + */ + Promise.any = async (promises) => { + // Promise.all() is the polar opposite of Promise.any() + // in that it returns as soon as there is a first rejection + // but without it, it returns an array of resolved results + return Promise.all( + promises.map(p => { + return new Promise((resolve, reject) => + // swap reject and resolve, so that we can use Promise.all() + // and get the result we need + Promise.resolve(p).then(reject, resolve) + ); + }) + // now, swap errors and values back + ).then( + err => Promise.reject(err), + val => Promise.resolve(val) + ); + }; -/** - * getting content using regular HTTP(S) fetch() - */ -let fetchContentFromGatewayIPNS = (url) => { - - console.log(`Samizdat: pre-configured gateways:\n ${gateways.join('\n ')}`) + + /** + * getting content using regular HTTP(S) fetch() + */ + let fetchContentFromGatewayIPNS = (url) => { - // we're going to try a random gateway and building an URL of the form: - // https://<gateway_address>/<ipnsPubkey>/<rest_of_URL> - var ipnsUrl = ipnsPubKey + url.replace(/https?:\/\/[^/]+/, '') - - // pick 3 gateways, at random - var sourceGateways = [...gateways] - // if we have fewer than 3 gateways configured, use all of them - if (sourceGateways.length <= 3) { - var useGateways = sourceGateways - // otherwise get 3 at random - } else { - var useGateways = new Array() - while (useGateways.length < 3) { - // put in the address while we're at it - useGateways.push( - sourceGateways - .splice(Math.floor(Math.random() * sourceGateways.length), 1)[0] + ipnsUrl - ) + // we're going to try a random gateway and building an URL of the form: + // https://<gateway_address>/<pubkey>/<rest_of_URL> + var ipnsUrl = config.ipnsPubkey + url.replace(/https?:\/\/[^/]+/, '') + + // pick 3 gateways, at random + var sourceGateways = [...config.ipfsGateways] + // if we have fewer than 3 gateways configured, use all of them + if (sourceGateways.length <= 3) { + var useGateways = sourceGateways + // otherwise get 3 at random + } else { + var useGateways = new Array() + while (useGateways.length < 3) { + // put in the address while we're at it + useGateways.push( + sourceGateways + .splice(Math.floor(Math.random() * sourceGateways.length), 1)[0] + ipnsUrl + ) + } } + + // debug log + console.log(`Samizdat: gateway IPNS fetching:\n ${useGateways.join('\n ')}`) + + return Promise.any( + useGateways.map( + u=>fetch(u, {cache: "reload"}) + )) + .then((response) => { + // 4xx? 5xx? that's a paddlin' + if (response.status >= 400) { + // throw an Error to fall back to other plugins: + throw new Error('HTTP Error: ' + response.status + ' ' + response.statusText); + } + // all good, it seems + console.log("(COMMIT_UNKNOWN) Fetched:", response.url); + + // we need to create a new Response object + // with all the headers added explicitly, + // since response.headers is immutable + var init = { + status: response.status, + statusText: response.statusText, + headers: {} + }; + response.headers.forEach(function(val, header){ + init.headers[header] = val; + }); + + // add the X-Samizdat-* headers to the mix + init.headers['X-Samizdat-Method'] = 'gateway-ipns' + init.headers['X-Samizdat-ETag'] = response.headers.get('ETag') + + // return the new response, using the Blob from the original one + return response + .blob() + .then((blob) => { + return new Response( + blob, + init + ) + }) + }) } - // debug log - console.log(`Samizdat: gateway IPNS fetching:\n ${useGateways.join('\n ')}`) - - return Promise.any( - useGateways.map( - u=>fetch(u, {cache: "reload"}) - )) - .then((response) => { - // 4xx? 5xx? that's a paddlin' - if (response.status >= 400) { - // throw an Error to fall back to other plugins: - throw new Error('HTTP Error: ' + response.status + ' ' + response.statusText); - } - // all good, it seems - console.log("(COMMIT_UNKNOWN) Fetched:", response.url); - - // we need to create a new Response object - // with all the headers added explicitly, - // since response.headers is immutable - var init = { - status: response.status, - statusText: response.statusText, - headers: {} - }; - response.headers.forEach(function(val, header){ - init.headers[header] = val; - }); - - // add the X-Samizdat-* headers to the mix - init.headers['X-Samizdat-Method'] = 'gateway-ipns' - init.headers['X-Samizdat-ETag'] = response.headers.get('ETag') - - // return the new response, using the Blob from the original one - return response - .blob() - .then((blob) => { - return new Response( - blob, - init - ) - }) - }) -} - -// initialize the SamizdatPlugins array -if (!Array.isArray(self.SamizdatPlugins)) { - self.SamizdatPlugins = new Array() -} + // initialize the SamizdatPlugins array + if (!Array.isArray(self.SamizdatPlugins)) { + self.SamizdatPlugins = new Array() + } -// and add ourselves to it -// with some additional metadata -self.SamizdatPlugins.push({ - name: 'gateway-ipns', - description: 'HTTP(S) fetch() from IPNS via known public gateways', - version: 'COMMIT_UNKNOWN', - fetch: fetchContentFromGatewayIPNS -}) + // and add ourselves to it + // with some additional metadata + self.SamizdatPlugins.push({ + name: 'gateway-ipns', + description: 'HTTP(S) fetch() from IPNS via known public gateways', + version: 'COMMIT_UNKNOWN', + fetch: fetchContentFromGatewayIPNS + }) + +// done with not poluting the global namespace +})() diff --git a/plugins/gun-ipfs.js b/plugins/gun-ipfs.js index 90d915055d4414c894273f3aaa0944598f1d5f86..8fab65b8db256b4d388bc4b8098d8064cc0096ed 100644 --- a/plugins/gun-ipfs.js +++ b/plugins/gun-ipfs.js @@ -11,431 +11,450 @@ |* === General stuff and setup === *| \* ========================================================================= */ -var ipfs; -var gun; -var gunUser; - -// the pubkey of the preconfigured Gun user -const gunPubKey = 'WUK5ylwqqgUorceQRa84qfbBFhk7eNRDUoPbGK05SyE.-yohFhTzWPpDT-UDMuKGgemOUrw_cMMYWpy6plILqrg' +// no polluting of the global namespace please +(function () { + + var ipfs; + var gun; + var gunUser; + + // sane defaults + let defaultConfig = { + // the pubkey of the preconfigured Gun user; always needs to be set in config.js + gunPubkey: null, + // the IPFS gateway we're using for verification when publishing; default is usually ok + ipfsGateway: 'https://gateway.ipfs.io' + } -// the IPFS gateway we're using for verification -const IPFSGateway = 'https://gateway.ipfs.io' + // merge the defaults with settings from SamizdatConfig + let config = {...defaultConfig, ...self.SamizdatConfig.plugins["gun+ipfs"]} -/** - * this is apparently needed by Gun - * and `window` does not exist in ServiceWorker context - */ -if (typeof window === 'undefined') { - console.log('(COMMIT_UNKNOWN) redefining window...') - var window = self; -} + // reality check: Gun pubkey needs to be set to a non-empty string + if (typeof(config.gunPubkey) !== "string" || config.gunPubkey === "") { + let err = new Error("gunPubkey not confgured") + console.error(err) + throw err + } -/** - * importing stuff works differently between a browser window context - * and a ServiceWorker context, because things just can't be easy and sane - */ -function doImport() { - var args = Array.prototype.slice.call(arguments); - if (typeof self.importScripts !== 'undefined') { - self.importScripts.apply(self, args) - } else { - console.log('(COMMIT_UNKNOWN) assuming these scripts are already included:') - args.forEach(function(src){ - console.log('+--', src) - }) + /** + * this is apparently needed by Gun + * and `window` does not exist in ServiceWorker context + */ + if (typeof window === 'undefined') { + console.log('(COMMIT_UNKNOWN) redefining window...') + var window = self; } -} - -async function setup_ipfs() { - if (ipfs === undefined) { - ipfs = false // we don't want to start a few times over - console.log('(COMMIT_UNKNOWN) Importing IPFS-related libraries...'); - doImport( - "./lib/ipfs.js"); - console.log('(COMMIT_UNKNOWN) Setting up IPFS...') - ipfs = await self.Ipfs.create(); - console.log('+-- IPFS loaded :: ipfs is : ' + typeof ipfs) + + /** + * importing stuff works differently between a browser window context + * and a ServiceWorker context, because things just can't be easy and sane + */ + function doImport() { + var args = Array.prototype.slice.call(arguments); + if (typeof self.importScripts !== 'undefined') { + self.importScripts.apply(self, args) + } else { + console.log('(COMMIT_UNKNOWN) assuming these scripts are already included:') + args.forEach(function(src){ + console.log('+--', src) + }) + } } -} - -async function setup_gun() { - if (gun === undefined) { - gun = false // we don't want to start a few times over - console.log('(COMMIT_UNKNOWN) Importing Gun-related libraries...'); - doImport( - "./lib/gun.js", - "./lib/sea.js", - "./lib/webrtc.js"); - console.log('(COMMIT_UNKNOWN) Setting up Gun...') - gun = Gun(['https://gunjs.herokuapp.com/gun', 'https://samizdat.is/gun']); - console.log('+-- Gun loaded :: gun is : ' + typeof gun); + + async function setup_ipfs() { + if (ipfs === undefined) { + ipfs = false // we don't want to start a few times over + console.log('(COMMIT_UNKNOWN) Importing IPFS-related libraries...'); + doImport( + "./lib/ipfs.js"); + console.log('(COMMIT_UNKNOWN) Setting up IPFS...') + ipfs = await self.Ipfs.create(); + console.log('+-- IPFS loaded :: ipfs is : ' + typeof ipfs) + } } - if ( (gun !== false) && (gun !== undefined) && (gunUser === undefined) ) { - gunUser = false // we don't want to start a few times over - console.log('(COMMIT_UNKNOWN) Setting up gunUser...') - gunUser = gun.user(gunPubKey) - console.log('+-- Gun init complete :: gunUser is: ' + typeof gunUser); + + async function setup_gun() { + if (gun === undefined) { + gun = false // we don't want to start a few times over + console.log('(COMMIT_UNKNOWN) Importing Gun-related libraries...'); + doImport( + "./lib/gun.js", + "./lib/sea.js", + "./lib/webrtc.js"); + console.log('(COMMIT_UNKNOWN) Setting up Gun...') + gun = Gun(['https://gunjs.herokuapp.com/gun', 'https://samizdat.is/gun']); + console.log('+-- Gun loaded :: gun is : ' + typeof gun); + } + if ( (gun !== false) && (gun !== undefined) && (gunUser === undefined) ) { + gunUser = false // we don't want to start a few times over + console.log('(COMMIT_UNKNOWN) Setting up gunUser...') + gunUser = gun.user(config.gunPubkey) + console.log('+-- Gun init complete :: gunUser is: ' + typeof gunUser); + } } -} - -async function setup_gun_ipfs() { - if (ipfs === undefined || gun === undefined) { - console.log('(COMMIT_UNKNOWN) Setting up Samizdat...') - setup_ipfs(); - setup_gun(); - } else { - console.log('(COMMIT_UNKNOWN) Samizdat setup already underway (ipfs: ' + ( (ipfs) ? 'done' : 'loading' ) + ', gun: ' + ( (gun) ? 'done' : 'loading' ) + ')') + + async function setup_gun_ipfs() { + if (ipfs === undefined || gun === undefined) { + console.log('(COMMIT_UNKNOWN) Setting up Samizdat...') + setup_ipfs(); + setup_gun(); + } else { + console.log('(COMMIT_UNKNOWN) Samizdat setup already underway (ipfs: ' + ( (ipfs) ? 'done' : 'loading' ) + ', gun: ' + ( (gun) ? 'done' : 'loading' ) + ')') + } } -} -/* ========================================================================= *\ -|* === Main functionality === *| -\* ========================================================================= */ + /* ========================================================================= *\ + |* === Main functionality === *| + \* ========================================================================= */ + + let getGunData = (gunaddr) => { + return new Promise( + (resolve, reject) => { + console.log('(COMMIT_UNKNOWN) getGunData()'); + console.log('(COMMIT_UNKNOWN) getGunData() :: +-- gunUser : ' + typeof gunUser); + console.log('(COMMIT_UNKNOWN) getGunData() :: +-- gunaddr[] : ' + gunaddr); + + // get the data + gunUser + .get(gunaddr[0]) + .get(gunaddr[1]) + .once(function(addr){ + if (typeof addr !== 'undefined') { + console.log("2.1 IPFS address: '" + addr + "'"); + resolve(addr); + } else { + // looks like we didn't get anything + reject(new Error('IPFS address is undefined for: ' + gunaddr[1])) + } + // ToDo: what happens when we hit the timeout here? + }, {wait: 5000}); + } + ); + }; -let getGunData = (gunaddr) => { - return new Promise( - (resolve, reject) => { - console.log('(COMMIT_UNKNOWN) getGunData()'); - console.log('(COMMIT_UNKNOWN) getGunData() :: +-- gunUser : ' + typeof gunUser); - console.log('(COMMIT_UNKNOWN) getGunData() :: +-- gunaddr[] : ' + gunaddr); - - // get the data - gunUser - .get(gunaddr[0]) - .get(gunaddr[1]) - .once(function(addr){ - if (typeof addr !== 'undefined') { - console.log("2.1 IPFS address: '" + addr + "'"); - resolve(addr); - } else { - // looks like we didn't get anything - reject(new Error('IPFS address is undefined for: ' + gunaddr[1])) - } - // ToDo: what happens when we hit the timeout here? - }, {wait: 5000}); + /** + * the workhorse of this plugin + */ + async function getContentFromGunAndIPFS(url) { + var urlArray = url.replace(/https?:\/\//, '').split('/') + var gunaddr = [urlArray[0], '/' + urlArray.slice(1).join('/')] + + /* + * if the gunaddr[1] ends in '/', append 'index.html' to it + */ + if (gunaddr[1].charAt(gunaddr[1].length - 1) === '/') { + console.log("NOTICE: address ends in '/', assuming '/index.html' should be appended."); + gunaddr[1] += 'index.html'; } - ); -}; -/** - * the workhorse of this plugin - */ -async function getContentFromGunAndIPFS(url) { - var urlArray = url.replace(/https?:\/\//, '').split('/') - var gunaddr = [urlArray[0], '/' + urlArray.slice(1).join('/')] + console.log("2. Starting Gun lookup of: '" + gunaddr.join(', ') + "'"); + console.log(" +-- gun : " + typeof gun); + console.log(" +-- gunUser : " + typeof gunUser); - /* - * if the gunaddr[1] ends in '/', append 'index.html' to it - */ - if (gunaddr[1].charAt(gunaddr[1].length - 1) === '/') { - console.log("NOTICE: address ends in '/', assuming '/index.html' should be appended."); - gunaddr[1] += 'index.html'; + /* + * naïvely assume content type based on file extension + * TODO: this needs a fix + */ + var contentType = ''; + switch (gunaddr.slice(-1)[0].split('.', -1)[1].toLowerCase()) { + case 'html': + case 'htm': + contentType = 'text/html'; + break; + case 'css': + contentType = 'text/css'; + break; + case 'js': + contentType = 'text/javascript'; + break; + case 'svg': + contentType = 'image/svg+xml'; + break; + case 'ico': + contentType = 'image/x-icon'; + break; + } + console.log(" +-- guessed contentType : " + contentType); + + return getGunData(gunaddr).then(ipfsaddr => { + console.log("3. Starting IPFS lookup of: '" + ipfsaddr + "'"); + return ipfs.get(ipfsaddr).next(); + }).then(file => { + // we only need one + if (file.value.content) { + async function getContent(source) { + var content = new Uint8Array() + var data = await source.next() + while (! data.done) { + var newContent = new Uint8Array(content.length + data.value.length); + newContent.set(content) + newContent.set(data.value, content.length) + content = newContent + data = await source.next() + } + return content + } + return getContent(file.value.content).then((content)=>{ + console.log('4. Got a Gun-addressed IPFS-stored file: ' + file.value.path + '; content is: ' + typeof content); + // creating and populating the blob + var blob = new Blob( + [content], + {'type': contentType} + ); + + return new Response( + blob, + { + 'status': 200, + 'statusText': 'OK', + 'headers': { + 'Content-Type': contentType, + 'ETag': file.value.path, + 'X-Samizdat-Method': 'gun+ipfs', + 'X-Samizdat-ETag': file.value.path + } + } + ); + }) + }; + }); } - console.log("2. Starting Gun lookup of: '" + gunaddr.join(', ') + "'"); - console.log(" +-- gun : " + typeof gun); - console.log(" +-- gunUser : " + typeof gunUser); + /* ========================================================================= *\ + |* === Publishing stuff === *| + \* ========================================================================= */ + /* - * naïvely assume content type based on file extension - * TODO: this needs a fix + * these are used for adding content to IPFS and Gun */ - var contentType = ''; - switch (gunaddr.slice(-1)[0].split('.', -1)[1].toLowerCase()) { - case 'html': - case 'htm': - contentType = 'text/html'; - break; - case 'css': - contentType = 'text/css'; - break; - case 'js': - contentType = 'text/javascript'; - break; - case 'svg': - contentType = 'image/svg+xml'; - break; - case 'ico': - contentType = 'image/x-icon'; - break; - } - console.log(" +-- guessed contentType : " + contentType); - - return getGunData(gunaddr).then(ipfsaddr => { - console.log("3. Starting IPFS lookup of: '" + ipfsaddr + "'"); - return ipfs.get(ipfsaddr).next(); - }).then(file => { - // we only need one - if (file.value.content) { - async function getContent(source) { - var content = new Uint8Array() - var data = await source.next() - while (! data.done) { - var newContent = new Uint8Array(content.length + data.value.length); - newContent.set(content) - newContent.set(data.value, content.length) - content = newContent - data = await source.next() - } - return content - } - return getContent(file.value.content).then((content)=>{ - console.log('4. Got a Gun-addressed IPFS-stored file: ' + file.value.path + '; content is: ' + typeof content); - // creating and populating the blob - var blob = new Blob( - [content], - {'type': contentType} - ); - - return new Response( - blob, - { - 'status': 200, - 'statusText': 'OK', - 'headers': { - 'Content-Type': contentType, - 'ETag': file.value.path, - 'X-Samizdat-Method': 'gun+ipfs', - 'X-Samizdat-ETag': file.value.path - } - } - ); - }) - }; - }); -} -/* ========================================================================= *\ -|* === Publishing stuff === *| -\* ========================================================================= */ -/* - * these are used for adding content to IPFS and Gun - */ - - -/** - * adding stuff to IPFS - * accepts an array of URLs - * - * returns a Promise that resolves to an object mapping URLs to IPFS hashes - */ -let addToIPFS = (resources) => { - return new Promise((resolve, reject) => { - - console.log("Adding to IPFS...") - console.log("+-- number of resources:", resources.length) - var ipfs_addresses = {}; - - resources.forEach(function(res){ - console.log(" +-- handling internal resource:", res) + /** + * adding stuff to IPFS + * accepts an array of URLs + * + * returns a Promise that resolves to an object mapping URLs to IPFS hashes + */ + let addToIPFS = (resources) => { + return new Promise((resolve, reject) => { - ipfs.add(Ipfs.urlSource(res)) - .then((result) => { - // add to the list -- this is needed to add stuff to Gun - // result.path is just the filename stored in IPFS, not the actual path! - // res holds the full URL - // what we need in ipfs_addresses is in fact the absolute path (no domain, no scheme) - var abs_path = res.replace(window.location.origin, '') - ipfs_addresses[abs_path] = '/ipfs/' + result.cid.string - console.log("Added to IPFS: " + abs_path + ' as ' + ipfs_addresses[abs_path]) - // if we seem to have all we need, resolve! - if (Object.keys(ipfs_addresses).length === resources.length) resolve(ipfs_addresses); - }) - - }); - }) -} + console.log("Adding to IPFS...") + console.log("+-- number of resources:", resources.length) + var ipfs_addresses = {}; + + resources.forEach(function(res){ + console.log(" +-- handling internal resource:", res) + + ipfs.add(Ipfs.urlSource(res)) + .then((result) => { + // add to the list -- this is needed to add stuff to Gun + // result.path is just the filename stored in IPFS, not the actual path! + // res holds the full URL + // what we need in ipfs_addresses is in fact the absolute path (no domain, no scheme) + var abs_path = res.replace(window.location.origin, '') + ipfs_addresses[abs_path] = '/ipfs/' + result.cid.string + console.log("Added to IPFS: " + abs_path + ' as ' + ipfs_addresses[abs_path]) + // if we seem to have all we need, resolve! + if (Object.keys(ipfs_addresses).length === resources.length) resolve(ipfs_addresses); + }) + + }); + }) + } -/** - * verification that content pushed to IPFS - * is, in fact, available in IPFS - * - * a nice side-effect is that this will pre-load the content on - * a gateway, which tends to be a large (and fast) IPFS node - * - * this is prety naïve, in that it pulls the content from an ipfs gateway - * and assumes all is well if it get a HTTP 200 and any content - * - * that is, it does *not* check that the content matches what was pushed - * we trust IPFS here, I guess - * - * finally, we're using a regular fetch() instead of just going through our - * ipfs object because our IPFS object might have things cached and we want - * to test a completey independent route - * - * takes a object mapping paths to IPFS addresses - * and returns a Promise that resolves to true - */ -let verifyInIPFS = (ipfs_addresses) => { - return new Promise((resolve, reject) => { - console.log('Checking IPFS content against a gateway...') - console.log('+-- gateway in use: ' + IPFSGateway) - // get the list of IPFS addresses - var updatedPaths = Object.values(ipfs_addresses) - for (path in ipfs_addresses) { - // start the fetch - fetch(IPFSGateway + ipfs_addresses[path]) - .then((response) => { - ipfsaddr = response.url.replace(IPFSGateway, '') - if (response.ok) { - console.log('+-- verified: ' + ipfsaddr) - var pathIndex = updatedPaths.indexOf(ipfsaddr) - if (pathIndex > -1) { - updatedPaths.splice(pathIndex, 1) - } - if (updatedPaths.length === 0) { - console.log('All updates confirmed successful!') - resolve(ipfs_addresses); + /** + * verification that content pushed to IPFS + * is, in fact, available in IPFS + * + * a nice side-effect is that this will pre-load the content on + * a gateway, which tends to be a large (and fast) IPFS node + * + * this is prety naïve, in that it pulls the content from an ipfs gateway + * and assumes all is well if it get a HTTP 200 and any content + * + * that is, it does *not* check that the content matches what was pushed + * we trust IPFS here, I guess + * + * finally, we're using a regular fetch() instead of just going through our + * ipfs object because our IPFS object might have things cached and we want + * to test a completey independent route + * + * takes a object mapping paths to IPFS addresses + * and returns a Promise that resolves to true + */ + let verifyInIPFS = (ipfs_addresses) => { + return new Promise((resolve, reject) => { + console.log('Checking IPFS content against a gateway...') + console.log('+-- gateway in use: ' + config.ipfsGateway) + // get the list of IPFS addresses + var updatedPaths = Object.values(ipfs_addresses) + for (path in ipfs_addresses) { + // start the fetch + fetch(config.ipfsGateway + ipfs_addresses[path]) + .then((response) => { + ipfsaddr = response.url.replace(config.ipfsGateway, '') + if (response.ok) { + console.log('+-- verified: ' + ipfsaddr) + var pathIndex = updatedPaths.indexOf(ipfsaddr) + if (pathIndex > -1) { + updatedPaths.splice(pathIndex, 1) + } + if (updatedPaths.length === 0) { + console.log('All updates confirmed successful!') + resolve(ipfs_addresses); + } + } else { + reject(new Error('HTTP error (' + response.status + ' ' + response.statusText + ' for: ' + ipfsaddr)) } - } else { - reject(new Error('HTTP error (' + response.status + ' ' + response.statusText + ' for: ' + ipfsaddr)) - } - }) - .catch((err) => { - // it would be nice to have the failed path here somehow - // alternatively, updating updatedPaths with info on failed - // requests might work - reject(err) - }) - } - }) -} - -/** - * auth a Gun admin user - * (and verify it's the correct one with regards to the configured gunPubKey) - */ -let authGunAdmin = (user, pass) => { - return new Promise((resolve, reject) => { - // we need a separate Gun instance, otherwise gu will get merged with gunUser - // and we want these to be separate - var g = Gun(['https://gunjs.herokuapp.com/gun', 'https://samizdat.is/gun']) - var gu = g.user() - gu.auth(user, pass, (userReference) => { - if (userReference.err) { - reject(new Error(userReference.err)) - // reality check -- does it match our preconfigured pubkey? - } else if (gu._.soul.slice(1) === gunPubKey) { - console.log('Gun Admin user authenticated using password.'); - // we need to keep the reference to g, otherwise gu becomes unusable - var gApi = { - user: gu, - gun: g - } - resolve(gApi) - } else { - reject(new Error('Password-authenticated user does not match preconfigured pubkey!')) + }) + .catch((err) => { + // it would be nice to have the failed path here somehow + // alternatively, updating updatedPaths with info on failed + // requests might work + reject(err) + }) } }) - }) -} + } -/** - * add IPFS addresses to Gun - */ -let addToGun = (user, pass, ipfs_addresses) => { - // we need an authenticated Gun user - return authGunAdmin(user, pass) - .then((gunAPI) => { - console.log('+-- adding new IPFS addresses to Gun...') - gunAPI.user.get(window.location.host).put(ipfs_addresses /*, function(ack) {...}*/); - return gunAPI; + /** + * auth a Gun admin user + * (and verify it's the correct one with regards to the configured config.gunPubkey) + */ + let authGunAdmin = (user, pass) => { + return new Promise((resolve, reject) => { + // we need a separate Gun instance, otherwise gu will get merged with gunUser + // and we want these to be separate + var g = Gun(['https://gunjs.herokuapp.com/gun', 'https://samizdat.is/gun']) + var gu = g.user() + gu.auth(user, pass, (userReference) => { + if (userReference.err) { + reject(new Error(userReference.err)) + // reality check -- does it match our preconfigured pubkey? + } else if (gu._.soul.slice(1) === config.gunPubkey) { + console.log('Gun Admin user authenticated using password.'); + // we need to keep the reference to g, otherwise gu becomes unusable + var gApi = { + user: gu, + gun: g + } + resolve(gApi) + } else { + reject(new Error('Password-authenticated user does not match preconfigured pubkey!')) + } + }) }) - /** - * regular confirmations don't seem to work - * - * so instead we're using the regular read-only Gun user - * to .get() the data that we've .put() just a minute ago - * - * we then subscribe to the .on() events and once we notice the correct - * addresseswe consider our job done and quit. - */ - .then((gunAPI) => { - // get the paths - console.log('+-- starting verification of updated Gun data...') - var updatedPaths = Object.keys(ipfs_addresses) - for (path in ipfs_addresses) { - console.log(' +-- watching: ' + path) - //debuglog('watching path for updates:', path) - // using the global gunUser to check if updates propagated - gunUser.get(window.location.host).get(path).on(function(updaddr, updpath){ - /*debuglog('+--', updpath) - debuglog(' updated :', ipfs_addresses[updpath]) - debuglog(' received :', updaddr)*/ - if (ipfs_addresses[updpath] == updaddr) { - // update worked! - gunUser.get(window.location.host).get(updpath).off() - console.log('+-- update confirmed for:', updpath, '[' + updaddr + ']') - var pathIndex = updatedPaths.indexOf(updpath) - if (pathIndex > -1) { - updatedPaths.splice(pathIndex, 1) - } - if (updatedPaths.length === 0) { - console.log('All updates confirmed successful!') - return true; + } + + /** + * add IPFS addresses to Gun + */ + let addToGun = (user, pass, ipfs_addresses) => { + // we need an authenticated Gun user + return authGunAdmin(user, pass) + .then((gunAPI) => { + console.log('+-- adding new IPFS addresses to Gun...') + gunAPI.user.get(window.location.host).put(ipfs_addresses /*, function(ack) {...}*/); + return gunAPI; + }) + /** + * regular confirmations don't seem to work + * + * so instead we're using the regular read-only Gun user + * to .get() the data that we've .put() just a minute ago + * + * we then subscribe to the .on() events and once we notice the correct + * addresseswe consider our job done and quit. + */ + .then((gunAPI) => { + // get the paths + console.log('+-- starting verification of updated Gun data...') + var updatedPaths = Object.keys(ipfs_addresses) + for (path in ipfs_addresses) { + console.log(' +-- watching: ' + path) + //debuglog('watching path for updates:', path) + // using the global gunUser to check if updates propagated + gunUser.get(window.location.host).get(path).on(function(updaddr, updpath){ + /*debuglog('+--', updpath) + debuglog(' updated :', ipfs_addresses[updpath]) + debuglog(' received :', updaddr)*/ + if (ipfs_addresses[updpath] == updaddr) { + // update worked! + gunUser.get(window.location.host).get(updpath).off() + console.log('+-- update confirmed for:', updpath, '[' + updaddr + ']') + var pathIndex = updatedPaths.indexOf(updpath) + if (pathIndex > -1) { + updatedPaths.splice(pathIndex, 1) + } + if (updatedPaths.length === 0) { + console.log('All updates confirmed successful!') + return true; + } } - } - }) - } - }) -} + }) + } + }) + } -/** - * example code for of adding content to IPFS, verifying it was successfully added, - * and adding the new addresses to Gun (and verifying changes propagated) - * - * TODO: this should accept a URL, a Response, or a list of URLs, - * and handle stuff appropriately - */ -let publishContent = (resource, user, password) => { - - if (typeof resource === 'string') { - // we need this as an array of strings - resource = [resource] - } else if (typeof resource === 'object') { - if (!Array.isArray(resource)) { - // TODO: this needs to be implemented such that the Response is used directly - // but that would require all called functions to also accept a Response - // and act accordingly; #ThisIsComplicated - throw new Error("Handling a Response: not implemented yet") + /** + * example code for of adding content to IPFS, verifying it was successfully added, + * and adding the new addresses to Gun (and verifying changes propagated) + * + * TODO: this should accept a URL, a Response, or a list of URLs, + * and handle stuff appropriately + */ + let publishContent = (resource, user, password) => { + + if (typeof resource === 'string') { + // we need this as an array of strings + resource = [resource] + } else if (typeof resource === 'object') { + if (!Array.isArray(resource)) { + // TODO: this needs to be implemented such that the Response is used directly + // but that would require all called functions to also accept a Response + // and act accordingly; #ThisIsComplicated + throw new Error("Handling a Response: not implemented yet") + } + } else { + // everything else -- that's a paddlin'! + throw new TypeError("Only accepts: string, Array of string, Response.") } - } else { - // everything else -- that's a paddlin'! - throw new TypeError("Only accepts: string, Array of string, Response.") + + // add to IPFS + var ipfsPromise = addToIPFS(resource) + return Promise.all([ + // verify stuff ended up in IPFS + ipfsPromise.then(verifyInIPFS), + // add to Gun and verify Gun updates propagation + ipfsPromise.then((hashes) => { + addToGun(user, password, hashes) + }) + ]) } - - // add to IPFS - var ipfsPromise = addToIPFS(resource) - return Promise.all([ - // verify stuff ended up in IPFS - ipfsPromise.then(verifyInIPFS), - // add to Gun and verify Gun updates propagation - ipfsPromise.then((hashes) => { - addToGun(user, password, hashes) - }) - ]) -} -/* ========================================================================= *\ -|* === Initialization === *| -\* ========================================================================= */ + /* ========================================================================= *\ + |* === Initialization === *| + \* ========================================================================= */ -// we probably need to handle this better -setup_gun_ipfs(); - -// initialize the SamizdatPlugins array -if (!Array.isArray(self.SamizdatPlugins)) { - self.SamizdatPlugins = new Array() -} - -// and add ourselves to it -// with some additional metadata -self.SamizdatPlugins.push({ - name: 'gun+ipfs', - description: 'Decentralized resource fetching using Gun for address resolution and IPFS for content delivery.', - version: 'COMMIT_UNKNOWN', - fetch: getContentFromGunAndIPFS, - publish: publishContent -}) + // we probably need to handle this better + setup_gun_ipfs(); + + // initialize the SamizdatPlugins array + if (!Array.isArray(self.SamizdatPlugins)) { + self.SamizdatPlugins = new Array() + } + + // and add ourselves to it + // with some additional metadata + self.SamizdatPlugins.push({ + name: 'gun+ipfs', + description: 'Decentralized resource fetching using Gun for address resolution and IPFS for content delivery.', + version: 'COMMIT_UNKNOWN', + fetch: getContentFromGunAndIPFS, + publish: publishContent + }) + +// done with not poluting the global namespace +})() diff --git a/plugins/ipns-ipfs.js b/plugins/ipns-ipfs.js index 5c549930fedf5dd33d92c0ac97f1ea0909d45365..53ef4cb2f8e8210cfc2bae51b52ddc37c70a3a22 100644 --- a/plugins/ipns-ipfs.js +++ b/plugins/ipns-ipfs.js @@ -18,308 +18,326 @@ |* === General stuff and setup === *| \* ========================================================================= */ -var ipfs; +// no polluting of the global namespace please +(function () { -// the pubkey of the preconfigured IPNS node -const ipnsPubKey = 'QmYGVgGGfD5N4Xcc78CcMJ99dKcH6K6myhd4Uenv5yJwiJ' + var ipfs; -// the IPFS gateway we're using for verification -const IPFSGateway = 'https://gateway.ipfs.io' + // sane defaults + let defaultConfig = { + // the pubkey of the preconfigured IPNS node; always needs to be set in config.js + ipnsPubkey: null, + // the IPFS gateway we're using for verification when publishing; default is usually ok + ipfsGateway: 'https://gateway.ipfs.io' + } -/** - * importing stuff works differently between a browser window context - * and a ServiceWorker context, because things just can't be easy and sane - */ -function doImport() { - var args = Array.prototype.slice.call(arguments); - if (typeof self.importScripts !== 'undefined') { - self.importScripts.apply(self, args) - } else { - console.log('(COMMIT_UNKNOWN) assuming these scripts are already included:') - args.forEach(function(src){ - console.log('+--', src) - }) + // merge the defaults with settings from SamizdatConfig + let config = {...defaultConfig, ...self.SamizdatConfig.plugins["ipns+ipfs"]} + + // reality check: Gun pubkey needs to be set to a non-empty string + if (typeof(config.ipnsPubkey) !== "string" || config.ipnsPubkey === "") { + let err = new Error("ipnsPubkey not confgured") + console.error(err) + throw err } -} -async function setup_ipfs() { - if (ipfs === undefined) { - ipfs = false // we don't want to start a few times over - console.log('(COMMIT_UNKNOWN) Importing IPFS-related libraries...'); - doImport( - "./lib/ipfs.js"); - console.log('(COMMIT_UNKNOWN) Setting up IPFS...') - ipfs = await self.Ipfs.create({ - config: { - dht: { - enabled: true, - clientMode: true - } - }, - libp2p: { + /** + * importing stuff works differently between a browser window context + * and a ServiceWorker context, because things just can't be easy and sane + */ + function doImport() { + var args = Array.prototype.slice.call(arguments); + if (typeof self.importScripts !== 'undefined') { + self.importScripts.apply(self, args) + } else { + console.log('(COMMIT_UNKNOWN) assuming these scripts are already included:') + args.forEach(function(src){ + console.log('+--', src) + }) + } + } + + async function setup_ipfs() { + if (ipfs === undefined) { + ipfs = false // we don't want to start a few times over + console.log('(COMMIT_UNKNOWN) Importing IPFS-related libraries...'); + doImport( + "./lib/ipfs.js"); + console.log('(COMMIT_UNKNOWN) Setting up IPFS...') + ipfs = await self.Ipfs.create({ config: { dht: { enabled: true, clientMode: true } + }, + libp2p: { + config: { + dht: { + enabled: true, + clientMode: true + } + } } - } - }); - console.log('+-- IPFS loaded :: ipfs is : ' + typeof ipfs) + }); + console.log('+-- IPFS loaded :: ipfs is : ' + typeof ipfs) + } } -} -/* ========================================================================= *\ -|* === Main functionality === *| -\* ========================================================================= */ + /* ========================================================================= *\ + |* === Main functionality === *| + \* ========================================================================= */ -/** - * the workhorse of this plugin - */ -async function getContentFromIPNSAndIPFS(url) { - return new Error("Not implemented yet.") - - var urlArray = url.replace(/https?:\/\//, '').split('/') - var gunaddr = [urlArray[0], '/' + urlArray.slice(1).join('/')] - - /* - * if the gunaddr[1] ends in '/', append 'index.html' to it + /** + * the workhorse of this plugin */ - if (gunaddr[1].charAt(gunaddr[1].length - 1) === '/') { - console.log("NOTICE: address ends in '/', assuming '/index.html' should be appended."); - gunaddr[1] += 'index.html'; - } + async function getContentFromIPNSAndIPFS(url) { + return new Error("Not implemented yet.") + + var urlArray = url.replace(/https?:\/\//, '').split('/') + var gunaddr = [urlArray[0], '/' + urlArray.slice(1).join('/')] - console.log("2. Starting Gun lookup of: '" + gunaddr.join(', ') + "'"); - console.log(" +-- gun : " + typeof gun); - console.log(" +-- gunUser : " + typeof gunUser); + /* + * if the gunaddr[1] ends in '/', append 'index.html' to it + */ + if (gunaddr[1].charAt(gunaddr[1].length - 1) === '/') { + console.log("NOTICE: address ends in '/', assuming '/index.html' should be appended."); + gunaddr[1] += 'index.html'; + } - /* - * naïvely assume content type based on file extension - * TODO: this needs a fix - */ - var contentType = ''; - switch (gunaddr.slice(-1)[0].split('.', -1)[1].toLowerCase()) { - case 'html': - case 'htm': - contentType = 'text/html'; - break; - case 'css': - contentType = 'text/css'; - break; - case 'js': - contentType = 'text/javascript'; - break; - case 'svg': - contentType = 'image/svg+xml'; - break; - case 'ico': - contentType = 'image/x-icon'; - break; - } - console.log(" +-- guessed contentType : " + contentType); + console.log("2. Starting Gun lookup of: '" + gunaddr.join(', ') + "'"); + console.log(" +-- gun : " + typeof gun); + console.log(" +-- gunUser : " + typeof gunUser); - return getGunData(gunaddr).then(ipfsaddr => { - console.log("3. Starting IPFS lookup of: '" + ipfsaddr + "'"); - return ipfs.get(ipfsaddr).next(); - }).then(file => { - // we only need one - if (file.value.content) { - async function getContent(source) { - var content = new Uint8Array() - var data = await source.next() - while (! data.done) { - var newContent = new Uint8Array(content.length + data.value.length); - newContent.set(content) - newContent.set(data.value, content.length) - content = newContent - data = await source.next() + /* + * naïvely assume content type based on file extension + * TODO: this needs a fix + */ + var contentType = ''; + switch (gunaddr.slice(-1)[0].split('.', -1)[1].toLowerCase()) { + case 'html': + case 'htm': + contentType = 'text/html'; + break; + case 'css': + contentType = 'text/css'; + break; + case 'js': + contentType = 'text/javascript'; + break; + case 'svg': + contentType = 'image/svg+xml'; + break; + case 'ico': + contentType = 'image/x-icon'; + break; + } + console.log(" +-- guessed contentType : " + contentType); + + return getGunData(gunaddr).then(ipfsaddr => { + console.log("3. Starting IPFS lookup of: '" + ipfsaddr + "'"); + return ipfs.get(ipfsaddr).next(); + }).then(file => { + // we only need one + if (file.value.content) { + async function getContent(source) { + var content = new Uint8Array() + var data = await source.next() + while (! data.done) { + var newContent = new Uint8Array(content.length + data.value.length); + newContent.set(content) + newContent.set(data.value, content.length) + content = newContent + data = await source.next() + } + return content } - return content - } - return getContent(file.value.content).then((content)=>{ - console.log('4. Got a Gun-addressed IPFS-stored file: ' + file.value.path + '; content is: ' + typeof content); - // creating and populating the blob - var blob = new Blob( - [content], - {'type': contentType} - ); + return getContent(file.value.content).then((content)=>{ + console.log('4. Got a Gun-addressed IPFS-stored file: ' + file.value.path + '; content is: ' + typeof content); + // creating and populating the blob + var blob = new Blob( + [content], + {'type': contentType} + ); - return new Response( - blob, - { - 'status': 200, - 'statusText': 'OK', - 'headers': { - 'Content-Type': contentType, - 'ETag': file.value.path, - 'X-Samizdat-Method': 'gun+ipfs', - 'X-Samizdat-ETag': file.value.path + return new Response( + blob, + { + 'status': 200, + 'statusText': 'OK', + 'headers': { + 'Content-Type': contentType, + 'ETag': file.value.path, + 'X-Samizdat-Method': 'gun+ipfs', + 'X-Samizdat-ETag': file.value.path + } } - } - ); - }) - }; - }); -} + ); + }) + }; + }); + } -/* ========================================================================= *\ -|* === Publishing stuff === *| -\* ========================================================================= */ -/* - * these are used for adding content to IPFS and Gun - */ + /* ========================================================================= *\ + |* === Publishing stuff === *| + \* ========================================================================= */ + /* + * these are used for adding content to IPFS and Gun + */ -/** - * adding stuff to IPFS - * accepts an array of URLs - * - * returns a Promise that resolves to an object mapping URLs to IPFS hashes - */ -let addToIPFS = (resources) => { - return new Error("Not implemented yet.") - - return new Promise((resolve, reject) => { + /** + * adding stuff to IPFS + * accepts an array of URLs + * + * returns a Promise that resolves to an object mapping URLs to IPFS hashes + */ + let addToIPFS = (resources) => { + return new Error("Not implemented yet.") - console.log("Adding to IPFS...") - console.log("+-- number of resources:", resources.length) - var ipfs_addresses = {}; + return new Promise((resolve, reject) => { + + console.log("Adding to IPFS...") + console.log("+-- number of resources:", resources.length) + var ipfs_addresses = {}; - resources.forEach(function(res){ - console.log(" +-- handling internal resource:", res) + resources.forEach(function(res){ + console.log(" +-- handling internal resource:", res) + + ipfs.add(Ipfs.urlSource(res)) + .then((result) => { + // add to the list -- this is needed to add stuff to Gun + // result.path is just the filename stored in IPFS, not the actual path! + // res holds the full URL + // what we need in ipfs_addresses is in fact the absolute path (no domain, no scheme) + var abs_path = res.replace(window.location.origin, '') + ipfs_addresses[abs_path] = '/ipfs/' + result.cid.string + console.log("Added to IPFS: " + abs_path + ' as ' + ipfs_addresses[abs_path]) + // if we seem to have all we need, resolve! + if (Object.keys(ipfs_addresses).length === resources.length) resolve(ipfs_addresses); + }) - ipfs.add(Ipfs.urlSource(res)) - .then((result) => { - // add to the list -- this is needed to add stuff to Gun - // result.path is just the filename stored in IPFS, not the actual path! - // res holds the full URL - // what we need in ipfs_addresses is in fact the absolute path (no domain, no scheme) - var abs_path = res.replace(window.location.origin, '') - ipfs_addresses[abs_path] = '/ipfs/' + result.cid.string - console.log("Added to IPFS: " + abs_path + ' as ' + ipfs_addresses[abs_path]) - // if we seem to have all we need, resolve! - if (Object.keys(ipfs_addresses).length === resources.length) resolve(ipfs_addresses); - }) - - }); - }) -} + }); + }) + } -/** - * verification that content pushed to IPFS - * is, in fact, available in IPFS - * - * a nice side-effect is that this will pre-load the content on - * a gateway, which tends to be a large (and fast) IPFS node - * - * this is prety naïve, in that it pulls the content from an ipfs gateway - * and assumes all is well if it get a HTTP 200 and any content - * - * that is, it does *not* check that the content matches what was pushed - * we trust IPFS here, I guess - * - * finally, we're using a regular fetch() instead of just going through our - * ipfs object because our IPFS object might have things cached and we want - * to test a completey independent route - * - * takes a object mapping paths to IPFS addresses - * and returns a Promise that resolves to true - */ -let verifyInIPFS = (ipfs_addresses) => { - return new Error("Not implemented yet.") - - return new Promise((resolve, reject) => { - console.log('Checking IPFS content against a gateway...') - console.log('+-- gateway in use: ' + IPFSGateway) - // get the list of IPFS addresses - var updatedPaths = Object.values(ipfs_addresses) - for (path in ipfs_addresses) { - // start the fetch - fetch(IPFSGateway + ipfs_addresses[path]) - .then((response) => { - ipfsaddr = response.url.replace(IPFSGateway, '') - if (response.ok) { - console.log('+-- verified: ' + ipfsaddr) - var pathIndex = updatedPaths.indexOf(ipfsaddr) - if (pathIndex > -1) { - updatedPaths.splice(pathIndex, 1) - } - if (updatedPaths.length === 0) { - console.log('All updates confirmed successful!') - resolve(ipfs_addresses); + /** + * verification that content pushed to IPFS + * is, in fact, available in IPFS + * + * a nice side-effect is that this will pre-load the content on + * a gateway, which tends to be a large (and fast) IPFS node + * + * this is prety naïve, in that it pulls the content from an ipfs gateway + * and assumes all is well if it get a HTTP 200 and any content + * + * that is, it does *not* check that the content matches what was pushed + * we trust IPFS here, I guess + * + * finally, we're using a regular fetch() instead of just going through our + * ipfs object because our IPFS object might have things cached and we want + * to test a completey independent route + * + * takes a object mapping paths to IPFS addresses + * and returns a Promise that resolves to true + */ + let verifyInIPFS = (ipfs_addresses) => { + return new Error("Not implemented yet.") + + return new Promise((resolve, reject) => { + console.log('Checking IPFS content against a gateway...') + console.log('+-- gateway in use: ' + config.ipfsGateway) + // get the list of IPFS addresses + var updatedPaths = Object.values(ipfs_addresses) + for (path in ipfs_addresses) { + // start the fetch + fetch(config.ipfsGateway + ipfs_addresses[path]) + .then((response) => { + ipfsaddr = response.url.replace(config.ipfsGateway, '') + if (response.ok) { + console.log('+-- verified: ' + ipfsaddr) + var pathIndex = updatedPaths.indexOf(ipfsaddr) + if (pathIndex > -1) { + updatedPaths.splice(pathIndex, 1) + } + if (updatedPaths.length === 0) { + console.log('All updates confirmed successful!') + resolve(ipfs_addresses); + } + } else { + reject(new Error('HTTP error (' + response.status + ' ' + response.statusText + ' for: ' + ipfsaddr)) } - } else { - reject(new Error('HTTP error (' + response.status + ' ' + response.statusText + ' for: ' + ipfsaddr)) - } - }) - .catch((err) => { - // it would be nice to have the failed path here somehow - // alternatively, updating updatedPaths with info on failed - // requests might work - reject(err) - }) - } - }) -} + }) + .catch((err) => { + // it would be nice to have the failed path here somehow + // alternatively, updating updatedPaths with info on failed + // requests might work + reject(err) + }) + } + }) + } -/** - * example code for of adding content to IPFS, verifying it was successfully added, - * and adding the new addresses to Gun (and verifying changes propagated) - * - * TODO: this should accept a URL, a Response, or a list of URLs, - * and handle stuff appropriately - */ -let publishContent = (resource, user, password) => { - return new Error("Not implemented yet.") - - if (typeof resource === 'string') { - // we need this as an array of strings - resource = [resource] - } else if (typeof resource === 'object') { - if (!Array.isArray(resource)) { - // TODO: this needs to be implemented such that the Response is used directly - // but that would require all called functions to also accept a Response - // and act accordingly; #ThisIsComplicated - throw new Error("Handling a Response: not implemented yet") + /** + * example code for of adding content to IPFS, verifying it was successfully added, + * and adding the new addresses to Gun (and verifying changes propagated) + * + * TODO: this should accept a URL, a Response, or a list of URLs, + * and handle stuff appropriately + */ + let publishContent = (resource, user, password) => { + return new Error("Not implemented yet.") + + if (typeof resource === 'string') { + // we need this as an array of strings + resource = [resource] + } else if (typeof resource === 'object') { + if (!Array.isArray(resource)) { + // TODO: this needs to be implemented such that the Response is used directly + // but that would require all called functions to also accept a Response + // and act accordingly; #ThisIsComplicated + throw new Error("Handling a Response: not implemented yet") + } + } else { + // everything else -- that's a paddlin'! + throw new TypeError("Only accepts: string, Array of string, Response.") } - } else { - // everything else -- that's a paddlin'! - throw new TypeError("Only accepts: string, Array of string, Response.") + + // add to IPFS + var ipfsPromise = addToIPFS(resource) + return Promise.all([ + // verify stuff ended up in IPFS + ipfsPromise.then(verifyInIPFS), + // add to Gun and verify Gun updates propagation + ipfsPromise.then((hashes) => { + addToGun(user, password, hashes) + }) + ]) } - - // add to IPFS - var ipfsPromise = addToIPFS(resource) - return Promise.all([ - // verify stuff ended up in IPFS - ipfsPromise.then(verifyInIPFS), - // add to Gun and verify Gun updates propagation - ipfsPromise.then((hashes) => { - addToGun(user, password, hashes) - }) - ]) -} -/* ========================================================================= *\ -|* === Initialization === *| -\* ========================================================================= */ + /* ========================================================================= *\ + |* === Initialization === *| + \* ========================================================================= */ -// we probably need to handle this better -setup_ipfs(); + // we probably need to handle this better + setup_ipfs(); -// initialize the SamizdatPlugins array -if (!Array.isArray(self.SamizdatPlugins)) { - self.SamizdatPlugins = new Array() -} + // initialize the SamizdatPlugins array + if (!Array.isArray(self.SamizdatPlugins)) { + self.SamizdatPlugins = new Array() + } + + // and add ourselves to it + // with some additional metadata + self.SamizdatPlugins.push({ + name: 'ipns+ipfs', + description: 'Decentralized resource fetching using IPNS for address resolution and IPFS for content delivery.', + version: 'COMMIT_UNKNOWN', + fetch: getContentFromIPNSAndIPFS, + publish: publishContent + }) -// and add ourselves to it -// with some additional metadata -self.SamizdatPlugins.push({ - name: 'ipns+ipfs', - description: 'Decentralized resource fetching using IPNS for address resolution and IPFS for content delivery.', - version: 'COMMIT_UNKNOWN', - fetch: getContentFromIPNSAndIPFS, - publish: publishContent -}) +// done with not poluting the global namespace +})() diff --git a/service-worker.js b/service-worker.js index 1378414810dbe764ad5d72c1690409e37f3c2003..011202d61e03553d0a456a61c0885a252efcce0c 100644 --- a/service-worker.js +++ b/service-worker.js @@ -1,7 +1,7 @@ /* * Samizdat Service Worker. * - * Strategy (not fully implemented yet): + * Default strategy: * 1. Try to load from main website. * 2. If loading fails, load from Samizdat. * 3. If loading is too slow, load from Samizdat. @@ -14,17 +14,37 @@ if (!Array.isArray(self.SamizdatPlugins)) { self.SamizdatPlugins = new Array() } +// initialize the SamizdatConfig array +// +// this also sets some sane defaults, +// which then can be modified via config.js +if (typeof self.SamizdatConfig !== 'object' || self.SamizdatConfig === null) { + self.SamizdatConfig = { + // how long do we wait before we decide that a plugin is unresponsive, + // and move on? + defaultPluginTimeout: 10000, + // plugins settings namespace + plugins: {} + } +} + // load the plugins // // order in which plugins are loaded defines the order // in which they are called! -self.importScripts( +try { + self.importScripts( + "./config.js", "./plugins/fetch.js", "./plugins/cache.js", "./plugins/gateway-ipns.js", "./plugins/gun-ipfs.js"); - -console.log('(COMMIT_UNKNOWN) SamizdatPlugins.length:', self.SamizdatPlugins.length) +} catch(e) { + // we only get a cryptic "Error while registering a service worker" + // unless we explicitly print the errors out in the console + console.error(e) + throw e +} /** * fetch counter per clientId @@ -246,7 +266,11 @@ let samizdatFetch = (plugin, url, reqInfo) => { // run the plugin return Promise.race([ plugin.fetch(url), - promiseTimeout(10000, false, `Samizdat request using ${plugin.name} timed out.`) + promiseTimeout( + self.SamizdatConfig.defaultPluginTimeout, + false, + `Samizdat request using ${plugin.name} timed out after ${self.SamizdatConfig.defaultPluginTimeout}ms.` + ) ]) }