iop 0.1.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/CHANGES.md +3 -0
- data/README.md +90 -0
- data/lib/iop/digest.rb +45 -0
- data/lib/iop/file.rb +209 -0
- data/lib/iop/net/ftp.rb +206 -0
- data/lib/iop/openssl.rb +147 -0
- data/lib/iop/securerandom.rb +49 -0
- data/lib/iop/string.rb +89 -0
- data/lib/iop/zlib.rb +179 -0
- data/lib/iop/zstdlib.rb +104 -0
- data/lib/iop.rb +250 -0
- data/test/digest_test.rb +18 -0
- data/test/io_test.rb +23 -0
- data/test/net_ftp_test.rb +39 -0
- data/test/secureransom_test.rb +16 -0
- data/test/string_test.rb +17 -0
- metadata +70 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2631f9835b20e87513aed68b469ca251815ccd9517c1b6879555ab851bdae02a
|
4
|
+
data.tar.gz: b1a7d1794c962323692117181c1d5485343ed38d3fec65738af5c21076801fd4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d1cbb150db80d8de4aae4caa144618c143c84572949488508ee149b7fdcb8040385c966022a242fea08caf7533a96035fa0ee7e466cc8a583faec70b3e3c48b4
|
7
|
+
data.tar.gz: 42f468cd335d5087d4471b0f17fd190d0b7346611fe6f00da8a3a565c85042ce8a41f0a351cbe8bd40ef316311eaeae5456ec8fa79af71db3461621313435f6c
|
data/CHANGES.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
# [IOP](https://bitbucket.org/fougas/iop) - the data processing pipeline construction framework for Ruby
|
2
|
+
|
3
|
+
## Synopsis
|
4
|
+
|
5
|
+
IOP is intended for construction of the data processing pipelines in a manner of UNIX shell pipes.
|
6
|
+
|
7
|
+
Instead of the standard Ruby way of handling such I/O tasks in form of nested blocks the IOP offers a simpler flat chaining scheme.
|
8
|
+
|
9
|
+
Consider the example:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
# One-liner example
|
13
|
+
(FileReader.new('input.dat') | GzipCompressor.new | DigestComputer.new(MD5.new) | FileWriter.new('output.dat.gz')).process!
|
14
|
+
```
|
15
|
+
|
16
|
+
The above snippet reads input file and compresses it into the GZip-compatible output file simultaneously computing the MD5 hash of compressed data being written.
|
17
|
+
|
18
|
+
The next snippet presents the incremental pipeline construction capability - a feature not easily implementable with the standard Ruby I/O blocks nesting.
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
# Incremental pipeline construction example
|
22
|
+
pipe = FileReader.new('input')
|
23
|
+
pipe |= GzipCompressor.new if need_compression?
|
24
|
+
pipe |= FileWriter.new('output')
|
25
|
+
pipe.process!
|
26
|
+
```
|
27
|
+
|
28
|
+
Here the GZip compression is made optional and is thrown in depending on external condition.
|
29
|
+
|
30
|
+
## Features
|
31
|
+
|
32
|
+
The following capabilities are currently implemented:
|
33
|
+
|
34
|
+
- String splitting/merging
|
35
|
+
- IO or local file reading/writing
|
36
|
+
- FTP file reading/writing
|
37
|
+
- Digest computing
|
38
|
+
- GZip/Zlib (de)compression
|
39
|
+
- Zstd (de)compression
|
40
|
+
- Symmetric cipher (de,en)cryption
|
41
|
+
- Random data generation
|
42
|
+
|
43
|
+
## Basic usage
|
44
|
+
|
45
|
+
- IOP is split into a set of files which should be required separately depending on which components are needed.
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
require 'iop/file'
|
49
|
+
require 'iop/zlib'
|
50
|
+
require 'iop/digest'
|
51
|
+
require 'iop/string'
|
52
|
+
```
|
53
|
+
|
54
|
+
- The `IOP` module can be included into current namespace to conserve some writing.
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
include IOP
|
58
|
+
```
|
59
|
+
|
60
|
+
- A chain of processing objects is created either in-line or incrementally.
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
pipe = StringSplitter.new('Greetings from IOP', 10)
|
64
|
+
pipe |= GzipCompressor.new | (digest = DigestComputer.new(MD5.new))
|
65
|
+
pipe |= FileWriter.new('output.gz')
|
66
|
+
```
|
67
|
+
|
68
|
+
It is convenient to set local variables to the created instances which are expected to have some kind of valuable state.
|
69
|
+
|
70
|
+
- The actual processing is initiated with the `process!` method.
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
pipe.process!
|
74
|
+
```
|
75
|
+
|
76
|
+
The IOP instances do normally perform self-cleanup operations, such as closing file handles, network connections etc., even during exception handling.
|
77
|
+
|
78
|
+
- The variable-bound instances can be then examined.
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
puts digest.hexdigest
|
82
|
+
```
|
83
|
+
|
84
|
+
For further information refer to IOP documentation.
|
85
|
+
|
86
|
+
# The end
|
87
|
+
|
88
|
+
Cheers,
|
89
|
+
|
90
|
+
Oleg A. Khlybov <[fougas@mail.ru](mailto:fougas@mail.ru)>
|
data/lib/iop/digest.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'iop'
|
2
|
+
require 'digest'
|
3
|
+
|
4
|
+
|
5
|
+
module IOP
|
6
|
+
|
7
|
+
|
8
|
+
#
|
9
|
+
# Filter class to compute digest of the data being passed through.
|
10
|
+
# It can be used with digest computing classes from +digest+ and +openssl+ standard Ruby modules.
|
11
|
+
#
|
12
|
+
# ### Use case: generate 1024 bytes of random data and compute and print MD5 hash sum of it.
|
13
|
+
#
|
14
|
+
# require 'iop/digest'
|
15
|
+
# require 'iop/securerandom'
|
16
|
+
# ( IOP::SecureRandomGenerator.new(1024) | ( d = IOP::DigestComputer.new(Digest::MD5.new)) ).process!
|
17
|
+
# puts d.digest.hexdigest
|
18
|
+
#
|
19
|
+
# @since 0.1
|
20
|
+
#
|
21
|
+
class DigestComputer
|
22
|
+
|
23
|
+
include Feed
|
24
|
+
include Sink
|
25
|
+
|
26
|
+
# Returns digest object passed to constructor.
|
27
|
+
attr_reader :digest
|
28
|
+
|
29
|
+
|
30
|
+
# Creates class instance.
|
31
|
+
#
|
32
|
+
# @param digest computer instance to be fed with data
|
33
|
+
def initialize(digest)
|
34
|
+
@digest = digest
|
35
|
+
end
|
36
|
+
|
37
|
+
def process(data = nil)
|
38
|
+
digest.update(data) unless data.nil?
|
39
|
+
super
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
end
|
data/lib/iop/file.rb
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
require 'iop'
|
2
|
+
|
3
|
+
|
4
|
+
module IOP
|
5
|
+
|
6
|
+
|
7
|
+
#
|
8
|
+
# Feed class to read data from external +IO+ stream and send it in blocks downstream.
|
9
|
+
#
|
10
|
+
# Contrary to {FileReader}, this class does not manage attached +IO+ instance, e.g.
|
11
|
+
# it makes no attempt to close it after processing.
|
12
|
+
#
|
13
|
+
# ### Use case: sequential read of two 1024-byte blocks from the same +IO+ stream.
|
14
|
+
#
|
15
|
+
# require 'iop/file'
|
16
|
+
# require 'iop/string'
|
17
|
+
# io = File.new('input.dat', 'rb')
|
18
|
+
# begin
|
19
|
+
# ( IOP::IOReader.new(io, size: 1024) | (first = IOP::StringMerger.new) ).process!
|
20
|
+
# ( IOP::IOReader.new(io, size: 1024, offset: 1024) | (second = IOP::StringMerger.new) ).process!
|
21
|
+
# ensure
|
22
|
+
# io.close
|
23
|
+
# end
|
24
|
+
# puts first.to_s
|
25
|
+
# puts second.to_s
|
26
|
+
#
|
27
|
+
# @since 0.1
|
28
|
+
#
|
29
|
+
class IOReader
|
30
|
+
|
31
|
+
include Feed
|
32
|
+
|
33
|
+
# Creates class instance.
|
34
|
+
#
|
35
|
+
# @param io [IO] +IO+ instance to read data from
|
36
|
+
#
|
37
|
+
# @param size [Integer] total number of bytes to read; +nil+ value instructs to read until end-of-data is reached
|
38
|
+
#
|
39
|
+
# @param offset [Integer] offset in bytes from the stream start to seek to; +nil+ value means no seeking is performed
|
40
|
+
#
|
41
|
+
# @param block_size [Integer] size of blocks to read data with
|
42
|
+
def initialize(io, size: nil, offset: nil, block_size: DEFAULT_BLOCK_SIZE)
|
43
|
+
@block_size = size.nil? ? block_size : IOP.min(size, block_size)
|
44
|
+
@left = @size = size
|
45
|
+
@offset = offset
|
46
|
+
@io = io
|
47
|
+
end
|
48
|
+
|
49
|
+
def process!
|
50
|
+
@io.seek(@offset) unless @offset.nil?
|
51
|
+
data = IOP.allocate_string(@block_size)
|
52
|
+
loop do
|
53
|
+
read_size = @size.nil? ? @block_size : IOP.min(@left, @block_size)
|
54
|
+
break if read_size.zero?
|
55
|
+
if @io.read(read_size, data).nil?
|
56
|
+
if @size.nil?
|
57
|
+
break
|
58
|
+
else
|
59
|
+
raise EOFError, INSUFFICIENT_DATA
|
60
|
+
end
|
61
|
+
else
|
62
|
+
unless @left.nil?
|
63
|
+
@left -= data.size
|
64
|
+
raise IOError, EXTRA_DATA if @left < 0
|
65
|
+
end
|
66
|
+
end
|
67
|
+
process(data) unless data.size.zero?
|
68
|
+
end
|
69
|
+
process
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
#
|
76
|
+
# Feed class to read data from local file and send it in blocks downstream.
|
77
|
+
#
|
78
|
+
# Contrary to {IOReader}, this class manages underlying +IO+ instance in order to close it when the process is finished
|
79
|
+
# even if exception is risen.
|
80
|
+
#
|
81
|
+
# ### Use case: compute MD5 hash sum of the first 1024 bytes of a local file.
|
82
|
+
# require 'iop/file'
|
83
|
+
# require 'iop/digest'
|
84
|
+
# ( IOP::FileReader.new('input.dat', size: 1024) | (d = IOP::DigestComputer.new(Digest::MD5.new)) ).process!
|
85
|
+
# puts d.digest.hexdigest
|
86
|
+
#
|
87
|
+
# @since 0.1
|
88
|
+
#
|
89
|
+
class FileReader < IOReader
|
90
|
+
|
91
|
+
# Creates class instance.
|
92
|
+
#
|
93
|
+
# @param file [String] name of file to read from
|
94
|
+
#
|
95
|
+
# @param mode [String] open mode for the file; refer to {File} for details
|
96
|
+
#
|
97
|
+
# @param options [Hash] extra keyword parameters passed to {IOReader} constructor
|
98
|
+
def initialize(file, mode: 'rb', **options)
|
99
|
+
super(nil, **options)
|
100
|
+
@file = file
|
101
|
+
@mode = mode
|
102
|
+
end
|
103
|
+
|
104
|
+
def process!
|
105
|
+
@io = File.new(@file, @mode)
|
106
|
+
begin
|
107
|
+
super
|
108
|
+
ensure
|
109
|
+
@io.close
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
#
|
117
|
+
# Sink class to write received upstream data to external +IO+ stream.
|
118
|
+
#
|
119
|
+
# Contrary to {FileWriter}, this class does not manage attached +IO+ instance, e.g.
|
120
|
+
# it makes no attempt to close it after processing.
|
121
|
+
#
|
122
|
+
# ### Use case: concatenate two files.
|
123
|
+
#
|
124
|
+
# require 'iop/file'
|
125
|
+
# io = File.new('output.dat', 'wb')
|
126
|
+
# begin
|
127
|
+
# ( IOP::FileReader.new('file1.dat') | IOP::IOWriter.new(io) ).process!
|
128
|
+
# ( IOP::FileReader.new('file2.dat') | IOP::IOWriter.new(io) ).process!
|
129
|
+
# ensure
|
130
|
+
# io.close
|
131
|
+
# end
|
132
|
+
#
|
133
|
+
# @since 0.1
|
134
|
+
#
|
135
|
+
class IOWriter
|
136
|
+
|
137
|
+
include Sink
|
138
|
+
|
139
|
+
# Creates class instance.
|
140
|
+
#
|
141
|
+
# @param io [IO] +IO+ instance to write data to
|
142
|
+
def initialize(io)
|
143
|
+
@io = io
|
144
|
+
end
|
145
|
+
|
146
|
+
def process(data = nil)
|
147
|
+
@io.write(data)
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
|
152
|
+
|
153
|
+
#
|
154
|
+
# Sink class to write received upstream data to a local file.
|
155
|
+
#
|
156
|
+
# Contrary to {IOWriter}, this class manages underlying +IO+ instance in order to close it when the process is finished
|
157
|
+
# even if exception is risen.
|
158
|
+
#
|
159
|
+
# ### Use case: generate 1024 bytes of random data and write it to file.
|
160
|
+
#
|
161
|
+
# require 'iop/file'
|
162
|
+
# require 'iop/securerandom'
|
163
|
+
# ( IOP::SecureRandomGenerator.new(1024) | IOP::FileWriter.new('random.dat') ).process!
|
164
|
+
#
|
165
|
+
# @since 0.1
|
166
|
+
#
|
167
|
+
class FileWriter < IOWriter
|
168
|
+
|
169
|
+
# Creates class instance.
|
170
|
+
#
|
171
|
+
# @param file [String] name of file to write to
|
172
|
+
#
|
173
|
+
# @param mode [String] open mode for the file; refer to {File} for details
|
174
|
+
def initialize(file, mode: 'wb')
|
175
|
+
super(nil)
|
176
|
+
@file = file
|
177
|
+
@mode = mode
|
178
|
+
end
|
179
|
+
|
180
|
+
def process!
|
181
|
+
@io = File.new(@file, @mode)
|
182
|
+
begin
|
183
|
+
super
|
184
|
+
ensure
|
185
|
+
@io.close
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
|
191
|
+
|
192
|
+
# @private
|
193
|
+
class IOSegmentReader
|
194
|
+
|
195
|
+
include BufferingFeed
|
196
|
+
|
197
|
+
def initialize(io, block_size: DEFAULT_BLOCK_SIZE)
|
198
|
+
@io = io
|
199
|
+
@block_size = block_size
|
200
|
+
end
|
201
|
+
|
202
|
+
private def next_data
|
203
|
+
@io.read(@block_size)
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
207
|
+
|
208
|
+
|
209
|
+
end
|
data/lib/iop/net/ftp.rb
ADDED
@@ -0,0 +1,206 @@
|
|
1
|
+
require 'iop'
|
2
|
+
require 'net/ftp'
|
3
|
+
|
4
|
+
|
5
|
+
# @private
|
6
|
+
class Net::FTP
|
7
|
+
public :transfercmd, :voidresp
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
module IOP
|
12
|
+
|
13
|
+
|
14
|
+
# @private
|
15
|
+
module FTPFile
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def setup
|
20
|
+
if @ftp.is_a?(String)
|
21
|
+
@ftp = Net::FTP.open(@ftp, @options)
|
22
|
+
@managed = true
|
23
|
+
@ftp.login
|
24
|
+
end
|
25
|
+
unless @offset.nil?
|
26
|
+
# Override resume status when offset is specified remembering current value
|
27
|
+
@resume = @ftp.resume
|
28
|
+
@ftp.resume = true
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def cleanup
|
33
|
+
# Revert resume status if previously overridden
|
34
|
+
@ftp.resume = @resume unless @resume.nil?
|
35
|
+
@ftp.close if @managed
|
36
|
+
end
|
37
|
+
|
38
|
+
def transfercmd(cmd, offset = nil)
|
39
|
+
@ftp.transfercmd(cmd, offset)
|
40
|
+
end
|
41
|
+
|
42
|
+
def voidresp
|
43
|
+
@ftp.voidresp
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
#
|
49
|
+
# Feed class to read file from FTP server.
|
50
|
+
#
|
51
|
+
# This class an adapter for the standard Ruby +Net::FTP+ class.
|
52
|
+
#
|
53
|
+
# ### Use case: retrieve file from FTP server and store it locally.
|
54
|
+
#
|
55
|
+
# require 'iop/net/ftp'
|
56
|
+
# ( IOP::FTPFileReader.new('ftp.gnu.org', '/pub/README') | IOP::FileWriter.new('README') ).process!
|
57
|
+
#
|
58
|
+
# @since 0.1
|
59
|
+
#
|
60
|
+
class FTPFileReader
|
61
|
+
|
62
|
+
include Feed
|
63
|
+
include FTPFile
|
64
|
+
|
65
|
+
# Creates class instance.
|
66
|
+
#
|
67
|
+
# @param ftp [String, Net::FTP] FTP server to connect to
|
68
|
+
#
|
69
|
+
# @param file [String] file name to process
|
70
|
+
#
|
71
|
+
# @param size [Integer] total number of bytes to read; +nil+ value instructs to read until end-of-data is reached
|
72
|
+
#
|
73
|
+
# @param offset [Integer] offset in bytes from the stream start to seek to; +nil+ value means no seeking is performed
|
74
|
+
#
|
75
|
+
# @param block_size [Integer] size of blocks to process data with
|
76
|
+
#
|
77
|
+
# @param options [Hash] extra keyword parameters passed to +Net::FTP+ constructor
|
78
|
+
#
|
79
|
+
# _ftp_ can be either a +String+ of +Net::FTP+ instance.
|
80
|
+
# If it is a string a corresponding +Net::FTP+ instance will be created with _options_ passed to its constructor.
|
81
|
+
#
|
82
|
+
# If _ftp_ is a string, a created FTP connection is managed, e.g. it is closed after the process is complete,
|
83
|
+
# otherwise supplied object is left as is and no closing is performed.
|
84
|
+
# This allows to reuse FTP connection for a sequence of operations.
|
85
|
+
#
|
86
|
+
# Refer to +Net::FTP+ documentation for available options.
|
87
|
+
def initialize(ftp, file, size: nil, offset: nil, block_size: DEFAULT_BLOCK_SIZE, **options)
|
88
|
+
@block_size = size.nil? ? block_size : IOP.min(size, block_size)
|
89
|
+
@left = @size = size
|
90
|
+
@options = options
|
91
|
+
@offset = offset
|
92
|
+
@file = file
|
93
|
+
@ftp = ftp
|
94
|
+
end
|
95
|
+
|
96
|
+
def process!
|
97
|
+
setup
|
98
|
+
begin
|
99
|
+
# FTP logic taken from Net::FTP#retrbinary
|
100
|
+
@io = transfercmd('RETR ' << @file, @offset)
|
101
|
+
begin
|
102
|
+
loop do
|
103
|
+
read_size = @size.nil? ? @block_size : IOP.min(@left, @block_size)
|
104
|
+
break if read_size.zero?
|
105
|
+
data = @io.read(read_size)
|
106
|
+
if data.nil?
|
107
|
+
if @size.nil?
|
108
|
+
break
|
109
|
+
else
|
110
|
+
raise EOFError, INSUFFICIENT_DATA
|
111
|
+
end
|
112
|
+
else
|
113
|
+
unless @left.nil?
|
114
|
+
@left -= data.size
|
115
|
+
raise IOError, EXTRA_DATA if @left < 0
|
116
|
+
end
|
117
|
+
process(data) unless data.size.zero?
|
118
|
+
end
|
119
|
+
end
|
120
|
+
process
|
121
|
+
@io.shutdown(Socket::SHUT_WR)
|
122
|
+
@io.read_timeout = 1
|
123
|
+
@io.read
|
124
|
+
ensure
|
125
|
+
@io.close
|
126
|
+
end
|
127
|
+
voidresp
|
128
|
+
ensure
|
129
|
+
cleanup
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
|
137
|
+
#
|
138
|
+
# Sink class to write file to FTP server.
|
139
|
+
#
|
140
|
+
# This class an adapter for the standard Ruby +Net::FTP+ class.
|
141
|
+
#
|
142
|
+
# ### Use case: store a number of files filled with random data to an FTP server reusing connection.
|
143
|
+
#
|
144
|
+
# require 'iop/net/ftp'
|
145
|
+
# require 'iop/securerandom'
|
146
|
+
# ftp = Net::FTP.open('ftp.server', username: 'user')
|
147
|
+
# begin
|
148
|
+
# ftp.login
|
149
|
+
# (1..3).each do |i|
|
150
|
+
# ( IOP::SecureRandomGenerator.new(1024) | IOP::FTPFileWriter.new(ftp, "random#{i}.dat") ).process!
|
151
|
+
# end
|
152
|
+
# ensure
|
153
|
+
# ftp.close
|
154
|
+
# end
|
155
|
+
#
|
156
|
+
# @since 0.1
|
157
|
+
#
|
158
|
+
class FTPFileWriter
|
159
|
+
|
160
|
+
include Sink
|
161
|
+
include FTPFile
|
162
|
+
|
163
|
+
# Creates class instance.
|
164
|
+
#
|
165
|
+
# @param ftp [String, Net::FTP] FTP server to connect to
|
166
|
+
#
|
167
|
+
# @param file [String] file name to process
|
168
|
+
#
|
169
|
+
# @param options [Hash] extra keyword parameters passed to +Net::FTP+ constructor
|
170
|
+
#
|
171
|
+
# _ftp_ can be either a +String+ of +Net::FTP+ instance.
|
172
|
+
# If it is a string a corresponding +Net::FTP+ instance will be created with _options_ passed to its constructor.
|
173
|
+
#
|
174
|
+
# If _ftp_ is a string, a created FTP connection is managed, e.g. it is closed after the process is complete,
|
175
|
+
# otherwise supplied object is left as is and no closing is performed.
|
176
|
+
# This allows to reuse FTP connection for a sequence of operations.
|
177
|
+
def initialize(ftp, file, **options)
|
178
|
+
@options = options
|
179
|
+
@file = file
|
180
|
+
@ftp = ftp
|
181
|
+
end
|
182
|
+
|
183
|
+
def process!
|
184
|
+
setup
|
185
|
+
begin
|
186
|
+
# FTP logic taken from Net::FTP#storbinary
|
187
|
+
@io = transfercmd('STOR ' << @file)
|
188
|
+
begin
|
189
|
+
super
|
190
|
+
ensure
|
191
|
+
@io.close
|
192
|
+
end
|
193
|
+
voidresp
|
194
|
+
ensure
|
195
|
+
cleanup
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def process(data = nil)
|
200
|
+
@io.write(data) unless data.nil?
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|
204
|
+
|
205
|
+
|
206
|
+
end
|
data/lib/iop/openssl.rb
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'iop'
|
2
|
+
require 'openssl'
|
3
|
+
|
4
|
+
|
5
|
+
module IOP
|
6
|
+
|
7
|
+
|
8
|
+
# Default cipher ID for OpenSSL adapters.
|
9
|
+
DEFAULT_OPENSSL_CIPHER = 'AES-256-CBC'.freeze
|
10
|
+
|
11
|
+
|
12
|
+
#
|
13
|
+
# Filter class to perform encryption with a symmetric key algorithm (ciphering) of the data passed through.
|
14
|
+
#
|
15
|
+
# The class is an adapter for +OpenSSL::Cipher+ & compatible classes.
|
16
|
+
#
|
17
|
+
# ### Use case: generate 1024 bytes of random data encrypt is with default cipher algorithm and generated key & initial vector.
|
18
|
+
#
|
19
|
+
# require 'iop/openssl'
|
20
|
+
# require 'iop/securerandom'
|
21
|
+
# ( IOP::SecureRandomGenerator.new(1024) | (c = IOP::CipherEncryptor.new) ).process!
|
22
|
+
# puts c.key
|
23
|
+
#
|
24
|
+
# @since 0.1
|
25
|
+
#
|
26
|
+
class CipherEncryptor
|
27
|
+
|
28
|
+
include Feed
|
29
|
+
include Sink
|
30
|
+
|
31
|
+
# Returns initial vector (IV) for encryption session.
|
32
|
+
attr_reader :iv
|
33
|
+
|
34
|
+
# Returns encryption key.
|
35
|
+
attr_reader :key
|
36
|
+
|
37
|
+
# Creates class instance.
|
38
|
+
#
|
39
|
+
# @param cipher [String, OpenSSL::Cipher] cipher used for encryption
|
40
|
+
#
|
41
|
+
# @param key [String] string representing an encryption key or +nil+
|
42
|
+
#
|
43
|
+
# @param iv [String] string representing an initial vector or +nil+
|
44
|
+
#
|
45
|
+
# _cipher_ can be either a +String+ or +OpenSSL::Cipher+ instance.
|
46
|
+
# If it is a string, a corresponding +OpenSSL::Cipher+ instance will be created.
|
47
|
+
#
|
48
|
+
# If _key_ is +nil+, a new key will be generated in secure manner which can be accessed later with {#key} method.
|
49
|
+
#
|
50
|
+
# If _iv_ is +nil+, a new initial vector will be generated in secure manner which can be accessed later with {#iv} method.
|
51
|
+
# If _iv_ is +nil+ the generated initial vector will be injected into the downstream data preceding the encrypted data itself.
|
52
|
+
#
|
53
|
+
# Note that key and initial vector are both cipher-dependent. Refer to +OpenSSL::Cipher+ documentation for more information.
|
54
|
+
def initialize(cipher = DEFAULT_OPENSSL_CIPHER, key: nil, iv: nil)
|
55
|
+
@cipher = cipher.is_a?(String) ? OpenSSL::Cipher.new(cipher) : cipher
|
56
|
+
@cipher.encrypt
|
57
|
+
@key = key.nil? ? @cipher.random_key : @cipher.key = key
|
58
|
+
@iv = if iv.nil?
|
59
|
+
@embed_iv = true
|
60
|
+
@cipher.random_iv
|
61
|
+
else
|
62
|
+
@cipher.iv = iv
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def process(data = nil)
|
67
|
+
unless @continue
|
68
|
+
@continue = true
|
69
|
+
super(iv) if @embed_iv
|
70
|
+
@buffer = IOP.allocate_string(data.size)
|
71
|
+
end
|
72
|
+
if data.nil?
|
73
|
+
super(@cipher.final)
|
74
|
+
super
|
75
|
+
else
|
76
|
+
super(@cipher.update(data, @buffer)) unless data.size.zero?
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
#
|
84
|
+
# Filter class to perform decryption with a symmetric key algorithm (ciphering) of the data passed through.
|
85
|
+
#
|
86
|
+
# The class is an adapter for +OpenSSL::Cipher+ & compatible classes.
|
87
|
+
#
|
88
|
+
# ### Use case: decrypt a file with default algorithm and embedded initial vector.
|
89
|
+
#
|
90
|
+
# require 'iop/file'
|
91
|
+
# require 'iop/openssl'
|
92
|
+
# ( IOP::FileReader.new('input.aes') | IOP::CipherDecryptor.new(key: my_secret_key) | (s = IOP::StringMerger.new) ).process!
|
93
|
+
# puts s.to_s
|
94
|
+
#
|
95
|
+
# @since 0.1
|
96
|
+
#
|
97
|
+
class CipherDecryptor
|
98
|
+
|
99
|
+
include Feed
|
100
|
+
include Sink
|
101
|
+
|
102
|
+
# Returns initial vector (IV) for decryption session.
|
103
|
+
attr_reader :iv
|
104
|
+
|
105
|
+
# Returns decryption key.
|
106
|
+
attr_reader :key
|
107
|
+
|
108
|
+
# Creates class instance.
|
109
|
+
#
|
110
|
+
# @param cipher [String, OpenSSL::Cipher] cipher used for decryption
|
111
|
+
#
|
112
|
+
# @param key [String] string representing an encryption key
|
113
|
+
#
|
114
|
+
# @param iv [String] string representing an initial vector or +nil+
|
115
|
+
#
|
116
|
+
# _cipher_ can be either a +String+ or +OpenSSL::Cipher+ instance.
|
117
|
+
# If it is a string, a corresponding +OpenSSL::Cipher+ instance will be created.
|
118
|
+
#
|
119
|
+
# If _iv_ is +nil+, the initial vector will be obtained from the upstream data. Refer to {CipherEncryptor#initialize} for details.
|
120
|
+
def initialize(cipher = DEFAULT_OPENSSL_CIPHER, key:, iv: nil)
|
121
|
+
@cipher = cipher.is_a?(String) ? OpenSSL::Cipher.new(cipher) : cipher
|
122
|
+
@cipher.decrypt
|
123
|
+
@cipher.key = @key = key
|
124
|
+
@cipher.iv = @iv = iv unless iv.nil?
|
125
|
+
end
|
126
|
+
|
127
|
+
def process(data = nil)
|
128
|
+
unless @continue
|
129
|
+
@continue = true
|
130
|
+
@buffer = IOP.allocate_string(data.size)
|
131
|
+
if iv.nil?
|
132
|
+
@cipher.iv = @iv = data[0, @cipher.iv_len]
|
133
|
+
data = data[@cipher.iv_len..-1]
|
134
|
+
end
|
135
|
+
end
|
136
|
+
if data.nil?
|
137
|
+
super(@cipher.final)
|
138
|
+
super
|
139
|
+
else
|
140
|
+
super(@cipher.update(data, @buffer)) unless data.size.zero?
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
|
147
|
+
end
|