ftw 0.0.11 → 0.0.13

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 CHANGED
@@ -6,6 +6,7 @@ require "ftw/pool"
6
6
  require "ftw/websocket"
7
7
  require "addressable/uri"
8
8
  require "cabin"
9
+ require "openssl"
9
10
 
10
11
  # This should act as a proper web agent.
11
12
  #
@@ -36,6 +37,16 @@ require "cabin"
36
37
  # TODO(sissel): TBD: implement cookies... delicious chocolate chip cookies.
37
38
  class FTW::Agent
38
39
  include FTW::Protocol
40
+ require "ftw/agent/configuration"
41
+ include FTW::Agent::Configuration
42
+
43
+ class TooManyRedirects < StandardError
44
+ attr_accessor :response
45
+ def initialize(reason, response)
46
+ super(reason)
47
+ @response = response
48
+ end
49
+ end
39
50
 
40
51
  # List of standard HTTP methods described in RFC2616
41
52
  STANDARD_METHODS = %w(options get head post put delete trace connect)
@@ -48,7 +59,15 @@ class FTW::Agent
48
59
  @pool = FTW::Pool.new
49
60
  @logger = Cabin::Channel.get
50
61
 
51
- @redirect_max = 20
62
+ configuration[REDIRECTION_LIMIT] = 20
63
+
64
+ @certificate_store = OpenSSL::X509::Store.new
65
+ @certificate_store.add_file("/etc/ssl/certs/ca-bundle.trust.crt")
66
+ @certificate_store.verify_callback = proc do |*args|
67
+ p :verify_callback => args
68
+ true
69
+ end
70
+
52
71
  end # def initialize
53
72
 
54
73
  # Define all the standard HTTP methods (Per RFC2616)
@@ -139,6 +158,10 @@ class FTW::Agent
139
158
  end
140
159
  end
141
160
 
161
+ if options.include?(:body)
162
+ request.body = options[:body]
163
+ end
164
+
142
165
  return request
143
166
  end # def request
144
167
 
@@ -160,15 +183,15 @@ class FTW::Agent
160
183
  p :error => error
161
184
  raise error
162
185
  end
163
- connection.secure if request.protocol == "https"
186
+
187
+ if request.protocol == "https"
188
+ connection.secure(:certificate_store => @certificate_store)
189
+ end
164
190
  response = request.execute(connection)
165
191
 
166
192
  redirects = 0
193
+ # Follow redirects
167
194
  while response.redirect? and response.headers.include?("Location")
168
- redirects += 1
169
- if redirects > @redirect_max
170
- # TODO(sissel): Abort somehow...
171
- end
172
195
  # RFC2616 section 10.3.3 indicates HEAD redirects must not include a
173
196
  # body. Otherwise, the redirect response can have a body, so let's
174
197
  # throw it away.
@@ -178,15 +201,24 @@ class FTW::Agent
178
201
  elsif response.content?
179
202
  # Throw away the body
180
203
  response.body = connection
181
- # read_body will release the connection
204
+ # read_body will consume the body and release this connection
182
205
  response.read_body { |chunk| }
183
206
  end
184
207
 
185
208
  # TODO(sissel): If this response has any cookies, store them in the
186
209
  # agent's cookie store
187
210
 
188
- @logger.debug("Redirecting", :location => response.headers["Location"])
189
211
  redirects += 1
212
+ if redirects > configuration[REDIRECTION_LIMIT]
213
+ # TODO(sissel): include original a useful debugging information like
214
+ # the trace of redirections, etc.
215
+ raise TooManyRedirects.new("Redirect more than " \
216
+ "#{configuration[REDIRECTION_LIMIT]} times, aborting.", response)
217
+ # I don't like this api from FTW::Agent. I think 'get' and other methods
218
+ # should return (object, error), and if there's an error
219
+ end
220
+
221
+ @logger.debug("Redirecting", :location => response.headers["Location"])
190
222
  request.use_uri(response.headers["Location"])
191
223
  connection, error = connect(request.headers["Host"], request.port)
192
224
  # TODO(sissel): Do better error handling than raising.
@@ -194,9 +226,11 @@ class FTW::Agent
194
226
  p :error => error
195
227
  raise error
196
228
  end
197
- connection.secure if request.protocol == "https"
229
+ if request.protocol == "https"
230
+ connection.secure(:certificate_store => @certificate_store)
231
+ end
198
232
  response = request.execute(connection)
199
- end
233
+ end # while being redirected
200
234
 
201
235
  # RFC 2616 section 9.4, HEAD requests MUST NOT have a message body.
202
236
  if request.method != "HEAD"
@@ -249,5 +283,9 @@ class FTW::Agent
249
283
  return connection, nil
250
284
  end # def connect
251
285
 
