aws-sdk-s3 1.68.1 → 1.69.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/lib/aws-sdk-s3.rb +3 -1
  3. data/lib/aws-sdk-s3/bucket.rb +2 -0
  4. data/lib/aws-sdk-s3/bucket_acl.rb +2 -0
  5. data/lib/aws-sdk-s3/bucket_cors.rb +2 -0
  6. data/lib/aws-sdk-s3/bucket_lifecycle.rb +2 -0
  7. data/lib/aws-sdk-s3/bucket_lifecycle_configuration.rb +2 -0
  8. data/lib/aws-sdk-s3/bucket_logging.rb +2 -0
  9. data/lib/aws-sdk-s3/bucket_notification.rb +2 -0
  10. data/lib/aws-sdk-s3/bucket_policy.rb +2 -0
  11. data/lib/aws-sdk-s3/bucket_region_cache.rb +2 -0
  12. data/lib/aws-sdk-s3/bucket_request_payment.rb +2 -0
  13. data/lib/aws-sdk-s3/bucket_tagging.rb +2 -0
  14. data/lib/aws-sdk-s3/bucket_versioning.rb +2 -0
  15. data/lib/aws-sdk-s3/bucket_website.rb +2 -0
  16. data/lib/aws-sdk-s3/client.rb +3 -1
  17. data/lib/aws-sdk-s3/client_api.rb +2 -0
  18. data/lib/aws-sdk-s3/customizations.rb +3 -0
  19. data/lib/aws-sdk-s3/customizations/bucket.rb +2 -0
  20. data/lib/aws-sdk-s3/customizations/multipart_upload.rb +2 -0
  21. data/lib/aws-sdk-s3/customizations/object.rb +2 -0
  22. data/lib/aws-sdk-s3/customizations/object_summary.rb +2 -0
  23. data/lib/aws-sdk-s3/customizations/types/list_object_versions_output.rb +2 -0
  24. data/lib/aws-sdk-s3/encryption.rb +2 -0
  25. data/lib/aws-sdk-s3/encryption/client.rb +2 -0
  26. data/lib/aws-sdk-s3/encryption/decrypt_handler.rb +11 -0
  27. data/lib/aws-sdk-s3/encryption/default_cipher_provider.rb +2 -0
  28. data/lib/aws-sdk-s3/encryption/default_key_provider.rb +2 -0
  29. data/lib/aws-sdk-s3/encryption/encrypt_handler.rb +11 -0
  30. data/lib/aws-sdk-s3/encryption/errors.rb +2 -0
  31. data/lib/aws-sdk-s3/encryption/io_auth_decrypter.rb +2 -0
  32. data/lib/aws-sdk-s3/encryption/io_decrypter.rb +8 -1
  33. data/lib/aws-sdk-s3/encryption/io_encrypter.rb +2 -0
  34. data/lib/aws-sdk-s3/encryption/key_provider.rb +2 -0
  35. data/lib/aws-sdk-s3/encryption/kms_cipher_provider.rb +2 -0
  36. data/lib/aws-sdk-s3/encryption/materials.rb +2 -0
  37. data/lib/aws-sdk-s3/encryption/utils.rb +2 -0
  38. data/lib/aws-sdk-s3/encryptionV2/client.rb +378 -0
  39. data/lib/aws-sdk-s3/encryptionV2/decrypt_handler.rb +194 -0
  40. data/lib/aws-sdk-s3/encryptionV2/default_cipher_provider.rb +104 -0
  41. data/lib/aws-sdk-s3/encryptionV2/default_key_provider.rb +38 -0
  42. data/lib/aws-sdk-s3/encryptionV2/encrypt_handler.rb +63 -0
  43. data/lib/aws-sdk-s3/encryptionV2/errors.rb +13 -0
  44. data/lib/aws-sdk-s3/encryptionV2/io_auth_decrypter.rb +56 -0
  45. data/lib/aws-sdk-s3/encryptionV2/io_decrypter.rb +30 -0
  46. data/lib/aws-sdk-s3/encryptionV2/io_encrypter.rb +67 -0
  47. data/lib/aws-sdk-s3/encryptionV2/key_provider.rb +29 -0
  48. data/lib/aws-sdk-s3/encryptionV2/kms_cipher_provider.rb +84 -0
  49. data/lib/aws-sdk-s3/encryptionV2/materials.rb +58 -0
  50. data/lib/aws-sdk-s3/encryptionV2/utils.rb +116 -0
  51. data/lib/aws-sdk-s3/encryption_v2.rb +20 -0
  52. data/lib/aws-sdk-s3/errors.rb +2 -0
  53. data/lib/aws-sdk-s3/event_streams.rb +2 -0
  54. data/lib/aws-sdk-s3/file_downloader.rb +2 -0
  55. data/lib/aws-sdk-s3/file_part.rb +2 -0
  56. data/lib/aws-sdk-s3/file_uploader.rb +2 -0
  57. data/lib/aws-sdk-s3/legacy_signer.rb +2 -0
  58. data/lib/aws-sdk-s3/multipart_file_uploader.rb +2 -0
  59. data/lib/aws-sdk-s3/multipart_stream_uploader.rb +2 -0
  60. data/lib/aws-sdk-s3/multipart_upload.rb +2 -0
  61. data/lib/aws-sdk-s3/multipart_upload_error.rb +2 -0
  62. data/lib/aws-sdk-s3/multipart_upload_part.rb +2 -0
  63. data/lib/aws-sdk-s3/object.rb +2 -0
  64. data/lib/aws-sdk-s3/object_acl.rb +2 -0
  65. data/lib/aws-sdk-s3/object_copier.rb +2 -0
  66. data/lib/aws-sdk-s3/object_multipart_copier.rb +2 -0
  67. data/lib/aws-sdk-s3/object_summary.rb +2 -0
  68. data/lib/aws-sdk-s3/object_version.rb +2 -0
  69. data/lib/aws-sdk-s3/plugins/accelerate.rb +2 -0
  70. data/lib/aws-sdk-s3/plugins/bucket_arn.rb +2 -0
  71. data/lib/aws-sdk-s3/plugins/bucket_dns.rb +2 -0
  72. data/lib/aws-sdk-s3/plugins/bucket_name_restrictions.rb +2 -0
  73. data/lib/aws-sdk-s3/plugins/dualstack.rb +2 -0
  74. data/lib/aws-sdk-s3/plugins/expect_100_continue.rb +2 -0
  75. data/lib/aws-sdk-s3/plugins/get_bucket_location_fix.rb +2 -0
  76. data/lib/aws-sdk-s3/plugins/http_200_errors.rb +2 -0
  77. data/lib/aws-sdk-s3/plugins/iad_regional_endpoint.rb +2 -0
  78. data/lib/aws-sdk-s3/plugins/location_constraint.rb +2 -0
  79. data/lib/aws-sdk-s3/plugins/md5s.rb +2 -0
  80. data/lib/aws-sdk-s3/plugins/redirects.rb +2 -0
  81. data/lib/aws-sdk-s3/plugins/s3_host_id.rb +2 -0
  82. data/lib/aws-sdk-s3/plugins/s3_signer.rb +2 -0
  83. data/lib/aws-sdk-s3/plugins/sse_cpk.rb +2 -0
  84. data/lib/aws-sdk-s3/plugins/url_encoded_keys.rb +2 -0
  85. data/lib/aws-sdk-s3/presigned_post.rb +2 -0
  86. data/lib/aws-sdk-s3/presigner.rb +2 -0
  87. data/lib/aws-sdk-s3/resource.rb +2 -0
  88. data/lib/aws-sdk-s3/types.rb +2 -0
  89. data/lib/aws-sdk-s3/waiters.rb +2 -0
  90. metadata +16 -2
