sgx-catapult.rb

  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