robust_server_socket 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -17,6 +17,8 @@ Gem для межсервисной аутентификации, использ
17
17
  - **DDoS-атаками**: Необходимость ограничения частоты запросов
18
18
  - **Boilerplate кодом**: Повторяющаяся логика валидации в каждом сервисе
19
19
 
20
+ #### Даже если инфраструктура находится за DMZ, в своей локальной сети, остаётся пространство для SSRF или OpenRedirect атак
21
+
20
22
  ### Решение
21
23
 
22
24
  RobustServerSocket предоставляет:
@@ -24,7 +26,7 @@ RobustServerSocket предоставляет:
24
26
  - **RSA-дешифрование**: Проверка подлинности токенов
25
27
  - **Whitelist клиентов**: Только разрешённые сервисы
26
28
  - **Защиту от replay**: Блэклист использованных токенов в Redis
27
- - **Rate limiting**: Ограничение запросов на клиента
29
+ - **Rate limiting**: Скользящее окно запросов на клиента
28
30
 
29
31
  ## КАК ЭТО РАБОТАЕТ (HOW)
30
32
 
@@ -54,18 +56,18 @@ RobustServerSocket предоставляет:
54
56
  ### Поток валидации
55
57
 
56
58
  1. **Расшифровка**: Base64 decode → RSA decrypt с приватным ключом
57
- 2. **Парсинг**: Извлечение `{client_name}_{timestamp}` из токена
59
+ 2. **Парсинг**: Извлечение `{client_name}_{timestamp_ms}` из токена
58
60
  3. **Whitelist**: Проверка client_name в `allowed_services`
59
- 4. **Rate limit**: Проверка количества запросов в окне
61
+ 4. **Rate limit**: Скользящее окно — проверка количества запросов за `rate_limit_window_seconds`
60
62
  5. **Replay check**: Проверка, что токен не использован (Redis)
61
- 6. **Staleness**: Проверка timestamp на актуальность
63
+ 6. **Staleness**: Проверка timestamp на актуальность (с допуском ±30 секунд на рассинхрон часов)
62
64
 
63
65
  ### Модульная система
64
66
 
65
67
  Проверки подключаются через `using_modules`:
66
68
  - `:client_auth_protection` — whitelist клиентов
67
- - `:replay_attack_protection` — защита от повторного использования
68
- - `:dos_attack_protection` — rate limiting
69
+ - `:replay_attack_protection` — защита от повторного использования токена
70
+ - `:rate_limit_protection` — rate limiting по скользящему окну
69
71
 
70
72
  ## 📋 Содержание
71
73
 
@@ -88,21 +90,19 @@ RobustServerSocket реализует многоуровневую систем
88
90
  - **Идентификация по имени**: Каждый клиент должен быть явно указан в `allowed_services`
89
91
 
90
92
  ### 3. Защита от перехвата токенов (replay-attack)
91
- - **Защита от replay-attack**: использованные токены добавляются в черный список и имеют время жизни, при включенном модуле `:replay_attack_protection`
92
- - **Staleness**: Токены автоматически становятся недействительными после истечения времени
93
- - **Blacklisting использованных токенов**: Redis как хранилище черного списка
94
- - **Настраиваемое время жизни токенов в черном списке**: по умолчанию 10 минут
95
- - **Настраиваемое ttl токена**: Должно быть в окно ответа между серверами, по умолчанию 10сек
96
-
97
- ### 4. Защита от DoS
98
- - **Защита от DDoS**: Ограничение количества запросов от каждого клиента, при включенном модуле `:dos_attack_protection`
99
- - **Sliding window**: Распределение запросов во времени
100
- - **Fail-open стратегия**: Если Redis недоступен, запросы пропускаются (для надёжности)
101
-
102
- -
93
+ - **Защита от replay-attack**: использованные токены добавляются в черный список, при включенном модуле `:replay_attack_protection`
94
+ - **Staleness**: Токены автоматически становятся недействительными после истечения `token_expiration_time`
95
+ - **Допуск на рассинхрон часов**: ±30 секунд (CLOCK_SKEW)
96
+ - **TTL блэклиста**: вычисляется автоматически как `token_expiration_time + CLOCK_SKEW`
97
+
98
+ ### 4. Rate limiting
99
+ - **Скользящее окно**: при включенном модуле `:rate_limit_protection` — точный подсчёт запросов без граничного burst-эффекта фиксированного окна
100
+ - **Fail-open стратегия**: если Redis недоступен, запросы пропускаются (для надёжности сервиса)
101
+ - **Изоляция по клиентам**: лимит считается отдельно для каждого `client_name`
102
+
103
103
  ### 5. Защита от SSL stripping MITM attack
