vinesmod 0.4.5

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.
Files changed (151) hide show
  1. data/Gemfile +3 -0
  2. data/LICENSE +19 -0
  3. data/README.md +43 -0
  4. data/Rakefile +57 -0
  5. data/bin/vines +93 -0
  6. data/conf/certs/README +39 -0
  7. data/conf/certs/ca-bundle.crt +3366 -0
  8. data/conf/config.rb +149 -0
  9. data/lib/vines.rb +197 -0
  10. data/lib/vines/cluster.rb +246 -0
  11. data/lib/vines/cluster/connection.rb +26 -0
  12. data/lib/vines/cluster/publisher.rb +55 -0
  13. data/lib/vines/cluster/pubsub.rb +92 -0
  14. data/lib/vines/cluster/sessions.rb +125 -0
  15. data/lib/vines/cluster/subscriber.rb +108 -0
  16. data/lib/vines/command/bcrypt.rb +12 -0
  17. data/lib/vines/command/cert.rb +50 -0
  18. data/lib/vines/command/init.rb +68 -0
  19. data/lib/vines/command/register.rb +27 -0
  20. data/lib/vines/command/restart.rb +12 -0
  21. data/lib/vines/command/schema.rb +24 -0
  22. data/lib/vines/command/start.rb +28 -0
  23. data/lib/vines/command/stop.rb +18 -0
  24. data/lib/vines/command/unregister.rb +27 -0
  25. data/lib/vines/config.rb +213 -0
  26. data/lib/vines/config/host.rb +119 -0
  27. data/lib/vines/config/port.rb +132 -0
  28. data/lib/vines/config/pubsub.rb +108 -0
  29. data/lib/vines/contact.rb +111 -0
  30. data/lib/vines/daemon.rb +78 -0
  31. data/lib/vines/error.rb +150 -0
  32. data/lib/vines/jid.rb +95 -0
  33. data/lib/vines/kit.rb +35 -0
  34. data/lib/vines/log.rb +24 -0
  35. data/lib/vines/router.rb +179 -0
  36. data/lib/vines/stanza.rb +175 -0
  37. data/lib/vines/stanza/iq.rb +48 -0
  38. data/lib/vines/stanza/iq/auth.rb +18 -0
  39. data/lib/vines/stanza/iq/disco_info.rb +45 -0
  40. data/lib/vines/stanza/iq/disco_items.rb +29 -0
  41. data/lib/vines/stanza/iq/error.rb +16 -0
  42. data/lib/vines/stanza/iq/ping.rb +16 -0
  43. data/lib/vines/stanza/iq/private_storage.rb +83 -0
  44. data/lib/vines/stanza/iq/query.rb +10 -0
  45. data/lib/vines/stanza/iq/register.rb +42 -0
  46. data/lib/vines/stanza/iq/result.rb +16 -0
  47. data/lib/vines/stanza/iq/roster.rb +140 -0
  48. data/lib/vines/stanza/iq/session.rb +17 -0
  49. data/lib/vines/stanza/iq/vcard.rb +56 -0
  50. data/lib/vines/stanza/iq/version.rb +25 -0
  51. data/lib/vines/stanza/message.rb +43 -0
  52. data/lib/vines/stanza/presence.rb +156 -0
  53. data/lib/vines/stanza/presence/error.rb +23 -0
  54. data/lib/vines/stanza/presence/probe.rb +37 -0
  55. data/lib/vines/stanza/presence/subscribe.rb +42 -0
  56. data/lib/vines/stanza/presence/subscribed.rb +51 -0
  57. data/lib/vines/stanza/presence/unavailable.rb +15 -0
  58. data/lib/vines/stanza/presence/unsubscribe.rb +38 -0
  59. data/lib/vines/stanza/presence/unsubscribed.rb +38 -0
  60. data/lib/vines/stanza/pubsub.rb +22 -0
  61. data/lib/vines/stanza/pubsub/create.rb +39 -0
  62. data/lib/vines/stanza/pubsub/delete.rb +41 -0
  63. data/lib/vines/stanza/pubsub/publish.rb +66 -0
  64. data/lib/vines/stanza/pubsub/subscribe.rb +44 -0
  65. data/lib/vines/stanza/pubsub/unsubscribe.rb +30 -0
  66. data/lib/vines/storage.rb +188 -0
  67. data/lib/vines/storage/local.rb +165 -0
  68. data/lib/vines/storage/null.rb +39 -0
  69. data/lib/vines/storage/sql.rb +260 -0
  70. data/lib/vines/store.rb +94 -0
  71. data/lib/vines/stream.rb +247 -0
  72. data/lib/vines/stream/client.rb +84 -0
  73. data/lib/vines/stream/client/auth.rb +74 -0
  74. data/lib/vines/stream/client/auth_restart.rb +29 -0
  75. data/lib/vines/stream/client/bind.rb +72 -0
  76. data/lib/vines/stream/client/bind_restart.rb +24 -0
  77. data/lib/vines/stream/client/closed.rb +13 -0
  78. data/lib/vines/stream/client/ready.rb +17 -0
  79. data/lib/vines/stream/client/session.rb +210 -0
  80. data/lib/vines/stream/client/start.rb +27 -0
  81. data/lib/vines/stream/client/tls.rb +38 -0
  82. data/lib/vines/stream/component.rb +58 -0
  83. data/lib/vines/stream/component/handshake.rb +26 -0
  84. data/lib/vines/stream/component/ready.rb +23 -0
  85. data/lib/vines/stream/component/start.rb +19 -0
  86. data/lib/vines/stream/http.rb +157 -0
  87. data/lib/vines/stream/http/auth.rb +22 -0
  88. data/lib/vines/stream/http/bind.rb +32 -0
  89. data/lib/vines/stream/http/bind_restart.rb +37 -0
  90. data/lib/vines/stream/http/ready.rb +29 -0
  91. data/lib/vines/stream/http/request.rb +172 -0
  92. data/lib/vines/stream/http/session.rb +120 -0
  93. data/lib/vines/stream/http/sessions.rb +65 -0
  94. data/lib/vines/stream/http/start.rb +23 -0
  95. data/lib/vines/stream/parser.rb +78 -0
  96. data/lib/vines/stream/sasl.rb +92 -0
  97. data/lib/vines/stream/server.rb +150 -0
  98. data/lib/vines/stream/server/auth.rb +13 -0
  99. data/lib/vines/stream/server/auth_restart.rb +13 -0
  100. data/lib/vines/stream/server/final_restart.rb +21 -0
  101. data/lib/vines/stream/server/outbound/auth.rb +31 -0
  102. data/lib/vines/stream/server/outbound/auth_restart.rb +20 -0
  103. data/lib/vines/stream/server/outbound/auth_result.rb +32 -0
  104. data/lib/vines/stream/server/outbound/final_features.rb +28 -0
  105. data/lib/vines/stream/server/outbound/final_restart.rb +20 -0
  106. data/lib/vines/stream/server/outbound/start.rb +20 -0
  107. data/lib/vines/stream/server/outbound/tls.rb +30 -0
  108. data/lib/vines/stream/server/outbound/tls_result.rb +34 -0
  109. data/lib/vines/stream/server/ready.rb +24 -0
  110. data/lib/vines/stream/server/start.rb +13 -0
  111. data/lib/vines/stream/server/tls.rb +13 -0
  112. data/lib/vines/stream/state.rb +60 -0
  113. data/lib/vines/token_bucket.rb +55 -0
  114. data/lib/vines/user.rb +123 -0
  115. data/lib/vines/version.rb +5 -0
  116. data/lib/vines/xmpp_server.rb +43 -0
  117. data/vines.gemspec +36 -0
  118. data/web/404.html +51 -0
  119. data/web/apple-touch-icon.png +0 -0
  120. data/web/chat/coffeescripts/chat.coffee +362 -0
  121. data/web/chat/coffeescripts/init.coffee +15 -0
  122. data/web/chat/index.html +16 -0
  123. data/web/chat/javascripts/app.js +1 -0
  124. data/web/chat/stylesheets/chat.css +144 -0
  125. data/web/favicon.png +0 -0
  126. data/web/lib/coffeescripts/button.coffee +25 -0
  127. data/web/lib/coffeescripts/contact.coffee +32 -0
  128. data/web/lib/coffeescripts/filter.coffee +49 -0
  129. data/web/lib/coffeescripts/layout.coffee +30 -0
  130. data/web/lib/coffeescripts/login.coffee +68 -0
  131. data/web/lib/coffeescripts/logout.coffee +5 -0
  132. data/web/lib/coffeescripts/navbar.coffee +84 -0
  133. data/web/lib/coffeescripts/notification.coffee +14 -0
  134. data/web/lib/coffeescripts/router.coffee +40 -0
  135. data/web/lib/coffeescripts/session.coffee +229 -0
  136. data/web/lib/coffeescripts/transfer.coffee +106 -0
  137. data/web/lib/images/dark-gray.png +0 -0
  138. data/web/lib/images/default-user.png +0 -0
  139. data/web/lib/images/light-gray.png +0 -0
  140. data/web/lib/images/logo-large.png +0 -0
  141. data/web/lib/images/logo-small.png +0 -0
  142. data/web/lib/images/white.png +0 -0
  143. data/web/lib/javascripts/base.js +12 -0
  144. data/web/lib/javascripts/icons.js +110 -0
  145. data/web/lib/javascripts/jquery.cookie.js +91 -0
  146. data/web/lib/javascripts/jquery.js +4 -0
  147. data/web/lib/javascripts/raphael.js +6 -0
  148. data/web/lib/javascripts/strophe.js +1 -0
  149. data/web/lib/stylesheets/base.css +385 -0
  150. data/web/lib/stylesheets/login.css +68 -0
  151. metadata +423 -0
