1%%--------------------------------------------------------------------
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2012-2018. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20%%----------------------------------------------------------------------
21%% A netconf server used for testing of netconfc
22-module(ns).
23
24%-compile(export_all).
25-include_lib("common_test/src/ct_netconfc.hrl").
26
27
28%%%-----------------------------------------------------------------
29%%% API
30-export([start/1,
31	 stop/1,
32	 hello/1,
33	 hello/2,
34	 expect/1,
35	 expect/2,
36	 expect_reply/2,
37	 expect_reply/3,
38	 expect_do/2,
39	 expect_do/3,
40	 expect_do_reply/3,
41	 expect_do_reply/4,
42	 hupp/1,
43	 hupp/2]).
44
45%%%-----------------------------------------------------------------
46%%% ssh_channel callbacks
47-export([init/1,
48	 terminate/2,
49	 handle_ssh_msg/2,
50	 handle_msg/2]).
51
52%%%-----------------------------------------------------------------
53%% Server specifications
54-define(SERVER_DATA_NAMESPACE, "ClientTest").
55-define(CAPABILITIES,?CAPABILITIES_VSN("1.0")).
56-define(CAPABILITIES_VSN(Vsn),
57	[
58	 ?NETCONF_BASE_CAP ++ Vsn,
59	 "urn:ietf:params:netconf:capability:writable-running:1.0",
60	 "urn:ietf:params:netconf:capability:candidate:1.0",
61	 "urn:ietf:params:netconf:capability:confirmed-commit:1.0",
62	 "urn:ietf:params:netconf:capability:rollback-on-error:1.0",
63	 "urn:ietf:params:netconf:capability:startup:1.0",
64	 "urn:ietf:params:netconf:capability:url:1.0",
65	 "urn:ietf:params:netconf:capability:xpath:1.0",
66	 "urn:ietf:params:netconf:capability:notification:1.0",
67	 "urn:ietf:params:netconf:capability:interleave:1.0",
68	 ?ACTION_NAMESPACE,
69	 ?SERVER_DATA_NAMESPACE
70	]).
71-define(SSH_PORT, 2060).
72-define(ssh_config(Dir),[{port, ?SSH_PORT},
73			 {interface, {127,0,0,1}},
74			 {system_dir, Dir},
75			 {user_dir, Dir},
76			 {user_passwords, [{"xxx","xxx"}]},
77			 {password, "global-xxx"}]).
78
79%% Some help for debugging
80%-define(dbg(F,A),io:format(F,A)).
81-define(dbg(F,A),ok).
82-define(dbg_event(Event,Expect),
83	?dbg("Event: ~p~nExpected: ~p~n",[Event,Expect])).
84
85%% State
86-record(session, {cb,
87		  connection,
88		  buffer = <<>>,
89		  session_id}).
90
91
92%%%-----------------------------------------------------------------
93%%% API
94
95%% Start the netconf server and use the given directory as system_dir
96%% and user_dir
97start(Dir) ->
98    spawn(fun() -> init_server(Dir) end).
99
100%% Stop the netconf server
101stop(Pid) ->
102    Ref = erlang:monitor(process,Pid),
103    Pid ! stop,
104    receive {'DOWN',Ref,process,Pid,_} -> ok end.
105
106%% Set the session id for the hello message.
107%% If this is not called prior to starting the session, no hello
108%% message will be sent.
109%% 'Stuff' indicates some special handling to e.g. provoke error cases
110hello(SessionId) ->
111    hello(SessionId,undefined).
112hello(SessionId,Stuff) ->
113    insert(hello,{SessionId,Stuff}).
114
115%% Tell server to expect the given message without doing any further
116%% actions. To be called directly before sending a request.
117expect(Expect) ->
118    expect_do_reply(Expect,undefined,undefined).
119expect(SessionId,Expect) ->
120    expect_do_reply(SessionId,Expect,undefined,undefined).
121
122%% Tell server to expect the given message and reply with the give
123%% reply. To be called directly before sending a request.
124expect_reply(Expect,Reply) ->
125    expect_do_reply(Expect,undefined,Reply).
126expect_reply(SessionId,Expect,Reply) ->
127    expect_do_reply(SessionId,Expect,undefined,Reply).
128
129%% Tell server to expect the given message and perform an action. To
130%% be called directly before sending a request.
131expect_do(Expect,Do) ->
132    expect_do_reply(Expect,Do,undefined).
133expect_do(SessionId,Expect,Do) ->
134    expect_do_reply(SessionId,Expect,Do,undefined).
135
136%% Tell server to expect the given message, perform an action and
137%% reply with the given reply. To be called directly before sending a
138%% request.
139expect_do_reply(Expect,Do,Reply) ->
140    add_expect(1,{Expect,Do,Reply}).
141expect_do_reply(SessionId,Expect,Do,Reply) ->
142    add_expect(SessionId,{Expect,Do,Reply}).
143
144%% Hupp the server - i.e. tell it to do something -
145%% e.g. hupp(send_event) will cause send_event(State) to be called on
146%% the session channel process.
147hupp({send_events,N}) ->
148    hupp(send,[make_msg({event,N})]);
149hupp(kill) ->
150    hupp(1,fun hupp_kill/1,[]).
151
152hupp(send,Data) ->
153    hupp(1,fun hupp_send/2,[Data]).
154
155hupp(SessionId,Fun,Args) when is_function(Fun) ->
156    [{_,Pid}] = lookup({channel_process,SessionId}),
157    Pid ! {hupp,Fun,Args}.
158
159%%%-----------------------------------------------------------------
160%%% Main loop of the netconf server
161init_server(Dir) ->
162    register(main_ns_proc,self()),
163    ets:new(ns_tab,[set,named_table,public]),
164    Config = ?ssh_config(Dir),
165    {_,Host} = lists:keyfind(interface, 1, Config),
166    {_,Port} = lists:keyfind(port, 1, Config),
167    Opts = lists:filter(fun({Key,_}) ->
168				lists:member(Key,[system_dir,
169						  password,
170						  user_passwords,
171						  pwdfun])
172			end,
173			Config),
174    {ok, Daemon} =
175	ssh:daemon(Host, Port,
176		   [{subsystems,[{"netconf",{?MODULE,[]}}]}
177		    |Opts]),
178    loop(Daemon).
179
180loop(Daemon) ->
181    receive
182	stop ->
183	    ssh:stop_daemon(Daemon),
184	    ok;
185	{table_trans,Fun,Args,From} ->
186	    %% Simple transaction mechanism for ets table
187	    R = apply(Fun,Args),
188	    From ! {table_trans_done,R},
189	    loop(Daemon)
190    end.
191
192%%----------------------------------------------------------------------
193%% Behaviour callback functions (ssh_channel)
194%%----------------------------------------------------------------------
195init([]) ->
196    {ok, undefined}.
197
198terminate(_Reason, _State) ->
199    ok.
200
201handle_ssh_msg({ssh_cm,CM,{data, Ch, _Type = 0, Data}}, State) ->
202    %% io:format("~p~n",[{self(),Data,CM,Ch,State}]),
203    data_for_channel(CM, Ch, Data, State);
204handle_ssh_msg({ssh_cm,CM,{closed, Ch}}, State)  ->
205    %% erlang:display({self(),closed,CM,Ch,State}),
206    stop_channel(CM, Ch, State);
207handle_ssh_msg({ssh_cm,CM,{eof, Ch}}, State) ->
208    %% erlang:display({self(),eof,CM,Ch,State}),
209    data_for_channel(CM,Ch, <<>>, State).
210
211
212handle_msg({'EXIT', _Pid, _Reason}, State) ->
213    {ok, State};
214handle_msg({ssh_channel_up,Ch,CM},undefined) ->
215    %% erlang:display({self(),up,CM,Ch}),
216    ConnRef = {CM,Ch},
217    SessionId = maybe_hello(ConnRef),
218    insert({channel_process,SessionId},self()), % used to hupp the server
219    {ok, #session{connection = ConnRef,
220		  session_id = SessionId}};
221handle_msg({hupp,Fun,Args},State) ->
222    {ok,apply(Fun,Args ++ [State])}.
223
224data_for_channel(CM, Ch, Data, State) ->
225    try data(Data, State) of
226	{ok, NewState} ->
227	    case erase(stop) of
228		true ->
229		    stop_channel(CM, Ch, NewState);
230		_ ->
231		    {ok, NewState}
232	    end
233    catch
234	Class:Reason:Stacktrace ->
235	    error_logger:error_report([{?MODULE, data_for_channel},
236				       {request, Data},
237				       {buffer, State#session.buffer},
238				       {reason, {Class, Reason}},
239				       {stacktrace, Stacktrace}]),
240	    stop_channel(CM, Ch, State)
241    end.
242
243data(Data, State = #session{connection = ConnRef,
244			    buffer = Buffer,
245			    session_id = SessionId}) ->
246    AllData = <<Buffer/binary,Data/binary>>,
247    case find_endtag(AllData) of
248	{ok,Msgs,Rest} ->
249	    [check_expected(SessionId,ConnRef,Msg) || Msg <- Msgs],
250	    {ok,State#session{buffer=Rest}};
251	need_more ->
252	    {ok,State#session{buffer=AllData}}
253    end.
254
255stop_channel(CM, Ch, State) ->
256    ssh_connection:close(CM,Ch),
257    {stop, Ch, State}.
258
259
260%%%-----------------------------------------------------------------
261%%% Functions to trigg via hupp/1:
262
263%% Send data spontaneously - e.g. an event
264hupp_send(Data,State = #session{connection = ConnRef}) ->
265    send(ConnRef,Data),
266    State.
267hupp_kill(State = #session{connection = ConnRef}) ->
268    kill(ConnRef),
269    State.
270
271%%%-----------------------------------------------------------------
272%%% Internal functions
273
274
275%%% Send ssh data to the client
276send({CM,Ch},Data) ->
277    ssh_connection:send(CM, Ch, Data).
278
279%%% Split into many small parts and send to client
280send_frag({CM,Ch},Data) ->
281    Sz = rand:uniform(1000),
282    case Data of
283	<<Chunk:Sz/binary,Rest/binary>> ->
284	    ssh_connection:send(CM, Ch, Chunk),
285	    send_frag({CM,Ch},Rest);
286	Chunk ->
287	    ssh_connection:send(CM, Ch, Chunk)
288    end.
289
290
291%%% Kill ssh connection
292kill({CM,Ch}) ->
293    ssh_connection:close(CM,Ch).
294
295add_expect(SessionId,Add) ->
296    table_trans(fun do_add_expect/2,[SessionId,Add]).
297
298table_trans(Fun,Args) ->
299    S = self(),
300    case whereis(main_ns_proc) of
301	S ->
302	    apply(Fun,Args);
303	Pid ->
304	    Ref = erlang:monitor(process,Pid),
305	    Pid ! {table_trans,Fun,Args,self()},
306	    receive
307		{table_trans_done,Result} ->
308		    erlang:demonitor(Ref,[flush]),
309		    Result;
310		{'DOWN',Ref,process,Pid,Reason} ->
311		    exit({main_ns_proc_died,Reason})
312	    after 20000 ->
313		    exit(table_trans_timeout)
314	    end
315    end.
316
317do_add_expect(SessionId,Add) ->
318    case lookup({expect,SessionId}) of
319	[] ->
320	    insert({expect,SessionId},[Add]);
321	[{_,First}] ->
322	    insert({expect,SessionId},First ++ [Add])
323    end,
324    ok.
325
326do_get_expect(SessionId) ->
327    case lookup({expect,SessionId}) of
328	[{_,[{Expect,Do,Reply}|Rest]}] ->
329	    insert({expect,SessionId},Rest),
330	    {Expect,Do,Reply};
331	_ ->
332	    error
333    end.
334
335insert(Key,Value) ->
336    ets:insert(ns_tab,{Key,Value}).
337lookup(Key) ->
338    ets:lookup(ns_tab,Key).
339
340maybe_hello(ConnRef) ->
341    case lookup(hello) of
342	[{hello,{SessionId,Stuff}}] ->
343	    %% erlang:display({SessionId,Stuff}),
344	    ets:delete(ns_tab,hello),
345	    insert({session,SessionId},ConnRef),
346	    reply(ConnRef,{hello,SessionId,Stuff}),
347	    SessionId;
348	[] ->
349	    undefined
350    end.
351
352find_endtag(Data) ->
353    case binary:split(Data,[?END_TAG],[global]) of
354	[Data] ->
355	    need_more;
356	Msgs ->
357	    {ok,lists:sublist(Msgs,length(Msgs)-1),lists:last(Msgs)}
358    end.
359
360check_expected(SessionId,ConnRef,Msg) ->
361    %% io:format("~p~n",[{check_expected,SessionId,Msg}]),
362    case table_trans(fun do_get_expect/1,[SessionId]) of
363	{Expect,Do,Reply} ->
364	    %% erlang:display({got,io_lib:format("~s",[Msg])}),
365	    %% erlang:display({expected,Expect}),
366	    match(Msg,Expect),
367	    do(ConnRef, Do),
368	    reply(ConnRef,Reply);
369	error ->
370	    ct:sleep(1000),
371	    exit({error,{got_unexpected,SessionId,Msg,ets:tab2list(ns_tab)}})
372    end.
373
374match(Msg,Expect) ->
375    ?dbg("Match: ~p~n",[Msg]),
376    {ok,ok,<<>>} = xmerl_sax_parser:stream(Msg,[{event_fun,fun event/3},
377						{event_state,Expect}]).
378
379event(Event,_Loc,Expect) ->
380    ?dbg_event(Event,Expect),
381    event(Event,Expect).
382
383event(startDocument,Expect) -> match(Expect);
384event({startElement,_,Name,_,Attrs},[{se,Name}|Match]) ->
385    msg_id(Name,Attrs),
386    Match;
387event({startElement,_,Name,_,Attrs},[ignore,{se,Name}|Match]) ->
388    msg_id(Name,Attrs),
389    Match;
390event({startElement,_,Name,_,Attrs},[{se,Name,As}|Match]) ->
391    msg_id(Name,Attrs),
392    match_attrs(Name,As,Attrs),
393    Match;
394event({startElement,_,Name,_,Attrs},[ignore,{se,Name,As}|Match]) ->
395    msg_id(Name,Attrs),
396    match_attrs(Name,As,Attrs),
397    Match;
398event({startPrefixMapping,_,Ns},[{ns,Ns}|Match]) -> Match;
399event({startPrefixMapping,_,Ns},[ignore,{ns,Ns}|Match]) -> Match;
400event({endPrefixMapping,_},Match) -> Match;
401event({characters,Chs},[{characters,Chs}|Match]) -> Match;
402event({endElement,_,Name,_},[{ee,Name}|Match]) -> Match;
403event({endElement,_,Name,_},[ignore,{ee,Name}|Match]) -> Match;
404event(endDocument,Match) when Match==[]; Match==[ignore] -> ok;
405event(_,[ignore|_]=Match) -> Match;
406event(Event,Match) -> throw({nomatch,{Event,Match}}).
407
408msg_id("rpc",Attrs) ->
409    case lists:keyfind("message-id",3,Attrs) of
410	{_,_,_,Str} -> put(msg_id,Str);
411	false -> erase(msg_id)
412    end;
413msg_id(_,_) ->
414    ok.
415
416match_attrs(Name,[{Key,Value}|As],Attrs) ->
417    case lists:keyfind(atom_to_list(Key),3,Attrs) of
418	{_,_,_,Value} -> match_attrs(Name,As,Attrs);
419	false -> throw({missing_attr,Key,Name,Attrs});
420	_ -> throw({faulty_attr_value,Key,Name,Attrs})
421    end;
422match_attrs(_,[],_) ->
423    ok.
424
425do(ConnRef, close) ->
426    ets:match_delete(ns_tab,{{session,'_'},ConnRef}),
427    put(stop,true);
428do(_ConnRef, {kill,SessionId}) ->
429    case lookup({session,SessionId}) of
430	[{_,Owner}] ->
431	    ets:delete(ns_tab,{session,SessionId}),
432	    kill(Owner);
433	_ ->
434	    exit({no_session_to_kill,SessionId})
435    end;
436do(_, undefined) ->
437    ok.
438
439reply(_,undefined) ->
440    ?dbg("no reply~n",[]),
441    ok;
442reply(ConnRef,{fragmented,Reply}) ->
443    ?dbg("Reply fragmented: ~p~n",[Reply]),
444    send_frag(ConnRef,make_msg(Reply));
445reply(ConnRef,Reply) ->
446    ?dbg("Reply: ~p~n",[Reply]),
447    send(ConnRef, make_msg(Reply)).
448
449from_simple(Simple) ->
450    unicode_c2b(xmerl:export_simple_element(Simple,xmerl_xml)).
451
452xml(Content) when is_binary(Content) ->
453    xml([Content]);
454xml(Content) when is_list(Content) ->
455    Msgs = [<<Msg/binary,"\n",?END_TAG/binary>> || Msg <- Content],
456    MsgsBin = list_to_binary(Msgs),
457    <<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", MsgsBin/binary>>.
458
459rpc_reply(Content) when is_binary(Content) ->
460    MsgId = case erase(msg_id) of
461		undefined -> <<>>;
462		Id -> unicode_c2b([" message-id=\"",Id,"\""])
463	    end,
464    <<"<rpc-reply xmlns=\"",?NETCONF_NAMESPACE,"\"",MsgId/binary,">\n",
465      Content/binary,"\n</rpc-reply>">>;
466rpc_reply(Content) ->
467    rpc_reply(unicode_c2b(Content)).
468
469session_id(no_session_id) ->
470    <<>>;
471session_id(SessionId0) ->
472    SessionId = unicode_c2b(integer_to_list(SessionId0)),
473    <<"<session-id>",SessionId/binary,"</session-id>\n">>.
474
475capabilities(undefined) ->
476    CapsXml = unicode_c2b([["<capability>",C,"</capability>\n"]
477			      || C <- ?CAPABILITIES]),
478    <<"<capabilities>\n",CapsXml/binary,"</capabilities>\n">>;
479capabilities({base,Vsn}) ->
480    CapsXml = unicode_c2b([["<capability>",C,"</capability>\n"]
481			      || C <- ?CAPABILITIES_VSN(Vsn)]),
482    <<"<capabilities>\n",CapsXml/binary,"</capabilities>\n">>;
483capabilities(no_base) ->
484    [_|Caps] = ?CAPABILITIES,
485    CapsXml = unicode_c2b([["<capability>",C,"</capability>\n"] || C <- Caps]),
486    <<"<capabilities>\n",CapsXml/binary,"</capabilities>\n">>;
487capabilities(no_caps) ->
488    <<>>.
489
490%%%-----------------------------------------------------------------
491%%% Match received netconf message from the client.  Add a new clause
492%%% for each new message to recognize. The clause argument shall match
493%%% the Expect argument in expect/1, expect_reply/2 or
494%%% expect_do_reply/3.
495%%%
496%%% match(term()) -> [Match].
497%%% Match = ignore | {se,Name} | {se,Name,Attrs} | {ee,Name} |
498%%%         {ns,Namespace} | {characters,Chs}
499%%% Name = string()
500%%% Chs = string()
501%%% Attrs = [{atom(),string()}]
502%%% Namespace = string()
503%%%
504%%% 'se' means start element, 'ee' means end element - i.e. to match
505%%% an XML element you need one 'se' entry and one 'ee' entry with the
506%%% same name in the match list. 'characters' can be used for matching
507%%% character data (cdata) inside an element.
508match(hello) ->
509    [ignore,{se,"hello"},ignore,{ee,"hello"},ignore];
510match('close-session') ->
511    [ignore,{se,"rpc"},{se,"close-session"},
512     {ee,"close-session"},{ee,"rpc"},ignore];
513match('edit-config') ->
514    [ignore,{se,"rpc"},{se,"edit-config"},{se,"target"},ignore,{ee,"target"},
515     {se,"config"},ignore,{ee,"config"},{ee,"edit-config"},{ee,"rpc"},ignore];
516match({'edit-config',{'default-operation',DO}}) ->
517    [ignore,{se,"rpc"},{se,"edit-config"},{se,"target"},ignore,{ee,"target"},
518     {se,"default-operation"},{characters,DO},{ee,"default-operation"},
519     {se,"config"},ignore,{ee,"config"},{ee,"edit-config"},{ee,"rpc"},ignore];
520match('get') ->
521    match({get,subtree});
522match({'get',FilterType}) ->
523    [ignore,{se,"rpc"},{se,"get"},{se,"filter",[{type,atom_to_list(FilterType)}]},
524     ignore,{ee,"filter"},{ee,"get"},{ee,"rpc"},ignore];
525match('get-config') ->
526    match({'get-config',subtree});
527match({'get-config',FilterType}) ->
528    [ignore,{se,"rpc"},{se,"get-config"},{se,"source"},ignore,{ee,"source"},
529     {se,"filter",[{type,atom_to_list(FilterType)}]},ignore,{ee,"filter"},
530     {ee,"get-config"},{ee,"rpc"},ignore];
531match('copy-config') ->
532    [ignore,{se,"rpc"},{se,"copy-config"},{se,"target"},ignore,{ee,"target"},
533     {se,"source"},ignore,{ee,"source"},{ee,"copy-config"},{ee,"rpc"},ignore];
534match('delete-config') ->
535    [ignore,{se,"rpc"},{se,"delete-config"},{se,"target"},ignore,{ee,"target"},
536     {ee,"delete-config"},{ee,"rpc"},ignore];
537match('lock') ->
538    [ignore,{se,"rpc"},{se,"lock"},{se,"target"},ignore,{ee,"target"},
539     {ee,"lock"},{ee,"rpc"},ignore];
540match('unlock') ->
541    [ignore,{se,"rpc"},{se,"unlock"},{se,"target"},ignore,{ee,"target"},
542     {ee,"unlock"},{ee,"rpc"},ignore];
543match('kill-session') ->
544    [ignore,{se,"rpc"},{se,"kill-session"},{se,"session-id"},ignore,
545     {ee,"session-id"},{ee,"kill-session"},{ee,"rpc"},ignore];
546match(action) ->
547    [ignore,{se,"rpc"},{ns,?ACTION_NAMESPACE},{se,"action"},{se,"data"},ignore,
548     {ee,"data"},{ee,"action"},{ee,"rpc"},ignore];
549match({'create-subscription',Content}) ->
550    [ignore,{se,"rpc"},{ns,?NETCONF_NOTIF_NAMESPACE},
551     {se,"create-subscription"}] ++
552	lists:flatmap(fun(X) ->
553			      [{se,atom_to_list(X)},ignore,{ee,atom_to_list(X)}]
554		      end, Content) ++
555	[{ee,"create-subscription"},{ee,"rpc"},ignore];
556match(any) ->
557    [ignore].
558
559
560
561%%%-----------------------------------------------------------------
562%%% Make message to send to the client.
563%%% Add a new clause for each new message that shall be sent. The
564%%% clause shall match the Reply argument in expect_reply/2 or
565%%% expect_do_reply/3.
566make_msg({hello,SessionId,Stuff}) ->
567    SessionIdXml = session_id(SessionId),
568    CapsXml = capabilities(Stuff),
569    xml(<<"<hello xmlns=\"",?NETCONF_NAMESPACE,"\">\n",CapsXml/binary,
570	  SessionIdXml/binary,"</hello>">>);
571make_msg(ok) ->
572    xml(rpc_reply("<ok/>"));
573
574make_msg({ok,Data}) ->
575    xml(rpc_reply(from_simple({ok,Data})));
576
577make_msg({data,Data}) ->
578    xml(rpc_reply(from_simple({data,Data})));
579
580make_msg({event,N}) ->
581    Notification = <<"<notification xmlns=\"",?NETCONF_NOTIF_NAMESPACE,"\">"
582	  "<eventTime>2012-06-14T14:50:54+02:00</eventTime>"
583	  "<event xmlns=\"http://my.namespaces.com/event\">"
584	  "<severity>major</severity>"
585	  "<description>Something terrible happened</description>"
586	  "</event>"
587	  "</notification>">>,
588    xml(lists:duplicate(N,Notification));
589make_msg(Xml) when is_binary(Xml) orelse
590		   (is_list(Xml) andalso is_binary(hd(Xml))) ->
591    xml(Xml);
592make_msg(Simple) when is_tuple(Simple) ->
593    xml(from_simple(Simple)).
594
595%%%-----------------------------------------------------------------
596%%% Convert to unicode binary, since we use UTF-8 encoding in XML
597unicode_c2b(Characters) ->
598    unicode:characters_to_binary(Characters).
599