1#!/usr/bin/env ruby
2#
3# Copyright (C) 2017 Denver Gingerich <denver@ossguy.com>
4#
5# This file is part of sgx-catapult.
6#
7# sgx-catapult is free software: you can redistribute it and/or modify it under
8# the terms of the GNU Affero General Public License as published by the Free
9# Software Foundation, either version 3 of the License, or (at your option) any
10# later version.
11#
12# sgx-catapult is distributed in the hope that it will be useful, but WITHOUT
13# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
14# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
15# details.
16#
17# You should have received a copy of the GNU Affero General Public License along
18# with sgx-catapult. If not, see <http://www.gnu.org/licenses/>.
19
20require 'blather/client/dsl'
21require 'json'
22require 'net/http'
23require 'redis/connection/hiredis'
24require 'uri'
25
26if ARGV.size != 6 then
27 puts "Usage: sgx-catapult.rb <component_jid> <component_password> " +
28 "<server_hostname> <server_port> <redis_hostname> <redis_port>"
29 exit 0
30end
31
32module SGXcatapult
33 extend Blather::DSL
34
35 def self.run
36 client.run
37 end
38
39 def self.error_msg(orig, query_node, type, name, text = nil)
40 orig.add_child(query_node)
41 orig.type = :error
42
43 error = Nokogiri::XML::Node.new 'error', orig.document
44 error['type'] = type
45 orig.add_child(error)
46
47 suberr = Nokogiri::XML::Node.new name, orig.document
48 suberr['xmlns'] = 'urn:ietf:params:xml:ns:xmpp-stanzas'
49 error.add_child(suberr)
50
51 # TODO: add some explanatory xml:lang='en' text (see text param)
52 puts "RESPONSE3: #{orig.inspect}"
53 return orig
54 end
55
56 setup ARGV[0], ARGV[1], ARGV[2], ARGV[3]
57
58 message :chat?, :body do |m|
59 begin
60 puts "#{m.from.to_s} -> #{m.to.to_s} #{m.body}"
61 msg = Blather::Stanza::Message.new(m.from, 'thx for "' +
62 m.body + '"')
63 msg.from = m.to
64 write_to_stream msg
65 rescue => e
66 # TODO: do something better with this info
67 say m.from, e.inspect
68 end
69 end
70
71 iq '/iq/ns:query', :ns =>
72 'http://jabber.org/protocol/disco#items' do |i, xpath_result|
73
74 write_to_stream i.reply
75 end
76
77 iq '/iq/ns:query', :ns =>
78 'http://jabber.org/protocol/disco#info' do |i, xpath_result|
79
80 msg = i.reply
81 msg.identities = [{:name =>
82 'Soprani.ca Gateway to XMPP - Catapult',
83 :type => 'sms-ctplt', :category => 'gateway'}]
84 msg.features = ["jabber:iq:register",
85 "jabber:iq:gateway", "jabber:iq:private",
86 "http://jabber.org/protocol/disco#info",
87 "http://jabber.org/protocol/commands",
88 "http://jabber.org/protocol/muc"]
89 write_to_stream msg
90 end
91
92 iq '/iq/ns:query', :ns => 'jabber:iq:register' do |i, qn|
93 puts "IQ: #{i.inspect}"
94
95 if i.type == :set
96 xn = qn.children.find { |v| v.element_name == "x" }
97
98 user_id = ''
99 api_token = ''
100 api_secret = ''
101 phone_num = ''
102
103 if xn.nil?
104 user_id = qn.children.find {
105 |v| v.element_name == "nick" }
106 api_token = qn.children.find {
107 |v| v.element_name == "username" }
108 api_secret = qn.children.find {
109 |v| v.element_name == "password" }
110 phone_num = qn.children.find {
111 |v| v.element_name == "phone" }
112 else
113 for field in xn.children
114 if field.element_name == "field"
115 val = field.children.find { |v|
116 v.element_name == "value" }
117
118 case field['var']
119 when 'nick'
120 user_id = val.text
121 when 'username'
122 api_token = val.text
123 when 'password'
124 api_secret = val.text
125 when 'phone'
126 phone_num = val.text
127 else
128 # TODO: error
129 puts "?: " +field['var']
130 end
131 end
132 end
133 end
134
135 if phone_num[0] != '+'
136 # TODO: add text re number not (yet) supported
137 write_to_stream error_msg(i.reply, qn, :modify,
138 'policy-violation')
139 next
140 end
141
142 uri = URI.parse('https://api.catapult.inetwork.com')
143 http = Net::HTTP.new(uri.host, uri.port)
144 http.use_ssl = true
145 request = Net::HTTP::Get.new('/v1/users/' + user_id +
146 '/phoneNumbers/' + phone_num)
147 request.basic_auth api_token, api_secret
148 response = http.request(request)
149
150 puts 'API response: ' + response.to_s + ' with code ' +
151 response.code + ', body "' + response.body + '"'
152
153 if response.code == '200'
154 params = JSON.parse response.body
155 if params['numberState'] == 'enabled'
156 num_key = "catapult_num-" + phone_num
157
158 bare_jid = i.from.to_s.split('/', 2)[0]
159 cred_key = "catapult_cred-" + bare_jid
160
161 # TODO: pre-validate ARGV[5] is integer
162 conn = Hiredis::Connection.new
163 conn.connect(ARGV[4], ARGV[5].to_i)
164
165 conn.write ["EXISTS", num_key]
166 if conn.read == 1
167 conn.disconnect
168
169 # TODO: add txt re num exists
170 write_to_stream error_msg(
171 i.reply, qn, :cancel,
172 'conflict')
173 next
174 end
175
176 conn.write ["EXISTS", cred_key]
177 if conn.read == 1
178 conn.disconnect
179
180 # TODO: add txt re already exist
181 write_to_stream error_msg(
182 i.reply, qn, :cancel,
183 'conflict')
184 next
185 end
186
187 conn.write ["RPUSH",num_key,bare_jid]
188 if conn.read != 1
189 conn.disconnect
190
191 # TODO: catch/relay RuntimeError
192 # TODO: add txt re push failure
193 write_to_stream error_msg(
194 i.reply, qn, :cancel,
195 'internal-server-error')
196 next
197 end
198
199 conn.write ["RPUSH",cred_key,user_id]
200 conn.write ["RPUSH",cred_key,api_token]
201 conn.write ["RPUSH",cred_key,api_secret]
202 conn.write ["RPUSH",cred_key,phone_num]
203
204 for n in 1..4 do
205 # TODO: catch/relay RuntimeError
206 result = conn.read
207 if result != n
208 conn.disconnect
209
210 write_to_stream(
211 error_msg(
212 i.reply, qn, :cancel,
213 'internal-server-error')
214 )
215 next
216 end
217 end
218 conn.disconnect
219
220 write_to_stream i.reply
221 else
222 # TODO: add text re number disabled
223 write_to_stream error_msg(i.reply, qn,
224 :modify, 'not-acceptable')
225 end
226 elsif response.code == '401'
227 # TODO: add text re bad credentials
228 write_to_stream error_msg(i.reply, qn, :auth,
229 'not-authorized')
230 elsif response.code == '404'
231 # TODO: add text re number not found or disabled
232 write_to_stream error_msg(i.reply, qn, :cancel,
233 'item-not-found')
234 else
235 # TODO: add text re misc error, and mention code
236 write_to_stream error_msg(i.reply, qn, :modify,
237 'not-acceptable')
238 end
239
240 elsif i.type == :get
241 orig = i.reply
242
243 msg = Nokogiri::XML::Node.new 'query',orig.document
244 msg['xmlns'] = 'jabber:iq:register'
245 n1 = Nokogiri::XML::Node.new 'instructions',msg.document
246 n1.content= "Enter the information from your Account " +
247 "page as well as the Phone Number\nin your " +
248 "account you want to use (ie. '+12345678901')" +
249 ".\nUser Id is nick, API Token is username, " +
250 "API Secret is password, Phone Number is phone"+
251 ".\n\nThe source code for this gateway is at " +
252 "https://github.com/ossguy/sgx-catapult ." +
253 "\nCopyright (C) 2017 Denver Gingerich, " +
254 "licensed under AGPLv3+."
255 n2 = Nokogiri::XML::Node.new 'nick',msg.document
256 n3 = Nokogiri::XML::Node.new 'username',msg.document
257 n4 = Nokogiri::XML::Node.new 'password',msg.document
258 n5 = Nokogiri::XML::Node.new 'phone',msg.document
259 msg.add_child(n1)
260 msg.add_child(n2)
261 msg.add_child(n3)
262 msg.add_child(n4)
263 msg.add_child(n5)
264
265 x = Blather::Stanza::X.new :form, [
266 {:required => true, :type => :"text-single",
267 :label => 'User Id', :var => 'nick'},
268 {:required => true, :type => :"text-single",
269 :label => 'API Token', :var => 'username'},
270 {:required => true, :type => :"text-private",
271 :label => 'API Secret', :var => 'password'},
272 {:required => true, :type => :"text-single",
273 :label => 'Phone Number', :var => 'phone'}
274 ]
275 x.title= 'Register for ' +
276 'Soprani.ca Gateway to XMPP - Catapult'
277 x.instructions= "Enter the details from your Account " +
278 "page as well as the Phone Number\nin your " +
279 "account you want to use (ie. '+12345678901')" +
280 ".\n\nThe source code for this gateway is at " +
281 "https://github.com/ossguy/sgx-catapult ." +
282 "\nCopyright (C) 2017 Denver Gingerich, " +
283 "licensed under AGPLv3+."
284 msg.add_child(x)
285
286 orig.add_child(msg)
287 puts "RESPONSE2: #{orig.inspect}"
288 write_to_stream orig
289 puts "SENT"
290 end
291 end
292
293 subscription(:request?) do |s|
294 # TODO: are these the best to return? really need '!' here?
295 #write_to_stream s.approve!
296 #write_to_stream s.request!
297 end
298end
299
300[:INT, :TERM].each do |sig|
301 trap(sig) {
302 puts 'Shutting down gateway...'
303 SGXcatapult.shutdown
304 puts 'Gateway has terminated.'
305
306 EM.stop
307 }
308end
309
310EM.run do
311 SGXcatapult.run
312end