diff --git a/packages/server/src/cli.ts b/packages/server/src/cli.ts index 8433a403..7f43212a 100644 --- a/packages/server/src/cli.ts +++ b/packages/server/src/cli.ts @@ -38,6 +38,7 @@ commander.version(process.env.VERSION || "development") .option("-P, --password ", "DEPRECATED: Use the PASSWORD environment variable instead. Specify a password for authentication.") .option("--disable-telemetry", "Disables ALL telemetry.", false) .option("--socket ", "Listen on a UNIX socket. Host and port will be ignored when set.") + .option("--trust-proxy", "Trust the X-Forwarded-For header, useful when using a reverse proxy.", false) .option("--install-extension ", "Install an extension by its ID.") .option("--bootstrap-fork ", "Used for development. Never set.") .option("--extra-args ", "Used for development. Never set.") @@ -74,6 +75,7 @@ const bold = (text: string | number): string | number => { readonly cert?: string; readonly certKey?: string; readonly socket?: string; + readonly trustProxy?: boolean; readonly installExtension?: string; @@ -273,6 +275,7 @@ const bold = (text: string | number): string | number => { }, }, password, + trustProxy: options.trustProxy, httpsOptions: hasCustomHttps ? { key: certKeyData, cert: certData, diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index b7d9a12c..4c73743a 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -31,6 +31,7 @@ interface CreateAppOptions { httpsOptions?: https.ServerOptions; allowHttp?: boolean; bypassAuth?: boolean; + trustProxy?: boolean; } export const createApp = async (options: CreateAppOptions): Promise<{ @@ -62,6 +63,21 @@ export const createApp = async (options: CreateAppOptions): Promise<{ return true; }; + const remoteAddress = (req: http.IncomingMessage): string | void => { + let xForwardedFor = req.headers["x-forwarded-for"]; + if (Array.isArray(xForwardedFor)) { + xForwardedFor = xForwardedFor.join(", "); + } + + if (options.trustProxy && xForwardedFor !== undefined) { + const addresses = xForwardedFor.split(",").map(s => s.trim()); + + return addresses.pop(); + } + + return req.socket.remoteAddress; + }; + const isAuthed = (req: http.IncomingMessage): boolean => { try { if (!options.password || options.bypassAuth) { @@ -70,7 +86,20 @@ export const createApp = async (options: CreateAppOptions): Promise<{ // Try/catch placed here just in case const cookies = parseCookies(req); - if (cookies.password && safeCompare(cookies.password, options.password)) { + if (cookies.password) { + if (!safeCompare(cookies.password, options.password)) { + let userAgent = req.headers["user-agent"]; + if (Array.isArray(userAgent)) { + userAgent = userAgent.join(", "); + } + logger.info("Failed login attempt", + field("password", cookies.password), + field("remote_address", remoteAddress(req)), + field("user_agent", userAgent)); + + return false; + } + return true; } } catch (ex) { @@ -214,7 +243,9 @@ export const createApp = async (options: CreateAppOptions): Promise<{ const staticGzip = expressStaticGzip(path.join(baseDir, "build/web")); app.use((req, res, next) => { - logger.trace(`\u001B[1m${req.method} ${res.statusCode} \u001B[0m${req.originalUrl}`, field("host", req.hostname), field("ip", req.ip)); + logger.trace(`\u001B[1m${req.method} ${res.statusCode} \u001B[0m${req.originalUrl}`, + field("host", req.hostname), + field("remote_address", remoteAddress(req))); // Force HTTPS unless allowing HTTP. if (!isEncrypted(req.socket) && !options.allowHttp) {