├── .hound.yml ├── .rspec ├── lib ├── blather │ ├── version.rb │ ├── stanza │ │ ├── pubsub │ │ │ ├── errors.rb │ │ │ ├── subscribe.rb │ │ │ ├── create.rb │ │ │ ├── affiliations.rb │ │ │ ├── unsubscribe.rb │ │ │ ├── subscriptions.rb │ │ │ ├── retract.rb │ │ │ ├── items.rb │ │ │ ├── publish.rb │ │ │ ├── event.rb │ │ │ └── subscription.rb │ │ ├── disco.rb │ │ ├── presence │ │ │ ├── muc.rb │ │ │ ├── c.rb │ │ │ ├── subscription.rb │ │ │ └── muc_user.rb │ │ ├── muc │ │ │ └── muc_user_base.rb │ │ ├── iq │ │ │ ├── ping.rb │ │ │ ├── query.rb │ │ │ ├── ibr.rb │ │ │ └── ibb.rb │ │ ├── pubsub_owner │ │ │ ├── purge.rb │ │ │ └── delete.rb │ │ ├── pubsub_owner.rb │ │ ├── message │ │ │ └── muc_user.rb │ │ └── pubsub.rb │ ├── core_ext │ │ ├── ipaddr.rb │ │ └── eventmachine.rb │ ├── stream │ │ ├── features │ │ │ ├── tls.rb │ │ │ ├── session.rb │ │ │ ├── register.rb │ │ │ └── resource.rb │ │ ├── client.rb │ │ ├── component.rb │ │ ├── features.rb │ │ └── parser.rb │ ├── errors │ │ ├── sasl_error.rb │ │ ├── stream_error.rb │ │ └── stanza_error.rb │ ├── errors.rb │ ├── cert_store.rb │ ├── file_transfer │ │ ├── ibb.rb │ │ └── s5b.rb │ ├── client.rb │ ├── roster.rb │ ├── xmpp_node.rb │ └── file_transfer.rb └── blather.rb ├── yard └── templates │ └── default │ └── class │ ├── text │ └── handlers.erb │ ├── setup.rb │ └── html │ └── handlers.erb ├── Gemfile ├── spec ├── support │ └── mock_server.rb ├── spec_helper.rb ├── blather │ ├── stream │ │ ├── ssl_spec.rb │ │ └── component_spec.rb │ ├── errors │ │ ├── sasl_error_spec.rb │ │ └── stream_error_spec.rb │ ├── errors_spec.rb │ ├── stanza │ │ ├── pubsub_owner_spec.rb │ │ ├── presence │ │ │ ├── muc_spec.rb │ │ │ ├── c_spec.rb │ │ │ ├── muc_user_spec.rb │ │ │ └── subscription_spec.rb │ │ ├── iq │ │ │ ├── ping_spec.rb │ │ │ ├── s5b_spec.rb │ │ │ ├── query_spec.rb │ │ │ ├── si_spec.rb │ │ │ ├── vcard_spec.rb │ │ │ └── ibb_spec.rb │ │ ├── pubsub_owner │ │ │ ├── purge_spec.rb │ │ │ └── delete_spec.rb │ │ ├── pubsub │ │ │ ├── create_spec.rb │ │ │ ├── affiliations_spec.rb │ │ │ ├── subscribe_spec.rb │ │ │ ├── retract_spec.rb │ │ │ ├── subscriptions_spec.rb │ │ │ ├── items_spec.rb │ │ │ ├── publish_spec.rb │ │ │ ├── unsubscribe_spec.rb │ │ │ └── event_spec.rb │ │ ├── iq_spec.rb │ │ └── pubsub_spec.rb │ ├── xmpp_node_spec.rb │ ├── jid_spec.rb │ ├── roster_spec.rb │ └── roster_item_spec.rb ├── fixtures │ └── certificate.crt └── blather_spec.rb ├── Guardfile ├── .gitignore ├── examples ├── execute.rb ├── rosterprint.rb ├── echo.rb ├── trusted_echo.rb ├── MUC_echo.rb ├── stream_only.rb ├── ping_pong.rb ├── xmpp4r │ └── echo.rb ├── certs │ └── README └── print_hierarchy.rb ├── .travis.yml ├── Rakefile ├── LICENSE └── blather.gemspec /.hound.yml: -------------------------------------------------------------------------------- 1 | LineLength: 2 | Enabled: false 3 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --colour 3 | --tty 4 | -------------------------------------------------------------------------------- /lib/blather/version.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | VERSION = '2.0.0' 3 | end 4 | -------------------------------------------------------------------------------- /yard/templates/default/class/text/handlers.erb: -------------------------------------------------------------------------------- 1 | <%= indent(wrap("Handler Stack: #{@handler_stack * ', '}")) %>] 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'activesupport', '~> 3.0' if RUBY_VERSION == "1.9.2" 6 | -------------------------------------------------------------------------------- /spec/support/mock_server.rb: -------------------------------------------------------------------------------- 1 | class MockServer; end 2 | 3 | module ServerMock 4 | def receive_data(data) 5 | @server ||= MockServer.new 6 | @server.receive_data data, self 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard 'rspec', cmd: 'bundle exec rspec' do 2 | watch(%r{^spec/.+_spec\.rb$}) 3 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 4 | watch('spec/spec_helper.rb') { "spec/" } 5 | end 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | Manifest 3 | doc 4 | coverage 5 | *.bundle 6 | Gemfile.lock 7 | mkmf.log 8 | *.o 9 | Makefile 10 | rdoc 11 | *.gem 12 | *.log 13 | *.pid 14 | notes 15 | .yardoc 16 | *.rbc 17 | vendor 18 | .rvmrc 19 | .rbx 20 | tmp 21 | -------------------------------------------------------------------------------- /yard/templates/default/class/setup.rb: -------------------------------------------------------------------------------- 1 | def init 2 | super 3 | sections.place(:handlers).after(:box_info) 4 | end 5 | 6 | def handlers 7 | @handler_stack = object.inheritance_tree.map { |o| o.tag(:handler).name if (o.respond_to?(:tag) && o.tag(:handler)) }.compact 8 | return if @handler_stack.empty? 9 | erb(:handlers) 10 | end 11 | -------------------------------------------------------------------------------- /examples/execute.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | require 'blather/client' 5 | 6 | message :chat?, :body => 'exit' do |m| 7 | say m.from, 'Exiting ...' 8 | shutdown 9 | end 10 | 11 | message :chat?, :body do |m| 12 | begin 13 | say m.from, eval(m.body) 14 | rescue => e 15 | say m.from, e.inspect 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.3.7 4 | - 2.4.4 5 | - jruby-9.1.17.0 6 | - rbx-3.105 7 | - ruby-head 8 | jdk: 9 | - openjdk8 # for jruby 10 | matrix: 11 | allow_failures: 12 | - rvm: ruby-head 13 | - rvm: rbx-3.105 14 | before_install: 15 | - gem install bundler 16 | notifications: 17 | irc: "irc.freenode.org#adhearsion" 18 | -------------------------------------------------------------------------------- /lib/blather/stanza/pubsub/errors.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stanza 3 | 4 | # # PusSub Error Stanza 5 | # 6 | # @private 7 | class PubSubErrors < PubSub 8 | def node 9 | read_attr :node 10 | end 11 | 12 | def node=(node) 13 | write_attr :node, node 14 | end 15 | end # PubSubErrors 16 | 17 | end # Stanza 18 | end # Blather 19 | -------------------------------------------------------------------------------- /examples/rosterprint.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Prints out each roster entry 4 | 5 | require 'rubygems' 6 | require 'blather/client' 7 | 8 | when_ready do 9 | my_roster.grouped.each do |group, items| 10 | puts "#{'*'*3} #{group || 'Ungrouped'} #{'*'*3}" 11 | items.each { |item| puts "- #{item.name} (#{item.jid})" } 12 | puts 13 | end 14 | shutdown 15 | end 16 | -------------------------------------------------------------------------------- /examples/echo.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | require 'blather/client' 5 | 6 | when_ready { puts "Connected ! send messages to #{jid.stripped}." } 7 | 8 | subscription :request? do |s| 9 | write_to_stream s.approve! 10 | end 11 | 12 | message :chat?, :body => 'exit' do |m| 13 | say m.from, 'Exiting ...' 14 | shutdown 15 | end 16 | 17 | message :chat?, :body do |m| 18 | say m.from, "You sent: #{m.body}" 19 | end 20 | -------------------------------------------------------------------------------- /lib/blather/core_ext/ipaddr.rb: -------------------------------------------------------------------------------- 1 | # @private 2 | class IPAddr 3 | PrivateRanges = [ 4 | IPAddr.new("10.0.0.0/8"), 5 | IPAddr.new("172.16.0.0/12"), 6 | IPAddr.new("192.168.0.0/16") 7 | ] 8 | 9 | def private? 10 | return false unless self.ipv4? 11 | PrivateRanges.each do |ipr| 12 | return true if ipr.include?(self) 13 | end 14 | return false 15 | end 16 | 17 | def public? 18 | !private? 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /examples/trusted_echo.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | require 'blather/client' 5 | 6 | setup 'chris@vines.local', 'test', 'vines.local', 5222, "./certs" 7 | 8 | when_ready { puts "Connected ! send messages to #{jid.stripped}." } 9 | 10 | subscription :request? do |s| 11 | write_to_stream s.approve! 12 | end 13 | 14 | message :chat?, :body => 'exit' do |m| 15 | say m.from, 'Exiting ...' 16 | shutdown 17 | end 18 | 19 | message :chat?, :body do |m| 20 | say m.from, "You sent: #{m.body}" 21 | end 22 | -------------------------------------------------------------------------------- /examples/MUC_echo.rb: -------------------------------------------------------------------------------- 1 | require 'blather/client/dsl' 2 | 3 | module MUC 4 | extend Blather::DSL 5 | when_ready do 6 | puts "Connected ! send messages to #{jid.stripped}." 7 | join 'room_name', 'nick_name' 8 | end 9 | 10 | message :groupchat?, :body, proc { |m| m.from != jid.stripped }, delay: nil do |m| 11 | echo = Blather::Stanza::Message.new 12 | echo.to = room 13 | echo.body = m.body 14 | echo.type = 'groupchat' 15 | client.write echo 16 | end 17 | end 18 | MUC.setup 'username', 'password' 19 | EM.run { MUC.run } 20 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | ENV['RUBY_FLAGS'] = "-I#{%w(lib ext bin spec).join(File::PATH_SEPARATOR)}" 3 | 4 | require 'rubygems' 5 | require 'bundler/gem_tasks' 6 | require 'bundler/setup' 7 | 8 | task :default => :spec 9 | task :test => :spec 10 | 11 | require 'rspec/core/rake_task' 12 | RSpec::Core::RakeTask.new 13 | 14 | require 'yard' 15 | YARD::Tags::Library.define_tag 'Blather handler', :handler, :with_name 16 | YARD::Templates::Engine.register_template_path 'yard/templates' 17 | 18 | YARD::Rake::YardocTask.new(:doc) do |t| 19 | t.options = ['--no-private', '-m', 'markdown', '-o', './doc/public/yard'] 20 | end 21 | -------------------------------------------------------------------------------- /yard/templates/default/class/html/handlers.erb: -------------------------------------------------------------------------------- 1 | 15 |

16 | Handler Stack: 17 | [<%= @handler_stack * ', ' %>] 18 |