104
104
  - **Принудительное HTTPS на сервере**: Все запросы должны быть совершены по HTTPS, чтобы защитить токены от перехвата
105
- - **Включается на RobustClientSoket, ключём `ssl_verify: true`**
105
+ - **Включается на RobustClientSocket, ключём `ssl_verify: true`**
106
106
 
107
107
  ## 📦 Установка
108
108
 
@@ -114,6 +114,7 @@ gem 'robust_server_socket'
114
114
  ```ruby
115
115
  gem 'robust_client_socket'
116
116
  ```
117
+
117
118
  ## ⚙️ Конфигурация
118
119
 
119
120
  Создайте файл `config/initializers/robust_server_socket.rb`:
@@ -121,140 +122,119 @@ gem 'robust_client_socket'
121
122
  ```ruby
122
123
  RobustServerSocket.configure do |c|
123
124
  c.using_modules = %i[
124
- :client_auth_protection
125
- :replay_attack_protection
126
- :dos_attack_protection
125
+ client_auth_protection
126
+ replay_attack_protection
127
+ rate_limit_protection
127
128
  ]
128
-
129
+
129
130
  # Приватный ключ сервиса (RSA-2048 или выше)
130
131
  c.private_key = ENV['ROBUST_SERVER_PRIVATE_KEY']
131
- c.token_expiration_time = 3
132
-
132
+
133
+ # Время жизни токена в секундах (должно совпадать с TTL на клиенте)
134
+ c.token_expiration_time = 10
135
+
133
136
  # Список разрешённых сервисов (whitelist)
134
- # Должен совпадать с именами RobustClientSocket клиента
135
- # Для client_auth_protection
137
+ # Должен совпадать с service_name RobustClientSocket клиента
136
138
  c.allowed_services = %w[core payments notifications]
137
-
138
- # Redis для работы replay_attack_protection и ddos_attack_protection
139
+
140
+ # Redis для replay_attack_protection и rate_limit_protection
139
141
  c.redis_url = ENV.fetch('REDIS_URL', 'redis://localhost:6379/0')
140
142
  c.redis_pass = ENV['REDIS_PASSWORD']
141
143
 
142
- # ddos_attack_protection
143
- # Максимальное количество запросов в окне времени (по умолчанию: 100)
144
- c.rate_limit_max_requests = 100
145
- # Размер временного окна в секундах (по умолчанию: 60)
146
- c.rate_limit_window_seconds = 60
144
+ # rate_limit_protection
145
+ c.rate_limit_max_requests = 100 # максимум запросов в окне (по умолчанию: 100)
146
+ c.rate_limit_window_seconds = 60 # размер окна в секундах (по умолчанию: 60)
147
147
  end
148
148
 
149
- # Загрузка конфигурации с валидацией
150
149
  RobustServerSocket.load!
151
150
  ```
