private_pub_plus 1.0.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b4ac9b51e06591b63f0129bf5d46d41ff2abb3bf
4
+ data.tar.gz: 8ab9a851ac353c79bde3218e7eaf98eadde9d998
5
+ SHA512:
6
+ metadata.gz: 92681a6f81ef940a9e07357626cec5bca7ffc9983118bf3a38c0e2c09d9ffe35cab9b4e979e6db227ea0800f81d2ed948c7dac4cec862053b2c43aabc23353f2
7
+ data.tar.gz: 88ed9116c56e5bbee7f11e8da6d5f79a4952cf8483fee5ca0afe1b43eff887c5fbacdc9b83ff21d60e04c4c250529f97c111acbb4f6651f5d6f038478061e4a7
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
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,43 @@
1
+ # Private Pub Plus
2
+
3
+ Private Pub Plus 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
+ Inspired by [Private pub Gem](https://github.com/ryanb/private_pub)
6
+ Watch [RailsCasts Episode 316](http://railscasts.com/episodes/316-private-pub) for a demonstration of Private Pub.
7
+
8
+
9
+ ## Setup
10
+
11
+ 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.
12
+
13
+ ```ruby
14
+ gem "private_pub_plus", git: "git://github.com/amritsingh/private_pub.git"
15
+ gem "thin"
16
+ ```
17
+
18
+ Run the generator to create the initial files.
19
+
20
+ ```
21
+ rails g private_pub_plus:install
22
+ ```
23
+ This will generate a file in Rails root directory - 'private_pub.ru'.
24
+ Authentication of subscribe and publish messages can be modified based on the needs of the application.
25
+
26
+
27
+ Next, start up Faye using the rackup file that was generated.
28
+
29
+ ```
30
+ rackup private_pub.ru -s thin -E production
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ Use the `publish_to` method to send data to that channel.
36
+
37
+ ```ruby
38
+ PrivatePubPlus.publish_to "/messages/new", :chat_message => "Hello, world!" do |channel, data|
39
+ # construct a message here. This message is sent to the subscribed clients e.g.
40
+ # {:channel => channel, :data => {:channel => channel, :data => data, :time => Time.now}, :ext => {:private_pub_token => <secret_token from config/private_pub.yml>}}
41
+ end
42
+ ```
43
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rspec/core/rake_task'
4
+
5
+ desc "Run RSpec"
6
+ RSpec::Core::RakeTask.new do |t|
7
+ t.verbose = false
8
+ end
9
+
10
+ task :default => [:spec]
@@ -0,0 +1,14 @@
1
+ module PrivatePubPlus
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
+ copy_file "private_pub.ru", "private_pub.ru"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,45 @@
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
+ module PrivatePubPlus
8
+ # This class is an extension for the Faye::RackAdapter.
9
+ # It is used inside of PrivatePubPlus.faye_app.
10
+ class FayeExtension
11
+ # Callback to handle incoming Faye messages. This authenticates both
12
+ # subscribe and publish calls.
13
+ def incoming(message, callback)
14
+ if message["channel"] == "/meta/subscribe"
15
+ authenticate_subscribe(message)
16
+ elsif message["channel"] !~ %r{^/meta/}
17
+ authenticate_publish(message)
18
+ end
19
+ callback.call(message)
20
+ end
21
+
22
+ private
23
+ def authenticate_subscribe(message)
24
+ # TODO:
25
+ # Code to authenticate the subscribe message.
26
+ end
27
+
28
+ def authenticate_publish(message)
29
+ # TODO:
30
+ # Code to authenticate the publish message
31
+ if PrivatePubPlus.config[:secret_token].nil?
32
+ raise Error, "No secret_token config set, ensure private_pub.yml is loaded properly."
33
+ elsif message["ext"]["private_pub_token"] != PrivatePubPlus.config[:secret_token]
34
+ message["error"] = "Incorrect token."
35
+ else
36
+ message["ext"]["private_pub_token"] = nil
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ Faye::WebSocket.load_adapter('thin')
43
+
44
+ PrivatePubPlus.load_config(File.expand_path("../config/private_pub.yml", __FILE__), ENV["RAILS_ENV"] || "development")
45
+ run PrivatePubPlus.faye_app
@@ -0,0 +1,10 @@
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: "<%= defined?(SecureRandom) ? SecureRandom.hex(32) : ActiveSupport::SecureRandom.hex(32) %>"
10
+ signature_expiration: 3600 # one hour
@@ -0,0 +1,14 @@
1
+ module PrivatePubPlus
2
+ class Engine < Rails::Engine
3
+ # Loads the private_pub.yml file if it exists.
4
+ initializer "private_pub.config" do
5
+ path = Rails.root.join("config/private_pub.yml")
6
+ PrivatePubPlus.load_config(path, Rails.env) if path.exist?
7
+ end
8
+
9
+ # Adds the ViewHelpers into ActionView::Base
10
+ #initializer "private_pub.view_helpers" do
11
+ # ActionView::Base.send :include, ViewHelpers
12
+ #end
13
+ end
14
+ end
@@ -0,0 +1,39 @@
1
+ module PrivatePubPlus
2
+ # This class is an extension for the Faye::RackAdapter.
3
+ # It is used inside of PrivatePubPlus.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 = PrivatePubPlus.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 PrivatePubPlus.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 PrivatePubPlus.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"] != PrivatePubPlus.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,3 @@
1
+ module PrivatePubPlus
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,85 @@
1
+ require "digest/sha1"
2
+ require "net/http"
3
+ require "net/https"
4
+ require "yaml"
5
+
6
+ require "private_pub_plus/faye_extension"
7
+ require "private_pub_plus/engine" if defined? Rails
8
+
9
+ module PrivatePubPlus
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_file(filename)[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, &block)
30
+ m = nil
31
+ if block_given?
32
+ m = block.call(channel, data)
33
+ else
34
+ m = message(channel, data)
35
+ end
36
+ publish_message(m)
37
+ end
38
+
39
+ # Sends the given message hash to the Faye server using Net::HTTP.
40
+ def publish_message(message)
41
+ raise Error, "No server specified, ensure private_pub.yml was loaded properly." unless config[:server]
42
+ url = URI.parse(config[:server])
43
+
44
+ form = Net::HTTP::Post.new(url.path.empty? ? '/' : url.path)
45
+ form.set_form_data(:message => message.to_json)
46
+
47
+ http = Net::HTTP.new(url.host, url.port)
48
+ http.use_ssl = url.scheme == "https"
49
+ http.start {|h| h.request(form)}
50
+ end
51
+
52
+ # Returns a message hash for sending to Faye
53
+ def message(channel, data)
54
+ message = {:channel => channel, :data => {:channel => channel}, :ext => {:private_pub_token => config[:secret_token]}}
55
+ if data.kind_of? String
56
+ message[:data][:eval] = data
57
+ else
58
+ message[:data][:data] = data
59
+ end
60
+ message
61
+ end
62
+
63
+ # Returns a subscription hash to pass to the PrivatePubPlus.sign call in JavaScript.
64
+ # Any options passed are merged to the hash.
65
+ def subscription(options = {})
66
+ sub = {:server => config[:server], :timestamp => (Time.now.to_f * 1000).round}.merge(options)
67
+ sub[:signature] = Digest::SHA1.hexdigest([config[:secret_token], sub[:channel], sub[:timestamp]].join)
68
+ sub
69
+ end
70
+
71
+ # Determine if the signature has expired given a timestamp.
72
+ def signature_expired?(timestamp)
73
+ timestamp < ((Time.now.to_f - config[:signature_expiration])*1000).round if config[:signature_expiration]
74
+ end
75
+
76
+ # Returns the Faye Rack application.
77
+ # Any options given are passed to the Faye::RackAdapter.
78
+ def faye_app(options = {})
79
+ options = {:mount => "/faye", :timeout => 45, :extensions => [FayeExtension.new]}.merge(options)
80
+ Faye::RackAdapter.new(options)
81
+ end
82
+ end
83
+
84
+ reset_config
85
+ end
@@ -0,0 +1,8 @@
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
@@ -0,0 +1,141 @@
1
+ require "spec_helper"
2
+
3
+ describe PrivatePubPlus do
4
+ before(:each) do
5
+ PrivatePubPlus.reset_config
6
+ end
7
+
8
+ it "defaults server to nil" do
9
+ PrivatePubPlus.config[:server].should be_nil
10
+ end
11
+
12
+ it "defaults signature_expiration to nil" do
13
+ PrivatePubPlus.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
+ PrivatePubPlus.subscription[:timestamp].should eq((time.to_f * 1000).round)
20
+ end
21
+
22
+ it "loads a simple configuration file via load_config" do
23
+ PrivatePubPlus.load_config("spec/fixtures/private_pub.yml", "production")
24
+ PrivatePubPlus.config[:server].should eq("http://example.com/faye")
25
+ PrivatePubPlus.config[:secret_token].should eq("PRODUCTION_SECRET_TOKEN")
26
+ PrivatePubPlus.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
+ PrivatePubPlus.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
+ PrivatePubPlus.config[:server] = "server"
37
+ subscription = PrivatePubPlus.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
+ PrivatePubPlus.config[:secret_token] = "token"
45
+ subscription = PrivatePubPlus.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
+ PrivatePubPlus.config[:secret_token] = "token"
51
+ PrivatePubPlus.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
+ PrivatePubPlus.config[:secret_token] = "token"
63
+ PrivatePubPlus.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
+ PrivatePubPlus.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
+ PrivatePubPlus.publish_message(message).should eq(:result)
87
+ end
88
+
89
+ it "it should use HTTPS if the server URL says so" do
90
+ PrivatePubPlus.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
+ PrivatePubPlus.publish_message('foo')
97
+ end
98
+
99
+ it "it should not use HTTPS if the server URL says not to" do
100
+ PrivatePubPlus.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
+ PrivatePubPlus.publish_message('foo')
107
+ end
108
+
109
+ it "raises an exception if no server is specified when calling publish_message" do
110
+ lambda {
111
+ PrivatePubPlus.publish_message("foo")
112
+ }.should raise_error(PrivatePubPlus::Error)
113
+ end
114
+
115
+ it "publish_to passes message to publish_message call" do
116
+ PrivatePubPlus.should_receive(:message).with("chan", "foo").and_return("message")
117
+ PrivatePubPlus.should_receive(:publish_message).with("message").and_return(:result)
118
+ PrivatePubPlus.publish_to("chan", "foo").should eq(:result)
119
+ end
120
+
121
+ it "has a Faye rack app instance" do
122
+ PrivatePubPlus.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
+ PrivatePubPlus.config[:signature_expiration] = 30*60
127
+ time = PrivatePubPlus.subscription[:timestamp] - 31*60*1000
128
+ PrivatePubPlus.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
+ PrivatePubPlus.config[:signature_expiration] = 30*60
133
+ time = PrivatePubPlus.subscription[:timestamp] - 29*60*1000
134
+ PrivatePubPlus.signature_expired?(time).should be_false
135
+ end
136
+
137
+ it "says signature has not expired when expiration is nil" do
138
+ PrivatePubPlus.config[:signature_expiration] = nil
139
+ PrivatePubPlus.signature_expired?(0).should be_false
140
+ end
141
+ end
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'json'
4
+ require 'faye'
5
+ # require 'lib/private_pub/faye_extension'
6
+ Bundler.require(:default)
7
+
8
+ RSpec.configure do |config|
9
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: private_pub_plus
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Amrit Singh
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-04-15 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
+ description: Private pub/sub messaging in Rails through Faye. Modified the private
56
+ pub gem from https://github.com/ryanb/private_pub to make it customizable. Removed
57
+ js support
58
+ email: amrit0403@gmail.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - Gemfile
64
+ - LICENSE
65
+ - README.md
66
+ - Rakefile
67
+ - lib/generators/private_pub_plus/install_generator.rb
68
+ - lib/generators/private_pub_plus/templates/private_pub.ru
69
+ - lib/generators/private_pub_plus/templates/private_pub.yml
70
+ - lib/private_pub_plus.rb
71
+ - lib/private_pub_plus/engine.rb
72
+ - lib/private_pub_plus/faye_extension.rb
73
+ - lib/private_pub_plus/version.rb
74
+ - spec/fixtures/private_pub.yml
75
+ - spec/private_pub_plus_spec.rb
76
+ - spec/spec_helper.rb
77
+ homepage: http://github.com/amritsingh/private_pub
78
+ licenses: []
79
+ metadata: {}
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: 1.3.4
94
+ requirements: []
95
+ rubyforge_project: private_pub_plus
96
+ rubygems_version: 2.4.5
97
+ signing_key:
98
+ specification_version: 4
99
+ summary: Private pub/sub messaging in Rails.
100
+ test_files: []