robust_server_socket 0.3.2 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.en.md +614 -0
- data/README.md +326 -0
- data/lib/robust_server_socket/cacher.rb +132 -0
- data/lib/robust_server_socket/client_token.rb +20 -41
- data/lib/robust_server_socket/configuration.rb +22 -3
- data/lib/robust_server_socket/modules/client_auth_protection.rb +20 -0
- data/lib/robust_server_socket/modules/dos_attack_protection.rb +21 -0
- data/lib/robust_server_socket/modules/replay_attack_protection.rb +50 -0
- data/lib/robust_server_socket/rate_limiter.rb +6 -15
- data/lib/robust_server_socket/secure_token/decrypt.rb +0 -2
- data/lib/robust_server_socket.rb +22 -6
- data/lib/version.rb +1 -1
- metadata +8 -3
- data/lib/robust_server_socket/secure_token/cacher.rb +0 -138
|
@@ -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
|
|
@@ -75,12 +75,17 @@ files:
|
|
|
75
75
|
- ".rspec"
|
|
76
76
|
- CODE_OF_CONDUCT.md
|
|
77
77
|
- LICENSE.txt
|
|
78
|
+
- README.en.md
|
|
79
|
+
- README.md
|
|
78
80
|
- Rakefile
|
|
79
81
|
- lib/robust_server_socket.rb
|
|
82
|
+
- lib/robust_server_socket/cacher.rb
|
|
80
83
|
- lib/robust_server_socket/client_token.rb
|
|
81
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
|
|
82
88
|
- lib/robust_server_socket/rate_limiter.rb
|
|
83
|
-
- lib/robust_server_socket/secure_token/cacher.rb
|
|
84
89
|
- lib/robust_server_socket/secure_token/decrypt.rb
|
|
85
90
|
- lib/version.rb
|
|
86
91
|
- robust_server_socket.gemspec
|
|
@@ -101,7 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
101
106
|
- !ruby/object:Gem::Version
|
|
102
107
|
version: '0'
|
|
103
108
|
requirements: []
|
|
104
|
-
rubygems_version:
|
|
109
|
+
rubygems_version: 4.0.6
|
|
105
110
|
specification_version: 4
|
|
106
111
|
summary: Robust Server Socket gem for RobustPro
|
|
107
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
|