ftw 0.0.16 → 0.0.17
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.
- data/lib/ftw/agent.rb +114 -14
- data/lib/ftw/agent/configuration.rb +22 -1
- data/lib/ftw/connection.rb +13 -4
- data/lib/ftw/version.rb +1 -1
- metadata +2 -2
data/lib/ftw/agent.rb
CHANGED
@@ -64,14 +64,106 @@ class FTW::Agent
|
|
64
64
|
configuration[REDIRECTION_LIMIT] = 20
|
65
65
|
|
66
66
|
@certificate_store = OpenSSL::X509::Store.new
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
67
|
+
if File.readable?(OpenSSL::X509::DEFAULT_CERT_FILE)
|
68
|
+
@logger.debug("Adding default certificate file",
|
69
|
+
:path => OpenSSL::X509::DEFAULT_CERT_FILE)
|
70
|
+
@certificate_store.add_file(OpenSSL::X509::DEFAULT_CERT_FILE)
|
71
71
|
end
|
72
72
|
|
73
|
+
# Handle the local user/app trust store as well.
|
74
|
+
if File.directory?(configuration[SSL_TRUST_STORE])
|
75
|
+
# This is a directory, so use add_path
|
76
|
+
@logger.debug("Adding SSL_TRUST_STORE",
|
77
|
+
:path => configuration[SSL_TRUST_STORE])
|
78
|
+
@certificate_store.add_path(configuration[SSL_TRUST_STORE])
|
79
|
+
end
|
80
|
+
|
81
|
+
# TODO(sissel): Add custom paths for ssl certs
|
73
82
|
end # def initialize
|
74
83
|
|
84
|
+
# Verify a certificate.
|
85
|
+
#
|
86
|
+
# host => the host (string)
|
87
|
+
# port => the port (number)
|
88
|
+
# verified => true/false, was this cert verified by our certificate store?
|
89
|
+
# context => an OpenSSL::SSL::StoreContext
|
90
|
+
def certificate_verify(host, port, verified, context)
|
91
|
+
# Now verify the entire chain.
|
92
|
+
begin
|
93
|
+
@logger.debug("Verify peer via OpenSSL::X509::Store",
|
94
|
+
:verified => verified, :chain => context.chain.collect { |c| c.subject },
|
95
|
+
:context => context, :depth => context.error_depth,
|
96
|
+
:error => context.error, :string => context.error_string)
|
97
|
+
# Untrusted certificate; prompt to accept if possible.
|
98
|
+
if !verified and STDOUT.tty?
|
99
|
+
# TODO(sissel): Factor this out into a verify callback where this
|
100
|
+
# happens to be the default.
|
101
|
+
|
102
|
+
puts "Untrusted certificate found; here's what I know:"
|
103
|
+
puts " Why it's untrusted: (#{context.error}) #{context.error_string}"
|
104
|
+
puts " What you think it's for: #{host} (port #{port})"
|
105
|
+
cn = context.chain[0].subject.to_s.split("/").grep(/^CN=/).first.split("=",2).last rescue "<unknown, no CN?>"
|
106
|
+
puts " What it's actually for: #{cn}"
|
107
|
+
puts " Full chain:"
|
108
|
+
context.chain.each_with_index do |cert, i|
|
109
|
+
puts " Subject(#{i}): #{cert.subject}"
|
110
|
+
end
|
111
|
+
print "Trust? [(N)o/(Y)es/(P)ersistent] "
|
112
|
+
|
113
|
+
system("stty raw")
|
114
|
+
answer = $stdin.getc.downcase
|
115
|
+
system("stty sane")
|
116
|
+
puts
|
117
|
+
|
118
|
+
if ["y", "p"].include?(answer)
|
119
|
+
# TODO(sissel): Factor this out into Agent::Trust or somesuch
|
120
|
+
context.chain.each do |cert|
|
121
|
+
# For each certificate, add it to the in-process certificate store.
|
122
|
+
begin
|
123
|
+
@certificate_store.add_cert(cert)
|
124
|
+
rescue OpenSSL::X509::StoreError => e
|
125
|
+
# If the cert is already trusted, move along.
|
126
|
+
if e.to_s != "cert already in hash table"
|
127
|
+
raise # this is a real error, reraise.
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# TODO(sissel): Factor this out into Agent::Trust or somesuch
|
132
|
+
# For each certificate, if persistence is requested, write the cert to
|
133
|
+
# the configured ssl trust store (usually ~/.ftw/ssl-trust.db/)
|
134
|
+
if answer == "p" # persist this trusted cert
|
135
|
+
require "fileutils"
|
136
|
+
if !File.directory?(configuration[SSL_TRUST_STORE])
|
137
|
+
FileUtils.mkdir_p(configuration[SSL_TRUST_STORE])
|
138
|
+
end
|
139
|
+
|
140
|
+
# openssl verify recommends the 'ca path' have files named by the
|
141
|
+
# hashed subject name. Turns out openssl really expects the
|
142
|
+
# hexadecimal version of this.
|
143
|
+
name = File.join(configuration[SSL_TRUST_STORE], cert.subject.hash.to_s(16))
|
144
|
+
# Find a filename that doesn't exist.
|
145
|
+
num = 0
|
146
|
+
num += 1 while File.exists?("#{name}.#{num}")
|
147
|
+
|
148
|
+
# Write it out
|
149
|
+
path = "#{name}.#{num}"
|
150
|
+
@logger.info("Persisting certificate", :subject => cert.subject, :path => path)
|
151
|
+
File.write(path, cert.to_pem)
|
152
|
+
end # if answer == "p"
|
153
|
+
end # context.chain.each
|
154
|
+
return true
|
155
|
+
end # if answer was "y" or "p"
|
156
|
+
end # if !verified and stdout is a tty
|
157
|
+
|
158
|
+
return verified
|
159
|
+
rescue => e
|
160
|
+
# We have to rescue all and emit because openssl verify_callback ignores
|
161
|
+
# exceptions silently
|
162
|
+
@logger.error(e)
|
163
|
+
return verified
|
164
|
+
end
|
165
|
+
end # def certificate_verify
|
166
|
+
|
75
167
|
# Define all the standard HTTP methods (Per RFC2616)
|
76
168
|
# As an example, for "get" method, this will define these methods:
|
77
169
|
#
|
@@ -180,15 +272,11 @@ class FTW::Agent
|
|
180
272
|
def execute(request)
|
181
273
|
# TODO(sissel): Make redirection-following optional, but default.
|
182
274
|
|
183
|
-
connection, error = connect(request.headers["Host"], request.port)
|
275
|
+
connection, error = connect(request.headers["Host"], request.port, request.protocol == "https")
|
184
276
|
if !error.nil?
|
185
277
|
p :error => error
|
186
278
|
raise error
|
187
279
|
end
|
188
|
-
|
189
|
-
if request.protocol == "https"
|
190
|
-
connection.secure(:certificate_store => @certificate_store)
|
191
|
-
end
|
192
280
|
response = request.execute(connection)
|
193
281
|
|
194
282
|
redirects = 0
|
@@ -222,15 +310,12 @@ class FTW::Agent
|
|
222
310
|
|
223
311
|
@logger.debug("Redirecting", :location => response.headers["Location"])
|
224
312
|
request.use_uri(response.headers["Location"])
|
225
|
-
connection, error = connect(request.headers["Host"], request.port)
|
313
|
+
connection, error = connect(request.headers["Host"], request.port, request.protocol == "https")
|
226
314
|
# TODO(sissel): Do better error handling than raising.
|
227
315
|
if !error.nil?
|
228
316
|
p :error => error
|
229
317
|
raise error
|
230
318
|
end
|
231
|
-
if request.protocol == "https"
|
232
|
-
connection.secure(:certificate_store => @certificate_store)
|
233
|
-
end
|
234
319
|
response = request.execute(connection)
|
235
320
|
end # while being redirected
|
236
321
|
|
@@ -258,10 +343,11 @@ class FTW::Agent
|
|
258
343
|
end # def shutdown
|
259
344
|
|
260
345
|
# Returns a FTW::Connection connected to this host:port.
|
261
|
-
def connect(host, port)
|
346
|
+
def connect(host, port, secure=false)
|
262
347
|
address = "#{host}:#{port}"
|
263
348
|
@logger.debug("Fetching from pool", :address => address)
|
264
349
|
error = nil
|
350
|
+
|
265
351
|
connection = @pool.fetch(address) do
|
266
352
|
@logger.info("New connection to #{address}")
|
267
353
|
connection = FTW::Connection.new(address)
|
@@ -282,6 +368,20 @@ class FTW::Agent
|
|
282
368
|
|
283
369
|
@logger.debug("Pool fetched a connection", :connection => connection)
|
284
370
|
connection.mark
|
371
|
+
|
372
|
+
if secure
|
373
|
+
# Curry a certificate_verify callback for this connection.
|
374
|
+
verify_callback = proc do |verified, context|
|
375
|
+
begin
|
376
|
+
certificate_verify(host, port, verified, context)
|
377
|
+
rescue => e
|
378
|
+
@logger.error("Error in certificate_verify call", :exception => e)
|
379
|
+
end
|
380
|
+
end
|
381
|
+
connection.secure(:certificate_store => @certificate_store,
|
382
|
+
:verify_callback => verify_callback)
|
383
|
+
end # if secure
|
384
|
+
|
285
385
|
return connection, nil
|
286
386
|
end # def connect
|
287
387
|
|
@@ -6,8 +6,29 @@ module FTW::Agent::Configuration
|
|
6
6
|
# giving up.
|
7
7
|
REDIRECTION_LIMIT = "redirection-limit".freeze
|
8
8
|
|
9
|
+
# SSL Trust Store
|
10
|
+
SSL_TRUST_STORE = "ssl.trustdb".freeze
|
11
|
+
|
12
|
+
private
|
13
|
+
|
9
14
|
# Get the configuration hash
|
10
15
|
def configuration
|
11
|
-
return @configuration ||=
|
16
|
+
return @configuration ||= default_configuration
|
12
17
|
end # def configuration
|
18
|
+
|
19
|
+
# default configuration
|
20
|
+
def default_configuration
|
21
|
+
require "tmpdir"
|
22
|
+
home = File.join(ENV.fetch("HOME", tmpdir), ".ftw")
|
23
|
+
return {
|
24
|
+
REDIRECTION_LIMIT => 20,
|
25
|
+
SSL_TRUST_STORE => File.join(home, "ssl-trust.db")
|
26
|
+
}
|
27
|
+
end # def default_configuration
|
28
|
+
|
29
|
+
def tmpdir
|
30
|
+
return File.join(Dir.tmpdir, "ftw-#{Process.uid}")
|
31
|
+
end # def tmpdir
|
32
|
+
|
33
|
+
public(:configuration)
|
13
34
|
end # def FTW::Agent::Configuration
|
data/lib/ftw/connection.rb
CHANGED
@@ -300,19 +300,28 @@ class FTW::Connection
|
|
300
300
|
#
|
301
301
|
# * :certificate_store, an OpenSSL::X509::Store
|
302
302
|
# * :timeout, a timeout threshold in seconds.
|
303
|
-
def secure(options=
|
303
|
+
def secure(options=nil)
|
304
304
|
# Skip this if we're already secure.
|
305
305
|
return if secured?
|
306
306
|
|
307
|
-
|
308
|
-
|
307
|
+
defaults = {
|
308
|
+
:timeout => nil,
|
309
|
+
#:certificate_store => OpenSSL::SSL::SSLContext::DEFAULT_CERT_STORE
|
310
|
+
}
|
311
|
+
settings = defaults.merge(options) unless options.nil?
|
309
312
|
|
310
|
-
@logger.info("Securing this connection", :peer => peer)
|
313
|
+
@logger.info("Securing this connection", :peer => peer, :options => settings)
|
311
314
|
# Wrap this connection with TLS/SSL
|
312
315
|
sslcontext = OpenSSL::SSL::SSLContext.new
|
313
316
|
# If you use VERIFY_NONE, you are removing the trust feature of TLS. Don't do that.
|
314
317
|
# Encryption without trust means you don't know who you are talking to.
|
315
318
|
sslcontext.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
319
|
+
sslcontext.verify_callback = proc do |*args|
|
320
|
+
@logger.debug("Verify peer via FTW::Connection#secure", :callback => settings[:verify_callback])
|
321
|
+
if settings[:verify_callback].respond_to?(:call)
|
322
|
+
settings[:verify_callback].call(*args)
|
323
|
+
end
|
324
|
+
end
|
316
325
|
sslcontext.ssl_version = :TLSv1
|
317
326
|
sslcontext.cert_store = options[:certificate_store]
|
318
327
|
@socket = OpenSSL::SSL::SSLSocket.new(@socket, sslcontext)
|
data/lib/ftw/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ftw
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.17
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-04-
|
12
|
+
date: 2012-04-24 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: json
|