浏览代码

feat(syncing-server): add traffic abuse checks (#1014)

Karol Sójko 1 年之前
父节点
当前提交
b7173346d2

+ 2 - 2
packages/syncing-server/bin/statistics.ts

@@ -28,7 +28,7 @@ const sendStatistics = async (
       const dateNMinutesAgo = timer.getUTCDateNMinutesAgo(minutesToProcess - i)
       const dateNMinutesAgo = timer.getUTCDateNMinutesAgo(minutesToProcess - i)
       const timestamp = timer.convertDateToMicroseconds(dateNMinutesAgo)
       const timestamp = timer.convertDateToMicroseconds(dateNMinutesAgo)
 
 
-      const statistics = await metricsStore.getStatistics(
+      const statistics = await metricsStore.getMetricsSummary(
         metricToProcess,
         metricToProcess,
         timestamp,
         timestamp,
         timestamp + Time.MicrosecondsInAMinute,
         timestamp + Time.MicrosecondsInAMinute,
@@ -64,7 +64,7 @@ const sendStatistics = async (
       const dateNMinutesAgo = timer.getUTCDateNMinutesAgo(minutesToProcess - i)
       const dateNMinutesAgo = timer.getUTCDateNMinutesAgo(minutesToProcess - i)
       const timestamp = timer.convertDateToMicroseconds(dateNMinutesAgo)
       const timestamp = timer.convertDateToMicroseconds(dateNMinutesAgo)
 
 
-      const statistics = await metricsStore.getUserBasedStatistics(metricToProcess, timestamp)
+      const statistics = await metricsStore.getUserBasedMetricsSummary(metricToProcess, timestamp)
 
 
       if (statistics.sampleCount === 0) {
       if (statistics.sampleCount === 0) {
         continue
         continue

+ 43 - 0
packages/syncing-server/src/Bootstrap/Container.ts

@@ -166,6 +166,7 @@ import { SendEventToClients } from '../Domain/UseCase/Syncing/SendEventToClients
 import { MetricsStoreInterface } from '../Domain/Metrics/MetricsStoreInterface'
 import { MetricsStoreInterface } from '../Domain/Metrics/MetricsStoreInterface'
 import { RedisMetricStore } from '../Infra/Redis/RedisMetricStore'
 import { RedisMetricStore } from '../Infra/Redis/RedisMetricStore'
 import { DummyMetricStore } from '../Infra/Dummy/DummyMetricStore'
 import { DummyMetricStore } from '../Infra/Dummy/DummyMetricStore'
+import { CheckForTrafficAbuse } from '../Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuse'
 
 
 export class ContainerConfigLoader {
 export class ContainerConfigLoader {
   private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
   private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
@@ -472,6 +473,33 @@ export class ContainerConfigLoader {
       })
       })
 
 
     // env vars
     // env vars
+    container
+      .bind(TYPES.Sync_STRICT_ABUSE_PROTECTION)
+      .toConstantValue(env.get('STRICT_ABUSE_PROTECTION', true) === 'true')
+    container
+      .bind(TYPES.Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD)
+      .toConstantValue(
+        env.get('ITEM_OPERATIONS_ABUSE_THRESHOLD', true) ? +env.get('ITEM_OPERATIONS_ABUSE_THRESHOLD', true) : 500,
+      )
+    container
+      .bind(TYPES.Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES)
+      .toConstantValue(
+        env.get('ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES', true)
+          ? +env.get('ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES', true)
+          : 5,
+      )
+    container
+      .bind(TYPES.Sync_PAYLOAD_SIZE_ABUSE_THRESHOLD)
+      .toConstantValue(
+        env.get('PAYLOAD_SIZE_ABUSE_THRESHOLD', true) ? +env.get('PAYLOAD_SIZE_ABUSE_THRESHOLD', true) : 20_000_000,
+      )
+    container
+      .bind(TYPES.Sync_PAYLOAD_SIZE_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES)
+      .toConstantValue(
+        env.get('PAYLOAD_SIZE_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES', true)
+          ? +env.get('PAYLOAD_SIZE_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES', true)
+          : 5,
+      )
     container.bind(TYPES.Sync_AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
     container.bind(TYPES.Sync_AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
     container
     container
       .bind(TYPES.Sync_REVISIONS_FREQUENCY)
       .bind(TYPES.Sync_REVISIONS_FREQUENCY)
@@ -933,6 +961,14 @@ export class ContainerConfigLoader {
           context.container.get(TYPES.Sync_SyncResponseFactory20200115),
           context.container.get(TYPES.Sync_SyncResponseFactory20200115),
         )
         )
       })
       })
+    container
+      .bind<CheckForTrafficAbuse>(TYPES.Sync_CheckForTrafficAbuse)
+      .toConstantValue(
+        new CheckForTrafficAbuse(
+          container.get<MetricsStoreInterface>(TYPES.Sync_MetricsStore),
+          container.get<TimerInterface>(TYPES.Sync_Timer),
+        ),
+      )
 
 
     // Handlers
     // Handlers
     container
     container
@@ -1094,11 +1130,18 @@ export class ContainerConfigLoader {
         .bind<BaseItemsController>(TYPES.Sync_BaseItemsController)
         .bind<BaseItemsController>(TYPES.Sync_BaseItemsController)
         .toConstantValue(
         .toConstantValue(
           new BaseItemsController(
           new BaseItemsController(
+            container.get<CheckForTrafficAbuse>(TYPES.Sync_CheckForTrafficAbuse),
             container.get<SyncItems>(TYPES.Sync_SyncItems),
             container.get<SyncItems>(TYPES.Sync_SyncItems),
             container.get<CheckIntegrity>(TYPES.Sync_CheckIntegrity),
             container.get<CheckIntegrity>(TYPES.Sync_CheckIntegrity),
             container.get<GetItem>(TYPES.Sync_GetItem),
             container.get<GetItem>(TYPES.Sync_GetItem),
             container.get<MapperInterface<Item, ItemHttpRepresentation>>(TYPES.Sync_ItemHttpMapper),
             container.get<MapperInterface<Item, ItemHttpRepresentation>>(TYPES.Sync_ItemHttpMapper),
             container.get<SyncResponseFactoryResolverInterface>(TYPES.Sync_SyncResponseFactoryResolver),
             container.get<SyncResponseFactoryResolverInterface>(TYPES.Sync_SyncResponseFactoryResolver),
+            container.get<Logger>(TYPES.Sync_Logger),
+            container.get<boolean>(TYPES.Sync_STRICT_ABUSE_PROTECTION),
+            container.get<number>(TYPES.Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES),
+            container.get<number>(TYPES.Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD),
+            container.get<number>(TYPES.Sync_PAYLOAD_SIZE_ABUSE_THRESHOLD),
+            container.get<number>(TYPES.Sync_PAYLOAD_SIZE_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES),
             container.get<ControllerContainerInterface>(TYPES.Sync_ControllerContainer),
             container.get<ControllerContainerInterface>(TYPES.Sync_ControllerContainer),
           ),
           ),
         )
         )

+ 10 - 0
packages/syncing-server/src/Bootstrap/Types.ts

@@ -42,6 +42,15 @@ const TYPES = {
   Sync_VALET_TOKEN_SECRET: Symbol.for('Sync_VALET_TOKEN_SECRET'),
   Sync_VALET_TOKEN_SECRET: Symbol.for('Sync_VALET_TOKEN_SECRET'),
   Sync_VALET_TOKEN_TTL: Symbol.for('Sync_VALET_TOKEN_TTL'),
   Sync_VALET_TOKEN_TTL: Symbol.for('Sync_VALET_TOKEN_TTL'),
   Sync_IS_CONFIGURED_FOR_HOME_SERVER_OR_SELF_HOSTING: Symbol.for('Sync_IS_CONFIGURED_FOR_HOME_SERVER_OR_SELF_HOSTING'),
   Sync_IS_CONFIGURED_FOR_HOME_SERVER_OR_SELF_HOSTING: Symbol.for('Sync_IS_CONFIGURED_FOR_HOME_SERVER_OR_SELF_HOSTING'),
+  Sync_STRICT_ABUSE_PROTECTION: Symbol.for('Sync_STRICT_ABUSE_PROTECTION'),
+  Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES: Symbol.for(
+    'Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES',
+  ),
+  Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD: Symbol.for('Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD'),
+  Sync_PAYLOAD_SIZE_ABUSE_THRESHOLD: Symbol.for('Sync_PAYLOAD_SIZE_ABUSE_THRESHOLD'),
+  Sync_PAYLOAD_SIZE_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES: Symbol.for(
+    'Sync_PAYLOAD_SIZE_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES',
+  ),
   // use cases
   // use cases
   Sync_SyncItems: Symbol.for('Sync_SyncItems'),
   Sync_SyncItems: Symbol.for('Sync_SyncItems'),
   Sync_CheckIntegrity: Symbol.for('Sync_CheckIntegrity'),
   Sync_CheckIntegrity: Symbol.for('Sync_CheckIntegrity'),
@@ -85,6 +94,7 @@ const TYPES = {
   Sync_TransferSharedVault: Symbol.for('Sync_TransferSharedVault'),
   Sync_TransferSharedVault: Symbol.for('Sync_TransferSharedVault'),
   Sync_TransferSharedVaultItems: Symbol.for('Sync_TransferSharedVaultItems'),
   Sync_TransferSharedVaultItems: Symbol.for('Sync_TransferSharedVaultItems'),
   Sync_DumpItem: Symbol.for('Sync_DumpItem'),
   Sync_DumpItem: Symbol.for('Sync_DumpItem'),
+  Sync_CheckForTrafficAbuse: Symbol.for('Sync_CheckForTrafficAbuse'),
   // Handlers
   // Handlers
   Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'),
   Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'),
   Sync_AccountDeletionVerificationRequestedEventHandler: Symbol.for(
   Sync_AccountDeletionVerificationRequestedEventHandler: Symbol.for(

+ 12 - 20
packages/syncing-server/src/Domain/Metrics/MetricsStoreInterface.ts

@@ -1,25 +1,17 @@
+import { Uuid } from '@standardnotes/domain-core'
+
 import { Metric } from './Metric'
 import { Metric } from './Metric'
+import { MetricsSummary } from './MetricsSummary'
 
 
 export interface MetricsStoreInterface {
 export interface MetricsStoreInterface {
-  storeUserBasedMetric(metric: Metric, value: number, userUuid: string): Promise<void>
-  getUserBasedStatistics(
-    name: string,
-    timestamp: number,
-  ): Promise<{
-    sum: number
-    max: number
-    min: number
-    sampleCount: number
-  }>
   storeMetric(metric: Metric): Promise<void>
   storeMetric(metric: Metric): Promise<void>
-  getStatistics(
-    name: string,
-    from: number,
-    to: number,
-  ): Promise<{
-    sum: number
-    max: number
-    min: number
-    sampleCount: number
-  }>
+  storeUserBasedMetric(metric: Metric, value: number, userUuid: Uuid): Promise<void>
+  getUserBasedMetricsSummaryWithinTimeRange(dto: {
+    metricName: string
+    userUuid: Uuid
+    from: Date
+    to: Date
+  }): Promise<MetricsSummary>
+  getUserBasedMetricsSummary(name: string, timestamp: number): Promise<MetricsSummary>
+  getMetricsSummary(name: string, from: number, to: number): Promise<MetricsSummary>
 }
 }

+ 6 - 0
packages/syncing-server/src/Domain/Metrics/MetricsSummary.ts

@@ -0,0 +1,6 @@
+export interface MetricsSummary {
+  sum: number
+  max: number
+  min: number
+  sampleCount: number
+}

+ 97 - 0
packages/syncing-server/src/Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuse.spec.ts

@@ -0,0 +1,97 @@
+import { TimerInterface } from '@standardnotes/time'
+import { MetricsStoreInterface } from '../../../Metrics/MetricsStoreInterface'
+import { CheckForTrafficAbuse } from './CheckForTrafficAbuse'
+import { MetricsSummary } from '../../../Metrics/MetricsSummary'
+import { Metric } from '../../../Metrics/Metric'
+
+describe('CheckForTrafficAbuse', () => {
+  let metricsStore: MetricsStoreInterface
+  let timer: TimerInterface
+  let timeframeLengthInMinutes: number
+  let threshold: number
+
+  const createUseCase = () => new CheckForTrafficAbuse(metricsStore, timer)
+
+  beforeEach(() => {
+    const metricsSummary: MetricsSummary = {
+      sum: 101,
+      max: 0,
+      min: 0,
+      sampleCount: 0,
+    }
+
+    metricsStore = {} as jest.Mocked<MetricsStoreInterface>
+    metricsStore.getUserBasedMetricsSummaryWithinTimeRange = jest.fn().mockReturnValue(metricsSummary)
+
+    timer = {} as TimerInterface
+    timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(0)
+    timer.getUTCDateNMinutesAgo = jest.fn().mockReturnValue(new Date(0))
+    timer.getUTCDate = jest.fn().mockReturnValue(new Date(10))
+
+    timeframeLengthInMinutes = 5
+
+    threshold = 100
+  })
+
+  it('returns a failure result if the user uuid is invalid', async () => {
+    const result = await createUseCase().execute({
+      userUuid: 'invalid',
+      metricToCheck: Metric.NAMES.ItemOperation,
+      timeframeLengthInMinutes,
+      threshold,
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+
+  it('return a failure result if the metric name is invalid', async () => {
+    const result = await createUseCase().execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      metricToCheck: 'invalid',
+      timeframeLengthInMinutes,
+      threshold,
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+
+  it('returns a failure result if the metric summary is above the threshold', async () => {
+    const metricsSummary: MetricsSummary = {
+      sum: 101,
+      max: 0,
+      min: 0,
+      sampleCount: 0,
+    }
+
+    metricsStore.getUserBasedMetricsSummaryWithinTimeRange = jest.fn().mockReturnValue(metricsSummary)
+
+    const result = await createUseCase().execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      metricToCheck: Metric.NAMES.ItemOperation,
+      timeframeLengthInMinutes,
+      threshold,
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+
+  it('returns a success result if the metric summary is below the threshold', async () => {
+    const metricsSummary: MetricsSummary = {
+      sum: 99,
+      max: 0,
+      min: 0,
+      sampleCount: 0,
+    }
+
+    metricsStore.getUserBasedMetricsSummaryWithinTimeRange = jest.fn().mockReturnValue(metricsSummary)
+
+    const result = await createUseCase().execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      metricToCheck: Metric.NAMES.ItemOperation,
+      timeframeLengthInMinutes,
+      threshold,
+    })
+
+    expect(result.isFailed()).toBeFalsy()
+  })
+})

+ 44 - 0
packages/syncing-server/src/Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuse.ts

@@ -0,0 +1,44 @@
+import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+import { TimerInterface } from '@standardnotes/time'
+
+import { CheckForTrafficAbuseDTO } from './CheckForTrafficAbuseDTO'
+import { MetricsStoreInterface } from '../../../Metrics/MetricsStoreInterface'
+import { Metric } from '../../../Metrics/Metric'
+import { MetricsSummary } from '../../../Metrics/MetricsSummary'
+
+export class CheckForTrafficAbuse implements UseCaseInterface<MetricsSummary> {
+  constructor(
+    private metricsStore: MetricsStoreInterface,
+    private timer: TimerInterface,
+  ) {}
+
+  async execute(dto: CheckForTrafficAbuseDTO): Promise<Result<MetricsSummary>> {
+    const userUuidOrError = Uuid.create(dto.userUuid)
+    if (userUuidOrError.isFailed()) {
+      return Result.fail(userUuidOrError.getError())
+    }
+    const userUuid = userUuidOrError.getValue()
+
+    const metricToCheckOrError = Metric.create({
+      name: dto.metricToCheck,
+      timestamp: this.timer.getTimestampInMicroseconds(),
+    })
+    if (metricToCheckOrError.isFailed()) {
+      return Result.fail(metricToCheckOrError.getError())
+    }
+    const metricToCheck = metricToCheckOrError.getValue()
+
+    const metricsSummary = await this.metricsStore.getUserBasedMetricsSummaryWithinTimeRange({
+      metricName: metricToCheck.props.name,
+      userUuid,
+      from: this.timer.getUTCDateNMinutesAgo(dto.timeframeLengthInMinutes),
+      to: this.timer.getUTCDate(),
+    })
+
+    if (metricsSummary.sum > dto.threshold) {
+      return Result.fail(`Traffic abuse detected for metric: ${metricToCheck.props.name}`)
+    }
+
+    return Result.ok(metricsSummary)
+  }
+}

+ 6 - 0
packages/syncing-server/src/Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuseDTO.ts

@@ -0,0 +1,6 @@
+export interface CheckForTrafficAbuseDTO {
+  userUuid: string
+  metricToCheck: string
+  timeframeLengthInMinutes: number
+  threshold: number
+}

+ 1 - 1
packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItem.ts

@@ -145,7 +145,7 @@ export class SaveNewItem implements UseCaseInterface<Item> {
         timestamp: this.timer.getTimestampInMicroseconds(),
         timestamp: this.timer.getTimestampInMicroseconds(),
       }).getValue(),
       }).getValue(),
       newItem.props.contentSize,
       newItem.props.contentSize,
-      userUuid.value,
+      userUuid,
     )
     )
 
 
     await this.metricsStore.storeMetric(
     await this.metricsStore.storeMetric(

+ 1 - 1
packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.ts

@@ -186,7 +186,7 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
         timestamp: this.timer.getTimestampInMicroseconds(),
         timestamp: this.timer.getTimestampInMicroseconds(),
       }).getValue(),
       }).getValue(),
       dto.existingItem.props.contentSize,
       dto.existingItem.props.contentSize,
-      userUuid.value,
+      userUuid,
     )
     )
 
 
     /* istanbul ignore next */
     /* istanbul ignore next */

+ 20 - 10
packages/syncing-server/src/Infra/Dummy/DummyMetricStore.ts

@@ -1,11 +1,25 @@
+import { Uuid } from '@standardnotes/domain-core'
+
 import { MetricsStoreInterface } from '../../Domain/Metrics/MetricsStoreInterface'
 import { MetricsStoreInterface } from '../../Domain/Metrics/MetricsStoreInterface'
 import { Metric } from '../../Domain/Metrics/Metric'
 import { Metric } from '../../Domain/Metrics/Metric'
+import { MetricsSummary } from '../../Domain/Metrics/MetricsSummary'
 
 
 export class DummyMetricStore implements MetricsStoreInterface {
 export class DummyMetricStore implements MetricsStoreInterface {
-  async getUserBasedStatistics(
-    _name: string,
-    _timestamp: number,
-  ): Promise<{ sum: number; max: number; min: number; sampleCount: number }> {
+  async getUserBasedMetricsSummaryWithinTimeRange(_dto: {
+    metricName: string
+    userUuid: Uuid
+    from: Date
+    to: Date
+  }): Promise<MetricsSummary> {
+    return {
+      sum: 0,
+      max: 0,
+      min: 0,
+      sampleCount: 0,
+    }
+  }
+
+  async getUserBasedMetricsSummary(_name: string, _timestamp: number): Promise<MetricsSummary> {
     return {
     return {
       sum: 0,
       sum: 0,
       max: 0,
       max: 0,
@@ -14,7 +28,7 @@ export class DummyMetricStore implements MetricsStoreInterface {
     }
     }
   }
   }
 
 
-  async storeUserBasedMetric(_metric: Metric, _value: number, _userUuid: string): Promise<void> {
+  async storeUserBasedMetric(_metric: Metric, _value: number, _userUuid: Uuid): Promise<void> {
     // do nothing
     // do nothing
   }
   }
 
 
@@ -22,11 +36,7 @@ export class DummyMetricStore implements MetricsStoreInterface {
     // do nothing
     // do nothing
   }
   }
 
 
-  async getStatistics(
-    _name: string,
-    _from: number,
-    _to: number,
-  ): Promise<{ sum: number; max: number; min: number; sampleCount: number }> {
+  async getMetricsSummary(_name: string, _from: number, _to: number): Promise<MetricsSummary> {
     return {
     return {
       sum: 0,
       sum: 0,
       max: 0,
       max: 0,

+ 25 - 1
packages/syncing-server/src/Infra/InversifyExpressUtils/AnnotatedItemsController.ts

@@ -11,18 +11,42 @@ import { SyncItems } from '../../Domain/UseCase/Syncing/SyncItems/SyncItems'
 import { BaseItemsController } from './Base/BaseItemsController'
 import { BaseItemsController } from './Base/BaseItemsController'
 import { MapperInterface } from '@standardnotes/domain-core'
 import { MapperInterface } from '@standardnotes/domain-core'
 import { ItemHttpRepresentation } from '../../Mapping/Http/ItemHttpRepresentation'
 import { ItemHttpRepresentation } from '../../Mapping/Http/ItemHttpRepresentation'
+import { CheckForTrafficAbuse } from '../../Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuse'
+import { Logger } from 'winston'
 
 
 @controller('/items', TYPES.Sync_AuthMiddleware)
 @controller('/items', TYPES.Sync_AuthMiddleware)
 export class AnnotatedItemsController extends BaseItemsController {
 export class AnnotatedItemsController extends BaseItemsController {
   constructor(
   constructor(
+    @inject(TYPES.Sync_CheckForTrafficAbuse) override checkForTrafficAbuse: CheckForTrafficAbuse,
     @inject(TYPES.Sync_SyncItems) override syncItems: SyncItems,
     @inject(TYPES.Sync_SyncItems) override syncItems: SyncItems,
     @inject(TYPES.Sync_CheckIntegrity) override checkIntegrity: CheckIntegrity,
     @inject(TYPES.Sync_CheckIntegrity) override checkIntegrity: CheckIntegrity,
     @inject(TYPES.Sync_GetItem) override getItem: GetItem,
     @inject(TYPES.Sync_GetItem) override getItem: GetItem,
     @inject(TYPES.Sync_ItemHttpMapper) override itemHttpMapper: MapperInterface<Item, ItemHttpRepresentation>,
     @inject(TYPES.Sync_ItemHttpMapper) override itemHttpMapper: MapperInterface<Item, ItemHttpRepresentation>,
     @inject(TYPES.Sync_SyncResponseFactoryResolver)
     @inject(TYPES.Sync_SyncResponseFactoryResolver)
     override syncResponseFactoryResolver: SyncResponseFactoryResolverInterface,
     override syncResponseFactoryResolver: SyncResponseFactoryResolverInterface,
+    @inject(TYPES.Sync_Logger) override logger: Logger,
+    @inject(TYPES.Sync_STRICT_ABUSE_PROTECTION) override strictAbuseProtection: boolean,
+    @inject(TYPES.Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES)
+    override itemOperationsAbuseTimeframeLengthInMinutes: number,
+    @inject(TYPES.Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD) override itemOperationsAbuseThreshold: number,
+    @inject(TYPES.Sync_PAYLOAD_SIZE_ABUSE_THRESHOLD) override payloadSizeAbuseThreshold: number,
+    @inject(TYPES.Sync_PAYLOAD_SIZE_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES)
+    override payloadSizeAbuseTimeframeLengthInMinutes: number,
   ) {
   ) {
-    super(syncItems, checkIntegrity, getItem, itemHttpMapper, syncResponseFactoryResolver)
+    super(
+      checkForTrafficAbuse,
+      syncItems,
+      checkIntegrity,
+      getItem,
+      itemHttpMapper,
+      syncResponseFactoryResolver,
+      logger,
+      strictAbuseProtection,
+      itemOperationsAbuseTimeframeLengthInMinutes,
+      itemOperationsAbuseThreshold,
+      payloadSizeAbuseThreshold,
+      payloadSizeAbuseTimeframeLengthInMinutes,
+    )
   }
   }
 
 
   @httpPost('/sync')
   @httpPost('/sync')

+ 41 - 0
packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseItemsController.ts

@@ -11,14 +11,24 @@ import { ApiVersion } from '../../../Domain/Api/ApiVersion'
 import { SyncItems } from '../../../Domain/UseCase/Syncing/SyncItems/SyncItems'
 import { SyncItems } from '../../../Domain/UseCase/Syncing/SyncItems/SyncItems'
 import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation'
 import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation'
 import { ItemHash } from '../../../Domain/Item/ItemHash'
 import { ItemHash } from '../../../Domain/Item/ItemHash'
+import { CheckForTrafficAbuse } from '../../../Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuse'
+import { Metric } from '../../../Domain/Metrics/Metric'
+import { Logger } from 'winston'
 
 
 export class BaseItemsController extends BaseHttpController {
 export class BaseItemsController extends BaseHttpController {
   constructor(
   constructor(
+    protected checkForTrafficAbuse: CheckForTrafficAbuse,
     protected syncItems: SyncItems,
     protected syncItems: SyncItems,
     protected checkIntegrity: CheckIntegrity,
     protected checkIntegrity: CheckIntegrity,
     protected getItem: GetItem,
     protected getItem: GetItem,
     protected itemHttpMapper: MapperInterface<Item, ItemHttpRepresentation>,
     protected itemHttpMapper: MapperInterface<Item, ItemHttpRepresentation>,
     protected syncResponseFactoryResolver: SyncResponseFactoryResolverInterface,
     protected syncResponseFactoryResolver: SyncResponseFactoryResolverInterface,
+    protected logger: Logger,
+    protected strictAbuseProtection: boolean,
+    protected itemOperationsAbuseTimeframeLengthInMinutes: number,
+    protected itemOperationsAbuseThreshold: number,
+    protected payloadSizeAbuseThreshold: number,
+    protected payloadSizeAbuseTimeframeLengthInMinutes: number,
     private controllerContainer?: ControllerContainerInterface,
     private controllerContainer?: ControllerContainerInterface,
   ) {
   ) {
     super()
     super()
@@ -31,6 +41,37 @@ export class BaseItemsController extends BaseHttpController {
   }
   }
 
 
   async sync(request: Request, response: Response): Promise<results.JsonResult> {
   async sync(request: Request, response: Response): Promise<results.JsonResult> {
+    const checkForItemOperationsAbuseResult = await this.checkForTrafficAbuse.execute({
+      metricToCheck: Metric.NAMES.ItemOperation,
+      userUuid: response.locals.user.uuid,
+      threshold: this.itemOperationsAbuseThreshold,
+      timeframeLengthInMinutes: this.itemOperationsAbuseTimeframeLengthInMinutes,
+    })
+    if (checkForItemOperationsAbuseResult.isFailed()) {
+      this.logger.warn(checkForItemOperationsAbuseResult.getError(), {
+        userId: response.locals.user.uuid,
+      })
+      if (this.strictAbuseProtection) {
+        return this.json({ error: { message: checkForItemOperationsAbuseResult.getError() } }, 429)
+      }
+    }
+
+    const checkForPayloadSizeAbuseResult = await this.checkForTrafficAbuse.execute({
+      metricToCheck: Metric.NAMES.ContentSizeUtilized,
+      userUuid: response.locals.user.uuid,
+      threshold: this.payloadSizeAbuseThreshold,
+      timeframeLengthInMinutes: this.payloadSizeAbuseTimeframeLengthInMinutes,
+    })
+    if (checkForPayloadSizeAbuseResult.isFailed()) {
+      this.logger.warn(checkForPayloadSizeAbuseResult.getError(), {
+        userId: response.locals.user.uuid,
+      })
+
+      if (this.strictAbuseProtection) {
+        return this.json({ error: { message: checkForPayloadSizeAbuseResult.getError() } }, 429)
+      }
+    }
+
     const itemHashes: ItemHash[] = []
     const itemHashes: ItemHash[] = []
     if ('items' in request.body) {
     if ('items' in request.body) {
       for (const itemHashInput of request.body.items) {
       for (const itemHashInput of request.body.items) {

+ 69 - 12
packages/syncing-server/src/Infra/Redis/RedisMetricStore.ts

@@ -3,6 +3,8 @@ import { TimerInterface } from '@standardnotes/time'
 
 
 import { MetricsStoreInterface } from '../../Domain/Metrics/MetricsStoreInterface'
 import { MetricsStoreInterface } from '../../Domain/Metrics/MetricsStoreInterface'
 import { Metric } from '../../Domain/Metrics/Metric'
 import { Metric } from '../../Domain/Metrics/Metric'
+import { Uuid } from '@standardnotes/domain-core'
+import { MetricsSummary } from '../../Domain/Metrics/MetricsSummary'
 
 
 export class RedisMetricStore implements MetricsStoreInterface {
 export class RedisMetricStore implements MetricsStoreInterface {
   private readonly METRIC_PREFIX = 'metric'
   private readonly METRIC_PREFIX = 'metric'
@@ -13,15 +15,61 @@ export class RedisMetricStore implements MetricsStoreInterface {
     private timer: TimerInterface,
     private timer: TimerInterface,
   ) {}
   ) {}
 
 
-  async storeUserBasedMetric(metric: Metric, value: number, userUuid: string): Promise<void> {
+  async getUserBasedMetricsSummaryWithinTimeRange(dto: {
+    metricName: string
+    userUuid: Uuid
+    from: Date
+    to: Date
+  }): Promise<MetricsSummary> {
+    const keys = this.getKeysRepresentingMinutesBetweenFromAndTo(dto.from, dto.to)
+
+    let sum = 0
+    let max = 0
+    let min = 0
+    let sampleCount = 0
+
+    const values = await this.redisClient.mget(
+      keys.map((key) => `${this.METRIC_PER_USER_PREFIX}:${dto.userUuid.value}:${dto.metricName}:${key}`),
+    )
+
+    for (const value of values) {
+      if (!value) {
+        continue
+      }
+
+      const valueAsNumber = Number(value)
+
+      sum += valueAsNumber
+      sampleCount++
+
+      if (valueAsNumber > max) {
+        max = valueAsNumber
+      }
+
+      if (valueAsNumber < min) {
+        min = valueAsNumber
+      }
+    }
+
+    return {
+      sum,
+      max,
+      min,
+      sampleCount,
+    }
+  }
+
+  async storeUserBasedMetric(metric: Metric, value: number, userUuid: Uuid): Promise<void> {
     const date = this.timer.convertMicrosecondsToDate(metric.props.timestamp)
     const date = this.timer.convertMicrosecondsToDate(metric.props.timestamp)
     const dateToTheMinuteString = this.timer.convertDateToFormattedString(date, 'YYYY-MM-DD HH:mm')
     const dateToTheMinuteString = this.timer.convertDateToFormattedString(date, 'YYYY-MM-DD HH:mm')
-    const key = `${this.METRIC_PER_USER_PREFIX}:${userUuid}:${metric.props.name}:${dateToTheMinuteString}`
+    const key = `${this.METRIC_PER_USER_PREFIX}:${userUuid.value}:${metric.props.name}:${dateToTheMinuteString}`
 
 
     const pipeline = this.redisClient.pipeline()
     const pipeline = this.redisClient.pipeline()
 
 
     pipeline.incrbyfloat(key, value)
     pipeline.incrbyfloat(key, value)
-    pipeline.incr(`${this.METRIC_PER_USER_PREFIX}:${userUuid}:${Metric.NAMES.ItemOperation}:${dateToTheMinuteString}`)
+    pipeline.incr(
+      `${this.METRIC_PER_USER_PREFIX}:${userUuid.value}:${Metric.NAMES.ItemOperation}:${dateToTheMinuteString}`,
+    )
 
 
     const expirationTime = 60 * 60 * 24
     const expirationTime = 60 * 60 * 24
     pipeline.expire(key, expirationTime)
     pipeline.expire(key, expirationTime)
@@ -29,10 +77,7 @@ export class RedisMetricStore implements MetricsStoreInterface {
     await pipeline.exec()
     await pipeline.exec()
   }
   }
 
 
-  async getUserBasedStatistics(
-    name: string,
-    timestamp: number,
-  ): Promise<{ sum: number; max: number; min: number; sampleCount: number }> {
+  async getUserBasedMetricsSummary(name: string, timestamp: number): Promise<MetricsSummary> {
     const date = this.timer.convertMicrosecondsToDate(timestamp)
     const date = this.timer.convertMicrosecondsToDate(timestamp)
     const dateToTheMinuteString = this.timer.convertDateToFormattedString(date, 'YYYY-MM-DD HH:mm')
     const dateToTheMinuteString = this.timer.convertDateToFormattedString(date, 'YYYY-MM-DD HH:mm')
 
 
@@ -74,11 +119,7 @@ export class RedisMetricStore implements MetricsStoreInterface {
     }
     }
   }
   }
 
 
-  async getStatistics(
-    name: string,
-    from: number,
-    to: number,
-  ): Promise<{ sum: number; max: number; min: number; sampleCount: number }> {
+  async getMetricsSummary(name: string, from: number, to: number): Promise<MetricsSummary> {
     const keysRepresentingSecondsBetweenFromAndTo = this.getKeysRepresentingSecondsBetweenFromAndTo(from, to)
     const keysRepresentingSecondsBetweenFromAndTo = this.getKeysRepresentingSecondsBetweenFromAndTo(from, to)
 
 
     let sum = 0
     let sum = 0
@@ -132,6 +173,22 @@ export class RedisMetricStore implements MetricsStoreInterface {
     await pipeline.exec()
     await pipeline.exec()
   }
   }
 
 
+  private getKeysRepresentingMinutesBetweenFromAndTo(from: Date, to: Date): string[] {
+    const keys: string[] = []
+
+    let currentMinute = from
+
+    while (currentMinute <= to) {
+      const dateToTheMinuteString = this.timer.convertDateToFormattedString(currentMinute, 'YYYY-MM-DD HH:mm')
+
+      keys.push(dateToTheMinuteString)
+
+      currentMinute = new Date(currentMinute.getTime() + 60 * 1000)
+    }
+
+    return keys
+  }
+
   private getKeysRepresentingSecondsBetweenFromAndTo(from: number, to: number): string[] {
   private getKeysRepresentingSecondsBetweenFromAndTo(from: number, to: number): string[] {
     const keys: string[] = []
     const keys: string[] = []