anjlab-ruby-smpp 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. data/CHANGELOG +52 -0
  2. data/CONTRIBUTORS.txt +11 -0
  3. data/Gemfile +8 -0
  4. data/Gemfile.lock +18 -0
  5. data/LICENSE +20 -0
  6. data/README.rdoc +89 -0
  7. data/Rakefile +53 -0
  8. data/VERSION +1 -0
  9. data/config/environment.rb +2 -0
  10. data/examples/PDU1.example +26 -0
  11. data/examples/PDU2.example +26 -0
  12. data/examples/sample_gateway.rb +137 -0
  13. data/examples/sample_smsc.rb +102 -0
  14. data/lib/smpp.rb +25 -0
  15. data/lib/smpp/base.rb +308 -0
  16. data/lib/smpp/encoding/utf8_encoder.rb +37 -0
  17. data/lib/smpp/optional_parameter.rb +35 -0
  18. data/lib/smpp/pdu/base.rb +183 -0
  19. data/lib/smpp/pdu/bind_base.rb +25 -0
  20. data/lib/smpp/pdu/bind_receiver.rb +4 -0
  21. data/lib/smpp/pdu/bind_receiver_response.rb +4 -0
  22. data/lib/smpp/pdu/bind_resp_base.rb +17 -0
  23. data/lib/smpp/pdu/bind_transceiver.rb +4 -0
  24. data/lib/smpp/pdu/bind_transceiver_response.rb +4 -0
  25. data/lib/smpp/pdu/deliver_sm.rb +142 -0
  26. data/lib/smpp/pdu/deliver_sm_response.rb +12 -0
  27. data/lib/smpp/pdu/enquire_link.rb +11 -0
  28. data/lib/smpp/pdu/enquire_link_response.rb +11 -0
  29. data/lib/smpp/pdu/generic_nack.rb +20 -0
  30. data/lib/smpp/pdu/submit_multi.rb +68 -0
  31. data/lib/smpp/pdu/submit_multi_response.rb +49 -0
  32. data/lib/smpp/pdu/submit_sm.rb +91 -0
  33. data/lib/smpp/pdu/submit_sm_response.rb +31 -0
  34. data/lib/smpp/pdu/unbind.rb +11 -0
  35. data/lib/smpp/pdu/unbind_response.rb +12 -0
  36. data/lib/smpp/receiver.rb +27 -0
  37. data/lib/smpp/server.rb +223 -0
  38. data/lib/smpp/transceiver.rb +109 -0
  39. data/lib/sms.rb +9 -0
  40. data/ruby-smpp.gemspec +96 -0
  41. data/test/delegate.rb +28 -0
  42. data/test/encoding_test.rb +231 -0
  43. data/test/optional_parameter_test.rb +30 -0
  44. data/test/pdu_parsing_test.rb +111 -0
  45. data/test/receiver_test.rb +232 -0
  46. data/test/responsive_delegate.rb +53 -0
  47. data/test/server.rb +56 -0
  48. data/test/smpp_test.rb +239 -0
  49. data/test/submit_sm_test.rb +40 -0
  50. data/test/transceiver_test.rb +35 -0
  51. metadata +133 -0
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Sample SMPP SMS Gateway.
4
+
5
+ require 'rubygems'
6
+ require File.dirname(__FILE__) + '/../lib/smpp'
7
+ require File.dirname(__FILE__) + '/../lib/smpp/server'
8
+
9
+ # set up logger
10
+ Smpp::Base.logger = Logger.new('smsc.log')
11
+
12
+ # the transceiver
13
+ $tx = nil
14
+
15
+ # We use EventMachine to receive keyboard input (which we send as MT messages).
16
+ # A "real" gateway would probably get its MTs from a message queue instead.
17
+ module KeyboardHandler
18
+ include EventMachine::Protocols::LineText2
19
+
20
+ def receive_line(data)
21
+ puts "Sending MO: #{data}"
22
+ from = '1111111111'
23
+ to = '1111111112'
24
+ $tx.send_mo(from, to, data)
25
+
26
+
27
+ # if you want to send messages with custom options, uncomment below code, this configuration allows the sender ID to be alpha numeric
28
+ # $tx.send_mt(123, "RubySmpp", to, "Testing RubySmpp as sender id",{
29
+ # :source_addr_ton=> 5,
30
+ # :service_type => 1,
31
+ # :source_addr_ton => 5,
32
+ # :source_addr_npi => 0 ,
33
+ # :dest_addr_ton => 2,
34
+ # :dest_addr_npi => 1,
35
+ # :esm_class => 3 ,
36
+ # :protocol_id => 0,
37
+ # :priority_flag => 0,
38
+ # :schedule_delivery_time => nil,
39
+ # :validity_period => nil,
40
+ # :registered_delivery=> 1,
41
+ # :replace_if_present_flag => 0,
42
+ # :data_coding => 0,
43
+ # :sm_default_msg_id => 0
44
+ # })
45
+
46
+ # if you want to send message to multiple destinations , uncomment below code
47
+ # $tx.send_multi_mt(123, from, ["919900000001","919900000002","919900000003"], "I am echoing that ruby-smpp is great")
48
+ prompt
49
+ end
50
+ end
51
+
52
+ def prompt
53
+ print "Enter MO body: "
54
+ $stdout.flush
55
+ end
56
+
57
+ def logger
58
+ Smpp::Base.logger
59
+ end
60
+
61
+ def start(config)
62
+
63
+ # Run EventMachine in loop so we can reconnect when the SMSC drops our connection.
64
+ loop do
65
+ EventMachine::run do
66
+ $tx = EventMachine::start_server(
67
+ config[:host],
68
+ config[:port],
69
+ Smpp::Server,
70
+ config
71
+ )
72
+ end
73
+ logger.warn "Event loop stopped. Restarting in 5 seconds.."
74
+ sleep 5
75
+ end
76
+ end
77
+
78
+ # Start the Gateway
79
+ begin
80
+ puts "Starting SMS Gateway"
81
+
82
+ # SMPP properties. These parameters the ones provided sample_gateway.rb and
83
+ # will work with it.
84
+ config = {
85
+ :host => 'localhost',
86
+ :port => 6000,
87
+ :system_id => 'hugo',
88
+ :password => 'ggoohu',
89
+ :system_type => 'vma', # default given according to SMPP 3.4 Spec
90
+ :interface_version => 52,
91
+ :source_ton => 0,
92
+ :source_npi => 1,
93
+ :destination_ton => 1,
94
+ :destination_npi => 1,
95
+ :source_address_range => '',
96
+ :destination_address_range => '',
97
+ :enquire_link_delay_secs => 10
98
+ }
99
+ start(config)
100
+ rescue Exception => ex
101
+ puts "Exception in SMS Gateway: #{ex} at #{ex.backtrace[0]}"
102
+ end
data/lib/smpp.rb ADDED
@@ -0,0 +1,25 @@
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/receiver.rb'
14
+ require 'smpp/optional_parameter'
15
+ require 'smpp/pdu/base.rb'
16
+ require 'smpp/pdu/bind_base.rb'
17
+ require 'smpp/pdu/bind_resp_base.rb'
18
+
19
+ # Load all PDUs
20
+ Dir.glob(File.join(File.dirname(__FILE__), 'smpp', 'pdu', '*.rb')) do |f|
21
+ require f unless f.match('base.rb$')
22
+ end
23
+
24
+ # Default logger. Invoke this call in your client to use another logger.
25
+ Smpp::Base.logger = Logger.new(STDOUT)
data/lib/smpp/base.rb ADDED
@@ -0,0 +1,308 @@
1
+ require 'timeout'
2
+ require 'scanf'
3
+ require 'monitor'
4
+ require 'eventmachine'
5
+
6
+ module Smpp
7
+ class InvalidStateException < Exception; end
8
+
9
+ class Base < EventMachine::Connection
10
+ include Smpp
11
+
12
+ # :bound or :unbound
13
+ attr_accessor :state
14
+
15
+ def initialize(config, delegate)
16
+ @state = :unbound
17
+ @config = config
18
+ @data = ""
19
+ @delegate = delegate
20
+
21
+ # Array of un-acked MT message IDs indexed by sequence number.
22
+ # As soon as we receive SubmitSmResponse we will use this to find the
23
+ # associated message ID, and then create a pending delivery report.
24
+ @ack_ids = {}
25
+
26
+ ed = @config[:enquire_link_delay_secs] || 5
27
+ comm_inactivity_timeout = 2 * ed
28
+ end
29
+
30
+ # queries the state of the transmitter - is it bound?
31
+ def unbound?
32
+ @state == :unbound
33
+ end
34
+
35
+ def bound?
36
+ @state == :bound
37
+ end
38
+
39
+ def Base.logger
40
+ @@logger
41
+ end
42
+
43
+ def Base.logger=(logger)
44
+ @@logger = logger
45
+ end
46
+
47
+ def logger
48
+ @@logger
49
+ end
50
+
51
+
52
+ # invoked by EventMachine when connected
53
+ def post_init
54
+ # send Bind PDU if we are a binder (eg
55
+ # Receiver/Transmitter/Transceiver
56
+ send_bind unless defined?(am_server?) && am_server?
57
+
58
+ # start timer that will periodically send enquire link PDUs
59
+ start_enquire_link_timer(@config[:enquire_link_delay_secs]) if @config[:enquire_link_delay_secs]
60
+ rescue Exception => ex
61
+ logger.error "Error starting RX: #{ex.message} at #{ex.backtrace[0]}"
62
+ end
63
+
64
+ # sets up a periodic timer that will periodically enquire as to the
65
+ # state of the connection
66
+ # Note: to add in custom executable code (that only runs on an open
67
+ # connection), derive from the appropriate Smpp class and overload the
68
+ # method named: periodic_call_method
69
+ def start_enquire_link_timer(delay_secs)
70
+ logger.info "Starting enquire link timer (with #{delay_secs}s interval)"
71
+ EventMachine::PeriodicTimer.new(delay_secs) do
72
+ if error?
73
+ logger.warn "Link timer: Connection is in error state. Disconnecting."
74
+ close_connection
75
+ elsif unbound?
76
+ logger.warn "Link is unbound, waiting until next #{delay_secs} interval before querying again"
77
+ else
78
+
79
+ # if the user has defined a method to be called periodically, do
80
+ # it now - and continue if it indicates to do so
81
+ rval = defined?(periodic_call_method) ? periodic_call_method : true
82
+
83
+ # only send an OK if this worked
84
+ write_pdu Pdu::EnquireLink.new if rval
85
+ end
86
+ end
87
+ end
88
+
89
+ # EventMachine::Connection#receive_data
90
+ def receive_data(data)
91
+ #append data to buffer
92
+ @data << data
93
+
94
+ while (@data.length >=4)
95
+ cmd_length = @data[0..3].unpack('N').first
96
+ if(@data.length < cmd_length)
97
+ #not complete packet ... break
98
+ break
99
+ end
100
+
101
+ pkt = @data.slice!(0,cmd_length)
102
+
103
+ begin
104
+ # parse incoming PDU
105
+ pdu = read_pdu(pkt)
106
+
107
+ # let subclass process it
108
+ process_pdu(pdu) if pdu
109
+ rescue Exception => e
110
+ logger.error "Error receiving data: #{e}\n#{e.backtrace.join("\n")}"
111
+ if @delegate.respond_to?(:data_error)
112
+ @delegate.data_error(e)
113
+ end
114
+ end
115
+
116
+ end
117
+ end
118
+
119
+ # EventMachine::Connection#unbind
120
+ # Invoked by EM when connection is closed. Delegates should consider
121
+ # breaking the event loop and reconnect when they receive this callback.
122
+ def unbind
123
+ if @delegate.respond_to?(:unbound)
124
+ @delegate.unbound(self)
125
+ end
126
+ end
127
+
128
+ def send_unbind
129
+ write_pdu Pdu::Unbind.new
130
+ @state = :unbound
131
+ end
132
+
133
+ # process common PDUs
134
+ # returns true if no further processing necessary
135
+ def process_pdu(pdu)
136
+ case pdu
137
+ when Pdu::EnquireLinkResponse
138
+ # nop
139
+ when Pdu::EnquireLink
140
+ write_pdu(Pdu::EnquireLinkResponse.new(pdu.sequence_number))
141
+ when Pdu::Unbind
142
+ @state = :unbound
143
+ write_pdu(Pdu::UnbindResponse.new(pdu.sequence_number, Pdu::Base::ESME_ROK))
144
+ close_connection
145
+ when Pdu::UnbindResponse
146
+ logger.info "Unbound OK. Closing connection."
147
+ close_connection
148
+ when Pdu::GenericNack
149
+ logger.warn "Received NACK! (error code #{pdu.error_code})."
150
+ # we don't take this lightly: close the connection
151
+ close_connection
152
+ when Pdu::DeliverSm
153
+ begin
154
+ logger.debug "ESM CLASS #{pdu.esm_class}"
155
+ if pdu.esm_class != 4
156
+ # MO message
157
+ if @delegate.respond_to?(:mo_received)
158
+ @delegate.mo_received(self, pdu)
159
+ end
160
+ else
161
+ # Delivery report
162
+ if @delegate.respond_to?(:delivery_report_received)
163
+ @delegate.delivery_report_received(self, pdu)
164
+ end
165
+ end
166
+ write_pdu(Pdu::DeliverSmResponse.new(pdu.sequence_number))
167
+ rescue => e
168
+ logger.warn "Send Receiver Temporary App Error due to #{e.inspect} raised in delegate"
169
+ write_pdu(Pdu::DeliverSmResponse.new(pdu.sequence_number, Pdu::Base::ESME_RX_T_APPN))
170
+ end
171
+ when Pdu::BindTransceiverResponse
172
+ case pdu.command_status
173
+ when Pdu::Base::ESME_ROK
174
+ logger.debug "Bound OK."
175
+ @state = :bound
176
+ if @delegate.respond_to?(:bound)
177
+ @delegate.bound(self)
178
+ end
179
+ when Pdu::Base::ESME_RINVPASWD
180
+ logger.warn "Invalid password."
181
+ # scheduele the connection to close, which eventually will cause the unbound() delegate
182
+ # method to be invoked.
183
+ close_connection
184
+ when Pdu::Base::ESME_RINVSYSID
185
+ logger.warn "Invalid system id."
186
+ close_connection
187
+ else
188
+ logger.warn "Unexpected BindTransceiverResponse. Command status: #{pdu.command_status}"
189
+ close_connection
190
+ end
191
+ when Pdu::SubmitSmResponse
192
+ mt_message_id = @ack_ids.delete(pdu.sequence_number)
193
+ if !mt_message_id
194
+ raise "Got SubmitSmResponse for unknown sequence_number: #{pdu.sequence_number}"
195
+ end
196
+ if pdu.command_status != Pdu::Base::ESME_ROK
197
+ logger.error "Error status in SubmitSmResponse: #{pdu.command_status}"
198
+ if @delegate.respond_to?(:message_rejected)
199
+ @delegate.message_rejected(self, mt_message_id, pdu)
200
+ end
201
+ else
202
+ logger.info "Got OK SubmitSmResponse (#{pdu.message_id} -> #{mt_message_id})"
203
+ if @delegate.respond_to?(:message_accepted)
204
+ @delegate.message_accepted(self, mt_message_id, pdu)
205
+ end
206
+ end
207
+ when Pdu::SubmitMultiResponse
208
+ mt_message_id = @ack_ids[pdu.sequence_number]
209
+ if !mt_message_id
210
+ raise "Got SubmitMultiResponse for unknown sequence_number: #{pdu.sequence_number}"
211
+ end
212
+ if pdu.command_status != Pdu::Base::ESME_ROK
213
+ logger.error "Error status in SubmitMultiResponse: #{pdu.command_status}"
214
+ if @delegate.respond_to?(:message_rejected)
215
+ @delegate.message_rejected(self, mt_message_id, pdu)
216
+ end
217
+ else
218
+ logger.info "Got OK SubmitMultiResponse (#{pdu.message_id} -> #{mt_message_id})"
219
+ if @delegate.respond_to?(:message_accepted)
220
+ @delegate.message_accepted(self, mt_message_id, pdu)
221
+ end
222
+ end
223
+ when Pdu::BindReceiverResponse
224
+ case pdu.command_status
225
+ when Pdu::Base::ESME_ROK
226
+ logger.debug "Bound OK."
227
+ @state = :bound
228
+ if @delegate.respond_to?(:bound)
229
+ @delegate.bound(self)
230
+ end
231
+ when Pdu::Base::ESME_RINVPASWD
232
+ logger.warn "Invalid password."
233
+ # scheduele the connection to close, which eventually will cause the unbound() delegate
234
+ # method to be invoked.
235
+ close_connection
236
+ when Pdu::Base::ESME_RINVSYSID
237
+ logger.warn "Invalid system id."
238
+ close_connection
239
+ else
240
+ logger.warn "Unexpected BindReceiverResponse. Command status: #{pdu.command_status}"
241
+ close_connection
242
+ end
243
+ else
244
+ logger.warn "(#{self.class.name}) Received unexpected PDU: #{pdu.to_human}."
245
+ close_connection
246
+ end
247
+ end
248
+
249
+ private
250
+ def write_pdu(pdu)
251
+ logger.debug "<- #{pdu.to_human}"
252
+ hex_debug pdu.data, "<- "
253
+ send_data pdu.data
254
+ end
255
+
256
+ def read_pdu(data)
257
+ pdu = nil
258
+ # we may either receive a new request or a response to a previous response.
259
+ begin
260
+ pdu = Pdu::Base.create(data)
261
+ if !pdu
262
+ logger.warn "Not able to parse PDU!"
263
+ else
264
+ logger.debug "-> " + pdu.to_human
265
+ end
266
+ hex_debug data, "-> "
267
+ rescue Exception => ex
268
+ logger.error "Exception while reading PDUs: #{ex} in #{ex.backtrace[0]}"
269
+ raise
270
+ end
271
+ pdu
272
+ end
273
+
274
+ def hex_debug(data, prefix = "")
275
+ Base.hex_debug(data, prefix)
276
+ end
277
+
278
+ def Base.hex_debug(data, prefix = "")
279
+ logger.debug do
280
+ message = "Hex dump follows:\n"
281
+ hexdump(data).each_line do |line|
282
+ message << (prefix + line.chomp + "\n")
283
+ end
284
+ message
285
+ end
286
+ end
287
+
288
+ def Base.hexdump(target)
289
+ width=16
290
+ group=2
291
+
292
+ output = ""
293
+ n=0
294
+ ascii=''
295
+ target.each_byte { |b|
296
+ if n%width == 0
297
+ output << "%s\n%08x: "%[ascii,n]
298
+ ascii='| '
299
+ end
300
+ output << "%02x"%b
301
+ output << ' ' if (n+=1)%group==0
302
+ ascii << "%s"%b.chr.tr('^ -~','.')
303
+ }
304
+ output << ' '*(((2+width-ascii.size)*(2*group+1))/group.to_f).ceil+ascii
305
+ output[1..-1]
306
+ end
307
+ end
308
+ end
@@ -0,0 +1,37 @@
1
+ require 'iconv'
2
+
3
+ module Smpp
4
+ module Encoding
5
+
6
+ # This class is not required by smpp.rb at all, you need to bring it in yourself.
7
+ # This class also requires iconv, you'll need to ensure it is installed.
8
+ class Utf8Encoder
9
+
10
+ EURO_TOKEN = "_X_EURO_X_"
11
+
12
+ GSM_ESCAPED_CHARACTERS = {
13
+ ?( => "\173", # {
14
+ ?) => "\175", # }
15
+ 184 => "\174", # |
16
+ ?< => "\133", # [
17
+ ?> => "\135", # ]
18
+ ?= => "\176", # ~
19
+ ?/ => "\134", # \
20
+ 134 => "\252", # ^
21
+ ?e => EURO_TOKEN
22
+ }
23
+
24
+ def encode(data_coding, short_message)
25
+ if data_coding < 2
26
+ sm = short_message.gsub(/\215./) { |match| GSM_ESCAPED_CHARACTERS[match[1]] }
27
+ sm = Iconv.conv("UTF-8", "HP-ROMAN8", sm)
28
+ sm.gsub(EURO_TOKEN, "\342\202\254")
29
+ elsif data_coding == 8
30
+ Iconv.conv("UTF-8", "UTF-16BE", short_message)
31
+ else
32
+ short_message
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end