websocker 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,208 @@
1
+ require "base64"
2
+ require "socket"
3
+ require "uri"
4
+ require "digest/md5"
5
+ require "digest/sha1"
6
+ require "openssl"
7
+ require "stringio"
8
+ require "logger"
9
+
10
+ # implement a websocket client that speaks the hybi-10 protocol
11
+ module Websocker
12
+ class Client
13
+ class NotConnectedError < RuntimeError; end
14
+
15
+ class HandshakeNegotiationError < RuntimeError; end
16
+
17
+ def initialize(opts = {})
18
+ @host = opts[:host]
19
+ @port = opts[:port] || 80
20
+ @origin = opts[:origin] || "localhost"
21
+ @path = opts[:path] || "/"
22
+ @connected = false
23
+ @logger = opts[:logger] || Logger.new(STDOUT)
24
+ @logger.debug "Connecting to #{@host}:#{@port}"
25
+ end
26
+
27
+ def connect
28
+ @sock = TCPSocket.open(@host, @port)
29
+ @connected = true
30
+ key = generateKey
31
+ @sock.write handshake(key)
32
+ headers = read_headers
33
+ received_key = headers['Sec-WebSocket-Accept']
34
+ expected_key = expected_security_key_answer(key)
35
+ raise HandshakeNegotiationError, 'Key Mismatch' unless received_key == expected_key
36
+ @sock
37
+ end
38
+
39
+ def listen
40
+ @loop = Thread.new do
41
+ while @connected
42
+ message = read
43
+ puts "listen: #{message}"
44
+ @on_message.call(message) unless @on_message.nil?
45
+ end
46
+ end
47
+ end
48
+
49
+ def on_message(&blk)
50
+ @on_message = blk
51
+ end
52
+
53
+ def on_closed(&blk)
54
+ @on_closed = blk
55
+ end
56
+
57
+ def send(data)
58
+ byte1 = 0x80 | 1
59
+ write_byte(byte1)
60
+
61
+ # write length
62
+ if data.size <= 125
63
+ byte2 = data.size
64
+ write_byte(byte2)
65
+ elsif data.size <= 65535
66
+ byte2 = 0b10000000 | 126
67
+ write_byte(byte2)
68
+ # write length in next two bytes
69
+ @sock.write [length].pack('n') # 16-bit unsigned
70
+ else
71
+ byte2 = 0b10000000 | 127
72
+ write_byte(byte2)
73
+ # write length in next eight bytes
74
+ @sock.write [length].pack('Q') # 64-bit unsigned
75
+ end
76
+
77
+ @sock.write(data)
78
+ @sock.flush
79
+
80
+ puts "Writing #{byte1}, #{byte2}, #{data}"
81
+ end
82
+
83
+ def close
84
+ @logger.debug "Connection closed"
85
+ @connected = false
86
+ @on_closed unless @on_closed.nil?
87
+ end
88
+
89
+ private
90
+
91
+ def handshake(key)
92
+ hello = "GET #{@path} HTTP/1.1\r\n"
93
+ hello << "Host: #{@host}\r\n"
94
+ hello << "Upgrade: websocket\r\n"
95
+ hello << "Connection: Upgrade\r\n"
96
+ hello << "Sec-WebSocket-Version: 13\r\n"
97
+ hello << "Sec-WebSocket-Key: #{key}\r\n"
98
+ hello << "Sec-WebSocket-Origin: #{@origin}\r\n"
99
+ hello << "\r\n"
100
+ end
101
+
102
+ def read_headers
103
+ line = @sock.gets
104
+ @logger.debug line
105
+ headers = {}
106
+ while line = @sock.gets
107
+ line = line.chomp
108
+ @logger.debug line
109
+ break if line.empty?
110
+ raise HandshakeNegotiationError unless line =~ /(\S+): (.*)/n
111
+ headers[$1.to_s] = $2
112
+ end
113
+ headers
114
+ end
115
+
116
+ def expected_security_key_answer(key)
117
+ Base64.encode64(Digest::SHA1.digest("#{key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11")).gsub(/\n/, "")
118
+ end
119
+
120
+ def generateKey
121
+ Base64.encode64((0..16).map { rand(255).chr } .join).strip
122
+ end
123
+
124
+ def read(buffer = '')
125
+ fin, opcode, mask, len, masking_key, payload = read_frame
126
+
127
+ @logger.debug "Read: opcode: #{opcode}: #{payload}"
128
+
129
+ if opcode == 0x8 # connection closed
130
+ close
131
+ else
132
+ if fin then
133
+ return buffer + payload
134
+ else
135
+ return read(buffer + payload)
136
+ end
137
+ end
138
+ end
139
+
140
+ # write an unsigned byte
141
+ def write_byte(byte)
142
+ @sock.write [byte].pack("C")
143
+ end
144
+
145
+ # fin: 1 bit, indicates this is the final fragment in a message
146
+ # rsv1, rsv2, rsv3: 1 bit, reserved, usually zero unless used by websocket extensions
147
+ # opcode: 4 bits; 0 continuation, 1 text, 2 bin, 8 closed
148
+ # mask: 1 bit, indicates payload is masked
149
+ # len: 7 bits, payload length
150
+ # payload: variable
151
+ def read_frame
152
+ byte = read_and_unpack_byte
153
+ fin = (byte & 0b10000000) == 0b10000000
154
+ rsv1 = byte & 0b01000000
155
+ rsv2 = byte & 0b00100000
156
+ rsv3 = byte & 0b00010000
157
+ opcode = byte & 0b00001111
158
+
159
+ @logger.debug "unexpected value: rsv1: #{rsv1}" unless rsv1 == 0
160
+ @logger.debug "unexpected value: rsv2: #{rsv2}" unless rsv2 == 0
161
+ @logger.debug "unexpected value: rsv3: #{rsv3}" unless rsv3 == 0
162
+
163
+ byte = read_and_unpack_byte
164
+ mask = (byte & 0b10000000) == 0b10000000
165
+ lenflag = byte & 0b01111111
166
+
167
+ # if len <= 125, this is the length
168
+ # if len == 126, length is encoded on next two bytes
169
+ # if len == 127, length is encoded on next eight bytes
170
+ len = case lenflag
171
+ when 126 # 2 bytes
172
+ bytes = @sock.read(2)
173
+ len = bytes.unpack("n")[0]
174
+ when 127 # 8 bytes
175
+ bytes = @sock.read(8)
176
+ len = bytes.unpack("Q")[0]
177
+ else
178
+ lenflag
179
+ end
180
+
181
+ if mask then
182
+ @logger.debugmask
183
+ masking_key = @sock.read(4).unpack("C*")
184
+ end
185
+
186
+ payload = @sock.read(len)
187
+ payload = apply_mask(payload, masking_key) if mask
188
+
189
+ return fin, opcode, mask, len, masking_key, payload
190
+ end
191
+
192
+ def apply_mask(payload, masking_key)
193
+ bytes = payload.unpack("C*")
194
+ converted = []
195
+ bytes.each_with_index do |b,i|
196
+ converted.push(b ^ masking_key[i%4])
197
+ end
198
+ return converted.pack("C*")
199
+ end
200
+
201
+ # reads a byte and returns an 8-bit unsigned integer
202
+ def read_and_unpack_byte
203
+ byte = @sock.read(1)
204
+ @logger.debug "read_and_unpack_byte: #{byte}"
205
+ byte = byte.unpack('C')[0]
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,3 @@
1
+ module Websocker
2
+ VERSION = "0.0.2"
3
+ end
data/lib/websocker.rb ADDED
@@ -0,0 +1,3 @@
1
+ require "websocker/client"
2
+ require "websocker/version"
3
+
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: websocker
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 2
10
+ version: 0.0.2
11
+ platform: ruby
12
+ authors:
13
+ - Lawrence Mcalpin
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-02-24 00:00:00 Z
19
+ dependencies: []
20
+
21
+ description: A simple implementation of a Websocket client.
22
+ email:
23
+ - lmcalpin+turntable_api@gmail.com
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files: []
29
+
30
+ files:
31
+ - lib/websocker/client.rb
32
+ - lib/websocker/version.rb
33
+ - lib/websocker.rb
34
+ homepage: ""
35
+ licenses: []
36
+
37
+ post_install_message:
38
+ rdoc_options: []
39
+
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ none: false
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ hash: 3
48
+ segments:
49
+ - 0
50
+ version: "0"
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ hash: 3
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ requirements: []
61
+
62
+ rubyforge_project: websocker
63
+ rubygems_version: 1.8.16
64
+ signing_key:
65
+ specification_version: 3
66
+ summary: Library for communicating with Websocket servers.
67
+ test_files: []
68
+