2024-06-10 12:40:37 -07:00
import jwt from 'jsonwebtoken' ;
2021-12-29 18:08:41 -05:00
import * as fs from 'fs' ;
import { sep } from 'path' ;
import ini from 'ini' ;
import parseHocon from 'hocon-parser' ;
Replace deprecated request/request-promise with axios
request has been deprecated since 2020 with an unfixed SSRF advisory and
pins vulnerable copies of form-data (critical), qs, tough-cookie and
uuid - 8 of the 13 remaining production audit findings, none fixable by
version bumps (issue #1634, item 1).
All 36 backend files that imported request-promise now use a small
compatibility wrapper (server/utils/request.ts) backed by axios, which
is already a production dependency. The wrapper accepts the existing
options shape (qs, form - object or pre-encoded string, body,
baseUrl/uri, rejectUnauthorized, json), resolves with the response body
directly, and rejects with a plain object mirroring request-promise's
StatusCodeError/RequestError shape, so CommonService.handleError works
unchanged (ECONNREFUSED -> 503, Eclair StatusCodeError -> 500, nested
error body extraction). Auth headers are excluded from rejected errors
so they cannot leak into logs. Callers without json: true (block
explorer, currency rates) still get raw text bodies, and LND's
line-delimited /v2/router/send stream still surfaces as a string for
the existing parser.
Only behavioral code change: CLN verifyMessage used request-promise's
callback style and was ported to the same promise style as signMessage;
four Eclair handlers gained explicit returns to satisfy
noImplicitReturns once the import became typed.
Production npm audit drops from 13 findings (2 critical) to 6 low, all
in the crypto-browserify/elliptic chain tracked in #1634.
Verified against the docker regtest fixture with 43 API checks across
LND, Core Lightning and Eclair: reads, invoice creation, a routed LND
payment over the streaming endpoint, cross-implementation payments from
CLN and Eclair, message sign/verify, channel backup to disk, and
bad-invoice/node-unreachable error mapping. Lint and both production
builds are clean.
2026-07-19 12:08:05 -07:00
import request from '../../utils/request.js' ;
2021-12-29 18:08:41 -05:00
import { Database } from '../../utils/database.js' ;
import { Logger } from '../../utils/logger.js' ;
import { Common } from '../../utils/common.js' ;
import { WSServer } from '../../utils/webSocketServer.js' ;
2024-11-18 20:27:51 -08:00
import { Authentication } from '../../models/config.model.js' ;
2021-12-29 18:08:41 -05:00
const options = { url : '' } ;
const logger = Logger ;
const common = Common ;
const wsServer = WSServer ;
const databaseService = Database ;
2024-06-10 12:40:37 -07:00
// Set local block explorer URL after first API call
// if the selected node block explorer has working REST API suite
// otherwise set it to mempool.space
let blockExplorerUrl = '' ;
export const getExplorerFeesRecommended = ( req , res , next ) => {
logger . log ( { selectedNode : req . session . selectedNode , level : 'INFO' , fileName : 'RTLConf' , msg : 'Getting Recommended Fee Rates..' } ) ;
options . url = ( blockExplorerUrl === '' ) ?
req . session . selectedNode . settings . blockExplorerUrl + '/api/v1/fees/recommended' :
blockExplorerUrl + '/api/v1/fees/recommended' ;
request ( options ) . then ( ( body ) => {
blockExplorerUrl = req . session . selectedNode . settings . blockExplorerUrl ;
logger . log ( { selectedNode : req . session . selectedNode , level : 'INFO' , fileName : 'RTLConf' , msg : 'Recommended Fee Rates Received' , data : body } ) ;
res . status ( 200 ) . json ( JSON . parse ( body ) ) ;
} ) . catch ( ( errRes ) => {
blockExplorerUrl = 'https://mempool.space' ;
options . url = blockExplorerUrl + '/api/v1/fees/recommended' ;
return request ( options ) . then ( ( body ) => {
logger . log ( { selectedNode : req . session . selectedNode , level : 'INFO' , fileName : 'RTLConf' , msg : 'Recommended Fee Rates Received' , data : body } ) ;
res . status ( 200 ) . json ( JSON . parse ( body ) ) ;
} ) . catch ( ( errRes ) => {
const errMsg = 'Get Recommended Fee Rates Error' ;
const err = common . handleError ( { statusCode : 500 , message : errMsg , error : errRes } , 'RTLConf' , errMsg , req . session . selectedNode ) ;
return res . status ( err . statusCode ) . json ( { message : err . error , error : err . error } ) ;
} ) ;
} ) ;
2021-12-29 18:08:41 -05:00
} ;
2024-06-10 12:40:37 -07:00
export const getExplorerTransaction = ( req , res , next ) => {
logger . log ( { selectedNode : req . session . selectedNode , level : 'INFO' , fileName : 'RTLConf' , msg : 'Getting Transaction From Block Explorer..' } ) ;
options . url = ( blockExplorerUrl === '' ) ?
req . session . selectedNode . settings . blockExplorerUrl + '/api/tx/' + req . params . txid :
blockExplorerUrl + '/api/tx/' + req . params . txid ;
request ( options ) . then ( ( body ) => {
blockExplorerUrl = req . session . selectedNode . settings . blockExplorerUrl ;
logger . log ( { selectedNode : req . session . selectedNode , level : 'INFO' , fileName : 'RTLConf' , msg : 'Transaction From Block Explorer Received' , data : body } ) ;
res . status ( 200 ) . json ( JSON . parse ( body ) ) ;
} ) . catch ( ( errRes ) => {
blockExplorerUrl = 'https://mempool.space' ;
options . url = blockExplorerUrl + '/api/tx/' + req . params . txid ;
return request ( options ) . then ( ( body ) => {
logger . log ( { selectedNode : req . session . selectedNode , level : 'INFO' , fileName : 'RTLConf' , msg : 'Transaction From Block Explorer Received' , data : body } ) ;
res . status ( 200 ) . json ( JSON . parse ( body ) ) ;
} ) . catch ( ( errRes ) => {
const errMsg = 'Get Transaction From Block Explorer Error' ;
const err = common . handleError ( { statusCode : 500 , message : errMsg , error : errRes } , 'RTLConf' , errMsg , req . session . selectedNode ) ;
return res . status ( err . statusCode ) . json ( { message : err . error , error : err . error } ) ;
} ) ;
} ) ;
} ;
export const getCurrencyRates = ( req , res , next ) => {
logger . log ( { selectedNode : req . session . selectedNode , level : 'INFO' , fileName : 'RTLConf' , msg : 'Getting Currency Rates..' } ) ;
options . url = 'https://blockchain.info/ticker' ;
request ( options ) . then ( ( body ) => {
logger . log ( { selectedNode : req . session . selectedNode , level : 'INFO' , fileName : 'RTLConf' , msg : 'Currency Rates Received' , data : body } ) ;
res . status ( 200 ) . json ( JSON . parse ( body ) ) ;
} ) . catch ( ( errRes ) => {
const errMsg = 'Get Rates Error' ;
const err = common . handleError ( { statusCode : 500 , message : errMsg , error : errRes } , 'RTLConf' , errMsg , req . session . selectedNode ) ;
return res . status ( err . statusCode ) . json ( { message : err . error , error : err . error } ) ;
} ) ;
} ;
export const getFile = ( req , res , next ) => {
logger . log ( { selectedNode : req . session . selectedNode , level : 'INFO' , fileName : 'RTLConf' , msg : 'Getting File..' } ) ;
const file = req . query . path ? req . query . path : ( req . session . selectedNode . settings . channelBackupPath + sep + 'channel-' + req . query . channel ? . replace ( ':' , '-' ) + '.bak' ) ;
logger . log ( { selectedNode : req . session . selectedNode , level : 'DEBUG' , fileName : 'RTLConf' , msg : 'Channel Point' , data : req . query . channel } ) ;
logger . log ( { selectedNode : req . session . selectedNode , level : 'DEBUG' , fileName : 'RTLConf' , msg : 'File Path' , data : file } ) ;
fs . readFile ( file , 'utf8' , ( errRes , data ) => {
2021-12-29 18:08:41 -05:00
if ( errRes ) {
2024-06-10 12:40:37 -07:00
if ( errRes . code && errRes . code === 'ENOENT' ) {
errRes . code = 'File Not Found!' ;
2021-12-29 18:08:41 -05:00
}
2024-06-10 12:40:37 -07:00
const errMsg = 'Reading File Error' ;
const err = common . handleError ( { statusCode : 500 , message : errMsg , error : errRes } , 'RTLConf' , errMsg , req . session . selectedNode ) ;
return res . status ( err . statusCode ) . json ( { message : err . error , error : err . error } ) ;
2021-12-29 18:08:41 -05:00
}
else {
2024-06-10 12:40:37 -07:00
logger . log ( { selectedNode : req . session . selectedNode , level : 'INFO' , fileName : 'RTLConf' , msg : 'File Data Received' , data : data } ) ;
res . status ( 200 ) . json ( data ) ;
2021-12-29 18:08:41 -05:00
}
} ) ;
} ;
2024-06-10 12:40:37 -07:00
export const getApplicationSettings = ( req , res , next ) => {
2021-12-29 18:08:41 -05:00
logger . log ( { selectedNode : req . session . selectedNode , level : 'INFO' , fileName : 'RTLConf' , msg : 'Getting RTL Configuration..' } ) ;
2024-11-18 20:27:51 -08:00
const appConfData = common . removeSecureData ( JSON . parse ( JSON . stringify ( common . appConfig ) ) ) ;
appConfData . allowPasswordUpdate = common . appConfig . allowPasswordUpdate ;
appConfData . enable2FA = common . appConfig . enable2FA ;
appConfData . selectedNodeIndex = ( req . session . selectedNode && req . session . selectedNode . index ? req . session . selectedNode . index : common . selectedNode . index ) ;
common . appConfig . selectedNodeIndex = appConfData . selectedNodeIndex ;
const token = req . headers . authorization ? req . headers . authorization . split ( ' ' ) [ 1 ] : '' ;
jwt . verify ( token , common . secret _key , ( err , user ) => {
if ( err ) {
// Delete unnecessary data for initial response (without security token)
const selNodeIdx = appConfData . nodes . findIndex ( ( node ) => node . index === appConfData . selectedNodeIndex ) || 0 ;
delete appConfData . SSO . rtlCookiePath ;
delete appConfData . SSO . cookieValue ;
delete appConfData . SSO . logoutRedirectLink ;
appConfData . secret2FA = '' ;
appConfData . dbDirectoryPath = '' ;
appConfData . nodes [ selNodeIdx ] . authentication = new Authentication ( ) ;
delete appConfData . nodes [ selNodeIdx ] . settings . bitcoindConfigPath ;
delete appConfData . nodes [ selNodeIdx ] . settings . lnServerUrl ;
delete appConfData . nodes [ selNodeIdx ] . settings . swapServerUrl ;
delete appConfData . nodes [ selNodeIdx ] . settings . boltzServerUrl ;
delete appConfData . nodes [ selNodeIdx ] . settings . enableOffers ;
delete appConfData . nodes [ selNodeIdx ] . settings . enablePeerswap ;
delete appConfData . nodes [ selNodeIdx ] . settings . channelBackupPath ;
appConfData . nodes = [ appConfData . nodes [ selNodeIdx ] ] ;
2021-12-29 18:08:41 -05:00
}
2024-11-18 20:27:51 -08:00
logger . log ( { selectedNode : req . session . selectedNode , level : 'INFO' , fileName : 'RTLConf' , msg : 'RTL Configuration Received' , data : appConfData } ) ;
res . status ( 200 ) . json ( appConfData ) ;
2021-12-29 18:08:41 -05:00
} ) ;
} ;
2024-06-10 12:40:37 -07:00
export const updateSelectedNode = ( req , res , next ) => {
logger . log ( { selectedNode : req . session . selectedNode , level : 'INFO' , fileName : 'RTLConf' , msg : 'Updating Selected Node..' } ) ;
const selNodeIndex = req . params . currNodeIndex ? + req . params . currNodeIndex : common . selectedNode ? + common . selectedNode . index : 1 ;
req . session . selectedNode = common . findNode ( selNodeIndex ) ;
common . selectedNode = req . session . selectedNode ;
if ( req . headers && req . headers . authorization && req . headers . authorization !== '' ) {
wsServer . updateLNWSClientDetails ( req . session . id , + req . session . selectedNode . index , + req . params . prevNodeIndex ) ;
if ( req . params . prevNodeIndex !== '-1' ) {
databaseService . unloadDatabase ( req . params . prevNodeIndex , req . session . id ) ;
2021-12-29 18:08:41 -05:00
}
2024-06-10 12:40:37 -07:00
if ( req . params . currNodeIndex !== '-1' ) {
databaseService . loadDatabase ( req . session ) ;
2021-12-29 18:08:41 -05:00
}
}
2024-06-10 12:40:37 -07:00
blockExplorerUrl = '' ;
logger . log ( { selectedNode : req . session . selectedNode , level : 'INFO' , fileName : 'RTLConf' , msg : 'Selected Node Updated To ' + req . session . selectedNode . lnNode || '' } ) ;
res . status ( 200 ) . json ( common . removeAuthSecureData ( JSON . parse ( JSON . stringify ( req . session . selectedNode ) ) ) ) ;
2021-12-29 18:08:41 -05:00
} ;
export const getConfig = ( req , res , next ) => {
logger . log ( { selectedNode : req . session . selectedNode , level : 'INFO' , fileName : 'RTLConf' , msg : 'Reading Configuration File..' } ) ;
let confFile = '' ;
let fileFormat = 'INI' ;
switch ( req . params . nodeType ) {
case 'ln' :
2024-06-10 12:40:37 -07:00
confFile = req . session . selectedNode . authentication . configPath ;
2021-12-29 18:08:41 -05:00
break ;
case 'bitcoind' :
2024-06-10 12:40:37 -07:00
confFile = req . session . selectedNode . settings . bitcoindConfigPath ;
2021-12-29 18:08:41 -05:00
break ;
case 'rtl' :
fileFormat = 'JSON' ;
2024-06-10 12:40:37 -07:00
confFile = common . appConfig . rtlConfFilePath + sep + 'RTL-Config.json' ;
2021-12-29 18:08:41 -05:00
break ;
default :
confFile = '' ;
break ;
}
2022-01-16 15:55:50 -05:00
logger . log ( { selectedNode : req . session . selectedNode , level : 'DEBUG' , fileName : 'RTLConf' , msg : 'Node Type' , data : req . params . nodeType } ) ;
logger . log ( { selectedNode : req . session . selectedNode , level : 'DEBUG' , fileName : 'RTLConf' , msg : 'File Path' , data : confFile } ) ;
2021-12-29 18:08:41 -05:00
fs . readFile ( confFile , 'utf8' , ( errRes , data ) => {
if ( errRes ) {
const errMsg = 'Reading Config Error' ;
const err = common . handleError ( { statusCode : 500 , message : errMsg , error : errRes } , 'RTLConf' , errMsg , req . session . selectedNode ) ;
return res . status ( err . statusCode ) . json ( { message : err . error , error : err . error } ) ;
}
else {
let jsonConfig = { } ;
if ( fileFormat === 'JSON' ) {
jsonConfig = JSON . parse ( data ) ;
}
else {
fileFormat = 'INI' ;
2022-11-23 18:20:23 -08:00
data = data ? . replace ( 'color=#' , 'color=' ) ;
2021-12-29 18:08:41 -05:00
jsonConfig = ini . parse ( data ) ;
2022-01-16 15:55:50 -05:00
if ( jsonConfig [ 'Application Options' ] && jsonConfig [ 'Application Options' ] . color ) {
jsonConfig [ 'Application Options' ] . color = '#' + jsonConfig [ 'Application Options' ] . color ;
}
2024-06-10 12:40:37 -07:00
if ( req . params . nodeType === 'ln' && req . session . selectedNode . lnImplementation === 'ECL' && ! jsonConfig [ 'eclair.api.password' ] ) {
2021-12-29 18:08:41 -05:00
fileFormat = 'HOCON' ;
jsonConfig = parseHocon ( data ) ;
}
}
2024-06-10 12:40:37 -07:00
jsonConfig = common . maskPasswords ( jsonConfig ) ;
2022-11-23 18:20:23 -08:00
const responseJSON = ( fileFormat === 'JSON' ) ? jsonConfig : ini . stringify ( jsonConfig ) ? . replace ( 'color=\\#' , 'color=#' ) ;
2022-01-16 15:55:50 -05:00
logger . log ( { selectedNode : req . session . selectedNode , level : 'INFO' , fileName : 'RTLConf' , msg : 'Configuration File Data Received' , data : responseJSON } ) ;
2021-12-29 18:08:41 -05:00
res . status ( 200 ) . json ( { format : fileFormat , data : responseJSON } ) ;
}
} ) ;
} ;
2024-06-10 12:40:37 -07:00
export const updateNodeSettings = ( req , res , next ) => {
logger . log ( { selectedNode : req . session . selectedNode , level : 'INFO' , fileName : 'RTLConf' , msg : 'Updating Node Settings..' } ) ;
const RTLConfFile = common . appConfig . rtlConfFilePath + sep + 'RTL-Config.json' ;
2026-05-14 11:51:10 +02:00
try {
const config = JSON . parse ( fs . readFileSync ( RTLConfFile , 'utf-8' ) ) ;
const node = config . nodes . find ( ( node ) => ( node . index === req . session . selectedNode . index ) ) ;
if ( node && node . settings ) {
node . settings = { ... node . settings , ... req . body . settings } ;
if ( node . authentication && req . body . authentication ) {
if ( req . body . authentication . boltzMacaroonPath ) {
node . authentication . boltzMacaroonPath = req . body . authentication . boltzMacaroonPath ;
}
else {
delete node . authentication . boltzMacaroonPath ;
}
if ( req . body . authentication . swapMacaroonPath ) {
node . authentication . swapMacaroonPath = req . body . authentication . swapMacaroonPath ;
}
else {
delete node . authentication . swapMacaroonPath ;
}
2026-05-14 11:39:11 +02:00
}
}
fs . writeFileSync ( RTLConfFile , JSON . stringify ( config , null , 2 ) , 'utf-8' ) ;
const selectedNode = common . findNode ( req . session . selectedNode . index ) ;
if ( selectedNode && selectedNode . settings ) {
selectedNode . settings = { ... selectedNode . settings , ... req . body . settings } ;
if ( selectedNode . authentication && req . body . authentication ) {
if ( req . body . authentication . boltzMacaroonPath ) {
selectedNode . authentication . boltzMacaroonPath = req . body . authentication . boltzMacaroonPath ;
}
else {
delete selectedNode . authentication . boltzMacaroonPath ;
}
if ( req . body . authentication . swapMacaroonPath ) {
selectedNode . authentication . swapMacaroonPath = req . body . authentication . swapMacaroonPath ;
}
else {
delete selectedNode . authentication . swapMacaroonPath ;
}
2026-05-14 10:32:38 +02:00
}
2024-06-10 12:40:37 -07:00
common . replaceNode ( req , selectedNode ) ;
}
let responseNode = JSON . parse ( JSON . stringify ( common . selectedNode ) ) ;
responseNode = common . removeAuthSecureData ( responseNode ) ;
logger . log ( { selectedNode : req . session . selectedNode , level : 'INFO' , fileName : 'RTLConf' , msg : 'Node Settings Updated' , data : responseNode } ) ;
res . status ( 201 ) . json ( responseNode ) ;
2021-12-29 18:08:41 -05:00
}
catch ( errRes ) {
2024-06-10 12:40:37 -07:00
const errMsg = 'Update Node Settings Error' ;
2021-12-29 18:08:41 -05:00
const err = common . handleError ( { statusCode : 500 , message : errMsg , error : errRes } , 'RTLConf' , errMsg , req . session . selectedNode ) ;
return res . status ( err . statusCode ) . json ( { message : err . error , error : err . error } ) ;
}
} ;
2024-06-10 12:40:37 -07:00
export const updateApplicationSettings = ( req , res , next ) => {
logger . log ( { selectedNode : req . session . selectedNode , level : 'INFO' , fileName : 'RTLConf' , msg : 'Updating Application Settings..' } ) ;
const RTLConfFile = common . appConfig . rtlConfFilePath + sep + 'RTL-Config.json' ;
2021-12-29 18:08:41 -05:00
try {
2026-05-12 17:40:33 +02:00
const oldConfig = JSON . parse ( fs . readFileSync ( RTLConfFile , 'utf-8' ) ) ;
2026-05-14 10:32:38 +02:00
const config = common . addSecureData ( JSON . parse ( JSON . stringify ( req . body ) ) ) ;
2026-05-14 12:10:07 +02:00
const runtimeConfig = oldConfig ;
2026-05-12 17:40:33 +02:00
Object . keys ( config ) . forEach ( ( key ) => {
if ( key !== 'nodes' ) {
2026-05-14 10:32:38 +02:00
runtimeConfig [ key ] = config [ key ] ;
2026-05-12 17:40:33 +02:00
}
} ) ;
2026-05-14 10:32:38 +02:00
if ( config . nodes && config . nodes . length > 0 ) {
const oldNodes = ( common . appConfig . nodes && common . appConfig . nodes . length > 0 ) ? common . appConfig . nodes : ( oldConfig . nodes || [ ] ) ;
const newNodesMap = new Map ( config . nodes . map ( ( node ) => [ node . index , node ] ) ) ;
const updatedAndExistingNodes = oldNodes . map ( ( oldNode ) => {
const newNode = newNodesMap . get ( oldNode . index ) ;
newNodesMap . delete ( oldNode . index ) ;
2026-05-12 17:40:33 +02:00
const node = newNode ? {
... oldNode ,
... newNode ,
authentication : { ... ( oldNode . authentication || { } ) , ... ( newNode . authentication || { } ) } ,
settings : { ... ( oldNode . settings || { } ) , ... ( newNode . settings || { } ) }
} : {
... oldNode ,
authentication : { ... ( oldNode . authentication || { } ) } ,
settings : { ... ( oldNode . settings || { } ) }
} ;
return node ;
} ) ;
2026-05-14 10:32:38 +02:00
const newOnlyNodes = [ ... newNodesMap . values ( ) ] . map ( ( newNode ) => JSON . parse ( JSON . stringify ( newNode ) ) ) ;
runtimeConfig . nodes = [ ... updatedAndExistingNodes , ... newOnlyNodes ] ;
2026-05-12 17:40:33 +02:00
}
common . appConfig = JSON . parse ( JSON . stringify ( {
2026-05-14 10:32:38 +02:00
... runtimeConfig ,
2026-05-12 17:40:33 +02:00
selectedNodeIndex : config . selectedNodeIndex !== undefined ?
config . selectedNodeIndex : common . appConfig . selectedNodeIndex ,
enable2FA : config . enable2FA !== undefined ?
config . enable2FA : common . appConfig . enable2FA ,
allowPasswordUpdate : config . allowPasswordUpdate !== undefined ?
config . allowPasswordUpdate : common . appConfig . allowPasswordUpdate ,
rtlConfFilePath : common . appConfig . rtlConfFilePath ,
rtlPass : common . appConfig . rtlPass
} ) ) ;
2026-05-14 10:32:38 +02:00
const fileConfig = JSON . parse ( JSON . stringify ( common . appConfig ) ) ;
delete fileConfig . selectedNodeIndex ;
delete fileConfig . enable2FA ;
delete fileConfig . allowPasswordUpdate ;
delete fileConfig . rtlConfFilePath ;
delete fileConfig . rtlPass ;
2026-05-14 12:10:07 +02:00
delete fileConfig . multiPass ;
2026-05-14 10:32:38 +02:00
fileConfig . nodes ? . forEach ( ( node ) => {
delete node . authentication ? . options ;
delete node . authentication ? . runeValue ;
} ) ;
fs . writeFileSync ( RTLConfFile , JSON . stringify ( fileConfig , null , 2 ) , 'utf-8' ) ;
2024-06-10 12:40:37 -07:00
const newConfig = JSON . parse ( JSON . stringify ( common . appConfig ) ) ;
logger . log ( { selectedNode : req . session . selectedNode , level : 'INFO' , fileName : 'RTLConf' , msg : 'Application Settings Updated' , data : common . maskPasswords ( newConfig ) } ) ;
res . status ( 201 ) . json ( common . removeSecureData ( newConfig ) ) ;
2021-12-29 18:08:41 -05:00
}
catch ( errRes ) {
2024-06-10 12:40:37 -07:00
const errMsg = 'Update Default Node Error' ;
2021-12-29 18:08:41 -05:00
const err = common . handleError ( { statusCode : 500 , message : errMsg , error : errRes } , 'RTLConf' , errMsg , req . session . selectedNode ) ;
return res . status ( err . statusCode ) . json ( { message : err . error , error : err . error } ) ;
}
} ;