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.
- data/lib/websocker/client.rb +208 -0
- data/lib/websocker/version.rb +3 -0
- data/lib/websocker.rb +3 -0
- metadata +68 -0
|
@@ -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
|
data/lib/websocker.rb
ADDED
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
|
+
|