private_pub 0.0.0 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +3 -0
- data/Gemfile +3 -0
- data/README.rdoc +16 -32
- data/Rakefile +10 -0
- data/lib/generators/private_pub/install_generator.rb +16 -0
- data/lib/generators/private_pub/templates/faye.ru +7 -0
- data/lib/generators/private_pub/templates/private_pub.js +27 -0
- data/lib/generators/private_pub/templates/private_pub_helper.rb +15 -0
- data/lib/generators/private_pub/templates/private_pub_initializer.rb +2 -0
- data/lib/private_pub/faye_extension.rb +29 -0
- data/lib/private_pub.rb +57 -1
- data/spec/private_pub/faye_extension_spec.rb +48 -0
- data/spec/private_pub_spec.rb +51 -0
- data/spec/spec_helper.rb +6 -0
- metadata +25 -3
data/CHANGELOG.rdoc
ADDED
data/Gemfile
ADDED
data/README.rdoc
CHANGED
@@ -1,71 +1,55 @@
|
|
1
1
|
= Private Pub
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
This is a Ruby gem for use in a Rails application to publish and subscribe to messages through a separate server such as {Faye}[http://faye.jcoglan.com/].
|
3
|
+
This is a Ruby gem for use with Rails to publish and subscribe to messages through a separate server such as {Faye}[http://faye.jcoglan.com/]. All channels are automatically made private.
|
6
4
|
|
7
5
|
|
8
6
|
== Setup
|
9
7
|
|
10
|
-
Add the gem to your Gemfile
|
8
|
+
Add the gem to your Gemfile and run +bundle+.
|
11
9
|
|
12
10
|
gem "private_pub"
|
13
11
|
|
14
|
-
Run the generator to create the
|
12
|
+
Run the generator to create the initial files.
|
15
13
|
|
16
14
|
rails g private_pub:install
|
17
15
|
|
18
|
-
Add the JavaScript
|
16
|
+
Add the generated JavaScript to your layout file. Currently this uses jQuery, so you will need to customize it if you aren't using jQuery.
|
19
17
|
|
20
18
|
<%= javascript_include_tag "private_pub" %>
|
21
19
|
|
22
|
-
Next
|
23
|
-
|
24
|
-
PrivatePub.server = "http://localhost:9292/faye"
|
20
|
+
Next, install and start up Faye using the rackup file that was generated.
|
25
21
|
|
26
|
-
|
22
|
+
gem install faye
|
23
|
+
rackup faye.ru -s thin -E production
|
27
24
|
|
28
|
-
|
29
|
-
require File.expand_path("../config/initializers/private_pub.rb", __FILE__)
|
30
|
-
faye_server.add_extension(PrivatePub.faye_extension)
|
25
|
+
It's not necessary to add the faye.js since that will be handled automatically.
|
31
26
|
|
32
27
|
|
33
28
|
== Usage
|
34
29
|
|
35
|
-
|
30
|
+
Use the +subscribe_to+ helper method on any page to subscribe to a channel.
|
36
31
|
|
37
32
|
<%= subscribe_to "/messages/new" %>
|
38
33
|
|
39
|
-
|
34
|
+
Use the +publish_to+ helper method to publish messages to that channel. If a block of JavaScript is passed it will be evaluated automatically. This is usually done through a JavaScript AJAX response.
|
40
35
|
|
41
36
|
<% publish_to "/messages/new" do %>
|
42
37
|
$("#chat").append("<%= escape_javascript render(@messages) %>");
|
43
38
|
<% end %>
|
44
39
|
|
45
|
-
|
46
|
-
|
47
|
-
Alternatively you can pass anything you want to the +publish_to+ method (+to_json+ will be called on it).
|
48
|
-
|
49
|
-
publish_to "/messages/new", @message
|
50
|
-
|
51
|
-
And then handle that in a callback in JavaScript.
|
52
|
-
|
53
|
-
var private_pub = new PrivatePub;
|
54
|
-
private_pub.subscribe("/messages/new", function(data) {
|
55
|
-
// data contains whatever json was passed in
|
56
|
-
});
|
40
|
+
There will be alternative ways to publish/subscribe to messages in the future.
|
57
41
|
|
58
42
|
|
59
43
|
== Security
|
60
44
|
|
61
|
-
Security is handled automatically for you. Only the Rails app is able to publish. Users are only able to
|
45
|
+
Security is handled automatically for you. Only the Rails app is able to publish. Users are only able to receive messages on the channels you subscribe them to. This means every channel is private.
|
62
46
|
|
63
|
-
Here's how it works. The +
|
47
|
+
Here's how it works. The +subscribe_to+ helper will output an element containing data information about the channel.
|
64
48
|
|
65
|
-
<span class="private_pub_subscription" data-channel="/messages/new" data-
|
49
|
+
<span class="private_pub_subscription" data-channel="/messages/new" data-signature="2aae6c35c94fcfb415dbe95f408b9ce91ee846ed" data-timestamp="13019431281234"></span>
|
66
50
|
|
67
|
-
The
|
51
|
+
The data-signature is a combination of the channel, timestamp, and secret token set in the Rails app. This is checked by the Faye extension when subscribing to a channel to ensure the signature is correct. The signature is automatically expired after 1 hour but this can be configured.
|
68
52
|
|
69
|
-
PrivatePub.
|
53
|
+
PrivatePub.signature_expiration = 10.minutes
|
70
54
|
|
71
55
|
Or +nil+ for no expiration. Note: if Faye is on a separate server from the Rails app it's important that the time is in sync.
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
class PrivatePub
|
2
|
+
module Generators
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
4
|
+
def self.source_root
|
5
|
+
File.dirname(__FILE__) + "/templates"
|
6
|
+
end
|
7
|
+
|
8
|
+
def copy_files
|
9
|
+
template "private_pub_initializer.rb", "config/initializers/private_pub.rb"
|
10
|
+
copy_file "private_pub_helper.rb", "app/helpers/private_pub_helper.rb"
|
11
|
+
copy_file "private_pub.js", "public/javascripts/private_pub.js"
|
12
|
+
copy_file "faye.ru", "faye.ru"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
PrivatePubExtension = {
|
2
|
+
outgoing: function(message, callback) {
|
3
|
+
if (message.channel == "/meta/subscribe") {
|
4
|
+
// Attach the signature and timestamp to subscription messages
|
5
|
+
var subscription = $(".private_pub_subscription[data-channel='" + message.subscription + "']");
|
6
|
+
if (!message.ext) message.ext = {};
|
7
|
+
message.ext.private_pub_signature = subscription.data("signature");
|
8
|
+
message.ext.private_pub_timestamp = subscription.data("timestamp");
|
9
|
+
}
|
10
|
+
callback(message);
|
11
|
+
}
|
12
|
+
};
|
13
|
+
|
14
|
+
jQuery(function() {
|
15
|
+
var faye;
|
16
|
+
if ($(".private_pub_subscription").length > 0) {
|
17
|
+
jQuery.getScript($(".private_pub_subscription").data("server") + ".js", function() {
|
18
|
+
faye = new Faye.Client($(".private_pub_subscription").data("server"));
|
19
|
+
faye.addExtension(PrivatePubExtension);
|
20
|
+
$(".private_pub_subscription").each(function(index) {
|
21
|
+
faye.subscribe($(this).data("channel"), function(data) {
|
22
|
+
if (data._eval) eval(data._eval);
|
23
|
+
});
|
24
|
+
});
|
25
|
+
});
|
26
|
+
}
|
27
|
+
});
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module PrivatePubHelper
|
2
|
+
def publish_to(channel, &block)
|
3
|
+
message = {:channel => channel, :data => {:_eval => capture(&block)}, :ext => {:private_pub_token => PrivatePub.secret_token}}
|
4
|
+
PrivatePub.publish(:message => message.to_json)
|
5
|
+
end
|
6
|
+
|
7
|
+
def subscribe_to(channel)
|
8
|
+
subscription = PrivatePub.subscription(:channel => channel)
|
9
|
+
content_tag :span, "", :class => "private_pub_subscription",
|
10
|
+
"data-server" => PrivatePub.server,
|
11
|
+
"data-channel" => subscription[:channel],
|
12
|
+
"data-signature" => subscription[:signature],
|
13
|
+
"data-timestamp" => subscription[:timestamp]
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class PrivatePub
|
2
|
+
class FayeExtension
|
3
|
+
def incoming(message, callback)
|
4
|
+
if message["channel"] == "/meta/subscribe"
|
5
|
+
authenticate_subscribe(message)
|
6
|
+
elsif message["channel"] !~ %r{^/meta/}
|
7
|
+
authenticate_publish(message)
|
8
|
+
end
|
9
|
+
callback.call(message)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def authenticate_subscribe(message)
|
15
|
+
subscription = PrivatePub.subscription(:channel => message["subscription"], :timestamp => message["ext"]["private_pub_timestamp"])
|
16
|
+
if message["ext"]["private_pub_signature"] != subscription[:signature]
|
17
|
+
message["error"] = "Incorrect signature."
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def authenticate_publish(message)
|
22
|
+
if PrivatePub.secret_token.nil?
|
23
|
+
raise Error, "No token set in PrivatePub.secret_token, set this to match the token used in the web app."
|
24
|
+
elsif message["ext"]["private_pub_token"] != PrivatePub.secret_token
|
25
|
+
message["error"] = "Incorrect token."
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/private_pub.rb
CHANGED
@@ -1 +1,57 @@
|
|
1
|
-
|
1
|
+
require "digest/sha1"
|
2
|
+
require "net/http"
|
3
|
+
|
4
|
+
require "private_pub/faye_extension"
|
5
|
+
|
6
|
+
class PrivatePub
|
7
|
+
class Error < StandardError; end
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def server=(server)
|
11
|
+
@config[:server] = server
|
12
|
+
end
|
13
|
+
|
14
|
+
def server
|
15
|
+
@config[:server]
|
16
|
+
end
|
17
|
+
|
18
|
+
def signature_expiration=(signature_expiration)
|
19
|
+
@config[:signature_expiration] = signature_expiration
|
20
|
+
end
|
21
|
+
|
22
|
+
def signature_expiration
|
23
|
+
@config[:signature_expiration]
|
24
|
+
end
|
25
|
+
|
26
|
+
def secret_token=(secret_token)
|
27
|
+
@config[:secret_token] = secret_token
|
28
|
+
end
|
29
|
+
|
30
|
+
def secret_token
|
31
|
+
@config[:secret_token]
|
32
|
+
end
|
33
|
+
|
34
|
+
def reset_config
|
35
|
+
@config = {
|
36
|
+
:server => "http://localhost:9292/faye",
|
37
|
+
:signature_expiration => 60 * 60, # one hour
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def subscription(options = {})
|
42
|
+
sub = {:timestamp => (Time.now.to_f * 1000).round}.merge(options)
|
43
|
+
sub[:signature] = Digest::SHA1.hexdigest([secret_token, sub[:channel], sub[:timestamp]].join)
|
44
|
+
sub
|
45
|
+
end
|
46
|
+
|
47
|
+
def publish(data)
|
48
|
+
Net::HTTP.post_form(URI.parse(PrivatePub.server), data)
|
49
|
+
end
|
50
|
+
|
51
|
+
def faye_extension
|
52
|
+
FayeExtension.new
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
reset_config
|
57
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe PrivatePub::FayeExtension do
|
4
|
+
before(:each) do
|
5
|
+
PrivatePub.reset_config
|
6
|
+
@faye = PrivatePub::FayeExtension.new
|
7
|
+
@message = {"channel" => "/meta/subscribe", "ext" => {}}
|
8
|
+
end
|
9
|
+
|
10
|
+
it "adds an error on an incoming subscription with a bad signature" do
|
11
|
+
@message["subscription"] = "hello"
|
12
|
+
@message["ext"]["private_pub_signature"] = "bad"
|
13
|
+
@message["ext"]["private_pub_timestamp"] = "123"
|
14
|
+
message = @faye.incoming(@message, lambda { |m| m })
|
15
|
+
message["error"].should == "Incorrect signature."
|
16
|
+
end
|
17
|
+
|
18
|
+
it "has no error when the signature matches the subscription" do
|
19
|
+
sub = PrivatePub.subscription(:timestamp => 123, :channel => "hello")
|
20
|
+
@message["subscription"] = sub[:channel]
|
21
|
+
@message["ext"]["private_pub_signature"] = sub[:signature]
|
22
|
+
@message["ext"]["private_pub_timestamp"] = sub[:timestamp]
|
23
|
+
message = @faye.incoming(@message, lambda { |m| m })
|
24
|
+
message["error"].should be_nil
|
25
|
+
end
|
26
|
+
|
27
|
+
it "has an error when trying to publish to a custom channel with a bad token" do
|
28
|
+
PrivatePub.secret_token = "good"
|
29
|
+
@message["channel"] = "/custom/channel"
|
30
|
+
@message["ext"]["private_pub_token"] = "bad"
|
31
|
+
message = @faye.incoming(@message, lambda { |m| m })
|
32
|
+
message["error"].should == "Incorrect token."
|
33
|
+
end
|
34
|
+
|
35
|
+
it "raises an exception when attempting to call a custom channel without a secret_token set" do
|
36
|
+
@message["channel"] = "/custom/channel"
|
37
|
+
@message["ext"]["private_pub_token"] = "bad"
|
38
|
+
lambda {
|
39
|
+
message = @faye.incoming(@message, lambda { |m| m })
|
40
|
+
}.should raise_error("No token set in PrivatePub.secret_token, set this to match the token used in the web app.")
|
41
|
+
end
|
42
|
+
|
43
|
+
it "has no error on other meta calls" do
|
44
|
+
@message["channel"] = "/meta/connect"
|
45
|
+
message = @faye.incoming(@message, lambda { |m| m })
|
46
|
+
message["error"].should be_nil
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe PrivatePub do
|
4
|
+
before(:each) do
|
5
|
+
PrivatePub.reset_config
|
6
|
+
end
|
7
|
+
|
8
|
+
it "has secret token, server, and signature expiration settings" do
|
9
|
+
PrivatePub.secret_token = "secret token"
|
10
|
+
PrivatePub.secret_token.should == "secret token"
|
11
|
+
PrivatePub.server = "http://localhost/"
|
12
|
+
PrivatePub.server.should == "http://localhost/"
|
13
|
+
PrivatePub.signature_expiration = 1000
|
14
|
+
PrivatePub.signature_expiration.should == 1000
|
15
|
+
end
|
16
|
+
|
17
|
+
it "defaults server to localhost:9292/faye" do
|
18
|
+
PrivatePub.server.should == "http://localhost:9292/faye"
|
19
|
+
end
|
20
|
+
|
21
|
+
it "defaults signature_expiration to 1 hour" do
|
22
|
+
PrivatePub.signature_expiration.should == 60 * 60
|
23
|
+
end
|
24
|
+
|
25
|
+
it "defaults subscription timestamp to current time in milliseconds" do
|
26
|
+
time = Time.now
|
27
|
+
Time.stub!(:now).and_return(time)
|
28
|
+
PrivatePub.subscription[:timestamp].should == (time.to_f * 1000).round
|
29
|
+
end
|
30
|
+
|
31
|
+
it "includes channel and custom time in subscription" do
|
32
|
+
subscription = PrivatePub.subscription(:timestamp => 123, :channel => "hello")
|
33
|
+
subscription[:timestamp].should == 123
|
34
|
+
subscription[:channel].should == "hello"
|
35
|
+
end
|
36
|
+
|
37
|
+
it "does a sha1 digest of channel, timestamp, and secret token" do
|
38
|
+
PrivatePub.secret_token = "token"
|
39
|
+
subscription = PrivatePub.subscription(:timestamp => 123, :channel => "channel")
|
40
|
+
subscription[:signature].should == Digest::SHA1.hexdigest("tokenchannel123")
|
41
|
+
end
|
42
|
+
|
43
|
+
it "publishes to server using Net::HTTP" do
|
44
|
+
Net::HTTP.should_receive(:post_form).with(URI.parse(PrivatePub.server), "hello world").and_return(:result)
|
45
|
+
PrivatePub.publish("hello world").should == :result
|
46
|
+
end
|
47
|
+
|
48
|
+
it "has a FayeExtension instance" do
|
49
|
+
PrivatePub.faye_extension.should be_kind_of(PrivatePub::FayeExtension)
|
50
|
+
end
|
51
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: private_pub
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.
|
5
|
+
version: 0.1.0
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Ryan Bates
|
@@ -12,8 +12,18 @@ cert_chain: []
|
|
12
12
|
|
13
13
|
date: 2011-04-04 00:00:00 -07:00
|
14
14
|
default_executable:
|
15
|
-
dependencies:
|
16
|
-
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: rspec
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ~>
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: 2.1.0
|
25
|
+
type: :development
|
26
|
+
version_requirements: *id001
|
17
27
|
description: Private pub/sub messaging in Rails through Faye.
|
18
28
|
email: ryan@railscasts.com
|
19
29
|
executables: []
|
@@ -23,7 +33,19 @@ extensions: []
|
|
23
33
|
extra_rdoc_files: []
|
24
34
|
|
25
35
|
files:
|
36
|
+
- lib/generators/private_pub/install_generator.rb
|
37
|
+
- lib/generators/private_pub/templates/faye.ru
|
38
|
+
- lib/generators/private_pub/templates/private_pub.js
|
39
|
+
- lib/generators/private_pub/templates/private_pub_helper.rb
|
40
|
+
- lib/generators/private_pub/templates/private_pub_initializer.rb
|
41
|
+
- lib/private_pub/faye_extension.rb
|
26
42
|
- lib/private_pub.rb
|
43
|
+
- spec/private_pub/faye_extension_spec.rb
|
44
|
+
- spec/private_pub_spec.rb
|
45
|
+
- spec/spec_helper.rb
|
46
|
+
- CHANGELOG.rdoc
|
47
|
+
- Gemfile
|
48
|
+
- Rakefile
|
27
49
|
- README.rdoc
|
28
50
|
has_rdoc: true
|
29
51
|
homepage: http://github.com/ryanb/private_pub
|