em-ws-client 0.1.2 → 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.
@@ -1,7 +1,11 @@
1
+ $:.unshift File.dirname(__FILE__) + "/lib"
2
+
3
+ require "em-ws-client"
4
+
1
5
  spec = Gem::Specification.new do |s|
2
6
  s.name = "em-ws-client"
3
- s.version = "0.1.2"
4
- s.date = "2011-09-26"
7
+ s.version = EM::WebSocketClient::Version
8
+ s.date = "2012-04-14"
5
9
  s.summary = "EventMachine WebSocket Client"
6
10
  s.email = "dan@shove.io"
7
11
  s.homepage = "https://github.com/dansimpson/em-ws-client"
@@ -9,7 +13,6 @@ spec = Gem::Specification.new do |s|
9
13
  s.has_rdoc = true
10
14
 
11
15
  s.add_dependency("eventmachine", "~> 1.0.0.beta.4")
12
- s.add_dependency("state_machine", "~> 1.0.2")
13
16
 
14
17
  s.authors = ["Dan Simpson"]
15
18
 
@@ -1,196 +1,16 @@
1
1
  require "rubygems"
2
2
  require "eventmachine"
3
- require "state_machine"
4
3
  require "uri"
5
- require "digest/sha1"
6
4
  require "base64"
7
- require "codec/draft10encoder.rb"
8
- require "codec/draft10decoder.rb"
9
-
10
-
11
- module EM
12
- class WebSocketClient
13
-
14
- Version = "0.1.2"
15
-
16
- class WebSocketConnection < EM::Connection
17
-
18
- def client=(client)
19
- @client = client
20
- @client.connection = self
21
- end
22
-
23
- def receive_data(data)
24
- @client.receive_data data
25
- end
26
-
27
- def unbind(reason=nil)
28
- @client.disconnect
29
- end
30
-
31
- end
32
-
33
- attr_accessor :connection
34
-
35
- state_machine :initial => :disconnected do
36
-
37
- # States
38
- state :disconnected
39
- state :connecting
40
- state :negotiating
41
- state :established
42
- state :failed
43
-
44
- after_transition :to => :connecting, :do => :connect
45
- after_transition :to => :negotiating, :do => :on_negotiating
46
- after_transition :to => :established, :do => :on_established
47
-
48
- event :start do
49
- transition :disconnected => :connecting
50
- end
51
-
52
- event :negotiate do
53
- transition :connecting => :negotiating
54
- end
55
-
56
- event :complete do
57
- transition :negotiating => :established
58
- end
59
-
60
- event :error do
61
- transition all => :failed
62
- end
63
-
64
- event :disconnect do
65
- transition all => :disconnected
66
- end
67
-
68
- end
69
-
70
- def initialize uri, origin="em-websocket-client"
71
- super();
72
-
73
- @uri = URI.parse(uri)
74
- @origin = origin
75
- @queue = []
76
-
77
- @encoder = Draft10Encoder.new
78
- @decoder = Draft10Decoder.new
79
-
80
- @request_key = build_request_key
81
- @buffer = ""
82
-
83
- start
84
- end
85
-
86
- # Called on opening of the websocket
87
- def onopen &block
88
- @open_handler = block
89
- end
90
-
91
- # Called on the close of the connection
92
- def onclose &block
93
- @cblock = block
94
- end
95
-
96
- # Called when a message is received
97
- def onmessage &block
98
- @message_handler = block
99
- end
100
-
101
- # EM callback
102
- def receive_data(data)
103
- if negotiating?
104
- @buffer << data
105
- request, rest = @buffer.split("\r\n\r\n", 2)
106
- if rest
107
- @buffer = ""
108
- handle_response(request)
109
- receive_data rest
110
- end
111
- else
112
- message = @decoder.decode(data)
113
- if message
114
- if @message_handler
115
- @message_handler.call(message)
116
- end
117
- end
118
- end
119
- end
120
-
121
- # Send a WebSocket frame to the remote
122
- # host.
123
- def send_data data
124
- if established?
125
- connection.send_data(@encoder.encode(data))
126
- else
127
- @queue << data
128
- end
129
- end
130
-
131
- private
132
-
133
- # Connect to the remote host and synchonize the connection
134
- # and this client object
135
- def connect
136
- EM.connect @uri.host, @uri.port || 80, WebSocketConnection do |conn|
137
- conn.client = self
138
- negotiate
139
- end
140
- end
141
-
142
- # Send HTTP request with upgrade goodies
143
- # to the remote host
144
- def on_negotiating
145
- request = "GET #{@uri.path} HTTP/1.1\r\n"
146
- request << "Upgrade: WebSocket\r\n"
147
- request << "Connection: Upgrade\r\n"
148
- request << "Host: #{@uri.host}\r\n"
149
- request << "Sec-WebSocket-Key: #{@request_key}\r\n"
150
- request << "Sec-WebSocket-Version: 8\r\n"
151
- request << "Sec-WebSocket-Origin: #{@origin}\r\n"
152
- request << "\r\n"
153
- connection.send_data(request)
154
- end
155
-
156
- def on_established
157
- if @open_handler
158
- @open_handler.call
159
- end
160
-
161
- while !@queue.empty?
162
- send_data @queue.shift
163
- end
164
- end
165
-
166
- # Handle the HTTP response and ensure it's valid
167
- # by checking the Sec-WebSocket-Accept header
168
- def handle_response response
169
- lines = response.split("\r\n")
170
- table = {}
171
-
172
- lines.each do |line|
173
- header = /^([^:]+):\s*(.+)$/.match(line)
174
- table[header[1].downcase.strip] = header[2].strip if header
175
- end
176
-
177
- if table["sec-websocket-accept"] == build_response_key
178
- complete
179
- else
180
- error
181
- end
182
- end
183
-
184
- # Build a unique request key to match against
185
- def build_request_key
186
- Base64.encode64(Time.now.to_i.to_s(16)).chomp
187
- end
5
+ require "digest/sha1"
188
6
 
