private_pub 0.3.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## 1.0.0 (January 15, 2012)
2
+
3
+ * setting config defaults to nil so everything must be set in `private_pub.yml`
4
+
5
+ * Documentation improvements
6
+
7
+
1
8
  ## 0.3.0 (January 14, 2012)
2
9
 
3
10
  * adding `PrivatePub.publish_to` method for publishing from anywhere - issue #15
data/LICENSE CHANGED
@@ -1,5 +1,5 @@
1
1
  Copyright (c) 2012 Ryan Bates
2
-
2
+
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
5
5
  "Software"), to deal in the Software without restriction, including
@@ -7,10 +7,10 @@ without limitation the rights to use, copy, modify, merge, publish,
7
7
  distribute, sublicense, and/or sell copies of the Software, and to
8
8
  permit persons to whom the Software is furnished to do so, subject to
9
9
  the following conditions:
10
-
10
+
11
11
  The above copyright notice and this permission notice shall be
12
12
  included in all copies or substantial portions of the Software.
13
-
13
+
14
14
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
15
  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
16
  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
data/README.md CHANGED
@@ -61,7 +61,7 @@ This JavaScript will be immediately evaluated on all clients who have subscribed
61
61
 
62
62
  ## Alternative Usage
63
63
 
64
- If you prefer to work through JSON instead of JavaScript templates to handle AJAX responses, you can pass an argument to `publish_to` instead of a block and it will be converted `to_json` behind the scenes. This can also be done anywhere (such as the controller).
64
+ If you prefer to work through JSON instead of `.js.erb` templates, you can pass a hash to `publish_to` instead of a block and it will be converted `to_json` behind the scenes. This can be done anywhere (such as the controller).
65
65
 
66
66
  ```ruby
67
67
  PrivatePub.publish_to "/messages/new", :chat_message => "Hello, world!"
@@ -75,14 +75,21 @@ PrivatePub.subscribe("/messages/new", function(data, channel) {
75
75
  });
76
76
  ```
77
77
 
78
- The Ruby `subscribe_to` call is still necessary with this approach to grant the user access to the channel. The JavaScript is just a callback for any custom behavior.
78
+ The Ruby `subscribe_to` helper call is still necessary with this approach to grant the user access to the channel. The JavaScript is just a callback for any custom behavior.
79
79
 
80
80
 
81
- ## Security
81
+ ## Configuration
82
82
 
83
- Security is handled automatically for you. Only the Rails app is able to publish messages. Users are only able to receive messages on the channels you subscribe them to so every channel is private.
83
+ The configuration is set separately for each environment in the generated `config/private_pub.yml` file. Here are the options.
84
84
 
85
- Here's how it works. The `subscribe_to` helper will output a script element containing data information about the channel.
85
+ * `server`: The URL to use for the Faye server such as `http://localhost:9292/faye`.
86
+ * `secret_token`: A secret hash to secure the server. Can be any string.
87
+ * `signature_expiration`: The length of time in seconds before a subscription signature expires. If this is not set there is no expiration. Note: if Faye is on a separate server from the Rails app, the system clocks must be in sync for the expiration to work properly.
88
+
89
+
90
+ ## How It Works
91
+
92
+ The `subscribe_to` helper will output the following script which subscribes the user to a specific channel and server.
86
93
 
87
94
  ```html
88
95
  <script type="text/javascript">
@@ -95,21 +102,11 @@ Here's how it works. The `subscribe_to` helper will output a script element cont
95
102
  </script>
96
103
  ```
97
104
 
98
- The 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 automatically expires after 1 hour but this can be configured in the generated YAML config file.
99
-
100
- ```yaml
101
- signature_expiration: 600 # 10 minutes, expressed in seconds
102
- ```
103
-
104
- Or use a blank value for no expiration.
105
-
106
- ```yaml
107
- signature_expiration:
108
- ```
105
+ The signature and timestamp checked on the Faye server to ensure users are only able to access channels you subscribe them to. The signature will automatically expire after the time specified in the configuration.
109
106
 
110
- Note: if Faye is on a separate server from the Rails app it's important that the system clocks be in sync so the expiration works properly.
107
+ The `publish_to` method will send a post request to the Faye server (using `Net::HTTP`) instructing it to send the given data back to the browser.
111
108
 
112
109
 
113
110
  ## Development & Feedback
114
111
 
