socketio-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/Gemfile +4 -0
- data/README.md +58 -0
- data/Rakefile +7 -0
- data/example/complex.rb +1 -0
- data/example/remote_log.rb +14 -0
- data/example/servers/complex.js +1 -0
- data/example/servers/remote_log.js +18 -0
- data/example/servers/simple.js +10 -0
- data/example/simple.rb +10 -0
- data/lib/SocketIO.rb +169 -0
- data/lib/parser.rb +13 -0
- data/lib/web_socket.rb +429 -0
- data/pkg/socket.io-client-0.0.1.gem +0 -0
- data/pkg/socketio-client-0.0.1.gem +0 -0
- data/socketio-client.gemspec +24 -0
- data/spec/parser_spec.rb +22 -0
- data/spec/setup/echo.js +17 -0
- data/spec/setup/start.rb +8 -0
- data/spec/setup/stop.rb +0 -0
- data/spec/socketio_spec.rb +46 -0
- metadata +81 -0
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# Simple Socket IO client
|
2
|
+
|
3
|
+
Quick and kinda dirty socket.io client using web sockets
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
This client currently supports:
|
8
|
+
|
9
|
+
* Listeners for all 9 possible message
|
10
|
+
* Send messages of the type:
|
11
|
+
* message
|
12
|
+
* json
|
13
|
+
* event
|
14
|
+
|
15
|
+
## How to use:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
require 'socketIO'
|
19
|
+
|
20
|
+
client = SocketIO.connect("localhost") do
|
21
|
+
before_start do
|
22
|
+
on_message {|message| puts "incoming message: #{message}"}
|
23
|
+
on_event('news') { |data| puts data.first} # data is an array fo things.
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
## Sync vs Async
|
30
|
+
|
31
|
+
You can start the socket io syncronously and then continue with your work
|
32
|
+
this crates threads so be careful.
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
require 'socketIO'
|
36
|
+
|
37
|
+
client = SocketIO.connect("localhost", sync: true) do
|
38
|
+
before_start do
|
39
|
+
on_message {|message| puts message}
|
40
|
+
on_disconnect {puts "I GOT A DISCONNECT"}
|
41
|
+
end
|
42
|
+
|
43
|
+
after_start do
|
44
|
+
emit("loadLogs", "/var/www/rails_app/log/production.log")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
puts "socket still running"
|
49
|
+
loop do
|
50
|
+
sleep 10
|
51
|
+
puts 'zzz'
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
## Examples
|
56
|
+
|
57
|
+
examples can be found in the examples/ folder.
|
58
|
+
A corrosponding server can be found in the examples/servers
|
data/Rakefile
ADDED
data/example/complex.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# not yet implemented
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'socketIO'
|
2
|
+
|
3
|
+
client = SocketIO.connect("localhost") do
|
4
|
+
before_start do
|
5
|
+
on_message {|message| puts "incoming message: #{message}"}
|
6
|
+
on_disconnect {puts "I GOT A TDISCONNECT"}
|
7
|
+
end
|
8
|
+
|
9
|
+
after_start do
|
10
|
+
emit("loadLogs", "/Users/lyon/test/rails_app/log/development.log")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
puts "thread exited and I have the power back"
|
@@ -0,0 +1 @@
|
|
1
|
+
// not yet implemented
|
@@ -0,0 +1,18 @@
|
|
1
|
+
var io = require('socket.io').listen(80)
|
2
|
+
,spawn = require('child_process').spawn
|
3
|
+
|
4
|
+
console.log("Up and running. waiting on connections")
|
5
|
+
io.sockets.on('connection', function(socket){
|
6
|
+
console.log("new connection.")
|
7
|
+
|
8
|
+
socket.on("loadLogs", function(path){
|
9
|
+
var tail = spawn("tail", ["-F", path])
|
10
|
+
|
11
|
+
console.log("Begining tail on: " + path)
|
12
|
+
tail.stdout.on("data", function(data){
|
13
|
+
console.log(data.toString("utf8"))
|
14
|
+
socket.send(data.toString("utf8"))
|
15
|
+
})
|
16
|
+
})
|
17
|
+
|
18
|
+
})
|
data/example/simple.rb
ADDED
data/lib/SocketIO.rb
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
require 'web_socket'
|
2
|
+
require 'rest_client'
|
3
|
+
require 'json'
|
4
|
+
require 'parser'
|
5
|
+
|
6
|
+
module SocketIO
|
7
|
+
|
8
|
+
def self.connect(host, options = {}, &block)
|
9
|
+
response = RestClient.get "http://#{host}/socket.io/1/"
|
10
|
+
# resonse should be in the form of sessionid:heartbeattimeout:closetimeout:supported stuff
|
11
|
+
response_array = response.split(':')
|
12
|
+
response_array = [host] + response_array << options
|
13
|
+
cli = Client.new(*response_array)
|
14
|
+
cli.instance_eval(&block) if block
|
15
|
+
cli.start
|
16
|
+
end
|
17
|
+
|
18
|
+
class Client
|
19
|
+
VERSION = "0.0.1"
|
20
|
+
|
21
|
+
[:INT, :TERM].each do |sig|
|
22
|
+
Signal.trap(sig) do
|
23
|
+
puts
|
24
|
+
puts "bye"
|
25
|
+
exit
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# The state of the Socket.IO socket can be disconnected, disconnecting, connected and connecting.
|
30
|
+
# The transport connection can be closed, closing, open, and opening.
|
31
|
+
|
32
|
+
def initialize(host, session_id, heartbeat_timeout, connection_timeout, supported_transports, options = {})
|
33
|
+
@host = host
|
34
|
+
@session_id = session_id
|
35
|
+
@hb_timeout = heartbeat_timeout
|
36
|
+
@connect_timeout = connection_timeout
|
37
|
+
@supported_transports = supported_transports
|
38
|
+
@options = options
|
39
|
+
@reconnect = options[:reconnect]
|
40
|
+
@on_event = {}
|
41
|
+
end
|
42
|
+
|
43
|
+
def start
|
44
|
+
self.instance_eval(&@before_start) if @before_start
|
45
|
+
connect_transport
|
46
|
+
start_recieve_loop
|
47
|
+
self.instance_eval(&@after_start) if @after_start
|
48
|
+
@thread.join unless @options[:sync]
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
def connect_transport
|
53
|
+
if @supported_transports.include? "websocket"
|
54
|
+
@transport = WebSocket.new("ws://#{@host}/socket.io/1/websocket/#{@session_id}")
|
55
|
+
else
|
56
|
+
raise "We only support WebSockets.. and this server doesnt like web sockets.. O NO!!"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def start_recieve_loop
|
61
|
+
@thread = Thread.new() do
|
62
|
+
while data = @transport.receive()
|
63
|
+
decoded = Parser.decode(data)
|
64
|
+
case decoded[:type]
|
65
|
+
when '0'
|
66
|
+
@on_disconnect.call if @on_disconnect
|
67
|
+
when '1'
|
68
|
+
@on_connect.call if @on_connect
|
69
|
+
when '2'
|
70
|
+
send_heartbeat
|
71
|
+
@on_heartbeat.call if @on_heartbeat
|
72
|
+
when '3'
|
73
|
+
@on_message.call decoded[:data] if @on_message
|
74
|
+
when '4'
|
75
|
+
@on_json_message.call decoded[:data] if @on_json_message
|
76
|
+
when '5'
|
77
|
+
message = JSON.parse(decoded[:data])
|
78
|
+
@on_event[message['name']].call message['args'] if @on_event[message['name']]
|
79
|
+
when '6'
|
80
|
+
@on_error.call decoded[:data] if @on_error
|
81
|
+
when '7'
|
82
|
+
@on_ack.call if @on_ack
|
83
|
+
when '8'
|
84
|
+
@on_noop.call if @on_noop
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
@thread
|
89
|
+
end
|
90
|
+
|
91
|
+
def disconnect
|
92
|
+
@transport.send("0::")
|
93
|
+
end
|
94
|
+
|
95
|
+
def disconnected
|
96
|
+
if @reconnect
|
97
|
+
connect_transport
|
98
|
+
start_recieve_loop
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def join
|
103
|
+
@thread.join
|
104
|
+
end
|
105
|
+
|
106
|
+
def send_heartbeat
|
107
|
+
@transport.send("2::") #rescue false
|
108
|
+
end
|
109
|
+
|
110
|
+
def send_message(string)
|
111
|
+
@transport.send("3:::#{string}") #rescue false
|
112
|
+
end
|
113
|
+
alias :send :send_message
|
114
|
+
|
115
|
+
def send_json_message(hash)
|
116
|
+
@transport.send("4:::#{hash.to_json}") # rescue false
|
117
|
+
end
|
118
|
+
|
119
|
+
def send_event(name, hash)
|
120
|
+
@transport.send("5:::#{{name: name, args: [hash]}.to_json}") # rescue false
|
121
|
+
end
|
122
|
+
alias :emit :send_event
|
123
|
+
|
124
|
+
def before_start(&block)
|
125
|
+
@before_start = block
|
126
|
+
end
|
127
|
+
|
128
|
+
def after_start(&block)
|
129
|
+
@after_start = block
|
130
|
+
end
|
131
|
+
|
132
|
+
def on_disconnect(&block)
|
133
|
+
@on_disconnect = block
|
134
|
+
end
|
135
|
+
|
136
|
+
def on_connect(&block)
|
137
|
+
@on_connect = block
|
138
|
+
end
|
139
|
+
|
140
|
+
def on_heartbeat(&block)
|
141
|
+
@on_heartbeat = block
|
142
|
+
end
|
143
|
+
|
144
|
+
def on_message(&block)
|
145
|
+
@on_message = block
|
146
|
+
end
|
147
|
+
|
148
|
+
def on_json_message(&block)
|
149
|
+
@on_json_message = block
|
150
|
+
end
|
151
|
+
|
152
|
+
def on_event(name, &block)
|
153
|
+
@on_event[name] = block
|
154
|
+
end
|
155
|
+
|
156
|
+
def on_ack(&block)
|
157
|
+
@on_ack = block
|
158
|
+
end
|
159
|
+
|
160
|
+
def on_error(&block)
|
161
|
+
@on_error = block
|
162
|
+
end
|
163
|
+
|
164
|
+
def on_noop(&block)
|
165
|
+
@on_noop = block
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
data/lib/parser.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
module Parser
|
2
|
+
@regexp = /([^:]+):([0-9]+)?(\+)?:([^:]+)?:?([\s\S]*)?/
|
3
|
+
|
4
|
+
# returns hash as {type: '1', id: '1', end_point: '4', data: [{key: value}]}
|
5
|
+
def self.decode(string)
|
6
|
+
if pieces = string.match(@regexp)
|
7
|
+
{type: pieces[1], id: pieces[2], end_point: pieces[4], data: pieces[5]}
|
8
|
+
else
|
9
|
+
{type: '0'}
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
data/lib/web_socket.rb
ADDED
@@ -0,0 +1,429 @@
|
|
1
|
+
# Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
|
2
|
+
# Lincense: New BSD Lincense
|
3
|
+
# Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75
|
4
|
+
# Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
|
5
|
+
# Reference: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07
|
6
|
+
# Reference: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10
|
7
|
+
|
8
|
+
require "base64"
|
9
|
+
require "socket"
|
10
|
+
require "uri"
|
11
|
+
require "digest/md5"
|
12
|
+
require "digest/sha1"
|
13
|
+
require "openssl"
|
14
|
+
require "stringio"
|
15
|
+
|
16
|
+
|
17
|
+
class WebSocket
|
18
|
+
|
19
|
+
class << self
|
20
|
+
|
21
|
+
attr_accessor(:debug)
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
class Error < RuntimeError
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
WEB_SOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
30
|
+
OPCODE_CONTINUATION = 0x00
|
31
|
+
OPCODE_TEXT = 0x01
|
32
|
+
OPCODE_BINARY = 0x02
|
33
|
+
OPCODE_CLOSE = 0x08
|
34
|
+
OPCODE_PING = 0x09
|
35
|
+
OPCODE_PONG = 0x0a
|
36
|
+
|
37
|
+
def initialize(arg, params = {})
|
38
|
+
if params[:server] # server
|
39
|
+
|
40
|
+
@server = params[:server]
|
41
|
+
@socket = arg
|
42
|
+
line = gets().chomp()
|
43
|
+
if !(line =~ /\AGET (\S+) HTTP\/1.1\z/n)
|
44
|
+
raise(WebSocket::Error, "invalid request: #{line}")
|
45
|
+
end
|
46
|
+
@path = $1
|
47
|
+
read_header()
|
48
|
+
if @header["sec-websocket-version"]
|
49
|
+
@web_socket_version = @header["sec-websocket-version"]
|
50
|
+
@key3 = nil
|
51
|
+
elsif @header["sec-websocket-key1"] && @header["sec-websocket-key2"]
|
52
|
+
@web_socket_version = "hixie-76"
|
53
|
+
@key3 = read(8)
|
54
|
+
else
|
55
|
+
@web_socket_version = "hixie-75"
|
56
|
+
@key3 = nil
|
57
|
+
end
|
58
|
+
if !@server.accepted_origin?(self.origin)
|
59
|
+
raise(WebSocket::Error,
|
60
|
+
("Unaccepted origin: %s (server.accepted_domains = %p)\n\n" +
|
61
|
+
"To accept this origin, write e.g. \n" +
|
62
|
+
" WebSocketServer.new(..., :accepted_domains => [%p]), or\n" +
|
63
|
+
" WebSocketServer.new(..., :accepted_domains => [\"*\"])\n") %
|
64
|
+
[self.origin, @server.accepted_domains, @server.origin_to_domain(self.origin)])
|
65
|
+
end
|
66
|
+
@handshaked = false
|
67
|
+
|
68
|
+
else # client
|
69
|
+
|
70
|
+
@web_socket_version = "hixie-76"
|
71
|
+
uri = arg.is_a?(String) ? URI.parse(arg) : arg
|
72
|
+
|
73
|
+
if uri.scheme == "ws"
|
74
|
+
default_port = 80
|
75
|
+
elsif uri.scheme = "wss"
|
76
|
+
default_port = 443
|
77
|
+
else
|
78
|
+
raise(WebSocket::Error, "unsupported scheme: #{uri.scheme}")
|
79
|
+
end
|
80
|
+
|
81
|
+
@path = (uri.path.empty? ? "/" : uri.path) + (uri.query ? "?" + uri.query : "")
|
82
|
+
host = uri.host + (uri.port == default_port ? "" : ":#{uri.port}")
|
83
|
+
origin = params[:origin] || "http://#{uri.host}"
|
84
|
+
key1 = generate_key()
|
85
|
+
key2 = generate_key()
|
86
|
+
key3 = generate_key3()
|
87
|
+
|
88
|
+
socket = TCPSocket.new(uri.host, uri.port || default_port)
|
89
|
+
|
90
|
+
if uri.scheme == "ws"
|
91
|
+
@socket = socket
|
92
|
+
else
|
93
|
+
@socket = ssl_handshake(socket)
|
94
|
+
end
|
95
|
+
|
96
|
+
write(
|
97
|
+
"GET #{@path} HTTP/1.1\r\n" +
|
98
|
+
"Upgrade: WebSocket\r\n" +
|
99
|
+
"Connection: Upgrade\r\n" +
|
100
|
+
"Host: #{host}\r\n" +
|
101
|
+
"Origin: #{origin}\r\n" +
|
102
|
+
"Sec-WebSocket-Key1: #{key1}\r\n" +
|
103
|
+
"Sec-WebSocket-Key2: #{key2}\r\n" +
|
104
|
+
"\r\n" +
|
105
|
+
"#{key3}")
|
106
|
+
flush()
|
107
|
+
|
108
|
+
line = gets().chomp()
|
109
|
+
raise(WebSocket::Error, "bad response: #{line}") if !(line =~ /\AHTTP\/1.1 101 /n)
|
110
|
+
read_header()
|
111
|
+
if (@header["sec-websocket-origin"] || "").downcase() != origin.downcase()
|
112
|
+
raise(WebSocket::Error,
|
113
|
+
"origin doesn't match: '#{@header["sec-websocket-origin"]}' != '#{origin}'")
|
114
|
+
end
|
115
|
+
reply_digest = read(16)
|
116
|
+
expected_digest = hixie_76_security_digest(key1, key2, key3)
|
117
|
+
if reply_digest != expected_digest
|
118
|
+
raise(WebSocket::Error,
|
119
|
+
"security digest doesn't match: %p != %p" % [reply_digest, expected_digest])
|
120
|
+
end
|
121
|
+
@handshaked = true
|
122
|
+
|
123
|
+
end
|
124
|
+
@received = []
|
125
|
+
@buffer = ""
|
126
|
+
@closing_started = false
|
127
|
+
end
|
128
|
+
|
129
|
+
attr_reader(:server, :header, :path)
|
130
|
+
|
131
|
+
def handshake(status = nil, header = {})
|
132
|
+
if @handshaked
|
133
|
+
raise(WebSocket::Error, "handshake has already been done")
|
134
|
+
end
|
135
|
+
status ||= "101 Switching Protocols"
|
136
|
+
def_header = {}
|
137
|
+
case @web_socket_version
|
138
|
+
when "hixie-75"
|
139
|
+
def_header["WebSocket-Origin"] = self.origin
|
140
|
+
def_header["WebSocket-Location"] = self.location
|
141
|
+
extra_bytes = ""
|
142
|
+
when "hixie-76"
|
143
|
+
def_header["Sec-WebSocket-Origin"] = self.origin
|
144
|
+
def_header["Sec-WebSocket-Location"] = self.location
|
145
|
+
extra_bytes = hixie_76_security_digest(
|
146
|
+
@header["Sec-WebSocket-Key1"], @header["Sec-WebSocket-Key2"], @key3)
|
147
|
+
else
|
148
|
+
def_header["Sec-WebSocket-Accept"] = security_digest(@header["sec-websocket-key"])
|
149
|
+
extra_bytes = ""
|
150
|
+
end
|
151
|
+
header = def_header.merge(header)
|
152
|
+
header_str = header.map(){ |k, v| "#{k}: #{v}\r\n" }.join("")
|
153
|
+
# Note that Upgrade and Connection must appear in this order.
|
154
|
+
write(
|
155
|
+
"HTTP/1.1 #{status}\r\n" +
|
156
|
+
"Upgrade: websocket\r\n" +
|
157
|
+
"Connection: Upgrade\r\n" +
|
158
|
+
"#{header_str}\r\n#{extra_bytes}")
|
159
|
+
flush()
|
160
|
+
@handshaked = true
|
161
|
+
end
|
162
|
+
|
163
|
+
def send(data)
|
164
|
+
if !@handshaked
|
165
|
+
raise(WebSocket::Error, "call WebSocket\#handshake first")
|
166
|
+
end
|
167
|
+
case @web_socket_version
|
168
|
+
when "hixie-75", "hixie-76"
|
169
|
+
data = force_encoding(data.dup(), "ASCII-8BIT")
|
170
|
+
write("\x00#{data}\xff")
|
171
|
+
flush()
|
172
|
+
else
|
173
|
+
send_frame(OPCODE_TEXT, data, !@server)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def receive()
|
178
|
+
if !@handshaked
|
179
|
+
raise(WebSocket::Error, "call WebSocket\#handshake first")
|
180
|
+
end
|
181
|
+
case @web_socket_version
|
182
|
+
|
183
|
+
when "hixie-75", "hixie-76"
|
184
|
+
packet = gets("\xff")
|
185
|
+
return nil if !packet
|
186
|
+
if packet =~ /\A\x00(.*)\xff\z/nm
|
187
|
+
return force_encoding($1, "UTF-8")
|
188
|
+
elsif packet == "\xff" && read(1) == "\x00" # closing
|
189
|
+
close(1005, "", :peer)
|
190
|
+
return nil
|
191
|
+
else
|
192
|
+
raise(WebSocket::Error, "input must be either '\\x00...\\xff' or '\\xff\\x00'")
|
193
|
+
end
|
194
|
+
|
195
|
+
else
|
196
|
+
begin
|
197
|
+
bytes = read(2).unpack("C*")
|
198
|
+
fin = (bytes[0] & 0x80) != 0
|
199
|
+
opcode = bytes[0] & 0x0f
|
200
|
+
mask = (bytes[1] & 0x80) != 0
|
201
|
+
plength = bytes[1] & 0x7f
|
202
|
+
if plength == 126
|
203
|
+
bytes = read(2)
|
204
|
+
plength = bytes.unpack("n")[0]
|
205
|
+
elsif plength == 127
|
206
|
+
bytes = read(8)
|
207
|
+
(high, low) = bytes.unpack("NN")
|
208
|
+
plength = high * (2 ** 32) + low
|
209
|
+
end
|
210
|
+
if @server && !mask
|
211
|
+
# Masking is required.
|
212
|
+
@socket.close()
|
213
|
+
raise(WebSocket::Error, "received unmasked data")
|
214
|
+
end
|
215
|
+
mask_key = mask ? read(4).unpack("C*") : nil
|
216
|
+
payload = read(plength)
|
217
|
+
payload = apply_mask(payload, mask_key) if mask
|
218
|
+
case opcode
|
219
|
+
when OPCODE_TEXT
|
220
|
+
return force_encoding(payload, "UTF-8")
|
221
|
+
when OPCODE_BINARY
|
222
|
+
raise(WebSocket::Error, "received binary data, which is not supported")
|
223
|
+
when OPCODE_CLOSE
|
224
|
+
close(1005, "", :peer)
|
225
|
+
return nil
|
226
|
+
when OPCODE_PING
|
227
|
+
raise(WebSocket::Error, "received ping, which is not supported")
|
228
|
+
when OPCODE_PONG
|
229
|
+
else
|
230
|
+
raise(WebSocket::Error, "received unknown opcode: %d" % opcode)
|
231
|
+
end
|
232
|
+
rescue EOFError
|
233
|
+
return nil
|
234
|
+
end
|
235
|
+
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def tcp_socket
|
240
|
+
return @socket
|
241
|
+
end
|
242
|
+
|
243
|
+
def host
|
244
|
+
return @header["host"]
|
245
|
+
end
|
246
|
+
|
247
|
+
def origin
|
248
|
+
case @web_socket_version
|
249
|
+
when "7", "8"
|
250
|
+
name = "sec-websocket-origin"
|
251
|
+
else
|
252
|
+
name = "origin"
|
253
|
+
end
|
254
|
+
if @header[name]
|
255
|
+
return @header[name]
|
256
|
+
else
|
257
|
+
raise(WebSocket::Error, "%s header is missing" % name)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
def location
|
262
|
+
return "ws://#{self.host}#{@path}"
|
263
|
+
end
|
264
|
+
|
265
|
+
# Does closing handshake.
|
266
|
+
def close(code = 1005, reason = "", origin = :self)
|
267
|
+
if !@closing_started
|
268
|
+
case @web_socket_version
|
269
|
+
when "hixie-75", "hixie-76"
|
270
|
+
write("\xff\x00")
|
271
|
+
else
|
272
|
+
if code == 1005
|
273
|
+
payload = ""
|
274
|
+
else
|
275
|
+
payload = [code].pack("n") + force_encoding(reason.dup(), "ASCII-8BIT")
|
276
|
+
end
|
277
|
+
send_frame(OPCODE_CLOSE, payload, false)
|
278
|
+
end
|
279
|
+
end
|
280
|
+
@socket.close() if origin == :peer
|
281
|
+
@closing_started = true
|
282
|
+
end
|
283
|
+
|
284
|
+
def close_socket()
|
285
|
+
@socket.close()
|
286
|
+
end
|
287
|
+
|
288
|
+
private
|
289
|
+
|
290
|
+
NOISE_CHARS = ("\x21".."\x2f").to_a() + ("\x3a".."\x7e").to_a()
|
291
|
+
|
292
|
+
def read_header()
|
293
|
+
@header = {}
|
294
|
+
while line = gets()
|
295
|
+
line = line.chomp()
|
296
|
+
break if line.empty?
|
297
|
+
if !(line =~ /\A(\S+): (.*)\z/n)
|
298
|
+
raise(WebSocket::Error, "invalid request: #{line}")
|
299
|
+
end
|
300
|
+
@header[$1] = $2
|
301
|
+
@header[$1.downcase()] = $2
|
302
|
+
end
|
303
|
+
if !(@header["upgrade"] =~ /\AWebSocket\z/i)
|
304
|
+
raise(WebSocket::Error, "invalid Upgrade: " + @header["upgrade"])
|
305
|
+
end
|
306
|
+
if !(@header["connection"] =~ /\AUpgrade\z/i)
|
307
|
+
raise(WebSocket::Error, "invalid Connection: " + @header["connection"])
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
def send_frame(opcode, payload, mask)
|
312
|
+
payload = force_encoding(payload.dup(), "ASCII-8BIT")
|
313
|
+
# Setting StringIO's encoding to ASCII-8BIT.
|
314
|
+
buffer = StringIO.new(force_encoding("", "ASCII-8BIT"))
|
315
|
+
write_byte(buffer, 0x80 | opcode)
|
316
|
+
masked_byte = mask ? 0x80 : 0x00
|
317
|
+
if payload.bytesize <= 125
|
318
|
+
write_byte(buffer, masked_byte | payload.bytesize)
|
319
|
+
elsif payload.bytesize < 2 ** 16
|
320
|
+
write_byte(buffer, masked_byte | 126)
|
321
|
+
buffer.write([payload.bytesize].pack("n"))
|
322
|
+
else
|
323
|
+
write_byte(buffer, masked_byte | 127)
|
324
|
+
buffer.write([payload.bytesize / (2 ** 32), payload.bytesize % (2 ** 32)].pack("NN"))
|
325
|
+
end
|
326
|
+
if mask
|
327
|
+
mask_key = Array.new(4){ rand(256) }
|
328
|
+
buffer.write(mask_key.pack("C*"))
|
329
|
+
payload = apply_mask(payload, mask_key)
|
330
|
+
end
|
331
|
+
buffer.write(payload)
|
332
|
+
write(buffer.string)
|
333
|
+
end
|
334
|
+
|
335
|
+
def gets(rs = $/)
|
336
|
+
line = @socket.gets(rs)
|
337
|
+
$stderr.printf("recv> %p\n", line) if WebSocket.debug
|
338
|
+
return line
|
339
|
+
end
|
340
|
+
|
341
|
+
def read(num_bytes)
|
342
|
+
str = @socket.read(num_bytes)
|
343
|
+
$stderr.printf("recv> %p\n", str) if WebSocket.debug
|
344
|
+
if str && str.bytesize == num_bytes
|
345
|
+
return str
|
346
|
+
else
|
347
|
+
raise(EOFError)
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
def write(data)
|
352
|
+
if WebSocket.debug
|
353
|
+
data.scan(/\G(.*?(\n|\z))/n) do
|
354
|
+
$stderr.printf("send> %p\n", $&) if !$&.empty?
|
355
|
+
end
|
356
|
+
end
|
357
|
+
@socket.write(data)
|
358
|
+
end
|
359
|
+
|
360
|
+
def flush()
|
361
|
+
@socket.flush()
|
362
|
+
end
|
363
|
+
|
364
|
+
def write_byte(buffer, byte)
|
365
|
+
buffer.write([byte].pack("C"))
|
366
|
+
end
|
367
|
+
|
368
|
+
def security_digest(key)
|
369
|
+
return Base64.encode64(Digest::SHA1.digest(key + WEB_SOCKET_GUID)).gsub(/\n/, "")
|
370
|
+
end
|
371
|
+
|
372
|
+
def hixie_76_security_digest(key1, key2, key3)
|
373
|
+
bytes1 = websocket_key_to_bytes(key1)
|
374
|
+
bytes2 = websocket_key_to_bytes(key2)
|
375
|
+
return Digest::MD5.digest(bytes1 + bytes2 + key3)
|
376
|
+
end
|
377
|
+
|
378
|
+
def apply_mask(payload, mask_key)
|
379
|
+
orig_bytes = payload.unpack("C*")
|
380
|
+
new_bytes = []
|
381
|
+
orig_bytes.each_with_index() do |b, i|
|
382
|
+
new_bytes.push(b ^ mask_key[i % 4])
|
383
|
+
end
|
384
|
+
return new_bytes.pack("C*")
|
385
|
+
end
|
386
|
+
|
387
|
+
def generate_key()
|
388
|
+
spaces = 1 + rand(12)
|
389
|
+
max = 0xffffffff / spaces
|
390
|
+
number = rand(max + 1)
|
391
|
+
key = (number * spaces).to_s()
|
392
|
+
(1 + rand(12)).times() do
|
393
|
+
char = NOISE_CHARS[rand(NOISE_CHARS.size)]
|
394
|
+
pos = rand(key.size + 1)
|
395
|
+
key[pos...pos] = char
|
396
|
+
end
|
397
|
+
spaces.times() do
|
398
|
+
pos = 1 + rand(key.size - 1)
|
399
|
+
key[pos...pos] = " "
|
400
|
+
end
|
401
|
+
return key
|
402
|
+
end
|
403
|
+
|
404
|
+
def generate_key3()
|
405
|
+
return [rand(0x100000000)].pack("N") + [rand(0x100000000)].pack("N")
|
406
|
+
end
|
407
|
+
|
408
|
+
def websocket_key_to_bytes(key)
|
409
|
+
num = key.gsub(/[^\d]/n, "").to_i() / key.scan(/ /).size
|
410
|
+
return [num].pack("N")
|
411
|
+
end
|
412
|
+
|
413
|
+
def force_encoding(str, encoding)
|
414
|
+
if str.respond_to?(:force_encoding)
|
415
|
+
return str.force_encoding(encoding)
|
416
|
+
else
|
417
|
+
return str
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
def ssl_handshake(socket)
|
422
|
+
ssl_context = OpenSSL::SSL::SSLContext.new()
|
423
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
|
424
|
+
ssl_socket.sync_close = true
|
425
|
+
ssl_socket.connect()
|
426
|
+
return ssl_socket
|
427
|
+
end
|
428
|
+
|
429
|
+
end
|
Binary file
|
Binary file
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require 'socketIO'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "socketio-client"
|
7
|
+
s.version = SocketIO::Client::VERSION
|
8
|
+
s.authors = ["Lyon"]
|
9
|
+
s.email = ["lyon@delorum.com"]
|
10
|
+
s.homepage = "http://github.com/lyondhill/socket.io-ruby-client"
|
11
|
+
s.summary = %q{A basic Socket.io client implememtation written for ruby}
|
12
|
+
s.description = %q{uses a very simple web socket}
|
13
|
+
|
14
|
+
s.rubyforge_project = "socketio-client"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
# s.add_development_dependency "rspec"
|
23
|
+
s.add_runtime_dependency "rest-client"
|
24
|
+
end
|
data/spec/parser_spec.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'socketIO'
|
2
|
+
|
3
|
+
describe Parser do
|
4
|
+
|
5
|
+
it 'should be able to decode all valid messages' do
|
6
|
+
Parser.decode("0").should == {type: "0"}
|
7
|
+
Parser.decode("1::").should == {type: "1", id: nil, end_point: nil, data: ""}
|
8
|
+
Parser.decode("2::").should == {type: "2", id: nil, end_point: nil, data: ""}
|
9
|
+
Parser.decode("3:::hay you").should == {type: "3", id: nil, end_point: nil, data: "hay you"}
|
10
|
+
Parser.decode("4:::{\"can\":\"youcall\"}").should == {type: "4", id: nil, end_point: nil, data: "{\"can\":\"youcall\"}"}
|
11
|
+
Parser.decode("5:::hay you").should == {type: "5", id: nil, end_point: nil, data: "hay you"}
|
12
|
+
Parser.decode("6:::").should == {type: "6", id: nil, end_point: nil, data: ""}
|
13
|
+
Parser.decode("7:::there is an error").should == {type: "7", id: nil, end_point: nil, data: "there is an error"}
|
14
|
+
Parser.decode("8:::").should == {type: "8", id: nil, end_point: nil, data: ""}
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should give a disconnect if bad input" do
|
18
|
+
Parser.decode("hay dude").should == {type: "0"}
|
19
|
+
Parser.decode("9").should == {type: "0"}
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
data/spec/setup/echo.js
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
var io = require('socket.io').listen(80);
|
2
|
+
|
3
|
+
io.sockets.on('connection', function(socket) {
|
4
|
+
|
5
|
+
socket.on('message', function(msg) {
|
6
|
+
socket.send(msg)
|
7
|
+
})
|
8
|
+
|
9
|
+
socket.on('event', function(data) {
|
10
|
+
socket.emit('event', data);
|
11
|
+
});
|
12
|
+
|
13
|
+
socket.on('dc', function() {
|
14
|
+
socket.close()
|
15
|
+
})
|
16
|
+
|
17
|
+
});
|
data/spec/setup/start.rb
ADDED
data/spec/setup/stop.rb
ADDED
File without changes
|
@@ -0,0 +1,46 @@
|
|
1
|
+
describe SocketIO do
|
2
|
+
|
3
|
+
before :all do
|
4
|
+
@client = SocketIO.connect("localhost", :sync => true)
|
5
|
+
end
|
6
|
+
|
7
|
+
it "can send a heartbeat" do
|
8
|
+
@client.send_heartbeat
|
9
|
+
# should not bonk. haha
|
10
|
+
end
|
11
|
+
|
12
|
+
it "gets a message back when it sends one" do
|
13
|
+
count = 0
|
14
|
+
@client.on_message do |msg|
|
15
|
+
count += 1 if msg == "hay dude"
|
16
|
+
end
|
17
|
+
@client.send("hay dude")
|
18
|
+
sleep 0.5
|
19
|
+
count.should == 1
|
20
|
+
end
|
21
|
+
|
22
|
+
it "gets an emit back when it hits the echo" do
|
23
|
+
count = 0
|
24
|
+
@client.on_event("event") do |data|
|
25
|
+
count += 1 if data[0] == {"first"=>"element", "second"=>"guy"}
|
26
|
+
end
|
27
|
+
@client.emit("event", {first: "element", second: "guy"})
|
28
|
+
sleep 0.5
|
29
|
+
count.should == 1
|
30
|
+
end
|
31
|
+
|
32
|
+
it "can have a block for every thing" do
|
33
|
+
@client.on_disconnect { }
|
34
|
+
@client.on_connect { }
|
35
|
+
@client.on_heartbeat { }
|
36
|
+
@client.on_message { |msg| }
|
37
|
+
@client.on_json_message { |json| }
|
38
|
+
@client.on_event('en') { |event_hash| }
|
39
|
+
@client.on_ack { }
|
40
|
+
@client.on_error { |data| }
|
41
|
+
@client.on_noop { }
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
|
46
|
+
end
|
metadata
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: socketio-client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Lyon
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-11-10 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rest-client
|
16
|
+
requirement: &2154912940 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2154912940
|
25
|
+
description: uses a very simple web socket
|
26
|
+
email:
|
27
|
+
- lyon@delorum.com
|
28
|
+
executables: []
|
29
|
+
extensions: []
|
30
|
+
extra_rdoc_files: []
|
31
|
+
files:
|
32
|
+
- Gemfile
|
33
|
+
- README.md
|
34
|
+
- Rakefile
|
35
|
+
- example/complex.rb
|
36
|
+
- example/remote_log.rb
|
37
|
+
- example/servers/complex.js
|
38
|
+
- example/servers/remote_log.js
|
39
|
+
- example/servers/simple.js
|
40
|
+
- example/simple.rb
|
41
|
+
- lib/SocketIO.rb
|
42
|
+
- lib/parser.rb
|
43
|
+
- lib/web_socket.rb
|
44
|
+
- pkg/socket.io-client-0.0.1.gem
|
45
|
+
- pkg/socketio-client-0.0.1.gem
|
46
|
+
- socketio-client.gemspec
|
47
|
+
- spec/parser_spec.rb
|
48
|
+
- spec/setup/echo.js
|
49
|
+
- spec/setup/start.rb
|
50
|
+
- spec/setup/stop.rb
|
51
|
+
- spec/socketio_spec.rb
|
52
|
+
homepage: http://github.com/lyondhill/socket.io-ruby-client
|
53
|
+
licenses: []
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options: []
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
none: false
|
60
|
+
requirements:
|
61
|
+
- - ! '>='
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0'
|
64
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
requirements: []
|
71
|
+
rubyforge_project: socketio-client
|
72
|
+
rubygems_version: 1.8.11
|
73
|
+
signing_key:
|
74
|
+
specification_version: 3
|
75
|
+
summary: A basic Socket.io client implememtation written for ruby
|
76
|
+
test_files:
|
77
|
+
- spec/parser_spec.rb
|
78
|
+
- spec/setup/echo.js
|
79
|
+
- spec/setup/start.rb
|
80
|
+
- spec/setup/stop.rb
|
81
|
+
- spec/socketio_spec.rb
|