aboutsummaryrefslogtreecommitdiffstats
path: root/lib/ssh/src/ssh_sftpd.erl
diff options
context:
space:
mode:
authorErlang/OTP <[email protected]>2009-11-20 14:54:40 +0000
committerErlang/OTP <[email protected]>2009-11-20 14:54:40 +0000
commit84adefa331c4159d432d22840663c38f155cd4c1 (patch)
treebff9a9c66adda4df2106dfd0e5c053ab182a12bd /lib/ssh/src/ssh_sftpd.erl
downloadotp-84adefa331c4159d432d22840663c38f155cd4c1.tar.gz
otp-84adefa331c4159d432d22840663c38f155cd4c1.tar.bz2
otp-84adefa331c4159d432d22840663c38f155cd4c1.zip
The R13B03 release.OTP_R13B03
Diffstat (limited to 'lib/ssh/src/ssh_sftpd.erl')
-rw-r--r--lib/ssh/src/ssh_sftpd.erl932
1 files changed, 932 insertions, 0 deletions
diff --git a/lib/ssh/src/ssh_sftpd.erl b/lib/ssh/src/ssh_sftpd.erl
new file mode 100644
index 0000000000..10b8ede1e4
--- /dev/null
+++ b/lib/ssh/src/ssh_sftpd.erl
@@ -0,0 +1,932 @@
+%%
+%% %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: SFTP server daemon
+
+-module(ssh_sftpd).
+
+%%-behaviour(gen_server).
+-behaviour(ssh_channel).
+
+-include_lib("kernel/include/file.hrl").
+
+-include("ssh.hrl").
+-include("ssh_xfer.hrl").
+
+%%--------------------------------------------------------------------
+%% External exports
+-export([subsystem_spec/1,
+ listen/1, listen/2, listen/3, stop/1]).
+
+-export([init/1, handle_ssh_msg/2, handle_msg/2, terminate/2, code_change/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
+ handles % list of open handles
+ %% handle is either {<int>, directory, {Path, unread|eof}} or
+ %% {<int>, file, {Path, IoDevice}}
+ }).
+
+%%====================================================================
+%% API
+%%====================================================================
+subsystem_spec(Options) ->
+ {"sftp", {?MODULE, Options}}.
+
+%%% DEPRECATED START %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%--------------------------------------------------------------------
+%% Function: listen() -> Pid | {error,Error}
+%% Description: Starts the server
+%%--------------------------------------------------------------------
+listen(Port) ->
+ listen(any, Port, []).
+listen(Port, Options) ->
+ listen(any, Port, Options).
+listen(Addr, Port, Options) ->
+ SubSystems = [subsystem_spec(Options)],
+ ssh:daemon(Addr, Port, [{subsystems, SubSystems} |Options]).
+
+%%--------------------------------------------------------------------
+%% Function: stop(Pid) -> ok
+%% Description: Stops the listener
+%%--------------------------------------------------------------------
+stop(Pid) ->
+ ssh_cli:stop(Pid).
+
+
+%%% DEPRECATED END %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%====================================================================
+%% 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(vsn, Options, 5),
+
+ {ok, State#state{cwd = CWD, root = Root, max_files = MaxLength,
+ handles = [], pending = <<>>,
+ xf = #ssh_xfer{vsn = Vsn, ext = []}}}.
+
+
+%%--------------------------------------------------------------------
+%% Function: code_change(OldVsn, State, Extra) -> {ok, NewState}
+%% Description:
+%%--------------------------------------------------------------------
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+
+%%--------------------------------------------------------------------
+%% 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, _, 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_ssh_msg(Args) -> {ok, State} | {stop, ChannelId, State}
+%%
+%% Description: Handles other messages
+%%--------------------------------------------------------------------
+handle_msg({ssh_channel_up, ChannelId, ConnectionManager},
+ #state{xf =Xf} = State) ->
+ {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, <<?UINT32(Len), Msg:Len/binary, Rest/binary>>,
+ State = #state{pending = <<>>}) ->
+ <<Op, ?UINT32(ReqId), Data/binary>> = 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, <<Pending/binary, Data/binary>>,
+ 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, <<?UINT32(Vsn)>>),
+ State#state{xf = XF1};
+handle_op(?SSH_FXP_REALPATH, ReqId,
+ <<?UINT32(Rlen), RPath:Rlen/binary>>,
+ State0) ->
+ RelPath0 = binary_to_list(RPath),
+ RelPath = relate_file_name(RelPath0, State0, _Canonicalize=false),
+ {Res, State} = resolve_symlinks(RelPath, State0),
+ case Res of
+ {ok, AbsPath} ->
+ NewAbsPath = chroot_filename(AbsPath, State),
+ ?dbg(true, "handle_op ?SSH_FXP_REALPATH: RelPath=~p AbsPath=~p\n",
+ [RelPath, NewAbsPath]),
+ 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,
+ <<?UINT32(RLen), RPath:RLen/binary>>,
+ State0 = #state{file_handler = FileMod, file_state = FS0}) ->
+ RelPath = binary_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 ->
+ ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_NOT_A_DIRECTORY,
+ "Not a directory"),
+ State1;
+ true ->
+ add_handle(State1, XF, ReqId, directory, {RelPath,unread})
+ end;
+handle_op(?SSH_FXP_READDIR, ReqId,
+ <<?UINT32(HLen), BinHandle:HLen/binary>>,
+ 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,
+ <<?UINT32(HLen), BinHandle:HLen/binary>>,
+ 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, <<?UINT32(HLen), BinHandle:HLen/binary,
+ ?UINT64(Offset), ?UINT32(Len)>>,
+ 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,
+ <<?UINT32(HLen), BinHandle:HLen/binary, ?UINT64(Offset),
+ ?UINT32(Len), Data:Len/binary>>, 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, <<?UINT32(PLen), BPath:PLen/binary>>,
+ State = #state{file_handler = FileMod, file_state = FS0}) ->
+ RelPath = binary_to_list(BPath),
+ 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, <<?UINT32(PLen), BPath:PLen/binary,
+ Attr/binary>>, 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, <<?UINT32(PLen), BPath:PLen/binary,
+ Attr/binary>>,
+ 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, <<?UINT32(HLen), BinHandle:HLen/binary,
+ Attr/binary>>,
+ 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, <<?UINT32(PLen), BPath:PLen/binary>>,
+ State0 = #state{file_handler = FileMod, file_state = FS0}) ->
+ Path = relate_file_name(BPath, State0),
+ %% case FileMod:is_dir(Path) of %% This version 6 we still have ver 5
+ %% true ->
+ %% ssh_xfer:xf_send_status(State#state.xf, ReqId,
+ %% ?SSH_FX_FILE_IS_A_DIRECTORY);
+ %% 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, <<?UINT32(PLen), BPath:PLen/binary>>,
+ 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 = <<?UINT32(PLen), _:PLen/binary, ?UINT32(PLen2),
+ _:PLen2/binary>>,
+ State = #state{xf = #ssh_xfer{vsn = Vsn}}) when Vsn==3; Vsn==4 ->
+ handle_op(?SSH_FXP_RENAME, ReqId, <<Bin/binary, 0:32>>, State);
+
+handle_op(?SSH_FXP_RENAME, ReqId,
+ <<?UINT32(PLen), BPath:PLen/binary, ?UINT32(PLen2),
+ BPath2:PLen2/binary, ?UINT32(Flags)>>,
+ 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,
+ <<?UINT32(PLen), BPath:PLen/binary, ?UINT32(PLen2),
+ BPath2:PLen2/binary>>,
+ State0 = #state{file_handler = FileMod, file_state = FS0}) ->
+ Path = relate_file_name(BPath, State0),
+ Path2 = relate_file_name(BPath2, State0),
+ {Status, FS1} = FileMod:make_symlink(Path2, Path, 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)),
+ 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),
+ ?dbg(true, "read_dir: AbsPath=~p\n", [AbsPath]),
+ 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),
+ ?dbg(true, "read_dir: AbsPath=~p\n", [AbsPath]),
+ {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),
+ ?dbg(true, "get_attrs fun: F=~p\n", [F]),
+ 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) when Vsn =< 3->
+ <<?UINT32(BLen), BPath:BLen/binary>> = Data,
+ stat(ReqId, binary_to_list(BPath), State, F);
+stat(Vsn, ReqId, Data, State, F) when Vsn >= 4->
+ <<?UINT32(BLen), BPath:BLen/binary, ?UINT32(_Flags)>> = Data,
+ stat(ReqId, binary_to_list(BPath), State, F).
+
+fstat(Vsn, ReqId, Data, State) when Vsn =< 3->
+ <<?UINT32(HLen), Handle:HLen/binary>> = Data,
+ fstat(ReqId, Handle, State);
+fstat(Vsn, ReqId, Data, State) when Vsn >= 4->
+ <<?UINT32(HLen), Handle:HLen/binary, ?UINT32(_Flags)>> = 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,
+ ?dbg(false, "stat: AbsPath=~p\n", [AbsPath]),
+ {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.
+
+decode_4_open_flag(create_new) ->
+ [write];
+decode_4_open_flag(create_truncate) ->
+ [write];
+decode_4_open_flag(truncate_existing) ->
+ [write];
+decode_4_open_flag(open_existing) ->
+ [read].
+
+decode_4_flags([OpenFlag | Flags]) ->
+ decode_4_flags(Flags, decode_4_open_flag(OpenFlag)).
+
+decode_4_flags([], Flags) ->
+ Flags;
+decode_4_flags([append_data|R], _Flags) ->
+ decode_4_flags(R, [append]);
+decode_4_flags([append_data_atomic|R], _Flags) ->
+ decode_4_flags(R, [append]);
+decode_4_flags([_|R], Flags) ->
+ decode_4_flags(R, Flags).
+
+decode_4_access_flag(read_data) ->
+ [read];
+decode_4_access_flag(list_directory) ->
+ [read];
+decode_4_access_flag(write_data) ->
+ [write];
+decode_4_access_flag(add_file) ->
+ [write];
+decode_4_access_flag(add_subdirectory) ->
+ [read];
+decode_4_access_flag(append_data) ->
+ [append];
+decode_4_access_flag(_) ->
+ [read].
+
+decode_4_acess([_ | _] = Flags) ->
+ lists:map(fun(Flag) ->
+ [decode_4_access_flag(Flag)]
+ end, Flags);
+decode_4_acess([]) ->
+ [].
+
+open(Vsn, ReqId, Data, State) when Vsn =< 3 ->
+ <<?UINT32(BLen), BPath:BLen/binary, ?UINT32(PFlags),
+ _Attrs/binary>> = Data,
+ Path = binary_to_list(BPath),
+ Flags = ssh_xfer:decode_open_flags(Vsn, PFlags) -- [creat, excl, trunc],
+ ?dbg(true, "open: Flags=~p\n", [Flags]),
+ do_open(ReqId, State, Path, Flags);
+open(Vsn, ReqId, Data, State) when Vsn >= 4 ->
+ <<?UINT32(BLen), BPath:BLen/binary, ?UINT32(Access),
+ ?UINT32(PFlags), _Attrs/binary>> = Data,
+ Path = binary_to_list(BPath),
+ FlagBits = ssh_xfer:decode_open_flags(Vsn, PFlags),
+ AcessBits = ssh_xfer:decode_ace_mask(Access),
+ ?dbg(true, "open: Fl=~p\n", [FlagBits]),
+ %% TODO: This is to make sure the Access flags are not ignored
+ %% but this should be thought through better. This solution should
+ %% be considered a hack in order to buy some time. At least
+ %% it works better than when the Access flags where totally ignored.
+ %% A better solution may need some code refactoring that we do
+ %% not have time for right now.
+ AcessFlags = decode_4_acess(AcessBits),
+ Flags = lists:append(lists:umerge(
+ [[decode_4_flags(FlagBits)] | AcessFlags])),
+
+ ?dbg(true, "open: Flags=~p\n", [Flags]),
+
+ do_open(ReqId, State, Path, Flags).
+
+do_open(ReqId, State0, Path, Flags) ->
+ #state{file_handler = FileMod, file_state = FS0, root = Root} = State0,
+ XF = State0#state.xf,
+ F = [raw, binary | Flags],
+ %% case FileMod:is_dir(Path) of %% This is version 6 we still have 5
+ %% true ->
+ %% ssh_xfer:xf_send_status(State#state.xf, ReqId,
+ %% ?SSH_FX_FILE_IS_A_DIRECTORY);
+ %% false ->
+
+ AbsPath = case Root of
+ "" ->
+ Path;
+ _ ->
+ relate_file_name(Path, State0)
+ end,
+
+ {Res, FS1} = FileMod:open(AbsPath, F, FS0),
+ State1 = State0#state{file_state = FS1},
+ case Res of
+ {ok, IoDevice} ->
+ add_handle(State1, XF, ReqId, file, {Path,IoDevice});
+ {error, Error} ->
+ ssh_xfer:xf_send_status(State1#state.xf, ReqId,
+ ssh_xfer:encode_erlang_status(Error)),
+ State1
+ 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:reverse(tl(lists:reverse(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}.
+
+
+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(binary_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{root = Root}, Canonicalize) ->
+ case is_within_root(Root, File) of
+ true ->
+ File;
+ false ->
+ RelFile = make_relative_filename(File),
+ NewFile = relate_filename_to_path(RelFile, Root, Canonicalize),
+ case is_within_root(Root, NewFile) of
+ true ->
+ NewFile;
+ false ->
+ Root
+ end
+ 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),
+ ?dbg(true, "set_stat DecodedAttr=~p\n", [DecodedAttr]),
+ 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),
+ ?dbg(true, "set_stat Path=~p\nInfo=~p\nOldInfo=~p\nNewInfo=~p\n",
+ [Path, Info, OldInfo, NewInfo]),
+ {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).