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