webtube 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,74 @@
1
+ require 'set'
2
+ require 'thread'
3
+ require 'webtube'
4
+
5
+ class Webtube
6
+ # A tracker for live [[Webtube]] instances and their threads. This allows a
7
+ # shutdowning WEBrick to gently close the pending WebSockets.
8
+ class Vital_Statistics
9
+ # A [[ThreadGroup]] into which the Webtube threads can add themselves.
10
+ # Note that [[Vital_Statistics]] does not forcefully move them (nor could
11
+ # it -- a Webtube does not get a thread before [[Webtube#run]] is called,
12
+ # which is normally _after_ [[Vital_Statistics#birth]] gets called).
13
+ #
14
+ # When Webtube is being integrated with WEBrick by [[webtube/webrick.rb]],
15
+ # assigning Webtube-specific threads into this group will cause WEBrick's
16
+ # standard shutdown procedure to not try to [[Thread#join]] them as it does
17
+ # to ordinary WEBrick threads. Instead, the integration code will call
18
+ # [[Vital_Statistics#close_all]], to request that each Webtube close
19
+ # itself, and then join the threads from [[Vital_Statistics#thread_group]].
20
+ attr_reader :thread_group
21
+
22
+ def initialize logger
23
+ super()
24
+ @logger = logger
25
+ @webtubes = Set.new
26
+ @mutex = Mutex.new
27
+ @thread_group = ThreadGroup.new
28
+ return
29
+ end
30
+
31
+ def birth webtube
32
+ @mutex.synchronize do
33
+ @webtubes.add webtube
34
+ end
35
+ return
36
+ end
37
+
38
+ def death webtube
39
+ @mutex.synchronize do
40
+ @webtubes.delete webtube
41
+ end
42
+ return
43
+ end
44
+
45
+ # Construct a list of all the currently living [[Webtube]] instances. Note
46
+ # that while the operation is atomic, the Webtube infrastructure is
47
+ # inherently multithreaded, so the list can get slightly stale immediately
48
+ # and should, in most contexts, be considered informative.
49
+ def to_a
50
+ return @mutex.synchronize{@webtubes.to_a}
51
+ end
52
+
53
+ # The default status code in a shutdown situation is 1001 'going away'.
54
+ def close_all status_code = 1001, explanation = ""
55
+ # Note that we're only mutexing off extracting the content of
56
+ # [[@webtubes]] (see [[to_a]]). We can't mutex the whole block, for as
57
+ # the webtubes will be closing, they'll want to notify us about it, and
58
+ # that is also mutexed.
59
+ #
60
+ # This is not as bad as it may sound, for the webserver shouldn't be
61
+ # accepting new connections anymore anyway by the time it'll start
62
+ # closing the old ones.
63
+ self.to_a.each do |webtube|
64
+ begin
65
+ webtube.close status_code, explanation
66
+ rescue Exception => e
67
+ # log and continue
68
+ logger.error e
69
+ end
70
+ end
71
+ return
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,187 @@
1
+ # webtube/webrick.rb -- WEBrick integration for Webtube, an implementation of
2
+ # the WebSocket protocol
3
+
4
+ require 'webrick'
5
+ require 'webrick/httprequest'
6
+ require 'webrick/httpserver'
7
+ require 'webrick/httpservlet'
8
+ require 'webrick/httpservlet/abstract'
9
+ require 'webtube'
10
+ require 'webtube/vital-statistics'
11
+
12
+ module WEBrick
13
+ class HTTPRequest
14
+ def websocket_upgrade_request?
15
+ return self.request_method.upcase == 'GET' &&
16
+ self.http_version >= '1.1' &&
17
+ (self['Connection'] || '').downcase == 'upgrade' &&
18
+ (self['Upgrade'] || '').downcase == 'websocket' &&
19
+ !self['Sec-WebSocket-Key'].nil?
20
+ end
21
+ end
22
+
23
+ class HTTPServer
24
+ # Attach a [[Webtube::Vital_Statistics]] to new [[WEBrick::HTTPServer]]
25
+ # instances so that the live webtubes could be closed upon shutdown
26
+
27
+ alias orig_initialize_before_webtube_integration initialize
28
+ def initialize config = {}, default = Config::HTTP
29
+ orig_initialize_before_webtube_integration config, default
30
+ @webtubes = Webtube::Vital_Statistics.new @logger
31
+ return
32
+ end
33
+
34
+ def webtubes
35
+ result = @webtubes
36
+ # Usually, this should be it.
37
+ if result.nil? then # ... but ...
38
+ # Well, it would seem that our extended constructor was not called.
39
+ # How could this have happened?
40
+ result = @webtubes = Webtube::Vital_Statistics.new
41
+ @logger.warn "@webtubes has not been set up before accessing it. I " +
42
+ "have attempted to correct this ex post facto, but doing it now " +
43
+ "is a race condition, and I may have lost track of some webtubes " +
44
+ "as a result. The next time, please load webtube/webrick.rb " +
45
+ "/before/ instantiating your WEBrick::Server."
46
+ end
47
+ return result
48
+ end
49
+
50
+ alias orig_shutdown_before_webtube_integration shutdown
51
+ def shutdown
52
+ # We'll need to call the original shutdown code first, for we want to
53
+ # stop accepting new Webtube connections before 'close all Webtube
54
+ # connections' will have a proper, thread-safe meaning.
55
+ orig_shutdown_before_webtube_integration
56
+ webtubes.close_all
57
+ webtubes.thread_group.list.each &:join
58
+ return
59
+ end
60
+
61
+ # Given a [[request]] and a [[response]] object, as prepared by a
62
+ # [[WEBrick::HTTPServer]] for processing in a portlet, attempt to accept
63
+ # the client's request to establish a WebSocket connection. The
64
+ # [[request]] must actually contain such a request; see
65
+ # [[websocket_upgrade_request?]].
66
+ #
67
+ # The attempt will fail in the theoretical case the client and the server
68
+ # can't agree on the protocol version to use. In such a case,
69
+ # [[accept_webtube]] will prepare a 426 'Upgrade required' response,
70
+ # explaining in plain text what the problem is and advertising, using the
71
+ # [[Sec-WebSocket-Version]] header field, the protocol version
72
+ # (specifically, 13) it is prepared to speak. When this happens, the
73
+ # WebSocket session will never be set up and no [[listener]] events will be
74
+ # called.
75
+ #
76
+ # Note that [[accept_webtube]] will manipulate [[response]] and return
77
+ # immediately. The actual WebSocket session will begin once WEBrick
78
+ # attempts to deliver the [[response]], and will be marked by the newly
79
+ # constructed [[Webtube]] instance delivering an [[onopen]] event to
80
+ # [[listener]].
81
+ #
82
+ # Also note that the loop to process incoming WebSocket frames will hog the
83
+ # whole thread; in order to deliver asynchronous messages over the
84
+ # WebSocket, [[Webtube#send_message]] needs to be called from another
85
+ # thread. (For synchronous messages, it can safely be called from the
86
+ # handlers inside [[listener]].)
87
+ #
88
+ # See [[Webtube#run]] for a list of the supported methods for the
89
+ # [[listener]].
90
+ def accept_webtube request, response, listener,
91
+ session: nil, context: nil
92
+ # Check that the client speaks our version
93
+ unless (request['Sec-WebSocket-Version'] || '').split(/\s*,\s*/).
94
+ include? '13' then
95
+ @logger.error "Sec-WebSocket-Version mismatch"
96
+ response.status, response.reason_phrase = '426', 'Upgrade required'
97
+ response['Content-type'] = 'text/plain'
98
+ response['Sec-WebSocket-Version'] = '13'
99
+ # advertise the version we speak
100
+ response.body = "This WebSocket server only speaks version 13 of the " +
101
+ "protocol, as specified by RFC 6455.\n"
102
+ else
103
+ response.status, response.reason_phrase = '101', 'Hello WebSocket'
104
+ response['Upgrade'] = 'websocket'
105
+ response['Sec-WebSocket-Accept'] = Digest::SHA1.base64digest(
106
+ request['Sec-WebSocket-Key'] +
107
+ '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
108
+ response['Sec-WebSocket-Version'] = '13'
109
+ response.keep_alive = false
110
+ # so that WEBrick will close the TCP socket when we're done
111
+ vital_statistics = self.webtubes
112
+ (class << response; self; end).instance_eval do
113
+ # We'll need to deliver the [[Connection: Upgrade]] header;
114
+ # unfortunately, HTTPResponse#setup_header would munge it if we set
115
+ # this header field in the ordinary way. Accordingly, we'll have to
116
+ # override the method.
117
+ define_method :setup_header do ||
118
+ super()
119
+ @header['connection'] = 'Upgrade'
120
+ return
121
+ end
122
+
123
+ # Replace [[response.send_body]] with the WS engine. WEBrick will
124
+ # call it automatically after sending the response header.
125
+ #
126
+ # Also notify the server's attached [[Webtube::Vital_Statistics]]
127
+ # instance so that server shutdown could also close all pending
128
+ # Webtubes.
129
+ define_method :send_body do |socket|
130
+ webtube = Webtube.new(socket, true, close_socket: false)
131
+ begin
132
+ vital_statistics.birth webtube
133
+ webtube.header = request
134
+ webtube.session = session
135
+ webtube.context = context
136
+ # Reassign us from the WEBrick's thread group to the one
137
+ # maintained by [[Webtube::Vital_Statistics]].
138
+ vital_statistics.thread_group.add Thread.current
139
+ # And now, run!
140
+ webtube.run listener
141
+ ensure
142
+ vital_statistics.death webtube
143
+ end
144
+ return
145
+ end
146
+ end
147
+ end
148
+ return
149
+ end
150
+ end
151
+
152
+ module HTTPServlet
153
+ class WebtubeHandler < AbstractServlet
154
+ def get_instance server, *options
155
+ return self
156
+ end
157
+
158
+ def initialize server, listener
159
+ super server
160
+ @listener = listener
161
+ return
162
+ end
163
+
164
+ def do_GET request, response
165
+ if request.websocket_upgrade_request? then
166
+ @server.accept_webtube request, response, @listener
167
+ else
168
+ response.status, response.reason_phrase =
169
+ '426', 'Upgrade to WebSocket'
170
+ response['Sec-WebSocket-Version'] = '13'
171
+ # advertise the version we speak
172
+ # prepare a human-readable content
173
+ response['Content-type'] = 'text/plain'
174
+ response.body = "426\n\nThis is a WebSocket-only resource."
175
+ end
176
+ return
177
+ end
178
+ end
179
+ end
180
+
181
+ class HTTPServer
182
+ def mount_webtube dir, listener
183
+ mount dir, HTTPServlet::WebtubeHandler.new(self, listener)
184
+ return
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,47 @@
1
+ #! /usr/bin/ruby
2
+
3
+ # A sample WEBrick server using the Webtube API. It listens on port 8888 and
4
+ # provides two services: [[/diag]], which logs all the events from
5
+ # [[Webtube#run]] and remains silent towards the client (although note that
6
+ # the Webtube library pongs the pings), and [[/echo]], which echos.
7
+
8
+ require 'webrick'
9
+ require 'webtube/webrick'
10
+
11
+ class << diagnostic_listener = Object.new
12
+ def respond_to? name
13
+ return (name.to_s =~ /^on/ or super)
14
+ end
15
+
16
+ def method_missing name, *args
17
+ output = "- #{name}("
18
+ args.each_with_index do |arg, i|
19
+ output << ', ' unless i.zero?
20
+ if i.zero? and arg.is_a? Webtube then
21
+ output << arg.to_s
22
+ else
23
+ output << arg.inspect
24
+ end
25
+ end
26
+ output << ")"
27
+ puts output
28
+ return
29
+ end
30
+ end
31
+
32
+ class << echo_listener = Object.new
33
+ def onmessage webtube, data, opcode
34
+ webtube.send_message data, opcode
35
+ return
36
+ end
37
+ end
38
+
39
+ server = WEBrick::HTTPServer.new(:Port => 8888)
40
+ server.mount_webtube '/diag', diagnostic_listener
41
+ server.mount_webtube '/echo', echo_listener
42
+
43
+ begin
44
+ server.start
45
+ ensure
46
+ server.shutdown
47
+ end
@@ -0,0 +1,17 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'webtube'
3
+ s.version = '1.0.0'
4
+ s.date = '2014-10-19'
5
+ s.homepage = 'https://github.com/digwuren/webtube'
6
+ s.summary = 'A Ruby implementation of the [[WebSocket]] protocol'
7
+ s.author = 'Andres Soolo'
8
+ s.email = 'dig@mirky.net'
9
+ s.files = File.read('Manifest.txt').split(/\n/)
10
+ s.executables << 'wsc'
11
+ s.license = 'GPL-3'
12
+ s.description = <<EOD
13
+ Webtube is an implementation of the [[WebSocket]] protocol for [[Ruby]]. Some
14
+ integration with the [[WEBrick]] server is also included.
15
+ EOD
16
+ s.has_rdoc = false
17
+ end
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: webtube
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Andres Soolo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-10-19 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |
14
+ Webtube is an implementation of the [[WebSocket]] protocol for [[Ruby]]. Some
15
+ integration with the [[WEBrick]] server is also included.
16
+ email: dig@mirky.net
17
+ executables:
18
+ - wsc
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - GPL-3
23
+ - Manifest.txt
24
+ - README
25
+ - bin/wsc
26
+ - lib/webtube.rb
27
+ - lib/webtube/vital-statistics.rb
28
+ - lib/webtube/webrick.rb
29
+ - sample-server.rb
30
+ - webtube.gemspec
31
+ homepage: https://github.com/digwuren/webtube
32
+ licenses:
33
+ - GPL-3
34
+ metadata: {}
35
+ post_install_message:
36
+ rdoc_options: []
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubyforge_project:
51
+ rubygems_version: 2.0.14
52
+ signing_key:
53
+ specification_version: 4
54
+ summary: A Ruby implementation of the [[WebSocket]] protocol
55
+ test_files: []