hephaestus 0.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 +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
|