ftw 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+