rev 0.1.4 → 0.2.0

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.
@@ -2,11 +2,7 @@
2
2
  # Copyright (C)2007 Tony Arcieri
3
3
  # You can redistribute this under the terms of the Ruby license
4
4
  # See file LICENSE for details
5
- #++
6
-
7
- require File.dirname(__FILE__) + '/../rev'
8
-
9
- #--
5
+ #
10
6
  # Gimpy hacka asynchronous DNS resolver
11
7
  #
12
8
  # Word to the wise: I don't know what I'm doing here. This was cobbled together as
@@ -110,6 +106,8 @@ module Rev
110
106
  end
111
107
 
112
108
  def request_question(hostname)
109
+ raise ArgumentError, "hostname cannot be nil" if hostname.nil?
110
+
113
111
  # Query name
114
112
  message = hostname.split('.').map { |s| [s.size].pack('C') << s }.join + "\0"
115
113
 
@@ -142,15 +140,15 @@ module Rev
142
140
  return unless id == 2
143
141
 
144
142
  # Check the QR value and confirm this message is a response
145
- qr = message[2].unpack('B1').first.to_i
143
+ qr = message[2..2].unpack('B1').first.to_i
146
144
  return unless qr == 1
147
145
 
148
146
  # Check the RCODE (lower nibble) and ensure there wasn't an error
149
- rcode = message[3].unpack('B8').first[4..7].to_i(2)
147
+ rcode = message[3..3].unpack('B8').first[4..7].to_i(2)
150
148
  return unless rcode == 0
151
149
 
152
150
  # Extract the question and answer counts
153
- qdcount, ancount = message[4..7].unpack('nn').map(&:to_i)
151
+ qdcount, ancount = message[4..7].unpack('nn').map { |n| n.to_i }
154
152
 
155
153
  # We only asked one question
156
154
  return unless qdcount == 1
@@ -5,7 +5,6 @@
5
5
  # See file LICENSE for details
6
6
  #++
7
7
 
8
- require File.dirname(__FILE__) + '/../rev'
9
8
  require File.dirname(__FILE__) + '/../http11_client'
10
9
 
11
10
  module Rev
@@ -79,13 +78,13 @@ module Rev
79
78
  remote_host + (remote_port.to_i != 80 ? ":#{remote_port}" : "")
80
79
  end
81
80
 
82
- def encode_request(method, uri, query)
83
- HTTP_REQUEST_HEADER % [method.to_s.upcase, encode_query(uri, query)]
81
+ def encode_request(method, path, query)
82
+ HTTP_REQUEST_HEADER % [method.to_s.upcase, encode_query(path, query)]
84
83
  end
85
84
 
86
- def encode_query(uri, query)
87
- return uri unless query
88
- uri + "?" + query.map { |k, v| encode_param(k, v) }.join('&')
85
+ def encode_query(path, query)
86
+ return path unless query
87
+ path + "?" + query.map { |k, v| encode_param(k, v) }.join('&')
89
88
  end
90
89
 
91
90
  # URL encodes a single k=v parameter.
@@ -167,27 +166,21 @@ module Rev
167
166
  # body: String
168
167
  # Specify the request body (you must encode it for now)
169
168
  #
170
- def request(method, uri, options = {})
169
+ def request(method, path, options = {})
170
+ raise ArgumentError, "invalid request path" unless path[0] == '/'
171
171
  raise RuntimeError, "request already sent" if @requested
172
172
 
173
- @method, @uri, @options = method, uri, options
173
+ @method, @path, @options = method, path, options
174
174
  @requested = true
175
175
 
176
176
  return unless @connected
177
177
  send_request
178
178
  end
179
-
180
- # Requests can be made through method missing by invoking the HTTP method to use, i.e.:
181
- #
182
- # httpclient.get(path, options)
183
- #
184
- # Valid for: get, post, put, delete, head
185
- #
186
- # To use other HTTP methods, invoke the request method directly
187
- #
188
- def method_missing(method, *args)
189
- raise NoMethodError, "method not supported" unless ALLOWED_METHODS.include? method.to_sym
190
- request method, *args
179
+
180
+ # Enable the HttpClient if it has been disabled
181
+ def enable
182
+ super
183
+ dispatch unless @data.empty?
191
184
  end
192
185
 
193
186
  # Called when response header has been received
@@ -205,8 +198,9 @@ module Rev
205
198
  close
206
199
  end
207
200
 
