├── .gitignore ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── bin └── vines ├── conf ├── certs │ ├── README │ └── ca-bundle.crt └── config.rb ├── lib ├── vines.rb └── vines │ ├── cli.rb │ ├── cluster.rb │ ├── cluster │ ├── connection.rb │ ├── publisher.rb │ ├── pubsub.rb │ ├── sessions.rb │ └── subscriber.rb │ ├── command │ ├── bcrypt.rb │ ├── cert.rb │ ├── init.rb │ ├── ldap.rb │ ├── restart.rb │ ├── schema.rb │ ├── start.rb │ └── stop.rb │ ├── config.rb │ ├── config │ ├── host.rb │ ├── port.rb │ └── pubsub.rb │ ├── contact.rb │ ├── daemon.rb │ ├── error.rb │ ├── jid.rb │ ├── kit.rb │ ├── log.rb │ ├── router.rb │ ├── stanza.rb │ ├── stanza │ ├── iq.rb │ ├── iq │ │ ├── auth.rb │ │ ├── disco_info.rb │ │ ├── disco_items.rb │ │ ├── error.rb │ │ ├── ping.rb │ │ ├── private_storage.rb │ │ ├── query.rb │ │ ├── result.rb │ │ ├── roster.rb │ │ ├── session.rb │ │ ├── vcard.rb │ │ └── version.rb │ ├── message.rb │ ├── presence.rb │ ├── presence │ │ ├── error.rb │ │ ├── probe.rb │ │ ├── subscribe.rb │ │ ├── subscribed.rb │ │ ├── unavailable.rb │ │ ├── unsubscribe.rb │ │ └── unsubscribed.rb │ ├── pubsub.rb │ └── pubsub │ │ ├── create.rb │ │ ├── delete.rb │ │ ├── publish.rb │ │ ├── subscribe.rb │ │ └── unsubscribe.rb │ ├── storage.rb │ ├── storage │ ├── ldap.rb │ ├── local.rb │ └── null.rb │ ├── store.rb │ ├── stream.rb │ ├── stream │ ├── client.rb │ ├── client │ │ ├── auth.rb │ │ ├── auth_restart.rb │ │ ├── bind.rb │ │ ├── bind_restart.rb │ │ ├── closed.rb │ │ ├── ready.rb │ │ ├── session.rb │ │ ├── start.rb │ │ └── tls.rb │ ├── component.rb │ ├── component │ │ ├── handshake.rb │ │ ├── ready.rb │ │ └── start.rb │ ├── http.rb │ ├── http │ │ ├── auth.rb │ │ ├── bind.rb │ │ ├── bind_restart.rb │ │ ├── ready.rb │ │ ├── request.rb │ │ ├── session.rb │ │ ├── sessions.rb │ │ └── start.rb │ ├── parser.rb │ ├── sasl.rb │ ├── server.rb │ ├── server │ │ ├── auth.rb │ │ ├── auth_restart.rb │ │ ├── final_restart.rb │ │ ├── outbound │ │ │ ├── auth.rb │ │ │ ├── auth_restart.rb │ │ │ ├── auth_result.rb │ │ │ ├── final_features.rb │ │ │ ├── final_restart.rb │ │ │ ├── start.rb │ │ │ ├── tls.rb │ │ │ └── tls_result.rb │ │ ├── ready.rb │ │ ├── start.rb │ │ └── tls.rb │ └── state.rb │ ├── token_bucket.rb │ ├── user.rb │ ├── version.rb │ └── xmpp_server.rb ├── script ├── bootstrap └── tests ├── test ├── cluster │ ├── publisher_test.rb │ ├── sessions_test.rb │ └── subscriber_test.rb ├── config │ ├── host_test.rb │ └── pubsub_test.rb ├── config_test.rb ├── contact_test.rb ├── error_test.rb ├── ext │ └── nokogiri.rb ├── jid_test.rb ├── kit_test.rb ├── router_test.rb ├── stanza │ ├── iq │ │ ├── disco_info_test.rb │ │ ├── disco_items_test.rb │ │ ├── private_storage_test.rb │ │ ├── roster_test.rb │ │ ├── session_test.rb │ │ ├── vcard_test.rb │ │ └── version_test.rb │ ├── iq_test.rb │ ├── message_test.rb │ ├── presence │ │ ├── probe_test.rb │ │ └── subscribe_test.rb │ └── pubsub │ │ ├── create_test.rb │ │ ├── delete_test.rb │ │ ├── publish_test.rb │ │ ├── subscribe_test.rb │ │ └── unsubscribe_test.rb ├── stanza_test.rb ├── storage │ ├── ldap_test.rb │ ├── local_test.rb │ ├── mock_redis.rb │ ├── null_test.rb │ └── storage_tests.rb ├── storage_test.rb ├── store_test.rb ├── stream │ ├── client │ │ ├── auth_test.rb │ │ ├── ready_test.rb │ │ └── session_test.rb │ ├── component │ │ ├── handshake_test.rb │ │ ├── ready_test.rb │ │ └── start_test.rb │ ├── http │ │ ├── auth_test.rb │ │ ├── ready_test.rb │ │ ├── request_test.rb │ │ ├── sessions_test.rb │ │ └── start_test.rb │ ├── parser_test.rb │ ├── sasl_test.rb │ └── server │ │ ├── auth_test.rb │ │ ├── outbound │ │ └── auth_test.rb │ │ └── ready_test.rb ├── test_helper.rb ├── token_bucket_test.rb └── user_test.rb ├── vines.gemspec └── web ├── 404.html ├── apple-touch-icon.png └── favicon.png /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .bundle 3 | .ruby-version 4 | Gemfile.lock 5 | pkg 6 | _site 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2014 Negative Code 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vines XMPP Server 2 | 3 | Vines is an XMPP chat server that supports thousands of simultaneous connections, 4 | using EventMachine for asynchronous IO. User data is stored in a 5 | [SQL database](https://github.com/negativecode/vines-sql), 6 | [CouchDB](https://github.com/negativecode/vines-couchdb), 7 | [MongoDB](https://github.com/negativecode/vines-mongodb), 8 | [Redis](https://github.com/negativecode/vines-redis), the file system, or a 9 | custom storage implementation that you provide. LDAP authentication can be used 10 | so user names and passwords aren't stored in the chat database. SSL encryption 11 | is mandatory on all client and server connections. 12 | 13 | The server includes support for web chat clients, using BOSH (XMPP over HTTP). A 14 | sample web application is available in the 15 | [vines-web](https://github.com/negativecode/vines-web) gem. 16 | 17 | Additional documentation can be found at [getvines.org](http://www.getvines.org/). 18 | 19 | ## Usage 20 | 21 | ``` 22 | $ gem install vines 23 | $ vines init wonderland.lit 24 | $ cd wonderland.lit && vines start 25 | ``` 26 | 27 | Login with your favorite chat program (iChat, Adium, Pidgin, etc.) to start chatting! 28 | 29 | ## Dependencies 30 | 31 | Vines requires Ruby 1.9.3 or better. Instructions for installing the 32 | needed OS packages, as well as Ruby itself, are available at 33 | http://www.getvines.org/ruby. 34 | 35 | ## Development 36 | 37 | ``` 38 | $ script/bootstrap 39 | $ script/tests 40 | ``` 41 | 42 | ## Standards support 43 | 44 | Vines implements the full XMPP specs in [RFC 6120](http://www.rfc-editor.org/rfc/rfc6120.txt) 45 | and [RFC 6121](http://www.rfc-editor.org/rfc/rfc6121.txt). It also implements 46 | the following extensions. 47 | 48 | - [XEP-0030](https://xmpp.org/extensions/xep-0030.html) Service Discovery 49 | - [XEP-0049](https://xmpp.org/extensions/xep-0049.html) Private XML Storage 50 | - [XEP-0054](https://xmpp.org/extensions/xep-0054.html) vcard-temp 51 | - [XEP-0060](https://xmpp.org/extensions/xep-0060.html) Publish-Subscribe 52 | - [XEP-0092](https://xmpp.org/extensions/xep-0092.html) Software Version 53 | - [XEP-0114](https://xmpp.org/extensions/xep-0114.html) Component Protocol 54 | - [XEP-0124](https://xmpp.org/extensions/xep-0124.html) Bidirectional-streams Over Synchronous HTTP (BOSH) 55 | - [XEP-0199](https://xmpp.org/extensions/xep-0199.html) XMPP Ping 56 | - [XEP-0206](https://xmpp.org/extensions/xep-0206.html) XMPP Over BOSH 57 | 58 | ## Contact 59 | 60 | * David Graham 61 | 62 | ## License 63 | 64 | Vines is released under the MIT license. Check the LICENSE file for details. 65 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'rake' 4 | require 'rake/clean' 5 | require 'rake/testtask' 6 | 7 | CLOBBER.include('pkg') 8 | 9 | directory 'pkg' 10 | 11 | desc 'Build distributable packages' 12 | task :build => [:pkg] do 13 | system 'gem build vines.gemspec && mv vines-*.gem pkg/' 14 | end 15 | 16 | Rake::TestTask.new(:test) do |test| 17 | test.libs << 'test' 18 | test.libs << 'test/storage' 19 | test.pattern = 'test/**/*_test.rb' 20 | test.warning = false 21 | end 22 | 23 | task :default => [:clobber, :test, :build] 24 | -------------------------------------------------------------------------------- /bin/vines: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'vines' 4 | Vines::CLI.start 5 | -------------------------------------------------------------------------------- /conf/certs/README: -------------------------------------------------------------------------------- 1 | The certs/ directory contains the TLS certificates required for encrypting 2 | client to server and server to server XMPP connections. TLS encryption 3 | is mandatory for these streams so this directory must be configured properly. 4 | 5 | The ca-bundle.crt file contains root Certificate Authority (CA) certificates. 6 | These are used to validate certificates presented during TLS handshake 7 | negotiation. The source for this file is the cacert.pem file available 8 | at http://curl.haxx.se/docs/caextract.html. 9 | 10 | Any self-signed CA certificate placed in this directory will be considered 11 | a trusted certificate. For example, let's say you're running the wonderland.lit 12 | XMPP server and would like to allow verona.lit to connect a server to server 13 | stream. The verona.lit server hasn't purchased a legitimate TLS certificate 14 | from a CA known in ca-bundle.crt. Instead, they've created a self-signed 15 | certificate and sent it to you. Place the certificate in this directory 16 | with a name of verona.lit.crt and it will be trusted. TLS connections from 17 | verona.lit will now work. 18 | 19 | For TLS connections to work for a virtual host, two files are needed in this 20 | directory: .key and .crt. The key file must contain 21 | a PEM encoded private key. Do not give this file to anyone. This is your 22 | private key so keep it private. The crt file must contain a PEM encoded TLS 23 | certificate. This contains your public key so feel free to share it with other 24 | servers. 25 | 26 | For example, when you add a new virtual host named wonderland.lit to 27 | the XMPP server, you need to run the 'vines cert wonderland.lit' command to 28 | generate the private key file and the self-signed certificate file in this 29 | directory. After running that command you will have a certs/wonderland.lit.key 30 | and a certs/wonderland.lit.crt file. 31 | 32 | Alternatively, you can purchase a TLS certificate from a CA (e.g. RapidSSL, 33 | VeriSign, etc.) and place it in this directory. This will avoid the hassles 34 | of managing self-signed certificates. 35 | 36 | Certificates for wildcard domains, like *.wonderland.lit, can be placed in this 37 | directory with a name of wonderland.lit.crt with a matching wonderland.lit.key 38 | file. The wildcard files will be used to secure connections to any subdomain 39 | under wonderland.lit (tea.wonderland.lit, party.wonderland.lit, etc). 40 | -------------------------------------------------------------------------------- /lib/vines/cli.rb: -------------------------------------------------------------------------------- 1 | module Vines 2 | # The command line application that's invoked by the `vines` binary included 3 | # in the gem. Parses the command line arguments to create a new server 4 | # directory, and starts and stops the server. 5 | class CLI 6 | COMMANDS = %w[start stop restart init bcrypt cert ldap schema] 7 | 8 | def self.start 9 | self.new.start 10 | end 11 | 12 | # Run the command line application to parse arguments and run sub-commands. 13 | # Exits the process with a non-zero return code to indicate failure. 14 | # 15 | # Returns nothing. 16 | def start 17 | register_storage 18 | opts = parse(ARGV) 19 | check_config(opts) 20 | command = Command.const_get(opts[:command].capitalize).new 21 | begin 22 | command.run(opts) 23 | rescue SystemExit 24 | # do nothing 25 | rescue Exception => e 26 | puts e.message 27 | exit(1) 28 | end 29 | end 30 | 31 | private 32 | 33 | # Try to load various storage backends provided by vines-* gems and register 34 | # them with the storage system for the config file to use. 35 | # 36 | # Returns nothing. 37 | def register_storage 38 | %w[couchdb mongodb redis sql].each do |backend| 39 | begin 40 | require 'vines/storage/%s' % backend 41 | rescue LoadError 42 | # do nothing 43 | end 44 | end 45 | end 46 | 47 | # Parse the command line arguments and run the matching sub-command 48 | # (e.g. init, start, stop, etc). 49 | # 50 | # args - The ARGV array provided by the command line. 51 | # 52 | # Returns nothing. 53 | def parse(args) 54 | options = {} 55 | parser = OptionParser.new do |opts| 56 | opts.banner = "Usage: vines [options] #{COMMANDS.join('|')}" 57 | 58 | opts.separator "" 59 | opts.separator "Daemon options:" 60 | 61 | opts.on('-d', '--daemonize', 'Run daemonized in the background') do |daemonize| 62 | options[:daemonize] = daemonize 63 | end 64 | 65 | options[:log] = 'log/vines.log' 66 | opts.on('-l', '--log FILE', 'File to redirect output (default: log/vines.log)') do |log| 67 | options[:log] = log 68 | end 69 | 70 | options[:pid] = 'pid/vines.pid' 71 | opts.on('-P', '--pid FILE', 'File to store PID (default: pid/vines.pid)') do |pid| 72 | options[:pid] = pid 73 | end 74 | 75 | opts.separator "" 76 | opts.separator "Common options:" 77 | 78 | opts.on('-h', '--help', 'Show this message') do |help| 79 | options[:help] = help 80 | end 81 | 82 | opts.on('-v', '--version', 'Show version') do |version| 83 | options[:version] = version 84 | end 85 | end 86 | 87 | begin 88 | parser.parse!(args) 89 | rescue 90 | puts parser 91 | exit(1) 92 | end 93 | 94 | if options[:version] 95 | puts Vines::VERSION 96 | exit 97 | end 98 | 99 | if options[:help] 100 | puts parser 101 | exit 102 | end 103 | 104 | command = args.shift 105 | unless COMMANDS.include?(command) 106 | puts parser 107 | exit(1) 108 | end 109 | 110 | options.tap do |opts| 111 | opts[:args] = args 112 | opts[:command] = command 113 | opts[:config] = File.expand_path('conf/config.rb') 114 | opts[:pid] = File.expand_path(opts[:pid]) 115 | opts[:log] = File.expand_path(opts[:log]) 116 | end 117 | end 118 | 119 | # Many commands must be run in the context of a vines server directory 120 | # created with `vines init`. If the command can't find the server's config 121 | # file, print an error message and exit. 122 | # 123 | # Returns nothing. 124 | def check_config(opts) 125 | return if %w[bcrypt init].include?(opts[:command]) 126 | unless File.exists?(opts[:config]) 127 | puts "No config file found at #{opts[:config]}" 128 | exit(1) 129 | end 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/vines/cluster/connection.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Cluster 5 | # Create and cache a redis database connection. 6 | class Connection 7 | attr_accessor :host, :port, :database, :password 8 | 9 | def initialize 10 | @redis, @host, @port, @database, @password = nil, nil, nil, nil, nil 11 | end 12 | 13 | # Return a shared redis connection. 14 | def connect 15 | @redis ||= create 16 | end 17 | 18 | # Return a new redis connection. 19 | def create 20 | conn = EM::Hiredis::Client.new(@host, @port, @password, @database) 21 | conn.connect 22 | conn 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/vines/cluster/publisher.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Cluster 5 | # Broadcast messages to other cluster nodes via redis pubsub channels. All 6 | # members subscribe to a channel for heartbeats, online, and offline 7 | # messages from other nodes. This allows new nodes to be added to the 8 | # cluster dynamically, without configuring all other nodes. 9 | class Publisher 10 | include Vines::Log 11 | 12 | ALL, STANZA, USER = %w[cluster:nodes:all stanza user].map {|s| s.freeze } 13 | 14 | def initialize(cluster) 15 | @cluster = cluster 16 | end 17 | 18 | # Publish a :heartbeat, :online, or :offline message to the nodes:all 19 | # broadcast channel. 20 | def broadcast(type) 21 | redis.publish(ALL, { 22 | from: @cluster.id, 23 | type: type, 24 | time: Time.now.to_i 25 | }.to_json) 26 | end 27 | 28 | # Send the stanza to the node hosting the user's session. The stanza is 29 | # published to the channel to which the remote node is listening for 30 | # messages. 31 | def route(stanza, node) 32 | log.debug { "Sent cluster stanza: %s -> %s\n%s\n" % [@cluster.id, node, stanza] } 33 | redis.publish("cluster:nodes:#{node}", { 34 | from: @cluster.id, 35 | type: STANZA, 36 | stanza: stanza.to_s 37 | }.to_json) 38 | end 39 | 40 | # Notify the remote node that the user's roster has changed and it should 41 | # reload the user from storage. 42 | def update_user(jid, node) 43 | redis.publish("cluster:nodes:#{node}", { 44 | from: @cluster.id, 45 | type: USER, 46 | jid: jid.to_s 47 | }.to_json) 48 | end 49 | 50 | def redis 51 | @cluster.connection 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/vines/cluster/pubsub.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Cluster 5 | # Manages the pubsub topic list and subscribers stored in redis. When a 6 | # message is published to a topic, the receiving cluster node broadcasts 7 | # the message to all subscribers at all other cluster nodes. 8 | class PubSub 9 | def initialize(cluster) 10 | @cluster = cluster 11 | end 12 | 13 | # Create a pubsub topic (a.k.a. node), in the given domain, to which 14 | # messages may be published. The domain argument will be one of the 15 | # configured pubsub subdomains in conf/config.rb (e.g. games.wonderland.lit, 16 | # topics.wonderland.lit, etc). 17 | def add_node(domain, node) 18 | redis.sadd("pubsub:#{domain}:nodes", node) 19 | end 20 | 21 | # Remove a pubsub topic so messages may no longer be broadcast to it. 22 | def delete_node(domain, node) 23 | redis.smembers("pubsub:#{domain}:subscribers_#{node}") do |subscribers| 24 | redis.multi 25 | subscribers.each do |jid| 26 | redis.srem("pubsub:#{domain}:subscriptions_#{jid}", node) 27 | end 28 | redis.del("pubsub:#{domain}:subscribers_#{node}") 29 | redis.srem("pubsub:#{domain}:nodes", node) 30 | redis.exec 31 | end 32 | end 33 | 34 | # Subscribe the JID to the pubsub topic so it will receive any messages 35 | # published to it. 36 | def subscribe(domain, node, jid) 37 | jid = JID.new(jid) 38 | redis.multi 39 | redis.sadd("pubsub:#{domain}:subscribers_#{node}", jid.to_s) 40 | redis.sadd("pubsub:#{domain}:subscriptions_#{jid}", node) 41 | redis.exec 42 | end 43 | 44 | # Unsubscribe the JID from the pubsub topic, deregistering its interest 45 | # in receiving any messages published to it. 46 | def unsubscribe(domain, node, jid) 47 | jid = JID.new(jid) 48 | redis.multi 49 | redis.srem("pubsub:#{domain}:subscribers_#{node}", jid.to_s) 50 | redis.srem("pubsub:#{domain}:subscriptions_#{jid}", node) 51 | redis.exec 52 | redis.scard("pubsub:#{domain}:subscribers_#{node}") do |count| 53 | delete_node(domain, node) if count == 0 54 | end 55 | end 56 | 57 | # Unsubscribe the JID from all pubsub topics. This is useful when the 58 | # JID's session ends by logout or disconnect. 59 | def unsubscribe_all(domain, jid) 60 | jid = JID.new(jid) 61 | redis.smembers("pubsub:#{domain}:subscriptions_#{jid}") do |nodes| 62 | nodes.each do |node| 63 | unsubscribe(domain, node, jid) 64 | end 65 | end 66 | end 67 | 68 | # Return true if the pubsub topic exists and messages may be published to it. 69 | def node?(domain, node) 70 | @cluster.query(:sismember, "pubsub:#{domain}:nodes", node) == 1 71 | end 72 | 73 | # Return true if the JID is a registered subscriber to the pubsub topic and 74 | # messages published to it should be routed to the JID. 75 | def subscribed?(domain, node, jid) 76 | jid = JID.new(jid) 77 | @cluster.query(:sismember, "pubsub:#{domain}:subscribers_#{node}", jid.to_s) == 1 78 | end 79 | 80 | # Return a list of JIDs subscribed to the pubsub topic. 81 | def subscribers(domain, node) 82 | @cluster.query(:smembers, "pubsub:#{domain}:subscribers_#{node}") 83 | end 84 | 85 | private 86 | 87 | def redis 88 | @cluster.connection 89 | end 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/vines/command/bcrypt.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | module Command 5 | class Bcrypt 6 | def run(opts) 7 | raise 'vines bcrypt ' unless opts[:args].size == 1 8 | puts BCrypt::Password.create(opts[:args].first) 9 | end 10 | end 11 | end 12 | end -------------------------------------------------------------------------------- /lib/vines/command/cert.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | module Command 5 | class Cert 6 | def run(opts) 7 | raise 'vines cert ' unless opts[:args].size == 1 8 | require opts[:config] 9 | create_cert(opts[:args].first, Config.instance.certs) 10 | end 11 | 12 | def create_cert(domain, dir) 13 | domain = domain.downcase 14 | key = OpenSSL::PKey::RSA.generate(2048) 15 | ca = OpenSSL::X509::Name.parse("/C=US/ST=Colorado/L=Denver/O=Vines XMPP Server/CN=#{domain}") 16 | cert = OpenSSL::X509::Certificate.new 17 | cert.version = 2 18 | cert.subject = ca 19 | cert.issuer = ca 20 | cert.serial = Time.now.to_i 21 | cert.public_key = key.public_key 22 | cert.not_before = Time.now - (24 * 60 * 60) 23 | cert.not_after = Time.now + (365 * 24 * 60 * 60) 24 | 25 | factory = OpenSSL::X509::ExtensionFactory.new 26 | factory.subject_certificate = cert 27 | factory.issuer_certificate = cert 28 | cert.extensions = [ 29 | %w[basicConstraints CA:TRUE], 30 | %w[subjectKeyIdentifier hash], 31 | %w[subjectAltName] << [domain, hostname].map {|n| "DNS:#{n}" }.join(',') 32 | ].map {|k, v| factory.create_ext(k, v) } 33 | 34 | cert.sign(key, OpenSSL::Digest::SHA1.new) 35 | 36 | {'key' => key, 'crt' => cert}.each_pair do |ext, o| 37 | name = File.join(dir, "#{domain}.#{ext}") 38 | File.open(name, 'w:utf-8') {|f| f.write(o.to_pem) } 39 | File.chmod(0600, name) if ext == 'key' 40 | end 41 | end 42 | 43 | private 44 | 45 | def hostname 46 | Socket.gethostbyname(Socket.gethostname).first.downcase 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/vines/command/init.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | module Command 5 | class Init 6 | def run(opts) 7 | raise 'vines init ' unless opts[:args].size == 1 8 | domain = opts[:args].first.downcase 9 | dir = File.expand_path(domain) 10 | raise "Directory already initialized: #{domain}" if File.exists?(dir) 11 | Dir.mkdir(dir) 12 | 13 | create_directories(dir) 14 | create_users(domain, dir) 15 | update_config(domain, dir) 16 | Command::Cert.new.create_cert(domain, File.join(dir, 'conf/certs')) 17 | 18 | puts "Initialized server directory: #{domain}" 19 | puts "Run 'cd #{domain} && vines start' to begin" 20 | end 21 | 22 | private 23 | 24 | # Limit file system database directory access so the server is the only 25 | # process managing the data. The config.rb file contains component and 26 | # database passwords, so restrict access to just the server user as well. 27 | def create_directories(dir) 28 | %w[conf web].each do |sub| 29 | FileUtils.cp_r(File.expand_path("../../../../#{sub}", __FILE__), dir) 30 | end 31 | %w[data log pid].each do |sub| 32 | Dir.mkdir(File.join(dir, sub), 0700) 33 | end 34 | File.chmod(0600, File.join(dir, 'conf/config.rb')) 35 | end 36 | 37 | def update_config(domain, dir) 38 | config = File.expand_path('conf/config.rb', dir) 39 | text = File.read(config, encoding: 'utf-8') 40 | File.open(config, 'w:utf-8') do |f| 41 | f.write(text.gsub('wonderland.lit', domain)) 42 | end 43 | end 44 | 45 | def create_users(domain, dir) 46 | password = 'secr3t' 47 | alice, arthur = %w[alice arthur].map do |jid| 48 | User.new(jid: [jid, domain].join('@'), 49 | password: BCrypt::Password.create(password).to_s) 50 | end 51 | 52 | [[alice, arthur], [arthur, alice]].each do |user, contact| 53 | user.roster << Contact.new( 54 | jid: contact.jid, 55 | name: contact.jid.node.capitalize, 56 | subscription: 'both', 57 | groups: %w[Buddies]) 58 | end 59 | 60 | storage = Storage::Local.new { dir(File.join(dir, 'data')) } 61 | [alice, arthur].each do |user| 62 | storage.save_user(user) 63 | puts "Created example user #{user.jid} with password #{password}" 64 | end 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/vines/command/ldap.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | module Command 5 | class Ldap 6 | def run(opts) 7 | raise 'vines ldap ' unless opts[:args].size == 1 8 | require opts[:config] 9 | domain = opts[:args].first 10 | unless storage = Config.instance.vhost(domain).storage rescue nil 11 | raise "#{domain} virtual host not found in conf/config.rb" 12 | end 13 | unless storage.ldap? 14 | raise "LDAP connector not configured for #{domain} virtual host" 15 | end 16 | $stdout.write('JID: ') 17 | jid = $stdin.gets.chomp 18 | jid = [jid, domain].join('@') unless jid.include?('@') 19 | $stdout.write('Password: ') 20 | `stty -echo` 21 | password = $stdin.gets.chomp 22 | `stty echo` 23 | puts 24 | 25 | begin 26 | user = storage.ldap.authenticate(jid, password) 27 | rescue => e 28 | raise "LDAP connection failed: #{e.message}" 29 | end 30 | 31 | filter = storage.ldap.filter(jid) 32 | raise "User not found with filter:\n #{filter}" unless user 33 | name = user.name.empty? ? '' : user.name 34 | puts "Found user #{name} with filter:\n #{filter}" 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/vines/command/restart.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | module Command 5 | class Restart 6 | def run(opts) 7 | Stop.new.run(opts) 8 | Start.new.run(opts) 9 | end 10 | end 11 | end 12 | end -------------------------------------------------------------------------------- /lib/vines/command/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | module Command 5 | class Schema 6 | def run(opts) 7 | raise 'vines schema ' unless opts[:args].size == 1 8 | require opts[:config] 9 | domain = opts[:args].first 10 | unless storage = Config.instance.vhost(domain).storage rescue nil 11 | raise "#{domain} virtual host not found in conf/config.rb" 12 | end 13 | unless storage.respond_to?(:create_schema) 14 | raise "SQL storage not configured for #{domain} virtual host" 15 | end 16 | begin 17 | storage.create_schema 18 | rescue => e 19 | raise "Schema creation failed: #{e.message}" 20 | end 21 | end 22 | end 23 | end 24 | end -------------------------------------------------------------------------------- /lib/vines/command/start.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | module Command 5 | class Start 6 | def run(opts) 7 | raise 'vines [--pid FILE] start' unless opts[:args].size == 0 8 | require opts[:config] 9 | server = XmppServer.new(Config.instance) 10 | daemonize(opts) if opts[:daemonize] 11 | server.start 12 | end 13 | 14 | private 15 | 16 | def daemonize(opts) 17 | daemon = Daemon.new(:pid => opts[:pid], :stdout => opts[:log], 18 | :stderr => opts[:log]) 19 | if daemon.running? 20 | raise "Vines is running as process #{daemon.pid}" 21 | else 22 | puts "Vines has started" 23 | daemon.start 24 | end 25 | end 26 | end 27 | end 28 | end -------------------------------------------------------------------------------- /lib/vines/command/stop.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | module Command 5 | class Stop 6 | def run(opts) 7 | raise 'vines [--pid FILE] stop' unless opts[:args].size == 0 8 | daemon = Daemon.new(:pid => opts[:pid]) 9 | if daemon.running? 10 | daemon.stop 11 | puts 'Vines has been shutdown' 12 | else 13 | puts 'Vines is not running' 14 | end 15 | end 16 | end 17 | end 18 | end -------------------------------------------------------------------------------- /lib/vines/config/host.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Config 5 | 6 | # Provides the DSL methods for the virtual host definitions in the 7 | # conf/config.rb file. Host instances can be accessed at runtime through 8 | # the +Config#vhosts+ method. 9 | class Host 10 | attr_reader :pubsubs 11 | 12 | def initialize(config, name, &block) 13 | @config, @name = config, name.downcase 14 | @storage, @ldap = nil, nil 15 | @cross_domain_messages = false 16 | @private_storage = false 17 | @components, @pubsubs = {}, {} 18 | validate_domain(@name) 19 | instance_eval(&block) 20 | raise "storage required for #{@name}" unless @storage 21 | end 22 | 23 | def storage(name=nil, &block) 24 | if name 25 | raise "one storage mechanism per host allowed" if @storage 26 | @storage = Storage.from_name(name, &block) 27 | @storage.ldap = @ldap 28 | else 29 | @storage 30 | end 31 | end 32 | 33 | def ldap(host='localhost', port=636, &block) 34 | @ldap = Storage::Ldap.new(host, port, &block) 35 | @storage.ldap = @ldap if @storage 36 | end 37 | 38 | def cross_domain_messages(enabled) 39 | @cross_domain_messages = !!enabled 40 | end 41 | 42 | def cross_domain_messages? 43 | @cross_domain_messages 44 | end 45 | 46 | def components(options=nil) 47 | return @components unless options 48 | 49 | names = options.keys.map {|domain| "#{domain}.#{@name}".downcase } 50 | raise "duplicate component domains not allowed" if dupes?(names, @components.keys) 51 | raise "pubsub domains overlap component domains" if dupes?(names, @pubsubs.keys) 52 | 53 | options.each do |domain, password| 54 | raise 'component domain required' if (domain || '').to_s.strip.empty? 55 | raise 'component password required' if (password || '').strip.empty? 56 | name = "#{domain}.#{@name}".downcase 57 | raise "components must be one level below their host: #{name}" if domain.to_s.include?('.') 58 | validate_domain(name) 59 | @components[name] = password 60 | end 61 | end 62 | 63 | def component?(domain) 64 | !!@components[domain.to_s] 65 | end 66 | 67 | def password(domain) 68 | @components[domain.to_s] 69 | end 70 | 71 | def pubsub(*domains) 72 | domains.flatten! 73 | raise 'define at least one pubsub domain' if domains.empty? 74 | names = domains.map {|domain| "#{domain}.#{@name}".downcase } 75 | raise "duplicate pubsub domains not allowed" if dupes?(names, @pubsubs.keys) 76 | raise "pubsub domains overlap component domains" if dupes?(names, @components.keys) 77 | domains.each do |domain| 78 | raise 'pubsub domain required' if (domain || '').to_s.strip.empty? 79 | name = "#{domain}.#{@name}".downcase 80 | raise "pubsub domains must be one level below their host: #{name}" if domain.to_s.include?('.') 81 | validate_domain(name) 82 | @pubsubs[name] = PubSub.new(@config, name) 83 | end 84 | end 85 | 86 | def pubsub?(domain) 87 | @pubsubs.key?(domain.to_s) 88 | end 89 | 90 | # Unsubscribe this JID from all pubsub topics hosted at this virtual host. 91 | # This should be called when the user's session ends via logout or 92 | # disconnect. 93 | def unsubscribe_pubsub(jid) 94 | @pubsubs.values.each do |pubsub| 95 | pubsub.unsubscribe_all(jid) 96 | end 97 | end 98 | 99 | def disco_items 100 | [@components.keys, @pubsubs.keys].flatten.sort 101 | end 102 | 103 | def private_storage(enabled) 104 | @private_storage = !!enabled 105 | end 106 | 107 | def private_storage? 108 | @private_storage 109 | end 110 | 111 | private 112 | 113 | # Return true if the arrays contain any duplicate items. 114 | def dupes?(a, b) 115 | a.uniq.size != a.size || b.uniq.size != b.size || (a & b).any? 116 | end 117 | 118 | # Prevent domains in config files that won't form valid JIDs. 119 | def validate_domain(name) 120 | jid = JID.new(name) 121 | raise "incorrect domain: #{name}" if jid.node || jid.resource 122 | end 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /lib/vines/config/port.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Config 5 | class Port 6 | include Vines::Log 7 | 8 | attr_reader :config, :stream 9 | 10 | %w[host port].each do |name| 11 | define_method(name) do 12 | @settings[name.to_sym] 13 | end 14 | end 15 | 16 | def initialize(config, host, port, &block) 17 | @config, @settings = config, {} 18 | instance_eval(&block) if block 19 | defaults = {:host => host, :port => port, 20 | :max_resources_per_account => 5, :max_stanza_size => 128 * 1024} 21 | @settings = defaults.merge(@settings) 22 | end 23 | 24 | def max_stanza_size(max=nil) 25 | if max 26 | # rfc 6120 section 13.12 27 | @settings[:max_stanza_size] = [10000, max].max 28 | else 29 | @settings[:max_stanza_size] 30 | end 31 | end 32 | 33 | def start 34 | type = stream.name.split('::').last.downcase 35 | log.info("Accepting #{type} connections on #{host}:#{port}") 36 | EventMachine::start_server(host, port, stream, config) 37 | end 38 | end 39 | 40 | class ClientPort < Port 41 | def initialize(config, host='0.0.0.0', port=5222, &block) 42 | @stream = Vines::Stream::Client 43 | super(config, host, port, &block) 44 | end 45 | 46 | def max_resources_per_account(max=nil) 47 | if max 48 | @settings[:max_resources_per_account] = max 49 | else 50 | @settings[:max_resources_per_account] 51 | end 52 | end 53 | 54 | def start 55 | super 56 | config.cluster.start if config.cluster? 57 | end 58 | end 59 | 60 | class ServerPort < Port 61 | def initialize(config, host='0.0.0.0', port=5269, &block) 62 | @hosts, @stream = [], Vines::Stream::Server 63 | super(config, host, port, &block) 64 | end 65 | 66 | def hosts(*hosts) 67 | if hosts.any? 68 | @hosts << hosts 69 | @hosts.flatten! 70 | else 71 | @hosts 72 | end 73 | end 74 | end 75 | 76 | class HttpPort < Port 77 | def initialize(config, host='0.0.0.0', port=5280, &block) 78 | @stream = Vines::Stream::Http 79 | super(config, host, port, &block) 80 | defaults = {:root => File.expand_path('web'), :bind => '/xmpp'} 81 | @settings = defaults.merge(@settings) 82 | end 83 | 84 | def max_resources_per_account(max=nil) 85 | if max 86 | @settings[:max_resources_per_account] = max 87 | else 88 | @settings[:max_resources_per_account] 89 | end 90 | end 91 | 92 | def root(dir=nil) 93 | if dir 94 | @settings[:root] = File.expand_path(dir) 95 | else 96 | @settings[:root] 97 | end 98 | end 99 | 100 | def bind(url=nil) 101 | if url 102 | @settings[:bind] = url 103 | else 104 | @settings[:bind] 105 | end 106 | end 107 | 108 | def vroute(id=nil) 109 | if id 110 | id = id.to_s.strip 111 | @settings[:vroute] = id.empty? ? nil : id 112 | else 113 | @settings[:vroute] 114 | end 115 | end 116 | 117 | def start 118 | super 119 | if config.cluster? && vroute.nil? 120 | log.warn("vroute sticky session cookie not set") 121 | end 122 | end 123 | end 124 | 125 | class ComponentPort < Port 126 | def initialize(config, host='0.0.0.0', port=5347, &block) 127 | @stream = Vines::Stream::Component 128 | super(config, host, port, &block) 129 | end 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/vines/config/pubsub.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Config 5 | # Provides the configuration DSL to conf/config.rb for pubsub subdomains and 6 | # exposes the storage and notification systems that the pubsub stanzas need 7 | # to process. This class hides the complexity of determining pubsub behavior 8 | # in a standalone vs. clustered chat server environment from the stanzas. 9 | class PubSub 10 | def initialize(config, name) 11 | @config, @name = config, name 12 | @nodes = {} 13 | end 14 | 15 | def add_node(id) 16 | if @config.cluster? 17 | @config.cluster.add_pubsub_node(@name, id) 18 | else 19 | @nodes[id] ||= Set.new 20 | end 21 | end 22 | 23 | def delete_node(id) 24 | if @config.cluster? 25 | @config.cluster.delete_pubsub_node(@name, id) 26 | else 27 | @nodes.delete(id) 28 | end 29 | end 30 | 31 | def subscribe(node, jid) 32 | return unless node?(node) && @config.allowed?(jid, @name) 33 | if @config.cluster? 34 | @config.cluster.subscribe_pubsub(@name, node, jid) 35 | else 36 | @nodes[node] << JID.new(jid) 37 | end 38 | end 39 | 40 | def unsubscribe(node, jid) 41 | return unless node?(node) 42 | if @config.cluster? 43 | @config.cluster.unsubscribe_pubsub(@name, node, jid) 44 | else 45 | @nodes[node].delete(JID.new(jid)) 46 | delete_node(node) if subscribers(node).empty? 47 | end 48 | end 49 | 50 | def unsubscribe_all(jid) 51 | if @config.cluster? 52 | @config.cluster.unsubscribe_all_pubsub(@name, jid) 53 | else 54 | @nodes.keys.each do |node| 55 | unsubscribe(node, jid) 56 | end 57 | end 58 | end 59 | 60 | def node?(node) 61 | if @config.cluster? 62 | @config.cluster.pubsub_node?(@name, node) 63 | else 64 | @nodes.key?(node) 65 | end 66 | end 67 | 68 | def subscribed?(node, jid) 69 | return false unless node?(node) 70 | if @config.cluster? 71 | @config.cluster.pubsub_subscribed?(@name, node, jid) 72 | else 73 | @nodes[node].include?(JID.new(jid)) 74 | end 75 | end 76 | 77 | def publish(node, stanza) 78 | stanza['id'] = Kit.uuid 79 | stanza['from'] = @name 80 | 81 | local, remote = subscribers(node).partition {|jid| @config.local_jid?(jid) } 82 | 83 | local.flat_map do |jid| 84 | @config.router.connected_resources(jid, @name) 85 | end.each do |recipient| 86 | stanza['to'] = recipient.user.jid.to_s 87 | recipient.write(stanza) 88 | end 89 | 90 | remote.each do |jid| 91 | el = stanza.clone 92 | el['to'] = jid.to_s 93 | @config.router.route(el) rescue nil # ignore RemoteServerNotFound 94 | end 95 | end 96 | 97 | private 98 | 99 | def subscribers(node) 100 | if @config.cluster? 101 | @config.cluster.pubsub_subscribers(@name, node) 102 | else 103 | @nodes[node] || [] 104 | end 105 | end 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /lib/vines/contact.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Contact 5 | include Comparable 6 | 7 | attr_accessor :name, :subscription, :ask, :groups 8 | attr_reader :jid 9 | 10 | def initialize(args={}) 11 | @jid = JID.new(args[:jid]).bare 12 | raise ArgumentError, 'invalid jid' if @jid.empty? 13 | @name = args[:name] 14 | @subscription = args[:subscription] || 'none' 15 | @ask = args[:ask] 16 | @groups = args[:groups] || [] 17 | end 18 | 19 | def <=>(contact) 20 | contact.is_a?(Contact) ? self.jid.to_s <=> contact.jid.to_s : nil 21 | end 22 | 23 | alias :eql? :== 24 | 25 | def hash 26 | jid.to_s.hash 27 | end 28 | 29 | def update_from(contact) 30 | @name = contact.name 31 | @subscription = contact.subscription 32 | @ask = contact.ask 33 | @groups = contact.groups.clone 34 | end 35 | 36 | # Returns true if this contact is in a state that allows the user 37 | # to subscribe to their presence updates. 38 | def can_subscribe? 39 | @ask == 'subscribe' && %w[none from].include?(@subscription) 40 | end 41 | 42 | def subscribe_to 43 | @subscription = (@subscription == 'none') ? 'to' : 'both' 44 | @ask = nil 45 | end 46 | 47 | def unsubscribe_to 48 | @subscription = (@subscription == 'both') ? 'from' : 'none' 49 | end 50 | 51 | def subscribe_from 52 | @subscription = (@subscription == 'none') ? 'from' : 'both' 53 | @ask = nil 54 | end 55 | 56 | def unsubscribe_from 57 | @subscription = (@subscription == 'both') ? 'to' : 'none' 58 | end 59 | 60 | # Returns true if the user is subscribed to this contact's 61 | # presence updates. 62 | def subscribed_to? 63 | %w[to both].include?(@subscription) 64 | end 65 | 66 | # Returns true if the user has a presence subscription from 67 | # this contact. The contact is subscribed to this user's presence. 68 | def subscribed_from? 69 | %w[from both].include?(@subscription) 70 | end 71 | 72 | # Returns a hash of this contact's attributes suitable for persisting in 73 | # a document store. 74 | def to_h 75 | { 76 | 'name' => @name, 77 | 'subscription' => @subscription, 78 | 'ask' => @ask, 79 | 'groups' => @groups.sort! 80 | } 81 | end 82 | 83 | # Write an iq stanza to the recipient stream representing this contact's 84 | # current roster item state. 85 | def send_roster_push(recipient) 86 | doc = Nokogiri::XML::Document.new 87 | node = doc.create_element('iq', 88 | 'id' => Kit.uuid, 89 | 'to' => recipient.user.jid.to_s, 90 | 'type' => 'set') 91 | node << doc.create_element('query', 'xmlns' => NAMESPACES[:roster]) do |query| 92 | query << to_roster_xml 93 | end 94 | recipient.write(node) 95 | end 96 | 97 | # Returns this contact as an xmpp element. 98 | def to_roster_xml 99 | doc = Nokogiri::XML::Document.new 100 | doc.create_element('item') do |el| 101 | el['ask'] = @ask unless @ask.nil? || @ask.empty? 102 | el['jid'] = @jid.bare.to_s 103 | el['name'] = @name unless @name.nil? || @name.empty? 104 | el['subscription'] = @subscription 105 | @groups.sort!.each do |group| 106 | el << doc.create_element('group', group) 107 | end 108 | end 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/vines/daemon.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | 5 | # Fork the current process into the background and manage pid 6 | # files so we can kill the process later. 7 | class Daemon 8 | 9 | # Configure a new daemon process. Arguments hash can include the following 10 | # keys: :pid (pid file name, required), 11 | # :stdin, :stdout, :stderr (default to /dev/null) 12 | def initialize(args) 13 | @pid = args[:pid] 14 | raise ArgumentError.new('pid file is required') unless @pid 15 | raise ArgumentError.new('pid must be a file name') if File.directory?(@pid) 16 | raise ArgumentError.new('pid file must be writable') unless File.writable?(File.dirname(@pid)) 17 | @stdin, @stdout, @stderr = [:stdin, :stdout, :stderr].map {|k| args[k] || '/dev/null' } 18 | end 19 | 20 | # Fork the current process into the background to start the 21 | # daemon. Do nothing if the daemon is already running. 22 | def start 23 | daemonize unless running? 24 | end 25 | 26 | # Use the pid stored in the pid file created from a previous 27 | # call to start to send a TERM signal to the process. Do nothing 28 | # if the daemon is not running. 29 | def stop 30 | 10.times do 31 | break unless running? 32 | Process.kill('TERM', pid) 33 | sleep(0.1) 34 | end 35 | end 36 | 37 | # Returns true if the process is running as determined by the numeric 38 | # pid stored in the pid file created by a previous call to start. 39 | def running? 40 | begin 41 | pid && Process.kill(0, pid) 42 | rescue Errno::ESRCH 43 | delete_pid 44 | false 45 | rescue Errno::EPERM 46 | true 47 | end 48 | end 49 | 50 | # Returns the numeric process ID from the pid file. 51 | # If the pid file does not exist, returns nil. 52 | def pid 53 | File.read(@pid).to_i if File.exists?(@pid) 54 | end 55 | 56 | private 57 | 58 | def delete_pid 59 | File.delete(@pid) if File.exists?(@pid) 60 | end 61 | 62 | # Fork process into background twice to release it from 63 | # the controlling tty. Point open file descriptors shared 64 | # with the parent process to separate destinations (e.g. /dev/null). 65 | def daemonize 66 | exit if fork 67 | Process.setsid 68 | exit if fork 69 | Dir.chdir('/') 70 | $stdin.reopen(@stdin) 71 | $stdout.reopen(@stdout, 'a').sync = true 72 | $stderr.reopen(@stderr, 'a').sync = true 73 | File.open(@pid, 'w') {|f| f.write(Process.pid) } 74 | at_exit { delete_pid } 75 | trap('TERM') { exit } 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/vines/jid.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class JID 5 | include Comparable 6 | 7 | PATTERN = /\A(?:([^@]*)@)??([^@\/]*)(?:\/(.*?))?\Z/.freeze 8 | 9 | # http://tools.ietf.org/html/rfc6122#appendix-A 10 | NODE_PREP = /[[:cntrl:] "&'\/:<>@]/.freeze 11 | 12 | # http://tools.ietf.org/html/rfc3454#appendix-C 13 | NAME_PREP = /[[:cntrl:] ]/.freeze 14 | 15 | # http://tools.ietf.org/html/rfc6122#appendix-B 16 | RESOURCE_PREP = /[[:cntrl:]]/.freeze 17 | 18 | attr_reader :node, :domain, :resource 19 | attr_writer :resource 20 | 21 | def self.new(node, domain=nil, resource=nil) 22 | node.is_a?(JID) ? node : super 23 | end 24 | 25 | def initialize(node, domain=nil, resource=nil) 26 | @node, @domain, @resource = node, domain, resource 27 | 28 | if @domain.nil? && @resource.nil? 29 | @node, @domain, @resource = @node.to_s.scan(PATTERN).first 30 | end 31 | [@node, @domain].each {|part| part.downcase! if part } 32 | 33 | validate 34 | end 35 | 36 | # Strip the resource part from this JID and return it as a new 37 | # JID object. The new JID contains only the optional node part 38 | # and the required domain part from the original. This JID remains 39 | # unchanged. 40 | def bare 41 | JID.new(@node, @domain) 42 | end 43 | 44 | # Return true if this is a bare JID without a resource part. 45 | def bare? 46 | @resource.nil? 47 | end 48 | 49 | # Return true if this is a domain-only JID without a node or resource part. 50 | def domain? 51 | !empty? && to_s == @domain 52 | end 53 | 54 | # Return true if this JID is equal to the empty string ''. That is, it's 55 | # missing the node, domain, and resource parts that form a valid JID. It 56 | # makes for easier error handling to be able to create JID objects from 57 | # strings and then check if they're empty rather than nil. 58 | def empty? 59 | to_s == '' 60 | end 61 | 62 | def <=>(jid) 63 | self.to_s <=> jid.to_s 64 | end 65 | 66 | def eql?(jid) 67 | jid.is_a?(JID) && self == jid 68 | end 69 | 70 | def hash 71 | self.to_s.hash 72 | end 73 | 74 | def to_s 75 | s = @domain 76 | s = "#{@node}@#{s}" if @node 77 | s = "#{s}/#{@resource}" if @resource 78 | s 79 | end 80 | 81 | private 82 | 83 | def validate 84 | [@node, @domain, @resource].each do |part| 85 | raise ArgumentError, 'jid too long' if (part || '').size > 1023 86 | end 87 | raise ArgumentError, 'empty node' if @node && @node.strip.empty? 88 | raise ArgumentError, 'node contains invalid characters' if @node && @node =~ NODE_PREP 89 | raise ArgumentError, 'empty resource' if @resource && @resource.strip.empty? 90 | raise ArgumentError, 'resource contains invalid characters' if @resource && @resource =~ RESOURCE_PREP 91 | raise ArgumentError, 'empty domain' if @domain == '' && (@node || @resource) 92 | raise ArgumentError, 'domain contains invalid characters' if @domain && @domain =~ NAME_PREP 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /lib/vines/kit.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | # A module for utility methods with no better home. 5 | module Kit 6 | # Create a hex-encoded, SHA-512 HMAC of the data, using the secret key. 7 | def self.hmac(key, data) 8 | digest = OpenSSL::Digest.new("sha512") 9 | OpenSSL::HMAC.hexdigest(digest, key, data) 10 | end 11 | 12 | # Generates a random uuid per rfc 4122 that's useful for including in 13 | # stream, iq, and other xmpp stanzas. 14 | def self.uuid 15 | SecureRandom.uuid 16 | end 17 | 18 | # Generates a random 128 character authentication token. 19 | def self.auth_token 20 | SecureRandom.hex(64) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/vines/log.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | module Log 5 | @@logger = nil 6 | def log 7 | unless @@logger 8 | @@logger = Logger.new(STDOUT) 9 | @@logger.level = Logger::INFO 10 | @@logger.progname = 'vines' 11 | @@logger.formatter = Class.new(Logger::Formatter) do 12 | def initialize 13 | @time = "%Y-%m-%dT%H:%M:%SZ".freeze 14 | @fmt = "[%s] %5s -- %s: %s\n".freeze 15 | end 16 | def call(severity, time, program, msg) 17 | @fmt % [time.utc.strftime(@time), severity, program, msg2str(msg)] 18 | end 19 | end.new 20 | end 21 | @@logger 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/vines/stanza/iq.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stanza 5 | class Iq < Stanza 6 | register "/iq" 7 | 8 | VALID_TYPES = %w[get set result error].freeze 9 | 10 | VALID_TYPES.each do |type| 11 | define_method "#{type}?" do 12 | self['type'] == type 13 | end 14 | end 15 | 16 | def process 17 | if self['id'] && VALID_TYPES.include?(self['type']) 18 | route_iq or raise StanzaErrors::FeatureNotImplemented.new(@node, 'cancel') 19 | else 20 | raise StanzaErrors::BadRequest.new(@node, 'modify') 21 | end 22 | end 23 | 24 | def to_result 25 | doc = Document.new 26 | doc.create_element('iq', 27 | 'from' => validate_to || stream.domain, 28 | 'id' => self['id'], 29 | 'to' => stream.user.jid, 30 | 'type' => 'result') 31 | end 32 | 33 | private 34 | 35 | # Return false if this IQ stanza is addressed to the server, or a pubsub 36 | # service hosted here, and must be handled locally. Return true if the 37 | # stanza must not be handled locally and has been routed to the appropriate 38 | # component, s2s, or c2s stream. 39 | def route_iq 40 | to = validate_to 41 | return false if to.nil? || stream.config.vhost?(to) || to_pubsub_domain? 42 | self['from'] = stream.user.jid.to_s 43 | local? ? broadcast(stream.connected_resources(to)) : route 44 | true 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/vines/stanza/iq/auth.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stanza 5 | class Iq 6 | class Auth < Query 7 | register "/iq[@id and @type='get']/ns:query", 'ns' => NAMESPACES[:non_sasl] 8 | 9 | def process 10 | # XEP-0078 says we MUST send a service-unavailable error 11 | # here, but Adium 1.4.1 won't login if we do that, so just 12 | # swallow this stanza. 13 | # raise StanzaErrors::ServiceUnavailable.new(@node, 'cancel') 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/vines/stanza/iq/disco_info.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stanza 5 | class Iq 6 | class DiscoInfo < Query 7 | NS = NAMESPACES[:disco_info] 8 | 9 | register "/iq[@id and @type='get']/ns:query", 'ns' => NS 10 | 11 | def process 12 | return if route_iq || !allowed? 13 | result = to_result.tap do |el| 14 | el << el.document.create_element('query') do |query| 15 | query.default_namespace = NS 16 | if to_pubsub_domain? 17 | identity(query, 'pubsub', 'service') 18 | pubsub = [:pubsub_create, :pubsub_delete, :pubsub_instant, :pubsub_item_ids, :pubsub_publish, :pubsub_subscribe] 19 | features(query, :disco_info, :ping, :pubsub, *pubsub) 20 | else 21 | identity(query, 'server', 'im') 22 | features = [:disco_info, :disco_items, :ping, :vcard, :version] 23 | features << :storage if stream.config.private_storage?(validate_to || stream.domain) 24 | features(query, features) 25 | end 26 | end 27 | end 28 | stream.write(result) 29 | end 30 | 31 | private 32 | 33 | def identity(query, category, type) 34 | query << query.document.create_element('identity', 'category' => category, 'type' => type) 35 | end 36 | 37 | def features(query, *features) 38 | features.flatten.each do |feature| 39 | query << query.document.create_element('feature', 'var' => NAMESPACES[feature]) 40 | end 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/vines/stanza/iq/disco_items.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stanza 5 | class Iq 6 | class DiscoItems < Query 7 | NS = NAMESPACES[:disco_items] 8 | 9 | register "/iq[@id and @type='get']/ns:query", 'ns' => NS 10 | 11 | def process 12 | return if route_iq || !allowed? 13 | result = to_result.tap do |el| 14 | el << el.document.create_element('query') do |query| 15 | query.default_namespace = NS 16 | unless to_pubsub_domain? 17 | to = (validate_to || stream.domain).to_s 18 | stream.config.vhost(to).disco_items.each do |domain| 19 | query << el.document.create_element('item', 'jid' => domain) 20 | end 21 | end 22 | end 23 | end 24 | stream.write(result) 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/vines/stanza/iq/error.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stanza 5 | class Iq 6 | class Error < Iq 7 | register "/iq[@id and @type='error']" 8 | 9 | def process 10 | return if route_iq 11 | # do nothing 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/vines/stanza/iq/ping.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stanza 5 | class Iq 6 | class Ping < Iq 7 | register "/iq[@id and @type='get']/ns:ping", 'ns' => NAMESPACES[:ping] 8 | 9 | def process 10 | return if route_iq || !allowed? 11 | stream.write(to_result) 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/vines/stanza/iq/private_storage.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stanza 5 | class Iq 6 | # Implements the Private Storage feature defined in XEP-0049. Clients are 7 | # allowed to save arbitrary XML documents on the server, identified by 8 | # element name and namespace. 9 | class PrivateStorage < Query 10 | NS = NAMESPACES[:storage] 11 | 12 | register "/iq[@id and (@type='get' or @type='set')]/ns:query", 'ns' => NS 13 | 14 | def process 15 | validate_to_address 16 | validate_storage_enabled 17 | validate_children_size 18 | validate_namespaces 19 | get? ? retrieve_fragment : update_fragment 20 | end 21 | 22 | private 23 | 24 | def retrieve_fragment 25 | found = storage.find_fragment(stream.user.jid, elements.first.elements.first) 26 | raise StanzaErrors::ItemNotFound.new(self, 'cancel') unless found 27 | 28 | result = to_result do |node| 29 | node << node.document.create_element('query') do |query| 30 | query.default_namespace = NS 31 | query << found 32 | end 33 | end 34 | stream.write(result) 35 | end 36 | 37 | def update_fragment 38 | elements.first.elements.each do |node| 39 | storage.save_fragment(stream.user.jid, node) 40 | end 41 | stream.write(to_result) 42 | end 43 | 44 | private 45 | 46 | def to_result 47 | super.tap do |node| 48 | node['from'] = stream.user.jid.to_s 49 | yield node if block_given? 50 | end 51 | end 52 | 53 | def validate_children_size 54 | size = elements.first.elements.size 55 | if (get? && size != 1) || (set? && size == 0) 56 | raise StanzaErrors::NotAcceptable.new(self, 'modify') 57 | end 58 | end 59 | 60 | def validate_to_address 61 | to = validate_to 62 | unless to.nil? || to == stream.user.jid.bare 63 | raise StanzaErrors::Forbidden.new(self, 'cancel') 64 | end 65 | end 66 | 67 | def validate_storage_enabled 68 | unless stream.config.private_storage?(stream.domain) 69 | raise StanzaErrors::ServiceUnavailable.new(self, 'cancel') 70 | end 71 | end 72 | 73 | def validate_namespaces 74 | elements.first.elements.each do |node| 75 | if node.namespace.nil? || NAMESPACES.values.include?(node.namespace.href) 76 | raise StanzaErrors::NotAcceptable.new(self, 'modify') 77 | end 78 | end 79 | end 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/vines/stanza/iq/query.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stanza 5 | class Iq 6 | class Query < Iq 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/vines/stanza/iq/result.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stanza 5 | class Iq 6 | class Result < Iq 7 | register "/iq[@id and @type='result']" 8 | 9 | def process 10 | return if route_iq 11 | # do nothing 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/vines/stanza/iq/session.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stanza 5 | class Iq 6 | # Session support is deprecated, but Adium requires it, so reply with an 7 | # iq result stanza. 8 | class Session < Iq 9 | register "/iq[@id and @type='set']/ns:session", 'ns' => NAMESPACES[:session] 10 | 11 | def process 12 | stream.write(to_result) 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/vines/stanza/iq/vcard.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stanza 5 | class Iq 6 | class Vcard < Iq 7 | NS = NAMESPACES[:vcard] 8 | 9 | register "/iq[@id and @type='get' or @type='set']/ns:vCard", 'ns' => NS 10 | 11 | def process 12 | return unless allowed? 13 | if local? 14 | get? ? vcard_query : vcard_update 15 | else 16 | self['from'] = stream.user.jid.to_s 17 | route 18 | end 19 | end 20 | 21 | private 22 | 23 | def vcard_query 24 | to = validate_to 25 | jid = to ? to.bare : stream.user.jid.bare 26 | card = storage(jid.domain).find_vcard(jid) 27 | 28 | raise StanzaErrors::ItemNotFound.new(self, 'cancel') unless card 29 | 30 | doc = Document.new 31 | result = doc.create_element('iq') do |node| 32 | node['from'] = jid.to_s unless jid == stream.user.jid.bare 33 | node['id'] = self['id'] 34 | node['to'] = stream.user.jid.to_s 35 | node['type'] = 'result' 36 | node << card 37 | end 38 | stream.write(result) 39 | end 40 | 41 | def vcard_update 42 | to = validate_to 43 | unless to.nil? || to == stream.user.jid.bare 44 | raise StanzaErrors::Forbidden.new(self, 'auth') 45 | end 46 | 47 | storage.save_vcard(stream.user.jid, elements.first) 48 | 49 | result = to_result 50 | result.remove_attribute('from') 51 | stream.write(result) 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/vines/stanza/iq/version.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stanza 5 | class Iq 6 | class Version < Query 7 | NS = NAMESPACES[:version] 8 | 9 | register "/iq[@id and @type='get']/ns:query", 'ns' => NS 10 | 11 | def process 12 | return if route_iq || to_pubsub_domain? || !allowed? 13 | result = to_result.tap do |node| 14 | node << node.document.create_element('query') do |query| 15 | query.default_namespace = NS 16 | query << node.document.create_element('name', 'Vines') 17 | query << node.document.create_element('version', VERSION) 18 | end 19 | end 20 | stream.write(result) 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/vines/stanza/message.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stanza 5 | class Message < Stanza 6 | register "/message" 7 | 8 | TYPE, FROM = %w[type from].map {|s| s.freeze } 9 | VALID_TYPES = %w[chat error groupchat headline normal].freeze 10 | 11 | VALID_TYPES.each do |type| 12 | define_method "#{type}?" do 13 | self[TYPE] == type 14 | end 15 | end 16 | 17 | def process 18 | unless self[TYPE].nil? || VALID_TYPES.include?(self[TYPE]) 19 | raise StanzaErrors::BadRequest.new(self, 'modify') 20 | end 21 | 22 | if local? 23 | to = validate_to || stream.user.jid.bare 24 | recipients = stream.connected_resources(to) 25 | if recipients.empty? 26 | if user = storage(to.domain).find_user(to) 27 | # TODO Implement offline messaging storage 28 | raise StanzaErrors::ServiceUnavailable.new(self, 'cancel') 29 | end 30 | else 31 | broadcast(recipients) 32 | end 33 | else 34 | self[FROM] = stream.user.jid.to_s 35 | route 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/vines/stanza/presence/error.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stanza 5 | class Presence 6 | class Error < Presence 7 | register "/presence[@type='error']" 8 | 9 | def process 10 | inbound? ? process_inbound : process_outbound 11 | end 12 | 13 | def process_outbound 14 | # FIXME Implement error handling 15 | end 16 | 17 | def process_inbound 18 | # FIXME Implement error handling 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/vines/stanza/presence/probe.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stanza 5 | class Presence 6 | class Probe < Presence 7 | register "/presence[@type='probe']" 8 | 9 | def process 10 | inbound? ? process_inbound : process_outbound 11 | end 12 | 13 | def process_outbound 14 | self['from'] = stream.user.jid.to_s 15 | local? ? process_inbound : route 16 | end 17 | 18 | def process_inbound 19 | to = validate_to 20 | raise StanzaErrors::BadRequest.new(self, 'modify') unless to 21 | 22 | user = storage(to.domain).find_user(to) 23 | unless user && user.subscribed_from?(stream.user.jid) 24 | auto_reply_to_subscription_request(to.bare, 'unsubscribed') 25 | else 26 | stream.available_resources(to).each do |recipient| 27 | el = recipient.last_broadcast_presence.clone 28 | el['from'] = recipient.user.jid.to_s 29 | el['to'] = stream.user.jid.to_s 30 | stream.write(el) 31 | end 32 | end 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/vines/stanza/presence/subscribe.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stanza 5 | class Presence 6 | class Subscribe < Presence 7 | register "/presence[@type='subscribe']" 8 | 9 | def process 10 | stamp_from 11 | inbound? ? process_inbound : process_outbound 12 | end 13 | 14 | def process_outbound 15 | to = stamp_to 16 | stream.user.request_subscription(to) 17 | storage.save_user(stream.user) 18 | stream.update_user_streams(stream.user) 19 | local? ? process_inbound : route 20 | send_roster_push(to) 21 | end 22 | 23 | def process_inbound 24 | to = stamp_to 25 | contact = storage(to.domain).find_user(to) 26 | if contact.nil? 27 | auto_reply_to_subscription_request(to, 'unsubscribed') 28 | elsif contact.subscribed_from?(stream.user.jid) 29 | auto_reply_to_subscription_request(to, 'subscribed') 30 | else 31 | recipients = stream.available_resources(to) 32 | if recipients.empty? 33 | # TODO store subscription request per RFC 6121 3.1.3 #4 34 | else 35 | broadcast_to_available_resources([@node], to) 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/vines/stanza/presence/subscribed.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stanza 5 | class Presence 6 | class Subscribed < Presence 7 | register "/presence[@type='subscribed']" 8 | 9 | def process 10 | stamp_from 11 | inbound? ? process_inbound : process_outbound 12 | end 13 | 14 | def process_outbound 15 | to = stamp_to 16 | stream.user.add_subscription_from(to) 17 | storage.save_user(stream.user) 18 | stream.update_user_streams(stream.user) 19 | local? ? process_inbound : route 20 | send_roster_push(to) 21 | send_known_presence(to) 22 | end 23 | 24 | def process_inbound 25 | to = stamp_to 26 | user = storage(to.domain).find_user(to) 27 | contact = user.contact(stream.user.jid) if user 28 | return unless contact && contact.can_subscribe? 29 | contact.subscribe_to 30 | storage(to.domain).save_user(user) 31 | stream.update_user_streams(user) 32 | broadcast_subscription_change(contact) 33 | end 34 | 35 | private 36 | 37 | # After approving a contact's subscription to this user's presence, 38 | # broadcast this user's most recent presence stanzas to the contact. 39 | def send_known_presence(to) 40 | stanzas = stream.available_resources(stream.user.jid).map do |stream| 41 | stream.last_broadcast_presence.clone.tap do |node| 42 | node['from'] = stream.user.jid.to_s 43 | node['id'] = Kit.uuid 44 | end 45 | end 46 | broadcast_to_available_resources(stanzas, to) 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/vines/stanza/presence/unavailable.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stanza 5 | class Presence 6 | class Unavailable < Presence 7 | register "/presence[@type='unavailable']" 8 | 9 | def process 10 | inbound? ? inbound_broadcast_presence : outbound_broadcast_presence 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/vines/stanza/presence/unsubscribe.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stanza 5 | class Presence 6 | class Unsubscribe < Presence 7 | register "/presence[@type='unsubscribe']" 8 | 9 | def process 10 | stamp_from 11 | inbound? ? process_inbound : process_outbound 12 | end 13 | 14 | def process_outbound 15 | to = stamp_to 16 | return unless stream.user.subscribed_to?(to) 17 | stream.user.remove_subscription_to(to) 18 | storage.save_user(stream.user) 19 | stream.update_user_streams(stream.user) 20 | local? ? process_inbound : route 21 | send_roster_push(to) 22 | end 23 | 24 | def process_inbound 25 | to = stamp_to 26 | user = storage(to.domain).find_user(to) 27 | return unless user && user.subscribed_from?(stream.user.jid) 28 | contact = user.contact(stream.user.jid) 29 | contact.unsubscribe_from 30 | storage(to.domain).save_user(user) 31 | stream.update_user_streams(user) 32 | broadcast_subscription_change(contact) 33 | send_unavailable(to, stream.user.jid.bare) 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/vines/stanza/presence/unsubscribed.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stanza 5 | class Presence 6 | class Unsubscribed < Presence 7 | register "/presence[@type='unsubscribed']" 8 | 9 | def process 10 | stamp_from 11 | inbound? ? process_inbound : process_outbound 12 | end 13 | 14 | def process_outbound 15 | to = stamp_to 16 | return unless stream.user.subscribed_from?(to) 17 | send_unavailable(stream.user.jid, to) 18 | stream.user.remove_subscription_from(to) 19 | storage.save_user(stream.user) 20 | stream.update_user_streams(stream.user) 21 | local? ? process_inbound : route 22 | send_roster_push(to) 23 | end 24 | 25 | def process_inbound 26 | to = stamp_to 27 | user = storage(to.domain).find_user(to) 28 | return unless user && user.subscribed_to?(stream.user.jid) 29 | contact = user.contact(stream.user.jid) 30 | contact.unsubscribe_to 31 | storage(to.domain).save_user(user) 32 | stream.update_user_streams(user) 33 | broadcast_subscription_change(contact) 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/vines/stanza/pubsub.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stanza 5 | class PubSub < Iq 6 | 7 | private 8 | 9 | # Return the Config::PubSub system for the domain to which this stanza is 10 | # addressed or nil if it's not to a pubsub subdomain. 11 | def pubsub 12 | stream.config.pubsub(validate_to) 13 | end 14 | 15 | # Raise feature-not-implemented if this stanza is addressed to the chat 16 | # server itself, rather than a pubsub subdomain. 17 | def validate_to_address 18 | raise StanzaErrors::FeatureNotImplemented.new(self, 'cancel') unless pubsub 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/vines/stanza/pubsub/create.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stanza 5 | class PubSub 6 | class Create < PubSub 7 | NS = NAMESPACES[:pubsub] 8 | 9 | register "/iq[@id and @type='set']/ns:pubsub/ns:create", 'ns' => NS 10 | 11 | def process 12 | return if route_iq || !allowed? 13 | validate_to_address 14 | 15 | node = self.xpath('ns:pubsub/ns:create', 'ns' => NS) 16 | raise StanzaErrors::BadRequest.new(self, 'modify') if node.size != 1 17 | node = node.first 18 | 19 | id = (node['node'] || '').strip 20 | id = Kit.uuid if id.empty? 21 | raise StanzaErrors::Conflict.new(self, 'cancel') if pubsub.node?(id) 22 | pubsub.add_node(id) 23 | send_result_iq(id) 24 | end 25 | 26 | private 27 | 28 | def send_result_iq(id) 29 | el = to_result 30 | el << el.document.create_element('pubsub') do |node| 31 | node.default_namespace = NS 32 | node << el.document.create_element('create', 'node' => id) 33 | end 34 | stream.write(el) 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/vines/stanza/pubsub/delete.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stanza 5 | class PubSub 6 | class Delete < PubSub 7 | NS = NAMESPACES[:pubsub] 8 | 9 | register "/iq[@id and @type='set']/ns:pubsub/ns:delete", 'ns' => NS 10 | 11 | def process 12 | return if route_iq || !allowed? 13 | validate_to_address 14 | 15 | node = self.xpath('ns:pubsub/ns:delete', 'ns' => NS) 16 | raise StanzaErrors::BadRequest.new(self, 'modify') if node.size != 1 17 | node = node.first 18 | 19 | id = node['node'] 20 | raise StanzaErrors::ItemNotFound.new(self, 'cancel') unless pubsub.node?(id) 21 | 22 | pubsub.publish(id, message(id)) 23 | pubsub.delete_node(id) 24 | stream.write(to_result) 25 | end 26 | 27 | private 28 | 29 | def message(id) 30 | doc = Document.new 31 | doc.create_element('message') do |node| 32 | node << node.document.create_element('event') do |event| 33 | event.default_namespace = NAMESPACES[:pubsub_event] 34 | event << node.document.create_element('delete', 'node' => id) 35 | end 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/vines/stanza/pubsub/publish.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stanza 5 | class PubSub 6 | class Publish < PubSub 7 | NS = NAMESPACES[:pubsub] 8 | 9 | register "/iq[@id and @type='set']/ns:pubsub/ns:publish", 'ns' => NS 10 | 11 | def process 12 | return if route_iq || !allowed? 13 | validate_to_address 14 | 15 | node = self.xpath('ns:pubsub/ns:publish', 'ns' => NS) 16 | raise StanzaErrors::BadRequest.new(self, 'modify') if node.size != 1 17 | node = node.first 18 | id = node['node'] 19 | 20 | raise StanzaErrors::ItemNotFound.new(self, 'cancel') unless pubsub.node?(id) 21 | 22 | item = node.xpath('ns:item', 'ns' => NS) 23 | raise StanzaErrors::BadRequest.new(self, 'modify') unless item.size == 1 24 | item = item.first 25 | unless item['id'] 26 | item['id'] = Kit.uuid 27 | include_item = true 28 | end 29 | 30 | raise StanzaErrors::BadRequest.new(self, 'modify') unless item.elements.size == 1 31 | pubsub.publish(id, message(id, item)) 32 | send_result_iq(id, include_item ? item : nil) 33 | end 34 | 35 | private 36 | 37 | def message(node, item) 38 | doc = Document.new 39 | doc.create_element('message') do |message| 40 | message << doc.create_element('event') do |event| 41 | event.default_namespace = NAMESPACES[:pubsub_event] 42 | event << doc.create_element('items', 'node' => node) do |items| 43 | items << doc.create_element('item', 'id' => item['id'], 'publisher' => stream.user.jid.to_s) do |el| 44 | el << item.elements.first 45 | end 46 | end 47 | end 48 | end 49 | end 50 | 51 | def send_result_iq(node, item) 52 | result = to_result 53 | if item 54 | result << result.document.create_element('pubsub') do |pubsub| 55 | pubsub.default_namespace = NS 56 | pubsub << result.document.create_element('publish', 'node' => node) do |publish| 57 | publish << result.document.create_element('item', 'id' => item['id']) 58 | end 59 | end 60 | end 61 | stream.write(result) 62 | end 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/vines/stanza/pubsub/subscribe.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stanza 5 | class PubSub 6 | class Subscribe < PubSub 7 | NS = NAMESPACES[:pubsub] 8 | 9 | register "/iq[@id and @type='set']/ns:pubsub/ns:subscribe", 'ns' => NS 10 | 11 | def process 12 | return if route_iq || !allowed? 13 | validate_to_address 14 | 15 | node = self.xpath('ns:pubsub/ns:subscribe', 'ns' => NS) 16 | raise StanzaErrors::BadRequest.new(self, 'modify') if node.size != 1 17 | node = node.first 18 | id, jid = node['node'], JID.new(node['jid']) 19 | 20 | raise StanzaErrors::BadRequest.new(self, 'modify') unless stream.user.jid.bare == jid.bare 21 | raise StanzaErrors::ItemNotFound.new(self, 'cancel') unless pubsub.node?(id) 22 | raise StanzaErrors::PolicyViolation.new(self, 'wait') if pubsub.subscribed?(id, jid) 23 | 24 | pubsub.subscribe(id, jid) 25 | send_result_iq(id, jid) 26 | end 27 | 28 | private 29 | 30 | def send_result_iq(id, jid) 31 | result = to_result 32 | result << result.document.create_element('pubsub') do |node| 33 | node.default_namespace = NS 34 | node << result.document.create_element('subscription', 35 | 'node' => id, 36 | 'jid' => jid.to_s, 37 | 'subscription' => 'subscribed') 38 | end 39 | stream.write(result) 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/vines/stanza/pubsub/unsubscribe.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stanza 5 | class PubSub 6 | class Unsubscribe < PubSub 7 | NS = NAMESPACES[:pubsub] 8 | 9 | register "/iq[@id and @type='set']/ns:pubsub/ns:unsubscribe", 'ns' => NS 10 | 11 | def process 12 | return if route_iq || !allowed? 13 | validate_to_address 14 | 15 | node = self.xpath('ns:pubsub/ns:unsubscribe', 'ns' => NS) 16 | raise StanzaErrors::BadRequest.new(self, 'modify') if node.size != 1 17 | node = node.first 18 | id, jid = node['node'], JID.new(node['jid']) 19 | 20 | raise StanzaErrors::Forbidden.new(self, 'auth') unless stream.user.jid.bare == jid.bare 21 | raise StanzaErrors::ItemNotFound.new(self, 'cancel') unless pubsub.node?(id) 22 | raise StanzaErrors::UnexpectedRequest.new(self, 'cancel') unless pubsub.subscribed?(id, jid) 23 | 24 | pubsub.unsubscribe(id, jid) 25 | stream.write(to_result) 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/vines/storage/ldap.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Storage 5 | 6 | # Authenticates usernames and passwords against an LDAP directory. This can 7 | # provide authentication logic for the other, full-featured Storage 8 | # implementations while they store and retrieve the rest of the user 9 | # information. 10 | class Ldap 11 | @@required = [:host, :port] 12 | %w[tls dn password basedn object_class user_attr name_attr groupdn].each do |name| 13 | @@required << name.to_sym unless name == 'groupdn' 14 | define_method name do |*args| 15 | @config[name.to_sym] = args.first 16 | end 17 | end 18 | 19 | def initialize(host='localhost', port=636, &block) 20 | @config = {host: host, port: port} 21 | instance_eval(&block) 22 | @@required.each {|key| raise "Must provide #{key}" if @config[key].nil? } 23 | end 24 | 25 | # Validates a username and password by binding to the LDAP instance with 26 | # those credentials. If the bind succeeds, the user's attributes are 27 | # retrieved. 28 | def authenticate(username, password) 29 | username = JID.new(username).to_s rescue nil 30 | return if [username, password].any? {|arg| (arg || '').strip.empty? } 31 | 32 | ldap = connect(@config[:dn], @config[:password]) 33 | entries = ldap.search( 34 | attributes: [@config[:name_attr], 'mail'], 35 | filter: filter(username)) 36 | return unless entries && entries.size == 1 37 | 38 | user = if connect(entries.first.dn, password).bind 39 | name = entries.first[@config[:name_attr]].first 40 | User.new(jid: username, name: name.to_s, roster: []) 41 | end 42 | user 43 | end 44 | 45 | # Return an LDAP search filter for a user optionally belonging to the 46 | # group defined by the groupdn config attribute. 47 | def filter(username) 48 | clas = Net::LDAP::Filter.eq('objectClass', @config[:object_class]) 49 | uid = Net::LDAP::Filter.eq(@config[:user_attr], username) 50 | filter = clas & uid 51 | if group = @config[:groupdn] 52 | memberOf = Net::LDAP::Filter.eq('memberOf', group) 53 | isMemberOf = Net::LDAP::Filter.eq('isMemberOf', group) 54 | filter = filter & (memberOf | isMemberOf) 55 | end 56 | filter 57 | end 58 | 59 | private 60 | 61 | def connect(dn, password) 62 | options = [:host, :port, :base].zip( 63 | @config.values_at(:host, :port, :basedn)) 64 | Net::LDAP.new(Hash[options]).tap do |ldap| 65 | ldap.encryption(:simple_tls) if @config[:tls] 66 | ldap.auth(dn, password) 67 | end 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/vines/storage/null.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Storage 5 | # A storage implementation that does not persist data to any form of storage. 6 | # When looking up the storage object for a domain, it's easier to treat a 7 | # missing domain with a Null storage than checking for nil. 8 | # 9 | # For example, presence subscription stanzas sent to a pubsub subdomain 10 | # have no storage. Rather than checking for nil storage or pubsub addresses, 11 | # it's easier to treat stanzas to pubsub domains as Null storage that never 12 | # finds or saves users and their rosters. 13 | class Null < Storage 14 | def find_user(jid) 15 | nil 16 | end 17 | 18 | def save_user(user) 19 | # do nothing 20 | end 21 | 22 | def find_vcard(jid) 23 | nil 24 | end 25 | 26 | def save_vcard(jid, card) 27 | # do nothing 28 | end 29 | 30 | def find_fragment(jid, node) 31 | nil 32 | end 33 | 34 | def save_fragment(jid, node) 35 | # do nothing 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/vines/stream/client.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | # Implements the XMPP protocol for client-to-server (c2s) streams. This 6 | # serves connected streams using the jabber:client namespace. 7 | class Client < Stream 8 | MECHANISMS = %w[PLAIN].freeze 9 | 10 | def initialize(config) 11 | super 12 | @session = Client::Session.new(self) 13 | end 14 | 15 | # Delegate behavior to the session that's storing our stream state. 16 | def method_missing(name, *args) 17 | @session.send(name, *args) 18 | end 19 | 20 | %w[advance domain state user user=].each do |name| 21 | define_method name do |*args| 22 | @session.send(name, *args) 23 | end 24 | end 25 | 26 | %w[max_stanza_size max_resources_per_account].each do |name| 27 | define_method name do |*args| 28 | config[:client].send(name, *args) 29 | end 30 | end 31 | 32 | # Return an array of allowed authentication mechanisms advertised as 33 | # client stream features. 34 | def authentication_mechanisms 35 | MECHANISMS 36 | end 37 | 38 | def ssl_handshake_completed 39 | if get_peer_cert 40 | close_connection unless cert_domain_matches?(@session.domain) 41 | end 42 | end 43 | 44 | def unbind 45 | @session.unbind!(self) 46 | super 47 | end 48 | 49 | def start(node) 50 | to, from = %w[to from].map {|a| node[a] } 51 | @session.domain = to unless @session.domain 52 | send_stream_header(from) 53 | raise StreamErrors::NotAuthorized if domain_change?(to) 54 | raise StreamErrors::UnsupportedVersion unless node['version'] == '1.0' 55 | raise StreamErrors::ImproperAddressing unless valid_address?(@session.domain) 56 | raise StreamErrors::HostUnknown unless config.vhost?(@session.domain) 57 | raise StreamErrors::InvalidNamespace unless node.namespaces['xmlns'] == NAMESPACES[:client] 58 | raise StreamErrors::InvalidNamespace unless node.namespaces['xmlns:stream'] == NAMESPACES[:stream] 59 | end 60 | 61 | private 62 | 63 | # The `to` domain address set on the initial stream header must not change 64 | # during stream restarts. This prevents a user from authenticating in one 65 | # domain, then using a stream in a different domain. 66 | # 67 | # to - The String domain JID to verify (e.g. 'wonderland.lit'). 68 | # 69 | # Returns true if the client connection is misbehaving and should be closed. 70 | def domain_change?(to) 71 | to != @session.domain 72 | end 73 | 74 | def send_stream_header(to) 75 | attrs = { 76 | 'xmlns' => NAMESPACES[:client], 77 | 'xmlns:stream' => NAMESPACES[:stream], 78 | 'xml:lang' => 'en', 79 | 'id' => Kit.uuid, 80 | 'from' => @session.domain, 81 | 'version' => '1.0' 82 | } 83 | attrs['to'] = to if to 84 | write "" % attrs.to_a.map{|k,v| "#{k}='#{v}'"}.join(' ') 85 | end 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/vines/stream/client/auth.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Client 6 | class Auth < State 7 | NS = NAMESPACES[:sasl] 8 | MECHANISM = 'mechanism'.freeze 9 | AUTH = 'auth'.freeze 10 | PLAIN = 'PLAIN'.freeze 11 | EXTERNAL = 'EXTERNAL'.freeze 12 | SUCCESS = %Q{}.freeze 13 | MAX_AUTH_ATTEMPTS = 3 14 | 15 | def initialize(stream, success=BindRestart) 16 | super 17 | @attempts = 0 18 | @sasl = SASL.new(stream) 19 | end 20 | 21 | def node(node) 22 | raise StreamErrors::NotAuthorized unless auth?(node) 23 | if node.text.empty? 24 | send_auth_fail(SaslErrors::MalformedRequest.new) 25 | elsif stream.authentication_mechanisms.include?(node[MECHANISM]) 26 | case node[MECHANISM] 27 | when PLAIN then plain_auth(node) 28 | when EXTERNAL then external_auth(node) 29 | end 30 | else 31 | send_auth_fail(SaslErrors::InvalidMechanism.new) 32 | end 33 | end 34 | 35 | private 36 | 37 | def auth?(node) 38 | node.name == AUTH && namespace(node) == NS 39 | end 40 | 41 | def plain_auth(node) 42 | stream.user = @sasl.plain_auth(node.text) 43 | send_auth_success 44 | rescue => e 45 | send_auth_fail(e) 46 | end 47 | 48 | def external_auth(node) 49 | @sasl.external_auth(node.text) 50 | send_auth_success 51 | rescue => e 52 | send_auth_fail(e) 53 | stream.write('') 54 | stream.close_connection_after_writing 55 | end 56 | 57 | def send_auth_success 58 | stream.write(SUCCESS) 59 | stream.reset 60 | advance 61 | end 62 | 63 | def send_auth_fail(condition) 64 | @attempts += 1 65 | if @attempts >= MAX_AUTH_ATTEMPTS 66 | stream.error(StreamErrors::PolicyViolation.new("max authentication attempts exceeded")) 67 | else 68 | stream.error(condition) 69 | end 70 | end 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/vines/stream/client/auth_restart.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Client 6 | class AuthRestart < State 7 | def initialize(stream, success=Auth) 8 | super 9 | end 10 | 11 | def node(node) 12 | raise StreamErrors::NotAuthorized unless stream?(node) 13 | stream.start(node) 14 | doc = Document.new 15 | features = doc.create_element('stream:features') do |el| 16 | el << doc.create_element('mechanisms') do |parent| 17 | parent.default_namespace = NAMESPACES[:sasl] 18 | stream.authentication_mechanisms.each do |name| 19 | parent << doc.create_element('mechanism', name) 20 | end 21 | end 22 | end 23 | stream.write(features) 24 | advance 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/vines/stream/client/bind.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Client 6 | class Bind < State 7 | NS = NAMESPACES[:bind] 8 | MAX_ATTEMPTS = 5 9 | 10 | def initialize(stream, success=Ready) 11 | super 12 | @attempts = 0 13 | end 14 | 15 | def node(node) 16 | @attempts += 1 17 | raise StreamErrors::NotAuthorized unless bind?(node) 18 | raise StreamErrors::PolicyViolation.new('max bind attempts reached') if @attempts > MAX_ATTEMPTS 19 | raise StanzaErrors::ResourceConstraint.new(node, 'wait') if resource_limit_reached? 20 | 21 | stream.bind!(resource(node)) 22 | doc = Document.new 23 | result = doc.create_element('iq', 'id' => node['id'], 'type' => 'result') do |el| 24 | el << doc.create_element('bind') do |bind| 25 | bind.default_namespace = NS 26 | bind << doc.create_element('jid', stream.user.jid.to_s) 27 | end 28 | end 29 | stream.write(result) 30 | send_empty_features 31 | advance 32 | end 33 | 34 | private 35 | 36 | # Write the final element to the stream, indicating 37 | # stream negotiation is complete and the client is cleared to send 38 | # stanzas. 39 | def send_empty_features 40 | stream.write('') 41 | end 42 | 43 | def bind?(node) 44 | node.name == 'iq' && node['type'] == 'set' && node.xpath('ns:bind', 'ns' => NS).any? 45 | end 46 | 47 | def resource(node) 48 | el = node.xpath('ns:bind/ns:resource', 'ns' => NS).first 49 | resource = el ? el.text.strip : '' 50 | generate = resource.empty? || !resource_valid?(resource) || resource_used?(resource) 51 | generate ? Kit.uuid : resource 52 | end 53 | 54 | def resource_limit_reached? 55 | used = stream.connected_resources(stream.user.jid.bare).size 56 | used >= stream.max_resources_per_account 57 | end 58 | 59 | def resource_used?(resource) 60 | stream.available_resources(stream.user.jid).any? do |c| 61 | c.user.jid.resource == resource 62 | end 63 | end 64 | 65 | def resource_valid?(resource) 66 | jid = stream.user.jid 67 | JID.new(jid.node, jid.domain, resource) rescue false 68 | end 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/vines/stream/client/bind_restart.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Client 6 | class BindRestart < State 7 | def initialize(stream, success=Bind) 8 | super 9 | end 10 | 11 | def node(node) 12 | raise StreamErrors::NotAuthorized unless stream?(node) 13 | stream.start(node) 14 | doc = Document.new 15 | features = doc.create_element('stream:features') do |el| 16 | el << doc.create_element('bind', 'xmlns' => NAMESPACES[:bind]) 17 | end 18 | stream.write(features) 19 | advance 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/vines/stream/client/closed.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Client 6 | class Closed < State 7 | def node(node) 8 | # ignore data received after close_connection 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/vines/stream/client/ready.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Client 6 | class Ready < State 7 | def node(node) 8 | stanza = to_stanza(node) 9 | raise StreamErrors::UnsupportedStanzaType unless stanza 10 | stanza.validate_to 11 | stanza.validate_from 12 | stanza.process 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/vines/stream/client/start.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Client 6 | class Start < State 7 | def initialize(stream, success=TLS) 8 | super 9 | end 10 | 11 | def node(node) 12 | raise StreamErrors::NotAuthorized unless stream?(node) 13 | stream.start(node) 14 | doc = Document.new 15 | features = doc.create_element('stream:features') do |el| 16 | el << doc.create_element('starttls') do |tls| 17 | tls.default_namespace = NAMESPACES[:tls] 18 | tls << doc.create_element('required') 19 | end 20 | end 21 | stream.write(features) 22 | advance 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/vines/stream/client/tls.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Client 6 | class TLS < State 7 | NS = NAMESPACES[:tls] 8 | PROCEED = %Q{}.freeze 9 | FAILURE = %Q{}.freeze 10 | STARTTLS = 'starttls'.freeze 11 | 12 | def initialize(stream, success=AuthRestart) 13 | super 14 | end 15 | 16 | def node(node) 17 | raise StreamErrors::NotAuthorized unless starttls?(node) 18 | if stream.encrypt? 19 | stream.write(PROCEED) 20 | stream.encrypt 21 | stream.reset 22 | advance 23 | else 24 | stream.write(FAILURE) 25 | stream.write('') 26 | stream.close_connection_after_writing 27 | end 28 | end 29 | 30 | private 31 | 32 | def starttls?(node) 33 | node.name == STARTTLS && namespace(node) == NS 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/vines/stream/component.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | 6 | # Implements the XMPP protocol for trusted, external component (XEP-0114) 7 | # streams. This serves connected streams using the jabber:component:accept 8 | # namespace. 9 | class Component < Stream 10 | attr_reader :remote_domain 11 | 12 | def initialize(config) 13 | super 14 | @remote_domain = nil 15 | @stream_id = Kit.uuid 16 | advance(Start.new(self)) 17 | end 18 | 19 | def max_stanza_size 20 | config[:component].max_stanza_size 21 | end 22 | 23 | def ready? 24 | state.class == Component::Ready 25 | end 26 | 27 | def stream_type 28 | :component 29 | end 30 | 31 | def start(node) 32 | @remote_domain = node['to'] 33 | send_stream_header 34 | raise StreamErrors::ImproperAddressing unless valid_address?(@remote_domain) 35 | raise StreamErrors::HostUnknown unless config.component?(@remote_domain) 36 | raise StreamErrors::InvalidNamespace unless node.namespaces['xmlns'] == NAMESPACES[:component] 37 | raise StreamErrors::InvalidNamespace unless node.namespaces['xmlns:stream'] == NAMESPACES[:stream] 38 | end 39 | 40 | def secret 41 | password = config.component_password(@remote_domain) 42 | Digest::SHA1.hexdigest(@stream_id + password) 43 | end 44 | 45 | private 46 | 47 | def send_stream_header 48 | attrs = { 49 | 'xmlns' => NAMESPACES[:component], 50 | 'xmlns:stream' => NAMESPACES[:stream], 51 | 'id' => @stream_id, 52 | 'from' => @remote_domain 53 | } 54 | write "" % attrs.to_a.map{|k,v| "#{k}='#{v}'"}.join(' ') 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/vines/stream/component/handshake.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Component 6 | class Handshake < State 7 | def initialize(stream, success=Ready) 8 | super 9 | end 10 | 11 | def node(node) 12 | raise StreamErrors::NotAuthorized unless handshake?(node) 13 | stream.write('') 14 | stream.router << stream 15 | advance 16 | end 17 | 18 | private 19 | 20 | def handshake?(node) 21 | node.name == 'handshake' && node.text == stream.secret 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/vines/stream/component/ready.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Component 6 | class Ready < State 7 | def node(node) 8 | stanza = to_stanza(node) 9 | raise StreamErrors::UnsupportedStanzaType unless stanza 10 | to, from = stanza.validate_to, stanza.validate_from 11 | raise StreamErrors::ImproperAddressing unless to && from 12 | raise StreamErrors::InvalidFrom unless from.domain == stream.remote_domain 13 | stream.user = User.new(jid: from) 14 | if stanza.local? || stanza.to_pubsub_domain? 15 | stanza.process 16 | else 17 | stanza.route 18 | end 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/vines/stream/component/start.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Component 6 | class Start < State 7 | def initialize(stream, success=Handshake) 8 | super 9 | end 10 | 11 | def node(node) 12 | raise StreamErrors::NotAuthorized unless stream?(node) 13 | stream.start(node) 14 | advance 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/vines/stream/http/auth.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Http 6 | class Auth < Client::Auth 7 | def initialize(stream, success=BindRestart) 8 | super 9 | end 10 | 11 | def node(node) 12 | unless stream.valid_session?(node['sid']) && body?(node) && node['rid'] 13 | raise StreamErrors::NotAuthorized 14 | end 15 | nodes = stream.parse_body(node) 16 | raise StreamErrors::NotAuthorized unless nodes.size == 1 17 | super(nodes.first) 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/vines/stream/http/bind.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Http 6 | class Bind < Client::Bind 7 | FEATURES = %Q{}.freeze 8 | 9 | def initialize(stream, success=Ready) 10 | super 11 | end 12 | 13 | def node(node) 14 | unless stream.valid_session?(node['sid']) && body?(node) && node['rid'] 15 | raise StreamErrors::NotAuthorized 16 | end 17 | nodes = stream.parse_body(node) 18 | raise StreamErrors::NotAuthorized unless nodes.size == 1 19 | super(nodes.first) 20 | end 21 | 22 | private 23 | 24 | # Override Client::Bind#send_empty_features to properly namespace the 25 | # empty features element. 26 | def send_empty_features 27 | stream.write(FEATURES) 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/vines/stream/http/bind_restart.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Http 6 | class BindRestart < State 7 | def initialize(stream, success=Bind) 8 | super 9 | end 10 | 11 | def node(node) 12 | raise StreamErrors::NotAuthorized unless restart?(node) 13 | 14 | doc = Document.new 15 | body = doc.create_element('body') do |el| 16 | el.add_namespace(nil, NAMESPACES[:http_bind]) 17 | el.add_namespace('stream', NAMESPACES[:stream]) 18 | el << doc.create_element('stream:features') do |features| 19 | features << doc.create_element('bind', 'xmlns' => NAMESPACES[:bind]) 20 | end 21 | end 22 | stream.reply(body) 23 | advance 24 | end 25 | 26 | private 27 | 28 | def restart?(node) 29 | session = stream.valid_session?(node['sid']) 30 | restart = node.attribute_with_ns('restart', NAMESPACES[:bosh]).value rescue nil 31 | domain = node['to'] == stream.domain 32 | session && body?(node) && domain && restart == 'true' && node['rid'] 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/vines/stream/http/ready.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Http 6 | class Ready < Client::Ready 7 | RID, SID, TYPE, TERMINATE = %w[rid sid type terminate].map {|s| s.freeze } 8 | 9 | def node(node) 10 | unless stream.valid_session?(node[SID]) && body?(node) && node[RID] 11 | raise StreamErrors::NotAuthorized 12 | end 13 | stream.parse_body(node).each do |child| 14 | begin 15 | super(child) 16 | rescue StanzaError => e 17 | stream.error(e) 18 | end 19 | end 20 | stream.terminate if terminate?(node) 21 | end 22 | 23 | def terminate?(node) 24 | node[TYPE] == TERMINATE 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/vines/stream/http/session.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Http 6 | class Session < Client::Session 7 | include Nokogiri::XML 8 | 9 | attr_accessor :content_type, :hold, :inactivity, :wait 10 | 11 | CONTENT_TYPE = 'text/xml; charset=utf-8'.freeze 12 | 13 | def initialize(stream) 14 | super 15 | @state = Http::Start.new(stream) 16 | @inactivity, @wait, @hold = 20, 60, 1 17 | @replied = Time.now 18 | @requests, @responses = [], [] 19 | @content_type = CONTENT_TYPE 20 | end 21 | 22 | def close 23 | Sessions.delete(@id) 24 | router.delete(self) 25 | delete_from_cluster 26 | unsubscribe_pubsub 27 | @requests.each {|req| req.stream.close_connection } 28 | @requests.clear 29 | @responses.clear 30 | @state = Client::Closed.new(nil) 31 | @unbound = true 32 | @available = false 33 | broadcast_unavailable 34 | end 35 | 36 | def ready? 37 | @state.class == Http::Ready 38 | end 39 | 40 | def requests 41 | @requests.clone 42 | end 43 | 44 | def expired? 45 | respond_to_expired_requests 46 | @requests.empty? && (Time.now - @replied > @inactivity) 47 | end 48 | 49 | # Resume this session from its most recent state with a new client 50 | # stream and incoming node. 51 | def resume(stream, node) 52 | stream.session.requests.each do |req| 53 | request(req) 54 | end 55 | stream.session = self 56 | @state.stream = stream 57 | @state.node(node) 58 | end 59 | 60 | def request(request) 61 | if @responses.any? 62 | request.reply(wrap_body(@responses.join), @content_type) 63 | @replied = Time.now 64 | @responses.clear 65 | else 66 | while @requests.size >= @hold 67 | @requests.shift.reply(wrap_body(''), @content_type) 68 | @replied = Time.now 69 | end 70 | @requests << request 71 | end 72 | end 73 | 74 | # Send an HTTP 200 OK response wrapping the XMPP node content back 75 | # to the client. 76 | # 77 | # node - The XML::Node to send to the client. 78 | # 79 | # Returns nothing. 80 | def reply(node) 81 | if request = @requests.shift 82 | request.reply(node, @content_type) 83 | @replied = Time.now 84 | end 85 | end 86 | 87 | # Write the XMPP node to the client stream after wrapping it in a BOSH 88 | # body tag. If there's a waiting request, the node is written 89 | # immediately. If not, it's queued until the next request arrives. 90 | # 91 | # data - The XML String or XML::Node to send in the next HTTP response. 92 | # 93 | # Returns nothing. 94 | def write(node) 95 | if request = @requests.shift 96 | request.reply(wrap_body(node), @content_type) 97 | @replied = Time.now 98 | else 99 | @responses << node.to_s 100 | end 101 | end 102 | 103 | def unbind!(stream) 104 | @requests.reject! {|req| req.stream == stream } 105 | end 106 | 107 | private 108 | 109 | def respond_to_expired_requests 110 | expired = @requests.select {|req| req.age > @wait } 111 | expired.each do |request| 112 | request.reply(wrap_body(''), @content_type) 113 | @requests.delete(request) 114 | @replied = Time.now 115 | end 116 | end 117 | 118 | def wrap_body(data) 119 | doc = Document.new 120 | doc.create_element('body') do |node| 121 | node.add_namespace(nil, NAMESPACES[:http_bind]) 122 | node.inner_html = data.to_s 123 | end 124 | end 125 | end 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /lib/vines/stream/http/sessions.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Http 6 | # Sessions is a cache of Http::Session objects for transient HTTP 7 | # connections. The cache is monitored for expired client connections. 8 | class Sessions 9 | include Vines::Log 10 | 11 | @@instance = nil 12 | def self.instance 13 | @@instance ||= self.new 14 | end 15 | 16 | def self.[](sid) 17 | instance[sid] 18 | end 19 | 20 | def self.[]=(sid, session) 21 | instance[sid] = session 22 | end 23 | 24 | def self.delete(sid) 25 | instance.delete(sid) 26 | end 27 | 28 | def initialize 29 | @sessions = {} 30 | start_timer 31 | end 32 | 33 | def []=(sid, session) 34 | @sessions[sid] = session 35 | end 36 | 37 | def [](sid) 38 | @sessions[sid] 39 | end 40 | 41 | def delete(sid) 42 | @sessions.delete(sid) 43 | end 44 | 45 | private 46 | 47 | # Check for expired clients to cleanup every second. 48 | def start_timer 49 | @timer ||= EventMachine::PeriodicTimer.new(1) { cleanup } 50 | end 51 | 52 | # Remove cached information for all expired connections. An expired 53 | # HTTP client is one that has no queued requests and has had no activity 54 | # for over 20 seconds. 55 | def cleanup 56 | @sessions.each_value do |session| 57 | session.close if session.expired? 58 | end 59 | rescue => e 60 | log.error("Expired session cleanup failed: #{e}") 61 | end 62 | end 63 | end 64 | end 65 | end -------------------------------------------------------------------------------- /lib/vines/stream/http/start.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Http 6 | class Start < State 7 | def initialize(stream, success=Auth) 8 | super 9 | end 10 | 11 | def node(node) 12 | raise StreamErrors::NotAuthorized unless body?(node) 13 | if session = Sessions[node['sid']] 14 | session.resume(stream, node) 15 | else 16 | stream.start(node) 17 | advance 18 | end 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/vines/stream/parser.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Parser < Nokogiri::XML::SAX::Document 6 | include Nokogiri::XML 7 | STREAM_NAME = 'stream'.freeze 8 | STREAM_URI = 'http://etherx.jabber.org/streams'.freeze 9 | IGNORE = NAMESPACES.values_at(:client, :component, :server) 10 | 11 | def initialize(&block) 12 | @listeners, @node = Hash.new {|h, k| h[k] = []}, nil 13 | @parser = Nokogiri::XML::SAX::PushParser.new(self) 14 | instance_eval(&block) if block 15 | end 16 | 17 | [:stream_open, :stream_close, :stanza].each do |name| 18 | define_method(name) do |&block| 19 | @listeners[name] << block 20 | end 21 | end 22 | 23 | def <<(data) 24 | @parser << data 25 | self 26 | end 27 | 28 | def start_element_namespace(name, attrs=[], prefix=nil, uri=nil, ns=[]) 29 | el = node(name, attrs, prefix, uri, ns) 30 | if stream?(name, uri) 31 | notify(:stream_open, el) 32 | else 33 | @node << el if @node 34 | @node = el 35 | end 36 | end 37 | 38 | def end_element_namespace(name, prefix=nil, uri=nil) 39 | if stream?(name, uri) 40 | notify(:stream_close) 41 | elsif @node.parent != @node.document 42 | @node = @node.parent 43 | else 44 | notify(:stanza, @node) 45 | @node = nil 46 | end 47 | end 48 | 49 | def characters(chars) 50 | @node << Text.new(chars, @node.document) if @node 51 | end 52 | alias :cdata_block :characters 53 | 54 | private 55 | 56 | def notify(msg, node=nil) 57 | @listeners[msg].each do |b| 58 | (node ? b.call(node) : b.call) rescue nil 59 | end 60 | end 61 | 62 | def stream?(name, uri) 63 | name == STREAM_NAME && uri == STREAM_URI 64 | end 65 | 66 | def node(name, attrs=[], prefix=nil, uri=nil, ns=[]) 67 | ignore = stream?(name, uri) ? [] : IGNORE 68 | doc = @node ? @node.document : Document.new 69 | node = doc.create_element(name) do |node| 70 | attrs.each {|attr| node[attr.localname] = attr.value } 71 | ns.each {|prefix, uri| node.add_namespace(prefix, uri) unless ignore.include?(uri) } 72 | doc << node unless @node 73 | end 74 | node.namespace = node.add_namespace(prefix, uri) unless ignore.include?(uri) 75 | node 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/vines/stream/server/auth.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Server 6 | class Auth < Client::Auth 7 | def initialize(stream, success=FinalRestart) 8 | super 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/vines/stream/server/auth_restart.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Server 6 | class AuthRestart < Client::AuthRestart 7 | def initialize(stream, success=Auth) 8 | super 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/vines/stream/server/final_restart.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Server 6 | class FinalRestart < State 7 | def initialize(stream, success=Ready) 8 | super 9 | end 10 | 11 | def node(node) 12 | raise StreamErrors::NotAuthorized unless stream?(node) 13 | stream.start(node) 14 | stream.write('') 15 | stream.router << stream 16 | advance 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/vines/stream/server/outbound/auth.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Server 6 | class Outbound 7 | class Auth < State 8 | NS = NAMESPACES[:sasl] 9 | 10 | def initialize(stream, success=AuthResult) 11 | super 12 | end 13 | 14 | def node(node) 15 | raise StreamErrors::NotAuthorized unless external?(node) 16 | authzid = Base64.strict_encode64(stream.domain) 17 | stream.write(%Q{#{authzid}}) 18 | advance 19 | end 20 | 21 | private 22 | 23 | def external?(node) 24 | external = node.xpath("ns:mechanisms/ns:mechanism[text()='EXTERNAL']", 'ns' => NS).any? 25 | node.name == 'features' && namespace(node) == NAMESPACES[:stream] && external 26 | end 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/vines/stream/server/outbound/auth_restart.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Server 6 | class Outbound 7 | class AuthRestart < State 8 | def initialize(stream, success=Auth) 9 | super 10 | end 11 | 12 | def node(node) 13 | raise StreamErrors::NotAuthorized unless stream?(node) 14 | advance 15 | end 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/vines/stream/server/outbound/auth_result.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Server 6 | class Outbound 7 | class AuthResult < State 8 | SUCCESS = 'success'.freeze 9 | FAILURE = 'failure'.freeze 10 | 11 | def initialize(stream, success=FinalRestart) 12 | super 13 | end 14 | 15 | def node(node) 16 | raise StreamErrors::NotAuthorized unless namespace(node) == NAMESPACES[:sasl] 17 | case node.name 18 | when SUCCESS 19 | stream.start(node) 20 | stream.reset 21 | advance 22 | when FAILURE 23 | stream.close_connection 24 | else 25 | raise StreamErrors::NotAuthorized 26 | end 27 | end 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/vines/stream/server/outbound/final_features.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Server 6 | class Outbound 7 | class FinalFeatures < State 8 | def initialize(stream, success=Server::Ready) 9 | super 10 | end 11 | 12 | def node(node) 13 | raise StreamErrors::NotAuthorized unless empty_features?(node) 14 | stream.router << stream 15 | advance 16 | stream.notify_connected 17 | end 18 | 19 | private 20 | 21 | def empty_features?(node) 22 | node.name == 'features' && namespace(node) == NAMESPACES[:stream] && node.elements.empty? 23 | end 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/vines/stream/server/outbound/final_restart.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Server 6 | class Outbound 7 | class FinalRestart < State 8 | def initialize(stream, success=FinalFeatures) 9 | super 10 | end 11 | 12 | def node(node) 13 | raise StreamErrors::NotAuthorized unless stream?(node) 14 | advance 15 | end 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/vines/stream/server/outbound/start.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Server 6 | class Outbound 7 | class Start < State 8 | def initialize(stream, success=TLS) 9 | super 10 | end 11 | 12 | def node(node) 13 | raise StreamErrors::NotAuthorized unless stream?(node) 14 | advance 15 | end 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/vines/stream/server/outbound/tls.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Server 6 | class Outbound 7 | class TLS < State 8 | NS = NAMESPACES[:tls] 9 | 10 | def initialize(stream, success=TLSResult) 11 | super 12 | end 13 | 14 | def node(node) 15 | raise StreamErrors::NotAuthorized unless tls?(node) 16 | stream.write("") 17 | advance 18 | end 19 | 20 | private 21 | 22 | def tls?(node) 23 | tls = node.xpath('ns:starttls', 'ns' => NS).any? 24 | node.name == 'features' && namespace(node) == NAMESPACES[:stream] && tls 25 | end 26 | end 27 | end 28 | end 29 | end 30 | end -------------------------------------------------------------------------------- /lib/vines/stream/server/outbound/tls_result.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Server 6 | class Outbound 7 | class TLSResult < State 8 | NS = NAMESPACES[:tls] 9 | PROCEED = 'proceed'.freeze 10 | FAILURE = 'failure'.freeze 11 | 12 | def initialize(stream, success=AuthRestart) 13 | super 14 | end 15 | 16 | def node(node) 17 | raise StreamErrors::NotAuthorized unless namespace(node) == NS 18 | case node.name 19 | when PROCEED 20 | stream.encrypt 21 | stream.start(node) 22 | stream.reset 23 | advance 24 | when FAILURE 25 | stream.close_connection 26 | else 27 | raise StreamErrors::NotAuthorized 28 | end 29 | end 30 | end 31 | end 32 | end 33 | end 34 | end -------------------------------------------------------------------------------- /lib/vines/stream/server/ready.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Server 6 | class Ready < State 7 | def node(node) 8 | stanza = to_stanza(node) 9 | raise StreamErrors::UnsupportedStanzaType unless stanza 10 | to, from = stanza.validate_to, stanza.validate_from 11 | raise StreamErrors::ImproperAddressing unless to && from 12 | raise StreamErrors::InvalidFrom unless from.domain == stream.remote_domain 13 | raise StreamErrors::HostUnknown unless to.domain == stream.domain 14 | stream.user = User.new(jid: from) 15 | if stanza.local? || stanza.to_pubsub_domain? 16 | stanza.process 17 | else 18 | stanza.route 19 | end 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/vines/stream/server/start.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Server 6 | class Start < Client::Start 7 | def initialize(stream, success=TLS) 8 | super 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/vines/stream/server/tls.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | class Server 6 | class TLS < Client::TLS 7 | def initialize(stream, success=AuthRestart) 8 | super 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/vines/stream/state.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class Stream 5 | 6 | # The base class of Stream state machines. States know how to process XML 7 | # nodes and advance to their next valid state or fail the stream. 8 | class State 9 | include Nokogiri::XML 10 | include Vines::Log 11 | 12 | attr_accessor :stream 13 | 14 | BODY = 'body'.freeze 15 | STREAM = 'stream'.freeze 16 | 17 | def initialize(stream, success=nil) 18 | @stream, @success = stream, success 19 | end 20 | 21 | def node(node) 22 | raise 'subclass must implement' 23 | end 24 | 25 | def ==(state) 26 | self.class == state.class 27 | end 28 | 29 | def eql?(state) 30 | state.is_a?(State) && self == state 31 | end 32 | 33 | def hash 34 | self.class.hash 35 | end 36 | 37 | private 38 | 39 | def advance 40 | stream.advance(@success.new(stream)) 41 | end 42 | 43 | def stream?(node) 44 | node.name == STREAM && namespace(node) == NAMESPACES[:stream] 45 | end 46 | 47 | def body?(node) 48 | node.name == BODY && namespace(node) == NAMESPACES[:http_bind] 49 | end 50 | 51 | def namespace(node) 52 | node.namespace ? node.namespace.href : nil 53 | end 54 | 55 | def to_stanza(node) 56 | Stanza.from_node(node, stream) 57 | end 58 | end 59 | end 60 | end -------------------------------------------------------------------------------- /lib/vines/token_bucket.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | 5 | # The token bucket algorithm is useful for rate limiting. 6 | # Before an operation can be completed, a token is taken from 7 | # the bucket. If no tokens are available, the operation fails. 8 | # The bucket is refilled with tokens at the maximum allowed rate 9 | # of operations. 10 | class TokenBucket 11 | 12 | # Create a full bucket with `capacity` number of tokens to be filled 13 | # at the given rate of tokens/second. 14 | # 15 | # capacity - The Fixnum maximum number of tokens the bucket can hold. 16 | # rate - The Fixnum number of tokens per second at which the bucket is 17 | # refilled. 18 | def initialize(capacity, rate) 19 | raise ArgumentError.new('capacity must be > 0') unless capacity > 0 20 | raise ArgumentError.new('rate must be > 0') unless rate > 0 21 | @capacity = capacity 22 | @tokens = capacity 23 | @rate = rate 24 | @timestamp = Time.new 25 | end 26 | 27 | # Remove tokens from the bucket if it's full enough. There's no way, or 28 | # need, to add tokens to the bucket. It refills over time. 29 | # 30 | # tokens - The Fixnum number of tokens to attempt to take from the bucket. 31 | # 32 | # Returns true if the bucket contains enough tokens to take, false if the 33 | # bucket isn't full enough to satisy the request. 34 | def take(tokens) 35 | raise ArgumentError.new('tokens must be > 0') unless tokens > 0 36 | tokens <= fill ? @tokens -= tokens : false 37 | end 38 | 39 | private 40 | 41 | # Add tokens to the bucket at the `rate` provided in the constructor. This 42 | # fills the bucket slowly over time. 43 | # 44 | # Returns the Fixnum number of tokens left in the bucket. 45 | def fill 46 | if @tokens < @capacity 47 | now = Time.new 48 | @tokens += (@rate * (now - @timestamp)).round 49 | @tokens = @capacity if @tokens > @capacity 50 | @timestamp = now 51 | end 52 | @tokens 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/vines/user.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | class User 5 | include Comparable 6 | 7 | attr_accessor :name, :password, :roster 8 | attr_reader :jid 9 | 10 | def initialize(args={}) 11 | @jid = JID.new(args[:jid]) 12 | raise ArgumentError, 'invalid jid' if @jid.empty? 13 | 14 | @name = args[:name] 15 | @password = args[:password] 16 | @roster = args[:roster] || [] 17 | end 18 | 19 | def <=>(user) 20 | user.is_a?(User) ? self.jid.to_s <=> user.jid.to_s : nil 21 | end 22 | 23 | alias :eql? :== 24 | 25 | def hash 26 | jid.to_s.hash 27 | end 28 | 29 | # Update this user's information from the given user object. 30 | def update_from(user) 31 | @name = user.name 32 | @password = user.password 33 | @roster = user.roster.map {|c| c.clone } 34 | end 35 | 36 | # Return true if the jid is on this user's roster. 37 | def contact?(jid) 38 | !contact(jid).nil? 39 | end 40 | 41 | # Returns the contact with this jid or nil if not found. 42 | def contact(jid) 43 | bare = JID.new(jid).bare 44 | @roster.find {|c| c.jid.bare == bare } 45 | end 46 | 47 | # Returns true if the user is subscribed to this contact's 48 | # presence updates. 49 | def subscribed_to?(jid) 50 | contact = contact(jid) 51 | contact && contact.subscribed_to? 52 | end 53 | 54 | # Returns true if the user has a presence subscription from this contact. 55 | # The contact is subscribed to this user's presence. 56 | def subscribed_from?(jid) 57 | contact = contact(jid) 58 | contact && contact.subscribed_from? 59 | end 60 | 61 | # Removes the contact with this jid from the user's roster. 62 | def remove_contact(jid) 63 | bare = JID.new(jid).bare 64 | @roster.reject! {|c| c.jid.bare == bare } 65 | end 66 | 67 | # Returns a list of the contacts to which this user has 68 | # successfully subscribed. 69 | def subscribed_to_contacts 70 | @roster.select {|c| c.subscribed_to? } 71 | end 72 | 73 | # Returns a list of the contacts that are subscribed to this user's 74 | # presence updates. 75 | def subscribed_from_contacts 76 | @roster.select {|c| c.subscribed_from? } 77 | end 78 | 79 | # Update the contact's jid on this user's roster to signal that this user 80 | # has requested the contact's permission to receive their presence updates. 81 | def request_subscription(jid) 82 | unless contact = contact(jid) 83 | contact = Contact.new(:jid => jid) 84 | @roster << contact 85 | end 86 | contact.ask = 'subscribe' if %w[none from].include?(contact.subscription) 87 | end 88 | 89 | # Add the user's jid to this contact's roster with a subscription state of 90 | # 'from.' This signals that this contact has approved a user's subscription. 91 | def add_subscription_from(jid) 92 | unless contact = contact(jid) 93 | contact = Contact.new(:jid => jid) 94 | @roster << contact 95 | end 96 | contact.subscribe_from 97 | end 98 | 99 | def remove_subscription_to(jid) 100 | if contact = contact(jid) 101 | contact.unsubscribe_to 102 | end 103 | end 104 | 105 | def remove_subscription_from(jid) 106 | if contact = contact(jid) 107 | contact.unsubscribe_from 108 | end 109 | end 110 | 111 | # Returns this user's roster contacts as an iq query element. 112 | def to_roster_xml(id) 113 | doc = Nokogiri::XML::Document.new 114 | doc.create_element('iq', 'id' => id, 'type' => 'result') do |el| 115 | el << doc.create_element('query', 'xmlns' => 'jabber:iq:roster') do |query| 116 | @roster.sort!.each do |contact| 117 | query << contact.to_roster_xml 118 | end 119 | end 120 | end 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /lib/vines/version.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | VERSION = '0.4.10' 5 | end 6 | -------------------------------------------------------------------------------- /lib/vines/xmpp_server.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Vines 4 | 5 | # The main starting point for the XMPP server process. Starts the 6 | # EventMachine processing loop and registers the XMPP protocol handler 7 | # with the ports defined in the server configuration file. 8 | class XmppServer 9 | include Vines::Log 10 | 11 | def initialize(config) 12 | @config = config 13 | end 14 | 15 | def start 16 | log.info('XMPP server started') 17 | at_exit { log.fatal('XMPP server stopped') } 18 | EM.epoll 19 | EM.kqueue 20 | EM.run do 21 | @config.ports.each {|port| port.start } 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | echo "==> Installing gem dependencies . . ." 6 | bundle install --binstubs .bundle/bin --path .bundle/gems 7 | 8 | echo "==> Vines is installed!" 9 | 10 | -------------------------------------------------------------------------------- /script/tests: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | bundle exec rake test 6 | 7 | -------------------------------------------------------------------------------- /test/cluster/publisher_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Cluster::Publisher do 6 | subject { Vines::Cluster::Publisher.new(cluster) } 7 | let(:connection) { MiniTest::Mock.new } 8 | let(:cluster) { MiniTest::Mock.new } 9 | 10 | before do 11 | cluster.expect :id, 'abc' 12 | cluster.expect :connection, connection 13 | end 14 | 15 | describe '#broadcast' do 16 | before do 17 | msg = {from: 'abc', type: 'online', time: Time.now.to_i}.to_json 18 | connection.expect :publish, nil, ["cluster:nodes:all", msg] 19 | end 20 | 21 | it 'publishes the message to every cluster node' do 22 | subject.broadcast(:online) 23 | connection.verify 24 | cluster.verify 25 | end 26 | end 27 | 28 | describe '#route' do 29 | let(:stanza) { "hello" } 30 | 31 | before do 32 | msg = {from: 'abc', type: 'stanza', stanza: stanza}.to_json 33 | connection.expect :publish, nil, ["cluster:nodes:node-42", msg] 34 | end 35 | 36 | it 'publishes the message to just one cluster node' do 37 | subject.route(stanza, "node-42") 38 | connection.verify 39 | cluster.verify 40 | end 41 | end 42 | 43 | describe '#update_user' do 44 | let(:jid) { Vines::JID.new('alice@wonderland.lit') } 45 | 46 | before do 47 | msg = {from: 'abc', type: 'user', jid: jid.to_s}.to_json 48 | connection.expect :publish, nil, ["cluster:nodes:node-42", msg] 49 | end 50 | 51 | it 'publishes the new user to just one cluster node' do 52 | subject.update_user(jid, "node-42") 53 | connection.verify 54 | cluster.verify 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/cluster/sessions_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | require 'storage/storage_tests' 5 | require 'storage/mock_redis' 6 | 7 | describe Vines::Cluster::Sessions do 8 | subject { Vines::Cluster::Sessions.new(cluster) } 9 | let(:connection) { MockRedis.new } 10 | let(:cluster) { OpenStruct.new(id: 'abc', connection: connection) } 11 | let(:jid1) { 'alice@wonderland.lit/tea' } 12 | let(:jid2) { 'alice@wonderland.lit/cake' } 13 | 14 | describe 'when saving to the cluster' do 15 | it 'writes to a redis hash' do 16 | StorageTests::EMLoop.new do 17 | subject.save(jid1, {available: true, interested: true}) 18 | subject.save(jid2, {available: false, interested: false}) 19 | EM.next_tick do 20 | session1 = {node: 'abc', available: true, interested: true} 21 | session2 = {node: 'abc', available: false, interested: false} 22 | connection.db["sessions:alice@wonderland.lit"].size.must_equal 2 23 | connection.db["sessions:alice@wonderland.lit"]['tea'].must_equal session1.to_json 24 | connection.db["sessions:alice@wonderland.lit"]['cake'].must_equal session2.to_json 25 | connection.db["cluster:nodes:abc"].to_a.must_equal [jid1, jid2] 26 | end 27 | end 28 | end 29 | end 30 | 31 | describe 'when deleting from the cluster' do 32 | it 'removes from a redis hash' do 33 | StorageTests::EMLoop.new do 34 | connection.db["sessions:alice@wonderland.lit"] = {} 35 | connection.db["sessions:alice@wonderland.lit"]['tea'] = {node: 'abc', available: true}.to_json 36 | connection.db["sessions:alice@wonderland.lit"]['cake'] = {node: 'abc', available: true}.to_json 37 | connection.db["cluster:nodes:abc"] = Set.new([jid1, jid2]) 38 | 39 | subject.delete(jid1) 40 | EM.next_tick do 41 | connection.db["sessions:alice@wonderland.lit"].size.must_equal 1 42 | connection.db["cluster:nodes:abc"].to_a.must_equal [jid2] 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/cluster/subscriber_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Cluster::Subscriber do 6 | subject { Vines::Cluster::Subscriber.new(cluster) } 7 | let(:connection) { MiniTest::Mock.new } 8 | let(:cluster) { MiniTest::Mock.new } 9 | let(:now) { Time.now.to_i } 10 | 11 | before do 12 | cluster.expect :id, 'abc' 13 | end 14 | 15 | describe '#subscribe' do 16 | before do 17 | cluster.expect :connect, connection 18 | connection.expect :subscribe, nil, ['cluster:nodes:all'] 19 | connection.expect :subscribe, nil, ['cluster:nodes:abc'] 20 | connection.expect :on, nil, [:message] 21 | end 22 | 23 | it 'subscribes to its own channel and the broadcast channel' do 24 | subject.subscribe 25 | connection.verify 26 | cluster.verify 27 | end 28 | end 29 | 30 | describe 'when receiving a heartbeat broadcast message' do 31 | before do 32 | cluster.expect :poke, nil, ['node-42', now] 33 | end 34 | 35 | it 'pokes the session manager for the broadcasting node' do 36 | msg = {from: 'node-42', type: 'heartbeat', time: now}.to_json 37 | subject.send(:on_message, 'cluster:nodes:all', msg) 38 | connection.verify 39 | cluster.verify 40 | end 41 | end 42 | 43 | describe 'when receiving an initial online broadcast message' do 44 | before do 45 | cluster.expect :poke, nil, ['node-42', now] 46 | end 47 | 48 | it 'pokes the session manager for the broadcasting node' do 49 | msg = {from: 'node-42', type: 'online', time: now}.to_json 50 | subject.send(:on_message, 'cluster:nodes:all', msg) 51 | connection.verify 52 | cluster.verify 53 | end 54 | end 55 | 56 | describe 'when receiving an offline broadcast message' do 57 | before do 58 | cluster.expect :delete_sessions, nil, ['node-42'] 59 | end 60 | 61 | it 'deletes the sessions for the broadcasting node' do 62 | msg = {from: 'node-42', type: 'offline', time: now}.to_json 63 | subject.send(:on_message, 'cluster:nodes:all', msg) 64 | connection.verify 65 | cluster.verify 66 | end 67 | end 68 | 69 | describe 'when receiving a stanza routed to my node' do 70 | let(:stream) { MiniTest::Mock.new } 71 | let(:stanza) { "hello" } 72 | let(:xml) { Nokogiri::XML(stanza).root } 73 | 74 | before do 75 | stream.expect :write, nil, [xml] 76 | cluster.expect :connected_resources, [stream], ['alice@wonderland.lit/tea'] 77 | end 78 | 79 | it 'writes the stanza to the connected user streams' do 80 | msg = {from: 'node-42', type: 'stanza', stanza: stanza}.to_json 81 | subject.send(:on_message, 'cluster:nodes:abc', msg) 82 | stream.verify 83 | connection.verify 84 | cluster.verify 85 | end 86 | end 87 | 88 | describe 'when receiving a user update message to my node' do 89 | let(:alice) { Vines::User.new(jid: 'alice@wonderland.lit/tea') } 90 | let(:storage) { MiniTest::Mock.new } 91 | let(:stream) { MiniTest::Mock.new } 92 | 93 | before do 94 | storage.expect :find_user, alice, [alice.jid.bare] 95 | stream.expect :user, alice 96 | cluster.expect :storage, storage, ['wonderland.lit'] 97 | cluster.expect :connected_resources, [stream], [alice.jid.bare] 98 | end 99 | 100 | it 'reloads the user from storage and updates their connected streams' do 101 | msg = {from: 'node-42', type: 'user', jid: alice.jid.to_s}.to_json 102 | subject.send(:on_message, 'cluster:nodes:abc', msg) 103 | storage.verify 104 | stream.verify 105 | connection.verify 106 | cluster.verify 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /test/contact_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Contact do 6 | subject do 7 | Vines::Contact.new( 8 | jid: 'alice@wonderland.lit', 9 | name: "Alice", 10 | groups: %w[Friends Buddies], 11 | subscription: 'from') 12 | end 13 | 14 | describe 'contact equality checks' do 15 | let(:alice) { Vines::Contact.new(jid: 'alice@wonderland.lit') } 16 | let(:hatter) { Vines::Contact.new(jid: 'hatter@wonderland.lit') } 17 | 18 | it 'uses class in equality check' do 19 | (subject <=> 42).must_be_nil 20 | end 21 | 22 | it 'is equal to itself' do 23 | assert subject == subject 24 | assert subject.eql?(subject) 25 | assert subject.hash == subject.hash 26 | end 27 | 28 | it 'is equal to another contact with the same jid' do 29 | assert subject == alice 30 | assert subject.eql?(alice) 31 | assert subject.hash == alice.hash 32 | end 33 | 34 | it 'is not equal to a different jid' do 35 | refute subject == hatter 36 | refute subject.eql?(hatter) 37 | refute subject.hash == hatter.hash 38 | end 39 | end 40 | 41 | describe 'initialize' do 42 | it 'raises when not given a jid' do 43 | -> { Vines::Contact.new }.must_raise ArgumentError 44 | -> { Vines::Contact.new(jid: '') }.must_raise ArgumentError 45 | end 46 | 47 | it 'accepts a domain-only jid' do 48 | contact = Vines::Contact.new(jid: 'tea.wonderland.lit') 49 | contact.jid.to_s.must_equal 'tea.wonderland.lit' 50 | end 51 | end 52 | 53 | describe '#to_roster_xml' do 54 | let(:expected) do 55 | node(%q{ 56 | 57 | Buddies 58 | Friends 59 | 60 | }) 61 | end 62 | 63 | it 'sorts group names' do 64 | subject.to_roster_xml.must_equal expected 65 | end 66 | end 67 | 68 | describe '#send_roster_push' do 69 | let(:recipient) { MiniTest::Mock.new } 70 | let(:expected) do 71 | node(%q{ 72 | 73 | 74 | 75 | Buddies 76 | Friends 77 | 78 | 79 | 80 | }) 81 | end 82 | 83 | before do 84 | recipient.expect :user, Vines::User.new(jid: 'hatter@wonderland.lit') 85 | class << recipient 86 | attr_accessor :nodes 87 | def write(node) 88 | @nodes ||= [] 89 | @nodes << node 90 | end 91 | end 92 | end 93 | 94 | it '' do 95 | subject.send_roster_push(recipient) 96 | recipient.verify 97 | recipient.nodes.size.must_equal 1 98 | recipient.nodes.first.remove_attribute('id') # id is random 99 | recipient.nodes.first.must_equal expected 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /test/error_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::XmppError do 6 | describe Vines::SaslErrors do 7 | it 'does not require a text element' do 8 | expected = %q{} 9 | Vines::SaslErrors::TemporaryAuthFailure.new.to_xml.must_equal expected 10 | end 11 | 12 | it 'includes a text element when message is given' do 13 | text = %q{busted} 14 | expected = %q{%s} % text 15 | Vines::SaslErrors::TemporaryAuthFailure.new('busted').to_xml.must_equal expected 16 | end 17 | end 18 | 19 | describe Vines::StreamErrors do 20 | it 'does not require a text element' do 21 | expected = %q{} 22 | Vines::StreamErrors::InternalServerError.new.to_xml.must_equal expected 23 | end 24 | 25 | it 'includes a text element when message is given' do 26 | text = %q{busted} 27 | expected = %q{%s} % text 28 | Vines::StreamErrors::InternalServerError.new('busted').to_xml.must_equal expected 29 | end 30 | end 31 | 32 | describe Vines::StanzaErrors do 33 | it 'raises when given a bad type' do 34 | node = node('') 35 | -> { Vines::StanzaErrors::BadRequest.new(node, 'bogus') }.must_raise RuntimeError 36 | end 37 | 38 | it 'raises when given a bad stanza' do 39 | node = node('') 40 | -> { Vines::StanzaErrors::BadRequest.new(node, 'modify') }.must_raise RuntimeError 41 | end 42 | 43 | it 'does not require a text element' do 44 | error = %q{} 45 | expected = %q{%s} % error 46 | node = node(%Q{}) 47 | Vines::StanzaErrors::BadRequest.new(node, 'modify').to_xml.must_equal expected 48 | end 49 | 50 | it 'includes a text element when message is given' do 51 | text = %q{busted} 52 | error = %q{%s} % text 53 | expected = %q{%s} % error 54 | node = node(%Q{}) 55 | Vines::StanzaErrors::BadRequest.new(node, 'modify', 'busted').to_xml.must_equal expected 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /test/ext/nokogiri.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Nokogiri 4 | module XML 5 | class Node 6 | # Override equality testing so we can use MiniTest::Mock#expect with 7 | # Nokogiri::XML::Node arguments. Node's default behavior considers 8 | # all nodes unequal. 9 | def ==(node) 10 | self.to_s == node.to_s 11 | end 12 | end 13 | end 14 | end -------------------------------------------------------------------------------- /test/kit_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Kit do 6 | describe '#hmac' do 7 | it 'generates a SHA-512 HMAC' do 8 | Vines::Kit.hmac('secret', 'username').length.must_equal 128 9 | assert_equal Vines::Kit.hmac('s1', 'u1'), Vines::Kit.hmac('s1', 'u1') 10 | refute_equal Vines::Kit.hmac('s1', 'u1'), Vines::Kit.hmac('s2', 'u1') 11 | refute_equal Vines::Kit.hmac('s1', 'u1'), Vines::Kit.hmac('s1', 'u2') 12 | end 13 | end 14 | 15 | describe '#uuid' do 16 | it 'returns a random uuid' do 17 | ids = Array.new(1000) { Vines::Kit.uuid } 18 | assert ids.all? {|id| !id.nil? } 19 | assert ids.all? {|id| id.length == 36 } 20 | assert ids.all? {|id| id.match(/\w{8}-\w{4}-[4]\w{3}-[89ab]\w{3}-\w{12}/) } 21 | ids.uniq.length.must_equal ids.length 22 | end 23 | end 24 | 25 | describe '#auth_token' do 26 | it 'returns a random 128 character token' do 27 | Vines::Kit.auth_token.wont_equal Vines::Kit.auth_token 28 | Vines::Kit.auth_token.length.must_equal 128 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/stanza/iq/disco_info_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Stanza::Iq::DiscoInfo do 6 | subject { Vines::Stanza::Iq::DiscoInfo.new(xml, stream) } 7 | let(:stream) { MiniTest::Mock.new } 8 | let(:alice) { Vines::User.new(jid: 'alice@wonderland.lit/home') } 9 | let(:config) do 10 | Vines::Config.new do 11 | host 'wonderland.lit' do 12 | storage(:fs) { dir Dir.tmpdir } 13 | end 14 | end 15 | end 16 | 17 | let(:xml) do 18 | query = %q{} 19 | node(%Q{#{query}}) 20 | end 21 | 22 | before do 23 | class << stream 24 | attr_accessor :config, :user 25 | end 26 | stream.config = config 27 | stream.user = alice 28 | end 29 | 30 | describe 'when private storage is disabled' do 31 | let(:expected) do 32 | node(%Q{ 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | }) 44 | end 45 | 46 | it 'returns info stanza without the private storage feature' do 47 | config.vhost('wonderland.lit').private_storage false 48 | stream.expect :write, nil, [expected] 49 | subject.process 50 | stream.verify 51 | end 52 | end 53 | 54 | describe 'when private storage is enabled' do 55 | let(:expected) do 56 | node(%Q{ 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | }) 69 | end 70 | 71 | it 'announces private storage feature in info stanza result' do 72 | config.vhost('wonderland.lit').private_storage true 73 | stream.expect :write, nil, [expected] 74 | subject.process 75 | stream.verify 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /test/stanza/iq/disco_items_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Stanza::Iq::DiscoItems do 6 | subject { Vines::Stanza::Iq::DiscoItems.new(xml, stream) } 7 | let(:stream) { MiniTest::Mock.new } 8 | let(:alice) { Vines::User.new(jid: 'alice@wonderland.lit/home') } 9 | let(:config) do 10 | Vines::Config.new do 11 | host 'wonderland.lit' do 12 | storage(:fs) { dir Dir.tmpdir } 13 | components 'tea' => 'secr3t', 'cake' => 'passw0rd' 14 | end 15 | end 16 | end 17 | 18 | before do 19 | class << stream 20 | attr_accessor :config, :user 21 | end 22 | stream.config = config 23 | stream.user = alice 24 | end 25 | 26 | describe 'when querying server items' do 27 | let(:xml) do 28 | query = %q{} 29 | node(%Q{#{query}}) 30 | end 31 | 32 | let(:result) do 33 | node(%q{ 34 | 35 | 36 | 37 | 38 | 39 | 40 | }) 41 | end 42 | 43 | it 'includes component domains in output' do 44 | stream.expect :write, nil, [result] 45 | subject.process 46 | stream.verify 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/stanza/iq/session_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Stanza::Iq::Session do 6 | subject { Vines::Stanza::Iq::Session.new(xml, stream) } 7 | let(:stream) { MiniTest::Mock.new } 8 | let(:alice) { Vines::User.new(jid: 'alice@wonderland.lit/tea') } 9 | 10 | describe 'when session initiation is requested' do 11 | let(:xml) { node(%q{}) } 12 | let(:result) { node(%q{}) } 13 | 14 | before do 15 | stream.expect :domain, 'wonderland.lit' 16 | stream.expect :user, alice 17 | stream.expect :write, nil, [result] 18 | end 19 | 20 | it 'just returns a result to satisy older clients' do 21 | subject.process 22 | stream.verify 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/stanza/iq/vcard_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Stanza::Iq::Vcard do 6 | subject { Vines::Stanza::Iq::Vcard.new(xml, stream) } 7 | let(:alice) { Vines::User.new(jid: 'alice@wonderland.lit/tea') } 8 | let(:stream) { MiniTest::Mock.new } 9 | let(:storage) { MiniTest::Mock.new } 10 | let(:config) do 11 | Vines::Config.new do 12 | host 'wonderland.lit' do 13 | cross_domain_messages true 14 | storage(:fs) { dir Dir.tmpdir } 15 | end 16 | end 17 | end 18 | 19 | before do 20 | class << stream 21 | attr_accessor :config, :domain, :user 22 | end 23 | stream.config = config 24 | stream.domain = 'wonderland.lit' 25 | stream.user = alice 26 | end 27 | 28 | describe 'when getting vcard' do 29 | describe 'and addressed to a remote jid' do 30 | let(:xml) { get('romeo@verona.lit') } 31 | let(:router) { MiniTest::Mock.new } 32 | 33 | before do 34 | router.expect :route, nil, [xml] 35 | stream.expect :router, router 36 | end 37 | 38 | it 'routes rather than handle locally' do 39 | subject.process 40 | stream.verify 41 | router.verify 42 | end 43 | end 44 | 45 | describe 'and missing to address' do 46 | let(:xml) { get('') } 47 | let(:card) { vcard('Alice') } 48 | let(:expected) { result(alice.jid, '', card) } 49 | 50 | before do 51 | storage.expect :find_vcard, card, [alice.jid.bare] 52 | stream.expect :storage, storage, ['wonderland.lit'] 53 | stream.expect :write, nil, [expected] 54 | end 55 | 56 | it 'sends vcard for authenticated jid' do 57 | subject.process 58 | stream.verify 59 | storage.verify 60 | end 61 | end 62 | 63 | describe 'for another user' do 64 | let(:xml) { get(hatter) } 65 | let(:card) { vcard('Hatter') } 66 | let(:hatter) { Vines::JID.new('hatter@wonderland.lit') } 67 | let(:expected) { result(alice.jid, hatter, card) } 68 | 69 | before do 70 | storage.expect :find_vcard, card, [hatter] 71 | stream.expect :storage, storage, ['wonderland.lit'] 72 | stream.expect :write, nil, [expected] 73 | end 74 | 75 | it 'succeeds and returns vcard with from address' do 76 | subject.process 77 | stream.verify 78 | storage.verify 79 | end 80 | end 81 | 82 | describe 'for missing vcard' do 83 | let(:xml) { get('') } 84 | 85 | before do 86 | storage.expect :find_vcard, nil, [alice.jid.bare] 87 | stream.expect :storage, storage, ['wonderland.lit'] 88 | end 89 | 90 | it 'returns an item-not-found stanza error' do 91 | -> { subject.process }.must_raise Vines::StanzaErrors::ItemNotFound 92 | stream.verify 93 | storage.verify 94 | end 95 | end 96 | end 97 | 98 | describe 'when setting vcard' do 99 | describe 'and addressed to another user' do 100 | let(:xml) { set('hatter@wonderland.lit') } 101 | 102 | it 'raises a forbidden stanza error' do 103 | -> { subject.process }.must_raise Vines::StanzaErrors::Forbidden 104 | stream.verify 105 | end 106 | end 107 | 108 | describe 'and missing to address' do 109 | let(:xml) { set('') } 110 | let(:card) { vcard('Alice') } 111 | let(:expected) { result(alice.jid) } 112 | 113 | before do 114 | storage.expect :save_vcard, nil, [alice.jid, card] 115 | stream.expect :storage, storage, ['wonderland.lit'] 116 | stream.expect :write, nil, [expected] 117 | end 118 | 119 | it 'succeeds and returns an iq result' do 120 | subject.process 121 | stream.verify 122 | storage.verify 123 | end 124 | end 125 | end 126 | 127 | private 128 | 129 | def vcard(name) 130 | node(%Q{#{name}}) 131 | end 132 | 133 | def get(to) 134 | card = '' 135 | iq(id: 42, to: to, type: 'get', body: card) 136 | end 137 | 138 | def set(to) 139 | card = 'Alice' 140 | iq(id: 42, to: to, type: 'set', body: card) 141 | end 142 | 143 | def result(to, from=nil, card=nil) 144 | iq(from: from, id: 42, to: to, type: 'result', body: card) 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /test/stanza/iq/version_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Stanza::Iq::Version do 6 | subject { Vines::Stanza::Iq::Version.new(xml, stream) } 7 | let(:alice) { Vines::User.new(jid: 'alice@wonderland.lit/tea') } 8 | let(:stream) { MiniTest::Mock.new } 9 | let(:config) do 10 | Vines::Config.new do 11 | host 'wonderland.lit' do 12 | storage(:fs) { dir Dir.tmpdir } 13 | end 14 | end 15 | end 16 | 17 | before do 18 | class << stream 19 | attr_accessor :config, :user 20 | end 21 | stream.config = config 22 | stream.user = alice 23 | end 24 | 25 | describe 'when not addressed to the server' do 26 | let(:router) { MiniTest::Mock.new } 27 | let(:xml) { node(%q{}) } 28 | 29 | before do 30 | router.expect :route, nil, [xml] 31 | stream.expect :router, router 32 | end 33 | 34 | it 'routes the stanza to the recipient jid' do 35 | subject.process 36 | stream.verify 37 | router.verify 38 | end 39 | end 40 | 41 | describe 'when missing a to address' do 42 | let(:xml) { node(%q{}) } 43 | let(:expected) do 44 | node(%Q{ 45 | 46 | 47 | Vines 48 | #{Vines::VERSION} 49 | 50 | }) 51 | end 52 | 53 | before do 54 | stream.expect :domain, 'wonderland.lit' 55 | stream.expect :domain, 'wonderland.lit' 56 | stream.expect :write, nil, [expected] 57 | end 58 | 59 | it 'returns a version result when missing a to jid' do 60 | subject.process 61 | stream.verify 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/stanza/iq_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Stanza::Iq do 6 | subject { Vines::Stanza::Iq.new(xml, stream) } 7 | let(:stream) { MiniTest::Mock.new } 8 | let(:alice) { Vines::User.new(jid: 'alice@wonderland.lit/tea') } 9 | let(:hatter) { Vines::User.new(jid: 'hatter@wonderland.lit/crumpets') } 10 | let(:config) do 11 | Vines::Config.new do 12 | host 'wonderland.lit' do 13 | storage(:fs) { dir Dir.tmpdir } 14 | end 15 | end 16 | end 17 | 18 | before do 19 | class << stream 20 | attr_accessor :config, :user 21 | end 22 | stream.user = hatter 23 | stream.config = config 24 | end 25 | 26 | describe 'when addressed to a user rather than the server itself' do 27 | let(:recipient) { MiniTest::Mock.new } 28 | let(:xml) do 29 | node(%q{ 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 42 | 43 | 44 | 45 | 46 | 47 | }) 48 | end 49 | 50 | before do 51 | recipient.expect :user, alice, [] 52 | recipient.expect :write, nil, [xml] 53 | stream.expect :connected_resources, [recipient], [alice.jid] 54 | end 55 | 56 | it 'routes the stanza to the users connected resources' do 57 | subject.process 58 | stream.verify 59 | recipient.verify 60 | end 61 | end 62 | 63 | describe 'when given no type or body elements' do 64 | let(:xml) { node('') } 65 | 66 | it 'raises a feature-not-implemented stanza error' do 67 | -> { subject.process }.must_raise Vines::StanzaErrors::FeatureNotImplemented 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /test/stanza/message_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Stanza::Message do 6 | subject { Vines::Stanza::Message.new(xml, stream) } 7 | let(:stream) { MiniTest::Mock.new } 8 | let(:alice) { Vines::User.new(jid: 'alice@wonderland.lit/tea') } 9 | let(:romeo) { Vines::User.new(jid: 'romeo@verona.lit/balcony') } 10 | let(:config) do 11 | Vines::Config.new do 12 | host 'wonderland.lit' do 13 | storage(:fs) { dir Dir.tmpdir } 14 | end 15 | end 16 | end 17 | 18 | before do 19 | class << stream 20 | attr_accessor :config, :user 21 | end 22 | stream.user = alice 23 | stream.config = config 24 | end 25 | 26 | describe 'when message type attribute is invalid' do 27 | let(:xml) { node('hello!') } 28 | 29 | it 'raises a bad-request stanza error' do 30 | -> { subject.process }.must_raise Vines::StanzaErrors::BadRequest 31 | end 32 | end 33 | 34 | describe 'when the to address is missing' do 35 | let(:xml) { node('hello!') } 36 | let(:recipient) { MiniTest::Mock.new } 37 | 38 | before do 39 | recipient.expect :user, alice 40 | recipient.expect :write, nil, [xml] 41 | stream.expect :connected_resources, [recipient], [alice.jid.bare] 42 | end 43 | 44 | it 'sends the message to the senders connected streams' do 45 | subject.process 46 | stream.verify 47 | recipient.verify 48 | end 49 | end 50 | 51 | describe 'when addressed to a non-user' do 52 | let(:bogus) { Vines::JID.new('bogus@wonderland.lit/cake') } 53 | let(:xml) { node(%Q{hello!}) } 54 | let(:storage) { MiniTest::Mock.new } 55 | 56 | before do 57 | storage.expect :find_user, nil, [bogus] 58 | stream.expect :storage, storage, [bogus.domain] 59 | stream.expect :connected_resources, [], [bogus] 60 | end 61 | 62 | it 'ignores the stanza' do 63 | subject.process 64 | stream.verify 65 | storage.verify 66 | end 67 | end 68 | 69 | describe 'when addressed to an offline user' do 70 | let(:hatter) { Vines::User.new(jid: 'hatter@wonderland.lit/cake') } 71 | let(:xml) { node(%Q{hello!}) } 72 | let(:storage) { MiniTest::Mock.new } 73 | 74 | before do 75 | storage.expect :find_user, hatter, [hatter.jid] 76 | stream.expect :storage, storage, [hatter.jid.domain] 77 | stream.expect :connected_resources, [], [hatter.jid] 78 | end 79 | 80 | it 'raises a service-unavailable stanza error' do 81 | -> { subject.process }.must_raise Vines::StanzaErrors::ServiceUnavailable 82 | stream.verify 83 | storage.verify 84 | end 85 | end 86 | 87 | describe 'when address to a local user in a different domain' do 88 | let(:xml) { node(%Q{hello!}) } 89 | let(:expected) { node(%Q{hello!}) } 90 | let(:recipient) { MiniTest::Mock.new } 91 | 92 | before do 93 | recipient.expect :user, romeo 94 | recipient.expect :write, nil, [expected] 95 | 96 | config.host 'verona.lit' do 97 | storage(:fs) { dir Dir.tmpdir } 98 | end 99 | 100 | stream.expect :connected_resources, [recipient], [romeo.jid] 101 | end 102 | 103 | it 'delivers the stanza to the user' do 104 | subject.process 105 | stream.verify 106 | recipient.verify 107 | end 108 | end 109 | 110 | describe 'when addressed to a remote user' do 111 | let(:xml) { node(%Q{hello!}) } 112 | let(:expected) { node(%Q{hello!}) } 113 | let(:router) { MiniTest::Mock.new } 114 | 115 | before do 116 | router.expect :route, nil, [expected] 117 | stream.expect :router, router 118 | end 119 | 120 | it 'routes rather than handle locally' do 121 | subject.process 122 | stream.verify 123 | router.verify 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /test/stanza/presence/probe_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Stanza::Presence::Probe do 6 | def setup 7 | @alice = Vines::JID.new('alice@wonderland.lit/tea') 8 | @stream = MiniTest::Mock.new 9 | @config = Vines::Config.new do 10 | host 'wonderland.lit' do 11 | storage(:fs) { dir Dir.tmpdir } 12 | end 13 | end 14 | end 15 | 16 | def test_missing_to_address_raises 17 | node = node(%q{}) 18 | stanza = Vines::Stanza::Presence::Probe.new(node, @stream) 19 | def stanza.inbound?; false; end 20 | 21 | @stream.expect(:user, Vines::User.new(jid: @alice)) 22 | 23 | assert_raises(Vines::StanzaErrors::BadRequest) { stanza.process } 24 | assert @stream.verify 25 | end 26 | 27 | def test_to_remote_address_routes 28 | node = node(%q{}) 29 | stanza = Vines::Stanza::Presence::Probe.new(node, @stream) 30 | def stanza.inbound?; false; end 31 | 32 | expected = node(%Q{}) 33 | router = MiniTest::Mock.new 34 | router.expect(:route, nil, [expected]) 35 | 36 | @stream.expect(:router, router) 37 | @stream.expect(:user, Vines::User.new(jid: @alice)) 38 | @stream.expect(:config, @config) 39 | 40 | stanza.process 41 | assert @stream.verify 42 | assert router.verify 43 | end 44 | 45 | private 46 | 47 | def node(xml) 48 | Nokogiri::XML(xml).root 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/stanza/presence/subscribe_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Stanza::Presence::Subscribe do 6 | subject { Vines::Stanza::Presence::Subscribe.new(xml, stream) } 7 | let(:stream) { MiniTest::Mock.new } 8 | let(:alice) { Vines::JID.new('alice@wonderland.lit/tea') } 9 | let(:hatter) { Vines::JID.new('hatter@wonderland.lit') } 10 | let(:contact) { Vines::Contact.new(jid: hatter) } 11 | 12 | before do 13 | class << stream 14 | attr_accessor :user, :nodes 15 | def write(node) 16 | @nodes ||= [] 17 | @nodes << node 18 | end 19 | end 20 | end 21 | 22 | describe 'outbound subscription to a local jid, but missing contact' do 23 | let(:xml) { node(%q{}) } 24 | let(:user) { MiniTest::Mock.new } 25 | let(:storage) { MiniTest::Mock.new } 26 | let(:recipient) { MiniTest::Mock.new } 27 | 28 | before do 29 | class << user 30 | attr_accessor :jid 31 | end 32 | user.jid = alice 33 | user.expect :request_subscription, nil, [hatter] 34 | user.expect :contact, contact, [hatter] 35 | 36 | storage.expect :save_user, nil, [user] 37 | storage.expect :find_user, nil, [hatter] 38 | 39 | recipient.expect :user, user 40 | class << recipient 41 | attr_accessor :nodes 42 | def write(node) 43 | @nodes ||= [] 44 | @nodes << node 45 | end 46 | end 47 | 48 | stream.user = user 49 | stream.expect :domain, 'wonderland.lit' 50 | stream.expect :storage, storage, ['wonderland.lit'] 51 | stream.expect :storage, storage, ['wonderland.lit'] 52 | stream.expect :interested_resources, [recipient], [alice] 53 | stream.expect :update_user_streams, nil, [user] 54 | 55 | class << subject 56 | def route_iq; false; end 57 | def inbound?; false; end 58 | def local?; true; end 59 | end 60 | end 61 | 62 | it 'rejects the subscription with an unsubscribed response' do 63 | subject.process 64 | stream.verify 65 | user.verify 66 | storage.verify 67 | stream.nodes.size.must_equal 1 68 | 69 | expected = node(%q{}) 70 | stream.nodes.first.must_equal expected 71 | end 72 | 73 | it 'sends a roster set to the interested resources with subscription none' do 74 | subject.process 75 | recipient.nodes.size.must_equal 1 76 | 77 | query = %q{} 78 | expected = node(%Q{#{query}}) 79 | recipient.nodes.first.remove_attribute('id') # id is random 80 | recipient.nodes.first.must_equal expected 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /test/stanza/pubsub/create_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Stanza::PubSub::Create do 6 | subject { Vines::Stanza::PubSub::Create.new(xml, stream) } 7 | let(:user) { Vines::User.new(jid: 'alice@wonderland.lit/tea') } 8 | let(:stream) { MiniTest::Mock.new } 9 | let(:config) do 10 | Vines::Config.new do 11 | host 'wonderland.lit' do 12 | storage(:fs) { dir Dir.tmpdir } 13 | pubsub 'games' 14 | end 15 | end 16 | end 17 | 18 | before do 19 | class << stream 20 | attr_accessor :config, :nodes, :user 21 | def write(node) 22 | @nodes ||= [] 23 | @nodes << node 24 | end 25 | end 26 | stream.config = config 27 | stream.user = user 28 | end 29 | 30 | describe 'when missing a to address' do 31 | let(:xml) { create('') } 32 | 33 | it 'raises a feature-not-implemented stanza error' do 34 | stream.expect :domain, 'wonderland.lit' 35 | -> { subject.process }.must_raise Vines::StanzaErrors::FeatureNotImplemented 36 | stream.verify 37 | end 38 | end 39 | 40 | describe 'when addressed to bare server domain' do 41 | let(:xml) { create('wonderland.lit') } 42 | 43 | it 'raises a feature-not-implemented stanza error' do 44 | -> { subject.process }.must_raise Vines::StanzaErrors::FeatureNotImplemented 45 | stream.verify 46 | end 47 | end 48 | 49 | describe 'when addressed to a non-pubsub component' do 50 | let(:router) { MiniTest::Mock.new } 51 | let(:xml) { create('bogus.wonderland.lit') } 52 | 53 | before do 54 | router.expect :route, nil, [xml] 55 | stream.expect :router, router 56 | end 57 | 58 | it 'routes rather than handle locally' do 59 | subject.process 60 | stream.verify 61 | router.verify 62 | end 63 | end 64 | 65 | describe 'when attempting to create multiple nodes' do 66 | let(:xml) { create('games.wonderland.lit', true) } 67 | 68 | it 'raises a bad-request stanza error' do 69 | -> { subject.process }.must_raise Vines::StanzaErrors::BadRequest 70 | stream.verify 71 | end 72 | end 73 | 74 | describe 'when attempting to create duplicate nodes' do 75 | let(:pubsub) { MiniTest::Mock.new } 76 | let(:xml) { create('games.wonderland.lit') } 77 | 78 | it 'raises a conflict stanza error' do 79 | pubsub.expect :node?, true, ['game_13'] 80 | subject.stub :pubsub, pubsub do 81 | -> { subject.process }.must_raise Vines::StanzaErrors::Conflict 82 | end 83 | stream.verify 84 | pubsub.verify 85 | end 86 | end 87 | 88 | describe 'when given a valid stanza' do 89 | let(:xml) { create('games.wonderland.lit') } 90 | let(:expected) { result(user.jid, 'games.wonderland.lit') } 91 | 92 | it 'sends an iq result stanza to sender' do 93 | subject.process 94 | stream.nodes.size.must_equal 1 95 | stream.nodes.first.must_equal expected 96 | stream.verify 97 | end 98 | end 99 | 100 | private 101 | 102 | def create(to, multiple=false) 103 | extra_create = "" if multiple 104 | body = %Q{ 105 | 106 | 107 | #{extra_create} 108 | } 109 | iq(type: 'set', to: to, id: 42, body: body) 110 | end 111 | 112 | def result(to, from) 113 | body = '' 114 | iq(from: from, id: 42, to: to, type: 'result', body: body) 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /test/stanza_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Stanza do 6 | subject { Vines::Stanza::Message.new(xml, stream) } 7 | let(:alice) { Vines::JID.new('alice@wonderland.lit/tea') } 8 | let(:romeo) { Vines::JID.new('romeo@verona.lit/balcony') } 9 | let(:stream) { MiniTest::Mock.new } 10 | let(:config) do 11 | Vines::Config.new do 12 | host 'wonderland.lit' do 13 | storage(:fs) { dir Dir.tmpdir } 14 | end 15 | end 16 | end 17 | 18 | describe 'when stanza contains no addresses' do 19 | let(:xml) { node(%Q{hello!}) } 20 | 21 | it 'validates them as nil' do 22 | subject.validate_to.must_be_nil 23 | subject.validate_from.must_be_nil 24 | stream.verify 25 | end 26 | end 27 | 28 | describe 'when stanza contains valid addresses' do 29 | let(:xml) { node(%Q{hello!}) } 30 | 31 | it 'validates and returns JID objects' do 32 | subject.validate_to.must_equal romeo 33 | subject.validate_from.must_equal alice 34 | stream.verify 35 | end 36 | end 37 | 38 | describe 'when stanza contains invalid addresses' do 39 | let(:xml) { node(%Q{hello!}) } 40 | 41 | it 'raises a jid-malformed stanza error' do 42 | -> { subject.validate_to }.must_raise Vines::StanzaErrors::JidMalformed 43 | -> { subject.validate_from }.must_raise Vines::StanzaErrors::JidMalformed 44 | stream.verify 45 | end 46 | end 47 | 48 | describe 'when receiving a non-routable stanza type' do 49 | let(:xml) { node('') } 50 | 51 | it 'handles locally rather than routing' do 52 | subject.local?.must_equal true 53 | stream.verify 54 | end 55 | end 56 | 57 | describe 'when stanza is missing a to address' do 58 | let(:xml) { node(%Q{hello!}) } 59 | 60 | it 'handles locally rather than routing' do 61 | subject.local?.must_equal true 62 | stream.verify 63 | end 64 | end 65 | 66 | describe 'when stanza is addressed to a local jid' do 67 | let(:xml) { node(%Q{hello!}) } 68 | 69 | it 'handles locally rather than routing' do 70 | stream.expect :config, config 71 | subject.local?.must_equal true 72 | stream.verify 73 | end 74 | end 75 | 76 | describe 'when stanza is addressed to a remote jid' do 77 | let(:xml) { node(%Q{hello!}) } 78 | 79 | it 'is not considered a local stanza' do 80 | stream.expect :config, config 81 | subject.local?.must_equal false 82 | stream.verify 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /test/storage/local_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'storage_tests' 4 | require 'test_helper' 5 | 6 | describe Vines::Storage::Local do 7 | include StorageTests 8 | 9 | DIR = Dir.mktmpdir 10 | 11 | def setup 12 | Dir.mkdir(DIR) unless File.exists?(DIR) 13 | %w[user vcard fragment].each do |d| 14 | Dir.mkdir(File.join(DIR, d)) 15 | end 16 | 17 | files = { 18 | :empty => "#{DIR}/user/empty@wonderland.lit", 19 | :no_pass => "#{DIR}/user/no_password@wonderland.lit", 20 | :clear_pass => "#{DIR}/user/clear_password@wonderland.lit", 21 | :bcrypt => "#{DIR}/user/bcrypt_password@wonderland.lit", 22 | :full => "#{DIR}/user/full@wonderland.lit", 23 | :vcard => "#{DIR}/vcard/full@wonderland.lit", 24 | :fragment => "#{DIR}/fragment/full@wonderland.lit-#{StorageTests::FRAGMENT_ID}" 25 | } 26 | File.open(files[:empty], 'w') {|f| f.write('') } 27 | File.open(files[:no_pass], 'w') {|f| f.write('foo: bar') } 28 | File.open(files[:clear_pass], 'w') {|f| f.write('password: secret') } 29 | File.open(files[:bcrypt], 'w') {|f| f.write("password: #{BCrypt::Password.create('secret')}") } 30 | File.open(files[:full], 'w') do |f| 31 | f.puts("password: #{BCrypt::Password.create('secret')}") 32 | f.puts("name: Tester") 33 | f.puts("roster:") 34 | f.puts(" contact1@wonderland.lit:") 35 | f.puts(" name: Contact1") 36 | f.puts(" groups: [Group1, Group2]") 37 | f.puts(" contact2@wonderland.lit:") 38 | f.puts(" name: Contact2") 39 | f.puts(" groups: [Group3, Group4]") 40 | end 41 | File.open(files[:vcard], 'w') {|f| f.write(StorageTests::VCARD.to_xml) } 42 | File.open(files[:fragment], 'w') {|f| f.write(StorageTests::FRAGMENT.to_xml) } 43 | end 44 | 45 | def teardown 46 | FileUtils.remove_entry_secure(DIR) 47 | end 48 | 49 | def storage 50 | Vines::Storage::Local.new { dir DIR } 51 | end 52 | 53 | def test_init 54 | assert_raises(RuntimeError) { Vines::Storage::Local.new {} } 55 | assert_raises(RuntimeError) { Vines::Storage::Local.new { dir 'bogus' } } 56 | assert_raises(RuntimeError) { Vines::Storage::Local.new { dir '/sbin' } } 57 | Vines::Storage::Local.new { dir DIR } # shouldn't raise an error 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/storage/mock_redis.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | # A mock redis storage implementation that saves data to an in-memory Hash. 4 | class MockRedis 5 | attr_reader :db 6 | 7 | # Mimic em-hiredis behavior. 8 | def self.defer(method) 9 | old = instance_method(method) 10 | define_method method do |*args, &block| 11 | result = old.bind(self).call(*args) 12 | deferred = EM::DefaultDeferrable.new 13 | deferred.callback(&block) if block 14 | EM.next_tick { deferred.succeed(result) } 15 | deferred 16 | end 17 | end 18 | 19 | def initialize 20 | @db = {} 21 | end 22 | 23 | def del(key) 24 | @db.delete(key) 25 | end 26 | defer :del 27 | 28 | def get(key) 29 | @db[key] 30 | end 31 | defer :get 32 | 33 | def set(key, value) 34 | @db[key] = value 35 | end 36 | defer :set 37 | 38 | def hget(key, field) 39 | @db[key][field] rescue nil 40 | end 41 | defer :hget 42 | 43 | def hdel(key, field) 44 | @db[key].delete(field) rescue nil 45 | end 46 | defer :hdel 47 | 48 | def hgetall(key) 49 | (@db[key] || {}).map do |k, v| 50 | [k, v] 51 | end.flatten 52 | end 53 | defer :hgetall 54 | 55 | def hset(key, field, value) 56 | @db[key] ||= {} 57 | @db[key][field] = value 58 | end 59 | defer :hset 60 | 61 | def hmset(key, *args) 62 | @db[key] = Hash[*args] 63 | end 64 | defer :hmset 65 | 66 | def sadd(key, obj) 67 | @db[key] ||= Set.new 68 | @db[key] << obj 69 | end 70 | defer :sadd 71 | 72 | def srem(key, obj) 73 | @db[key].delete(obj) rescue nil 74 | end 75 | defer :srem 76 | 77 | def smembers 78 | @db[key].to_a rescue [] 79 | end 80 | defer :smembers 81 | 82 | def flushdb 83 | @db.clear 84 | end 85 | defer :flushdb 86 | 87 | def multi 88 | @transaction = true 89 | end 90 | defer :multi 91 | 92 | def exec 93 | raise 'transaction must start with multi' unless @transaction 94 | @transaction = false 95 | end 96 | defer :exec 97 | end -------------------------------------------------------------------------------- /test/storage/null_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Storage::Null do 6 | before do 7 | @storage = Vines::Storage::Null.new 8 | @user = Vines::User.new(jid: 'alice@wonderland.lit') 9 | end 10 | 11 | def test_find_user_returns_nil 12 | assert_nil @storage.find_user(@user.jid) 13 | @storage.save_user(@user) 14 | assert_nil @storage.find_user(@user.jid) 15 | end 16 | 17 | def test_find_vcard_returns_nil 18 | assert_nil @storage.find_vcard(@user.jid) 19 | @storage.save_vcard(@user.jid, 'card') 20 | assert_nil @storage.find_vcard(@user.jid) 21 | end 22 | 23 | def test_find_fragment_returns_nil 24 | assert_nil @storage.find_fragment(@user.jid, 'node') 25 | @storage.save_fragment(@user.jid, 'node') 26 | assert_nil @storage.find_fragment(@user.jid, 'node') 27 | nil 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/storage_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'storage_tests' 4 | require 'test_helper' 5 | 6 | describe Vines::Storage do 7 | ALICE = 'alice@wonderland.lit'.freeze 8 | 9 | class MockLdapStorage < Vines::Storage 10 | attr_reader :authenticate_calls, :find_user_calls, :save_user_calls 11 | 12 | def initialize(found_user=nil) 13 | @found_user = found_user 14 | @authenticate_calls = @find_user_calls = @save_user_calls = 0 15 | @ldap = Class.new do 16 | attr_accessor :user, :auth 17 | def authenticate(username, password) 18 | @auth ||= [] 19 | @auth << [username, password] 20 | @user 21 | end 22 | end.new 23 | end 24 | 25 | def authenticate(username, password) 26 | @authenticate_calls += 1 27 | nil 28 | end 29 | wrap_ldap :authenticate 30 | 31 | def find_user(jid) 32 | @find_user_calls += 1 33 | @found_user 34 | end 35 | 36 | def save_user(user) 37 | @save_user_calls += 1 38 | end 39 | end 40 | 41 | describe '#authenticate_with_ldap' do 42 | it 'fails when given a bad password' do 43 | StorageTests::EMLoop.new do 44 | storage = MockLdapStorage.new 45 | storage.ldap.user = nil 46 | user = storage.authenticate(ALICE, 'bogus') 47 | assert_nil user 48 | assert_equal 0, storage.authenticate_calls 49 | assert_equal 0, storage.find_user_calls 50 | assert_equal 0, storage.save_user_calls 51 | assert_equal [ALICE, 'bogus'], storage.ldap.auth.first 52 | end 53 | end 54 | 55 | it 'succeeds when user exists in database' do 56 | StorageTests::EMLoop.new do 57 | alice = Vines::User.new(:jid => ALICE) 58 | storage = MockLdapStorage.new(alice) 59 | storage.ldap.user = alice 60 | user = storage.authenticate(ALICE, 'secr3t') 61 | refute_nil user 62 | assert_equal ALICE, user.jid.to_s 63 | assert_equal 0, storage.authenticate_calls 64 | assert_equal 1, storage.find_user_calls 65 | assert_equal 0, storage.save_user_calls 66 | assert_equal [ALICE, 'secr3t'], storage.ldap.auth.first 67 | end 68 | end 69 | 70 | it 'succeeds and saves user to the database' do 71 | StorageTests::EMLoop.new do 72 | alice = Vines::User.new(:jid => ALICE) 73 | storage = MockLdapStorage.new 74 | storage.ldap.user = alice 75 | user = storage.authenticate(ALICE, 'secr3t') 76 | refute_nil user 77 | assert_equal ALICE, user.jid.to_s 78 | assert_equal 0, storage.authenticate_calls 79 | assert_equal 1, storage.find_user_calls 80 | assert_equal 1, storage.save_user_calls 81 | assert_equal [ALICE, 'secr3t'], storage.ldap.auth.first 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /test/stream/client/ready_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Stream::Client::Ready do 6 | STANZAS = [] 7 | 8 | before do 9 | @stream = MiniTest::Mock.new 10 | @state = Vines::Stream::Client::Ready.new(@stream, nil) 11 | def @state.to_stanza(node) 12 | if node.name == 'bogus' 13 | nil 14 | else 15 | stanza = MiniTest::Mock.new 16 | stanza.expect(:process, nil) 17 | stanza.expect(:validate_to, nil) 18 | stanza.expect(:validate_from, nil) 19 | STANZAS << stanza 20 | stanza 21 | end 22 | end 23 | end 24 | 25 | after do 26 | STANZAS.clear 27 | end 28 | 29 | it 'processes a valid node' do 30 | node = node('') 31 | @state.node(node) 32 | assert_equal 1, STANZAS.size 33 | assert STANZAS.map {|s| s.verify }.all? 34 | end 35 | 36 | it 'raises an unsupported-stanza-type stream error for invalid node' do 37 | node = node('') 38 | assert_raises(Vines::StreamErrors::UnsupportedStanzaType) { @state.node(node) } 39 | assert STANZAS.empty? 40 | end 41 | 42 | private 43 | 44 | def node(xml) 45 | Nokogiri::XML(xml).root 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/stream/client/session_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Stream::Client::Session do 6 | subject { Vines::Stream::Client::Session.new(stream) } 7 | let(:another) { Vines::Stream::Client::Session.new(stream) } 8 | let(:stream) { OpenStruct.new(config: nil) } 9 | 10 | describe 'session equality checks' do 11 | it 'uses class in equality check' do 12 | (subject <=> 42).must_be_nil 13 | end 14 | 15 | it 'is equal to itself' do 16 | assert subject == subject 17 | assert subject.eql?(subject) 18 | assert subject.hash == subject.hash 19 | end 20 | 21 | it 'is not equal to another session' do 22 | refute subject == another 23 | refute subject.eql?(another) 24 | refute subject.hash == another.hash 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/stream/component/handshake_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Stream::Component::Handshake do 6 | subject { Vines::Stream::Component::Handshake.new(stream) } 7 | let(:stream) { MiniTest::Mock.new } 8 | 9 | describe 'when invalid element is received' do 10 | it 'raises a not-authorized stream error' do 11 | node = node('') 12 | -> { subject.node(node) }.must_raise Vines::StreamErrors::NotAuthorized 13 | end 14 | end 15 | 16 | describe 'when handshake with no text is received' do 17 | it 'raises a not-authorized stream error' do 18 | stream.expect :secret, 'secr3t' 19 | node = node('') 20 | -> { subject.node(node) }.must_raise Vines::StreamErrors::NotAuthorized 21 | stream.verify 22 | end 23 | end 24 | 25 | describe 'when handshake with invalid secret is received' do 26 | it 'raises a not-authorized stream error' do 27 | stream.expect :secret, 'secr3t' 28 | node = node('bogus') 29 | -> { subject.node(node) }.must_raise Vines::StreamErrors::NotAuthorized 30 | stream.verify 31 | end 32 | end 33 | 34 | describe 'when good handshake is received' do 35 | let(:router) { MiniTest::Mock.new } 36 | 37 | before do 38 | router.expect :<<, nil, [stream] 39 | stream.expect :router, router 40 | stream.expect :secret, 'secr3t' 41 | stream.expect :write, nil, [''] 42 | stream.expect :advance, nil, [Vines::Stream::Component::Ready.new(stream)] 43 | end 44 | 45 | it 'completes the handshake and advances the stream into the ready state' do 46 | node = node('secr3t') 47 | subject.node(node) 48 | stream.verify 49 | router.verify 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/stream/component/ready_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Stream::Component::Ready do 6 | subject { Vines::Stream::Component::Ready.new(stream, nil) } 7 | let(:alice) { Vines::User.new(jid: 'alice@tea.wonderland.lit') } 8 | let(:hatter) { Vines::User.new(jid: 'hatter@wonderland.lit') } 9 | let(:stream) { MiniTest::Mock.new } 10 | let(:config) do 11 | Vines::Config.new do 12 | host 'wonderland.lit' do 13 | storage(:fs) { dir Dir.tmpdir } 14 | end 15 | end 16 | end 17 | 18 | before do 19 | class << stream 20 | attr_accessor :config 21 | end 22 | stream.config = config 23 | end 24 | 25 | describe 'when missing to and from addresses' do 26 | it 'raises an improper-addressing stream error' do 27 | node = node('') 28 | -> { subject.node(node) }.must_raise Vines::StreamErrors::ImproperAddressing 29 | stream.verify 30 | end 31 | end 32 | 33 | describe 'when missing from address' do 34 | it 'raises an improper-addressing stream error' do 35 | node = node(%q{}) 36 | -> { subject.node(node) }.must_raise Vines::StreamErrors::ImproperAddressing 37 | stream.verify 38 | end 39 | end 40 | 41 | describe 'when missing to address' do 42 | it 'raises an improper-addressing stream error' do 43 | node = node(%q{}) 44 | -> { subject.node(node) }.must_raise Vines::StreamErrors::ImproperAddressing 45 | stream.verify 46 | end 47 | end 48 | 49 | describe 'when from address domain does not match component domain' do 50 | it 'raises and invalid-from stream error' do 51 | stream.expect :remote_domain, 'tea.wonderland.lit' 52 | node = node(%q{}) 53 | -> { subject.node(node) }.must_raise Vines::StreamErrors::InvalidFrom 54 | stream.verify 55 | end 56 | end 57 | 58 | describe 'when unrecognized element is received' do 59 | it 'raises an unsupported-stanza-type stream error' do 60 | node = node('') 61 | -> { subject.node(node) }.must_raise Vines::StreamErrors::UnsupportedStanzaType 62 | stream.verify 63 | end 64 | end 65 | 66 | describe 'when addressed to a remote jid' do 67 | let(:router) { MiniTest::Mock.new } 68 | let(:xml) { node(%q{}) } 69 | 70 | before do 71 | router.expect :route, nil, [xml] 72 | stream.expect :remote_domain, 'tea.wonderland.lit' 73 | stream.expect :user=, nil, [alice] 74 | stream.expect :router, router 75 | end 76 | 77 | it 'routes rather than handle locally' do 78 | subject.node(xml) 79 | stream.verify 80 | router.verify 81 | end 82 | end 83 | 84 | describe 'when addressed to a local jid' do 85 | let(:recipient) { MiniTest::Mock.new } 86 | let(:xml) { node(%q{}) } 87 | 88 | before do 89 | recipient.expect :user, hatter 90 | recipient.expect :write, nil, [xml] 91 | stream.expect :remote_domain, 'tea.wonderland.lit' 92 | stream.expect :user=, nil, [alice] 93 | stream.expect :user, alice 94 | stream.expect :connected_resources, [recipient], [hatter.jid] 95 | end 96 | 97 | it 'sends the message to the connected stream' do 98 | subject.node(xml) 99 | stream.verify 100 | recipient.verify 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /test/stream/component/start_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Stream::Component::Start do 6 | before do 7 | @stream = MiniTest::Mock.new 8 | @state = Vines::Stream::Component::Start.new(@stream) 9 | end 10 | 11 | it 'raises not-authorized stream error for invalid element' do 12 | node = node('') 13 | assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) } 14 | end 15 | 16 | it 'raises not-authorized stream error for missing stream namespace' do 17 | node = node('') 18 | assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) } 19 | end 20 | 21 | it 'raises not-authorized stream error for invalid stream namespace' do 22 | node = node('') 23 | assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) } 24 | end 25 | 26 | it 'advances the state machine for valid stream header' do 27 | node = node(%q{}) 28 | @stream.expect(:start, nil, [node]) 29 | @stream.expect(:advance, nil, [Vines::Stream::Component::Handshake.new(@stream)]) 30 | @state.node(node) 31 | assert @stream.verify 32 | end 33 | 34 | private 35 | 36 | def node(xml) 37 | Nokogiri::XML(xml).root 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/stream/http/auth_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Stream::Http::Auth do 6 | before do 7 | @stream = MiniTest::Mock.new 8 | @state = Vines::Stream::Http::Auth.new(@stream, nil) 9 | end 10 | 11 | def test_missing_body_raises_error 12 | node = node('') 13 | @stream.expect(:valid_session?, true, [nil]) 14 | assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) } 15 | end 16 | 17 | def test_body_with_missing_namespace_raises_error 18 | node = node('') 19 | @stream.expect(:valid_session?, true, ['12']) 20 | assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) } 21 | end 22 | 23 | def test_missing_rid_raises_error 24 | node = node('') 25 | @stream.expect(:valid_session?, true, ['12']) 26 | assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) } 27 | end 28 | 29 | def test_invalid_session_raises_error 30 | @stream.expect(:valid_session?, false, ['12']) 31 | node = node('') 32 | assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) } 33 | assert @stream.verify 34 | end 35 | 36 | def test_empty_body_raises_error 37 | node = node('') 38 | @stream.expect(:valid_session?, true, ['12']) 39 | @stream.expect(:parse_body, [], [node]) 40 | assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) } 41 | assert @stream.verify 42 | end 43 | 44 | def test_body_with_two_children_raises_error 45 | node = node('') 46 | message = node('') 47 | @stream.expect(:valid_session?, true, ['12']) 48 | @stream.expect(:parse_body, [message, message], [node]) 49 | assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) } 50 | assert @stream.verify 51 | end 52 | 53 | def test_valid_body_processes 54 | auth = node(%Q{}) 55 | node = node('') 56 | node << auth 57 | @stream.expect(:valid_session?, true, ['12']) 58 | @stream.expect(:parse_body, [auth], [node]) 59 | # this error means we correctly called the parent method Client#node 60 | @stream.expect(:error, nil, [Vines::SaslErrors::MalformedRequest.new]) 61 | @state.node(node) 62 | assert @stream.verify 63 | end 64 | 65 | private 66 | 67 | def node(xml) 68 | Nokogiri::XML(xml).root 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /test/stream/http/ready_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Stream::Http::Ready do 6 | subject { Vines::Stream::Http::Ready.new(stream, nil) } 7 | let(:stream) { MiniTest::Mock.new } 8 | let(:alice) { Vines::User.new(jid: 'alice@wonderland.lit') } 9 | let(:hatter) { Vines::User.new(jid: 'hatter@wonderland.lit') } 10 | let(:config) do 11 | Vines::Config.new do 12 | host 'wonderland.lit' do 13 | storage(:fs) { dir Dir.tmpdir } 14 | end 15 | end 16 | end 17 | 18 | it "raises when body element is missing" do 19 | node = node('') 20 | stream.expect :valid_session?, true, [nil] 21 | -> { subject.node(node) }.must_raise Vines::StreamErrors::NotAuthorized 22 | end 23 | 24 | it "raises when namespace is missing" do 25 | node = node('') 26 | stream.expect :valid_session?, true, ['12'] 27 | -> { subject.node(node) }.must_raise Vines::StreamErrors::NotAuthorized 28 | end 29 | 30 | it "raises when rid attribute is missing" do 31 | node = node('') 32 | stream.expect :valid_session?, true, ['12'] 33 | -> { subject.node(node) }.must_raise Vines::StreamErrors::NotAuthorized 34 | end 35 | 36 | it "raises when session id is invalid" do 37 | stream.expect :valid_session?, false, ['12'] 38 | node = node('') 39 | -> { subject.node(node) }.must_raise Vines::StreamErrors::NotAuthorized 40 | stream.verify 41 | end 42 | 43 | it "processes when body element is empty" do 44 | node = node('') 45 | stream.expect :valid_session?, true, ['12'] 46 | stream.expect :parse_body, [], [node] 47 | subject.node(node) 48 | stream.verify 49 | end 50 | 51 | describe 'when receiving multiple stanzas in one body element' do 52 | let(:recipient) { MiniTest::Mock.new } 53 | let(:bogus) { node('raises stanza error') } 54 | let(:ok) { node('but processes this message') } 55 | let(:xml) { node(%Q{#{bogus}#{ok}}) } 56 | let(:raises) { Vines::Stanza.from_node(bogus, stream) } 57 | let(:processes) { Vines::Stanza.from_node(ok, stream) } 58 | 59 | before do 60 | recipient.expect :user, hatter 61 | recipient.expect :write, nil, [Vines::Stanza::Message] 62 | 63 | stream.expect :valid_session?, true, ['12'] 64 | stream.expect :parse_body, [raises, processes], [xml] 65 | stream.expect :error, nil, [Vines::StanzaErrors::BadRequest] 66 | stream.expect :config, config 67 | stream.expect :user, alice 68 | stream.expect :connected_resources, [recipient], [hatter.jid] 69 | end 70 | 71 | it 'processes all stanzas' do 72 | subject.node(xml) 73 | stream.verify 74 | recipient.verify 75 | end 76 | end 77 | 78 | it "terminates the session" do 79 | node = node('') 80 | stream.expect :valid_session?, true, ['12'] 81 | stream.expect :parse_body, [], [node] 82 | stream.expect :terminate, nil 83 | subject.node(node) 84 | stream.verify 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /test/stream/http/sessions_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Stream::Http::Sessions do 6 | class MockSessions < Vines::Stream::Http::Sessions 7 | def start_timer 8 | # do nothing 9 | end 10 | end 11 | 12 | def setup 13 | @sessions = MockSessions.new 14 | end 15 | 16 | def test_session_add_and_delete 17 | session = "session" 18 | assert_nil @sessions['42'] 19 | @sessions['42'] = session 20 | assert_equal session, @sessions['42'] 21 | @sessions.delete('42') 22 | assert_nil @sessions['42'] 23 | end 24 | 25 | def test_access_singleton_through_class_methods 26 | session = "session" 27 | assert_nil MockSessions['42'] 28 | MockSessions['42'] = session 29 | assert_equal session, MockSessions['42'] 30 | MockSessions.delete('42') 31 | assert_nil MockSessions['42'] 32 | end 33 | 34 | def test_cleanup 35 | live = MiniTest::Mock.new 36 | live.expect(:expired?, false) 37 | 38 | dead = MiniTest::Mock.new 39 | dead.expect(:expired?, true) 40 | dead.expect(:close, nil) 41 | 42 | @sessions['live'] = live 43 | @sessions['dead'] = dead 44 | 45 | @sessions.send(:cleanup) 46 | assert live.verify 47 | assert dead.verify 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/stream/http/start_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Stream::Http::Start do 6 | before do 7 | @stream = MiniTest::Mock.new 8 | @state = Vines::Stream::Http::Start.new(@stream) 9 | end 10 | 11 | def test_missing_body_raises_error 12 | node = node('') 13 | assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) } 14 | end 15 | 16 | def test_body_with_missing_namespace_raises_error 17 | node = node('') 18 | assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) } 19 | end 20 | 21 | def test_missing_session_starts_stream 22 | EM.run do 23 | node = node('') 24 | @stream.expect(:start, nil, [node]) 25 | @stream.expect(:advance, nil, [Vines::Stream::Http::Auth.new(@stream)]) 26 | @state.node(node) 27 | assert @stream.verify 28 | EM.stop 29 | end 30 | end 31 | 32 | def test_valid_session_resumes_stream 33 | EM.run do 34 | node = node('') 35 | session = MiniTest::Mock.new 36 | session.expect(:resume, nil, [@stream, node]) 37 | Vines::Stream::Http::Sessions['123'] = session 38 | @state.node(node) 39 | assert @stream.verify 40 | assert session.verify 41 | EM.stop 42 | end 43 | end 44 | 45 | private 46 | 47 | def node(xml) 48 | Nokogiri::XML(xml).root 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/stream/server/auth_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Stream::Server::Auth do 6 | # disable logging for tests 7 | Class.new.extend(Vines::Log).log.level = Logger::FATAL 8 | 9 | subject { Vines::Stream::Server::Auth.new(stream) } 10 | let(:stream) { MiniTest::Mock.new } 11 | 12 | before do 13 | class << stream 14 | attr_accessor :remote_domain 15 | end 16 | stream.remote_domain = 'wonderland.lit' 17 | end 18 | 19 | describe 'when given a valid authzid' do 20 | before do 21 | stream.expect :cert_domain_matches?, true, ['wonderland.lit'] 22 | stream.expect :write, nil, [''] 23 | stream.expect :advance, nil, [Vines::Stream::Server::FinalRestart] 24 | stream.expect :reset, nil 25 | stream.expect :authentication_mechanisms, ['EXTERNAL'] 26 | end 27 | 28 | it 'passes external auth with empty authzid' do 29 | node = external('=') 30 | subject.node(node) 31 | stream.verify 32 | end 33 | 34 | it 'passes external auth with authzid matching from domain' do 35 | node = external(Base64.strict_encode64('wonderland.lit')) 36 | subject.node(node) 37 | stream.verify 38 | end 39 | end 40 | 41 | describe 'when given an invalid authzid' do 42 | before do 43 | stream.expect :write, nil, [''] 44 | stream.expect :close_connection_after_writing, nil 45 | stream.expect :error, nil, [Vines::SaslErrors::InvalidAuthzid] 46 | stream.expect :authentication_mechanisms, ['EXTERNAL'] 47 | end 48 | 49 | it 'fails external auth with mismatched from domain' do 50 | node = external(Base64.strict_encode64('verona.lit')) 51 | subject.node(node) 52 | stream.verify 53 | end 54 | end 55 | 56 | private 57 | 58 | def external(authzid) 59 | node(%Q{#{authzid}}) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/stream/server/outbound/auth_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Stream::Server::Outbound::Auth do 6 | before do 7 | @stream = MiniTest::Mock.new 8 | @state = Vines::Stream::Server::Outbound::Auth.new(@stream) 9 | end 10 | 11 | def test_invalid_element 12 | node = node('') 13 | assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) } 14 | end 15 | 16 | def test_invalid_sasl_element 17 | node = node(%Q{}) 18 | assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) } 19 | end 20 | 21 | def test_missing_namespace 22 | node = node('') 23 | assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) } 24 | end 25 | 26 | def test_invalid_namespace 27 | node = node('') 28 | assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) } 29 | end 30 | 31 | def test_missing_mechanisms 32 | node = node(%Q{}) 33 | assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) } 34 | end 35 | 36 | def test_missing_mechanisms_namespace 37 | node = node(%Q{}) 38 | assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) } 39 | end 40 | 41 | def test_missing_mechanism 42 | mechanisms = %q{} 43 | node = node(%Q{#{mechanisms}}) 44 | assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) } 45 | end 46 | 47 | def test_missing_mechanism_text 48 | mechanisms = %q{} 49 | node = node(%Q{#{mechanisms}}) 50 | assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) } 51 | end 52 | 53 | def test_invalid_mechanism_text 54 | mechanisms = %q{BOGUS} 55 | node = node(%Q{#{mechanisms}}) 56 | assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) } 57 | end 58 | 59 | def test_valid_mechanism 60 | @stream.expect(:domain, 'wonderland.lit') 61 | expected = %Q{d29uZGVybGFuZC5saXQ=} 62 | @stream.expect(:write, nil, [expected]) 63 | @stream.expect(:advance, nil, [Vines::Stream::Server::Outbound::AuthResult.new(@stream)]) 64 | mechanisms = %q{EXTERNAL} 65 | node = node(%Q{#{mechanisms}}) 66 | @state.node(node) 67 | assert @stream.verify 68 | end 69 | 70 | private 71 | 72 | def node(xml) 73 | Nokogiri::XML(xml).root 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /test/stream/server/ready_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::Stream::Server::Ready do 6 | subject { Vines::Stream::Server::Ready.new(stream, nil) } 7 | let(:stream) { MiniTest::Mock.new } 8 | 9 | SERVER_STANZAS = [] 10 | 11 | before do 12 | def subject.to_stanza(node) 13 | Vines::Stanza.from_node(node, stream).tap do |stanza| 14 | def stanza.process 15 | SERVER_STANZAS << self 16 | end if stanza 17 | end 18 | end 19 | end 20 | 21 | after do 22 | SERVER_STANZAS.clear 23 | end 24 | 25 | it 'processes a valid node' do 26 | config = MiniTest::Mock.new 27 | config.expect(:local_jid?, true, [Vines::JID.new('romeo@verona.lit')]) 28 | 29 | stream.expect(:config, config) 30 | stream.expect(:remote_domain, 'wonderland.lit') 31 | stream.expect(:domain, 'verona.lit') 32 | stream.expect(:user=, nil, [Vines::User.new(jid: 'alice@wonderland.lit')]) 33 | 34 | node = node(%Q{}) 35 | subject.node(node) 36 | assert_equal 1, SERVER_STANZAS.size 37 | assert stream.verify 38 | assert config.verify 39 | end 40 | 41 | it 'raises unsupported-stanza-type stream error' do 42 | node = node('') 43 | -> { subject.node(node) }.must_raise Vines::StreamErrors::UnsupportedStanzaType 44 | assert SERVER_STANZAS.empty? 45 | assert stream.verify 46 | end 47 | 48 | it 'raises improper-addressing stream error when to address is missing' do 49 | node = node(%Q{}) 50 | -> { subject.node(node) }.must_raise Vines::StreamErrors::ImproperAddressing 51 | assert SERVER_STANZAS.empty? 52 | assert stream.verify 53 | end 54 | 55 | it 'raises jid-malformed stanza error when to address is invalid' do 56 | node = node(%Q{}) 57 | -> { subject.node(node) }.must_raise Vines::StanzaErrors::JidMalformed 58 | assert SERVER_STANZAS.empty? 59 | assert stream.verify 60 | end 61 | 62 | it 'raises improper-addressing stream error' do 63 | node = node(%Q{}) 64 | -> { subject.node(node) }.must_raise Vines::StreamErrors::ImproperAddressing 65 | assert SERVER_STANZAS.empty? 66 | assert stream.verify 67 | end 68 | 69 | it 'raises jid-malformed stanza error for invalid from address' do 70 | node = node(%Q{}) 71 | -> { subject.node(node) }.must_raise Vines::StanzaErrors::JidMalformed 72 | assert SERVER_STANZAS.empty? 73 | assert stream.verify 74 | end 75 | 76 | it 'raises invalid-from stream error' do 77 | stream.expect(:remote_domain, 'wonderland.lit') 78 | node = node(%Q{}) 79 | -> { subject.node(node) }.must_raise Vines::StreamErrors::InvalidFrom 80 | assert SERVER_STANZAS.empty? 81 | assert stream.verify 82 | end 83 | 84 | it 'raises host-unknown stream error' do 85 | stream.expect(:remote_domain, 'wonderland.lit') 86 | stream.expect(:domain, 'verona.lit') 87 | node = node(%Q{}) 88 | -> { subject.node(node) }.must_raise Vines::StreamErrors::HostUnknown 89 | assert SERVER_STANZAS.empty? 90 | assert stream.verify 91 | end 92 | 93 | private 94 | 95 | def node(xml) 96 | Nokogiri::XML(xml).root 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'tmpdir' 4 | require 'vines' 5 | require 'ext/nokogiri' 6 | require 'minitest/autorun' 7 | 8 | class MiniTest::Spec 9 | 10 | # Build an xml node with the given attributes. This is useful as a 11 | # quick way to build a node to use as expected stanza output from a 12 | # Stream#write call. 13 | # 14 | # options - The Hash of xml attributes to include on the iq element. Attribute 15 | # values of nil or empty? are excluded from the generated element. 16 | # :body - The String xml content to include in the iq element. 17 | # 18 | # Examples 19 | # 20 | # iq(from: from, id: 42, to: to, type: 'result', body: card) 21 | # 22 | # Returns a Nokogiri::XML::Node. 23 | def iq(options) 24 | body = options.delete(:body) 25 | options.delete_if {|k, v| v.nil? || v.to_s.empty? } 26 | attrs = options.map {|k, v| "#{k}=\"#{v}\"" }.join(' ') 27 | node("#{body}") 28 | end 29 | 30 | # Parse xml into a nokogiri node. Strip excessive whitespace from the xml 31 | # content before parsing because it affects comparisons in MiniTest::Mock 32 | # expectations. 33 | # 34 | # xml - The String of xml content to parse. 35 | # 36 | # Returns a Nokogiri::XML::Node. 37 | def node(xml) 38 | xml = xml.strip.gsub(/\n|\s{2,}/, '') 39 | Nokogiri::XML(xml).root 40 | end 41 | end 42 | 43 | -------------------------------------------------------------------------------- /test/token_bucket_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::TokenBucket do 6 | subject { Vines::TokenBucket.new(10, 1) } 7 | 8 | it 'raises with invalid capacity and rate values' do 9 | -> { Vines::TokenBucket.new(0, 1) }.must_raise ArgumentError 10 | -> { Vines::TokenBucket.new(1, 0) }.must_raise ArgumentError 11 | -> { Vines::TokenBucket.new(-1, 1) }.must_raise ArgumentError 12 | -> { Vines::TokenBucket.new(1, -1) }.must_raise ArgumentError 13 | end 14 | 15 | it 'does not allow taking a negative number of tokens' do 16 | -> { subject.take(-1) }.must_raise ArgumentError 17 | end 18 | 19 | it 'does not allow taking more tokens than its capacity' do 20 | refute subject.take(11) 21 | end 22 | 23 | it 'allows taking all tokens, but no more' do 24 | assert subject.take(10) 25 | refute subject.take(1) 26 | end 27 | 28 | it 'refills over time' do 29 | assert subject.take(10) 30 | refute subject.take(1) 31 | Time.stub(:new, Time.now + 1) do 32 | assert subject.take(1) 33 | refute subject.take(1) 34 | end 35 | end 36 | 37 | it 'does not refill over capacity' do 38 | assert subject.take(10) 39 | refute subject.take(1) 40 | Time.stub(:new, Time.now + 15) do 41 | refute subject.take(11) 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/user_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test_helper' 4 | 5 | describe Vines::User do 6 | subject { Vines::User.new(jid: 'alice@wonderland.lit', name: 'Alice', password: 'secr3t') } 7 | 8 | describe 'user equality checks' do 9 | let(:alice) { Vines::User.new(jid: 'alice@wonderland.lit') } 10 | let(:hatter) { Vines::User.new(jid: 'hatter@wonderland.lit') } 11 | 12 | it 'uses class in equality check' do 13 | (subject <=> 42).must_be_nil 14 | end 15 | 16 | it 'is equal to itself' do 17 | assert subject == subject 18 | assert subject.eql?(subject) 19 | assert subject.hash == subject.hash 20 | end 21 | 22 | it 'is equal to another user with the same jid' do 23 | assert subject == alice 24 | assert subject.eql?(alice) 25 | assert subject.hash == alice.hash 26 | end 27 | 28 | it 'is not equal to a different jid' do 29 | refute subject == hatter 30 | refute subject.eql?(hatter) 31 | refute subject.hash == hatter.hash 32 | end 33 | end 34 | 35 | describe 'initialize' do 36 | it 'raises when not given a jid' do 37 | -> { Vines::User.new }.must_raise ArgumentError 38 | -> { Vines::User.new(jid: '') }.must_raise ArgumentError 39 | end 40 | 41 | it 'has an empty roster' do 42 | subject.roster.wont_be_nil 43 | subject.roster.size.must_equal 0 44 | end 45 | end 46 | 47 | describe '#update_from' do 48 | let(:updated) { Vines::User.new(jid: 'alice2@wonderland.lit', name: 'Alice 2', password: "secr3t 2") } 49 | 50 | before do 51 | subject.roster << Vines::Contact.new(jid: 'hatter@wonderland.lit', name: "Hatter") 52 | updated.roster << Vines::Contact.new(jid: 'cat@wonderland.lit', name: "Cheshire") 53 | end 54 | 55 | it 'updates jid, name, and password' do 56 | subject.update_from(updated) 57 | subject.jid.to_s.must_equal 'alice@wonderland.lit' 58 | subject.name.must_equal 'Alice 2' 59 | subject.password.must_equal 'secr3t 2' 60 | end 61 | 62 | it 'overwrites the entire roster' do 63 | subject.update_from(updated) 64 | subject.roster.size.must_equal 1 65 | subject.roster.first.must_equal updated.roster.first 66 | end 67 | 68 | it 'clones roster entries' do 69 | subject.update_from(updated) 70 | updated.roster.first.name = 'Updated Contact 2' 71 | subject.roster.first.name.must_equal 'Cheshire' 72 | end 73 | end 74 | 75 | describe '#to_roster_xml' do 76 | let(:expected) do 77 | node(%q{ 78 | 79 | 80 | AB 81 | C 82 | 83 | 84 | }) 85 | end 86 | 87 | before do 88 | subject.roster << Vines::Contact.new(jid: 'b@wonderland.lit', name: "Contact 2", groups: %w[C]) 89 | subject.roster << Vines::Contact.new(jid: 'a@wonderland.lit', name: "Contact 1", groups: %w[B A]) 90 | end 91 | 92 | it 'sorts group names' do 93 | subject.to_roster_xml(42).must_equal expected 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /vines.gemspec: -------------------------------------------------------------------------------- 1 | require './lib/vines/version' 2 | 3 | Gem::Specification.new do |s| 4 | s.name = 'vines' 5 | s.version = Vines::VERSION 6 | s.summary = %q[An XMPP chat server that's easy to install and run.] 7 | s.description = %q[A scalable XMPP chat server.] 8 | 9 | s.authors = ['David Graham'] 10 | s.email = %w[david@negativecode.com] 11 | s.homepage = 'http://www.getvines.org' 12 | s.license = 'MIT' 13 | 14 | s.files = Dir['[A-Z]*', 'vines.gemspec', '{bin,lib,conf,web}/**/*'] - ['Gemfile.lock'] 15 | s.test_files = Dir['test/**/*'] 16 | s.executables = %w[vines] 17 | s.require_path = 'lib' 18 | 19 | s.add_dependency 'bcrypt', '~> 3.1' 20 | s.add_dependency 'em-hiredis', '~> 0.1.1' 21 | s.add_dependency 'eventmachine', '~> 1.0' 22 | s.add_dependency 'http_parser.rb', '~> 0.6' 23 | s.add_dependency 'net-ldap', '~> 0.6' 24 | s.add_dependency 'nokogiri', '~> 1.6' 25 | 26 | s.add_development_dependency 'minitest', '~> 5.3' 27 | s.add_development_dependency 'rake', '~> 10.3' 28 | 29 | s.required_ruby_version = '>= 1.9.3' 30 | end 31 | -------------------------------------------------------------------------------- /web/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vines 7 | 8 | 24 | 25 | 26 |

This is not the page you're looking for.

27 | 28 | 29 | -------------------------------------------------------------------------------- /web/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/negativecode/vines/245d6c971dd8604d74265d67fd4e2a78319d71a2/web/apple-touch-icon.png -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/negativecode/vines/245d6c971dd8604d74265d67fd4e2a78319d71a2/web/favicon.png --------------------------------------------------------------------------------