You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

331 lines
9.2 KiB

const axios = require('axios');
const semverGt = require('semver/functions/gt');
const semverSatisfies = require('semver/functions/satisfies');
const semverMinVersion = require('semver/ranges/min-version');
const encode = require('lndconnect').encode;
const diskLogic = require('logic/disk.js');
const constants = require('utils/const.js');
const NodeError = require('models/errors.js').NodeError;
async function getInfo() {
try {
const info = await diskLogic.readUmbrelVersionFile();
return info;
} catch (error) {
throw new NodeError('Unable to get system information');
}
};
async function getHiddenServiceUrl() {
try {
const url = await diskLogic.readHiddenService('web');
return url;
} catch (error) {
throw new NodeError('Unable to get hidden service url');
}
};
async function getElectrumConnectionDetails() {
try {
const address = await diskLogic.readElectrumHiddenService();
const port = constants.ELECTRUM_PORT;
const connectionString = `${address}:${port}:t`;
return {
address,
port,
connectionString
};
} catch (error) {
throw new NodeError('Unable to get Electrum hidden service url');
}
};
async function getBitcoinP2PConnectionDetails() {
try {
const address = await diskLogic.readBitcoinP2PHiddenService();
const port = constants.BITCOIN_P2P_PORT;
const connectionString = `${address}:${port}`;
return {
address,
port,
connectionString
};
} catch (error) {
throw new NodeError('Unable to get Bitcoin P2P hidden service url');
}
};
async function getBitcoinRPCConnectionDetails() {
try {
const [user, hiddenService] = await Promise.all([
diskLogic.readUserFile(),
diskLogic.readBitcoinRPCHiddenService(),
]);
const label = encodeURIComponent(`${user.name}'s Umbrel`);
const rpcuser = constants.BITCOIN_RPC_USER;
const rpcpassword = constants.BITCOIN_RPC_PASSWORD;
const address = hiddenService;
const port = constants.BITCOIN_RPC_PORT;
const connectionString = `btcrpc://${rpcuser}:${rpcpassword}@${address}:${port}?label=${label}`;
return {
rpcuser,
rpcpassword,
address,
port,
connectionString
};
} catch (error) {
throw new NodeError('Unable to get Bitcoin RPC connection details');
}
};
async function getAvailableUpdate() {
try {
const current = await diskLogic.readUmbrelVersionFile();
const currentVersion = current.version;
// 'tag' should be master to begin with
let tag = 'master';
let data;
let isNewVersionAvailable = true;
let isCompatibleWithCurrentVersion = false;
// Try finding for a new update until there's a new version available
// which is compatible with the currently installed version
while (isNewVersionAvailable && !isCompatibleWithCurrentVersion) {
const infoUrl = `https://raw.githubusercontent.com/${constants.GITHUB_REPO}/${tag}/info.json?time=${Date.now()}`;
const latestVersionInfo = await axios.get(infoUrl);
data = latestVersionInfo.data;
let latestVersion = data.version;
let requiresVersionRange = data.requires;
// A new version is available if the latest version > local version
isNewVersionAvailable = semverGt(latestVersion, currentVersion);
// It's compatible with the current version if current version
// satisfies the 'requires' condition of the new version
isCompatibleWithCurrentVersion = semverSatisfies(currentVersion, requiresVersionRange);
// Calculate the minimum required version
let minimumVersionRequired = `v${semverMinVersion(requiresVersionRange)}`;
// If the minimum required version is what we just checked for, exit
// This usually happens when an OTA update breaking release x.y.z is made
// that also has x.y.z as the minimum required version
if (tag === minimumVersionRequired) {
break;
}
// Update tag to the minimum required version for the next loop run
tag = minimumVersionRequired;
}
if (isNewVersionAvailable && isCompatibleWithCurrentVersion) {
return data;
}
return "Your Umbrel is up-to-date";
}
catch (error) {
throw new NodeError('Unable to check for update');
}
};
async function getUpdateStatus() {
try {
const status = await diskLogic.readUpdateStatusFile()
return status;
} catch (error) {
throw new NodeError('Unable to get update status');
}
}
async function startUpdate() {
let availableUpdate;
// Fetch available update
try {
availableUpdate = await getAvailableUpdate();
if (!availableUpdate.version) {
return availableUpdate;
}
} catch (error) {
throw new NodeError('Unable to fetch latest release');
}
// Make sure an update is not already in progress
const updateInProgress = await diskLogic.updateLockFileExists();
if (updateInProgress) {
throw new NodeError('An update is already in progress');
}
// Update status file with update version
try {
const updateStatus = await diskLogic.readUpdateStatusFile();
updateStatus.updateTo = `v${availableUpdate.version}`;
await diskLogic.writeUpdateStatusFile(updateStatus);
} catch (error) {
throw new NodeError('Could not update the update-status file');
}
// Write update signal file
try {
await diskLogic.writeUpdateSignalFile()
return { message: "Updating to Umbrel v" + availableUpdate.version };
} catch (error) {
throw new NodeError('Unable to write update signal file');
}
}
async function getBackupStatus() {
try {
const status = await diskLogic.readBackupStatusFile()
return status;
} catch (error) {
throw new NodeError('Unable to get backup status');
}
}
async function getLndConnectUrls() {
let cert;
try {
cert = await diskLogic.readLndCert();
} catch (error) {
throw new NodeError('Unable to read lnd cert file');
}
let macaroon;
try {
macaroon = await diskLogic.readLndAdminMacaroon();
} catch (error) {
throw new NodeError('Unable to read lnd macaroon file');
}
let restTorHost;
try {
restTorHost = await diskLogic.readLndRestHiddenService();
restTorHost += ':8080';
} catch (error) {
throw new NodeError('Unable to read lnd REST hostname file');
}
const restTor = encode({
host: restTorHost,
cert,
macaroon,
});
let grpcTorHost;
try {
grpcTorHost = await diskLogic.readLndGrpcHiddenService();
grpcTorHost += ':10009';
} catch (error) {
throw new NodeError('Unable to read lnd gRPC hostname file');
}
const grpcTor = encode({
host: grpcTorHost,
cert,
macaroon,
});
let restLocalHost = `${constants.DEVICE_HOSTNAME}:8080`;
const restLocal = encode({
host: restLocalHost,
cert,
macaroon,
});
let grpcLocalHost = `${constants.DEVICE_HOSTNAME}:10009`;
const grpcLocal = encode({
host: grpcLocalHost,
cert,
macaroon,
});
return {
restTor,
restLocal,
grpcTor,
grpcLocal
};
}
async function requestDebug() {
try {
await diskLogic.writeSignalFile('debug');
return "Debug requested";
} catch (error) {
throw new NodeError('Could not write the signal file');
}
}
async function getDebugResult() {
try {
return await diskLogic.readDebugStatusFile();
} catch (error) {
throw new NodeError('Unable to get debug results');
}
}
async function requestShutdown() {
try {
await diskLogic.shutdown();
return "Shutdown requested";
} catch (error) {
throw new NodeError('Unable to request shutdown');
}
};
async function requestReboot() {
try {
await diskLogic.reboot();
return "Reboot requested";
} catch (error) {
throw new NodeError('Unable to request reboot');
}
};
async function status() {
try {
const highMemoryUsage = await diskLogic.memoryWarningStatusFileExists();
return {
highMemoryUsage
};
} catch (error) {
throw new NodeError('Unable check system status');
}
};
async function clearMemoryWarning() {
try {
await diskLogic.deleteMemoryWarningStatusFile();
return "High memory warning dismissed"
} catch (error) {
throw new NodeError('Unable to dismiss high memory warning');
}
};
module.exports = {
getInfo,
getHiddenServiceUrl,
getElectrumConnectionDetails,
getBitcoinP2PConnectionDetails,
getBitcoinRPCConnectionDetails,
getAvailableUpdate,
getUpdateStatus,
startUpdate,
getBackupStatus,
getLndConnectUrls,
requestDebug,
getDebugResult,
requestShutdown,
requestReboot,
status,
clearMemoryWarning,
};