208
- # Called when an error occurs during the request
201
+ # Called when an error occurs dispatching the request
209
202
  def on_error(reason)
203
+ close
210
204
  raise RuntimeError, reason
211
205
  end
212
206
 
@@ -217,10 +211,10 @@ module Rev
217
211
  #
218
212
  # Rev callbacks
219
213
  #
220
-
214
+
221
215
  def on_connect
222
216
  @connected = true
223
- send_request if @method and @uri
217
+ send_request if @method and @path
224
218
  end
225
219
 
226
220
  def on_read(data)
@@ -256,7 +250,7 @@ module Rev
256
250
  head['connection'] ||= 'close'
257
251
 
258
252
  # Build the request
259
- request_header = encode_request(@method, @uri, query)
253
+ request_header = encode_request(@method, @path, query)
260
254
  request_header << encode_headers(head)
261
255
  request_header << encode_cookies(cookies) if cookies
262
256
  request_header << CRLF
@@ -273,7 +267,7 @@ module Rev
273
267
  #
274
268
 
275
269
  def dispatch
276
- while case @state
270
+ while enabled? and case @state
277
271
  when :response_header
278
272
  parse_response_header
279
273
  when :chunk_header
@@ -296,7 +290,13 @@ module Rev
296
290
  def parse_header(header)
297
291
  return false if @data.empty?
298
292
 
299
- @parser_nbytes = @parser.execute(header, @data.to_str, @parser_nbytes)
293
+ begin
294
+ @parser_nbytes = @parser.execute(header, @data.to_str, @parser_nbytes)
295
+ rescue Rev::HttpClientParserError
296
+ on_error "invalid HTTP format, parsing fails"
297
+ @state = :invalid
298
+ end
299
+
300
300
  return false unless @parser.finished?
301
301
 
302
302
  # Clear parsed data from the buffer
@@ -385,12 +385,15 @@ module Rev
385
385
  end
386
386
 
387
387
  def process_body
388
- # FIXME the proper thing to do here is probably to keep reading until
389
- # the socket closes, then assume that's the end of the body, provided
390
- # the server has specified Connection: close
391
388
  if @bytes_remaining.nil?
392
- on_error "no content length specified"
393
- @state = :invalid
389
+ on_body_data @data.read
390
+ return false
391
+ end
392
+
393
+ if @bytes_remaining.zero?
394
+ on_request_complete
395
+ @state = :finished
396
+ return false
394
397
  end
395
398
 
396
399
  if @data.size < @bytes_remaining
@@ -413,4 +416,4 @@ module Rev
413
416
  false
414
417
  end
415
418
  end
416
- end
419
+ end
@@ -4,12 +4,10 @@
4
4
  # See file LICENSE for details
5
5
  #++
6
6
 
7
- require File.dirname(__FILE__) + '/../rev'
8
-
9
7
  module Rev
10
8
  # A buffered I/O class witch fits into the Rev Watcher framework.
11
9
  # It provides both an observer which reads data as it's received
12
- # from the wire and a buffered writer which stores data and writes
10
+ # from the wire and a buffered write watcher which stores data and writes
13
11
  # it out each time the socket becomes writable.
14
12
  #
15
13
  # This class is primarily meant as a base class for other streams
@@ -20,11 +18,8 @@ module Rev
20
18
  INPUT_SIZE = 16384
21
19
 
22
20
  def initialize(io)
23
- # Output buffer
21
+ @io = io
24
22
  @write_buffer = Rev::Buffer.new
25
-
26
- # Coerce the argument into an IO object if possible
27
- @io = ::IO.try_convert(io)
28
23
  super(@io)
29
24
  end
30
25
 
@@ -50,7 +45,9 @@ module Rev
50
45
 
51
46
  # Write data in a buffered, non-blocking manner
52
47
  def write(data)
53
- buffered_write data
48
+ @write_buffer << data
49
+ schedule_write
50
+ data.size
54
51
  end
55
52
 
56
53
  # Number of bytes are currently in the output buffer
@@ -61,7 +58,7 @@ module Rev
61
58
  # Close the IO stream
62
59
  def close
63
60
  detach if attached?
64
- @writer.detach if @writer and @writer.attached?
61
+ detach_write_watcher
65
62
  @io.close unless @io.closed?
66
63
 
67
64
  on_close
@@ -76,62 +73,69 @@ module Rev
76
73
  #########
77
74
  protected
78
75
  #########
