lygneo-vines 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +3 -0
- data/LICENSE +19 -0
- data/README.md +7 -0
- data/Rakefile +23 -0
- data/bin/vines +4 -0
- data/conf/certs/README +39 -0
- data/conf/certs/ca-bundle.crt +3895 -0
- data/conf/config.rb +42 -0
- data/lib/vines/cli.rb +132 -0
- data/lib/vines/cluster/connection.rb +26 -0
- data/lib/vines/cluster/publisher.rb +55 -0
- data/lib/vines/cluster/pubsub.rb +92 -0
- data/lib/vines/cluster/sessions.rb +125 -0
- data/lib/vines/cluster/subscriber.rb +108 -0
- data/lib/vines/cluster.rb +246 -0
- data/lib/vines/command/bcrypt.rb +12 -0
- data/lib/vines/command/cert.rb +50 -0
- data/lib/vines/command/init.rb +68 -0
- data/lib/vines/command/ldap.rb +38 -0
- data/lib/vines/command/restart.rb +12 -0
- data/lib/vines/command/schema.rb +24 -0
- data/lib/vines/command/start.rb +28 -0
- data/lib/vines/command/stop.rb +18 -0
- data/lib/vines/config/host.rb +125 -0
- data/lib/vines/config/port.rb +132 -0
- data/lib/vines/config/pubsub.rb +108 -0
- data/lib/vines/config.rb +223 -0
- data/lib/vines/daemon.rb +78 -0
- data/lib/vines/error.rb +150 -0
- data/lib/vines/follower.rb +111 -0
- data/lib/vines/jid.rb +95 -0
- data/lib/vines/kit.rb +23 -0
- data/lib/vines/log.rb +24 -0
- data/lib/vines/router.rb +179 -0
- data/lib/vines/stanza/iq/auth.rb +18 -0
- data/lib/vines/stanza/iq/disco_info.rb +45 -0
- data/lib/vines/stanza/iq/disco_items.rb +29 -0
- data/lib/vines/stanza/iq/error.rb +16 -0
- data/lib/vines/stanza/iq/ping.rb +16 -0
- data/lib/vines/stanza/iq/private_storage.rb +83 -0
- data/lib/vines/stanza/iq/query.rb +10 -0
- data/lib/vines/stanza/iq/result.rb +16 -0
- data/lib/vines/stanza/iq/roster.rb +140 -0
- data/lib/vines/stanza/iq/session.rb +17 -0
- data/lib/vines/stanza/iq/vcard.rb +56 -0
- data/lib/vines/stanza/iq/version.rb +25 -0
- data/lib/vines/stanza/iq.rb +48 -0
- data/lib/vines/stanza/message.rb +40 -0
- data/lib/vines/stanza/presence/error.rb +23 -0
- data/lib/vines/stanza/presence/probe.rb +37 -0
- data/lib/vines/stanza/presence/subscribe.rb +42 -0
- data/lib/vines/stanza/presence/subscribed.rb +51 -0
- data/lib/vines/stanza/presence/unavailable.rb +15 -0
- data/lib/vines/stanza/presence/unsubscribe.rb +38 -0
- data/lib/vines/stanza/presence/unsubscribed.rb +38 -0
- data/lib/vines/stanza/presence.rb +141 -0
- data/lib/vines/stanza/pubsub/create.rb +39 -0
- data/lib/vines/stanza/pubsub/delete.rb +41 -0
- data/lib/vines/stanza/pubsub/publish.rb +66 -0
- data/lib/vines/stanza/pubsub/subscribe.rb +44 -0
- data/lib/vines/stanza/pubsub/unsubscribe.rb +30 -0
- data/lib/vines/stanza/pubsub.rb +22 -0
- data/lib/vines/stanza.rb +175 -0
- data/lib/vines/storage/ldap.rb +71 -0
- data/lib/vines/storage/local.rb +139 -0
- data/lib/vines/storage/null.rb +39 -0
- data/lib/vines/storage/sql.rb +138 -0
- data/lib/vines/storage.rb +239 -0
- data/lib/vines/store.rb +110 -0
- data/lib/vines/stream/client/auth.rb +74 -0
- data/lib/vines/stream/client/auth_restart.rb +29 -0
- data/lib/vines/stream/client/bind.rb +72 -0
- data/lib/vines/stream/client/bind_restart.rb +24 -0
- data/lib/vines/stream/client/closed.rb +13 -0
- data/lib/vines/stream/client/ready.rb +17 -0
- data/lib/vines/stream/client/session.rb +210 -0
- data/lib/vines/stream/client/start.rb +27 -0
- data/lib/vines/stream/client/tls.rb +38 -0
- data/lib/vines/stream/client.rb +84 -0
- data/lib/vines/stream/component/handshake.rb +26 -0
- data/lib/vines/stream/component/ready.rb +23 -0
- data/lib/vines/stream/component/start.rb +19 -0
- data/lib/vines/stream/component.rb +58 -0
- data/lib/vines/stream/http/auth.rb +22 -0
- data/lib/vines/stream/http/bind.rb +32 -0
- data/lib/vines/stream/http/bind_restart.rb +37 -0
- data/lib/vines/stream/http/ready.rb +29 -0
- data/lib/vines/stream/http/request.rb +172 -0
- data/lib/vines/stream/http/session.rb +120 -0
- data/lib/vines/stream/http/sessions.rb +65 -0
- data/lib/vines/stream/http/start.rb +23 -0
- data/lib/vines/stream/http.rb +157 -0
- data/lib/vines/stream/parser.rb +79 -0
- data/lib/vines/stream/sasl.rb +128 -0
- data/lib/vines/stream/server/auth.rb +13 -0
- data/lib/vines/stream/server/auth_restart.rb +13 -0
- data/lib/vines/stream/server/final_restart.rb +21 -0
- data/lib/vines/stream/server/outbound/auth.rb +31 -0
- data/lib/vines/stream/server/outbound/auth_restart.rb +20 -0
- data/lib/vines/stream/server/outbound/auth_result.rb +32 -0
- data/lib/vines/stream/server/outbound/final_features.rb +28 -0
- data/lib/vines/stream/server/outbound/final_restart.rb +20 -0
- data/lib/vines/stream/server/outbound/start.rb +20 -0
- data/lib/vines/stream/server/outbound/tls.rb +30 -0
- data/lib/vines/stream/server/outbound/tls_result.rb +34 -0
- data/lib/vines/stream/server/ready.rb +24 -0
- data/lib/vines/stream/server/start.rb +13 -0
- data/lib/vines/stream/server/tls.rb +13 -0
- data/lib/vines/stream/server.rb +150 -0
- data/lib/vines/stream/state.rb +60 -0
- data/lib/vines/stream.rb +247 -0
- data/lib/vines/token_bucket.rb +55 -0
- data/lib/vines/user.rb +123 -0
- data/lib/vines/version.rb +6 -0
- data/lib/vines/xmpp_server.rb +25 -0
- data/lib/vines.rb +203 -0
- data/test/cluster/publisher_test.rb +57 -0
- data/test/cluster/sessions_test.rb +47 -0
- data/test/cluster/subscriber_test.rb +109 -0
- data/test/config/host_test.rb +369 -0
- data/test/config/pubsub_test.rb +187 -0
- data/test/config_test.rb +732 -0
- data/test/error_test.rb +58 -0
- data/test/ext/nokogiri.rb +14 -0
- data/test/follower_test.rb +102 -0
- data/test/jid_test.rb +147 -0
- data/test/kit_test.rb +31 -0
- data/test/router_test.rb +243 -0
- data/test/stanza/iq/disco_info_test.rb +78 -0
- data/test/stanza/iq/disco_items_test.rb +49 -0
- data/test/stanza/iq/private_storage_test.rb +184 -0
- data/test/stanza/iq/roster_test.rb +229 -0
- data/test/stanza/iq/session_test.rb +25 -0
- data/test/stanza/iq/vcard_test.rb +146 -0
- data/test/stanza/iq/version_test.rb +64 -0
- data/test/stanza/iq_test.rb +70 -0
- data/test/stanza/message_test.rb +126 -0
- data/test/stanza/presence/probe_test.rb +50 -0
- data/test/stanza/presence/subscribe_test.rb +83 -0
- data/test/stanza/pubsub/create_test.rb +116 -0
- data/test/stanza/pubsub/delete_test.rb +169 -0
- data/test/stanza/pubsub/publish_test.rb +309 -0
- data/test/stanza/pubsub/subscribe_test.rb +205 -0
- data/test/stanza/pubsub/unsubscribe_test.rb +148 -0
- data/test/stanza_test.rb +85 -0
- data/test/storage/ldap_test.rb +201 -0
- data/test/storage/local_test.rb +59 -0
- data/test/storage/mock_redis.rb +97 -0
- data/test/storage/null_test.rb +29 -0
- data/test/storage/storage_tests.rb +182 -0
- data/test/storage_test.rb +85 -0
- data/test/store_test.rb +130 -0
- data/test/stream/client/auth_test.rb +137 -0
- data/test/stream/client/ready_test.rb +47 -0
- data/test/stream/client/session_test.rb +27 -0
- data/test/stream/component/handshake_test.rb +52 -0
- data/test/stream/component/ready_test.rb +103 -0
- data/test/stream/component/start_test.rb +39 -0
- data/test/stream/http/auth_test.rb +70 -0
- data/test/stream/http/ready_test.rb +86 -0
- data/test/stream/http/request_test.rb +209 -0
- data/test/stream/http/sessions_test.rb +49 -0
- data/test/stream/http/start_test.rb +50 -0
- data/test/stream/parser_test.rb +122 -0
- data/test/stream/sasl_test.rb +195 -0
- data/test/stream/server/auth_test.rb +61 -0
- data/test/stream/server/outbound/auth_test.rb +75 -0
- data/test/stream/server/ready_test.rb +98 -0
- data/test/test_helper.rb +42 -0
- data/test/token_bucket_test.rb +44 -0
- data/test/user_test.rb +96 -0
- data/vines.gemspec +30 -0
- metadata +387 -0
@@ -0,0 +1,150 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
# Implements the XMPP protocol for server-to-server (s2s) streams. This
|
6
|
+
# serves connected streams using the jabber:server namespace. This handles
|
7
|
+
# both accepting incoming s2s streams and initiating outbound s2s streams
|
8
|
+
# to other servers.
|
9
|
+
class Server < Stream
|
10
|
+
MECHANISMS = %w[EXTERNAL].freeze
|
11
|
+
|
12
|
+
# Starts the connection to the remote server. When the stream is
|
13
|
+
# connected and ready to send stanzas it will yield to the callback
|
14
|
+
# block. The callback is run on the EventMachine reactor thread. The
|
15
|
+
# yielded stream will be nil if the remote connection failed. We need to
|
16
|
+
# use a background thread to avoid blocking the server on DNS SRV
|
17
|
+
# lookups.
|
18
|
+
def self.start(config, to, from, &callback)
|
19
|
+
op = proc do
|
20
|
+
Resolv::DNS.open do |dns|
|
21
|
+
dns.getresources("_xmpp-server._tcp.#{to}", Resolv::DNS::Resource::IN::SRV)
|
22
|
+
end.sort! {|a,b| a.priority == b.priority ? b.weight <=> a.weight : a.priority <=> b.priority }
|
23
|
+
end
|
24
|
+
cb = proc do |srv|
|
25
|
+
if srv.empty?
|
26
|
+
srv << {target: to, port: 5269}
|
27
|
+
class << srv.first
|
28
|
+
def method_missing(name); self[name]; end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
Server.connect(config, to, from, srv, callback)
|
32
|
+
end
|
33
|
+
EM.defer(proc { op.call rescue [] }, cb)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.connect(config, to, from, srv, callback)
|
37
|
+
if srv.empty?
|
38
|
+
# fiber so storage calls work properly
|
39
|
+
Fiber.new { callback.call(nil) }.resume
|
40
|
+
else
|
41
|
+
begin
|
42
|
+
rr = srv.shift
|
43
|
+
opts = {to: to, from: from, srv: srv, callback: callback}
|
44
|
+
EM.connect(rr.target.to_s, rr.port, Server, config, opts)
|
45
|
+
rescue => e
|
46
|
+
connect(config, to, from, srv, callback)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_reader :domain
|
52
|
+
attr_accessor :remote_domain
|
53
|
+
|
54
|
+
def initialize(config, options={})
|
55
|
+
super(config)
|
56
|
+
@connected = false
|
57
|
+
@remote_domain = options[:to]
|
58
|
+
@domain = options[:from]
|
59
|
+
@srv = options[:srv]
|
60
|
+
@callback = options[:callback]
|
61
|
+
@outbound = @remote_domain && @domain
|
62
|
+
start = @outbound ? Outbound::Start.new(self) : Start.new(self)
|
63
|
+
advance(start)
|
64
|
+
end
|
65
|
+
|
66
|
+
def post_init
|
67
|
+
super
|
68
|
+
send_stream_header if @outbound
|
69
|
+
end
|
70
|
+
|
71
|
+
def max_stanza_size
|
72
|
+
config[:server].max_stanza_size
|
73
|
+
end
|
74
|
+
|
75
|
+
def ssl_handshake_completed
|
76
|
+
close_connection unless cert_domain_matches?(@remote_domain)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Return an array of allowed authentication mechanisms advertised as
|
80
|
+
# server stream features.
|
81
|
+
def authentication_mechanisms
|
82
|
+
MECHANISMS
|
83
|
+
end
|
84
|
+
|
85
|
+
def stream_type
|
86
|
+
:server
|
87
|
+
end
|
88
|
+
|
89
|
+
def unbind
|
90
|
+
super
|
91
|
+
if @outbound && !@connected
|
92
|
+
Server.connect(config, @remote_domain, @domain, @srv, @callback)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def vhost?(domain)
|
97
|
+
config.vhost?(domain)
|
98
|
+
end
|
99
|
+
|
100
|
+
def notify_connected
|
101
|
+
@connected = true
|
102
|
+
if @callback
|
103
|
+
@callback.call(self)
|
104
|
+
@callback = nil
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def ready?
|
109
|
+
state.class == Server::Ready
|
110
|
+
end
|
111
|
+
|
112
|
+
def start(node)
|
113
|
+
if @outbound then send_stream_header; return end
|
114
|
+
to, from = %w[to from].map {|a| node[a] }
|
115
|
+
@domain, @remote_domain = to, from unless @domain
|
116
|
+
send_stream_header
|
117
|
+
raise StreamErrors::NotAuthorized if domain_change?(to, from)
|
118
|
+
raise StreamErrors::UnsupportedVersion unless node['version'] == '1.0'
|
119
|
+
raise StreamErrors::ImproperAddressing unless valid_address?(@domain) && valid_address?(@remote_domain)
|
120
|
+
raise StreamErrors::HostUnknown unless config.vhost?(@domain) || config.pubsub?(@domain) || config.component?(@domain)
|
121
|
+
raise StreamErrors::NotAuthorized unless config.s2s?(@remote_domain) && config.allowed?(@domain, @remote_domain)
|
122
|
+
raise StreamErrors::InvalidNamespace unless node.namespaces['xmlns'] == NAMESPACES[:server]
|
123
|
+
raise StreamErrors::InvalidNamespace unless node.namespaces['xmlns:stream'] == NAMESPACES[:stream]
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
# The +to+ and +from+ domain addresses set on the initial stream header
|
129
|
+
# must not change during stream restarts. This prevents a server from
|
130
|
+
# authenticating as one domain, then sending stanzas from users in a
|
131
|
+
# different domain.
|
132
|
+
def domain_change?(to, from)
|
133
|
+
to != @domain || from != @remote_domain
|
134
|
+
end
|
135
|
+
|
136
|
+
def send_stream_header
|
137
|
+
attrs = {
|
138
|
+
'xmlns' => NAMESPACES[:server],
|
139
|
+
'xmlns:stream' => NAMESPACES[:stream],
|
140
|
+
'xml:lang' => 'en',
|
141
|
+
'id' => Kit.uuid,
|
142
|
+
'from' => @domain,
|
143
|
+
'to' => @remote_domain,
|
144
|
+
'version' => '1.0'
|
145
|
+
}
|
146
|
+
write "<stream:stream %s>" % attrs.to_a.map{|k,v| "#{k}='#{v}'"}.join(' ')
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,60 @@
|
|
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
|
data/lib/vines/stream.rb
ADDED
@@ -0,0 +1,247 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
# The base class for various XMPP streams (c2s, s2s, component, http),
|
5
|
+
# containing behavior common to all streams like rate limiting, stanza
|
6
|
+
# parsing, and stream error handling.
|
7
|
+
class Stream < EventMachine::Connection
|
8
|
+
include Vines::Log
|
9
|
+
|
10
|
+
ERROR = 'error'.freeze
|
11
|
+
PAD = 20
|
12
|
+
|
13
|
+
attr_reader :config, :domain
|
14
|
+
attr_accessor :user
|
15
|
+
|
16
|
+
def initialize(config)
|
17
|
+
@config = config
|
18
|
+
end
|
19
|
+
|
20
|
+
def post_init
|
21
|
+
@remote_addr, @local_addr = addresses
|
22
|
+
@user, @closed, @stanza_size = nil, false, 0
|
23
|
+
@bucket = TokenBucket.new(100, 10)
|
24
|
+
@store = Store.new(@config.certs)
|
25
|
+
@nodes = EM::Queue.new
|
26
|
+
process_node_queue
|
27
|
+
create_parser
|
28
|
+
log.info { "%s %21s -> %s" %
|
29
|
+
['Stream connected:'.ljust(PAD), @remote_addr, @local_addr] }
|
30
|
+
end
|
31
|
+
|
32
|
+
# Initialize a new XML parser for this connection. This is called when the
|
33
|
+
# stream is first connected as well as for stream restarts during
|
34
|
+
# negotiation. Subclasses can override this method to provide a different
|
35
|
+
# type of parser (e.g. HTTP).
|
36
|
+
def create_parser
|
37
|
+
@parser = Parser.new.tap do |p|
|
38
|
+
p.stream_open {|node| @nodes.push(node) }
|
39
|
+
p.stream_close { close_connection }
|
40
|
+
p.stanza {|node| @nodes.push(node) }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Advance the state machine into the +Closed+ state so any remaining queued
|
45
|
+
# nodes are not processed while we're waiting for EM to actually close the
|
46
|
+
# connection.
|
47
|
+
def close_connection(after_writing=false)
|
48
|
+
super
|
49
|
+
@closed = true
|
50
|
+
advance(Client::Closed.new(self))
|
51
|
+
end
|
52
|
+
|
53
|
+
def receive_data(data)
|
54
|
+
return if @closed
|
55
|
+
@stanza_size += data.bytesize
|
56
|
+
if @stanza_size < max_stanza_size
|
57
|
+
@parser << data rescue error(StreamErrors::NotWellFormed.new)
|
58
|
+
else
|
59
|
+
error(StreamErrors::PolicyViolation.new('max stanza size reached'))
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Reset the connection's XML parser when a new <stream:stream> header
|
64
|
+
# is received.
|
65
|
+
def reset
|
66
|
+
create_parser
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns the storage system for the domain. If no domain is given,
|
70
|
+
# the stream's storage mechanism is returned.
|
71
|
+
def storage(domain=nil)
|
72
|
+
@config.storage(domain || self.domain)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns the Vines::Config::Host virtual host for the stream's domain.
|
76
|
+
def vhost
|
77
|
+
@config.vhost(domain)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Reload the user's information into their active connections. Call this
|
81
|
+
# after storage.save_user() to sync the new user state with their other
|
82
|
+
# connections.
|
83
|
+
def update_user_streams(user)
|
84
|
+
connected_resources(user.jid.bare).each do |stream|
|
85
|
+
stream.user.update_from(user)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def connected_resources(jid)
|
90
|
+
router.connected_resources(jid, user.jid)
|
91
|
+
end
|
92
|
+
|
93
|
+
def available_resources(*jid)
|
94
|
+
router.available_resources(*jid, user.jid)
|
95
|
+
end
|
96
|
+
|
97
|
+
def interested_resources(*jid)
|
98
|
+
router.interested_resources(*jid, user.jid)
|
99
|
+
end
|
100
|
+
|
101
|
+
def ssl_verify_peer(pem)
|
102
|
+
# EM is supposed to close the connection when this returns false,
|
103
|
+
# but it only does that for inbound connections, not when we
|
104
|
+
# make a connection to another server.
|
105
|
+
@store.trusted?(pem).tap do |trusted|
|
106
|
+
close_connection unless trusted
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def cert_domain_matches?(domain)
|
111
|
+
@store.domain?(get_peer_cert, domain)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Send the data over the wire to this client.
|
115
|
+
def write(data)
|
116
|
+
log_node(data, :out)
|
117
|
+
if data.respond_to?(:to_xml)
|
118
|
+
data = data.to_xml(:indent => 0)
|
119
|
+
end
|
120
|
+
send_data(data)
|
121
|
+
end
|
122
|
+
|
123
|
+
def encrypt
|
124
|
+
cert, key = @store.files_for_domain(domain)
|
125
|
+
start_tls(cert_chain_file: cert, private_key_file: key, verify_peer: true)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Returns true if the TLS certificate and private key files for this domain
|
129
|
+
# exist and can be used to encrypt this stream.
|
130
|
+
def encrypt?
|
131
|
+
!@store.files_for_domain(domain).nil?
|
132
|
+
end
|
133
|
+
|
134
|
+
def unbind
|
135
|
+
router.delete(self)
|
136
|
+
log.info { "%s %21s -> %s" %
|
137
|
+
['Stream disconnected:'.ljust(PAD), @remote_addr, @local_addr] }
|
138
|
+
log.info { "Streams connected: #{router.size}" }
|
139
|
+
end
|
140
|
+
|
141
|
+
# Advance the stream's state machine to the new state. XML nodes received
|
142
|
+
# by the stream will be passed to this state's +node+ method.
|
143
|
+
def advance(state)
|
144
|
+
@state = state
|
145
|
+
end
|
146
|
+
|
147
|
+
# Stream level errors close the stream while stanza and SASL errors are
|
148
|
+
# written to the client and leave the stream open. All exceptions should
|
149
|
+
# pass through this method for consistent handling.
|
150
|
+
def error(e)
|
151
|
+
case e
|
152
|
+
when SaslError, StanzaError
|
153
|
+
write(e.to_xml)
|
154
|
+
when StreamError
|
155
|
+
send_stream_error(e)
|
156
|
+
close_stream
|
157
|
+
else
|
158
|
+
log.error(e)
|
159
|
+
send_stream_error(StreamErrors::InternalServerError.new)
|
160
|
+
close_stream
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def router
|
165
|
+
@config.router
|
166
|
+
end
|
167
|
+
|
168
|
+
private
|
169
|
+
|
170
|
+
# Return the remote and local socket addresses used by this connection.
|
171
|
+
def addresses
|
172
|
+
[get_peername, get_sockname].map do |addr|
|
173
|
+
addr ? Socket.unpack_sockaddr_in(addr)[0, 2].reverse.join(':') : 'unknown'
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Write the StreamError's xml to the stream. Subclasses can override
|
178
|
+
# this method with custom error writing behavior.
|
179
|
+
def send_stream_error(e)
|
180
|
+
write(e.to_xml)
|
181
|
+
end
|
182
|
+
|
183
|
+
# Write a closing stream tag to the stream then close the stream. Subclasses
|
184
|
+
# can override this method for custom close behavior.
|
185
|
+
def close_stream
|
186
|
+
write('</stream:stream>')
|
187
|
+
close_connection_after_writing
|
188
|
+
end
|
189
|
+
|
190
|
+
def error?(node)
|
191
|
+
ns = node.namespace ? node.namespace.href : nil
|
192
|
+
node.name == ERROR && ns == NAMESPACES[:stream]
|
193
|
+
end
|
194
|
+
|
195
|
+
# Schedule a queue pop on the EM thread to handle the next element. This
|
196
|
+
# guarantees all stanzas received on this stream are processed in order.
|
197
|
+
# http://tools.ietf.org/html/rfc6120#section-10.1
|
198
|
+
def process_node_queue
|
199
|
+
@nodes.pop do |node|
|
200
|
+
Fiber.new do
|
201
|
+
process_node(node)
|
202
|
+
process_node_queue
|
203
|
+
end.resume unless @closed
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def process_node(node)
|
208
|
+
log_node(node, :in)
|
209
|
+
@stanza_size = 0
|
210
|
+
enforce_rate_limit
|
211
|
+
if error?(node)
|
212
|
+
close_stream
|
213
|
+
else
|
214
|
+
state.node(node)
|
215
|
+
end
|
216
|
+
rescue => e
|
217
|
+
error(e)
|
218
|
+
end
|
219
|
+
|
220
|
+
def enforce_rate_limit
|
221
|
+
unless @bucket.take(1)
|
222
|
+
raise StreamErrors::PolicyViolation.new('rate limit exceeded')
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def log_node(node, direction)
|
227
|
+
return unless log.debug?
|
228
|
+
from, to = @remote_addr, @local_addr
|
229
|
+
from, to = to, from if direction == :out
|
230
|
+
label = (direction == :out) ? 'Sent' : 'Received'
|
231
|
+
log.debug("%s %21s -> %s\n%s\n" %
|
232
|
+
["#{label} stanza:".ljust(PAD), from, to, node])
|
233
|
+
end
|
234
|
+
|
235
|
+
# Returns the current +State+ of the stream's state machine. Provided as a
|
236
|
+
# method so subclasses can override the behavior.
|
237
|
+
def state
|
238
|
+
@state
|
239
|
+
end
|
240
|
+
|
241
|
+
# Return +true+ if this is a valid domain-only JID that can be used in
|
242
|
+
# stream initiation stanza headers.
|
243
|
+
def valid_address?(jid)
|
244
|
+
JID.new(jid).domain? rescue false
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
@@ -0,0 +1,55 @@
|
|
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
|
data/lib/vines/user.rb
ADDED
@@ -0,0 +1,123 @@
|
|
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 follower?(jid)
|
38
|
+
!follower(jid).nil?
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns the follower with this jid or nil if not found.
|
42
|
+
def follower(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 follower's
|
48
|
+
# presence updates.
|
49
|
+
def subscribed_to?(jid)
|
50
|
+
follower = follower(jid)
|
51
|
+
follower && follower.subscribed_to?
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns true if the user has a presence subscription from this follower.
|
55
|
+
# The follower is subscribed to this user's presence.
|
56
|
+
def subscribed_from?(jid)
|
57
|
+
follower = follower(jid)
|
58
|
+
follower && follower.subscribed_from?
|
59
|
+
end
|
60
|
+
|
61
|
+
# Removes the follower with this jid from the user's roster.
|
62
|
+
def remove_follower(jid)
|
63
|
+
bare = JID.new(jid).bare
|
64
|
+
@roster.reject! {|c| c.jid.bare == bare }
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns a list of the followers to which this user has
|
68
|
+
# successfully subscribed.
|
69
|
+
def subscribed_to_followers
|
70
|
+
@roster.select {|c| c.subscribed_to? }
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns a list of the followers that are subscribed to this user's
|
74
|
+
# presence updates.
|
75
|
+
def subscribed_from_followers
|
76
|
+
@roster.select {|c| c.subscribed_from? }
|
77
|
+
end
|
78
|
+
|
79
|
+
# Update the follower's jid on this user's roster to signal that this user
|
80
|
+
# has requested the follower's permission to receive their presence updates.
|
81
|
+
def request_subscription(jid)
|
82
|
+
unless follower = follower(jid)
|
83
|
+
follower = Follower.new(:jid => jid)
|
84
|
+
@roster << follower
|
85
|
+
end
|
86
|
+
follower.ask = 'subscribe' if %w[none from].include?(follower.subscription)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Add the user's jid to this follower's roster with a subscription state of
|
90
|
+
# 'from.' This signals that this follower has approved a user's subscription.
|
91
|
+
def add_subscription_from(jid)
|
92
|
+
unless follower = follower(jid)
|
93
|
+
follower = Follower.new(:jid => jid)
|
94
|
+
@roster << follower
|
95
|
+
end
|
96
|
+
follower.subscribe_from
|
97
|
+
end
|
98
|
+
|
99
|
+
def remove_subscription_to(jid)
|
100
|
+
if follower = follower(jid)
|
101
|
+
follower.unsubscribe_to
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def remove_subscription_from(jid)
|
106
|
+
if follower = follower(jid)
|
107
|
+
follower.unsubscribe_from
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns this user's roster followers 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 |follower|
|
117
|
+
query << follower.to_roster_xml
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,25 @@
|
|
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
|