aboutsummaryrefslogtreecommitdiffstats
path: root/lib/diameter/src/base/diameter_capx.erl
blob: 30ac847a54f85c482314633a4e1c3056915eb5f7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2010-2017. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%

%%
%% This module builds CER and CEA records for use during capabilities
%% exchange. All of a CER/CEA is built from AVP values configured on
%% the service in question but values for Supported-Vendor-Id,
%% Vendor-Specific-Application-Id, Auth-Application-Id and
%% Acct-Application-id are also obtained using an older method that
%% remains only for backwards compatibility. With this method, each
%% dictionary module was required to export a cer/0 that returned a
%% diameter_base_CER record (or corresponding list, although the list
%% is also a later addition). Each returned CER contributes its member
%% values for the aforementioned four AVPs to the resulting CER, with
%% remaining AVP's either unspecified or identical to those configured
%% on the service. Auth-Application-Id and Acct-Application-id were
%% originally treated a little differently, each callback being
%% required to return either no value of the same value as the other
%% callbacks, but this coupled the callback modules unnecessarily. (A
%% union is backwards compatible to boot.)
%%
%% Values obtained from the service and callbacks are all included
%% when building a CER. Older code with only callback can continue to
%% use them, newer code should probably stick to service configuration
%% (since this is more explicit) or mix at their own peril.
%%
%% The cer/0 callback is now undocumented (despite never being fully
%% documented to begin with) and should be considered deprecated even
%% by those poor souls still using it.
%%

-module(diameter_capx).

-export([build_CER/2,
         recv_CER/3,
         recv_CEA/3,
         make_caps/2,
         binary_caps/1]).

-include_lib("diameter/include/diameter.hrl").
-include("diameter_internal.hrl").

-define(SUCCESS,    2001).  %% DIAMETER_SUCCESS
-define(NOAPP,      5010).  %% DIAMETER_NO_COMMON_APPLICATION
-define(NOSECURITY, 5017).  %% DIAMETER_NO_COMMON_SECURITY

-define(NO_INBAND_SECURITY, 0).
-define(TLS, 1).

%% ===========================================================================

-type tried(T) :: {ok, T} | {error, {term(), list()}}.

