434 lines
15 KiB
JavaScript
434 lines
15 KiB
JavaScript
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};
|
|
|
|
|