ruby-smpp 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ == 0.1.0 2008-04-07
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
data/License.txt ADDED
@@ -0,0 +1,20 @@
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.
data/Manifest.txt ADDED
@@ -0,0 +1,35 @@
1
+ History.txt
2
+ License.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ examples/sample_gateway.rb
7
+ config/hoe.rb
8
+ config/requirements.rb
9
+ lib/smpp.rb
10
+ lib/smpp/version.rb
11
+ lib/smpp/base.rb
12
+ lib/smpp/pdu/base.rb
13
+ lib/smpp/pdu/bind_transceiver.rb
14
+ lib/smpp/pdu/bind_transceiver_response.rb
15
+ lib/smpp/pdu/deliver_sm.rb
16
+ lib/smpp/pdu/deliver_sm_response.rb
17
+ lib/smpp/pdu/enquire_link.rb
18
+ lib/smpp/pdu/enquire_link_response.rb
19
+ lib/smpp/pdu/generic_nack.rb
20
+ lib/smpp/pdu/submit_sm.rb
21
+ lib/smpp/pdu/submit_sm_response.rb
22
+ lib/smpp/pdu/unbind.rb
23
+ lib/smpp/pdu/unbind_response.rb
24
+ lib/smpp/transceiver.rb
25
+ lib/sms.rb
26
+ log/
27
+ script/console
28
+ script/destroy
29
+ script/generate
30
+ script/txt2html
31
+ setup.rb
32
+ tasks/deployment.rake
33
+ tasks/environment.rake
34
+ test/smpp_test.rb
35
+ test/test_helper.rb
data/README.txt ADDED
@@ -0,0 +1,86 @@
1
+ = Ruby-SMPP
2
+
3
+ * http://ruby-smpp.rubyforge.org
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
+ Glossary
12
+ -----------------
13
+ SMSC: SMS Center. Mobile operators normally operate an SMSC in their network. The SMSC stores and forwards SMS messages.
14
+ MO: Mobile Originated SMS: originated in a mobile phone, ie. sent by an end user.
15
+ MT: Mobile Terminated SMS: terminated in a mobile phone, ie. received by an end user.
16
+ DR: Delivery Report, or delivery notification. When you send an MT message, you should receive a DR after a while.
17
+ PDU: Protcol Base Unit, the data units that SMPP is comprised of. This implementation does _not_ implement all SMPP PDUs.
18
+
19
+ Protocol
20
+ -----------------
21
+ The SMPP 3.4 protocol spec can be downloaded here: http://smsforum.net/SMPP_v3_4_Issue1_2.zip
22
+
23
+ Testing
24
+ -----------------
25
+ Logica provides an SMPP simulator that you can download from http://opensmpp.logica.com/. You can
26
+ also sign up for a demo SMPP account at one of the many bulk-SMS providers out there.
27
+
28
+ == FEATURES/PROBLEMS:
29
+
30
+ * Implements only typical client subset of SMPP 3.4 with single-connection Transceiver as opposed to dual-connection Transmitter + Receiver.
31
+ * Contributors are encouraged to add missing PDUs.
32
+ * Need more test cases!
33
+
34
+ == BASIC USAGE:
35
+
36
+ Start the transceiver. Receive callbacks whenever incoming messages or delivery reports arrive. Send messages with Transceiver#send_mt.
37
+
38
+ <pre>
39
+ # connect to SMSC
40
+ tx = EventMachine::run do
41
+ $tx = EventMachine::connect(
42
+ host,
43
+ port,
44
+ Smpp::Transceiver,
45
+ config, # a property hash
46
+ mo_proc, # the proc invoked on incoming (MO) messages
47
+ dr_proc, # the proc invoked on delivery reports
48
+ pdr_storage) # hash-like storage for pending delivery reports
49
+ end
50
+
51
+ # send a message
52
+ tx.send_mt(id, from, to, body)
53
+ </pre>
54
+
55
+ For a more complete example, see examples/sample_gateway.rb
56
+
57
+ == REQUIREMENTS:
58
+
59
+ * Eventmachine 0.10.0
60
+
61
+ == INSTALL:
62
+
63
+ * sudo gem install ruby-smpp
64
+
65
+ == LICENSE:
66
+
67
+ Copyright (c) 2008 Apparat AS
68
+
69
+ Permission is hereby granted, free of charge, to any person obtaining
70
+ a copy of this software and associated documentation files (the
71
+ 'Software'), to deal in the Software without restriction, including
72
+ without limitation the rights to use, copy, modify, merge, publish,
73
+ distribute, sublicense, and/or sell copies of the Software, and to
74
+ permit persons to whom the Software is furnished to do so, subject to
75
+ the following conditions:
76
+
77
+ The above copyright notice and this permission notice shall be
78
+ included in all copies or substantial portions of the Software.
79
+
80
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
81
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
82
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
83
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
84
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
85
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
86
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ task :test do
2
+ ruby "test/smpp_test.rb"
3
+ end
4
+
5
+ require 'config/requirements'
6
+ require 'config/hoe' # setup Hoe + all gem configuration
7
+
8
+ Dir['tasks/**/*.rake'].each { |rake| load rake }
9
+
data/config/hoe.rb ADDED
@@ -0,0 +1,70 @@
1
+ require 'smpp/version'
2
+
3
+ AUTHOR = 'August Z. Flatby'
4
+ EMAIL = "august@apparat.no"
5
+ DESCRIPTION="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."
6
+ GEM_NAME = 'ruby-smpp'
7
+ RUBYFORGE_PROJECT = 'ruby-smpp' # The unix name for your project
8
+ HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
9
+ DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
10
+
11
+ @config_file = "~/.rubyforge/user-config.yml"
12
+ @config = nil
13
+ RUBYFORGE_USERNAME = "augustzf"
14
+ def rubyforge_username
15
+ unless @config
16
+ begin
17
+ @config = YAML.load(File.read(File.expand_path(@config_file)))
18
+ rescue
19
+ puts <<-EOS
20
+ ERROR: No rubyforge config file found: #{@config_file}
21
+ Run 'rubyforge setup' to prepare your env for access to Rubyforge
22
+ - See http://newgem.rubyforge.org/rubyforge.html for more details
23
+ EOS
24
+ exit
25
+ end
26
+ end
27
+ RUBYFORGE_USERNAME.replace @config["username"]
28
+ end
29
+
30
+
31
+ REV = nil
32
+ # UNCOMMENT IF REQUIRED:
33
+ # REV = `svn info`.each {|line| if line =~ /^Revision:/ then k,v = line.split(': '); break v.chomp; else next; end} rescue nil
34
+ VERS = Smpp::VERSION::STRING + (REV ? ".#{REV}" : "")
35
+ RDOC_OPTS = ['--quiet', '--title', 'ruby-smpp documentation',
36
+ "--opname", "index.html",
37
+ "--line-numbers",
38
+ "--main", "README",
39
+ "--inline-source"]
40
+
41
+ class Hoe
42
+ def extra_deps
43
+ @extra_deps.reject! { |x| Array(x).first == 'hoe' }
44
+ @extra_deps
45
+ end
46
+ end
47
+
48
+ # Generate all the Rake tasks
49
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
50
+ $hoe = Hoe.new(GEM_NAME, VERS) do |p|
51
+ p.developer(AUTHOR, EMAIL)
52
+ p.description = DESCRIPTION
53
+ p.summary = DESCRIPTION
54
+ p.url = HOMEPATH
55
+ p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
56
+ p.test_globs = ["test/**/test_*.rb"]
57
+ p.clean_globs |= ['**/.*.sw?', '*.gem', '.config', '**/.DS_Store'] #An array of file patterns to delete on clean.
58
+
59
+ # == Optional
60
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
61
+ p.extra_deps = [['eventmachine', '>= 0.10.0']]
62
+
63
+ #p.spec_extras = {} # A hash of extra values to set in the gemspec.
64
+
65
+ end
66
+
67
+ CHANGES = $hoe.paragraphs_of('History.txt', 0..1).join("\\n\\n")
68
+ PATH = (RUBYFORGE_PROJECT == GEM_NAME) ? RUBYFORGE_PROJECT : "#{RUBYFORGE_PROJECT}/#{GEM_NAME}"
69
+ $hoe.remote_rdoc_dir = File.join(PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,''), 'rdoc')
70
+ $hoe.rsync_args = '-av --delete --ignore-errors'
@@ -0,0 +1,15 @@
1
+ require 'fileutils'
2
+ include FileUtils
3
+
4
+ require 'rubygems'
5
+ %w[rake hoe newgem rubigen].each do |req_gem|
6
+ begin
7
+ require req_gem
8
+ rescue LoadError
9
+ puts "This Rakefile requires the '#{req_gem}' RubyGem."
10
+ puts "Installation: gem install #{req_gem} -y"
11
+ exit
12
+ end
13
+ end
14
+
15
+ $:.unshift(File.join(File.dirname(__FILE__), %w[.. lib]))
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Sample SMPP SMS Gateway. A proper SMS gateway listens for MOs (incoming
4
+ # messages) and DRs (delivery reports), and submit MTs (outgoing messages).
5
+ #
6
+ # An SMS gateway can be the endpoint for a shortcode. For example, the
7
+ # shortcode 2210 in Norway is implemented as a number of gateways like this
8
+ # (one for each mobile operator). Hence, it will receive MOs (deliver_sm) from
9
+ # end users addressed to shortcode 2210 and send MTs (submit_sm) to end users
10
+ # from shortcode 2210.
11
+ #
12
+ # This gateway logs activity (including incoming MO and DR) to log/sms_gateway.log
13
+ # and accepts outgoing (MT) messages from the console. This may be useful for
14
+ # testing your SMPP setup.
15
+
16
+ gem 'ruby-smpp'
17
+ require 'smpp'
18
+
19
+ # set up logger
20
+ Smpp::Base.logger = Logger.new(File.join(File.dirname(__FILE__), '..', 'log/sms_gateway.log'))
21
+
22
+ # the transceiver
23
+ $tx = nil
24
+
25
+ # We use EventMachine to receive keyboard input (which we send as MT messages).
26
+ # A "real" gateway would probably get its MTs from a message queue instead.
27
+ module KeyboardHandler
28
+ include EventMachine::Protocols::LineText2
29
+
30
+ def receive_line(data)
31
+ puts "Sending MT: #{data}"
32
+ from = '2210'
33
+ to = '4790000000'
34
+ $tx.send_mt(123, from, to, data)
35
+ prompt
36
+ end
37
+ end
38
+
39
+ def prompt
40
+ print "Enter MT body: "
41
+ $stdout.flush
42
+ end
43
+
44
+ def logger
45
+ Smpp::Base.logger
46
+ end
47
+
48
+ def start(config)
49
+ # The transceiver sends MT messages to the SMSC. It needs a storage with Hash-like
50
+ # semantics to map SMSC message IDs to your own message IDs.
51
+ pdr_storage = {}
52
+
53
+ # The block invoked when we receive an MO message from the SMSC
54
+ mo_proc = Proc.new do |sender, receiver, msg|
55
+ begin
56
+ # This is where you'd enqueue or store the MO message for further processing.
57
+ logger.info "Received MO from <#{sender}> to <#{receiver}>: <#{msg}>"
58
+ rescue Exception => ex
59
+ logger.error "Exception processing MO: #{ex}"
60
+ end
61
+ end
62
+
63
+ # Invoked on delivery reports
64
+ dr_proc = Proc.new do |msg_reference, operator_status_code|
65
+ begin
66
+ # The SMSC returns its own message reference. Look up (and delete) our stored objects
67
+ # based on this reference.
68
+ pending_message = pdr_storage.delete(msg_reference)
69
+ logger.info "Received DR for #{pending_message}: #{operator_status_code}"
70
+ rescue Exception => ex
71
+ logger.error "Error processing DR: #{ex}"
72
+ end
73
+ end
74
+
75
+ # Run EventMachine in loop so we can reconnect when the SMSC drops our connection.
76
+ loop do
77
+ EventMachine::run do
78
+ $tx = EventMachine::connect(
79
+ config[:host],
80
+ config[:port],
81
+ Smpp::Transceiver,
82
+ config,
83
+ mo_proc,
84
+ dr_proc,
85
+ pdr_storage)
86
+ # Start consuming MT messages (in this case, from the console)
87
+ # Normally, you'd hook this up to a message queue such as Starling
88
+ # or ActiveMQ via STOMP.
89
+ EventMachine::open_keyboard(KeyboardHandler)
90
+ end
91
+ logger.warn "Event loop stopped. Restarting in 5 seconds.."
92
+ sleep 5
93
+ end
94
+ end
95
+
96
+ # Start the Gateway
97
+ begin
98
+ puts "Starting SMS Gateway"
99
+
100
+ # SMPP properties. These parameters work well with the Logica SMPP simulator.
101
+ # Consult the SMPP spec or your mobile operator for the correct settings of
102
+ # the other properties.
103
+ config = {
104
+ :host => 'localhost',
105
+ :port => 6000,
106
+ :system_id => 'jorge',
107
+ :password => 'jorge',
108
+ :source_ton => 0,
109
+ :source_npi => 1,
110
+ :destination_ton => 1,
111
+ :destination_npi => 1,
112
+ :source_address_range => '',
113
+ :destination_address_range => '',
114
+ :enquire_link_delay_secs => 10
115
+ }
116
+ prompt
117
+ start(config)
118
+ rescue Exception => ex
119
+ puts "Exception in SMS Gateway: #{ex} at #{ex.backtrace[0]}"
120
+ end
data/lib/smpp.rb ADDED
@@ -0,0 +1,21 @@
1
+ # SMPP v3.4 subset implementation.
2
+ # SMPP is a short message peer-to-peer protocol typically used to communicate
3
+ # with SMS Centers (SMSCs) over TCP/IP.
4
+ #
5
+ # August Z. Flatby
6
+ # august@apparat.no
7
+
8
+ require 'logger'
9
+
10
+ $:.unshift(File.dirname(__FILE__))
11
+ require 'smpp/base.rb'
12
+ require 'smpp/transceiver.rb'
13
+ require 'smpp/pdu/base.rb'
14
+
15
+ # Load all PDUs
16
+ Dir.glob(File.join(File.dirname(__FILE__), 'smpp', 'pdu', '*.rb')) do |f|
17
+ require f unless f.match('base.rb$')
18
+ end
19
+
20
+ # Default logger. Invoke this call in your client to use another logger.
21
+ Smpp::Base.logger = Logger.new(STDOUT)
data/lib/smpp/base.rb ADDED
@@ -0,0 +1,124 @@
1
+ require 'timeout'
2
+ require 'iconv'
3
+ require 'scanf'
4
+ require 'monitor'
5
+ require 'eventmachine'
6
+
7
+ module Smpp
8
+ class InvalidStateException < Exception; end
9
+
10
+ class Base < EventMachine::Connection
11
+ include Smpp
12
+
13
+ # :bound or :unbound
14
+ attr_accessor :state
15
+
16
+ def Base.logger
17
+ @@logger
18
+ end
19
+
20
+ def Base.logger=(logger)
21
+ @@logger = logger
22
+ end
23
+
24
+ def logger
25
+ @@logger
26
+ end
27
+
28
+ def initialize(config)
29
+ @config = config
30
+ end
31
+
32
+ # invoked by EventMachine when connected
33
+ def post_init
34
+ # send Bind PDU
35
+ send_bind
36
+
37
+ # start timer that will periodically send enquire link PDUs
38
+ start_enquire_link_timer(@config[:enquire_link_delay_secs]) if @config[:enquire_link_delay_secs]
39
+ rescue Exception => ex
40
+ logger.error "Error starting RX: #{ex.message} at #{ex.backtrace[0]}"
41
+ end
42
+
43
+ def start_enquire_link_timer(delay_secs)
44
+ logger.info "Starting enquire link timer (with #{delay_secs}s interval)"
45
+ EventMachine::PeriodicTimer.new(delay_secs) do
46
+ if error?
47
+ logger.warn "Link timer: Connection is in error state. Terminating loop."
48
+ EventMachine::stop_event_loop
49
+ else
50
+ write_pdu Pdu::EnquireLink.new
51
+ end
52
+ end
53
+ end
54
+
55
+ # EventMachine::Connection#receive_data
56
+ def receive_data(data)
57
+ # parse incoming PDU
58
+ pdu = read_pdu(data)
59
+
60
+ # let subclass process it
61
+ process_pdu(pdu) if pdu
62
+ end
63
+
64
+ # EventMachine::Connection#unbind
65
+ def unbind
66
+ logger.warn "EventMachine: unbind invoked in bound state" if @state == :bound
67
+ end
68
+
69
+ def send_unbind
70
+ #raise rescue logger.debug "Unbinding, now?? #{$!.backtrace[1..5].join("\n")}"
71
+ write_pdu Pdu::Unbind.new
72
+ # leave it to the subclass to process the UnbindResponse
73
+ @state = :unbound
74
+ end
75
+
76
+ # process common PDUs
77
+ # returns true if no further processing necessary
78
+ def process_pdu(pdu)
79
+ case pdu
80
+ when Pdu::EnquireLinkResponse
81
+ # nop
82
+ when Pdu::EnquireLink
83
+ write_pdu(Pdu::EnquireLinkResponse.new(pdu.sequence_number))
84
+ when Pdu::Unbind
85
+ @state = :unbound
86
+ write_pdu(Pdu::UnbindResponse.new(pdu.sequence_number, Pdu::Base::ESME_ROK))
87
+ EventMachine::stop_event_loop
88
+ when Pdu::UnbindResponse
89
+ logger.info "Unbound OK. Closing connection."
90
+ close_connection
91
+ when Pdu::GenericNack
92
+ logger.warn "Received NACK! (error code #{pdu.error_code})."
93
+ # we don't take this lightly: stop the event loop
94
+ EventMachine::stop_event_loop
95
+ else
96
+ logger.warn "(#{self.class.name}) Received unexpected PDU: #{pdu.to_human}."
97
+ EventMachine::stop_event_loop
98
+ end
99
+ end
100
+
101
+ private
102
+ def write_pdu(pdu)
103
+ logger.debug "<- #{pdu.to_human}"
104
+ send_data pdu.data
105
+ end
106
+
107
+ def read_pdu(data)
108
+ pdu = nil
109
+ # we may either receive a new request or a response to a previous response.
110
+ begin
111
+ pdu = Pdu::Base.create(data)
112
+ if !pdu
113
+ logger.warn "Not able to parse PDU!"
114
+ else
115
+ logger.debug "-> " + pdu.to_human
116
+ end
117
+ rescue Exception => ex
118
+ logger.error "Exception while reading PDUs: #{ex} in #{ex.backtrace[0]}"
119
+ raise
120
+ end
121
+ pdu
122
+ end
123
+ end
124
+ end