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,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