diff --git a/README.rdoc b/README.rdoc index e5c0afe..7dfef53 100644 --- a/README.rdoc +++ b/README.rdoc @@ -33,6 +33,10 @@ For a quick test, download smscsim.jar and smpp.jar from the Logica site, and st java -cp smscsim.jar:smpp.jar com.logica.smscsim.Simulator +If you the previous line gave you an error try this. + + java -cp opensmpp-core-3.0.0.jar:opensmpp-sim-3.0.0.jar org.smpp.smscsim.Simulator + Then type 1 (start simulation), and enter 6000 for port number. The simulator then starts a server socket on a background thread. In another terminal window, start the sample sms gateway from the ruby-smpp/examples directory by typing: ruby sample_gateway.rb diff --git a/lib/smpp/base.rb b/lib/smpp/base.rb index fe373f3..ea91a71 100644 --- a/lib/smpp/base.rb +++ b/lib/smpp/base.rb @@ -25,6 +25,10 @@ def initialize(config, delegate) # associated message ID, and then create a pending delivery report. @ack_ids = {} + # Array of PDU sequence numbers waiting for a response PDU. + # We will timeout after waiting (default) 10 seconds + @response_timers = {} + ed = @config[:enquire_link_delay_secs] || 5 comm_inactivity_timeout = 2 * ed end @@ -53,6 +57,9 @@ def logger # invoked by EventMachine when connected def post_init + # TLS settings + start_tls @config[:start_tls] if @config[:start_tls] + # send Bind PDU if we are a binder (eg # Receiver/Transmitter/Transceiver send_bind unless defined?(am_server?) && am_server? @@ -93,16 +100,13 @@ def start_enquire_link_timer(delay_secs) def receive_data(data) #append data to buffer @data << data - while (@data.length >=4) cmd_length = @data[0..3].unpack('N').first if(@data.length < cmd_length) #not complete packet ... break break end - pkt = @data.slice!(0,cmd_length) - begin # parse incoming PDU pdu = read_pdu(pkt) @@ -157,12 +161,15 @@ def process_pdu(pdu) when Pdu::DeliverSm begin logger.debug "ESM CLASS #{pdu.esm_class}" - if pdu.esm_class != 4 + case pdu.esm_class + when Pdu::Base::ESM_CLASS_DEFAULT, Pdu::Base::ESM_CLASS_DEFAULT_UDHI # MO message run_callback(:mo_received, self, pdu) - else + when Pdu::Base::ESM_CLASS_DELVR_REP, Pdu::Base::ESM_CLASS_DELVR_ACK, Pdu::Base::ESM_CLASS_USER_ACK, Pdu::Base::ESM_CLASS_INTER_ACK # Delivery report run_callback(:delivery_report_received, self, pdu) + else + raise "Unknown ESM Class #{pdu.esm_class}" end write_pdu(Pdu::DeliverSmResponse.new(pdu.sequence_number)) rescue => e @@ -191,16 +198,16 @@ def process_pdu(pdu) close_connection end when Pdu::SubmitSmResponse - mt_message_id = @ack_ids.delete(pdu.sequence_number) - if !mt_message_id + metadata = @ack_ids.delete(pdu.sequence_number) + if !metadata raise "Got SubmitSmResponse for unknown sequence_number: #{pdu.sequence_number}" end if pdu.command_status != Pdu::Base::ESME_ROK logger.error "Error status in SubmitSmResponse: #{pdu.command_status}" - run_callback(:message_rejected, self, mt_message_id, pdu) + run_callback(:message_rejected, self, metadata, pdu) else - logger.info "Got OK SubmitSmResponse (#{pdu.message_id} -> #{mt_message_id})" - run_callback(:message_accepted, self, mt_message_id, pdu) + logger.info "Got OK SubmitSmResponse (#{pdu.message_id} -> #{metadata})" + run_callback(:message_accepted, self, metadata, pdu) end when Pdu::SubmitMultiResponse mt_message_id = @ack_ids[pdu.sequence_number] @@ -268,6 +275,7 @@ def write_pdu(pdu) logger.debug "<- #{pdu.to_human}" hex_debug pdu.data, "<- " send_data pdu.data + set_response_timer(pdu) if !pdu.response? end def read_pdu(data) @@ -275,6 +283,8 @@ def read_pdu(data) # we may either receive a new request or a response to a previous response. begin pdu = Pdu::Base.create(data) + cancel_response_timer(pdu) if pdu.response? + if !pdu logger.warn "Not able to parse PDU!" else @@ -288,6 +298,28 @@ def read_pdu(data) pdu end + def set_response_timer(pdu) + @response_timers[pdu.sequence_number] = EventMachine::Timer.new(response_timer) do + run_callback(:response_timeout, pdu.sequence_number) + end + end + + def cancel_response_timer(pdu) + if timer = @response_timers.delete(pdu.sequence_number) + begin + timer.cancel + rescue Exception => ex + logger.error "Error removing response_timer for #{pdu.sequence_number}: #{ex.message}" + end + else + logger.error "WARNING Received sequence number #{pdu.sequence_number} but no associated response_timer found in #{@response_timers.keys.inspect}" + end + end + + def response_timer + @config[:response_timer]||=10 + end + def hex_debug(data, prefix = "") Base.hex_debug(data, prefix) end diff --git a/lib/smpp/pdu/base.rb b/lib/smpp/pdu/base.rb index f8fe617..96fffcc 100644 --- a/lib/smpp/pdu/base.rb +++ b/lib/smpp/pdu/base.rb @@ -67,6 +67,14 @@ class Base SUBMIT_MULTI = 0X00000021 SUBMIT_MULTI_RESP = 0X80000021 + #ESM Classes + ESM_CLASS_DEFAULT = 0 + ESM_CLASS_DELVR_REP = 4 + ESM_CLASS_DELVR_ACK = 8 + ESM_CLASS_USER_ACK = 16 + ESM_CLASS_INTER_ACK = 32 + ESM_CLASS_DEFAULT_UDHI = 64 + OPTIONAL_RECEIPTED_MESSAGE_ID = 0x001E OPTIONAL_MESSAGE_STATE = 0x0427 @@ -97,6 +105,10 @@ def logger Smpp::Base.logger end + def response? + @command_id > 0X80000000 + end + def to_human # convert header (4 bytes) to array of 4-byte ints a = @data.to_s.unpack('N4') diff --git a/lib/smpp/pdu/submit_sm.rb b/lib/smpp/pdu/submit_sm.rb index 3a776a7..87b9e6c 100644 --- a/lib/smpp/pdu/submit_sm.rb +++ b/lib/smpp/pdu/submit_sm.rb @@ -18,10 +18,10 @@ def initialize(source_addr, destination_addr, short_message, options={}, seq = n @service_type = options[:service_type]? options[:service_type] :'' @source_addr_ton = options[:source_addr_ton]?options[:source_addr_ton]:0 # network specific @source_addr_npi = options[:source_addr_npi]?options[:source_addr_npi]:1 # unknown - @source_addr = source_addr + @source_addr = source_addr.to_s @dest_addr_ton = options[:dest_addr_ton]?options[:dest_addr_ton]:1 # international @dest_addr_npi = options[:dest_addr_npi]?options[:dest_addr_npi]:1 # unknown - @destination_addr = destination_addr + @destination_addr = destination_addr.to_s @esm_class = options[:esm_class]?options[:esm_class]:0 # default smsc mode @protocol_id = options[:protocol_id]?options[:protocol_id]:0 @priority_flag = options[:priority_flag]?options[:priority_flag]:0 @@ -41,7 +41,7 @@ def initialize(source_addr, destination_addr, short_message, options={}, seq = n pdu_body = [@service_type, @source_addr_ton, @source_addr_npi, @source_addr, @dest_addr_ton, @dest_addr_npi, @destination_addr, @esm_class, @protocol_id, @priority_flag, @schedule_delivery_time, @validity_period, @registered_delivery, @replace_if_present_flag, @data_coding, @sm_default_msg_id, @sm_length, payload].pack("A*xCCA*xCCA*xCCCA*xA*xCCCCCA*") - + if @optional_parameters pdu_body << optional_parameters_to_buffer(@optional_parameters) end diff --git a/lib/smpp/transceiver.rb b/lib/smpp/transceiver.rb index e11d49f..0f3de14 100644 --- a/lib/smpp/transceiver.rb +++ b/lib/smpp/transceiver.rb @@ -14,6 +14,7 @@ # unbound(transceiver) class Smpp::Transceiver < Smpp::Base + ALL_EMOJI = /[\u{00A9}\u{00AE}\u{203C}-\u{3299}\u{1F004}-\u{1F9C0}]/ # Send an MT SMS message. Delegate will receive message_accepted callback when SMSC # acknowledges, or the message_rejected callback upon error @@ -33,36 +34,69 @@ def send_mt(message_id, source_addr, destination_addr, short_message, options={} # Send a concatenated message with a body of > 160 characters as multiple messages. def send_concat_mt(message_id, source_addr, destination_addr, message, options = {}) - logger.debug "Sending concatenated MT: #{message}" + if @state == :bound - # Split the message into parts of 153 characters. (160 - 7 characters for UDH) + # Split the message into parts of 152 characters. (160 - 8 characters for UDH because we use 16 bit message_id) or + # Split it to 66 parts in case of UCS2 encodeing (70 - 8 characters for UDH because we use 16 bit message_id). parts = [] - while message.size > 0 do - parts << message.slice!(0..Smpp::Transceiver.get_message_part_size(options)) - end - - 0.upto(parts.size-1) do |i| - udh = sprintf("%c", 5) # UDH is 5 bytes. - udh << sprintf("%c%c", 0, 3) # This is a concatenated message - - #TODO Figure out why this needs to be an int here, it's a string elsewhere - udh << sprintf("%c", message_id) # The ID for the entire concatenated message - - udh << sprintf("%c", parts.size) # How many parts this message consists of - udh << sprintf("%c", i+1) # This is part i+1 - + part = [] + # If message body is ucs2 encoded we will convert it back on the fly to utf8 then we will + # split it to parts and then encode each part back to binary + if options[:data_coding] == 8 + max_part_size = Smpp::Transceiver.get_message_part_size(options) + shadow_message = message + shadow_message.force_encoding(Encoding::UTF_16BE) + shadow_message = shadow_message.encode(Encoding::UTF_8, :invalid => :replace, :undef => :replace, :replace => '') + shadow_message_arr = shadow_message.chars.to_a + shadow_message_arr.each.with_index.inject(0) do |part_size,(value,index)| + value.scan(ALL_EMOJI).empty? ? part_size += 1 : part_size += 2 + value = value.encode(Encoding::UTF_16BE, :invalid => :replace, :undef => :replace, :replace => '') + part << value + if (max_part_size == part_size or (max_part_size == (part_size - 1 ) and value.size == 2) or index + 1 == shadow_message.chars.to_a.size or (shadow_message_arr[index + 1].scan(ALL_EMOJI).empty? == false and (max_part_size-1) == part_size)) + parts << part.map{ |char| char.force_encoding(Encoding::BINARY) }.join + part = [] + part_size = 0 + end + part_size + end + # shadow_message = message + # shadow_message.force_encoding(Encoding::UTF_16BE) + # shadow_message = shadow_message.encode(Encoding::UTF_8, :invalid => :replace, :undef => :replace, :replace => '') + # shadow_message.chars.to_a.each_slice(Smpp::Transceiver.get_message_part_size(options)) do |part| + # part = part.join + # part = part.encode(Encoding::UTF_16BE, :invalid => :replace, :undef => :replace, :replace => '') + # part.force_encoding(Encoding::BINARY) + # parts << part + # end + else + while message.size > 0 do + parts << message.slice!(0...Smpp::Transceiver.get_message_part_size(options)) + end + end + + 0.upto(parts.size-1) do |i| + # New encoding style taken from + # https://github.com/Eloi/ruby-smpp/commit/6c2c20297cde4d3473c4c8362abed6ded6d59c09?diff=unified + udh = [ 5, # UDH is 5 bytes. + 0, 3, # This is a concatenated message + message_id % 256, # Ensure single byte message_id + parts.size, # How many parts this message consists of + i + 1 # This is part i+1 + ].pack('C'*6) + + options[:esm_class] = 64 # This message contains a UDH header. options[:udh] = udh - pdu = Pdu::SubmitSm.new(source_addr, destination_addr, parts[i], options) + write_pdu pdu # This is definately a bit hacky - multiple PDUs are being associated with a single # message_id. - @ack_ids[pdu.sequence_number] = message_id - end + @ack_ids[pdu.sequence_number] = {:message_id => message_id, :part_number => i + 1, :parts_size => parts.size } + end else - raise InvalidStateException, "Transceiver is unbound. Connot send MT messages." + raise InvalidStateException, "Transceiver is unbound. Cannot send MT messages." end end @@ -74,7 +108,7 @@ def send_multi_mt(message_id, source_addr, destination_addr_arr, short_message, if @state == :bound pdu = Pdu::SubmitMulti.new(source_addr, destination_addr_arr, short_message, options) write_pdu pdu - + # keep the message ID so we can associate the SMSC message ID with our message # when the response arrives. @ack_ids[pdu.sequence_number] = message_id @@ -105,6 +139,7 @@ def self.get_message_part_size options return 134 if options[:data_coding] == 5 return 134 if options[:data_coding] == 6 return 134 if options[:data_coding] == 7 + return 67 if options[:data_coding] == 8 and options[:emoji] == true return 67 if options[:data_coding] == 8 return 153 end diff --git a/test/pdu_parsing_test.rb b/test/pdu_parsing_test.rb index 78b85e9..7821424 100644 --- a/test/pdu_parsing_test.rb +++ b/test/pdu_parsing_test.rb @@ -19,6 +19,7 @@ def test_recieve_single_message assert_equal "447803029837", pdu.destination_addr assert_nil pdu.udh assert_equal "Test", pdu.short_message + assert_equal false, pdu.response? end def test_recieve_part_one_of_multi_part_message @@ -86,6 +87,7 @@ def test_submit_sm_response_clean pdu = create_pdu(data) assert_equal Smpp::Pdu::SubmitSmResponse, pdu.class assert_equal "54114-0415V-2120E-09H1Q", pdu.message_id + assert_equal true, pdu.response? end def test_submit_sm_response_with_optional_params