152
- `using_modules` - это используемые модули, добавление или удаление которых изменит поведение гема.
153
-
154
- ### Опции конфигурации сервиса
155
-
156
- | Параметр | Тип | Обязательный | Default | Описание |
157
- |-----------------------------|-----|-------------|------------------------------------------------------------------------------|-------------------------------------------------|
158
- | `private_key` | String | ✅ | - | Приватный RSA ключ сервиса (RSA-2048 или выше) |
159
- | `token_expiration_time` | Integer | ✅ | 10 | Время жизни токена в секундах |
160
- | `store_used_token_time` | Integer | ✅ | 600 | Время жизни токена в блеклисте в секундах |
161
- | `allowed_services` | Array | ❌ | - | Список разрешённых сервисов (whitelist) |
162
- | `redis_url` | String | | - | URL для подключения к Redis |
163
- | `using_modules` | Array | ❌ | [:client_auth_protection, :replay_attack_protection, :dos_attack_protection] | Используемые модули |
164
- | `redis_pass` | String | ❌ | nil | Пароль для Redis (если требуется) |
165
- | `rate_limit_max_requests` | Integer | ❌ | 100 | Максимальное количество запросов в окне времени |
166
- | `rate_limit_window_seconds` | Integer | | 60 | Размер временного окна в секундах |
151
+
152
+ ### Опции конфигурации
153
+
154
+ | Параметр | Тип | Обязательный | Default | Описание |
155
+ |-----------------------------|---------|-------------|-------------------------------------------------------------------------------------|-------------------------------------------------|
156
+ | `private_key` | String | ✅ | — | Приватный RSA ключ сервиса (RSA-2048 или выше) |
157
+ | `token_expiration_time` | Integer | ✅ | 10 | Время жизни токена в секундах |
158
+ | `allowed_services` | Array | ✅ | | Список разрешённых сервисов (whitelist) |
159
+ | `redis_url` | String | ✅ | | URL для подключения к Redis |
160
+ | `redis_pass` | String | ❌ | nil | Пароль для Redis |
161
+ | `using_modules` | Array | | `[:client_auth_protection, :rate_limit_protection, :replay_attack_protection]` | Используемые модули |
162
+ | `rate_limit_max_requests` | Integer | ❌ | 100 | Максимальное количество запросов в окне |
163
+ | `rate_limit_window_seconds` | Integer | ❌ | 60 | Размер временного окна в секундах |
164
+
165
+ > `store_used_token_time` больше не является конфигурируемым вычисляется автоматически как `token_expiration_time + 30` (CLOCK_SKEW).
166
+
167
+ ### Совместимость с RobustClientSocket
168
+
169
+ Токен содержит таймстамп в **миллисекундах**. RobustClientSocket начиная с версии X.X должен генерировать:
170
+
171
+ ```ruby
172
+ Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)
173
+ ```
174
+
175
+ Устаревший `Time.now.utc.to_i` (секунды) приведёт к тому, что все токены будут отклонены как `stale`.
167
176
 
168
177
  ## 🚀 Использование
169
178
 
170
179
  ### Базовая авторизация
171
180
 
