iostreams 1.11.0 → 2.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 +4 -4
- data/README.md +20 -2
- data/Rakefile +7 -0
- data/lib/io_streams/builder.rb +9 -9
- data/lib/io_streams/bzip2/writer.rb +1 -1
- data/lib/io_streams/encode/reader.rb +2 -2
- data/lib/io_streams/encode/writer.rb +5 -5
- data/lib/io_streams/gzip/reader.rb +1 -1
- data/lib/io_streams/gzip/writer.rb +1 -1
- data/lib/io_streams/io_streams.rb +45 -19
- data/lib/io_streams/line/reader.rb +2 -2
- data/lib/io_streams/line/writer.rb +1 -1
- data/lib/io_streams/path.rb +2 -2
- data/lib/io_streams/paths/file.rb +10 -10
- data/lib/io_streams/paths/http.rb +80 -7
- data/lib/io_streams/paths/matcher.rb +3 -3
- data/lib/io_streams/paths/s3.rb +3 -3
- data/lib/io_streams/paths/sftp.rb +7 -8
- data/lib/io_streams/pgp/reader.rb +23 -10
- data/lib/io_streams/pgp/writer.rb +93 -32
- data/lib/io_streams/pgp.rb +188 -60
- data/lib/io_streams/reader.rb +4 -4
- data/lib/io_streams/record/reader.rb +3 -4
- data/lib/io_streams/record/writer.rb +3 -4
- data/lib/io_streams/row/reader.rb +1 -1
- data/lib/io_streams/row/writer.rb +1 -1
- data/lib/io_streams/stream.rb +36 -30
- data/lib/io_streams/symmetric_encryption/reader.rb +2 -2
- data/lib/io_streams/symmetric_encryption/writer.rb +4 -4
- data/lib/io_streams/tabular/header.rb +18 -6
- data/lib/io_streams/tabular/parser/array.rb +0 -10
- data/lib/io_streams/tabular/parser/csv.rb +6 -38
- data/lib/io_streams/tabular/parser/fixed.rb +5 -5
- data/lib/io_streams/tabular/parser/psv.rb +0 -12
- data/lib/io_streams/tabular.rb +5 -10
- data/lib/io_streams/utils.rb +3 -2
- data/lib/io_streams/version.rb +1 -1
- data/lib/io_streams/writer.rb +6 -6
- data/lib/io_streams/xlsx/reader.rb +1 -1
- data/lib/io_streams/zip/writer.rb +22 -10
- data/lib/iostreams.rb +0 -1
- metadata +28 -111
- data/lib/io_streams/deprecated.rb +0 -216
- data/lib/io_streams/tabular/utility/csv_row.rb +0 -105
- data/test/builder_test.rb +0 -311
- data/test/bzip2_reader_test.rb +0 -27
- data/test/bzip2_writer_test.rb +0 -56
- data/test/deprecated_test.rb +0 -121
- data/test/encode_reader_test.rb +0 -51
- data/test/encode_writer_test.rb +0 -90
- data/test/files/embedded_lines_test.csv +0 -7
- data/test/files/multiple_files.zip +0 -0
- data/test/files/spreadsheet.xlsx +0 -0
- data/test/files/test.csv +0 -4
- data/test/files/test.json +0 -3
- data/test/files/test.psv +0 -4
- data/test/files/text file.txt +0 -3
- data/test/files/text.txt +0 -3
- data/test/files/text.txt.bz2 +0 -0
- data/test/files/text.txt.gz +0 -0
- data/test/files/text.txt.gz.zip +0 -0
- data/test/files/text.zip +0 -0
- data/test/files/text.zip.gz +0 -0
- data/test/files/unclosed_quote_large_test.csv +0 -1658
- data/test/files/unclosed_quote_test.csv +0 -4
- data/test/files/unclosed_quote_test2.csv +0 -3
- data/test/gzip_reader_test.rb +0 -27
- data/test/gzip_writer_test.rb +0 -52
- data/test/io_streams_test.rb +0 -132
- data/test/line_reader_test.rb +0 -325
- data/test/line_writer_test.rb +0 -59
- data/test/minimal_file_reader.rb +0 -25
- data/test/path_test.rb +0 -55
- data/test/paths/file_test.rb +0 -213
- data/test/paths/http_test.rb +0 -34
- data/test/paths/matcher_test.rb +0 -120
- data/test/paths/s3_test.rb +0 -220
- data/test/paths/sftp_test.rb +0 -106
- data/test/pgp_reader_test.rb +0 -46
- data/test/pgp_test.rb +0 -267
- data/test/pgp_writer_test.rb +0 -130
- data/test/record_reader_test.rb +0 -60
- data/test/record_writer_test.rb +0 -82
- data/test/row_reader_test.rb +0 -35
- data/test/row_writer_test.rb +0 -56
- data/test/stream_test.rb +0 -577
- data/test/tabular_test.rb +0 -338
- data/test/test_helper.rb +0 -40
- data/test/utils_test.rb +0 -20
- data/test/xlsx_reader_test.rb +0 -37
- data/test/zip_reader_test.rb +0 -53
- data/test/zip_writer_test.rb +0 -48
|
@@ -23,8 +23,6 @@ module IOStreams
|
|
|
23
23
|
# output.write('Hello World')
|
|
24
24
|
# end
|
|
25
25
|
class SFTP < IOStreams::Path
|
|
26
|
-
include SemanticLogger::Loggable if defined?(SemanticLogger)
|
|
27
|
-
|
|
28
26
|
class << self
|
|
29
27
|
attr_accessor :sshpass_bin, :sftp_bin, :sshpass_wait_seconds, :before_password_wait_seconds
|
|
30
28
|
end
|
|
@@ -195,7 +193,7 @@ module IOStreams
|
|
|
195
193
|
# Give time for password to be processed and stdin to be passed to sftp process.
|
|
196
194
|
sleep self.class.sshpass_wait_seconds
|
|
197
195
|
|
|
198
|
-
writer.puts "get #{remote_file_name} #{local_file_name}"
|
|
196
|
+
writer.puts "get #{remote_file_name.inspect} #{local_file_name.inspect}"
|
|
199
197
|
writer.puts "bye"
|
|
200
198
|
writer.close
|
|
201
199
|
out = reader.read.chomp
|
|
@@ -311,7 +309,7 @@ module IOStreams
|
|
|
311
309
|
|
|
312
310
|
def build_ssh_options
|
|
313
311
|
options = ssh_options.dup
|
|
314
|
-
options[:logger] ||= logger if
|
|
312
|
+
options[:logger] ||= IOStreams.logger if IOStreams.logger
|
|
315
313
|
options[:port] ||= port
|
|
316
314
|
options[:max_pkt_size] ||= 65_536
|
|
317
315
|
options[:password] ||= @password
|
|
@@ -319,15 +317,16 @@ module IOStreams
|
|
|
319
317
|
end
|
|
320
318
|
|
|
321
319
|
def map_log_level
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
case logger.level
|
|
320
|
+
level = IOStreams.logger&.level
|
|
321
|
+
case level
|
|
325
322
|
when :trace
|
|
326
323
|
"DEBUG3"
|
|
327
324
|
when :warn
|
|
328
325
|
"ERROR"
|
|
326
|
+
when Symbol
|
|
327
|
+
level.to_s
|
|
329
328
|
else
|
|
330
|
-
|
|
329
|
+
"INFO"
|
|
331
330
|
end
|
|
332
331
|
end
|
|
333
332
|
end
|
|
@@ -20,23 +20,36 @@ module IOStreams
|
|
|
20
20
|
# Name of file to read from
|
|
21
21
|
#
|
|
22
22
|
# passphrase: [String]
|
|
23
|
-
# Pass phrase for private key to decrypt the file with
|
|
24
|
-
|
|
23
|
+
# Pass phrase for private key to decrypt the file with.
|
|
24
|
+
# Not required when the file is signed but not encrypted.
|
|
25
|
+
#
|
|
26
|
+
# ignore_mdc_error: [true|false]
|
|
27
|
+
# Decrypt files that lack MDC (Modification Detection Code) integrity protection.
|
|
28
|
+
# Some legacy/enterprise systems (e.g. Workday) still produce such files, which
|
|
29
|
+
# modern GnuPG refuses to decrypt with `gpg: decryption forced to fail!`.
|
|
30
|
+
# Only enable this for files from a trusted source: without MDC the decrypted
|
|
31
|
+
# contents are not protected against tampering.
|
|
32
|
+
# Default: false
|
|
33
|
+
def self.file(file_name, passphrase: nil, ignore_mdc_error: false)
|
|
25
34
|
# Cannot use `passphrase: self.default_passphrase` since it is considered private
|
|
26
35
|
passphrase ||= default_passphrase
|
|
27
|
-
raise(ArgumentError, "Missing both passphrase and IOStreams::Pgp::Reader.default_passphrase") unless passphrase
|
|
28
36
|
|
|
37
|
+
args = []
|
|
29
38
|
# Use --pinentry-mode loopback for all GnuPG versions >= 2.1
|
|
30
|
-
loopback
|
|
31
|
-
|
|
39
|
+
args += ["--pinentry-mode", "loopback"] if IOStreams::Pgp.pgp_version.to_f >= 2.1
|
|
32
40
|
# Use --no-symkey-cache for GnuPG versions >= 2.4 to avoid caching session keys
|
|
33
|
-
|
|
41
|
+
args << "--no-symkey-cache" if IOStreams::Pgp.pgp_version.to_f >= 2.4
|
|
42
|
+
args << "--ignore-mdc-error" if ignore_mdc_error
|
|
43
|
+
args += ["--batch", "--no-tty", "--yes", "--decrypt"]
|
|
44
|
+
# Only feed a passphrase when one is supplied; sign-only files need none.
|
|
45
|
+
args += ["--passphrase-fd", "0"] if passphrase
|
|
46
|
+
args << file_name.to_s
|
|
34
47
|
|
|
35
|
-
command
|
|
36
|
-
IOStreams
|
|
48
|
+
command = IOStreams::Pgp.gpg_command(*args)
|
|
49
|
+
IOStreams.logger&.debug { "IOStreams::Pgp::Reader.open: #{command.shelljoin}" }
|
|
37
50
|
|
|
38
51
|
# Read decrypted contents from stdout
|
|
39
|
-
Open3.popen3(command) do |stdin, stdout, stderr, waith_thr|
|
|
52
|
+
Open3.popen3(*command) do |stdin, stdout, stderr, waith_thr|
|
|
40
53
|
stdin.puts(passphrase) if passphrase
|
|
41
54
|
stdin.close
|
|
42
55
|
result =
|
|
@@ -54,4 +67,4 @@ module IOStreams
|
|
|
54
67
|
end
|
|
55
68
|
end
|
|
56
69
|
end
|
|
57
|
-
end
|
|
70
|
+
end
|
|
@@ -26,18 +26,42 @@ module IOStreams
|
|
|
26
26
|
@audit_recipient = nil
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
# Write to a PGP / GPG file, encrypting the contents as it is written.
|
|
29
|
+
# Write to a PGP / GPG file, encrypting and/or signing the contents as it is written.
|
|
30
30
|
#
|
|
31
31
|
# file_name: [String]
|
|
32
32
|
# Name of file to write to.
|
|
33
33
|
#
|
|
34
|
+
# encrypt: [true|false]
|
|
35
|
+
# Whether to encrypt the file for the supplied recipient(s).
|
|
36
|
+
# When set to false the file is signed but not encrypted, in which case a
|
|
37
|
+
# :signer must be supplied and :recipient / :import_and_trust_key are ignored.
|
|
38
|
+
# Default: true
|
|
39
|
+
#
|
|
34
40
|
# recipient: [String|Array<String>]
|
|
35
41
|
# One or more emails of users for which to encrypt the file.
|
|
42
|
+
# Ignored when encrypt is false.
|
|
36
43
|
#
|
|
37
44
|
# import_and_trust_key: [String|Array<String>]
|
|
38
45
|
# One or more pgp keys to import and then use to encrypt the file.
|
|
39
46
|
# Note: Ascii Keys can contain multiple keys, only the last one in the file is used.
|
|
40
47
|
#
|
|
48
|
+
# import_and_trust_level: [Integer]
|
|
49
|
+
# The owner-trust level to assign to keys supplied via :import_and_trust_key.
|
|
50
|
+
# 1 : Undefined (no opinion)
|
|
51
|
+
# 2 : Never (do not trust)
|
|
52
|
+
# 3 : Marginal
|
|
53
|
+
# 4 : Full
|
|
54
|
+
# 5 : Ultimate
|
|
55
|
+
# Default: 5 : Ultimate
|
|
56
|
+
#
|
|
57
|
+
# SECURITY WARNING:
|
|
58
|
+
# Only import and trust keys received from a verified, trusted source.
|
|
59
|
+
# The default trust level is `5` (Ultimate), which tells GPG to treat the imported key
|
|
60
|
+
# as if it were one of your own keys. An ultimately trusted key is implicitly valid and
|
|
61
|
+
# can in turn confer validity on other keys it has signed. Importing an attacker supplied
|
|
62
|
+
# key at this level allows that attacker to impersonate other recipients.
|
|
63
|
+
# When the key cannot be fully verified, supply a lower `import_and_trust_level`.
|
|
64
|
+
#
|
|
41
65
|
# signer: [String]
|
|
42
66
|
# Name of user with which to sign the encypted file.
|
|
43
67
|
# Default: default_signer or do not sign.
|
|
@@ -55,63 +79,100 @@ module IOStreams
|
|
|
55
79
|
# compress_level: [Integer]
|
|
56
80
|
# Compression level
|
|
57
81
|
# Default: 6
|
|
82
|
+
#
|
|
83
|
+
# Note: There is intentionally no option here to disable MDC (Modification Detection
|
|
84
|
+
# Code) integrity protection on the files we produce. The reader exposes
|
|
85
|
+
# `ignore_mdc_error:` so we can *consume* legacy files that lack MDC (see Reader),
|
|
86
|
+
# but we never want to *generate* them: MDC is what protects the encrypted contents
|
|
87
|
+
# against tampering, and modern GnuPG mandates it for current ciphers anyway
|
|
88
|
+
# (`--disable-mdc` is a no-op unless an obsolete cipher is forced). Omitting MDC on
|
|
89
|
+
# output would only weaken files we create, with no upside for this library.
|
|
58
90
|
def self.file(file_name,
|
|
91
|
+
encrypt: true,
|
|
59
92
|
recipient: nil,
|
|
60
93
|
import_and_trust_key: nil,
|
|
94
|
+
import_and_trust_level: 5,
|
|
61
95
|
signer: default_signer,
|
|
62
96
|
signer_passphrase: default_signer_passphrase,
|
|
63
97
|
compress: :zip,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
# Backward compatibility
|
|
71
|
-
compress = compression if compression
|
|
98
|
+
compress_level: 6)
|
|
99
|
+
if encrypt
|
|
100
|
+
raise(ArgumentError, "Requires either :recipient or :import_and_trust_key") unless recipient || import_and_trust_key
|
|
101
|
+
elsif !signer
|
|
102
|
+
raise(ArgumentError, "Requires a :signer when encrypt is false")
|
|
103
|
+
end
|
|
72
104
|
|
|
73
105
|
compress_level = 0 if compress == :none
|
|
74
106
|
|
|
75
|
-
recipients =
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
107
|
+
recipients =
|
|
108
|
+
if encrypt
|
|
109
|
+
collect_recipients(recipient, import_and_trust_key, import_and_trust_level)
|
|
110
|
+
else
|
|
111
|
+
[]
|
|
112
|
+
end
|
|
81
113
|
|
|
82
|
-
# Write to stdin, with encrypted contents being written to the file
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
command << " -o \"#{file_name}\""
|
|
114
|
+
# Write to stdin, with the encrypted and/or signed contents being written to the file
|
|
115
|
+
args = build_args(
|
|
116
|
+
file_name: file_name,
|
|
117
|
+
encrypt: encrypt,
|
|
118
|
+
signer: signer,
|
|
119
|
+
signer_passphrase: signer_passphrase,
|
|
120
|
+
compress: compress,
|
|
121
|
+
compress_level: compress_level,
|
|
122
|
+
recipients: recipients
|
|
123
|
+
)
|
|
124
|
+
command = IOStreams::Pgp.gpg_command(*args)
|
|
94
125
|
|
|
95
|
-
|
|
126
|
+
# Do not log the command, it may contain the signer passphrase.
|
|
127
|
+
action = encrypt ? "encrypt" : "sign"
|
|
128
|
+
IOStreams.logger&.debug { "IOStreams::Pgp::Writer.open: #{action} -o #{file_name}" }
|
|
96
129
|
|
|
97
130
|
result = nil
|
|
98
|
-
Open3.popen2e(command) do |stdin, out, waith_thr|
|
|
131
|
+
Open3.popen2e(*command) do |stdin, out, waith_thr|
|
|
99
132
|
begin
|
|
100
133
|
stdin.binmode
|
|
101
134
|
result = yield(stdin)
|
|
102
135
|
stdin.close
|
|
103
136
|
rescue Errno::EPIPE
|
|
104
137
|
# Ignore broken pipe because gpg terminates early due to an error
|
|
105
|
-
::
|
|
138
|
+
::FileUtils.rm_f(file_name)
|
|
106
139
|
raise(Pgp::Failure, "GPG Failed writing to encrypted file: #{file_name}: #{out.read.chomp}")
|
|
107
140
|
end
|
|
108
141
|
unless waith_thr.value.success?
|
|
109
|
-
::
|
|
142
|
+
::FileUtils.rm_f(file_name)
|
|
110
143
|
raise(Pgp::Failure, "GPG Failed to create encrypted file: #{file_name}: #{out.read.chomp}")
|
|
111
144
|
end
|
|
112
145
|
end
|
|
113
146
|
result
|
|
114
147
|
end
|
|
148
|
+
|
|
149
|
+
def self.build_args(file_name:, encrypt:, signer:, signer_passphrase:, compress:, compress_level:, recipients:)
|
|
150
|
+
args = ["--batch", "--no-tty", "--yes"]
|
|
151
|
+
args << "--encrypt" if encrypt
|
|
152
|
+
args += ["--sign", "--local-user", signer.to_s] if signer
|
|
153
|
+
if signer_passphrase
|
|
154
|
+
args += ["--pinentry-mode", "loopback"] if IOStreams::Pgp.pgp_version.to_f >= 2.1
|
|
155
|
+
args << "--no-symkey-cache" if IOStreams::Pgp.pgp_version.to_f >= 2.4
|
|
156
|
+
args += ["--passphrase", signer_passphrase.to_s]
|
|
157
|
+
end
|
|
158
|
+
args += ["-z", compress_level.to_s] if compress_level != 6
|
|
159
|
+
args += ["--compress-algo", compress.to_s] unless compress == :none
|
|
160
|
+
recipients.each { |address| args += ["--recipient", address.to_s] }
|
|
161
|
+
args += ["-o", file_name.to_s]
|
|
162
|
+
args
|
|
163
|
+
end
|
|
164
|
+
private_class_method :build_args
|
|
165
|
+
|
|
166
|
+
def self.collect_recipients(recipient, import_and_trust_key, import_and_trust_level)
|
|
167
|
+
recipients = Array(recipient)
|
|
168
|
+
recipients << audit_recipient if audit_recipient
|
|
169
|
+
|
|
170
|
+
Array(import_and_trust_key).each do |key|
|
|
171
|
+
recipients << IOStreams::Pgp.import_and_trust(key: key, trust_level: import_and_trust_level)
|
|
172
|
+
end
|
|
173
|
+
recipients
|
|
174
|
+
end
|
|
175
|
+
private_class_method :collect_recipients
|
|
115
176
|
end
|
|
116
177
|
end
|
|
117
|
-
end
|
|
178
|
+
end
|