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