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 +48 -10
- data/lib/ftw/agent/configuration.rb +9 -0
- data/lib/ftw/connection.rb +31 -13
- data/lib/ftw/http/message.rb +6 -1
- data/lib/ftw/request.rb +8 -0
- data/lib/ftw/version.rb +1 -1
- data/lib/rack/handler/ftw.rb +10 -0
- metadata +4 -3
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
data/lib/ftw/connection.rb
CHANGED
@@ -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.
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
295
|
-
sslcontext.
|
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.
|
data/lib/ftw/http/message.rb
CHANGED
@@ -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
|
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
data/lib/rack/handler/ftw.rb
CHANGED
@@ -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.
|
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
|
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.
|
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
|