private_pub 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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) %>"