mirror of
https://github.com/louislam/dockge.git
synced 2024-11-25 09:43:25 +01:00
382 lines
11 KiB
TypeScript
382 lines
11 KiB
TypeScript
import { MainRouter } from "./routers/main-router";
|
|
import * as fs from "node:fs";
|
|
import { PackageJson } from "type-fest";
|
|
import { Database } from "./database";
|
|
import packageJSON from "../package.json";
|
|
import { log } from "./log";
|
|
import * as socketIO from "socket.io";
|
|
import express, { Express } from "express";
|
|
import { parse } from "ts-command-line-args";
|
|
import https from "https";
|
|
import http from "http";
|
|
import { Router } from "./router";
|
|
import { Socket } from "socket.io";
|
|
import { MainSocketHandler } from "./socket-handlers/main-socket-handler";
|
|
import { SocketHandler } from "./socket-handler";
|
|
import { Settings } from "./settings";
|
|
import checkVersion from "./check-version";
|
|
import dayjs from "dayjs";
|
|
import { R } from "redbean-node";
|
|
import { genSecret, isDev } from "./util-common";
|
|
import { generatePasswordHash } from "./password-hash";
|
|
import { Bean } from "redbean-node/dist/bean";
|
|
import { DockgeSocket } from "./util-server";
|
|
import { DockerSocketHandler } from "./socket-handlers/docker-socket-handler";
|
|
import { Terminal } from "./terminal";
|
|
|
|
export interface Arguments {
|
|
sslKey? : string;
|
|
sslCert? : string;
|
|
sslKeyPassphrase? : string;
|
|
port? : number;
|
|
hostname? : string;
|
|
dataDir? : string;
|
|
}
|
|
|
|
export class DockgeServer {
|
|
app : Express;
|
|
httpServer : http.Server;
|
|
packageJSON : PackageJson;
|
|
io : socketIO.Server;
|
|
config : Arguments;
|
|
indexHTML : string;
|
|
terminal : Terminal;
|
|
|
|
/**
|
|
* List of express routers
|
|
*/
|
|
routerList : Router[] = [
|
|
new MainRouter(),
|
|
];
|
|
|
|
/**
|
|
* List of socket handlers
|
|
*/
|
|
socketHandlerList : SocketHandler[] = [
|
|
new MainSocketHandler(),
|
|
new DockerSocketHandler(),
|
|
];
|
|
|
|
/**
|
|
* Show Setup Page
|
|
*/
|
|
needSetup = false;
|
|
|
|
jwtSecret? : string;
|
|
|
|
/**
|
|
*
|
|
*/
|
|
constructor() {
|
|
if (!process.env.NODE_ENV) {
|
|
process.env.NODE_ENV = "production";
|
|
}
|
|
|
|
// Log NODE ENV
|
|
log.info("server", "NODE_ENV: " + process.env.NODE_ENV);
|
|
|
|
// Load arguments
|
|
const args = this.config = parse<Arguments>({
|
|
sslKey: {
|
|
type: String,
|
|
optional: true,
|
|
},
|
|
sslCert: {
|
|
type: String,
|
|
optional: true,
|
|
},
|
|
sslKeyPassphrase: {
|
|
type: String,
|
|
optional: true,
|
|
},
|
|
port: {
|
|
type: Number,
|
|
optional: true,
|
|
},
|
|
hostname: {
|
|
type: String,
|
|
optional: true,
|
|
},
|
|
dataDir: {
|
|
type: String,
|
|
optional: true,
|
|
}
|
|
});
|
|
|
|
// Load from environment variables or default values if args are not set
|
|
args.sslKey = args.sslKey || process.env.DOCKGE_SSL_KEY || undefined;
|
|
args.sslCert = args.sslCert || process.env.DOCKGE_SSL_CERT || undefined;
|
|
args.sslKeyPassphrase = args.sslKeyPassphrase || process.env.DOCKGE_SSL_KEY_PASSPHRASE || undefined;
|
|
args.port = args.port || parseInt(process.env.DOCKGE_PORT) || 5001;
|
|
args.hostname = args.hostname || process.env.DOCKGE_HOSTNAME || undefined;
|
|
args.dataDir = args.dataDir || process.env.DOCKGE_DATA_DIR || "./data/";
|
|
|
|
log.debug("server", args);
|
|
|
|
this.packageJSON = packageJSON as PackageJson;
|
|
|
|
this.initDataDir();
|
|
|
|
this.terminal = new Terminal(this);
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
async serve() {
|
|
// Connect to database
|
|
try {
|
|
await Database.init(this);
|
|
} catch (e) {
|
|
log.error("server", "Failed to prepare your database: " + e.message);
|
|
process.exit(1);
|
|
}
|
|
|
|
// First time setup if needed
|
|
let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [
|
|
"jwtSecret",
|
|
]);
|
|
|
|
if (! jwtSecretBean) {
|
|
log.info("server", "JWT secret is not found, generate one.");
|
|
jwtSecretBean = await this.initJWTSecret();
|
|
log.info("server", "Stored JWT secret into database");
|
|
} else {
|
|
log.debug("server", "Load JWT secret from database.");
|
|
}
|
|
|
|
this.jwtSecret = jwtSecretBean.value;
|
|
|
|
const userCount = (await R.knex("user").count("id as count").first()).count;
|
|
|
|
log.debug("server", "User count: " + userCount);
|
|
|
|
// If there is no record in user table, it is a new Dockge instance, need to setup
|
|
if (userCount == 0) {
|
|
log.info("server", "No user, need setup");
|
|
this.needSetup = true;
|
|
}
|
|
|
|
// Create express
|
|
this.app = express();
|
|
|
|
if (this.config.sslKey && this.config.sslCert) {
|
|
log.info("server", "Server Type: HTTPS");
|
|
this.httpServer = https.createServer({
|
|
key: fs.readFileSync(this.config.sslKey),
|
|
cert: fs.readFileSync(this.config.sslCert),
|
|
passphrase: this.config.sslKeyPassphrase,
|
|
}, this.app);
|
|
} else {
|
|
log.info("server", "Server Type: HTTP");
|
|
this.httpServer = http.createServer(this.app);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
for (const router of this.routerList) {
|
|
this.app.use(router.create(this.app, this));
|
|
}
|
|
|
|
let cors = undefined;
|
|
|
|
if (isDev) {
|
|
cors = {
|
|
origin: "*",
|
|
};
|
|
}
|
|
|
|
// Create Socket.io
|
|
this.io = new socketIO.Server(this.httpServer, {
|
|
cors,
|
|
});
|
|
|
|
this.io.on("connection", (socket: Socket) => {
|
|
log.info("server", "Socket connected!");
|
|
|
|
this.sendInfo(socket, true);
|
|
|
|
if (this.needSetup) {
|
|
log.info("server", "Redirect to setup page");
|
|
socket.emit("setup");
|
|
}
|
|
|
|
// Create socket handlers
|
|
for (const socketHandler of this.socketHandlerList) {
|
|
socketHandler.create(socket as DockgeSocket, this);
|
|
}
|
|
});
|
|
|
|
// Listen
|
|
this.httpServer.listen(5001, this.config.hostname, () => {
|
|
if (this.config.hostname) {
|
|
log.info( "server", `Listening on ${this.config.hostname}:${this.config.port}`);
|
|
} else {
|
|
log.info("server", `Listening on ${this.config.port}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Emits the version information to the client.
|
|
* @param socket Socket.io socket instance
|
|
* @param hideVersion Should we hide the version information in the response?
|
|
* @returns
|
|
*/
|
|
async sendInfo(socket : Socket, hideVersion = false) {
|
|
let versionProperty;
|
|
let latestVersionProperty;
|
|
let isContainer;
|
|
|
|
if (!hideVersion) {
|
|
versionProperty = packageJSON.version;
|
|
latestVersionProperty = checkVersion.latestVersion;
|
|
isContainer = (process.env.DOCKGE_IS_CONTAINER === "1");
|
|
}
|
|
|
|
socket.emit("info", {
|
|
versionProperty,
|
|
latestVersionProperty,
|
|
isContainer,
|
|
primaryBaseURL: await Settings.get("primaryBaseURL"),
|
|
serverTimezone: await this.getTimezone(),
|
|
serverTimezoneOffset: this.getTimezoneOffset(),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the IP of the client connected to the socket
|
|
* @param {Socket} socket Socket to query
|
|
* @returns IP of client
|
|
*/
|
|
async getClientIP(socket : Socket) : Promise<string> {
|
|
let clientIP = socket.client.conn.remoteAddress;
|
|
|
|
if (clientIP === undefined) {
|
|
clientIP = "";
|
|
}
|
|
|
|
if (await Settings.get("trustProxy")) {
|
|
const forwardedFor = socket.client.conn.request.headers["x-forwarded-for"];
|
|
|
|
if (typeof forwardedFor === "string") {
|
|
return forwardedFor.split(",")[0].trim();
|
|
} else if (typeof socket.client.conn.request.headers["x-real-ip"] === "string") {
|
|
return socket.client.conn.request.headers["x-real-ip"];
|
|
}
|
|
}
|
|
return clientIP.replace(/^::ffff:/, "");
|
|
}
|
|
|
|
/**
|
|
* Attempt to get the current server timezone
|
|
* If this fails, fall back to environment variables and then make a
|
|
* guess.
|
|
* @returns {Promise<string>} Current timezone
|
|
*/
|
|
async getTimezone() {
|
|
// 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");
|
|
}
|
|
|
|
const timezone = await Settings.get("serverTimezone");
|
|
|
|
// 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 {
|
|
const 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";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the current offset
|
|
* @returns {string} Time offset
|
|
*/
|
|
getTimezoneOffset() {
|
|
return dayjs().format("Z");
|
|
}
|
|
|
|
/**
|
|
* Throw an error if the timezone is invalid
|
|
* @param {string} timezone Timezone to test
|
|
* @returns {void}
|
|
* @throws The timezone is invalid
|
|
*/
|
|
checkTimezone(timezone : string) {
|
|
try {
|
|
dayjs.utc("2013-11-18 11:55").tz(timezone).format();
|
|
} catch (e) {
|
|
throw new Error("Invalid timezone:" + timezone);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize the data directory
|
|
*/
|
|
initDataDir() {
|
|
// Check if a directory
|
|
if (!fs.lstatSync(this.config.dataDir).isDirectory()) {
|
|
throw new Error(`Fatal error: ${this.config.dataDir} is not a directory`);
|
|
}
|
|
|
|
if (! fs.existsSync(this.config.dataDir)) {
|
|
fs.mkdirSync(this.config.dataDir, { recursive: true });
|
|
}
|
|
log.info("server", `Data Dir: ${this.config.dataDir}`);
|
|
}
|
|
|
|
/**
|
|
* Init or reset JWT secret
|
|
* @returns JWT secret
|
|
*/
|
|
async initJWTSecret() : Promise<Bean> {
|
|
let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [
|
|
"jwtSecret",
|
|
]);
|
|
|
|
if (!jwtSecretBean) {
|
|
jwtSecretBean = R.dispense("setting");
|
|
jwtSecretBean.key = "jwtSecret";
|
|
}
|
|
|
|
jwtSecretBean.value = generatePasswordHash(genSecret());
|
|
await R.store(jwtSecretBean);
|
|
return jwtSecretBean;
|
|
}
|
|
}
|