2
%% <year>2004-2007</year>
3
%% <holder>Ericsson AB, All Rights Reserved</holder>
4
%% Copyright Ericsson AB 2005-2009. All Rights Reserved.
6
6
%% The contents of this file are subject to the Erlang Public License,
7
7
%% Version 1.1, (the "License"); you may not use this file except in
8
8
%% compliance with the License. You should have received a copy of the
9
9
%% Erlang Public License along with this software. If not, it can be
10
10
%% retrieved online at http://www.erlang.org/.
12
12
%% Software distributed under the License is distributed on an "AS IS"
13
13
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
14
14
%% the License for the specific language governing rights and limitations
15
15
%% under the License.
17
%% The Initial Developer of the Original Code is Ericsson AB.
21
22
%%% Description: SFTP server daemon
23
24
-module(ssh_sftpd).
25
-behaviour(gen_server).
26
%%-behaviour(gen_server).
27
-behaviour(ssh_channel).
27
%%--------------------------------------------------------------------
29
%%--------------------------------------------------------------------
30
29
-include_lib("kernel/include/file.hrl").
32
31
-include("ssh.hrl").
33
32
-include("ssh_xfer.hrl").
35
-define(DEFAULT_TIMEOUT, 5000).
37
34
%%--------------------------------------------------------------------
38
35
%% External exports
39
36
-export([subsystem_spec/1,
40
37
listen/1, listen/2, listen/3, stop/1]).
42
%% gen_server callbacks
43
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
44
terminate/2, code_change/3]).
39
-export([init/1, handle_ssh_msg/2, handle_msg/2, terminate/2, code_change/3]).
47
42
xf, % [{channel,ssh_xfer states}...]
111
102
{{ok, Default}, FS1} = FileMod:get_cwd(FS0),
112
103
CWD = proplists:get_value(cwd, Options, Default),
113
Root = proplists:get_value(root, Options, ""),
114
State = #state{cwd = CWD, root = Root, handles = [], pending = <<>>,
115
file_handler = FileMod, file_state = FS1},
105
Root0 = proplists:get_value(root, Options, ""),
107
%% Get the root of the file system (symlinks must be followed,
108
%% otherwise the realpath call won't work). But since symbolic links
109
%% isn't supported on all plattforms we have to use the root property
110
%% supplied by the user.
112
case resolve_symlinks(Root0,
114
file_handler = FileMod,
115
file_state = FS1}) of
116
{{ok, Root1}, State0} ->
118
{{error, _}, State0} ->
121
MaxLength = proplists:get_value(max_files, Options, 0),
123
Vsn = proplists:get_value(vsn, Options, 5),
125
{ok, State#state{cwd = CWD, root = Root, max_files = MaxLength,
126
handles = [], pending = <<>>,
127
xf = #ssh_xfer{vsn = Vsn, ext = []}}}.
130
%%--------------------------------------------------------------------
131
%% Function: code_change(OldVsn, State, Extra) -> {ok, NewState}
133
%%--------------------------------------------------------------------
134
code_change(_OldVsn, State, _Extra) ->
118
%%--------------------------------------------------------------------
119
%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
120
%% {reply, Reply, State, Timeout} |
121
%% {noreply, State} |
122
%% {noreply, State, Timeout} |
123
%% {stop, Reason, Reply, State} |
124
%% {stop, Reason, State}
125
%% Description: Handling call messages
126
%%--------------------------------------------------------------------
127
handle_call(_Request, _From, State) ->
129
{reply, Reply, State}.
131
%%--------------------------------------------------------------------
132
%% Function: handle_cast(Msg, State) -> {noreply, State} |
133
%% {noreply, State, Timeout} |
134
%% {stop, Reason, State}
135
%% Description: Handling cast messages
136
%%--------------------------------------------------------------------
137
handle_cast(_Msg, State) ->
140
%%--------------------------------------------------------------------
141
%% Function: handle_info(Info, State) -> {noreply, State} |
142
%% {noreply, State, Timeout} |
143
%% {stop, Reason, State}
144
%% Description: Handling all non call/cast messages
145
%%--------------------------------------------------------------------
147
handle_info({ssh_cm, CM, {open, Channel, RemoteChannel, _Type}}, State) ->
148
XF = #ssh_xfer{vsn = 5, ext = [], cm = CM, channel = Channel},
149
State1 = State#state{xf = XF, remote_channel = RemoteChannel},
151
handle_info({ssh_cm, CM, {data, Channel, Type, Data}}, State) ->
152
ssh_connection:adjust_window(CM, Channel, size(Data)),
138
%%--------------------------------------------------------------------
139
%% Function: handle_ssh_msg(Args) -> {ok, State} | {stop, ChannelId, State}
141
%% Description: Handles channel messages
142
%%--------------------------------------------------------------------
143
handle_ssh_msg({ssh_cm, _ConnectionManager,
144
{data, _ChannelId, Type, Data}}, State) ->
153
145
State1 = handle_data(Type, Data, State),
156
handle_info({ssh_cm, CM, {subsystem, _, WantReply, "sftp"}},
157
#state{remote_channel = ChannelId} = State) ->
158
ssh_connection:reply_request(CM, WantReply, success, ChannelId),
161
%% The client has terminated the session
162
%% TODO: why check channel in xf ssh_xfer?
163
handle_info({ssh_cm, _, {eof, Channel}},
164
State = #state{xf = #ssh_xfer{channel = Channel}}) ->
165
{stop, normal, State};
167
handle_info({ssh_cm, _CM, {closed, _Channel}}, State) ->
168
%% ignore -- we'll get an {eof, Channel} soon??
171
handle_info(_Info, State) ->
172
?dbg(true, "handle_info: Info=~p State=~p\n", [_Info, State]),
148
handle_ssh_msg({ssh_cm, _, {eof, ChannelId}}, State) ->
149
{stop, ChannelId, State};
151
handle_ssh_msg({ssh_cm, _, {signal, _, _}}, State) ->
152
%% Ignore signals according to RFC 4254 section 6.9.
155
handle_ssh_msg({ssh_cm, _, {exit_signal, ChannelId, _, Error, _}}, State) ->
156
Report = io_lib:format("Connection closed by peer ~n Error ~p~n",
158
error_logger:error_report(Report),
159
{stop, ChannelId, State};
161
handle_ssh_msg({ssh_cm, _, {exit_status, ChannelId, 0}}, State) ->
162
{stop, ChannelId, State};
164
handle_ssh_msg({ssh_cm, _, {exit_status, ChannelId, Status}}, State) ->
166
Report = io_lib:format("Connection closed by peer ~n Status ~p~n",
168
error_logger:error_report(Report),
169
{stop, ChannelId, State}.
171
%%--------------------------------------------------------------------
172
%% Function: handle_ssh_msg(Args) -> {ok, State} | {stop, ChannelId, State}
174
%% Description: Handles other messages
175
%%--------------------------------------------------------------------
176
handle_msg({ssh_channel_up, ChannelId, ConnectionManager},
177
#state{xf =Xf} = State) ->
178
{ok, State#state{xf = Xf#ssh_xfer{cm = ConnectionManager,
179
channel = ChannelId}}}.
181
%%--------------------------------------------------------------------
182
%% Function: terminate(Reason, State) -> void()
183
%% Description: This function is called by a gen_server when it is about to
184
%% terminate. It should be the opposite of Module:init/1 and do any necessary
185
%% cleaning up. When it returns, the gen_server terminates with Reason.
186
%% The return value is ignored.
187
%%--------------------------------------------------------------------
188
terminate(_, #state{handles=Handles, file_handler=FileMod, file_state=FS}) ->
189
CloseFun = fun({_, file, {_, Fd}}, FS0) ->
190
{_Res, FS1} = FileMod:close(Fd, FS0),
195
lists:foldl(CloseFun, FS, Handles),
198
%%--------------------------------------------------------------------
199
%%% Internal functions
200
%%--------------------------------------------------------------------
175
201
handle_data(0, <<?UINT32(Len), Msg:Len/binary, Rest/binary>>,
176
202
State = #state{pending = <<>>}) ->
177
203
<<Op, ?UINT32(ReqId), Data/binary>> = Msg,
202
224
State#state{xf = XF1};
203
225
handle_op(?SSH_FXP_REALPATH, ReqId,
204
226
<<?UINT32(Rlen), RPath:Rlen/binary>>,
205
#state{root = Root} = State) ->
206
RelPath = binary_to_list(RPath),
207
AbsPath = relate_file_name(RelPath, State),
208
NewAbsPath = case AbsPath of
214
?dbg(true, "handle_op ?SSH_FXP_REALPATH: RelPath=~p AbsPath=~p\n",
215
[RelPath, NewAbsPath]),
217
Attr = #ssh_xfer_attr{type=directory},
218
ssh_xfer:xf_send_name(XF, ReqId, NewAbsPath, Attr),
228
RelPath0 = binary_to_list(RPath),
229
RelPath = relate_file_name(RelPath0, State0, _Canonicalize=false),
230
{Res, State} = resolve_symlinks(RelPath, State0),
233
NewAbsPath = chroot_filename(AbsPath, State),
234
?dbg(true, "handle_op ?SSH_FXP_REALPATH: RelPath=~p AbsPath=~p\n",
235
[RelPath, NewAbsPath]),
237
Attr = #ssh_xfer_attr{type=directory},
238
ssh_xfer:xf_send_name(XF, ReqId, NewAbsPath, Attr),
240
{error, _} = Error ->
241
send_status(Error, ReqId, State)
220
243
handle_op(?SSH_FXP_OPENDIR, ReqId,
221
244
<<?UINT32(RLen), RPath:RLen/binary>>,
222
245
State0 = #state{file_handler = FileMod, file_state = FS0}) ->
407
430
State1 = State0#state{file_state = FS1},
408
431
send_status(Status, ReqId, State1).
410
%%--------------------------------------------------------------------
411
%% Function: terminate(Reason, State) -> void()
412
%% Description: This function is called by a gen_server when it is about to
413
%% terminate. It should be the opposite of Module:init/1 and do any necessary
414
%% cleaning up. When it returns, the gen_server terminates with Reason.
415
%% The return value is ignored.
416
%%--------------------------------------------------------------------
417
terminate(_, #state{handles=Handles, file_handler=FileMod, file_state=FS}) ->
418
CloseFun = fun({_, file, {_, Fd}}, FS0) ->
419
{_Res, FS1} = FileMod:close(Fd, FS0),
424
lists:foldl(CloseFun, FS, Handles),
427
%%--------------------------------------------------------------------
428
%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
429
%% Description: Convert process state when code is changed
430
%%--------------------------------------------------------------------
431
code_change(_OldVsn, State, _Extra) ->
434
%%--------------------------------------------------------------------
435
%%% Internal functions
436
%%--------------------------------------------------------------------
438
433
new_handle([], H) ->
440
435
new_handle([{N, _} | Rest], H) when N > H ->
462
457
%%% read_dir/5: read directory, send names, and return new state
463
read_dir(State0 = #state{file_handler = FileMod, file_state = FS0},
464
XF, ReqId, Handle, RelPath) ->
458
read_dir(State0 = #state{file_handler = FileMod, max_files = MaxLength, file_state = FS0},
459
XF, ReqId, Handle, RelPath, {cache, Files}) ->
460
AbsPath = relate_file_name(RelPath, State0),
461
?dbg(true, "read_dir: AbsPath=~p\n", [AbsPath]),
463
length(Files) > MaxLength ->
464
{ToSend, NewCache} = lists:split(MaxLength, Files),
465
{NamesAndAttrs, FS1} = get_attrs(AbsPath, ToSend, FileMod, FS0),
466
ssh_xfer:xf_send_names(XF, ReqId, NamesAndAttrs),
467
Handles = lists:keyreplace(Handle, 1,
468
State0#state.handles,
469
{Handle, directory, {RelPath,{cache, NewCache}}}),
470
State0#state{handles = Handles, file_state = FS1};
472
{NamesAndAttrs, FS1} = get_attrs(AbsPath, Files, FileMod, FS0),
473
ssh_xfer:xf_send_names(XF, ReqId, NamesAndAttrs),
474
Handles = lists:keyreplace(Handle, 1,
475
State0#state.handles,
476
{Handle, directory, {RelPath,eof}}),
477
State0#state{handles = Handles, file_state = FS1}
479
read_dir(State0 = #state{file_handler = FileMod, max_files = MaxLength, file_state = FS0},
480
XF, ReqId, Handle, RelPath, _Status) ->
465
481
AbsPath = relate_file_name(RelPath, State0),
466
482
?dbg(true, "read_dir: AbsPath=~p\n", [AbsPath]),
467
483
{Res, FS1} = FileMod:list_dir(AbsPath, FS0),
485
{ok, Files} when MaxLength == 0 orelse MaxLength > length(Files) ->
486
{NamesAndAttrs, FS2} = get_attrs(AbsPath, Files, FileMod, FS1),
487
ssh_xfer:xf_send_names(XF, ReqId, NamesAndAttrs),
488
Handles = lists:keyreplace(Handle, 1,
489
State0#state.handles,
490
{Handle, directory, {RelPath,eof}}),
491
State0#state{handles = Handles, file_state = FS2};
470
{NamesAndAttrs, FS2} = get_attrs(AbsPath, Files, FileMod, FS1),
493
{ToSend, Cache} = lists:split(MaxLength, Files),
494
{NamesAndAttrs, FS2} = get_attrs(AbsPath, ToSend, FileMod, FS1),
471
495
ssh_xfer:xf_send_names(XF, ReqId, NamesAndAttrs),
472
496
Handles = lists:keyreplace(Handle, 1,
473
497
State0#state.handles,
474
{Handle, directory, {RelPath,eof}}),
498
{Handle, directory, {RelPath,{cache, Cache}}}),
475
499
State0#state{handles = Handles, file_state = FS2};
476
500
{error, Error} ->
477
501
State1 = State0#state{file_state = FS1},
478
502
send_status({error, Error}, ReqId, State1)
481
506
%%% get_attrs: get stat of each file and return
482
507
get_attrs(RelPath, Files, FileMod, FS) ->
483
508
get_attrs(RelPath, Files, FileMod, FS, []).
571
618
?dbg(true, "open: Flags=~p\n", [Flags]),
572
619
do_open(ReqId, State, Path, Flags);
573
620
open(Vsn, ReqId, Data, State) when Vsn >= 4 ->
574
<<?UINT32(BLen), BPath:BLen/binary, ?UINT32(_Access),
621
<<?UINT32(BLen), BPath:BLen/binary, ?UINT32(Access),
575
622
?UINT32(PFlags), _Attrs/binary>> = Data,
576
623
Path = binary_to_list(BPath),
577
Fl = ssh_xfer:decode_open_flags(Vsn, PFlags),
578
?dbg(true, "open: Fl=~p\n", [Fl]),
579
Flags = decode_4_flags(Fl),
624
FlagBits = ssh_xfer:decode_open_flags(Vsn, PFlags),
625
AcessBits = ssh_xfer:decode_ace_mask(Access),
626
?dbg(true, "open: Fl=~p\n", [FlagBits]),
627
%% TODO: This is to make sure the Access flags are not ignored
628
%% but this should be thought through better. This solution should
629
%% be considered a hack in order to buy some time. At least
630
%% it works better than when the Access flags where totally ignored.
631
%% A better solution may need some code refactoring that we do
632
%% not have time for right now.
633
AcessFlags = decode_4_acess(AcessBits),
634
Flags = lists:append(lists:umerge(
635
[[decode_4_flags(FlagBits)] | AcessFlags])),
580
637
?dbg(true, "open: Flags=~p\n", [Flags]),
581
639
do_open(ReqId, State, Path, Flags).
583
641
do_open(ReqId, State0, Path, Flags) ->
611
relate_file_name(File, State) when binary(File) ->
612
relate_file_name(binary_to_list(File), State);
613
relate_file_name(File, #state{cwd = CWD, root = ""}) ->
615
relate_file_name(File, #state{root = Root}) ->
616
case within_root(Root, File) of
620
NewFile = relate(make_relative_filename(File), Root),
621
within_root(Root, NewFile)
624
within_root(Root, File) ->
625
case lists:prefix(Root, File) of
669
%% resolve all symlinks in a path
670
resolve_symlinks(Path, State) ->
671
resolve_symlinks(Path, _LinkCnt=32, State).
673
resolve_symlinks(Path, LinkCnt, State0) ->
674
resolve_symlinks_2(filename:split(Path), State0, LinkCnt, []).
676
resolve_symlinks_2(_Path, State, LinkCnt, _AccPath) when LinkCnt =:= 0 ->
677
%% Too many links (there might be a symlink loop)
678
{{error, emlink}, State};
679
resolve_symlinks_2(["." | RestPath], State0, LinkCnt, AccPath) ->
680
resolve_symlinks_2(RestPath, State0, LinkCnt, AccPath);
681
resolve_symlinks_2([".." | RestPath], State0, LinkCnt, AccPath) ->
682
%% Remove the last path component
683
AccPathComps0 = filename:split(AccPath),
684
Path = case lists:reverse(tl(lists:reverse(AccPathComps0))) of
688
filename:join(AccPathComps)
690
resolve_symlinks_2(RestPath, State0, LinkCnt, Path);
691
resolve_symlinks_2([PathComp | RestPath], State0, LinkCnt, AccPath0) ->
692
#state{file_handler = FileMod, file_state = FS0} = State0,
693
AccPath1 = filename:join(AccPath0, PathComp),
694
{Res, FS1} = FileMod:read_link(AccPath1, FS0),
695
State1 = State0#state{file_state = FS1},
697
{ok, Target0} -> % path is a symlink
698
%% The target may be a relative or an absolute path and
699
%% may contain symlinks
700
Target1 = filename:absname(Target0, AccPath0),
701
{FollowRes, State2} = resolve_symlinks(Target1, LinkCnt-1, State1),
704
resolve_symlinks_2(RestPath, State2, LinkCnt-1, Target);
705
{error, _} = Error ->
708
{error, einval} -> % path exists, but is not a symlink
709
resolve_symlinks_2(RestPath, State1, LinkCnt, AccPath1);
710
{error, _} = Error ->
713
resolve_symlinks_2([], State, _LinkCnt, AccPath) ->
714
{{ok, AccPath}, State}.
717
relate_file_name(File, State) ->
718
relate_file_name(File, State, _Canonicalize=true).
720
relate_file_name(File, State, Canonicalize) when is_binary(File) ->
721
relate_file_name(binary_to_list(File), State, Canonicalize);
722
relate_file_name(File, #state{cwd = CWD, root = ""}, Canonicalize) ->
723
relate_filename_to_path(File, CWD, Canonicalize);
724
relate_file_name(File, #state{root = Root}, Canonicalize) ->
725
case is_within_root(Root, File) of
729
RelFile = make_relative_filename(File),
730
NewFile = relate_filename_to_path(RelFile, Root, Canonicalize),
731
case is_within_root(Root, NewFile) of
739
is_within_root(Root, File) ->
740
lists:prefix(Root, File).
632
742
%% Remove leading slash (/), if any, in order to make the filename
633
743
%% relative (to the root)
634
744
make_relative_filename("/") -> "./"; % Make it relative and preserve /
635
745
make_relative_filename("/"++File) -> File;
636
746
make_relative_filename(File) -> File.
638
relate(File0, Path) ->
748
relate_filename_to_path(File0, Path, Canonicalize) ->
639
749
File1 = filename:absname(File0, Path),
640
Parts = fix_file_name(filename:split(File1), []),
641
File2 = filename:join(Parts),
750
File2 = if Canonicalize -> canonicalize_filename(File1);
642
753
ensure_trailing_slash_is_preserved(File0, File2).
644
755
%% It seems as if the openssh client (observed with the
696
807
%%% fix file just a little: a/b/.. -> a and a/. -> a
697
fix_file_name([".." | Rest], ["/"] = Acc) ->
698
fix_file_name(Rest, Acc);
699
fix_file_name([".." | Rest], [_Dir | Paths]) ->
700
fix_file_name(Rest, Paths);
701
fix_file_name(["." | Rest], Acc) ->
702
fix_file_name(Rest, Acc);
703
fix_file_name([A | Rest], Acc) ->
704
fix_file_name(Rest, [A | Acc]);
705
fix_file_name([], Acc) ->
808
canonicalize_filename(File0) ->
809
File = filename:join(canonicalize_filename_2(filename:split(File0), [])),
810
ensure_trailing_slash_is_preserved(File0, File).
812
canonicalize_filename_2([".." | Rest], ["/"] = Acc) ->
813
canonicalize_filename_2(Rest, Acc);
814
canonicalize_filename_2([".." | Rest], [_Dir | Paths]) ->
815
canonicalize_filename_2(Rest, Paths);
816
canonicalize_filename_2(["." | Rest], Acc) ->
817
canonicalize_filename_2(Rest, Acc);
818
canonicalize_filename_2([A | Rest], Acc) ->
819
canonicalize_filename_2(Rest, [A | Acc]);
820
canonicalize_filename_2([], Acc) ->
706
821
lists:reverse(Acc).
823
%% return a filename which is relative to the root directory
824
%% (any filename that's outside the root directory is forced to the root)
825
chroot_filename(Filename, #state{root = Root}) ->
826
FilenameComps0 = filename:split(Filename),
827
RootComps = filename:split(Root),
828
filename:join(chroot_filename_2(FilenameComps0, RootComps)).
830
chroot_filename_2([PathComp | FilenameRest], [PathComp | RootRest]) ->
831
chroot_filename_2(FilenameRest, RootRest);
832
chroot_filename_2(FilenameComps, []) when length(FilenameComps) > 0 ->
833
%% Ensure there's a leading / (filename:join above will take care
834
%% of any duplicates)
835
["/" | FilenameComps];
836
chroot_filename_2(_FilenameComps, _RootComps) ->
837
%% The filename is either outside the root or at the root. In
838
%% both cases we want to force the filename to the root.
708
842
read_file(ReqId, IoDevice, Offset, Len,
709
843
State0 = #state{file_handler = FileMod, file_state = FS0}) ->
710
844
{Res1, FS1} = FileMod:position(IoDevice, {bof, Offset}, FS0),