xrc 0.0.1
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/.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
|