em-websocket-server 0.13 → 0.15
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/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
|