115
- 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.
112
+ Questions or comments? Please use the [issue tracker](https://github.com/ryanb/private_pub/issues). Tests can be run with `bundle` and `rake` commands.
@@ -7,3 +7,4 @@ test:
7
7
  production:
8
8
  server: "http://example.com/faye"
9
9
  secret_token: "<%= ActiveSupport::SecureRandom.hex(32) %>"
10
+ signature_expiration: 3600 # one hour
data/lib/private_pub.rb CHANGED
@@ -10,29 +10,31 @@ module PrivatePub
10
10
  class << self
11
11
  attr_reader :config
12
12
 
13
+ # Resets the configuration to the default (empty hash)
13
14
  def reset_config
14
- @config = {
15
- :server => "http://localhost:9292/faye",
16
- :signature_expiration => 60 * 60, # one hour
17
- }
15
+ @config = {}
18
16
  end
19
17
 
18
+ # Loads the configuration from a given YAML file and environment (such as production)
20
19
  def load_config(filename, environment)
21
20
  yaml = YAML.load_file(filename)[environment.to_s]
22
21
  raise ArgumentError, "The #{environment} environment does not exist in #{filename}" if yaml.nil?
23
22
  yaml.each { |k, v| config[k.to_sym] = v }
24
23
  end
25
24
 
26
- def subscription(options = {})
27
- sub = {:timestamp => (Time.now.to_f * 1000).round}.merge(options)
28
- sub[:signature] = Digest::SHA1.hexdigest([config[:secret_token], sub[:channel], sub[:timestamp]].join)
29
- sub
30
- end
31
-
25
+ # Publish the given data to a specific channel. This ends up sending
26
+ # a Net::HTTP POST request to the Faye server.
32
27
  def publish_to(channel, data)
33
28
  publish_message(message(channel, data))
34
29
  end
35
30
 
31
+ # Sends the given message hash to the Faye server using Net::HTTP.
32
+ def publish_message(message)
33
+ raise Error, "No server specified, ensure private_pub.yml was loaded properly." unless config[:server]
34
+ Net::HTTP.post_form(URI.parse(config[:server]), :message => message.to_json)
35
+ end
36
+
37
+ # Returns a message hash for sending to Faye
36
38
  def message(channel, data)
37
39
  message = {:channel => channel, :data => {:channel => channel}, :ext => {:private_pub_token => config[:secret_token]}}
38
40
  if data.kind_of? String
@@ -43,14 +45,21 @@ module PrivatePub
43
45
  message
44
46
  end
45
47
 
46
- def publish_message(message)
47
- Net::HTTP.post_form(URI.parse(config[:server]), :message => message.to_json)
48
+ # Returns a subscription hash to pass to the PrivatePub.sign call in JavaScript.
49
+ # Any options passed are merged to the hash.
50
+ def subscription(options = {})
51
+ sub = {:server => config[:server], :timestamp => (Time.now.to_f * 1000).round}.merge(options)
52
+ sub[:signature] = Digest::SHA1.hexdigest([config[:secret_token], sub[:channel], sub[:timestamp]].join)
53
+ sub
48
54
  end
49
55
 
56
+ # Determine if the signature has expired given a timestamp.
50
57
  def signature_expired?(timestamp)
51
58
  timestamp < ((Time.now.to_f - config[:signature_expiration])*1000).round if config[:signature_expiration]
52
59
  end
53
60
 
61
+ # Returns the Faye Rack application.
62
+ # Any options given are passed to the Faye::RackAdapter.
54
63
  def faye_app(options = {})
55
64
  options = {:mount => "/faye", :timeout => 45, :extensions => [FayeExtension.new]}.merge(options)
56
65
  Faye::RackAdapter.new(options)
@@ -2,11 +2,13 @@ require "private_pub/view_helpers"
2
2
 
3
3
  module PrivatePub
4
4
  class Engine < Rails::Engine
5
+ # Loads the private_pub.yml file if it exists.
5
6
  initializer "private_pub.config" do
6
7
  path = Rails.root.join("config/private_pub.yml")
7
8
  PrivatePub.load_config(path, Rails.env) if path.exist?
8
9
  end
9
10
 
11
+ # Adds the ViewHelpers into ActionView::Base
10
12
  initializer "private_pub.view_helpers" do
11
13
  ActionView::Base.send :include, ViewHelpers
12
14
  end
@@ -1,5 +1,9 @@
1
1
  module PrivatePub
2
+ # This class is an extension for the Faye::RackAdapter.
3
+ # It is used inside of PrivatePub.faye_app.
2
4
  class FayeExtension
5
+ # Callback to handle incoming Faye messages. This authenticates both
6
+ # subscribe and publish calls.
3
7
  def incoming(message, callback)
4
8
  if message["channel"] == "/meta/subscribe"
5
9
  authenticate_subscribe(message)
@@ -11,6 +15,7 @@ module PrivatePub
11
15
 
12
16
  private
13
17
 
18
+ # Ensure the subscription signature is correct and that it has not expired.
14
19
  def authenticate_subscribe(message)
15
20
  subscription = PrivatePub.subscription(:channel => message["subscription"], :timestamp => message["ext"]["private_pub_timestamp"])
16
21
  if message["ext"]["private_pub_signature"] != subscription[:signature]
@@ -20,6 +25,7 @@ module PrivatePub
20
25
  end
21
26
  end
22
27
 
28
+ # Ensures the secret token is correct before publishing.
23
29
  def authenticate_publish(message)
24
30
  if PrivatePub.config[:secret_token].nil?
25
31
  raise Error, "No secret_token config set, ensure private_pub.yml is loaded properly."
@@ -1,12 +1,19 @@
1
1
  module PrivatePub
2
2
  module ViewHelpers
3
+ # Publish the given data or block to the client by sending
4
+ # a Net::HTTP POST request to the Faye server. If a block
5
+ # or string is passed in, it is evaluated as JavaScript
6
+ # on the client. Otherwise it will be converted to JSON
7
+ # for use in a JavaScript callback.
3
8
  def publish_to(channel, data = nil, &block)
4
9
  PrivatePub.publish_to(channel, data || capture(&block))
5
10
  end
6
11
 
12
+ # Subscribe the client to the given channel. This generates
13
+ # some JavaScript calling PrivatePub.sign with the subscription
14
+ # options.
7
15
  def subscribe_to(channel)
8
16
  subscription = PrivatePub.subscription(:channel => channel)
9
- subscription[:server] = PrivatePub.config[:server]
10
17
  content_tag "script", :type => "text/javascript" do
11
18
  raw("PrivatePub.sign(#{subscription.to_json});")
12
19
  end
@@ -6,5 +6,3 @@ production:
6
6
  server: http://example.com/faye
7
7
  secret_token: PRODUCTION_SECRET_TOKEN
8
8
  signature_expiration: 600
9
- no_signature_expiration:
10
- signature_expiration:
@@ -12,7 +12,7 @@ describe PrivatePub::FayeExtension do
12
12
  @message["ext"]["private_pub_signature"] = "bad"
13
13
  @message["ext"]["private_pub_timestamp"] = "123"
14
14
  message = @faye.incoming(@message, lambda { |m| m })
15
- message["error"].should == "Incorrect signature."
15
+ message["error"].should eq("Incorrect signature.")
16
16
  end
17
17
 
18
18
  it "has no error when the signature matches the subscription" do
@@ -24,13 +24,14 @@ 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
27
+ it "has an error when signature just expired" do
28
+ PrivatePub.config[:signature_expiration] = 1
28
29
  sub = PrivatePub.subscription(:timestamp => 123, :channel => "hello")
29
30
  @message["subscription"] = sub[:channel]
30
31
  @message["ext"]["private_pub_signature"] = sub[:signature]
31
32
  @message["ext"]["private_pub_timestamp"] = sub[:timestamp]
32
33
  message = @faye.incoming(@message, lambda { |m| m })
33
- message["error"].should == "Signature has expired."
34
+ message["error"].should eq("Signature has expired.")
34
35
  end
35
36
 
36
37
  it "has an error when trying to publish to a custom channel with a bad token" do
@@ -38,7 +39,7 @@ describe PrivatePub::FayeExtension do
38
39
  @message["channel"] = "/custom/channel"
39
40
  @message["ext"]["private_pub_token"] = "bad"
40
41
  message = @faye.incoming(@message, lambda { |m| m })
41
- message["error"].should == "Incorrect token."
42
+ message["error"].should eq("Incorrect token.")
42
43
  end
43
44
 
44
45
  it "raises an exception when attempting to call a custom channel without a secret_token set" do
@@ -54,13 +55,13 @@ describe PrivatePub::FayeExtension do
54
55
  message = @faye.incoming(@message, lambda { |m| m })
55
56
  message["error"].should be_nil
56
57
  end
57
-
58
+
58
59
  it "should not let message carry the private pub token after server's validation" do
59
60
  PrivatePub.config[:secret_token] = "good"
60
61
  @message["channel"] = "/custom/channel"
61
62
  @message["ext"]["private_pub_token"] = PrivatePub.config[:secret_token]
62
63
  message = @faye.incoming(@message, lambda { |m| m })
63
- message['ext']["private_pub_token"].should be_nil
64
+ message['ext']["private_pub_token"].should be_nil
64
65
  end
65
-
66
+
66
67
  end
@@ -5,30 +5,25 @@ describe PrivatePub do
5
5
  PrivatePub.reset_config
6
6
  end
7
7
 
8
- it "defaults server to localhost:9292/faye" do
9
- PrivatePub.config[:server].should == "http://localhost:9292/faye"
8
+ it "defaults server to nil" do
9
+ PrivatePub.config[:server].should be_nil
10
10
  end
11
11
 
12
- it "defaults signature_expiration to 1 hour" do
13
- PrivatePub.config[:signature_expiration].should == 60 * 60
12
+ it "defaults signature_expiration to nil" do
13
+ PrivatePub.config[:signature_expiration].should be_nil
14
14
  end
15
15
 
16
16
  it "defaults subscription timestamp to current time in milliseconds" do
17
17
  time = Time.now
18
18
  Time.stub!(:now).and_return(time)
19
- PrivatePub.subscription[:timestamp].should == (time.to_f * 1000).round
19
+ PrivatePub.subscription[:timestamp].should eq((time.to_f * 1000).round)
20
20
  end
21
21
 
22
22
  it "loads a simple configuration file via load_config" do
23
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
24
+ PrivatePub.config[:server].should eq("http://example.com/faye")
25
+ PrivatePub.config[:secret_token].should eq("PRODUCTION_SECRET_TOKEN")
26
+ PrivatePub.config[:signature_expiration].should eq(600)
32
27
  end
33
28
 
34
29
  it "raises an exception if an invalid environment is passed to load_config" do
@@ -37,16 +32,18 @@ describe PrivatePub do
37
32
  }.should raise_error ArgumentError
