webrocket 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/README.md +104 -0
- data/Rakefile +7 -0
- data/lib/webrocket.rb +56 -0
- data/lib/webrocket/version.rb +3 -0
- data/lib/webrocket/webrick-extender.rb +27 -0
- data/lib/webrocket/websocket.rb +164 -0
- data/sample/chat.html +47 -0
- data/sample/chat.rb +60 -0
- data/sample/echo.html +29 -0
- data/sample/echo.rb +35 -0
- data/test/test_webrocket.rb +115 -0
- data/webrocket.gemspec +24 -0
- metadata +102 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6f34536b978cbb71a56ae12ea296681c3f09e1d1
|
4
|
+
data.tar.gz: 210d6e96989a3657513fabc93278250800bfe491
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0e2587f050883ca1fee5a31c50b9a2c2e863aa2f4268e0bada83d66ddb159ef44be21aa1d8e8ff5e9cd358309f1b7f2654ce3283ad413647b82b6980625d58fb
|
7
|
+
data.tar.gz: 9aafb6ae9a6ae54dcd78a04e36222a06f7022c51bad724669d01b8d7c05ba5ee9be354455fffef982287d5536ea9298ff8127308b7bdbecc29ce13438c280c4e
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
[![Build Status](https://img.shields.io/travis/unak/WEBrocket.svg)](https://travis-ci.org/unak/WEBrocket)
|
2
|
+
[![Version ](https://img.shields.io/gem/v/webrocket.svg)](https://rubygems.org/gems/webrocket)
|
3
|
+
|
4
|
+
WEBrocket
|
5
|
+
=========
|
6
|
+
|
7
|
+
WebSocket extension for WEBrick.
|
8
|
+
This library is under development.
|
9
|
+
|
10
|
+
WebSocket specification is based on [RFC6455](http://www.rfc-editor.org/rfc/rfc6455.txt).
|
11
|
+
|
12
|
+
|
13
|
+
Requirements
|
14
|
+
------------
|
15
|
+
|
16
|
+
+ Ruby 1.9.3 or later.
|
17
|
+
|
18
|
+
|
19
|
+
Installation
|
20
|
+
------------
|
21
|
+
|
22
|
+
Add this line to your application's Gemfile:
|
23
|
+
|
24
|
+
gem 'WEBrocket'
|
25
|
+
|
26
|
+
And then execute:
|
27
|
+
|
28
|
+
$ bundle
|
29
|
+
|
30
|
+
Or install it yourself as:
|
31
|
+
|
32
|
+
$ gem install WEBrocket
|
33
|
+
|
34
|
+
|
35
|
+
How to use
|
36
|
+
----------
|
37
|
+
|
38
|
+
require "webrick"
|
39
|
+
require "webrocket"
|
40
|
+
|
41
|
+
# write listner
|
42
|
+
class Listener
|
43
|
+
def on_open(websocket)
|
44
|
+
# do somthing
|
45
|
+
end
|
46
|
+
|
47
|
+
def on_close(websocket)
|
48
|
+
# do somthing
|
49
|
+
end
|
50
|
+
|
51
|
+
def on_recv(websocket, data, type)
|
52
|
+
# do something
|
53
|
+
end
|
54
|
+
|
55
|
+
def on_shutdown
|
56
|
+
# do something
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
server = WEBrick::HTTPServer.new
|
61
|
+
server.mount_websocket("sample", Listener.new, "test")
|
62
|
+
server.start
|
63
|
+
|
64
|
+
See sample directory for more details.
|
65
|
+
|
66
|
+
|
67
|
+
Limitations
|
68
|
+
-----------
|
69
|
+
|
70
|
+
+ Fragmentation is not supported yet.
|
71
|
+
+ TLS is not supported yet.
|
72
|
+
|
73
|
+
|
74
|
+
License
|
75
|
+
-------
|
76
|
+
|
77
|
+
Copyright (c) 2013 NAKAMURA Usaku usa@garbagecollect.jp
|
78
|
+
|
79
|
+
Redistribution and use in source and binary forms, with or without
|
80
|
+
modification, are permitted provided that the following conditions are met:
|
81
|
+
|
82
|
+
1. Redistributions of source code must retain the above copyright notice,
|
83
|
+
this list of conditions and the following disclaimer.
|
84
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
85
|
+
this list of conditions and the following disclaimer in the documentation
|
86
|
+
and/or other materials provided with the distribution.
|
87
|
+
|
88
|
+
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY
|
89
|
+
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
90
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
91
|
+
DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY
|
92
|
+
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
93
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
94
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
95
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
96
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
97
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
98
|
+
|
99
|
+
|
100
|
+
Supplimentary
|
101
|
+
-------------
|
102
|
+
|
103
|
+
I've found that there are many projects named webrocket, but, sorry, I can't
|
104
|
+
stop naming this one the same name :)
|
data/Rakefile
ADDED
data/lib/webrocket.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require "base64"
|
2
|
+
require "digest/sha1"
|
3
|
+
require "webrocket/webrick-extender"
|
4
|
+
require "webrocket/websocket"
|
5
|
+
require "webrocket/version"
|
6
|
+
|
7
|
+
module WEBrocket
|
8
|
+
def self.attach(server, req, res, listener, subprotocol)
|
9
|
+
key = req["Sec-WebSocket-Key"]
|
10
|
+
unless key
|
11
|
+
res.status = WEBrick::HTTPStatus::RC_BAD_REQUEST
|
12
|
+
return
|
13
|
+
end
|
14
|
+
key += "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
15
|
+
res["Sec-WebSocket-Accept"] = Base64.strict_encode64(Digest::SHA1.digest(key))
|
16
|
+
|
17
|
+
if req["Sec-WebSocket-Version"].to_i < 13
|
18
|
+
res.status = WEBrick::HTTPStatus::RC_BAD_REQUEST
|
19
|
+
res["Sec-WebSocket-Version"] = 13
|
20
|
+
return
|
21
|
+
end
|
22
|
+
|
23
|
+
if req["Sec-WebSocket-Protocol"]
|
24
|
+
protocols = req["Sec-WebSocket-Protocol"].split(/\s*,\s*/)
|
25
|
+
if protocols.include?(subprotocol)
|
26
|
+
res["Sec-WebSocket-Protocol"] = subprotocol
|
27
|
+
elsif protocols.include?("null")
|
28
|
+
res["Sec-WebSocket-Protocol"] = "null"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
res.status = WEBrick::HTTPStatus::RC_SWITCHING_PROTOCOLS
|
33
|
+
res["Upgrade"] = "websocket"
|
34
|
+
#res["Connection"] = "Upgrade"
|
35
|
+
|
36
|
+
@keep_alive = true
|
37
|
+
|
38
|
+
res.define_singleton_method(:send_response) do |sock|
|
39
|
+
begin
|
40
|
+
setup_header
|
41
|
+
@header["connection"] = "Upgrade"
|
42
|
+
send_header(sock)
|
43
|
+
ws = WEBrocket::WebSocket.new(sock, listener, subprotocol)
|
44
|
+
server.add_shutdown_listener(ws.method(:on_shutdown))
|
45
|
+
ws.start
|
46
|
+
@keep_alive = false
|
47
|
+
rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ENOTCONN => ex
|
48
|
+
@logger.debug(ex)
|
49
|
+
@keep_alive = false
|
50
|
+
rescue StandardError => ex
|
51
|
+
@logger.error(ex)
|
52
|
+
@keep_alive = false
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "webrick/httpserver"
|
2
|
+
require "webrocket"
|
3
|
+
|
4
|
+
module WEBrick
|
5
|
+
class HTTPServer
|
6
|
+
def mount_websocket(path, listener, subprotocol)
|
7
|
+
mount_proc(path) do |req, res|
|
8
|
+
WEBrocket.attach(self, req, res, listener, subprotocol)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_shutdown_listener(prc = nil, &block)
|
13
|
+
@shutdown_listeners ||= []
|
14
|
+
@shutdown_listeners.push(prc || block)
|
15
|
+
end
|
16
|
+
|
17
|
+
alias _original_shutdown shutdown
|
18
|
+
def shutdown
|
19
|
+
if @shutdown_listeners
|
20
|
+
@shutdown_listeners.each do |listener|
|
21
|
+
listener.call
|
22
|
+
end
|
23
|
+
end
|
24
|
+
_original_shutdown
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
require "webrocket"
|
2
|
+
|
3
|
+
module WEBrocket
|
4
|
+
class WebSocket
|
5
|
+
class InternalError < StandardError; end
|
6
|
+
|
7
|
+
OP_CONTINUE = 0x0
|
8
|
+
OP_TEXT = 0x1
|
9
|
+
OP_BINARY = 0x2
|
10
|
+
OP_CLOSE = 0x8
|
11
|
+
OP_PING = 0x9
|
12
|
+
OP_PONG = 0xA
|
13
|
+
|
14
|
+
CLOSE_OK = [1000].pack("n")
|
15
|
+
CLOSE_CLOSED = [1001].pack("n")
|
16
|
+
CLOSE_PROTOCOL_ERROR = [1002].pack("n")
|
17
|
+
CLOSE_BROKEN_DATA = [1003].pack("n")
|
18
|
+
CLOSE_BROKEN_MESSAGE = [1007].pack("n")
|
19
|
+
CLOSE_POLICY_ERROR = [1008].pack("n")
|
20
|
+
CLOSE_TOO_BIG = [1009].pack("n")
|
21
|
+
CLOSE_UNKNOWN_ERROR = [1011].pack("n")
|
22
|
+
|
23
|
+
def initialize(sock, listener, subprotocol)
|
24
|
+
@sock = sock
|
25
|
+
@listener = listener
|
26
|
+
@subprotocol = subprotocol
|
27
|
+
@type = nil
|
28
|
+
@reason = nil
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_reader :subprotocol
|
32
|
+
|
33
|
+
def start
|
34
|
+
@listener.on_open(self) if @listener.respond_to?(:on_open)
|
35
|
+
begin
|
36
|
+
while true
|
37
|
+
r = IO.select([@sock])
|
38
|
+
recv_frame if r
|
39
|
+
end
|
40
|
+
rescue IOError
|
41
|
+
@reason ||= CLOSE_CLOSED
|
42
|
+
ensure
|
43
|
+
@reason ||= CLOSE_UNKNOWN_ERROR
|
44
|
+
send_close_frame(@reason) unless @sock.closed?
|
45
|
+
@listener.on_close(self) if @listener.respond_to?(:on_close)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def send(data, type = :text)
|
50
|
+
nil until IO.select([], [@sock])
|
51
|
+
case type
|
52
|
+
when :text
|
53
|
+
op = OP_TEXT
|
54
|
+
when :binary
|
55
|
+
op = OP_BINARY
|
56
|
+
else
|
57
|
+
raise InternalError, "unknown type: #{type}"
|
58
|
+
end
|
59
|
+
send_frame(op, data)
|
60
|
+
end
|
61
|
+
|
62
|
+
def on_shutdown
|
63
|
+
send_close_frame unless @sock.closed?
|
64
|
+
@listener.on_shutdown if @listener.respond_to?(:on_shutdown)
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
def send_close_frame(data = "")
|
69
|
+
send_frame(OP_CLOSE, data)
|
70
|
+
end
|
71
|
+
|
72
|
+
def send_frame(opcode, data = "")
|
73
|
+
data = data.dup.force_encoding('binary')
|
74
|
+
if data.bytesize <= 125
|
75
|
+
@sock.write([0x80 | opcode, data.bytesize].pack("cc") + data)
|
76
|
+
elsif data.bytesize <= 0xffff
|
77
|
+
@sock.write([0x80 | opcode, 126, data.bytesize].pack("ccn") + data)
|
78
|
+
else # under 64bit
|
79
|
+
@sock.write([0x80 | opcode, 127, data.bytesize / 0x1_0000_0000, data.bytesize % 0x1_0000_0000].pack("ccNN") + data)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def recv_frame
|
84
|
+
header = @sock.read(2)
|
85
|
+
return unless header
|
86
|
+
if header.bytesize != 2
|
87
|
+
@reason = CLOSE_PROTOCOL_ERROR
|
88
|
+
raise InternalError, "something wrong: header = '#{header}'"
|
89
|
+
end
|
90
|
+
h0, h1 = header.unpack("cc")
|
91
|
+
len = h1 & 0x7F
|
92
|
+
if len == 126
|
93
|
+
tmp = @sock.read(2)
|
94
|
+
if !tmp || tmp.bytesize != 2
|
95
|
+
@reason = CLOSE_PROTOCOL_ERROR
|
96
|
+
raise InternalError, "something wrong: size = '#{tmp}'"
|
97
|
+
end
|
98
|
+
len = tmp.unpack("n")
|
99
|
+
elsif len == 127
|
100
|
+
tmp = @sock.read(8)
|
101
|
+
if !tmp || tmp.bytesize != 8
|
102
|
+
@reason = CLOSE_PROTOCOL_ERROR
|
103
|
+
raise InternalError, "something wrong: size = '#{tmp}'"
|
104
|
+
end
|
105
|
+
l0, l1 = tmp.unpack("NN")
|
106
|
+
len = l0 * 0x1_0000_0000 + l1
|
107
|
+
end
|
108
|
+
if len > 0
|
109
|
+
if h1 & 0x80 != 0
|
110
|
+
key = @sock.read(4)
|
111
|
+
if !key || key.bytesize != 4
|
112
|
+
@reason = CLOSE_PROTOCOL_ERROR
|
113
|
+
raise InternalError, "something wrong: key = '#{key}'"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
data = @sock.read(len)
|
118
|
+
if !data || data.bytesize != len
|
119
|
+
@reason = CLOSE_PROTOCOL_ERROR
|
120
|
+
raise InternalError, "something wrong"
|
121
|
+
end
|
122
|
+
|
123
|
+
if h1 & 0x80 != 0
|
124
|
+
data = data.bytes.map.with_index{|b, i| (b ^ (key[i % 4].ord)).chr}.join
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
op = h0 & 0xF
|
129
|
+
|
130
|
+
if h0 & 0x80 == 0
|
131
|
+
# XXX
|
132
|
+
@reason = CLOSE_TOO_BIG
|
133
|
+
raise InternalError, "fragmented data is not supported yet"
|
134
|
+
end
|
135
|
+
|
136
|
+
case op
|
137
|
+
when OP_CONTINUE, OP_TEXT, OP_BINARY
|
138
|
+
if op == OP_TEXT
|
139
|
+
@type = :text
|
140
|
+
elsif op == OP_BINARY
|
141
|
+
@type = :binary
|
142
|
+
elsif !@type
|
143
|
+
@reason = CLOSE_PROTOCOL_ERROR
|
144
|
+
raise InternalError, "continued data at the first"
|
145
|
+
end
|
146
|
+
if @type == :text
|
147
|
+
data.force_encoding('utf-8')
|
148
|
+
raise InternalError, "invalid encoding" unless data.valid_encoding?
|
149
|
+
end
|
150
|
+
@listener.on_message(self, data, @type) if @listener.respond_to?(:on_message)
|
151
|
+
when OP_CLOSE
|
152
|
+
@reason = CLOSE_OK
|
153
|
+
raise IOError
|
154
|
+
when OP_PING
|
155
|
+
send_frame(OP_PONG, data)
|
156
|
+
when OP_PONG
|
157
|
+
# nothing to do
|
158
|
+
else
|
159
|
+
@reason = CLOSE_PROTOCOL_ERROR
|
160
|
+
raise InternalError, "unknown opcode: 0x%x" % op
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
data/sample/chat.html
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
<html lang="ja">
|
2
|
+
<head>
|
3
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
4
|
+
<title>chat sample</title>
|
5
|
+
<script type="text/javascript"><!--
|
6
|
+
var conn;
|
7
|
+
|
8
|
+
function escapeHTML(str) {
|
9
|
+
return str.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
10
|
+
}
|
11
|
+
|
12
|
+
function login() {
|
13
|
+
console.log("login");
|
14
|
+
var user = document.getElementById("text").value;
|
15
|
+
if (user != "") {
|
16
|
+
document.getElementById("text").value = "";
|
17
|
+
document.getElementById("send").innerHTML = "send";
|
18
|
+
document.getElementById("send").setAttribute("onclick", "message();");
|
19
|
+
|
20
|
+
conn = new WebSocket("ws://localhost:10080/chat", "chat");
|
21
|
+
conn.onopen = function() {
|
22
|
+
conn.send("\uFEFF" + user);
|
23
|
+
};
|
24
|
+
conn.onerror = function(error) {
|
25
|
+
var elem = document.getElementById("server");
|
26
|
+
elem.innerHTML = "<b>WebSocket error: " + escapeHTML(error) + "</b><br>\n" + elem.innerHTML;
|
27
|
+
};
|
28
|
+
conn.onmessage = function(v) {
|
29
|
+
var elem = document.getElementById("server");
|
30
|
+
elem.innerHTML = escapeHTML(v.data) + "<br>\n" + elem.innerHTML;
|
31
|
+
};
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
function message() {
|
36
|
+
console.log("message");
|
37
|
+
conn.send(document.getElementById("text").value);
|
38
|
+
document.getElementById("text").value = "";
|
39
|
+
}
|
40
|
+
--></script>
|
41
|
+
</head>
|
42
|
+
<body>
|
43
|
+
<input id="text" value=""/><button id="send" onclick="login();">login</button>
|
44
|
+
<p id="server">
|
45
|
+
</p>
|
46
|
+
</body>
|
47
|
+
</html>
|
data/sample/chat.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
#!ruby
|
2
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", File.dirname(__FILE__)))
|
3
|
+
require "webrick"
|
4
|
+
require "webrocket"
|
5
|
+
|
6
|
+
# write listner
|
7
|
+
class Listener
|
8
|
+
def initialize
|
9
|
+
@clients = []
|
10
|
+
@m = Mutex.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def on_open(websocket)
|
14
|
+
p [:open, websocket]
|
15
|
+
@m.synchronize do
|
16
|
+
@clients.push(websocket)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def on_close(websocket)
|
21
|
+
p [:close, websocket]
|
22
|
+
@m.synchronize do
|
23
|
+
user = websocket.instance_variable_get(:@chat_user)
|
24
|
+
if user
|
25
|
+
@clients.each do |cl|
|
26
|
+
cl.send("logout #{user}", :text)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
@clients.delete(websocket)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def on_message(websocket, data, type)
|
34
|
+
user = websocket.instance_variable_get(:@chat_user)
|
35
|
+
@m.synchronize do
|
36
|
+
@clients.each do |cl|
|
37
|
+
if user
|
38
|
+
cl.send("#{user}: #{data}", :text)
|
39
|
+
else
|
40
|
+
cl.send("login #{data}", :text)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
unless user
|
45
|
+
websocket.instance_variable_set(:@chat_user, data)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def on_shutdown
|
50
|
+
p [:shutdown]
|
51
|
+
# nothing to do
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
server = WEBrick::HTTPServer.new(Port: 10080)
|
56
|
+
server.mount_websocket("/chat", Listener.new, "chat")
|
57
|
+
trap(:INT) do
|
58
|
+
server.shutdown
|
59
|
+
end
|
60
|
+
server.start
|
data/sample/echo.html
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
<html lang="ja">
|
2
|
+
<head>
|
3
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
4
|
+
<script type="text/javascript"><!--
|
5
|
+
function escapeHTML(str){
|
6
|
+
return str.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
7
|
+
}
|
8
|
+
|
9
|
+
var conn = new WebSocket("ws://localhost:10080/echo", "echo");
|
10
|
+
conn.onopen = function(){
|
11
|
+
var elem = document.getElementById("server");
|
12
|
+
elem.innerHTML = "<b>connected</b><br>\n" + elem.innerHTML;
|
13
|
+
};
|
14
|
+
conn.onerror = function(error){
|
15
|
+
var elem = document.getElementById("server");
|
16
|
+
elem.innerHTML = "<b>WebSocket error: " + escapeHTML(error) + "</b><br>\n" + elem.innerHTML;
|
17
|
+
};
|
18
|
+
conn.onmessage = function(v){
|
19
|
+
var elem = document.getElementById("server");
|
20
|
+
elem.innerHTML = escapeHTML(v.data) + "<br>\n" + elem.innerHTML;
|
21
|
+
}
|
22
|
+
--></script>
|
23
|
+
</head>
|
24
|
+
<body>
|
25
|
+
<input id="text" value="hello!"/><button onclick="conn.send(document.getElementById('text').value);">send</button>
|
26
|
+
<p id="server">
|
27
|
+
</p>
|
28
|
+
</body>
|
29
|
+
</html>
|
data/sample/echo.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
#!ruby
|
2
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", File.dirname(__FILE__)))
|
3
|
+
require "webrick"
|
4
|
+
require "webrocket"
|
5
|
+
|
6
|
+
# write listner
|
7
|
+
class Listener
|
8
|
+
def on_open(websocket)
|
9
|
+
# do somthing
|
10
|
+
p :open
|
11
|
+
end
|
12
|
+
|
13
|
+
def on_close(websocket)
|
14
|
+
# do somthing
|
15
|
+
p :close
|
16
|
+
end
|
17
|
+
|
18
|
+
def on_message(websocket, data, type)
|
19
|
+
# do something
|
20
|
+
p [:message, data, type]
|
21
|
+
websocket.send(data, type)
|
22
|
+
end
|
23
|
+
|
24
|
+
def on_shutdown
|
25
|
+
# do something
|
26
|
+
p :shutdown
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
server = WEBrick::HTTPServer.new(Port: 10080)
|
31
|
+
server.mount_websocket("/echo", Listener.new, "echo")
|
32
|
+
trap(:INT) do
|
33
|
+
server.shutdown
|
34
|
+
end
|
35
|
+
server.start
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'net/http'
|
3
|
+
require 'socket'
|
4
|
+
require 'test/unit'
|
5
|
+
require 'webrick'
|
6
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', File.dirname(__FILE__)))
|
7
|
+
require 'webrocket'
|
8
|
+
|
9
|
+
class TestWEBrocket < Test::Unit::TestCase
|
10
|
+
def start_test_server(&block)
|
11
|
+
logger = Object.new
|
12
|
+
logger.instance_eval do
|
13
|
+
def <<(msg)
|
14
|
+
@log ||= ''
|
15
|
+
@log << msg
|
16
|
+
end
|
17
|
+
|
18
|
+
def log
|
19
|
+
@log ||= ''
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
server = WEBrick::HTTPServer.new(
|
24
|
+
:BindAddress => "127.0.0.1",
|
25
|
+
:Port => 0,
|
26
|
+
:DocumentRoot => File.dirname(__FILE__),
|
27
|
+
:ShutdownSocketWithoutClose => true,
|
28
|
+
:ServerType => Thread,
|
29
|
+
:Logger => WEBrick::Log.new(logger),
|
30
|
+
:AccessLog => [[logger, ""]]
|
31
|
+
)
|
32
|
+
|
33
|
+
begin
|
34
|
+
th = server.start
|
35
|
+
addr = server.listeners[0].addr
|
36
|
+
block.call(server, addr[3], addr[1], logger)
|
37
|
+
ensure
|
38
|
+
server.shutdown
|
39
|
+
th.join
|
40
|
+
end
|
41
|
+
|
42
|
+
logger.log
|
43
|
+
end
|
44
|
+
|
45
|
+
class Listener
|
46
|
+
def initialize(open = proc{}, close = proc{}, message = proc{}, shutdown = proc{})
|
47
|
+
@open = open
|
48
|
+
@close = close
|
49
|
+
@message = message
|
50
|
+
@shutdown = shutdown
|
51
|
+
end
|
52
|
+
|
53
|
+
def on_open(websocket)
|
54
|
+
@open.call(websocket)
|
55
|
+
end
|
56
|
+
|
57
|
+
def on_close(websocket)
|
58
|
+
@close.call(websocket)
|
59
|
+
end
|
60
|
+
|
61
|
+
def on_message(websocket, data, type)
|
62
|
+
@message.call(websocket, data, type)
|
63
|
+
end
|
64
|
+
|
65
|
+
def on_shutdown
|
66
|
+
@shutdown.call
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_websocket
|
71
|
+
shutdowned = false # yes, I know shutdown is a noun
|
72
|
+
start_test_server do |server, host, port, logger|
|
73
|
+
http = Net::HTTP.new(host, port)
|
74
|
+
|
75
|
+
req = Net::HTTP::Get.new('/')
|
76
|
+
http.request(req) do |res|
|
77
|
+
assert_kind_of Net::HTTPOK, res.response
|
78
|
+
end
|
79
|
+
|
80
|
+
opened = false
|
81
|
+
open = lambda {|websocket|
|
82
|
+
opened = true
|
83
|
+
}
|
84
|
+
close = lambda {|websocket|
|
85
|
+
opened = false
|
86
|
+
}
|
87
|
+
message = lambda {|websocket, data, type|
|
88
|
+
}
|
89
|
+
shutdown = lambda {
|
90
|
+
shutdowned = true
|
91
|
+
}
|
92
|
+
server.mount_websocket('/test', Listener.new(open, close, message, shutdown), 'test')
|
93
|
+
|
94
|
+
req = Net::HTTP::Get.new('/test')
|
95
|
+
req['Upgrade'] = 'websocket'
|
96
|
+
req['Connection'] = 'Upgrade'
|
97
|
+
req['Sec-WebSocket-Version'] = '13'
|
98
|
+
req['Sec-WebSocket-Key'] = Base64.encode64('0'*16)
|
99
|
+
|
100
|
+
refute opened
|
101
|
+
sock = nil
|
102
|
+
http.request(req) do |res|
|
103
|
+
assert_kind_of Net::HTTPSwitchProtocol, res.response
|
104
|
+
sock = http.instance_variable_get(:@socket).io.dup
|
105
|
+
end
|
106
|
+
|
107
|
+
assert opened
|
108
|
+
|
109
|
+
sock.shutdown
|
110
|
+
|
111
|
+
refute shutdowned
|
112
|
+
end
|
113
|
+
assert shutdowned
|
114
|
+
end
|
115
|
+
end
|
data/webrocket.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'webrocket/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "webrocket"
|
8
|
+
spec.version = WEBrocket::VERSION
|
9
|
+
spec.authors = ["U.Nakamura"]
|
10
|
+
spec.email = ["usa@garbagecollect.jp"]
|
11
|
+
spec.description = %q{WebSocket extension for WEBrick}
|
12
|
+
spec.summary = %q{WebSocket extension for WEBrick}
|
13
|
+
spec.homepage = "https://github.com/unak/WEBRocket"
|
14
|
+
spec.license = "BSD-2-Clause"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "test-unit"
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: webrocket
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- U.Nakamura
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-04-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: test-unit
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: WebSocket extension for WEBrick
|
56
|
+
email:
|
57
|
+
- usa@garbagecollect.jp
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".gitignore"
|
63
|
+
- ".travis.yml"
|
64
|
+
- Gemfile
|
65
|
+
- README.md
|
66
|
+
- Rakefile
|
67
|
+
- lib/webrocket.rb
|
68
|
+
- lib/webrocket/version.rb
|
69
|
+
- lib/webrocket/webrick-extender.rb
|
70
|
+
- lib/webrocket/websocket.rb
|
71
|
+
- sample/chat.html
|
72
|
+
- sample/chat.rb
|
73
|
+
- sample/echo.html
|
74
|
+
- sample/echo.rb
|
75
|
+
- test/test_webrocket.rb
|
76
|
+
- webrocket.gemspec
|
77
|
+
homepage: https://github.com/unak/WEBRocket
|
78
|
+
licenses:
|
79
|
+
- BSD-2-Clause
|
80
|
+
metadata: {}
|
81
|
+
post_install_message:
|
82
|
+
rdoc_options: []
|
83
|
+
require_paths:
|
84
|
+
- lib
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
requirements: []
|
96
|
+
rubyforge_project:
|
97
|
+
rubygems_version: 2.2.2
|
98
|
+
signing_key:
|
99
|
+
specification_version: 4
|
100
|
+
summary: WebSocket extension for WEBrick
|
101
|
+
test_files:
|
102
|
+
- test/test_webrocket.rb
|