2021-07-27 13:47:13 -04:00
const tcpp = require ( "tcp-ping" ) ;
2023-01-11 12:04:58 -05:00
const ping = require ( "@louislam/ping" ) ;
2021-07-27 13:47:13 -04:00
const { R } = require ( "redbean-node" ) ;
2023-12-10 07:40:40 -05:00
const { log , genSecret , badgeConstants } = require ( "../src/util" ) ;
2021-08-09 01:34:44 -04:00
const passwordHash = require ( "./password-hash" ) ;
2021-08-23 10:30:11 -04:00
const { Resolver } = require ( "dns" ) ;
2021-10-13 12:22:49 -04:00
const iconv = require ( "iconv-lite" ) ;
const chardet = require ( "chardet" ) ;
2022-01-03 09:48:52 -05:00
const chroma = require ( "chroma-js" ) ;
2022-05-13 09:40:46 -04:00
const mssql = require ( "mssql" ) ;
2022-06-15 13:12:47 -04:00
const { Client } = require ( "pg" ) ;
2022-06-15 14:00:14 -04:00
const postgresConParse = require ( "pg-connection-string" ) . parse ;
2022-11-17 13:34:02 -05:00
const mysql = require ( "mysql2" ) ;
2022-05-13 13:58:23 -04:00
const { NtlmClient } = require ( "axios-ntlm" ) ;
2022-06-29 02:57:40 -04:00
const { Settings } = require ( "./settings" ) ;
2022-08-03 01:00:39 -04:00
const grpc = require ( "@grpc/grpc-js" ) ;
const protojs = require ( "protobufjs" ) ;
2022-05-12 05:48:38 -04:00
const radiusClient = require ( "node-radius-client" ) ;
2023-01-05 09:58:24 -05:00
const redis = require ( "redis" ) ;
2023-08-02 03:40:19 -04:00
const oidc = require ( "openid-client" ) ;
2023-10-15 14:20:38 -04:00
const tls = require ( "tls" ) ;
2023-08-02 03:40:19 -04:00
2022-05-12 05:48:38 -04:00
const {
dictionaries : {
rfc2865 : { file , attributes } ,
} ,
} = require ( "node-radius-utils" ) ;
2022-09-25 07:38:28 -04:00
const dayjs = require ( "dayjs" ) ;
2021-08-09 01:34:44 -04:00
2023-07-17 04:15:44 -04:00
// SASLOptions used in JSDoc
// eslint-disable-next-line no-unused-vars
const { Kafka , SASLOptions } = require ( "kafkajs" ) ;
2023-10-08 19:01:54 -04:00
const crypto = require ( "crypto" ) ;
2023-01-05 06:22:15 -05:00
2023-07-17 04:15:44 -04:00
const isWindows = process . platform === /^win/ . test ( process . platform ) ;
2021-08-09 01:34:44 -04:00
/ * *
* Init or reset JWT secret
2023-08-11 03:46:41 -04:00
* @ returns { Promise < Bean > } JWT secret
2021-08-09 01:34:44 -04:00
* /
exports . initJWTSecret = async ( ) => {
let jwtSecretBean = await R . findOne ( "setting" , " `key` = ? " , [
"jwtSecret" ,
] ) ;
2021-11-03 21:46:43 -04:00
if ( ! jwtSecretBean ) {
2021-08-09 01:34:44 -04:00
jwtSecretBean = R . dispense ( "setting" ) ;
jwtSecretBean . key = "jwtSecret" ;
}
2022-03-29 05:38:48 -04:00
jwtSecretBean . value = passwordHash . generate ( genSecret ( ) ) ;
2021-08-09 01:34:44 -04:00
await R . store ( jwtSecretBean ) ;
return jwtSecretBean ;
2021-09-20 04:22:18 -04:00
} ;
2021-07-01 02:03:06 -04:00
2023-08-02 03:40:19 -04:00
/ * *
* Decodes a jwt and returns the payload portion without verifying the jqt .
* @ param { string } jwt The input jwt as a string
2023-08-11 03:46:41 -04:00
* @ returns { object } Decoded jwt payload object
2023-08-02 03:40:19 -04:00
* /
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 14:56:40 -04: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 02:03:06 -04:00
exports . tcping = function ( hostname , port ) {
return new Promise ( ( resolve , reject ) => {
tcpp . ping ( {
address : hostname ,
port : port ,
attempts : 1 ,
2021-08-05 07:04:38 -04:00
} , function ( err , data ) {
2021-07-01 02:03:06 -04: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 04:22:18 -04:00
} ;
2021-07-01 05:00:23 -04:00
2022-04-20 14:56:40 -04:00
/ * *
* Ping the specified machine
* @ param { string } hostname Hostname / address of machine
2023-08-11 03:46:41 -04:00
* @ param { number } size Size of packet to send
2022-04-20 14:56:40 -04:00
* @ returns { Promise < number > } Time for ping in ms rounded to nearest integer
* /
2022-07-14 03:32:51 -04:00
exports . ping = async ( hostname , size = 56 ) => {
2021-08-10 09:03:14 -04:00
try {
2023-01-06 15:12:21 -05:00
return await exports . pingAsync ( hostname , false , size ) ;
2021-08-10 09:03:14 -04:00
} catch ( e ) {
// If the host cannot be resolved, try again with ipv6
2024-01-05 07:43:03 -05:00
log . debug ( "ping" , "IPv6 error message: " + e . message ) ;
2023-03-04 07:29:52 -05:00
2023-03-04 07:41:08 -05: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 07:29:52 -05:00
if ( ! e . message ) {
return await exports . pingAsync ( hostname , true , size ) ;
2021-08-10 09:03:14 -04:00
} else {
throw e ;
}
}
2021-09-20 04:22:18 -04:00
} ;
2021-08-10 09:03:14 -04:00
2022-04-20 14:56:40 -04:00
/ * *
* Ping the specified machine
* @ param { string } hostname Hostname / address of machine to ping
* @ param { boolean } ipv6 Should IPv6 be used ?
2023-08-11 03:46:41 -04:00
* @ param { number } size Size of ping packet to send
2022-04-20 14:56:40 -04:00
* @ returns { Promise < number > } Time for ping in ms rounded to nearest integer
* /
2022-07-14 03:32:51 -04:00
exports . pingAsync = function ( hostname , ipv6 = false , size = 56 ) {
2021-07-01 05:00:23 -04:00
return new Promise ( ( resolve , reject ) => {
2023-01-03 15:03:36 -05:00
ping . promise . probe ( hostname , {
v6 : ipv6 ,
2023-01-05 06:30:55 -05:00
min _reply : 1 ,
2023-01-16 12:21:01 -05:00
deadline : 10 ,
2023-01-06 15:09:40 -05:00
packetSize : size ,
2023-01-03 15:03:36 -05:00
} ) . then ( ( res ) => {
// If ping failed, it will set field to unknown
2023-01-04 12:32:27 -05:00
if ( res . alive ) {
2023-01-03 15:03:36 -05:00
resolve ( res . time ) ;
2023-01-04 12:32:27 -05:00
} else {
2023-01-05 06:22:15 -05:00
if ( isWindows ) {
reject ( new Error ( exports . convertToUTF8 ( res . output ) ) ) ;
} else {
reject ( new Error ( res . output ) ) ;
}
2021-07-01 05:00:23 -04:00
}
2023-01-03 15:03:36 -05:00
} ) . catch ( ( err ) => {
reject ( err ) ;
2021-07-01 05:00:23 -04:00
} ) ;
} ) ;
2021-09-20 04:22:18 -04:00
} ;
2021-07-09 02:14:03 -04:00
2023-07-17 04:15:44 -04:00
/ * *
* Monitor Kafka using Producer
2023-08-11 03:46:41 -04:00
* @ param { string [ ] } brokers List of kafka brokers to connect , host and
* port joined by ':'
2023-07-17 04:15:44 -04:00
* @ param { string } topic Topic name to produce into
* @ param { string } message Message to produce
2023-08-11 03:46:41 -04: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 04:15:44 -04:00
* /
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 15:30:15 -04:00
producer . send ( {
topic : topic ,
messages : [ {
value : message ,
} ] ,
} ) . then ( ( _ ) => {
2023-07-17 04:15:44 -04:00
resolve ( "Message sent successfully" ) ;
2023-09-23 15:30:15 -04:00
} ) . catch ( ( e ) => {
2023-07-17 04:15:44 -04:00
connectedToKafka = true ;
producer . disconnect ( ) ;
clearTimeout ( timeoutID ) ;
reject ( new Error ( "Error sending message: " + e . message ) ) ;
2023-09-23 15:30:15 -04:00
} ) . finally ( ( ) => {
connectedToKafka = true ;
clearTimeout ( timeoutID ) ;
} ) ;
2023-07-17 04:15:44 -04: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 15:30:15 -04:00
if ( ! connectedToKafka ) {
clearTimeout ( timeoutID ) ;
reject ( new Error ( "producer.network.request_timeout" ) ) ;
}
2023-07-17 04:15:44 -04:00
} ) ;
producer . on ( "producer.disconnect" , ( _ ) => {
if ( ! connectedToKafka ) {
clearTimeout ( timeoutID ) ;
reject ( new Error ( "producer.disconnect" ) ) ;
}
} ) ;
} ) ;
} ;
2022-05-13 13:58:23 -04:00
/ * *
* Use NTLM Auth for a http request .
2023-08-11 03:46:41 -04:00
* @ param { object } options The http request options
* @ param { object } ntlmOptions The auth options
* @ returns { Promise < ( string [ ] | object [ ] | object ) > } NTLM response
2022-05-13 13:58:23 -04: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-04-20 14:56:40 -04:00
/ * *
* Resolves a given record using the specified DNS server
* @ param { string } hostname The hostname of the record to lookup
2022-04-21 08:01:22 -04:00
* @ param { string } resolverServer The DNS server to use
2022-04-27 13:26:11 -04:00
* @ param { string } resolverPort Port the DNS server is listening on
2022-04-20 14:56:40 -04:00
* @ param { string } rrtype The type of record to request
2023-08-11 03:46:41 -04:00
* @ returns { Promise < ( string [ ] | object [ ] | object ) > } DNS response
2022-04-20 14:56:40 -04:00
* /
2022-04-23 20:06:45 -04:00
exports . dnsResolve = function ( hostname , resolverServer , resolverPort , rrtype ) {
2021-08-22 18:05:48 -04:00
const resolver = new Resolver ( ) ;
2022-04-15 14:59:32 -04:00
// Remove brackets from IPv6 addresses so we can re-add them to
// prevent issues with ::1:5300 (::1 port 5300)
2022-04-23 20:06:45 -04:00
resolverServer = resolverServer . replace ( "[" , "" ) . replace ( "]" , "" ) ;
2022-06-01 01:05:12 -04:00
resolver . setServers ( [ ` [ ${ resolverServer } ]: ${ resolverPort } ` ] ) ;
2021-08-22 18:05:48 -04:00
return new Promise ( ( resolve , reject ) => {
2022-04-17 03:43:03 -04:00
if ( rrtype === "PTR" ) {
2021-08-22 18:05:48 -04: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 04:22:18 -04:00
} ) ;
} ;
2021-08-22 18:05:48 -04:00
2022-05-12 13:48:03 -04:00
/ * *
* Run a query on SQL Server
* @ param { string } connectionString The database connection string
* @ param { string } query The query to validate the database with
2023-08-11 03:46:41 -04:00
* @ returns { Promise < ( string [ ] | object [ ] | object ) > } Response from
* server
2022-05-12 13:48:03 -04:00
* /
2023-01-01 09:27:14 -05:00
exports . mssqlQuery = async function ( connectionString , query ) {
let pool ;
try {
pool = new mssql . ConnectionPool ( connectionString ) ;
await pool . connect ( ) ;
2023-10-12 14:50:10 -04:00
if ( ! query ) {
query = "SELECT 1" ;
}
2023-01-01 09:27:14 -05:00
await pool . request ( ) . query ( query ) ;
pool . close ( ) ;
} catch ( e ) {
if ( pool ) {
pool . close ( ) ;
}
throw e ;
}
2022-05-12 13:48:03 -04:00
} ;
2022-06-15 13:12:47 -04:00
/ * *
* Run a query on Postgres
* @ param { string } connectionString The database connection string
* @ param { string } query The query to validate the database with
2023-08-11 03:46:41 -04:00
* @ returns { Promise < ( string [ ] | object [ ] | object ) > } Response from
* server
2022-06-15 13:12:47 -04:00
* /
exports . postgresQuery = function ( connectionString , query ) {
return new Promise ( ( resolve , reject ) => {
2022-06-15 14:00:14 -04:00
const config = postgresConParse ( connectionString ) ;
2023-10-12 14:50:10 -04:00
// Fix #3868, which true/false is not parsed to boolean
if ( typeof config . ssl === "string" ) {
config . ssl = config . ssl === "true" ;
}
2022-06-15 14:00:14 -04:00
if ( config . password === "" ) {
// See https://github.com/brianc/node-postgres/issues/1927
2023-10-12 14:50:10 -04:00
reject ( new Error ( "Password is undefined." ) ) ;
return ;
2022-06-15 14:00:14 -04:00
}
2023-10-12 14:50:10 -04:00
const client = new Client ( config ) ;
2022-06-15 14:00:14 -04:00
2023-10-12 14:50:10 -04:00
client . on ( "error" , ( error ) => {
log . debug ( "postgres" , "Error caught in the error event handler." ) ;
reject ( error ) ;
} ) ;
2022-06-15 13:12:47 -04:00
2023-01-14 08:06:10 -05:00
client . connect ( ( err ) => {
if ( err ) {
2022-06-15 13:12:47 -04:00
reject ( err ) ;
client . end ( ) ;
2023-01-14 08:06:10 -05:00
} else {
// Connected here
2023-02-14 13:50:49 -05:00
try {
// No query provided by user, use SELECT 1
if ( ! query || ( typeof query === "string" && query . trim ( ) === "" ) ) {
query = "SELECT 1" ;
2023-01-14 08:06:10 -05:00
}
2023-02-14 13:50:49 -05:00
client . query ( query , ( err , res ) => {
if ( err ) {
reject ( err ) ;
} else {
resolve ( res ) ;
}
client . end ( ) ;
} ) ;
} catch ( e ) {
reject ( e ) ;
2023-11-22 06:50:03 -05:00
client . end ( ) ;
2023-02-14 13:50:49 -05:00
}
2023-01-14 08:06:10 -05:00
}
} ) ;
2022-06-15 13:12:47 -04:00
} ) ;
} ;
2022-10-01 20:52:53 -04: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 12:38:56 -04:00
* @ param { ? string } password The password to use
2023-08-11 03:46:41 -04:00
* @ returns { Promise < ( string ) > } Response from server
2022-10-01 20:52:53 -04:00
* /
2023-10-15 12:38:56 -04:00
exports . mysqlQuery = function ( connectionString , query , password = undefined ) {
2022-10-01 20:52:53 -04:00
return new Promise ( ( resolve , reject ) => {
2023-10-15 12:38:56 -04:00
const connection = mysql . createConnection ( {
uri : connectionString ,
password
} ) ;
2023-03-24 04:08:30 -04:00
connection . on ( "error" , ( err ) => {
reject ( err ) ;
} ) ;
connection . query ( query , ( err , res ) => {
if ( err ) {
2022-10-01 20:52:53 -04:00
reject ( err ) ;
2023-03-24 04:08:30 -04:00
} else {
2023-03-24 04:24:00 -04: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 04:08:30 -04:00
}
2023-06-06 08:28:51 -04:00
try {
connection . end ( ) ;
} catch ( _ ) {
connection . destroy ( ) ;
}
2023-03-24 04:08:30 -04:00
} ) ;
2022-10-01 20:52:53 -04:00
} ) ;
} ;
2022-10-12 12:32:05 -04: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 03:46:41 -04: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 12:32:05 -04:00
* /
2022-05-12 05:48:38 -04:00
exports . radius = function (
hostname ,
username ,
password ,
calledStationId ,
callingStationId ,
secret ,
2022-10-12 12:32:05 -04:00
port = 1812 ,
2023-05-23 06:18:54 -04:00
timeout = 2500 ,
2022-05-12 05:48:38 -04:00
) {
const client = new radiusClient ( {
host : hostname ,
2022-10-12 12:32:05 -04:00
hostPort : port ,
2023-05-23 06:18:54 -04:00
timeout : timeout ,
2023-07-27 05:42:22 -04:00
retries : 1 ,
2022-05-12 05:48:38 -04: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 05:42:22 -04:00
} ) . catch ( ( error ) => {
if ( error . response ? . code ) {
throw Error ( error . response . code ) ;
} else {
throw Error ( error . message ) ;
}
2022-05-12 05:48:38 -04:00
} ) ;
} ;
2023-01-05 09:58:24 -05:00
/ * *
* Redis server ping
* @ param { string } dsn The redis connection string
2024-05-19 17:34:01 -04:00
* @ param { boolean } rejectUnauthorized If false , allows unverified server certificates .
* @ returns { Promise < any > } Response from server
2023-01-05 09:58:24 -05:00
* /
2024-05-19 17:34:01 -04:00
exports . redisPingAsync = function ( dsn , rejectUnauthorized ) {
2023-01-13 06:10:07 -05:00
return new Promise ( ( resolve , reject ) => {
const client = redis . createClient ( {
2024-05-19 17:34:01 -04:00
url : dsn ,
socket : {
rejectUnauthorized
}
2023-01-13 06:10:07 -05:00
} ) ;
client . on ( "error" , ( err ) => {
2023-06-14 11:49:33 -04:00
if ( client . isOpen ) {
client . disconnect ( ) ;
}
2023-01-13 06:10:07 -05:00
reject ( err ) ;
} ) ;
client . connect ( ) . then ( ( ) => {
2023-06-14 11:49:33 -04:00
if ( ! client . isOpen ) {
client . emit ( "error" , new Error ( "connection isn't open" ) ) ;
2023-06-09 16:26:02 -04:00
}
2023-06-14 11:49:33 -04: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 06:10:07 -05:00
} ) ;
2023-01-05 09:58:24 -05:00
} ) ;
} ;
2022-05-12 05:48:38 -04:00
2022-04-20 14:56:40 -04:00
/ * *
* Retrieve value of setting based on key
* @ param { string } key Key of setting to retrieve
2022-05-06 02:41:34 -04:00
* @ returns { Promise < any > } Value
2022-07-31 11:41:29 -04:00
* @ deprecated Use await Settings . get ( key )
2022-04-20 14:56:40 -04:00
* /
2021-07-09 02:14:03 -04:00
exports . setting = async function ( key ) {
2022-06-29 02:57:40 -04:00
return await Settings . get ( key ) ;
2021-09-20 04:22:18 -04:00
} ;
2021-07-09 02:14:03 -04:00
2022-04-20 14:56:40 -04:00
/ * *
2023-08-11 03:46:41 -04:00
* Sets the specified setting to specified value
2022-04-20 14:56:40 -04:00
* @ 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 12:16:13 -04:00
exports . setSetting = async function ( key , value , type = null ) {
2022-06-29 02:57:40 -04:00
await Settings . set ( key , value , type ) ;
2021-09-20 04:22:18 -04:00
} ;
2021-07-21 14:02:35 -04:00
2022-04-20 14:56:40 -04:00
/ * *
* Get settings based on type
2022-06-29 02:57:40 -04:00
* @ param { string } type The type of setting
2023-08-11 03:46:41 -04:00
* @ returns { Promise < Bean > } Settings of requested type
2022-04-20 14:56:40 -04:00
* /
2021-07-09 02:14:03 -04:00
exports . getSettings = async function ( type ) {
2022-06-29 02:57:40 -04:00
return await Settings . getSettings ( type ) ;
2021-09-20 04:22:18 -04:00
} ;
2021-07-21 00:09:09 -04:00
2022-04-20 14:56:40 -04:00
/ * *
* Set settings based on type
2022-06-29 02:57:40 -04:00
* @ param { string } type Type of settings to set
2023-08-11 03:46:41 -04:00
* @ param { object } data Values of settings
2022-04-20 14:56:40 -04:00
* @ returns { Promise < void > }
* /
2021-07-31 09:57:58 -04:00
exports . setSettings = async function ( type , data ) {
2022-06-29 02:57:40 -04:00
await Settings . setSettings ( type , data ) ;
2021-09-20 04:22:18 -04:00
} ;
2021-07-31 09:57:58 -04:00
2021-07-21 00:09:09 -04:00
// ssl-checker by @dyaa
2022-04-20 14:56:40 -04:00
//https://github.com/dyaa/ssl-checker/blob/master/src/index.ts
2021-07-21 00:09:09 -04:00
2022-04-20 14:56:40 -04:00
/ * *
* Get number of days between two dates
* @ param { Date } validFrom Start date
* @ param { Date } validTo End date
2023-08-11 03:46:41 -04:00
* @ returns { number } Number of days
2022-04-20 14:56:40 -04:00
* /
2021-07-21 00:09:09 -04:00
const getDaysBetween = ( validFrom , validTo ) =>
Math . round ( Math . abs ( + validFrom - + validTo ) / 8.64 e7 ) ;
2022-04-20 14:56:40 -04:00
/ * *
* Get days remaining from a time range
* @ param { Date } validFrom Start date
* @ param { Date } validTo End date
2023-08-11 03:46:41 -04:00
* @ returns { number } Number of days remaining
2022-04-20 14:56:40 -04:00
* /
2021-07-21 00:09:09 -04: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 14:56:40 -04:00
/ * *
* Fix certificate info for display
2023-08-11 03:46:41 -04: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 14:56:40 -04:00
* /
2021-10-01 06:44:32 -04:00
const parseCertificateInfo = function ( info ) {
let link = info ;
2021-11-08 02:39:17 -05:00
let i = 0 ;
const existingList = { } ;
2021-10-01 06:44:32 -04:00
while ( link ) {
2022-04-16 02:50:48 -04:00
log . debug ( "cert" , ` [ ${ i } ] ${ link . fingerprint } ` ) ;
2021-11-08 02:39:17 -05:00
2021-10-01 06:44:32 -04: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 02:39:17 -05:00
existingList [ link . fingerprint ] = true ;
2021-10-01 06:44:32 -04:00
// Move up the chain until loop is encountered
if ( link . issuerCertificate == null ) {
2023-01-12 05:34:37 -05:00
link . certType = ( i === 0 ) ? "self-signed" : "root CA" ;
2021-10-01 06:44:32 -04:00
break ;
2021-11-08 02:39:17 -05:00
} else if ( link . issuerCertificate . fingerprint in existingList ) {
2023-01-12 05:34:37 -05: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 02:50:48 -04:00
log . debug ( "cert" , ` [Last] ${ link . issuerCertificate . fingerprint } ` ) ;
2023-01-12 05:34:37 -05:00
link . certType = ( i === 0 ) ? "self-signed" : "root CA" ;
2021-10-01 06:44:32 -04:00
link . issuerCertificate = null ;
break ;
} else {
2023-01-12 05:34:37 -05:00
link . certType = ( i === 0 ) ? "server" : "intermediate CA" ;
2021-10-01 06:44:32 -04:00
link = link . issuerCertificate ;
}
2021-11-08 02:39:17 -05:00
// Should be no use, but just in case.
if ( i > 500 ) {
throw new Error ( "Dead loop occurred in parseCertificateInfo" ) ;
}
i ++ ;
2021-07-21 00:09:09 -04:00
}
2021-10-01 06:44:32 -04:00
return info ;
} ;
2021-07-21 00:09:09 -04:00
2022-04-20 14:56:40 -04:00
/ * *
* Check if certificate is valid
2024-04-06 06:43:08 -04:00
* @ param { tls . TLSSocket } socket TLSSocket , which may or may not be connected
2024-04-25 11:45:30 -04:00
* @ returns { object } Object containing certificate information
2022-04-20 14:56:40 -04:00
* /
2024-04-06 06:43:08 -04: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-12 13:21:12 -05:00
}
2024-04-06 06:43:08 -04:00
const info = socket . getPeerCertificate ( true ) ;
const valid = socket . authorized || false ;
2021-07-21 00:09:09 -04:00
2022-04-16 02:50:48 -04:00
log . debug ( "cert" , "Parsing Certificate Info" ) ;
2021-10-01 06:44:32 -04:00
const parsedInfo = parseCertificateInfo ( info ) ;
2021-07-21 00:09:09 -04:00
2024-04-06 06:43:08 -04:00
if ( process . env . TIMELOGGER === "1" ) {
log . debug ( "monitor" , "Cert Info Query Time: " + ( dayjs ( ) . valueOf ( ) - certInfoStartTime ) + "ms" ) ;
}
2021-07-21 00:09:09 -04:00
return {
2021-10-01 06:44:32 -04:00
valid : valid ,
certInfo : parsedInfo
2021-07-21 00:09:09 -04:00
} ;
2021-09-20 04:22:18 -04:00
} ;
2021-08-05 07:04:38 -04:00
2022-04-20 14:56:40 -04:00
/ * *
* Check if the provided status code is within the accepted ranges
2022-07-18 10:06:25 -04:00
* @ param { number } status The status code to check
2022-04-21 15:02:18 -04:00
* @ param { string [ ] } acceptedCodes An array of accepted status codes
2022-04-20 14:56:40 -04:00
* @ returns { boolean } True if status code within range , false otherwise
* /
2022-04-16 13:39:49 -04:00
exports . checkStatusCode = function ( status , acceptedCodes ) {
if ( acceptedCodes == null || acceptedCodes . length === 0 ) {
2021-08-05 07:04:38 -04:00
return false ;
}
2022-04-16 13:39:49 -04:00
for ( const codeRange of acceptedCodes ) {
2023-08-07 15:22:32 -04:00
if ( typeof codeRange !== "string" ) {
log . error ( "monitor" , ` Accepted status code not a string. ${ codeRange } is of type ${ typeof codeRange } ` ) ;
continue ;
}
2022-04-16 13:39:49 -04:00
const codeRangeSplit = codeRange . split ( "-" ) . map ( string => parseInt ( string ) ) ;
if ( codeRangeSplit . length === 1 ) {
if ( status === codeRangeSplit [ 0 ] ) {
2021-08-05 07:04:38 -04:00
return true ;
}
2022-04-16 13:39:49 -04:00
} else if ( codeRangeSplit . length === 2 ) {
if ( status >= codeRangeSplit [ 0 ] && status <= codeRangeSplit [ 1 ] ) {
2021-08-05 07:04:38 -04:00
return true ;
}
} else {
2023-08-07 15:22:32 -04:00
log . error ( "monitor" , ` ${ codeRange } is not a valid status code range ` ) ;
continue ;
2021-08-05 07:04:38 -04:00
}
}
return false ;
2021-09-20 04:22:18 -04:00
} ;
2021-08-30 02:55:33 -04:00
2022-04-20 14:56:40 -04: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 03:46:41 -04:00
* @ returns { number } Total clients in room
2022-04-20 14:56:40 -04:00
* /
2021-08-30 02:55:33 -04:00
exports . getTotalClientInRoom = ( io , roomName ) => {
const sockets = io . sockets ;
2021-11-03 21:46:43 -04:00
if ( ! sockets ) {
2021-08-30 02:55:33 -04:00
return 0 ;
}
const adapter = sockets . adapter ;
2021-11-03 21:46:43 -04:00
if ( ! adapter ) {
2021-08-30 02:55:33 -04:00
return 0 ;
}
const room = adapter . rooms . get ( roomName ) ;
if ( room ) {
return room . size ;
} else {
return 0 ;
}
2021-09-20 04:22:18 -04:00
} ;
2021-09-11 07:40:03 -04:00
2022-04-20 14:56:40 -04:00
/ * *
* Allow CORS all origins if development
2023-08-11 03:46:41 -04:00
* @ param { object } res Response object from axios
* @ returns { void }
2022-04-20 14:56:40 -04:00
* /
2021-09-11 07:40:03 -04:00
exports . allowDevAllOrigin = ( res ) => {
if ( process . env . NODE _ENV === "development" ) {
exports . allowAllOrigin ( res ) ;
}
2021-09-20 04:22:18 -04:00
} ;
2021-09-11 07:40:03 -04:00
2022-04-20 14:56:40 -04:00
/ * *
* Allow CORS all origins
2023-08-11 03:46:41 -04:00
* @ param { object } res Response object from axios
* @ returns { void }
2022-04-20 14:56:40 -04:00
* /
2021-09-11 07:40:03 -04:00
exports . allowAllOrigin = ( res ) => {
res . header ( "Access-Control-Allow-Origin" , "*" ) ;
2023-02-11 01:41:02 -05:00
res . header ( "Access-Control-Allow-Methods" , "GET, PUT, POST, DELETE, OPTIONS" ) ;
2021-09-11 07:40:03 -04:00
res . header ( "Access-Control-Allow-Headers" , "Origin, X-Requested-With, Content-Type, Accept" ) ;
2021-09-20 04:22:18 -04:00
} ;
2021-09-16 10:48:28 -04:00
2022-04-20 14:56:40 -04:00
/ * *
* Check if a user is logged in
* @ param { Socket } socket Socket instance
2023-08-11 03:46:41 -04:00
* @ returns { void }
* @ throws The user is not logged in
2022-04-20 14:56:40 -04:00
* /
2021-09-16 10:48:28 -04:00
exports . checkLogin = ( socket ) => {
2021-11-03 21:46:43 -04:00
if ( ! socket . userID ) {
2021-09-16 10:48:28 -04:00
throw new Error ( "You are not logged in." ) ;
}
2021-09-20 04:22:18 -04:00
} ;
2021-10-05 07:13:57 -04:00
2022-03-29 05:38:48 -04:00
/ * *
* For logged - in users , double - check the password
2022-04-21 08:01:22 -04:00
* @ param { Socket } socket Socket . io instance
2023-08-11 03:46:41 -04: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 05:38:48 -04:00
* /
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 ;
} ;
2021-10-13 12:22:49 -04:00
/ * *
2022-04-20 14:56:40 -04:00
* Convert unknown string to UTF8
* @ param { Uint8Array } body Buffer
2023-08-11 03:46:41 -04:00
* @ returns { string } UTF8 string
2021-10-13 12:22:49 -04:00
* /
exports . convertToUTF8 = ( body ) => {
const guessEncoding = chardet . detect ( body ) ;
const str = iconv . decode ( body , guessEncoding ) ;
return str . toString ( ) ;
} ;
2021-10-29 06:24:47 -04:00
2022-01-03 10:04:37 -05:00
/ * *
* Returns a color code in hex format based on a given percentage :
* 0 % => hue = 10 => red
* 100 % => hue = 90 => green
2022-04-30 09:36:00 -04:00
* @ param { number } percentage float , 0 to 1
2023-08-11 03:46:41 -04:00
* @ param { number } maxHue Maximum hue - int
* @ param { number } minHue Minimum hue - int
* @ returns { string } Color in hex
2022-01-03 10:04:37 -05:00
* /
2022-01-03 09:48:52 -05: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 06:21:53 -05:00
return badgeConstants . naColor ;
2022-01-03 09:48:52 -05:00
}
} ;
2022-01-04 10:00:21 -05:00
/ * *
* Joins and array of string to one string after filtering out empty values
2023-08-11 03:46:41 -04:00
* @ param { string [ ] } parts Strings to join
* @ param { string } connector Separator for joined strings
* @ returns { string } Joined strings
2022-01-04 10:00:21 -05:00
* /
exports . filterAndJoin = ( parts , connector = "" ) => {
return parts . filter ( ( part ) => ! ! part && part !== "" ) . join ( connector ) ;
} ;
2022-06-01 01:05:12 -04:00
/ * *
2023-02-09 04:42:02 -05:00
* Send an Error response
2023-08-11 03:46:41 -04:00
* @ param { object } res Express response object
* @ param { string } msg Message to send
* @ returns { void }
2022-06-01 01:05:12 -04:00
* /
2023-02-09 04:42:02 -05: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 01:05:12 -04:00
} ;
2022-08-03 01:00:39 -04:00
2023-08-11 03:46:41 -04: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 07:38:28 -04:00
function timeObjectConvertTimezone ( obj , timezone , timeObjectToUTC = true ) {
2022-10-10 08:48:11 -04:00
let offsetString ;
if ( timezone ) {
offsetString = dayjs ( ) . tz ( timezone ) . format ( "Z" ) ;
} else {
offsetString = dayjs ( ) . format ( "Z" ) ;
}
2022-09-25 07:38:28 -04: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 05:02:16 -04:00
if ( obj . minutes < 0 ) {
obj . minutes += 60 ;
obj . hours -- ;
} else if ( obj . minutes > 60 ) {
obj . minutes -= 60 ;
obj . hours ++ ;
}
2022-09-25 07:38:28 -04:00
if ( obj . hours < 0 ) {
obj . hours += 24 ;
} else if ( obj . hours > 24 ) {
obj . hours -= 24 ;
}
return obj ;
}
2022-10-10 08:48:11 -04:00
/ * *
2023-08-11 03:46:41 -04: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 08:48:11 -04:00
* /
module . exports . timeObjectToUTC = ( obj , timezone = undefined ) => {
2022-09-25 07:38:28 -04:00
return timeObjectConvertTimezone ( obj , timezone , true ) ;
} ;
2022-10-10 08:48:11 -04:00
/ * *
2023-08-11 03:46:41 -04: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 08:48:11 -04:00
* /
module . exports . timeObjectToLocal = ( obj , timezone = undefined ) => {
2022-09-25 07:38:28 -04:00
return timeObjectConvertTimezone ( obj , timezone , false ) ;
} ;
2022-10-26 09:41:21 -04:00
2022-08-03 01:00:39 -04:00
/ * *
* Create gRPC client stib
2023-08-11 03:46:41 -04:00
* @ param { object } options from gRPC client
* @ returns { Promise < object > } Result of gRPC query
2022-08-03 01:00:39 -04:00
* /
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-28 20:10:58 -05: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 01:00:39 -04:00
} ) ;
} ;
2023-08-03 13:10:15 -04:00
2023-10-15 14:20:38 -04:00
/ * *
* 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 ) ;
} ;
2023-10-08 19:01:54 -04:00
module . exports . SHAKE256 _LENGTH = 16 ;
/ * *
2023-10-09 12:39:55 -04: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-08 19:01:54 -04:00
* /
module . exports . shake256 = ( data , len ) => {
if ( ! data ) {
return "" ;
}
return crypto . createHash ( "shake256" , { outputLength : len } )
. update ( data )
. digest ( "hex" ) ;
} ;
2023-10-12 09:26:11 -04: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 08:09:56 -04:00
2023-08-03 13:10:15 -04: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-10-31 22:10:48 -04: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 00:50:51 -05:00
// Just in case, as 0 timeout here will cause the request to be aborted immediately
if ( ! timeoutMs || timeoutMs <= 0 ) {
timeoutMs = 5000 ;
}
2023-10-31 22:10:48 -04:00
return AbortSignal . timeout ( timeoutMs ) ;
} catch ( _ ) {
// v16-: AbortSignal.timeout is not supported
try {
const abortController = new AbortController ( ) ;
2023-11-12 00:50:51 -05:00
setTimeout ( ( ) => abortController . abort ( ) , timeoutMs ) ;
2023-10-31 22:10:48 -04:00
return abortController . signal ;
} catch ( _ ) {
// v15-: AbortController is not supported
return null ;
}
}
} ;