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,35 @@
1
+ class Smpp::OptionalParameter
2
+
3
+ attr_reader :tag, :value
4
+
5
+ def initialize(tag, value)
6
+ @tag = tag
7
+ @value = value
8
+ end
9
+
10
+ def [](symbol)
11
+ self.send symbol
12
+ end
13
+
14
+ def to_s
15
+ self.inspect
16
+ end
17
+
18
+ #class methods
19
+ class << self
20
+ def from_wire_data(data)
21
+
22
+ return nil if data.nil?
23
+ tag, length, remaining_bytes = data.unpack('H4na*')
24
+ tag = tag.hex
25
+
26
+ raise "invalid data, cannot parse optional parameters" if tag == 0 or length.nil?
27
+
28
+ value = remaining_bytes.slice!(0...length)
29
+
30
+ return new(tag, value), remaining_bytes
31
+ end
32
+
33
+ end
34
+
35
+ end
@@ -0,0 +1,183 @@
1
+ # PDUs are the protcol base units in SMPP
2
+ module Smpp::Pdu
3
+ class Base
4
+ #Protocol Version
5
+ PROTOCOL_VERSION = 0x34
6
+ # Error constants
7
+ ESME_ROK = 0x00000000 # OK!
8
+ ESME_RINVMSGLEN = 0x00000001 # Message Length is invalid
9
+ ESME_RINVCMDLEN = 0x00000002 # Command Length is invalid
10
+ ESME_RINVCMDID = 0x00000003 # Invalid Command ID
11
+ ESME_RINVBNDSTS = 0x00000004 # Incorrect BIND Status for given com-
12
+ ESME_RALYBND = 0x00000005 # ESME Already in Bound State
13
+ ESME_RINVPRTFLG = 0x00000006 # Invalid Priority Flag
14
+ ESME_RINVREGDLVFLG = 0x00000007 # Invalid Registered Delivery Flag
15
+ ESME_RSYSERR = 0x00000008 # System Error
16
+ ESME_RINVSRCADR = 0x0000000A # Invalid Source Address
17
+ ESME_RINVDSTADR = 0x0000000B # Invalid Dest Addr
18
+ ESME_RINVMSGID = 0x0000000C # Message ID is invalid
19
+ ESME_RBINDFAIL = 0x0000000D # Bind Failed
20
+ ESME_RINVPASWD = 0x0000000E # Invalid Password
21
+ ESME_RINVSYSID = 0x0000000F # Invalid System ID
22
+ ESME_RCANCELFAIL = 0x00000011 # Cancel SM Failed
23
+ ESME_RREPLACEFAIL = 0x00000013 # Replace SM Failed
24
+ ESME_RMSGQFUL = 0x00000014 # Message Queue Full
25
+ ESME_RINVSERTYP = 0x00000015 # Invalid Service Type
26
+ ESME_RINVNUMDESTS = 0x00000033 # Invalid number of destinations
27
+ ESME_RINVDLNAME = 0x00000034 # Invalid Distribution List name
28
+ ESME_RINVDESTFLAG = 0x00000040 # Destination flag is invalid
29
+ ESME_RINVSUBREP = 0x00000042 # Invalid ‘submit with replace’ request
30
+ ESME_RINVESMCLASS = 0x00000043 # Invalid esm_class field data
31
+ ESME_RCNTSUBDL = 0x00000044 # Cannot Submit to Distribution List
32
+ ESME_RSUBMITFAIL = 0x00000045 # submit_sm or submit_multi failed
33
+ ESME_RINVSRCTON = 0x00000048 # Invalid Source address TON
34
+ ESME_RINVSRCNPI = 0x00000049 # Invalid Source address NPI
35
+ ESME_RINVDSTTON = 0x00000050 # Invalid Destination address TON
36
+ ESME_RINVDSTNPI = 0x00000051 # Invalid Destination address NPI
37
+ ESME_RINVSYSTYP = 0x00000053 # Invalid system_type field
38
+ ESME_RINVREPFLAG = 0x00000054 # Invalid replace_if_present flag
39
+ ESME_RINVNUMMSGS = 0x00000055 # Invalid number of messages
40
+ ESME_RTHROTTLED = 0x00000058 # Throttling error (ESME has exceeded allowed message limits)
41
+
42
+ ESME_RX_T_APPN = 0x00000064 # ESME Receiver Temporary App Error Code
43
+
44
+ # PDU types
45
+ GENERIC_NACK = 0X80000000
46
+ BIND_RECEIVER = 0X00000001
47
+ BIND_RECEIVER_RESP = 0X80000001
48
+ BIND_TRANSMITTER = 0X00000002
49
+ BIND_TRANSMITTER_RESP = 0X80000002
50
+ BIND_TRANSCEIVER = 0X00000009
51
+ BIND_TRANSCEIVER_RESP = 0X80000009
52
+ QUERY_SM = 0X00000003
53
+ QUERY_SM_RESP = 0X80000003
54
+ SUBMIT_SM = 0X00000004
55
+ SUBMIT_SM_RESP = 0X80000004
56
+ DELIVER_SM = 0X00000005
57
+ DELIVER_SM_RESP = 0X80000005
58
+ UNBIND = 0X00000006
59
+ UNBIND_RESP = 0X80000006
60
+ REPLACE_SM = 0X00000007
61
+ REPLACE_SM_RESP = 0X80000007
62
+ CANCEL_SM = 0X00000008
63
+ CANCEL_SM_RESP = 0X80000008
64
+ ENQUIRE_LINK = 0X00000015
65
+ ENQUIRE_LINK_RESP = 0X80000015
66
+ SUBMIT_MULTI = 0X00000021
67
+ SUBMIT_MULTI_RESP = 0X80000021
68
+
69
+ OPTIONAL_RECEIPTED_MESSAGE_ID = 0x001E
70
+ OPTIONAL_MESSAGE_STATE = 0x0427
71
+
72
+ SEQUENCE_MAX = 0x7FFFFFFF
73
+
74
+ # PDU sequence number.
75
+ @@seq = [Time.now.to_i]
76
+
77
+ # Add monitor to sequence counter for thread safety
78
+ @@seq.extend(MonitorMixin)
79
+
80
+ #factory class registry
81
+ @@cmd_map = {}
82
+
83
+ attr_reader :command_id, :command_status, :sequence_number, :body, :data
84
+
85
+ def initialize(command_id, command_status, seq, body='')
86
+ length = 16 + body.length
87
+ @command_id = command_id
88
+ @command_status = command_status
89
+ @body = body
90
+ @sequence_number = seq
91
+ @data = fixed_int(length) + fixed_int(command_id) + fixed_int(command_status) + fixed_int(seq) + body
92
+ end
93
+
94
+ def logger
95
+ Smpp::Base.logger
96
+ end
97
+
98
+ def to_human
99
+ # convert header (4 bytes) to array of 4-byte ints
100
+ a = @data.to_s.unpack('N4')
101
+ 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)
102
+ end
103
+
104
+ # return int as binary string of 4 octets
105
+ def Base.fixed_int(value)
106
+ arr = [value >> 24, value >> 16, value >> 8, value & 0xff]
107
+ arr.pack("cccc")
108
+ end
109
+
110
+ def fixed_int(value)
111
+ Base.fixed_int(value)
112
+ end
113
+
114
+ #expects a hash like {tag => Smpp::OptionalParameter}
115
+ def Base.optional_parameters_to_buffer(optionals)
116
+ output = ""
117
+ optionals.each do |tag, optional_param|
118
+ length = optional_param.value.length
119
+ buffer = []
120
+ buffer += [tag >> 8, tag & 0xff]
121
+ buffer += [length >> 8, length & 0xff]
122
+ output << buffer.pack('cccc') << optional_param.value
123
+ end
124
+ output
125
+ end
126
+
127
+ def optional_parameters_to_buffer(optionals)
128
+ Base.optional_parameters_to_buffer(optionals)
129
+ end
130
+
131
+ def next_sequence_number
132
+ Base.next_sequence_number
133
+ end
134
+
135
+ def Base.next_sequence_number
136
+ @@seq.synchronize do
137
+ (@@seq[0] += 1) % SEQUENCE_MAX
138
+ end
139
+ end
140
+
141
+ #This factory should be implemented in every subclass that can create itself from wire
142
+ #data. The subclass should also register itself with the 'handles_cmd' class method.
143
+ def Base.from_wire_data(seq, status, body)
144
+ raise Exception.new("#{self.class} claimed to handle wire data, but doesn't.")
145
+ end
146
+
147
+ # PDU factory method for common client PDUs (used to create PDUs from wire data)
148
+ def Base.create(data)
149
+ header = data[0..15]
150
+ if !header
151
+ return nil
152
+ end
153
+ len, cmd, status, seq = header.unpack('N4')
154
+ body = data[16..-1]
155
+
156
+ #if a class has been registered to handle this command_id, try
157
+ #to create an instance from the wire data
158
+ if @@cmd_map[cmd]
159
+ @@cmd_map[cmd].from_wire_data(seq, status, body)
160
+ else
161
+ Smpp::Base.logger.error "Unknown PDU: #{"0x%08x" % cmd}"
162
+ return nil
163
+ end
164
+ end
165
+
166
+ #maps a subclass as the handler for a particulular pdu
167
+ def Base.handles_cmd(command_id)
168
+ @@cmd_map[command_id] = self
169
+ end
170
+
171
+ def Base.parse_optional_parameters(remaining_bytes)
172
+ optionals = {}
173
+ while not remaining_bytes.empty?
174
+ optional = {}
175
+ optional_parameter, remaining_bytes = Smpp::OptionalParameter.from_wire_data(remaining_bytes)
176
+ optionals[optional_parameter.tag] = optional_parameter
177
+ end
178
+
179
+ return optionals
180
+ end
181
+
182
+ end
183
+ end
@@ -0,0 +1,25 @@
1
+ # this class serves as the base for all the bind* commands.
2
+ # since the command format remains the same for all bind commands,
3
+ # sub classes just change the @@command_id
4
+ class Smpp::Pdu::BindBase < Smpp::Pdu::Base
5
+ class << self; attr_accessor :command_id ; end
6
+
7
+ attr_reader :system_id, :password, :system_type, :addr_ton, :addr_npi, :address_range
8
+
9
+ def initialize(system_id, password, system_type, addr_ton, addr_npi, address_range, seq = nil)
10
+ @system_id, @password, @system_type, @addr_ton, @addr_npi, @address_range =
11
+ system_id, password, system_type, addr_ton, addr_npi, address_range
12
+
13
+ seq ||= next_sequence_number
14
+ 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)
15
+ super(self.class.command_id, 0, seq, body)
16
+ end
17
+
18
+ def self.from_wire_data(seq, status, body)
19
+ #unpack the body
20
+ system_id, password, system_type, interface_version, addr_ton,
21
+ addr_npi, address_range = body.unpack("Z*Z*Z*CCCZ*")
22
+
23
+ self.new(system_id, password, system_type, addr_ton, addr_npi, address_range, seq)
24
+ end
25
+ end
@@ -0,0 +1,4 @@
1
+ class Smpp::Pdu::BindReceiver < Smpp::Pdu::BindBase
2
+ @command_id = BIND_RECEIVER
3
+ handles_cmd BIND_RECEIVER
4
+ end
@@ -0,0 +1,4 @@
1
+ class Smpp::Pdu::BindReceiverResponse < Smpp::Pdu::BindRespBase
2
+ @command_id = BIND_RECEIVER_RESP
3
+ handles_cmd BIND_RECEIVER_RESP
4
+ end
@@ -0,0 +1,17 @@
1
+ class Smpp::Pdu::BindRespBase < Smpp::Pdu::Base
2
+ class << self; attr_accessor :command_id ; end
3
+ attr_accessor :system_id
4
+
5
+ def initialize(seq, status, system_id)
6
+ seq ||= next_sequence_number
7
+ system_id = system_id.to_s + "\000"
8
+ super(self.class.command_id, status, seq, system_id) # pass in system_id as body for simple debugging
9
+ @system_id = system_id
10
+ end
11
+
12
+ def self.from_wire_data(seq, status, body)
13
+ system_id = body.chomp("\000")
14
+ new(seq, status, system_id)
15
+ end
16
+
17
+ end
@@ -0,0 +1,4 @@
1
+ class Smpp::Pdu::BindTransceiver < Smpp::Pdu::BindBase
2
+ @command_id = BIND_TRANSCEIVER
3
+ handles_cmd BIND_TRANSCEIVER
4
+ end
@@ -0,0 +1,4 @@
1
+ class Smpp::Pdu::BindTransceiverResponse < Smpp::Pdu::BindRespBase
2
+ @command_id = BIND_TRANSCEIVER_RESP
3
+ handles_cmd BIND_TRANSCEIVER_RESP
4
+ end
@@ -0,0 +1,142 @@
1
+
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
+ pdu_body = sprintf("%s\0%c%c%s\0%c%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,
45
+ @dest_addr_ton, @dest_addr_npi, @destination_addr, @esm_class, @protocol_id, @priority_flag, @schedule_delivery_time, @validity_period,
46
+ @registered_delivery, @replace_if_present_flag, @data_coding, @sm_default_msg_id, @sm_length, payload)
47
+
48
+ seq ||= next_sequence_number
49
+
50
+ super(DELIVER_SM, 0, seq, pdu_body)
51
+ end
52
+
53
+ def total_parts
54
+ @udh ? @udh[4] : 0
55
+ end
56
+
57
+ def part
58
+ @udh ? @udh[5] : 0
59
+ end
60
+
61
+ def message_id
62
+ @udh ? @udh[3] : 0
63
+ end
64
+
65
+ def self.from_wire_data(seq, status, body)
66
+ options = {}
67
+ # brutally unpack it
68
+ options[:service_type],
69
+ options[:source_addr_ton],
70
+ options[:source_addr_npi],
71
+ source_addr,
72
+ options[:dest_addr_ton],
73
+ options[:dest_addr_npi],
74
+ destination_addr,
75
+ options[:esm_class],
76
+ options[:protocol_id],
77
+ options[:priority_flag],
78
+ options[:schedule_delivery_time],
79
+ options[:validity_period],
80
+ options[:registered_delivery],
81
+ options[:replace_if_present_flag],
82
+ options[:data_coding],
83
+ options[:sm_default_msg_id],
84
+ options[:sm_length],
85
+ remaining_bytes = body.unpack('Z*CCZ*CCZ*CCCZ*Z*CCCCCa*')
86
+
87
+ short_message = remaining_bytes.slice!(0...options[:sm_length])
88
+
89
+ #everything left in remaining_bytes is 3.4 optional parameters
90
+ options[:optional_parameters] = parse_optional_parameters(remaining_bytes)
91
+
92
+ #parse the 'standard' optional parameters for delivery receipts
93
+ options[:optional_parameters].each do |tag, tlv|
94
+ if OPTIONAL_MESSAGE_STATE == tag
95
+ value = tlv[:value].unpack('C')
96
+ options[:message_state] = value[0] if value
97
+
98
+ elsif OPTIONAL_RECEIPTED_MESSAGE_ID == tag
99
+ value = tlv[:value].unpack('A*')
100
+ options[:receipted_message_id] = value[0] if value
101
+ end
102
+ end
103
+
104
+ # Check to see if body has a 5 bit header
105
+ if short_message.unpack("c")[0] == 5
106
+ options[:udh] = short_message.slice!(0..5).unpack("CCCCCC")
107
+ end
108
+
109
+ #Note: if the SM is a delivery receipt (esm_class=4) then the short_message _may_ be in this format:
110
+ # "id:Smsc2013 sub:1 dlvrd:1 submit date:0610171515 done date:0610171515 stat:0 err:0 text:blah"
111
+ # or this format:
112
+ # "4790000000SMSAlert^id:1054BC63 sub:0 dlvrd:1 submit date:0610231217 done date:0610231217 stat:DELIVRD err: text:"
113
+ # (according to the SMPP spec, the format is vendor specific)
114
+ # For example, Tele2 (Norway):
115
+ # "<msisdn><shortcode>?id:10ea34755d3d4f7a20900cdb3349e549 sub:001 dlvrd:001 submit date:0611011228 done date:0611011230 stat:DELIVRD err:000 Text:abc'!10ea34755d3d4f7a20900cdb3349e549"
116
+ if options[:esm_class] == 4
117
+ msg_ref_match = short_message.match(/id:([^ ]*)/)
118
+ if msg_ref_match
119
+ options[:msg_reference] = msg_ref_match[1]
120
+ end
121
+
122
+ stat_match = short_message.match(/stat:([^ ]*)/)
123
+ if stat_match
124
+ options[:stat] = stat_match[1]
125
+ end
126
+
127
+ Smpp::Base.logger.debug "DeliverSM with source_addr=#{source_addr}, destination_addr=#{destination_addr}, msg_reference=#{options[:msg_reference]}, stat=#{options[:stat]}"
128
+ else
129
+ Smpp::Base.logger.debug "DeliverSM with source_addr=#{source_addr}, destination_addr=#{destination_addr}"
130
+ end
131
+
132
+ #yield the data_coding and short_message to the encoder if one is set
133
+ short_message = @@encoder.encode(options[:data_coding], short_message) if @@encoder.respond_to?(:encode)
134
+
135
+ new(source_addr, destination_addr, short_message, options, seq)
136
+ end
137
+
138
+ #set an encoder that can be called to yield the data_coding and short_message
139
+ def self.data_encoder=(encoder)
140
+ @@encoder = encoder
141
+ end
142
+ end
@@ -0,0 +1,12 @@
1
+ class Smpp::Pdu::DeliverSmResponse < Smpp::Pdu::Base
2
+ handles_cmd DELIVER_SM_RESP
3
+
4
+ def initialize(seq, status=ESME_ROK)
5
+ seq ||= next_sequence_number
6
+ super(DELIVER_SM_RESP, status, seq, "\000") # body must be NULL..!
7
+ end
8
+
9
+ def self.from_wire_data(seq, status, body)
10
+ new(seq, status)
11
+ end
12
+ end