19 | -------------------------------------------------------------------------------- /examples/stream_only.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | require 'blather' 5 | 6 | trap(:INT) { EM.stop } 7 | trap(:TERM) { EM.stop } 8 | EM.run do 9 | Blather::Stream::Client.start(Class.new { 10 | attr_accessor :jid 11 | 12 | def post_init(stream, jid = nil) 13 | @stream = stream 14 | self.jid = jid 15 | 16 | @stream.send_data Blather::Stanza::Presence::Status.new 17 | puts "Stream started!" 18 | end 19 | 20 | def receive_data(stanza) 21 | @stream.send_data stanza.reply! 22 | end 23 | 24 | def unbind 25 | puts "Stream ended!" 26 | end 27 | }.new, 'echo@jabber.local', 'echo') 28 | end 29 | -------------------------------------------------------------------------------- /lib/blather/stream/features/tls.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stream 3 | 4 | # @private 5 | class TLS < Features 6 | class TLSFailure < BlatherError 7 | register :tls_failure 8 | end 9 | 10 | TLS_NS = 'urn:ietf:params:xml:ns:xmpp-tls'.freeze 11 | register TLS_NS 12 | 13 | def receive_data(stanza) 14 | case stanza.element_name 15 | when 'starttls' 16 | @stream.send "" 17 | when 'proceed' 18 | @stream.start_tls(:verify_peer => true) 19 | @stream.start 20 | else 21 | fail! TLSFailure.new 22 | end 23 | end 24 | 25 | end #TLS 26 | 27 | end #Stream 28 | end #Blather 29 | -------------------------------------------------------------------------------- /lib/blather/stream/client.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stream 3 | 4 | # @private 5 | class Client < Stream 6 | LANG = 'en' 7 | VERSION = '1.0' 8 | NAMESPACE = 'jabber:client' 9 | 10 | def start 11 | @parser = Parser.new self 12 | send "" 13 | end 14 | 15 | def send(stanza) 16 | stanza.from = self.jid if stanza.is_a?(Stanza) && !stanza.from.nil? 17 | super stanza 18 | end 19 | 20 | def cleanup 21 | @parser.finish if @parser 22 | super 23 | end 24 | end #Client 25 | 26 | end #Stream 27 | end #Blather 28 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'blather' 2 | require 'countdownlatch' 3 | 4 | Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each {|f| require f} 5 | 6 | RSpec.configure do |config| 7 | config.mock_with :mocha 8 | config.filter_run :focus => true 9 | config.run_all_when_everything_filtered = true 10 | 11 | config.before(:each) do 12 | EM.stubs(:schedule).yields 13 | EM.stubs(:defer).yields 14 | Blather::Stream::Parser.debug = true 15 | Blather.logger = Logger.new($stdout).tap { |logger| logger.level = Logger::DEBUG } 16 | end 17 | end 18 | 19 | def parse_stanza(xml) 20 | Nokogiri::XML.parse xml 21 | end 22 | 23 | def jruby? 24 | RUBY_PLATFORM =~ /java/ 25 | end 26 | -------------------------------------------------------------------------------- /spec/blather/stream/ssl_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Blather::CertStore do 4 | let(:cert_dir) { File.expand_path '../../fixtures', File.dirname(__FILE__) } 5 | let(:cert_path) { File.join cert_dir, 'certificate.crt' } 6 | let(:cert) { File.read cert_path } 7 | 8 | subject do 9 | Blather::CertStore.new cert_dir 10 | end 11 | 12 | it 'can verify valid cert' do 13 | expect(subject.trusted?(cert)).to be true 14 | end 15 | 16 | it 'can verify invalid cert' do 17 | expect(subject.trusted?('foo bar baz')).to be_nil 18 | end 19 | 20 | it 'cannot verify when the cert authority is not trusted' do 21 | @store = Blather::CertStore.new("../") 22 | expect(@store.trusted?(cert)).to be false 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/blather/stanza/disco.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stanza 3 | 4 | # # Disco Base class 5 | # 6 | # Use Blather::Stanza::DiscoInfo or Blather::Stanza::DiscoItems 7 | class Disco < Iq::Query 8 | 9 | # Get the name of the node 10 | # 11 | # @return [String] the node name 12 | def node 13 | query[:node] 14 | end 15 | 16 | # Set the name of the node 17 | # 18 | # @param [#to_s] node the new node name 19 | def node=(node) 20 | query[:node] = node 21 | end 22 | 23 | # Compare two Disco objects by name, type and category 24 | # @param [Disco] o the Identity object to compare against 25 | # @return [true, false] 26 | def eql?(o, *fields) 27 | super o, *(fields + [:node]) 28 | end 29 | end 30 | 31 | end # Stanza 32 | end # Blather 33 | -------------------------------------------------------------------------------- /lib/blather/stanza/presence/muc.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stanza 3 | class Presence 4 | 5 | class MUC < Status 6 | register :muc_join, :x, "http://jabber.org/protocol/muc" 7 | 8 | def self.new(*args) 9 | new_node = super 10 | new_node.muc 11 | new_node 12 | end 13 | 14 | module InstanceMethods 15 | def inherit(node) 16 | muc.remove 17 | super 18 | self 19 | end 20 | 21 | def muc 22 | unless muc = find_first('ns:x', :ns => MUC.registered_ns) 23 | self << (muc = XMPPNode.new('x', self.document)) 24 | muc.namespace = self.class.registered_ns 25 | end 26 | muc 27 | end 28 | end 29 | 30 | include InstanceMethods 31 | end # MUC 32 | 33 | end # Presence 34 | end # Stanza 35 | end # Blather 36 | -------------------------------------------------------------------------------- /spec/fixtures/certificate.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICAzCCAWwCCQDvLJRwh2NPnjANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJH 3 | QjETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0 4 | cyBQdHkgTHRkMCAXDTEyMTIyMDAxMjgyOVoYDzI4MzQwNTA1MDEyODI5WjBFMQsw 5 | CQYDVQQGEwJHQjETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJu 6 | ZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDj 7 | EoRb/MYNjHrxq6cu6TVyM4hcimy5WWy369BvarONxpmHlrvDUiMQoZNBxyV//hZH 8 | Y3JSHrNxMCxU1vShghNZzNEZg45UBWMs3LS/IHnFvf+4gYbffEJCGMhjyx4zAxRA 9 | gegDAYIeGHtWAA6SIe4SIzsMtAJC0Nf8H3AZJdjvSwIDAQABMA0GCSqGSIb3DQEB 10 | BQUAA4GBAGkyf9MlpCesU7jxiZbI3jIdCEMiG5XsGp0c+nELufcsus4MySN0xZTt 11 | zvBBvWLpbv8zkxkw+kamvksObslx4G6hg1RvKP/Zdjue5mVnUTNKlXu/wQOTVPNo 12 | V3iBcJUwe3uIIlt2szA6o5BRt29XSLiNIJvFhAHCVg+3VioXChRr 13 | -----END CERTIFICATE----- 14 | -------------------------------------------------------------------------------- /examples/ping_pong.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'blather/client/dsl' 3 | $stdout.sync = true 4 | 5 | module Ping 6 | extend Blather::DSL 7 | def self.run; client.run; end 8 | 9 | setup 'ping@your.jabber.server', 'password' 10 | 11 | status :from => /pong@your\.jabber\.server/ do |s| 12 | puts "serve!" 13 | say s.from, 'ping' 14 | end 15 | 16 | message :chat?, :body => 'pong' do |m| 17 | puts "ping!" 18 | say m.from, 'ping' 19 | end 20 | end 21 | 22 | module Pong 23 | extend Blather::DSL 24 | def self.run; client.run; end 25 | 26 | setup 'pong@your.jabber.server', 'password' 27 | message :chat?, :body => 'ping' do |m| 28 | puts "pong!" 29 | say m.from, 'pong' 30 | end 31 | end 32 | 33 | trap(:INT) { EM.stop } 34 | trap(:TERM) { EM.stop } 35 | EM.run do 36 | Ping.run 37 | Pong.run 38 | end 39 | -------------------------------------------------------------------------------- /spec/blather_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Blather do 4 | 5 | describe "while accessing to Logger object" do 6 | it "should return a Logger instance" do 7 | expect(Blather.logger).to be_instance_of Logger 8 | end 9 | end 10 | 11 | describe "while using the log method" do 12 | after do 13 | Blather.default_log_level = :debug 14 | end 15 | 16 | it "should forward to debug by default" do 17 | Blather.logger.expects(:debug).with("foo bar").once 18 | Blather.log "foo bar" 19 | end 20 | 21 | %w.each do |val| 22 | it "should forward to #{val} if configured that default level" do 23 | Blather.logger.expects(val.to_sym).with("foo bar").once 24 | Blather.default_log_level = val.to_sym 25 | Blather.log "foo bar" 26 | end 27 | end 28 | 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /examples/xmpp4r/echo.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | # This bot will reply to every message it receives. To end the game, send 'exit' 4 | 5 | require 'rubygems' 6 | require 'xmpp4r/client' 7 | include Jabber 8 | 9 | # settings 10 | if ARGV.length != 2 11 | puts "Run with ./echo_thread.rb user@server/resource password" 12 | exit 1 13 | end 14 | myJID = JID.new(ARGV[0]) 15 | myPassword = ARGV[1] 16 | cl = Client.new(myJID) 17 | cl.connect 18 | cl.auth(myPassword) 19 | cl.send(Presence.new) 20 | puts "Connected ! send messages to #{myJID.strip.to_s}." 21 | mainthread = Thread.current 22 | cl.add_message_callback do |m| 23 | if m.type != :error 24 | m2 = Message.new(m.from, "You sent: #{m.body}") 25 | m2.type = m.type 26 | cl.send(m2) 27 | if m.body == 'exit' 28 | m2 = Message.new(m.from, "Exiting ...") 29 | m2.type = m.type 30 | cl.send(m2) 31 | mainthread.wakeup 32 | end 33 | end 34 | end 35 | Thread.stop 36 | cl.close 37 | -------------------------------------------------------------------------------- /lib/blather/stream/component.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stream 3 | 4 | # @private 5 | class Component < Stream 6 | NAMESPACE = 'jabber:component:accept' 7 | 8 | def receive(node) # :nodoc: 9 | if node.element_name == 'handshake' 10 | ready! 11 | else 12 | super 13 | end 14 | 15 | if node.document.find_first('/stream:stream[not(stream:error)]', :xmlns => NAMESPACE, :stream => STREAM_NS) 16 | send "#{Digest::SHA1.hexdigest(node['id']+@password)}" 17 | end 18 | end 19 | 20 | def send(stanza) 21 | stanza.from ||= self.jid if stanza.respond_to?(:from) && stanza.respond_to?(:from=) 22 | super stanza 23 | end 24 | 25 | def start 26 | @parser = Parser.new self 27 | send "" 28 | end 29 | 30 | def cleanup 31 | @parser.finish if @parser 32 | super 33 | end 34 | end #Client 35 | 36 | end #Stream 37 | end #Blather 38 | -------------------------------------------------------------------------------- /lib/blather/errors/sasl_error.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | 3 | # General SASL Errors 4 | # Check #name for the error name 5 | # 6 | # @handler :sasl_error 7 | class SASLError < BlatherError 8 | # @private 9 | SASL_ERR_NS = 'urn:ietf:params:xml:ns:xmpp-sasl' 10 | 11 | class_attribute :err_name 12 | # @private 13 | @@registrations = {} 14 | 15 | register :sasl_error 16 | 17 | # Import the stanza 18 | # 19 | # @param [Blather::XMPPNode] node the error node 20 | # @return [Blather::SASLError] 21 | def self.import(node) 22 | self.new node 23 | end 24 | 25 | # Create a new SASLError 26 | # 27 | # @param [Blather::XMPPNode] node the error node 28 | def initialize(node) 29 | super() 30 | @node = node 31 | end 32 | 33 | # The actual error name 34 | # 35 | # @return [Symbol] a symbol representing the error name 36 | def name 37 | if @node 38 | name = @node.find_first('ns:*', :ns => SASL_ERR_NS).element_name 39 | name.gsub('-', '_').to_sym 40 | end 41 | end 42 | end # SASLError 43 | 44 | end # Blather 45 | -------------------------------------------------------------------------------- /spec/blather/errors/sasl_error_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | def sasl_error_node(err_name = 'aborted') 4 | node = Blather::XMPPNode.new 'failure' 5 | node.namespace = Blather::SASLError::SASL_ERR_NS 6 | 7 | node << Blather::XMPPNode.new(err_name, node.document) 8 | node 9 | end 10 | 11 | describe Blather::SASLError do 12 | it 'can import a node' do 13 | expect(Blather::SASLError).to respond_to :import 14 | e = Blather::SASLError.import sasl_error_node 15 | expect(e).to be_kind_of Blather::SASLError 16 | end 17 | 18 | describe 'each XMPP SASL error type' do 19 | %w[ aborted 20 | incorrect-encoding 21 | invalid-authzid 22 | invalid-mechanism 23 | mechanism-too-weak 24 | not-authorized 25 | temporary-auth-failure 26 | ].each do |error_type| 27 | it "handles the name for #{error_type}" do 28 | e = Blather::SASLError.import sasl_error_node(error_type) 29 | expect(e.name).to eq(error_type.gsub('-','_').to_sym) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/blather/errors_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Blather::BlatherError do 4 | it 'is handled by :error' do 5 | expect(Blather::BlatherError.new.handler_hierarchy).to eq([:error]) 6 | end 7 | end 8 | 9 | describe 'Blather::ParseError' do 10 | before { @error = Blather::ParseError.new('"') } 11 | 12 | it 'is registers with the handler hierarchy' do 13 | expect(@error.handler_hierarchy).to eq([:parse_error, :error]) 14 | end 15 | 16 | it 'contains the error message' do 17 | expect(@error).to respond_to :message 18 | expect(@error.message).to eq('"') 19 | end 20 | end 21 | 22 | describe 'Blather::UnknownResponse' do 23 | before { @error = Blather::UnknownResponse.new(Blather::XMPPNode.new('foo-bar')) } 24 | 25 | it 'is registers with the handler hierarchy' do 26 | expect(@error.handler_hierarchy).to eq([:unknown_response_error, :error]) 27 | end 28 | 29 | it 'holds on to a copy of the failure node' do 30 | expect(@error).to respond_to :node 31 | expect(@error.node.element_name).to eq('foo-bar') 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/blather/stream/features/session.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stream 3 | 4 | # @private 5 | class Session < Features 6 | SESSION_NS = 'urn:ietf:params:xml:ns:xmpp-session'.freeze 7 | register SESSION_NS 8 | 9 | def initialize(stream, succeed, fail) 10 | super 11 | @to = @stream.jid.domain 12 | end 13 | 14 | def receive_data(stanza) 15 | @node = stanza 16 | case stanza.element_name 17 | when 'session' then session 18 | when 'iq' then check_response 19 | else fail!(UnknownResponse.new(stanza)) 20 | end 21 | end 22 | 23 | private 24 | def check_response 25 | if @node[:type] == 'result' 26 | succeed! 27 | else 28 | fail!(StanzaError.import(@node)) 29 | end 30 | end 31 | 32 | ## 33 | # Send a start session command 34 | def session 35 | response = Stanza::Iq.new :set 36 | response.to = @to 37 | response << (sess = XMPPNode.new('session', response.document)) 38 | sess.namespace = SESSION_NS 39 | 40 | @stream.send response 41 | end 42 | end 43 | 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Blather 2 | 3 | Copyright (c) 2012 Adhearsion Foundation Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/blather/stream/features/register.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stream 3 | class Register < Features 4 | REGISTER_NS = "http://jabber.org/features/iq-register".freeze 5 | 6 | register REGISTER_NS 7 | 8 | def initialize(stream, succeed, fail) 9 | super 10 | @jid = @stream.jid 11 | @pass = @stream.password 12 | end 13 | 14 | def receive_data(stanza) 15 | error_node = stanza.xpath("//error").first 16 | 17 | if error_node 18 | fail!(BlatherError.new(stanza)) 19 | elsif stanza['type'] == 'result' && (stanza.content.empty? || stanza.children.find { |v| v.element_name == "query" }) 20 | succeed! 21 | else 22 | @stream.send register_query 23 | end 24 | end 25 | 26 | def register_query 27 | node = Blather::Stanza::Iq::Query.new(:set) 28 | query_node = node.xpath('//query').first 29 | query_node['xmlns'] = 'jabber:iq:register' 30 | Nokogiri::XML::Builder.with(query_node) do |xml| 31 | xml.username @jid.node 32 | xml.password @pass 33 | end 34 | node 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /examples/certs/README: -------------------------------------------------------------------------------- 1 | The certs/ directory contains the TLS certificates required for encrypting 2 | client to server XMPP connections. 3 | 4 | The ca-bundle.crt file contains root Certificate Authority (CA) certificates. 5 | These are used to validate certificates presented during TLS handshake 6 | negotiation. The source for this file is the cacert.pem file available 7 | at http://curl.haxx.se/docs/caextract.html. 8 | 9 | Any self-signed CA certificate placed in this directory will be considered 10 | a trusted certificate. For example, let's say you're running the wonderland.lit 11 | XMPP server and would like to and verify the ssl cert during TLS. 12 | The wonderland.lit server hasn't purchased a legitimate TLS certificate 13 | from a CA known in ca-bundle.crt. Instead, they've created a self-signed 14 | certificate and sent it to you. Place the certificate in this directory 15 | with a name of wonderland.lit.crt and it will be trusted. Trusted TLS connections 16 | from wonderland.lit will now work. 17 | 18 | Alternatively, you can purchase a TLS certificate from a CA (e.g. Go Daddy, 19 | VeriSign, etc.) and place it in this directory. This will avoid the hassles 20 | of managing self-signed certificates. 21 | -------------------------------------------------------------------------------- /spec/blather/stanza/pubsub_owner_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'fixtures/pubsub' 3 | 4 | describe Blather::Stanza::PubSubOwner do 5 | it 'registers itself' do 6 | expect(Blather::XMPPNode.class_from_registration(:pubsub, 'http://jabber.org/protocol/pubsub#owner')).to eq(Blather::Stanza::PubSubOwner) 7 | end 8 | 9 | it 'ensures a pubusb node is present on create' do 10 | pubsub = Blather::Stanza::PubSubOwner.new 11 | expect(pubsub.find_first('/iq/ns:pubsub', :ns => Blather::Stanza::PubSubOwner.registered_ns)).not_to be_nil 12 | end 13 | 14 | it 'ensures a pubsub node exists when calling #pubsub' do 15 | pubsub = Blather::Stanza::PubSubOwner.new 16 | pubsub.remove_children :pubsub 17 | expect(pubsub.find_first('/iq/ns:pubsub', :ns => Blather::Stanza::PubSubOwner.registered_ns)).to be_nil 18 | 19 | expect(pubsub.pubsub).not_to be_nil 20 | expect(pubsub.find_first('/iq/ns:pubsub', :ns => Blather::Stanza::PubSubOwner.registered_ns)).not_to be_nil 21 | end 22 | 23 | it 'sets the host if requested' do 24 | aff = Blather::Stanza::PubSubOwner.new :get, 'pubsub.jabber.local' 25 | expect(aff.to).to eq(Blather::JID.new('pubsub.jabber.local')) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/blather/stanza/muc/muc_user_base.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stanza 3 | class MUC 4 | 5 | module MUCUserBase 6 | MUC_USER_NAMESPACE = "http://jabber.org/protocol/muc#user" 7 | 8 | def self.included(klass) 9 | klass.extend ClassMethods 10 | end 11 | 12 | module ClassMethods 13 | def new(*args) 14 | super.tap { |e| e.muc_user } 15 | end 16 | end 17 | 18 | def inherit(node) 19 | muc_user.remove 20 | super 21 | self 22 | end 23 | 24 | def password 25 | find_password_node && password_node.content 26 | end 27 | 28 | def password=(var) 29 | password_node.content = var 30 | end 31 | 32 | def muc_user 33 | unless muc_user = find_first('ns:x', :ns => MUC_USER_NAMESPACE) 34 | self << (muc_user = XMPPNode.new('x', self.document)) 35 | muc_user.namespace = self.class.registered_ns 36 | end 37 | muc_user 38 | end 39 | 40 | def password_node 41 | unless pw = find_password_node 42 | muc_user << (pw = XMPPNode.new('password', self.document)) 43 | end 44 | pw 45 | end 46 | 47 | def find_password_node 48 | muc_user.find_first 'ns:password', :ns => MUC_USER_NAMESPACE 49 | end 50 | end # MUCUserBase 51 | 52 | end # MUC 53 | end # Stanza 54 | end # Blather 55 | -------------------------------------------------------------------------------- /lib/blather/stanza/iq/ping.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stanza 3 | class Iq 4 | 5 | # # Ping Stanza 6 | # 7 | # [XEP-0199: XMPP Ping](http://xmpp.org/extensions/xep-0199.html) 8 | # 9 | # This is a base class for any Ping based Iq stanzas. 10 | # 11 | # @handler :ping 12 | class Ping < Iq 13 | # @private 14 | register :ping, :ping, 'urn:xmpp:ping' 15 | 16 | # Overrides the parent method to ensure a ping node is created 17 | # 18 | # @see Blather::Stanza::Iq.new 19 | def self.new(type = :get, to = nil, id = nil) 20 | node = super 21 | node.ping 22 | node 23 | end 24 | 25 | # Overrides the parent method to ensure the current ping node is destroyed 26 | # 27 | # @see Blather::Stanza::Iq#inherit 28 | def inherit(node) 29 | ping.remove 30 | super 31 | end 32 | 33 | # Ping node accessor 34 | # If a ping node exists it will be returned. 35 | # Otherwise a new node will be created and returned 36 | # 37 | # @return [Balather::XMPPNode] 38 | def ping 39 | p = find_first 'ns:ping', :ns => self.class.registered_ns 40 | 41 | unless p 42 | (self << (p = XMPPNode.new('ping', self.document))) 43 | p.namespace = self.class.registered_ns 44 | end 45 | p 46 | end 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/blather/stanza/iq/query.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stanza 3 | class Iq 4 | 5 | # # Query Stanza 6 | # 7 | # This is a base class for any query based Iq stanzas. It provides a base set 8 | # of methods for working with query stanzas 9 | # 10 | # @handler :query 11 | class Query < Iq 12 | register :query, :query 13 | 14 | # Overrides the parent method to ensure a query node is created 15 | # 16 | # @see Blather::Stanza::Iq.new 17 | def self.new(*) 18 | node = super 19 | node.query 20 | node 21 | end 22 | 23 | # Overrides the parent method to ensure the current query node is destroyed 24 | # 25 | # @see Blather::Stanza::Iq#inherit 26 | def inherit(node) 27 | query.remove 28 | super 29 | end 30 | 31 | # Query node accessor 32 | # If a query node exists it will be returned. 33 | # Otherwise a new node will be created and returned 34 | # 35 | # @return [Balather::XMPPNode] 36 | def query 37 | q = if self.class.registered_ns 38 | find_first('query_ns:query', :query_ns => self.class.registered_ns) 39 | else 40 | find_first('query') 41 | end 42 | 43 | unless q 44 | (self << (q = XMPPNode.new('query', self.document))) 45 | q.namespace = self.class.registered_ns 46 | end 47 | q 48 | end 49 | end #Query 50 | 51 | end #Iq 52 | end #Stanza 53 | end 54 | -------------------------------------------------------------------------------- /spec/blather/stanza/presence/muc_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | def muc_xml 4 | <<-XML 5 | 8 | 9 | 10 | XML 11 | end 12 | 13 | describe 'Blather::Stanza::Presence::MUC' do 14 | it 'registers itself' do 15 | expect(Blather::XMPPNode.class_from_registration(:x, 'http://jabber.org/protocol/muc' )).to eq(Blather::Stanza::Presence::MUC) 16 | end 17 | 18 | it 'must be importable' do 19 | c = Blather::XMPPNode.parse(muc_xml) 20 | expect(c).to be_kind_of Blather::Stanza::Presence::MUC::InstanceMethods 21 | expect(c.xpath('ns:x', :ns => Blather::Stanza::Presence::MUC.registered_ns).count).to eq(1) 22 | end 23 | 24 | it 'ensures a form node is present on create' do 25 | c = Blather::Stanza::Presence::MUC.new 26 | expect(c.xpath('ns:x', :ns => Blather::Stanza::Presence::MUC.registered_ns)).not_to be_empty 27 | end 28 | 29 | it 'ensures a form node exists when calling #muc' do 30 | c = Blather::Stanza::Presence::MUC.new 31 | c.remove_children :x 32 | expect(c.xpath('ns:x', :ns => Blather::Stanza::Presence::MUC.registered_ns)).to be_empty 33 | 34 | expect(c.muc).not_to be_nil 35 | expect(c.xpath('ns:x', :ns => Blather::Stanza::Presence::MUC.registered_ns)).not_to be_empty 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/blather/stanza/iq/ping_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | def ping_xml 4 | <<-XML 5 | 6 | 7 | 8 | XML 9 | end 10 | 11 | describe Blather::Stanza::Iq::Ping do 12 | it 'registers itself' do 13 | expect(Blather::XMPPNode.class_from_registration(:ping, 'urn:xmpp:ping')).to eq(Blather::Stanza::Iq::Ping) 14 | end 15 | 16 | it 'can be imported' do 17 | node = Blather::XMPPNode.parse ping_xml 18 | expect(node).to be_instance_of Blather::Stanza::Iq::Ping 19 | end 20 | 21 | it 'ensures a ping node is present on create' do 22 | iq = Blather::Stanza::Iq::Ping.new 23 | expect(iq.xpath('ns:ping', :ns => 'urn:xmpp:ping')).not_to be_empty 24 | end 25 | 26 | it 'ensures a ping node exists when calling #ping' do 27 | iq = Blather::Stanza::Iq::Ping.new 28 | iq.ping.remove 29 | expect(iq.xpath('ns:ping', :ns => 'urn:xmpp:ping')).to be_empty 30 | 31 | expect(iq.ping).not_to be_nil 32 | expect(iq.xpath('ns:ping', :ns => 'urn:xmpp:ping')).not_to be_empty 33 | end 34 | 35 | it 'responds with an empty IQ' do 36 | ping = Blather::Stanza::Iq::Ping.new :get, 'one@example.com', 'abc123' 37 | ping.from = 'two@example.com' 38 | expected_pong = Blather::Stanza::Iq::Ping.new(:result, 'two@example.com', 'abc123').tap do |pong| 39 | pong.from = 'one@example.com' 40 | end 41 | reply = ping.reply 42 | expect(reply).to eq(expected_pong) 43 | expect(reply.children.count).to eq(0) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/blather/stanza/pubsub_owner/purge.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stanza 3 | class PubSubOwner 4 | 5 | # # PubSubOwner Purge Stanza 6 | # 7 | # [XEP-0060 Section 8.5 - Purge All Node Items](http://xmpp.org/extensions/xep-0060.html#owner-purge) 8 | # 9 | # @handler :pubsub_purge 10 | class Purge < PubSubOwner 11 | register :pubsub_purge, :purge, self.registered_ns 12 | 13 | # Create a new purge stanza 14 | # 15 | # @param [Blather::Stanza::Iq::VALID_TYPES] type the IQ stanza type 16 | # @param [String] host the host to send the request to 17 | # @param [String] node the name of the node to purge 18 | def self.new(type = :set, host = nil, node = nil) 19 | new_node = super(type, host) 20 | new_node.node = node 21 | new_node 22 | end 23 | 24 | # Get the name of the node to delete 25 | # 26 | # @return [String] 27 | def node 28 | purge_node[:node] 29 | end 30 | 31 | # Set the name of the node to delete 32 | # 33 | # @param [String] node 34 | def node=(node) 35 | purge_node[:node] = node 36 | end 37 | 38 | # Get or create the actual purge node on the stanza 39 | # 40 | # @return [Blather::XMPPNode] 41 | def purge_node 42 | unless purge_node = pubsub.find_first('ns:purge', :ns => self.class.registered_ns) 43 | self.pubsub << (purge_node = XMPPNode.new('purge', self.document)) 44 | purge_node.namespace = self.pubsub.namespace 45 | end 46 | purge_node 47 | end 48 | end # Retract 49 | 50 | end # PubSub 51 | end # Stanza 52 | end # Blather 53 | -------------------------------------------------------------------------------- /lib/blather/stanza/pubsub_owner/delete.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stanza 3 | class PubSubOwner 4 | 5 | # # PubSubOwner Delete Stanza 6 | # 7 | # [XEP-0060 Section 8.4 Delete a Node](http://xmpp.org/extensions/xep-0060.html#owner-delete) 8 | # 9 | # @handler :pubsub_delete 10 | class Delete < PubSubOwner 11 | register :pubsub_delete, :delete, self.registered_ns 12 | 13 | # Create a new delete stanza 14 | # 15 | # @param [Blather::Stanza::Iq::VALID_TYPES] type the IQ stanza type 16 | # @param [String] host the host to send the request to 17 | # @param [String] node the name of the node to delete 18 | def self.new(type = :set, host = nil, node = nil) 19 | new_node = super(type, host) 20 | new_node.node = node 21 | new_node 22 | end 23 | 24 | # Get the name of the node to delete 25 | # 26 | # @return [String] 27 | def node 28 | delete_node[:node] 29 | end 30 | 31 | # Set the name of the node to delete 32 | # 33 | # @param [String] node 34 | def node=(node) 35 | delete_node[:node] = node 36 | end 37 | 38 | # Get or create the actual delete node on the stanza 39 | # 40 | # @return [Blather::XMPPNode] 41 | def delete_node 42 | unless delete_node = pubsub.find_first('ns:delete', :ns => self.class.registered_ns) 43 | self.pubsub << (delete_node = XMPPNode.new('delete', self.document)) 44 | delete_node.namespace = self.pubsub.namespace 45 | end 46 | delete_node 47 | end 48 | end # Retract 49 | 50 | end # PubSub 51 | end # Stanza 52 | end # Blather 53 | -------------------------------------------------------------------------------- /lib/blather/stanza/pubsub_owner.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stanza 3 | 4 | # # PubSubOwner Base Class 5 | # 6 | # [XEP-0060 - Publish-Subscribe](http://xmpp.org/extensions/xep-0060.html) 7 | # 8 | # @handler :pubsub_owner 9 | class PubSubOwner < Iq 10 | register :pubsub_owner, :pubsub, 'http://jabber.org/protocol/pubsub#owner' 11 | 12 | # Creates the proper class from the stana's child 13 | # @private 14 | def self.import(node) 15 | klass = nil 16 | if pubsub = node.document.find_first('//ns:pubsub', :ns => self.registered_ns) 17 | pubsub.children.each { |e| break if klass = class_from_registration(e.element_name, (e.namespace.href if e.namespace)) } 18 | end 19 | (klass || self).new(node[:type]).inherit(node) 20 | end 21 | 22 | # Overrides the parent to ensure a pubsub node is created 23 | # @private 24 | def self.new(type = nil, host = nil) 25 | new_node = super type 26 | new_node.to = host 27 | new_node.pubsub 28 | new_node 29 | end 30 | 31 | # Overrides the parent to ensure the pubsub node is destroyed 32 | # @private 33 | def inherit(node) 34 | remove_children :pubsub 35 | super 36 | end 37 | 38 | # Get or create the pubsub node on the stanza 39 | # 40 | # @return [Blather::XMPPNode] 41 | def pubsub 42 | unless p = find_first('ns:pubsub', :ns => self.class.registered_ns) 43 | self << (p = XMPPNode.new('pubsub', self.document)) 44 | p.namespace = self.class.registered_ns 45 | end 46 | p 47 | end 48 | end # PubSubOwner 49 | 50 | end # Stanza 51 | end # Blather 52 | -------------------------------------------------------------------------------- /spec/blather/xmpp_node_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Blather::XMPPNode do 4 | before { @doc = Nokogiri::XML::Document.new } 5 | 6 | it 'generates a node based on the registered_name' do 7 | foo = Class.new(Blather::XMPPNode) 8 | foo.registered_name = 'foo' 9 | expect(foo.new.element_name).to eq('foo') 10 | end 11 | 12 | it 'sets the namespace on creation' do 13 | foo = Class.new(Blather::XMPPNode) 14 | foo.registered_ns = 'foo' 15 | expect(foo.new('foo').namespace.href).to eq('foo') 16 | end 17 | 18 | it 'registers sub classes' do 19 | class RegistersSubClass < Blather::XMPPNode; register 'foo', 'foo:bar'; end 20 | expect(RegistersSubClass.registered_name).to eq('foo') 21 | expect(RegistersSubClass.registered_ns).to eq('foo:bar') 22 | expect(Blather::XMPPNode.class_from_registration('foo', 'foo:bar')).to eq(RegistersSubClass) 23 | end 24 | 25 | it 'imports another node' do 26 | class ImportSubClass < Blather::XMPPNode; register 'foo', 'foo:bar'; end 27 | n = Blather::XMPPNode.new('foo') 28 | n.namespace = 'foo:bar' 29 | expect(Blather::XMPPNode.import(n)).to be_kind_of ImportSubClass 30 | end 31 | 32 | it 'can convert itself into a stanza' do 33 | class StanzaConvert < Blather::XMPPNode; register 'foo'; end 34 | n = Blather::XMPPNode.new('foo') 35 | expect(n.to_stanza).to be_kind_of StanzaConvert 36 | end 37 | 38 | it 'can parse a string and import it' do 39 | class StanzaParse < Blather::XMPPNode; register 'foo'; end 40 | string = '' 41 | n = Nokogiri::XML(string).root 42 | i = Blather::XMPPNode.import n 43 | expect(i).to be_kind_of StanzaParse 44 | p = Blather::XMPPNode.parse string 45 | expect(p).to be_kind_of StanzaParse 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/blather/stream/features/resource.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stream 3 | 4 | # @private 5 | class Resource < Features 6 | BIND_NS = 'urn:ietf:params:xml:ns:xmpp-bind'.freeze 7 | register BIND_NS 8 | 9 | def initialize(stream, succeed, fail) 10 | super 11 | @jid = stream.jid 12 | end 13 | 14 | def receive_data(stanza) 15 | @node = stanza 16 | case stanza.element_name 17 | when 'bind' then bind 18 | when 'iq' then result 19 | else fail!(UnknownResponse.new(@node)) 20 | end 21 | end 22 | 23 | private 24 | ## 25 | # Respond to the bind request 26 | # If @jid has a resource set already request it from the server 27 | def bind 28 | response = Stanza::Iq.new :set 29 | @id = response.id 30 | 31 | response << (binder = XMPPNode.new('bind', response.document)) 32 | binder.namespace = BIND_NS 33 | 34 | if @jid.resource 35 | binder << (resource = XMPPNode.new('resource', binder.document)) 36 | resource.content = @jid.resource 37 | end 38 | 39 | @stream.send response 40 | end 41 | 42 | ## 43 | # Process the result from the server 44 | # Sets the sends the JID (now bound to a resource) 45 | # back to the stream 46 | def result 47 | if @node[:type] == 'error' 48 | fail! StanzaError.import(@node) 49 | return 50 | end 51 | 52 | # ensure this is a response to our original request 53 | if @id == @node['id'] 54 | @stream.jid = JID.new @node.find_first('bind_ns:bind/bind_ns:jid', :bind_ns => BIND_NS).content 55 | succeed! 56 | else 57 | fail!("BIND result ID mismatch. Expected: #{@id}. Received: #{@node['id']}") 58 | end 59 | end 60 | end #Resource 61 | 62 | end #Stream 63 | end #Blather 64 | -------------------------------------------------------------------------------- /blather.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "blather/version" 4 | 5 | module RubyVersion 6 | def rbx? 7 | defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx' 8 | end 9 | 10 | def jruby? 11 | RUBY_PLATFORM =~ /java/ 12 | end 13 | end 14 | 15 | include RubyVersion 16 | Gem::Specification.extend RubyVersion 17 | 18 | Gem::Specification.new do |s| 19 | s.name = "blather" 20 | s.version = Blather::VERSION 21 | s.platform = Gem::Platform::RUBY 22 | s.authors = ["Jeff Smick", "Ben Langfeld"] 23 | s.email = %q{blather@adhearsion.com} 24 | s.homepage = "http://adhearsion.com/blather" 25 | s.summary = %q{Simpler XMPP built for speed} 26 | s.description = %q{An XMPP DSL for Ruby written on top of EventMachine and Nokogiri} 27 | s.license = 'MIT' 28 | 29 | s.files = `git ls-files`.split("\n") 30 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 31 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 32 | s.require_paths = ["lib"] 33 | 34 | s.rdoc_options = %w{--charset=UTF-8} 35 | s.extra_rdoc_files = %w{LICENSE README.md} 36 | 37 | s.add_dependency "eventmachine", ["~> 1.2", ">= 1.2.6"] 38 | s.add_dependency "nokogiri", ["~> 1.8", ">= 1.8.3"] 39 | s.add_dependency "niceogiri", ["~> 1.0"] 40 | s.add_dependency "activesupport", [">= 2.3.11"] 41 | 42 | s.add_development_dependency "rake" 43 | s.add_development_dependency "rspec", ["~> 3.0"] 44 | s.add_development_dependency "mocha", ["~> 1.0"] 45 | s.add_development_dependency "guard-rspec" 46 | s.add_development_dependency "yard", ["~> 0.9.11"] 47 | s.add_development_dependency "bluecloth" unless jruby? || rbx? 48 | s.add_development_dependency "countdownlatch" 49 | s.add_development_dependency 'rb-fsevent', ['~> 0.9'] 50 | end 51 | -------------------------------------------------------------------------------- /lib/blather/errors.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | # Main error class 3 | # This starts the error hierarchy 4 | # 5 | # @handler :error 6 | class BlatherError < StandardError 7 | class_attribute :handler_hierarchy 8 | self.handler_hierarchy ||= [] 9 | 10 | # @private 11 | @@handler_list = [] 12 | 13 | # Register the class's handler 14 | # 15 | # @param [Symbol] handler the handler name 16 | def self.register(handler) 17 | @@handler_list << handler 18 | self.handler_hierarchy = [handler] + self.handler_hierarchy 19 | end 20 | 21 | # The list of registered handlers 22 | # 23 | # @return [Array] a list of currently registered handlers 24 | def self.handler_list 25 | @@handler_list 26 | end 27 | 28 | register :error 29 | 30 | # @private 31 | # HACK!! until I can refactor the entire Error object model 32 | def id 33 | nil 34 | end 35 | end # BlatherError 36 | 37 | # Used in cases where a stanza only allows specific values for its attributes 38 | # and an invalid value is attempted. 39 | # 40 | # @handler :argument_error 41 | class ArgumentError < BlatherError 42 | register :argument_error 43 | end # ArgumentError 44 | 45 | # The stream handler received a response it didn't know how to handle 46 | # 47 | # @handler :unknown_response_error 48 | class UnknownResponse < BlatherError 49 | register :unknown_response_error 50 | attr_reader :node 51 | 52 | def initialize(node) 53 | @node = node 54 | end 55 | end # UnknownResponse 56 | 57 | # Something bad happened while parsing the incoming stream 58 | # 59 | # @handler :parse_error 60 | class ParseError < BlatherError 61 | register :parse_error 62 | attr_reader :message 63 | 64 | def initialize(msg) 65 | @message = msg.to_s 66 | end 67 | end # ParseError 68 | 69 | end # Blather 70 | -------------------------------------------------------------------------------- /spec/blather/stanza/iq/s5b_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | def s5b_open_xml 4 | <<-XML 5 | 9 | 11 | 15 | 19 | 20 | 21 | XML 22 | end 23 | 24 | describe Blather::Stanza::Iq::S5b do 25 | it 'registers itself' do 26 | expect(Blather::XMPPNode.class_from_registration(:query, 'http://jabber.org/protocol/bytestreams')).to eq(Blather::Stanza::Iq::S5b) 27 | end 28 | 29 | it 'can be imported' do 30 | node = Blather::XMPPNode.parse s5b_open_xml 31 | expect(node).to be_instance_of Blather::Stanza::Iq::S5b 32 | end 33 | 34 | it 'can get sid' do 35 | node = Blather::XMPPNode.parse s5b_open_xml 36 | expect(node.sid).to eq('vxf9n471bn46') 37 | end 38 | 39 | it 'can get streamhosts' do 40 | node = Blather::XMPPNode.parse s5b_open_xml 41 | expect(node.streamhosts.size).to eq(2) 42 | end 43 | 44 | it 'can set streamhosts' do 45 | node = Blather::Stanza::Iq::S5b.new 46 | node.streamhosts += [{:jid => 'test@example.com/foo', :host => '192.168.5.1', :port => 123}] 47 | expect(node.streamhosts.size).to eq(1) 48 | node.streamhosts += [Blather::Stanza::Iq::S5b::StreamHost.new('test2@example.com/foo', '192.168.5.2', 123)] 49 | expect(node.streamhosts.size).to eq(2) 50 | end 51 | 52 | it 'can get and set streamhost-used' do 53 | node = Blather::Stanza::Iq::S5b.new 54 | node.streamhost_used = 'used@example.com/foo' 55 | expect(node.streamhost_used.jid.to_s).to eq('used@example.com/foo') 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/blather/stanza/iq/ibr.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stanza 3 | class Iq 4 | 5 | # # In-Band Registration 6 | # 7 | # [XEP-0077: In-Band Registration](https://xmpp.org/extensions/xep-0077.html) 8 | # 9 | # @handler :ibr 10 | class IBR < Query 11 | register :ibr, nil, "jabber:iq:register" 12 | 13 | def registered=(reg) 14 | query.at_xpath("./ns:registered", ns: self.class.registered_ns)&.remove 15 | node = Nokogiri::XML::Node.new("registered", document) 16 | node.default_namespace = self.class.registered_ns 17 | query << node if reg 18 | end 19 | 20 | def registered? 21 | !!query.at_xpath("./ns:registered", ns: self.class.registered_ns) 22 | end 23 | 24 | def remove! 25 | query.children.remove 26 | node = Nokogiri::XML::Node.new("remove", document) 27 | node.default_namespace = self.class.registered_ns 28 | query << node 29 | end 30 | 31 | def remove? 32 | !!query.at_xpath("./ns:remove", ns: self.class.registered_ns) 33 | end 34 | 35 | def form 36 | X.find_or_create(query) 37 | end 38 | 39 | [ 40 | "instructions", 41 | "username", 42 | "nick", 43 | "password", 44 | "name", 45 | "first", 46 | "last", 47 | "email", 48 | "address", 49 | "city", 50 | "state", 51 | "zip", 52 | "phone", 53 | "url", 54 | "date" 55 | ].each do |tag| 56 | define_method("#{tag}=") do |v| 57 | query.at_xpath("./ns:#{tag}", ns: self.class.registered_ns)&.remove 58 | node = Nokogiri::XML::Node.new(tag, document) 59 | node.default_namespace = self.class.registered_ns 60 | node.content = v 61 | query << node 62 | end 63 | 64 | define_method(tag) do 65 | query.at_xpath("./ns:#{tag}", ns: self.class.registered_ns)&.content 66 | end 67 | end 68 | end 69 | 70 | end #Iq 71 | end #Stanza 72 | end 73 | -------------------------------------------------------------------------------- /spec/blather/stanza/pubsub_owner/purge_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'fixtures/pubsub' 3 | 4 | describe Blather::Stanza::PubSubOwner::Purge do 5 | it 'registers itself' do 6 | expect(Blather::XMPPNode.class_from_registration(:purge, 'http://jabber.org/protocol/pubsub#owner')).to eq(Blather::Stanza::PubSubOwner::Purge) 7 | end 8 | 9 | it 'can be imported' do 10 | expect(Blather::XMPPNode.parse(<<-NODE)).to be_instance_of Blather::Stanza::PubSubOwner::Purge 11 | 15 | 16 | 17 | 18 | 19 | NODE 20 | end 21 | 22 | it 'ensures an purge node is present on create' do 23 | purge = Blather::Stanza::PubSubOwner::Purge.new 24 | expect(purge.find('//ns:pubsub/ns:purge', :ns => Blather::Stanza::PubSubOwner.registered_ns)).not_to be_empty 25 | end 26 | 27 | it 'ensures an purge node exists when calling #purge_node' do 28 | purge = Blather::Stanza::PubSubOwner::Purge.new 29 | purge.pubsub.remove_children :purge 30 | expect(purge.find('//ns:pubsub/ns:purge', :ns => Blather::Stanza::PubSubOwner.registered_ns)).to be_empty 31 | 32 | expect(purge.purge_node).not_to be_nil 33 | expect(purge.find('//ns:pubsub/ns:purge', :ns => Blather::Stanza::PubSubOwner.registered_ns)).not_to be_empty 34 | end 35 | 36 | it 'defaults to a set node' do 37 | purge = Blather::Stanza::PubSubOwner::Purge.new 38 | expect(purge.type).to eq(:set) 39 | end 40 | 41 | it 'sets the host if requested' do 42 | purge = Blather::Stanza::PubSubOwner::Purge.new :set, 'pubsub.jabber.local' 43 | expect(purge.to).to eq(Blather::JID.new('pubsub.jabber.local')) 44 | end 45 | 46 | it 'sets the node' do 47 | purge = Blather::Stanza::PubSubOwner::Purge.new :set, 'host', 'node-name' 48 | expect(purge.node).to eq('node-name') 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/blather/stanza/iq/ibb.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stanza 3 | class Iq 4 | # # In-Band Bytestreams Stanza 5 | # 6 | # [XEP-0047: In-Band Bytestreams](http://xmpp.org/extensions/xep-0047.html) 7 | # 8 | # @handler :ibb_open 9 | # @handler :ibb_data 10 | # @handler :ibb_close 11 | class Ibb < Iq 12 | # @private 13 | NS_IBB = 'http://jabber.org/protocol/ibb' 14 | 15 | # Overrides the parent method to remove open, close and data nodes 16 | # 17 | # @see Blather::Stanza#reply 18 | def reply 19 | reply = super 20 | reply.remove_children :open 21 | reply.remove_children :close 22 | reply.remove_children :data 23 | reply 24 | end 25 | 26 | # An Open stanza to 27 | class Open < Ibb 28 | register :ibb_open, :open, NS_IBB 29 | 30 | # Find open node 31 | # 32 | # @return [Nokogiri::XML::Element] 33 | def open 34 | find_first('ns:open', :ns => NS_IBB) 35 | end 36 | 37 | # Get the sid of the file transfer 38 | # 39 | # @return [String] 40 | def sid 41 | open['sid'] 42 | end 43 | 44 | end 45 | 46 | # A Data stanza 47 | class Data < Ibb 48 | register :ibb_data, :data, NS_IBB 49 | 50 | # Find data node 51 | # 52 | # @return [Nokogiri::XML::Element] 53 | def data 54 | find_first('ns:data', :ns => NS_IBB) 55 | end 56 | 57 | # Get the sid of the file transfer 58 | # 59 | # @return [String] 60 | def sid 61 | data['sid'] 62 | end 63 | end 64 | 65 | # A Close stanza 66 | class Close < Ibb 67 | register :ibb_close, :close, NS_IBB 68 | 69 | # Find close node 70 | # 71 | # @return [Nokogiri::XML::Element] 72 | def close 73 | find_first('ns:close', :ns => NS_IBB) 74 | end 75 | 76 | # Get the sid of the file transfer 77 | # 78 | # @return [String] 79 | def sid 80 | close['sid'] 81 | end 82 | end 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/blather/stanza/pubsub/subscribe.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stanza 3 | class PubSub 4 | 5 | # # PubSub Subscribe Stanza 6 | # 7 | # [XEP-0060 Section 6.1 - Subscribe to a Node](http://xmpp.org/extensions/xep-0060.html#subscriber-subscribe) 8 | # 9 | # @handler :pubsub_subscribe 10 | class Subscribe < PubSub 11 | register :pubsub_subscribe, :subscribe, self.registered_ns 12 | 13 | # Create a new subscription node 14 | # 15 | # @param [Blather::Stanza::Iq::VALID_TYPES] type the IQ stanza type 16 | # @param [String] host the host name to send the request to 17 | # @param [String] node the node to subscribe to 18 | # @param [Blather::JID, #to_s] jid see {#jid=} 19 | def self.new(type = :set, host = nil, node = nil, jid = nil) 20 | new_node = super(type, host) 21 | new_node.node = node 22 | new_node.jid = jid 23 | new_node 24 | end 25 | 26 | # Get the JID of the entity to subscribe 27 | # 28 | # @return [Blather::JID] 29 | def jid 30 | JID.new(subscribe[:jid]) 31 | end 32 | 33 | # Set the JID of the entity to subscribe 34 | # 35 | # @param [Blather::JID, #to_s] jid 36 | def jid=(jid) 37 | subscribe[:jid] = jid 38 | end 39 | 40 | # Get the name of the node to subscribe to 41 | # 42 | # @return [String] 43 | def node 44 | subscribe[:node] 45 | end 46 | 47 | # Set the name of the node to subscribe to 48 | # 49 | # @param [String] node 50 | def node=(node) 51 | subscribe[:node] = node 52 | end 53 | 54 | # Get or create the actual subscribe node on the stanza 55 | # 56 | # @return [Blather::XMPPNode] 57 | def subscribe 58 | unless subscribe = pubsub.find_first('ns:subscribe', :ns => self.class.registered_ns) 59 | self.pubsub << (subscribe = XMPPNode.new('subscribe', self.document)) 60 | subscribe.namespace = self.pubsub.namespace 61 | end 62 | subscribe 63 | end 64 | end # Subscribe 65 | 66 | end # PubSub 67 | end # Stanza 68 | end # Blather 69 | -------------------------------------------------------------------------------- /spec/blather/stanza/pubsub_owner/delete_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'fixtures/pubsub' 3 | 4 | describe Blather::Stanza::PubSubOwner::Delete do 5 | it 'registers itself' do 6 | expect(Blather::XMPPNode.class_from_registration(:delete, 'http://jabber.org/protocol/pubsub#owner')).to eq(Blather::Stanza::PubSubOwner::Delete) 7 | end 8 | 9 | it 'can be imported' do 10 | expect(Blather::XMPPNode.parse(<<-NODE)).to be_instance_of Blather::Stanza::PubSubOwner::Delete 11 | 15 | 16 | 17 | 18 | 19 | NODE 20 | end 21 | 22 | it 'ensures an delete node is present on delete' do 23 | delete = Blather::Stanza::PubSubOwner::Delete.new 24 | expect(delete.find('//ns:pubsub/ns:delete', :ns => Blather::Stanza::PubSubOwner.registered_ns)).not_to be_empty 25 | end 26 | 27 | it 'ensures an delete node exists when calling #delete_node' do 28 | delete = Blather::Stanza::PubSubOwner::Delete.new 29 | delete.pubsub.remove_children :delete 30 | expect(delete.find('//ns:pubsub/ns:delete', :ns => Blather::Stanza::PubSubOwner.registered_ns)).to be_empty 31 | 32 | expect(delete.delete_node).not_to be_nil 33 | expect(delete.find('//ns:pubsub/ns:delete', :ns => Blather::Stanza::PubSubOwner.registered_ns)).not_to be_empty 34 | end 35 | 36 | it 'defaults to a set node' do 37 | delete = Blather::Stanza::PubSubOwner::Delete.new 38 | expect(delete.type).to eq(:set) 39 | end 40 | 41 | it 'sets the host if requested' do 42 | delete = Blather::Stanza::PubSubOwner::Delete.new :set, 'pubsub.jabber.local' 43 | expect(delete.to).to eq(Blather::JID.new('pubsub.jabber.local')) 44 | end 45 | 46 | it 'sets the node' do 47 | delete = Blather::Stanza::PubSubOwner::Delete.new :set, 'host', 'node-name' 48 | expect(delete.node).to eq('node-name') 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/blather/cert_store.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Blather 4 | 5 | # An X509 certificate store that validates certificate trust chains. 6 | # This uses the #{cert_directory}/*.crt files as the list of trusted root 7 | # CA certificates. 8 | class CertStore 9 | def initialize(cert_directory) 10 | @cert_directory = cert_directory 11 | @store = OpenSSL::X509::Store.new 12 | certs.each {|c| @store.add_cert(c) } 13 | end 14 | 15 | # Return true if the certificate is signed by a CA certificate in the 16 | # store. If the certificate can be trusted, it's added to the store so 17 | # it can be used to trust other certs. 18 | def trusted?(pem) 19 | if cert = OpenSSL::X509::Certificate.new(pem) 20 | @store.verify(cert).tap do |trusted| 21 | begin 22 | @store.add_cert(cert) if trusted 23 | rescue OpenSSL::X509::StoreError 24 | end 25 | end 26 | end 27 | rescue OpenSSL::X509::CertificateError 28 | nil 29 | end 30 | 31 | # Return true if the domain name matches one of the names in the 32 | # certificate. In other words, is the certificate provided to us really 33 | # for the domain to which we think we're connected? 34 | def domain?(pem, domain) 35 | if cert = OpenSSL::X509::Certificate.new(pem) 36 | OpenSSL::SSL.verify_certificate_identity(cert, domain) 37 | end 38 | end 39 | 40 | # Return the trusted root CA certificates installed in the @cert_directory. These 41 | # certificates are used to start the trust chain needed to validate certs 42 | # we receive from clients and servers. 43 | def certs 44 | @certs ||= begin 45 | pattern = /-{5}BEGIN CERTIFICATE-{5}\n.*?-{5}END CERTIFICATE-{5}\n/m 46 | Dir[File.join(@cert_directory, '*.crt')] 47 | .map {|f| File.read(f) } 48 | .map {|c| c.scan(pattern) } 49 | .flatten 50 | .map {|c| OpenSSL::X509::Certificate.new(c) } 51 | .reject {|c| c.not_after < Time.now } 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/blather/stanza/pubsub/create.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stanza 3 | class PubSub 4 | 5 | # # PubSub Create Stanza 6 | # 7 | # [XEP-0060 Section 8.1 - Create a Node](http://xmpp.org/extensions/xep-0060.html#owner-create) 8 | # 9 | # @handler :pubsub_create 10 | class Create < PubSub 11 | register :pubsub_create, :create, self.registered_ns 12 | 13 | # Create a new Create Stanza 14 | # 15 | # @param [] type the node type 16 | # @param [String, nil] host the host to send the request to 17 | # @param [String, nil] node the name of the node to create 18 | def self.new(type = :set, host = nil, node = nil) 19 | new_node = super(type, host) 20 | new_node.create_node 21 | new_node.configure_node 22 | new_node.node = node 23 | new_node 24 | end 25 | 26 | # Get the name of the node to create 27 | # 28 | # @return [String, nil] 29 | def node 30 | create_node[:node] 31 | end 32 | 33 | # Set the name of the node to create 34 | # 35 | # @param [String, nil] node 36 | def node=(node) 37 | create_node[:node] = node 38 | end 39 | 40 | # Get or create the actual create node on the stanza 41 | # 42 | # @return [Blather::XMPPNode] 43 | def create_node 44 | unless create_node = pubsub.find_first('ns:create', :ns => self.class.registered_ns) 45 | self.pubsub << (create_node = XMPPNode.new('create', self.document)) 46 | create_node.namespace = self.pubsub.namespace 47 | end 48 | create_node 49 | end 50 | 51 | # Get or create the actual configure node on the stanza 52 | # 53 | # @return [Blather::XMPPNode] 54 | def configure_node 55 | unless configure_node = pubsub.find_first('ns:configure', :ns => self.class.registered_ns) 56 | self.pubsub << (configure_node = XMPPNode.new('configure', self.document)) 57 | configure_node.namespace = self.pubsub.namespace 58 | end 59 | configure_node 60 | end 61 | end # Create 62 | 63 | end # PubSub 64 | end # Stanza 65 | end # Blather 66 | -------------------------------------------------------------------------------- /spec/blather/stanza/presence/c_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | def c_xml 4 | <<-XML 5 | 6 | 10 | 11 | XML 12 | end 13 | 14 | describe 'Blather::Stanza::Presence::C' do 15 | it 'registers itself' do 16 | expect(Blather::XMPPNode.class_from_registration(:c, 'http://jabber.org/protocol/caps' )).to eq(Blather::Stanza::Presence::C) 17 | end 18 | 19 | it 'must be importable' do 20 | c = Blather::XMPPNode.parse c_xml 21 | expect(c).to be_kind_of Blather::Stanza::Presence::C::InstanceMethods 22 | expect(c.hash).to eq(:'sha-1') 23 | expect(c.node).to eq('http://www.chatopus.com') 24 | expect(c.ver).to eq('zHyEOgxTrkpSdGcQKH8EFPLsriY=') 25 | end 26 | 27 | it 'ensures hash is one of Blather::Stanza::Presence::C::VALID_HASH_TYPES' do 28 | expect { Blather::Stanza::Presence::C.new nil, nil, :invalid_type_name }.to raise_error(Blather::ArgumentError) 29 | 30 | Blather::Stanza::Presence::C::VALID_HASH_TYPES.each do |valid_hash| 31 | c = Blather::Stanza::Presence::C.new nil, nil, valid_hash 32 | expect(c.hash).to eq(valid_hash.to_sym) 33 | end 34 | end 35 | 36 | it 'can set a hash on creation' do 37 | c = Blather::Stanza::Presence::C.new nil, nil, :md5 38 | expect(c.hash).to eq(:md5) 39 | end 40 | 41 | it 'can set a node on creation' do 42 | c = Blather::Stanza::Presence::C.new 'http://www.chatopus.com' 43 | expect(c.node).to eq('http://www.chatopus.com') 44 | end 45 | 46 | it 'can set a ver on creation' do 47 | c = Blather::Stanza::Presence::C.new nil, 'zHyEOgxTrkpSdGcQKH8EFPLsriY=' 48 | expect(c.ver).to eq('zHyEOgxTrkpSdGcQKH8EFPLsriY=') 49 | end 50 | 51 | it 'is equal on import and creation' do 52 | p = Blather::XMPPNode.parse c_xml 53 | c = Blather::Stanza::Presence::C.new 'http://www.chatopus.com', 'zHyEOgxTrkpSdGcQKH8EFPLsriY=', 'sha-1' 54 | expect(p).to eq(c) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/blather/stanza/pubsub/create_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'fixtures/pubsub' 3 | 4 | describe Blather::Stanza::PubSub::Create do 5 | it 'registers itself' do 6 | expect(Blather::XMPPNode.class_from_registration(:create, 'http://jabber.org/protocol/pubsub')).to eq(Blather::Stanza::PubSub::Create) 7 | end 8 | 9 | it 'can be imported' do 10 | expect(Blather::XMPPNode.parse(<<-NODE)).to be_instance_of Blather::Stanza::PubSub::Create 11 | 15 | 16 | 17 | 18 | 19 | 20 | NODE 21 | end 22 | 23 | it 'ensures a create node is present on create' do 24 | create = Blather::Stanza::PubSub::Create.new 25 | expect(create.find('//ns:pubsub/ns:create', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 26 | end 27 | 28 | it 'ensures a configure node is present on create' do 29 | create = Blather::Stanza::PubSub::Create.new 30 | expect(create.find('//ns:pubsub/ns:configure', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 31 | end 32 | 33 | it 'ensures a create node exists when calling #create_node' do 34 | create = Blather::Stanza::PubSub::Create.new 35 | create.pubsub.remove_children :create 36 | expect(create.find('//ns:pubsub/ns:create', :ns => Blather::Stanza::PubSub.registered_ns)).to be_empty 37 | 38 | expect(create.create_node).not_to be_nil 39 | expect(create.find('//ns:pubsub/ns:create', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 40 | end 41 | 42 | it 'defaults to a set node' do 43 | create = Blather::Stanza::PubSub::Create.new 44 | expect(create.type).to eq(:set) 45 | end 46 | 47 | it 'sets the host if requested' do 48 | create = Blather::Stanza::PubSub::Create.new :set, 'pubsub.jabber.local' 49 | expect(create.to).to eq(Blather::JID.new('pubsub.jabber.local')) 50 | end 51 | 52 | it 'sets the node' do 53 | create = Blather::Stanza::PubSub::Create.new :set, 'host', 'node-name' 54 | expect(create.node).to eq('node-name') 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/blather/stanza/iq/query_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Blather::Stanza::Iq::Query do 4 | it 'registers itself' do 5 | expect(Blather::XMPPNode.class_from_registration(:query, nil)).to eq(Blather::Stanza::Iq::Query) 6 | end 7 | 8 | it 'can be imported' do 9 | string = <<-XML 10 | 11 | 12 | 13 | 14 | 15 | XML 16 | expect(Blather::XMPPNode.parse(string)).to be_instance_of Blather::Stanza::Iq::Query 17 | end 18 | 19 | it 'ensures a query node is present on create' do 20 | query = Blather::Stanza::Iq::Query.new 21 | expect(query.xpath('query')).not_to be_empty 22 | end 23 | 24 | it 'ensures a query node exists when calling #query' do 25 | query = Blather::Stanza::Iq::Query.new 26 | query.remove_child :query 27 | expect(query.xpath('query')).to be_empty 28 | 29 | expect(query.query).not_to be_nil 30 | expect(query.xpath('query')).not_to be_empty 31 | end 32 | 33 | [:get, :set, :result, :error].each do |type| 34 | it "can be set as \"#{type}\"" do 35 | query = Blather::Stanza::Iq::Query.new type 36 | expect(query.type).to eq(type) 37 | end 38 | end 39 | 40 | it 'sets type to "result" on reply' do 41 | query = Blather::Stanza::Iq::Query.new 42 | expect(query.type).to eq(:get) 43 | reply = expect(query.reply.type).to eq(:result) 44 | end 45 | 46 | it 'sets type to "result" on reply!' do 47 | query = Blather::Stanza::Iq::Query.new 48 | expect(query.type).to eq(:get) 49 | query.reply! 50 | expect(query.type).to eq(:result) 51 | end 52 | 53 | it 'can be registered under a namespace' do 54 | class QueryNs < Blather::Stanza::Iq::Query; register :query_ns, nil, 'query:ns'; end 55 | expect(Blather::XMPPNode.class_from_registration(:query, 'query:ns')).to eq(QueryNs) 56 | query_ns = QueryNs.new 57 | expect(query_ns.xpath('query')).to be_empty 58 | expect(query_ns.xpath('ns:query', :ns => 'query:ns').size).to eq(1) 59 | 60 | query_ns.query 61 | query_ns.query 62 | expect(query_ns.xpath('ns:query', :ns => 'query:ns').size).to eq(1) 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/blather/stanza/pubsub/affiliations.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stanza 3 | class PubSub 4 | 5 | # # PubSub Affiliations Stanza 6 | # 7 | # [XEP-0060 Section 8.9 - Manage Affiliations](http://xmpp.org/extensions/xep-0060.html#owner-affiliations) 8 | # 9 | # @handler :pubsub_affiliations 10 | class Affiliations < PubSub 11 | register :pubsub_affiliations, :affiliations, self.registered_ns 12 | 13 | include Enumerable 14 | alias_method :find, :xpath 15 | 16 | # Overrides the parent to ensure an affiliation node is created 17 | # @private 18 | def self.new(type = nil, host = nil) 19 | new_node = super 20 | new_node.affiliations 21 | new_node 22 | end 23 | 24 | # Kill the affiliations node before running inherit 25 | # @private 26 | def inherit(node) 27 | affiliations.remove 28 | super 29 | end 30 | 31 | # Get or create the affiliations node 32 | # 33 | # @return [Blather::XMPPNode] 34 | def affiliations 35 | aff = pubsub.find_first('ns:affiliations', :ns => self.class.registered_ns) 36 | unless aff 37 | self.pubsub << (aff = XMPPNode.new('affiliations', self.document)) 38 | end 39 | aff 40 | end 41 | 42 | # Convenience method for iterating over the list 43 | # 44 | # @see #list for the format of the yielded input 45 | def each(&block) 46 | list.each &block 47 | end 48 | 49 | # Get the number of affiliations 50 | # 51 | # @return [Fixnum] 52 | def size 53 | list.size 54 | end 55 | 56 | # Get the hash of affilations as affiliation-type => [nodes] 57 | # 58 | # @example 59 | # 60 | # { :owner => ['node1', 'node2'], 61 | # :publisher => ['node3'], 62 | # :outcast => ['node4'], 63 | # :member => ['node5'], 64 | # :none => ['node6'] } 65 | # 66 | # @return [Hash Array>] 67 | def list 68 | items = affiliations.find('//ns:affiliation', :ns => self.class.registered_ns) 69 | items.inject({}) do |hash, item| 70 | hash[item[:affiliation].to_sym] ||= [] 71 | hash[item[:affiliation].to_sym] << item[:node] 72 | hash 73 | end 74 | end 75 | end # Affiliations 76 | 77 | end # PubSub 78 | end # Stanza 79 | end # Blather 80 | -------------------------------------------------------------------------------- /spec/blather/stanza/iq_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Blather::Stanza::Iq do 4 | it 'registers itself' do 5 | expect(Blather::XMPPNode.class_from_registration(:iq, nil)).to eq(Blather::Stanza::Iq) 6 | end 7 | 8 | it 'must be importable' do 9 | string = "" 10 | expect(Blather::XMPPNode.parse(string)).to be_instance_of Blather::Stanza::Iq 11 | end 12 | 13 | it 'creates a new Iq stanza defaulted as a get' do 14 | expect(Blather::Stanza::Iq.new.type).to eq(:get) 15 | end 16 | 17 | it 'sets the id when created' do 18 | expect(Blather::Stanza::Iq.new.id).not_to be_nil 19 | end 20 | 21 | it 'creates a new Stanza::Iq object on import' do 22 | expect(Blather::Stanza::Iq.import(Blather::XMPPNode.new('iq'))).to be_kind_of Blather::Stanza::Iq 23 | end 24 | 25 | it 'creates a proper object based on its children' do 26 | n = Blather::XMPPNode.new('iq') 27 | n << Blather::XMPPNode.new('query', n.document) 28 | expect(Blather::Stanza::Iq.import(n)).to be_kind_of Blather::Stanza::Iq::Query 29 | end 30 | 31 | it 'ensures type is one of Stanza::Iq::VALID_TYPES' do 32 | expect { Blather::Stanza::Iq.new :invalid_type_name }.to raise_error(Blather::ArgumentError) 33 | 34 | Blather::Stanza::Iq::VALID_TYPES.each do |valid_type| 35 | n = Blather::Stanza::Iq.new valid_type 36 | expect(n.type).to eq(valid_type) 37 | end 38 | end 39 | 40 | Blather::Stanza::Iq::VALID_TYPES.each do |valid_type| 41 | it "provides a helper (#{valid_type}?) for type #{valid_type}" do 42 | expect(Blather::Stanza::Iq.new).to respond_to :"#{valid_type}?" 43 | end 44 | end 45 | 46 | it 'removes the body when replying' do 47 | iq = Blather::Stanza::Iq.new :get, 'me@example.com' 48 | iq.from = 'them@example.com' 49 | iq << Blather::XMPPNode.new('query', iq.document) 50 | r = iq.reply 51 | expect(r.children.empty?).to eq(true) 52 | end 53 | 54 | it 'does not remove the body when replying if we ask to keep it' do 55 | iq = Blather::Stanza::Iq.new :get, 'me@example.com' 56 | iq.from = 'them@example.com' 57 | iq << Blather::XMPPNode.new('query', iq.document) 58 | r = iq.reply :remove_children => false 59 | expect(r.children.empty?).to eq(false) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/blather/stanza/pubsub/affiliations_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'fixtures/pubsub' 3 | 4 | def control_affiliations 5 | { :owner => ['node1', 'node2'], 6 | :publisher => ['node3'], 7 | :outcast => ['node4'], 8 | :member => ['node5'], 9 | :none => ['node6'] } 10 | end 11 | 12 | describe Blather::Stanza::PubSub::Affiliations do 13 | it 'registers itself' do 14 | expect(Blather::XMPPNode.class_from_registration(:affiliations, Blather::Stanza::PubSub.registered_ns)).to eq(Blather::Stanza::PubSub::Affiliations) 15 | end 16 | 17 | it 'can be imported' do 18 | expect(Blather::XMPPNode.parse(affiliations_xml)).to be_instance_of Blather::Stanza::PubSub::Affiliations 19 | end 20 | 21 | it 'ensures an affiliations node is present on create' do 22 | affiliations = Blather::Stanza::PubSub::Affiliations.new 23 | expect(affiliations.find_first('//ns:affiliations', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_nil 24 | end 25 | 26 | it 'ensures an affiliations node exists when calling #affiliations' do 27 | affiliations = Blather::Stanza::PubSub::Affiliations.new 28 | affiliations.pubsub.remove_children :affiliations 29 | expect(affiliations.find_first('//ns:affiliations', :ns => Blather::Stanza::PubSub.registered_ns)).to be_nil 30 | 31 | expect(affiliations.affiliations).not_to be_nil 32 | expect(affiliations.find_first('//ns:affiliations', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_nil 33 | end 34 | 35 | it 'defaults to a get node' do 36 | expect(Blather::Stanza::PubSub::Affiliations.new.type).to eq(:get) 37 | end 38 | 39 | it 'sets the host if requested' do 40 | aff = Blather::Stanza::PubSub::Affiliations.new :get, 'pubsub.jabber.local' 41 | expect(aff.to).to eq(Blather::JID.new('pubsub.jabber.local')) 42 | end 43 | 44 | it 'can import an affiliates result node' do 45 | node = parse_stanza(affiliations_xml).root 46 | 47 | affiliations = Blather::Stanza::PubSub::Affiliations.new.inherit node 48 | expect(affiliations.size).to eq(5) 49 | expect(affiliations.list).to eq(control_affiliations) 50 | end 51 | 52 | it 'will iterate over each affiliation' do 53 | Blather::XMPPNode.parse(affiliations_xml).each do |type, nodes| 54 | expect(nodes).to eq(control_affiliations[type]) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/blather/stream/features.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stream 3 | 4 | # @private 5 | class Features 6 | @@features = {} 7 | def self.register(ns) 8 | @@features[ns] = self 9 | end 10 | 11 | def self.from_namespace(ns) 12 | @@features[ns] 13 | end 14 | 15 | def initialize(stream, succeed, fail) 16 | @stream, @succeed, @fail = stream, succeed, fail 17 | end 18 | 19 | def receive_data(stanza) 20 | if @feature 21 | @feature.receive_data stanza 22 | else 23 | @features ||= stanza 24 | next! 25 | end 26 | end 27 | 28 | def next! 29 | if starttls = @features.at_xpath("tls:starttls",{"tls" => "urn:ietf:params:xml:ns:xmpp-tls"}) 30 | @feature = TLS.new(@stream, nil, @fail) 31 | @feature.receive_data(starttls) 32 | return 33 | end 34 | 35 | bind = @features.at_xpath('ns:bind', ns: 'urn:ietf:params:xml:ns:xmpp-bind') 36 | session = @features.at_xpath('ns:session', ns: 'urn:ietf:params:xml:ns:xmpp-session') 37 | if bind && session && @features.children.last != session 38 | bind.after session 39 | end 40 | 41 | @idx = @idx ? @idx+1 : 0 42 | if stanza = @features.children[@idx] 43 | if stanza.namespaces['xmlns'] && (klass = self.class.from_namespace(stanza.namespaces['xmlns'])) 44 | @feature = klass.new( 45 | @stream, 46 | proc { 47 | if (klass == Blather::Stream::Register && stanza = feature?(:mechanisms)) 48 | @idx = @features.children.index(stanza) 49 | @feature = Blather::Stream::SASL.new @stream, proc { next! }, @fail 50 | @feature.receive_data stanza 51 | else 52 | next! 53 | end 54 | }, 55 | (klass == Blather::Stream::SASL && feature?(:register)) ? proc { next! } : @fail 56 | ) 57 | @feature.receive_data stanza 58 | else 59 | next! 60 | end 61 | else 62 | succeed! 63 | end 64 | end 65 | 66 | def succeed! 67 | @succeed.call 68 | end 69 | 70 | def fail!(msg) 71 | @fail.call msg 72 | end 73 | 74 | def feature?(feature) 75 | @features && @features.children.find { |v| v.element_name == feature.to_s } 76 | end 77 | end 78 | 79 | end #Stream 80 | end #Blather 81 | -------------------------------------------------------------------------------- /examples/print_hierarchy.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'blather' 3 | 4 | class Object 5 | begin 6 | ObjectSpace.each_object(Class.new) {} 7 | 8 | # Exclude this class unless it's a subclass of our supers and is defined. 9 | # We check defined? in case we find a removed class that has yet to be 10 | # garbage collected. This also fails for anonymous classes -- please 11 | # submit a patch if you have a workaround. 12 | def subclasses_of(*superclasses) 13 | subclasses = [] 14 | 15 | superclasses.each do |sup| 16 | ObjectSpace.each_object(class << sup; self; end) do |k| 17 | if k != sup && (k.name.blank? || eval("defined?(::#{k}) && ::#{k}.object_id == k.object_id")) 18 | subclasses << k 19 | end 20 | end 21 | end 22 | 23 | subclasses 24 | end 25 | rescue RuntimeError 26 | # JRuby and any implementations which cannot handle the objectspace traversal 27 | # above fall back to this implementation 28 | def subclasses_of(*superclasses) 29 | subclasses = [] 30 | 31 | superclasses.each do |sup| 32 | ObjectSpace.each_object(Class) do |k| 33 | if superclasses.any? { |superclass| k < superclass } && 34 | (k.name.blank? || eval("defined?(::#{k}) && ::#{k}.object_id == k.object_id")) 35 | subclasses << k 36 | end 37 | end 38 | subclasses.uniq! 39 | end 40 | subclasses 41 | end 42 | end 43 | end 44 | 45 | class Hash 46 | def deep_merge(second) 47 | # From: http://www.ruby-forum.com/topic/142809 48 | # Author: Stefan Rusterholz 49 | merger = proc { |key,v1,v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 } 50 | self.merge(second, &merger) 51 | end 52 | end 53 | 54 | handlers = {} 55 | (Object.subclasses_of(Blather::Stanza) + Object.subclasses_of(Blather::BlatherError)).each do |klass| 56 | handlers = handlers.deep_merge klass.handler_hierarchy.inject('klass' => klass.to_s.gsub('Blather::', '')) { |h,k| {k.to_s => h} } 57 | end 58 | 59 | level = 0 60 | runner = proc do |k,v| 61 | next if k == 'klass' 62 | 63 | str = '' 64 | if level > 0 65 | (level - 1).times { str << '| ' } 66 | str << '|- ' 67 | end 68 | 69 | puts str+k 70 | if Hash === v 71 | level += 1 72 | v.sort.each &runner 73 | level -= 1 74 | end 75 | end 76 | 77 | handlers.sort.each &runner 78 | -------------------------------------------------------------------------------- /lib/blather/errors/stream_error.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | 3 | # Stream Errors 4 | # [RFC3920 Section 9.3](http://xmpp.org/rfcs/rfc3920.html#streams-error-rules) 5 | # 6 | # @handler :stream_error 7 | class StreamError < BlatherError 8 | # @private 9 | STREAM_ERR_NS = 'urn:ietf:params:xml:ns:xmpp-streams' 10 | 11 | register :stream_error 12 | 13 | attr_reader :text, :extras 14 | 15 | # Factory method for instantiating the proper class for the error 16 | # 17 | # @param [Blather::XMPPNode] node the importable node 18 | def self.import(node) 19 | name = node.find_first('descendant::*[name()!="text"]', STREAM_ERR_NS).element_name 20 | 21 | text = node.find_first 'descendant::*[name()="text"]', STREAM_ERR_NS 22 | text = text.content if text 23 | 24 | extras = node.find("descendant::*[namespace-uri()!='#{STREAM_ERR_NS}']").map { |n| n } 25 | 26 | self.new name, text, extras 27 | end 28 | 29 | # Create a new Stream Error 30 | # [RFC3920 Section 4.7.2](http://xmpp.org/rfcs/rfc3920.html#rfc.section.4.7.2) 31 | # 32 | # @param [String] name the error name 33 | # @param [String, nil] text optional error text 34 | # @param [Array] extras an array of extras to attach to the 35 | # error 36 | def initialize(name, text = nil, extras = []) 37 | @name = name 38 | @text = text 39 | @extras = extras 40 | end 41 | 42 | # The error name 43 | # 44 | # @return [Symbol] 45 | def name 46 | @name.gsub('-','_').to_sym 47 | end 48 | 49 | # Creates an XML node from the error 50 | # 51 | # @return [Blather::XMPPNode] 52 | def to_node 53 | node = XMPPNode.new('error') 54 | node.namespace = {'stream' => Blather::Stream::STREAM_NS} 55 | 56 | node << (err = XMPPNode.new(@name, node.document)) 57 | err.namespace = 'urn:ietf:params:xml:ns:xmpp-streams' 58 | 59 | if self.text 60 | node << (text = XMPPNode.new('text', node.document)) 61 | text.namespace = 'urn:ietf:params:xml:ns:xmpp-streams' 62 | text.content = self.text 63 | end 64 | 65 | self.extras.each { |extra| node << extra.dup } 66 | node 67 | end 68 | 69 | # Convert the object to a proper node then convert it to a string 70 | # 71 | # @return [String] 72 | def to_xml(*args) 73 | to_node.to_xml(*args) 74 | end 75 | 76 | # @private 77 | def inspect 78 | "Stream Error (#{@name}): #{self.text}" + (self.extras.empty? ? '' : " [#{self.extras}]") 79 | end 80 | # @private 81 | alias_method :to_s, :inspect 82 | end # StreamError 83 | 84 | end # Blather 85 | -------------------------------------------------------------------------------- /lib/blather/file_transfer/ibb.rb: -------------------------------------------------------------------------------- 1 | require "base64" 2 | 3 | module Blather 4 | class FileTransfer 5 | # In-Band Bytestreams Transfer helper 6 | # Takes care of accepting, declining and offering file transfers through the stream 7 | class Ibb 8 | def initialize(stream, iq) 9 | @stream = stream 10 | @iq = iq 11 | @seq = 0 12 | end 13 | 14 | # Accept an incoming file-transfer 15 | # 16 | # @param [module] handler the handler for incoming data, see Blather::FileTransfer::SimpleFileReceiver for an example 17 | # @param [Array] params the params to be passed into the handler 18 | def accept(handler, *params) 19 | @io_read, @io_write = IO.pipe 20 | EM::attach @io_read, handler, *params 21 | 22 | @stream.register_handler :ibb_data, :from => @iq.from, :sid => @iq.sid do |iq| 23 | if iq.data['seq'] == @seq.to_s 24 | begin 25 | @io_write << Base64.decode64(iq.data.content) 26 | 27 | @stream.write iq.reply 28 | 29 | @seq += 1 30 | @seq = 0 if @seq > 65535 31 | rescue Errno::EPIPE => e 32 | @stream.write StanzaError.new(iq, 'not-acceptable', :cancel).to_node 33 | end 34 | else 35 | @stream.write StanzaError.new(iq, 'unexpected-request', :wait).to_node 36 | end 37 | true 38 | end 39 | 40 | @stream.register_handler :ibb_close, :from => @iq.from, :sid => @iq.sid do |iq| 41 | @stream.write iq.reply 42 | @stream.clear_handlers :ibb_data, :from => @iq.from, :sid => @iq.sid 43 | @stream.clear_handlers :ibb_close, :from => @iq.from, :sid => @iq.sid 44 | 45 | @io_write.close 46 | true 47 | end 48 | 49 | @stream.clear_handlers :ibb_open, :from => @iq.from 50 | @stream.clear_handlers :ibb_open, :from => @iq.from, :sid => @iq.sid 51 | @stream.write @iq.reply 52 | end 53 | 54 | # Decline an incoming file-transfer 55 | def decline 56 | @stream.clear_handlers :ibb_open, :from => @iq.from 57 | @stream.clear_handlers :ibb_data, :from => @iq.from, :sid => @iq.sid 58 | @stream.clear_handlers :ibb_close, :from => @iq.from, :sid => @iq.sid 59 | @stream.write StanzaError.new(@iq, 'not-acceptable', :cancel).to_node 60 | end 61 | 62 | # Offer a file to somebody, not implemented yet 63 | def offer 64 | # TODO: implement 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/blather/stanza/pubsub/unsubscribe.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stanza 3 | class PubSub 4 | 5 | # # PubSub Unsubscribe Stanza 6 | # 7 | # [XEP-0060 Section 6.2 - Unsubscribe from a Node](http://xmpp.org/extensions/xep-0060.html#subscriber-unsubscribe) 8 | # 9 | # @handler :pubsub_unsubscribe 10 | class Unsubscribe < PubSub 11 | register :pubsub_unsubscribe, :unsubscribe, self.registered_ns 12 | 13 | # Create a new unsubscribe node 14 | # 15 | # @param [Blather::Stanza::Iq::VALID_TYPES] type the IQ stanza type 16 | # @param [String] host the host to send the request to 17 | # @param [String] node the node to unsubscribe from 18 | # @param [Blather::JID, #to_s] jid the JID of the unsubscription 19 | # @param [String] subid the subscription ID of the unsubscription 20 | def self.new(type = :set, host = nil, node = nil, jid = nil, subid = nil) 21 | new_node = super(type, host) 22 | new_node.node = node 23 | new_node.jid = jid 24 | new_node.subid = subid 25 | new_node 26 | end 27 | 28 | # Get the JID of the unsubscription 29 | # 30 | # @return [Blather::JID] 31 | def jid 32 | JID.new(unsubscribe[:jid]) 33 | end 34 | 35 | # Set the JID of the unsubscription 36 | # 37 | # @param [Blather::JID, #to_s] jid 38 | def jid=(jid) 39 | unsubscribe[:jid] = jid 40 | end 41 | 42 | # Get the name of the node to unsubscribe from 43 | # 44 | # @return [String] 45 | def node 46 | unsubscribe[:node] 47 | end 48 | 49 | # Set the name of the node to unsubscribe from 50 | # 51 | # @param [String] node 52 | def node=(node) 53 | unsubscribe[:node] = node 54 | end 55 | 56 | # Get the subscription ID to unsubscribe from 57 | # 58 | # @return [String] 59 | def subid 60 | unsubscribe[:subid] 61 | end 62 | 63 | # Set the subscription ID to unsubscribe from 64 | # 65 | # @param [String] node 66 | def subid=(subid) 67 | unsubscribe[:subid] = subid 68 | end 69 | 70 | # Get or create the actual unsubscribe node 71 | # 72 | # @return [Blather::XMPPNode] 73 | def unsubscribe 74 | unless unsubscribe = pubsub.find_first('ns:unsubscribe', :ns => self.class.registered_ns) 75 | self.pubsub << (unsubscribe = XMPPNode.new('unsubscribe', self.document)) 76 | unsubscribe.namespace = self.pubsub.namespace 77 | end 78 | unsubscribe 79 | end 80 | end # Unsubscribe 81 | 82 | end # PubSub 83 | end # Stanza 84 | end # Blather 85 | -------------------------------------------------------------------------------- /spec/blather/stanza/pubsub_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'fixtures/pubsub' 3 | 4 | describe Blather::Stanza::PubSub do 5 | it 'registers itself' do 6 | expect(Blather::XMPPNode.class_from_registration(:pubsub, 'http://jabber.org/protocol/pubsub')).to eq(Blather::Stanza::PubSub) 7 | end 8 | 9 | it 'ensures a pubusb node is present on create' do 10 | pubsub = Blather::Stanza::PubSub.new 11 | expect(pubsub.find_first('/iq/ns:pubsub', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_nil 12 | end 13 | 14 | it 'ensures a pubsub node exists when calling #pubsub' do 15 | pubsub = Blather::Stanza::PubSub.new 16 | pubsub.remove_children :pubsub 17 | expect(pubsub.find_first('/iq/ns:pubsub', :ns => Blather::Stanza::PubSub.registered_ns)).to be_nil 18 | 19 | expect(pubsub.pubsub).not_to be_nil 20 | expect(pubsub.find_first('/iq/ns:pubsub', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_nil 21 | end 22 | 23 | it 'sets the host if requested' do 24 | aff = Blather::Stanza::PubSub.new :get, 'pubsub.jabber.local' 25 | expect(aff.to).to eq(Blather::JID.new('pubsub.jabber.local')) 26 | end 27 | 28 | it 'ensures newly inherited items are PubSubItem objects' do 29 | pubsub = Blather::XMPPNode.parse(items_all_nodes_xml) 30 | expect(pubsub.items.map { |i| i.class }.uniq).to eq([Blather::Stanza::PubSub::PubSubItem]) 31 | end 32 | end 33 | 34 | describe Blather::Stanza::PubSub::PubSubItem do 35 | it 'can be initialized with just an ID' do 36 | id = 'foobarbaz' 37 | item = Blather::Stanza::PubSub::Items::PubSubItem.new id 38 | expect(item.id).to eq(id) 39 | end 40 | 41 | it 'can be initialized with a payload' do 42 | payload = 'foobarbaz' 43 | item = Blather::Stanza::PubSub::Items::PubSubItem.new 'foo', payload 44 | expect(item.payload).to eq(payload) 45 | end 46 | 47 | it 'allows the payload to be set' do 48 | item = Blather::Stanza::PubSub::Items::PubSubItem.new 49 | expect(item.payload).to be_nil 50 | item.payload = 'testing' 51 | expect(item.payload).to eq('testing') 52 | expect(item.content).to eq('testing') 53 | end 54 | 55 | it 'allows the payload to be unset' do 56 | payload = 'foobarbaz' 57 | item = Blather::Stanza::PubSub::Items::PubSubItem.new 'foo', payload 58 | expect(item.payload).to eq(payload) 59 | item.payload = nil 60 | expect(item.payload).to be_nil 61 | end 62 | 63 | it 'makes payloads readable as string' do 64 | payload = Blather::XMPPNode.new 'foo' 65 | item = Blather::Stanza::PubSub::Items::PubSubItem.new 'bar', payload 66 | expect(item.payload).to eq(payload.to_s) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/blather/stanza/pubsub/subscriptions.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stanza 3 | class PubSub 4 | 5 | # # PubSub Subscriptions Stanza 6 | # 7 | # [XEP-0060 Section 5.6 Retrieve Subscriptions](http://xmpp.org/extensions/xep-0060.html#entity-subscriptions) 8 | # 9 | # @handler :pubsub_subscriptions 10 | class Subscriptions < PubSub 11 | register :pubsub_subscriptions, :subscriptions, self.registered_ns 12 | 13 | include Enumerable 14 | alias_method :find, :xpath 15 | 16 | # Overrides the parent to ensure a subscriptions node is created 17 | # @private 18 | def self.new(type = nil, host = nil) 19 | new_node = super type 20 | new_node.to = host 21 | new_node.subscriptions 22 | new_node 23 | end 24 | 25 | # Overrides the parent to ensure the subscriptions node is destroyed 26 | # @private 27 | def inherit(node) 28 | subscriptions.remove 29 | super 30 | end 31 | 32 | # Get or create the actual subscriptions node 33 | # 34 | # @return [Blather::XMPPNode] 35 | def subscriptions 36 | subs = pubsub.find_first('ns:subscriptions', :ns => self.class.registered_ns) 37 | unless subs 38 | self.pubsub << (subs = XMPPNode.new('subscriptions', self.document)) 39 | end 40 | subs 41 | end 42 | 43 | # Iterate over the list of subscriptions 44 | # 45 | # @yieldparam [Hash] subscription 46 | # @see {#list} 47 | def each(&block) 48 | list.each &block 49 | end 50 | 51 | # Get the size of the subscriptions list 52 | # 53 | # @return [Fixnum] 54 | def size 55 | list.size 56 | end 57 | 58 | # Get a hash of subscriptions 59 | # 60 | # @example 61 | # { :subscribed => [{:node => 'node1', :jid => 'francisco@denmark.lit', :subid => 'fd8237yr872h3f289j2'}, {:node => 'node2', :jid => 'francisco@denmark.lit', :subid => 'h8394hf8923ju'}], 62 | # :unconfigured => [{:node => 'node3', :jid => 'francisco@denmark.lit'}], 63 | # :pending => [{:node => 'node4', :jid => 'francisco@denmark.lit'}], 64 | # :none => [{:node => 'node5', :jid => 'francisco@denmark.lit'}] } 65 | # 66 | # @return [Hash] 67 | def list 68 | subscriptions.find('//ns:subscription', :ns => self.class.registered_ns).inject({}) do |hash, item| 69 | hash[item[:subscription].to_sym] ||= [] 70 | sub = { 71 | :node => item[:node], 72 | :jid => item[:jid] 73 | } 74 | sub[:subid] = item[:subid] if item[:subid] 75 | hash[item[:subscription].to_sym] << sub 76 | hash 77 | end 78 | end 79 | end # Subscriptions 80 | 81 | end # PubSub 82 | end # Stanza 83 | end # Blather 84 | -------------------------------------------------------------------------------- /lib/blather/stanza/pubsub/retract.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stanza 3 | class PubSub 4 | 5 | # # PubSub Retract Stanza 6 | # 7 | # [XEP-0060 Section 7.2 - Delete an Item from a Node](http://xmpp.org/extensions/xep-0060.html#publisher-delete) 8 | # 9 | # @handler :pubsub_retract 10 | class Retract < PubSub 11 | register :pubsub_retract, :retract, self.registered_ns 12 | 13 | include Enumerable 14 | alias_method :find, :xpath 15 | 16 | # Createa new Retraction stanza 17 | # 18 | # @param [String] host the host to send the request to 19 | # @param [String] node the node to retract items from 20 | # @param [Blather::Stanza::Iq::VALID_TYPES] type the IQ stanza type 21 | # @param [Array] retractions an array of ids to retract 22 | def self.new(host = nil, node = nil, type = :set, retractions = []) 23 | new_node = super(type, host) 24 | new_node.node = node 25 | new_node.retractions = retractions 26 | new_node 27 | end 28 | 29 | # Get the name of the node to retract from 30 | # 31 | # @return [String] 32 | def node 33 | retract[:node] 34 | end 35 | 36 | # Set the name of the node to retract from 37 | # 38 | # @param [String] node 39 | def node=(node) 40 | retract[:node] = node 41 | end 42 | 43 | # Get or create the actual retract node 44 | # 45 | # @return [Blather::XMPPNode] 46 | def retract 47 | unless retract = pubsub.find_first('ns:retract', :ns => self.class.registered_ns) 48 | self.pubsub << (retract = XMPPNode.new('retract', self.document)) 49 | retract.namespace = self.pubsub.namespace 50 | end 51 | retract 52 | end 53 | 54 | # Set the retraction ids 55 | # 56 | # @overload retractions=(id) 57 | # @param [String] id an ID to retract 58 | # @overload retractions=(ids) 59 | # @param [Array] ids an array of IDs to retract 60 | def retractions=(retractions = []) 61 | [retractions].flatten.each do |id| 62 | self.retract << PubSubItem.new(id, nil, self.document) 63 | end 64 | end 65 | 66 | # Get the list of item IDs to retract 67 | # 68 | # @return [Array] 69 | def retractions 70 | retract.find('ns:item', :ns => self.class.registered_ns).map do |i| 71 | i[:id] 72 | end 73 | end 74 | 75 | # Iterate over each retraction ID 76 | # 77 | # @yieldparam [String] id an ID to retract 78 | def each(&block) 79 | retractions.each &block 80 | end 81 | 82 | # The size of the retractions array 83 | # 84 | # @return [Fixnum] 85 | def size 86 | retractions.size 87 | end 88 | end # Retract 89 | 90 | end # PubSub 91 | end # Stanza 92 | end # Blather 93 | -------------------------------------------------------------------------------- /lib/blather/stanza/presence/c.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stanza 3 | class Presence 4 | 5 | # # Entity Capabilities Stanza 6 | # 7 | # [XEP-0115 - Entity Capabilities](http://http://xmpp.org/extensions/xep-0115.html) 8 | # 9 | # Blather handles c nodes through this class. It provides a set of helper methods 10 | # to quickly deal with capabilites presence stanzas. 11 | # 12 | # @handler :c 13 | class C < Presence 14 | register :c, :c, 'http://jabber.org/protocol/caps' 15 | 16 | # @private 17 | VALID_HASH_TYPES = %w[md2 md5 sha-1 sha-224 sha-256 sha-384 sha-512].freeze 18 | 19 | def self.new(node = nil, ver = nil, hash = 'sha-1') 20 | new_node = super() 21 | new_node.c 22 | new_node.hash = hash 23 | new_node.node = node 24 | new_node.ver = ver 25 | parse new_node.to_xml 26 | end 27 | 28 | module InstanceMethods 29 | 30 | # @private 31 | def inherit(node) 32 | c.remove 33 | super 34 | self 35 | end 36 | 37 | # Get the name of the node 38 | # 39 | # @return [String, nil] 40 | def node 41 | c[:node] 42 | end 43 | 44 | # Set the name of the node 45 | # 46 | # @param [String, nil] node the new node name 47 | def node=(node) 48 | c[:node] = node 49 | end 50 | 51 | # Get the name of the hash 52 | # 53 | # @return [Symbol, nil] 54 | def hash 55 | c[:hash] && c[:hash].to_sym 56 | end 57 | 58 | # Set the name of the hash 59 | # 60 | # @param [String, nil] hash the new hash name 61 | def hash=(hash) 62 | if hash && !VALID_HASH_TYPES.include?(hash.to_s) 63 | raise ArgumentError, "Invalid Hash Type (#{hash}), use: #{VALID_HASH_TYPES*' '}" 64 | end 65 | c[:hash] = hash 66 | end 67 | 68 | # Get the ver 69 | # 70 | # @return [String, nil] 71 | def ver 72 | c[:ver] 73 | end 74 | 75 | # Set the ver 76 | # 77 | # @param [String, nil] ver the new ver 78 | def ver=(ver) 79 | c[:ver] = ver 80 | end 81 | 82 | # C node accessor 83 | # If a c node exists it will be returned. 84 | # Otherwise a new node will be created and returned 85 | # 86 | # @return [Blather::XMPPNode] 87 | def c 88 | unless c = find_first('ns:c', :ns => C.registered_ns) 89 | self << (c = XMPPNode.new('c', self.document)) 90 | c.namespace = self.class.registered_ns 91 | end 92 | c 93 | end 94 | end 95 | 96 | include InstanceMethods 97 | end # C 98 | end #Presence 99 | end #Stanza 100 | end 101 | -------------------------------------------------------------------------------- /lib/blather/client.rb: -------------------------------------------------------------------------------- 1 | require 'optparse' 2 | 3 | if !defined?(Blather::DSL) 4 | require File.join(File.dirname(__FILE__), *%w[client dsl]) 5 | 6 | include Blather::DSL 7 | end 8 | 9 | options = {} 10 | optparse = OptionParser.new do |opts| 11 | opts.banner = "Run with #{$0} [options] user@server/resource password [host] [port]" 12 | 13 | opts.on('-D', '--debug', 'Run in debug mode (you will see all XMPP communication)') do 14 | options[:debug] = true 15 | end 16 | 17 | opts.on('-d', '--daemonize', 'Daemonize the process') do |daemonize| 18 | options[:daemonize] = daemonize 19 | end 20 | 21 | opts.on('--pid=[PID]', 'Write the PID to this file') do |pid| 22 | if !File.writable?(File.dirname(pid)) 23 | $stderr.puts "Unable to write log file to #{pid}" 24 | exit 1 25 | end 26 | options[:pid] = pid 27 | end 28 | 29 | opts.on('--log=[LOG]', 'Write to the [LOG] file instead of stdout/stderr') do |log| 30 | if !File.writable?(File.dirname(log)) 31 | $stderr.puts "Unable to write log file to #{log}" 32 | exit 1 33 | end 34 | options[:log] = log 35 | end 36 | 37 | opts.on('--certs=[CERTS DIRECTORY]', 'The directory path where the trusted certificates are stored') do |certs| 38 | if !File.directory?(certs) 39 | $stderr.puts "The certs directory path (#{certs}) is no good." 40 | exit 1 41 | end 42 | options[:certs] = certs 43 | end 44 | 45 | opts.on_tail('-h', '--help', 'Show this message') do 46 | puts opts 47 | exit 48 | end 49 | 50 | opts.on_tail('-v', '--version', 'Show version') do 51 | require 'yaml' 52 | version = YAML.load_file File.join(File.dirname(__FILE__), %w[.. .. VERSION.yml]) 53 | puts "Blather v#{version[:major]}.#{version[:minor]}.#{version[:patch]}" 54 | exit 55 | end 56 | end 57 | optparse.parse! 58 | 59 | at_exit do 60 | unless client.setup? 61 | if ARGV.length < 2 62 | puts optparse 63 | exit 1 64 | end 65 | client.setup(*ARGV) 66 | end 67 | 68 | def at_exit_run(options) 69 | $stdin.reopen("/dev/null") if options[:daemonize] && $stdin.tty? 70 | 71 | if options[:log] 72 | log = File.new(options[:log], 'a') 73 | log.sync = options[:debug] 74 | $stdout.reopen log 75 | $stderr.reopen $stdout 76 | end 77 | 78 | Blather.logger.level = Logger::DEBUG if options[:debug] 79 | 80 | trap(:INT) { EM.stop } 81 | trap(:TERM) { EM.stop } 82 | EM.run { client.run } 83 | end 84 | 85 | if options[:daemonize] 86 | pid = fork do 87 | Process.setsid 88 | exit if fork 89 | File.open(options[:pid], 'w') { |f| f << Process.pid } if options[:pid] 90 | at_exit_run options 91 | FileUtils.rm(options[:pid]) if options[:pid] 92 | end 93 | ::Process.detach pid 94 | exit 95 | else 96 | at_exit_run options 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/blather/stanza/presence/subscription.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stanza 3 | class Presence 4 | 5 | # # Subscription Stanza 6 | # 7 | # [RFC 3921 Section 8 - Integration of Roster Items and Presence Subscriptions](http://xmpp.org/rfcs/rfc3921.html#rfc.section.8) 8 | # 9 | # Blather handles subscription request/response through this class. It 10 | # provides a set of helper methods to quickly transform the stanza into a 11 | # response. 12 | # 13 | # @handler :subscription 14 | class Subscription < Presence 15 | register :subscription, :subscription 16 | 17 | # Create a new Subscription stanza 18 | # 19 | # @param [Blather::JID, #to_s] to the JID to subscribe to 20 | # @param [Symbol, nil] type the subscription type 21 | def self.new(to = nil, type = nil) 22 | node = super() 23 | node.to = to 24 | node.type = type 25 | node 26 | end 27 | 28 | module InstanceMethods 29 | 30 | # Set the to value on the stanza 31 | # 32 | # @param [Blather::JID, #to_s] to a JID to subscribe to 33 | def to=(to) 34 | super JID.new(to).stripped 35 | end 36 | 37 | # Transform the stanza into an approve stanza 38 | # makes approving requests simple 39 | # 40 | # @example approve an incoming request 41 | # subscription(:request?) { |s| write_to_stream s.approve! } 42 | # @return [self] 43 | def approve! 44 | self.type = :subscribed 45 | reply_if_needed! 46 | end 47 | 48 | # Transform the stanza into a refuse stanza 49 | # makes refusing requests simple 50 | # 51 | # @example refuse an incoming request 52 | # subscription(:request?) { |s| write_to_stream s.refuse! } 53 | # @return [self] 54 | def refuse! 55 | self.type = :unsubscribed 56 | reply_if_needed! 57 | end 58 | 59 | # Transform the stanza into an unsubscribe stanza 60 | # makes unsubscribing simple 61 | # 62 | # @return [self] 63 | def unsubscribe! 64 | self.type = :unsubscribe 65 | reply_if_needed! 66 | end 67 | 68 | # Transform the stanza into a cancel stanza 69 | # makes canceling simple 70 | # 71 | # @return [self] 72 | def cancel! 73 | self.type = :unsubscribed 74 | reply_if_needed! 75 | end 76 | 77 | # Transform the stanza into a request stanza 78 | # makes requests simple 79 | # 80 | # @return [self] 81 | def request! 82 | self.type = :subscribe 83 | reply_if_needed! 84 | end 85 | 86 | # Check if the stanza is a request 87 | # 88 | # @return [true, false] 89 | def request? 90 | self.type == :subscribe 91 | end 92 | end 93 | 94 | include InstanceMethods 95 | 96 | end #Subscription 97 | 98 | end #Presence 99 | end #Stanza 100 | end 101 | -------------------------------------------------------------------------------- /spec/blather/stanza/pubsub/subscribe_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'fixtures/pubsub' 3 | 4 | describe Blather::Stanza::PubSub::Subscribe do 5 | it 'registers itself' do 6 | expect(Blather::XMPPNode.class_from_registration(:subscribe, 'http://jabber.org/protocol/pubsub')).to eq(Blather::Stanza::PubSub::Subscribe) 7 | end 8 | 9 | it 'can be imported' do 10 | expect(Blather::XMPPNode.parse(subscribe_xml)).to be_instance_of Blather::Stanza::PubSub::Subscribe 11 | end 12 | 13 | it 'ensures an subscribe node is present on create' do 14 | subscribe = Blather::Stanza::PubSub::Subscribe.new :set, 'host', 'node', 'jid' 15 | expect(subscribe.find('//ns:pubsub/ns:subscribe', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 16 | end 17 | 18 | it 'ensures an subscribe node exists when calling #subscribe' do 19 | subscribe = Blather::Stanza::PubSub::Subscribe.new :set, 'host', 'node', 'jid' 20 | subscribe.pubsub.remove_children :subscribe 21 | expect(subscribe.find('//ns:pubsub/ns:subscribe', :ns => Blather::Stanza::PubSub.registered_ns)).to be_empty 22 | 23 | expect(subscribe.subscribe).not_to be_nil 24 | expect(subscribe.find('//ns:pubsub/ns:subscribe', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 25 | end 26 | 27 | it 'defaults to a set node' do 28 | subscribe = Blather::Stanza::PubSub::Subscribe.new :set, 'host', 'node', 'jid' 29 | expect(subscribe.type).to eq(:set) 30 | end 31 | 32 | it 'sets the host if requested' do 33 | subscribe = Blather::Stanza::PubSub::Subscribe.new :set, 'pubsub.jabber.local', 'node', 'jid' 34 | expect(subscribe.to).to eq(Blather::JID.new('pubsub.jabber.local')) 35 | end 36 | 37 | it 'sets the node' do 38 | subscribe = Blather::Stanza::PubSub::Subscribe.new :set, 'host', 'node-name', 'jid' 39 | expect(subscribe.node).to eq('node-name') 40 | end 41 | 42 | it 'has a node attribute' do 43 | subscribe = Blather::Stanza::PubSub::Subscribe.new :set, 'host', 'node-name', 'jid' 44 | expect(subscribe.find('//ns:pubsub/ns:subscribe[@node="node-name"]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 45 | expect(subscribe.node).to eq('node-name') 46 | 47 | subscribe.node = 'new-node' 48 | expect(subscribe.find('//ns:pubsub/ns:subscribe[@node="new-node"]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 49 | expect(subscribe.node).to eq('new-node') 50 | end 51 | 52 | it 'has a jid attribute' do 53 | subscribe = Blather::Stanza::PubSub::Subscribe.new :set, 'host', 'node-name', 'jid' 54 | expect(subscribe.find('//ns:pubsub/ns:subscribe[@jid="jid"]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 55 | expect(subscribe.jid).to eq(Blather::JID.new('jid')) 56 | 57 | subscribe.jid = Blather::JID.new('n@d/r') 58 | expect(subscribe.find('//ns:pubsub/ns:subscribe[@jid="n@d/r"]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 59 | expect(subscribe.jid).to eq(Blather::JID.new('n@d/r')) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/blather/stanza/pubsub/items.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stanza 3 | class PubSub 4 | 5 | # # PubSub Items Stanza 6 | # 7 | # [XEP-0060 Section 6.5 - Retrieve Items from a Node](http://xmpp.org/extensions/xep-0060.html#subscriber-retrieve) 8 | # 9 | # @handler :pubsub_items 10 | class Items < PubSub 11 | register :pubsub_items, :items, self.registered_ns 12 | 13 | include Enumerable 14 | alias_method :find, :xpath 15 | 16 | # Create a new Items request 17 | # 18 | # @param [String] host the pubsub host to send the request to 19 | # @param [String] path the path of the node 20 | # @param [Array] list an array of IDs to request 21 | # @param [#to_s] max the maximum number of items to return 22 | # 23 | # @return [Blather::Stanza::PubSub::Items] 24 | def self.request(host, path, list = [], max = nil) 25 | node = self.new :get, host 26 | 27 | node.node = path 28 | node.max_items = max 29 | 30 | (list || []).each do |id| 31 | node.items_node << PubSubItem.new(id, nil, node.document) 32 | end 33 | 34 | node 35 | end 36 | 37 | # Overrides the parent to ensure an items node is created 38 | # @private 39 | def self.new(type = nil, host = nil) 40 | new_node = super 41 | new_node.items 42 | new_node 43 | end 44 | 45 | # Get the node name 46 | # 47 | # @return [String] 48 | def node 49 | items_node[:node] 50 | end 51 | 52 | # Set the node name 53 | # 54 | # @param [String, nil] node 55 | def node=(node) 56 | items_node[:node] = node 57 | end 58 | 59 | # Get the max number of items requested 60 | # 61 | # @return [Fixnum, nil] 62 | def max_items 63 | items_node[:max_items].to_i if items_node[:max_items] 64 | end 65 | 66 | # Set the max number of items requested 67 | # 68 | # @param [Fixnum, nil] max_items 69 | def max_items=(max_items) 70 | items_node[:max_items] = max_items 71 | end 72 | 73 | # Iterate over the list of items 74 | # 75 | # @yieldparam [Blather::Stanza::PubSub::PubSubItem] item 76 | def each(&block) 77 | items.each &block 78 | end 79 | 80 | # Get the list of items on this stanza 81 | # 82 | # @return [Array] 83 | def items 84 | items_node.find('ns:item', :ns => self.class.registered_ns).map do |i| 85 | PubSubItem.new(nil,nil,self.document).inherit i 86 | end 87 | end 88 | 89 | # Get or create the actual items node 90 | # 91 | # @return [Blather::XMPPNode] 92 | def items_node 93 | unless node = self.pubsub.find_first('ns:items', :ns => self.class.registered_ns) 94 | (self.pubsub << (node = XMPPNode.new('items', self.document))) 95 | node.namespace = self.pubsub.namespace 96 | end 97 | node 98 | end 99 | end # Items 100 | 101 | end # PubSub 102 | end # Stanza 103 | end # Blather 104 | -------------------------------------------------------------------------------- /spec/blather/jid_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Blather::JID do 4 | it 'does nothing if creaded from Blather::JID' do 5 | jid = Blather::JID.new 'n@d/r' 6 | expect(Blather::JID.new(jid).object_id).to eq(jid.object_id) 7 | end 8 | 9 | it 'creates a new Blather::JID from (n,d,r)' do 10 | jid = Blather::JID.new('n', 'd', 'r') 11 | expect(jid.node).to eq('n') 12 | expect(jid.domain).to eq('d') 13 | expect(jid.resource).to eq('r') 14 | end 15 | 16 | it 'creates a new Blather::JID from (n,d)' do 17 | jid = Blather::JID.new('n', 'd') 18 | expect(jid.node).to eq('n') 19 | expect(jid.domain).to eq('d') 20 | end 21 | 22 | it 'creates a new Blather::JID from (n@d)' do 23 | jid = Blather::JID.new('n@d') 24 | expect(jid.node).to eq('n') 25 | expect(jid.domain).to eq('d') 26 | end 27 | 28 | it 'creates a new Blather::JID from (n@d/r)' do 29 | jid = Blather::JID.new('n@d/r') 30 | expect(jid.node).to eq('n') 31 | expect(jid.domain).to eq('d') 32 | expect(jid.resource).to eq('r') 33 | end 34 | 35 | it 'requires at least a node' do 36 | expect { Blather::JID.new }.to raise_error ::ArgumentError 37 | end 38 | 39 | it 'ensures length of node is no more than 1023 characters' do 40 | expect { Blather::JID.new('n'*1024) }.to raise_error Blather::ArgumentError 41 | end 42 | 43 | it 'ensures length of domain is no more than 1023 characters' do 44 | expect { Blather::JID.new('n', 'd'*1024) }.to raise_error Blather::ArgumentError 45 | end 46 | 47 | it 'ensures length of resource is no more than 1023 characters' do 48 | expect { Blather::JID.new('n', 'd', 'r'*1024) }.to raise_error Blather::ArgumentError 49 | end 50 | 51 | it 'compares Blather::JIDs' do 52 | expect(Blather::JID.new('a@b/c') <=> Blather::JID.new('d@e/f')).to eq(-1) 53 | expect(Blather::JID.new('a@b/c') <=> Blather::JID.new('a@b/c')).to eq(0) 54 | expect(Blather::JID.new('d@e/f') <=> Blather::JID.new('a@b/c')).to eq(1) 55 | end 56 | 57 | it 'checks for equality' do 58 | expect(Blather::JID.new('n@d/r') == Blather::JID.new('n@d/r')).to eq(true) 59 | expect(Blather::JID.new('n@d/r').eql?(Blather::JID.new('n@d/r'))).to eq(true) 60 | end 61 | 62 | it 'will strip' do 63 | jid = Blather::JID.new('n@d/r') 64 | expect(jid.stripped).to eq(Blather::JID.new('n@d')) 65 | expect(jid).to eq(Blather::JID.new('n@d/r')) 66 | end 67 | 68 | it 'will strip itself' do 69 | jid = Blather::JID.new('n@d/r') 70 | jid.strip! 71 | expect(jid).to eq(Blather::JID.new('n@d')) 72 | end 73 | 74 | it 'has a string representation' do 75 | expect(Blather::JID.new('n@d/r').to_s).to eq('n@d/r') 76 | expect(Blather::JID.new('n', 'd', 'r').to_s).to eq('n@d/r') 77 | expect(Blather::JID.new('n', 'd').to_s).to eq('n@d') 78 | end 79 | 80 | it 'provides a #stripped? helper' do 81 | jid = Blather::JID.new 'a@b/c' 82 | expect(jid).to respond_to :stripped? 83 | expect(jid.stripped?).not_to equal true 84 | jid.strip! 85 | expect(jid.stripped?).to eq(true) 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/blather/stanza/message/muc_user.rb: -------------------------------------------------------------------------------- 1 | require 'blather/stanza/muc/muc_user_base' 2 | 3 | module Blather 4 | class Stanza 5 | class Message 6 | 7 | class MUCUser < Message 8 | include Blather::Stanza::MUC::MUCUserBase 9 | 10 | register :muc_user_message, :x, "http://jabber.org/protocol/muc#user" 11 | 12 | def self.new(to = nil, body = nil, type = :normal) 13 | super 14 | end 15 | 16 | def invite? 17 | !!find_invite_node 18 | end 19 | 20 | def invite_decline? 21 | !!find_decline_node 22 | end 23 | 24 | def invite 25 | if invite = find_invite_node 26 | Invite.new invite 27 | else 28 | muc_user << (invite = Invite.new nil, nil, nil, self.document) 29 | invite 30 | end 31 | end 32 | 33 | def find_invite_node 34 | muc_user.find_first 'ns:invite', :ns => self.class.registered_ns 35 | end 36 | 37 | def decline 38 | if decline = find_decline_node 39 | Decline.new decline 40 | else 41 | muc_user << (decline = Decline.new nil, nil, nil, self.document) 42 | decline 43 | end 44 | end 45 | 46 | def find_decline_node 47 | muc_user.find_first 'ns:decline', :ns => self.class.registered_ns 48 | end 49 | 50 | class InviteBase < XMPPNode 51 | def self.new(element_name, to = nil, from = nil, reason = nil, document = nil) 52 | new_node = super element_name, document 53 | 54 | case to 55 | when self 56 | to.document ||= document 57 | return to 58 | when Nokogiri::XML::Node 59 | new_node.inherit to 60 | when Hash 61 | new_node.to = to[:to] 62 | new_node.from = to[:from] 63 | new_node.reason = to[:reason] 64 | else 65 | new_node.to = to 66 | new_node.from = from 67 | new_node.reason = reason 68 | end 69 | new_node 70 | end 71 | 72 | def to 73 | read_attr :to 74 | end 75 | 76 | def to=(val) 77 | write_attr :to, val 78 | end 79 | 80 | def from 81 | read_attr :from 82 | end 83 | 84 | def from=(val) 85 | write_attr :from, val 86 | end 87 | 88 | def reason 89 | reason_node.content.strip 90 | end 91 | 92 | def reason=(val) 93 | reason_node.content = val 94 | end 95 | 96 | def reason_node 97 | unless reason = find_first('ns:reason', :ns => MUCUser.registered_ns) 98 | self << (reason = XMPPNode.new('reason', self.document)) 99 | end 100 | reason 101 | end 102 | end 103 | 104 | class Invite < InviteBase 105 | def self.new(*args) 106 | new_node = super :invite, *args 107 | end 108 | end 109 | 110 | class Decline < InviteBase 111 | def self.new(*args) 112 | new_node = super :decline, *args 113 | end 114 | end 115 | end # MUC 116 | 117 | end # Presence 118 | end # Stanza 119 | end # Blather 120 | -------------------------------------------------------------------------------- /lib/blather.rb: -------------------------------------------------------------------------------- 1 | # Require the necessary files 2 | %w[ 3 | rubygems 4 | eventmachine 5 | niceogiri 6 | ipaddr 7 | digest/md5 8 | digest/sha1 9 | logger 10 | openssl 11 | 12 | active_support/core_ext/class/attribute 13 | active_support/core_ext/object/blank 14 | 15 | blather/core_ext/eventmachine 16 | blather/core_ext/ipaddr 17 | 18 | blather/cert_store 19 | blather/errors 20 | blather/errors/sasl_error 21 | blather/errors/stanza_error 22 | blather/errors/stream_error 23 | blather/file_transfer 24 | blather/file_transfer/ibb 25 | blather/file_transfer/s5b 26 | blather/jid 27 | blather/roster 28 | blather/roster_item 29 | blather/xmpp_node 30 | 31 | blather/stanza 32 | blather/stanza/iq 33 | blather/stanza/iq/command 34 | blather/stanza/iq/ibb 35 | blather/stanza/iq/ping 36 | blather/stanza/iq/query 37 | blather/stanza/iq/ibr 38 | blather/stanza/iq/roster 39 | blather/stanza/iq/s5b 40 | blather/stanza/iq/si 41 | blather/stanza/iq/vcard 42 | blather/stanza/disco 43 | blather/stanza/disco/disco_info 44 | blather/stanza/disco/disco_items 45 | blather/stanza/disco/capabilities 46 | blather/stanza/message 47 | blather/stanza/message/muc_user 48 | blather/stanza/presence 49 | blather/stanza/presence/c 50 | blather/stanza/presence/status 51 | blather/stanza/presence/subscription 52 | blather/stanza/presence/muc 53 | blather/stanza/presence/muc_user 54 | 55 | blather/stanza/pubsub 56 | blather/stanza/pubsub/affiliations 57 | blather/stanza/pubsub/create 58 | blather/stanza/pubsub/event 59 | blather/stanza/pubsub/items 60 | blather/stanza/pubsub/publish 61 | blather/stanza/pubsub/retract 62 | blather/stanza/pubsub/subscribe 63 | blather/stanza/pubsub/subscription 64 | blather/stanza/pubsub/subscriptions 65 | blather/stanza/pubsub/unsubscribe 66 | 67 | blather/stanza/pubsub_owner 68 | blather/stanza/pubsub_owner/delete 69 | blather/stanza/pubsub_owner/purge 70 | 71 | blather/stanza/x 72 | 73 | blather/stream 74 | blather/stream/client 75 | blather/stream/component 76 | blather/stream/parser 77 | blather/stream/features 78 | blather/stream/features/resource 79 | blather/stream/features/sasl 80 | blather/stream/features/session 81 | blather/stream/features/tls 82 | blather/stream/features/register 83 | ].each { |r| require r } 84 | 85 | module Blather 86 | @@logger = nil 87 | 88 | class << self 89 | 90 | # Default logger level. Any internal call to log() will forward the log message to 91 | # the default log level 92 | attr_accessor :default_log_level 93 | 94 | def logger 95 | @@logger ||= Logger.new($stdout).tap {|logger| logger.level = Logger::INFO } 96 | end 97 | 98 | def logger=(logger) 99 | @@logger = logger 100 | end 101 | 102 | def default_log_level 103 | @default_log_level ||= :debug # by default is debug (as it used to be) 104 | end 105 | 106 | def log(message) 107 | logger.send self.default_log_level, message 108 | end 109 | 110 | end 111 | 112 | end 113 | -------------------------------------------------------------------------------- /spec/blather/stanza/presence/muc_user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | def muc_user_xml 4 | <<-XML 5 | 8 | 9 | 12 | 13 | 14 | foobar 15 | 16 | 17 | XML 18 | end 19 | 20 | describe 'Blather::Stanza::Presence::MUCUser' do 21 | it 'must be importable' do 22 | muc_user = Blather::XMPPNode.parse(muc_user_xml) 23 | expect(muc_user).to be_kind_of Blather::Stanza::Presence::MUCUser::InstanceMethods 24 | expect(muc_user.affiliation).to eq(:none) 25 | expect(muc_user.jid).to eq('hag66@shakespeare.lit/pda') 26 | expect(muc_user.role).to eq(:participant) 27 | expect(muc_user.status_codes).to eq([100, 110]) 28 | expect(muc_user.password).to eq('foobar') 29 | end 30 | 31 | it 'ensures a form node is present on create' do 32 | c = Blather::Stanza::Presence::MUCUser.new 33 | expect(c.xpath('ns:x', :ns => Blather::Stanza::Presence::MUCUser.registered_ns)).not_to be_empty 34 | end 35 | 36 | it 'ensures a form node exists when calling #muc' do 37 | c = Blather::Stanza::Presence::MUCUser.new 38 | c.remove_children :x 39 | expect(c.xpath('ns:x', :ns => Blather::Stanza::Presence::MUCUser.registered_ns)).to be_empty 40 | 41 | expect(c.muc_user).not_to be_nil 42 | expect(c.xpath('ns:x', :ns => Blather::Stanza::Presence::MUCUser.registered_ns)).not_to be_empty 43 | end 44 | 45 | it "must be able to set the affiliation" do 46 | muc_user = Blather::Stanza::Presence::MUCUser.new 47 | expect(muc_user.affiliation).to eq(nil) 48 | muc_user.affiliation = :none 49 | expect(muc_user.affiliation).to eq(:none) 50 | end 51 | 52 | it "must be able to set the role" do 53 | muc_user = Blather::Stanza::Presence::MUCUser.new 54 | expect(muc_user.role).to eq(nil) 55 | muc_user.role = :participant 56 | expect(muc_user.role).to eq(:participant) 57 | end 58 | 59 | it "must be able to set the jid" do 60 | muc_user = Blather::Stanza::Presence::MUCUser.new 61 | expect(muc_user.jid).to eq(nil) 62 | muc_user.jid = 'foo@bar.com' 63 | expect(muc_user.jid).to eq('foo@bar.com') 64 | end 65 | 66 | it "must be able to set the status codes" do 67 | muc_user = Blather::Stanza::Presence::MUCUser.new 68 | expect(muc_user.status_codes).to eq([]) 69 | muc_user.status_codes = [100, 110] 70 | expect(muc_user.status_codes).to eq([100, 110]) 71 | muc_user.status_codes = [500] 72 | expect(muc_user.status_codes).to eq([500]) 73 | end 74 | 75 | it "must be able to set the password" do 76 | muc_user = Blather::Stanza::Presence::MUCUser.new 77 | expect(muc_user.password).to eq(nil) 78 | muc_user.password = 'barbaz' 79 | expect(muc_user.password).to eq('barbaz') 80 | muc_user.password = 'hello_world' 81 | expect(muc_user.password).to eq('hello_world') 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/blather/stanza/pubsub/retract_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'fixtures/pubsub' 3 | 4 | describe Blather::Stanza::PubSub::Retract do 5 | it 'registers itself' do 6 | expect(Blather::XMPPNode.class_from_registration(:retract, 'http://jabber.org/protocol/pubsub')).to eq(Blather::Stanza::PubSub::Retract) 7 | end 8 | 9 | it 'can be imported' do 10 | expect(Blather::XMPPNode.parse(retract_xml)).to be_instance_of Blather::Stanza::PubSub::Retract 11 | end 12 | 13 | it 'ensures an retract node is present on create' do 14 | retract = Blather::Stanza::PubSub::Retract.new 15 | expect(retract.find('//ns:pubsub/ns:retract', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 16 | end 17 | 18 | it 'ensures an retract node exists when calling #retract' do 19 | retract = Blather::Stanza::PubSub::Retract.new 20 | retract.pubsub.remove_children :retract 21 | expect(retract.find('//ns:pubsub/ns:retract', :ns => Blather::Stanza::PubSub.registered_ns)).to be_empty 22 | 23 | expect(retract.retract).not_to be_nil 24 | expect(retract.find('//ns:pubsub/ns:retract', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 25 | end 26 | 27 | it 'defaults to a set node' do 28 | retract = Blather::Stanza::PubSub::Retract.new 29 | expect(retract.type).to eq(:set) 30 | end 31 | 32 | it 'sets the host if requested' do 33 | retract = Blather::Stanza::PubSub::Retract.new 'pubsub.jabber.local' 34 | expect(retract.to).to eq(Blather::JID.new('pubsub.jabber.local')) 35 | end 36 | 37 | it 'sets the node' do 38 | retract = Blather::Stanza::PubSub::Retract.new 'host', 'node-name' 39 | expect(retract.node).to eq('node-name') 40 | end 41 | 42 | it 'can set the retractions as a string' do 43 | retract = Blather::Stanza::PubSub::Retract.new 'host', 'node' 44 | retract.retractions = 'id1' 45 | expect(retract.xpath('//ns:retract[ns:item[@id="id1"]]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 46 | end 47 | 48 | it 'can set the retractions as an array' do 49 | retract = Blather::Stanza::PubSub::Retract.new 'host', 'node' 50 | retract.retractions = %w[id1 id2] 51 | expect(retract.xpath('//ns:retract[ns:item[@id="id1"] and ns:item[@id="id2"]]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 52 | end 53 | 54 | it 'will iterate over each item' do 55 | retract = Blather::Stanza::PubSub::Retract.new.inherit parse_stanza(retract_xml).root 56 | expect(retract.retractions.size).to eq(1) 57 | expect(retract.size).to eq(retract.retractions.size) 58 | expect(retract.retractions).to eq(%w[ae890ac52d0df67ed7cfdf51b644e901]) 59 | end 60 | 61 | it 'has a node attribute' do 62 | retract = Blather::Stanza::PubSub::Retract.new 63 | expect(retract).to respond_to :node 64 | expect(retract.node).to be_nil 65 | retract.node = 'node-name' 66 | expect(retract.node).to eq('node-name') 67 | expect(retract.xpath('//ns:retract[@node="node-name"]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 68 | end 69 | 70 | it 'will iterate over each retraction' do 71 | Blather::XMPPNode.parse(retract_xml).each do |i| 72 | expect(i).to include "ae890ac52d0df67ed7cfdf51b644e901" 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /spec/blather/stanza/pubsub/subscriptions_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'fixtures/pubsub' 3 | 4 | def control_subscriptions 5 | { :subscribed => [{:node => 'node1', :jid => 'francisco@denmark.lit', :subid => 'fd8237yr872h3f289j2'}, {:node => 'node2', :jid => 'francisco@denmark.lit', :subid => 'h8394hf8923ju'}], 6 | :unconfigured => [{:node => 'node3', :jid => 'francisco@denmark.lit'}], 7 | :pending => [{:node => 'node4', :jid => 'francisco@denmark.lit'}], 8 | :none => [{:node => 'node5', :jid => 'francisco@denmark.lit'}] } 9 | end 10 | 11 | describe Blather::Stanza::PubSub::Subscriptions do 12 | it 'registers itself' do 13 | expect(Blather::XMPPNode.class_from_registration(:subscriptions, 'http://jabber.org/protocol/pubsub')).to eq(Blather::Stanza::PubSub::Subscriptions) 14 | end 15 | 16 | it 'can be imported' do 17 | expect(Blather::XMPPNode.parse(subscriptions_xml)).to be_instance_of Blather::Stanza::PubSub::Subscriptions 18 | end 19 | 20 | it 'ensures an subscriptions node is present on create' do 21 | subscriptions = Blather::Stanza::PubSub::Subscriptions.new 22 | expect(subscriptions.find('//ns:pubsub/ns:subscriptions', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 23 | end 24 | 25 | it 'ensures an subscriptions node exists when calling #subscriptions' do 26 | subscriptions = Blather::Stanza::PubSub::Subscriptions.new 27 | subscriptions.pubsub.remove_children :subscriptions 28 | expect(subscriptions.find('//ns:pubsub/ns:subscriptions', :ns => Blather::Stanza::PubSub.registered_ns)).to be_empty 29 | 30 | expect(subscriptions.subscriptions).not_to be_nil 31 | expect(subscriptions.find('//ns:pubsub/ns:subscriptions', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 32 | end 33 | 34 | it 'ensures the subscriptions node is not duplicated when calling #subscriptions' do 35 | subscriptions = Blather::Stanza::PubSub::Subscriptions.new 36 | subscriptions.pubsub.remove_children :subscriptions 37 | expect(subscriptions.find('//ns:pubsub/ns:subscriptions', :ns => Blather::Stanza::PubSub.registered_ns)).to be_empty 38 | 39 | 5.times { subscriptions.subscriptions } 40 | expect(subscriptions.find('//ns:pubsub/ns:subscriptions', :ns => Blather::Stanza::PubSub.registered_ns).count).to eq(1) 41 | end 42 | 43 | it 'defaults to a get node' do 44 | aff = Blather::Stanza::PubSub::Subscriptions.new 45 | expect(aff.type).to eq(:get) 46 | end 47 | 48 | it 'sets the host if requested' do 49 | aff = Blather::Stanza::PubSub::Subscriptions.new :get, 'pubsub.jabber.local' 50 | expect(aff.to).to eq(Blather::JID.new('pubsub.jabber.local')) 51 | end 52 | 53 | it 'can import a subscriptions result node' do 54 | node = parse_stanza(subscriptions_xml).root 55 | 56 | subscriptions = Blather::Stanza::PubSub::Subscriptions.new.inherit node 57 | expect(subscriptions.size).to eq(4) 58 | expect(subscriptions.list).to eq(control_subscriptions) 59 | end 60 | 61 | it 'will iterate over each subscription' do 62 | node = parse_stanza(subscriptions_xml).root 63 | subscriptions = Blather::Stanza::PubSub::Subscriptions.new.inherit node 64 | subscriptions.each do |type, nodes| 65 | expect(nodes).to eq(control_subscriptions[type]) 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/blather/stream/parser.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stream 3 | 4 | # @private 5 | class Parser < Nokogiri::XML::SAX::Document 6 | @@debug = false 7 | def self.debug; @@debug; end 8 | def self.debug=(debug); @@debug = debug; end 9 | 10 | def initialize(receiver) 11 | @receiver = receiver 12 | @current = nil 13 | @namespaces = {} 14 | @namespace_definitions = [] 15 | @parser = Nokogiri::XML::SAX::PushParser.new self 16 | @parser.options = Nokogiri::XML::ParseOptions::NOENT 17 | end 18 | 19 | def receive_data(string) 20 | Blather.log "PARSING: (#{string})" if @@debug 21 | @parser << string 22 | self 23 | rescue Nokogiri::XML::SyntaxError => e 24 | error e.message 25 | end 26 | alias_method :<<, :receive_data 27 | 28 | def start_element_namespace(elem, attrs, prefix, uri, namespaces) 29 | Blather.log "START ELEM: (#{{:elem => elem, :attrs => attrs, :prefix => prefix, :uri => uri, :ns => namespaces}.inspect})" if @@debug 30 | 31 | args = [elem] 32 | args << @current.document if @current 33 | node = XMPPNode.new *args 34 | node.document.root = node unless @current 35 | 36 | ns_keys = namespaces.map { |pre, href| pre } 37 | @namespace_definitions.push [] 38 | namespaces.each do |pre, href| 39 | next if @namespace_definitions.flatten.include?(@namespaces[[pre, href]]) 40 | ns = node.add_namespace(pre, href) 41 | @namespaces[[pre, href]] ||= ns 42 | end 43 | @namespaces[[prefix, uri]] ||= node.add_namespace(prefix, uri) if prefix && !ns_keys.include?(prefix) 44 | node.namespace = @namespaces[[prefix, uri]] 45 | 46 | attrs.each do |attr| 47 | node["#{attr.prefix + ':' if attr.prefix}#{attr.localname}"] = attr.value 48 | end 49 | 50 | unless @receiver.stopped? 51 | @current << node if @current 52 | @current = node 53 | end 54 | 55 | deliver(node) if elem == 'stream' 56 | end 57 | 58 | def end_element_namespace(elem, prefix, uri) 59 | Blather.log "END ELEM: #{{:elem => elem, :prefix => prefix, :uri => uri}.inspect}" if @@debug 60 | 61 | if elem == 'stream' 62 | node = XMPPNode.new('end') 63 | node.namespace = {prefix => uri} 64 | deliver node 65 | elsif @current.parent != @current.document 66 | @namespace_definitions.pop 67 | @current = @current.parent 68 | else 69 | deliver @current 70 | end 71 | end 72 | 73 | def characters(chars = '') 74 | Blather.log "CHARS: #{chars}" if @@debug 75 | @current << Nokogiri::XML::Text.new(chars, @current.document) if @current 76 | end 77 | 78 | def warning(msg) 79 | Blather.log "PARSE WARNING: #{msg}" if @@debug 80 | end 81 | 82 | def error(msg) 83 | raise ParseError.new(msg) 84 | end 85 | 86 | def finish 87 | @parser.finish 88 | rescue ParseError, RuntimeError 89 | end 90 | 91 | private 92 | def deliver(node) 93 | @current, @namespaces, @namespace_definitions = nil, {}, [] 94 | @receiver.receive node 95 | end 96 | end #Parser 97 | 98 | end #Stream 99 | end #Blather 100 | -------------------------------------------------------------------------------- /lib/blather/stanza/pubsub/publish.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stanza 3 | class PubSub 4 | 5 | # # PubSub Publish Stanza 6 | # 7 | # [XEP-0060 Section 7.1 - Publish an Item to a Node](http://xmpp.org/extensions/xep-0060.html#publisher-publish) 8 | # 9 | # @handler :pubsub_publish 10 | class Publish < PubSub 11 | register :pubsub_publish, :publish, self.registered_ns 12 | 13 | include Enumerable 14 | alias_method :find, :xpath 15 | 16 | # Create a new publish node 17 | # 18 | # @param [String, nil] host the host to pushlish the node to 19 | # @param [String, nil] node the name of the node to publish to 20 | # @param [Blather::Stanza::Iq::VALID_TYPES] type the node type 21 | # @param [#to_s] payload the payload to publish see {#payload=} 22 | def self.new(host = nil, node = nil, type = :set, payload = nil) 23 | new_node = super(type, host) 24 | new_node.node = node 25 | new_node.payload = payload if payload 26 | new_node 27 | end 28 | 29 | # Set the payload to publish 30 | # 31 | # @overload payload=(hash) 32 | # Set the payload as a set of ID => payload entries 33 | # @param [Hash payload>] hash 34 | # @overload payload=(array) 35 | # Set the list of payloads all at once 36 | # @param [Array<#to_s>] array 37 | # @overload payload=(string) 38 | # Set the payload as a string 39 | # @param [#to_s] string 40 | def payload=(payload) 41 | payload = case payload 42 | when Hash then payload.to_a 43 | when Array then payload.map { |v| [nil, v] } 44 | else [[nil, payload]] 45 | end 46 | payload.each do |id, value| 47 | self.publish << PubSubItem.new(id, value, self.document) 48 | end 49 | end 50 | 51 | # Get the name of the node to publish to 52 | # 53 | # @return [String, nil] 54 | def node 55 | publish[:node] 56 | end 57 | 58 | # Set the name of the node to publish to 59 | # 60 | # @param [String, nil] node 61 | def node=(node) 62 | publish[:node] = node 63 | end 64 | 65 | # Get or create the actual publish node 66 | # 67 | # @return [Blather::XMPPNode] 68 | def publish 69 | unless publish = pubsub.find_first('ns:publish', :ns => self.class.registered_ns) 70 | self.pubsub << (publish = XMPPNode.new('publish', self.document)) 71 | publish.namespace = self.pubsub.namespace 72 | end 73 | publish 74 | end 75 | 76 | # Get the list of items 77 | # 78 | # @return [Array] 79 | def items 80 | publish.find('ns:item', :ns => self.class.registered_ns).map do |i| 81 | PubSubItem.new(nil,nil,self.document).inherit i 82 | end 83 | end 84 | 85 | # Iterate over the list of items 86 | # 87 | # @yield [item] a block to accept each item 88 | # @yieldparam [Blather::Stanza::PubSub::PubSubItem] 89 | def each(&block) 90 | items.each &block 91 | end 92 | 93 | # Get the size of the items list 94 | # 95 | # @return [Fixnum] 96 | def size 97 | items.size 98 | end 99 | end # Publish 100 | 101 | end # PubSub 102 | end # Stanza 103 | end # Blather 104 | -------------------------------------------------------------------------------- /spec/blather/stanza/pubsub/items_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'fixtures/pubsub' 3 | 4 | describe Blather::Stanza::PubSub::Items do 5 | it 'registers itself' do 6 | expect(Blather::XMPPNode.class_from_registration(:items, 'http://jabber.org/protocol/pubsub')).to eq(Blather::Stanza::PubSub::Items) 7 | end 8 | 9 | it 'can be imported' do 10 | expect(Blather::XMPPNode.parse(items_all_nodes_xml)).to be_instance_of Blather::Stanza::PubSub::Items 11 | end 12 | 13 | it 'ensures an items node is present on create' do 14 | items = Blather::Stanza::PubSub::Items.new 15 | expect(items.find('//ns:pubsub/ns:items', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 16 | end 17 | 18 | it 'ensures an items node exists when calling #items' do 19 | items = Blather::Stanza::PubSub::Items.new 20 | items.pubsub.remove_children :items 21 | expect(items.find('//ns:pubsub/ns:items', :ns => Blather::Stanza::PubSub.registered_ns)).to be_empty 22 | 23 | expect(items.items).not_to be_nil 24 | expect(items.find('//ns:pubsub/ns:items', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 25 | end 26 | 27 | it 'defaults to a get node' do 28 | aff = Blather::Stanza::PubSub::Items.new 29 | expect(aff.type).to eq(:get) 30 | end 31 | 32 | it 'ensures newly inherited items are PubSubItem objects' do 33 | items = Blather::XMPPNode.parse(items_all_nodes_xml) 34 | expect(items.map { |i| i.class }.uniq).to eq([Blather::Stanza::PubSub::PubSubItem]) 35 | end 36 | 37 | it 'will iterate over each item' do 38 | n = parse_stanza items_all_nodes_xml 39 | items = Blather::Stanza::PubSub::Items.new.inherit n.root 40 | count = 0 41 | items.each { |i| expect(i).to be_instance_of Blather::Stanza::PubSub::PubSubItem; count += 1 } 42 | expect(count).to eq(4) 43 | end 44 | 45 | it 'can create an items request node to request all items' do 46 | host = 'pubsub.jabber.local' 47 | node = 'princely_musings' 48 | 49 | items = Blather::Stanza::PubSub::Items.request host, node 50 | expect(items.find("//ns:items[@node=\"#{node}\"]", :ns => Blather::Stanza::PubSub.registered_ns).size).to eq(1) 51 | expect(items.to).to eq(Blather::JID.new(host)) 52 | expect(items.node).to eq(node) 53 | end 54 | 55 | it 'can create an items request node to request some items' do 56 | host = 'pubsub.jabber.local' 57 | node = 'princely_musings' 58 | items = %w[item1 item2] 59 | 60 | items_xpath = items.map { |i| "@id=\"#{i}\"" } * ' or ' 61 | 62 | items = Blather::Stanza::PubSub::Items.request host, node, items 63 | expect(items.find("//ns:items[@node=\"#{node}\"]/ns:item[#{items_xpath}]", :ns => Blather::Stanza::PubSub.registered_ns).size).to eq(2) 64 | expect(items.to).to eq(Blather::JID.new(host)) 65 | expect(items.node).to eq(node) 66 | end 67 | 68 | it 'can create an items request node to request "max_number" of items' do 69 | host = 'pubsub.jabber.local' 70 | node = 'princely_musings' 71 | max = 3 72 | 73 | items = Blather::Stanza::PubSub::Items.request host, node, nil, max 74 | expect(items.find("//ns:pubsub/ns:items[@node=\"#{node}\" and @max_items=\"#{max}\"]", :ns => Blather::Stanza::PubSub.registered_ns).size).to eq(1) 75 | expect(items.to).to eq(Blather::JID.new(host)) 76 | expect(items.node).to eq(node) 77 | expect(items.max_items).to eq(max) 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /spec/blather/stanza/pubsub/publish_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'fixtures/pubsub' 3 | 4 | describe Blather::Stanza::PubSub::Publish do 5 | it 'registers itself' do 6 | expect(Blather::XMPPNode.class_from_registration(:publish, 'http://jabber.org/protocol/pubsub')).to eq(Blather::Stanza::PubSub::Publish) 7 | end 8 | 9 | it 'can be imported' do 10 | expect(Blather::XMPPNode.parse(publish_xml)).to be_instance_of Blather::Stanza::PubSub::Publish 11 | end 12 | 13 | it 'ensures an publish node is present on create' do 14 | publish = Blather::Stanza::PubSub::Publish.new 15 | expect(publish.find('//ns:pubsub/ns:publish', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 16 | end 17 | 18 | it 'ensures an publish node exists when calling #publish' do 19 | publish = Blather::Stanza::PubSub::Publish.new 20 | publish.pubsub.remove_children :publish 21 | expect(publish.find('//ns:pubsub/ns:publish', :ns => Blather::Stanza::PubSub.registered_ns)).to be_empty 22 | 23 | expect(publish.publish).not_to be_nil 24 | expect(publish.find('//ns:pubsub/ns:publish', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 25 | end 26 | 27 | it 'defaults to a set node' do 28 | publish = Blather::Stanza::PubSub::Publish.new 29 | expect(publish.type).to eq(:set) 30 | end 31 | 32 | it 'sets the host if requested' do 33 | publish = Blather::Stanza::PubSub::Publish.new 'pubsub.jabber.local' 34 | expect(publish.to).to eq(Blather::JID.new('pubsub.jabber.local')) 35 | end 36 | 37 | it 'sets the node' do 38 | publish = Blather::Stanza::PubSub::Publish.new 'host', 'node-name' 39 | expect(publish.node).to eq('node-name') 40 | end 41 | 42 | it 'will iterate over each item' do 43 | publish = Blather::Stanza::PubSub::Publish.new.inherit parse_stanza(publish_xml).root 44 | count = 0 45 | publish.each do |i| 46 | expect(i).to be_instance_of Blather::Stanza::PubSub::PubSubItem 47 | count += 1 48 | end 49 | expect(count).to eq(1) 50 | end 51 | 52 | it 'has a node attribute' do 53 | publish = Blather::Stanza::PubSub::Publish.new 54 | expect(publish).to respond_to :node 55 | expect(publish.node).to be_nil 56 | publish.node = 'node-name' 57 | expect(publish.node).to eq('node-name') 58 | expect(publish.xpath('//ns:publish[@node="node-name"]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 59 | end 60 | 61 | it 'can set the payload with a hash' do 62 | payload = {'id1' => 'payload1', 'id2' => 'payload2'} 63 | publish = Blather::Stanza::PubSub::Publish.new 64 | publish.payload = payload 65 | expect(publish.size).to eq(2) 66 | expect(publish.xpath('/iq/ns:pubsub/ns:publish[ns:item[@id="id1" and .="payload1"] and ns:item[@id="id2" and .="payload2"]]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 67 | end 68 | 69 | it 'can set the payload with an array' do 70 | payload = %w[payload1 payload2] 71 | publish = Blather::Stanza::PubSub::Publish.new 72 | publish.payload = payload 73 | expect(publish.size).to eq(2) 74 | expect(publish.xpath('/iq/ns:pubsub/ns:publish[ns:item[.="payload1"] and ns:item[.="payload2"]]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 75 | end 76 | 77 | it 'can set the payload with a string' do 78 | publish = Blather::Stanza::PubSub::Publish.new 79 | publish.payload = 'payload' 80 | expect(publish.size).to eq(1) 81 | expect(publish.xpath('/iq/ns:pubsub/ns:publish[ns:item[.="payload"]]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/blather/stanza/presence/subscription_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Blather::Stanza::Presence::Subscription do 4 | it 'registers itself' do 5 | expect(Blather::XMPPNode.class_from_registration(:subscription, nil)).to eq(Blather::Stanza::Presence::Subscription) 6 | end 7 | 8 | [:subscribe, :subscribed, :unsubscribe, :unsubscribed].each do |type| 9 | it "must be importable as #{type}" do 10 | expect(Blather::XMPPNode.parse("")).to be_kind_of Blather::Stanza::Presence::Subscription::InstanceMethods 11 | end 12 | end 13 | 14 | it 'can set to on creation' do 15 | sub = Blather::Stanza::Presence::Subscription.new 'a@b' 16 | expect(sub.to.to_s).to eq('a@b') 17 | end 18 | 19 | it 'can set a type on creation' do 20 | sub = Blather::Stanza::Presence::Subscription.new nil, :subscribed 21 | expect(sub.type).to eq(:subscribed) 22 | end 23 | 24 | it 'strips Blather::JIDs when setting #to' do 25 | sub = Blather::Stanza::Presence::Subscription.new 'a@b/c' 26 | expect(sub.to.to_s).to eq('a@b') 27 | end 28 | 29 | it 'generates an approval using #approve!' do 30 | sub = Blather::Stanza.import Nokogiri::XML('').root 31 | sub.approve! 32 | expect(sub.to).to eq('a@b') 33 | expect(sub.type).to eq(:subscribed) 34 | end 35 | 36 | it 'generates a refusal using #refuse!' do 37 | jid = Blather::JID.new 'a@b' 38 | sub = Blather::Stanza::Presence::Subscription.new 39 | sub.from = jid 40 | sub.refuse! 41 | expect(sub.to).to eq(jid) 42 | expect(sub.type).to eq(:unsubscribed) 43 | end 44 | 45 | it 'generates an unsubscript using #unsubscribe!' do 46 | jid = Blather::JID.new 'a@b' 47 | sub = Blather::Stanza::Presence::Subscription.new 48 | sub.from = jid 49 | sub.unsubscribe! 50 | expect(sub.to).to eq(jid) 51 | expect(sub.type).to eq(:unsubscribe) 52 | end 53 | 54 | it 'generates a cancellation using #cancel!' do 55 | jid = Blather::JID.new 'a@b' 56 | sub = Blather::Stanza::Presence::Subscription.new 57 | sub.from = jid 58 | sub.cancel! 59 | expect(sub.to).to eq(jid) 60 | expect(sub.type).to eq(:unsubscribed) 61 | end 62 | 63 | it 'generates a request using #request!' do 64 | jid = Blather::JID.new 'a@b' 65 | sub = Blather::Stanza::Presence::Subscription.new 66 | sub.from = jid 67 | sub.request! 68 | expect(sub.to).to eq(jid) 69 | expect(sub.type).to eq(:subscribe) 70 | end 71 | 72 | it 'has a #request? helper' do 73 | sub = Blather::Stanza::Presence::Subscription.new 74 | expect(sub).to respond_to :request? 75 | sub.type = :subscribe 76 | expect(sub.request?).to eq(true) 77 | end 78 | 79 | it "successfully routes chained actions" do 80 | from = Blather::JID.new("foo@bar.com") 81 | to = Blather::JID.new("baz@quux.com") 82 | sub = Blather::Stanza::Presence::Subscription.new 83 | sub.from = from 84 | sub.to = to 85 | sub.cancel! 86 | sub.unsubscribe! 87 | expect(sub.type).to eq(:unsubscribe) 88 | expect(sub.to).to eq(from) 89 | expect(sub.from).to eq(to) 90 | end 91 | 92 | it "will inherit only another node's attributes" do 93 | inheritable = Blather::XMPPNode.new 'foo' 94 | inheritable[:bar] = 'baz' 95 | 96 | sub = Blather::Stanza::Presence::Subscription.new 97 | expect(sub).to respond_to :inherit 98 | 99 | sub.inherit inheritable 100 | expect(sub[:bar]).to eq('baz') 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /spec/blather/stanza/iq/si_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | def si_xml 4 | <<-XML 5 | 6 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | XML 26 | end 27 | 28 | describe Blather::Stanza::Iq::Si do 29 | it 'registers itself' do 30 | expect(Blather::XMPPNode.class_from_registration(:si, 'http://jabber.org/protocol/si')).to eq(Blather::Stanza::Iq::Si) 31 | end 32 | 33 | it 'can be imported' do 34 | node = Blather::XMPPNode.parse si_xml 35 | expect(node).to be_instance_of Blather::Stanza::Iq::Si 36 | expect(node.si).to be_instance_of Blather::Stanza::Iq::Si::Si 37 | end 38 | 39 | it 'ensures a si node is present on create' do 40 | iq = Blather::Stanza::Iq::Si.new 41 | expect(iq.xpath('ns:si', :ns => 'http://jabber.org/protocol/si')).not_to be_empty 42 | end 43 | 44 | it 'ensures a si node exists when calling #si' do 45 | iq = Blather::Stanza::Iq::Si.new 46 | iq.si.remove 47 | expect(iq.xpath('ns:si', :ns => 'http://jabber.org/protocol/si')).to be_empty 48 | 49 | expect(iq.si).not_to be_nil 50 | expect(iq.xpath('ns:si', :ns => 'http://jabber.org/protocol/si')).not_to be_empty 51 | end 52 | 53 | it 'ensures a si node is replaced when calling #si=' do 54 | iq = Blather::XMPPNode.parse si_xml 55 | 56 | new_si = Blather::Stanza::Iq::Si::Si.new 57 | new_si.id = 'a1' 58 | 59 | iq.si = new_si 60 | 61 | expect(iq.xpath('ns:si', :ns => 'http://jabber.org/protocol/si').size).to eq(1) 62 | expect(iq.si.id).to eq('a1') 63 | end 64 | end 65 | 66 | describe Blather::Stanza::Iq::Si::Si do 67 | it 'can set and get attributes' do 68 | si = Blather::Stanza::Iq::Si::Si.new 69 | si.id = 'a1' 70 | si.mime_type = 'text/plain' 71 | si.profile = 'http://jabber.org/protocol/si/profile/file-transfer' 72 | expect(si.id).to eq('a1') 73 | expect(si.mime_type).to eq('text/plain') 74 | expect(si.profile).to eq('http://jabber.org/protocol/si/profile/file-transfer') 75 | end 76 | end 77 | 78 | describe Blather::Stanza::Iq::Si::Si::File do 79 | it 'can be initialized with name and size' do 80 | file = Blather::Stanza::Iq::Si::Si::File.new('test.txt', 123) 81 | expect(file.name).to eq('test.txt') 82 | expect(file.size).to eq(123) 83 | end 84 | 85 | it 'can be initialized with node' do 86 | node = Blather::XMPPNode.parse si_xml 87 | 88 | file = Blather::Stanza::Iq::Si::Si::File.new node.find_first('.//ns:file', :ns => 'http://jabber.org/protocol/si/profile/file-transfer') 89 | expect(file.name).to eq('test.txt') 90 | expect(file.size).to eq(1022) 91 | end 92 | 93 | it 'can set and get description' do 94 | file = Blather::Stanza::Iq::Si::Si::File.new('test.txt', 123) 95 | file.desc = 'This is a test. If this were a real file...' 96 | expect(file.desc).to eq('This is a test. If this were a real file...') 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /spec/blather/stanza/iq/vcard_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | def vcard_xml 4 | <<-XML 5 | 6 | 7 | Romeo 8 | 9 | 10 | XML 11 | end 12 | 13 | describe Blather::Stanza::Iq::Vcard do 14 | it 'registers itself' do 15 | expect(Blather::XMPPNode.class_from_registration(:vCard, 'vcard-temp')).to eq(Blather::Stanza::Iq::Vcard) 16 | end 17 | 18 | it 'can be imported' do 19 | query = Blather::XMPPNode.parse vcard_xml 20 | expect(query).to be_instance_of Blather::Stanza::Iq::Vcard 21 | expect(query.vcard).to be_instance_of Blather::Stanza::Iq::Vcard::Vcard 22 | end 23 | 24 | it 'ensures a vcard node is present on create' do 25 | query = Blather::Stanza::Iq::Vcard.new 26 | expect(query.xpath('ns:vCard', :ns => 'vcard-temp')).not_to be_empty 27 | end 28 | 29 | it 'ensures a vcard node exists when calling #vcard' do 30 | query = Blather::Stanza::Iq::Vcard.new 31 | query.vcard.remove 32 | expect(query.xpath('ns:vCard', :ns => 'vcard-temp')).to be_empty 33 | 34 | expect(query.vcard).not_to be_nil 35 | expect(query.xpath('ns:vCard', :ns => 'vcard-temp')).not_to be_empty 36 | end 37 | 38 | it 'ensures a vcard node is replaced when calling #vcard=' do 39 | query = Blather::XMPPNode.parse vcard_xml 40 | 41 | new_vcard = Blather::Stanza::Iq::Vcard::Vcard.new 42 | new_vcard["NICKNAME"] = 'Mercutio' 43 | 44 | query.vcard = new_vcard 45 | 46 | expect(query.xpath('ns:vCard', :ns => 'vcard-temp').size).to eq(1) 47 | expect(query.find_first('ns:vCard/ns:NICKNAME', :ns => 'vcard-temp').content).to eq('Mercutio') 48 | end 49 | end 50 | 51 | describe Blather::Stanza::Iq::Vcard::Vcard do 52 | it 'can set vcard elements' do 53 | query = Blather::Stanza::Iq::Vcard.new :set 54 | query.vcard['NICKNAME'] = 'Romeo' 55 | expect(query.find_first('ns:vCard/ns:NICKNAME', :ns => 'vcard-temp').content).to eq('Romeo') 56 | end 57 | 58 | it 'can set deep vcard elements' do 59 | query = Blather::Stanza::Iq::Vcard.new :set 60 | query.vcard['PHOTO/TYPE'] = 'image/png' 61 | query.vcard['PHOTO/BINVAL'] = '====' 62 | expect(query.find_first('ns:vCard/ns:PHOTO', :ns => 'vcard-temp').children.size).to eq(2) 63 | expect(query.find_first('ns:vCard/ns:PHOTO', :ns => 'vcard-temp').children.detect { |n| n.element_name == 'TYPE' && n.content == 'image/png' }).not_to be_nil 64 | expect(query.find_first('ns:vCard/ns:PHOTO', :ns => 'vcard-temp').children.detect { |n| n.element_name == 'BINVAL' && n.content == '====' }).not_to be_nil 65 | end 66 | 67 | it 'can get vcard elements' do 68 | query = Blather::Stanza::Iq::Vcard.new :set 69 | query.vcard['NICKNAME'] = 'Romeo' 70 | expect(query.vcard['NICKNAME']).to eq('Romeo') 71 | end 72 | 73 | it 'can get deep vcard elements' do 74 | query = Blather::Stanza::Iq::Vcard.new :set 75 | query.vcard['PHOTO/TYPE'] = 'image/png' 76 | query.vcard['PHOTO/BINVAL'] = '====' 77 | expect(query.vcard['PHOTO/TYPE']).to eq('image/png') 78 | expect(query.vcard['PHOTO/BINVAL']).to eq('====') 79 | end 80 | 81 | it 'returns nil on vcard elements which does not exist' do 82 | query = Blather::Stanza::Iq::Vcard.new :set 83 | query.vcard['NICKNAME'] = 'Romeo' 84 | expect(query.vcard['FN']).to be_nil 85 | end 86 | 87 | it 'can update vcard elements' do 88 | query = Blather::XMPPNode.parse vcard_xml 89 | expect(query.vcard['NICKNAME']).to eq('Romeo') 90 | query.vcard['NICKNAME'] = 'Mercutio' 91 | expect(query.vcard['NICKNAME']).to eq('Mercutio') 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/blather/errors/stanza_error.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | 3 | # Stanza errors 4 | # RFC3920 Section 9.3 (http://xmpp.org/rfcs/rfc3920.html#stanzas-error) 5 | # 6 | # @handler :stanza_error 7 | class StanzaError < BlatherError 8 | # @private 9 | STANZA_ERR_NS = 'urn:ietf:params:xml:ns:xmpp-stanzas' 10 | # @private 11 | VALID_TYPES = [:cancel, :continue, :modify, :auth, :wait].freeze 12 | 13 | register :stanza_error 14 | 15 | attr_reader :original, :name, :type, :text, :extras 16 | 17 | # Factory method for instantiating the proper class for the error 18 | # 19 | # @param [Blather::XMPPNode] node the error node to import 20 | # @return [Blather::StanzaError] 21 | def self.import(node) 22 | original = node.copy 23 | original.remove_child 'error' 24 | 25 | error_node = node.find_first '//*[local-name()="error"]' 26 | 27 | name = error_node.find_first('child::*[name()!="text"]', STANZA_ERR_NS).element_name 28 | type = error_node['type'] 29 | text = node.find_first 'descendant::*[name()="text"]', STANZA_ERR_NS 30 | text = text.content if text 31 | 32 | extras = error_node.find("descendant::*[name()!='text' and name()!='#{name}']").map { |n| n } 33 | 34 | self.new original, name, type, text, extras 35 | end 36 | 37 | # Create a new StanzaError 38 | # 39 | # @param [Blather::XMPPNode] original the original stanza 40 | # @param [String] name the error name 41 | # @param [#to_s] type the error type as specified in 42 | # [RFC3920](http://xmpp.org/rfcs/rfc3920.html#rfc.section.9.3.2) 43 | # @param [String, nil] text additional text for the error 44 | # @param [Array] extras an array of extra nodes to add 45 | def initialize(original, name, type, text = nil, extras = []) 46 | @original = original 47 | @name = name 48 | self.type = type 49 | @text = text 50 | @extras = extras 51 | end 52 | 53 | # Set the error type 54 | # 55 | # @param [#to_sym] type the new error type. Must be on of 56 | # Blather::StanzaError::VALID_TYPES 57 | # @see [RFC3920 Section 9.3.2](http://xmpp.org/rfcs/rfc3920.html#rfc.section.9.3.2) 58 | def type=(type) 59 | type = type.to_sym 60 | if !VALID_TYPES.include?(type) 61 | raise ArgumentError, "Invalid Type (#{type}), use: #{VALID_TYPES*' '}" 62 | end 63 | @type = type 64 | end 65 | 66 | # The error name 67 | # 68 | # @return [Symbol] 69 | def name 70 | @name.gsub('-','_').to_sym 71 | end 72 | 73 | # Creates an XML node from the error 74 | # 75 | # @return [Blather::XMPPNode] 76 | def to_node 77 | node = self.original.reply 78 | node.type = 'error' 79 | node << (error_node = XMPPNode.new('error')) 80 | 81 | error_node << (err = XMPPNode.new(@name, error_node.document)) 82 | error_node['type'] = self.type 83 | err.namespace = 'urn:ietf:params:xml:ns:xmpp-stanzas' 84 | 85 | if self.text 86 | error_node << (text = XMPPNode.new('text', error_node.document)) 87 | text.namespace = 'urn:ietf:params:xml:ns:xmpp-stanzas' 88 | text.content = self.text 89 | end 90 | 91 | self.extras.each { |extra| error_node << extra.dup } 92 | node 93 | end 94 | 95 | # Convert the object to a proper node then convert it to a string 96 | # 97 | # @return [String] 98 | def to_xml(*args) 99 | to_node.to_xml(*args) 100 | end 101 | 102 | # @private 103 | def inspect 104 | "Stanza Error (#{@name}): #{self.text} [#{self.extras}]" 105 | end 106 | # @private 107 | alias_method :to_s, :inspect 108 | end # StanzaError 109 | 110 | end # Blather 111 | -------------------------------------------------------------------------------- /lib/blather/stanza/presence/muc_user.rb: -------------------------------------------------------------------------------- 1 | require 'blather/stanza/muc/muc_user_base' 2 | 3 | module Blather 4 | class Stanza 5 | class Presence 6 | 7 | class MUCUser < Presence 8 | include Blather::Stanza::MUC::MUCUserBase 9 | 10 | def self.decorator_modules 11 | super + [Blather::Stanza::MUC::MUCUserBase] 12 | end 13 | 14 | register :muc_user_presence, :x, MUC_USER_NAMESPACE 15 | 16 | module InstanceMethods 17 | 18 | def affiliation 19 | item.affiliation 20 | end 21 | 22 | def affiliation=(val) 23 | item.affiliation = val 24 | end 25 | 26 | def role 27 | item.role 28 | end 29 | 30 | def role=(val) 31 | item.role = val 32 | end 33 | 34 | def jid 35 | item.jid 36 | end 37 | 38 | def jid=(val) 39 | item.jid = val 40 | end 41 | 42 | def status_codes 43 | status.map &:code 44 | end 45 | 46 | def status_codes=(val) 47 | muc_user.remove_children :status 48 | val.each do |code| 49 | muc_user << Status.new(code) 50 | end 51 | end 52 | 53 | def item 54 | if item = muc_user.find_first('ns:item', :ns => MUCUser.registered_ns) 55 | Item.new item 56 | else 57 | muc_user << (item = Item.new nil, nil, nil, self.document) 58 | item 59 | end 60 | end 61 | 62 | def status 63 | muc_user.find('ns:status', :ns => MUCUser.registered_ns).map do |status| 64 | Status.new status 65 | end 66 | end 67 | end 68 | 69 | include InstanceMethods 70 | 71 | class Item < XMPPNode 72 | def self.new(affiliation = nil, role = nil, jid = nil, document = nil) 73 | new_node = super :item, document 74 | 75 | case affiliation 76 | when self 77 | affiliation.document ||= document 78 | return affiliation 79 | when Nokogiri::XML::Node 80 | new_node.inherit affiliation 81 | when Hash 82 | new_node.affiliation = affiliation[:affiliation] 83 | new_node.role = affiliation[:role] 84 | new_node.jid = affiliation[:jid] 85 | else 86 | new_node.affiliation = affiliation 87 | new_node.role = role 88 | new_node.jid = jid 89 | end 90 | new_node 91 | end 92 | 93 | def affiliation 94 | read_attr :affiliation, :to_sym 95 | end 96 | 97 | def affiliation=(val) 98 | write_attr :affiliation, val 99 | end 100 | 101 | def role 102 | read_attr :role, :to_sym 103 | end 104 | 105 | def role=(val) 106 | write_attr :role, val 107 | end 108 | 109 | def jid 110 | read_attr :jid 111 | end 112 | 113 | def jid=(val) 114 | write_attr :jid, val 115 | end 116 | end 117 | 118 | class Status < XMPPNode 119 | def self.new(code = nil) 120 | new_node = super :status 121 | 122 | case code 123 | when self.class 124 | return code 125 | when Nokogiri::XML::Node 126 | new_node.inherit code 127 | when Hash 128 | new_node.code = code[:code] 129 | else 130 | new_node.code = code 131 | end 132 | new_node 133 | end 134 | 135 | def code 136 | read_attr :code, :to_i 137 | end 138 | 139 | def code=(val) 140 | write_attr :code, val 141 | end 142 | end 143 | end # MUC 144 | 145 | end # Presence 146 | end # Stanza 147 | end # Blather 148 | -------------------------------------------------------------------------------- /lib/blather/roster.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | 3 | # Local Roster 4 | # Takes care of adding/removing JIDs through the stream 5 | class Roster 6 | include Enumerable 7 | 8 | attr_reader :version 9 | 10 | # Create a new roster 11 | # 12 | # @param [Blather::Stream] stream the stream the roster should use to 13 | # update roster entries 14 | # @param [Blather::Stanza::Iq::Roster] stanza a roster stanza used to preload 15 | # the roster 16 | # @return [Blather::Roster] 17 | def initialize(stream, stanza = nil) 18 | @stream = stream 19 | @items = {} 20 | process(stanza) if stanza 21 | end 22 | 23 | # Process any incoming stanzas and either adds or removes the 24 | # corresponding RosterItem 25 | # 26 | # @param [Blather::Stanza::Iq::Roster] stanza a roster stanza 27 | def process(stanza) 28 | @version = stanza.version 29 | stanza.items.each do |i| 30 | case i.subscription 31 | when :remove then @items.delete(key(i.jid)) 32 | else @items[key(i.jid)] = RosterItem.new(i) 33 | end 34 | end 35 | end 36 | 37 | # Pushes a JID into the roster 38 | # 39 | # @param [String, Blather::JID, #jid] elem a JID to add to the roster 40 | # @return [self] 41 | # @see #push 42 | def <<(elem) 43 | push elem 44 | self 45 | end 46 | 47 | # Push a JID into the roster and update the server 48 | # 49 | # @param [String, Blather::JID, #jid] elem a jid to add to the roster 50 | # @param [true, false] send send the update over the wire 51 | # @see Blather::JID 52 | def push(elem, send = true) 53 | jid = elem.respond_to?(:jid) && elem.jid ? elem.jid : JID.new(elem) 54 | @items[key(jid)] = node = RosterItem.new(elem) 55 | 56 | @stream.write(node.to_stanza(:set)) if send 57 | end 58 | alias_method :add, :push 59 | 60 | # Remove a JID from the roster and update the server 61 | # 62 | # @param [String, Blather::JID] jid the JID to remove from the roster 63 | def delete(jid) 64 | @items.delete key(jid) 65 | item = Stanza::Iq::Roster::RosterItem.new(jid, nil, :remove) 66 | @stream.write Stanza::Iq::Roster.new(:set, item) 67 | end 68 | alias_method :remove, :delete 69 | 70 | # Get a RosterItem by JID 71 | # 72 | # @param [String, Blather::JID] jid the jid of the item to return 73 | # @return [Blather::RosterItem, nil] the associated RosterItem 74 | def [](jid) 75 | items[key(jid)] 76 | end 77 | 78 | # Iterate over all RosterItems 79 | # 80 | # @yield [Blather::RosterItem] yields each RosterItem 81 | def each(&block) 82 | items.values.each &block 83 | end 84 | 85 | # Get a duplicate of all RosterItems 86 | # 87 | # @return [Array] a duplicate of all RosterItems 88 | def items 89 | @items.dup 90 | end 91 | 92 | # Number of items in the roster 93 | # 94 | # @return [Integer] the number of items in the roster 95 | def length 96 | @items.length 97 | end 98 | 99 | # A hash of items keyed by group 100 | # 101 | # @return [Hash Array>] 102 | def grouped 103 | @items.values.sort.inject(Hash.new{|h,k|h[k]=[]}) do |hash, item| 104 | item.groups.each { |group| hash[group] << item } 105 | hash 106 | end 107 | end 108 | 109 | private 110 | # Creates a stripped jid 111 | def self.key(jid) 112 | JID.new(jid).stripped.to_s 113 | end 114 | 115 | # Instance method to wrap around the class method 116 | def key(jid) 117 | self.class.key(jid) 118 | end 119 | end # Roster 120 | 121 | end # Blather 122 | -------------------------------------------------------------------------------- /spec/blather/roster_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Blather::Roster do 4 | before do 5 | @stream = mock() 6 | @stream.stubs(:write) 7 | 8 | @stanza = mock() 9 | items = 4.times.map { |n| Blather::Stanza::Iq::Roster::RosterItem.new(jid: "n@d/#{n}r") } 10 | @stanza.stubs(:items).returns(items) 11 | @stanza.stubs(:version).returns('24d091d0dcfab1b3') 12 | 13 | @roster = Blather::Roster.new(@stream, @stanza) 14 | end 15 | 16 | it 'initializes with items' do 17 | expect(@roster.items.map { |_,i| i.jid.to_s }).to eq(@stanza.items.map { |i| i.jid.stripped.to_s }.uniq) 18 | end 19 | 20 | it 'processes @stanzas with remove requests' do 21 | s = @roster['n@d/0r'] 22 | s.subscription = :remove 23 | expect { @roster.process(s.to_stanza) }.to change(@roster, :length).by -1 24 | end 25 | 26 | it 'processes @stanzas with add requests' do 27 | s = Blather::Stanza::Iq::Roster::RosterItem.new('a@b/c').to_stanza 28 | expect { @roster.process(s) }.to change(@roster, :length).by 1 29 | end 30 | 31 | it 'allows a jid to be pushed' do 32 | jid = 'a@b/c' 33 | expect { @roster.push(jid) }.to change(@roster, :length).by 1 34 | expect(@roster[jid]).not_to be_nil 35 | end 36 | 37 | it 'allows an item to be pushed' do 38 | jid = 'a@b/c' 39 | item = Blather::RosterItem.new(Blather::JID.new(jid)) 40 | expect { @roster.push(item) }.to change(@roster, :length).by 1 41 | expect(@roster[jid]).not_to be_nil 42 | end 43 | 44 | it 'aliases #<< to #push and returns self to allow for chaining' do 45 | jid = 'a@b/c' 46 | item = Blather::RosterItem.new(Blather::JID.new(jid)) 47 | jid2 = 'd@e/f' 48 | item2 = Blather::RosterItem.new(Blather::JID.new(jid2)) 49 | expect { @roster << item << item2 }.to change(@roster, :length).by 2 50 | expect(@roster[jid]).not_to be_nil 51 | expect(@roster[jid2]).not_to be_nil 52 | end 53 | 54 | it 'sends a @roster addition over the wire' do 55 | client = mock(:write => nil) 56 | roster = Blather::Roster.new client, @stanza 57 | roster.push('a@b/c') 58 | end 59 | 60 | it 'removes a Blather::JID' do 61 | expect { @roster.delete 'n@d' }.to change(@roster, :length).by -1 62 | end 63 | 64 | it 'sends a @roster removal over the wire' do 65 | client = mock(:write => nil) 66 | roster = Blather::Roster.new client, @stanza 67 | roster.delete('a@b/c') 68 | end 69 | 70 | it 'returns an item through []' do 71 | item = @roster['n@d'] 72 | expect(item).to be_kind_of Blather::RosterItem 73 | expect(item.jid).to eq(Blather::JID.new('n@d')) 74 | end 75 | 76 | it 'responds to #each' do 77 | expect(@roster).to respond_to :each 78 | end 79 | 80 | it 'cycles through all the items using #each' do 81 | expect(@roster.map { |i| i }.sort).to eq(@roster.items.values.sort) 82 | end 83 | 84 | it 'yields RosterItems from #each' do 85 | @roster.map { |i| expect(i).to be_kind_of Blather::RosterItem } 86 | end 87 | 88 | it 'returns a duplicate of items through #items' do 89 | items = @roster.items 90 | items.delete 'n@d' 91 | expect(items).not_to equal @roster.items 92 | end 93 | 94 | it 'will group roster items' do 95 | @roster.delete 'n@d' 96 | item1 = Blather::RosterItem.new("n1@d") 97 | item1.groups = ['group1', 'group2'] 98 | item2 = Blather::RosterItem.new("n2@d") 99 | item2.groups = ['group1', 'group3'] 100 | @roster << item1 << item2 101 | 102 | expect(@roster.grouped).to eq({ 103 | 'group1' => [item1, item2], 104 | 'group2' => [item1], 105 | 'group3' => [item2] 106 | }) 107 | end 108 | 109 | it 'has a version' do 110 | expect(@roster.version).to eq @stanza.version 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /spec/blather/stream/component_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Blather::Stream::Component do 4 | let(:client) { mock 'Client' } 5 | let(:server_port) { 50000 - rand(1000) } 6 | let(:jid) { 'comp.id' } 7 | 8 | before do 9 | [:unbind, :post_init, :jid=].each do |m| 10 | client.stubs(m) unless client.respond_to?(m) 11 | end 12 | client.stubs(:jid).returns jid 13 | end 14 | 15 | def mocked_server(times = nil, &block) 16 | MockServer.any_instance.expects(:receive_data).send(*(times ? [:times, times] : [:at_least, 1])).with &block 17 | EventMachine::run { 18 | # Mocked server 19 | EventMachine::start_server '127.0.0.1', server_port, ServerMock 20 | 21 | # Blather::Stream connection 22 | EM.connect('127.0.0.1', server_port, Blather::Stream::Component, client, jid, 'secret') { |c| @stream = c } 23 | } 24 | end 25 | 26 | it 'can be started' do 27 | params = [client, 'comp.id', 'secret', 'host', 1234] 28 | EM.expects(:connect).with do |*parms| 29 | parms[0] == 'host' && 30 | parms[1] == 1234 && 31 | parms[3] == client && 32 | parms[4] == 'comp.id' 33 | end 34 | 35 | Blather::Stream::Component.start *params 36 | end 37 | 38 | it 'shakes hands with the server' do 39 | state = nil 40 | mocked_server(2) do |val, server| 41 | case state 42 | when nil 43 | state = :started 44 | server.send_data "" 45 | expect(val).to match(/stream:stream/) 46 | 47 | when :started 48 | server.send_data '' 49 | EM.stop 50 | expect(val).to eq("#{Digest::SHA1.hexdigest('12345'+"secret")}") 51 | 52 | end 53 | end 54 | end 55 | 56 | it 'raises a NoConnection exception if the connection is unbound before it can be completed' do 57 | expect do 58 | EventMachine::run { 59 | EM.add_timer(0.5) { EM.stop if EM.reactor_running? } 60 | 61 | Blather::Stream::Component.start client, jid, 'pass', '127.0.0.1', 50000 - rand(1000) 62 | } 63 | end.to raise_error Blather::Stream::ConnectionFailed 64 | end 65 | 66 | it 'starts the stream once the connection is complete' do 67 | mocked_server(1) { |val, _| EM.stop; expect(val).to match(/stream:stream/) } 68 | end 69 | 70 | it 'sends stanzas to the client when the stream is ready' do 71 | client.stubs :post_init 72 | client.expects(:receive_data).with do |n| 73 | EM.stop 74 | n.kind_of? Blather::XMPPNode 75 | end 76 | 77 | state = nil 78 | mocked_server(2) do |val, server| 79 | case state 80 | when nil 81 | state = :started 82 | server.send_data "" 83 | expect(val).to match(/stream:stream/) 84 | 85 | when :started 86 | server.send_data '' 87 | server.send_data "Message!" 88 | expect(val).to eq("#{Digest::SHA1.hexdigest('12345'+"secret")}") 89 | 90 | end 91 | end 92 | end 93 | 94 | it 'sends stanzas to the wire ensuring "from" is set' do 95 | EM.expects(:next_tick).at_least(1).yields 96 | 97 | msg = Blather::Stanza::Message.new 'to@jid.com', 'body' 98 | comp = Blather::Stream::Component.new nil, client, 'jid.com', 'pass' 99 | comp.expects(:send_data).with { |s| expect(s).to match(/^]*from="jid\.com"/) } 100 | comp.send msg 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/blather/stanza/pubsub.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stanza 3 | 4 | # # Pubsub Stanza 5 | # 6 | # [XEP-0060 - Publish-Subscribe](http://xmpp.org/extensions/xep-0060.html) 7 | # 8 | # The base class for all PubSub nodes. This provides helper methods common to 9 | # all PubSub nodes. 10 | # 11 | # @handler :pubsub_node 12 | class PubSub < Iq 13 | register :pubsub_node, :pubsub, 'http://jabber.org/protocol/pubsub' 14 | 15 | # @private 16 | def self.import(node) 17 | klass = nil 18 | if pubsub = node.document.find_first('//ns:pubsub', :ns => self.registered_ns) 19 | pubsub.children.detect do |e| 20 | ns = e.namespace ? e.namespace.href : nil 21 | klass = class_from_registration(e.element_name, ns) 22 | end 23 | end 24 | (klass || self).new(node[:type]).inherit(node) 25 | end 26 | 27 | # Overwrites the parent constructor to ensure a pubsub node is present. 28 | # Also allows the addition of a host attribute 29 | # 30 | # @param [] type the IQ type 31 | # @param [String, nil] host the host the node should be sent to 32 | def self.new(type = nil, host = nil) 33 | new_node = super type 34 | new_node.to = host 35 | new_node.pubsub 36 | new_node 37 | end 38 | 39 | # Overrides the parent to ensure the current pubsub node is destroyed before 40 | # inheritting the new content 41 | # 42 | # @private 43 | def inherit(node) 44 | remove_children :pubsub 45 | super 46 | end 47 | 48 | # Get or create the pubsub node on the stanza 49 | # 50 | # @return [Blather::XMPPNode] 51 | def pubsub 52 | p = find_first('ns:pubsub', :ns => self.class.registered_ns) || 53 | find_first('pubsub', :ns => self.class.registered_ns) 54 | 55 | unless p 56 | self << (p = XMPPNode.new('pubsub', self.document)) 57 | p.namespace = self.class.registered_ns 58 | end 59 | p 60 | end 61 | end # PubSub 62 | 63 | # # PubSubItem Fragment 64 | # 65 | # This fragment is found in many places throughout the pubsub spec 66 | # This is a convenience class to attach methods to the node 67 | class PubSubItem < XMPPNode 68 | # Create a new PubSubItem 69 | # 70 | # @param [String, nil] id the id of the stanza 71 | # @param [#to_s, nil] payload the payload to attach to this item. 72 | # @param [XML::Document, nil] document the document the node should be 73 | # attached to. This should be the document of the parent PubSub node. 74 | def self.new(id = nil, payload = nil, document = nil) 75 | return id if id.class == self 76 | 77 | new_node = super 'item', document 78 | new_node.id = id 79 | new_node.payload = payload if payload 80 | new_node 81 | end 82 | 83 | # Get the item's ID 84 | # 85 | # @return [String, nil] 86 | def id 87 | read_attr :id 88 | end 89 | 90 | # Set the item's ID 91 | # 92 | # @param [#to_s] id the new ID 93 | def id=(id) 94 | write_attr :id, id 95 | end 96 | 97 | alias_method :payload_node, :child 98 | 99 | # Get the item's payload 100 | # 101 | # @return [String, nil] 102 | def payload 103 | children.empty? ? nil : children.to_s 104 | end 105 | 106 | # Set the item's payload 107 | # 108 | # @param [String, XMPPNode, nil] payload the payload 109 | def payload=(payload) 110 | children.map &:remove 111 | return unless payload 112 | if payload.is_a?(String) 113 | self.content = payload 114 | else 115 | self << payload 116 | end 117 | end 118 | end # PubSubItem 119 | 120 | end # Stanza 121 | end # Blather 122 | -------------------------------------------------------------------------------- /spec/blather/stanza/pubsub/unsubscribe_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'fixtures/pubsub' 3 | 4 | describe Blather::Stanza::PubSub::Unsubscribe do 5 | it 'registers itself' do 6 | expect(Blather::XMPPNode.class_from_registration(:unsubscribe, 'http://jabber.org/protocol/pubsub')).to eq(Blather::Stanza::PubSub::Unsubscribe) 7 | end 8 | 9 | it 'can be imported' do 10 | expect(Blather::XMPPNode.parse(unsubscribe_xml)).to be_instance_of Blather::Stanza::PubSub::Unsubscribe 11 | end 12 | 13 | it 'ensures an unsubscribe node is present on create' do 14 | unsubscribe = Blather::Stanza::PubSub::Unsubscribe.new :set, 'host', 'node', 'jid' 15 | expect(unsubscribe.find('//ns:pubsub/ns:unsubscribe', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 16 | end 17 | 18 | it 'ensures an unsubscribe node exists when calling #unsubscribe' do 19 | unsubscribe = Blather::Stanza::PubSub::Unsubscribe.new :set, 'host', 'node', 'jid' 20 | unsubscribe.pubsub.remove_children :unsubscribe 21 | expect(unsubscribe.find('//ns:pubsub/ns:unsubscribe', :ns => Blather::Stanza::PubSub.registered_ns)).to be_empty 22 | 23 | expect(unsubscribe.unsubscribe).not_to be_nil 24 | expect(unsubscribe.find('//ns:pubsub/ns:unsubscribe', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 25 | end 26 | 27 | it 'defaults to a set node' do 28 | unsubscribe = Blather::Stanza::PubSub::Unsubscribe.new :set, 'host', 'node', 'jid' 29 | expect(unsubscribe.type).to eq(:set) 30 | end 31 | 32 | it 'sets the host if requested' do 33 | unsubscribe = Blather::Stanza::PubSub::Unsubscribe.new :set, 'pubsub.jabber.local', 'node', 'jid' 34 | expect(unsubscribe.to).to eq(Blather::JID.new('pubsub.jabber.local')) 35 | end 36 | 37 | it 'sets the node' do 38 | unsubscribe = Blather::Stanza::PubSub::Unsubscribe.new :set, 'host', 'node-name', 'jid' 39 | expect(unsubscribe.node).to eq('node-name') 40 | end 41 | 42 | it 'has a node attribute' do 43 | unsubscribe = Blather::Stanza::PubSub::Unsubscribe.new :set, 'host', 'node-name', 'jid' 44 | expect(unsubscribe.find('//ns:pubsub/ns:unsubscribe[@node="node-name"]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 45 | expect(unsubscribe.node).to eq('node-name') 46 | 47 | unsubscribe.node = 'new-node' 48 | expect(unsubscribe.find('//ns:pubsub/ns:unsubscribe[@node="new-node"]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 49 | expect(unsubscribe.node).to eq('new-node') 50 | end 51 | 52 | it 'has a jid attribute' do 53 | unsubscribe = Blather::Stanza::PubSub::Unsubscribe.new :set, 'host', 'node-name', 'jid' 54 | expect(unsubscribe.find('//ns:pubsub/ns:unsubscribe[@jid="jid"]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 55 | expect(unsubscribe.jid).to eq(Blather::JID.new('jid')) 56 | 57 | unsubscribe.jid = Blather::JID.new('n@d/r') 58 | expect(unsubscribe.find('//ns:pubsub/ns:unsubscribe[@jid="n@d/r"]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 59 | expect(unsubscribe.jid).to eq(Blather::JID.new('n@d/r')) 60 | end 61 | 62 | it 'has a subid attribute' do 63 | unsubscribe = Blather::Stanza::PubSub::Unsubscribe.new :set, 'host', 'node-name', 'jid' 64 | expect(unsubscribe.find('//ns:pubsub/ns:unsubscribe[@subid="subid"]', :ns => Blather::Stanza::PubSub.registered_ns)).to be_empty 65 | 66 | unsubscribe = Blather::Stanza::PubSub::Unsubscribe.new :set, 'host', 'node-name', 'jid', 'subid' 67 | expect(unsubscribe.find('//ns:pubsub/ns:unsubscribe[@subid="subid"]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 68 | expect(unsubscribe.subid).to eq('subid') 69 | 70 | unsubscribe.subid = 'newsubid' 71 | expect(unsubscribe.find('//ns:pubsub/ns:unsubscribe[@subid="newsubid"]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty 72 | expect(unsubscribe.subid).to eq('newsubid') 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/blather/errors/stream_error_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | def stream_error_node(error = 'internal-server-error', msg = nil) 4 | node = Blather::XMPPNode.new('error') 5 | node.namespace = {'stream' => Blather::Stream::STREAM_NS} 6 | 7 | node << (err = Blather::XMPPNode.new(error, node.document)) 8 | err.namespace = 'urn:ietf:params:xml:ns:xmpp-streams' 9 | 10 | if msg 11 | node << (text = Blather::XMPPNode.new('text', node.document)) 12 | text.namespace = 'urn:ietf:params:xml:ns:xmpp-streams' 13 | text.content = msg 14 | end 15 | 16 | node << (extra = Blather::XMPPNode.new('extra-error', node.document)) 17 | extra.namespace = 'blather:stream:error' 18 | extra.content = 'Blather Error' 19 | 20 | node 21 | end 22 | 23 | describe 'Blather::StreamError' do 24 | it 'can import a node' do 25 | err = stream_error_node 'internal-server-error', 'the message' 26 | expect(Blather::StreamError).to respond_to :import 27 | e = Blather::StreamError.import err 28 | expect(e).to be_kind_of Blather::StreamError 29 | 30 | expect(e.name).to eq(:internal_server_error) 31 | expect(e.text).to eq('the message') 32 | expect(e.extras).to eq(err.find('descendant::*[name()="extra-error"]', 'blather:stream:error').map {|n|n}) 33 | end 34 | end 35 | 36 | describe 'Blather::StreamError when instantiated' do 37 | before do 38 | @err_name = 'internal-server-error' 39 | @msg = 'the server has experienced a misconfiguration' 40 | @err = Blather::StreamError.import stream_error_node(@err_name, @msg) 41 | end 42 | 43 | it 'provides a err_name attribute' do 44 | expect(@err).to respond_to :name 45 | expect(@err.name).to eq(@err_name.gsub('-','_').to_sym) 46 | end 47 | 48 | it 'provides a text attribute' do 49 | expect(@err).to respond_to :text 50 | expect(@err.text).to eq(@msg) 51 | end 52 | 53 | it 'provides an extras attribute' do 54 | expect(@err).to respond_to :extras 55 | expect(@err.extras).to be_instance_of Array 56 | expect(@err.extras.size).to eq(1) 57 | expect(@err.extras.first.element_name).to eq('extra-error') 58 | end 59 | 60 | it 'describes itself' do 61 | expect(@err.to_s).to match(/#{@type}/) 62 | expect(@err.to_s).to match(/#{@msg}/) 63 | 64 | expect(@err.inspect).to match(/#{@type}/) 65 | expect(@err.inspect).to match(/#{@msg}/) 66 | end 67 | 68 | it 'can be turned into xml' do 69 | expect(@err).to respond_to :to_xml 70 | doc = parse_stanza @err.to_xml 71 | expect(doc.xpath("//err_ns:internal-server-error", :err_ns => Blather::StreamError::STREAM_ERR_NS)).not_to be_empty 72 | expect(doc.xpath("//err_ns:text[.='the server has experienced a misconfiguration']", :err_ns => Blather::StreamError::STREAM_ERR_NS)).not_to be_empty 73 | expect(doc.xpath("//err_ns:extra-error[.='Blather Error']", :err_ns => 'blather:stream:error')).not_to be_empty 74 | end 75 | end 76 | 77 | describe 'Each XMPP stream error type' do 78 | %w[ bad-format 79 | bad-namespace-prefix 80 | conflict 81 | connection-timeout 82 | host-gone 83 | host-unknown 84 | improper-addressing 85 | internal-server-error 86 | invalid-from 87 | invalid-id 88 | invalid-namespace 89 | invalid-xml 90 | not-authorized 91 | policy-violation 92 | remote-connection-failed 93 | resource-constraint 94 | restricted-xml 95 | see-other-host 96 | system-shutdown 97 | undefined-condition 98 | unsupported-encoding 99 | unsupported-stanza-type 100 | unsupported-version 101 | xml-not-well-formed 102 | ].each do |error_type| 103 | it "handles the name for #{error_type}" do 104 | e = Blather::StreamError.import stream_error_node(error_type) 105 | expect(e.name).to eq(error_type.gsub('-','_').to_sym) 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /lib/blather/file_transfer/s5b.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class FileTransfer 3 | # SOCKS5 Bytestreams Transfer helper 4 | # Takes care of accepting, declining and offering file transfers through the stream 5 | class S5b 6 | 7 | # Set this to false if you don't want to fallback to In-Band Bytestreams 8 | attr_accessor :allow_ibb_fallback 9 | 10 | # Set this to true if the buddies of your bot will be in the same local network 11 | # 12 | # Usually IM clients advertise all network addresses which they can determine. 13 | # Skipping the local ones can save time if your bot is not in the same local network as it's buddies 14 | attr_accessor :allow_private_ips 15 | 16 | def initialize(stream, iq) 17 | @stream = stream 18 | @iq = iq 19 | @allow_ibb_fallback = true 20 | @allow_private_ips = false 21 | end 22 | 23 | # Accept an incoming file-transfer 24 | # 25 | # @param [module] handler the handler for incoming data, see Blather::FileTransfer::SimpleFileReceiver for an example 26 | # @param [Array] params the params to be passed into the handler 27 | def accept(handler, *params) 28 | @streamhosts = @iq.streamhosts 29 | @streamhosts.delete_if {|s| begin IPAddr.new(s.host).private? rescue false end } unless @allow_private_ips 30 | @socket_address = Digest::SHA1.hexdigest("#{@iq.sid}#{@iq.from}#{@iq.to}") 31 | 32 | @handler = handler 33 | @params = params 34 | 35 | connect_next_streamhost 36 | @stream.clear_handlers :s5b_open, :from => @iq.from 37 | end 38 | 39 | # Decline an incoming file-transfer 40 | def decline 41 | @stream.clear_handlers :s5b_open, :from => @iq.from 42 | @stream.write StanzaError.new(@iq, 'not-acceptable', :auth).to_node 43 | end 44 | 45 | # Offer a file to somebody, not implemented yet 46 | def offer 47 | # TODO: implement 48 | end 49 | 50 | private 51 | 52 | def connect_next_streamhost 53 | if streamhost = @streamhosts.shift 54 | connect(streamhost) 55 | else 56 | if @allow_ibb_fallback 57 | @stream.register_handler :ibb_open, :from => @iq.from, :sid => @iq.sid do |iq| 58 | transfer = Blather::FileTransfer::Ibb.new(@stream, iq) 59 | transfer.accept(@handler, *@params) 60 | true 61 | end 62 | end 63 | 64 | @stream.write StanzaError.new(@iq, 'item-not-found', :cancel).to_node 65 | end 66 | end 67 | 68 | def connect(streamhost) 69 | begin 70 | socket = EM.connect streamhost.host, streamhost.port, SocketConnection, @socket_address, 0, @handler, *@params 71 | 72 | socket.callback do 73 | answer = @iq.reply 74 | answer.streamhosts = nil 75 | answer.streamhost_used = streamhost.jid 76 | 77 | @stream.write answer 78 | end 79 | 80 | socket.errback do 81 | connect_next_streamhost 82 | end 83 | rescue EventMachine::ConnectionError => e 84 | connect_next_streamhost 85 | end 86 | end 87 | 88 | # @private 89 | class SocketConnection < EM::P::Socks5 90 | include EM::Deferrable 91 | 92 | def initialize(host, port, handler, *params) 93 | super(host, port) 94 | @@handler = handler 95 | @params = params 96 | end 97 | 98 | def post_init 99 | self.succeed 100 | 101 | class << self 102 | include @@handler 103 | end 104 | send(:initialize, *@params) 105 | post_init 106 | end 107 | 108 | def unbind 109 | self.fail if @socks_state != :connected 110 | end 111 | end 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/blather/xmpp_node.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | 3 | # Base XML Node 4 | # All XML classes subclass XMPPNode it allows the addition of helpers 5 | class XMPPNode < Niceogiri::XML::Node 6 | # @private 7 | BASE_NAMES = %w[presence message iq].freeze 8 | 9 | # @private 10 | @@registrations = {} 11 | 12 | class_attribute :registered_ns, :registered_name 13 | 14 | # Register a new stanza class to a name and/or namespace 15 | # 16 | # This registers a namespace that is used when looking 17 | # up the class name of the object to instantiate when a new 18 | # stanza is received 19 | # 20 | # @param [#to_s] name the name of the node 21 | # @param [String, nil] ns the namespace the node belongs to 22 | def self.register(name, ns = nil) 23 | self.registered_name = name.to_s 24 | self.registered_ns = ns 25 | @@registrations[[self.registered_name, self.registered_ns]] = self 26 | end 27 | 28 | # Find the class to use given the name and namespace of a stanza 29 | # 30 | # @param [#to_s] name the name to lookup 31 | # @param [String, nil] xmlns the namespace the node belongs to 32 | # @return [Class, nil] the class appropriate for the name/ns combination 33 | def self.class_from_registration(name, ns = nil) 34 | reg = @@registrations[[name.to_s, ns]] 35 | return @@registrations[[name.to_s, nil]] if !reg && ["jabber:client", "jabber:component:accept"].include?(ns) 36 | 37 | reg 38 | end 39 | 40 | # Import an XML::Node to the appropriate class 41 | # 42 | # Looks up the class the node should be then creates it based on the 43 | # elements of the XML::Node 44 | # @param [XML::Node] node the node to import 45 | # @return the appropriate object based on the node name and namespace 46 | def self.import(node, *decorators) 47 | ns = (node.namespace.href if node.namespace) 48 | klass = class_from_registration(node.element_name, ns) 49 | if klass && klass != self 50 | klass.import(node, *decorators) 51 | else 52 | new(node.element_name).decorate(*decorators).inherit(node) 53 | end 54 | end 55 | 56 | # Parse a string as XML and import to the appropriate class 57 | # 58 | # @param [String] string the string to parse 59 | # @return the appropriate object based on the node name and namespace 60 | def self.parse(string) 61 | import Nokogiri::XML(string).root 62 | end 63 | 64 | # Create a new Node object 65 | # 66 | # @param [String, nil] name the element name 67 | # @param [XML::Document, nil] doc the document to attach the node to. If 68 | # not provided one will be created 69 | # @return a new object with the registered name and namespace 70 | def self.new(name = registered_name, doc = nil) 71 | super name, doc, BASE_NAMES.include?(name.to_s) ? nil : self.registered_ns 72 | end 73 | 74 | def self.decorator_modules 75 | if self.const_defined?(:InstanceMethods) 76 | [self::InstanceMethods] 77 | else 78 | [] 79 | end 80 | end 81 | 82 | def decorate(*decorators) 83 | decorators.each do |decorator| 84 | decorator.decorator_modules.each do |mod| 85 | extend mod 86 | end 87 | 88 | @handler_hierarchy.unshift decorator.handler_hierarchy.first if decorator.respond_to?(:handler_hierarchy) 89 | end 90 | self 91 | end 92 | 93 | def content_from(name, ns = nil) 94 | content = super 95 | if !content && !ns 96 | return super("ns:#{name}", ns: "jabber:client") || super("ns:#{name}", ns: "jabber:component:accept") 97 | end 98 | 99 | content 100 | end 101 | 102 | # Turn the object into a proper stanza 103 | # 104 | # @return a stanza object 105 | def to_stanza 106 | self.class.import self 107 | end 108 | end # XMPPNode 109 | 110 | end # Blather 111 | -------------------------------------------------------------------------------- /spec/blather/stanza/iq/ibb_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | def ibb_open_xml 4 | <<-XML 5 | 9 | 13 | 14 | XML 15 | end 16 | 17 | def ibb_data_xml 18 | <<-XML 19 | 23 | 24 | qANQR1DBwU4DX7jmYZnncmUQB/9KuKBddzQH+tZ1ZywKK0yHKnq57kWq+RFtQdCJ 25 | WpdWpR0uQsuJe7+vh3NWn59/gTc5MDlX8dS9p0ovStmNcyLhxVgmqS8ZKhsblVeu 26 | IpQ0JgavABqibJolc3BKrVtVV1igKiX/N7Pi8RtY1K18toaMDhdEfhBRzO/XB0+P 27 | AQhYlRjNacGcslkhXqNjK5Va4tuOAPy2n1Q8UUrHbUd0g+xJ9Bm0G0LZXyvCWyKH 28 | kuNEHFQiLuCY6Iv0myq6iX6tjuHehZlFSh80b5BVV9tNLwNR5Eqz1klxMhoghJOA 29 | 30 | 31 | XML 32 | end 33 | 34 | def ibb_close_xml 35 | <<-XML 36 | 40 | 41 | 42 | XML 43 | end 44 | 45 | describe Blather::Stanza::Iq::Ibb::Open do 46 | it 'registers itself' do 47 | expect(Blather::XMPPNode.class_from_registration(:open, 'http://jabber.org/protocol/ibb')).to eq(Blather::Stanza::Iq::Ibb::Open) 48 | end 49 | 50 | it 'can be imported' do 51 | node = Blather::XMPPNode.parse ibb_open_xml 52 | expect(node).to be_instance_of Blather::Stanza::Iq::Ibb::Open 53 | end 54 | 55 | it 'has open node' do 56 | node = Blather::XMPPNode.parse ibb_open_xml 57 | expect(node.open).to be_kind_of Nokogiri::XML::Element 58 | end 59 | 60 | it 'can get sid' do 61 | node = Blather::XMPPNode.parse ibb_open_xml 62 | expect(node.sid).to eq('i781hf64') 63 | end 64 | 65 | it 'deleted open node on reply' do 66 | node = Blather::XMPPNode.parse ibb_open_xml 67 | reply = node.reply 68 | expect(reply.open).to be_nil 69 | end 70 | end 71 | 72 | describe Blather::Stanza::Iq::Ibb::Data do 73 | it 'registers itself' do 74 | expect(Blather::XMPPNode.class_from_registration(:data, 'http://jabber.org/protocol/ibb')).to eq(Blather::Stanza::Iq::Ibb::Data) 75 | end 76 | 77 | it 'can be imported' do 78 | node = Blather::XMPPNode.parse ibb_data_xml 79 | expect(node).to be_instance_of Blather::Stanza::Iq::Ibb::Data 80 | end 81 | 82 | it 'has data node' do 83 | node = Blather::XMPPNode.parse ibb_data_xml 84 | expect(node.data).to be_kind_of Nokogiri::XML::Element 85 | end 86 | 87 | it 'can get sid' do 88 | node = Blather::XMPPNode.parse ibb_data_xml 89 | expect(node.sid).to eq('i781hf64') 90 | end 91 | 92 | it 'deleted data node on reply' do 93 | node = Blather::XMPPNode.parse ibb_data_xml 94 | reply = node.reply 95 | expect(reply.data).to be_nil 96 | end 97 | end 98 | 99 | describe Blather::Stanza::Iq::Ibb::Close do 100 | it 'registers itself' do 101 | expect(Blather::XMPPNode.class_from_registration(:close, 'http://jabber.org/protocol/ibb')).to eq(Blather::Stanza::Iq::Ibb::Close) 102 | end 103 | 104 | it 'can be imported' do 105 | node = Blather::XMPPNode.parse ibb_close_xml 106 | expect(node).to be_instance_of Blather::Stanza::Iq::Ibb::Close 107 | end 108 | 109 | it 'has close node' do 110 | node = Blather::XMPPNode.parse ibb_close_xml 111 | expect(node.close).to be_kind_of Nokogiri::XML::Element 112 | end 113 | 114 | it 'can get sid' do 115 | node = Blather::XMPPNode.parse ibb_close_xml 116 | expect(node.sid).to eq('i781hf64') 117 | end 118 | 119 | it 'deleted close node on reply' do 120 | node = Blather::XMPPNode.parse ibb_close_xml 121 | reply = node.reply 122 | expect(reply.close).to be_nil 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /lib/blather/stanza/pubsub/event.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stanza 3 | class PubSub 4 | 5 | # # PubSub Event Stanza 6 | # 7 | # [XEP-0060](http://xmpp.org/extensions/xep-0060.html) 8 | # 9 | # The PubSub Event stanza is used in many places. Please see the XEP for more 10 | # information. 11 | # 12 | # @handler :pubsub_event 13 | class Event < Message 14 | # @private 15 | SHIM_NS = 'http://jabber.org/protocol/shim'.freeze 16 | 17 | register :pubsub_event, :event, 'http://jabber.org/protocol/pubsub#event' 18 | 19 | # Ensures the event_node is created 20 | # @private 21 | def self.new(type = nil) 22 | node = super 23 | node.event_node 24 | node 25 | end 26 | 27 | # Kill the event_node node before running inherit 28 | # @private 29 | def inherit(node) 30 | event_node.remove 31 | super 32 | end 33 | 34 | # Get the name of the node 35 | # 36 | # @return [String, nil] 37 | def node 38 | !purge? ? items_node[:node] : purge_node[:node] 39 | end 40 | 41 | # Get a list of retractions 42 | # 43 | # @return [Array] 44 | def retractions 45 | items_node.find('//ns:retract', :ns => self.class.registered_ns).map do |i| 46 | i[:id] 47 | end 48 | end 49 | 50 | # Check if this is a retractions stanza 51 | # 52 | # @return [Boolean] 53 | def retractions? 54 | !retractions.empty? 55 | end 56 | 57 | # Get the list of items attached to this event 58 | # 59 | # @return [Array] 60 | def items 61 | items_node.find('//ns:item', :ns => self.class.registered_ns).map do |i| 62 | PubSubItem.new(nil,nil,self.document).inherit i 63 | end 64 | end 65 | 66 | # Check if this stanza has items 67 | # 68 | # @return [Boolean] 69 | def items? 70 | !items.empty? 71 | end 72 | 73 | # Check if this is a purge stanza 74 | # 75 | # @return [XML::Node, nil] 76 | def purge? 77 | purge_node 78 | end 79 | 80 | # Get or create the actual event node 81 | # 82 | # @return [Blather::XMPPNode] 83 | def event_node 84 | node = find_first('//ns:event', :ns => self.class.registered_ns) 85 | node = find_first('//event', self.class.registered_ns) unless node 86 | unless node 87 | (self << (node = XMPPNode.new('event', self.document))) 88 | node.namespace = self.class.registered_ns 89 | end 90 | node 91 | end 92 | 93 | # Get or create the actual items node 94 | # 95 | # @return [Blather::XMPPNode] 96 | def items_node 97 | node = find_first('ns:event/ns:items', :ns => self.class.registered_ns) 98 | unless node 99 | (self.event_node << (node = XMPPNode.new('items', self.document))) 100 | node.namespace = event_node.namespace 101 | end 102 | node 103 | end 104 | 105 | # Get the actual purge node 106 | # 107 | # @return [Blather::XMPPNode] 108 | def purge_node 109 | event_node.find_first('//ns:purge', :ns => self.class.registered_ns) 110 | end 111 | 112 | # Get the subscription IDs associated with this event 113 | # 114 | # @return [Array] 115 | def subscription_ids 116 | find('//ns:header[@name="SubID"]', :ns => SHIM_NS).map do |n| 117 | n.content 118 | end 119 | end 120 | 121 | # Check if this is a subscription stanza 122 | # 123 | # @return [XML::Node, nil] 124 | def subscription? 125 | subscription_node 126 | end 127 | 128 | # Get the actual subscription node 129 | # 130 | # @return [Blather::XMPPNode] 131 | def subscription_node 132 | event_node.find_first('//ns:subscription', :ns => self.class.registered_ns) 133 | end 134 | alias_method :subscription, :subscription_node 135 | end # Event 136 | 137 | end # PubSub 138 | end # Stanza 139 | end # Blather 140 | -------------------------------------------------------------------------------- /lib/blather/file_transfer.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | # File Transfer helper 3 | # Takes care of accepting, declining and offering file transfers through the stream 4 | class FileTransfer 5 | 6 | # Set this to false if you don't want to use In-Band Bytestreams 7 | attr_accessor :allow_ibb 8 | 9 | # Set this to false if you don't want to use SOCKS5 Bytestreams 10 | attr_accessor :allow_s5b 11 | 12 | # Set this to true if you want SOCKS5 Bytestreams to attempt to use private network addresses 13 | attr_accessor :allow_private_ips 14 | 15 | # Create a new FileTransfer 16 | # 17 | # @param [Blather::Stream] stream the stream the file transfer should use 18 | # @param [Blather::Stanza::Iq::Si] iq a si iq used to stream-initiation 19 | def initialize(stream, iq = nil) 20 | @stream = stream 21 | @allow_s5b = true 22 | @allow_ibb = true 23 | 24 | Blather.logger.debug "File transfers on the local network are ignored by default. Set #allow_private_ips = true if you need local network file transfers." 25 | 26 | @iq = iq 27 | end 28 | 29 | # Accept an incoming file-transfer 30 | # 31 | # @param [module] handler the handler for incoming data, see Blather::FileTransfer::SimpleFileReceiver for an example 32 | # @param [Array] params the params to be passed into the handler 33 | def accept(handler, *params) 34 | answer = @iq.reply 35 | 36 | answer.si.feature.x.type = :submit 37 | 38 | supported_methods = @iq.si.feature.x.field("stream-method").options.map(&:value) 39 | if supported_methods.include?(Stanza::Iq::S5b::NS_S5B) and @allow_s5b 40 | answer.si.feature.x.fields = {:var => 'stream-method', :value => Stanza::Iq::S5b::NS_S5B} 41 | 42 | @stream.register_handler :s5b_open, :from => @iq.from do |iq| 43 | transfer = Blather::FileTransfer::S5b.new(@stream, iq) 44 | transfer.allow_ibb_fallback = true if @allow_ibb 45 | transfer.allow_private_ips = true if @allow_private_ips 46 | EM.next_tick { transfer.accept(handler, *params) } 47 | true 48 | end 49 | 50 | @stream.write answer 51 | elsif supported_methods.include?(Stanza::Iq::Ibb::NS_IBB) and @allow_ibb 52 | answer.si.feature.x.fields = {:var => 'stream-method', :value => Stanza::Iq::Ibb::NS_IBB} 53 | 54 | @stream.register_handler :ibb_open, :from => @iq.from do |iq| 55 | transfer = Blather::FileTransfer::Ibb.new(@stream, iq) 56 | EM.next_tick { transfer.accept(handler, *params) } 57 | true 58 | end 59 | 60 | @stream.write answer 61 | else 62 | reason = XMPPNode.new('no-valid-streams') 63 | reason.namespace = Blather::Stanza::Iq::Si::NS_SI 64 | 65 | @stream.write StanzaError.new(@iq, 'bad-request', 'cancel', nil, [reason]).to_node 66 | end 67 | end 68 | 69 | # Decline an incoming file-transfer 70 | def decline 71 | answer = StanzaError.new(@iq, 'forbidden', 'cancel', 'Offer declined').to_node 72 | 73 | @stream.write answer 74 | end 75 | 76 | # Offer a file to somebody, not implemented yet 77 | def offer 78 | # TODO: implement 79 | end 80 | 81 | # Simple handler for incoming file transfers 82 | # 83 | # You can define your own handler and pass it to the accept method. 84 | module SimpleFileReceiver 85 | def initialize(path, size) 86 | @path = path 87 | @size = size 88 | @transferred = 0 89 | end 90 | 91 | # @private 92 | def post_init 93 | @file = File.open(@path, "w") 94 | end 95 | 96 | # @private 97 | def receive_data(data) 98 | @transferred += data.size 99 | @file.write data 100 | end 101 | 102 | # @private 103 | def unbind 104 | @file.close 105 | File.delete(@path) unless @transferred == @size 106 | end 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /spec/blather/stanza/pubsub/event_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'fixtures/pubsub' 3 | 4 | describe Blather::Stanza::PubSub::Event do 5 | it 'registers itself' do 6 | expect(Blather::XMPPNode.class_from_registration(:event, 'http://jabber.org/protocol/pubsub#event')).to eq(Blather::Stanza::PubSub::Event) 7 | end 8 | 9 | it 'is importable' do 10 | expect(Blather::XMPPNode.parse(event_notification_xml)).to be_instance_of Blather::Stanza::PubSub::Event 11 | end 12 | 13 | it 'ensures a query node is present on create' do 14 | evt = Blather::Stanza::PubSub::Event.new 15 | expect(evt.find('ns:event', :ns => Blather::Stanza::PubSub::Event.registered_ns)).not_to be_empty 16 | end 17 | 18 | it 'ensures an event node exists when calling #event_node' do 19 | evt = Blather::Stanza::PubSub::Event.new 20 | evt.remove_children :event 21 | expect(evt.find('*[local-name()="event"]')).to be_empty 22 | 23 | expect(evt.event_node).not_to be_nil 24 | expect(evt.find('ns:event', :ns => Blather::Stanza::PubSub::Event.registered_ns)).not_to be_empty 25 | end 26 | 27 | it 'ensures an items node exists when calling #items_node' do 28 | evt = Blather::Stanza::PubSub::Event.new 29 | evt.remove_children :items 30 | expect(evt.find('*[local-name()="items"]')).to be_empty 31 | 32 | expect(evt.items_node).not_to be_nil 33 | expect(evt.find('ns:event/ns:items', :ns => Blather::Stanza::PubSub::Event.registered_ns)).not_to be_empty 34 | end 35 | 36 | it 'knows the associated node name' do 37 | evt = Blather::XMPPNode.parse(event_with_payload_xml) 38 | expect(evt.node).to eq('princely_musings') 39 | end 40 | 41 | it 'ensures newly inherited items are PubSubItem objects' do 42 | evt = Blather::XMPPNode.parse(event_with_payload_xml) 43 | expect(evt.items?).to eq(true) 44 | expect(evt.retractions?).to eq(false) 45 | expect(evt.items.map { |i| i.class }.uniq).to eq([Blather::Stanza::PubSub::PubSubItem]) 46 | end 47 | 48 | it 'will iterate over each item' do 49 | evt = Blather::XMPPNode.parse(event_with_payload_xml) 50 | evt.items.each { |i| expect(i.class).to eq(Blather::Stanza::PubSub::PubSubItem) } 51 | end 52 | 53 | it 'handles receiving subscription ids' do 54 | evt = Blather::XMPPNode.parse(event_subids_xml) 55 | expect(evt.subscription_ids).to eq(['123-abc', '004-yyy']) 56 | end 57 | 58 | it 'can have a list of retractions' do 59 | evt = Blather::XMPPNode.parse(<<-NODE) 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | NODE 68 | expect(evt.retractions?).to eq(true) 69 | expect(evt.items?).to eq(false) 70 | expect(evt.retractions).to eq(%w[ae890ac52d0df67ed7cfdf51b644e901]) 71 | end 72 | 73 | it 'can be a purge' do 74 | evt = Blather::XMPPNode.parse(<<-NODE) 75 | 76 | 77 | 78 | 79 | 80 | NODE 81 | expect(evt.purge?).not_to be_nil 82 | expect(evt.node).to eq('princely_musings') 83 | end 84 | 85 | it 'can be a subscription notification' do 86 | evt = Blather::XMPPNode.parse(<<-NODE) 87 | 88 | 89 | 90 | 91 | 92 | NODE 93 | expect(evt.subscription?).not_to be_nil 94 | expect(evt.subscription[:jid]).to eq('francisco@denmark.lit') 95 | expect(evt.subscription[:subscription]).to eq('subscribed') 96 | expect(evt.subscription[:node]).to eq('/example.com/test') 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/blather/core_ext/eventmachine.rb: -------------------------------------------------------------------------------- 1 | # @private 2 | module EventMachine 3 | # @private 4 | module Protocols 5 | # Basic SOCKS v5 client implementation 6 | # 7 | # Use as you would any regular connection: 8 | # 9 | # class MyConn < EM::P::Socks5 10 | # def post_init 11 | # send_data("sup") 12 | # end 13 | # 14 | # def receive_data(data) 15 | # send_data("you said: #{data}") 16 | # end 17 | # end 18 | # 19 | # EM.connect socks_host, socks_port, MyConn, host, port 20 | # 21 | # @private 22 | class Socks5 < Connection 23 | def initialize(host, port) 24 | @host = host 25 | @port = port 26 | @socks_error_code = nil 27 | @buffer = '' 28 | @socks_state = :method_negotiation 29 | @socks_methods = [0] # TODO: other authentication methods 30 | setup_methods 31 | end 32 | 33 | def setup_methods 34 | class << self 35 | def post_init; socks_post_init; end 36 | def receive_data(*a); socks_receive_data(*a); end 37 | end 38 | end 39 | 40 | def restore_methods 41 | class << self 42 | remove_method :post_init 43 | remove_method :receive_data 44 | end 45 | end 46 | 47 | def socks_post_init 48 | packet = [5, @socks_methods.size].pack('CC') + @socks_methods.pack('C*') 49 | send_data(packet) 50 | end 51 | 52 | def socks_receive_data(data) 53 | @buffer << data 54 | 55 | if @socks_state == :method_negotiation 56 | return if @buffer.size < 2 57 | 58 | header_resp = @buffer.slice! 0, 2 59 | _, method_code = header_resp.unpack("cc") 60 | 61 | if @socks_methods.include?(method_code) 62 | @socks_state = :connecting 63 | packet = [5, 1, 0].pack("C*") 64 | 65 | if @host =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ # IPv4 66 | packet << [1, $1.to_i, $2.to_i, $3.to_i, $4.to_i].pack("C*") 67 | elsif @host.include?(":") # IPv6 68 | l, r = if @host =~ /^(.*)::(.*)$/ 69 | [$1,$2].map {|i| i.split ":"} 70 | else 71 | [@host.split(":"),[]] 72 | end 73 | dec_groups = (l + Array.new(8-l.size-r.size, '0') + r).map {|i| i.hex} 74 | packet << ([4] + dec_groups).pack("Cn8") 75 | else # Domain 76 | packet << [3, @host.length, @host].pack("CCA*") 77 | end 78 | packet << [@port].pack("n") 79 | 80 | send_data packet 81 | else 82 | @socks_state = :invalid 83 | @socks_error_code = method_code 84 | close_connection 85 | return 86 | end 87 | elsif @socks_state == :connecting 88 | return if @buffer.size < 4 89 | 90 | header_resp = @buffer.slice! 0, 4 91 | _, response_code, _, address_type = header_resp.unpack("C*") 92 | 93 | if response_code == 0 94 | case address_type 95 | when 1 96 | @buffer.slice! 0, 4 97 | when 3 98 | len = @buffer.slice! 0, 1 99 | @buffer.slice! 0, len.unpack("C").first 100 | when 4 101 | @buffer.slice! 0, 16 102 | else 103 | @socks_state = :invalid 104 | @socks_error_code = address_type 105 | close_connection 106 | return 107 | end 108 | @buffer.slice! 0, 2 109 | 110 | @socks_state = :connected 111 | restore_methods 112 | 113 | post_init 114 | receive_data(@buffer) unless @buffer.empty? 115 | else 116 | @socks_state = :invalid 117 | @socks_error_code = response_code 118 | close_connection 119 | return 120 | end 121 | end 122 | end 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /lib/blather/stanza/pubsub/subscription.rb: -------------------------------------------------------------------------------- 1 | module Blather 2 | class Stanza 3 | class PubSub 4 | 5 | # # PubSub Subscription Stanza 6 | # 7 | # [XEP-0060 Section 8.8 Manage Subscriptions](http://xmpp.org/extensions/xep-0060.html#owner-subscriptions) 8 | # 9 | # @handler :pubsub_subscription 10 | class Subscription < PubSub 11 | # @private 12 | VALID_TYPES = [:none, :pending, :subscribed, :unconfigured] 13 | 14 | register :pubsub_subscription, :subscription, self.registered_ns 15 | 16 | # Create a new subscription request node 17 | # 18 | # @param [Blather::Stanza::Iq::VALID_TYPES] type the IQ type 19 | # @param [String] host the host to send the request to 20 | # @param [String] node the node to look for requests on 21 | # @param [Blather::JID, #to_s] jid the JID of the subscriber 22 | # @param [String] subid the subscription ID 23 | # @param [VALID_TYPES] subscription the subscription type 24 | def self.new(type = :result, host = nil, node = nil, jid = nil, subid = nil, subscription = nil) 25 | new_node = super(type, host) 26 | new_node.node = node 27 | new_node.jid = jid 28 | new_node.subid = subid 29 | new_node.subscription = subscription 30 | new_node 31 | end 32 | 33 | # Check if the type is none 34 | # 35 | # @return [Boolean] 36 | def none? 37 | self.subscription == :none 38 | end 39 | 40 | # Check if the type is pending 41 | # 42 | # @return [Boolean] 43 | def pending? 44 | self.subscription == :pending 45 | end 46 | 47 | # Check if the type is subscribed 48 | # 49 | # @return [Boolean] 50 | def subscribed? 51 | self.subscription == :subscribed 52 | end 53 | 54 | # Check if the type is unconfigured 55 | # 56 | # @return [Boolean] 57 | def unconfigured? 58 | self.subscription == :unconfigured 59 | end 60 | 61 | # Get the JID of the subscriber 62 | # 63 | # @return [Blather::JID] 64 | def jid 65 | JID.new(subscription_node[:jid]) 66 | end 67 | 68 | # Set the JID of the subscriber 69 | # 70 | # @param [Blather::JID, #to_s] jid 71 | def jid=(jid) 72 | subscription_node[:jid] = jid 73 | end 74 | 75 | # Get the name of the subscription node 76 | # 77 | # @return [String] 78 | def node 79 | subscription_node[:node] 80 | end 81 | 82 | # Set the name of the subscription node 83 | # 84 | # @param [String] node 85 | def node=(node) 86 | subscription_node[:node] = node 87 | end 88 | 89 | # Get the ID of the subscription 90 | # 91 | # @return [String] 92 | def subid 93 | subscription_node[:subid] 94 | end 95 | 96 | # Set the ID of the subscription 97 | # 98 | # @param [String] subid 99 | def subid=(subid) 100 | subscription_node[:subid] = subid 101 | end 102 | 103 | # Get the subscription type 104 | # 105 | # @return [VALID_TYPES, nil] 106 | def subscription 107 | s = subscription_node[:subscription] 108 | s.to_sym if s 109 | end 110 | 111 | # Set the subscription type 112 | # 113 | # @param [VALID_TYPES, nil] subscription 114 | def subscription=(subscription) 115 | if subscription && !VALID_TYPES.include?(subscription.to_sym) 116 | raise ArgumentError, "Invalid Type (#{type}), use: #{VALID_TYPES*' '}" 117 | end 118 | subscription_node[:subscription] = subscription 119 | end 120 | 121 | # Get or create the actual subscription node 122 | # 123 | # @return [Blather::XMPPNode] 124 | def subscription_node 125 | unless subscription = pubsub.find_first('ns:subscription', :ns => self.class.registered_ns) 126 | self.pubsub << (subscription = XMPPNode.new('subscription', self.document)) 127 | subscription.namespace = self.pubsub.namespace 128 | end 129 | subscription 130 | end 131 | end # Subscribe 132 | 133 | end # PubSub 134 | end # Stanza 135 | end # Blather 136 | -------------------------------------------------------------------------------- /spec/blather/roster_item_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Blather::RosterItem do 4 | it 'can be initialized with Blather::JID' do 5 | jid = Blather::JID.new(jid) 6 | i = Blather::RosterItem.new jid 7 | expect(i.jid).to eq(jid) 8 | end 9 | 10 | it 'can be initialized with an Iq::RosterItem' do 11 | jid = 'n@d/r' 12 | i = Blather::RosterItem.new Blather::Stanza::Iq::Roster::RosterItem.new(jid) 13 | expect(i.jid).to eq(Blather::JID.new(jid).stripped) 14 | end 15 | 16 | it 'can be initialized with a string' do 17 | jid = 'n@d/r' 18 | i = Blather::RosterItem.new jid 19 | expect(i.jid).to eq(Blather::JID.new(jid).stripped) 20 | end 21 | 22 | it 'returns the same object when intialized with a Blather::RosterItem' do 23 | control = Blather::RosterItem.new 'n@d/r' 24 | expect(Blather::RosterItem.new(control)).to be control 25 | end 26 | 27 | it 'has a Blather::JID setter that strips the Blather::JID' do 28 | jid = Blather::JID.new('n@d/r') 29 | i = Blather::RosterItem.new nil 30 | i.jid = jid 31 | expect(i.jid).to eq(jid.stripped) 32 | end 33 | 34 | it 'has a subscription setter that forces a symbol' do 35 | i = Blather::RosterItem.new nil 36 | i.subscription = 'remove' 37 | expect(i.subscription).to eq(:remove) 38 | end 39 | 40 | it 'forces the type of subscription' do 41 | expect { Blather::RosterItem.new(nil).subscription = 'foo' }.to raise_error Blather::ArgumentError 42 | end 43 | 44 | it 'returns :none if the subscription field is blank' do 45 | expect(Blather::RosterItem.new(nil).subscription).to eq(:none) 46 | end 47 | 48 | it 'ensure #ask is a symbol' do 49 | i = Blather::RosterItem.new(nil) 50 | i.ask = 'subscribe' 51 | expect(i.ask).to eq(:subscribe) 52 | end 53 | 54 | it 'forces #ask to be :subscribe or nothing at all' do 55 | expect { Blather::RosterItem.new(nil).ask = 'foo' }.to raise_error Blather::ArgumentError 56 | end 57 | 58 | it 'generates a stanza with #to_stanza' do 59 | jid = Blather::JID.new('n@d/r') 60 | i = Blather::RosterItem.new jid 61 | s = i.to_stanza 62 | expect(s).to be_kind_of Blather::Stanza::Iq::Roster 63 | expect(s.items.first.jid).to eq(jid.stripped) 64 | end 65 | 66 | it 'returns status based on priority' do 67 | setup_item_with_presences 68 | expect(@i.status).to eq(@p3) 69 | end 70 | 71 | it 'returns status based on priority and state' do 72 | setup_item_with_presences 73 | 74 | @p4 = Blather::Stanza::Presence::Status.new 75 | @p4.type = :unavailable 76 | @p4.from = 'n@d/d' 77 | @p4.priority = 15 78 | @i.status = @p4 79 | 80 | expect(@i.status).to eq(@p3) 81 | end 82 | 83 | it 'returns status based on resource' do 84 | setup_item_with_presences 85 | expect(@i.status('a')).to eq(@p) 86 | end 87 | 88 | def setup_item_with_presences 89 | @jid = Blather::JID.new('n@d/r') 90 | @i = Blather::RosterItem.new @jid 91 | 92 | @p = Blather::Stanza::Presence::Status.new(:away) 93 | @p.from = 'n@d/a' 94 | @p.priority = 0 95 | 96 | @p2 = Blather::Stanza::Presence::Status.new(:dnd) 97 | @p2.from = 'n@d/b' 98 | @p2.priority = -1 99 | 100 | @p3 = Blather::Stanza::Presence::Status.new(:dnd) 101 | @p3.from = 'n@d/c' 102 | @p3.priority = 10 103 | 104 | @i.status = @p 105 | @i.status = @p2 106 | @i.status = @p3 107 | end 108 | 109 | it 'removes old unavailable presences' do 110 | setup_item_with_presences 111 | 112 | 50.times do |i| 113 | p = Blather::Stanza::Presence::Status.new 114 | p.type = :unavailable 115 | p.from = "n@d/#{i}" 116 | @i.status = p 117 | end 118 | 119 | expect(@i.statuses.size).to eq(4) 120 | end 121 | 122 | it 'initializes groups to [nil] if the item is not part of a group' do 123 | i = Blather::RosterItem.new 'n@d' 124 | expect(i.groups).to eq([nil]) 125 | end 126 | 127 | it 'can determine equality' do 128 | item1 = Blather::RosterItem.new 'n@d' 129 | item2 = Blather::RosterItem.new 'n@d' 130 | item1.groups = %w[group1 group2] 131 | item2.groups = %w[group1 group2] 132 | expect(item1 == item2).to eq(true) 133 | end 134 | end 135 | --------------------------------------------------------------------------------