class ServerSet { constructor(servers, unreachable_neighbors) { if (!servers || !Array.isArray(servers)) { throw new Error('no servers') } if (!unreachable_neighbors || typeof unreachable_neighbors.queryUnreachable !== 'function' || typeof unreachable_neighbors.unreachable !== 'function') { throw new Error('no unreachable_neighbors') } this.servers = [...new Set(servers)] // deduplicate this.unreachable_neighbors = unreachable_neighbors; } add(server) { console.log('adding server', server) if (!server || typeof server !== 'string') { throw new Error('server must be a string') } if (server in this.servers) { console.log('server already in set', server) return } this.servers.push(server); } async get(auth, target) { if (!auth || typeof auth.buildAuthHeader !== 'function') { throw new Error('no auth') } for (const server of this.servers) { try { if (this.unreachable_neighbors.queryUnreachable(server)) { continue } const url = "https://" + server + target // TODO https return await fetch(url, { method: 'GET', headers: { ...auth.buildAuthHeader(url) }, credentials: 'omit' }).catch(err => { console.error('get from server failed', server, err) this.unreachable_neighbors.unreachable(server) } ).then(response => response.json()) } catch (e) { console.error('get from server failed', server, e) } } throw new Error('all servers failed') } async post(auth, target, data) { if (!auth || typeof auth.buildAuthHeader !== 'function') { throw new Error('no auth') } for (const server of this.servers) { try { if (this.unreachable_neighbors.queryUnreachable(server)) { continue } const url = "https://" + server + target // TODO https return await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...auth.buildAuthHeader(url, data) }, credentials: 'omit', body: JSON.stringify(data) }).catch(err => { console.error('post to server failed', server, err) this.unreachable_neighbors.unreachable(server) } ).then(response => response.json()) } catch (e) { console.error('post to server failed', server, e) } } throw new Error('all servers failed') } async patch(auth, target, data) { if (!auth || typeof auth.buildAuthHeader !== 'function') { throw new Error('no auth') } for (const server of this.servers) { try { if (this.unreachable_neighbors.queryUnreachable(server)) { continue } const url = "https://" + server + target // TODO https return await fetch(url, { method: 'PATCH', headers: { 'Content-Type': 'application/json', ...auth.buildAuthHeader(url, data) }, credentials: 'omit', body: JSON.stringify(data) }).catch(err => { console.error('patch to server failed', server, err) this.unreachable_neighbors.unreachable(server) } ).then(response => response.json()) } catch (e) { console.error('patch to server failed', server, e) } } throw new Error('all servers failed') } async put(auth, target, data) { if (!auth || typeof auth.buildAuthHeader !== 'function') { throw new Error('no auth') } for (const server of this.servers) { try { if (this.unreachable_neighbors.queryUnreachable(server)) { continue } const url = "https://" + server + target // TODO https return await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json', ...auth.buildAuthHeader(url, data) }, credentials: 'omit', body: JSON.stringify(data) }).catch(err => { console.error('put to server failed', server, err) this.unreachable_neighbors.unreachable(server) } ).then(response => response.json()) } catch (e) { console.error('put to server failed', server, e) } } throw new Error('all servers failed') } async delete(auth, target) { if (!auth || typeof auth.buildAuthHeader !== 'function') { throw new Error('no auth') } for (const server of this.servers) { try { if (this.unreachable_neighbors.queryUnreachable(server)) { continue } const url = "https://" + server + target // TODO https return await fetch(url, { method: 'DELETE', headers: { ...auth.buildAuthHeader(url) }, credentials: 'omit' }).catch(err => { console.error('delete from server failed', server, err) this.unreachable_neighbors.unreachable(server) } ) } catch (e) { console.error('delete from server failed', server, e) } } throw new Error('all servers failed') } async getRaw(auth, target) { if (!auth || typeof auth.buildAuthHeader !== 'function') { throw new Error('no auth') } for (const server of this.servers) { try { if (this.unreachable_neighbors.queryUnreachable(server)) { continue } const url = "https://" + server + target // TODO https return await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json', ...auth.buildAuthHeader(url, data) }, credentials: 'omit' }).catch(err => { console.error('get from server failed', server, err) this.unreachable_neighbors.unreachable(server) } ) } catch (e) { console.error('put to server failed', server, e) } } throw new Error('all servers failed') } async delete(auth, target) { if (!auth || typeof auth.buildAuthHeader !== 'function') { throw new Error('no auth') } for (const server of this.servers) { try { if (this.unreachable_neighbors.queryUnreachable(server)) { continue } const url = "https://" + server + target // TODO https return await fetch(url, { method: 'DELETE', headers: { ...auth.buildAuthHeader(url) }, credentials: 'omit' }).catch(err => { console.error('delete from server failed', server, err) this.unreachable_neighbors.unreachable(server) } ) } catch (e) { console.error('delete from server failed', server, e) } } throw new Error('all servers failed') } } class ServerSetUnion { constructor(serverSets) { if (!serverSets || !Array.isArray(serverSets)) { throw new Error('no serverSets') } this.serverSets = serverSets; } add(serverset) { if (!serverset || !(serverset instanceof ServerSet)) { throw new Error('no serverset') } if (this.serverSets.find(s => serverset.servers.every(s2 => s.servers.includes(s2)))) { console.warn('serverset already in union', serverset) return } this.serverSets.push(serverset) } async get(auth, target) { try { return await this.serverSets.reduce(async (acc, serverset) => { return acc.then(async (acc) => { return acc.concat(await serverset.get(auth, target)) }) }, Promise.resolve([])) } catch (e) { throw new Error('all servers failed') } } async post(auth, target, data) { try { return await this.serverSets.reduce(async (acc, serverset) => { return acc.then(async (acc) => { return acc.concat(await serverset.post(auth, target, data)) }) }, Promise.resolve([])) } catch (e) { throw new Error('all servers failed') } } async patch(auth, target, data) { try { return await this.serverSets.reduce(async (acc, serverset) => { return acc.then(async (acc) => { return acc.concat(await serverset.patch(auth, target, data)) }) }, Promise.resolve([])) } catch (e) { throw new Error('all servers failed') } } async put(auth, target, data) { try { return await this.serverSets.reduce(async (acc, serverset) => { return acc.then(async (acc) => { return acc.concat(await serverset.put(auth, target, data)) }) }, Promise.resolve([])) } catch (e) { throw new Error('all servers failed') } } async delete(auth, target) { try { return await this.serverSets.reduce(async (acc, serverset) => { return acc.then(async (acc) => { return acc.concat(await serverset.delete(auth, target)) }) }, Promise.resolve([])) } catch (e) { throw new Error('all servers failed') } } } class ServerSetUnion { constructor(serverSets) { if (!serverSets || !Array.isArray(serverSets)) { throw new Error('no serverSets') } this.serverSets = serverSets; } add(serverset) { if (!serverset || !(serverset instanceof ServerSet)) { throw new Error('no serverset') } if (this.serverSets.find(s => serverset.servers.every(s2 => s.servers.includes(s2)))) { console.warn('serverset already in union', serverset) return } this.serverSets.push(serverset) } async get(auth, target) { try { return await this.serverSets.reduce(async (acc, serverset) => { return acc.then(async (acc) => { return acc.concat(await serverset.get(auth, target)) }) }, Promise.resolve([])) } catch (e) { throw new Error('all servers failed') } } async post(auth, target, data) { try { return await this.serverSets.reduce(async (acc, serverset) => { return acc.then(async (acc) => { return acc.concat(await serverset.post(auth, target, data)) }) }, Promise.resolve([])) } catch (e) { throw new Error('all servers failed') } } async patch(auth, target, data) { try { return await this.serverSets.reduce(async (acc, serverset) => { return acc.then(async (acc) => { return acc.concat(await serverset.patch(auth, target, data)) }) }, Promise.resolve([])) } catch (e) { throw new Error('all servers failed') } } async put(auth, target, data) { try { return await this.serverSets.reduce(async (acc, serverset) => { return acc.then(async (acc) => { return acc.concat(await serverset.put(auth, target, data)) }) }, Promise.resolve([])) } catch (e) { throw new Error('all servers failed') } } async delete(auth, target) { try { return await this.serverSets.reduce(async (acc, serverset) => { return acc.then(async (acc) => { return acc.concat(await serverset.delete(auth, target)) }) }, Promise.resolve([])) } catch (e) { throw new Error('all servers failed') } } } class authMethod { constructor(method, auth) { this.method = method; this.auth = auth; } buildAuthHeader(url, data) { return this.method(this.auth, {url, data}) } } function createSignAuth(username, signKey) { const context = {username, signKey} if (!context.signKey || !context.username || typeof context.username !== 'string' || !(context.signKey instanceof Uint8Array) || context.signKey.length !== 64) { throw new Error('no signKey or username') } return new authMethod(({signKey, username}, {url, data}) => { const json = JSON.stringify(data) const signature = nacl.crypto_sign_detached(nacl.encode_utf8(url + (data ? json : "")), signKey) return {'Authorization': 'Signature ' + username + ':' + nacl.to_hex(signature)} }, context) } function createTokenAuth(token) { const context = {token} if (!context.token) { throw new Error('no token') } return new authMethod(({token}, {url, data}) => { return {'Authorization': 'Token ' + token} }, context) } function createNullAuth() { return new authMethod(() => { return {} }, {}) } export {ServerSet, ServerSetUnion, createSignAuth, createTokenAuth, createNullAuth};