ftw 0.0.1

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.
@@ -0,0 +1,92 @@
1
+ require "ftw/namespace"
2
+ require "ftw/http/headers"
3
+ require "ftw/crlf"
4
+
5
+ # HTTP Message, RFC2616
6
+ module FTW::HTTP::Message
7
+ include FTW::CRLF
8
+
9
+ # The HTTP headers. See FTW::HTTP::Headers
10
+ # RFC2616 5.3 - <http://tools.ietf.org/html/rfc2616#section-5.3>
11
+ attr_reader :headers
12
+
13
+ # The HTTP version. See VALID_VERSIONS for valid versions.
14
+ # This will always be a Numeric object.
15
+ # Both Request and Responses have version, so put it in the parent class.
16
+ attr_accessor :version
17
+ VALID_VERSIONS = [1.0, 1.1]
18
+
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
23
+ def initialize
24
+ @headers = FTW::HTTP::Headers.new
25
+ @body = nil
26
+ end # def initialize
27
+
28
+ # get a header value
29
+ public
30
+ def [](header)
31
+ return @headers[header]
32
+ end # def []
33
+
34
+ public
35
+ def []=(header, value)
36
+ @headers[header] = header
37
+ end # def []=
38
+
39
+ # See RFC2616 section 4.3: <http://tools.ietf.org/html/rfc2616#section-4.3>
40
+ public
41
+ def body=(message_body)
42
+ # TODO(sissel): if message_body is a string, set Content-Length header
43
+ # TODO(sissel): if it's an IO object, set Transfer-Encoding to chunked
44
+ # TODO(sissel): if it responds to each or appears to be Enumerable, then
45
+ # set Transfer-Encoding to chunked.
46
+ @body = message_body
47
+ end # def body=
48
+
49
+ public
50
+ def body
51
+ # TODO(sissel): verification todos follow...
52
+ # TODO(sissel): RFC2616 section 4.3 - if there is a message body
53
+ # then one of "Transfer-Encoding" *or* "Content-Length" MUST be present.
54
+ # otherwise, if neither header is present, no body is present.
55
+ # TODO(sissel): Responses to HEAD requests or those with status 1xx, 204,
56
+ # or 304 MUST NOT have a body. All other requests have a message body,
57
+ # even if that body is of zero length.
58
+ return @body
59
+ end # def body
60
+
61
+ # Does this message have a message body?
62
+ public
63
+ def body?
64
+ return @body.nil?
65
+ end # def body?
66
+
67
+ # Set the HTTP version. Must be a valid version. See VALID_VERSIONS.
68
+ public
69
+ def version=(ver)
70
+ # Accept string "1.0" or simply "1", etc.
71
+ ver = ver.to_f if !ver.is_a?(Float)
72
+
73
+ if !VALID_VERSIONS.include?(ver)
74
+ raise ArgumentError.new("#{self.class.name}#version = #{ver.inspect} is" \
75
+ "invalid. It must be a number, one of #{VALID_VERSIONS.join(", ")}")
76
+ end
77
+ @version = ver
78
+ end # def version=
79
+
80
+ # Serialize this Request according to RFC2616
81
+ # Note: There is *NO* trailing CRLF. This is intentional.
82
+ # The RFC defines:
83
+ # generic-message = start-line
84
+ # *(message-header CRLF)
85
+ # CRLF
86
+ # [ message-body ]
87
+ # Thus, the CRLF between header and body is not part of the header.
88
+ public
89
+ def to_s
90
+ return [start_line, @headers].join(CRLF)
91
+ end
92
+ end # class FTW::HTTP::Message
@@ -0,0 +1,3 @@
1
+ module FTW
2
+ module HTTP; end
3
+ end
@@ -0,0 +1,102 @@
1
+ require "ftw/namespace"
2
+ require "ftw/http/message"
3
+ require "addressable/uri" # gem addressable
4
+ require "uri" # ruby stdlib
5
+ require "http/parser" # gem http_parser.rb
6
+ require "ftw/crlf"
7
+
8
+ # An HTTP Request.
9
+ #
10
+ # See RFC2616 section 5: <http://tools.ietf.org/html/rfc2616#section-5>
11
+ class FTW::Request
12
+ include FTW::HTTP::Message
13
+ include FTW::CRLF
14
+
15
+ # The http method. Like GET, PUT, POST, etc..
16
+ # RFC2616 5.1.1 - <http://tools.ietf.org/html/rfc2616#section-5.1.1>
17
+ #
18
+ # Warning: this accessor obscures the ruby Kernel#method() method.
19
+ # I would like to call this 'verb', but my preference is first to adhere to
20
+ # RFC terminology. Further, ruby's stdlib Net::HTTP calls this 'method' as
21
+ # well (See Net::HTTPGenericRequest).
22
+ attr_accessor :method
23
+
24
+ # This is the Request-URI. Many people call this the 'path' of the request.
25
+ # RFC2616 5.1.2 - <http://tools.ietf.org/html/rfc2616#section-5.1.2>
26
+ attr_accessor :request_uri
27
+
28
+ # Lemmings. Everyone else calls Request-URI the 'path' - so I should too.
29
+ alias_method :path, :request_uri
30
+
31
+ public
32
+ def initialize(uri=nil)
33
+ super()
34
+ use_uri(uri) if !uri.nil?
35
+ @version = 1.1
36
+ end # def initialize
37
+
38
+ # Set the connection to use for this request.
39
+ public
40
+ def connection=(connection)
41
+ @connection = connection
42
+ end # def connection=
43
+
44
+ public
45
+ def execute(connection)
46
+ connection.write(to_s + CRLF)
47
+
48
+ parser = HTTP::Parser.new
49
+ parser.on_headers_complete = proc { state = :body; :stop }
50
+
51
+ data = connection.read(16384)
52
+ parser << data
53
+ # TODO(sissel): use connection.unread() if we finish reading headers
54
+ # and there's still some data left that is part of the body.
55
+ end # def execute
56
+
57
+ # TODO(sissel): Methods to write:
58
+ # 1. Parsing a request, use HTTP::Parser from http_parser.rb
59
+ # 2. Building a request from a URI or Addressable::URI
60
+
61
+ public
62
+ def use_uri(uri)
63
+ # Convert URI objects to Addressable::URI
64
+ uri = Addressable::URI.parse(uri.to_s) if uri.is_a?(URI)
65
+
66
+ # TODO(sissel): Use normalized versions of these fields?
67
+ # uri.host
68
+ # uri.port
69
+ # uri.scheme
70
+ # uri.path
71
+ # uri.password
72
+ # uri.user
73
+ @request_uri = uri.path
74
+ @headers.set("Host", uri.host)
75
+
76
+ # TODO(sissel): support authentication
77
+ end # def use_uri
78
+
79
+ # Set the method for this request. Usually something like "GET" or "PUT"
80
+ # etc. See <http://tools.ietf.org/html/rfc2616#section-5.1.1>
81
+ public
82
+ def method=(method)
83
+ # RFC2616 5.1.1 doesn't say the method has to be uppercase.
84
+ # It can be any 'token' besides the ones defined in section 5.1.1:
85
+ # The grammar for 'token' is:
86
+ # token = 1*<any CHAR except CTLs or separators>
87
+ # TODO(sissel): support section 5.1.1 properly. Don't upcase, but
88
+ # maybe upcase things that are defined in 5.1.1 like GET, etc.
89
+ @method = method.upcase
90
+ end # def method=
91
+
92
+ # Get the request line (first line of the http request)
93
+ # From the RFC: Request-Line = Method SP Request-URI SP HTTP-Version CRLF
94
+ #
95
+ # Note: I skip the trailing CRLF. See the to_s method where it is provided.
96
+ def request_line
97
+ return "#{method} #{request_uri} HTTP/#{version}"
98
+ end # def request_line
99
+
100
+ # Define the Message's start_line as request_line
101
+ alias_method :start_line, :request_line
102
+ end # class FTW::Request < Message
@@ -0,0 +1,5 @@
1
+ require "ftw/namespace"
2
+
3
+ module FTW
4
+ VERSION = "0.0.1"
5
+ end
@@ -0,0 +1 @@
1
+ require "net/ftw"
@@ -0,0 +1,5 @@
1
+ require "net/ftw/http/client"
2
+ require "net/ftw/connection"
3
+ require "net/ftw/dns"
4
+ require "net/ftw/websocket"
5
+ #require "net/ftw/spdy"
@@ -0,0 +1,10 @@
1
+ require "net/ftw/namespace"
2
+
3
+ # Goal: Provide a nice way to sanely access web crap.
4
+ # * websockets
5
+ # * http
6
+ # * spdy
7
+ # * https
8
+ class Net::FTW::UserAgent
9
+
10
+ end
@@ -0,0 +1,296 @@
1
+ require "cabin" # rubygem "cabin"
2
+ require "net/ftw/dns"
3
+ require "net/ftw/namespace"
4
+ require "socket"
5
+ require "timeout" # ruby stdlib, just for the Timeout exception.
6
+ require "backport-bij" # for Array#rotate, IO::WaitWritable, etc, in ruby < 1.9
7
+
8
+ # TODO(sissel): What's the API look like here?
9
+ # EventMachine::Connection has these:
10
+ # * events: post_init (and connection_completed), receive_data, unbind
11
+ # * methods: send_data, close
12
+ # Socket has
13
+ # * no events
14
+ # * methods: connect, read, write, close
15
+ #
16
+ # Actual events:
17
+ # * connected
18
+ # * disconnected(reason)
19
+ # * timeout, connection reset, connection refused, write error, read
20
+ # error, etc
21
+ # * data received
22
+ #
23
+ # Methods
24
+ # * send data
25
+ # * reconnect
26
+ # * get socket
27
+ # * disconnect
28
+ #
29
+
30
+ # A network connection. This is TCP.
31
+ #
32
+ # Example:
33
+ #
34
+ # conn = Net::FTW::Connection.new("www.google.com:80")
35
+ # conn.on(CONNECTED) do |address|
36
+ # puts "Connected to #{address} (#{conn.peer})"
37
+ # conn.write("GET / HTTP/1.0\r\n\r\n")
38
+ # end
39
+ # conn.on(DATA) do |data|
40
+ # puts data
41
+ # end
42
+ # conn.run
43
+ #
44
+ # You can use IO::select on this objects of this type.
45
+ class Net::FTW::Connection
46
+
47
+ # Events
48
+ CONNECTED = :connected
49
+ DISCONNECTED = :disconnected
50
+ READER_CLOSED = :reader_closed
51
+ DATA = :data
52
+
53
+ # Disconnection reasons
54
+ TIMEOUT = :timeout
55
+ REFUSED = :refused
56
+ LOST = :lost
57
+ INTENTIONAL = :intentional
58
+
59
+ # A new network connection.
60
+ # The 'destination' argument can be an array of strings or a single string.
61
+ # String format is expected to be "host:port"
62
+ #
63
+ # Example:
64
+ #
65
+ # conn = Net::FTW::Connection.new(["1.2.3.4:80", "1.2.3.5:80"])
66
+ #
67
+ # If you specify multiple destinations, they are used in a round-robin
68
+ # decision made during reconnection.
69
+ public
70
+ def initialize(destinations)
71
+ if destinations.is_a?(String)
72
+ @destinations = [destinations]
73
+ else
74
+ @destinations = destinations
75
+ end
76
+
77
+ # Handlers are key => array of callbacks
78
+ @handlers = Hash.new { |h,k| h[k] = [] }
79
+
80
+ on(CONNECTED) { |address| connected(address) }
81
+ on(DISCONNECTED) { |reason, error| disconnected(reason, error) }
82
+
83
+ @connect_timeout = 2
84
+
85
+ # Use a fixed-size string that we set to BINARY encoding.
86
+ # Not all byte sequences are UTF-8 friendly :0
87
+ @read_size = 16384
88
+ @read_buffer = " " * @read_size
89
+
90
+ # Tell Ruby 1.9 that this string is a binary string, not utf-8 or somesuch.
91
+ if @read_buffer.respond_to?(:force_encoding)
92
+ @read_buffer.force_encoding("BINARY")
93
+ end
94
+
95
+ # TODO(sissel): Validate @destinations
96
+ end # def initialize
97
+
98
+ # Register an event callback
99
+ # Valid events:
100
+ #
101
+ # * Net::FTW::Connection::CONNECTED - 1 argument, the host:port string connected to.
102
+ # * Net::FTW::Connection::DISCONNECTED - 2 arguments, the reason and the
103
+ # exception (if any)
104
+ # * Net::FTW::Connection::DATA - 1 argument to block, the data read
105
+ #
106
+ # Disconnection reasons:
107
+ # * :timeout
108
+ # * :refused
109
+ # * :closed
110
+ # * :lost
111
+ public
112
+ def on(event, &block)
113
+ @handlers[event] << block
114
+ end # def on
115
+
116
+ # Trigger an event with arguments.
117
+ # All callbacks for the event will be invoked in the order they were
118
+ # registered. See the 'on' method for registering callbacks.
119
+ public
120
+ def trigger(event, *args)
121
+ @handlers[event].each do |block|
122
+ block.call(*args)
123
+ end
124
+ end # def trigger
125
+
126
+ public
127
+ def connect(timeout=nil)
128
+ # TODO(sissel): Raise if we're already connected?
129
+ close if connected?
130
+ host, port = @destinations.first.split(":")
131
+ @destinations = @destinations.rotate # round-robin
132
+
133
+ # Do dns resolution on the host. If there are multiple
134
+ # addresses resolved, return one at random.
135
+ @remote_address = Net::FTW::DNS.singleton.resolve_random(host)
136
+
137
+ family = @remote_address.include?(":") ? Socket::AF_INET6 : Socket::AF_INET
138
+ @socket = Socket.new(family, Socket::SOCK_STREAM, 0)
139
+ sockaddr = Socket.pack_sockaddr_in(port, @remote_address)
140
+ # TODO(sissel): Support local address binding
141
+
142
+ # Connect with timeout
143
+ begin
144
+ @socket.connect_nonblock(sockaddr)
145
+ rescue IO::WaitWritable
146
+ # Ruby actually raises Errno::EINPROGRESS, but for some reason
147
+ # the documentation says to use this IO::WaitWritable thing...
148
+ # I don't get it, but whatever :(
149
+ if writable?(timeout)
150
+ begin
151
+ @socket.connect_nonblock(sockaddr) # check connection failure
152
+ rescue Errno::EISCONN # Ignore, we're already connected.
153
+ rescue Errno::ECONNREFUSED => e
154
+ # Fire 'disconnected' event with reason :refused
155
+ trigger(DISCONNECTED, :refused, e)
156
+ end
157
+ else
158
+ # Connection timeout
159
+ # Fire 'disconnected' event with reason :timeout
160
+ trigger(DISCONNECTED, :connect_timeout, nil)
161
+ end
162
+ end
163
+
164
+ # We're now connected.
165
+ trigger(CONNECTED, "#{host}:#{port}")
166
+ end # def connect
167
+
168
+ # Is this Connection connected?
169
+ public
170
+ def connected?
171
+ return @connected
172
+ end # def connected?
173
+
174
+ # Write data to this connection.
175
+ # This method blocks until the write succeeds unless a timeout is given.
176
+ #
177
+ # Returns the number of bytes written (See IO#syswrite)
178
+ public
179
+ def write(data, timeout=nil)
180
+ #connect if !connected?
181
+ if writable?(timeout)
182
+ return @socket.syswrite(data)
183
+ else
184
+ raise Timeout::Error.new
185
+ end
186
+ end # def write
187
+
188
+ # Read data from this connection
189
+ # This method blocks until the read succeeds unless a timeout is given.
190
+ #
191
+ # This method is not guaranteed to read exactly 'length' bytes. See
192
+ # IO#sysread
193
+ public
194
+ def read(length, timeout=nil)
195
+ if readable?(timeout)
196
+ begin
197
+ @socket.sysread(length, @read_buffer)
198
+ return @read_buffer
199
+ rescue EOFError
200
+ trigger(READER_CLOSED)
201
+ end
202
+ else
203
+ raise Timeout::Error.new
204
+ end
205
+ end # def read
206
+
207
+ # End this connection
208
+ public
209
+ def disconnect(reason=INTENTIONAL)
210
+ begin
211
+ #@reader_closed = true
212
+ @socket.close_read
213
+ rescue IOError => e
214
+ # Ignore
215
+ end
216
+
217
+ begin
218
+ @socket.close_write
219
+ rescue IOError => e
220
+ # Ignore
221
+ end
222
+
223
+ trigger(DISCONNECTED, reason)
224
+ end # def disconnect
225
+
226
+ # Is this connection writable? Returns true if it is writable within
227
+ # the timeout period. False otherwise.
228
+ #
229
+ # The time out is in seconds. Fractional seconds are OK.
230
+ public
231
+ def writable?(timeout)
232
+ ready = IO.select(nil, [@socket], nil, timeout)
233
+ return !ready.nil?
234
+ end # def writable?
235
+
236
+ # Is this connection readable? Returns true if it is readable within
237
+ # the timeout period. False otherwise.
238
+ #
239
+ # The time out is in seconds. Fractional seconds are OK.
240
+ public
241
+ def readable?(timeout)
242
+ #return false if @reader_closed
243
+ ready = IO.select([@socket], nil, nil, timeout)
244
+ return !ready.nil?
245
+ end # def readable?
246
+
247
+ protected
248
+ def connected(address)
249
+ @remote_address = nil
250
+ @connected = true
251
+ end # def connected
252
+
253
+ protected
254
+ def disconnected(reason, error)
255
+ @remote_address = nil
256
+ @connected = false
257
+ end # def disconnected
258
+
259
+ # The host:port
260
+ public
261
+ def peer
262
+ return @remote_address
263
+ end # def peer
264
+
265
+ # Run this Connection.
266
+ # This is generally meant for Threaded or synchronous operation.
267
+ # For EventMachine, see TODO(sissel): Implement EventMachine support.
268
+ public
269
+ def run
270
+ connect(@connect_timeout) if not connected?
271
+ while connected?
272
+ read_and_trigger
273
+ end
274
+ end # def run
275
+
276
+ # Read data and trigger data callbacks.
277
+ #
278
+ # This is mainly useful if you are implementing your own run loops
279
+ # and IO::select shenanigans.
280
+ public
281
+ def read_and_trigger
282
+ data = read(@read_size)
283
+ if data.length == 0
284
+ disconnect(EOFError)
285
+ else
286
+ trigger(DATA, data)
287
+ end
288
+ end # def read_and_trigger
289
+
290
+ # Support 'to_io' so you can use IO::select on this object.
291
+ public
292
+ def to_io
293
+ return @socket
294
+ end
295
+ end # class Net::FTW::Connection
296
+