2023-01-12 01:04:58 +08:00
const ping = require ( "@louislam/ping" ) ;
2021-07-27 19:47:13 +02:00
const { R } = require ( "redbean-node" ) ;
2025-05-24 02:57:39 +02:00
const {
2026-01-09 02:10:36 +01:00
log ,
genSecret ,
badgeConstants ,
PING _PACKET _SIZE _DEFAULT ,
PING _GLOBAL _TIMEOUT _DEFAULT ,
PING _COUNT _DEFAULT ,
PING _PER _REQUEST _TIMEOUT _DEFAULT ,
2025-05-24 02:57:39 +02:00
} = require ( "../src/util" ) ;
2021-08-09 13:34:44 +08:00
const passwordHash = require ( "./password-hash" ) ;
2021-10-14 00:22:49 +08:00
const iconv = require ( "iconv-lite" ) ;
const chardet = require ( "chardet" ) ;
2022-01-03 15:48:52 +01:00
const chroma = require ( "chroma-js" ) ;
2024-06-24 02:08:39 +08:00
const { NtlmClient } = require ( "./modules/axios-ntlm/lib/ntlmClient.js" ) ;
2024-10-09 07:43:44 +08:00
const { Settings } = require ( "./settings" ) ;
2025-10-09 14:05:39 -07:00
const RadiusClient = require ( "./radius-client" ) ;
2023-08-02 09:40:19 +02:00
const oidc = require ( "openid-client" ) ;
2023-10-16 02:20:38 +08:00
const tls = require ( "tls" ) ;
2025-06-25 13:39:00 +08:00
const { exists } = require ( "fs" ) ;
2026-03-25 02:37:19 +08:00
const { networkInterfaces } = require ( "os" ) ;
2023-08-02 09:40:19 +02:00
2022-05-12 11:48:38 +02:00
const {
dictionaries : {
rfc2865 : { file , attributes } ,
} ,
} = require ( "node-radius-utils" ) ;
2022-09-25 19:38:28 +08:00
const dayjs = require ( "dayjs" ) ;
2026-01-09 09:18:17 +06:00
dayjs . extend ( require ( "dayjs/plugin/utc" ) ) ;
2021-08-09 13:34:44 +08:00
2023-07-17 11:45:44 +03:30
// SASLOptions used in JSDoc
// eslint-disable-next-line no-unused-vars
const { Kafka , SASLOptions } = require ( "kafkajs" ) ;
2023-10-09 07:01:54 +08:00
const crypto = require ( "crypto" ) ;
2023-01-05 19:22:15 +08:00
2023-07-17 11:45:44 +03:30
const isWindows = process . platform === /^win/ . test ( process . platform ) ;
2021-08-09 13:34:44 +08:00
/ * *
* Init or reset JWT secret
2023-08-11 09:46:41 +02:00
* @ returns { Promise < Bean > } JWT secret
2021-08-09 13:34:44 +08:00
* /
exports . initJWTSecret = async ( ) => {
2026-01-09 02:10:36 +01:00
let jwtSecretBean = await R . findOne ( "setting" , " `key` = ? " , [ "jwtSecret" ] ) ;
2021-08-09 13:34:44 +08:00
2021-11-03 21:46:43 -04:00
if ( ! jwtSecretBean ) {
2021-08-09 13:34:44 +08:00
jwtSecretBean = R . dispense ( "setting" ) ;
jwtSecretBean . key = "jwtSecret" ;
}
2025-06-19 14:29:43 +08:00
jwtSecretBean . value = await passwordHash . generate ( genSecret ( ) ) ;
2021-08-09 13:34:44 +08:00
await R . store ( jwtSecretBean ) ;
return jwtSecretBean ;
2021-09-20 16:22:18 +08:00
} ;
2021-07-01 14:03:06 +08:00
2023-08-02 09:40:19 +02:00
/ * *
2025-06-29 19:37:41 -05:00
* Decodes a jwt and returns the payload portion without verifying the jwt .
2023-08-02 09:40:19 +02:00
* @ param { string } jwt The input jwt as a string
2023-08-11 09:46:41 +02:00
* @ returns { object } Decoded jwt payload object
2023-08-02 09:40:19 +02:00
* /
exports . decodeJwt = ( jwt ) => {
return JSON . parse ( Buffer . from ( jwt . split ( "." ) [ 1 ] , "base64" ) . toString ( ) ) ;
} ;
/ * *
2025-06-29 19:37:41 -05:00
* Gets an Access Token from an oidc / oauth2 provider
* @ param { string } tokenEndpoint The token URI from the auth service provider
2023-08-02 09:40:19 +02:00
* @ param { string } clientId The oidc / oauth application client id
* @ param { string } clientSecret The oidc / oauth application client secret
2025-06-29 19:37:41 -05:00
* @ param { string } scope The scope ( s ) for which the token should be issued for
* @ param { string } audience The audience for which the token should be issued for
* @ param { string } authMethod The method used to send the credentials . Default client _secret _basic
2023-08-02 09:40:19 +02:00
* @ returns { Promise < oidc . TokenSet > } TokenSet promise if the token request was successful
* /
2026-01-09 02:10:36 +01:00
exports . getOidcTokenClientCredentials = async (
tokenEndpoint ,
clientId ,
clientSecret ,
scope ,
audience ,
authMethod = "client_secret_basic"
) => {
2023-08-02 09:40:19 +02:00
const oauthProvider = new oidc . Issuer ( { token _endpoint : tokenEndpoint } ) ;
let client = new oauthProvider . Client ( {
client _id : clientId ,
client _secret : clientSecret ,
2026-01-09 02:10:36 +01:00
token _endpoint _auth _method : authMethod ,
2023-08-02 09:40:19 +02:00
} ) ;
// 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 ;
}
2025-06-29 19:37:41 -05:00
if ( audience ) {
grantParams . audience = audience ;
}
2023-08-02 09:40:19 +02:00
return await client . grant ( grantParams ) ;
} ;
2022-04-20 19:56:40 +01:00
/ * *
* Ping the specified machine
2025-05-24 02:57:39 +02:00
* @ param { string } destAddr Hostname / IP address of machine to ping
* @ param { number } count Number of packets to send before stopping
* @ param { string } sourceAddr Source address for sending / receiving echo requests
* @ param { boolean } numeric If true , IP addresses will be output instead of symbolic hostnames
* @ param { number } size Size ( in bytes ) of echo request to send
* @ param { number } deadline Maximum time in seconds before ping stops , regardless of packets sent
* @ param { number } timeout Maximum time in seconds to wait for each response
2022-04-20 19:56:40 +01:00
* @ returns { Promise < number > } Time for ping in ms rounded to nearest integer
* /
2025-05-24 02:57:39 +02:00
exports . ping = async (
destAddr ,
count = PING _COUNT _DEFAULT ,
sourceAddr = "" ,
numeric = true ,
size = PING _PACKET _SIZE _DEFAULT ,
deadline = PING _GLOBAL _TIMEOUT _DEFAULT ,
2026-01-09 02:10:36 +01:00
timeout = PING _PER _REQUEST _TIMEOUT _DEFAULT
2025-05-24 02:57:39 +02:00
) => {
2021-08-10 21:03:14 +08:00
try {
2025-05-24 02:57:39 +02:00
return await exports . pingAsync ( destAddr , false , count , sourceAddr , numeric , size , deadline , timeout ) ;
2021-08-10 21:03:14 +08:00
} catch ( e ) {
// If the host cannot be resolved, try again with ipv6
2024-01-05 12:43:03 +00:00
log . debug ( "ping" , "IPv6 error message: " + e . message ) ;
2023-03-04 20:29:52 +08:00
2023-03-04 20:41:08 +08: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 20:29:52 +08:00
if ( ! e . message ) {
2025-05-24 02:57:39 +02:00
return await exports . pingAsync ( destAddr , true , count , sourceAddr , numeric , size , deadline , timeout ) ;
2021-08-10 21:03:14 +08:00
} else {
throw e ;
}
}
2021-09-20 16:22:18 +08:00
} ;
2021-08-10 21:03:14 +08:00
2022-04-20 19:56:40 +01:00
/ * *
* Ping the specified machine
2025-05-24 02:57:39 +02:00
* @ param { string } destAddr Hostname / IP address of machine to ping
2022-04-20 19:56:40 +01:00
* @ param { boolean } ipv6 Should IPv6 be used ?
2025-05-24 02:57:39 +02:00
* @ param { number } count Number of packets to send before stopping
* @ param { string } sourceAddr Source address for sending / receiving echo requests
* @ param { boolean } numeric If true , IP addresses will be output instead of symbolic hostnames
* @ param { number } size Size ( in bytes ) of echo request to send
* @ param { number } deadline Maximum time in seconds before ping stops , regardless of packets sent
* @ param { number } timeout Maximum time in seconds to wait for each response
2022-04-20 19:56:40 +01:00
* @ returns { Promise < number > } Time for ping in ms rounded to nearest integer
* /
2025-05-24 02:57:39 +02:00
exports . pingAsync = function (
destAddr ,
ipv6 = false ,
count = PING _COUNT _DEFAULT ,
sourceAddr = "" ,
numeric = true ,
size = PING _PACKET _SIZE _DEFAULT ,
deadline = PING _GLOBAL _TIMEOUT _DEFAULT ,
2026-01-09 02:10:36 +01:00
timeout = PING _PER _REQUEST _TIMEOUT _DEFAULT
2025-05-24 02:57:39 +02:00
) {
2026-01-11 18:28:07 +07:00
try {
const url = new URL ( ` http:// ${ destAddr } ` ) ;
destAddr = url . hostname ;
if ( destAddr . startsWith ( "[" ) && destAddr . endsWith ( "]" ) ) {
destAddr = destAddr . slice ( 1 , - 1 ) ;
}
} catch ( e ) {
// ignore
}
2021-07-01 17:00:23 +08:00
return new Promise ( ( resolve , reject ) => {
2026-01-09 02:10:36 +01:00
ping . promise
. probe ( destAddr , {
v6 : ipv6 ,
min _reply : count ,
sourceAddr : sourceAddr ,
numeric : numeric ,
packetSize : size ,
deadline : deadline ,
timeout : timeout ,
} )
. then ( ( res ) => {
// If ping failed, it will set field to unknown
if ( res . alive ) {
resolve ( res . time ) ;
2023-01-05 19:22:15 +08:00
} else {
2026-01-09 02:10:36 +01:00
if ( isWindows ) {
reject ( new Error ( exports . convertToUTF8 ( res . output ) ) ) ;
} else {
reject ( new Error ( res . output ) ) ;
}
2023-01-05 19:22:15 +08:00
}
2026-01-09 02:10:36 +01:00
} )
. catch ( ( err ) => {
reject ( err ) ;
} ) ;
2021-07-01 17:00:23 +08:00
} ) ;
2021-09-20 16:22:18 +08:00
} ;
2021-07-09 14:14:03 +08:00
2023-07-17 11:45:44 +03:30
/ * *
* Monitor Kafka using Producer
2023-08-11 09:46:41 +02:00
* @ param { string [ ] } brokers List of kafka brokers to connect , host and
* port joined by ':'
2023-07-17 11:45:44 +03:30
* @ param { string } topic Topic name to produce into
* @ param { string } message Message to produce
2023-08-11 09:46:41 +02:00
* @ 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
2023-07-17 11:45:44 +03:30
* /
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 ;
2026-01-09 02:10:36 +01:00
const timeoutID = setTimeout (
( ) => {
log . debug ( "kafkaProducer" , "KafkaProducer timeout triggered" ) ;
connectedToKafka = true ;
reject ( new Error ( "Timeout" ) ) ;
} ,
interval * 1000 * 0.8
) ;
2023-07-17 11:45:44 +03:30
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 ,
2026-01-09 02:10:36 +01:00
} ,
2023-07-17 11:45:44 +03:30
} ) ;
2026-01-09 02:10:36 +01:00
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 ) => {
2023-07-17 11:45:44 +03:30
connectedToKafka = true ;
producer . disconnect ( ) ;
clearTimeout ( timeoutID ) ;
reject ( new Error ( "Error in producer connection: " + e . message ) ) ;
2026-01-09 02:10:36 +01:00
} ) ;
2023-07-17 11:45:44 +03:30
producer . on ( "producer.network.request_timeout" , ( _ ) => {
2023-09-23 23:00:15 +03:30
if ( ! connectedToKafka ) {
clearTimeout ( timeoutID ) ;
reject ( new Error ( "producer.network.request_timeout" ) ) ;
}
2023-07-17 11:45:44 +03:30
} ) ;
producer . on ( "producer.disconnect" , ( _ ) => {
if ( ! connectedToKafka ) {
clearTimeout ( timeoutID ) ;
reject ( new Error ( "producer.disconnect" ) ) ;
}
} ) ;
} ) ;
} ;
2022-05-13 12:58:23 -05:00
/ * *
* Use NTLM Auth for a http request .
2023-08-11 09:46:41 +02:00
* @ param { object } options The http request options
* @ param { object } ntlmOptions The auth options
* @ returns { Promise < ( string [ ] | object [ ] | object ) > } NTLM response
2022-05-13 12:58:23 -05:00
* /
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-03 21:46:43 -04:00
} ;
2022-10-12 17:32:05 +01: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
2023-08-11 09:46:41 +02:00
* @ param { number } port Port to contact radius server on
* @ param { number } timeout Timeout for connection to use
* @ returns { Promise < any > } Response from server
2022-10-12 17:32:05 +01:00
* /
2022-05-12 11:48:38 +02:00
exports . radius = function (
hostname ,
username ,
password ,
calledStationId ,
callingStationId ,
secret ,
2022-10-12 17:32:05 +01:00
port = 1812 ,
2026-01-09 02:10:36 +01:00
timeout = 2500
2022-05-12 11:48:38 +02:00
) {
2025-10-09 14:05:39 -07:00
const client = new RadiusClient ( {
2022-05-12 11:48:38 +02:00
host : hostname ,
2022-10-12 17:32:05 +01:00
hostPort : port ,
2023-05-23 18:18:54 +08:00
timeout : timeout ,
2023-07-27 17:42:22 +08:00
retries : 1 ,
2026-01-09 02:10:36 +01:00
dictionaries : [ file ] ,
2022-05-12 11:48:38 +02:00
} ) ;
2026-01-09 02:10:36 +01:00
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 ) => {
2026-01-22 07:57:28 -05:00
// Preserve error stack trace and provide better context
2026-01-09 02:10:36 +01:00
if ( error . response ? . code ) {
2026-01-22 07:57:28 -05:00
const radiusError = new Error ( ` RADIUS ${ error . response . code } from ${ hostname } : ${ port } ` ) ;
radiusError . response = error . response ;
radiusError . originalError = error ;
throw radiusError ;
2026-01-09 02:10:36 +01:00
} else {
2026-01-22 07:57:28 -05:00
// Preserve original error message and stack trace
const enhancedError = new Error (
` RADIUS authentication failed for ${ hostname } : ${ port } : ${ error . message } `
) ;
enhancedError . originalError = error ;
enhancedError . stack = error . stack || enhancedError . stack ;
throw enhancedError ;
2026-01-09 02:10:36 +01:00
}
} ) ;
2022-05-12 11:48:38 +02:00
} ;
2024-10-09 07:43:44 +08:00
/ * *
* 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 ) ;
} ;
2026-01-26 07:39:42 +02:00
// 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 ;
} ;
module . exports . getDaysRemaining = getDaysRemaining ;
2022-04-20 19:56:40 +01:00
/ * *
* Fix certificate info for display
2023-08-11 09:46:41 +02:00
* @ param { object } info The chain obtained from getPeerCertificate ( )
* @ returns { object } An object representing certificate information
* @ throws The certificate chain length exceeded 500.
2022-04-20 19:56:40 +01:00
* /
2021-10-01 18:44:32 +08:00
const parseCertificateInfo = function ( info ) {
let link = info ;
2021-11-08 15:39:17 +08:00
let i = 0 ;
const existingList = { } ;
2021-10-01 18:44:32 +08:00
while ( link ) {
2022-04-16 14:50:48 +08:00
log . debug ( "cert" , ` [ ${ i } ] ${ link . fingerprint } ` ) ;
2021-11-08 15:39:17 +08:00
2021-10-01 18:44:32 +08: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 ( ", " ) ;
2026-01-09 09:18:17 +06:00
link . daysRemaining = dayjs . utc ( link . validTo ) . diff ( dayjs . utc ( ) , "day" ) ;
2021-10-01 18:44:32 +08:00
2021-11-08 15:39:17 +08:00
existingList [ link . fingerprint ] = true ;
2021-10-01 18:44:32 +08:00
// Move up the chain until loop is encountered
if ( link . issuerCertificate == null ) {
2026-01-09 02:10:36 +01:00
link . certType = i === 0 ? "self-signed" : "root CA" ;
2021-10-01 18:44:32 +08:00
break ;
2021-11-08 15:39:17 +08:00
} else if ( link . issuerCertificate . fingerprint in existingList ) {
2023-01-12 11:34:37 +01: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 14:50:48 +08:00
log . debug ( "cert" , ` [Last] ${ link . issuerCertificate . fingerprint } ` ) ;
2026-01-09 02:10:36 +01:00
link . certType = i === 0 ? "self-signed" : "root CA" ;
2021-10-01 18:44:32 +08:00
link . issuerCertificate = null ;
break ;
} else {
2026-01-09 02:10:36 +01:00
link . certType = i === 0 ? "server" : "intermediate CA" ;
2021-10-01 18:44:32 +08:00
link = link . issuerCertificate ;
}
2021-11-08 15:39:17 +08:00
// Should be no use, but just in case.
if ( i > 500 ) {
throw new Error ( "Dead loop occurred in parseCertificateInfo" ) ;
}
i ++ ;
2021-07-21 12:09:09 +08:00
}
2021-10-01 18:44:32 +08:00
return info ;
} ;
2021-07-21 12:09:09 +08:00
2022-04-20 19:56:40 +01:00
/ * *
* Check if certificate is valid
2024-04-06 18:43:08 +08:00
* @ param { tls . TLSSocket } socket TLSSocket , which may or may not be connected
2026-01-08 06:22:08 +01:00
* @ returns { null | { valid : boolean , certInfo : object } } Object containing certificate information
2022-04-20 19:56:40 +01:00
* /
2024-04-06 18:43:08 +08:00
exports . checkCertificate = function ( socket ) {
let certInfoStartTime = dayjs ( ) . valueOf ( ) ;
// Return null if there is no socket
if ( socket === undefined || socket == null ) {
return null ;
2022-12-13 02:21:12 +08:00
}
2024-04-06 18:43:08 +08:00
const info = socket . getPeerCertificate ( true ) ;
const valid = socket . authorized || false ;
2021-07-21 12:09:09 +08:00
2022-04-16 14:50:48 +08:00
log . debug ( "cert" , "Parsing Certificate Info" ) ;
2021-10-01 18:44:32 +08:00
const parsedInfo = parseCertificateInfo ( info ) ;
2021-07-21 12:09:09 +08:00
2024-04-06 18:43:08 +08:00
if ( process . env . TIMELOGGER === "1" ) {
log . debug ( "monitor" , "Cert Info Query Time: " + ( dayjs ( ) . valueOf ( ) - certInfoStartTime ) + "ms" ) ;
}
2021-07-21 12:09:09 +08:00
return {
2021-10-01 18:44:32 +08:00
valid : valid ,
2026-01-09 02:10:36 +01:00
certInfo : parsedInfo ,
2021-07-21 12:09:09 +08:00
} ;
2021-09-20 16:22:18 +08:00
} ;
2021-08-05 19:04:38 +08:00
2025-12-01 10:12:47 +08:00
/ * *
* Checks if the certificate is valid for the provided hostname .
* Defaults to true if feature ` X509Certificate ` is not available , or input is not valid .
* @ param { Buffer } certBuffer - The certificate buffer .
* @ param { string } hostname - The hostname to compare against .
* @ returns { boolean } True if the certificate is valid for the provided hostname , false otherwise .
* /
exports . checkCertificateHostname = function ( certBuffer , hostname ) {
let X509Certificate ;
try {
X509Certificate = require ( "node:crypto" ) . X509Certificate ;
} catch ( _ ) {
// X509Certificate is not available in this version of Node.js
return true ;
}
if ( ! X509Certificate || ! certBuffer || ! hostname ) {
return true ;
}
let certObject = new X509Certificate ( certBuffer ) ;
return certObject . checkHost ( hostname ) !== undefined ;
} ;
2022-04-20 19:56:40 +01:00
/ * *
* Check if the provided status code is within the accepted ranges
2022-07-18 22:06:25 +08:00
* @ param { number } status The status code to check
2022-04-21 20:02:18 +01:00
* @ param { string [ ] } acceptedCodes An array of accepted status codes
2022-04-20 19:56:40 +01:00
* @ returns { boolean } True if status code within range , false otherwise
* /
2022-04-17 01:39:49 +08:00
exports . checkStatusCode = function ( status , acceptedCodes ) {
if ( acceptedCodes == null || acceptedCodes . length === 0 ) {
2021-08-05 19:04:38 +08:00
return false ;
}
2022-04-17 01:39:49 +08: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 ;
}
2026-01-09 02:10:36 +01:00
const codeRangeSplit = codeRange . split ( "-" ) . map ( ( string ) => parseInt ( string ) ) ;
2022-04-17 01:39:49 +08:00
if ( codeRangeSplit . length === 1 ) {
if ( status === codeRangeSplit [ 0 ] ) {
2021-08-05 19:04:38 +08:00
return true ;
}
2022-04-17 01:39:49 +08:00
} else if ( codeRangeSplit . length === 2 ) {
if ( status >= codeRangeSplit [ 0 ] && status <= codeRangeSplit [ 1 ] ) {
2021-08-05 19:04:38 +08: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 19:04:38 +08:00
}
}
return false ;
2021-09-20 16:22:18 +08:00
} ;
2021-08-30 14:55:33 +08:00
2022-04-20 19:56:40 +01:00
/ * *
* Get total number of clients in room
* @ param { Server } io Socket server instance
* @ param { string } roomName Name of room to check
2023-08-11 09:46:41 +02:00
* @ returns { number } Total clients in room
2022-04-20 19:56:40 +01:00
* /
2021-08-30 14:55:33 +08:00
exports . getTotalClientInRoom = ( io , roomName ) => {
const sockets = io . sockets ;
2021-11-03 21:46:43 -04:00
if ( ! sockets ) {
2021-08-30 14:55:33 +08:00
return 0 ;
}
const adapter = sockets . adapter ;
2021-11-03 21:46:43 -04:00
if ( ! adapter ) {
2021-08-30 14:55:33 +08:00
return 0 ;
}
const room = adapter . rooms . get ( roomName ) ;
if ( room ) {
return room . size ;
} else {
return 0 ;
}
2021-09-20 16:22:18 +08:00
} ;
2021-09-11 19:40:03 +08:00
2022-04-20 19:56:40 +01:00
/ * *
* Allow CORS all origins if development
2023-08-11 09:46:41 +02:00
* @ param { object } res Response object from axios
* @ returns { void }
2022-04-20 19:56:40 +01:00
* /
2021-09-11 19:40:03 +08:00
exports . allowDevAllOrigin = ( res ) => {
if ( process . env . NODE _ENV === "development" ) {
exports . allowAllOrigin ( res ) ;
}
2021-09-20 16:22:18 +08:00
} ;
2021-09-11 19:40:03 +08:00
2022-04-20 19:56:40 +01:00
/ * *
* Allow CORS all origins
2023-08-11 09:46:41 +02:00
* @ param { object } res Response object from axios
* @ returns { void }
2022-04-20 19:56:40 +01:00
* /
2021-09-11 19:40:03 +08:00
exports . allowAllOrigin = ( res ) => {
res . header ( "Access-Control-Allow-Origin" , "*" ) ;
2023-02-11 14:41:02 +08:00
res . header ( "Access-Control-Allow-Methods" , "GET, PUT, POST, DELETE, OPTIONS" ) ;
2021-09-11 19:40:03 +08:00
res . header ( "Access-Control-Allow-Headers" , "Origin, X-Requested-With, Content-Type, Accept" ) ;
2021-09-20 16:22:18 +08:00
} ;
2021-09-16 22:48:28 +08:00
2022-04-20 19:56:40 +01:00
/ * *
* Check if a user is logged in
* @ param { Socket } socket Socket instance
2023-08-11 09:46:41 +02:00
* @ returns { void }
* @ throws The user is not logged in
2022-04-20 19:56:40 +01:00
* /
2021-09-16 22:48:28 +08:00
exports . checkLogin = ( socket ) => {
2021-11-03 21:46:43 -04:00
if ( ! socket . userID ) {
2021-09-16 22:48:28 +08:00
throw new Error ( "You are not logged in." ) ;
}
2021-09-20 16:22:18 +08:00
} ;
2021-10-05 19:13:57 +08:00
2022-03-29 17:38:48 +08:00
/ * *
* For logged - in users , double - check the password
2022-04-21 13:01:22 +01:00
* @ param { Socket } socket Socket . io instance
2023-08-11 09:46:41 +02:00
* @ 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
2022-03-29 17:38:48 +08:00
* /
exports . doubleCheckPassword = async ( socket , currentPassword ) => {
if ( typeof currentPassword !== "string" ) {
throw new Error ( "Wrong data type?" ) ;
}
2026-01-09 02:10:36 +01:00
let user = await R . findOne ( "user" , " id = ? AND active = 1 " , [ socket . userID ] ) ;
2022-03-29 17:38:48 +08:00
if ( ! user || ! passwordHash . verify ( currentPassword , user . password ) ) {
throw new Error ( "Incorrect current password" ) ;
}
return user ;
} ;
2021-10-14 00:22:49 +08:00
/ * *
2022-04-20 19:56:40 +01:00
* Convert unknown string to UTF8
* @ param { Uint8Array } body Buffer
2023-08-11 09:46:41 +02:00
* @ returns { string } UTF8 string
2021-10-14 00:22:49 +08:00
* /
exports . convertToUTF8 = ( body ) => {
const guessEncoding = chardet . detect ( body ) ;
const str = iconv . decode ( body , guessEncoding ) ;
return str . toString ( ) ;
} ;
2021-10-29 18:24:47 +08:00
2022-01-03 16:04:37 +01:00
/ * *
* Returns a color code in hex format based on a given percentage :
* 0 % => hue = 10 => red
* 100 % => hue = 90 => green
2022-04-30 21:36:00 +08:00
* @ param { number } percentage float , 0 to 1
2023-08-11 09:46:41 +02:00
* @ param { number } maxHue Maximum hue - int
* @ param { number } minHue Minimum hue - int
* @ returns { string } Color in hex
2022-01-03 16:04:37 +01:00
* /
2022-01-03 15:48:52 +01: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 12:21:53 +01:00
return badgeConstants . naColor ;
2022-01-03 15:48:52 +01:00
}
} ;
2022-01-04 16:00:21 +01:00
/ * *
* Joins and array of string to one string after filtering out empty values
2023-08-11 09:46:41 +02:00
* @ param { string [ ] } parts Strings to join
* @ param { string } connector Separator for joined strings
* @ returns { string } Joined strings
2022-01-04 16:00:21 +01:00
* /
exports . filterAndJoin = ( parts , connector = "" ) => {
return parts . filter ( ( part ) => ! ! part && part !== "" ) . join ( connector ) ;
} ;
2022-06-01 13:05:12 +08:00
/ * *
2023-02-09 17:42:02 +08:00
* Send an Error response
2023-08-11 09:46:41 +02:00
* @ param { object } res Express response object
* @ param { string } msg Message to send
* @ returns { void }
2022-06-01 13:05:12 +08:00
* /
2023-02-09 17:42:02 +08:00
module . exports . sendHttpError = ( res , msg = "" ) => {
if ( msg . includes ( "SQLITE_BUSY" ) || msg . includes ( "SQLITE_LOCKED" ) ) {
res . status ( 503 ) . json ( {
2026-01-09 02:10:36 +01:00
status : "fail" ,
msg : msg ,
2023-02-09 17:42:02 +08:00
} ) ;
} else if ( msg . toLowerCase ( ) . includes ( "not found" ) ) {
res . status ( 404 ) . json ( {
2026-01-09 02:10:36 +01:00
status : "fail" ,
msg : msg ,
2023-02-09 17:42:02 +08:00
} ) ;
} else {
res . status ( 403 ) . json ( {
2026-01-09 02:10:36 +01:00
status : "fail" ,
msg : msg ,
2023-02-09 17:42:02 +08:00
} ) ;
}
2022-06-01 13:05:12 +08:00
} ;
2022-08-03 12:00:39 +07:00
2023-08-11 09:46:41 +02:00
/ * *
* 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
* /
2022-09-25 19:38:28 +08:00
function timeObjectConvertTimezone ( obj , timezone , timeObjectToUTC = true ) {
2022-10-10 20:48:11 +08:00
let offsetString ;
if ( timezone ) {
offsetString = dayjs ( ) . tz ( timezone ) . format ( "Z" ) ;
} else {
offsetString = dayjs ( ) . format ( "Z" ) ;
}
2022-09-25 19:38:28 +08:00
let hours = parseInt ( offsetString . substring ( 1 , 3 ) ) ;
let minutes = parseInt ( offsetString . substring ( 4 , 6 ) ) ;
2026-01-09 02:10:36 +01:00
if ( ( timeObjectToUTC && offsetString . startsWith ( "+" ) ) || ( ! timeObjectToUTC && offsetString . startsWith ( "-" ) ) ) {
2022-09-25 19:38:28 +08:00
hours *= - 1 ;
minutes *= - 1 ;
}
obj . hours += hours ;
obj . minutes += minutes ;
// Handle out of bound
2022-10-12 17:02:16 +08:00
if ( obj . minutes < 0 ) {
obj . minutes += 60 ;
obj . hours -- ;
} else if ( obj . minutes > 60 ) {
obj . minutes -= 60 ;
obj . hours ++ ;
}
2022-09-25 19:38:28 +08:00
if ( obj . hours < 0 ) {
obj . hours += 24 ;
} else if ( obj . hours > 24 ) {
obj . hours -= 24 ;
}
return obj ;
}
2022-10-10 20:48:11 +08:00
/ * *
2023-08-11 09:46:41 +02:00
* Convert time object to UTC
* @ param { object } obj Object to convert
* @ param { string } timezone Timezone of time object
* @ returns { object } Updated time object
2022-10-10 20:48:11 +08:00
* /
module . exports . timeObjectToUTC = ( obj , timezone = undefined ) => {
2022-09-25 19:38:28 +08:00
return timeObjectConvertTimezone ( obj , timezone , true ) ;
} ;
2022-10-10 20:48:11 +08:00
/ * *
2023-08-11 09:46:41 +02:00
* Convert time object to local time
* @ param { object } obj Object to convert
* @ param { string } timezone Timezone to convert to
* @ returns { object } Updated object
2022-10-10 20:48:11 +08:00
* /
module . exports . timeObjectToLocal = ( obj , timezone = undefined ) => {
2022-09-25 19:38:28 +08:00
return timeObjectConvertTimezone ( obj , timezone , false ) ;
} ;
2022-10-26 20:41:21 +07:00
2023-10-16 02:20:38 +08:00
/ * *
* Returns an array of SHA256 fingerprints for all known root certificates .
* @ returns { Set } A set of SHA256 fingerprints .
* /
module . exports . rootCertificatesFingerprints = ( ) => {
2026-01-09 02:10:36 +01:00
let fingerprints = tls . rootCertificates . map ( ( cert ) => {
2023-10-16 02:20:38 +08:00
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 ) ;
2026-01-09 02:10:36 +01:00
return shasum
. digest ( "hex" )
. toUpperCase ( )
. replace ( /(.{2})(?!$)/g , "$1:" ) ;
2023-10-16 02:20:38 +08:00
} ) ;
2026-01-09 02:10:36 +01:00
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
2023-10-16 02:20:38 +08:00
return new Set ( fingerprints ) ;
} ;
2023-10-09 07:01:54 +08:00
module . exports . SHAKE256 _LENGTH = 16 ;
/ * *
2023-10-09 21:39:55 +05:00
* @ param { string } data The data to be hashed
* @ param { number } len Output length of the hash
* @ returns { string } The hashed data in hex format
2023-10-09 07:01:54 +08:00
* /
module . exports . shake256 = ( data , len ) => {
if ( ! data ) {
return "" ;
}
2026-01-09 02:10:36 +01:00
return crypto . createHash ( "shake256" , { outputLength : len } ) . update ( data ) . digest ( "hex" ) ;
2023-10-09 07:01:54 +08:00
} ;
2023-10-12 21:26:11 +08:00
/ * *
* 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 ) ;
} ;
2023-08-09 20:09:56 +08:00
2023-08-04 01:10:15 +08:00
// For unit test, export functions
if ( process . env . TEST _BACKEND ) {
module . exports . _ _test = {
parseCertificateInfo ,
} ;
module . exports . _ _getPrivateFunction = ( functionName ) => {
return module . exports . _ _test [ functionName ] ;
} ;
}
2023-11-01 10:10:48 +08:00
/ * *
* 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 {
2023-11-12 13:50:51 +08:00
// Just in case, as 0 timeout here will cause the request to be aborted immediately
if ( ! timeoutMs || timeoutMs <= 0 ) {
timeoutMs = 5000 ;
}
2023-11-01 10:10:48 +08:00
return AbortSignal . timeout ( timeoutMs ) ;
} catch ( _ ) {
// v16-: AbortSignal.timeout is not supported
try {
const abortController = new AbortController ( ) ;
2023-11-12 13:50:51 +08:00
setTimeout ( ( ) => abortController . abort ( ) , timeoutMs ) ;
2023-11-01 10:10:48 +08:00
return abortController . signal ;
} catch ( _ ) {
// v15-: AbortController is not supported
return null ;
}
}
} ;
2025-06-25 13:39:00 +08:00
/ * *
* Async version of fs . existsSync
* @ param { PathLike } path File path
* @ returns { Promise < boolean > } True if file exists , false otherwise
* /
function fsExists ( path ) {
return new Promise ( function ( resolve , reject ) {
exists ( path , function ( exists ) {
resolve ( exists ) ;
} ) ;
} ) ;
}
module . exports . fsExists = fsExists ;
2025-10-28 00:27:29 +08:00
2026-01-26 07:39:42 +02:00
/ * *
* Encode user and password to Base64 encoding
* for HTTP "basic" auth , as per RFC - 7617
* @ param { string | null } user - The username ( defaults to empty string if null / undefined )
* @ param { string | null } pass - The password ( defaults to empty string if null / undefined )
* @ returns { string } Encoded Base64 string
* /
function encodeBase64 ( user , pass ) {
return Buffer . from ( ` ${ user || "" } : ${ pass || "" } ` ) . toString ( "base64" ) ;
}
module . exports . encodeBase64 = encodeBase64 ;
/ * *
* checks certificate chain for expiring certificates
* @ param { object } monitor - The monitor object
* @ param { object } tlsInfoObject Information about certificate
* @ returns { Promise < void > }
* /
async function checkCertExpiryNotifications ( monitor , tlsInfoObject ) {
if ( ! tlsInfoObject || ! tlsInfoObject . certInfo || ! tlsInfoObject . certInfo . daysRemaining ) {
return ;
}
let notificationList = await R . getAll (
"SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id " ,
[ monitor . id ]
) ;
if ( ! notificationList . length > 0 ) {
// fail fast. If no notification is set, all the following checks can be skipped.
log . debug ( "monitor" , "No notification, no need to send cert notification" ) ;
return ;
}
let notifyDays = await Settings . get ( "tlsExpiryNotifyDays" ) ;
if ( notifyDays == null || ! Array . isArray ( notifyDays ) ) {
// Reset Default
2026-01-31 01:41:51 +02:00
await Settings . set ( "tlsExpiryNotifyDays" , [ 7 , 14 , 21 ] , "general" ) ;
2026-01-26 07:39:42 +02:00
notifyDays = [ 7 , 14 , 21 ] ;
}
for ( const targetDays of notifyDays ) {
let certInfo = tlsInfoObject . certInfo ;
while ( certInfo ) {
let subjectCN = certInfo . subject [ "CN" ] ;
if ( monitor . rootCertificates . has ( certInfo . fingerprint256 ) ) {
log . debug (
"monitor" ,
` Known root cert: ${ certInfo . certType } certificate " ${ subjectCN } " ( ${ certInfo . daysRemaining } days valid) on ${ targetDays } deadline. `
) ;
break ;
} else if ( certInfo . daysRemaining > targetDays ) {
log . debug (
"monitor" ,
` No need to send cert notification for ${ certInfo . certType } certificate " ${ subjectCN } " ( ${ certInfo . daysRemaining } days valid) on ${ targetDays } deadline. `
) ;
} else {
log . debug (
"monitor" ,
` call sendCertNotificationByTargetDays for ${ targetDays } deadline on certificate ${ subjectCN } . `
) ;
await monitor . sendCertNotificationByTargetDays (
subjectCN ,
certInfo . certType ,
certInfo . daysRemaining ,
targetDays ,
notificationList
) ;
}
certInfo = certInfo . issuerCertificate ;
}
}
}
module . exports . checkCertExpiryNotifications = checkCertExpiryNotifications ;
2025-10-28 00:27:29 +08:00
/ * *
* By default , command - exists will throw a null error if the command does not exist , which is ugly . The function makes it better .
* Read more : https : //github.com/mathisonian/command-exists/issues/22
* @ param { string } command Command to check
* @ returns { Promise < boolean > } True if command exists , false otherwise
* /
async function commandExists ( command ) {
try {
await require ( "command-exists" ) ( command ) ;
return true ;
} catch ( e ) {
return false ;
}
}
module . exports . commandExists = commandExists ;
2026-02-23 17:04:48 +02:00
/ * *
* Log the server 's listening URLs, similar to Vite' s dev server output .
* When no hostname is specified ( bound to all interfaces ) , it prints
* localhost plus every non - internal network address .
* @ param { string } tag Log tag ( e . g . "server" , "setup-database" )
* @ param { number } port Port number
* @ param { string } hostname Bound hostname , if any
2026-03-25 02:37:19 +08:00
* @ param { boolean } isHTTPS Whether the server is using HTTPS
2026-02-23 17:04:48 +02:00
* @ returns { void }
* /
2026-03-25 02:37:19 +08:00
module . exports . printServerUrls = ( tag , port , hostname , isHTTPS = false ) => {
try {
// If hostname is specified, just print that one.
if ( hostname ) {
log . info ( tag , ` Listening on: ` , createURL ( isHTTPS , hostname , port ) ) ;
return ;
}
// Since no hostname is specified, which means the server is bound to all interfaces, we need to print all possible URLs.
const nets = networkInterfaces ( ) ;
2026-02-23 17:04:48 +02:00
2026-03-25 02:37:19 +08:00
log . info ( tag , "Listening on:" ) ;
log . info ( tag , ` - ` , createURL ( isHTTPS , "localhost" , port ) ) ;
// Prepare a list of valid address
const addressList = [ ] ;
for ( const iface of Object . values ( nets ) ) {
for ( const addr of iface ) {
if ( ! addr . internal ) {
addressList . push ( addr ) ;
}
}
}
2026-02-23 17:04:48 +02:00
2026-03-25 02:37:19 +08:00
// Sort IPv4 addresses first
addressList . sort ( ( a , b ) => {
if ( a . family === "IPv4" && b . family === "IPv6" ) {
return - 1 ;
} else if ( a . family === "IPv6" && b . family === "IPv4" ) {
return 1 ;
} else {
return a . address . localeCompare ( b . address ) ;
}
} ) ;
2026-02-23 17:04:48 +02:00
2026-03-25 02:37:19 +08:00
for ( const address of addressList ) {
if ( ! address . internal ) {
const host = address . family === "IPv6" ? ` [ ${ address . address } ] ` : address . address ;
log . info ( tag , ` - ` , createURL ( isHTTPS , host , port ) ) ;
2026-02-23 17:04:48 +02:00
}
}
2026-03-25 02:37:19 +08:00
} catch ( e ) {
log . error ( tag , "Error printing server URLs: " + e . message ) ;
2026-02-23 17:04:48 +02:00
}
} ;
2026-03-25 02:37:19 +08:00
/ * *
* Construct a URL a bit more safely
* @ param { boolean } isHTTPS Whether the URL should use HTTPS protocol
* @ param { string } hostname The hostname to use in the URL
* @ param { number } port The port
* @ returns { string } The constructed URL as a string
* /
function createURL ( isHTTPS , hostname , port = 80 ) {
const url = new URL ( ( isHTTPS ? "https" : "http" ) + ` :// ` + hostname ) ;
url . port = String ( port ) ;
// Prefer origin if available, it doesn't contain the trailing slash
return url . origin || url . toString ( ) ;
}