├── VERSION ├── .ruby-version ├── .travis.yml ├── lib ├── smpp │ ├── version.rb │ ├── pdu │ │ ├── bind_receiver.rb │ │ ├── bind_transceiver.rb │ │ ├── bind_transmitter.rb │ │ ├── bind_receiver_response.rb │ │ ├── bind_transceiver_response.rb │ │ ├── bind_transmitter_response.rb │ │ ├── unbind.rb │ │ ├── enquire_link.rb │ │ ├── enquire_link_response.rb │ │ ├── unbind_response.rb │ │ ├── deliver_sm_response.rb │ │ ├── bind_resp_base.rb │ │ ├── generic_nack.rb │ │ ├── submit_sm_response.rb │ │ ├── bind_base.rb │ │ ├── submit_multi_response.rb │ │ ├── submit_multi.rb │ │ ├── submit_sm.rb │ │ ├── deliver_sm.rb │ │ └── base.rb │ ├── optional_parameter.rb │ ├── receiver.rb │ ├── encoding │ │ └── utf8_encoder.rb │ ├── transmitter.rb │ ├── transceiver.rb │ ├── server.rb │ └── base.rb ├── sms.rb └── smpp.rb ├── config └── environment.rb ├── Gemfile ├── Rakefile ├── .gitignore ├── Gemfile.lock ├── CONTRIBUTORS.txt ├── test ├── delegate.rb ├── optional_parameter_test.rb ├── transceiver_test.rb ├── submit_sm_test.rb ├── responsive_delegate.rb ├── server.rb ├── pdu_parsing_test.rb ├── encoding_test.rb ├── receiver_test.rb └── smpp_test.rb ├── examples ├── PDU2.example ├── PDU1.example ├── sample_smsc.rb └── sample_gateway.rb ├── LICENSE ├── ruby-smpp.gemspec ├── CHANGELOG └── README.rdoc /VERSION: -------------------------------------------------------------------------------- 1 | 0.6.0 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 1.9.3-p392 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | -------------------------------------------------------------------------------- /lib/smpp/version.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Smpp 4 | VERSION = "0.6.0" 5 | end 6 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require "rubygems" 4 | require "bundler/setup" 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in ruby-smpp.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /lib/smpp/pdu/bind_receiver.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | class Smpp::Pdu::BindReceiver < Smpp::Pdu::BindBase 3 | @command_id = BIND_RECEIVER 4 | handles_cmd BIND_RECEIVER 5 | end 6 | -------------------------------------------------------------------------------- /lib/smpp/pdu/bind_transceiver.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | class Smpp::Pdu::BindTransceiver < Smpp::Pdu::BindBase 3 | @command_id = BIND_TRANSCEIVER 4 | handles_cmd BIND_TRANSCEIVER 5 | end 6 | -------------------------------------------------------------------------------- /lib/smpp/pdu/bind_transmitter.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | class Smpp::Pdu::BindTransmitter < Smpp::Pdu::BindBase 3 | @command_id = BIND_TRANSMITTER 4 | handles_cmd BIND_TRANSMITTER 5 | end 6 | -------------------------------------------------------------------------------- /lib/sms.rb: -------------------------------------------------------------------------------- 1 | # Basic SMS class for sample gateway 2 | 3 | class Sms 4 | attr_accessor :id, :from, :to, :body 5 | 6 | def initialize(body) 7 | self.body = body 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/smpp/pdu/bind_receiver_response.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | class Smpp::Pdu::BindReceiverResponse < Smpp::Pdu::BindRespBase 3 | @command_id = BIND_RECEIVER_RESP 4 | handles_cmd BIND_RECEIVER_RESP 5 | end 6 | -------------------------------------------------------------------------------- /lib/smpp/pdu/bind_transceiver_response.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | class Smpp::Pdu::BindTransceiverResponse < Smpp::Pdu::BindRespBase 3 | @command_id = BIND_TRANSCEIVER_RESP 4 | handles_cmd BIND_TRANSCEIVER_RESP 5 | end 6 | -------------------------------------------------------------------------------- /lib/smpp/pdu/bind_transmitter_response.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | class Smpp::Pdu::BindTransmitterResponse < Smpp::Pdu::BindRespBase 3 | @command_id = BIND_TRANSMITTER_RESP 4 | handles_cmd BIND_TRANSMITTER_RESP 5 | end 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require "bundler/gem_tasks" 3 | 4 | require 'rake/testtask' 5 | Rake::TestTask.new(:test) do |test| 6 | test.libs << 'lib' << 'test' 7 | test.pattern = 'test/**/*_test.rb' 8 | test.verbose = true 9 | end 10 | 11 | task :default => :test 12 | -------------------------------------------------------------------------------- /lib/smpp/pdu/unbind.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | class Smpp::Pdu::Unbind < Smpp::Pdu::Base 4 | handles_cmd UNBIND 5 | 6 | def initialize(seq=next_sequence_number) 7 | super(UNBIND, 0, seq) 8 | end 9 | 10 | def self.from_wire_data(seq, status, body) 11 | new(seq) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | 4 | ## TEXTMATE 5 | *.tmproj 6 | tmtags 7 | 8 | ## EMACS 9 | *~ 10 | \#* 11 | .\#* 12 | 13 | ## VIM 14 | *.swp 15 | 16 | ## PROJECT::GENERAL 17 | coverage 18 | rdoc 19 | pkg 20 | tags 21 | .bundle 22 | vendor/ruby 23 | .rvmrc 24 | 25 | ## PROJECT::SPECIFIC 26 | *.log 27 | nbproject 28 | -------------------------------------------------------------------------------- /lib/smpp/pdu/enquire_link.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | class Smpp::Pdu::EnquireLink < Smpp::Pdu::Base 4 | handles_cmd ENQUIRE_LINK 5 | 6 | def initialize(seq = next_sequence_number) 7 | super(ENQUIRE_LINK, 0, seq) 8 | end 9 | 10 | def self.from_wire_data(seq, status, body) 11 | new(seq) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/smpp/pdu/enquire_link_response.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | class Smpp::Pdu::EnquireLinkResponse < Smpp::Pdu::Base 4 | handles_cmd ENQUIRE_LINK_RESP 5 | 6 | def initialize(seq = next_sequence_number) 7 | super(ENQUIRE_LINK_RESP, ESME_ROK, seq) 8 | end 9 | 10 | def self.from_wire_data(seq, status, body) 11 | new(seq) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/smpp/pdu/unbind_response.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | class Smpp::Pdu::UnbindResponse < Smpp::Pdu::Base 4 | handles_cmd UNBIND_RESP 5 | 6 | def initialize(seq, status) 7 | seq ||= next_sequence_number 8 | super(UNBIND_RESP, status, seq) 9 | end 10 | 11 | def self.from_wire_data(seq, status, body) 12 | new(seq, status) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | ruby-smpp (0.6.0) 5 | eventmachine (>= 0.10.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | eventmachine (0.12.10) 11 | rake (0.9.2) 12 | test-unit (2.5.4) 13 | 14 | PLATFORMS 15 | ruby 16 | 17 | DEPENDENCIES 18 | bundler (~> 1.3) 19 | rake 20 | ruby-smpp! 21 | test-unit 22 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | Maintainer: 2 | Ray Krueger 3 | August Z. Flatby 4 | 5 | Contributors: 6 | Abhishek Parolkar 7 | Taryn East 8 | Josh Bryan 9 | Jon Wood 10 | Jacob Eiler 11 | Valdis Pornieks 12 | -------------------------------------------------------------------------------- /lib/smpp/pdu/deliver_sm_response.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | class Smpp::Pdu::DeliverSmResponse < Smpp::Pdu::Base 4 | handles_cmd DELIVER_SM_RESP 5 | 6 | def initialize(seq, status=ESME_ROK) 7 | seq ||= next_sequence_number 8 | super(DELIVER_SM_RESP, status, seq, "\000") # body must be NULL..! 9 | end 10 | 11 | def self.from_wire_data(seq, status, body) 12 | new(seq, status) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/smpp/pdu/bind_resp_base.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | class Smpp::Pdu::BindRespBase < Smpp::Pdu::Base 3 | class << self; attr_accessor :command_id ; end 4 | attr_accessor :system_id 5 | 6 | def initialize(seq, status, system_id) 7 | seq ||= next_sequence_number 8 | system_id = system_id.to_s + "\000" 9 | super(self.class.command_id, status, seq, system_id) # pass in system_id as body for simple debugging 10 | @system_id = system_id 11 | end 12 | 13 | def self.from_wire_data(seq, status, body) 14 | system_id = body.chomp("\000") 15 | new(seq, status, system_id) 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /test/delegate.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | # the delagate receives callbacks when interesting things happen on the connection 4 | class Delegate 5 | 6 | def mo_received(transceiver, pdu) 7 | puts "** mo_received" 8 | end 9 | 10 | def delivery_report_received(transceiver, pdu) 11 | puts "** delivery_report_received" 12 | end 13 | 14 | def message_accepted(transceiver, mt_message_id, pdu) 15 | puts "** message_sent" 16 | end 17 | 18 | def message_rejected(transceiver, mt_message_id, pdu) 19 | puts "** message_rejected" 20 | end 21 | 22 | def bound(transceiver) 23 | puts "** bound" 24 | end 25 | 26 | def unbound(transceiver) 27 | puts "** unbound" 28 | EventMachine::stop_event_loop 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/smpp/pdu/generic_nack.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | # signals invalid message header 4 | class Smpp::Pdu::GenericNack < Smpp::Pdu::Base 5 | handles_cmd GENERIC_NACK 6 | 7 | attr_accessor :error_code 8 | 9 | def initialize(seq, error_code, original_sequence_code = nil) 10 | #TODO: original_sequence_code used to be passed to this function 11 | #however, a GENERIC_NACK has only one sequence number and no body 12 | #so this is a useless variable. I leave it here only to preserve 13 | #the API, but it has no practical use. 14 | seq ||= next_sequence_number 15 | super(GENERIC_NACK, error_code, seq) 16 | @error_code = error_code 17 | end 18 | 19 | def self.from_wire_data(seq, status, body) 20 | new(seq,status,body) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/smpp/optional_parameter.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | class Smpp::OptionalParameter 4 | 5 | attr_reader :tag, :value 6 | 7 | def initialize(tag, value) 8 | @tag = tag 9 | @value = value 10 | end 11 | 12 | def [](symbol) 13 | self.send symbol 14 | end 15 | 16 | def to_s 17 | self.inspect 18 | end 19 | 20 | #class methods 21 | class << self 22 | def from_wire_data(data) 23 | 24 | return nil if data.nil? 25 | tag, length, remaining_bytes = data.unpack('H4na*') 26 | tag = tag.hex 27 | 28 | raise "invalid data, cannot parse optional parameters" if tag == 0 or length.nil? 29 | 30 | value = remaining_bytes.slice!(0...length) 31 | 32 | return new(tag, value), remaining_bytes 33 | end 34 | 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /examples/PDU2.example: -------------------------------------------------------------------------------- 1 | 2 | Example PDU when sender address is an alphanumeric keyword 3 | 4 | type_name: submit_sm 5 | command_id: 4 = 0x00000004 6 | command_status: 0 = 0x00000000 7 | sequence_number: 5 = 0x00000005 8 | service_type: "1" 9 | source_addr_ton: 5 = 0x00000005 10 | source_addr_npi: 0 = 0x00000000 11 | source_addr: "RUBYSMPP" 12 | dest_addr_ton: 2 = 0x00000002 13 | dest_addr_npi: 1 = 0x00000001 14 | destination_addr: "919900000000" # number masked 15 | esm_class: 3 = 0x00000003 16 | protocol_id: 0 = 0x00000000 17 | priority_flag: 0 = 0x00000000 18 | schedule_delivery_time: NULL 19 | validity_period: NULL 20 | registered_delivery: 1 = 0x00000001 21 | replace_if_present_flag: 0 = 0x00000000 22 | data_coding: 0 = 0x00000000 23 | sm_default_msg_id: 0 = 0x00000000 24 | sm_length: 5 = 0x00000005 25 | short_message: "hello" 26 | 27 | -------------------------------------------------------------------------------- /examples/PDU1.example: -------------------------------------------------------------------------------- 1 | 2 | This is the PDU being sent when sender address is numeric 3 | 4 | type_name: submit_sm 5 | command_id: 4 = 0x00000004 6 | command_status: 0 = 0x00000000 7 | sequence_number: 3 = 0x00000003 8 | service_type: "1" 9 | source_addr_ton: 2 = 0x00000002 10 | source_addr_npi: 1 = 0x00000001 11 | source_addr: "919900000000" #number masked 12 | dest_addr_ton: 2 = 0x00000002 13 | dest_addr_npi: 1 = 0x00000001 14 | destination_addr: "919900000000" #number masked 15 | esm_class: 3 = 0x00000003 16 | protocol_id: 0 = 0x00000000 17 | priority_flag: 0 = 0x00000000 18 | schedule_delivery_time: NULL 19 | validity_period: NULL 20 | registered_delivery: 1 = 0x00000001 21 | replace_if_present_flag: 0 = 0x00000000 22 | data_coding: 0 = 0x00000000 23 | sm_default_msg_id: 0 = 0x00000000 24 | sm_length: 9 = 0x00000009 25 | short_message: "test smpp" 26 | 27 | -------------------------------------------------------------------------------- /test/optional_parameter_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require 'rubygems' 3 | require 'test/unit' 4 | require 'smpp' 5 | 6 | class OptionalParameterTest < Test::Unit::TestCase 7 | include Smpp 8 | 9 | def test_symbol_accessor 10 | op = OptionalParameter.new(0x2150, "abcd") 11 | assert_equal "abcd", op[:value] 12 | assert_equal 0x2150, op[:tag] 13 | end 14 | 15 | def test_bad_data_does_not_puke 16 | assert_raise RuntimeError do 17 | OptionalParameter.from_wire_data("") 18 | end 19 | end 20 | 21 | def test_from_wire_data 22 | data = "\041\120\000\002\001\177" 23 | op, remaining_data = OptionalParameter.from_wire_data(data) 24 | assert_not_nil op, "op should not be nil" 25 | assert_equal 0x2150, op.tag 26 | assert_equal "\001\177", op.value 27 | assert_equal "", remaining_data 28 | end 29 | 30 | end 31 | 32 | -------------------------------------------------------------------------------- /lib/smpp.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # SMPP v3.4 subset implementation. 3 | # SMPP is a short message peer-to-peer protocol typically used to communicate 4 | # with SMS Centers (SMSCs) over TCP/IP. 5 | # 6 | # August Z. Flatby 7 | # august@apparat.no 8 | 9 | require 'logger' 10 | 11 | $:.unshift(File.dirname(__FILE__)) 12 | require 'smpp/base.rb' 13 | require 'smpp/transceiver.rb' 14 | require 'smpp/transmitter.rb' 15 | require 'smpp/receiver.rb' 16 | require 'smpp/optional_parameter' 17 | require 'smpp/pdu/base.rb' 18 | require 'smpp/pdu/bind_base.rb' 19 | require 'smpp/pdu/bind_resp_base.rb' 20 | 21 | # Load all PDUs 22 | Dir.glob(File.join(File.dirname(__FILE__), 'smpp', 'pdu', '*.rb')) do |f| 23 | require f unless f.match('base.rb$') 24 | end 25 | 26 | # Default logger. Invoke this call in your client to use another logger. 27 | Smpp::Base.logger = Logger.new(STDOUT) 28 | -------------------------------------------------------------------------------- /lib/smpp/receiver.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | # The SMPP Receiver maintains a unidirectional connection to an SMSC. 4 | # Provide a config hash with connection options to get started. 5 | # See the sample_gateway.rb for examples of config values. 6 | # The receiver accepts a delegate object that may implement 7 | # the following (all optional) methods: 8 | # 9 | # mo_received(receiver, pdu) 10 | # delivery_report_received(receiver, pdu) 11 | # bound(receiver) 12 | # unbound(receiver) 13 | 14 | class Smpp::Receiver < Smpp::Base 15 | 16 | # Send BindReceiverResponse PDU. 17 | def send_bind 18 | raise IOError, 'Receiver already bound.' unless unbound? 19 | pdu = Pdu::BindReceiver.new( 20 | @config[:system_id], 21 | @config[:password], 22 | @config[:system_type], 23 | @config[:source_ton], 24 | @config[:source_npi], 25 | @config[:source_address_range]) 26 | write_pdu(pdu) 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /lib/smpp/pdu/submit_sm_response.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | class Smpp::Pdu::SubmitSmResponse < Smpp::Pdu::Base 4 | handles_cmd SUBMIT_SM_RESP 5 | 6 | attr_accessor :message_id 7 | attr_accessor :optional_parameters 8 | 9 | def initialize(seq, status, message_id, optional_parameters=nil) 10 | seq ||= next_sequence_number 11 | body = message_id.to_s + "\000" 12 | super(SUBMIT_SM_RESP, status, seq, body) 13 | @message_id = message_id 14 | @optional_parameters = optional_parameters 15 | end 16 | 17 | def optional_parameter(tag) 18 | if optional_parameters 19 | if param = optional_parameters[tag] 20 | param.value 21 | end 22 | end 23 | end 24 | 25 | def self.from_wire_data(seq, status, body) 26 | message_id, remaining_bytes = body.unpack("Z*a*") 27 | optionals = nil 28 | if remaining_bytes && !remaining_bytes.empty? 29 | optionals = parse_optional_parameters(remaining_bytes) 30 | end 31 | new(seq, status, message_id, optionals) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008 Apparat AS 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/smpp/pdu/bind_base.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # this class serves as the base for all the bind* commands. 3 | # since the command format remains the same for all bind commands, 4 | # sub classes just change the @@command_id 5 | class Smpp::Pdu::BindBase < Smpp::Pdu::Base 6 | class << self; attr_accessor :command_id ; end 7 | 8 | attr_reader :system_id, :password, :system_type, :addr_ton, :addr_npi, :address_range 9 | 10 | def initialize(system_id, password, system_type, addr_ton, addr_npi, address_range, seq = nil) 11 | @system_id, @password, @system_type, @addr_ton, @addr_npi, @address_range = 12 | system_id, password, system_type, addr_ton, addr_npi, address_range 13 | 14 | seq ||= next_sequence_number 15 | body = sprintf("%s\0%s\0%s\0%c%c%c%s\0", system_id, password,system_type, PROTOCOL_VERSION, addr_ton, addr_npi, address_range) 16 | super(self.class.command_id, 0, seq, body) 17 | end 18 | 19 | def self.from_wire_data(seq, status, body) 20 | #unpack the body 21 | system_id, password, system_type, interface_version, addr_ton, 22 | addr_npi, address_range = body.unpack("Z*Z*Z*CCCZ*") 23 | 24 | self.new(system_id, password, system_type, addr_ton, addr_npi, address_range, seq) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /ruby-smpp.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'smpp/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "ruby-smpp" 8 | spec.version = Smpp::VERSION 9 | spec.authors = ["Ray Krueger", "August Z. Flatby"] 10 | spec.email = ["raykrueger@gmail.com"] 11 | spec.description = %q{Ruby implementation of the SMPP protocol, based on EventMachine. SMPP is a protocol that allows ordinary people outside the mobile network to exchange SMS messages directly with mobile operators.} 12 | spec.summary = %q{Ruby implementation of the SMPP protocol, based on EventMachine.} 13 | spec.homepage = "http://github.com/raykrueger/ruby-smpp" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_dependency "eventmachine", ">= 0.10.0" 22 | 23 | spec.add_development_dependency "bundler", "~> 1.3" 24 | spec.add_development_dependency "rake" 25 | spec.add_development_dependency "test-unit" 26 | end 27 | -------------------------------------------------------------------------------- /test/transceiver_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require "rubygems" 3 | require "test/unit" 4 | require File.expand_path(File.dirname(__FILE__) + "../../lib/smpp") 5 | 6 | class TransceiverTest < Test::Unit::TestCase 7 | def test_get_message_part_size_8 8 | options = {:data_coding => 8} 9 | assert_equal(67, Smpp::Transceiver.get_message_part_size(options)) 10 | end 11 | def test_get_message_part_size_0_and_1 12 | options = {:data_coding => 0} 13 | assert_equal(153, Smpp::Transceiver.get_message_part_size(options)) 14 | options = {:data_coding => 1} 15 | assert_equal(153, Smpp::Transceiver.get_message_part_size(options)) 16 | end 17 | def test_get_message_part_size_nil 18 | options = {} 19 | assert_equal(153, Smpp::Transceiver.get_message_part_size(options)) 20 | end 21 | def test_get_message_part_size_other 22 | options = {:data_coding => 3} 23 | assert_equal(134, Smpp::Transceiver.get_message_part_size(options)) 24 | options = {:data_coding => 5} 25 | assert_equal(134, Smpp::Transceiver.get_message_part_size(options)) 26 | options = {:data_coding => 6} 27 | assert_equal(134, Smpp::Transceiver.get_message_part_size(options)) 28 | options = {:data_coding => 7} 29 | assert_equal(134, Smpp::Transceiver.get_message_part_size(options)) 30 | end 31 | def test_get_message_part_size_non_existant_data_coding 32 | options = {:data_coding => 666} 33 | assert_equal(153, Smpp::Transceiver.get_message_part_size(options)) 34 | end 35 | end 36 | 37 | -------------------------------------------------------------------------------- /test/submit_sm_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require 'rubygems' 3 | require 'test/unit' 4 | require 'smpp' 5 | 6 | class SubmitSmTest < Test::Unit::TestCase 7 | 8 | def test_this_is_not_a_real_test 9 | value = [383 >> 8, 383 & 0xff] 10 | optionals = {0x2150 => Smpp::OptionalParameter.new(0x2150, value.pack('cc'))} 11 | pdu = Smpp::Pdu::SubmitSm.new('12345', '54321', "Ba Ba Boosh", {:optional_parameters => optionals}) 12 | Smpp::Base.hex_debug(pdu.data) 13 | puts "PDU DATA", pdu.data.inspect 14 | 15 | end 16 | 17 | def test_fixnum_optional_parameter 18 | value = [383 >> 8, 383 & 0xff] 19 | optionals = {0x2150 => Smpp::OptionalParameter.new(0x2150, value.pack('cc'))} 20 | 21 | pdu = Smpp::Pdu::SubmitSm.new('12345', '54321', "Ba Ba Boosh", {:optional_parameters => optionals}) 22 | pdu_from_wire = Smpp::Pdu::Base.create(pdu.data) 23 | 24 | assert optional = pdu_from_wire.optional_parameters[0x2150] 25 | 26 | optional_value = optional[:value].unpack('n')[0] 27 | assert_equal 383, optional_value 28 | end 29 | 30 | def test_string_optional_parameter 31 | optionals = {0x2150 => Smpp::OptionalParameter.new(0x2150, "boosh")} 32 | 33 | pdu = Smpp::Pdu::SubmitSm.new('12345', '54321', "Ba Ba Boosh", {:optional_parameters => optionals}) 34 | pdu_from_wire = Smpp::Pdu::Base.create(pdu.data) 35 | 36 | assert optional = pdu_from_wire.optional_parameters[0x2150] 37 | 38 | optional_value = optional[:value].unpack("A*")[0] 39 | assert_equal 'boosh', optional_value 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/responsive_delegate.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | #TODO This should be made prettier with mocha 3 | class ResponsiveDelegate 4 | attr_reader :seq, :event_counter 5 | 6 | def initialize 7 | @seq = 0 8 | @event_counter = nil 9 | end 10 | def seq 11 | @seq += 1 12 | end 13 | def count_function 14 | func = caller(1)[0].split("`")[1].split("'")[0].to_sym 15 | @event_counter = {} unless @event_counter.is_a?(Hash) 16 | @event_counter[func] = 0 if @event_counter[func].nil? 17 | @event_counter[func]+=1 18 | end 19 | 20 | def mo_received(transceiver, pdu) 21 | count_function 22 | puts "** mo_received" 23 | end 24 | 25 | def delivery_report_received(transceiver, pdu) 26 | count_function 27 | puts "** delivery_report_received" 28 | end 29 | 30 | def message_accepted(transceiver, mt_message_id, pdu) 31 | count_function 32 | puts "** message_sent" 33 | #sending messages from delegate to escape making a fake message sender - not nice :( 34 | $tx.send_mt(self.seq, 1, 2, "short_message @ message_accepted") 35 | end 36 | 37 | def message_rejected(transceiver, mt_message_id, pdu) 38 | count_function 39 | puts "** message_rejected" 40 | $tx.send_mt(self.seq, 1, 2, "short_message @ message_rejected") 41 | end 42 | 43 | def bound(transceiver) 44 | count_function 45 | puts "** bound" 46 | $tx.send_mt(self.seq, 1, 2, "short_message @ bound") 47 | end 48 | 49 | def unbound(transceiver) 50 | count_function 51 | puts "** unbound" 52 | EventMachine::stop_event_loop 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/smpp/encoding/utf8_encoder.rb: -------------------------------------------------------------------------------- 1 | #encoding: ASCII-8BIT 2 | require 'iconv' if RUBY_VERSION =~ /\A1\.(8|9)/ 3 | 4 | module Smpp 5 | module Encoding 6 | 7 | # This class is not required by smpp.rb at all, you need to bring it in yourself. 8 | # This class also requires iconv, you'll need to ensure it is installed. 9 | class Utf8Encoder 10 | 11 | EURO_TOKEN = "_X_EURO_X_" 12 | 13 | GSM_ESCAPED_CHARACTERS = { 14 | ?( => "\173", # { 15 | ?) => "\175", # } 16 | 184 => "\174", # | 17 | ?< => "\133", # [ 18 | ?> => "\135", # ] 19 | ?= => "\176", # ~ 20 | ?/ => "\134", # \ 21 | 134 => "\136", # ^ 22 | ?e => EURO_TOKEN 23 | } 24 | 25 | def encode(data_coding, short_message) 26 | if data_coding < 2 27 | sm = short_message.gsub(/\215./) do |match| 28 | lookup = match[1] 29 | alternate_lookup = lookup.bytes.first if has_encoding?(lookup) 30 | GSM_ESCAPED_CHARACTERS[lookup] || GSM_ESCAPED_CHARACTERS[alternate_lookup] 31 | end 32 | sm = Iconv.conv("UTF-8", "HP-ROMAN8", sm) 33 | euro_token = "\342\202\254" 34 | euro_token.force_encoding("UTF-8") if has_encoding?(euro_token) 35 | sm.gsub(EURO_TOKEN, euro_token) 36 | elsif data_coding == 8 37 | Iconv.conv("UTF-8", "UTF-16BE", short_message) 38 | else 39 | short_message 40 | end 41 | end 42 | 43 | private 44 | 45 | def has_encoding?(str) 46 | str.respond_to?(:encoding) 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/smpp/pdu/submit_multi_response.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | # Recieving response for an MT message sent to multiple addresses 4 | # Author: Abhishek Parolkar, (abhishek[at]parolkar.com) 5 | class Smpp::Pdu::SubmitMultiResponse < Smpp::Pdu::Base 6 | class UnsuccessfulSme 7 | Struct.new(:dest_addr_ton, :dest_addr_npi, :destination_addr, :error_status_code) 8 | end 9 | 10 | handles_cmd SUBMIT_MULTI_RESP 11 | 12 | attr_accessor :message_id, :unsuccess_smes 13 | 14 | def initialize(seq, status, message_id, unsuccess_smes = []) 15 | @unsuccess_smes = unsuccess_smes 16 | seq ||= next_sequence_number 17 | 18 | packed_smes = "" 19 | unsuccess_smes.each do |sme| 20 | packed_smes << [ 21 | sme.dest_addr_ton, 22 | sme.dest_addr_npi, 23 | sme.destination_addr, 24 | sme.error_status_code 25 | ].pack("CCZ*N") 26 | end 27 | body = [message_id, unsuccess_smes.size, packed_smes].pack("Z*Ca*") 28 | 29 | super(SUBMIT_MULTI_RESP, status, seq, body) 30 | @message_id = message_id 31 | end 32 | 33 | def self.from_wire_data(seq, status, body) 34 | message_id, no_unsuccess, rest = body.unpack("Z*Ca*") 35 | unsuccess_smes = [] 36 | 37 | no_unsuccess.times do |i| 38 | #unpack the next sme 39 | dest_addr_ton, dest_addr_npi, destination_addr, error_status_code = 40 | rest.unpack("CCZ*N") 41 | #remove the SME from rest 42 | rest.slice!(0,7 + destination_addr.length) 43 | unsuccess_smes << UnsuccessfulSme.new(dest_addr_ton, dest_addr_npi, destination_addr, error_status_code) 44 | end 45 | 46 | new(seq, status, message_id, unsuccess_smes) 47 | end 48 | 49 | 50 | 51 | end 52 | -------------------------------------------------------------------------------- /test/server.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # a server which immediately requests the client to unbind 3 | module Server 4 | def self.config 5 | { 6 | :host => 'localhost', 7 | :port => 2775, 8 | :system_id => 'foo', 9 | :password => 'bar', 10 | :system_type => '', 11 | :source_ton => 0, 12 | :source_npi => 1, 13 | :destination_ton => 1, 14 | :destination_npi => 1, 15 | :source_address_range => '', 16 | :destination_address_range => '' 17 | } 18 | end 19 | 20 | module Unbind 21 | def receive_data(data) 22 | send_data Smpp::Pdu::Unbind.new.data 23 | end 24 | end 25 | 26 | module SubmitSmResponse 27 | def receive_data(data) 28 | # problem: our Pdu's should have factory methods for "both ways"; ie. when created 29 | # by client, and when created from wire data. 30 | send_data Smpp::Pdu::SubmitSmResponse.new(1, 2, "100").data 31 | end 32 | end 33 | 34 | module SubmitSmResponseWithErrorStatus 35 | attr_reader :state #state=nil => bind => state=bound => send =>state=sent => unbind => state=unbound 36 | def receive_data(data) 37 | if @state.nil? 38 | @state = 'bound' 39 | pdu = Smpp::Pdu::Base.create(data) 40 | response_pdu = Smpp::Pdu::BindTransceiverResponse.new(pdu.sequence_number,Smpp::Pdu::Base::ESME_ROK,Server::config[:system_id]) 41 | send_data response_pdu.data 42 | elsif @state == 'bound' 43 | @state = 'sent' 44 | pdu = Smpp::Pdu::Base.create(data) 45 | pdu.to_human 46 | send_data Smpp::Pdu::SubmitSmResponse.new(pdu.sequence_number, Smpp::Pdu::Base::ESME_RINVDSTADR, pdu.body).data 47 | #send_data Smpp::Pdu::SubmitSmResponse.new(1, 2, "100").data 48 | elsif @state == 'sent' 49 | @state = 'unbound' 50 | send_data Smpp::Pdu::Unbind.new.data 51 | else 52 | raise "unexpected state" 53 | end 54 | end 55 | end 56 | 57 | end 58 | -------------------------------------------------------------------------------- /lib/smpp/transmitter.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # The SMPP Transmitter maintains a unidirectional connection to an SMSC. 3 | # Provide a config hash with connection options to get started. 4 | # See the sample_gateway.rb for examples of config values. 5 | 6 | class Smpp::Transmitter < Smpp::Base 7 | 8 | attr_reader :ack_ids 9 | 10 | # Send an MT SMS message. Delegate will receive message_accepted callback when SMSC 11 | # acknowledges, or the message_rejected callback upon error 12 | def send_mt(message_id, source_addr, destination_addr, short_message, options={}) 13 | logger.debug "Sending MT: #{short_message}" 14 | if @state == :bound 15 | pdu = Pdu::SubmitSm.new(source_addr, destination_addr, short_message, options) 16 | write_pdu(pdu) 17 | 18 | # keep the message ID so we can associate the SMSC message ID with our message 19 | # when the response arrives. 20 | @ack_ids[pdu.sequence_number] = message_id 21 | else 22 | raise InvalidStateException, "Transmitter is unbound. Cannot send MT messages." 23 | end 24 | end 25 | 26 | def send_concat_mt(message_id, source_addr, destination_addr, message, options = {}) 27 | if @state == :bound 28 | # Split the message into parts of 134 characters. 29 | parts = [] 30 | while message.size > 0 do 31 | parts << message.slice!(0..133) 32 | end 33 | 0.upto(parts.size-1) do |i| 34 | udh = sprintf("%c", 5) # UDH is 5 bytes. 35 | udh << sprintf("%c%c", 0, 3) # This is a concatenated message 36 | udh << sprintf("%c", message_id) # The ID for the entire concatenated message 37 | udh << sprintf("%c", parts.size) # How many parts this message consists of 38 | 39 | udh << sprintf("%c", i+1) # This is part i+1 40 | 41 | combined_options = { 42 | :esm_class => 64, # This message contains a UDH header. 43 | :udh => udh 44 | }.merge(options) 45 | 46 | pdu = Smpp::Pdu::SubmitSm.new(source_addr, destination_addr, parts[i], combined_options) 47 | write_pdu(pdu) 48 | end 49 | else 50 | raise InvalidStateException, "Transmitter is unbound. Cannot send MT messages." 51 | end 52 | end 53 | 54 | def send_bind 55 | raise IOError, 'Transmitter already bound.' unless unbound? 56 | pdu = Pdu::BindTransmitter.new( 57 | @config[:system_id], 58 | @config[:password], 59 | @config[:system_type], 60 | @config[:source_ton], 61 | @config[:source_npi], 62 | @config[:source_address_range]) 63 | write_pdu(pdu) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | = 0.3.0 2 | * Added begin/rescue handling in receive_data to keep the connection alive. Delegates can implement data_error(Exception) and re-raise the error if they wish to break the connection. 3 | 4 | = 0.1.3 5 | * Transition maintenance from August to Ray Krueger 6 | * Changed the expected signature of transceiver delegates to use the full PDU instead of picking off attributes one by one 7 | * Added support for parsing optional parameters from delivery receipts in deliver_sm PDUs (this is also the fix for trailing bytes from MOs) 8 | 9 | = 0.1.2 2009.01.22 10 | 11 | == Delegate methods 12 | 13 | - Some API breakage has been introduced in order to allow a delegate callback model from the transceiver. Also, the Receiver and Transmitter classes have been removed due to bad code reuse. 14 | 15 | = 2008.12.11 16 | 17 | == Updates from Jon Wood: 18 | 19 | - Closes connections cleanly instead of stopping the event loop. 20 | - Adds a send_concat_mt method to Transceivers (not implemented for Transmitter/Receiver), which does the neccesary encoding and message splitting to send messages > 160 characters by splitting them into multiple parts. 21 | 22 | = 2008.10.20 23 | 24 | == Patch from Josh Bryan: 25 | 26 | Fixes smpp/base.rb so that it can handle partial pdu's and multiple pdu's available on the stream. Previously, base.rb assumed that all data available on the TCP stream constituted the next pdu. Though this often works in low load environments, moderately high traffic environments often see multiple pdu's back to back on the stream before event machine cycles through another turn of the reactor. Also, pdu's can be split over multiple TCP packets. Thus, incomplete or multiple pdu's may be delivered to Base#receive_data, and Base needs to be able buffer and split the pdu's. 27 | 28 | = 2008.10.13 29 | 30 | == Patch from Taryn East: 31 | 32 | I added the receiver and transmitter equivalents of the transmitter class, and also hacked in some code that would support basic setup of an smpp server. Note: we were only using this to set up a test-harness for our own ruby-smpp code, so it's pretty basic and doesn't support all messages - or even do anything useful with an incoming deliver_sm. But it's better than nothing at all ;) 33 | 34 | NOTE: Taryn's patch currently lives in a separate branch (taryn-patch), awaiting merge. 35 | 36 | = 2008.06.25 37 | 38 | == Check-ins from Abhishek Parolkar 39 | 40 | I have implemented 4.5.1 section of SMPPv3.4: support for sending an MT message to multiple destination addresses. 41 | 42 | - Added system_type paramenter to BindTransceiver.new 43 | - Small change in submit_sm to discard '@' char in message push 44 | - Made submit_sm more configurable with options 45 | - Added PDU examples for submit_sm 46 | - Added example to use submit_multi and submit_sm 47 | 48 | = 2008.04.07 49 | 50 | == Initial release (August Z. Flatby) 51 | 52 | My first open source project after all these years! Ah.. it feels good. 53 | -------------------------------------------------------------------------------- /examples/sample_smsc.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # encoding: UTF-8 3 | 4 | 5 | # Sample SMPP SMS Gateway. 6 | 7 | require 'rubygems' 8 | require File.dirname(__FILE__) + '/../lib/smpp' 9 | require File.dirname(__FILE__) + '/../lib/smpp/server' 10 | 11 | # set up logger 12 | Smpp::Base.logger = Logger.new('smsc.log') 13 | 14 | # the transceiver 15 | $tx = nil 16 | 17 | # We use EventMachine to receive keyboard input (which we send as MT messages). 18 | # A "real" gateway would probably get its MTs from a message queue instead. 19 | module KeyboardHandler 20 | include EventMachine::Protocols::LineText2 21 | 22 | def receive_line(data) 23 | puts "Sending MO: #{data}" 24 | from = '1111111111' 25 | to = '1111111112' 26 | $tx.send_mo(from, to, data) 27 | 28 | 29 | # if you want to send messages with custom options, uncomment below code, this configuration allows the sender ID to be alpha numeric 30 | # $tx.send_mt(123, "RubySmpp", to, "Testing RubySmpp as sender id",{ 31 | # :source_addr_ton=> 5, 32 | # :service_type => 1, 33 | # :source_addr_ton => 5, 34 | # :source_addr_npi => 0 , 35 | # :dest_addr_ton => 2, 36 | # :dest_addr_npi => 1, 37 | # :esm_class => 3 , 38 | # :protocol_id => 0, 39 | # :priority_flag => 0, 40 | # :schedule_delivery_time => nil, 41 | # :validity_period => nil, 42 | # :registered_delivery=> 1, 43 | # :replace_if_present_flag => 0, 44 | # :data_coding => 0, 45 | # :sm_default_msg_id => 0 46 | # }) 47 | 48 | # if you want to send message to multiple destinations , uncomment below code 49 | # $tx.send_multi_mt(123, from, ["919900000001","919900000002","919900000003"], "I am echoing that ruby-smpp is great") 50 | prompt 51 | end 52 | end 53 | 54 | def prompt 55 | print "Enter MO body: " 56 | $stdout.flush 57 | end 58 | 59 | def logger 60 | Smpp::Base.logger 61 | end 62 | 63 | def start(config) 64 | 65 | # Run EventMachine in loop so we can reconnect when the SMSC drops our connection. 66 | loop do 67 | EventMachine::run do 68 | $tx = EventMachine::start_server( 69 | config[:host], 70 | config[:port], 71 | Smpp::Server, 72 | config 73 | ) 74 | end 75 | logger.warn "Event loop stopped. Restarting in 5 seconds.." 76 | sleep 5 77 | end 78 | end 79 | 80 | # Start the Gateway 81 | begin 82 | puts "Starting SMS Gateway" 83 | 84 | # SMPP properties. These parameters the ones provided sample_gateway.rb and 85 | # will work with it. 86 | config = { 87 | :host => 'localhost', 88 | :port => 6000, 89 | :system_id => 'hugo', 90 | :password => 'ggoohu', 91 | :system_type => 'vma', # default given according to SMPP 3.4 Spec 92 | :interface_version => 52, 93 | :source_ton => 0, 94 | :source_npi => 1, 95 | :destination_ton => 1, 96 | :destination_npi => 1, 97 | :source_address_range => '', 98 | :destination_address_range => '', 99 | :enquire_link_delay_secs => 10 100 | } 101 | start(config) 102 | rescue Exception => ex 103 | puts "Exception in SMS Gateway: #{ex} at #{ex.backtrace[0]}" 104 | end 105 | -------------------------------------------------------------------------------- /lib/smpp/pdu/submit_multi.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | # Sending an MT message to multiple addresses 4 | # Author: Abhishek Parolkar, (abhishek[at]parolkar.com) 5 | #TODO: Implement from_wire_data for this pdu class. 6 | class Smpp::Pdu::SubmitMulti < Smpp::Pdu::Base 7 | IS_SMEADDR = 1 # type of dest_flag 8 | IS_DISTLISTNAME = 2 #type of dest_flag 9 | 10 | # Note: short_message (the SMS body) must be in iso-8859-1 format 11 | def initialize(source_addr, destination_addr_array, short_message, options={}) 12 | options.merge!( 13 | :esm_class => 0, # default smsc mode 14 | :dcs => 3 # iso-8859-1 15 | ) { |key, old_val, new_val| old_val } 16 | 17 | @msg_body = short_message 18 | 19 | udh = options[:udh] 20 | service_type = '' 21 | source_addr_ton = 0 # network specific 22 | source_addr_npi = 1 # unknown 23 | number_of_dests = destination_addr_array.length # Max value can be 254 24 | dest_addr_ton = 1 # international 25 | dest_addr_npi = 1 # unknown 26 | dest_addresses = build_destination_addresses(destination_addr_array,dest_addr_ton,dest_addr_npi,IS_SMEADDR) 27 | esm_class = options[:esm_class] 28 | protocol_id = 0 29 | priority_flag = 0 30 | schedule_delivery_time = '' 31 | validity_period = '' 32 | registered_delivery = 1 # we want delivery notifications 33 | replace_if_present_flag = 0 34 | data_coding = options[:dcs] 35 | sm_default_msg_id = 0 36 | payload = udh ? udh + short_message : short_message # this used to be (short_message + "\0") 37 | sm_length = payload.length 38 | 39 | # craft the string/byte buffer 40 | pdu_body = sprintf("%s\0%c%c%s\0%c%s\0%c%c%c%s\0%s\0%c%c%c%c%c%s", service_type, source_addr_ton, source_addr_npi, source_addr, number_of_dests,dest_addresses, esm_class, protocol_id, priority_flag, schedule_delivery_time, validity_period, 41 | registered_delivery, replace_if_present_flag, data_coding, sm_default_msg_id, sm_length, payload) 42 | super(SUBMIT_MULTI, 0, next_sequence_number, pdu_body) 43 | end 44 | 45 | # some special formatting is needed for SubmitSm PDUs to show the actual message content 46 | def to_human 47 | # convert header (4 bytes) to array of 4-byte ints 48 | a = @data.to_s.unpack('N4') 49 | sprintf("(%22s) len=%3d cmd=%8s status=%1d seq=%03d (%s)", self.class.to_s[11..-1], a[0], a[1].to_s(16), a[2], a[3], @msg_body[0..30]) 50 | end 51 | 52 | def build_destination_addresses(dest_array,dest_addr_ton,dest_addr_npi, dest_flag = IS_SMEADDR) 53 | formatted_array = Array.new 54 | dest_array.each { |dest_elem| 55 | if dest_flag == IS_SMEADDR 56 | packet_str = sprintf("%c%c%c%s",IS_SMEADDR,dest_addr_ton,dest_addr_npi,dest_elem) 57 | formatted_array.push(packet_str) 58 | 59 | elsif dest_flag == IS_DISTLISTNAME 60 | packet_str = sprintf("%c%s",IS_SMEADDR,dest_elem) 61 | formatted_array.push(packet_str) 62 | 63 | end 64 | 65 | } 66 | 67 | formatted_array.join("\0"); 68 | end 69 | 70 | end 71 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Ruby-SMPP 2 | 3 | {}[https://travis-ci.org/raykrueger/ruby-smpp] 4 | 5 | == DESCRIPTION: 6 | 7 | Ruby-SMPP is a Ruby implementation of the SMPP v3.4 protocol. It is suitable for writing gateway daemons that communicate with SMSCs for sending and receiving SMS messages. 8 | 9 | The implementation is based on the Ruby/EventMachine library. 10 | 11 | NOTE: Breaking change from 0.1.2 to 0.1.3. See below. 12 | 13 | For help please use the Google group here: http://groups.google.com/group/ruby-smpp/topics 14 | === Glossary 15 | 16 | 17 | * SMSC: SMS Center. Mobile operators normally operate an SMSC in their network. The SMSC stores and forwards SMS messages. 18 | * MO: Mobile Originated SMS: originated in a mobile phone, ie. sent by an end user. 19 | * MT: Mobile Terminated SMS: terminated in a mobile phone, ie. received by an end user. 20 | * DR: Delivery Report, or delivery notification. When you send an MT message, you should receive a DR after a while. 21 | * PDU: Protcol Base Unit, the data units that SMPP is comprised of. This implementation does _not_ implement all SMPP PDUs. 22 | 23 | === Protocol 24 | 25 | The SMPP 3.4 protocol spec can be downloaded here: http://smsforum.net/SMPP_v3_4_Issue1_2.zip 26 | 27 | === Testing/Sample Code 28 | 29 | Logica provides an SMPP simulator that you can download from http://opensmpp.logica.com. You can 30 | also sign up for a demo SMPP account at one of the many bulk-SMS providers out there. 31 | 32 | For a quick test, download smscsim.jar and smpp.jar from the Logica site, and start the simulator by typing: 33 | 34 | java -cp smscsim.jar:smpp.jar com.logica.smscsim.Simulator 35 | 36 | 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: 37 | 38 | ruby sample_gateway.rb 39 | 40 | You will be able to send MT messages from the sample gateway terminal window by typing the message body. In the simulator terminal window you should see SMPP PDUs being sent from the sample gateway. 41 | 42 | You can also send MO messages from the simulator to the sample gateway by typing 7 (log to screen off) and then 4 (send message). MO messages received by the sample gateway will be logged to ./sms_gateway.log. 43 | 44 | == FEATURES/PROBLEMS: 45 | 46 | 47 | * Implements only typical client subset of SMPP 3.4 with single-connection Transceiver. 48 | * Contributors are encouraged to add missing PDUs. 49 | 50 | == BASIC USAGE: 51 | 52 | Start the transceiver. Receive delegate callbacks whenever incoming messages or delivery reports arrive. Send messages with Transceiver#send_mt. 53 | 54 | # connect to SMSC 55 | tx = EventMachine::run do 56 | $tx = EventMachine::connect( 57 | host, 58 | port, 59 | Smpp::Transceiver, 60 | config, # a property hash 61 | delegate # delegate class that will receive callbacks on MOs and DRs and other events 62 | end 63 | 64 | # send a message 65 | tx.send_mt(id, from, to, body) 66 | 67 | As of 0.1.3 the delegate method signatures are as follows: 68 | * mo_received(transceiver, deliver_sm_pdu) 69 | * delivery_report_received(transceiver, deliver_sm_pdu) 70 | * message_accepted(transceiver, mt_message_id, submit_sm_response_pdu) 71 | * message_rejected(transceiver, mt_message_id, submit_sm_response_pdu) 72 | * bound(transceiver) 73 | * unbound(transceiver) 74 | 75 | Where 'pdu' above is the actual actual instance of the Smpp:Pdu:: class created. 76 | 77 | For a more complete example, see examples/sample_gateway.rb 78 | 79 | == REQUIREMENTS: 80 | 81 | 82 | * Eventmachine >= 0.10.0 83 | 84 | == INSTALL: 85 | 86 | sudo gem install ruby-smpp 87 | 88 | == LICENSE: 89 | 90 | Copyright (c) 2008 Apparat AS 91 | Released under the MIT license. 92 | -------------------------------------------------------------------------------- /test/pdu_parsing_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require 'rubygems' 3 | require 'test/unit' 4 | require File.expand_path(File.dirname(__FILE__) + "../../lib/smpp") 5 | 6 | class PduParsingTest < Test::Unit::TestCase 7 | 8 | def test_recieve_single_message 9 | raw_data = <<-EOF 10 | 0000 003d 0000 0005 0000 0000 0000 0002 11 | 0001 0134 3437 3830 3330 3239 3833 3700 12 | 0101 3434 3738 3033 3032 3938 3337 0000 13 | 0000 0000 0000 0000 0454 6573 74 14 | EOF 15 | 16 | pdu = create_pdu(raw_data) 17 | assert_equal Smpp::Pdu::DeliverSm, pdu.class 18 | assert_equal "447803029837", pdu.source_addr 19 | assert_equal "447803029837", pdu.destination_addr 20 | assert_nil pdu.udh 21 | assert_equal "Test", pdu.short_message 22 | end 23 | 24 | def test_recieve_part_one_of_multi_part_message 25 | part_one_message = <<-EOF 26 | 0000 00d8 0000 0005 0000 0000 0000 0001 27 | 0001 0134 3437 3937 3334 3238 3634 3400 28 | 0101 3434 3739 3736 3232 3430 3137 0000 29 | 0000 0000 0000 0000 9f05 0003 b402 0154 30 | 6869 7320 6973 2061 206c 6f6e 6720 6d65 31 | 7373 6167 6520 746f 2074 6573 7420 7768 32 | 6574 6865 7220 6f72 206e 6f74 2077 6520 33 | 6765 7420 7468 6520 6865 6164 6572 2069 34 | 6e66 6f20 7669 6120 7468 6520 534d 5343 35 | 2074 6861 7420 7765 2077 6f75 6c64 2072 36 | 6571 7569 7265 2074 6f20 6265 2061 626c 37 | 6520 746f 2072 6563 6f6d 706f 7365 206c 38 | 6f6e 6720 6d65 7373 6167 6573 2069 6e20 39 | 6861 7368 626c 7565 40 | EOF 41 | 42 | pdu = create_pdu(part_one_message) 43 | assert_equal Smpp::Pdu::DeliverSm, pdu.class 44 | assert_equal "447973428644", pdu.source_addr 45 | assert_equal "447976224017", pdu.destination_addr 46 | assert_equal [5, 0, 3, 180, 2, 1], pdu.udh 47 | 48 | assert_equal 2, pdu.total_parts, "Have total parts of the message" 49 | assert_equal 1, pdu.part, "Correctly show the part" 50 | assert_equal 180, pdu.message_id 51 | 52 | assert_equal "This is a long message to test whether or not we get the header info via the SMSC that we would require to be able to recompose long messages in hashblue", pdu.short_message 53 | end 54 | 55 | def test_recieve_part_two_of_multi_part_message 56 | part_one_message = <<-EOF 57 | 0000 0062 0000 0005 0000 0000 0000 0002 58 | 0001 0134 3437 3937 3334 3238 3634 3400 59 | 0101 3434 3739 3736 3232 3430 3137 0000 60 | 0000 0000 0000 0000 2905 0003 b402 0220 61 | 616e 6420 7072 6f76 6964 6520 6120 676f 62 | 6f64 2075 7365 7220 6578 7065 7269 656e 63 | 6365 64 | EOF 65 | 66 | pdu = create_pdu(part_one_message) 67 | assert_equal Smpp::Pdu::DeliverSm, pdu.class 68 | assert_equal "447973428644", pdu.source_addr 69 | assert_equal "447976224017", pdu.destination_addr 70 | assert_equal [5, 0, 3, 180, 2, 2], pdu.udh 71 | 72 | assert_equal 2, pdu.total_parts, "Have total parts of the message" 73 | assert_equal 2, pdu.part, "Correctly show the part" 74 | assert_equal 180, pdu.message_id 75 | 76 | assert_equal " and provide a good user experience", pdu.short_message 77 | end 78 | 79 | def test_submit_sm_response_clean 80 | data = <<-EOF 81 | 0000 0028 8000 0004 0000 0000 4da8 ebed 82 | 3534 3131 342d 3034 3135 562d 3231 3230 83 | 452d 3039 4831 5100 84 | EOF 85 | 86 | pdu = create_pdu(data) 87 | assert_equal Smpp::Pdu::SubmitSmResponse, pdu.class 88 | assert_equal "54114-0415V-2120E-09H1Q", pdu.message_id 89 | end 90 | 91 | def test_submit_sm_response_with_optional_params 92 | data = <<-EOF 93 | 0000 0031 8000 0004 0000 042e 4da8 e9a1 94 | 0021 5300 0201 7721 6700 1653 6f75 7263 95 | 6520 6164 6472 6573 7320 6465 6e69 6564 96 | 2e 97 | EOF 98 | 99 | pdu = create_pdu(data) 100 | assert_equal Smpp::Pdu::SubmitSmResponse, pdu.class 101 | assert_equal "", pdu.message_id 102 | assert pdu.optional_parameters 103 | assert_equal "Source address denied.", pdu.optional_parameter(0x2167) 104 | end 105 | 106 | protected 107 | def create_pdu(raw_data) 108 | hex_data = [raw_data.chomp.gsub(/\s/,"")].pack("H*") 109 | Smpp::Pdu::Base.create(hex_data) 110 | end 111 | 112 | end 113 | -------------------------------------------------------------------------------- /lib/smpp/pdu/submit_sm.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | # Sending an MT message 4 | class Smpp::Pdu::SubmitSm < Smpp::Pdu::Base 5 | handles_cmd SUBMIT_SM 6 | attr_reader :service_type, :source_addr_ton, :source_addr_npi, :source_addr, :dest_addr_ton, :dest_addr_npi, 7 | :destination_addr, :esm_class, :protocol_id, :priority_flag, :schedule_delivery_time, 8 | :validity_period, :registered_delivery, :replace_if_present_flag, :data_coding, 9 | :sm_default_msg_id, :sm_length, :udh, :short_message, :optional_parameters 10 | 11 | 12 | # Note: short_message (the SMS body) must be in iso-8859-1 format 13 | def initialize(source_addr, destination_addr, short_message, options={}, seq = nil) 14 | 15 | @msg_body = short_message 16 | 17 | @udh = options[:udh] 18 | @service_type = options[:service_type]? options[:service_type] :'' 19 | @source_addr_ton = options[:source_addr_ton]?options[:source_addr_ton]:0 # network specific 20 | @source_addr_npi = options[:source_addr_npi]?options[:source_addr_npi]:1 # unknown 21 | @source_addr = source_addr.to_s 22 | @dest_addr_ton = options[:dest_addr_ton]?options[:dest_addr_ton]:1 # international 23 | @dest_addr_npi = options[:dest_addr_npi]?options[:dest_addr_npi]:1 # unknown 24 | @destination_addr = destination_addr.to_s 25 | @esm_class = options[:esm_class]?options[:esm_class]:0 # default smsc mode 26 | @protocol_id = options[:protocol_id]?options[:protocol_id]:0 27 | @priority_flag = options[:priority_flag]?options[:priority_flag]:0 28 | @schedule_delivery_time = options[:schedule_delivery_time]?options[:schedule_delivery_time]:'' 29 | @validity_period = options[:validity_period]?options[:validity_period]:'' 30 | @registered_delivery = options[:registered_delivery]?options[:registered_delivery]:1 # we want delivery notifications 31 | @replace_if_present_flag = options[:replace_if_present_flag]?options[:replace_if_present_flag]:0 32 | @data_coding = options[:data_coding]?options[:data_coding]:3 # iso-8859-1 33 | @sm_default_msg_id = options[:sm_default_msg_id]?options[:sm_default_msg_id]:0 34 | @short_message = short_message 35 | payload = @udh ? @udh + @short_message : @short_message 36 | @sm_length = payload.length 37 | 38 | @optional_parameters = options[:optional_parameters] 39 | 40 | # craft the string/byte buffer 41 | pdu_body = [@service_type, @source_addr_ton, @source_addr_npi, @source_addr, @dest_addr_ton, @dest_addr_npi, @destination_addr, 42 | @esm_class, @protocol_id, @priority_flag, @schedule_delivery_time, @validity_period, @registered_delivery, 43 | @replace_if_present_flag, @data_coding, @sm_default_msg_id, @sm_length, payload].pack("A*xCCA*xCCA*xCCCA*xA*xCCCCCA*") 44 | 45 | if @optional_parameters 46 | pdu_body << optional_parameters_to_buffer(@optional_parameters) 47 | end 48 | 49 | seq ||= next_sequence_number 50 | 51 | super(SUBMIT_SM, 0, seq, pdu_body) 52 | end 53 | 54 | # some special formatting is needed for SubmitSm PDUs to show the actual message content 55 | def to_human 56 | # convert header (4 bytes) to array of 4-byte ints 57 | a = @data.to_s.unpack('N4') 58 | sprintf("(%22s) len=%3d cmd=%8s status=%1d seq=%03d (%s)", self.class.to_s[11..-1], a[0], a[1].to_s(16), a[2], a[3], @msg_body[0..30]) 59 | end 60 | 61 | def self.from_wire_data(seq, status, body) 62 | options = {} 63 | 64 | options[:service_type], 65 | options[:source_addr_ton], 66 | options[:source_addr_npi], 67 | source_addr, 68 | options[:dest_addr_ton], 69 | options[:dest_addr_npi], 70 | destination_addr, 71 | options[:esm_class], 72 | options[:protocol_id], 73 | options[:priority_flag], 74 | options[:schedule_delivery_time], 75 | options[:validity_period], 76 | options[:registered_delivery], 77 | options[:replace_if_present_flag], 78 | options[:data_coding], 79 | options[:sm_default_msg_id], 80 | options[:sm_length], 81 | remaining_bytes = body.unpack('Z*CCZ*CCZ*CCCZ*Z*CCCCCa*') 82 | 83 | short_message = remaining_bytes.slice!(0...options[:sm_length]) 84 | 85 | #everything left in remaining_bytes is 3.4 optional parameters 86 | options[:optional_parameters] = parse_optional_parameters(remaining_bytes) 87 | 88 | Smpp::Base.logger.debug "SubmitSM with source_addr=#{source_addr}, destination_addr=#{destination_addr}" 89 | 90 | new(source_addr, destination_addr, short_message, options, seq) 91 | end 92 | 93 | end 94 | -------------------------------------------------------------------------------- /examples/sample_gateway.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # encoding: UTF-8 3 | 4 | 5 | # Sample SMS gateway that can receive MOs (mobile originated messages) and 6 | # DRs (delivery reports), and send MTs (mobile terminated messages). 7 | # MTs are, in the name of simplicity, entered on the command line in the format 8 | # 9 | # MOs and DRs will be dumped to standard out. 10 | 11 | require 'rubygems' 12 | require File.dirname(__FILE__) + '/../lib/smpp' 13 | 14 | LOGFILE = File.dirname(__FILE__) + "/sms_gateway.log" 15 | Smpp::Base.logger = Logger.new(LOGFILE) 16 | 17 | # We use EventMachine to receive keyboard input (which we send as MT messages). 18 | # A "real" gateway would probably get its MTs from a message queue instead. 19 | module KeyboardHandler 20 | include EventMachine::Protocols::LineText2 21 | 22 | def receive_line(data) 23 | sender, receiver, *body_parts = data.split 24 | unless sender && receiver && body_parts.size > 0 25 | puts "Syntax: " 26 | else 27 | body = body_parts.join(' ') 28 | puts "Sending MT from #{sender} to #{receiver}: #{body}" 29 | SampleGateway.send_mt(sender, receiver, body) 30 | end 31 | prompt 32 | end 33 | 34 | def prompt 35 | print "MT: " 36 | $stdout.flush 37 | end 38 | end 39 | 40 | class SampleGateway 41 | 42 | # MT id counter. 43 | @@mt_id = 0 44 | 45 | # expose SMPP transceiver's send_mt method 46 | def self.send_mt(*args) 47 | @@mt_id += 1 48 | @@tx.send_mt(@@mt_id, *args) 49 | end 50 | 51 | def logger 52 | Smpp::Base.logger 53 | end 54 | 55 | def start(config) 56 | # The transceiver sends MT messages to the SMSC. It needs a storage with Hash-like 57 | # semantics to map SMSC message IDs to your own message IDs. 58 | pdr_storage = {} 59 | 60 | # Run EventMachine in loop so we can reconnect when the SMSC drops our connection. 61 | puts "Connecting to SMSC..." 62 | loop do 63 | EventMachine::run do 64 | @@tx = EventMachine::connect( 65 | config[:host], 66 | config[:port], 67 | Smpp::Transceiver, 68 | config, 69 | self # delegate that will receive callbacks on MOs and DRs and other events 70 | ) 71 | print "MT: " 72 | $stdout.flush 73 | 74 | # Start consuming MT messages (in this case, from the console) 75 | # Normally, you'd hook this up to a message queue such as Starling 76 | # or ActiveMQ via STOMP. 77 | EventMachine::open_keyboard(KeyboardHandler) 78 | end 79 | puts "Disconnected. Reconnecting in 5 seconds.." 80 | sleep 5 81 | end 82 | end 83 | 84 | # ruby-smpp delegate methods 85 | 86 | def mo_received(transceiver, pdu) 87 | logger.info "Delegate: mo_received: from #{pdu.source_addr} to #{pdu.destination_addr}: #{pdu.short_message}" 88 | end 89 | 90 | def delivery_report_received(transceiver, pdu) 91 | logger.info "Delegate: delivery_report_received: ref #{pdu.msg_reference} stat #{pdu.stat}" 92 | end 93 | 94 | def message_accepted(transceiver, mt_message_id, pdu) 95 | logger.info "Delegate: message_accepted: id #{mt_message_id} smsc ref id: #{pdu.message_id}" 96 | end 97 | 98 | def message_rejected(transceiver, mt_message_id, pdu) 99 | logger.info "Delegate: message_rejected: id #{mt_message_id} smsc ref id: #{pdu.message_id}" 100 | end 101 | 102 | def bound(transceiver) 103 | logger.info "Delegate: transceiver bound" 104 | end 105 | 106 | def unbound(transceiver) 107 | logger.info "Delegate: transceiver unbound" 108 | EventMachine::stop_event_loop 109 | end 110 | 111 | end 112 | 113 | # Start the Gateway 114 | begin 115 | puts "Starting SMS Gateway. Please check the log at #{LOGFILE}" 116 | 117 | # SMPP properties. These parameters work well with the Logica SMPP simulator. 118 | # Consult the SMPP spec or your mobile operator for the correct settings of 119 | # the other properties. 120 | config = { 121 | :host => '127.0.0.1', 122 | :port => 6000, 123 | :system_id => 'hugo', 124 | :password => 'ggoohu', 125 | :system_type => '', # default given according to SMPP 3.4 Spec 126 | :interface_version => 52, 127 | :source_ton => 0, 128 | :source_npi => 1, 129 | :destination_ton => 1, 130 | :destination_npi => 1, 131 | :source_address_range => '', 132 | :destination_address_range => '', 133 | :enquire_link_delay_secs => 10 134 | } 135 | gw = SampleGateway.new 136 | gw.start(config) 137 | rescue Exception => ex 138 | puts "Exception in SMS Gateway: #{ex} at #{ex.backtrace.join("\n")}" 139 | end 140 | -------------------------------------------------------------------------------- /lib/smpp/transceiver.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | # The SMPP Transceiver maintains a bidirectional connection to an SMSC. 4 | # Provide a config hash with connection options to get started. 5 | # See the sample_gateway.rb for examples of config values. 6 | # The transceiver accepts a delegate object that may implement 7 | # the following (all optional) methods: 8 | # 9 | # mo_received(transceiver, pdu) 10 | # delivery_report_received(transceiver, pdu) 11 | # message_accepted(transceiver, mt_message_id, pdu) 12 | # message_rejected(transceiver, mt_message_id, pdu) 13 | # bound(transceiver) 14 | # unbound(transceiver) 15 | 16 | class Smpp::Transceiver < Smpp::Base 17 | 18 | # Send an MT SMS message. Delegate will receive message_accepted callback when SMSC 19 | # acknowledges, or the message_rejected callback upon error 20 | def send_mt(message_id, source_addr, destination_addr, short_message, options={}) 21 | logger.debug "Sending MT: #{short_message}" 22 | if @state == :bound 23 | pdu = Pdu::SubmitSm.new(source_addr, destination_addr, short_message, options) 24 | write_pdu pdu 25 | 26 | # keep the message ID so we can associate the SMSC message ID with our message 27 | # when the response arrives. 28 | @ack_ids[pdu.sequence_number] = message_id 29 | else 30 | raise InvalidStateException, "Transceiver is unbound. Cannot send MT messages." 31 | end 32 | end 33 | 34 | # Send a concatenated message with a body of > 160 characters as multiple messages. 35 | def send_concat_mt(message_id, source_addr, destination_addr, message, options = {}) 36 | logger.debug "Sending concatenated MT: #{message}" 37 | if @state == :bound 38 | # Split the message into parts of 153 characters. (160 - 7 characters for UDH) 39 | parts = [] 40 | while message.size > 0 do 41 | parts << message.slice!(0..Smpp::Transceiver.get_message_part_size(options)) 42 | end 43 | 44 | 0.upto(parts.size-1) do |i| 45 | udh = sprintf("%c", 5) # UDH is 5 bytes. 46 | udh << sprintf("%c%c", 0, 3) # This is a concatenated message 47 | 48 | #TODO Figure out why this needs to be an int here, it's a string elsewhere 49 | udh << sprintf("%c", message_id) # The ID for the entire concatenated message 50 | 51 | udh << sprintf("%c", parts.size) # How many parts this message consists of 52 | udh << sprintf("%c", i+1) # This is part i+1 53 | 54 | options[:esm_class] = 64 # This message contains a UDH header. 55 | options[:udh] = udh 56 | 57 | pdu = Pdu::SubmitSm.new(source_addr, destination_addr, parts[i], options) 58 | write_pdu pdu 59 | 60 | # This is definately a bit hacky - multiple PDUs are being associated with a single 61 | # message_id. 62 | @ack_ids[pdu.sequence_number] = message_id 63 | end 64 | else 65 | raise InvalidStateException, "Transceiver is unbound. Connot send MT messages." 66 | end 67 | end 68 | 69 | # Send MT SMS message for multiple dest_address 70 | # Author: Abhishek Parolkar (abhishek[at]parolkar.com) 71 | # USAGE: $tx.send_multi_mt(123, "9100000000", ["9199000000000","91990000000001","9199000000002"], "Message here") 72 | def send_multi_mt(message_id, source_addr, destination_addr_arr, short_message, options={}) 73 | logger.debug "Sending Multiple MT: #{short_message}" 74 | if @state == :bound 75 | pdu = Pdu::SubmitMulti.new(source_addr, destination_addr_arr, short_message, options) 76 | write_pdu pdu 77 | 78 | # keep the message ID so we can associate the SMSC message ID with our message 79 | # when the response arrives. 80 | @ack_ids[pdu.sequence_number] = message_id 81 | else 82 | raise InvalidStateException, "Transceiver is unbound. Cannot send MT messages." 83 | end 84 | end 85 | 86 | # Send BindTransceiverResponse PDU. 87 | def send_bind 88 | raise IOError, 'Receiver already bound.' unless unbound? 89 | pdu = Pdu::BindTransceiver.new( 90 | @config[:system_id], 91 | @config[:password], 92 | @config[:system_type], 93 | @config[:source_ton], 94 | @config[:source_npi], 95 | @config[:source_address_range]) 96 | write_pdu(pdu) 97 | end 98 | 99 | # Use data_coding to find out what message part size we can use 100 | # http://en.wikipedia.org/wiki/SMS#Message_size 101 | def self.get_message_part_size options 102 | return 153 if options[:data_coding].nil? 103 | return 153 if options[:data_coding] == 0 104 | return 134 if options[:data_coding] == 3 105 | return 134 if options[:data_coding] == 5 106 | return 134 if options[:data_coding] == 6 107 | return 134 if options[:data_coding] == 7 108 | return 67 if options[:data_coding] == 8 109 | return 153 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/smpp/pdu/deliver_sm.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # Received for MO message or delivery notification 3 | class Smpp::Pdu::DeliverSm < Smpp::Pdu::Base 4 | handles_cmd DELIVER_SM 5 | 6 | attr_reader :service_type, :source_addr_ton, :source_addr_npi, :source_addr, :dest_addr_ton, :dest_addr_npi, 7 | :destination_addr, :esm_class, :protocol_id, :priority_flag, :schedule_delivery_time, 8 | :validity_period, :registered_delivery, :replace_if_present_flag, :data_coding, 9 | :sm_default_msg_id, :sm_length, :stat, :msg_reference, :udh, :short_message, 10 | :message_state, :receipted_message_id, :optional_parameters 11 | 12 | @@encoder = nil 13 | 14 | def initialize(source_addr, destination_addr, short_message, options={}, seq=nil) 15 | 16 | @udh = options[:udh] 17 | @service_type = options[:service_type]? options[:service_type] :'' 18 | @source_addr_ton = options[:source_addr_ton]?options[:source_addr_ton]:0 # network specific 19 | @source_addr_npi = options[:source_addr_npi]?options[:source_addr_npi]:1 # unknown 20 | @source_addr = source_addr 21 | @dest_addr_ton = options[:dest_addr_ton]?options[:dest_addr_ton]:1 # international 22 | @dest_addr_npi = options[:dest_addr_npi]?options[:dest_addr_npi]:1 # unknown 23 | @destination_addr = destination_addr 24 | @esm_class = options[:esm_class]?options[:esm_class]:0 # default smsc mode 25 | @protocol_id = options[:protocol_id]?options[:protocol_id]:0 26 | @priority_flag = options[:priority_flag]?options[:priority_flag]:0 27 | @schedule_delivery_time = options[:schedule_delivery_time]?options[:schedule_delivery_time]:'' 28 | @validity_period = options[:validity_period]?options[:validity_period]:'' 29 | @registered_delivery = options[:registered_delivery]?options[:registered_delivery]:1 # we want delivery notifications 30 | @replace_if_present_flag = options[:replace_if_present_flag]?options[:replace_if_present_flag]:0 31 | @data_coding = options[:data_coding]?options[:data_coding]:3 # iso-8859-1 32 | @sm_default_msg_id = options[:sm_default_msg_id]?options[:sm_default_msg_id]:0 33 | @short_message = short_message 34 | payload = @udh ? @udh.to_s + @short_message : @short_message 35 | @sm_length = payload.length 36 | 37 | #fields set for delivery report 38 | @stat = options[:stat] 39 | @msg_reference = options[:msg_reference] 40 | @receipted_message_id = options[:receipted_message_id] 41 | @message_state = options[:message_state] 42 | @optional_parameters = options[:optional_parameters] 43 | 44 | 45 | pdu_body = [@service_type, @source_addr_ton, @source_addr_npi, @source_addr, 46 | @dest_addr_ton, @dest_addr_npi, @destination_addr, @esm_class, @protocol_id, @priority_flag, @schedule_delivery_time, @validity_period, 47 | @registered_delivery, @replace_if_present_flag, @data_coding, @sm_default_msg_id, @sm_length, payload].pack("A*xCCA*xCCA*xCCCA*xA*xCCCCCA*") 48 | 49 | seq ||= next_sequence_number 50 | 51 | super(DELIVER_SM, 0, seq, pdu_body) 52 | end 53 | 54 | def total_parts 55 | @udh ? @udh[4] : 0 56 | end 57 | 58 | def part 59 | @udh ? @udh[5] : 0 60 | end 61 | 62 | def message_id 63 | @udh ? @udh[3] : 0 64 | end 65 | 66 | def self.from_wire_data(seq, status, body) 67 | options = {} 68 | # brutally unpack it 69 | options[:service_type], 70 | options[:source_addr_ton], 71 | options[:source_addr_npi], 72 | source_addr, 73 | options[:dest_addr_ton], 74 | options[:dest_addr_npi], 75 | destination_addr, 76 | options[:esm_class], 77 | options[:protocol_id], 78 | options[:priority_flag], 79 | options[:schedule_delivery_time], 80 | options[:validity_period], 81 | options[:registered_delivery], 82 | options[:replace_if_present_flag], 83 | options[:data_coding], 84 | options[:sm_default_msg_id], 85 | options[:sm_length], 86 | remaining_bytes = body.unpack('Z*CCZ*CCZ*CCCZ*Z*CCCCCa*') 87 | 88 | short_message = remaining_bytes.slice!(0...options[:sm_length]) 89 | 90 | #everything left in remaining_bytes is 3.4 optional parameters 91 | options[:optional_parameters] = parse_optional_parameters(remaining_bytes) 92 | 93 | #parse the 'standard' optional parameters for delivery receipts 94 | options[:optional_parameters].each do |tag, tlv| 95 | if OPTIONAL_MESSAGE_STATE == tag 96 | value = tlv[:value].unpack('C') 97 | options[:message_state] = value[0] if value 98 | 99 | elsif OPTIONAL_RECEIPTED_MESSAGE_ID == tag 100 | value = tlv[:value].unpack('A*') 101 | options[:receipted_message_id] = value[0] if value 102 | end 103 | end 104 | 105 | # Check to see if body has a 5 bit header 106 | if short_message.unpack("c")[0] == 5 107 | options[:udh] = short_message.slice!(0..5).unpack("CCCCCC") 108 | end 109 | 110 | #Note: if the SM is a delivery receipt (esm_class=4) then the short_message _may_ be in this format: 111 | # "id:Smsc2013 sub:1 dlvrd:1 submit date:0610171515 done date:0610171515 stat:0 err:0 text:blah" 112 | # or this format: 113 | # "4790000000SMSAlert^id:1054BC63 sub:0 dlvrd:1 submit date:0610231217 done date:0610231217 stat:DELIVRD err: text:" 114 | # (according to the SMPP spec, the format is vendor specific) 115 | # For example, Tele2 (Norway): 116 | # "?id:10ea34755d3d4f7a20900cdb3349e549 sub:001 dlvrd:001 submit date:0611011228 done date:0611011230 stat:DELIVRD err:000 Text:abc'!10ea34755d3d4f7a20900cdb3349e549" 117 | if options[:esm_class] == 4 118 | # id is in the mandatory part 119 | msg_ref_match = short_message.match(/id:([^ ]*)/) 120 | # id is not found, search it in the optional part 121 | msg_ref_match = remaining_bytes.match(/id:([^ ]*)/) if !msg_ref_match 122 | 123 | if msg_ref_match 124 | options[:msg_reference] = msg_ref_match[1] 125 | end 126 | 127 | # stat is the mandatory part 128 | stat_match = short_message.match(/stat:([^ ]*)/) 129 | # stat is not found, search it in the optional part 130 | stat_match = remaining_bytes.match(/stat:([^ ]*)/) if !stat_match 131 | 132 | if stat_match 133 | options[:stat] = stat_match[1] 134 | end 135 | 136 | Smpp::Base.logger.debug "DeliverSM with source_addr=#{source_addr}, destination_addr=#{destination_addr}, msg_reference=#{options[:msg_reference]}, stat=#{options[:stat]}" 137 | else 138 | Smpp::Base.logger.debug "DeliverSM with source_addr=#{source_addr}, destination_addr=#{destination_addr}" 139 | end 140 | 141 | #yield the data_coding and short_message to the encoder if one is set 142 | short_message = @@encoder.encode(options[:data_coding], short_message) if @@encoder.respond_to?(:encode) 143 | 144 | new(source_addr, destination_addr, short_message, options, seq) 145 | end 146 | 147 | #set an encoder that can be called to yield the data_coding and short_message 148 | def self.data_encoder=(encoder) 149 | @@encoder = encoder 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /lib/smpp/pdu/base.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # PDUs are the protcol base units in SMPP 3 | module Smpp::Pdu 4 | class Base 5 | #Protocol Version 6 | PROTOCOL_VERSION = 0x34 7 | # Error constants 8 | ESME_ROK = 0x00000000 # OK! 9 | ESME_RINVMSGLEN = 0x00000001 # Message Length is invalid 10 | ESME_RINVCMDLEN = 0x00000002 # Command Length is invalid 11 | ESME_RINVCMDID = 0x00000003 # Invalid Command ID 12 | ESME_RINVBNDSTS = 0x00000004 # Incorrect BIND Status for given com- 13 | ESME_RALYBND = 0x00000005 # ESME Already in Bound State 14 | ESME_RINVPRTFLG = 0x00000006 # Invalid Priority Flag 15 | ESME_RINVREGDLVFLG = 0x00000007 # Invalid Registered Delivery Flag 16 | ESME_RSYSERR = 0x00000008 # System Error 17 | ESME_RINVSRCADR = 0x0000000A # Invalid Source Address 18 | ESME_RINVDSTADR = 0x0000000B # Invalid Dest Addr 19 | ESME_RINVMSGID = 0x0000000C # Message ID is invalid 20 | ESME_RBINDFAIL = 0x0000000D # Bind Failed 21 | ESME_RINVPASWD = 0x0000000E # Invalid Password 22 | ESME_RINVSYSID = 0x0000000F # Invalid System ID 23 | ESME_RCANCELFAIL = 0x00000011 # Cancel SM Failed 24 | ESME_RREPLACEFAIL = 0x00000013 # Replace SM Failed 25 | ESME_RMSGQFUL = 0x00000014 # Message Queue Full 26 | ESME_RINVSERTYP = 0x00000015 # Invalid Service Type 27 | ESME_RINVNUMDESTS = 0x00000033 # Invalid number of destinations 28 | ESME_RINVDLNAME = 0x00000034 # Invalid Distribution List name 29 | ESME_RINVDESTFLAG = 0x00000040 # Destination flag is invalid 30 | ESME_RINVSUBREP = 0x00000042 # Invalid ‘submit with replace’ request 31 | ESME_RINVESMCLASS = 0x00000043 # Invalid esm_class field data 32 | ESME_RCNTSUBDL = 0x00000044 # Cannot Submit to Distribution List 33 | ESME_RSUBMITFAIL = 0x00000045 # submit_sm or submit_multi failed 34 | ESME_RINVSRCTON = 0x00000048 # Invalid Source address TON 35 | ESME_RINVSRCNPI = 0x00000049 # Invalid Source address NPI 36 | ESME_RINVDSTTON = 0x00000050 # Invalid Destination address TON 37 | ESME_RINVDSTNPI = 0x00000051 # Invalid Destination address NPI 38 | ESME_RINVSYSTYP = 0x00000053 # Invalid system_type field 39 | ESME_RINVREPFLAG = 0x00000054 # Invalid replace_if_present flag 40 | ESME_RINVNUMMSGS = 0x00000055 # Invalid number of messages 41 | ESME_RTHROTTLED = 0x00000058 # Throttling error (ESME has exceeded allowed message limits) 42 | 43 | ESME_RX_T_APPN = 0x00000064 # ESME Receiver Temporary App Error Code 44 | 45 | # PDU types 46 | GENERIC_NACK = 0X80000000 47 | BIND_RECEIVER = 0X00000001 48 | BIND_RECEIVER_RESP = 0X80000001 49 | BIND_TRANSMITTER = 0X00000002 50 | BIND_TRANSMITTER_RESP = 0X80000002 51 | BIND_TRANSCEIVER = 0X00000009 52 | BIND_TRANSCEIVER_RESP = 0X80000009 53 | QUERY_SM = 0X00000003 54 | QUERY_SM_RESP = 0X80000003 55 | SUBMIT_SM = 0X00000004 56 | SUBMIT_SM_RESP = 0X80000004 57 | DELIVER_SM = 0X00000005 58 | DELIVER_SM_RESP = 0X80000005 59 | UNBIND = 0X00000006 60 | UNBIND_RESP = 0X80000006 61 | REPLACE_SM = 0X00000007 62 | REPLACE_SM_RESP = 0X80000007 63 | CANCEL_SM = 0X00000008 64 | CANCEL_SM_RESP = 0X80000008 65 | ENQUIRE_LINK = 0X00000015 66 | ENQUIRE_LINK_RESP = 0X80000015 67 | SUBMIT_MULTI = 0X00000021 68 | SUBMIT_MULTI_RESP = 0X80000021 69 | 70 | OPTIONAL_RECEIPTED_MESSAGE_ID = 0x001E 71 | OPTIONAL_MESSAGE_STATE = 0x0427 72 | 73 | SEQUENCE_MAX = 0x7FFFFFFF 74 | 75 | # PDU sequence number. 76 | @@seq = [Time.now.to_i] 77 | 78 | # Add monitor to sequence counter for thread safety 79 | @@seq.extend(MonitorMixin) 80 | 81 | #factory class registry 82 | @@cmd_map = {} 83 | 84 | attr_reader :command_id, :command_status, :sequence_number, :body, :data 85 | 86 | def initialize(command_id, command_status, seq, body='') 87 | length = 16 + body.length 88 | @command_id = command_id 89 | @command_status = command_status 90 | @body = body 91 | @sequence_number = seq 92 | header = [length, command_id, command_status, seq].pack("NNNN") 93 | @data = header + body 94 | end 95 | 96 | def logger 97 | Smpp::Base.logger 98 | end 99 | 100 | def to_human 101 | # convert header (4 bytes) to array of 4-byte ints 102 | a = @data.to_s.unpack('N4') 103 | sprintf("(%22s) len=%3d cmd=%8s status=%1d seq=%03d (%s)", self.class.to_s[11..-1], a[0], a[1].to_s(16), a[2], a[3], @body) 104 | end 105 | 106 | #expects a hash like {tag => Smpp::OptionalParameter} 107 | def Base.optional_parameters_to_buffer(optionals) 108 | output = "" 109 | optionals.each do |tag, optional_param| 110 | length = optional_param.value.to_s.length 111 | buffer = [] 112 | buffer += [tag >> 8, tag & 0xff] 113 | buffer += [length >> 8, length & 0xff] 114 | output << buffer.pack('cccc') << optional_param.value 115 | end 116 | output 117 | end 118 | 119 | def optional_parameters_to_buffer(optionals) 120 | Base.optional_parameters_to_buffer(optionals) 121 | end 122 | 123 | def next_sequence_number 124 | Base.next_sequence_number 125 | end 126 | 127 | def Base.next_sequence_number 128 | @@seq.synchronize do 129 | (@@seq[0] += 1) % SEQUENCE_MAX 130 | end 131 | end 132 | 133 | #This factory should be implemented in every subclass that can create itself from wire 134 | #data. The subclass should also register itself with the 'handles_cmd' class method. 135 | def Base.from_wire_data(seq, status, body) 136 | raise Exception.new("#{self.class} claimed to handle wire data, but doesn't.") 137 | end 138 | 139 | # PDU factory method for common client PDUs (used to create PDUs from wire data) 140 | def Base.create(data) 141 | header = data[0..15] 142 | if !header 143 | return nil 144 | end 145 | len, cmd, status, seq = header.unpack('N4') 146 | body = data[16..-1] 147 | 148 | #if a class has been registered to handle this command_id, try 149 | #to create an instance from the wire data 150 | if @@cmd_map[cmd] 151 | @@cmd_map[cmd].from_wire_data(seq, status, body) 152 | else 153 | Smpp::Base.logger.error "Unknown PDU: #{"0x%08x" % cmd}" 154 | return nil 155 | end 156 | end 157 | 158 | #maps a subclass as the handler for a particulular pdu 159 | def Base.handles_cmd(command_id) 160 | @@cmd_map[command_id] = self 161 | end 162 | 163 | def Base.parse_optional_parameters(remaining_bytes) 164 | optionals = {} 165 | while not remaining_bytes.empty? 166 | optional = {} 167 | optional_parameter, remaining_bytes = Smpp::OptionalParameter.from_wire_data(remaining_bytes) 168 | optionals[optional_parameter.tag] = optional_parameter 169 | end 170 | 171 | return optionals 172 | end 173 | 174 | end 175 | end 176 | -------------------------------------------------------------------------------- /test/encoding_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'rubygems' 4 | require 'test/unit' 5 | require 'smpp/encoding/utf8_encoder' 6 | 7 | require File.expand_path(File.dirname(__FILE__) + "../../lib/smpp") 8 | 9 | class EncodingTest < Test::Unit::TestCase 10 | 11 | 12 | def setup 13 | ::Smpp::Pdu::DeliverSm.data_encoder = ::Smpp::Encoding::Utf8Encoder.new 14 | end 15 | 16 | def teardown 17 | ::Smpp::Pdu::DeliverSm.data_encoder = nil 18 | end 19 | 20 | def test_should_decode_pound_sign_from_hp_roman_8_to_utf_8 21 | raw_data = <<-EOF 22 | 0000 003d 0000 0005 0000 0000 0000 0002 23 | 0001 0134 3437 3830 3330 3239 3833 3700 24 | 0101 3434 3738 3033 3032 3938 3337 0000 25 | 0000 0000 0000 0000 2950 6C65 6173 6520 26 | 6465 706F 7369 7420 BB35 2069 6E74 6F20 27 | 6D79 2061 6363 6F75 6E74 2C20 4A6F 73C5 28 | EOF 29 | 30 | assert_encoded(raw_data, "Please deposit \302\2435 into my account, Jos\303\251", :ascii_assertion => false) 31 | end 32 | 33 | def test_should_unescape_gsm_escaped_euro_symbol 34 | raw_data = <<-EOF 35 | 0000 003d 0000 0005 0000 0000 0000 0002 36 | 0001 0134 3437 3830 3330 3239 3833 3700 37 | 0101 3434 3738 3033 3032 3938 3337 0000 38 | 0000 0000 0000 0000 1950 6c65 6173 6520 39 | 6465 706f 7369 7420 8d65 3520 7468 616e 40 | 6b73 41 | EOF 42 | 43 | assert_encoded(raw_data, "Please deposit \342\202\2545 thanks", :ascii_assertion => false) 44 | end 45 | 46 | def test_should_unescape_gsm_escaped_left_curly_bracket_symbol 47 | raw_data = <<-EOF 48 | 0000 003d 0000 0005 0000 0000 0000 0002 49 | 0001 0134 3437 3830 3330 3239 3833 3700 50 | 0101 3434 3738 3033 3032 3938 3337 0000 51 | 0000 0000 0000 0000 028d 28 52 | EOF 53 | 54 | pdu = create_pdu(raw_data) 55 | assert_equal Smpp::Pdu::DeliverSm, pdu.class 56 | assert_equal 0, pdu.data_coding 57 | 58 | assert_equal "{", pdu.short_message 59 | end 60 | 61 | def test_should_unescape_gsm_escaped_right_curly_bracket_symbol 62 | raw_data = <<-EOF 63 | 0000 003d 0000 0005 0000 0000 0000 0002 64 | 0001 0134 3437 3830 3330 3239 3833 3700 65 | 0101 3434 3738 3033 3032 3938 3337 0000 66 | 0000 0000 0000 0000 028d 29 67 | EOF 68 | 69 | pdu = create_pdu(raw_data) 70 | assert_equal Smpp::Pdu::DeliverSm, pdu.class 71 | assert_equal 0, pdu.data_coding 72 | 73 | assert_equal "}", pdu.short_message 74 | end 75 | 76 | def test_should_unescape_gsm_escaped_tilde_symbol 77 | raw_data = <<-EOF 78 | 0000 003d 0000 0005 0000 0000 0000 0002 79 | 0001 0134 3437 3830 3330 3239 3833 3700 80 | 0101 3434 3738 3033 3032 3938 3337 0000 81 | 0000 0000 0000 0000 028d 3d 82 | EOF 83 | 84 | assert_encoded(raw_data, "~") 85 | end 86 | 87 | def test_should_unescape_gsm_escaped_left_square_bracket_symbol 88 | raw_data = <<-EOF 89 | 0000 003d 0000 0005 0000 0000 0000 0002 90 | 0001 0134 3437 3830 3330 3239 3833 3700 91 | 0101 3434 3738 3033 3032 3938 3337 0000 92 | 0000 0000 0000 0000 028d 3c 93 | EOF 94 | 95 | pdu = create_pdu(raw_data) 96 | assert_equal Smpp::Pdu::DeliverSm, pdu.class 97 | assert_equal 0, pdu.data_coding 98 | 99 | assert_equal "[", pdu.short_message 100 | end 101 | 102 | def test_should_unescape_gsm_escaped_right_square_bracket_symbol 103 | raw_data = <<-EOF 104 | 0000 003d 0000 0005 0000 0000 0000 0002 105 | 0001 0134 3437 3830 3330 3239 3833 3700 106 | 0101 3434 3738 3033 3032 3938 3337 0000 107 | 0000 0000 0000 0000 028d 3e 108 | EOF 109 | 110 | pdu = create_pdu(raw_data) 111 | assert_equal Smpp::Pdu::DeliverSm, pdu.class 112 | assert_equal 0, pdu.data_coding 113 | 114 | assert_equal "]", pdu.short_message 115 | end 116 | 117 | def test_should_unescape_gsm_escaped_backslash_symbol 118 | raw_data = <<-EOF 119 | 0000 003d 0000 0005 0000 0000 0000 0002 120 | 0001 0134 3437 3830 3330 3239 3833 3700 121 | 0101 3434 3738 3033 3032 3938 3337 0000 122 | 0000 0000 0000 0000 028d 2f 123 | EOF 124 | 125 | pdu = create_pdu(raw_data) 126 | assert_equal Smpp::Pdu::DeliverSm, pdu.class 127 | assert_equal 0, pdu.data_coding 128 | 129 | assert_equal "\\", pdu.short_message 130 | end 131 | 132 | def test_should_unescape_gsm_escaped_vertical_bar_symbol 133 | raw_data = <<-EOF 134 | 0000 003d 0000 0005 0000 0000 0000 0002 135 | 0001 0134 3437 3830 3330 3239 3833 3700 136 | 0101 3434 3738 3033 3032 3938 3337 0000 137 | 0000 0000 0000 0000 028d b8 138 | EOF 139 | 140 | assert_encoded(raw_data, "|") 141 | end 142 | 143 | def test_should_unescape_gsm_escaped_caret_or_circumflex_symbol 144 | raw_data = <<-EOF 145 | 0000 003d 0000 0005 0000 0000 0000 0002 146 | 0001 0134 3437 3830 3330 3239 3833 3700 147 | 0101 3434 3738 3033 3032 3938 3337 0000 148 | 0000 0000 0000 0000 028d 86 149 | EOF 150 | 151 | pdu = create_pdu(raw_data) 152 | assert_equal Smpp::Pdu::DeliverSm, pdu.class 153 | assert_equal 0, pdu.data_coding 154 | 155 | expected = "^" 156 | assert_equal expected, pdu.short_message 157 | end 158 | 159 | def test_should_unescape_gsm_escaped_characters_together 160 | raw_data = <<-EOF 161 | 0000 003d 0000 0005 0000 0000 0000 0002 162 | 0001 0134 3437 3830 3330 3239 3833 3700 163 | 0101 3434 3738 3033 3032 3938 3337 0000 164 | 0000 0000 0000 0000 4054 6573 748d b869 165 | 6e67 208d 8620 7374 6167 2f69 6e67 208d 166 | 3d20 6575 726f 208d 6520 616e 6420 8d28 167 | 6f74 688d 2f65 7220 8d3c 2063 6861 7261 168 | 8d3e 6374 6572 738d 29 169 | EOF 170 | 171 | assertion = "Test|ing ^ stag/ing ~ euro € and {oth\\er [ chara]cters}" 172 | assert_encoded(raw_data, assertion, :ascii_assertion => false) 173 | end 174 | 175 | def test_should_convert_ucs_2_into_utf_8_where_data_coding_indicates_its_presence 176 | raw_data = <<-EOF 177 | 0000 003d 0000 0005 0000 0000 0000 0002 178 | 0001 0134 3437 3830 3330 3239 3833 3700 179 | 0101 3434 3738 3033 3032 3938 3337 0000 180 | 0000 0000 0000 0800 0E00 db00 f100 ef00 181 | e700 f800 6401 13 182 | EOF 183 | 184 | pdu = create_pdu(raw_data) 185 | assert_equal Smpp::Pdu::DeliverSm, pdu.class 186 | assert_equal 8, pdu.data_coding 187 | 188 | expected = "\303\233\303\261\303\257\303\247\303\270d\304\223" # Ûñïçødē 189 | assert_equal expected, pdu.short_message 190 | end 191 | 192 | def test_should_decode_pound_sign_from_hp_roman_8_to_utf_8_when_data_coding_set_to_1 193 | raw_data = <<-EOF 194 | 0000 0096 0000 0005 0000 0000 0000 1b10 195 | 0005 004d 6f6e 6579 416c 6572 7400 0101 196 | 3434 3737 3738 3030 3036 3133 0000 0000 197 | 0000 0000 0100 5fbb 506f 756e 6473 bb20 198 | EOF 199 | 200 | assert_encoded(raw_data, "£Pounds£ ", :asserted_data_coding => 1, :ascii_assertion => false) 201 | end 202 | 203 | protected 204 | def create_pdu(raw_data) 205 | hex_data = [raw_data.chomp.gsub(" ","").gsub(/\n/,"")].pack("H*") 206 | Smpp::Pdu::Base.create(hex_data) 207 | end 208 | 209 | private 210 | 211 | def assert_encoded(raw_data, assertion, options = {}) 212 | options[:ascii_assertion] = true unless options[:ascii_assertion] == false 213 | pdu = create_pdu(raw_data) 214 | assert_equal Smpp::Pdu::DeliverSm, pdu.class 215 | assert_equal options[:asserted_data_coding] || 0, pdu.data_coding 216 | 217 | assertion.encode!('ASCII-8BIT') if options[:ascii_assertion] && assertion.respond_to?(:encoding) 218 | assert_equal assertion, pdu.short_message 219 | end 220 | end 221 | -------------------------------------------------------------------------------- /lib/smpp/server.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | # -------- 4 | # This is experimental stuff submitted by taryn@taryneast.org 5 | # -------- 6 | 7 | # the opposite of a client-based receiver, the server transmitter will send 8 | # out MOs to the client when set up 9 | class Smpp::Server < Smpp::Base 10 | 11 | attr_accessor :bind_status 12 | 13 | # Expects a config hash, 14 | # a proc to invoke for incoming (MO) messages, 15 | # a proc to invoke for delivery reports, 16 | # and optionally a hash-like storage for pending delivery reports. 17 | def initialize(config, received_messages = [], sent_messages = []) 18 | super(config, nil) 19 | @state = :unbound 20 | @received_messages = received_messages 21 | @sent_messages = sent_messages 22 | 23 | ed = @config[:enquire_link_delay_secs] || 5 24 | comm_inactivity_timeout = [ed - 5, 3].max 25 | rescue Exception => ex 26 | logger.error "Exception setting up server: #{ex}" 27 | raise 28 | end 29 | 30 | 31 | ####################################################################### 32 | # Session management functions 33 | ####################################################################### 34 | # Session helpers 35 | 36 | # convenience methods 37 | # is this session currently bound? 38 | def bound? 39 | @state == :bound 40 | end 41 | # is this session currently unbound? 42 | def unbound? 43 | @state == :unbound 44 | end 45 | # set of valid bind statuses 46 | BIND_STATUSES = {:transmitter => :bound_tx, 47 | :receiver => :bound_rx, :transceiver => :bound_trx} 48 | # set the bind status based on the common-name for the bind class 49 | def set_bind_status(bind_classname) 50 | @bind_status = BIND_STATUSES[bind_classname] 51 | end 52 | # and kill the bind status when done 53 | def unset_bind_status 54 | @bind_status = nil 55 | end 56 | # what is the bind_status? 57 | def bind_status 58 | @bind_status 59 | end 60 | # convenience function - are we able to transmit in this bind-Status? 61 | def transmitting? 62 | # not transmitting if not bound 63 | return false if unbound? || bind_status.nil? 64 | # receivers can't transmit 65 | bind_status != :bound_rx 66 | end 67 | # convenience function - are we able to receive in this bind-Status? 68 | def receiving? 69 | # not receiving if not bound 70 | return false if unbound? || bind_status.nil? 71 | # transmitters can't receive 72 | bind_status != :bound_tx 73 | end 74 | 75 | def am_server? 76 | true 77 | end 78 | 79 | # REVISIT - not sure if these are using the correct data. Currently just 80 | # pulls the data straight out of the given pdu and sends it right back. 81 | # 82 | def fetch_bind_response_class(bind_classname) 83 | # check we have a valid classname - probably overkill as only our code 84 | # will send the classnames through 85 | raise IOError, "bind class name missing" if bind_classname.nil? 86 | raise IOError, "bind class name: #{bind_classname} unknown" unless BIND_STATUSES.has_key?(bind_classname) 87 | 88 | case bind_classname 89 | when :transceiver 90 | return Smpp::Pdu::BindTransceiverResponse 91 | when :transmitter 92 | return Smpp::Pdu::BindTransmitterResponse 93 | when :receiver 94 | return Smpp::Pdu::BindReceiverResponse 95 | end 96 | end 97 | 98 | # actually perform the action of binding the session to the given session 99 | # type 100 | def bind_session(bind_pdu, bind_classname) 101 | # TODO: probably should not "raise" here - what's better? 102 | raise IOError, "Session already bound." if bound? 103 | response_class = fetch_bind_response_class(bind_classname) 104 | 105 | # TODO: look inside the pdu for the password and check it 106 | 107 | send_bind_response(bind_pdu, response_class) 108 | 109 | @state = :bound 110 | set_bind_status(bind_classname) 111 | end 112 | 113 | # Send BindReceiverResponse PDU - used in response to a "bind_receiver" 114 | # pdu. 115 | def send_bind_response(bind_pdu, bind_class) 116 | resp_pdu = bind_class.new( 117 | bind_pdu.sequence_number, 118 | # currently assume that it binds ok 119 | Pdu::Base::ESME_ROK, 120 | # TODO: not sure where we get the system ID 121 | # is this the session id? 122 | bind_pdu.system_id) 123 | write_pdu(resp_pdu) 124 | end 125 | 126 | ####################################################################### 127 | # Message submission (transmitter) functions (used by transmitter and 128 | # transceiver-bound system) 129 | # Note - we only support submit_sm message type, not submit_multi or 130 | # data_sm message types 131 | ####################################################################### 132 | # Receive an incoming message to send to the network and respond 133 | # REVISIT = just a stub 134 | def receive_sm(pdu) 135 | # TODO: probably should not "raise" here - what's better? 136 | raise IOError, "Connection not bound." if unbound? 137 | # Doesn't matter if it's a TX/RX/TRX, have to send a SubmitSmResponse: 138 | # raise IOError, "Connection not set to receive" unless receiving? 139 | 140 | # Must respond to SubmitSm requests with the same sequence number 141 | m_seq = pdu.sequence_number 142 | # add the id to the list of ids of which we're awaiting acknowledgement 143 | @received_messages << m_seq 144 | 145 | # In theory this is where the MC would actually do something useful with 146 | # the PDU - eg send it on to the network. We'd check if it worked and 147 | # send a failure PDU if it failed. 148 | # 149 | # Given this is a dummy MC, that's not necessary, so all our responses 150 | # will be OK. 151 | 152 | # so respond with a successful response 153 | pdu = Pdu::SubmitSmResponse.new(m_seq, Pdu::Base::ESME_ROK, message_id = '' ) 154 | write_pdu pdu 155 | @received_messages.delete m_seq 156 | 157 | logger.info "Received submit sm message: #{m_seq}" 158 | end 159 | 160 | ####################################################################### 161 | # Message delivery (receiver) functions (used by receiver and 162 | # transceiver-bound system) 163 | ####################################################################### 164 | # When we get an incoming SMS to send on to the client, we need to 165 | # initiate one of these PDUs. 166 | # Note - data doesn't have to be valid, as we're not really doing much 167 | # useful with it. Only the params that will be pulled out by the test 168 | # system need to be valid. 169 | def deliver_sm(from, to, message, config = {}) 170 | # TODO: probably should not "raise" here - what's better? 171 | raise IOError, "Connection not bound." if unbound? 172 | raise IOError, "Connection not set to receive" unless receiving? 173 | 174 | # submit the given message 175 | new_pdu = Pdu::DeliverSm.new(from, to, message, config) 176 | write_pdu(new_pdu) 177 | # add the id to the list of ids of which we're awaiting acknowledgement 178 | @sent_messages << m_seq 179 | 180 | logger.info "Delivered SM message id: #{m_seq}" 181 | 182 | new_pdu 183 | end 184 | 185 | # Acknowledge delivery of an outgoing MO message 186 | # REVISIT = just a stub 187 | def accept_deliver_sm_response(pdu) 188 | m_seq = pdu.sequence_number 189 | # add the id to the list of ids we're awaiting acknowledgement of 190 | # REVISIT - what id do we need to store? 191 | unless @sent_messages && @sent_messages.include?(m_seq) 192 | logger.error("Received deliver response for message for which we have no saved id: #{m_seq}") 193 | else 194 | @sent_messages.delete(m_seq) 195 | logger.info "Acknowledged receipt of SM delivery message id: #{m_seq}" 196 | end 197 | end 198 | 199 | 200 | # a PDU is received 201 | # these pdus are all responses to a message sent by the client and require 202 | # their own special response 203 | def process_pdu(pdu) 204 | case pdu 205 | # client has asked to set up a connection 206 | when Pdu::BindTransmitter 207 | bind_session(pdu, :transmitter) 208 | when Pdu::BindReceiver 209 | bind_session(pdu, :receiver) 210 | when Pdu::BindTransceiver 211 | bind_session(pdu, :transceiver) 212 | # client has acknowledged receipt of a message we sent to them 213 | when Pdu::DeliverSmResponse 214 | accept_deliver_sm_response(pdu) # acknowledge its sending 215 | 216 | # client has asked for a message to be sent 217 | when Pdu::SubmitSm 218 | receive_sm(pdu) 219 | else 220 | # for generic functions or default fallback 221 | super(pdu) 222 | end 223 | end 224 | 225 | end 226 | -------------------------------------------------------------------------------- /test/receiver_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require 'rubygems' 3 | require 'test/unit' 4 | require 'smpp' 5 | 6 | class ReceiverTest < Test::Unit::TestCase 7 | 8 | class RecordingDelegate 9 | attr_reader :received_pdus, :received_delivery_report_pdus, :states 10 | def initialize 11 | @received_pdus, @received_delivery_report_pdus, @states = [], [], [] 12 | end 13 | def mo_received(receiver, pdu) 14 | @received_pdus << pdu 15 | end 16 | def delivery_report_received(receiver, pdu) 17 | @received_delivery_report_pdus << pdu 18 | end 19 | def bound(receiver) 20 | @states << :bound 21 | end 22 | end 23 | 24 | class ExceptionRaisingDelegate < RecordingDelegate 25 | def mo_received(receiver, pdu) 26 | raise "exception in delegate" 27 | end 28 | end 29 | 30 | def test_receiving_bind_receiver_response_with_ok_status_should_become_bound 31 | receiver = build_receiver 32 | bind_receiver_response = Smpp::Pdu::BindReceiverResponse.new(nil, Smpp::Pdu::Base::ESME_ROK, 1) 33 | 34 | receiver.process_pdu(bind_receiver_response) 35 | 36 | assert receiver.bound? 37 | end 38 | 39 | def test_receiving_bind_receiver_response_with_ok_status_should_invoke_bound_on_delegate 40 | delegate = RecordingDelegate.new 41 | receiver = build_receiver(delegate) 42 | bind_receiver_response = Smpp::Pdu::BindReceiverResponse.new(nil, Smpp::Pdu::Base::ESME_ROK, 1) 43 | 44 | receiver.process_pdu(bind_receiver_response) 45 | 46 | assert_equal [:bound], delegate.states 47 | end 48 | 49 | def test_receiving_bind_receiver_response_with_ok_status_should_not_error_if_method_doesnt_exist_on_delegate 50 | delegate = Object.new 51 | receiver = build_receiver(delegate) 52 | bind_receiver_response = Smpp::Pdu::BindReceiverResponse.new(nil, Smpp::Pdu::Base::ESME_ROK, 1) 53 | 54 | assert_nothing_raised { receiver.process_pdu(bind_receiver_response) } 55 | end 56 | 57 | def test_receiving_bind_receiver_response_with_error_status_should_not_become_bound 58 | receiver = build_receiver 59 | bind_receiver_response = Smpp::Pdu::BindReceiverResponse.new(nil, Smpp::Pdu::Base::ESME_RBINDFAIL, 1) 60 | 61 | receiver.process_pdu(bind_receiver_response) 62 | 63 | assert receiver.unbound? 64 | end 65 | 66 | def test_receiving_bind_receiver_response_with_error_status_should_not_invoke_bound_on_delegate 67 | delegate = RecordingDelegate.new 68 | receiver = build_receiver(delegate) 69 | bind_receiver_response = Smpp::Pdu::BindReceiverResponse.new(nil, Smpp::Pdu::Base::ESME_RBINDFAIL, 1) 70 | 71 | receiver.process_pdu(bind_receiver_response) 72 | 73 | assert_equal [], delegate.states 74 | end 75 | 76 | def test_receiving_bind_receiver_response_with_error_status_should_close_connection 77 | receiver = build_receiver 78 | bind_receiver_response = Smpp::Pdu::BindReceiverResponse.new(nil, Smpp::Pdu::Base::ESME_RBINDFAIL, 1) 79 | 80 | receiver.process_pdu(bind_receiver_response) 81 | 82 | assert_equal 1, receiver.close_connections 83 | end 84 | 85 | def test_receiving_deliver_sm_should_send_deliver_sm_response 86 | delegate = RecordingDelegate.new 87 | receiver = build_receiver(delegate) 88 | deliver_sm = Smpp::Pdu::DeliverSm.new("from", "to", "message") 89 | 90 | receiver.process_pdu(deliver_sm) 91 | 92 | first_sent_data = receiver.sent_data.first 93 | assert_not_nil first_sent_data 94 | actual_response = Smpp::Pdu::Base.create(first_sent_data) 95 | expected_response = Smpp::Pdu::DeliverSmResponse.new(deliver_sm.sequence_number) 96 | assert_equal expected_response.to_human, actual_response.to_human 97 | end 98 | 99 | def test_receiving_deliver_sm_should_send_error_response_if_delegate_raises_exception 100 | delegate = ExceptionRaisingDelegate.new 101 | receiver = build_receiver(delegate) 102 | deliver_sm = Smpp::Pdu::DeliverSm.new("from", "to", "message") 103 | 104 | receiver.process_pdu(deliver_sm) 105 | 106 | first_sent_data = receiver.sent_data.first 107 | assert_not_nil first_sent_data 108 | actual_response = Smpp::Pdu::Base.create(first_sent_data) 109 | expected_response = Smpp::Pdu::DeliverSmResponse.new(deliver_sm.sequence_number, Smpp::Pdu::Base::ESME_RX_T_APPN) 110 | assert_equal expected_response.to_human, actual_response.to_human 111 | end 112 | 113 | def test_receiving_deliver_sm_should_still_send_deliver_sm_response_when_no_delegate_is_provided 114 | delegate = nil 115 | receiver = build_receiver(delegate) 116 | deliver_sm = Smpp::Pdu::DeliverSm.new("from", "to", "message") 117 | 118 | receiver.process_pdu(deliver_sm) 119 | 120 | first_sent_data = receiver.sent_data.first 121 | assert_not_nil first_sent_data 122 | actual_response = Smpp::Pdu::Base.create(first_sent_data) 123 | expected_response = Smpp::Pdu::DeliverSmResponse.new(deliver_sm.sequence_number) 124 | assert_equal expected_response.to_human, actual_response.to_human 125 | end 126 | 127 | def test_receiving_deliver_sm_should_invoke_mo_received_on_delegate 128 | delegate = RecordingDelegate.new 129 | receiver = build_receiver(delegate) 130 | deliver_sm = Smpp::Pdu::DeliverSm.new("from", "to", "message") 131 | 132 | receiver.process_pdu(deliver_sm) 133 | 134 | first_received_pdu = delegate.received_pdus.first 135 | assert_not_nil first_received_pdu 136 | assert_equal deliver_sm.to_human, first_received_pdu.to_human 137 | end 138 | 139 | def test_receiving_deliver_sm_should_not_error_if_mo_received_method_doesnt_exist_on_delegate 140 | delegate = Object.new 141 | receiver = build_receiver(delegate) 142 | deliver_sm = Smpp::Pdu::DeliverSm.new("from", "to", "message") 143 | 144 | assert_nothing_raised { receiver.process_pdu(deliver_sm) } 145 | end 146 | 147 | def test_receiving_deliver_sm_for_esm_class_4_should_invoke_delivery_report_received_on_delegate 148 | delegate = RecordingDelegate.new 149 | receiver = build_receiver(delegate) 150 | deliver_sm = Smpp::Pdu::DeliverSm.new("from", "to", "message", :esm_class => 4) 151 | 152 | receiver.process_pdu(deliver_sm) 153 | 154 | first_received_delivery_report_pdu = delegate.received_delivery_report_pdus.first 155 | assert_not_nil first_received_delivery_report_pdu 156 | assert_equal deliver_sm.to_human, first_received_delivery_report_pdu.to_human 157 | end 158 | 159 | def test_receiving_deliver_sm_should_not_error_if_received_delivery_report_method_doesnt_exist_on_delegate 160 | delegate = Object.new 161 | receiver = build_receiver(delegate) 162 | deliver_sm = Smpp::Pdu::DeliverSm.new("from", "to", "message", :esm_class => 4) 163 | 164 | assert_nothing_raised { receiver.process_pdu(deliver_sm) } 165 | end 166 | 167 | private 168 | 169 | def build_receiver(delegate = nil) 170 | receiver = Smpp::Receiver.new(1, {}, delegate) 171 | class << receiver 172 | attr_reader :sent_data, :close_connections 173 | def send_data(data) 174 | @sent_data = (@sent_data || []) + [data] 175 | end 176 | def close_connection 177 | @close_connections = (@close_connections || 0) + 1 178 | end 179 | end 180 | receiver 181 | end 182 | 183 | end 184 | 185 | require 'server' 186 | require 'delegate' 187 | 188 | class SmppTest < Test::Unit::TestCase 189 | 190 | def config 191 | Server::config 192 | end 193 | 194 | def test_transceiver_should_bind_and_unbind_then_stop 195 | EventMachine.run { 196 | EventMachine.start_server "localhost", 9000, Server::Unbind 197 | EventMachine.connect "localhost", 9000, Smpp::Receiver, config, Delegate.new 198 | } 199 | # should not hang here: the server's response should have caused the client to terminate 200 | end 201 | 202 | def test_bind_receiver 203 | pdu1 = Smpp::Pdu::BindReceiver.new( 204 | config[:system_id], 205 | config[:password], 206 | config[:system_type], 207 | config[:source_ton], 208 | config[:source_npi], 209 | config[:source_address_range] 210 | ) 211 | 212 | pdu2 = Smpp::Pdu::Base.create(pdu1.data) 213 | 214 | assert_instance_of(Smpp::Pdu::BindReceiver, pdu2) 215 | assert_equal(pdu1.system_id, pdu2.system_id) 216 | assert_equal(pdu1.password, pdu2.password) 217 | assert_equal(pdu1.system_type, pdu2.system_type) 218 | assert_equal(pdu1.addr_ton, pdu2.addr_ton) 219 | assert_equal(pdu1.addr_npi, pdu2.addr_npi) 220 | assert_equal(pdu1.address_range, pdu2.address_range) 221 | assert_equal(pdu1.sequence_number, pdu2.sequence_number) 222 | assert_equal(pdu1.command_status, pdu2.command_status) 223 | end 224 | 225 | def test_bind_receiver_response 226 | pdu1 = Smpp::Pdu::BindReceiverResponse.new(nil, Smpp::Pdu::Base::ESME_ROK, config[:system_id]) 227 | pdu2 = Smpp::Pdu::Base.create(pdu1.data) 228 | assert_instance_of(Smpp::Pdu::BindReceiverResponse, pdu2) 229 | assert_equal(pdu1.system_id, pdu2.system_id) 230 | assert_equal(pdu1.sequence_number, pdu2.sequence_number) 231 | assert_equal(pdu1.command_status, pdu2.command_status) 232 | end 233 | end 234 | -------------------------------------------------------------------------------- /test/smpp_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require 'rubygems' 3 | require 'test/unit' 4 | require 'smpp' 5 | require 'server' 6 | require 'delegate' 7 | require 'responsive_delegate' 8 | 9 | class Poller 10 | def start 11 | 12 | end 13 | end 14 | 15 | 16 | class SmppTest < Test::Unit::TestCase 17 | 18 | def config 19 | Server::config 20 | end 21 | 22 | def test_transceiver_should_bind_and_unbind_then_stop 23 | EventMachine.run { 24 | EventMachine.start_server "localhost", 9000, Server::Unbind 25 | EventMachine.connect "localhost", 9000, Smpp::Transceiver, config, Delegate.new 26 | } 27 | # should not hang here: the server's response should have caused the client to terminate 28 | end 29 | 30 | def test_transceiver_api_should_respond_to_message_rejected 31 | $tx = nil 32 | delegate = ResponsiveDelegate.new 33 | EventMachine.run { 34 | EventMachine.start_server "localhost", 9000, Server::SubmitSmResponseWithErrorStatus 35 | $tx = EventMachine.connect "localhost", 9000, Smpp::Transceiver, config, delegate 36 | } 37 | assert_equal(delegate.event_counter[:message_rejected], 1) 38 | end 39 | 40 | def test_bind_transceiver 41 | pdu1 = Smpp::Pdu::BindTransceiver.new( 42 | config[:system_id], 43 | config[:password], 44 | config[:system_type], 45 | config[:source_ton], 46 | config[:source_npi], 47 | config[:source_address_range] 48 | ) 49 | 50 | pdu2 = Smpp::Pdu::Base.create(pdu1.data) 51 | 52 | assert_instance_of(Smpp::Pdu::BindTransceiver, pdu2) 53 | assert_equal(pdu1.system_id, pdu2.system_id) 54 | assert_equal(pdu1.password, pdu2.password) 55 | assert_equal(pdu1.system_type, pdu2.system_type) 56 | assert_equal(pdu1.addr_ton, pdu2.addr_ton) 57 | assert_equal(pdu1.addr_npi, pdu2.addr_npi) 58 | assert_equal(pdu1.address_range, pdu2.address_range) 59 | assert_equal(pdu1.sequence_number, pdu2.sequence_number) 60 | assert_equal(pdu1.command_status, pdu2.command_status) 61 | end 62 | 63 | def test_bind_transceiver_response 64 | pdu1 = Smpp::Pdu::BindTransceiverResponse.new(nil, Smpp::Pdu::Base::ESME_ROK, config[:system_id]) 65 | pdu2 = Smpp::Pdu::Base.create(pdu1.data) 66 | assert_instance_of(Smpp::Pdu::BindTransceiverResponse, pdu2) 67 | assert_equal(pdu1.system_id, pdu2.system_id) 68 | assert_equal(pdu1.sequence_number, pdu2.sequence_number) 69 | assert_equal(pdu1.command_status, pdu2.command_status) 70 | end 71 | 72 | def test_deliver_sm 73 | pdu1 = Smpp::Pdu::DeliverSm.new( '11111', '1111111111', "This is a test" ) 74 | pdu2 = Smpp::Pdu::Base.create(pdu1.data) 75 | assert_instance_of(Smpp::Pdu::DeliverSm, pdu2) 76 | assert_equal(pdu1.udh, pdu2.udh) 77 | assert_equal(pdu1.short_message, pdu2.short_message) 78 | assert_equal(pdu1.service_type, pdu2.service_type) 79 | assert_equal(pdu1.source_addr_ton, pdu2.source_addr_ton) 80 | assert_equal(pdu1.source_addr_npi, pdu2.source_addr_npi) 81 | assert_equal(pdu1.source_addr, pdu2.source_addr) 82 | assert_equal(pdu1.dest_addr_ton, pdu2.dest_addr_ton) 83 | assert_equal(pdu1.dest_addr_npi, pdu2.dest_addr_npi) 84 | assert_equal(pdu1.destination_addr, pdu2.destination_addr) 85 | assert_equal(pdu1.esm_class, pdu2.esm_class) 86 | assert_equal(pdu1.protocol_id, pdu2.protocol_id) 87 | assert_equal(pdu1.priority_flag, pdu2.priority_flag) 88 | assert_equal(pdu1.schedule_delivery_time, pdu2.schedule_delivery_time) 89 | assert_equal(pdu1.validity_period, pdu2.validity_period) 90 | assert_equal(pdu1.registered_delivery, pdu2.registered_delivery) 91 | assert_equal(pdu1.replace_if_present_flag, pdu2.replace_if_present_flag) 92 | assert_equal(pdu1.data_coding, pdu2.data_coding) 93 | assert_equal(pdu1.sm_default_msg_id, pdu2.sm_default_msg_id) 94 | assert_equal(pdu1.sm_length, pdu2.sm_length) 95 | assert_equal(pdu1.stat, pdu2.stat) 96 | assert_equal(pdu1.msg_reference, pdu2.msg_reference) 97 | assert_equal(pdu1.sequence_number, pdu2.sequence_number) 98 | assert_equal(pdu1.command_status, pdu2.command_status) 99 | end 100 | 101 | def test_submit_sm 102 | pdu1 = Smpp::Pdu::SubmitSm.new( '11111', '1111111111', "This is a test" ) 103 | pdu2 = Smpp::Pdu::Base.create(pdu1.data) 104 | assert_instance_of(Smpp::Pdu::SubmitSm, pdu2) 105 | assert_equal(pdu1.udh, pdu2.udh) 106 | assert_equal(pdu1.short_message, pdu2.short_message) 107 | assert_equal(pdu1.service_type, pdu2.service_type) 108 | assert_equal(pdu1.source_addr_ton, pdu2.source_addr_ton) 109 | assert_equal(pdu1.source_addr_npi, pdu2.source_addr_npi) 110 | assert_equal(pdu1.source_addr, pdu2.source_addr) 111 | assert_equal(pdu1.dest_addr_ton, pdu2.dest_addr_ton) 112 | assert_equal(pdu1.dest_addr_npi, pdu2.dest_addr_npi) 113 | assert_equal(pdu1.destination_addr, pdu2.destination_addr) 114 | assert_equal(pdu1.esm_class, pdu2.esm_class) 115 | assert_equal(pdu1.protocol_id, pdu2.protocol_id) 116 | assert_equal(pdu1.priority_flag, pdu2.priority_flag) 117 | assert_equal(pdu1.schedule_delivery_time, pdu2.schedule_delivery_time) 118 | assert_equal(pdu1.validity_period, pdu2.validity_period) 119 | assert_equal(pdu1.registered_delivery, pdu2.registered_delivery) 120 | assert_equal(pdu1.replace_if_present_flag, pdu2.replace_if_present_flag) 121 | assert_equal(pdu1.data_coding, pdu2.data_coding) 122 | assert_equal(pdu1.sm_default_msg_id, pdu2.sm_default_msg_id) 123 | assert_equal(pdu1.sm_length, pdu2.sm_length) 124 | assert_equal(pdu1.sequence_number, pdu2.sequence_number) 125 | assert_equal(pdu1.command_status, pdu2.command_status) 126 | end 127 | 128 | def test_submit_sm_receiving_invalid_status 129 | pdu1 = Smpp::Pdu::SubmitSm.new( '11111', '1111111111', "This is a test" ) 130 | pdu2 = Smpp::Pdu::Base.create(pdu1.data) 131 | end 132 | 133 | def test_deliver_sm_response 134 | pdu1 = Smpp::Pdu::DeliverSmResponse.new( nil ) 135 | pdu2 = Smpp::Pdu::Base.create(pdu1.data) 136 | assert_instance_of(Smpp::Pdu::DeliverSmResponse, pdu2) 137 | assert_equal(pdu1.sequence_number, pdu2.sequence_number) 138 | assert_equal(pdu1.command_status, pdu2.command_status) 139 | end 140 | 141 | def test_submit_sm_response 142 | pdu1 = Smpp::Pdu::SubmitSmResponse.new( nil, Smpp::Pdu::Base::ESME_ROK, 3 ) 143 | pdu2 = Smpp::Pdu::Base.create(pdu1.data) 144 | assert_instance_of(Smpp::Pdu::SubmitSmResponse, pdu2) 145 | assert_equal(pdu1.sequence_number, pdu2.sequence_number) 146 | assert_equal(pdu1.command_status, pdu2.command_status) 147 | end 148 | 149 | def test_enquire_link 150 | pdu1 = Smpp::Pdu::EnquireLink.new( ) 151 | pdu2 = Smpp::Pdu::Base.create(pdu1.data) 152 | assert_instance_of(Smpp::Pdu::EnquireLink, pdu2) 153 | assert_equal(pdu1.sequence_number, pdu2.sequence_number) 154 | assert_equal(pdu1.command_status, pdu2.command_status) 155 | end 156 | 157 | def test_enquire_link_resp 158 | pdu1 = Smpp::Pdu::EnquireLinkResponse.new( ) 159 | pdu2 = Smpp::Pdu::Base.create(pdu1.data) 160 | assert_instance_of(Smpp::Pdu::EnquireLinkResponse, pdu2) 161 | assert_equal(pdu1.sequence_number, pdu2.sequence_number) 162 | assert_equal(pdu1.command_status, pdu2.command_status) 163 | end 164 | 165 | def test_generic_nack 166 | pdu1 = Smpp::Pdu::GenericNack.new(nil, Smpp::Pdu::Base::ESME_RTHROTTLED ) 167 | pdu2 = Smpp::Pdu::Base.create(pdu1.data) 168 | assert_instance_of(Smpp::Pdu::GenericNack, pdu2) 169 | assert_equal(pdu1.sequence_number, pdu2.sequence_number) 170 | assert_equal(pdu1.command_status, pdu2.command_status) 171 | end 172 | 173 | def test_unbind 174 | pdu1 = Smpp::Pdu::Unbind.new() 175 | pdu2 = Smpp::Pdu::Base.create(pdu1.data) 176 | assert_instance_of(Smpp::Pdu::Unbind, pdu2) 177 | assert_equal(pdu1.sequence_number, pdu2.sequence_number) 178 | assert_equal(pdu1.command_status, pdu2.command_status) 179 | end 180 | 181 | def test_unbind_response 182 | pdu1 = Smpp::Pdu::UnbindResponse.new(nil, Smpp::Pdu::Base::ESME_ROK) 183 | pdu2 = Smpp::Pdu::Base.create(pdu1.data) 184 | assert_instance_of(Smpp::Pdu::UnbindResponse, pdu2) 185 | assert_equal(pdu1.sequence_number, pdu2.sequence_number) 186 | assert_equal(pdu1.command_status, pdu2.command_status) 187 | end 188 | 189 | #TODO: This test is known to fail since this portion of the library is incomplete. 190 | def _todo_test_submit_multi 191 | pdu1 = Smpp::Pdu::SubmitMulti.new( '11111', ['1111111111','1111111112','1111111113'], "This is a test" ) 192 | pdu2 = Smpp::Pdu::Base.create(pdu1.data) 193 | assert_instance_of(Smpp::Pdu::SubmitMulti, pdu2) 194 | assert_equal(pdu1.udh, pdu2.udh) 195 | assert_equal(pdu1.short_message, pdu2.short_message) 196 | assert_equal(pdu1.service_type, pdu2.service_type) 197 | assert_equal(pdu1.source_addr_ton, pdu2.source_addr_ton) 198 | assert_equal(pdu1.source_addr_npi, pdu2.source_addr_npi) 199 | assert_equal(pdu1.source_addr, pdu2.source_addr) 200 | assert_equal(pdu1.dest_addr_ton, pdu2.dest_addr_ton) 201 | assert_equal(pdu1.dest_addr_npi, pdu2.dest_addr_npi) 202 | assert_equal(pdu1.destination_addr_array, pdu2.destination_addr_array) 203 | assert_equal(pdu1.esm_class, pdu2.esm_class) 204 | assert_equal(pdu1.protocol_id, pdu2.protocol_id) 205 | assert_equal(pdu1.priority_flag, pdu2.priority_flag) 206 | assert_equal(pdu1.schedule_delivery_time, pdu2.schedule_delivery_time) 207 | assert_equal(pdu1.validity_period, pdu2.validity_period) 208 | assert_equal(pdu1.registered_delivery, pdu2.registered_delivery) 209 | assert_equal(pdu1.replace_if_present_flag, pdu2.replace_if_present_flag) 210 | assert_equal(pdu1.data_coding, pdu2.data_coding) 211 | assert_equal(pdu1.sm_default_msg_id, pdu2.sm_default_msg_id) 212 | assert_equal(pdu1.sm_length, pdu2.sm_length) 213 | assert_equal(pdu1.sequence_number, pdu2.sequence_number) 214 | assert_equal(pdu1.command_status, pdu2.command_status) 215 | end 216 | 217 | def _test_submit_multi_response 218 | smes = [ 219 | Smpp::Pdu::SubmitMultiResponse::UnsuccessfulSme.new(1,1,'1111111111', Smpp::Pdu::Base::ESME_RINVDSTADR), 220 | Smpp::Pdu::SubmitMultiResponse::UnsuccessfulSme.new(1,1,'1111111112', Smpp::Pdu::Base::ESME_RINVDSTADR), 221 | Smpp::Pdu::SubmitMultiResponse::UnsuccessfulSme.new(1,1,'1111111113', Smpp::Pdu::Base::ESME_RINVDSTADR), 222 | ] 223 | pdu1 = Smpp::Pdu::SubmitMultiResponse.new( nil, Smpp::Pdu::Base::ESME_ROK, '3', smes ) 224 | pdu2 = Smpp::Pdu::Base.create(pdu1.data) 225 | 226 | assert_instance_of(Smpp::Pdu::SubmitMultiResponse, pdu2) 227 | assert_equal(pdu1.unsuccess_smes, pdu2.unsuccess_smes) 228 | assert_equal(pdu1.sequence_number, pdu2.sequence_number) 229 | assert_equal(pdu1.command_status, pdu2.command_status) 230 | end 231 | 232 | def test_should_parse_ref_and_stat_from_deliver_sm 233 | direct = Smpp::Pdu::DeliverSm.new( '1', '2', "419318028472222#id:11f8f46639bd4f7a209016e1a181e3ae sub:001 dlvrd:001 submit date:0902191702 done date:0902191702 stat:DELIVRD err:000 Text:TVILLING: Sl? ut h?'!11f8f46639bd4f7a209016e1a181e3ae", :esm_class => 4) 234 | parsed = Smpp::Pdu::Base.create(direct.data) 235 | assert_equal("DELIVRD", parsed.stat) 236 | assert_equal("11f8f46639bd4f7a209016e1a181e3ae", parsed.msg_reference) 237 | end 238 | 239 | 240 | end 241 | -------------------------------------------------------------------------------- /lib/smpp/base.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'timeout' 4 | require 'scanf' 5 | require 'monitor' 6 | require 'eventmachine' 7 | 8 | module Smpp 9 | class InvalidStateException < Exception; end 10 | 11 | class Base < EventMachine::Connection 12 | include Smpp 13 | 14 | # :bound or :unbound 15 | attr_accessor :state 16 | 17 | def initialize(config, delegate) 18 | @state = :unbound 19 | @config = config 20 | @data = "" 21 | @delegate = delegate 22 | 23 | # Array of un-acked MT message IDs indexed by sequence number. 24 | # As soon as we receive SubmitSmResponse we will use this to find the 25 | # associated message ID, and then create a pending delivery report. 26 | @ack_ids = {} 27 | 28 | ed = @config[:enquire_link_delay_secs] || 5 29 | comm_inactivity_timeout = 2 * ed 30 | end 31 | 32 | # queries the state of the transmitter - is it bound? 33 | def unbound? 34 | @state == :unbound 35 | end 36 | 37 | def bound? 38 | @state == :bound 39 | end 40 | 41 | def Base.logger 42 | @@logger 43 | end 44 | 45 | def Base.logger=(logger) 46 | @@logger = logger 47 | end 48 | 49 | def logger 50 | @@logger 51 | end 52 | 53 | 54 | # invoked by EventMachine when connected 55 | def post_init 56 | # send Bind PDU if we are a binder (eg 57 | # Receiver/Transmitter/Transceiver 58 | send_bind unless defined?(am_server?) && am_server? 59 | 60 | # start timer that will periodically send enquire link PDUs 61 | start_enquire_link_timer(@config[:enquire_link_delay_secs]) if @config[:enquire_link_delay_secs] 62 | rescue Exception => ex 63 | logger.error "Error starting RX: #{ex.message} at #{ex.backtrace[0]}" 64 | end 65 | 66 | # sets up a periodic timer that will periodically enquire as to the 67 | # state of the connection 68 | # Note: to add in custom executable code (that only runs on an open 69 | # connection), derive from the appropriate Smpp class and overload the 70 | # method named: periodic_call_method 71 | def start_enquire_link_timer(delay_secs) 72 | logger.info "Starting enquire link timer (with #{delay_secs}s interval)" 73 | timer = EventMachine::PeriodicTimer.new(delay_secs) do 74 | if error? 75 | logger.warn "Link timer: Connection is in error state. Disconnecting." 76 | timer.cancel 77 | close_connection 78 | elsif unbound? 79 | logger.warn "Link is unbound, waiting until next #{delay_secs} interval before querying again" 80 | else 81 | 82 | # if the user has defined a method to be called periodically, do 83 | # it now - and continue if it indicates to do so 84 | rval = defined?(periodic_call_method) ? periodic_call_method : true 85 | 86 | # only send an OK if this worked 87 | write_pdu Pdu::EnquireLink.new if rval 88 | end 89 | end 90 | end 91 | 92 | # EventMachine::Connection#receive_data 93 | def receive_data(data) 94 | #append data to buffer 95 | @data << data 96 | 97 | while (@data.length >=4) 98 | cmd_length = @data[0..3].unpack('N').first 99 | if(@data.length < cmd_length) 100 | #not complete packet ... break 101 | break 102 | end 103 | 104 | pkt = @data.slice!(0,cmd_length) 105 | 106 | begin 107 | # parse incoming PDU 108 | pdu = read_pdu(pkt) 109 | 110 | # let subclass process it 111 | process_pdu(pdu) if pdu 112 | rescue Exception => e 113 | logger.error "Error receiving data: #{e}\n#{e.backtrace.join("\n")}" 114 | run_callback(:data_error, e) 115 | end 116 | 117 | end 118 | end 119 | 120 | # EventMachine::Connection#unbind 121 | # Invoked by EM when connection is closed. Delegates should consider 122 | # breaking the event loop and reconnect when they receive this callback. 123 | def unbind 124 | run_callback(:unbound, self) 125 | end 126 | 127 | def send_unbind 128 | write_pdu Pdu::Unbind.new 129 | @state = :unbound 130 | end 131 | 132 | def run_callback(cb, *args) 133 | if @delegate.respond_to?(cb) 134 | @delegate.send(cb, *args) 135 | end 136 | end 137 | 138 | # process common PDUs 139 | # returns true if no further processing necessary 140 | def process_pdu(pdu) 141 | case pdu 142 | when Pdu::EnquireLinkResponse 143 | # nop 144 | when Pdu::EnquireLink 145 | write_pdu(Pdu::EnquireLinkResponse.new(pdu.sequence_number)) 146 | when Pdu::Unbind 147 | @state = :unbound 148 | write_pdu(Pdu::UnbindResponse.new(pdu.sequence_number, Pdu::Base::ESME_ROK)) 149 | close_connection 150 | when Pdu::UnbindResponse 151 | logger.info "Unbound OK. Closing connection." 152 | close_connection 153 | when Pdu::GenericNack 154 | logger.warn "Received NACK! (error code #{pdu.error_code})." 155 | # we don't take this lightly: close the connection 156 | close_connection 157 | when Pdu::DeliverSm 158 | begin 159 | logger.debug "ESM CLASS #{pdu.esm_class}" 160 | if pdu.esm_class != 4 161 | # MO message 162 | run_callback(:mo_received, self, pdu) 163 | else 164 | # Delivery report 165 | run_callback(:delivery_report_received, self, pdu) 166 | end 167 | write_pdu(Pdu::DeliverSmResponse.new(pdu.sequence_number)) 168 | rescue => e 169 | logger.warn "Send Receiver Temporary App Error due to #{e.inspect} raised in delegate" 170 | write_pdu(Pdu::DeliverSmResponse.new(pdu.sequence_number, Pdu::Base::ESME_RX_T_APPN)) 171 | end 172 | when Pdu::BindTransceiverResponse 173 | case pdu.command_status 174 | when Pdu::Base::ESME_ROK 175 | logger.debug "Bound OK." 176 | @state = :bound 177 | run_callback(:bound, self) 178 | when Pdu::Base::ESME_RINVPASWD 179 | logger.warn "Invalid password." 180 | # schedule the connection to close, which eventually will cause the unbound() delegate 181 | # method to be invoked. 182 | run_callback(:invalid_password, self) 183 | close_connection 184 | when Pdu::Base::ESME_RINVSYSID 185 | logger.warn "Invalid system id." 186 | run_callback(:invalid_system_id, self) 187 | close_connection 188 | else 189 | logger.warn "Unexpected BindTransceiverResponse. Command status: #{pdu.command_status}" 190 | run_callback(:unexpected_error, self) 191 | close_connection 192 | end 193 | when Pdu::SubmitSmResponse 194 | mt_message_id = @ack_ids.delete(pdu.sequence_number) 195 | if !mt_message_id 196 | raise "Got SubmitSmResponse for unknown sequence_number: #{pdu.sequence_number}" 197 | end 198 | if pdu.command_status != Pdu::Base::ESME_ROK 199 | logger.error "Error status in SubmitSmResponse: #{pdu.command_status}" 200 | run_callback(:message_rejected, self, mt_message_id, pdu) 201 | else 202 | logger.info "Got OK SubmitSmResponse (#{pdu.message_id} -> #{mt_message_id})" 203 | run_callback(:message_accepted, self, mt_message_id, pdu) 204 | end 205 | when Pdu::SubmitMultiResponse 206 | mt_message_id = @ack_ids[pdu.sequence_number] 207 | if !mt_message_id 208 | raise "Got SubmitMultiResponse for unknown sequence_number: #{pdu.sequence_number}" 209 | end 210 | if pdu.command_status != Pdu::Base::ESME_ROK 211 | logger.error "Error status in SubmitMultiResponse: #{pdu.command_status}" 212 | run_callback(:message_rejected, self, mt_message_id, pdu) 213 | else 214 | logger.info "Got OK SubmitMultiResponse (#{pdu.message_id} -> #{mt_message_id})" 215 | run_callback(:message_accepted, self, mt_message_id, pdu) 216 | end 217 | when Pdu::BindReceiverResponse 218 | case pdu.command_status 219 | when Pdu::Base::ESME_ROK 220 | logger.debug "Bound OK." 221 | @state = :bound 222 | run_callback(:bound, self) 223 | when Pdu::Base::ESME_RINVPASWD 224 | logger.warn "Invalid password." 225 | run_callback(:invalid_password, self) 226 | # scheduele the connection to close, which eventually will cause the unbound() delegate 227 | # method to be invoked. 228 | close_connection 229 | when Pdu::Base::ESME_RINVSYSID 230 | logger.warn "Invalid system id." 231 | run_callback(:invalid_system_id, self) 232 | close_connection 233 | else 234 | logger.warn "Unexpected BindReceiverResponse. Command status: #{pdu.command_status}" 235 | run_callback(:unexpected_error, self) 236 | close_connection 237 | end 238 | when Pdu::BindTransmitterResponse 239 | case pdu.command_status 240 | when Pdu::Base::ESME_ROK 241 | logger.debug "Bound OK." 242 | @state = :bound 243 | run_callback(:bound, self) 244 | when Pdu::Base::ESME_RINVPASWD 245 | logger.warn "Invalid password." 246 | run_callback(:invalid_password, self) 247 | # schedule the connection to close, which eventually will cause the unbound() delegate 248 | # method to be invoked. 249 | close_connection 250 | when Pdu::Base::ESME_RINVSYSID 251 | logger.warn "Invalid system id." 252 | run_callback(:invalid_system_id, self) 253 | close_connection 254 | else 255 | logger.warn "Unexpected BindReceiverResponse. Command status: #{pdu.command_status}" 256 | run_callback(:unexpected_error, self) 257 | close_connection 258 | end 259 | else 260 | logger.warn "(#{self.class.name}) Received unexpected PDU: #{pdu.to_human}." 261 | run_callback(:unexpected_pdu, self, pdu) 262 | close_connection 263 | end 264 | end 265 | 266 | private 267 | def write_pdu(pdu) 268 | logger.debug "<- #{pdu.to_human}" 269 | hex_debug pdu.data, "<- " 270 | send_data pdu.data 271 | end 272 | 273 | def read_pdu(data) 274 | pdu = nil 275 | # we may either receive a new request or a response to a previous response. 276 | begin 277 | pdu = Pdu::Base.create(data) 278 | if !pdu 279 | logger.warn "Not able to parse PDU!" 280 | else 281 | logger.debug "-> " + pdu.to_human 282 | end 283 | hex_debug data, "-> " 284 | rescue Exception => ex 285 | logger.error "Exception while reading PDUs: #{ex} in #{ex.backtrace[0]}" 286 | raise 287 | end 288 | pdu 289 | end 290 | 291 | def hex_debug(data, prefix = "") 292 | Base.hex_debug(data, prefix) 293 | end 294 | 295 | def Base.hex_debug(data, prefix = "") 296 | logger.debug do 297 | message = "Hex dump follows:\n" 298 | hexdump(data).each_line do |line| 299 | message << (prefix + line.chomp + "\n") 300 | end 301 | message 302 | end 303 | end 304 | 305 | def Base.hexdump(target) 306 | width=16 307 | group=2 308 | 309 | output = "" 310 | n=0 311 | ascii='' 312 | target.each_byte { |b| 313 | if n%width == 0 314 | output << "%s\n%08x: "%[ascii,n] 315 | ascii='| ' 316 | end 317 | output << "%02x"%b 318 | output << ' ' if (n+=1)%group==0 319 | ascii << "%s"%b.chr.tr('^ -~','.') 320 | } 321 | output << ' '*(((2+width-ascii.size)*(2*group+1))/group.to_f).ceil+ascii 322 | output[1..-1] 323 | end 324 | end 325 | end 326 | --------------------------------------------------------------------------------