macaw_framework 1.2.4 → 1.2.6
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/CHANGELOG.md +8 -0
- data/README.md +15 -8
- data/lib/macaw_framework/aspects/cache_aspect.rb +5 -4
- data/lib/macaw_framework/core/common/server_base.rb +14 -15
- data/lib/macaw_framework/core/thread_server.rb +5 -3
- data/lib/macaw_framework/version.rb +1 -1
- data/lib/macaw_framework.rb +35 -25
- data/sig/macaw_framework/macaw.rbs +4 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2353cd5ffe4a264a2eacab71fc2de65c5c4a6b2cd925666529e36b8bdde0ada6
|
4
|
+
data.tar.gz: 4855cf3e106380f6c6718058001cd6cbd888a6c0f1927f833da3bc97cf648785
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 85f5262044b8b3029ad507d501655f72ccd022630b34fc72a5606fe993d223a7e83b245b2ee858adbedf1723717d2d050e1276badf2a353d89612d2c544253a4
|
7
|
+
data.tar.gz: 31b67715e3f2b2c792262d53f6488ae0b8df38c2c1f700fd5590b45927f49dc47fa8c84db884d2ec560b0e981582091b3f38a0f8af06bbddb26efd26f33e2e58
|
data/CHANGELOG.md
CHANGED
@@ -128,3 +128,11 @@
|
|
128
128
|
|
129
129
|
- Fixing small bug on lof during endpoint declaration
|
130
130
|
- Disclosing security issue on session storage
|
131
|
+
|
132
|
+
## [1.2.5]
|
133
|
+
|
134
|
+
- Improvements to cache usability
|
135
|
+
|
136
|
+
## [1.2.6]
|
137
|
+
|
138
|
+
- Improving session strategy and fixing vulnerabilities on it.
|
data/README.md
CHANGED
@@ -104,7 +104,7 @@ m.start!
|
|
104
104
|
### Caching: Improve performance by caching responses and configuring cache invalidation
|
105
105
|
|
106
106
|
```ruby
|
107
|
-
m.get('/cached_data', cache:
|
107
|
+
m.get('/cached_data', cache: ["header_to_cache", "query_param_to_cache"]) do |context|
|
108
108
|
# Retrieve data
|
109
109
|
end
|
110
110
|
```
|
@@ -113,6 +113,15 @@ end
|
|
113
113
|
|
114
114
|
### Session management: Handle user sessions with server-side in-memory storage
|
115
115
|
|
116
|
+
Session will only be enabled if it's configurations exists in the `application.json` file.
|
117
|
+
The session mechanism works by recovering the Session ID from a client sent header. The default
|
118
|
+
header is `X-Session-ID`, but it can be changed in the `application.json` file.
|
119
|
+
|
120
|
+
This header will be sent back to the user on every response if Session is enabled. Also, the
|
121
|
+
session ID will be automatically generated and sent to a client if this client does not provide
|
122
|
+
a session id in the HTTP request. In the case of the client sending an ID of an expired session
|
123
|
+
the framework will return a new session with a new ID.
|
124
|
+
|
116
125
|
```ruby
|
117
126
|
m.get('/login') do |context|
|
118
127
|
# Authenticate user
|
@@ -129,8 +138,6 @@ m.get('/dashboard') do |context|
|
|
129
138
|
end
|
130
139
|
```
|
131
140
|
|
132
|
-
**Caution: This feature is vulnerable to IP spoofing and may disrupt sessions on devices sharing the same network (e.g., Wi-Fi).**
|
133
|
-
|
134
141
|
### Configuration: Customize various aspects of the framework through the application.json configuration file, such as rate limiting, SSL support, and Prometheus integration
|
135
142
|
|
136
143
|
```json
|
@@ -140,11 +147,7 @@ end
|
|
140
147
|
"bind": "localhost",
|
141
148
|
"threads": 200,
|
142
149
|
"cache": {
|
143
|
-
"cache_invalidation": 3600
|
144
|
-
"ignore_headers": [
|
145
|
-
"header-to-be-ignored-from-caching-strategy",
|
146
|
-
"another-header-to-be-ignored-from-caching-strategy"
|
147
|
-
]
|
150
|
+
"cache_invalidation": 3600
|
148
151
|
},
|
149
152
|
"prometheus": {
|
150
153
|
"endpoint": "/metrics"
|
@@ -159,6 +162,10 @@ end
|
|
159
162
|
"key_type": "EC",
|
160
163
|
"cert_file_name": "path/to/cert/file/file.crt",
|
161
164
|
"key_file_name": "path/to/cert/key/file.key"
|
165
|
+
},
|
166
|
+
"session": {
|
167
|
+
"secure_header": "X-Session-ID",
|
168
|
+
"invalidation_time": 3600
|
162
169
|
}
|
163
170
|
}
|
164
171
|
}
|
@@ -6,7 +6,7 @@ module CacheAspect
|
|
6
6
|
def call_endpoint(cache, *args)
|
7
7
|
return super(*args) unless !cache[:cache].nil? && cache[:endpoints_to_cache]&.include?(args[0])
|
8
8
|
|
9
|
-
cache_filtered_name = cache_name_filter(args[1], cache[:
|
9
|
+
cache_filtered_name = cache_name_filter(args[1], cache[:cached_methods][args[0]])
|
10
10
|
|
11
11
|
cache[:cache].mutex.synchronize do
|
12
12
|
return cache[:cache].cache[cache_filtered_name][0] unless cache[:cache].cache[cache_filtered_name].nil?
|
@@ -19,9 +19,10 @@ module CacheAspect
|
|
19
19
|
|
20
20
|
private
|
21
21
|
|
22
|
-
def cache_name_filter(client_data,
|
23
|
-
filtered_headers = client_data[:headers]
|
24
|
-
|
22
|
+
def cache_name_filter(client_data, cached_methods_params)
|
23
|
+
filtered_headers = client_data[:headers]&.filter { |key, _value| cached_methods_params&.include?(key) }
|
24
|
+
filtered_params = client_data[:params]&.filter { |key, _value| cached_methods_params&.include?(key) }
|
25
|
+
[{ params: filtered_params, headers: filtered_headers }].to_s.to_sym
|
25
26
|
end
|
26
27
|
|
27
28
|
def should_cache_response?(status)
|
@@ -8,6 +8,7 @@ require_relative "../../utils/supported_ssl_versions"
|
|
8
8
|
require_relative "../../aspects/prometheus_aspect"
|
9
9
|
require_relative "../../aspects/logging_aspect"
|
10
10
|
require_relative "../../aspects/cache_aspect"
|
11
|
+
require "securerandom"
|
11
12
|
|
12
13
|
##
|
13
14
|
# Base module for Server classes. It contains
|
@@ -21,14 +22,14 @@ module ServerBase
|
|
21
22
|
|
22
23
|
private
|
23
24
|
|
24
|
-
def call_endpoint(name, client_data,
|
25
|
+
def call_endpoint(name, client_data, session_id, _client_ip)
|
25
26
|
@macaw.send(
|
26
27
|
name.to_sym,
|
27
28
|
{
|
28
29
|
headers: client_data[:headers],
|
29
30
|
body: client_data[:body],
|
30
31
|
params: client_data[:params],
|
31
|
-
client: @session[
|
32
|
+
client: @session[session_id][0]
|
32
33
|
}
|
33
34
|
)
|
34
35
|
end
|
@@ -42,12 +43,14 @@ module ServerBase
|
|
42
43
|
raise EndpointNotMappedError unless @macaw.respond_to?(method_name)
|
43
44
|
raise TooManyRequestsError unless @rate_limit.nil? || @rate_limit.allow?(client.peeraddr[3])
|
44
45
|
|
45
|
-
declare_client_session(client)
|
46
46
|
client_data = get_client_data(body, headers, parameters)
|
47
|
+
session_id = declare_client_session(client_data[:headers], @macaw.secure_header) if @macaw.session
|
47
48
|
|
48
49
|
@macaw_log&.info("Running #{path.gsub("\n", "").gsub("\r", "")}")
|
49
50
|
message, status, response_headers = call_endpoint(@prometheus_middleware, @macaw_log, @cache,
|
50
|
-
method_name, client_data, client.peeraddr[3])
|
51
|
+
method_name, client_data, session_id, client.peeraddr[3])
|
52
|
+
response_headers ||= {}
|
53
|
+
response_headers[@macaw.secure_header] = session_id if @macaw.session
|
51
54
|
status ||= 200
|
52
55
|
message ||= nil
|
53
56
|
response_headers ||= nil
|
@@ -69,9 +72,11 @@ module ServerBase
|
|
69
72
|
end
|
70
73
|
end
|
71
74
|
|
72
|
-
def declare_client_session(
|
73
|
-
|
74
|
-
|
75
|
+
def declare_client_session(headers, secure_header_name)
|
76
|
+
session_id = headers[secure_header_name] || SecureRandom.uuid
|
77
|
+
session_id = SecureRandom.uuid if @session[session_id].nil?
|
78
|
+
@session[session_id] ||= [{}, Time.now]
|
79
|
+
session_id
|
75
80
|
end
|
76
81
|
|
77
82
|
def set_rate_limiting
|
@@ -83,12 +88,6 @@ module ServerBase
|
|
83
88
|
)
|
84
89
|
end
|
85
90
|
|
86
|
-
def set_cache_ignored_h
|
87
|
-
return unless @macaw.config&.dig("macaw", "cache", "ignore_headers")
|
88
|
-
|
89
|
-
@macaw.config["macaw"]["cache"]["ignore_headers"] || []
|
90
|
-
end
|
91
|
-
|
92
91
|
def set_ssl
|
93
92
|
ssl_config = @macaw.config["macaw"]["ssl"] if @macaw.config&.dig("macaw", "ssl")
|
94
93
|
ssl_config ||= nil
|
@@ -116,7 +115,7 @@ module ServerBase
|
|
116
115
|
end
|
117
116
|
|
118
117
|
def set_session
|
119
|
-
@session
|
118
|
+
@session ||= {}
|
120
119
|
inv = if @macaw.config&.dig("macaw", "session", "invalidation_time")
|
121
120
|
MemoryInvalidationMiddleware.new(@macaw.config["macaw"]["session"]["invalidation_time"])
|
122
121
|
else
|
@@ -128,7 +127,7 @@ module ServerBase
|
|
128
127
|
def set_features
|
129
128
|
@is_shutting_down = false
|
130
129
|
set_rate_limiting
|
131
|
-
set_session
|
130
|
+
set_session if @macaw.session
|
132
131
|
set_ssl
|
133
132
|
end
|
134
133
|
end
|
@@ -32,11 +32,13 @@ class ThreadServer
|
|
32
32
|
@macaw_log = macaw.macaw_log
|
33
33
|
@num_threads = macaw.threads
|
34
34
|
@work_queue = Queue.new
|
35
|
-
ignored_headers = set_cache_ignored_h
|
36
35
|
set_features
|
37
36
|
@rate_limit ||= nil
|
38
|
-
|
39
|
-
|
37
|
+
@cache = {
|
38
|
+
cache: cache,
|
39
|
+
endpoints_to_cache: endpoints_to_cache || [],
|
40
|
+
cached_methods: macaw.cached_methods
|
41
|
+
}
|
40
42
|
@prometheus = prometheus
|
41
43
|
@prometheus_middleware = prometheus_mw
|
42
44
|
@workers = []
|
data/lib/macaw_framework.rb
CHANGED
@@ -19,30 +19,16 @@ module MacawFramework
|
|
19
19
|
# Class responsible for creating endpoints and
|
20
20
|
# starting the web server.
|
21
21
|
class Macaw
|
22
|
-
|
23
|
-
# Array containing the routes defined in the application
|
24
|
-
attr_reader :routes, :macaw_log, :config, :jobs
|
22
|
+
attr_reader :routes, :macaw_log, :config, :jobs, :cached_methods, :secure_header, :session
|
25
23
|
attr_accessor :port, :bind, :threads
|
26
24
|
|
27
25
|
##
|
26
|
+
# Initialize Macaw Class
|
28
27
|
# @param {Logger} custom_log
|
28
|
+
# @param {ThreadServer} server
|
29
|
+
# @param {String?} dir
|
29
30
|
def initialize(custom_log: Logger.new($stdout), server: ThreadServer, dir: nil)
|
30
|
-
|
31
|
-
@routes = []
|
32
|
-
@macaw_log ||= custom_log
|
33
|
-
@config = JSON.parse(File.read("application.json"))
|
34
|
-
@port = @config["macaw"]["port"] || 8080
|
35
|
-
@bind = @config["macaw"]["bind"] || "localhost"
|
36
|
-
@threads = @config["macaw"]["threads"] || 200
|
37
|
-
unless @config["macaw"]["cache"].nil?
|
38
|
-
@cache = MemoryInvalidationMiddleware.new(@config["macaw"]["cache"]["cache_invalidation"].to_i || 3_600)
|
39
|
-
end
|
40
|
-
@prometheus = Prometheus::Client::Registry.new if @config["macaw"]["prometheus"]
|
41
|
-
@prometheus_middleware = PrometheusMiddleware.new if @config["macaw"]["prometheus"]
|
42
|
-
@prometheus_middleware.configure_prometheus(@prometheus, @config, self) if @config["macaw"]["prometheus"]
|
43
|
-
rescue StandardError => e
|
44
|
-
@macaw_log&.warn(e.message)
|
45
|
-
end
|
31
|
+
apply_options(custom_log)
|
46
32
|
create_endpoint_public_files(dir)
|
47
33
|
@port ||= 8080
|
48
34
|
@bind ||= "localhost"
|
@@ -65,7 +51,7 @@ module MacawFramework
|
|
65
51
|
# macaw.get("/hello") do |context|
|
66
52
|
# return "Hello World!", 200, { "Content-Type" => "text/plain" }
|
67
53
|
# end
|
68
|
-
def get(path, cache:
|
54
|
+
def get(path, cache: [], &block)
|
69
55
|
map_new_endpoint("get", cache, path, &block)
|
70
56
|
end
|
71
57
|
|
@@ -81,7 +67,7 @@ module MacawFramework
|
|
81
67
|
# macaw.post("/hello") do |context|
|
82
68
|
# return "Hello World!", 200, { "Content-Type" => "text/plain" }
|
83
69
|
# end
|
84
|
-
def post(path, cache:
|
70
|
+
def post(path, cache: [], &block)
|
85
71
|
map_new_endpoint("post", cache, path, &block)
|
86
72
|
end
|
87
73
|
|
@@ -96,7 +82,7 @@ module MacawFramework
|
|
96
82
|
# macaw.put("/hello") do |context|
|
97
83
|
# return "Hello World!", 200, { "Content-Type" => "text/plain" }
|
98
84
|
# end
|
99
|
-
def put(path, cache:
|
85
|
+
def put(path, cache: [], &block)
|
100
86
|
map_new_endpoint("put", cache, path, &block)
|
101
87
|
end
|
102
88
|
|
@@ -111,7 +97,7 @@ module MacawFramework
|
|
111
97
|
# macaw.patch("/hello") do |context|
|
112
98
|
# return "Hello World!", 200, { "Content-Type" => "text/plain" }
|
113
99
|
# end
|
114
|
-
def patch(path, cache:
|
100
|
+
def patch(path, cache: [], &block)
|
115
101
|
map_new_endpoint("patch", cache, path, &block)
|
116
102
|
end
|
117
103
|
|
@@ -126,7 +112,7 @@ module MacawFramework
|
|
126
112
|
# macaw.delete("/hello") do |context|
|
127
113
|
# return "Hello World!", 200, { "Content-Type" => "text/plain" }
|
128
114
|
# end
|
129
|
-
def delete(path, cache:
|
115
|
+
def delete(path, cache: [], &block)
|
130
116
|
map_new_endpoint("delete", cache, path, &block)
|
131
117
|
end
|
132
118
|
|
@@ -191,12 +177,36 @@ module MacawFramework
|
|
191
177
|
|
192
178
|
private
|
193
179
|
|
180
|
+
def apply_options(custom_log)
|
181
|
+
@routes = []
|
182
|
+
@cached_methods = {}
|
183
|
+
@macaw_log ||= custom_log
|
184
|
+
@config = JSON.parse(File.read("application.json"))
|
185
|
+
@port = @config["macaw"]["port"] || 8080
|
186
|
+
@bind = @config["macaw"]["bind"] || "localhost"
|
187
|
+
@session = false
|
188
|
+
unless @config["macaw"]["session"].nil?
|
189
|
+
@session = true
|
190
|
+
@secure_header = @config["macaw"]["session"]["secure_header"] || "X-Session-ID"
|
191
|
+
end
|
192
|
+
@threads = @config["macaw"]["threads"] || 200
|
193
|
+
unless @config["macaw"]["cache"].nil?
|
194
|
+
@cache = MemoryInvalidationMiddleware.new(@config["macaw"]["cache"]["cache_invalidation"].to_i || 3_600)
|
195
|
+
end
|
196
|
+
@prometheus = Prometheus::Client::Registry.new if @config["macaw"]["prometheus"]
|
197
|
+
@prometheus_middleware = PrometheusMiddleware.new if @config["macaw"]["prometheus"]
|
198
|
+
@prometheus_middleware.configure_prometheus(@prometheus, @config, self) if @config["macaw"]["prometheus"]
|
199
|
+
rescue StandardError => e
|
200
|
+
@macaw_log&.warn(e.message)
|
201
|
+
end
|
202
|
+
|
194
203
|
def server_loop(server)
|
195
204
|
server.run
|
196
205
|
end
|
197
206
|
|
198
207
|
def map_new_endpoint(prefix, cache, path, &block)
|
199
|
-
@endpoints_to_cache << "#{prefix}.#{RequestDataFiltering.sanitize_method_name(path)}"
|
208
|
+
@endpoints_to_cache << "#{prefix}.#{RequestDataFiltering.sanitize_method_name(path)}" unless cache.empty?
|
209
|
+
@cached_methods["#{prefix}.#{RequestDataFiltering.sanitize_method_name(path)}"] = cache unless cache.empty?
|
200
210
|
path_clean = RequestDataFiltering.extract_path(path)
|
201
211
|
slash = path[0] == "/" ? "" : "/"
|
202
212
|
@macaw_log&.info("Defining #{prefix.upcase} endpoint at #{slash}#{path}")
|
@@ -2,6 +2,7 @@ module MacawFramework
|
|
2
2
|
class Macaw
|
3
3
|
@bind: String
|
4
4
|
@cache: untyped
|
5
|
+
@cached_methods: Hash[String, Array[String]]
|
5
6
|
@config: Hash[String, untyped]
|
6
7
|
@cron_runner: CronRunner
|
7
8
|
@endpoints_to_cache: Array[String]
|
@@ -9,8 +10,11 @@ module MacawFramework
|
|
9
10
|
|
10
11
|
@prometheus: untyped
|
11
12
|
@prometheus_middleware: untyped
|
13
|
+
@secure_header: String
|
12
14
|
@server: untyped
|
13
15
|
|
16
|
+
@server_class: untyped
|
17
|
+
@session: bool
|
14
18
|
@threads: Integer
|
15
19
|
|
16
20
|
attr_accessor bind: String
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: macaw_framework
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.2.
|
4
|
+
version: 1.2.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aria Diniz
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-04-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: prometheus-client
|