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.
@@ -64,14 +64,106 @@ class FTW::Agent
64
64
  configuration[REDIRECTION_LIMIT] = 20
65
65
 
66
66
  @certificate_store = OpenSSL::X509::Store.new
67
- @certificate_store.add_file("/etc/ssl/certs/ca-bundle.trust.crt")
68
- @certificate_store.verify_callback = proc do |*args|
69
- p :verify_callback => args
70
- true
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 ||= Hash.new
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
@@ -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
- options[:timeout] ||= nil
308
- options[:certificate_store] ||= OpenSSL::SSL::SSLContext::DEFAULT_CERT_STORE
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)
@@ -3,5 +3,5 @@ require "ftw/namespace"
3
3
  # :nodoc:
4
4
  module FTW
5
5
  # The version of this library
6
- VERSION = "0.0.16"
6
+ VERSION = "0.0.17"
7
7
  end
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.16
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-20 00:00:00.000000000 Z
12
+ date: 2012-04-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: json