79
-
80
- # Buffered writer
81
- def buffered_write(data)
82
- @write_buffer << data
83
- schedule_write
84
- data.size
85
- end
86
76
 
87
- # Attempt to write the contents of the output buffer
88
- def write_output_buffer
77
+ # Read from the input buffer and dispatch to on_read
78
+ def on_readable
79
+ begin
80
+ on_read @io.read_nonblock(INPUT_SIZE)
81
+ rescue Errno::ECONNRESET, EOFError
82
+ close
83
+ end
84
+ end
85
+
86
+ # Write the contents of the output buffer
87
+ def on_writable
89
88
  begin
90
89
  @write_buffer.write_to(@io)
91
- rescue Errno::EPIPE
90
+ rescue Errno::EPIPE, Errno::ECONNRESET
92
91
  return close
93
92
  end
94
93
 
95
94
  if @write_buffer.empty?
96
- @writer.disable if @writer and @writer.enabled?
95
+ disable_write_watcher
97
96
  on_write_complete
98
97
  end
99
98
  end
100
99
 
101
- # Inherited callback from IOWatcher
102
- def on_readable
100
+ # Schedule a write to be performed when the IO object becomes writable
101
+ def schedule_write
103
102
  begin
104
- on_read @io.read_nonblock(INPUT_SIZE)
105
- rescue Errno::ECONNRESET, EOFError
106
- close
103
+ enable_write_watcher
104
+ rescue IOError
107
105
  end
108
106
  end
109
107
 
110
- # Schedule a write to be performed when the IO object becomes writable
111
- def schedule_write
112
- return if @writer and @writer.enabled?
113
- if @writer
114
- @writer.enable
108
+ # Return a handle to the writing IOWatcher
109
+ def write_watcher
110
+ @write_watcher ||= WriteWatcher.new(@io, self)
111
+ end
112
+
113
+ def enable_write_watcher
114
+ if write_watcher.attached?
115
+ write_watcher.enable unless write_watcher.enabled?
115
116
  else
116
- begin
117
- @writer = Writer.new(@io, self)
118
- rescue IOError
119
- return
120
- end
121
-
122
- @writer.attach(evloop)
117
+ write_watcher.attach(evloop)
123
118
  end
124
119
  end
120
+
121
+ def disable_write_watcher
122
+ @write_watcher.disable if @write_watcher and @write_watcher.enabled?
123
+ end
124
+
125
+ def detach_write_watcher
126
+ @write_watcher.detach if @write_watcher and @write_watcher.attached?
127
+ end
125
128
 
126
- class Writer < IOWatcher
127
- def initialize(io, buffered_io)
128
- @buffered_io = buffered_io
129
- super(io, :w)
129
+ class WriteWatcher < IOWatcher
130
+ def initialize(ruby_io, rev_io)
131
+ @rev_io = rev_io
132
+ super(ruby_io, :w)
130
133
  end
131
134
 
135
+ # Delegate on_writable to the Rev::IO object
132
136
  def on_writable
133
- @buffered_io.__send__(:write_output_buffer)
137
+ @rev_io.__send__(:on_writable)
134
138
  end
135
139
  end
136
140
  end
137
- end
141
+ end
@@ -4,8 +4,6 @@
4
4
  # See file LICENSE for details
5
5
  #++
6
6
 
7
- require File.dirname(__FILE__) + '/../rev'
8
-
9
7
  module Rev
10
8
  class IOWatcher
11
9
  # The actual implementation of this class resides in the C extension
@@ -5,7 +5,6 @@
5
5
  #++
6
6
 
7
7
  require 'socket'
8
- require File.dirname(__FILE__) + '/../rev'
9
8
 
10
9
  module Rev
11
10
  # Listeners wait for incoming connections. When a listener receives a
@@ -4,7 +4,6 @@
4
4
  # See file LICENSE for details
5
5
  #++
6
6
 
7
- require File.dirname(__FILE__) + '/../rev'
8
7
  require 'thread'
9
8
 
10
9
  # Monkeypatch Thread to include a method for obtaining the default Rev::Loop
@@ -18,9 +17,17 @@ module Rev
18
17
  class Loop
19
18
  attr_reader :watchers
20
19
 
