anjlab-ruby-smpp 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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