webtube 1.0.0

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.
@@ -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: []