simplepub 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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