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 ADDED
@@ -0,0 +1,3 @@
1
+ 0.1.0 (April 4, 2011)
2
+
3
+ * initial release
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/README.rdoc CHANGED
@@ -1,71 +1,55 @@
1
1
  = Private Pub
2
2
 
3
- THIS PROJECT IS CURRENTLY VAPORWARE. I am writing the {readme first}[http://tom.preston-werner.com/2010/08/23/readme-driven-development.html].
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 JavaScript file and initializer.
12
+ Run the generator to create the initial files.
15
13
 
16
14
  rails g private_pub:install
17
15
 
18
- Add the JavaScript file to your layout file.
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 setup Faye and configure the generated initializer file to point to the faye server if it's not already. DON'T add the faye.js file to your layout because this will be done automatically.
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
- Finally add the PrivatePub extension to Faye through the rackup file. You'll need to load the initializer file there too so it sets the secret token (or set it some other way).
22
+ gem install faye
23
+ rackup faye.ru -s thin -E production
27
24
 
28
- require "private_pub"
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
- This adds two helper methods to Rails: +subscribe_to+ and +publish_to+. Let's say you're building a chat application. On the page displaying a chat you can subscribe to anything incoming on the "/messages/new" channel.
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
- And then publish to that channel when a new message is added in the Rails app.
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
- That bit of JavaScript will be published and executed to all subscribing clients.
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 subscribe to messages on the channels they subscribe to. This means every channel is private.
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 +subscribe+ helper will output an element containing data information about the channel.
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-key="2aae6c35c94fcfb415dbe95f408b9ce91ee846ed" timestamp="13019431281234"></span>
49
+ <span class="private_pub_subscription" data-channel="/messages/new" data-signature="2aae6c35c94fcfb415dbe95f408b9ce91ee846ed" data-timestamp="13019431281234"></span>
66
50
 
67
- The key is a combination of the message name, timestamp, and secret token set in the Rails app. This is checked by the Faye extension when subscribing to a channel to ensure the key is correct. The key is automatically expired after 1 hour but this can be configured.
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.key_expiration = 10.minutes
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,10 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rspec/core/rake_task'
4
+
5
+ desc "Run RSpec"
6
+ RSpec::Core::RakeTask.new do |t|
7
+ t.verbose = false
8
+ end
9
+
10
+ task :default => :spec
@@ -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,7 @@
1
+ require "faye"
2
+ require "private_pub"
3
+ require File.expand_path("../config/initializers/private_pub.rb", __FILE__)
4
+
5
+ faye_server = Faye::RackAdapter.new(:mount => '/faye', :timeout => 45)
6
+ faye_server.add_extension(PrivatePub.faye_extension)
7
+ run faye_server
@@ -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,2 @@
1
+ PrivatePub.server = "http://localhost:9292/faye"
2
+ PrivatePub.secret_token = "<%= ActiveSupport::SecureRandom.hex(32) %>"
@@ -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
- # TODO
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
@@ -0,0 +1,6 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ Bundler.require(:default)
4
+
5
+ RSpec.configure do |config|
6
+ end
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: private_pub
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.0.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