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.
- checksums.yaml +7 -0
- data/GPL-3 +674 -0
- data/Manifest.txt +9 -0
- data/README +779 -0
- data/bin/wsc +239 -0
- data/lib/webtube.rb +757 -0
- data/lib/webtube/vital-statistics.rb +74 -0
- data/lib/webtube/webrick.rb +187 -0
- data/sample-server.rb +47 -0
- data/webtube.gemspec +17 -0
- metadata +55 -0
@@ -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
|
data/sample-server.rb
ADDED
@@ -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
|
data/webtube.gemspec
ADDED
@@ -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: []
|