Merge pull request #41 from shocknet/gun-issues

Gun issues
This commit is contained in:
Daniel Lugo 2020-02-26 17:39:40 -04:00 committed by GitHub
commit 266c43c9a4
6 changed files with 375 additions and 239 deletions

View file

@ -77,7 +77,9 @@
"line-comment-position": "off",
// if someone does this it's probably intentional
"no-useless-concat": "off"
"no-useless-concat": "off",
"no-plusplus": "off"
},
"parser": "babel-eslint",
"env": {

View file

@ -2,6 +2,7 @@
* @format
*/
const Gun = require('gun')
// @ts-ignore
Gun.log = () => {}
// @ts-ignore
require('gun/lib/open')
@ -12,6 +13,8 @@ const logger = require('winston')
/** @type {import('../contact-api/SimpleGUN').ISEA} */
// @ts-ignore
const SEAx = require('gun/sea')
// @ts-ignore
SEAx.throw = true
/** @type {import('../contact-api/SimpleGUN').ISEA} */
const mySEA = {}
@ -36,6 +39,20 @@ mySEA.encrypt = (msg, secret) => {
)
}
if (typeof secret !== 'string') {
throw new TypeError(
`mySEA.encrypt() -> expected secret to be a an string, args: |msg| -- ${JSON.stringify(
secret
)}`
)
}
if (secret.length < 1) {
throw new TypeError(
`mySEA.encrypt() -> expected secret to be a populated string`
)
}
// Avoid this: https://github.com/amark/gun/issues/804 and any other issues
const sanitizedMsg = $$__SHOCKWALLET__MSG__ + msg
@ -87,23 +104,69 @@ mySEA.decrypt = (encMsg, secret) => {
})
}
mySEA.secret = (recipientOrSenderEpub, recipientOrSenderSEA) => {
mySEA.secret = async (recipientOrSenderEpub, recipientOrSenderSEA) => {
if (typeof recipientOrSenderEpub !== 'string') {
throw new TypeError('epub has to be an string')
throw new TypeError(
'epub has to be an string, args:' +
`${JSON.stringify(recipientOrSenderEpub)} -- ${JSON.stringify(
recipientOrSenderSEA
)}`
)
}
if (recipientOrSenderEpub.length === 0) {
throw new TypeError(
'epub has to be populated string, args: ' +
`${JSON.stringify(recipientOrSenderEpub)} -- ${JSON.stringify(
recipientOrSenderSEA
)}`
)
}
if (typeof recipientOrSenderSEA !== 'object') {
throw new TypeError('sea has to be an object')
throw new TypeError(
'sea has to be an object, args: ' +
`${JSON.stringify(recipientOrSenderEpub)} -- ${JSON.stringify(
recipientOrSenderSEA
)}`
)
}
if (recipientOrSenderEpub === recipientOrSenderSEA.pub) {
throw new Error('Do not use pub for mysecret')
}
return SEAx.secret(recipientOrSenderEpub, recipientOrSenderSEA).then(sec => {
if (typeof sec !== 'string') {
throw new TypeError('Could not generate secret')
}
return sec
})
if (recipientOrSenderSEA === null) {
throw new TypeError(
'sea has to be nont null, args: ' +
`${JSON.stringify(recipientOrSenderEpub)} -- ${JSON.stringify(
recipientOrSenderSEA
)}`
)
}
if (recipientOrSenderEpub === recipientOrSenderSEA.pub) {
throw new Error(
'Do not use pub for mysecret, args: ' +
`${JSON.stringify(recipientOrSenderEpub)} -- ${JSON.stringify(
recipientOrSenderSEA
)}`
)
}
const sec = await SEAx.secret(recipientOrSenderEpub, recipientOrSenderSEA)
if (typeof sec !== 'string') {
throw new TypeError(
`Could not generate secret, args: ${JSON.stringify(
recipientOrSenderEpub
)} -- ${JSON.stringify(recipientOrSenderSEA)}`
)
}
if (sec.length === 0) {
throw new TypeError(
`SEA.secret returned an empty string!, args: ${JSON.stringify(
recipientOrSenderEpub
)} -- ${JSON.stringify(recipientOrSenderSEA)}`
)
}
return sec
}
const auth = require('../../auth/auth')
@ -127,10 +190,17 @@ const Event = require('../event-constants')
* @prop {Record<string, any>} origBody
*/
/**
* @typedef {object} EncryptedEmission
* @prop {string} encryptedData
* @prop {string} encryptedKey
* @prop {string} iv
*/
// TO DO: move to common repo
/**
* @typedef {object} SimpleSocket
* @prop {(eventName: string, data: Emission) => void} emit
* @prop {(eventName: string, data: Emission|EncryptedEmission) => void} emit
* @prop {(eventName: string, handler: (data: any) => void) => void} on
* @prop {{ query: { 'x-shockwallet-device-id': string }}} handshake
*/
@ -146,13 +216,16 @@ let user
/* eslint-enable init-declarations */
let _currentAlias = ''
let _currentPass = ''
/** @type {string|null} */
let _currentAlias = null
/** @type {string|null} */
let _currentPass = null
let mySec = ''
/** @type {string|null} */
let mySec = null
/** @returns {string} */
const getMySecret = () => mySec
const getMySecret = () => /** @type {string} */ (mySec)
let _isAuthenticating = false
let _isRegistering = false
@ -161,18 +234,43 @@ const isAuthenticated = () => typeof user.is === 'object' && user.is !== null
const isAuthenticating = () => _isAuthenticating
const isRegistering = () => _isRegistering
const getGun = () => {
return gun
}
const getUser = () => {
return user
}
/**
* Returns a promise containing the public key of the newly created user.
* @param {string} alias
* @param {string} pass
* @param {UserGUNNode=} user
* @returns {Promise<string>}
*/
const authenticate = async (alias, pass) => {
const authenticate = async (alias, pass, user = getUser()) => {
const isFreshGun = user !== getUser()
if (isFreshGun) {
const ack = await new Promise(res => {
user.auth(alias, pass, _ack => {
res(_ack)
})
})
if (typeof ack.err === 'string') {
throw new Error(ack.err)
} else if (typeof ack.sea === 'object') {
return ack.sea.pub
} else {
throw new Error('Unknown error.')
}
}
if (isAuthenticated()) {
const currAlias = user.is && user.is.alias
if (alias !== currAlias) {
if (alias !== _currentAlias) {
throw new Error(
`Tried to re-authenticate with an alias different to that of stored one, tried: ${alias} - stored: ${currAlias}, logoff first if need to change aliases.`
`Tried to re-authenticate with an alias different to that of stored one, tried: ${alias} - stored: ${_currentAlias}, logoff first if need to change aliases.`
)
}
// move this to a subscription; implement off() ? todo
@ -202,7 +300,7 @@ const authenticate = async (alias, pass) => {
} else if (typeof ack.sea === 'object') {
mySec = await mySEA.secret(user._.sea.epub, user._.sea)
_currentAlias = user.is ? user.is.alias : ''
_currentAlias = alias
_currentPass = await mySEA.encrypt(pass, mySec)
await new Promise(res => setTimeout(res, 5000))
@ -220,47 +318,44 @@ const logoff = () => {
user.leave()
}
const instantiateGun = async () => {
let mySecret = ''
if (user && user.is) {
mySecret = /** @type {string} */ (await mySEA.secret(
user._.sea.epub,
user._.sea
))
}
if (typeof mySecret !== 'string') {
throw new TypeError("typeof mySec !== 'string'")
}
const _gun = new Gun({
const instantiateGun = () => {
const _gun = /** @type {unknown} */ (new Gun({
axe: false,
peers: Config.PEERS
})
}))
// please typescript
const __gun = /** @type {unknown} */ (_gun)
gun = /** @type {GUNNode} */ (_gun)
gun = /** @type {GUNNode} */ (__gun)
// eslint-disable-next-line require-atomic-updates
user = gun.user()
if (_currentAlias && _currentPass) {
const pass = await mySEA.decrypt(_currentPass, mySecret)
if (typeof pass !== 'string') {
throw new Error('could not decrypt stored in memory current pass')
}
user.leave()
await authenticate(_currentAlias, pass)
}
}
instantiateGun()
const freshGun = async () => {
const _gun = /** @type {unknown} */ (new Gun({
axe: false,
peers: Config.PEERS
}))
const gun = /** @type {GUNNode} */ (_gun)
const user = gun.user()
if (!_currentAlias || !_currentPass || !mySec) {
throw new Error('Called freshGun() without alias, pass and secret cached')
}
const pass = await mySEA.decrypt(_currentPass, mySec)
if (typeof pass !== 'string') {
throw new Error('could not decrypt stored in memory current pass')
}
await authenticate(_currentAlias, pass, user)
return { gun, user }
}
/**
* @param {string} token
* @returns {Promise<boolean>}
@ -296,14 +391,6 @@ const throwOnInvalidToken = async token => {
}
}
const getGun = () => {
return gun
}
const getUser = () => {
return user
}
class Mediator {
/**
* @param {Readonly<SimpleSocket>} socket
@ -1144,9 +1231,7 @@ const register = async (alias, pass) => {
// restart instances so write to user graph work, there's an issue with gun
// (at least on node) where after initial user creation, writes to user graph
// don't work
await instantiateGun()
user.leave()
instantiateGun()
return authenticate(alias, pass).then(async pub => {
await API.Actions.setDisplayName('anon' + pub.slice(0, 8), user)
@ -1179,9 +1264,9 @@ module.exports = {
isAuthenticating,
isRegistering,
register,
instantiateGun,
getGun,
getUser,
mySEA,
getMySecret
getMySecret,
freshGun
}

View file

@ -57,22 +57,12 @@ const __createOutgoingFeed = async (withPublicKey, user, SEA) => {
user._.sea
)
const maybeEncryptedForMeOutgoingFeedID = await Utils.tryAndWait(
(_, user) =>
new Promise(res => {
user
.get(Key.RECIPIENT_TO_OUTGOING)
.get(withPublicKey)
.once(data => {
res(data)
})
})
)
const maybeOutgoingID = await Utils.recipientToOutgoingID(withPublicKey)
let outgoingFeedID = ''
// if there was no stored outgoing, create an outgoing feed
if (typeof maybeEncryptedForMeOutgoingFeedID !== 'string') {
if (typeof maybeOutgoingID !== 'string') {
/** @type {PartialOutgoing} */
const newPartialOutgoingFeed = {
with: encryptedForMeRecipientPub
@ -138,18 +128,7 @@ const __createOutgoingFeed = async (withPublicKey, user, SEA) => {
// otherwise decrypt stored outgoing
else {
const decryptedOID = await SEA.decrypt(
maybeEncryptedForMeOutgoingFeedID,
mySecret
)
if (typeof decryptedOID !== 'string') {
throw new TypeError(
"__createOutgoingFeed() -> typeof decryptedOID !== 'string'"
)
}
outgoingFeedID = decryptedOID
outgoingFeedID = maybeOutgoingID
}
if (typeof outgoingFeedID === 'undefined') {
@ -539,8 +518,8 @@ const sendHandshakeRequest = async (recipientPublicKey, gun, user, SEA) => {
const lastRequestIDSentToUser = maybeLastRequestIDSentToUser
console.log('sendHR() -> before alreadyContactedOnCurrHandshakeNode')
/** @type {boolean} */
const alreadyContactedOnCurrHandshakeNode = await Utils.tryAndWait(
const hrInHandshakeNode = await Utils.tryAndWait(
gun =>
new Promise(res => {
gun
@ -548,11 +527,16 @@ const sendHandshakeRequest = async (recipientPublicKey, gun, user, SEA) => {
.get(currentHandshakeAddress)
.get(lastRequestIDSentToUser)
.once(data => {
res(typeof data !== 'undefined')
res(data)
})
})
}),
// force retry on undefined in case the undefined was a false negative
v => typeof v === 'undefined'
)
const alreadyContactedOnCurrHandshakeNode =
typeof hrInHandshakeNode !== 'undefined'
if (alreadyContactedOnCurrHandshakeNode) {
throw new Error(ErrorCode.ALREADY_REQUESTED_HANDSHAKE)
}

View file

@ -1,6 +1,8 @@
/**
* @format
*/
const logger = require('winston')
const ErrorCode = require('../errorCode')
const Key = require('../key')
const Schema = require('../schema')
@ -12,6 +14,8 @@ const Utils = require('../utils')
* @typedef {import('../SimpleGUN').UserGUNNode} UserGUNNode
*/
let procid = 0
/**
* @throws {Error} NOT_AUTH
* @param {UserGUNNode} user
@ -23,17 +27,16 @@ const onAcceptedRequests = (user, SEA) => {
throw new Error(ErrorCode.NOT_AUTH)
}
const mySecret = require('../../Mediator').getMySecret()
if (typeof mySecret !== 'string') {
console.log("Jobs.onAcceptedRequests() -> typeof mySecret !== 'string'")
return
}
procid++
user
.get(Key.STORED_REQS)
.map()
.once(async (storedReq, id) => {
logger.info(
`------------------------------------\nPROCID:${procid}\n---------------------------------------`
)
const mySecret = require('../../Mediator').getMySecret()
try {
if (!Schema.isStoredRequest(storedReq)) {
throw new TypeError(
@ -70,93 +73,101 @@ const onAcceptedRequests = (user, SEA) => {
return
}
const gun = require('../../Mediator').getGun()
const user = require('../../Mediator').getUser()
const recipientEpub = await Utils.pubToEpub(recipientPub)
const ourSecret = await SEA.secret(recipientEpub, user._.sea)
if (typeof ourSecret !== 'string') {
throw new TypeError("typeof ourSecret !== 'string'")
}
await Utils.tryAndWait(
(gun, user) =>
new Promise((res, rej) => {
gun
.get(Key.HANDSHAKE_NODES)
.get(requestAddress)
.get(sentReqID)
.on(async sentReq => {
if (!Schema.isHandshakeRequest(sentReq)) {
rej(
new Error(
'sent request found in handshake node not a handshake request'
)
)
return
}
// The response can be decrypted with the same secret regardless of who
// wrote to it last (see HandshakeRequest definition).
// This could be our feed ID for the recipient, or the recipient's feed
// id if he accepted the request.
const feedID = await SEA.decrypt(sentReq.response, ourSecret)
if (typeof feedID !== 'string') {
throw new TypeError("typeof feedID !== 'string'")
}
const feedIDExistsOnRecipientsOutgoings = await Utils.tryAndWait(
gun =>
new Promise(res => {
gun
.user(recipientPub)
.get(Key.OUTGOINGS)
.get(feedID)
.once(feed => {
res(typeof feed !== 'undefined')
})
})
await new Promise((res, rej) => {
gun
.get(Key.HANDSHAKE_NODES)
.get(requestAddress)
.get(sentReqID)
.on(async sentReq => {
if (!Schema.isHandshakeRequest(sentReq)) {
rej(
new Error(
'sent request found in handshake node not a handshake request'
)
)
return
}
if (!feedIDExistsOnRecipientsOutgoings) {
return
}
// The response can be decrypted with the same secret regardless of who
// wrote to it last (see HandshakeRequest definition).
// This could be our feed ID for the recipient, or the recipient's feed
// id if he accepted the request.
const feedID = await SEA.decrypt(sentReq.response, ourSecret)
const encryptedForMeIncomingID = await SEA.encrypt(
feedID,
mySecret
)
if (typeof feedID !== 'string') {
throw new TypeError("typeof feedID !== 'string'")
}
await new Promise((res, rej) => {
user
.get(Key.USER_TO_INCOMING)
.get(recipientPub)
.put(encryptedForMeIncomingID, ack => {
if (ack.err) {
rej(new Error(ack.err))
} else {
res()
}
logger.info(`onAcceptedRequests -> decrypted feed ID: ${feedID}`)
logger.info(
'Will now try to access the other users outgoing feed'
)
const maybeFeedOnRecipientsOutgoings = await Utils.tryAndWait(
gun =>
new Promise(res => {
gun
.user(recipientPub)
.get(Key.OUTGOINGS)
.get(feedID)
.once(feed => {
res(feed)
})
})
}),
// retry on undefined, might be a false negative
v => typeof v === 'undefined'
)
await new Promise((res, rej) => {
user
.get(Key.STORED_REQS)
.get(id)
.put(null, ack => {
if (ack.err) {
rej(new Error(ack.err))
} else {
res()
}
})
})
const feedIDExistsOnRecipientsOutgoings =
typeof maybeFeedOnRecipientsOutgoings === 'object' &&
maybeFeedOnRecipientsOutgoings !== null
// ensure this listeners gets called at least once
res()
})
if (!feedIDExistsOnRecipientsOutgoings) {
return
}
const encryptedForMeIncomingID = await SEA.encrypt(
feedID,
mySecret
)
await new Promise((res, rej) => {
user
.get(Key.USER_TO_INCOMING)
.get(recipientPub)
.put(encryptedForMeIncomingID, ack => {
if (ack.err) {
rej(new Error(ack.err))
} else {
res()
}
})
})
await new Promise((res, rej) => {
user
.get(Key.STORED_REQS)
.get(id)
.put(null, ack => {
if (ack.err) {
rej(new Error(ack.err))
} else {
res()
}
})
})
// ensure this listeners gets called at least once
res()
})
)
})
} catch (err) {
console.warn(`Jobs.onAcceptedRequests() -> ${err.message}`)
console.log(err)

View file

@ -1,6 +1,8 @@
/**
* @format
*/
const logger = require('winston')
const ErrorCode = require('../errorCode')
const Key = require('../key')
@ -39,18 +41,96 @@ const timeout10 = promise => {
/**
* @template T
* @param {(gun: GUNNode, user: UserGUNNode) => Promise<T>} promGen The function
* receives the most recent gun and user instances.
* @param {Promise<T>} promise
* @returns {Promise<T>}
*/
const tryAndWait = promGen =>
timeout10(
promGen(
require('../../Mediator/index').getGun(),
require('../../Mediator/index').getUser()
const timeout5 = promise => {
return Promise.race([
promise,
new Promise((_, rej) => {
setTimeout(() => {
rej(new Error(ErrorCode.TIMEOUT_ERR))
}, 5000)
})
])
}
/**
* @template T
* @param {(gun: GUNNode, user: UserGUNNode) => Promise<T>} promGen The function
* receives the most recent gun and user instances.
* @param {((resolvedValue: unknown) => boolean)=} shouldRetry
* @returns {Promise<T>}
*/
const tryAndWait = async (promGen, shouldRetry = () => false) => {
/* eslint-disable no-empty */
/* eslint-disable init-declarations */
// If hang stop at 10, wait 3, retry, if hang stop at 5, reinstate, warm for
// 5, retry, stop at 10, err
/** @type {T} */
let resolvedValue
try {
resolvedValue = await timeout10(
promGen(
require('../../Mediator/index').getGun(),
require('../../Mediator/index').getUser()
)
)
if (shouldRetry(resolvedValue)) {
logger.info(
'force retrying' +
` args: ${promGen.toString()} -- ${shouldRetry.toString()}`
)
} else {
return resolvedValue
}
} catch (e) {
logger.error(e)
}
logger.info(
`\n retrying \n` +
` args: ${promGen.toString()} -- ${shouldRetry.toString()}`
)
await delay(3000)
try {
resolvedValue = await timeout5(
promGen(
require('../../Mediator/index').getGun(),
require('../../Mediator/index').getUser()
)
)
if (shouldRetry(resolvedValue)) {
logger.info(
'force retrying' +
` args: ${promGen.toString()} -- ${shouldRetry.toString()}`
)
} else {
return resolvedValue
}
} catch (e) {
logger.error(e)
}
logger.info(
`\n recreating a fresh gun and retrying one last time \n` +
` args: ${promGen.toString()} -- ${shouldRetry.toString()}`
)
const { gun, user } = await require('../../Mediator/index').freshGun()
return timeout10(promGen(gun, user))
/* eslint-enable no-empty */
/* eslint-enable init-declarations */
}
/**
* @param {string} pub
* @returns {Promise<string>}
@ -74,7 +154,7 @@ const pubToEpub = async pub => {
return epub
} catch (err) {
console.log(err)
logger.error(err)
throw new Error(`pubToEpub() -> ${err.message}`)
}
}
@ -86,18 +166,20 @@ const pubToEpub = async pub => {
* @returns {Promise<string|null>}
*/
const recipientPubToLastReqSentID = async recipientPub => {
const lastReqSentID = await tryAndWait(async (_, user) => {
const userToLastReqSent = user.get(Key.USER_TO_LAST_REQUEST_SENT)
const data = await userToLastReqSent.get(recipientPub).then()
const maybeLastReqSentID = await tryAndWait(
(_, user) => {
const userToLastReqSent = user.get(Key.USER_TO_LAST_REQUEST_SENT)
return userToLastReqSent.get(recipientPub).then()
},
// retry on undefined, in case it is a false negative
v => typeof v === 'undefined'
)
if (typeof data !== 'string') {
return null
}
if (typeof maybeLastReqSentID !== 'string') {
return null
}
return data
})
return lastReqSentID
return maybeLastReqSentID
}
/**
@ -127,11 +209,15 @@ const successfulHandshakeAlreadyExists = async recipientPub => {
* @returns {Promise<string|null>}
*/
const recipientToOutgoingID = async recipientPub => {
const maybeEncryptedOutgoingID = await require('../../Mediator/index')
.getUser()
.get(Key.RECIPIENT_TO_OUTGOING)
.get(recipientPub)
.then()
const maybeEncryptedOutgoingID = await tryAndWait(
(_, user) =>
user
.get(Key.RECIPIENT_TO_OUTGOING)
.get(recipientPub)
.then(),
// force retry in case undefined is a false negative
v => typeof v === 'undefined'
)
if (typeof maybeEncryptedOutgoingID === 'string') {
const outgoingID = await require('../../Mediator/index').mySEA.decrypt(
@ -145,22 +231,6 @@ const recipientToOutgoingID = async recipientPub => {
return null
}
/**
*
* @param {string} userPub
* @returns {Promise<string|null>}
*/
const currHandshakeAddress = async userPub => {
const maybeAddr = await tryAndWait(gun =>
gun
.user(userPub)
.get(Key.CURRENT_HANDSHAKE_ADDRESS)
.then()
)
return typeof maybeAddr === 'string' ? maybeAddr : null
}
/**
* @template T
* @param {T[]} arr
@ -223,22 +293,6 @@ const dataHasSoul = listenerData =>
*/
const defaultName = pub => 'anon' + pub.slice(0, 8)
/**
* @param {string} pub
* @param {string} incomingID
* @returns {Promise<boolean>}
*/
const didDisconnect = async (pub, incomingID) => {
const feed = await require('../../Mediator/index')
.getGun()
.user(pub)
.get(Key.OUTGOINGS)
.get(incomingID)
.then()
return feed === null
}
module.exports = {
asyncMap,
asyncFilter,
@ -249,10 +303,9 @@ module.exports = {
recipientPubToLastReqSentID,
successfulHandshakeAlreadyExists,
recipientToOutgoingID,
currHandshakeAddress,
tryAndWait,
mySecret,
promisifyGunNode: require('./promisifygun'),
didDisconnect,
asyncForEach
asyncForEach,
timeout5
}

View file

@ -1570,6 +1570,7 @@ module.exports = async (
const Events = require('../services/gunDB/contact-api/events')
const user = require('../services/gunDB/Mediator').getUser()
const Key = require('../services/gunDB/contact-api/key')
const {timeout5} = require('../services/gunDB/contact-api/utils')
app.get(`/api/gun/${GunEvent.ON_RECEIVED_REQUESTS}`, (_, res) => {
try {
@ -1619,7 +1620,7 @@ module.exports = async (
app.get(`/api/gun/${GunEvent.ON_AVATAR}`, async (_, res) => {
try {
res.json({
data: await user.get(Key.PROFILE).get(Key.AVATAR).then()
data: await timeout5(user.get(Key.PROFILE).get(Key.AVATAR).then())
})
} catch (err) {
res.status(500).json({
@ -1631,7 +1632,7 @@ module.exports = async (
app.get(`/api/gun/${GunEvent.ON_DISPLAY_NAME}`, async (_, res) => {
try {
res.json({
data: await user.get(Key.PROFILE).get(Key.DISPLAY_NAME).then()
data: await timeout5(user.get(Key.PROFILE).get(Key.DISPLAY_NAME).then())
})
} catch (err) {
res.status(500).json({
@ -1643,7 +1644,7 @@ module.exports = async (
app.get(`/api/gun/${GunEvent.ON_HANDSHAKE_ADDRESS}`, async (_, res) => {
try {
res.json({
data: await user.get(Key.CURRENT_HANDSHAKE_ADDRESS).then()
data: await timeout5(user.get(Key.CURRENT_HANDSHAKE_ADDRESS).then())
})
} catch (err) {
res.status(500).json({