em-websocket-server 0.13 → 0.15
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +98 -38
- data/em-websocket-server.gemspec +17 -27
- data/lib/em-websocket-server.rb +15 -0
- data/lib/em-websocket-server/protocol/version76.rb +49 -0
- data/lib/em-websocket-server/request.rb +109 -0
- data/lib/em-websocket-server/server.rb +157 -0
- metadata +9 -21
- data/examples/chat.html +0 -37
- data/examples/chat.rb +0 -70
- data/examples/tictactoe/tictactoe.html +0 -51
- data/examples/tictactoe/tictactoe.js +0 -130
- data/examples/tictactoe/tictactoe.rb +0 -213
- data/examples/timesync.html +0 -29
- data/examples/timesync.rb +0 -37
- data/lib/web_socket.rb +0 -13
- data/lib/web_socket/client.rb +0 -71
- data/lib/web_socket/frame.rb +0 -20
- data/lib/web_socket/server.rb +0 -202
- data/lib/web_socket/util.rb +0 -55
data/README.markdown
CHANGED
@@ -9,48 +9,108 @@
|
|
9
9
|
##Dependencies
|
10
10
|
- eventmachine http://github.com/eventmachine/eventmachine
|
11
11
|
|
12
|
-
##
|
12
|
+
##Explain
|
13
|
+
To leverage em-websocket-server, you simply need to extend EM::WebSocket::Server
|
14
|
+
and register the server with eventmachine. When a client connects, EventMachine will
|
15
|
+
create a new instance of your class, and allow your application specific code to be
|
16
|
+
executed in the context of said instance.
|
17
|
+
|
18
|
+
##Methods to override:
|
19
|
+
|
20
|
+
#called on exception
|
21
|
+
on_error error
|
22
|
+
|
23
|
+
#called when a client sends a message
|
24
|
+
on_receive msg
|
25
|
+
|
26
|
+
#called when a client connects
|
27
|
+
on_connect
|
28
|
+
|
29
|
+
#called when a client is disconnected
|
30
|
+
on_disconnect
|
31
|
+
|
32
|
+
##Other useful methods
|
33
|
+
|
34
|
+
#send a message
|
35
|
+
send_message msg
|
36
|
+
|
37
|
+
#close the connection
|
38
|
+
unbind
|
39
|
+
|
40
|
+
##Macros
|
41
|
+
macros are used to configure your application server.
|
42
|
+
|
43
|
+
class MySweetHandler < EM::WebSocket::Server
|
44
|
+
|
45
|
+
#secure incoming connections
|
46
|
+
secure
|
47
|
+
|
48
|
+
#secure incoming connections, with given key/cert
|
49
|
+
secure {
|
50
|
+
:private_key_file => "/path/to/private/key",
|
51
|
+
:cert_chain_file => "/path/to/ssl/certificate"
|
52
|
+
}
|
53
|
+
|
54
|
+
#provide a flash socket policy
|
55
|
+
flash_policy "/usr/local/policies/domain.com/crossdomain.xml"
|
56
|
+
|
57
|
+
end
|
13
58
|
|
14
|
-
Not yet... coming soon
|
15
59
|
|
16
60
|
##Quick Example
|
17
61
|
|
18
|
-
|
19
|
-
|
20
|
-
require 'json'
|
62
|
+
require "rubygems"
|
63
|
+
require "em-websocket-server"
|
21
64
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
65
|
+
class EchoServer < EM::WebSocket::Server
|
66
|
+
|
67
|
+
def on_connect
|
68
|
+
EM::WebSocket::Log.debug "Connected"
|
69
|
+
end
|
70
|
+
|
71
|
+
def on_receive msg
|
72
|
+
send_message msg
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
EM.run do
|
78
|
+
EM.start_server "0.0.0.0", 8000, EchoServer
|
79
|
+
end
|
80
|
+
|
81
|
+
##SSL
|
82
|
+
|
83
|
+
class SecureEchoServer < EM::WebSocket::Server
|
84
|
+
|
85
|
+
#provide cert and key
|
86
|
+
secure {
|
87
|
+
:private_key_file => "/path/to/private/key",
|
88
|
+
:cert_chain_file => "/path/to/ssl/certificate"
|
89
|
+
}
|
90
|
+
|
91
|
+
...
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
EM.run do
|
96
|
+
EM.start_server "0.0.0.0", 443, SecureEchoServer
|
97
|
+
end
|
98
|
+
|
99
|
+
##Custom Flash Policy
|
100
|
+
|
101
|
+
class FlashyEchoServer < EM::WebSocket::Server
|
102
|
+
flash_policy "/usr/local/policies/domain.com/crossdomain.xml"
|
103
|
+
end
|
104
|
+
|
105
|
+
EM.run do
|
106
|
+
EM.start_server "0.0.0.0", 8000, FlashyEchoServer
|
107
|
+
end
|
108
|
+
|
109
|
+
##Todo
|
110
|
+
* Testing
|
111
|
+
* Better inline documentation
|
112
|
+
* Web client library with flash based fallback
|
53
113
|
|
54
114
|
##Thanks
|
55
|
-
sidonath
|
56
|
-
TheBreeze
|
115
|
+
* sidonath
|
116
|
+
* TheBreeze
|
data/em-websocket-server.gemspec
CHANGED
@@ -1,32 +1,22 @@
|
|
1
1
|
spec = Gem::Specification.new do |s|
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
s.has_rdoc = false
|
10
|
-
|
2
|
+
s.name = 'em-websocket-server'
|
3
|
+
s.version = '0.15'
|
4
|
+
s.date = '2010-08-30'
|
5
|
+
s.summary = 'An evented ruby websocket server built on top of EventMachine'
|
6
|
+
s.email = "dan.simpson@gmail.com"
|
7
|
+
s.homepage = "http://github.com/dansimpson/em-websocket-server"
|
8
|
+
s.description = "An evented ruby websocket server built on top of EventMachine"
|
11
9
|
|
12
|
-
|
13
|
-
|
10
|
+
s.authors = ["Dan Simpson"]
|
11
|
+
s.add_dependency('eventmachine', '>= 0.12.10')
|
14
12
|
|
15
13
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
"examples/tictactoe/tictactoe.js",
|
25
|
-
"examples/tictactoe/tictactoe.rb",
|
26
|
-
"lib/web_socket.rb",
|
27
|
-
"lib/web_socket/client.rb",
|
28
|
-
"lib/web_socket/server.rb",
|
29
|
-
"lib/web_socket/frame.rb",
|
30
|
-
"lib/web_socket/util.rb"
|
31
|
-
]
|
14
|
+
s.files = [
|
15
|
+
"README.markdown",
|
16
|
+
"em-websocket-server.gemspec",
|
17
|
+
"lib/em-websocket-server.rb",
|
18
|
+
"lib/em-websocket-server/server.rb",
|
19
|
+
"lib/em-websocket-server/request.rb",
|
20
|
+
"lib/em-websocket-server/protocol/version76.rb"
|
21
|
+
]
|
32
22
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "eventmachine"
|
3
|
+
require "logger"
|
4
|
+
require "digest"
|
5
|
+
|
6
|
+
module EM
|
7
|
+
module WebSocket
|
8
|
+
Version = 0.50
|
9
|
+
Log = Logger.new STDOUT
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
require "em-websocket-server/server.rb"
|
14
|
+
require "em-websocket-server/request.rb"
|
15
|
+
require "em-websocket-server/protocol/version76.rb"
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module EM
|
2
|
+
module WebSocket
|
3
|
+
module Protocol
|
4
|
+
module Version76
|
5
|
+
|
6
|
+
# generate protocol 76 compatible response headers
|
7
|
+
def response
|
8
|
+
response = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
|
9
|
+
response << "Upgrade: WebSocket\r\n"
|
10
|
+
response << "Connection: Upgrade\r\n"
|
11
|
+
response << "Sec-WebSocket-Origin: #{origin}\r\n"
|
12
|
+
response << "Sec-WebSocket-Location: #{scheme}://#{host}#{path}\r\n"
|
13
|
+
|
14
|
+
if protocol
|
15
|
+
response << "Sec-WebSocket-Protocol: #{protocol}\r\n"
|
16
|
+
end
|
17
|
+
|
18
|
+
response << "\r\n"
|
19
|
+
response << Digest::MD5.digest(keyset)
|
20
|
+
|
21
|
+
response
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
# generate a keyset from the 3 secure keys
|
27
|
+
def keyset
|
28
|
+
[:sec_websocket_key1,:sec_websocket_key2].collect { |k|
|
29
|
+
partify(@headers[k])
|
30
|
+
}.push(@headers[:sec_websocket_key3]).join
|
31
|
+
end
|
32
|
+
|
33
|
+
# decode a websocket key and create a token for
|
34
|
+
# use in the response
|
35
|
+
# +key+ the key value to decode
|
36
|
+
def partify key
|
37
|
+
nums = key.scan(/[0-9]/).join.to_i
|
38
|
+
spaces = key.scan(/ /).size
|
39
|
+
|
40
|
+
raise "Key Error: #{key} has no spaces" if spaces == 0
|
41
|
+
raise "Key Error: #{key} nums #{nums} is not an integral multiple of #{spaces}" if (nums % spaces) != 0
|
42
|
+
|
43
|
+
[nums / spaces].pack("N*")
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module EM
|
2
|
+
module WebSocket
|
3
|
+
|
4
|
+
class Request
|
5
|
+
|
6
|
+
Path = /^GET (\/[^\s]*) HTTP\/[\d|\.]+$/.freeze
|
7
|
+
Header = /^([^:]+):\s*(.+)$/.freeze
|
8
|
+
|
9
|
+
# create a new request object
|
10
|
+
# +data+ the string value of the HTTP headers for parsing
|
11
|
+
def initialize data=nil
|
12
|
+
parse(data) if data
|
13
|
+
end
|
14
|
+
|
15
|
+
# parse the HTTP headers from an incoming
|
16
|
+
# request into actionable information
|
17
|
+
# +data+ The header data as a string
|
18
|
+
def parse data
|
19
|
+
|
20
|
+
lines = data.split("\r\n")
|
21
|
+
|
22
|
+
if Path =~ lines.shift
|
23
|
+
@headers = {
|
24
|
+
:path => $1
|
25
|
+
}
|
26
|
+
else
|
27
|
+
raise "Invalid request: #{data}"
|
28
|
+
end
|
29
|
+
|
30
|
+
#breaks when we get to the empty line
|
31
|
+
while((line = lines.shift) && !line.empty?)
|
32
|
+
if Header =~ line
|
33
|
+
self[$1] = $2
|
34
|
+
else
|
35
|
+
raise "Invalid header: #{line}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
if is_secure?
|
40
|
+
if lines.empty?
|
41
|
+
raise "Key 3 is required for protocol version 76"
|
42
|
+
end
|
43
|
+
@headers[:sec_websocket_key3] = lines.last
|
44
|
+
extend Protocol::Version76
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
# generate a response for this request
|
50
|
+
# may be overridden by other protocol modules
|
51
|
+
def response
|
52
|
+
response = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
|
53
|
+
response << "Upgrade: WebSocket\r\n"
|
54
|
+
response << "Connection: Upgrade\r\n"
|
55
|
+
response << "WebSocket-Origin: #{origin}\r\n"
|
56
|
+
response << "WebSocket-Location: #{scheme}://#{host}#{path}\r\n"
|
57
|
+
response << "\r\n"
|
58
|
+
response
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
# returns a header value
|
63
|
+
# +key+ the symbol value of the header field
|
64
|
+
def [](key)
|
65
|
+
@headers[key.to_sym]
|
66
|
+
end
|
67
|
+
|
68
|
+
# sets the header value
|
69
|
+
# +key+ the header field name
|
70
|
+
# +val+ the value of said field
|
71
|
+
def []=(key,val)
|
72
|
+
@headers[key.downcase.gsub(/-/,"_").to_sym] = val
|
73
|
+
end
|
74
|
+
|
75
|
+
# is the websocket connection supplying secure
|
76
|
+
# web socket fields in the header
|
77
|
+
def is_secure?
|
78
|
+
@headers.has_key? :sec_websocket_key1
|
79
|
+
end
|
80
|
+
|
81
|
+
# the websocket protocol that is used
|
82
|
+
def protocol
|
83
|
+
@headers[:sec_websocket_protocol]
|
84
|
+
end
|
85
|
+
|
86
|
+
# the origin domain of the connected client
|
87
|
+
def origin
|
88
|
+
@headers[:origin]
|
89
|
+
end
|
90
|
+
|
91
|
+
# the host which the client connected to
|
92
|
+
def host
|
93
|
+
@headers[:host]
|
94
|
+
end
|
95
|
+
|
96
|
+
# the request path portion of the request URI
|
97
|
+
def path
|
98
|
+
@headers[:path]
|
99
|
+
end
|
100
|
+
|
101
|
+
# the websocket scheme, either ws, or wss for
|
102
|
+
# a TLS secured connection
|
103
|
+
def scheme
|
104
|
+
"ws"
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
module EM
|
2
|
+
module WebSocket
|
3
|
+
class Server < EM::Connection
|
4
|
+
|
5
|
+
Pack = /\000([^\377]*)\377/.freeze
|
6
|
+
Frame = /^[\x00]|[\xff]$/.freeze
|
7
|
+
|
8
|
+
attr_accessor :connected, :request
|
9
|
+
|
10
|
+
def initialize *args
|
11
|
+
super
|
12
|
+
@request = nil
|
13
|
+
@buffer = ""
|
14
|
+
@connected = false
|
15
|
+
end
|
16
|
+
|
17
|
+
# do not override!
|
18
|
+
def post_init
|
19
|
+
start_tls(self.class.tls_opts) if self.class.secure?
|
20
|
+
end
|
21
|
+
|
22
|
+
# close the connection
|
23
|
+
def unbind
|
24
|
+
on_disconnect
|
25
|
+
end
|
26
|
+
|
27
|
+
# send a message to the websocket client
|
28
|
+
# +msg+ the message the client should receive
|
29
|
+
def send_message msg
|
30
|
+
send_data "\x00#{msg}\xff"
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
# override
|
36
|
+
def on_receive msg
|
37
|
+
end
|
38
|
+
|
39
|
+
# override this method
|
40
|
+
def on_connect
|
41
|
+
end
|
42
|
+
|
43
|
+
# override this method
|
44
|
+
def on_disconnect
|
45
|
+
end
|
46
|
+
|
47
|
+
# override this method
|
48
|
+
def on_error ex
|
49
|
+
Log.fatal ex
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# called when the web socket connection
|
55
|
+
# is fully ready
|
56
|
+
def on_ready
|
57
|
+
@connected = true
|
58
|
+
on_connect
|
59
|
+
end
|
60
|
+
|
61
|
+
# when the connection receives data from the client
|
62
|
+
# we either handshake or handle the message at
|
63
|
+
# the app layer
|
64
|
+
def receive_data data
|
65
|
+
|
66
|
+
if @connected
|
67
|
+
#parse each frame and dispatch
|
68
|
+
while msg = data.slice!(Pack)
|
69
|
+
on_receive msg.gsub(Frame, "")
|
70
|
+
end
|
71
|
+
else
|
72
|
+
if data =~ /</
|
73
|
+
Log.debug "Sending flash policy #{self.class.policy_content}"
|
74
|
+
send_data self.class.policy
|
75
|
+
close_connection_after_writing
|
76
|
+
else
|
77
|
+
handshake data
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# parse the request, validate the origin and path
|
83
|
+
# and respond with appropiate headers for a
|
84
|
+
# healthy relationship with the client
|
85
|
+
def handshake data
|
86
|
+
begin
|
87
|
+
@request = Request.new(data)
|
88
|
+
send_data @request.response
|
89
|
+
on_ready
|
90
|
+
rescue Exception => ex
|
91
|
+
on_error ex
|
92
|
+
close_connection
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
class << self
|
97
|
+
|
98
|
+
# set the flash policy this is sent to flash clients
|
99
|
+
# +policy+ either a string containing XML or a
|
100
|
+
# path to a XML policy file
|
101
|
+
def flash_policy policy
|
102
|
+
if policy =~ /\.xml$/
|
103
|
+
@policy = File.read(policy)
|
104
|
+
else
|
105
|
+
@policy = policy
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# get the flash policy content
|
110
|
+
def policy_content
|
111
|
+
@policy || default_policy
|
112
|
+
end
|
113
|
+
|
114
|
+
# secure any instance of this connection
|
115
|
+
# with TLS
|
116
|
+
# +opts+ the EM specific options hash for starting
|
117
|
+
# tls on the connection. Important options:
|
118
|
+
# :private_key_file
|
119
|
+
# :cert_chain_file
|
120
|
+
def secure opts={}
|
121
|
+
@tls_opts = opts
|
122
|
+
end
|
123
|
+
|
124
|
+
# is the connection secured with TLS?
|
125
|
+
def secure?
|
126
|
+
@tls_opts != nil
|
127
|
+
end
|
128
|
+
|
129
|
+
# the TLS options used for the secured connection
|
130
|
+
def tls_opts
|
131
|
+
@tls_opts
|
132
|
+
end
|
133
|
+
|
134
|
+
# add a domain to the list of allowable domains
|
135
|
+
# +domain+ the domain that is allowed eg: cnet.com
|
136
|
+
def accept_domain domain
|
137
|
+
@accepted = [] unless @accepted
|
138
|
+
@accepted << domain
|
139
|
+
end
|
140
|
+
|
141
|
+
# the set of domains that the server should
|
142
|
+
# accept the connection from. If the list is
|
143
|
+
# empty, the server will accept all connections
|
144
|
+
def accept_domains
|
145
|
+
@accepted || []
|
146
|
+
end
|
147
|
+
|
148
|
+
# the default flash policy content, which accepts
|
149
|
+
# from all domains to all ports (maybe not a good thing)
|
150
|
+
def default_policy
|
151
|
+
"<?xml version=\"1.0\"?><cross-domain-policy><allow-access-from domain=\"*\" to-ports=\"*\"/></cross-domain-policy>"
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|