Skip to content

Commit

Permalink
Merge pull request #2 from linagora/hashes-rate-limit
Browse files Browse the repository at this point in the history
feat: hashes rate limit for "lookup" endpoints
  • Loading branch information
jcabannes authored Jan 5, 2024
2 parents 488aeb3 + d192040 commit c8cf3de
Show file tree
Hide file tree
Showing 18 changed files with 1,272 additions and 956 deletions.
1,193 changes: 567 additions & 626 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"landing"
],
"scripts": {
"build": "npm run softClean && lerna run build --skip-nx-cache",
"build": "npm run softClean && lerna run build",
"clean": "rimraf packages/*/dist packages/*/coverage ./node_modules/.cache/nx packages/*/example/*.js*",
"doc": "node documentation/swagger.cjs",
"format:check": "prettier --check .",
Expand All @@ -33,7 +33,7 @@
"lint-fix": "eslint --fix packages/*/src --ext .ts",
"prepare": "husky install",
"softClean": "rimraf packages/*/dist",
"test": "lerna run test --skip-nx-cache"
"test": "lerna run test"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^25.0.7",
Expand Down
1 change: 1 addition & 0 deletions packages/federation-server/server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const conf = {
database_name: process.env.DATABASE_NAME,
database_user: process.env.DATABASE_USER,
database_password: process.env.DATABASE_PASSWORD,
hashes_rate_limit: process.env.HASHES_RATE_LIMIT,
is_federation_server: true,
ldap_base: process.env.LDAP_BASE,
ldap_filter: process.env.LDAP_FILTER,
Expand Down
4 changes: 4 additions & 0 deletions packages/federation-server/src/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
"database_host": "./tokens.db",
"database_name": "",
"database_password": "",
"database_ssl": false,
"database_user": "",
"database_vacuum_delay": 3600,
"hashes_rate_limit": 100,
"is_federation_server": true,
"key_delay": 3600,
"keys_depth": 5,
Expand All @@ -23,6 +25,7 @@
"matrix_database_host": null,
"matrix_database_name": null,
"matrix_database_password": null,
"matrix_database_ssl": false,
"matrix_database_user": null,
"pepperCron": "0 0 * * *",
"policies": null,
Expand All @@ -40,6 +43,7 @@
"userdb_host": "./tokens.db",
"userdb_name": "",
"userdb_password": "",
"userdb_ssl": false,
"userdb_user": "",
"template_dir": "./templates",
"trust_x_forwarded_for": false,
Expand Down
78 changes: 68 additions & 10 deletions packages/federation-server/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1875,7 +1875,7 @@ describe('Federation server', () => {
)
})

it('should send an error if "addresses" is not a string array', async () => {
it('should send an error if "addresses" is not an array', async () => {
const response = await request(app)
.post('/_matrix/identity/v2/lookup')
.set('Accept', 'application/json')
Expand All @@ -1901,12 +1901,7 @@ describe('Federation server', () => {
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${authToken}`)
.send({
addresses: [
'@dwho:example.com',
'@rtyler:example.com',
2,
'@msmith:example.com'
],
addresses: ['hash1', 'hash2', 2, 'hash3'],
algorithm: 'sha256',
pepper: 'test_pepper'
})
Expand All @@ -1921,6 +1916,27 @@ describe('Federation server', () => {
)
})

it('should send an error if exceeds hashes limit', async () => {
const response = await request(app)
.post('/_matrix/identity/v2/lookup')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${authToken}`)
.send({
addresses: Array.from({ length: 101 }, (_, i) => `hash${i}`),
algorithm: 'sha256',
pepper: 'test_pepper'
})

expect(response.statusCode).toEqual(400)
expect(JSON.stringify(response.body)).toEqual(
JSON.stringify({
errcode: 'M_INVALID_PARAM',
error:
'Error field: Adresses limit of 100 exceeded (property: addresses)'
})
)
})

it('should send an error if getting hashes from db fails', async () => {
jest
.spyOn(federationServer.db, 'get')
Expand All @@ -1934,7 +1950,7 @@ describe('Federation server', () => {
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${authToken}`)
.send({
addresses: ['@dwho:example.com', '@rtyler:example.com'],
addresses: ['hash1', 'hash2'],
algorithm: 'sha256',
pepper: 'test_pepper'
})
Expand All @@ -1961,7 +1977,7 @@ describe('Federation server', () => {
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${authToken}`)
.send({
addresses: ['@dwho:example.com', '@rtyler:example.com'],
addresses: ['hash1', 'hash2'],
algorithm: 'sha256',
pepper: 'test_pepper'
})
Expand Down Expand Up @@ -1990,7 +2006,7 @@ describe('Federation server', () => {
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${authToken}`)
.send({
addresses: ['@dwho:example.com', '@rtyler:example.com'],
addresses: ['hash1', 'hash2'],
algorithm: 'sha256',
pepper: 'test_pepper'
})
Expand Down Expand Up @@ -2161,6 +2177,48 @@ describe('Federation server', () => {
)
})

it('should send an error if "mappings" values is not an array', async () => {
const response = await request(app)
.post('/_matrix/identity/v2/lookups')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${authToken}`)
.send({
mappings: { 'test.example.com': 2 },
algorithm: 'sha256',
pepper: 'test_pepper'
})

expect(response.statusCode).toEqual(400)
expect(JSON.stringify(response.body)).toEqual(
JSON.stringify({
errcode: 'M_INVALID_PARAM',
error:
'Error field: Mappings object values are not string arrays (property: mappings)'
})
)
})

it('should send an error if one address is not a string', async () => {
const response = await request(app)
.post('/_matrix/identity/v2/lookups')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${authToken}`)
.send({
mappings: { 'test.example.com': ['hash1', 'hash2', 2, 'hash3'] },
algorithm: 'sha256',
pepper: 'test_pepper'
})

expect(response.statusCode).toEqual(400)
expect(JSON.stringify(response.body)).toEqual(
JSON.stringify({
errcode: 'M_INVALID_PARAM',
error:
'Error field: Mappings object values are not string arrays (property: mappings)'
})
)
})

it('should send an error if getting pepper in keys table fails', async () => {
jest
.spyOn(federationServer.db, 'get')
Expand Down
34 changes: 24 additions & 10 deletions packages/federation-server/src/middlewares/validation.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { body } from 'express-validator'
import { body, type ValidationChain } from 'express-validator'

export const commonValidators = [
body('algorithm').exists().isString(),
body('pepper').exists().isString()
]

export const lookupValidator = body('addresses')
.exists()
.isArray()
.custom((value, { req }) => {
if ((value as any[]).some((address) => typeof address !== 'string')) {
throw new Error('One of the address is not a string')
}
return true
})
export const lookupValidator = (hashesLimit: number): ValidationChain =>
body('addresses')
.exists()
.isArray()
.custom((value, { req }) => {
if ((value as any[]).some((address) => typeof address !== 'string')) {
throw new Error('One of the address is not a string')
} else if ((value as string[]).length > hashesLimit) {
throw new Error(`Adresses limit of ${hashesLimit} exceeded`)
}
return true
})

export const lookupsValidator = [
body('pepper').exists().isString(),
Expand All @@ -24,6 +27,17 @@ export const lookupsValidator = [
if (Object.keys(value).length > 1) {
throw new Error('Only one server address is allowed')
}
if (
Object.keys(value).length === 1 &&
!(
Array.isArray(Object.values(value)[0]) &&
(Object.values(value)[0] as any[]).every(
(hash) => typeof hash === 'string'
)
)
) {
throw new Error('Mappings object values are not string arrays')
}
return true
})
]
2 changes: 1 addition & 1 deletion packages/federation-server/src/routes/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export default (
logger
),
...commonValidators,
lookupValidator,
lookupValidator(conf.hashes_rate_limit as number),
lookup(conf, db),
errorMiddleware
)
Expand Down
1 change: 1 addition & 0 deletions packages/matrix-identity-server/src/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"database_user": "",
"database_vacuum_delay": 3600,
"federation_servers": null,
"hashes_rate_limit": 100,
"is_federation_server": false,
"key_delay": 3600,
"keys_depth": 5,
Expand Down
8 changes: 5 additions & 3 deletions packages/matrix-identity-server/src/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ type Update = (
type Get = (
table: Collections,
fields: string[],
filterFields: Record<string, string | number | Array<string | number>>
filterFields: Record<string, string | number | Array<string | number>>,
order?: string
) => Promise<DbGetResult>
type GetCount = (
table: Collections,
Expand Down Expand Up @@ -202,9 +203,10 @@ class IdentityServerDb implements IdDbBackend {
getHigherThan(
table: Collections,
fields: string[],
filterFields: Record<string, string | number | Array<string | number>>
filterFields: Record<string, string | number | Array<string | number>>,
order?: string
): Promise<DbGetResult> {
return this.db.getHigherThan(table, fields, filterFields)
return this.db.getHigherThan(table, fields, filterFields, order)
}

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async
Expand Down
Loading

0 comments on commit c8cf3de

Please sign in to comment.