This commit is contained in:
j3d1 2023-05-16 00:43:33 +02:00
parent 21cb4018a3
commit 06f7c515ef
4 changed files with 237 additions and 83 deletions

107
frontend/src/federation.js Normal file
View file

@ -0,0 +1,107 @@
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 = servers;
this.unreachable_neighbors = unreachable_neighbors;
}
add(server) {
this.servers.push(server);
}
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 = "http://" + server + target // TODO https
return await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...auth.buildAuthHeader(url)
},
credentials: 'omit',
body: JSON.stringify(data)
}).catch(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 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 = "http://" + server + target // TODO https
return await fetch(url, {
method: 'GET',
headers: {
...auth.buildAuthHeader(url)
},
credentials: 'omit'
}).catch(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')
}
}
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)
}
export {ServerSet, createSignAuth, createTokenAuth}

View file

@ -2,7 +2,7 @@ import {createStore} from 'vuex';
import router from '@/router'; import router from '@/router';
import FallBackResolver from "@/dns"; import FallBackResolver from "@/dns";
import NeighborsCache from "@/neigbors"; import NeighborsCache from "@/neigbors";
import {useRoute} from "vue-router"; import {createSignAuth, createTokenAuth, ServerSet} from "@/federation";
export default createStore({ export default createStore({
@ -15,7 +15,8 @@ export default createStore({
item_map: {}, item_map: {},
//notifications: [], //notifications: [],
messages: [], messages: [],
home_server: null, home_servers: null,
all_friends_servers: null,
resolver: new FallBackResolver(), resolver: new FallBackResolver(),
unreachable_neighbors: new NeighborsCache(), unreachable_neighbors: new NeighborsCache(),
}, },
@ -45,11 +46,20 @@ export default createStore({
localStorage.setItem('remember', remember); localStorage.setItem('remember', remember);
}, },
setInventoryItems(state, {url, items}) { setInventoryItems(state, {url, items}) {
console.log('setInventoryItems', url, items)
state.item_map[url] = items; state.item_map[url] = items;
}, },
setFriends(state, friends) { setFriends(state, friends) {
state.friends = friends; state.friends = friends;
}, },
setHomeServers(state, home_servers) {
console.log('setHomeServer', home_servers)
state.home_servers = home_servers;
},
setAllFriendsServers(state, servers) {
console.log('setAllFriendsServers', servers)
state.all_friends_servers = servers;
},
logout(state) { logout(state) {
state.user = null; state.user = null;
state.token = null; state.token = null;
@ -72,6 +82,7 @@ export default createStore({
} else { } else {
} }
router.push('/'); router.push('/');
/*if (this.$route.query.redirect) { /*if (this.$route.query.redirect) {
router.push({path: this.$route.query.redirect}); router.push({path: this.$route.query.redirect});
} else { } else {
@ -83,33 +94,30 @@ export default createStore({
actions: { actions: {
async login({commit, dispatch, state}, {username, password, remember}) { async login({commit, dispatch, state}, {username, password, remember}) {
commit('setRemember', remember); commit('setRemember', remember);
const data = await dispatch('apiLocalPost', { const data = await fetch('/auth/token/', {
target: '/auth/token/', data: { method: 'POST',
username: username, password: password headers: {'Content-Type': 'application/json'},
} body: JSON.stringify({username: username, password: password}),
}) credentials: 'omit'
}).then(r => r.json())
if (data.token) { if (data.token) {
commit('setToken', data.token); commit('setToken', data.token);
commit('setUser', username); commit('setUser', username);
const j = await dispatch('apiLocalGet', {target: '/auth/keys/'}) const j = await fetch('/auth/keys/', {
method: 'GET',
headers: {'Authorization': 'Token ' + data.token},
credentials: 'omit'
}).then(r => r.json())
const k = j.key const k = j.key
commit('setKey', k) commit('setKey', k)
const s = await dispatch('lookupServer', {username}).then(servers => new ServerSet(servers, state.unreachable_neighbors))
commit('setHomeServers', s)
return true; return true;
} else { } else {
return false; return false;
} }
}, },
async getFriends({commit, dispatch, state}) { async lookupServer({state}, {username}) {
const home_server = "localhost:8000"
const data = await dispatch('apiFederatedGet', {
host: home_server,
target: '/api/friends/'
})
console.log('getFriends', data)
commit('setFriends', data)
return data
},
async getFriendServer({state}, {username}) {
const domain = username.split('@')[1] const domain = username.split('@')[1]
if (domain === 'example.eleon') if (domain === 'example.eleon')
return ['10.23.42.186:8000']; return ['10.23.42.186:8000'];
@ -126,7 +134,31 @@ export default createStore({
(result) => result.map( (result) => result.map(
(answer) => answer.target + ':' + answer.port)) (answer) => answer.target + ':' + answer.port))
}, },
async apiFederatedGet({state}, {host, target}) { async getHomeServers({state, dispatch, commit}) {
if (state.home_servers)
return state.home_servers
const promise = dispatch('lookupServer', {username: state.user}).then(servers => new ServerSet(servers, state.unreachable_neighbors))
commit('setHomeServers', promise)
return promise
},
async getAllFriendsServers({state, dispatch, commit}) {
if (state.all_friends_servers)
return state.all_friends_servers
const promise = (async () => {
const servers = new ServerSet([], state.unreachable_neighbors)
for (const friend of state.friends) {
const s = await dispatch('lookupServer', {username: friend})
servers.add(s)
}
return servers
})()
commit('setAllFriendsServers', promise)
return promise
},
async getFriendServers({state, dispatch, commit}, {username}) {
return dispatch('lookupServer', {username}).then(servers => new ServerSet(servers, state.unreachable_neighbors))
},
/*async apiFederatedGet({state}, {host, target}) {
if (state.unreachable_neighbors.queryUnreachable(host)) { if (state.unreachable_neighbors.queryUnreachable(host)) {
throw new Error('unreachable neighbor') throw new Error('unreachable neighbor')
} }
@ -185,73 +217,99 @@ export default createStore({
credentials: 'omit', credentials: 'omit',
body: JSON.stringify(data) body: JSON.stringify(data)
}).then(response => response.json()) }).then(response => response.json())
},*/
async fetchInventoryItems({commit, dispatch, getters}) {
const servers = await dispatch('getHomeServers')
const items = await servers.get(getters.signAuth, '/api/inventory_items/')
commit('setInventoryItems', {url: '/', items})
return items
}, },
async requestFriend({state, dispatch}, {username}) { async searchInventories({state, dispatch, getters}, {query}) {
const servers = await dispatch('getAllFriendsServers')
return await servers.get(getters.signAuth, '/api/inventory/search/?q=' + query)
},
/*async searchInventoryItems() {
try {
const servers = await this.fetchFriends().then(friends => friends.map(friend => this.lookupServer({username: friend.name})))
const urls = servers.map(server => server.then(s => {
return {host: s, target: "/api/inventory_items/"}
}))
urls.map(url => url.then(u => this.apiFederatedGet(u).then(items => {
this.setInventoryItems({url: u.domain, items})
}).catch(e => {
}))) // TODO: handle error
} catch (e) {
console.error(e)
}
},*/
async fetchFriends({commit, dispatch, getters, state}) {
const servers = await dispatch('getHomeServers')
const data = await servers.get(getters.signAuth, '/api/friends/')
commit('setFriends', data)
return data
},
async fetchFriendRequests({state, dispatch, getters}) {
const servers = await dispatch('getHomeServers')
return await servers.get(getters.signAuth, '/api/friendrequests/')
},
async requestFriend({state, dispatch, getters}, {username}) {
console.log('requesting friend ' + username) console.log('requesting friend ' + username)
if (username in state.friends) { if (username in state.friends) {
return true; return true;
} }
state.home_server = 'localhost:8000' const home_servers = await dispatch('getHomeServers')
const home_reply = await dispatch('apiFederatedPost', { const home_reply = home_servers.post(getters.signAuth, '/api/friendrequests/', {
host: state.home_server, befriender: state.user,
target: '/api/friendrequests/', befriendee: username
data: {befriender: state.user, befriendee: username}
}) })
if (home_reply.status !== 'pending' || !home_reply.secret) if (home_reply.status !== 'pending' || !home_reply.secret)
return false; return false;
console.log('home_reply', home_reply) console.log('home_reply', home_reply)
const befriendee_server = await dispatch('getFriendServer', {username}) const befriendee_servers = await dispatch('getFriendServers', {username})
const ext_reply = await dispatch('apiFederatedPost', { const ext_reply = befriendee_servers.post(getters.signAuth, '/api/friendrequests/', {
host: befriendee_server[0],
target: '/api/friendrequests/',
data: {
befriender: state.user, befriender: state.user,
befriendee: username, befriendee: username,
befriender_key: nacl.to_hex(state.keypair.signPk), befriender_key: nacl.to_hex(state.keypair.signPk),
secret: home_reply.secret secret: home_reply.secret
}
}) })
console.log('ext_reply', ext_reply) console.log('ext_reply', ext_reply)
return true; return true;
}, },
async acceptFriend({state, dispatch}, {id, secret, befriender}) { async acceptFriend({state, dispatch, getters}, {id, secret, befriender}) {
console.log('accepting friend ' + id) console.log('accepting friend ' + id)
state.home_server = 'localhost:8000' const home_servers = await dispatch('getHomeServers')
const home_reply = await dispatch('apiFederatedPost', { const home_reply = await home_servers.post(getters.signAuth, '/api/friends/', {
host: state.home_server,
target: '/api/friends/',
data: {
friend_request_id: id, secret: secret friend_request_id: id, secret: secret
}
}) })
console.log('home_reply', home_reply) console.log('home_reply', home_reply)
const ext_server = await dispatch('getFriendServer', {username: befriender}) const ext_servers = await dispatch('getFriendServers', {username: befriender})
const ext_reply = await dispatch('apiFederatedPost', { const ext_reply = await ext_servers.post(getters.signAuth, '/api/friendrequests/', {
host: ext_server[0],
target: '/api/friendrequests/',
data: {
befriender: state.user, befriender: state.user,
befriendee: befriender, befriendee: befriender,
befriender_key: nacl.to_hex(state.keypair.signPk), befriender_key: nacl.to_hex(state.keypair.signPk),
secret: secret secret: secret
}
}) })
console.log('ext_reply', ext_reply) console.log('ext_reply', ext_reply)
return true return true
}, },
async declineFriend({state, dispatch}, args) { async declineFriend({state, dispatch}, args) {
// TODO implement
console.log('declining friend ' + args) console.log('declining friend ' + args)
}, },
async fetchFriendRequests({state, dispatch}) {
const requests = await dispatch('apiLocalGet', {target: '/api/friendrequests/'})
return requests
}
}, },
getters: { getters: {
isLoggedIn(state) { isLoggedIn(state) {
return state.user !== null && state.token !== null; return state.user !== null && state.token !== null;
}, },
signAuth(state) {
console.log('signAuth', state.user, state.keypair.signSk)
return createSignAuth(state.user, state.keypair.signSk)
},
tokenAuth(state) {
console.log('tokenAuth', state.token)
return createTokenAuth(state.token)
},
inventory_items(state) { inventory_items(state) {
return Object.entries(state.item_map).reduce((acc, [url, items]) => { return Object.entries(state.item_map).reduce((acc, [url, items]) => {
return acc.concat(items) return acc.concat(items)

View file

@ -78,7 +78,8 @@
<tr v-for="request in requests" :key="request.befriender"> <tr v-for="request in requests" :key="request.befriender">
<td>{{ request.befriender }}</td> <td>{{ request.befriender }}</td>
<td class="d-none d-md-table-cell"> <td class="d-none d-md-table-cell">
{{ request.befriender_public_key.slice(0,32) }}...</td> {{ request.befriender_public_key.slice(0, 32) }}...
</td>
<td class="table-action"> <td class="table-action">
<button class="btn btn-sm btn-success" @click="tryAcceptFriend(request)"> <button class="btn btn-sm btn-success" @click="tryAcceptFriend(request)">
<b-icon-check></b-icon-check> <b-icon-check></b-icon-check>
@ -128,11 +129,11 @@ export default {
} }
}, },
methods: { methods: {
...mapActions(['getFriends', "getFriendServer", "requestFriend", "acceptFriend", "fetchFriendRequests", "declineFriend"]), ...mapActions(['fetchFriends', "lookupServer", "requestFriend", "acceptFriend", "fetchFriendRequests", "declineFriend"]),
fetchContent() { fetchContent() {
this.getFriends().then((friends) => { this.fetchFriends().then((friends) => {
friends.map((friend) => { friends.map((friend) => {
this.getFriendServer(friend).then((server) => { this.lookupServer(friend).then((server) => {
this.friends[friend.username] = {...friend, server: server} this.friends[friend.username] = {...friend, server: server}
}) })
}) })
@ -151,21 +152,24 @@ export default {
this.newfriend = "" this.newfriend = ""
this.fetchContent() this.fetchContent()
} }
}).catch(() => {}) }).catch(() => {
})
}, },
tryAcceptFriend(request) { tryAcceptFriend(request) {
this.acceptFriend({id: request.id, secret: request.secret, befriender: request.befriender}).then((ok) => { this.acceptFriend({id: request.id, secret: request.secret, befriender: request.befriender}).then((ok) => {
if (ok) { if (ok) {
this.fetchContent() this.fetchContent()
} }
}).catch(() => {}) }).catch(() => {
})
}, },
tryRejectFriend(friend) { tryRejectFriend(friend) {
this.declineFriend({username: friend}).then((ok) => { this.declineFriend({username: friend}).then((ok) => {
if (ok) { if (ok) {
this.fetchContent() this.fetchContent()
} }
}).catch(() => {}) }).catch(() => {
})
} }
}, },
mounted() { mounted() {

View file

@ -39,7 +39,7 @@
</table> </table>
</div> </div>
<div class="card"> <div class="card">
<button class="btn" @click="getInventoryItems">Refresh</button> <button class="btn" @click="fetchInventoryItems">Refresh</button>
<router-link to="/inventory/new" class="btn btn-primary">Add</router-link> <router-link to="/inventory/new" class="btn btn-primary">Add</router-link>
</div> </div>
</div> </div>
@ -67,25 +67,10 @@ export default {
} }
}, },
methods: { methods: {
...mapActions(["apiFederatedGet", "getFriends", "getFriendServer"]), ...mapActions(["fetchInventoryItems"]),
...mapMutations(["setInventoryItems"]),
async getInventoryItems() {
try {
const servers = await this.getFriends().then(friends => friends.map(friend => this.getFriendServer({username: friend})))
const urls = servers.map(server => server.then(s => {
return {host: s, target: "/api/inventory_items/"}
}))
urls.map(url => url.then(u => this.apiFederatedGet(u).then(items => {
this.setInventoryItems({url: u.domain, items})
}).catch(e => {
}))) // TODO: handle error
} catch (e) {
console.error(e)
}
},
}, },
async mounted() { async mounted() {
await this.getInventoryItems() await this.fetchInventoryItems()
} }
} }
</script> </script>