ftw 0.0.4 → 0.0.5
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 +35 -16
- data/lib/ftw/agent.rb +9 -4
- data/lib/ftw/connection.rb +5 -13
- data/lib/ftw/dns.rb +4 -5
- data/lib/ftw/http/headers.rb +31 -11
- data/lib/ftw/http/message.rb +36 -21
- data/lib/ftw/request.rb +6 -4
- data/lib/ftw/response.rb +5 -21
- data/lib/ftw/version.rb +1 -1
- data/lib/ftw/websocket.rb +5 -8
- data/lib/ftw/websocket/parser.rb +22 -9
- metadata +16 -13
data/README.md
CHANGED
@@ -1,10 +1,21 @@
|
|
1
1
|
# For The Web
|
2
2
|
|
3
|
-
|
3
|
+
## Getting Started
|
4
4
|
|
5
|
-
|
5
|
+
For doing client stuff (http requests, etc), you'll want {FTW::Agent}.
|
6
6
|
|
7
|
-
|
7
|
+
For doing server stuff (http serving, etc), you'll want {FTW::Server}. (not implemented yet)
|
8
|
+
|
9
|
+
## Overview
|
10
|
+
|
11
|
+
net/http is pretty much not good. Additionally, DNS behavior in ruby changes quite frequently.
|
12
|
+
|
13
|
+
I primarily want two things in both client and server operations:
|
14
|
+
|
15
|
+
* A consistent API with good documentation and tests
|
16
|
+
* Modern web features: websockets, spdy, etc.
|
17
|
+
|
18
|
+
Desired features:
|
8
19
|
|
9
20
|
* A HTTP client that acts as a full user agent, not just a single connections. (With connection reuse)
|
10
21
|
* HTTP and SPDY support.
|
@@ -14,35 +25,43 @@ I want:
|
|
14
25
|
* Server and Client modes.
|
15
26
|
* Support for both normal operation and EventMachine would be nice.
|
16
27
|
|
17
|
-
|
28
|
+
For reference:
|
18
29
|
|
19
|
-
*
|
20
|
-
* Logging, yo. With cabin, obviously.
|
21
|
-
* [DNS in Ruby stdlib is broken](https://github.com/jordansissel/experiments/tree/master/ruby/dns-resolving-bug), I need to write my own
|
30
|
+
* [DNS in Ruby stdlib is broken](https://github.com/jordansissel/experiments/tree/master/ruby/dns-resolving-bug), so I need to provide my own DNS api.
|
22
31
|
|
23
|
-
## API
|
32
|
+
## Agent API
|
24
33
|
|
25
34
|
### Common case
|
26
35
|
|
27
36
|
agent = FTW::Agent.new
|
37
|
+
|
28
38
|
request = agent.get("http://www.google.com/")
|
29
39
|
response = request.execute
|
40
|
+
puts response.body.read
|
30
41
|
|
31
42
|
# Simpler
|
32
|
-
response = agent.get!("http://www.google.com/")
|
43
|
+
response = agent.get!("http://www.google.com/").read
|
44
|
+
puts response.body.read
|
33
45
|
|
34
46
|
### SPDY
|
35
47
|
|
36
|
-
|
48
|
+
SPDY should automatically be attempted. The caller should be unaware.
|
49
|
+
|
50
|
+
I do not plan on exposing any direct means for invoking SPDY.
|
37
51
|
|
38
52
|
### WebSockets
|
39
53
|
|
40
54
|
# 'http(s)' or 'ws(s)' urls are valid here. They will mean the same thing.
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
55
|
+
websocket = agent.websocket!("http://somehost/endpoint")
|
56
|
+
|
57
|
+
websocket.publish("Hello world")
|
58
|
+
websocket.each do |message|
|
59
|
+
puts :received => message
|
60
|
+
end
|
61
|
+
|
62
|
+
## Server API
|
45
63
|
|
46
|
-
|
47
|
-
# Now websocket.read receives a message, websocket.write sends a message.
|
64
|
+
TBD. Will likely surround 'rack' somehow.
|
48
65
|
|
66
|
+
It's possible the 'cramp' gem supports all the server-side features we need
|
67
|
+
(except for SPDY, I suppose, which I might be able to contribute upstream)
|
data/lib/ftw/agent.rb
CHANGED
@@ -37,6 +37,10 @@ require "logger"
|
|
37
37
|
class FTW::Agent
|
38
38
|
STANDARD_METHODS = %w(options get head post put delete trace connect)
|
39
39
|
|
40
|
+
# Everything is private by default.
|
41
|
+
# At the bottom of this class, public methods will be declared.
|
42
|
+
private
|
43
|
+
|
40
44
|
def initialize
|
41
45
|
@pool = FTW::Pool.new
|
42
46
|
@logger = Cabin::Channel.get($0)
|
@@ -91,7 +95,6 @@ class FTW::Agent
|
|
91
95
|
# This will send the http request. If the websocket handshake
|
92
96
|
# is successful, a FTW::WebSocket instance will be returned.
|
93
97
|
# Otherwise, a FTW::Response will be returned.
|
94
|
-
public
|
95
98
|
def websocket!(uri, options={})
|
96
99
|
# TODO(sissel): Use FTW::Agent#upgrade! ?
|
97
100
|
req = request("GET", uri, options)
|
@@ -132,7 +135,6 @@ class FTW::Agent
|
|
132
135
|
# The 'options' hash supports the following keys:
|
133
136
|
#
|
134
137
|
# * :headers => { string => string, ... }. This allows you to set header values.
|
135
|
-
public
|
136
138
|
def request(method, uri, options)
|
137
139
|
@logger.info("Creating new request", :method => method, :uri => uri, :options => options)
|
138
140
|
request = FTW::Request.new(uri)
|
@@ -155,7 +157,9 @@ class FTW::Agent
|
|
155
157
|
# is opened.
|
156
158
|
#
|
157
159
|
# Redirects are always followed.
|
158
|
-
|
160
|
+
#
|
161
|
+
# @params
|
162
|
+
# @return [FTW::Response] the response for this request.
|
159
163
|
def execute(request)
|
160
164
|
# TODO(sissel): Make redirection-following optional, but default.
|
161
165
|
|
@@ -206,7 +210,6 @@ class FTW::Agent
|
|
206
210
|
end # def execute
|
207
211
|
|
208
212
|
# Returns a FTW::Connection connected to this host:port.
|
209
|
-
private
|
210
213
|
def connect(host, port)
|
211
214
|
address = "#{host}:#{port}"
|
212
215
|
@logger.debug("Fetching from pool", :address => address)
|
@@ -220,4 +223,6 @@ class FTW::Agent
|
|
220
223
|
connection.mark
|
221
224
|
return connection
|
222
225
|
end # def connect
|
226
|
+
|
227
|
+
public(:initialize, :execute, :websocket!, :upgrade!)
|
223
228
|
end # class FTW::Agent
|
data/lib/ftw/connection.rb
CHANGED
@@ -19,6 +19,8 @@ class FTW::Connection
|
|
19
19
|
include FTW::Poolable
|
20
20
|
include Cabin::Inspectable
|
21
21
|
|
22
|
+
private
|
23
|
+
|
22
24
|
# A new network connection.
|
23
25
|
# The 'destination' argument can be an array of strings or a single string.
|
24
26
|
# String format is expected to be "host:port"
|
@@ -29,7 +31,6 @@ class FTW::Connection
|
|
29
31
|
#
|
30
32
|
# If you specify multiple destinations, they are used in a round-robin
|
31
33
|
# decision made during reconnection.
|
32
|
-
public
|
33
34
|
def initialize(destinations)
|
34
35
|
if destinations.is_a?(String)
|
35
36
|
@destinations = [destinations]
|
@@ -64,7 +65,6 @@ class FTW::Connection
|
|
64
65
|
#
|
65
66
|
# Timeout value is optional. If no timeout is given, this method
|
66
67
|
# blocks until a connection is successful or an error occurs.
|
67
|
-
public
|
68
68
|
def connect(timeout=nil)
|
69
69
|
# TODO(sissel): Raise if we're already connected?
|
70
70
|
close if connected?
|
@@ -114,7 +114,6 @@ class FTW::Connection
|
|
114
114
|
end # def connect
|
115
115
|
|
116
116
|
# Is this Connection connected?
|
117
|
-
public
|
118
117
|
def connected?
|
119
118
|
return @connected
|
120
119
|
end # def connected?
|
@@ -125,7 +124,6 @@ class FTW::Connection
|
|
125
124
|
# This method is not guaranteed to have written the full data given.
|
126
125
|
#
|
127
126
|
# Returns the number of bytes written (See also IO#syswrite)
|
128
|
-
public
|
129
127
|
def write(data, timeout=nil)
|
130
128
|
#connect if !connected?
|
131
129
|
if writable?(timeout)
|
@@ -140,7 +138,6 @@ class FTW::Connection
|
|
140
138
|
#
|
141
139
|
# This method is not guaranteed to read exactly 'length' bytes. See
|
142
140
|
# IO#sysread
|
143
|
-
public
|
144
141
|
def read(timeout=nil)
|
145
142
|
data = ""
|
146
143
|
data.force_encoding("BINARY") if data.respond_to?(:force_encoding)
|
@@ -170,13 +167,11 @@ class FTW::Connection
|
|
170
167
|
end # def read
|
171
168
|
|
172
169
|
# Push back some data onto the connection's read buffer.
|
173
|
-
public
|
174
170
|
def pushback(data)
|
175
171
|
@pushback_buffer << data
|
176
172
|
end # def pushback
|
177
173
|
|
178
174
|
# End this connection, specifying why.
|
179
|
-
public
|
180
175
|
def disconnect(reason)
|
181
176
|
begin
|
182
177
|
@socket.close_read
|
@@ -195,7 +190,6 @@ class FTW::Connection
|
|
195
190
|
# the timeout period. False otherwise.
|
196
191
|
#
|
197
192
|
# The time out is in seconds. Fractional seconds are OK.
|
198
|
-
public
|
199
193
|
def writable?(timeout)
|
200
194
|
ready = IO.select(nil, [@socket], nil, timeout)
|
201
195
|
return !ready.nil?
|
@@ -205,7 +199,6 @@ class FTW::Connection
|
|
205
199
|
# the timeout period. False otherwise.
|
206
200
|
#
|
207
201
|
# The time out is in seconds. Fractional seconds are OK.
|
208
|
-
public
|
209
202
|
def readable?(timeout)
|
210
203
|
#return false if @reader_closed
|
211
204
|
ready = IO.select([@socket], nil, nil, timeout)
|
@@ -213,19 +206,16 @@ class FTW::Connection
|
|
213
206
|
end # def readable?
|
214
207
|
|
215
208
|
# The host:port
|
216
|
-
public
|
217
209
|
def peer
|
218
210
|
return @remote_address
|
219
211
|
end # def peer
|
220
212
|
|
221
213
|
# Support 'to_io' so you can use IO::select on this object.
|
222
|
-
public
|
223
214
|
def to_io
|
224
215
|
return @socket
|
225
216
|
end # def to_io
|
226
217
|
|
227
218
|
# Secure this connection with TLS.
|
228
|
-
public
|
229
219
|
def secure(timeout=nil, options={})
|
230
220
|
# Skip this if we're already secure.
|
231
221
|
return if secured?
|
@@ -277,9 +267,11 @@ class FTW::Connection
|
|
277
267
|
end # def secure
|
278
268
|
|
279
269
|
# Has this connection been secured?
|
280
|
-
public
|
281
270
|
def secured?
|
282
271
|
return @secure
|
283
272
|
end # def secured?
|
273
|
+
|
274
|
+
public(:connect, :connected?, :write, :read, :pushback, :disconnect,
|
275
|
+
:writable?, :readable?, :peer, :to_io, :secure, :secured?)
|
284
276
|
end # class FTW::Connection
|
285
277
|
|
data/lib/ftw/dns.rb
CHANGED
@@ -15,15 +15,15 @@ class FTW::DNS
|
|
15
15
|
V4_IN_V6_PREFIX = "0:" * 12
|
16
16
|
|
17
17
|
# Get a singleton instance of FTW::DNS
|
18
|
-
public
|
19
18
|
def self.singleton
|
20
19
|
@resolver ||= self.new
|
21
20
|
end # def self.singleton
|
22
21
|
|
22
|
+
private
|
23
|
+
|
23
24
|
# Resolve a hostname.
|
24
25
|
#
|
25
26
|
# It will return an array of all known addresses for the host.
|
26
|
-
public
|
27
27
|
def resolve(hostname)
|
28
28
|
official, aliases, family, *addresses = Socket.gethostbyname(hostname)
|
29
29
|
# We ignore family, here. Ruby will return v6 *and* v4 addresses in
|
@@ -43,18 +43,15 @@ class FTW::DNS
|
|
43
43
|
#
|
44
44
|
# Use this method if you are connecting to a hostname that resolves to
|
45
45
|
# multiple addresses.
|
46
|
-
public
|
47
46
|
def resolve_random(hostname)
|
48
47
|
addresses = resolve(hostname)
|
49
48
|
return addresses[rand(addresses.size)]
|
50
49
|
end # def resolve_random
|
51
50
|
|
52
|
-
private
|
53
51
|
def unpack_v4(address)
|
54
52
|
return address.unpack("C4").join(".")
|
55
53
|
end # def unpack_v4
|
56
54
|
|
57
|
-
private
|
58
55
|
def unpack_v6(address)
|
59
56
|
if address.length == 16
|
60
57
|
# Unpack 16 bit chunks, convert to hex, join with ":"
|
@@ -68,4 +65,6 @@ class FTW::DNS
|
|
68
65
|
"::" + unpack_v4(address)
|
69
66
|
end
|
70
67
|
end # def unpack_v6
|
68
|
+
|
69
|
+
public(:resolve, :resolve_random)
|
71
70
|
end # class FTW::DNS
|
data/lib/ftw/http/headers.rb
CHANGED
@@ -16,24 +16,29 @@ class FTW::HTTP::Headers
|
|
16
16
|
include Enumerable
|
17
17
|
include FTW::CRLF
|
18
18
|
|
19
|
-
|
20
|
-
|
19
|
+
private
|
20
|
+
|
21
|
+
# Make a new headers container.
|
22
|
+
#
|
23
|
+
# @param [Hash, optional] a hash of headers to start with.
|
21
24
|
def initialize(headers={})
|
22
25
|
super()
|
23
|
-
@version = 1.1
|
24
26
|
@headers = headers
|
25
27
|
end # def initialize
|
26
28
|
|
27
29
|
# Set a header field to a specific value.
|
28
30
|
# Any existing value(s) for this field are destroyed.
|
31
|
+
#
|
32
|
+
# @param [String] the name of the field to set
|
33
|
+
# @param [String or Array] the value of the field to set
|
29
34
|
def set(field, value)
|
30
35
|
@headers[field.downcase] = value
|
31
36
|
end # def set
|
32
37
|
|
33
38
|
alias_method :[]=, :set
|
34
39
|
|
35
|
-
#
|
36
|
-
#
|
40
|
+
# Does this header include this field name?
|
41
|
+
# @return [true, false]
|
37
42
|
def include?(field)
|
38
43
|
@headers.include?(field.downcase)
|
39
44
|
end # def include?
|
@@ -93,9 +98,9 @@ class FTW::HTTP::Headers
|
|
93
98
|
|
94
99
|
# Get a field value.
|
95
100
|
#
|
96
|
-
#
|
97
|
-
#
|
98
|
-
#
|
101
|
+
# @return [String] if there is only one value for this field
|
102
|
+
# @return [Array] if there are multiple values for this field
|
103
|
+
# @return [nil] if there are no values for this field
|
99
104
|
def get(field)
|
100
105
|
field = field.downcase
|
101
106
|
return @headers[field]
|
@@ -119,18 +124,33 @@ class FTW::HTTP::Headers
|
|
119
124
|
end
|
120
125
|
end # end each
|
121
126
|
|
122
|
-
|
127
|
+
# @return [Hash] String keys and values of String (field value) or Array (of String field values)
|
123
128
|
def to_hash
|
124
129
|
return @headers
|
125
130
|
end # def to_hash
|
126
131
|
|
127
|
-
|
132
|
+
# Serialize this object to a string in HTTP format described by RFC2616
|
133
|
+
#
|
134
|
+
# Example:
|
135
|
+
#
|
136
|
+
# headers = FTW::HTTP::Headers.new
|
137
|
+
# headers.add("Host", "example.com")
|
138
|
+
# headers.add("X-Forwarded-For", "1.2.3.4")
|
139
|
+
# headers.add("X-Forwarded-For", "192.168.0.1")
|
140
|
+
# puts headers.to_s
|
141
|
+
#
|
142
|
+
# # Result
|
143
|
+
# Host: example.com
|
144
|
+
# X-Forwarded-For: 1.2.3.4
|
145
|
+
# X-Forwarded-For: 192.168.0.1
|
128
146
|
def to_s
|
129
147
|
return @headers.collect { |name, value| "#{name}: #{value}" }.join(CRLF) + CRLF
|
130
148
|
end # def to_s
|
131
149
|
|
132
|
-
|
150
|
+
# Inspect this object
|
133
151
|
def inspect
|
134
152
|
return "#{self.class.name} <#{to_hash.inspect}>"
|
135
153
|
end # def inspect
|
154
|
+
|
155
|
+
public(:set, :[]=, :include?, :add, :remove, :get, :[], :each, :to_hash, :to_s, :inspect)
|
136
156
|
end # class FTW::HTTP::Headers
|
data/lib/ftw/http/message.rb
CHANGED
@@ -3,41 +3,52 @@ require "ftw/http/headers"
|
|
3
3
|
require "ftw/crlf"
|
4
4
|
|
5
5
|
# HTTP Message, RFC2616
|
6
|
+
# For specification, see RFC2616 section 4: <http://tools.ietf.org/html/rfc2616#section-4>
|
7
|
+
#
|
8
|
+
# You probably won't use this class much. Instead, check out {FTW::Request} and {FTW::Response}
|
6
9
|
module FTW::HTTP::Message
|
7
10
|
include FTW::CRLF
|
8
11
|
|
9
|
-
# The HTTP headers
|
12
|
+
# The HTTP headers - See {FTW::HTTP::Headers}.
|
10
13
|
# RFC2616 5.3 - <http://tools.ietf.org/html/rfc2616#section-5.3>
|
11
14
|
attr_reader :headers
|
12
15
|
|
13
|
-
# The HTTP version. See VALID_VERSIONS for valid versions.
|
16
|
+
# The HTTP version. See {VALID_VERSIONS} for valid versions.
|
14
17
|
# This will always be a Numeric object.
|
15
18
|
# Both Request and Responses have version, so put it in the parent class.
|
16
19
|
attr_accessor :version
|
20
|
+
|
21
|
+
# HTTP Versions that are valid.
|
17
22
|
VALID_VERSIONS = [1.0, 1.1]
|
18
23
|
|
19
|
-
|
20
|
-
|
21
|
-
#
|
22
|
-
public
|
24
|
+
private
|
25
|
+
|
26
|
+
# A new HTTP message.
|
23
27
|
def initialize
|
24
28
|
@headers = FTW::HTTP::Headers.new
|
25
29
|
@body = nil
|
26
30
|
end # def initialize
|
27
31
|
|
28
|
-
#
|
29
|
-
|
30
|
-
|
32
|
+
# Get a header value by field name.
|
33
|
+
#
|
34
|
+
# @param [String] the name of the field. (case insensitive)
|
35
|
+
def [](field)
|
31
36
|
return @headers[header]
|
32
37
|
end # def []
|
33
38
|
|
34
|
-
|
35
|
-
|
36
|
-
|
39
|
+
# Set a header field
|
40
|
+
#
|
41
|
+
# @param [String] the name of the field. (case insensitive)
|
42
|
+
# @param [String] the value to set for this field
|
43
|
+
def []=(field, value)
|
44
|
+
@headers[field] = header
|
37
45
|
end # def []=
|
38
46
|
|
47
|
+
# Set the body of this message
|
48
|
+
#
|
49
|
+
# The 'message_body' can be an IO-like object, Enumerable, or String.
|
50
|
+
#
|
39
51
|
# See RFC2616 section 4.3: <http://tools.ietf.org/html/rfc2616#section-4.3>
|
40
|
-
public
|
41
52
|
def body=(message_body)
|
42
53
|
# TODO(sissel): if message_body is a string, set Content-Length header
|
43
54
|
# TODO(sissel): if it's an IO object, set Transfer-Encoding to chunked
|
@@ -46,7 +57,10 @@ module FTW::HTTP::Message
|
|
46
57
|
@body = message_body
|
47
58
|
end # def body=
|
48
59
|
|
49
|
-
|
60
|
+
# Get the body of this message
|
61
|
+
#
|
62
|
+
# Returns an Enumerable, IO-like object, or String, depending on how this
|
63
|
+
# message was built.
|
50
64
|
def body
|
51
65
|
# TODO(sissel): verification todos follow...
|
52
66
|
# TODO(sissel): RFC2616 section 4.3 - if there is a message body
|
@@ -58,22 +72,21 @@ module FTW::HTTP::Message
|
|
58
72
|
return @body
|
59
73
|
end # def body
|
60
74
|
|
61
|
-
#
|
62
|
-
|
75
|
+
# Should this message have a content?
|
76
|
+
#
|
77
|
+
# In HTTP 1.1, there is a body if response sets Content-Length *or*
|
78
|
+
# Transfer-Encoding, it has a body. Otherwise, there is no body.
|
63
79
|
def content?
|
64
|
-
# In HTTP 1.1, there is a body if response sets Content-Length *or*
|
65
|
-
# Transfer-Encoding, it has a body. Otherwise, there is no body.
|
66
80
|
return (headers.include?("Content-Length") and headers["Content-Length"].to_i > 0) \
|
67
81
|
|| headers.include?("Transfer-Encoding")
|
68
82
|
end # def content?
|
69
83
|
|
70
|
-
|
84
|
+
# Does this message have a body?
|
71
85
|
def body?
|
72
86
|
return @body.nil?
|
73
87
|
end # def body?
|
74
88
|
|
75
89
|
# Set the HTTP version. Must be a valid version. See VALID_VERSIONS.
|
76
|
-
public
|
77
90
|
def version=(ver)
|
78
91
|
# Accept string "1.0" or simply "1", etc.
|
79
92
|
ver = ver.to_f if !ver.is_a?(Float)
|
@@ -93,8 +106,10 @@ module FTW::HTTP::Message
|
|
93
106
|
# CRLF
|
94
107
|
# [ message-body ]
|
95
108
|
# Thus, the CRLF between header and body is not part of the header.
|
96
|
-
public
|
97
109
|
def to_s
|
98
110
|
return [start_line, @headers].join(CRLF)
|
99
111
|
end
|
112
|
+
|
113
|
+
public(:initialize, :headers, :version, :version=, :[], :[]=, :body=, :body,
|
114
|
+
:content?, :body?, :to_s)
|
100
115
|
end # class FTW::HTTP::Message
|
data/lib/ftw/request.rb
CHANGED
@@ -15,6 +15,8 @@ class FTW::Request
|
|
15
15
|
include FTW::CRLF
|
16
16
|
include Cabin::Inspectable
|
17
17
|
|
18
|
+
private
|
19
|
+
|
18
20
|
# The http method. Like GET, PUT, POST, etc..
|
19
21
|
# RFC2616 5.1.1 - <http://tools.ietf.org/html/rfc2616#section-5.1.1>
|
20
22
|
#
|
@@ -47,7 +49,6 @@ class FTW::Request
|
|
47
49
|
# Make a new request with a uri if given.
|
48
50
|
#
|
49
51
|
# The uri is used to set the address, protocol, Host header, etc.
|
50
|
-
public
|
51
52
|
def initialize(uri=nil)
|
52
53
|
super()
|
53
54
|
@port = 80
|
@@ -66,7 +67,6 @@ class FTW::Request
|
|
66
67
|
# The 'connection' should be a FTW::Connection instance, but it might work
|
67
68
|
# with a normal IO object.
|
68
69
|
#
|
69
|
-
public
|
70
70
|
def execute(connection)
|
71
71
|
tries = 3
|
72
72
|
begin
|
@@ -116,7 +116,6 @@ class FTW::Request
|
|
116
116
|
end # def execute
|
117
117
|
|
118
118
|
# Use a URI to help fill in parts of this Request.
|
119
|
-
public
|
120
119
|
def use_uri(uri)
|
121
120
|
# Convert URI objects to Addressable::URI
|
122
121
|
case uri
|
@@ -142,7 +141,6 @@ class FTW::Request
|
|
142
141
|
|
143
142
|
# Set the method for this request. Usually something like "GET" or "PUT"
|
144
143
|
# etc. See <http://tools.ietf.org/html/rfc2616#section-5.1.1>
|
145
|
-
public
|
146
144
|
def method=(method)
|
147
145
|
# RFC2616 5.1.1 doesn't say the method has to be uppercase.
|
148
146
|
# It can be any 'token' besides the ones defined in section 5.1.1:
|
@@ -163,4 +161,8 @@ class FTW::Request
|
|
163
161
|
|
164
162
|
# Define the Message's start_line as request_line
|
165
163
|
alias_method :start_line, :request_line
|
164
|
+
|
165
|
+
public(:method, :method=, :request_uri, :request_uri=, :path, :port, :port=,
|
166
|
+
:protocol, :protocol=, :execute, :use_uri, :request_line, :start_line)
|
167
|
+
|
166
168
|
end # class FTW::Request < Message
|
data/lib/ftw/response.rb
CHANGED
@@ -9,10 +9,6 @@ require "http/parser" # gem http_parser.rb
|
|
9
9
|
class FTW::Response
|
10
10
|
include FTW::HTTP::Message
|
11
11
|
|
12
|
-
# The HTTP version number
|
13
|
-
# See RFC2616 section 6.1: <http://tools.ietf.org/html/rfc2616#section-6.1>
|
14
|
-
attr_reader :version
|
15
|
-
|
16
12
|
# The http status code (RFC2616 6.1.1)
|
17
13
|
# See RFC2616 section 6.1.1: <http://tools.ietf.org/html/rfc2616#section-6.1.1>
|
18
14
|
attr_reader :status
|
@@ -51,8 +47,9 @@ class FTW::Response
|
|
51
47
|
|
52
48
|
attr_accessor :body
|
53
49
|
|
50
|
+
private
|
51
|
+
|
54
52
|
# Create a new Response.
|
55
|
-
public
|
56
53
|
def initialize
|
57
54
|
super
|
58
55
|
@logger = Cabin::Channel.get
|
@@ -60,21 +57,18 @@ class FTW::Response
|
|
60
57
|
end # def initialize
|
61
58
|
|
62
59
|
# Is this response a redirect?
|
63
|
-
public
|
64
60
|
def redirect?
|
65
61
|
# redirects are 3xx
|
66
62
|
return @status >= 300 && @status < 400
|
67
63
|
end # redirect?
|
68
64
|
|
69
65
|
# Is this response an error?
|
70
|
-
public
|
71
66
|
def error?
|
72
67
|
# 4xx and 5xx are errors
|
73
68
|
return @status >= 400 && @status < 600
|
74
69
|
end # def error?
|
75
70
|
|
76
71
|
# Set the status code
|
77
|
-
public
|
78
72
|
def status=(code)
|
79
73
|
code = code.to_i if !code.is_a?(Fixnum)
|
80
74
|
# TODO(sissel): Validate that 'code' is a 3 digit number
|
@@ -86,7 +80,6 @@ class FTW::Response
|
|
86
80
|
end # def status=
|
87
81
|
|
88
82
|
# Get the status-line string, like "HTTP/1.0 200 OK"
|
89
|
-
public
|
90
83
|
def status_line
|
91
84
|
# First line is 'Status-Line' from RFC2616 section 6.1
|
92
85
|
# Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
|
@@ -97,19 +90,10 @@ class FTW::Response
|
|
97
90
|
# Define the Message's start_line as status_line
|
98
91
|
alias_method :start_line, :status_line
|
99
92
|
|
100
|
-
# Set the body of this response. In most cases this will be a FTW::Connection when
|
101
|
-
# Response objects are being created by a FTW::Agent. In Server cases,
|
102
|
-
# the body is likely to be a string or enumerable.
|
103
|
-
public
|
104
|
-
def body=(connection_or_string_or_enumerable)
|
105
|
-
@body = connection_or_string_or_enumerable
|
106
|
-
end # def body=
|
107
|
-
|
108
93
|
# Read the body of this Response. The block is called with chunks of the
|
109
94
|
# response as they are read in.
|
110
95
|
#
|
111
96
|
# This method is generally only called by http clients, not servers.
|
112
|
-
public
|
113
97
|
def read_body(&block)
|
114
98
|
if @body.respond_to?(:read)
|
115
99
|
if headers.include?("Content-Length") and headers["Content-Length"].to_i > 0
|
@@ -129,7 +113,6 @@ class FTW::Response
|
|
129
113
|
|
130
114
|
# Read the length bytes from the body. Yield each chunk read to the block
|
131
115
|
# given. This method is generally only called by http clients, not servers.
|
132
|
-
private
|
133
116
|
def read_body_length(length, &block)
|
134
117
|
remaining = length
|
135
118
|
while remaining > 0
|
@@ -148,7 +131,6 @@ class FTW::Response
|
|
148
131
|
end # def read_body_length
|
149
132
|
|
150
133
|
# This is kind of messed, need to fix it.
|
151
|
-
private
|
152
134
|
def read_body_chunked(&block)
|
153
135
|
parser = HTTP::Parser.new
|
154
136
|
|
@@ -169,11 +151,13 @@ class FTW::Response
|
|
169
151
|
end # def read_body_chunked
|
170
152
|
|
171
153
|
# Is this Response the result of a successful Upgrade request?
|
172
|
-
public
|
173
154
|
def upgrade?
|
174
155
|
return false unless status == 101 # "Switching Protocols"
|
175
156
|
return false unless headers["Connection"] == "Upgrade"
|
176
157
|
return true
|
177
158
|
end # def upgrade?
|
159
|
+
|
160
|
+
public(:status=, :status, :reason, :initialize, :upgrade?, :redirect?,
|
161
|
+
:error?, :status_line, :read_body)
|
178
162
|
end # class FTW::Response
|
179
163
|
|
data/lib/ftw/version.rb
CHANGED
data/lib/ftw/websocket.rb
CHANGED
@@ -4,6 +4,7 @@ require "base64" # stdlib
|
|
4
4
|
require "digest/sha1" # stdlib
|
5
5
|
require "cabin"
|
6
6
|
require "ftw/websocket/parser"
|
7
|
+
require "ftw/crlf"
|
7
8
|
|
8
9
|
# WebSockets, RFC6455.
|
9
10
|
#
|
@@ -19,6 +20,8 @@ class FTW::WebSocket
|
|
19
20
|
|
20
21
|
WEBSOCKET_ACCEPT_UUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
21
22
|
|
23
|
+
private
|
24
|
+
|
22
25
|
# Protocol phases
|
23
26
|
# 1. tcp connect
|
24
27
|
# 2. http handshake (RFC6455 section 4)
|
@@ -26,7 +29,6 @@ class FTW::WebSocket
|
|
26
29
|
|
27
30
|
# Creates a new websocket and fills in the given http request with any
|
28
31
|
# necessary settings.
|
29
|
-
public
|
30
32
|
def initialize(request)
|
31
33
|
@key_nonce = generate_key_nonce
|
32
34
|
@request = request
|
@@ -38,14 +40,12 @@ class FTW::WebSocket
|
|
38
40
|
# after the websocket upgrade and handshake have been successful.
|
39
41
|
#
|
40
42
|
# You probably don't call this yourself.
|
41
|
-
public
|
42
43
|
def connection=(connection)
|
43
44
|
@connection = connection
|
44
45
|
end # def connection=
|
45
46
|
|
46
47
|
# Prepare the request. This sets any required headers and attributes as
|
47
48
|
# specified by RFC6455
|
48
|
-
private
|
49
49
|
def prepare(request)
|
50
50
|
# RFC6455 section 4.1:
|
51
51
|
# "2. The method of the request MUST be GET, and the HTTP version MUST
|
@@ -73,7 +73,6 @@ class FTW::WebSocket
|
|
73
73
|
end # def prepare
|
74
74
|
|
75
75
|
# Generate a websocket key nonce.
|
76
|
-
private
|
77
76
|
def generate_key_nonce
|
78
77
|
# RFC6455 section 4.1 says:
|
79
78
|
# ---
|
@@ -95,7 +94,6 @@ class FTW::WebSocket
|
|
95
94
|
end # def generate_key_nonce
|
96
95
|
|
97
96
|
# Is this Response acceptable for our WebSocket Upgrade request?
|
98
|
-
public
|
99
97
|
def handshake_ok?(response)
|
100
98
|
# See RFC6455 section 4.2.2
|
101
99
|
return false unless response.status == 101 # "Switching Protocols"
|
@@ -115,7 +113,6 @@ class FTW::WebSocket
|
|
115
113
|
# break from it.
|
116
114
|
#
|
117
115
|
# The text payload of each message will be yielded to the block.
|
118
|
-
public
|
119
116
|
def each(&block)
|
120
117
|
loop do
|
121
118
|
payload = @parser.feed(@connection.read)
|
@@ -133,7 +130,6 @@ class FTW::WebSocket
|
|
133
130
|
# message[3] ^ key[3]
|
134
131
|
# message[4] ^ key[0]
|
135
132
|
# ...
|
136
|
-
private
|
137
133
|
def mask(message, key)
|
138
134
|
masked = []
|
139
135
|
mask_bytes = key.unpack("C4")
|
@@ -148,7 +144,6 @@ class FTW::WebSocket
|
|
148
144
|
# Publish a message text.
|
149
145
|
#
|
150
146
|
# This will send a websocket text frame over the connection.
|
151
|
-
public
|
152
147
|
def publish(message)
|
153
148
|
# TODO(sissel): Support server and client modes.
|
154
149
|
# Server MUST NOT mask. Client MUST mask.
|
@@ -190,5 +185,7 @@ class FTW::WebSocket
|
|
190
185
|
@connection.write(data.pack(pack))
|
191
186
|
end
|
192
187
|
end # def publish
|
188
|
+
|
189
|
+
public(:initialize, :connection=, :handshake_ok?, :each, :publish)
|
193
190
|
end # class FTW::WebSocket
|
194
191
|
|
data/lib/ftw/websocket/parser.rb
CHANGED
@@ -22,12 +22,29 @@ require "ftw/websocket"
|
|
22
22
|
# + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|
23
23
|
# | Payload Data continued ... |
|
24
24
|
# +---------------------------------------------------------------+
|
25
|
+
#
|
26
|
+
# Example use:
|
27
|
+
#
|
28
|
+
# socket = FTW::Connection.new("example.com:80")
|
29
|
+
# parser = FTW::WebSocket::Parser.new
|
30
|
+
# # ... do HTTP Upgrade request to websockets
|
31
|
+
# loop do
|
32
|
+
# data = socket.sysread(4096)
|
33
|
+
# payload = parser.feed(data)
|
34
|
+
# if payload
|
35
|
+
# # We got a full websocket frame, print the payload.
|
36
|
+
# p :payload => payload
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
#
|
25
40
|
class FTW::WebSocket::Parser
|
26
41
|
# XXX: Implement control frames: http://tools.ietf.org/html/rfc6455#section-5.5
|
27
42
|
|
28
43
|
# States are based on the minimal unit of 'byte'
|
29
44
|
STATES = [ :flags_and_opcode, :mask_and_payload_init, :payload_length, :payload ]
|
30
45
|
|
46
|
+
private
|
47
|
+
|
31
48
|
# A new WebSocket protocol parser.
|
32
49
|
def initialize
|
33
50
|
@logger = Cabin::Channel.get($0)
|
@@ -42,7 +59,6 @@ class FTW::WebSocket::Parser
|
|
42
59
|
end # def initialize
|
43
60
|
|
44
61
|
# Transition to a specified state and set the next required read length.
|
45
|
-
private
|
46
62
|
def transition(state, next_length)
|
47
63
|
@logger.debug("Transitioning", :transition => state, :nextlen => next_length)
|
48
64
|
@state = state
|
@@ -53,7 +69,9 @@ class FTW::WebSocket::Parser
|
|
53
69
|
#
|
54
70
|
# Currently, it will return the raw payload of websocket messages.
|
55
71
|
# Otherwise, it returns nil if no complete message has yet been consumed.
|
56
|
-
|
72
|
+
#
|
73
|
+
# @param [String] the string data to feed into the parser.
|
74
|
+
# @return [String, nil] the websocket message payload, if any, nil otherwise.
|
57
75
|
def feed(data)
|
58
76
|
@buffer << data
|
59
77
|
while have?(@need)
|
@@ -66,13 +84,11 @@ class FTW::WebSocket::Parser
|
|
66
84
|
end # def <<
|
67
85
|
|
68
86
|
# Do we have at least 'length' bytes in the buffer?
|
69
|
-
private
|
70
87
|
def have?(length)
|
71
88
|
return length <= @buffer.size
|
72
89
|
end # def have?
|
73
90
|
|
74
91
|
# Get 'length' string from the buffer.
|
75
|
-
private
|
76
92
|
def get(length=nil)
|
77
93
|
length = @need if length.nil?
|
78
94
|
data = @buffer[0 ... length]
|
@@ -81,14 +97,12 @@ class FTW::WebSocket::Parser
|
|
81
97
|
end # def get
|
82
98
|
|
83
99
|
# Set the minimum number of bytes we need in the buffer for the next read.
|
84
|
-
private
|
85
100
|
def need(length)
|
86
101
|
@need = length
|
87
102
|
end # def need
|
88
103
|
|
89
104
|
# State: Flags (fin, etc) and Opcode.
|
90
105
|
# See: http://tools.ietf.org/html/rfc6455#section-5.3
|
91
|
-
private
|
92
106
|
def flags_and_opcode
|
93
107
|
# 0
|
94
108
|
# 0 1 2 3 4 5 6 7
|
@@ -112,7 +126,6 @@ class FTW::WebSocket::Parser
|
|
112
126
|
|
113
127
|
# State: mask_and_payload_init
|
114
128
|
# See: http://tools.ietf.org/html/rfc6455#section-5.2
|
115
|
-
private
|
116
129
|
def mask_and_payload_init
|
117
130
|
byte = get.bytes.first
|
118
131
|
@mask = byte & 0x80 # first bit (msb)
|
@@ -136,7 +149,6 @@ class FTW::WebSocket::Parser
|
|
136
149
|
# This is the 'extended payload length' with support for both 16
|
137
150
|
# and 64 bit lengths.
|
138
151
|
# See: http://tools.ietf.org/html/rfc6455#section-5.2
|
139
|
-
private
|
140
152
|
def payload_length
|
141
153
|
# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
142
154
|
# +-+-+-+-+-------+-+-------------+-------------------------------+
|
@@ -169,7 +181,6 @@ class FTW::WebSocket::Parser
|
|
169
181
|
# Read the full payload and return it.
|
170
182
|
# See: http://tools.ietf.org/html/rfc6455#section-5.3
|
171
183
|
#
|
172
|
-
private
|
173
184
|
def payload
|
174
185
|
# TODO(sissel): Handle massive payload lengths without exceeding memory.
|
175
186
|
# Perhaps if the payload is large (say, larger than 500KB by default),
|
@@ -180,4 +191,6 @@ class FTW::WebSocket::Parser
|
|
180
191
|
transition(:flags_and_opcode, 1)
|
181
192
|
return data
|
182
193
|
end # def payload
|
194
|
+
|
195
|
+
public(:feed)
|
183
196
|
end # class FTW::WebSocket::Parser
|
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.5
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -11,32 +11,33 @@ bindir: bin
|
|
11
11
|
cert_chain: []
|
12
12
|
date: 2012-02-13 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
|
-
description: Trying to build a solid and sane API for client and server
|
14
|
+
description: For The Web. Trying to build a solid and sane API for client and server
|
15
|
+
web stuff. Client and Server operations for HTTP, WebSockets, SPDY, etc.
|
15
16
|
email:
|
16
17
|
- jls@semicomplete.com
|
17
18
|
executables: []
|
18
19
|
extensions: []
|
19
20
|
extra_rdoc_files: []
|
20
21
|
files:
|
21
|
-
- lib/ftw/
|
22
|
+
- lib/ftw/websocket.rb
|
22
23
|
- lib/ftw/pool.rb
|
24
|
+
- lib/ftw/agent.rb
|
25
|
+
- lib/ftw/version.rb
|
26
|
+
- lib/ftw/namespace.rb
|
27
|
+
- lib/ftw/crlf.rb
|
28
|
+
- lib/ftw/connection.rb
|
29
|
+
- lib/ftw/websocket/parser.rb
|
30
|
+
- lib/ftw/request.rb
|
23
31
|
- lib/ftw/cookies.rb
|
24
32
|
- lib/ftw/http/headers.rb
|
25
33
|
- lib/ftw/http/message.rb
|
26
|
-
- lib/ftw/websocket/parser.rb
|
27
|
-
- lib/ftw/connection.rb
|
28
|
-
- lib/ftw/request.rb
|
29
|
-
- lib/ftw/namespace.rb
|
30
|
-
- lib/ftw/dns.rb
|
31
34
|
- lib/ftw/response.rb
|
35
|
+
- lib/ftw/dns.rb
|
32
36
|
- lib/ftw/poolable.rb
|
33
|
-
- lib/ftw/websocket.rb
|
34
|
-
- lib/ftw/crlf.rb
|
35
|
-
- lib/ftw/version.rb
|
36
37
|
- lib/ftw.rb
|
38
|
+
- test/ftw/crlf.rb
|
37
39
|
- test/ftw/http/headers.rb
|
38
40
|
- test/ftw/http/dns.rb
|
39
|
-
- test/ftw/crlf.rb
|
40
41
|
- test/testing.rb
|
41
42
|
- test/all.rb
|
42
43
|
- README.md
|
@@ -65,5 +66,7 @@ rubyforge_project:
|
|
65
66
|
rubygems_version: 1.8.10
|
66
67
|
signing_key:
|
67
68
|
specification_version: 3
|
68
|
-
summary: For The Web.
|
69
|
+
summary: For The Web. Trying to build a solid and sane API for client and server web
|
70
|
+
stuff. Client and Server operations for HTTP, WebSockets, SPDY, etc.
|
69
71
|
test_files: []
|
72
|
+
has_rdoc:
|