em-ws-client 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.
- data/.gitignore +1 -0
- data/Gemfile +5 -0
- data/README.markdown +40 -0
- data/Rakefile +1 -0
- data/em-ws-client.gemspec +19 -0
- data/example/echo.rb +33 -0
- data/lib/codec/draft10decoder.rb +122 -0
- data/lib/codec/draft10encoder.rb +45 -0
- data/lib/em-websocket-client.rb +196 -0
- data/spec/em-ws-client.rb +0 -0
- metadata +77 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Gemfile.lock
|
data/Gemfile
ADDED
data/README.markdown
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# EM WebSocket Client
|
2
|
+
Mimics the browser client implementation to some degree. Currently implements hixie draft 10.
|
3
|
+
|
4
|
+
# Installing
|
5
|
+
```bash
|
6
|
+
$ gem install em-ws-client
|
7
|
+
```
|
8
|
+
|
9
|
+
# Establishing a Connection
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
EM.run do
|
13
|
+
|
14
|
+
# Establish the connection
|
15
|
+
connection = EM::WebSocketClient.new("ws://server/path")
|
16
|
+
|
17
|
+
connection.onopen do
|
18
|
+
# Handle open event
|
19
|
+
end
|
20
|
+
|
21
|
+
conn.onclose do
|
22
|
+
# Handle close event
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
```
|
27
|
+
|
28
|
+
# Sending Data
|
29
|
+
Send data as a string. It can be JSON, CSV, etc. As long
|
30
|
+
as you can serialize it.
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
connection.send_data "message"
|
34
|
+
|
35
|
+
# JSON
|
36
|
+
connection.send_data {
|
37
|
+
:category => "fun",
|
38
|
+
:message => "times"
|
39
|
+
}.to_json
|
40
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
$: << File.dirname(__FILE__) + "/lib"
|
@@ -0,0 +1,19 @@
|
|
1
|
+
spec = Gem::Specification.new do |s|
|
2
|
+
s.name = "em-ws-client"
|
3
|
+
s.version = "0.1"
|
4
|
+
s.date = "2011-09-26"
|
5
|
+
s.summary = "EventMachine WebSocket Client"
|
6
|
+
s.email = "dan@shove.io"
|
7
|
+
s.homepage = "https://github.com/dansimpson/em-ws-client"
|
8
|
+
s.description = "A simple, fun, evented WebSocket client for your ruby projects"
|
9
|
+
s.has_rdoc = true
|
10
|
+
|
11
|
+
s.add_dependency("eventmachine", "= 0.12.10")
|
12
|
+
s.add_dependency("state_machine", "= 1.0.2")
|
13
|
+
|
14
|
+
s.authors = ["Dan Simpson"]
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
|
19
|
+
end
|
data/example/echo.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
$: << File.dirname(__FILE__) + "/../lib/"
|
2
|
+
|
3
|
+
require "em-websocket-client"
|
4
|
+
|
5
|
+
|
6
|
+
dec = EM::Draft10Decoder.new
|
7
|
+
enc = EM::Draft10Encoder.new
|
8
|
+
|
9
|
+
puts dec.decode(enc.encode("monkey"))
|
10
|
+
|
11
|
+
EM.run do
|
12
|
+
|
13
|
+
conn = EM::WebSocketClient.new("ws://localhost:8080/test")
|
14
|
+
puts conn.state
|
15
|
+
puts conn.disconnected?
|
16
|
+
|
17
|
+
conn.onopen do
|
18
|
+
puts "Opened"
|
19
|
+
|
20
|
+
EM.add_periodic_timer(2) do
|
21
|
+
conn.send_data "Ping!"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
conn.onclose do
|
26
|
+
puts "Closed"
|
27
|
+
end
|
28
|
+
|
29
|
+
conn.onmessage do |msg|
|
30
|
+
puts msg
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module EM
|
2
|
+
|
3
|
+
# A replaying decoder for the IETF Hybi
|
4
|
+
# WebSocket protocol specification
|
5
|
+
class Draft10Decoder
|
6
|
+
|
7
|
+
CONTINUATION = 0x0
|
8
|
+
TEXT_FRAME = 0x1
|
9
|
+
BINARY_FRAME = 0x2
|
10
|
+
CLOSE = 0x8
|
11
|
+
PING = 0x9
|
12
|
+
PONG = 0xA
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@buffer = ""
|
16
|
+
@chunks = ""
|
17
|
+
end
|
18
|
+
|
19
|
+
# Decode a WebSocket frame
|
20
|
+
# +data+ the frame data
|
21
|
+
# returns false if the packet is incomplete
|
22
|
+
# and a decoded message otherwise
|
23
|
+
def decode data
|
24
|
+
|
25
|
+
# broken frame
|
26
|
+
if data && data.length < 2
|
27
|
+
return false
|
28
|
+
end
|
29
|
+
|
30
|
+
# put the data into the buffer, as
|
31
|
+
# we might be replaying
|
32
|
+
@buffer << data
|
33
|
+
|
34
|
+
# decode the first 2 bytes, with
|
35
|
+
# opcode, lengthgth, masking bit, and frag bit
|
36
|
+
h1, h2 = @buffer.unpack("CC")
|
37
|
+
|
38
|
+
# check the fragmentation bit to see
|
39
|
+
# if this is a message fragment
|
40
|
+
@chunked = ((h1 & 0x80) != 0x80)
|
41
|
+
|
42
|
+
# used to keep track of our position in the buffer
|
43
|
+
offset = 2
|
44
|
+
|
45
|
+
# see above for possible opcodes
|
46
|
+
opcode = (h1 & 0x0F)
|
47
|
+
|
48
|
+
# the leading length idicator
|
49
|
+
length = (h2 & 0x7F)
|
50
|
+
|
51
|
+
# masking bit, is the data masked with
|
52
|
+
# a specified masking key?
|
53
|
+
masked = ((h2 & 0x80) == 0x80)
|
54
|
+
|
55
|
+
# spare no bytes hybi!
|
56
|
+
if length > 125
|
57
|
+
if length == 126
|
58
|
+
length = @buffer.unpack("@#{offset}n").first
|
59
|
+
offset += 2
|
60
|
+
else
|
61
|
+
length1, length2 = @buffer.unpack("@#{offset}NN")
|
62
|
+
# TODO.. bigint?
|
63
|
+
offset += 8
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# unpack the masking key
|
68
|
+
if masked
|
69
|
+
key = @buffer.unpack("@#{offset}N").first
|
70
|
+
offset += 4
|
71
|
+
end
|
72
|
+
|
73
|
+
# replay on next frame
|
74
|
+
if @buffer.size < (length + offset)
|
75
|
+
return false
|
76
|
+
end
|
77
|
+
|
78
|
+
# Read the important bits
|
79
|
+
payload = @buffer.unpack("@#{offset}C*")
|
80
|
+
|
81
|
+
# Unmask the data if it's masked
|
82
|
+
if masked
|
83
|
+
payload.size.times do |i|
|
84
|
+
payload[i] = ((payload[i] ^ (key >> ((3 - (i % 4)) * 8))) & 0xFF)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# finally, extract the message!
|
89
|
+
payload = payload.pack("C*")
|
90
|
+
|
91
|
+
case opcode
|
92
|
+
when CONTINUATION
|
93
|
+
@chunks << payload
|
94
|
+
unless @chunked
|
95
|
+
result = @chunks
|
96
|
+
@chunks = ""
|
97
|
+
return result
|
98
|
+
end
|
99
|
+
false
|
100
|
+
when TEXT_FRAME
|
101
|
+
unless @chunked
|
102
|
+
@buffer = ""
|
103
|
+
#@buffer.slice!(offset + length, -1)
|
104
|
+
return payload
|
105
|
+
end
|
106
|
+
false
|
107
|
+
when BINARY_FRAME
|
108
|
+
false #TODO
|
109
|
+
when CLOSE
|
110
|
+
false #TODO
|
111
|
+
when PING
|
112
|
+
false #TODO send pong
|
113
|
+
when PONG
|
114
|
+
false
|
115
|
+
else
|
116
|
+
false
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module EM
|
2
|
+
class Draft10Encoder
|
3
|
+
|
4
|
+
# Encode a standard payload to a hybi10
|
5
|
+
# WebSocket frame
|
6
|
+
def encode data
|
7
|
+
frame = []
|
8
|
+
frame << (0x1 | 0x80)
|
9
|
+
|
10
|
+
packr = "CC"
|
11
|
+
|
12
|
+
# append frame length and mask bit 0x80
|
13
|
+
len = data.size
|
14
|
+
if len <= 125
|
15
|
+
frame << (len | 0x80)
|
16
|
+
elsif length < 65536
|
17
|
+
frame << (126 | 0x80)
|
18
|
+
frame << (len)
|
19
|
+
packr << "n"
|
20
|
+
else
|
21
|
+
frame << (127 | 0x80)
|
22
|
+
frame << (len >> 32)
|
23
|
+
frame << (len & 0xFFFFFFFF)
|
24
|
+
packr << "NN"
|
25
|
+
end
|
26
|
+
|
27
|
+
# generate a masking key
|
28
|
+
key = rand(2 ** 31)
|
29
|
+
|
30
|
+
# mask each byte with the key
|
31
|
+
frame << key
|
32
|
+
packr << "N"
|
33
|
+
|
34
|
+
# The spec says we have to waste cycles and
|
35
|
+
# impact the atmosphere with a small amount of
|
36
|
+
# heat dissapation
|
37
|
+
data.size.times do |i|
|
38
|
+
frame << ((data.getbyte(i) ^ (key >> ((3 - (i % 4)) * 8))) & 0xFF)
|
39
|
+
end
|
40
|
+
|
41
|
+
frame.pack("#{packr}C*")
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "eventmachine"
|
3
|
+
require "state_machine"
|
4
|
+
require "uri"
|
5
|
+
require "digest/sha1"
|
6
|
+
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"
|
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
|
188
|
+
|
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
|
+
|
195
|
+
end
|
196
|
+
end
|
File without changes
|
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: em-ws-client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Dan Simpson
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-09-26 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: eventmachine
|
16
|
+
requirement: &70353611766080 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - =
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.12.10
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70353611766080
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: state_machine
|
27
|
+
requirement: &70353611765580 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - =
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.0.2
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70353611765580
|
36
|
+
description: A simple, fun, evented WebSocket client for your ruby projects
|
37
|
+
email: dan@shove.io
|
38
|
+
executables: []
|
39
|
+
extensions: []
|
40
|
+
extra_rdoc_files: []
|
41
|
+
files:
|
42
|
+
- .gitignore
|
43
|
+
- Gemfile
|
44
|
+
- README.markdown
|
45
|
+
- Rakefile
|
46
|
+
- em-ws-client.gemspec
|
47
|
+
- example/echo.rb
|
48
|
+
- lib/codec/draft10decoder.rb
|
49
|
+
- lib/codec/draft10encoder.rb
|
50
|
+
- lib/em-websocket-client.rb
|
51
|
+
- spec/em-ws-client.rb
|
52
|
+
homepage: https://github.com/dansimpson/em-ws-client
|
53
|
+
licenses: []
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options: []
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
none: false
|
60
|
+
requirements:
|
61
|
+
- - ! '>='
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0'
|
64
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
requirements: []
|
71
|
+
rubyforge_project:
|
72
|
+
rubygems_version: 1.8.6
|
73
|
+
signing_key:
|
74
|
+
specification_version: 3
|
75
|
+
summary: EventMachine WebSocket Client
|
76
|
+
test_files:
|
77
|
+
- spec/em-ws-client.rb
|