jabber4r-revive 0.9.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/.gitignore +5 -4
  2. data/.rspec +3 -3
  3. data/.travis.yml +7 -7
  4. data/CHANGELOG +11 -1
  5. data/Gemfile +3 -3
  6. data/README.md +29 -29
  7. data/Rakefile +70 -70
  8. data/jabber4r-revive.gemspec +25 -25
  9. data/lib/jabber4r.rb +38 -33
  10. data/lib/jabber4r/bosh.rb +21 -0
  11. data/lib/jabber4r/bosh/authentication.rb +13 -0
  12. data/lib/jabber4r/bosh/authentication/non_sasl.rb +219 -0
  13. data/lib/jabber4r/bosh/authentication/sasl.rb +239 -0
  14. data/lib/jabber4r/bosh/session.rb +144 -0
  15. data/lib/jabber4r/connection.rb +259 -258
  16. data/lib/jabber4r/debugger.rb +60 -60
  17. data/lib/jabber4r/jid.rb +20 -19
  18. data/lib/jabber4r/protocol.rb +249 -257
  19. data/lib/jabber4r/protocol/authentication.rb +14 -0
  20. data/lib/jabber4r/protocol/authentication/non_sasl.rb +138 -0
  21. data/lib/jabber4r/protocol/authentication/sasl.rb +88 -0
  22. data/lib/jabber4r/protocol/iq.rb +259 -259
  23. data/lib/jabber4r/protocol/message.rb +245 -245
  24. data/lib/jabber4r/protocol/parsed_xml_element.rb +207 -207
  25. data/lib/jabber4r/protocol/presence.rb +160 -160
  26. data/lib/jabber4r/protocol/xml_element.rb +143 -143
  27. data/lib/jabber4r/rexml_1.8_patch.rb +15 -15
  28. data/lib/jabber4r/roster.rb +38 -38
  29. data/lib/jabber4r/session.rb +615 -615
  30. data/lib/jabber4r/version.rb +10 -3
  31. data/spec/lib/jabber4r/bosh/authentication/non_sasl_spec.rb +79 -0
  32. data/spec/lib/jabber4r/bosh/authentication/sasl_spec.rb +42 -0
  33. data/spec/lib/jabber4r/bosh/session_spec.rb +406 -0
  34. data/spec/lib/jabber4r/bosh_spec.rb +0 -0
  35. data/spec/lib/jabber4r/connection_spec.rb +174 -174
  36. data/spec/lib/jabber4r/debugger_spec.rb +35 -35
  37. data/spec/lib/jabber4r/jid_spec.rb +197 -197
  38. data/spec/lib/jabber4r/protocol/authentication/non_sasl_spec.rb +79 -0
  39. data/spec/lib/jabber4r/protocol/authentication/sasl_spec.rb +42 -0
  40. data/spec/spec_helper.rb +11 -11
  41. data/spec/support/mocks/tcp_socket_mock.rb +8 -8
  42. metadata +61 -45
  43. data/Gemfile.lock +0 -45
  44. data/lib/jabber4r/bosh_session.rb +0 -224
  45. data/spec/lib/jabber4r/bosh_session_spec.rb +0 -150