-spec build_CER(#diameter_caps{}, module())
   -> tried(CER)
 when CER :: tuple().

build_CER(Caps, Dict) ->
    try_it([fun bCER/2, Caps, Dict]).

-spec recv_CER(CER, #diameter_service{}, module())
   -> tried({[diameter:'Unsigned32'()],
             #diameter_caps{},
             CEA})
 when CER :: tuple(),
      CEA :: tuple().

recv_CER(CER, Svc, Dict) ->
    try_it([fun rCER/3, CER, Svc, Dict]).

-spec recv_CEA(CEA, #diameter_service{}, module())
   -> tried({[diameter:'Unsigned32'()],
             [diameter:'Unsigned32'()],
             #diameter_caps{}})
 when CEA :: tuple().

recv_CEA(CEA, Svc, Dict) ->
    try_it([fun rCEA/3, CEA, Svc, Dict]).

make_caps(Caps, Opts) ->
    try_it([fun mk_caps/2, Caps, Opts]).

%% ===========================================================================
%% ===========================================================================

try_it([Fun | Args]) ->
    try apply(Fun, Args) of
        T -> {ok, T}
    catch
        throw: ?FAILURE(Reason) -> {error, Reason}
    end.

%% mk_caps/2

mk_caps(Caps0, Opts) ->
    {Caps, _} = lists:foldl(fun set_cap/2,
                            {Caps0, #diameter_caps{_ = false}},
                            Opts),
    Caps.

-define(SC(K,F),
        set_cap({K, Val}, {Caps, #diameter_caps{F = false} = C}) ->
            {Caps#diameter_caps{F = cap(K, copy(Val))},
             C#diameter_caps{F = true}}).

?SC('Origin-Host',         origin_host);
?SC('Origin-Realm',        origin_realm);
?SC('Host-IP-Address',     host_ip_address);
?SC('Vendor-Id',           vendor_id);
?SC('Product-Name',        product_name);
?SC('Origin-State-Id',     origin_state_id);
?SC('Supported-Vendor-Id', supported_vendor_id);
?SC('Auth-Application-Id', auth_application_id);
?SC('Inband-Security-Id',  inband_security_id);
?SC('Acct-Application-Id', acct_application_id);
?SC('Vendor-Specific-Application-Id', vendor_specific_application_id);
?SC('Firmware-Revision',   firmware_revision);

set_cap({Key, _}, _) ->
    ?THROW({duplicate, Key}).

cap(K, V)
  when K == 'Origin-Host';
       K == 'Origin-Realm';
       K == 'Vendor-Id';
       K == 'Product-Name' ->
    V;

cap('Host-IP-Address', Vs)
  when is_list(Vs) ->
    lists:map(fun ipaddr/1, Vs);

cap(K, V)
  when K == 'Firmware-Revision';
       K == 'Origin-State-Id' ->
    [V];

cap(_, Vs)
  when is_list(Vs) ->
    Vs;

cap(K, V) ->
    ?THROW({invalid, {K,V}}).

ipaddr(A) ->
    try
        diameter_lib:ipaddr(A)
    catch
        error: {invalid_address, _} = T ->
            ?THROW(T)
    end.

%% bCER/2
%%
%% Build a CER record to send to a remote peer.

%% Use the fact that diameter_caps is expected to have the same field
%% names as CER.
bCER(#diameter_caps{} = Rec, Dict) ->
    RecName = Dict:msg2rec('CER'),
    Values = lists:zip(Dict:'#info-'(RecName, fields),
                       tl(tuple_to_list(Rec))),
    Dict:'#new-'(RecName, [{K, map(K, V, Dict)} || {K,V} <- Values]).

%% map/3
%%
%% Deal with differerences in common dictionary AVP's to make changes
%% transparent in service/transport config. In particular, one
%% annoying difference between RFC 3588 and RFC 6733.
%%
%% RFC 6773 changes the definition of Vendor-Specific-Application-Id,
%% giving Vendor-Id arity 1 instead of 3588's 1*. This causes woe
%% since the corresponding dictionaries expect different values for a
%% 'Vendor-Id': a list for 3588, an integer for 6733.

map('Vendor-Specific-Application-Id' = T, L, Dict) ->
    RecName = Dict:name2rec(T),
    Rec = Dict:'#new-'(RecName, []),
    Def = Dict:'#get-'('Vendor-Id', Rec),
    [vsa(V, Def) || V <- L];
map(_, V, _) ->
    V.

vsa({_, N, _, _} = Rec, [])
  when is_integer(N) ->
    setelement(2, Rec, [N]);

vsa({_, [N], _, _} = Rec, undefined)
  when is_integer(N) ->
    setelement(2, Rec, N);

vsa([_|_] = L, Def) ->
    [vid(T, Def) || T <- L];

vsa(T, _) ->
    T.

vid({'Vendor-Id' = K, N}, [])
  when is_integer(N) ->
    {K, [N]};

vid({'Vendor-Id' = K, [N]}, undefined) ->
    {K, N};

vid(T, _) ->
    T.

%% rCER/3
%%
%% Build a CEA record to send to a remote peer in response to an
%% incoming CER. RFC 3588 gives no guidance on what should be sent
%% here: should we advertise applications that the peer hasn't sent in
%% its CER (aside from the relay application) or not? If we send
%% applications that the peer hasn't advertised then the peer may have
%% to be aware of the possibility. If we don't then we just look like
%% a server that supports a subset (possibly) of what the client
%% advertised, so this feels like the path of least incompatibility.
%% However, the current draft standard (draft-ietf-dime-rfc3588bis-26,
%% expires 24 July 2011) says this in section 5.3, Capabilities
%% Exchange:
%%
%%   The receiver of the Capabilities-Exchange-Request (CER) MUST
%%   determine common applications by computing the intersection of its
%%   own set of supported Application Id against all of the application
%%   identifier AVPs (Auth-Application-Id, Acct-Application-Id and Vendor-
%%   Specific-Application-Id) present in the CER.  The value of the
%%   Vendor-Id AVP in the Vendor-Specific-Application-Id MUST NOT be used
%%   during computation.  The sender of the Capabilities-Exchange-Answer
%%   (CEA) SHOULD include all of its supported applications as a hint to
%%   the receiver regarding all of its application capabilities.
%%
%% Both RFC and the draft also say this:
%%
%%   The receiver only issues commands to its peers that have advertised
%%   support for the Diameter application that defines the command.  A
%%   Diameter node MUST cache the supported applications in order to
%%   ensure that unrecognized commands and/or AVPs are not unnecessarily
%%   sent to a peer.
%%
%% That is, each side sends all of its capabilities and is responsible for
%% not sending commands that the peer doesn't support.

%% 6.10.  Inband-Security-Id AVP
%%
%%   NO_INBAND_SECURITY                0
%%      This peer does not support TLS.  This is the default value, if the
%%      AVP is omitted.
%%
%%   TLS                               1
%%      This node supports TLS security, as defined by [TLS].

rCER(CER, #diameter_service{capabilities = LCaps} = Svc, Dict) ->
    CEA = cea_from_cer(bCER(LCaps, Dict), Dict),
    RCaps = capx_to_caps(CER, Dict),
    SApps = common_applications(LCaps, RCaps, Svc),

    {SApps,
     RCaps,
     build_CEA(SApps,
               LCaps,
               RCaps,
               Dict,
               Dict:'#set-'({'Result-Code', ?SUCCESS}, CEA))}.

build_CEA([], _, _, Dict, CEA) ->
    Dict:'#set-'({'Result-Code', ?NOAPP}, CEA);

build_CEA(_, LCaps, RCaps, Dict, CEA) ->
    case common_security(LCaps, RCaps) of
        [] ->
            Dict:'#set-'({'Result-Code', ?NOSECURITY}, CEA);
        [_] = IS ->
            Dict:'#set-'({'Inband-Security-Id', inband_security(IS)}, CEA)
    end.

%% Only set Inband-Security-Id if different from the default, since
%% RFC 6733 recommends against the AVP:
%%
%% 6.10.  Inband-Security-Id AVP
%%
%%    The Inband-Security-Id AVP (AVP Code 299) is of type Unsigned32 and
%%    is used in order to advertise support of the security portion of the
%%    application.  The use of this AVP in CER and CEA messages is NOT
%%    RECOMMENDED.  Instead, discovery of a Diameter entity's security
%%    capabilities can be done either through static configuration or via
%%    Diameter Peer Discovery as described in Section 5.2.

inband_security([?NO_INBAND_SECURITY]) ->
    [];
inband_security([_] = IS) ->
    IS.

%% common_security/2

common_security(#diameter_caps{inband_security_id = LS},
                #diameter_caps{inband_security_id = RS}) ->
    cs(LS, RS).

%% Unspecified is equivalent to NO_INBAND_SECURITY.
cs([], RS) ->
    cs([?NO_INBAND_SECURITY], RS);
cs(LS, []) ->
    cs(LS, [?NO_INBAND_SECURITY]);

%% Agree on TLS if both parties support it. When sending CEA, this is
%% to ensure the peer is clear that we will be expecting a TLS
%% handshake since there is no ssl:maybe_accept that would allow the
%% peer to choose between TLS or not upon reception of our CEA. When
%% receiving CEA it deals with a server that isn't explicit about its choice.
%% TODO: Make the choice configurable.
cs(LS, RS) ->
    Is = ordsets:to_list(ordsets:intersection(ordsets:from_list(LS),
                                              ordsets:from_list(RS))),
    case lists:member(?TLS, Is) of
        true ->
            [?TLS];
        false when [] == Is ->
            Is;
        false ->
            [hd(Is)]  %% probably NO_INBAND_SECURITY
    end.
%% The only two values defined by RFC 3588 are NO_INBAND_SECURITY and
%% TLS but don't enforce this. In theory this allows some other
%% security mechanism we don't have to know about, although in
%% practice something there may be a need for more synchronization
%% than notification by way of an event subscription offers.

%% cea_from_cer/2

%% CER is a subset of CEA, the latter adding Result-Code and a few
%% more AVP's.
cea_from_cer(CER, Dict) ->
    RecName = Dict:msg2rec('CEA'),
    [_ | Values] = Dict:'#get-'(CER),
    Dict:'#new-'([RecName | Values]).

%% rCEA/3

rCEA(CEA, #diameter_service{capabilities = LCaps} = Svc, Dict) ->
    RCaps = capx_to_caps(CEA, Dict),
    SApps = common_applications(LCaps, RCaps, Svc),
    IS = common_security(LCaps, RCaps),

    {SApps, IS, RCaps}.

%% capx_to_caps/2

capx_to_caps(CEX, Dict) ->
    [OH, OR, IP, VId, PN, OSI, SV, Auth, IS, Acct, VSA, FR, X]
        = Dict:'#get-'(['Origin-Host',
                        'Origin-Realm',
                        'Host-IP-Address',
                        'Vendor-Id',
                        'Product-Name',
                        'Origin-State-Id',
                        'Supported-Vendor-Id',
                        'Auth-Application-Id',
                        'Inband-Security-Id',
                        'Acct-Application-Id',
                        'Vendor-Specific-Application-Id',
                        'Firmware-Revision',
                        'AVP'],
                       CEX),
    #diameter_caps{origin_host = copy(OH),
                   origin_realm = copy(OR),
                   vendor_id = VId,
                   product_name = copy(PN),
                   origin_state_id = OSI,
                   host_ip_address = IP,
                   supported_vendor_id = SV,
                   auth_application_id = Auth,
                   inband_security_id = IS,
                   acct_application_id = Acct,
                   vendor_specific_application_id = VSA,
                   firmware_revision = FR,
                   avp = X}.

%% Copy binaries to avoid retaining a reference to a large binary
%% containing AVPs we aren't interested in.
copy(B)
  when is_binary(B) ->
    binary:copy(B);

copy(T) ->
    T.

%% binary_caps/1
%%
%% Encode stringish capabilities with {string_decode, false}.

binary_caps(Caps) ->
    lists:foldl(fun bcaps/2, Caps, [#diameter_caps.origin_host,
                                    #diameter_caps.origin_realm,
                                    #diameter_caps.product_name]).

bcaps(N, Caps) ->
    case element(N, Caps) of
        undefined ->
            Caps;
        V ->
            setelement(N, Caps, iolist_to_binary(V))
    end.

%% ---------------------------------------------------------------------------
%% ---------------------------------------------------------------------------

%% common_applications/3
%%
%% Identify the (local) applications to be supported on the connection
%% in question.

common_applications(LCaps, RCaps, #diameter_service{applications = Apps}) ->
    LA = app_union(LCaps),
    RA = app_union(RCaps),

    lists:foldl(fun(I,A) -> ca(I, Apps, RA, A) end, [], LA).

ca(Id, Apps, RA, Acc) ->
    Relay = lists:member(?APP_ID_RELAY, RA),
    #diameter_app{alias = Alias} = find_app(Id, Apps),
    tcons(Relay                        %% peer is a relay
          orelse ?APP_ID_RELAY == Id   %% we're a relay
          orelse lists:member(Id, RA), %% app is supported by the peer
          Id,
          Alias,
          Acc).
%% 5.3 of the RFC states that a peer advertising itself as a relay must
%% be interpreted as having common applications.

%% Extract the list of all application identifiers from Auth-Application-Id,
%% Acct-Application-Id and Vendor-Specific-Application-Id.
app_union(#diameter_caps{auth_application_id = U,
                         acct_application_id = C,
                         vendor_specific_application_id = V}) ->
    set_list(U ++ C ++ lists:flatmap(fun vsa_apps/1, V)).

vsa_apps(Vals)
  when is_list(Vals) ->
    lists:flatmap(fun({'Vendor-Id', _}) -> []; ({_, Ids}) -> Ids end, Vals);
vsa_apps(Rec)
  when is_tuple(Rec) ->
    [_Name, _VendorId | Idss] = tuple_to_list(Rec),
    lists:append(Idss).

%% It's a configuration error for a locally advertised application not
%% to be represented in Apps. Don't just match on lists:keyfind/3 in
%% order to generate a more helpful error.
find_app(Id, Apps) ->
    case lists:keyfind(Id, #diameter_app.id, Apps) of
        #diameter_app{} = A ->
            A;
        false ->
            ?THROW({app_not_configured, Id})
    end.

set_list(L) ->
    sets:to_list(sets:from_list(L)).

tcons(true, K, V, Acc) ->
    [{K,V} | Acc];
tcons(false, _, _, Acc) ->
    Acc.