38
33
  end
39
34
 
40
- it "includes channel and custom time in subscription" do
35
+ it "includes channel, server, and custom time in subscription" do
36
+ PrivatePub.config[:server] = "server"
41
37
  subscription = PrivatePub.subscription(:timestamp => 123, :channel => "hello")
42
- subscription[:timestamp].should == 123
43
- subscription[:channel].should == "hello"
38
+ subscription[:timestamp].should eq(123)
39
+ subscription[:channel].should eq("hello")
40
+ subscription[:server].should eq("server")
44
41
  end
45
42
 
46
43
  it "does a sha1 digest of channel, timestamp, and secret token" do
47
44
  PrivatePub.config[:secret_token] = "token"
48
45
  subscription = PrivatePub.subscription(:timestamp => 123, :channel => "channel")
49
- subscription[:signature].should == Digest::SHA1.hexdigest("tokenchannel123")
46
+ subscription[:signature].should eq(Digest::SHA1.hexdigest("tokenchannel123"))
50
47
  end
51
48
 
52
49
  it "formats a message hash given a channel and a string for eval" do
@@ -74,15 +71,22 @@ describe PrivatePub do
74
71
  end
75
72
 
76
73
  it "publish message as json to server using Net::HTTP" do
74
+ PrivatePub.config[:server] = "http://localhost"
77
75
  message = stub(:to_json => "message_json")
