faye-websocket 0.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of faye-websocket might be problematic. Click here for more details.

@@ -0,0 +1,132 @@
1
+ = Faye::WebSocket
2
+
3
+ This is a robust, general-purpose WebSocket implementation extracted from the
4
+ {Faye}[http://faye.jcoglan.com] project. It provides classes for easily building
5
+ WebSocket servers and clients in Ruby. It does not provide a server itself, but
6
+ rather makes it easy to handle WebSocket connections within an existing
7
+ {Rack}[http://rack.rubyforge.org/] application running under
8
+ {Thin}[http://code.macournoyer.com/thin/]. It does not provide any abstraction
9
+ other than the standard {WebSocket API}[http://dev.w3.org/html5/websockets/].
10
+
11
+ The server-side socket can process {draft-75}[http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75],
12
+ {draft-76}[http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76],
13
+ {hybi-07}[http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07]
14
+ and later versions of the protocol. It selects protocol versions automatically,
15
+ supports both +text+ and +binary+ messages, and transparently handles +ping+,
16
+ +pong+, +close+ and fragmented messages.
17
+
18
+
19
+ == Accepting WebSocket connections in Rack
20
+
21
+ You can handle WebSockets on the server side by listening for HTTP Upgrade
22
+ requests, and creating a new socket for the request. This socket object exposes
23
+ the usual WebSocket methods for receiving and sending messages. For example this
24
+ is how you'd implement an echo server:
25
+
26
+ require 'faye/websocket'
27
+ require 'rack'
28
+ require 'eventmachine'
29
+
30
+ app = lambda do |env|
31
+ if env['HTTP_UPGRADE']
32
+ ws = Faye::WebSocket.new(env)
33
+
34
+ ws.onmessage = lambda do |event|
35
+ ws.send(event.data)
36
+ end
37
+
38
+ ws.onclose = lambda do |event|
39
+ p [:close, event.code, event.reason]
40
+ ws = nil
41
+ end
42
+
43
+ # Thin async response
44
+ [-1, {}, []]
45
+
46
+ else
47
+ # Normal HTTP request
48
+ [200, {'Content-Type' => 'text/plain'}, ['Hello']]
49
+ end
50
+ end
51
+
52
+ EM.run {
53
+ thin = Rack::Handler.get('thin')
54
+ thin.run(app, :Port => 9292)
55
+ }
56
+
57
+
58
+ == Using the WebSocket client
59
+
60
+ The client supports both the plain-text +ws+ protocol and the encrypted +wss+
61
+ protocol, and has exactly the same interface as a socket you would use in a web
62
+ browser. On the wire it identifies itself as hybi-08, though it's compatible
63
+ with servers speaking later versions of the protocol, at least up to version 17.
64
+
65
+ require 'faye/websocket'
66
+ require 'eventmachine'
67
+
68
+ EM.run {
69
+ ws = Faye::WebSocket::Client.new('ws://www.example.com/')
70
+
71
+ ws.onopen = lambda do |event|
72
+ p [:open]
73
+ ws.send('Hello, world!')
74
+ end
75
+
76
+ ws.onmessage = lambda do |event|
77
+ p [:message, event.data]
78
+ end
79
+
80
+ ws.onclose = lambda do |event|
81
+ p [:close, event.code, event.reason]
82
+ ws = nil
83
+ end
84
+ }
85
+
86
+
87
+ == WebSocket API
88
+
89
+ The WebSocket API consists of several event handlers and a method for sending
90
+ messages.
91
+
92
+ * <b><tt>onopen</tt></b> fires when the socket connection is established. Event
93
+ has no attributes.
94
+ * <b><tt>onerror</tt></b> fires when the connection attempt fails. Event has no
95
+ attributes.
96
+ * <b><tt>onmessage</tt></b> fires when the socket receives a message. Event has
97
+ one attribute, <b><tt>data</tt></b>, which is either a +String+ (for text
98
+ frames) or an +Array+ of byte-sized integers (for binary frames).
99
+ * <b><tt>onclose</tt></b> fires when either the client or the server closes the
100
+ connection. Event has two optional attributes, <b><tt>code</tt></b> and
101
+ <b><tt>reason</tt></b>, that expose the status code and message sent by the
102
+ peer that closed the connection.
103
+ * <b><tt>send(message)</tt></b> accepts either a +String+ or an +Array+ of
104
+ byte-sized integers and sends a text or binary message over the connection to
105
+ the other peer.
106
+ * <b><tt>close(code, reason)</tt></b> closes the connection, sending the given
107
+ status code and reason text, both of which are optional.
108
+
109
+
110
+ == License
111
+
112
+ (The MIT License)
113
+
114
+ Copyright (c) 2009-2011 James Coglan
115
+
116
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
117
+ this software and associated documentation files (the 'Software'), to deal in
118
+ the Software without restriction, including without limitation the rights to use,
119
+ copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
120
+ Software, and to permit persons to whom the Software is furnished to do so,
121
+ subject to the following conditions:
122
+
123
+ The above copyright notice and this permission notice shall be included in all
124
+ copies or substantial portions of the Software.
125
+
126
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
127
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
128
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
129
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
130
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
131
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
132
+
@@ -0,0 +1,45 @@
1
+ require 'rubygems'
2
+ require File.expand_path('../../lib/faye/websocket', __FILE__)
3
+ require 'cgi'
4
+
5
+ EM.run {
6
+ host = 'ws://localhost:9001'
7
+ agent = "Faye (Ruby #{RUBY_VERSION})"
8
+ cases = 0
9
+ skip = [247,248,249,250,251,252,253,254,255,
10
+ 256,257,258,259,260,261,262,263,264]
11
+
12
+ socket = Faye::WebSocket::Client.new("#{host}/getCaseCount")
13
+
14
+ socket.onmessage = lambda do |event|
15
+ puts "Total cases to run: #{event.data}"
16
+ cases = event.data.to_i
17
+ end
18
+
19
+ socket.onclose = lambda do |event|
20
+ run_case = lambda do |n|
21
+ if n > cases
22
+ socket = Faye::WebSocket::Client.new("#{host}/updateReports?agent=#{CGI.escape agent}")
23
+ socket.onclose = lambda { |e| EM.stop }
24
+
25
+ elsif skip.include?(n)
26
+ EM.next_tick { run_case.call(n+1) }
27
+
28
+ else
29
+ puts "Running test case ##{n} ..."
30
+ socket = Faye::WebSocket::Client.new("#{host}/runCase?case=#{n}&agent=#{CGI.escape agent}")
31
+
32
+ socket.onmessage = lambda do |event|
33
+ socket.send(event.data)
34
+ end
35
+
36
+ socket.onclose = lambda do |event|
37
+ run_case.call(n + 1)
38
+ end
39
+ end
40
+ end
41
+
42
+ run_case.call(1)
43
+ end
44
+ }
45
+
@@ -0,0 +1,30 @@
1
+ require 'rubygems'
2
+ require File.expand_path('../../lib/faye/websocket', __FILE__)
3
+ require 'eventmachine'
4
+
5
+ port = ARGV[0] || 7000
6
+ secure = ARGV[1] == 'ssl'
7
+
8
+ EM.run {
9
+ scheme = secure ? 'wss' : 'ws'
10
+ url = "#{scheme}://localhost:#{port}/"
11
+ socket = Faye::WebSocket::Client.new(url)
12
+
13
+ puts "Connecting to #{socket.url}"
14
+
15
+ socket.onopen = lambda do |event|
16
+ p [:open]
17
+ socket.send("Hello, WebSocket!")
18
+ end
19
+
20
+ socket.onmessage = lambda do |event|
21
+ p [:message, event.data]
22
+ # socket.close 1002, 'Going away'
23
+ end
24
+
25
+ socket.onclose = lambda do |event|
26
+ p [:close, event.code, event.reason]
27
+ EM.stop
28
+ end
29
+ }
30
+
@@ -0,0 +1,35 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
5
+ <title>WebSocket test</title>
6
+ </head>
7
+ <body>
8
+
9
+ <h1>WebSocket test</h1>
10
+ <ul></ul>
11
+
12
+ <script type="text/javascript">
13
+ var logger = document.getElementsByTagName('ul')[0],
14
+ Socket = window.MozWebSocket || window.WebSocket,
15
+ socket = new Socket('ws://' + location.hostname + ':' + location.port + '/'),
16
+ index = 0;
17
+
18
+ socket.onopen = function() {
19
+ logger.innerHTML += '<li>OPEN</li>';
20
+ socket.send('Hello, world');
21
+ };
22
+
23
+ socket.onmessage = function(event) {
24
+ logger.innerHTML += '<li>MESSAGE: ' + event.data + '</li>';
25
+ setTimeout(function() { socket.send(++index + ' ' + event.data) }, 2000);
26
+ };
27
+
28
+ socket.onclose = function(event) {
29
+ logger.innerHTML += '<li>CLOSE: ' + event.code + ', ' + event.reason + '</li>';
30
+ };
31
+ </script>
32
+
33
+ </body>
34
+ </html>
35
+
@@ -0,0 +1,43 @@
1
+ require 'rubygems'
2
+ require File.expand_path('../../lib/faye/websocket', __FILE__)
3
+ require 'rack'
4
+ require 'eventmachine'
5
+
6
+ port = ARGV[0] || 7000
7
+ secure = ARGV[1] == 'ssl'
8
+
9
+ static = Rack::File.new(File.dirname(__FILE__))
10
+
11
+ app = lambda do |env|
12
+ if env['HTTP_UPGRADE']
13
+ socket = Faye::WebSocket.new(env)
14
+ p [:open, socket.url, socket.version]
15
+
16
+ socket.onmessage = lambda do |event|
17
+ socket.send(event.data)
18
+ end
19
+
20
+ socket.onclose = lambda do |event|
21
+ p [:close, event.code, event.reason]
22
+ socket = nil
23
+ end
24
+
25
+ [-1, {}, []]
26
+ else
27
+ static.call(env)
28
+ end
29
+ end
30
+
31
+ EM.run {
32
+ thin = Rack::Handler.get('thin')
33
+ thin.run(app, :Port => port) do |server|
34
+ if secure
35
+ server.ssl = true
36
+ server.ssl_options = {
37
+ :private_key_file => File.expand_path('../../spec/server.key', __FILE__),
38
+ :cert_chain_file => File.expand_path('../../spec/server.crt', __FILE__)
39
+ }
40
+ end
41
+ end
42
+ }
43
+
@@ -0,0 +1,75 @@
1
+ # WebSocket extensions for Thin
2
+ # Based on code from the Cramp project
3
+ # http://github.com/lifo/cramp
4
+
5
+ # Copyright (c) 2009-2011 Pratik Naik
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining
8
+ # a copy of this software and associated documentation files (the
9
+ # "Software"), to deal in the Software without restriction, including
10
+ # without limitation the rights to use, copy, modify, merge, publish,
11
+ # distribute, sublicense, and/or sell copies of the Software, and to
12
+ # permit persons to whom the Software is furnished to do so, subject to
13
+ # the following conditions:
14
+ #
15
+ # The above copyright notice and this permission notice shall be
16
+ # included in all copies or substantial portions of the Software.
17
+ #
18
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
+
26
+ class Thin::Connection
27
+ def receive_data(data)
28
+ trace { data }
29
+
30
+ case @serving
31
+ when :websocket
32
+ callback = @request.env[Thin::Request::WEBSOCKET_RECEIVE_CALLBACK]
33
+ callback.call(data) if callback
34
+ else
35
+ if @request.parse(data)
36
+ if @request.websocket?
37
+ @request.env['em.connection'] = self
38
+ @response.persistent!
39
+ @response.websocket = true
40
+ @serving = :websocket
41
+ end
42
+
43
+ process
44
+ end
45
+ end
46
+ rescue Thin::InvalidRequest => e
47
+ log "!! Invalid request"
48
+ log_error e
49
+ close_connection
50
+ end
51
+ end
52
+
53
+ class Thin::Request
54
+ WEBSOCKET_RECEIVE_CALLBACK = 'websocket.receive_callback'.freeze
55
+ def websocket?
56
+ @env['HTTP_CONNECTION'] and
57
+ @env['HTTP_CONNECTION'].split(/\s*,\s*/).include?('Upgrade') and
58
+ ['WebSocket', 'websocket'].include?(@env['HTTP_UPGRADE'])
59
+ end
60
+ end
61
+
62
+ class Thin::Response
63
+ # Headers for sending Websocket upgrade
64
+ attr_accessor :websocket
65
+
66
+ def each
67
+ yield(head) unless websocket
68
+ if @body.is_a?(String)
69
+ yield @body
70
+ else
71
+ @body.each { |chunk| yield chunk }
72
+ end
73
+ end
74
+ end
75
+
@@ -0,0 +1,126 @@
1
+ # API and protocol references:
2
+ #
3
+ # * http://dev.w3.org/html5/websockets/
4
+ # * http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#interface-eventtarget
5
+ # * http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#interface-event
6
+ # * http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75
7
+ # * http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
8
+ # * http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17
9
+
10
+ require 'base64'
11
+ require 'digest/md5'
12
+ require 'digest/sha1'
13
+ require 'forwardable'
14
+ require 'net/http'
15
+ require 'uri'
16
+
17
+ require 'eventmachine'
18
+ require 'thin'
19
+ require File.dirname(__FILE__) + '/thin_extensions'
20
+
21
+ module Faye
22
+ class WebSocket
23
+
24
+ root = File.expand_path('../websocket', __FILE__)
25
+
26
+ autoload :API, root + '/api'
27
+ autoload :Client, root + '/client'
28
+ autoload :Draft75Parser, root + '/draft75_parser'
29
+ autoload :Draft76Parser, root + '/draft76_parser'
30
+ autoload :Protocol8Parser, root + '/protocol8_parser'
31
+
32
+ # http://www.w3.org/International/questions/qa-forms-utf-8.en.php
33
+ UTF8_MATCH = /^([\x00-\x7F]|[\xC2-\xDF][\x80-\xBF]|\xE0[\xA0-\xBF][\x80-\xBF]|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}|\xED[\x80-\x9F][\x80-\xBF]|\xF0[\x90-\xBF][\x80-\xBF]{2}|[\xF1-\xF3][\x80-\xBF]{3}|\xF4[\x80-\x8F][\x80-\xBF]{2})*$/
34
+
35
+ def self.encode(string, validate_encoding = false)
36
+ if Array === string
37
+ return nil if validate_encoding and !valid_utf8?(string)
38
+ string = string.pack('C*')
39
+ end
40
+ return string unless string.respond_to?(:force_encoding)
41
+ string.force_encoding('UTF-8')
42
+ end
43
+
44
+ def self.valid_utf8?(byte_array)
45
+ UTF8_MATCH =~ byte_array.pack('C*') ? true : false
46
+ end
47
+
48
+ def self.parser(env)
49
+ if env['HTTP_SEC_WEBSOCKET_VERSION']
50
+ Protocol8Parser
51
+ elsif env['HTTP_SEC_WEBSOCKET_KEY1']
52
+ Draft76Parser
53
+ else
54
+ Draft75Parser
55
+ end
56
+ end
57
+
58
+ extend Forwardable
59
+ def_delegators :@parser, :version
60
+
61
+ attr_reader :env
62
+ include API
63
+
64
+ def initialize(env)
65
+ @env = env
66
+ @callback = @env['async.callback']
67
+ @stream = Stream.new(self, @env['em.connection'])
68
+ @callback.call [200, {}, @stream]
69
+
70
+ @url = determine_url
71
+ @ready_state = CONNECTING
72
+ @buffered_amount = 0
73
+
74
+ @parser = WebSocket.parser(@env).new(self)
75
+ @stream.write(@parser.handshake_response)
76
+
77
+ @ready_state = OPEN
78
+
79
+ event = Event.new('open')
80
+ event.init_event('open', false, false)
81
+ dispatch_event(event)
82
+
83
+ @env[Thin::Request::WEBSOCKET_RECEIVE_CALLBACK] = @parser.method(:parse)
84
+ end
85
+
86
+ private
87
+
88
+ def determine_url
89
+ secure = if @env.has_key?('HTTP_X_FORWARDED_PROTO')
90
+ @env['HTTP_X_FORWARDED_PROTO'] == 'https'
91
+ else
92
+ @env['HTTP_ORIGIN'] =~ /^https:/i
93
+ end
94
+
95
+ scheme = secure ? 'wss:' : 'ws:'
96
+ "#{ scheme }//#{ @env['HTTP_HOST'] }#{ @env['REQUEST_URI'] }"
97
+ end
98
+ end
99
+
100
+ class WebSocket::Stream
101
+ include EventMachine::Deferrable
102
+
103
+ extend Forwardable
104
+ def_delegators :@connection, :close_connection, :close_connection_after_writing
105
+
106
+ def initialize(web_socket, connection)
107
+ @web_socket = web_socket
108
+ @connection = connection
109
+ end
110
+
111
+ def each(&callback)
112
+ @data_callback = callback
113
+ end
114
+
115
+ def fail
116
+ @web_socket.close(1006, '', false)
117
+ end
118
+
119
+ def write(data)
120
+ return unless @data_callback
121
+ @data_callback.call(data)
122
+ end
123
+ end
124
+
125
+ end
126
+