peritus_private_pub 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +49 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.md +143 -0
- data/Rakefile +12 -0
- data/app/assets/javascripts/private_pub.js +97 -0
- data/lib/generators/private_pub/install_generator.rb +17 -0
- data/lib/generators/private_pub/templates/private_pub.ru +10 -0
- data/lib/generators/private_pub/templates/private_pub.yml +10 -0
- data/lib/private_pub.rb +79 -0
- data/lib/private_pub/engine.rb +16 -0
- data/lib/private_pub/faye_extension.rb +39 -0
- data/lib/private_pub/view_helpers.rb +22 -0
- data/peritus_private_pub.gemspec +21 -0
- data/spec/fixtures/private_pub.yml +8 -0
- data/spec/javascripts/private_pub_spec.js +165 -0
- data/spec/javascripts/support/jasmine.yml +73 -0
- data/spec/javascripts/support/jasmine_config.rb +23 -0
- data/spec/javascripts/support/jasmine_runner.rb +32 -0
- data/spec/private_pub/faye_extension_spec.rb +67 -0
- data/spec/private_pub_spec.rb +141 -0
- data/spec/spec_helper.rb +8 -0
- metadata +120 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 14270700def1ba2d17469debf6d2f34b84ee4112
|
4
|
+
data.tar.gz: a8416de63706b9c7b195aaf77b6c21179bde4914
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9c4517563e98fa99db8f1efe10297897b3f227ed7bca5b92838879ecedb15cc0506d792a82cbf4bad2b6bdd7243177d3bbdf0fb69596ddd8e64fc03b8229bfe7
|
7
|
+
data.tar.gz: 78726586e1a43e6ab3c5925415ffbb0e20ffa5de42c1bb505ed520859575312f22ad14e95672f5ca205ab1e0f7d00dd0b5873bff3635091b9c7cd61e274fcbab
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
## 1.0.3 (August 20, 2012)
|
2
|
+
|
3
|
+
* fixed Faye startup error (thanks gitt) - issue #40
|
4
|
+
|
5
|
+
|
6
|
+
## 1.0.2 (August 20, 2012)
|
7
|
+
|
8
|
+
* added HTTPS support (thanks vanne)
|
9
|
+
|
10
|
+
|
11
|
+
## 1.0.1 (January 25, 2012)
|
12
|
+
|
13
|
+
* Rails 3.2 compatibility with SecureRandom fix (thanks windigo) - issue #26
|
14
|
+
|
15
|
+
|
16
|
+
## 1.0.0 (January 15, 2012)
|
17
|
+
|
18
|
+
* setting config defaults to nil so everything must be set in `private_pub.yml`
|
19
|
+
|
20
|
+
* Documentation improvements
|
21
|
+
|
22
|
+
|
23
|
+
## 0.3.0 (January 14, 2012)
|
24
|
+
|
25
|
+
* adding `PrivatePub.publish_to` method for publishing from anywhere - issue #15
|
26
|
+
|
27
|
+
* rewriting `private_pub.js` so it is framework agnostic
|
28
|
+
|
29
|
+
* Rails 3.1 compatibility (thanks BinaryMuse) - issue #25
|
30
|
+
|
31
|
+
* adding faye gem dependency so it doesn't need to be installed separately
|
32
|
+
|
33
|
+
* renaming `faye.ru` to `private_pub.ru`
|
34
|
+
|
35
|
+
* truncate token for client for security (thanks jameshuynh) - issue #19
|
36
|
+
|
37
|
+
|
38
|
+
## 0.2.0 (April 7, 2011)
|
39
|
+
|
40
|
+
* switched to YAML file for config. BACKWARDS INCOMPATIBLE: you will need to remove config/initializers/private_pub.rb
|
41
|
+
|
42
|
+
* moved view helpers into Railtie so helper file is no longer generated
|
43
|
+
|
44
|
+
* error out when signature has expired
|
45
|
+
|
46
|
+
|
47
|
+
## 0.1.0 (April 4, 2011)
|
48
|
+
|
49
|
+
* initial release
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 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.md
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
# Private Pub
|
2
|
+
|
3
|
+
Private Pub is a Ruby gem for use with Rails to publish and subscribe to messages through [Faye](http://faye.jcoglan.com/). It allows you to easily provide real-time updates through an open socket without tying up a Rails process. All channels are private so users can only listen to events you subscribe them to.
|
4
|
+
|
5
|
+
Watch [RailsCasts Episode 316](http://railscasts.com/episodes/316-private-pub) for a demonstration of Private Pub.
|
6
|
+
|
7
|
+
|
8
|
+
## Setup
|
9
|
+
|
10
|
+
Add the gem to your Gemfile and run the `bundle` command to install it. You'll probably want to add "thin" to your Gemfile as well to serve Faye.
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem "private_pub"
|
14
|
+
gem "thin"
|
15
|
+
```
|
16
|
+
|
17
|
+
Run the generator to create the initial files.
|
18
|
+
|
19
|
+
```
|
20
|
+
rails g private_pub:install
|
21
|
+
```
|
22
|
+
|
23
|
+
Next, start up Faye using the rackup file that was generated.
|
24
|
+
|
25
|
+
```
|
26
|
+
rackup private_pub.ru -s thin -E production
|
27
|
+
```
|
28
|
+
|
29
|
+
**In Rails 3.1** add the JavaScript file to your application.js file manifest.
|
30
|
+
|
31
|
+
```javascript
|
32
|
+
//= require private_pub
|
33
|
+
```
|
34
|
+
|
35
|
+
**In Rails 3.0** add the generated private_pub.js file to your layout.
|
36
|
+
|
37
|
+
```rhtml
|
38
|
+
<%= javascript_include_tag "private_pub" %>
|
39
|
+
```
|
40
|
+
|
41
|
+
It's not necessary to include faye.js since that will be handled automatically for you.
|
42
|
+
|
43
|
+
|
44
|
+
## Usage
|
45
|
+
|
46
|
+
Use the `subscribe_to` helper method on any page to subscribe to a channel.
|
47
|
+
|
48
|
+
```rhtml
|
49
|
+
<%= subscribe_to "/messages/new" %>
|
50
|
+
```
|
51
|
+
|
52
|
+
Use the `publish_to` helper method to send JavaScript to that channel. This is usually done in a JavaScript AJAX template (such as a create.js.erb file).
|
53
|
+
|
54
|
+
```rhtml
|
55
|
+
<% publish_to "/messages/new" do %>
|
56
|
+
$("#chat").append("<%= j render(@messages) %>");
|
57
|
+
<% end %>
|
58
|
+
```
|
59
|
+
|
60
|
+
This JavaScript will be immediately evaluated on all clients who have subscribed to that channel. In this example they will see the new chat message appear in real-time without reloading the browser.
|
61
|
+
|
62
|
+
|
63
|
+
## Alternative Usage
|
64
|
+
|
65
|
+
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).
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
PrivatePub.publish_to "/messages/new", :chat_message => "Hello, world!"
|
69
|
+
```
|
70
|
+
|
71
|
+
And then handle this through JavaScript on the client side.
|
72
|
+
|
73
|
+
```javascript
|
74
|
+
PrivatePub.subscribe("/messages/new", function(data, channel) {
|
75
|
+
$("#chat").append(data.chat_message);
|
76
|
+
});
|
77
|
+
```
|
78
|
+
|
79
|
+
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.
|
80
|
+
|
81
|
+
|
82
|
+
## Configuration
|
83
|
+
|
84
|
+
The configuration is set separately for each environment in the generated `config/private_pub.yml` file. Here are the options.
|
85
|
+
|
86
|
+
* `server`: The URL to use for the Faye server such as `http://localhost:9292/faye`.
|
87
|
+
* `secret_token`: A secret hash to secure the server. Can be any string.
|
88
|
+
* `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.
|
89
|
+
|
90
|
+
|
91
|
+
## How It Works
|
92
|
+
|
93
|
+
The `subscribe_to` helper will output the following script which subscribes the user to a specific channel and server.
|
94
|
+
|
95
|
+
```html
|
96
|
+
<script type="text/javascript">
|
97
|
+
PrivatePub.sign({
|
98
|
+
channel: "/messages/new",
|
99
|
+
timestamp: 1302306682972,
|
100
|
+
signature: "dc1c71d3e959ebb6f49aa6af0c86304a0740088d",
|
101
|
+
server: "http://localhost:9292/faye"
|
102
|
+
});
|
103
|
+
</script>
|
104
|
+
```
|
105
|
+
|
106
|
+
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.
|
107
|
+
|
108
|
+
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.
|
109
|
+
|
110
|
+
|
111
|
+
## Serving Faye over HTTPS (with Thin)
|
112
|
+
|
113
|
+
To server Faye over HTTPS you could create a thin configuration file `config/private_pub_thin.yml` similar to the following:
|
114
|
+
|
115
|
+
```yaml
|
116
|
+
---
|
117
|
+
port: 4443
|
118
|
+
ssl: true
|
119
|
+
ssl_key_file: /path/to/server.pem
|
120
|
+
ssl_cert_file: /path/to/certificate_chain.pem
|
121
|
+
environment: production
|
122
|
+
rackup: private_pub.ru
|
123
|
+
```
|
124
|
+
|
125
|
+
The `certificate_chain.pem` file should contain your signed certificate, followed by intermediate certificates (if any) and the root certificate of the CA that signed the key.
|
126
|
+
|
127
|
+
Next reconfigure the URL in `config/private_pub.yml` to look like `https://your.hostname.com:4443/faye`
|
128
|
+
|
129
|
+
Finally start up Thin from the project root.
|
130
|
+
|
131
|
+
```
|
132
|
+
thin -C config/private_pub_thin.yml start
|
133
|
+
```
|
134
|
+
|
135
|
+
|
136
|
+
## Project Status
|
137
|
+
|
138
|
+
Unfortunately I have not had time to actively work on this project recently. If you find a critical issue where it does not work as documented please [ping me on Twitter](http://twitter.com/rbates) and I'll take a look.
|
139
|
+
|
140
|
+
|
141
|
+
## Development & Feedback
|
142
|
+
|
143
|
+
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/Rakefile
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
function buildPrivatePub(doc) {
|
2
|
+
var self = {
|
3
|
+
connecting: false,
|
4
|
+
fayeClient: null,
|
5
|
+
fayeCallbacks: [],
|
6
|
+
subscriptions: {},
|
7
|
+
subscriptionObjects: {},
|
8
|
+
subscriptionCallbacks: {},
|
9
|
+
|
10
|
+
faye: function(callback) {
|
11
|
+
if (self.fayeClient) {
|
12
|
+
callback(self.fayeClient);
|
13
|
+
} else {
|
14
|
+
self.fayeCallbacks.push(callback);
|
15
|
+
if (self.subscriptions.server && !self.connecting) {
|
16
|
+
self.connecting = true;
|
17
|
+
var script = doc.createElement("script");
|
18
|
+
script.type = "text/javascript";
|
19
|
+
script.src = self.subscriptions.server + ".js";
|
20
|
+
script.onload = self.connectToFaye;
|
21
|
+
doc.documentElement.appendChild(script);
|
22
|
+
}
|
23
|
+
}
|
24
|
+
},
|
25
|
+
|
26
|
+
connectToFaye: function() {
|
27
|
+
self.fayeClient = new Faye.Client(self.subscriptions.server);
|
28
|
+
self.fayeClient.addExtension(self.fayeExtension);
|
29
|
+
for (var i=0; i < self.fayeCallbacks.length; i++) {
|
30
|
+
self.fayeCallbacks[i](self.fayeClient);
|
31
|
+
};
|
32
|
+
},
|
33
|
+
|
34
|
+
fayeExtension: {
|
35
|
+
outgoing: function(message, callback) {
|
36
|
+
if (message.channel == "/meta/subscribe") {
|
37
|
+
// Attach the signature and timestamp to subscription messages
|
38
|
+
var subscription = self.subscriptions[message.subscription];
|
39
|
+
if (!message.ext) message.ext = {};
|
40
|
+
message.ext.private_pub_signature = subscription.signature;
|
41
|
+
message.ext.private_pub_timestamp = subscription.timestamp;
|
42
|
+
}
|
43
|
+
callback(message);
|
44
|
+
}
|
45
|
+
},
|
46
|
+
|
47
|
+
sign: function(options) {
|
48
|
+
if (!self.subscriptions.server) {
|
49
|
+
self.subscriptions.server = options.server;
|
50
|
+
}
|
51
|
+
self.subscriptions[options.channel] = options;
|
52
|
+
self.faye(function(faye) {
|
53
|
+
var sub = faye.subscribe(options.channel, self.handleResponse);
|
54
|
+
self.subscriptionObjects[options.channel] = sub;
|
55
|
+
if (options.subscription) {
|
56
|
+
options.subscription(sub);
|
57
|
+
}
|
58
|
+
});
|
59
|
+
},
|
60
|
+
|
61
|
+
handleResponse: function(message) {
|
62
|
+
if (message.eval) {
|
63
|
+
eval(message.eval);
|
64
|
+
}
|
65
|
+
if (callback = self.subscriptionCallbacks[message.channel]) {
|
66
|
+
callback(message.data, message.channel);
|
67
|
+
}
|
68
|
+
},
|
69
|
+
|
70
|
+
subscription: function(channel) {
|
71
|
+
return self.subscriptionObjects[channel];
|
72
|
+
},
|
73
|
+
|
74
|
+
unsubscribeAll: function() {
|
75
|
+
for (var i in self.subscriptionObjects) {
|
76
|
+
if ( self.subscriptionObjects.hasOwnProperty(i) ) {
|
77
|
+
self.unsubscribe(i);
|
78
|
+
}
|
79
|
+
}
|
80
|
+
},
|
81
|
+
|
82
|
+
unsubscribe: function(channel) {
|
83
|
+
var sub = self.subscription(channel);
|
84
|
+
if (sub) {
|
85
|
+
sub.cancel();
|
86
|
+
delete self.subscriptionObjects[channel];
|
87
|
+
}
|
88
|
+
},
|
89
|
+
|
90
|
+
subscribe: function(channel, callback) {
|
91
|
+
self.subscriptionCallbacks[channel] = callback;
|
92
|
+
}
|
93
|
+
};
|
94
|
+
return self;
|
95
|
+
}
|
96
|
+
|
97
|
+
var PrivatePub = buildPrivatePub(document);
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module 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.yml", "config/private_pub.yml"
|
10
|
+
if ::Rails.version < "3.1"
|
11
|
+
copy_file "../../../../app/assets/javascripts/private_pub.js", "public/javascripts/private_pub.js"
|
12
|
+
end
|
13
|
+
copy_file "private_pub.ru", "private_pub.ru"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# Run with: rackup private_pub.ru -s thin -E production
|
2
|
+
require "bundler/setup"
|
3
|
+
require "yaml"
|
4
|
+
require "faye"
|
5
|
+
require "private_pub"
|
6
|
+
|
7
|
+
Faye::WebSocket.load_adapter('thin')
|
8
|
+
|
9
|
+
PrivatePub.load_config(File.expand_path("../config/private_pub.yml", __FILE__), ENV["RAILS_ENV"] || "development")
|
10
|
+
run PrivatePub.faye_app
|
@@ -0,0 +1,10 @@
|
|
1
|
+
development:
|
2
|
+
server: "http://dev:9292/faye"
|
3
|
+
secret_token: "secret"
|
4
|
+
test:
|
5
|
+
server: "http://test:9292/faye"
|
6
|
+
secret_token: "secret"
|
7
|
+
production:
|
8
|
+
server: "http://example.com/faye"
|
9
|
+
secret_token: "<%= defined?(SecureRandom) ? SecureRandom.hex(32) : ActiveSupport::SecureRandom.hex(32) %>"
|
10
|
+
signature_expiration: 3600 # one hour
|
data/lib/private_pub.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
require "digest/sha1"
|
2
|
+
require "net/http"
|
3
|
+
require "net/https"
|
4
|
+
require "erb"
|
5
|
+
|
6
|
+
require "private_pub/faye_extension"
|
7
|
+
require "private_pub/engine" if defined? Rails
|
8
|
+
|
9
|
+
module PrivatePub
|
10
|
+
class Error < StandardError; end
|
11
|
+
|
12
|
+
class << self
|
13
|
+
attr_reader :config
|
14
|
+
|
15
|
+
# Resets the configuration to the default (empty hash)
|
16
|
+
def reset_config
|
17
|
+
@config = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
# Loads the configuration from a given YAML file and environment (such as production)
|
21
|
+
def load_config(filename, environment)
|
22
|
+
yaml = YAML.load(ERB.new(File.read(filename)).result)[environment.to_s]
|
23
|
+
raise ArgumentError, "The #{environment} environment does not exist in #{filename}" if yaml.nil?
|
24
|
+
yaml.each { |k, v| config[k.to_sym] = v }
|
25
|
+
end
|
26
|
+
|
27
|
+
# Publish the given data to a specific channel. This ends up sending
|
28
|
+
# a Net::HTTP POST request to the Faye server.
|
29
|
+
def publish_to(channel, data)
|
30
|
+
publish_message(message(channel, data))
|
31
|
+
end
|
32
|
+
|
33
|
+
# Sends the given message hash to the Faye server using Net::HTTP.
|
34
|
+
def publish_message(message)
|
35
|
+
raise Error, "No server specified, ensure private_pub.yml was loaded properly." unless config[:server]
|
36
|
+
url = URI.parse(config[:server])
|
37
|
+
|
38
|
+
form = Net::HTTP::Post.new(url.path.empty? ? '/' : url.path)
|
39
|
+
form.set_form_data(:message => message.to_json)
|
40
|
+
|
41
|
+
http = Net::HTTP.new(url.host, url.port)
|
42
|
+
http.use_ssl = url.scheme == "https"
|
43
|
+
http.start {|h| h.request(form)}
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns a message hash for sending to Faye
|
47
|
+
def message(channel, data)
|
48
|
+
message = {:channel => channel, :data => {:channel => channel}, :ext => {:private_pub_token => config[:secret_token]}}
|
49
|
+
if data.kind_of? String
|
50
|
+
message[:data][:eval] = data
|
51
|
+
else
|
52
|
+
message[:data][:data] = data
|
53
|
+
end
|
54
|
+
message
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns a subscription hash to pass to the PrivatePub.sign call in JavaScript.
|
58
|
+
# Any options passed are merged to the hash.
|
59
|
+
def subscription(options = {})
|
60
|
+
sub = {:server => config[:server], :timestamp => (Time.now.to_f * 1000).round}.merge(options)
|
61
|
+
sub[:signature] = Digest::SHA1.hexdigest([config[:secret_token], sub[:channel], sub[:timestamp]].join)
|
62
|
+
sub
|
63
|
+
end
|
64
|
+
|
65
|
+
# Determine if the signature has expired given a timestamp.
|
66
|
+
def signature_expired?(timestamp)
|
67
|
+
timestamp < ((Time.now.to_f - config[:signature_expiration])*1000).round if config[:signature_expiration]
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns the Faye Rack application.
|
71
|
+
# Any options given are passed to the Faye::RackAdapter.
|
72
|
+
def faye_app(options = {})
|
73
|
+
options = {:mount => "/faye", :timeout => 45, :extensions => [FayeExtension.new]}.merge(options)
|
74
|
+
Faye::RackAdapter.new(options)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
reset_config
|
79
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require "private_pub/view_helpers"
|
2
|
+
|
3
|
+
module PrivatePub
|
4
|
+
class Engine < Rails::Engine
|
5
|
+
# Loads the private_pub.yml file if it exists.
|
6
|
+
initializer "private_pub.config" do
|
7
|
+
path = Rails.root.join("config/private_pub.yml")
|
8
|
+
PrivatePub.load_config(path, Rails.env) if path.exist?
|
9
|
+
end
|
10
|
+
|
11
|
+
# Adds the ViewHelpers into ActionView::Base
|
12
|
+
initializer "private_pub.view_helpers" do
|
13
|
+
ActionView::Base.send :include, ViewHelpers
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module PrivatePub
|
2
|
+
# This class is an extension for the Faye::RackAdapter.
|
3
|
+
# It is used inside of PrivatePub.faye_app.
|
4
|
+
class FayeExtension
|
5
|
+
# Callback to handle incoming Faye messages. This authenticates both
|
6
|
+
# subscribe and publish calls.
|
7
|
+
def incoming(message, callback)
|
8
|
+
if message["channel"] == "/meta/subscribe"
|
9
|
+
authenticate_subscribe(message)
|
10
|
+
elsif message["channel"] !~ %r{^/meta/}
|
11
|
+
authenticate_publish(message)
|
12
|
+
end
|
13
|
+
callback.call(message)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
# Ensure the subscription signature is correct and that it has not expired.
|
19
|
+
def authenticate_subscribe(message)
|
20
|
+
subscription = PrivatePub.subscription(:channel => message["subscription"], :timestamp => message["ext"]["private_pub_timestamp"])
|
21
|
+
if message["ext"]["private_pub_signature"] != subscription[:signature]
|
22
|
+
message["error"] = "Incorrect signature."
|
23
|
+
elsif PrivatePub.signature_expired? message["ext"]["private_pub_timestamp"].to_i
|
24
|
+
message["error"] = "Signature has expired."
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Ensures the secret token is correct before publishing.
|
29
|
+
def authenticate_publish(message)
|
30
|
+
if PrivatePub.config[:secret_token].nil?
|
31
|
+
raise Error, "No secret_token config set, ensure private_pub.yml is loaded properly."
|
32
|
+
elsif message["ext"]["private_pub_token"] != PrivatePub.config[:secret_token]
|
33
|
+
message["error"] = "Incorrect token."
|
34
|
+
else
|
35
|
+
message["ext"]["private_pub_token"] = nil
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module PrivatePub
|
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.
|
8
|
+
def publish_to(channel, data = nil, &block)
|
9
|
+
PrivatePub.publish_to(channel, data || capture(&block))
|
10
|
+
end
|
11
|
+
|
12
|
+
# Subscribe the client to the given channel. This generates
|
13
|
+
# some JavaScript calling PrivatePub.sign with the subscription
|
14
|
+
# options.
|
15
|
+
def subscribe_to(channel)
|
16
|
+
subscription = PrivatePub.subscription(:channel => channel)
|
17
|
+
content_tag "script", :type => "text/javascript" do
|
18
|
+
raw("PrivatePub.sign(#{subscription.to_json});")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "peritus_private_pub"
|
3
|
+
s.version = "1.0.0"
|
4
|
+
s.author = "Tyler DeWitt"
|
5
|
+
s.email = "tdewitt@peritus.com"
|
6
|
+
s.homepage = "https://github.com/PeritusSolutions/peritus_private_pub"
|
7
|
+
s.summary = "Private pub/sub messaging in Rails."
|
8
|
+
s.description = "Private pub/sub messaging in Rails through Faye."
|
9
|
+
|
10
|
+
s.files = Dir["{app,lib,spec}/**/*", "[A-Z]*", "init.rb"] - ["Gemfile.lock"]
|
11
|
+
s.require_path = "lib"
|
12
|
+
|
13
|
+
s.add_dependency 'faye'
|
14
|
+
|
15
|
+
s.add_development_dependency 'rake'
|
16
|
+
s.add_development_dependency 'rspec', '~> 2.8.0'
|
17
|
+
s.add_development_dependency 'jasmine', '>= 1.1.1'
|
18
|
+
|
19
|
+
s.rubyforge_project = s.name
|
20
|
+
s.required_rubygems_version = ">= 1.3.4"
|
21
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
describe("PrivatePub", function() {
|
2
|
+
var pub, doc;
|
3
|
+
beforeEach(function() {
|
4
|
+
Faye = {}; // To simulate global Faye object
|
5
|
+
doc = {};
|
6
|
+
pub = buildPrivatePub(doc);
|
7
|
+
});
|
8
|
+
|
9
|
+
it("adds a subscription callback", function() {
|
10
|
+
pub.subscribe("hello", "callback");
|
11
|
+
expect(pub.subscriptionCallbacks["hello"]).toEqual("callback");
|
12
|
+
});
|
13
|
+
|
14
|
+
it("has a fayeExtension which adds matching subscription signature and timestamp to outgoing message", function() {
|
15
|
+
var called = false;
|
16
|
+
var message = {channel: "/meta/subscribe", subscription: "hello"}
|
17
|
+
pub.subscriptions["hello"] = {signature: "abcd", timestamp: "1234"}
|
18
|
+
pub.fayeExtension.outgoing(message, function(message) {
|
19
|
+
expect(message.ext.private_pub_signature).toEqual("abcd");
|
20
|
+
expect(message.ext.private_pub_timestamp).toEqual("1234");
|
21
|
+
called = true;
|
22
|
+
});
|
23
|
+
expect(called).toBeTruthy();
|
24
|
+
});
|
25
|
+
|
26
|
+
it("evaluates javascript in message response", function() {
|
27
|
+
pub.handleResponse({eval: 'self.subscriptions.foo = "bar"'});
|
28
|
+
expect(pub.subscriptions.foo).toEqual("bar");
|
29
|
+
});
|
30
|
+
|
31
|
+
it("triggers callback matching message channel in response", function() {
|
32
|
+
var called = false;
|
33
|
+
pub.subscribe("test", function(data, channel) {
|
34
|
+
expect(data).toEqual("abcd");
|
35
|
+
expect(channel).toEqual("test");
|
36
|
+
called = true;
|
37
|
+
});
|
38
|
+
pub.handleResponse({channel: "test", data: "abcd"});
|
39
|
+
expect(called).toBeTruthy();
|
40
|
+
});
|
41
|
+
|
42
|
+
it("adds a faye subscription with response handler when signing", function() {
|
43
|
+
var faye = {subscribe: jasmine.createSpy()};
|
44
|
+
spyOn(pub, 'faye').andCallFake(function(callback) {
|
45
|
+
callback(faye);
|
46
|
+
});
|
47
|
+
var options = {server: "server", channel: "somechannel"};
|
48
|
+
pub.sign(options);
|
49
|
+
expect(faye.subscribe).toHaveBeenCalledWith("somechannel", pub.handleResponse);
|
50
|
+
expect(pub.subscriptions.server).toEqual("server");
|
51
|
+
expect(pub.subscriptions.somechannel).toEqual(options);
|
52
|
+
});
|
53
|
+
|
54
|
+
it("adds a faye subscription with response handler when signing", function() {
|
55
|
+
var faye = {subscribe: jasmine.createSpy()};
|
56
|
+
spyOn(pub, 'faye').andCallFake(function(callback) {
|
57
|
+
callback(faye);
|
58
|
+
});
|
59
|
+
var options = {server: "server", channel: "somechannel"};
|
60
|
+
pub.sign(options);
|
61
|
+
expect(faye.subscribe).toHaveBeenCalledWith("somechannel", pub.handleResponse);
|
62
|
+
expect(pub.subscriptions.server).toEqual("server");
|
63
|
+
expect(pub.subscriptions.somechannel).toEqual(options);
|
64
|
+
});
|
65
|
+
|
66
|
+
it("takes a callback for subscription object when signing", function(){
|
67
|
+
var faye = {subscribe: function(){ return "subscription"; }};
|
68
|
+
spyOn(pub, 'faye').andCallFake(function(callback) {
|
69
|
+
callback(faye);
|
70
|
+
});
|
71
|
+
var options = { server: "server", channel: "somechannel" };
|
72
|
+
options.subscription = jasmine.createSpy();
|
73
|
+
pub.sign(options);
|
74
|
+
expect(options.subscription).toHaveBeenCalledWith("subscription");
|
75
|
+
});
|
76
|
+
|
77
|
+
it("returns the subscription object for a subscribed channel", function(){
|
78
|
+
var faye = {subscribe: function(){ return "subscription"; }};
|
79
|
+
spyOn(pub, 'faye').andCallFake(function(callback) {
|
80
|
+
callback(faye);
|
81
|
+
});
|
82
|
+
var options = { server: "server", channel: "somechannel" };
|
83
|
+
pub.sign(options);
|
84
|
+
expect(pub.subscription("somechannel")).toEqual("subscription")
|
85
|
+
});
|
86
|
+
|
87
|
+
it("unsubscribes a channel by name", function(){
|
88
|
+
var sub = { cancel: jasmine.createSpy() };
|
89
|
+
var faye = {subscribe: function(){ return sub; }};
|
90
|
+
spyOn(pub, 'faye').andCallFake(function(callback) {
|
91
|
+
callback(faye);
|
92
|
+
});
|
93
|
+
var options = { server: "server", channel: "somechannel" };
|
94
|
+
pub.sign(options);
|
95
|
+
expect(pub.subscription("somechannel")).toEqual(sub);
|
96
|
+
pub.unsubscribe("somechannel");
|
97
|
+
expect(sub.cancel).toHaveBeenCalled();
|
98
|
+
expect(pub.subscription("somechannel")).toBeFalsy();
|
99
|
+
});
|
100
|
+
|
101
|
+
it("unsubscribes all channels", function(){
|
102
|
+
var created = 0;
|
103
|
+
var sub = function() {
|
104
|
+
created ++;
|
105
|
+
var sub = { cancel: function(){ created --; } };
|
106
|
+
return sub;
|
107
|
+
};
|
108
|
+
var faye = { subscribe: function(){ return sub(); }};
|
109
|
+
spyOn(pub, 'faye').andCallFake(function(callback) {
|
110
|
+
callback(faye);
|
111
|
+
});
|
112
|
+
pub.sign({server: "server", channel: "firstchannel"});
|
113
|
+
pub.sign({server: "server", channel: "secondchannel"});
|
114
|
+
expect(created).toEqual(2);
|
115
|
+
expect(pub.subscription("firstchannel")).toBeTruthy();
|
116
|
+
expect(pub.subscription("secondchannel")).toBeTruthy();
|
117
|
+
pub.unsubscribeAll()
|
118
|
+
expect(created).toEqual(0);
|
119
|
+
expect(pub.subscription("firstchannel")).toBeFalsy();
|
120
|
+
expect(pub.subscription("secondchannel")).toBeFalsy();
|
121
|
+
});
|
122
|
+
|
123
|
+
it("triggers faye callback function immediately when fayeClient is available", function() {
|
124
|
+
var called = false;
|
125
|
+
pub.fayeClient = "faye";
|
126
|
+
pub.faye(function(faye) {
|
127
|
+
expect(faye).toEqual("faye");
|
128
|
+
called = true;
|
129
|
+
});
|
130
|
+
expect(called).toBeTruthy();
|
131
|
+
});
|
132
|
+
|
133
|
+
it("adds fayeCallback when client and server aren't available", function() {
|
134
|
+
pub.faye("callback");
|
135
|
+
expect(pub.fayeCallbacks[0]).toEqual("callback");
|
136
|
+
});
|
137
|
+
|
138
|
+
it("adds a script tag loading faye js when the server is present", function() {
|
139
|
+
script = {};
|
140
|
+
doc.createElement = function() { return script; };
|
141
|
+
doc.documentElement = {appendChild: jasmine.createSpy()};
|
142
|
+
pub.subscriptions.server = "path/to/faye";
|
143
|
+
pub.faye("callback");
|
144
|
+
expect(pub.fayeCallbacks[0]).toEqual("callback");
|
145
|
+
expect(script.type).toEqual("text/javascript");
|
146
|
+
expect(script.src).toEqual("path/to/faye.js");
|
147
|
+
expect(script.onload).toEqual(pub.connectToFaye);
|
148
|
+
expect(doc.documentElement.appendChild).toHaveBeenCalledWith(script);
|
149
|
+
});
|
150
|
+
|
151
|
+
it("connects to faye server, adds extension, and executes callbacks", function() {
|
152
|
+
callback = jasmine.createSpy();
|
153
|
+
client = {addExtension: jasmine.createSpy()};
|
154
|
+
Faye.Client = function(server) {
|
155
|
+
expect(server).toEqual("server")
|
156
|
+
return client;
|
157
|
+
};
|
158
|
+
pub.subscriptions.server = "server";
|
159
|
+
pub.fayeCallbacks.push(callback);
|
160
|
+
pub.connectToFaye();
|
161
|
+
expect(pub.fayeClient).toEqual(client);
|
162
|
+
expect(client.addExtension).toHaveBeenCalledWith(pub.fayeExtension);
|
163
|
+
expect(callback).toHaveBeenCalledWith(client);
|
164
|
+
});
|
165
|
+
});
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# src_files
|
2
|
+
#
|
3
|
+
# Return an array of filepaths relative to src_dir to include before jasmine specs.
|
4
|
+
# Default: []
|
5
|
+
#
|
6
|
+
# EXAMPLE:
|
7
|
+
#
|
8
|
+
# src_files:
|
9
|
+
# - lib/source1.js
|
10
|
+
# - lib/source2.js
|
11
|
+
# - dist/**/*.js
|
12
|
+
#
|
13
|
+
src_files:
|
14
|
+
- app/assets/javascripts/private_pub.js
|
15
|
+
|
16
|
+
# stylesheets
|
17
|
+
#
|
18
|
+
# Return an array of stylesheet filepaths relative to src_dir to include before jasmine specs.
|
19
|
+
# Default: []
|
20
|
+
#
|
21
|
+
# EXAMPLE:
|
22
|
+
#
|
23
|
+
# stylesheets:
|
24
|
+
# - css/style.css
|
25
|
+
# - stylesheets/*.css
|
26
|
+
#
|
27
|
+
stylesheets:
|
28
|
+
|
29
|
+
# helpers
|
30
|
+
#
|
31
|
+
# Return an array of filepaths relative to spec_dir to include before jasmine specs.
|
32
|
+
# Default: ["helpers/**/*.js"]
|
33
|
+
#
|
34
|
+
# EXAMPLE:
|
35
|
+
#
|
36
|
+
# helpers:
|
37
|
+
# - helpers/**/*.js
|
38
|
+
#
|
39
|
+
helpers:
|
40
|
+
|
41
|
+
# spec_files
|
42
|
+
#
|
43
|
+
# Return an array of filepaths relative to spec_dir to include.
|
44
|
+
# Default: ["**/*[sS]pec.js"]
|
45
|
+
#
|
46
|
+
# EXAMPLE:
|
47
|
+
#
|
48
|
+
# spec_files:
|
49
|
+
# - **/*[sS]pec.js
|
50
|
+
#
|
51
|
+
spec_files:
|
52
|
+
|
53
|
+
# src_dir
|
54
|
+
#
|
55
|
+
# Source directory path. Your src_files must be returned relative to this path. Will use root if left blank.
|
56
|
+
# Default: project root
|
57
|
+
#
|
58
|
+
# EXAMPLE:
|
59
|
+
#
|
60
|
+
# src_dir: public
|
61
|
+
#
|
62
|
+
src_dir:
|
63
|
+
|
64
|
+
# spec_dir
|
65
|
+
#
|
66
|
+
# Spec directory path. Your spec_files must be returned relative to this path.
|
67
|
+
# Default: spec/javascripts
|
68
|
+
#
|
69
|
+
# EXAMPLE:
|
70
|
+
#
|
71
|
+
# spec_dir: spec/javascripts
|
72
|
+
#
|
73
|
+
spec_dir:
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Jasmine
|
2
|
+
class Config
|
3
|
+
|
4
|
+
# Add your overrides or custom config code here
|
5
|
+
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
|
10
|
+
# Note - this is necessary for rspec2, which has removed the backtrace
|
11
|
+
module Jasmine
|
12
|
+
class SpecBuilder
|
13
|
+
def declare_spec(parent, spec)
|
14
|
+
me = self
|
15
|
+
example_name = spec["name"]
|
16
|
+
@spec_ids << spec["id"]
|
17
|
+
backtrace = @example_locations[parent.description + " " + example_name]
|
18
|
+
parent.it example_name, {} do
|
19
|
+
me.report_spec(spec["id"])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
$:.unshift(ENV['JASMINE_GEM_PATH']) if ENV['JASMINE_GEM_PATH'] # for gem testing purposes
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'jasmine'
|
5
|
+
jasmine_config_overrides = File.expand_path(File.join(File.dirname(__FILE__), 'jasmine_config.rb'))
|
6
|
+
require jasmine_config_overrides if File.exist?(jasmine_config_overrides)
|
7
|
+
if Jasmine::Dependencies.rspec2?
|
8
|
+
require 'rspec'
|
9
|
+
else
|
10
|
+
require 'spec'
|
11
|
+
end
|
12
|
+
|
13
|
+
jasmine_config = Jasmine::Config.new
|
14
|
+
spec_builder = Jasmine::SpecBuilder.new(jasmine_config)
|
15
|
+
|
16
|
+
should_stop = false
|
17
|
+
|
18
|
+
if Jasmine::Dependencies.rspec2?
|
19
|
+
RSpec.configuration.after(:suite) do
|
20
|
+
spec_builder.stop if should_stop
|
21
|
+
end
|
22
|
+
else
|
23
|
+
Spec::Runner.configure do |config|
|
24
|
+
config.after(:suite) do
|
25
|
+
spec_builder.stop if should_stop
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
spec_builder.start
|
31
|
+
should_stop = true
|
32
|
+
spec_builder.declare_suites
|
@@ -0,0 +1,67 @@
|
|
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 eq("Incorrect signature.")
|
16
|
+
end
|
17
|
+
|
18
|
+
it "has no error when the signature matches the subscription" do
|
19
|
+
sub = PrivatePub.subscription(: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 signature just expired" do
|
28
|
+
PrivatePub.config[:signature_expiration] = 1
|
29
|
+
sub = PrivatePub.subscription(:timestamp => 123, :channel => "hello")
|
30
|
+
@message["subscription"] = sub[:channel]
|
31
|
+
@message["ext"]["private_pub_signature"] = sub[:signature]
|
32
|
+
@message["ext"]["private_pub_timestamp"] = sub[:timestamp]
|
33
|
+
message = @faye.incoming(@message, lambda { |m| m })
|
34
|
+
message["error"].should eq("Signature has expired.")
|
35
|
+
end
|
36
|
+
|
37
|
+
it "has an error when trying to publish to a custom channel with a bad token" do
|
38
|
+
PrivatePub.config[:secret_token] = "good"
|
39
|
+
@message["channel"] = "/custom/channel"
|
40
|
+
@message["ext"]["private_pub_token"] = "bad"
|
41
|
+
message = @faye.incoming(@message, lambda { |m| m })
|
42
|
+
message["error"].should eq("Incorrect token.")
|
43
|
+
end
|
44
|
+
|
45
|
+
it "raises an exception when attempting to call a custom channel without a secret_token set" do
|
46
|
+
@message["channel"] = "/custom/channel"
|
47
|
+
@message["ext"]["private_pub_token"] = "bad"
|
48
|
+
lambda {
|
49
|
+
message = @faye.incoming(@message, lambda { |m| m })
|
50
|
+
}.should raise_error("No secret_token config set, ensure private_pub.yml is loaded properly.")
|
51
|
+
end
|
52
|
+
|
53
|
+
it "has no error on other meta calls" do
|
54
|
+
@message["channel"] = "/meta/connect"
|
55
|
+
message = @faye.incoming(@message, lambda { |m| m })
|
56
|
+
message["error"].should be_nil
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should not let message carry the private pub token after server's validation" do
|
60
|
+
PrivatePub.config[:secret_token] = "good"
|
61
|
+
@message["channel"] = "/custom/channel"
|
62
|
+
@message["ext"]["private_pub_token"] = PrivatePub.config[:secret_token]
|
63
|
+
message = @faye.incoming(@message, lambda { |m| m })
|
64
|
+
message['ext']["private_pub_token"].should be_nil
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe PrivatePub do
|
4
|
+
before(:each) do
|
5
|
+
PrivatePub.reset_config
|
6
|
+
end
|
7
|
+
|
8
|
+
it "defaults server to nil" do
|
9
|
+
PrivatePub.config[:server].should be_nil
|
10
|
+
end
|
11
|
+
|
12
|
+
it "defaults signature_expiration to nil" do
|
13
|
+
PrivatePub.config[:signature_expiration].should be_nil
|
14
|
+
end
|
15
|
+
|
16
|
+
it "defaults subscription timestamp to current time in milliseconds" do
|
17
|
+
time = Time.now
|
18
|
+
Time.stub!(:now).and_return(time)
|
19
|
+
PrivatePub.subscription[:timestamp].should eq((time.to_f * 1000).round)
|
20
|
+
end
|
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 eq("http://example.com/faye")
|
25
|
+
PrivatePub.config[:secret_token].should eq("PRODUCTION_SECRET_TOKEN")
|
26
|
+
PrivatePub.config[:signature_expiration].should eq(600)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "raises an exception if an invalid environment is passed to load_config" do
|
30
|
+
lambda {
|
31
|
+
PrivatePub.load_config("spec/fixtures/private_pub.yml", :test)
|
32
|
+
}.should raise_error ArgumentError
|
33
|
+
end
|
34
|
+
|
35
|
+
it "includes channel, server, and custom time in subscription" do
|
36
|
+
PrivatePub.config[:server] = "server"
|
37
|
+
subscription = PrivatePub.subscription(:timestamp => 123, :channel => "hello")
|
38
|
+
subscription[:timestamp].should eq(123)
|
39
|
+
subscription[:channel].should eq("hello")
|
40
|
+
subscription[:server].should eq("server")
|
41
|
+
end
|
42
|
+
|
43
|
+
it "does a sha1 digest of channel, timestamp, and secret token" do
|
44
|
+
PrivatePub.config[:secret_token] = "token"
|
45
|
+
subscription = PrivatePub.subscription(:timestamp => 123, :channel => "channel")
|
46
|
+
subscription[:signature].should eq(Digest::SHA1.hexdigest("tokenchannel123"))
|
47
|
+
end
|
48
|
+
|
49
|
+
it "formats a message hash given a channel and a string for eval" do
|
50
|
+
PrivatePub.config[:secret_token] = "token"
|
51
|
+
PrivatePub.message("chan", "foo").should eq(
|
52
|
+
:ext => {:private_pub_token => "token"},
|
53
|
+
:channel => "chan",
|
54
|
+
:data => {
|
55
|
+
:channel => "chan",
|
56
|
+
:eval => "foo"
|
57
|
+
}
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
it "formats a message hash given a channel and a hash" do
|
62
|
+
PrivatePub.config[:secret_token] = "token"
|
63
|
+
PrivatePub.message("chan", :foo => "bar").should eq(
|
64
|
+
:ext => {:private_pub_token => "token"},
|
65
|
+
:channel => "chan",
|
66
|
+
:data => {
|
67
|
+
:channel => "chan",
|
68
|
+
:data => {:foo => "bar"}
|
69
|
+
}
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "publish message as json to server using Net::HTTP" do
|
74
|
+
PrivatePub.config[:server] = "http://localhost"
|
75
|
+
message = 'foo'
|
76
|
+
form = mock(:post).as_null_object
|
77
|
+
http = mock(:http).as_null_object
|
78
|
+
|
79
|
+
Net::HTTP::Post.should_receive(:new).with('/').and_return(form)
|
80
|
+
form.should_receive(:set_form_data).with(message: 'foo'.to_json)
|
81
|
+
|
82
|
+
Net::HTTP.should_receive(:new).with('localhost', 80).and_return(http)
|
83
|
+
http.should_receive(:start).and_yield(http)
|
84
|
+
http.should_receive(:request).with(form).and_return(:result)
|
85
|
+
|
86
|
+
PrivatePub.publish_message(message).should eq(:result)
|
87
|
+
end
|
88
|
+
|
89
|
+
it "it should use HTTPS if the server URL says so" do
|
90
|
+
PrivatePub.config[:server] = "https://localhost"
|
91
|
+
http = mock(:http).as_null_object
|
92
|
+
|
93
|
+
Net::HTTP.should_receive(:new).and_return(http)
|
94
|
+
http.should_receive(:use_ssl=).with(true)
|
95
|
+
|
96
|
+
PrivatePub.publish_message('foo')
|
97
|
+
end
|
98
|
+
|
99
|
+
it "it should not use HTTPS if the server URL says not to" do
|
100
|
+
PrivatePub.config[:server] = "http://localhost"
|
101
|
+
http = mock(:http).as_null_object
|
102
|
+
|
103
|
+
Net::HTTP.should_receive(:new).and_return(http)
|
104
|
+
http.should_receive(:use_ssl=).with(false)
|
105
|
+
|
106
|
+
PrivatePub.publish_message('foo')
|
107
|
+
end
|
108
|
+
|
109
|
+
it "raises an exception if no server is specified when calling publish_message" do
|
110
|
+
lambda {
|
111
|
+
PrivatePub.publish_message("foo")
|
112
|
+
}.should raise_error(PrivatePub::Error)
|
113
|
+
end
|
114
|
+
|
115
|
+
it "publish_to passes message to publish_message call" do
|
116
|
+
PrivatePub.should_receive(:message).with("chan", "foo").and_return("message")
|
117
|
+
PrivatePub.should_receive(:publish_message).with("message").and_return(:result)
|
118
|
+
PrivatePub.publish_to("chan", "foo").should eq(:result)
|
119
|
+
end
|
120
|
+
|
121
|
+
it "has a Faye rack app instance" do
|
122
|
+
PrivatePub.faye_app.should be_kind_of(Faye::RackAdapter)
|
123
|
+
end
|
124
|
+
|
125
|
+
it "says signature has expired when time passed in is greater than expiration" do
|
126
|
+
PrivatePub.config[:signature_expiration] = 30*60
|
127
|
+
time = PrivatePub.subscription[:timestamp] - 31*60*1000
|
128
|
+
PrivatePub.signature_expired?(time).should be_true
|
129
|
+
end
|
130
|
+
|
131
|
+
it "says signature has not expired when time passed in is less than expiration" do
|
132
|
+
PrivatePub.config[:signature_expiration] = 30*60
|
133
|
+
time = PrivatePub.subscription[:timestamp] - 29*60*1000
|
134
|
+
PrivatePub.signature_expired?(time).should be_false
|
135
|
+
end
|
136
|
+
|
137
|
+
it "says signature has not expired when expiration is nil" do
|
138
|
+
PrivatePub.config[:signature_expiration] = nil
|
139
|
+
PrivatePub.signature_expired?(0).should be_false
|
140
|
+
end
|
141
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: peritus_private_pub
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tyler DeWitt
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-04-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: faye
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 2.8.0
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 2.8.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: jasmine
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.1.1
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.1.1
|
69
|
+
description: Private pub/sub messaging in Rails through Faye.
|
70
|
+
email: tdewitt@peritus.com
|
71
|
+
executables: []
|
72
|
+
extensions: []
|
73
|
+
extra_rdoc_files: []
|
74
|
+
files:
|
75
|
+
- CHANGELOG.md
|
76
|
+
- Gemfile
|
77
|
+
- LICENSE
|
78
|
+
- README.md
|
79
|
+
- Rakefile
|
80
|
+
- app/assets/javascripts/private_pub.js
|
81
|
+
- lib/generators/private_pub/install_generator.rb
|
82
|
+
- lib/generators/private_pub/templates/private_pub.ru
|
83
|
+
- lib/generators/private_pub/templates/private_pub.yml
|
84
|
+
- lib/private_pub.rb
|
85
|
+
- lib/private_pub/engine.rb
|
86
|
+
- lib/private_pub/faye_extension.rb
|
87
|
+
- lib/private_pub/view_helpers.rb
|
88
|
+
- peritus_private_pub.gemspec
|
89
|
+
- spec/fixtures/private_pub.yml
|
90
|
+
- spec/javascripts/private_pub_spec.js
|
91
|
+
- spec/javascripts/support/jasmine.yml
|
92
|
+
- spec/javascripts/support/jasmine_config.rb
|
93
|
+
- spec/javascripts/support/jasmine_runner.rb
|
94
|
+
- spec/private_pub/faye_extension_spec.rb
|
95
|
+
- spec/private_pub_spec.rb
|
96
|
+
- spec/spec_helper.rb
|
97
|
+
homepage: https://github.com/PeritusSolutions/peritus_private_pub
|
98
|
+
licenses: []
|
99
|
+
metadata: {}
|
100
|
+
post_install_message:
|
101
|
+
rdoc_options: []
|
102
|
+
require_paths:
|
103
|
+
- lib
|
104
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '0'
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: 1.3.4
|
114
|
+
requirements: []
|
115
|
+
rubyforge_project: peritus_private_pub
|
116
|
+
rubygems_version: 2.4.5
|
117
|
+
signing_key:
|
118
|
+
specification_version: 4
|
119
|
+
summary: Private pub/sub messaging in Rails.
|
120
|
+
test_files: []
|