rfc-ws-client 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,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: []