172
181
  ```ruby
173
- # В контроллере или middleware
174
182
  class ApiController < ApplicationController
175
183
  before_action :authenticate_service!
176
-
184
+
177
185
  private
178
-
186
+
179
187
  def authenticate_service!
180
- # Хедер, прописанный в RobustClientSocket (SECURE-TOKEN default)
181
188
  token = request.headers['SECURE-TOKEN']&.sub(/^Bearer /, '')
182
-
183
- @current_service = RobustServerSocket::ClientToken.validate!(token) # bang method (рейзит ошибки)
189
+ @current_service = RobustServerSocket::ClientToken.validate!(token)
184
190
  rescue RobustServerSocket::ClientToken::InvalidToken
185
191
  render json: { error: 'Invalid token' }, status: :unauthorized
186
- rescue RobustServerSocket::ClientToken::UnauthorizedClient
192
+ rescue RobustServerSocket::Modules::ClientAuthProtection::UnauthorizedClient
187
193
  render json: { error: 'Unauthorized service' }, status: :forbidden
188
- rescue RobustServerSocket::ClientToken::UsedToken
194
+ rescue RobustServerSocket::Modules::ReplayAttackProtection::UsedToken
189
195
  render json: { error: 'Token already used' }, status: :unauthorized
190
- rescue RobustServerSocket::ClientToken::StaleToken
196
+ rescue RobustServerSocket::Modules::ReplayAttackProtection::StaleToken
191
197
  render json: { error: 'Token expired' }, status: :unauthorized
192
198
  rescue RobustServerSocket::RateLimiter::RateLimitExceeded => e
193
199
  render json: { error: e.message }, status: :too_many_requests
194
200
  end
195
-
196
- def authenticate_service
197
- token = request.headers['SECURE-TOKEN']&.sub(/^Bearer /, '')
198
- @current_service = RobustServerSocket::ClientToken.valid?(token) # не рейзит
199
-
200
- if @current_service
201
- # Токен валиден
202
- else
203
- # Токен невалиден
204
- render json: { error: 'Unauthorized' }, status: :unauthorized
205
- end
206
- end
207
201
  end
208
202
  ```
209
203
 
210
- ### Расширенное использование
204
+ ### valid? (не рейзит)
211
205
 
212
206
  ```ruby
213
- # Создание объекта токена
214
- token_string = request.headers['Authorization']&.sub(/^Bearer /, '')
215
- client_token = RobustServerSocket::ClientToken.new(token_string)
207
+ token = request.headers['SECURE-TOKEN']&.sub(/^Bearer /, '')
208
+ client_token = RobustServerSocket::ClientToken.new(token)
216
209
 
217
- # Проверка валидности (возвращает true/false)
218
210
  if client_token.valid?
219
- # Получение имени клиента
220
211
  client_name = client_token.client
221
- puts "Authorized client: #{client_name}"
222
212
  else
223
- # Токен невалиден
224
213
  render json: { error: 'Unauthorized' }, status: :unauthorized
225
214
  end
226
-
227
- # Быстрая валидация с исключениями
228
- begin
229
- service_token = RobustServerSocket::ClientToken.validate!(token_string)
230
- client_name = service_token.client
231
- rescue => e
232
- # Обработка специфичных ошибок
233
- end
234
215
  ```
235
216
 
236
217
  ## ❌ Обработка ошибок
237
218
 
238
219
  ### Типы исключений
239
220
 
240
- | Исключение | Причина | HTTP статус | Действие |
241
- |-----------|---------|-------------|----------|
242
- | `InvalidToken` | Токен не может быть расшифрован или имеет неверный формат | 401 | Проверьте корректность токена и ключей |
243
- | `UnauthorizedClient` | Клиент не в whitelist | 403 | Добавьте клиента в `allowed_services` |
244
- | `UsedToken` | Токен уже был использован | 401 | Клиент должен запросить новый токен |
245
- | `StaleToken` | Токен истёк | 401 | Клиент должен запросить новый токен |
246
- | `RateLimitExceeded` | Превышен лимит запросов | 429 | Клиент должен подождать или ретраить позже |
221
+ | Исключение | Причина | HTTP статус |
222
+ |---------------------------------------------------------|----------------------------------------------|-------------|
223
+ | `ClientToken::InvalidToken` | Токен не расшифрован или неверный формат | 401 |
224
+ | `Modules::ClientAuthProtection::UnauthorizedClient` | Клиент не в whitelist | 403 |
225
+ | `Modules::ReplayAttackProtection::UsedToken` | Токен уже был использован | 401 |
226
+ | `Modules::ReplayAttackProtection::StaleToken` | Токен истёк или из будущего (>30s) | 401 |
227
+ | `RateLimiter::RateLimitExceeded` | Превышен лимит запросов | 429 |
247
228
 
248
229
  ### Централизованная обработка
249
230
 
