aboutsummaryrefslogtreecommitdiffstats
path: root/lib/ssh/src/ssh_cli.erl
diff options
context:
space:
mode:
Diffstat (limited to 'lib/ssh/src/ssh_cli.erl')
-rw-r--r--lib/ssh/src/ssh_cli.erl500
1 files changed, 500 insertions, 0 deletions
diff --git a/lib/ssh/src/ssh_cli.erl b/lib/ssh/src/ssh_cli.erl
new file mode 100644
index 0000000000..964f35121a
--- /dev/null
+++ b/lib/ssh/src/ssh_cli.erl
@@ -0,0 +1,500 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2005-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%
+%% Description: a gen_server implementing a simple
+%% terminal (using the group module) for a CLI
+%% over SSH
+
+-module(ssh_cli).
+
+-behaviour(ssh_channel).
+
+-include("ssh.hrl").
+-include("ssh_connect.hrl").
+
+%% ssh_channel callbacks
+-export([init/1, handle_ssh_msg/2, handle_msg/2, terminate/2]).
+
+%% backwards compatibility
+-export([listen/1, listen/2, listen/3, listen/4, stop/1]).
+
+%% state
+-record(state, {
+ cm,
+ channel,
+ pty,
+ group,
+ buf,
+ shell,
+ exec
+ }).
+
+%%====================================================================
+%% ssh_channel callbacks
+%%====================================================================
+
+%%--------------------------------------------------------------------
+%% Function: init(Args) -> {ok, State}
+%%
+%% Description: Initiates the CLI
+%%--------------------------------------------------------------------
+init([Shell, Exec]) ->
+ {ok, #state{shell = Shell, exec = Exec}};
+init([Shell]) ->
+ {ok, #state{shell = Shell}}.
+
+%%--------------------------------------------------------------------
+%% Function: handle_ssh_msg(Args) -> {ok, State} | {stop, ChannelId, State}
+%%
+%% Description: Handles channel messages received on the ssh-connection.
+%%--------------------------------------------------------------------
+handle_ssh_msg({ssh_cm, _ConnectionManager,
+ {data, _ChannelId, _Type, Data}},
+ #state{group = Group} = State) ->
+ Group ! {self(), {data, binary_to_list(Data)}},
+ {ok, State};
+
+handle_ssh_msg({ssh_cm, ConnectionManager,
+ {pty, ChannelId, WantReply,
+ {TermName, Width, Height, PixWidth, PixHeight, Modes}}},
+ State0) ->
+ State = State0#state{pty =
+ #ssh_pty{term = TermName,
+ width = not_zero(Width, 80),
+ height = not_zero(Height, 24),
+ pixel_width = PixWidth,
+ pixel_height = PixHeight,
+ modes = Modes}},
+ set_echo(State),
+ ssh_connection:reply_request(ConnectionManager, WantReply,
+ success, ChannelId),
+ {ok, State};
+
+handle_ssh_msg({ssh_cm, ConnectionManager,
+ {env, ChannelId, WantReply, _Var, _Value}}, State) ->
+ ssh_connection:reply_request(ConnectionManager,
+ WantReply, failure, ChannelId),
+ {ok, State};
+
+handle_ssh_msg({ssh_cm, ConnectionManager,
+ {window_change, ChannelId, Width, Height, PixWidth, PixHeight}},
+ #state{buf = Buf, pty = Pty0} = State) ->
+ Pty = Pty0#ssh_pty{width = Width, height = Height,
+ pixel_width = PixWidth,
+ pixel_height = PixHeight},
+ {Chars, NewBuf} = io_request({window_change, Pty0}, Buf, Pty),
+ write_chars(ConnectionManager, ChannelId, Chars),
+ {ok, State#state{pty = Pty, buf = NewBuf}};
+
+handle_ssh_msg({ssh_cm, ConnectionManager,
+ {shell, ChannelId, WantReply}}, State) ->
+ NewState = start_shell(ConnectionManager, State),
+ ssh_connection:reply_request(ConnectionManager, WantReply,
+ success, ChannelId),
+ {ok, NewState#state{channel = ChannelId,
+ cm = ConnectionManager}};
+
+handle_ssh_msg({ssh_cm, ConnectionManager,
+ {exec, ChannelId, WantReply, Cmd}}, #state{exec=undefined} = State) ->
+ {Reply, Status} = exec(Cmd),
+ write_chars(ConnectionManager,
+ ChannelId, io_lib:format("~p\n", [Reply])),
+ ssh_connection:reply_request(ConnectionManager, WantReply,
+ success, ChannelId),
+ ssh_connection:exit_status(ConnectionManager, ChannelId, Status),
+ ssh_connection:send_eof(ConnectionManager, ChannelId),
+ {stop, ChannelId, State#state{channel = ChannelId, cm = ConnectionManager}};
+handle_ssh_msg({ssh_cm, ConnectionManager,
+ {exec, ChannelId, WantReply, Cmd}}, State) ->
+ NewState = start_shell(ConnectionManager, Cmd, State),
+ ssh_connection:reply_request(ConnectionManager, WantReply,
+ success, ChannelId),
+ {ok, NewState#state{channel = ChannelId,
+ cm = ConnectionManager}};
+
+handle_ssh_msg({ssh_cm, _ConnectionManager, {eof, _ChannelId}}, State) ->
+ {ok, State};
+
+handle_ssh_msg({ssh_cm, _, {signal, _, _}}, State) ->
+ %% Ignore signals according to RFC 4254 section 6.9.
+ {ok, State};
+
+handle_ssh_msg({ssh_cm, _, {exit_signal, ChannelId, _, Error, _}}, State) ->
+ Report = io_lib:format("Connection closed by peer ~n Error ~p~n",
+ [Error]),
+ error_logger:error_report(Report),
+ {stop, ChannelId, State};
+
+handle_ssh_msg({ssh_cm, _, {exit_status, ChannelId, 0}}, State) ->
+ {stop, ChannelId, State};
+
+handle_ssh_msg({ssh_cm, _, {exit_status, ChannelId, Status}}, State) ->
+
+ Report = io_lib:format("Connection closed by peer ~n Status ~p~n",
+ [Status]),
+ error_logger:error_report(Report),
+ {stop, ChannelId, State}.
+
+%%--------------------------------------------------------------------
+%% Function: handle_msg(Args) -> {ok, State} | {stop, ChannelId, State}
+%%
+%% Description: Handles other channel messages.
+%%--------------------------------------------------------------------
+handle_msg({ssh_channel_up, ChannelId, ConnectionManager},
+ #state{channel = ChannelId,
+ cm = ConnectionManager} = State) ->
+ {ok, State};
+
+handle_msg({Group, Req}, #state{group = Group, buf = Buf, pty = Pty,
+ cm = ConnectionManager,
+ channel = ChannelId} = State) ->
+ {Chars, NewBuf} = io_request(Req, Buf, Pty),
+ write_chars(ConnectionManager, ChannelId, Chars),
+ {ok, State#state{buf = NewBuf}};
+
+handle_msg({'EXIT', Group, _Reason}, #state{group = Group,
+ channel = ChannelId} = State) ->
+ {stop, ChannelId, State};
+
+handle_msg(_, State) ->
+ {ok, State}.
+
+%%--------------------------------------------------------------------
+%% Function: terminate(Reason, State) -> void()
+%% Description: Called when the channel process is trminated
+%%--------------------------------------------------------------------
+terminate(_Reason, _State) ->
+ ok.
+
+%%--------------------------------------------------------------------
+%%% Internal functions
+%%--------------------------------------------------------------------
+
+exec(Cmd) ->
+ eval(parse(scan(Cmd))).
+
+scan(Cmd) ->
+ erl_scan:string(Cmd).
+
+parse({ok, Tokens, _}) ->
+ erl_parse:parse_exprs(Tokens);
+parse(Error) ->
+ Error.
+
+eval({ok, Expr_list}) ->
+ case (catch erl_eval:exprs(Expr_list,
+ erl_eval:new_bindings())) of
+ {value, Value, _NewBindings} ->
+ {Value, 0};
+ {'EXIT', {Error, _}} ->
+ {Error, -1};
+ Error ->
+ {Error, -1}
+ end;
+eval(Error) ->
+ {Error, -1}.
+
+%%% io_request, handle io requests from the user process,
+%%% Note, this is not the real I/O-protocol, but the mockup version
+%%% used between edlin and a user_driver. The protocol tags are
+%%% similar, but the message set is different.
+%%% The protocol only exists internally between edlin and a character
+%%% displaying device...
+%%% We are *not* really unicode aware yet, we just filter away characters
+%%% beyond the latin1 range. We however handle the unicode binaries...
+io_request({window_change, OldTty}, Buf, Tty) ->
+ window_change(Tty, OldTty, Buf);
+io_request({put_chars, Cs}, Buf, Tty) ->
+ put_chars(bin_to_list(Cs), Buf, Tty);
+io_request({put_chars, unicode, Cs}, Buf, Tty) ->
+ put_chars([Ch || Ch <- unicode:characters_to_list(Cs,unicode), Ch =< 255], Buf, Tty);
+io_request({insert_chars, Cs}, Buf, Tty) ->
+ insert_chars(bin_to_list(Cs), Buf, Tty);
+io_request({insert_chars, unicode, Cs}, Buf, Tty) ->
+ insert_chars([Ch || Ch <- unicode:characters_to_list(Cs,unicode), Ch =< 255], Buf, Tty);
+io_request({move_rel, N}, Buf, Tty) ->
+ move_rel(N, Buf, Tty);
+io_request({delete_chars,N}, Buf, Tty) ->
+ delete_chars(N, Buf, Tty);
+io_request(beep, Buf, _Tty) ->
+ {[7], Buf};
+
+%% New in R12
+io_request({get_geometry,columns},Buf,Tty) ->
+ {ok, Tty#ssh_pty.width, Buf};
+io_request({get_geometry,rows},Buf,Tty) ->
+ {ok, Tty#ssh_pty.height, Buf};
+io_request({requests,Rs}, Buf, Tty) ->
+ io_requests(Rs, Buf, Tty, []);
+io_request(tty_geometry, Buf, Tty) ->
+ io_requests([{move_rel, 0}, {put_chars, unicode, [10]}], Buf, Tty, []);
+ %{[], Buf};
+io_request(_R, Buf, _Tty) ->
+ {[], Buf}.
+
+io_requests([R|Rs], Buf, Tty, Acc) ->
+ {Chars, NewBuf} = io_request(R, Buf, Tty),
+ io_requests(Rs, NewBuf, Tty, [Acc|Chars]);
+io_requests([], Buf, _Tty, Acc) ->
+ {Acc, Buf}.
+
+%%% return commands for cursor navigation, assume everything is ansi
+%%% (vt100), add clauses for other terminal types if needed
+ansi_tty(N, L) ->
+ ["\e[", integer_to_list(N), L].
+
+get_tty_command(up, N, _TerminalType) ->
+ ansi_tty(N, $A);
+get_tty_command(down, N, _TerminalType) ->
+ ansi_tty(N, $B);
+get_tty_command(right, N, _TerminalType) ->
+ ansi_tty(N, $C);
+get_tty_command(left, N, _TerminalType) ->
+ ansi_tty(N, $D).
+
+
+-define(PAD, 10).
+-define(TABWIDTH, 8).
+
+%% convert input characters to buffer and to writeout
+%% Note that the buf is reversed but the buftail is not
+%% (this is handy; the head is always next to the cursor)
+conv_buf([], AccBuf, AccBufTail, AccWrite, Col) ->
+ {AccBuf, AccBufTail, lists:reverse(AccWrite), Col};
+conv_buf([13, 10 | Rest], _AccBuf, AccBufTail, AccWrite, _Col) ->
+ conv_buf(Rest, [], tl2(AccBufTail), [10, 13 | AccWrite], 0);
+conv_buf([13 | Rest], _AccBuf, AccBufTail, AccWrite, _Col) ->
+ conv_buf(Rest, [], tl1(AccBufTail), [13 | AccWrite], 0);
+conv_buf([10 | Rest], _AccBuf, AccBufTail, AccWrite, _Col) ->
+ conv_buf(Rest, [], tl1(AccBufTail), [10, 13 | AccWrite], 0);
+conv_buf([C | Rest], AccBuf, AccBufTail, AccWrite, Col) ->
+ conv_buf(Rest, [C | AccBuf], tl1(AccBufTail), [C | AccWrite], Col + 1).
+
+
+%%% put characters at current position (possibly overwriting
+%%% characters after current position in buffer)
+put_chars(Chars, {Buf, BufTail, Col}, _Tty) ->
+ {NewBuf, NewBufTail, WriteBuf, NewCol} =
+ conv_buf(Chars, Buf, BufTail, [], Col),
+ {WriteBuf, {NewBuf, NewBufTail, NewCol}}.
+
+%%% insert character at current position
+insert_chars([], {Buf, BufTail, Col}, _Tty) ->
+ {[], {Buf, BufTail, Col}};
+insert_chars(Chars, {Buf, BufTail, Col}, Tty) ->
+ {NewBuf, _NewBufTail, WriteBuf, NewCol} =
+ conv_buf(Chars, Buf, [], [], Col),
+ M = move_cursor(NewCol + length(BufTail), NewCol, Tty),
+ {[WriteBuf, BufTail | M], {NewBuf, BufTail, NewCol}}.
+
+%%% delete characters at current position, (backwards if negative argument)
+delete_chars(0, {Buf, BufTail, Col}, _Tty) ->
+ {[], {Buf, BufTail, Col}};
+delete_chars(N, {Buf, BufTail, Col}, Tty) when N > 0 ->
+ NewBufTail = nthtail(N, BufTail),
+ M = move_cursor(Col + length(NewBufTail) + N, Col, Tty),
+ {[NewBufTail, lists:duplicate(N, $ ) | M],
+ {Buf, NewBufTail, Col}};
+delete_chars(N, {Buf, BufTail, Col}, Tty) -> % N < 0
+ NewBuf = nthtail(-N, Buf),
+ NewCol = Col + N,
+ M1 = move_cursor(Col, NewCol, Tty),
+ M2 = move_cursor(NewCol + length(BufTail) - N, NewCol, Tty),
+ {[M1, BufTail, lists:duplicate(-N, $ ) | M2],
+ {NewBuf, BufTail, NewCol}}.
+
+%%% Window change, redraw the current line (and clear out after it
+%%% if current window is wider than previous)
+window_change(Tty, OldTty, Buf)
+ when OldTty#ssh_pty.width == Tty#ssh_pty.width ->
+ {[], Buf};
+window_change(Tty, OldTty, {Buf, BufTail, Col}) ->
+ M1 = move_cursor(Col, 0, OldTty),
+ N = max(Tty#ssh_pty.width - OldTty#ssh_pty.width, 0) * 2,
+ S = lists:reverse(Buf, [BufTail | lists:duplicate(N, $ )]),
+ M2 = move_cursor(length(Buf) + length(BufTail) + N, Col, Tty),
+ {[M1, S | M2], {Buf, BufTail, Col}}.
+
+%% move around in buffer, respecting pad characters
+step_over(0, Buf, [?PAD | BufTail], Col) ->
+ {[?PAD | Buf], BufTail, Col+1};
+step_over(0, Buf, BufTail, Col) ->
+ {Buf, BufTail, Col};
+step_over(N, [C | Buf], BufTail, Col) when N < 0 ->
+ N1 = ifelse(C == ?PAD, N, N+1),
+ step_over(N1, Buf, [C | BufTail], Col-1);
+step_over(N, Buf, [C | BufTail], Col) when N > 0 ->
+ N1 = ifelse(C == ?PAD, N, N-1),
+ step_over(N1, [C | Buf], BufTail, Col+1).
+
+%%% an empty line buffer
+empty_buf() -> {[], [], 0}.
+
+%%% col and row from position with given width
+col(N, W) -> N rem W.
+row(N, W) -> N div W.
+
+%%% move relative N characters
+move_rel(N, {Buf, BufTail, Col}, Tty) ->
+ {NewBuf, NewBufTail, NewCol} = step_over(N, Buf, BufTail, Col),
+ M = move_cursor(Col, NewCol, Tty),
+ {M, {NewBuf, NewBufTail, NewCol}}.
+
+%%% give move command for tty
+move_cursor(A, A, _Tty) ->
+ [];
+move_cursor(From, To, #ssh_pty{width=Width, term=Type}) ->
+ Tcol = case col(To, Width) - col(From, Width) of
+ 0 -> "";
+ I when I < 0 -> get_tty_command(left, -I, Type);
+ I -> get_tty_command(right, I, Type)
+ end,
+ Trow = case row(To, Width) - row(From, Width) of
+ 0 -> "";
+ J when J < 0 -> get_tty_command(up, -J, Type);
+ J -> get_tty_command(down, J, Type)
+ end,
+ [Tcol | Trow].
+
+%% %%% write out characters
+%% %%% make sure that there is data to send
+%% %%% before calling ssh_connection:send
+write_chars(ConnectionManager, ChannelId, Chars) ->
+ case erlang:iolist_size(Chars) of
+ 0 ->
+ ok;
+ _ ->
+ ssh_connection:send(ConnectionManager, ChannelId,
+ ?SSH_EXTENDED_DATA_DEFAULT, Chars)
+ end.
+
+%%% tail, works with empty lists
+tl1([_|A]) -> A;
+tl1(_) -> [].
+
+%%% second tail
+tl2([_,_|A]) -> A;
+tl2(_) -> [].
+
+%%% nthtail as in lists, but no badarg if n > the length of list
+nthtail(0, A) -> A;
+nthtail(N, [_ | A]) when N > 0 -> nthtail(N-1, A);
+nthtail(_, _) -> [].
+
+%%% utils
+max(A, B) when A > B -> A;
+max(_A, B) -> B.
+
+ifelse(Cond, A, B) ->
+ case Cond of
+ true -> A;
+ _ -> B
+ end.
+
+bin_to_list(B) when is_binary(B) ->
+ binary_to_list(B);
+bin_to_list(L) when is_list(L) ->
+ lists:flatten([bin_to_list(A) || A <- L]);
+bin_to_list(I) when is_integer(I) ->
+ I.
+
+start_shell(ConnectionManager, State) ->
+ Shell = State#state.shell,
+ ShellFun = case is_function(Shell) of
+ true ->
+ case erlang:fun_info(Shell, arity) of
+ {arity, 1} ->
+ {ok, User} =
+ ssh_userreg:lookup_user(ConnectionManager),
+ fun() -> Shell(User) end;
+ {arity, 2} ->
+ {ok, User} =
+ ssh_userreg:lookup_user(ConnectionManager),
+ {ok, PeerAddr} =
+ ssh_cm:get_peer_addr(ConnectionManager),
+ fun() -> Shell(User, PeerAddr) end;
+ _ ->
+ Shell
+ end;
+ _ ->
+ Shell
+ end,
+ Echo = get_echo(State#state.pty),
+ Group = group:start(self(), ShellFun, [{echo, Echo}]),
+ State#state{group = Group, buf = empty_buf()}.
+
+start_shell(_ConnectionManager, Cmd, #state{exec={M, F, A}} = State) ->
+ Group = group:start(self(), {M, F, A++[Cmd]}, [{echo,false}]),
+ State#state{group = Group, buf = empty_buf()}.
+
+
+% Pty can be undefined if the client never sets any pty options before
+% starting the shell.
+get_echo(undefined) ->
+ true;
+get_echo(#ssh_pty{modes = Modes}) ->
+ case proplists:get_value(echo, Modes, 1) of
+ 0 ->
+ false;
+ _ ->
+ true
+ end.
+
+% Group is undefined if the pty options are sent between open and
+% shell messages.
+set_echo(#state{group = undefined}) ->
+ ok;
+set_echo(#state{group = Group, pty = Pty}) ->
+ Echo = get_echo(Pty),
+ Group ! {self(), echo, Echo}.
+
+not_zero(0, B) ->
+ B;
+not_zero(A, _) ->
+ A.
+
+%%% Backwards compatibility
+
+%%--------------------------------------------------------------------
+%% Function: listen(...) -> {ok,Pid} | ignore | {error,Error}
+%% Description: Starts a listening server
+%% Note that the pid returned is NOT the pid of this gen_server;
+%% this server is started when an SSH connection is made on the
+%% listening port
+%%--------------------------------------------------------------------
+listen(Shell) ->
+ listen(Shell, 22).
+
+listen(Shell, Port) ->
+ listen(Shell, Port, []).
+
+listen(Shell, Port, Opts) ->
+ listen(Shell, any, Port, Opts).
+
+listen(Shell, HostAddr, Port, Opts) ->
+ ssh:daemon(HostAddr, Port, [{shell, Shell} | Opts]).
+
+
+%%--------------------------------------------------------------------
+%% Function: stop(Pid) -> ok
+%% Description: Stops the listener
+%%--------------------------------------------------------------------
+stop(Pid) ->
+ ssh:stop_listener(Pid).