websocker 0.0.2

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.
@@ -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
+