ftw 0.0.16 → 0.0.17

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