%% esaml - SAML for erlang %% %% Copyright (c) 2013, Alex Wilson and the University of Queensland %% All rights reserved. %% %% Distributed subject to the terms of the 2-clause BSD license, see %% the LICENSE file in the root of the distribution. %% @doc XML canonocialisation for xmerl %% %% Functions for performing XML canonicalisation (C14n), as specified %% at http://www.w3.org/TR/xml-c14n . %% %% These routines work on xmerl data structures (see the xmerl user guide %% for details). -module(xmerl_c14n). -export([c14n/3, c14n/2, c14n/1, xml_safe_string/2, xml_safe_string/1, canon_name/1]). -include_lib("xmerl/include/xmerl.hrl"). -include_lib("public_key/include/public_key.hrl"). %% @doc Returns the canonical namespace-URI-prefix-resolved version of an XML name. %% @private -spec canon_name(Prefix :: string(), Name :: string() | atom(), Nsp :: #xmlNamespace{}) -> string(). canon_name(Ns, Name, Nsp) -> NsPartRaw = case Ns of empty -> Nsp#xmlNamespace.default; [] -> if Nsp == [] -> 'urn:oasis:names:tc:SAML:2.0:assertion'; true -> Nsp#xmlNamespace.default end; _ -> case proplists:get_value(Ns, Nsp#xmlNamespace.nodes) of undefined -> error({ns_not_found, Ns, Nsp}); Uri -> atom_to_list(Uri) end end, NsPart = if is_atom(NsPartRaw) -> atom_to_list(NsPartRaw); true -> NsPartRaw end, NamePart = if is_atom(Name) -> atom_to_list(Name); true -> Name end, lists:flatten([NsPart | NamePart]). %% @doc Returns the canonical URI name of an XML element or attribute. %% @private -spec canon_name(#xmlElement{} | #xmlAttribute{}) -> string(). canon_name(#xmlAttribute{name = Name, nsinfo = Exp, namespace = Nsp}) -> case Exp of {Ns, Nme} -> canon_name(Ns, Nme, Nsp); _ -> canon_name([], Name, Nsp) end; canon_name(#xmlElement{name = Name, nsinfo = Exp, namespace = Nsp}) -> case Exp of {Ns, Nme} -> canon_name(Ns, Nme, Nsp); _ -> canon_name([], Name, Nsp) end. %% @doc Compares two XML attributes for c14n purposes -spec attr_lte(A :: #xmlAttribute{}, B :: #xmlAttribute{}) -> true | false. attr_lte(AttrA, AttrB) -> A = canon_name(AttrA), B = canon_name(AttrB), PrefixedA = case AttrA#xmlAttribute.nsinfo of {_, _} -> true; _ -> false end, PrefixedB = case AttrB#xmlAttribute.nsinfo of {_, _} -> true; _ -> false end, if (PrefixedA) andalso (not PrefixedB) -> false; (not PrefixedA) andalso (PrefixedB) -> true; true -> A =< B end. %% @doc Cleans out all namespace definitions from an attribute list and returns it sorted. %% @private -spec clean_sort_attrs(Attrs :: [#xmlAttribute{}]) -> [#xmlAttribute{}]. clean_sort_attrs(Attrs) -> lists:sort(fun(A,B) -> attr_lte(A, B) end, lists:filter(fun(Attr) -> case Attr#xmlAttribute.nsinfo of {"xmlns", _} -> false; _ -> case Attr#xmlAttribute.name of 'xmlns' -> false; _ -> true end end end, Attrs)). %% @doc Returns the list of namespace prefixes "needed" by an element in canonical form %% @private -spec needed_ns(Elem :: #xmlElement{}, InclNs :: [string()]) -> [string()]. needed_ns(#xmlElement{nsinfo = NsInfo, attributes = Attrs}, InclNs) -> NeededNs1 = case NsInfo of {Nas, _} -> [Nas]; _ -> [] end, % show through namespaces that apply at the bottom level? this part of the spec is retarded %KidElems = [K || K <- Kids, element(1, K) =:= xmlElement], NeededNs2 = NeededNs1, %case KidElems of %[] -> [K || {K,V} <- E#xmlElement.namespace#xmlNamespace.nodes]; %_ -> NeededNs1 %end, lists:foldl(fun(Attr, Needed) -> case Attr#xmlAttribute.nsinfo of {"xmlns", Prefix} -> case lists:member(Prefix, InclNs) of true -> [Prefix | Needed]; _ -> Needed end; {Ns, _Name} -> case lists:member(Ns, Needed) of true -> Needed; _ -> [Ns | Needed] end; _ -> Needed end end, NeededNs2, Attrs). %% @doc Make xml ok to eat, in a non-quoted situation. %% @private -spec xml_safe_string(term()) -> string(). xml_safe_string(Term) -> xml_safe_string(Term, false). %% @doc Make xml ok to eat %% @private -spec xml_safe_string(String :: term(), Quotes :: boolean()) -> string(). xml_safe_string(Atom, Quotes) when is_atom(Atom) -> xml_safe_string(atom_to_list(Atom), Quotes); xml_safe_string(Bin, Quotes) when is_binary(Bin) -> xml_safe_string(binary_to_list(Bin), Quotes); xml_safe_string([], _) -> []; xml_safe_string(Str, Quotes) when is_list(Str) -> [Next | Rest] = Str, if (not Quotes andalso ([Next] =:= "\n")) -> [Next | xml_safe_string(Rest, Quotes)]; (Next < 32) -> lists:flatten(["&#x" ++ integer_to_list(Next, 16) ++ ";" | xml_safe_string(Rest, Quotes)]); (Quotes andalso ([Next] =:= "\"")) -> lists:flatten([""" | xml_safe_string(Rest, Quotes)]); ([Next] =:= "&") -> lists:flatten(["&" | xml_safe_string(Rest, Quotes)]); ([Next] =:= "<") -> lists:flatten(["<" | xml_safe_string(Rest, Quotes)]); (not Quotes andalso ([Next] =:= ">")) -> lists:flatten([">" | xml_safe_string(Rest, Quotes)]); true -> [Next | xml_safe_string(Rest, Quotes)] end; xml_safe_string(Term, Quotes) -> xml_safe_string(io_lib:format("~p", [Term]), Quotes). %% @doc Worker function for canonicalisation (c14n). It accumulates the canonical string data %% for a given XML "thing" (element/attribute/whatever) %% @private -type xml_thing() :: #xmlDocument{} | #xmlElement{} | #xmlAttribute{} | #xmlPI{} | #xmlText{} | #xmlComment{}. -spec c14n(XmlThing :: xml_thing(), KnownNs :: [{string(), string()}], ActiveNS :: [string()], Comments :: boolean(), InclNs :: [string()], Acc :: [string() | number()]) -> [string() | number()]. c14n(#xmlText{value = Text}, _KnownNS, _ActiveNS, _Comments, _InclNs, Acc) -> [xml_safe_string(Text) | Acc]; c14n(#xmlComment{value = Text}, _KnownNS, _ActiveNS, true, _InclNs, Acc) -> ["-->", xml_safe_string(Text), "\n\n\n\n\n\n", [{namespace_conformant, true}, {document, true}]), WithoutComments = "\nHello, world!\n", WithoutComments = c14n(Doc, false), WithComments = "\nHello, world!\n\n\n", WithComments = c14n(Doc, true). c14n_3_2_test() -> {Doc, _} = xmerl_scan:string("\n \n A B \n \n A\n \n B\n A B \n C\n \n", [{namespace_conformant, true}, {document, true}]), Target = "\n \n A B \n \n A\n \n B\n A B \n C\n \n", Target = c14n(Doc, true). c14n_3_3_test() -> {Doc, _} = xmerl_scan:string("]>\n\n \n \n \n \n \n \n \n \n \n \n \n \n", [{namespace_conformant, true}, {document, true}]), Target = "\n \n \n \n \n \n \n \n \n \n \n \n \n", Target = c14n(Doc, true). c14n_3_4_test() -> {Doc, _} = xmerl_scan:string("\n\n]>\n\n First line Second line\n 2\n \"0\" && value<\"10\" ?\"valid\":\"error\"]]>\n valid\n \n \n \n", [{namespace_conformant, true}, {document, true}]), Target = "\n First line\n\nSecond line\n 2\n value>\"0\" && value<\"10\" ?\"valid\":\"error\"\n "0" && value<"10" ?"valid":"error"\">valid\n \n \n \n", Target = c14n(Doc, true). default_ns_test() -> {Doc, _} = xmerl_scan:string("blah", [{namespace_conformant, true}]), Target = "blah", Target = c14n(Doc, true), {Doc2, _} = xmerl_scan:string("foo", [{namespace_conformant, true}]), Target2 = "foo", Target2 = c14n(Doc2, true). omit_default_ns_test() -> {Doc, _} = xmerl_scan:string("", [{namespace_conformant, true}]), Target = "", Target = c14n(Doc, true). c14n_inclns_test() -> {Doc, []} = xmerl_scan:string("foo", [{namespace_conformant, true}]), Target1 = "foo", Target1 = c14n(Doc, false), Target2 = "foo", Target2 = c14n(Doc, false, ["bar"]). -endif.