robust_server_socket 0.3.3 → 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 +67 -105
- data/README.md +106 -167
- data/lib/robust_server_socket/cacher.rb +132 -0
- data/lib/robust_server_socket/client_token.rb +16 -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 +6 -3
- data/lib/robust_server_socket/secure_token/cacher.rb +0 -138
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b7b680d77f57104621b01b7abca736c21d819abc4e59355f5e70c3dff05df2eb
|
|
4
|
+
data.tar.gz: 15d86badaa9f0c76c97f330886188a3517b7d4dfd38d94c27f7c000cee1088d3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cbc390327dc4b6d4f382333c822cc367d0b936958d6ad8def8149e6b55a97d4137ff30690b3c3f4d9156120726a02ef12550e65065effb827a0adbe1bd196597
|
|
7
|
+
data.tar.gz: f8bc821809cabf3d0769c37549160f3d7b1456c0bc5bb3b787530171ba0eb83389fb9b4a4f65f94abc182fea34cabc740c1ca54ccd0f08d7578d5407956b0865
|
data/README.en.md
CHANGED
|
@@ -2,7 +2,70 @@
|
|
|
2
2
|
|
|
3
3
|
Gem for inter-service authorization, used in pair with RobustClientSocket
|
|
4
4
|
|
|
5
|
-
### ⚠️ Not Production Tested (yet)
|
|
5
|
+
### ⚠️ Not Production Tested (yet) but tested in staging environment
|
|
6
|
+
|
|
7
|
+
`Not vibecoded`
|
|
8
|
+
|
|
9
|
+
## WHY
|
|
10
|
+
|
|
11
|
+
### The Problem
|
|
12
|
+
|
|
13
|
+
When building microservice architecture, the server side faces:
|
|
14
|
+
|
|
15
|
+
- **Lack of verification**: How to verify that a request came from a trusted service?
|
|
16
|
+
- **Replay attacks**: Intercepted requests can be replayed
|
|
17
|
+
- **DDoS attacks**: Need to limit request frequency
|
|
18
|
+
- **Boilerplate code**: Repetitive validation logic in every service
|
|
19
|
+
|
|
20
|
+
### The Solution
|
|
21
|
+
|
|
22
|
+
RobustServerSocket provides:
|
|
23
|
+
|
|
24
|
+
- **RSA decryption**: Token authenticity verification
|
|
25
|
+
- **Client whitelist**: Only authorized services allowed
|
|
26
|
+
- **Replay protection**: Blacklist of used tokens in Redis
|
|
27
|
+
- **Rate limiting**: Per-client request limits
|
|
28
|
+
|
|
29
|
+
## HOW IT WORKS
|
|
30
|
+
|
|
31
|
+
### Architecture
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Incoming request with 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
|
+
### Validation Flow
|
|
55
|
+
|
|
56
|
+
1. **Decryption**: Base64 decode → RSA decrypt with private key
|
|
57
|
+
2. **Parsing**: Extract `{client_name}_{timestamp}` from token
|
|
58
|
+
3. **Whitelist**: Verify client_name is in `allowed_services`
|
|
59
|
+
4. **Rate limit**: Check request count within window
|
|
60
|
+
5. **Replay check**: Verify token hasn't been used (Redis)
|
|
61
|
+
6. **Staleness**: Verify timestamp is current
|
|
62
|
+
|
|
63
|
+
### Modular System
|
|
64
|
+
|
|
65
|
+
Checks are enabled via `using_modules`:
|
|
66
|
+
- `:client_auth_protection` — client whitelist
|
|
67
|
+
- `:replay_attack_protection` — prevent token reuse
|
|
68
|
+
- `:dos_attack_protection` — rate limiting
|
|
6
69
|
|
|
7
70
|
## 📋 Table of Contents
|
|
8
71
|
|
|
@@ -84,12 +147,7 @@ RobustServerSocket.configure do |c|
|
|
|
84
147
|
# Redis for storing used tokens
|
|
85
148
|
c.redis_url = ENV.fetch('REDIS_URL', 'redis://localhost:6379/0')
|
|
86
149
|
c.redis_pass = ENV['REDIS_PASSWORD'] # can be nil for local development
|
|
87
|
-
|
|
88
|
-
# OPTIONAL PARAMETERS: Rate Limiting
|
|
89
|
-
|
|
90
|
-
# Enable rate limiting (default: false)
|
|
91
|
-
c.rate_limit_enabled = true
|
|
92
|
-
|
|
150
|
+
|
|
93
151
|
# Maximum requests per time window (default: 100)
|
|
94
152
|
c.rate_limit_max_requests = 100
|
|
95
153
|
|
|
@@ -169,74 +227,6 @@ rescue => e
|
|
|
169
227
|
end
|
|
170
228
|
```
|
|
171
229
|
|
|
172
|
-
### Manual Rate Limiting
|
|
173
|
-
|
|
174
|
-
```ruby
|
|
175
|
-
# Check current attempt count
|
|
176
|
-
attempts = RobustServerSocket::RateLimiter.current_attempts('core')
|
|
177
|
-
puts "Core service made #{attempts} requests"
|
|
178
|
-
|
|
179
|
-
# Reset counter for specific client
|
|
180
|
-
RobustServerSocket::RateLimiter.reset!('core')
|
|
181
|
-
|
|
182
|
-
# Check with exception on exceeded limit
|
|
183
|
-
begin
|
|
184
|
-
RobustServerSocket::RateLimiter.check!('core')
|
|
185
|
-
rescue RobustServerSocket::RateLimiter::RateLimitExceeded => e
|
|
186
|
-
puts e.message # "Rate limit exceeded for core: 101/100 requests per 60s"
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
# Check without exception (returns false when exceeded)
|
|
190
|
-
if RobustServerSocket::RateLimiter.check('core')
|
|
191
|
-
# Limit not exceeded
|
|
192
|
-
else
|
|
193
|
-
# Limit exceeded
|
|
194
|
-
end
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
## 🚦 Rate Limiting (Request Rate Limiting)
|
|
198
|
-
|
|
199
|
-
### How It Works
|
|
200
|
-
|
|
201
|
-
Rate Limiter protects your service from overload by limiting the number of requests from each client within a time window.
|
|
202
|
-
|
|
203
|
-
**Characteristics:**
|
|
204
|
-
- **Per-client counters**: Separate counter for each service
|
|
205
|
-
- **Sliding window**: Window resets automatically after time expires
|
|
206
|
-
- **Atomicity**: Increment and check are performed atomically (Redis LUA script)
|
|
207
|
-
- **Fail-open**: When Redis is unavailable, requests are allowed (not blocked)
|
|
208
|
-
|
|
209
|
-
### Limit Configuration
|
|
210
|
-
|
|
211
|
-
```ruby
|
|
212
|
-
RobustServerSocket.configure do |c|
|
|
213
|
-
# Enable rate limiting
|
|
214
|
-
c.rate_limit_enabled = true
|
|
215
|
-
|
|
216
|
-
# For low-traffic microservices
|
|
217
|
-
c.rate_limit_max_requests = 50
|
|
218
|
-
c.rate_limit_window_seconds = 60
|
|
219
|
-
end
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
### Monitoring
|
|
223
|
-
|
|
224
|
-
```ruby
|
|
225
|
-
# Check current state
|
|
226
|
-
clients = ['core', 'payments', 'notifications']
|
|
227
|
-
clients.each do |client|
|
|
228
|
-
attempts = RobustServerSocket::RateLimiter.current_attempts(client)
|
|
229
|
-
max = RobustServerSocket.configuration.rate_limit_max_requests
|
|
230
|
-
puts "#{client}: #{attempts}/#{max}"
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
# In metrics (Prometheus, StatsD, etc.)
|
|
234
|
-
clients.each do |client|
|
|
235
|
-
attempts = RobustServerSocket::RateLimiter.current_attempts(client)
|
|
236
|
-
Metrics.gauge("rate_limiter.attempts.#{client}", attempts)
|
|
237
|
-
end
|
|
238
|
-
```
|
|
239
|
-
|
|
240
230
|
## ❌ Error Handling
|
|
241
231
|
|
|
242
232
|
### Exception Types
|
|
@@ -292,32 +282,6 @@ def rate_limit_response(exception)
|
|
|
292
282
|
end
|
|
293
283
|
```
|
|
294
284
|
|
|
295
|
-
## 💡 Usage Recommendations
|
|
296
|
-
|
|
297
|
-
### 1. Key Management
|
|
298
|
-
|
|
299
|
-
**✅ DO:**
|
|
300
|
-
```ruby
|
|
301
|
-
# Store keys in environment variables
|
|
302
|
-
c.private_key = ENV['ROBUST_SERVER_PRIVATE_KEY']
|
|
303
|
-
|
|
304
|
-
# Use secrets management (AWS Secrets Manager, Vault, etc.)
|
|
305
|
-
c.private_key = Rails.application.credentials.dig(:robust_server, :private_key)
|
|
306
|
-
|
|
307
|
-
# Generate keys correctly
|
|
308
|
-
# openssl genrsa -out private_key.pem 2048
|
|
309
|
-
# openssl rsa -in private_key.pem -pubout -out public_key.pem
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
**❌ DON'T:**
|
|
313
|
-
```ruby
|
|
314
|
-
# DON'T commit keys to git
|
|
315
|
-
c.private_key = "-----BEGIN PRIVATE KEY-----\nMII..."
|
|
316
|
-
|
|
317
|
-
# DON'T use weak keys
|
|
318
|
-
# Minimum RSA-2048, RSA-4096 recommended for high security
|
|
319
|
-
```
|
|
320
|
-
|
|
321
285
|
### 2. Redis Configuration
|
|
322
286
|
|
|
323
287
|
**✅ DO:**
|
|
@@ -482,7 +446,7 @@ REDIS_POOL = ConnectionPool.new(size: 25, timeout: 5) do
|
|
|
482
446
|
)
|
|
483
447
|
end
|
|
484
448
|
|
|
485
|
-
# In RobustServerSocket::
|
|
449
|
+
# In RobustServerSocket::Cacher
|
|
486
450
|
def self.with_redis
|
|
487
451
|
REDIS_POOL.with do |redis|
|
|
488
452
|
yield redis
|
|
@@ -505,7 +469,6 @@ end
|
|
|
505
469
|
```ruby
|
|
506
470
|
RobustServerSocket.configure do |c|
|
|
507
471
|
# For high-load systems
|
|
508
|
-
c.rate_limit_enabled = true
|
|
509
472
|
c.rate_limit_max_requests = 1000 # Increase limit
|
|
510
473
|
c.rate_limit_window_seconds = 60
|
|
511
474
|
|
|
@@ -638,7 +601,6 @@ end
|
|
|
638
601
|
|
|
639
602
|
## 📚 Additional Resources
|
|
640
603
|
|
|
641
|
-
- [BENCHMARK_ANALYSIS.md](BENCHMARK_ANALYSIS.md)
|
|
642
604
|
- [RobustClientSocket documentation](../robust_client_socket/README.md)
|
|
643
605
|
- [RSA encryption best practices](https://www.openssl.org/docs/)
|
|
644
606
|
- [Redis security guide](https://redis.io/topics/security)
|
|
@@ -649,4 +611,4 @@ See [MIT-LICENSE](MIT-LICENSE) file
|
|
|
649
611
|
|
|
650
612
|
## 🐛 Bugs and Suggestions
|
|
651
613
|
|
|
652
|
-
Report issues
|
|
614
|
+
Report issues to my telegram @cruel_mango or to email tee0zed@gmail.com
|
data/README.md
CHANGED
|
@@ -1,8 +1,71 @@
|
|
|
1
1
|
# RobustServerSocket
|
|
2
2
|
|
|
3
|
-
Gem для межсервисной
|
|
3
|
+
Gem для межсервисной аутентификации, используется в паре с RobustClientSocket
|
|
4
4
|
|
|
5
|
-
### ⚠️ Not Production Tested (yet)
|
|
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
|
|
6
69
|
|
|
7
70
|
## 📋 Содержание
|
|
8
71
|
|
|
@@ -19,33 +82,27 @@ RobustServerSocket реализует многоуровневую систем
|
|
|
19
82
|
### 1. Криптографическая защита
|
|
20
83
|
- **RSA-2048 шифрование**: Используется пара ключей RSA с минимальной длиной 2048 бит
|
|
21
84
|
- **Валидация ключей**: Автоматическая проверка размера ключа при конфигурации
|
|
22
|
-
- **Асимметричное шифрование**: Приватный ключ на сервере, публичный — у клиентов
|
|
23
|
-
|
|
24
|
-
### 2. Защита от повторного использования токенов
|
|
25
|
-
- **Одноразовые токены**: Каждый токен может быть использован только один раз
|
|
26
|
-
- **Blacklist в Redis**: Использованные токены автоматически добавляются в черный список
|
|
27
|
-
- **Атомарная проверка**: Race condition защищена благодаря Redis Lua скриптам
|
|
28
|
-
|
|
29
|
-
### 3. Временные ограничения
|
|
30
|
-
- **Expiration time**: Настраиваемое время жизни токена
|
|
31
|
-
- **Автоматическое истечение**: Токены автоматически становятся недействительными после истечения времени
|
|
32
|
-
- **Защита от replay attacks**: Старые токены не могут быть использованы повторно
|
|
33
85
|
|
|
34
|
-
###
|
|
35
|
-
- **Whitelist клиентов**: Только авторизованные сервисы могут
|
|
86
|
+
### 2. Контроль доступа
|
|
87
|
+
- **Whitelist клиентов**: Только авторизованные сервисы могут подключаться, при включенном модуле `:client_auth_protection`
|
|
36
88
|
- **Идентификация по имени**: Каждый клиент должен быть явно указан в `allowed_services`
|
|
37
|
-
- **Валидация формата токена**: Строгая проверка структуры токена
|
|
38
89
|
|
|
39
|
-
###
|
|
40
|
-
- **Защита от
|
|
41
|
-
- **
|
|
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**: Распределение запросов во времени
|
|
42
100
|
- **Fail-open стратегия**: Если Redis недоступен, запросы пропускаются (для надёжности)
|
|
43
|
-
- **Per-client лимиты**: Индивидуальные счётчики для каждого клиента
|
|
44
101
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
102
|
+
-
|
|
103
|
+
### 5. Защита от SSL stripping MITM attack
|
|
104
|
+
- **Принудительное HTTPS на сервере**: Все запросы должны быть совершены по HTTPS, чтобы защитить токены от перехвата
|
|
105
|
+
- **Включается на RobustClientSoket, ключём `ssl_verify: true`**
|
|
49
106
|
|
|
50
107
|
## 📦 Установка
|
|
51
108
|
|
|
@@ -53,13 +110,21 @@ RobustServerSocket реализует многоуровневую систем
|
|
|
53
110
|
gem 'robust_server_socket'
|
|
54
111
|
```
|
|
55
112
|
|
|
113
|
+
и на клиенте:
|
|
114
|
+
```ruby
|
|
115
|
+
gem 'robust_client_socket'
|
|
116
|
+
```
|
|
56
117
|
## ⚙️ Конфигурация
|
|
57
118
|
|
|
58
119
|
Создайте файл `config/initializers/robust_server_socket.rb`:
|
|
59
120
|
|
|
60
121
|
```ruby
|
|
61
122
|
RobustServerSocket.configure do |c|
|
|
62
|
-
|
|
123
|
+
c.using_modules = %i[
|
|
124
|
+
:client_auth_protection
|
|
125
|
+
:replay_attack_protection
|
|
126
|
+
:dos_attack_protection
|
|
127
|
+
]
|
|
63
128
|
|
|
64
129
|
# Приватный ключ сервиса (RSA-2048 или выше)
|
|
65
130
|
c.private_key = ENV['ROBUST_SERVER_PRIVATE_KEY']
|
|
@@ -67,15 +132,14 @@ RobustServerSocket.configure do |c|
|
|
|
67
132
|
|
|
68
133
|
# Список разрешённых сервисов (whitelist)
|
|
69
134
|
# Должен совпадать с именами RobustClientSocket клиента
|
|
135
|
+
# Для client_auth_protection
|
|
70
136
|
c.allowed_services = %w[core payments notifications]
|
|
71
137
|
|
|
72
|
-
# Redis для работы
|
|
138
|
+
# Redis для работы replay_attack_protection и ddos_attack_protection
|
|
73
139
|
c.redis_url = ENV.fetch('REDIS_URL', 'redis://localhost:6379/0')
|
|
74
140
|
c.redis_pass = ENV['REDIS_PASSWORD']
|
|
75
141
|
|
|
76
|
-
#
|
|
77
|
-
# Включить ограничение частоты запросов (по умолчанию: false)
|
|
78
|
-
c.rate_limit_enabled = true
|
|
142
|
+
# ddos_attack_protection
|
|
79
143
|
# Максимальное количество запросов в окне времени (по умолчанию: 100)
|
|
80
144
|
c.rate_limit_max_requests = 100
|
|
81
145
|
# Размер временного окна в секундах (по умолчанию: 60)
|
|
@@ -85,19 +149,21 @@ end
|
|
|
85
149
|
# Загрузка конфигурации с валидацией
|
|
86
150
|
RobustServerSocket.load!
|
|
87
151
|
```
|
|
152
|
+
`using_modules` - это используемые модули, добавление или удаление которых изменит поведение гема.
|
|
88
153
|
|
|
89
154
|
### Опции конфигурации сервиса
|
|
90
155
|
|
|
91
|
-
| Параметр
|
|
92
|
-
|
|
93
|
-
| `private_key`
|
|
94
|
-
| `token_expiration_time`
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
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 | Размер временного окна в секундах |
|
|
101
167
|
|
|
102
168
|
## 🚀 Использование
|
|
103
169
|
|
|
@@ -167,61 +233,6 @@ rescue => e
|
|
|
167
233
|
end
|
|
168
234
|
```
|
|
169
235
|
|
|
170
|
-
### Rate Limiting вручную
|
|
171
|
-
|
|
172
|
-
```ruby
|
|
173
|
-
# Проверка текущего количества попыток
|
|
174
|
-
attempts = RobustServerSocket::RateLimiter.current_attempts('core')
|
|
175
|
-
puts "Core service made #{attempts} requests"
|
|
176
|
-
|
|
177
|
-
# Сброс счётчика для конкретного клиента
|
|
178
|
-
RobustServerSocket::RateLimiter.reset!('core')
|
|
179
|
-
|
|
180
|
-
# Проверка с исключением при превышении
|
|
181
|
-
begin
|
|
182
|
-
RobustServerSocket::RateLimiter.check!('core')
|
|
183
|
-
rescue RobustServerSocket::RateLimiter::RateLimitExceeded => e
|
|
184
|
-
puts e.message # "Rate limit exceeded for core: 101/100 requests per 60s"
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
# Проверка без исключения (возвращает false при превышении)
|
|
188
|
-
if RobustServerSocket::RateLimiter.check('core')
|
|
189
|
-
# Лимит не превышен
|
|
190
|
-
else
|
|
191
|
-
# Лимит превышен
|
|
192
|
-
end
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
## 🚦 Rate Limiting (Ограничение частоты запросов)
|
|
196
|
-
|
|
197
|
-
### Принцип работы
|
|
198
|
-
|
|
199
|
-
Rate Limiter защищает ваш сервис от перегрузки, ограничивая количество запросов от каждого клиента в определённом временном окне.
|
|
200
|
-
|
|
201
|
-
**Характеристики:**
|
|
202
|
-
- **Per-client counters**: Отдельный счётчик для каждого сервиса
|
|
203
|
-
- **Sliding window**: Окно сбрасывается автоматически после истечения времени
|
|
204
|
-
- **Атомарность**: Инкремент и проверка выполняются атомарно (Redis LUA script)
|
|
205
|
-
- **Fail-open**: При недоступности Redis запросы пропускаются (не блокируются)
|
|
206
|
-
|
|
207
|
-
### Мониторинг
|
|
208
|
-
|
|
209
|
-
```ruby
|
|
210
|
-
# Проверка текущего состояния
|
|
211
|
-
clients = ['core', 'payments', 'notifications']
|
|
212
|
-
clients.each do |client|
|
|
213
|
-
attempts = RobustServerSocket::RateLimiter.current_attempts(client)
|
|
214
|
-
max = RobustServerSocket.configuration.rate_limit_max_requests
|
|
215
|
-
puts "#{client}: #{attempts}/#{max}"
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
# В метриках (Prometheus, StatsD и т.д.)
|
|
219
|
-
clients.each do |client|
|
|
220
|
-
attempts = RobustServerSocket::RateLimiter.current_attempts(client)
|
|
221
|
-
Metrics.gauge("rate_limiter.attempts.#{client}", attempts)
|
|
222
|
-
end
|
|
223
|
-
```
|
|
224
|
-
|
|
225
236
|
## ❌ Обработка ошибок
|
|
226
237
|
|
|
227
238
|
### Типы исключений
|
|
@@ -277,77 +288,6 @@ def rate_limit_response(exception)
|
|
|
277
288
|
end
|
|
278
289
|
```
|
|
279
290
|
|
|
280
|
-
## 💡 Рекомендации по использованию
|
|
281
|
-
|
|
282
|
-
### 1. Управление ключами
|
|
283
|
-
|
|
284
|
-
**✅ DO:**
|
|
285
|
-
```ruby
|
|
286
|
-
# Храните ключи в переменных окружения
|
|
287
|
-
c.private_key = ENV['ROBUST_SERVER_PRIVATE_KEY']
|
|
288
|
-
|
|
289
|
-
# Используйте secrets management (AWS Secrets Manager, Vault, и т.д.)
|
|
290
|
-
c.private_key = Rails.application.credentials.dig(:robust_server, :private_key)
|
|
291
|
-
|
|
292
|
-
# Генерируйте ключи правильно
|
|
293
|
-
# openssl genrsa -out private_key.pem 2048
|
|
294
|
-
# openssl rsa -in private_key.pem -pubout -out public_key.pem
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
**❌ DON'T:**
|
|
298
|
-
```ruby
|
|
299
|
-
# НЕ коммитьте ключи в git
|
|
300
|
-
c.private_key = "-----BEGIN PRIVATE KEY-----\nMII..."
|
|
301
|
-
|
|
302
|
-
# НЕ используйте слабые ключи
|
|
303
|
-
# Минимум RSA-2048, рекомендуется RSA-4096 для высокой безопасности
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
### 2. Конфигурация Redis
|
|
307
|
-
|
|
308
|
-
**✅ DO:**
|
|
309
|
-
```ruby
|
|
310
|
-
# Используйте отдельный namespace для каждого окружения
|
|
311
|
-
c.redis_url = ENV.fetch('REDIS_URL', 'redis://localhost:6379/0')
|
|
312
|
-
|
|
313
|
-
# Настройте connection pool в production
|
|
314
|
-
# В config/initializers/redis.rb
|
|
315
|
-
Redis.current = ConnectionPool.new(size: 5, timeout: 5) do
|
|
316
|
-
Redis.new(url: ENV['REDIS_URL'], password: ENV['REDIS_PASSWORD'])
|
|
317
|
-
end
|
|
318
|
-
|
|
319
|
-
# Мониторьте состояние Redis
|
|
320
|
-
# Используйте Redis Sentinel или Cluster для высокой доступности
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
**❌ DON'T:**
|
|
324
|
-
```ruby
|
|
325
|
-
# НЕ используйте одну БД Redis для всех окружений, используйте отдельную bd redis
|
|
326
|
-
# НЕ игнорируйте ошибки Redis (rate limiter уже fail-open, но логируйте их)
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
### 5. Whitelist сервисов
|
|
330
|
-
|
|
331
|
-
```ruby
|
|
332
|
-
# Явно указывайте только необходимые сервисы
|
|
333
|
-
c.allowed_services = %w[core payments] # ✅
|
|
334
|
-
|
|
335
|
-
# НЕ используйте wildcards или регулярные выражения
|
|
336
|
-
c.allowed_services = %w[*] # ❌ ОПАСНО!
|
|
337
|
-
|
|
338
|
-
# Синхронизируйте с keychain клиента
|
|
339
|
-
# Server (robust_server_socket):
|
|
340
|
-
c.allowed_services = %w[core]
|
|
341
|
-
|
|
342
|
-
# Client (robust_client_socket):
|
|
343
|
-
c.keychain = {
|
|
344
|
-
core: { # ← Должно совпадать
|
|
345
|
-
base_uri: 'https://core.example.com',
|
|
346
|
-
public_key: '-----BEGIN PUBLIC KEY-----...'
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
```
|
|
350
|
-
|
|
351
291
|
## 🤝 Интеграция с RobustClientSocket
|
|
352
292
|
|
|
353
293
|
Для полноценной работы необходимо настроить клиентскую часть:
|
|
@@ -373,7 +313,6 @@ end
|
|
|
373
313
|
|
|
374
314
|
## 📚 Дополнительные ресурсы
|
|
375
315
|
|
|
376
|
-
- [BENCHMARK_ANALYSIS.md](BENCHMARK_ANALYSIS.md)
|
|
377
316
|
- [RobustClientSocket documentation](https://github.com/tee0zed/robust_client_socket)
|
|
378
317
|
- [RSA encryption best practices](https://www.openssl.org/docs/)
|
|
379
318
|
- [Redis security guide](https://redis.io/topics/security)
|
|
@@ -384,4 +323,4 @@ end
|
|
|
384
323
|
|
|
385
324
|
## 🐛 Баги и предложения
|
|
386
325
|
|
|
387
|
-
Сообщайте о
|
|
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,15 +1,8 @@
|
|
|
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|
|
|
@@ -23,34 +16,24 @@ module RobustServerSocket
|
|
|
23
16
|
end
|
|
24
17
|
|
|
25
18
|
def validate!
|
|
26
|
-
raise InvalidToken unless
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
RateLimiter.check!(client)
|
|
30
|
-
|
|
31
|
-
result = atomic_validate_and_log_token
|
|
32
|
-
|
|
33
|
-
case result
|
|
34
|
-
when 'stale'
|
|
35
|
-
raise StaleToken
|
|
36
|
-
when 'used'
|
|
37
|
-
raise UsedToken
|
|
38
|
-
when 'ok'
|
|
39
|
-
true
|
|
40
|
-
else
|
|
41
|
-
raise InvalidToken, "Unexpected validation result: #{result}"
|
|
42
|
-
end
|
|
19
|
+
raise InvalidToken unless validate_decrypted_token
|
|
20
|
+
modules_checks!
|
|
43
21
|
end
|
|
44
22
|
|
|
45
23
|
def valid?
|
|
46
|
-
|
|
47
|
-
client &&
|
|
48
|
-
RateLimiter.check(client) &&
|
|
49
|
-
atomic_validate_and_log_token == 'ok')
|
|
24
|
+
validate_decrypted_token && modules_checks
|
|
50
25
|
rescue StandardError
|
|
51
26
|
false
|
|
52
27
|
end
|
|
53
28
|
|
|
29
|
+
def modules_checks
|
|
30
|
+
true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def modules_checks!
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
|
|
54
37
|
def client
|
|
55
38
|
@client ||= begin
|
|
56
39
|
target = client_name.strip
|
|
@@ -58,23 +41,14 @@ module RobustServerSocket
|
|
|
58
41
|
end
|
|
59
42
|
end
|
|
60
43
|
|
|
61
|
-
def token_not_expired?
|
|
62
|
-
token_expiration_time > Time.now.utc.to_i - timestamp
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def atomic_validate_and_log_token
|
|
66
|
-
SecureToken::Cacher.atomic_validate_and_log(
|
|
67
|
-
decrypted_token,
|
|
68
|
-
token_expiration_time + 300,
|
|
69
|
-
timestamp,
|
|
70
|
-
token_expiration_time
|
|
71
|
-
)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
44
|
def decrypted_token
|
|
75
45
|
@decrypted_token ||= SecureToken::Decrypt.call(@secure_token)
|
|
76
46
|
end
|
|
77
47
|
|
|
48
|
+
def validate_decrypted_token
|
|
49
|
+
!!decrypted_token
|
|
50
|
+
end
|
|
51
|
+
|
|
78
52
|
private
|
|
79
53
|
|
|
80
54
|
def allowed_clients
|
|
@@ -101,6 +75,7 @@ module RobustServerSocket
|
|
|
101
75
|
RobustServerSocket.configuration.token_expiration_time
|
|
102
76
|
end
|
|
103
77
|
|
|
78
|
+
|
|
104
79
|
# Do we need it? It would be useful only if public_key compromised
|
|
105
80
|
# def secure_compare(a, b)
|
|
106
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
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
require_relative 'secure_token/cacher'
|
|
2
|
-
|
|
3
1
|
module RobustServerSocket
|
|
4
2
|
class RateLimiter
|
|
5
3
|
RateLimitExceeded = Class.new(StandardError)
|
|
@@ -8,6 +6,7 @@ module RobustServerSocket
|
|
|
8
6
|
def check!(client_name)
|
|
9
7
|
unless (attempts = check(client_name))
|
|
10
8
|
actual_attempts = current_attempts(client_name)
|
|
9
|
+
|
|
11
10
|
raise RateLimitExceeded, "Rate limit exceeded for #{client_name}: #{actual_attempts}/#{max_requests} requests per #{window_seconds}s"
|
|
12
11
|
end
|
|
13
12
|
|
|
@@ -15,8 +14,6 @@ module RobustServerSocket
|
|
|
15
14
|
end
|
|
16
15
|
|
|
17
16
|
def check(client_name)
|
|
18
|
-
return 0 unless rate_limit_enabled?
|
|
19
|
-
|
|
20
17
|
key = rate_limit_key(client_name)
|
|
21
18
|
attempts = increment_attempts(key)
|
|
22
19
|
|
|
@@ -26,18 +23,16 @@ module RobustServerSocket
|
|
|
26
23
|
end
|
|
27
24
|
|
|
28
25
|
def current_attempts(client_name)
|
|
29
|
-
return 0 unless rate_limit_enabled?
|
|
30
|
-
|
|
31
26
|
key = rate_limit_key(client_name)
|
|
32
|
-
|
|
27
|
+
Cacher.get(key).to_i
|
|
33
28
|
end
|
|
34
29
|
|
|
35
30
|
def reset!(client_name)
|
|
36
31
|
key = rate_limit_key(client_name)
|
|
37
|
-
|
|
32
|
+
Cacher.with_redis do |conn|
|
|
38
33
|
conn.del(key)
|
|
39
34
|
end
|
|
40
|
-
rescue
|
|
35
|
+
rescue Cacher::RedisConnectionError => e
|
|
41
36
|
handle_redis_error(e, 'reset')
|
|
42
37
|
nil
|
|
43
38
|
end
|
|
@@ -45,13 +40,13 @@ module RobustServerSocket
|
|
|
45
40
|
private
|
|
46
41
|
|
|
47
42
|
def increment_attempts(key)
|
|
48
|
-
|
|
43
|
+
Cacher.with_redis do |conn|
|
|
49
44
|
attempts = conn.incr(key)
|
|
50
45
|
# Set expiration only on first attempt to ensure atomic window
|
|
51
46
|
conn.expire(key, window_seconds) if attempts == 1
|
|
52
47
|
attempts
|
|
53
48
|
end
|
|
54
|
-
rescue
|
|
49
|
+
rescue Cacher::RedisConnectionError => e
|
|
55
50
|
handle_redis_error(e, 'increment_attempts')
|
|
56
51
|
0 # Fail open: allow request if Redis is down
|
|
57
52
|
end
|
|
@@ -60,10 +55,6 @@ module RobustServerSocket
|
|
|
60
55
|
"rate_limit:#{client_name}"
|
|
61
56
|
end
|
|
62
57
|
|
|
63
|
-
def rate_limit_enabled?
|
|
64
|
-
RobustServerSocket.configuration.rate_limit_enabled
|
|
65
|
-
end
|
|
66
|
-
|
|
67
58
|
def max_requests
|
|
68
59
|
RobustServerSocket.configuration.rate_limit_max_requests
|
|
69
60
|
end
|
data/lib/robust_server_socket.rb
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'base64'
|
|
4
|
+
require 'openssl'
|
|
5
|
+
require 'redis'
|
|
6
|
+
require 'connection_pool'
|
|
7
|
+
|
|
3
8
|
require_relative 'robust_server_socket/configuration'
|
|
9
|
+
require_relative 'robust_server_socket/secure_token/decrypt'
|
|
10
|
+
require_relative 'robust_server_socket/client_token'
|
|
4
11
|
|
|
5
12
|
module RobustServerSocket
|
|
6
13
|
extend RobustServerSocket::Configuration
|
|
@@ -10,12 +17,21 @@ module RobustServerSocket
|
|
|
10
17
|
def load!
|
|
11
18
|
raise 'You must correctly configure RobustServerSocket first!' unless configured?
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
configuration.using_modules.each do |mod|
|
|
21
|
+
raise ArgumentError, 'Module must be a Symbol!' unless mod.is_a?(Symbol)
|
|
22
|
+
|
|
23
|
+
require_relative "robust_server_socket/modules/#{mod}"
|
|
24
|
+
ClientToken.include eval(mod.to_s.split('_').map(&:capitalize).unshift('Modules::').join)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
ClientToken.class_eval(<<~METHOD)
|
|
28
|
+
def modules_checks
|
|
29
|
+
#{(RobustServerSocket.configuration._modules_check_rows.empty? ? ['true'] : RobustServerSocket.configuration._modules_check_rows.map(&:strip)).join(' && ')}
|
|
30
|
+
end
|
|
17
31
|
|
|
18
|
-
|
|
19
|
-
|
|
32
|
+
def modules_checks!
|
|
33
|
+
#{(RobustServerSocket.configuration._bang_modules_check_rows.empty? ? ['true'] : RobustServerSocket.configuration._bang_modules_check_rows).join}
|
|
34
|
+
end
|
|
35
|
+
METHOD
|
|
20
36
|
end
|
|
21
37
|
end
|
data/lib/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: robust_server_socket
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- tee_zed
|
|
@@ -79,10 +79,13 @@ files:
|
|
|
79
79
|
- README.md
|
|
80
80
|
- Rakefile
|
|
81
81
|
- lib/robust_server_socket.rb
|
|
82
|
+
- lib/robust_server_socket/cacher.rb
|
|
82
83
|
- lib/robust_server_socket/client_token.rb
|
|
83
84
|
- lib/robust_server_socket/configuration.rb
|
|
85
|
+
- lib/robust_server_socket/modules/client_auth_protection.rb
|
|
86
|
+
- lib/robust_server_socket/modules/dos_attack_protection.rb
|
|
87
|
+
- lib/robust_server_socket/modules/replay_attack_protection.rb
|
|
84
88
|
- lib/robust_server_socket/rate_limiter.rb
|
|
85
|
-
- lib/robust_server_socket/secure_token/cacher.rb
|
|
86
89
|
- lib/robust_server_socket/secure_token/decrypt.rb
|
|
87
90
|
- lib/version.rb
|
|
88
91
|
- robust_server_socket.gemspec
|
|
@@ -103,7 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
103
106
|
- !ruby/object:Gem::Version
|
|
104
107
|
version: '0'
|
|
105
108
|
requirements: []
|
|
106
|
-
rubygems_version:
|
|
109
|
+
rubygems_version: 4.0.6
|
|
107
110
|
specification_version: 4
|
|
108
111
|
summary: Robust Server Socket gem for RobustPro
|
|
109
112
|
test_files: []
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
require 'redis'
|
|
2
|
-
require 'connection_pool'
|
|
3
|
-
|
|
4
|
-
module RobustServerSocket
|
|
5
|
-
module SecureToken
|
|
6
|
-
module Cacher
|
|
7
|
-
class RedisConnectionError < StandardError; end
|
|
8
|
-
|
|
9
|
-
class << self
|
|
10
|
-
# Atomically validate token: check expiration and usage, then mark as used
|
|
11
|
-
# Returns: 'ok', 'stale', or 'used'
|
|
12
|
-
def atomic_validate_and_log(key, ttl, timestamp, expiration_time)
|
|
13
|
-
current_time = Time.now.utc.to_i
|
|
14
|
-
|
|
15
|
-
redis.with do |conn|
|
|
16
|
-
conn.eval(
|
|
17
|
-
lua_atomic_validate,
|
|
18
|
-
keys: [key],
|
|
19
|
-
argv: [ttl, timestamp, expiration_time, current_time]
|
|
20
|
-
)
|
|
21
|
-
end
|
|
22
|
-
rescue ::Redis::BaseConnectionError => e
|
|
23
|
-
handle_redis_error(e, 'atomic_validate_and_log')
|
|
24
|
-
raise RedisConnectionError, "Failed to validate token: #{e.message}"
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def incr(key, ttl = nil)
|
|
28
|
-
ttl_value = ttl || ttl_seconds
|
|
29
|
-
|
|
30
|
-
redis.with do |conn|
|
|
31
|
-
conn.pipelined do |pipeline|
|
|
32
|
-
pipeline.incrby(key, 1)
|
|
33
|
-
pipeline.expire(key, ttl_value)
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
rescue ::Redis::BaseConnectionError => e
|
|
37
|
-
handle_redis_error(e, 'incr')
|
|
38
|
-
raise RedisConnectionError, "Failed to increment key: #{e.message}"
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def get(key)
|
|
42
|
-
redis.with do |conn|
|
|
43
|
-
conn.get(key)
|
|
44
|
-
end
|
|
45
|
-
rescue ::Redis::BaseConnectionError => e
|
|
46
|
-
handle_redis_error(e, 'get')
|
|
47
|
-
nil # Fallback for reads
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def health_check
|
|
51
|
-
redis.with do |conn|
|
|
52
|
-
conn.ping == 'PONG'
|
|
53
|
-
end
|
|
54
|
-
rescue ::Redis::BaseConnectionError
|
|
55
|
-
false
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def with_redis(&block)
|
|
59
|
-
redis.with(&block)
|
|
60
|
-
rescue ::Redis::BaseConnectionError => e
|
|
61
|
-
handle_redis_error(e, 'with_redis')
|
|
62
|
-
raise ::RedisConnectionError, "Redis operation failed: #{e.message}"
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Clear cached Redis connection pool (useful for hot reloading in development)
|
|
66
|
-
def clear_redis_pool_cache!
|
|
67
|
-
@pool = nil
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
private
|
|
71
|
-
|
|
72
|
-
def lua_atomic_validate
|
|
73
|
-
<<~LUA
|
|
74
|
-
local key = KEYS[1]
|
|
75
|
-
local ttl = tonumber(ARGV[1])
|
|
76
|
-
local timestamp = tonumber(ARGV[2])
|
|
77
|
-
local expiration_time = tonumber(ARGV[3])
|
|
78
|
-
local current_time = tonumber(ARGV[4])
|
|
79
|
-
|
|
80
|
-
-- Check if token is expired
|
|
81
|
-
if expiration_time <= (current_time - timestamp) then
|
|
82
|
-
return 'stale'
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
-- Check if token was already used
|
|
86
|
-
local current = redis.call('GET', key)
|
|
87
|
-
if current and tonumber(current) > 0 then
|
|
88
|
-
return 'used'
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
-- Mark token as used
|
|
92
|
-
redis.call('INCRBY', key, 1)
|
|
93
|
-
redis.call('EXPIRE', key, ttl)
|
|
94
|
-
|
|
95
|
-
return 'ok'
|
|
96
|
-
LUA
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def ttl_seconds
|
|
100
|
-
::RobustServerSocket.configuration.token_expiration_time + 60
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
# Cache Redis connection pool at module level for the lifetime of the Rails process
|
|
104
|
-
# This avoids recreating the connection pool on every Redis operation
|
|
105
|
-
def redis
|
|
106
|
-
@pool ||= ::ConnectionPool::Wrapper.new(**pool_config) do
|
|
107
|
-
::Redis.new(redis_config)
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def pool_config
|
|
112
|
-
{
|
|
113
|
-
size: ENV.fetch('REDIS_POOL_SIZE', 25).to_i,
|
|
114
|
-
timeout: ENV.fetch('REDIS_POOL_TIMEOUT', 1).to_f
|
|
115
|
-
}
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def redis_config
|
|
119
|
-
config = {
|
|
120
|
-
url: ::RobustServerSocket.configuration.redis_url,
|
|
121
|
-
reconnect_attempts: 3,
|
|
122
|
-
timeout: 1.0,
|
|
123
|
-
connect_timeout: 2.0
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
password = ::RobustServerSocket.configuration.redis_pass
|
|
127
|
-
config[:password] = password if password && !password.empty?
|
|
128
|
-
|
|
129
|
-
config
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def handle_redis_error(error, operation)
|
|
133
|
-
warn "Redis operation '#{operation}' failed: #{error.class} - #{error.message}"
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
end
|