vines-services 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|