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 +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
|