robust_server_socket 0.3.3 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +9 -0
- data/.ruby-version +1 -0
- data/README.en.md +170 -494
- data/README.md +156 -244
- data/Rakefile +4 -8
- data/lib/robust_server_socket/cacher.rb +142 -0
- data/lib/robust_server_socket/client_token.rb +18 -43
- data/lib/robust_server_socket/configuration.rb +35 -4
- data/lib/robust_server_socket/modules/client_auth_protection.rb +22 -0
- data/lib/robust_server_socket/modules/rate_limit_protection.rb +23 -0
- data/lib/robust_server_socket/modules/replay_attack_protection.rb +48 -0
- data/lib/robust_server_socket/rate_limiter.rb +13 -37
- data/lib/robust_server_socket/secure_token/decrypt.rb +4 -8
- data/lib/robust_server_socket.rb +23 -7
- data/lib/version.rb +1 -1
- data/robust_server_socket.gemspec +12 -12
- metadata +10 -5
- data/lib/robust_server_socket/secure_token/cacher.rb +0 -138
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobustServerSocket
|
|
4
|
+
module Cacher # rubocop:disable Metrics/ModuleLength
|
|
5
|
+
class RedisConnectionError < StandardError; end
|
|
6
|
+
|
|
7
|
+
CLOCK_SKEW_MS = 30_000
|
|
8
|
+
|
|
9
|
+
class << self # rubocop:disable Metrics/ClassLength
|
|
10
|
+
def atomic_validate_and_log(key, ttl, timestamp_ms, expiration_time)
|
|
11
|
+
current_ms = Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)
|
|
12
|
+
redis.with do |conn|
|
|
13
|
+
conn.eval(lua_atomic_validate_and_log, keys: [key],
|
|
14
|
+
argv: [ttl, timestamp_ms, expiration_time, current_ms])
|
|
15
|
+
end
|
|
16
|
+
rescue ::Redis::BaseConnectionError => e
|
|
17
|
+
handle_redis_error(e, 'atomic_validate_and_log')
|
|
18
|
+
raise RedisConnectionError, "Failed to validate token: #{e.message}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def incr_sliding_window_count(key, window_seconds)
|
|
22
|
+
now_ns = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
|
|
23
|
+
redis.with do |conn|
|
|
24
|
+
conn.eval(lua_sliding_window, keys: [key],
|
|
25
|
+
argv: [now_ns, window_seconds * 1_000_000_000, window_seconds, now_ns.to_s])
|
|
26
|
+
end
|
|
27
|
+
rescue ::Redis::BaseConnectionError => e
|
|
28
|
+
handle_redis_error(e, 'incr_sliding_window_count')
|
|
29
|
+
raise RedisConnectionError, "Failed to count sliding window: #{e.message}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def get(key)
|
|
33
|
+
redis.with do |conn|
|
|
34
|
+
conn.get(key)
|
|
35
|
+
end
|
|
36
|
+
rescue ::Redis::BaseConnectionError => e
|
|
37
|
+
handle_redis_error(e, 'get')
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def health_check
|
|
42
|
+
redis.with do |conn|
|
|
43
|
+
conn.ping == 'PONG'
|
|
44
|
+
end
|
|
45
|
+
rescue ::Redis::BaseConnectionError
|
|
46
|
+
false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def with_redis(&block)
|
|
50
|
+
redis.with(&block)
|
|
51
|
+
rescue ::Redis::BaseConnectionError => e
|
|
52
|
+
handle_redis_error(e, 'with_redis')
|
|
53
|
+
raise RedisConnectionError, "Redis operation failed: #{e.message}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Clear cached Redis connection pool (useful for hot reloading in development)
|
|
57
|
+
def clear_redis_pool_cache!
|
|
58
|
+
@redis = nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def lua_sliding_window
|
|
64
|
+
<<~LUA
|
|
65
|
+
local key = KEYS[1]
|
|
66
|
+
local now_ns = tonumber(ARGV[1])
|
|
67
|
+
local window_ns = tonumber(ARGV[2])
|
|
68
|
+
local window_s = tonumber(ARGV[3])
|
|
69
|
+
local member = ARGV[4]
|
|
70
|
+
|
|
71
|
+
redis.call('ZREMRANGEBYSCORE', key, '-inf', now_ns - window_ns)
|
|
72
|
+
redis.call('ZADD', key, now_ns, member)
|
|
73
|
+
redis.call('EXPIRE', key, window_s)
|
|
74
|
+
return redis.call('ZCARD', key)
|
|
75
|
+
LUA
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def lua_atomic_validate_and_log
|
|
79
|
+
<<~LUA
|
|
80
|
+
local key = KEYS[1]
|
|
81
|
+
local ttl = tonumber(ARGV[1])
|
|
82
|
+
local timestamp_ms = tonumber(ARGV[2])
|
|
83
|
+
local expiration_ms = tonumber(ARGV[3]) * 1000
|
|
84
|
+
local current_ms = tonumber(ARGV[4])
|
|
85
|
+
|
|
86
|
+
if timestamp_ms > current_ms + #{CLOCK_SKEW_MS} then
|
|
87
|
+
return 'stale'
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
if expiration_ms <= (current_ms - timestamp_ms) then
|
|
91
|
+
return 'stale'
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
-- Check if token was already used
|
|
95
|
+
local current = redis.call('GET', key)
|
|
96
|
+
if current and tonumber(current) > 0 then
|
|
97
|
+
return 'used'
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
-- Mark token as used
|
|
101
|
+
redis.call('INCRBY', key, 1)
|
|
102
|
+
redis.call('EXPIRE', key, ttl)
|
|
103
|
+
|
|
104
|
+
return 'ok'
|
|
105
|
+
LUA
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Cache Redis connection pool at module level for the lifetime of the Rails process
|
|
109
|
+
# This avoids recreating the connection pool on every Redis operation
|
|
110
|
+
def redis
|
|
111
|
+
@redis ||= ::ConnectionPool::Wrapper.new(**pool_config) do
|
|
112
|
+
::Redis.new(redis_config)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def pool_config
|
|
117
|
+
{
|
|
118
|
+
size: ENV.fetch('REDIS_POOL_SIZE', 25).to_i,
|
|
119
|
+
timeout: ENV.fetch('REDIS_POOL_TIMEOUT', 1).to_f
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def redis_config
|
|
124
|
+
config = {
|
|
125
|
+
url: ::RobustServerSocket.configuration.redis_url,
|
|
126
|
+
reconnect_attempts: 3,
|
|
127
|
+
timeout: 1.0,
|
|
128
|
+
connect_timeout: 2.0
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
password = ::RobustServerSocket.configuration.redis_pass
|
|
132
|
+
config[:password] = password if password && !password.empty?
|
|
133
|
+
|
|
134
|
+
config
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def handle_redis_error(error, operation)
|
|
138
|
+
warn "Redis operation '#{operation}' failed: #{error.class} - #{error.message}"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -1,20 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
require_relative 'secure_token/decrypt'
|
|
3
|
-
require_relative 'rate_limiter'
|
|
1
|
+
# frozen_string_literal: true
|
|
4
2
|
|
|
5
3
|
module RobustServerSocket
|
|
6
4
|
class ClientToken
|
|
7
5
|
TOKEN_REGEXP = /\A(.+)_(\d{10,})\z/.freeze
|
|
8
6
|
|
|
9
7
|
InvalidToken = Class.new(StandardError)
|
|
10
|
-
UnauthorizedClient = Class.new(StandardError)
|
|
11
|
-
UsedToken = Class.new(StandardError)
|
|
12
|
-
StaleToken = Class.new(StandardError)
|
|
13
8
|
|
|
14
9
|
def self.validate!(secure_token)
|
|
15
|
-
new(secure_token).tap
|
|
16
|
-
instance.validate!
|
|
17
|
-
end
|
|
10
|
+
new(secure_token).tap(&:validate!)
|
|
18
11
|
end
|
|
19
12
|
|
|
20
13
|
def initialize(secure_token)
|
|
@@ -23,34 +16,25 @@ module RobustServerSocket
|
|
|
23
16
|
end
|
|
24
17
|
|
|
25
18
|
def validate!
|
|
26
|
-
|
|
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
|
+
modules_checks!
|
|
20
|
+
rescue SecureToken::InvalidToken => e
|
|
21
|
+
raise InvalidToken, e.message
|
|
43
22
|
end
|
|
44
23
|
|
|
45
24
|
def valid?
|
|
46
|
-
|
|
47
|
-
client &&
|
|
48
|
-
RateLimiter.check(client) &&
|
|
49
|
-
atomic_validate_and_log_token == 'ok')
|
|
25
|
+
modules_checks
|
|
50
26
|
rescue StandardError
|
|
51
27
|
false
|
|
52
28
|
end
|
|
53
29
|
|
|
30
|
+
def modules_checks
|
|
31
|
+
true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def modules_checks!
|
|
35
|
+
true
|
|
36
|
+
end
|
|
37
|
+
|
|
54
38
|
def client
|
|
55
39
|
@client ||= begin
|
|
56
40
|
target = client_name.strip
|
|
@@ -58,19 +42,6 @@ module RobustServerSocket
|
|
|
58
42
|
end
|
|
59
43
|
end
|
|
60
44
|
|
|
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
45
|
def decrypted_token
|
|
75
46
|
@decrypted_token ||= SecureToken::Decrypt.call(@secure_token)
|
|
76
47
|
end
|
|
@@ -93,6 +64,7 @@ module RobustServerSocket
|
|
|
93
64
|
@split_token ||= begin
|
|
94
65
|
match_data = decrypted_token.to_s.match(TOKEN_REGEXP)
|
|
95
66
|
raise InvalidToken, 'Invalid token format' unless match_data
|
|
67
|
+
|
|
96
68
|
match_data.captures
|
|
97
69
|
end
|
|
98
70
|
end
|
|
@@ -114,6 +86,9 @@ module RobustServerSocket
|
|
|
114
86
|
raise InvalidToken, 'Token cannot be empty' if token.empty?
|
|
115
87
|
raise InvalidToken, 'Token too long' if token.length > 2048
|
|
116
88
|
|
|
89
|
+
# Check for null-byte injection
|
|
90
|
+
raise InvalidToken, 'Token contains invalid characters' if token.include?("\x00")
|
|
91
|
+
|
|
117
92
|
token
|
|
118
93
|
end
|
|
119
94
|
end
|
|
@@ -1,12 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module RobustServerSocket
|
|
2
4
|
module Configuration
|
|
3
5
|
MIN_KEY_SIZE = 2048
|
|
4
6
|
|
|
7
|
+
ConfigurationError = Class.new(StandardError)
|
|
8
|
+
|
|
5
9
|
attr_reader :configuration, :configured
|
|
6
10
|
|
|
11
|
+
def _push_modules_check_code(code)
|
|
12
|
+
configuration._modules_check_rows.push(code)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def _push_bang_modules_check_code(code)
|
|
16
|
+
configuration._bang_modules_check_rows.push(code)
|
|
17
|
+
end
|
|
18
|
+
|
|
7
19
|
def configure
|
|
8
20
|
@configuration ||= ConfigStore.new
|
|
9
21
|
yield(configuration)
|
|
22
|
+
validate_configuration!
|
|
10
23
|
validate_key_security!
|
|
11
24
|
|
|
12
25
|
@configured = true
|
|
@@ -29,13 +42,25 @@ module RobustServerSocket
|
|
|
29
42
|
|
|
30
43
|
private
|
|
31
44
|
|
|
45
|
+
def validate_configuration!
|
|
46
|
+
validate_positive!(:rate_limit_max_requests)
|
|
47
|
+
validate_positive!(:rate_limit_window_seconds)
|
|
48
|
+
validate_positive!(:token_expiration_time)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def validate_positive!(attr)
|
|
52
|
+
return if configuration.public_send(attr).to_i.positive?
|
|
53
|
+
|
|
54
|
+
raise ConfigurationError, "#{attr} must be positive"
|
|
55
|
+
end
|
|
56
|
+
|
|
32
57
|
def validate_key_security!
|
|
33
58
|
key = ::OpenSSL::PKey::RSA.new(configuration.private_key)
|
|
34
59
|
key_bits = key.n.num_bits
|
|
35
60
|
|
|
36
61
|
if key_bits < MIN_KEY_SIZE
|
|
37
62
|
raise SecurityError,
|
|
38
|
-
|
|
63
|
+
"RSA key size (#{key_bits} bits) below minimum (#{MIN_KEY_SIZE} bits)"
|
|
39
64
|
end
|
|
40
65
|
rescue ::OpenSSL::PKey::RSAError => e
|
|
41
66
|
raise SecurityError, "Invalid private key: #{e.message}"
|
|
@@ -43,13 +68,19 @@ module RobustServerSocket
|
|
|
43
68
|
end
|
|
44
69
|
|
|
45
70
|
class ConfigStore
|
|
46
|
-
attr_accessor :allowed_services, :private_key, :token_expiration_time,
|
|
47
|
-
:
|
|
71
|
+
attr_accessor :allowed_services, :private_key, :token_expiration_time,
|
|
72
|
+
:redis_url, :redis_pass,
|
|
73
|
+
:rate_limit_max_requests, :rate_limit_window_seconds, :using_modules
|
|
74
|
+
|
|
75
|
+
attr_reader :_modules_check_rows, :_bang_modules_check_rows
|
|
48
76
|
|
|
49
77
|
def initialize
|
|
50
|
-
@rate_limit_enabled = false
|
|
51
78
|
@rate_limit_max_requests = 100
|
|
52
79
|
@rate_limit_window_seconds = 60
|
|
80
|
+
@token_expiration_time = 10
|
|
81
|
+
@using_modules = %i[client_auth_protection rate_limit_protection replay_attack_protection]
|
|
82
|
+
@_modules_check_rows = []
|
|
83
|
+
@_bang_modules_check_rows = []
|
|
53
84
|
end
|
|
54
85
|
end
|
|
55
86
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobustServerSocket
|
|
4
|
+
module Modules
|
|
5
|
+
module ClientAuthProtection
|
|
6
|
+
UnauthorizedClient = Class.new(StandardError)
|
|
7
|
+
|
|
8
|
+
def self.included(_base)
|
|
9
|
+
RobustServerSocket._push_modules_check_code('validate_client')
|
|
10
|
+
RobustServerSocket._push_bang_modules_check_code("validate_client!\n")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def validate_client
|
|
14
|
+
!!client
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def validate_client!
|
|
18
|
+
raise UnauthorizedClient unless validate_client
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../cacher'
|
|
4
|
+
require_relative '../rate_limiter'
|
|
5
|
+
|
|
6
|
+
module RobustServerSocket
|
|
7
|
+
module Modules
|
|
8
|
+
module RateLimitProtection
|
|
9
|
+
def self.included(_base)
|
|
10
|
+
RobustServerSocket._push_modules_check_code('validate_rate_limit')
|
|
11
|
+
RobustServerSocket._push_bang_modules_check_code("validate_rate_limit!\n")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def validate_rate_limit
|
|
15
|
+
RateLimiter.check(client)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def validate_rate_limit!
|
|
19
|
+
RateLimiter.check!(client)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../cacher'
|
|
4
|
+
|
|
5
|
+
module RobustServerSocket
|
|
6
|
+
module Modules
|
|
7
|
+
module ReplayAttackProtection
|
|
8
|
+
UsedToken = Class.new(StandardError)
|
|
9
|
+
StaleToken = Class.new(StandardError)
|
|
10
|
+
|
|
11
|
+
def self.included(_base)
|
|
12
|
+
RobustServerSocket._push_modules_check_code('atomic_validate_and_log_token')
|
|
13
|
+
RobustServerSocket._push_bang_modules_check_code("atomic_validate_and_log_token!\n")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def atomic_validate_and_log_token!
|
|
17
|
+
result = Cacher.atomic_validate_and_log(
|
|
18
|
+
decrypted_token, store_used_token_time, timestamp, token_expiration_time
|
|
19
|
+
)
|
|
20
|
+
handle_validation_result!(result)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def atomic_validate_and_log_token
|
|
24
|
+
Cacher.atomic_validate_and_log(
|
|
25
|
+
decrypted_token,
|
|
26
|
+
store_used_token_time,
|
|
27
|
+
timestamp,
|
|
28
|
+
token_expiration_time
|
|
29
|
+
) == 'ok'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def handle_validation_result!(result)
|
|
35
|
+
case result
|
|
36
|
+
when 'ok' then true
|
|
37
|
+
when 'stale' then raise StaleToken
|
|
38
|
+
when 'used' then raise UsedToken
|
|
39
|
+
else raise StandardError, "Unexpected result: #{result}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def store_used_token_time
|
|
44
|
+
RobustServerSocket.configuration.token_expiration_time + Cacher::CLOCK_SKEW_MS / 1000
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RobustServerSocket
|
|
4
4
|
class RateLimiter
|
|
@@ -6,64 +6,40 @@ module RobustServerSocket
|
|
|
6
6
|
|
|
7
7
|
class << self
|
|
8
8
|
def check!(client_name)
|
|
9
|
-
|
|
10
|
-
actual_attempts = current_attempts(client_name)
|
|
11
|
-
raise RateLimitExceeded, "Rate limit exceeded for #{client_name}: #{actual_attempts}/#{max_requests} requests per #{window_seconds}s"
|
|
12
|
-
end
|
|
9
|
+
return if check(client_name)
|
|
13
10
|
|
|
14
|
-
|
|
11
|
+
raise RateLimitExceeded,
|
|
12
|
+
"Rate limit exceeded for #{client_name}: max #{max_requests} per #{window_seconds}s"
|
|
15
13
|
end
|
|
16
14
|
|
|
17
15
|
def check(client_name)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
key = rate_limit_key(client_name)
|
|
21
|
-
attempts = increment_attempts(key)
|
|
22
|
-
|
|
23
|
-
return false if attempts > max_requests
|
|
24
|
-
|
|
25
|
-
attempts
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def current_attempts(client_name)
|
|
29
|
-
return 0 unless rate_limit_enabled?
|
|
30
|
-
|
|
31
|
-
key = rate_limit_key(client_name)
|
|
32
|
-
SecureToken::Cacher.get(key).to_i
|
|
16
|
+
attempts = record_attempt(client_name)
|
|
17
|
+
attempts <= max_requests
|
|
33
18
|
end
|
|
34
19
|
|
|
35
20
|
def reset!(client_name)
|
|
36
21
|
key = rate_limit_key(client_name)
|
|
37
|
-
|
|
22
|
+
Cacher.with_redis do |conn|
|
|
38
23
|
conn.del(key)
|
|
39
24
|
end
|
|
40
|
-
rescue
|
|
25
|
+
rescue Cacher::RedisConnectionError => e
|
|
41
26
|
handle_redis_error(e, 'reset')
|
|
42
27
|
nil
|
|
43
28
|
end
|
|
44
29
|
|
|
45
30
|
private
|
|
46
31
|
|
|
47
|
-
def
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
attempts
|
|
53
|
-
end
|
|
54
|
-
rescue SecureToken::Cacher::RedisConnectionError => e
|
|
55
|
-
handle_redis_error(e, 'increment_attempts')
|
|
56
|
-
0 # Fail open: allow request if Redis is down
|
|
32
|
+
def record_attempt(client_name)
|
|
33
|
+
Cacher.incr_sliding_window_count(rate_limit_key(client_name), window_seconds)
|
|
34
|
+
rescue Cacher::RedisConnectionError => e
|
|
35
|
+
handle_redis_error(e, 'record_attempt')
|
|
36
|
+
0
|
|
57
37
|
end
|
|
58
38
|
|
|
59
39
|
def rate_limit_key(client_name)
|
|
60
40
|
"rate_limit:#{client_name}"
|
|
61
41
|
end
|
|
62
42
|
|
|
63
|
-
def rate_limit_enabled?
|
|
64
|
-
RobustServerSocket.configuration.rate_limit_enabled
|
|
65
|
-
end
|
|
66
|
-
|
|
67
43
|
def max_requests
|
|
68
44
|
RobustServerSocket.configuration.rate_limit_max_requests
|
|
69
45
|
end
|
|
@@ -1,22 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RobustServerSocket
|
|
4
4
|
module SecureToken
|
|
5
|
-
BASE64_REGEXP =
|
|
5
|
+
BASE64_REGEXP = %r{\A[A-Za-z0-9+/]+={0,2}\z}.freeze
|
|
6
6
|
InvalidToken = Class.new(StandardError)
|
|
7
7
|
|
|
8
8
|
module Decrypt
|
|
9
9
|
class << self
|
|
10
10
|
def call(token)
|
|
11
|
-
unless token.is_a?(String) && token.match?(BASE64_REGEXP)
|
|
12
|
-
raise InvalidToken, 'Invalid token format'
|
|
13
|
-
end
|
|
11
|
+
raise InvalidToken, 'Invalid token format' unless token.is_a?(String) && token.match?(BASE64_REGEXP)
|
|
14
12
|
|
|
15
13
|
decoded_token = ::Base64.strict_decode64(token)
|
|
16
14
|
|
|
17
|
-
if decoded_token.bytesize > 1024
|
|
18
|
-
raise InvalidToken, 'Token too large'
|
|
19
|
-
end
|
|
15
|
+
raise InvalidToken, 'Token too large' if decoded_token.bytesize > 1024
|
|
20
16
|
|
|
21
17
|
private_key.private_decrypt(decoded_token, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING).force_encoding('UTF-8')
|
|
22
18
|
rescue ::OpenSSL::PKey::RSAError, ArgumentError
|
data/lib/robust_server_socket.rb
CHANGED
|
@@ -1,21 +1,37 @@
|
|
|
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
|
|
7
14
|
|
|
8
15
|
module_function
|
|
9
16
|
|
|
10
|
-
def load!
|
|
17
|
+
def load! # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
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 const_get(mod.to_s.split('_').map(&:capitalize).unshift('Modules::').join)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
ClientToken.class_eval(<<~METHOD, __FILE__, __LINE__ + 1)
|
|
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
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require './lib/version'
|
|
4
4
|
|
|
5
5
|
Gem::Specification.new do |spec|
|
|
6
|
-
spec.name =
|
|
6
|
+
spec.name = 'robust_server_socket'
|
|
7
7
|
spec.version = RobustServerSocket::VERSION
|
|
8
|
-
spec.authors = [
|
|
9
|
-
spec.email = [
|
|
10
|
-
spec.description =
|
|
11
|
-
spec.summary =
|
|
12
|
-
spec.license =
|
|
13
|
-
spec.required_ruby_version =
|
|
8
|
+
spec.authors = ['tee_zed']
|
|
9
|
+
spec.email = ['tee0zed@gmail.com']
|
|
10
|
+
spec.description = 'Robust Server Socket'
|
|
11
|
+
spec.summary = 'Robust Server Socket gem for RobustPro'
|
|
12
|
+
spec.license = 'MIT'
|
|
13
|
+
spec.required_ruby_version = '>= 2.7.0'
|
|
14
14
|
|
|
15
15
|
# Specify which files should be added to the gem when it is released.
|
|
16
16
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
@@ -20,13 +20,13 @@ Gem::Specification.new do |spec|
|
|
|
20
20
|
f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
|
-
spec.bindir =
|
|
23
|
+
spec.bindir = 'exe'
|
|
24
24
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
25
|
-
spec.require_paths = [
|
|
26
|
-
spec.add_dependency '
|
|
25
|
+
spec.require_paths = ['lib']
|
|
26
|
+
spec.add_dependency 'hiredis'
|
|
27
27
|
spec.add_dependency 'rake'
|
|
28
28
|
spec.add_dependency 'redis'
|
|
29
|
-
spec.add_dependency '
|
|
29
|
+
spec.add_dependency 'rspec'
|
|
30
30
|
|
|
31
31
|
# Uncomment to register a new dependency of your gem
|
|
32
32
|
# spec.add_dependency "example-gem", "~> 1.0"
|
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.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- tee_zed
|
|
@@ -10,7 +10,7 @@ cert_chain: []
|
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
|
-
name:
|
|
13
|
+
name: hiredis
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
@@ -52,7 +52,7 @@ dependencies:
|
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '0'
|
|
54
54
|
- !ruby/object:Gem::Dependency
|
|
55
|
-
name:
|
|
55
|
+
name: rspec
|
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
|
57
57
|
requirements:
|
|
58
58
|
- - ">="
|
|
@@ -73,16 +73,21 @@ extensions: []
|
|
|
73
73
|
extra_rdoc_files: []
|
|
74
74
|
files:
|
|
75
75
|
- ".rspec"
|
|
76
|
+
- ".rubocop.yml"
|
|
77
|
+
- ".ruby-version"
|
|
76
78
|
- CODE_OF_CONDUCT.md
|
|
77
79
|
- LICENSE.txt
|
|
78
80
|
- README.en.md
|
|
79
81
|
- README.md
|
|
80
82
|
- Rakefile
|
|
81
83
|
- lib/robust_server_socket.rb
|
|
84
|
+
- lib/robust_server_socket/cacher.rb
|
|
82
85
|
- lib/robust_server_socket/client_token.rb
|
|
83
86
|
- lib/robust_server_socket/configuration.rb
|
|
87
|
+
- lib/robust_server_socket/modules/client_auth_protection.rb
|
|
88
|
+
- lib/robust_server_socket/modules/rate_limit_protection.rb
|
|
89
|
+
- lib/robust_server_socket/modules/replay_attack_protection.rb
|
|
84
90
|
- lib/robust_server_socket/rate_limiter.rb
|
|
85
|
-
- lib/robust_server_socket/secure_token/cacher.rb
|
|
86
91
|
- lib/robust_server_socket/secure_token/decrypt.rb
|
|
87
92
|
- lib/version.rb
|
|
88
93
|
- robust_server_socket.gemspec
|
|
@@ -103,7 +108,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
103
108
|
- !ruby/object:Gem::Version
|
|
104
109
|
version: '0'
|
|
105
110
|
requirements: []
|
|
106
|
-
rubygems_version:
|
|
111
|
+
rubygems_version: 4.0.6
|
|
107
112
|
specification_version: 4
|
|
108
113
|
summary: Robust Server Socket gem for RobustPro
|
|
109
114
|
test_files: []
|