simpleblockingwebsocketclient 0.42

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
+ exp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in simpleblockingwebsocketclient.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # HTML5 WebSocket client implementation in Ruby.
2
+
3
+ __For server and non-blocking client, em-websocket ( https://github.com/igrigorik/em-websocket ) may be a better choice, especially if you want to use EventMachine.__
4
+
5
+ ## Usage
6
+
7
+ Connects to Web Socket server at host example.com port 10081.
8
+ `client = WebSocket.new("ws://example.com:10081/") { |msg| puts message}`
9
+ The block specifies what should be done with received messages. In this case they are simply printed to stdout.
10
+
11
+ For sending data, use send():
12
+ `client.send("Hello")`
13
+
14
+ See the samples/ directory for actual example code.
15
+
16
+ ## Supported WebSocket protocol versions
17
+
18
+ WebSocket client speaks version hixie-76.
19
+
20
+ ## License
21
+
22
+ New BSD License.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,405 @@
1
+ require "base64"
2
+ require "socket"
3
+ require "uri"
4
+ require "digest/md5"
5
+ require "digest/sha1"
6
+ require "openssl"
7
+ require "stringio"
8
+
9
+
10
+ class WebSocket
11
+
12
+ class << self
13
+
14
+ attr_accessor(:debug)
15
+
16
+ end
17
+
18
+ class Error < RuntimeError
19
+
20
+ end
21
+
22
+ WEB_SOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
23
+ OPCODE_CONTINUATION = 0x00
24
+ OPCODE_TEXT = 0x01
25
+ OPCODE_BINARY = 0x02
26
+ OPCODE_CLOSE = 0x08
27
+ OPCODE_PING = 0x09
28
+ OPCODE_PONG = 0x0a
29
+
30
+ def initialize(arg, params = {})
31
+
32
+ @web_socket_version = "hixie-76"
33
+ uri = arg.is_a?(String) ? URI.parse(arg) : arg
34
+
35
+ if uri.scheme == "ws"
36
+ default_port = 80
37
+ elsif uri.scheme = "wss"
38
+ default_port = 443
39
+ else
40
+ raise(WebSocket::Error, "unsupported scheme: #{uri.scheme}")
41
+ end
42
+
43
+ @path = (uri.path.empty? ? "/" : uri.path) + (uri.query ? "?" + uri.query : "")
44
+ host = uri.host + ((!uri.port || uri.port == default_port) ? "" : ":#{uri.port}")
45
+ origin = params[:origin] || "http://#{uri.host}"
46
+ key1 = generate_key()
47
+ key2 = generate_key()
48
+ key3 = generate_key3()
49
+
50
+ socket = TCPSocket.new(uri.host, uri.port || default_port)
51
+
52
+ if uri.scheme == "ws"
53
+ @socket = socket
54
+ else
55
+ @socket = ssl_handshake(socket)
56
+ end
57
+
58
+ write(
59
+ "GET #{@path} HTTP/1.1\r\n" +
60
+ "Upgrade: WebSocket\r\n" +
61
+ "Connection: Upgrade\r\n" +
62
+ "Host: #{host}\r\n" +
63
+ "Origin: #{origin}\r\n" +
64
+ "Sec-WebSocket-Key1: #{key1}\r\n" +
65
+ "Sec-WebSocket-Key2: #{key2}\r\n" +
66
+ "\r\n" +
67
+ "#{key3}")
68
+ flush()
69
+
70
+ line = gets().chomp()
71
+ raise(WebSocket::Error, "bad response: #{line}") if !(line =~ /\AHTTP\/1.1 101 /n)
72
+ read_header()
73
+ if (@header["sec-websocket-origin"] || "").downcase() != origin.downcase()
74
+ raise(WebSocket::Error,
75
+ "origin doesn't match: '#{@header["sec-websocket-origin"]}' != '#{origin}'")
76
+ end
77
+ reply_digest = read(16)
78
+ expected_digest = hixie_76_security_digest(key1, key2, key3)
79
+ if reply_digest != expected_digest
80
+ raise(WebSocket::Error,
81
+ "security digest doesn't match: %p != %p" % [reply_digest, expected_digest])
82
+ end
83
+ @handshaked = true
84
+
85
+ @received = []
86
+ @buffer = ""
87
+ @closing_started = false
88
+
89
+ if block_given?
90
+ @rth = Thread.new do
91
+ while data = receive()
92
+ yield data
93
+ end
94
+ end
95
+ end
96
+
97
+ end
98
+
99
+ attr_reader(:server, :header, :path)
100
+
101
+ def handshake(status = nil, header = {})
102
+ if @handshaked
103
+ raise(WebSocket::Error, "handshake has already been done")
104
+ end
105
+ status ||= "101 Switching Protocols"
106
+ def_header = {}
107
+ case @web_socket_version
108
+ when "hixie-75"
109
+ def_header["WebSocket-Origin"] = self.origin
110
+ def_header["WebSocket-Location"] = self.location
111
+ extra_bytes = ""
112
+ when "hixie-76"
113
+ def_header["Sec-WebSocket-Origin"] = self.origin
114
+ def_header["Sec-WebSocket-Location"] = self.location
115
+ extra_bytes = hixie_76_security_digest(
116
+ @header["Sec-WebSocket-Key1"], @header["Sec-WebSocket-Key2"], @key3)
117
+ else
118
+ def_header["Sec-WebSocket-Accept"] = security_digest(@header["sec-websocket-key"])
119
+ extra_bytes = ""
120
+ end
121
+ header = def_header.merge(header)
122
+ header_str = header.map(){ |k, v| "#{k}: #{v}\r\n" }.join("")
123
+ # Note that Upgrade and Connection must appear in this order.
124
+ write(
125
+ "HTTP/1.1 #{status}\r\n" +
126
+ "Upgrade: websocket\r\n" +
127
+ "Connection: Upgrade\r\n" +
128
+ "#{header_str}\r\n#{extra_bytes}")
129
+ flush()
130
+ @handshaked = true
131
+ end
132
+
133
+ def send(data)
134
+ if !@handshaked
135
+ raise(WebSocket::Error, "call WebSocket\#handshake first")
136
+ end
137
+ case @web_socket_version
138
+ when "hixie-75", "hixie-76"
139
+ data = force_encoding(data.dup(), "ASCII-8BIT")
140
+ write("\x00#{data}\xff")
141
+ flush()
142
+ else
143
+ send_frame(OPCODE_TEXT, data, !@server)
144
+ end
145
+ end
146
+
147
+ def receive()
148
+ if !@handshaked
149
+ raise(WebSocket::Error, "call WebSocket\#handshake first")
150
+ end
151
+ case @web_socket_version
152
+
153
+ when "hixie-75", "hixie-76"
154
+ packet = gets("\xff")
155
+ return nil if !packet
156
+ if packet =~ /\A\x00(.*)\xff\z/nm
157
+ return force_encoding($1, "UTF-8")
158
+ elsif packet == "\xff" && read(1) == "\x00" # closing
159
+ close(1005, "", :peer)
160
+ return nil
161
+ else
162
+ raise(WebSocket::Error, "input must be either '\\x00...\\xff' or '\\xff\\x00'")
163
+ end
164
+
165
+ else
166
+ begin
167
+ bytes = read(2).unpack("C*")
168
+ fin = (bytes[0] & 0x80) != 0
169
+ opcode = bytes[0] & 0x0f
170
+ mask = (bytes[1] & 0x80) != 0
171
+ plength = bytes[1] & 0x7f
172
+ if plength == 126
173
+ bytes = read(2)
174
+ plength = bytes.unpack("n")[0]
175
+ elsif plength == 127
176
+ bytes = read(8)
177
+ (high, low) = bytes.unpack("NN")
178
+ plength = high * (2 ** 32) + low
179
+ end
180
+ if @server && !mask
181
+ # Masking is required.
182
+ @socket.close()
183
+ raise(WebSocket::Error, "received unmasked data")
184
+ end
185
+ mask_key = mask ? read(4).unpack("C*") : nil
186
+ payload = read(plength)
187
+ payload = apply_mask(payload, mask_key) if mask
188
+ case opcode
189
+ when OPCODE_TEXT
190
+ return force_encoding(payload, "UTF-8")
191
+ when OPCODE_BINARY
192
+ raise(WebSocket::Error, "received binary data, which is not supported")
193
+ when OPCODE_CLOSE
194
+ close(1005, "", :peer)
195
+ return nil
196
+ when OPCODE_PING
197
+ raise(WebSocket::Error, "received ping, which is not supported")
198
+ when OPCODE_PONG
199
+ else
200
+ raise(WebSocket::Error, "received unknown opcode: %d" % opcode)
201
+ end
202
+ rescue EOFError
203
+ return nil
204
+ end
205
+
206
+ end
207
+ end
208
+
209
+ def tcp_socket
210
+ return @socket
211
+ end
212
+
213
+ def host
214
+ return @header["host"]
215
+ end
216
+
217
+ def origin
218
+ case @web_socket_version
219
+ when "7", "8"
220
+ name = "sec-websocket-origin"
221
+ else
222
+ name = "origin"
223
+ end
224
+ if @header[name]
225
+ return @header[name]
226
+ else
227
+ raise(WebSocket::Error, "%s header is missing" % name)
228
+ end
229
+ end
230
+
231
+ def location
232
+ return "ws://#{self.host}#{@path}"
233
+ end
234
+
235
+ # Does closing handshake.
236
+ def close(code = 1005, reason = "", origin = :self)
237
+ if !@closing_started
238
+ case @web_socket_version
239
+ when "hixie-75", "hixie-76"
240
+ write("\xff\x00")
241
+ else
242
+ if code == 1005
243
+ payload = ""
244
+ else
245
+ payload = [code].pack("n") + force_encoding(reason.dup(), "ASCII-8BIT")
246
+ end
247
+ send_frame(OPCODE_CLOSE, payload, false)
248
+ end
249
+ end
250
+ @socket.close() if origin == :peer
251
+ @closing_started = true
252
+ end
253
+
254
+ def close_socket()
255
+ @socket.close()
256
+ end
257
+
258
+ private
259
+
260
+ NOISE_CHARS = ("\x21".."\x2f").to_a() + ("\x3a".."\x7e").to_a()
261
+
262
+ def read_header()
263
+ @header = {}
264
+ while line = gets()
265
+ line = line.chomp()
266
+ break if line.empty?
267
+ if !(line =~ /\A(\S+): (.*)\z/n)
268
+ raise(WebSocket::Error, "invalid request: #{line}")
269
+ end
270
+ @header[$1] = $2
271
+ @header[$1.downcase()] = $2
272
+ end
273
+ if !@header["upgrade"]
274
+ raise(WebSocket::Error, "Upgrade header is missing")
275
+ end
276
+ if !(@header["upgrade"] =~ /\AWebSocket\z/i)
277
+ raise(WebSocket::Error, "invalid Upgrade: " + @header["upgrade"])
278
+ end
279
+ if !@header["connection"]
280
+ raise(WebSocket::Error, "Connection header is missing")
281
+ end
282
+ if @header["connection"].split(/,/).grep(/\A\s*Upgrade\s*\z/i).empty?
283
+ raise(WebSocket::Error, "invalid Connection: " + @header["connection"])
284
+ end
285
+ end
286
+
287
+ def send_frame(opcode, payload, mask)
288
+ payload = force_encoding(payload.dup(), "ASCII-8BIT")
289
+ # Setting StringIO's encoding to ASCII-8BIT.
290
+ buffer = StringIO.new(force_encoding("", "ASCII-8BIT"))
291
+ write_byte(buffer, 0x80 | opcode)
292
+ masked_byte = mask ? 0x80 : 0x00
293
+ if payload.bytesize <= 125
294
+ write_byte(buffer, masked_byte | payload.bytesize)
295
+ elsif payload.bytesize < 2 ** 16
296
+ write_byte(buffer, masked_byte | 126)
297
+ buffer.write([payload.bytesize].pack("n"))
298
+ else
299
+ write_byte(buffer, masked_byte | 127)
300
+ buffer.write([payload.bytesize / (2 ** 32), payload.bytesize % (2 ** 32)].pack("NN"))
301
+ end
302
+ if mask
303
+ mask_key = Array.new(4){ rand(256) }
304
+ buffer.write(mask_key.pack("C*"))
305
+ payload = apply_mask(payload, mask_key)
306
+ end
307
+ buffer.write(payload)
308
+ write(buffer.string)
309
+ end
310
+
311
+ def gets(rs = $/)
312
+ line = @socket.gets(rs)
313
+ $stderr.printf("recv> %p\n", line) if WebSocket.debug
314
+ return line
315
+ end
316
+
317
+ def read(num_bytes)
318
+ str = @socket.read(num_bytes)
319
+ $stderr.printf("recv> %p\n", str) if WebSocket.debug
320
+ if str && str.bytesize == num_bytes
321
+ return str
322
+ else
323
+ raise(EOFError)
324
+ end
325
+ end
326
+
327
+ def write(data)
328
+ if WebSocket.debug
329
+ data.scan(/\G(.*?(\n|\z))/n) do
330
+ $stderr.printf("send> %p\n", $&) if !$&.empty?
331
+ end
332
+ end
333
+ @socket.write(data)
334
+ end
335
+
336
+ def flush()
337
+ @socket.flush()
338
+ end
339
+
340
+ def write_byte(buffer, byte)
341
+ buffer.write([byte].pack("C"))
342
+ end
343
+
344
+ def security_digest(key)
345
+ return Base64.encode64(Digest::SHA1.digest(key + WEB_SOCKET_GUID)).gsub(/\n/, "")
346
+ end
347
+
348
+ def hixie_76_security_digest(key1, key2, key3)
349
+ bytes1 = websocket_key_to_bytes(key1)
350
+ bytes2 = websocket_key_to_bytes(key2)
351
+ return Digest::MD5.digest(bytes1 + bytes2 + key3)
352
+ end
353
+
354
+ def apply_mask(payload, mask_key)
355
+ orig_bytes = payload.unpack("C*")
356
+ new_bytes = []
357
+ orig_bytes.each_with_index() do |b, i|
358
+ new_bytes.push(b ^ mask_key[i % 4])
359
+ end
360
+ return new_bytes.pack("C*")
361
+ end
362
+
363
+ def generate_key()
364
+ spaces = 1 + rand(12)
365
+ max = 0xffffffff / spaces
366
+ number = rand(max + 1)
367
+ key = (number * spaces).to_s()
368
+ (1 + rand(12)).times() do
369
+ char = NOISE_CHARS[rand(NOISE_CHARS.size)]
370
+ pos = rand(key.size + 1)
371
+ key[pos...pos] = char
372
+ end
373
+ spaces.times() do
374
+ pos = 1 + rand(key.size - 1)
375
+ key[pos...pos] = " "
376
+ end
377
+ return key
378
+ end
379
+
380
+ def generate_key3()
381
+ return [rand(0x100000000)].pack("N") + [rand(0x100000000)].pack("N")
382
+ end
383
+
384
+ def websocket_key_to_bytes(key)
385
+ num = key.gsub(/[^\d]/n, "").to_i() / key.scan(/ /).size
386
+ return [num].pack("N")
387
+ end
388
+
389
+ def force_encoding(str, encoding)
390
+ if str.respond_to?(:force_encoding)
391
+ return str.force_encoding(encoding)
392
+ else
393
+ return str
394
+ end
395
+ end
396
+
397
+ def ssl_handshake(socket)
398
+ ssl_context = OpenSSL::SSL::SSLContext.new()
399
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
400
+ ssl_socket.sync_close = true
401
+ ssl_socket.connect()
402
+ return ssl_socket
403
+ end
404
+
405
+ end
@@ -0,0 +1,21 @@
1
+ # Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
2
+ # Lincense: New BSD Lincense
3
+
4
+ $LOAD_PATH << File.dirname(__FILE__) + "/../lib"
5
+ require "web_socket"
6
+
7
+ if ARGV.size != 1
8
+ $stderr.puts("Usage: ruby samples/stdio_client.rb ws://HOST:PORT/")
9
+ exit(1)
10
+ end
11
+
12
+ client = WebSocket.new(ARGV[0]) { |data| puts data}
13
+ puts("Connected")
14
+
15
+ $stdin.each_line() do |line|
16
+ data = line.chomp()
17
+ client.send(data)
18
+ printf("Sent: %p\n", data)
19
+ end
20
+ puts "closing connection..."
21
+ client.close()
@@ -0,0 +1,16 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.authors = ["Marvin Frick"]
5
+ gem.email = ["frick@informatik.uni-luebeck.de"]
6
+ gem.description = %q{Ruby gem to connect to a websocket server}
7
+ gem.summary = %q{Allows you to connect to a websocket server-side, using the hixie-76 protocol version. Sending and receiving data is supported. That's it.}
8
+ gem.homepage = "https://github.com/MrMarvin/simpleblockingwebsocketclient"
9
+
10
+ gem.files = `git ls-files`.split($\)
11
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
12
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
13
+ gem.name = "simpleblockingwebsocketclient"
14
+ gem.require_paths = ["lib"]
15
+ gem.version = "0.42"
16
+ end
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simpleblockingwebsocketclient
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.42'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Marvin Frick
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-12-18 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Ruby gem to connect to a websocket server
15
+ email:
16
+ - frick@informatik.uni-luebeck.de
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - .gitignore
22
+ - Gemfile
23
+ - README.md
24
+ - Rakefile
25
+ - lib/simpleblockingwebsocketclient.rb
26
+ - pkg/simpleblockingwebsocketclient-0.42.gem
27
+ - samples/stdio_client.rb
28
+ - simpleblockingwebsocketclient.gemspec
29
+ homepage: https://github.com/MrMarvin/simpleblockingwebsocketclient
30
+ licenses: []
31
+ post_install_message:
32
+ rdoc_options: []
33
+ require_paths:
34
+ - lib
35
+ required_ruby_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ! '>='
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ none: false
41
+ required_rubygems_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ none: false
47
+ requirements: []
48
+ rubyforge_project:
49
+ rubygems_version: 1.8.24
50
+ signing_key:
51
+ specification_version: 3
52
+ summary: Allows you to connect to a websocket server-side, using the hixie-76 protocol
53
+ version. Sending and receiving data is supported. That's it.
54
+ test_files: []