@@ -0,0 +1,194 @@
1
+ require 'base64'
2
+
3
+ module Aws
4
+ module S3
5
+ module EncryptionV2
6
+ # @api private
7
+ class DecryptHandler < Seahorse::Client::Handler
8
+
9
+ V1_ENVELOPE_KEYS = %w(
10
+ x-amz-key
11
+ x-amz-iv
12
+ x-amz-matdesc
13
+ )
14
+
15
+ V2_ENVELOPE_KEYS = %w(
16
+ x-amz-key-v2
17
+ x-amz-iv
18
+ x-amz-cek-alg
19
+ x-amz-wrap-alg
20
+ x-amz-matdesc
21
+ )
22
+
23
+ V2_OPTIONAL_KEYS = %w(x-amz-tag-len)
24
+
25
+ POSSIBLE_ENVELOPE_KEYS = (V1_ENVELOPE_KEYS +
26
+ V2_ENVELOPE_KEYS + V2_OPTIONAL_KEYS).uniq
27
+
28
+ POSSIBLE_WRAPPING_FORMATS = %w(
29
+ AES/GCM
30
+ kms
31
+ kms+context
32
+ RSA-OAEP-SHA1
33
+ )
34
+
35
+ POSSIBLE_ENCRYPTION_FORMATS = %w(
36
+ AES/GCM/NoPadding
37
+ AES/CBC/PKCS5Padding
38
+ AES/CBC/PKCS7Padding
39
+ )
40
+
41
+ def call(context)
42
+ attach_http_event_listeners(context)
43
+ apply_cse_user_agent(context)
44
+ @handler.call(context)
45
+ end
46
+
47
+ private
48
+
49
+ def attach_http_event_listeners(context)
50
+
51
+ context.http_response.on_headers(200) do
52
+ cipher, envelope = decryption_cipher(context)
53
+ decrypter = body_contains_auth_tag?(envelope) ?
54
+ authenticated_decrypter(context, cipher, envelope) :
55
+ IODecrypter.new(cipher, context.http_response.body)
56
+ context.http_response.body = decrypter
57
+ end
58
+
59
+ context.http_response.on_success(200) do
60
+ decrypter = context.http_response.body
61
+ decrypter.finalize
62
+ decrypter.io.rewind if decrypter.io.respond_to?(:rewind)
63
+ context.http_response.body = decrypter.io
64
+ end
65
+
66
+ context.http_response.on_error do
67
+ if context.http_response.body.respond_to?(:io)
68
+ context.http_response.body = context.http_response.body.io
69
+ end
70
+ end
71
+ end
72
+
73
+ def decryption_cipher(context)
74
+ if envelope = get_encryption_envelope(context)
75
+ [context[:encryption][:cipher_provider].decryption_cipher(envelope),
76
+ envelope]
77
+ else
78
+ raise Errors::DecryptionError, "unable to locate encryption envelope"
79
+ end
80
+ end
81
+
82
+ def get_encryption_envelope(context)
83
+ if context[:encryption][:envelope_location] == :metadata
84
+ envelope_from_metadata(context) || envelope_from_instr_file(context)
85
+ else
86
+ envelope_from_instr_file(context) || envelope_from_metadata(context)
87
+ end
88
+ end
89
+
90
+ def envelope_from_metadata(context)
91
+ possible_envelope = {}
92
+ POSSIBLE_ENVELOPE_KEYS.each do |suffix|
93
+ if value = context.http_response.headers["x-amz-meta-#{suffix}"]
94
+ possible_envelope[suffix] = value
95
+ end
96
+ end
97
+ extract_envelope(possible_envelope)
98
+ end
99
+
100
+ def envelope_from_instr_file(context)
101
+ suffix = context[:encryption][:instruction_file_suffix]
102
+ possible_envelope = Json.load(context.client.get_object(
103
+ bucket: context.params[:bucket],
104
+ key: context.params[:key] + suffix
105
+ ).body.read)
106
+ extract_envelope(possible_envelope)
107
+ rescue S3::Errors::ServiceError, Json::ParseError
108
+ nil
109
+ end
110
+
111
+ def extract_envelope(hash)
112
+ return nil unless hash
113
+ return v1_envelope(hash) if hash.key?('x-amz-key')
114
+ return v2_envelope(hash) if hash.key?('x-amz-key-v2')
115
+ if hash.keys.any? { |key| key.match(/^x-amz-key-(.+)$/) }
116
+ msg = "unsupported envelope encryption version #{$1}"
117
+ raise Errors::DecryptionError, msg
118
+ end
119
+ end
120
+
121
+ def v1_envelope(envelope)
122
+ envelope
123
+ end
124
+
125
+ def v2_envelope(envelope)
126
+ unless POSSIBLE_ENCRYPTION_FORMATS.include? envelope['x-amz-cek-alg']
127
+ alg = envelope['x-amz-cek-alg'].inspect
128
+ msg = "unsupported content encrypting key (cek) format: #{alg}"
129
+ raise Errors::DecryptionError, msg
130
+ end
131
+ unless POSSIBLE_WRAPPING_FORMATS.include? envelope['x-amz-wrap-alg']
132
+ alg = envelope['x-amz-wrap-alg'].inspect
133
+ msg = "unsupported key wrapping algorithm: #{alg}"
134
+ raise Errors::DecryptionError, msg
135
+ end
136
+ unless (missing_keys = V2_ENVELOPE_KEYS - envelope.keys).empty?
137
+ msg = "incomplete v2 encryption envelope:\n"
138
+ msg += " missing: #{missing_keys.join(',')}\n"
139
+ raise Errors::DecryptionError, msg
140
+ end
141
+ envelope
142
+ end
143
+
144
+ # When the x-amz-meta-x-amz-tag-len header is present, it indicates
145
+ # that the body of this object has a trailing auth tag. The header
146
+ # indicates the length of that tag.
147
+ #
148
+ # This method fetches the tag from the end of the object by
149
+ # making a GET Object w/range request. This auth tag is used
150
+ # to initialize the cipher, and the decrypter truncates the
151
+ # auth tag from the body when writing the final bytes.
152
+ def authenticated_decrypter(context, cipher, envelope)
153
+ if RUBY_VERSION.match(/1.9/)
154
+ raise "authenticated decryption not supported by OpenSSL in Ruby version ~> 1.9"
155
+ raise Aws::Errors::NonSupportedRubyVersionError, msg
156
+ end
157
+ http_resp = context.http_response
158
+ content_length = http_resp.headers['content-length'].to_i
159
+ auth_tag_length = envelope['x-amz-tag-len']
160
+ auth_tag_length = auth_tag_length.to_i / 8
161
+
162
+ auth_tag = context.client.get_object(
163
+ bucket: context.params[:bucket],
164
+ key: context.params[:key],
165
+ range: "bytes=-#{auth_tag_length}"
166
+ ).body.read
167
+
168
+ cipher.auth_tag = auth_tag
169
+ cipher.auth_data = ''
170
+
171
+ # The encrypted object contains both the cipher text
172
+ # plus a trailing auth tag.
173
+ IOAuthDecrypter.new(
174
+ io: http_resp.body,
175
+ encrypted_content_length: content_length - auth_tag_length,
176
+ cipher: cipher)
177
+ end
178
+
179
+ def body_contains_auth_tag?(envelope)
180
+ envelope.include? 'x-amz-tag-len'
181
+ end
182
+
183
+ def apply_cse_user_agent(context)
184
+ if context.config.user_agent_suffix.nil?
185
+ context.config.user_agent_suffix = 'CSE_V2'
186
+ elsif !context.config.user_agent_suffix.include? 'CSE_V2'
187
+ context.config.user_agent_suffix += ' CSE_V2'
188
+ end
189
+ end
190
+
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,104 @@
1
+ require 'base64'
2
+
3
+ module Aws
4
+ module S3
5
+ module EncryptionV2
6
+ # @api private
7
+ class DefaultCipherProvider
8
+
9
+ def initialize(options = {})
10
+ @key_provider = options[:key_provider]
11
+ end
12
+
13
+ # @return [Array<Hash,Cipher>] Creates an returns a new encryption
14
+ # envelope and encryption cipher.
15
+ def encryption_cipher
16
+ cipher = Utils.aes_encryption_cipher(:GCM)
17
+ cek_alg = 'AES/GCM/NoPadding'
18
+ if @key_provider.encryption_materials.key.is_a? OpenSSL::PKey::RSA
19
+ wrap_alg = 'RSA-OAEP-SHA1'
20
+ enc_key = encode64(encrypt_rsa(envelope_key(cipher), cek_alg))
21
+ else
22
+ wrap_alg = 'AES/GCM'
23
+ enc_key = encode64(encrypt_aes_gcm(envelope_key(cipher), cek_alg))
24
+ end
25
+ envelope = {
26
+ 'x-amz-key-v2' => enc_key,
27
+ 'x-amz-cek-alg' => cek_alg,
28
+ 'x-amz-tag-len' => 16 * 8,
29
+ 'x-amz-wrap-alg' => wrap_alg,
30
+ 'x-amz-iv' => encode64(envelope_iv(cipher)),
31
+ 'x-amz-matdesc' => materials_description,
32
+ }
33
+ cipher.auth_data = '' # auth_data must be set after key and iv
34
+ [envelope, cipher]
35
+ end
36
+
37
+ # @return [Cipher] Given an encryption envelope, returns a
38
+ # decryption cipher.
39
+ def decryption_cipher(envelope)
40
+ if envelope.key? 'x-amz-key'
41
+ # Support for decryption of legacy objects
42
+ master_key = @key_provider.key_for(envelope['x-amz-matdesc'])
43
+ key = Utils.decrypt(master_key, decode64(envelope['x-amz-key']))
44
+ iv = decode64(envelope['x-amz-iv'])
45
+ Utils.aes_decryption_cipher(:CBC, key, iv)
46
+ else
47
+ master_key = @key_provider.key_for(envelope['x-amz-matdesc'])
48
+ if envelope['x-amz-cek-alg'] != 'AES/GCM/NoPadding'
49
+ raise ArgumentError, 'Unsupported cek-alg: ' \
50
+ "#{envelope['x-amz-cek-alg']}"
51
+ end
52
+ key =
53
+ case envelope['x-amz-wrap-alg']
54
+ when 'AES/GCM'
55
+ Utils.decrypt_aes_gcm(master_key,
56
+ decode64(envelope['x-amz-key-v2']),
57
+ envelope['x-amz-cek-alg'])
58
+ when 'RSA-OAEP-SHA1'
59
+ key, cek_alg = Utils.decrypt_rsa(master_key, decode64(envelope['x-amz-key-v2']))
60
+ raise Errors::DecryptionError unless cek_alg == envelope['x-amz-cek-alg']
61
+ key
62
+ else
63
+ raise ArgumentError, 'Unsupported wrap-alg: ' \
64
+ "#{envelope['x-amz-wrap-alg']}"
65
+ end
66
+ iv = decode64(envelope['x-amz-iv'])
67
+ Utils.aes_decryption_cipher(:GCM, key, iv)
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def envelope_key(cipher)
74
+ cipher.key = cipher.random_key
75
+ end
76
+
77
+ def envelope_iv(cipher)
78
+ cipher.iv = cipher.random_iv
79
+ end
80
+
81
+ def encrypt_aes_gcm(data, auth_data)
82
+ Utils.encrypt_aes_gcm(@key_provider.encryption_materials.key, data, auth_data)
83
+ end
84
+
85
+ def encrypt_rsa(data, auth_data)
86
+ Utils.encrypt_rsa(@key_provider.encryption_materials.key, data, auth_data)
87
+ end
88
+
89
+ def materials_description
90
+ @key_provider.encryption_materials.description
91
+ end
92
+
93
+ def encode64(str)
94
+ Base64.encode64(str).split("\n") * ''
95
+ end
96
+
97
+ def decode64(str)
98
+ Base64.decode64(str)
99
+ end
100
+
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,38 @@
1
+ module Aws
2
+ module S3
3
+ module EncryptionV2
4
+
5
+ # The default key provider is constructed with a single key
6
+ # that is used for both encryption and decryption, ignoring
7
+ # the possible per-object envelope encryption materials description.
8
+ # @api private
9
+ class DefaultKeyProvider
10
+
11
+ include KeyProvider
12
+
13
+ # @option options [required, OpenSSL::PKey::RSA, String] :encryption_key
14
+ # The master key to use for encrypting objects.
15
+ # @option options [String<JSON>] :materials_description ('{}')
16
+ # A description of the encryption key.
17
+ def initialize(options = {})
18
+ @encryption_materials = Materials.new(
19
+ key: options[:encryption_key],
20
+ description: options[:materials_description] || '{}'
21
+ )
22
+ end
23
+
24
+ # @return [Materials]
25
+ def encryption_materials
26
+ @encryption_materials
27
+ end
28
+
29
+ # @param [String<JSON>] materials_description
30
+ # @return Returns the key given in the constructor.
31
+ def key_for(materials_description)
32
+ @encryption_materials.key
33
+ end
34
+
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,63 @@
1
+ require 'base64'
2
+
3
+ module Aws
4
+ module S3
5
+ module EncryptionV2
6
+ # @api private
7
+ class EncryptHandler < Seahorse::Client::Handler
8
+
9
+ def call(context)
10
+ if RUBY_VERSION.match(/1.9/)
11
+ raise "authenticated encryption not supported by OpenSSL in Ruby version ~> 1.9"
12
+ raise Aws::Errors::NonSupportedRubyVersionError, msg
13
+ end
14
+ envelope, cipher = context[:encryption][:cipher_provider].encryption_cipher
15
+ context[:encryption][:cipher] = cipher
16
+ apply_encryption_envelope(context, envelope)
17
+ apply_encryption_cipher(context, cipher)
18
+ apply_cse_user_agent(context)
19
+ @handler.call(context)
20
+ end
21
+
22
+ private
23
+
24
+ def apply_encryption_envelope(context, envelope)
25
+ if context[:encryption][:envelope_location] == :instruction_file
26
+ suffix = context[:encryption][:instruction_file_suffix]
27
+ context.client.put_object(
28
+ bucket: context.params[:bucket],
29
+ key: context.params[:key] + suffix,
30
+ body: Json.dump(envelope)
31
+ )
32
+ else # :metadata
33
+ context.params[:metadata] ||= {}
34
+ context.params[:metadata].update(envelope)
35
+ end
36
+ end
37
+
38
+ def apply_encryption_cipher(context, cipher)
39
+ io = context.params[:body] || ''
40
+ io = StringIO.new(io) if io.is_a? String
41
+ context.params[:body] = IOEncrypter.new(cipher, io)
42
+ context.params[:metadata] ||= {}
43
+ context.params[:metadata]['x-amz-unencrypted-content-length'] = io.size
44
+ if context.params.delete(:content_md5)
45
+ raise ArgumentError, 'content_md5 is not supported'
46
+ end
47
+ context.http_response.on_headers do
48
+ context.params[:body].close
49
+ end
50
+ end
51
+
52
+ def apply_cse_user_agent(context)
53
+ if context.config.user_agent_suffix.nil?
54
+ context.config.user_agent_suffix = 'CSE_V2'
55
+ elsif !context.config.user_agent_suffix.include? 'CSE_V2'
56
+ context.config.user_agent_suffix += ' CSE_V2'
57
+ end
58
+ end
59
+
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,13 @@
1
+ module Aws
2
+ module S3
3
+ module EncryptionV2
4
+ module Errors
5
+
6
+ class DecryptionError < RuntimeError; end
7
+
8
+ class EncryptionError < RuntimeError; end
9
+
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,56 @@
1
+ module Aws
2
+ module S3
3
+ module EncryptionV2
4
+ # @api private
5
+ class IOAuthDecrypter
6
+
7
+ # @option options [required, IO#write] :io
8
+ # An IO-like object that responds to {#write}.
9
+ # @option options [required, Integer] :encrypted_content_length
10
+ # The number of bytes to decrypt from the `:io` object.
11
+ # This should be the total size of `:io` minus the length of
12
+ # the cipher auth tag.
13
+ # @option options [required, OpenSSL::Cipher] :cipher An initialized
14
+ # cipher that can be used to decrypt the bytes as they are
15
+ # written to the `:io` object. The cipher should already have
16
+ # its `#auth_tag` set.
17
+ def initialize(options = {})
18
+ @decrypter = IODecrypter.new(options[:cipher], options[:io])
19
+ @max_bytes = options[:encrypted_content_length]
20
+ @bytes_written = 0
21
+ end
22
+
23
+ def write(chunk)
24
+ chunk = truncate_chunk(chunk)
25
+ if chunk.bytesize > 0
26
+ @bytes_written += chunk.bytesize
27
+ @decrypter.write(chunk)
28
+ end
29
+ end
30
+
31
+ def finalize
32
+ @decrypter.finalize
33
+ end
34
+
35
+ def io
36
+ @decrypter.io
37
+ end
38
+
39
+ private
40
+
41
+ def truncate_chunk(chunk)
42
+ if chunk.bytesize + @bytes_written <= @max_bytes
43
+ chunk
44
+ elsif @bytes_written < @max_bytes
45
+ chunk[0..(@max_bytes - @bytes_written - 1)]
46
+ else
47
+ # If the tag was sent over after the full body has been read,
48
+ # we don't want to accidentally append it.
49
+ ""
50
+ end
51
+ end
52
+
53
+ end
54
+ end
55
+ end
56
+ end