const tcpp = require ( "tcp-ping" ) ;
const ping = require ( "@louislam/ping" ) ;
const { R } = require ( "redbean-node" ) ;
const { log , genSecret , badgeConstants } = require ( "../src/util" ) ;
const passwordHash = require ( "./password-hash" ) ;
const { Resolver } = require ( "dns" ) ;
const iconv = require ( "iconv-lite" ) ;
const chardet = require ( "chardet" ) ;
const chroma = require ( "chroma-js" ) ;
const mssql = require ( "mssql" ) ;
const { Client } = require ( "pg" ) ;
const postgresConParse = require ( "pg-connection-string" ) . parse ;
const mysql = require ( "mysql2" ) ;
const { NtlmClient } = require ( "./modules/axios-ntlm/lib/ntlmClient.js" ) ;
const { Settings } = require ( "./settings" ) ;
const grpc = require ( "@grpc/grpc-js" ) ;
const protojs = require ( "protobufjs" ) ;
const radiusClient = require ( "node-radius-client" ) ;
const redis = require ( "redis" ) ;
const oidc = require ( "openid-client" ) ;
const tls = require ( "tls" ) ;
const {
dictionaries : {
rfc2865 : { file , attributes } ,
} ,
} = require ( "node-radius-utils" ) ;
const dayjs = require ( "dayjs" ) ;
// SASLOptions used in JSDoc
// eslint-disable-next-line no-unused-vars
const { Kafka , SASLOptions } = require ( "kafkajs" ) ;
const crypto = require ( "crypto" ) ;
const isWindows = process . platform === /^win/ . test ( process . platform ) ;
/ * *
* Init or reset JWT secret
* @ returns { Promise < Bean > } JWT secret
* /
exports . initJWTSecret = async ( ) => {
let jwtSecretBean = await R . findOne ( "setting" , " `key` = ? " , [
"jwtSecret" ,
] ) ;
if ( ! jwtSecretBean ) {
jwtSecretBean = R . dispense ( "setting" ) ;
jwtSecretBean . key = "jwtSecret" ;
}
jwtSecretBean . value = passwordHash . generate ( genSecret ( ) ) ;
await R . store ( jwtSecretBean ) ;
return jwtSecretBean ;
} ;
/ * *
* Decodes a jwt and returns the payload portion without verifying the jqt .
* @ param { string } jwt The input jwt as a string
* @ returns { object } Decoded jwt payload object
* /
exports . decodeJwt = ( jwt ) => {
return JSON . parse ( Buffer . from ( jwt . split ( "." ) [ 1 ] , "base64" ) . toString ( ) ) ;
} ;
/ * *
* Gets a Access Token form a oidc / oauth2 provider
* @ param { string } tokenEndpoint The token URI form the auth service provider
* @ param { string } clientId The oidc / oauth application client id
* @ param { string } clientSecret The oidc / oauth application client secret
* @ param { string } scope The scope the for which the token should be issued for
* @ param { string } authMethod The method on how to sent the credentials . Default client _secret _basic
* @ returns { Promise < oidc . TokenSet > } TokenSet promise if the token request was successful
* /
exports . getOidcTokenClientCredentials = async ( tokenEndpoint , clientId , clientSecret , scope , authMethod = "client_secret_basic" ) => {
const oauthProvider = new oidc . Issuer ( { token _endpoint : tokenEndpoint } ) ;
let client = new oauthProvider . Client ( {
client _id : clientId ,
client _secret : clientSecret ,
token _endpoint _auth _method : authMethod
} ) ;
// Increase default timeout and clock tolerance
client [ oidc . custom . http _options ] = ( ) => ( { timeout : 10000 } ) ;
client [ oidc . custom . clock _tolerance ] = 5 ;
let grantParams = { grant _type : "client_credentials" } ;
if ( scope ) {
grantParams . scope = scope ;
}
return await client . grant ( grantParams ) ;
} ;
/ * *
* Send TCP request to specified hostname and port
* @ param { string } hostname Hostname / address of machine
* @ param { number } port TCP port to test
* @ returns { Promise < number > } Maximum time in ms rounded to nearest integer
* /
exports . tcping = function ( hostname , port ) {
return new Promise ( ( resolve , reject ) => {
tcpp . ping ( {
address : hostname ,
port : port ,
attempts : 1 ,
} , function ( err , data ) {
if ( err ) {
reject ( err ) ;
}
if ( data . results . length >= 1 && data . results [ 0 ] . err ) {
reject ( data . results [ 0 ] . err ) ;
}
resolve ( Math . round ( data . max ) ) ;
} ) ;
} ) ;
} ;
/ * *
* Ping the specified machine
* @ param { string } hostname Hostname / address of machine
* @ param { number } size Size of packet to send
* @ returns { Promise < number > } Time for ping in ms rounded to nearest integer
* /
exports . ping = async ( hostname , size = 56 ) => {
try {
return await exports . pingAsync ( hostname , false , size ) ;
} catch ( e ) {
// If the host cannot be resolved, try again with ipv6
log . debug ( "ping" , "IPv6 error message: " + e . message ) ;
// As node-ping does not report a specific error for this, try again if it is an empty message with ipv6 no matter what.
if ( ! e . message ) {
return await exports . pingAsync ( hostname , true , size ) ;
} else {
throw e ;
}
}
} ;
/ * *
* Ping the specified machine
* @ param { string } hostname Hostname / address of machine to ping
* @ param { boolean } ipv6 Should IPv6 be used ?
* @ param { number } size Size of ping packet to send
* @ returns { Promise < number > } Time for ping in ms rounded to nearest integer
* /
exports . pingAsync = function ( hostname , ipv6 = false , size = 56 ) {
return new Promise ( ( resolve , reject ) => {
ping . promise . probe ( hostname , {
v6 : ipv6 ,
min _reply : 1 ,
deadline : 10 ,
packetSize : size ,
} ) . then ( ( res ) => {
// If ping failed, it will set field to unknown
if ( res . alive ) {
resolve ( res . time ) ;
} else {
if ( isWindows ) {
reject ( new Error ( exports . convertToUTF8 ( res . output ) ) ) ;
} else {
reject ( new Error ( res . output ) ) ;
}
}
} ) . catch ( ( err ) => {
reject ( err ) ;
} ) ;
} ) ;
} ;
/ * *
* Monitor Kafka using Producer
* @ param { string [ ] } brokers List of kafka brokers to connect , host and
* port joined by ':'
* @ param { string } topic Topic name to produce into
* @ param { string } message Message to produce
* @ param { object } options Kafka client options . Contains ssl , clientId ,
* allowAutoTopicCreation and interval ( interval defaults to 20 ,
* allowAutoTopicCreation defaults to false , clientId defaults to
* "Uptime-Kuma" and ssl defaults to false )
* @ param { SASLOptions } saslOptions Options for kafka client
* Authentication ( SASL ) ( defaults to { } )
* @ returns { Promise < string > } Status message
* /
exports . kafkaProducerAsync = function ( brokers , topic , message , options = { } , saslOptions = { } ) {
return new Promise ( ( resolve , reject ) => {
const { interval = 20 , allowAutoTopicCreation = false , ssl = false , clientId = "Uptime-Kuma" } = options ;
let connectedToKafka = false ;
const timeoutID = setTimeout ( ( ) => {
log . debug ( "kafkaProducer" , "KafkaProducer timeout triggered" ) ;
connectedToKafka = true ;
reject ( new Error ( "Timeout" ) ) ;
} , interval * 1000 * 0.8 ) ;
if ( saslOptions . mechanism === "None" ) {
saslOptions = undefined ;
}
let client = new Kafka ( {
brokers : brokers ,
clientId : clientId ,
sasl : saslOptions ,
retry : {
retries : 0 ,
} ,
ssl : ssl ,
} ) ;
let producer = client . producer ( {
allowAutoTopicCreation : allowAutoTopicCreation ,
retry : {
retries : 0 ,
}
} ) ;
producer . connect ( ) . then (
( ) => {
producer . send ( {
topic : topic ,
messages : [ {
value : message ,
} ] ,
} ) . then ( ( _ ) => {
resolve ( "Message sent successfully" ) ;
} ) . catch ( ( e ) => {
connectedToKafka = true ;
producer . disconnect ( ) ;
clearTimeout ( timeoutID ) ;
reject ( new Error ( "Error sending message: " + e . message ) ) ;
} ) . finally ( ( ) => {
connectedToKafka = true ;
clearTimeout ( timeoutID ) ;
} ) ;
}
) . catch (
( e ) => {
connectedToKafka = true ;
producer . disconnect ( ) ;
clearTimeout ( timeoutID ) ;
reject ( new Error ( "Error in producer connection: " + e . message ) ) ;
}
) ;
producer . on ( "producer.network.request_timeout" , ( _ ) => {
if ( ! connectedToKafka ) {
clearTimeout ( timeoutID ) ;
reject ( new Error ( "producer.network.request_timeout" ) ) ;
}
} ) ;
producer . on ( "producer.disconnect" , ( _ ) => {
if ( ! connectedToKafka ) {
clearTimeout ( timeoutID ) ;
reject ( new Error ( "producer.disconnect" ) ) ;
}
} ) ;
} ) ;
} ;
/ * *
* Use NTLM Auth for a http request .
* @ param { object } options The http request options
* @ param { object } ntlmOptions The auth options
* @ returns { Promise < ( string [ ] | object [ ] | object ) > } NTLM response
* /
exports . httpNtlm = function ( options , ntlmOptions ) {
return new Promise ( ( resolve , reject ) => {
let client = NtlmClient ( ntlmOptions ) ;
client ( options )
. then ( ( resp ) => {
resolve ( resp ) ;
} )
. catch ( ( err ) => {
reject ( err ) ;
} ) ;
} ) ;
} ;
/ * *
* Resolves a given record using the specified DNS server
* @ param { string } hostname The hostname of the record to lookup
* @ param { string } resolverServer The DNS server to use
* @ param { string } resolverPort Port the DNS server is listening on
* @ param { string } rrtype The type of record to request
* @ returns { Promise < ( string [ ] | object [ ] | object ) > } DNS response
* /
exports . dnsResolve = function ( hostname , resolverServer , resolverPort , rrtype ) {
const resolver = new Resolver ( ) ;
// Remove brackets from IPv6 addresses so we can re-add them to
// prevent issues with ::1:5300 (::1 port 5300)
resolverServer = resolverServer . replace ( "[" , "" ) . replace ( "]" , "" ) ;
resolver . setServers ( [ ` [ ${ resolverServer } ]: ${ resolverPort } ` ] ) ;
return new Promise ( ( resolve , reject ) => {
if ( rrtype === "PTR" ) {
resolver . reverse ( hostname , ( err , records ) => {
if ( err ) {
reject ( err ) ;
} else {
resolve ( records ) ;
}
} ) ;
} else {
resolver . resolve ( hostname , rrtype , ( err , records ) => {
if ( err ) {
reject ( err ) ;
} else {
resolve ( records ) ;
}
} ) ;
}
} ) ;
} ;
/ * *
* Run a query on SQL Server
* @ param { string } connectionString The database connection string
* @ param { boolean } handleEmptyResult Should empty results be handled as an error
* @ param { string } query The query to validate the database with
* @ returns { Promise < ( string [ ] | object [ ] | object ) > } Response from
* server
* /
exports . mssqlQuery = async function ( connectionString , handleEmptyResult , query ) {
let pool ;
try {
pool = new mssql . ConnectionPool ( connectionString ) ;
await pool . connect ( ) ;
if ( ! query ) {
query = "SELECT 1" ;
}
const result = await pool . request ( ) . query ( query ) ;
if ( Array . isArray ( result . recordset ) ) {
if ( handleEmptyResult && result . recordset . length === 0 ) {
throw new Error ( "No rows returned" ) ;
}
}
pool . close ( ) ;
} catch ( e ) {
if ( pool ) {
pool . close ( ) ;
}
throw e ;
}
} ;
/ * *
* Run a query on Postgres
* @ param { string } connectionString The database connection string
* @ param { boolean } handleEmptyResult Should empty results be handled as an error
* @ param { string } query The query to validate the database with
* @ returns { Promise < ( string [ ] | object [ ] | object ) > } Response from
* server
* /
exports . postgresQuery = function ( connectionString , handleEmptyResult , query ) {
return new Promise ( ( resolve , reject ) => {
const config = postgresConParse ( connectionString ) ;
// Fix #3868, which true/false is not parsed to boolean
if ( typeof config . ssl === "string" ) {
config . ssl = config . ssl === "true" ;
}
if ( config . password === "" ) {
// See https://github.com/brianc/node-postgres/issues/1927
reject ( new Error ( "Password is undefined." ) ) ;
return ;
}
const client = new Client ( config ) ;
client . on ( "error" , ( error ) => {
log . debug ( "postgres" , "Error caught in the error event handler." ) ;
reject ( error ) ;
} ) ;
client . connect ( ( err ) => {
if ( err ) {
reject ( err ) ;
client . end ( ) ;
} else {
// Connected here
try {
// No query provided by user, use SELECT 1
if ( ! query || ( typeof query === "string" && query . trim ( ) === "" ) ) {
query = "SELECT 1" ;
}
client . query ( query , ( err , res ) => {
if ( err ) {
reject ( err ) ;
} else {
if ( Array . isArray ( res . rows ) ) {
if ( handleEmptyResult && res . rows . length === 0 ) {
reject ( new Error ( "No rows returned" ) ) ;
} else {
resolve ( res ) ;
}
}
}
client . end ( ) ;
} ) ;
} catch ( e ) {
reject ( e ) ;
client . end ( ) ;
}
}
} ) ;
} ) ;
} ;
/ * *
* Run a query on MySQL / MariaDB
* @ param { string } connectionString The database connection string
* @ param { boolean } handleEmptyResult Should empty results be handled as an error
* @ param { string } query The query to validate the database with
* @ param { ? string } password The password to use
* @ returns { Promise < ( string ) > } Response from server
* /
exports . mysqlQuery = function ( connectionString , handleEmptyResult , query , password = undefined ) {
return new Promise ( ( resolve , reject ) => {
const connection = mysql . createConnection ( {
uri : connectionString ,
password
} ) ;
connection . on ( "error" , ( err ) => {
reject ( err ) ;
} ) ;
connection . query ( query , ( err , res ) => {
if ( err ) {
reject ( err ) ;
} else {
if ( Array . isArray ( res ) ) {
if ( handleEmptyResult && res . length === 0 ) {
reject ( new Error ( "No rows returned" ) ) ;
} else {
resolve ( "Rows: " + res . length ) ;
}
} else {
resolve ( "No Error, but the result is not an array. Type: " + typeof res ) ;
}
}
try {
connection . end ( ) ;
} catch ( _ ) {
connection . destroy ( ) ;
}
} ) ;
} ) ;
} ;
/ * *
* Query radius server
* @ param { string } hostname Hostname of radius server
* @ param { string } username Username to use
* @ param { string } password Password to use
* @ param { string } calledStationId ID of called station
* @ param { string } callingStationId ID of calling station
* @ param { string } secret Secret to use
* @ param { number } port Port to contact radius server on
* @ param { number } timeout Timeout for connection to use
* @ returns { Promise < any > } Response from server
* /
exports . radius = function (
hostname ,
username ,
password ,
calledStationId ,
callingStationId ,
secret ,
port = 1812 ,
timeout = 2500 ,
) {
const client = new radiusClient ( {
host : hostname ,
hostPort : port ,
timeout : timeout ,
retries : 1 ,
dictionaries : [ file ] ,
} ) ;
return client . accessRequest ( {
secret : secret ,
attributes : [
[ attributes . USER _NAME , username ] ,
[ attributes . USER _PASSWORD , password ] ,
[ attributes . CALLING _STATION _ID , callingStationId ] ,
[ attributes . CALLED _STATION _ID , calledStationId ] ,
] ,
} ) . catch ( ( error ) => {
if ( error . response ? . code ) {
throw Error ( error . response . code ) ;
} else {
throw Error ( error . message ) ;
}
} ) ;
} ;
/ * *
* Redis server ping
* @ param { string } dsn The redis connection string
* @ param { boolean } rejectUnauthorized If false , allows unverified server certificates .
* @ returns { Promise < any > } Response from server
* /
exports . redisPingAsync = function ( dsn , rejectUnauthorized ) {
return new Promise ( ( resolve , reject ) => {
const client = redis . createClient ( {
url : dsn ,
socket : {
rejectUnauthorized
}
} ) ;
client . on ( "error" , ( err ) => {
if ( client . isOpen ) {
client . disconnect ( ) ;
}
reject ( err ) ;
} ) ;
client . connect ( ) . then ( ( ) => {
if ( ! client . isOpen ) {
client . emit ( "error" , new Error ( "connection isn't open" ) ) ;
}
client . ping ( ) . then ( ( res , err ) => {
if ( client . isOpen ) {
client . disconnect ( ) ;
}
if ( err ) {
reject ( err ) ;
} else {
resolve ( res ) ;
}
} ) . catch ( error => reject ( error ) ) ;
} ) ;
} ) ;
} ;
/ * *
* Retrieve value of setting based on key
* @ param { string } key Key of setting to retrieve
* @ returns { Promise < any > } Value
* @ deprecated Use await Settings . get ( key )
* /
exports . setting = async function ( key ) {
return await Settings . get ( key ) ;
} ;
/ * *
* Sets the specified setting to specified value
* @ param { string } key Key of setting to set
* @ param { any } value Value to set to
* @ param { ? string } type Type of setting
* @ returns { Promise < void > }
* /
exports . setSetting = async function ( key , value , type = null ) {
await Settings . set ( key , value , type ) ;
} ;
/ * *
* Get settings based on type
* @ param { string } type The type of setting
* @ returns { Promise < Bean > } Settings of requested type
* /
exports . getSettings = async function ( type ) {
return await Settings . getSettings ( type ) ;
} ;
/ * *
* Set settings based on type
* @ param { string } type Type of settings to set
* @ param { object } data Values of settings
* @ returns { Promise < void > }
* /
exports . setSettings = async function ( type , data ) {
await Settings . setSettings ( type , data ) ;
} ;
// ssl-checker by @dyaa
//https://github.com/dyaa/ssl-checker/blob/master/src/index.ts
/ * *
* Get number of days between two dates
* @ param { Date } validFrom Start date
* @ param { Date } validTo End date
* @ returns { number } Number of days
* /
const getDaysBetween = ( validFrom , validTo ) =>
Math . round ( Math . abs ( + validFrom - + validTo ) / 8.64 e7 ) ;
/ * *
* Get days remaining from a time range
* @ param { Date } validFrom Start date
* @ param { Date } validTo End date
* @ returns { number } Number of days remaining
* /
const getDaysRemaining = ( validFrom , validTo ) => {
const daysRemaining = getDaysBetween ( validFrom , validTo ) ;
if ( new Date ( validTo ) . getTime ( ) < new Date ( ) . getTime ( ) ) {
return - daysRemaining ;
}
return daysRemaining ;
} ;
/ * *
* Fix certificate info for display
* @ param { object } info The chain obtained from getPeerCertificate ( )
* @ returns { object } An object representing certificate information
* @ throws The certificate chain length exceeded 500.
* /
const parseCertificateInfo = function ( info ) {
let link = info ;
let i = 0 ;
const existingList = { } ;
while ( link ) {
log . debug ( "cert" , ` [ ${ i } ] ${ link . fingerprint } ` ) ;
if ( ! link . valid _from || ! link . valid _to ) {
break ;
}
link . validTo = new Date ( link . valid _to ) ;
link . validFor = link . subjectaltname ? . replace ( /DNS:|IP Address:/g , "" ) . split ( ", " ) ;
link . daysRemaining = getDaysRemaining ( new Date ( ) , link . validTo ) ;
existingList [ link . fingerprint ] = true ;
// Move up the chain until loop is encountered
if ( link . issuerCertificate == null ) {
link . certType = ( i === 0 ) ? "self-signed" : "root CA" ;
break ;
} else if ( link . issuerCertificate . fingerprint in existingList ) {
// a root CA certificate is typically "signed by itself" (=> "self signed certificate") and thus the "issuerCertificate" is a reference to itself.
log . debug ( "cert" , ` [Last] ${ link . issuerCertificate . fingerprint } ` ) ;
link . certType = ( i === 0 ) ? "self-signed" : "root CA" ;
link . issuerCertificate = null ;
break ;
} else {
link . certType = ( i === 0 ) ? "server" : "intermediate CA" ;
link = link . issuerCertificate ;
}
// Should be no use, but just in case.
if ( i > 500 ) {
throw new Error ( "Dead loop occurred in parseCertificateInfo" ) ;
}
i ++ ;
}
return info ;
} ;
/ * *
* Check if certificate is valid
* @ param { tls . TLSSocket } socket TLSSocket , which may or may not be connected
* @ returns { object } Object containing certificate information
* /
exports . checkCertificate = function ( socket ) {
let certInfoStartTime = dayjs ( ) . valueOf ( ) ;
// Return null if there is no socket
if ( socket === undefined || socket == null ) {
return null ;
}
const info = socket . getPeerCertificate ( true ) ;
const valid = socket . authorized || false ;
log . debug ( "cert" , "Parsing Certificate Info" ) ;
const parsedInfo = parseCertificateInfo ( info ) ;
if ( process . env . TIMELOGGER === "1" ) {
log . debug ( "monitor" , "Cert Info Query Time: " + ( dayjs ( ) . valueOf ( ) - certInfoStartTime ) + "ms" ) ;
}
return {
valid : valid ,
certInfo : parsedInfo
} ;
} ;
/ * *
* Check if the provided status code is within the accepted ranges
* @ param { number } status The status code to check
* @ param { string [ ] } acceptedCodes An array of accepted status codes
* @ returns { boolean } True if status code within range , false otherwise
* /
exports . checkStatusCode = function ( status , acceptedCodes ) {
if ( acceptedCodes == null || acceptedCodes . length === 0 ) {
return false ;
}
for ( const codeRange of acceptedCodes ) {
if ( typeof codeRange !== "string" ) {
log . error ( "monitor" , ` Accepted status code not a string. ${ codeRange } is of type ${ typeof codeRange } ` ) ;
continue ;
}
const codeRangeSplit = codeRange . split ( "-" ) . map ( string => parseInt ( string ) ) ;
if ( codeRangeSplit . length === 1 ) {
if ( status === codeRangeSplit [ 0 ] ) {
return true ;
}
} else if ( codeRangeSplit . length === 2 ) {
if ( status >= codeRangeSplit [ 0 ] && status <= codeRangeSplit [ 1 ] ) {
return true ;
}
} else {
log . error ( "monitor" , ` ${ codeRange } is not a valid status code range ` ) ;
continue ;
}
}
return false ;
} ;
/ * *
* Get total number of clients in room
* @ param { Server } io Socket server instance
* @ param { string } roomName Name of room to check
* @ returns { number } Total clients in room
* /
exports . getTotalClientInRoom = ( io , roomName ) => {
const sockets = io . sockets ;
if ( ! sockets ) {
return 0 ;
}
const adapter = sockets . adapter ;
if ( ! adapter ) {
return 0 ;
}
const room = adapter . rooms . get ( roomName ) ;
if ( room ) {
return room . size ;
} else {
return 0 ;
}
} ;
/ * *
* Allow CORS all origins if development
* @ param { object } res Response object from axios
* @ returns { void }
* /
exports . allowDevAllOrigin = ( res ) => {
if ( process . env . NODE _ENV === "development" ) {
exports . allowAllOrigin ( res ) ;
}
} ;
/ * *
* Allow CORS all origins
* @ param { object } res Response object from axios
* @ returns { void }
* /
exports . allowAllOrigin = ( res ) => {
res . header ( "Access-Control-Allow-Origin" , "*" ) ;
res . header ( "Access-Control-Allow-Methods" , "GET, PUT, POST, DELETE, OPTIONS" ) ;
res . header ( "Access-Control-Allow-Headers" , "Origin, X-Requested-With, Content-Type, Accept" ) ;
} ;
/ * *
* Check if a user is logged in
* @ param { Socket } socket Socket instance
* @ returns { void }
* @ throws The user is not logged in
* /
exports . checkLogin = ( socket ) => {
if ( ! socket . userID ) {
throw new Error ( "You are not logged in." ) ;
}
} ;
/ * *
* For logged - in users , double - check the password
* @ param { Socket } socket Socket . io instance
* @ param { string } currentPassword Password to validate
* @ returns { Promise < Bean > } User
* @ throws The current password is not a string
* @ throws The provided password is not correct
* /
exports . doubleCheckPassword = async ( socket , currentPassword ) => {
if ( typeof currentPassword !== "string" ) {
throw new Error ( "Wrong data type?" ) ;
}
let user = await R . findOne ( "user" , " id = ? AND active = 1 " , [
socket . userID ,
] ) ;
if ( ! user || ! passwordHash . verify ( currentPassword , user . password ) ) {
throw new Error ( "Incorrect current password" ) ;
}
return user ;
} ;
/ * *
* Convert unknown string to UTF8
* @ param { Uint8Array } body Buffer
* @ returns { string } UTF8 string
* /
exports . convertToUTF8 = ( body ) => {
const guessEncoding = chardet . detect ( body ) ;
const str = iconv . decode ( body , guessEncoding ) ;
return str . toString ( ) ;
} ;
/ * *
* Returns a color code in hex format based on a given percentage :
* 0 % => hue = 10 => red
* 100 % => hue = 90 => green
* @ param { number } percentage float , 0 to 1
* @ param { number } maxHue Maximum hue - int
* @ param { number } minHue Minimum hue - int
* @ returns { string } Color in hex
* /
exports . percentageToColor = ( percentage , maxHue = 90 , minHue = 10 ) => {
const hue = percentage * ( maxHue - minHue ) + minHue ;
try {
return chroma ( ` hsl( ${ hue } , 90%, 40%) ` ) . hex ( ) ;
} catch ( err ) {
return badgeConstants . naColor ;
}
} ;
/ * *
* Joins and array of string to one string after filtering out empty values
* @ param { string [ ] } parts Strings to join
* @ param { string } connector Separator for joined strings
* @ returns { string } Joined strings
* /
exports . filterAndJoin = ( parts , connector = "" ) => {
return parts . filter ( ( part ) => ! ! part && part !== "" ) . join ( connector ) ;
} ;
/ * *
* Send an Error response
* @ param { object } res Express response object
* @ param { string } msg Message to send
* @ returns { void }
* /
module . exports . sendHttpError = ( res , msg = "" ) => {
if ( msg . includes ( "SQLITE_BUSY" ) || msg . includes ( "SQLITE_LOCKED" ) ) {
res . status ( 503 ) . json ( {
"status" : "fail" ,
"msg" : msg ,
} ) ;
} else if ( msg . toLowerCase ( ) . includes ( "not found" ) ) {
res . status ( 404 ) . json ( {
"status" : "fail" ,
"msg" : msg ,
} ) ;
} else {
res . status ( 403 ) . json ( {
"status" : "fail" ,
"msg" : msg ,
} ) ;
}
} ;
/ * *
* Convert timezone of time object
* @ param { object } obj Time object to update
* @ param { string } timezone New timezone to set
* @ param { boolean } timeObjectToUTC Convert time object to UTC
* @ returns { object } Time object with updated timezone
* /
function timeObjectConvertTimezone ( obj , timezone , timeObjectToUTC = true ) {
let offsetString ;
if ( timezone ) {
offsetString = dayjs ( ) . tz ( timezone ) . format ( "Z" ) ;
} else {
offsetString = dayjs ( ) . format ( "Z" ) ;
}
let hours = parseInt ( offsetString . substring ( 1 , 3 ) ) ;
let minutes = parseInt ( offsetString . substring ( 4 , 6 ) ) ;
if (
( timeObjectToUTC && offsetString . startsWith ( "+" ) ) ||
( ! timeObjectToUTC && offsetString . startsWith ( "-" ) )
) {
hours *= - 1 ;
minutes *= - 1 ;
}
obj . hours += hours ;
obj . minutes += minutes ;
// Handle out of bound
if ( obj . minutes < 0 ) {
obj . minutes += 60 ;
obj . hours -- ;
} else if ( obj . minutes > 60 ) {
obj . minutes -= 60 ;
obj . hours ++ ;
}
if ( obj . hours < 0 ) {
obj . hours += 24 ;
} else if ( obj . hours > 24 ) {
obj . hours -= 24 ;
}
return obj ;
}
/ * *
* Convert time object to UTC
* @ param { object } obj Object to convert
* @ param { string } timezone Timezone of time object
* @ returns { object } Updated time object
* /
module . exports . timeObjectToUTC = ( obj , timezone = undefined ) => {
return timeObjectConvertTimezone ( obj , timezone , true ) ;
} ;
/ * *
* Convert time object to local time
* @ param { object } obj Object to convert
* @ param { string } timezone Timezone to convert to
* @ returns { object } Updated object
* /
module . exports . timeObjectToLocal = ( obj , timezone = undefined ) => {
return timeObjectConvertTimezone ( obj , timezone , false ) ;
} ;
/ * *
* Create gRPC client stib
* @ param { object } options from gRPC client
* @ returns { Promise < object > } Result of gRPC query
* /
module . exports . grpcQuery = async ( options ) => {
const { grpcUrl , grpcProtobufData , grpcServiceName , grpcEnableTls , grpcMethod , grpcBody } = options ;
const protocObject = protojs . parse ( grpcProtobufData ) ;
const protoServiceObject = protocObject . root . lookupService ( grpcServiceName ) ;
const Client = grpc . makeGenericClientConstructor ( { } ) ;
const credentials = grpcEnableTls ? grpc . credentials . createSsl ( ) : grpc . credentials . createInsecure ( ) ;
const client = new Client (
grpcUrl ,
credentials
) ;
const grpcService = protoServiceObject . create ( function ( method , requestData , cb ) {
const fullServiceName = method . fullName ;
const serviceFQDN = fullServiceName . split ( "." ) ;
const serviceMethod = serviceFQDN . pop ( ) ;
const serviceMethodClientImpl = ` / ${ serviceFQDN . slice ( 1 ) . join ( "." ) } / ${ serviceMethod } ` ;
log . debug ( "monitor" , ` gRPC method ${ serviceMethodClientImpl } ` ) ;
client . makeUnaryRequest (
serviceMethodClientImpl ,
arg => arg ,
arg => arg ,
requestData ,
cb ) ;
} , false , false ) ;
return new Promise ( ( resolve , _ ) => {
try {
return grpcService [ ` ${ grpcMethod } ` ] ( JSON . parse ( grpcBody ) , function ( err , response ) {
const responseData = JSON . stringify ( response ) ;
if ( err ) {
return resolve ( {
code : err . code ,
errorMessage : err . details ,
data : ""
} ) ;
} else {
log . debug ( "monitor:" , ` gRPC response: ${ JSON . stringify ( response ) } ` ) ;
return resolve ( {
code : 1 ,
errorMessage : "" ,
data : responseData
} ) ;
}
} ) ;
} catch ( err ) {
return resolve ( {
code : - 1 ,
errorMessage : ` Error ${ err } . Please review your gRPC configuration option. The service name must not include package name value, and the method name must follow camelCase format ` ,
data : ""
} ) ;
}
} ) ;
} ;
/ * *
* Returns an array of SHA256 fingerprints for all known root certificates .
* @ returns { Set } A set of SHA256 fingerprints .
* /
module . exports . rootCertificatesFingerprints = ( ) => {
let fingerprints = tls . rootCertificates . map ( cert => {
let certLines = cert . split ( "\n" ) ;
certLines . shift ( ) ;
certLines . pop ( ) ;
let certBody = certLines . join ( "" ) ;
let buf = Buffer . from ( certBody , "base64" ) ;
const shasum = crypto . createHash ( "sha256" ) ;
shasum . update ( buf ) ;
return shasum . digest ( "hex" ) . toUpperCase ( ) . replace ( /(.{2})(?!$)/g , "$1:" ) ;
} ) ;
fingerprints . push ( "6D:99:FB:26:5E:B1:C5:B3:74:47:65:FC:BC:64:8F:3C:D8:E1:BF:FA:FD:C4:C2:F9:9B:9D:47:CF:7F:F1:C2:4F" ) ; // ISRG X1 cross-signed with DST X3
fingerprints . push ( "8B:05:B6:8C:C6:59:E5:ED:0F:CB:38:F2:C9:42:FB:FD:20:0E:6F:2F:F9:F8:5D:63:C6:99:4E:F5:E0:B0:27:01" ) ; // ISRG X2 cross-signed with ISRG X1
return new Set ( fingerprints ) ;
} ;
module . exports . SHAKE256 _LENGTH = 16 ;
/ * *
* @ param { string } data The data to be hashed
* @ param { number } len Output length of the hash
* @ returns { string } The hashed data in hex format
* /
module . exports . shake256 = ( data , len ) => {
if ( ! data ) {
return "" ;
}
return crypto . createHash ( "shake256" , { outputLength : len } )
. update ( data )
. digest ( "hex" ) ;
} ;
/ * *
* Non await sleep
* Source : https : //stackoverflow.com/questions/59099454/is-there-a-way-to-call-sleep-without-await-keyword
* @ param { number } n Milliseconds to wait
* @ returns { void }
* /
module . exports . wait = ( n ) => {
Atomics . wait ( new Int32Array ( new SharedArrayBuffer ( 4 ) ) , 0 , 0 , n ) ;
} ;
// For unit test, export functions
if ( process . env . TEST _BACKEND ) {
module . exports . _ _test = {
parseCertificateInfo ,
} ;
module . exports . _ _getPrivateFunction = ( functionName ) => {
return module . exports . _ _test [ functionName ] ;
} ;
}
/ * *
* Generates an abort signal with the specified timeout .
* @ param { number } timeoutMs - The timeout in milliseconds .
* @ returns { AbortSignal | null } - The generated abort signal , or null if not supported .
* /
module . exports . axiosAbortSignal = ( timeoutMs ) => {
try {
// Just in case, as 0 timeout here will cause the request to be aborted immediately
if ( ! timeoutMs || timeoutMs <= 0 ) {
timeoutMs = 5000 ;
}
return AbortSignal . timeout ( timeoutMs ) ;
} catch ( _ ) {
// v16-: AbortSignal.timeout is not supported
try {
const abortController = new AbortController ( ) ;
setTimeout ( ( ) => abortController . abort ( ) , timeoutMs ) ;
return abortController . signal ;
} catch ( _ ) {
// v15-: AbortController is not supported
return null ;
}
}
} ;