julewire-rack 1.0.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 +7 -0
- data/CHANGELOG.md +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +15 -0
- data/julewire-rack.gemspec +37 -0
- data/lib/julewire/rack/capture/body_content_type.rb +85 -0
- data/lib/julewire/rack/capture/buffered_response_body.rb +96 -0
- data/lib/julewire/rack/capture/header_selection.rb +37 -0
- data/lib/julewire/rack/capture/headers.rb +58 -0
- data/lib/julewire/rack/capture/json_body.rb +45 -0
- data/lib/julewire/rack/capture/request_body.rb +106 -0
- data/lib/julewire/rack/capture/settings.rb +40 -0
- data/lib/julewire/rack/version.rb +7 -0
- data/lib/julewire/rack.rb +13 -0
- data/lib/julewire-rack.rb +3 -0
- metadata +101 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0505d309f127982b88645a56045d2f472d36acb14efe781f6f08290559f8763e
|
|
4
|
+
data.tar.gz: 00b881c32d5761ab740773b80098c9c47950ceb574e65d38f1d4c1e14d36adbe
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: '078de25613aef97e313246e95e5207771a61439ed22f4b5ca97c1276593bd00863285504486275505d187fab88ab63dedbe69913b617e0e9e4c29acdc7abde7a'
|
|
7
|
+
data.tar.gz: c35d17e54f37c11328f0582497a44084faf7f3ac28f9d5418b637d905d8327441efe71b4af276c4a88c9bc60c3e47daa27893044d8353db3a260c1b2fb34556b
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexander Grebennik
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Julewire Rack
|
|
2
|
+
|
|
3
|
+
Rack-family request lifecycle support for Julewire integrations.
|
|
4
|
+
|
|
5
|
+
This gem is deliberately framework-neutral. It is the place for Rack request
|
|
6
|
+
primitives that can be shared by Rails, Grape, and other Rack-based adapters.
|
|
7
|
+
Rails and ActiveSupport EventReporter helpers stay in `julewire-rails_support`.
|
|
8
|
+
|
|
9
|
+
It owns capture helpers for request bodies, response bodies, headers, content
|
|
10
|
+
types, and JSON-ish payload extraction. It does not install middleware by
|
|
11
|
+
itself; framework gems decide when those primitives belong in a request
|
|
12
|
+
pipeline.
|
|
13
|
+
|
|
14
|
+
Applications normally install a framework adapter, not this support gem. Use it
|
|
15
|
+
directly when writing a Rack-shaped Julewire integration.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/julewire/rack/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "julewire-rack"
|
|
7
|
+
spec.version = Julewire::Rack::VERSION
|
|
8
|
+
spec.authors = ["Alexander Grebennik"]
|
|
9
|
+
spec.email = ["slbug@users.noreply.github.com", "sl.bug.sl@gmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Rack request lifecycle support for Julewire."
|
|
12
|
+
spec.description = "Rack-family support primitives for Julewire request integrations."
|
|
13
|
+
spec.homepage = "https://github.com/slbug/julewire"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
spec.required_ruby_version = ">= 3.4"
|
|
16
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
17
|
+
spec.metadata["source_code_uri"] = "https://github.com/slbug/julewire/tree/main/gems/rack"
|
|
18
|
+
spec.metadata["changelog_uri"] = "https://github.com/slbug/julewire/blob/main/gems/rack/CHANGELOG.md"
|
|
19
|
+
|
|
20
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
21
|
+
|
|
22
|
+
spec.files = Dir.chdir(__dir__) do
|
|
23
|
+
Dir[
|
|
24
|
+
"CHANGELOG.md",
|
|
25
|
+
"LICENSE.txt",
|
|
26
|
+
"README.md",
|
|
27
|
+
"julewire-rack.gemspec",
|
|
28
|
+
"lib/**/*.rb"
|
|
29
|
+
]
|
|
30
|
+
end
|
|
31
|
+
spec.executables = []
|
|
32
|
+
spec.require_paths = ["lib"]
|
|
33
|
+
|
|
34
|
+
spec.add_dependency "julewire-core", ">= 1.0"
|
|
35
|
+
spec.add_dependency "rack", ">= 3.2"
|
|
36
|
+
spec.add_dependency "zeitwerk", ">= 2.8.1"
|
|
37
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Rack
|
|
5
|
+
module Capture
|
|
6
|
+
module BodyContentType
|
|
7
|
+
JSON_ONLY = ["application/json", %r{\Aapplication/.+\+json\z}].freeze
|
|
8
|
+
BINARY_MEDIA_TYPES = %w[
|
|
9
|
+
application/gzip
|
|
10
|
+
application/octet-stream
|
|
11
|
+
application/pdf
|
|
12
|
+
application/x-gzip
|
|
13
|
+
application/zip
|
|
14
|
+
].freeze
|
|
15
|
+
BINARY_MEDIA_PREFIXES = %w[
|
|
16
|
+
audio/
|
|
17
|
+
font/
|
|
18
|
+
image/
|
|
19
|
+
video/
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def allowed?(target, selector:)
|
|
24
|
+
media_type = media_type_for(target)
|
|
25
|
+
return false if binary?(media_type)
|
|
26
|
+
return true if selector == true
|
|
27
|
+
return false unless selector
|
|
28
|
+
|
|
29
|
+
return false if media_type.empty?
|
|
30
|
+
|
|
31
|
+
Array(selector).any? { matches?(media_type, it) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def binary?(media_type)
|
|
35
|
+
value = media_type.to_s
|
|
36
|
+
return true if BINARY_MEDIA_TYPES.include?(value)
|
|
37
|
+
|
|
38
|
+
BINARY_MEDIA_PREFIXES.any? { value.start_with?(it) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def media_type_for(target)
|
|
42
|
+
normalized_media_type(raw_content_type(target))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def raw_content_type(target)
|
|
46
|
+
direct_content_type(target) || header_content_type(target)
|
|
47
|
+
rescue StandardError
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def direct_content_type(target)
|
|
52
|
+
return target.media_type if target.respond_to?(:media_type)
|
|
53
|
+
return target.content_mime_type.to_s if target.respond_to?(:content_mime_type) && target.content_mime_type
|
|
54
|
+
return target.content_type if target.respond_to?(:content_type)
|
|
55
|
+
|
|
56
|
+
target.get_header("CONTENT_TYPE") if target.respond_to?(:get_header)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def header_content_type(target)
|
|
60
|
+
header_value(target.headers, "content-type") if target.respond_to?(:headers)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def header_value(headers, key)
|
|
64
|
+
return unless headers.respond_to?(:[])
|
|
65
|
+
|
|
66
|
+
headers[key] || headers[key.upcase] || headers[key.split("-").map(&:capitalize).join("-")]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def matches?(media_type, matcher)
|
|
70
|
+
case matcher
|
|
71
|
+
when Regexp
|
|
72
|
+
matcher.match?(media_type)
|
|
73
|
+
else
|
|
74
|
+
media_type == normalized_media_type(matcher)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def normalized_media_type(value)
|
|
79
|
+
value.to_s.partition(";").first.strip.downcase
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Rack
|
|
5
|
+
module Capture
|
|
6
|
+
class BufferedResponseBody
|
|
7
|
+
class << self
|
|
8
|
+
def call(response, **) = new(response, **).summary_fields
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(response, content_types:, limit:, mode: Settings::STRING_BODY)
|
|
12
|
+
@response = response
|
|
13
|
+
@content_types = content_types
|
|
14
|
+
@limit = limit
|
|
15
|
+
@mode = mode
|
|
16
|
+
@captured = +""
|
|
17
|
+
@total_bytes = 0
|
|
18
|
+
@truncated = false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def summary_fields
|
|
22
|
+
return {} unless BodyContentType.allowed?(@response, selector: @content_types)
|
|
23
|
+
|
|
24
|
+
body_parts = response_body_parts
|
|
25
|
+
return {} unless body_parts
|
|
26
|
+
|
|
27
|
+
if @limit.nil?
|
|
28
|
+
fields = unlimited_single_part_fields(body_parts)
|
|
29
|
+
return fields if fields
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
capture(body_parts)
|
|
33
|
+
return {} if @total_bytes.zero? && @captured.empty?
|
|
34
|
+
|
|
35
|
+
JsonBody.fields(:response, @captured, bytes: @total_bytes, truncated: @truncated, mode: @mode)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def response_body_parts
|
|
41
|
+
return unless @response.respond_to?(:stream)
|
|
42
|
+
|
|
43
|
+
stream = @response.stream
|
|
44
|
+
return if stream.respond_to?(:to_path)
|
|
45
|
+
return unless stream.respond_to?(:to_ary)
|
|
46
|
+
|
|
47
|
+
stream.to_ary
|
|
48
|
+
rescue StandardError
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def capture(body_parts)
|
|
53
|
+
body_parts.each do |part|
|
|
54
|
+
body = body_string(part)
|
|
55
|
+
next unless body
|
|
56
|
+
|
|
57
|
+
capture_part(body)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def body_string(part)
|
|
62
|
+
return part.to_str if part.respond_to?(:to_str)
|
|
63
|
+
|
|
64
|
+
nil
|
|
65
|
+
rescue StandardError
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def unlimited_single_part_fields(body_parts)
|
|
70
|
+
return unless body_parts.length == 1
|
|
71
|
+
|
|
72
|
+
body = body_string(body_parts.first)
|
|
73
|
+
return {} unless body && !body.empty?
|
|
74
|
+
|
|
75
|
+
JsonBody.fields(:response, body, bytes: body.bytesize, truncated: false, mode: @mode)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def capture_part(body)
|
|
79
|
+
bytesize = body.bytesize
|
|
80
|
+
@total_bytes += bytesize
|
|
81
|
+
|
|
82
|
+
return @captured << body if @limit.nil?
|
|
83
|
+
|
|
84
|
+
remaining = @limit - @captured.bytesize
|
|
85
|
+
if remaining <= 0
|
|
86
|
+
@truncated = true if bytesize.positive?
|
|
87
|
+
return
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
@captured << body.byteslice(0, remaining)
|
|
91
|
+
@truncated = true if bytesize > remaining
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Rack
|
|
5
|
+
module Capture
|
|
6
|
+
class HeaderSelection
|
|
7
|
+
SENSITIVE_HEADERS = %w[
|
|
8
|
+
authorization
|
|
9
|
+
cookie
|
|
10
|
+
proxy-authorization
|
|
11
|
+
set-cookie
|
|
12
|
+
x-api-key
|
|
13
|
+
].freeze
|
|
14
|
+
SENSITIVE_HEADER_SET = SENSITIVE_HEADERS.to_h { [it, true] }.freeze
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def build(selector)
|
|
18
|
+
return new(true) if selector == true
|
|
19
|
+
return unless selector
|
|
20
|
+
|
|
21
|
+
new(Array(selector).to_h { [normalize_name(it), true] })
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def normalize_name(name) = name.to_s.tr("_", "-").downcase
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(selection)
|
|
28
|
+
@selection = selection
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def include?(name)
|
|
32
|
+
@selection == true ? !SENSITIVE_HEADER_SET.key?(name) : @selection.key?(name)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Rack
|
|
5
|
+
module Capture
|
|
6
|
+
module Headers
|
|
7
|
+
class << self
|
|
8
|
+
def request(request, selector:)
|
|
9
|
+
env = request.env if request.respond_to?(:env)
|
|
10
|
+
return {} unless env.respond_to?(:each)
|
|
11
|
+
|
|
12
|
+
selection = HeaderSelection.build(selector)
|
|
13
|
+
return {} unless selection
|
|
14
|
+
|
|
15
|
+
capture_headers(env, selection) { request_header_name(it) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def response(headers, selector:)
|
|
19
|
+
return {} unless headers.respond_to?(:each)
|
|
20
|
+
|
|
21
|
+
selection = HeaderSelection.build(selector)
|
|
22
|
+
return {} unless selection
|
|
23
|
+
|
|
24
|
+
capture_headers(headers, selection) { HeaderSelection.normalize_name(it) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def capture_headers(headers, selection)
|
|
30
|
+
captured = {}
|
|
31
|
+
headers.each do |key, value|
|
|
32
|
+
name = yield key
|
|
33
|
+
next unless name && selection.include?(name)
|
|
34
|
+
|
|
35
|
+
captured[name] = header_value(value)
|
|
36
|
+
end
|
|
37
|
+
captured
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def request_header_name(key)
|
|
41
|
+
name = key.to_s
|
|
42
|
+
return "content-type" if name == "CONTENT_TYPE"
|
|
43
|
+
return "content-length" if name == "CONTENT_LENGTH"
|
|
44
|
+
return unless name.start_with?("HTTP_")
|
|
45
|
+
|
|
46
|
+
HeaderSelection.normalize_name(name.delete_prefix("HTTP_"))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def header_value(value)
|
|
50
|
+
return value.join(", ") if value.is_a?(Array)
|
|
51
|
+
|
|
52
|
+
value.to_s
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Julewire
|
|
6
|
+
module Rack
|
|
7
|
+
module Capture
|
|
8
|
+
module JsonBody
|
|
9
|
+
BODY_KEYS = { request: :request_body, response: :response_body }.freeze
|
|
10
|
+
BODY_BYTES_KEYS = { request: :request_body_bytes, response: :response_body_bytes }.freeze
|
|
11
|
+
BODY_TRUNCATED_KEYS = { request: :request_body_truncated, response: :response_body_truncated }.freeze
|
|
12
|
+
JSON_KEYS = { request: :request_body_json, response: :response_body_json }.freeze
|
|
13
|
+
PARSE_ERROR_KEYS = { request: :request_body_parse_error, response: :response_body_parse_error }.freeze
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def fields(prefix, body, bytes:, truncated:, mode:)
|
|
17
|
+
fields = {
|
|
18
|
+
BODY_BYTES_KEYS.fetch(prefix) => bytes,
|
|
19
|
+
BODY_TRUNCATED_KEYS.fetch(prefix) => truncated
|
|
20
|
+
}
|
|
21
|
+
return fields_with_raw_body(fields, prefix, body) unless mode == Settings::JSON_BODY
|
|
22
|
+
return fields if truncated
|
|
23
|
+
|
|
24
|
+
append_parsed_fields(fields, prefix, body)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def fields_with_raw_body(fields, prefix, body)
|
|
30
|
+
fields[BODY_KEYS.fetch(prefix)] = body
|
|
31
|
+
fields
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def append_parsed_fields(fields, prefix, body)
|
|
35
|
+
fields[JSON_KEYS.fetch(prefix)] = JSON.parse(body)
|
|
36
|
+
fields
|
|
37
|
+
rescue JSON::ParserError => e
|
|
38
|
+
fields[PARSE_ERROR_KEYS.fetch(prefix)] = e.class.name
|
|
39
|
+
fields
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Rack
|
|
5
|
+
module Capture
|
|
6
|
+
class RequestBody
|
|
7
|
+
class << self
|
|
8
|
+
def call(request, **) = new(request, **).summary_fields
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(request, content_types:, limit:, mode:)
|
|
12
|
+
@request = request
|
|
13
|
+
@content_types = content_types
|
|
14
|
+
@limit = limit
|
|
15
|
+
@mode = mode
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def summary_fields
|
|
19
|
+
return {} unless BodyContentType.allowed?(@request, selector: @content_types)
|
|
20
|
+
|
|
21
|
+
captured, bytes, truncated = capture_body
|
|
22
|
+
return {} if captured.nil? || (captured.empty? && !truncated)
|
|
23
|
+
|
|
24
|
+
JsonBody.fields(:request, captured, bytes: bytes, truncated: truncated, mode: @mode)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def capture_body
|
|
30
|
+
body = bounded_body
|
|
31
|
+
bytes = content_length || body.bytesize
|
|
32
|
+
captured, truncated = capture(body, total_bytes: bytes)
|
|
33
|
+
[captured, bytes, truncated]
|
|
34
|
+
rescue StandardError
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def bounded_body
|
|
39
|
+
return request_body if @limit.nil?
|
|
40
|
+
|
|
41
|
+
length = content_length
|
|
42
|
+
return request_body if length && length <= @limit
|
|
43
|
+
|
|
44
|
+
read_body_stream(limit: @limit + 1)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def request_body
|
|
48
|
+
# Rack/Rails may already buffer raw_post; the byte cap applies after that read.
|
|
49
|
+
return @request.raw_post if @request.respond_to?(:raw_post)
|
|
50
|
+
|
|
51
|
+
read_body_stream(limit: nil)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def read_body_stream(limit:)
|
|
55
|
+
io = @request.body
|
|
56
|
+
original_position = nil
|
|
57
|
+
body = nil
|
|
58
|
+
begin
|
|
59
|
+
original_position = body_stream_position(io)
|
|
60
|
+
body = io.read(limit)
|
|
61
|
+
ensure
|
|
62
|
+
restore_body_stream(io, original_position)
|
|
63
|
+
end
|
|
64
|
+
body.to_str
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def body_stream_position(io)
|
|
68
|
+
io.pos
|
|
69
|
+
rescue StandardError
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def restore_body_stream(io, original_position)
|
|
74
|
+
io.rewind
|
|
75
|
+
rescue StandardError
|
|
76
|
+
nil
|
|
77
|
+
ensure
|
|
78
|
+
restore_body_stream_position(io, original_position)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def restore_body_stream_position(io, original_position)
|
|
82
|
+
return unless original_position
|
|
83
|
+
|
|
84
|
+
io.pos = original_position
|
|
85
|
+
rescue StandardError
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def capture(body, total_bytes:)
|
|
90
|
+
return [body, false] if @limit.nil? || total_bytes <= @limit
|
|
91
|
+
|
|
92
|
+
[body.byteslice(0, @limit), true]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def content_length
|
|
96
|
+
value = @request.content_length if @request.respond_to?(:content_length)
|
|
97
|
+
value = @request.get_header("CONTENT_LENGTH") if value.nil?
|
|
98
|
+
integer = Integer(value)
|
|
99
|
+
integer if integer.positive?
|
|
100
|
+
rescue StandardError
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Rack
|
|
5
|
+
module Capture
|
|
6
|
+
class Settings
|
|
7
|
+
JSON_BODY = :json
|
|
8
|
+
STRING_BODY = :string
|
|
9
|
+
CAPTURE_BODY_VALUES = [false, true, JSON_BODY, "json"].freeze
|
|
10
|
+
private_constant :CAPTURE_BODY_VALUES
|
|
11
|
+
|
|
12
|
+
include Julewire::Core::Integration::Settings
|
|
13
|
+
|
|
14
|
+
setting :body, default: false, predicate: true, validate: :validate_body
|
|
15
|
+
setting :body_bytes, default: 65_536, validate: byte_limit
|
|
16
|
+
setting :body_content_types, default: BodyContentType::JSON_ONLY
|
|
17
|
+
setting :headers, default: false, predicate: true
|
|
18
|
+
|
|
19
|
+
def body_mode
|
|
20
|
+
case body
|
|
21
|
+
when JSON_BODY, "json"
|
|
22
|
+
JSON_BODY
|
|
23
|
+
else
|
|
24
|
+
STRING_BODY
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def enabled? = headers? || body?
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def validate_body(value)
|
|
33
|
+
return value if CAPTURE_BODY_VALUES.include?(value)
|
|
34
|
+
|
|
35
|
+
raise Error, "body must be false, true, or :json"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: julewire-rack
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Alexander Grebennik
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: julewire-core
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rack
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.2'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.2'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: zeitwerk
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: 2.8.1
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: 2.8.1
|
|
54
|
+
description: Rack-family support primitives for Julewire request integrations.
|
|
55
|
+
email:
|
|
56
|
+
- slbug@users.noreply.github.com
|
|
57
|
+
- sl.bug.sl@gmail.com
|
|
58
|
+
executables: []
|
|
59
|
+
extensions: []
|
|
60
|
+
extra_rdoc_files: []
|
|
61
|
+
files:
|
|
62
|
+
- CHANGELOG.md
|
|
63
|
+
- LICENSE.txt
|
|
64
|
+
- README.md
|
|
65
|
+
- julewire-rack.gemspec
|
|
66
|
+
- lib/julewire-rack.rb
|
|
67
|
+
- lib/julewire/rack.rb
|
|
68
|
+
- lib/julewire/rack/capture/body_content_type.rb
|
|
69
|
+
- lib/julewire/rack/capture/buffered_response_body.rb
|
|
70
|
+
- lib/julewire/rack/capture/header_selection.rb
|
|
71
|
+
- lib/julewire/rack/capture/headers.rb
|
|
72
|
+
- lib/julewire/rack/capture/json_body.rb
|
|
73
|
+
- lib/julewire/rack/capture/request_body.rb
|
|
74
|
+
- lib/julewire/rack/capture/settings.rb
|
|
75
|
+
- lib/julewire/rack/version.rb
|
|
76
|
+
homepage: https://github.com/slbug/julewire
|
|
77
|
+
licenses:
|
|
78
|
+
- MIT
|
|
79
|
+
metadata:
|
|
80
|
+
homepage_uri: https://github.com/slbug/julewire
|
|
81
|
+
source_code_uri: https://github.com/slbug/julewire/tree/main/gems/rack
|
|
82
|
+
changelog_uri: https://github.com/slbug/julewire/blob/main/gems/rack/CHANGELOG.md
|
|
83
|
+
rubygems_mfa_required: 'true'
|
|
84
|
+
rdoc_options: []
|
|
85
|
+
require_paths:
|
|
86
|
+
- lib
|
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
88
|
+
requirements:
|
|
89
|
+
- - ">="
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '3.4'
|
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
97
|
+
requirements: []
|
|
98
|
+
rubygems_version: 4.0.14
|
|
99
|
+
specification_version: 4
|
|
100
|
+
summary: Rack request lifecycle support for Julewire.
|
|
101
|
+
test_files: []
|