250
231
  ```ruby
251
- # В ApplicationController
252
232
  rescue_from RobustServerSocket::ClientToken::InvalidToken,
253
- RobustServerSocket::ClientToken::UsedToken,
254
- RobustServerSocket::ClientToken::StaleToken,
233
+ RobustServerSocket::Modules::ReplayAttackProtection::UsedToken,
234
+ RobustServerSocket::Modules::ReplayAttackProtection::StaleToken,
255
235
  with: :unauthorized_response
256
236
 
257
- rescue_from RobustServerSocket::ClientToken::UnauthorizedClient,
237
+ rescue_from RobustServerSocket::Modules::ClientAuthProtection::UnauthorizedClient,
258
238
  with: :forbidden_response
259
239
 
260
240
  rescue_from RobustServerSocket::RateLimiter::RateLimitExceeded,
@@ -263,26 +243,17 @@ rescue_from RobustServerSocket::RateLimiter::RateLimitExceeded,
263
243
  private
264
244
 
265
245
  def unauthorized_response(exception)
266
- render json: {
267
- error: 'Authentication failed',
268
- message: exception.message,
269
- type: exception.class.name
270
- }, status: :unauthorized
246
+ render json: { error: 'Authentication failed', message: exception.message }, status: :unauthorized
271
247
  end
272
248
 
273
249
  def forbidden_response(exception)
274
- render json: {
275
- error: 'Access denied',
276
- message: exception.message,
277
- type: exception.class.name
278
- }, status: :forbidden
250
+ render json: { error: 'Access denied', message: exception.message }, status: :forbidden
279
251
  end
280
252
 
281
253
  def rate_limit_response(exception)
282
254
  render json: {
283
255
  error: 'Too many requests',
284
256
  message: exception.message,
285
- type: exception.class.name,
286
257
  retry_after: RobustServerSocket.configuration.rate_limit_window_seconds
287
258
  }, status: :too_many_requests
288
259
  end
@@ -290,8 +261,6 @@ end
290
261
 
291
262
  ## 🤝 Интеграция с RobustClientSocket
292
263
 
293
- Для полноценной работы необходимо настроить клиентскую часть:
294
-
295
264
  ```ruby
296
265
  # На клиенте (RobustClientSocket)
297
266
  RobustClientSocket.configure do |c|
@@ -299,21 +268,25 @@ RobustClientSocket.configure do |c|
299
268
  c.keychain = {
300
269
  payments: {
301
270
  base_uri: 'https://payments.example.com',
302
- public_key: '-----BEGIN PUBLIC KEY-----...' # Публичный ключ сервера payments
271
+ public_key: '-----BEGIN PUBLIC KEY-----...'
303
272
  }
304
273
  }
305
274
  end
306
275
 
307
276
  # На сервере (RobustServerSocket)
308
277
  RobustServerSocket.configure do |c|
309
- c.allowed_services = %w[core] # ← Соответствует service_name клиента
310
- c.private_key = '-----BEGIN PRIVATE KEY-----...' # Приватная пара к public_key
278
+ c.allowed_services = %w[core]
279
+ c.private_key = '-----BEGIN PRIVATE KEY-----...'
311
280
  end
