hephaestus 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +13 -0
- data/LICENSE.txt +9 -0
- data/README.md +19 -0
- data/bin/hephaestus +55 -0
- data/lib/hephaestus/actions/strip_comments_action.rb +263 -0
- data/lib/hephaestus/actions.rb +116 -0
- data/lib/hephaestus/app_builder.rb +168 -0
- data/lib/hephaestus/exit_on_failure.rb +22 -0
- data/lib/hephaestus/generators/app_generator.rb +158 -0
- data/lib/hephaestus/generators/base.rb +65 -0
- data/lib/hephaestus/generators/config_generator.rb +102 -0
- data/lib/hephaestus/generators/core_generator.rb +50 -0
- data/lib/hephaestus/generators/deployment_generator.rb +18 -0
- data/lib/hephaestus/generators/lib_generator.rb +16 -0
- data/lib/hephaestus/generators/license_generator.rb +16 -0
- data/lib/hephaestus/generators/rubocop_generator.rb +18 -0
- data/lib/hephaestus/generators/sorbet_generator.rb +16 -0
- data/lib/hephaestus/version.rb +11 -0
- data/lib/hephaestus.rb +21 -0
- data/templates/Gemfile.erb +121 -0
- data/templates/Procfile.debug +2 -0
- data/templates/Procfile.dev +2 -0
- data/templates/README.md.erb +1 -0
- data/templates/app/controllers/application_controller.rb +107 -0
- data/templates/app/controllers/concerns/authable.rb +32 -0
- data/templates/app/controllers/root_controller.rb +11 -0
- data/templates/app/controllers/settings_controller.rb +7 -0
- data/templates/app/controllers/staff_controller.rb +15 -0
- data/templates/app/controllers/yetto_controller.rb +30 -0
- data/templates/app/jobs/application_job.rb +10 -0
- data/templates/app/jobs/update_yetto_job.rb +27 -0
- data/templates/app/lib/body_parameter/yetto_parameters.rb +8 -0
- data/templates/app/lib/body_parameter.rb +6 -0
- data/templates/app/lib/constants/app.rb +8 -0
- data/templates/app/lib/headers/yetto.rb +17 -0
- data/templates/app/lib/headers.rb +5 -0
- data/templates/app/lib/path_parameter/yetto_parameters.rb +25 -0
- data/templates/app/lib/path_parameter.rb +8 -0
- data/templates/app/lib/plug_app/http.rb +34 -0
- data/templates/app/lib/plug_app/middleware/malformed_request.rb +110 -0
- data/templates/app/lib/plug_app/middleware/not_found.rb +41 -0
- data/templates/app/lib/plug_app/middleware/openapi_validation.rb +54 -0
- data/templates/app/lib/plug_app/middleware/tracing_attributes.rb +42 -0
- data/templates/app/lib/query_parameter.rb +6 -0
- data/templates/app/serializers/error_serializer.rb +16 -0
- data/templates/app/services/yetto_service.rb +61 -0
- data/templates/app/views/settings/index.json.jbuilder +15 -0
- data/templates/config/initializers/cors.rb +18 -0
- data/templates/config/initializers/environment.rb +30 -0
- data/templates/config/initializers/filter_parameter_logging.rb +22 -0
- data/templates/config/initializers/inflections.rb +20 -0
- data/templates/config/initializers/lograge.rb +25 -0
- data/templates/config/initializers/open_telemetry.rb +27 -0
- data/templates/config/initializers/sidekiq.rb +11 -0
- data/templates/config/initializers/slack_webhook_logger.rb +17 -0
- data/templates/config/sidekiq.yml +18 -0
- data/templates/hephaestus_gitignore +296 -0
- data/templates/lib/plug_app/schemas/api/2023-03-06/components/parameters/headers/yetto.json +42 -0
- data/templates/lib/plug_app/schemas/api/2023-03-06/components/parameters/plugInstallation.json +12 -0
- data/templates/lib/plug_app/schemas/api/2023-03-06/components/schemas/plug.json +9 -0
- data/templates/lib/plug_app/schemas/api/2023-03-06/components/schemas/responses.json +64 -0
- data/templates/lib/plug_app/schemas/api/2023-03-06/components/schemas/yetto.json +1 -0
- data/templates/lib/plug_app/schemas/api/2023-03-06/openapi.json +27 -0
- data/templates/lib/plug_app/schemas/api/2023-03-06/paths/plug.json +91 -0
- data/templates/lib/plug_app/schemas/api/2023-03-06/paths/yetto/after_create_message.json +41 -0
- data/templates/lib/plug_app/schemas/api/2023-03-06/paths/yetto/after_create_plug_installation.json +41 -0
- data/templates/lib/tasks/test_tasks.rake +10 -0
- data/templates/script/ci +7 -0
- data/templates/script/hmac_text +22 -0
- data/templates/script/licenses +51 -0
- data/templates/script/ngrok +5 -0
- data/templates/script/security_checks/brakeman +5 -0
- data/templates/script/security_checks/bundle-audit +5 -0
- data/templates/script/server +5 -0
- data/templates/script/server-debug +5 -0
- data/templates/script/test +5 -0
- data/templates/script/typecheck +42 -0
- data/templates/sorbet/custom.rbi +14 -0
- data/templates/test/controllers/root_controller_test.rb +12 -0
- data/templates/test/controllers/settings_controller_test.rb +27 -0
- data/templates/test/controllers/yetto_controller_test.rb +130 -0
- data/templates/test/jobs/update_yetto_job_test.rb +41 -0
- data/templates/test/support/api.rb +74 -0
- data/templates/test/support/rails.rb +39 -0
- data/templates/test/support/webmocks/slack_webmock.rb +24 -0
- data/templates/test/support/webmocks/yetto.rb +94 -0
- data/templates/test/support/webmocks.rb +5 -0
- data/templates/test/test_helper.rb +24 -0
- metadata +209 -0
@@ -0,0 +1,107 @@
|
|
1
|
+
# typed: false
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
class ApplicationController < ActionController::Base
|
5
|
+
include ActionController::MimeResponds
|
6
|
+
|
7
|
+
rescue_from ActionController::UnknownFormat, with: :not_acceptable
|
8
|
+
|
9
|
+
before_action :set_request_span_context
|
10
|
+
def set_request_span_context
|
11
|
+
end
|
12
|
+
|
13
|
+
after_action :set_response_span_context
|
14
|
+
def set_response_span_context
|
15
|
+
OpenTelemetry::Trace.current_span.add_attributes({
|
16
|
+
OpenTelemetry::SemanticConventions::Trace::HTTP_RESPONSE_CONTENT_LENGTH => response.headers["content-length"] || 0,
|
17
|
+
})
|
18
|
+
end
|
19
|
+
|
20
|
+
def no_content
|
21
|
+
head(:no_content)
|
22
|
+
end
|
23
|
+
|
24
|
+
def okay
|
25
|
+
render(
|
26
|
+
json: {
|
27
|
+
message: "OK",
|
28
|
+
}.to_json,
|
29
|
+
status: :ok,
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
def created
|
34
|
+
render(
|
35
|
+
json: {
|
36
|
+
message: "Created",
|
37
|
+
}.to_json,
|
38
|
+
status: :created,
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
def bad_request
|
43
|
+
render(
|
44
|
+
json: {
|
45
|
+
errors: [
|
46
|
+
{
|
47
|
+
message: "Bad Request",
|
48
|
+
},
|
49
|
+
],
|
50
|
+
}.to_json,
|
51
|
+
status: :bad_request,
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
def forbidden
|
56
|
+
render(
|
57
|
+
json: {
|
58
|
+
errors: [
|
59
|
+
{
|
60
|
+
message: "Forbidden",
|
61
|
+
},
|
62
|
+
],
|
63
|
+
}.to_json,
|
64
|
+
status: :forbidden,
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
def not_acceptable(e)
|
69
|
+
render(
|
70
|
+
json: ::ErrorSerializer.format("Not Acceptable").to_json,
|
71
|
+
status: :not_acceptable,
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
def not_found
|
76
|
+
render(
|
77
|
+
json: ::ErrorSerializer.format("Not Found").to_json,
|
78
|
+
status: :not_found,
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
def service_unavailable(msg)
|
83
|
+
render(
|
84
|
+
json: {
|
85
|
+
errors: [
|
86
|
+
{
|
87
|
+
message: "Service Unavailable: #{msg}",
|
88
|
+
},
|
89
|
+
],
|
90
|
+
}.to_json,
|
91
|
+
status: :service_unavailable,
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
def bad_gateway
|
96
|
+
render(
|
97
|
+
json: {
|
98
|
+
errors: [
|
99
|
+
{
|
100
|
+
message: "Bad Gateway",
|
101
|
+
},
|
102
|
+
],
|
103
|
+
}.to_json,
|
104
|
+
status: :bad_gateway,
|
105
|
+
)
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Authable
|
5
|
+
extend T::Sig
|
6
|
+
|
7
|
+
include ActionDispatch::Http::Cache::Response
|
8
|
+
|
9
|
+
include ActionController::Helpers::ClassMethods
|
10
|
+
include ActionController::HttpAuthentication::Basic::ControllerMethods
|
11
|
+
include BodyParameter::YettoParameters
|
12
|
+
|
13
|
+
SHA256_DIGEST = OpenSSL::Digest.new("sha256")
|
14
|
+
|
15
|
+
sig { void }
|
16
|
+
def from_yetto?
|
17
|
+
return bad_request if request.headers.blank?
|
18
|
+
|
19
|
+
yetto_signature = request.headers.fetch(Headers::Yetto::HEADER_SIGNATURE, "")
|
20
|
+
|
21
|
+
return bad_request unless yetto_signature.start_with?("sha256=")
|
22
|
+
|
23
|
+
hmac_header = yetto_signature.split("sha256=").last
|
24
|
+
body = request.env["RAW_POST_DATA"]
|
25
|
+
|
26
|
+
calculated_hmac = OpenSSL::HMAC.hexdigest(SHA256_DIGEST, YETTO_PLUG_APP_TOKEN, body)
|
27
|
+
|
28
|
+
return true if ActiveSupport::SecurityUtils.secure_compare(calculated_hmac, hmac_header)
|
29
|
+
|
30
|
+
bad_request
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
class StaffController < ApplicationController
|
5
|
+
extend T::Sig
|
6
|
+
|
7
|
+
class << self
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
sig { params(request: ActionDispatch::Request).returns(T::Boolean) }
|
11
|
+
def staff_request?(request)
|
12
|
+
false
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# typed: false
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
class YettoController < ApplicationController
|
5
|
+
include BodyParameter::YettoParameters
|
6
|
+
include PathParameter::YettoParameters
|
7
|
+
include Authable
|
8
|
+
|
9
|
+
include Headers::Yetto
|
10
|
+
|
11
|
+
before_action :from_yetto?
|
12
|
+
|
13
|
+
def event
|
14
|
+
case pparam_yetto_event
|
15
|
+
when Headers::Yetto::EVENT_AFTER_CREATE
|
16
|
+
case pparam_yetto_record_type
|
17
|
+
when Headers::Yetto::RECORD_TYPE_PLUG_INSTALLATION
|
18
|
+
|
19
|
+
no_content
|
20
|
+
when Headers::Yetto::RECORD_TYPE_MESSAGE
|
21
|
+
|
22
|
+
no_content
|
23
|
+
else
|
24
|
+
not_found
|
25
|
+
end
|
26
|
+
else
|
27
|
+
not_found
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# typed: false
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
class ApplicationJob < ActiveJob::Base
|
5
|
+
# Automatically retry jobs that encountered a deadlock
|
6
|
+
# retry_on ActiveRecord::Deadlocked
|
7
|
+
|
8
|
+
# Most jobs are safe to ignore if the underlying records are no longer available
|
9
|
+
# discard_on ActiveJob::DeserializationError
|
10
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# typed: false
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Send updated data to Yetto to store in the database
|
5
|
+
# This can be used to update installation data or message data
|
6
|
+
|
7
|
+
class UpdateYettoJob < ApplicationJob
|
8
|
+
queue_as :default
|
9
|
+
|
10
|
+
def perform(params)
|
11
|
+
type = params.delete(:type)
|
12
|
+
|
13
|
+
organization_id = params.fetch(:organization, {}).fetch(:id, nil)
|
14
|
+
inbox_id = params.fetch(:inbox, {}).fetch(:id, nil)
|
15
|
+
plug_installation_id = params.fetch(:plug_installation, {}).fetch(:id, nil)
|
16
|
+
|
17
|
+
case type
|
18
|
+
when "installation"
|
19
|
+
YettoService.update_installation(organization_id, inbox_id, plug_installation_id, params)
|
20
|
+
when "switch"
|
21
|
+
plug_id = params.fetch(:plug, {}).fetch(:id, nil)
|
22
|
+
YettoService.create_switch(organization_id, inbox_id, plug_id, params)
|
23
|
+
when "message"
|
24
|
+
YettoService.create_message(organization_id, inbox_id, params)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# typed: false
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Headers
|
5
|
+
module Yetto
|
6
|
+
YETTO_DELIVERY_ID = "HTTP_X_YETTO_DELIVERY_ID"
|
7
|
+
|
8
|
+
HEADER_EVENT = "HTTP_X_YETTO_EVENT"
|
9
|
+
EVENT_AFTER_CREATE = "after_create"
|
10
|
+
|
11
|
+
HEADER_RECORD_TYPE = "HTTP_X_YETTO_RECORD_TYPE"
|
12
|
+
RECORD_TYPE_PLUG_INSTALLATION = "plug_installation"
|
13
|
+
RECORD_TYPE_MESSAGE = "message"
|
14
|
+
|
15
|
+
HEADER_SIGNATURE = "HTTP_X_YETTO_SIGNATURE"
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# typed: false
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module PathParameter
|
5
|
+
module YettoParameters
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
sig { returns(String) }
|
9
|
+
def pparam_yetto_event
|
10
|
+
yetto_path_params.fetch(:event, "")
|
11
|
+
end
|
12
|
+
|
13
|
+
sig { returns(String) }
|
14
|
+
def pparam_yetto_record_type
|
15
|
+
yetto_path_params.fetch(:record_type, "")
|
16
|
+
end
|
17
|
+
|
18
|
+
sig { returns(ActionController::Parameters) }
|
19
|
+
def yetto_path_params
|
20
|
+
return ActionController::Parameters.new if params.blank?
|
21
|
+
|
22
|
+
params.permit(:event, :record_type)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module PlugApp
|
5
|
+
module HTTP
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
OK = "OK"
|
9
|
+
OK_I = 200
|
10
|
+
|
11
|
+
CREATED = "Created"
|
12
|
+
CREATED_I = 201
|
13
|
+
NO_CONTENT = "No Content"
|
14
|
+
NO_CONTENT_I = 204
|
15
|
+
|
16
|
+
NOT_FOUND = "Not Found"
|
17
|
+
NOT_FOUND_I = 404
|
18
|
+
BAD_REQUEST = "Bad Request"
|
19
|
+
BAD_REQUEST_I = 400
|
20
|
+
UNAUTHORIZED = "Unauthorized"
|
21
|
+
UNAUTHORIZED_I = 401
|
22
|
+
FORBIDDEN = "Forbidden"
|
23
|
+
FORBIDDEN_I = 403
|
24
|
+
NOT_ACCEPTABLE = "Not Acceptable"
|
25
|
+
NOT_ACCEPTABLE_I = 406
|
26
|
+
SERVICE_UNAVAILABLE = "Service Unavailable"
|
27
|
+
SERVICE_UNAVAILABLE_I = 503
|
28
|
+
|
29
|
+
sig { params(status: Integer).returns(T::Boolean) }
|
30
|
+
def status_ok?(status)
|
31
|
+
status == OK_I
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module PlugApp
|
5
|
+
module Middleware
|
6
|
+
# There is no valid reason for a request to contain a malformed string
|
7
|
+
# so just return HTTP 400 (Bad Request) if we receive one
|
8
|
+
class MalformedRequest
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
include ActionController::HttpAuthentication::Basic
|
12
|
+
|
13
|
+
NULL_BYTE_REGEX = T.let(Regexp.new(Regexp.escape("\u0000")).freeze, Regexp)
|
14
|
+
|
15
|
+
sig { returns(T.untyped) }
|
16
|
+
attr_reader :app
|
17
|
+
|
18
|
+
sig { params(app: T.untyped).void }
|
19
|
+
def initialize(app)
|
20
|
+
@app = T.let(app, T.untyped)
|
21
|
+
end
|
22
|
+
|
23
|
+
sig { params(env: T.untyped).returns(T.untyped) }
|
24
|
+
def call(env)
|
25
|
+
return [PlugApp::HTTP::BAD_REQUEST_I, { "Content-Type" => "text/plain" }, [PlugApp::HTTP::BAD_REQUEST]] if request_contains_malformed_string?(env)
|
26
|
+
|
27
|
+
app.call(env)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
sig { params(env: T.untyped).returns(T::Boolean) }
|
33
|
+
def request_contains_malformed_string?(env)
|
34
|
+
# Duplicate the env, so it is not modified when accessing the parameters
|
35
|
+
# https://github.com/rails/rails/blob/34991a6ae2fc68347c01ea7382fa89004159e019/actionpack/lib/action_dispatch/http/parameters.rb#L59
|
36
|
+
request = ActionDispatch::Request.new(env.dup)
|
37
|
+
|
38
|
+
return true if malformed_path?(request.path)
|
39
|
+
return true if credentials_malformed?(request)
|
40
|
+
|
41
|
+
request.params.values.any? do |value|
|
42
|
+
param_has_null_byte?(value)
|
43
|
+
end
|
44
|
+
rescue ActionController::BadRequest
|
45
|
+
# If we can't build an ActionDispatch::Request something's wrong
|
46
|
+
# This would also happen if `#params` contains invalid UTF-8
|
47
|
+
# in this case we'll return a 400
|
48
|
+
true
|
49
|
+
end
|
50
|
+
|
51
|
+
sig { params(path: String).returns(T::Boolean) }
|
52
|
+
def malformed_path?(path)
|
53
|
+
string_malformed?(Rack::Utils.unescape(path))
|
54
|
+
rescue ArgumentError
|
55
|
+
# Rack::Utils.unescape raised this, path is malformed.
|
56
|
+
true
|
57
|
+
end
|
58
|
+
|
59
|
+
sig { params(request: T.untyped).returns(T::Boolean) }
|
60
|
+
def credentials_malformed?(request)
|
61
|
+
credentials = if has_basic_credentials?(request)
|
62
|
+
decode_credentials(request).presence
|
63
|
+
else
|
64
|
+
request.authorization.presence
|
65
|
+
end
|
66
|
+
|
67
|
+
return false unless credentials
|
68
|
+
|
69
|
+
string_malformed?(credentials)
|
70
|
+
end
|
71
|
+
|
72
|
+
sig { params(value: T.untyped, depth: Integer).returns(T::Boolean) }
|
73
|
+
def param_has_null_byte?(value, depth = 0)
|
74
|
+
# Guard against possible attack sending large amounts of nested params
|
75
|
+
# Should be safe as deeply nested params are highly uncommon.
|
76
|
+
return false if depth > 2
|
77
|
+
|
78
|
+
depth += 1
|
79
|
+
|
80
|
+
if value.respond_to?(:match)
|
81
|
+
string_malformed?(value)
|
82
|
+
elsif value.respond_to?(:values)
|
83
|
+
value.values.any? do |hash_value|
|
84
|
+
param_has_null_byte?(hash_value, depth)
|
85
|
+
end
|
86
|
+
elsif value.is_a?(Array)
|
87
|
+
value.any? do |array_value|
|
88
|
+
param_has_null_byte?(array_value, depth)
|
89
|
+
end
|
90
|
+
else
|
91
|
+
false
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
sig { params(string: String).returns(T::Boolean) }
|
96
|
+
def string_malformed?(string)
|
97
|
+
# We're using match instead of include because that raises an ArgumentError
|
98
|
+
# when the string contains invalid UTF-8
|
99
|
+
#
|
100
|
+
# We try to encode the string from ASCII-8BIT to UTF8. If we failed to do
|
101
|
+
# so for certain characters in the string, those chars are probably incomplete
|
102
|
+
# multibyte characters.
|
103
|
+
string.dup.force_encoding(Encoding::UTF_8).match?(NULL_BYTE_REGEX)
|
104
|
+
rescue ArgumentError, Encoding::UndefinedConversionError
|
105
|
+
# If we're here, we caught a malformed string. Return true
|
106
|
+
true
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# typed: false
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "openapi_first"
|
5
|
+
|
6
|
+
module PlugApp
|
7
|
+
module Middleware
|
8
|
+
# frozen_string_literal: true
|
9
|
+
|
10
|
+
# Rack::NotFound is a default endpoint. Optionally initialize with the
|
11
|
+
# path to a custom 404 page, to override the standard response body.
|
12
|
+
#
|
13
|
+
# Examples:
|
14
|
+
#
|
15
|
+
# Serve default 404 response:
|
16
|
+
# run Rack::NotFound.new
|
17
|
+
#
|
18
|
+
# Serve a custom 404 page:
|
19
|
+
# run Rack::NotFound.new('path/to/your/404.html')
|
20
|
+
|
21
|
+
class NotFound
|
22
|
+
F = ::File
|
23
|
+
|
24
|
+
def initialize(path = nil, content_type = "text/html")
|
25
|
+
if path.nil?
|
26
|
+
@content = "Not found\n"
|
27
|
+
else
|
28
|
+
@content = F.read(path)
|
29
|
+
@content = F.read(path)
|
30
|
+
end
|
31
|
+
@length = @content.bytesize.to_s
|
32
|
+
|
33
|
+
@content_type = content_type
|
34
|
+
end
|
35
|
+
|
36
|
+
def call(env)
|
37
|
+
[404, { "Content-Type" => @content_type, "Content-Length" => @length }, [@content]]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# typed: false
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "openapi_first"
|
5
|
+
|
6
|
+
module PlugApp
|
7
|
+
module Middleware
|
8
|
+
class OpenapiValidation
|
9
|
+
API_PATH_PREFIX = "/api/"
|
10
|
+
SPEC_PATH = Rails.root.join("lib/plug_app/schemas/api/2023-03-06/openapi.json")
|
11
|
+
|
12
|
+
def initialize(app)
|
13
|
+
@app = app
|
14
|
+
spec = OpenapiFirst.load(SPEC_PATH)
|
15
|
+
@response_validator = OpenapiFirst::ResponseValidator.new(spec)
|
16
|
+
@request_validator = OpenapiFirst::RequestValidation.new(->(_env) {}, spec: SPEC_PATH, raise_error: true)
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(env)
|
20
|
+
request = Rack::Request.new(env)
|
21
|
+
|
22
|
+
if request.path.starts_with?(API_PATH_PREFIX)
|
23
|
+
# force content-type to JSON
|
24
|
+
if env["CONTENT_TYPE"] == "application/x-www-form-urlencoded"
|
25
|
+
env["CONTENT_TYPE"] = "application/json"
|
26
|
+
end
|
27
|
+
|
28
|
+
begin
|
29
|
+
@request_validator.call(env)
|
30
|
+
# response = @app.call(env)
|
31
|
+
# @response_validator.validate(request, response)
|
32
|
+
rescue OpenapiFirst::NotFoundError
|
33
|
+
PlugApp::Middleware::NotFound.new.call(env)
|
34
|
+
rescue OpenapiFirst::RequestInvalidError => e
|
35
|
+
error_hsh = ::ErrorSerializer.format(e.message)
|
36
|
+
|
37
|
+
return [PlugApp::HTTP::BAD_REQUEST_I, { "Content-Type" => "application/json" }, [error_hsh]]
|
38
|
+
rescue => e
|
39
|
+
raise e unless Rails.env.production?
|
40
|
+
|
41
|
+
logger.error(
|
42
|
+
"openapi.request_validation.error",
|
43
|
+
path: request.path,
|
44
|
+
method: request.env["REQUEST_METHOD"],
|
45
|
+
error_message: e.message,
|
46
|
+
)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
@app.call(env)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# typed: false
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "openapi_first"
|
5
|
+
require "active_support/parameter_filter"
|
6
|
+
|
7
|
+
module PlugApp
|
8
|
+
module Middleware
|
9
|
+
class TracingAttributes
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
sig { returns(T.untyped) }
|
13
|
+
attr_reader :app
|
14
|
+
|
15
|
+
HTTP_REQUEST_BODY = "http.request.body"
|
16
|
+
|
17
|
+
sig { params(app: T.untyped).void }
|
18
|
+
def initialize(app)
|
19
|
+
@app = T.let(app, T.untyped)
|
20
|
+
@filterer = ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters)
|
21
|
+
end
|
22
|
+
|
23
|
+
sig { params(env: T.untyped).returns(T.untyped) }
|
24
|
+
def call(env)
|
25
|
+
request = ActionDispatch::Request.new(env.dup)
|
26
|
+
|
27
|
+
OpenTelemetry::Trace.current_span.add_attributes({
|
28
|
+
OpenTelemetry::VERSION => PlugApp::Application::GIT_SHA,
|
29
|
+
OpenTelemetry::SemanticConventions::Trace::HTTP_REQUEST_CONTENT_LENGTH => env["CONTENT_LENGTH"].to_i,
|
30
|
+
HTTP_REQUEST_BODY => filtered_params(request),
|
31
|
+
})
|
32
|
+
|
33
|
+
app.call(env)
|
34
|
+
end
|
35
|
+
|
36
|
+
def filtered_params(request)
|
37
|
+
params = request.params
|
38
|
+
@filterer.filter(params).to_json
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|