%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2002-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %% %% %CopyrightEnd% %% %% -module(ftp). -behaviour(gen_server). -export([start/0, start_service/1, stop/0, stop_service/1, services/0, service_info/1 ]). %% Added for backward compatibility -export([start_standalone/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, mkdir/2, nlist/1, 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/2, 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"). %% 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(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_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 %%%========================================================================= start() -> application:start(ftp). start_standalone(Options) -> try {ok, StartOptions} = start_options(Options), {ok, OpenOptions} = open_options(Options), {ok, SocketOptions} = socket_options(Options), case start_link(StartOptions, []) of {ok, Pid} -> call(Pid, {open, ip_comm, OpenOptions, SocketOptions}, plain); Error1 -> Error1 end catch throw:Error2 -> Error2 end. start_service(Options) -> try {ok, StartOptions} = start_options(Options), {ok, OpenOptions} = open_options(Options), {ok, SocketOptions} = socket_options(Options), case ftp_sup:start_child([[[{client, self()} | StartOptions], []]]) of {ok, Pid} -> call(Pid, {open, ip_comm, OpenOptions, SocketOptions}, plain); Error1 -> Error1 end catch throw:Error2 -> Error2 end. stop() -> application:stop(ftp). stop_service(Pid) -> close(Pid). services() -> [{ftpc, Pid} || {_, Pid, _, _} <- supervisor:which_children(ftp_sup)]. service_info(Pid) -> {ok, Info} = call(Pid, info, list), {ok, [proplists:lookup(mode, Info), proplists:lookup(local_port, Info), proplists:lookup(peer, Info), proplists:lookup(peer_port, Info)]}. %%%========================================================================= %%% API - CLIENT FUNCTIONS %%%========================================================================= %%-------------------------------------------------------------------------- %% open(HostOrOtpList, , ) -> {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()}. %% open({option_list, Options}) when is_list(Options) -> try {ok, StartOptions} = start_options(Options), {ok, OpenOptions} = open_options(Options), {ok, SockOpts} = socket_options(Options), case ftp_sup:start_child([[[{client, self()} | StartOptions], []]]) of {ok, Pid} -> call(Pid, {open, ip_comm, OpenOptions, SockOpts}, plain); Error1 -> Error1 end catch throw:Error2 -> Error2 end; %% open(Host) -> open(Host, []). -spec open(Host :: string() | inet:ip_address(), Opts :: list()) -> {'ok', Pid :: pid()} | {'error', Reason :: 'ehost' | term()}. %% open(Host, Port) when is_integer(Port) -> open(Host, [{port, Port}]); %% open(Host, Opts) when is_list(Opts) -> try {ok, StartOptions} = start_options(Opts), {ok, OpenOptions} = open_options([{host, Host}|Opts]), {ok, SocketOptions} = socket_options(Opts), case start_link(StartOptions, []) of {ok, Pid} -> do_open(Pid, OpenOptions, SocketOptions, tls_options(Opts)); Error1 -> Error1 end catch throw:Error2 -> Error2 end. do_open(Pid, OpenOptions, SocketOptions, TLSOpts) -> case call(Pid, {open, ip_comm, OpenOptions, SocketOptions}, plain) of {ok, Pid} -> maybe_tls_upgrade(Pid, TLSOpts); Error -> Error end. %%-------------------------------------------------------------------------- %% user(Pid, User, Pass, ) -> 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()}. 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(), 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(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()}. account(Pid, Acc) -> case is_name_sane(Acc) of true -> call(Pid, {account, Acc}, atom); _ -> {error, eacct} end. %%-------------------------------------------------------------------------- %% 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()}. pwd(Pid) -> call(Pid, pwd, ctrl). %%-------------------------------------------------------------------------- %% lpwd(Pid) -> {ok, Dir} %% Pid = pid() %% Dir = string() %% %% Description: Get the current working directory at local server. %%-------------------------------------------------------------------------- -spec lpwd(Pid :: pid()) -> {'ok', Dir :: string()}. lpwd(Pid) -> call(Pid, lpwd, string). %%-------------------------------------------------------------------------- %% 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()}. cd(Pid, Dir) -> case is_name_sane(Dir) of true -> call(Pid, {cd, Dir}, atom); _ -> {error, efnamena} end. %%-------------------------------------------------------------------------- %% 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()}. lcd(Pid, Dir) -> call(Pid, {lcd, Dir}, string). %%-------------------------------------------------------------------------- %% ls(Pid) -> Result %% ls(Pid, ) -> 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()}. ls(Pid) -> ls(Pid, ""). -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. %%-------------------------------------------------------------------------- %% 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()}. nlist(Pid) -> nlist(Pid, ""). -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. %%-------------------------------------------------------------------------- %% 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()}. 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. %%-------------------------------------------------------------------------- %% 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()}. delete(Pid, File) -> case is_name_sane(File) of true -> call(Pid, {delete, File}, string); _ -> {error, efnamena} end. %%-------------------------------------------------------------------------- %% 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()}. mkdir(Pid, Dir) -> case is_name_sane(Dir) of true -> call(Pid, {mkdir, Dir}, atom); _ -> {error, efnamena} end. %%-------------------------------------------------------------------------- %% 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()}. rmdir(Pid, Dir) -> case is_name_sane(Dir) of true -> call(Pid, {rmdir, Dir}, atom); _ -> {error, efnamena} end. %%-------------------------------------------------------------------------- %% 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()}. type(Pid, Type) -> call(Pid, {type, Type}, atom). %%-------------------------------------------------------------------------- %% 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()}. 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. %%-------------------------------------------------------------------------- %% 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()}. recv_bin(Pid, RemoteFile) -> case is_name_sane(RemoteFile) of true -> call(Pid, {recv_bin, RemoteFile}, bin); _ -> {error, efnamena} end. %%-------------------------------------------------------------------------- %% 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()}. recv_chunk_start(Pid, RemoteFile) -> case is_name_sane(RemoteFile) of true -> call(Pid, {recv_chunk_start, RemoteFile}, atom); _ -> {error, efnamena} end. %%-------------------------------------------------------------------------- %% 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()}. recv_chunk(Pid) -> call(Pid, recv_chunk, atom). %%-------------------------------------------------------------------------- %% 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()}. 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. %%-------------------------------------------------------------------------- %% 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() %% %% 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. %%-------------------------------------------------------------------------- %% 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. %%-------------------------------------------------------------------------- -spec append_chunk_start(Pid :: pid(), RemoteFile :: string()) -> '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. %%-------------------------------------------------------------------------- %% 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}. %%-------------------------------------------------------------------------- %% 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()}. append_chunk(Pid, Bin) when is_binary(Bin) -> call(Pid, {transfer_chunk, Bin}, atom); append_chunk(_Pid, _Bin) -> {error, enotbinary}. %%-------------------------------------------------------------------------- %% 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()}. send_chunk_end(Pid) -> call(Pid, chunk_end, atom). %%-------------------------------------------------------------------------- %% 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()}. append_chunk_end(Pid) -> call(Pid, chunk_end, atom). %%-------------------------------------------------------------------------- %% 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()}. append(Pid, LocalFileName) -> append(Pid, LocalFileName, LocalFileName). -spec append(Pid :: pid(), LocalFileName :: string(), RemoteFileName :: string()) -> 'ok' | {'error', Reason :: term()}. append(Pid, LocalFileName, RemotFileName) -> case is_name_sane(RemotFileName) of true -> call(Pid, {append, LocalFileName, RemotFileName}, atom); _ -> {error, efnamena} end. %%-------------------------------------------------------------------------- %% 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()}. 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}. %%-------------------------------------------------------------------------- %% quote(Pid, Cmd) -> list() %% Pid = pid() %% Cmd = string() %% %% Description: Send arbitrary ftp command. %%-------------------------------------------------------------------------- -spec quote(Pid :: pid(), Cmd :: string()) -> list(). quote(Pid, Cmd) when is_list(Cmd) -> call(Pid, {quote, Cmd}, atom). %%-------------------------------------------------------------------------- %% close(Pid) -> ok %% Pid = pid() %% %% Description: End the ftp session. %%-------------------------------------------------------------------------- -spec close(Pid :: pid()) -> 'ok'. close(Pid) -> cast(Pid, close), ok. %%-------------------------------------------------------------------------- %% formaterror(Tag) -> string() %% Tag = atom() | {error, atom()} %% %% Description: Return diagnostics. %%-------------------------------------------------------------------------- -spec formaterror(Tag :: term()) -> string(). formaterror(Tag) -> ftp_response:error_string(Tag). info(Pid) -> call(Pid, info, list). %%-------------------------------------------------------------------------- %% 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, Opts, {CtrlOpts, DataPassOpts, DataActOpts}}}, From, State) -> case key_search(host, Opts, undefined) of undefined -> {stop, normal, {error, ehost}, State}; Host -> Mode = key_search(mode, Opts, ?DEFAULT_MODE), Port = key_search(port, Opts, ?FTP_PORT), 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), State2 = State#state{client = From, mode = Mode, progress = progress(Progress), ipfamily = IpFamily, sockopts_ctrl = CtrlOpts, sockopts_data_passive = DataPassOpts, sockopts_data_active = DataActOpts, dtimeout = DTimeout, ftp_extension = FtpExt}, case setup_ctrl_connection(Host, Port, Timeout, State2) of {ok, State3, WaitTimeout} -> {noreply, State3, WaitTimeout}; {error, _Reason} -> gen_server:reply(From, {error, ehost}), {stop, normal, State2#state{client = undefined}} end end; handle_call({_, {open, tls_upgrade, TLSOptions}}, From, State0) -> _ = send_ctrl_message(State0, mk_cmd("AUTH TLS", [])), State = activate_ctrl_connection(State0), {noreply, State#state{client = From, caller = open, tls_options = TLSOptions}}; 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, 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("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{} = 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 oly 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, 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_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 = <>}}; 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 = {CtrlData, AccLines, LineStatus}} = State0) -> ?DBG('--ctrl ~p ----> ~s~p~n',[Socket,<>,State]), case ftp_response:parse_lines(<>, 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, NewCtrlData} -> ?DBG(' ...Continue... ctrl_data=~p~n',[NewCtrlData]), State = activate_ctrl_connection(State0), {noreply, State#state{ctrl_data = NewCtrlData}} 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 start_service/1 calls ftp_sup:start_child/1 to start an %% : instance of the ftp process. Also called by start_standalone/1 %%-------------------------------------------------------------------------- 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 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 %%-------------------------------------------------------------------------- handle_ctrl_result({tls_upgrade, _}, #state{csock = {tcp, Socket}, tls_options = TLSOptions, timeout = Timeout, caller = open, client = From} = State0) -> ?DBG('<--ctrl ssl:connect(~p, ~p)~n~p~n',[Socket,TLSOptions,State0]), case ssl:connect(Socket, TLSOptions, Timeout) of {ok, TLSSocket} -> State1 = State0#state{csock = {ssl,TLSSocket}}, _ = send_ctrl_message(State1, mk_cmd("PBSZ 0", [])), State = activate_ctrl_connection(State1), {noreply, State#state{tls_upgrading_data_connection = {true, pbsz}} }; {error, _} = Error -> gen_server:reply(From, {Error, self()}), {stop, normal, State0#state{client = undefined, caller = undefined, tls_upgrading_data_connection = false}} end; 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 -> case State0#state.client of undefined -> {stop, ERROR, State0}; From -> gen_server:reply(From, ERROR), {stop, normal, State0#state{client = undefined}} end end; handle_ctrl_result({pos_compl, _}, #state{caller = {handle_dir_result, Dir, Data}, client = From} = State) -> case Dir of "" -> % Current directory gen_server:reply(From, {ok, Data}), {noreply, State#state{client = undefined, caller = undefined}}; _ -> %% %% Dir cannot be assumed to be a dir. It is a string that %% could be a dir, but could also be a file or even a string %% containing wildcards (*). %% %% %% If there is only one line it might be a directory with one %% %% file but it might be an error message that the directory %% %% was not found. So in this case we have to endure a little %% %% overhead to be able to give a good return value. Alas not %% %% all ftp implementations behave the same and returning %% %% an error string is allowed by the FTP RFC. %% case lists:dropwhile(fun(?CR) -> false;(_) -> true end, %% binary_to_list(Data)) of %% L when (L =:= [?CR, ?LF]) orelse (L =:= []) -> %% send_ctrl_message(State, mk_cmd("PWD", [])), %% activate_ctrl_connection(State), %% {noreply, %% State#state{caller = {handle_dir_data, Dir, Data}}}; %% _ -> %% gen_server:reply(From, {ok, Data}), %% {noreply, State#state{client = undefined, %% caller = undefined}} %% end %% gen_server:reply(From, {ok, Data}), {noreply, State#state{client = undefined, caller = undefined}} end; handle_ctrl_result({pos_compl, Lines}, #state{caller = {handle_dir_data, Dir, DirData}} = State0) -> OldDir = pwd_result(Lines), _ = send_ctrl_message(State0, mk_cmd("CWD ~s", [Dir])), State = activate_ctrl_connection(State0), {noreply, State#state{caller = {handle_dir_data_second_phase, OldDir, DirData}}}; handle_ctrl_result({Status, _}, #state{caller = {handle_dir_data, _, _}} = State) -> ctrl_result_response(Status, State, {error, epath}); handle_ctrl_result(S={_Status, _}, #state{caller = {handle_dir_result, _, _}} = State) -> %% OTP-5731, macosx ctrl_result_response(S, State, {error, epath}); handle_ctrl_result({pos_compl, _}, #state{caller = {handle_dir_data_second_phase, OldDir, DirData}} = State0) -> _ = send_ctrl_message(State0, mk_cmd("CWD ~s", [OldDir])), State = activate_ctrl_connection(State0), {noreply, State#state{caller = {handle_dir_data_third_phase, DirData}}}; handle_ctrl_result({Status, _}, #state{caller = {handle_dir_data_second_phase, _, _}} = State) -> ctrl_result_response(Status, State, {error, epath}); handle_ctrl_result(_, #state{caller = {handle_dir_data_third_phase, DirData}, client = From} = State) -> gen_server:reply(From, {ok, DirData}), {noreply, State#state{client = undefined, caller = undefined}}; 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 -> case State0#state.client of undefined -> {stop, ERROR, State0}; From -> gen_server:reply(From, ERROR), {stop, normal, State0#state{client = undefined}} end 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{client = From, caller = start_chunk_transfer} = State0) -> case accept_data_connection(State0) of {ok, State1} -> State = start_chunk(State1), {noreply, State}; {error, _Reason} = ERROR -> case State0#state.client of undefined -> {stop, ERROR, State0}; From -> gen_server:reply(From, ERROR), {stop, normal, State0#state{client = undefined}} end 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}}}; %%-------------------------------------------------------------------------- %% 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 -> case State0#state.client of undefined -> {stop, ERROR, State0}; From -> gen_server:reply(From, ERROR), {stop, normal, State0#state{client = undefined}} end 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 -> case State0#state.client of undefined -> {stop, ERROR, State0}; From -> gen_server:reply(From, ERROR), {stop, normal, State0#state{client = undefined}} end 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 -> case State0#state.client of undefined -> {stop, ERROR, State0}; From -> gen_server:reply(From, ERROR), {stop, normal, State0#state{client = undefined}} end end; %%-------------------------------------------------------------------------- %% Default handle_ctrl_result({Status, _Lines}, #state{client = From} = State) when From =/= undefined -> ctrl_result_response(Status, State, {error, Status}). %%-------------------------------------------------------------------------- %% Help functions to handle_ctrl_result %%-------------------------------------------------------------------------- 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(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, {error, Status}, State#state{client = undefined}}; {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}}. %%-------------------------------------------------------------------------- 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. 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. 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}}}. 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. 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. accept_data_connection(#state{mode = active, dtimeout = DTimeout, tls_options = TLSOptions, dsock = {lsock, LSock}} = State0) -> case gen_tcp:accept(LSock, DTimeout) of {ok, Socket} when is_list(TLSOptions) -> gen_tcp:close(LSock), ?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 = TLSOptions} = State) when is_list(TLSOptions) -> ?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}. 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) -> ignore_return_value( case socket_type(Socket) of tcp -> inet:setopts(unwrap_socket(Socket), [{active, once}]); ssl -> ssl:setopts(unwrap_socket(Socket), [{active, once}]) 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} -> {ok, 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}; 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}) -> ftp_progress:report(ProgressPid, {transfer_size, 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). maybe_tls_upgrade(Pid, undefined) -> {ok, Pid}; maybe_tls_upgrade(Pid, TLSOptions) -> catch ssl:start(), call(Pid, {open, tls_upgrade, TLSOptions}, plain). 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 open_options(Options) -> ValidateMode = fun(active) -> true; (passive) -> true; (_) -> false end, ValidateHost = fun(Host) when is_list(Host) -> true; (Host) when is_tuple(Host) andalso ((size(Host) =:= 4) orelse (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, 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, ?FTP_PORT}, {ipfamily, ValidateIpFamily, false, inet}, {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, []). 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. tls_options(Options) -> %% Options will be validated by ssl application proplists:get_value(tls, Options, undefined). 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.