robust_client_socket 0.5.2 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/BENCHMARK_ANALYSIS.md +389 -0
- data/README.en.md +491 -0
- data/README.md +67 -307
- data/img.png +0 -0
- data/img_1.png +0 -0
- data/lib/robust_client_socket/http/helpers.rb +1 -1
- data/lib/version.rb +1 -1
- data/robust_client_socket.gemspec +1 -1
- data/spec/lib/robust_client_socket/http/helpers_spec.rb +3 -3
- metadata +6 -3
data/README.en.md
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
# RobustClientSocket
|
|
2
|
+
|
|
3
|
+
⚠️ Not Production Tested (yet)
|
|
4
|
+
|
|
5
|
+
`Not vibecoded`
|
|
6
|
+
|
|
7
|
+
HTTP client for secure inter-service communications with automatic authorization token generation.
|
|
8
|
+
|
|
9
|
+
## WHY
|
|
10
|
+
|
|
11
|
+
### The Problem
|
|
12
|
+
|
|
13
|
+
When building microservice communication, the following challenges arise:
|
|
14
|
+
|
|
15
|
+
- **Lack of authentication**: HTTP requests between services are unprotected
|
|
16
|
+
- **Replay attacks**: Intercepted requests can be reused maliciously
|
|
17
|
+
- **Manual key management**: Complexity of handling RSA keys for each service
|
|
18
|
+
- **Boilerplate code**: Repetitive code for encryption and request sending
|
|
19
|
+
|
|
20
|
+
### The Solution
|
|
21
|
+
|
|
22
|
+
RobustClientSocket provides:
|
|
23
|
+
|
|
24
|
+
- **Automatic tokenization**: Each request receives a unique encrypted token
|
|
25
|
+
- **RSA encryption**: Asymmetric encryption with key validation
|
|
26
|
+
- **Keychain management**: Auto-generation of HTTP clients for each service
|
|
27
|
+
- **Replay attack protection**: Timestamp in every token
|
|
28
|
+
|
|
29
|
+
## HOW IT WORKS
|
|
30
|
+
|
|
31
|
+
### Architecture
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
┌───────────────────┐ ┌───────────────────┐
|
|
35
|
+
│ Your Service │ │ Target Service │
|
|
36
|
+
│ │ │ (RobustServer) │
|
|
37
|
+
└─────────┬─────────┘ └─────────┬─────────┘
|
|
38
|
+
│ │
|
|
39
|
+
v v
|
|
40
|
+
┌───────────────────┐ ┌───────────────────┐
|
|
41
|
+
│ RobustClientSocket│ │ RobustServerSocket│
|
|
42
|
+
│ │ │ │
|
|
43
|
+
│ 1. Generate token │────>│ 1. Decrypt token │
|
|
44
|
+
│ 2. RSA encrypt │ │ 2. Validate │
|
|
45
|
+
│ 3. Send request │ │ 3. Check limits │
|
|
46
|
+
└───────────────────┘ └───────────────────┘
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Request Flow
|
|
50
|
+
|
|
51
|
+
1. **Token formation**: `{client_name}_{timestamp}`
|
|
52
|
+
2. **RSA encryption**: Token is encrypted with target service's public key
|
|
53
|
+
3. **Base64 encoding**: For HTTP header transmission
|
|
54
|
+
4. **Request sending**: Request with `Secure-Token` header
|
|
55
|
+
5. **Validation**: Server decrypts and validates the token
|
|
56
|
+
|
|
57
|
+
### Token Structure
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
Base64(RSA_Encrypt("{service_name}_{unix_timestamp}"))
|
|
61
|
+
|
|
62
|
+
Example: Base64(RSA_Encrypt("core_1704067200"))
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 📋 Table of Contents
|
|
66
|
+
|
|
67
|
+
- [Security Features](#security-features)
|
|
68
|
+
- [Installation](#installation)
|
|
69
|
+
- [Configuration](#configuration)
|
|
70
|
+
- [Usage](#usage)
|
|
71
|
+
- [HTTP Methods](#http-methods)
|
|
72
|
+
- [Error Handling](#error-handling)
|
|
73
|
+
- [SSL/TLS Settings](#ssltls-settings)
|
|
74
|
+
|
|
75
|
+
## 🔒 Security Features
|
|
76
|
+
|
|
77
|
+
RobustClientSocket provides secure communication between microservices:
|
|
78
|
+
|
|
79
|
+
### 1. Automatic Tokenization
|
|
80
|
+
- **On-the-fly Token Generation**: Each request automatically gets a new token
|
|
81
|
+
- **Timestamps**: Tokens contain UTC timestamp for replay attack protection
|
|
82
|
+
- **One-time Tokens**: Each request uses a unique token
|
|
83
|
+
|
|
84
|
+
### 2. RSA Encryption
|
|
85
|
+
- **RSA-2048 Minimum**: Automatic key size validation
|
|
86
|
+
- **PKCS1_OAEP_PADDING**: Secure padding for attack protection
|
|
87
|
+
- **Key Validation**: Public key correctness verification during configuration
|
|
88
|
+
|
|
89
|
+
### 3. TLS/SSL Protection
|
|
90
|
+
- **TLS 1.2+**: Modern encryption protocols
|
|
91
|
+
- **Certificate Verification**: VERIFY_PEER mode for production
|
|
92
|
+
- **Configurable Cipher Suites**: ECDHE support for forward secrecy
|
|
93
|
+
- **HTTPS Enforcement**: Mandatory HTTPS usage in production
|
|
94
|
+
|
|
95
|
+
### 4. Header Protection
|
|
96
|
+
- **Custom Headers**: Configurable header name for token
|
|
97
|
+
- **User-Agent Identification**: Client versioning
|
|
98
|
+
- **Content-Type Control**: Automatic headers for JSON
|
|
99
|
+
|
|
100
|
+
### 5. Timeout Attack Protection
|
|
101
|
+
- **Connection Timeout**: Configurable connection timeout (default: 5s)
|
|
102
|
+
- **Request Timeout**: Configurable request timeout (default: 10s)
|
|
103
|
+
- **Hang Protection**: Automatic interruption of long requests
|
|
104
|
+
|
|
105
|
+
### 6. Multi-service Architecture
|
|
106
|
+
- **Keychain Management**: Separate keys for each service
|
|
107
|
+
- **Automatic Client Generation**: Dynamic class creation for services
|
|
108
|
+
- **Configuration Isolation**: Independent settings for each service
|
|
109
|
+
|
|
110
|
+
## 📦 Installation
|
|
111
|
+
|
|
112
|
+
Add to Gemfile:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
gem 'robust_client_socket'
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Then execute:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
bundle install
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## ⚙️ Configuration
|
|
125
|
+
|
|
126
|
+
Create file `config/initializers/robust_client_socket.rb`:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
RobustClientSocket.configure do |c|
|
|
130
|
+
# REQUIRED: Current service (client) name
|
|
131
|
+
# Must match allowed_services on server
|
|
132
|
+
c.client_name = 'core'
|
|
133
|
+
|
|
134
|
+
# OPTIONAL: Header name for token (default: 'Secure-Token')
|
|
135
|
+
c.header_name = 'Secure-Token'
|
|
136
|
+
|
|
137
|
+
# KEYCHAIN: Configuration for each target service
|
|
138
|
+
|
|
139
|
+
# Basic configuration (without SSL validation)
|
|
140
|
+
c.payments = {
|
|
141
|
+
base_uri: 'http://localhost:3001',
|
|
142
|
+
public_key: ENV['PAYMENTS_PUBLIC_KEY']
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# Production configuration with SSL
|
|
146
|
+
c.notifications = {
|
|
147
|
+
base_uri: 'https://notifications.example.com',
|
|
148
|
+
public_key: ENV['NOTIFICATIONS_PUBLIC_KEY'],
|
|
149
|
+
ssl_verify: true, # Enable SSL certificate verification
|
|
150
|
+
timeout: 15, # Request timeout (seconds)
|
|
151
|
+
open_timeout: 5 # Connection timeout (seconds)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# Configuration with custom cipher suites
|
|
155
|
+
c.core = {
|
|
156
|
+
base_uri: 'https://core.example.com',
|
|
157
|
+
public_key: ENV['CORE_PUBLIC_KEY'],
|
|
158
|
+
ssl_verify: true,
|
|
159
|
+
ciphers: %w[
|
|
160
|
+
ECDHE-RSA-AES128-GCM-SHA256
|
|
161
|
+
ECDHE-RSA-AES256-GCM-SHA384
|
|
162
|
+
ECDHE-ECDSA-AES128-GCM-SHA256
|
|
163
|
+
ECDHE-ECDSA-AES256-GCM-SHA384
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Load configuration and create clients
|
|
169
|
+
RobustClientSocket.load!
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Service Configuration Options
|
|
173
|
+
|
|
174
|
+
| Parameter | Type | Required | Default | Description |
|
|
175
|
+
|----------|-----|--------------|---------|----------|
|
|
176
|
+
| `base_uri` | String | ✅ | - | Target service URL |
|
|
177
|
+
| `public_key` | String | ✅ | - | Service RSA public key |
|
|
178
|
+
| `ssl_verify` | Boolean | ❌ | false | Enable SSL certificate verification |
|
|
179
|
+
| `timeout` | Integer | ❌ | 10 | Request timeout (seconds) |
|
|
180
|
+
| `open_timeout` | Integer | ❌ | 5 | Connection timeout (seconds) |
|
|
181
|
+
| `ciphers` | Array/String | ❌ | See below | TLS cipher suites |
|
|
182
|
+
|
|
183
|
+
**Default cipher suites:**
|
|
184
|
+
```
|
|
185
|
+
ECDHE-RSA-AES128-GCM-SHA256
|
|
186
|
+
ECDHE-RSA-AES256-GCM-SHA384
|
|
187
|
+
ECDHE-ECDSA-AES128-GCM-SHA256
|
|
188
|
+
ECDHE-ECDSA-AES256-GCM-SHA384
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## 🚀 Usage
|
|
192
|
+
|
|
193
|
+
### Automatic Client Generation
|
|
194
|
+
|
|
195
|
+
After `RobustClientSocket.load!` classes are automatically created for each represented service:
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
# Configuration
|
|
199
|
+
c.payments = { base_uri: '...', public_key: '...' }
|
|
200
|
+
c.notifications = { base_uri: '...', public_key: '...' }
|
|
201
|
+
c.user_management = { base_uri: '...', public_key: '...' }
|
|
202
|
+
|
|
203
|
+
# After load! classes are available:
|
|
204
|
+
RobustClientSocket::Payments # payments -> Payments
|
|
205
|
+
RobustClientSocket::Notifications # notifications -> Notifications
|
|
206
|
+
RobustClientSocket::UserManagement # user_management -> UserManagement
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Basic Usage
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
# GET request
|
|
213
|
+
response = RobustClientSocket::Payments.get('/api/v1/transactions')
|
|
214
|
+
|
|
215
|
+
if response.success?
|
|
216
|
+
transactions = response.parsed_response
|
|
217
|
+
puts "Transactions received: #{transactions.count}"
|
|
218
|
+
else
|
|
219
|
+
puts "Error: #{response.code} - #{response.message}"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# POST request with body
|
|
223
|
+
response = RobustClientSocket::Notifications.post(
|
|
224
|
+
'/api/v1/send',
|
|
225
|
+
body: {
|
|
226
|
+
user_id: 123,
|
|
227
|
+
message: 'Hello World',
|
|
228
|
+
type: 'email'
|
|
229
|
+
}.to_json
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# PUT request
|
|
233
|
+
response = RobustClientSocket::UserManagement.put(
|
|
234
|
+
'/api/v1/users/123',
|
|
235
|
+
body: { name: 'John Doe' }.to_json
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# DELETE request
|
|
239
|
+
response = RobustClientSocket::Payments.delete('/api/v1/transactions/456')
|
|
240
|
+
|
|
241
|
+
# PATCH request
|
|
242
|
+
response = RobustClientSocket::UserManagement.patch(
|
|
243
|
+
'/api/v1/users/123',
|
|
244
|
+
body: { status: 'active' }.to_json
|
|
245
|
+
)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Query Parameters
|
|
249
|
+
|
|
250
|
+
```ruby
|
|
251
|
+
# GET with query parameters
|
|
252
|
+
response = RobustClientSocket::Payments.get(
|
|
253
|
+
'/api/v1/transactions',
|
|
254
|
+
query: {
|
|
255
|
+
status: 'completed',
|
|
256
|
+
date_from: '2024-01-01',
|
|
257
|
+
limit: 100
|
|
258
|
+
}
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Automatically converted to:
|
|
262
|
+
# /api/v1/transactions?status=completed&date_from=2024-01-01&limit=100
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Custom Headers
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
# Adding additional headers
|
|
269
|
+
response = RobustClientSocket::Payments.get(
|
|
270
|
+
'/api/v1/transactions',
|
|
271
|
+
headers: {
|
|
272
|
+
'X-Request-ID' => SecureRandom.uuid,
|
|
273
|
+
'X-User-ID' => current_user.id.to_s
|
|
274
|
+
}
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Secure-Token header is added automatically!
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## 🌐 HTTP Methods
|
|
281
|
+
|
|
282
|
+
All standard HTTPpartry methods:
|
|
283
|
+
|
|
284
|
+
### GET
|
|
285
|
+
```ruby
|
|
286
|
+
RobustClientSocket::ServiceName.get(path, options = {})
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### POST
|
|
290
|
+
```ruby
|
|
291
|
+
RobustClientSocket::ServiceName.post(path, options = {})
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### PUT
|
|
295
|
+
```ruby
|
|
296
|
+
RobustClientSocket::ServiceName.put(path, options = {})
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### DELETE
|
|
300
|
+
```ruby
|
|
301
|
+
RobustClientSocket::ServiceName.delete(path, options = {})
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### PATCH
|
|
305
|
+
```ruby
|
|
306
|
+
RobustClientSocket::ServiceName.patch(path, options = {})
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### HEAD
|
|
310
|
+
```ruby
|
|
311
|
+
RobustClientSocket::ServiceName.head(path, options = {})
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### OPTIONS
|
|
315
|
+
```ruby
|
|
316
|
+
RobustClientSocket::ServiceName.options(path, options = {})
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Request Options
|
|
320
|
+
|
|
321
|
+
```ruby
|
|
322
|
+
{
|
|
323
|
+
body: '{"key": "value"}', # Request body (String or Hash)
|
|
324
|
+
query: { param: 'value' }, # Query parameters (Hash)
|
|
325
|
+
headers: { 'X-Custom': 'value' }, # Additional headers (Hash)
|
|
326
|
+
timeout: 30, # Override request timeout
|
|
327
|
+
open_timeout: 10 # Override connection timeout
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## ❌ Error Handling
|
|
332
|
+
|
|
333
|
+
### Exception Types
|
|
334
|
+
|
|
335
|
+
| Exception | Reason | Action |
|
|
336
|
+
|-----------|---------|----------|
|
|
337
|
+
| `InsecureConnectionError` | HTTP used in production with `ssl_verify: true` | Use HTTPS |
|
|
338
|
+
| `InvalidCredentialsError` | Missing `base_uri` or `public_key` | Check configuration |
|
|
339
|
+
| `SecurityError` | Key less than 2048 bits or invalid | Use correct RSA-2048+ key |
|
|
340
|
+
| `OpenSSL::PKey::RSAError` | Encryption error | Check public key format |
|
|
341
|
+
| `Timeout::Error` | Timeout exceeded | Increase timeout or check service |
|
|
342
|
+
| `SocketError` | Service unavailable | Check base_uri and network |
|
|
343
|
+
|
|
344
|
+
### Error Handling
|
|
345
|
+
|
|
346
|
+
```ruby
|
|
347
|
+
begin
|
|
348
|
+
response = RobustClientSocket::Payments.post(
|
|
349
|
+
'/api/v1/charge',
|
|
350
|
+
body: { amount: 1000 }.to_json
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
if response.success?
|
|
354
|
+
# Success
|
|
355
|
+
elsif response.code == 401
|
|
356
|
+
# Authorization problem
|
|
357
|
+
Rails.logger.error("Auth failed: token may be rejected by server")
|
|
358
|
+
elsif response.code == 422
|
|
359
|
+
# Validation error
|
|
360
|
+
errors = response.parsed_response['errors']
|
|
361
|
+
elsif response.code >= 500
|
|
362
|
+
# Server error
|
|
363
|
+
Rails.logger.error("Server error: #{response.code}")
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
rescue RobustClientSocket::HTTP::Client::InsecureConnectionError => e
|
|
367
|
+
Rails.logger.error("Insecure connection: #{e.message}")
|
|
368
|
+
# Use HTTPS in production
|
|
369
|
+
|
|
370
|
+
rescue Timeout::Error => e
|
|
371
|
+
Rails.logger.error("Request timeout: #{e.message}")
|
|
372
|
+
# Retry or timeout handling
|
|
373
|
+
|
|
374
|
+
rescue SocketError => e
|
|
375
|
+
Rails.logger.error("Service unavailable: #{e.message}")
|
|
376
|
+
# Service unavailable
|
|
377
|
+
|
|
378
|
+
rescue SecurityError => e
|
|
379
|
+
Rails.logger.error("Security error: #{e.message}")
|
|
380
|
+
# Problem with keys or encryption
|
|
381
|
+
|
|
382
|
+
rescue StandardError => e
|
|
383
|
+
Rails.logger.error("Unexpected error: #{e.class} - #{e.message}")
|
|
384
|
+
end
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## 🔐 SSL/TLS Settings
|
|
388
|
+
|
|
389
|
+
### Production Configuration
|
|
390
|
+
|
|
391
|
+
```ruby
|
|
392
|
+
RobustClientSocket.configure do |c|
|
|
393
|
+
c.client_name = 'core'
|
|
394
|
+
|
|
395
|
+
c.payments = {
|
|
396
|
+
base_uri: 'https://payments.example.com',
|
|
397
|
+
public_key: ENV['PAYMENTS_PUBLIC_KEY'],
|
|
398
|
+
|
|
399
|
+
# Enable SSL validation
|
|
400
|
+
ssl_verify: true,
|
|
401
|
+
|
|
402
|
+
# TLS 1.2+ with secure ciphers
|
|
403
|
+
ciphers: %w[
|
|
404
|
+
ECDHE-RSA-AES128-GCM-SHA256
|
|
405
|
+
ECDHE-RSA-AES256-GCM-SHA384
|
|
406
|
+
]
|
|
407
|
+
}
|
|
408
|
+
end
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### 2. Synchronization with Server
|
|
412
|
+
|
|
413
|
+
```ruby
|
|
414
|
+
# Client
|
|
415
|
+
RobustClientSocket.configure do |c|
|
|
416
|
+
c.client_name = 'core' # ← Important: current service name
|
|
417
|
+
|
|
418
|
+
c.payments = {
|
|
419
|
+
base_uri: 'https://payments.example.com',
|
|
420
|
+
public_key: '-----BEGIN PUBLIC KEY-----...' # PAYMENTS service public key
|
|
421
|
+
}
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Server (payments)
|
|
425
|
+
RobustServerSocket.configure do |c|
|
|
426
|
+
c.allowed_services = %w[core] # ← Must contain 'core'
|
|
427
|
+
c.private_key = '-----BEGIN PRIVATE KEY-----...' # Private pair to public key above
|
|
428
|
+
end
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
## 🤝 Integration with RobustServerSocket
|
|
432
|
+
|
|
433
|
+
### Full Setup Example
|
|
434
|
+
|
|
435
|
+
**Service A (client):**
|
|
436
|
+
```ruby
|
|
437
|
+
# config/initializers/robust_client_socket.rb
|
|
438
|
+
RobustClientSocket.configure do |c|
|
|
439
|
+
c.client_name = 'service_a'
|
|
440
|
+
|
|
441
|
+
c.service_b = {
|
|
442
|
+
base_uri: ENV['SERVICE_B_URL'],
|
|
443
|
+
public_key: ENV['SERVICE_B_PUBLIC_KEY'],
|
|
444
|
+
ssl_verify: Rails.env.production?
|
|
445
|
+
}
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
RobustClientSocket.load!
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
**Service B (server):**
|
|
452
|
+
```ruby
|
|
453
|
+
# config/initializers/robust_server_socket.rb
|
|
454
|
+
RobustServerSocket.configure do |c|
|
|
455
|
+
c.allowed_services = %w[service_a] # Allow service_a
|
|
456
|
+
c.private_key = ENV['SERVICE_B_PRIVATE_KEY']
|
|
457
|
+
c.token_expiration_time = 3
|
|
458
|
+
c.redis_url = ENV['REDIS_URL']
|
|
459
|
+
c.redis_pass = ENV['REDIS_PASSWORD']
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
RobustServerSocket.load!
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
**Key Pair Generation:**
|
|
466
|
+
```bash
|
|
467
|
+
# Generate private key (for Service B)
|
|
468
|
+
openssl genrsa -out service_b_private.pem 2048
|
|
469
|
+
|
|
470
|
+
# Generate public key (for Service A)
|
|
471
|
+
openssl rsa -in service_b_private.pem -pubout -out service_b_public.pem
|
|
472
|
+
|
|
473
|
+
# Add to environment variables
|
|
474
|
+
# Service A: SERVICE_B_PUBLIC_KEY=$(cat service_b_public.pem)
|
|
475
|
+
# Service B: SERVICE_B_PRIVATE_KEY=$(cat service_b_private.pem)
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
## 📚 Additional Resources
|
|
479
|
+
|
|
480
|
+
- [BENCHMARK_ANALYSIS.md](../BENCHMARK_ANALYSIS.md)
|
|
481
|
+
- [RobustServerSocket documentation](../robust_server_socket/README.ru.md)
|
|
482
|
+
- [HTTParty documentation](https://github.com/jnunemaker/httparty)
|
|
483
|
+
- [OpenSSL Ruby documentation](https://ruby-doc.org/stdlib/libdoc/openssl/rdoc/OpenSSL.html)
|
|
484
|
+
|
|
485
|
+
## 📝 License
|
|
486
|
+
|
|
487
|
+
See [LICENSE.txt](LICENSE.txt) file
|
|
488
|
+
|
|
489
|
+
## 🐛 Bugs and Suggestions
|
|
490
|
+
|
|
491
|
+
Report issues to my telegram @cruel_mango or to email tee0zed@gmail.com
|