const phin = require('phin')
const log = require('sesajstools/utils/log')
const config = require('./config')
const myBaseId = config.application.baseId
const myBaseUrl = config.application.baseUrl
const myApiTokens = config.apiTokens || []
/**
* Vérifie sesatheque par rapport à notre conf
* - si c'est nous, vérif baseId + baseUrl Ok (sinon erorr),
* et si y'a un apiToken on le vérifie aussi (warning s'il est inconnu)
* - sinon, si on la connaît on vérifie que c'est sous le même baseId/baseUrl
* @private
* @param {{baseId: string, baseUrl: string}} sesatheque La sésathèque à vérifier (on vérifie un éventuel apiToken si c'est notre sésathèque)
* @param {string[]} errors Un tableau pour y ajouter des erreurs éventuelles
* @param {string[]} warnings Idem pour des warnings (pb d'apiToken)
*/
function checkSesathequeIsSameHere (sesatheque, errors, warnings) {
const { baseId, baseUrl, apiToken } = sesatheque
if (baseUrl === myBaseUrl) {
if (baseId !== myBaseId) errors.push(`Le sesalab nous connait sous ${baseId} alors qu’on est identifié par ${myBaseId} => ${myBaseUrl}`)
if (apiToken && !myApiTokens.includes(apiToken)) warnings.push(`Le sesalab utilise un apiToken inconnu de ${myBaseId}`)
} else if (baseId === myBaseId) {
if (baseUrl !== myBaseUrl) errors.push(`Le sesalab a déclaré une autre sésathèque (${baseUrl}) avec notre baseId ${myBaseId} => ${myBaseUrl}`)
} else {
// on regarde si on la connait
config.sesatheques.some(st => {
if (st.baseId === baseId) {
if (st.baseUrl === baseUrl) return true
errors.push(`${baseId} est connue ici (${myBaseId}) avec l’url ${st.baseUrl} et non ${baseUrl}`)
} else if (st.baseUrl === baseUrl) {
errors.push(`${baseUrl} est connue ici (${myBaseId}) avec la baseId ${st.baseId} et non ${baseId}`)
}
return false
})
}
}
/**
* Vérification croisée de configuration
*
* Ce module fourni les méthodes permettant de vérifier la cohérence des configurations
* entre sesalabs et sésathèques, en comparant les infos fournies à la configuration locale.
* Ces méthodes renvoient des promesses, qui rejettent en cas de pb majeur, ou résolvent avec un éventuel tableau de warnings (que le contrôleur devra faire suivre)
* @module
* @name checkConfig
*/
/**
* Valide la config de nos sésathèques externes en allant les interroger (pendant 30s max si elles répondent pas, au delà on résoud avec les pbs en console)
* @return {Promise<undefined|string[]>} Rejet si la conf locale est défectueuse, ou que le registrar signale un problème. En cas de timeout ou de réponse inattendue du registrar, on résoud en renvoyant des warnings
*/
function checkLocalOnRemote () {
// On vérifie pendant 10 min (en cas d'update bloquant sur une autre sesathèque)
const maxWait = 10 * 60 * 1000
// attente avant l'essai suivant (ms)
const retryDelay = 5000
// pas la peine d'attendre une réponse plus longtemps (ms)
const timeout = 30 * 1000
const toCheck = {
sesatheques: config.sesatheques.concat([{ baseId: myBaseId, baseUrl: myBaseUrl }]),
sesalabs: config.sesalabs
}
/**
* Retourne une promesse (qui rejette si la sésathèque concernée
* répond avec une erreur au format attendu, résoud sinon)
* @param {string} baseUrl
* @return {Promise<string[]>} Résoud avec un array de warnings ou rien, rejette si l'autre sésathèque a explicitement retourné une erreur
*/
const checkOne = (baseUrl) => {
if (baseUrl === myBaseUrl) return Promise.resolve(['Inutile de se déclarer soi-même dans config.sesatheques'])
const start = Date.now()
return new Promise((resolve, reject) => {
const phinOptions = {
url: `${baseUrl}api/checkSesatheque`,
method: 'POST',
data: toCheck,
parse: 'json',
timeout
}
// on passe par une inner function pour la rappeler en cas de réponse foireuse dans le timeout donné
// (pour laisser l'autre sésathèque démarrer)
function callOnce () {
// si ça répond ok on résoud,
// si ça répond avec une erreur au bon format on rejette avec l'erreur,
// sinon on râle en console mais on résoud
phin(phinOptions).then(result => {
if (result.success) {
if (result.message) log(`${phinOptions.url} a répondu « ${result.message} » pour ${myBaseId}`)
return resolve(result.warnings)
}
if (result.errors) {
return reject(new Error(`Erreurs lors de la vérification sur ${baseUrl} :\n- ${result.errors.join('\n- ')}`))
}
if (result.message) {
// si !success, message est une erreur
reject(new Error(result.message))
}
}).catch(error => {
const elapsed = Date.now() - start
if (elapsed < maxWait) return setTimeout(callOnce, retryDelay)
// on a dépassé le délai max et la sesatheque distante répond toujours pas,
let warning
const secs = Math.round(elapsed / 1000)
switch (error.code) {
case 'ECONNREFUSED':
warning = `${baseUrl} refuse toujours la connexion après ${secs}s d’attente`
break
case 'ETIMEDOUT':
warning = `${baseUrl} n’a toujours pas répondu après ${secs}s d’attente`
break
default:
log.error(error)
warning = `Pb dans la réponse de ${baseUrl} : ${error.message}`
}
// => resolve avec warning
return resolve([warning])
})
} // callOnce
callOnce()
})
} // checkOne
return Promise.all(config.sesatheques.map(({ baseUrl }) => checkOne(baseUrl)))
} // checkLocalOnRemote
/**
* Valide la config d'un sesalab qui nous appelle en lui retournant son baseId, ou les erreurs (sync)
* @param {string} baseUrl La baseUrl du sesalab
* @param {Object[]} sesatheques Ses sésathèques (avec baseId, baseUrl et éventuellement apiToken)
* @return {{baseId: string, errors: string[]}}
*/
function checkSesalab (baseUrl, sesatheques) {
const errors = []
const warnings = []
// la baseId qu'on va attribuer à ce sesalab
let baseId
if (!baseUrl) errors.push('Requête invalide, baseUrl manquante')
if (typeof baseUrl !== 'string') errors.push(`baseUrl invalide (${typeof baseUrl})`)
if (!Array.isArray(sesatheques) || !sesatheques.length) errors.push('sesatheques manquantes')
else if (!sesatheques.every(st => st.baseId && st.baseUrl)) errors.push('chaque sesatheque doit avoir baseId et baseUrl')
// pas la peine d'aller plus loin si y'a déjà des erreurs
if (errors.length) return ({ errors })
// vérif que le sesalab annoncé est connu
config.sesalabs.some(knownSesalab => {
if (knownSesalab.baseUrl === baseUrl) {
baseId = knownSesalab.baseId
return true
}
return false
})
if (!baseId) {
errors.push(`${baseUrl} n'est pas dans les sesalabs connus de la sesathèque ${myBaseUrl} (${myBaseId})`)
}
// vérif des sésathèques, si on connait ça doit être sous la même forme
sesatheques.forEach((st) => checkSesathequeIsSameHere(st, errors, warnings))
return { baseId, errors, warnings }
}
/**
* Valide la config d'une sésathèque distante (vs ce qu'on a en configuration ici)
* @param sesalabs
* @param sesatheques
*/
function checkSesatheque ({ sesalabs, sesatheques }) {
const errors = []
const warnings = []
// pour les sesalab, on vérifie juste que si on en a en commun ils ont la même baseUrl
if (sesalabs) {
if (Array.isArray(sesalabs)) {
const mySesalabsById = {}
const mySesalabsByUrl = {}
config.sesalabs.forEach(({ baseId, baseUrl }) => {
mySesalabsById[baseId] = baseUrl
mySesalabsByUrl[baseUrl] = baseId
})
sesalabs.forEach(({ baseId, baseUrl }) => {
const myUrl = mySesalabsById[baseId]
const myId = mySesalabsByUrl[baseUrl]
if (myUrl && myUrl !== baseUrl) errors.push(`Le sesalab ${baseId} est connu ici avec ${myUrl} et pas ${baseUrl}`)
if (myId && myId !== baseId) errors.push(`Le sesalab ${baseUrl} est connu ici sous ${myId} et pas ${baseId}`)
})
} else {
errors.push('paramètres incorrects, sesalabs doit être un Array')
}
}
// pour les sésathèques, si on connait ça doit être sous la même forme
if (Array.isArray(sesatheques)) {
sesatheques.forEach((st) => checkSesathequeIsSameHere(st, errors, warnings))
} else {
errors.push('paramètres incorrects, sesatheques doit être un Array')
}
return { errors, warnings }
}
module.exports = {
checkLocalOnRemote,
checkSesalab,
checkSesatheque
}