em-websocket-server 0.13 → 0.15

Sign up to get free protection for your applications and to get access to all the features.
@@ -9,48 +9,108 @@
9
9
  ##Dependencies
10
10
  - eventmachine http://github.com/eventmachine/eventmachine
11
11
 
12
- ##Docs
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
- require 'rubygems'
19
- require 'em-websocket-server'
20
- require 'json'
62
+ require "rubygems"
63
+ require "em-websocket-server"
21
64
 
22
- #create a channel for pub sub
23
- $chatroom = EM::Channel.new
24
-
25
- class ChatServer < WebSocket::Server
26
-
27
- #subscribe to the channel on client connect
28
- def on_connect
29
- @sid = $chatroom.subscribe do |msg|
30
- send_message msg
31
- end
32
- end
33
-
34
- #unsubscribe on client disconnect
35
- def on_disconnect
36
- $chatroom.unsubscribe(@sid)
37
- end
38
-
39
- #publish the message to the channel on
40
- #client message received
41
- def on_receive msg
42
- $chatroom.push msg
43
- end
44
-
45
- end
46
-
47
- #start the event machine on port 8000 and have
48
- #it instantiate ChatServer objects for each
49
- #client connection
50
- EM.run do
51
- EM.start_server "0.0.0.0", 8000, ChatServer
52
- end
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
@@ -1,32 +1,22 @@
1
1
  spec = Gem::Specification.new do |s|
2
- s.name = 'em-websocket-server'
3
- s.version = '0.13'
4
- s.date = '2009-12-14'
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"
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
- s.authors = ["Dan Simpson"]
13
- s.add_dependency('eventmachine', '>= 0.12.4')
10
+ s.authors = ["Dan Simpson"]
11
+ s.add_dependency('eventmachine', '>= 0.12.10')
14
12
 
15
13
 
16
- s.files = [
17
- "README.markdown",
18
- "em-websocket-server.gemspec",
19
- "examples/chat.rb",
20
- "examples/chat.html",
21
- "examples/timesync.html",
22
- "examples/timesync.rb",
23
- "examples/tictactoe/tictactoe.html",
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