hoodie 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +4 -0
- data/LICENSE +201 -0
- data/README.md +1 -0
- data/Rakefile +14 -0
- data/hoodie.gemspec +30 -0
- data/lib/hoodie.rb +31 -0
- data/lib/hoodie/blank.rb +123 -0
- data/lib/hoodie/file.rb +69 -0
- data/lib/hoodie/hash.rb +167 -0
- data/lib/hoodie/memoizable.rb +44 -0
- data/lib/hoodie/obfuscate.rb +121 -0
- data/lib/hoodie/os.rb +43 -0
- data/lib/hoodie/path_finder.rb +75 -0
- data/lib/hoodie/stash.rb +118 -0
- data/lib/hoodie/stash/disk_store.rb +151 -0
- data/lib/hoodie/stash/mem_store.rb +150 -0
- data/lib/hoodie/utils.rb +134 -0
- data/lib/hoodie/version.rb +22 -0
- metadata +161 -0
data/lib/hoodie/hash.rb
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# Author: Stefano Harding <riddopic@gmail.com>
|
4
|
+
#
|
5
|
+
# Copyright (C) 2014 Stefano Harding
|
6
|
+
#
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
+
# you may not use this file except in compliance with the License.
|
9
|
+
# You may obtain a copy of the License at
|
10
|
+
#
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
#
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
# See the License for the specific language governing permissions and
|
17
|
+
# limitations under the License.
|
18
|
+
#
|
19
|
+
|
20
|
+
class Hash
|
21
|
+
# Returns a compacted copy (contains no key/value pairs having
|
22
|
+
# nil? values)
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# hash = { a: 100, b: nil, c: false, d: '' }
|
26
|
+
# hash.compact # => { a: 100, c: false, d: '' }
|
27
|
+
# hash # => { a: 100, b: nil, c: false, d: '' }
|
28
|
+
#
|
29
|
+
# @return [Hash]
|
30
|
+
#
|
31
|
+
def compact
|
32
|
+
select { |_, value| !value.nil? }
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns a new hash with all keys converted using the block operation.
|
36
|
+
#
|
37
|
+
# @example
|
38
|
+
# hash = { name: 'Tiggy', age: '15' }
|
39
|
+
#
|
40
|
+
# hash.transform_keys{ |key| key.to_s.upcase }
|
41
|
+
# # => { "AGE" => "15", "NAME" => "Tiggy" }
|
42
|
+
#
|
43
|
+
# @return [Hash]
|
44
|
+
#
|
45
|
+
def transform_keys
|
46
|
+
return enum_for(:transform_keys) unless block_given?
|
47
|
+
result = self.class.new
|
48
|
+
each_key do |key|
|
49
|
+
result[yield(key)] = self[key]
|
50
|
+
end
|
51
|
+
result
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns a new hash, recursively converting all keys by the
|
55
|
+
# block operation.
|
56
|
+
#
|
57
|
+
# @return [Hash]
|
58
|
+
#
|
59
|
+
def recursively_transform_keys(&block)
|
60
|
+
_recursively_transform_keys_in_object(self, &block)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns a new hash with all keys downcased and converted
|
64
|
+
# to symbols.
|
65
|
+
#
|
66
|
+
# @return [Hash]
|
67
|
+
#
|
68
|
+
def normalize_keys
|
69
|
+
transform_keys { |key| key.downcase.to_sym rescue key }
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns a new Hash, recursively downcasing and converting all
|
73
|
+
# keys to symbols.
|
74
|
+
#
|
75
|
+
# @return [Hash]
|
76
|
+
#
|
77
|
+
def recursively_normalize_keys
|
78
|
+
recursively_transform_keys { |key| key.downcase.to_sym rescue key }
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns a new hash with all keys converted to symbols.
|
82
|
+
#
|
83
|
+
# @return [Hash]
|
84
|
+
#
|
85
|
+
def symbolize_keys
|
86
|
+
transform_keys { |key| key.to_sym rescue key }
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns a new Hash, recursively converting all keys to symbols.
|
90
|
+
#
|
91
|
+
# @return [Hash]
|
92
|
+
#
|
93
|
+
def recursively_symbolize_keys
|
94
|
+
recursively_transform_keys { |key| key.downcase.to_sym rescue key }
|
95
|
+
end
|
96
|
+
|
97
|
+
# Returns a new hash with all keys converted to strings.
|
98
|
+
#
|
99
|
+
# @return [Hash]
|
100
|
+
#
|
101
|
+
def stringify_keys
|
102
|
+
transform_keys { |key| key.to_s rescue key }
|
103
|
+
end
|
104
|
+
|
105
|
+
# Returns a new Hash, recursively converting all keys to strings.
|
106
|
+
#
|
107
|
+
# @return [Hash]
|
108
|
+
#
|
109
|
+
def recursively_stringify_key
|
110
|
+
recursively_transform_keys { |key| key.to_s rescue key }
|
111
|
+
end
|
112
|
+
|
113
|
+
class UndefinedPathError < StandardError; end
|
114
|
+
# Recursively searchs a nested datastructure for a key and returns
|
115
|
+
# the value. If a block is provided its value will be returned if
|
116
|
+
# the key does not exist
|
117
|
+
#
|
118
|
+
# @example
|
119
|
+
# options = { server: { location: { row: { rack: 34 } } } }
|
120
|
+
# options.recursive_fetch :server, :location, :row, :rack
|
121
|
+
# # => 34
|
122
|
+
# options.recursive_fetch(:non_existent_key) { 'default' }
|
123
|
+
# # => "default"
|
124
|
+
#
|
125
|
+
# @return [Hash, Array, String] value for key
|
126
|
+
#
|
127
|
+
def recursive_fetch(*args, &block)
|
128
|
+
args.reduce(self) do |obj, arg|
|
129
|
+
begin
|
130
|
+
arg = Integer(arg) if obj.is_a? Array
|
131
|
+
obj.fetch(arg)
|
132
|
+
rescue ArgumentError, IndexError, NoMethodError => e
|
133
|
+
break block.call(arg) if block
|
134
|
+
raise UndefinedPathError, "Could not fetch path (#{args.join(' > ')}) at #{arg}", e.backtrace
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def recursive_merge(other)
|
140
|
+
hash = self.dup
|
141
|
+
other.each do |key, value|
|
142
|
+
myval = self[key]
|
143
|
+
if value.is_a?(Hash) && myval.is_a?(Hash)
|
144
|
+
hash[key] = myval.recursive_merge(value)
|
145
|
+
else
|
146
|
+
hash[key] = value
|
147
|
+
end
|
148
|
+
end
|
149
|
+
hash
|
150
|
+
end
|
151
|
+
|
152
|
+
private # P R O P R I E T À P R I V A T A divieto di accesso
|
153
|
+
|
154
|
+
# support methods for recursively transforming nested hashes and arrays
|
155
|
+
def _recursively_transform_keys_in_object(object, &block)
|
156
|
+
case object
|
157
|
+
when Hash
|
158
|
+
object.each_with_object({}) do |(key, value), result|
|
159
|
+
result[yield(key)] = _recursively_transform_keys_in_object(value, &block)
|
160
|
+
end
|
161
|
+
when Array
|
162
|
+
object.map { |e| _recursively_transform_keys_in_object(e, &block) }
|
163
|
+
else
|
164
|
+
object
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# Author: Stefano Harding <riddopic@gmail.com>
|
4
|
+
#
|
5
|
+
# Copyright (C) 2014 Stefano Harding
|
6
|
+
#
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
+
# you may not use this file except in compliance with the License.
|
9
|
+
# You may obtain a copy of the License at
|
10
|
+
#
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
#
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
# See the License for the specific language governing permissions and
|
17
|
+
# limitations under the License.
|
18
|
+
#
|
19
|
+
|
20
|
+
require 'hoodie/stash' unless defined?(Stash)
|
21
|
+
|
22
|
+
# Memoization is an optimization that saves the return value of a
|
23
|
+
# method so it doesn't need to be re-computed every time that method
|
24
|
+
# is called.
|
25
|
+
module Memoizable
|
26
|
+
|
27
|
+
# Create a new memoized method. To use, extend class with Memoizable,
|
28
|
+
# then, in initialize, call memoize
|
29
|
+
#
|
30
|
+
# @return [undefined]
|
31
|
+
#
|
32
|
+
def memoize(methods, cache = nil)
|
33
|
+
cache ||= Stash.new
|
34
|
+
methods.each do |name|
|
35
|
+
uncached_name = "#{name}_uncached".to_sym
|
36
|
+
(class << self; self; end).class_eval do
|
37
|
+
alias_method uncached_name, name
|
38
|
+
define_method(name) do |*a, &b|
|
39
|
+
cache.cache(name) { send uncached_name, *a, &b }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# Author: Stefano Harding <riddopic@gmail.com>
|
4
|
+
#
|
5
|
+
# Copyright (C) 2014 Stefano Harding
|
6
|
+
#
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
+
# you may not use this file except in compliance with the License.
|
9
|
+
# You may obtain a copy of the License at
|
10
|
+
#
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
#
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
# See the License for the specific language governing permissions and
|
17
|
+
# limitations under the License.
|
18
|
+
#
|
19
|
+
|
20
|
+
module Hoodie
|
21
|
+
begin
|
22
|
+
require 'openssl'
|
23
|
+
INCOMPREHENSIBLE_ERROR = nil
|
24
|
+
rescue LoadError => err
|
25
|
+
raise unless err.to_s.include?('openssl')
|
26
|
+
warn "Oea pieoYreb h wYoerh dl hwsnhoib r Lrbea tbte wbnaetvoouahe h rbe."
|
27
|
+
warn "olorbvtelYShnSben irrSwoet eto eihSrLoS'do n See wLiape."
|
28
|
+
INCOMPREHENSIBLE_ERROR = err
|
29
|
+
end
|
30
|
+
|
31
|
+
require 'digest/sha2'
|
32
|
+
require "base64"
|
33
|
+
|
34
|
+
# Befuddle and enlighten values in StashCache::Store
|
35
|
+
#
|
36
|
+
module Obfuscate
|
37
|
+
ESOTERIC_TYPE = "aes-256-cbc" unless defined?(ESOTERIC_TYPE)
|
38
|
+
|
39
|
+
def self.check_platform_can_discombobulate!
|
40
|
+
return true unless INCOMPREHENSIBLE_ERROR
|
41
|
+
raise INCOMPREHENSIBLE_ERROR.class, "b0rked! #{INCOMPREHENSIBLE_ERROR}"
|
42
|
+
end
|
43
|
+
|
44
|
+
# Befuddle the given string
|
45
|
+
#
|
46
|
+
# @param plaintext the text to befuddle
|
47
|
+
# @param [String] befuddle_pass secret passphrase to befuddle with
|
48
|
+
#
|
49
|
+
# @return [String] befuddleed text, suitable for deciphering with
|
50
|
+
# Obfuscate#enlighten (decrypt)
|
51
|
+
#
|
52
|
+
def self.befuddle plaintext, befuddle_pass, options={}
|
53
|
+
cipher = new_cipher :befuddle, befuddle_pass, options
|
54
|
+
cipher.iv = iv = cipher.random_iv
|
55
|
+
ciphertext = cipher.update(plaintext)
|
56
|
+
ciphertext << cipher.final
|
57
|
+
Base64.encode64(combine_iv_and_ciphertext(iv, ciphertext))
|
58
|
+
end
|
59
|
+
|
60
|
+
# Enlighten the given string, using the key and id supplied
|
61
|
+
#
|
62
|
+
# @param ciphertext the text to enlighten, probably produced with
|
63
|
+
# Obfuscate#befuddle (encrypt)
|
64
|
+
# @param [String] befuddle_pass secret passphrase to enlighten with
|
65
|
+
#
|
66
|
+
# @return [String] the enlightened plaintext
|
67
|
+
#
|
68
|
+
def self.enlighten enc_ciphertext, befuddle_pass, options={}
|
69
|
+
iv_and_ciphertext = Base64.decode64(enc_ciphertext)
|
70
|
+
cipher = new_cipher :enlighten, befuddle_pass, options
|
71
|
+
cipher.iv, ciphertext = separate_iv_and_ciphertext(cipher, iv_and_ciphertext)
|
72
|
+
plaintext = cipher.update(ciphertext)
|
73
|
+
plaintext << cipher.final
|
74
|
+
plaintext
|
75
|
+
end
|
76
|
+
|
77
|
+
private # P R O P R I E T À P R I V A T A divieto di accesso
|
78
|
+
|
79
|
+
# Cipher create machine do, befuddle engage, enlighten. Dials set
|
80
|
+
# direction to infinity
|
81
|
+
#
|
82
|
+
# @param [:befuddle, :enlighten] to befuddle or enlighten
|
83
|
+
# @param [String] befuddle_pass secret passphrase to enlighten
|
84
|
+
#
|
85
|
+
def self.new_cipher direction, befuddle_pass, options={}
|
86
|
+
check_platform_can_discombobulate!
|
87
|
+
cipher = OpenSSL::Cipher::Cipher.new(ESOTERIC_TYPE)
|
88
|
+
case direction
|
89
|
+
when :befuddle
|
90
|
+
cipher.encrypt
|
91
|
+
when :enlighten
|
92
|
+
cipher.decrypt
|
93
|
+
else raise "Bad cipher direction #{direction}"
|
94
|
+
end
|
95
|
+
cipher.key = befuddle_key(befuddle_pass, options)
|
96
|
+
cipher
|
97
|
+
end
|
98
|
+
|
99
|
+
# prepend the initialization vector to the encoded message
|
100
|
+
def self.combine_iv_and_ciphertext iv, message
|
101
|
+
message.force_encoding("BINARY") if message.respond_to?(:force_encoding)
|
102
|
+
iv.force_encoding("BINARY") if iv.respond_to?(:force_encoding)
|
103
|
+
iv + message
|
104
|
+
end
|
105
|
+
|
106
|
+
# pull the initialization vector from the front of the encoded message
|
107
|
+
def self.separate_iv_and_ciphertext cipher, iv_and_ciphertext
|
108
|
+
idx = cipher.iv_len
|
109
|
+
[ iv_and_ciphertext[0..(idx-1)], iv_and_ciphertext[idx..-1] ]
|
110
|
+
end
|
111
|
+
|
112
|
+
# Convert the befuddle_pass passphrase into the key used for befuddletion
|
113
|
+
def self.befuddle_key befuddle_pass, options={}
|
114
|
+
befuddle_pass = befuddle_pass.to_s
|
115
|
+
raise 'Missing befuddled password!' if befuddle_pass.empty?
|
116
|
+
# this provides the required 256 bits of key for the aes-256-cbc
|
117
|
+
# cipher
|
118
|
+
Digest::SHA256.digest(befuddle_pass)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
data/lib/hoodie/os.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# Author: Stefano Harding <riddopic@gmail.com>
|
4
|
+
#
|
5
|
+
# Copyright (C) 2014 Stefano Harding
|
6
|
+
#
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
+
# you may not use this file except in compliance with the License.
|
9
|
+
# You may obtain a copy of the License at
|
10
|
+
#
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
#
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
# See the License for the specific language governing permissions and
|
17
|
+
# limitations under the License.
|
18
|
+
#
|
19
|
+
|
20
|
+
require 'rbconfig' unless defined?(RbConfig)
|
21
|
+
|
22
|
+
# Finds out the current Operating System.
|
23
|
+
module OS
|
24
|
+
def self.windows?
|
25
|
+
windows = /cygwin|mswin|mingw|bccwin|wince|emx/i
|
26
|
+
(RbConfig::CONFIG['host_os'] =~ windows) != nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.mac?
|
30
|
+
mac = /darwin|mac os/i
|
31
|
+
(RbConfig::CONFIG['host_os'] =~ mac) != nil
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.unix?
|
35
|
+
unix = /solaris|bsd/i
|
36
|
+
(RbConfig::CONFIG['host_os'] =~ unix) != nil
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.linux?
|
40
|
+
linux = /linux/i
|
41
|
+
(RbConfig::CONFIG['host_os'] =~ linux) != nil
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# Author: Stefano Harding <riddopic@gmail.com>
|
4
|
+
#
|
5
|
+
# Copyright (C) 2014 Stefano Harding
|
6
|
+
#
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
+
# you may not use this file except in compliance with the License.
|
9
|
+
# You may obtain a copy of the License at
|
10
|
+
#
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
#
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
# See the License for the specific language governing permissions and
|
17
|
+
# limitations under the License.
|
18
|
+
#
|
19
|
+
|
20
|
+
# TODO: This doesn't belong in here, it's cookbook specific...
|
21
|
+
require 'anemone' unless defined?(Anemone)
|
22
|
+
require 'hoodie/memoizable' unless defined?(Memoizable)
|
23
|
+
|
24
|
+
class PathFinder
|
25
|
+
include Memoizable
|
26
|
+
|
27
|
+
def initialize(url)
|
28
|
+
@url = url
|
29
|
+
memoize [:fetch], Stash.new(DiskStash::Cache.new)
|
30
|
+
end
|
31
|
+
|
32
|
+
def fetch(path)
|
33
|
+
results = []
|
34
|
+
Anemone.crawl(@url, discard_page_bodies: true) do |anemone|
|
35
|
+
anemone.on_pages_like(/\/#{path}\/\w+\/\w+\.(ini|zip)$/i) do |page|
|
36
|
+
results << page.to_hash
|
37
|
+
end
|
38
|
+
end
|
39
|
+
results.reduce({}, :recursive_merge)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# to_hash smoke cache
|
44
|
+
#
|
45
|
+
module Anemone
|
46
|
+
class Page
|
47
|
+
def to_hash
|
48
|
+
file = ::File.basename(@url.to_s)
|
49
|
+
key = ::File.basename(file, '.*').downcase.to_sym
|
50
|
+
type = ::File.extname(file)[1..-1].downcase.to_sym
|
51
|
+
id = Hoodie::Obfuscate.befuddle(file, Digest::MD5.hexdigest(body.to_s))
|
52
|
+
mtime = Time.parse(@headers['last-modified'][0]).to_i
|
53
|
+
utime = Time::now.to_i
|
54
|
+
state = utime > mtime ? :clean : :dirty
|
55
|
+
key = { key => { type => {
|
56
|
+
id: id,
|
57
|
+
cache_state: state,
|
58
|
+
file: file,
|
59
|
+
key: key,
|
60
|
+
type: type,
|
61
|
+
url: @url.to_s,
|
62
|
+
mtime: mtime,
|
63
|
+
links: links.map(&:to_s),
|
64
|
+
code: @code,
|
65
|
+
visited: @visited,
|
66
|
+
depth: @depth,
|
67
|
+
referer: @referer.to_s,
|
68
|
+
fetched: @fetched,
|
69
|
+
utime: utime,
|
70
|
+
md5_digest: Digest::MD5.hexdigest(body.to_s),
|
71
|
+
sha256_digest: Digest::SHA256.hexdigest(body.to_s)
|
72
|
+
}}}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|