webtube 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|