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 +7 -0
- data/LICENSE +3 -3
- data/README.md +15 -18
- data/lib/generators/private_pub/templates/private_pub.yml +1 -0
- data/lib/private_pub.rb +21 -12
- data/lib/private_pub/engine.rb +2 -0
- data/lib/private_pub/faye_extension.rb +6 -0
- data/lib/private_pub/view_helpers.rb +8 -1
- data/spec/fixtures/private_pub.yml +0 -2
- data/spec/private_pub/faye_extension_spec.rb +8 -7
- data/spec/private_pub_spec.rb +24 -20
- metadata +9 -9
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
|
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
|
-
##
|
81
|
+
## Configuration
|
82
82
|
|
83
|
-
|
83
|
+
The configuration is set separately for each environment in the generated `config/private_pub.yml` file. Here are the options.
|
84
84
|
|
85
|
-
|
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
|
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
|
-
|
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).
|
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.
|
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
|
-
|
27
|
-
|
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
|
-
|
47
|
-
|
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)
|
data/lib/private_pub/engine.rb
CHANGED
@@ -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
|
@@ -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
|
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
|
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
|
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
|
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
|
data/spec/private_pub_spec.rb
CHANGED
@@ -5,30 +5,25 @@ describe PrivatePub do
|
|
5
5
|
PrivatePub.reset_config
|
6
6
|
end
|
7
7
|
|
8
|
-
it "defaults server to
|
9
|
-
PrivatePub.config[:server].should
|
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
|
13
|
-
PrivatePub.config[:signature_expiration].should
|
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
|
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
|
25
|
-
PrivatePub.config[:secret_token].should
|
26
|
-
PrivatePub.config[:signature_expiration].should
|
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
|
43
|
-
subscription[:channel].should
|
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
|
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(
|
79
|
-
PrivatePub.publish_message(message).should
|
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
|
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.
|
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: &
|
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: *
|
24
|
+
version_requirements: *70255009397180
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: rake
|
27
|
-
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: *
|
35
|
+
version_requirements: *70255009396460
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: rspec
|
38
|
-
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: *
|
46
|
+
version_requirements: *70255009395280
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: jasmine
|
49
|
-
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: *
|
57
|
+
version_requirements: *70255009393320
|
58
58
|
description: Private pub/sub messaging in Rails through Faye.
|
59
59
|
email: ryan@railscasts.com
|
60
60
|
executables: []
|