312
281
  ```
313
282
 
283
+ ## 🗺️ TODO
284
+
285
+ - [ ] **Per-client ключи для rate limiter** — настраиваемые индивидуальные лимиты для каждого `client_name` вместо единого глобального лимита
286
+
314
287
  ## 📚 Дополнительные ресурсы
315
288
 
316
- - [RobustClientSocket documentation](https://github.com/tee0zed/robust_client_socket)
289
+ - [RobustClientSocket](https://github.com/tee0zed/robust_client_socket)
317
290
  - [RSA encryption best practices](https://www.openssl.org/docs/)
318
291
  - [Redis security guide](https://redis.io/topics/security)
319
292
 
data/Rakefile CHANGED
@@ -1,12 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/gem_tasks"
4
- require "rake/testtask"
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
5
 
6
- Rake::TestTask.new(:test) do |t|
7
- t.libs << "test"
8
- t.libs << "lib"
9
- t.test_files = FileList["test/**/test_*.rb"]
10
- end
6
+ RSpec::Core::RakeTask.new(:spec)
11
7
 
12
- task default: :test
8
+ task default: :spec
@@ -1,35 +1,32 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RobustServerSocket
2
- module Cacher
4
+ module Cacher # rubocop:disable Metrics/ModuleLength
3
5
  class RedisConnectionError < StandardError; end
4
6
 
5
- class << self
6
- # Atomically validate token: check expiration and usage, then mark as used
7
- # Returns: 'ok', 'stale', or 'used'
8
- def atomic_validate_and_log(key, ttl, timestamp, expiration_time)
9
- current_time = Time.now.utc.to_i
7
+ CLOCK_SKEW_MS = 30_000
10
8
 
9
+ class << self # rubocop:disable Metrics/ClassLength
10
+ def atomic_validate_and_log(key, ttl, timestamp_ms, expiration_time)
11
+ current_ms = Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)
11
12
  redis.with do |conn|
12
- conn.eval(
13
- lua_atomic_validate_and_log,
14
- keys: [key],
15
- argv: [ttl, timestamp, expiration_time, current_time]
16
- )
13
+ conn.eval(lua_atomic_validate_and_log, keys: [key],
14
+ argv: [ttl, timestamp_ms, expiration_time, current_ms])
17
15
  end
18
16
  rescue ::Redis::BaseConnectionError => e
19
17
  handle_redis_error(e, 'atomic_validate_and_log')
20
18
  raise RedisConnectionError, "Failed to validate token: #{e.message}"
21
19
  end
22
20
 
23
- def incr(key)
21
+ def incr_sliding_window_count(key, window_seconds)
22
+ now_ns = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
24
23
  redis.with do |conn|
25
- conn.pipelined do |pipeline|
26
- pipeline.incrby(key, 1)
27
- pipeline.expire(key, ttl_seconds)
28
- end
24
+ conn.eval(lua_sliding_window, keys: [key],
25
+ argv: [now_ns, window_seconds * 1_000_000_000, window_seconds, now_ns.to_s])
29
26
  end
30
27
  rescue ::Redis::BaseConnectionError => e
31
- handle_redis_error(e, 'incr')
32
- raise RedisConnectionError, "Failed to increment key: #{e.message}"
28
+ handle_redis_error(e, 'incr_sliding_window_count')
29
+ raise RedisConnectionError, "Failed to count sliding window: #{e.message}"
33
30
  end
34
31
 
35
32
  def get(key)
@@ -53,52 +50,65 @@ module RobustServerSocket
53
50
  redis.with(&block)
54
51
  rescue ::Redis::BaseConnectionError => e
55
52
  handle_redis_error(e, 'with_redis')
56
- raise ::RedisConnectionError, "Redis operation failed: #{e.message}"
53
+ raise RedisConnectionError, "Redis operation failed: #{e.message}"
57
54
  end
58
55
 
59
56
  # Clear cached Redis connection pool (useful for hot reloading in development)
60
57
  def clear_redis_pool_cache!
61
- @pool = nil
58
+ @redis = nil
62
59
  end
63
60
 
64
61
  private
65
62
 
63
+ def lua_sliding_window
64
+ <<~LUA
65
+ local key = KEYS[1]
66
+ local now_ns = tonumber(ARGV[1])
67
+ local window_ns = tonumber(ARGV[2])
68
+ local window_s = tonumber(ARGV[3])
69
+ local member = ARGV[4]
70
+
71
+ redis.call('ZREMRANGEBYSCORE', key, '-inf', now_ns - window_ns)
72
+ redis.call('ZADD', key, now_ns, member)
73
+ redis.call('EXPIRE', key, window_s)
74
+ return redis.call('ZCARD', key)
75
+ LUA
76
+ end
77
+
66
78
  def lua_atomic_validate_and_log
67
79
  <<~LUA
68
80
  local key = KEYS[1]
69
81
  local ttl = tonumber(ARGV[1])
70
- local timestamp = tonumber(ARGV[2])
71
- local expiration_time = tonumber(ARGV[3])
72
- local current_time = tonumber(ARGV[4])
73
-
74
- -- Check if token is expired
75
- if expiration_time <= (current_time - timestamp) then
82
+ local timestamp_ms = tonumber(ARGV[2])
83
+ local expiration_ms = tonumber(ARGV[3]) * 1000
84
+ local current_ms = tonumber(ARGV[4])
85
+
86
+ if timestamp_ms > current_ms + #{CLOCK_SKEW_MS} then
76
87
  return 'stale'
77
88
  end
78
-
89
+
90
+ if expiration_ms <= (current_ms - timestamp_ms) then
91
+ return 'stale'
92
+ end
93
+
79
94
  -- Check if token was already used
80
95
  local current = redis.call('GET', key)
81
96
  if current and tonumber(current) > 0 then
82
97
  return 'used'
83
98
  end
84
-
99
+
85
100
  -- Mark token as used
86
101
  redis.call('INCRBY', key, 1)
87
102
  redis.call('EXPIRE', key, ttl)
88
-
103
+
89
104
  return 'ok'
90
105
  LUA
91
106
  end
92
107
 
93
- def ttl_seconds
94
- # `+ 10` secs, for token storing and expiration check validity
95
- ::RobustServerSocket.configuration.token_expiration_time + 10
96
- end
97
-
98
108
  # Cache Redis connection pool at module level for the lifetime of the Rails process
99
109
  # This avoids recreating the connection pool on every Redis operation
100
110
  def redis
101
- @pool ||= ::ConnectionPool::Wrapper.new(**pool_config) do
111
+ @redis ||= ::ConnectionPool::Wrapper.new(**pool_config) do
102
112
  ::Redis.new(redis_config)
103
113
  end
104
114
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RobustServerSocket
2
4
  class ClientToken
3
5
  TOKEN_REGEXP = /\A(.+)_(\d{10,})\z/.freeze
@@ -5,9 +7,7 @@ module RobustServerSocket
5
7
  InvalidToken = Class.new(StandardError)
6
8
 
7
9
  def self.validate!(secure_token)
8
- new(secure_token).tap do |instance|
9
- instance.validate!
10
- end
10
+ new(secure_token).tap(&:validate!)
11
11
  end
12
12
 
13
13
  def initialize(secure_token)
@@ -16,12 +16,13 @@ module RobustServerSocket
16
16
  end
17
17
 
18
18
  def validate!
19
- raise InvalidToken unless validate_decrypted_token
20
19
  modules_checks!
20
+ rescue SecureToken::InvalidToken => e
21
+ raise InvalidToken, e.message
21
22
  end
22
23
 
23
24
  def valid?
24
- validate_decrypted_token && modules_checks
25
+ modules_checks
25
26
  rescue StandardError
26
27
  false
27
28
  end
@@ -45,10 +46,6 @@ module RobustServerSocket
45
46
  @decrypted_token ||= SecureToken::Decrypt.call(@secure_token)
46
47
  end
47
48
 
48
- def validate_decrypted_token
49
- !!decrypted_token
50
- end
51
-
52
49
  private
53
50
 
54
51
  def allowed_clients
@@ -67,6 +64,7 @@ module RobustServerSocket
67
64
  @split_token ||= begin
68
65
  match_data = decrypted_token.to_s.match(TOKEN_REGEXP)
69
66
  raise InvalidToken, 'Invalid token format' unless match_data
67
+
70
68
  match_data.captures
71
69
  end
72
70
  end
@@ -75,7 +73,6 @@ module RobustServerSocket
75
73
  RobustServerSocket.configuration.token_expiration_time
76
74
  end
77
75
 
78
-
79
76
  # Do we need it? It would be useful only if public_key compromised
80
77
  # def secure_compare(a, b)
81
78
  # return false unless a.bytesize == b.bytesize
@@ -89,6 +86,9 @@ module RobustServerSocket
89
86
  raise InvalidToken, 'Token cannot be empty' if token.empty?
90
87
  raise InvalidToken, 'Token too long' if token.length > 2048
91
88
 
89
+ # Check for null-byte injection
90
+ raise InvalidToken, 'Token contains invalid characters' if token.include?("\x00")
91
+
92
92
  token
93
93
  end
94
94
  end
@@ -1,7 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RobustServerSocket
2
4
  module Configuration
3
5
  MIN_KEY_SIZE = 2048
4
6
 
7
+ ConfigurationError = Class.new(StandardError)
8
+
5
9
  attr_reader :configuration, :configured
6
10
 
7
11
  def _push_modules_check_code(code)
@@ -15,6 +19,7 @@ module RobustServerSocket
15
19
  def configure
16
20
  @configuration ||= ConfigStore.new
17
21
  yield(configuration)
22
+ validate_configuration!
18
23
  validate_key_security!
19
24
 
20
25
  @configured = true
@@ -37,13 +42,25 @@ module RobustServerSocket
37
42
 
38
43
  private
39
44
 
45
+ def validate_configuration!
46
+ validate_positive!(:rate_limit_max_requests)
47
+ validate_positive!(:rate_limit_window_seconds)
48
+ validate_positive!(:token_expiration_time)
49
+ end
50
+
51
+ def validate_positive!(attr)
52
+ return if configuration.public_send(attr).to_i.positive?
53
+
54
+ raise ConfigurationError, "#{attr} must be positive"
55
+ end
56
+
40
57
  def validate_key_security!
41
58
  key = ::OpenSSL::PKey::RSA.new(configuration.private_key)
42
59
  key_bits = key.n.num_bits
43
60
 
44
61
  if key_bits < MIN_KEY_SIZE
45
62
  raise SecurityError,
46
- "RSA key size (#{key_bits} bits) below minimum (#{MIN_KEY_SIZE} bits)"
63
+ "RSA key size (#{key_bits} bits) below minimum (#{MIN_KEY_SIZE} bits)"
47
64
  end
48
65
  rescue ::OpenSSL::PKey::RSAError => e
49
66
  raise SecurityError, "Invalid private key: #{e.message}"
@@ -51,7 +68,8 @@ module RobustServerSocket
51
68
  end
52
69
 
53
70
  class ConfigStore
54
- attr_accessor :allowed_services, :private_key, :token_expiration_time, :store_used_token_time, :redis_url, :redis_pass,
71
+ attr_accessor :allowed_services, :private_key, :token_expiration_time,
72
+ :redis_url, :redis_pass,
55
73
  :rate_limit_max_requests, :rate_limit_window_seconds, :using_modules
56
74
 
57
75
  attr_reader :_modules_check_rows, :_bang_modules_check_rows
@@ -59,14 +77,8 @@ module RobustServerSocket
59
77
  def initialize
60
78
  @rate_limit_max_requests = 100
61
79
  @rate_limit_window_seconds = 60
62
- @store_used_token_time = 600
63
80
  @token_expiration_time = 10
64
- @using_modules = %i[
65
- client_auth_protection
66
- dos_attack_protection
67
- replay_attack_protection
68
- ]
69
-
81
+ @using_modules = %i[client_auth_protection rate_limit_protection replay_attack_protection]
70
82
  @_modules_check_rows = []
71
83
  @_bang_modules_check_rows = []
72
84
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RobustServerSocket
2
4
  module Modules
3
5
  module ClientAuthProtection