thunderhub/server/helpers/fileHelpers.ts
Anthony Potdevin 581185e6b0
chore: 🔧 remove client (#111)
* chore: 🔧 remove client

* chore: 🔧 change cookie name

* chore: 🔧 remove auth param

* chore: 🔧 remove auth components

* chore: 🔧 add getaccount query

* fix: 🐛 tests

* chore: 🔧 get account

* chore: 🔧 status check

* chore: 🔧 remove log

* chore: 🔧 update apollo client

* refactor: ♻️ server side props

* chore: 🔧 ssr queries

* chore: 🔧 more ssr queries

* chore: 🔧 type check

* chore: 🔧 increase nodeinfo limit

Co-authored-by: apotdevin <apotdevincab@gmail.com>
2020-08-05 08:37:02 +02:00

421 lines
10 KiB
TypeScript

import fs from 'fs';
import crypto from 'crypto';
import path from 'path';
import os from 'os';
import { logger } from 'server/helpers/logger';
import yaml from 'js-yaml';
import bcrypt from 'bcryptjs';
import { AccountType as ContextAccountType } from 'server/types/apiTypes';
import { getUUID } from './auth';
type EncodingType = 'hex' | 'utf-8';
type BitcoinNetwork = 'mainnet' | 'regtest' | 'testnet';
type AccountType = {
name?: string;
serverUrl?: string;
lndDir?: string;
network?: BitcoinNetwork;
macaroonPath?: string;
certificatePath?: string;
password?: string | null;
macaroon?: string;
certificate?: string;
};
type ParsedAccount = {
name: string;
id: string;
socket: string;
macaroon: string;
cert: string;
password: string;
};
type AccountConfigType = {
hashed: boolean | null;
masterPassword: string | null;
defaultNetwork: string | null;
accounts: AccountType[];
};
const isValidNetwork = (network: string | null): network is BitcoinNetwork =>
network === 'mainnet' || network === 'regtest' || network === 'testnet';
export const PRE_PASS_STRING = 'thunderhub-';
export const readFile = (
filePath: string,
encoding: EncodingType = 'hex'
): string | null => {
if (filePath === '') {
return null;
}
const fileExists = fs.existsSync(filePath);
if (!fileExists) {
logger.error(`No file found at path: ${filePath}`);
return null;
} else {
try {
const file = fs.readFileSync(filePath, encoding);
return file;
} catch (err) {
logger.error('Something went wrong while reading the file: \n' + err);
return null;
}
}
};
export const parseYaml = (filePath: string): AccountConfigType | null => {
if (filePath === '') {
return null;
}
const yamlConfig = readFile(filePath, 'utf-8');
if (!yamlConfig) {
return null;
}
try {
const yamlObject = yaml.safeLoad(yamlConfig);
// TODO: validate this, before returning?
return yamlObject as AccountConfigType;
} catch (err) {
logger.error(
'Something went wrong while parsing the YAML config file: \n' + err
);
return null;
}
};
const saveHashedYaml = (config: AccountConfigType, filePath: string): void => {
if (filePath === '' || !config) return;
logger.info('Saving new yaml file with hashed passwords');
try {
const yamlString = yaml.safeDump(config);
fs.writeFileSync(filePath, yamlString);
logger.info('Succesfully saved');
} catch (error) {
logger.error(
'Error saving yaml file with hashed passwords. Passwords are still in cleartext on your server.'
);
}
};
export const hashPasswords = (
isHashed: boolean,
config: AccountConfigType,
filePath: string
): AccountConfigType => {
// Return early when passwords are already hashed
if (isHashed) return config;
let hasChanged = false;
const cloned = { ...config };
let hashedMasterPassword = config?.masterPassword;
if (
hashedMasterPassword &&
hashedMasterPassword.indexOf(PRE_PASS_STRING) < 0
) {
hasChanged = true;
hashedMasterPassword = `${PRE_PASS_STRING}${bcrypt.hashSync(
hashedMasterPassword,
12
)}`;
}
cloned.masterPassword = hashedMasterPassword;
const hashedAccounts: AccountType[] = [];
for (let i = 0; i < config.accounts.length; i++) {
const account: AccountType = config.accounts[i];
if (account.password) {
let hashedPassword = account.password;
if (hashedPassword.indexOf(PRE_PASS_STRING) < 0) {
hasChanged = true;
hashedPassword = `${PRE_PASS_STRING}${bcrypt.hashSync(
account.password,
12
)}`;
}
hashedAccounts.push({ ...account, password: hashedPassword });
} else {
hashedAccounts.push(account);
}
}
cloned.accounts = hashedAccounts;
hasChanged && saveHashedYaml(cloned, filePath);
return cloned;
};
const getCertificate = ({
certificate,
certificatePath,
lndDir,
}: AccountType): string | null => {
if (certificate) {
return certificate;
}
if (certificatePath) {
return readFile(certificatePath);
}
if (lndDir) {
return readFile(path.join(lndDir, 'tls.cert'));
}
return null;
};
const getMacaroon = (
{ macaroon, macaroonPath, network, lndDir }: AccountType,
defaultNetwork: BitcoinNetwork
): string | null => {
if (macaroon) {
return macaroon;
}
if (macaroonPath) {
return readFile(macaroonPath);
}
if (!lndDir) {
return null;
}
return readFile(
path.join(
lndDir,
'data',
'chain',
'bitcoin',
network || defaultNetwork,
'admin.macaroon'
)
);
};
export const getAccounts = (filePath: string): ContextAccountType[] => {
if (filePath === '') {
logger.verbose('No account config file path provided');
return [];
}
const accountConfig = parseYaml(filePath);
if (!accountConfig) {
logger.info(`No account config file found at path ${filePath}`);
return [];
}
return getAccountsFromYaml(accountConfig, filePath) as ContextAccountType[];
};
export const getParsedAccount = (
account: AccountType,
index: number,
masterPassword: string | null,
defaultNetwork: BitcoinNetwork
): ParsedAccount | null => {
const {
name,
serverUrl,
network,
lndDir,
macaroonPath,
macaroon: macaroonValue,
password,
} = account;
const missingFields: string[] = [];
if (!name) missingFields.push('name');
if (!serverUrl) missingFields.push('server url');
if (!lndDir && !macaroonPath && !macaroonValue) {
missingFields.push('macaroon');
}
if (missingFields.length > 0) {
const text = missingFields.join(', ');
logger.error(`Account in index ${index} is missing the fields ${text}`);
return null;
}
if (network && !isValidNetwork(network)) {
logger.error(`Account ${name} has invalid network: ${network}`);
return null;
}
if (!password && !masterPassword) {
logger.error(
`You must set a password for account ${name} or set a master password`
);
return null;
}
const cert = getCertificate(account);
if (!cert) {
logger.warn(
`No certificate for account ${name}. Make sure you don't need it to connect.`
);
}
const macaroon = getMacaroon(account, defaultNetwork);
if (!macaroon) {
logger.error(
`Account ${name} has neither lnd directory, macaroon nor macaroon path specified.`
);
return null;
}
const id = getUUID(`${name}${serverUrl}${macaroon}${cert}`);
return {
name: name || '',
id,
socket: serverUrl || '',
macaroon,
cert: cert || '',
password: password || masterPassword || '',
};
};
export const getAccountsFromYaml = (
config: AccountConfigType,
filePath: string
) => {
const { hashed, accounts: preAccounts } = config;
if (!preAccounts || preAccounts.length <= 0) {
logger.warn(`Account config found at path ${filePath} but had no accounts`);
return null;
}
const { defaultNetwork, masterPassword, accounts } = hashPasswords(
hashed || false,
config,
filePath
);
const network: BitcoinNetwork = isValidNetwork(defaultNetwork)
? defaultNetwork
: 'mainnet';
const parsedAccounts = accounts
.map((account, index) =>
getParsedAccount(account, index, masterPassword, network)
)
.filter(Boolean);
logger.info(
`Server accounts that will be available: ${parsedAccounts
.map(account => account?.name)
.join(', ')}`
);
return parsedAccounts;
};
export const readMacaroons = (macaroonPath: string): string | null => {
if (macaroonPath === '') {
logger.verbose('No macaroon path provided');
return null;
}
const adminExists = fs.existsSync(`${macaroonPath}/admin.macaroon`);
if (!adminExists) {
logger.error(
`No admin.macaroon file found at path: ${macaroonPath}/admin.macaroon`
);
return null;
} else {
try {
const ssoAdmin = fs.readFileSync(`${macaroonPath}/admin.macaroon`, 'hex');
return ssoAdmin;
} catch (err) {
logger.error(
'Something went wrong while reading the admin.macaroon: \n' + err
);
return null;
}
}
};
export const createDirectory = (dirname: string) => {
const initDir = path.isAbsolute(dirname) ? path.sep : '';
dirname.split(path.sep).reduce((parentDir, childDir) => {
const curDir = path.resolve(parentDir, childDir);
try {
if (!fs.existsSync(curDir)) {
fs.mkdirSync(curDir);
}
} catch (err) {
if (err.code !== 'EEXIST') {
if (err.code === 'ENOENT') {
throw new Error(
`ENOENT: No such file or directory, mkdir '${dirname}'. Ensure that path separator is '${
os.platform() === 'win32' ? '\\\\' : '/'
}'`
);
} else {
throw err;
}
}
}
return curDir;
}, initDir);
};
export const readCookie = (cookieFile: string): string | null => {
if (cookieFile === '') {
logger.verbose('No cookie path provided');
return null;
}
const exists = fs.existsSync(cookieFile);
if (exists) {
try {
logger.verbose(`Found cookie at path ${cookieFile}`);
const cookie = fs.readFileSync(cookieFile, 'utf-8');
return cookie;
} catch (err) {
logger.error('Something went wrong while reading cookie: \n' + err);
throw new Error(err);
}
} else {
try {
const dirname = path.dirname(cookieFile);
createDirectory(dirname);
fs.writeFileSync(cookieFile, crypto.randomBytes(64).toString('hex'));
logger.info(`Cookie created at directory: ${dirname}`);
const cookie = fs.readFileSync(cookieFile, 'utf-8');
return cookie;
} catch (err) {
logger.error('Something went wrong while reading the cookie: \n' + err);
throw new Error(err);
}
}
};
export const refreshCookie = (cookieFile: string) => {
try {
logger.verbose('Refreshing cookie for next authentication');
fs.writeFileSync(cookieFile, crypto.randomBytes(64).toString('hex'));
} catch (err) {
logger.error('Something went wrong while refreshing cookie: \n' + err);
throw new Error(err);
}
};