21
- # Retrieve the default event loop for the current thread
22
- def self.default
23
- Thread.current._rev_loop
20
+ # In Ruby 1.9 we want a Rev::Loop per thread, but Ruby 1.8 is unithreaded
21
+ if RUBY_VERSION.gsub('.', '').to_i >= 190
22
+ # Retrieve the default event loop for the current thread
23
+ def self.default
24
+ Thread.current._rev_loop
25
+ end
26
+ else
27
+ # Retrieve the default event loop
28
+ def self.default
29
+ @@_rev_loop ||= Rev::Loop.new
30
+ end
24
31
  end
25
32
 
26
33
  # Create a new Rev::Loop
@@ -4,8 +4,6 @@
4
4
  # See file LICENSE for details
5
5
  #++
6
6
 
7
- require File.dirname(__FILE__) + '/../rev'
8
-
9
7
  module Rev
10
8
  class Server < Listener
11
9
  # Servers listen for incoming connections and create new connection objects
@@ -35,8 +33,8 @@ module Rev
35
33
 
36
34
  def on_connection(socket)
37
35
  connection = @klass.new(socket, *@args).attach(evloop)
38
- @block.(connection) if @block
39
- connection.on_connect
36
+ connection.__send__(:on_connect)
37
+ @block.call(connection) if @block
40
38
  end
41
39
  end
42
40
 
@@ -6,7 +6,6 @@
6
6
 
7
7
  require 'socket'
8
8
  require 'resolv'
9
- require File.dirname(__FILE__) + '/../rev'
10
9
 
11
10
  module Rev
12
11
  class Socket < IO
@@ -173,7 +172,7 @@ module Rev
173
172
  # DNSResolver only supports IPv4 so we can safely assume an IPv4 address
174
173
  socket = TCPConnectSocket.new(::Socket::AF_INET, addr, port, host)
175
174
  initialize(socket, *args)
176
- @connector = Connector.new(self, socket)
175
+ @connector = Socket::Connector.new(self, socket)
177
176
  @resolver = nil
178
177
  }
179
178
  @sock.attach(evloop)
