net-ws 0.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,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rbenv-version ADDED
@@ -0,0 +1 @@
1
+ 1.9.3-p194
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in net-ws.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Eric Wollesen
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # Net::Ws
2
+
3
+ A ruby websocket client built on top of ruby's Net::HTTP.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'net-ws'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install net-ws
18
+
19
+ ## Usage
20
+
21
+ ws = Net::WS.new("ws://localhost:9000")
22
+ ws.open
23
+ ws.ping
24
+ puts ws.send_text("Hello, World!")
25
+
26
+ ## Contributing
27
+
28
+ 1. Fork it
29
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
30
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
31
+ 4. Push to the branch (`git push origin my-new-feature`)
32
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,5 @@
1
+ module Net
2
+ module Ws
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
data/lib/net/ws.rb ADDED
@@ -0,0 +1,355 @@
1
+ require "net/http"
2
+ require "uri"
3
+ require "base64"
4
+ require "digest/sha1"
5
+
6
+
7
+ module URI
8
+ class WS < HTTP; end
9
+
10
+ @@schemes["WS"] = WS
11
+ @@schemes["WSS"] = WS
12
+ end
13
+
14
+
15
+ module Net
16
+
17
+ class WS < HTTP
18
+ class Error < StandardError ; end
19
+
20
+ FIN_FALSE = 0
21
+ FIN_TRUE = 1
22
+ HEADER_ACCEPT = "Sec-WebSocket-Accept".freeze
23
+ HEADER_CONNECTION = "Connection".freeze
24
+ HEADER_CONNECTION_VALUE = "Upgrade".freeze
25
+ HEADER_EXTENSIONS = "Sec-WebSocket-Extensions".freeze
26
+ HEADER_KEY = "Sec-WebSocket-Key".freeze
27
+ HEADER_SUBPROTOCOL = "Sec-WebSocket-Protocol".freeze
28
+ HEADER_UPGRADE = "Upgrade".freeze
29
+ HEADER_UPGRADE_VALUE = "websocket".freeze
30
+ HEADER_VERSION = "Sec-WebSocket-Version".freeze
31
+ HEADER_VERSION_VALUE = "13".freeze
32
+ LENGTH_IS_16BIT = 126
33
+ LENGTH_IS_64BIT = 127
34
+ OPCODE_CLOSE = 0x08
35
+ OPCODE_PING = 0x09
36
+ OPCODE_PONG = 0x0A
37
+ OPCODE_TEXT = 0x01
38
+ SEC_WEBSOCKET_KEY_LEN = 16
39
+ SEC_WEBSOCKET_SUFFIX = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11".freeze
40
+ STATE_CLOSING = "state_closing".freeze
41
+ STATE_CONNECTED = "state_connected".freeze
42
+ STATE_CONNECTING = "state_connecting".freeze
43
+ STATE_FINISHED = "state_finished".freeze
44
+ UNSIGNED_16BIT_MAX = (2**16) - 1
45
+ UNSIGNED_64BIT_MAX = (2**64) - 1
46
+ WEBSOCKET_SCHEME_WS = "ws".freeze
47
+ WEBSOCKET_SCHEME_WSS = "wss".freeze
48
+ WEBSOCKET_SCHEMES = [WEBSOCKET_SCHEME_WS, WEBSOCKET_SCHEME_WSS].freeze
49
+
50
+ def initialize(uri, options=nil)
51
+ options ||= {}
52
+ @uri = URI(uri)
53
+ @on_message = options[:on_message]
54
+ @subprotocols = options[:subprotocols].is_a?(Array) ? \
55
+ options[:subprotocols] : \
56
+ [options[:subprocotols]]
57
+ @connection_state = nil
58
+
59
+ super(@uri.host, @uri.port)
60
+ end
61
+
62
+ def to_io
63
+ @socket.io
64
+ end
65
+
66
+ def open(request_uri=nil)
67
+ return false if STATE_CONNECTING == @connection_state
68
+
69
+ @connection_state = STATE_CONNECTING
70
+ @request_uri = request_uri || @uri.request_uri || "/"
71
+
72
+ perform_opening_handshake.tap do |successful|
73
+ @connection_state = STATE_CONNECTED if successful
74
+ end
75
+ end
76
+
77
+ def ping(message=nil)
78
+ send_frame(FIN_TRUE, 0, OPCODE_PING, message)
79
+ receive_frame
80
+ end
81
+
82
+ def close
83
+ _send_close
84
+ Timeout.timeout(10) {receive_frame}
85
+ rescue Timeout::Error
86
+ finish
87
+ end
88
+
89
+ def send_text(data)
90
+ # TODO fragmentation
91
+ send_frame(FIN_TRUE, 0, OPCODE_TEXT, data)
92
+ end
93
+
94
+ def receive_message
95
+ # TODO fragmentation
96
+ receive_frame
97
+ end
98
+
99
+
100
+ protected
101
+
102
+ def finish
103
+ super
104
+ @connection_state = STATE_FINISHED
105
+ end
106
+
107
+ def _send_close
108
+ send_frame(FIN_TRUE, 0, OPCODE_CLOSE)
109
+ @connection_state = STATE_CLOSING
110
+ end
111
+
112
+ def receive_frame
113
+ header = @socket.read(2).unpack("n").first
114
+ fin = (header >> 15) & 0x1
115
+ rsv = (header >> 12) & 0x7
116
+ opcode = (header >> 8) & 0xF
117
+ masked = ((header >> 7) & 0x1) > 0
118
+ length = extract_length(header)
119
+
120
+ raise NotImplementedError, "Fragmentation is not supported" unless fin
121
+
122
+ extract_payload(masked, length).tap do |payload|
123
+ if control_frame?(opcode)
124
+ handle_control_frame(opcode, payload)
125
+ else
126
+ @on_message.call(payload) if @on_message
127
+ end
128
+ end
129
+ end
130
+
131
+ def extract_payload(masked, length)
132
+ if masked
133
+ extract_masked_payload(length)
134
+ else
135
+ extract_unmasked_payload(length)
136
+ end
137
+ end
138
+
139
+ def extract_masked_payload(length)
140
+ $stderr.puts("Warning: Masked server response received. " +
141
+ "This should never happen.")
142
+ masking_key = @socket.read(4).unpack("CCCC")
143
+
144
+ unmask(masking_key, extract_unmasked_payload(length))
145
+ end
146
+
147
+ def extract_unmasked_payload(length)
148
+ @socket.read(length)
149
+ end
150
+
151
+ # Control frames are identified by opcodes where the most significant bit
152
+ # of the opcode is 1.
153
+ # -- http://tools.ietf.org/html/rfc6455#section-5.5
154
+ #
155
+ # Since there are presently 4 bits used for the opcode, the most
156
+ # significant bit is the "8" bit.
157
+ def control_frame?(opcode)
158
+ (opcode & 0x8) > 0
159
+ end
160
+
161
+ def extract_length(header)
162
+ length_code = header & 0x7f
163
+
164
+ case length_code
165
+ when LENGTH_IS_16BIT
166
+ @socket.read(2).unpack("n").first
167
+ when LENGTH_IS_64BIT
168
+ @socket.read(8).unpack("NN").inject(0) do |sum, int|
169
+ (sum << 32) + int
170
+ end
171
+ else
172
+ length_code
173
+ end
174
+ end
175
+
176
+ def handle_control_frame(opcode, payload=nil)
177
+ case opcode
178
+ when OPCODE_CLOSE
179
+ _send_close unless STATE_CLOSING == @connection_state
180
+ finish
181
+ when OPCODE_PING
182
+ pong(payload)
183
+ when OPCODE_PONG
184
+ payload
185
+ else
186
+ fail_websocket_connection("Unhandled opcode: #{opcode.inspect}")
187
+ end
188
+ end
189
+
190
+ def pong(payload)
191
+ send_frame(FIN_TRUE, 0, OPCODE_PONG, payload)
192
+ end
193
+
194
+ def send_frame(fin, rsv, opcode, payload=nil)
195
+ send_frame_header(fin, rsv, opcode)
196
+ send_mask_flag_and_payload_size(payload)
197
+ masking_key = send_masking_key
198
+ send_frame_payload(payload, masking_key)
199
+ end
200
+
201
+ def send_frame_header(fin, rsv, opcode)
202
+ bytes = []
203
+
204
+ bytes << (fin << 7) + (rsv << 4) + opcode
205
+
206
+ @socket.write(bytes.pack("C*"))
207
+ end
208
+
209
+ def send_mask_flag_and_payload_size(payload)
210
+ mask_flag = (1 << 7)
211
+
212
+ if payload.nil?
213
+ @socket.write [mask_flag].pack("C")
214
+ elsif payload.size < LENGTH_IS_16BIT
215
+ @socket.write [mask_flag + payload.size].pack("C")
216
+ elsif payload.size <= UNSIGNED_16BIT_MAX
217
+ @socket.write [mask_flag + 126].pack("C")
218
+ @socket.write [payload.size].pack("n*")
219
+ elsif payload.size <= UNSIGNED_64BIT_MAX
220
+ @socket.write [mask_flag + 127, payload.size].pack("C")
221
+ @socket.write [payload.size].pack("n*")
222
+ else
223
+ raise Error, "Unhandled payload size: #{payload.size.inspect}"
224
+ end
225
+ end
226
+
227
+ def send_masking_key
228
+ generate_masking_key.tap do |key|
229
+ @socket.write key.pack("CCCC")
230
+ end
231
+ end
232
+
233
+ def send_frame_payload(payload, masking_key)
234
+ return unless payload
235
+
236
+ # FIXME we only support text for now
237
+ @socket.write(mask(payload.unpack("U*"), masking_key).pack("C*"))
238
+ end
239
+
240
+ def mask(payload, key)
241
+ i = 0
242
+
243
+ payload.map do |octet|
244
+ masked = octet ^ key[i % 4]
245
+ i += 1
246
+ masked
247
+ end
248
+ end
249
+
250
+ def generate_masking_key
251
+ val = rand(2**32)
252
+ [val >> 24, (val >> 16) & 0x0f, (val >> 8) & 0x0f, val & 0x0f]
253
+ end
254
+
255
+ def perform_opening_handshake
256
+ unless STATE_CONNECTING == @connection_state
257
+ raise Error, "Connection state error"
258
+ end
259
+
260
+ response = send_opening_handshake
261
+ handle_opening_handshake_response(response)
262
+ end
263
+
264
+ def send_opening_handshake
265
+ headers = {
266
+ HEADER_UPGRADE => HEADER_UPGRADE_VALUE,
267
+ HEADER_CONNECTION => HEADER_CONNECTION_VALUE,
268
+ HEADER_KEY => generate_sec_websocket_key,
269
+ HEADER_VERSION => HEADER_VERSION_VALUE,
270
+ HEADER_SUBPROTOCOL => @subprotocols.join(","),
271
+ }
272
+
273
+ get = Net::HTTP::Get.new(@request_uri, headers)
274
+
275
+ start
276
+ request(get)
277
+ end
278
+
279
+ def generate_sec_websocket_key
280
+ c = []
281
+
282
+ SEC_WEBSOCKET_KEY_LEN.times {c << "%c" % [rand(127)]}
283
+
284
+ @sec_websocket_key = Base64.encode64(c.join("")).strip
285
+ end
286
+
287
+ def handle_opening_handshake_response(response)
288
+ case response.code
289
+ when "101"
290
+ validate_opening_handshake_response(response)
291
+ else
292
+ raise Error, "Unhandled opening handshake response #{response.inspect}"
293
+ end
294
+ end
295
+
296
+ def validate_opening_handshake_response(response)
297
+ unless valid_upgrade_header?(response[HEADER_UPGRADE])
298
+ fail_websocket_connection("Upgrade header")
299
+ end
300
+
301
+ unless valid_connection_header?(response[HEADER_CONNECTION])
302
+ fail_websocket_connection("Connection header")
303
+ end
304
+
305
+ unless valid_sec_websocket_accept_header?(response[HEADER_ACCEPT])
306
+ fail_websocket_connection("Sec-WebSocket-Accept header")
307
+ end
308
+
309
+ unless valid_sec_websocket_extensions_header?(response[HEADER_EXTENSIONS])
310
+ fail_websocket_connection("Sec-WebSocket-Extensions header")
311
+ end
312
+
313
+ unless valid_sec_websocket_subprotocol_header?(response[HEADER_SUBPROTOCOL])
314
+ fail_websocket_connection("Sec-WebSocket-Subprotocol header")
315
+ end
316
+
317
+ true
318
+ end
319
+
320
+ def valid_upgrade_header?(upgrade_header="")
321
+ /websocket/i === upgrade_header
322
+ end
323
+
324
+ def fail_websocket_connection(reason="Unknown reason")
325
+ raise Error, "Fail websocket connection: #{reason.inspect}."
326
+ end
327
+
328
+ def valid_connection_header?(connection_header="")
329
+ /upgrade/i === connection_header
330
+ end
331
+
332
+ def valid_sec_websocket_accept_header?(header_value)
333
+ header_value &&
334
+ expected_sec_websocket_accept_header == header_value.strip
335
+ end
336
+
337
+ def expected_sec_websocket_accept_header
338
+ payload = @sec_websocket_key + SEC_WEBSOCKET_SUFFIX
339
+
340
+ Base64.encode64(Digest::SHA1.digest(payload)).strip
341
+ end
342
+
343
+ def valid_sec_websocket_extensions_header?(header_value="")
344
+ nil_or_empty?(header_value)
345
+ end
346
+
347
+ def nil_or_empty?(value)
348
+ value.nil? || value.strip.empty?
349
+ end
350
+
351
+ def valid_sec_websocket_subprotocol_header?(header_value="")
352
+ nil_or_empty?(header_value)
353
+ end
354
+ end
355
+ end
data/net-ws.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/net/ws/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Eric Wollesen"]
6
+ gem.email = ["ericw@xmtp.net"]
7
+ gem.description = %q{A websocket client built on top of Net::HTTP.}
8
+ gem.summary = %q{A websocket client built on top of Net::HTTP.}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "net-ws"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Net::Ws::VERSION
17
+
18
+ gem.add_development_dependency("rake")
19
+ gem.add_development_dependency("pry")
20
+ gem.add_development_dependency("minitest")
21
+ gem.add_development_dependency("minitest-spec")
22
+ gem.add_development_dependency("minitest-rg")
23
+
24
+ end
@@ -0,0 +1,3 @@
1
+ require "net/ws"
2
+ require "minitest/autorun"
3
+ require "minitest/rg"
@@ -0,0 +1,9 @@
1
+ require File.expand_path("../test_helper", File.dirname(__FILE__))
2
+
3
+ describe Net::WS do
4
+
5
+ it "should be sane" do
6
+ true.must_equal true
7
+ end
8
+
9
+ end
metadata ADDED
@@ -0,0 +1,144 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: net-ws
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Eric Wollesen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-18 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: pry
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: minitest
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: minitest-spec
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: minitest-rg
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ description: A websocket client built on top of Net::HTTP.
95
+ email:
96
+ - ericw@xmtp.net
97
+ executables: []
98
+ extensions: []
99
+ extra_rdoc_files: []
100
+ files:
101
+ - .gitignore
102
+ - .rbenv-version
103
+ - Gemfile
104
+ - LICENSE
105
+ - README.md
106
+ - Rakefile
107
+ - lib/net/ws.rb
108
+ - lib/net/ws/version.rb
109
+ - net-ws.gemspec
110
+ - test/test_helper.rb
111
+ - test/unit/ws_spec.rb
112
+ homepage: ''
113
+ licenses: []
114
+ post_install_message:
115
+ rdoc_options: []
116
+ require_paths:
117
+ - lib
118
+ required_ruby_version: !ruby/object:Gem::Requirement
119
+ none: false
120
+ requirements:
121
+ - - ! '>='
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ segments:
125
+ - 0
126
+ hash: -3324455753849074669
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
128
+ none: false
129
+ requirements:
130
+ - - ! '>='
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ segments:
134
+ - 0
135
+ hash: -3324455753849074669
136
+ requirements: []
137
+ rubyforge_project:
138
+ rubygems_version: 1.8.23
139
+ signing_key:
140
+ specification_version: 3
141
+ summary: A websocket client built on top of Net::HTTP.
142
+ test_files:
143
+ - test/test_helper.rb
144
+ - test/unit/ws_spec.rb