78
- Net::HTTP.should_receive(:post_form).with(URI.parse(PrivatePub.config[:server]), :message => "message_json").and_return(:result)
79
- PrivatePub.publish_message(message).should == :result
76
+ Net::HTTP.should_receive(:post_form).with(URI.parse("http://localhost"), :message => "message_json").and_return(:result)
77
+ PrivatePub.publish_message(message).should eq(:result)
78
+ end
79
+
80
+ it "raises an exception if no server is specified when calling publish_message" do
81
+ lambda {
82
+ PrivatePub.publish_message("foo")
83
+ }.should raise_error(PrivatePub::Error)
80
84
  end
81
85
 
82
86
  it "publish_to passes message to publish_message call" do
83
87
  PrivatePub.should_receive(:message).with("chan", "foo").and_return("message")
84
88
  PrivatePub.should_receive(:publish_message).with("message").and_return(:result)
85
- PrivatePub.publish_to("chan", "foo").should == :result
89
+ PrivatePub.publish_to("chan", "foo").should eq(:result)
86
90
  end
87
91
 
88
92
  it "has a Faye rack app instance" do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: private_pub
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 1.0.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -13,7 +13,7 @@ date: 2012-01-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: faye
16
- requirement: &70172946940680 !ruby/object:Gem::Requirement
16
+ requirement: &70255009397180 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70172946940680
24
+ version_requirements: *70255009397180
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rake
27
- requirement: &70172946940000 !ruby/object:Gem::Requirement
27
+ requirement: &70255009396460 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '0'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *70172946940000
35
+ version_requirements: *70255009396460
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: rspec
38
- requirement: &70172946939000 !ruby/object:Gem::Requirement
38
+ requirement: &70255009395280 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ~>
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: 2.8.0
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *70172946939000
46
+ version_requirements: *70255009395280
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: jasmine
49
- requirement: &70172946936820 !ruby/object:Gem::Requirement
49
+ requirement: &70255009393320 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,7 +54,7 @@ dependencies:
54
54
  version: 1.1.1
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *70172946936820
57
+ version_requirements: *70255009393320
58
58
  description: Private pub/sub messaging in Rails through Faye.
59
59
  email: ryan@railscasts.com
60
60
  executables: []