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.
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.markdown +86 -20
- data/Rakefile +9 -1
- data/autobahn/fuzzer.rb +94 -0
- data/autobahn/report.html +3615 -0
- data/em-ws-client.gemspec +6 -3
- data/lib/em-ws-client.rb +10 -190
- data/lib/em-ws-client/client.rb +300 -0
- data/lib/em-ws-client/decoder.rb +238 -0
- data/lib/em-ws-client/encoder.rb +74 -0
- data/lib/em-ws-client/handshake.rb +97 -0
- data/lib/em-ws-client/protocol.rb +12 -0
- data/spec/codec_spec.rb +15 -0
- data/spec/handshake_spec.rb +96 -0
- data/spec/helper.rb +3 -0
- metadata +16 -20
- data/example/echo.rb +0 -33
- data/lib/codec/draft10decoder.rb +0 -122
- data/lib/codec/draft10encoder.rb +0 -45
- data/spec/em-ws-client.rb +0 -0
data/em-ws-client.gemspec
CHANGED
@@ -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 =
|
4
|
-
s.date = "
|
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
|
|
data/lib/em-ws-client.rb
CHANGED
@@ -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 "
|
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
|
-
|
190
|
-
|
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
|