iop 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|