%%%---------------------------------------------------------------------- %%% File : sip.erl %%% Author : Evgeniy Khramtsov %%% Purpose : Main SIP instance %%% Created : 14 Jul 2009 by Evgeniy Khramtsov %%% %%% %%% Copyright (C) 2002-2017 ProcessOne, SARL. All Rights Reserved. %%% %%% 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 %%% %%% 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. %%% %%%---------------------------------------------------------------------- -module(esip). -behaviour(gen_server). %% API -export([start_link/0, start/0, stop/0]). -export([add_hdr/3, add_listener/3, callback/1, callback/2, callback/3, cancel/1, cancel/2, check_auth/4, close_dialog/1, connect/1, connect/2, decode/1, decode_uri/1, decode_uri_field/1, del_listener/2, dialog_id/2, encode/1, encode_uri/1, encode_uri_field/1, error_status/1, escape/1, filter_hdrs/2, get_branch/1, get_config/0, get_config_value/1, get_hdr/2, get_hdr/3, get_hdrs/2, get_node_by_tag/1, get_param/2, get_param/3, get_so_path/0, has_param/2, hex_encode/1, make_auth/6, make_branch/0, make_branch/1, make_callid/0, make_cseq/0, make_hdrs/0, make_hexstr/1, make_response/2, make_response/3, make_tag/0, match/2, mod/0, open_dialog/4, quote/1, reason/1, reply/2, request/2, request/3, request/4, rm_hdr/2, send/2, set_config_value/2, set_hdr/3, set_param/3, split_hdrs/2, stop_transaction/1, timer1/0, timer2/0, timer4/0, to_lower/1, unescape/1, unquote/1, warning/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -include("esip.hrl"). -include("esip_lib.hrl"). -record(state, {node_id}). -callback request(#sip{}, #sip_socket{}) -> #sip{} | ok | drop. -callback request(#sip{}, #sip_socket{}, #trid{}) -> #sip{} | wait | {module(), atom(), list()} | function() | {error, any()}. -callback response(#sip{}, #sip_socket{}) -> any(). -callback message_in(ping | #sip{}, #sip_socket{}) -> pang | pong | drop | ok | #sip{}. -callback message_out(#sip{}, #sip_socket{}) -> #sip{} | ok | drop. -callback locate(#sip{}) -> #uri{} | #via{} | ok. -callback data_in(iodata(), #sip_socket{}) -> any(). -callback data_out(iodata(), #sip_socket{}) -> any(). %%==================================================================== %% API %%==================================================================== start() -> application:start(esip). stop() -> application:stop(esip). start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). get_so_path() -> EbinDir = filename:dirname(code:which(?MODULE)), AppDir = filename:dirname(EbinDir), filename:join([AppDir, "priv", "lib"]). add_listener(Port, Transport, Opts) -> esip_listener:add_listener(Port, Transport, Opts). del_listener(Port, Transport) -> esip_listener:del_listener(Port, Transport). connect(SIPMsg) -> connect(SIPMsg, []). connect(SIPMsg, Opts) -> esip_transport:connect(SIPMsg, Opts). request(SIPSocket, Request) -> request(SIPSocket, Request, undefined). request(SIPSocket, Request, TU) -> request(SIPSocket, Request, TU, []). request(SIPSocket, Request, TU, Opts) -> esip_transaction:request(SIPSocket, Request, TU, Opts). reply(RequestOrTrID, Response) -> esip_transaction:reply(RequestOrTrID, Response). cancel(RequestOrTrID) -> cancel(RequestOrTrID, undefined). cancel(RequestOrTrID, TU) -> esip_transaction:cancel(RequestOrTrID, TU). send(SIPSocket, ReqOrResp) -> esip_transport:send(SIPSocket, ReqOrResp). stop_transaction(TrID) -> esip_transaction:stop(TrID). make_tag() -> {NodeID, N} = gen_server:call(?MODULE, make_tag), iolist_to_binary([NodeID, $-, int_to_list(N)]). make_branch() -> N = gen_server:call(?MODULE, make_branch), iolist_to_binary(["z9hG4bK-", int_to_list(N)]). make_branch(Hdrs) -> case get_hdrs('via', Hdrs) of [] -> make_branch(); [Via|_] -> TopBranch = to_lower(get_param(<<"branch">>, Via#via.params)), Cookie = atom_to_list(erlang:get_cookie()), ID = hex_encode(erlang:md5([TopBranch, Cookie])), iolist_to_binary(["z9hG4bK-", ID]) end. make_callid() -> N = gen_server:call(?MODULE, make_callid), iolist_to_binary([int_to_list(N)]). make_cseq() -> gen_server:call(?MODULE, make_cseq). make_hdrs() -> Hdrs = [{'cseq', make_cseq()}, {'max-forwards', get_config_value(max_forwards)}, {'call-id', make_callid()}], case get_config_value(software) of undefined -> Hdrs; Software -> [{'user-agent', Software}|Hdrs] end. dialog_id(Type, SIPMsg) -> esip_dialog:id(Type, SIPMsg). open_dialog(Request, ResponseOrTag, TypeOrState, TU) -> esip_dialog:open(Request, ResponseOrTag, TypeOrState, TU). close_dialog(DialogID) -> esip_dialog:close(DialogID). decode(Data) -> esip_codec:decode(Data). decode_uri(Data) -> esip_codec:decode_uri(Data). decode_uri_field(Data) -> esip_codec:decode_uri_field(Data). encode(R) -> esip_codec:encode(R). encode_uri(URI) -> esip_codec:encode_uri(URI). encode_uri_field(URI) -> esip_codec:encode_uri_field(URI). match(Arg1, Arg2) -> esip_codec:match(Arg1, Arg2). rm_hdr(Hdr, Hdrs) -> rm_key(Hdr, Hdrs). add_hdr(Hdr, Val, Hdrs) -> add_key(Hdr, Val, Hdrs). set_hdr(Hdr, Val, Hdrs) -> set_key(Hdr, Val, Hdrs). get_hdr(Hdr, Hdrs) -> get_key(Hdr, Hdrs, undefined). get_hdr(Hdr, Hdrs, Default) -> get_key(Hdr, Hdrs, Default). get_hdrs(Hdr, Hdrs) -> get_keys(Hdr, Hdrs). filter_hdrs(HdrList, Hdrs) -> filter_keys(HdrList, Hdrs). split_hdrs(HdrOrHdrList, Hdrs) -> split_keys(HdrOrHdrList, Hdrs). set_param(Param, Val, Params) -> set_key(Param, Val, Params). get_param(Param, Params) -> get_key(Param, Params, <<>>). get_param(Param, Params, Default) -> get_key(Param, Params, Default). has_param(Param, Params) -> has_key(Param, Params). get_branch(Hdrs) -> [Via|_] = get_hdr('via', Hdrs), get_param(<<"branch">>, Via#via.params). escape(Bin) -> esip_codec:escape(Bin). unescape(Bin) -> esip_codec:unescape(Bin). -compile({inline, [{is_equal, 2}, {member, 2}, {keysearch, 2}]}). is_equal(K1, K2) when is_binary(K1), is_binary(K2) -> esip_codec:strcasecmp(K1, K2); is_equal(K1, K2) -> K1 == K2. member(X, Xs) -> if is_atom(X) -> %% lists:member/2 is BIF, so works faster lists:member(X, Xs); true -> member1(X, Xs) end. member1(X, [Y|T]) -> case is_equal(X, Y) of true -> true; _ -> member1(X, T) end; member1(_X, []) -> false. keysearch(K, Ks) -> if is_atom(K) -> %% lists:keysearch/3 is a BIF, so works faster case lists:keysearch(K, 1, Ks) of {value, {_, Val}} -> {ok, Val}; _ -> error end; true -> keysearch1(K, Ks) end. keysearch1(_K, []) -> error; keysearch1(K, [{K1, V}|T]) -> case is_equal(K, K1) of true -> {ok, V}; false -> keysearch1(K, T) end. rm_key(Key, Keys) -> lists:filter( fun({K, _}) -> not is_equal(Key, K) end, Keys). set_key(Key, Val, Keys) -> Res = lists:foldl( fun({K, V}, Acc) -> case is_equal(K, Key) of true -> Acc; false -> [{K, V}|Acc] end end, [], Keys), lists:reverse([{Key, Val}|Res]). get_key(Key, Keys, Default) -> case keysearch(Key, Keys) of {ok, Val} -> Val; _ -> Default end. has_key(Key, Keys) -> case get_key(Key, Keys, undefined) of undefined -> false; _ -> true end. add_key(Key, Val, Keys) -> case lists:foldl( fun({K, V}, {S, Acc}) -> case S == false andalso is_equal(K, Key) of true -> {true, [{K, V}, {Key, Val}|Acc]}; false -> {S, [{K, V}|Acc]} end end, {false, []}, Keys) of {true, Res} -> lists:reverse(Res); {false, Res} -> lists:reverse([{Key, Val}|Res]) end. get_keys(Key, Keys) -> lists:flatmap( fun({K, V}) -> case is_equal(K, Key) of true when is_list(V) -> V; true -> [V]; false -> [] end end, Keys). filter_keys(KeyList, Keys) -> lists:filter( fun({Key, _}) -> member(Key, KeyList) end, Keys). split_keys(KeyList, Keys) when is_list(KeyList) -> lists:partition( fun({Key, _}) -> member(Key, KeyList) end, Keys); split_keys(Key, Keys) -> lists:foldr( fun({K, V}, {H, T}) -> case is_equal(K, Key) of true when is_list(V) -> {V++H, T}; true -> {[V|H], T}; false -> {H, [{K, V}|T]} end end, {[], []}, Keys). make_response(Req, Resp) -> make_response(Req, Resp, <<>>). make_response(#sip{hdrs = ReqHdrs, method = Method, type = request}, #sip{status = Status, hdrs = RespHdrs, method = RespMethod, type = response} = Resp, Tag) -> NeedHdrs = if Status == 100 -> filter_hdrs(['via', 'from', 'call-id', 'cseq', 'max-forwards', 'to', 'timestamp'], ReqHdrs); Status > 100, Status < 300 -> filter_hdrs(['via', 'record-route', 'from', 'call-id', 'cseq', 'max-forwards', 'to'], ReqHdrs); true -> filter_hdrs(['via', 'from', 'call-id', 'cseq', 'max-forwards', 'to'], ReqHdrs) end, NewNeedHdrs = if Status > 100 -> {ToName, ToURI, ToParams} = get_hdr('to', NeedHdrs), case has_param(<<"tag">>, ToParams) of false -> NewTo = {ToName, ToURI, [{<<"tag">>, Tag}|ToParams]}, set_hdr('to', NewTo, NeedHdrs); true -> NeedHdrs end; true -> NeedHdrs end, NewMethod = if RespMethod /= undefined -> RespMethod; true -> Method end, ResultHdrs = case get_config_value(software) of undefined -> NewNeedHdrs ++ RespHdrs; Software -> NewNeedHdrs ++ [{'server', Software}|RespHdrs] end, Resp#sip{method = NewMethod, hdrs = ResultHdrs}. make_auth({Type, Params}, Method, Body, OrigURI, Username, Password) -> Nonce = esip:get_param(<<"nonce">>, Params), QOPs = esip:get_param(<<"qop">>, Params), Algo = case esip:get_param(<<"algorithm">>, Params) of <<>> -> <<"MD5">>; Algo1 -> Algo1 end, Realm = esip:get_param(<<"realm">>, Params), OpaqueParam = case esip:get_param(<<"opaque">>, Params) of <<>> -> []; Opaque -> [{<<"opaque">>, Opaque}] end, CNonce = make_hexstr(20), NC = <<"00000001">>, %% TODO URI = if is_binary(OrigURI) -> OrigURI; is_record(OrigURI, uri) -> iolist_to_binary(esip_codec:encode_uri(OrigURI)) end, QOPList = esip_codec:split(unquote(QOPs), $,), QOP = case lists:member(<<"auth">>, QOPList) of true -> <<"auth">>; false -> case lists:member(<<"auth-int">>, QOPList) of true -> <<"auth-int">>; false -> <<>> end end, Response = compute_digest(Nonce, CNonce, NC, QOP, Algo, Realm, URI, Method, Body, Username, Password), {Type, [{<<"username">>, quote(Username)}, {<<"realm">>, Realm}, {<<"nonce">>, Nonce}, {<<"uri">>, quote(URI)}, {<<"response">>, quote(Response)}, {<<"algorithm">>, Algo} | if QOP /= <<>> -> [{<<"cnonce">>, quote(CNonce)}, {<<"nc">>, NC}, {<<"qop">>, QOP}]; true -> [] end] ++ OpaqueParam}. check_auth({Type, Params}, Method, Body, Password) -> case to_lower(Type) of <<"digest">> -> NewMethod = case Method of <<"ACK">> -> <<"INVITE">>; _ -> Method end, Nonce = esip:get_param(<<"nonce">>, Params), NC = esip:get_param(<<"nc">>, Params), CNonce = esip:get_param(<<"cnonce">>, Params), QOP = esip:get_param(<<"qop">>, Params), Algo = esip:get_param(<<"algorithm">>, Params), Username = esip:get_param(<<"username">>, Params), Realm = esip:get_param(<<"realm">>, Params), URI = esip:get_param(<<"uri">>, Params), Response = unquote(esip:get_param(<<"response">>, Params)), Response == compute_digest(Nonce, CNonce, NC, QOP, Algo, Realm, URI, NewMethod, Body, Username, Password); _ -> false end. make_hexstr(N) -> hex_encode(rand_bytes(N)). hex_encode(Data) -> << <<(esip_codec:to_hex(X))/binary>> || <> <= Data >>. to_lower(Bin) -> esip_codec:to_lower(Bin). get_config() -> ets:tab2list(esip_config). get_config_value(Key) -> case ets:lookup(esip_config, Key) of [{_, Val}] -> Val; _ -> undefined end. mod() -> get_config_value(module). set_config_value(Key, Val) -> ets:insert(esip_config, {Key, Val}). callback({M, F, A}) -> callback(M, F, A). callback(F, Args) when is_function(F) -> case catch apply(F, Args) of {'EXIT', _} = Err -> ?ERROR_MSG("failed to process callback:~n" "** Function: ~p~n" "** Args: ~p~n" "** Reason: ~p", [F, Args, Err]), {error, internal_server_error}; Result -> Result end; callback(F, Args) -> callback(get_config_value(module), F, Args). callback(Mod, Fun, Args) -> case catch apply(Mod, Fun, Args) of {'EXIT', _} = Err -> ?ERROR_MSG("failed to process callback:~n" "** Function: ~p:~p/~p~n" "** Args: ~p~n" "** Reason: ~p", [Mod, Fun, length(Args), Args, Err]), {error, internal_server_error}; Result -> Result end. %% Basic Timers timer1() -> get_config_value(timer1). timer2() -> get_config_value(timer2). timer4() -> get_config_value(timer4). error_status({error, Err}) -> error_status(Err); error_status(timeout) -> {408, reason(408)}; error_status(no_contact_header) -> {400, <<"Missed Contact header">>}; error_status(too_many_transactions) -> {500, <<"Too Many Transactions">>}; error_status(unsupported_uri_scheme) -> {416, reason(416)}; error_status(unsupported_transport) -> {503, "Unsupported Transport"}; error_status(Err) when is_atom(Err) -> case inet:format_error(Err) of "unknown POSIX error" -> {500, reason(500)}; [H|T] when H >= $a, H =< $z -> {503, list_to_binary([H - $ |T])}; Txt -> {503, Txt} end; error_status(_) -> {500, reason(500)}. get_node_by_tag(Tag) -> case esip_codec:split(Tag, $-, 1) of [NodeID, _] -> get_node_by_id(NodeID); _ -> node() end. get_node_by_id(NodeID) when is_binary(NodeID) -> case catch erlang:binary_to_existing_atom(NodeID, utf8) of {'EXIT', _} -> node(); Res -> get_node_by_id(Res) end; get_node_by_id(NodeID) -> case global:whereis_name(NodeID) of Pid when is_pid(Pid) -> node(Pid); _ -> node() end. %% From http://www.iana.org/assignments/sip-parameters reason(100) -> <<"Trying">>; reason(180) -> <<"Ringing">>; reason(181) -> <<"Call Is Being Forwarded">>; reason(182) -> <<"Queued">>; reason(183) -> <<"Session Progress">>; reason(200) -> <<"OK">>; reason(202) -> <<"Accepted">>; reason(204) -> <<"No Notification">>; reason(300) -> <<"Multiple Choices">>; reason(301) -> <<"Moved Permanently">>; reason(302) -> <<"Moved Temporarily">>; reason(305) -> <<"Use Proxy">>; reason(380) -> <<"Alternative Service">>; reason(400) -> <<"Bad Request">>; reason(401) -> <<"Unauthorized">>; reason(402) -> <<"Payment Required">>; reason(403) -> <<"Forbidden">>; reason(404) -> <<"Not Found">>; reason(405) -> <<"Method Not Allowed">>; reason(406) -> <<"Not Acceptable">>; reason(407) -> <<"Proxy Authentication Required">>; reason(408) -> <<"Request Timeout">>; reason(410) -> <<"Gone">>; reason(412) -> <<"Conditional Request Failed">>; reason(413) -> <<"Request Entity Too Large">>; reason(414) -> <<"Request-URI Too Long">>; reason(415) -> <<"Unsupported Media Type">>; reason(416) -> <<"Unsupported URI Scheme">>; reason(417) -> <<"Unknown Resource-Priority">>; reason(420) -> <<"Bad Extension">>; reason(421) -> <<"Extension Required">>; reason(422) -> <<"Session Interval Too Small">>; reason(423) -> <<"Interval Too Brief">>; reason(428) -> <<"Use Identity Header">>; reason(429) -> <<"Provide Referrer Identity">>; reason(430) -> <<"Flow Failed">>; reason(433) -> <<"Anonymity Disallowed">>; reason(436) -> <<"Bad Identity-Info">>; reason(437) -> <<"Unsupported Certificate">>; reason(438) -> <<"Invalid Identity Header">>; reason(439) -> <<"First Hop Lacks Outbound Support">>; reason(440) -> <<"Max-Breadth Exceeded">>; reason(469) -> <<"Bad Info Package">>; reason(470) -> <<"Consent Needed">>; reason(480) -> <<"Temporarily Unavailable">>; reason(481) -> <<"Call/Transaction Does Not Exist">>; reason(482) -> <<"Loop Detected">>; reason(483) -> <<"Too Many Hops">>; reason(484) -> <<"Address Incomplete">>; reason(485) -> <<"Ambiguous">>; reason(486) -> <<"Busy Here">>; reason(487) -> <<"Request Terminated">>; reason(488) -> <<"Not Acceptable Here">>; reason(489) -> <<"Bad Event">>; reason(491) -> <<"Request Pending">>; reason(493) -> <<"Undecipherable">>; reason(494) -> <<"Security Agreement Required">>; reason(500) -> <<"Server Internal Error">>; reason(501) -> <<"Not Implemented">>; reason(502) -> <<"Bad Gateway">>; reason(503) -> <<"Service Unavailable">>; reason(504) -> <<"Server Time-out">>; reason(505) -> <<"Version Not Supported">>; reason(513) -> <<"Message Too Large">>; reason(580) -> <<"Precondition Failure">>; reason(600) -> <<"Busy Everywhere">>; reason(603) -> <<"Decline">>; reason(604) -> <<"Does Not Exist Anywhere">>; reason(606) -> <<"Not Acceptable">>; reason(Status) when Status > 100, Status < 200 -> <<"Session Progress">>; reason(Status) when Status > 200, Status < 300 -> <<"Accepted">>; reason(Status) when Status > 300, Status < 400 -> <<"Multiple Choices">>; reason(Status) when Status > 400, Status < 500 -> <<"Bad Request">>; reason(Status) when Status > 500, Status < 600 -> <<"Server Internal Error">>; reason(Status) when Status > 600, Status < 700 -> <<"Busy Everywhere">>. warning(300) -> <<"\"Incompatible network protocol\"">>; warning(301) -> <<"\"Incompatible network address formats\"">>; warning(302) -> <<"\"Incompatible transport protocol\"">>; warning(303) -> <<"\"Incompatible bandwidth units\"">>; warning(304) -> <<"\"Media type not available\"">>; warning(305) -> <<"\"Incompatible media format\"">>; warning(306) -> <<"\"Attribute not understood\"">>; warning(307) -> <<"\"Session description parameter not understood\"">>; warning(330) -> <<"\"Multicast not available\"">>; warning(331) -> <<"\"Unicast not available\"">>; warning(370) -> <<"\"Insufficient bandwidth\"">>; warning(380) -> <<"\"SIPS Not Allowed\"">>; warning(381) -> <<"\"SIPS Required\"">>; warning(399) -> <<"\"Miscellaneous warning\"">>; warning(Code) when Code > 300, Code < 400 -> <<"\"\"">>. %%==================================================================== %% gen_server callbacks %%==================================================================== init([]) -> ets:new(esip_config, [named_table, public]), set_config([]), NodeID = list_to_binary(integer_to_list(rand_uniform(1 bsl 32))), register_node(NodeID), {ok, #state{node_id = NodeID}}. handle_call(make_tag, _From, State) -> {reply, {State#state.node_id, rand_uniform(1 bsl 32)}, State}; handle_call(make_branch, _From, State) -> {reply, rand_uniform(1 bsl 48), State}; handle_call(make_callid, _From, State) -> {reply, rand_uniform(1 bsl 48), State}; handle_call(make_cseq, _From, State) -> {reply, rand_uniform(1 bsl 10), State}; handle_call(stop, _From, State) -> {stop, normal, State}; handle_call(_Request, _From, State) -> {reply, bad_request, State}. handle_cast(_Msg, State) -> {noreply, State}. handle_info(_Info, State) -> ?ERROR_MSG("got unexpected info: ~p", [_Info]), {noreply, State}. terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- default_config() -> Software = case catch application:get_key(esip, vsn) of {ok, [_|_] = Ver} -> list_to_binary(["esip/", Ver]); _ -> <<"esip">> end, [{max_forwards, 70}, {timer1, 500}, {timer2, 4000}, {timer4, 5000}, {software, Software}, {max_msg_size, 128*1024}]. set_config(Opts) -> lists:foreach( fun({Key, Value}) -> ets:insert(esip_config, {Key, Value}); (_) -> ok end, default_config() ++ Opts). int_to_list(N) -> erlang:integer_to_list(N). register_node(NodeID) -> global:register_name(erlang:binary_to_atom(NodeID, utf8), self()). unquote(<<$", Rest/binary>>) -> case size(Rest) - 1 of Size when Size > 0 -> <> = Rest, Result; _ -> <<>> end; unquote(Val) -> Val. quote(Val) -> <<$", Val/binary, $">>. md5_digest(Data) -> hex_encode(erlang:md5(Data)). compute_digest(Nonce, CNonce, NC, QOP, Algo, Realm, URI, Method, Body, Username, Password) -> AlgoL = to_lower(Algo), QOPL = to_lower(QOP), A1 = if AlgoL == <<"md5">>; AlgoL == <<>> -> [unquote(Username), $:, unquote(Realm), $:, Password]; true -> [md5_digest([unquote(Username), $:, unquote(Realm), $:, Password]), $:, unquote(Nonce), $:, unquote(CNonce)] end, A2 = if QOPL == <<"auth">>; QOPL == <<>> -> [Method, $:, unquote(URI)]; true -> [Method, $:, unquote(URI), $:, md5_digest(Body)] end, if QOPL == <<"auth">>; QOPL == <<"auth-int">> -> md5_digest( [md5_digest(A1), $:, unquote(Nonce), $:, NC, $:, unquote(CNonce), $:, unquote(QOP), $:, md5_digest(A2)]); true -> md5_digest( [md5_digest(A1), $:, unquote(Nonce), $:, md5_digest(A2)]) end. -ifdef(STRONG_RAND_BYTES). rand_bytes(N) -> crypto:strong_rand_bytes(N). -else. rand_bytes(N) -> crypto:rand_bytes(N). -endif. -ifdef(RAND_UNIFORM). rand_uniform(N) -> rand:uniform(N). -else. rand_uniform(N) -> crypto:rand_uniform(1, N). -endif.