diff options
Diffstat (limited to 'server')
-rw-r--r-- | server/controllers/api/server/logs.ts | 41 | ||||
-rw-r--r-- | server/helpers/custom-validators/logs.ts | 36 | ||||
-rw-r--r-- | server/initializers/config.ts | 7 | ||||
-rw-r--r-- | server/initializers/constants.ts | 6 | ||||
-rw-r--r-- | server/middlewares/validators/logs.ts | 52 | ||||
-rw-r--r-- | server/tests/api/check-params/logs.ts | 59 | ||||
-rw-r--r-- | server/tests/api/server/logs.ts | 65 |
7 files changed, 251 insertions, 15 deletions
diff --git a/server/controllers/api/server/logs.ts b/server/controllers/api/server/logs.ts index 8aa4b7190..ed0aa6e8e 100644 --- a/server/controllers/api/server/logs.ts +++ b/server/controllers/api/server/logs.ts | |||
@@ -3,15 +3,29 @@ import { readdir, readFile } from 'fs-extra' | |||
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import { isArray } from '@server/helpers/custom-validators/misc' | 4 | import { isArray } from '@server/helpers/custom-validators/misc' |
5 | import { logger, mtimeSortFilesDesc } from '@server/helpers/logger' | 5 | import { logger, mtimeSortFilesDesc } from '@server/helpers/logger' |
6 | import { LogLevel } from '../../../../shared/models/server/log-level.type' | 6 | import { pick } from '@shared/core-utils' |
7 | import { ClientLogCreate, HttpStatusCode } from '@shared/models' | ||
8 | import { ServerLogLevel } from '../../../../shared/models/server/server-log-level.type' | ||
7 | import { UserRight } from '../../../../shared/models/users' | 9 | import { UserRight } from '../../../../shared/models/users' |
8 | import { CONFIG } from '../../../initializers/config' | 10 | import { CONFIG } from '../../../initializers/config' |
9 | import { AUDIT_LOG_FILENAME, LOG_FILENAME, MAX_LOGS_OUTPUT_CHARACTERS } from '../../../initializers/constants' | 11 | import { AUDIT_LOG_FILENAME, LOG_FILENAME, MAX_LOGS_OUTPUT_CHARACTERS } from '../../../initializers/constants' |
10 | import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares' | 12 | import { asyncMiddleware, authenticate, buildRateLimiter, ensureUserHasRight, optionalAuthenticate } from '../../../middlewares' |
11 | import { getAuditLogsValidator, getLogsValidator } from '../../../middlewares/validators/logs' | 13 | import { createClientLogValidator, getAuditLogsValidator, getLogsValidator } from '../../../middlewares/validators/logs' |
14 | |||
15 | const createClientLogRateLimiter = buildRateLimiter({ | ||
16 | windowMs: CONFIG.RATES_LIMIT.RECEIVE_CLIENT_LOG.WINDOW_MS, | ||
17 | max: CONFIG.RATES_LIMIT.RECEIVE_CLIENT_LOG.MAX | ||
18 | }) | ||
12 | 19 | ||
13 | const logsRouter = express.Router() | 20 | const logsRouter = express.Router() |
14 | 21 | ||
22 | logsRouter.post('/logs/client', | ||
23 | createClientLogRateLimiter, | ||
24 | optionalAuthenticate, | ||
25 | createClientLogValidator, | ||
26 | createClientLog | ||
27 | ) | ||
28 | |||
15 | logsRouter.get('/logs', | 29 | logsRouter.get('/logs', |
16 | authenticate, | 30 | authenticate, |
17 | ensureUserHasRight(UserRight.MANAGE_LOGS), | 31 | ensureUserHasRight(UserRight.MANAGE_LOGS), |
@@ -34,6 +48,21 @@ export { | |||
34 | 48 | ||
35 | // --------------------------------------------------------------------------- | 49 | // --------------------------------------------------------------------------- |
36 | 50 | ||
51 | function createClientLog (req: express.Request, res: express.Response) { | ||
52 | const logInfo = req.body as ClientLogCreate | ||
53 | |||
54 | const meta = { | ||
55 | tags: [ 'client' ], | ||
56 | username: res.locals.oauth?.token?.User?.username, | ||
57 | |||
58 | ...pick(logInfo, [ 'userAgent', 'stackTrace', 'meta', 'url' ]) | ||
59 | } | ||
60 | |||
61 | logger.log(logInfo.level, `Client log: ${logInfo.message}`, meta) | ||
62 | |||
63 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
64 | } | ||
65 | |||
37 | const auditLogNameFilter = generateLogNameFilter(AUDIT_LOG_FILENAME) | 66 | const auditLogNameFilter = generateLogNameFilter(AUDIT_LOG_FILENAME) |
38 | async function getAuditLogs (req: express.Request, res: express.Response) { | 67 | async function getAuditLogs (req: express.Request, res: express.Response) { |
39 | const output = await generateOutput({ | 68 | const output = await generateOutput({ |
@@ -63,7 +92,7 @@ async function generateOutput (options: { | |||
63 | startDateQuery: string | 92 | startDateQuery: string |
64 | endDateQuery?: string | 93 | endDateQuery?: string |
65 | 94 | ||
66 | level: LogLevel | 95 | level: ServerLogLevel |
67 | nameFilter: RegExp | 96 | nameFilter: RegExp |
68 | tagsOneOf?: string[] | 97 | tagsOneOf?: string[] |
69 | }) { | 98 | }) { |
@@ -104,7 +133,7 @@ async function getOutputFromFile (options: { | |||
104 | path: string | 133 | path: string |
105 | startDate: Date | 134 | startDate: Date |
106 | endDate: Date | 135 | endDate: Date |
107 | level: LogLevel | 136 | level: ServerLogLevel |
108 | currentSize: number | 137 | currentSize: number |
109 | tagsOneOf: Set<string> | 138 | tagsOneOf: Set<string> |
110 | }) { | 139 | }) { |
@@ -116,7 +145,7 @@ async function getOutputFromFile (options: { | |||
116 | 145 | ||
117 | let logTime: number | 146 | let logTime: number |
118 | 147 | ||
119 | const logsLevel: { [ id in LogLevel ]: number } = { | 148 | const logsLevel: { [ id in ServerLogLevel ]: number } = { |
120 | audit: -1, | 149 | audit: -1, |
121 | debug: 0, | 150 | debug: 0, |
122 | info: 1, | 151 | info: 1, |
diff --git a/server/helpers/custom-validators/logs.ts b/server/helpers/custom-validators/logs.ts index 0f266ed3b..41d45cbb2 100644 --- a/server/helpers/custom-validators/logs.ts +++ b/server/helpers/custom-validators/logs.ts | |||
@@ -1,14 +1,42 @@ | |||
1 | import validator from 'validator' | ||
2 | import { CONSTRAINTS_FIELDS } from '@server/initializers/constants' | ||
3 | import { ClientLogLevel, ServerLogLevel } from '@shared/models' | ||
1 | import { exists } from './misc' | 4 | import { exists } from './misc' |
2 | import { LogLevel } from '../../../shared/models/server/log-level.type' | ||
3 | 5 | ||
4 | const logLevels: LogLevel[] = [ 'debug', 'info', 'warn', 'error' ] | 6 | const serverLogLevels: Set<ServerLogLevel> = new Set([ 'debug', 'info', 'warn', 'error' ]) |
7 | const clientLogLevels: Set<ClientLogLevel> = new Set([ 'warn', 'error' ]) | ||
5 | 8 | ||
6 | function isValidLogLevel (value: any) { | 9 | function isValidLogLevel (value: any) { |
7 | return exists(value) && logLevels.includes(value) | 10 | return exists(value) && serverLogLevels.has(value) |
11 | } | ||
12 | |||
13 | function isValidClientLogMessage (value: any) { | ||
14 | return typeof value === 'string' && validator.isLength(value, CONSTRAINTS_FIELDS.LOGS.CLIENT_MESSAGE) | ||
15 | } | ||
16 | |||
17 | function isValidClientLogLevel (value: any) { | ||
18 | return exists(value) && clientLogLevels.has(value) | ||
19 | } | ||
20 | |||
21 | function isValidClientLogStackTrace (value: any) { | ||
22 | return typeof value === 'string' && validator.isLength(value, CONSTRAINTS_FIELDS.LOGS.CLIENT_STACK_TRACE) | ||
23 | } | ||
24 | |||
25 | function isValidClientLogMeta (value: any) { | ||
26 | return typeof value === 'string' && validator.isLength(value, CONSTRAINTS_FIELDS.LOGS.CLIENT_META) | ||
27 | } | ||
28 | |||
29 | function isValidClientLogUserAgent (value: any) { | ||
30 | return typeof value === 'string' && validator.isLength(value, CONSTRAINTS_FIELDS.LOGS.CLIENT_USER_AGENT) | ||
8 | } | 31 | } |
9 | 32 | ||
10 | // --------------------------------------------------------------------------- | 33 | // --------------------------------------------------------------------------- |
11 | 34 | ||
12 | export { | 35 | export { |
13 | isValidLogLevel | 36 | isValidLogLevel, |
37 | isValidClientLogMessage, | ||
38 | isValidClientLogStackTrace, | ||
39 | isValidClientLogMeta, | ||
40 | isValidClientLogLevel, | ||
41 | isValidClientLogUserAgent | ||
14 | } | 42 | } |
diff --git a/server/initializers/config.ts b/server/initializers/config.ts index 0943ffe2d..ba0f756ef 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts | |||
@@ -149,6 +149,10 @@ const CONFIG = { | |||
149 | WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.login.window')), | 149 | WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.login.window')), |
150 | MAX: config.get<number>('rates_limit.login.max') | 150 | MAX: config.get<number>('rates_limit.login.max') |
151 | }, | 151 | }, |
152 | RECEIVE_CLIENT_LOG: { | ||
153 | WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.receive_client_log.window')), | ||
154 | MAX: config.get<number>('rates_limit.receive_client_log.max') | ||
155 | }, | ||
152 | ASK_SEND_EMAIL: { | 156 | ASK_SEND_EMAIL: { |
153 | WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.ask_send_email.window')), | 157 | WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.ask_send_email.window')), |
154 | MAX: config.get<number>('rates_limit.ask_send_email.max') | 158 | MAX: config.get<number>('rates_limit.ask_send_email.max') |
@@ -165,7 +169,8 @@ const CONFIG = { | |||
165 | ANONYMIZE_IP: config.get<boolean>('log.anonymize_ip'), | 169 | ANONYMIZE_IP: config.get<boolean>('log.anonymize_ip'), |
166 | LOG_PING_REQUESTS: config.get<boolean>('log.log_ping_requests'), | 170 | LOG_PING_REQUESTS: config.get<boolean>('log.log_ping_requests'), |
167 | LOG_TRACKER_UNKNOWN_INFOHASH: config.get<boolean>('log.log_tracker_unknown_infohash'), | 171 | LOG_TRACKER_UNKNOWN_INFOHASH: config.get<boolean>('log.log_tracker_unknown_infohash'), |
168 | PRETTIFY_SQL: config.get<boolean>('log.prettify_sql') | 172 | PRETTIFY_SQL: config.get<boolean>('log.prettify_sql'), |
173 | ACCEPT_CLIENT_LOG: config.get<boolean>('log.accept_client_log') | ||
169 | }, | 174 | }, |
170 | OPEN_TELEMETRY: { | 175 | OPEN_TELEMETRY: { |
171 | METRICS: { | 176 | METRICS: { |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 009f878fc..8cb4d5f4a 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -365,6 +365,12 @@ const CONSTRAINTS_FIELDS = { | |||
365 | VIDEO_STUDIO: { | 365 | VIDEO_STUDIO: { |
366 | TASKS: { min: 1, max: 10 }, // Number of tasks | 366 | TASKS: { min: 1, max: 10 }, // Number of tasks |
367 | CUT_TIME: { min: 0 } // Value | 367 | CUT_TIME: { min: 0 } // Value |
368 | }, | ||
369 | LOGS: { | ||
370 | CLIENT_MESSAGE: { min: 1, max: 1000 }, // Length | ||
371 | CLIENT_STACK_TRACE: { min: 1, max: 5000 }, // Length | ||
372 | CLIENT_META: { min: 1, max: 5000 }, // Length | ||
373 | CLIENT_USER_AGENT: { min: 1, max: 200 } // Length | ||
368 | } | 374 | } |
369 | } | 375 | } |
370 | 376 | ||
diff --git a/server/middlewares/validators/logs.ts b/server/middlewares/validators/logs.ts index 901d8ca64..324ba6915 100644 --- a/server/middlewares/validators/logs.ts +++ b/server/middlewares/validators/logs.ts | |||
@@ -1,11 +1,56 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { query } from 'express-validator' | 2 | import { body, query } from 'express-validator' |
3 | import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc' | ||
3 | import { isStringArray } from '@server/helpers/custom-validators/search' | 4 | import { isStringArray } from '@server/helpers/custom-validators/search' |
4 | import { isValidLogLevel } from '../../helpers/custom-validators/logs' | 5 | import { CONFIG } from '@server/initializers/config' |
6 | import { HttpStatusCode } from '@shared/models' | ||
7 | import { | ||
8 | isValidClientLogLevel, | ||
9 | isValidClientLogMessage, | ||
10 | isValidClientLogMeta, | ||
11 | isValidClientLogStackTrace, | ||
12 | isValidClientLogUserAgent, | ||
13 | isValidLogLevel | ||
14 | } from '../../helpers/custom-validators/logs' | ||
5 | import { isDateValid, toArray } from '../../helpers/custom-validators/misc' | 15 | import { isDateValid, toArray } from '../../helpers/custom-validators/misc' |
6 | import { logger } from '../../helpers/logger' | 16 | import { logger } from '../../helpers/logger' |
7 | import { areValidationErrors } from './shared' | 17 | import { areValidationErrors } from './shared' |
8 | 18 | ||
19 | const createClientLogValidator = [ | ||
20 | body('message') | ||
21 | .custom(isValidClientLogMessage).withMessage('Should have a valid log message'), | ||
22 | |||
23 | body('url') | ||
24 | .custom(isUrlValid).withMessage('Should have a valid log url'), | ||
25 | |||
26 | body('level') | ||
27 | .custom(isValidClientLogLevel).withMessage('Should have a valid log message'), | ||
28 | |||
29 | body('stackTrace') | ||
30 | .optional() | ||
31 | .custom(isValidClientLogStackTrace).withMessage('Should have a valid log stack trace'), | ||
32 | |||
33 | body('meta') | ||
34 | .optional() | ||
35 | .custom(isValidClientLogMeta).withMessage('Should have a valid log meta'), | ||
36 | |||
37 | body('userAgent') | ||
38 | .optional() | ||
39 | .custom(isValidClientLogUserAgent).withMessage('Should have a valid log user agent'), | ||
40 | |||
41 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
42 | logger.debug('Checking createClientLogValidator parameters.', { parameters: req.query }) | ||
43 | |||
44 | if (CONFIG.LOG.ACCEPT_CLIENT_LOG !== true) { | ||
45 | return res.sendStatus(HttpStatusCode.FORBIDDEN_403) | ||
46 | } | ||
47 | |||
48 | if (areValidationErrors(req, res)) return | ||
49 | |||
50 | return next() | ||
51 | } | ||
52 | ] | ||
53 | |||
9 | const getLogsValidator = [ | 54 | const getLogsValidator = [ |
10 | query('startDate') | 55 | query('startDate') |
11 | .custom(isDateValid).withMessage('Should have a start date that conforms to ISO 8601'), | 56 | .custom(isDateValid).withMessage('Should have a start date that conforms to ISO 8601'), |
@@ -49,5 +94,6 @@ const getAuditLogsValidator = [ | |||
49 | 94 | ||
50 | export { | 95 | export { |
51 | getLogsValidator, | 96 | getLogsValidator, |
52 | getAuditLogsValidator | 97 | getAuditLogsValidator, |
98 | createClientLogValidator | ||
53 | } | 99 | } |
diff --git a/server/tests/api/check-params/logs.ts b/server/tests/api/check-params/logs.ts index 970671c15..fa67408b7 100644 --- a/server/tests/api/check-params/logs.ts +++ b/server/tests/api/check-params/logs.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { cleanupTests, createSingleServer, makeGetRequest, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' | 4 | import { expect } from 'chai' |
5 | import { HttpStatusCode } from '@shared/models' | 5 | import { HttpStatusCode } from '@shared/models' |
6 | import { cleanupTests, createSingleServer, makeGetRequest, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' | ||
6 | 7 | ||
7 | describe('Test logs API validators', function () { | 8 | describe('Test logs API validators', function () { |
8 | const path = '/api/v1/server/logs' | 9 | const path = '/api/v1/server/logs' |
@@ -95,6 +96,62 @@ describe('Test logs API validators', function () { | |||
95 | }) | 96 | }) |
96 | }) | 97 | }) |
97 | 98 | ||
99 | describe('When creating client logs', function () { | ||
100 | const base = { | ||
101 | level: 'warn' as 'warn', | ||
102 | message: 'my super message', | ||
103 | url: 'https://example.com/toto' | ||
104 | } | ||
105 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 | ||
106 | |||
107 | it('Should fail with an invalid level', async function () { | ||
108 | await server.logs.createLogClient({ payload: { ...base, level: '' as any }, expectedStatus }) | ||
109 | await server.logs.createLogClient({ payload: { ...base, level: undefined }, expectedStatus }) | ||
110 | await server.logs.createLogClient({ payload: { ...base, level: 'toto' as any }, expectedStatus }) | ||
111 | }) | ||
112 | |||
113 | it('Should fail with an invalid message', async function () { | ||
114 | await server.logs.createLogClient({ payload: { ...base, message: undefined }, expectedStatus }) | ||
115 | await server.logs.createLogClient({ payload: { ...base, message: '' }, expectedStatus }) | ||
116 | await server.logs.createLogClient({ payload: { ...base, message: 'm'.repeat(2500) }, expectedStatus }) | ||
117 | }) | ||
118 | |||
119 | it('Should fail with an invalid url', async function () { | ||
120 | await server.logs.createLogClient({ payload: { ...base, url: undefined }, expectedStatus }) | ||
121 | await server.logs.createLogClient({ payload: { ...base, url: 'toto' }, expectedStatus }) | ||
122 | }) | ||
123 | |||
124 | it('Should fail with an invalid stackTrace', async function () { | ||
125 | await server.logs.createLogClient({ payload: { ...base, stackTrace: 's'.repeat(10000) }, expectedStatus }) | ||
126 | }) | ||
127 | |||
128 | it('Should fail with an invalid userAgent', async function () { | ||
129 | await server.logs.createLogClient({ payload: { ...base, userAgent: 's'.repeat(500) }, expectedStatus }) | ||
130 | }) | ||
131 | |||
132 | it('Should fail with an invalid meta', async function () { | ||
133 | await server.logs.createLogClient({ payload: { ...base, meta: 's'.repeat(10000) }, expectedStatus }) | ||
134 | }) | ||
135 | |||
136 | it('Should succeed with the correct params', async function () { | ||
137 | await server.logs.createLogClient({ payload: { ...base, stackTrace: 'stackTrace', meta: '{toto}', userAgent: 'userAgent' } }) | ||
138 | }) | ||
139 | |||
140 | it('Should rate limit log creation', async function () { | ||
141 | let fail = false | ||
142 | |||
143 | for (let i = 0; i < 10; i++) { | ||
144 | try { | ||
145 | await server.logs.createLogClient({ token: null, payload: base }) | ||
146 | } catch { | ||
147 | fail = true | ||
148 | } | ||
149 | } | ||
150 | |||
151 | expect(fail).to.be.true | ||
152 | }) | ||
153 | }) | ||
154 | |||
98 | after(async function () { | 155 | after(async function () { |
99 | await cleanupTests([ server ]) | 156 | await cleanupTests([ server ]) |
100 | }) | 157 | }) |
diff --git a/server/tests/api/server/logs.ts b/server/tests/api/server/logs.ts index 697f10337..ed7555fd7 100644 --- a/server/tests/api/server/logs.ts +++ b/server/tests/api/server/logs.ts | |||
@@ -2,6 +2,7 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { HttpStatusCode } from '@shared/models' | ||
5 | import { | 6 | import { |
6 | cleanupTests, | 7 | cleanupTests, |
7 | createSingleServer, | 8 | createSingleServer, |
@@ -198,6 +199,70 @@ describe('Test logs', function () { | |||
198 | }) | 199 | }) |
199 | }) | 200 | }) |
200 | 201 | ||
202 | describe('When creating log from the client', function () { | ||
203 | |||
204 | it('Should create a warn client log', async function () { | ||
205 | const now = new Date() | ||
206 | |||
207 | await server.logs.createLogClient({ | ||
208 | payload: { | ||
209 | level: 'warn', | ||
210 | url: 'http://example.com', | ||
211 | message: 'my super client message' | ||
212 | }, | ||
213 | token: null | ||
214 | }) | ||
215 | |||
216 | const body = await logsCommand.getLogs({ startDate: now }) | ||
217 | const logsString = JSON.stringify(body) | ||
218 | |||
219 | expect(logsString.includes('my super client message')).to.be.true | ||
220 | }) | ||
221 | |||
222 | it('Should create an error authenticated client log', async function () { | ||
223 | const now = new Date() | ||
224 | |||
225 | await server.logs.createLogClient({ | ||
226 | payload: { | ||
227 | url: 'https://example.com/page1', | ||
228 | level: 'error', | ||
229 | message: 'my super client message 2', | ||
230 | userAgent: 'super user agent', | ||
231 | meta: '{hello}', | ||
232 | stackTrace: 'super stack trace' | ||
233 | } | ||
234 | }) | ||
235 | |||
236 | const body = await logsCommand.getLogs({ startDate: now }) | ||
237 | const logsString = JSON.stringify(body) | ||
238 | |||
239 | expect(logsString.includes('my super client message 2')).to.be.true | ||
240 | expect(logsString.includes('super user agent')).to.be.true | ||
241 | expect(logsString.includes('super stack trace')).to.be.true | ||
242 | expect(logsString.includes('{hello}')).to.be.true | ||
243 | expect(logsString.includes('https://example.com/page1')).to.be.true | ||
244 | }) | ||
245 | |||
246 | it('Should refuse to create client logs', async function () { | ||
247 | await server.kill() | ||
248 | |||
249 | await server.run({ | ||
250 | log: { | ||
251 | accept_client_log: false | ||
252 | } | ||
253 | }) | ||
254 | |||
255 | await server.logs.createLogClient({ | ||
256 | payload: { | ||
257 | level: 'warn', | ||
258 | url: 'http://example.com', | ||
259 | message: 'my super client message' | ||
260 | }, | ||
261 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
262 | }) | ||
263 | }) | ||
264 | }) | ||
265 | |||
201 | after(async function () { | 266 | after(async function () { |
202 | await cleanupTests([ server ]) | 267 | await cleanupTests([ server ]) |
203 | }) | 268 | }) |