diaspora-vines 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/contact.rb +111 -0
- data/lib/vines/daemon.rb +78 -0
- data/lib/vines/error.rb +150 -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/contact_test.rb +102 -0
- data/test/error_test.rb +58 -0
- data/test/ext/nokogiri.rb +14 -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 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
|
@@ -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
|