@@ -0,0 +1,239 @@
1
+ # coding: utf-8
2
+
3
+ # License: see LICENSE
4
+ # Jabber4R - Jabber Instant Messaging Library for Ruby
5
+ # Copyright (C) 2002 Rich Kilmer <rich@infoether.com>
6
+ # Copyright (C) 2013 Sergey Fedorov <strech_ftf@mail.ru>
7
+
8
+ require "ox"
9
+
10
+ module Jabber::Bosh::Authentication
11
+ # This class provides SASL authentication for BOSH session
12
+ class SASL
13
+ attr_reader :session
14
+ attr_reader :mechanisms
15
+
16
+ def initialize(session)
17
+ @session = session
18
+ end
19
+
20
+ # Internal: Send open stream command to jabber server
21
+ #
22
+ # Raises XMLMalformedError
23
+ # Returns boolean
24
+ def open_stream
25
+ body = Ox::Element.new("body").tap do |element|
26
+ element[:xmlns] = "http://jabber.org/protocol/httpbind"
27
+ element["xmlns:xmpp"] = "urn:xmpp:xbosh"
28
+ element["xmpp:version"] = "1.0"
29
+ element[:content] = "text/xml; charset=utf-8"
30
+ element[:rid] = session.generate_next_rid
31
+ element[:to] = session.domain
32
+ element[:secure] = true
33
+ element[:wait] = 60
34
+ element[:hold] = 1
35
+ end
36
+
37
+ Jabber.debug(%Q[Open (rid="#{body.rid}") new session])
38
+
39
+ response = session.post(Ox.dump body)
40
+ xml = Ox.parse(response.body.tr("'", '"'))
41
+
42
+ define_stream_mechanisms(xml)
43
+
44
+ raise Jabber::XMLMalformedError,
45
+ "Couldn't find <body /> attribute [sid]" if xml[:sid].nil?
46
+
47
+ session.sid = xml.sid
48
+
49
+ true
50
+ end
51
+
52
+ # Internal: Send login request to jabber server
53
+ #
54
+ # password - String the password of jabber user
55
+ #
56
+ # Raises TypeError
57
+ # Raises XMLMalformedError
58
+ # Returns boolean
59
+ def pass_authentication(password)
60
+ # TODO : Define different exception type
61
+ raise TypeError,
62
+ "Server SASL mechanisms not include PLAIN mechanism" unless mechanisms.include?(:plain)
63
+
64
+ body = Ox::Element.new("body").tap do |element|
65
+ element[:xmlns] = 'http://jabber.org/protocol/httpbind'
66
+ element["xmpp:version"] = "1.0"
67
+ element["xmlns:xmpp"] = "urn:xmpp:xbosh"
68
+ element[:content] = "text/xml; charset=utf-8"
69
+ element[:rid] = session.generate_next_rid
70
+ element[:sid] = session.sid
71
+
72
+ element << Query.new(session.jid, password, mechanism: :plain).to_ox
73
+ end
74
+
75
+ Jabber.debug(%Q[Authenticate {SASL} (rid="#{body.rid}" sid="#{body.sid}") in opened session] +
76
+ %Q[ as #{session.jid.node}/#{session.jid.resource}])
77
+
78
+ response = session.post(Ox.dump body)
79
+ xml = Ox.parse(response.body.tr("'", '"'))
80
+
81
+ return false if xml.locate("success").empty?
82
+
83
+ true
84
+ end
85
+
86
+ def restart_stream
87
+ body = Ox::Element.new("body").tap do |element|
88
+ element[:xmlns] = 'http://jabber.org/protocol/httpbind'
89
+ element["xmlns:xmpp"] = "urn:xmpp:xbosh"
90
+ element["xmpp:version"] = "1.0"
91
+ element["xmpp:restart"] = true
92
+ element[:content] = "text/xml; charset=utf-8"
93
+ element[:rid] = session.generate_next_rid
94
+ element[:sid] = session.sid
95
+ element[:to] = session.jid.domain
96
+ end
97
+
98
+ response = session.post(Ox.dump body)
99
+ xml = Ox.parse(response.body.tr("'", '"'))
100
+
101
+ return false if xml.locate("stream:features/bind").empty?
102
+
103
+ true
104
+ end
105
+
106
+ def bind_resource
107
+ bind = Ox::Element.new("bind").tap do |element|
108
+ element[:xmlns] = "urn:ietf:params:xml:ns:xmpp-bind"
109
+
110
+ element << (Ox::Element.new("resource") << session.jid.resource)
111
+ end
112
+
113
+ iq = Ox::Element.new("iq").tap do |element|
114
+ element[:id] = Jabber.gen_random_id
115
+ element[:type] = "set"
116
+ element[:xmlns] = "jabber:client"
117
+
118
+ element << bind
119
+ end
120
+
121
+ body = Ox::Element.new("body").tap do |element|
122
+ element[:xmlns] = 'http://jabber.org/protocol/httpbind'
123
+ element["xmlns:xmpp"] = "urn:xmpp:xbosh"
124
+ element["xmpp:version"] = "1.0"
125
+ element[:content] = "text/xml; charset=utf-8"
126
+ element[:rid] = session.generate_next_rid
127
+ element[:sid] = session.sid
128
+
129
+ element << iq
130
+ end
131
+
132
+ response = session.post(Ox.dump body)
133
+ xml = Ox.parse(response.body.tr("'", '"'))
134
+
135
+ raise Jabber::XMLMalformedError, "Couldn't find xml tag <iq/>" if xml.locate("iq").empty?
136
+ return false unless xml.iq[:type] == "result"
137
+
138
+ true
139
+ end
140
+
141
+ # TODO : Make state machine
142
+ def authenticate(jid, password)
143
+ open_stream
144
+
145
+ return false if pass_authentication(password) == false
146
+ return false if restart_stream == false
147
+
148
+ bind_resource
149
+ end
150
+
151
+ # Public: ...
152
+ #
153
+ # xml - Ox::Element
154
+ #
155
+ # Returns Array[Symbol]
156
+ def define_stream_mechanisms(xml)
157
+ @mechanisms = xml.locate("stream:features/mechanisms/mechanism/*")
158
+ .map(&:downcase).map(&:to_sym)
159
+ end
160
+
161
+ private
162
+ class Query
163
+ # Full list of mechanisms
164
+ # http://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml
165
+ MECHANISMS = [:plain].freeze
166
+
167
+ attr_reader :jid, :password
168
+ attr_reader :mechanism
169
+
170
+ # Public: Creates new SASL authentication object
171
+ #
172
+ # jid - [Jabber::JID|String] the jid of jabber server user
173
+ # password - String the user password
174
+ # options - Hash the authentication options (default: Empty hash)
175
+ # :mechanism - Symbol the name of mechnism to use
176
+ #
177
+ # Examples
178
+ #
179
+ # non_sasl = Jabber::Protocol::Authentication::SASL.new("strech@localhost/res-1", "my-pass-phrase")
180
+ # non_sasl.plain? # => true
181
+ # non_sasl.to_xml # =>
182
+ #
183
+ # <auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="PLAIN">biwsbj1qdWxpZXQscj1vTXNUQUF3QUFBQU1BQUFBTlAwVEFBQUFBQUJQVTBBQQ==</auth>
184
+ def initialize(jid, password, options = {})
185
+ raise TypeError,
186
+ "Class(Jabber::JID) or Class(String) expected," +
187
+ " but #{jid.class} was given" unless jid.is_a?(Jabber::JID) || jid.is_a?(String)
188
+
189
+ @jid = jid.is_a?(Jabber::JID) ? jid : Jabber::JID.new(jid)
190
+ @password = password
191
+
192
+ @mechanism = options.fetch(:mechanism, :plain)
193
+
194
+ raise ArgumentError,
195
+ "Unknown authentication mechanism '#{mechanism}'," +
196
+ " available is [#{MECHANISMS * ", "}]" unless MECHANISMS.include?(mechanism)
197
+ end
198
+
199
+ # Public: Is SASL object is for plain authentication
200
+ #
201
+ # Returns boolean
202
+ def plain?
203
+ mechanism == :plain
204
+ end
205
+
206
+ # Public: Create XML string from SASL object
207
+ #
208
+ # Returns String
209
+ def dump
210
+ Ox.dump(send mechanism)
211
+ end
212
+ alias :to_xml :dump
213
+
214
+ # Public: Create Ox::Element from SASL object
215
+ #
216
+ # Returns Ox::Element
217
+ def to_ox
218
+ send(mechanism)
219
+ end
220
+
221
+ private
222
+ # Internal: Make xml object for plain authentication mechanism
223
+ #
224
+ # Returns Ox:Element
225
+ def plain
226
+ Ox::Element.new("auth").tap do |element|
227
+ element[:xmlns] = "urn:ietf:params:xml:ns:xmpp-sasl"
228
+ element[:mechanism] = "PLAIN"
229
+
230
+ element << self.class.generate_plain(jid, password)
231
+ end
232
+ end
233
+
234
+ def self.generate_plain(jid, password)
235
+ ["#{jid.strip}\x00#{jid.node}\x00#{password}"].pack("m").gsub(/\s/, "")
236
+ end
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,144 @@
1
+ # coding: utf-8
2
+
3
+ # License: see LICENSE
4
+ # Jabber4R - Jabber Instant Messaging Library for Ruby
5
+ # Copyright (C) 2002 Rich Kilmer <rich@infoether.com>
6
+ # Copyright (C) 2013 Sergey Fedorov <strech_ftf@mail.ru>
7
+
8
+ require "json"
9
+ require "net/http"
10
+
11
+ module Jabber::Bosh
12
+ # This class provide XMPP Over BOSH
13
+ # http://xmpp.org/extensions/xep-0206.html
14
+ class Session
15
+ # Public: Jabber user login
16
+ attr_reader :jid
17
+ # Public: Http request identifier
18
+ attr_accessor :rid
19
+ # Public: Session identifier
20
+ attr_accessor :sid
21
+
22
+ # Public: Bosh service domain
23
+ # FIXME : Replace to host
24
+ attr_reader :domain
25
+ # Public: Bosh service port
26
+ attr_reader :port
27
+ # Public: Bosh service http-bind uri
28
+ attr_reader :bind_uri
29
+
30
+ # Public: Create new BOSH-session and bind it to jabber http-bind service
31
+ #
32
+ # username - String the login of jabber server user
33
+ # password - String the password of jabber server user
34
+ # options - Hash the options for jabber http-bind service (default: Empty hash)
35
+ # :domain - String the jabber server domain indentificator
36
+ # :port - [String|Fixnum] the port of http-bind endpoint of jabber server
37
+ # :bind_uri - String the http-bind uri
38
+ # :use_sasl - boolean the flag defining authentication method
39
+ #
40
+ # Examples
41
+ #
42
+ # Jabber::Bosh::Session.bind("strech@localhost/res-1", "secret-pass")
43
+ # Jabber::Bosh::Session.bind("strech@localhost/res-1", "secret-pass", domain: "localhost")
44
+ #
45
+ # Raises Jabber::AuthenticationError
46
+ # Returns Jabber::Bosh::Session
47
+ # TODO : Change arguments passing into initialize method
48
+ def self.bind(username, password, options = {})
49
+ domain, port, bind_uri, use_sasl = Jabber::Bosh::DEFAULTS.dup.merge!(options).values
50
+
51
+ session = new(domain, port, bind_uri, use_sasl: use_sasl)
52
+ raise Jabber::AuthenticationError, "Failed to login" unless session.authenticate(username, password)
53
+
54
+ session
55
+ end
56
+
57
+ # Public: Create new BOSH-session (not binded to http-bind service)
58
+ #
59
+ # domain - String the jabber server domain
60
+ # port - [String|Fixnum] the port of http-bind endpoint of jabber server
61
+ # bind_uri - String the http-bind uri
62
+ #
63
+ # Returns Jabber::BoshSession
64
+ def initialize(domain, port, bind_uri, options = {})
65
+ @domain, @port, @bind_uri = domain, port, bind_uri
66
+ @use_sasl = options.fetch(:use_sasl, Jabber::Bosh::DEFAULTS[:use_sasl])
67
+
68
+ @alive = false
69
+ end
70
+
71
+ # Public: Authenticate user in jabber server by his username and password
72
+ # NOTE: This authentication is SASL http://xmpp.org/rfcs/rfc3920.html#sasl
73
+ # or Non-SASL http://xmpp.org/extensions/xep-0078.html
74
+ #
75
+ # Returns boolean
76
+ def authenticate(username, password)
77
+ @jid = username.is_a?(Jabber::JID) ? username : Jabber::JID.new(username)
78
+
79
+ authentication = authentication_technology.new(self)
80
+ @alive = authentication.authenticate(jid, password)
81
+ end
82
+
83
+ # Public: Is BOSH-session active? (no polling consider)
84
+ #
85
+ # Returns boolean
86
+ def alive?
87
+ @alive
88
+ end
89
+
90
+ # Public: Is BOSH-session uses sasl authentication
91
+ #
92
+ # Returns boolean
93
+ def sasl?
94
+ @use_sasl
95
+ end
96
+
97
+ # Public: Represent BOSH-session as json object
98
+ #
99
+ # Returns String
100
+ def to_json
101
+ {jid: jid.to_s, rid: rid, sid: sid}.to_json
102
+ end
103
+
104
+ # Internal: Send HTTP-post request on HTTP-bind uri
105
+ #
106
+ # body - String data, which will be sended
107
+ #
108
+ # Raises Net::HTTPBadResponse
109
+ # Returns Net:HttpResponse
110
+ def post(body)
111
+ request = Net::HTTP::Post.new(bind_uri)
112
+ request.body = body
113
+ request.content_length = request.body.size
114
+ request["Content-Type"] = "text/xml; charset=utf-8"
115
+
116
+ Jabber.debug("Sending POST request - #{body.strip}")
117
+
118
+ response = Net::HTTP.new(domain, port).start { |http| http.request(request) }
119
+
120
+ Jabber.debug("Receiving POST response - #{response.code}: #{response.body.inspect}")
121
+
122
+ unless response.is_a?(Net::HTTPSuccess)
123
+ raise Net::HTTPBadResponse, "Net::HTTPSuccess expected, but #{response.class} was received"
124
+ end
125
+
126
+ response
127
+ end
128
+
129
+ # Internal: Generate next request id for http post request
130
+ #
131
+ # FIXME : Will be replaced to Generator class
132
+ #
133
+ # Returns Fixnum
134
+ def generate_next_rid
135
+ @rid ||= rand(1_000_000_000)
136
+ @rid += 1
137
+ end
138
+
139
+ private
140
+ def authentication_technology
141
+ sasl? ? Jabber::Bosh::Authentication::SASL : Jabber::Bosh::Authentication::NonSASL
142
+ end
143
+ end
144
+ end
@@ -1,258 +1,259 @@
1
- # coding: utf-8
2
-
3
- # License: see LICENSE
4
- # Jabber4R - Jabber Instant Messaging Library for Ruby
5
- # Copyright (C) 2002 Rich Kilmer <rich@infoether.com>
6
-
7
- module Jabber
8
- # The connection class encapsulates the connection to the Jabber
9
- # service including managing the socket and controlling the parsing
10
- # of the Jabber XML stream.
11
- class Connection
12
- DISCONNECTED = 1
13
- CONNECTED = 2
14
-
15
- # Public
16
- attr_reader :host, :port, :status, :input, :output
17
-
18
- # Internal
19
- attr_reader :poll_thread, :parser_thread
20
-
21
- # Internal
22
- attr_reader :filters, :handlers
23
-
24
- # Internal
25
- attr_reader :socket, :parser
26
-
27
- def initialize(host, port = 5222)
28
- @host, @port = host, port
29
-
30
- @handlers, @filters = {}, {}
31
-
32
- @poll_counter = 10
33
- @mutex = Mutex.new
34
-
35
- @status = DISCONNECTED
36
- end
37
-
38
- # Public: Connects to the Jabber server through a TCP Socket and
39
- # starts the Jabber parser.
40
- #
41
- # Returns nothing
42
- def connect
43
- @socket = TCPSocket.new(@host, @port)
44
- @parser = Jabber::Protocol.Parser.new(socket, self)
45
-
46
- register_parsing_thread
47
- register_polling_thread
48
-
49
- @status = CONNECTED
50
- end
51
-
52
- # Internal: Register new parser thread
53
- #
54
- # Returns nothing
55
- def register_parsing_thread
56
- @parser_thread = Thread.new { parser.parse }
57
- end
58
-
59
- # Internal: Register new polling thread
60
- #
61
- # Returns nothing
62
- def register_polling_thread
63
- @poll_thread = Thread.new { poll }
64
- end
65
-
66
- # Public: Closes the connection to the Jabber service
67
- #
68
- # Returns nothing
69
- def close
70
- parser_thread.kill if parser_thread # why if?
71
- poll_thread.kill
72
- socket.close if socket
73
-
74
- @status = DISCONNECTED
75
- end
76
- alias :disconnect :close
77
-
78
- # Public: Returns if this connection is connected to a Jabber service
79
- #
80
- # Returns boolean
81
- def connected?
82
- status == CONNECTED
83
- end
84
-
85
- # Public: Returns if this connection is NOT connected to a Jabber service
86
- #
87
- # Returns boolean
88
- def disconnected?
89
- status == DISCONNECTED
90
- end
91
-
92
- # Public: Adds a filter block to process received XML messages
93
- #
94
- # name - String the name of filter
95
- # block - Block of code
96
- #
97
- # Returns nothing
98
- def add_filter(name, &block)
99
- raise ArgumentError, "Expected block to be given" if block.nil?
100
-
101
- @filters[name] = block
102
- end
103
-
104
- # Public: Removes a filter block
105
- #
106
- # name - String the name of filter
107
- #
108
- # Returns Block of code
109
- def remove_filter(name)
110
- filters.delete(name)
111
- end
112
-
113
- # Public: Receiving xml element, and processing it
114
- # NOTE: Synchonized by Mutex
115
- #
116
- # xml - String the string containing xml
117
- # proc_object - Proc the proc object to call (default: nil)
118
- # block - Block of ruby code
119
- #
120
- # Returns nothing
121
- def send(xml, proc_object = nil, &block)
122
- @mutex.synchronize { write_to_socket(xml, proc_object, &block) }
123
- end
124
-
125
- # Public: Receiving xml element, and processing it
126
- # NOTE: Synchonized by Mutex
127
- #
128
- # xml_element - ParsedXMLElement the received from socket xml element
129
- #
130
- # Returns nothing
131
- def receive(xml)
132
- @mutex.synchronize { process_xml_from_socket(xml) }
133
- end
134
-
135
- # Internal: Sends XML data to the socket and (optionally) waits
136
- # to process received data.
137
- # NOTE: If both habdler and block are given, handler has higher proirity
138
- #
139
- # xml - String the xml data to send
140
- # handler - [Proc|Lambda|#call] the proc object or labda to handle response data (optional)
141
- # block - Block the block of ruby code (optional)
142
- #
143
- # Returns nothing
144
- def write_to_socket(xml, handler = nil, &block)
145
- Jabber.debug("SENDING:\n#{xml}")
146
-
147
- handler = block if handler.nil?
148
- handlers[Thread.current] = handler unless handler.nil?
149
-
150
- socket.write(xml)
151
-
152
- @poll_counter = 10
153
- end
154
-
155
- # Internal: Processes a received ParsedXMLElement and executes
156
- # registered handlers and filters against it
157
- #
158
- # xml - ParsedXMLElement The received element
159
- #
160
- # Returns nothing
161
- def process_xml_from_socket(xml)
162
- sleep 0.1 while wait_for_consume?
163
-
164
- Jabber.debug("RECEIVED:\n#{xml}")
165
-
166
- consume_xml_by_handlers(xml) || consume_xml_by_filters(xml)
167
- end
168
-
169
- # Internal: Processes a received ParsedXMLElement by handlers
170
- #
171
- # xml - ParsedXMLElement The received element
172
- #
173
- # Returns boolean
174
- def consume_xml_by_handlers(xml)
175
- handlers.each do |thread, block|
176
- begin
177
- block.call(xml)
178
-
179
- if xml.element_consumed?
180
- handlers.delete(thread)
181
- thread.wakeup if thread.alive?
182
-
183
- return true
184
- end
185
- rescue Exception => error
186
- puts error.to_s
187
- puts error.backtrace.join("\n")
188
- end
189
- end
190
-
191
- false
192
- end
193
-
194
- # Internal: Processes a received ParsedXMLElement by filters
195
- #
196
- # xml - ParsedXMLElement The received element
197
- #
198
- # Returns boolean
199
- def consume_xml_by_filters(xml)
200
- filters.each_value do |block|
201
- begin
202
- block.call(xml)
203
-
204
- return true if xml.element_consumed?
205
- rescue Exception => error
206
- puts error.to_s
207
- puts error.backtrace.join("\n")
208
- end
209
- end
210
-
211
- false
212
- end
213
-
214
- # Internal: Should we wait for next part of socket data
215
- #
216
- # Returns boolean
217
- def wait_for_consume?
218
- handlers.size.zero? && filters.size.zero?
219
- end
220
-
221
- ##
222
- # Mounts a block to handle exceptions if they occur during the
223
- # poll send. This will likely be the first indication that
224
- # the socket dropped in a Jabber Session.
225
- #
226
- def on_connection_exception(&block)
227
- @exception_block = block
228
- end
229
-
230
- def parse_failure(exception = nil)
231
- Thread.new { @exception_block.call(exception) if @exception_block }
232
- end
233
-
234
- ############################################################################
235
- # All that under needs to be REFACTORED #
236
- ############################################################################
237
-
238
- ##
239
- # Starts a polling thread to send "keep alive" data to prevent
240
- # the Jabber connection from closing for inactivity.
241
- #
242
- def poll
243
- sleep 10
244
- while true
245
- sleep 2
246
- @poll_counter = @poll_counter - 1
247
- if @poll_counter < 0
248
- begin
249
- send(" \t ")
250
- rescue
251
- Thread.new {@exception_block.call if @exception_block}
252
- break
253
- end
254
- end
255
- end
256
- end
257
- end
258
- end
1
+ # coding: utf-8
2
+
3
+ # License: see LICENSE
4
+ # Jabber4R - Jabber Instant Messaging Library for Ruby
5
+ # Copyright (C) 2002 Rich Kilmer <rich@infoether.com>
6
+ # Copyright (C) 2013 Sergey Fedorov <strech_ftf@mail.ru>
7
+
8
+ module Jabber
9
+ # The connection class encapsulates the connection to the Jabber
10
+ # service including managing the socket and controlling the parsing
11
+ # of the Jabber XML stream.
12
+ class Connection
13
+ DISCONNECTED = 1
14
+ CONNECTED = 2
15
+
16
+ # Public
17
+ attr_reader :domain, :port, :status, :input, :output
18
+
19
+ # Internal
20
+ attr_reader :poll_thread, :parser_thread
21
+
22
+ # Internal
23
+ attr_reader :filters, :handlers
24
+
25
+ # Internal
26
+ attr_reader :socket, :parser
27
+
28
+ def initialize(domain, port = 5222)
29
+ @domain, @port = domain, port
30
+
31
+ @handlers, @filters = {}, {}
32
+
33
+ @poll_counter = 10
34
+ @mutex = Mutex.new
35
+
36
+ @status = DISCONNECTED
37
+ end
38
+
39
+ # Public: Connects to the Jabber server through a TCP Socket and
40
+ # starts the Jabber parser.
41
+ #
42
+ # Returns nothing
43
+ def connect
44
+ @socket = TCPSocket.new(@domain, @port)
45
+ @parser = Jabber::Protocol.Parser.new(socket, self)
46
+
47
+ register_parsing_thread
48
+ register_polling_thread
49
+
50
+ @status = CONNECTED
51
+ end
52
+
53
+ # Internal: Register new parser thread
54
+ #
55
+ # Returns nothing
56
+ def register_parsing_thread
57
+ @parser_thread = Thread.new { parser.parse }
58
+ end
59
+
60
+ # Internal: Register new polling thread
61
+ #
62
+ # Returns nothing
63
+ def register_polling_thread
64
+ @poll_thread = Thread.new { poll }
65
+ end
66
+
67
+ # Public: Closes the connection to the Jabber service
68
+ #
69
+ # Returns nothing
70
+ def close
71
+ parser_thread.kill if parser_thread # why if?
72
+ poll_thread.kill
73
+ socket.close if socket
74
+
75
+ @status = DISCONNECTED
76
+ end
77
+ alias :disconnect :close
78
+
79
+ # Public: Returns if this connection is connected to a Jabber service
80
+ #
81
+ # Returns boolean
82
+ def connected?
83
+ status == CONNECTED
84
+ end
85
+
86
+ # Public: Returns if this connection is NOT connected to a Jabber service
87
+ #
88
+ # Returns boolean
89
+ def disconnected?
90
+ status == DISCONNECTED
91
+ end
92
+
93
+ # Public: Adds a filter block to process received XML messages
94
+ #
95
+ # name - String the name of filter
96
+ # block - Block of code
97
+ #
98
+ # Returns nothing
99
+ def add_filter(name, &block)
100
+ raise ArgumentError, "Expected block to be given" if block.nil?
101
+
102
+ @filters[name] = block
103
+ end
104
+
105
+ # Public: Removes a filter block
106
+ #
107
+ # name - String the name of filter
108
+ #
109
+ # Returns Block of code
110
+ def remove_filter(name)
111
+ filters.delete(name)
112
+ end
113
+
114
+ # Public: Receiving xml element, and processing it
115
+ # NOTE: Synchonized by Mutex
116
+ #
117
+ # xml - String the string containing xml
118
+ # proc_object - Proc the proc object to call (default: nil)
119
+ # block - Block of ruby code
120
+ #
121
+ # Returns nothing
122
+ def send(xml, proc_object = nil, &block)
123
+ @mutex.synchronize { write_to_socket(xml, proc_object, &block) }
124
+ end
125
+
126
+ # Public: Receiving xml element, and processing it
127
+ # NOTE: Synchonized by Mutex
128
+ #
129
+ # xml_element - ParsedXMLElement the received from socket xml element
130
+ #
131
+ # Returns nothing
132
+ def receive(xml)
133
+ @mutex.synchronize { process_xml_from_socket(xml) }
134
+ end
135
+
136
+ # Internal: Sends XML data to the socket and (optionally) waits
137
+ # to process received data.
138
+ # NOTE: If both habdler and block are given, handler has higher proirity
139
+ #
140
+ # xml - String the xml data to send
141
+ # handler - [Proc|Lambda|#call] the proc object or labda to handle response data (optional)
142
+ # block - Block the block of ruby code (optional)
143
+ #
144
+ # Returns nothing
145
+ def write_to_socket(xml, handler = nil, &block)
146
+ Jabber.debug("SENDING:\n#{xml}")
147
+
148
+ handler = block if handler.nil?
149
+ handlers[Thread.current] = handler unless handler.nil?
150
+
151
+ socket.write(xml)
152
+
153
+ @poll_counter = 10
154
+ end
155
+
156
+ # Internal: Processes a received ParsedXMLElement and executes
157
+ # registered handlers and filters against it
158
+ #
159
+ # xml - ParsedXMLElement The received element
160
+ #
161
+ # Returns nothing
162
+ def process_xml_from_socket(xml)
163
+ sleep 0.1 while wait_for_consume?
164
+
165
+ Jabber.debug("RECEIVED:\n#{xml}")
166
+
167
+ consume_xml_by_handlers(xml) || consume_xml_by_filters(xml)
168
+ end
169
+
170
+ # Internal: Processes a received ParsedXMLElement by handlers
171
+ #
172
+ # xml - ParsedXMLElement The received element
173
+ #
174
+ # Returns boolean
175
+ def consume_xml_by_handlers(xml)
176
+ handlers.each do |thread, block|
177
+ begin
178
+ block.call(xml)
179
+
180
+ if xml.element_consumed?
181
+ handlers.delete(thread)
182
+ thread.wakeup if thread.alive?
183
+
184
+ return true
185
+ end
186
+ rescue Exception => error
187
+ puts error.to_s
188
+ puts error.backtrace.join("\n")
189
+ end
190
+ end
191
+
192
+ false
193
+ end
194
+
195
+ # Internal: Processes a received ParsedXMLElement by filters
196
+ #
197
+ # xml - ParsedXMLElement The received element
198
+ #
199
+ # Returns boolean
200
+ def consume_xml_by_filters(xml)
201
+ filters.each_value do |block|
202
+ begin
203
+ block.call(xml)
204
+
205
+ return true if xml.element_consumed?
206
+ rescue Exception => error
207
+ puts error.to_s
208
+ puts error.backtrace.join("\n")
209
+ end
210
+ end
211
+
212
+ false
213
+ end
214
+
215
+ # Internal: Should we wait for next part of socket data
216
+ #
217
+ # Returns boolean
218
+ def wait_for_consume?
219
+ handlers.size.zero? && filters.size.zero?
220
+ end
221
+
222
+ ##
223
+ # Mounts a block to handle exceptions if they occur during the
224
+ # poll send. This will likely be the first indication that
225
+ # the socket dropped in a Jabber Session.
226
+ #
227
+ def on_connection_exception(&block)
228
+ @exception_block = block
229
+ end
230
+
231
+ def parse_failure(exception = nil)
232
+ Thread.new { @exception_block.call(exception) if @exception_block }
233
+ end
234
+
235
+ ############################################################################
236
+ # All that under needs to be REFACTORED #
237
+ ############################################################################
238
+
239
+ ##
240
+ # Starts a polling thread to send "keep alive" data to prevent
241
+ # the Jabber connection from closing for inactivity.
242
+ #
243
+ def poll
244
+ sleep 10
245
+ while true
246
+ sleep 2
247
+ @poll_counter = @poll_counter - 1
248
+ if @poll_counter < 0
249
+ begin
250
+ send(" \t ")
251
+ rescue
252
+ Thread.new {@exception_block.call if @exception_block}
253
+ break
254
+ end
255
+ end
256
+ end
257
+ end
258
+ end
259
+ end