private_pub 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc CHANGED
@@ -1,3 +1,12 @@
1
+ 0.2.0 (April 7, 2011)
2
+
3
+ * switched to YAML file for config. BACKWARDS INCOMPATIBLE: you will need to remove config/initializers/private_pub.rb
4
+
5
+ * moved view helpers into Railtie so helper file is no longer generated
6
+
7
+ * error out when signature has expired
8
+
9
+
1
10
  0.1.0 (April 4, 2011)
2
11
 
3
12
  * initial release
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Ryan Bates
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc CHANGED
@@ -1,6 +1,8 @@
1
1
  = Private Pub
2
2
 
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.
3
+ This is a Ruby gem for use with Rails to publish and subscribe to messages through {Faye}[http://faye.jcoglan.com/]. All channels are automatically made private.
4
+
5
+ This project was motivated by {Railscasts episode #260}[http://railscasts.com/episodes/260-messaging-with-faye].
4
6
 
5
7
 
6
8
  == Setup
@@ -22,7 +24,7 @@ Next, install and start up Faye using the rackup file that was generated.
22
24
  gem install faye
23
25
  rackup faye.ru -s thin -E production
24
26
 
25
- It's not necessary to add the faye.js since that will be handled automatically.
27
+ It's not necessary to include faye.js since that will be handled automatically.
26
28
 
27
29
 
28
30
  == Usage
@@ -31,13 +33,13 @@ Use the +subscribe_to+ helper method on any page to subscribe to a channel.
31
33
 
32
34
  <%= subscribe_to "/messages/new" %>
33
35
 
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.
36
+ 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 in a JavaScript AJAX response (such as a <tt>create.js.erb</tt> file).
35
37
 
36
38
  <% publish_to "/messages/new" do %>
37
39
  $("#chat").append("<%= escape_javascript render(@messages) %>");
38
40
  <% end %>
39
41
 
40
- There will be alternative ways to publish/subscribe to messages in the future.
42
+ There will be alternative ways to publish/subscribe to channels in the future.
41
43
 
42
44
 
43
45
  == Security
@@ -48,8 +50,17 @@ Here's how it works. The +subscribe_to+ helper will output an element containing
48
50
 
49
51
  <span class="private_pub_subscription" data-channel="/messages/new" data-signature="2aae6c35c94fcfb415dbe95f408b9ce91ee846ed" data-timestamp="13019431281234"></span>
50
52
 
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.
53
+ 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 in the generated YAML file.
54
+
55
+ signature_expiration: 600 # 10 minutes, expressed in seconds
56
+
57
+ Or use a blank value for no expiration.
58
+
59
+ signature_expiration:
60
+
61
+ Note: if Faye is on a separate server from the Rails app it's important that the system clocks be in sync.
62
+
52
63
 
53
- PrivatePub.signature_expiration = 10.minutes
64
+ == Development & Feedback
54
65
 
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.
66
+ Questions or comments? Please use the {issue tracker}[https://github.com/ryanb/private_pub/issues]. If you would like to contribue to this project, clone this repository and run +bundle+ and +rake+ to run the tests.
@@ -1,4 +1,4 @@
1
- class PrivatePub
1
+ module PrivatePub
2
2
  module Generators
3
3
  class InstallGenerator < Rails::Generators::Base
4
4
  def self.source_root
@@ -6,8 +6,9 @@ class PrivatePub
6
6
  end
7
7
 
8
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"
9
+ remove_file "config/initializers/private_pub.rb"
10
+ remove_file "app/helpers/private_pub_helper.rb"
11
+ template "private_pub.yml", "config/private_pub.yml"
11
12
  copy_file "private_pub.js", "public/javascripts/private_pub.js"
12
13
  copy_file "faye.ru", "faye.ru"
13
14
  end
@@ -1,7 +1,12 @@
1
+ # Run with: rackup faye.ru -s thin -E production
2
+ require "yaml"
1
3
  require "faye"
2
- require "private_pub"
3
- require File.expand_path("../config/initializers/private_pub.rb", __FILE__)
4
+ begin
5
+ require "private_pub"
6
+ rescue LoadError
7
+ require "bundler/setup"
8
+ require "private_pub"
9
+ end
4
10
 
5
- faye_server = Faye::RackAdapter.new(:mount => '/faye', :timeout => 45)
6
- faye_server.add_extension(PrivatePub.faye_extension)
7
- run faye_server
11
+ PrivatePub.load_config(File.expand_path("../config/private_pub.yml", __FILE__), ENV["RAILS_ENV"] || "development")
12
+ run Faye::RackAdapter.new(:mount => "/faye", :timeout => 45, :extensions => [PrivatePub.faye_extension])
@@ -0,0 +1,9 @@
1
+ development:
2
+ server: "http://localhost:9292/faye"
3
+ secret_token: "secret"
4
+ test:
5
+ server: "http://localhost:9292/faye"
6
+ secret_token: "secret"
7
+ production:
8
+ server: "http://example.com/faye"
9
+ secret_token: "<%= ActiveSupport::SecureRandom.hex(32) %>"
@@ -1,4 +1,4 @@
1
- class PrivatePub
1
+ module PrivatePub
2
2
  class FayeExtension
3
3
  def incoming(message, callback)
4
4
  if message["channel"] == "/meta/subscribe"
@@ -15,13 +15,15 @@ class PrivatePub
15
15
  subscription = PrivatePub.subscription(:channel => message["subscription"], :timestamp => message["ext"]["private_pub_timestamp"])
16
16
  if message["ext"]["private_pub_signature"] != subscription[:signature]
17
17
  message["error"] = "Incorrect signature."
18
+ elsif PrivatePub.signature_expired? message["ext"]["private_pub_timestamp"].to_i
19
+ message["error"] = "Signature has expired."
18
20
  end
19
21
  end
20
22
 
21
23
  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
24
+ if PrivatePub.config[:secret_token].nil?
25
+ raise Error, "No secret_token config set, ensure private_pub.yml is loaded properly."
26
+ elsif message["ext"]["private_pub_token"] != PrivatePub.config[:secret_token]
25
27
  message["error"] = "Incorrect token."
26
28
  end
27
29
  end
@@ -0,0 +1,14 @@
1
+ require "private_pub/view_helpers"
2
+
3
+ module PrivatePub
4
+ class Railtie < Rails::Railtie
5
+ initializer "private_pub.config" do
6
+ path = Rails.root.join("config/private_pub.yml")
7
+ PrivatePub.load_config(path, Rails.env) if path.exist?
8
+ end
9
+
10
+ initializer "private_pub.view_helpers" do
11
+ ActionView::Base.send :include, ViewHelpers
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ module PrivatePub
2
+ module ViewHelpers
3
+ def publish_to(channel, &block)
4
+ message = {:channel => channel, :data => {:_eval => capture(&block)}, :ext => {:private_pub_token => PrivatePub.config[:secret_token]}}
5
+ PrivatePub.publish(:message => message.to_json)
6
+ end
7
+
8
+ def subscribe_to(channel)
9
+ subscription = PrivatePub.subscription(:channel => channel)
10
+ content_tag :span, "", :class => "private_pub_subscription",
11
+ "data-server" => PrivatePub.config[:server],
12
+ "data-channel" => subscription[:channel],
13
+ "data-signature" => subscription[:signature],
14
+ "data-timestamp" => subscription[:timestamp]
15
+ end
16
+ end
17
+ end
data/lib/private_pub.rb CHANGED
@@ -2,34 +2,13 @@ require "digest/sha1"
2
2
  require "net/http"
3
3
 
4
4
  require "private_pub/faye_extension"
5
+ require "private_pub/railtie" if defined? Rails
5
6
 
6
- class PrivatePub
7
+ module PrivatePub
7
8
  class Error < StandardError; end
8
9
 
9
10
  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
11
+ attr_reader :config
33
12
 
34
13
  def reset_config
35
14
  @config = {
@@ -38,19 +17,29 @@ class PrivatePub
38
17
  }
39
18
  end
40
19
 
20
+ def load_config(filename, environment)
21
+ yaml = YAML.load_file(filename)[environment.to_s]
22
+ raise ArgumentError, "The #{environment} environment does not exist in #{filename}" if yaml.nil?
23
+ yaml.each { |k, v| config[k.to_sym] = v }
24
+ end
25
+
41
26
  def subscription(options = {})
42
27
  sub = {:timestamp => (Time.now.to_f * 1000).round}.merge(options)
43
- sub[:signature] = Digest::SHA1.hexdigest([secret_token, sub[:channel], sub[:timestamp]].join)
28
+ sub[:signature] = Digest::SHA1.hexdigest([config[:secret_token], sub[:channel], sub[:timestamp]].join)
44
29
  sub
45
30
  end
46
31
 
47
32
  def publish(data)
48
- Net::HTTP.post_form(URI.parse(PrivatePub.server), data)
33
+ Net::HTTP.post_form(URI.parse(config[:server]), data)
49
34
  end
50
35
 
51
36
  def faye_extension
52
37
  FayeExtension.new
53
38
  end
39
+
40
+ def signature_expired?(timestamp)
41
+ timestamp < ((Time.now.to_f - config[:signature_expiration])*1000).round if config[:signature_expiration]
42
+ end
54
43
  end
55
44
 
56
45
  reset_config
@@ -0,0 +1,10 @@
1
+ development:
2
+ server: http://dev.local:9292/faye
3
+ secret_token: DEVELOPMENT_SECRET_TOKEN
4
+ signature_expiration: 600
5
+ production:
6
+ server: http://example.com/faye
7
+ secret_token: PRODUCTION_SECRET_TOKEN
8
+ signature_expiration: 600
9
+ no_signature_expiration:
10
+ signature_expiration:
@@ -16,7 +16,7 @@ describe PrivatePub::FayeExtension do
16
16
  end
17
17
 
18
18
  it "has no error when the signature matches the subscription" do
19
- sub = PrivatePub.subscription(:timestamp => 123, :channel => "hello")
19
+ sub = PrivatePub.subscription(:channel => "hello")
20
20
  @message["subscription"] = sub[:channel]
21
21
  @message["ext"]["private_pub_signature"] = sub[:signature]
22
22
  @message["ext"]["private_pub_timestamp"] = sub[:timestamp]
@@ -24,8 +24,17 @@ describe PrivatePub::FayeExtension do
24
24
  message["error"].should be_nil
25
25
  end
26
26
 
27
+ it "has an error when signature is just expired" do
28
+ sub = PrivatePub.subscription(:timestamp => 123, :channel => "hello")
29
+ @message["subscription"] = sub[:channel]
30
+ @message["ext"]["private_pub_signature"] = sub[:signature]
31
+ @message["ext"]["private_pub_timestamp"] = sub[:timestamp]
32
+ message = @faye.incoming(@message, lambda { |m| m })
33
+ message["error"].should == "Signature has expired."
34
+ end
35
+
27
36
  it "has an error when trying to publish to a custom channel with a bad token" do
28
- PrivatePub.secret_token = "good"
37
+ PrivatePub.config[:secret_token] = "good"
29
38
  @message["channel"] = "/custom/channel"
30
39
  @message["ext"]["private_pub_token"] = "bad"
31
40
  message = @faye.incoming(@message, lambda { |m| m })
@@ -37,7 +46,7 @@ describe PrivatePub::FayeExtension do
37
46
  @message["ext"]["private_pub_token"] = "bad"
38
47
  lambda {
39
48
  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.")
49
+ }.should raise_error("No secret_token config set, ensure private_pub.yml is loaded properly.")
41
50
  end
42
51
 
43
52
  it "has no error on other meta calls" do
@@ -5,21 +5,12 @@ describe PrivatePub do
5
5
  PrivatePub.reset_config
6
6
  end
7
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
8
  it "defaults server to localhost:9292/faye" do
18
- PrivatePub.server.should == "http://localhost:9292/faye"
9
+ PrivatePub.config[:server].should == "http://localhost:9292/faye"
19
10
  end
20
11
 
21
12
  it "defaults signature_expiration to 1 hour" do
22
- PrivatePub.signature_expiration.should == 60 * 60
13
+ PrivatePub.config[:signature_expiration].should == 60 * 60
23
14
  end
24
15
 
25
16
  it "defaults subscription timestamp to current time in milliseconds" do
@@ -28,6 +19,24 @@ describe PrivatePub do
28
19
  PrivatePub.subscription[:timestamp].should == (time.to_f * 1000).round
29
20
  end
30
21
 
22
+ it "loads a simple configuration file via load_config" do
23
+ PrivatePub.load_config("spec/fixtures/private_pub.yml", "production")
24
+ PrivatePub.config[:server].should == "http://example.com/faye"
25
+ PrivatePub.config[:secret_token].should == "PRODUCTION_SECRET_TOKEN"
26
+ PrivatePub.config[:signature_expiration].should == 600
27
+ end
28
+
29
+ it "supports a nil signature_expiration via a blank value in the configuration file" do
30
+ PrivatePub.load_config("spec/fixtures/private_pub.yml", :no_signature_expiration)
31
+ PrivatePub.config[:signature_expiration].should be_nil
32
+ end
33
+
34
+ it "raises an exception if an invalid environment is passed to load_config" do
35
+ lambda {
36
+ PrivatePub.load_config("spec/fixtures/private_pub.yml", :test)
37
+ }.should raise_error ArgumentError
38
+ end
39
+
31
40
  it "includes channel and custom time in subscription" do
32
41
  subscription = PrivatePub.subscription(:timestamp => 123, :channel => "hello")
33
42
  subscription[:timestamp].should == 123
@@ -35,17 +44,34 @@ describe PrivatePub do
35
44
  end
36
45
 
37
46
  it "does a sha1 digest of channel, timestamp, and secret token" do
38
- PrivatePub.secret_token = "token"
47
+ PrivatePub.config[:secret_token] = "token"
39
48
  subscription = PrivatePub.subscription(:timestamp => 123, :channel => "channel")
40
49
  subscription[:signature].should == Digest::SHA1.hexdigest("tokenchannel123")
41
50
  end
42
51
 
43
52
  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)
53
+ Net::HTTP.should_receive(:post_form).with(URI.parse(PrivatePub.config[:server]), "hello world").and_return(:result)
45
54
  PrivatePub.publish("hello world").should == :result
46
55
  end
47
56
 
48
57
  it "has a FayeExtension instance" do
49
58
  PrivatePub.faye_extension.should be_kind_of(PrivatePub::FayeExtension)
50
59
  end
60
+
61
+ it "says signature has expired when time passed in is greater than expiration" do
62
+ PrivatePub.config[:signature_expiration] = 30*60
63
+ time = PrivatePub.subscription[:timestamp] - 31*60*1000
64
+ PrivatePub.signature_expired?(time).should be_true
65
+ end
66
+
67
+ it "says signature has not expired when time passed in is less than expiration" do
68
+ PrivatePub.config[:signature_expiration] = 30*60
69
+ time = PrivatePub.subscription[:timestamp] - 29*60*1000
70
+ PrivatePub.signature_expired?(time).should be_false
71
+ end
72
+
73
+ it "says signature has not expired when expiration is nil" do
74
+ PrivatePub.config[:signature_expiration] = nil
75
+ PrivatePub.signature_expired?(0).should be_false
76
+ end
51
77
  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.1.0
5
+ version: 0.2.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - Ryan Bates
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-04-04 00:00:00 -07:00
13
+ date: 2011-04-07 00:00:00 -07:00
14
14
  default_executable:
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
@@ -36,15 +36,18 @@ files:
36
36
  - lib/generators/private_pub/install_generator.rb
37
37
  - lib/generators/private_pub/templates/faye.ru
38
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
39
+ - lib/generators/private_pub/templates/private_pub.yml
41
40
  - lib/private_pub/faye_extension.rb
41
+ - lib/private_pub/railtie.rb
42
+ - lib/private_pub/view_helpers.rb
42
43
  - lib/private_pub.rb
44
+ - spec/fixtures/private_pub.yml
43
45
  - spec/private_pub/faye_extension_spec.rb
44
46
  - spec/private_pub_spec.rb
45
47
  - spec/spec_helper.rb
46
48
  - CHANGELOG.rdoc
47
49
  - Gemfile
50
+ - LICENSE
48
51
  - Rakefile
49
52
  - README.rdoc
50
53
  has_rdoc: true
@@ -1,15 +0,0 @@
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
@@ -1,2 +0,0 @@
1
- PrivatePub.server = "http://localhost:9292/faye"
2
- PrivatePub.secret_token = "<%= ActiveSupport::SecureRandom.hex(32) %>"