@@ -0,0 +1,184 @@
1
+ #--
2
+ # Copyright (C)2007 Tony Arcieri
3
+ # You can redistribute this under the terms of the Ruby license
4
+ # See file LICENSE for details
5
+ #++
6
+
7
+ require 'openssl'
8
+
9
+ # --
10
+ # Rev implements SSL by subclassing OpenSSL::SSL::SSLSocket in C
11
+ # and adding implementations for non-blocking versions of the
12
+ # methods it provides. Unfortunately, this relies on hacks specific
13
+ # to Ruby 1.9. If you'd like to find a workaround which is compatible
14
+ # with Ruby 1.8, have a look at rev_ssl.c and find out if you can
15
+ # properly initialize an OpenSSL::SSL::SSLSocket.
16
+ # ++
17
+ if RUBY_VERSION.gsub('.', '').to_i < 190
18
+ raise LoadError, "Rev::SSL not supported in this Ruby version, sorry"
19
+ end
20
+
21
+ module Rev
22
+ # Monkeypatch Rev::IO to include SSL support. This can be accomplished
23
+ # by extending any Rev:IO (or subclass) object with Rev::SSL after the
24
+ # connection has completed, e.g.
25
+ #
26
+ # class MySocket < Rev::TCPSocket
27
+ # def on_connect
28
+ # extend Rev::SSL
29
+ # ssl_client_start
30
+ # end
31
+ # end
32
+ #
33
+ # Please be aware that SSL is only supported on Ruby 1.9 at the present time.
34
+ #
35
+ module SSL
36
+ # Start SSL explicitly in client mode. After calling this, callbacks
37
+ # will fire for checking the peer certificate (ssl_peer_cert) and
38
+ # its validity (ssl_verify_result)
39
+ def ssl_client_start
40
+ raise "ssl already started" if @ssl_socket
41
+
42
+ context = respond_to?(:ssl_context) ? ssl_context : OpenSSL::SSL::SSLContext.new
43
+ @ssl_socket = SSL::IO.new(@io, context)
44
+ @ssl_init = proc { @ssl_socket.connect_nonblock }
45
+
46
+ ssl_init
47
+ end
48
+
49
+ # Start SSL explicitly in server mode. After calling this, callbacks
50
+ # will fire for checking the peer certificate (ssl_peer_cert) and
51
+ # its validity (ssl_verify_result)
52
+ def ssl_server_start
53
+ raise "ssl already started" if @ssl_socket
54
+
55
+ @ssl_socket = SSL::IO.new(@io, ssl_context)
56
+ @ssl_init = proc { @ssl_socket.accept_nonblock }
57
+
58
+ ssl_init
59
+ end
60
+
61
+ #########
62
+ protected
63
+ #########
64
+
65
+ def ssl_init
66
+ begin
67
+ @ssl_init.call
68
+ ssl_init_complete
69
+ rescue SSL::IO::ReadAgain
70
+ enable unless enabled?
71
+ rescue SSL::IO::WriteAgain
72
+ enable_write_watcher
73
+ rescue OpenSSL::SSL::SSLError, Errno::ECONNRESET
74
+ close
75
+ rescue => ex
76
+ if respond_to? :on_ssl_error
77
+ on_ssl_error(ex)
78
+ else raise ex
79
+ end
80
+ end
81
+ end
82
+
83
+ def ssl_init_complete
84
+ @ssl_init = nil
85
+ enable unless enabled?
86
+
87
+ on_peer_cert(@ssl_socket.peer_cert) if respond_to? :on_peer_cert
88
+ on_ssl_result(@ssl_socket.verify_result) if respond_to? :on_ssl_result
89
+ on_ssl_connect if respond_to? :on_ssl_connect
90
+ end
91
+
92
+ def on_readable
93
+ if @ssl_init
94
+ disable
95
+ ssl_init
96
+ return
97
+ end
98
+
99
+ begin
100
+ on_read @ssl_socket.read_nonblock(Rev::IO::INPUT_SIZE)
101
+ rescue Errno::AGAIN, SSL::IO::ReadAgain
102
+ return
103
+ rescue OpenSSL::SSL::SSLError, Errno::ECONNRESET, EOFError
104
+ close
105
+ end
106
+ end
107
+
108
+ def on_writable
109
+ if @ssl_init
110
+ disable_write_watcher
111
+ ssl_init
112
+ return
113
+ end
114
+
115
+ begin
116
+ nbytes = @ssl_socket.write_nonblock @write_buffer.to_str
117
+ rescue Errno::EAGAIN, SSL::IO::WriteAgain
118
+ return
119
+ rescue OpenSSL::SSL::SSLError, Errno::EPIPE, Errno::ECONNRESET
120
+ close
121
+ return
122
+ end
123
+
124
+ @write_buffer.read(nbytes)
125
+
126
+ if @write_buffer.empty?
127
+ disable_write_watcher
128
+ on_write_complete
129
+ end
130
+ end
131
+ end
132
+
133
+ # A socket class for outgoing and incoming SSL connections. Please be aware
134
+ # that SSL is only supported on Ruby 1.9 at the present time.
135
+ class SSLSocket < TCPSocket
136
+ # Perform a non-blocking connect to the given host and port
137
+ def self.connect(addr, port, *args)
138
+ super.instance_eval { @connecting = true; self }
139
+ end
140
+
141
+ # Returns the OpenSSL::SSL::SSLContext for to use for the session.
142
+ # By default no certificates will be checked. If you would like
143
+ # any certificate checking to be performed, please override this
144
+ # class and return a context loaded with the appropriate certificates.
145
+ def ssl_context
146
+ @ssl_context ||= OpenSSL::SSL::SSLContext.new
147
+ end
148
+
149
+ # Called when SSL handshaking has successfully completed
150
+ def on_ssl_connect; end
151
+ event_callback :on_ssl_connect
152
+
153
+ # Called when peer certificate has successfully been received.
154
+ # Equivalent to OpenSSL::SSL::SSLSocket#peer_cert
155
+ def on_peer_cert(peer_cert); end
156
+ event_callback :on_peer_cert
157
+
158
+ # Called when SSL handshaking has been completed successfully.
159
+ # Equivalent to OpenSSL::SSL::SSLSocket#verify_result
160
+ def on_ssl_result(result); end
161
+ event_callback :on_ssl_result
162
+
163
+ # Called if an error occurs during SSL session initialization
164
+ def on_ssl_error(exception); end
165
+ event_callback :on_ssl_error
166
+
167
+ #########
168
+ protected
169
+ #########
170
+
171
+ def on_connect
172
+ extend SSL
173
+ @connecting ? ssl_client_start : ssl_server_start
174
+ end
175
+ end
176
+
177
+ # A class for implementing SSL servers. Please be aware that SSL is only
178
+ # supported on Ruby 1.9 at the present time.
179
+ class SSLServer < TCPServer
180
+ def initialize(host, port, klass = SSLSocket, *args, &block)
181
+ super
182
+ end
183
+ end
184
+ end