robust_client_socket 0.5.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 +7 -0
- data/.rspec +1 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +732 -0
- data/Rakefile +12 -0
- data/lib/robust_client_socket/configuration.rb +67 -0
- data/lib/robust_client_socket/http/client.rb +90 -0
- data/lib/robust_client_socket/http/helpers.rb +68 -0
- data/lib/robust_client_socket/http/httparty_overrides.rb +20 -0
- data/lib/robust_client_socket.rb +29 -0
- data/lib/version.rb +5 -0
- data/robust_client_socket.gemspec +33 -0
- data/spec/lib/payrent_client_socket/http/client_spec.rb +137 -0
- data/spec/lib/payrent_client_socket.rb +31 -0
- data/spec/lib/robust_client_socket/configuration_spec.rb +131 -0
- data/spec/lib/robust_client_socket/http/client_spec.rb +137 -0
- data/spec/lib/robust_client_socket/http/helpers_spec.rb +80 -0
- data/spec/lib/robust_client_socket/http/httparty_overrides_spec.rb +65 -0
- data/spec/spec_helper.rb +99 -0
- metadata +114 -0
data/README.md
ADDED
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
# RobustClientSocket
|
|
2
|
+
|
|
3
|
+
HTTP-клиент для защищённых межсервисных коммуникаций с автоматической генерацией токенов авторизации.
|
|
4
|
+
|
|
5
|
+
## 📋 Содержание
|
|
6
|
+
|
|
7
|
+
- [Функции безопасности](#функции-безопасности)
|
|
8
|
+
- [Установка](#установка)
|
|
9
|
+
- [Конфигурация](#конфигурация)
|
|
10
|
+
- [Использование](#использование)
|
|
11
|
+
- [Методы HTTP](#методы-http)
|
|
12
|
+
- [Обработка ошибок](#обработка-ошибок)
|
|
13
|
+
- [SSL/TLS настройки](#ssltls-настройки)
|
|
14
|
+
- [Рекомендации по использованию](#рекомендации-по-использованию)
|
|
15
|
+
- [Безопасность в production](#безопасность-в-production)
|
|
16
|
+
|
|
17
|
+
## 🔒 Функции безопасности
|
|
18
|
+
|
|
19
|
+
RobustClientSocket обеспечивает защищённую коммуникацию между микросервисами:
|
|
20
|
+
|
|
21
|
+
### 1. Автоматическая токенизация
|
|
22
|
+
- **Генерация токенов на лету**: Каждый запрос автоматически получает новый токен
|
|
23
|
+
- **Временные метки**: Токены содержат UTC timestamp для защиты от replay attacks
|
|
24
|
+
- **Одноразовые токены**: Каждый запрос использует уникальный токен
|
|
25
|
+
|
|
26
|
+
### 2. RSA шифрование
|
|
27
|
+
- **RSA-2048 минимум**: Автоматическая валидация размера ключа
|
|
28
|
+
- **PKCS1_OAEP_PADDING**: Безопасный padding для защиты от атак
|
|
29
|
+
- **Валидация ключей**: Проверка корректности публичных ключей при конфигурации
|
|
30
|
+
|
|
31
|
+
### 3. TLS/SSL защита
|
|
32
|
+
- **TLS 1.2+**: Современные протоколы шифрования
|
|
33
|
+
- **Проверка сертификатов**: VERIFY_PEER режим для production
|
|
34
|
+
- **Настраиваемые cipher suites**: Поддержка ECDHE для forward secrecy
|
|
35
|
+
- **HTTPS enforcement**: Обязательное использование HTTPS в production
|
|
36
|
+
|
|
37
|
+
### 4. Защита заголовков
|
|
38
|
+
- **Кастомные заголовки**: Настраиваемое имя заголовка для токена
|
|
39
|
+
- **User-Agent идентификация**: Версионирование клиента
|
|
40
|
+
- **Content-Type контроль**: Автоматические заголовки для JSON
|
|
41
|
+
|
|
42
|
+
### 5. Защита от timeout атак
|
|
43
|
+
- **Connection timeout**: Настраиваемый таймаут подключения (default: 5s)
|
|
44
|
+
- **Request timeout**: Настраиваемый таймаут запроса (default: 10s)
|
|
45
|
+
- **Защита от зависания**: Автоматическое прерывание долгих запросов
|
|
46
|
+
|
|
47
|
+
### 6. Мультисервисная архитектура
|
|
48
|
+
- **Keychain управление**: Отдельные ключи для каждого сервиса
|
|
49
|
+
- **Автоматическая генерация клиентов**: Динамическое создание классов для сервисов
|
|
50
|
+
- **Изоляция конфигураций**: Независимые настройки для каждого сервиса
|
|
51
|
+
|
|
52
|
+
## 📦 Установка
|
|
53
|
+
|
|
54
|
+
Добавьте в Gemfile:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
gem 'robust_client_socket'
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Затем выполните:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
bundle install
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## ⚙️ Конфигурация
|
|
67
|
+
|
|
68
|
+
Создайте файл `config/initializers/robust_client_socket.rb`:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
RobustClientSocket.configure do |c|
|
|
72
|
+
# ОБЯЗАТЕЛЬНО: Имя текущего сервиса (клиента)
|
|
73
|
+
# Должно совпадать с allowed_services на сервере
|
|
74
|
+
c.client_name = 'core'
|
|
75
|
+
|
|
76
|
+
# ОПЦИОНАЛЬНО: Имя заголовка для токена (default: 'Secure-Token')
|
|
77
|
+
c.header_name = 'Secure-Token'
|
|
78
|
+
|
|
79
|
+
# KEYCHAIN: Конфигурация для каждого целевого сервиса
|
|
80
|
+
|
|
81
|
+
# Базовая конфигурация (без SSL валидации)
|
|
82
|
+
c.payments = {
|
|
83
|
+
base_uri: 'http://localhost:3001',
|
|
84
|
+
public_key: ENV['PAYMENTS_PUBLIC_KEY']
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# Production конфигурация с SSL
|
|
88
|
+
c.notifications = {
|
|
89
|
+
base_uri: 'https://notifications.example.com',
|
|
90
|
+
public_key: ENV['NOTIFICATIONS_PUBLIC_KEY'],
|
|
91
|
+
ssl_verify: true, # Включить проверку SSL сертификатов
|
|
92
|
+
timeout: 15, # Таймаут запроса (секунды)
|
|
93
|
+
open_timeout: 5 # Таймаут подключения (секунды)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Конфигурация с кастомными cipher suites
|
|
97
|
+
c.core = {
|
|
98
|
+
base_uri: 'https://core.example.com',
|
|
99
|
+
public_key: ENV['CORE_PUBLIC_KEY'],
|
|
100
|
+
ssl_verify: true,
|
|
101
|
+
ciphers: %w[
|
|
102
|
+
ECDHE-RSA-AES128-GCM-SHA256
|
|
103
|
+
ECDHE-RSA-AES256-GCM-SHA384
|
|
104
|
+
ECDHE-ECDSA-AES128-GCM-SHA256
|
|
105
|
+
ECDHE-ECDSA-AES256-GCM-SHA384
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Загрузка конфигурации и создание клиентов
|
|
111
|
+
RobustClientSocket.load!
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Опции конфигурации сервиса
|
|
115
|
+
|
|
116
|
+
| Параметр | Тип | Обязательный | Default | Описание |
|
|
117
|
+
|----------|-----|--------------|---------|----------|
|
|
118
|
+
| `base_uri` | String | ✅ | - | URL целевого сервиса |
|
|
119
|
+
| `public_key` | String | ✅ | - | Публичный RSA ключ сервиса |
|
|
120
|
+
| `ssl_verify` | Boolean | ❌ | false | Включить проверку SSL сертификатов |
|
|
121
|
+
| `timeout` | Integer | ❌ | 10 | Таймаут запроса (секунды) |
|
|
122
|
+
| `open_timeout` | Integer | ❌ | 5 | Таймаут подключения (секунды) |
|
|
123
|
+
| `ciphers` | Array/String | ❌ | См. ниже | TLS cipher suites |
|
|
124
|
+
|
|
125
|
+
**Default cipher suites:**
|
|
126
|
+
```
|
|
127
|
+
ECDHE-RSA-AES128-GCM-SHA256
|
|
128
|
+
ECDHE-RSA-AES256-GCM-SHA384
|
|
129
|
+
ECDHE-ECDSA-AES128-GCM-SHA256
|
|
130
|
+
ECDHE-ECDSA-AES256-GCM-SHA384
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## 🚀 Использование
|
|
134
|
+
|
|
135
|
+
### Автоматическая генерация клиентов
|
|
136
|
+
|
|
137
|
+
После `RobustClientSocket.load!` автоматически создаются классы для каждого сервиса в keychain:
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
# Конфигурация
|
|
141
|
+
c.payments = { base_uri: '...', public_key: '...' }
|
|
142
|
+
c.notifications = { base_uri: '...', public_key: '...' }
|
|
143
|
+
c.user_management = { base_uri: '...', public_key: '...' }
|
|
144
|
+
|
|
145
|
+
# После load! доступны классы:
|
|
146
|
+
RobustClientSocket::Payments # payments -> Payments
|
|
147
|
+
RobustClientSocket::Notifications # notifications -> Notifications
|
|
148
|
+
RobustClientSocket::UserManagement # user_management -> UserManagement
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Базовое использование
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
# GET запрос
|
|
155
|
+
response = RobustClientSocket::Payments.get('/api/v1/transactions')
|
|
156
|
+
|
|
157
|
+
if response.success?
|
|
158
|
+
transactions = response.parsed_response
|
|
159
|
+
puts "Получено транзакций: #{transactions.count}"
|
|
160
|
+
else
|
|
161
|
+
puts "Ошибка: #{response.code} - #{response.message}"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# POST запрос с телом
|
|
165
|
+
response = RobustClientSocket::Notifications.post(
|
|
166
|
+
'/api/v1/send',
|
|
167
|
+
body: {
|
|
168
|
+
user_id: 123,
|
|
169
|
+
message: 'Hello World',
|
|
170
|
+
type: 'email'
|
|
171
|
+
}.to_json
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# PUT запрос
|
|
175
|
+
response = RobustClientSocket::UserManagement.put(
|
|
176
|
+
'/api/v1/users/123',
|
|
177
|
+
body: { name: 'John Doe' }.to_json
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# DELETE запрос
|
|
181
|
+
response = RobustClientSocket::Payments.delete('/api/v1/transactions/456')
|
|
182
|
+
|
|
183
|
+
# PATCH запрос
|
|
184
|
+
response = RobustClientSocket::UserManagement.patch(
|
|
185
|
+
'/api/v1/users/123',
|
|
186
|
+
body: { status: 'active' }.to_json
|
|
187
|
+
)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Query параметры
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
# GET с query параметрами
|
|
194
|
+
response = RobustClientSocket::Payments.get(
|
|
195
|
+
'/api/v1/transactions',
|
|
196
|
+
query: {
|
|
197
|
+
status: 'completed',
|
|
198
|
+
date_from: '2024-01-01',
|
|
199
|
+
limit: 100
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Автоматически преобразуется в:
|
|
204
|
+
# /api/v1/transactions?status=completed&date_from=2024-01-01&limit=100
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Кастомные заголовки
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
# Добавление дополнительных заголовков
|
|
211
|
+
response = RobustClientSocket::Payments.get(
|
|
212
|
+
'/api/v1/transactions',
|
|
213
|
+
headers: {
|
|
214
|
+
'X-Request-ID' => SecureRandom.uuid,
|
|
215
|
+
'X-User-ID' => current_user.id.to_s
|
|
216
|
+
}
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Secure-Token заголовок добавляется автоматически!
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Обработка ответов
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
response = RobustClientSocket::Payments.get('/api/v1/balance')
|
|
226
|
+
|
|
227
|
+
# Проверка успешности
|
|
228
|
+
if response.success?
|
|
229
|
+
# 2xx статус коды
|
|
230
|
+
data = response.parsed_response
|
|
231
|
+
puts "Balance: #{data['amount']}"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Доступ к деталям
|
|
235
|
+
response.code # HTTP статус код (Integer)
|
|
236
|
+
response.message # HTTP статус сообщение (String)
|
|
237
|
+
response.body # Сырое тело ответа (String)
|
|
238
|
+
response.parsed_response # Распарсенный JSON (Hash)
|
|
239
|
+
response.headers # Заголовки ответа (Hash)
|
|
240
|
+
|
|
241
|
+
# Проверка конкретных статусов
|
|
242
|
+
if response.code == 200
|
|
243
|
+
# OK
|
|
244
|
+
elsif response.code == 404
|
|
245
|
+
# Not Found
|
|
246
|
+
elsif response.code == 401
|
|
247
|
+
# Unauthorized - возможно истёк токен
|
|
248
|
+
elsif response.code == 429
|
|
249
|
+
# Rate limit exceeded
|
|
250
|
+
end
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## 🌐 Методы HTTP
|
|
254
|
+
|
|
255
|
+
Все стандартные HTTP методы поддерживаются:
|
|
256
|
+
|
|
257
|
+
### GET
|
|
258
|
+
```ruby
|
|
259
|
+
RobustClientSocket::ServiceName.get(path, options = {})
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### POST
|
|
263
|
+
```ruby
|
|
264
|
+
RobustClientSocket::ServiceName.post(path, options = {})
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### PUT
|
|
268
|
+
```ruby
|
|
269
|
+
RobustClientSocket::ServiceName.put(path, options = {})
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### DELETE
|
|
273
|
+
```ruby
|
|
274
|
+
RobustClientSocket::ServiceName.delete(path, options = {})
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### PATCH
|
|
278
|
+
```ruby
|
|
279
|
+
RobustClientSocket::ServiceName.patch(path, options = {})
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### HEAD
|
|
283
|
+
```ruby
|
|
284
|
+
RobustClientSocket::ServiceName.head(path, options = {})
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### OPTIONS
|
|
288
|
+
```ruby
|
|
289
|
+
RobustClientSocket::ServiceName.options(path, options = {})
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Опции запроса
|
|
293
|
+
|
|
294
|
+
```ruby
|
|
295
|
+
{
|
|
296
|
+
body: '{"key": "value"}', # Тело запроса (String или Hash)
|
|
297
|
+
query: { param: 'value' }, # Query параметры (Hash)
|
|
298
|
+
headers: { 'X-Custom': 'value' }, # Дополнительные заголовки (Hash)
|
|
299
|
+
timeout: 30, # Переопределить таймаут запроса
|
|
300
|
+
open_timeout: 10 # Переопределить таймаут подключения
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## ❌ Обработка ошибок
|
|
305
|
+
|
|
306
|
+
### Типы исключений
|
|
307
|
+
|
|
308
|
+
| Исключение | Причина | Действие |
|
|
309
|
+
|-----------|---------|----------|
|
|
310
|
+
| `InsecureConnectionError` | HTTP используется в production с `ssl_verify: true` | Используйте HTTPS |
|
|
311
|
+
| `InvalidCredentialsError` | Отсутствуют `base_uri` или `public_key` | Проверьте конфигурацию |
|
|
312
|
+
| `SecurityError` | Ключ меньше 2048 бит или невалиден | Используйте корректный RSA-2048+ ключ |
|
|
313
|
+
| `OpenSSL::PKey::RSAError` | Ошибка шифрования | Проверьте формат публичного ключа |
|
|
314
|
+
| `Timeout::Error` | Превышен таймаут | Увеличьте timeout или проверьте сервис |
|
|
315
|
+
| `SocketError` | Сервис недоступен | Проверьте base_uri и сеть |
|
|
316
|
+
|
|
317
|
+
### Обработка ошибок
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
begin
|
|
321
|
+
response = RobustClientSocket::Payments.post(
|
|
322
|
+
'/api/v1/charge',
|
|
323
|
+
body: { amount: 1000 }.to_json
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
if response.success?
|
|
327
|
+
# Успешно
|
|
328
|
+
elsif response.code == 401
|
|
329
|
+
# Проблема авторизации
|
|
330
|
+
Rails.logger.error("Auth failed: token may be rejected by server")
|
|
331
|
+
elsif response.code == 422
|
|
332
|
+
# Validation error
|
|
333
|
+
errors = response.parsed_response['errors']
|
|
334
|
+
elsif response.code >= 500
|
|
335
|
+
# Server error
|
|
336
|
+
Rails.logger.error("Server error: #{response.code}")
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
rescue RobustClientSocket::HTTP::Client::InsecureConnectionError => e
|
|
340
|
+
Rails.logger.error("Insecure connection: #{e.message}")
|
|
341
|
+
# Используйте HTTPS в production
|
|
342
|
+
|
|
343
|
+
rescue Timeout::Error => e
|
|
344
|
+
Rails.logger.error("Request timeout: #{e.message}")
|
|
345
|
+
# Retry или обработка таймаута
|
|
346
|
+
|
|
347
|
+
rescue SocketError => e
|
|
348
|
+
Rails.logger.error("Service unavailable: #{e.message}")
|
|
349
|
+
# Сервис недоступен
|
|
350
|
+
|
|
351
|
+
rescue SecurityError => e
|
|
352
|
+
Rails.logger.error("Security error: #{e.message}")
|
|
353
|
+
# Проблема с ключами или шифрованием
|
|
354
|
+
|
|
355
|
+
rescue StandardError => e
|
|
356
|
+
Rails.logger.error("Unexpected error: #{e.class} - #{e.message}")
|
|
357
|
+
end
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Retry стратегия
|
|
361
|
+
|
|
362
|
+
```ruby
|
|
363
|
+
def call_with_retry(max_attempts: 3, backoff: 2)
|
|
364
|
+
attempts = 0
|
|
365
|
+
|
|
366
|
+
begin
|
|
367
|
+
attempts += 1
|
|
368
|
+
response = RobustClientSocket::Payments.get('/api/v1/status')
|
|
369
|
+
|
|
370
|
+
return response if response.success?
|
|
371
|
+
|
|
372
|
+
# Retry на 5xx и таймаутах
|
|
373
|
+
if response.code >= 500 && attempts < max_attempts
|
|
374
|
+
sleep(backoff ** attempts)
|
|
375
|
+
retry
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
response
|
|
379
|
+
|
|
380
|
+
rescue Timeout::Error, SocketError => e
|
|
381
|
+
if attempts < max_attempts
|
|
382
|
+
sleep(backoff ** attempts)
|
|
383
|
+
retry
|
|
384
|
+
else
|
|
385
|
+
raise
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
## 🔐 SSL/TLS настройки
|
|
392
|
+
|
|
393
|
+
### Production конфигурация
|
|
394
|
+
|
|
395
|
+
```ruby
|
|
396
|
+
RobustClientSocket.configure do |c|
|
|
397
|
+
c.client_name = 'core'
|
|
398
|
+
|
|
399
|
+
c.payments = {
|
|
400
|
+
base_uri: 'https://payments.example.com',
|
|
401
|
+
public_key: ENV['PAYMENTS_PUBLIC_KEY'],
|
|
402
|
+
|
|
403
|
+
# Включить SSL валидацию
|
|
404
|
+
ssl_verify: true,
|
|
405
|
+
|
|
406
|
+
# TLS 1.2+ с безопасными ciphers
|
|
407
|
+
ciphers: %w[
|
|
408
|
+
ECDHE-RSA-AES128-GCM-SHA256
|
|
409
|
+
ECDHE-RSA-AES256-GCM-SHA384
|
|
410
|
+
]
|
|
411
|
+
}
|
|
412
|
+
end
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Рекомендуемые cipher suites
|
|
416
|
+
|
|
417
|
+
**High Security (рекомендуется):**
|
|
418
|
+
```ruby
|
|
419
|
+
ciphers: %w[
|
|
420
|
+
ECDHE-RSA-AES128-GCM-SHA256
|
|
421
|
+
ECDHE-RSA-AES256-GCM-SHA384
|
|
422
|
+
ECDHE-ECDSA-AES128-GCM-SHA256
|
|
423
|
+
ECDHE-ECDSA-AES256-GCM-SHA384
|
|
424
|
+
]
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
**Balanced (совместимость + безопасность):**
|
|
428
|
+
```ruby
|
|
429
|
+
ciphers: %w[
|
|
430
|
+
ECDHE-RSA-AES128-GCM-SHA256
|
|
431
|
+
ECDHE-RSA-AES256-GCM-SHA384
|
|
432
|
+
AES128-GCM-SHA256
|
|
433
|
+
AES256-GCM-SHA384
|
|
434
|
+
]
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### Проверка SSL в разных окружениях
|
|
438
|
+
|
|
439
|
+
```ruby
|
|
440
|
+
c.payments = {
|
|
441
|
+
base_uri: ENV['PAYMENTS_URL'],
|
|
442
|
+
public_key: ENV['PAYMENTS_PUBLIC_KEY'],
|
|
443
|
+
# Включать SSL только в production
|
|
444
|
+
ssl_verify: Rails.env.production?
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
## 💡 Рекомендации по использованию
|
|
449
|
+
|
|
450
|
+
### 1. Управление ключами
|
|
451
|
+
|
|
452
|
+
**✅ DO:**
|
|
453
|
+
```ruby
|
|
454
|
+
# Храните ключи в переменных окружения
|
|
455
|
+
c.payments = {
|
|
456
|
+
base_uri: ENV['PAYMENTS_URL'],
|
|
457
|
+
public_key: ENV['PAYMENTS_PUBLIC_KEY']
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
# Используйте secrets management
|
|
461
|
+
c.payments = {
|
|
462
|
+
base_uri: 'https://payments.example.com',
|
|
463
|
+
public_key: Rails.application.credentials.dig(:payments, :public_key)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
# Один файл для всех публичных ключей
|
|
467
|
+
# config/public_keys/payments.pem
|
|
468
|
+
c.payments = {
|
|
469
|
+
base_uri: ENV['PAYMENTS_URL'],
|
|
470
|
+
public_key: File.read(Rails.root.join('config/public_keys/payments.pem'))
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
**❌ DON'T:**
|
|
475
|
+
```ruby
|
|
476
|
+
# НЕ коммитьте ключи в git
|
|
477
|
+
c.payments = {
|
|
478
|
+
public_key: "-----BEGIN PUBLIC KEY-----\nMII..."
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
# НЕ используйте один ключ для всех сервисов
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### 2. Настройка таймаутов
|
|
485
|
+
|
|
486
|
+
**Рекомендации по таймаутам:**
|
|
487
|
+
|
|
488
|
+
```ruby
|
|
489
|
+
# Быстрые операции (чтение)
|
|
490
|
+
c.cache_service = {
|
|
491
|
+
base_uri: 'https://cache.example.com',
|
|
492
|
+
public_key: ENV['CACHE_PUBLIC_KEY'],
|
|
493
|
+
timeout: 3, # 3 секунды
|
|
494
|
+
open_timeout: 1 # 1 секунда
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
# Стандартные операции
|
|
498
|
+
c.api_service = {
|
|
499
|
+
base_uri: 'https://api.example.com',
|
|
500
|
+
public_key: ENV['API_PUBLIC_KEY'],
|
|
501
|
+
timeout: 10, # 10 секунд (default)
|
|
502
|
+
open_timeout: 5 # 5 секунд (default)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
# Долгие операции (обработка, экспорт)
|
|
506
|
+
c.processor = {
|
|
507
|
+
base_uri: 'https://processor.example.com',
|
|
508
|
+
public_key: ENV['PROCESSOR_PUBLIC_KEY'],
|
|
509
|
+
timeout: 60, # 60 секунд
|
|
510
|
+
open_timeout: 10 # 10 секунд
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### 3. Логирование и мониторинг
|
|
515
|
+
|
|
516
|
+
```ruby
|
|
517
|
+
# Wrapper для логирования всех запросов
|
|
518
|
+
module RobustClientSocketLogger
|
|
519
|
+
def self.call(service_name, method, path, options = {})
|
|
520
|
+
start_time = Time.now
|
|
521
|
+
|
|
522
|
+
response = RobustClientSocket.const_get(service_name).send(method, path, options)
|
|
523
|
+
|
|
524
|
+
duration = ((Time.now - start_time) * 1000).round(2)
|
|
525
|
+
|
|
526
|
+
Rails.logger.info(
|
|
527
|
+
"RobustClientSocket Request: " \
|
|
528
|
+
"service=#{service_name} method=#{method} path=#{path} " \
|
|
529
|
+
"status=#{response.code} duration=#{duration}ms"
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
# Метрики
|
|
533
|
+
Metrics.timing("robust_client.#{service_name}.#{method}", duration)
|
|
534
|
+
Metrics.increment("robust_client.#{service_name}.status.#{response.code}")
|
|
535
|
+
|
|
536
|
+
response
|
|
537
|
+
rescue StandardError => e
|
|
538
|
+
Rails.logger.error(
|
|
539
|
+
"RobustClientSocket Error: " \
|
|
540
|
+
"service=#{service_name} method=#{method} path=#{path} " \
|
|
541
|
+
"error=#{e.class} message=#{e.message}"
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
Metrics.increment("robust_client.#{service_name}.error.#{e.class.name}")
|
|
545
|
+
raise
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
# Использование
|
|
550
|
+
response = RobustClientSocketLogger.call(
|
|
551
|
+
'Payments',
|
|
552
|
+
:post,
|
|
553
|
+
'/api/v1/charge',
|
|
554
|
+
body: { amount: 1000 }.to_json
|
|
555
|
+
)
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### 4. Синхронизация с сервером
|
|
559
|
+
|
|
560
|
+
```ruby
|
|
561
|
+
# Клиент
|
|
562
|
+
RobustClientSocket.configure do |c|
|
|
563
|
+
c.client_name = 'core' # ← Важно: имя текущего сервиса
|
|
564
|
+
|
|
565
|
+
c.payments = {
|
|
566
|
+
base_uri: 'https://payments.example.com',
|
|
567
|
+
public_key: '-----BEGIN PUBLIC KEY-----...' # Публичный ключ PAYMENTS сервиса
|
|
568
|
+
}
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
# Сервер (payments)
|
|
572
|
+
RobustServerSocket.configure do |c|
|
|
573
|
+
c.allowed_services = %w[core] # ← Должно содержать 'core'
|
|
574
|
+
c.private_key = '-----BEGIN PRIVATE KEY-----...' # Приватная пара к публичному ключу выше
|
|
575
|
+
end
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
### 5. Тестирование
|
|
579
|
+
|
|
580
|
+
```ruby
|
|
581
|
+
# spec/support/robust_client_socket.rb
|
|
582
|
+
RSpec.configure do |config|
|
|
583
|
+
config.before(:suite) do
|
|
584
|
+
RobustClientSocket.configure do |c|
|
|
585
|
+
c.client_name = 'test_service'
|
|
586
|
+
|
|
587
|
+
c.payments = {
|
|
588
|
+
base_uri: 'http://localhost:3001',
|
|
589
|
+
public_key: File.read(Rails.root.join('spec/fixtures/keys/payments_public.pem'))
|
|
590
|
+
}
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
RobustClientSocket.load!
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
# Тест с WebMock
|
|
598
|
+
require 'webmock/rspec'
|
|
599
|
+
|
|
600
|
+
RSpec.describe 'Payments integration' do
|
|
601
|
+
before do
|
|
602
|
+
stub_request(:post, "http://localhost:3001/api/v1/charge")
|
|
603
|
+
.to_return(status: 200, body: { success: true }.to_json)
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
it 'creates charge' do
|
|
607
|
+
response = RobustClientSocket::Payments.post(
|
|
608
|
+
'/api/v1/charge',
|
|
609
|
+
body: { amount: 1000 }.to_json
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
expect(response.success?).to be true
|
|
613
|
+
expect(response.parsed_response['success']).to be true
|
|
614
|
+
end
|
|
615
|
+
end
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
## 🔐 Безопасность в Production
|
|
619
|
+
|
|
620
|
+
### Чеклист безопасности
|
|
621
|
+
|
|
622
|
+
- [ ] **Публичные ключи хранятся в secrets manager**
|
|
623
|
+
- [ ] **Использованы RSA-2048 или выше ключи**
|
|
624
|
+
- [ ] **ssl_verify: true для production**
|
|
625
|
+
- [ ] **HTTPS используется для всех сервисов**
|
|
626
|
+
- [ ] **client_name синхронизирован с allowed_services серверов**
|
|
627
|
+
- [ ] **Настроены разумные таймауты**
|
|
628
|
+
- [ ] **Безопасные cipher suites настроены**
|
|
629
|
+
- [ ] **Логирование всех запросов включено**
|
|
630
|
+
- [ ] **Метрики собираются**
|
|
631
|
+
- [ ] **Retry логика реализована для критичных запросов**
|
|
632
|
+
|
|
633
|
+
### Мониторинг
|
|
634
|
+
|
|
635
|
+
```ruby
|
|
636
|
+
# Метрики для отслеживания
|
|
637
|
+
# - Количество запросов по сервисам
|
|
638
|
+
# - Время ответа (percentiles: p50, p95, p99)
|
|
639
|
+
# - Количество ошибок по типам
|
|
640
|
+
# - Количество таймаутов
|
|
641
|
+
# - Количество retry попыток
|
|
642
|
+
|
|
643
|
+
class RobustClientMetrics
|
|
644
|
+
def self.track(service, method, path)
|
|
645
|
+
start = Time.now
|
|
646
|
+
response = yield
|
|
647
|
+
duration = ((Time.now - start) * 1000).round(2)
|
|
648
|
+
|
|
649
|
+
# StatsD/Prometheus metrics
|
|
650
|
+
Metrics.timing("robust_client.request.duration", duration, tags: [
|
|
651
|
+
"service:#{service}",
|
|
652
|
+
"method:#{method}",
|
|
653
|
+
"status:#{response.code}"
|
|
654
|
+
])
|
|
655
|
+
|
|
656
|
+
Metrics.increment("robust_client.request.count", tags: [
|
|
657
|
+
"service:#{service}",
|
|
658
|
+
"method:#{method}",
|
|
659
|
+
"status:#{response.code}"
|
|
660
|
+
])
|
|
661
|
+
|
|
662
|
+
response
|
|
663
|
+
rescue StandardError => e
|
|
664
|
+
Metrics.increment("robust_client.error.count", tags: [
|
|
665
|
+
"service:#{service}",
|
|
666
|
+
"error:#{e.class.name}"
|
|
667
|
+
])
|
|
668
|
+
raise
|
|
669
|
+
end
|
|
670
|
+
end
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
## 🤝 Интеграция с RobustServerSocket
|
|
674
|
+
|
|
675
|
+
### Полный пример настройки
|
|
676
|
+
|
|
677
|
+
**Сервис A (client):**
|
|
678
|
+
```ruby
|
|
679
|
+
# config/initializers/robust_client_socket.rb
|
|
680
|
+
RobustClientSocket.configure do |c|
|
|
681
|
+
c.client_name = 'service_a'
|
|
682
|
+
|
|
683
|
+
c.service_b = {
|
|
684
|
+
base_uri: ENV['SERVICE_B_URL'],
|
|
685
|
+
public_key: ENV['SERVICE_B_PUBLIC_KEY'],
|
|
686
|
+
ssl_verify: Rails.env.production?
|
|
687
|
+
}
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
RobustClientSocket.load!
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
**Сервис B (server):**
|
|
694
|
+
```ruby
|
|
695
|
+
# config/initializers/robust_server_socket.rb
|
|
696
|
+
RobustServerSocket.configure do |c|
|
|
697
|
+
c.allowed_services = %w[service_a] # Разрешить service_a
|
|
698
|
+
c.private_key = ENV['SERVICE_B_PRIVATE_KEY']
|
|
699
|
+
c.token_expiration_time = 3
|
|
700
|
+
c.redis_url = ENV['REDIS_URL']
|
|
701
|
+
c.redis_pass = ENV['REDIS_PASSWORD']
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
RobustServerSocket.load!
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
**Генерация пары ключей:**
|
|
708
|
+
```bash
|
|
709
|
+
# Генерация приватного ключа (для Service B)
|
|
710
|
+
openssl genrsa -out service_b_private.pem 2048
|
|
711
|
+
|
|
712
|
+
# Генерация публичного ключа (для Service A)
|
|
713
|
+
openssl rsa -in service_b_private.pem -pubout -out service_b_public.pem
|
|
714
|
+
|
|
715
|
+
# Добавить в переменные окружения
|
|
716
|
+
# Service A: SERVICE_B_PUBLIC_KEY=$(cat service_b_public.pem)
|
|
717
|
+
# Service B: SERVICE_B_PRIVATE_KEY=$(cat service_b_private.pem)
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
## 📚 Дополнительные ресурсы
|
|
721
|
+
|
|
722
|
+
- [RobustServerSocket documentation](../robust_server_socket/README.ru.md)
|
|
723
|
+
- [HTTParty documentation](https://github.com/jnunemaker/httparty)
|
|
724
|
+
- [OpenSSL Ruby documentation](https://ruby-doc.org/stdlib/libdoc/openssl/rdoc/OpenSSL.html)
|
|
725
|
+
|
|
726
|
+
## 📝 Лицензия
|
|
727
|
+
|
|
728
|
+
См. файл [LICENSE.txt](LICENSE.txt)
|
|
729
|
+
|
|
730
|
+
## 🐛 Баги и предложения
|
|
731
|
+
|
|
732
|
+
Сообщайте о проблемах через issue tracker вашего репозитория.
|