macaw_framework 1.4.2 → 1.5.0
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 +1 -1
- data/CHANGELOG.md +46 -1
- data/Gemfile +0 -1
- data/README.md +11 -87
- data/ext/macaw_framework_ext/extconf.rb +7 -0
- data/ext/macaw_framework_ext/macaw_framework_ext.c +8 -0
- data/ext/macaw_framework_ext/macaw_framework_ext.h +10 -0
- data/lib/macaw_framework/aspects/cache_aspect.rb +9 -6
- data/lib/macaw_framework/aspects/logging_aspect.rb +11 -2
- data/lib/macaw_framework/cache.rb +7 -8
- data/lib/macaw_framework/core/common/server_base.rb +47 -44
- data/lib/macaw_framework/core/thread_server.rb +23 -33
- data/lib/macaw_framework/data_filters/log_data_filter.rb +2 -1
- data/lib/macaw_framework/data_filters/request_data_filtering.rb +20 -9
- data/lib/macaw_framework/data_filters/response_data_filter.rb +8 -7
- data/lib/macaw_framework/errors/payload_too_large_error.rb +5 -0
- data/lib/macaw_framework/macaw.rb +21 -62
- data/lib/macaw_framework/middlewares/memory_invalidation_middleware.rb +2 -1
- data/lib/macaw_framework/utils/supported_ssl_versions.rb +0 -2
- data/lib/macaw_framework/version.rb +1 -1
- data/lib/macaw_framework.rb +8 -0
- metadata +10 -21
- data/lib/macaw_framework/aspects/prometheus_aspect.rb +0 -28
- data/lib/macaw_framework/core/cron_runner.rb +0 -50
- data/lib/macaw_framework/middlewares/prometheus_middleware.rb +0 -48
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 803f40a0a12304b555d3f998e323b4e475f10194518f566dc98e24c6b3984461
|
|
4
|
+
data.tar.gz: c0e90bf375a372105324e4444419ce05b6362e056bc2d2894820653cf9d7ef0a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0d43d8371216644879492cf22e3269aa743b4d7f48638e2e06d6fa72c5d2ee0975611111306742457c57b0d443937b6983ee95c00a8400c897aa6c9ff8a083b9
|
|
7
|
+
data.tar.gz: 7f453d9a093520e088dce07c93c9a818869511dc8b63a9737de6d958edd9ba88281d7f2104a46f0c39db945041de06e2d8adf016eb860d87df2fa406f38292f7
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -161,5 +161,50 @@
|
|
|
161
161
|
## [1.4.1] - 2026-02-01
|
|
162
162
|
- Fixing issue when re-spawning new threads
|
|
163
163
|
|
|
164
|
-
## [1.4.
|
|
164
|
+
## [1.4.2] - 2026-02-24
|
|
165
165
|
- Removing unused Rate Limiting Middleware
|
|
166
|
+
|
|
167
|
+
## [1.4.3] - 2026-03-04
|
|
168
|
+
- Fix critical bug in `sanitize_parameter_value` where the first `gsub` result was discarded, leaving special characters unsanitized
|
|
169
|
+
- Fix header parsing regex to correctly accept real-world headers (Authorization, Cookie, User-Agent, Host with port, etc.)
|
|
170
|
+
- Fix deadlock in `maintain_worker_pool` caused by non-reentrant mutex re-acquisition when respawning dead workers
|
|
171
|
+
- Add missing `require 'digest'` in `LogDataFilter`, preventing `NameError` when sensitive fields are configured
|
|
172
|
+
- Fix thread-safety in `Cache#write` and `Cache#read`: both now always synchronize on the internal mutex
|
|
173
|
+
- Fix thread-safety of `@session` hash: `declare_client_session` now synchronizes on a dedicated mutex
|
|
174
|
+
- Fix `**kwargs` being silently dropped in `LoggingAspect` and `PrometheusAspect` when forwarding to `super`
|
|
175
|
+
- Fix `CacheAspect` holding mutex during entire endpoint execution on cache miss; endpoint now runs outside the lock
|
|
176
|
+
- Remove blocking `sleep(2)` from `MemoryInvalidationMiddleware` constructor; eviction thread no longer halts server startup
|
|
177
|
+
- Remove `sleep(1)` per cron job from `CronRunner#start_cron_job_thread`; startup is no longer O(N) seconds
|
|
178
|
+
- Remove dead code in `CronRunner`: duplicate `start_delay ||= 0` and always-true `unless start_delay.nil?` guard
|
|
179
|
+
- Make `application.json` path configurable via `MACAW_CONFIG` env var; rescue `Errno::ENOENT` and `JSON::ParserError` with graceful fallback
|
|
180
|
+
- Remove broken SSL2 and SSL3 from `SupportedSSLVersions` (POODLE/DROWN, unavailable on modern OpenSSL)
|
|
181
|
+
- `LoggingAspect` now logs request params, body, and response status on every endpoint call
|
|
182
|
+
- Use `Process.clock_gettime(Process::CLOCK_MONOTONIC)` in `PrometheusAspect` instead of `Time.now` for accurate duration measurement
|
|
183
|
+
- Fix Prometheus `/metrics` endpoint `Content-Type` to `text/plain; version=0.0.4; charset=utf-8`
|
|
184
|
+
- Fix RFC 7230 non-compliance: remove stray space before `\r\n` in HTTP status line
|
|
185
|
+
- Add eviction thread error rescue in `MemoryInvalidationMiddleware` to prevent silent thread death
|
|
186
|
+
- Set `frozen_string_literal: true` consistently across all library files
|
|
187
|
+
- Implement HTTP keep-alive: persistent TCP connections now reuse the same socket for multiple requests without a new handshake
|
|
188
|
+
- Add configurable per-connection read timeout (default 30 s, configurable via `keep_alive_timeout` in `application.json`) to prevent idle or faulty clients from holding worker threads indefinitely
|
|
189
|
+
- Server now responds with `Connection: keep-alive` or `Connection: close` headers according to the client's request
|
|
190
|
+
- Fix request parser to properly detect client EOF and raise `EOFError` instead of propagating `nil` through the parsing pipeline
|
|
191
|
+
|
|
192
|
+
## [1.5.0] - 2026-03-04
|
|
193
|
+
- Use `IO#timeout=` (Ruby 3.2+) for socket timeout with `SO_RCVTIMEO` fallback for older Ruby and SSL sockets
|
|
194
|
+
- Remove Prometheus integration: `PrometheusAspect`, `PrometheusMiddleware`, and `prometheus-client` gem dependency removed
|
|
195
|
+
- Remove built-in session management: `declare_client_session`, `set_session`, `@session`, and all related configuration removed
|
|
196
|
+
- Remove `CronRunner` and `setup_job`: periodic job scheduling is no longer a responsibility of the framework; use a dedicated job library instead
|
|
197
|
+
- Remove `start_without_server!` method, which existed solely to support cron-only deployments
|
|
198
|
+
- Handle SIGTERM for graceful shutdown in containerised deployments; SIGTERM now triggers the same shutdown path as SIGINT
|
|
199
|
+
- Add `Content-Length: 0` to inline 404 and 500 error responses for RFC 7230 compliance
|
|
200
|
+
- Change default `bind` from `'localhost'` to `'0.0.0.0'` so the server is reachable in containers without explicit configuration
|
|
201
|
+
- Sanitize `\r` and `\n` from response header keys and values to prevent HTTP response splitting attacks
|
|
202
|
+
- Add configurable request body size limit (`max_body_size` in `application.json`, default 1 MB); requests exceeding the limit are rejected with 413 Content Too Large before the body is read
|
|
203
|
+
- Fix `maintain_worker_pool` iteration bug: `each_with_index` + `delete_at` skipped elements after a deletion; replaced with `reject!` + bulk respawn
|
|
204
|
+
- Fix cache TTL fallback: `nil.to_i` returned 0 when `cache_invalidation` was absent, silently creating a zero-TTL cache; replaced with safe navigation `&.to_i || 3_600`
|
|
205
|
+
- Fix `CacheAspect` crash when endpoint returns `nil`: guard added before `response[1]` access
|
|
206
|
+
- Replace `sanitize_parameter_value` character stripping with proper CGI URL-decoding (preserves emails, UUIDs, decimal values)
|
|
207
|
+
- Broaden HTTP version pattern in request parser from `HTTP/1.1` literal to regex — HTTP/1.0 requests now route correctly
|
|
208
|
+
- Add `rescue StandardError` guard in `Cache#invalidation_process` to prevent silent background eviction-thread death
|
|
209
|
+
- Fix `ThreadServer#shutdown` poison-pill count: uses actual `@workers.size` instead of `@num_threads`
|
|
210
|
+
- Add C extension scaffold (`ext/macaw_framework_ext/`) as a foundation for future native performance-sensitive routines
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
# MacawFramework
|
|
3
3
|
|
|
4
4
|
MacawFramework is a lightweight, easy-to-use web framework for Ruby designed to simplify the development of small to
|
|
5
|
-
medium-sized web applications.
|
|
6
|
-
MacawFramework provides developers with the essential tools to quickly build and deploy their applications.
|
|
5
|
+
medium-sized web applications. Weighing less than 26Kb with support for various HTTP methods and response caching,
|
|
7
6
|
|
|
8
7
|
- [MacawFramework](#macawframework)
|
|
9
8
|
* [Features](#features)
|
|
@@ -14,11 +13,8 @@ MacawFramework provides developers with the essential tools to quickly build and
|
|
|
14
13
|
* [Usage](#usage)
|
|
15
14
|
+ [Basic routing: Define routes with support for GET, POST, PUT, PATCH, and DELETE HTTP methods](#basic-routing-define-routes-with-support-for-get-post-put-patch-and-delete-http-methods)
|
|
16
15
|
+ [Caching: Improve performance by caching responses and configuring cache invalidation](#caching-improve-performance-by-caching-responses-and-configuring-cache-invalidation)
|
|
17
|
-
+ [
|
|
18
|
-
+ [Configuration: Customize various aspects of the framework through the application.json configuration file, such as SSL support and Prometheus integration](#configuration-customize-various-aspects-of-the-framework-through-the-applicationjson-configuration-file-such-as-ssl-support-and-prometheus-integration)
|
|
19
|
-
+ [Monitoring: Easily monitor your application performance and metrics with built-in Prometheus support](#monitoring-easily-monitor-your-application-performance-and-metrics-with-built-in-prometheus-support)
|
|
16
|
+
+ [Configuration: Customize various aspects of the framework through the application.json configuration file](#configuration-customize-various-aspects-of-the-framework-through-the-applicationjson-configuration-file)
|
|
20
17
|
+ [Routing for "public" Folder: Serve Static Assets](#routing-for-public-folder-serve-static-assets)
|
|
21
|
-
+ [Periodic Jobs](#periodic-jobs)
|
|
22
18
|
+ [Tips](#tips)
|
|
23
19
|
* [Contributing](#contributing)
|
|
24
20
|
* [License](#license)
|
|
@@ -27,10 +23,8 @@ MacawFramework provides developers with the essential tools to quickly build and
|
|
|
27
23
|
## Features
|
|
28
24
|
|
|
29
25
|
- Simple routing with support for GET, POST, PUT, PATCH, and DELETE HTTP methods
|
|
30
|
-
-
|
|
31
|
-
- Session management with server-side in-memory storage
|
|
26
|
+
- Response caching middleware for improved performance
|
|
32
27
|
- SSL support
|
|
33
|
-
- Prometheus integration for monitoring and metrics
|
|
34
28
|
- Less than 26Kb
|
|
35
29
|
- Easy to learn
|
|
36
30
|
|
|
@@ -52,7 +46,7 @@ We evaluated MacawFramework (Version 1.2.0) to assess its ability to handle simu
|
|
|
52
46
|
|
|
53
47
|
MacawFramework is built to be highly compatible, since it uses only native Ruby code:
|
|
54
48
|
|
|
55
|
-
- **MRI**: MacawFramework is compatible with Matz's Ruby Interpreter (MRI), version 3.
|
|
49
|
+
- **MRI**: MacawFramework is compatible with Matz's Ruby Interpreter (MRI), version 3.2.0 and onwards. If you are using this version or a more recent one, you should not encounter any compatibility issues.
|
|
56
50
|
|
|
57
51
|
- **TruffleRuby**: TruffleRuby is another Ruby interpreter that is fully compatible with MacawFramework. This provides developers with more flexibility in their choice of Ruby interpreter.
|
|
58
52
|
|
|
@@ -93,10 +87,9 @@ end
|
|
|
93
87
|
|
|
94
88
|
m.post('/submit_data/:path_variable') do |context|
|
|
95
89
|
context[:body] # Client body data
|
|
96
|
-
context[:params] # Client params, like URL parameters or variables
|
|
90
|
+
context[:params] # Client params, like URL parameters or path variables
|
|
97
91
|
context[:headers] # Client headers
|
|
98
92
|
context[:params][:path_variable] # The defined path variable can be found in :params
|
|
99
|
-
context[:client] # Client session
|
|
100
93
|
end
|
|
101
94
|
|
|
102
95
|
m.start!
|
|
@@ -125,70 +118,30 @@ MacawFramework::Cache.read(:name) # Maria
|
|
|
125
118
|
|
|
126
119
|
Manual cache does not need any additional configuration.
|
|
127
120
|
|
|
128
|
-
###
|
|
129
|
-
|
|
130
|
-
Session will only be enabled if it's configurations exists in the `application.json` file.
|
|
131
|
-
The session mechanism works by recovering the Session ID from a client sent header. The default
|
|
132
|
-
header is `X-Session-ID`, but it can be changed in the `application.json` file.
|
|
133
|
-
|
|
134
|
-
This header will be sent back to the user on every response if Session is enabled. Also, the
|
|
135
|
-
session ID will be automatically generated and sent to a client if this client does not provide
|
|
136
|
-
a session id in the HTTP request. In the case of the client sending an ID of an expired session
|
|
137
|
-
the framework will return a new session with a new ID.
|
|
138
|
-
|
|
139
|
-
```ruby
|
|
140
|
-
m = MacawFramework::Macaw.new
|
|
141
|
-
|
|
142
|
-
m.get('/login') do |context|
|
|
143
|
-
# Authenticate user
|
|
144
|
-
context[:client][:user_id] = user_id
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
m.get('/dashboard') do |context|
|
|
148
|
-
# Check if the user is logged in
|
|
149
|
-
if context[:client][:user_id]
|
|
150
|
-
# Show dashboard
|
|
151
|
-
else
|
|
152
|
-
# Redirect to login
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
### Configuration: Customize various aspects of the framework through the application.json configuration file, such as SSL support and Prometheus integration
|
|
121
|
+
### Configuration: Customize various aspects of the framework through the application.json configuration file
|
|
158
122
|
|
|
159
123
|
```json
|
|
160
124
|
{
|
|
161
125
|
"macaw": {
|
|
162
126
|
"port": 8080,
|
|
163
|
-
"bind": "
|
|
127
|
+
"bind": "0.0.0.0",
|
|
164
128
|
"threads": 200,
|
|
129
|
+
"keep_alive_timeout": 30,
|
|
130
|
+
"max_body_size": 1048576,
|
|
165
131
|
"cache": {
|
|
166
132
|
"cache_invalidation": 3600
|
|
167
133
|
},
|
|
168
|
-
"prometheus": {
|
|
169
|
-
"endpoint": "/metrics"
|
|
170
|
-
},
|
|
171
134
|
"ssl": {
|
|
172
|
-
"min": "
|
|
135
|
+
"min": "TLS1.2",
|
|
173
136
|
"max": "TLS1.3",
|
|
174
137
|
"key_type": "EC",
|
|
175
138
|
"cert_file_name": "path/to/cert/file/file.crt",
|
|
176
139
|
"key_file_name": "path/to/cert/key/file.key"
|
|
177
|
-
},
|
|
178
|
-
"session": {
|
|
179
|
-
"secure_header": "X-Session-ID",
|
|
180
|
-
"invalidation_time": 3600
|
|
181
140
|
}
|
|
182
141
|
}
|
|
183
142
|
}
|
|
184
143
|
```
|
|
185
144
|
|
|
186
|
-
### Monitoring: Easily monitor your application performance and metrics with built-in Prometheus support
|
|
187
|
-
|
|
188
|
-
```shell
|
|
189
|
-
curl http://localhost:8080/metrics
|
|
190
|
-
```
|
|
191
|
-
|
|
192
145
|
### Routing for "public" Folder: Serve Static Assets
|
|
193
146
|
|
|
194
147
|
MacawFramework allows you to serve static assets, such as CSS, JavaScript, images, etc., through the "public" folder.
|
|
@@ -207,29 +160,6 @@ be accessible at http://yourdomain.com/img/logo.png without any additional confi
|
|
|
207
160
|
|
|
208
161
|
#### Caution: This is incompatible with most non-unix systems, such as Windows. If you are using a non-unix system, you will need to manually configure the "public" folder and use dir as nil to avoid problems.
|
|
209
162
|
|
|
210
|
-
### Periodic Jobs
|
|
211
|
-
|
|
212
|
-
Macaw Framework supports the declaration of periodic jobs right in your application code. This feature allows developers to
|
|
213
|
-
define tasks that run at set intervals, starting after an optional delay. Each job runs in a separate thread, meaning
|
|
214
|
-
your periodic jobs can execute in parallel without blocking the rest of your application.
|
|
215
|
-
|
|
216
|
-
Here's an example of how to declare a periodic job:
|
|
217
|
-
|
|
218
|
-
```ruby
|
|
219
|
-
m = MacawFramework::Macaw.new
|
|
220
|
-
|
|
221
|
-
m.setup_job(interval: 5, start_delay: 5, job_name: "cron job 1") do
|
|
222
|
-
puts "i'm a periodic job that runs every 5 secs!"
|
|
223
|
-
end
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
Values for interval and start_delay are in seconds.
|
|
227
|
-
|
|
228
|
-
**Caution: Defining a lot of jobs with low interval can severely degrade performance.**
|
|
229
|
-
|
|
230
|
-
If you want to build an application with just cron jobs, that don't need to run a web server, you can start
|
|
231
|
-
MacawFramework without running a web server with the `start_without_server!` method, instead of `start!`.
|
|
232
|
-
|
|
233
163
|
### Tips
|
|
234
164
|
|
|
235
165
|
- The automatic logging and log aspect are now optional. To disable them, simply start Macaw with `custom_log` set to nil.
|
|
@@ -270,19 +200,13 @@ m.threads = 300
|
|
|
270
200
|
- If the SSL configuration is provided in the `application.json` file with valid certificate and key files, the TCP server
|
|
271
201
|
will be wrapped with HTTPS security using the provided certificate.
|
|
272
202
|
|
|
273
|
-
- The supported values for `min` and `max` in the SSL configuration are: `
|
|
203
|
+
- The supported values for `min` and `max` in the SSL configuration are: `TLS1.2` and `TLS1.3`,
|
|
274
204
|
and the supported values for `key_type` are `RSA` and `EC`.
|
|
275
205
|
|
|
276
|
-
- If Prometheus is enabled, a GET endpoint will be defined at path `/metrics` to collect Prometheus metrics. This path
|
|
277
|
-
is configurable via the `application.json` file.
|
|
278
|
-
|
|
279
206
|
- The verb methods must always return a string or nil (used as the response), a number corresponding to the HTTP status
|
|
280
207
|
code to be returned to the client, and the response headers as a Hash or nil. If an endpoint doesn't return a value or
|
|
281
208
|
returns nil for body, status code, and headers, a default 200 OK status will be sent as the response.
|
|
282
209
|
|
|
283
|
-
- For cron jobs without a start_delay, a value of 0 will be used. For a job without a name, a unique name will be generated
|
|
284
|
-
for it.
|
|
285
|
-
|
|
286
210
|
- Ensure the "public" folder is placed in the same directory as the main.rb file: The "public" folder should contain any static assets,
|
|
287
211
|
such as CSS, JavaScript, or images, that your web application requires. Placing it in the same directory as the main.rb file ensures
|
|
288
212
|
that the server can correctly serve these assets.
|
|
@@ -4,17 +4,20 @@
|
|
|
4
4
|
# Aspect that provide cache for the endpoints.
|
|
5
5
|
module CacheAspect
|
|
6
6
|
def call_endpoint(cache, *args, **kwargs)
|
|
7
|
-
return super(*args, **kwargs)
|
|
7
|
+
return super(*args, **kwargs) if cache[:cache].nil? || !cache[:endpoints_to_cache]&.include?(args[0])
|
|
8
8
|
|
|
9
9
|
cache_filtered_name = cache_name_filter(args[1], cache[:cached_methods][args[0]])
|
|
10
10
|
|
|
11
|
-
cache[:cache].mutex.synchronize
|
|
12
|
-
|
|
11
|
+
cached_response = cache[:cache].mutex.synchronize { cache[:cache].cache[cache_filtered_name]&.dig(0) }
|
|
12
|
+
return cached_response unless cached_response.nil?
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
response = super(*args, **kwargs)
|
|
15
|
+
if response && should_cache_response?(response[1])
|
|
16
|
+
cache[:cache].mutex.synchronize do
|
|
17
|
+
cache[:cache].cache[cache_filtered_name] = [response, Time.now]
|
|
18
|
+
end
|
|
17
19
|
end
|
|
20
|
+
response
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
private
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# frozen_string_literal:
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'logger'
|
|
4
4
|
require_relative '../data_filters/log_data_filter'
|
|
@@ -11,13 +11,22 @@ module LoggingAspect
|
|
|
11
11
|
def call_endpoint(logger, *args, **kwargs)
|
|
12
12
|
return super(*args, **kwargs) if logger.nil?
|
|
13
13
|
|
|
14
|
+
endpoint_name = args[1]
|
|
15
|
+
client_data = args[2]
|
|
16
|
+
|
|
17
|
+
logger.info("Calling endpoint: #{endpoint_name} | " \
|
|
18
|
+
"params: #{LogDataFilter.sanitize_for_logging(client_data[:params].to_s)} | " \
|
|
19
|
+
"body: #{LogDataFilter.sanitize_for_logging(client_data[:body])}")
|
|
20
|
+
|
|
14
21
|
begin
|
|
15
|
-
response = super(*args)
|
|
22
|
+
response = super(*args, **kwargs)
|
|
16
23
|
rescue StandardError => e
|
|
17
24
|
logger.error("#{e.message}\n#{e.backtrace.join("\n")}")
|
|
18
25
|
raise e
|
|
19
26
|
end
|
|
20
27
|
|
|
28
|
+
logger.info("Response from endpoint: #{endpoint_name} | status: #{response&.dig(1)}")
|
|
29
|
+
|
|
21
30
|
response
|
|
22
31
|
end
|
|
23
32
|
end
|
|
@@ -37,13 +37,8 @@ class MacawFramework::Cache
|
|
|
37
37
|
# @example
|
|
38
38
|
# MacawFramework::Cache.write("name", "Maria", expires_in: 7200)
|
|
39
39
|
def write(tag, value, expires_in: 3600)
|
|
40
|
-
|
|
41
|
-
@
|
|
42
|
-
@cache.store(tag, { value: value, expires_in: Time.now + expires_in })
|
|
43
|
-
end
|
|
44
|
-
else
|
|
45
|
-
@cache[tag][:value] = value
|
|
46
|
-
@cache[tag][:expires_in] = Time.now + expires_in
|
|
40
|
+
@mutex.synchronize do
|
|
41
|
+
@cache.store(tag, { value: value, expires_in: Time.now + expires_in })
|
|
47
42
|
end
|
|
48
43
|
end
|
|
49
44
|
|
|
@@ -65,7 +60,9 @@ class MacawFramework::Cache
|
|
|
65
60
|
#
|
|
66
61
|
# @example
|
|
67
62
|
# MacawFramework::Cache.read("name") # Maria
|
|
68
|
-
def read(tag)
|
|
63
|
+
def read(tag)
|
|
64
|
+
@mutex.synchronize { @cache.dig(tag, :value) }
|
|
65
|
+
end
|
|
69
66
|
|
|
70
67
|
private
|
|
71
68
|
|
|
@@ -86,6 +83,8 @@ class MacawFramework::Cache
|
|
|
86
83
|
@mutex.synchronize do
|
|
87
84
|
@cache.delete_if { |_, v| v[:expires_in] < Time.now }
|
|
88
85
|
end
|
|
86
|
+
rescue StandardError => e
|
|
87
|
+
warn "Cache invalidation error: #{e.message}"
|
|
89
88
|
end
|
|
90
89
|
end
|
|
91
90
|
end
|
|
@@ -3,10 +3,8 @@
|
|
|
3
3
|
require_relative '../../middlewares/memory_invalidation_middleware'
|
|
4
4
|
require_relative '../../data_filters/response_data_filter'
|
|
5
5
|
require_relative '../../utils/supported_ssl_versions'
|
|
6
|
-
require_relative '../../aspects/prometheus_aspect'
|
|
7
6
|
require_relative '../../aspects/logging_aspect'
|
|
8
7
|
require_relative '../../aspects/cache_aspect'
|
|
9
|
-
require 'securerandom'
|
|
10
8
|
|
|
11
9
|
##
|
|
12
10
|
# Base module for Server classes. It contains
|
|
@@ -16,18 +14,16 @@ require 'securerandom'
|
|
|
16
14
|
module ServerBase
|
|
17
15
|
prepend CacheAspect
|
|
18
16
|
prepend LoggingAspect
|
|
19
|
-
prepend PrometheusAspect
|
|
20
17
|
|
|
21
18
|
private
|
|
22
19
|
|
|
23
|
-
def call_endpoint(name, client_data
|
|
20
|
+
def call_endpoint(name, client_data)
|
|
24
21
|
@macaw.send(
|
|
25
22
|
name.to_sym,
|
|
26
23
|
{
|
|
27
24
|
headers: client_data[:headers],
|
|
28
25
|
body: client_data[:body],
|
|
29
|
-
params: client_data[:params]
|
|
30
|
-
client: @session&.dig(session_id)&.dig(0)
|
|
26
|
+
params: client_data[:params]
|
|
31
27
|
}
|
|
32
28
|
)
|
|
33
29
|
end
|
|
@@ -37,27 +33,29 @@ module ServerBase
|
|
|
37
33
|
end
|
|
38
34
|
|
|
39
35
|
def handle_client(client)
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
apply_socket_timeout(client)
|
|
37
|
+
loop do
|
|
38
|
+
max_body = @macaw.max_body_size || 1_048_576
|
|
39
|
+
_path, method_name, headers, body, parameters = RequestDataFiltering.parse_request_data(client, @macaw.routes,
|
|
40
|
+
max_body)
|
|
41
|
+
raise EndpointNotMappedError unless @macaw.respond_to?(method_name)
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
client.print "HTTP/1.1 500 Internal Server Error\r\n\r\n"
|
|
60
|
-
@macaw_log&.error(e.full_message)
|
|
43
|
+
keep_alive = keep_alive_connection?(headers)
|
|
44
|
+
client.write build_response(method_name, headers, body, parameters, keep_alive)
|
|
45
|
+
break unless keep_alive
|
|
46
|
+
rescue Errno::ECONNRESET, Errno::EPIPE, IOError
|
|
47
|
+
break
|
|
48
|
+
rescue EndpointNotMappedError
|
|
49
|
+
client.print "HTTP/1.1 404 Not Found\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"
|
|
50
|
+
break
|
|
51
|
+
rescue PayloadTooLargeError
|
|
52
|
+
client.print "HTTP/1.1 413 Content Too Large\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"
|
|
53
|
+
break
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
client.print "HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"
|
|
56
|
+
@macaw_log&.error(e.full_message)
|
|
57
|
+
break
|
|
58
|
+
end
|
|
61
59
|
ensure
|
|
62
60
|
begin
|
|
63
61
|
client.close
|
|
@@ -66,11 +64,29 @@ module ServerBase
|
|
|
66
64
|
end
|
|
67
65
|
end
|
|
68
66
|
|
|
69
|
-
def
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
def build_response(method_name, headers, body, parameters, keep_alive)
|
|
68
|
+
client_data = get_client_data(body, headers, parameters)
|
|
69
|
+
message, status, response_headers = call_endpoint(@macaw_log, @cache, method_name, client_data)
|
|
70
|
+
response_headers ||= {}
|
|
71
|
+
status ||= 200
|
|
72
|
+
response_headers['Connection'] = keep_alive ? 'keep-alive' : 'close'
|
|
73
|
+
response_headers['Content-Length'] = message.to_s.bytesize
|
|
74
|
+
ResponseDataFilter.mount_response(status, response_headers, message)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def apply_socket_timeout(client)
|
|
78
|
+
timeout = @macaw.keep_alive_timeout || 30
|
|
79
|
+
client.timeout = timeout
|
|
80
|
+
rescue NoMethodError
|
|
81
|
+
io = client.respond_to?(:to_io) ? client.to_io : client
|
|
82
|
+
timeval = [timeout, 0].pack('l_2')
|
|
83
|
+
io.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, timeval)
|
|
84
|
+
rescue StandardError
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def keep_alive_connection?(headers)
|
|
89
|
+
headers['Connection']&.downcase != 'close'
|
|
74
90
|
end
|
|
75
91
|
|
|
76
92
|
def set_ssl
|
|
@@ -99,21 +115,8 @@ module ServerBase
|
|
|
99
115
|
raise e
|
|
100
116
|
end
|
|
101
117
|
|
|
102
|
-
def set_session
|
|
103
|
-
return unless @macaw.session
|
|
104
|
-
|
|
105
|
-
@session ||= {}
|
|
106
|
-
inv = if @macaw.config&.dig('macaw', 'session', 'invalidation_time')
|
|
107
|
-
MemoryInvalidationMiddleware.new(@macaw.config['macaw']['session']['invalidation_time'])
|
|
108
|
-
else
|
|
109
|
-
MemoryInvalidationMiddleware.new
|
|
110
|
-
end
|
|
111
|
-
inv.cache = @session
|
|
112
|
-
end
|
|
113
|
-
|
|
114
118
|
def set_features
|
|
115
119
|
@is_shutting_down = false
|
|
116
|
-
set_session
|
|
117
120
|
set_ssl
|
|
118
121
|
end
|
|
119
122
|
end
|
|
@@ -13,19 +13,13 @@ class ThreadServer
|
|
|
13
13
|
|
|
14
14
|
attr_reader :context
|
|
15
15
|
|
|
16
|
-
# rubocop:disable Metrics/ParameterLists
|
|
17
|
-
|
|
18
16
|
##
|
|
19
17
|
# Create a new instance of ThreadServer.
|
|
20
18
|
# @param {Macaw} macaw
|
|
21
|
-
# @param {
|
|
22
|
-
# @param {Integer} port
|
|
23
|
-
# @param {String} bind
|
|
24
|
-
# @param {Integer} num_threads
|
|
19
|
+
# @param {Array} endpoints_to_cache
|
|
25
20
|
# @param {MemoryInvalidationMiddleware} cache
|
|
26
|
-
# @param {Prometheus::Client:Registry} prometheus
|
|
27
21
|
# @return {Server}
|
|
28
|
-
def initialize(macaw, endpoints_to_cache = nil, cache = nil
|
|
22
|
+
def initialize(macaw, endpoints_to_cache = nil, cache = nil)
|
|
29
23
|
@port = macaw.port
|
|
30
24
|
@bind = macaw.bind
|
|
31
25
|
@macaw = macaw
|
|
@@ -38,13 +32,9 @@ class ThreadServer
|
|
|
38
32
|
endpoints_to_cache: endpoints_to_cache || [],
|
|
39
33
|
cached_methods: macaw.cached_methods
|
|
40
34
|
}
|
|
41
|
-
@prometheus = prometheus
|
|
42
|
-
@prometheus_middleware = prometheus_mw
|
|
43
35
|
@workers = []
|
|
44
36
|
end
|
|
45
37
|
|
|
46
|
-
# rubocop:enable Metrics/ParameterLists
|
|
47
|
-
|
|
48
38
|
##
|
|
49
39
|
# Start running the webserver.
|
|
50
40
|
def run
|
|
@@ -81,40 +71,40 @@ class ThreadServer
|
|
|
81
71
|
sleep 0.1
|
|
82
72
|
end
|
|
83
73
|
|
|
84
|
-
@
|
|
74
|
+
worker_count = @workers_mutex.synchronize { @workers.size }
|
|
75
|
+
worker_count.times { @work_queue << :shutdown }
|
|
85
76
|
@workers.each(&:join)
|
|
86
|
-
@server
|
|
77
|
+
@server&.close
|
|
87
78
|
end
|
|
88
79
|
|
|
89
80
|
private
|
|
90
81
|
|
|
91
82
|
def spawn_worker
|
|
92
|
-
@workers_mutex.synchronize
|
|
93
|
-
|
|
94
|
-
loop do
|
|
95
|
-
client = @work_queue.pop
|
|
96
|
-
break if client == :shutdown
|
|
83
|
+
@workers_mutex.synchronize { create_worker }
|
|
84
|
+
end
|
|
97
85
|
|
|
98
|
-
|
|
99
|
-
|
|
86
|
+
def create_worker
|
|
87
|
+
t = Thread.new do
|
|
88
|
+
loop do
|
|
89
|
+
client = @work_queue.pop
|
|
90
|
+
break if client == :shutdown
|
|
91
|
+
|
|
92
|
+
handle_client(client)
|
|
100
93
|
end
|
|
101
|
-
@workers << t
|
|
102
|
-
t
|
|
103
94
|
end
|
|
95
|
+
@workers << t
|
|
96
|
+
t
|
|
104
97
|
end
|
|
105
98
|
|
|
106
99
|
def maintain_worker_pool
|
|
107
100
|
@workers_mutex.synchronize do
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
spawn_worker
|
|
116
|
-
end
|
|
117
|
-
end
|
|
101
|
+
return if @is_shutting_down
|
|
102
|
+
|
|
103
|
+
dead_count = @workers.count { |w| !w.alive? }
|
|
104
|
+
@workers.select!(&:alive?)
|
|
105
|
+
dead_count.times do
|
|
106
|
+
@macaw_log&.error('Worker thread died, respawning...')
|
|
107
|
+
create_worker
|
|
118
108
|
end
|
|
119
109
|
end
|
|
120
110
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'cgi/escape'
|
|
3
4
|
require_relative '../errors/endpoint_not_mapped_error'
|
|
5
|
+
require_relative '../errors/payload_too_large_error'
|
|
4
6
|
|
|
5
7
|
##
|
|
6
8
|
# Module containing methods to filter Strings
|
|
@@ -10,14 +12,17 @@ module RequestDataFiltering
|
|
|
10
12
|
##
|
|
11
13
|
# Method responsible for extracting information
|
|
12
14
|
# provided by the client like Headers and Body
|
|
13
|
-
def self.parse_request_data(client, routes)
|
|
14
|
-
|
|
15
|
+
def self.parse_request_data(client, routes, max_body_size = 1_048_576)
|
|
16
|
+
first_line = client.gets
|
|
17
|
+
raise EOFError if first_line.nil?
|
|
18
|
+
|
|
19
|
+
path, parameters = extract_url_parameters(first_line.gsub(%r{\s*HTTP/\d+(?:\.\d+)?\s*$}i, ''))
|
|
15
20
|
parameters = {} if parameters.nil?
|
|
16
21
|
|
|
17
22
|
method_name = sanitize_method_name(path)
|
|
18
23
|
method_name = select_path(method_name, routes, parameters)
|
|
19
24
|
body_first_line, headers = extract_headers(client)
|
|
20
|
-
body = extract_body(client, body_first_line, headers['Content-Length'].to_i)
|
|
25
|
+
body = extract_body(client, body_first_line, headers['Content-Length'].to_i, max_body_size)
|
|
21
26
|
[path, method_name, headers, body, parameters]
|
|
22
27
|
end
|
|
23
28
|
|
|
@@ -74,8 +79,8 @@ module RequestDataFiltering
|
|
|
74
79
|
def self.extract_headers(client)
|
|
75
80
|
header = client.gets&.delete("\n")&.delete("\r")
|
|
76
81
|
headers = {}
|
|
77
|
-
while header&.match(
|
|
78
|
-
split_header = header.split(':')
|
|
82
|
+
while header&.match(/\A[^:]+:\s*.+/)
|
|
83
|
+
split_header = header.split(':', 2)
|
|
79
84
|
headers[split_header[0].strip] = split_header[1].strip
|
|
80
85
|
header = client.gets&.delete("\n")&.delete("\r")
|
|
81
86
|
end
|
|
@@ -84,7 +89,9 @@ module RequestDataFiltering
|
|
|
84
89
|
|
|
85
90
|
##
|
|
86
91
|
# Method responsible for extracting the body from request
|
|
87
|
-
def self.extract_body(client, body_first_line, content_length)
|
|
92
|
+
def self.extract_body(client, body_first_line, content_length, max_body_size = 1_048_576)
|
|
93
|
+
raise PayloadTooLargeError if content_length > max_body_size
|
|
94
|
+
|
|
88
95
|
body = client&.read(content_length)
|
|
89
96
|
body_first_line << body.to_s
|
|
90
97
|
end
|
|
@@ -113,9 +120,13 @@ module RequestDataFiltering
|
|
|
113
120
|
end
|
|
114
121
|
|
|
115
122
|
##
|
|
116
|
-
# Method responsible for sanitizing the parameter value
|
|
123
|
+
# Method responsible for sanitizing the parameter value.
|
|
124
|
+
# URL-decodes percent-encoded characters (e.g. %40 → @, %20 → space, + → space).
|
|
117
125
|
def self.sanitize_parameter_value(value)
|
|
118
|
-
value
|
|
119
|
-
|
|
126
|
+
return nil if value.nil?
|
|
127
|
+
|
|
128
|
+
CGI.unescape(value)
|
|
129
|
+
rescue ArgumentError
|
|
130
|
+
value
|
|
120
131
|
end
|
|
121
132
|
end
|
|
@@ -12,20 +12,21 @@ module ResponseDataFilter
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def self.mount_first_response_line(status, headers)
|
|
15
|
-
|
|
16
|
-
separator = "
|
|
17
|
-
|
|
18
|
-
"HTTP/1.1 #{status} #{HTTP_STATUS_CODE_MAP[status]}#{separator}"
|
|
15
|
+
reason = HTTP_STATUS_CODE_MAP[status] || 'Unknown'
|
|
16
|
+
separator = headers.nil? ? "\r\n\r\n" : "\r\n"
|
|
17
|
+
"HTTP/1.1 #{status} #{reason}#{separator}"
|
|
19
18
|
end
|
|
20
19
|
|
|
21
20
|
def self.mount_response_headers(headers)
|
|
22
21
|
return '' if headers.nil?
|
|
23
22
|
|
|
24
|
-
response = ''
|
|
23
|
+
response = +''
|
|
25
24
|
headers.each do |key, value|
|
|
26
|
-
|
|
25
|
+
safe_key = key.to_s.gsub(/[\r\n]/, '')
|
|
26
|
+
safe_value = value.to_s.gsub(/[\r\n]/, '')
|
|
27
|
+
response << "#{safe_key}: #{safe_value}\r\n"
|
|
27
28
|
end
|
|
28
|
-
response
|
|
29
|
+
response << "\r\n"
|
|
29
30
|
response
|
|
30
31
|
end
|
|
31
32
|
end
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'errors/endpoint_not_mapped_error'
|
|
4
|
-
require_relative '
|
|
4
|
+
require_relative 'errors/payload_too_large_error'
|
|
5
5
|
require_relative 'data_filters/request_data_filtering'
|
|
6
6
|
require_relative 'middlewares/memory_invalidation_middleware'
|
|
7
|
-
require_relative 'core/cron_runner'
|
|
8
7
|
require_relative 'core/thread_server'
|
|
9
8
|
require_relative 'version'
|
|
10
|
-
require 'prometheus/client'
|
|
11
|
-
require 'securerandom'
|
|
12
9
|
require 'singleton'
|
|
13
10
|
require 'pathname'
|
|
14
11
|
require 'logger'
|
|
@@ -23,7 +20,7 @@ module MacawFramework; end
|
|
|
23
20
|
# Class responsible for creating endpoints and
|
|
24
21
|
# starting the web server.
|
|
25
22
|
class MacawFramework::Macaw
|
|
26
|
-
attr_reader :routes, :macaw_log, :config, :
|
|
23
|
+
attr_reader :routes, :macaw_log, :config, :cached_methods, :keep_alive_timeout, :max_body_size
|
|
27
24
|
attr_accessor :port, :bind, :threads
|
|
28
25
|
|
|
29
26
|
##
|
|
@@ -119,26 +116,6 @@ class MacawFramework::Macaw
|
|
|
119
116
|
map_new_endpoint('delete', cache, path, &block)
|
|
120
117
|
end
|
|
121
118
|
|
|
122
|
-
##
|
|
123
|
-
# Spawn and start a thread running the defined periodic job.
|
|
124
|
-
# @param {Integer} interval
|
|
125
|
-
# @param {Integer?} start_delay
|
|
126
|
-
# @param {String} job_name
|
|
127
|
-
# @param {Proc} block
|
|
128
|
-
# @example
|
|
129
|
-
#
|
|
130
|
-
# macaw = MacawFramework::Macaw.new
|
|
131
|
-
# macaw.setup_job(interval: 60, start_delay: 60, job_name: "job 1") do
|
|
132
|
-
# puts "I'm a periodic job that runs every minute"
|
|
133
|
-
# end
|
|
134
|
-
##
|
|
135
|
-
def setup_job(interval: 60, start_delay: 0, job_name: "job_#{SecureRandom.uuid}", &block)
|
|
136
|
-
@cron_runner ||= CronRunner.new(self)
|
|
137
|
-
@jobs ||= []
|
|
138
|
-
@cron_runner.start_cron_job_thread(interval, start_delay, job_name, &block)
|
|
139
|
-
@jobs << job_name
|
|
140
|
-
end
|
|
141
|
-
|
|
142
119
|
##
|
|
143
120
|
# Starts the web server
|
|
144
121
|
def start!
|
|
@@ -153,7 +130,8 @@ class MacawFramework::Macaw
|
|
|
153
130
|
@macaw_log.info("Number of threads: #{@threads}")
|
|
154
131
|
@macaw_log.info('---------------------------------')
|
|
155
132
|
end
|
|
156
|
-
@server = @server_class.new(self, @endpoints_to_cache, @cache
|
|
133
|
+
@server = @server_class.new(self, @endpoints_to_cache, @cache)
|
|
134
|
+
Signal.trap('TERM') { raise Interrupt }
|
|
157
135
|
server_loop(@server)
|
|
158
136
|
rescue Interrupt
|
|
159
137
|
if @macaw_log.nil?
|
|
@@ -167,35 +145,19 @@ class MacawFramework::Macaw
|
|
|
167
145
|
end
|
|
168
146
|
end
|
|
169
147
|
|
|
170
|
-
##
|
|
171
|
-
# This method is intended to start the framework
|
|
172
|
-
# without an web server. This can be useful when
|
|
173
|
-
# you just want to keep cron jobs running, without
|
|
174
|
-
# mapping any HTTP endpoints.
|
|
175
|
-
def start_without_server!
|
|
176
|
-
@macaw_log.nil? ? puts('Application starting') : @macaw_log.info('Application starting')
|
|
177
|
-
loop { sleep(3600) }
|
|
178
|
-
rescue Interrupt
|
|
179
|
-
@macaw_log.nil? ? puts('Macaw stop flying for some seeds.') : @macaw_log.info('Macaw stop flying for some seeds.')
|
|
180
|
-
end
|
|
181
|
-
|
|
182
148
|
private
|
|
183
149
|
|
|
184
150
|
def setup_default_configs
|
|
185
151
|
@port ||= 8080
|
|
186
|
-
@bind ||= '
|
|
152
|
+
@bind ||= '0.0.0.0'
|
|
187
153
|
@config ||= nil
|
|
188
154
|
@threads ||= 200
|
|
189
155
|
@endpoints_to_cache = []
|
|
190
|
-
@prometheus ||= nil
|
|
191
|
-
@prometheus_middleware ||= nil
|
|
192
156
|
end
|
|
193
157
|
|
|
194
158
|
def apply_options(custom_log)
|
|
195
159
|
setup_basic_config(custom_log)
|
|
196
|
-
setup_session
|
|
197
160
|
setup_cache
|
|
198
|
-
setup_prometheus
|
|
199
161
|
rescue StandardError => e
|
|
200
162
|
@macaw_log&.warn(e.message)
|
|
201
163
|
end
|
|
@@ -203,33 +165,30 @@ class MacawFramework::Macaw
|
|
|
203
165
|
def setup_cache
|
|
204
166
|
return if @config['macaw']['cache'].nil?
|
|
205
167
|
|
|
206
|
-
@cache = MemoryInvalidationMiddleware.new(@config
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
def setup_session
|
|
210
|
-
@session = false
|
|
211
|
-
return if @config['macaw']['session'].nil?
|
|
212
|
-
|
|
213
|
-
@session = true
|
|
214
|
-
@secure_header = @config['macaw']['session']['secure_header'] || 'X-Session-ID'
|
|
168
|
+
@cache = MemoryInvalidationMiddleware.new(@config.dig('macaw', 'cache', 'cache_invalidation')&.to_i || 3_600)
|
|
215
169
|
end
|
|
216
170
|
|
|
217
171
|
def setup_basic_config(custom_log)
|
|
218
172
|
@routes = []
|
|
219
173
|
@cached_methods = {}
|
|
220
174
|
@macaw_log ||= custom_log
|
|
221
|
-
|
|
175
|
+
config_file = ENV.fetch('MACAW_CONFIG', 'application.json')
|
|
176
|
+
@config = JSON.parse(File.read(config_file))
|
|
222
177
|
@port = @config['macaw']['port'] || 8080
|
|
223
|
-
@bind = @config['macaw']['bind'] || '
|
|
178
|
+
@bind = @config['macaw']['bind'] || '0.0.0.0'
|
|
224
179
|
@threads = @config['macaw']['threads'] || 200
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
@
|
|
231
|
-
@
|
|
232
|
-
|
|
180
|
+
@keep_alive_timeout = @config['macaw']['keep_alive_timeout'] || 30
|
|
181
|
+
@max_body_size = @config.dig('macaw', 'max_body_size')&.to_i || 1_048_576
|
|
182
|
+
rescue Errno::ENOENT
|
|
183
|
+
@macaw_log&.warn("Config file '#{config_file}' not found, using default settings.")
|
|
184
|
+
@config = { 'macaw' => {} }
|
|
185
|
+
@keep_alive_timeout = 30
|
|
186
|
+
@max_body_size = 1_048_576
|
|
187
|
+
rescue JSON::ParserError => e
|
|
188
|
+
@macaw_log&.warn("Config file '#{config_file}' is not valid JSON: #{e.message}. Using default settings.")
|
|
189
|
+
@config = { 'macaw' => {} }
|
|
190
|
+
@keep_alive_timeout = 30
|
|
191
|
+
@max_body_size = 1_048_576
|
|
233
192
|
end
|
|
234
193
|
|
|
235
194
|
def server_loop(server)
|
|
@@ -4,8 +4,6 @@ require 'openssl'
|
|
|
4
4
|
|
|
5
5
|
module SupportedSSLVersions
|
|
6
6
|
VERSIONS = {
|
|
7
|
-
'SSL2' => OpenSSL::SSL::SSL2_VERSION,
|
|
8
|
-
'SSL3' => OpenSSL::SSL::SSL3_VERSION,
|
|
9
7
|
'TLS1.1' => OpenSSL::SSL::TLS1_1_VERSION,
|
|
10
8
|
'TLS1.2' => OpenSSL::SSL::TLS1_2_VERSION,
|
|
11
9
|
'TLS1.3' => OpenSSL::SSL::TLS1_3_VERSION
|
data/lib/macaw_framework.rb
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Load the C extension when available (built via `rake compile` or `gem install`).
|
|
4
|
+
# Falls back transparently to pure-Ruby mode if the extension has not been compiled.
|
|
5
|
+
begin
|
|
6
|
+
require 'macaw_framework_ext'
|
|
7
|
+
rescue LoadError
|
|
8
|
+
nil
|
|
9
|
+
end
|
|
10
|
+
|
|
3
11
|
##
|
|
4
12
|
# Main module for all Macaw classes
|
|
5
13
|
module MacawFramework; end
|
metadata
CHANGED
|
@@ -1,35 +1,22 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: macaw_framework
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Aria Diniz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
12
|
-
- !ruby/object:Gem::Dependency
|
|
13
|
-
name: prometheus-client
|
|
14
|
-
requirement: !ruby/object:Gem::Requirement
|
|
15
|
-
requirements:
|
|
16
|
-
- - "~>"
|
|
17
|
-
- !ruby/object:Gem::Version
|
|
18
|
-
version: '4.1'
|
|
19
|
-
type: :runtime
|
|
20
|
-
prerelease: false
|
|
21
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
-
requirements:
|
|
23
|
-
- - "~>"
|
|
24
|
-
- !ruby/object:Gem::Version
|
|
25
|
-
version: '4.1'
|
|
11
|
+
dependencies: []
|
|
26
12
|
description: |-
|
|
27
13
|
A lightweight web framework designed for building efficient backend applications. Initially
|
|
28
14
|
created for study purposes, now production-ready and open for contributions.
|
|
29
15
|
email:
|
|
30
16
|
- aria.diniz.dev@gmail.com
|
|
31
17
|
executables: []
|
|
32
|
-
extensions:
|
|
18
|
+
extensions:
|
|
19
|
+
- ext/macaw_framework_ext/extconf.rb
|
|
33
20
|
extra_rdoc_files: []
|
|
34
21
|
files:
|
|
35
22
|
- ".rubocop.yml"
|
|
@@ -41,21 +28,22 @@ files:
|
|
|
41
28
|
- README.md
|
|
42
29
|
- Rakefile
|
|
43
30
|
- SECURITY.md
|
|
31
|
+
- ext/macaw_framework_ext/extconf.rb
|
|
32
|
+
- ext/macaw_framework_ext/macaw_framework_ext.c
|
|
33
|
+
- ext/macaw_framework_ext/macaw_framework_ext.h
|
|
44
34
|
- lib/macaw_framework.rb
|
|
45
35
|
- lib/macaw_framework/aspects/cache_aspect.rb
|
|
46
36
|
- lib/macaw_framework/aspects/logging_aspect.rb
|
|
47
|
-
- lib/macaw_framework/aspects/prometheus_aspect.rb
|
|
48
37
|
- lib/macaw_framework/cache.rb
|
|
49
38
|
- lib/macaw_framework/core/common/server_base.rb
|
|
50
|
-
- lib/macaw_framework/core/cron_runner.rb
|
|
51
39
|
- lib/macaw_framework/core/thread_server.rb
|
|
52
40
|
- lib/macaw_framework/data_filters/log_data_filter.rb
|
|
53
41
|
- lib/macaw_framework/data_filters/request_data_filtering.rb
|
|
54
42
|
- lib/macaw_framework/data_filters/response_data_filter.rb
|
|
55
43
|
- lib/macaw_framework/errors/endpoint_not_mapped_error.rb
|
|
44
|
+
- lib/macaw_framework/errors/payload_too_large_error.rb
|
|
56
45
|
- lib/macaw_framework/macaw.rb
|
|
57
46
|
- lib/macaw_framework/middlewares/memory_invalidation_middleware.rb
|
|
58
|
-
- lib/macaw_framework/middlewares/prometheus_middleware.rb
|
|
59
47
|
- lib/macaw_framework/utils/http_status_code.rb
|
|
60
48
|
- lib/macaw_framework/utils/supported_ssl_versions.rb
|
|
61
49
|
- lib/macaw_framework/version.rb
|
|
@@ -75,6 +63,7 @@ metadata:
|
|
|
75
63
|
documentation_uri: https://rubydoc.info/gems/macaw_framework
|
|
76
64
|
homepage_uri: https://github.com/ariasdiniz/macaw_framework
|
|
77
65
|
source_code_uri: https://github.com/ariasdiniz/macaw_framework
|
|
66
|
+
changelog_uri: https://github.com/ariasdiniz/macaw_framework/blob/main/CHANGELOG.md
|
|
78
67
|
rdoc_options: []
|
|
79
68
|
require_paths:
|
|
80
69
|
- lib
|
|
@@ -82,7 +71,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
82
71
|
requirements:
|
|
83
72
|
- - ">="
|
|
84
73
|
- !ruby/object:Gem::Version
|
|
85
|
-
version: 3.
|
|
74
|
+
version: 3.2.0
|
|
86
75
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
87
76
|
requirements:
|
|
88
77
|
- - ">="
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
##
|
|
4
|
-
# Aspect that provides application metrics using prometheus.
|
|
5
|
-
module PrometheusAspect
|
|
6
|
-
def call_endpoint(prometheus_middleware, *args, **kwargs)
|
|
7
|
-
return super(*args, **kwargs) if prometheus_middleware.nil?
|
|
8
|
-
|
|
9
|
-
start_time = Time.now
|
|
10
|
-
|
|
11
|
-
begin
|
|
12
|
-
response = super(*args)
|
|
13
|
-
ensure
|
|
14
|
-
duration = (Time.now - start_time) * 1_000
|
|
15
|
-
|
|
16
|
-
endpoint_name = args[2].split('.').join('/')
|
|
17
|
-
|
|
18
|
-
prometheus_middleware.request_duration_milliseconds.with_labels(endpoint: endpoint_name).observe(duration)
|
|
19
|
-
prometheus_middleware.request_count.with_labels(endpoint: endpoint_name).increment
|
|
20
|
-
if response
|
|
21
|
-
prometheus_middleware.response_count.with_labels(endpoint: endpoint_name,
|
|
22
|
-
status: response[1]).increment
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
response
|
|
27
|
-
end
|
|
28
|
-
end
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
##
|
|
4
|
-
# This module is responsible to set up a new thread
|
|
5
|
-
# for each cron job defined
|
|
6
|
-
class CronRunner
|
|
7
|
-
def initialize(macaw)
|
|
8
|
-
@logger = macaw.macaw_log
|
|
9
|
-
@macaw = macaw
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
##
|
|
13
|
-
# Will start a thread for the defined cron job
|
|
14
|
-
# @param {Integer} interval
|
|
15
|
-
# @param {Integer?} start_delay
|
|
16
|
-
# @param {String} job_name
|
|
17
|
-
# @param {Proc} block
|
|
18
|
-
def start_cron_job_thread(interval, start_delay, job_name, &block)
|
|
19
|
-
start_delay ||= 0
|
|
20
|
-
raise "interval can't be <= 0 and start_delay can't be < 0!" if interval <= 0 || start_delay.negative?
|
|
21
|
-
|
|
22
|
-
@logger&.info("Starting thread for job #{job_name}")
|
|
23
|
-
start_delay ||= 0
|
|
24
|
-
thread = Thread.new do
|
|
25
|
-
name = job_name
|
|
26
|
-
interval_thread = interval
|
|
27
|
-
unless start_delay.nil?
|
|
28
|
-
@logger&.info("Job #{name} scheduled with delay. Will start running in #{start_delay} seconds.")
|
|
29
|
-
sleep(start_delay)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
loop do
|
|
33
|
-
start_time = Time.now
|
|
34
|
-
@logger&.info("Running job #{name}")
|
|
35
|
-
block.call
|
|
36
|
-
@logger&.info("Job #{name} executed with success. New execution in #{interval_thread} seconds.")
|
|
37
|
-
|
|
38
|
-
execution_time = Time.now - start_time
|
|
39
|
-
sleep_time = [interval_thread - execution_time, 0].max
|
|
40
|
-
sleep(sleep_time)
|
|
41
|
-
rescue StandardError => e
|
|
42
|
-
@logger&.error("Error executing cron job with name #{name}: #{e.message}")
|
|
43
|
-
sleep(interval)
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
sleep(1)
|
|
47
|
-
@logger&.info("Thread for job #{job_name} started")
|
|
48
|
-
thread
|
|
49
|
-
end
|
|
50
|
-
end
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'prometheus/client'
|
|
4
|
-
require 'prometheus/client/formats/text'
|
|
5
|
-
|
|
6
|
-
##
|
|
7
|
-
# Middleware responsible to configure prometheus
|
|
8
|
-
# defining metrics and an endpoint to access them.
|
|
9
|
-
class PrometheusMiddleware
|
|
10
|
-
attr_accessor :request_duration_milliseconds, :request_count, :response_count
|
|
11
|
-
|
|
12
|
-
def configure_prometheus(prometheus_registry, configurations, macaw)
|
|
13
|
-
return nil unless prometheus_registry
|
|
14
|
-
|
|
15
|
-
@request_duration_milliseconds = Prometheus::Client::Histogram.new(
|
|
16
|
-
:request_duration_milliseconds,
|
|
17
|
-
docstring: 'The duration of each request in milliseconds',
|
|
18
|
-
labels: [:endpoint],
|
|
19
|
-
buckets: (100..1000).step(100).to_a + (2000..10_000).step(1000).to_a
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
@request_count = Prometheus::Client::Counter.new(
|
|
23
|
-
:request_count,
|
|
24
|
-
docstring: 'The total number of requests received',
|
|
25
|
-
labels: [:endpoint]
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
@response_count = Prometheus::Client::Counter.new(
|
|
29
|
-
:response_count,
|
|
30
|
-
docstring: 'The total number of responses sent',
|
|
31
|
-
labels: %i[endpoint status]
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
prometheus_registry.register(@request_duration_milliseconds)
|
|
35
|
-
prometheus_registry.register(@request_count)
|
|
36
|
-
prometheus_registry.register(@response_count)
|
|
37
|
-
prometheus_endpoint(prometheus_registry, configurations, macaw)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
private
|
|
41
|
-
|
|
42
|
-
def prometheus_endpoint(prometheus_registry, configurations, macaw)
|
|
43
|
-
endpoint = configurations['macaw']['prometheus']['endpoint'] || '/metrics'
|
|
44
|
-
macaw.get(endpoint) do |_context|
|
|
45
|
-
[Prometheus::Client::Formats::Text.marshal(prometheus_registry), 200, { 'Content-Type' => 'plaintext' }]
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|