2021-07-27 19:47:13 +02:00
const tcpp = require ( "tcp-ping" ) ;
2023-01-11 19:04:58 +02:00
const ping = require ( "@louislam/ping" ) ;
2021-07-27 19:47:13 +02:00
const { R } = require ( "redbean-node" ) ;
2022-04-13 17:33:37 +02:00
const { log , genSecret } = require ( "../src/util" ) ;
2021-08-09 07:34:44 +02:00
const passwordHash = require ( "./password-hash" ) ;
2021-08-23 16:30:11 +02:00
const { Resolver } = require ( "dns" ) ;
2022-04-13 18:30:32 +02:00
const childProcess = require ( "child_process" ) ;
2021-10-13 18:22:49 +02:00
const iconv = require ( "iconv-lite" ) ;
const chardet = require ( "chardet" ) ;
2021-11-04 03:46:43 +02:00
const mqtt = require ( "mqtt" ) ;
2022-01-03 16:48:52 +02:00
const chroma = require ( "chroma-js" ) ;
2022-01-04 13:21:53 +02:00
const { badgeConstants } = require ( "./config" ) ;
2022-05-13 15:40:46 +02:00
const mssql = require ( "mssql" ) ;
2022-06-15 19:12:47 +02:00
const { Client } = require ( "pg" ) ;
2022-06-15 20:00:14 +02:00
const postgresConParse = require ( "pg-connection-string" ) . parse ;
2022-11-17 20:34:02 +02:00
const mysql = require ( "mysql2" ) ;
2022-11-17 03:50:34 +02:00
const { MongoClient } = require ( "mongodb" ) ;
2022-05-13 19:58:23 +02:00
const { NtlmClient } = require ( "axios-ntlm" ) ;
2022-06-29 08:57:40 +02:00
const { Settings } = require ( "./settings" ) ;
2022-08-03 07:00:39 +02:00
const grpc = require ( "@grpc/grpc-js" ) ;
const protojs = require ( "protobufjs" ) ;
2022-05-12 11:48:38 +02:00
const radiusClient = require ( "node-radius-client" ) ;
2023-01-05 16:58:24 +02:00
const redis = require ( "redis" ) ;
2023-08-02 09:40:19 +02:00
const oidc = require ( "openid-client" ) ;
2022-05-12 11:48:38 +02:00
const {
dictionaries : {
rfc2865 : { file , attributes } ,
} ,
} = require ( "node-radius-utils" ) ;
2022-09-25 13:38:28 +02:00
const dayjs = require ( "dayjs" ) ;
2021-08-09 07:34:44 +02:00
2023-07-17 10:15:44 +02:00
// SASLOptions used in JSDoc
// eslint-disable-next-line no-unused-vars
const { Kafka , SASLOptions } = require ( "kafkajs" ) ;
2023-10-09 01:01:54 +02:00
const crypto = require ( "crypto" ) ;
2023-01-05 13:22:15 +02:00
2023-07-17 10:15:44 +02:00
const isWindows = process . platform === /^win/ . test ( process . platform ) ;
2021-08-09 07:34:44 +02:00
/ * *
* Init or reset JWT secret
* @ returns { Promise < Bean > }
* /
exports . initJWTSecret = async ( ) => {
let jwtSecretBean = await R . findOne ( "setting" , " `key` = ? " , [
"jwtSecret" ,
] ) ;
2021-11-04 03:46:43 +02:00
if ( ! jwtSecretBean ) {
2021-08-09 07:34:44 +02:00
jwtSecretBean = R . dispense ( "setting" ) ;
jwtSecretBean . key = "jwtSecret" ;
}
2022-03-29 11:38:48 +02:00
jwtSecretBean . value = passwordHash . generate ( genSecret ( ) ) ;
2021-08-09 07:34:44 +02:00
await R . store ( jwtSecretBean ) ;
return jwtSecretBean ;
2021-09-20 10:22:18 +02:00
} ;
2021-07-01 08:03:06 +02:00
2023-08-02 09:40:19 +02:00
/ * *
* 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 ) ;
} ;
2022-04-20 20:56:40 +02:00
/ * *
* 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
* /
2021-07-01 08:03:06 +02:00
exports . tcping = function ( hostname , port ) {
return new Promise ( ( resolve , reject ) => {
tcpp . ping ( {
address : hostname ,
port : port ,
attempts : 1 ,
2021-08-05 13:04:38 +02:00
} , function ( err , data ) {
2021-07-01 08:03:06 +02:00
if ( err ) {
reject ( err ) ;
}
if ( data . results . length >= 1 && data . results [ 0 ] . err ) {
reject ( data . results [ 0 ] . err ) ;
}
resolve ( Math . round ( data . max ) ) ;
} ) ;
} ) ;
2021-09-20 10:22:18 +02:00
} ;
2021-07-01 11:00:23 +02:00
2022-04-20 20:56:40 +02:00
/ * *
* Ping the specified machine
* @ param { string } hostname Hostname / address of machine
2022-07-14 09:32:51 +02:00
* @ param { number } [ size = 56 ] Size of packet to send
2022-04-20 20:56:40 +02:00
* @ returns { Promise < number > } Time for ping in ms rounded to nearest integer
* /
2022-07-14 09:32:51 +02:00
exports . ping = async ( hostname , size = 56 ) => {
2021-08-10 15:03:14 +02:00
try {
2023-01-06 22:12:21 +02:00
return await exports . pingAsync ( hostname , false , size ) ;
2021-08-10 15:03:14 +02:00
} catch ( e ) {
// If the host cannot be resolved, try again with ipv6
2023-03-04 14:29:52 +02:00
console . debug ( "ping" , "IPv6 error message: " + e . message ) ;
2023-03-04 14:41:08 +02:00
// As node-ping does not report a specific error for this, try again if it is an empty message with ipv6 no matter what.
2023-03-04 14:29:52 +02:00
if ( ! e . message ) {
return await exports . pingAsync ( hostname , true , size ) ;
2021-08-10 15:03:14 +02:00
} else {
throw e ;
}
}
2021-09-20 10:22:18 +02:00
} ;
2021-08-10 15:03:14 +02:00
2022-04-20 20:56:40 +02:00
/ * *
* Ping the specified machine
* @ param { string } hostname Hostname / address of machine to ping
* @ param { boolean } ipv6 Should IPv6 be used ?
2023-01-06 22:09:40 +02:00
* @ param { number } [ size = 56 ] Size of ping packet to send
2022-04-20 20:56:40 +02:00
* @ returns { Promise < number > } Time for ping in ms rounded to nearest integer
* /
2022-07-14 09:32:51 +02:00
exports . pingAsync = function ( hostname , ipv6 = false , size = 56 ) {
2021-07-01 11:00:23 +02:00
return new Promise ( ( resolve , reject ) => {
2023-01-03 22:03:36 +02:00
ping . promise . probe ( hostname , {
v6 : ipv6 ,
2023-01-05 13:30:55 +02:00
min _reply : 1 ,
2023-01-16 19:21:01 +02:00
deadline : 10 ,
2023-01-06 22:09:40 +02:00
packetSize : size ,
2023-01-03 22:03:36 +02:00
} ) . then ( ( res ) => {
// If ping failed, it will set field to unknown
2023-01-04 19:32:27 +02:00
if ( res . alive ) {
2023-01-03 22:03:36 +02:00
resolve ( res . time ) ;
2023-01-04 19:32:27 +02:00
} else {
2023-01-05 13:22:15 +02:00
if ( isWindows ) {
reject ( new Error ( exports . convertToUTF8 ( res . output ) ) ) ;
} else {
reject ( new Error ( res . output ) ) ;
}
2021-07-01 11:00:23 +02:00
}
2023-01-03 22:03:36 +02:00
} ) . catch ( ( err ) => {
reject ( err ) ;
2021-07-01 11:00:23 +02:00
} ) ;
} ) ;
2021-09-20 10:22:18 +02:00
} ;
2021-07-09 08:14:03 +02:00
2022-04-21 14:01:22 +02:00
/ * *
* MQTT Monitor
2022-04-21 19:53:07 +02:00
* @ param { string } hostname Hostname / address of machine to test
* @ param { string } topic MQTT topic
* @ param { string } okMessage Expected result
* @ param { Object } [ options = { } ] MQTT options . Contains port , username ,
* password and interval ( interval defaults to 20 )
* @ returns { Promise < string > }
2022-04-21 14:01:22 +02:00
* /
2021-11-22 10:21:53 +02:00
exports . mqttAsync = function ( hostname , topic , okMessage , options = { } ) {
2021-11-04 03:46:43 +02:00
return new Promise ( ( resolve , reject ) => {
2021-11-22 10:21:53 +02:00
const { port , username , password , interval = 20 } = options ;
2022-01-13 06:42:34 +02:00
// Adds MQTT protocol to the hostname if not already present
2023-01-05 09:57:48 +02:00
if ( ! /^(?:http|mqtt|ws)s?:\/\// . test ( hostname ) ) {
2022-01-13 06:42:34 +02:00
hostname = "mqtt://" + hostname ;
2021-11-04 03:46:43 +02:00
}
2022-01-13 06:42:34 +02:00
2022-01-20 20:20:54 +02:00
const timeoutID = setTimeout ( ( ) => {
2022-04-16 07:37:17 +02:00
log . debug ( "mqtt" , "MQTT timeout triggered" ) ;
2022-01-20 20:20:54 +02:00
client . end ( ) ;
2022-04-16 19:06:47 +02:00
reject ( new Error ( "Timeout" ) ) ;
2022-04-16 09:01:53 +02:00
} , interval * 1000 * 0.8 ) ;
2022-01-20 20:20:54 +02:00
2023-01-05 12:42:19 +02:00
const mqttUrl = ` ${ hostname } : ${ port } ` ;
2022-01-13 06:42:34 +02:00
2023-01-05 09:57:48 +02:00
log . debug ( "mqtt" , ` MQTT connecting to ${ mqttUrl } ` ) ;
2022-01-13 06:42:34 +02:00
2023-01-05 09:57:48 +02:00
let client = mqtt . connect ( mqttUrl , {
2022-01-13 06:42:34 +02:00
username ,
password
} ) ;
client . on ( "connect" , ( ) => {
2022-04-16 19:06:47 +02:00
log . debug ( "mqtt" , "MQTT connected" ) ;
2022-04-16 08:50:48 +02:00
try {
2022-04-16 19:06:47 +02:00
log . debug ( "mqtt" , "MQTT subscribe topic" ) ;
2022-04-16 08:50:48 +02:00
client . subscribe ( topic ) ;
} catch ( e ) {
2022-04-16 09:01:53 +02:00
client . end ( ) ;
clearTimeout ( timeoutID ) ;
2022-04-16 08:50:48 +02:00
reject ( new Error ( "Cannot subscribe topic" ) ) ;
}
2022-01-13 06:42:34 +02:00
} ) ;
client . on ( "error" , ( error ) => {
client . end ( ) ;
2022-01-20 20:20:54 +02:00
clearTimeout ( timeoutID ) ;
2022-01-13 06:42:34 +02:00
reject ( error ) ;
} ) ;
client . on ( "message" , ( messageTopic , message ) => {
2022-04-26 01:26:57 +02:00
if ( messageTopic === topic ) {
2022-01-20 20:20:54 +02:00
client . end ( ) ;
clearTimeout ( timeoutID ) ;
2022-04-18 07:04:55 +02:00
if ( okMessage != null && okMessage !== "" && message . toString ( ) !== okMessage ) {
reject ( new Error ( ` Message Mismatch - Topic: ${ messageTopic } ; Message: ${ message . toString ( ) } ` ) ) ;
2022-01-13 06:42:34 +02:00
} else {
2022-04-18 07:04:55 +02:00
resolve ( ` Topic: ${ messageTopic } ; Message: ${ message . toString ( ) } ` ) ;
2022-01-13 06:42:34 +02:00
}
}
} ) ;
2021-11-04 03:46:43 +02:00
} ) ;
2022-05-13 19:58:23 +02:00
} ;
2023-07-17 10:15:44 +02:00
/ * *
* Monitor Kafka using Producer
* @ param { string } topic Topic name to produce into
* @ param { string } message Message to produce
* @ param { Object } [ options = { interval = 20 , allowAutoTopicCreation = false , ssl = false , clientId = "Uptime-Kuma" } ]
* 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 { string [ ] } brokers List of kafka brokers to connect , host and port joined by ':'
* @ param { SASLOptions } [ saslOptions = { } ] Options for kafka client Authentication ( SASL ) ( defaults to
* { } )
* @ returns { Promise < string > }
* /
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 (
( ) => {
2023-09-23 21:30:15 +02:00
producer . send ( {
topic : topic ,
messages : [ {
value : message ,
} ] ,
} ) . then ( ( _ ) => {
2023-07-17 10:15:44 +02:00
resolve ( "Message sent successfully" ) ;
2023-09-23 21:30:15 +02:00
} ) . catch ( ( e ) => {
2023-07-17 10:15:44 +02:00
connectedToKafka = true ;
producer . disconnect ( ) ;
clearTimeout ( timeoutID ) ;
reject ( new Error ( "Error sending message: " + e . message ) ) ;
2023-09-23 21:30:15 +02:00
} ) . finally ( ( ) => {
connectedToKafka = true ;
clearTimeout ( timeoutID ) ;
} ) ;
2023-07-17 10:15:44 +02:00
}
) . catch (
( e ) => {
connectedToKafka = true ;
producer . disconnect ( ) ;
clearTimeout ( timeoutID ) ;
reject ( new Error ( "Error in producer connection: " + e . message ) ) ;
}
) ;
producer . on ( "producer.network.request_timeout" , ( _ ) => {
2023-09-23 21:30:15 +02:00
if ( ! connectedToKafka ) {
clearTimeout ( timeoutID ) ;
reject ( new Error ( "producer.network.request_timeout" ) ) ;
}
2023-07-17 10:15:44 +02:00
} ) ;
producer . on ( "producer.disconnect" , ( _ ) => {
if ( ! connectedToKafka ) {
clearTimeout ( timeoutID ) ;
reject ( new Error ( "producer.disconnect" ) ) ;
}
} ) ;
} ) ;
} ;
2022-05-13 19:58:23 +02:00
/ * *
* 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 ) > }
* /
exports . httpNtlm = function ( options , ntlmOptions ) {
return new Promise ( ( resolve , reject ) => {
let client = NtlmClient ( ntlmOptions ) ;
client ( options )
. then ( ( resp ) => {
resolve ( resp ) ;
} )
. catch ( ( err ) => {
reject ( err ) ;
} ) ;
} ) ;
2021-11-04 03:46:43 +02:00
} ;
2022-04-20 20:56:40 +02:00
/ * *
* Resolves a given record using the specified DNS server
* @ param { string } hostname The hostname of the record to lookup
2022-04-21 14:01:22 +02:00
* @ param { string } resolverServer The DNS server to use
2022-04-27 19:26:11 +02:00
* @ param { string } resolverPort Port the DNS server is listening on
2022-04-20 20:56:40 +02:00
* @ param { string } rrtype The type of record to request
* @ returns { Promise < ( string [ ] | Object [ ] | Object ) > }
* /
2022-04-24 02:06:45 +02:00
exports . dnsResolve = function ( hostname , resolverServer , resolverPort , rrtype ) {
2021-08-23 00:05:48 +02:00
const resolver = new Resolver ( ) ;
2022-04-15 20:59:32 +02:00
// Remove brackets from IPv6 addresses so we can re-add them to
// prevent issues with ::1:5300 (::1 port 5300)
2022-04-24 02:06:45 +02:00
resolverServer = resolverServer . replace ( "[" , "" ) . replace ( "]" , "" ) ;
2022-06-01 07:05:12 +02:00
resolver . setServers ( [ ` [ ${ resolverServer } ]: ${ resolverPort } ` ] ) ;
2021-08-23 00:05:48 +02:00
return new Promise ( ( resolve , reject ) => {
2022-04-17 09:43:03 +02:00
if ( rrtype === "PTR" ) {
2021-08-23 00:05:48 +02:00
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 ) ;
}
} ) ;
}
2021-09-20 10:22:18 +02:00
} ) ;
} ;
2021-08-23 00:05:48 +02:00
2022-05-12 19:48:03 +02:00
/ * *
* Run a query on SQL Server
* @ param { string } connectionString The database connection string
* @ param { string } query The query to validate the database with
* @ returns { Promise < ( string [ ] | Object [ ] | Object ) > }
* /
2023-01-01 16:27:14 +02:00
exports . mssqlQuery = async function ( connectionString , query ) {
let pool ;
try {
pool = new mssql . ConnectionPool ( connectionString ) ;
await pool . connect ( ) ;
2023-10-12 20:50:10 +02:00
if ( ! query ) {
query = "SELECT 1" ;
}
2023-01-01 16:27:14 +02:00
await pool . request ( ) . query ( query ) ;
pool . close ( ) ;
} catch ( e ) {
if ( pool ) {
pool . close ( ) ;
}
throw e ;
}
2022-05-12 19:48:03 +02:00
} ;
2022-06-15 19:12:47 +02:00
/ * *
* Run a query on Postgres
* @ param { string } connectionString The database connection string
* @ param { string } query The query to validate the database with
* @ returns { Promise < ( string [ ] | Object [ ] | Object ) > }
* /
exports . postgresQuery = function ( connectionString , query ) {
return new Promise ( ( resolve , reject ) => {
2022-06-15 20:00:14 +02:00
const config = postgresConParse ( connectionString ) ;
2023-10-12 20:50:10 +02:00
// Fix #3868, which true/false is not parsed to boolean
if ( typeof config . ssl === "string" ) {
config . ssl = config . ssl === "true" ;
}
2022-06-15 20:00:14 +02:00
if ( config . password === "" ) {
// See https://github.com/brianc/node-postgres/issues/1927
2023-10-12 20:50:10 +02:00
reject ( new Error ( "Password is undefined." ) ) ;
return ;
2022-06-15 20:00:14 +02:00
}
2023-10-12 20:50:10 +02:00
const client = new Client ( config ) ;
2022-06-15 20:00:14 +02:00
2023-10-12 20:50:10 +02:00
client . on ( "error" , ( error ) => {
log . debug ( "postgres" , "Error caught in the error event handler." ) ;
reject ( error ) ;
} ) ;
2022-06-15 19:12:47 +02:00
2023-01-14 15:06:10 +02:00
client . connect ( ( err ) => {
if ( err ) {
2022-06-15 19:12:47 +02:00
reject ( err ) ;
client . end ( ) ;
2023-01-14 15:06:10 +02:00
} else {
// Connected here
2023-02-14 20:50:49 +02:00
try {
// No query provided by user, use SELECT 1
if ( ! query || ( typeof query === "string" && query . trim ( ) === "" ) ) {
query = "SELECT 1" ;
2023-01-14 15:06:10 +02:00
}
2023-02-14 20:50:49 +02:00
client . query ( query , ( err , res ) => {
if ( err ) {
reject ( err ) ;
} else {
resolve ( res ) ;
}
client . end ( ) ;
} ) ;
} catch ( e ) {
reject ( e ) ;
}
2023-01-14 15:06:10 +02:00
}
} ) ;
2022-06-15 19:12:47 +02:00
} ) ;
} ;
2022-10-02 02:52:53 +02:00
/ * *
* Run a query on MySQL / MariaDB
* @ param { string } connectionString The database connection string
* @ param { string } query The query to validate the database with
2023-10-15 18:38:56 +02:00
* @ param { ? string } password The password to use
2023-03-24 10:08:30 +02:00
* @ returns { Promise < ( string ) > }
2022-10-02 02:52:53 +02:00
* /
2023-10-15 18:38:56 +02:00
exports . mysqlQuery = function ( connectionString , query , password = undefined ) {
2022-10-02 02:52:53 +02:00
return new Promise ( ( resolve , reject ) => {
2023-10-15 18:38:56 +02:00
const connection = mysql . createConnection ( {
uri : connectionString ,
password
} ) ;
2023-03-24 10:08:30 +02:00
connection . on ( "error" , ( err ) => {
reject ( err ) ;
} ) ;
connection . query ( query , ( err , res ) => {
if ( err ) {
2022-10-02 02:52:53 +02:00
reject ( err ) ;
2023-03-24 10:08:30 +02:00
} else {
2023-03-24 10:24:00 +02:00
if ( Array . isArray ( res ) ) {
resolve ( "Rows: " + res . length ) ;
} else {
resolve ( "No Error, but the result is not an array. Type: " + typeof res ) ;
}
2023-03-24 10:08:30 +02:00
}
2023-06-06 14:28:51 +02:00
try {
connection . end ( ) ;
} catch ( _ ) {
connection . destroy ( ) ;
}
2023-03-24 10:08:30 +02:00
} ) ;
2022-10-02 02:52:53 +02:00
} ) ;
} ;
2022-11-17 03:50:34 +02:00
/ * *
* Connect to and Ping a MongoDB database
* @ param { string } connectionString The database connection string
* @ returns { Promise < ( string [ ] | Object [ ] | Object ) > }
* /
exports . mongodbPing = async function ( connectionString ) {
2023-01-14 19:36:49 +02:00
let client = await MongoClient . connect ( connectionString ) ;
let dbPing = await client . db ( ) . command ( { ping : 1 } ) ;
await client . close ( ) ;
if ( dbPing [ "ok" ] === 1 ) {
return "UP" ;
} else {
throw Error ( "failed" ) ;
2022-11-17 03:50:34 +02:00
}
2022-11-17 05:21:15 +02:00
} ;
2022-11-17 03:50:34 +02:00
2022-10-12 18:32:05 +02:00
/ * *
* 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 = 1812 ] Port to contact radius server on
2023-05-23 12:18:54 +02:00
* @ param { number } [ timeout = 2500 ] Timeout for connection to use
2022-10-12 18:32:05 +02:00
* @ returns { Promise < any > }
* /
2022-05-12 11:48:38 +02:00
exports . radius = function (
hostname ,
username ,
password ,
calledStationId ,
callingStationId ,
secret ,
2022-10-12 18:32:05 +02:00
port = 1812 ,
2023-05-23 12:18:54 +02:00
timeout = 2500 ,
2022-05-12 11:48:38 +02:00
) {
const client = new radiusClient ( {
host : hostname ,
2022-10-12 18:32:05 +02:00
hostPort : port ,
2023-05-23 12:18:54 +02:00
timeout : timeout ,
2023-07-27 11:42:22 +02:00
retries : 1 ,
2022-05-12 11:48:38 +02:00
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 ] ,
] ,
2023-07-27 11:42:22 +02:00
} ) . catch ( ( error ) => {
if ( error . response ? . code ) {
throw Error ( error . response . code ) ;
} else {
throw Error ( error . message ) ;
}
2022-05-12 11:48:38 +02:00
} ) ;
} ;
2023-01-05 16:58:24 +02:00
/ * *
* Redis server ping
* @ param { string } dsn The redis connection string
* /
2023-01-13 13:10:07 +02:00
exports . redisPingAsync = function ( dsn ) {
return new Promise ( ( resolve , reject ) => {
const client = redis . createClient ( {
2023-06-09 20:54:17 +02:00
url : dsn
2023-01-13 13:10:07 +02:00
} ) ;
client . on ( "error" , ( err ) => {
2023-06-14 17:49:33 +02:00
if ( client . isOpen ) {
client . disconnect ( ) ;
}
2023-01-13 13:10:07 +02:00
reject ( err ) ;
} ) ;
client . connect ( ) . then ( ( ) => {
2023-06-14 17:49:33 +02:00
if ( ! client . isOpen ) {
client . emit ( "error" , new Error ( "connection isn't open" ) ) ;
2023-06-09 22:26:02 +02:00
}
2023-06-14 17:49:33 +02:00
client . ping ( ) . then ( ( res , err ) => {
if ( client . isOpen ) {
client . disconnect ( ) ;
}
if ( err ) {
reject ( err ) ;
} else {
resolve ( res ) ;
}
} ) . catch ( error => reject ( error ) ) ;
2023-01-13 13:10:07 +02:00
} ) ;
2023-01-05 16:58:24 +02:00
} ) ;
} ;
2022-05-12 11:48:38 +02:00
2022-04-20 20:56:40 +02:00
/ * *
* Retrieve value of setting based on key
* @ param { string } key Key of setting to retrieve
2022-05-06 08:41:34 +02:00
* @ returns { Promise < any > } Value
2022-07-31 17:41:29 +02:00
* @ deprecated Use await Settings . get ( key )
2022-04-20 20:56:40 +02:00
* /
2021-07-09 08:14:03 +02:00
exports . setting = async function ( key ) {
2022-06-29 08:57:40 +02:00
return await Settings . get ( key ) ;
2021-09-20 10:22:18 +02:00
} ;
2021-07-09 08:14:03 +02:00
2022-04-20 20:56:40 +02:00
/ * *
* Sets the specified setting to specifed 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 > }
* /
2021-10-09 18:16:13 +02:00
exports . setSetting = async function ( key , value , type = null ) {
2022-06-29 08:57:40 +02:00
await Settings . set ( key , value , type ) ;
2021-09-20 10:22:18 +02:00
} ;
2021-07-21 20:02:35 +02:00
2022-04-20 20:56:40 +02:00
/ * *
* Get settings based on type
2022-06-29 08:57:40 +02:00
* @ param { string } type The type of setting
2022-04-20 20:56:40 +02:00
* @ returns { Promise < Bean > }
* /
2021-07-09 08:14:03 +02:00
exports . getSettings = async function ( type ) {
2022-06-29 08:57:40 +02:00
return await Settings . getSettings ( type ) ;
2021-09-20 10:22:18 +02:00
} ;
2021-07-21 06:09:09 +02:00
2022-04-20 20:56:40 +02:00
/ * *
* Set settings based on type
2022-06-29 08:57:40 +02:00
* @ param { string } type Type of settings to set
2022-04-20 20:56:40 +02:00
* @ param { Object } data Values of settings
* @ returns { Promise < void > }
* /
2021-07-31 15:57:58 +02:00
exports . setSettings = async function ( type , data ) {
2022-06-29 08:57:40 +02:00
await Settings . setSettings ( type , data ) ;
2021-09-20 10:22:18 +02:00
} ;
2021-07-31 15:57:58 +02:00
2021-07-21 06:09:09 +02:00
// ssl-checker by @dyaa
2022-04-20 20:56:40 +02:00
//https://github.com/dyaa/ssl-checker/blob/master/src/index.ts
2021-07-21 06:09:09 +02:00
2022-04-20 20:56:40 +02:00
/ * *
* Get number of days between two dates
* @ param { Date } validFrom Start date
* @ param { Date } validTo End date
* @ returns { number }
* /
2021-07-21 06:09:09 +02:00
const getDaysBetween = ( validFrom , validTo ) =>
Math . round ( Math . abs ( + validFrom - + validTo ) / 8.64 e7 ) ;
2022-04-20 20:56:40 +02:00
/ * *
* Get days remaining from a time range
* @ param { Date } validFrom Start date
* @ param { Date } validTo End date
* @ returns { number }
* /
2021-07-21 06:09:09 +02:00
const getDaysRemaining = ( validFrom , validTo ) => {
const daysRemaining = getDaysBetween ( validFrom , validTo ) ;
if ( new Date ( validTo ) . getTime ( ) < new Date ( ) . getTime ( ) ) {
return - daysRemaining ;
}
return daysRemaining ;
} ;
2022-04-20 20:56:40 +02:00
/ * *
* Fix certificate info for display
* @ param { Object } info The chain obtained from getPeerCertificate ( )
* @ returns { Object } An object representing certificate information
* /
2021-10-01 12:44:32 +02:00
const parseCertificateInfo = function ( info ) {
let link = info ;
2021-11-08 09:39:17 +02:00
let i = 0 ;
const existingList = { } ;
2021-10-01 12:44:32 +02:00
while ( link ) {
2022-04-16 08:50:48 +02:00
log . debug ( "cert" , ` [ ${ i } ] ${ link . fingerprint } ` ) ;
2021-11-08 09:39:17 +02:00
2021-10-01 12:44:32 +02:00
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 ) ;
2021-11-08 09:39:17 +02:00
existingList [ link . fingerprint ] = true ;
2021-10-01 12:44:32 +02:00
// Move up the chain until loop is encountered
if ( link . issuerCertificate == null ) {
2023-01-12 12:34:37 +02:00
link . certType = ( i === 0 ) ? "self-signed" : "root CA" ;
2021-10-01 12:44:32 +02:00
break ;
2021-11-08 09:39:17 +02:00
} else if ( link . issuerCertificate . fingerprint in existingList ) {
2023-01-12 12:34:37 +02:00
// a root CA certificate is typically "signed by itself" (=> "self signed certificate") and thus the "issuerCertificate" is a reference to itself.
2022-04-16 08:50:48 +02:00
log . debug ( "cert" , ` [Last] ${ link . issuerCertificate . fingerprint } ` ) ;
2023-01-12 12:34:37 +02:00
link . certType = ( i === 0 ) ? "self-signed" : "root CA" ;
2021-10-01 12:44:32 +02:00
link . issuerCertificate = null ;
break ;
} else {
2023-01-12 12:34:37 +02:00
link . certType = ( i === 0 ) ? "server" : "intermediate CA" ;
2021-10-01 12:44:32 +02:00
link = link . issuerCertificate ;
}
2021-11-08 09:39:17 +02:00
// Should be no use, but just in case.
if ( i > 500 ) {
throw new Error ( "Dead loop occurred in parseCertificateInfo" ) ;
}
i ++ ;
2021-07-21 06:09:09 +02:00
}
2021-10-01 12:44:32 +02:00
return info ;
} ;
2021-07-21 06:09:09 +02:00
2022-04-20 20:56:40 +02:00
/ * *
* Check if certificate is valid
* @ param { Object } res Response object from axios
* @ returns { Object } Object containing certificate information
* /
2021-10-01 12:44:32 +02:00
exports . checkCertificate = function ( res ) {
2022-12-12 20:21:12 +02:00
if ( ! res . request . res . socket ) {
throw new Error ( "No socket found" ) ;
}
2021-10-01 12:44:32 +02:00
const info = res . request . res . socket . getPeerCertificate ( true ) ;
const valid = res . request . res . socket . authorized || false ;
2021-07-21 06:09:09 +02:00
2022-04-16 08:50:48 +02:00
log . debug ( "cert" , "Parsing Certificate Info" ) ;
2021-10-01 12:44:32 +02:00
const parsedInfo = parseCertificateInfo ( info ) ;
2021-07-21 06:09:09 +02:00
return {
2021-10-01 12:44:32 +02:00
valid : valid ,
certInfo : parsedInfo
2021-07-21 06:09:09 +02:00
} ;
2021-09-20 10:22:18 +02:00
} ;
2021-08-05 13:04:38 +02:00
2022-04-20 20:56:40 +02:00
/ * *
* Check if the provided status code is within the accepted ranges
2022-07-18 16:06:25 +02:00
* @ param { number } status The status code to check
2022-04-21 21:02:18 +02:00
* @ param { string [ ] } acceptedCodes An array of accepted status codes
2022-04-20 20:56:40 +02:00
* @ returns { boolean } True if status code within range , false otherwise
* /
2022-04-16 19:39:49 +02:00
exports . checkStatusCode = function ( status , acceptedCodes ) {
if ( acceptedCodes == null || acceptedCodes . length === 0 ) {
2021-08-05 13:04:38 +02:00
return false ;
}
2022-04-16 19:39:49 +02:00
for ( const codeRange of acceptedCodes ) {
2023-08-07 21:22:32 +02:00
if ( typeof codeRange !== "string" ) {
log . error ( "monitor" , ` Accepted status code not a string. ${ codeRange } is of type ${ typeof codeRange } ` ) ;
continue ;
}
2022-04-16 19:39:49 +02:00
const codeRangeSplit = codeRange . split ( "-" ) . map ( string => parseInt ( string ) ) ;
if ( codeRangeSplit . length === 1 ) {
if ( status === codeRangeSplit [ 0 ] ) {
2021-08-05 13:04:38 +02:00
return true ;
}
2022-04-16 19:39:49 +02:00
} else if ( codeRangeSplit . length === 2 ) {
if ( status >= codeRangeSplit [ 0 ] && status <= codeRangeSplit [ 1 ] ) {
2021-08-05 13:04:38 +02:00
return true ;
}
} else {
2023-08-07 21:22:32 +02:00
log . error ( "monitor" , ` ${ codeRange } is not a valid status code range ` ) ;
continue ;
2021-08-05 13:04:38 +02:00
}
}
return false ;
2021-09-20 10:22:18 +02:00
} ;
2021-08-30 08:55:33 +02:00
2022-04-20 20:56:40 +02:00
/ * *
* Get total number of clients in room
* @ param { Server } io Socket server instance
* @ param { string } roomName Name of room to check
* @ returns { number }
* /
2021-08-30 08:55:33 +02:00
exports . getTotalClientInRoom = ( io , roomName ) => {
const sockets = io . sockets ;
2021-11-04 03:46:43 +02:00
if ( ! sockets ) {
2021-08-30 08:55:33 +02:00
return 0 ;
}
const adapter = sockets . adapter ;
2021-11-04 03:46:43 +02:00
if ( ! adapter ) {
2021-08-30 08:55:33 +02:00
return 0 ;
}
const room = adapter . rooms . get ( roomName ) ;
if ( room ) {
return room . size ;
} else {
return 0 ;
}
2021-09-20 10:22:18 +02:00
} ;
2021-09-11 13:40:03 +02:00
2022-04-20 20:56:40 +02:00
/ * *
* Allow CORS all origins if development
* @ param { Object } res Response object from axios
* /
2021-09-11 13:40:03 +02:00
exports . allowDevAllOrigin = ( res ) => {
if ( process . env . NODE _ENV === "development" ) {
exports . allowAllOrigin ( res ) ;
}
2021-09-20 10:22:18 +02:00
} ;
2021-09-11 13:40:03 +02:00
2022-04-20 20:56:40 +02:00
/ * *
* Allow CORS all origins
* @ param { Object } res Response object from axios
* /
2021-09-11 13:40:03 +02:00
exports . allowAllOrigin = ( res ) => {
res . header ( "Access-Control-Allow-Origin" , "*" ) ;
res . header ( "Access-Control-Allow-Headers" , "Origin, X-Requested-With, Content-Type, Accept" ) ;
2021-09-20 10:22:18 +02:00
} ;
2021-09-16 16:48:28 +02:00
2022-04-20 20:56:40 +02:00
/ * *
* Check if a user is logged in
* @ param { Socket } socket Socket instance
* /
2021-09-16 16:48:28 +02:00
exports . checkLogin = ( socket ) => {
2021-11-04 03:46:43 +02:00
if ( ! socket . userID ) {
2021-09-16 16:48:28 +02:00
throw new Error ( "You are not logged in." ) ;
}
2021-09-20 10:22:18 +02:00
} ;
2021-10-05 13:13:57 +02:00
2022-03-29 11:38:48 +02:00
/ * *
* For logged - in users , double - check the password
2022-04-21 14:01:22 +02:00
* @ param { Socket } socket Socket . io instance
* @ param { string } currentPassword
2022-03-29 11:38:48 +02:00
* @ returns { Promise < Bean > }
* /
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 ;
} ;
2022-04-20 20:56:40 +02:00
/** Start Unit tests */
2021-10-05 13:13:57 +02:00
exports . startUnitTest = async ( ) => {
console . log ( "Starting unit test..." ) ;
const npm = /^win/ . test ( process . platform ) ? "npm.cmd" : "npm" ;
2022-10-05 08:26:30 +02:00
const child = childProcess . spawn ( npm , [ "run" , "jest-backend" ] ) ;
2021-10-05 13:13:57 +02:00
child . stdout . on ( "data" , ( data ) => {
console . log ( data . toString ( ) ) ;
} ) ;
child . stderr . on ( "data" , ( data ) => {
console . log ( data . toString ( ) ) ;
} ) ;
child . on ( "close" , function ( code ) {
console . log ( "Jest exit code: " + code ) ;
2021-10-05 14:40:40 +02:00
process . exit ( code ) ;
2021-10-05 13:13:57 +02:00
} ) ;
} ;
2021-10-13 18:22:49 +02:00
2022-06-16 14:40:42 +02:00
/** Start end-to-end tests */
2022-06-16 11:28:17 +02:00
exports . startE2eTests = async ( ) => {
console . log ( "Starting unit test..." ) ;
const npm = /^win/ . test ( process . platform ) ? "npm.cmd" : "npm" ;
const child = childProcess . spawn ( npm , [ "run" , "cy:run" ] ) ;
child . stdout . on ( "data" , ( data ) => {
console . log ( data . toString ( ) ) ;
} ) ;
child . stderr . on ( "data" , ( data ) => {
console . log ( data . toString ( ) ) ;
} ) ;
child . on ( "close" , function ( code ) {
console . log ( "Jest exit code: " + code ) ;
process . exit ( code ) ;
} ) ;
} ;
2021-10-13 18:22:49 +02:00
/ * *
2022-04-20 20:56:40 +02:00
* Convert unknown string to UTF8
* @ param { Uint8Array } body Buffer
2021-10-13 18:22:49 +02:00
* @ returns { string }
* /
exports . convertToUTF8 = ( body ) => {
const guessEncoding = chardet . detect ( body ) ;
const str = iconv . decode ( body , guessEncoding ) ;
return str . toString ( ) ;
} ;
2021-10-29 12:24:47 +02:00
2022-01-03 17:04:37 +02:00
/ * *
* Returns a color code in hex format based on a given percentage :
* 0 % => hue = 10 => red
* 100 % => hue = 90 => green
*
2022-04-30 15:36:00 +02:00
* @ param { number } percentage float , 0 to 1
2022-04-30 15:36:07 +02:00
* @ param { number } maxHue
2022-01-03 17:04:37 +02:00
* @ param { number } minHue , int
* @ returns { string } , hex value
* /
2022-01-03 16:48:52 +02:00
exports . percentageToColor = ( percentage , maxHue = 90 , minHue = 10 ) => {
const hue = percentage * ( maxHue - minHue ) + minHue ;
try {
return chroma ( ` hsl( ${ hue } , 90%, 40%) ` ) . hex ( ) ;
} catch ( err ) {
2022-01-04 13:21:53 +02:00
return badgeConstants . naColor ;
2022-01-03 16:48:52 +02:00
}
} ;
2022-01-04 17:00:21 +02:00
/ * *
* Joins and array of string to one string after filtering out empty values
*
* @ param { string [ ] } parts
* @ param { string } connector
* @ returns { string }
* /
exports . filterAndJoin = ( parts , connector = "" ) => {
return parts . filter ( ( part ) => ! ! part && part !== "" ) . join ( connector ) ;
} ;
2022-06-01 07:05:12 +02:00
/ * *
2023-02-09 11:42:02 +02:00
* Send an Error response
2022-06-01 07:05:12 +02:00
* @ param { Object } res Express response object
* @ param { string } [ msg = "" ] Message to send
* /
2023-02-09 11:42:02 +02:00
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 ,
} ) ;
}
2022-06-01 07:05:12 +02:00
} ;
2022-08-03 07:00:39 +02:00
2022-09-25 13:38:28 +02:00
function timeObjectConvertTimezone ( obj , timezone , timeObjectToUTC = true ) {
2022-10-10 14:48:11 +02:00
let offsetString ;
if ( timezone ) {
offsetString = dayjs ( ) . tz ( timezone ) . format ( "Z" ) ;
} else {
offsetString = dayjs ( ) . format ( "Z" ) ;
}
2022-09-25 13:38:28 +02:00
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
2022-10-12 11:02:16 +02:00
if ( obj . minutes < 0 ) {
obj . minutes += 60 ;
obj . hours -- ;
} else if ( obj . minutes > 60 ) {
obj . minutes -= 60 ;
obj . hours ++ ;
}
2022-09-25 13:38:28 +02:00
if ( obj . hours < 0 ) {
obj . hours += 24 ;
} else if ( obj . hours > 24 ) {
obj . hours -= 24 ;
}
return obj ;
}
2022-10-10 14:48:11 +02:00
/ * *
*
* @ param { object } obj
* @ param { string } timezone
* @ returns { object }
* /
module . exports . timeObjectToUTC = ( obj , timezone = undefined ) => {
2022-09-25 13:38:28 +02:00
return timeObjectConvertTimezone ( obj , timezone , true ) ;
} ;
2022-10-10 14:48:11 +02:00
/ * *
*
* @ param { object } obj
* @ param { string } timezone
* @ returns { object }
* /
module . exports . timeObjectToLocal = ( obj , timezone = undefined ) => {
2022-09-25 13:38:28 +02:00
return timeObjectConvertTimezone ( obj , timezone , false ) ;
} ;
2022-10-26 15:41:21 +02:00
2022-08-03 07:00:39 +02:00
/ * *
* Create gRPC client stib
* @ param { Object } options from gRPC client
* /
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 , _ ) => {
2022-12-29 03:10:58 +02:00
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 : ""
} ) ;
}
2022-08-03 07:00:39 +02:00
} ) ;
} ;
2023-08-03 19:10:15 +02:00
2023-10-09 01:01:54 +02:00
module . exports . SHAKE256 _LENGTH = 16 ;
/ * *
*
* @ param { string } data
* @ param { number } len
* @ return { string }
* /
module . exports . shake256 = ( data , len ) => {
if ( ! data ) {
return "" ;
}
return crypto . createHash ( "shake256" , { outputLength : len } )
. update ( data )
. digest ( "hex" ) ;
} ;
2023-08-03 19:10:15 +02:00
// For unit test, export functions
if ( process . env . TEST _BACKEND ) {
module . exports . _ _test = {
parseCertificateInfo ,
} ;
module . exports . _ _getPrivateFunction = ( functionName ) => {
return module . exports . _ _test [ functionName ] ;
} ;
}