From 3c15ff32e89e401b4dde2b8acc9699be2614b996 Mon Sep 17 00:00:00 2001 From: Anders Svensson Date: Wed, 18 May 2011 18:29:12 +0200 Subject: Initial commit of the diameter application. The application provides an implementation of the Diameter protocol as defined in RFC 3588. --- lib/diameter/include/diameter.hrl | 130 ++++++++++ lib/diameter/include/diameter_gen.hrl | 431 ++++++++++++++++++++++++++++++++++ 2 files changed, 561 insertions(+) create mode 100644 lib/diameter/include/diameter.hrl create mode 100644 lib/diameter/include/diameter_gen.hrl (limited to 'lib/diameter/include') diff --git a/lib/diameter/include/diameter.hrl b/lib/diameter/include/diameter.hrl new file mode 100644 index 0000000000..8bd1ad1e51 --- /dev/null +++ b/lib/diameter/include/diameter.hrl @@ -0,0 +1,130 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2010-2011. 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/. +%% +%% 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. +%% +%% %CopyrightEnd% +%% + +-ifndef(diameter_hrl). +-define(diameter_hrl, true). + +%% RFC 3588, 2.4: +-define(DIAMETER_APP_ID_COMMON, 0). +-define(DIAMETER_APP_ID_ACCOUNTING, 3). +-define(DIAMETER_APP_ID_RELAY, 16#FFFFFFFF). + +%% Corresponding dictionaries: +-define(DIAMETER_DICT_COMMON, diameter_gen_base_rfc3588). +-define(DIAMETER_DICT_ACCOUNTING, diameter_gen_base_accounting). +-define(DIAMETER_DICT_RELAY, diameter_gen_relay). + +%% Events sent to processes that have subscribed with +%% diameter:subscribe/1. +%% +-record(diameter_event, + {service, %% name + info}). %% tuple() + +%% diameter_packet records are passed through the encode/decode +%% interface supplied by a dictionary module configured on a Diameter +%% application. For an incoming message the bin field contains the +%% received binary and the header, avps, msg and errors fields the +%% result of decoding. + +-record(diameter_packet, + {header, %% #diameter_header{} + avps, %% deep list() of #diameter_avp{} + msg, %% fully decoded message + bin, %% binary received/sent over the wire + errors = [],%% list() of Result-Code | {Result-Code, #diameter_avp{}} + transport_data}). + +-record(diameter_header, + {version, %% 8-bit unsigned + length, %% 24-bit unsigned + cmd_code, %% 8-bit unsigned + application_id, %% 24-bit unsigned + hop_by_hop_id, %% 32-bit unsigned + end_to_end_id, %% 32-bit unsigned + is_request, %% boolean() R flag + is_proxiable, %% boolean() P flag + is_error, %% boolean() E flag + is_retransmitted}). %% boolean() T flag + +-record(diameter_avp, + {code, %% 32-bit unsigned + vendor_id, %% 32-bit unsigned + is_mandatory = false, %% boolean() M flag + need_encryption = false, %% boolean() P flag + data, %% encoded binary() + name, %% atom() AVP name + value, %% decoded term() decoded | undefined + type, %% atom() type name, + index}). %% non_neg_integer() | undefined + +%% A diameter_caps record corresponds to capabilities configuration on +%% diameter:start_service/2. In application callbacks is identifies +%% the peer connection for which the callback is taking place, and in +%% this case each field is a 2-tuple specifying the host (ie. local) +%% and peer (ie. remote) values, host values having been configured +%% and peer values having been received at capabilities exchange. + +-record(diameter_caps, + {origin_host, %% 'DiameterIdentity'() + origin_realm, %% 'DiameterIdentity'() + host_ip_address = [], %% ['Address'()] + vendor_id, %% 'Unsigned32'() + product_name, %% 'OctetString'() + origin_state_id = [], %% ['Unsigned32'()] + supported_vendor_id = [], %% ['Unsigned32'()] + auth_application_id = [], %% ['Unsigned32'()] + inband_security_id = [], %% ['Unsigned32'()] + acct_application_id = [], %% ['Unsigned32'()] + vendor_specific_application_id = [], %% ['Grouped'()] + firmware_revision = [], %% ['Unsigned32()] + avp = []}). + +%% AVP's of type DiameterURI are encoded as a diameter_uri record. +%% Note that AVP's of type IPFilterRule and QoSFilterRule are currently +%% encoded simply as OctetString's. + +-record(diameter_uri, + {type, %% aaa | aaas + fqdn, %% string() + port = 3868, %% non_neg_integer(), + transport = sctp, %% | tcp, + protocol = diameter}). %% | radius | 'tacacs+' + +%% The diameter service and diameter_apps records are only passed +%% through the transport interface when starting a transport process, +%% although typically a transport implementation will (and probably +%% should) only be interested diameter_service.host_ip_address. + +-record(diameter_service, + {pid, + capabilities, %% #diameter_caps{} + applications = []}). %% [#diameter_app{}] + +-record(diameter_app, + {alias, %% option 'alias' + dictionary, %% option 'dictionary', module() + module, %% [Mod | Args] callback module() and extra args + init_state, %% option 'state', initial callback state + id, %% 32-bit unsigned application identifier = Dict:id() + mutable = false, %% boolean(), do traffic callbacks modify state? + answer_errors = report}). %% | callback | discard + %% how to handle containing errors? + +-endif. %% -ifdef(diameter_hrl). diff --git a/lib/diameter/include/diameter_gen.hrl b/lib/diameter/include/diameter_gen.hrl new file mode 100644 index 0000000000..4c91954a21 --- /dev/null +++ b/lib/diameter/include/diameter_gen.hrl @@ -0,0 +1,431 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2010-2011. 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/. +%% +%% 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. +%% +%% %CopyrightEnd% +%% + +%% +%% This file contains code that's included by encode/decode modules +%% generated by diameter_codegen.erl. This code does most of the work, the +%% generated code being kept simple. +%% + +-define(THROW(T), throw({?MODULE, T})). + +%%% --------------------------------------------------------------------------- +%%% # encode_avps/3 +%%% +%%% Returns: binary() +%%% --------------------------------------------------------------------------- + +encode_avps(Name, Vals) + when is_list(Vals) -> + encode_avps(Name, '#set-'(Vals, newrec(Name))); + +encode_avps(Name, Rec) -> + try + list_to_binary(encode(Name, Rec)) + catch + throw: {?MODULE, Reason} -> + diameter_dbg:log({encode, error}, + ?MODULE, + ?LINE, + {Reason, Name, Rec}), + erlang:error(list_to_tuple(Reason ++ [Name, Rec, ?MODULE])); + error: Reason -> + Stack = erlang:get_stacktrace(), + diameter_dbg:log({encode, failure}, + ?MODULE, + ?LINE, + {Reason, Name, Rec, Stack}), + erlang:error({encode_failure, Reason, Name, Rec, ?MODULE, Stack}) + end. + +%% encode/2 + +encode(Name, Rec) -> + lists:flatmap(fun(A) -> encode(Name, A, '#get-'(A, Rec)) end, + '#info-'(element(1, Rec), fields)). + +%% encode/3 + +encode(Name, AvpName, Values) -> + e(Name, AvpName, avp_arity(Name, AvpName), Values). + +%% e/4 + +e(_, AvpName, 1, undefined) -> + ?THROW([mandatory_avp_missing, AvpName]); + +e(Name, AvpName, 1, Value) -> + e(Name, AvpName, [Value]); + +e(_, _, {0,_}, []) -> + []; + +e(_, AvpName, _, T) + when not is_list(T) -> + ?THROW([repeated_avp_as_non_list, AvpName, T]); + +e(_, AvpName, {Min, _}, L) + when length(L) < Min -> + ?THROW([repeated_avp_insufficient_arity, AvpName, Min, L]); + +e(_, AvpName, {_, Max}, L) + when Max < length(L) -> + ?THROW([repeated_avp_excessive_arity, AvpName, Max, L]); + +e(Name, AvpName, _, Values) -> + e(Name, AvpName, Values). + +%% e/3 + +e(Name, 'AVP', Values) -> + [pack_AVP(Name, A) || A <- Values]; + +e(_, AvpName, Values) -> + e(AvpName, Values). + +%% e/2 + +e(AvpName, Values) -> + H = avp_header(AvpName), + [diameter_codec:pack_avp(H, avp(encode, V, AvpName)) || V <- Values]. + +%% pack_AVP/2 + +%% No value: assume AVP data is already encoded. The normal case will +%% be when this is passed back from #diameter_packet.errors as a +%% consequence of a failed decode. Any AVP can be encoded this way +%% however, which side-steps any arity checks for known AVP's and +%% could potentially encode something unfortunate. +pack_AVP(_, #diameter_avp{value = undefined} = A) -> + diameter_codec:pack_avp(A); + +%% Missing name for value encode. +pack_AVP(_, #diameter_avp{name = N, value = V}) + when N == undefined; + N == 'AVP' -> + ?THROW([value_with_nameless_avp, N, V]); + +%% Or not. Ensure that 'AVP' is the appropriate field. Note that if we +%% don't know this AVP at all then the encode will fail. +pack_AVP(Name, #diameter_avp{name = AvpName, + value = Data}) -> + 0 == avp_arity(Name, AvpName) + orelse ?THROW([known_avp_as_AVP, Name, AvpName, Data]), + e(AvpName, [Data]). + +%%% --------------------------------------------------------------------------- +%%% # decode_avps/2 +%%% +%%% Returns: {Rec, Avps, Failed} +%%% +%%% Rec = decoded message record +%%% Avps = list of Avp +%%% Failed = list of {ResultCode, #diameter_avp{}} +%%% +%%% Avp = #diameter_avp{} if type is not Grouped +%%% | list of Avp where first element has type Grouped +%%% and following elements are its component +%%% AVP's. +%%% --------------------------------------------------------------------------- + +decode_avps(Name, Recs) -> + d_rc(Name, lists:foldl(fun(T,A) -> decode(Name, T, A) end, + {[], {newrec(Name), []}}, + Recs)). + +newrec(Name) -> + '#new-'(name2rec(Name)). + +%% No errors so far: keep looking. +d_rc(Name, {Avps, {Rec, [] = Failed}}) -> + try + true = have_required_avps(Rec, Name), + {Rec, Avps, Failed} + catch + throw: {?MODULE, {AvpName, Reason}} -> + diameter_dbg:log({decode, error}, + ?MODULE, + ?LINE, + {AvpName, Reason, Rec}), + {Rec, Avps, [{5005, empty_avp(AvpName)}]} + end; +%% 3588: +%% +%% DIAMETER_MISSING_AVP 5005 +%% The request did not contain an AVP that is required by the Command +%% Code definition. If this value is sent in the Result-Code AVP, a +%% 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. + +%% Or not. Only need to report the first error so look no further. +d_rc(_, {Avps, {Rec, Failed}}) -> + {Rec, Avps, lists:reverse(Failed)}. + +%% empty_avp/1 + +empty_avp(Name) -> + {Code, Flags, VId} = avp_header(Name), + {Name, Type} = avp_name(Code, VId), + #diameter_avp{name = Name, + code = Code, + vendor_id = VId, + is_mandatory = 0 /= (Flags band 2#01000000), + need_encryption = 0 /= (Flags band 2#00100000), + data = empty_value(Name), + type = Type}. + +%% have_required_avps/2 + +have_required_avps(Rec, Name) -> + lists:foreach(fun(F) -> hra(Name, F, Rec) end, + '#info-'(element(1, Rec), fields)), + true. + +hra(Name, AvpName, Rec) -> + Arity = avp_arity(Name, AvpName), + hra(Arity, '#get-'(AvpName, Rec)) + orelse ?THROW({AvpName, {insufficient_arity, Arity}}). + +%% Maximum arities have already been checked in building the record. + +hra({Min, _}, L) -> + Min =< length(L); +hra(N, V) -> + N /= 1 orelse V /= undefined. + +%% 3588, ch 7: +%% +%% The Result-Code AVP describes the error that the Diameter node +%% encountered in its processing. In case there are multiple errors, +%% the Diameter node MUST report only the first error it encountered +%% (detected possibly in some implementation dependent order). The +%% specific errors that can be described by this AVP are described in +%% the following section. + +%% decode/3 + +decode(Name, #diameter_avp{code = Code, vendor_id = Vid} = Avp, Acc) -> + decode(Name, avp_name(Code, Vid), Avp, Acc). + +%% decode/4 + +%% Don't know this AVP: see if it can be packed in an 'AVP' field +%% undecoded, unless it's mandatory. Need to give Failed-AVP special +%% treatment since it'll contain any unrecognized mandatory AVP's. +decode(Name, 'AVP', #diameter_avp{is_mandatory = M} = Avp, {Avps, Acc}) -> + {[Avp | Avps], if M, Name /= 'Failed-AVP' -> + unknown(Avp, Acc); + true -> + pack_AVP(Name, Avp, Acc) + end}; +%% Note that the type field is 'undefined' in this case. + +%% Or try to decode. +decode(Name, {AvpName, Type}, Avp, Acc) -> + d(Name, Avp#diameter_avp{name = AvpName, type = Type}, Acc). + +%% d/3 + +d(Name, Avp, {Avps, Acc}) -> + #diameter_avp{name = AvpName, + data = Data} + = Avp, + + try avp(decode, Data, AvpName) of + V -> + {H, A} = ungroup(V, Avp), + {[H | Avps], pack_avp(Name, A, Acc)} + catch + 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_dbg:log({decode, failure}, + ?MODULE, + ?LINE, + {Reason, Avp, erlang:get_stacktrace()}), + {Rec, Failed} = Acc, + {[Avp|Avps], {Rec, [{rc(Reason), Avp} | Failed]}} + end. + +%% 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. +rc({'DIAMETER', RC, _}) -> + RC; + +%% 3588: +%% +%% DIAMETER_INVALID_AVP_VALUE 5004 +%% The request contained an AVP with an invalid value in its data +%% portion. A Diameter message indicating this error MUST include +%% the offending AVPs within a Failed-AVP AVP. +rc(_) -> + 5004. + +%% ungroup/2 +%% +%% Returns: {Avp, Dec} +%% +%% Avp = #diameter_avp{} if type is not Grouped +%% | list of Avp where first element has type Grouped +%% and following elements are its component +%% AVP's. +%% = as for decode_avps/2 +%% +%% Dec = #diameter_avp{}, either Avp or its head in the list case. + +%% The decoded value in the Grouped case is as returned by grouped_avp/3: +%% a record and a list of component AVP's. +ungroup(V, #diameter_avp{type = 'Grouped'} = Avp) -> + {Rec, As} = V, + A = Avp#diameter_avp{value = Rec}, + {[A|As], A}; + +%% Otherwise it's just a plain value. +ungroup(V, #diameter_avp{} = Avp) -> + A = Avp#diameter_avp{value = V}, + {A, A}. + +%% pack_avp/3 + +pack_avp(Name, #diameter_avp{name = AvpName} = Avp, Acc) -> + pack_avp(Name, avp_arity(Name, AvpName), Avp, Acc). + +%% pack_avp/4 + +pack_avp(Name, 0, Avp, Acc) -> + pack_AVP(Name, Avp, Acc); + +pack_avp(_, Arity, Avp, Acc) -> + pack(Arity, Avp#diameter_avp.name, Avp, Acc). + +%% pack_AVP/3 + +pack_AVP(Name, Avp, Acc) -> + case avp_arity(Name, 'AVP') of + 0 -> + unknown(Avp, Acc); + Arity -> + pack(Arity, 'AVP', Avp, Acc) + end. + +%% 3588: +%% +%% DIAMETER_AVP_UNSUPPORTED 5001 +%% The peer received a message that contained an AVP that is not +%% recognized or supported and was marked with the Mandatory bit. A +%% Diameter message with this error MUST contain one or more Failed- +%% AVP AVP containing the AVPs that caused the failure. +%% +%% DIAMETER_AVP_NOT_ALLOWED 5008 +%% A message was received with an AVP that MUST NOT be present. The +%% Failed-AVP AVP MUST be included and contain a copy of the +%% offending AVP. +%% +unknown(#diameter_avp{is_mandatory = B} = Avp, {Rec, Failed}) -> + {Rec, [{if B -> 5001; true -> 5008 end, Avp} | Failed]}. + +%% pack/4 + +pack(Arity, FieldName, Avp, {Rec, _} = Acc) -> + pack('#get-'(FieldName, Rec), Arity, FieldName, Avp, Acc). + +%% pack/5 + +pack(undefined, 1, FieldName, Avp, Acc) -> + p(FieldName, fun(V) -> V end, Avp, Acc); + +%% 3588: +%% +%% DIAMETER_AVP_OCCURS_TOO_MANY_TIMES 5009 +%% A message was received that included an AVP that appeared more +%% often than permitted in the message definition. The Failed-AVP +%% 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). + +%% p/4 + +p(F, Fun, Avp, {Rec, Failed}) -> + {'#set-'({F, Fun(value(F, Avp))}, Rec), Failed}. + +value('AVP', Avp) -> + Avp; +value(_, Avp) -> + Avp#diameter_avp.value. + +%%% --------------------------------------------------------------------------- +%%% # grouped_avp/3 +%%% --------------------------------------------------------------------------- + +grouped_avp(decode, Name, Data) -> + {Rec, Avps, []} = decode_avps(Name, diameter_codec:collect_avps(Data)), + {Rec, Avps}; +%% Note that a failed match here will result in 5004. Note that this is +%% the only AVP type that doesn't just return the decoded value, also +%% returning the list of component #diameter_avp{}'s. + +grouped_avp(encode, Name, Data) -> + encode_avps(Name, Data). + +%%% --------------------------------------------------------------------------- +%%% # empty_group/1 +%%% --------------------------------------------------------------------------- + +empty_group(Name) -> + list_to_binary(empty_body(Name)). + +empty_body(Name) -> + [z(F, avp_arity(Name, F)) || F <- '#info-'(name2rec(Name), fields)]. + +z(Name, 1) -> + z(Name); +z(_, {0,_}) -> + []; +z(Name, {Min, _}) -> + lists:duplicate(Min, z(Name)). + +z('AVP') -> + <<0:64/integer>>; %% minimal header +z(Name) -> + Bin = diameter_codec:pack_avp(avp_header(Name), empty_value(Name)), + << <<0>> || <<_>> <= Bin >>. + +%%% --------------------------------------------------------------------------- +%%% # empty/1 +%%% --------------------------------------------------------------------------- + +empty(AvpName) -> + avp(encode, zero, AvpName). -- cgit v1.2.3