vines-services 0.1.0
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.
- data/LICENSE +19 -0
- data/README +40 -0
- data/Rakefile +130 -0
- data/bin/vines-services +95 -0
- data/conf/config.rb +25 -0
- data/lib/vines/services/command/init.rb +209 -0
- data/lib/vines/services/command/restart.rb +14 -0
- data/lib/vines/services/command/start.rb +30 -0
- data/lib/vines/services/command/stop.rb +20 -0
- data/lib/vines/services/command/views.rb +26 -0
- data/lib/vines/services/component.rb +26 -0
- data/lib/vines/services/config.rb +105 -0
- data/lib/vines/services/connection.rb +120 -0
- data/lib/vines/services/controller/attributes_controller.rb +19 -0
- data/lib/vines/services/controller/base_controller.rb +99 -0
- data/lib/vines/services/controller/disco_info_controller.rb +61 -0
- data/lib/vines/services/controller/labels_controller.rb +17 -0
- data/lib/vines/services/controller/members_controller.rb +44 -0
- data/lib/vines/services/controller/messages_controller.rb +66 -0
- data/lib/vines/services/controller/probes_controller.rb +45 -0
- data/lib/vines/services/controller/services_controller.rb +81 -0
- data/lib/vines/services/controller/subscriptions_controller.rb +39 -0
- data/lib/vines/services/controller/systems_controller.rb +45 -0
- data/lib/vines/services/controller/transfers_controller.rb +58 -0
- data/lib/vines/services/controller/uploads_controller.rb +62 -0
- data/lib/vines/services/controller/users_controller.rb +127 -0
- data/lib/vines/services/core_ext/blather.rb +46 -0
- data/lib/vines/services/core_ext/couchrest.rb +33 -0
- data/lib/vines/services/indexer.rb +195 -0
- data/lib/vines/services/priority_queue.rb +94 -0
- data/lib/vines/services/roster.rb +70 -0
- data/lib/vines/services/storage/couchdb/fragment.rb +23 -0
- data/lib/vines/services/storage/couchdb/service.rb +170 -0
- data/lib/vines/services/storage/couchdb/system.rb +141 -0
- data/lib/vines/services/storage/couchdb/upload.rb +66 -0
- data/lib/vines/services/storage/couchdb/user.rb +137 -0
- data/lib/vines/services/storage/couchdb/vcard.rb +13 -0
- data/lib/vines/services/storage/couchdb.rb +157 -0
- data/lib/vines/services/storage.rb +33 -0
- data/lib/vines/services/throttle.rb +26 -0
- data/lib/vines/services/version.rb +7 -0
- data/lib/vines/services/vql/compiler.rb +94 -0
- data/lib/vines/services/vql/vql.citrus +115 -0
- data/lib/vines/services/vql/vql.rb +186 -0
- data/lib/vines/services.rb +71 -0
- data/test/config_test.rb +242 -0
- data/test/priority_queue_test.rb +23 -0
- data/test/storage/couchdb_test.rb +30 -0
- data/test/vql/compiler_test.rb +96 -0
- data/test/vql/vql_test.rb +233 -0
- data/web/coffeescripts/api.coffee +51 -0
- data/web/coffeescripts/commands.coffee +18 -0
- data/web/coffeescripts/files.coffee +315 -0
- data/web/coffeescripts/init.coffee +21 -0
- data/web/coffeescripts/services.coffee +356 -0
- data/web/coffeescripts/setup.coffee +503 -0
- data/web/coffeescripts/systems.coffee +371 -0
- data/web/images/default-service.png +0 -0
- data/web/images/linux.png +0 -0
- data/web/images/mac.png +0 -0
- data/web/images/run.png +0 -0
- data/web/images/windows.png +0 -0
- data/web/index.html +17 -0
- data/web/stylesheets/common.css +52 -0
- data/web/stylesheets/files.css +218 -0
- data/web/stylesheets/services.css +181 -0
- data/web/stylesheets/setup.css +117 -0
- data/web/stylesheets/systems.css +142 -0
- metadata +230 -0
@@ -0,0 +1,105 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
module Services
|
5
|
+
# A Config object is passed to the xmpp connections to give them access
|
6
|
+
# to server configuration information like component subdomain host names,
|
7
|
+
# storage systems, etc. This class provides the DSL methods used in the
|
8
|
+
# conf/config.rb file.
|
9
|
+
class Config
|
10
|
+
LOG_LEVELS = %w[debug info warn error fatal].freeze
|
11
|
+
|
12
|
+
attr_reader :vhosts
|
13
|
+
|
14
|
+
@@instance = nil
|
15
|
+
def self.configure(&block)
|
16
|
+
@@instance = self.new(&block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.instance
|
20
|
+
@@instance
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(&block)
|
24
|
+
@vhosts = {}
|
25
|
+
instance_eval(&block)
|
26
|
+
raise "must define at least one virtual host" if @vhosts.empty?
|
27
|
+
end
|
28
|
+
|
29
|
+
def host(*names, &block)
|
30
|
+
names = names.flatten.map {|name| name.downcase }
|
31
|
+
dupes = names.uniq.size != names.size || (@vhosts.keys & names).any?
|
32
|
+
raise "one host definition per domain allowed" if dupes
|
33
|
+
names.each do |name|
|
34
|
+
@vhosts[name] = Host.new(name, &block)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def log(level)
|
39
|
+
const = Logger.const_get(level.to_s.upcase) rescue nil
|
40
|
+
unless LOG_LEVELS.include?(level.to_s) && const
|
41
|
+
raise "log level must be one of: #{LOG_LEVELS.join(', ')}"
|
42
|
+
end
|
43
|
+
log = Class.new.extend(Vines::Log).log
|
44
|
+
log.progname = 'vines-service'
|
45
|
+
log.level = const
|
46
|
+
Blather.logger.level = const
|
47
|
+
end
|
48
|
+
|
49
|
+
def hosts
|
50
|
+
@vhosts.values
|
51
|
+
end
|
52
|
+
|
53
|
+
class Host
|
54
|
+
attr_reader :name
|
55
|
+
|
56
|
+
def initialize(name, &block)
|
57
|
+
@name, @storage, @uploads, @upstream = name, nil, nil, []
|
58
|
+
instance_eval(&block)
|
59
|
+
raise "storage required for #{@name}" unless @storage
|
60
|
+
raise "upstream connection required for #{@name}" if @upstream.empty?
|
61
|
+
unless @uploads
|
62
|
+
@uploads = File.expand_path('data/upload')
|
63
|
+
FileUtils.mkdir_p(@uploads)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def uploads(dir=nil)
|
68
|
+
return @uploads unless dir
|
69
|
+
@uploads = File.expand_path(dir)
|
70
|
+
begin
|
71
|
+
FileUtils.mkdir_p(@uploads)
|
72
|
+
rescue
|
73
|
+
raise "can't create #{@uploads}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def storage(name=nil, &block)
|
78
|
+
if name
|
79
|
+
raise "one storage mechanism per host allowed" if @storage
|
80
|
+
@storage = Storage.from_name(name, &block)
|
81
|
+
else
|
82
|
+
@storage
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def upstream(host, port, password)
|
87
|
+
raise 'host, port, and password required for upstream connections' unless
|
88
|
+
host && port && password
|
89
|
+
@upstream << {host: host, port: port, password: password}
|
90
|
+
end
|
91
|
+
|
92
|
+
def start
|
93
|
+
@upstream.each do |info|
|
94
|
+
stream = Vines::Services::Connection.new(
|
95
|
+
host: info[:host],
|
96
|
+
port: info[:port],
|
97
|
+
password: info[:password],
|
98
|
+
vhost: self)
|
99
|
+
stream.start
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
module Services
|
5
|
+
# Connects the component process to the chat server and provides the protocol
|
6
|
+
# used by the web user interface and the agents.
|
7
|
+
class Connection
|
8
|
+
include Vines::Log
|
9
|
+
|
10
|
+
@@controllers = []
|
11
|
+
def self.register(*args, klass)
|
12
|
+
@@controllers << [args, klass]
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(options)
|
16
|
+
host, port, password, @vhost = *options.values_at(:host, :port, :password, :vhost)
|
17
|
+
@stream = Blather::Client.setup(@vhost.name, password, host, port)
|
18
|
+
@throttle = Throttle.new(@stream)
|
19
|
+
@queues = {}
|
20
|
+
|
21
|
+
@stream.register_handler(:disconnected) do
|
22
|
+
log.info("Stream disconnected, reconnecting . . .")
|
23
|
+
EM::Timer.new(10) do
|
24
|
+
self.class.new(options).start
|
25
|
+
end
|
26
|
+
true # prevent EM.stop
|
27
|
+
end
|
28
|
+
|
29
|
+
@stream.register_handler(:ready) do
|
30
|
+
log.info("Connected #{@stream.jid} component to #{host}:#{port}")
|
31
|
+
Fiber.new do
|
32
|
+
broadcast_presence
|
33
|
+
end.resume
|
34
|
+
end
|
35
|
+
|
36
|
+
@stream.register_handler(:iq, '/iq[@type="get"]/ns:query', :ns => 'jabber:iq:version') do |node|
|
37
|
+
if node.to == @stream.jid
|
38
|
+
iq = Blather::Stanza::Iq::Query.new(:result)
|
39
|
+
iq.id, iq.to = node.id, node.from
|
40
|
+
iq.query.add_child("<name>Vines Services</name>")
|
41
|
+
iq.query.add_child("<version>#{VERSION}</version>")
|
42
|
+
@stream.write(iq)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
@@controllers.each do |args, klass|
|
47
|
+
@stream.register_handler(*args) do |node|
|
48
|
+
jid = node.from.to_s
|
49
|
+
start = !@queues.key?(jid)
|
50
|
+
queue = (@queues[jid] ||= EM::Queue.new)
|
51
|
+
queue.push([node, klass])
|
52
|
+
process_node_queue(jid) if start
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def start
|
58
|
+
@stream.run
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# Send initial presence from each service JID to its users when the
|
64
|
+
# component starts.
|
65
|
+
def broadcast_presence
|
66
|
+
services = CouchModels::Service.find_all
|
67
|
+
users = services.map {|service| service.users }.flatten.uniq
|
68
|
+
nodes = users.map {|jid| available(@stream.jid, jid) }
|
69
|
+
nodes << services.map do |service|
|
70
|
+
service.users.map {|jid| available(service.jid, jid) }
|
71
|
+
end
|
72
|
+
nodes.flatten!
|
73
|
+
@throttle.async_send(nodes)
|
74
|
+
end
|
75
|
+
|
76
|
+
def available(from, to)
|
77
|
+
Blather::Stanza::Presence::Status.new.tap do |node|
|
78
|
+
node.from = from
|
79
|
+
node.to = to
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# We must process or deliver stanzas in the order they are received, so
|
84
|
+
# we create a node queue for each sending JID and process it in this loop.
|
85
|
+
#
|
86
|
+
# The process loop continues until the queue is empty for this JID, then
|
87
|
+
# the queue is deleted. JID's come and go frequently, without notifying the
|
88
|
+
# component, so we must delete their queue as soon as it's empty to avoid
|
89
|
+
# maintaining a growing list of queues for JID's we will never see again.
|
90
|
+
#
|
91
|
+
# Each node is wrapped its own Fiber so it can be paused and resumed
|
92
|
+
# during asynchronous IO.
|
93
|
+
def process_node_queue(jid)
|
94
|
+
if @queues[jid].empty?
|
95
|
+
@queues.delete(jid)
|
96
|
+
else
|
97
|
+
@queues[jid].pop do |pair|
|
98
|
+
Fiber.new do
|
99
|
+
process_node(*pair)
|
100
|
+
process_node_queue(jid)
|
101
|
+
end.resume
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Create a new controller instance to process the node.
|
107
|
+
def process_node(node, klass)
|
108
|
+
begin
|
109
|
+
klass.new(node, @stream, @vhost).process
|
110
|
+
rescue Forbidden
|
111
|
+
log.warn("Authorization failed for #{node.from}:\n#{node}")
|
112
|
+
@stream.write(Blather::StanzaError.new(node, 'forbidden', :auth).to_node)
|
113
|
+
rescue Exception => e
|
114
|
+
log.error("Error processing node: #{e.message}")
|
115
|
+
@stream.write(Blather::StanzaError.new(node, 'internal-server-error', :cancel).to_node)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
module Services
|
5
|
+
module Controller
|
6
|
+
class AttributesController < BaseController
|
7
|
+
register :iq, "/iq[@type='get' or @type='set']/ns:query",
|
8
|
+
'ns' => 'http://getvines.com/protocol/systems/attributes'
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def get
|
13
|
+
forbidden! unless current_user.manage_services?
|
14
|
+
send_result(rows: System.find_attributes)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
module Services
|
5
|
+
module Controller
|
6
|
+
class BaseController
|
7
|
+
include Vines::Log
|
8
|
+
include CouchModels
|
9
|
+
include Nokogiri::XML
|
10
|
+
|
11
|
+
def self.register(*args)
|
12
|
+
Connection.register(*args, self)
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :storage, :node, :stream, :uploads
|
16
|
+
|
17
|
+
def initialize(node, stream, vhost)
|
18
|
+
@node, @stream, @storage = node, stream, vhost.storage
|
19
|
+
@uploads = vhost.uploads
|
20
|
+
@current_user = nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def process
|
24
|
+
# must be addressed to component, not a service
|
25
|
+
return unless to_component?
|
26
|
+
if node.get?
|
27
|
+
get
|
28
|
+
elsif node.set? && node.elements.first['action'] == 'delete'
|
29
|
+
delete
|
30
|
+
elsif node.set?
|
31
|
+
save
|
32
|
+
end
|
33
|
+
rescue Forbidden
|
34
|
+
raise
|
35
|
+
rescue
|
36
|
+
send_error('not-acceptable')
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def get
|
42
|
+
send_error('feature-not-implemented')
|
43
|
+
end
|
44
|
+
|
45
|
+
def save
|
46
|
+
send_error('feature-not-implemented')
|
47
|
+
end
|
48
|
+
|
49
|
+
def delete
|
50
|
+
send_error('feature-not-implemented')
|
51
|
+
end
|
52
|
+
|
53
|
+
def send_result(obj=nil)
|
54
|
+
iq = Blather::Stanza::Iq::Query.new(:result).tap do |result|
|
55
|
+
result.id, result.to, result.from = node.id, node.from, node.to
|
56
|
+
result.query.content = obj.to_json if obj
|
57
|
+
result.query.namespace = node.elements.first.namespace.href
|
58
|
+
end
|
59
|
+
stream.write(iq)
|
60
|
+
end
|
61
|
+
|
62
|
+
def send_error(condition, obj=nil)
|
63
|
+
err = Blather::StanzaError.new(node, condition, :modify).tap do |error|
|
64
|
+
error.extras << Blather::XMPPNode.new('vines-error').tap do |verr|
|
65
|
+
verr.namespace = 'http://getvines.com/error'
|
66
|
+
verr.content = obj.to_json
|
67
|
+
end if obj
|
68
|
+
end
|
69
|
+
stream.write(err.to_node)
|
70
|
+
end
|
71
|
+
|
72
|
+
def send_doc(doc)
|
73
|
+
if doc
|
74
|
+
send_result(doc.to_result)
|
75
|
+
else
|
76
|
+
send_error('item-not-found')
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Return true if the stanza is addressed to the component's JID rather
|
81
|
+
# than to a service JID.
|
82
|
+
def to_component?
|
83
|
+
node.to == stream.jid
|
84
|
+
end
|
85
|
+
|
86
|
+
def forbidden!
|
87
|
+
raise Forbidden
|
88
|
+
end
|
89
|
+
|
90
|
+
# Return the User object for the user that sent this stanza. Useful for
|
91
|
+
# permission authorization checks before performing server actions.
|
92
|
+
def current_user
|
93
|
+
jid = node.from.stripped.to_s.downcase
|
94
|
+
@current_user ||= User.find_by_jid(jid)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
module Services
|
5
|
+
module Controller
|
6
|
+
class DiscoInfoController < BaseController
|
7
|
+
register :disco_info, :get?
|
8
|
+
|
9
|
+
def process
|
10
|
+
reply = (node.to == stream.jid) ? component : service
|
11
|
+
stream.write(reply) if reply
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
# Return the discovery reply node for a query addressed to the
|
17
|
+
# component JID itself (e.g. vines.wonderland.lit), rather than to a
|
18
|
+
# service JID. Advertise the http://getvines.com/protocol feature
|
19
|
+
# so agents can discover the component JID with which to communicate.
|
20
|
+
def component
|
21
|
+
disco_node.tap do |disco|
|
22
|
+
disco.identities = {
|
23
|
+
name: 'Vines Services',
|
24
|
+
type: 'bot',
|
25
|
+
category: 'component'
|
26
|
+
}
|
27
|
+
disco.features = %w[
|
28
|
+
http://jabber.org/protocol/bytestreams
|
29
|
+
http://jabber.org/protocol/disco#info
|
30
|
+
http://jabber.org/protocol/si
|
31
|
+
http://jabber.org/protocol/si/profile/file-transfer
|
32
|
+
http://jabber.org/protocol/xhtml-im
|
33
|
+
http://getvines.com/protocol
|
34
|
+
jabber:iq:version
|
35
|
+
]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Return the discovery reply node for a query addressed to a service
|
40
|
+
# JID (e.g. web_servers@vines.wonderland.lit). Ignore, rather than send
|
41
|
+
# an error, for queries to service JID's that don't exist or to which
|
42
|
+
# the user doesn't have access.
|
43
|
+
def service
|
44
|
+
found = Service.find_by_jid(node.to.to_s)
|
45
|
+
return unless found && found.users.include?(node.from.stripped.to_s)
|
46
|
+
disco_node.tap do |disco|
|
47
|
+
disco.features = %w[http://jabber.org/protocol/xhtml-im]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def disco_node
|
52
|
+
Blather::Stanza::DiscoInfo.new(:result).tap do |disco|
|
53
|
+
disco.id = node.id
|
54
|
+
disco.to = node.from
|
55
|
+
disco.from = node.to
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
module Services
|
5
|
+
module Controller
|
6
|
+
class LabelsController < BaseController
|
7
|
+
register :iq, "/iq[@type='get' or @type='set']/ns:query",
|
8
|
+
'ns' => 'http://getvines.com/protocol/files/labels'
|
9
|
+
|
10
|
+
def get
|
11
|
+
forbidden! unless current_user.manage_files?
|
12
|
+
send_result(rows: Upload.find_labels)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
module Services
|
5
|
+
module Controller
|
6
|
+
class MembersController < BaseController
|
7
|
+
register :iq, "/iq[@type='get' or @type='set']/ns:query",
|
8
|
+
'ns' => 'http://getvines.com/protocol/services/members'
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def get
|
13
|
+
forbidden! unless current_user.manage_services?
|
14
|
+
if id = node.elements.first['id']
|
15
|
+
rows = Service.find(id).members rescue []
|
16
|
+
send_result(rows: rows)
|
17
|
+
else
|
18
|
+
find_members_by_vql
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Compile the VQL syntax into a SQL query to find matching member
|
23
|
+
# systems. Return a list of members or the parser error so the user
|
24
|
+
# can debug their query syntax.
|
25
|
+
def find_members_by_vql
|
26
|
+
begin
|
27
|
+
code = node.elements.first.content
|
28
|
+
sql, params = VQL::Compiler.new.to_sql(code)
|
29
|
+
query(sql, params)
|
30
|
+
rescue Exception => e
|
31
|
+
send_result(ok: false, error: e.message)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def query(sql, params)
|
36
|
+
storage.query(sql, params) do |rows|
|
37
|
+
members = rows.map {|row| {name: row[0], os: row[1]} }
|
38
|
+
send_result(ok: true, rows: members)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
module Services
|
5
|
+
module Controller
|
6
|
+
# Broadcast messages from a user to a group of systems (a.k.a. a service).
|
7
|
+
# Responses from the agents are routed back through the component so the
|
8
|
+
# user appears to be talking to just the service's JID, not each and every
|
9
|
+
# agent that belongs to the service.
|
10
|
+
class MessagesController < BaseController
|
11
|
+
register :message, :chat?, :body
|
12
|
+
|
13
|
+
NS = 'http://getvines.com/protocol'.freeze
|
14
|
+
|
15
|
+
def process
|
16
|
+
current_user.system? ? forward_to_user : forward_to_service
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# Forward the agent's response message to the user that sent it the
|
22
|
+
# command.
|
23
|
+
def forward_to_user
|
24
|
+
jid = node.xpath('/message/ns:jid', 'ns' => NS).first
|
25
|
+
return unless jid
|
26
|
+
jid.remove
|
27
|
+
node.from = node.to
|
28
|
+
node.to = jid.content
|
29
|
+
stream.write(node)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Forward the user's message to the members of the service. Tag the
|
33
|
+
# message with a jid element, identifying the user executing the command
|
34
|
+
# like: <jid xmlns="http://getvines.com/protocol">alice@wonderland.lit/tea</jid>
|
35
|
+
# When the agent receives a message from one of its services, it checks
|
36
|
+
# this jid element for the user permissions with which to run the command.
|
37
|
+
#
|
38
|
+
# Ignore the message, rather than send an error, if the user lacks
|
39
|
+
# privilege to access the service to avoid directory harvesting.
|
40
|
+
def forward_to_service
|
41
|
+
service = Service.find_by_jid(node.to.stripped)
|
42
|
+
if service && service.user?(node.from.stripped)
|
43
|
+
service.members.each do |member|
|
44
|
+
to = Blather::JID.new(member['name'], node.from.domain)
|
45
|
+
stream.write(create_message(to))
|
46
|
+
end
|
47
|
+
else
|
48
|
+
log.warn("#{node.from} denied access to #{node.to}")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Copy the message's content into a new message destined for the given
|
53
|
+
# agent JID. Identify this command/response exchange with the message's
|
54
|
+
# thread value. Tag the message with a jid element, identifying the
|
55
|
+
# original user that sent the command.
|
56
|
+
def create_message(to)
|
57
|
+
Blather::Stanza::Message.new(to, node.body).tap do |msg|
|
58
|
+
msg.thread = node.thread || Kit.uuid
|
59
|
+
msg.from = node.to
|
60
|
+
msg << msg.document.create_element('jid', node.from, xmlns: NS)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
module Services
|
5
|
+
module Controller
|
6
|
+
# Reply to presence probes if the user has privilege to access the
|
7
|
+
# requested service JID. Reply to all probes to the component itself.
|
8
|
+
class ProbesController < BaseController
|
9
|
+
register :presence, :probe?
|
10
|
+
|
11
|
+
def process
|
12
|
+
from, to = node.from.stripped, node.to.stripped
|
13
|
+
if approved?
|
14
|
+
stream.write(available(to, from))
|
15
|
+
else
|
16
|
+
stream.write(unsubscribed(to, from))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def approved?
|
23
|
+
return true if to_component?
|
24
|
+
found = Service.find_by_jid(node.to)
|
25
|
+
found && found.user?(node.from.stripped)
|
26
|
+
end
|
27
|
+
|
28
|
+
def available(from, to)
|
29
|
+
Blather::Stanza::Presence::Status.new.tap do |stanza|
|
30
|
+
stanza.from = from
|
31
|
+
stanza.to = to
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def unsubscribed(from, to)
|
36
|
+
Blather::Stanza::Presence::Subscription.new.tap do |stanza|
|
37
|
+
stanza.type = :unsubscribed
|
38
|
+
stanza.from = from
|
39
|
+
stanza.to = to
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
module Services
|
5
|
+
module Controller
|
6
|
+
class ServicesController < BaseController
|
7
|
+
register :iq, "/iq[@type='get' or @type='set']/ns:query",
|
8
|
+
'ns' => 'http://getvines.com/protocol/services'
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def get
|
13
|
+
forbidden! unless current_user.manage_services?
|
14
|
+
|
15
|
+
if id = node.elements.first['id']
|
16
|
+
send_doc(Service.find(id))
|
17
|
+
elsif name = node.elements.first['name']
|
18
|
+
send_doc(Service.find_by_name(name))
|
19
|
+
else
|
20
|
+
rows = Service.find_all.map do |doc|
|
21
|
+
{id: doc.id, name: doc.name, size: doc.size}
|
22
|
+
end
|
23
|
+
send_result(rows: rows)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def save
|
28
|
+
forbidden! unless current_user.manage_services?
|
29
|
+
|
30
|
+
obj = JSON.parse(node.elements.first.content)
|
31
|
+
raise 'name required' unless obj['name']
|
32
|
+
obj['jid'] = to_jid(obj['name'])
|
33
|
+
obj['users'] = validate_users(obj['users'])
|
34
|
+
|
35
|
+
begin
|
36
|
+
compiled = VQL::Compiler.new.to_js(obj['code'])
|
37
|
+
rescue Exception => e
|
38
|
+
send_error('not-acceptable', {error: e.message})
|
39
|
+
return
|
40
|
+
end
|
41
|
+
|
42
|
+
service = Service.find(obj['id']) || Service.new
|
43
|
+
members = service.members
|
44
|
+
if service.update_attributes(obj)
|
45
|
+
send_result(service.to_result)
|
46
|
+
members << Service.find(service.id).members
|
47
|
+
System.notify_members(stream, node.from, members.flatten.uniq)
|
48
|
+
else
|
49
|
+
send_error('not-acceptable')
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Ensure the JID's that are given access to this service actually exist
|
54
|
+
# and are not system accounts. System users may never have access to
|
55
|
+
# services. Return the list of JID's that pass validation.
|
56
|
+
def validate_users(jids)
|
57
|
+
return [] unless jids
|
58
|
+
users = User.by_jid.keys(jids)
|
59
|
+
users.map {|u| u.system? ? nil : u.jid }.compact
|
60
|
+
end
|
61
|
+
|
62
|
+
# Create a JID for the service from its given name so we can address
|
63
|
+
# stanzas to the service.
|
64
|
+
def to_jid(name)
|
65
|
+
Blather::JID.new(CGI.escape(name), stream.jid.domain).to_s.downcase
|
66
|
+
end
|
67
|
+
|
68
|
+
def delete
|
69
|
+
forbidden! unless current_user.manage_services?
|
70
|
+
|
71
|
+
if service = Service.find(node.elements.first['id'])
|
72
|
+
service.destroy
|
73
|
+
send_result
|
74
|
+
else
|
75
|
+
send_error('item-not-found')
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|