ftw 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,10 +1,21 @@
1
1
  # For The Web
2
2
 
3
- net/http is pretty much not good. dns behavior in ruby changes quite frequently.
3
+ ## Getting Started
4
4
 
5
- Above all else, I want a consistent API and behavior that I can rely on. Ruby stdlib is not that thing.
5
+ For doing client stuff (http requests, etc), you'll want {FTW::Agent}.
6
6
 
7
- I want:
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
- ## TODO
28
+ For reference:
18
29
 
19
- * Tests, yo.
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 Scratch
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
- # SPDY should automatically be attempted. The caller should be unaware.
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
- request = agent.websocket("http://somehost/endpoint")
42
- # Set auth header
43
- request["Authorization"] = ...
44
- request["Cookie"] = ...
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
- websocket, error = request.execute
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)
@@ -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
- public
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
@@ -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
 
@@ -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
@@ -16,24 +16,29 @@ class FTW::HTTP::Headers
16
16
  include Enumerable
17
17
  include FTW::CRLF
18
18
 
19
- # Make a new headers container. You can pass a hash of
20
- public
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
- # Set a header field to a specific value.
36
- # Any existing value(s) for this field are destroyed.
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
- # This will return:
97
- # * String if there is only a single value for this field
98
- # * Array of String if there are multiple values for this field
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
- public
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
- public
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
- public
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
@@ -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. See FTW::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
- # A new HTTP Message. You probably won't use this class much.
20
- # See RFC2616 section 4: <http://tools.ietf.org/html/rfc2616#section-4>
21
- # See Request and Response.
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
- # get a header value
29
- public
30
- def [](header)
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
- public
35
- def []=(header, value)
36
- @headers[header] = header
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
- public
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
- # Does this message have a message body / content?
62
- public
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
- public
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
@@ -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
@@ -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
 
@@ -1,5 +1,5 @@
1
1
  require "ftw/namespace"
2
2
 
3
3
  module FTW
4
- VERSION = "0.0.4"
4
+ VERSION = "0.0.5"
5
5
  end
@@ -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
 
@@ -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
- public
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
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 web stuff.
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/agent.rb
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. HTTP, WebSockets, SPDY, etc.
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: