Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
home:Ledest:erlang:23
erlang
6722-ftp-create-internal-typed-api.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File 6722-ftp-create-internal-typed-api.patch of Package erlang
From 8122362e060919ea73d276b03ae78702adbf9220 Mon Sep 17 00:00:00 2001 From: Kiko Fernandez-Reyes <kiko@erlang.org> Date: Fri, 17 Feb 2023 15:13:57 +0100 Subject: [PATCH 2/2] ftp: create internal typed api removes some type information from module `ftp.erl` because this information should not be needed by users of `ftp`. before this change, users of `ftp` module knew that if they get an error as follows `{error, Reason}`, `Reason` was a specific atom(), i.e., `Reason :: 'ehost' | ...`. users of `ftp` should not rely on the internal atom used, since they are expected to call `ftp:formaterror/1` to understand the reason behind the error, as in any other OTP app. however, this type information can be useful to maintainers of OTP. Thus, `ftp.erl` contains the necessary type information for users of the ftp application. the new module `ftp_internal.erl` has been introduced and contains the actual implementation of the functions from `ftp.erl`, so that `ftp.erl` is now a simple wrapper around `ftp_internal.erl`. `ftp_internal.erl` contains the more precise error types for the type variable `Reason` and also contains the implementation of the `ftp` functions. --- lib/ftp/src/Makefile | 1 + lib/ftp/src/ftp.app.src | 1 + lib/ftp/src/ftp.erl | 2372 ++------------------------------ lib/ftp/src/ftp_internal.erl | 2467 ++++++++++++++++++++++++++++++++++ lib/ftp/src/ftp_sup.erl | 2 +- lib/ftp/test/ftp_SUITE.erl | 4 +- 6 files changed, 2582 insertions(+), 2265 deletions(-) create mode 100644 lib/ftp/src/ftp_internal.erl diff --git a/lib/ftp/src/Makefile b/lib/ftp/src/Makefile index 7f18498b13..ee59f5675c 100644 --- a/lib/ftp/src/Makefile +++ b/lib/ftp/src/Makefile @@ -42,6 +42,7 @@ BEHAVIOUR_MODULES= MODULES= \ ftp \ + ftp_internal \ ftp_app \ ftp_progress \ ftp_response \ diff --git a/lib/ftp/src/ftp.app.src b/lib/ftp/src/ftp.app.src index 1592e7dafa..88b194f5b5 100644 --- a/lib/ftp/src/ftp.app.src +++ b/lib/ftp/src/ftp.app.src @@ -12,6 +12,7 @@ ftp, ftp_app, ftp_progress, + ftp_internal, ftp_response, ftp_sup ]}, diff --git a/lib/ftp/src/ftp.erl b/lib/ftp/src/ftp.erl index 07bc5184e2..6071ce7705 100644 --- a/lib/ftp/src/ftp.erl +++ b/lib/ftp/src/ftp.erl @@ -21,8 +21,6 @@ -module(ftp). --behaviour(gen_server). - -deprecated([{start_service, 1, "use ftp:open/2 instead"}, {stop_service, 1, "use ftp:close/1 instead"}]). @@ -32,8 +30,6 @@ stop_service/1 ]). --export([start_link/1, start_link/2]). - %% API - Client interface -export([cd/2, close/1, delete/2, formaterror/1, lcd/2, lpwd/1, ls/1, ls/2, @@ -50,83 +46,8 @@ append_chunk/2, append_chunk_end/1, append_chunk_start/2, info/1, latest_ctrl_response/1]). -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). - -include("ftp_internal.hrl"). -%% Constants used in internal state definition --define(CONNECTION_TIMEOUT, 60*1000). --define(DATA_ACCEPT_TIMEOUT, infinity). --define(DEFAULT_MODE, passive). --define(PROGRESS_DEFAULT, ignore). --define(FTP_EXT_DEFAULT, false). - -%% Internal Constants --define(FTP_PORT, 21). --define(FTPS_PORT, 990). --define(FILE_BUFSIZE, 4096). - - -%%%========================================================================= -%%% Data Types -%%%========================================================================= - -%% Internal state --record(state, { - csock = undefined, % socket() - Control connection socket - dsock = undefined, % socket() - Data connection socket - tls_options = undefined, % list() - verbose = false, % boolean() - ldir = undefined, % string() - Current local directory - type = ftp_server_default, % atom() - binary | ascii - chunk = false, % boolean() - Receiving data chunks - mode = ?DEFAULT_MODE, % passive | active - timeout = ?CONNECTION_TIMEOUT, % integer() - %% Data received so far on the data connection - data = <<>>, % binary() - %% Data received so far on the control connection - %% {BinStream, AccLines}. If a binary sequence - %% ends with ?CR then keep it in the binary to - %% be able to detect if the next received byte is ?LF - %% and hence the end of the response is reached! - ctrl_data = {<<>>, [], start}, % {binary(), [bytes()], LineStatus} - %% pid() - Client pid (note not the same as "From") - latest_ctrl_response = "", - owner = undefined, - client = undefined, % "From" to be used in gen_server:reply/2 - %% Function that activated a connection and maybe some - %% data needed further on. - caller = undefined, % term() - ipfamily, % inet | inet6 | inet6fb4 - sockopts_ctrl = [], - sockopts_data_passive = [], - sockopts_data_active = [], - progress = ignore, % ignore | pid() - dtimeout = ?DATA_ACCEPT_TIMEOUT, % non_neg_integer() | infinity - tls_ctrl_session_reuse = false, % boolean() - tls_upgrading_data_connection = false, - ftp_extension = ?FTP_EXT_DEFAULT - }). - --record(recv_chunk_closing, { - dconn_closed = false, - pos_compl_received = false, - client_called_us = false - }). - - --type shortage_reason() :: 'etnospc' | 'epnospc'. --type restriction_reason() :: 'epath' | 'efnamena' | 'elogin' | 'enotbinary'. --type common_reason() :: 'econn' | 'eclosed' | term(). --type file_write_error_reason() :: term(). % See file:write for more info - --define(DBG(F,A), 'n/a'). -%%-define(DBG(F,A), io:format(F,A)). -%%-define(DBG(F,A), ct:pal("~p:~p " ++ if is_list(F) -> F; is_atom(F) -> atom_to_list(F) end, [?MODULE,?LINE|A])). - - %%%========================================================================= %%% API %%%========================================================================= @@ -138,23 +59,19 @@ start() -> %% ftp client processes should always be part of ftp supervisor tree. %% We consider it a bug that the "standalone" concept of inets was %% not removed when ftp was broken out, and it is now fixed. +-spec start_service(ServiceConfig) -> {ok, Pid} | {error, Reason} when + ServiceConfig :: [{Property, Value}], + Property :: proplists:property(), + Value :: term(), + Pid :: pid(), + Reason :: term(). start_service(Options) -> - try - {ok, StartOptions} = start_options(Options), - case ftp_sup:start_child([[[{client, self()} | StartOptions], []]]) of - {ok, Pid} -> - call(Pid, {open, ip_comm, Options}, plain); - Error1 -> - Error1 - end - catch - throw:Error2 -> - Error2 - end. + ftp_internal:start_service(Options). stop() -> application:stop(ftp). +-spec stop_service(Pid :: pid() | term()) -> ok. stop_service(Pid) -> close(Pid). @@ -163,17 +80,11 @@ stop_service(Pid) -> %%%========================================================================= %%-------------------------------------------------------------------------- -%% open(HostOrOtpList, <Port>, <Flags>) -> {ok, Pid} | {error, ehost} -%% HostOrOtpList = string() | [{option_list, Options}] -%% Port = integer(), -%% Flags = [Flag], -%% Flag = verbose | debug | trace -%% %% Description: Start an ftp client and connect to a host. %%-------------------------------------------------------------------------- -spec open(Host :: string() | inet:ip_address()) -> - {'ok', Pid :: pid()} | {'error', Reason :: 'ehost' | term()}. + {'ok', Pid :: pid()} | {'error', Reason :: term()}. %% <BACKWARD-COMPATIBILLITY> open({option_list, Options}) when is_list(Options) -> @@ -181,96 +92,74 @@ open({option_list, Options}) when is_list(Options) -> %% </BACKWARD-COMPATIBILLITY> open(Host) -> - open(Host, []). - --spec open(Host :: string() | inet:ip_address(), Opts :: list()) -> - {'ok', Pid :: pid()} | {'error', Reason :: 'ehost' | term()}. - -%% <BACKWARD-COMPATIBILLITY> -open(Host, Port) when is_integer(Port) -> - open(Host, [{port, Port}]); -%% </BACKWARD-COMPATIBILLITY> - -open(Host, Options) when is_list(Options) -> - start_service([{host,Host}|Options]). + ftp_internal:open(Host). + + +-spec open(Host :: string() | inet:ip_address(), Opts) -> + {'ok', Pid :: pid()} | {'error', Reason :: term()} when + Opts :: [Opt], + Opt :: StartOption | OpenOption, + StartOption :: {verbose, Verbose} | {debug, Debug}, + Verbose :: boolean(), + Debug :: disable | debug | trace, + OpenOption :: {ipfamily, IpFamily} | {port, Port :: port()} | {mode, Mode} + | {tls, TLSOptions :: [ssl:tls_option()]} | {tls_sec_method, TLSSecMethod :: ftps | ftpes} + | {tls_ctrl_session_reuse, TLSSessionReuse :: boolean() } | {timeout, Timeout :: timeout()} + | {dtimeout, DTimeout :: timeout()} | {progress, Progress} | {sock_ctrl, SocketCtrls} + | {sock_data_act, [SocketControl]} | {sock_data_pass, [SocketControl]}, + SocketCtrls :: [SocketControl], + IpFamily :: inet | inet6 | inet6fb4, + Mode :: active | passive, + Module :: atom(), + Function :: atom(), + InitialData :: term(), + Progress :: ignore | {Module, Function, InitialData}, + SocketControl :: gen_tcp:option(). +open(Host, Port) -> + ftp_internal:open(Host, Port). %%-------------------------------------------------------------------------- -%% user(Pid, User, Pass, <Acc>) -> ok | {error, euser} | {error, econn} -%% | {error, eacct} -%% Pid = pid(), -%% User = Pass = Acc = string() -%% %% Description: Login with or without a supplied account name. %%-------------------------------------------------------------------------- -spec user(Pid :: pid(), User :: string(), Pass :: string()) -> - 'ok' | {'error', Reason :: 'euser' | common_reason()}. + 'ok' | {'error', Reason :: term()}. user(Pid, User, Pass) -> - case {is_name_sane(User), is_name_sane(Pass)} of - {true, true} -> - call(Pid, {user, User, Pass}, atom); - _ -> - {error, euser} - end. + ftp_internal:user(Pid, User, Pass). -spec user(Pid :: pid(), User :: string(), Pass :: string(), - Acc :: string()) -> - 'ok' | {'error', Reason :: 'euser' | common_reason()}. - -user(Pid, User, Pass, Acc) -> - case {is_name_sane(User), is_name_sane(Pass), is_name_sane(Acc)} of - {true, true, true} -> - call(Pid, {user, User, Pass, Acc}, atom); - _ -> - {error, euser} - end. + Account :: string()) -> + 'ok' | {'error', Reason :: term()}. +user(Pid, User, Pass, Account) -> + ftp_internal:user(Pid, User, Pass, Account). %%-------------------------------------------------------------------------- -%% account(Pid, Acc) -> ok | {error, eacct} -%% Pid = pid() -%% Acc= string() -%% %% Description: Set a user Account. %%-------------------------------------------------------------------------- -spec account(Pid :: pid(), Acc :: string()) -> - 'ok' | {'error', Reason :: 'eacct' | common_reason()}. + 'ok' | {'error', Reason :: term()}. account(Pid, Acc) -> - case is_name_sane(Acc) of - true -> - call(Pid, {account, Acc}, atom); - _ -> - {error, eacct} - end. - + ftp_internal:account(Pid, Acc). %%-------------------------------------------------------------------------- -%% pwd(Pid) -> {ok, Dir} | {error, elogin} | {error, econn} -%% Pid = pid() -%% Dir = string() -%% %% Description: Get the current working directory at remote server. %%-------------------------------------------------------------------------- -spec pwd(Pid :: pid()) -> {'ok', Dir :: string()} | - {'error', Reason :: restriction_reason() | common_reason()}. + {'error', Reason :: term()}. pwd(Pid) -> - call(Pid, pwd, ctrl). - + ftp_internal:pwd(Pid). %%-------------------------------------------------------------------------- -%% lpwd(Pid) -> {ok, Dir} -%% Pid = pid() -%% Dir = string() -%% %% Description: Get the current working directory at local server. %%-------------------------------------------------------------------------- @@ -278,317 +167,179 @@ pwd(Pid) -> {'ok', Dir :: string()}. lpwd(Pid) -> - call(Pid, lpwd, string). + ftp_internal:lpwd(Pid). %%-------------------------------------------------------------------------- -%% cd(Pid, Dir) -> ok | {error, epath} | {error, elogin} | {error, econn} -%% Pid = pid() -%% Dir = string() -%% %% Description: Change current working directory at remote server. %%-------------------------------------------------------------------------- -spec cd(Pid :: pid(), Dir :: string()) -> - 'ok' | {'error', Reason :: restriction_reason() | common_reason()}. + 'ok' | {'error', Reason :: term()}. cd(Pid, Dir) -> - case is_name_sane(Dir) of - true -> - call(Pid, {cd, Dir}, atom); - _ -> - {error, efnamena} - end. - + ftp_internal:cd(Pid, Dir). %%-------------------------------------------------------------------------- -%% lcd(Pid, Dir) -> ok | {error, epath} -%% Pid = pid() -%% Dir = string() -%% %% Description: Change current working directory for the local client. %%-------------------------------------------------------------------------- -spec lcd(Pid :: pid(), Dir :: string()) -> - 'ok' | {'error', Reason :: restriction_reason()}. + 'ok' | {'error', Reason :: term()}. lcd(Pid, Dir) -> - call(Pid, {lcd, Dir}, string). - + ftp_internal:lcd(Pid, Dir). %%-------------------------------------------------------------------------- -%% ls(Pid) -> Result -%% ls(Pid, <Dir>) -> Result -%% -%% Pid = pid() -%% Dir = string() -%% Result = {ok, Listing} | {error, Reason} -%% Listing = string() -%% Reason = epath | elogin | econn -%% %% Description: Returns a list of files in long format. %%-------------------------------------------------------------------------- -spec ls(Pid :: pid()) -> {'ok', Listing :: string()} | - {'error', Reason :: restriction_reason() | common_reason()}. + {'error', Reason :: term()}. ls(Pid) -> ls(Pid, ""). -spec ls(Pid :: pid(), Dir :: string()) -> {'ok', Listing :: string()} | - {'error', Reason :: restriction_reason() | common_reason()}. + {'error', Reason :: term()}. ls(Pid, Dir) -> - case is_name_sane(Dir) of - true -> - call(Pid, {dir, long, Dir}, string); - _ -> - {error, efnamena} - end. + ftp_internal:ls(Pid, Dir). %%-------------------------------------------------------------------------- -%% nlist(Pid) -> Result -%% nlist(Pid, Pathname) -> Result -%% -%% Pid = pid() -%% Pathname = string() -%% Result = {ok, Listing} | {error, Reason} -%% Listing = string() -%% Reason = epath | elogin | econn -%% %% Description: Returns a list of files in short format %%-------------------------------------------------------------------------- -spec nlist(Pid :: pid()) -> {'ok', Listing :: string()} | - {'error', Reason :: restriction_reason() | common_reason()}. + {'error', Reason :: term()}. nlist(Pid) -> nlist(Pid, ""). -spec nlist(Pid :: pid(), Pathname :: string()) -> {'ok', Listing :: string()} | - {'error', Reason :: restriction_reason() | common_reason()}. + {'error', Reason :: term()}. nlist(Pid, Dir) -> - case is_name_sane(Dir) of - true -> - call(Pid, {dir, short, Dir}, string); - _ -> - {error, efnamena} - end. - + ftp_internal:nlist(Pid, Dir). %%-------------------------------------------------------------------------- -%% rename(Pid, Old, New) -> ok | {error, epath} | {error, elogin} -%% | {error, econn} -%% Pid = pid() -%% CurrFile = NewFile = string() -%% %% Description: Rename a file at remote server. %%-------------------------------------------------------------------------- -spec rename(Pid :: pid(), Old :: string(), New :: string()) -> - 'ok' | {'error', Reason :: restriction_reason() | common_reason()}. + 'ok' | {'error', Reason :: term()}. rename(Pid, Old, New) -> - case {is_name_sane(Old), is_name_sane(New)} of - {true, true} -> - call(Pid, {rename, Old, New}, string); - _ -> - {error, efnamena} - end. - + ftp_internal:rename(Pid, Old, New). %%-------------------------------------------------------------------------- -%% delete(Pid, File) -> ok | {error, epath} | {error, elogin} | -%% {error, econn} -%% Pid = pid() -%% File = string() -%% %% Description: Remove file at remote server. %%-------------------------------------------------------------------------- -spec delete(Pid :: pid(), File :: string()) -> - 'ok' | {'error', Reason :: restriction_reason() | common_reason()}. + 'ok' | {'error', Reason :: term()}. delete(Pid, File) -> - case is_name_sane(File) of - true -> - call(Pid, {delete, File}, string); - _ -> - {error, efnamena} - end. - + ftp_internal:delete(Pid, File). %%-------------------------------------------------------------------------- -%% mkdir(Pid, Dir) -> ok | {error, epath} | {error, elogin} | {error, econn} -%% Pid = pid(), -%% Dir = string() -%% %% Description: Make directory at remote server. %%-------------------------------------------------------------------------- -spec mkdir(Pid :: pid(), Dir :: string()) -> - 'ok' | {'error', Reason :: restriction_reason() | common_reason()}. + 'ok' | {'error', Reason :: term()}. mkdir(Pid, Dir) -> - case is_name_sane(Dir) of - true -> - call(Pid, {mkdir, Dir}, atom); - _ -> - {error, efnamena} - end. - + ftp_internal:mkdir(Pid, Dir). %%-------------------------------------------------------------------------- -%% rmdir(Pid, Dir) -> ok | {error, epath} | {error, elogin} | {error, econn} -%% Pid = pid(), -%% Dir = string() -%% %% Description: Remove directory at remote server. %%-------------------------------------------------------------------------- -spec rmdir(Pid :: pid(), Dir :: string()) -> - 'ok' | {'error', Reason :: restriction_reason() | common_reason()}. + 'ok' | {'error', Reason :: term()}. rmdir(Pid, Dir) -> - case is_name_sane(Dir) of - true -> - call(Pid, {rmdir, Dir}, atom); - _ -> - {error, efnamena} - end. - + ftp_internal:rmdir(Pid, Dir). %%-------------------------------------------------------------------------- -%% type(Pid, Type) -> ok | {error, etype} | {error, elogin} | {error, econn} -%% Pid = pid() -%% Type = ascii | binary -%% %% Description: Set transfer type. %%-------------------------------------------------------------------------- -spec type(Pid :: pid(), Type :: ascii | binary) -> - 'ok' | - {'error', Reason :: 'etype' | restriction_reason() | common_reason()}. + 'ok' | {'error', Reason :: term()}. type(Pid, Type) -> - call(Pid, {type, Type}, atom). + ftp_internal:type(Pid, Type). %%-------------------------------------------------------------------------- -%% recv(Pid, RemoteFileName [, LocalFileName]) -> ok | {error, epath} | -%% {error, elogin} | {error, econn} -%% Pid = pid() -%% RemoteFileName = LocalFileName = string() -%% %% Description: Transfer file from remote server. %%-------------------------------------------------------------------------- -spec recv(Pid :: pid(), RemoteFileName :: string()) -> - 'ok' | {'error', Reason :: restriction_reason() | - common_reason() | - file_write_error_reason()}. + 'ok' | {'error', Reason :: term()}. -recv(Pid, RemotFileName) -> - recv(Pid, RemotFileName, RemotFileName). +recv(Pid, RemoteFileName) -> + ftp_internal:recv(Pid, RemoteFileName). -spec recv(Pid :: pid(), RemoteFileName :: string(), LocalFileName :: string()) -> 'ok' | {'error', Reason :: term()}. -recv(Pid, RemotFileName, LocalFileName) -> - case is_name_sane(RemotFileName) of - true -> - call(Pid, {recv, RemotFileName, LocalFileName}, atom); - _ -> - {error, efnamena} - end. +recv(Pid, RemoteFileName, LocalFileName) -> + ftp_internal:recv(Pid, RemoteFileName, LocalFileName). %%-------------------------------------------------------------------------- -%% recv_bin(Pid, RemoteFile) -> {ok, Bin} | {error, epath} | {error, elogin} -%% | {error, econn} -%% Pid = pid() -%% RemoteFile = string() -%% Bin = binary() -%% %% Description: Transfer file from remote server into binary. %%-------------------------------------------------------------------------- -spec recv_bin(Pid :: pid(), RemoteFile :: string()) -> - {'ok', Bin :: binary()} | - {'error', Reason :: restriction_reason() | common_reason()}. + {'ok', Bin :: binary()} | {'error', Reason :: term()}. recv_bin(Pid, RemoteFile) -> - case is_name_sane(RemoteFile) of - true -> - call(Pid, {recv_bin, RemoteFile}, bin); - _ -> - {error, efnamena} - end. + ftp_internal:recv_bin(Pid, RemoteFile). %%-------------------------------------------------------------------------- -%% recv_chunk_start(Pid, RemoteFile) -> ok | {error, elogin} | {error, epath} -%% | {error, econn} -%% Pid = pid() -%% RemoteFile = string() -%% %% Description: Start receive of chunks of remote file. %%-------------------------------------------------------------------------- -spec recv_chunk_start(Pid :: pid(), RemoteFile :: string()) -> - 'ok' | {'error', Reason :: restriction_reason() | common_reason()}. + 'ok' | {'error', Reason :: term()}. recv_chunk_start(Pid, RemoteFile) -> - case is_name_sane(RemoteFile) of - true -> - call(Pid, {recv_chunk_start, RemoteFile}, atom); - _ -> - {error, efnamena} - end. + ftp_internal:recv_chunk_start(Pid, RemoteFile). %%-------------------------------------------------------------------------- -%% recv_chunk(Pid, RemoteFile) -> ok | {ok, Bin} | {error, Reason} -%% Pid = pid() -%% RemoteFile = string() -%% %% Description: Transfer file from remote server into binary in chunks %%-------------------------------------------------------------------------- -spec recv_chunk(Pid :: pid()) -> 'ok' | {'ok', Bin :: binary()} | - {'error', Reason :: restriction_reason() | common_reason()}. + {'error', Reason :: term()}. recv_chunk(Pid) -> - call(Pid, recv_chunk, atom). + ftp_internal:recv_chunk(Pid). %%-------------------------------------------------------------------------- -%% send(Pid, LocalFileName [, RemotFileName]) -> ok | {error, epath} -%% | {error, elogin} -%% | {error, econn} -%% Pid = pid() -%% LocalFileName = RemotFileName = string() -%% %% Description: Transfer file to remote server. %%-------------------------------------------------------------------------- -spec send(Pid :: pid(), LocalFileName :: string()) -> - 'ok' | - {'error', Reason :: restriction_reason() | - common_reason() | - shortage_reason()}. + 'ok' | {'error', Reason :: term()}. send(Pid, LocalFileName) -> send(Pid, LocalFileName, LocalFileName). @@ -596,74 +347,34 @@ send(Pid, LocalFileName) -> -spec send(Pid :: pid(), LocalFileName :: string(), RemoteFileName :: string()) -> - 'ok' | - {'error', Reason :: restriction_reason() | - common_reason() | - shortage_reason()}. + 'ok' | {'error', Reason :: term()}. send(Pid, LocalFileName, RemotFileName) -> - case is_name_sane(RemotFileName) of - true -> - call(Pid, {send, LocalFileName, RemotFileName}, atom); - _ -> - {error, efnamena} - end. + ftp_internal:send(Pid, LocalFileName, RemotFileName). %%-------------------------------------------------------------------------- -%% send_bin(Pid, Bin, RemoteFile) -> ok | {error, epath} | {error, elogin} -%% | {error, enotbinary} | {error, econn} -%% Pid = pid() -%% Bin = binary() -%% RemoteFile = string() -%% %% Description: Transfer a binary to a remote file. %%-------------------------------------------------------------------------- -spec send_bin(Pid :: pid(), Bin :: binary(), RemoteFile :: string()) -> - 'ok' | - {'error', Reason :: restriction_reason() | - common_reason() | - shortage_reason()}. - -send_bin(Pid, Bin, RemoteFile) when is_binary(Bin) -> - case is_name_sane(RemoteFile) of - true -> - call(Pid, {send_bin, Bin, RemoteFile}, atom); - _ -> - {error, efnamena} - end; -send_bin(_Pid, _Bin, _RemoteFile) -> - {error, enotbinary}. - - -%%-------------------------------------------------------------------------- -%% send_chunk_start(Pid, RemoteFile) -> ok | {error, elogin} | {error, epath} -%% | {error, econn} -%% Pid = pid() -%% RemoteFile = string() -%% + 'ok' | {'error', Reason :: term()}. + +send_bin(Pid, Bin, RemoteFile) -> + ftp_internal:send_bin(Pid, Bin, RemoteFile). + + +%%-------------------------------------------------------------------------- %% Description: Start transfer of chunks to remote file. %%-------------------------------------------------------------------------- -spec send_chunk_start(Pid :: pid(), RemoteFile :: string()) -> - 'ok' | {'error', Reason :: restriction_reason() | common_reason()}. + 'ok' | {'error', Reason :: term()}. send_chunk_start(Pid, RemoteFile) -> - case is_name_sane(RemoteFile) of - true -> - call(Pid, {send_chunk_start, RemoteFile}, atom); - _ -> - {error, efnamena} - end. - + ftp_internal:send_chunk_start(Pid, RemoteFile). %%-------------------------------------------------------------------------- -%% append_chunk_start(Pid, RemoteFile) -> ok | {error, elogin} | -%% {error, epath} | {error, econn} -%% Pid = pid() -%% RemoteFile = string() -%% %% Description: Start append chunks of data to remote file. %%-------------------------------------------------------------------------- @@ -671,109 +382,58 @@ send_chunk_start(Pid, RemoteFile) -> 'ok' | {'error', Reason :: term()}. append_chunk_start(Pid, RemoteFile) -> - case is_name_sane(RemoteFile) of - true -> - call(Pid, {append_chunk_start, RemoteFile}, atom); - _ -> - {error, efnamena} - end. + ftp_internal:append_chunk_start(Pid, RemoteFile). %%-------------------------------------------------------------------------- -%% send_chunk(Pid, Bin) -> ok | {error, elogin} | {error, enotbinary} -%% | {error, echunk} | {error, econn} -%% Pid = pid() -%% Bin = binary(). -%% %% Purpose: Send chunk to remote file. %%-------------------------------------------------------------------------- -spec send_chunk(Pid :: pid(), Bin :: binary()) -> - 'ok' | - {'error', Reason :: 'echunk' | - restriction_reason() | - common_reason()}. - -send_chunk(Pid, Bin) when is_binary(Bin) -> - call(Pid, {transfer_chunk, Bin}, atom); -send_chunk(_Pid, _Bin) -> - {error, enotbinary}. + 'ok' | {'error', Reason :: term()}. +send_chunk(Pid, Bin) -> + ftp_internal:send_chunk(Pid, Bin). %%-------------------------------------------------------------------------- -%% append_chunk(Pid, Bin) -> ok | {error, elogin} | {error, enotbinary} -%% | {error, echunk} | {error, econn} -%% Pid = pid() -%% Bin = binary() -%% %% Description: Append chunk to remote file. %%-------------------------------------------------------------------------- -spec append_chunk(Pid :: pid(), Bin :: binary()) -> - 'ok' | - {'error', Reason :: 'echunk' | - restriction_reason() | - common_reason()}. + 'ok' | {'error', Reason :: term()}. -append_chunk(Pid, Bin) when is_binary(Bin) -> - call(Pid, {transfer_chunk, Bin}, atom); -append_chunk(_Pid, _Bin) -> - {error, enotbinary}. +append_chunk(Pid, Bin) -> + ftp_internal:append_chunk(Pid, Bin). %%-------------------------------------------------------------------------- -%% send_chunk_end(Pid) -> ok | {error, elogin} | {error, echunk} -%% | {error, econn} -%% Pid = pid() -%% %% Description: End sending of chunks to remote file. %%-------------------------------------------------------------------------- -spec send_chunk_end(Pid :: pid()) -> - 'ok' | - {'error', Reason :: restriction_reason() | - common_reason() | - shortage_reason()}. + 'ok' | {'error', Reason :: term()}. send_chunk_end(Pid) -> - call(Pid, chunk_end, atom). + ftp_internal:send_chunk_end(Pid). %%-------------------------------------------------------------------------- -%% append_chunk_end(Pid) -> ok | {error, elogin} | {error, echunk} -%% | {error, econn} -%% Pid = pid() -%% %% Description: End appending of chunks to remote file. %%-------------------------------------------------------------------------- -spec append_chunk_end(Pid :: pid()) -> - 'ok' | - {'error', Reason :: restriction_reason() | - common_reason() | - shortage_reason()}. + 'ok' | {'error', Reason :: term()}. append_chunk_end(Pid) -> - call(Pid, chunk_end, atom). + ftp_internal:append_chunk_end(Pid). %%-------------------------------------------------------------------------- -%% append(Pid, LocalFileName [, RemotFileName]) -> ok | {error, epath} -%% | {error, elogin} -%% | {error, econn} -%% Pid = pid() -%% LocalFileName = RemotFileName = string() -%% %% Description: Append the local file to the remote file %%-------------------------------------------------------------------------- -spec append(Pid :: pid(), LocalFileName :: string()) -> - 'ok' | - {'error', Reason :: 'epath' | - 'elogin' | - 'etnospc' | - 'epnospc' | - 'efnamena' | common_reason()}. + 'ok' | {'error', Reason :: term()}. append(Pid, LocalFileName) -> append(Pid, LocalFileName, LocalFileName). @@ -784,1873 +444,61 @@ append(Pid, LocalFileName) -> 'ok' | {'error', Reason :: term()}. append(Pid, LocalFileName, RemotFileName) -> - case is_name_sane(RemotFileName) of - true -> - call(Pid, {append, LocalFileName, RemotFileName}, atom); - _ -> - {error, efnamena} - end. + ftp_internal:append(Pid, LocalFileName, RemotFileName). %%-------------------------------------------------------------------------- -%% append_bin(Pid, Bin, RemoteFile) -> ok | {error, epath} | {error, elogin} -%% | {error, enotbinary} | {error, econn} -%% Pid = pid() -%% Bin = binary() -%% RemoteFile = string() -%% %% Purpose: Append a binary to a remote file. %%-------------------------------------------------------------------------- -spec append_bin(Pid :: pid(), Bin :: binary(), RemoteFile :: string()) -> - 'ok' | - {'error', Reason :: restriction_reason() | - common_reason() | - shortage_reason()}. + 'ok' | {'error', Reason :: term()}. -append_bin(Pid, Bin, RemoteFile) when is_binary(Bin) -> - case is_name_sane(RemoteFile) of - true -> - call(Pid, {append_bin, Bin, RemoteFile}, atom); - _ -> - {error, efnamena} - end; -append_bin(_Pid, _Bin, _RemoteFile) -> - {error, enotbinary}. +append_bin(Pid, Bin, RemoteFile) -> + ftp_internal:append_bin(Pid, Bin, RemoteFile). %%-------------------------------------------------------------------------- -%% quote(Pid, Cmd) -> list() -%% Pid = pid() -%% Cmd = string() -%% %% Description: Send arbitrary ftp command. %%-------------------------------------------------------------------------- --spec quote(Pid :: pid(), Cmd :: string()) -> list(). +-spec quote(Pid :: pid(), Cmd :: string()) -> [FTPLine :: string()]. quote(Pid, Cmd) when is_list(Cmd) -> - call(Pid, {quote, Cmd}, atom). + ftp_internal:quote(Pid, Cmd). %%-------------------------------------------------------------------------- -%% close(Pid) -> ok -%% Pid = pid() -%% %% Description: End the ftp session. %%-------------------------------------------------------------------------- -spec close(Pid :: pid()) -> 'ok'. - close(Pid) -> - cast(Pid, close), - ok. + ftp_internal:close(Pid). %%-------------------------------------------------------------------------- -%% formaterror(Tag) -> string() -%% Tag = atom() | {error, atom()} -%% %% Description: Return diagnostics. %%-------------------------------------------------------------------------- --spec formaterror(Tag :: term()) -> string(). +-spec formaterror(Tag :: atom() | {error, atom()}) -> string(). formaterror(Tag) -> ftp_response:error_string(Tag). info(Pid) -> - call(Pid, info, list). + ftp_internal:info(Pid). %%-------------------------------------------------------------------------- -%% latest_ctrl_response(Pid) -> string() -%% Pid = pid() -%% %% Description: The latest received response from the server %%-------------------------------------------------------------------------- -spec latest_ctrl_response(Pid :: pid()) -> string(). latest_ctrl_response(Pid) -> - call(Pid, latest_ctrl_response, string). - - -%%%======================================================================== -%%% gen_server callback functions -%%%======================================================================== - -%%------------------------------------------------------------------------- -%% init(Args) -> {ok, State} | {ok, State, Timeout} | {stop, Reason} -%% Description: Initiates the erlang process that manages a ftp connection. -%%------------------------------------------------------------------------- -init(Options) -> - process_flag(trap_exit, true), - - %% Keep track of the client - {value, {client, Client}} = lists:keysearch(client, 1, Options), - erlang:monitor(process, Client), - - %% Make sure inet is started - _ = inet_db:start(), - - %% Where are we - {ok, Dir} = file:get_cwd(), - - %% Maybe activate dbg - case key_search(debug, Options, disable) of - trace -> - dbg:tracer(), - dbg:p(all, [call]), - {ok, _} = dbg:tpl(ftp, [{'_', [], [{return_trace}]}]), - {ok, _} = dbg:tpl(ftp_response, [{'_', [], [{return_trace}]}]), - {ok, _} = dbg:tpl(ftp_progress, [{'_', [], [{return_trace}]}]), - ok; - debug -> - dbg:tracer(), - dbg:p(all, [call]), - {ok, _} = dbg:tp(ftp, [{'_', [], [{return_trace}]}]), - {ok, _} = dbg:tp(ftp_response, [{'_', [], [{return_trace}]}]), - {ok, _} = dbg:tp(ftp_progress, [{'_', [], [{return_trace}]}]), - ok; - _ -> - %% Keep silent - ok - end, - - %% Verbose? - Verbose = key_search(verbose, Options, false), - - %% IpFamily? - IpFamily = key_search(ipfamily, Options, inet), - - State = #state{owner = Client, - verbose = Verbose, - ipfamily = IpFamily, - ldir = Dir}, - - %% Set process prio - Priority = key_search(priority, Options, low), - process_flag(priority, Priority), - - %% And we are done - {ok, State}. - - -%%-------------------------------------------------------------------------- -%% handle_call(Request, From, State) -> {reply, Reply, State} | -%% {reply, Reply, State, Timeout} | -%% {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, Reply, State} | -%% Description: Handle incoming requests. -%%------------------------------------------------------------------------- - -%% Anyone can ask this question -handle_call({_, info}, _, #state{verbose = Verbose, - mode = Mode, - timeout = Timeout, - ipfamily = IpFamily, - csock = Socket, - progress = Progress} = State) -> - {ok, {_, LocalPort}} = sockname(Socket), - {ok, {Address, Port}} = peername(Socket), - Options = [{verbose, Verbose}, - {ipfamily, IpFamily}, - {mode, Mode}, - {peer, Address}, - {peer_port, Port}, - {local_port, LocalPort}, - {timeout, Timeout}, - {progress, Progress}], - {reply, {ok, Options}, State}; - -handle_call({_,latest_ctrl_response}, _, #state{latest_ctrl_response=Resp} = State) -> - {reply, {ok,Resp}, State}; - -%% But everything else must come from the owner -handle_call({Pid, _}, _, #state{owner = Owner} = State) when Owner =/= Pid -> - {reply, {error, not_connection_owner}, State}; - -handle_call({_, {open, ip_comm, Options}}, From, State) -> - {ok, Opts} = open_options(Options), - - case key_search(host, Opts, undefined) of - undefined -> - {stop, normal, {error, ehost}, State}; - Host -> - TLSSecMethod = key_search(tls_sec_method, Opts, undefined), - TLSOpts = key_search(tls, Opts, undefined), - TLSReuse = key_search(tls_ctrl_session_reuse, Opts, false), - Mode = key_search(mode, Opts, ?DEFAULT_MODE), - Port0 = key_search(port, Opts, 0), - Port = if Port0 == 0, TLSSecMethod == ftps -> ?FTPS_PORT; Port0 == 0 -> ?FTP_PORT; true -> Port0 end, - Timeout = key_search(timeout, Opts, ?CONNECTION_TIMEOUT), - DTimeout = key_search(dtimeout, Opts, ?DATA_ACCEPT_TIMEOUT), - Progress = key_search(progress, Opts, ignore), - IpFamily = key_search(ipfamily, Opts, inet), - FtpExt = key_search(ftp_extension, Opts, ?FTP_EXT_DEFAULT), - - {ok, {CtrlOpts, DataPassOpts, DataActOpts}} = socket_options(Options), - - State2 = State#state{client = From, - mode = Mode, - progress = progress(Progress), - ipfamily = IpFamily, - sockopts_ctrl = CtrlOpts, - sockopts_data_passive = DataPassOpts, - sockopts_data_active = DataActOpts, - timeout = Timeout, - dtimeout = DTimeout, - ftp_extension = FtpExt}, - - case setup_ctrl_connection(Host, Port, Timeout, State2) of - {ok, State3, WaitTimeout} when is_list(TLSOpts), TLSSecMethod == ftps -> - handle_ctrl_result({tls_upgrade, TLSSecMethod}, - State3#state{tls_options = TLSOpts, - tls_ctrl_session_reuse = TLSReuse, - timeout = WaitTimeout }); - {ok, State3, WaitTimeout} when is_list(TLSOpts) -> - {noreply, State3#state{tls_options = TLSOpts, tls_ctrl_session_reuse = TLSReuse }, WaitTimeout}; - {ok, State3, WaitTimeout} -> - {noreply, State3, WaitTimeout}; - {error, _Reason} -> - gen_server:reply(From, {error, ehost}), - {stop, normal, State2#state{client = undefined}} - end - end; - -handle_call({_, {user, User, Password}}, From, - #state{csock = CSock} = State) when (CSock =/= undefined) -> - handle_user(User, Password, "", State#state{client = From}); - -handle_call({_, {user, User, Password, Acc}}, From, - #state{csock = CSock} = State) when (CSock =/= undefined) -> - handle_user(User, Password, Acc, State#state{client = From}); - -handle_call({_, {account, Acc}}, From, State)-> - handle_user_account(Acc, State#state{client = From}); - -handle_call({_, pwd}, From, #state{chunk = false} = State0) -> - _ = send_ctrl_message(State0, mk_cmd("PWD", [])), - State = activate_ctrl_connection(State0), - {noreply, State#state{client = From, caller = pwd}}; - -handle_call({_, lpwd}, From, #state{ldir = LDir} = State) -> - {reply, {ok, LDir}, State#state{client = From}}; - -handle_call({_, {cd, Dir}}, From, #state{chunk = false} = State0) -> - _ = send_ctrl_message(State0, mk_cmd("CWD ~s", [Dir])), - State = activate_ctrl_connection(State0), - {noreply, State#state{client = From, caller = cd}}; - -handle_call({_,{lcd, Dir}}, _From, #state{ldir = LDir0} = State) -> - LDir = filename:absname(Dir, LDir0), - case file:read_file_info(LDir) of %% FIX better check that LDir is a dir. - {ok, _ } -> - {reply, ok, State#state{ldir = LDir}}; - _ -> - {reply, {error, epath}, State} - end; - -handle_call({_, {dir, Len, Dir}}, {_Pid, _} = From, - #state{chunk = false} = State) -> - setup_data_connection(State#state{caller = {dir, Dir, Len}, - client = From}); -handle_call({_, {rename, CurrFile, NewFile}}, From, - #state{chunk = false} = State0) -> - _ = send_ctrl_message(State0, mk_cmd("RNFR ~s", [CurrFile])), - State = activate_ctrl_connection(State0), - {noreply, State#state{caller = {rename, NewFile}, client = From}}; - -handle_call({_, {delete, File}}, {_Pid, _} = From, - #state{chunk = false} = State0) -> - _ = send_ctrl_message(State0, mk_cmd("DELE ~s", [File])), - State = activate_ctrl_connection(State0), - {noreply, State#state{client = From}}; - -handle_call({_, {mkdir, Dir}}, From, #state{chunk = false} = State0) -> - _ = send_ctrl_message(State0, mk_cmd("MKD ~s", [Dir])), - State = activate_ctrl_connection(State0), - {noreply, State#state{client = From}}; - -handle_call({_,{rmdir, Dir}}, From, #state{chunk = false} = State0) -> - _ = send_ctrl_message(State0, mk_cmd("RMD ~s", [Dir])), - State = activate_ctrl_connection(State0), - {noreply, State#state{client = From}}; - -handle_call({_,{type, Type}}, From, #state{chunk = false} = State0) -> - case Type of - ascii -> - _ = send_ctrl_message(State0, mk_cmd("TYPE A", [])), - State = activate_ctrl_connection(State0), - {noreply, State#state{caller = type, type = ascii, - client = From}}; - binary -> - _ = send_ctrl_message(State0, mk_cmd("TYPE I", [])), - State = activate_ctrl_connection(State0), - {noreply, State#state{caller = type, type = binary, - client = From}}; - _ -> - {reply, {error, etype}, State0} - end; -handle_call({_,{recv, RemoteFile, LocalFile}}, From, - #state{chunk = false, ldir = LocalDir} = State) -> - progress_report({remote_file, RemoteFile}, State), - NewLocalFile = filename:absname(LocalFile, LocalDir), - - case file_open(NewLocalFile, write) of - {ok, Fd} -> - setup_data_connection(State#state{client = From, - caller = - {recv_file, - RemoteFile, Fd}}); - {error, _What} -> - {reply, {error, epath}, State} - end; -handle_call({_, {recv_bin, RemoteFile}}, From, #state{chunk = false} = - State) -> - setup_data_connection(State#state{caller = {recv_bin, RemoteFile}, - client = From}); -handle_call({_,{recv_chunk_start, RemoteFile}}, From, #state{chunk = false} - = State) -> - setup_data_connection(State#state{caller = {start_chunk_transfer, - "RETR", RemoteFile}, - client = From}); - -handle_call({_, recv_chunk}, _, #state{chunk = false} = State) -> - {reply, {error, "ftp:recv_chunk_start/2 not called"}, State}; -handle_call({_, recv_chunk}, _From, #state{chunk = true, - data = Bin, - caller = #recv_chunk_closing{dconn_closed = true, - pos_compl_received = true, - client_called_us = true - } - } = State0) -> - case Bin of - <<>> -> - {reply, ok, State0#state{caller = undefined, - chunk = false, - client = undefined}}; - Data -> - {reply, Data, State0#state{caller = undefined, - chunk = false, - client = undefined}} - end; -handle_call({_, recv_chunk}, _From, #state{chunk = true, - caller = #recv_chunk_closing{dconn_closed = true, - pos_compl_received = true - } - } = State0) -> - %% The ftp:recv_chunk call was the last event we waited for, finnish and clean up - ?DBG("Data connection closed recv_chunk_closing ftp:recv_chunk, last event",[]), - State = activate_ctrl_connection(State0), - {reply, ok, State#state{caller = undefined, - chunk = false, - client = undefined}}; -handle_call({_, recv_chunk}, From, #state{chunk = true, - caller = #recv_chunk_closing{pos_compl_received = true - } = R - } = State0) -> - State = activate_data_connection(State0), - {noreply, State#state{client = From, caller = R#recv_chunk_closing{client_called_us=true}}}; - -handle_call({_, recv_chunk}, From, #state{chunk = true, - caller = #recv_chunk_closing{} = R - } = State) -> - %% Waiting for more, don't care what - ?DBG("recv_chunk_closing ftp:recv_chunk, get more",[]), - {noreply, State#state{client = From, caller = R#recv_chunk_closing{client_called_us=true}}}; - -handle_call({_, recv_chunk}, From, #state{chunk = true} = State0) -> - State = activate_data_connection(State0), - {noreply, State#state{client = From, caller = recv_chunk}}; - -handle_call({_, {send, LocalFile, RemoteFile}}, From, - #state{chunk = false, ldir = LocalDir} = State) -> - progress_report({local_file, filename:absname(LocalFile, LocalDir)}, - State), - setup_data_connection(State#state{caller = {transfer_file, - {"STOR", - LocalFile, RemoteFile}}, - client = From}); -handle_call({_, {append, LocalFile, RemoteFile}}, From, - #state{chunk = false} = State) -> - setup_data_connection(State#state{caller = {transfer_file, - {"APPE", - LocalFile, RemoteFile}}, - client = From}); -handle_call({_, {send_bin, Bin, RemoteFile}}, From, - #state{chunk = false} = State) -> - setup_data_connection(State#state{caller = {transfer_data, - {"STOR", Bin, RemoteFile}}, - client = From}); -handle_call({_,{append_bin, Bin, RemoteFile}}, From, - #state{chunk = false} = State) -> - setup_data_connection(State#state{caller = {transfer_data, - {"APPE", Bin, RemoteFile}}, - client = From}); -handle_call({_, {send_chunk_start, RemoteFile}}, From, #state{chunk = false} - = State) -> - setup_data_connection(State#state{caller = {start_chunk_transfer, - "STOR", RemoteFile}, - client = From}); -handle_call({_, {append_chunk_start, RemoteFile}}, From, #state{chunk = false} - = State) -> - setup_data_connection(State#state{caller = {start_chunk_transfer, - "APPE", RemoteFile}, - client = From}); -handle_call({_, {transfer_chunk, Bin}}, _, #state{chunk = true} = State) -> - send_data_message(State, Bin), - {reply, ok, State}; - -handle_call({_, {transfer_chunk, _}}, _, #state{chunk = false} = State) -> - {reply, {error, echunk}, State}; - -handle_call({_, chunk_end}, From, #state{chunk = true} = State0) -> - close_data_connection(State0), - State = activate_ctrl_connection(State0), - {noreply, State#state{client = From, dsock = undefined, - caller = end_chunk_transfer, chunk = false}}; - -handle_call({_, chunk_end}, _, #state{chunk = false} = State) -> - {reply, {error, echunk}, State}; - -handle_call({_, {quote, Cmd}}, From, #state{chunk = false} = State0) -> - _ = send_ctrl_message(State0, mk_cmd(Cmd, [])), - State = activate_ctrl_connection(State0), - {noreply, State#state{client = From, caller = quote}}; - -handle_call({_, _Req}, _From, #state{csock = CSock} = State) - when (CSock =:= undefined) -> - {reply, {error, not_connected}, State}; - -handle_call(_, _, #state{chunk = true} = State) -> - {reply, {error, echunk}, State}; - -%% Catch all - This can only happen if the application programmer writes -%% really bad code that violates the API. -handle_call(Request, _Timeout, State) -> - {stop, {'API_violation_connection_closed', Request}, - {error, {connection_terminated, 'API_violation'}}, State}. - -%%-------------------------------------------------------------------------- -%% handle_cast(Request, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handles cast messages. -%%------------------------------------------------------------------------- -handle_cast({Pid, close}, #state{owner = Pid} = State) -> - _ = send_ctrl_message(State, mk_cmd("QUIT", [])), - close_ctrl_connection(State), - close_data_connection(State), - {stop, normal, State#state{csock = undefined, dsock = undefined}}; - -handle_cast({Pid, close}, State) -> - Report = io_lib:format("A none owner process ~p tried to close an " - "ftp connection: ~n", [Pid]), - error_logger:info_report(Report), - {noreply, State}; - -%% Catch all - This can only happen if the application programmer writes -%% really bad code that violates the API. -handle_cast(Msg, State) -> - {stop, {'API_violation_connection_closed', Msg}, State}. - -%%-------------------------------------------------------------------------- -%% handle_info(Msg, State) -> {noreply, State} | {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handles tcp messages from the ftp-server. -%% Note: The order of the function clauses is significant. -%%-------------------------------------------------------------------------- - -handle_info(timeout, #state{caller = open} = State) -> - {stop, timeout, State}; - -handle_info(timeout, State) -> - {noreply, State}; - -%%% Data socket messages %%% -handle_info({Trpt, Socket, Data}, - #state{dsock = {Trpt,Socket}, - caller = {recv_file, Fd}} = State0) when Trpt==tcp;Trpt==ssl -> - ?DBG('L~p --data ~p ----> ~s~p~n',[?LINE,Socket,Data,State0]), - ok = file_write(binary_to_list(Data), Fd), - progress_report({binary, Data}, State0), - State = activate_data_connection(State0), - {noreply, State}; - -handle_info({Trpt, Socket, Data}, #state{dsock = {Trpt,Socket}, client = From, - caller = recv_chunk} - = State) when Trpt==tcp;Trpt==ssl -> - ?DBG('L~p --data ~p ----> ~s~p~n',[?LINE,Socket,Data,State]), - gen_server:reply(From, {ok, Data}), - {noreply, State#state{client = undefined, caller = undefined, data = <<>>}}; - -handle_info({Trpt, Socket, Data}, #state{dsock = {Trpt,Socket}} = State0) when Trpt==tcp;Trpt==ssl -> - ?DBG('L~p --data ~p ----> ~s~p~n',[?LINE,Socket,Data,State0]), - State = activate_data_connection(State0), - {noreply, State#state{data = <<(State#state.data)/binary, - Data/binary>>}}; - -handle_info({Cls, Socket}, #state{dsock = {Trpt,Socket}, - caller = {recv_file, Fd}} = State0) - when {Cls,Trpt}=={tcp_closed,tcp} ; {Cls,Trpt}=={ssl_closed,ssl} -> - file_close(Fd), - progress_report({transfer_size, 0}, State0), - State = activate_ctrl_connection(State0), - ?DBG("Data channel close",[]), - {noreply, State#state{dsock = undefined, data = <<>>}}; - -handle_info({Cls, Socket}, #state{dsock = {Trpt,Socket}, - client = Client, - caller = recv_chunk} = State0) - when {Cls,Trpt}=={tcp_closed,tcp} ; {Cls,Trpt}=={ssl_closed,ssl} -> - ?DBG("Data channel close recv_chunk",[]), - State = activate_ctrl_connection(State0), - {noreply, State#state{dsock = undefined, - caller = #recv_chunk_closing{dconn_closed = true, - client_called_us = Client =/= undefined} - }}; -handle_info({Cls, Socket}, #state{dsock = {Trpt,Socket}, - caller = #recv_chunk_closing{client_called_us = true, - pos_compl_received = true} = R} = State) - when {Cls,Trpt}=={tcp_closed,tcp} ; {Cls,Trpt}=={ssl_closed,ssl} -> - %% Maybe handle unprocessed chunk message before acking final chunk - self() ! {Cls, Socket}, - {noreply, State#state{caller = R#recv_chunk_closing{dconn_closed = true}}}; - -handle_info({Cls, Socket}, #state{dsock = {Trpt,Socket}, caller = recv_bin, - data = Data} = State0) - when {Cls,Trpt}=={tcp_closed,tcp} ; {Cls,Trpt}=={ssl_closed,ssl} -> - ?DBG("Data channel close",[]), - State = activate_ctrl_connection(State0), - {noreply, State#state{dsock = undefined, data = <<>>, - caller = {recv_bin, Data}}}; - -handle_info({Cls, Socket}, #state{dsock = {Trpt,Socket}, data = Data, - caller = {handle_dir_result, Dir}} - = State0) when {Cls,Trpt}=={tcp_closed,tcp} ; {Cls,Trpt}=={ssl_closed,ssl} -> - ?DBG("Data channel close",[]), - State = activate_ctrl_connection(State0), - {noreply, State#state{dsock = undefined, - caller = {handle_dir_result, Dir, Data}, -% data = <<?CR,?LF>>}}; - data = <<>>}}; - -handle_info({Err, Socket, Reason}, #state{dsock = {Trpt,Socket}, - client = From} = State) - when {Err,Trpt}=={tcp_error,tcp} ; {Err,Trpt}=={ssl_error,ssl} -> - gen_server:reply(From, {error, Reason}), - close_data_connection(State), - {noreply, State#state{dsock = undefined, client = undefined, - data = <<>>, caller = undefined, chunk = false}}; - -%%% Ctrl socket messages %%% -handle_info({Transport, Socket, Data}, #state{csock = {Transport, Socket}, - verbose = Verbose, - caller = Caller, - client = From, - ctrl_data = {BinCtrlData, AccLines, - LineStatus}} - = State0) -> - ?DBG('--ctrl ~p ----> ~s~p~n',[Socket,<<BinCtrlData/binary, Data/binary>>,State0]), - case ftp_response:parse_lines(<<BinCtrlData/binary, Data/binary>>, - AccLines, LineStatus) of - {ok, Lines, NextMsgData} -> - verbose(Lines, Verbose, 'receive'), - CtrlResult = ftp_response:interpret(Lines), - case Caller of - quote -> - gen_server:reply(From, string:tokens(Lines, [?CR, ?LF])), - {noreply, State0#state{client = undefined, - caller = undefined, - latest_ctrl_response = Lines, - ctrl_data = {NextMsgData, [], - start}}}; - _ -> - ?DBG(' ...handle_ctrl_result(~p,...) ctrl_data=~p~n',[CtrlResult,{NextMsgData, [], start}]), - handle_ctrl_result(CtrlResult, - State0#state{latest_ctrl_response = Lines, - ctrl_data = - {NextMsgData, [], start}}) - end; - {continue, CtrlData} when CtrlData =/= State0#state.ctrl_data -> - ?DBG(' ...Continue... ctrl_data=~p~n',[CtrlData]), - State1 = State0#state{ctrl_data = CtrlData}, - State = activate_ctrl_connection(State1), - {noreply, State}; - {continue, _CtrlData} -> - ?DBG(' ...Continue... ctrl_data=~p~n',[_CtrlData]), - {noreply, State0} - end; - -%% If the server closes the control channel it is -%% the expected behavior that connection process terminates. -handle_info({Cls, Socket}, #state{csock = {Trpt, Socket}}) - when {Cls,Trpt}=={tcp_closed,tcp} ; {Cls,Trpt}=={ssl_closed,ssl} -> - exit(normal); %% User will get error message from terminate/2 - -handle_info({Err, Socket, Reason}, _) when Err==tcp_error ; Err==ssl_error -> - Report = - io_lib:format("~p on socket: ~p for reason: ~p~n", - [Err, Socket, Reason]), - error_logger:error_report(Report), - %% If tcp does not work the only option is to terminate, - %% this is the expected behavior under these circumstances. - exit(normal); %% User will get error message from terminate/2 - -%% Monitor messages - if the process owning the ftp connection goes -%% down there is no point in continuing. -handle_info({'DOWN', _Ref, _Type, _Process, normal}, State) -> - {stop, normal, State#state{client = undefined}}; - -handle_info({'DOWN', _Ref, _Type, _Process, shutdown}, State) -> - {stop, normal, State#state{client = undefined}}; - -handle_info({'DOWN', _Ref, _Type, _Process, timeout}, State) -> - {stop, normal, State#state{client = undefined}}; - -handle_info({'DOWN', _Ref, _Type, Process, Reason}, State) -> - {stop, {stopped, {'EXIT', Process, Reason}}, - State#state{client = undefined}}; - -handle_info({'EXIT', Pid, Reason}, #state{progress = Pid} = State) -> - Report = io_lib:format("Progress reporting stopped for reason ~p~n", - [Reason]), - error_logger:info_report(Report), - {noreply, State#state{progress = ignore}}; - -%% Catch all - throws away unknown messages (This could happen by "accident" -%% so we do not want to crash, but we make a log entry as it is an -%% unwanted behaviour.) -handle_info(Info, State) -> - Report = io_lib:format("ftp : ~p : Unexpected message: ~p~nState: ~p~n", - [self(), Info, State]), - error_logger:info_report(Report), - {noreply, State}. - -%%-------------------------------------------------------------------------- -%% terminate/2 and code_change/3 -%%-------------------------------------------------------------------------- -terminate(normal, State) -> - %% If terminate reason =/= normal the progress reporting process will - %% be killed by the exit signal. - progress_report(stop, State), - do_terminate({error, econn}, State); -terminate(Reason, State) -> - Report = io_lib:format("Ftp connection closed due to: ~p~n", [Reason]), - error_logger:error_report(Report), - do_terminate({error, eclosed}, State). - -do_terminate(ErrorMsg, State) -> - close_data_connection(State), - close_ctrl_connection(State), - case State#state.client of - undefined -> - ok; - From -> - gen_server:reply(From, ErrorMsg) - end, - ok. - -code_change(_Vsn, State1, upgrade_from_pre_5_12) -> - {state, CSock, DSock, Verbose, LDir, Type, Chunk, Mode, Timeout, - Data, CtrlData, Owner, Client, Caller, IPv6Disable, Progress} = State1, - IpFamily = - if - (IPv6Disable =:= true) -> - inet; - true -> - inet6fb4 - end, - State2 = #state{csock = CSock, - dsock = DSock, - verbose = Verbose, - ldir = LDir, - type = Type, - chunk = Chunk, - mode = Mode, - timeout = Timeout, - data = Data, - ctrl_data = CtrlData, - owner = Owner, - client = Client, - caller = Caller, - ipfamily = IpFamily, - progress = Progress}, - {ok, State2}; - -code_change(_Vsn, State1, downgrade_to_pre_5_12) -> - #state{csock = CSock, - dsock = DSock, - verbose = Verbose, - ldir = LDir, - type = Type, - chunk = Chunk, - mode = Mode, - timeout = Timeout, - data = Data, - ctrl_data = CtrlData, - owner = Owner, - client = Client, - caller = Caller, - ipfamily = IpFamily, - progress = Progress} = State1, - IPv6Disable = - if - (IpFamily =:= inet) -> - true; - true -> - false - end, - State2 = - {state, CSock, DSock, Verbose, LDir, Type, Chunk, Mode, Timeout, - Data, CtrlData, Owner, Client, Caller, IPv6Disable, Progress}, - {ok, State2}; - -code_change(_Vsn, State, _Extra) -> - {ok, State}. + ftp_internal:latest_ctrl_response(Pid). - -%%%========================================================================= -%% Start/stop -%%%========================================================================= -%%-------------------------------------------------------------------------- -%% start_link([Opts, GenServerOptions]) -> {ok, Pid} | {error, Reason} -%% -%% Description: Callback function for the ftp supervisor. It is called -%% : when open or legacy is called. -%%-------------------------------------------------------------------------- -start_link([Opts, GenServerOptions]) -> - start_link(Opts, GenServerOptions). - -start_link(Opts, GenServerOptions) -> - case lists:keysearch(client, 1, Opts) of - {value, _} -> - %% Via the supervisor - gen_server:start_link(?MODULE, Opts, GenServerOptions); - false -> - Opts2 = [{client, self()} | Opts], - gen_server:start_link(?MODULE, Opts2, GenServerOptions) - end. - - -%%% Stop functionality is handled by close/1 - -%%%======================================================================== -%%% Internal functions -%%%======================================================================== - -%%-------------------------------------------------------------------------- -%%% Help functions to handle_call and/or handle_ctrl_result -%%-------------------------------------------------------------------------- -%% User handling --spec handle_user(User, Password, Account, State) -> Result when - User :: io:format(), - Password :: io:format(), - Account :: io:format(), - State :: #state{}, - Result :: {noreply, #state{}}. -handle_user(User, Password, Acc, State0) -> - _ = send_ctrl_message(State0, mk_cmd("USER ~s", [User])), - State = activate_ctrl_connection(State0), - {noreply, State#state{caller = {handle_user, Password, Acc}}}. - -handle_user_passwd(Password, Acc, State0) -> - _ = send_ctrl_message(State0, mk_cmd("PASS ~s", [Password])), - State = activate_ctrl_connection(State0), - {noreply, State#state{caller = {handle_user_passwd, Acc}}}. - -handle_user_account(Acc, State0) -> - _ = send_ctrl_message(State0, mk_cmd("ACCT ~s", [Acc])), - State = activate_ctrl_connection(State0), - {noreply, State#state{caller = handle_user_account}}. - - -%%-------------------------------------------------------------------------- -%% handle_ctrl_result -%%-------------------------------------------------------------------------- --type ctrl_status_operation() :: efnamena - | elogin - | enofile - | epath - | error - | etnospc - | epnospc - | efnamena - | econn - | perm_neg_compl - | pos_compl - | pos_interm - | pos_interm_acct - | pos_prel - | tls_upgrade - | trans_neg_compl. - --spec handle_ctrl_result(Operation, State) -> Result when - Operation :: {ctrl_status_operation(), list() | atom()}, - State :: #state{}, - Result :: {noreply, #state{}, integer()} - | {noreply, #state{}} - | {stop, normal | {error, Reason}, #state{}} - | {error, term()}, - Reason :: term(). -handle_ctrl_result({pos_compl, _}, #state{csock = {tcp, _Socket}, - tls_options = TLSOptions, - timeout = Timeout, - caller = open} - = State0) when is_list(TLSOptions) -> - _ = send_ctrl_message(State0, mk_cmd("AUTH TLS", [])), - State = activate_ctrl_connection(State0), - {noreply, State, Timeout}; - -handle_ctrl_result({tls_upgrade, S}, #state{csock = {tcp, Socket}, - tls_options = TLSOptions, - timeout = Timeout, - caller = open, client = From} - = State0) when is_list(TLSOptions) -> - ?DBG('<--ctrl ssl:connect(~p, ~p)~n~p~n',[Socket,TLSOptions,State0]), - catch ssl:start(), - case ssl:connect(Socket, TLSOptions, Timeout) of - {ok, TLSSocket} when S == ftps -> - State1 = State0#state{csock = {ssl,TLSSocket}}, - State = activate_ctrl_connection(State1), - {noreply, State#state{tls_upgrading_data_connection = pending}, Timeout}; - {ok, TLSSocket} -> - State1 = State0#state{csock = {ssl,TLSSocket}}, - handle_ctrl_result({pos_compl, S}, State1#state{tls_upgrading_data_connection = pending}); - {error, _} = Error -> - gen_server:reply(From, Error), - {stop, normal, State0#state{client = undefined, - caller = undefined, - tls_upgrading_data_connection = false}} - end; - -handle_ctrl_result({pos_compl, _}, #state{tls_upgrading_data_connection = pending} = State0) -> - _ = send_ctrl_message(State0, mk_cmd("PBSZ 0", [])), - State = activate_ctrl_connection(State0), - {noreply, State#state{tls_upgrading_data_connection = {true, pbsz}}}; - -handle_ctrl_result({pos_compl, _}, #state{tls_upgrading_data_connection = {true, pbsz}} = State0) -> - _ = send_ctrl_message(State0, mk_cmd("PROT P", [])), - State = activate_ctrl_connection(State0), - {noreply, State#state{tls_upgrading_data_connection = {true, prot}}}; - -handle_ctrl_result({pos_compl, _}, #state{tls_upgrading_data_connection = {true, prot}, - client = From} = State) -> - gen_server:reply(From, {ok, self()}), - {noreply, State#state{client = undefined, - caller = undefined, - tls_upgrading_data_connection = false}}; -handle_ctrl_result({pos_compl, _}, #state{caller = open, client = From} - = State) -> - gen_server:reply(From, {ok, self()}), - {noreply, State#state{client = undefined, - caller = undefined }}; -handle_ctrl_result({_, Lines}, #state{caller = open} = State) -> - ctrl_result_response(econn, State, {error, Lines}); - -%%-------------------------------------------------------------------------- -%% Data connection setup active mode -handle_ctrl_result({pos_compl, _Lines}, - #state{mode = active, - caller = {setup_data_connection, - {LSock, Caller}}} = State) -> - handle_caller(State#state{caller = Caller, dsock = {lsock, LSock}}); - -handle_ctrl_result({Status, _Lines}, - #state{mode = active, - caller = {setup_data_connection, {LSock, _}}} - = State) -> - close_connection({tcp,LSock}), - ctrl_result_response(Status, State, {error, Status}); - -%% Data connection setup passive mode -handle_ctrl_result({pos_compl, Lines}, - #state{mode = passive, - ipfamily = inet6, - client = From, - caller = {setup_data_connection, Caller}, - csock = CSock, - sockopts_data_passive = SockOpts, - timeout = Timeout} - = State) -> - [_, PortStr | _] = lists:reverse(string:tokens(Lines, "|")), - {ok, {IP, _}} = peername(CSock), - case connect(IP, list_to_integer(PortStr), SockOpts, Timeout, State) of - {ok, _, Socket} -> - handle_caller(State#state{caller = Caller, dsock = {tcp, Socket}}); - {error, _Reason} = Error -> - gen_server:reply(From, Error), - {noreply, State#state{client = undefined, caller = undefined}} - end; - -handle_ctrl_result({pos_compl, Lines}, - #state{mode = passive, - ipfamily = inet, - client = From, - caller = {setup_data_connection, Caller}, - timeout = Timeout, - sockopts_data_passive = SockOpts, - ftp_extension = false} = State) -> - - {_, [?LEFT_PAREN | Rest]} = - lists:splitwith(fun(?LEFT_PAREN) -> false; (_) -> true end, Lines), - {NewPortAddr, _} = - lists:splitwith(fun(?RIGHT_PAREN) -> false; (_) -> true end, Rest), - [A1, A2, A3, A4, P1, P2] = - lists:map(fun(X) -> list_to_integer(X) end, - string:tokens(NewPortAddr, [$,])), - IP = {A1, A2, A3, A4}, - Port = (P1 * 256) + P2, - - ?DBG('<--data tcp connect to ~p:~p, Caller=~p~n',[IP,Port,Caller]), - case connect(IP, Port, SockOpts, Timeout, State) of - {ok, _, Socket} -> - handle_caller(State#state{caller = Caller, dsock = {tcp,Socket}}); - {error, _Reason} = Error -> - gen_server:reply(From, Error), - {noreply,State#state{client = undefined, caller = undefined}} - end; - -handle_ctrl_result({pos_compl, Lines}, - #state{mode = passive, - ipfamily = inet, - client = From, - caller = {setup_data_connection, Caller}, - csock = CSock, - timeout = Timeout, - sockopts_data_passive = SockOpts, - ftp_extension = true} = State) -> - - [_, PortStr | _] = lists:reverse(string:tokens(Lines, "|")), - {ok, {IP, _}} = peername(CSock), - - ?DBG('<--data tcp connect to ~p:~p, Caller=~p~n',[IP,PortStr,Caller]), - case connect(IP, list_to_integer(PortStr), SockOpts, Timeout, State) of - {ok, _, Socket} -> - handle_caller(State#state{caller = Caller, dsock = {tcp, Socket}}); - {error, _Reason} = Error -> - gen_server:reply(From, Error), - {noreply, State#state{client = undefined, caller = undefined}} - end; - - -%% FTP server does not support passive mode: try to fallback on active mode -handle_ctrl_result(_, - #state{mode = passive, - caller = {setup_data_connection, Caller}} = State) -> - setup_data_connection(State#state{mode = active, caller = Caller}); - - -%%-------------------------------------------------------------------------- -%% User handling -handle_ctrl_result({pos_interm, _}, - #state{caller = {handle_user, PassWord, Acc}} = State) -> - handle_user_passwd(PassWord, Acc, State); -handle_ctrl_result({Status, _}, - #state{caller = {handle_user, _, _}} = State) -> - ctrl_result_response(Status, State, {error, euser}); - -%% Accounts -handle_ctrl_result({pos_interm_acct, _}, - #state{caller = {handle_user_passwd, Acc}} = State) - when Acc =/= "" -> - handle_user_account(Acc, State); -handle_ctrl_result({Status, _}, - #state{caller = {handle_user_passwd, _}} = State) -> - ctrl_result_response(Status, State, {error, euser}); - -%%-------------------------------------------------------------------------- -%% Print current working directory -handle_ctrl_result({pos_compl, Lines}, - #state{caller = pwd, client = From} = State) -> - Dir = pwd_result(Lines), - gen_server:reply(From, {ok, Dir}), - {noreply, State#state{client = undefined, caller = undefined}}; - -%%-------------------------------------------------------------------------- -%% Directory listing -handle_ctrl_result({pos_prel, _}, #state{caller = {dir, Dir}} = State0) -> - case accept_data_connection(State0) of - {ok, State1} -> - State = activate_data_connection(State1), - {noreply, State#state{caller = {handle_dir_result, Dir}}}; - {error, _Reason} = Error -> - ctrl_result_response(error, State0, Error) - end; - -handle_ctrl_result({pos_compl, _}, #state{caller = {handle_dir_result, ""=_CurrentDir, - Data}, client = From}= State) -> - gen_server:reply(From, {ok, Data}), - {noreply, State#state{client = undefined, - caller = undefined}}; - -handle_ctrl_result({pos_compl, _}, #state{caller = {handle_dir_result, _Dir, - Data}, client = From}= State) -> - gen_server:reply(From, {ok, Data}), - {noreply, State#state{client = undefined, - caller = undefined}}; - -handle_ctrl_result({pos_compl, _}=Operation, #state{caller = {handle_dir_result, Dir}, - data = Data}= State) -> - handle_ctrl_result(Operation, State#state{caller = {handle_dir_result, Dir, Data}}); - -handle_ctrl_result(S={_Status, _}, - #state{caller = {handle_dir_result, _, _}} = State) -> - %% OTP-5731, macosx - ctrl_result_response(S, State, {error, epath}); - -handle_ctrl_result({Status, _}, #state{caller = cd} = State) -> - ctrl_result_response(Status, State, {error, Status}); - -handle_ctrl_result(Status={epath, _}, #state{caller = {dir,_}} = State) -> - ctrl_result_response(Status, State, {error, epath}); - -%%-------------------------------------------------------------------------- -%% File renaming -handle_ctrl_result({pos_interm, _}, #state{caller = {rename, NewFile}} - = State0) -> - _ = send_ctrl_message(State0, mk_cmd("RNTO ~s", [NewFile])), - State = activate_ctrl_connection(State0), - {noreply, State#state{caller = rename_second_phase}}; - -handle_ctrl_result({Status, _}, - #state{caller = {rename, _}} = State) -> - ctrl_result_response(Status, State, {error, Status}); - -handle_ctrl_result({Status, _}, - #state{caller = rename_second_phase} = State) -> - ctrl_result_response(Status, State, {error, Status}); - -%%-------------------------------------------------------------------------- -%% File handling - recv_bin -handle_ctrl_result({pos_prel, _}, #state{caller = recv_bin} = State0) -> - case accept_data_connection(State0) of - {ok, State1} -> - State = activate_data_connection(State1), - {noreply, State}; - {error, _Reason} = Error -> - ctrl_result_response(error, State0, Error) - end; - -handle_ctrl_result({pos_compl, _}, #state{caller = {recv_bin, Data}, - client = From} = State) -> - gen_server:reply(From, {ok, Data}), - close_data_connection(State), - {noreply, State#state{client = undefined, caller = undefined}}; - -handle_ctrl_result({Status, _}, #state{caller = recv_bin} = State) -> - close_data_connection(State), - ctrl_result_response(Status, State#state{dsock = undefined}, - {error, epath}); - -handle_ctrl_result({Status, _}, #state{caller = {recv_bin, _}} = State) -> - close_data_connection(State), - ctrl_result_response(Status, State#state{dsock = undefined}, - {error, epath}); -%%-------------------------------------------------------------------------- -%% File handling - start_chunk_transfer -handle_ctrl_result({pos_prel, _}, #state{caller = start_chunk_transfer} - = State0) -> - case accept_data_connection(State0) of - {ok, State1} -> - State = start_chunk(State1), - {noreply, State}; - {error, _Reason} = Error -> - ctrl_result_response(error, State0, Error) - end; - -%%-------------------------------------------------------------------------- -%% File handling - chunk_transfer complete - -handle_ctrl_result({pos_compl, _}, #state{client = From, - caller = #recv_chunk_closing{dconn_closed = true, - client_called_us = true, - pos_compl_received = false - }} - = State0) when From =/= undefined -> - %% The pos_compl was the last event we waited for, finnish and clean up - ?DBG("recv_chunk_closing pos_compl, last event",[]), - gen_server:reply(From, ok), - State = activate_ctrl_connection(State0), - {noreply, State#state{caller = undefined, - chunk = false, - client = undefined}}; - -handle_ctrl_result({pos_compl, _}, #state{caller = #recv_chunk_closing{}=R} - = State0) -> - %% Waiting for more, don't care what - ?DBG("recv_chunk_closing pos_compl, wait more",[]), - {noreply, State0#state{caller = R#recv_chunk_closing{pos_compl_received=true}}}; - -handle_ctrl_result({pos_compl, _}, #state{caller = undefined, chunk = true} - = State0) -> - %% Waiting for user to call recv_chunk - {noreply, State0#state{caller = #recv_chunk_closing{pos_compl_received=true}}}; - -%%-------------------------------------------------------------------------- -%% File handling - recv_file -handle_ctrl_result({pos_prel, _}, #state{caller = {recv_file, _}} = State0) -> - case accept_data_connection(State0) of - {ok, State1} -> - State = activate_data_connection(State1), - {noreply, State}; - {error, _Reason} = Error -> - ctrl_result_response(error, State0, Error) - end; - -handle_ctrl_result({Status, _}, #state{caller = {recv_file, Fd}} = State) -> - file_close(Fd), - close_data_connection(State), - ctrl_result_response(Status, State#state{dsock = undefined}, - {error, epath}); -%%-------------------------------------------------------------------------- -%% File handling - transfer_* -handle_ctrl_result({pos_prel, _}, #state{caller = {transfer_file, Fd}} - = State0) -> - case accept_data_connection(State0) of - {ok, State1} -> - send_file(State1, Fd); - {error, _Reason} = Error -> - ctrl_result_response(error, State0, Error) - end; - -handle_ctrl_result({pos_prel, _}, #state{caller = {transfer_data, Bin}} - = State0) -> - case accept_data_connection(State0) of - {ok, State} -> - send_bin(State, Bin); - {error, _Reason} = Error -> - ctrl_result_response(error, State0, Error) - end; - -%%-------------------------------------------------------------------------- -%% Default -handle_ctrl_result({Status, _Lines}, #state{client = From} = State) - when From =/= undefined -> - ctrl_result_response(Status, State, {error, Status}); -handle_ctrl_result(CtrlMsg, #state{caller = undefined} = State) -> - logger:log(info, #{protocol => ftp, unexpected_msg => CtrlMsg}), - {noreply, State}. - -%%-------------------------------------------------------------------------- -%% Help functions to handle_ctrl_result -%%-------------------------------------------------------------------------- - --spec ctrl_result_response(Status, State, Error) -> Result when - Status :: ctrl_status_operation() | {ctrl_status_operation(), _}, - State :: #state{}, - Error :: {error, Reason}, - Reason :: term(), - Result :: {noreply, #state{}} | Error. -ctrl_result_response(pos_compl, #state{client = From} = State, _) -> - gen_server:reply(From, ok), - {noreply, State#state{client = undefined, caller = undefined}}; - -ctrl_result_response(enofile, #state{client = From} = State, _) -> - gen_server:reply(From, {error, enofile}), - {noreply, State#state{client = undefined, caller = undefined}}; - -ctrl_result_response(error, State0, {error, _Reason} = Error) -> - case State0#state.client of - undefined -> - {stop, Error, State0}; - From -> - gen_server:reply(From, Error), - State = activate_ctrl_connection(State0), - {noreply, State} - end; - -ctrl_result_response(Status, #state{client = From} = State, _) - when (Status =:= etnospc) orelse - (Status =:= epnospc) orelse - (Status =:= efnamena) orelse - (Status =:= econn) -> - gen_server:reply(From, {error, Status}), - {stop, normal, State#state{client = undefined}}; - -ctrl_result_response(_, #state{client = From} = State, ErrorMsg) -> - gen_server:reply(From, ErrorMsg), - {noreply, State#state{client = undefined, caller = undefined}}. - -%%-------------------------------------------------------------------------- --spec handle_caller(State) -> Result when - State :: #state{}, - Result :: {noreply, #state{}}. -handle_caller(#state{caller = {dir, Dir, Len}} = State0) -> - Cmd = case Len of - short -> "NLST"; - long -> "LIST" - end, - _ = case Dir of - "" -> - send_ctrl_message(State0, mk_cmd(Cmd, "")); - _ -> - send_ctrl_message(State0, mk_cmd(Cmd ++ " ~s", [Dir])) - end, - State = activate_ctrl_connection(State0), - {noreply, State#state{caller = {dir, Dir}}}; - -handle_caller(#state{caller = {recv_bin, RemoteFile}} = State0) -> - _ = send_ctrl_message(State0, mk_cmd("RETR ~s", [RemoteFile])), - State = activate_ctrl_connection(State0), - {noreply, State#state{caller = recv_bin}}; - -handle_caller(#state{caller = {start_chunk_transfer, Cmd, RemoteFile}} = - State0) -> - _ = send_ctrl_message(State0, mk_cmd("~s ~s", [Cmd, RemoteFile])), - State = activate_ctrl_connection(State0), - {noreply, State#state{caller = start_chunk_transfer}}; - -handle_caller(#state{caller = {recv_file, RemoteFile, Fd}} = State0) -> - _ = send_ctrl_message(State0, mk_cmd("RETR ~s", [RemoteFile])), - State = activate_ctrl_connection(State0), - {noreply, State#state{caller = {recv_file, Fd}}}; - -handle_caller(#state{caller = {transfer_file, {Cmd, LocalFile, RemoteFile}}, - ldir = LocalDir, client = From} = State0) -> - case file_open(filename:absname(LocalFile, LocalDir), read) of - {ok, Fd} -> - _ = send_ctrl_message(State0, mk_cmd("~s ~s", [Cmd, RemoteFile])), - State = activate_ctrl_connection(State0), - {noreply, State#state{caller = {transfer_file, Fd}}}; - {error, _} -> - gen_server:reply(From, {error, epath}), - {noreply, State0#state{client = undefined, caller = undefined, - dsock = undefined}} - end; - -handle_caller(#state{caller = {transfer_data, {Cmd, Bin, RemoteFile}}} = - State0) -> - _ = send_ctrl_message(State0, mk_cmd("~s ~s", [Cmd, RemoteFile])), - State = activate_ctrl_connection(State0), - {noreply, State#state{caller = {transfer_data, Bin}}}. - -%% ----------- FTP SERVER COMMUNICATION ------------------------- - -%% Connect to FTP server at Host (default is TCP port 21) -%% in order to establish a control connection. --spec setup_ctrl_connection(Host, Port, Timeout, State) -> Result when - Host :: inet:socket_address() | inet:hostname(), - Port :: inet:port_number(), - Timeout :: timeout(), - State :: #state{}, - Reason :: timeout | inet:posix(), - Result :: {ok, State, integer()} | {error, Reason}. -setup_ctrl_connection(Host, Port, Timeout, #state{sockopts_ctrl = SockOpts} = State0) -> - MsTime = erlang:monotonic_time(), - case connect(Host, Port, SockOpts, Timeout, State0) of - {ok, IpFam, CSock} -> - State1 = State0#state{csock = {tcp, CSock}, ipfamily = IpFam}, - State = activate_ctrl_connection(State1), - case Timeout - millisec_passed(MsTime) of - Timeout2 when (Timeout2 >= 0) -> - {ok, State#state{caller = open}, Timeout2}; - _ -> - %% Oups: Simulate timeout - {ok, State#state{caller = open}, 0} - end; - Error -> - Error - end. - --spec setup_data_connection(State) -> Result when - State :: #state{}, - Result :: {noreply, State}. -setup_data_connection(#state{mode = active, - caller = Caller, - csock = CSock, - sockopts_data_active = SockOpts, - ftp_extension = FtpExt} = State0) -> - case (catch sockname(CSock)) of - {ok, {{_, _, _, _, _, _, _, _} = IP0, _}} -> - IP = proplists:get_value(ip, SockOpts, IP0), - {ok, LSock} = - gen_tcp:listen(0, [{ip, IP}, {active, false}, - inet6, binary, {packet, 0} | - lists:keydelete(ip,1,SockOpts)]), - {ok, {_, Port}} = sockname({tcp,LSock}), - IpAddress = inet_parse:ntoa(IP), - Cmd = mk_cmd("EPRT |2|~s|~p|", [IpAddress, Port]), - _ = send_ctrl_message(State0, Cmd), - State = activate_ctrl_connection(State0), - {noreply, State#state{caller = {setup_data_connection, - {LSock, Caller}}}}; - {ok, {{_,_,_,_} = IP0, _}} -> - IP = proplists:get_value(ip, SockOpts, IP0), - {ok, LSock} = gen_tcp:listen(0, [{ip, IP}, {active, false}, - binary, {packet, 0} | - lists:keydelete(ip,1,SockOpts)]), - {ok, Port} = inet:port(LSock), - _ = case FtpExt of - false -> - {IP1, IP2, IP3, IP4} = IP, - {Port1, Port2} = {Port div 256, Port rem 256}, - send_ctrl_message(State0, - mk_cmd("PORT ~w,~w,~w,~w,~w,~w", - [IP1, IP2, IP3, IP4, Port1, Port2])); - true -> - IpAddress = inet_parse:ntoa(IP), - Cmd = mk_cmd("EPRT |1|~s|~p|", [IpAddress, Port]), - send_ctrl_message(State0, Cmd) - end, - State = activate_ctrl_connection(State0), - {noreply, State#state{caller = {setup_data_connection, - {LSock, Caller}}}} - end; - -setup_data_connection(#state{mode = passive, ipfamily = inet6, - caller = Caller} = State0) -> - _ = send_ctrl_message(State0, mk_cmd("EPSV", [])), - State = activate_ctrl_connection(State0), - {noreply, State#state{caller = {setup_data_connection, Caller}}}; - -setup_data_connection(#state{mode = passive, ipfamily = inet, - caller = Caller, - ftp_extension = false} = State0) -> - _ = send_ctrl_message(State0, mk_cmd("PASV", [])), - State = activate_ctrl_connection(State0), - {noreply, State#state{caller = {setup_data_connection, Caller}}}; - -setup_data_connection(#state{mode = passive, ipfamily = inet, - caller = Caller, - ftp_extension = true} = State0) -> - _ = send_ctrl_message(State0, mk_cmd("EPSV", [])), - State = activate_ctrl_connection(State0), - {noreply, State#state{caller = {setup_data_connection, Caller}}}. - --spec connect(Host, Port, SockOpts, Timeout, State) -> Result when - Host :: inet:socket_address() | inet:hostname(), - Port :: inet:port_number(), - SockOpts :: [inet:inet_backend() | gen_tcp:connect_option()], - Timeout :: timeout(), - State :: #state{}, - Reason :: timeout | inet:posix(), - Result :: {ok, inet:address_family(), gen_tcp:socket()} | {error, Reason}. -connect(Host, Port, SockOpts, Timeout, #state{ipfamily = inet = IpFam}) -> - connect2(Host, Port, IpFam, SockOpts, Timeout); - -connect(Host, Port, SockOpts, Timeout, #state{ipfamily = inet6 = IpFam}) -> - connect2(Host, Port, IpFam, SockOpts, Timeout); - -connect(Host, Port, SockOpts, Timeout, #state{ipfamily = inet6fb4}) -> - case inet:getaddr(Host, inet6) of - {ok, {0, 0, 0, 0, 0, 16#ffff, _, _} = IPv6} -> - case inet:getaddr(Host, inet) of - {ok, IPv4} -> - IpFam = inet, - connect2(IPv4, Port, IpFam, SockOpts, Timeout); - - _ -> - IpFam = inet6, - connect2(IPv6, Port, IpFam, SockOpts, Timeout) - end; - - {ok, IPv6} -> - IpFam = inet6, - connect2(IPv6, Port, IpFam, SockOpts, Timeout); - - _ -> - case inet:getaddr(Host, inet) of - {ok, IPv4} -> - IpFam = inet, - connect2(IPv4, Port, IpFam, SockOpts, Timeout); - Error -> - Error - end - end. - --spec connect2(Host, Port, IpFam, SockOpts, Timeout) -> Result when - Host :: inet:socket_address() | inet:hostname(), - Port :: inet:port_number(), - SockOpts :: [inet:inet_backend() | gen_tcp:connect_option()], - Timeout :: timeout(), - IpFam :: inet:address_family(), - Reason :: timeout | inet:posix(), - Result :: {ok, inet:address_family(), gen_tcp:socket()} | {error, Reason}. -connect2(Host, Port, IpFam, SockOpts, Timeout) -> - Opts = [IpFam, binary, {packet, 0}, {active, false} | SockOpts], - case gen_tcp:connect(Host, Port, Opts, Timeout) of - {ok, Sock} -> - {ok, IpFam, Sock}; - Error -> - Error - end. - --spec accept_data_connection_tls_options(State) -> Result when - State :: #state{}, - Result :: [tuple()]. -accept_data_connection_tls_options(#state{ csock = {ssl,Socket}, tls_options = TO0, tls_ctrl_session_reuse = true }) -> - TO = lists:keydelete(reuse_sessions, 1, TO0), - {ok, [{session_id,SSLSessionId},{session_data,SSLSessionData}]} = ssl:connection_information(Socket, [session_id, session_data]), - lists:keystore(reuse_session, 1, TO, {reuse_session,{SSLSessionId,SSLSessionData}}); -accept_data_connection_tls_options(#state{ tls_options = TO }) -> - TO. - --spec accept_data_connection(State) -> Result when - State :: #state{}, - Result :: {ok, #state{}} | {error, Reason}, - Reason :: term(). -accept_data_connection(#state{mode = active, - dtimeout = DTimeout, - tls_options = TLSOptions0, - dsock = {lsock, LSock}} = State0) -> - case gen_tcp:accept(LSock, DTimeout) of - {ok, Socket} when is_list(TLSOptions0) -> - gen_tcp:close(LSock), - TLSOptions = accept_data_connection_tls_options(State0), - ?DBG('<--data ssl:connect(~p, ~p)~n~p~n',[Socket,TLSOptions,State0]), - case ssl:connect(Socket, TLSOptions, DTimeout) of - {ok, TLSSocket} -> - {ok, State0#state{dsock={ssl,TLSSocket}}}; - {error, Reason} -> - {error, {ssl_connect_failed, Reason}} - end; - {ok, Socket} -> - gen_tcp:close(LSock), - {ok, State0#state{dsock={tcp,Socket}}}; - {error, Reason} -> - {error, {data_connect_failed, Reason}} - end; - -accept_data_connection(#state{mode = passive, - dtimeout = DTimeout, - dsock = {tcp,Socket}, - tls_options = TLSOptions0} = State) when is_list(TLSOptions0) -> - TLSOptions = accept_data_connection_tls_options(State), - ?DBG('<--data ssl:connect(~p, ~p)~n~p~n',[Socket,TLSOptions,State]), - case ssl:connect(Socket, TLSOptions, DTimeout) of - {ok, TLSSocket} -> - {ok, State#state{dsock={ssl,TLSSocket}}}; - {error, Reason} -> - {error, {ssl_connect_failed, Reason}} - end; -accept_data_connection(#state{mode = passive} = State) -> - {ok,State}. - --spec send_ctrl_message(State, Message) -> _ when - State :: #state{}, - Message :: [term() | Message]. -send_ctrl_message(_S=#state{csock = Socket, verbose = Verbose}, Message) -> - verbose(lists:flatten(Message),Verbose,send), - ?DBG('<--ctrl ~p ---- ~s~p~n',[Socket,Message,_S]), - _ = send_message(Socket, Message). - -send_data_message(_S=#state{dsock = Socket}, Message) -> - ?DBG('<==data ~p ==== ~s~n~p~n',[Socket,Message,_S]), - case send_message(Socket, Message) of - ok -> - ok; - {error, Reason} -> - Report = io_lib:format("send/2 for socket ~p failed with " - "reason ~p~n", [Socket, Reason]), - error_logger:error_report(Report), - %% If tcp/ssl does not work the only option is to terminate, - %% this is the expected behavior under these circumstances. - exit(normal) %% User will get error message from terminate/2 - end. - -send_message({tcp, Socket}, Message) -> - gen_tcp:send(Socket, Message); -send_message({ssl, Socket}, Message) -> - ssl:send(Socket, Message). - -activate_ctrl_connection(#state{csock = CSock, ctrl_data = {<<>>, _, _}} = State) -> - _ = activate_connection(CSock), - State; -activate_ctrl_connection(#state{csock = CSock} = State0) -> - _ = activate_connection(CSock), - %% We have already received at least part of the next control message, - %% that has been saved in ctrl_data, process this first. - {noreply, State} = handle_info({socket_type(CSock), unwrap_socket(CSock), <<>>}, State0), - State. - -activate_data_connection(#state{dsock = DSock} = State) -> - _ = activate_connection(DSock), - State. - -activate_connection(Socket) -> - case socket_type(Socket) of - tcp -> - _ = activate_connection(inet, tcp_closed, Socket); - ssl -> - _ = activate_connection(ssl, ssl_closed, Socket) - end. - -activate_connection(API, CloseTag, Socket0) -> - Socket = unwrap_socket(Socket0), - case API:setopts(Socket, [{active, once}]) of - ok -> - ok; - {error, _} -> %% inet can return einval instead of closed - self() ! {CloseTag, Socket} - end. - -ignore_return_value(_) -> ok. - -unwrap_socket({tcp,Socket}) -> Socket; -unwrap_socket({ssl,Socket}) -> Socket. - -socket_type({tcp,_Socket}) -> tcp; -socket_type({ssl,_Socket}) -> ssl. - -close_ctrl_connection(#state{csock = undefined}) -> ok; -close_ctrl_connection(#state{csock = Socket}) -> close_connection(Socket). - -close_data_connection(#state{dsock = undefined}) -> ok; -close_data_connection(#state{dsock = Socket}) -> close_connection(Socket). - -close_connection({lsock,Socket}) -> ignore_return_value( gen_tcp:close(Socket) ); -close_connection({tcp, Socket}) -> ignore_return_value( gen_tcp:close(Socket) ); -close_connection({ssl, Socket}) -> ignore_return_value( ssl:close(Socket) ). - -%% ------------ FILE HANDLING ---------------------------------------- -send_file(#state{tls_upgrading_data_connection = {true, CTRL, _}} = State, Fd) -> - {noreply, State#state{tls_upgrading_data_connection = {true, CTRL, ?MODULE, send_file, Fd}}}; -send_file(State0, Fd) -> - case file_read(Fd) of - {ok, N, Bin} when N > 0 -> - send_data_message(State0, Bin), - progress_report({binary, Bin}, State0), - send_file(State0, Fd); - {ok, _, _} -> - file_close(Fd), - close_data_connection(State0), - progress_report({transfer_size, 0}, State0), - State = activate_ctrl_connection(State0), - {noreply, State#state{caller = transfer_file_second_phase, - dsock = undefined}}; - {error, Reason} -> - gen_server:reply(State0#state.client, {error, Reason}), - {stop, normal, State0#state{client = undefined}} - end. - -file_open(File, Option) -> - file:open(File, [raw, binary, Option]). - -file_close(Fd) -> - ignore_return_value( file:close(Fd) ). - -file_read(Fd) -> - case file:read(Fd, ?FILE_BUFSIZE) of - {ok, Bytes} when is_binary(Bytes) -> - {ok, byte_size(Bytes), Bytes}; - eof -> - {ok, 0, []}; - Other -> - Other - end. - -file_write(Bytes, Fd) -> - file:write(Fd, Bytes). - -%% -------------- MISC ---------------------------------------------- - -call(GenServer, Msg, Format) -> - call(GenServer, Msg, Format, infinity). -call(GenServer, Msg, Format, Timeout) -> - Req = {self(), Msg}, - case (catch gen_server:call(GenServer, Req, Timeout)) of - {ok, Bin} when is_binary(Bin) andalso (Format =:= string) -> - {ok, binary_to_list(Bin)}; - {'EXIT', _, _} -> - {error, eclosed}; - {'EXIT', _} -> - {error, eclosed}; - Result -> - Result - end. - -cast(GenServer, Msg) -> - gen_server:cast(GenServer, {self(), Msg}). - -send_bin(#state{tls_upgrading_data_connection = {true, CTRL, _}} = State, Bin) -> - State#state{tls_upgrading_data_connection = {true, CTRL, ?MODULE, send_bin, Bin}}; -send_bin(State0, Bin) -> - send_data_message(State0, Bin), - close_data_connection(State0), - State = activate_ctrl_connection(State0), - {noreply, State#state{caller = transfer_data_second_phase, - dsock = undefined}}. - -mk_cmd(Fmt, Args) -> - [io_lib:format(Fmt, Args)| [?CR, ?LF]]. % Deep list ok. - -is_name_sane([]) -> - true; -is_name_sane([?CR| _]) -> - false; -is_name_sane([?LF| _]) -> - false; -is_name_sane([_| Rest]) -> - is_name_sane(Rest). - -pwd_result(Lines) -> - {_, [?DOUBLE_QUOTE | Rest]} = - lists:splitwith(fun(?DOUBLE_QUOTE) -> false; (_) -> true end, Lines), - {Dir, _} = - lists:splitwith(fun(?DOUBLE_QUOTE) -> false; (_) -> true end, Rest), - Dir. - - -key_search(Key, List, Default) -> - case lists:keysearch(Key, 1, List) of - {value, {_,Val}} -> - Val; - false -> - Default - end. - -verbose(Lines, true, Direction) -> - DirStr = - case Direction of - send -> - "Sending: "; - _ -> - "Receiving: " - end, - Str = string:strip(string:strip(Lines, right, ?LF), right, ?CR), - erlang:display(DirStr++Str); -verbose(_, false,_) -> - ok. - -progress(Options) -> - ftp_progress:start_link(Options). - -progress_report(_, #state{progress = ignore}) -> - ok; -progress_report(stop, #state{progress = ProgressPid}) -> - ftp_progress:stop(ProgressPid); -progress_report({binary, Data}, #state{progress = ProgressPid}) when is_binary(Data) -> - ftp_progress:report(ProgressPid, {transfer_size, byte_size(Data)}); -progress_report(Report, #state{progress = ProgressPid}) -> - ftp_progress:report(ProgressPid, Report). - - -peername({tcp, Socket}) -> inet:peername(Socket); -peername({ssl, Socket}) -> ssl:peername(Socket). - -sockname({tcp, Socket}) -> inet:sockname(Socket); -sockname({ssl, Socket}) -> ssl:sockname(Socket). - -start_chunk(#state{tls_upgrading_data_connection = {true, CTRL, _}} = State) -> - State#state{tls_upgrading_data_connection = {true, CTRL, ?MODULE, start_chunk, undefined}}; -start_chunk(#state{client = From} = State) -> - gen_server:reply(From, ok), - State#state{chunk = true, - client = undefined, - caller = undefined}. - - -%% This function extracts the start options from the -%% Valid options: -%% debug, -%% verbose -%% ipfamily -%% priority -%% flags (for backward compatibillity) -start_options(Options) -> - case lists:keysearch(flags, 1, Options) of - {value, {flags, Flags}} -> - Verbose = lists:member(verbose, Flags), - IsTrace = lists:member(trace, Flags), - IsDebug = lists:member(debug, Flags), - DebugLevel = - if - (IsTrace =:= true) -> - trace; - IsDebug =:= true -> - debug; - true -> - disable - end, - {ok, [{verbose, Verbose}, - {debug, DebugLevel}, - {priority, low}]}; - false -> - ValidateVerbose = - fun(true) -> true; - (false) -> true; - (_) -> false - end, - ValidateDebug = - fun(trace) -> true; - (debug) -> true; - (disable) -> true; - (_) -> false - end, - ValidatePriority = - fun(low) -> true; - (normal) -> true; - (high) -> true; - (_) -> false - end, - ValidOptions = - [{verbose, ValidateVerbose, false, false}, - {debug, ValidateDebug, false, disable}, - {priority, ValidatePriority, false, low}], - validate_options(Options, ValidOptions, []) - end. - - -%% This function extracts and validates the open options from the -%% Valid options: -%% mode -%% host -%% port -%% timeout -%% dtimeout -%% progress -%% ftp_extension - --spec open_options([tuple()]) -> {ok, [tuple()]} | no_return(). -open_options(Options) -> - ValidateMode = - fun(active) -> true; - (passive) -> true; - (_) -> false - end, - ValidateHost = - fun(Host) when is_list(Host) -> - true; - (Host) when tuple_size(Host) =:= 4; tuple_size(Host) =:= 8 -> - true; - (_) -> - false - end, - ValidatePort = - fun(Port) when is_integer(Port) andalso (Port >= 0) -> true; - (_) -> false - end, - ValidateIpFamily = - fun(inet) -> true; - (inet6) -> true; - (inet6fb4) -> true; - (_) -> false - end, - ValidateTLS = - fun(TLS) when is_list(TLS) -> true; - (undefined) -> true; - (_) -> false - end, - ValidateTLSSecMethod = - fun(ftpes) -> true; - (ftps) -> true; - (_) -> false - end, - ValidateTLSCtrlSessionReuse = - fun(Reuse) when is_boolean(Reuse) -> true; - (_) -> false - end, - ValidateTimeout = - fun(Timeout) when is_integer(Timeout) andalso (Timeout >= 0) -> true; - (_) -> false - end, - ValidateDTimeout = - fun(DTimeout) when is_integer(DTimeout) andalso (DTimeout >= 0) -> true; - (infinity) -> true; - (_) -> false - end, - ValidateProgress = - fun(ignore) -> - true; - ({Mod, Func, _InitProgress}) when is_atom(Mod) andalso - is_atom(Func) -> - true; - (_) -> - false - end, - ValidateFtpExtension = - fun(true) -> true; - (false) -> true; - (_) -> false - end, - ValidOptions = - [{mode, ValidateMode, false, ?DEFAULT_MODE}, - {host, ValidateHost, true, ehost}, - {port, ValidatePort, false, 0}, - {ipfamily, ValidateIpFamily, false, inet}, - {tls, ValidateTLS, false, undefined}, - {tls_sec_method, ValidateTLSSecMethod, false, ftpes}, - {tls_ctrl_session_reuse, ValidateTLSCtrlSessionReuse, false, false}, - {timeout, ValidateTimeout, false, ?CONNECTION_TIMEOUT}, - {dtimeout, ValidateDTimeout, false, ?DATA_ACCEPT_TIMEOUT}, - {progress, ValidateProgress, false, ?PROGRESS_DEFAULT}, - {ftp_extension, ValidateFtpExtension, false, ?FTP_EXT_DEFAULT}], - validate_options(Options, ValidOptions, []). - -%% validates socket options and set defaults --spec socket_options([tuple()]) -> {ok, tuple()} | no_return(). -socket_options(Options) -> - CtrlOpts = proplists:get_value(sock_ctrl, Options, []), - DataActOpts = proplists:get_value(sock_data_act, Options, CtrlOpts), - DataPassOpts = proplists:get_value(sock_data_pass, Options, CtrlOpts), - case [O || O <- lists:usort(CtrlOpts++DataPassOpts++DataActOpts), - not valid_socket_option(O)] of - [] -> - {ok, {CtrlOpts, DataPassOpts, DataActOpts}}; - Invalid -> - throw({error,{sock_opts,Invalid}}) - end. - - -valid_socket_option(inet ) -> false; -valid_socket_option(inet6 ) -> false; -valid_socket_option({ipv6_v6only, _}) -> false; -valid_socket_option({active,_} ) -> false; -valid_socket_option({packet,_} ) -> false; -valid_socket_option({mode,_} ) -> false; -valid_socket_option(binary ) -> false; -valid_socket_option(list ) -> false; -valid_socket_option({header,_} ) -> false; -valid_socket_option({packet_size,_} ) -> false; -valid_socket_option(_) -> true. - - --spec validate_options(Options, ValidOptions, Acc) -> Result when - Options :: [tuple()], - ValidOptions :: [tuple()], - Acc :: [tuple()], - Result :: {ok, [tuple()]} | no_return(). -validate_options([], [], Acc) -> - {ok, lists:reverse(Acc)}; -validate_options([], ValidOptions, Acc) -> - %% Check if any mandatory options are missing! - case [{Key, Reason} || {Key, _, true, Reason} <- ValidOptions] of - [] -> - Defaults = - [{Key, Default} || {Key, _, _, Default} <- ValidOptions], - {ok, lists:reverse(Defaults ++ Acc)}; - [{_, Reason}|_Missing] -> - throw({error, Reason}) - end; -validate_options([{Key, Value}|Options], ValidOptions, Acc) -> - case lists:keysearch(Key, 1, ValidOptions) of - {value, {Key, Validate, _, Default}} -> - case (catch Validate(Value)) of - true -> - NewValidOptions = lists:keydelete(Key, 1, ValidOptions), - validate_options(Options, NewValidOptions, - [{Key, Value} | Acc]); - _ -> - NewValidOptions = lists:keydelete(Key, 1, ValidOptions), - validate_options(Options, NewValidOptions, - [{Key, Default} | Acc]) - end; - false -> - validate_options(Options, ValidOptions, Acc) - end; -validate_options([_|Options], ValidOptions, Acc) -> - validate_options(Options, ValidOptions, Acc). - -%% Help function, elapsed milliseconds since T0 -millisec_passed(T0) -> - %% OTP 18 - erlang:convert_time_unit(erlang:monotonic_time() - T0, - native, - micro_seconds) div 1000. diff --git a/lib/ftp/src/ftp_internal.erl b/lib/ftp/src/ftp_internal.erl new file mode 100644 index 0000000000..2b8ee8f9e1 --- /dev/null +++ b/lib/ftp/src/ftp_internal.erl @@ -0,0 +1,2467 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2002-2023. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% +%% + +-module(ftp_internal). + +-behaviour(gen_server). + +-export([start_service/1]). + +-export([start_link/1, start_link/2]). + +%% API - Client interface +-export([cd/2, close/1, delete/2, + lcd/2, lpwd/1, ls/2, + mkdir/2, nlist/2, + open/1, open/2, + pwd/1, quote/2, + recv/2, recv/3, recv_bin/2, + recv_chunk_start/2, recv_chunk/1, + rename/3, rmdir/2, + send/2, send/3, send_bin/3, + send_chunk_start/2, send_chunk/2, send_chunk_end/1, + type/2, user/3, user/4, account/2, + append/3, append_bin/3, + append_chunk/2, append_chunk_end/1, append_chunk_start/2, + info/1, latest_ctrl_response/1]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, + handle_info/2, terminate/2, code_change/3]). + +-include("ftp_internal.hrl"). + +-define(DBG(F,A), 'n/a'). +%%-define(DBG(F,A), io:format(F,A)). +%%-define(DBG(F,A), ct:pal("~p:~p " ++ if is_list(F) -> F; is_atom(F) -> atom_to_list(F) end, [?MODULE,?LINE|A])). + +%% Constants used in internal state definition +-define(CONNECTION_TIMEOUT, 60*1000). +-define(DATA_ACCEPT_TIMEOUT, infinity). +-define(DEFAULT_MODE, passive). +-define(PROGRESS_DEFAULT, ignore). +-define(FTP_EXT_DEFAULT, false). + +%% Internal Constants +-define(FTP_PORT, 21). +-define(FTPS_PORT, 990). +-define(FILE_BUFSIZE, 4096). + + +%%%========================================================================= +%%% Data Types +%%%========================================================================= + +%% Internal state +-record(state, { + csock = undefined :: undefined % Control connection socket + | { tcp | ssl, inet:socket() | ssl:socket() | ssl:sslsocket()}, + dsock = undefined :: undefined % Data connection socket + | { tcp | ssl | lsock, inet:socket() | ssl:socket() | ssl:sslsocket() | gen_tcp:socket()}, + tls_options = undefined :: undefined | [tuple()], + verbose = false :: boolean(), + ldir = undefined :: undefined | file:filename_all(), % Current local directory + type = ftp_server_default :: atom(), % binary | ascii + chunk = false :: boolean(), % Receiving data chunks + mode = ?DEFAULT_MODE :: active | passive, + timeout = ?CONNECTION_TIMEOUT :: timeout(), + %% Data received so far on the data connection + data = <<>> :: binary(), + %% Data received so far on the control connection + %% {BinStream, AccLines}. If a binary sequence + %% ends with ?CR then keep it in the binary to + %% be able to detect if the next received byte is ?LF + %% and hence the end of the response is reached! + ctrl_data = {<<>>, [], start} :: {binary(), [byte()], term()}, + %% pid() - Client pid (note not the same as "From") + latest_ctrl_response = "" :: string(), + owner = undefined :: undefined | term(), + client = undefined :: undefined | gen_server:from(), % "From" to be used in gen_server:reply/2 + %% Function that activated a connection and maybe some + %% data needed further on. + caller = undefined :: term(), + ipfamily :: inet:address_family() | inet6fb4, + sockopts_ctrl = [] :: list(), + sockopts_data_passive = [] :: list(), + sockopts_data_active = [] :: list(), + progress = ignore :: ignore | pid(), + dtimeout = ?DATA_ACCEPT_TIMEOUT :: non_neg_integer() | infinity, + tls_ctrl_session_reuse = false :: boolean(), + tls_upgrading_data_connection = false :: boolean() | atom() | tuple(), + ftp_extension = ?FTP_EXT_DEFAULT :: boolean() + }). + +-record(recv_chunk_closing, { + dconn_closed = false, + pos_compl_received = false, + client_called_us = false + }). + + +-type shortage_reason() :: 'etnospc' | 'epnospc'. +-type restriction_reason() :: 'epath' | 'efnamena' | 'elogin' | 'enotbinary'. +-type common_reason() :: 'econn' | 'eclosed' | term(). +-type file_write_error_reason() :: file:posix() | badarg | terminated. % See file:write for more info + + +%% This should be made an internal function when we remove the deprecation +%% ftp client processes should always be part of ftp supervisor tree. +%% We consider it a bug that the "standalone" concept of inets was +%% not removed when ftp was broken out, and it is now fixed. +-spec start_service(ServiceConfig) -> {ok, Pid} | {error, Reason} when + ServiceConfig :: [{Property, Value}], + Property :: proplists:property(), + Value :: term(), + Pid :: pid(), + Reason :: term(). +start_service(Options) -> + try + {ok, StartOptions} = start_options(Options), + case ftp_sup:start_child([[[{client, self()} | StartOptions], []]]) of + {ok, Pid} -> + call(Pid, {open, ip_comm, Options}, plain); + Error1 -> + Error1 + end + catch + throw:Error2 -> + Error2 + end. + +-spec open(Host :: string() | inet:ip_address()) -> + {'ok', Pid :: pid()} | {'error', Reason :: 'ehost' | term()}. + +open(Host) -> + open(Host, []). + +-spec open(Host :: string() | inet:ip_address(), Opts) -> + {'ok', Pid :: pid()} | {'error', Reason :: 'ehost' | term()} when + Opts :: [Opt], + Opt :: StartOption | OpenOption, + StartOption :: {verbose, Verbose} | {debug, Debug}, + Verbose :: boolean(), + Debug :: disable | debug | trace, + OpenOption :: {ipfamily, IpFamily} | {port, Port :: port()} | {mode, Mode} + | {tls, TLSOptions :: [ssl:tls_option()]} | {tls_sec_method, TLSSecMethod :: ftps | ftpes} + | {tls_ctrl_session_reuse, TLSSessionReuse :: boolean() } | {timeout, Timeout :: timeout()} + | {dtimeout, DTimeout :: timeout()} | {progress, Progress} | {sock_ctrl, SocketCtrls} + | {sock_data_act, [SocketControl]} | {sock_data_pass, [SocketControl]}, + SocketCtrls :: [SocketControl], + IpFamily :: inet | inet6 | inet6fb4, + Mode :: active | passive, + Module :: atom(), + Function :: atom(), + InitialData :: term(), + Progress :: ignore | {Module, Function, InitialData}, + SocketControl :: gen_tcp:option(). +%% <BACKWARD-COMPATIBILLITY> +open(Host, Port) when is_integer(Port) -> + open(Host, [{port, Port}]); +%% </BACKWARD-COMPATIBILLITY> + +open(Host, Options) when is_list(Options) -> + start_service([{host,Host}|Options]). + + +%%-------------------------------------------------------------------------- +%% Description: Login with or without a supplied account name. +%%-------------------------------------------------------------------------- +-spec user(Pid :: pid(), + User :: string(), + Pass :: string()) -> + 'ok' | {'error', Reason :: 'euser' | common_reason()}. + +user(Pid, User, Pass) -> + case {is_name_sane(User), is_name_sane(Pass)} of + {true, true} -> + call(Pid, {user, User, Pass}, atom); + _ -> + {error, euser} + end. + +-spec user(Pid :: pid(), + User :: string(), + Pass :: string(), + Account :: string()) -> + 'ok' | {'error', Reason :: 'euser' | common_reason()}. + +user(Pid, User, Pass, Account) -> + case {is_name_sane(User), is_name_sane(Pass), is_name_sane(Account)} of + {true, true, true} -> + call(Pid, {user, User, Pass, Account}, atom); + _ -> + {error, euser} + end. + + +%%-------------------------------------------------------------------------- +%% Description: Set a user Account. +%%-------------------------------------------------------------------------- + +-spec account(Pid :: pid(), Acc :: string()) -> + 'ok' | {'error', Reason :: 'eacct' | common_reason()}. + +account(Pid, Acc) -> + case is_name_sane(Acc) of + true -> + call(Pid, {account, Acc}, atom); + _ -> + {error, eacct} + end. + + +%%-------------------------------------------------------------------------- +%% Description: Get the current working directory at remote server. +%%-------------------------------------------------------------------------- + +-spec pwd(Pid :: pid()) -> + {'ok', Dir :: string()} | + {'error', Reason :: restriction_reason() | common_reason()}. + +pwd(Pid) -> + call(Pid, pwd, ctrl). + + +%%-------------------------------------------------------------------------- +%% Description: Get the current working directory at local server. +%%-------------------------------------------------------------------------- + +-spec lpwd(Pid :: pid()) -> + {'ok', Dir :: string()}. + +lpwd(Pid) -> + call(Pid, lpwd, string). + + +%%-------------------------------------------------------------------------- +%% Description: Change current working directory at remote server. +%%-------------------------------------------------------------------------- + +-spec cd(Pid :: pid(), Dir :: string()) -> + 'ok' | {'error', Reason :: restriction_reason() | common_reason()}. + +cd(Pid, Dir) -> + case is_name_sane(Dir) of + true -> + call(Pid, {cd, Dir}, atom); + _ -> + {error, efnamena} + end. + + +%%-------------------------------------------------------------------------- +%% Description: Change current working directory for the local client. +%%-------------------------------------------------------------------------- + +-spec lcd(Pid :: pid(), Dir :: string()) -> + 'ok' | {'error', Reason :: restriction_reason()}. + +lcd(Pid, Dir) -> + call(Pid, {lcd, Dir}, string). + + +%%-------------------------------------------------------------------------- +%% Description: Returns a list of files in long format. +%%-------------------------------------------------------------------------- + +-spec ls(Pid :: pid(), Dir :: string()) -> + {'ok', Listing :: string()} | + {'error', Reason :: restriction_reason() | common_reason()}. + +ls(Pid, Dir) -> + case is_name_sane(Dir) of + true -> + call(Pid, {dir, long, Dir}, string); + _ -> + {error, efnamena} + end. + + +%%-------------------------------------------------------------------------- +%% Description: Returns a list of files in short format +%%-------------------------------------------------------------------------- + +-spec nlist(Pid :: pid(), Pathname :: string()) -> + {'ok', Listing :: string()} | + {'error', Reason :: restriction_reason() | common_reason()}. + +nlist(Pid, Dir) -> + case is_name_sane(Dir) of + true -> + call(Pid, {dir, short, Dir}, string); + _ -> + {error, efnamena} + end. + + +%%-------------------------------------------------------------------------- +%% Description: Rename a file at remote server. +%%-------------------------------------------------------------------------- + +-spec rename(Pid :: pid(), Old :: string(), New :: string()) -> + 'ok' | {'error', Reason :: restriction_reason() | common_reason()}. + +rename(Pid, Old, New) -> + case {is_name_sane(Old), is_name_sane(New)} of + {true, true} -> + call(Pid, {rename, Old, New}, string); + _ -> + {error, efnamena} + end. + + +%%-------------------------------------------------------------------------- +%% Description: Remove file at remote server. +%%-------------------------------------------------------------------------- + +-spec delete(Pid :: pid(), File :: string()) -> + 'ok' | {'error', Reason :: restriction_reason() | common_reason()}. + +delete(Pid, File) -> + case is_name_sane(File) of + true -> + call(Pid, {delete, File}, string); + _ -> + {error, efnamena} + end. + + +%%-------------------------------------------------------------------------- +%% Description: Make directory at remote server. +%%-------------------------------------------------------------------------- + +-spec mkdir(Pid :: pid(), Dir :: string()) -> + 'ok' | {'error', Reason :: restriction_reason() | common_reason()}. + +mkdir(Pid, Dir) -> + case is_name_sane(Dir) of + true -> + call(Pid, {mkdir, Dir}, atom); + _ -> + {error, efnamena} + end. + + +%%-------------------------------------------------------------------------- +%% Description: Remove directory at remote server. +%%-------------------------------------------------------------------------- + +-spec rmdir(Pid :: pid(), Dir :: string()) -> + 'ok' | {'error', Reason :: restriction_reason() | common_reason()}. + +rmdir(Pid, Dir) -> + case is_name_sane(Dir) of + true -> + call(Pid, {rmdir, Dir}, atom); + _ -> + {error, efnamena} + end. + + +%%-------------------------------------------------------------------------- +%% Description: Set transfer type. +%%-------------------------------------------------------------------------- + +-spec type(Pid :: pid(), Type :: ascii | binary) -> + 'ok' | + {'error', Reason :: 'etype' | restriction_reason() | common_reason()}. + +type(Pid, Type) -> + call(Pid, {type, Type}, atom). + + +%%-------------------------------------------------------------------------- +%% Description: Transfer file from remote server. +%%-------------------------------------------------------------------------- + +-spec recv(Pid :: pid(), RemoteFileName :: string()) -> + 'ok' | {'error', Reason :: restriction_reason() | + common_reason() | + file_write_error_reason()}. + +recv(Pid, RemotFileName) -> + recv(Pid, RemotFileName, RemotFileName). + +-spec recv(Pid :: pid(), + RemoteFileName :: string(), + LocalFileName :: string()) -> + 'ok' | {'error', Reason :: term()}. + +recv(Pid, RemotFileName, LocalFileName) -> + case is_name_sane(RemotFileName) of + true -> + call(Pid, {recv, RemotFileName, LocalFileName}, atom); + _ -> + {error, efnamena} + end. + + +%%-------------------------------------------------------------------------- +%% Description: Transfer file from remote server into binary. +%%-------------------------------------------------------------------------- + +-spec recv_bin(Pid :: pid(), + RemoteFile :: string()) -> + {'ok', Bin :: binary()} | + {'error', Reason :: restriction_reason() | common_reason()}. + +recv_bin(Pid, RemoteFile) -> + case is_name_sane(RemoteFile) of + true -> + call(Pid, {recv_bin, RemoteFile}, bin); + _ -> + {error, efnamena} + end. + + +%%-------------------------------------------------------------------------- +%% Description: Start receive of chunks of remote file. +%%-------------------------------------------------------------------------- + +-spec recv_chunk_start(Pid :: pid(), + RemoteFile :: string()) -> + 'ok' | {'error', Reason :: restriction_reason() | common_reason()}. + +recv_chunk_start(Pid, RemoteFile) -> + case is_name_sane(RemoteFile) of + true -> + call(Pid, {recv_chunk_start, RemoteFile}, atom); + _ -> + {error, efnamena} + end. + + +%%-------------------------------------------------------------------------- +%% Description: Transfer file from remote server into binary in chunks +%%-------------------------------------------------------------------------- + +-spec recv_chunk(Pid :: pid()) -> + 'ok' | + {'ok', Bin :: binary()} | + {'error', Reason :: restriction_reason() | common_reason()}. + +recv_chunk(Pid) -> + call(Pid, recv_chunk, atom). + + +%%-------------------------------------------------------------------------- +%% Description: Transfer file to remote server. +%%-------------------------------------------------------------------------- + +-spec send(Pid :: pid(), LocalFileName :: string()) -> + 'ok' | + {'error', Reason :: restriction_reason() | + common_reason() | + shortage_reason()}. + +send(Pid, LocalFileName) -> + send(Pid, LocalFileName, LocalFileName). + +-spec send(Pid :: pid(), + LocalFileName :: string(), + RemoteFileName :: string()) -> + 'ok' | + {'error', Reason :: restriction_reason() | + common_reason() | + shortage_reason()}. + +send(Pid, LocalFileName, RemotFileName) -> + case is_name_sane(RemotFileName) of + true -> + call(Pid, {send, LocalFileName, RemotFileName}, atom); + _ -> + {error, efnamena} + end. + + +%%-------------------------------------------------------------------------- +%% Description: Transfer a binary to a remote file. +%%-------------------------------------------------------------------------- + +-spec send_bin(Pid :: pid(), Bin :: binary(), RemoteFile :: string()) -> + 'ok' | + {'error', Reason :: restriction_reason() | + common_reason() | + shortage_reason()}. + +send_bin(Pid, Bin, RemoteFile) when is_binary(Bin) -> + case is_name_sane(RemoteFile) of + true -> + call(Pid, {send_bin, Bin, RemoteFile}, atom); + _ -> + {error, efnamena} + end; +send_bin(_Pid, _Bin, _RemoteFile) -> + {error, enotbinary}. + + +%%-------------------------------------------------------------------------- +%% Description: Start transfer of chunks to remote file. +%%-------------------------------------------------------------------------- + +-spec send_chunk_start(Pid :: pid(), RemoteFile :: string()) -> + 'ok' | {'error', Reason :: restriction_reason() | common_reason()}. + +send_chunk_start(Pid, RemoteFile) -> + case is_name_sane(RemoteFile) of + true -> + call(Pid, {send_chunk_start, RemoteFile}, atom); + _ -> + {error, efnamena} + end. + + +%%-------------------------------------------------------------------------- +%% Description: Start append chunks of data to remote file. +%%-------------------------------------------------------------------------- + +-spec append_chunk_start(Pid :: pid(), RemoteFile :: string()) -> + 'ok' | {'error', Reason :: restriction_reason() | common_reason()}. + +append_chunk_start(Pid, RemoteFile) -> + case is_name_sane(RemoteFile) of + true -> + call(Pid, {append_chunk_start, RemoteFile}, atom); + _ -> + {error, efnamena} + end. + + +%%-------------------------------------------------------------------------- +%% Purpose: Send chunk to remote file. +%%-------------------------------------------------------------------------- + +-spec send_chunk(Pid :: pid(), Bin :: binary()) -> + 'ok' | + {'error', Reason :: 'echunk' | + restriction_reason() | + common_reason()}. + +send_chunk(Pid, Bin) when is_binary(Bin) -> + call(Pid, {transfer_chunk, Bin}, atom); +send_chunk(_Pid, _Bin) -> + {error, enotbinary}. + + +%%-------------------------------------------------------------------------- +%% Description: Append chunk to remote file. +%%-------------------------------------------------------------------------- + +-spec append_chunk(Pid :: pid(), Bin :: binary()) -> + 'ok' | + {'error', Reason :: 'echunk' | + restriction_reason() | + common_reason()}. + +append_chunk(Pid, Bin) when is_binary(Bin) -> + call(Pid, {transfer_chunk, Bin}, atom); +append_chunk(_Pid, _Bin) -> + {error, enotbinary}. + + +%%-------------------------------------------------------------------------- +%% Description: End sending of chunks to remote file. +%%-------------------------------------------------------------------------- + +-spec send_chunk_end(Pid :: pid()) -> + 'ok' | + {'error', Reason :: restriction_reason() | + common_reason() | + shortage_reason()}. + +send_chunk_end(Pid) -> + call(Pid, chunk_end, atom). + + +%%-------------------------------------------------------------------------- +%% Description: End appending of chunks to remote file. +%%-------------------------------------------------------------------------- + +-spec append_chunk_end(Pid :: pid()) -> + 'ok' | + {'error', Reason :: echunk | + restriction_reason() | + common_reason() | + shortage_reason()}. + +append_chunk_end(Pid) -> + call(Pid, chunk_end, atom). + + +%%-------------------------------------------------------------------------- +%% Description: Append the local file to the remote file +%%-------------------------------------------------------------------------- + +-spec append(Pid :: pid(), + LocalFileName :: string(), + RemoteFileName :: string()) -> + 'ok' | {'error', Reason} when + Reason :: epath | elogin | etnospc | epnospc | efnamena | common_reason(). + +append(Pid, LocalFileName, RemotFileName) -> + case is_name_sane(RemotFileName) of + true -> + call(Pid, {append, LocalFileName, RemotFileName}, atom); + _ -> + {error, efnamena} + end. + + +%%-------------------------------------------------------------------------- +%% Purpose: Append a binary to a remote file. +%%-------------------------------------------------------------------------- + +-spec append_bin(Pid :: pid(), + Bin :: binary(), + RemoteFile :: string()) -> + 'ok' | + {'error', Reason :: restriction_reason() | + common_reason() | + shortage_reason()}. + +append_bin(Pid, Bin, RemoteFile) when is_binary(Bin) -> + case is_name_sane(RemoteFile) of + true -> + call(Pid, {append_bin, Bin, RemoteFile}, atom); + _ -> + {error, efnamena} + end; +append_bin(_Pid, _Bin, _RemoteFile) -> + {error, enotbinary}. + + +%%-------------------------------------------------------------------------- +%% Description: Send arbitrary ftp command. +%%-------------------------------------------------------------------------- + +-spec quote(Pid :: pid(), Cmd :: string()) -> [FTPLine :: string()]. + +quote(Pid, Cmd) when is_list(Cmd) -> + call(Pid, {quote, Cmd}, atom). + + +%%-------------------------------------------------------------------------- +%% Description: End the ftp session. +%%-------------------------------------------------------------------------- + +-spec close(Pid :: pid()) -> 'ok'. +close(Pid) -> + cast(Pid, close), + ok. + + +%%-------------------------------------------------------------------------- +%% Description: Return diagnostics. +%%-------------------------------------------------------------------------- + +info(Pid) -> + call(Pid, info, list). + + +%%-------------------------------------------------------------------------- +%% Description: The latest received response from the server +%%-------------------------------------------------------------------------- + +-spec latest_ctrl_response(Pid :: pid()) -> string(). + +latest_ctrl_response(Pid) -> + call(Pid, latest_ctrl_response, string). + + +%%%======================================================================== +%%% gen_server callback functions +%%%======================================================================== + +%%------------------------------------------------------------------------- +%% init(Args) -> {ok, State} | {ok, State, Timeout} | {stop, Reason} +%% Description: Initiates the erlang process that manages a ftp connection. +%%------------------------------------------------------------------------- +init(Options) -> + process_flag(trap_exit, true), + + %% Keep track of the client + {value, {client, Client}} = lists:keysearch(client, 1, Options), + erlang:monitor(process, Client), + + %% Make sure inet is started + _ = inet_db:start(), + + %% Where are we + {ok, Dir} = file:get_cwd(), + + %% Maybe activate dbg + case key_search(debug, Options, disable) of + trace -> + dbg:tracer(), + dbg:p(all, [call]), + {ok, _} = dbg:tpl(ftp_internal, [{'_', [], [{return_trace}]}]), + {ok, _} = dbg:tpl(ftp_response, [{'_', [], [{return_trace}]}]), + {ok, _} = dbg:tpl(ftp_progress, [{'_', [], [{return_trace}]}]), + ok; + debug -> + dbg:tracer(), + dbg:p(all, [call]), + {ok, _} = dbg:tp(ftp_internal, [{'_', [], [{return_trace}]}]), + {ok, _} = dbg:tp(ftp_response, [{'_', [], [{return_trace}]}]), + {ok, _} = dbg:tp(ftp_progress, [{'_', [], [{return_trace}]}]), + ok; + _ -> + %% Keep silent + ok + end, + + %% Verbose? + Verbose = key_search(verbose, Options, false), + + %% IpFamily? + IpFamily = key_search(ipfamily, Options, inet), + + State = #state{owner = Client, + verbose = Verbose, + ipfamily = IpFamily, + ldir = Dir}, + + %% Set process prio + Priority = key_search(priority, Options, low), + process_flag(priority, Priority), + + %% And we are done + {ok, State}. + + +%%-------------------------------------------------------------------------- +%% handle_call(Request, From, State) -> {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | +%% Description: Handle incoming requests. +%%------------------------------------------------------------------------- + +%% Anyone can ask this question +handle_call({_, info}, _, #state{verbose = Verbose, + mode = Mode, + timeout = Timeout, + ipfamily = IpFamily, + csock = Socket, + progress = Progress} = State) -> + {ok, {_, LocalPort}} = sockname(Socket), + {ok, {Address, Port}} = peername(Socket), + Options = [{verbose, Verbose}, + {ipfamily, IpFamily}, + {mode, Mode}, + {peer, Address}, + {peer_port, Port}, + {local_port, LocalPort}, + {timeout, Timeout}, + {progress, Progress}], + {reply, {ok, Options}, State}; + +handle_call({_,latest_ctrl_response}, _, #state{latest_ctrl_response=Resp} = State) -> + {reply, {ok,Resp}, State}; + +%% But everything else must come from the owner +handle_call({Pid, _}, _, #state{owner = Owner} = State) when Owner =/= Pid -> + {reply, {error, not_connection_owner}, State}; + +handle_call({_, {open, ip_comm, Options}}, From, State) -> + {ok, Opts} = open_options(Options), + + case key_search(host, Opts, undefined) of + undefined -> + {stop, normal, {error, ehost}, State}; + Host -> + TLSSecMethod = key_search(tls_sec_method, Opts, undefined), + TLSOpts = key_search(tls, Opts, undefined), + TLSReuse = key_search(tls_ctrl_session_reuse, Opts, false), + Mode = key_search(mode, Opts, ?DEFAULT_MODE), + Port0 = key_search(port, Opts, 0), + Port = if Port0 == 0, TLSSecMethod == ftps -> ?FTPS_PORT; Port0 == 0 -> ?FTP_PORT; true -> Port0 end, + Timeout = key_search(timeout, Opts, ?CONNECTION_TIMEOUT), + DTimeout = key_search(dtimeout, Opts, ?DATA_ACCEPT_TIMEOUT), + Progress = key_search(progress, Opts, ignore), + IpFamily = key_search(ipfamily, Opts, inet), + FtpExt = key_search(ftp_extension, Opts, ?FTP_EXT_DEFAULT), + + {ok, {CtrlOpts, DataPassOpts, DataActOpts}} = socket_options(Options), + + State2 = State#state{client = From, + mode = Mode, + progress = progress(Progress), + ipfamily = IpFamily, + sockopts_ctrl = CtrlOpts, + sockopts_data_passive = DataPassOpts, + sockopts_data_active = DataActOpts, + timeout = Timeout, + dtimeout = DTimeout, + ftp_extension = FtpExt}, + + case setup_ctrl_connection(Host, Port, Timeout, State2) of + {ok, State3, WaitTimeout} when is_list(TLSOpts), TLSSecMethod == ftps -> + handle_ctrl_result({tls_upgrade, TLSSecMethod}, + State3#state{tls_options = TLSOpts, + tls_ctrl_session_reuse = TLSReuse, + timeout = WaitTimeout }); + {ok, State3, WaitTimeout} when is_list(TLSOpts) -> + {noreply, State3#state{tls_options = TLSOpts, tls_ctrl_session_reuse = TLSReuse }, WaitTimeout}; + {ok, State3, WaitTimeout} -> + {noreply, State3, WaitTimeout}; + {error, _Reason} -> + gen_server:reply(From, {error, ehost}), + {stop, normal, State2#state{client = undefined}} + end + end; + +handle_call({_, {user, User, Password}}, From, + #state{csock = CSock} = State) when (CSock =/= undefined) -> + handle_user(User, Password, "", State#state{client = From}); + +handle_call({_, {user, User, Password, Acc}}, From, + #state{csock = CSock} = State) when (CSock =/= undefined) -> + handle_user(User, Password, Acc, State#state{client = From}); + +handle_call({_, {account, Acc}}, From, State)-> + handle_user_account(Acc, State#state{client = From}); + +handle_call({_, pwd}, From, #state{chunk = false} = State0) -> + _ = send_ctrl_message(State0, mk_cmd("PWD", [])), + State = activate_ctrl_connection(State0), + {noreply, State#state{client = From, caller = pwd}}; + +handle_call({_, lpwd}, From, #state{ldir = LDir} = State) -> + {reply, {ok, LDir}, State#state{client = From}}; + +handle_call({_, {cd, Dir}}, From, #state{chunk = false} = State0) -> + _ = send_ctrl_message(State0, mk_cmd("CWD ~s", [Dir])), + State = activate_ctrl_connection(State0), + {noreply, State#state{client = From, caller = cd}}; + +handle_call({_,{lcd, Dir}}, _From, #state{ldir = LDir0} = State) -> + LDir = filename:absname(Dir, LDir0), + case file:read_file_info(LDir) of %% FIX better check that LDir is a dir. + {ok, _ } -> + {reply, ok, State#state{ldir = LDir}}; + _ -> + {reply, {error, epath}, State} + end; + +handle_call({_, {dir, Len, Dir}}, {_Pid, _} = From, + #state{chunk = false} = State) -> + setup_data_connection(State#state{caller = {dir, Dir, Len}, + client = From}); +handle_call({_, {rename, CurrFile, NewFile}}, From, + #state{chunk = false} = State0) -> + _ = send_ctrl_message(State0, mk_cmd("RNFR ~s", [CurrFile])), + State = activate_ctrl_connection(State0), + {noreply, State#state{caller = {rename, NewFile}, client = From}}; + +handle_call({_, {delete, File}}, {_Pid, _} = From, + #state{chunk = false} = State0) -> + _ = send_ctrl_message(State0, mk_cmd("DELE ~s", [File])), + State = activate_ctrl_connection(State0), + {noreply, State#state{client = From}}; + +handle_call({_, {mkdir, Dir}}, From, #state{chunk = false} = State0) -> + _ = send_ctrl_message(State0, mk_cmd("MKD ~s", [Dir])), + State = activate_ctrl_connection(State0), + {noreply, State#state{client = From}}; + +handle_call({_,{rmdir, Dir}}, From, #state{chunk = false} = State0) -> + _ = send_ctrl_message(State0, mk_cmd("RMD ~s", [Dir])), + State = activate_ctrl_connection(State0), + {noreply, State#state{client = From}}; + +handle_call({_,{type, Type}}, From, #state{chunk = false} = State0) -> + case Type of + ascii -> + _ = send_ctrl_message(State0, mk_cmd("TYPE A", [])), + State = activate_ctrl_connection(State0), + {noreply, State#state{caller = type, type = ascii, + client = From}}; + binary -> + _ = send_ctrl_message(State0, mk_cmd("TYPE I", [])), + State = activate_ctrl_connection(State0), + {noreply, State#state{caller = type, type = binary, + client = From}}; + _ -> + {reply, {error, etype}, State0} + end; +handle_call({_,{recv, RemoteFile, LocalFile}}, From, + #state{chunk = false, ldir = LocalDir} = State) -> + progress_report({remote_file, RemoteFile}, State), + NewLocalFile = filename:absname(LocalFile, LocalDir), + + case file_open(NewLocalFile, write) of + {ok, Fd} -> + setup_data_connection(State#state{client = From, + caller = + {recv_file, + RemoteFile, Fd}}); + {error, _What} -> + {reply, {error, epath}, State} + end; +handle_call({_, {recv_bin, RemoteFile}}, From, #state{chunk = false} = + State) -> + setup_data_connection(State#state{caller = {recv_bin, RemoteFile}, + client = From}); +handle_call({_,{recv_chunk_start, RemoteFile}}, From, #state{chunk = false} + = State) -> + setup_data_connection(State#state{caller = {start_chunk_transfer, + "RETR", RemoteFile}, + client = From}); + +handle_call({_, recv_chunk}, _, #state{chunk = false} = State) -> + {reply, {error, "ftp:recv_chunk_start/2 not called"}, State}; +handle_call({_, recv_chunk}, _From, #state{chunk = true, + data = Bin, + caller = #recv_chunk_closing{dconn_closed = true, + pos_compl_received = true, + client_called_us = true + } + } = State0) -> + case Bin of + <<>> -> + {reply, ok, State0#state{caller = undefined, + chunk = false, + client = undefined}}; + Data -> + {reply, Data, State0#state{caller = undefined, + chunk = false, + client = undefined}} + end; +handle_call({_, recv_chunk}, _From, #state{chunk = true, + caller = #recv_chunk_closing{dconn_closed = true, + pos_compl_received = true + } + } = State0) -> + %% The ftp:recv_chunk call was the last event we waited for, finnish and clean up + ?DBG("Data connection closed recv_chunk_closing ftp:recv_chunk, last event",[]), + State = activate_ctrl_connection(State0), + {reply, ok, State#state{caller = undefined, + chunk = false, + client = undefined}}; +handle_call({_, recv_chunk}, From, #state{chunk = true, + caller = #recv_chunk_closing{pos_compl_received = true + } = R + } = State0) -> + State = activate_data_connection(State0), + {noreply, State#state{client = From, caller = R#recv_chunk_closing{client_called_us=true}}}; + +handle_call({_, recv_chunk}, From, #state{chunk = true, + caller = #recv_chunk_closing{} = R + } = State) -> + %% Waiting for more, don't care what + ?DBG("recv_chunk_closing ftp:recv_chunk, get more",[]), + {noreply, State#state{client = From, caller = R#recv_chunk_closing{client_called_us=true}}}; + +handle_call({_, recv_chunk}, From, #state{chunk = true} = State0) -> + State = activate_data_connection(State0), + {noreply, State#state{client = From, caller = recv_chunk}}; + +handle_call({_, {send, LocalFile, RemoteFile}}, From, + #state{chunk = false, ldir = LocalDir} = State) -> + progress_report({local_file, filename:absname(LocalFile, LocalDir)}, + State), + setup_data_connection(State#state{caller = {transfer_file, + {"STOR", + LocalFile, RemoteFile}}, + client = From}); +handle_call({_, {append, LocalFile, RemoteFile}}, From, + #state{chunk = false} = State) -> + setup_data_connection(State#state{caller = {transfer_file, + {"APPE", + LocalFile, RemoteFile}}, + client = From}); +handle_call({_, {send_bin, Bin, RemoteFile}}, From, + #state{chunk = false} = State) -> + setup_data_connection(State#state{caller = {transfer_data, + {"STOR", Bin, RemoteFile}}, + client = From}); +handle_call({_,{append_bin, Bin, RemoteFile}}, From, + #state{chunk = false} = State) -> + setup_data_connection(State#state{caller = {transfer_data, + {"APPE", Bin, RemoteFile}}, + client = From}); +handle_call({_, {send_chunk_start, RemoteFile}}, From, #state{chunk = false} + = State) -> + setup_data_connection(State#state{caller = {start_chunk_transfer, + "STOR", RemoteFile}, + client = From}); +handle_call({_, {append_chunk_start, RemoteFile}}, From, #state{chunk = false} + = State) -> + setup_data_connection(State#state{caller = {start_chunk_transfer, + "APPE", RemoteFile}, + client = From}); +handle_call({_, {transfer_chunk, Bin}}, _, #state{chunk = true} = State) -> + send_data_message(State, Bin), + {reply, ok, State}; + +handle_call({_, {transfer_chunk, _}}, _, #state{chunk = false} = State) -> + {reply, {error, echunk}, State}; + +handle_call({_, chunk_end}, From, #state{chunk = true} = State0) -> + close_data_connection(State0), + State = activate_ctrl_connection(State0), + {noreply, State#state{client = From, dsock = undefined, + caller = end_chunk_transfer, chunk = false}}; + +handle_call({_, chunk_end}, _, #state{chunk = false} = State) -> + {reply, {error, echunk}, State}; + +handle_call({_, {quote, Cmd}}, From, #state{chunk = false} = State0) -> + _ = send_ctrl_message(State0, mk_cmd(Cmd, [])), + State = activate_ctrl_connection(State0), + {noreply, State#state{client = From, caller = quote}}; + +handle_call({_, _Req}, _From, #state{csock = CSock} = State) + when (CSock =:= undefined) -> + {reply, {error, not_connected}, State}; + +handle_call(_, _, #state{chunk = true} = State) -> + {reply, {error, echunk}, State}; + +%% Catch all - This can only happen if the application programmer writes +%% really bad code that violates the API. +handle_call(Request, _Timeout, State) -> + {stop, {'API_violation_connection_closed', Request}, + {error, {connection_terminated, 'API_violation'}}, State}. + +%%-------------------------------------------------------------------------- +%% handle_cast(Request, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handles cast messages. +%%------------------------------------------------------------------------- +handle_cast({Pid, close}, #state{owner = Pid} = State) -> + _ = send_ctrl_message(State, mk_cmd("QUIT", [])), + close_ctrl_connection(State), + close_data_connection(State), + {stop, normal, State#state{csock = undefined, dsock = undefined}}; + +handle_cast({Pid, close}, State) -> + Report = io_lib:format("A none owner process ~p tried to close an " + "ftp connection: ~n", [Pid]), + error_logger:info_report(Report), + {noreply, State}; + +%% Catch all - This can only happen if the application programmer writes +%% really bad code that violates the API. +handle_cast(Msg, State) -> + {stop, {'API_violation_connection_closed', Msg}, State}. + +%%-------------------------------------------------------------------------- +%% handle_info(Msg, State) -> {noreply, State} | {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handles tcp messages from the ftp-server. +%% Note: The order of the function clauses is significant. +%%-------------------------------------------------------------------------- + +handle_info(timeout, #state{caller = open} = State) -> + {stop, timeout, State}; + +handle_info(timeout, State) -> + {noreply, State}; + +%%% Data socket messages %%% +handle_info({Trpt, Socket, Data}, + #state{dsock = {Trpt,Socket}, + caller = {recv_file, Fd}} = State0) when Trpt==tcp;Trpt==ssl -> + ?DBG('L~p --data ~p ----> ~s~p~n',[?LINE,Socket,Data,State0]), + ok = file_write(binary_to_list(Data), Fd), + progress_report({binary, Data}, State0), + State = activate_data_connection(State0), + {noreply, State}; + +handle_info({Trpt, Socket, Data}, #state{dsock = {Trpt,Socket}, client = From, + caller = recv_chunk} + = State) when Trpt==tcp;Trpt==ssl -> + ?DBG('L~p --data ~p ----> ~s~p~n',[?LINE,Socket,Data,State]), + gen_server:reply(From, {ok, Data}), + {noreply, State#state{client = undefined, caller = undefined, data = <<>>}}; + +handle_info({Trpt, Socket, Data}, #state{dsock = {Trpt,Socket}} = State0) when Trpt==tcp;Trpt==ssl -> + ?DBG('L~p --data ~p ----> ~s~p~n',[?LINE,Socket,Data,State0]), + State = activate_data_connection(State0), + {noreply, State#state{data = <<(State#state.data)/binary, + Data/binary>>}}; + +handle_info({Cls, Socket}, #state{dsock = {Trpt,Socket}, + caller = {recv_file, Fd}} = State0) + when {Cls,Trpt}=={tcp_closed,tcp} ; {Cls,Trpt}=={ssl_closed,ssl} -> + file_close(Fd), + progress_report({transfer_size, 0}, State0), + State = activate_ctrl_connection(State0), + ?DBG("Data channel close",[]), + {noreply, State#state{dsock = undefined, data = <<>>}}; + +handle_info({Cls, Socket}, #state{dsock = {Trpt,Socket}, + client = Client, + caller = recv_chunk} = State0) + when {Cls,Trpt}=={tcp_closed,tcp} ; {Cls,Trpt}=={ssl_closed,ssl} -> + ?DBG("Data channel close recv_chunk",[]), + State = activate_ctrl_connection(State0), + {noreply, State#state{dsock = undefined, + caller = #recv_chunk_closing{dconn_closed = true, + client_called_us = Client =/= undefined} + }}; +handle_info({Cls, Socket}, #state{dsock = {Trpt,Socket}, + caller = #recv_chunk_closing{client_called_us = true, + pos_compl_received = true} = R} = State) + when {Cls,Trpt}=={tcp_closed,tcp} ; {Cls,Trpt}=={ssl_closed,ssl} -> + %% Maybe handle unprocessed chunk message before acking final chunk + self() ! {Cls, Socket}, + {noreply, State#state{caller = R#recv_chunk_closing{dconn_closed = true}}}; + +handle_info({Cls, Socket}, #state{dsock = {Trpt,Socket}, caller = recv_bin, + data = Data} = State0) + when {Cls,Trpt}=={tcp_closed,tcp} ; {Cls,Trpt}=={ssl_closed,ssl} -> + ?DBG("Data channel close",[]), + State = activate_ctrl_connection(State0), + {noreply, State#state{dsock = undefined, data = <<>>, + caller = {recv_bin, Data}}}; + +handle_info({Cls, Socket}, #state{dsock = {Trpt,Socket}, data = Data, + caller = {handle_dir_result, Dir}} + = State0) when {Cls,Trpt}=={tcp_closed,tcp} ; {Cls,Trpt}=={ssl_closed,ssl} -> + ?DBG("Data channel close",[]), + State = activate_ctrl_connection(State0), + {noreply, State#state{dsock = undefined, + caller = {handle_dir_result, Dir, Data}, +% data = <<?CR,?LF>>}}; + data = <<>>}}; + +handle_info({Err, Socket, Reason}, #state{dsock = {Trpt,Socket}, + client = From} = State) + when {Err,Trpt}=={tcp_error,tcp} ; {Err,Trpt}=={ssl_error,ssl} -> + gen_server:reply(From, {error, Reason}), + close_data_connection(State), + {noreply, State#state{dsock = undefined, client = undefined, + data = <<>>, caller = undefined, chunk = false}}; + +%%% Ctrl socket messages %%% +handle_info({Transport, Socket, Data}, #state{csock = {Transport, Socket}, + verbose = Verbose, + caller = Caller, + client = From, + ctrl_data = {BinCtrlData, AccLines, + LineStatus}} + = State0) -> + ?DBG('--ctrl ~p ----> ~s~p~n',[Socket,<<BinCtrlData/binary, Data/binary>>,State0]), + case ftp_response:parse_lines(<<BinCtrlData/binary, Data/binary>>, + AccLines, LineStatus) of + {ok, Lines, NextMsgData} -> + verbose(Lines, Verbose, 'receive'), + CtrlResult = ftp_response:interpret(Lines), + case Caller of + quote -> + gen_server:reply(From, string:tokens(Lines, [?CR, ?LF])), + {noreply, State0#state{client = undefined, + caller = undefined, + latest_ctrl_response = Lines, + ctrl_data = {NextMsgData, [], + start}}}; + _ -> + ?DBG(' ...handle_ctrl_result(~p,...) ctrl_data=~p~n',[CtrlResult,{NextMsgData, [], start}]), + handle_ctrl_result(CtrlResult, + State0#state{latest_ctrl_response = Lines, + ctrl_data = + {NextMsgData, [], start}}) + end; + {continue, CtrlData} when CtrlData =/= State0#state.ctrl_data -> + ?DBG(' ...Continue... ctrl_data=~p~n',[CtrlData]), + State1 = State0#state{ctrl_data = CtrlData}, + State = activate_ctrl_connection(State1), + {noreply, State}; + {continue, _CtrlData} -> + ?DBG(' ...Continue... ctrl_data=~p~n',[_CtrlData]), + {noreply, State0} + end; + +%% If the server closes the control channel it is +%% the expected behavior that connection process terminates. +handle_info({Cls, Socket}, #state{csock = {Trpt, Socket}}) + when {Cls,Trpt}=={tcp_closed,tcp} ; {Cls,Trpt}=={ssl_closed,ssl} -> + exit(normal); %% User will get error message from terminate/2 + +handle_info({Err, Socket, Reason}, _) when Err==tcp_error ; Err==ssl_error -> + Report = + io_lib:format("~p on socket: ~p for reason: ~p~n", + [Err, Socket, Reason]), + error_logger:error_report(Report), + %% If tcp does not work the only option is to terminate, + %% this is the expected behavior under these circumstances. + exit(normal); %% User will get error message from terminate/2 + +%% Monitor messages - if the process owning the ftp connection goes +%% down there is no point in continuing. +handle_info({'DOWN', _Ref, _Type, _Process, normal}, State) -> + {stop, normal, State#state{client = undefined}}; + +handle_info({'DOWN', _Ref, _Type, _Process, shutdown}, State) -> + {stop, normal, State#state{client = undefined}}; + +handle_info({'DOWN', _Ref, _Type, _Process, timeout}, State) -> + {stop, normal, State#state{client = undefined}}; + +handle_info({'DOWN', _Ref, _Type, Process, Reason}, State) -> + {stop, {stopped, {'EXIT', Process, Reason}}, + State#state{client = undefined}}; + +handle_info({'EXIT', Pid, Reason}, #state{progress = Pid} = State) -> + Report = io_lib:format("Progress reporting stopped for reason ~p~n", + [Reason]), + error_logger:info_report(Report), + {noreply, State#state{progress = ignore}}; + +%% Catch all - throws away unknown messages (This could happen by "accident" +%% so we do not want to crash, but we make a log entry as it is an +%% unwanted behaviour.) +handle_info(Info, State) -> + Report = io_lib:format("ftp : ~p : Unexpected message: ~p~nState: ~p~n", + [self(), Info, State]), + error_logger:info_report(Report), + {noreply, State}. + +%%-------------------------------------------------------------------------- +%% terminate/2 and code_change/3 +%%-------------------------------------------------------------------------- +terminate(normal, State) -> + %% If terminate reason =/= normal the progress reporting process will + %% be killed by the exit signal. + progress_report(stop, State), + do_terminate({error, econn}, State); +terminate(Reason, State) -> + Report = io_lib:format("Ftp connection closed due to: ~p~n", [Reason]), + error_logger:error_report(Report), + do_terminate({error, eclosed}, State). + +do_terminate(ErrorMsg, State) -> + close_data_connection(State), + close_ctrl_connection(State), + case State#state.client of + undefined -> + ok; + From -> + gen_server:reply(From, ErrorMsg) + end, + ok. + +code_change(_Vsn, State1, upgrade_from_pre_5_12) -> + {state, CSock, DSock, Verbose, LDir, Type, Chunk, Mode, Timeout, + Data, CtrlData, Owner, Client, Caller, IPv6Disable, Progress} = State1, + IpFamily = + if + (IPv6Disable =:= true) -> + inet; + true -> + inet6fb4 + end, + State2 = #state{csock = CSock, + dsock = DSock, + verbose = Verbose, + ldir = LDir, + type = Type, + chunk = Chunk, + mode = Mode, + timeout = Timeout, + data = Data, + ctrl_data = CtrlData, + owner = Owner, + client = Client, + caller = Caller, + ipfamily = IpFamily, + progress = Progress}, + {ok, State2}; + +code_change(_Vsn, State1, downgrade_to_pre_5_12) -> + #state{csock = CSock, + dsock = DSock, + verbose = Verbose, + ldir = LDir, + type = Type, + chunk = Chunk, + mode = Mode, + timeout = Timeout, + data = Data, + ctrl_data = CtrlData, + owner = Owner, + client = Client, + caller = Caller, + ipfamily = IpFamily, + progress = Progress} = State1, + IPv6Disable = + if + (IpFamily =:= inet) -> + true; + true -> + false + end, + State2 = + {state, CSock, DSock, Verbose, LDir, Type, Chunk, Mode, Timeout, + Data, CtrlData, Owner, Client, Caller, IPv6Disable, Progress}, + {ok, State2}; + +code_change(_Vsn, State, _Extra) -> + {ok, State}. + + +%%%========================================================================= +%% Start/stop +%%%========================================================================= +%%-------------------------------------------------------------------------- +%% start_link([Opts, GenServerOptions]) -> {ok, Pid} | {error, Reason} +%% +%% Description: Callback function for the ftp supervisor. It is called +%% : when open or legacy is called. +%%-------------------------------------------------------------------------- +start_link([Opts, GenServerOptions]) -> + start_link(Opts, GenServerOptions). + +start_link(Opts, GenServerOptions) -> + case lists:keysearch(client, 1, Opts) of + {value, _} -> + %% Via the supervisor + gen_server:start_link(?MODULE, Opts, GenServerOptions); + false -> + Opts2 = [{client, self()} | Opts], + gen_server:start_link(?MODULE, Opts2, GenServerOptions) + end. + + +%%% Stop functionality is handled by close/1 + +%%%======================================================================== +%%% Internal functions +%%%======================================================================== + +%%-------------------------------------------------------------------------- +%%% Help functions to handle_call and/or handle_ctrl_result +%%-------------------------------------------------------------------------- +%% User handling +-spec handle_user(User, Password, Account, State) -> Result when + User :: io:format(), + Password :: io:format(), + Account :: io:format(), + State :: #state{}, + Result :: {noreply, #state{}}. +handle_user(User, Password, Acc, State0) -> + _ = send_ctrl_message(State0, mk_cmd("USER ~s", [User])), + State = activate_ctrl_connection(State0), + {noreply, State#state{caller = {handle_user, Password, Acc}}}. + +handle_user_passwd(Password, Acc, State0) -> + _ = send_ctrl_message(State0, mk_cmd("PASS ~s", [Password])), + State = activate_ctrl_connection(State0), + {noreply, State#state{caller = {handle_user_passwd, Acc}}}. + +handle_user_account(Acc, State0) -> + _ = send_ctrl_message(State0, mk_cmd("ACCT ~s", [Acc])), + State = activate_ctrl_connection(State0), + {noreply, State#state{caller = handle_user_account}}. + + +%%-------------------------------------------------------------------------- +%% handle_ctrl_result +%%-------------------------------------------------------------------------- +-type ctrl_status_operation() :: efnamena + | elogin + | enofile + | epath + | error + | etnospc + | epnospc + | efnamena + | econn + | perm_neg_compl + | pos_compl + | pos_interm + | pos_interm_acct + | pos_prel + | tls_upgrade + | trans_neg_compl. + +-spec handle_ctrl_result(Operation, State) -> Result when + Operation :: {ctrl_status_operation(), atom() | string()}, + State :: #state{}, + Result :: {noreply, #state{}, integer()} + | {noreply, #state{}} + | {stop, normal | {error, Reason}, #state{}} + | {error, term()}, + Reason :: term(). +handle_ctrl_result({pos_compl, _}, #state{csock = {tcp, _Socket}, + tls_options = TLSOptions, + timeout = Timeout, + caller = open} + = State0) when is_list(TLSOptions) -> + _ = send_ctrl_message(State0, mk_cmd("AUTH TLS", [])), + State = activate_ctrl_connection(State0), + {noreply, State, Timeout}; + +handle_ctrl_result({tls_upgrade, S}, #state{csock = {tcp, Socket}, + tls_options = TLSOptions, + timeout = Timeout, + caller = open, client = From} + = State0) when is_list(TLSOptions) -> + ?DBG('<--ctrl ssl:connect(~p, ~p)~n~p~n',[Socket,TLSOptions,State0]), + catch ssl:start(), + case ssl:connect(Socket, TLSOptions, Timeout) of + {ok, TLSSocket} when S == ftps -> + State1 = State0#state{csock = {ssl,TLSSocket}}, + State = activate_ctrl_connection(State1), + {noreply, State#state{tls_upgrading_data_connection = pending}, Timeout}; + {ok, TLSSocket} -> + State1 = State0#state{csock = {ssl,TLSSocket}}, + handle_ctrl_result({pos_compl, S}, State1#state{tls_upgrading_data_connection = pending}); + {error, _} = Error -> + gen_server:reply(From, Error), + {stop, normal, State0#state{client = undefined, + caller = undefined, + tls_upgrading_data_connection = false}} + end; + +handle_ctrl_result({pos_compl, _}, #state{tls_upgrading_data_connection = pending} = State0) -> + _ = send_ctrl_message(State0, mk_cmd("PBSZ 0", [])), + State = activate_ctrl_connection(State0), + {noreply, State#state{tls_upgrading_data_connection = {true, pbsz}}}; + +handle_ctrl_result({pos_compl, _}, #state{tls_upgrading_data_connection = {true, pbsz}} = State0) -> + _ = send_ctrl_message(State0, mk_cmd("PROT P", [])), + State = activate_ctrl_connection(State0), + {noreply, State#state{tls_upgrading_data_connection = {true, prot}}}; + +handle_ctrl_result({pos_compl, _}, #state{tls_upgrading_data_connection = {true, prot}, + client = From} = State) -> + gen_server:reply(From, {ok, self()}), + {noreply, State#state{client = undefined, + caller = undefined, + tls_upgrading_data_connection = false}}; +handle_ctrl_result({pos_compl, _}, #state{caller = open, client = From} + = State) -> + gen_server:reply(From, {ok, self()}), + {noreply, State#state{client = undefined, + caller = undefined }}; +handle_ctrl_result({_, Lines}, #state{caller = open} = State) -> + ctrl_result_response(econn, State, {error, Lines}); + +%%-------------------------------------------------------------------------- +%% Data connection setup active mode +handle_ctrl_result({pos_compl, _Lines}, + #state{mode = active, + caller = {setup_data_connection, + {LSock, Caller}}} = State) -> + handle_caller(State#state{caller = Caller, dsock = {lsock, LSock}}); + +handle_ctrl_result({Status, _Lines}, + #state{mode = active, + caller = {setup_data_connection, {LSock, _}}} + = State) -> + close_connection({tcp,LSock}), + ctrl_result_response(Status, State, {error, Status}); + +%% Data connection setup passive mode +handle_ctrl_result({pos_compl, Lines}, + #state{mode = passive, + ipfamily = inet6, + client = From, + caller = {setup_data_connection, Caller}, + csock = CSock, + sockopts_data_passive = SockOpts, + timeout = Timeout} + = State) when is_list(Lines) -> + [_, PortStr | _] = lists:reverse(string:tokens(Lines, "|")), + {ok, {IP, _}} = peername(CSock), + case connect(IP, list_to_integer(PortStr), SockOpts, Timeout, State) of + {ok, _, Socket} -> + handle_caller(State#state{caller = Caller, dsock = {tcp, Socket}}); + {error, _Reason} = Error -> + gen_server:reply(From, Error), + {noreply, State#state{client = undefined, caller = undefined}} + end; + +handle_ctrl_result({pos_compl, Lines}, + #state{mode = passive, + ipfamily = inet, + client = From, + caller = {setup_data_connection, Caller}, + timeout = Timeout, + sockopts_data_passive = SockOpts, + ftp_extension = false} = State) when is_list(Lines) -> + + {_, [?LEFT_PAREN | Rest]} = + lists:splitwith(fun(?LEFT_PAREN) -> false; (_) -> true end, Lines), + {NewPortAddr, _} = + lists:splitwith(fun(?RIGHT_PAREN) -> false; (_) -> true end, Rest), + [A1, A2, A3, A4, P1, P2] = + lists:map(fun(X) -> list_to_integer(X) end, + string:tokens(NewPortAddr, [$,])), + IP = {A1, A2, A3, A4}, + Port = (P1 * 256) + P2, + + ?DBG('<--data tcp connect to ~p:~p, Caller=~p~n',[IP,Port,Caller]), + case connect(IP, Port, SockOpts, Timeout, State) of + {ok, _, Socket} -> + handle_caller(State#state{caller = Caller, dsock = {tcp,Socket}}); + {error, _Reason} = Error -> + gen_server:reply(From, Error), + {noreply,State#state{client = undefined, caller = undefined}} + end; + +handle_ctrl_result({pos_compl, Lines}, + #state{mode = passive, + ipfamily = inet, + client = From, + caller = {setup_data_connection, Caller}, + csock = CSock, + timeout = Timeout, + sockopts_data_passive = SockOpts, + ftp_extension = true} = State) when is_list(Lines) -> + + [_, PortStr | _] = lists:reverse(string:tokens(Lines, "|")), + {ok, {IP, _}} = peername(CSock), + + ?DBG('<--data tcp connect to ~p:~p, Caller=~p~n',[IP,PortStr,Caller]), + case connect(IP, list_to_integer(PortStr), SockOpts, Timeout, State) of + {ok, _, Socket} -> + handle_caller(State#state{caller = Caller, dsock = {tcp, Socket}}); + {error, _Reason} = Error -> + gen_server:reply(From, Error), + {noreply, State#state{client = undefined, caller = undefined}} + end; + + +%% FTP server does not support passive mode: try to fallback on active mode +handle_ctrl_result(_, + #state{mode = passive, + caller = {setup_data_connection, Caller}} = State) -> + setup_data_connection(State#state{mode = active, caller = Caller}); + + +%%-------------------------------------------------------------------------- +%% User handling +handle_ctrl_result({pos_interm, _}, + #state{caller = {handle_user, PassWord, Acc}} = State) -> + handle_user_passwd(PassWord, Acc, State); +handle_ctrl_result({Status, _}, + #state{caller = {handle_user, _, _}} = State) -> + ctrl_result_response(Status, State, {error, euser}); + +%% Accounts +handle_ctrl_result({pos_interm_acct, _}, + #state{caller = {handle_user_passwd, Acc}} = State) + when Acc =/= "" -> + handle_user_account(Acc, State); +handle_ctrl_result({Status, _}, + #state{caller = {handle_user_passwd, _}} = State) -> + ctrl_result_response(Status, State, {error, euser}); + +%%-------------------------------------------------------------------------- +%% Print current working directory +handle_ctrl_result({pos_compl, Lines}, + #state{caller = pwd, client = From} = State) -> + Dir = pwd_result(Lines), + gen_server:reply(From, {ok, Dir}), + {noreply, State#state{client = undefined, caller = undefined}}; + +%%-------------------------------------------------------------------------- +%% Directory listing +handle_ctrl_result({pos_prel, _}, #state{caller = {dir, Dir}} = State0) -> + case accept_data_connection(State0) of + {ok, State1} -> + State = activate_data_connection(State1), + {noreply, State#state{caller = {handle_dir_result, Dir}}}; + {error, _Reason} = Error -> + ctrl_result_response(error, State0, Error) + end; + +handle_ctrl_result({pos_compl, _}, #state{caller = {handle_dir_result, ""=_CurrentDir, + Data}, client = From}= State) -> + gen_server:reply(From, {ok, Data}), + {noreply, State#state{client = undefined, + caller = undefined}}; + +handle_ctrl_result({pos_compl, _}, #state{caller = {handle_dir_result, _Dir, + Data}, client = From}= State) -> + gen_server:reply(From, {ok, Data}), + {noreply, State#state{client = undefined, + caller = undefined}}; + +handle_ctrl_result({pos_compl, _}=Operation, #state{caller = {handle_dir_result, Dir}, + data = Data}= State) -> + handle_ctrl_result(Operation, State#state{caller = {handle_dir_result, Dir, Data}}); + +handle_ctrl_result(S={_Status, _}, + #state{caller = {handle_dir_result, _, _}} = State) -> + %% OTP-5731, macosx + ctrl_result_response(S, State, {error, epath}); + +handle_ctrl_result({Status, _}, #state{caller = cd} = State) -> + ctrl_result_response(Status, State, {error, Status}); + +handle_ctrl_result(Status={epath, _}, #state{caller = {dir,_}} = State) -> + ctrl_result_response(Status, State, {error, epath}); + +%%-------------------------------------------------------------------------- +%% File renaming +handle_ctrl_result({pos_interm, _}, #state{caller = {rename, NewFile}} + = State0) -> + _ = send_ctrl_message(State0, mk_cmd("RNTO ~s", [NewFile])), + State = activate_ctrl_connection(State0), + {noreply, State#state{caller = rename_second_phase}}; + +handle_ctrl_result({Status, _}, + #state{caller = {rename, _}} = State) -> + ctrl_result_response(Status, State, {error, Status}); + +handle_ctrl_result({Status, _}, + #state{caller = rename_second_phase} = State) -> + ctrl_result_response(Status, State, {error, Status}); + +%%-------------------------------------------------------------------------- +%% File handling - recv_bin +handle_ctrl_result({pos_prel, _}, #state{caller = recv_bin} = State0) -> + case accept_data_connection(State0) of + {ok, State1} -> + State = activate_data_connection(State1), + {noreply, State}; + {error, _Reason} = Error -> + ctrl_result_response(error, State0, Error) + end; + +handle_ctrl_result({pos_compl, _}, #state{caller = {recv_bin, Data}, + client = From} = State) -> + gen_server:reply(From, {ok, Data}), + close_data_connection(State), + {noreply, State#state{client = undefined, caller = undefined}}; + +handle_ctrl_result({Status, _}, #state{caller = recv_bin} = State) -> + close_data_connection(State), + ctrl_result_response(Status, State#state{dsock = undefined}, + {error, epath}); + +handle_ctrl_result({Status, _}, #state{caller = {recv_bin, _}} = State) -> + close_data_connection(State), + ctrl_result_response(Status, State#state{dsock = undefined}, + {error, epath}); +%%-------------------------------------------------------------------------- +%% File handling - start_chunk_transfer +handle_ctrl_result({pos_prel, _}, #state{caller = start_chunk_transfer} + = State0) -> + case accept_data_connection(State0) of + {ok, State1} -> + State = start_chunk(State1), + {noreply, State}; + {error, _Reason} = Error -> + ctrl_result_response(error, State0, Error) + end; + +%%-------------------------------------------------------------------------- +%% File handling - chunk_transfer complete + +handle_ctrl_result({pos_compl, _}, #state{client = From, + caller = #recv_chunk_closing{dconn_closed = true, + client_called_us = true, + pos_compl_received = false + }} + = State0) when From =/= undefined -> + %% The pos_compl was the last event we waited for, finnish and clean up + ?DBG("recv_chunk_closing pos_compl, last event",[]), + gen_server:reply(From, ok), + State = activate_ctrl_connection(State0), + {noreply, State#state{caller = undefined, + chunk = false, + client = undefined}}; + +handle_ctrl_result({pos_compl, _}, #state{caller = #recv_chunk_closing{}=R} + = State0) -> + %% Waiting for more, don't care what + ?DBG("recv_chunk_closing pos_compl, wait more",[]), + {noreply, State0#state{caller = R#recv_chunk_closing{pos_compl_received=true}}}; + +handle_ctrl_result({pos_compl, _}, #state{caller = undefined, chunk = true} + = State0) -> + %% Waiting for user to call recv_chunk + {noreply, State0#state{caller = #recv_chunk_closing{pos_compl_received=true}}}; + +%%-------------------------------------------------------------------------- +%% File handling - recv_file +handle_ctrl_result({pos_prel, _}, #state{caller = {recv_file, _}} = State0) -> + case accept_data_connection(State0) of + {ok, State1} -> + State = activate_data_connection(State1), + {noreply, State}; + {error, _Reason} = Error -> + ctrl_result_response(error, State0, Error) + end; + +handle_ctrl_result({Status, _}, #state{caller = {recv_file, Fd}} = State) -> + file_close(Fd), + close_data_connection(State), + ctrl_result_response(Status, State#state{dsock = undefined}, + {error, epath}); +%%-------------------------------------------------------------------------- +%% File handling - transfer_* +handle_ctrl_result({pos_prel, _}, #state{caller = {transfer_file, Fd}} + = State0) -> + case accept_data_connection(State0) of + {ok, State1} -> + send_file(State1, Fd); + {error, _Reason} = Error -> + ctrl_result_response(error, State0, Error) + end; + +handle_ctrl_result({pos_prel, _}, #state{caller = {transfer_data, Bin}} + = State0) -> + case accept_data_connection(State0) of + {ok, State} -> + send_bin(State, Bin); + {error, _Reason} = Error -> + ctrl_result_response(error, State0, Error) + end; + +%%-------------------------------------------------------------------------- +%% Default +handle_ctrl_result({Status, _Lines}, #state{client = From} = State) + when From =/= undefined -> + ctrl_result_response(Status, State, {error, Status}); +handle_ctrl_result(CtrlMsg, #state{caller = undefined} = State) -> + logger:log(info, #{protocol => ftp, unexpected_msg => CtrlMsg}), + {noreply, State}. + +%%-------------------------------------------------------------------------- +%% Help functions to handle_ctrl_result +%%-------------------------------------------------------------------------- + +-spec ctrl_result_response(Status, State, Error) -> Result when + Status :: ctrl_status_operation() | {ctrl_status_operation(), _}, + State :: #state{}, + Error :: {error, string() | Status | atom() | Reason}, + Reason :: term(), + Result :: {noreply, #state{}} + | {stop, normal | {error, Reason}, #state{}} + | {error, term()}. + +ctrl_result_response(pos_compl, #state{client = From} = State, _) -> + gen_server:reply(From, ok), + {noreply, State#state{client = undefined, caller = undefined}}; + +ctrl_result_response(enofile, #state{client = From} = State, _) -> + gen_server:reply(From, {error, enofile}), + {noreply, State#state{client = undefined, caller = undefined}}; + +ctrl_result_response(error, State0, {error, _Reason} = Error) -> + case State0#state.client of + undefined -> + {stop, Error, State0}; + From -> + gen_server:reply(From, Error), + State = activate_ctrl_connection(State0), + {noreply, State} + end; + +ctrl_result_response(Status, #state{client = From} = State, _) + when (Status =:= etnospc) orelse + (Status =:= epnospc) orelse + (Status =:= efnamena) orelse + (Status =:= econn) -> + gen_server:reply(From, {error, Status}), + {stop, normal, State#state{client = undefined}}; + +ctrl_result_response(_, #state{client = From} = State, ErrorMsg) -> + gen_server:reply(From, ErrorMsg), + {noreply, State#state{client = undefined, caller = undefined}}. + +%%-------------------------------------------------------------------------- +-spec handle_caller(State) -> Result when + State :: #state{}, + Result :: {noreply, #state{}}. +handle_caller(#state{caller = {dir, Dir, Len}} = State0) -> + Cmd = case Len of + short -> "NLST"; + long -> "LIST" + end, + _ = case Dir of + "" -> + send_ctrl_message(State0, mk_cmd(Cmd, "")); + _ -> + send_ctrl_message(State0, mk_cmd(Cmd ++ " ~s", [Dir])) + end, + State = activate_ctrl_connection(State0), + {noreply, State#state{caller = {dir, Dir}}}; + +handle_caller(#state{caller = {recv_bin, RemoteFile}} = State0) -> + _ = send_ctrl_message(State0, mk_cmd("RETR ~s", [RemoteFile])), + State = activate_ctrl_connection(State0), + {noreply, State#state{caller = recv_bin}}; + +handle_caller(#state{caller = {start_chunk_transfer, Cmd, RemoteFile}} = + State0) -> + _ = send_ctrl_message(State0, mk_cmd("~s ~s", [Cmd, RemoteFile])), + State = activate_ctrl_connection(State0), + {noreply, State#state{caller = start_chunk_transfer}}; + +handle_caller(#state{caller = {recv_file, RemoteFile, Fd}} = State0) -> + _ = send_ctrl_message(State0, mk_cmd("RETR ~s", [RemoteFile])), + State = activate_ctrl_connection(State0), + {noreply, State#state{caller = {recv_file, Fd}}}; + +handle_caller(#state{caller = {transfer_file, {Cmd, LocalFile, RemoteFile}}, + ldir = LocalDir, client = From} = State0) + when (is_binary(LocalFile) orelse is_list(LocalFile) orelse is_atom(LocalFile)) -> + case file_open(filename:absname(LocalFile, LocalDir), read) of + {ok, Fd} -> + _ = send_ctrl_message(State0, mk_cmd("~s ~s", [Cmd, RemoteFile])), + State = activate_ctrl_connection(State0), + {noreply, State#state{caller = {transfer_file, Fd}}}; + {error, _} -> + gen_server:reply(From, {error, epath}), + {noreply, State0#state{client = undefined, caller = undefined, + dsock = undefined}} + end; + +handle_caller(#state{caller = {transfer_data, {Cmd, Bin, RemoteFile}}} = + State0) -> + _ = send_ctrl_message(State0, mk_cmd("~s ~s", [Cmd, RemoteFile])), + State = activate_ctrl_connection(State0), + {noreply, State#state{caller = {transfer_data, Bin}}}. + +%% ----------- FTP SERVER COMMUNICATION ------------------------- + +%% Connect to FTP server at Host (default is TCP port 21) +%% in order to establish a control connection. +-spec setup_ctrl_connection(Host, Port, Timeout, State) -> Result when + Host :: inet:ip_address() | inet:hostname(), + Port :: inet:port_number(), + Timeout :: non_neg_integer(), + State :: #state{}, + Reason :: timeout | inet:posix(), + Result :: {ok, State, integer()} | {error, Reason}. +setup_ctrl_connection(Host, Port, Timeout, #state{sockopts_ctrl = SockOpts} = State0) -> + MsTime = erlang:monotonic_time(), + case connect(Host, Port, SockOpts, Timeout, State0) of + {ok, IpFam, CSock} -> + State1 = State0#state{csock = {tcp, CSock}, ipfamily = IpFam}, + State = activate_ctrl_connection(State1), + case Timeout - millisec_passed(MsTime) of + Timeout2 when (Timeout2 >= 0) -> + {ok, State#state{caller = open}, Timeout2}; + _ -> + %% Oups: Simulate timeout + {ok, State#state{caller = open}, 0} + end; + Error -> + Error + end. + +-spec setup_data_connection(State) -> Result when + State :: #state{}, + Result :: {noreply, State}. +setup_data_connection(#state{mode = active, + caller = Caller, + csock = CSock, + sockopts_data_active = SockOpts, + ftp_extension = FtpExt} = State0) -> + case (catch sockname(CSock)) of + {ok, {{_, _, _, _, _, _, _, _} = IP0, _}} -> + IP = proplists:get_value(ip, SockOpts, IP0), + {ok, LSock} = + gen_tcp:listen(0, [{ip, IP}, {active, false}, + inet6, binary, {packet, 0} | + lists:keydelete(ip,1,SockOpts)]), + {ok, {_, Port}} = sockname({tcp,LSock}), + IpAddress = inet_parse:ntoa(IP), + Cmd = mk_cmd("EPRT |2|~s|~p|", [IpAddress, Port]), + _ = send_ctrl_message(State0, Cmd), + State = activate_ctrl_connection(State0), + {noreply, State#state{caller = {setup_data_connection, + {LSock, Caller}}}}; + {ok, {{_,_,_,_} = IP0, _}} -> + IP = proplists:get_value(ip, SockOpts, IP0), + {ok, LSock} = gen_tcp:listen(0, [{ip, IP}, {active, false}, + binary, {packet, 0} | + lists:keydelete(ip,1,SockOpts)]), + {ok, Port} = inet:port(LSock), + _ = case FtpExt of + false -> + {IP1, IP2, IP3, IP4} = IP, + {Port1, Port2} = {Port div 256, Port rem 256}, + send_ctrl_message(State0, + mk_cmd("PORT ~w,~w,~w,~w,~w,~w", + [IP1, IP2, IP3, IP4, Port1, Port2])); + true -> + IpAddress = inet_parse:ntoa(IP), + Cmd = mk_cmd("EPRT |1|~s|~p|", [IpAddress, Port]), + send_ctrl_message(State0, Cmd) + end, + State = activate_ctrl_connection(State0), + {noreply, State#state{caller = {setup_data_connection, + {LSock, Caller}}}} + end; + +setup_data_connection(#state{mode = passive, ipfamily = inet6, + caller = Caller} = State0) -> + _ = send_ctrl_message(State0, mk_cmd("EPSV", [])), + State = activate_ctrl_connection(State0), + {noreply, State#state{caller = {setup_data_connection, Caller}}}; + +setup_data_connection(#state{mode = passive, ipfamily = inet, + caller = Caller, + ftp_extension = false} = State0) -> + _ = send_ctrl_message(State0, mk_cmd("PASV", [])), + State = activate_ctrl_connection(State0), + {noreply, State#state{caller = {setup_data_connection, Caller}}}; + +setup_data_connection(#state{mode = passive, ipfamily = inet, + caller = Caller, + ftp_extension = true} = State0) -> + _ = send_ctrl_message(State0, mk_cmd("EPSV", [])), + State = activate_ctrl_connection(State0), + {noreply, State#state{caller = {setup_data_connection, Caller}}}. + +-spec connect(Host, Port, SockOpts, Timeout, State) -> Result when + Host :: inet:ip_address() | inet:hostname(), + Port :: inet:port_number(), + SockOpts :: [inet:inet_backend() | gen_tcp:connect_option()], + Timeout :: timeout(), + State :: #state{}, + Reason :: timeout | inet:posix(), + Result :: {ok, inet:address_family(), gen_tcp:socket()} | {error, Reason}. +connect(Host, Port, SockOpts, Timeout, #state{ipfamily = inet = IpFam}) -> + connect2(Host, Port, IpFam, SockOpts, Timeout); + +connect(Host, Port, SockOpts, Timeout, #state{ipfamily = inet6 = IpFam}) -> + connect2(Host, Port, IpFam, SockOpts, Timeout); + +connect(Host, Port, SockOpts, Timeout, #state{ipfamily = inet6fb4}) -> + case inet:getaddr(Host, inet6) of + {ok, {0, 0, 0, 0, 0, 16#ffff, _, _} = IPv6} -> + case inet:getaddr(Host, inet) of + {ok, IPv4} -> + IpFam = inet, + connect2(IPv4, Port, IpFam, SockOpts, Timeout); + + _ -> + IpFam = inet6, + connect2(IPv6, Port, IpFam, SockOpts, Timeout) + end; + + {ok, IPv6} -> + IpFam = inet6, + connect2(IPv6, Port, IpFam, SockOpts, Timeout); + + _ -> + case inet:getaddr(Host, inet) of + {ok, IPv4} -> + IpFam = inet, + connect2(IPv4, Port, IpFam, SockOpts, Timeout); + Error -> + Error + end + end. + +-spec connect2(Host, Port, IpFam, SockOpts, Timeout) -> Result when + Host :: inet:socket_address() | inet:hostname(), + Port :: inet:port_number(), + SockOpts :: [inet:inet_backend() | gen_tcp:connect_option()], + Timeout :: timeout(), + IpFam :: inet:address_family(), + Reason :: timeout | inet:posix(), + Result :: {ok, inet:address_family(), gen_tcp:socket()} | {error, Reason}. +connect2(Host, Port, IpFam, SockOpts, Timeout) -> + Opts = [IpFam, binary, {packet, 0}, {active, false} | SockOpts], + case gen_tcp:connect(Host, Port, Opts, Timeout) of + {ok, Sock} -> + {ok, IpFam, Sock}; + Error -> + Error + end. + +-spec accept_data_connection_tls_options(State) -> Result when + State :: #state{}, + Result :: [tuple()]. +accept_data_connection_tls_options(#state{ csock = {ssl,Socket}, tls_options = TO0, tls_ctrl_session_reuse = true }) -> + TO = lists:keydelete(reuse_sessions, 1, TO0), + {ok, [{session_id,SSLSessionId},{session_data,SSLSessionData}]} = ssl:connection_information(Socket, [session_id, session_data]), + lists:keystore(reuse_session, 1, TO, {reuse_session,{SSLSessionId,SSLSessionData}}); +accept_data_connection_tls_options(#state{ tls_options = TO }) -> + TO. + +-spec accept_data_connection(State) -> Result when + State :: #state{}, + Result :: {ok, #state{}} | {error, Reason}, + Reason :: term(). +accept_data_connection(#state{mode = active, + dtimeout = DTimeout, + tls_options = TLSOptions0, + dsock = {lsock, LSock}} = State0) -> + case gen_tcp:accept(LSock, DTimeout) of + {ok, Socket} when is_list(TLSOptions0) -> + gen_tcp:close(LSock), + TLSOptions = accept_data_connection_tls_options(State0), + ?DBG('<--data ssl:connect(~p, ~p)~n~p~n',[Socket,TLSOptions,State0]), + case ssl:connect(Socket, TLSOptions, DTimeout) of + {ok, TLSSocket} -> + {ok, State0#state{dsock={ssl,TLSSocket}}}; + {error, Reason} -> + {error, {ssl_connect_failed, Reason}} + end; + {ok, Socket} -> + gen_tcp:close(LSock), + {ok, State0#state{dsock={tcp,Socket}}}; + {error, Reason} -> + {error, {data_connect_failed, Reason}} + end; + +accept_data_connection(#state{mode = passive, + dtimeout = DTimeout, + dsock = {tcp,Socket}, + tls_options = TLSOptions0} = State) when is_list(TLSOptions0) -> + TLSOptions = accept_data_connection_tls_options(State), + ?DBG('<--data ssl:connect(~p, ~p)~n~p~n',[Socket,TLSOptions,State]), + case ssl:connect(Socket, TLSOptions, DTimeout) of + {ok, TLSSocket} -> + {ok, State#state{dsock={ssl,TLSSocket}}}; + {error, Reason} -> + {error, {ssl_connect_failed, Reason}} + end; +accept_data_connection(#state{mode = passive} = State) -> + {ok,State}. + +-spec send_ctrl_message(State, Message) -> _ when + State :: #state{}, + Message :: [term()]. +send_ctrl_message(_S=#state{csock = Socket, verbose = Verbose}, Message) -> + verbose(lists:flatten(Message),Verbose,send), + ?DBG('<--ctrl ~p ---- ~s~p~n',[Socket,Message,_S]), + _ = send_message(Socket, Message). + +send_data_message(_S=#state{dsock = Socket}, Message) -> + ?DBG('<==data ~p ==== ~s~n~p~n',[Socket,Message,_S]), + case send_message(Socket, Message) of + ok -> + ok; + {error, Reason} -> + Report = io_lib:format("send/2 for socket ~p failed with " + "reason ~p~n", [Socket, Reason]), + error_logger:error_report(Report), + %% If tcp/ssl does not work the only option is to terminate, + %% this is the expected behavior under these circumstances. + exit(normal) %% User will get error message from terminate/2 + end. + +send_message({tcp, Socket}, Message) -> + gen_tcp:send(Socket, Message); +send_message({ssl, Socket}, Message) -> + ssl:send(Socket, Message). + +activate_ctrl_connection(#state{csock = CSock, ctrl_data = {<<>>, _, _}} = State) -> + _ = activate_connection(CSock), + State; +activate_ctrl_connection(#state{csock = CSock} = State0) -> + _ = activate_connection(CSock), + %% We have already received at least part of the next control message, + %% that has been saved in ctrl_data, process this first. + {noreply, State} = handle_info({socket_type(CSock), unwrap_socket(CSock), <<>>}, State0), + State. + +activate_data_connection(#state{dsock = DSock} = State) -> + _ = activate_connection(DSock), + State. + +activate_connection(Socket) -> + case socket_type(Socket) of + tcp -> + _ = activate_connection(inet, tcp_closed, Socket); + ssl -> + _ = activate_connection(ssl, ssl_closed, Socket) + end. + +activate_connection(API, CloseTag, Socket0) -> + Socket = unwrap_socket(Socket0), + case API:setopts(Socket, [{active, once}]) of + ok -> + ok; + {error, _} -> %% inet can return einval instead of closed + self() ! {CloseTag, Socket} + end. + +ignore_return_value(_) -> ok. + +unwrap_socket({tcp,Socket}) -> Socket; +unwrap_socket({ssl,Socket}) -> Socket. + +socket_type({tcp,_Socket}) -> tcp; +socket_type({ssl,_Socket}) -> ssl. + +close_ctrl_connection(#state{csock = undefined}) -> ok; +close_ctrl_connection(#state{csock = Socket}) -> close_connection(Socket). + +close_data_connection(#state{dsock = undefined}) -> ok; +close_data_connection(#state{dsock = Socket}) -> close_connection(Socket). + +close_connection({lsock,Socket}) -> ignore_return_value( gen_tcp:close(Socket) ); +close_connection({tcp, Socket}) -> ignore_return_value( gen_tcp:close(Socket) ); +close_connection({ssl, Socket}) -> ignore_return_value( ssl:close(Socket) ). + +%% ------------ FILE HANDLING ---------------------------------------- +send_file(#state{tls_upgrading_data_connection = {true, CTRL, _}} = State, Fd) -> + {noreply, State#state{tls_upgrading_data_connection = {true, CTRL, ?MODULE, send_file, Fd}}}; +send_file(#state{client = Client}=State0, Fd) -> + case file_read(Fd) of + {ok, N, Bin} when N > 0 -> + send_data_message(State0, Bin), + progress_report({binary, Bin}, State0), + send_file(State0, Fd); + {ok, _, _} -> + file_close(Fd), + close_data_connection(State0), + progress_report({transfer_size, 0}, State0), + State = activate_ctrl_connection(State0), + {noreply, State#state{caller = transfer_file_second_phase, + dsock = undefined}}; + {error, Reason} -> + gen_server:reply(Client, {error, Reason}), + {stop, normal, State0#state{client = undefined}} + end. + +file_open(File, Option) -> + file:open(File, [raw, binary, Option]). + +file_close(Fd) -> + ignore_return_value( file:close(Fd) ). + +file_read(Fd) -> + case file:read(Fd, ?FILE_BUFSIZE) of + {ok, Bytes} when is_binary(Bytes) -> + {ok, byte_size(Bytes), Bytes}; + eof -> + {ok, 0, []}; + Other -> + Other + end. + +file_write(Bytes, Fd) -> + file:write(Fd, Bytes). + +%% -------------- MISC ---------------------------------------------- + +call(GenServer, Msg, Format) -> + call(GenServer, Msg, Format, infinity). +call(GenServer, Msg, Format, Timeout) -> + Req = {self(), Msg}, + case (catch gen_server:call(GenServer, Req, Timeout)) of + {ok, Bin} when is_binary(Bin) andalso (Format =:= string) -> + {ok, binary_to_list(Bin)}; + {'EXIT', _, _} -> + {error, eclosed}; + {'EXIT', _} -> + {error, eclosed}; + Result -> + Result + end. + +cast(GenServer, Msg) -> + gen_server:cast(GenServer, {self(), Msg}). + +send_bin(#state{tls_upgrading_data_connection = {true, CTRL, _}} = State, Bin) -> + State#state{tls_upgrading_data_connection = {true, CTRL, ?MODULE, send_bin, Bin}}; +send_bin(State0, Bin) -> + send_data_message(State0, Bin), + close_data_connection(State0), + State = activate_ctrl_connection(State0), + {noreply, State#state{caller = transfer_data_second_phase, + dsock = undefined}}. + +mk_cmd(Fmt, Args) -> + [io_lib:format(Fmt, Args)| [?CR, ?LF]]. % Deep list ok. + +is_name_sane([]) -> + true; +is_name_sane([?CR| _]) -> + false; +is_name_sane([?LF| _]) -> + false; +is_name_sane([_| Rest]) -> + is_name_sane(Rest). + +pwd_result(Lines) -> + {_, [?DOUBLE_QUOTE | Rest]} = + lists:splitwith(fun(?DOUBLE_QUOTE) -> false; (_) -> true end, Lines), + {Dir, _} = + lists:splitwith(fun(?DOUBLE_QUOTE) -> false; (_) -> true end, Rest), + Dir. + + +key_search(Key, List, Default) -> + case lists:keysearch(Key, 1, List) of + {value, {_,Val}} -> + Val; + false -> + Default + end. + +verbose(Lines, true, Direction) -> + DirStr = + case Direction of + send -> + "Sending: "; + _ -> + "Receiving: " + end, + Str = string:strip(string:strip(Lines, right, ?LF), right, ?CR), + erlang:display(DirStr++Str); +verbose(_, false,_) -> + ok. + +progress(Options) -> + ftp_progress:start_link(Options). + +progress_report(_, #state{progress = ignore}) -> + ok; +progress_report(stop, #state{progress = ProgressPid}) when is_pid(ProgressPid) -> + ftp_progress:stop(ProgressPid); +progress_report({binary, Data}, #state{progress = ProgressPid}) when is_binary(Data), is_pid(ProgressPid) -> + ftp_progress:report(ProgressPid, {transfer_size, byte_size(Data)}); +progress_report(Report, #state{progress = ProgressPid}) when is_pid(ProgressPid) -> + ftp_progress:report(ProgressPid, Report). + + +peername({tcp, Socket}) -> inet:peername(Socket); +peername({ssl, Socket}) -> ssl:peername(Socket). + +sockname({tcp, Socket}) -> inet:sockname(Socket); +sockname({ssl, Socket}) -> ssl:sockname(Socket). + +start_chunk(#state{tls_upgrading_data_connection = {true, CTRL, _}} = State) -> + State#state{tls_upgrading_data_connection = {true, CTRL, ?MODULE, start_chunk, undefined}}; +start_chunk(#state{client = From} = State) -> + gen_server:reply(From, ok), + State#state{chunk = true, + client = undefined, + caller = undefined}. + + +%% This function extracts the start options from the +%% Valid options: +%% debug, +%% verbose +%% ipfamily +%% priority +%% flags (for backward compatibillity) +start_options(Options) -> + case lists:keysearch(flags, 1, Options) of + {value, {flags, Flags}} -> + Verbose = lists:member(verbose, Flags), + IsTrace = lists:member(trace, Flags), + IsDebug = lists:member(debug, Flags), + DebugLevel = + if + (IsTrace =:= true) -> + trace; + IsDebug =:= true -> + debug; + true -> + disable + end, + {ok, [{verbose, Verbose}, + {debug, DebugLevel}, + {priority, low}]}; + false -> + ValidateVerbose = + fun(true) -> true; + (false) -> true; + (_) -> false + end, + ValidateDebug = + fun(trace) -> true; + (debug) -> true; + (disable) -> true; + (_) -> false + end, + ValidatePriority = + fun(low) -> true; + (normal) -> true; + (high) -> true; + (_) -> false + end, + ValidOptions = + [{verbose, ValidateVerbose, false, false}, + {debug, ValidateDebug, false, disable}, + {priority, ValidatePriority, false, low}], + validate_options(Options, ValidOptions, []) + end. + + +%% This function extracts and validates the open options from the +%% Valid options: +%% mode +%% host +%% port +%% timeout +%% dtimeout +%% progress +%% ftp_extension + +-spec open_options([tuple()]) -> {ok, [tuple()]} | no_return(). +open_options(Options) -> + ValidateMode = + fun(active) -> true; + (passive) -> true; + (_) -> false + end, + ValidateHost = + fun(Host) when is_list(Host) -> + true; + (Host) when tuple_size(Host) =:= 4; tuple_size(Host) =:= 8 -> + true; + (_) -> + false + end, + ValidatePort = + fun(Port) when is_integer(Port) andalso (Port >= 0) -> true; + (_) -> false + end, + ValidateIpFamily = + fun(inet) -> true; + (inet6) -> true; + (inet6fb4) -> true; + (_) -> false + end, + ValidateTLS = + fun(TLS) when is_list(TLS) -> true; + (undefined) -> true; + (_) -> false + end, + ValidateTLSSecMethod = + fun(ftpes) -> true; + (ftps) -> true; + (_) -> false + end, + ValidateTLSCtrlSessionReuse = + fun(Reuse) when is_boolean(Reuse) -> true; + (_) -> false + end, + ValidateTimeout = + fun(Timeout) when is_integer(Timeout) andalso (Timeout >= 0) -> true; + (_) -> false + end, + ValidateDTimeout = + fun(DTimeout) when is_integer(DTimeout) andalso (DTimeout >= 0) -> true; + (infinity) -> true; + (_) -> false + end, + ValidateProgress = + fun(ignore) -> + true; + ({Mod, Func, _InitProgress}) when is_atom(Mod) andalso + is_atom(Func) -> + true; + (_) -> + false + end, + ValidateFtpExtension = + fun(true) -> true; + (false) -> true; + (_) -> false + end, + ValidOptions = + [{mode, ValidateMode, false, ?DEFAULT_MODE}, + {host, ValidateHost, true, ehost}, + {port, ValidatePort, false, 0}, + {ipfamily, ValidateIpFamily, false, inet}, + {tls, ValidateTLS, false, undefined}, + {tls_sec_method, ValidateTLSSecMethod, false, ftpes}, + {tls_ctrl_session_reuse, ValidateTLSCtrlSessionReuse, false, false}, + {timeout, ValidateTimeout, false, ?CONNECTION_TIMEOUT}, + {dtimeout, ValidateDTimeout, false, ?DATA_ACCEPT_TIMEOUT}, + {progress, ValidateProgress, false, ?PROGRESS_DEFAULT}, + {ftp_extension, ValidateFtpExtension, false, ?FTP_EXT_DEFAULT}], + validate_options(Options, ValidOptions, []). + +%% validates socket options and set defaults +-spec socket_options(Options :: [{atom(), [term()]}]) -> {ok, tuple()} | no_return(). +socket_options(Options) -> + CtrlOpts = proplists:get_value(sock_ctrl, Options, []), + DataActOpts = proplists:get_value(sock_data_act, Options, CtrlOpts), + DataPassOpts = proplists:get_value(sock_data_pass, Options, CtrlOpts), + case [O || O <- lists:usort(CtrlOpts++DataPassOpts++DataActOpts), + not valid_socket_option(O)] of + [] -> + {ok, {CtrlOpts, DataPassOpts, DataActOpts}}; + Invalid -> + throw({error,{sock_opts,Invalid}}) + end. + + +valid_socket_option(inet ) -> false; +valid_socket_option(inet6 ) -> false; +valid_socket_option({ipv6_v6only, _}) -> false; +valid_socket_option({active,_} ) -> false; +valid_socket_option({packet,_} ) -> false; +valid_socket_option({mode,_} ) -> false; +valid_socket_option(binary ) -> false; +valid_socket_option(list ) -> false; +valid_socket_option({header,_} ) -> false; +valid_socket_option({packet_size,_} ) -> false; +valid_socket_option(_) -> true. + + +-spec validate_options(Options, ValidOptions, Acc) -> Result when + Options :: [tuple()], + ValidOptions :: [tuple()], + Acc :: [tuple()], + Result :: {ok, [tuple()]} | no_return(). +validate_options([], [], Acc) -> + {ok, lists:reverse(Acc)}; +validate_options([], ValidOptions, Acc) -> + %% Check if any mandatory options are missing! + case [{Key, Reason} || {Key, _, true, Reason} <- ValidOptions] of + [] -> + Defaults = + [{Key, Default} || {Key, _, _, Default} <- ValidOptions], + {ok, lists:reverse(Defaults ++ Acc)}; + [{_, Reason}|_Missing] -> + throw({error, Reason}) + end; +validate_options([{Key, Value}|Options], ValidOptions, Acc) -> + case lists:keysearch(Key, 1, ValidOptions) of + {value, {Key, Validate, _, Default}} -> + case (catch Validate(Value)) of + true -> + NewValidOptions = lists:keydelete(Key, 1, ValidOptions), + validate_options(Options, NewValidOptions, + [{Key, Value} | Acc]); + _ -> + NewValidOptions = lists:keydelete(Key, 1, ValidOptions), + validate_options(Options, NewValidOptions, + [{Key, Default} | Acc]) + end; + false -> + validate_options(Options, ValidOptions, Acc) + end; +validate_options([_|Options], ValidOptions, Acc) -> + validate_options(Options, ValidOptions, Acc). + +%% Help function, elapsed milliseconds since T0 +millisec_passed(T0) -> + %% OTP 18 + erlang:convert_time_unit(erlang:monotonic_time() - T0, + native, + micro_seconds) div 1000. diff --git a/lib/ftp/src/ftp_sup.erl b/lib/ftp/src/ftp_sup.erl index f30046802f..51a88e6a6c 100644 --- a/lib/ftp/src/ftp_sup.erl +++ b/lib/ftp/src/ftp_sup.erl @@ -61,7 +61,7 @@ init(_) -> %%==================================================================== child_specs() -> [#{id => undefined, - start => {ftp, start_link, []}, + start => {ftp_internal, start_link, []}, restart => temporary, shutdown => 4000, type => worker, diff --git a/lib/ftp/test/ftp_SUITE.erl b/lib/ftp/test/ftp_SUITE.erl index 9239d18f74..843cd38b8a 100644 --- a/lib/ftp/test/ftp_SUITE.erl +++ b/lib/ftp/test/ftp_SUITE.erl @@ -1078,10 +1078,10 @@ error_datafail(Config) -> % and erlang:group_leader/2 does not work under ct dbg:start(), dbg:tracer(process, {fun - ({trace,P,call,{ftp,verbose,[M,_,'receive']}}, ok) when P == Pid -> Self ! M, ok; + ({trace,P,call,{ftp_internal,verbose,[M,_,'receive']}}, ok) when P == Pid -> Self ! M, ok; (_, ok) -> ok end, ok}), - dbg:tpl(ftp, verbose, []), + dbg:tpl(ftp_internal, verbose, []), dbg:p(Pid, [call]), {error,_} = ftp:ls(Pid), dbg:stop_clear(), -- 2.35.3
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor