simpleblockingwebsocketclient 0.42

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