simplepub 1.0.4

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.
@@ -0,0 +1,61 @@
1
+ module Simplepub
2
+ module InterfaceMethods
3
+ # Publish the given data to a specific channel. This ends up sending
4
+ # a Net::HTTP POST request to the Faye server.
5
+ def publish_to(channel, data)
6
+ publish_message(message(channel, data))
7
+ end
8
+
9
+ # Sends the given message hash to the Faye server using Net::HTTP.
10
+ def publish_message(message)
11
+ raise Error, "No server specified, ensure simplepub.yml was loaded properly." unless config[:server]
12
+ url = URI.parse(config[:server])
13
+
14
+ form = Net::HTTP::Post.new(url.path.empty? ? '/' : url.path)
15
+ form.set_form_data(:message => message.to_json)
16
+
17
+ http = Net::HTTP.new(url.host, url.port)
18
+ http.use_ssl = url.scheme == "https"
19
+
20
+ http.ca_path = config.ca_path if config.ca_path?
21
+ http.ca_file = config.ca_file if config.ca_file?
22
+
23
+ http.start {|h| h.request(form)}
24
+ end
25
+
26
+ # Returns a message hash for sending to Faye
27
+ def message(channel, data)
28
+ message = {:channel => channel, :data => {:channel => channel}, :ext => {:simplepub_token => config[:secret_token]}}
29
+ if data.kind_of? String
30
+ message[:data][:eval] = data
31
+ else
32
+ message[:data][:data] = data
33
+ end
34
+ message
35
+ end
36
+
37
+ # Returns a subscription hash to pass to the Simplepub.sign call in JavaScript.
38
+ # Any options passed are merged to the hash.
39
+ def subscription(options = {})
40
+ sub = {:server => config[:server], :timestamp => (Time.now.to_f * 1000).round}.merge(options)
41
+ sub[:signature] = Digest::SHA1.hexdigest([config[:secret_token], sub[:channel], sub[:timestamp]].join)
42
+ sub
43
+ end
44
+
45
+ # Determine if the signature has expired given a timestamp.
46
+ def signature_expired?(timestamp)
47
+ timestamp < ((Time.now.to_f - config[:signature_expiration])*1000).round if config[:signature_expiration]
48
+ end
49
+
50
+ # Returns the Faye Rack application.
51
+ # Any options given are passed to the Faye::RackAdapter.
52
+ def faye_app(options = {})
53
+ options = {:mount => "/faye", :timeout => 45, :extensions => [FayeExtension.new]}.merge(options)
54
+ Faye::RackAdapter.new(options)
55
+ end
56
+
57
+ def rackup_file
58
+ File.expand_path('../rack_config.ru', __FILE__)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,10 @@
1
+ # Run with: rackup simplepub.ru -s thin -E production
2
+ require "bundler/setup"
3
+ require "faye"
4
+ require "simplepub"
5
+
6
+ Faye::WebSocket.load_adapter('thin')
7
+
8
+ Simplepub.load_config
9
+
10
+ run Simplepub.faye_app
@@ -0,0 +1,3 @@
1
+ module Simplepub
2
+ VERSION = "1.0.4"
3
+ end
@@ -0,0 +1,22 @@
1
+ module Simplepub
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
+ Simplepub.publish_to(channel, data || capture(&block))
10
+ end
11
+
12
+ # Subscribe the client to the given channel. This generates
13
+ # some JavaScript calling Simplepub.sign with the subscription
14
+ # options.
15
+ def subscribe_to(channel)
16
+ subscription = Simplepub.subscription(:channel => channel)
17
+ content_tag "script", :type => "text/javascript" do
18
+ raw("Simplepub.sign(#{subscription.to_json});")
19
+ end
20
+ end
21
+ end
22
+ end
data/lib/simplepub.rb ADDED
@@ -0,0 +1,24 @@
1
+ require "simplepub/version"
2
+ require 'json'
3
+ require "digest/sha1"
4
+ require "net/http"
5
+ require "net/https"
6
+
7
+ require "simplepub/error"
8
+ require "simplepub/configuration"
9
+ require "simplepub/interface_methods"
10
+ require "simplepub/faye_extension"
11
+ require "simplepub/engine" if defined? Rails
12
+
13
+ module Simplepub
14
+ extend Configuration
15
+ extend InterfaceMethods
16
+
17
+ reset_config
18
+
19
+ def self.templates_root
20
+ File.dirname(__FILE__) + "/templates"
21
+ end
22
+
23
+ autoload :Cli, 'simplepub/cli'
24
+ end
@@ -0,0 +1,10 @@
1
+ # Run with: rackup simplepub.ru -s thin -E production
2
+ require "bundler/setup"
3
+ require "yaml"
4
+ require "faye"
5
+ require "simplepub"
6
+
7
+ Faye::WebSocket.load_adapter('thin')
8
+
9
+ Simplepub.load_config(File.expand_path("../config/simplepub.yml", __FILE__), ENV["RAILS_ENV"] || "development")
10
+ run Simplepub.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
data/simplepub.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'simplepub/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "simplepub"
8
+ spec.version = Simplepub::VERSION
9
+ spec.authors = ["Jorge Calás Lozano", "Ryan Bates"]
10
+ spec.email = ["calas@qvitta.net", "ryan@railscasts.com"]
11
+ spec.description = "Simple private pub/sub messaging for Ruby using Faye."
12
+ spec.summary = "Simple private pub/sub messaging for Ruby."
13
+ spec.homepage = "https://speedyrails.com"
14
+ spec.license = "MIT"
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_dependency 'faye'
21
+ spec.add_dependency 'thor'
22
+ spec.add_dependency 'hashie'
23
+ spec.add_dependency 'thin'
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.3"
26
+ spec.add_development_dependency "rake"
27
+ spec.add_development_dependency "rspec"
28
+ spec.add_development_dependency "jasmine"
29
+ 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,15 @@
1
+ beforeEach(function() {
2
+ this.addMatchers({
3
+ /**
4
+ // Example matcher:
5
+ //
6
+ // expect(player).not.toBePlaying(song);
7
+
8
+ toBePlaying: function(expectedSong) {
9
+ var player = this.actual;
10
+ return player.currentlyPlayingSong === expectedSong &&
11
+ player.isPlaying;
12
+ }
13
+ **/
14
+ });
15
+ });
@@ -0,0 +1,165 @@
1
+ describe("Simplepub", function() {
2
+ var pub, doc;
3
+ beforeEach(function() {
4
+ Faye = {}; // To simulate global Faye object
5
+ doc = {};
6
+ pub = buildSimplepub(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.simplepub_signature).toEqual("abcd");
20
+ expect(message.ext.simplepub_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,76 @@
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/simplepub.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
+ - "**/*[sS]pec.js"
53
+ - "**/*[sS]pec.coffee"
54
+ - "**/*[sS]pec.js.coffee"
55
+
56
+ # src_dir
57
+ #
58
+ # Source directory path. Your src_files must be returned relative to this path. Will use root if left blank.
59
+ # Default: project root
60
+ #
61
+ # EXAMPLE:
62
+ #
63
+ # src_dir: public
64
+ #
65
+ src_dir:
66
+
67
+ # spec_dir
68
+ #
69
+ # Spec directory path. Your spec_files must be returned relative to this path.
70
+ # Default: spec/javascripts
71
+ #
72
+ # EXAMPLE:
73
+ #
74
+ # spec_dir: spec/javascripts
75
+ #
76
+ 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,11 @@
1
+ #Use this file to set/override Jasmine configuration options
2
+ #You can remove it if you don't need it.
3
+ #This file is loaded *after* jasmine.yml is interpreted.
4
+ #
5
+ #Example: using a different boot file.
6
+ #Jasmine.configure do |config|
7
+ # config.boot_dir = '/absolute/path/to/boot_dir'
8
+ # config.boot_files = lambda { ['/absolute/path/to/boot_dir/file.js'] }
9
+ #end
10
+ #
11
+
@@ -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 Simplepub::FayeExtension do
4
+ before(:each) do
5
+ Simplepub.reset_config
6
+ @faye = Simplepub::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"]["simplepub_signature"] = "bad"
13
+ @message["ext"]["simplepub_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 = Simplepub.subscription(:channel => "hello")
20
+ @message["subscription"] = sub[:channel]
21
+ @message["ext"]["simplepub_signature"] = sub[:signature]
22
+ @message["ext"]["simplepub_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
+ Simplepub.config[:signature_expiration] = 1
29
+ sub = Simplepub.subscription(:timestamp => 123, :channel => "hello")
30
+ @message["subscription"] = sub[:channel]
31
+ @message["ext"]["simplepub_signature"] = sub[:signature]
32
+ @message["ext"]["simplepub_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
+ Simplepub.config[:secret_token] = "good"
39
+ @message["channel"] = "/custom/channel"
40
+ @message["ext"]["simplepub_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"]["simplepub_token"] = "bad"
48
+ lambda {
49
+ message = @faye.incoming(@message, lambda { |m| m })
50
+ }.should raise_error("No secret_token config set, ensure simplepub.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
+ Simplepub.config[:secret_token] = "good"
61
+ @message["channel"] = "/custom/channel"
62
+ @message["ext"]["simplepub_token"] = Simplepub.config[:secret_token]
63
+ message = @faye.incoming(@message, lambda { |m| m })
64
+ message['ext']["simplepub_token"].should be_nil
65
+ end
66
+
67
+ end