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 != 7 then
27 puts "Usage: sgx-catapult.rb <component_jid> <component_password> " +
28 "<server_hostname> <server_port> " +
29 "<redis_hostname> <redis_port> <delivery_receipt_url>"
30 exit 0
31end
32
33module SGXcatapult
34 extend Blather::DSL
35
36 def self.run
37 client.run
38 end
39
40 def self.error_msg(orig, query_node, type, name, text = nil)
41 orig.add_child(query_node)
42 orig.type = :error
43
44 error = Nokogiri::XML::Node.new 'error', orig.document
45 error['type'] = type
46 orig.add_child(error)
47
48 suberr = Nokogiri::XML::Node.new name, orig.document
49 suberr['xmlns'] = 'urn:ietf:params:xml:ns:xmpp-stanzas'
50 error.add_child(suberr)
51
52 # TODO: add some explanatory xml:lang='en' text (see text param)
53 puts "RESPONSE3: #{orig.inspect}"
54 return orig
55 end
56
57 setup ARGV[0], ARGV[1], ARGV[2], ARGV[3]
58
59 message :chat?, :body do |m|
60 num_dest = m.to.to_s.split('@', 2)[0]
61
62 if num_dest[0] != '+'
63 # TODO: add text re number not (yet) supported/implmnted
64 write_to_stream error_msg(m.reply, m.body, :modify,
65 'policy-violation')
66 next
67 end
68
69 bare_jid = m.from.to_s.split('/', 2)[0]
70 cred_key = "catapult_cred-" + bare_jid
71
72 conn = Hiredis::Connection.new
73 conn.connect(ARGV[4], ARGV[5].to_i)
74
75 conn.write ["EXISTS", cred_key]
76 if conn.read == 0
77 conn.disconnect
78
79 # TODO: add text re credentials not being registered
80 write_to_stream error_msg(m.reply, m.body, :auth,
81 'registration-required')
82 next
83 end
84
85 conn.write ["LRANGE", cred_key, 0, 3]
86 creds = conn.read
87 conn.disconnect
88
89 uri = URI.parse('https://api.catapult.inetwork.com')
90 http = Net::HTTP.new(uri.host, uri.port)
91 http.use_ssl = true
92 request = Net::HTTP::Post.new('/v1/users/' + creds[0] +
93 '/messages')
94 request.basic_auth creds[1], creds[2]
95 request.add_field('Content-Type', 'application/json')
96 request.body = JSON.dump({
97 'from' => creds[3],
98 'to' => num_dest,
99 'text' => m.body,
100 'tag' => m.id, # TODO: message has it?
101 'receiptRequested' => 'all',
102 'callbackUrl' => ARGV[6]
103 })
104 response = http.request(request)
105
106 puts 'API response to send: ' + response.to_s + ' with code ' +
107 response.code + ', body "' + response.body + '"'
108
109 if response.code != '201'
110 # TODO: add text re unexpected code; mention code number
111 write_to_stream error_msg(m.reply, m.body, :cancel,
112 'internal-server-error')
113 next
114 end
115
116 # TODO: don't echo message; leave in until we rcv msgs properly
117 begin
118 puts "#{m.from.to_s} -> #{m.to.to_s} #{m.body}"
119 msg = Blather::Stanza::Message.new(m.from, 'thx for "' +
120 m.body + '"')
121 msg.from = m.to
122 write_to_stream msg
123 rescue => e
124 # TODO: do something better with this info
125 say m.from, e.inspect
126 end
127 end
128
129 iq '/iq/ns:query', :ns =>
130 'http://jabber.org/protocol/disco#items' do |i, xpath_result|
131
132 write_to_stream i.reply
133 end
134
135 iq '/iq/ns:query', :ns =>
136 'http://jabber.org/protocol/disco#info' do |i, xpath_result|
137
138 msg = i.reply
139 msg.identities = [{:name =>
140 'Soprani.ca Gateway to XMPP - Catapult',
141 :type => 'sms-ctplt', :category => 'gateway'}]
142 msg.features = ["jabber:iq:register",
143 "jabber:iq:gateway", "jabber:iq:private",
144 "http://jabber.org/protocol/disco#info",
145 "http://jabber.org/protocol/commands",
146 "http://jabber.org/protocol/muc"]
147 write_to_stream msg
148 end
149
150 iq '/iq/ns:query', :ns => 'jabber:iq:register' do |i, qn|
151 puts "IQ: #{i.inspect}"
152
153 if i.type == :set
154 xn = qn.children.find { |v| v.element_name == "x" }
155
156 user_id = ''
157 api_token = ''
158 api_secret = ''
159 phone_num = ''
160
161 if xn.nil?
162 user_id = qn.children.find {
163 |v| v.element_name == "nick" }
164 api_token = qn.children.find {
165 |v| v.element_name == "username" }
166 api_secret = qn.children.find {
167 |v| v.element_name == "password" }
168 phone_num = qn.children.find {
169 |v| v.element_name == "phone" }
170 else
171 for field in xn.children
172 if field.element_name == "field"
173 val = field.children.find { |v|
174 v.element_name == "value" }
175
176 case field['var']
177 when 'nick'
178 user_id = val.text
179 when 'username'
180 api_token = val.text
181 when 'password'
182 api_secret = val.text
183 when 'phone'
184 phone_num = val.text
185 else
186 # TODO: error
187 puts "?: " +field['var']
188 end
189 end
190 end
191 end
192
193 if phone_num[0] != '+'
194 # TODO: add text re number not (yet) supported
195 write_to_stream error_msg(i.reply, qn, :modify,
196 'policy-violation')
197 next
198 end
199
200 uri = URI.parse('https://api.catapult.inetwork.com')
201 http = Net::HTTP.new(uri.host, uri.port)
202 http.use_ssl = true
203 request = Net::HTTP::Get.new('/v1/users/' + user_id +
204 '/phoneNumbers/' + phone_num)
205 request.basic_auth api_token, api_secret
206 response = http.request(request)
207
208 puts 'API response: ' + response.to_s + ' with code ' +
209 response.code + ', body "' + response.body + '"'
210
211 if response.code == '200'
212 params = JSON.parse response.body
213 if params['numberState'] == 'enabled'
214 num_key = "catapult_num-" + phone_num
215
216 bare_jid = i.from.to_s.split('/', 2)[0]
217 cred_key = "catapult_cred-" + bare_jid
218
219 # TODO: pre-validate ARGV[5] is integer
220 conn = Hiredis::Connection.new
221 conn.connect(ARGV[4], ARGV[5].to_i)
222
223 conn.write ["EXISTS", num_key]
224 if conn.read == 1
225 conn.disconnect
226
227 # TODO: add txt re num exists
228 write_to_stream error_msg(
229 i.reply, qn, :cancel,
230 'conflict')
231 next
232 end
233
234 conn.write ["EXISTS", cred_key]
235 if conn.read == 1
236 conn.disconnect
237
238 # TODO: add txt re already exist
239 write_to_stream error_msg(
240 i.reply, qn, :cancel,
241 'conflict')
242 next
243 end
244
245 conn.write ["RPUSH",num_key,bare_jid]
246 if conn.read != 1
247 conn.disconnect
248
249 # TODO: catch/relay RuntimeError
250 # TODO: add txt re push failure
251 write_to_stream error_msg(
252 i.reply, qn, :cancel,
253 'internal-server-error')
254 next
255 end
256
257 conn.write ["RPUSH",cred_key,user_id]
258 conn.write ["RPUSH",cred_key,api_token]
259 conn.write ["RPUSH",cred_key,api_secret]
260 conn.write ["RPUSH",cred_key,phone_num]
261
262 for n in 1..4 do
263 # TODO: catch/relay RuntimeError
264 result = conn.read
265 if result != n
266 conn.disconnect
267
268 write_to_stream(
269 error_msg(
270 i.reply, qn, :cancel,
271 'internal-server-error')
272 )
273 next
274 end
275 end
276 conn.disconnect
277
278 write_to_stream i.reply
279 else
280 # TODO: add text re number disabled
281 write_to_stream error_msg(i.reply, qn,
282 :modify, 'not-acceptable')
283 end
284 elsif response.code == '401'
285 # TODO: add text re bad credentials
286 write_to_stream error_msg(i.reply, qn, :auth,
287 'not-authorized')
288 elsif response.code == '404'
289 # TODO: add text re number not found or disabled
290 write_to_stream error_msg(i.reply, qn, :cancel,
291 'item-not-found')
292 else
293 # TODO: add text re misc error, and mention code
294 write_to_stream error_msg(i.reply, qn, :modify,
295 'not-acceptable')
296 end
297
298 elsif i.type == :get
299 orig = i.reply
300
301 msg = Nokogiri::XML::Node.new 'query',orig.document
302 msg['xmlns'] = 'jabber:iq:register'
303 n1 = Nokogiri::XML::Node.new 'instructions',msg.document
304 n1.content= "Enter the information from your Account " +
305 "page as well as the Phone Number\nin your " +
306 "account you want to use (ie. '+12345678901')" +
307 ".\nUser Id is nick, API Token is username, " +
308 "API Secret is password, Phone Number is phone"+
309 ".\n\nThe source code for this gateway is at " +
310 "https://github.com/ossguy/sgx-catapult ." +
311 "\nCopyright (C) 2017 Denver Gingerich, " +
312 "licensed under AGPLv3+."
313 n2 = Nokogiri::XML::Node.new 'nick',msg.document
314 n3 = Nokogiri::XML::Node.new 'username',msg.document
315 n4 = Nokogiri::XML::Node.new 'password',msg.document
316 n5 = Nokogiri::XML::Node.new 'phone',msg.document
317 msg.add_child(n1)
318 msg.add_child(n2)
319 msg.add_child(n3)
320 msg.add_child(n4)
321 msg.add_child(n5)
322
323 x = Blather::Stanza::X.new :form, [
324 {:required => true, :type => :"text-single",
325 :label => 'User Id', :var => 'nick'},
326 {:required => true, :type => :"text-single",
327 :label => 'API Token', :var => 'username'},
328 {:required => true, :type => :"text-private",
329 :label => 'API Secret', :var => 'password'},
330 {:required => true, :type => :"text-single",
331 :label => 'Phone Number', :var => 'phone'}
332 ]
333 x.title= 'Register for ' +
334 'Soprani.ca Gateway to XMPP - Catapult'
335 x.instructions= "Enter the details from your Account " +
336 "page as well as the Phone Number\nin your " +
337 "account you want to use (ie. '+12345678901')" +
338 ".\n\nThe source code for this gateway is at " +
339 "https://github.com/ossguy/sgx-catapult ." +
340 "\nCopyright (C) 2017 Denver Gingerich, " +
341 "licensed under AGPLv3+."
342 msg.add_child(x)
343
344 orig.add_child(msg)
345 puts "RESPONSE2: #{orig.inspect}"
346 write_to_stream orig
347 puts "SENT"
348 end
349 end
350
351 subscription(:request?) do |s|
352 # TODO: are these the best to return? really need '!' here?
353 #write_to_stream s.approve!
354 #write_to_stream s.request!
355 end
356end
357
358[:INT, :TERM].each do |sig|
359 trap(sig) {
360 puts 'Shutting down gateway...'
361 SGXcatapult.shutdown
362 puts 'Gateway has terminated.'
363
364 EM.stop
365 }
366end
367
368EM.run do
369 SGXcatapult.run
370end