286
+ # TODO(sissel): Implement methods for managing the certificate store
287
+ # TODO(sissel): Implement methods for managing the cookie store
288
+ # TODO(sissel): Implement methods for managing the cache
289
+ # TODO(sissel): Implement configuration stuff? Is FTW::Agent::Configuration the best way?
252
290
  public(:initialize, :execute, :websocket!, :upgrade!, :shutdown)
253
291
  end # class FTW::Agent
@@ -0,0 +1,9 @@
1
+ require "ftw"
2
+
3
+ module FTW::Agent::Configuration
4
+ REDIRECTION_LIMIT = "redirection-limit".freeze
5
+
6
+ def configuration
7
+ return @configuration ||= Hash.new
8
+ end
9
+ end
@@ -5,6 +5,7 @@ require "ftw/namespace"
5
5
  require "socket"
6
6
  require "timeout" # ruby stdlib, just for the Timeout exception.
7
7
  require "backports" # for Array#rotate, IO::WaitWritable, etc, in ruby < 1.9
8
+ require "openssl"
8
9
 
9
10
  # A network connection. This is TCP.
10
11
  #
@@ -53,6 +54,7 @@ class FTW::Connection
53
54
  @destinations = destinations
54
55
  end
55
56
 
57
+ @mode = :client
56
58
  setup
57
59
  end # def initialize
58
60
 
@@ -136,7 +138,7 @@ class FTW::Connection
136
138
  @socket = Socket.new(family, Socket::SOCK_STREAM, 0)
137
139
 
138
140
  # This api is terrible. pack_sockaddr_in? This isn't C, man...
139
- @logger.info("packing", :data => [port.to_i, @remote_address])
141
+ @logger.debug("packing", :data => [port.to_i, @remote_address])
140
142
  sockaddr = Socket.pack_sockaddr_in(port.to_i, @remote_address)
141
143
  # TODO(sissel): Support local address binding
142
144
 
@@ -148,13 +150,18 @@ class FTW::Connection
148
150
  # the documentation says to use this IO::WaitWritable thing...
149
151
  # I don't get it, but whatever :(
150
152
 
151
- if writable?(timeout)
153
+ writable = writable?(timeout)
154
+
155
+ # http://jira.codehaus.org/browse/JRUBY-6528; IO.select doesn't behave correctly
156
+ # on JRuby < 1.7, so work around it.
157
+ if writable || (RUBY_PLATFORM == "java" and JRUBY_VERSION < "1.7.0")
152
158
  begin
153
159
  @socket.connect_nonblock(sockaddr) # check connection failure
154
160
  rescue Errno::EISCONN
155
161
  # Ignore, we're already connected.
156
162
  rescue Errno::ECONNREFUSED => e
157
163
  # Fire 'disconnected' event with reason :refused
164
+ @socket.close
158
165
  return ConnectRefused.new("#{host}[#{@remote_address}]:#{port}")
159
166
  rescue Errno::ETIMEDOUT
160
167
  # This occurs when the system's TCP timeout hits, we have no control
@@ -162,11 +169,16 @@ class FTW::Connection
162
169
  # for this, but I haven't checked..
163
170
  # TODO(sissel): We should instead do 'retry' unless we've exceeded
164
171
  # the timeout.
172
+ @socket.close
173
+ return ConnectTimeout.new("#{host}[#{@remote_address}]:#{port}")
174
+ rescue Errno::EINPROGRESS
175
+ # If we get here, it's likely JRuby version < 1.7.0. EINPROGRESS at
176
+ # this point in the code means that we have timed out.
177
+ @socket.close
165
178
  return ConnectTimeout.new("#{host}[#{@remote_address}]:#{port}")
166
179
  end
167
180
  else
168
- # Connection timeout
169
- # Fire 'disconnected' event with reason :timeout
181
+ # Connection timeout;
170
182
  return ConnectTimeout.new("#{host}[#{@remote_address}]:#{port}")
171
183
  end
172
184
  end
@@ -279,29 +291,35 @@ class FTW::Connection
279
291
  end # def to_io
280
292
 
281
293
  # Secure this connection with TLS.
282
- def secure(timeout=nil, options={})
294
+ #
295
+ # Options:
296
+ #
297
+ # * :certificate_store, an OpenSSL::X509::Store
298
+ # * :timeout, a timeout threshold in seconds.
299
+ def secure(options={})
283
300
  # Skip this if we're already secure.
284
301
  return if secured?
285
302
 
286
- @logger.debug("Securing this connection", :peer => peer, :connection => self)
303
+ options[:timeout] ||= nil
304
+ options[:certificate_store] ||= OpenSSL::SSL::SSLContext::DEFAULT_CERT_STORE
305
+
306
+ @logger.info("Securing this connection", :peer => peer)
287
307
  # Wrap this connection with TLS/SSL
288
- require "openssl"
289
308
  sslcontext = OpenSSL::SSL::SSLContext.new
