1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2004-2018. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20%%
21-module(ftp_SUITE).
22
23-include_lib("kernel/include/file.hrl").
24-include_lib("common_test/include/ct.hrl").
25
26%% Note: This directive should only be used in test suites.
27-compile(export_all).
28
29-define(FTP_USER, "anonymous").
30-define(FTP_PASS(Cmnt), (fun({ok,__H}) -> "ftp_SUITE_"++Cmnt++"@" ++ __H;
31                            (_) -> "ftp_SUITE_"++Cmnt++"@localhost"
32                         end)(inet:gethostname())
33       ).
34
35-define(BAD_HOST, "badhostname").
36-define(BAD_USER, "baduser").
37-define(BAD_DIR,  "baddirectory").
38
39-record(progress, {
40          current = 0,
41          total
42         }).
43
44%%--------------------------------------------------------------------
45%% Common Test interface functions -----------------------------------
46%%--------------------------------------------------------------------
47suite() ->
48    [{timetrap,{seconds,20}}].
49
50all() ->
51    [
52     {group, ftp_passive},
53     {group, ftp_active},
54     {group, ftpes_passive},
55     {group, ftpes_active},
56     {group, ftps_passive},
57     {group, ftps_active},
58     {group, ftpes_passive_reuse},
59     {group, ftpes_active_reuse},
60     {group, ftps_passive_reuse},
61     {group, ftps_active_reuse},
62     {group, ftp_sup},
63     app,
64     appup,
65     error_ehost,
66     error_datafail,
67     clean_shutdown
68    ].
69
70groups() ->
71    [
72     {ftp_passive, [], ftp_tests()},
73     {ftp_active, [], ftp_tests()},
74     {ftpes_passive, [], ftp_tests_smoke()},
75     {ftpes_active, [], ftp_tests_smoke()},
76     {ftps_passive, [], ftp_tests_smoke()},
77     {ftps_active, [], ftp_tests_smoke()},
78     {ftpes_passive_reuse, [], ftp_tests_smoke()},
79     {ftpes_active_reuse, [], ftp_tests_smoke()},
80     {ftps_passive_reuse, [], ftp_tests_smoke()},
81     {ftps_active_reuse, [], ftp_tests_smoke()},
82     {ftp_sup, [], ftp_sup_tests()}
83    ].
84
85ftp_tests()->
86    [
87     user,
88     bad_user,
89     pwd,
90     cd,
91     lcd,
92     ls,
93     nlist,
94     rename,
95     delete,
96     mkdir,
97     rmdir,
98     send,
99     send_3,
100     send_bin,
101     send_chunk,
102     append,
103     append_bin,
104     append_chunk,
105     recv,
106     recv_3,
107     recv_bin,
108     recv_bin_twice,
109     recv_chunk,
110     recv_chunk_twice,
111     recv_chunk_three_times,
112     recv_chunk_delay,
113     type,
114     quote,
115     error_elogin,
116     progress_report_send,
117     progress_report_recv,
118     not_owner,
119     unexpected_call,
120     unexpected_cast,
121     unexpected_bang
122    ].
123
124ftp_tests_smoke() ->
125    [
126     ls
127    ].
128
129ftp_sup_tests() ->
130    [
131     ftp_worker
132    ].
133
134%%--------------------------------------------------------------------
135
136%%% Config
137%%% key                        meaning
138%%% ................................................................
139%%% ftpservers                list of servers to check if they are available
140%%%                        The element is:
141%%%                          {Name, % string(). The os command name
142%%%                        Path, % string(). The os PATH syntax, e.g "/bin:/usr/bin"
143%%%                           StartCommand, % fun()->{ok,start_result()} | {error,string()}.
144%%% % The command to start the daemon with.
145%%%                           ChkUp, % fun(start_result()) -> string(). Os command to check
146%%% %       if the server is running. [] if not running.
147%%% %       The string in string() is suitable for logging.
148%%%                           StopCommand, % fun(start_result()) -> void(). The command to stop the daemon with.
149%%%                           AugmentFun, % fun(config()) -> config() Adds two funs for transforming names of files
150%%% %       and directories to the form they are returned from this server
151%%%                           ServerHost, % string(). Mostly "localhost"
152%%%                           ServerPort % pos_integer()
153%%%                          }
154%%%
155
156-define(default_ftp_servers,
157        [{"vsftpd",
158          "/sbin:/usr/sbin:/usr/local/sbin",
159          fun(__CONF__, AbsName) ->
160                  DataDir = proplists:get_value(data_dir,__CONF__),
161                  ConfFile = filename:join(DataDir, "vsftpd.conf"),
162                  PrivDir = proplists:get_value(priv_dir,__CONF__),
163                  AnonRoot = PrivDir,
164                  Cmd0 = AbsName,
165                  Args0 = [filename:join(DataDir,"vsftpd.conf"),
166                           "-oftpd_banner=erlang_otp_testing",
167                           "-oanon_root=\"" ++ AnonRoot ++ "\""
168                          ],
169                  Args1 = lists:append(Args0, case proplists:get_value(name, proplists:get_value(tc_group_properties,__CONF__,[])) of
170                      ftp_active -> ["-opasv_enable=NO"];
171                      ftp_passive -> ["-oport_enable=NO"];
172                      _ -> []
173                  end),
174                  Args = case proplists:get_value(ftpd_ssl,__CONF__) of
175                      true ->
176                          A0 = [
177                                "-ossl_enable=YES",
178                                "-orsa_cert_file=\"" ++ filename:join(DataDir,"server-cert.pem") ++ "\"",
179                                "-orsa_private_key_file=\"" ++ filename:join(DataDir,"server-key.pem") ++ "\"",
180                                "-oforce_anon_logins_ssl=YES",
181                                "-oforce_anon_data_ssl=YES"
182                               ],
183                          A1 = case proplists:get_value(ftpd_ssl_reuse,__CONF__) of
184                              true -> ["-orequire_ssl_reuse=YES"];
185                              _ -> []
186                          end,
187                          A2 = case proplists:get_value(ftpd_ssl_implicit,__CONF__) of
188                              true -> ["-oimplicit_ssl=YES"];
189                              _ -> []
190                          end,
191                          lists:append([Args1, A0, A1, A2]);
192                      _ ->
193                          Args1
194                  end,
195                  % eof on stdin does not kill vsftpd
196                  Cmd = "script -qefc '" ++ "stty -echo intr ^D && exec " ++ string:join([Cmd0|Args], " ") ++ "' /dev/null",
197                  Parent = self(),
198                  Helper = spawn(fun() ->
199                      case os:cmd("ps ax | grep erlang_otp_testing | awk '/vsftpd/{print $1}'") of
200                          [] ->
201                              % OpenSSL system_default_sect CipherString may reject the SHA1 signed testing certificates
202                              case open_port({spawn,Cmd},[{env,[{"OPENSSL_CONF","/dev/null"}]},exit_status]) of
203                                  Port when is_port(Port) ->
204                                      timer:sleep(500),        % give it a chance to actually open the listening socket
205                                      Parent ! {ok,Port},
206                                      receive {From,close} ->
207                                          true = erlang:port_command(Port, [4]),
208                                          receive {Port,{exit_status,Status}} ->
209                                              ct:log("vsftpd exit with status ~b", [Status - 128])
210                                          after 500 ->
211                                              ct:log("vsftpd requires violence", []),
212                                              os:cmd("kill -9 `ps ax | grep erlang_otp_testing | awk '/vsftpd/{print $1}'`")
213                                          end,
214                                          From ! ok
215                                      end;
216                                  _Else ->
217                                      Parent ! {error,open_port}
218                              end;
219                          OSPids ->
220                              Parent ! {error,{existing,OSPids}}
221                      end
222                  end),
223                  receive
224                      {ok,Port} ->
225                          ct:log("Config file:~n~s~n~nServer start command:~n  ~s~nResult:~n  ~p",
226                                  [case file:read_file(ConfFile) of
227                                      {ok,X} -> X;
228                                      _ -> ""
229                                  end,
230                                  Cmd, erlang:port_info(Port)
231                          ]),
232                          {ok, {Helper, Port}};
233                      {error,_} = Error ->
234                          ct:fail("open_port: ~p", [Error]),
235                          Error
236                  end
237          end,
238          fun(_StartResult = {_Helper, Port}) -> erlang:port_info(Port)
239          end,
240          fun(_StartResult = {Helper, _Port}) -> Helper ! {self(), close}, receive ok -> ok end
241          end,
242          fun(__CONF__) ->
243                  AnonRoot = proplists:get_value(priv_dir,__CONF__),
244                  [{id2ftp, fun(Id) -> filename:join(AnonRoot,Id) end},
245                   {id2ftp_result,fun(Id) -> filename:join(AnonRoot,Id) end} | __CONF__]
246          end,
247          "localhost",
248          9999
249         }
250        ]
251       ).
252
253
254init_per_suite(Config) ->
255    % remove anything defunct from previoused crashed runs
256    os:cmd("kill -9 `ps ax | grep erlang_otp_testing | awk '/vsftpd/{print $1}'`"),
257
258    case find_executable(Config) of
259        false ->
260            {skip, "No ftp server found"};
261        {ok,Data} ->
262            TstDir = filename:join(proplists:get_value(priv_dir,Config), "test"),
263            file:make_dir(TstDir),
264            ftp_test_lib:make_cert_files(proplists:get_value(data_dir,Config)),
265            [{test_dir,TstDir},{ftpd_data,Data} | Config]
266    end.
267
268end_per_suite(_Config) ->
269    ok.
270
271%%--------------------------------------------------------------------
272init_per_group(Group, Config) when Group == ftpes_passive;
273                                   Group == ftpes_active;
274                                   Group == ftps_passive;
275                                   Group == ftps_active;
276                                   Group == ftpes_passive_reuse;
277                                   Group == ftpes_active_reuse;
278                                   Group == ftps_passive_reuse;
279                                   Group == ftps_active_reuse ->
280    catch crypto:stop(),
281    try crypto:start() of
282        ok when Group == ftpes_passive; Group == ftpes_active ->
283            start_ftpd([{ftpd_ssl,true}|Config]);
284        ok when Group == ftps_passive; Group == ftps_active ->
285            start_ftpd([{ftpd_ssl,true},{ftpd_ssl_implicit,true}|Config]);
286        ok when Group == ftpes_passive_reuse; Group == ftpes_active_reuse ->
287            start_ftpd([{ftpd_ssl,true},{ftpd_ssl_reuse,true}|Config]);
288        ok when Group == ftps_passive_reuse; Group == ftps_active_reuse ->
289            start_ftpd([{ftpd_ssl,true},{ftpd_ssl_reuse,true},{ftpd_ssl_implicit,true}|Config])
290    catch
291        _:_ ->
292            {skip, "Crypto did not start"}
293    end;
294
295init_per_group(_Group, Config) ->
296    start_ftpd(Config).
297
298end_per_group(_Group, Config) ->
299    stop_ftpd(Config),
300    Config.
301
302%%--------------------------------------------------------------------
303init_per_testcase(T, Config0) when T =:= app; T =:= appup ->
304    Config0;
305init_per_testcase(Case, Config0) ->
306    application:ensure_started(ftp),
307    case Case of
308        error_datafail ->
309            catch crypto:stop(),
310            try crypto:start() of
311                ok ->
312                    Config = start_ftpd([{ftpd_ssl,true},{ftpd_ssl_reuse,true}|Config0]),
313                    init_per_testcase2(Case, Config)
314            catch
315                _:_ ->
316                    {skip, "Crypto did not start"}
317            end;
318        clean_shutdown ->
319            Config = start_ftpd(Config0),
320            init_per_testcase2(Case, Config);
321        _ ->
322            init_per_testcase2(Case, Config0)
323    end.
324
325init_per_testcase2(Case, Config0) ->
326    Group = proplists:get_value(name, proplists:get_value(tc_group_properties,Config0)),
327
328    TLSB = vsftpd_tls(),
329    TLS = [{tls,TLSB}],
330    SSL = [{tls_sec_method,ftps}|TLS],
331    TLSReuse = [{tls_ctrl_session_reuse,true}|TLS],
332    SSLReuse = [{tls_sec_method,ftps}|TLSReuse],
333    ACTIVE = [{mode,active}],
334    PASSIVE = [{mode,passive}],
335    CaseOpts = case Case of
336                   progress_report_send -> [{progress, {?MODULE,progress,#progress{}}}];
337                   progress_report_recv -> [{progress, {?MODULE,progress,#progress{}}}];
338                   _ -> []
339               end,
340    ExtraOpts = [{verbose,true} | CaseOpts],
341    Config =
342        case Group of
343            ftp_active          -> ftp__open(Config0,              ACTIVE ++ ExtraOpts);
344            ftpes_active        -> ftp__open(Config0, TLS      ++  ACTIVE ++ ExtraOpts);
345            ftps_active         -> ftp__open(Config0, SSL      ++  ACTIVE ++ ExtraOpts);
346            ftp_passive         -> ftp__open(Config0,             PASSIVE ++ ExtraOpts);
347            ftpes_passive       -> ftp__open(Config0, TLS      ++ PASSIVE ++ ExtraOpts);
348            ftps_passive        -> ftp__open(Config0, SSL      ++ PASSIVE ++ ExtraOpts);
349            ftpes_passive_reuse -> ftp__open(Config0, TLSReuse ++ PASSIVE ++ ExtraOpts);
350            ftpes_active_reuse  -> ftp__open(Config0, TLSReuse ++  ACTIVE ++ ExtraOpts);
351            ftps_passive_reuse  -> ftp__open(Config0, SSLReuse ++ PASSIVE ++ ExtraOpts);
352            ftps_active_reuse   -> ftp__open(Config0, SSLReuse ++  ACTIVE ++ ExtraOpts);
353            ftp_sup             -> ftp__open(Config0, ACTIVE ++ ExtraOpts);
354            undefined           -> Config0
355        end,
356    case Case of
357        user           -> Config;
358        bad_user       -> Config;
359        error_elogin   -> Config;
360        error_ehost    -> Config;
361        clean_shutdown -> Config;
362        _ ->
363            ConfigN = if
364                Case == error_datafail ->
365                    ftp__open(Config, TLS++PASSIVE++ExtraOpts);
366                true ->
367                    Config
368            end,
369            Pid = proplists:get_value(ftp,ConfigN),
370            ok = ftp:user(Pid, ?FTP_USER, ?FTP_PASS(atom_to_list(Group)++"-"++atom_to_list(Case)) ),
371            ok = ftp:cd(Pid, proplists:get_value(priv_dir,ConfigN)),
372            ConfigN
373    end.
374
375end_per_testcase(T, _Config) when T =:= app; T =:= appup -> ok;
376end_per_testcase(user, _Config) -> ok;
377end_per_testcase(bad_user, _Config) -> ok;
378end_per_testcase(error_elogin, _Config) -> ok;
379end_per_testcase(error_ehost, _Config) -> ok;
380end_per_testcase(T, Config) when T =:= error_datafail; T =:= clean_shutdown ->
381    T == error_datafail andalso ftp__close(Config),
382    stop_ftpd(Config),
383    ok;
384end_per_testcase(_Case, Config) ->
385    case proplists:get_value(tc_status,Config) of
386        ok -> ok;
387        _ ->
388            try ftp:latest_ctrl_response(proplists:get_value(ftp,Config))
389            of
390                {ok,S} -> ct:log("***~n*** Latest ctrl channel response:~n***     ~p~n***",[S])
391            catch
392                _:_ -> ok
393            end
394    end,
395    Group = proplists:get_value(name, proplists:get_value(tc_group_properties,Config)),
396    case Group of
397        ftp_sup ->
398            ftp_stop_service(Config);
399        _Else ->
400            ftp__close(Config)
401    end.
402
403vsftpd_tls() ->
404    %% Workaround for interoperability issues with vsftpd =< 3.0.2:
405    %%
406    %% vsftpd =< 3.0.2 does not support ECDHE ciphers and the ssl application
407    %% removed ciphers with RSA key exchange from its default cipher list.
408    %% To allow interoperability with old versions of vsftpd, cipher suites
409    %% with RSA key exchange are appended to the default cipher list.
410    All = ssl:cipher_suites(all, 'tlsv1.2'),
411    Default = ssl:cipher_suites(default, 'tlsv1.2'),
412    RSASuites =
413        ssl:filter_cipher_suites(All, [{key_exchange, fun(rsa) -> true;
414                                                         (_) -> false end}]),
415    Suites = ssl:append_cipher_suites(RSASuites, Default),
416    [
417        {ciphers,Suites},
418        %% vsftpd =< 3.0.3 gets upset with anything later than tlsv1.2
419        {versions,['tlsv1.2']}
420    ].
421
422%%--------------------------------------------------------------------
423%% Test Cases --------------------------------------------------------
424%%--------------------------------------------------------------------
425app() ->
426    [{doc, "Test that the ftp app file is ok"}].
427app(Config) when is_list(Config) ->
428    ok = test_server:app_test(ftp).
429
430%%--------------------------------------------------------------------
431appup() ->
432    [{doc, "Test that the ftp appup file is ok"}].
433appup(Config) when is_list(Config) ->
434    ok = test_server:appup_test(ftp).
435
436%%--------------------------------------------------------------------
437
438user() -> [
439           {doc, "Open an ftp connection to a host, and logon as anonymous ftp,"
440            " then logoff"}].
441user(Config) ->
442    Pid = proplists:get_value(ftp, Config),
443    ok = ftp:user(Pid, ?FTP_USER, ?FTP_PASS("")),% logon
444    ok = ftp:close(Pid),                        % logoff
445    {error,eclosed} = ftp:pwd(Pid),                % check logoff result
446    ok.
447
448%%-------------------------------------------------------------------------
449bad_user() ->
450    [{doc, "Open an ftp connection to a host, and logon with bad user."}].
451bad_user(Config) ->
452    Pid = proplists:get_value(ftp, Config),
453    {error, euser} = ftp:user(Pid, ?BAD_USER, ?FTP_PASS("")),
454    ok.
455
456%%-------------------------------------------------------------------------
457pwd() ->
458    [{doc, "Test ftp:pwd/1 & ftp:lpwd/1"}].
459pwd(Config0) ->
460    Config = set_state([reset], Config0),
461    Pid = proplists:get_value(ftp, Config),
462    {ok, PWD} = ftp:pwd(Pid),
463    {ok, PathLpwd} = ftp:lpwd(Pid),
464    PWD = id2ftp_result("", Config),
465    PathLpwd = id2ftp_result("", Config).
466
467%%-------------------------------------------------------------------------
468cd() ->
469    ["Open an ftp connection, log on as anonymous ftp, and cd to a"
470     "directory and to a non-existent directory."].
471cd(Config0) ->
472    Dir = "test",
473    Config = set_state([reset,{mkdir,Dir}], Config0),
474    Pid = proplists:get_value(ftp, Config),
475    ok = ftp:cd(Pid, id2ftp(Dir,Config)),
476    {ok, PWD} = ftp:pwd(Pid),
477    ExpectedPWD = id2ftp_result(Dir, Config),
478    PWD = ExpectedPWD,
479    {error, epath} = ftp:cd(Pid, ?BAD_DIR),
480    ok.
481
482%%-------------------------------------------------------------------------
483lcd() ->
484    [{doc, "Test api function ftp:lcd/2"}].
485lcd(Config0) ->
486    Dir = "test",
487    Config = set_state([reset,{mkdir,Dir}], Config0),
488    Pid = proplists:get_value(ftp, Config),
489    ok = ftp:lcd(Pid, id2ftp(Dir,Config)),
490    {ok, PWD} = ftp:lpwd(Pid),
491    ExpectedPWD = id2ftp_result(Dir, Config),
492    PWD = ExpectedPWD,
493    {error, epath} = ftp:lcd(Pid, ?BAD_DIR).
494
495%%-------------------------------------------------------------------------
496ls() ->
497    [{doc, "Open an ftp connection; ls the current directory, and the "
498      "\"test\" directory. We assume that ls never fails, since "
499      "it's output is meant to be read by humans. "}].
500ls(Config0) ->
501    Config = set_state([reset,{mkdir,"test"}], Config0),
502    Pid = proplists:get_value(ftp, Config),
503    {ok, _R1} = ftp:ls(Pid),
504    {ok, _R2} = ftp:ls(Pid, id2ftp("test",Config)),
505    %% neither nlist nor ls operates on a directory
506    %% they operate on a pathname, which *can* be a
507    %% directory, but can also be a filename or a group
508    %% of files (including wildcards).
509    case proplists:get_value(wildcard_support, Config) of
510        true ->
511            {ok, _R3} = ftp:ls(Pid, id2ftp("te*",Config));
512        _ ->
513            ok
514    end.
515
516%%-------------------------------------------------------------------------
517nlist() ->
518    [{doc,"Open an ftp connection; nlist the current directory, and the "
519               "\"test\" directory. Nlist does not behave consistenly over "
520               "operating systems. On some it is an error to have an empty "
521               "directory."}].
522nlist(Config0) ->
523    Config = set_state([reset,{mkdir,"test"}], Config0),
524    Pid = proplists:get_value(ftp, Config),
525    {ok, _R1} = ftp:nlist(Pid),
526    {ok, _R2} = ftp:nlist(Pid, id2ftp("test",Config)),
527    %% neither nlist nor ls operates on a directory
528    %% they operate on a pathname, which *can* be a
529    %% directory, but can also be a filename or a group
530    %% of files (including wildcards).
531    case proplists:get_value(wildcard_support, Config) of
532        true ->
533            {ok, _R3} = ftp:nlist(Pid, id2ftp("te*",Config));
534        _ ->
535            ok
536    end.
537
538%%-------------------------------------------------------------------------
539rename() ->
540    [{doc, "Rename a file."}].
541rename(Config0) ->
542    Contents = <<"ftp_SUITE test ...">>,
543    OldFile = "old.txt",
544    NewFile = "new.txt",
545    Config = set_state([reset,{mkfile,OldFile,Contents}], Config0),
546    Pid = proplists:get_value(ftp, Config),
547
548    ok = ftp:rename(Pid,
549                    id2ftp(OldFile,Config),
550                    id2ftp(NewFile,Config)),
551
552    true = (chk_file(NewFile,Contents,Config)
553            and chk_no_file([OldFile],Config)),
554    {error,epath} = ftp:rename(Pid,
555                               id2ftp("non_existing_file",Config),
556                               id2ftp(NewFile,Config)),
557    ok.
558
559%%-------------------------------------------------------------------------
560send() ->
561    [{doc, "Transfer a file with ftp using send/2."}].
562send(Config0) ->
563    Contents = <<"ftp_SUITE test ...">>,
564    SrcDir = "data",
565    File = "file.txt",
566    Config = set_state([reset,{mkfile,[SrcDir,File],Contents}], Config0),
567    Pid = proplists:get_value(ftp, Config),
568
569    chk_no_file([File],Config),
570    chk_file([SrcDir,File],Contents,Config),
571
572    ok = ftp:lcd(Pid, id2ftp(SrcDir,Config)),
573    ok = ftp:cd(Pid, id2ftp("",Config)),
574    ok = ftp:send(Pid, File),
575    chk_file(File, Contents, Config),
576
577    {error,epath} = ftp:send(Pid, "non_existing_file"),
578    ok.
579
580%%-------------------------------------------------------------------------
581send_3() ->
582    [{doc, "Transfer a file with ftp using send/3."}].
583send_3(Config0) ->
584    Contents = <<"ftp_SUITE test ...">>,
585    Dir = "incoming",
586    File = "file.txt",
587    RemoteFile = "remfile.txt",
588    Config = set_state([reset,{mkfile,File,Contents},{mkdir,Dir}], Config0),
589    Pid = proplists:get_value(ftp, Config),
590
591    ok = ftp:cd(Pid, id2ftp(Dir,Config)),
592    ok = ftp:lcd(Pid, id2ftp("",Config)),
593    ok = ftp:send(Pid, File, RemoteFile),
594    chk_file([Dir,RemoteFile], Contents, Config),
595
596    {error,epath} = ftp:send(Pid, "non_existing_file", RemoteFile),
597    ok.
598
599%%-------------------------------------------------------------------------
600send_bin() ->
601    [{doc, "Send a binary."}].
602send_bin(Config0) ->
603    BinContents = <<"ftp_SUITE test ...">>,
604    File = "file.txt",
605    Config = set_state([reset], Config0),
606    Pid = proplists:get_value(ftp, Config),
607    {error, enotbinary} = ftp:send_bin(Pid, "some string", id2ftp(File,Config)),
608    ok = ftp:send_bin(Pid, BinContents, id2ftp(File,Config)),
609    chk_file(File, BinContents, Config),
610    {error, efnamena} = ftp:send_bin(Pid, BinContents, "/nothere/nohere"),
611    ok.
612
613%%-------------------------------------------------------------------------
614send_chunk() ->
615    [{doc, "Send a binary using chunks."}].
616send_chunk(Config0) ->
617    Contents1 = <<"1: ftp_SUITE test ...">>,
618    Contents2 = <<"2: ftp_SUITE test ...">>,
619    File = "file.txt",
620    Config = set_state([reset,{mkdir,"incoming"}], Config0),
621    Pid = proplists:get_value(ftp, Config),
622
623    ok = ftp:send_chunk_start(Pid, id2ftp(File,Config)),
624    {error, echunk} = ftp:send_chunk_start(Pid, id2ftp(File,Config)),
625    {error, echunk} = ftp:cd(Pid, "incoming"),
626    {error, enotbinary} = ftp:send_chunk(Pid, "some string"),
627    ok = ftp:send_chunk(Pid, Contents1),
628    ok = ftp:send_chunk(Pid, Contents2),
629    ok = ftp:send_chunk_end(Pid),
630    chk_file(File, <<Contents1/binary,Contents2/binary>>, Config),
631
632    {error, echunk} = ftp:send_chunk(Pid, Contents1),
633    {error, echunk} = ftp:send_chunk_end(Pid),
634    {error, efnamena} = ftp:send_chunk_start(Pid, "/"),
635    ok.
636
637%%-------------------------------------------------------------------------
638delete() ->
639    [{doc, "Delete a file."}].
640delete(Config0) ->
641    Contents = <<"ftp_SUITE test ...">>,
642    File = "file.txt",
643    Config = set_state([reset,{mkfile,File,Contents}], Config0),
644    Pid = proplists:get_value(ftp, Config),
645    ok = ftp:delete(Pid, id2ftp(File,Config)),
646    chk_no_file([File], Config),
647    {error,epath} = ftp:delete(Pid, id2ftp(File,Config)),
648    ok.
649
650%%-------------------------------------------------------------------------
651mkdir() ->
652    [{doc, "Make a remote directory."}].
653mkdir(Config0) ->
654    NewDir = "new_dir",
655    Config = set_state([reset], Config0),
656    Pid = proplists:get_value(ftp, Config),
657    ok = ftp:mkdir(Pid, id2ftp(NewDir,Config)),
658    chk_dir([NewDir], Config),
659    {error,epath} = ftp:mkdir(Pid, id2ftp(NewDir,Config)),
660    ok.
661
662%%-------------------------------------------------------------------------
663rmdir() ->
664    [{doc, "Remove a directory."}].
665rmdir(Config0) ->
666    Dir = "dir",
667    Config = set_state([reset,{mkdir,Dir}], Config0),
668    Pid = proplists:get_value(ftp, Config),
669    ok = ftp:rmdir(Pid, id2ftp(Dir,Config)),
670    chk_no_dir([Dir], Config),
671    {error,epath} = ftp:rmdir(Pid, id2ftp(Dir,Config)),
672    ok.
673
674%%-------------------------------------------------------------------------
675append() ->
676    [{doc, "Append a local file twice to a remote file"}].
677append(Config0) ->
678    SrcFile = "f_src.txt",
679    DstFile = "f_dst.txt",
680    Contents = <<"ftp_SUITE test ...">>,
681    Config = set_state([reset,{mkfile,SrcFile,Contents}], Config0),
682    Pid = proplists:get_value(ftp, Config),
683    ok = ftp:append(Pid, id2ftp(SrcFile,Config), id2ftp(DstFile,Config)),
684    ok = ftp:append(Pid, id2ftp(SrcFile,Config), id2ftp(DstFile,Config)),
685    chk_file(DstFile, <<Contents/binary,Contents/binary>>, Config),
686    {error,epath} = ftp:append(Pid, id2ftp("non_existing_file",Config), id2ftp(DstFile,Config)),
687    ok.
688
689%%-------------------------------------------------------------------------
690append_bin() ->
691    [{doc, "Append a local file twice to a remote file using append_bin"}].
692append_bin(Config0) ->
693    DstFile = "f_dst.txt",
694    Contents = <<"ftp_SUITE test ...">>,
695    Config = set_state([reset], Config0),
696    Pid = proplists:get_value(ftp, Config),
697    ok = ftp:append_bin(Pid, Contents, id2ftp(DstFile,Config)),
698    ok = ftp:append_bin(Pid, Contents, id2ftp(DstFile,Config)),
699    chk_file(DstFile, <<Contents/binary,Contents/binary>>, Config).
700
701%%-------------------------------------------------------------------------
702append_chunk() ->
703    [{doc, "Append chunks."}].
704append_chunk(Config0) ->
705    File = "f_dst.txt",
706    Contents = [<<"ER">>,<<"LE">>,<<"RL">>],
707    Config = set_state([reset], Config0),
708    Pid = proplists:get_value(ftp, Config),
709    ok = ftp:append_chunk_start(Pid, id2ftp(File,Config)),
710    {error, enotbinary} = ftp:append_chunk(Pid, binary_to_list(lists:nth(1,Contents))),
711    ok = ftp:append_chunk(Pid,lists:nth(1,Contents)),
712    ok = ftp:append_chunk(Pid,lists:nth(2,Contents)),
713    ok = ftp:append_chunk(Pid,lists:nth(3,Contents)),
714    ok = ftp:append_chunk_end(Pid),
715    chk_file(File, <<"ERLERL">>, Config).
716
717%%-------------------------------------------------------------------------
718recv() ->
719    [{doc, "Receive a file using recv/2"}].
720recv(Config0) ->
721    File1 = "f_dst1.txt",
722    File2 = "f_dst2.txt",
723    SrcDir = "a_dir",
724    Contents1 = <<"1 ftp_SUITE test ...">>,
725    Contents2 = <<"2 ftp_SUITE test ...">>,
726    Config = set_state([reset, {mkfile,[SrcDir,File1],Contents1}, {mkfile,[SrcDir,File2],Contents2}], Config0),
727    Pid = proplists:get_value(ftp, Config),
728    ok = ftp:cd(Pid, id2ftp(SrcDir,Config)),
729    ok = ftp:lcd(Pid, id2ftp("",Config)),
730    ok = ftp:recv(Pid, File1),
731    chk_file(File1, Contents1, Config),
732    ok = ftp:recv(Pid, File2),
733    chk_file(File2, Contents2, Config),
734    {error,epath} = ftp:recv(Pid, "non_existing_file"),
735    ok.
736
737%%-------------------------------------------------------------------------
738recv_3() ->
739    [{doc,"Receive a file using recv/3"}].
740recv_3(Config0) ->
741    DstFile = "f_src.txt",
742    SrcFile = "f_dst.txt",
743    Contents = <<"ftp_SUITE test ...">>,
744    Config = set_state([reset, {mkfile,SrcFile,Contents}], Config0),
745    Pid = proplists:get_value(ftp, Config),
746    ok = ftp:cd(Pid, id2ftp("",Config)),
747    ok = ftp:recv(Pid, SrcFile, id2abs(DstFile,Config)),
748    chk_file(DstFile, Contents, Config).
749
750%%-------------------------------------------------------------------------
751recv_bin() ->
752    [{doc, "Receive a file as a binary."}].
753recv_bin(Config0) ->
754    File = "f_dst.txt",
755    Contents = <<"ftp_SUITE test ...">>,
756    Config = set_state([reset, {mkfile,File,Contents}], Config0),
757    Pid = proplists:get_value(ftp, Config),
758    {ok,Received} = ftp:recv_bin(Pid, id2ftp(File,Config)),
759    find_diff(Received, Contents),
760    {error,epath} = ftp:recv_bin(Pid, id2ftp("non_existing_file",Config)),
761    ok.
762
763%%-------------------------------------------------------------------------
764recv_bin_twice() ->
765    [{doc, "Receive two files as a binaries."}].
766recv_bin_twice(Config0) ->
767    File1 = "f_dst1.txt",
768    File2 = "f_dst2.txt",
769    Contents1 = <<"1 ftp_SUITE test ...">>,
770    Contents2 = <<"2 ftp_SUITE test ...">>,
771    Config = set_state([reset, {mkfile,File1,Contents1}, {mkfile,File2,Contents2}], Config0),
772    ct:log("First transfer",[]),
773    Pid = proplists:get_value(ftp, Config),
774    {ok,Received1} = ftp:recv_bin(Pid, id2ftp(File1,Config)),
775    find_diff(Received1, Contents1),
776    ct:log("Second transfer",[]),
777    {ok,Received2} = ftp:recv_bin(Pid, id2ftp(File2,Config)),
778    find_diff(Received2, Contents2),
779    ct:log("Transfers ready!",[]),
780    {error,epath} = ftp:recv_bin(Pid, id2ftp("non_existing_file",Config)),
781    ok.
782%%-------------------------------------------------------------------------
783recv_chunk() ->
784    [{doc, "Receive a file using chunk-wise."}].
785recv_chunk(Config0) ->
786    File = "big_file.txt",
787    Contents = list_to_binary( lists:duplicate(1000, lists:seq(0,255)) ),
788    Config = set_state([reset, {mkfile,File,Contents}], Config0),
789    Pid = proplists:get_value(ftp, Config),
790    {error, "ftp:recv_chunk_start/2 not called"} = do_recv_chunk(Pid),
791    ok = ftp:recv_chunk_start(Pid, id2ftp(File,Config)),
792    {ok, ReceivedContents} = do_recv_chunk(Pid),
793    find_diff(ReceivedContents, Contents).
794
795recv_chunk_twice() ->
796    [{doc, "Receive two files using chunk-wise."}].
797recv_chunk_twice(Config0) ->
798    File1 = "big_file1.txt",
799    File2 = "big_file2.txt",
800    Contents1 = list_to_binary( lists:duplicate(1000, lists:seq(0,255)) ),
801    Contents2 = crypto:strong_rand_bytes(1200),
802    Config = set_state([reset, {mkfile,File1,Contents1}, {mkfile,File2,Contents2}], Config0),
803    Pid = proplists:get_value(ftp, Config),
804    {error, "ftp:recv_chunk_start/2 not called"} = do_recv_chunk(Pid),
805    ok = ftp:recv_chunk_start(Pid, id2ftp(File1,Config)),
806    {ok, ReceivedContents1} = do_recv_chunk(Pid),
807    ok = ftp:recv_chunk_start(Pid, id2ftp(File2,Config)),
808    {ok, ReceivedContents2} = do_recv_chunk(Pid),
809    find_diff(ReceivedContents1, Contents1),
810    find_diff(ReceivedContents2, Contents2).
811
812recv_chunk_three_times() ->
813    [{doc, "Receive two files using chunk-wise."},
814     {timetrap,{seconds,120}}].
815recv_chunk_three_times(Config0) ->
816    File1 = "big_file1.txt",
817    File2 = "big_file2.txt",
818    File3 = "big_file3.txt",
819    Contents1 = list_to_binary( lists:duplicate(1000, lists:seq(0,255)) ),
820    Contents2 = crypto:strong_rand_bytes(1200),
821    Contents3 = list_to_binary( lists:duplicate(1000, lists:seq(255,0,-1)) ),
822
823    Config = set_state([reset, {mkfile,File1,Contents1}, {mkfile,File2,Contents2}, {mkfile,File3,Contents3}], Config0),
824    Pid = proplists:get_value(ftp, Config),
825    {error, "ftp:recv_chunk_start/2 not called"} = do_recv_chunk(Pid),
826
827    ok = ftp:recv_chunk_start(Pid, id2ftp(File3,Config)),
828    {ok, ReceivedContents3} = do_recv_chunk(Pid),
829
830    ok = ftp:recv_chunk_start(Pid, id2ftp(File1,Config)),
831    {ok, ReceivedContents1} = do_recv_chunk(Pid),
832
833    ok = ftp:recv_chunk_start(Pid, id2ftp(File2,Config)),
834    {ok, ReceivedContents2} = do_recv_chunk(Pid),
835
836    find_diff(ReceivedContents1, Contents1),
837    find_diff(ReceivedContents2, Contents2),
838    find_diff(ReceivedContents3, Contents3).
839
840
841do_recv_chunk(Pid) ->
842    recv_chunk(Pid, <<>>).
843recv_chunk(Pid, Acc) ->
844    case ftp:recv_chunk(Pid) of
845        ok ->
846            {ok, Acc};
847        {ok, Bin} ->
848            recv_chunk(Pid, <<Acc/binary, Bin/binary>>);
849        Error ->
850            Error
851    end.
852
853recv_chunk_delay(Config0) when is_list(Config0) ->
854    File1 = "big_file1.txt",
855    Contents = list_to_binary(lists:duplicate(1000, lists:seq(0,255))),
856    Config = set_state([reset, {mkfile,File1,Contents}], Config0),
857    Pid = proplists:get_value(ftp, Config),
858    ok = ftp:recv_chunk_start(Pid, id2ftp(File1,Config)),
859    {ok, ReceivedContents} = delay_recv_chunk(Pid),
860    find_diff(ReceivedContents, Contents).
861
862delay_recv_chunk(Pid) ->
863     delay_recv_chunk(Pid, <<>>).
864delay_recv_chunk(Pid, Acc) ->
865    ct:pal("Received size ~p", [byte_size(Acc)]),
866    case ftp:recv_chunk(Pid) of
867         ok ->
868             {ok, Acc};
869         {ok, Bin} ->
870            ct:sleep(100),
871            delay_recv_chunk(Pid, <<Acc/binary, Bin/binary>>);
872        Error ->
873            Error
874     end.
875
876%%-------------------------------------------------------------------------
877type() ->
878    [{doc,"Test that we can change between ASCII and binary transfer mode"}].
879type(Config) ->
880    Pid = proplists:get_value(ftp, Config),
881    ok = ftp:type(Pid, ascii),
882    ok = ftp:type(Pid, binary),
883    ok = ftp:type(Pid, ascii),
884    {error, etype} = ftp:type(Pid, foobar).
885
886%%-------------------------------------------------------------------------
887quote(Config) ->
888    Pid = proplists:get_value(ftp, Config),
889    ["257 \""++_Rest] = ftp:quote(Pid, "pwd"), %% 257
890    [_| _] = ftp:quote(Pid, "help"),
891    %% This negativ test causes some ftp servers to hang. This test
892    %% is not important for the client, so we skip it for now.
893    %%["425 Can't build data connection: Connection refused."]
894    %% = ftp:quote(Pid, "list"),
895    ok.
896
897%%-------------------------------------------------------------------------
898progress_report_send() ->
899    [{doc, "Test the option progress for ftp:send/[2,3]"}].
900progress_report_send(Config) when is_list(Config) ->
901    ReportPid =
902        spawn_link(?MODULE, progress_report_receiver_init, [self(), 1]),
903    send(Config),
904    receive
905        {ReportPid, ok} ->
906            ok
907    end.
908
909%%-------------------------------------------------------------------------
910progress_report_recv() ->
911    [{doc, "Test the option progress for ftp:recv/[2,3]"}].
912progress_report_recv(Config) when is_list(Config) ->
913    ReportPid =
914         spawn_link(?MODULE, progress_report_receiver_init, [self(), 3]),
915    recv(Config),
916    receive
917         {ReportPid, ok} ->
918             ok
919    end.
920
921%%-------------------------------------------------------------------------
922
923not_owner() ->
924    [{doc, "Test what happens if a process that not owns the connection tries "
925    "to use it"}].
926not_owner(Config) when is_list(Config) ->
927    Pid = proplists:get_value(ftp, Config),
928
929    Parent = self(),
930    OtherPid = spawn_link(
931                 fun() ->
932                         {error, not_connection_owner} = ftp:pwd(Pid),
933                         ftp:close(Pid),
934                         Parent ! {self(), ok}
935                 end),
936    receive
937        {OtherPid, ok} ->
938            {ok, _} = ftp:pwd(Pid)
939    end.
940
941
942%%-------------------------------------------------------------------------
943
944
945unexpected_call()->
946    [{doc, "Test that behaviour of the ftp process if the api is abused"}].
947unexpected_call(Config) when is_list(Config) ->
948    Flag =  process_flag(trap_exit, true),
949    Pid = proplists:get_value(ftp, Config),
950
951    %% Serious programming fault, connetion will be shut down
952    case (catch gen_server:call(Pid, {self(), foobar, 10}, infinity)) of
953        {error, {connection_terminated, 'API_violation'}} ->
954            ok;
955        Unexpected1 ->
956            exit({unexpected_result, Unexpected1})
957    end,
958    ct:sleep(500),
959    undefined = process_info(Pid, status),
960    process_flag(trap_exit, Flag).
961%%-------------------------------------------------------------------------
962
963unexpected_cast()->
964    [{doc, "Test that behaviour of the ftp process if the api is abused"}].
965unexpected_cast(Config) when is_list(Config) ->
966    Flag = process_flag(trap_exit, true),
967    Pid = proplists:get_value(ftp, Config),
968    %% Serious programming fault, connetion will be shut down
969    gen_server:cast(Pid, {self(), foobar, 10}),
970    ct:sleep(500),
971    undefined = process_info(Pid, status),
972    process_flag(trap_exit, Flag).
973%%-------------------------------------------------------------------------
974
975unexpected_bang()->
976    [{doc, "Test that connection ignores unexpected bang"}].
977unexpected_bang(Config) when is_list(Config) ->
978    Flag = process_flag(trap_exit, true),
979    Pid = proplists:get_value(ftp, Config),
980    %% Could be an innocent misstake the connection lives.
981    Pid ! foobar,
982    ct:sleep(500),
983    {status, _} = process_info(Pid, status),
984    process_flag(trap_exit, Flag).
985
986%%-------------------------------------------------------------------------
987
988clean_shutdown() ->
989    [{doc, "Test that owning process that exits with reason "
990     "'shutdown' does not cause an error message. OTP 6035"}].
991
992clean_shutdown(Config) ->
993    Parent = self(),
994    HelperPid = spawn(
995                  fun() ->
996                          ftp__open(Config, [{verbose,true}]),
997                          Parent ! ok,
998                          receive
999                              nothing -> ok
1000                          end
1001                  end),
1002    receive
1003        ok ->
1004            PrivDir = proplists:get_value(priv_dir, Config),
1005            LogFile = filename:join([PrivDir,"ticket_6035.log"]),
1006             error_logger:logfile({open, LogFile}),
1007            exit(HelperPid, shutdown),
1008            timer:sleep(2000),
1009            error_logger:logfile(close),
1010            case unwanted_error_report(LogFile) of
1011                true ->  {fail, "Bad logfile"};
1012                false -> ok
1013            end
1014    end.
1015
1016%%-------------------------------------------------------------------------
1017ftp_worker() ->
1018    [{doc, "Makes sure the ftp worker processes are added and removed "
1019      "appropriatly to/from the supervison tree."}].
1020ftp_worker(Config) ->
1021    Pid = proplists:get_value(ftp,Config),
1022    case supervisor:which_children(ftp_sup) of
1023        [{_,_, worker, [ftp]}] ->
1024            ftp:close(Pid),
1025            ct:sleep(5000),
1026            [] = supervisor:which_children(ftp_sup),
1027            ok;
1028        Children ->
1029            ct:fail("Unexpected children: ~p",[Children])
1030    end.
1031
1032
1033%%%----------------------------------------------------------------
1034%%% Error codes not tested elsewhere
1035
1036error_elogin(Config0) ->
1037    Dir = "test",
1038    OldFile = "old.txt",
1039    NewFile = "new.txt",
1040    SrcDir = "data",
1041    File = "file.txt",
1042    Config = set_state([reset,
1043                        {mkdir,Dir},
1044                        {mkfile,OldFile,<<"Contents..">>},
1045                        {mkfile,[SrcDir,File],<<"Contents..">>}], Config0),
1046
1047    Pid = proplists:get_value(ftp, Config),
1048    ok = ftp:lcd(Pid, id2ftp(SrcDir,Config)),
1049    {error,elogin} = ftp:send(Pid, File),
1050    ok = ftp:lcd(Pid, id2ftp("",Config)),
1051    {error,elogin} = ftp:pwd(Pid),
1052    {error,elogin} = ftp:cd(Pid, id2ftp(Dir,Config)),
1053    {error,elogin} = ftp:rename(Pid,
1054                                id2ftp(OldFile,Config),
1055                                id2ftp(NewFile,Config)),
1056    ok.
1057
1058error_ehost(_Config) ->
1059    {error, ehost} = ftp:open("nohost.nodomain"),
1060    ok.
1061
1062%%%----------------------------------------------------------------
1063error_datafail() ->
1064    [{doc, "Test that failure to open data channel captures "
1065     "error emitted on ctrl chanenel"}].
1066
1067error_datafail(Config) ->
1068    Self = self(),
1069    Pid = proplists:get_value(ftp, Config),
1070    % ftp:latest_ctrl_response/1 returns {error,eclosed}
1071    % and erlang:group_leader/2 does not work under ct
1072    dbg:start(),
1073    dbg:tracer(process, {fun
1074        ({trace,P,call,{ftp,verbose,[M,_,'receive']}}, ok) when P == Pid -> Self ! M, ok;
1075        (_, ok) -> ok
1076    end, ok}),
1077    dbg:tpl(ftp, verbose, []),
1078    dbg:p(Pid, [call]),
1079    {error,_} = ftp:ls(Pid),
1080    dbg:stop_clear(),
1081    Recv = fun(Recv) ->
1082        receive
1083            Msg when is_list(Msg) ->
1084                case string:find(Msg, "session reuse required") of
1085                    nomatch -> Recv(Recv);
1086                    _ -> ok
1087                end
1088            after 2000 ->
1089                {fail, "missing error stating 'session reuse required'"}
1090        end
1091    end,
1092    Result = Recv(Recv),
1093    Result.
1094
1095%%--------------------------------------------------------------------
1096%% Internal functions  -----------------------------------------------
1097%%--------------------------------------------------------------------
1098
1099chk_file(Path=[C|_], ExpectedContents, Config) when 0<C,C=<255 ->
1100    chk_file([Path], ExpectedContents, Config);
1101
1102chk_file(PathList, ExpectedContents, Config) ->
1103    Path = filename:join(PathList),
1104    AbsPath = id2abs(Path,Config),
1105    case file:read_file(AbsPath) of
1106        {ok,ExpectedContents} ->
1107            true;
1108        {ok,ReadContents} ->
1109            {error,{diff,Pos,RC,LC}} = find_diff(ReadContents, ExpectedContents, 1),
1110            ct:log("Bad contents of ~p.~nGot:~n~p~nExpected:~n~p~nDiff at pos ~p ~nRead: ~p~nExp : ~p",
1111                   [AbsPath,ReadContents,ExpectedContents,Pos,RC,LC]),
1112            ct:fail("Bad contents of ~p", [Path]);
1113        {error,Error} ->
1114            try begin
1115                    {ok,CWD} = file:get_cwd(),
1116                    ct:log("file:get_cwd()=~p~nfiles:~n~p",[CWD,file:list_dir(CWD)])
1117                end
1118            of _ -> ok
1119            catch _:_ ->ok
1120            end,
1121            ct:fail("Error reading ~p: ~p",[Path,Error])
1122    end.
1123
1124
1125chk_no_file(Path=[C|_], Config) when 0<C,C=<255 ->
1126    chk_no_file([Path], Config);
1127
1128chk_no_file(PathList, Config) ->
1129    Path = filename:join(PathList),
1130    AbsPath = id2abs(Path,Config),
1131    case file:read_file(AbsPath) of
1132        {error,enoent} ->
1133            true;
1134        {ok,Contents} ->
1135            ct:log("File ~p exists although it shouldn't. Contents:~n~p",
1136                   [AbsPath,Contents]),
1137            ct:fail("File exists: ~p", [Path]);
1138        {error,Error} ->
1139            ct:fail("Unexpected error reading ~p: ~p",[Path,Error])
1140    end.
1141
1142
1143chk_dir(Path=[C|_], Config) when 0<C,C=<255 ->
1144    chk_dir([Path], Config);
1145
1146chk_dir(PathList, Config) ->
1147    Path = filename:join(PathList),
1148    AbsPath = id2abs(Path,Config),
1149    case file:read_file_info(AbsPath) of
1150        {ok, #file_info{type=directory}} ->
1151            true;
1152        {ok, #file_info{type=Type}} ->
1153            ct:fail("Expected dir ~p is a ~p",[Path,Type]);
1154        {error,Error} ->
1155            ct:fail("Expected dir ~p: ~p",[Path,Error])
1156    end.
1157
1158chk_no_dir(PathList, Config) ->
1159    Path = filename:join(PathList),
1160    AbsPath = id2abs(Path,Config),
1161    case file:read_file_info(AbsPath) of
1162        {error,enoent} ->
1163            true;
1164        {ok, #file_info{type=directory}} ->
1165            ct:fail("Dir ~p erroneously exists",[Path]);
1166        {ok, #file_info{type=Type}} ->
1167            ct:fail("~p ~p erroneously exists",[Type,Path]);
1168        {error,Error} ->
1169            ct:fail("Unexpected error for ~p: ~p",[Path,Error])
1170    end.
1171
1172%%--------------------------------------------------------------------
1173find_executable(Config) ->
1174    search_executable(proplists:get_value(ftpservers, Config, ?default_ftp_servers)).
1175
1176
1177search_executable([{Name,Paths,_StartCmd,_ChkUp,_StopCommand,_ConfigUpd,_Host,_Port}|Srvrs]) ->
1178    case os_find(Name,Paths) of
1179        false ->
1180            ct:log("~p not found",[Name]),
1181            search_executable(Srvrs);
1182        AbsName ->
1183            ct:comment("Found ~p",[AbsName]),
1184            {ok, {AbsName,_StartCmd,_ChkUp,_StopCommand,_ConfigUpd,_Host,_Port}}
1185    end;
1186search_executable([]) ->
1187    false.
1188
1189
1190os_find(Name, Paths) ->
1191    case os:find_executable(Name, Paths) of
1192        false -> os:find_executable(Name);
1193        AbsName -> AbsName
1194    end.
1195
1196%%%----------------------------------------------------------------
1197start_ftpd(Config0) ->
1198    {AbsName,StartCmd,_ChkUp,_StopCommand,ConfigRewrite,Host,Port} =
1199        proplists:get_value(ftpd_data, Config0),
1200    case StartCmd(Config0, AbsName) of
1201        {ok,StartResult} ->
1202            Config = [{ftpd_host,Host},
1203                      {ftpd_port,Port},
1204                      {ftpd_start_result,StartResult} | ConfigRewrite(Config0)],
1205            Options = case proplists:get_value(ftpd_ssl_implicit, Config) of
1206                true -> [{tls,vsftpd_tls()},{tls_sec_method,ftps}];
1207                _ -> [] % we do not need to test AUTH TLS
1208            end,
1209            try
1210                ftp__close(ftp__open(Config,[{verbose,true}|Options]))
1211            of
1212                Config1 when is_list(Config1) ->
1213                    ct:log("Usable ftp server ~p started on ~p:~p",[AbsName,Host,Port]),
1214                    Config
1215            catch
1216                Class:Exception ->
1217                    ct:log("Ftp server ~p started on ~p:~p but is unusable:~n~p:~p",
1218                           [AbsName,Host,Port,Class,Exception]),
1219                    catch stop_ftpd(Config),
1220                    {skip, [AbsName," started but unusuable"]}
1221            end;
1222        {error,Msg} ->
1223            {skip, [AbsName," not started: ",Msg]}
1224    end.
1225
1226stop_ftpd(Config) ->
1227    {_Name,_StartCmd,_ChkUp,StopCommand,_ConfigUpd,_Host,_Port} = proplists:get_value(ftpd_data, Config),
1228    StopCommand(proplists:get_value(ftpd_start_result,Config)).
1229
1230ftpd_running(Config) ->
1231    {_Name,_StartCmd,ChkUp,_StopCommand,_ConfigUpd,_Host,_Port} = proplists:get_value(ftpd_data, Config),
1232    undefined =/= ChkUp(proplists:get_value(ftpd_start_result,Config)).
1233
1234ftp__open(Config, Options) ->
1235    application:ensure_started(ftp),
1236    Host = proplists:get_value(ftpd_host,Config),
1237    Port = proplists:get_value(ftpd_port,Config),
1238    ct:log("Host=~p, Port=~p",[Host,Port]),
1239    {ok,Pid} = ftp:open(Host, [{port,Port} | Options]),
1240    [{ftp,Pid}|Config].
1241
1242ftp__close(Config) ->
1243    ok = ftp:close(proplists:get_value(ftp,Config)),
1244    Config.
1245
1246ftp_start_service(Config, Options) ->
1247    Host = proplists:get_value(ftpd_host,Config),
1248    Port = proplists:get_value(ftpd_port,Config),
1249    ct:log("Host=~p, Port=~p",[Host,Port]),
1250    {ok,Pid} = ftp:start_service([{host, Host},{port,Port} | Options]),
1251    [{ftp,Pid}|Config].
1252
1253ftp_stop_service(Config) ->
1254    ok = ftp:stop_service(proplists:get_value(ftp,Config)),
1255    Config.
1256
1257split(Cs) -> string:tokens(Cs, "\r\n").
1258
1259find_diff(Bin1, Bin2) ->
1260    case find_diff(Bin1, Bin2, 1) of
1261        {error, {diff,Pos,RC,LC}} ->
1262            ct:log("Contents differ at position ~p.~nOp1: ~p~nOp2: ~p",[Pos,RC,LC]),
1263            ct:fail("Contents differ at pos ~p",[Pos]);
1264        Other ->
1265            Other
1266    end.
1267
1268find_diff(A, A, _) -> true;
1269find_diff(<<H,T1/binary>>, <<H,T2/binary>>, Pos) -> find_diff(T1, T2, Pos+1);
1270find_diff(RC, LC, Pos) -> {error, {diff, Pos, RC, LC}}.
1271
1272set_state(Ops, Config) when is_list(Ops) -> lists:foldl(fun set_state/2, Config, Ops);
1273
1274set_state(reset, Config) ->
1275    rm('*', id2abs("",Config)),
1276    PrivDir = proplists:get_value(priv_dir,Config),
1277    file:set_cwd(PrivDir),
1278    ftp:lcd(proplists:get_value(ftp,Config),PrivDir),
1279    set_state({mkdir,""},Config);
1280set_state({mkdir,Id}, Config) ->
1281    Abs = id2abs(Id, Config),
1282    mk_path(Abs),
1283    file:make_dir(Abs),
1284    Config;
1285set_state({mkfile,Id,Contents}, Config) ->
1286    Abs = id2abs(Id, Config),
1287    mk_path(Abs),
1288    ok = file:write_file(Abs, Contents),
1289    Config.
1290
1291mk_path(Abs) -> lists:foldl(fun mk_path/2, [], filename:split(filename:dirname(Abs))).
1292
1293mk_path(F, Pfx) ->
1294    case file:read_file_info(AbsName=filename:join(Pfx,F)) of
1295        {ok,#file_info{type=directory}} ->
1296            AbsName;
1297        {error,eexist} ->
1298            AbsName;
1299        {error,enoent} ->
1300            ok = file:make_dir(AbsName),
1301            AbsName
1302    end.
1303
1304rm('*', Pfx) ->
1305    {ok,Fs} = file:list_dir(Pfx),
1306    lists:foreach(fun(F) -> rm(F, Pfx) end, Fs);
1307rm(F, Pfx) ->
1308    case file:read_file_info(AbsName=filename:join(Pfx,F)) of
1309        {ok,#file_info{type=directory}} ->
1310            {ok,Fs} = file:list_dir(AbsName),
1311            lists:foreach(fun(F1) -> rm(F1,AbsName) end, Fs),
1312            ok = file:del_dir(AbsName);
1313
1314        {ok,#file_info{type=regular}} ->
1315            ok = file:delete(AbsName);
1316
1317        {error,enoent} ->
1318            ok
1319    end.
1320
1321id2abs(Id, Conf) -> filename:join(proplists:get_value(priv_dir,Conf),ids(Id)).
1322id2ftp(Id, Conf) -> (proplists:get_value(id2ftp,Conf))(ids(Id)).
1323id2ftp_result(Id, Conf) -> (proplists:get_value(id2ftp_result,Conf))(ids(Id)).
1324
1325ids([[_|_]|_]=Ids) -> filename:join(Ids);
1326ids(Id) -> Id.
1327
1328
1329is_expected_absName(Id, File, Conf) -> File = (proplists:get_value(id2abs,Conf))(Id).
1330is_expected_ftpInName(Id, File, Conf) -> File = (proplists:get_value(id2ftp,Conf))(Id).
1331is_expected_ftpOutName(Id, File, Conf) -> File = (proplists:get_value(id2ftp_result,Conf))(Id).
1332
1333
1334%%%----------------------------------------------------------------
1335%%% Help functions for the option '{progress,Progress}'
1336%%%
1337
1338%%%----------------
1339%%% Callback:
1340
1341progress(#progress{} = P, _File, {file_size, Total} = M) ->
1342    ct:pal("Progress: ~p",[M]),
1343    progress_report_receiver ! start,
1344    P#progress{total = Total};
1345
1346progress(#progress{current = Current} = P, _File, {transfer_size, 0} = M) ->
1347    ct:pal("Progress: ~p",[M]),
1348    progress_report_receiver ! finish,
1349    case P#progress.total of
1350        unknown -> P;
1351        Current -> P;
1352        Total   -> ct:fail({error, {progress, {total,Total}, {current,Current}}}),
1353                   P
1354    end;
1355
1356progress(#progress{current = Current} = P, _File, {transfer_size, Size} = M) ->
1357    ct:pal("Progress: ~p",[M]),
1358    progress_report_receiver ! update,
1359    P#progress{current = Current + Size};
1360
1361progress(P, _File, M) ->
1362    ct:pal("Progress **** Strange: ~p",[M]),
1363    P.
1364
1365
1366%%%----------------
1367%%% Help process that counts the files transferred:
1368
1369progress_report_receiver_init(Parent, N) ->
1370    register(progress_report_receiver, self()),
1371    progress_report_receiver_expect_N_files(Parent, N).
1372
1373progress_report_receiver_expect_N_files(_Parent, 0) ->
1374    ct:pal("progress_report got all files!", []);
1375progress_report_receiver_expect_N_files(Parent, N) ->
1376    ct:pal("progress_report expects ~p more files",[N]),
1377    receive
1378        start -> ok
1379    end,
1380    progress_report_receiver_loop(Parent, N-1).
1381
1382
1383progress_report_receiver_loop(Parent, N) ->
1384    ct:pal("progress_report expect update | finish. N = ~p",[N]),
1385    receive
1386        update ->
1387            ct:pal("progress_report got update",[]),
1388            progress_report_receiver_loop(Parent, N);
1389        finish  ->
1390            ct:pal("progress_report got finish, send ~p to ~p",[{self(),ok}, Parent]),
1391            Parent ! {self(), ok},
1392            progress_report_receiver_expect_N_files(Parent, N)
1393    end.
1394
1395%%%----------------------------------------------------------------
1396%%% Help functions for bug OTP-6035
1397
1398unwanted_error_report(LogFile) ->
1399    case file:read_file(LogFile) of
1400        {ok, Bin} ->
1401            nomatch =/= binary:match(Bin, <<"=ERROR REPORT====">>);
1402        _ ->
1403            ct:fail({no_logfile, LogFile})
1404    end.
1405
1406