robust_server_socket 0.3.2 → 0.4.2
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.
- checksums.yaml +4 -4
- data/README.en.md +614 -0
- data/README.md +326 -0
- data/lib/robust_server_socket/cacher.rb +132 -0
- data/lib/robust_server_socket/client_token.rb +20 -41
- data/lib/robust_server_socket/configuration.rb +22 -3
- data/lib/robust_server_socket/modules/client_auth_protection.rb +20 -0
- data/lib/robust_server_socket/modules/dos_attack_protection.rb +21 -0
- data/lib/robust_server_socket/modules/replay_attack_protection.rb +50 -0
- data/lib/robust_server_socket/rate_limiter.rb +6 -15
- data/lib/robust_server_socket/secure_token/decrypt.rb +0 -2
- data/lib/robust_server_socket.rb +22 -6
- data/lib/version.rb +1 -1
- metadata +8 -3
- data/lib/robust_server_socket/secure_token/cacher.rb +0 -138
data/README.md
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# RobustServerSocket
|
|
2
|
+
|
|
3
|
+
Gem для межсервисной аутентификации, используется в паре с RobustClientSocket
|
|
4
|
+
|
|
5
|
+
### ⚠️ Not Production Tested (yet) but tested in staging environment
|
|
6
|
+
|
|
7
|
+
`Not vibecoded`
|
|
8
|
+
|
|
9
|
+
## ПОЧЕМУ (WHY)
|
|
10
|
+
|
|
11
|
+
### Проблема
|
|
12
|
+
|
|
13
|
+
При построении микросервисной архитектуры серверная сторона сталкивается с:
|
|
14
|
+
|
|
15
|
+
- **Отсутствием верификации**: Как проверить, что запрос пришёл от доверенного сервиса?
|
|
16
|
+
- **Replay-атаками**: Перехваченные запросы могут быть повторены
|
|
17
|
+
- **DDoS-атаками**: Необходимость ограничения частоты запросов
|
|
18
|
+
- **Boilerplate кодом**: Повторяющаяся логика валидации в каждом сервисе
|
|
19
|
+
|
|
20
|
+
### Решение
|
|
21
|
+
|
|
22
|
+
RobustServerSocket предоставляет:
|
|
23
|
+
|
|
24
|
+
- **RSA-дешифрование**: Проверка подлинности токенов
|
|
25
|
+
- **Whitelist клиентов**: Только разрешённые сервисы
|
|
26
|
+
- **Защиту от replay**: Блэклист использованных токенов в Redis
|
|
27
|
+
- **Rate limiting**: Ограничение запросов на клиента
|
|
28
|
+
|
|
29
|
+
## КАК ЭТО РАБОТАЕТ (HOW)
|
|
30
|
+
|
|
31
|
+
### Архитектура
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Входящий запрос с Secure-Token
|
|
35
|
+
│
|
|
36
|
+
v
|
|
37
|
+
┌──────────────────────────────┐
|
|
38
|
+
│ RobustServerSocket │
|
|
39
|
+
│ │
|
|
40
|
+
│ 1. RSA Decrypt │
|
|
41
|
+
│ 2. Validate Format │
|
|
42
|
+
│ 3. Check Client Whitelist │
|
|
43
|
+
│ 4. Check Rate Limit │
|
|
44
|
+
│ 5. Check Token Reuse │
|
|
45
|
+
│ 6. Check Token Expiration │
|
|
46
|
+
└──────────────┬───────────────┘
|
|
47
|
+
│
|
|
48
|
+
┌────────┼────────┐
|
|
49
|
+
v v
|
|
50
|
+
✅ Success ❌ Error
|
|
51
|
+
(continue) (401/403/429)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Поток валидации
|
|
55
|
+
|
|
56
|
+
1. **Расшифровка**: Base64 decode → RSA decrypt с приватным ключом
|
|
57
|
+
2. **Парсинг**: Извлечение `{client_name}_{timestamp}` из токена
|
|
58
|
+
3. **Whitelist**: Проверка client_name в `allowed_services`
|
|
59
|
+
4. **Rate limit**: Проверка количества запросов в окне
|
|
60
|
+
5. **Replay check**: Проверка, что токен не использован (Redis)
|
|
61
|
+
6. **Staleness**: Проверка timestamp на актуальность
|
|
62
|
+
|
|
63
|
+
### Модульная система
|
|
64
|
+
|
|
65
|
+
Проверки подключаются через `using_modules`:
|
|
66
|
+
- `:client_auth_protection` — whitelist клиентов
|
|
67
|
+
- `:replay_attack_protection` — защита от повторного использования
|
|
68
|
+
- `:dos_attack_protection` — rate limiting
|
|
69
|
+
|
|
70
|
+
## 📋 Содержание
|
|
71
|
+
|
|
72
|
+
- [Функции безопасности](#функции-безопасности)
|
|
73
|
+
- [Установка](#установка)
|
|
74
|
+
- [Конфигурация](#конфигурация)
|
|
75
|
+
- [Использование](#использование)
|
|
76
|
+
- [Обработка ошибок](#обработка-ошибок)
|
|
77
|
+
|
|
78
|
+
## 🔒 Функции безопасности
|
|
79
|
+
|
|
80
|
+
RobustServerSocket реализует многоуровневую систему защиты для межсервисных коммуникаций:
|
|
81
|
+
|
|
82
|
+
### 1. Криптографическая защита
|
|
83
|
+
- **RSA-2048 шифрование**: Используется пара ключей RSA с минимальной длиной 2048 бит
|
|
84
|
+
- **Валидация ключей**: Автоматическая проверка размера ключа при конфигурации
|
|
85
|
+
|
|
86
|
+
### 2. Контроль доступа
|
|
87
|
+
- **Whitelist клиентов**: Только авторизованные сервисы могут подключаться, при включенном модуле `:client_auth_protection`
|
|
88
|
+
- **Идентификация по имени**: Каждый клиент должен быть явно указан в `allowed_services`
|
|
89
|
+
|
|
90
|
+
### 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
|
+
-
|
|
103
|
+
### 5. Защита от SSL stripping MITM attack
|
|
104
|
+
- **Принудительное HTTPS на сервере**: Все запросы должны быть совершены по HTTPS, чтобы защитить токены от перехвата
|
|
105
|
+
- **Включается на RobustClientSoket, ключём `ssl_verify: true`**
|
|
106
|
+
|
|
107
|
+
## 📦 Установка
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
gem 'robust_server_socket'
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
и на клиенте:
|
|
114
|
+
```ruby
|
|
115
|
+
gem 'robust_client_socket'
|
|
116
|
+
```
|
|
117
|
+
## ⚙️ Конфигурация
|
|
118
|
+
|
|
119
|
+
Создайте файл `config/initializers/robust_server_socket.rb`:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
RobustServerSocket.configure do |c|
|
|
123
|
+
c.using_modules = %i[
|
|
124
|
+
:client_auth_protection
|
|
125
|
+
:replay_attack_protection
|
|
126
|
+
:dos_attack_protection
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
# Приватный ключ сервиса (RSA-2048 или выше)
|
|
130
|
+
c.private_key = ENV['ROBUST_SERVER_PRIVATE_KEY']
|
|
131
|
+
c.token_expiration_time = 3
|
|
132
|
+
|
|
133
|
+
# Список разрешённых сервисов (whitelist)
|
|
134
|
+
# Должен совпадать с именами RobustClientSocket клиента
|
|
135
|
+
# Для client_auth_protection
|
|
136
|
+
c.allowed_services = %w[core payments notifications]
|
|
137
|
+
|
|
138
|
+
# Redis для работы replay_attack_protection и ddos_attack_protection
|
|
139
|
+
c.redis_url = ENV.fetch('REDIS_URL', 'redis://localhost:6379/0')
|
|
140
|
+
c.redis_pass = ENV['REDIS_PASSWORD']
|
|
141
|
+
|
|
142
|
+
# ddos_attack_protection
|
|
143
|
+
# Максимальное количество запросов в окне времени (по умолчанию: 100)
|
|
144
|
+
c.rate_limit_max_requests = 100
|
|
145
|
+
# Размер временного окна в секундах (по умолчанию: 60)
|
|
146
|
+
c.rate_limit_window_seconds = 60
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Загрузка конфигурации с валидацией
|
|
150
|
+
RobustServerSocket.load!
|
|
151
|
+
```
|
|
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 | Размер временного окна в секундах |
|
|
167
|
+
|
|
168
|
+
## 🚀 Использование
|
|
169
|
+
|
|
170
|
+
### Базовая авторизация
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
# В контроллере или middleware
|
|
174
|
+
class ApiController < ApplicationController
|
|
175
|
+
before_action :authenticate_service!
|
|
176
|
+
|
|
177
|
+
private
|
|
178
|
+
|
|
179
|
+
def authenticate_service!
|
|
180
|
+
# Хедер, прописанный в RobustClientSocket (SECURE-TOKEN default)
|
|
181
|
+
token = request.headers['SECURE-TOKEN']&.sub(/^Bearer /, '')
|
|
182
|
+
|
|
183
|
+
@current_service = RobustServerSocket::ClientToken.validate!(token) # bang method (рейзит ошибки)
|
|
184
|
+
rescue RobustServerSocket::ClientToken::InvalidToken
|
|
185
|
+
render json: { error: 'Invalid token' }, status: :unauthorized
|
|
186
|
+
rescue RobustServerSocket::ClientToken::UnauthorizedClient
|
|
187
|
+
render json: { error: 'Unauthorized service' }, status: :forbidden
|
|
188
|
+
rescue RobustServerSocket::ClientToken::UsedToken
|
|
189
|
+
render json: { error: 'Token already used' }, status: :unauthorized
|
|
190
|
+
rescue RobustServerSocket::ClientToken::StaleToken
|
|
191
|
+
render json: { error: 'Token expired' }, status: :unauthorized
|
|
192
|
+
rescue RobustServerSocket::RateLimiter::RateLimitExceeded => e
|
|
193
|
+
render json: { error: e.message }, status: :too_many_requests
|
|
194
|
+
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
|
+
end
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Расширенное использование
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
# Создание объекта токена
|
|
214
|
+
token_string = request.headers['Authorization']&.sub(/^Bearer /, '')
|
|
215
|
+
client_token = RobustServerSocket::ClientToken.new(token_string)
|
|
216
|
+
|
|
217
|
+
# Проверка валидности (возвращает true/false)
|
|
218
|
+
if client_token.valid?
|
|
219
|
+
# Получение имени клиента
|
|
220
|
+
client_name = client_token.client
|
|
221
|
+
puts "Authorized client: #{client_name}"
|
|
222
|
+
else
|
|
223
|
+
# Токен невалиден
|
|
224
|
+
render json: { error: 'Unauthorized' }, status: :unauthorized
|
|
225
|
+
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
|
+
```
|
|
235
|
+
|
|
236
|
+
## ❌ Обработка ошибок
|
|
237
|
+
|
|
238
|
+
### Типы исключений
|
|
239
|
+
|
|
240
|
+
| Исключение | Причина | HTTP статус | Действие |
|
|
241
|
+
|-----------|---------|-------------|----------|
|
|
242
|
+
| `InvalidToken` | Токен не может быть расшифрован или имеет неверный формат | 401 | Проверьте корректность токена и ключей |
|
|
243
|
+
| `UnauthorizedClient` | Клиент не в whitelist | 403 | Добавьте клиента в `allowed_services` |
|
|
244
|
+
| `UsedToken` | Токен уже был использован | 401 | Клиент должен запросить новый токен |
|
|
245
|
+
| `StaleToken` | Токен истёк | 401 | Клиент должен запросить новый токен |
|
|
246
|
+
| `RateLimitExceeded` | Превышен лимит запросов | 429 | Клиент должен подождать или ретраить позже |
|
|
247
|
+
|
|
248
|
+
### Централизованная обработка
|
|
249
|
+
|
|
250
|
+
```ruby
|
|
251
|
+
# В ApplicationController
|
|
252
|
+
rescue_from RobustServerSocket::ClientToken::InvalidToken,
|
|
253
|
+
RobustServerSocket::ClientToken::UsedToken,
|
|
254
|
+
RobustServerSocket::ClientToken::StaleToken,
|
|
255
|
+
with: :unauthorized_response
|
|
256
|
+
|
|
257
|
+
rescue_from RobustServerSocket::ClientToken::UnauthorizedClient,
|
|
258
|
+
with: :forbidden_response
|
|
259
|
+
|
|
260
|
+
rescue_from RobustServerSocket::RateLimiter::RateLimitExceeded,
|
|
261
|
+
with: :rate_limit_response
|
|
262
|
+
|
|
263
|
+
private
|
|
264
|
+
|
|
265
|
+
def unauthorized_response(exception)
|
|
266
|
+
render json: {
|
|
267
|
+
error: 'Authentication failed',
|
|
268
|
+
message: exception.message,
|
|
269
|
+
type: exception.class.name
|
|
270
|
+
}, status: :unauthorized
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def forbidden_response(exception)
|
|
274
|
+
render json: {
|
|
275
|
+
error: 'Access denied',
|
|
276
|
+
message: exception.message,
|
|
277
|
+
type: exception.class.name
|
|
278
|
+
}, status: :forbidden
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def rate_limit_response(exception)
|
|
282
|
+
render json: {
|
|
283
|
+
error: 'Too many requests',
|
|
284
|
+
message: exception.message,
|
|
285
|
+
type: exception.class.name,
|
|
286
|
+
retry_after: RobustServerSocket.configuration.rate_limit_window_seconds
|
|
287
|
+
}, status: :too_many_requests
|
|
288
|
+
end
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## 🤝 Интеграция с RobustClientSocket
|
|
292
|
+
|
|
293
|
+
Для полноценной работы необходимо настроить клиентскую часть:
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
# На клиенте (RobustClientSocket)
|
|
297
|
+
RobustClientSocket.configure do |c|
|
|
298
|
+
c.service_name = 'core' # ← Должно быть в allowed_services сервера
|
|
299
|
+
c.keychain = {
|
|
300
|
+
payments: {
|
|
301
|
+
base_uri: 'https://payments.example.com',
|
|
302
|
+
public_key: '-----BEGIN PUBLIC KEY-----...' # Публичный ключ сервера payments
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# На сервере (RobustServerSocket)
|
|
308
|
+
RobustServerSocket.configure do |c|
|
|
309
|
+
c.allowed_services = %w[core] # ← Соответствует service_name клиента
|
|
310
|
+
c.private_key = '-----BEGIN PRIVATE KEY-----...' # Приватная пара к public_key
|
|
311
|
+
end
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## 📚 Дополнительные ресурсы
|
|
315
|
+
|
|
316
|
+
- [RobustClientSocket documentation](https://github.com/tee0zed/robust_client_socket)
|
|
317
|
+
- [RSA encryption best practices](https://www.openssl.org/docs/)
|
|
318
|
+
- [Redis security guide](https://redis.io/topics/security)
|
|
319
|
+
|
|
320
|
+
## 📝 Лицензия
|
|
321
|
+
|
|
322
|
+
См. файл [MIT-LICENSE](MIT-LICENSE)
|
|
323
|
+
|
|
324
|
+
## 🐛 Баги и предложения
|
|
325
|
+
|
|
326
|
+
Сообщайте о багах через ишью, или напрямую тг @cruel_mango или email tee0zed@gmail.com
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
module RobustServerSocket
|
|
2
|
+
module Cacher
|
|
3
|
+
class RedisConnectionError < StandardError; end
|
|
4
|
+
|
|
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
|
|
10
|
+
|
|
11
|
+
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
|
+
)
|
|
17
|
+
end
|
|
18
|
+
rescue ::Redis::BaseConnectionError => e
|
|
19
|
+
handle_redis_error(e, 'atomic_validate_and_log')
|
|
20
|
+
raise RedisConnectionError, "Failed to validate token: #{e.message}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def incr(key)
|
|
24
|
+
redis.with do |conn|
|
|
25
|
+
conn.pipelined do |pipeline|
|
|
26
|
+
pipeline.incrby(key, 1)
|
|
27
|
+
pipeline.expire(key, ttl_seconds)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
rescue ::Redis::BaseConnectionError => e
|
|
31
|
+
handle_redis_error(e, 'incr')
|
|
32
|
+
raise RedisConnectionError, "Failed to increment key: #{e.message}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def get(key)
|
|
36
|
+
redis.with do |conn|
|
|
37
|
+
conn.get(key)
|
|
38
|
+
end
|
|
39
|
+
rescue ::Redis::BaseConnectionError => e
|
|
40
|
+
handle_redis_error(e, 'get')
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def health_check
|
|
45
|
+
redis.with do |conn|
|
|
46
|
+
conn.ping == 'PONG'
|
|
47
|
+
end
|
|
48
|
+
rescue ::Redis::BaseConnectionError
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def with_redis(&block)
|
|
53
|
+
redis.with(&block)
|
|
54
|
+
rescue ::Redis::BaseConnectionError => e
|
|
55
|
+
handle_redis_error(e, 'with_redis')
|
|
56
|
+
raise ::RedisConnectionError, "Redis operation failed: #{e.message}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Clear cached Redis connection pool (useful for hot reloading in development)
|
|
60
|
+
def clear_redis_pool_cache!
|
|
61
|
+
@pool = nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def lua_atomic_validate_and_log
|
|
67
|
+
<<~LUA
|
|
68
|
+
local key = KEYS[1]
|
|
69
|
+
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
|
|
76
|
+
return 'stale'
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
-- Check if token was already used
|
|
80
|
+
local current = redis.call('GET', key)
|
|
81
|
+
if current and tonumber(current) > 0 then
|
|
82
|
+
return 'used'
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
-- Mark token as used
|
|
86
|
+
redis.call('INCRBY', key, 1)
|
|
87
|
+
redis.call('EXPIRE', key, ttl)
|
|
88
|
+
|
|
89
|
+
return 'ok'
|
|
90
|
+
LUA
|
|
91
|
+
end
|
|
92
|
+
|
|
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
|
+
# Cache Redis connection pool at module level for the lifetime of the Rails process
|
|
99
|
+
# This avoids recreating the connection pool on every Redis operation
|
|
100
|
+
def redis
|
|
101
|
+
@pool ||= ::ConnectionPool::Wrapper.new(**pool_config) do
|
|
102
|
+
::Redis.new(redis_config)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def pool_config
|
|
107
|
+
{
|
|
108
|
+
size: ENV.fetch('REDIS_POOL_SIZE', 25).to_i,
|
|
109
|
+
timeout: ENV.fetch('REDIS_POOL_TIMEOUT', 1).to_f
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def redis_config
|
|
114
|
+
config = {
|
|
115
|
+
url: ::RobustServerSocket.configuration.redis_url,
|
|
116
|
+
reconnect_attempts: 3,
|
|
117
|
+
timeout: 1.0,
|
|
118
|
+
connect_timeout: 2.0
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
password = ::RobustServerSocket.configuration.redis_pass
|
|
122
|
+
config[:password] = password if password && !password.empty?
|
|
123
|
+
|
|
124
|
+
config
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def handle_redis_error(error, operation)
|
|
128
|
+
warn "Redis operation '#{operation}' failed: #{error.class} - #{error.message}"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -1,35 +1,12 @@
|
|
|
1
|
-
require_relative 'secure_token/cacher'
|
|
2
|
-
require_relative 'secure_token/decrypt'
|
|
3
|
-
require_relative 'rate_limiter'
|
|
4
|
-
|
|
5
1
|
module RobustServerSocket
|
|
6
2
|
class ClientToken
|
|
7
3
|
TOKEN_REGEXP = /\A(.+)_(\d{10,})\z/.freeze
|
|
8
4
|
|
|
9
5
|
InvalidToken = Class.new(StandardError)
|
|
10
|
-
UnauthorizedClient = Class.new(StandardError)
|
|
11
|
-
UsedToken = Class.new(StandardError)
|
|
12
|
-
StaleToken = Class.new(StandardError)
|
|
13
6
|
|
|
14
7
|
def self.validate!(secure_token)
|
|
15
8
|
new(secure_token).tap do |instance|
|
|
16
|
-
|
|
17
|
-
raise UnauthorizedClient unless instance.client
|
|
18
|
-
|
|
19
|
-
RateLimiter.check!(instance.client)
|
|
20
|
-
|
|
21
|
-
result = instance.atomic_validate_and_log_token
|
|
22
|
-
|
|
23
|
-
case result
|
|
24
|
-
when 'stale'
|
|
25
|
-
raise StaleToken
|
|
26
|
-
when 'used'
|
|
27
|
-
raise UsedToken
|
|
28
|
-
when 'ok'
|
|
29
|
-
true
|
|
30
|
-
else
|
|
31
|
-
raise InvalidToken, "Unexpected validation result: #{result}"
|
|
32
|
-
end
|
|
9
|
+
instance.validate!
|
|
33
10
|
end
|
|
34
11
|
end
|
|
35
12
|
|
|
@@ -38,15 +15,25 @@ module RobustServerSocket
|
|
|
38
15
|
@client = nil
|
|
39
16
|
end
|
|
40
17
|
|
|
18
|
+
def validate!
|
|
19
|
+
raise InvalidToken unless validate_decrypted_token
|
|
20
|
+
modules_checks!
|
|
21
|
+
end
|
|
22
|
+
|
|
41
23
|
def valid?
|
|
42
|
-
|
|
43
|
-
client &&
|
|
44
|
-
RateLimiter.check(client) &&
|
|
45
|
-
atomic_validate_and_log_token == 'ok')
|
|
24
|
+
validate_decrypted_token && modules_checks
|
|
46
25
|
rescue StandardError
|
|
47
26
|
false
|
|
48
27
|
end
|
|
49
28
|
|
|
29
|
+
def modules_checks
|
|
30
|
+
true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def modules_checks!
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
|
|
50
37
|
def client
|
|
51
38
|
@client ||= begin
|
|
52
39
|
target = client_name.strip
|
|
@@ -54,23 +41,14 @@ module RobustServerSocket
|
|
|
54
41
|
end
|
|
55
42
|
end
|
|
56
43
|
|
|
57
|
-
def token_not_expired?
|
|
58
|
-
token_expiration_time > Time.now.utc.to_i - timestamp
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def atomic_validate_and_log_token
|
|
62
|
-
SecureToken::Cacher.atomic_validate_and_log(
|
|
63
|
-
decrypted_token,
|
|
64
|
-
token_expiration_time + 300,
|
|
65
|
-
timestamp,
|
|
66
|
-
token_expiration_time
|
|
67
|
-
)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
44
|
def decrypted_token
|
|
71
45
|
@decrypted_token ||= SecureToken::Decrypt.call(@secure_token)
|
|
72
46
|
end
|
|
73
47
|
|
|
48
|
+
def validate_decrypted_token
|
|
49
|
+
!!decrypted_token
|
|
50
|
+
end
|
|
51
|
+
|
|
74
52
|
private
|
|
75
53
|
|
|
76
54
|
def allowed_clients
|
|
@@ -97,6 +75,7 @@ module RobustServerSocket
|
|
|
97
75
|
RobustServerSocket.configuration.token_expiration_time
|
|
98
76
|
end
|
|
99
77
|
|
|
78
|
+
|
|
100
79
|
# Do we need it? It would be useful only if public_key compromised
|
|
101
80
|
# def secure_compare(a, b)
|
|
102
81
|
# return false unless a.bytesize == b.bytesize
|
|
@@ -4,6 +4,14 @@ module RobustServerSocket
|
|
|
4
4
|
|
|
5
5
|
attr_reader :configuration, :configured
|
|
6
6
|
|
|
7
|
+
def _push_modules_check_code(code)
|
|
8
|
+
configuration._modules_check_rows.push(code)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def _push_bang_modules_check_code(code)
|
|
12
|
+
configuration._bang_modules_check_rows.push(code)
|
|
13
|
+
end
|
|
14
|
+
|
|
7
15
|
def configure
|
|
8
16
|
@configuration ||= ConfigStore.new
|
|
9
17
|
yield(configuration)
|
|
@@ -43,13 +51,24 @@ module RobustServerSocket
|
|
|
43
51
|
end
|
|
44
52
|
|
|
45
53
|
class ConfigStore
|
|
46
|
-
attr_accessor :allowed_services, :private_key, :token_expiration_time, :redis_url, :redis_pass,
|
|
47
|
-
:
|
|
54
|
+
attr_accessor :allowed_services, :private_key, :token_expiration_time, :store_used_token_time, :redis_url, :redis_pass,
|
|
55
|
+
:rate_limit_max_requests, :rate_limit_window_seconds, :using_modules
|
|
56
|
+
|
|
57
|
+
attr_reader :_modules_check_rows, :_bang_modules_check_rows
|
|
48
58
|
|
|
49
59
|
def initialize
|
|
50
|
-
@rate_limit_enabled = false
|
|
51
60
|
@rate_limit_max_requests = 100
|
|
52
61
|
@rate_limit_window_seconds = 60
|
|
62
|
+
@store_used_token_time = 600
|
|
63
|
+
@token_expiration_time = 10
|
|
64
|
+
@using_modules = %i[
|
|
65
|
+
client_auth_protection
|
|
66
|
+
dos_attack_protection
|
|
67
|
+
replay_attack_protection
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
@_modules_check_rows = []
|
|
71
|
+
@_bang_modules_check_rows = []
|
|
53
72
|
end
|
|
54
73
|
end
|
|
55
74
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module RobustServerSocket
|
|
2
|
+
module Modules
|
|
3
|
+
module ClientAuthProtection
|
|
4
|
+
UnauthorizedClient = Class.new(StandardError)
|
|
5
|
+
|
|
6
|
+
def self.included(_base)
|
|
7
|
+
RobustServerSocket._push_modules_check_code('validate_client')
|
|
8
|
+
RobustServerSocket._push_bang_modules_check_code("validate_client!\n")
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def validate_client
|
|
12
|
+
!!client
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def validate_client!
|
|
16
|
+
raise UnauthorizedClient unless validate_client
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require_relative '../cacher'
|
|
2
|
+
require_relative '../rate_limiter'
|
|
3
|
+
|
|
4
|
+
module RobustServerSocket
|
|
5
|
+
module Modules
|
|
6
|
+
module DosAttackProtection
|
|
7
|
+
def self.included(_base)
|
|
8
|
+
RobustServerSocket._push_modules_check_code('validate_rate_limit')
|
|
9
|
+
RobustServerSocket._push_bang_modules_check_code("validate_rate_limit!\n")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def validate_rate_limit
|
|
13
|
+
!!RateLimiter.check(client)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def validate_rate_limit!
|
|
17
|
+
RateLimiter.check!(client)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require_relative '../cacher'
|
|
2
|
+
|
|
3
|
+
module RobustServerSocket
|
|
4
|
+
module Modules
|
|
5
|
+
module ReplayAttackProtection
|
|
6
|
+
UsedToken = Class.new(StandardError)
|
|
7
|
+
StaleToken = Class.new(StandardError)
|
|
8
|
+
|
|
9
|
+
def self.included(_base)
|
|
10
|
+
RobustServerSocket._push_modules_check_code('atomic_validate_and_log_token')
|
|
11
|
+
RobustServerSocket._push_bang_modules_check_code("atomic_validate_and_log_token!\n")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def atomic_validate_and_log_token!
|
|
15
|
+
result = Cacher.atomic_validate_and_log(
|
|
16
|
+
decrypted_token,
|
|
17
|
+
store_used_token_time,
|
|
18
|
+
timestamp,
|
|
19
|
+
token_expiration_time
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
case result
|
|
23
|
+
when 'ok'
|
|
24
|
+
true
|
|
25
|
+
when 'stale'
|
|
26
|
+
raise StaleToken
|
|
27
|
+
when 'used'
|
|
28
|
+
raise UsedToken
|
|
29
|
+
else
|
|
30
|
+
raise StandardError, "Unexpected result: #{result}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def atomic_validate_and_log_token
|
|
35
|
+
Cacher.atomic_validate_and_log(
|
|
36
|
+
decrypted_token,
|
|
37
|
+
store_used_token_time, # window for storing used token
|
|
38
|
+
timestamp,
|
|
39
|
+
token_expiration_time
|
|
40
|
+
) == 'ok'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def store_used_token_time
|
|
46
|
+
RobustServerSocket.configuration.store_used_token_time
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|