290
- sslcontext.ssl_version = :TLSv1
291
309
  # If you use VERIFY_NONE, you are removing the trust feature of TLS. Don't do that.
292
310
  # Encryption without trust means you don't know who you are talking to.
293
311
  sslcontext.verify_mode = OpenSSL::SSL::VERIFY_PEER
294
- # TODO(sissel): Try to be smart about setting this default.
295
- sslcontext.ca_path = "/etc/ssl/certs"
312
+ sslcontext.ssl_version = :TLSv1
313
+ sslcontext.cert_store = options[:certificate_store]
296
314
  @socket = OpenSSL::SSL::SSLSocket.new(@socket, sslcontext)
297
315
 
298
316
  # TODO(sissel): Set up local certificat/key stuff. This is required for
299
317
  # server-side ssl operation, I think.
300
318
 
301
319
  if client?
302
- do_secure(:connect_nonblock)
320
+ do_secure(:connect_nonblock, options[:timeout])
303
321
  else
304
- do_secure(:accept_nonblock)
322
+ do_secure(:accept_nonblock, options[:timeout])
305
323
  end
306
324
  end # def secure
307
325
 
@@ -313,7 +331,7 @@ class FTW::Connection
313
331
  # @param [Symbol] handshake_method The method to call on the socket to
314
332
  # complete the ssl handshake. See OpenSSL::SSL::SSLSocket#connect_nonblock
315
333
  # of #accept_nonblock for more details
316
- def do_secure(handshake_method)
334
+ def do_secure(handshake_method, timeout=nil)
317
335
  # SSLSocket#connect_nonblock will do the SSL/TLS handshake.
318
336
  # TODO(sissel): refactor this into a method that both secure and connect
319
337
  # methods can call.
@@ -54,6 +54,11 @@ module FTW::HTTP::Message
54
54
  # TODO(sissel): if it's an IO object, set Transfer-Encoding to chunked
55
55
  # TODO(sissel): if it responds to each or appears to be Enumerable, then
56
56
  # set Transfer-Encoding to chunked.
57
+ if message_body.is_a?(IO)
58
+ headers["Transfer-Encoding"] = "chunked"
59
+ else
60
+ headers["Content-Length"] = message_body.length
61
+ end
57
62
  @body = message_body
58
63
  end # def body=
59
64
 
@@ -83,7 +88,7 @@ module FTW::HTTP::Message
83
88
 
84
89
  # Does this message have a body?
85
90
  def body?
86
- return @body.nil?
91
+ return !@body.nil?
87
92
  end # def body?
88
93
 
89
94
  # Set the HTTP version. Must be a valid version. See VALID_VERSIONS.
data/lib/ftw/request.rb CHANGED
@@ -72,6 +72,14 @@ class FTW::Request
72
72
  tries = 3
73
73
  begin
74
74
  connection.write(to_s + CRLF)
75
+
76
+ if body?
77
+ if body.is_a?(String)
78
+ connection.write(body)
79
+ else
80
+ connection.write(data) while data = body.read(16384)
81
+ end
82
+ end
75
83
  rescue => e
76
84
  # TODO(sissel): Rescue specific exceptions, not just anything.
77
85
  # Reconnect and retry
data/lib/ftw/version.rb CHANGED
@@ -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.11"
6
+ VERSION = "0.0.13"
7
7
  end
@@ -93,6 +93,16 @@ class Rack::Handler::FTW
93
93
  logger.info("Starting server", :config => @config)
94
94
  @server = FTW::Server.new([@config[:Host], @config[:Port]].join(":"))
95
95
  @server.each_connection do |connection|
96
+ # The rack specification insists that 'rack.input' objects support
97
+ # #rewind. Bleh. Just lie about it and monkeypatch it in.
98
+ # This is required for Sinatra to accept 'post' requests, otherwise
99
+ # it barfs.
100
+ class << connection
101
+ def rewind(*args)
102
+ # lolrack, nothing to do here.
103
+ end
104
+ end
105
+
96
106
  @threads << Thread.new do
97
107
  handle_connection(connection)
98
108
  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.11
4
+ version: 0.0.13
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-03-24 00:00:00.000000000 Z
12
+ date: 2012-04-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: json
@@ -131,6 +131,7 @@ files:
131
131
  - lib/ftw/websocket.rb
132
132
  - lib/ftw/http/message.rb
133
133
  - lib/ftw/http/headers.rb
134
+ - lib/ftw/agent/configuration.rb
134
135
  - lib/ftw/request.rb
135
136
  - lib/ftw/protocol.rb
136
137
  - lib/ftw/response.rb
@@ -170,7 +171,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
170
171
  version: '0'
171
172
  requirements: []
172
173
  rubyforge_project:
173
- rubygems_version: 1.8.18
174
+ rubygems_version: 1.8.21
174
175
  signing_key:
175
176
  specification_version: 3
176
177
  summary: For The Web. Trying to build a solid and sane API for client and server web