rfc-ws-client 0.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,18 @@
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
18
+ .DS_Store
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rfc-ws-client.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Lucas Clemente
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,21 @@
1
+ # RFC Websocket Client (rfc-ws-client)
2
+
3
+ A simple RFC 6455 (Websocket) compatible client in ruby.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'rfc-ws-client'
10
+
11
+ ## Usage
12
+
13
+
14
+
15
+ ## Contributing
16
+
17
+ 1. Fork it
18
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
19
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
20
+ 4. Push to the branch (`git push origin my-new-feature`)
21
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/examples/echo.rb ADDED
@@ -0,0 +1,24 @@
1
+ $: << File.dirname(__FILE__) + "/../lib"
2
+
3
+ require "rfc-ws-client"
4
+
5
+ ws = RfcWebsocket::Websocket.new("ws://localhost:9001/getCaseCount")
6
+ count = ws.receive()[0].to_i
7
+ ws.close
8
+
9
+ (1..count).each do |i|
10
+ puts "#{i}/#{count}"
11
+ begin
12
+ ws = RfcWebsocket::Websocket.new "ws://localhost:9001/runCase?&case=#{i}&agent=rfc-ws-client"
13
+ while true
14
+ data, binary = ws.receive
15
+ break if data.nil?
16
+ ws.send_message data, binary: binary
17
+ end
18
+ rescue
19
+ end
20
+ end
21
+
22
+ puts "Updating reports and shutting down"
23
+ ws = RfcWebsocket::Websocket.new "ws://localhost:9001/updateReports?agent=rfc-ws-client"
24
+ ws.receive
@@ -0,0 +1,3 @@
1
+ module RfcWebsocket
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,239 @@
1
+ require "rfc-ws-client/version"
2
+
3
+ require "openssl"
4
+ require 'uri'
5
+ require 'socket'
6
+ require 'securerandom'
7
+ require "digest/sha1"
8
+ require 'rainbow'
9
+ require 'base64'
10
+
11
+ module RfcWebsocket
12
+ class Websocket
13
+ WEB_SOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
14
+ OPCODE_CONTINUATION = 0x01
15
+ OPCODE_TEXT = 0x01
16
+ OPCODE_BINARY = 0x02
17
+ OPCODE_CLOSE = 0x08
18
+ OPCODE_PING = 0x09
19
+ OPCODE_PONG = 0x0a
20
+ DEBUG = false
21
+
22
+ def initialize(uri, protocol = "")
23
+ uri = URI.parse(uri) unless uri.is_a?(URI)
24
+ @protocol = protocol
25
+
26
+ if uri.scheme == "ws"
27
+ default_port = 80
28
+ elsif uri.scheme = "wss"
29
+ default_port = 443
30
+ else
31
+ raise "unsupported scheme: #{uri.scheme}"
32
+ end
33
+ host = uri.host + ((!uri.port || uri.port == default_port) ? "" : ":#{uri.port}")
34
+ path = (uri.path.empty? ? "/" : uri.path) + (uri.query ? "?" + uri.query : "")
35
+
36
+ @socket = TCPSocket.new(uri.host, uri.port || default_port)
37
+ if uri.scheme == "wss"
38
+ @socket = OpenSSL::SSL::SSLSocket.new(@socket)
39
+ @socket.sync_close = true
40
+ @socket.connect
41
+ end
42
+
43
+ request_key = SecureRandom::base64(16)
44
+ write(handshake(host, path, request_key))
45
+ flush()
46
+
47
+ status_line = gets.chomp
48
+ raise "bad response: #{status_line}" unless status_line.start_with?("HTTP/1.1 101")
49
+
50
+ header = {}
51
+ while line = gets
52
+ line.chomp!
53
+ break if line.empty?
54
+ if !(line =~ /\A(\S+): (.*)\z/n)
55
+ raise "invalid response: #{line}"
56
+ end
57
+ header[$1.downcase] = $2
58
+ end
59
+ raise "upgrade missing" unless header["upgrade"]
60
+ raise "connection missing" unless header["connection"]
61
+ accept = header["sec-websocket-accept"]
62
+ raise "sec-websocket-accept missing" unless accept
63
+ expected_accept = Digest::SHA1.base64digest(request_key + WEB_SOCKET_GUID)
64
+ raise "sec-websocket-accept is invalid, actual: #{accept}, expected: #{expected_accept}" unless accept == expected_accept
65
+ end
66
+
67
+ def send_message(message, opts = {binary: false})
68
+ write(encode(message, opts[:binary] ? OPCODE_BINARY : OPCODE_TEXT))
69
+ end
70
+
71
+ def receive
72
+ begin
73
+ bytes = read(2).unpack("C*")
74
+ fin = (bytes[0] & 0x80) != 0
75
+ opcode = bytes[0] & 0x0f
76
+ mask = (bytes[1] & 0x80) != 0
77
+ length = bytes[1] & 0x7f
78
+ if bytes[0] & 0b01110000 != 0
79
+ raise "reserved bits must be 0"
80
+ end
81
+ if opcode > 7
82
+ if !fin
83
+ raise "control frame cannot be fragmented"
84
+ elsif length > 125
85
+ raise "Control frame is too large #{length}"
86
+ elsif opcode > 0xA
87
+ raise "Unexpected reserved opcode #{opcode}"
88
+ elsif opcode == OPCODE_CLOSE && length == 1
89
+ raise "Close control frame with payload of length 1"
90
+ end
91
+ else
92
+ if opcode != OPCODE_CONTINUATION && opcode != OPCODE_TEXT && opcode != OPCODE_BINARY
93
+ raise "Unexpected reserved opcode #{opcode}"
94
+ end
95
+ end
96
+ if length == 126
97
+ bytes = read(2)
98
+ length = bytes.unpack("n")[0]
99
+ elsif length == 127
100
+ bytes = read(8)
101
+ (high, low) = bytes.unpack("NN")
102
+ length = high * (2 ** 32) + low
103
+ end
104
+ mask_key = mask ? read(4).unpack("C*") : nil
105
+ payload = read(length)
106
+ payload = apply_mask(payload, mask_key) if mask
107
+ case opcode
108
+ when OPCODE_TEXT
109
+ return payload.force_encoding("UTF-8"), false
110
+ when OPCODE_BINARY
111
+ return payload, true
112
+ when OPCODE_CLOSE
113
+ code, explain = payload.unpack("nA*")
114
+ if explain && !explain.force_encoding("UTF-8").valid_encoding?
115
+ close(1007)
116
+ else
117
+ close(response_close_code(code))
118
+ end
119
+ return nil, nil
120
+ when OPCODE_PING
121
+ write(encode(payload, OPCODE_PONG))
122
+ #TODO fix recursion
123
+ return receive
124
+ when OPCODE_PONG
125
+ return receive
126
+ else
127
+ raise "received unknown opcode: #{opcode}"
128
+ end
129
+ rescue EOFError
130
+ return nil, nil
131
+ rescue => e
132
+ close(1002)
133
+ raise e
134
+ end
135
+ end
136
+
137
+ def close(code = 1000, msg = nil)
138
+ write(encode [code ? code : 1000, msg].pack("nA*"), OPCODE_CLOSE)
139
+ @socket.close
140
+ end
141
+
142
+ private
143
+
144
+ def gets(delim = $/)
145
+ line = @socket.gets(delim)
146
+ print line.color(:green) if DEBUG
147
+ line
148
+ end
149
+
150
+ def write(data)
151
+ print data.color(:yellow) if DEBUG
152
+ @socket.write(data)
153
+ @socket.flush
154
+ end
155
+
156
+ def read(num_bytes)
157
+ str = @socket.read(num_bytes)
158
+ if str && str.bytesize == num_bytes
159
+ print str.color(:green) if DEBUG
160
+ str
161
+ else
162
+ raise(EOFError)
163
+ end
164
+ end
165
+
166
+ def flush
167
+ @socket.flush
168
+ end
169
+
170
+ def handshake(host, path, request_key)
171
+ headers = ["GET #{path} HTTP/1.1"]
172
+ headers << "Connection: keep-alive, Upgrade"
173
+ headers << "Host: #{host}"
174
+ headers << "Sec-WebSocket-Key: #{request_key}"
175
+ headers << "Sec-WebSocket-Version: 13"
176
+ headers << "Upgrade: websocket"
177
+ headers << "User-Agent: rfc-ws-client"
178
+ headers << "\r\n"
179
+ headers.join "\r\n"
180
+ end
181
+
182
+ def encode(data, opcode)
183
+ frame = []
184
+ frame << (opcode | 0x80)
185
+
186
+ packr = "CC"
187
+
188
+ if opcode == OPCODE_TEXT
189
+ data.force_encoding("UTF-8")
190
+ if !data.valid_encoding?
191
+ raise "Invalid UTF!"
192
+ end
193
+ end
194
+
195
+ # append frame length and mask bit 0x80
196
+ len = data ? data.bytesize : 0
197
+ if len <= 125
198
+ frame << (len | 0x80)
199
+ elsif len < 65536
200
+ frame << (126 | 0x80)
201
+ frame << len
202
+ packr << "n"
203
+ else
204
+ frame << (127 | 0x80)
205
+ frame << len
206
+ packr << "L!>"
207
+ end
208
+
209
+ # generate a masking key
210
+ key = rand(2 ** 31)
211
+
212
+ # mask each byte with the key
213
+ frame << key
214
+ packr << "N"
215
+
216
+ # Apply the masking key to every byte
217
+ len.times do |i|
218
+ frame << ((data.getbyte(i) ^ (key >> ((3 - (i % 4)) * 8))) & 0xFF)
219
+ end
220
+
221
+ frame.pack("#{packr}C*")
222
+ end
223
+
224
+ def response_close_code(code)
225
+ case code
226
+ when 1000,1001,1002,1003,1007,1008,1009,1010,1011
227
+ 1000
228
+ when 3000..3999
229
+ 1000
230
+ when 4000..4999
231
+ 1000
232
+ when nil
233
+ 1000
234
+ else
235
+ 1002
236
+ end
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,18 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rfc-ws-client/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "rfc-ws-client"
8
+ gem.version = RfcWebsocket::VERSION
9
+ gem.authors = ["Lucas Clemente"]
10
+ gem.email = ["luke.clemente@gmail.com"]
11
+ gem.summary = %q{A simple Websocket client in ruby}
12
+ gem.homepage = "https://github.com/lucas-clemente/rfc-ws-client"
13
+
14
+ gem.files = `git ls-files`.split($/)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.require_paths = ["lib"]
18
+ end
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rfc-ws-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Lucas Clemente
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-19 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description:
15
+ email:
16
+ - luke.clemente@gmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - .gitignore
22
+ - Gemfile
23
+ - LICENSE.txt
24
+ - README.md
25
+ - Rakefile
26
+ - examples/echo.rb
27
+ - lib/rfc-ws-client.rb
28
+ - lib/rfc-ws-client/version.rb
29
+ - rfc-ws-client.gemspec
30
+ homepage: https://github.com/lucas-clemente/rfc-ws-client
31
+ licenses: []
32
+ post_install_message:
33
+ rdoc_options: []
34
+ require_paths:
35
+ - lib
36
+ required_ruby_version: !ruby/object:Gem::Requirement
37
+ none: false
38
+ requirements:
39
+ - - ! '>='
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ required_rubygems_version: !ruby/object:Gem::Requirement
43
+ none: false
44
+ requirements:
45
+ - - ! '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ requirements: []
49
+ rubyforge_project:
50
+ rubygems_version: 1.8.23
51
+ signing_key:
52
+ specification_version: 3
53
+ summary: A simple Websocket client in ruby
54
+ test_files: []