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 ADDED
@@ -0,0 +1 @@
1
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # A sample Gemfile
2
+ source "http://rubygems.org"
3
+
4
+ gem "eventmachine"
5
+ gem "state_machine", "= 1.0.0"
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