2022-04-19 15:38:59 +08:00
const express = require ( "express" ) ;
const https = require ( "https" ) ;
const fs = require ( "fs" ) ;
const http = require ( "http" ) ;
const { Server } = require ( "socket.io" ) ;
const { R } = require ( "redbean-node" ) ;
2022-04-19 16:46:45 +08:00
const { log } = require ( "../src/util" ) ;
2022-05-06 14:41:34 +08:00
const Database = require ( "./database" ) ;
const util = require ( "util" ) ;
2022-07-18 22:33:35 +08:00
const { CacheableDnsHttpAgent } = require ( "./cacheable-dns-http-agent" ) ;
2022-07-31 23:41:29 +08:00
const { Settings } = require ( "./settings" ) ;
2022-10-09 20:59:58 +08:00
const dayjs = require ( "dayjs" ) ;
2023-07-24 17:04:50 +08:00
const childProcess = require ( "child_process" ) ;
2023-08-03 20:54:11 +08:00
const path = require ( "path" ) ;
2023-10-10 03:15:10 +08:00
const { FBSD } = require ( "./util-server" ) ;
2023-07-15 21:27:39 +08:00
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead.
2022-04-19 15:38:59 +08:00
/ * *
* ` module.exports ` ( alias : ` server ` ) should be inside this class , in order to avoid circular dependency issue .
* @ type { UptimeKumaServer }
* /
class UptimeKumaServer {
/ * *
2023-08-11 09:46:41 +02:00
* Current server instance
2022-04-19 15:38:59 +08:00
* @ type { UptimeKumaServer }
* /
static instance = null ;
/ * *
* Main monitor list
* @ type { { } }
* /
monitorList = { } ;
2022-09-17 16:54:21 +08:00
/ * *
* Main maintenance list
* @ type { { } }
* /
maintenanceList = { } ;
2022-04-19 15:38:59 +08:00
entryPage = "dashboard" ;
app = undefined ;
httpServer = undefined ;
io = undefined ;
2022-05-30 15:45:44 +08:00
/ * *
* Cache Index HTML
* @ type { string }
* /
indexHTML = "" ;
2023-01-27 18:25:57 +08:00
/ * *
* @ type { { } }
* /
static monitorTypeList = {
} ;
2023-06-27 15:54:33 +08:00
/ * *
* Use for decode the auth object
* @ type { null }
* /
jwtSecret = null ;
2023-10-10 03:15:10 +08:00
/ * *
* Port
* @ type { number }
* /
port = undefined ;
/ * *
* Hostname
* @ type { string | undefined }
* /
hostname = undefined ;
/ * *
* Is SSL enabled ?
* /
isHTTPS = false ;
2023-08-11 09:46:41 +02:00
/ * *
* Get the current instance of the server if it exists , otherwise
* create a new instance .
* @ param { object } args Arguments to pass to instance constructor
* @ returns { UptimeKumaServer } Server instance
* /
2022-04-19 15:38:59 +08:00
static getInstance ( args ) {
if ( UptimeKumaServer . instance == null ) {
UptimeKumaServer . instance = new UptimeKumaServer ( args ) ;
}
return UptimeKumaServer . instance ;
}
2023-08-11 09:46:41 +02:00
/ * *
* @ param { object } args Arguments to initialise server with
* /
2022-04-19 15:38:59 +08:00
constructor ( args ) {
2023-10-10 03:15:10 +08:00
// Port
this . port = [ args . port , process . env . UPTIME _KUMA _PORT , process . env . PORT , 3001 ]
. map ( portValue => parseInt ( portValue ) )
. find ( portValue => ! isNaN ( portValue ) ) ;
// Hostname
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
// Dual-stack support for (::)
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
let hostEnv = FBSD ? null : process . env . HOST ;
this . hostname = args . host || process . env . UPTIME _KUMA _HOST || hostEnv ;
if ( this . hostname ) {
log . info ( "server" , "Custom hostname: " + this . hostname ) ;
}
2022-04-19 15:38:59 +08:00
// SSL
2022-04-19 16:46:45 +08:00
const sslKey = args [ "ssl-key" ] || process . env . UPTIME _KUMA _SSL _KEY || process . env . SSL _KEY || undefined ;
const sslCert = args [ "ssl-cert" ] || process . env . UPTIME _KUMA _SSL _CERT || process . env . SSL _CERT || undefined ;
2023-03-09 00:00:07 +08:00
const sslKeyPassphrase = args [ "ssl-key-passphrase" ] || process . env . UPTIME _KUMA _SSL _KEY _PASSPHRASE || process . env . SSL _KEY _PASSPHRASE || undefined ;
2022-04-19 15:38:59 +08:00
2022-04-19 16:46:45 +08:00
log . info ( "server" , "Creating express and socket.io instance" ) ;
2022-04-19 15:38:59 +08:00
this . app = express ( ) ;
if ( sslKey && sslCert ) {
2022-04-19 16:46:45 +08:00
log . info ( "server" , "Server Type: HTTPS" ) ;
2023-10-10 03:15:10 +08:00
this . isHTTPS = true ;
2022-04-19 15:38:59 +08:00
this . httpServer = https . createServer ( {
key : fs . readFileSync ( sslKey ) ,
2023-03-09 00:00:07 +08:00
cert : fs . readFileSync ( sslCert ) ,
passphrase : sslKeyPassphrase ,
2022-04-19 15:38:59 +08:00
} , this . app ) ;
} else {
2022-04-19 16:46:45 +08:00
log . info ( "server" , "Server Type: HTTP" ) ;
2023-10-10 03:15:10 +08:00
this . isHTTPS = false ;
2022-04-19 15:38:59 +08:00
this . httpServer = http . createServer ( this . app ) ;
}
2022-05-30 15:45:44 +08:00
try {
this . indexHTML = fs . readFileSync ( "./dist/index.html" ) . toString ( ) ;
} catch ( e ) {
// "dist/index.html" is not necessary for development
if ( process . env . NODE _ENV !== "development" ) {
log . error ( "server" , "Error: Cannot find 'dist/index.html', did you install correctly?" ) ;
process . exit ( 1 ) ;
}
}
2023-06-27 15:54:33 +08:00
// Set Monitor Types
UptimeKumaServer . monitorTypeList [ "real-browser" ] = new RealBrowserMonitorType ( ) ;
2023-07-19 20:58:21 +08:00
UptimeKumaServer . monitorTypeList [ "tailscale-ping" ] = new TailscalePing ( ) ;
2023-09-09 12:14:55 +02:00
UptimeKumaServer . monitorTypeList [ "dns" ] = new DnsMonitorType ( ) ;
2023-06-27 15:54:33 +08:00
2022-04-19 15:38:59 +08:00
this . io = new Server ( this . httpServer ) ;
}
2023-08-11 09:46:41 +02:00
/ * *
* Initialise app after the database has been set up
* @ returns { Promise < void > }
* /
2022-10-09 20:59:58 +08:00
async initAfterDatabaseReady ( ) {
2023-06-27 15:54:33 +08:00
// Static
this . app . use ( "/screenshots" , express . static ( Database . screenshotDir ) ) ;
2022-12-12 17:19:22 +08:00
await CacheableDnsHttpAgent . update ( ) ;
2022-10-09 20:59:58 +08:00
process . env . TZ = await this . getTimezone ( ) ;
dayjs . tz . setDefault ( process . env . TZ ) ;
log . debug ( "DEBUG" , "Timezone: " + process . env . TZ ) ;
log . debug ( "DEBUG" , "Current Time: " + dayjs . tz ( ) . format ( ) ) ;
2022-10-15 17:17:26 +08:00
2023-03-31 04:04:17 +08:00
await this . loadMaintenanceList ( ) ;
2022-10-09 20:59:58 +08:00
}
2023-01-05 22:19:05 +00:00
/ * *
* Send list of monitors to client
2023-08-11 09:46:41 +02:00
* @ param { Socket } socket Socket to send list on
* @ returns { object } List of monitors
2023-01-05 22:19:05 +00:00
* /
2022-04-19 15:38:59 +08:00
async sendMonitorList ( socket ) {
let list = await this . getMonitorJSONList ( socket . userID ) ;
this . io . to ( socket . userID ) . emit ( "monitorList" , list ) ;
return list ;
}
2022-04-19 16:46:45 +08:00
/ * *
* Get a list of monitors for the given user .
* @ param { string } userID - The ID of the user to get monitors for .
2023-08-11 09:46:41 +02:00
* @ returns { Promise < object > } A promise that resolves to an object with monitor IDs as keys and monitor objects as values .
2022-04-19 16:46:45 +08:00
*
* Generated by Trelent
* /
2022-04-19 15:38:59 +08:00
async getMonitorJSONList ( userID ) {
let result = { } ;
let monitorList = await R . find ( "monitor" , " user_id = ? ORDER BY weight DESC, name" , [
userID ,
] ) ;
for ( let monitor of monitorList ) {
result [ monitor . id ] = await monitor . toJSON ( ) ;
}
return result ;
}
2022-05-06 14:41:34 +08:00
2022-09-17 16:54:21 +08:00
/ * *
* Send maintenance list to client
* @ param { Socket } socket Socket . io instance to send to
2023-08-11 09:46:41 +02:00
* @ returns { object } Maintenance list
2022-09-17 16:54:21 +08:00
* /
async sendMaintenanceList ( socket ) {
2022-10-15 20:15:50 +08:00
return await this . sendMaintenanceListByUserID ( socket . userID ) ;
}
2023-01-05 22:19:05 +00:00
/ * *
* Send list of maintenances to user
2023-08-11 09:46:41 +02:00
* @ param { number } userID User to send list to
* @ returns { object } Maintenance list
2023-01-05 22:19:05 +00:00
* /
2022-10-15 20:15:50 +08:00
async sendMaintenanceListByUserID ( userID ) {
let list = await this . getMaintenanceJSONList ( userID ) ;
this . io . to ( userID ) . emit ( "maintenanceList" , list ) ;
2022-09-17 16:54:21 +08:00
return list ;
}
/ * *
* Get a list of maintenances for the given user .
* @ param { string } userID - The ID of the user to get maintenances for .
2023-08-11 09:46:41 +02:00
* @ returns { Promise < object > } A promise that resolves to an object with maintenance IDs as keys and maintenances objects as values .
2022-09-17 16:54:21 +08:00
* /
async getMaintenanceJSONList ( userID ) {
let result = { } ;
2023-03-31 04:04:17 +08:00
for ( let maintenanceID in this . maintenanceList ) {
result [ maintenanceID ] = await this . maintenanceList [ maintenanceID ] . toJSON ( ) ;
}
return result ;
}
/ * *
* Load maintenance list and run
2023-08-11 09:46:41 +02:00
* @ param { any } userID Unused
2023-03-31 04:04:17 +08:00
* @ returns { Promise < void > }
* /
async loadMaintenanceList ( userID ) {
let maintenanceList = await R . findAll ( "maintenance" , " ORDER BY end_date DESC, title" , [
2022-09-17 16:54:21 +08:00
] ) ;
for ( let maintenance of maintenanceList ) {
2023-03-31 04:04:17 +08:00
this . maintenanceList [ maintenance . id ] = maintenance ;
maintenance . run ( this ) ;
2022-09-17 16:54:21 +08:00
}
2023-03-31 04:04:17 +08:00
}
2022-09-17 16:54:21 +08:00
2023-08-11 09:46:41 +02:00
/ * *
* Retrieve a specific maintenance
* @ param { number } maintenanceID ID of maintenance to retrieve
* @ returns { ( object | null ) } Maintenance if it exists
* /
2023-03-31 04:04:17 +08:00
getMaintenance ( maintenanceID ) {
if ( this . maintenanceList [ maintenanceID ] ) {
return this . maintenanceList [ maintenanceID ] ;
}
return null ;
2022-09-17 16:54:21 +08:00
}
2022-05-06 14:41:34 +08:00
/ * *
* Write error to log file
* @ param { any } error The error to write
* @ param { boolean } outputToConsole Should the error also be output to console ?
2023-08-11 09:46:41 +02:00
* @ returns { void }
2022-05-06 14:41:34 +08:00
* /
static errorLog ( error , outputToConsole = true ) {
2023-08-03 20:54:11 +08:00
const errorLogStream = fs . createWriteStream ( path . join ( Database . dataDir , "/error.log" ) , {
2022-05-06 14:41:34 +08:00
flags : "a"
} ) ;
errorLogStream . on ( "error" , ( ) => {
log . info ( "" , "Cannot write to error.log" ) ;
} ) ;
if ( errorLogStream ) {
const dateTime = R . isoDateTime ( ) ;
errorLogStream . write ( ` [ ${ dateTime } ] ` + util . format ( error ) + "\n" ) ;
if ( outputToConsole ) {
console . error ( error ) ;
}
}
errorLogStream . end ( ) ;
}
2022-07-31 23:36:33 +08:00
2023-01-05 22:19:05 +00:00
/ * *
* Get the IP of the client connected to the socket
2023-08-11 09:46:41 +02:00
* @ param { Socket } socket Socket to query
* @ returns { string } IP of client
2023-01-05 22:19:05 +00:00
* /
2022-07-31 23:36:33 +08:00
async getClientIP ( socket ) {
2022-08-01 15:42:58 +08:00
let clientIP = socket . client . conn . remoteAddress ;
if ( clientIP === undefined ) {
clientIP = "" ;
}
2022-07-31 23:36:33 +08:00
2022-07-31 23:41:29 +08:00
if ( await Settings . get ( "trustProxy" ) ) {
2022-10-05 16:45:21 +01:00
const forwardedFor = socket . client . conn . request . headers [ "x-forwarded-for" ] ;
return ( typeof forwardedFor === "string" ? forwardedFor . split ( "," ) [ 0 ] . trim ( ) : null )
2022-07-31 23:36:33 +08:00
|| socket . client . conn . request . headers [ "x-real-ip" ]
2023-07-07 23:38:10 +02:00
|| clientIP . replace ( /^::ffff:/ , "" ) ;
2022-07-31 23:36:33 +08:00
} else {
2023-07-07 23:38:10 +02:00
return clientIP . replace ( /^::ffff:/ , "" ) ;
2022-07-31 23:36:33 +08:00
}
}
2022-10-09 20:59:58 +08:00
2023-01-05 22:19:05 +00:00
/ * *
* Attempt to get the current server timezone
* If this fails , fall back to environment variables and then make a
* guess .
2023-08-11 09:46:41 +02:00
* @ returns { Promise < string > } Current timezone
2023-01-05 22:19:05 +00:00
* /
2022-10-09 20:59:58 +08:00
async getTimezone ( ) {
2023-07-15 23:23:27 +08:00
// From process.env.TZ
try {
if ( process . env . TZ ) {
this . checkTimezone ( process . env . TZ ) ;
return process . env . TZ ;
}
} catch ( e ) {
log . warn ( "timezone" , e . message + " in process.env.TZ" ) ;
}
2022-10-09 20:59:58 +08:00
let timezone = await Settings . get ( "serverTimezone" ) ;
2023-07-15 23:23:27 +08:00
// From Settings
try {
log . debug ( "timezone" , "Using timezone from settings: " + timezone ) ;
if ( timezone ) {
this . checkTimezone ( timezone ) ;
return timezone ;
}
} catch ( e ) {
log . warn ( "timezone" , e . message + " in settings" ) ;
}
// Guess
try {
let guess = dayjs . tz . guess ( ) ;
log . debug ( "timezone" , "Guessing timezone: " + guess ) ;
if ( guess ) {
this . checkTimezone ( guess ) ;
return guess ;
} else {
return "UTC" ;
}
} catch ( e ) {
// Guess failed, fall back to UTC
log . debug ( "timezone" , "Guessed an invalid timezone. Use UTC as fallback" ) ;
return "UTC" ;
2022-10-09 20:59:58 +08:00
}
}
2023-01-05 22:19:05 +00:00
/ * *
* Get the current offset
2023-08-11 09:46:41 +02:00
* @ returns { string } Time offset
2023-01-05 22:19:05 +00:00
* /
2022-10-11 21:48:43 +08:00
getTimezoneOffset ( ) {
2022-10-11 18:23:17 +08:00
return dayjs ( ) . format ( "Z" ) ;
}
2023-07-15 23:23:27 +08:00
/ * *
* Throw an error if the timezone is invalid
2023-08-11 09:46:41 +02:00
* @ param { string } timezone Timezone to test
* @ returns { void }
* @ throws The timezone is invalid
2023-07-15 23:23:27 +08:00
* /
checkTimezone ( timezone ) {
try {
dayjs . utc ( "2013-11-18 11:55" ) . tz ( timezone ) . format ( ) ;
} catch ( e ) {
throw new Error ( "Invalid timezone:" + timezone ) ;
}
}
2023-01-05 22:19:05 +00:00
/ * *
* Set the current server timezone and environment variables
2023-08-11 09:46:41 +02:00
* @ param { string } timezone Timezone to set
* @ returns { Promise < void > }
2023-01-05 22:19:05 +00:00
* /
2022-10-09 20:59:58 +08:00
async setTimezone ( timezone ) {
2023-07-15 23:23:27 +08:00
this . checkTimezone ( timezone ) ;
2022-10-09 20:59:58 +08:00
await Settings . set ( "serverTimezone" , timezone , "general" ) ;
process . env . TZ = timezone ;
dayjs . tz . setDefault ( timezone ) ;
}
2022-10-15 17:17:26 +08:00
2023-07-24 17:04:50 +08:00
/ * *
* TODO : Listen logic should be moved to here
* @ returns { Promise < void > }
* /
async start ( ) {
2023-08-28 16:15:48 +08:00
let enable = await Settings . get ( "nscd" ) ;
if ( enable || enable === null ) {
this . startNSCDServices ( ) ;
}
2023-07-24 17:04:50 +08:00
}
/ * *
* Stop the server
* @ returns { Promise < void > }
* /
2022-10-15 17:17:26 +08:00
async stop ( ) {
2023-08-28 16:15:48 +08:00
let enable = await Settings . get ( "nscd" ) ;
if ( enable || enable === null ) {
this . stopNSCDServices ( ) ;
}
2023-07-24 17:04:50 +08:00
}
2023-03-31 04:04:17 +08:00
2023-07-24 17:04:50 +08:00
/ * *
* Start all system services ( e . g . nscd )
* For now , only used in Docker
2023-08-11 09:46:41 +02:00
* @ returns { void }
2023-07-24 17:04:50 +08:00
* /
2023-08-28 16:15:48 +08:00
startNSCDServices ( ) {
2023-07-24 17:04:50 +08:00
if ( process . env . UPTIME _KUMA _IS _CONTAINER ) {
try {
log . info ( "services" , "Starting nscd" ) ;
childProcess . execSync ( "sudo service nscd start" , { stdio : "pipe" } ) ;
} catch ( e ) {
log . info ( "services" , "Failed to start nscd" ) ;
}
}
}
/ * *
* Stop all system services
2023-08-11 09:46:41 +02:00
* @ returns { void }
2023-07-24 17:04:50 +08:00
* /
2023-08-28 16:15:48 +08:00
stopNSCDServices ( ) {
2023-07-24 17:04:50 +08:00
if ( process . env . UPTIME _KUMA _IS _CONTAINER ) {
try {
log . info ( "services" , "Stopping nscd" ) ;
childProcess . execSync ( "sudo service nscd stop" ) ;
} catch ( e ) {
log . info ( "services" , "Failed to stop nscd" ) ;
}
}
2022-10-15 17:17:26 +08:00
}
2022-04-19 15:38:59 +08:00
}
module . exports = {
UptimeKumaServer
} ;
2022-10-15 17:17:26 +08:00
2023-07-15 21:27:39 +08:00
// Must be at the end to avoid circular dependencies
2023-06-27 15:54:33 +08:00
const { RealBrowserMonitorType } = require ( "./monitor-types/real-browser-monitor-type" ) ;
2023-07-19 20:58:21 +08:00
const { TailscalePing } = require ( "./monitor-types/tailscale-ping" ) ;
2023-09-09 12:14:55 +02:00
const { DnsMonitorType } = require ( "./monitor-types/dns" ) ;