diff options
Diffstat (limited to 'lib/diameter/include/diameter_gen.hrl')
-rw-r--r-- | lib/diameter/include/diameter_gen.hrl | 442 |
1 files changed, 375 insertions, 67 deletions
diff --git a/lib/diameter/include/diameter_gen.hrl b/lib/diameter/include/diameter_gen.hrl index 55aae3a243..611ad796a9 100644 --- a/lib/diameter/include/diameter_gen.hrl +++ b/lib/diameter/include/diameter_gen.hrl @@ -1,18 +1,19 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2010-2013. All Rights Reserved. +%% Copyright Ericsson AB 2010-2015. All Rights Reserved. %% -%% The contents of this file are subject to the Erlang Public License, -%% Version 1.1, (the "License"); you may not use this file except in -%% compliance with the License. You should have received a copy of the -%% Erlang Public License along with this software. If not, it can be -%% retrieved online at http://www.erlang.org/. +%% 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 %% -%% Software distributed under the License is distributed on an "AS IS" -%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%% the License for the specific language governing rights and limitations -%% under the License. +%% 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% %% @@ -25,6 +26,21 @@ -define(THROW(T), throw({?MODULE, T})). +%% Tag common to generated dictionaries. +-define(TAG, diameter_gen). + +%% Key to a value in the process dictionary that determines whether or +%% not an unrecognized AVP setting the M-bit should be regarded as an +%% error or not. See is_strict/0. This is only used to relax M-bit +%% interpretation inside Grouped AVPs not setting the M-bit. The +%% service_opt() strict_mbit can be used to disable the check +%% globally. +-define(STRICT_KEY, strict). + +%% Key that says whether or not we should do a best-effort decode +%% within Failed-AVP. +-define(FAILED_KEY, failed). + -type parent_name() :: atom(). %% parent = Message or AVP -type parent_record() :: tuple(). %% -type avp_name() :: atom(). @@ -35,6 +51,25 @@ -type grouped_avp() :: nonempty_improper_list(#diameter_avp{}, [avp()]). -type avp() :: non_grouped_avp() | grouped_avp(). +%% Use a (hopefully) unique key when manipulating the process +%% dictionary. + +putr(K,V) -> + put({?TAG, K}, V). + +getr(K) -> + case get({?TAG, K}) of + undefined -> + V = erase({?MODULE, K}), %% written in old code + V == undefined orelse putr(K,V), + V; + V -> + V + end. + +eraser(K) -> + erase({?TAG, K}). + %% --------------------------------------------------------------------------- %% # encode_avps/2 %% --------------------------------------------------------------------------- @@ -154,9 +189,10 @@ decode_avps(Name, Recs) -> = lists:foldl(fun(T,A) -> decode(Name, T, A) end, {[], {newrec(Name), []}}, Recs), - {Rec, Avps, Failed ++ missing(Rec, Name)}. -%% Append 5005 errors so that a 5014 for the same AVP will take -%% precedence in a Result-Code/Failed-AVP setting. + {Rec, Avps, Failed ++ missing(Rec, Name, Failed)}. +%% Append 5005 errors so that errors are reported in the order +%% encountered. Failed-AVP should typically contain the first +%% encountered error accordg to the RFC. newrec(Name) -> '#new-'(name2rec(Name)). @@ -169,20 +205,36 @@ newrec(Name) -> %% Failed-AVP AVP SHOULD be included in the message. The Failed-AVP %% AVP MUST contain an example of the missing AVP complete with the %% Vendor-Id if applicable. The value field of the missing AVP -%% should be of correct minimum length and contain zeroes. - -missing(Rec, Name) -> - [{5005, empty_avp(F)} || F <- '#info-'(element(1, Rec), fields), - A <- [avp_arity(Name, F)], - false <- [have_arity(A, '#get-'(F, Rec))]]. +%% should be of correct minimum length and contain zeros. + +missing(Rec, Name, Failed) -> + Avps = lists:foldl(fun({_, #diameter_avp{code = C, vendor_id = V}}, A) -> + sets:add_element({C,V}, A) + end, + sets:new(), + Failed), + [{5005, A} || F <- '#info-'(element(1, Rec), fields), + not has_arity(avp_arity(Name, F), '#get-'(F, Rec)), + #diameter_avp{code = C, vendor_id = V} + = A <- [empty_avp(F)], + not sets:is_element({C,V}, Avps)]. %% Maximum arities have already been checked in building the record. -have_arity({Min, _}, L) -> - Min =< length(L); -have_arity(N, V) -> +has_arity({Min, _}, L) -> + has_prefix(Min, L); +has_arity(N, V) -> N /= 1 orelse V /= undefined. +%% Compare a non-negative integer and the length of a list without +%% computing the length. +has_prefix(0, _) -> + true; +has_prefix(_, []) -> + false; +has_prefix(N, L) -> + has_prefix(N-1, tl(L)). + %% empty_avp/1 empty_avp(Name) -> @@ -212,60 +264,244 @@ decode(Name, #diameter_avp{code = Code, vendor_id = Vid} = Avp, Acc) -> %% decode/4 +%% AVP is defined in the dictionary ... decode(Name, {AvpName, Type}, Avp, Acc) -> d(Name, Avp#diameter_avp{name = AvpName, type = Type}, Acc); +%% ... or not. decode(Name, 'AVP', Avp, Acc) -> decode_AVP(Name, Avp, Acc). -%% d/3 +%% 6733, 4.4: +%% +%% Receivers of a Grouped AVP that does not have the 'M' (mandatory) +%% bit set and one or more of the encapsulated AVPs within the group +%% has the 'M' (mandatory) bit set MAY simply be ignored if the +%% Grouped AVP itself is unrecognized. The rule applies even if the +%% encapsulated AVP with its 'M' (mandatory) bit set is further +%% encapsulated within other sub-groups, i.e., other Grouped AVPs +%% embedded within the Grouped AVP. +%% +%% The first sentence is slightly mangled, but take it to mean this: +%% +%% An unrecognized AVP of type Grouped that does not set the 'M' bit +%% MAY be ignored even if one of its encapsulated AVPs sets the 'M' +%% bit. +%% +%% The text above is a change from RFC 3588, which instead says this: +%% +%% Further, if any of the AVPs encapsulated within a Grouped AVP has +%% the 'M' (mandatory) bit set, the Grouped AVP itself MUST also +%% include the 'M' bit set. +%% +%% Both of these texts have problems. If the AVP is unknown then its +%% type is unknown since the type isn't sent over the wire, so the +%% 6733 text becomes a non-statement: don't know that the AVP not +%% setting the M-bit is of type Grouped, therefore can't know that its +%% data consists of encapsulated AVPs, therefore can't but ignore that +%% one of these might set the M-bit. It should be no worse if we know +%% the AVP to have type Grouped. +%% +%% Similarly, for the 3588 text: if we receive an AVP that doesn't set +%% the M-bit and don't know that the AVP has type Grouped then we +%% can't realize that its data contains an AVP that sets the M-bit, so +%% can't regard the AVP as erroneous on this account. Again, it should +%% be no worse if the type is known to be Grouped, but in this case +%% the RFC forces us to regard the AVP as erroneous. This is +%% inconsistent, and the 3588 text has never been enforced. +%% +%% So, if an AVP doesn't set the M-bit then we're free to ignore it, +%% regardless of the AVP's type. If we know the type to be Grouped +%% then we must ignore the M-bit on an encapsulated AVP. That means +%% packing such an encapsulated AVP into an 'AVP' field if need be, +%% not regarding the lack of a specific field as an error as is +%% otherwise the case. (The lack of an AVP-specific field being how we +%% defined the RFC's "unrecognized", which is slightly stronger than +%% "not defined".) -%% Don't try to decode the value of a Failed-AVP component since it -%% probably won't. Note that matching on 'Failed-AVP' assumes that -%% this is the RFC AVP, with code 279. Strictly, this doesn't need to -%% be the case, so we're assuming no one defines another Failed-AVP. -d('Failed-AVP' = Name, Avp, Acc) -> - decode_AVP(Name, Avp, Acc); +%% d/3 -%% Or try to decode. -d(Name, Avp, {Avps, Acc}) -> +d(Name, Avp, Acc) -> #diameter_avp{name = AvpName, - data = Data} + data = Data, + type = Type, + is_mandatory = M} = Avp, - try avp(decode, Data, AvpName) of + %% Use the process dictionary is to keep track of whether or not + %% to ignore an M-bit on an encapsulated AVP. Not ideal, but the + %% alternative requires widespread changes to be able to pass the + %% value around through the entire decode. The solution here is + %% simple in comparison, both to implement and to understand. + + Strict = relax(Type, M), + + %% Use the process dictionary again to keep track of whether we're + %% decoding within Failed-AVP and should ignore decode errors + %% altogether. + + Failed = relax(Name), %% Not AvpName or else a failed Failed-AVP + %% decode is packed into 'AVP'. + Mod = dict(Failed), %% Dictionary to decode in. + + %% On decode, a Grouped AVP is represented as a #diameter_avp{} + %% list with AVP as head and component AVPs as tail. On encode, + %% data can be a list of component AVPs. + + try Mod:avp(decode, Data, AvpName) of V -> + {Avps, T} = Acc, {H, A} = ungroup(V, Avp), - {[H | Avps], pack_avp(Name, A, Acc)} + {[H | Avps], pack_avp(Name, A, T)} catch + throw: {?TAG, {grouped, Error, ComponentAvps}} -> + g(is_failed(), Error, Name, trim(Avp), Acc, ComponentAvps); error: Reason -> - %% Failures here won't be visible since they're a "normal" - %% occurrence if the peer sends a faulty AVP that we need to - %% respond sensibly to. Log the occurence for traceability, - %% but the peer will also receive info in the resulting - %% answer-message. - diameter_lib:log({decode, failure}, - ?MODULE, - ?LINE, - {Reason, Avp, erlang:get_stacktrace()}), - {Rec, Failed} = Acc, - {[Avp|Avps], {Rec, [rc(Reason, Avp) | Failed]}} + d(is_failed(), Reason, Name, trim(Avp), Acc) + after + reset(?STRICT_KEY, Strict), + reset(?FAILED_KEY, Failed) end. +%% trim/1 +%% +%% Remove any extra bit that was added in diameter_codec to induce a +%% 5014 error. + +trim(#diameter_avp{data = <<0:1, Bin/binary>>} = Avp) -> + Avp#diameter_avp{data = Bin}; + +trim(Avps) + when is_list(Avps) -> + lists:map(fun trim/1, Avps); + +trim(Avp) -> + Avp. + +%% dict/1 +%% +%% Retrieve the dictionary for the best-effort decode of Failed-AVP, +%% as put by diameter_codec:decode/2. See that function for the +%% explanation. + +dict(true) -> + case get({diameter_codec, dictionary}) of + undefined -> + ?MODULE; + Mod -> + Mod + end; + +dict(_) -> + ?MODULE. + +%% g/5 + +%% Ignore decode errors within Failed-AVP (best-effort) ... +g(true, [_Error | Rec], Name, Avp, Acc, _ComponentAvps) -> + decode_AVP(Name, Avp#diameter_avp{value = Rec}, Acc); +g(true, _Error, Name, Avp, Acc, _ComponentAvps) -> + decode_AVP(Name, Avp, Acc); + +%% ... or not. +g(false, [Error | _Rec], _Name, Avp, Acc, ComponentAvps) -> + g(Error, Avp, Acc, ComponentAvps); +g(false, Error, _Name, Avp, Acc, ComponentAvps) -> + g(Error, Avp, Acc, ComponentAvps). + +%% g/4 + +g({RC, ErrorData}, Avp, Acc, ComponentAvps) -> + {Avps, {Rec, Errors}} = Acc, + E = Avp#diameter_avp{data = [ErrorData]}, + {[[Avp | trim(ComponentAvps)] | Avps], {Rec, [{RC, E} | Errors]}}. + +%% d/5 + +%% Ignore a decode error within Failed-AVP ... +d(true, _, Name, Avp, Acc) -> + decode_AVP(Name, Avp, Acc); + +%% ... or not. Failures here won't be visible since they're a "normal" +%% occurrence if the peer sends a faulty AVP that we need to respond +%% sensibly to. Log the occurence for traceability, but the peer will +%% also receive info in the resulting answer message. +d(false, Reason, Name, Avp, {Avps, Acc}) -> + Stack = diameter_lib:get_stacktrace(), + diameter_lib:log(decode_error, + ?MODULE, + ?LINE, + {Name, Avp#diameter_avp.name, Stack}), + {Rec, Failed} = Acc, + {[Avp|Avps], {Rec, [rc(Reason, Avp) | Failed]}}. + +%% relax/2 + +%% Set false in the process dictionary as soon as we see a Grouped AVP +%% that doesn't set the M-bit, so that is_strict() can say whether or +%% not to ignore the M-bit on an encapsulated AVP. +relax('Grouped', M) -> + case getr(?STRICT_KEY) of + undefined when not M -> + putr(?STRICT_KEY, M); + _ -> + false + end; +relax(_, _) -> + false. + +is_strict() -> + diameter_codec:getopt(strict_mbit) + andalso false /= getr(?STRICT_KEY). + +%% relax/1 +%% +%% Set true in the process dictionary as soon as we see Failed-AVP. +%% Matching on 'Failed-AVP' assumes that this is the RFC AVP. +%% Strictly, this doesn't need to be the case. + +relax('Failed-AVP') -> + putr(?FAILED_KEY, true); + +relax(_) -> + is_failed(). + +%% is_failed/0 +%% +%% Is the AVP currently being decoded nested within Failed-AVP? Note +%% that this is only true when Failed-AVP is the parent. In +%% particular, it's not true when Failed-AVP itself is being decoded +%% (unless nested). + +is_failed() -> + true == getr(?FAILED_KEY). + +%% is_failed/1 + +is_failed(Name) -> + 'Failed-AVP' == Name orelse is_failed(). + +%% reset/2 + +reset(Key, undefined) -> + eraser(Key); +reset(_, _) -> + ok. + %% decode_AVP/3 %% %% Don't know this AVP: see if it can be packed in an 'AVP' field %% undecoded. Note that the type field is 'undefined' in this case. decode_AVP(Name, Avp, {Avps, Acc}) -> - {[Avp | Avps], pack_AVP(Name, Avp, Acc)}. + {[trim(Avp) | Avps], pack_AVP(Name, Avp, Acc)}. %% rc/1 %% diameter_types will raise an error of this form to communicate %% DIAMETER_INVALID_AVP_LENGTH (5014). A module specified to a -%% @custom_types tag in a spec file can also raise an error of this -%% form. +%% @custom_types tag in a dictionary file can also raise an error of +%% this form. rc({'DIAMETER', 5014 = RC, _}, #diameter_avp{name = AvpName} = Avp) -> {RC, Avp#diameter_avp{data = empty_value(AvpName)}}; @@ -310,15 +546,25 @@ pack_avp(_, Arity, Avp, Acc) -> %% pack_AVP/3 -%% Give Failed-AVP special treatment since it'll contain any -%% unrecognized mandatory AVP's. -pack_AVP(Name, #diameter_avp{is_mandatory = true} = Avp, Acc) - when Name /= 'Failed-AVP' -> +%% Length failure was induced because of a header/payload length +%% mismatch. The AVP Length is reset to match the received data if +%% this AVP is encoded in an answer message, since the length is +%% computed. +%% +%% Data is a truncated header if command_code = undefined, otherwise +%% payload bytes. The former is padded to the length of a header if +%% the AVP reaches an outgoing encode in diameter_codec. +%% +%% RFC 6733 says that an AVP returned with 5014 can contain a minimal +%% payload for the AVP's type, but in this case we don't know the +%% type. + +pack_AVP(_, #diameter_avp{data = <<0:1, Data/binary>>} = Avp, Acc) -> {Rec, Failed} = Acc, - {Rec, [{5001, Avp} | Failed]}; + {Rec, [{5014, Avp#diameter_avp{data = Data}} | Failed]}; -pack_AVP(Name, #diameter_avp{is_mandatory = M} = Avp, Acc) -> - case avp_arity(Name, 'AVP') of +pack_AVP(Name, #diameter_avp{is_mandatory = M, name = AvpName} = Avp, Acc) -> + case pack_arity(Name, AvpName, M) of 0 -> {Rec, Failed} = Acc, {Rec, [{if M -> 5001; true -> 5008 end, Avp} | Failed]}; @@ -326,6 +572,32 @@ pack_AVP(Name, #diameter_avp{is_mandatory = M} = Avp, Acc) -> pack(Arity, 'AVP', Avp, Acc) end. +%% Give Failed-AVP special treatment since (1) it'll contain any +%% unrecognized mandatory AVP's and (2) the RFC 3588 grammar failed to +%% allow for Failed-AVP in an answer-message. + +pack_arity(Name, AvpName, M) -> + + %% Not testing just Name /= 'Failed-AVP' means we're changing the + %% packing of AVPs nested within Failed-AVP, but the point of + %% ignoring errors within Failed-AVP is to decode as much as + %% possible, and failing because a mandatory AVP couldn't be + %% packed into a dedicated field defeats that point. Note + %% is_failed/1 since is_failed/0 will return false when packing + %% 'AVP' within Failed-AVP. + + pack_arity(is_failed(Name) + orelse {Name, AvpName} == {'answer-message', 'Failed-AVP'} + orelse not M + orelse not is_strict(), + Name). + +pack_arity(true, Name) -> + avp_arity(Name, 'AVP'); + +pack_arity(false, _) -> + 0. + %% 3588: %% %% DIAMETER_AVP_UNSUPPORTED 5001 @@ -357,14 +629,17 @@ pack(undefined, 1, FieldName, Avp, Acc) -> %% AVP MUST be included and contain a copy of the first instance of %% the offending AVP that exceeded the maximum number of occurrences %% + pack(_, 1, _, Avp, {Rec, Failed}) -> {Rec, [{5009, Avp} | Failed]}; -pack(L, {_, Max}, _, Avp, {Rec, Failed}) - when length(L) == Max -> - {Rec, [{5009, Avp} | Failed]}; - -pack(L, _, FieldName, Avp, Acc) -> - p(FieldName, fun(V) -> [V|L] end, Avp, Acc). +pack(L, {_, Max}, FieldName, Avp, Acc) -> + case '*' /= Max andalso has_prefix(Max, L) of + true -> + {Rec, Failed} = Acc, + {Rec, [{5009, Avp} | Failed]}; + false -> + p(FieldName, fun(V) -> [V|L] end, Avp, Acc) + end. %% p/4 @@ -380,22 +655,55 @@ value(_, Avp) -> %% # grouped_avp/3 %% --------------------------------------------------------------------------- --spec grouped_avp(decode, avp_name(), binary()) +-spec grouped_avp(decode, avp_name(), bitstring()) -> {avp_record(), [avp()]}; (encode, avp_name(), avp_record() | avp_values()) -> binary() | no_return(). +%% Length error induced by diameter_codec:collect_avps/1: the AVP +%% length in the header was too short (insufficient for the extracted +%% header) or too long (past the end of the message). An empty payload +%% is sufficient according to the RFC text for 5014. +grouped_avp(decode, _Name, <<0:1, _/binary>>) -> + throw({?TAG, {grouped, {5014, []}, []}}); + grouped_avp(decode, Name, Data) -> - {Rec, Avps, []} = decode_avps(Name, diameter_codec:collect_avps(Data)), - {Rec, Avps}; -%% A failed match here will result in 5004. Note that this is the only -%% AVP type that doesn't just return the decoded record, also -%% returning the list of component AVP's. + grouped_decode(Name, diameter_codec:collect_avps(Data)); grouped_avp(encode, Name, Data) -> encode_avps(Name, Data). +%% grouped_decode/2 +%% +%% Note that Grouped is the only AVP type that doesn't just return a +%% decoded value, also returning the list of component diameter_avp +%% records. + +%% Length error in trailing component AVP. +grouped_decode(_Name, {Error, Acc}) -> + {5014, Avp} = Error, + throw({?TAG, {grouped, Error, [Avp | Acc]}}); + +%% 7.5. Failed-AVP AVP + +%% In the case where the offending AVP is embedded within a Grouped AVP, +%% the Failed-AVP MAY contain the grouped AVP, which in turn contains +%% the single offending AVP. The same method MAY be employed if the +%% grouped AVP itself is embedded in yet another grouped AVP and so on. +%% In this case, the Failed-AVP MAY contain the grouped AVP hierarchy up +%% to the single offending AVP. This enables the recipient to detect +%% the location of the offending AVP when embedded in a group. + +%% An error in decoding a component AVP throws the first fauly +%% component, which the catch in d/3 wraps in the Grouped AVP in +%% question. A partially decoded record is only used when ignoring +%% errors in Failed-AVP. +grouped_decode(Name, ComponentAvps) -> + {Rec, Avps, Es} = decode_avps(Name, ComponentAvps), + [] == Es orelse throw({?TAG, {grouped, [{_,_} = hd(Es) | Rec], Avps}}), + {Rec, Avps}. + %% --------------------------------------------------------------------------- %% # empty_group/1 %% --------------------------------------------------------------------------- |