Smoothing the update for origin check (#4216)

This commit is contained in:
Louis Lam 2023-12-12 16:23:41 +08:00 committed by GitHub
parent fa1214ae5e
commit f861a48dfc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 69 additions and 36 deletions

View file

@ -78,7 +78,6 @@ function disconnectAllSocketClients(username, password) {
// Disconnect all socket connections // Disconnect all socket connections
const socket = io(localWebSocketURL, { const socket = io(localWebSocketURL, {
transports: [ "websocket" ],
reconnection: false, reconnection: false,
timeout: 5000, timeout: 5000,
}); });

View file

@ -54,7 +54,10 @@ if (!process.env.UPTIME_KUMA_WS_ORIGIN_CHECK) {
log.info("server", "Node Env: " + process.env.NODE_ENV); log.info("server", "Node Env: " + process.env.NODE_ENV);
log.info("server", "Inside Container: " + (process.env.UPTIME_KUMA_IS_CONTAINER === "1")); 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"); log.info("server", "Importing Node libraries");
const fs = require("fs"); const fs = require("fs");

View file

@ -99,39 +99,63 @@ class UptimeKumaServer {
UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType(); UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType();
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing(); 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, { this.io = new Server(this.httpServer, {
allowRequest: (req, callback) => { cors,
let isOriginValid = true; allowRequest: async (req, callback) => {
const bypass = isDev || process.env.UPTIME_KUMA_WS_ORIGIN_CHECK === "bypass"; 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) { const clientIP = await this.getClientIPwithProxy(req.connection.remoteAddress, req.headers);
let host = req.headers.host; log.info("socket", `New ${transport} connection, IP = ${clientIP}`);
// If this is set, it means the request is from the browser // The following check is only for websocket connections, polling connections are already protected by CORS
let origin = req.headers.origin; if (transport === "polling") {
callback(null, true);
// If this is from the browser, check if the origin is allowed } else if (transport === "websocket") {
if (origin) { 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 { try {
let host = req.headers.host;
let origin = req.headers.origin;
let originURL = new URL(origin); let originURL = new URL(origin);
let xForwardedFor;
if (await Settings.get("trustProxy")) {
xForwardedFor = req.headers["x-forwarded-for"];
}
if (host !== originURL.host) { if (host !== originURL.host && xForwardedFor !== originURL.host) {
isOriginValid = false; callback(null, false);
log.error("auth", `Origin (${origin}) does not match host (${host}), IP: ${req.socket.remoteAddress}`); log.error("auth", `Origin (${origin}) does not match host (${host}), IP: ${clientIP}`);
} else {
callback(null, true);
} }
} catch (e) { } catch (e) {
// Invalid origin url, probably not from browser // Invalid origin url, probably not from browser
isOriginValid = false; callback(null, false);
log.error("auth", `Invalid origin url (${origin}), IP: ${req.socket.remoteAddress}`); 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 * Get the IP of the client connected to the socket
* @param {Socket} socket * @param {Socket} socket
* @returns {string} * @returns {Promise<string>}
*/ */
async getClientIP(socket) { getClientIP(socket) {
let clientIP = socket.client.conn.remoteAddress; return this.getClientIPwithProxy(socket.client.conn.remoteAddress, socket.client.conn.request.headers);
}
/**
*
* @param {string} clientIP
* @param {IncomingHttpHeaders} headers
* @returns {Promise<string>}
*/
async getClientIPwithProxy(clientIP, headers) {
if (clientIP === undefined) { if (clientIP === undefined) {
clientIP = ""; clientIP = "";
} }
if (await Settings.get("trustProxy")) { 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) return (typeof forwardedFor === "string" ? forwardedFor.split(",")[0].trim() : null)
|| socket.client.conn.request.headers["x-real-ip"] || headers["x-real-ip"]
|| clientIP.replace(/^::ffff:/, ""); || clientIP.replace(/^::ffff:/, "");
} else { } else {
return clientIP.replace(/^::ffff:/, ""); return clientIP.replace(/^::ffff:/, "");

View file

@ -91,21 +91,20 @@ export default {
this.socket.initedSocketIO = true; 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"; const env = process.env.NODE_ENV || "production";
if (env === "development" && isDevContainer()) { if (env === "development" && isDevContainer()) {
wsHost = protocol + getDevContainerServerHostname(); url = protocol + getDevContainerServerHostname();
} else if (env === "development" || localStorage.dev === "dev") { } else if (env === "development" || localStorage.dev === "dev") {
wsHost = protocol + location.hostname + ":3001"; url = protocol + location.hostname + ":3001";
} else { } else {
wsHost = protocol + location.host; // Connect to the current url
url = undefined;
} }
socket = io(wsHost, { socket = io(url);
transports: [ "websocket" ],
});
socket.on("info", (info) => { socket.on("info", (info) => {
this.info = info; this.info = info;