macaw_framework 0.2.0 → 1.0.1
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 +3 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile +6 -2
- data/README.md +98 -33
- data/SECURITY.md +13 -0
- data/lib/macaw_framework/aspects/cache_aspect.rb +18 -6
- data/lib/macaw_framework/aspects/logging_aspect.rb +2 -2
- data/lib/macaw_framework/aspects/prometheus_aspect.rb +1 -1
- data/lib/macaw_framework/core/server.rb +96 -17
- data/lib/macaw_framework/data_filters/response_data_filter.rb +31 -0
- data/lib/macaw_framework/errors/too_many_requests_error.rb +4 -0
- data/lib/macaw_framework/middlewares/{caching_middleware.rb → memory_invalidation_middleware.rb} +7 -4
- data/lib/macaw_framework/middlewares/prometheus_middleware.rb +1 -1
- data/lib/macaw_framework/middlewares/rate_limiter_middleware.rb +31 -0
- data/lib/macaw_framework/version.rb +1 -1
- data/lib/macaw_framework.rb +14 -14
- data/sig/macaw_framework/macaw.rbs +9 -2
- data/sig/{caching_middleware.rbs → memory_invalidation_middleware.rbs} +1 -1
- data/sig/server.rbs +3 -2
- metadata +14 -8
- /data/lib/macaw_framework/{middlewares → data_filters}/request_data_filtering.rb +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 03ac2a8df24de8757381abc1e9a85f7854ac71a49b44d51aa7a23f1d42f95380
|
4
|
+
data.tar.gz: 4431ce9ab63887660aaa5cc464c00a1dd3a81b08cf4f99bb53926d0aec440fc2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7f981191cafd8504428de742979979a82ce1609c69ab08afc9f0b8e131381d12cc298b9b7447679b1611719605b825fcf409c9b4afee2b0d0f3a4fa8f28b5c79
|
7
|
+
data.tar.gz: cda629f9c4c779d11712c18dd5f9b7c8c90805688630d75c14b7fcd7c485b3cdcb2ea2386b5f6bcde62296a7b283337ee45418a08b67e61686f23ef18bf56bb2
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -34,3 +34,16 @@
|
|
34
34
|
- Adding middleware for integration with Prometheus to collect metrics
|
35
35
|
- Adding a simple caching mechanism that can be enabled separately for each endpoint
|
36
36
|
- Performance and functional optimizations
|
37
|
+
|
38
|
+
## [1.0.0] - 2023-04-28
|
39
|
+
|
40
|
+
- Adding support to HTTPS/SSL using security certificates
|
41
|
+
- Implemented a middleware for rate limiting to prevent DoS attacks
|
42
|
+
- Improvement of caching strategy to ignore optional headers
|
43
|
+
- First production-ready version
|
44
|
+
|
45
|
+
## [1.0.1] - 2023-05-03
|
46
|
+
|
47
|
+
- Introducing server-side session management
|
48
|
+
- Fixing a bug with cache
|
49
|
+
- Improving README
|
data/Gemfile
CHANGED
@@ -11,6 +11,10 @@ gem "minitest", "~> 5.0"
|
|
11
11
|
|
12
12
|
gem "rubocop", "~> 1.21"
|
13
13
|
|
14
|
-
gem "simplecov", "~> 0.22.0"
|
15
|
-
|
16
14
|
gem "prometheus-client", "~> 4.1"
|
15
|
+
|
16
|
+
group :test do
|
17
|
+
gem "simplecov", "~> 0.21.2"
|
18
|
+
gem "simplecov-json"
|
19
|
+
gem "simplecov_json_formatter", "~> 0.1.2"
|
20
|
+
end
|
data/README.md
CHANGED
@@ -1,10 +1,17 @@
|
|
1
1
|
# MacawFramework
|
2
2
|
|
3
|
-
|
3
|
+
MacawFramework is a lightweight, easy-to-use web framework for Ruby designed to simplify the development of small to
|
4
|
+
medium-sized web applications. With support for various HTTP methods, caching, and session management, MacawFramework
|
5
|
+
provides developers with the essential tools to quickly build and deploy their applications.
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
-
|
7
|
+
## Features
|
8
|
+
|
9
|
+
- Simple routing with support for GET, POST, PUT, PATCH, and DELETE HTTP methods
|
10
|
+
- Caching middleware for improved performance
|
11
|
+
- Session management with server-side in-memory storage
|
12
|
+
- Basic rate limiting and SSL support
|
13
|
+
- Prometheus integration for monitoring and metrics
|
14
|
+
- Lightweight and easy to learn
|
8
15
|
|
9
16
|
## Installation
|
10
17
|
|
@@ -18,10 +25,56 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
18
25
|
|
19
26
|
## Usage
|
20
27
|
|
21
|
-
|
28
|
+
### Basic routing: Define routes with support for GET, POST, PUT, PATCH, and DELETE HTTP methods
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
require 'macaw_framework'
|
32
|
+
|
33
|
+
m = MacawFramework::Macaw.new
|
34
|
+
|
35
|
+
m.get('/hello_world') do |_context|
|
36
|
+
return "Hello, World!", 200, {"Content-Type" => "text/plain"}
|
37
|
+
end
|
38
|
+
|
39
|
+
m.post('/submit_data/:path_variable') do |context|
|
40
|
+
context[:body] # Client body data
|
41
|
+
context[:params] # Client params, like url parameters or variables
|
42
|
+
context[:headers] # Client headers
|
43
|
+
context[:params][:path_variable] # The defined path variable can be found in :params
|
44
|
+
context[:client] # Client session
|
45
|
+
end
|
46
|
+
|
47
|
+
m.start!
|
48
|
+
|
49
|
+
```
|
50
|
+
|
51
|
+
### Caching: Improve performance by caching responses and configuring cache invalidation
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
m.get('/cached_data', cache: true) do |context|
|
55
|
+
# Retrieve data
|
56
|
+
end
|
57
|
+
```
|
22
58
|
|
23
|
-
|
24
|
-
|
59
|
+
### Session management: Handle user sessions securely with server-side in-memory storage
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
m.get('/login') do |context|
|
63
|
+
# Authenticate user
|
64
|
+
context[:session][:user_id] = user_id
|
65
|
+
end
|
66
|
+
|
67
|
+
m.get('/dashboard') do |context|
|
68
|
+
# Check if the user is logged in
|
69
|
+
if context[:session][:user_id]
|
70
|
+
# Show dashboard
|
71
|
+
else
|
72
|
+
# Redirect to login
|
73
|
+
end
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
### Configuration: Customize various aspects of the framework through the application.json configuration file, such as rate limiting, SSL support, and Prometheus integration
|
25
78
|
|
26
79
|
```json
|
27
80
|
{
|
@@ -30,49 +83,61 @@ in the same directory of the script that will start the application with the fol
|
|
30
83
|
"bind": "localhost",
|
31
84
|
"threads": 10,
|
32
85
|
"cache": {
|
33
|
-
"cache_invalidation": 3600
|
86
|
+
"cache_invalidation": 3600,
|
87
|
+
"ignore_headers": [
|
88
|
+
"header-to-be-ignored-from-caching-strategy",
|
89
|
+
"another-header-to-be-ignored-from-caching-strategy"
|
90
|
+
]
|
91
|
+
},
|
92
|
+
"prometheus": {
|
93
|
+
"endpoint": "/metrics"
|
94
|
+
},
|
95
|
+
"rate_limiting": {
|
96
|
+
"window": 10,
|
97
|
+
"max_requests": 3
|
98
|
+
},
|
99
|
+
"ssl": {
|
100
|
+
"cert_file_name": "path/to/cert/file/file.crt",
|
101
|
+
"key_file_name": "path/to/cert/key/file.key"
|
34
102
|
}
|
35
103
|
}
|
36
104
|
}
|
37
105
|
```
|
38
106
|
|
39
|
-
|
40
|
-
should exist in the app main directory and it need the `cache_invalidation` config set.
|
107
|
+
### Monitoring: Easily monitor your application performance and metrics with built-in Prometheus support
|
41
108
|
|
42
|
-
|
109
|
+
```shell
|
110
|
+
curl http://localhost:8080/metrics
|
111
|
+
```
|
43
112
|
|
44
|
-
|
45
|
-
require 'macaw_framework'
|
46
|
-
require 'json'
|
113
|
+
### Tips
|
47
114
|
|
48
|
-
|
115
|
+
Cache invalidation time should be specified in seconds. In order to enable caching, The application.json file
|
116
|
+
should exist in the app main directory and it need the `cache_invalidation` config set. It is possible to
|
117
|
+
provide a list of strings in the property `ignore_headers`. All the client headers with the same name of any
|
118
|
+
of the strings provided will be ignored from caching strategy. This is useful to exclude headers like
|
119
|
+
correlation IDs from the caching strategy.
|
49
120
|
|
50
|
-
|
51
|
-
context[:body] # Returns the request body as string
|
52
|
-
context[:params] # Returns query parameters and path variables as a hash
|
53
|
-
context[:headers] # Returns headers as a hash
|
54
|
-
return JSON.pretty_generate({ hello_message: 'Hello World!' }), 200
|
55
|
-
end
|
121
|
+
URL parameters like `...endOfUrl?key1=value1&key2=value2` can be find in the `context[:params]`
|
56
122
|
|
57
|
-
|
58
|
-
|
59
|
-
context[:params] #
|
60
|
-
context[:headers] # Returns headers as a hash
|
61
|
-
context[:params][:path_variable] # The defined path variable can be found in :params
|
62
|
-
return JSON.pretty_generate({ hello_message: 'Hello World!' }), 200
|
123
|
+
```ruby
|
124
|
+
m.get('/test_params') do |context|
|
125
|
+
context[:params]["key1"] # returns: value1
|
63
126
|
end
|
64
|
-
|
65
|
-
m.start!
|
66
127
|
```
|
67
128
|
|
68
|
-
|
129
|
+
Rate Limit window should also be specified in seconds. Rate limit will be activated only if the `rate_limiting` config
|
130
|
+
exists inside `application.json`.
|
131
|
+
|
132
|
+
If the SSL configuration is provided in the `application.json` file with valid certificate and key files, the TCP server
|
133
|
+
will be wrapped with HTTPS security using the provided certificate.
|
69
134
|
|
70
135
|
If prometheus is enabled, a get endpoint will be defined at path `/metrics` to collect prometheus metrics. This path
|
71
136
|
is configurable via the `application.json` file.
|
72
137
|
|
73
|
-
The verb methods must always return a string or nil (used as the response)
|
74
|
-
code to be returned to the client. If an endpoint doesn't return a value or
|
75
|
-
code, a default 200 OK status will be sent as the response.
|
138
|
+
The verb methods must always return a string or nil (used as the response), a number corresponding to the HTTP status
|
139
|
+
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
|
140
|
+
returns nil for body, status code and headers, a default 200 OK status will be sent as the response.
|
76
141
|
|
77
142
|
## Contributing
|
78
143
|
|
data/SECURITY.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# Security Policy
|
2
|
+
|
3
|
+
## Supported Versions
|
4
|
+
|
5
|
+
| Version | Supported |
|
6
|
+
| ------- | ------------------ |
|
7
|
+
| 1.0.x | :white_check_mark: |
|
8
|
+
| < 1.x | :x: |
|
9
|
+
|
10
|
+
|
11
|
+
## Reporting a Vulnerability
|
12
|
+
|
13
|
+
If you find a vulnerability, please open an issue or send an e-mail to aria.diniz.dev@gmail.com
|
@@ -3,12 +3,24 @@
|
|
3
3
|
##
|
4
4
|
# Aspect that provide cache for the endpoints.
|
5
5
|
module CacheAspect
|
6
|
-
def call_endpoint(cache,
|
7
|
-
return super(*args) unless endpoints_to_cache
|
8
|
-
return cache.cache[args[2..].to_s.to_sym][0] unless cache.cache[args[2..].to_s.to_sym].nil?
|
6
|
+
def call_endpoint(cache, *args)
|
7
|
+
return super(*args) unless !cache[:cache].nil? && cache[:endpoints_to_cache]&.include?(args[0])
|
9
8
|
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
cache_filtered_name = cache_name_filter(args[1], cache[:ignored_headers])
|
10
|
+
|
11
|
+
cache[:cache].mutex.synchronize do
|
12
|
+
return cache[:cache].cache[cache_filtered_name][0] unless cache[:cache].cache[cache_filtered_name].nil?
|
13
|
+
|
14
|
+
response = super(*args)
|
15
|
+
cache[:cache].cache[cache_filtered_name] = [response, Time.now]
|
16
|
+
response
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def cache_name_filter(client_data, ignored_headers)
|
23
|
+
filtered_headers = client_data[:headers].filter { |key, _value| !ignored_headers&.include?(key) }
|
24
|
+
[{ body: client_data[:body], params: client_data[:params], headers: filtered_headers }].to_s.to_sym
|
13
25
|
end
|
14
26
|
end
|
@@ -8,8 +8,8 @@ require "logger"
|
|
8
8
|
# in the framework.
|
9
9
|
module LoggingAspect
|
10
10
|
def call_endpoint(logger, *args)
|
11
|
-
endpoint_name = args[
|
12
|
-
logger.info("Request received for #{endpoint_name} with arguments: #{args[
|
11
|
+
endpoint_name = args[1].split(".")[1..].join("/")
|
12
|
+
logger.info("Request received for #{endpoint_name} with arguments: #{args[2..]}")
|
13
13
|
|
14
14
|
begin
|
15
15
|
response = super(*args)
|
@@ -13,7 +13,7 @@ module PrometheusAspect
|
|
13
13
|
ensure
|
14
14
|
duration = (Time.now - start_time) * 1_000
|
15
15
|
|
16
|
-
endpoint_name = args[
|
16
|
+
endpoint_name = args[2].split(".").join("/")
|
17
17
|
|
18
18
|
prometheus_middleware.request_duration_milliseconds.with_labels(endpoint: endpoint_name).observe(duration)
|
19
19
|
prometheus_middleware.request_count.with_labels(endpoint: endpoint_name).increment
|
@@ -1,9 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "../middlewares/rate_limiter_middleware"
|
4
|
+
require_relative "../data_filters/response_data_filter"
|
5
|
+
require_relative "../middlewares/memory_invalidation_middleware"
|
6
|
+
require_relative "../errors/too_many_requests_error"
|
3
7
|
require_relative "../aspects/prometheus_aspect"
|
4
8
|
require_relative "../aspects/logging_aspect"
|
5
|
-
require_relative "../utils/http_status_code"
|
6
9
|
require_relative "../aspects/cache_aspect"
|
10
|
+
require "openssl"
|
7
11
|
|
8
12
|
##
|
9
13
|
# Class responsible for providing a default
|
@@ -12,7 +16,6 @@ class Server
|
|
12
16
|
prepend CacheAspect
|
13
17
|
prepend LoggingAspect
|
14
18
|
prepend PrometheusAspect
|
15
|
-
include HttpStatusCode
|
16
19
|
# rubocop:disable Metrics/ParameterLists
|
17
20
|
|
18
21
|
##
|
@@ -22,21 +25,23 @@ class Server
|
|
22
25
|
# @param {Integer} port
|
23
26
|
# @param {String} bind
|
24
27
|
# @param {Integer} num_threads
|
25
|
-
# @param {
|
28
|
+
# @param {MemoryInvalidationMiddleware} cache
|
26
29
|
# @param {Prometheus::Client:Registry} prometheus
|
27
30
|
# @return {Server}
|
28
|
-
def initialize(macaw,
|
29
|
-
|
30
|
-
@
|
31
|
-
@bind = bind
|
31
|
+
def initialize(macaw, endpoints_to_cache = nil, cache = nil, prometheus = nil, prometheus_mw = nil)
|
32
|
+
@port = macaw.port
|
33
|
+
@bind = macaw.bind
|
32
34
|
@macaw = macaw
|
33
|
-
@macaw_log =
|
34
|
-
@num_threads =
|
35
|
+
@macaw_log = macaw.macaw_log
|
36
|
+
@num_threads = macaw.threads
|
35
37
|
@work_queue = Queue.new
|
36
|
-
|
37
|
-
|
38
|
+
ignored_headers = set_cache_ignored_h
|
39
|
+
set_features
|
40
|
+
@rate_limit ||= nil
|
41
|
+
ignored_headers ||= nil
|
42
|
+
@cache = { cache: cache, endpoints_to_cache: endpoints_to_cache || [], ignored_headers: ignored_headers }
|
38
43
|
@prometheus = prometheus
|
39
|
-
@prometheus_middleware =
|
44
|
+
@prometheus_middleware = prometheus_mw
|
40
45
|
@workers = []
|
41
46
|
end
|
42
47
|
|
@@ -46,6 +51,7 @@ class Server
|
|
46
51
|
# Start running the webserver.
|
47
52
|
def run
|
48
53
|
@server = TCPServer.new(@bind, @port)
|
54
|
+
@server = OpenSSL::SSL::SSLServer.new(@server, @context) if @context
|
49
55
|
@num_threads.times do
|
50
56
|
@workers << Thread.new do
|
51
57
|
loop do
|
@@ -59,6 +65,8 @@ class Server
|
|
59
65
|
|
60
66
|
loop do
|
61
67
|
@work_queue << @server.accept
|
68
|
+
rescue OpenSSL::SSL::SSLError => e
|
69
|
+
@macaw_log.error("SSL error: #{e.message}")
|
62
70
|
rescue IOError, Errno::EBADF
|
63
71
|
break
|
64
72
|
end
|
@@ -77,13 +85,20 @@ class Server
|
|
77
85
|
def handle_client(client)
|
78
86
|
path, method_name, headers, body, parameters = RequestDataFiltering.parse_request_data(client, @macaw.routes)
|
79
87
|
raise EndpointNotMappedError unless @macaw.respond_to?(method_name)
|
88
|
+
raise TooManyRequestsError unless @rate_limit.nil? || @rate_limit.allow?(client.peeraddr[3])
|
89
|
+
|
90
|
+
declare_client_session(client)
|
91
|
+
client_data = get_client_data(body, headers, parameters)
|
80
92
|
|
81
93
|
@macaw_log.info("Running #{path.gsub("\n", "").gsub("\r", "")}")
|
82
|
-
message, status = call_endpoint(@prometheus_middleware, @macaw_log, @cache,
|
83
|
-
|
94
|
+
message, status, response_headers = call_endpoint(@prometheus_middleware, @macaw_log, @cache,
|
95
|
+
method_name, client_data, client.peeraddr[3])
|
84
96
|
status ||= 200
|
85
97
|
message ||= nil
|
86
|
-
|
98
|
+
response_headers ||= nil
|
99
|
+
client.puts ResponseDataFilter.mount_response(status, response_headers, message)
|
100
|
+
rescue TooManyRequestsError
|
101
|
+
client.print "HTTP/1.1 429 Too Many Requests\r\n\r\n"
|
87
102
|
rescue EndpointNotMappedError
|
88
103
|
client.print "HTTP/1.1 404 Not Found\r\n\r\n"
|
89
104
|
rescue StandardError => e
|
@@ -93,7 +108,71 @@ class Server
|
|
93
108
|
client.close
|
94
109
|
end
|
95
110
|
|
96
|
-
def
|
97
|
-
@
|
111
|
+
def declare_client_session(client)
|
112
|
+
@session[client.peeraddr[3]] ||= [{}, Time.now]
|
113
|
+
@session[client.peeraddr[3]] = [{}, Time.now] if @session[client.peeraddr[3]][0].nil?
|
114
|
+
end
|
115
|
+
|
116
|
+
def set_rate_limiting
|
117
|
+
return unless @macaw.config&.dig("macaw", "rate_limiting")
|
118
|
+
|
119
|
+
@rate_limit = RateLimiterMiddleware.new(
|
120
|
+
@macaw.config["macaw"]["rate_limiting"]["window"].to_i || 1,
|
121
|
+
@macaw.config["macaw"]["rate_limiting"]["max_requests"].to_i || 60
|
122
|
+
)
|
123
|
+
end
|
124
|
+
|
125
|
+
def set_cache_ignored_h
|
126
|
+
ignored_headers = []
|
127
|
+
if @macaw.config&.dig("macaw", "cache", "ignored_headers")
|
128
|
+
ignored_headers = @macaw.config["macaw"]["cache"]["ignore_headers"] || []
|
129
|
+
end
|
130
|
+
ignored_headers
|
131
|
+
end
|
132
|
+
|
133
|
+
def set_ssl
|
134
|
+
if @macaw.config&.dig("macaw", "ssl")
|
135
|
+
@context = OpenSSL::SSL::SSLContext.new
|
136
|
+
@context.cert = OpenSSL::X509::Certificate.new(File.read(@macaw.config["macaw"]["ssl"]["cert_file_name"]))
|
137
|
+
@context.key = OpenSSL::PKey::RSA.new(File.read(@macaw.config["macaw"]["ssl"]["key_file_name"]))
|
138
|
+
end
|
139
|
+
@context ||= nil
|
140
|
+
rescue IOError => e
|
141
|
+
@macaw_log.error("It was not possible to read files #{@macaw.config["macaw"]["ssl"]["cert_file_name"]} and
|
142
|
+
#{@macaw.config["macaw"]["ssl"]["key_file_name"]}. Please assure the files exists and their names are correct.")
|
143
|
+
@macaw_log.error(e.backtrace)
|
144
|
+
raise e
|
145
|
+
end
|
146
|
+
|
147
|
+
def set_session
|
148
|
+
@session = {}
|
149
|
+
inv = if @macaw.config&.dig("macaw", "session", "invalidation_time")
|
150
|
+
MemoryInvalidationMiddleware.new(@macaw.config["macaw"]["session"]["invalidation_time"])
|
151
|
+
else
|
152
|
+
MemoryInvalidationMiddleware.new
|
153
|
+
end
|
154
|
+
inv.cache = @session
|
155
|
+
end
|
156
|
+
|
157
|
+
def set_features
|
158
|
+
set_rate_limiting
|
159
|
+
set_session
|
160
|
+
set_ssl
|
161
|
+
end
|
162
|
+
|
163
|
+
def call_endpoint(name, client_data, client_ip)
|
164
|
+
@macaw.send(
|
165
|
+
name.to_sym,
|
166
|
+
{
|
167
|
+
headers: client_data[:headers],
|
168
|
+
body: client_data[:body],
|
169
|
+
params: client_data[:parameters],
|
170
|
+
client: @session[client_ip][0]
|
171
|
+
}
|
172
|
+
)
|
173
|
+
end
|
174
|
+
|
175
|
+
def get_client_data(body, headers, parameters)
|
176
|
+
{ body: body, headers: headers, parameters: parameters }
|
98
177
|
end
|
99
178
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../utils/http_status_code"
|
4
|
+
|
5
|
+
##
|
6
|
+
# Module responsible to filter and mount HTTP responses
|
7
|
+
module ResponseDataFilter
|
8
|
+
include HttpStatusCode
|
9
|
+
|
10
|
+
def self.mount_response(status, headers, body)
|
11
|
+
"#{mount_first_response_line(status, headers)}#{mount_response_headers(headers)}#{body}"
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.mount_first_response_line(status, headers)
|
15
|
+
separator = " \r\n\r\n"
|
16
|
+
separator = " \r\n" unless headers.nil?
|
17
|
+
|
18
|
+
"HTTP/1.1 #{status} #{HTTP_STATUS_CODE_MAP[status]}#{separator}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.mount_response_headers(headers)
|
22
|
+
return nil if headers.nil?
|
23
|
+
|
24
|
+
response = ""
|
25
|
+
headers.each do |key, value|
|
26
|
+
response += "#{key}: #{value}\r\n"
|
27
|
+
end
|
28
|
+
response += "\r\n\r\n"
|
29
|
+
response
|
30
|
+
end
|
31
|
+
end
|
data/lib/macaw_framework/middlewares/{caching_middleware.rb → memory_invalidation_middleware.rb}
RENAMED
@@ -3,16 +3,19 @@
|
|
3
3
|
##
|
4
4
|
# Middleware responsible for storing and
|
5
5
|
# invalidating cache.
|
6
|
-
class
|
7
|
-
attr_accessor :cache
|
6
|
+
class MemoryInvalidationMiddleware
|
7
|
+
attr_accessor :cache, :mutex
|
8
8
|
|
9
9
|
def initialize(inv_time_seconds = 3_600)
|
10
10
|
@cache = {}
|
11
|
+
@mutex = Mutex.new
|
11
12
|
Thread.new do
|
12
13
|
loop do
|
13
14
|
sleep(1)
|
14
|
-
@
|
15
|
-
@cache.
|
15
|
+
@mutex.synchronize do
|
16
|
+
@cache.each_pair do |key, value|
|
17
|
+
@cache.delete(key) if Time.now - value[1] >= inv_time_seconds
|
18
|
+
end
|
16
19
|
end
|
17
20
|
end
|
18
21
|
end
|
@@ -42,7 +42,7 @@ class PrometheusMiddleware
|
|
42
42
|
def prometheus_endpoint(prometheus_registry, configurations, macaw)
|
43
43
|
endpoint = configurations["macaw"]["prometheus"]["endpoint"] || "/metrics"
|
44
44
|
macaw.get(endpoint) do |_context|
|
45
|
-
[Prometheus::Client::Formats::Text.marshal(prometheus_registry), 200]
|
45
|
+
[Prometheus::Client::Formats::Text.marshal(prometheus_registry), 200, { "Content-Type" => "plaintext" }]
|
46
46
|
end
|
47
47
|
end
|
48
48
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
##
|
4
|
+
# Middleware responsible for implementing
|
5
|
+
# rate limiting
|
6
|
+
class RateLimiterMiddleware
|
7
|
+
attr_reader :window_size, :max_requests
|
8
|
+
|
9
|
+
def initialize(window_size, max_requests)
|
10
|
+
@window_size = window_size
|
11
|
+
@max_requests = max_requests
|
12
|
+
@client_timestamps = Hash.new { |key, value| key[value] = [] }
|
13
|
+
@mutex = Mutex.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def allow?(client_id)
|
17
|
+
@mutex.synchronize do
|
18
|
+
now = Time.now.to_i
|
19
|
+
timestamps = @client_timestamps[client_id]
|
20
|
+
|
21
|
+
timestamps.reject! { |timestamp| timestamp <= now - window_size }
|
22
|
+
|
23
|
+
if timestamps.length < max_requests
|
24
|
+
timestamps << now
|
25
|
+
true
|
26
|
+
else
|
27
|
+
false
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/macaw_framework.rb
CHANGED
@@ -2,8 +2,8 @@
|
|
2
2
|
|
3
3
|
require_relative "macaw_framework/errors/endpoint_not_mapped_error"
|
4
4
|
require_relative "macaw_framework/middlewares/prometheus_middleware"
|
5
|
-
require_relative "macaw_framework/
|
6
|
-
require_relative "macaw_framework/middlewares/
|
5
|
+
require_relative "macaw_framework/data_filters/request_data_filtering"
|
6
|
+
require_relative "macaw_framework/middlewares/memory_invalidation_middleware"
|
7
7
|
require_relative "macaw_framework/core/server"
|
8
8
|
require_relative "macaw_framework/version"
|
9
9
|
require "prometheus/client"
|
@@ -18,7 +18,7 @@ module MacawFramework
|
|
18
18
|
class Macaw
|
19
19
|
##
|
20
20
|
# Array containing the routes defined in the application
|
21
|
-
attr_reader :routes
|
21
|
+
attr_reader :routes, :port, :bind, :threads, :macaw_log, :config
|
22
22
|
|
23
23
|
##
|
24
24
|
# @param {Logger} custom_log
|
@@ -26,27 +26,27 @@ module MacawFramework
|
|
26
26
|
begin
|
27
27
|
@routes = []
|
28
28
|
@macaw_log ||= custom_log.nil? ? Logger.new($stdout) : custom_log
|
29
|
-
config = JSON.parse(File.read("application.json"))
|
30
|
-
@port = config["macaw"]["port"] || 8080
|
31
|
-
@bind = config["macaw"]["bind"] || "localhost"
|
32
|
-
@threads = config["macaw"]["threads"] || 5
|
33
|
-
unless config["macaw"]["cache"].nil?
|
34
|
-
@cache =
|
29
|
+
@config = JSON.parse(File.read("application.json"))
|
30
|
+
@port = @config["macaw"]["port"] || 8080
|
31
|
+
@bind = @config["macaw"]["bind"] || "localhost"
|
32
|
+
@threads = @config["macaw"]["threads"] || 5
|
33
|
+
unless @config["macaw"]["cache"].nil?
|
34
|
+
@cache = MemoryInvalidationMiddleware.new(@config["macaw"]["cache"]["cache_invalidation"].to_i || 3_600)
|
35
35
|
end
|
36
|
-
@prometheus = Prometheus::Client::Registry.new if config["macaw"]["prometheus"]
|
37
|
-
@prometheus_middleware = PrometheusMiddleware.new if config["macaw"]["prometheus"]
|
38
|
-
@prometheus_middleware.configure_prometheus(@prometheus, config, self) if config["macaw"]["prometheus"]
|
36
|
+
@prometheus = Prometheus::Client::Registry.new if @config["macaw"]["prometheus"]
|
37
|
+
@prometheus_middleware = PrometheusMiddleware.new if @config["macaw"]["prometheus"]
|
38
|
+
@prometheus_middleware.configure_prometheus(@prometheus, @config, self) if @config["macaw"]["prometheus"]
|
39
39
|
rescue StandardError => e
|
40
40
|
@macaw_log.error(e.message)
|
41
41
|
end
|
42
42
|
@port ||= 8080
|
43
43
|
@bind ||= "localhost"
|
44
|
+
@config ||= nil
|
44
45
|
@threads ||= 5
|
45
46
|
@endpoints_to_cache = []
|
46
47
|
@prometheus ||= nil
|
47
48
|
@prometheus_middleware ||= nil
|
48
|
-
@server = server.new(self, @
|
49
|
-
@prometheus_middleware)
|
49
|
+
@server = server.new(self, @endpoints_to_cache, @cache, @prometheus, @prometheus_middleware)
|
50
50
|
end
|
51
51
|
|
52
52
|
##
|
@@ -1,18 +1,25 @@
|
|
1
1
|
module MacawFramework
|
2
2
|
class Macaw
|
3
|
-
@bind:
|
3
|
+
@bind: String
|
4
4
|
@cache: untyped
|
5
|
+
@config: Hash[String, untyped]
|
5
6
|
@endpoints_to_cache: Array[String]
|
6
7
|
@macaw_log: Logger
|
7
|
-
@port: int
|
8
8
|
|
9
9
|
@prometheus: untyped
|
10
|
+
@prometheus_middleware: untyped
|
10
11
|
@server: Server
|
11
12
|
|
12
13
|
@threads: Integer
|
13
14
|
|
15
|
+
attr_reader bind: String
|
16
|
+
attr_reader config: Hash[String, untyped]
|
17
|
+
attr_reader macaw_log: Logger
|
18
|
+
attr_reader port: Integer
|
14
19
|
attr_reader routes: Array[String]
|
15
20
|
|
21
|
+
attr_reader threads: Integer
|
22
|
+
|
16
23
|
def delete: -> nil
|
17
24
|
|
18
25
|
def get: -> nil
|
data/sig/server.rbs
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
class Server
|
2
2
|
@bind: String
|
3
|
-
@cache:
|
3
|
+
@cache: Hash[Symbol, Array]
|
4
|
+
@context: OpenSSL::SSL::SSLContext
|
4
5
|
@endpoints_to_cache: Array[String]
|
5
6
|
@macaw: MacawFramework::Macaw
|
6
7
|
@macaw_log: Logger
|
@@ -9,7 +10,7 @@ class Server
|
|
9
10
|
|
10
11
|
@prometheus: untyped
|
11
12
|
@prometheus_middleware: untyped
|
12
|
-
@server: TCPServer
|
13
|
+
@server: TCPServer|OpenSSL::SSL::SSLServer
|
13
14
|
|
14
15
|
@threads: Integer
|
15
16
|
|
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: 0.
|
4
|
+
version: 1.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aria Diniz
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-05-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: prometheus-client
|
@@ -24,7 +24,9 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '4.1'
|
27
|
-
description:
|
27
|
+
description: |-
|
28
|
+
A lightweight web framework designed for building efficient backend applications. Initially
|
29
|
+
created for study purposes, now production-ready and open for contributions.
|
28
30
|
email:
|
29
31
|
- aria.diniz.dev@gmail.com
|
30
32
|
executables: []
|
@@ -38,23 +40,27 @@ files:
|
|
38
40
|
- LICENSE.txt
|
39
41
|
- README.md
|
40
42
|
- Rakefile
|
43
|
+
- SECURITY.md
|
41
44
|
- lib/macaw_framework.rb
|
42
45
|
- lib/macaw_framework/aspects/cache_aspect.rb
|
43
46
|
- lib/macaw_framework/aspects/logging_aspect.rb
|
44
47
|
- lib/macaw_framework/aspects/prometheus_aspect.rb
|
45
48
|
- lib/macaw_framework/core/server.rb
|
49
|
+
- lib/macaw_framework/data_filters/request_data_filtering.rb
|
50
|
+
- lib/macaw_framework/data_filters/response_data_filter.rb
|
46
51
|
- lib/macaw_framework/errors/endpoint_not_mapped_error.rb
|
47
|
-
- lib/macaw_framework/
|
52
|
+
- lib/macaw_framework/errors/too_many_requests_error.rb
|
53
|
+
- lib/macaw_framework/middlewares/memory_invalidation_middleware.rb
|
48
54
|
- lib/macaw_framework/middlewares/prometheus_middleware.rb
|
49
|
-
- lib/macaw_framework/middlewares/
|
55
|
+
- lib/macaw_framework/middlewares/rate_limiter_middleware.rb
|
50
56
|
- lib/macaw_framework/utils/http_status_code.rb
|
51
57
|
- lib/macaw_framework/version.rb
|
52
58
|
- macaw_logo.png
|
53
|
-
- sig/caching_middleware.rbs
|
54
59
|
- sig/http_status_code.rbs
|
55
60
|
- sig/logging_aspect.rbs
|
56
61
|
- sig/macaw_framework.rbs
|
57
62
|
- sig/macaw_framework/macaw.rbs
|
63
|
+
- sig/memory_invalidation_middleware.rbs
|
58
64
|
- sig/request_data_filtering.rbs
|
59
65
|
- sig/server.rbs
|
60
66
|
homepage: https://github.com/ariasdiniz/macaw_framework
|
@@ -79,8 +85,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
79
85
|
- !ruby/object:Gem::Version
|
80
86
|
version: '0'
|
81
87
|
requirements: []
|
82
|
-
rubygems_version: 3.4.
|
88
|
+
rubygems_version: 3.4.12
|
83
89
|
signing_key:
|
84
90
|
specification_version: 4
|
85
|
-
summary: A web framework
|
91
|
+
summary: A lightweight back-end web framework
|
86
92
|
test_files: []
|
File without changes
|