diameter 0.1.0.beta

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.
@@ -0,0 +1,267 @@
1
+ require 'diameter/avp_parser'
2
+ require 'diameter/u24'
3
+
4
+ # The Diameter module
5
+ module Diameter
6
+ # A Diameter message.
7
+ #
8
+ # @!attribute [r] version
9
+ # The Diameter protocol version (currently always 1)
10
+ # @!attribute [r] command_code
11
+ # The Diameter Command-Code of this messsage.
12
+ # @!attribute [r] app_id
13
+ # The Diameter application ID of this message, or 0 for base
14
+ # protocol messages.
15
+ # @!attribute [r] hbh
16
+ # The hop-by-hop identifier of this message.
17
+ # @!attribute [r] ete
18
+ # The end-to-end identifier of this message.
19
+ # @!attribute [r] request
20
+ # Whether this message is a request.
21
+ # @!attribute [r] answer
22
+ # Whether this message is an answer.
23
+ class Message
24
+ attr_reader :version, :command_code, :app_id, :hbh, :ete, :request, :answer
25
+ include Internals
26
+
27
+ # Creates a new Diameter message.
28
+ #
29
+ # @option opts [Fixnum] command_code
30
+ # The Diameter Command-Code of this messsage.
31
+ # @option opts [Fixnum] app_id
32
+ # The Diameter application ID of this message, or 0 for base
33
+ # protocol messages.
34
+ # @option opts [Fixnum] hbh
35
+ # The hop-by-hop identifier of this message.
36
+ # @option opts [Fixnum] ete
37
+ # The end-to-end identifier of this message.
38
+ # @option opts [true, false] request
39
+ # Whether this message is a request. Defaults to true.
40
+ # @option opts [true, false] proxyable
41
+ # Whether this message can be forwarded on. Defaults to true.
42
+ # @option opts [true, false] error
43
+ # Whether this message is a Diameter protocol error. Defaults to false.
44
+ # @option opts [Array<AVP>] avps
45
+ # The list of AVPs to include on this message.
46
+ def initialize(options = {})
47
+ @version = 1
48
+ @command_code = options[:command_code]
49
+ @app_id = options[:app_id]
50
+ @hbh = options[:hbh] || Message.next_hbh
51
+ @ete = options[:ete] || Message.next_ete
52
+
53
+ @request = options.fetch(:request, true)
54
+ @answer = !@request
55
+ @proxyable = options.fetch(:proxyable, false)
56
+ @retransmitted = false
57
+ @error = false
58
+
59
+ @avps = options[:avps] || []
60
+ end
61
+
62
+ # Represents this message (and all its AVPs) in human-readable
63
+ # string form.
64
+ #
65
+ # @see AVP::to_s for how the AVPs are represented.
66
+ # @return [String]
67
+ def to_s
68
+ "#{@command_code}: #{@avps.collect(&:to_s)}"
69
+ end
70
+
71
+ # Serializes a Diameter message (header plus AVPs) into the series
72
+ # of bytes representing it on the wire.
73
+ #
74
+ # @return [String] The byte-encoded form.
75
+ def to_wire
76
+ content = ''
77
+ @avps.each { |a| content += a.to_wire }
78
+ length_8, length_16 = Internals::UInt24.to_u8_and_u16(content.length + 20)
79
+ code_8, code_16 = Internals::UInt24.to_u8_and_u16(@command_code)
80
+ request_flag = @request ? '1' : '0'
81
+ proxy_flag = @proxyable ? '1' : '0'
82
+ flags_str = "#{request_flag}#{proxy_flag}000000"
83
+
84
+ header = [@version, length_8, length_16, flags_str, code_8, code_16, @app_id, @hbh, @ete].pack('CCnB8CnNNN')
85
+ header + content
86
+ end
87
+
88
+ # @!group AVP retrieval
89
+
90
+ # Returns the first AVP with the given name. Only covers "top-level"
91
+ # AVPs - it won't look inside Grouped AVPs.
92
+ #
93
+ # Also available as [], e.g. message['Result-Code']
94
+ #
95
+ # @param name [String] The AVP name, either one predefined in
96
+ # {Constants::AVAILABLE_AVPS} or user-defined with {AVP.define}
97
+ #
98
+ # @return [AVP] if there is an AVP with that name
99
+ # @return [nil] if there is not an AVP with that name
100
+ def avp_by_name(name)
101
+ code, _type, vendor = Internals::AVPNames.get(name)
102
+ avp_by_code(code, vendor)
103
+ end
104
+
105
+ # Returns all AVPs with the given name. Only covers "top-level"
106
+ # AVPs - it won't look inside Grouped AVPs.
107
+ #
108
+ # @param name [String] The AVP name, either one predefined in
109
+ # {Constants::AVAILABLE_AVPS} or user-defined with {AVP.define}
110
+ #
111
+ # @return [Array<AVP>]
112
+ def all_avps_by_name(name)
113
+ code, _type, vendor = Internals::AVPNames.get(name)
114
+ all_avps_by_code(code, vendor)
115
+ end
116
+
117
+ alias_method :avp, :avp_by_name
118
+ alias_method :[], :avp_by_name
119
+ alias_method :avps, :all_avps_by_name
120
+
121
+ # @private
122
+ # Prefer AVP.define and the by-name versions to this
123
+ #
124
+ # Returns the first AVP with the given code and vendor. Only covers "top-level"
125
+ # AVPs - it won't look inside Grouped AVPs.
126
+ #
127
+ # @param code [Fixnum] The AVP Code
128
+ # @param vendor [Fixnum] Optional vendor ID for a vendor-specific
129
+ # AVP.
130
+ # @return [AVP] if there is an AVP with that code/vendor
131
+ # @return [nil] if there is not an AVP with that code/vendor
132
+ def avp_by_code(code, vendor = 0)
133
+ avps = all_avps_by_code(code, vendor)
134
+ if avps.empty?
135
+ nil
136
+ else
137
+ avps[0]
138
+ end
139
+ end
140
+
141
+ # @private
142
+ # Prefer AVP.define and the by-name versions to this
143
+ #
144
+ # Returns all AVPs with the given code and vendor. Only covers "top-level"
145
+ # AVPs - it won't look inside Grouped AVPs.
146
+ #
147
+ # @param code [Fixnum] The AVP Code
148
+ # @param vendor [Fixnum] Optional vendor ID for a vendor-specific
149
+ # AVP.
150
+ # @return [Array<AVP>]
151
+ def all_avps_by_code(code, vendor = 0)
152
+ @avps.select do |a|
153
+ vendor_match =
154
+ if a.vendor_specific?
155
+ a.vendor_id == vendor
156
+ else
157
+ vendor == 0
158
+ end
159
+ (a.code == code) && vendor_match
160
+ end
161
+ end
162
+
163
+ # Does this message contain a (top-level) AVP with this name?
164
+ # @param name [String] The AVP name, either one predefined in
165
+ # {Constants::AVAILABLE_AVPS} or user-defined with {AVP.define}
166
+ #
167
+ # @return [true, false]
168
+ def has_avp?(name)
169
+ !!avp(name)
170
+ end
171
+
172
+ # @private
173
+ #
174
+ # Not recommended for normal use - all AVPs should be given to the
175
+ # constructor. Used to allow the stack to add appropriate
176
+ # Origin-Host/Origin-Realm AVPs to outbound messages.
177
+ #
178
+ # @param host [String] The Diameter Identity for the stack.
179
+ # @param realm [String] The Diameter realm for the stack.
180
+ def add_origin_host_and_realm(host, realm)
181
+ @avps << AVP.create("Origin-Host", host) unless has_avp? 'Origin-Host'
182
+ @avps << AVP.create("Origin-Realm", realm) unless has_avp? 'Origin-Realm'
183
+ end
184
+
185
+ # @!endgroup
186
+
187
+ # @!group Parsing
188
+
189
+ # Parses the first four bytes of the Diameter header to learn the
190
+ # length. Callers should use this to work out how many more bytes
191
+ # they need to read off a TCP connection to pass to self.from_bytes.
192
+ #
193
+ # @param header [String] A four-byte Diameter header
194
+ # @return [Fixnum] The message length field from the header
195
+ def self.length_from_header(header)
196
+ _version, length_8, length_16 = header.unpack('CCn')
197
+ Internals::UInt24.from_u8_and_u16(length_8, length_16)
198
+ end
199
+
200
+ # Parses a byte representation (a 20-byte header plus AVPs) into a
201
+ # DiameterMessage object.
202
+ #
203
+ # @param bytes [String] The on-the-wire byte representation of a
204
+ # Diameter message.
205
+ # @return [DiameterMessage] The parsed object form.
206
+ def self.from_bytes(bytes)
207
+ header = bytes[0..20]
208
+ version, _length_8, _length_16, flags_str, code_8, code_16, app_id, hbh, ete = header.unpack('CCnB8CnNNN')
209
+ command_code = Internals::UInt24.from_u8_and_u16(code_8, code_16)
210
+
211
+ request = (flags_str[0] == '1')
212
+ proxyable = (flags_str[1] == '1')
213
+
214
+ avps = Internals::AVPParser.parse_avps_int(bytes[20..-1])
215
+ Message.new(version: version, command_code: command_code, app_id: app_id, hbh: hbh, ete: ete, request: request, proxyable: proxyable, retransmitted: false, error: false, avps: avps)
216
+ end
217
+
218
+ # @!endgroup
219
+
220
+ # Generates an answer to this request, filling in a Result-Code or
221
+ # Experimental-Result AVP.
222
+ #
223
+ # @param result_code [Fixnum] The value for the Result-Code AVP
224
+ # @option opts [Fixnum] experimental_result_vendor
225
+ # If given, creates an Experimental-Result AVP with this vendor
226
+ # instead of the Result-Code AVP.
227
+ # @option opts [Array<String>] copying_avps
228
+ # A list of AVP names to copy from the request to the answer.
229
+ # @option opts [Array<Diameter::AVP>] avps
230
+ # A list of AVP objects to add on the answer.
231
+ # @return [Diameter::Message] The response created.
232
+ def create_answer(result_code, opts={})
233
+ fail "Cannot answer an answer" if answer
234
+
235
+ avps = opts.fetch(:avps, [])
236
+ avps << if opts[:experimental_result_vendor]
237
+ fail
238
+ else
239
+ AVP.create("Result-Code", result_code)
240
+ end
241
+
242
+ avps += opts.fetch(:copying_avps, []).collect do |name|
243
+ src_avp = avp_by_name(name)
244
+
245
+ fail if src_avp.nil?
246
+
247
+ src_avp.dup
248
+ end
249
+
250
+ Message.new(version: version, command_code: command_code, app_id: app_id, hbh: hbh, ete: ete, request: false, proxyable: @proxyable, retransmitted: false, error: false, avps: avps)
251
+ end
252
+
253
+ private
254
+ def self.next_hbh
255
+ @hbh ||= rand(10000)
256
+ @hbh += 1
257
+ @hbh
258
+ end
259
+
260
+ def self.next_ete
261
+ @ete ||= (Time.now.to_i & 0x00000fff) + (rand(2**32) & 0xfffff000)
262
+ @ete += 1
263
+ @ete
264
+ end
265
+
266
+ end
267
+ end
@@ -0,0 +1,59 @@
1
+ require 'diameter/diameter_logger'
2
+
3
+ module Diameter
4
+ # A Diameter peer entry in the peer table.
5
+ #
6
+ # @!attribute [rw] identity
7
+ # [String] The DiameterIdentity of this peer
8
+ # @!attribute [rw] realm
9
+ # [String] The Diameter realm of this peer
10
+ # @!attribute [rw] static
11
+ # [true, false] Whether this peer was dynamically discovered (and so
12
+ # might expire) or statically configured.
13
+ # @!attribute [rw] expiry_time
14
+ # [Time] For a dynamically discovered peer, the time when it stops
15
+ # being valid and dynamic discovery must happen again.
16
+ # @!attribute [rw] last_message_seen
17
+ # [Time] The last time traffic was received from this peer. Used for
18
+ # determining when to send watchdog messages, or for triggering failover.
19
+ # @!attribute [rw] cxn
20
+ # [Socket] The underlying network connection to this peer.
21
+ # @!attribute [rw] state
22
+ # [Keyword] The current state of this peer - :UP, :WATING or :CLOSED.
23
+
24
+ class Peer
25
+ attr_accessor :identity, :static, :cxn, :realm, :expiry_time, :last_message_seen
26
+ attr_reader :state
27
+
28
+ def initialize(identity)
29
+ @identity = identity
30
+ @state = :CLOSED
31
+ @state_change_q = Queue.new
32
+ end
33
+
34
+ # Blocks until the state of this peer changes to the desired value.
35
+ #
36
+ # @param state [Keyword] The state to change to.
37
+ def wait_for_state_change(state)
38
+ cur_state = @state
39
+ while (cur_state != state)
40
+ cur_state = @state_change_q.pop
41
+ end
42
+ end
43
+
44
+ # @todo Add further checking, making sure that the transition to
45
+ # new_state is valid according to the RFC 6733 state machine. Maybe
46
+ # use the micromachine gem?
47
+ def state=(new_state)
48
+ Diameter.logger.log(Logger::DEBUG, "State of peer #{identity} changed from #{@state} to #{new_state}")
49
+ @state = new_state
50
+ @state_change_q.push new_state
51
+ end
52
+
53
+ # Resets the last message seen time. Should be called when a message
54
+ # is received from this peer.
55
+ def reset_timer
56
+ self.last_message_seen = Time.now
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,359 @@
1
+ require 'uri'
2
+ require 'socket'
3
+ require 'diameter/peer'
4
+ require 'diameter/message'
5
+ require 'diameter/stack_transport_helpers'
6
+ require 'diameter/diameter_logger'
7
+ require 'concurrent'
8
+
9
+ module Diameter
10
+ class Stack
11
+ include Internals
12
+
13
+ # @!group Setup methods
14
+
15
+ # Stack constructor.
16
+ #
17
+ # @note The stack does not advertise any applications to peers by
18
+ # default - {#add_handler} must be called early on.
19
+ #
20
+ # @param host [String] The Diameter Identity of this stack (for
21
+ # the Origin-Host AVP).
22
+ # @param realm [String] The Diameter realm of this stack (for
23
+ # the Origin-Realm AVP).
24
+ def initialize(host, realm)
25
+ @local_host = host
26
+ @local_realm = realm
27
+
28
+ @auth_apps = []
29
+ @acct_apps = []
30
+
31
+ @pending_ete = {}
32
+
33
+ @tcp_helper = TCPStackHelper.new(self)
34
+ @peer_table = {}
35
+ @handlers = {}
36
+
37
+ @threadpool = pool = Concurrent::ThreadPoolExecutor.new(
38
+ min_threads: 5,
39
+ max_threads: 5,
40
+ max_queue: 100,
41
+ overflow_policy: :caller_runs
42
+ )
43
+
44
+
45
+ Diameter.logger.log(Logger::INFO, 'Stack initialized')
46
+ end
47
+
48
+ # Complete the stack initialization and begin reading from the TCP connections.
49
+ def start
50
+ @tcp_helper.start_main_loop
51
+ end
52
+
53
+ # Begins listening for inbound Diameter connections (making this a
54
+ # Diameter server instead of just a client).
55
+ #
56
+ # @param port [Fixnum] The TCP port to listen on (default 3868)
57
+ def listen_for_tcp(port=3868)
58
+ @tcp_helper.setup_new_listen_connection("0.0.0.0", port)
59
+ end
60
+
61
+ # Adds a handler for a specific Diameter application.
62
+ #
63
+ # @note If you expect to only send requests for this application,
64
+ # not receive them, the block can be a no-op (e.g. `{ nil }`)
65
+ #
66
+ # @param app_id [Fixnum] The Diameter application ID.
67
+ # @option opts [true, false] auth
68
+ # Whether we should advertise support for this application in
69
+ # the Auth-Application-ID AVP. Note that at least one of auth or
70
+ # acct must be specified.
71
+ # @option opts [true, false] acct
72
+ # Whether we should advertise support for this application in
73
+ # the Acct-Application-ID AVP. Note that at least one of auth or
74
+ # acct must be specified.
75
+ # @option opts [Fixnum] vendor
76
+ # If we should advertise support for this application in a
77
+ # Vendor-Specific-Application-Id AVP, this specifies the
78
+ # associated Vendor-Id.
79
+ #
80
+ # @yield [req, cxn] Passes a Diameter message (and its originating
81
+ # connection) for application-specific handling.
82
+ # @yieldparam [Message] req The parsed Diameter message from the peer.
83
+ # @yieldparam [Socket] cxn The TCP connection to the peer, to be
84
+ # passed to {Stack#send_answer}.
85
+ def add_handler(app_id, opts={}, &blk)
86
+ vendor = opts.fetch(:vendor, 0)
87
+ auth = opts.fetch(:auth, false)
88
+ acct = opts.fetch(:acct, false)
89
+
90
+ raise ArgumentError.new("Must specify at least one of auth or acct") unless auth or acct
91
+
92
+ @acct_apps << [app_id, vendor] if acct
93
+ @auth_apps << [app_id, vendor] if auth
94
+
95
+ @handlers[app_id] = blk
96
+ end
97
+
98
+ # @!endgroup
99
+
100
+ # This shuts the stack down, closing all TCP connections and
101
+ # terminating any background threads still waiting for an answer.
102
+ def shutdown
103
+ @tcp_helper.shutdown
104
+ @pending_ete.each do |ete, q|
105
+ Diameter.logger.debug("Shutting down queue #{q} as no answer has been received with EtE #{ete}")
106
+ q.push :shutdown
107
+ end
108
+ @threadpool.kill
109
+ @threadpool.wait_for_termination(5)
110
+ end
111
+
112
+ # Closes the given connection, blanking out any internal data
113
+ # structures associated with it.
114
+ #
115
+ # Likely to be moved to the Peer object in a future release/
116
+ #
117
+ # @param connection [Socket] The connection to close.
118
+ def close(connection)
119
+ @tcp_helper.close(connection)
120
+ end
121
+
122
+ # @!group Peer connections and message sending
123
+
124
+ # Creates a Peer connection to a Diameter agent at the specific
125
+ # network location indicated by peer_uri.
126
+ #
127
+ # @param peer_uri [URI] The aaa:// URI identifying the peer. Should
128
+ # contain a hostname/IP; may contain a port (default 3868).
129
+ # @param peer_host [String] The DiameterIdentity of this peer, which
130
+ # will uniquely identify it in the peer table.
131
+ # @param realm [String] The Diameter realm of this peer.
132
+ def connect_to_peer(peer_uri, peer_host, realm)
133
+ uri = URI(peer_uri)
134
+ cxn = @tcp_helper.setup_new_connection(uri.host, uri.port)
135
+ avps = [AVP.create('Origin-Host', @local_host),
136
+ AVP.create('Origin-Realm', @local_realm),
137
+ AVP.create('Host-IP-Address', IPAddr.new('127.0.0.1')),
138
+ AVP.create('Vendor-Id', 100),
139
+ AVP.create('Product-Name', 'ruby-diameter')
140
+ ]
141
+ avps += app_avps
142
+ cer_bytes = Message.new(version: 1, command_code: 257, app_id: 0, request: true, proxyable: false, retransmitted: false, error: false, avps: avps).to_wire
143
+ @tcp_helper.send(cer_bytes, cxn)
144
+ @peer_table[peer_host] = Peer.new(peer_host)
145
+ @peer_table[peer_host].state = :WAITING
146
+ @peer_table[peer_host].cxn = cxn
147
+ @peer_table[peer_host]
148
+ # Will move to :UP when the CEA is received
149
+ end
150
+
151
+ # Sends a Diameter request. This is routed to an appropriate peer
152
+ # based on the Destination-Host AVP.
153
+ #
154
+ # This adds this stack's Origin-Host and Origin-Realm AVPs, if
155
+ # those AVPs don't already exist.
156
+ #
157
+ # @param req [Message] The request to send.
158
+ def send_request(req)
159
+ fail "Must pass a request" unless req.request
160
+ req.add_origin_host_and_realm(@local_host, @local_realm)
161
+ peer_name = req.avp_by_name('Destination-Host').octet_string
162
+ state = peer_state(peer_name)
163
+ if state == :UP
164
+ peer = @peer_table[peer_name]
165
+ @tcp_helper.send(req.to_wire, peer.cxn)
166
+ q = Queue.new
167
+ @pending_ete[req.ete] = q
168
+ p = Concurrent::Promise.execute(executor: @threadpool) {
169
+ Diameter.logger.debug("Waiting for answer to message with EtE #{req.ete}, queue #{q}")
170
+ val = q.pop
171
+ Diameter.logger.debug("Promise fulfilled for message with EtE #{req.ete}")
172
+ val
173
+ }
174
+ return p
175
+ else
176
+ Diameter.logger.log(Logger::WARN, "Peer #{peer_name} is in state #{state} - cannot route")
177
+ end
178
+ end
179
+
180
+ # Sends a Diameter answer. This is sent over the same connection
181
+ # the request was received on (which needs to be passed into to
182
+ # this method).
183
+ #
184
+ # This adds this stack's Origin-Host and Origin-Realm AVPs, if
185
+ # those AVPs don't already exist.
186
+ #
187
+ # @param ans [Message] The Diameter answer
188
+ # @param original_cxn [Socket] The connection which the request
189
+ # came in on. This will have been passed to the block registered
190
+ # with {Stack#add_handler}.
191
+ def send_answer(ans, original_cxn)
192
+ fail "Must pass an answer" unless ans.answer
193
+ ans.add_origin_host_and_realm(@local_host, @local_realm)
194
+ @tcp_helper.send(ans.to_wire, original_cxn)
195
+ end
196
+
197
+ # Retrieves the current state of a peer, defaulting to :CLOSED if
198
+ # the peer does not exist.
199
+ #
200
+ # @param id [String] The Diameter identity of the peer.
201
+ # @return [Keyword] The state of the peer (:UP, :WAITING or :CLOSED).
202
+ def peer_state(id)
203
+ if !@peer_table.key? id
204
+ :CLOSED
205
+ else
206
+ @peer_table[id].state
207
+ end
208
+ end
209
+
210
+ # @!endgroup
211
+
212
+ # @private
213
+ # Handles a Diameter request straight from a network connection.
214
+ # Intended to be called by TCPStackHelper after it retrieves a
215
+ # message, not directly by users.
216
+ def handle_message(msg_bytes, cxn)
217
+ # Common processing - ensure that this message has come in on this
218
+ # peer's expected connection, and update the last time we saw
219
+ # activity on this peer
220
+ msg = Message.from_bytes(msg_bytes)
221
+ Diameter.logger.debug("Handling message #{msg}")
222
+ peer = msg.avp_by_name('Origin-Host').octet_string
223
+ if @peer_table[peer]
224
+ @peer_table[peer].reset_timer
225
+ unless @peer_table[peer].cxn == cxn
226
+ Diameter.logger.log(Logger::WARN, "Ignoring message - claims to be from #{peer} but comes from #{cxn} not #{@peer_table[peer].cxn}")
227
+ end
228
+ end
229
+
230
+ if msg.command_code == 257 && msg.answer
231
+ handle_cea(msg)
232
+ elsif msg.command_code == 257 && msg.request
233
+ handle_cer(msg, cxn)
234
+ elsif msg.command_code == 280 && msg.request
235
+ handle_dwr(msg, cxn)
236
+ elsif msg.command_code == 280 && msg.answer
237
+ # No-op - we've already updated our timestamp
238
+ elsif msg.answer
239
+ handle_other_answer(msg, cxn)
240
+ elsif @handlers.has_key? msg.app_id
241
+ @handlers[msg.app_id].call(msg, cxn)
242
+ else
243
+ fail "Received unknown message of type #{msg.command_code}"
244
+ end
245
+ end
246
+
247
+ private
248
+
249
+ def app_avps
250
+ avps = []
251
+
252
+ @auth_apps.each do |app_id, vendor|
253
+ avps << if vendor == 0
254
+ AVP.create("Auth-Application-Id", app_id)
255
+ else
256
+ AVP.create("Vendor-Specific-Application-Id",
257
+ [AVP.create("Auth-Application-Id", app_id),
258
+ AVP.create("Vendor-Id", vendor)])
259
+ end
260
+ end
261
+
262
+ @acct_apps.each do |app_id, vendor|
263
+ avps << if vendor == 0
264
+ AVP.create("Acct-Application-Id", app_id)
265
+ else
266
+ AVP.create("Vendor-Specific-Application-Id",
267
+ [AVP.create("Acct-Application-Id", app_id),
268
+ AVP.create("Vendor-Id", vendor)])
269
+ end
270
+ end
271
+
272
+ avps
273
+ end
274
+
275
+ def shared_apps(capabilities_msg)
276
+ peer_apps = capabilities_msg.all_avps_by_name("Auth-Application-Id").collect(&:uint32)
277
+ peer_apps += capabilities_msg.all_avps_by_name("Acct-Application-Id").collect(&:uint32)
278
+
279
+ capabilities_msg.all_avps_by_name("Vendor-Specific-Application-Id").each do |avp|
280
+ if avp.inner_avp("Auth-Application-Id")
281
+ peer_apps << avp.inner_avp("Auth-Application-Id").uint32
282
+ end
283
+
284
+ if avp.inner_avp("Acct-Application-Id")
285
+ peer_apps << avp.inner_avp("Acct-Application-Id").uint32
286
+ end
287
+ end
288
+
289
+ Diameter.logger.debug("Received app IDs #{peer_apps} from peer, have apps #{@handlers.keys}")
290
+
291
+ @handlers.keys.to_set & peer_apps.to_set
292
+ end
293
+
294
+ def handle_cer(cer, cxn)
295
+ if shared_apps(cer).empty?
296
+ rc = 5010
297
+ else
298
+ rc = 2001
299
+ end
300
+
301
+ cea = cer.create_answer(rc, avps:
302
+ [AVP.create('Origin-Host', @local_host),
303
+ AVP.create('Origin-Realm', @local_realm)] + app_avps)
304
+
305
+ @tcp_helper.send(cea.to_wire, cxn)
306
+
307
+ if rc == 2001
308
+ peer = cer.avp_by_name('Origin-Host').octet_string
309
+ Diameter.logger.debug("Creating peer table entry for peer #{peer}")
310
+ @peer_table[peer] = Peer.new(peer)
311
+ @peer_table[peer].state = :UP
312
+ @peer_table[peer].reset_timer
313
+ @peer_table[peer].cxn = cxn
314
+ else
315
+ @tcp_helper.close(cxn)
316
+ end
317
+ end
318
+
319
+ def handle_cea(cea)
320
+ peer = cea.avp_by_name('Origin-Host').octet_string
321
+ if @peer_table.has_key? peer
322
+ @peer_table[peer].state = :UP
323
+ @peer_table[peer].reset_timer
324
+ else
325
+ Diameter.logger.warn("Ignoring CEA from unknown peer #{peer}")
326
+ Diameter.logger.debug("Known peers are #{@peer_table.keys}")
327
+ end
328
+ end
329
+
330
+ def handle_dpr
331
+ end
332
+
333
+ def handle_dpa
334
+ end
335
+
336
+ def handle_dwr(dwr, cxn)
337
+ dwa = dwr.create_answer(2001, avps:
338
+ [AVP.create('Origin-Host', @local_host),
339
+ AVP.create('Origin-Realm', @local_realm)])
340
+
341
+ @tcp_helper.send(dwa.to_wire, cxn)
342
+ # send DWA
343
+ end
344
+
345
+ def handle_dwa
346
+ end
347
+
348
+ def handle_other_request
349
+ end
350
+
351
+ def handle_other_answer(msg, _cxn)
352
+ Diameter.logger.debug("Handling answer with End-to-End identifier #{msg.ete}")
353
+ q = @pending_ete[msg.ete]
354
+ q.push msg
355
+ Diameter.logger.debug("Passed answer to fulfil sender's Promise object'")
356
+ @pending_ete.delete msg.ete
357
+ end
358
+ end
359
+ end