ftw 0.0.1 → 0.0.4
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/README.md +7 -8
- data/lib/ftw.rb +4 -0
- data/lib/ftw/agent.rb +203 -20
- data/lib/ftw/connection.rb +117 -63
- data/lib/ftw/cookies.rb +87 -0
- data/lib/ftw/crlf.rb +1 -1
- data/lib/ftw/dns.rb +14 -5
- data/lib/ftw/http/headers.rb +15 -1
- data/lib/ftw/http/message.rb +9 -1
- data/lib/ftw/namespace.rb +1 -0
- data/lib/ftw/pool.rb +50 -0
- data/lib/ftw/poolable.rb +19 -0
- data/lib/ftw/request.rb +92 -28
- data/lib/ftw/response.rb +179 -0
- data/lib/ftw/version.rb +1 -1
- data/lib/ftw/websocket.rb +194 -0
- data/lib/ftw/websocket/parser.rb +183 -0
- data/test/all.rb +16 -0
- data/test/ftw/crlf.rb +12 -0
- data/test/ftw/http/dns.rb +6 -0
- data/test/{net/ftw → ftw}/http/headers.rb +5 -5
- data/test/testing.rb +0 -9
- metadata +13 -26
- data/lib/net-ftw.rb +0 -1
- data/lib/net/ftw.rb +0 -5
- data/lib/net/ftw/agent.rb +0 -10
- data/lib/net/ftw/connection.rb +0 -296
- data/lib/net/ftw/connection2.rb +0 -247
- data/lib/net/ftw/crlf.rb +0 -6
- data/lib/net/ftw/dns.rb +0 -57
- data/lib/net/ftw/http.rb +0 -2
- data/lib/net/ftw/http/client.rb +0 -116
- data/lib/net/ftw/http/client2.rb +0 -80
- data/lib/net/ftw/http/connection.rb +0 -42
- data/lib/net/ftw/http/headers.rb +0 -122
- data/lib/net/ftw/http/machine.rb +0 -38
- data/lib/net/ftw/http/message.rb +0 -91
- data/lib/net/ftw/http/request.rb +0 -80
- data/lib/net/ftw/http/response.rb +0 -80
- data/lib/net/ftw/http/server.rb +0 -5
- data/lib/net/ftw/machine.rb +0 -59
- data/lib/net/ftw/namespace.rb +0 -6
- data/lib/net/ftw/protocol/tls.rb +0 -12
- data/lib/net/ftw/websocket.rb +0 -139
- data/test/net/ftw/crlf.rb +0 -12
- data/test/net/ftw/http/dns.rb +0 -6
data/README.md
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
# For The Web
|
2
2
|
|
3
|
-
net/http is pretty much not good.
|
3
|
+
net/http is pretty much not good. dns behavior in ruby changes quite frequently.
|
4
|
+
|
5
|
+
Above all else, I want a consistent API and behavior that I can rely on. Ruby stdlib is not that thing.
|
4
6
|
|
5
7
|
I want:
|
6
8
|
|
7
|
-
* A HTTP client that acts as a full user agent, not just a single connection
|
9
|
+
* A HTTP client that acts as a full user agent, not just a single connections. (With connection reuse)
|
8
10
|
* HTTP and SPDY support.
|
9
11
|
* WebSockets support.
|
10
12
|
* SSL/TLS support.
|
@@ -12,12 +14,6 @@ I want:
|
|
12
14
|
* Server and Client modes.
|
13
15
|
* Support for both normal operation and EventMachine would be nice.
|
14
16
|
|
15
|
-
## DONE
|
16
|
-
|
17
|
-
* TCP connection
|
18
|
-
* DNS resolution (wraps Socket.gethostname)
|
19
|
-
* HTTP client partially done
|
20
|
-
|
21
17
|
## TODO
|
22
18
|
|
23
19
|
* Tests, yo.
|
@@ -32,6 +28,9 @@ I want:
|
|
32
28
|
request = agent.get("http://www.google.com/")
|
33
29
|
response = request.execute
|
34
30
|
|
31
|
+
# Simpler
|
32
|
+
response = agent.get!("http://www.google.com/")
|
33
|
+
|
35
34
|
### SPDY
|
36
35
|
|
37
36
|
# SPDY should automatically be attempted. The caller should be unaware.
|
data/lib/ftw.rb
ADDED
data/lib/ftw/agent.rb
CHANGED
@@ -1,40 +1,223 @@
|
|
1
1
|
require "ftw/namespace"
|
2
2
|
require "ftw/request"
|
3
3
|
require "ftw/connection"
|
4
|
+
require "ftw/pool"
|
5
|
+
require "ftw/websocket"
|
4
6
|
require "addressable/uri"
|
7
|
+
require "cabin"
|
8
|
+
require "logger"
|
5
9
|
|
6
|
-
# This should act as a proper agent.
|
10
|
+
# This should act as a proper web agent.
|
7
11
|
#
|
8
|
-
# *
|
9
|
-
# *
|
10
|
-
# * HTTP Upgrade support
|
11
|
-
# *
|
12
|
-
# *
|
12
|
+
# * Reuse connections.
|
13
|
+
# * SSL/TLS.
|
14
|
+
# * HTTP Upgrade support.
|
15
|
+
# * HTTP 1.1 (RFC2616).
|
16
|
+
# * WebSockets (RFC6455).
|
17
|
+
# * Support Cookies.
|
18
|
+
#
|
19
|
+
# All standard HTTP methods defined by RFC2616 are available as methods on
|
20
|
+
# this agent: get, head, put, etc.
|
21
|
+
#
|
22
|
+
# Example:
|
23
|
+
#
|
24
|
+
# agent = FTW::Agent.new
|
25
|
+
# request = agent.get("http://www.google.com/")
|
26
|
+
# response = agent.execute(request)
|
27
|
+
# puts response.body.read
|
28
|
+
#
|
29
|
+
# For any standard http method (like 'get') you can invoke it with '!' on the end
|
30
|
+
# and it will execute and return a FTW::Response object:
|
31
|
+
#
|
32
|
+
# agent = FTW::Agent.new
|
33
|
+
# response = agent.get!("http://www.google.com/")
|
34
|
+
# puts response.body.head
|
35
|
+
#
|
36
|
+
# TODO(sissel): TBD: implement cookies... delicious chocolate chip cookies.
|
13
37
|
class FTW::Agent
|
14
|
-
|
15
|
-
|
16
|
-
|
38
|
+
STANDARD_METHODS = %w(options get head post put delete trace connect)
|
39
|
+
|
17
40
|
def initialize
|
18
|
-
|
41
|
+
@pool = FTW::Pool.new
|
42
|
+
@logger = Cabin::Channel.get($0)
|
43
|
+
@logger.subscribe(Logger.new(STDOUT))
|
44
|
+
@logger.level = :warn
|
45
|
+
|
46
|
+
@redirect_max = 20
|
47
|
+
end # def initialize
|
48
|
+
|
49
|
+
# Define all the standard HTTP methods (Per RFC2616)
|
50
|
+
# As an example, for "get" method, this will define these methods:
|
51
|
+
#
|
52
|
+
# * FTW::Agent#get(uri, options={})
|
53
|
+
# * FTW::Agent#get!(uri, options={})
|
54
|
+
#
|
55
|
+
# The first one returns a FTW::Request you must pass to Agent#execute(...)
|
56
|
+
# The second does the execute for you and returns a FTW::Response.
|
57
|
+
#
|
58
|
+
# For a full list of these available methods, see STANDARD_METHODS.
|
59
|
+
STANDARD_METHODS.each do |name|
|
60
|
+
m = name.upcase
|
61
|
+
|
62
|
+
# define 'get' etc method.
|
63
|
+
define_method(name.to_sym) do |uri, options={}|
|
64
|
+
return request(m, uri, options)
|
65
|
+
end
|
66
|
+
|
67
|
+
# define 'get!' etc method.
|
68
|
+
define_method("#{name}!".to_sym) do |uri, options={}|
|
69
|
+
return execute(request(m, uri, options))
|
70
|
+
end
|
71
|
+
end # STANDARD_METHODS.each
|
19
72
|
|
20
|
-
#
|
21
|
-
#
|
22
|
-
|
23
|
-
|
24
|
-
|
73
|
+
# Send the request as an HTTP upgrade.
|
74
|
+
#
|
75
|
+
# Returns the response and the FTW::Connection for this connection.
|
76
|
+
# If the upgrade was denied, the connection returned will be nil.
|
77
|
+
def upgrade!(uri, protocol, options={})
|
78
|
+
req = request("GET", uri, options)
|
79
|
+
req.headers["Connection"] = "Upgrade"
|
80
|
+
req.headers["Upgrade"] = protocol
|
81
|
+
response = execute(req)
|
82
|
+
if response.status == 101
|
83
|
+
return response, response.body
|
84
|
+
else
|
85
|
+
return response, nil
|
86
|
+
end
|
87
|
+
end # def upgrade!
|
25
88
|
|
89
|
+
# Make a new websocket connection.
|
90
|
+
#
|
91
|
+
# This will send the http request. If the websocket handshake
|
92
|
+
# is successful, a FTW::WebSocket instance will be returned.
|
93
|
+
# Otherwise, a FTW::Response will be returned.
|
94
|
+
public
|
95
|
+
def websocket!(uri, options={})
|
96
|
+
# TODO(sissel): Use FTW::Agent#upgrade! ?
|
97
|
+
req = request("GET", uri, options)
|
98
|
+
ws = FTW::WebSocket.new(req)
|
99
|
+
response = execute(req)
|
100
|
+
if ws.handshake_ok?(response)
|
101
|
+
# response.body is a FTW::Connection
|
102
|
+
ws.connection = response.body
|
103
|
+
|
104
|
+
# TODO(sissel): Investigate this bug
|
105
|
+
# There seems to be a bug in http_parser.rb (or in this library) where
|
106
|
+
# websocket responses lead with a newline for some reason. Work around
|
107
|
+
# it.
|
108
|
+
data = response.body.read
|
109
|
+
if data[0] == "\n"
|
110
|
+
response.body.pushback(data[1..-1])
|
111
|
+
else
|
112
|
+
response.body.pushback(data)
|
113
|
+
end
|
114
|
+
return ws
|
115
|
+
else
|
116
|
+
return response
|
117
|
+
end
|
118
|
+
end # def websocket!
|
119
|
+
|
120
|
+
# Make a request. Returns a FTW::Request object.
|
121
|
+
#
|
122
|
+
# Arguments:
|
123
|
+
#
|
124
|
+
# * method - the http method
|
125
|
+
# * uri - the URI to make the request to
|
126
|
+
# * options - a hash of options
|
127
|
+
#
|
128
|
+
# uri can be a valid url or an Addressable::URI object.
|
129
|
+
# The uri will be used to choose the host/port to connect to. It also sets
|
130
|
+
# the protocol (https, etc). Further, it will set the 'Host' header.
|
131
|
+
#
|
132
|
+
# The 'options' hash supports the following keys:
|
133
|
+
#
|
134
|
+
# * :headers => { string => string, ... }. This allows you to set header values.
|
135
|
+
public
|
26
136
|
def request(method, uri, options)
|
137
|
+
@logger.info("Creating new request", :method => method, :uri => uri, :options => options)
|
27
138
|
request = FTW::Request.new(uri)
|
28
139
|
request.method = method
|
29
|
-
request.
|
140
|
+
request.headers.add("Connection", "keep-alive")
|
141
|
+
|
142
|
+
if options.include?(:headers)
|
143
|
+
options[:headers].each do |key, value|
|
144
|
+
request.headers.add(key, value)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
30
148
|
return request
|
31
149
|
end # def request
|
32
150
|
|
151
|
+
# Execute a FTW::Request in this Agent.
|
152
|
+
#
|
153
|
+
# If an existing, idle connection is already open to the target server
|
154
|
+
# of this Request, it will be reused. Otherwise, a new connection
|
155
|
+
# is opened.
|
156
|
+
#
|
157
|
+
# Redirects are always followed.
|
158
|
+
public
|
159
|
+
def execute(request)
|
160
|
+
# TODO(sissel): Make redirection-following optional, but default.
|
161
|
+
|
162
|
+
connection = connect(request.headers["Host"], request.port)
|
163
|
+
connection.secure if request.protocol == "https"
|
164
|
+
response = request.execute(connection)
|
165
|
+
|
166
|
+
redirects = 0
|
167
|
+
while response.redirect? and response.headers.include?("Location")
|
168
|
+
redirects += 1
|
169
|
+
if redirects > @redirect_max
|
170
|
+
# TODO(sissel): Abort somehow...
|
171
|
+
end
|
172
|
+
# RFC2616 section 10.3.3 indicates HEAD redirects must not include a
|
173
|
+
# body. Otherwise, the redirect response can have a body, so let's
|
174
|
+
# throw it away.
|
175
|
+
if request.method == "HEAD"
|
176
|
+
# Head requests have no body
|
177
|
+
connection.release
|
178
|
+
elsif response.content?
|
179
|
+
# Throw away the body
|
180
|
+
response.body = connection
|
181
|
+
# read_body will release the connection
|
182
|
+
response.read_body { |chunk| }
|
183
|
+
end
|
184
|
+
|
185
|
+
# TODO(sissel): If this response has any cookies, store them in the
|
186
|
+
# agent's cookie store
|
187
|
+
|
188
|
+
@logger.debug("Redirecting", :location => response.headers["Location"])
|
189
|
+
redirects += 1
|
190
|
+
request.use_uri(response.headers["Location"])
|
191
|
+
connection = connect(request.headers["Host"], request.port)
|
192
|
+
connection.secure if request.protocol == "https"
|
193
|
+
response = request.execute(connection)
|
194
|
+
end
|
195
|
+
|
196
|
+
# RFC 2616 section 9.4, HEAD requests MUST NOT have a message body.
|
197
|
+
if request.method != "HEAD"
|
198
|
+
response.body = connection
|
199
|
+
else
|
200
|
+
connection.release
|
201
|
+
end
|
202
|
+
|
203
|
+
# TODO(sissel): If this response has any cookies, store them in the
|
204
|
+
# agent's cookie store
|
205
|
+
return response
|
206
|
+
end # def execute
|
207
|
+
|
33
208
|
# Returns a FTW::Connection connected to this host:port.
|
34
|
-
# TODO(sissel): Implement connection reuse
|
35
|
-
# TODO(sissel): support SSL/TLS
|
36
209
|
private
|
37
|
-
def
|
38
|
-
|
210
|
+
def connect(host, port)
|
211
|
+
address = "#{host}:#{port}"
|
212
|
+
@logger.debug("Fetching from pool", :address => address)
|
213
|
+
connection = @pool.fetch(address) do
|
214
|
+
@logger.info("New connection to #{address}")
|
215
|
+
connection = FTW::Connection.new(address)
|
216
|
+
connection.connect
|
217
|
+
connection
|
218
|
+
end
|
219
|
+
@logger.debug("Pool fetched a connection", :connection => connection)
|
220
|
+
connection.mark
|
221
|
+
return connection
|
39
222
|
end # def connect
|
40
223
|
end # class FTW::Agent
|
data/lib/ftw/connection.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require "cabin" # rubygem "cabin"
|
2
|
-
require "
|
3
|
-
require "
|
2
|
+
require "ftw/dns"
|
3
|
+
require "ftw/poolable"
|
4
|
+
require "ftw/namespace"
|
4
5
|
require "socket"
|
5
6
|
require "timeout" # ruby stdlib, just for the Timeout exception.
|
6
7
|
require "backport-bij" # for Array#rotate, IO::WaitWritable, etc, in ruby < 1.9
|
@@ -9,7 +10,15 @@ require "backport-bij" # for Array#rotate, IO::WaitWritable, etc, in ruby < 1.9
|
|
9
10
|
#
|
10
11
|
# You can use IO::select on this objects of this type.
|
11
12
|
# (at least, in MRI you can)
|
13
|
+
#
|
14
|
+
# You can activate SSL/TLS on this connection by invoking FTW::Connection#secure
|
12
15
|
class FTW::Connection
|
16
|
+
class ConnectTimeout < StandardError; end
|
17
|
+
class ReadTimeout < StandardError; end
|
18
|
+
class WriteTimeout < StandardError; end
|
19
|
+
include FTW::Poolable
|
20
|
+
include Cabin::Inspectable
|
21
|
+
|
13
22
|
# A new network connection.
|
14
23
|
# The 'destination' argument can be an array of strings or a single string.
|
15
24
|
# String format is expected to be "host:port"
|
@@ -28,21 +37,33 @@ class FTW::Connection
|
|
28
37
|
@destinations = destinations
|
29
38
|
end
|
30
39
|
|
40
|
+
@logger = Cabin::Channel.get($0)
|
31
41
|
@connect_timeout = 2
|
32
42
|
|
33
43
|
# Use a fixed-size string that we set to BINARY encoding.
|
34
44
|
# Not all byte sequences are UTF-8 friendly :0
|
35
45
|
@read_size = 16384
|
36
46
|
@read_buffer = " " * @read_size
|
47
|
+
@pushback_buffer = ""
|
37
48
|
|
38
49
|
# Tell Ruby 1.9 that this string is a binary string, not utf-8 or somesuch.
|
39
50
|
if @read_buffer.respond_to?(:force_encoding)
|
40
51
|
@read_buffer.force_encoding("BINARY")
|
41
52
|
end
|
42
53
|
|
54
|
+
@inspectables = [:@destinations, :@connected, :@remote_address, :@secure]
|
55
|
+
@connected = false
|
56
|
+
@remote_address = nil
|
57
|
+
@secure = false
|
58
|
+
|
43
59
|
# TODO(sissel): Validate @destinations
|
60
|
+
# TODO(sissel): Barf if a destination is not of the form "host:port"
|
44
61
|
end # def initialize
|
45
62
|
|
63
|
+
# Connect now.
|
64
|
+
#
|
65
|
+
# Timeout value is optional. If no timeout is given, this method
|
66
|
+
# blocks until a connection is successful or an error occurs.
|
46
67
|
public
|
47
68
|
def connect(timeout=nil)
|
48
69
|
# TODO(sissel): Raise if we're already connected?
|
@@ -53,6 +74,8 @@ class FTW::Connection
|
|
53
74
|
# Do dns resolution on the host. If there are multiple
|
54
75
|
# addresses resolved, return one at random.
|
55
76
|
@remote_address = FTW::DNS.singleton.resolve_random(host)
|
77
|
+
@logger.debug("Connecting", :address => @remote_address,
|
78
|
+
:host => host, :port => port)
|
56
79
|
|
57
80
|
# Addresses with colon ':' in them are assumed to be IPv6
|
58
81
|
family = @remote_address.include?(":") ? Socket::AF_INET6 : Socket::AF_INET
|
@@ -72,20 +95,22 @@ class FTW::Connection
|
|
72
95
|
if writable?(timeout)
|
73
96
|
begin
|
74
97
|
@socket.connect_nonblock(sockaddr) # check connection failure
|
75
|
-
rescue Errno::EISCONN
|
98
|
+
rescue Errno::EISCONN
|
99
|
+
# Ignore, we're already connected.
|
76
100
|
rescue Errno::ECONNREFUSED => e
|
77
101
|
# Fire 'disconnected' event with reason :refused
|
78
|
-
|
102
|
+
return e
|
79
103
|
end
|
80
104
|
else
|
81
105
|
# Connection timeout
|
82
106
|
# Fire 'disconnected' event with reason :timeout
|
83
|
-
|
107
|
+
return ConnectTimeout.new
|
84
108
|
end
|
85
109
|
end
|
86
110
|
|
87
111
|
# We're now connected.
|
88
|
-
|
112
|
+
@connected = true
|
113
|
+
return true
|
89
114
|
end # def connect
|
90
115
|
|
91
116
|
# Is this Connection connected?
|
@@ -97,14 +122,16 @@ class FTW::Connection
|
|
97
122
|
# Write data to this connection.
|
98
123
|
# This method blocks until the write succeeds unless a timeout is given.
|
99
124
|
#
|
100
|
-
#
|
125
|
+
# This method is not guaranteed to have written the full data given.
|
126
|
+
#
|
127
|
+
# Returns the number of bytes written (See also IO#syswrite)
|
101
128
|
public
|
102
129
|
def write(data, timeout=nil)
|
103
130
|
#connect if !connected?
|
104
131
|
if writable?(timeout)
|
105
132
|
return @socket.syswrite(data)
|
106
133
|
else
|
107
|
-
raise
|
134
|
+
raise FTW::Connection::WriteTimeout.new(self.inspect)
|
108
135
|
end
|
109
136
|
end # def write
|
110
137
|
|
@@ -115,47 +142,53 @@ class FTW::Connection
|
|
115
142
|
# IO#sysread
|
116
143
|
public
|
117
144
|
def read(timeout=nil)
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
145
|
+
data = ""
|
146
|
+
data.force_encoding("BINARY") if data.respond_to?(:force_encoding)
|
147
|
+
have_pushback = !@pushback_buffer.empty?
|
148
|
+
if have_pushback
|
149
|
+
data << @pushback_buffer
|
150
|
+
@pushback_buffer = ""
|
151
|
+
# We have data 'now' so don't wait.
|
152
|
+
timeout = 0
|
153
|
+
end
|
124
154
|
|
155
|
+
if readable?(timeout)
|
125
156
|
begin
|
126
157
|
@socket.sysread(@read_size, @read_buffer)
|
127
|
-
|
128
|
-
|
129
|
-
|
158
|
+
data << @read_buffer
|
159
|
+
return data
|
160
|
+
rescue EOFError => e
|
161
|
+
raise e
|
130
162
|
end
|
131
163
|
else
|
132
|
-
|
164
|
+
if have_pushback
|
165
|
+
return data
|
166
|
+
else
|
167
|
+
raise ReadTimeout.new
|
168
|
+
end
|
133
169
|
end
|
134
170
|
end # def read
|
135
171
|
|
136
|
-
#
|
172
|
+
# Push back some data onto the connection's read buffer.
|
137
173
|
public
|
138
|
-
def
|
139
|
-
@
|
140
|
-
end # def
|
174
|
+
def pushback(data)
|
175
|
+
@pushback_buffer << data
|
176
|
+
end # def pushback
|
141
177
|
|
142
178
|
# End this connection, specifying why.
|
143
179
|
public
|
144
180
|
def disconnect(reason)
|
145
181
|
begin
|
146
|
-
#@reader_closed = true
|
147
182
|
@socket.close_read
|
148
183
|
rescue IOError => e
|
149
|
-
# Ignore
|
184
|
+
# Ignore, perhaps we shouldn't ignore.
|
150
185
|
end
|
151
186
|
|
152
187
|
begin
|
153
188
|
@socket.close_write
|
154
189
|
rescue IOError => e
|
155
|
-
# Ignore
|
190
|
+
# Ignore, perhaps we shouldn't ignore.
|
156
191
|
end
|
157
|
-
|
158
|
-
trigger(DISCONNECTED, reason)
|
159
192
|
end # def disconnect
|
160
193
|
|
161
194
|
# Is this connection writable? Returns true if it is writable within
|
@@ -179,53 +212,74 @@ class FTW::Connection
|
|
179
212
|
return !ready.nil?
|
180
213
|
end # def readable?
|
181
214
|
|
182
|
-
protected
|
183
|
-
def connected(address)
|
184
|
-
@remote_address = nil
|
185
|
-
@connected = true
|
186
|
-
end # def connected
|
187
|
-
|
188
|
-
protected
|
189
|
-
def disconnected(reason, error)
|
190
|
-
@remote_address = nil
|
191
|
-
@connected = false
|
192
|
-
end # def disconnected
|
193
|
-
|
194
215
|
# The host:port
|
195
216
|
public
|
196
217
|
def peer
|
197
218
|
return @remote_address
|
198
219
|
end # def peer
|
199
220
|
|
200
|
-
#
|
201
|
-
# This is generally meant for Threaded or synchronous operation.
|
202
|
-
# For EventMachine, see TODO(sissel): Implement EventMachine support.
|
221
|
+
# Support 'to_io' so you can use IO::select on this object.
|
203
222
|
public
|
204
|
-
def
|
205
|
-
|
206
|
-
|
207
|
-
read_and_trigger
|
208
|
-
end
|
209
|
-
end # def run
|
223
|
+
def to_io
|
224
|
+
return @socket
|
225
|
+
end # def to_io
|
210
226
|
|
211
|
-
#
|
212
|
-
#
|
213
|
-
# This is mainly useful if you are implementing your own run loops
|
214
|
-
# and IO::select shenanigans.
|
227
|
+
# Secure this connection with TLS.
|
215
228
|
public
|
216
|
-
def
|
217
|
-
|
218
|
-
if
|
219
|
-
|
220
|
-
|
221
|
-
|
229
|
+
def secure(timeout=nil, options={})
|
230
|
+
# Skip this if we're already secure.
|
231
|
+
return if secured?
|
232
|
+
|
233
|
+
@logger.debug("Securing this connection", :peer => peer, :connection => self)
|
234
|
+
# Wrap this connection with TLS/SSL
|
235
|
+
require "openssl"
|
236
|
+
sslcontext = OpenSSL::SSL::SSLContext.new
|
237
|
+
sslcontext.ssl_version = :TLSv1
|
238
|
+
# If you use VERIFY_NONE, you are removing an important piece
|
239
|
+
sslcontext.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
240
|
+
# TODO(sissel): Try to be smart about setting this default.
|
241
|
+
sslcontext.ca_path = "/etc/ssl/certs"
|
242
|
+
@socket = OpenSSL::SSL::SSLSocket.new(@socket, sslcontext)
|
243
|
+
|
244
|
+
# SSLSocket#connect_nonblock will do the SSL/TLS handshake.
|
245
|
+
# TODO(sissel): refactor this into a method that both secure and connect
|
246
|
+
# methods can call.
|
247
|
+
start = Time.now
|
248
|
+
begin
|
249
|
+
@socket.connect_nonblock
|
250
|
+
rescue IO::WaitReadable, IO::WaitWritable
|
251
|
+
# The ruby OpenSSL docs for 1.9.3 have example code saying I should use
|
252
|
+
# IO::WaitReadable, but in the real world it raises an SSLError with
|
253
|
+
# a specific string message instead of Errno::EAGAIN or IO::WaitReadable
|
254
|
+
# explicitly...
|
255
|
+
#
|
256
|
+
# This SSLSocket#connect_nonblock raising WaitReadable (Technically,
|
257
|
+
# OpenSSL::SSL::SSLError) is in contrast to what Socket#connect_nonblock
|
258
|
+
# raises, WaitWritable (ok, Errno::EINPROGRESS, technically)
|
259
|
+
# Ruby's SSL exception for 'this call would block' is pretty shitty.
|
260
|
+
#
|
261
|
+
# If the exception string is *not* 'read would block' we have a real
|
262
|
+
# problem.
|
263
|
+
|
264
|
+
if !timeout.nil?
|
265
|
+
time_left = timeout - (Time.now - start)
|
266
|
+
raise ConnectTimeout.new if time_left < 0
|
267
|
+
r, w, e = IO.select([@socket], [@socket], nil, time_left)
|
268
|
+
else
|
269
|
+
r, w, e = IO.select([@socket], [@socket], nil, timeout)
|
270
|
+
end
|
271
|
+
|
272
|
+
# try connect_nonblock again if the socket is ready
|
273
|
+
retry if r.size > 0 || w.size > 0
|
222
274
|
end
|
223
|
-
end # def read_and_trigger
|
224
275
|
|
225
|
-
|
276
|
+
@secure = true
|
277
|
+
end # def secure
|
278
|
+
|
279
|
+
# Has this connection been secured?
|
226
280
|
public
|
227
|
-
def
|
228
|
-
return @
|
229
|
-
end
|
281
|
+
def secured?
|
282
|
+
return @secure
|
283
|
+
end # def secured?
|
230
284
|
end # class FTW::Connection
|
231
285
|
|