189
- # Build the response key from the given request key
190
- # for comparison with the response value.
191
- def build_response_key
192
- Base64.encode64(Digest::SHA1.digest("#{@request_key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11")).chomp
193
- end
194
-
7
+ module EventMachine
8
+ module WebSocketCodec
195
9
  end
196
- end
10
+ end
11
+
12
+ require "em-ws-client/handshake.rb"
13
+ require "em-ws-client/protocol.rb"
14
+ require "em-ws-client/encoder.rb"
15
+ require "em-ws-client/decoder.rb"
16
+ require "em-ws-client/client.rb"
@@ -0,0 +1,300 @@
1
+ # encoding: UTF-8
2
+
3
+ module EventMachine
4
+
5
+ # Public: A fully functional WebSocket client
6
+ # implementation.
7
+ #
8
+ # Examples
9
+ #
10
+ # ws = WebSocketClient.new "ws://localhost/chat"
11
+ #
12
+ # ws.onmessage do |msg|
13
+ # puts msg
14
+ # end
15
+ #
16
+ # ws.onopen do
17
+ # ws.send_message "Hello!"
18
+ # end
19
+ class WebSocketClient
20
+
21
+ Version = "0.2.0"
22
+
23
+ class WebSocketError < StandardError; end
24
+
25
+ # Internal: Wrapper
26
+ class WebSocketConnection < EM::Connection
27
+
28
+ def client=(client)
29
+ @client = client
30
+ @client.socket = self
31
+ end
32
+
33
+ def receive_data(data)
34
+ @client.receive_data data
35
+ end
36
+
37
+ def unbind(reason=nil)
38
+ @client.unbind
39
+ end
40
+
41
+ end
42
+
43
+ include WebSocketCodec::Protocol
44
+
45
+ attr_accessor :socket
46
+
47
+ # Public: Initialize a WebSocket client
48
+ #
49
+ # uri - The endpoint host url
50
+ # origin - The origin you wish to claim
51
+ #
52
+ # Examples
53
+ #
54
+ # WebSocketClient.new "ws://localhost:9000/chat"
55
+ # WebSocketClient.new "ws://ws.site.com/chat", "http://www.site.com/chat"
56
+ #
57
+ # Returns the client
58
+ def initialize uri, origin="em-ws-client"
59
+ super();
60
+
61
+ @uri = URI.parse(uri)
62
+ @origin = origin
63
+ @buffer = ""
64
+
65
+ @encoder = WebSocketCodec::Encoder.new
66
+ @decoder = WebSocketCodec::Decoder.new
67
+ @handshake = WebSocketCodec::Handshake.new @uri, @origin
68
+
69
+ @callbacks = {}
70
+ @closing = false
71
+
72
+ connect
73
+ end
74
+
75
+ # Public: Close the connection
76
+ #
77
+ # Examples
78
+ #
79
+ # ws.unbind
80
+ # # => ?
81
+ #
82
+ # Returns
83
+ def unbind
84
+ emit :close
85
+ end
86
+
87
+ # Bind a callback to the open event
88
+ #
89
+ # block - A block which is called when
90
+ # the connection to the remote host is established
91
+ #
92
+ # Examples
93
+ #
94
+ # ws.onopen do
95
+ # end
96
+ #
97
+ # Returns nothing
98
+ def onopen &block
99
+ @callbacks[:open] = block
100
+ end
101
+
102
+ # Bind a callback to the close event
103
+ #
104
+ # block - A block which is called when
105
+ # the connection to the remote host is closed.
106
+ # Your block receives 2 arguments, with the second
107
+ # potentially being nil.
108
+ #
109
+ # Examples
110
+ #
111
+ # ws.onclose do |code, explain|
112
+ # end
113
+ #
114
+ # Returns nothing
115
+ def onclose &block
116
+ @callbacks[:close] = block
117
+ end
118
+
119
+ # Bind a callback to the message event
120
+ #
121
+ # block - A block which is called when a
122
+ # message is received. The first argument
123
+ # for the block is the message, and the second
124
+ # argument is a binary flag.
125
+ #
126
+ # Examples
127
+ #
128
+ # ws.onmessage do |message, binary|
129
+ # end
130
+ #
131
+ # Returns nothing
132
+ def onmessage &block
133
+ @callbacks[:frame] = block
134
+ end
135
+
136
+ # Bind a callback to the error event
137
+ #
138
+ # block - A block which is called when
139
+ # an error occurs. The connection is dropped
140
+ # immediately per spec. The first argument is
141
+ # the close code, and the second is the error.
142
+ #
143
+ # Examples
144
+ #
145
+ # ws.onerror do |close_code, error|
146
+ # end
147
+ #
148
+ # Returns nothing
149
+ def onerror &block
150
+ @callbacks[:error] = block
151
+ end
152
+
153
+ # Bind a callback to the ping event
154
+ #
155
+ # block - A block which is called when
156
+ # the remote host sends a ping. A single
157
+ # argument is sent, which contains the ping
158
+ # data sent from the remote host. A pong
159
+ # is automatically sent.
160
+ #
161
+ # Examples
162
+ #
163
+ # ws.onping do |data|
164
+ # end
165
+ #
166
+ # Returns nothing
167
+ def onping &block
168
+ @callbacks[:ping] = block
169
+ end
170
+
171
+ # Bind a callback to the pong event
172
+ #
173
+ # block - A block which is called when
174
+ # the remote host sends a pong in response
175
+ # to your ping. It's possible to get unwarrented
176
+ # pongs.
177
+ #
178
+ # Examples
179
+ #
180
+ # ws.onpong do |data|
181
+ # end
182
+ #
183
+ # Returns nothing
184
+ def onpong &block
185
+ @callbacks[:pong] = block
186
+ end
187
+
188
+ # Internal: called by eventmachine when data is
189
+ # received
190
+ def receive_data(data)
191
+ if @handshake.complete?
192
+ receive_message_data data
193
+ else
194
+ receive_handshake_data data
195
+ end
196
+ end
197
+
198
+
199
+ # Send a message to the remote host
200
+ #
201
+ # data - The string contents of your message
202
+ #
203
+ # Examples
204
+ #
205
+ # ws.onping do |data|
206
+ # end
207
+ #
208
+ # Returns nothing
209
+ def send_message data, binary=false
210
+ if established?
211
+ unless @closing
212
+ @socket.send_data(@encoder.encode(data.to_s, binary ? BINARY_FRAME : TEXT_FRAME))
213
+ end
214
+ else
215
+ raise WebSocketError.new "can't send on a closed channel"
216
+ end
217
+ end
218
+
219
+ def close code=1000, msg=nil
220
+ @closing = true
221
+ @socket.send_data @encoder.close(code, msg)
222
+ @socket.close_connection_after_writing
223
+ end
224
+
225
+ private
226
+
227
+ # Internal: is the handshake complete and valid?
228
+ def established?
229
+ @handshake.complete? && @handshake.valid?
230
+ end
231
+
232
+ # Internal: process ws data
233
+ def receive_message_data data
234
+ @decoder << data
235
+ end
236
+
237
+ # Internal: process handshake data
238
+ def receive_handshake_data data
239
+ @handshake << data
240
+ if @handshake.complete?
241
+ if @handshake.valid?
242
+ on_handshake_complete
243
+ else
244
+ emit :error, 1, "Handshake failed!"
245
+ @socket.unbind
246
+ end
247
+ end
248
+ end
249
+
250
+ # Internal: setup encoder/decoder and bind
251
+ # to all decoder events.
252
+ def on_handshake_complete
253
+
254
+ @decoder.onping do |data|
255
+ @socket.send_data @encoder.pong(data)
256
+ emit :ping, data
257
+ end
258
+
259
+ @decoder.onpong do |data|
260
+ emit :pong, data
261
+ end
262
+
263
+ @decoder.onclose do |code|
264
+ close code
265
+ end
266
+
267
+ @decoder.onframe do |frame, binary|
268
+ emit :frame, frame, binary
269
+ end
270
+
271
+ @decoder.onerror do |code, message|
272
+ close code, message
273
+ emit :error, code, message
274
+ end
275
+
276
+ emit :open
277
+
278
+ if @handshake.extra
279
+ receive_message_data @handshake.extra
280
+ end
281
+ end
282
+
283
+ # Internal: Connect to the remote host and synchonize the socket
284
+ # and this client object
285
+ def connect
286
+ EM.connect @uri.host, @uri.port || 80, WebSocketConnection do |conn|
287
+ conn.client = self
288
+ conn.send_data(@handshake.request)
289
+ end
290
+ end
291
+
292
+ # Internal: Emit an event
293
+ def emit event, *args
294
+ if @callbacks.key?(event)
295
+ @callbacks[event].call(*args)
296
+ end
297
+ end
298
+
299
+ end
300
+ end