xrc 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +34 -0
- data/Rakefile +2 -0
- data/lib/xrc.rb +43 -0
- data/lib/xrc/client.rb +318 -0
- data/lib/xrc/connection.rb +40 -0
- data/lib/xrc/connection/connector.rb +42 -0
- data/lib/xrc/connection/tsl_connector.rb +37 -0
- data/lib/xrc/elements/auth.rb +41 -0
- data/lib/xrc/elements/bind.rb +18 -0
- data/lib/xrc/elements/join.rb +20 -0
- data/lib/xrc/elements/message.rb +20 -0
- data/lib/xrc/elements/ping.rb +21 -0
- data/lib/xrc/elements/presence.rb +9 -0
- data/lib/xrc/elements/roster.rb +13 -0
- data/lib/xrc/elements/session.rb +13 -0
- data/lib/xrc/elements/starttls.rb +10 -0
- data/lib/xrc/elements/stream.rb +22 -0
- data/lib/xrc/hosts_resolver.rb +43 -0
- data/lib/xrc/jid.rb +44 -0
- data/lib/xrc/message_builder.rb +62 -0
- data/lib/xrc/messages/base.rb +37 -0
- data/lib/xrc/messages/message.rb +29 -0
- data/lib/xrc/messages/null.rb +6 -0
- data/lib/xrc/messages/private_message.rb +6 -0
- data/lib/xrc/messages/room_message.rb +6 -0
- data/lib/xrc/messages/subject.rb +29 -0
- data/lib/xrc/method_loggable.rb +22 -0
- data/lib/xrc/namespaces.rb +11 -0
- data/lib/xrc/parser.rb +89 -0
- data/lib/xrc/roster.rb +28 -0
- data/lib/xrc/socket.rb +7 -0
- data/lib/xrc/user.rb +27 -0
- data/lib/xrc/version.rb +3 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/xrc/client_spec.rb +75 -0
- data/spec/xrc/jid_spec.rb +35 -0
- data/xrc.gemspec +24 -0
- metadata +158 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
module Xrc
|
2
|
+
class Connection
|
3
|
+
attr_reader :block, :domain, :port, :socket
|
4
|
+
|
5
|
+
def initialize(options, &block)
|
6
|
+
@domain = options[:domain]
|
7
|
+
@port = options[:port]
|
8
|
+
@block = block
|
9
|
+
end
|
10
|
+
|
11
|
+
def connect
|
12
|
+
@socket = Connector.connect(domain: domain, port: port)
|
13
|
+
start
|
14
|
+
end
|
15
|
+
|
16
|
+
def encrypt
|
17
|
+
@socket = TslConnector.connect(socket: socket)
|
18
|
+
start
|
19
|
+
end
|
20
|
+
|
21
|
+
def open
|
22
|
+
write Elements::Stream.new(domain: domain)
|
23
|
+
end
|
24
|
+
|
25
|
+
def write(object)
|
26
|
+
socket << object.to_s
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def start
|
32
|
+
open
|
33
|
+
parse
|
34
|
+
end
|
35
|
+
|
36
|
+
def parse
|
37
|
+
Parser.new(socket: socket, &block).parse
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Xrc
|
2
|
+
class Connection
|
3
|
+
class Connector
|
4
|
+
def self.connect(*args)
|
5
|
+
new(*args).connect
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :options
|
9
|
+
|
10
|
+
def initialize(options)
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
def connect
|
15
|
+
hosts.find do |host|
|
16
|
+
if socket = connect_to(host)
|
17
|
+
break socket
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def domain
|
25
|
+
options[:domain]
|
26
|
+
end
|
27
|
+
|
28
|
+
def port
|
29
|
+
options[:port]
|
30
|
+
end
|
31
|
+
|
32
|
+
def hosts
|
33
|
+
HostsResolver.call(domain)
|
34
|
+
end
|
35
|
+
|
36
|
+
def connect_to(host)
|
37
|
+
TCPSocket.new(host, port)
|
38
|
+
rescue SocketError, Errno::ECONNREFUSED => exception
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Xrc
|
2
|
+
class Connection
|
3
|
+
class TslConnector
|
4
|
+
def self.connect(*args)
|
5
|
+
new(*args).connect
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :options
|
9
|
+
|
10
|
+
def initialize(options = {})
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
def connect
|
15
|
+
ssl_socket.connect
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def ssl_socket
|
21
|
+
ssl_socket = Socket.new(socket, context)
|
22
|
+
ssl_socket.sync_close = true
|
23
|
+
ssl_socket
|
24
|
+
end
|
25
|
+
|
26
|
+
def socket
|
27
|
+
options[:socket]
|
28
|
+
end
|
29
|
+
|
30
|
+
def context
|
31
|
+
context = OpenSSL::SSL::SSLContext.new
|
32
|
+
context.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
33
|
+
context
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Xrc
|
2
|
+
module Elements
|
3
|
+
class Auth < REXML::Element
|
4
|
+
attr_reader :options
|
5
|
+
|
6
|
+
def initialize(options = {})
|
7
|
+
super("auth")
|
8
|
+
@options = options
|
9
|
+
add_sasl_namespace
|
10
|
+
add_plain_mechanism_attribute
|
11
|
+
add_credentials_text
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def jid
|
17
|
+
options[:jid]
|
18
|
+
end
|
19
|
+
|
20
|
+
def password
|
21
|
+
options[:password]
|
22
|
+
end
|
23
|
+
|
24
|
+
def plain_credentials
|
25
|
+
"#{jid.strip}\x00#{jid.node}\x00#{password}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_sasl_namespace
|
29
|
+
add_namespace(Namespaces::SASL)
|
30
|
+
end
|
31
|
+
|
32
|
+
def add_plain_mechanism_attribute
|
33
|
+
attributes["mechanism"] = "PLAIN"
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_credentials_text
|
37
|
+
self.text = Base64.strict_encode64(plain_credentials)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Xrc
|
2
|
+
module Elements
|
3
|
+
class Bind < REXML::Element
|
4
|
+
def initialize(options = {})
|
5
|
+
super("iq")
|
6
|
+
bind = REXML::Element.new("bind")
|
7
|
+
bind.add_namespace(Namespaces::BIND)
|
8
|
+
if options[:resource]
|
9
|
+
resource = REXML::Element.new("resource")
|
10
|
+
resource.text = options[:resource]
|
11
|
+
bind.add(resource)
|
12
|
+
end
|
13
|
+
add_attributes("type" => "set")
|
14
|
+
add(bind)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Xrc
|
2
|
+
module Elements
|
3
|
+
class Join < Presence
|
4
|
+
def initialize(options)
|
5
|
+
super()
|
6
|
+
attributes["from"] = options[:from]
|
7
|
+
attributes["to"] = options[:to]
|
8
|
+
add(x)
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def x
|
14
|
+
element = REXML::Element.new("x")
|
15
|
+
element.add_namespace(Namespaces::MUC)
|
16
|
+
element
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Xrc
|
2
|
+
module Elements
|
3
|
+
class Message < REXML::Element
|
4
|
+
def initialize(options = {})
|
5
|
+
super("message")
|
6
|
+
attributes["from"] = options[:from]
|
7
|
+
attributes["to"] = options[:to]
|
8
|
+
attributes["type"] = options[:type]
|
9
|
+
body.text = options[:body]
|
10
|
+
add(body)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def body
|
16
|
+
@body ||= REXML::Element.new("body")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Xrc
|
2
|
+
module Elements
|
3
|
+
class Ping < REXML::Element
|
4
|
+
def initialize(options = {})
|
5
|
+
super("iq")
|
6
|
+
attributes["from"] = options[:from]
|
7
|
+
attributes["to"] = options[:to]
|
8
|
+
attributes["type"] = "get"
|
9
|
+
add(ping)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def ping
|
15
|
+
element = REXML::Element.new("ping")
|
16
|
+
element.add_namespace(Namespaces::PING)
|
17
|
+
element
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Xrc
|
2
|
+
module Elements
|
3
|
+
class Session < REXML::Element
|
4
|
+
def initialize
|
5
|
+
super("iq")
|
6
|
+
session = REXML::Element.new("session")
|
7
|
+
session.add_namespace(Namespaces::SESSION)
|
8
|
+
add_attributes("type" => "set")
|
9
|
+
add(session)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Xrc
|
2
|
+
module Elements
|
3
|
+
class Stream
|
4
|
+
attr_reader :options
|
5
|
+
|
6
|
+
def initialize(options)
|
7
|
+
@options = options
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_s
|
11
|
+
%W[
|
12
|
+
<stream:stream
|
13
|
+
xmlns:stream="http://etherx.jabber.org/streams"
|
14
|
+
xmlns="jabber:client"
|
15
|
+
to="#{options[:domain]}"
|
16
|
+
xml:lang="en"
|
17
|
+
version="1.0">
|
18
|
+
].join(" ")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Xrc
|
2
|
+
class HostsResolver
|
3
|
+
def self.call(*args)
|
4
|
+
new(*args).call
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_reader :domain
|
8
|
+
|
9
|
+
def initialize(domain)
|
10
|
+
@domain = domain
|
11
|
+
end
|
12
|
+
|
13
|
+
def call
|
14
|
+
resolved_hosts + [domain]
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def resolved_hosts
|
20
|
+
sorted_srvs.lazy.map(&:target).map(&:to_s).to_a
|
21
|
+
end
|
22
|
+
|
23
|
+
def sorted_srvs
|
24
|
+
@sorted_srvs ||= srvs.sort_by do |resource|
|
25
|
+
[resource.priority, -resource.weight]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def srvs
|
30
|
+
Resolv::DNS.open do |dns|
|
31
|
+
dns.getresources(name, type_class)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def name
|
36
|
+
"_xmpp-client._tcp.#{domain}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def type_class
|
40
|
+
Resolv::DNS::Resource::IN::SRV
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/xrc/jid.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
module Xrc
|
2
|
+
class Jid
|
3
|
+
PATTERN = /\A(?:([^@]*)@)??([^@\/]*)(?:\/(.*?))?\z/
|
4
|
+
|
5
|
+
attr_reader :raw
|
6
|
+
|
7
|
+
def initialize(raw)
|
8
|
+
@raw = raw
|
9
|
+
end
|
10
|
+
|
11
|
+
def node
|
12
|
+
sections[0]
|
13
|
+
end
|
14
|
+
|
15
|
+
def domain
|
16
|
+
sections[1]
|
17
|
+
end
|
18
|
+
|
19
|
+
def resource
|
20
|
+
sections[2]
|
21
|
+
end
|
22
|
+
|
23
|
+
def strip
|
24
|
+
"#{node}@#{domain}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
str = strip
|
29
|
+
str << "/#{resource}" if resource
|
30
|
+
str
|
31
|
+
end
|
32
|
+
|
33
|
+
def ==(jid)
|
34
|
+
jid = Jid.new(jid) unless jid.is_a?(Jid)
|
35
|
+
to_s == jid.to_s || strip == jid.strip
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def sections
|
41
|
+
@sections ||= raw.scan(PATTERN).first
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Xrc
|
2
|
+
class MessageBuilder
|
3
|
+
# Builds a Message object from a REXML::Element.
|
4
|
+
# @param [REXML::Element] element A message element
|
5
|
+
# @return [Xrc::Messages::Base] An ancestor of Xrc::Messages::Base instance
|
6
|
+
def self.build(element)
|
7
|
+
new(element).build
|
8
|
+
end
|
9
|
+
|
10
|
+
# @param [REXML::Element] element A message element
|
11
|
+
def initialize(element)
|
12
|
+
@element = element
|
13
|
+
end
|
14
|
+
|
15
|
+
def build
|
16
|
+
message_class.new(@element)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def message_class
|
22
|
+
case
|
23
|
+
when has_room_message?
|
24
|
+
Messages::RoomMessage
|
25
|
+
when has_private_message?
|
26
|
+
Messages::PrivateMessage
|
27
|
+
when has_subject?
|
28
|
+
Messages::Subject
|
29
|
+
else
|
30
|
+
Messages::Null
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def has_room_message?
|
35
|
+
has_groupchat_type? && has_body?
|
36
|
+
end
|
37
|
+
|
38
|
+
def has_private_message?
|
39
|
+
has_chat_type? && has_body?
|
40
|
+
end
|
41
|
+
|
42
|
+
def has_chat_type?
|
43
|
+
type == "chat"
|
44
|
+
end
|
45
|
+
|
46
|
+
def has_groupchat_type?
|
47
|
+
type == "groupchat"
|
48
|
+
end
|
49
|
+
|
50
|
+
def has_body?
|
51
|
+
!!@element.elements["body"]
|
52
|
+
end
|
53
|
+
|
54
|
+
def has_subject?
|
55
|
+
!!@element.elements["subject"]
|
56
|
+
end
|
57
|
+
|
58
|
+
def type
|
59
|
+
@element.attributes["type"].to_s
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|