diff --git a/extra/reset-password.js b/extra/reset-password.js index 7f23a83f0..a7cf3e20b 100644 --- a/extra/reset-password.js +++ b/extra/reset-password.js @@ -78,7 +78,6 @@ function disconnectAllSocketClients(username, password) { // Disconnect all socket connections const socket = io(localWebSocketURL, { - transports: [ "websocket" ], reconnection: false, timeout: 5000, }); diff --git a/server/server.js b/server/server.js index 018218ea7..870986d5a 100644 --- a/server/server.js +++ b/server/server.js @@ -54,7 +54,10 @@ if (!process.env.UPTIME_KUMA_WS_ORIGIN_CHECK) { log.info("server", "Node Env: " + process.env.NODE_ENV); log.info("server", "Inside Container: " + (process.env.UPTIME_KUMA_IS_CONTAINER === "1")); -log.info("server", "WebSocket Origin Check: " + process.env.UPTIME_KUMA_WS_ORIGIN_CHECK); + +if (process.env.UPTIME_KUMA_WS_ORIGIN_CHECK === "bypass") { + log.warn("server", "WebSocket Origin Check: " + process.env.UPTIME_KUMA_WS_ORIGIN_CHECK); +} log.info("server", "Importing Node libraries"); const fs = require("fs"); diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js index 556f19d55..d20021909 100644 --- a/server/uptime-kuma-server.js +++ b/server/uptime-kuma-server.js @@ -99,39 +99,63 @@ class UptimeKumaServer { UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType(); UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing(); + // Allow all CORS origins (polling) in development + let cors = undefined; + if (isDev) { + cors = { + origin: "*", + }; + } + this.io = new Server(this.httpServer, { - allowRequest: (req, callback) => { - let isOriginValid = true; - const bypass = isDev || process.env.UPTIME_KUMA_WS_ORIGIN_CHECK === "bypass"; + cors, + allowRequest: async (req, callback) => { + let transport; + // It should be always true, but just in case, because this property is not documented + if (req._query) { + transport = req._query.transport; + } else { + log.error("socket", "Ops!!! Cannot get transport type, assume that it is polling"); + transport = "polling"; + } - if (!bypass) { - let host = req.headers.host; + const clientIP = await this.getClientIPwithProxy(req.connection.remoteAddress, req.headers); + log.info("socket", `New ${transport} connection, IP = ${clientIP}`); - // If this is set, it means the request is from the browser - let origin = req.headers.origin; - - // If this is from the browser, check if the origin is allowed - if (origin) { + // The following check is only for websocket connections, polling connections are already protected by CORS + if (transport === "polling") { + callback(null, true); + } else if (transport === "websocket") { + const bypass = process.env.UPTIME_KUMA_WS_ORIGIN_CHECK === "bypass"; + if (bypass) { + log.info("auth", "WebSocket origin check is bypassed"); + callback(null, true); + } else if (!req.headers.origin) { + log.info("auth", "WebSocket with no origin is allowed"); + callback(null, true); + } else { try { + let host = req.headers.host; + let origin = req.headers.origin; let originURL = new URL(origin); + let xForwardedFor; + if (await Settings.get("trustProxy")) { + xForwardedFor = req.headers["x-forwarded-for"]; + } - if (host !== originURL.host) { - isOriginValid = false; - log.error("auth", `Origin (${origin}) does not match host (${host}), IP: ${req.socket.remoteAddress}`); + if (host !== originURL.host && xForwardedFor !== originURL.host) { + callback(null, false); + log.error("auth", `Origin (${origin}) does not match host (${host}), IP: ${clientIP}`); + } else { + callback(null, true); } } catch (e) { // Invalid origin url, probably not from browser - isOriginValid = false; - log.error("auth", `Invalid origin url (${origin}), IP: ${req.socket.remoteAddress}`); + callback(null, false); + log.error("auth", `Invalid origin url (${origin}), IP: ${clientIP}`); } - } else { - log.info("auth", `Origin is not set, IP: ${req.socket.remoteAddress}`); } - } else { - log.debug("auth", "Origin check is bypassed"); } - - callback(null, isOriginValid); } }); } @@ -268,20 +292,28 @@ class UptimeKumaServer { /** * Get the IP of the client connected to the socket * @param {Socket} socket - * @returns {string} + * @returns {Promise} */ - async getClientIP(socket) { - let clientIP = socket.client.conn.remoteAddress; + getClientIP(socket) { + return this.getClientIPwithProxy(socket.client.conn.remoteAddress, socket.client.conn.request.headers); + } + /** + * + * @param {string} clientIP + * @param {IncomingHttpHeaders} headers + * @returns {Promise} + */ + async getClientIPwithProxy(clientIP, headers) { if (clientIP === undefined) { clientIP = ""; } if (await Settings.get("trustProxy")) { - const forwardedFor = socket.client.conn.request.headers["x-forwarded-for"]; + const forwardedFor = headers["x-forwarded-for"]; return (typeof forwardedFor === "string" ? forwardedFor.split(",")[0].trim() : null) - || socket.client.conn.request.headers["x-real-ip"] + || headers["x-real-ip"] || clientIP.replace(/^::ffff:/, ""); } else { return clientIP.replace(/^::ffff:/, ""); diff --git a/src/mixins/socket.js b/src/mixins/socket.js index 41c7fabd3..c48a008a3 100644 --- a/src/mixins/socket.js +++ b/src/mixins/socket.js @@ -91,21 +91,20 @@ export default { this.socket.initedSocketIO = true; - let protocol = (location.protocol === "https:") ? "wss://" : "ws://"; + let protocol = location.protocol + "//"; - let wsHost; + let url; const env = process.env.NODE_ENV || "production"; if (env === "development" && isDevContainer()) { - wsHost = protocol + getDevContainerServerHostname(); + url = protocol + getDevContainerServerHostname(); } else if (env === "development" || localStorage.dev === "dev") { - wsHost = protocol + location.hostname + ":3001"; + url = protocol + location.hostname + ":3001"; } else { - wsHost = protocol + location.host; + // Connect to the current url + url = undefined; } - socket = io(wsHost, { - transports: [ "websocket" ], - }); + socket = io(url); socket.on("info", (info) => { this.info = info;