@@ -0,0 +1,29 @@
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
@@ -0,0 +1,72 @@
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 <stream:features/> 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('<stream:features/>')
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
@@ -0,0 +1,24 @@
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
@@ -0,0 +1,13 @@
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
@@ -0,0 +1,17 @@
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
@@ -0,0 +1,210 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ class Client
6
+ # A Session tracks the state of a client stream over its lifetime from
7
+ # negotiation to processing stanzas to shutdown. By disconnecting the
8
+ # stream's state from the stream, we can allow multiple TCP connections
9
+ # to access one logical session (e.g. HTTP streams).
10
+ class Session
11
+ include Comparable
12
+
13
+ attr_accessor :domain, :user
14
+ attr_reader :id, :last_broadcast_presence, :state
15
+
16
+ def initialize(stream)
17
+ @stream = stream
18
+ @id = Kit.uuid
19
+ @config = stream.config
20
+ @state = Client::Start.new(stream)
21
+ @available = false
22
+ @domain = nil
23
+ @last_broadcast_presence = nil
24
+ @requested_roster = false
25
+ @unbound = false
26
+ @user = nil
27
+ end
28
+
29
+ def <=>(session)
30
+ session.is_a?(Session) ? self.id <=> session.id : nil
31
+ end
32
+
33
+ alias :eql? :==
34
+
35
+ def hash
36
+ @id.hash
37
+ end
38
+
39
+ def advance(state)
40
+ @state = state
41
+ end
42
+
43
+ # Returns true if this client has properly authenticated with
44
+ # the server.
45
+ def authenticated?
46
+ !@user.nil?
47
+ end
48
+
49
+ # Notify the session that the client has sent an initial presence
50
+ # broadcast and is now considered to be an "available" resource.
51
+ # Available resources are sent presence subscription stanzas.
52
+ def available!
53
+ @available = true
54
+ save_to_cluster
55
+ end
56
+
57
+ # An available resource has sent initial presence and can
58
+ # receive presence subscription requests.
59
+ def available?
60
+ @available && connected?
61
+ end
62
+
63
+ # Complete resource binding with the given resource name, provided by the
64
+ # client or generated by the server. Once resource binding is completed,
65
+ # the stream is considered to be "connected" and ready for traffic.
66
+ def bind!(resource)
67
+ @user.jid.resource = resource
68
+ router << self
69
+ save_to_cluster
70
+ end
71
+
72
+ # A connected resource has authenticated and bound a resource
73
+ # identifier.
74
+ def connected?
75
+ !@unbound && authenticated? && !@user.jid.bare?
76
+ end
77
+
78
+ # An interested resource has requested its roster and can
79
+ # receive roster pushes.
80
+ def interested?
81
+ @requested_roster && connected?
82
+ end
83
+
84
+ def last_broadcast_presence=(node)
85
+ @last_broadcast_presence = node
86
+ save_to_cluster
87
+ end
88
+
89
+ def ready?
90
+ @state.class == Client::Ready
91
+ end
92
+
93
+ # Notify the session that the client has requested its roster and is now
94
+ # considered to be an "interested" resource. Interested resources are sent
95
+ # roster pushes when changes are made to their contacts.
96
+ def requested_roster!
97
+ @requested_roster = true
98
+ save_to_cluster
99
+ end
100
+
101
+ def stream_type
102
+ :client
103
+ end
104
+
105
+ def write(data)
106
+ @stream.write(data)
107
+ end
108
+
109
+ # Called by the stream when it's disconnected from the client. The stream
110
+ # passes itself to this method in case multiple streams are accessing this
111
+ # session (e.g. BOSH/HTTP).
112
+ def unbind!(stream)
113
+ router.delete(self)
114
+ delete_from_cluster
115
+ unsubscribe_pubsub
116
+ @unbound = true
117
+ @available = false
118
+ broadcast_unavailable
119
+ end
120
+
121
+ # Returns streams for available resources to which this user
122
+ # has successfully subscribed.
123
+ def available_subscribed_to_resources
124
+ subscribed = @user.subscribed_to_contacts.map {|c| c.jid }
125
+ router.available_resources(subscribed, @user.jid)
126
+ end
127
+
128
+ # Returns streams for available resources that are subscribed
129
+ # to this user's presence updates.
130
+ def available_subscribers
131
+ subscribed = @user.subscribed_from_contacts.map {|c| c.jid }
132
+ router.available_resources(subscribed, @user.jid)
133
+ end
134
+
135
+ # Returns contacts hosted at remote servers to which this user has
136
+ # successfully subscribed.
137
+ def remote_subscribed_to_contacts
138
+ @user.subscribed_to_contacts.reject do |c|
139
+ @config.local_jid?(c.jid)
140
+ end
141
+ end
142
+
143
+ # Returns contacts hosted at remote servers that are subscribed
144
+ # to this user's presence updates.
145
+ def remote_subscribers(to=nil)
146
+ jid = (to.nil? || to.empty?) ? nil : JID.new(to).bare
147
+ @user.subscribed_from_contacts.reject do |c|
148
+ @config.local_jid?(c.jid) || (jid && c.jid.bare != jid)
149
+ end
150
+ end
151
+
152
+ private
153
+
154
+ def broadcast_unavailable
155
+ return unless authenticated?
156
+ Fiber.new do
157
+ broadcast(unavailable, available_subscribers)
158
+ broadcast(unavailable, router.available_resources(@user.jid, @user.jid))
159
+ remote_subscribers.each do |contact|
160
+ node = unavailable
161
+ node['to'] = contact.jid.bare.to_s
162
+ router.route(node) rescue nil # ignore RemoteServerNotFound
163
+ end
164
+ end.resume
165
+ end
166
+
167
+ def unavailable
168
+ doc = Nokogiri::XML::Document.new
169
+ doc.create_element('presence',
170
+ 'from' => @user.jid.to_s,
171
+ 'type' => 'unavailable')
172
+ end
173
+
174
+ def broadcast(stanza, recipients)
175
+ recipients.each do |recipient|
176
+ stanza['to'] = recipient.user.jid.to_s
177
+ recipient.write(stanza)
178
+ end
179
+ end
180
+
181
+ def router
182
+ @config.router
183
+ end
184
+
185
+ def save_to_cluster
186
+ if @config.cluster?
187
+ @config.cluster.save_session(@user.jid, to_hash)
188
+ end
189
+ end
190
+
191
+ def delete_from_cluster
192
+ if connected? && @config.cluster?
193
+ @config.cluster.delete_session(@user.jid)
194
+ end
195
+ end
196
+
197
+ def unsubscribe_pubsub
198
+ if connected?
199
+ @config.vhost(@user.jid.domain).unsubscribe_pubsub(@user.jid)
200
+ end
201
+ end
202
+
203
+ def to_hash
204
+ presence = @last_broadcast_presence ? @last_broadcast_presence.to_s : nil
205
+ {available: @available, interested: @requested_roster, presence: presence.to_s}
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,27 @@
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
@@ -0,0 +1,38 @@
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{<proceed xmlns="#{NS}"/>}.freeze
9
+ FAILURE = %Q{<failure xmlns="#{NS}"/>}.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('</stream:stream>')
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
@@ -0,0 +1,58 @@
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 "<stream:stream %s>" % attrs.to_a.map{|k,v| "#{k}='#{v}'"}.join(' ')
55
+ end
56
+ end
57
+ end
58
+ end