File: connection.rb

package info (click to toggle)
ruby-xmpp4r 0.5.6-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye, forky, sid, trixie
  • size: 1,384 kB
  • sloc: ruby: 17,382; xml: 74; sh: 12; makefile: 4
file content (235 lines) | stat: -rw-r--r-- 6,586 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# =XMPP4R - XMPP Library for Ruby
# License:: Ruby's license (see the LICENSE file) or GNU GPL, at your option.
# Website::http://xmpp4r.github.io

begin
  require 'openssl'
rescue LoadError
end
require 'xmpp4r/stream'

module Jabber
  ##
  # The connection class manages the TCP connection to the Jabber server
  #
  class Connection  < Stream
    attr_reader :host, :port

    # How many seconds to wait for <stream:features/>
    # before proceeding
    attr_accessor :features_timeout

    # Keep-alive interval in seconds, or nil to disable keepalive,
    # defaults to 60 (see private method keepalive_loop for
    # implementation details)
    attr_accessor :keepalive_interval

    # Allow TLS negotiation? Defaults to true
    attr_accessor :allow_tls

    # Optional CA-Path for TLS-handshake
    attr_accessor :ssl_capath

    # Optional callback for verification of SSL peer
    attr_accessor :ssl_verifycb

    #whether to use the old and deprecated SSL protocol
    #Defaults to false
    attr_accessor :use_ssl

    class SSLSocketUTF8 < OpenSSL::SSL::SSLSocket
      # #readline returns incorrect encodings for UTF8 strings
      # because SSLSocket does not support encoding conversions
      # This forces a correct encoding, and pervents REXML exceptions
      def sysread *args
        super.force_encoding ::Encoding::UTF_8
      end
    end

    ##
    # Create a new connection to the given host and port
    def initialize
      super()
      @host = nil
      @port = nil
      @allow_tls = defined? OpenSSL
      @tls = false
      @ssl_capath = nil
      @ssl_verifycb = nil
      @features_timeout = 10
      @keepalive_interval = 60
      @use_ssl = false
    end

    ##
    # Connect to the Jabber server through a TCP Socket,
    # start the Jabber parser,
    # invoke to accept_features to wait for TLS,
    # start the keep-alive thread
    def connect(host, port)
      @host = host
      @port = port
      # Reset is_tls?, so that it works when reconnecting
      @tls = false

      Jabber::debuglog("CONNECTING:\n#{@host}:#{@port}")
      @socket = TCPSocket.new(@host, @port)

      # We want to use the old and deprecated SSL protocol (usually on port 5223)
      if @use_ssl
        ssl = SSLSocketUTF8.new(@socket)
        ssl.connect # start SSL session
        ssl.sync_close = true
        Jabber::debuglog("SSL connection established.")
        @socket = ssl
      end

      start

      accept_features

      unless @keepalive_interval.nil?
        @keepaliveThread = Thread.new do
          Thread.current.abort_on_exception = true
          keepalive_loop
        end
      end
    end

    ##
    # Closing connection:
    # first kill keepaliveThread (but only if it's not me), then call Stream#close!
    def close!
      @keepaliveThread.kill if @keepaliveThread and @keepaliveThread.alive? and @keepaliveThread != Thread.current
      super
      @keepaliveThread.kill if @keepaliveThread and @keepaliveThread.alive?
    end

    def accept_features
      begin
        Timeout::timeout(@features_timeout) {
          Jabber::debuglog("FEATURES: waiting...")
          @features_sem.wait
          Jabber::debuglog("FEATURES: waiting finished")
        }
      rescue Timeout::Error
        Jabber::debuglog("FEATURES: timed out when waiting, stream peer seems not XMPP compliant")
      end

      if @allow_tls and not is_tls? and @stream_features['starttls'] == 'urn:ietf:params:xml:ns:xmpp-tls'
        begin
          starttls
        rescue
          Jabber::debuglog("STARTTLS:\nFailure: #{$!}")
        end
      end
    end

    ##
    # Start the parser on the previously connected socket
    def start
      super(@socket)
    end

    ##
    # Do a <starttls/>
    # (will be automatically done by connect if stream peer supports this)
    def starttls
      stls = REXML::Element.new('starttls')
      stls.add_namespace('urn:ietf:params:xml:ns:xmpp-tls')

      reply = nil
      send(stls) { |r|
        reply = r
        true
      }
      if reply.name != 'proceed'
        raise ServerError.new(reply.first_element('error'))
      end
      # Don't be interrupted
      stop

      begin
        error = nil

        # Context/user set-able stuff
        ctx = OpenSSL::SSL::SSLContext.new
        if @ssl_capath
          ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
          ctx.ca_path = @ssl_capath
        else
          ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
        end
        ctx.verify_callback = @ssl_verifycb

        # SSL connection establishing
        sslsocket = SSLSocketUTF8.new(@socket, ctx)
        sslsocket.sync_close = true
        Jabber::debuglog("TLSv1: OpenSSL handshake in progress")
        sslsocket.connect

        # Make REXML believe it's a real socket
        class << sslsocket
          def kind_of?(o)
            o == IO ? true : super
          end
        end

        # We're done and will use it
        @tls = true
        @socket = sslsocket
      rescue
        error = $!
      ensure
        Jabber::debuglog("TLSv1: restarting parser")
        start
        accept_features
        raise error if error
      end
    end

    ##
    # Have we gone to TLS mode?
    # result:: [true] or [false]
    def is_tls?
      @tls
    end

    def generate_stream_start(to=nil, from=nil, id=nil, xml_lang="en", xmlns="jabber:client", version="1.0")
      stream_start_string = "<stream:stream xmlns:stream='http://etherx.jabber.org/streams' "
      stream_start_string += "xmlns='#{xmlns}' " unless xmlns.nil?
      stream_start_string += "to='#{to}' " unless to.nil?
      stream_start_string += "from='#{from}' " unless from.nil?
      stream_start_string += "id='#{id}' " unless id.nil?
      stream_start_string += "xml:lang='#{xml_lang}' " unless xml_lang.nil?
      stream_start_string += "version='#{version}' " unless version.nil?
      stream_start_string += ">"
      stream_start_string
    end
    private :generate_stream_start

    ##
    # A loop to send "keep alive" data to prevent
    # the Jabber connection from closing for inactivity.
    #
    # This loop sends a single white-space character if
    # no other data has been sent in the last @keepalive_interval
    # seconds.
    def keepalive_loop
      loop do
        unless is_connected?
          Thread.current.kill
        end
        difference = @last_send + @keepalive_interval - Time.now
        if difference <= 0
          send(' ')
          sleep @keepalive_interval
        else
          sleep(difference)
        end
      end
    end
    private :keepalive_loop

  end
end