diff --git a/.rubocop.yml b/.rubocop.yml index c69aa4120c7f7b7ba8ed3dc0c41df33b0a1d3dbf..59ccb1a8d0c1fa8bb816495721e3534999e26298 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -49,6 +49,9 @@ Style/AlignParameters: Style/BlockDelimiters: Enabled: false +Style/CaseIndentation: + EnforcedStyle: end + Style/Documentation: Enabled: false diff --git a/Gemfile b/Gemfile index d4a7109af3ffff61c8b3b47387a4c791f2664423..a99d68ebb5bf608f5a96b50577aa0cf4b79745f7 100644 --- a/Gemfile +++ b/Gemfile @@ -2,14 +2,11 @@ source 'https://rubygems.org' gem 'activesupport', '<5.0.0' gem 'blather' +gem 'em-hiredis' gem 'em-http-request' gem 'eventmachine', '1.0.1' gem 'promise.rb' -gem 'em-hiredis' -gem 'hiredis', '~> 0.6.0' -gem 'redis', '>= 3.2.0' - gem 'goliath' gem 'log4r' diff --git a/sgx-catapult.rb b/sgx-catapult.rb index acdac5b4a4600868669c94fea38a23f0cc7c3668..db2d7b5270987a9a974090cc2e9de66fa2f940b3 100755 --- a/sgx-catapult.rb +++ b/sgx-catapult.rb @@ -22,8 +22,6 @@ require 'blather/client/dsl' require 'em-hiredis' require 'em-http-request' require 'json' -require 'net/http' -require 'redis/connection/hiredis' require 'securerandom' require 'time' require 'uri' @@ -114,7 +112,8 @@ module SGXcatapult return orig end - setup ARGV[0], ARGV[1], ARGV[2], ARGV[3] + # workqueue_count MUST be 0 or else Blather uses threads! + setup ARGV[0], ARGV[1], ARGV[2], ARGV[3], nil, nil, workqueue_count: 0 def self.pass_on_message(m, users_num, jid) # setup delivery receipt; similar to a reply @@ -143,7 +142,9 @@ module SGXcatapult write_to_stream rcpt end - def self.call_catapult(token, secret, m, pth, body, head={}, code=[200]) + def self.call_catapult( + token, secret, m, pth, body=nil, head={}, code=[200] + ) EM::HttpRequest.new( "https://api.catapult.inetwork.com/#{pth}" ).public_send( @@ -159,10 +160,7 @@ module SGXcatapult if code.include?(http.response_header.status) http.response else - # TODO: add text; mention code number - EMPromise.reject( - [:cancel, 'internal-server-error'] - ) + EMPromise.reject(http.response_header.status) end } end @@ -198,7 +196,12 @@ module SGXcatapult )), {'Content-Type' => 'application/json'}, [201] - ) + ).catch { + # TODO: add text; mention code number + EMPromise.reject( + [:cancel, 'internal-server-error'] + ) + } end def self.validate_num(num) @@ -462,7 +465,11 @@ module SGXcatapult :put, path, @partial_data[cn[0]['sid']] - ), + ).catch { + EMPromise.reject([ + :cancel, 'internal-server-error' + ]) + }, to_catapult( i, "https://api.catapult.inetwork.com/" + @@ -537,285 +544,235 @@ module SGXcatapult write_to_stream msg end - def self.check_then_register(user_id, api_token, api_secret, phone_num, - i, qn) - - jid_key = "catapult_jid-" + phone_num - - bare_jid = i.from.to_s.split('/', 2)[0] - cred_key = "catapult_cred-" + bare_jid - - # TODO: pre-validate ARGV[5] is integer - conn = Hiredis::Connection.new - conn.connect(ARGV[4], ARGV[5].to_i) - - conn.write ["GET", jid_key] - existing_jid = conn.read - - if not existing_jid.nil? and existing_jid != bare_jid - conn.disconnect - - # TODO: add/log text re credentials exist already - write_to_stream error_msg( - i.reply, qn, :cancel, - 'conflict') - return false - end - - # ASSERT: existing_jid is nil or equal to bare_jid - - conn.write ["EXISTS", cred_key] - creds_exist = conn.read - if 1 == creds_exist - conn.write ["LRANGE", cred_key, 0, 3] - if [user_id, api_token, api_secret, phone_num] != - conn.read + def self.check_then_register(i, *creds) + jid_key = "catapult_jid-#{creds.last}" + bare_jid = i.from.stripped + cred_key = "catapult_cred-#{bare_jid}" - conn.disconnect - - # TODO: add/log txt re credentials exist already - write_to_stream error_msg( - i.reply, qn, :cancel, - 'conflict') - return false + REDIS.get(jid_key).then { |existing_jid| + if existing_jid && existing_jid != bare_jid + # TODO: add/log text: credentials exist already + EMPromise.reject([:cancel, 'conflict']) end - end - - # ASSERT: cred_key does not exist or its value equals input vals - - # not necessary if existing_jid non-nil, but easier to do anyway - conn.write ["SET", jid_key, bare_jid] - if conn.read != 'OK' - conn.disconnect - - # TODO: catch/relay RuntimeError - # TODO: add txt re push failure - write_to_stream error_msg( - i.reply, qn, :cancel, - 'internal-server-error') - return false - end - - if 1 == creds_exist - # per above ASSERT, cred_key value equals input already - conn.disconnect + }.then { + REDIS.lrange(cred_key, 0, 3) + }.then { |existing_creds| + # TODO: add/log text: credentials exist already + if existing_creds.length == 4 && creds != existing_creds + EMPromise.reject([:cancel, 'conflict']) + elsif existing_creds.length < 4 + REDIS.rpush(cred_key, *creds).then { |length| + if length != 4 + EMPromise.reject([ + :cancel, + 'internal-server-error' + ]) + end + } + end + }.then { + # not necessary if existing_jid non-nil, easier this way + REDIS.set(jid_key, bare_jid) + }.then { |result| + if result != 'OK' + # TODO: add txt re push failure + EMPromise.reject( + [:cancel, 'internal-server-error'] + ) + end + }.then { write_to_stream i.reply - return true - end - - conn.write ["RPUSH", cred_key, user_id] - conn.write ["RPUSH", cred_key, api_token] - conn.write ["RPUSH", cred_key, api_secret] - conn.write ["RPUSH", cred_key, phone_num] + } + end - # TODO: confirm cred_key list size == 4 + def self.creds_from_registration_query(qn) + xn = qn.children.find { |v| v.element_name == "x" } - (1..4).each do |n| - # TODO: catch/relay RuntimeError - result = conn.read - if result != n - conn.disconnect + if xn + xn.children.each_with_object({}) do |field, h| + next if field.element_name != "field" + val = field.children.find { |v| + v.element_name == "value" + } - write_to_stream error_msg( - i.reply, qn, :cancel, - 'internal-server-error') - return false + case field['var'] + when 'nick' + h[:user_id] = val.text + when 'username' + h[:api_token] = val.text + when 'password' + h[:api_secret] = val.text + when 'phone' + h[:phone_num] = val.text + else + # TODO: error + puts "?: #{field['var']}" + end end - end - conn.disconnect - - write_to_stream i.reply - - return true + else + qn.children.each_with_object({}) do |field, h| + case field.element_name + when "nick" + h[:user_id] = field.text + when "username" + h[:api_token] = field.text + when "password" + h[:api_secret] = field.text + when "phone" + h[:phone_num] = field.text + end + end + end.values_at(:user_id, :api_token, :api_secret, :phone_num) end - iq '/iq/ns:query', ns: 'jabber:iq:register' do |i, qn| - puts "IQ: #{i.inspect}" - - if i.type == :set - rn = qn.children.find { |v| v.element_name == "remove" } - if not rn.nil? + def self.process_registration(i, qn) + EMPromise.resolve( + qn.children.find { |v| v.element_name == "remove" } + ).then { |rn| + if rn puts "received - ignoring for now..." - next - end - - xn = qn.children.find { |v| v.element_name == "x" } - - user_id = '' - api_token = '' - api_secret = '' - phone_num = '' - - if xn.nil? - user_id = qn.children.find { |v| - v.element_name == "nick" - } - api_token = qn.children.find { |v| - v.element_name == "username" - } - api_secret = qn.children.find { |v| - v.element_name == "password" - } - phone_num = qn.children.find { |v| - v.element_name == "phone" - } + EMPromise.reject(:done) else - xn.children.each do |field| - if field.element_name == "field" - val = field.children.find { |v| - v.element_name == "value" - } - - case field['var'] - when 'nick' - user_id = val.text - when 'username' - api_token = val.text - when 'password' - api_secret = val.text - when 'phone' - phone_num = val.text - else - # TODO: error - puts "?: " +field['var'] - end - end - end + creds_from_registration_query(qn) end - - if phone_num[0] != '+' + }.then { |user_id, api_token, api_secret, phone_num| + if phone_num[0] == '+' + [user_id, api_token, api_secret, phone_num] + else # TODO: add text re number not (yet) supported - write_to_stream error_msg( - i.reply, qn, :cancel, - 'item-not-found' - ) - next + EMPromise.reject([:cancel, 'item-not-found']) end - - uri = URI.parse('https://api.catapult.inetwork.com') - http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = true - request = Net::HTTP::Get.new('/v1/users/' + user_id + - '/phoneNumbers/' + phone_num) - request.basic_auth api_token, api_secret - response = http.request(request) - - puts 'API response: ' + response.to_s + ' with code ' + - response.code + ', body "' + response.body + '"' - - if response.code == '200' - params = JSON.parse response.body + }.then { |user_id, api_token, api_secret, phone_num| + call_catapult( + api_token, + api_secret, + :get, + "v1/users/#{user_id}/phoneNumbers/#{phone_num}" + ).then { |response| + params = JSON.parse(response) if params['numberState'] == 'enabled' - if not check_then_register( - user_id, api_token, api_secret, - phone_num, i, qn + check_then_register( + i, + user_id, + api_token, + api_secret, + phone_num ) - next - end else # TODO: add text re number disabled - write_to_stream error_msg( - i.reply, qn, - :modify, 'not-acceptable' - ) + EMPromise.reject([:modify, 'not-acceptable']) end - elsif response.code == '401' + } + }.catch { |e| + EMPromise.reject(case e + when 401 # TODO: add text re bad credentials - write_to_stream error_msg( - i.reply, qn, :auth, - 'not-authorized' - ) - elsif response.code == '404' + [:auth, 'not-authorized'] + when 404 # TODO: add text re number not found or disabled - write_to_stream error_msg( - i.reply, qn, :cancel, - 'item-not-found' - ) + [:cancel, 'item-not-found'] + when Integer + [:modify, 'not-acceptable'] else - # TODO: add text re misc error, and mention code - write_to_stream error_msg( - i.reply, qn, :modify, - 'not-acceptable' + e + end) + } + end + + def self.registration_form(orig, existing_number=nil) + msg = Nokogiri::XML::Node.new 'query', orig.document + msg['xmlns'] = 'jabber:iq:register' + + if existing_number + msg.add_child( + Nokogiri::XML::Node.new( + 'registered', msg.document ) - end + ) + end - elsif i.type == :get - orig = i.reply + n1 = Nokogiri::XML::Node.new( + 'instructions', msg.document + ) + n1.content = "Enter the information from your Account "\ + "page as well as the Phone Number\nin your "\ + "account you want to use (ie. '+12345678901')"\ + ".\nUser Id is nick, API Token is username, "\ + "API Secret is password, Phone Number is phone"\ + ".\n\nThe source code for this gateway is at "\ + "https://gitlab.com/ossguy/sgx-catapult ."\ + "\nCopyright (C) 2017 Denver Gingerich and "\ + "others, licensed under AGPLv3+." + n2 = Nokogiri::XML::Node.new 'nick', msg.document + n3 = Nokogiri::XML::Node.new 'username', msg.document + n4 = Nokogiri::XML::Node.new 'password', msg.document + n5 = Nokogiri::XML::Node.new 'phone', msg.document + n5.content = existing_number.to_s + msg.add_child(n1) + msg.add_child(n2) + msg.add_child(n3) + msg.add_child(n4) + msg.add_child(n5) + + x = Blather::Stanza::X.new :form, [ + { + required: true, type: :"text-single", + label: 'User Id', var: 'nick' + }, + { + required: true, type: :"text-single", + label: 'API Token', var: 'username' + }, + { + required: true, type: :"text-private", + label: 'API Secret', var: 'password' + }, + { + required: true, type: :"text-single", + label: 'Phone Number', var: 'phone', + value: existing_number.to_s + } + ] + x.title = 'Register for '\ + 'Soprani.ca Gateway to XMPP - Catapult' + x.instructions = "Enter the details from your Account "\ + "page as well as the Phone Number\nin your "\ + "account you want to use (ie. '+12345678901')"\ + ".\n\nThe source code for this gateway is at "\ + "https://gitlab.com/ossguy/sgx-catapult ."\ + "\nCopyright (C) 2017 Denver Gingerich and "\ + "others, licensed under AGPLv3+." + msg.add_child(x) - bare_jid = i.from.to_s.split('/', 2)[0] - cred_key = "catapult_cred-" + bare_jid + orig.add_child(msg) - conn = Hiredis::Connection.new - conn.connect(ARGV[4], ARGV[5].to_i) - conn.write(["LINDEX", cred_key, 3]) - existing_number = conn.read - conn.disconnect + return orig + end - msg = Nokogiri::XML::Node.new 'query', orig.document - msg['xmlns'] = 'jabber:iq:register' + iq '/iq/ns:query', ns: 'jabber:iq:register' do |i, qn| + puts "IQ: #{i.inspect}" - if existing_number - msg.add_child( - Nokogiri::XML::Node.new('registered', msg.document) - ) + case i.type + when :set + process_registration(i, qn) + when :get + bare_jid = i.from.stripped + cred_key = "catapult_cred-#{bare_jid}" + REDIS.lindex(cred_key, 3).then { |existing_number| + reply = registration_form(i.reply, existing_number) + puts "RESPONSE2: #{reply.inspect}" + write_to_stream reply + } + else + # Unknown IQ, ignore for now + EMPromise.reject(:done) + end.catch { |e| + if e.is_a?(Array) && e.length == 2 + write_to_stream error_msg(i.reply, qn, *e) + elsif e != :done + EMPromise.reject(e) end - - n1 = Nokogiri::XML::Node.new 'instructions', msg.document - n1.content= "Enter the information from your Account "\ - "page as well as the Phone Number\nin your "\ - "account you want to use (ie. '+12345678901')"\ - ".\nUser Id is nick, API Token is username, "\ - "API Secret is password, Phone Number is phone"\ - ".\n\nThe source code for this gateway is at "\ - "https://gitlab.com/ossguy/sgx-catapult ."\ - "\nCopyright (C) 2017 Denver Gingerich and "\ - "others, licensed under AGPLv3+." - n2 = Nokogiri::XML::Node.new 'nick', msg.document - n3 = Nokogiri::XML::Node.new 'username', msg.document - n4 = Nokogiri::XML::Node.new 'password', msg.document - n5 = Nokogiri::XML::Node.new 'phone', msg.document - n5.content = existing_number.to_s - msg.add_child(n1) - msg.add_child(n2) - msg.add_child(n3) - msg.add_child(n4) - msg.add_child(n5) - - x = Blather::Stanza::X.new :form, [ - { - required: true, type: :"text-single", - label: 'User Id', var: 'nick' - }, - { - required: true, type: :"text-single", - label: 'API Token', var: 'username' - }, - { - required: true, type: :"text-private", - label: 'API Secret', var: 'password' - }, - { - required: true, type: :"text-single", - label: 'Phone Number', var: 'phone', - value: existing_number.to_s - } - ] - x.title= 'Register for '\ - 'Soprani.ca Gateway to XMPP - Catapult' - x.instructions= "Enter the details from your Account "\ - "page as well as the Phone Number\nin your "\ - "account you want to use (ie. '+12345678901')"\ - ".\n\nThe source code for this gateway is at "\ - "https://gitlab.com/ossguy/sgx-catapult ."\ - "\nCopyright (C) 2017 Denver Gingerich and "\ - "others, licensed under AGPLv3+." - msg.add_child(x) - - orig.add_child(msg) - puts "RESPONSE2: #{orig.inspect}" - write_to_stream orig - puts "SENT" - end + }.catch(&method(:panic)) end subscription(:request?) do |s|