%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1996-2016. 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.
%%
%% %CopyrightEnd%
%%
-module(auth).
-behaviour(gen_server).
-export([start_link/0]).
%% Old documented interface - deprecated
-export([is_auth/1, cookie/0, cookie/1, node_cookie/1, node_cookie/2]).
-deprecated([{is_auth,1}, {cookie,'_'}, {node_cookie, '_'}]).
%% New interface - meant for internal use within kernel only
-export([get_cookie/0, get_cookie/1,
set_cookie/1, set_cookie/2,
sync_cookie/0,
print/3]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-define(COOKIE_ETS_PROTECTION, protected).
-type cookie() :: atom().
-record(state, {
our_cookie :: cookie(), %% Our own cookie
other_cookies :: ets:tab() %% The send-cookies of other nodes
}).
-type state() :: #state{}.
-include("../include/file.hrl").
%%----------------------------------------------------------------------
%% Exported functions
%%----------------------------------------------------------------------
-spec start_link() -> {'ok',pid()} | {'error', term()} | 'ignore'.
start_link() ->
gen_server:start_link({local, auth}, auth, [], []).
%%--Deprecated interface------------------------------------------------
-spec is_auth(Node) -> 'yes' | 'no' when
Node :: node().
is_auth(Node) ->
case net_adm:ping(Node) of
pong -> yes;
pang -> no
end.
-spec cookie() -> Cookie when
Cookie :: cookie().
cookie() ->
get_cookie().
-spec cookie(TheCookie) -> 'true' when
TheCookie :: Cookie | [Cookie],
Cookie :: cookie().
cookie([Cookie]) ->
set_cookie(Cookie);
cookie(Cookie) ->
set_cookie(Cookie).
-spec node_cookie(Cookies :: [node() | cookie(),...]) -> 'yes' | 'no'.
node_cookie([Node, Cookie]) ->
node_cookie(Node, Cookie).
-spec node_cookie(Node, Cookie) -> 'yes' | 'no' when
Node :: node(),
Cookie :: cookie().
node_cookie(Node, Cookie) ->
set_cookie(Node, Cookie),
is_auth(Node).
%%--"New" interface-----------------------------------------------------
-spec get_cookie() -> 'nocookie' | cookie().
get_cookie() ->
get_cookie(node()).
-spec get_cookie(Node :: node()) -> 'nocookie' | cookie().
get_cookie(_Node) when node() =:= nonode@nohost ->
nocookie;
get_cookie(Node) ->
gen_server:call(auth, {get_cookie, Node}).
-spec set_cookie(Cookie :: cookie()) -> 'true'.
set_cookie(Cookie) ->
set_cookie(node(), Cookie).
-spec set_cookie(Node :: node(), Cookie :: cookie()) -> 'true'.
set_cookie(_Node, _Cookie) when node() =:= nonode@nohost ->
erlang:error(distribution_not_started);
set_cookie(Node, Cookie) ->
gen_server:call(auth, {set_cookie, Node, Cookie}).
-spec sync_cookie() -> any().
sync_cookie() ->
gen_server:call(auth, sync_cookie).
-spec print(Node :: node(), Format :: string(), Args :: [_]) -> 'ok'.
print(Node, Format, Args) ->
(catch gen_server:cast({auth, Node}, {print, Format, Args})).
%%--gen_server callbacks------------------------------------------------
-spec init([]) -> {'ok', state()}.
init([]) ->
process_flag(trap_exit, true),
{ok, init_cookie()}.
%% Opened is a list of servers we have opened up
%% The net kernel will let all message to the auth server
%% through as is
-type calls() :: 'echo' | 'sync_cookie'
| {'get_cookie', node()}
| {'set_cookie', node(), term()}.
-spec handle_call(calls(), {pid(), term()}, state()) ->
{'reply', 'hello' | 'true' | 'nocookie' | cookie(), state()}.
handle_call({get_cookie, Node}, {_From,_Tag}, State) when Node =:= node() ->
{reply, State#state.our_cookie, State};
handle_call({get_cookie, Node}, {_From,_Tag}, State) ->
case ets:lookup(State#state.other_cookies, Node) of
[{Node, Cookie}] ->
{reply, Cookie, State};
[] ->
{reply, State#state.our_cookie, State}
end;
handle_call({set_cookie, Node, Cookie}, {_From,_Tag}, State)
when Node =:= node() ->
{reply, true, State#state{our_cookie = Cookie}};
%%
%% Happens when the distribution is brought up and
%% someone might have set up the cookie for our new node name.
%%
handle_call({set_cookie, Node, Cookie}, {_From,_Tag}, State) ->
ets:insert(State#state.other_cookies, {Node, Cookie}),
{reply, true, State};
handle_call(sync_cookie, _From, State) ->
case ets:lookup(State#state.other_cookies, node()) of
[{_N,C}] ->
ets:delete(State#state.other_cookies, node()),
{reply, true, State#state{our_cookie = C}};
[] ->
{reply, true, State}
end;
handle_call(echo, _From, O) ->
{reply, hello, O}.
%%
%% handle_cast/2
%%
-spec handle_cast({'print', string(), [term()]}, state()) ->
{'noreply', state()}.
handle_cast({print,What,Args}, O) ->
%% always allow print outs
error_logger:error_msg(What, Args),
{noreply, O}.
%% A series of bad messages that may come (from older distribution versions).
-spec handle_info(term(), state()) -> {'noreply', state()}.
handle_info({From,badcookie,net_kernel,{From,spawn,_M,_F,_A,_Gleader}}, O) ->
auth:print(node(From) ,"~n** Unauthorized spawn attempt to ~w **~n",
[node()]),
erlang:disconnect_node(node(From)),
{noreply, O};
handle_info({From,badcookie,net_kernel,{From,spawn_link,_M,_F,_A,_Gleader}}, O) ->
auth:print(node(From),
"~n** Unauthorized spawn_link attempt to ~w **~n",
[node()]),
erlang:disconnect_node(node(From)),
{noreply, O};
handle_info({_From,badcookie,ddd_server,_Mess}, O) ->
%% Ignore bad messages to the ddd server, they will be resent
%% If the authentication is successful
{noreply, O};
handle_info({From,badcookie,rex,_Msg}, O) ->
auth:print(getnode(From),
"~n** Unauthorized rpc attempt to ~w **~n", [node()]),
disconnect_node(node(From)),
{noreply, O};
%% These two messages have to do with the old auth:is_auth() call (net_adm:ping)
handle_info({From,badcookie,net_kernel,{'$gen_call',{From,Tag},{is_auth,_Node}}}, O) -> %% ho ho
From ! {Tag, no},
{noreply, O};
handle_info({_From,badcookie,To,{{auth_reply,N},R}}, O) ->%% Let auth replys through
catch To ! {{auth_reply,N},R},
{noreply, O};
handle_info({From,badcookie,Name,Mess}, Opened) ->
%% This may be registered send as well as pid send.
case lists:member(Name, Opened) of
true ->
catch Name ! Mess;
false ->
case catch lists:member(element(1, Mess), Opened) of
true ->
catch Name ! Mess; %% Might be a pid as well
_ ->
auth:print(getnode(From),
"~n** Unauthorized send attempt ~w to ~w **~n",
[Mess,node()]),
erlang:disconnect_node(getnode(From))
end
end,
{noreply, Opened};
handle_info(_, O) -> % Ignore anything else especially EXIT signals
{noreply, O}.
-spec code_change(term(), state(), term()) -> {'ok', state()}.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
-spec terminate(term(), state()) -> 'ok'.
terminate(_Reason, _State) ->
ok.
getnode(P) when is_pid(P) -> node(P);
getnode(P) -> P.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%
%%% Cookie functions
%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Read cookie from $HOME/.erlang.cookie and set it.
init_cookie() ->
case init:get_argument(nocookie) of
error ->
case init:get_argument(setcookie) of
{ok, [[C0]]} ->
C = list_to_atom(C0),
#state{our_cookie = C,
other_cookies = ets:new(cookies,
[?COOKIE_ETS_PROTECTION])};
_ ->
%% Here is the default
case read_cookie() of
{error, Error} ->
error_logger:error_msg(Error, []),
%% Is this really this serious?
erlang:error(Error);
{ok, Co} ->
#state{our_cookie = list_to_atom(Co),
other_cookies = ets:new(
cookies,
[?COOKIE_ETS_PROTECTION])}
end
end;
_Other ->
#state{our_cookie = nocookie,
other_cookies = ets:new(cookies, [?COOKIE_ETS_PROTECTION])}
end.
read_cookie() ->
case init:get_argument(home) of
{ok, [[Home]]} ->
read_cookie(filename:join(Home, ".erlang.cookie"));
_ ->
{error, "No home for cookie file"}
end.
read_cookie(Name) ->
case file:raw_read_file_info(Name) of
{ok, #file_info {type=Type, mode=Mode, size=Size}} ->
case check_attributes(Name, Type, Mode, os:type()) of
ok -> read_cookie(Name, Size);
Error -> Error
end;
{error, enoent} ->
case create_cookie(Name) of
ok -> read_cookie(Name);
Error -> Error
end;
{error, Reason} ->
{error, make_error(Name, Reason)}
end.
read_cookie(Name, Size) ->
case file:open(Name, [raw, read]) of
{ok, File} ->
case file:read(File, Size) of
{ok, List} ->
ok = file:close(File),
check_cookie(List, []);
{error, Reason} ->
make_error(Name, Reason)
end;
{error, Reason} ->
make_error(Name, Reason)
end.
make_error(Name, Reason) ->
{error, "Error when reading " ++ Name ++ ": " ++ atom_to_list(Reason)}.
%% Verifies that only the owner can access the cookie file.
check_attributes(Name, Type, _Mode, _Os) when Type =/= regular ->
{error, "Cookie file " ++ Name ++ " is of type " ++ Type};
check_attributes(Name, _Type, Mode, {unix, _}) when (Mode band 8#077) =/= 0 ->
{error, "Cookie file " ++ Name ++ " must be accessible by owner only"};
check_attributes(_Name, _Type, _Mode, _Os) ->
ok.
%% Checks that the cookie has the correct format.
check_cookie([Letter|Rest], Result) when $\s =< Letter, Letter =< $~ ->
check_cookie(Rest, [Letter|Result]);
check_cookie([X|Rest], Result) ->
check_cookie1([X|Rest], Result);
check_cookie([], Result) ->
check_cookie1([], Result).
check_cookie1([$\n|Rest], Result) ->
check_cookie1(Rest, Result);
check_cookie1([$\r|Rest], Result) ->
check_cookie1(Rest, Result);
check_cookie1([$\s|Rest], Result) ->
check_cookie1(Rest, Result);
check_cookie1([_|_], _Result) ->
{error, "Bad characters in cookie"};
check_cookie1([], []) ->
{error, "Too short cookie string"};
check_cookie1([], Result) ->
{ok, lists:reverse(Result)}.
%% Creates a new, random cookie.
create_cookie(Name) ->
Seed = abs(erlang:monotonic_time()
bxor erlang:unique_integer()),
Cookie = random_cookie(20, Seed, []),
case file:open(Name, [write, raw]) of
{ok, File} ->
R1 = file:write(File, Cookie),
ok = file:close(File),
R2 = file:raw_write_file_info(Name, make_info(Name)),
case {R1, R2} of
{ok, ok} ->
ok;
{{error,Reason}, _} ->
{error,
lists:flatten(
io_lib:format("Failed to write to cookie file '~ts': ~p", [Name, Reason]))};
{ok, {error, Reason}} ->
{error, "Failed to change mode: " ++ atom_to_list(Reason)}
end;
{error,Reason} ->
{error,
lists:flatten(
io_lib:format("Failed to create cookie file '~ts': ~p", [Name, Reason]))}
end.
random_cookie(0, _, Result) ->
Result;
random_cookie(Count, X0, Result) ->
X = next_random(X0),
Letter = X*($Z-$A+1) div 16#1000000000 + $A,
random_cookie(Count-1, X, [Letter|Result]).
%% Returns suitable information for a new cookie.
%%
%% Note: Since the generated cookie depends on the time the file was
%% created, and the time can be seen plainly in the file, we will
%% round down the file creation times to the nearest midnight to
%% give crackers some more work.
make_info(Name) ->
Midnight =
case file:raw_read_file_info(Name) of
{ok, #file_info{atime={Date, _}}} ->
{Date, {0, 0, 0}};
_ ->
{{1990, 1, 1}, {0, 0, 0}}
end,
#file_info{mode=8#400, atime=Midnight, mtime=Midnight, ctime=Midnight}.
%% This RNG is from line 21 on page 102 in Knuth: The Art of Computer Programming,
%% Volume II, Seminumerical Algorithms.
%%
%% Returns an integer in the range 0..(2^35-1).
next_random(X) ->
(X*17059465+1) band 16#fffffffff.