UNPKG

dbgate-api

Version:

Allows run DbGate data-manipulation scripts.

443 lines (394 loc) 11.8 kB
const axios = require('axios'); const crypto = require('crypto'); const fs = require('fs-extra'); const _ = require('lodash'); const path = require('path'); const { getLicenseHttpHeaders } = require('./authProxy'); const { getLogger, extractErrorLogData, jsonLinesParse } = require('dbgate-tools'); const { datadir } = require('./directories'); const platformInfo = require('./platformInfo'); const connections = require('../controllers/connections'); const { isProApp } = require('./checkLicense'); const socket = require('./socket'); const config = require('../controllers/config'); const simpleEncryptor = require('simple-encryptor'); const currentVersion = require('../currentVersion'); const { getPublicIpInfo } = require('./hardwareFingerprint'); const logger = getLogger('cloudIntf'); let cloudFiles = null; const DBGATE_IDENTITY_URL = process.env.LOCAL_DBGATE_IDENTITY ? 'http://localhost:3103' : process.env.PROD_DBGATE_IDENTITY ? 'https://identity.dbgate.io' : process.env.DEVWEB || process.env.DEVMODE ? 'https://identity.dbgate.udolni.net' : 'https://identity.dbgate.io'; const DBGATE_CLOUD_URL = process.env.LOCAL_DBGATE_CLOUD ? 'http://localhost:3110' : process.env.PROD_DBGATE_CLOUD ? 'https://cloud.dbgate.io' : process.env.DEVWEB || process.env.DEVMODE ? 'https://cloud.dbgate.udolni.net' : 'https://cloud.dbgate.io'; async function createDbGateIdentitySession(client, redirectUri) { const resp = await axios.default.post( `${DBGATE_IDENTITY_URL}/api/create-session`, { client, redirectUri, }, { headers: { ...getLicenseHttpHeaders(), 'Content-Type': 'application/json', }, } ); return { sid: resp.data.sid, url: `${DBGATE_IDENTITY_URL}/api/signin/${resp.data.sid}`, }; } function startCloudTokenChecking(sid, callback) { const started = Date.now(); const interval = setInterval(async () => { if (Date.now() - started > 60 * 1000) { clearInterval(interval); return; } try { // console.log(`Checking cloud token for session: ${DBGATE_IDENTITY_URL}/api/get-token/${sid}`); const resp = await axios.default.get(`${DBGATE_IDENTITY_URL}/api/get-token/${sid}`, { headers: { ...getLicenseHttpHeaders(), }, }); // console.log('CHECK RESP:', resp.data); if (resp.data?.email) { clearInterval(interval); callback(resp.data); } } catch (err) { logger.error(extractErrorLogData(err), 'Error checking cloud token'); } }, 500); } async function readCloudTokenHolder(sid) { const resp = await axios.default.get(`${DBGATE_IDENTITY_URL}/api/get-token/${sid}`, { headers: { ...getLicenseHttpHeaders(), }, }); if (resp.data?.email) { return resp.data; } return null; } async function readCloudTestTokenHolder(email) { const resp = await axios.default.post( `${DBGATE_IDENTITY_URL}/api/test-token`, { email }, { headers: { ...getLicenseHttpHeaders(), }, } ); if (resp.data?.email) { return resp.data; } return null; } async function loadCloudFiles() { try { const fileContent = await fs.readFile(path.join(datadir(), 'cloud-files.jsonl'), 'utf-8'); const parsedJson = jsonLinesParse(fileContent); cloudFiles = _.sortBy(parsedJson, x => `${x.folder}/${x.title}`); } catch (err) { cloudFiles = []; } } async function getCloudUsedEngines() { try { const resp = await callCloudApiGet('content-engines'); return resp || []; } catch (err) { logger.error(extractErrorLogData(err), 'Error getting cloud content list'); return []; } } async function collectCloudFilesSearchTags() { const res = []; if (platformInfo.isElectron) { res.push('app'); } else { res.push('web'); } if (platformInfo.isWindows) { res.push('windows'); } if (platformInfo.isMac) { res.push('mac'); } if (platformInfo.isLinux) { res.push('linux'); } if (platformInfo.isAwsUbuntuLayout) { res.push('aws'); } if (platformInfo.isAzureUbuntuLayout) { res.push('azure'); } if (platformInfo.isSnap) { res.push('snap'); } if (platformInfo.isDocker) { res.push('docker'); } if (platformInfo.isNpmDist) { res.push('npm'); } const engines = await connections.getUsedEngines(); const engineTags = engines.map(engine => engine.split('@')[0]); res.push(...engineTags); const cloudEngines = await getCloudUsedEngines(); const cloudEngineTags = cloudEngines.map(engine => engine.split('@')[0]); res.push(...cloudEngineTags); // team-premium and trials will return the same cloud files as premium - no need to check res.push(isProApp() ? 'premium' : 'community'); return _.uniq(res); } async function getCloudSigninHolder() { const settingsValue = await config.getSettings(); const holder = settingsValue['cloudSigninTokenHolder']; return holder; } async function getCloudSigninHeaders(holder = null) { if (!holder) { holder = await getCloudSigninHolder(); } if (holder) { return { 'x-cloud-login': holder.token, }; } return null; } async function updateCloudFiles(isRefresh) { let lastCloudFilesTags; try { lastCloudFilesTags = await fs.readFile(path.join(datadir(), 'cloud-files-tags.txt'), 'utf-8'); } catch (err) { lastCloudFilesTags = ''; } const ipInfo = await getPublicIpInfo(); const tags = (await collectCloudFilesSearchTags()).join(','); let lastCheckedTm = 0; if (tags == lastCloudFilesTags && cloudFiles.length > 0) { lastCheckedTm = _.max(cloudFiles.map(x => parseInt(x.modifiedTm))); } logger.info({ tags, lastCheckedTm }, 'Downloading cloud files'); const resp = await axios.default.get( `${DBGATE_CLOUD_URL}/public-cloud-updates?lastCheckedTm=${lastCheckedTm}&tags=${tags}&isRefresh=${ isRefresh ? 1 : 0 }&country=${ipInfo?.country || ''}`, { headers: { ...getLicenseHttpHeaders(), ...(await getCloudInstanceHeaders()), 'x-app-version': currentVersion.version, }, } ); logger.info(`Downloaded ${resp.data.length} cloud files`); const filesByPath = lastCheckedTm == 0 ? {} : _.keyBy(cloudFiles, 'path'); for (const file of resp.data) { if (file.isDeleted) { delete filesByPath[file.path]; } else { filesByPath[file.path] = file; } } cloudFiles = Object.values(filesByPath); await fs.writeFile(path.join(datadir(), 'cloud-files.jsonl'), cloudFiles.map(x => JSON.stringify(x)).join('\n')); await fs.writeFile(path.join(datadir(), 'cloud-files-tags.txt'), tags); socket.emitChanged(`public-cloud-changed`); } async function startCloudFiles() { loadCloudFiles(); } async function getPublicCloudFiles() { if (!loadCloudFiles) { await loadCloudFiles(); } return cloudFiles; } async function getPublicFileData(path) { const resp = await axios.default.get(`${DBGATE_CLOUD_URL}/public/${path}`, { headers: { ...getLicenseHttpHeaders(), }, }); return resp.data; } async function refreshPublicFiles(isRefresh) { if (!cloudFiles) { await loadCloudFiles(); } try { await updateCloudFiles(isRefresh); } catch (err) { logger.error(extractErrorLogData(err), 'Error updating cloud files'); } } async function callCloudApiGet(endpoint, signinHolder = null, additionalHeaders = {}) { if (!signinHolder) { signinHolder = await getCloudSigninHolder(); } if (!signinHolder) { return null; } const signinHeaders = await getCloudSigninHeaders(signinHolder); const resp = await axios.default.get(`${DBGATE_CLOUD_URL}/${endpoint}`, { headers: { ...getLicenseHttpHeaders(), ...signinHeaders, ...additionalHeaders, }, validateStatus: status => status < 500, }); const { errorMessage, isLicenseLimit, limitedLicenseLimits } = resp.data; if (errorMessage) { return { apiErrorMessage: errorMessage, apiErrorIsLicenseLimit: isLicenseLimit, apiErrorLimitedLicenseLimits: limitedLicenseLimits, }; } return resp.data; } async function getCloudInstanceHeaders() { if (!(await fs.exists(path.join(datadir(), 'cloud-instance.txt')))) { const newInstanceId = crypto.randomUUID(); await fs.writeFile(path.join(datadir(), 'cloud-instance.txt'), newInstanceId); } const instanceId = await fs.readFile(path.join(datadir(), 'cloud-instance.txt'), 'utf-8'); return { 'x-cloud-instance': instanceId, }; } async function callCloudApiPost(endpoint, body, signinHolder = null) { if (!signinHolder) { signinHolder = await getCloudSigninHolder(); } if (!signinHolder) { return null; } const signinHeaders = await getCloudSigninHeaders(signinHolder); const resp = await axios.default.post(`${DBGATE_CLOUD_URL}/${endpoint}`, body, { headers: { ...getLicenseHttpHeaders(), ...signinHeaders, }, validateStatus: status => status < 500, }); const { errorMessage, isLicenseLimit, limitedLicenseLimits } = resp.data; if (errorMessage) { return { apiErrorMessage: errorMessage, apiErrorIsLicenseLimit: isLicenseLimit, apiErrorLimitedLicenseLimits: limitedLicenseLimits, }; } return resp.data; } async function getCloudFolderEncryptor(folid) { const { encryptionKey } = await callCloudApiGet(`folder-key/${folid}`); if (!encryptionKey) { throw new Error('No encryption key for folder: ' + folid); } return simpleEncryptor.createEncryptor(encryptionKey); } async function getCloudContent(folid, cntid) { const signinHolder = await getCloudSigninHolder(); if (!signinHolder) { throw new Error('No signed in'); } const encryptor = simpleEncryptor.createEncryptor(signinHolder.encryptionKey); const { content, name, type, contentAttributes, apiErrorMessage } = await callCloudApiGet( `content/${folid}/${cntid}`, signinHolder, { 'x-kehid': signinHolder.kehid, } ); if (apiErrorMessage) { return { apiErrorMessage }; } return { content: encryptor.decrypt(content), name, type, contentAttributes, }; } /** * * @returns Promise<{ cntid: string } | { apiErrorMessage: string }> */ async function putCloudContent(folid, cntid, content, name, type, contentAttributes) { const signinHolder = await getCloudSigninHolder(); if (!signinHolder) { throw new Error('No signed in'); } const encryptor = simpleEncryptor.createEncryptor(signinHolder.encryptionKey); const resp = await callCloudApiPost( `put-content`, { folid, cntid, name, type, kehid: signinHolder.kehid, content: encryptor.encrypt(content), contentAttributes, }, signinHolder ); socket.emitChanged('cloud-content-changed'); socket.emit('cloud-content-updated'); return resp; } const cloudConnectionCache = {}; async function loadCachedCloudConnection(folid, cntid) { const cacheKey = `${folid}|${cntid}`; if (!cloudConnectionCache[cacheKey]) { const { content } = await getCloudContent(folid, cntid); cloudConnectionCache[cacheKey] = { ...JSON.parse(content), _id: `cloud://${folid}/${cntid}`, }; } return cloudConnectionCache[cacheKey]; } function removeCloudCachedConnection(folid, cntid) { const cacheKey = `${folid}|${cntid}`; delete cloudConnectionCache[cacheKey]; } module.exports = { createDbGateIdentitySession, startCloudTokenChecking, startCloudFiles, getPublicCloudFiles, getPublicFileData, refreshPublicFiles, callCloudApiGet, callCloudApiPost, getCloudFolderEncryptor, getCloudContent, loadCachedCloudConnection, putCloudContent, removeCloudCachedConnection, readCloudTokenHolder, readCloudTestTokenHolder, };