diff --git a/lib/consts.js b/lib/consts.js index 3518e52b5..d9c532204 100644 --- a/lib/consts.js +++ b/lib/consts.js @@ -150,6 +150,11 @@ module.exports = { // hard limit for subscript execution (does not include waiting for promises) SUBSCRIPT_RUNTIME_TIMEOUT: 30 * 1000, + // how long is an authentication form valid + MAX_FORM_TTL: 1 * 24 * 3600 * 1000, + + NONCE_BYTES: 16, + generateWebhookTable() { let entries = []; diff --git a/lib/routes-ui.js b/lib/routes-ui.js index 4f0f921ce..e6dfe19fa 100644 --- a/lib/routes-ui.js +++ b/lib/routes-ui.js @@ -6,7 +6,6 @@ const consts = require('./consts'); const settings = require('./settings'); const tokens = require('./tokens'); const Joi = require('joi'); -const logger = require('./logger'); const { failAction, verifyAccountInfo, @@ -17,7 +16,8 @@ const { getSignedFormData, readEnvValue, getServiceHostname, - getByteSize + getByteSize, + parseSignedFormData } = require('./tools'); const packageData = require('../package.json'); const he = require('he'); @@ -70,7 +70,9 @@ const { TOTP_WINDOW_SIZE, FETCH_TIMEOUT, DEFAULT_DELIVERY_ATTEMPTS, - DEFAULT_MAX_BODY_SIZE + DEFAULT_MAX_BODY_SIZE, + MAX_FORM_TTL, + NONCE_BYTES } = consts; config.api = config.api || { @@ -924,7 +926,7 @@ function applyRoutes(server, call) { }, async failAction(request, h, err) { - logger.error({ msg: 'Failed to validate queue argument', err }); + request.logger.error({ msg: 'Failed to validate queue argument', err }); return h.redirect('/admin').takeover(); }, @@ -1097,7 +1099,7 @@ function applyRoutes(server, call) { return h.redirect('/admin/config/webhooks'); } catch (err) { await request.flash({ type: 'danger', message: `Failed to update configuration` }); - logger.error({ msg: 'Failed to update configuration', err }); + request.logger.error({ msg: 'Failed to update configuration', err }); return h.view( 'config/webhooks', @@ -1138,7 +1140,7 @@ function applyRoutes(server, call) { } await request.flash({ type: 'danger', message: `Failed to update configuration` }); - logger.error({ msg: 'Failed to update configuration', err }); + request.logger.error({ msg: 'Failed to update configuration', err }); return h .view( @@ -1263,7 +1265,7 @@ function applyRoutes(server, call) { return h.redirect('/admin/config/service'); } catch (err) { await request.flash({ type: 'danger', message: `Failed to update configuration` }); - logger.error({ msg: 'Failed to update configuration', err }); + request.logger.error({ msg: 'Failed to update configuration', err }); return h.view( 'config/service', @@ -1305,7 +1307,7 @@ function applyRoutes(server, call) { } await request.flash({ type: 'danger', message: `Failed to update configuration` }); - logger.error({ msg: 'Failed to update configuration', err }); + request.logger.error({ msg: 'Failed to update configuration', err }); return h .view( @@ -1419,7 +1421,7 @@ return true;` scriptEnvJson: JSON.stringify((await settings.get('scriptEnv')) || '{}'), examplePayloadsJson: JSON.stringify( (await getExampleDocumentsPayloads()).map(entry => - Object.assign({ ...entry }, { summary: undefined, riskAssessment: undefined, preview: undefined }) + Object.assign({}, entry, { summary: undefined, riskAssessment: undefined, preview: undefined }) ) ) }, @@ -1485,7 +1487,7 @@ return true;` return h.redirect('/admin/config/ai'); } catch (err) { await request.flash({ type: 'danger', message: `Failed to update configuration` }); - logger.error({ msg: 'Failed to update configuration', err }); + request.logger.error({ msg: 'Failed to update configuration', err }); let hasOpenAiAPIKey = !!(await settings.get('openAiAPIKey')); let openAiError = await getOpenAiError(); @@ -1518,7 +1520,7 @@ return true;` scriptEnvJson: JSON.stringify((await settings.get('scriptEnv')) || '{}'), examplePayloadsJson: JSON.stringify( (await getExampleDocumentsPayloads()).map(entry => - Object.assign({ ...entry }, { summary: undefined, riskAssessment: undefined, preview: undefined }) + Object.assign({}, entry, { summary: undefined, riskAssessment: undefined, preview: undefined }) ) ) }, @@ -1548,7 +1550,7 @@ return true;` } await request.flash({ type: 'danger', message: `Failed to update configuration` }); - logger.error({ msg: 'Failed to update configuration', err }); + request.logger.error({ msg: 'Failed to update configuration', err }); let hasOpenAiAPIKey = !!(await settings.get('openAiAPIKey')); let openAiError = await getOpenAiError(); @@ -1582,7 +1584,7 @@ return true;` scriptEnvJson: JSON.stringify((await settings.get('scriptEnv')) || '{}'), examplePayloadsJson: JSON.stringify( (await getExampleDocumentsPayloads()).map(entry => - Object.assign({ ...entry }, { summary: undefined, riskAssessment: undefined, preview: undefined }) + Object.assign({}, entry, { summary: undefined, riskAssessment: undefined, preview: undefined }) ) ), @@ -1623,7 +1625,7 @@ return true;` path: '/admin/config/ai/test-prompt', async handler(request) { try { - logger.info({ msg: 'Prompt test' }); + request.logger.info({ msg: 'Prompt test' }); const parsed = await simpleParser(Buffer.from(request.payload.emailFile, 'base64')); @@ -1661,7 +1663,7 @@ return true;` return { success: true, response }; } catch (err) { - logger.error({ msg: 'Failed to test prompt', err }); + request.logger.error({ msg: 'Failed to test prompt', err }); return { success: false, error: err.message }; } }, @@ -1837,7 +1839,7 @@ return true;` return h.redirect('/admin/config/logging'); } catch (err) { await request.flash({ type: 'danger', message: `Failed to update configuration` }); - logger.error({ msg: 'Failed to update configuration', err }); + request.logger.error({ msg: 'Failed to update configuration', err }); return h.view( 'config/logging', @@ -1871,7 +1873,7 @@ return true;` } await request.flash({ type: 'danger', message: `Failed to update configuration` }); - logger.error({ msg: 'Failed to update configuration', err }); + request.logger.error({ msg: 'Failed to update configuration', err }); return h .view( @@ -1901,12 +1903,12 @@ return true;` try { let requested = 0; for (let account of request.payload.accounts) { - logger.info({ msg: 'Request reconnect for logging', account }); + request.logger.info({ msg: 'Request reconnect for logging', account }); try { await call({ cmd: 'update', account }); requested++; } catch (err) { - logger.error({ msg: 'Reconnect request failed', action: 'request_reconnect', account, err }); + request.logger.error({ msg: 'Reconnect request failed', action: 'request_reconnect', account, err }); } } @@ -1915,7 +1917,7 @@ return true;` accounts: requested }; } catch (err) { - logger.error({ msg: 'Failed to request reconnect', err, accounts: request.payload.accounts }); + request.logger.error({ msg: 'Failed to request reconnect', err, accounts: request.payload.accounts }); return { success: false, error: err.message }; } }, @@ -2016,7 +2018,7 @@ return true;` date: new Date().toISOString(), event: 'test', data: { - nonce: crypto.randomBytes(12).toString('hex') + nonce: crypto.randomBytes(NONCE_BYTES).toString('base64url') } }), headers, @@ -2040,7 +2042,7 @@ return true;` duration }; } catch (err) { - logger.error({ msg: 'Failed posting webhook', webhooks, event: 'test', err }); + request.logger.error({ msg: 'Failed posting webhook', webhooks, event: 'test', err }); return { success: false, target: webhooks, @@ -2180,7 +2182,10 @@ return true;` let providerData = oauth2ProviderData(app.provider); let disabledScopes = {}; - if (app.skipScopes?.includes('SMTP.Send') || app.skipScopes?.includes('https://outlook.office.com/SMTP.Send')) { + if ( + (app.skipScopes && app.skipScopes.includes('SMTP.Send')) || + (app.skipScopes && app.skipScopes.includes('https://outlook.office.com/SMTP.Send')) + ) { disabledScopes.SMTP_Send = true; } @@ -2235,7 +2240,7 @@ return true;` return h.redirect('/admin/config/oauth'); } catch (err) { await request.flash({ type: 'danger', message: `Failed to delete the OAuth2 application` }); - logger.error({ msg: 'Failed to delete OAuth2 application', err, app: request.payload.app, remoteAddress: request.app.ip }); + request.logger.error({ msg: 'Failed to delete OAuth2 application', err, app: request.payload.app, remoteAddress: request.app.ip }); return h.redirect(`/admin/config/oauth/app/${request.payload.app}`); } }, @@ -2249,7 +2254,7 @@ return true;` async failAction(request, h, err) { await request.flash({ type: 'danger', message: `Failed to delete the OAuth2 application` }); - logger.error({ msg: 'Failed to delete delete the OAuth2 application', err }); + request.logger.error({ msg: 'Failed to delete delete the OAuth2 application', err }); return h.redirect('/admin/config/oauth').takeover(); }, @@ -2347,7 +2352,7 @@ return true;` return h.redirect(`/admin/config/oauth/app/${oauth2App.id}`); } catch (err) { await request.flash({ type: 'danger', message: `Failed to register OAuth2 app` }); - logger.error({ msg: 'Failed to register OAuth2 app', err }); + request.logger.error({ msg: 'Failed to register OAuth2 app', err }); let { provider, baseScopes } = request.payload; if (!provider || !OAUTH_PROVIDERS.hasOwnProperty(provider)) { @@ -2401,7 +2406,7 @@ return true;` } await request.flash({ type: 'danger', message: `Failed to register OAuth2 app` }); - logger.error({ msg: 'Failed to register OAuth2 app', err }); + request.logger.error({ msg: 'Failed to register OAuth2 app', err }); let { provider, baseScopes } = request.payload; if (!provider || !OAUTH_PROVIDERS.hasOwnProperty(provider)) { @@ -2544,7 +2549,7 @@ return true;` return h.redirect(`/admin/config/oauth/app/${oauth2App.id}`); } catch (err) { await request.flash({ type: 'danger', message: `Failed to update OAuth2 app` }); - logger.error({ msg: 'Failed to update OAuth2 app', app: request.payload.app, err }); + request.logger.error({ msg: 'Failed to update OAuth2 app', app: request.payload.app, err }); let providerData = oauth2ProviderData(appData.provider); @@ -2599,12 +2604,12 @@ return true;` let appData = await oauth2Apps.get(request.payload.app); if (!appData) { await request.flash({ type: 'danger', message: `Application was not found.` }); - logger.error({ msg: 'Application was not found.', app: request.payload.app }); + request.logger.error({ msg: 'Application was not found.', app: request.payload.app }); return h.redirect('/admin').takeover(); } await request.flash({ type: 'danger', message: `Failed to update OAuth2 app` }); - logger.error({ msg: 'Failed to update OAuth2 app', err }); + request.logger.error({ msg: 'Failed to update OAuth2 app', err }); let { provider } = request.payload; if (!provider || !OAUTH_PROVIDERS.hasOwnProperty(provider)) { @@ -2867,7 +2872,7 @@ return payload;`) return h.redirect(`/admin/webhooks/webhook/${createRequest.id}`); } catch (err) { await request.flash({ type: 'danger', message: `Failed to create webhook routing` }); - logger.error({ msg: 'Failed to create webhook routing', err }); + request.logger.error({ msg: 'Failed to create webhook routing', err }); return h.view( 'webhooks/new', @@ -2905,7 +2910,7 @@ return payload;`) } await request.flash({ type: 'danger', message: `Failed to create webhook routing` }); - logger.error({ msg: 'Failed to create webhook routing', err }); + request.logger.error({ msg: 'Failed to create webhook routing', err }); return h .view( @@ -3175,7 +3180,7 @@ return payload;`) return h.redirect(`/admin/webhooks/webhook/${request.payload.webhook}`); } catch (err) { await request.flash({ type: 'danger', message: `Failed to update Webhook Route` }); - logger.error({ msg: 'Failed to update Webhook Route', err }); + request.logger.error({ msg: 'Failed to update Webhook Route', err }); let webhook = await webhooks.get(request.payload.webhook); if (!webhook) { @@ -3222,7 +3227,7 @@ return payload;`) } await request.flash({ type: 'danger', message: `Failed to update Webhook Route` }); - logger.error({ msg: 'Failed to update Webhook Route', err }); + request.logger.error({ msg: 'Failed to update Webhook Route', err }); let webhook = await webhooks.get(request.payload.webhook); if (!webhook) { @@ -3317,7 +3322,7 @@ return payload;`) return h.redirect(accountWebhooksLink.pathname + accountWebhooksLink.search); } catch (err) { await request.flash({ type: 'danger', message: `Failed to delete the Webhook Route` }); - logger.error({ msg: 'Failed to delete Webhook Route', err, webhook: request.payload.webhook, remoteAddress: request.app.ip }); + request.logger.error({ msg: 'Failed to delete Webhook Route', err, webhook: request.payload.webhook, remoteAddress: request.app.ip }); return h.redirect(`/admin/webhooks/webhook/${request.payload.webhook}`); } }, @@ -3331,7 +3336,7 @@ return payload;`) async failAction(request, h, err) { await request.flash({ type: 'danger', message: `Failed to delete Webhook Route` }); - logger.error({ msg: 'Failed to delete delete Webhook Route', err }); + request.logger.error({ msg: 'Failed to delete delete Webhook Route', err }); return h.redirect('/admin/webhooks').takeover(); }, @@ -3603,7 +3608,7 @@ return payload;`) return h.redirect(`/admin/templates/template/${request.payload.template}`); } catch (err) { await request.flash({ type: 'danger', message: `Failed to update template` }); - logger.error({ msg: 'Failed to update template', err }); + request.logger.error({ msg: 'Failed to update template', err }); let template = await templates.get(request.payload.template); if (!template) { @@ -3666,7 +3671,7 @@ return payload;`) } await request.flash({ type: 'danger', message: `Failed to update template` }); - logger.error({ msg: 'Failed to update template', err }); + request.logger.error({ msg: 'Failed to update template', err }); let template = await templates.get(request.payload.template); if (!template) { @@ -3830,7 +3835,7 @@ return payload;`) return h.redirect(`/admin/templates/template/${createRequest.id}`); } catch (err) { await request.flash({ type: 'danger', message: `Failed to create template` }); - logger.error({ msg: 'Failed to create template', err }); + request.logger.error({ msg: 'Failed to create template', err }); let account; if (request.payload.account) { @@ -3885,7 +3890,7 @@ return payload;`) } await request.flash({ type: 'danger', message: `Failed to create template` }); - logger.error({ msg: 'Failed to create template', err }); + request.logger.error({ msg: 'Failed to create template', err }); let account; if (request.payload.account) { @@ -3962,7 +3967,7 @@ return payload;`) return h.redirect(accountTemplatesLink.pathname + accountTemplatesLink.search); } catch (err) { await request.flash({ type: 'danger', message: `Failed to delete the template` }); - logger.error({ msg: 'Failed to delete the template', err, template: request.payload.template, remoteAddress: request.app.ip }); + request.logger.error({ msg: 'Failed to delete the template', err, template: request.payload.template, remoteAddress: request.app.ip }); return h.redirect(`/admin/templates/template/${request.payload.template}`); } }, @@ -3976,7 +3981,7 @@ return payload;`) async failAction(request, h, err) { await request.flash({ type: 'danger', message: `Failed to delete the account` }); - logger.error({ msg: 'Failed to delete delete the account', err }); + request.logger.error({ msg: 'Failed to delete delete the account', err }); return h.redirect('/admin/templates').takeover(); }, @@ -3998,7 +4003,7 @@ return payload;`) path: '/admin/templates/test', async handler(request) { try { - logger.info({ msg: 'Trying to send test message', payload: request.payload }); + request.logger.info({ msg: 'Trying to send test message', payload: request.payload }); let template = await templates.get(request.payload.template); if (!template) { @@ -4047,7 +4052,7 @@ return payload;`) }; } } catch (err) { - logger.error({ msg: 'Failed sending test message', err }); + request.logger.error({ msg: 'Failed sending test message', err }); return { success: false, error: err.message @@ -4317,7 +4322,7 @@ return payload;`) return h.redirect(`/admin/gateways/gateway/${encodeURIComponent(result.gateway)}?state=${result.state}`); } catch (err) { await request.flash({ type: 'danger', message: `Failed to add new gateway` }); - logger.error({ msg: 'Failed to add new gateway', err }); + request.logger.error({ msg: 'Failed to add new gateway', err }); return h.view( 'gateways/new', @@ -4352,7 +4357,7 @@ return payload;`) } await request.flash({ type: 'danger', message: `Failed to add new gateway` }); - logger.error({ msg: 'Failed to add new gateway', err }); + request.logger.error({ msg: 'Failed to add new gateway', err }); return h .view( @@ -4429,7 +4434,7 @@ return payload;`) return h.redirect(`/admin/gateways/gateway/${encodeURIComponent(result.gateway)}`); } catch (err) { await request.flash({ type: 'danger', message: `Failed to update gateway` }); - logger.error({ msg: 'Failed to update gateway', err }); + request.logger.error({ msg: 'Failed to update gateway', err }); let gatewayObject = new Gateway({ gateway: request.payload.gateway, redis, secret: await getSecret() }); let gatewayData = await gatewayObject.loadGatewayData(); @@ -4471,7 +4476,7 @@ return payload;`) } await request.flash({ type: 'danger', message: `Failed to update gateway` }); - logger.error({ msg: 'Failed to update gateway', err }); + request.logger.error({ msg: 'Failed to update gateway', err }); let gatewayObject = new Gateway({ gateway: request.payload.gateway, redis, secret: await getSecret() }); let gatewayData = await gatewayObject.loadGatewayData(); @@ -4581,7 +4586,7 @@ return payload;`) return verifyResult.smtp; } catch (err) { - logger.error({ msg: 'Failed posting request', host, port, user, pass: !!pass, err }); + request.logger.error({ msg: 'Failed posting request', host, port, user, pass: !!pass, err }); return { success: false, error: err.message @@ -4636,7 +4641,7 @@ return payload;`) return h.redirect('/admin/gateways'); } catch (err) { await request.flash({ type: 'danger', message: `Failed to delete the gateway` }); - logger.error({ msg: 'Failed to delete the gateway', err, gateway: request.payload.gateway, remoteAddress: request.app.ip }); + request.logger.error({ msg: 'Failed to delete the gateway', err, gateway: request.payload.gateway, remoteAddress: request.app.ip }); return h.redirect(`/admin/gateways/${request.params.gateway}`); } }, @@ -4650,7 +4655,7 @@ return payload;`) async failAction(request, h, err) { await request.flash({ type: 'danger', message: `Failed to delete the gateway` }); - logger.error({ msg: 'Failed to delete delete the gateway', err }); + request.logger.error({ msg: 'Failed to delete delete the gateway', err }); return h.redirect('/admin/gateways').takeover(); }, @@ -4839,7 +4844,7 @@ return payload;`) token }; } catch (err) { - logger.error({ msg: 'Failed to generate token', err, remoteAddress: request.app.ip, description: request.payload.description }); + request.logger.error({ msg: 'Failed to generate token', err, remoteAddress: request.app.ip, description: request.payload.description }); if (Boom.isBoom(err)) { return Object.assign({ success: false }, err.output.payload); } @@ -4878,7 +4883,7 @@ return payload;`) return h.redirect('/admin/tokens'); } catch (err) { await request.flash({ type: 'danger', message: `Failed to delete access token` }); - logger.error({ msg: 'Failed to delete access token', err, token: request.payload.token, remoteAddress: request.app.ip }); + request.logger.error({ msg: 'Failed to delete access token', err, token: request.payload.token, remoteAddress: request.app.ip }); return h.redirect('/admin/tokens'); } }, @@ -4892,7 +4897,7 @@ return payload;`) async failAction(request, h, err) { await request.flash({ type: 'danger', message: `Failed to delete access token` }); - logger.error({ msg: 'Failed to delete access token', err }); + request.logger.error({ msg: 'Failed to delete access token', err }); return h.redirect('/admin/tokens').takeover(); }, @@ -4959,7 +4964,7 @@ return payload;`) return h.redirect('/admin/config/license'); } catch (err) { await request.flash({ type: 'danger', message: `Failed to register license key` }); - logger.error({ msg: 'Failed to register license key', err }); + request.logger.error({ msg: 'Failed to register license key', err }); return h.view( 'config/license', @@ -5000,7 +5005,7 @@ return payload;`) } await request.flash({ type: 'danger', message: `Failed to register license key` }); - logger.error({ msg: 'Failed to register license key', err }); + request.logger.error({ msg: 'Failed to register license key', err }); return h .view( @@ -5052,7 +5057,7 @@ return payload;`) return h.redirect('/admin/config/license'); } catch (err) { await request.flash({ type: 'danger', message: `Failed to unregister license key` }); - logger.error({ msg: 'Failed to unregister license key', err, token: request.payload.token, remoteAddress: request.app.ip }); + request.logger.error({ msg: 'Failed to unregister license key', err, token: request.payload.token, remoteAddress: request.app.ip }); return h.redirect('/admin/config/license'); } }, @@ -5066,7 +5071,7 @@ return payload;`) async failAction(request, h, err) { await request.flash({ type: 'danger', message: `Failed to unregister license key` }); - logger.error({ msg: 'Failed to unregister license key', err }); + request.logger.error({ msg: 'Failed to unregister license key', err }); return h.redirect('/admin/config/license').takeover(); }, @@ -5134,7 +5139,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} throw new Error('Failed to activate provisioned trial license'); } catch (err) { - logger.error({ msg: 'Failed to provision a trial license key', err, remoteAddress: request.app.ip }); + request.logger.error({ msg: 'Failed to provision a trial license key', err, remoteAddress: request.app.ip }); return { success: false, error: (err.response && err.response.error) || err.message }; } }, @@ -5201,7 +5206,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} }, async failAction(request, h, err) { - logger.error({ msg: 'Failed to validate login arguments', err }); + request.logger.error({ msg: 'Failed to validate login arguments', err }); return h.redirect('/admin/login').takeover(); }, @@ -5231,7 +5236,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} try { let rateLimit = await h.checkRateLimit(`login:${request.payload.username}`, 1, 10, 60); if (!rateLimit.success) { - logger.error({ msg: 'Rate limited', rateLimit }); + request.logger.error({ msg: 'Rate limited', rateLimit }); let err = new Error('Rate limited, please wait and try again'); err.responseText = err.message; throw err; @@ -5241,7 +5246,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} let totpEnabled = (await settings.get('totpEnabled')) || false; if (authData && authData.user && authData.user !== request.payload.username) { - logger.error({ msg: 'Invalid username', username: request.payload.username }); + request.logger.error({ msg: 'Invalid username', username: request.payload.username }); let err = new Error('Failed to authenticate'); err.details = { password: err.message }; throw err; @@ -5254,7 +5259,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} throw new Error('Invalid password'); } } catch (E) { - logger.error({ msg: 'Failed to verify password hash', err: E, hash: authData.password }); + request.logger.error({ msg: 'Failed to verify password hash', err: E, hash: authData.password }); let err = new Error('Failed to authenticate'); err.details = { password: err.message }; throw err; @@ -5291,7 +5296,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} } } catch (err) { await request.flash({ type: 'danger', message: err.responseText || `Failed to authenticate` }); - logger.error({ msg: 'Failed to authenticate', err }); + request.logger.error({ msg: 'Failed to authenticate', err }); let errors = err.details; @@ -5330,7 +5335,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} } await request.flash({ type: 'danger', message: `Failed to authenticate` }); - logger.error({ msg: 'Failed to authenticate', err }); + request.logger.error({ msg: 'Failed to authenticate', err }); return h .view( @@ -5396,7 +5401,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} }, async failAction(request, h, err) { - logger.error({ msg: 'Failed to validate login arguments', err }); + request.logger.error({ msg: 'Failed to validate login arguments', err }); return h.redirect('/admin/login').takeover(); }, @@ -5427,7 +5432,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} // attempt limiter let rateLimit = await h.checkRateLimit(`totp:attempt:${request.auth.credentials.user}`, 1, 10, 60); if (!rateLimit.success) { - logger.error({ msg: 'Rate limited', rateLimit }); + request.logger.error({ msg: 'Rate limited', rateLimit }); let err = new Error('Rate limited, please wait and try again'); err.responseText = err.message; throw err; @@ -5456,7 +5461,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} // code re-use limiter let reUseLimit = await h.checkRateLimit(`totp:code:${request.payload.code}`, 1, 1, 12 * 60); if (!reUseLimit.success) { - logger.error({ msg: 'TOTP code recently used', reUseLimit }); + request.logger.error({ msg: 'TOTP code recently used', reUseLimit }); let err = new Error('This code has been already used, please wait and try another code'); err.responseText = err.message; throw err; @@ -5479,7 +5484,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} await request.flash({ type: 'danger', message: err.responseText || `Failed to verify login` }); } - logger.error({ msg: 'Failed to verify login', err }); + request.logger.error({ msg: 'Failed to verify login', err }); let errors = err.details; @@ -5515,7 +5520,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} } await request.flash({ type: 'danger', message: `Failed to verify login` }); - logger.error({ msg: 'Failed to verify login', err }); + request.logger.error({ msg: 'Failed to verify login', err }); return h .view( @@ -5663,7 +5668,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} return h.redirect(`/admin/account/security`); } catch (err) { await request.flash({ type: 'danger', message: `Failed to enable 2FA` }); - logger.error({ msg: 'Failed to enable 2FA', err, remoteAddress: request.app.ip }); + request.logger.error({ msg: 'Failed to enable 2FA', err, remoteAddress: request.app.ip }); return h.redirect(`/admin/account/security`); } }, @@ -5677,7 +5682,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} async failAction(request, h, err) { await request.flash({ type: 'danger', message: `Failed to enable 2FA` }); - logger.error({ msg: 'Failed to enable 2FA', err }); + request.logger.error({ msg: 'Failed to enable 2FA', err }); return h.redirect('/admin').takeover(); }, @@ -5706,7 +5711,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} return h.redirect(`/admin/account/security`); } catch (err) { await request.flash({ type: 'danger', message: `Failed to disable 2FA` }); - logger.error({ msg: 'Failed to enable 2FA', err, remoteAddress: request.app.ip }); + request.logger.error({ msg: 'Failed to enable 2FA', err, remoteAddress: request.app.ip }); return h.redirect(`/admin/account/security`); } }, @@ -5720,7 +5725,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} async failAction(request, h, err) { await request.flash({ type: 'danger', message: `Failed to disable 2FA` }); - logger.error({ msg: 'Failed to disable 2FA', err }); + request.logger.error({ msg: 'Failed to disable 2FA', err }); return h.redirect('/admin').takeover(); }, @@ -5753,7 +5758,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} return h.redirect('/'); } catch (err) { await request.flash({ type: 'danger', message: `Failed to log out user sessions` }); - logger.error({ msg: 'Failed to log out user sessions', err, remoteAddress: request.app.ip }); + request.logger.error({ msg: 'Failed to log out user sessions', err, remoteAddress: request.app.ip }); return h.redirect(`/admin/account/security`); } }, @@ -5767,7 +5772,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} async failAction(request, h, err) { await request.flash({ type: 'danger', message: `Failed to log out user sessions` }); - logger.error({ msg: 'Failed to log out user sessions', err }); + request.logger.error({ msg: 'Failed to log out user sessions', err }); return h.redirect('/admin').takeover(); } @@ -5819,7 +5824,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} throw new Error('Invalid current password'); } } catch (E) { - logger.error({ msg: 'Failed to verify password hash', err: E, hash: authData.password }); + request.logger.error({ msg: 'Failed to verify password hash', err: E, hash: authData.password }); let err = new Error('Failed to verify current password'); err.details = { password0: err.message }; throw err; @@ -5864,7 +5869,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} return h.redirect('/admin/account/password'); } catch (err) { await request.flash({ type: 'danger', message: `Failed to update password` }); - logger.error({ msg: 'Failed to update password', err }); + request.logger.error({ msg: 'Failed to update password', err }); let username = (request.auth && request.auth.credentials && request.auth.credentials.user) || 'admin'; @@ -5904,7 +5909,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} } await request.flash({ type: 'danger', message: `Failed to update account password` }); - logger.error({ msg: 'Failed to update account password', err }); + request.logger.error({ msg: 'Failed to update account password', err }); let username = (request.auth && request.auth.credentials && request.auth.credentials.user) || 'admin'; @@ -6119,7 +6124,11 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} async handler(request, h) { let { data, signature } = await getSignedFormData({ account: request.payload.account, - name: request.payload.name + name: request.payload.name, + + // identify request + n: crypto.randomBytes(NONCE_BYTES).toString('base64'), + t: Date.now() }); let url = new URL(`accounts/new`, 'http://localhost'); @@ -6157,7 +6166,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} } await request.flash({ type: 'danger', message: `Failed to set up account${errors.account ? `: ${errors.account}` : ''}` }); - logger.error({ msg: 'Failed to update configuration', err }); + request.logger.error({ msg: 'Failed to update configuration', err }); return h.redirect('/admin/accounts').takeover(); }, @@ -6171,20 +6180,9 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} }); async function accountFormHandler(request, h) { - let data = Buffer.from(request.payload.data, 'base64url').toString(); - let serviceSecret = await settings.get('serviceSecret'); - if (serviceSecret) { - let hmac = crypto.createHmac('sha256', serviceSecret); - hmac.update(data); - if (hmac.digest('base64url') !== request.payload.sig) { - let error = Boom.boomify(new Error('Signature validation failed'), { statusCode: 403 }); - throw error; - } - } - - data = JSON.parse(data); + const data = await parseSignedFormData(redis, request.payload, gt); - let oauth2App = await oauth2Apps.get(request.payload.type); + const oauth2App = await oauth2Apps.get(request.payload.type); if (oauth2App && oauth2App.enabled) { // prepare account entry @@ -6193,26 +6191,19 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} account: data.account }; - if (data.name) { - accountData.name = data.name; - } - - if (data.email) { - accountData.email = data.email; - } - - if (data.redirectUrl) { - accountData._meta = { - redirectUrl: data.redirectUrl - }; + for (let key of ['name', 'email', 'syncFrom']) { + if (data[key]) { + accountData[key] = data[key]; + } } - const oAuth2Client = await oauth2Apps.getClient(oauth2App.id); - let nonce = crypto.randomBytes(12).toString('hex'); - accountData.notifyFrom = data.notifyFrom || new Date().toISOString(); - if (data.syncFrom) { - accountData.syncFrom = data.syncFrom; + + for (let key of ['redirectUrl', 'n', 't']) { + if (!accountData._meta) { + accountData._meta = {}; + } + accountData._meta[key] = data[key]; } if (data.delegated) { @@ -6225,11 +6216,16 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} provider: oauth2App.id }; + // throws if invalid or unknown app ID + const oAuth2Client = await oauth2Apps.getClient(oauth2App.id); + + const nonce = data.n || crypto.randomBytes(NONCE_BYTES).toString('base64url'); + // store account data await redis .multi() .set(`${REDIS_PREFIX}account:add:${nonce}`, JSON.stringify(accountData)) - .expire(`${REDIS_PREFIX}account:add:${nonce}`, 1 * 24 * 3600) + .expire(`${REDIS_PREFIX}account:add:${nonce}`, Math.floor(MAX_FORM_TTL / 1000)) .exec(); // Generate the url that will be used for the consent dialog. @@ -6273,16 +6269,8 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} return accountFormHandler(request, h); } - let data = Buffer.from(request.query.data, 'base64url').toString(); - let serviceSecret = await settings.get('serviceSecret'); - if (serviceSecret) { - let hmac = crypto.createHmac('sha256', serviceSecret); - hmac.update(data); - if (hmac.digest('base64url') !== request.query.sig) { - let error = Boom.boomify(new Error(gt.gettext('Signature validation failed')), { statusCode: 403 }); - throw error; - } - } + // throws if check fails + await parseSignedFormData(redis, request.query, gt); let oauth2apps = (await oauth2Apps.list(0, 100)).apps.filter(app => app.includeInListing); oauth2apps.forEach(app => { @@ -6315,7 +6303,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} }, async failAction(request, h, err) { - logger.error({ msg: 'Failed to validate request arguments', err }); + request.logger.error({ msg: 'Failed to validate request arguments', err }); let error = Boom.boomify(new Error(gt.gettext('Failed to validate request arguments')), { statusCode: 400 }); if (err.code) { error.output.payload.code = err.code; @@ -6355,7 +6343,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} }, async failAction(request, h, err) { - logger.error({ msg: 'Failed to validate request arguments', err }); + request.logger.error({ msg: 'Failed to validate request arguments', err }); let error = Boom.boomify(new Error(gt.gettext('Failed to validate request arguments')), { statusCode: 400 }); if (err.code) { error.output.payload.code = err.code; @@ -6384,24 +6372,13 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} path: '/accounts/new/imap', async handler(request, h) { - let data = Buffer.from(request.payload.data, 'base64url').toString(); - let serviceSecret = await settings.get('serviceSecret'); - if (serviceSecret) { - let hmac = crypto.createHmac('sha256', serviceSecret); - hmac.update(data); - if (hmac.digest('base64url') !== request.payload.sig) { - let error = Boom.boomify(new Error(gt.gettext('Signature validation failed')), { statusCode: 403 }); - throw error; - } - } - - data = JSON.parse(data); + await parseSignedFormData(redis, request.payload, gt); let serverSettings; try { serverSettings = await autodetectImapSettings(request.payload.email); } catch (err) { - logger.error({ msg: 'Failed to resolve email server settings', email: request.payload.email, err }); + request.logger.error({ msg: 'Failed to resolve email server settings', email: request.payload.email, err }); } let values = Object.assign( @@ -6454,7 +6431,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} } await request.flash({ type: 'danger', message: gt.gettext('Failed to process account') }); - logger.error({ msg: 'Failed to process account', err }); + request.logger.error({ msg: 'Failed to process account', err }); return h .view( @@ -6612,18 +6589,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} path: '/accounts/new/imap/server', async handler(request, h) { - let data = Buffer.from(request.payload.data, 'base64url').toString(); - let serviceSecret = await settings.get('serviceSecret'); - if (serviceSecret) { - let hmac = crypto.createHmac('sha256', serviceSecret); - hmac.update(data); - if (hmac.digest('base64url') !== request.payload.sig) { - let error = Boom.boomify(new Error(gt.gettext('Signature validation failed')), { statusCode: 403 }); - throw error; - } - } - - data = JSON.parse(data); + const data = await parseSignedFormData(redis, request.payload, gt); const accountData = { account: data.account || null, @@ -6664,6 +6630,20 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} const accountObject = new Account({ redis, call, secret: await getSecret() }); const result = await accountObject.create(accountData); + if (data.n) { + // store nonce to prevent this URL to be reused + const keyName = `${REDIS_PREFIX}account:form:${data.n}`; + try { + await redis + .multi() + .set(keyName, (data.t || '0').toString()) + .expire(keyName, Math.floor(MAX_FORM_TTL / 1000)) + .exec(); + } catch (err) { + request.logger.error({ msg: 'Failed to set nonce for an account form request', err }); + } + } + let httpRedirectUrl; if (data.redirectUrl) { const serviceUrl = await settings.get('serviceUrl'); @@ -6705,7 +6685,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} } await request.flash({ type: 'danger', message: gt.gettext('Failed to process account') }); - logger.error({ msg: 'Failed to process account', err }); + request.logger.error({ msg: 'Failed to process account', err }); return h .view( @@ -6957,7 +6937,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} return h.redirect('/admin/accounts'); } catch (err) { await request.flash({ type: 'danger', message: `Failed to delete the account` }); - logger.error({ msg: 'Failed to delete the account', err, account: request.payload.account, remoteAddress: request.app.ip }); + request.logger.error({ msg: 'Failed to delete the account', err, account: request.payload.account, remoteAddress: request.app.ip }); return h.redirect(`/admin/accounts/${request.params.account}`); } }, @@ -6971,7 +6951,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} async failAction(request, h, err) { await request.flash({ type: 'danger', message: `Failed to delete the account` }); - logger.error({ msg: 'Failed to delete delete the account', err }); + request.logger.error({ msg: 'Failed to delete delete the account', err }); return h.redirect('/admin/accounts').takeover(); }, @@ -6989,18 +6969,18 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} async handler(request) { let account = request.params.account; try { - logger.info({ msg: 'Request reconnect for logging', account }); + request.logger.info({ msg: 'Request reconnect for logging', account }); try { await call({ cmd: 'update', account }); } catch (err) { - logger.error({ msg: 'Reconnect request failed', action: 'request_reconnect', account, err }); + request.logger.error({ msg: 'Reconnect request failed', action: 'request_reconnect', account, err }); } return { success: true }; } catch (err) { - logger.error({ msg: 'Failed to request reconnect', err, account }); + request.logger.error({ msg: 'Failed to request reconnect', err, account }); return { success: false, error: err.message }; } }, @@ -7027,18 +7007,18 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} async handler(request) { let account = request.params.account; try { - logger.info({ msg: 'Request syncing', account }); + request.logger.info({ msg: 'Request syncing', account }); try { await call({ cmd: 'sync', account }); } catch (err) { - logger.error({ msg: 'Sync request failed', action: 'request_sync', account, err }); + request.logger.error({ msg: 'Sync request failed', action: 'request_sync', account, err }); } return { success: true }; } catch (err) { - logger.error({ msg: 'Failed to request syncing', err, account }); + request.logger.error({ msg: 'Failed to request syncing', err, account }); return { success: false, error: err.message }; } }, @@ -7066,7 +7046,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} let account = request.params.account; let accountObject = new Account({ redis, account }); try { - logger.info({ msg: 'Request to update account logging state', account, enabled: request.payload.enabled }); + request.logger.info({ msg: 'Request to update account logging state', account, enabled: request.payload.enabled }); await redis.hSetExists(accountObject.getAccountKey(), 'logs', request.payload.enabled ? 'true' : 'false'); @@ -7075,7 +7055,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} enabled: (await redis.hget(accountObject.getAccountKey(), 'logs')) === 'true' }; } catch (err) { - logger.error({ msg: 'Failed to update account logging state', err, account, enabled: request.payload.enabled }); + request.logger.error({ msg: 'Failed to update account logging state', err, account, enabled: request.payload.enabled }); return { success: false, error: err.message }; } }, @@ -7107,7 +7087,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} let account = request.params.account; let accountObject = new Account({ redis, account }); try { - logger.info({ msg: 'Request to flush logs', account }); + request.logger.info({ msg: 'Request to flush logs', account }); await redis.del(accountObject.getLogKey()); @@ -7115,7 +7095,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} success: true }; } catch (err) { - logger.error({ msg: 'Failed to flush logs', err, account }); + request.logger.error({ msg: 'Failed to flush logs', err, account }); return { success: false, error: err.message }; } }, @@ -7297,7 +7277,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} return h.redirect(`/admin/accounts/${request.params.account}`); } catch (err) { await request.flash({ type: 'danger', message: `Failed to update account settings` }); - logger.error({ msg: 'Failed to update account settings', err, account: request.params.account }); + request.logger.error({ msg: 'Failed to update account settings', err, account: request.params.account }); let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() }); let accountData = await accountObject.loadAccountData(); @@ -7342,7 +7322,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} } await request.flash({ type: 'danger', message: `Failed to update configuration` }); - logger.error({ msg: 'Failed to update configuration', err }); + request.logger.error({ msg: 'Failed to update configuration', err }); let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() }); let accountData = await accountObject.loadAccountData(); @@ -7506,7 +7486,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} return h.redirect('/admin/config/document-store'); } catch (err) { await request.flash({ type: 'danger', message: `Failed to update configuration` }); - logger.error({ msg: 'Failed to update configuration', err }); + request.logger.error({ msg: 'Failed to update configuration', err }); let hasDocumentStorePassword = !!(await settings.get('documentStorePassword')); @@ -7546,7 +7526,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} } await request.flash({ type: 'danger', message: `Failed to update configuration` }); - logger.error({ msg: 'Failed to update configuration', err }); + request.logger.error({ msg: 'Failed to update configuration', err }); let hasDocumentStorePassword = !!(await settings.get('documentStorePassword')); @@ -7619,7 +7599,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} return h.redirect('/admin/config/document-store/chat'); } catch (err) { await request.flash({ type: 'danger', message: `Failed to update configuration` }); - logger.error({ msg: 'Failed to update configuration', err }); + request.logger.error({ msg: 'Failed to update configuration', err }); return h.view( 'config/document-store/index', @@ -7660,7 +7640,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')} } await request.flash({ type: 'danger', message: `Failed to update configuration` }); - logger.error({ msg: 'Failed to update configuration', err }); + request.logger.error({ msg: 'Failed to update configuration', err }); return h .view( @@ -7778,7 +7758,7 @@ return payload;` return h.redirect(`/admin/config/document-store/pre-processing`); } catch (err) { await request.flash({ type: 'danger', message: `Failed to update Document Store pre-processing rules` }); - logger.error({ msg: 'Failed to update Document Store pre-processing rules', err }); + request.logger.error({ msg: 'Failed to update Document Store pre-processing rules', err }); return h.view( 'config/document-store/pre-processing/index', @@ -7818,7 +7798,7 @@ return payload;` } await request.flash({ type: 'danger', message: `Failed to update Document Store pre-processing rules` }); - logger.error({ msg: 'Failed to update Document Store pre-processing rules', err }); + request.logger.error({ msg: 'Failed to update Document Store pre-processing rules', err }); return h .view( @@ -7938,7 +7918,7 @@ return payload;` path: '/admin/config/document-store/mappings/new', async handler(request, h) { try { - const { index, client } = await getESClient(logger); + const { index, client } = await getESClient(request.logger); if (!client) { return; } @@ -7989,7 +7969,7 @@ return payload;` } else { await request.flash({ type: 'danger', message: err.responseText || `Failed to create mapping` }); } - logger.error({ msg: 'Failed to create mapping', err }); + request.logger.error({ msg: 'Failed to create mapping', err }); return h.view( 'config/document-store/mappings/new', @@ -8026,7 +8006,7 @@ return payload;` } await request.flash({ type: 'danger', message: `Failed to create mapping` }); - logger.error({ msg: 'Failed to create mapping', err }); + request.logger.error({ msg: 'Failed to create mapping', err }); return h .view( @@ -8109,7 +8089,14 @@ return payload;` duration }; } catch (err) { - logger.error({ msg: 'Failed posting request', documentStoreUrl, documentStoreAuthEnabled, documentStoreUsername, command: 'info', err }); + request.logger.error({ + msg: 'Failed posting request', + documentStoreUrl, + documentStoreAuthEnabled, + documentStoreUsername, + command: 'info', + err + }); return { success: false, duration, @@ -8182,7 +8169,7 @@ return payload;` server.route({ method: 'POST', path: '/admin/config/network/reload', - async handler() { + async handler(request) { try { await updatePublicInterfaces(); @@ -8193,7 +8180,7 @@ return payload;` addresses: await listPublicInterfaces(localAddresses) }; } catch (err) { - logger.error({ msg: 'Failed loading public IP addresses', err }); + request.logger.error({ msg: 'Failed loading public IP addresses', err }); return { success: false, error: err.message @@ -8227,7 +8214,7 @@ return payload;` return h.redirect('/admin/config/network'); } catch (err) { await request.flash({ type: 'danger', message: `Failed to update configuration` }); - logger.error({ msg: 'Failed to update configuration', err }); + request.logger.error({ msg: 'Failed to update configuration', err }); let smtpStrategies = ADDRESS_STRATEGIES.map(entry => Object.assign({ selected: request.payload.smtpStrategy === entry.key }, entry)); let imapStrategies = ADDRESS_STRATEGIES.map(entry => Object.assign({ selected: request.payload.imapStrategy === entry.key }, entry)); @@ -8270,7 +8257,7 @@ return payload;` } await request.flash({ type: 'danger', message: `Failed to update configuration` }); - logger.error({ msg: 'Failed to update configuration', err }); + request.logger.error({ msg: 'Failed to update configuration', err }); let smtpStrategies = ADDRESS_STRATEGIES.map(entry => Object.assign({ selected: request.payload.smtpStrategy === entry.key }, entry)); let imapStrategies = ADDRESS_STRATEGIES.map(entry => Object.assign({ selected: request.payload.imapStrategy === entry.key }, entry)); @@ -8329,7 +8316,7 @@ return payload;` return h.redirect('/admin/config/network'); } catch (err) { await request.flash({ type: 'danger', message: `Failed to delete address` }); - logger.error({ msg: 'Failed to delete address', err, localAddress: request.payload.localAddress, remoteAddress: request.app.ip }); + request.logger.error({ msg: 'Failed to delete address', err, localAddress: request.payload.localAddress, remoteAddress: request.app.ip }); return h.redirect('/admin/config/network'); } }, @@ -8343,7 +8330,7 @@ return payload;` async failAction(request, h, err) { await request.flash({ type: 'danger', message: `Failed to delete address` }); - logger.error({ msg: 'Failed to delete address', err }); + request.logger.error({ msg: 'Failed to delete address', err }); return h.redirect('/admin/config/network').takeover(); }, @@ -8430,14 +8417,14 @@ return payload;` try { await call({ cmd: 'imapProxyReload' }); } catch (err) { - logger.error({ msg: 'Reload request failed', action: 'request_reload_imap_proxy', err }); + request.logger.error({ msg: 'Reload request failed', action: 'request_reload_imap_proxy', err }); } } return h.redirect('/admin/config/imap-proxy'); } catch (err) { await request.flash({ type: 'danger', message: `Failed to update configuration` }); - logger.error({ msg: 'Failed to update configuration', err }); + request.logger.error({ msg: 'Failed to update configuration', err }); let availableAddresses = new Set( Object.values(os.networkInterfaces()) @@ -8488,7 +8475,7 @@ return payload;` } await request.flash({ type: 'danger', message: `Failed to update configuration` }); - logger.error({ msg: 'Failed to update configuration', err }); + request.logger.error({ msg: 'Failed to update configuration', err }); let availableAddresses = new Set( Object.values(os.networkInterfaces()) @@ -8601,14 +8588,14 @@ return payload;` try { await call({ cmd: 'smtpReload' }); } catch (err) { - logger.error({ msg: 'Reload request failed', action: 'request_reload_smtp', err }); + request.logger.error({ msg: 'Reload request failed', action: 'request_reload_smtp', err }); } } return h.redirect('/admin/config/smtp'); } catch (err) { await request.flash({ type: 'danger', message: `Failed to update configuration` }); - logger.error({ msg: 'Failed to update configuration', err }); + request.logger.error({ msg: 'Failed to update configuration', err }); let availableAddresses = new Set( Object.values(os.networkInterfaces()) @@ -8659,7 +8646,7 @@ return payload;` } await request.flash({ type: 'danger', message: `Failed to update configuration` }); - logger.error({ msg: 'Failed to update configuration', err }); + request.logger.error({ msg: 'Failed to update configuration', err }); let availableAddresses = new Set( Object.values(os.networkInterfaces()) @@ -8718,7 +8705,7 @@ return payload;` label: certificateData.label }; } catch (err) { - logger.error({ msg: 'Failed to request syncing', err }); + request.logger.error({ msg: 'Failed to request syncing', err }); return { success: false, error: err.message @@ -8801,7 +8788,7 @@ return payload;` let account = request.payload.account; try { - logger.info({ msg: 'Request SMTP test', account }); + request.logger.info({ msg: 'Request SMTP test', account }); let accountObject = new Account({ redis, account, call, secret: await getSecret() }); @@ -8891,7 +8878,7 @@ ${now}`, }; } } catch (err) { - logger.error({ msg: 'Failed to request test account', err, account }); + request.logger.error({ msg: 'Failed to request test account', err, account }); return { success: false, error: err.message }; } }, @@ -8920,7 +8907,7 @@ ${now}`, let user = request.payload.user; try { - logger.info({ msg: 'Request SMTP test response', user }); + request.logger.info({ msg: 'Request SMTP test response', user }); let deliveryStatus = (await redis.hgetall(`${REDIS_PREFIX}test-send:${user}`)) || {}; if (deliveryStatus.success === 'false') { @@ -8985,7 +8972,7 @@ ${now}`, return testResponse; } catch (err) { - logger.error({ msg: 'Failed to request test response', err, user }); + request.logger.error({ msg: 'Failed to request test response', err, user }); return { status: 'error', error: err.message }; } }, @@ -9050,7 +9037,7 @@ ${now}`, }, async failAction(request, h, err) { - logger.error({ msg: 'Failed to validate request arguments', err }); + request.logger.error({ msg: 'Failed to validate request arguments', err }); let error = Boom.boomify(new Error(gt.gettext('Failed to validate request arguments')), { statusCode: 400 }); if (err.code) { error.output.payload.code = err.code; @@ -9152,7 +9139,7 @@ ${now}`, ); } catch (err) { await request.flash({ type: 'danger', message: gt.gettext('Failed to process request') }); - logger.error({ msg: 'Failed to process subscription request', err }); + request.logger.error({ msg: 'Failed to process subscription request', err }); return h.view( 'unsubscribe', @@ -9185,7 +9172,7 @@ ${now}`, } await request.flash({ type: 'danger', message: gt.gettext('Failed to process request') }); - logger.error({ msg: 'Failed to process subscription request', err }); + request.logger.error({ msg: 'Failed to process subscription request', err }); return h .view( @@ -9301,7 +9288,7 @@ ${now}`, return h.redirect('/admin/internals'); } catch (err) { await request.flash({ type: 'danger', message: `Failed to kill thread` }); - logger.error({ msg: 'Failed to kill thread', err, thread: request.payload.thread, remoteAddress: request.app.ip }); + request.logger.error({ msg: 'Failed to kill thread', err, thread: request.payload.thread, remoteAddress: request.app.ip }); return h.redirect('/admin/internals'); } }, @@ -9315,7 +9302,7 @@ ${now}`, async failAction(request, h, err) { await request.flash({ type: 'danger', message: `Failed to kill thread` }); - logger.error({ msg: 'Failed to kill thread', err }); + request.logger.error({ msg: 'Failed to kill thread', err }); return h.redirect('/admin/internals').takeover(); }, @@ -9353,7 +9340,7 @@ ${now}`, .code(200); } catch (err) { await request.flash({ type: 'danger', message: `Failed to generate snapshot` }); - logger.error({ msg: 'Failed to generate snapshot', err, thread: request.payload.thread, remoteAddress: request.app.ip }); + request.logger.error({ msg: 'Failed to generate snapshot', err, thread: request.payload.thread, remoteAddress: request.app.ip }); return h.redirect('/admin/internals'); } }, @@ -9367,7 +9354,7 @@ ${now}`, async failAction(request, h, err) { await request.flash({ type: 'danger', message: `Failed to generate snapshot` }); - logger.error({ msg: 'Failed to generate snapshot', err }); + request.logger.error({ msg: 'Failed to generate snapshot', err }); return h.redirect('/admin/internals').takeover(); }, diff --git a/lib/tools.js b/lib/tools.js index 52d862b65..4c14d9646 100644 --- a/lib/tools.js +++ b/lib/tools.js @@ -29,7 +29,7 @@ const uuid = require('uuid'); const mimeTypes = require('nodemailer/lib/mime-funcs/mime-types'); const { v3: murmurhash } = require('murmurhash'); const { compare: compareVersions, validate: validateVersion } = require('compare-versions'); -const { REDIS_PREFIX, TLS_DEFAULTS, FETCH_TIMEOUT } = require('./consts'); +const { REDIS_PREFIX, TLS_DEFAULTS, FETCH_TIMEOUT, MAX_FORM_TTL } = require('./consts'); const ipaddr = require('ipaddr.js'); const bullmqPackage = require('bullmq/package.json'); const v8 = require('node:v8'); @@ -765,7 +765,7 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV3QUiYsp13nD9suD1/ZkEXnuMoSg } const ignoreMailCertErrors = await settings.get('ignoreMailCertErrors'); - if (ignoreMailCertErrors && imapConfig?.tls?.rejectUnauthorized !== false) { + if (ignoreMailCertErrors && imapConfig && imapConfig.tls && imapConfig.tls.rejectUnauthorized !== false) { imapConfig.tls = imapConfig.tls || {}; imapConfig.tls.rejectUnauthorized = false; } @@ -868,7 +868,7 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV3QUiYsp13nD9suD1/ZkEXnuMoSg smtpConfig.forceAuth = true; const ignoreMailCertErrors = await settings.get('ignoreMailCertErrors'); - if (ignoreMailCertErrors && smtpConfig?.tls?.rejectUnauthorized !== false) { + if (ignoreMailCertErrors && smtpConfig && smtpConfig.tls && smtpConfig.tls.rejectUnauthorized !== false) { smtpConfig.tls = smtpConfig.tls || {}; smtpConfig.tls.rejectUnauthorized = false; } @@ -1308,7 +1308,9 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV3QUiYsp13nD9suD1/ZkEXnuMoSg notifyFrom: (opts.notifyFrom && opts.notifyFrom.toISOString()) || null, subconnections: opts.subconnections && opts.subconnections.length ? opts.subconnections : null, redirectUrl: opts.redirectUrl, - delegated: opts.delegated + delegated: opts.delegated, + n: opts.n, + t: opts.t }) ) ); @@ -1322,6 +1324,35 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV3QUiYsp13nD9suD1/ZkEXnuMoSg return { data: data.toString('base64url'), signature }; }, + async parseSignedFormData(redis, payload, gt) { + let data = Buffer.from(payload.data, 'base64url').toString(); + let serviceSecret = await settings.get('serviceSecret'); + if (serviceSecret) { + let hmac = createHmac('sha256', serviceSecret); + hmac.update(data); + if (hmac.digest('base64url') !== payload.sig) { + let error = Boom.boomify(new Error(gt.gettext('Signature validation failed')), { statusCode: 403 }); + throw error; + } + } + + data = JSON.parse(data); + + if (data.n && data.t) { + if (data.t < Date.now() - (MAX_FORM_TTL - 60 * 1000)) { + let error = Boom.boomify(new Error(gt.gettext('Invalid or expired account setup URL')), { statusCode: 403 }); + throw error; + } + const nonceSeen = await redis.exists(`${REDIS_PREFIX}account:form:${data.n}`); + if (nonceSeen) { + let error = Boom.boomify(new Error(gt.gettext('Invalid or expired account setup URL')), { statusCode: 403 }); + throw error; + } + } + + return data; + }, + async setLicense(licenseData, licenseFile) { await settings.setLicense(licenseData, licenseFile); }, diff --git a/package.json b/package.json index 81a67d6cf..c3c6ecfc8 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "build-dist": "npx pkg --compress Brotli package.json && npm install && node winconf.js", "build-dist-fast": "npx pkg --debug package.json && npm install && node winconf.js", "licenses": "license-checker --excludePackages emailengine-app --json | node license-table.js > static/licenses.html", - "gettext": "find ./views -name \"*.hbs\" -print0 | xargs -0 xgettext-template -L Handlebars -o translations/messages.pot --force-po && jsxgettext lib/routes-ui.js workers/api.js -j -o translations/messages.pot", + "gettext": "find ./views -name \"*.hbs\" -print0 | xargs -0 xgettext-template -L Handlebars -o translations/messages.pot --force-po && jsxgettext lib/routes-ui.js workers/api.js lib/tools.js -j -o translations/messages.pot", "prepare-docker": "echo \"EE_DOCKER_LEGACY=$EE_DOCKER_LEGACY\" >> system.env && cat system.env", "update": "rm -rf node_modules package-lock.json && ncu -u && npm install && ./copy-static-files.sh && npm run licenses" }, diff --git a/translations/de.mo b/translations/de.mo index 484de1932..387988e8b 100644 Binary files a/translations/de.mo and b/translations/de.mo differ diff --git a/translations/de.po b/translations/de.po index 8cc35868e..662204096 100644 --- a/translations/de.po +++ b/translations/de.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" -"POT-Creation-Date: 2023-08-15 13:09+0000\n" +"POT-Creation-Date: 2023-10-31 12:21+0000\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -18,10 +18,6 @@ msgid_plural "%d days" msgstr[0] "%d Tag" msgstr[1] "%d Tage" -#: views/redirect.hbs:1 -msgid "Click here to continue…" -msgstr "Klicken Sie hier, um fortzufahren…" - #: views/unsubscribe.hbs:1 views/unsubscribe.hbs:79 msgid "Unsubscribe" msgstr "Abmelden" @@ -65,6 +61,10 @@ msgstr "E-Mail-Adresse" msgid "Enter your email address" msgstr "Geben Sie Ihre E-Mail-Adresse ein" +#: views/redirect.hbs:1 +msgid "Click here to continue…" +msgstr "Klicken Sie hier, um fortzufahren…" + #: views/accounts/register/imap.hbs:11 msgid "Your name" msgstr "Ihr Name" @@ -88,6 +88,14 @@ msgstr "Passwort für Ihr Konto eingeben" msgid "Continue" msgstr "Weiter" +#: views/accounts/register/index.hbs:2 +msgid "Choose your email account provider" +msgstr "Wählen Sie Ihren E-Mail-Kontoanbieter" + +#: views/accounts/register/index.hbs:15 +msgid "Standard IMAP" +msgstr "Standard IMAP" + #: views/accounts/register/imap-server.hbs:18 msgid "IMAP" msgstr "IMAP" @@ -196,45 +204,41 @@ msgstr "Der SMTP-Server antwortete mit der folgenden Meldung:" msgid "HTTP error!" msgstr "HTTP-Fehler!" -#: views/accounts/register/index.hbs:2 -msgid "Choose your email account provider" -msgstr "Wählen Sie Ihren E-Mail-Kontoanbieter" - -#: views/accounts/register/index.hbs:15 -msgid "Standard IMAP" -msgstr "Standard IMAP" - -#: lib/routes-ui.js:557 +#: lib/routes-ui.js:603 msgid "Invalid API key for OpenAI" msgstr "Ungültiger API-Schlüssel für OpenAI" -#: lib/routes-ui.js:4193 lib/routes-ui.js:6143 lib/routes-ui.js:6154 +#: lib/routes-ui.js:4573 lib/routes-ui.js:6491 lib/routes-ui.js:6502 msgid "Server hostname was not found" msgstr "Server-Hostname wurde nicht gefunden" -#: lib/routes-ui.js:4196 lib/routes-ui.js:6146 lib/routes-ui.js:6157 +#: lib/routes-ui.js:4576 lib/routes-ui.js:6494 lib/routes-ui.js:6505 msgid "Invalid username or password" msgstr "Ungültiger Benutzername oder Passwort" -#: lib/routes-ui.js:4200 lib/routes-ui.js:6161 +#: lib/routes-ui.js:4580 lib/routes-ui.js:6509 msgid "TLS protocol error" msgstr "TLS-Protokollfehler" -#: lib/routes-ui.js:5907 lib/routes-ui.js:6018 lib/routes-ui.js:6246 -msgid "Signature validation failed" -msgstr "Signaturüberprüfung fehlgeschlagen" - -#: lib/routes-ui.js:5944 lib/routes-ui.js:5984 lib/routes-ui.js:8621 +#: lib/routes-ui.js:6303 lib/routes-ui.js:6343 lib/routes-ui.js:9037 msgid "Failed to validate request arguments" msgstr "Validierung der Anforderungsargumente fehlgeschlagen" -#: lib/routes-ui.js:6081 lib/routes-ui.js:6332 +#: lib/routes-ui.js:6429 lib/routes-ui.js:6683 msgid "Failed to process account" msgstr "Konto konnte nicht bearbeitet werden" -#: lib/routes-ui.js:8721 lib/routes-ui.js:8754 +#: lib/routes-ui.js:9137 lib/routes-ui.js:9170 msgid "Failed to process request" msgstr "Anfrage konnte nicht bearbeitet werden" +#: lib/tools.js:1334 +msgid "Signature validation failed" +msgstr "Signaturüberprüfung fehlgeschlagen" + +#: lib/tools.js:1343 lib/tools.js:1348 +msgid "Invalid or expired account setup URL" +msgstr "Ungültige oder abgelaufene URL für die Kontoeinrichtung" + #~ msgid "Unknown OAuth provider" #~ msgstr "Unbekannter OAuth-Anbieter" diff --git a/translations/en.mo b/translations/en.mo index 8243368a8..42760caec 100644 Binary files a/translations/en.mo and b/translations/en.mo differ diff --git a/translations/en.po b/translations/en.po index b900fa244..4d2ff6dfb 100644 --- a/translations/en.po +++ b/translations/en.po @@ -1,8 +1,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" -"POT-Creation-Date: 2023-08-15 13:09+0000\n" -"PO-Revision-Date: 2023-08-16 11:06+0300\n" +"POT-Creation-Date: 2023-10-31 12:21+0000\n" +"PO-Revision-Date: 2023-10-31 14:22+0200\n" "Last-Translator: \n" "Language-Team: \n" "Language: en\n" @@ -19,10 +19,6 @@ msgid_plural "%d days" msgstr[0] "" msgstr[1] "" -#: views/redirect.hbs:1 -msgid "Click here to continue…" -msgstr "" - #: views/unsubscribe.hbs:1 views/unsubscribe.hbs:79 msgid "Unsubscribe" msgstr "" @@ -63,6 +59,10 @@ msgstr "" msgid "Enter your email address" msgstr "" +#: views/redirect.hbs:1 +msgid "Click here to continue…" +msgstr "" + #: views/accounts/register/imap.hbs:11 msgid "Your name" msgstr "" @@ -86,6 +86,14 @@ msgstr "" msgid "Continue" msgstr "" +#: views/accounts/register/index.hbs:2 +msgid "Choose your email account provider" +msgstr "" + +#: views/accounts/register/index.hbs:15 +msgid "Standard IMAP" +msgstr "" + #: views/accounts/register/imap-server.hbs:18 msgid "IMAP" msgstr "" @@ -187,42 +195,38 @@ msgstr "" msgid "HTTP error!" msgstr "" -#: views/accounts/register/index.hbs:2 -msgid "Choose your email account provider" -msgstr "" - -#: views/accounts/register/index.hbs:15 -msgid "Standard IMAP" -msgstr "" - -#: lib/routes-ui.js:557 +#: lib/routes-ui.js:603 msgid "Invalid API key for OpenAI" msgstr "" -#: lib/routes-ui.js:4193 lib/routes-ui.js:6143 lib/routes-ui.js:6154 +#: lib/routes-ui.js:4573 lib/routes-ui.js:6491 lib/routes-ui.js:6502 msgid "Server hostname was not found" msgstr "" -#: lib/routes-ui.js:4196 lib/routes-ui.js:6146 lib/routes-ui.js:6157 +#: lib/routes-ui.js:4576 lib/routes-ui.js:6494 lib/routes-ui.js:6505 msgid "Invalid username or password" msgstr "" -#: lib/routes-ui.js:4200 lib/routes-ui.js:6161 +#: lib/routes-ui.js:4580 lib/routes-ui.js:6509 msgid "TLS protocol error" msgstr "" -#: lib/routes-ui.js:5907 lib/routes-ui.js:6018 lib/routes-ui.js:6246 -msgid "Signature validation failed" -msgstr "" - -#: lib/routes-ui.js:5944 lib/routes-ui.js:5984 lib/routes-ui.js:8621 +#: lib/routes-ui.js:6303 lib/routes-ui.js:6343 lib/routes-ui.js:9037 msgid "Failed to validate request arguments" msgstr "" -#: lib/routes-ui.js:6081 lib/routes-ui.js:6332 +#: lib/routes-ui.js:6429 lib/routes-ui.js:6683 msgid "Failed to process account" msgstr "" -#: lib/routes-ui.js:8721 lib/routes-ui.js:8754 +#: lib/routes-ui.js:9137 lib/routes-ui.js:9170 msgid "Failed to process request" msgstr "" + +#: lib/tools.js:1334 +msgid "Signature validation failed" +msgstr "" + +#: lib/tools.js:1343 lib/tools.js:1348 +msgid "Invalid or expired account setup URL" +msgstr "" diff --git a/translations/et.mo b/translations/et.mo index a1c1c832f..59e3a1fa9 100644 Binary files a/translations/et.mo and b/translations/et.mo differ diff --git a/translations/et.po b/translations/et.po index 5f8390304..bbb453632 100644 --- a/translations/et.po +++ b/translations/et.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" -"POT-Creation-Date: 2023-08-15 13:09+0000\n" +"POT-Creation-Date: 2023-10-31 12:21+0000\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -18,10 +18,6 @@ msgid_plural "%d days" msgstr[0] "%d päev" msgstr[1] "%d päeva" -#: views/redirect.hbs:1 -msgid "Click here to continue…" -msgstr "Kliki siin, et jätkata…" - #: views/unsubscribe.hbs:1 views/unsubscribe.hbs:79 msgid "Unsubscribe" msgstr "Loobu tellimusest" @@ -65,6 +61,10 @@ msgstr "E-posti aadress" msgid "Enter your email address" msgstr "Sisestage oma e-posti aadress" +#: views/redirect.hbs:1 +msgid "Click here to continue…" +msgstr "Kliki siin, et jätkata…" + #: views/accounts/register/imap.hbs:11 msgid "Your name" msgstr "Sinu nimi" @@ -88,6 +88,14 @@ msgstr "Sisestage oma konto parool" msgid "Continue" msgstr "Jätka" +#: views/accounts/register/index.hbs:2 +msgid "Choose your email account provider" +msgstr "Valige oma e-posti konto pakkuja" + +#: views/accounts/register/index.hbs:15 +msgid "Standard IMAP" +msgstr "Standardne IMAP" + #: views/accounts/register/imap-server.hbs:18 msgid "IMAP" msgstr "IMAP" @@ -195,45 +203,41 @@ msgstr "SMTP server vastas järgmise teatega:" msgid "HTTP error!" msgstr "HTTP viga!" -#: views/accounts/register/index.hbs:2 -msgid "Choose your email account provider" -msgstr "Valige oma e-posti konto pakkuja" - -#: views/accounts/register/index.hbs:15 -msgid "Standard IMAP" -msgstr "Standardne IMAP" - -#: lib/routes-ui.js:557 +#: lib/routes-ui.js:603 msgid "Invalid API key for OpenAI" msgstr "OpenAI jaoks vale API võti" -#: lib/routes-ui.js:4193 lib/routes-ui.js:6143 lib/routes-ui.js:6154 +#: lib/routes-ui.js:4573 lib/routes-ui.js:6491 lib/routes-ui.js:6502 msgid "Server hostname was not found" msgstr "Serverit ei leitud" -#: lib/routes-ui.js:4196 lib/routes-ui.js:6146 lib/routes-ui.js:6157 +#: lib/routes-ui.js:4576 lib/routes-ui.js:6494 lib/routes-ui.js:6505 msgid "Invalid username or password" msgstr "Vigane kasutajanimi või parool" -#: lib/routes-ui.js:4200 lib/routes-ui.js:6161 +#: lib/routes-ui.js:4580 lib/routes-ui.js:6509 msgid "TLS protocol error" msgstr "TLS protokolli viga" -#: lib/routes-ui.js:5907 lib/routes-ui.js:6018 lib/routes-ui.js:6246 -msgid "Signature validation failed" -msgstr "Päringu allkirja kontroll ebaõnnestus" - -#: lib/routes-ui.js:5944 lib/routes-ui.js:5984 lib/routes-ui.js:8621 +#: lib/routes-ui.js:6303 lib/routes-ui.js:6343 lib/routes-ui.js:9037 msgid "Failed to validate request arguments" msgstr "Päringu argumentide kontroll ebaõnnestus" -#: lib/routes-ui.js:6081 lib/routes-ui.js:6332 +#: lib/routes-ui.js:6429 lib/routes-ui.js:6683 msgid "Failed to process account" msgstr "Konto töötlemine ebaõnnestus" -#: lib/routes-ui.js:8721 lib/routes-ui.js:8754 +#: lib/routes-ui.js:9137 lib/routes-ui.js:9170 msgid "Failed to process request" msgstr "Päringu töötlemine ebaõnnestus" +#: lib/tools.js:1334 +msgid "Signature validation failed" +msgstr "Päringu allkirja kontroll ebaõnnestus" + +#: lib/tools.js:1343 lib/tools.js:1348 +msgid "Invalid or expired account setup URL" +msgstr "Kehtetu või aegunud konto seadistamise URL" + #~ msgid "Unknown OAuth provider" #~ msgstr "Tundmatu OAuth teenus" diff --git a/translations/fr.mo b/translations/fr.mo index 23fd1dd85..3db76a69c 100644 Binary files a/translations/fr.mo and b/translations/fr.mo differ diff --git a/translations/fr.po b/translations/fr.po index 98c878a6d..c6ebad22c 100644 --- a/translations/fr.po +++ b/translations/fr.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" -"POT-Creation-Date: 2023-08-15 13:09+0000\n" +"POT-Creation-Date: 2023-10-31 12:21+0000\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -18,10 +18,6 @@ msgid_plural "%d days" msgstr[0] "%d jour" msgstr[1] "%d jours" -#: views/redirect.hbs:1 -msgid "Click here to continue…" -msgstr "Cliquez sur ici pour continuer…" - #: views/unsubscribe.hbs:1 views/unsubscribe.hbs:79 msgid "Unsubscribe" msgstr "Désabonnement" @@ -62,6 +58,10 @@ msgstr "Adresse e-mail" msgid "Enter your email address" msgstr "Entrez votre adresse email" +#: views/redirect.hbs:1 +msgid "Click here to continue…" +msgstr "Cliquez sur ici pour continuer…" + #: views/accounts/register/imap.hbs:11 msgid "Your name" msgstr "Votre nom" @@ -85,6 +85,14 @@ msgstr "Saisissez le mot de passe de votre compte" msgid "Continue" msgstr "Continuer" +#: views/accounts/register/index.hbs:2 +msgid "Choose your email account provider" +msgstr "Choisissez votre fournisseur de compte de messagerie" + +#: views/accounts/register/index.hbs:15 +msgid "Standard IMAP" +msgstr "IMAP Standard" + #: views/accounts/register/imap-server.hbs:18 msgid "IMAP" msgstr "IMAP" @@ -193,45 +201,41 @@ msgstr "Le serveur SMTP a répondu avec le message suivant :" msgid "HTTP error!" msgstr "Erreur HTTP!" -#: views/accounts/register/index.hbs:2 -msgid "Choose your email account provider" -msgstr "Choisissez votre fournisseur de compte de messagerie" - -#: views/accounts/register/index.hbs:15 -msgid "Standard IMAP" -msgstr "IMAP Standard" - -#: lib/routes-ui.js:557 +#: lib/routes-ui.js:603 msgid "Invalid API key for OpenAI" msgstr "Clé API invalide pour OpenAI" -#: lib/routes-ui.js:4193 lib/routes-ui.js:6143 lib/routes-ui.js:6154 +#: lib/routes-ui.js:4573 lib/routes-ui.js:6491 lib/routes-ui.js:6502 msgid "Server hostname was not found" msgstr "Le nom d'hôte du serveur est introuvable" -#: lib/routes-ui.js:4196 lib/routes-ui.js:6146 lib/routes-ui.js:6157 +#: lib/routes-ui.js:4576 lib/routes-ui.js:6494 lib/routes-ui.js:6505 msgid "Invalid username or password" msgstr "Nom d'utilisateur ou mot de passe invalide" -#: lib/routes-ui.js:4200 lib/routes-ui.js:6161 +#: lib/routes-ui.js:4580 lib/routes-ui.js:6509 msgid "TLS protocol error" msgstr "Erreur de protocole TLS" -#: lib/routes-ui.js:5907 lib/routes-ui.js:6018 lib/routes-ui.js:6246 -msgid "Signature validation failed" -msgstr "La validation de la signature a échoué" - -#: lib/routes-ui.js:5944 lib/routes-ui.js:5984 lib/routes-ui.js:8621 +#: lib/routes-ui.js:6303 lib/routes-ui.js:6343 lib/routes-ui.js:9037 msgid "Failed to validate request arguments" msgstr "Impossible de valider les arguments de la demande" -#: lib/routes-ui.js:6081 lib/routes-ui.js:6332 +#: lib/routes-ui.js:6429 lib/routes-ui.js:6683 msgid "Failed to process account" msgstr "Échec du traitement du compte" -#: lib/routes-ui.js:8721 lib/routes-ui.js:8754 +#: lib/routes-ui.js:9137 lib/routes-ui.js:9170 msgid "Failed to process request" msgstr "Le traitement de la demande a échoué." +#: lib/tools.js:1334 +msgid "Signature validation failed" +msgstr "La validation de la signature a échoué" + +#: lib/tools.js:1343 lib/tools.js:1348 +msgid "Invalid or expired account setup URL" +msgstr "URL de configuration du compte invalide ou expirée" + #~ msgid "Unknown OAuth provider" #~ msgstr "Fournisseur OAuth inconnu" diff --git a/translations/messages.pot b/translations/messages.pot index 17e439e3e..df8e0f33f 100644 --- a/translations/messages.pot +++ b/translations/messages.pot @@ -1,7 +1,7 @@ msgid "" msgstr "" "Content-Type: text/plain; charset=ascii\n" -"POT-Creation-Date: 2023-08-15 13:09+0000\n" +"POT-Creation-Date: 2023-10-31 12:21+0000\n" #: views/config/license.hbs:48 msgid "%d day" @@ -9,10 +9,6 @@ msgid_plural "%d days" msgstr[0] "" msgstr[1] "" -#: views/redirect.hbs:1 -msgid "Click here to continue…" -msgstr "" - #: views/unsubscribe.hbs:1 #: views/unsubscribe.hbs:79 msgid "Unsubscribe" @@ -57,6 +53,10 @@ msgstr "" msgid "Enter your email address" msgstr "" +#: views/redirect.hbs:1 +msgid "Click here to continue…" +msgstr "" + #: views/accounts/register/imap.hbs:11 msgid "Your name" msgstr "" @@ -80,6 +80,14 @@ msgstr "" msgid "Continue" msgstr "" +#: views/accounts/register/index.hbs:2 +msgid "Choose your email account provider" +msgstr "" + +#: views/accounts/register/index.hbs:15 +msgid "Standard IMAP" +msgstr "" + #: views/accounts/register/imap-server.hbs:18 msgid "IMAP" msgstr "" @@ -181,53 +189,48 @@ msgstr "" msgid "HTTP error!" msgstr "" -#: views/accounts/register/index.hbs:2 -msgid "Choose your email account provider" -msgstr "" - -#: views/accounts/register/index.hbs:15 -msgid "Standard IMAP" -msgstr "" - -#: lib/routes-ui.js:557 +#: lib/routes-ui.js:603 msgid "Invalid API key for OpenAI" msgstr "" -#: lib/routes-ui.js:4193 -#: lib/routes-ui.js:6143 -#: lib/routes-ui.js:6154 +#: lib/routes-ui.js:4573 +#: lib/routes-ui.js:6491 +#: lib/routes-ui.js:6502 msgid "Server hostname was not found" msgstr "" -#: lib/routes-ui.js:4196 -#: lib/routes-ui.js:6146 -#: lib/routes-ui.js:6157 +#: lib/routes-ui.js:4576 +#: lib/routes-ui.js:6494 +#: lib/routes-ui.js:6505 msgid "Invalid username or password" msgstr "" -#: lib/routes-ui.js:4200 -#: lib/routes-ui.js:6161 +#: lib/routes-ui.js:4580 +#: lib/routes-ui.js:6509 msgid "TLS protocol error" msgstr "" -#: lib/routes-ui.js:5907 -#: lib/routes-ui.js:6018 -#: lib/routes-ui.js:6246 -msgid "Signature validation failed" -msgstr "" - -#: lib/routes-ui.js:5944 -#: lib/routes-ui.js:5984 -#: lib/routes-ui.js:8621 +#: lib/routes-ui.js:6303 +#: lib/routes-ui.js:6343 +#: lib/routes-ui.js:9037 msgid "Failed to validate request arguments" msgstr "" -#: lib/routes-ui.js:6081 -#: lib/routes-ui.js:6332 +#: lib/routes-ui.js:6429 +#: lib/routes-ui.js:6683 msgid "Failed to process account" msgstr "" -#: lib/routes-ui.js:8721 -#: lib/routes-ui.js:8754 +#: lib/routes-ui.js:9137 +#: lib/routes-ui.js:9170 msgid "Failed to process request" +msgstr "" + +#: lib/tools.js:1334 +msgid "Signature validation failed" +msgstr "" + +#: lib/tools.js:1343 +#: lib/tools.js:1348 +msgid "Invalid or expired account setup URL" msgstr "" \ No newline at end of file diff --git a/views/config/document-store/chat.hbs b/views/config/document-store/chat.hbs index b4faaa1df..8e35cab73 100644 --- a/views/config/document-store/chat.hbs +++ b/views/config/document-store/chat.hbs @@ -307,7 +307,7 @@ let liElm = document.createElement('li'); liElm.classList.add('list-group-item') let smallElm = document.createElement('small'); - smallElm.textContent = `• ${messageData.subject}`; + smallElm.textContent = `\u2022 ${messageData.subject}`; chatResponseMessagesElm.appendChild(liElm); liElm.appendChild(smallElm) } diff --git a/views/config/license.hbs b/views/config/license.hbs index dbb12b92d..31b1a1ec4 100644 --- a/views/config/license.hbs +++ b/views/config/license.hbs @@ -177,7 +177,8 @@
- +
diff --git a/workers/api.js b/workers/api.js index 13a04bb68..edeac0d37 100644 --- a/workers/api.js +++ b/workers/api.js @@ -115,7 +115,9 @@ const { FETCH_TIMEOUT, DEFAULT_MAX_BODY_SIZE, DEFAULT_EENGINE_TIMEOUT, - DEFAULT_MAX_ATTACHMENT_SIZE + DEFAULT_MAX_ATTACHMENT_SIZE, + MAX_FORM_TTL, + NONCE_BYTES } = consts; const { fetch: fetchCmd, Agent } = require('undici'); @@ -1583,6 +1585,20 @@ When making API calls remember that requests against the same account are queued }); let result = await accountObject.create(accountData); + if (accountMeta.n) { + // store nonce to prevent this URL to be reused + const keyName = `${REDIS_PREFIX}account:form:${accountMeta.n}`; + try { + await redis + .multi() + .set(keyName, (accountMeta.t || '0').toString()) + .expire(keyName, Math.floor(MAX_FORM_TTL / 1000)) + .exec(); + } catch (err) { + request.logger.error({ msg: 'Failed to set nonce for an account form request', err }); + } + } + let httpRedirectUrl; if (redirectUrl) { let serviceUrl = await settings.get('serviceUrl'); @@ -1966,7 +1982,7 @@ When making API calls remember that requests against the same account are queued // redirect to OAuth2 consent screen const oAuth2Client = await oauth2Apps.getClient(request.payload.oauth2.provider); - let nonce = crypto.randomBytes(12).toString('hex'); + const nonce = crypto.randomBytes(NONCE_BYTES).toString('base64url'); const accountData = request.payload; @@ -1982,7 +1998,7 @@ When making API calls remember that requests against the same account are queued await redis .multi() .set(`${REDIS_PREFIX}account:add:${nonce}`, JSON.stringify(accountData)) - .expire(`${REDIS_PREFIX}account:add:${nonce}`, 1 * 24 * 3600) + .expire(`${REDIS_PREFIX}account:add:${nonce}`, Math.floor(MAX_FORM_TTL / 1000)) .exec(); // Generate the url that will be used for the consent dialog. @@ -2130,7 +2146,10 @@ When making API calls remember that requests against the same account are queued notifyFrom: request.payload.notifyFrom, subconnections: request.payload.subconnections, redirectUrl: request.payload.redirectUrl, - delegated: request.payload.delegated + delegated: request.payload.delegated, + // identify request + n: crypto.randomBytes(NONCE_BYTES).toString('base64'), + t: Date.now() }); let serviceUrl = await settings.get('serviceUrl');