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