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.
- checksums.yaml +7 -0
- data/lib/diameter/avp.rb +391 -0
- data/lib/diameter/avp_parser.rb +81 -0
- data/lib/diameter/constants.rb +54 -0
- data/lib/diameter/diameter_logger.rb +24 -0
- data/lib/diameter/fsm.rb +27 -0
- data/lib/diameter/message.rb +267 -0
- data/lib/diameter/peer.rb +59 -0
- data/lib/diameter/stack.rb +359 -0
- data/lib/diameter/stack_transport_helpers.rb +167 -0
- data/lib/diameter/u24.rb +36 -0
- data/lib/diameter.rb +2 -0
- metadata +139 -0
@@ -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
|