ftw 0.0.11 → 0.0.13

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