%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2005-2018. 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% %% %% %%% Description: SFTP server daemon -module(ssh_sftpd). -behaviour(ssh_server_channel). -include_lib("kernel/include/file.hrl"). -include("ssh.hrl"). -include("ssh_xfer.hrl"). -include("ssh_connect.hrl"). %% For ?DEFAULT_PACKET_SIZE and ?DEFAULT_WINDOW_SIZE %%-------------------------------------------------------------------- %% External exports -export([subsystem_spec/1]). -export([init/1, handle_ssh_msg/2, handle_msg/2, terminate/2]). -export([dbg_trace/3]). -record(state, { xf, % [{channel,ssh_xfer states}...] cwd, % current dir (on first connect) root, % root dir remote_channel, % remote channel pending, % binary() file_handler, % atom() - callback module file_state, % state for the file callback module max_files, % integer >= 0 max no files sent during READDIR options, % from the subsystem declaration handles % list of open handles %% handle is either {, directory, {Path, unread|eof}} or %% {, file, {Path, IoDevice}} }). %%==================================================================== %% API %%==================================================================== -spec subsystem_spec(Options) -> Spec when Options :: [ {cwd, string()} | {file_handler, CallbackModule::string()} | {max_files, integer()} | {root, string()} | {sftpd_vsn, integer()} ], Spec :: {Name, {CbMod,Options}}, Name :: string(), CbMod :: atom() . subsystem_spec(Options) -> {"sftp", {?MODULE, Options}}. %%==================================================================== %% subsystem callbacks %%==================================================================== %%-------------------------------------------------------------------- %% Function: init(Args) -> {ok, State} %% Description: Initiates the server %%-------------------------------------------------------------------- init(Options) -> {FileMod, FS0} = case proplists:get_value(file_handler, Options, {ssh_sftpd_file,[]}) of {F, S} -> {F, S}; F -> {F, []} end, {{ok, Default}, FS1} = FileMod:get_cwd(FS0), CWD = proplists:get_value(cwd, Options, Default), Root0 = proplists:get_value(root, Options, ""), %% Get the root of the file system (symlinks must be followed, %% otherwise the realpath call won't work). But since symbolic links %% isn't supported on all plattforms we have to use the root property %% supplied by the user. {Root, State} = case resolve_symlinks(Root0, #state{root = Root0, file_handler = FileMod, file_state = FS1}) of {{ok, Root1}, State0} -> {Root1, State0}; {{error, _}, State0} -> {Root0, State0} end, MaxLength = proplists:get_value(max_files, Options, 0), Vsn = proplists:get_value(sftpd_vsn, Options, 5), {ok, State#state{cwd = CWD, root = Root, max_files = MaxLength, options = Options, handles = [], pending = <<>>, xf = #ssh_xfer{vsn = Vsn, ext = []}}}. %%-------------------------------------------------------------------- %% Function: handle_ssh_msg(Args) -> {ok, State} | {stop, ChannelId, State} %% %% Description: Handles channel messages %%-------------------------------------------------------------------- handle_ssh_msg({ssh_cm, _ConnectionManager, {data, _ChannelId, Type, Data}}, State) -> State1 = handle_data(Type, Data, State), {ok, State1}; handle_ssh_msg({ssh_cm, _, {eof, ChannelId}}, State) -> {stop, ChannelId, 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, Signal, Error, _}}, State) -> Report = io_lib:format("Connection closed by peer signal ~p~n Error ~p~n", [Signal,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 messages %%-------------------------------------------------------------------- handle_msg({ssh_channel_up, ChannelId, ConnectionManager}, #state{xf = Xf, options = Options} = State) -> maybe_increase_recv_window(ConnectionManager, ChannelId, Options), {ok, State#state{xf = Xf#ssh_xfer{cm = ConnectionManager, channel = ChannelId}}}. %%-------------------------------------------------------------------- %% Function: terminate(Reason, State) -> void() %% Description: This function is called by a gen_server when it is about to %% terminate. It should be the opposite of Module:init/1 and do any necessary %% cleaning up. When it returns, the gen_server terminates with Reason. %% The return value is ignored. %%-------------------------------------------------------------------- terminate(_, #state{handles=Handles, file_handler=FileMod, file_state=FS}) -> CloseFun = fun({_, file, {_, Fd}}, FS0) -> {_Res, FS1} = FileMod:close(Fd, FS0), FS1; (_, FS0) -> FS0 end, lists:foldl(CloseFun, FS, Handles), ok. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- handle_data(0, <>, State = #state{pending = <<>>}) -> <> = Msg, NewState = handle_op(Op, ReqId, Data, State), case Rest of <<>> -> NewState; _ -> handle_data(0, Rest, NewState) end; handle_data(0, Data, State = #state{pending = <<>>}) -> State#state{pending = Data}; handle_data(Type, Data, State = #state{pending = Pending}) -> handle_data(Type, <>, State#state{pending = <<>>}). handle_op(?SSH_FXP_INIT, Version, B, State) when is_binary(B) -> XF = State#state.xf, Vsn = lists:min([XF#ssh_xfer.vsn, Version]), XF1 = XF#ssh_xfer{vsn = Vsn}, ssh_xfer:xf_send_reply(XF1, ?SSH_FXP_VERSION, <>), State#state{xf = XF1}; handle_op(?SSH_FXP_REALPATH, ReqId, <>, State0) -> RelPath = relate_file_name(RPath, State0, _Canonicalize=false), {Res, State} = resolve_symlinks(RelPath, State0), case Res of {ok, AbsPath} -> NewAbsPath = chroot_filename(AbsPath, State), XF = State#state.xf, Attr = #ssh_xfer_attr{type=directory}, ssh_xfer:xf_send_name(XF, ReqId, NewAbsPath, Attr), State; {error, _} = Error -> send_status(Error, ReqId, State) end; handle_op(?SSH_FXP_OPENDIR, ReqId, <>, State0 = #state{xf = #ssh_xfer{vsn = Vsn}, file_handler = FileMod, file_state = FS0}) -> RelPath = unicode:characters_to_list(RPath), AbsPath = relate_file_name(RelPath, State0), XF = State0#state.xf, {IsDir, FS1} = FileMod:is_dir(AbsPath, FS0), State1 = State0#state{file_state = FS1}, case IsDir of false when Vsn > 5 -> ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_NOT_A_DIRECTORY, "Not a directory"), State1; false -> ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_FAILURE, "Not a directory"), State1; true -> add_handle(State1, XF, ReqId, directory, {RelPath,unread}) end; handle_op(?SSH_FXP_READDIR, ReqId, <>, State) -> XF = State#state.xf, case get_handle(State#state.handles, BinHandle) of {_Handle, directory, {_RelPath, eof}} -> ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_EOF), State; {Handle, directory, {RelPath, Status}} -> read_dir(State, XF, ReqId, Handle, RelPath, Status); _ -> ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_INVALID_HANDLE), State end; handle_op(?SSH_FXP_CLOSE, ReqId, <>, State = #state{handles = Handles, xf = XF, file_handler = FileMod, file_state = FS0}) -> case get_handle(Handles, BinHandle) of {Handle, Type, T} -> FS1 = case Type of file -> close_our_file(T, FileMod, FS0); _ -> FS0 end, ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_OK), State#state{handles = lists:keydelete(Handle, 1, Handles), file_state = FS1}; _ -> ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_INVALID_HANDLE), State end; handle_op(?SSH_FXP_LSTAT, ReqId, Data, State) -> stat((State#state.xf)#ssh_xfer.vsn, ReqId, Data, State, read_link_info); handle_op(?SSH_FXP_STAT, ReqId, Data, State) -> stat((State#state.xf)#ssh_xfer.vsn, ReqId, Data, State, read_file_info); handle_op(?SSH_FXP_FSTAT, ReqId, Data, State) -> fstat((State#state.xf)#ssh_xfer.vsn, ReqId, Data, State); handle_op(?SSH_FXP_OPEN, ReqId, Data, State) -> open((State#state.xf)#ssh_xfer.vsn, ReqId, Data, State); handle_op(?SSH_FXP_READ, ReqId, <>, State) -> case get_handle(State#state.handles, BinHandle) of {_Handle, file, {_Path, IoDevice}} -> read_file(ReqId, IoDevice, Offset, Len, State); _ -> ssh_xfer:xf_send_status(State#state.xf, ReqId, ?SSH_FX_INVALID_HANDLE), State end; handle_op(?SSH_FXP_WRITE, ReqId, <>, State) -> case get_handle(State#state.handles, BinHandle) of {_Handle, file, {_Path, IoDevice}} -> write_file(ReqId, IoDevice, Offset, Data, State); _ -> ssh_xfer:xf_send_status(State#state.xf, ReqId, ?SSH_FX_INVALID_HANDLE), State end; handle_op(?SSH_FXP_READLINK, ReqId, <>, State = #state{file_handler = FileMod, file_state = FS0}) -> AbsPath = relate_file_name(RelPath, State), {Res, FS1} = FileMod:read_link(AbsPath, FS0), case Res of {ok, NewPath} -> ssh_xfer:xf_send_name(State#state.xf, ReqId, NewPath, #ssh_xfer_attr{type=regular}); {error, Error} -> ssh_xfer:xf_send_status(State#state.xf, ReqId, ssh_xfer:encode_erlang_status(Error)) end, State#state{file_state = FS1}; handle_op(?SSH_FXP_SETSTAT, ReqId, <>, State0) -> Path = relate_file_name(BPath, State0), {Status, State1} = set_stat(Attr, Path, State0), send_status(Status, ReqId, State1); handle_op(?SSH_FXP_MKDIR, ReqId, <>, State0 = #state{file_handler = FileMod, file_state = FS0}) -> Path = relate_file_name(BPath, State0), {Res, FS1} = FileMod:make_dir(Path, FS0), State1 = State0#state{file_state = FS1}, case Res of ok -> {_, State2} = set_stat(Attr, Path, State1), send_status(ok, ReqId, State2); {error, Error} -> send_status({error, Error}, ReqId, State1) end; handle_op(?SSH_FXP_FSETSTAT, ReqId, <>, State0 = #state{handles = Handles}) -> case get_handle(Handles, BinHandle) of {_Handle, _Type, {Path,_}} -> {Status, State1} = set_stat(Attr, Path, State0), send_status(Status, ReqId, State1); _ -> ssh_xfer:xf_send_status(State0#state.xf, ReqId, ?SSH_FX_INVALID_HANDLE), State0 end; handle_op(?SSH_FXP_REMOVE, ReqId, <>, State0 = #state{file_handler = FileMod, file_state = FS0, xf = #ssh_xfer{vsn = Vsn}}) -> Path = relate_file_name(BPath, State0), {IsDir, _FS1} = FileMod:is_dir(Path, FS0), case IsDir of %% This version 6 we still have ver 5 true when Vsn > 5 -> ssh_xfer:xf_send_status(State0#state.xf, ReqId, ?SSH_FX_FILE_IS_A_DIRECTORY, "File is a directory"), State0; true -> ssh_xfer:xf_send_status(State0#state.xf, ReqId, ?SSH_FX_FAILURE, "File is a directory"), State0; false -> {Status, FS1} = FileMod:delete(Path, FS0), State1 = State0#state{file_state = FS1}, send_status(Status, ReqId, State1) end; handle_op(?SSH_FXP_RMDIR, ReqId, <>, State0 = #state{file_handler = FileMod, file_state = FS0}) -> Path = relate_file_name(BPath, State0), {Status, FS1} = FileMod:del_dir(Path, FS0), State1 = State0#state{file_state = FS1}, send_status(Status, ReqId, State1); handle_op(?SSH_FXP_RENAME, ReqId, Bin = <>, State = #state{xf = #ssh_xfer{vsn = Vsn}}) when Vsn==3; Vsn==4 -> handle_op(?SSH_FXP_RENAME, ReqId, <>, State); handle_op(?SSH_FXP_RENAME, ReqId, <>, State0 = #state{file_handler = FileMod, file_state = FS0}) -> Path = relate_file_name(BPath, State0), Path2 = relate_file_name(BPath2, State0), case Flags band ?SSH_FXP_RENAME_ATOMIC of 0 -> case Flags band ?SSH_FXP_RENAME_OVERWRITE of 0 -> {Res, FS1} = FileMod:read_link_info(Path2, FS0), State1 = State0#state{file_state = FS1}, case Res of {ok, _Info} -> ssh_xfer:xf_send_status( State1#state.xf, ReqId, ?SSH_FX_FILE_ALREADY_EXISTS), State1; _ -> rename(Path, Path2, ReqId, State1) end; _ -> rename(Path, Path2, ReqId, State0) end; _ -> ssh_xfer:xf_send_status(State0#state.xf, ReqId, ?SSH_FX_OP_UNSUPPORTED), State0 end; handle_op(?SSH_FXP_SYMLINK, ReqId, <>, State0 = #state{file_handler = FileMod, file_state = FS0}) -> LinkPath = relate_file_name(Link, State0), TargetPath = relate_file_name(Target, State0), {Status, FS1} = FileMod:make_symlink(TargetPath, LinkPath, FS0), State1 = State0#state{file_state = FS1}, send_status(Status, ReqId, State1). new_handle([], H) -> H; new_handle([{N, _,_} | Rest], H) when N =< H -> new_handle(Rest, N+1); new_handle([_ | Rest], H) -> new_handle(Rest, H). add_handle(State, XF, ReqId, Type, DirFileTuple) -> Handles = State#state.handles, Handle = new_handle(Handles, 0), ssh_xfer:xf_send_handle(XF, ReqId, integer_to_list(Handle)), %% OBS: If you change handles-tuple also change new_handle! %% Is this this the best way to implement new handle? State#state{handles = [{Handle, Type, DirFileTuple} | Handles]}. get_handle(Handles, BinHandle) -> case (catch list_to_integer(binary_to_list(BinHandle))) of I when is_integer(I) -> case lists:keysearch(I, 1, Handles) of {value, T} -> T; false -> error end; _ -> error end. %%% read_dir/5: read directory, send names, and return new state read_dir(State0 = #state{file_handler = FileMod, max_files = MaxLength, file_state = FS0}, XF, ReqId, Handle, RelPath, {cache, Files}) -> AbsPath = relate_file_name(RelPath, State0), if length(Files) > MaxLength -> {ToSend, NewCache} = lists:split(MaxLength, Files), {NamesAndAttrs, FS1} = get_attrs(AbsPath, ToSend, FileMod, FS0), ssh_xfer:xf_send_names(XF, ReqId, NamesAndAttrs), Handles = lists:keyreplace(Handle, 1, State0#state.handles, {Handle, directory, {RelPath,{cache, NewCache}}}), State0#state{handles = Handles, file_state = FS1}; true -> {NamesAndAttrs, FS1} = get_attrs(AbsPath, Files, FileMod, FS0), ssh_xfer:xf_send_names(XF, ReqId, NamesAndAttrs), Handles = lists:keyreplace(Handle, 1, State0#state.handles, {Handle, directory, {RelPath,eof}}), State0#state{handles = Handles, file_state = FS1} end; read_dir(State0 = #state{file_handler = FileMod, max_files = MaxLength, file_state = FS0}, XF, ReqId, Handle, RelPath, _Status) -> AbsPath = relate_file_name(RelPath, State0), {Res, FS1} = FileMod:list_dir(AbsPath, FS0), case Res of {ok, Files} when MaxLength == 0 orelse MaxLength > length(Files) -> {NamesAndAttrs, FS2} = get_attrs(AbsPath, Files, FileMod, FS1), ssh_xfer:xf_send_names(XF, ReqId, NamesAndAttrs), Handles = lists:keyreplace(Handle, 1, State0#state.handles, {Handle, directory, {RelPath,eof}}), State0#state{handles = Handles, file_state = FS2}; {ok, Files} -> {ToSend, Cache} = lists:split(MaxLength, Files), {NamesAndAttrs, FS2} = get_attrs(AbsPath, ToSend, FileMod, FS1), ssh_xfer:xf_send_names(XF, ReqId, NamesAndAttrs), Handles = lists:keyreplace(Handle, 1, State0#state.handles, {Handle, directory, {RelPath,{cache, Cache}}}), State0#state{handles = Handles, file_state = FS2}; {error, Error} -> State1 = State0#state{file_state = FS1}, send_status({error, Error}, ReqId, State1) end. %%% get_attrs: get stat of each file and return get_attrs(RelPath, Files, FileMod, FS) -> get_attrs(RelPath, Files, FileMod, FS, []). get_attrs(_RelPath, [], _FileMod, FS, Acc) -> {lists:reverse(Acc), FS}; get_attrs(RelPath, [F | Rest], FileMod, FS0, Acc) -> Path = filename:absname(F, RelPath), case FileMod:read_link_info(Path, FS0) of {{ok, Info}, FS1} -> Attrs = ssh_sftp:info_to_attr(Info), get_attrs(RelPath, Rest, FileMod, FS1, [{F, Attrs} | Acc]); {{error, enoent}, FS1} -> get_attrs(RelPath, Rest, FileMod, FS1, Acc); {Error, FS1} -> {Error, FS1} end. close_our_file({_,Fd}, FileMod, FS0) -> {_Res, FS1} = FileMod:close(Fd, FS0), FS1. %%% stat: do the stat stat(_Vsn, ReqId, Data, State, F) -> <> = Data, stat(ReqId, unicode:characters_to_list(BPath), State, F). fstat(Vsn, ReqId, Data, State) when Vsn =< 3-> <> = Data, fstat(ReqId, Handle, State); fstat(Vsn, ReqId, Data, State) when Vsn >= 4-> <> = Data, fstat(ReqId, Handle, State). fstat(ReqId, BinHandle, State) -> case get_handle(State#state.handles, BinHandle) of {_Handle, _Type, {Path, _}} -> stat(ReqId, Path, State, read_file_info); _ -> ssh_xfer:xf_send_status(State#state.xf, ReqId, ?SSH_FX_INVALID_HANDLE), State end. stat(ReqId, RelPath, State0=#state{file_handler=FileMod, file_state=FS0}, F) -> AbsPath = relate_file_name(RelPath, State0), XF = State0#state.xf, {Res, FS1} = FileMod:F(AbsPath, FS0), State1 = State0#state{file_state = FS1}, case Res of {ok, FileInfo} -> ssh_xfer:xf_send_attr(XF, ReqId, ssh_sftp:info_to_attr(FileInfo)), State1; {error, E} -> send_status({error, E}, ReqId, State1) end. sftp_to_erlang_flag(read, Vsn) when Vsn == 3; Vsn == 4 -> read; sftp_to_erlang_flag(write, Vsn) when Vsn == 3; Vsn == 4 -> write; sftp_to_erlang_flag(append, Vsn) when Vsn == 3; Vsn == 4 -> append; sftp_to_erlang_flag(creat, Vsn) when Vsn == 3; Vsn == 4 -> write; sftp_to_erlang_flag(trunc, Vsn) when Vsn == 3; Vsn == 4 -> write; sftp_to_erlang_flag(excl, Vsn) when Vsn == 3; Vsn == 4 -> read; sftp_to_erlang_flag(create_new, Vsn) when Vsn > 4 -> write; sftp_to_erlang_flag(create_truncate, Vsn) when Vsn > 4 -> write; sftp_to_erlang_flag(open_existing, Vsn) when Vsn > 4 -> read; sftp_to_erlang_flag(open_or_create, Vsn) when Vsn > 4 -> write; sftp_to_erlang_flag(truncate_existing, Vsn) when Vsn > 4 -> write; sftp_to_erlang_flag(append_data, Vsn) when Vsn > 4 -> append; sftp_to_erlang_flag(append_data_atomic, Vsn) when Vsn > 4 -> append; sftp_to_erlang_flag(_, _) -> read. sftp_to_erlang_flags(Flags, Vsn) -> lists:map(fun(Flag) -> sftp_to_erlang_flag(Flag, Vsn) end, Flags). sftp_to_erlang_access_flag(read_data, _) -> read; sftp_to_erlang_access_flag(list_directory, _) -> read; sftp_to_erlang_access_flag(write_data, _) -> write; sftp_to_erlang_access_flag(append_data, _) -> append; sftp_to_erlang_access_flag(add_subdirectory, _) -> read; sftp_to_erlang_access_flag(add_file, _) -> write; sftp_to_erlang_access_flag(write_attributes, _) -> write; sftp_to_erlang_access_flag(_, _) -> read. sftp_to_erlang_access_flags(Flags, Vsn) -> lists:map(fun(Flag) -> sftp_to_erlang_access_flag(Flag, Vsn) end, Flags). open(Vsn, ReqId, Data, State) when Vsn =< 3 -> <> = Data, Path = unicode:characters_to_list(BPath), FlagBits = ssh_xfer:decode_open_flags(Vsn, PFlags), Flags = lists:usort(sftp_to_erlang_flags(FlagBits, Vsn)), do_open(ReqId, State, Path, Flags); open(Vsn, ReqId, Data, State) when Vsn >= 4 -> <> = Data, Path = unicode:characters_to_list(BPath), FlagBits = ssh_xfer:decode_open_flags(Vsn, PFlags), AcessBits = ssh_xfer:decode_ace_mask(Access), %% TODO: There are still flags that are not %% fully handled as SSH_FXF_ACCESS_TEXT_MODE and %% a lot a ACE flags, the later we may not need %% to understand as they are NFS flags AcessFlags = sftp_to_erlang_access_flags(AcessBits, Vsn), Flags = lists:usort(sftp_to_erlang_flags(FlagBits, Vsn) ++ AcessFlags), do_open(ReqId, State, Path, Flags). do_open(ReqId, State0, Path, Flags) -> #state{file_handler = FileMod, file_state = FS0, xf = #ssh_xfer{vsn = Vsn}} = State0, AbsPath = relate_file_name(Path, State0), {IsDir, _FS1} = FileMod:is_dir(AbsPath, FS0), case IsDir of true when Vsn > 5 -> ssh_xfer:xf_send_status(State0#state.xf, ReqId, ?SSH_FX_FILE_IS_A_DIRECTORY, "File is a directory"), State0; true -> ssh_xfer:xf_send_status(State0#state.xf, ReqId, ?SSH_FX_FAILURE, "File is a directory"), State0; false -> OpenFlags = [binary | Flags], {Res, FS1} = FileMod:open(AbsPath, OpenFlags, FS0), State1 = State0#state{file_state = FS1}, case Res of {ok, IoDevice} -> add_handle(State1, State0#state.xf, ReqId, file, {Path,IoDevice}); {error, Error} -> ssh_xfer:xf_send_status(State1#state.xf, ReqId, ssh_xfer:encode_erlang_status(Error)), State1 end end. %% resolve all symlinks in a path resolve_symlinks(Path, State) -> resolve_symlinks(Path, _LinkCnt=32, State). resolve_symlinks(Path, LinkCnt, State0) -> resolve_symlinks_2(filename:split(Path), State0, LinkCnt, []). resolve_symlinks_2(_Path, State, LinkCnt, _AccPath) when LinkCnt =:= 0 -> %% Too many links (there might be a symlink loop) {{error, emlink}, State}; resolve_symlinks_2(["." | RestPath], State0, LinkCnt, AccPath) -> resolve_symlinks_2(RestPath, State0, LinkCnt, AccPath); resolve_symlinks_2([".." | RestPath], State0, LinkCnt, AccPath) -> %% Remove the last path component AccPathComps0 = filename:split(AccPath), Path = case lists:droplast(AccPathComps0) of [] -> ""; AccPathComps -> filename:join(AccPathComps) end, resolve_symlinks_2(RestPath, State0, LinkCnt, Path); resolve_symlinks_2([PathComp | RestPath], State0, LinkCnt, AccPath0) -> #state{file_handler = FileMod, file_state = FS0} = State0, AccPath1 = filename:join(AccPath0, PathComp), {Res, FS1} = FileMod:read_link(AccPath1, FS0), State1 = State0#state{file_state = FS1}, case Res of {ok, Target0} -> % path is a symlink %% The target may be a relative or an absolute path and %% may contain symlinks Target1 = filename:absname(Target0, AccPath0), {FollowRes, State2} = resolve_symlinks(Target1, LinkCnt-1, State1), case FollowRes of {ok, Target} -> resolve_symlinks_2(RestPath, State2, LinkCnt-1, Target); {error, _} = Error -> {Error, State2} end; {error, einval} -> % path exists, but is not a symlink resolve_symlinks_2(RestPath, State1, LinkCnt, AccPath1); {error, _} = Error -> {Error, State1} end; resolve_symlinks_2([], State, _LinkCnt, AccPath) -> {{ok, AccPath}, State}. %% The File argument is always in a user visible file system, i.e. %% is under Root and is relative to CWD or Root, if starts with "/". %% The result of the function is always an absolute path in a %% "backend" file system. relate_file_name(File, State) -> relate_file_name(File, State, _Canonicalize=true). relate_file_name(File, State, Canonicalize) when is_binary(File) -> relate_file_name(unicode:characters_to_list(File), State, Canonicalize); relate_file_name(File, #state{cwd = CWD, root = ""}, Canonicalize) -> relate_filename_to_path(File, CWD, Canonicalize); relate_file_name(File, #state{cwd = CWD, root = Root}, Canonicalize) -> CWD1 = case is_within_root(Root, CWD) of true -> CWD; false -> Root end, AbsFile = case make_relative_filename(File) of File -> relate_filename_to_path(File, CWD1, Canonicalize); RelFile -> relate_filename_to_path(RelFile, Root, Canonicalize) end, case is_within_root(Root, AbsFile) of true -> AbsFile; false -> Root end. is_within_root(Root, File) -> lists:prefix(Root, File). %% Remove leading slash (/), if any, in order to make the filename %% relative (to the root) make_relative_filename("/") -> "./"; % Make it relative and preserve / make_relative_filename("/"++File) -> File; make_relative_filename(File) -> File. relate_filename_to_path(File0, Path, Canonicalize) -> File1 = filename:absname(File0, Path), File2 = if Canonicalize -> canonicalize_filename(File1); true -> File1 end, ensure_trailing_slash_is_preserved(File0, File2). %% It seems as if the openssh client (observed with the %% openssh-4.2p1-18.30 package on SLED 10), and possibly other clients %% as well (Maverick?), rely on the fact that a trailing slash (/) is %% preserved. If trailing slashes aren't preserved, symlinks which %% point at directories won't be properly identified as directories. %% %% A failing example: %% %% 1) assume the following directory structure: %% $ mkdir /tmp/symlink-target %% $ touch /tmp/symlink-target/foo %% $ ln -s /tmp/symlink-target /tmp/symlink %% %% 2) login using the sftp client in openssh %% sftp> cd /tmp/ %% sftp> ls symlink-target %% symlink-target/foo %% sftp> ls symlink %% symlink/ <===== foo should have been visible here %% sftp> cd symlink-target %% sftp> ls %% foo %% sftp> cd .. %% sftp> cd symlink %% sftp> ls %% <===== foo should have been visible here %% %% The symlinks are resolved by file:read_link_info/1 only if the path %% has a trailing slash, which seems to something that some of the %% sftp clients utilize: %% %% 1> file:read_link_info(".../symlink"). %% {ok,{file_info,4,symlink,read_write, %% {{2008,10,20},{10,25,26}}, %% {{2008,10,17},{16,22,33}}, %% {{2008,10,17},{16,22,33}}, %% 41471,1,2053,0,570447,20996,9935}} %% %% 2> file:read_link_info(".../symlink/"). %% {ok,{file_info,8192,directory,read_write, %% {{2008,10,20},{10,36,2}}, %% {{2008,10,20},{10,44,35}}, %% {{2008,10,20},{10,44,35}}, %% 17407,29,2053,0,521224,0,0}} ensure_trailing_slash_is_preserved(File0, File1) -> case {lists:suffix("/", File0), lists:suffix("/", File1)} of {true, false} -> File1 ++ "/"; _Other -> File1 end. %%% fix file just a little: a/b/.. -> a and a/. -> a canonicalize_filename(File0) -> File = filename:join(canonicalize_filename_2(filename:split(File0), [])), ensure_trailing_slash_is_preserved(File0, File). canonicalize_filename_2([".." | Rest], ["/"] = Acc) -> canonicalize_filename_2(Rest, Acc); canonicalize_filename_2([".." | Rest], [_Dir | Paths]) -> canonicalize_filename_2(Rest, Paths); canonicalize_filename_2(["." | Rest], Acc) -> canonicalize_filename_2(Rest, Acc); canonicalize_filename_2([A | Rest], Acc) -> canonicalize_filename_2(Rest, [A | Acc]); canonicalize_filename_2([], Acc) -> lists:reverse(Acc). %% return a filename which is relative to the root directory %% (any filename that's outside the root directory is forced to the root) chroot_filename(Filename, #state{root = Root}) -> FilenameComps0 = filename:split(Filename), RootComps = filename:split(Root), filename:join(chroot_filename_2(FilenameComps0, RootComps)). chroot_filename_2([PathComp | FilenameRest], [PathComp | RootRest]) -> chroot_filename_2(FilenameRest, RootRest); chroot_filename_2(FilenameComps, []) when length(FilenameComps) > 0 -> %% Ensure there's a leading / (filename:join above will take care %% of any duplicates) ["/" | FilenameComps]; chroot_filename_2(_FilenameComps, _RootComps) -> %% The filename is either outside the root or at the root. In %% both cases we want to force the filename to the root. ["/"]. read_file(ReqId, IoDevice, Offset, Len, State0 = #state{file_handler = FileMod, file_state = FS0}) -> {Res1, FS1} = FileMod:position(IoDevice, {bof, Offset}, FS0), case Res1 of {ok, _NewPos} -> {Res2, FS2} = FileMod:read(IoDevice, Len, FS1), State1 = State0#state{file_state = FS2}, case Res2 of {ok, Data} -> ssh_xfer:xf_send_data(State1#state.xf, ReqId, Data), State1; {error, Error} -> send_status({error, Error}, ReqId, State1); eof -> send_status(eof, ReqId, State1) end; {error, Error} -> State1 = State0#state{file_state = FS1}, send_status({error, Error}, ReqId, State1) end. write_file(ReqId, IoDevice, Offset, Data, State0 = #state{file_handler = FileMod, file_state = FS0}) -> {Res, FS1} = FileMod:position(IoDevice, {bof, Offset}, FS0), case Res of {ok, _NewPos} -> {Status, FS2} = FileMod:write(IoDevice, Data, FS1), State1 = State0#state{file_state = FS2}, send_status(Status, ReqId, State1); {error, Error} -> State1 = State0#state{file_state = FS1}, send_status({error, Error}, ReqId, State1) end. get_status(ok) -> ?SSH_FX_OK; get_status(eof) -> ?SSH_FX_EOF; get_status({error,Error}) -> ssh_xfer:encode_erlang_status(Error). send_status(Status, ReqId, State) -> ssh_xfer:xf_send_status(State#state.xf, ReqId, get_status(Status)), State. set_stat(<<>>, _Path, State) -> {ok, State}; set_stat(Attr, Path, State0 = #state{file_handler=FileMod, file_state=FS0}) -> {DecodedAttr, _Rest} = ssh_xfer:decode_ATTR((State0#state.xf)#ssh_xfer.vsn, Attr), Info = ssh_sftp:attr_to_info(DecodedAttr), {Res1, FS1} = FileMod:read_link_info(Path, FS0), case Res1 of {ok, OldInfo} -> NewInfo = set_file_info(Info, OldInfo), {Res2, FS2} = FileMod:write_file_info(Path, NewInfo, FS1), State1 = State0#state{file_state = FS2}, {Res2, State1}; {error, Error} -> State1 = State0#state{file_state = FS1}, {{error, Error}, State1} end. set_file_info_sel(undefined, F) -> F; set_file_info_sel(F, _) -> F. set_file_info(#file_info{atime = Dst_atime, mtime = Dst_mtime, ctime = Dst_ctime, mode = Dst_mode, uid = Dst_uid, gid = Dst_gid}, #file_info{atime = Src_atime, mtime = Src_mtime, ctime = Src_ctime, mode = Src_mode, uid = Src_uid, gid = Src_gid}) -> #file_info{atime = set_file_info_sel(Dst_atime, Src_atime), mtime = set_file_info_sel(Dst_mtime, Src_mtime), ctime = set_file_info_sel(Dst_ctime, Src_ctime), mode = set_file_info_sel(Dst_mode, Src_mode), uid = set_file_info_sel(Dst_uid, Src_uid), gid = set_file_info_sel(Dst_gid, Src_gid)}. rename(Path, Path2, ReqId, State0) -> #state{file_handler = FileMod, file_state = FS0} = State0, {Status, FS1} = FileMod:rename(Path, Path2, FS0), State1 = State0#state{file_state = FS1}, send_status(Status, ReqId, State1). maybe_increase_recv_window(ConnectionManager, ChannelId, Options) -> WantedRecvWindowSize = proplists:get_value(recv_window_size, Options, 1000000), NumPkts = WantedRecvWindowSize div ?DEFAULT_PACKET_SIZE, Increment = NumPkts*?DEFAULT_PACKET_SIZE - ?DEFAULT_WINDOW_SIZE, if Increment > 0 -> ssh_connection:adjust_window(ConnectionManager, ChannelId, Increment); Increment =< 0 -> do_nothing end. %%%################################################################ %%%# %%%# Tracing %%%# dbg_trace(points, _, _) -> [terminate]; dbg_trace(flags, terminate, _) -> [c]; dbg_trace(on, terminate, _) -> dbg:tp(?MODULE, terminate, 2, x); dbg_trace(off, terminate, _) -> dbg:ctpg(?MODULE, terminate, 2); dbg_trace(format, terminate, {call, {?MODULE,terminate, [Reason, State]}}) -> ["SftpD Terminating:\n", io_lib:format("Reason: ~p,~nState:~n~s", [Reason, wr_record(State)]) ]. ?wr_record(state).