Skip to content
34 changes: 27 additions & 7 deletions api/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,31 @@ function sslDisabled() {
return process.env.FORCE_DISABLE_SSL === 'true'
}

/**
* Coerce a raw trust-proxy value into the type Express expects.
* Accepts "true"/"false" (booleans), numeric strings (hop count),
* and any other string (IP/CIDR list or preset name) verbatim.
*/
function parseTrustProxy(raw: string): boolean | number | string {
const trimmed = raw.trim()
if (trimmed === 'true') return true
if (trimmed === 'false') return false
if (/^\d+$/.test(trimmed)) return parseInt(trimmed, 10)
return trimmed
}

/**
* Configure Express `trust proxy` from the `TRUST_PROXY` env var.
* An unset value leaves the default (false) in place.
*/
function configureTrustProxy() {
const raw = process.env.TRUST_PROXY
if (!raw) return
const value = parseTrustProxy(raw)
app.set('trust proxy', value)
logger.info(`Express 'trust proxy' set to: ${value}`)
}

// apis response codes
enum RESPONSE_CODES {
OK = 'OK',
Expand Down Expand Up @@ -203,6 +228,8 @@ export async function startServer(port: number | string, host?: string) {
// as the really first thing setup loggers so all logs will go to file if specified in settings
setupLogging(settings)

configureTrustProxy()

const httpsEnabled = process.env.HTTPS || settings?.gateway?.https

if (httpsEnabled) {
Expand Down Expand Up @@ -531,13 +558,6 @@ logger.info(`Version: ${utils.getVersion()}`)
logger.info('Application path:' + utils.getPath(true))
logger.info('Store path:' + storeDir)

if (process.env.TRUST_PROXY) {
app.set(
'trust proxy',
process.env.TRUST_PROXY === 'true' ? true : process.env.TRUST_PROXY,
)
}

app.use(
morgan(
':remote-addr :method :url :status :res[content-length] - :response-time ms',
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/env-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ This is the list of the supported environment variables:
- `ZUI_LOG_MAXSIZE`: The maximum size of a single log file. Default is `50m` (50MB)
- `NO_LOG_COLORS`: Set this env var to `'true'` to disable application log colors also in the console.
- `ZUI_NO_CONSOLE`: Set this env var to `'true'` to disable application log in the console.
- `TRUST_PROXY`: Set this env in order to trust the proxy. See [express behind proxies](https://expressjs.com/en/guide/behind-proxies.html) for more info about allowed values.
- `TRUST_PROXY`: Configures Express's `trust proxy` setting when running behind a reverse proxy. Accepts a hop count (e.g. `1`), `true`/`false`, a comma-separated list of trusted IPs/CIDRs, or one of the presets `loopback`/`linklocal`/`uniquelocal`. See [express behind proxies](https://expressjs.com/en/guide/behind-proxies.html) for the allowed values. Setting this resolves the `ERR_ERL_UNEXPECTED_X_FORWARDED_FOR` warning emitted by `express-rate-limit` and ensures rate limiters use the real client IP. Note: setting it to `true` is too permissive and triggers `ERR_ERL_PERMISSIVE_TRUST_PROXY` — prefer an explicit hop count or IP list.
- `FORCE_DISABLE_SSL`: Set this env var to `'true'` to disable SSL.
- `BASE_PATH`: Set this env var to the base path where the application is served. Default is `/`.
- `UID_DISCOVERY_PREFIX`: Sets the prefix used for MQTT Discovery `unique_id` of entities. Default is `zwavejs2mqtt_`.
Expand Down
38 changes: 38 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1337,6 +1337,24 @@ export default {
return
}

// Check if JWT token is expired before attempting connection
if (this.auth && this.user?.token) {
try {
const payload = JSON.parse(
atob(this.user.token.split('.')[1]),
)
if (payload.exp && payload.exp * 1000 < Date.now()) {
log.warn('JWT token expired, logging out')
await this.logout()
return
}
} catch (e) {
log.warn('Invalid JWT token, logging out')
await this.logout()
return
}
}

const auth = this.auth ? { token: this.user.token } : undefined

this.socket = io('/', {
Expand All @@ -1356,7 +1374,10 @@ export default {
'znifferState',
])

let authErrors = 0

this.socket.on('connect', () => {
authErrors = 0
this.updateStatus('Connected', 'success')
log.info('Socket connected')

Expand All @@ -1383,6 +1404,23 @@ export default {
this.updateStatus('Disconnected', 'error')
})

this.socket.on('connect_error', (err) => {
log.warn('Socket connect error', err.message)
// Matches the error thrown by server auth middleware
// in api/app.ts socketManager.authMiddleware
if (err.message === 'Authentication error') {
authErrors++
// Only logout after 3 consecutive auth failures
// to avoid logging out on transient issues
if (authErrors >= 3) {
this.socket.disconnect()
this.logout()
return
}
}
this.updateStatus('Reconnecting', 'warning')
})

this.socket.on('error', (err) => {
log.info('Socket error', err)
})
Expand Down
Loading