tubesock 0.1.2 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +7 -0
- data/Gemfile +1 -0
- data/README.md +3 -1
- data/Rakefile +9 -0
- data/lib/tubesock.rb +25 -23
- data/lib/tubesock/hijack.rb +33 -19
- data/lib/tubesock/version.rb +1 -1
- data/test/test_helper.rb +83 -0
- data/test/tubesock_test.rb +57 -0
- data/tubesock.gemspec +4 -1
- metadata +51 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0a5fc91c6e49797a1595e75ac1fa8425259370a3
|
4
|
+
data.tar.gz: 3af86ddcbe0fe0cf80aac51d25f27420ca2bb444
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b6fa82ff09d335de5942b2ee415a328c15a40ba579f82f1f1293dd6c40de9edb100eb633384d1debb7cde05c017ff69239e9ceff8da37bf8f8c583a9338757aa
|
7
|
+
data.tar.gz: 3b5050954b14fafc27df74ddaabb5a34f3e8399db95e3d256f5b71c14385f803a28a552a026fd679c2c3ec5e43408b9b0c3881806343e1dd2cc5129edf9d3f1f
|
data/.travis.yml
ADDED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# Tubesock
|
2
2
|
|
3
|
+
[![Build Status](https://travis-ci.org/ngauthier/tubesock.png)](https://travis-ci.org/ngauthier/tubesock) [![Code Climate](https://codeclimate.com/github/ngauthier/tubesock.png)](https://codeclimate.com/github/ngauthier/tubesock)
|
4
|
+
|
3
5
|
Tubesock lets you use websockets from rack and rails 4+ by using Rack's new hijack interface to access the underlying socket connection.
|
4
6
|
|
5
7
|
In contrast to other websocket libraries, Tubesock does not use a reactor (read: no eventmachine). Instead, it leverages Rails 4's new full-stack concurrency support. Note that this means you must use a concurrent server. We recommend Puma.
|
@@ -12,7 +14,7 @@ Add this line to your application's Gemfile:
|
|
12
14
|
gem 'tubesock'
|
13
15
|
# currently, the ability to setup a websocket from rack is only
|
14
16
|
# available on my fork. If/when the PR is merged this will become a gem dependency
|
15
|
-
gem 'websocket', github: "ngauthier/websocket-ruby"
|
17
|
+
gem 'websocket', github: "ngauthier/websocket-ruby"
|
16
18
|
```
|
17
19
|
|
18
20
|
And then execute:
|
data/Rakefile
CHANGED
data/lib/tubesock.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
require "tubesock/version"
|
2
|
-
require "tubesock/hijack" if defined?(
|
2
|
+
require "tubesock/hijack" if defined?(ActiveSupport)
|
3
|
+
require "websocket"
|
3
4
|
|
5
|
+
# Easily interact with WebSocket connections over Rack.
|
6
|
+
# TODO: Example with pure Rack
|
4
7
|
class Tubesock
|
5
8
|
class HijackNotAvailable < RuntimeError
|
6
9
|
end
|
@@ -30,15 +33,12 @@ class Tubesock
|
|
30
33
|
end
|
31
34
|
end
|
32
35
|
|
33
|
-
def self.websocket?(env)
|
34
|
-
env['REQUEST_METHOD'] == 'GET' and
|
35
|
-
env['HTTP_CONNECTION'] and
|
36
|
-
env['HTTP_CONNECTION'].split(/\s*,\s*/).include?('Upgrade') and
|
37
|
-
env['HTTP_UPGRADE'].downcase == 'websocket'
|
38
|
-
end
|
39
|
-
|
40
36
|
def send_data data, type = :text
|
41
|
-
frame = WebSocket::Frame::Outgoing::Server.new(
|
37
|
+
frame = WebSocket::Frame::Outgoing::Server.new(
|
38
|
+
version: @version,
|
39
|
+
data: JSON.dump(data),
|
40
|
+
type: type
|
41
|
+
)
|
42
42
|
@socket.write frame.to_s
|
43
43
|
rescue IOError
|
44
44
|
close
|
@@ -59,24 +59,26 @@ class Tubesock
|
|
59
59
|
def listen
|
60
60
|
Thread.new do
|
61
61
|
Thread.current.abort_on_exception = true
|
62
|
-
framebuffer = WebSocket::Frame::Incoming::Server.new(version: @version)
|
63
|
-
running = true
|
64
62
|
@open_handlers.each(&:call)
|
65
|
-
|
66
|
-
|
67
|
-
running = false if data == ""
|
68
|
-
framebuffer << data
|
69
|
-
while frame = framebuffer.next
|
70
|
-
data = frame.data
|
71
|
-
if data == ""
|
72
|
-
running = false
|
73
|
-
else
|
74
|
-
@message_handlers.each{|h| h.call(HashWithIndifferentAccess.new(JSON.load(data))) }
|
75
|
-
end
|
76
|
-
end
|
63
|
+
each_frame do |data|
|
64
|
+
@message_handlers.each{|h| h.call(JSON.load(data)) }
|
77
65
|
end
|
78
66
|
@close_handlers.each(&:call)
|
79
67
|
@socket.close
|
80
68
|
end
|
81
69
|
end
|
70
|
+
|
71
|
+
private
|
72
|
+
def each_frame
|
73
|
+
framebuffer = WebSocket::Frame::Incoming::Server.new(version: @version)
|
74
|
+
while !@socket.closed?
|
75
|
+
data, addrinfo = @socket.recvfrom(2000)
|
76
|
+
break if data.empty?
|
77
|
+
framebuffer << data
|
78
|
+
while frame = framebuffer.next
|
79
|
+
return if frame.type == :close
|
80
|
+
yield frame.data
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
82
84
|
end
|
data/lib/tubesock/hijack.rb
CHANGED
@@ -1,25 +1,39 @@
|
|
1
1
|
require 'active_support/concern'
|
2
|
-
|
3
|
-
|
4
|
-
|
2
|
+
# Interact with WebSocket connections in rails.
|
3
|
+
# Example:
|
4
|
+
# # The widget controller's `show` lets you edit a widget in
|
5
|
+
# # real time. The websocket frames from the client should be in JSON, for
|
6
|
+
# # example:
|
7
|
+
# # socket.send(JSON.stringify({name: "foo"}));
|
8
|
+
# class WidgetController < ApplicationController
|
9
|
+
# include Tubesock::Hijack
|
10
|
+
# def show
|
11
|
+
# widget = Widget.find params[:id]
|
12
|
+
# hijack do |tubesock|
|
13
|
+
# tubesock.onopen do
|
14
|
+
# widget.update_attribute :editing, true
|
15
|
+
# end
|
16
|
+
# tubesock.onmessage do |message|
|
17
|
+
# widget.update_attribute :name, message["name"]
|
18
|
+
# end
|
19
|
+
# tubesock.onclose do
|
20
|
+
# widget.update_attribute :editing, true
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
module Tubesock::Hijack
|
26
|
+
extend ActiveSupport::Concern
|
5
27
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
ActiveRecord::Base.clear_active_connections!
|
13
|
-
end
|
14
|
-
sock.listen
|
15
|
-
render text: nil, status: -1
|
16
|
-
else
|
17
|
-
render text: "Not found", status: :not_found
|
18
|
-
end
|
28
|
+
included do
|
29
|
+
def hijack
|
30
|
+
sock = Tubesock.hijack(env)
|
31
|
+
yield sock
|
32
|
+
sock.onclose do
|
33
|
+
ActiveRecord::Base.clear_active_connections! if defined? ActiveRecord
|
19
34
|
end
|
20
|
-
|
21
|
-
|
22
|
-
module ClassMethods
|
35
|
+
sock.listen
|
36
|
+
render text: nil, status: -1
|
23
37
|
end
|
24
38
|
end
|
25
39
|
end
|
data/lib/tubesock/version.rb
CHANGED
data/test/test_helper.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'simplecov'
|
3
|
+
SimpleCov.start
|
4
|
+
require 'active_support'
|
5
|
+
require 'minitest/autorun'
|
6
|
+
require 'webrick'
|
7
|
+
require 'stringio'
|
8
|
+
require 'tubesock'
|
9
|
+
require 'json'
|
10
|
+
|
11
|
+
class Tubesock::TestCase < Minitest::Unit::TestCase
|
12
|
+
end
|
13
|
+
|
14
|
+
# Abstract away frames and sockets so that
|
15
|
+
# tests can focus on message interaction
|
16
|
+
class Tubesock::TestCase::TestInteraction
|
17
|
+
def initialize
|
18
|
+
@client_socket, @server_socket = UNIXSocket.pair
|
19
|
+
end
|
20
|
+
|
21
|
+
def handshake
|
22
|
+
@handshake ||= WebSocket::Handshake::Client.new(:url => 'ws://example.com')
|
23
|
+
end
|
24
|
+
|
25
|
+
def version
|
26
|
+
handshake.version
|
27
|
+
end
|
28
|
+
|
29
|
+
def env
|
30
|
+
request = WEBrick::HTTPRequest.new( :ServerSoftware => "" )
|
31
|
+
request.parse(StringIO.new(handshake.to_s))
|
32
|
+
rest = handshake.to_s.slice((request.to_s.length..-1))
|
33
|
+
|
34
|
+
request.meta_vars.merge(
|
35
|
+
"rack.input" => StringIO.new(rest),
|
36
|
+
"rack.hijack" => proc {},
|
37
|
+
"rack.hijack_io" => @server_socket
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
def write_raw(message)
|
42
|
+
@client_socket << message
|
43
|
+
end
|
44
|
+
|
45
|
+
def write(message)
|
46
|
+
write_raw WebSocket::Frame::Outgoing::Client.new(
|
47
|
+
version: version,
|
48
|
+
data: JSON.dump(message),
|
49
|
+
type: :text
|
50
|
+
).to_s
|
51
|
+
end
|
52
|
+
|
53
|
+
def read
|
54
|
+
framebuffer = WebSocket::Frame::Incoming::Client.new(version: version)
|
55
|
+
data, addrinfo = @client_socket.recvfrom(2000)
|
56
|
+
framebuffer << data
|
57
|
+
JSON.load(framebuffer.next.data)
|
58
|
+
end
|
59
|
+
|
60
|
+
def join
|
61
|
+
@thread.join
|
62
|
+
end
|
63
|
+
|
64
|
+
def close
|
65
|
+
@client_socket.close
|
66
|
+
join
|
67
|
+
end
|
68
|
+
|
69
|
+
def tubesock
|
70
|
+
tubesock = Tubesock.hijack(env)
|
71
|
+
yield tubesock
|
72
|
+
@thread = tubesock.listen
|
73
|
+
@client_socket.recvfrom 2000 # flush the handshake
|
74
|
+
tubesock
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class MockProc
|
79
|
+
attr_reader :called
|
80
|
+
def to_proc
|
81
|
+
proc { @called = true }
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
# Force autoloading of classes
|
4
|
+
WebSocket::Frame::Incoming::Client
|
5
|
+
WebSocket::Frame::Outgoing::Client
|
6
|
+
WebSocket::Frame::Incoming::Server
|
7
|
+
WebSocket::Frame::Outgoing::Server
|
8
|
+
|
9
|
+
class TubesockTest < Tubesock::TestCase
|
10
|
+
|
11
|
+
def test_raise_exception_when_hijack_is_not_available
|
12
|
+
-> {
|
13
|
+
Tubesock.hijack({})
|
14
|
+
}.must_raise Tubesock::HijackNotAvailable
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_hijack
|
18
|
+
interaction = TestInteraction.new
|
19
|
+
|
20
|
+
opened = MockProc.new
|
21
|
+
closed = MockProc.new
|
22
|
+
|
23
|
+
interaction.tubesock do |tubesock|
|
24
|
+
tubesock.onopen &opened
|
25
|
+
tubesock.onclose &closed
|
26
|
+
|
27
|
+
tubesock.onmessage do |message|
|
28
|
+
tubesock.send_data message: "Hello #{message["name"]}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
interaction.write name: "Nick"
|
33
|
+
data = interaction.read
|
34
|
+
data["message"].must_equal "Hello Nick"
|
35
|
+
interaction.close
|
36
|
+
|
37
|
+
opened.called.must_equal true
|
38
|
+
closed.called.must_equal true
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_close_on_close_frame
|
42
|
+
interaction = TestInteraction.new
|
43
|
+
|
44
|
+
closed = MockProc.new
|
45
|
+
|
46
|
+
interaction.tubesock do |tubesock|
|
47
|
+
tubesock.onclose &closed
|
48
|
+
end
|
49
|
+
|
50
|
+
# That's what Firefox sends when disconnecting.
|
51
|
+
interaction.write_raw "\x88\x82\xA3\x95\xD5\xB1\xA0|".force_encoding('binary')
|
52
|
+
|
53
|
+
interaction.join
|
54
|
+
|
55
|
+
closed.called.must_equal true
|
56
|
+
end
|
57
|
+
end
|
data/tubesock.gemspec
CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.email = ["ngauthier@gmail.com"]
|
11
11
|
spec.description = %q{Websocket interface on Rack Hijack w/ Rails support}
|
12
12
|
spec.summary = %q{Handle websocket connections via Rack and Rails 4 using concurrency}
|
13
|
-
spec.homepage = ""
|
13
|
+
spec.homepage = "http://github.com/ngauthier/tubesock"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
16
16
|
spec.files = `git ls-files`.split($/)
|
@@ -23,4 +23,7 @@ Gem::Specification.new do |spec|
|
|
23
23
|
|
24
24
|
spec.add_development_dependency "bundler", "~> 1.3"
|
25
25
|
spec.add_development_dependency "rake"
|
26
|
+
spec.add_development_dependency "minitest", "~> 4.7.3"
|
27
|
+
spec.add_development_dependency "simplecov"
|
28
|
+
spec.add_development_dependency "activesupport"
|
26
29
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tubesock
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nick Gauthier
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-
|
11
|
+
date: 2013-06-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -66,6 +66,48 @@ dependencies:
|
|
66
66
|
- - '>='
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: minitest
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 4.7.3
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 4.7.3
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: simplecov
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: activesupport
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
69
111
|
description: Websocket interface on Rack Hijack w/ Rails support
|
70
112
|
email:
|
71
113
|
- ngauthier@gmail.com
|
@@ -74,6 +116,7 @@ extensions: []
|
|
74
116
|
extra_rdoc_files: []
|
75
117
|
files:
|
76
118
|
- .gitignore
|
119
|
+
- .travis.yml
|
77
120
|
- Gemfile
|
78
121
|
- LICENSE.txt
|
79
122
|
- README.md
|
@@ -81,8 +124,10 @@ files:
|
|
81
124
|
- lib/tubesock.rb
|
82
125
|
- lib/tubesock/hijack.rb
|
83
126
|
- lib/tubesock/version.rb
|
127
|
+
- test/test_helper.rb
|
128
|
+
- test/tubesock_test.rb
|
84
129
|
- tubesock.gemspec
|
85
|
-
homepage:
|
130
|
+
homepage: http://github.com/ngauthier/tubesock
|
86
131
|
licenses:
|
87
132
|
- MIT
|
88
133
|
metadata: {}
|
@@ -106,4 +151,6 @@ rubygems_version: 2.0.3
|
|
106
151
|
signing_key:
|
107
152
|
specification_version: 4
|
108
153
|
summary: Handle websocket connections via Rack and Rails 4 using concurrency
|
109
|
-
test_files:
|
154
|
+
test_files:
|
155
|
+
- test/test_helper.rb
|
156
|
+
- test/tubesock_test.rb
|