├── TODO ├── .gitignore ├── VERSION.yml ├── spec ├── spec_helper.rb └── ruby_bosh_spec.rb ├── autotest └── discover.rb ├── LICENSE ├── README ├── Rakefile ├── ruby_bosh.gemspec └── lib └── ruby_bosh.rb /TODO: -------------------------------------------------------------------------------- 1 | write basic tests 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw? 2 | .DS_Store 3 | coverage -------------------------------------------------------------------------------- /VERSION.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :major: 0 3 | :build: 4 | :minor: 7 5 | :patch: 1 6 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require File.join(File.dirname(__FILE__), '..', "lib", "ruby_bosh") 3 | 4 | -------------------------------------------------------------------------------- /autotest/discover.rb: -------------------------------------------------------------------------------- 1 | $:.push(File.join(File.dirname(__FILE__), %w[.. .. rspec])) 2 | 3 | Autotest.add_discovery do 4 | "rspec" 5 | end 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008 Pradeep Elankumaran 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. 21 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | ruby_bosh 2 | ========= 3 | 4 | The RubyBOSH library handles creating and pre-authenticating BOSH streams inside your Ruby application before passing them off to your template engine. 5 | 6 | This method allows you to hide authentication details for your users' XMPP accounts. 7 | 8 | Tested on Rails 2.x with eJabberd 1.2+ 9 | 10 | References 11 | ========== 12 | BOSH: http://xmpp.org/extensions/xep-0124.html 13 | XMPP via BOSH: http://xmpp.org/extensions/xep-0206.html 14 | 15 | Example 16 | ======= 17 | In your Ruby app controller (or equivalent): 18 | 19 | @session_jid, @session_id, @session_random_id = 20 | RubyBOSH.initialize_session("me@jabber.org", "my_password", "http://localhost:5280/http-bind") 21 | 22 | In your template, you would then pass these directly to your javascript BOSH connector: 23 | 24 | var bosh_jid = '<%= @session_jid %>'; 25 | var bosh_sid = '<%= @session_id %>'; 26 | var bosh_rid = '<%= @session_random_id %>'; 27 | 28 | // using Strophe: 29 | connect.attach(bosh_jid, bosh_sid, bosh_rid, onConnectHandlerFunction); 30 | 31 | Acknowledgements 32 | ================ 33 | Jack Moffit 34 | - thanks for the nice Django example :) 35 | #=> http://metajack.im/2008/10/03/getting-attached-to-strophe/ 36 | 37 | Copyright (c) 2008 Pradeep Elankumaran. See LICENSE for details. 38 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | 3 | begin 4 | require 'jeweler' 5 | Jeweler::Tasks.new do |s| 6 | s.name = "ruby_bosh" 7 | s.summary = %Q{A BOSH session pre-initializer for Ruby web applications} 8 | s.email = "pradeep@intridea.com" 9 | s.homepage = "http://github.com/skyfallsin/ruby_bosh" 10 | s.description = "An XMPP BOSH session pre-initializer for Ruby web applications" 11 | s.authors = ["Pradeep Elankumaran"] 12 | 13 | s.add_dependency("builder") 14 | s.add_dependency("rest-client") 15 | s.add_dependency("hpricot") 16 | s.add_dependency("SystemTimer") 17 | end 18 | rescue LoadError 19 | puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com" 20 | end 21 | 22 | require 'rdoc/task' 23 | Rake::RDocTask.new do |rdoc| 24 | rdoc.rdoc_dir = 'rdoc' 25 | rdoc.title = 'ruby_bosh' 26 | rdoc.options << '--line-numbers' << '--inline-source' 27 | rdoc.rdoc_files.include('README*') 28 | rdoc.rdoc_files.include('lib/**/*.rb') 29 | end 30 | 31 | require 'rake/testtask' 32 | Rake::TestTask.new(:test) do |t| 33 | t.libs << 'lib' << 'test' 34 | t.pattern = 'test/**/*_test.rb' 35 | t.verbose = false 36 | end 37 | 38 | begin 39 | require 'rcov/rcovtask' 40 | Rcov::RcovTask.new do |t| 41 | t.libs << 'test' 42 | t.test_files = FileList['test/**/*_test.rb'] 43 | t.verbose = true 44 | end 45 | rescue LoadError 46 | puts "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov" 47 | end 48 | 49 | begin 50 | require 'cucumber/rake/task' 51 | Cucumber::Rake::Task.new(:features) 52 | rescue LoadError 53 | puts "Cucumber is not available. In order to run features, you must: sudo gem install cucumber" 54 | end 55 | 56 | task :default => :test 57 | -------------------------------------------------------------------------------- /ruby_bosh.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{ruby_bosh} 8 | s.version = "0.7.1" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Pradeep Elankumaran"] 12 | s.date = %q{2011-07-10} 13 | s.description = %q{An XMPP BOSH session pre-initializer for Ruby web applications} 14 | s.email = %q{pradeep@intridea.com} 15 | s.extra_rdoc_files = [ 16 | "LICENSE", 17 | "README", 18 | "TODO" 19 | ] 20 | s.files = [ 21 | "LICENSE", 22 | "README", 23 | "Rakefile", 24 | "TODO", 25 | "VERSION.yml", 26 | "autotest/discover.rb", 27 | "lib/ruby_bosh.rb", 28 | "ruby_bosh.gemspec", 29 | "spec/ruby_bosh_spec.rb", 30 | "spec/spec_helper.rb" 31 | ] 32 | s.homepage = %q{http://github.com/skyfallsin/ruby_bosh} 33 | s.require_paths = ["lib"] 34 | s.rubygems_version = %q{1.3.7} 35 | s.summary = %q{A BOSH session pre-initializer for Ruby web applications} 36 | 37 | if s.respond_to? :specification_version then 38 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 39 | s.specification_version = 3 40 | 41 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 42 | s.add_runtime_dependency(%q, [">= 0"]) 43 | s.add_runtime_dependency(%q, [">= 0"]) 44 | s.add_runtime_dependency(%q, [">= 0"]) 45 | s.add_runtime_dependency(%q, [">= 0"]) 46 | else 47 | s.add_dependency(%q, [">= 0"]) 48 | s.add_dependency(%q, [">= 0"]) 49 | s.add_dependency(%q, [">= 0"]) 50 | s.add_dependency(%q, [">= 0"]) 51 | end 52 | else 53 | s.add_dependency(%q, [">= 0"]) 54 | s.add_dependency(%q, [">= 0"]) 55 | s.add_dependency(%q, [">= 0"]) 56 | s.add_dependency(%q, [">= 0"]) 57 | end 58 | end 59 | 60 | -------------------------------------------------------------------------------- /spec/ruby_bosh_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'spec_helper') 2 | 3 | describe RubyBOSH do 4 | before(:each) do 5 | RubyBOSH.logging = false 6 | @rbosh = RubyBOSH.new("skyfallsin@localhost", "skyfallsin", 7 | "http://localhost:5280/http-bind") 8 | #@rbosh.stub!(:success?).and_return(true) 9 | #@rbosh.stub!(:initialize_bosh_session).and_return(true) 10 | @rbosh.stub!(:send_auth_request).and_return(true) 11 | @rbosh.stub!(:send_restart_request).and_return(true) 12 | @rbosh.stub!(:request_resource_binding).and_return(true) 13 | @rbosh.stub!(:send_session_request).and_return(true) 14 | RestClient.stub!(:post).and_return("") 15 | end 16 | 17 | it "should set the sid attribute after the session creation request" do 18 | @rbosh.connect 19 | @rbosh.sid.should == '123456' 20 | end 21 | 22 | it "should update the rid on every call to the BOSH server" do 23 | @rbosh.rid = 100 24 | @rbosh.connect 25 | @rbosh.rid.should > 100 26 | end 27 | 28 | it "should return an array with [jid, sid, rid] on success" do 29 | s = @rbosh.connect 30 | s.should be_kind_of(Array) 31 | s.size.should == 3 32 | s.first.should == 'skyfallsin@localhost' 33 | s.last.should be_kind_of(Fixnum) 34 | s[1].should == '123456' 35 | end 36 | 37 | it "should return an array with [full_jid, sid, rid] on success" do 38 | RubyBOSH.logging = false 39 | @rbosh = RubyBOSH.new("skyfallsin@localhost", "skyfallsin", 40 | "http://localhost:5280/http-bind", :full_jid => true) 41 | #@rbosh.stub!(:success?).and_return(true) 42 | #@rbosh.stub!(:initialize_bosh_session).and_return(true) 43 | @rbosh.stub!(:send_auth_request).and_return(true) 44 | @rbosh.stub!(:send_restart_request).and_return(true) 45 | @rbosh.stub!(:request_resource_binding).and_return(true) 46 | @rbosh.stub!(:send_session_request).and_return(true) 47 | RestClient.stub!(:post).and_return("") 48 | @rbosh.resource = '54321' 49 | s = @rbosh.connect 50 | s.should be_kind_of(Array) 51 | s.size.should == 3 52 | s.first.should == 'skyfallsin@localhost/54321' 53 | s.last.should be_kind_of(Fixnum) 54 | s[1].should == '123456' 55 | end 56 | 57 | describe "Errors" do 58 | it "should crash with AuthFailed when its not a success?" do 59 | @rbosh.stub!(:send_session_request).and_return(false) 60 | lambda { @rbosh.connect }.should raise_error(RubyBOSH::AuthFailed) 61 | end 62 | 63 | it "should raise a ConnFailed if a connection could not be made to the XMPP server" do 64 | RestClient.stub!(:post).and_raise(Errno::ECONNREFUSED) 65 | lambda { @rbosh.connect }.should raise_error(RubyBOSH::ConnFailed) 66 | end 67 | 68 | it "should raise a Timeout::Error if the BOSH call takes forever" do 69 | SystemTimer.stub!(:timeout).and_raise(::Timeout::Error) 70 | lambda { @rbosh.connect }.should raise_error(RubyBOSH::Timeout) 71 | end 72 | 73 | it "should crash with a generic error on any other problem" do 74 | [RestClient::ServerBrokeConnection, RestClient::RequestTimeout].each{|err| 75 | RestClient.stub!(:post).and_raise(err) 76 | lambda { @rbosh.connect }.should raise_error(RubyBOSH::Error) 77 | } 78 | end 79 | 80 | after(:each) do 81 | lambda { @rbosh.connect }.should raise_error(RubyBOSH::Error) 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/ruby_bosh.rb: -------------------------------------------------------------------------------- 1 | require 'rest_client' 2 | require 'builder' 3 | require 'rexml/document' 4 | require 'base64' 5 | require 'hpricot' 6 | 7 | class RubyBOSH 8 | BOSH_XMLNS = 'http://jabber.org/protocol/httpbind' 9 | TLS_XMLNS = 'urn:ietf:params:xml:ns:xmpp-tls' 10 | SASL_XMLNS = 'urn:ietf:params:xml:ns:xmpp-sasl' 11 | BIND_XMLNS = 'urn:ietf:params:xml:ns:xmpp-bind' 12 | SESSION_XMLNS = 'urn:ietf:params:xml:ns:xmpp-session' 13 | CLIENT_XMLNS = 'jabber:client' 14 | 15 | class Error < StandardError; end 16 | class Timeout < RubyBOSH::Error; end 17 | class AuthFailed < RubyBOSH::Error; end 18 | class ConnFailed < RubyBOSH::Error; end 19 | 20 | @@logging = true 21 | def self.logging=(value) 22 | @@logging = value 23 | end 24 | 25 | attr_accessor :jid, :rid, :sid, :success, :resource 26 | def initialize(jid, pw, service_url, opts={}) 27 | @service_url = service_url 28 | @jid, @pw = jid, pw 29 | @host = jid.split("@").last 30 | @success = false 31 | @timeout = opts[:timeout] || 3 #seconds 32 | @headers = {"Content-Type" => "text/xml; charset=utf-8", 33 | "Accept" => "text/xml"} 34 | @wait = opts[:wait] || 5 35 | @hold = opts[:hold] || 3 36 | @window = opts[:window] || 5 37 | @full_jid = opts[:full_jid] || false 38 | end 39 | 40 | def success? 41 | @success == true 42 | end 43 | 44 | def self.initialize_session(*args) 45 | new(*args).connect 46 | end 47 | 48 | def connect 49 | initialize_bosh_session 50 | if send_auth_request 51 | # send_restart_request ## this is failing. I'm not sure what is supposed to do? 52 | request_resource_binding 53 | @success = send_session_request 54 | end 55 | 56 | raise RubyBOSH::AuthFailed, "could not authenticate #{@jid}" unless success? 57 | @rid += 1 #updates the rid for the next call from the browser 58 | resource = (@full_jid) ? "/#{@resource}" : "" 59 | ["#{@jid}#{resource}", @sid, @rid] 60 | end 61 | 62 | private 63 | def initialize_bosh_session 64 | response = deliver(construct_body(:wait => @wait, :to => @host, 65 | :hold => @hold, :window => @window, 66 | "xmpp:version" => '1.0')) 67 | parse(response) 68 | end 69 | 70 | def construct_body(params={}, &block) 71 | @rid ? @rid+=1 : @rid=rand(100000) 72 | 73 | builder = Builder::XmlMarkup.new 74 | parameters = {:rid => @rid, :xmlns => BOSH_XMLNS, 75 | "xmpp:version" => "1.0", 76 | "xmlns:xmpp" => "urn:xmpp:xbosh"}.merge(params) 77 | 78 | if block_given? 79 | builder.body(parameters) {|body| yield(body)} 80 | else 81 | builder.body(parameters) 82 | end 83 | end 84 | 85 | def send_auth_request 86 | request = construct_body(:sid => @sid) do |body| 87 | auth_string = "#{@jid}\x00#{@jid.split("@").first.strip}\x00#{@pw}" 88 | body.auth(Base64.encode64(auth_string).gsub(/\s/,''), 89 | :xmlns => SASL_XMLNS, :mechanism => 'PLAIN') 90 | end 91 | 92 | response = deliver(request) 93 | response.include?("success") 94 | end 95 | 96 | def send_restart_request 97 | request = construct_body(:sid => @sid, "xmpp:restart" => true, "xmlns:xmpp" => 'urn:xmpp:xbosh') 98 | deliver(request).include?("stream:features") 99 | end 100 | 101 | def request_resource_binding 102 | request = construct_body(:sid => @sid) do |body| 103 | body.iq(:id => "bind_#{rand(100000)}", :type => "set", 104 | :xmlns => "jabber:client") do |iq| 105 | iq.bind(:xmlns => BIND_XMLNS) do |bind| 106 | @resource = "bosh_#{rand(10000)}" 107 | bind.resource(@resource) 108 | end 109 | end 110 | end 111 | 112 | response = deliver(request) 113 | response.include?("") 114 | end 115 | 116 | def send_session_request 117 | request = construct_body(:sid => @sid) do |body| 118 | body.iq(:xmlns => CLIENT_XMLNS, :type => "set", 119 | :id => "sess_#{rand(100000)}") do |iq| 120 | iq.session(:xmlns => SESSION_XMLNS) 121 | end 122 | end 123 | 124 | response = deliver(request) 125 | response.include?("body") 126 | end 127 | 128 | def parse(_response) 129 | doc = Hpricot(_response.to_s) 130 | doc.search("//body").each do |body| 131 | @sid = body.attributes["sid"].to_s 132 | end 133 | _response 134 | end 135 | 136 | begin 137 | require 'system_timer' 138 | def deliver(xml) 139 | SystemTimer.timeout(@timeout) do 140 | send(xml) 141 | recv(RestClient.post(@service_url, xml, @headers)) 142 | end 143 | rescue ::Timeout::Error => e 144 | raise RubyBOSH::Timeout, e.message 145 | rescue Errno::ECONNREFUSED => e 146 | raise RubyBOSH::ConnFailed, "could not connect to #{@host}\n#{e.message}" 147 | rescue Exception => e 148 | raise RubyBOSH::Error, e.message 149 | end 150 | rescue LoadError 151 | warn "WARNING: using the built-in Timeout class which is known to have issues when used for opening connections. Install the SystemTimer gem if you want to make sure the Redis client will not hang." unless RUBY_VERSION >= "1.9" || RUBY_PLATFORM =~ /java/ 152 | 153 | require "timeout" 154 | def deliver(xml) 155 | Timeout.timeout(@timeout) do 156 | send(xml) 157 | recv(RestClient.post(@service_url, xml, @headers)) 158 | end 159 | rescue ::Timeout::Error => e 160 | raise RubyBOSH::Timeout, e.message 161 | rescue Errno::ECONNREFUSED => e 162 | raise RubyBOSH::ConnFailed, "could not connect to #{@host}\n#{e.message}" 163 | rescue Exception => e 164 | raise RubyBOSH::Error, e.message 165 | end 166 | end 167 | 168 | def send(msg) 169 | puts("Ruby-BOSH - SEND\n[#{now}]: #{msg}") if @@logging; msg 170 | end 171 | 172 | def recv(msg) 173 | puts("Ruby-BOSH - RECV\n[#{now}]: #{msg}") if @@logging; msg 174 | end 175 | 176 | private 177 | def now 178 | Time.now.strftime("%a %b %d %H:%M:%S %Y") 179 | end 180 | end 181 | 182 | 183 | if __FILE__ == $0 184 | p RubyBOSH.initialize_session(ARGV[0], ARGV[1], 185 | "http://localhost:5280/http-bind") 186 | end 187 | --------------------------------------------------------------------------------