em-ws-client 0.1

Sign up to get free protection for your applications and to get access to all the features.
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