diff --git a/.gitignore b/.gitignore index d4522a0..585643a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .eunit/* +.rebar/* deps/* ebin/* sample/ebin/* diff --git a/.travis.yml b/.travis.yml index 984566b..7536f4a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,3 @@ language: erlang otp_release: - - R16 + - R14B04 \ No newline at end of file diff --git a/Makefile b/Makefile deleted file mode 100644 index fdd07da..0000000 --- a/Makefile +++ /dev/null @@ -1,10 +0,0 @@ -all: compile - -compile: - rebar -v compile skip_deps=true - -test: - rebar -v eunit skip_deps=true - -clean: - rebar clean skip_deps=true diff --git a/erlang-bifrost.spec b/erlang-bifrost.spec deleted file mode 100644 index 874ba3d..0000000 --- a/erlang-bifrost.spec +++ /dev/null @@ -1,86 +0,0 @@ -%global realname bifrost -%global upstream madrat- -%global debug_package %{nil} - -%global git_url https://github.com/%{upstream}/%{realname} - -%if 0%{!?git_tag:1} -%global git_tag HEAD -%endif - -%global git_log %(GITDIR=`mktemp -d` && git clone -q %{git_url} $GITDIR && pushd $GITDIR >/dev/null && git reset -q --hard %{git_tag} && LOG=$(git log -n 1 --format='%h %H %ct') && popd >/dev/null && rm -Rf $GITDIR >/dev/null && echo $LOG) - -%if "%{git_log}" == "" - Can not get log info for %{git_tag} -%endif - -%global git_tag %(echo "%{git_log}" | cut -d ' ' -f1) -%global git_commit %(echo "%{git_log}" | cut -d ' ' -f2) -%global git_commit_time %(echo "%{git_log}" | cut -d ' ' -f3 | date --utc -d - +'%Y%m%d') - -%{echo:Building commit %{git_commit}... -} - -%global patchnumber 0 - -%bcond_without check - -Name: erlang-%{realname} -Version: 0.0.0 -Release: %{git_commit_time}.%{patchnumber}.%{git_tag}%{?dist} -Summary: Pluggable Erlang FTP Server -Group: Development/Libraries -License: MIT -URL: %{git_url} - -BuildRequires: erlang-rebar -%{!?_without_check:BuildRequires: erlang-meck >= 0.8.1} - -Requires: erlang-compiler%{?_isa} -Requires: erlang-crypto%{?_isa} -Requires: erlang-erts%{?_isa} >= R16 -Requires: erlang-inets%{?_isa} -Requires: erlang-kernel%{?_isa} -Requires: erlang-ssl%{?_isa} -Requires: erlang-stdlib%{?_isa} >= R16 -Requires: erlang-syntax_tools%{?_isa} -Provides: %{realname} = %{version}-%{release} - - -%description -Bifrost is an implementation of the FTP protocol that enables you to create an FTP server without worrying about the protocol details. Many legacy business systems still use FTP heavily for data transmission, and Bifrost can help bridge the gap between them and modern distributed systems. For example, using Bifrost you can pretty easily write an FTP server that serves files from Amazon's S3 and a Postgres SQL server instead of a filesystem. - -Bifrost also includes FTP/SSL support, if you supply a certificate. - -This version is fork of https://github.com/thorstadt/bifrost - -%prep -%setup -n %{upstream}-%{realname}-%{version} -T -c -git clone -q %{git_url} `pwd` -git reset -q --hard %{git_tag} -%{?_without_check:sed -rne '/meck/!p' -i.meck rebar.config} - -%build -make - -%install -install -D -m 644 ebin/%{realname}.app $RPM_BUILD_ROOT%{_libdir}/erlang/lib/%{realname}-%{version}/ebin/%{realname}.app -install -m 644 ebin/*.beam $RPM_BUILD_ROOT%{_libdir}/erlang/lib/%{realname}-%{version}/ebin/ -install -D -m 644 include/bifrost.hrl $RPM_BUILD_ROOT%{_libdir}/erlang/lib/%{realname}-%{version}/include/bifrost.hrl - -%check -%if %{with check} - make test -%endif - -%files -%doc LICENSE README.md sample -%dir %{_libdir}/erlang/lib/%{realname}-%{version} -%dir %{_libdir}/erlang/lib/%{realname}-%{version}/ebin -%dir %{_libdir}/erlang/lib/%{realname}-%{version}/include -%{_libdir}/erlang/lib/%{realname}-%{version}/ebin/* -%{_libdir}/erlang/lib/%{realname}-%{version}/include/bifrost.hrl - -%changelog -* Thu Nov 13 2014 madrat- 0.0.0 -- Initial release diff --git a/include/bifrost.hrl b/include/bifrost.hrl index 9825d7e..c054bc7 100644 --- a/include/bifrost.hrl +++ b/include/bifrost.hrl @@ -1,39 +1,38 @@ --record(connection_state, { - remote_address = undefined, % client's ip - authenticated_state = unauthenticated, % current state +-record(connection_state, + { + authenticated_state = unauthenticated, user_name, data_port = undefined, pasv_listen = undefined, ip_address = undefined, rnfr = undefined, - - module, % ftp-implementation module - module_state, % its data - - ssl_mode = disabled, % 'disabled' - NO SSL - % 'enabled' - allowed SSL and FTP - % 'only' - non secured FTP is not allowed - % old true and false also supported + module, + module_state, + ssl_mode = disabled, % 'disabled' - NO SSL + % 'enabled' - allowed SSL and FTP + % 'only' - non secured FTP is not allowed + % old true and false also supported ssl_cert = undefined, ssl_key = undefined, ssl_ca_cert = undefined, - protection_mode = clear, % clear | private + protection_mode = clear, + pb_size = 0, + control_socket = undefined, ssl_socket = undefined, - utf8 = true, - recv_block_size = 64*1024, - send_block_size = 64*1024, - - prev_cmd_notify = undefined, % previous command notification data {command, Arguments} | undefined - control_timeout = infinity, % control connection timeout for prev-command notification = tcp_gen:timeout() - - port_range = 0 % passive mode's port's range: - % 0 = ANY, - % N = {N, 65535} - % {minPort, maxPort} - % another values - will be skipped - }). + recv_block_size = 64*1024, + send_block_size = 64*1024, + prev_cmd_notify = undefined, % previous command notification data { command, Arguments } | undefined + control_timeout = infinity, % control connection timeout for prev-command notification = tcp_gen:timeout() + establish_active_connection_timeout = 60 * 1000 :: pos_integer() | infinity, % timeout on wait establish active connection (60 sec) + establish_passive_connection_timeout = 60 * 1000 :: pos_integer() | infinity, % timeout on wait establish passive connection (60 sec) + port_range = 0 % passive mode's port's range: + % 0 = ANY, + % N = {N, 65535} + % {minPort, maxPort} + % another values - will be skipped + }). -record(file_info, { diff --git a/rebar.config b/rebar.config index c83442a..95371f0 100644 --- a/rebar.config +++ b/rebar.config @@ -1,8 +1,9 @@ -{cover_enabled, true}. -{erl_opts, [debug_info]}. +%% THIS FILE IS GENERATED FOR release_3_10_0 %% -{deps, [ -% {meck, ".*", {git, "git://github.com/eproxus/meck.git", "0.8.1"}} -]}. +{cover_enabled,true}. +{erl_opts,[debug_info]}. +{deps,[{meck,".*", + {git,"git@git.eltex.loc:external/meck.git", + {branch,"release_3_10_0"}}}]}. +{clean_files,["*.eunit","ebin/*.beam"]}. -{clean_files, ["*.eunit", "ebin/*.beam"]}. diff --git a/sample/Makefile b/sample/Makefile deleted file mode 100644 index 3efd76f..0000000 --- a/sample/Makefile +++ /dev/null @@ -1,18 +0,0 @@ -all: compile - -compile: - rebar -v compile skip_deps=true - -start: compile - erl -pa ebin/ -eval 'application:start(bifrost_sample).' - -clean: - rebar clean skip_deps=true - rm -f dialyzer.plt - -dialyzer.plt: - dialyzer --output_plt dialyzer.plt --build_plt --apps erts kernel stdlib crypto mnesia sasl inets ssh eunit ssl compiler runtime_tools public_key tools asn1 hipe syntax_tools - -dialyze: dialyzer.plt compile - dialyzer --plt dialyzer.plt --src src --src ../src - diff --git a/sample/rebar.config b/sample/rebar.config index c4ea43f..63e06e6 100644 --- a/sample/rebar.config +++ b/sample/rebar.config @@ -1,7 +1,5 @@ -{erl_opts, [ debug_info, - {i, "../include"}, - {i, "include"}, - {src_dirs, [ "src", "../src" ]} - ]}. +{erl_opts, [debug_info]}. -{deps, [] }. +{deps, [ + {bifrost, ".*", {git, "https://github.com/dmitrii-zolotarev/bifrost.git", {branch, "master"}}} +]}. diff --git a/sample/src/bifrost_memory_server.erl b/sample/src/bifrost_memory_server.erl index 0333bab..9950f0a 100644 --- a/sample/src/bifrost_memory_server.erl +++ b/sample/src/bifrost_memory_server.erl @@ -7,17 +7,18 @@ -module(bifrost_memory_server). -include("../include/bifrost.hrl"). +-include_lib("eunit/include/eunit.hrl"). -behavior(gen_bifrost_server). % Bifrost callbacks -export([login/3, init/2, - check_user/2, + check_user/2, current_directory/1, make_directory/2, change_directory/2, - list_files/2, + list_files/3, remove_directory/2, remove_file/2, put_file/4, @@ -46,17 +47,16 @@ % Initialize the state init(InitialState, _) -> - error_logger:info_msg("Init ~w...", [?MODULE]), InitialState. % All users w/o any requirements check_user(State, Username) -> - case Username of - "root" -> - {error, "root account is locked for ftp", State}; - _Another -> - {ok, State} - end. + case Username of + "root" -> + {error, "root account is locked for ftp", State}; + _Another -> + {ok, State} + end. % Authenticate the user. Return {false, State} to fail. login(State, _Username, _Password) -> @@ -102,8 +102,8 @@ change_directory(State, Directory) -> {error, State} end. -disconnect(State, _Reason) -> - {ok, State}. +disconnect(_, Reason) -> + ok. % Delete a file remove_file(State, File) -> @@ -153,9 +153,9 @@ remove_directory(State, Directory) -> end. % List files in the current or specified directory. -list_files(State, "") -> - list_files(State, current_directory(State)); -list_files(State, Directory) -> +list_files(State, Options, "") -> + list_files(State, Options, current_directory(State)); +list_files(State, _Options, Directory) -> Target = absolute_path(State, Directory), Fs = get_fs(get_module_state(State)), case fetch_path(Fs, Target) of @@ -180,23 +180,18 @@ list_files(State, Directory) -> % Upload notification is arriving during it with FileRetrievalFun == notification % and _Status done or terminated put_file(State, _ProvidedFileName, notification, _Status) -> - {ok, State}; - + {ok, State}; put_file(State, ProvidedFileName, _Mode, FileRetrievalFun) -> FileName = lists:last(string:tokens(ProvidedFileName, "/")), Target = absolute_path(State, FileName), ModState = get_module_state(State), Fs = get_fs(ModState), - case read_from_fun(FileRetrievalFun) of - {ok, FileBytes, FileSize} -> - NewFs= set_path(Fs, Target, {file, + {ok, FileBytes, FileSize} = read_from_fun(FileRetrievalFun), + NewFs= set_path(Fs, Target, {file, FileBytes, new_file_info(FileName, file, FileSize)}), - NewModState = ModState#msrv_state{fs=NewFs}, - {ok, set_module_state(State, NewModState)}; - {error, Reason} -> - {error, Reason, State} - end. + NewModState = ModState#msrv_state{fs=NewFs}, + {ok, set_module_state(State, NewModState)}. % Returns {ok, fun(ByteCount)}, which is a function that reads ByteCount byes % and itself returns a continuation until {done, State} is returned. @@ -231,17 +226,13 @@ site_help(_) -> {error, not_found}. % Memory Server-specific Functions + read_from_fun(Fun) -> read_from_fun([], 0, Fun). - read_from_fun(Buffer, Count, Fun) -> case Fun() of - {ok, _Bytes, ReadCount} when Count + ReadCount >= 1024*1024 -> - {error, {550, "No space left on device."} }; - {ok, Bytes, ReadCount} -> read_from_fun(Buffer ++ [Bytes], Count + ReadCount, Fun); - done -> {ok, Buffer, Count} end. @@ -383,7 +374,6 @@ set_path({dir, Root, FileInfo}, [Current | Rest], Val) -> % Tests -ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). fs_with_paths([], State) -> State; diff --git a/sample/src/bifrost_sample_app.erl b/sample/src/bifrost_sample_app.erl index 6568c78..c860220 100644 --- a/sample/src/bifrost_sample_app.erl +++ b/sample/src/bifrost_sample_app.erl @@ -2,17 +2,9 @@ -behaviour(application). -export([start/2, stop/1]). --define(PORT, 2121). start(_Type, _Args) -> - case bifrost_sample_sup:start_link([{port, ?PORT}]) of - {ok, Pid} -> - error_logger:info_msg("Started at port ~w...", [?PORT]), - {ok, Pid}; - Else -> - error_logger:error_msg("Start error ~p", [Else]), - Else - end. + bifrost_sample_sup:start_link(). stop(_State) -> ok. diff --git a/sample/src/bifrost_sample_sup.erl b/sample/src/bifrost_sample_sup.erl index e7f66f9..24fbcdb 100644 --- a/sample/src/bifrost_sample_sup.erl +++ b/sample/src/bifrost_sample_sup.erl @@ -8,7 +8,7 @@ -behaviour(supervisor). %% API --export([start_link/1]). +-export([start_link/0]). %% Supervisor callbacks -export([init/1]). @@ -17,16 +17,21 @@ %% API functions %% =================================================================== -start_link(Args) -> - supervisor:start_link({local, ?MODULE}, ?MODULE, Args). +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). %% =================================================================== %% Supervisor callbacks %% =================================================================== -init(Args) -> +init([]) -> {ok, { {one_for_one, 5, 10}, [{bifrost, - {bifrost, start_link, [bifrost_memory_server, Args]}, - permanent, 5000, worker, [bifrost]}]} }. + {bifrost, start_link, [bifrost_memory_server, + [{port, 2121}]]}, + permanent, + 5000, + worker, + [bifrost]} + ]} }. diff --git a/src/bifrost.app.src b/src/bifrost.app.src index b62067d..5e4a298 100644 --- a/src/bifrost.app.src +++ b/src/bifrost.app.src @@ -1,7 +1,7 @@ {application, bifrost, [ {description, "Pluggable FTP server"}, - {vsn, "1"}, + {vsn, "semver"}, {registered, []}, {applications, [ kernel, diff --git a/src/bifrost.erl b/src/bifrost.erl index ac78fd6..7fcfda3 100644 --- a/src/bifrost.erl +++ b/src/bifrost.erl @@ -1,260 +1,263 @@ -%%%============================================================================= +%%%------------------------------------------------------------------- %%% File : bifrost.erl %%% Author : Ryan Crum %%% Description : Pluggable FTP Server gen_server -%%%============================================================================= +%%%------------------------------------------------------------------- + -module(bifrost). -behaviour(gen_server). -include("bifrost.hrl"). --export([start_link/2, establish_control_connection/2, await_connections/2, supervise_connections/1, - supervise_connections/2]). +-ifdef(TEST). +-compile(export_all). +-else. +-export([ + start_link/2, + establish_control_connection/2, + await_connections/2, + supervise_connections/2 + ]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-export([ + init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3 + ]). +-endif. %% TEST -define(MAX_TCPIP_PORT, 65535). --define(REPORT_TAG, ?MODULE). --define(ACK_TIMEOUT, 10000). % 10 second for init -%------------------------------------------------------------------------------- start_link(HookModule, Opts) -> - gen_server:start_link(?MODULE, {HookModule, Opts}, []). + gen_server:start_link(?MODULE, [HookModule, Opts], []). -%------------------------------------------------------------------------------- -% gen_server callbacks implementation -init({HookModule, Opts}) -> - try - DefState = #connection_state{module=HookModule}, - - Port = proplists:get_value(port, Opts, 21), - - SslMode = case proplists:get_value(ssl, Opts, DefState#connection_state.ssl_mode) of - disabled -> - disabled; - false -> - disabled; % legacy - Mode when Mode==enabled; - Mode==only; - Mode==true -> - % is SSL module started? - case lists:any(fun({ssl,_,_})->true;(_A)->false end, application:which_applications()) of - true -> - % convert Mode to enabled/only - if true == Mode -> enabled; true -> Mode end; - false -> - throw({stop, ssl_not_started}) - end - end, - - SslKey = proplists:get_value(ssl_key, Opts), - SslCert = proplists:get_value(ssl_cert, Opts), - CaSslCert = proplists:get_value(ca_ssl_cert, Opts), - - UTF8 = proplists:get_value(utf8, Opts, DefState#connection_state.utf8), - RecvBlockSize = proplists:get_value(recv_block_size, Opts, DefState#connection_state.recv_block_size), - SendBlockSize = proplists:get_value(recv_block_size, Opts, DefState#connection_state.send_block_size), - - ControlTimeout = proplists:get_value(control_timeout, Opts, DefState#connection_state.control_timeout), - - PortRange = case proplists:get_value(port_range, Opts, DefState#connection_state.port_range) of - 0 -> 0; - 1 -> 0; - {0, ?MAX_TCPIP_PORT} -> 0; - {1, ?MAX_TCPIP_PORT} -> 0; - - N when is_integer(N), N>=0, ?MAX_TCPIP_PORT>=N -> - {N, ?MAX_TCPIP_PORT}; - - {N,M} when is_integer(N), N>=0, ?MAX_TCPIP_PORT>=N andalso - is_integer(M), M>=0, ?MAX_TCPIP_PORT>=M, M>=N -> - {N, M}; - - _AnotherValue -> - throw({stop, lists:flatten(io_lib:format("Invalid port_range ~p", [_AnotherValue]))}) - end, - - case listen_socket(Port, [{active, false}, {reuseaddr, true}, list]) of - {ok, Listen} -> - IpAddress = proplists:get_value(ip_address, Opts, get_socket_addr(Listen)), - InitialState = DefState#connection_state{ - ip_address=IpAddress, - ssl_mode=SslMode, ssl_key=SslKey, ssl_cert=SslCert, ssl_ca_cert=CaSslCert, - utf8=UTF8, - recv_block_size=RecvBlockSize, - send_block_size=SendBlockSize, - control_timeout=ControlTimeout, - port_range=PortRange}, - - State = case HookModule:init(InitialState, Opts) of - {error, EReason} -> throw({stop, EReason}); - Value = #connection_state{} -> Value - end, - Supervisor = proc_lib:spawn_link(?MODULE, supervise_connections, [State]), - proc_lib:spawn_link(?MODULE, await_connections, [Listen, Supervisor]), - {ok, {listen_socket, Listen}}; - {error, Error} -> - {stop, Error} - end - catch - Type0:{stop, Reason} -> - error_logger:error_report({?REPORT_TAG, {init, Type0, Reason}}), - {stop, Reason}; - Type1:Exception -> - error_logger:error_report({?REPORT_TAG, {init, Type1, Exception}}), - {stop, Exception} - end. +%% gen_server callbacks implementation +init([HookModule, Opts]) -> + try + DefState = #connection_state{module=HookModule}, + Port = proplists:get_value(port, Opts, 21), + SslMode = case proplists:get_value(ssl, Opts, DefState#connection_state.ssl_mode) of + disabled -> disabled; + false -> disabled; % legacy + Mode when Mode == enabled; + Mode == only; + Mode == true -> + % is SSL module started? + case lists:any(fun({ssl,_,_})->true;(_A)->false end, application:which_applications()) of + true -> + if true == Mode -> enabled; true -> Mode end; + false -> + throw({stop, ssl_not_started}) + end + end, + SslKey = proplists:get_value(ssl_key, Opts), + SslCert = proplists:get_value(ssl_cert, Opts), + CaSslCert = proplists:get_value(ca_ssl_cert, Opts), + UTF8 = proplists:get_value(utf8, Opts, DefState#connection_state.utf8), + RecvBlockSize = proplists:get_value(recv_block_size, Opts, DefState#connection_state.recv_block_size), + SendBlockSize = proplists:get_value(recv_block_size, Opts, DefState#connection_state.send_block_size), + + ControlTimeout = proplists:get_value(control_timeout, Opts, DefState#connection_state.control_timeout), + + PortRange = case proplists:get_value(port_range, Opts, DefState#connection_state.port_range) of + 0 -> 0; + 1 -> 0; + {0, ?MAX_TCPIP_PORT} -> 0; + {1, ?MAX_TCPIP_PORT} -> 0; + N when is_integer(N), N>=0, ?MAX_TCPIP_PORT>=N -> + {N, ?MAX_TCPIP_PORT}; + {N,M} when is_integer(N), N>=0, ?MAX_TCPIP_PORT>=N andalso + is_integer(M), M>=0, ?MAX_TCPIP_PORT>=M, M>=N -> + {N, M}; + _AnotherValue1 -> + throw({stop, lists:flatten(io_lib:format("Invalid port_range ~p", [_AnotherValue1]))}) + end, + IpAddress = proplists:get_value(ip_address, Opts, {0,0,0,0}), + EActiveConnectionTimeout = + case proplists:get_value(establish_active_connection_timeout, Opts, DefState#connection_state.establish_active_connection_timeout) of + EstablishATimeout when is_integer(EstablishATimeout), EstablishATimeout > 0 -> + EstablishATimeout; + infinity -> + infinity; + _AnotherValue2 -> + throw({stop, lists:flatten(io_lib:format("Invalid establish_active_connection_timeout ~p", [_AnotherValue2]))}) + end, + EPassiveConnectionTimeout = + case proplists:get_value(establish_passive_connection_timeout, Opts, DefState#connection_state.establish_passive_connection_timeout) of + EstablishPTimeout when is_integer(EstablishPTimeout), EstablishPTimeout > 0 -> + EstablishPTimeout; + infinity -> + infinity; + _AnotherValue3 -> + throw({stop, lists:flatten(io_lib:format("Invalid establish_passive_connection_timeout ~p", [_AnotherValue3]))}) + end, + case listen_socket(Port, [{active, false}, {reuseaddr, true}, list, {ip, IpAddress}]) of + {ok, Listen} -> + InitialState = DefState#connection_state{ip_address = IpAddress, + ssl_mode = SslMode, + ssl_key = SslKey, + ssl_cert = SslCert, + ssl_ca_cert = CaSslCert, + utf8 = UTF8, + recv_block_size = RecvBlockSize, + send_block_size = SendBlockSize, + control_timeout = ControlTimeout, + establish_active_connection_timeout = EActiveConnectionTimeout, + establish_passive_connection_timeout = EPassiveConnectionTimeout, + port_range = PortRange}, + Self = self(), + Supervisor = proc_lib:spawn_link(?MODULE, + supervise_connections, + [Self, HookModule:init(InitialState, Opts)]), + proc_lib:spawn_link(?MODULE, + await_connections, + [Listen, Supervisor]), + HookModule:clean_alarm(), + {ok, {listen_socket, Listen}}; + {error, Error} -> + error_logger:error_report({bifrost, init_error, Error}), + HookModule:set_alarm(Error), + ignore + end + catch + _Type0:{stop, Reason} -> + error_logger:error_report({bifrost, init_error, Reason}), + HookModule:set_alarm(Reason), + ignore; + _Type1:Exception -> + error_logger:error_report({bifrost, init_exception, Exception}), + HookModule:set_alarm(Exception), + ignore + end. %------------------------------------------------------------------------------- + handle_call(_Request, _From, State) -> Reply = ok, {reply, Reply, State}. -%------------------------------------------------------------------------------- handle_cast(_Msg, State) -> {noreply, State}. -%------------------------------------------------------------------------------- handle_info(_Info, State) -> {noreply, State}. -%------------------------------------------------------------------------------- terminate(_Reason, {listen_socket, Socket}) -> gen_tcp:close(Socket); - terminate(_Reason, _State) -> ok. -%------------------------------------------------------------------------------- -code_change(_OldVsn, _State, _Extra) -> - {error, enotsup}. +code_change(_OldVsn, State, _Extra) -> + {ok, State}. -%------------------------------------------------------------------------------- get_socket_addr(Socket) -> - case inet:sockname(Socket) of - {ok, {Addr, _Port}} -> Addr; - _Any -> - undefined - end. + {ok, {Addr, _Port}} = inet:sockname(Socket), + Addr. %------------------------------------------------------------------------------- get_socket_port(Socket) -> - case inet:sockname(Socket) of - {ok, {_Addr, Port}} -> Port; - _Any -> - undefined - end. + {ok, {_Addr, Port}} = inet:sockname(Socket), + Port. %------------------------------------------------------------------------------- listen_socket({Start, End}, _TcpOpts, _NextPort) when End < Start -> - error_logger:error_report({?REPORT_TAG, {listen, "no free socket"}}), - {error, emfile}; + error_logger:warning_report({bifrost, listen_socket, "no free socket in range"}), + {error, emfile}; listen_socket({Start, End}, TcpOpts, random) -> - % if the for [Start, End] Start Start-1 < End and we have additional item for test - listen_socket({Start-1, End}, TcpOpts, random:uniform(End-Start+1)+Start-1); + %% if the for [Start, End] Start Start-1 < End and we have additional item for test + listen_socket({Start-1, End}, TcpOpts, rand:uniform(End-Start+1)+Start-1); listen_socket({Start, End}, TcpOpts, TryPort) when is_integer(TryPort) -> - case listen_socket(TryPort, TcpOpts) of - {error, eaddrinuse} -> - listen_socket({Start+1, End}, TcpOpts, Start+1); - Another -> - Another - end. + case listen_socket(TryPort, TcpOpts) of + {error, eaddrinuse} -> + listen_socket({Start+1, End}, TcpOpts, Start+1); + Another -> + Another + end. listen_socket({Start, End}, TcpOpts0) -> - % strategy - try a random port, after it - try from start to end - % the assumption a lot of ports and just few connections - TcpOpts = case proplists:get_value(reuseaddr, TcpOpts0, false) of - true -> - error_logger:warning_report({?REPORT_TAG, {listen, "find free listen socket with reuseaddr"}}), - proplists:delete(reuseaddr, TcpOpts0); - false -> - TcpOpts0 - end, - listen_socket({Start, End}, TcpOpts, random); - + %% strategy - try a random port, after it - try from start to end + %% the assumption a lot of ports and just few connections + TcpOpts = case proplists:get_value(reuseaddr, TcpOpts0, false) of + true -> + error_logger:warning_report({bifrost, listen_socket, "find free listen socket with reuseaddr"}), + proplists:delete(reuseaddr, TcpOpts0); + false -> + TcpOpts0 + end, + listen_socket({Start, End}, TcpOpts, random); listen_socket(Port, TcpOpts) when is_integer(Port) -> gen_tcp:listen(Port, TcpOpts). -%------------------------------------------------------------------------------- await_connections(Listen, Supervisor) -> case gen_tcp:accept(Listen) of {ok, Socket} -> - case inet:peername(Socket) of - {ok, {Addr, _Port}} -> - Supervisor!{new_connection, self(), {Socket, Addr}}, - receive - {ack, Worker} -> - % ssl:ssl_accept/2 will return {error, not_owner} otherwise - % BUG - some ugly clients may disconnect in the same packet - gen_tcp:controlling_process(Socket, Worker) - after ?ACK_TIMEOUT -> - error_logger:error_report({?REPORT_TAG, {connection, Addr, timeout}}), - gen_tcp:close(Socket), - skip - end; - {error, Reason} -> - % can not get info about client - skip it - error_logger:error_report({?REPORT_TAG, {peer, Reason}}), - gen_tcp:close(Socket), - skip - end; - Error -> - error_logger:error_report({?REPORT_TAG, {accept, Error}}), + Supervisor ! {new_connection, self(), Socket}, + receive + {ack, Worker} -> + %% ssl:ssl_accept/2 will return {error, not_owner} otherwise + case gen_tcp:controlling_process(Socket, Worker) of + ok -> + ok; + {error, Reason} -> + exit(Reason) + end + end; + _Error -> exit(bad_accept) end, await_connections(Listen, Supervisor). -%------------------------------------------------------------------------------- -supervise_connections(InitialState) -> - supervise_connections(InitialState, establish_control_connection). - -supervise_connections(InitialState, ControlConnection) -> +supervise_connections(ParentPid, InitialState) -> process_flag(trap_exit, true), + erlang:monitor(process, ParentPid), + connections_monitor(InitialState). + +connections_monitor(InitialState) -> receive - {new_connection, Acceptor, {Socket, DstAddr}} -> - WorkerState = updateState(InitialState, Socket, DstAddr), - Worker = proc_lib:spawn_link(?MODULE, ControlConnection, [Socket, WorkerState]), - Acceptor ! {ack, Worker}; + {new_connection, Acceptor, Socket} -> + Worker = proc_lib:spawn_link(?MODULE, + establish_control_connection, + [Socket, InitialState]), + Acceptor ! {ack, Worker}, + connections_monitor(InitialState); {'EXIT', _Pid, normal} -> % not a crash - ok; + connections_monitor(InitialState); {'EXIT', _Pid, shutdown} -> % manual termination, not a crash - ok; + connections_monitor(InitialState); {'EXIT', Pid, Info} -> - error_logger:error_report({?REPORT_TAG, {control_connection, Pid, Info}}), - ok; - Notif -> - error_logger:warning_report({?REPORT_TAG, {control_connection, unexpected, Notif}}), - ok - end, - supervise_connections(InitialState, ControlConnection). + error_logger:error_msg("Control connection ~p crashed: ~p~n", [Pid, Info]), + connections_monitor(InitialState); + {'DOWN', MonitorRef, process, _Object, _Info} -> + erlang:demonitor(MonitorRef, [flush]), + ok; + _ -> + connections_monitor(InitialState) + end. -%------------------------------------------------------------------------------- -updateState(InitialState, Socket, DstAddr) -> +establish_control_connection(Socket, InitialState) -> + respond({gen_tcp, Socket}, 220, "FTP Server Ready"), IpAddress = case InitialState#connection_state.ip_address of - undefined -> get_socket_addr(Socket); - {0, 0, 0, 0} -> get_socket_addr(Socket); - {0, 0, 0, 0, 0, 0, 0, 0}-> get_socket_addr(Socket); + undefined -> get_socket_addr(Socket); + {0, 0, 0, 0} -> get_socket_addr(Socket); + {0, 0, 0, 0, 0, 0} -> get_socket_addr(Socket); Ip -> Ip end, - InitialState#connection_state{ip_address=IpAddress, remote_address=DstAddr}. - -%------------------------------------------------------------------------------- -establish_control_connection(Socket, State) -> - error_logger:warning_report({?REPORT_TAG, {control_connection, establish, - {State#connection_state.remote_address, inet:sockname(Socket), inet:peername(Socket)}}}), - respond({gen_tcp, Socket}, 220, "FTP Server Ready"), - control_loop(none, {gen_tcp, Socket}, State). + control_loop(none, + {gen_tcp, Socket}, + InitialState#connection_state{control_socket=Socket, ip_address=IpAddress}). -%------------------------------------------------------------------------------- control_loop(HookPid, {SocketMod, RawSocket} = Socket, State0) -> case SocketMod:recv(RawSocket, 0, State0#connection_state.control_timeout) of {ok, Input} -> - {Command, Arg} = parse_input(Input), - State = prev_cmd_notify(Socket, State0, done), % get a valid command => let's notify about prev - case ftp_command(Socket, State, Command, Arg) of + {Command, Options, Arg} = parse_input(Input), + State = prev_cmd_notify(Socket, State0, done), % get a valid command => let's notify about prev + case ftp_command(Socket, State, Command, Options, Arg) of {ok, NewState} -> if is_pid(HookPid) -> HookPid ! {new_state, self(), NewState}, @@ -262,7 +265,7 @@ control_loop(HookPid, {SocketMod, RawSocket} = Socket, State0) -> {ack, HookPid} -> control_loop(HookPid, Socket, NewState); {done, HookPid} -> - disconnect(State, {error, breaked}), + disconnect(State, {error, breaked}), {error, closed} end; true -> @@ -272,7 +275,7 @@ control_loop(HookPid, {SocketMod, RawSocket} = Socket, State0) -> control_loop(HookPid, NewSock, NewState); {error, timeout} -> respond(Socket, 412, "Timed out. Closing control connection."), - disconnect(State, {error, timeout}), + disconnect(State, {error, timeout}), SocketMod:close(RawSocket), {error, timeout}; {error, closed} -> @@ -283,23 +286,20 @@ control_loop(HookPid, {SocketMod, RawSocket} = Socket, State0) -> SocketMod:close(RawSocket), {ok, quit}; quit -> - disconnect(State, exit), + disconnect(State, exit), SocketMod:close(RawSocket), {ok, quit} end; - - {error, timeout} -> - NewState = prev_cmd_notify(Socket, State0, timeout), - control_loop(HookPid, Socket, NewState); - + {error, timeout} -> + NewState = prev_cmd_notify(Socket, State0, timeout), + control_loop(HookPid, Socket, NewState); {error, Reason} -> - State = prev_cmd_notify(Socket, State0, terminated), + State = prev_cmd_notify(Socket, State0, terminated), disconnect(State, {error, Reason}), - error_logger:warning_report({?REPORT_TAG, {terminated, Reason, State0}}), + error_logger:warning_report({bifrost, connection_terminated}), {error, Reason} end. -%------------------------------------------------------------------------------- respond(Socket, ResponseCode) -> respond(Socket, ResponseCode, response_code_string(ResponseCode) ++ "."). @@ -307,44 +307,29 @@ respond({SocketMod, Socket}, ResponseCode, Message) -> Line = integer_to_list(ResponseCode) ++ " " ++ to_utf8(Message) ++ "\r\n", SocketMod:send(Socket, Line). -%------------------------------------------------------------------------------- -respondError(Socket, {Code, Message}, _Default) when is_integer(Code) -> - respond(Socket, Code, Message); - -respondError(Socket, Reason, {Code, Message}) -> - respond(Socket, Code, format_error(Message, Reason)). - -%------------------------------------------------------------------------------- respond_raw({SocketMod, Socket}, Line) -> SocketMod:send(Socket, to_utf8(Line) ++ "\r\n"). -%------------------------------------------------------------------------------- respond_feature(Socket, Name, true) -> - respond_raw(Socket, " " ++ Name); + respond_raw(Socket, " " ++ Name); respond_feature(_Socket, _Name, false) -> - ok. + ok. -%------------------------------------------------------------------------------- ssl_options(State) -> [{keyfile, State#connection_state.ssl_key}, {certfile, State#connection_state.ssl_cert}, {cacertfile, State#connection_state.ssl_ca_cert}]. -%------------------------------------------------------------------------------- data_connection(ControlSocket, State) -> respond(ControlSocket, 150), case establish_data_connection(State) of {ok, DataSocket} -> - error_logger:warning_report({?REPORT_TAG, {data_connection, establish, - {State#connection_state.remote_address, inet:sockname(DataSocket), inet:peername(DataSocket)}}}), - % switch socket's block - case inet:setopts(DataSocket, [{recbuf, State#connection_state.recv_block_size}]) of - ok -> - ok; - {error, Reason} -> - error_logger:error_report({?REPORT_TAG, {data_connection, setopts, {Reason, State}}}) - end, - + %% switch socket's block + case inet:setopts(DataSocket, [{recbuf, State#connection_state.recv_block_size}]) of + ok -> ok; + {error, Reason} -> + error_logger:warning_report({bifrost, data_connection_socket, Reason}) + end, case State#connection_state.protection_mode of clear -> {gen_tcp, DataSocket}; @@ -363,28 +348,26 @@ data_connection(ControlSocket, State) -> throw(Error) end. +%% passive -- accepts an inbound connection +establish_data_connection(#connection_state{pasv_listen={passive, Listen, _}, establish_passive_connection_timeout=Timeout}) -> + gen_tcp:accept(Listen, Timeout); -%------------------------------------------------------------------------------- -% passive -- accepts an inbound connection -establish_data_connection(#connection_state{pasv_listen={passive, Listen, _}}) -> - gen_tcp:accept(Listen); - -% active -- establishes an outbound connection -establish_data_connection(#connection_state{data_port={active, Addr, Port}}) -> - gen_tcp:connect(Addr, Port, [{active, false}, binary]). +%% active -- establishes an outbound connection +establish_data_connection(#connection_state{data_port={active, Addr, Port}, establish_active_connection_timeout=Timeout}) -> + gen_tcp:connect(Addr, Port, [{active, false}, binary], Timeout). -%------------------------------------------------------------------------------- pasv_connection(ControlSocket, State) -> case State#connection_state.pasv_listen of {passive, PasvListen, _} -> - % We should only have one passive socket open at a time, so close the current one - % and open a new one. + % We should only have one passive socket open at a time, so close the current one + % and open a new one. gen_tcp:close(PasvListen), pasv_connection(ControlSocket, State#connection_state{pasv_listen=undefined}); undefined -> case listen_socket(State#connection_state.port_range, [{active, false}, binary]) of {ok, Listen} -> - Port = get_socket_port(Listen), + {ok, {_, Port}} = inet:sockname(Listen), + Port = get_socket_port(Listen), Ip = State#connection_state.ip_address, PasvSocketInfo = {passive, Listen, @@ -405,142 +388,144 @@ pasv_connection(ControlSocket, State) -> end end. -%------------------------------------------------------------------------------- + +%%------------------------------------------------------------------------------- %% put_file (stor) need a notification - when next command arrived there is a grarantee %% that previouse 'stor' command was executed successfully %% so this function notify about previous command prev_cmd_notify(_Socket, State, Notif) -> - case State#connection_state.prev_cmd_notify of - undefined -> - State; - {stor, FileName} -> - Mod = State#connection_state.module, - State1 = case ftp_result(State, Mod:put_file(State, FileName, notification, Notif)) of - {ok, NewState} -> - NewState; - {error, Reason, NewState} -> - error_logger:warning_report({?REPORT_TAG, {notify, error, Reason, State}}), - NewState - end, - State1#connection_state{prev_cmd_notify=undefined}; - {Command, _Arg} when is_atom(Command) -> - %skip valid notification for another command - State#connection_state{prev_cmd_notify=undefined}; - - Notify -> - %skip notify - error_logger:warning_report({?REPORT_TAG, {nofity, unsupport, Notify, State}}), - State#connection_state{prev_cmd_notify=undefined} - end. + case State#connection_state.prev_cmd_notify of + undefined -> + State; + {stor, FileName} -> + Mod = State#connection_state.module, + State1 = case ftp_result(State, Mod:put_file(State, FileName, notification, Notif)) of + {ok, NewState} -> + NewState; + {error, Reason, NewState} -> + error_logger:warning_report({bifrost, notify_error, Reason}), + NewState + end, + State1#connection_state{prev_cmd_notify=undefined}; + {Command, _Arg} when is_atom(Command) -> + %%skip valid notification for another command + State#connection_state{prev_cmd_notify=undefined}; + + Notify -> + %%skip notify + error_logger:warning_report({bifrost, unsupported_nofity, Notify}), + State#connection_state{prev_cmd_notify=undefined} + end. -%------------------------------------------------------------------------------- +%%------------------------------------------------------------------------------- disconnect(State, Type) -> Mod = State#connection_state.module, Mod:disconnect(State, Type). -%------------------------------------------------------------------------------- +%%------------------------------------------------------------------------------- -spec ftp_result(#connection_state{}, term()) -> term(). ftp_result(State, {error}) -> - ftp_result(State, error); + ftp_result(State, error); ftp_result(State, {error, error}) -> - ftp_result(State, error); + ftp_result(State, error); ftp_result(State, error) -> - ftp_result(State, {error, State}); + ftp_result(State, {error, State}); ftp_result(_State, {error, #connection_state{}=NewState}) -> - {error, undef, NewState}; + {error, undef, NewState}; ftp_result(State, {error, Reason}) -> - {error, Reason, State}; + {error, Reason, State}; ftp_result(_State, {error, Reason, #connection_state{}=NewState}) -> - {error, Reason, NewState}; + {error, Reason, NewState}; ftp_result(_State, {error, #connection_state{}=NewState, Reason}) -> - {error, Reason, NewState}; + {error, Reason, NewState}; ftp_result(_State, Data) -> - Data. + Data. -spec ftp_result(#connection_state{}, term(), fun()) -> term(). ftp_result(State, Data, UserFunction) -> - ftp_result(State, UserFunction(State, Data)). + ftp_result(State, UserFunction(State, Data)). -%------------------------------------------------------------------------------- + + +%%------------------------------------------------------------------------------- %% FTP COMMANDS -ftp_command(Socket, State, Command, RawArg) -> +ftp_command(Socket, State, Command, Options, RawArg) -> Mod = State#connection_state.module, - case from_utf8(RawArg, State#connection_state.utf8) of - {error, List, _RestData} -> - error_logger:warning_report({?REPORT_TAG, {utf8, invalid, List, State}}), - respond(Socket, 501), - {ok, State}; - {incomplete, List, _Binary} -> - error_logger:warning_report({?REPORT_TAG, {utf8, incomplete, List, State}}), - respond(Socket, 501), - {ok, State}; - Arg -> - State1 = State#connection_state{prev_cmd_notify={Command, Arg}}, - ftp_command(Mod, Socket, State1, Command, Arg) - end. - -ftp_command(_Mod, Socket, _State, quit, _) -> + case from_utf8(RawArg, State#connection_state.utf8) of + {error, List, _RestData} -> + error_logger:warning_report({bifrost, invalid_utf8, List}), + respond(Socket, 501), + {ok, State}; + {incomplete, List, _Binary} -> + error_logger:warning_report({bifrost, incomplete_utf8, List}), + respond(Socket, 501), + {ok, State}; + Arg -> + State1 = State#connection_state{prev_cmd_notify={Command, Arg}}, + ftp_command(Mod, Socket, State1, Command, Options, Arg) + end. + +ftp_command(_Mod, Socket, _State, quit, _, _) -> respond(Socket, 200, "Goodbye."), quit; -ftp_command(_Mod, Socket, State=#connection_state{ssl_mode=disabled}, auth, _Arg) -> - respond(Socket, 504), - {ok, State}; - -ftp_command(_Mod, {_, RawSocket} = Socket, State, auth, Arg) -> - case string:to_lower(Arg) of - "tls" -> - respond(Socket, 234, "Command okay."), - case ssl:ssl_accept(RawSocket, ssl_options(State)) of - {ok, SslSocket} -> - {new_socket,State#connection_state{ssl_socket=SslSocket},{ssl, SslSocket}}; - {error, Reason} -> - % Command itself is executed and 234 is sent - % but if issue at SSL level - just disconnect is a solution - so returns quit - error_logger:error_report({?REPORT_TAG, {ssl_accept, Reason, State}}), - quit - end; - _Method -> - respond(Socket, 502, "Unsupported security extension."), - {ok, State} +ftp_command(_Mod, Socket, State=#connection_state{ssl_mode=disabled}, auth, _, _Arg) -> + respond(Socket, 504), + {ok, State}; + +ftp_command(_Mod, {_, RawSocket} = Socket, State, auth, _, Arg) -> + case string:to_lower(Arg) of + "tls" -> + respond(Socket, 234, "Command okay."), + case ssl:ssl_accept(RawSocket, ssl_options(State)) of + {ok, SslSocket} -> + {new_socket,State#connection_state{ssl_socket=SslSocket},{ssl, SslSocket}}; + {error, Reason} -> + %% Command itself is executed and 234 is sent + %% but if issue at SSL level - just disconnect is a solution - so returns quit + error_logger:error_report({bifrost, ssl_accept, Reason}), + quit + end; + _Method -> + respond(Socket, 502, "Unsupported security extension."), + {ok, State} end; -ftp_command(_Mod, Socket, State, feat, _Arg) -> - respond_raw(Socket, "211-Features"), - respond_feature(Socket, "UTF8", State#connection_state.utf8), - respond_feature(Socket, "AUTH TLS", State#connection_state.ssl_mode =/= disabled), - respond_feature(Socket, "PROT", State#connection_state.ssl_mode =/= disabled), - respond(Socket, 211, "End"), - {ok, State}; +ftp_command(_Mod, Socket, State, feat, _, _Arg) -> + respond_raw(Socket, "211-Features"), + respond_feature(Socket, "UTF8", State#connection_state.utf8), + respond_feature(Socket, "AUTH TLS", State#connection_state.ssl_mode =/= disabled), + respond_feature(Socket, "PROT", State#connection_state.ssl_mode =/= disabled), + respond(Socket, 211, "End"), + {ok, State}; -ftp_command(_Mod, Socket, State, opts, Arg) -> - case string:to_upper(Arg) of - "UTF8 ON" when State#connection_state.utf8 =:= true -> - respond(Socket, 200, "Accepted."); - _Option -> - respond(Socket, 501) - end, - {ok, State}; +ftp_command(_Mod, Socket, State, opts, _, Arg) -> + case string:to_upper(Arg) of + "UTF8 ON" when State#connection_state.utf8 =:= true -> + respond(Socket, 200, "Accepted."); + _Option -> + respond(Socket, 501) + end, + {ok, State}; % Allow only commands 'QUIT', 'AUTH', 'FEAT', 'OPTS' -% for ssl_mode == only -ftp_command(_, Socket, State=#connection_state{ssl_socket=undefined, ssl_mode=only}, _, _) -> - respond(Socket, 534, "Request denied for policy reasons (only ftps allowed)."), - {ok, State}; +% for ssl_mode == only +ftp_command(_, Socket, State=#connection_state{ssl_socket=undefined, ssl_mode=only}, _, _, _) -> + respond(Socket, 534, "Request denied for policy reasons (only ftps allowed)."), + {ok, State}; -ftp_command(_, Socket, State, pasv, _) -> +ftp_command(_, Socket, State, pasv, _, _) -> pasv_connection(Socket, State); - -ftp_command(_, Socket, State, prot, Arg) -> +ftp_command(_, Socket, State, prot, _, Arg) -> ProtMode = case string:to_lower(Arg) of "c" -> clear; _ -> private @@ -548,32 +533,32 @@ ftp_command(_, Socket, State, prot, Arg) -> respond(Socket, 200), {ok, State#connection_state{protection_mode=ProtMode}}; -ftp_command(_, Socket, State, pbsz, "0") -> +ftp_command(_, Socket, State, pbsz, _, "0") -> respond(Socket, 200), {ok, State}; -ftp_command(Mod, Socket, State, user, Arg) -> - case ftp_result(State, Mod:check_user(State, Arg)) of - {ok, NewState} -> - respond(Socket, 331), - {ok, NewState#connection_state{user_name=Arg, authenticated_state=unauthenticated}}; +ftp_command(Mod, Socket, State, user, _, Arg) -> + case ftp_result(State, Mod:check_user(State, Arg)) of + {ok, NewState} -> + respond(Socket, 331), + {ok, NewState#connection_state{user_name=Arg, authenticated_state=unauthenticated}}; {error, Reason, _State} -> - error_logger:warning_report({?REPORT_TAG, {command, user, Reason, State}}), - respondError(Socket, Reason, {421, "Login requirements"}), - {error, auth} - end; + error_logger:warning_report({bifrost, user_check, Reason}), + respond(Socket, 421, format_error("Login requirements", Reason)), + {error, auth} + end; -ftp_command(_, Socket, State, port, Arg) -> +ftp_command(_, Socket, State, port, _, Arg) -> case parse_address(Arg) of {ok, {Addr, Port}} -> respond(Socket, 200), {ok, State#connection_state{data_port = {active, Addr, Port}}}; - _ -> + _ -> respond(Socket, 452, "Error parsing address.") - end; + end; -ftp_command(Mod, Socket, State, pass, Arg) -> +ftp_command(Mod, Socket, State, pass, _, Arg) -> case Mod:login(State, State#connection_state.user_name, Arg) of {true, NewState} -> respond(Socket, 230), @@ -581,52 +566,53 @@ ftp_command(Mod, Socket, State, pass, Arg) -> {false, NewState} -> respond(Socket, 530, "Login incorrect."), {ok, NewState#connection_state{user_name=none, authenticated_state=unauthenticated}}; - _Quit -> + _Quit -> respond(Socket, 530, "Login incorrect."), {error, auth} - end; + end; %% ^^^ from this point down every command requires authentication ^^^ -ftp_command(_, Socket, State=#connection_state{authenticated_state=unauthenticated}, _, _) -> + +ftp_command(_, Socket, State=#connection_state{authenticated_state=unauthenticated}, _, _, _) -> respond(Socket, 530), {ok, State}; -ftp_command(_, Socket, State, rein, _) -> +ftp_command(_, Socket, State, rein, _, _) -> respond(Socket, 200), {ok, State#connection_state{user_name=none,authenticated_state=unauthenticated}}; -ftp_command(Mod, Socket, State, pwd, _) -> +ftp_command(Mod, Socket, State, pwd, _, _) -> respond(Socket, 257, "\"" ++ Mod:current_directory(State) ++ "\""), {ok, State}; -ftp_command(Mod, Socket, State, cdup, _) -> - ftp_command(Mod, Socket, State, cwd, ".."); +ftp_command(Mod, Socket, State, cdup, Options, _) -> + ftp_command(Mod, Socket, State, cwd, Options, ".."); -ftp_command(Mod, Socket, State, cwd, Arg) -> +ftp_command(Mod, Socket, State, cwd, _, Arg) -> case ftp_result(State, Mod:change_directory(State, Arg)) of {ok, NewState} -> - respond(Socket, 250, "Directory changed to \"" ++ Mod:current_directory(NewState) ++ "\"."), + respond(Socket, 250, "Directory changed to \"" ++ Mod:current_directory(NewState) ++ "\"."), {ok, NewState}; {error, Reason, NewState} -> - respondError(Socket, Reason, {550, "Unable to change directory"}), + respond(Socket, 550, format_error("Unable to change directory", Reason)), {ok, NewState} end; -ftp_command(Mod, Socket, State, mkd, Arg) -> +ftp_command(Mod, Socket, State, mkd, _, Arg) -> case ftp_result(State, Mod:make_directory(State, Arg)) of {ok, NewState} -> respond(Socket, 250, "\"" ++ Arg ++ "\" directory created."), {ok, NewState}; {error, Reason, NewState} -> - respondError(Socket, Reason, {550, "Unable to create directory"}), + respond(Socket, 550, format_error("Unable to create directory", Reason)), {ok, NewState} end; -ftp_command(Mod, Socket, State, nlst, Arg) -> - case ftp_result(State, Mod:list_files(State, Arg)) of +ftp_command(Mod, Socket, State, nlst, Options, Arg) -> + case ftp_result(State, Mod:list_files(State, Options, Arg)) of {error, Reason, NewState} -> - respondError(Socket, Reason, {451, "Unable to list"}), + respond(Socket, 451, format_error("Unable to list", Reason)), {ok, NewState}; Files when is_list(Files)-> DataSocket = data_connection(Socket, State), @@ -636,10 +622,10 @@ ftp_command(Mod, Socket, State, nlst, Arg) -> {ok, State} end; -ftp_command(Mod, Socket, State, list, Arg) -> - case ftp_result(State, Mod:list_files(State, Arg)) of +ftp_command(Mod, Socket, State, list, Options, Arg) -> + case ftp_result(State, Mod:list_files(State, Options, Arg)) of {error, Reason, NewState} -> - respondError(Socket, Reason, {451, "Unable to list"}), + respond(Socket, 451, format_error("Unable to list", Reason)), {ok, NewState}; Files when is_list(Files)-> DataSocket = data_connection(Socket, State), @@ -649,31 +635,31 @@ ftp_command(Mod, Socket, State, list, Arg) -> {ok, State} end; -ftp_command(Mod, Socket, State, rmd, Arg) -> +ftp_command(Mod, Socket, State, rmd, _, Arg) -> case ftp_result(State, Mod:remove_directory(State, Arg)) of {ok, NewState} -> respond(Socket, 200), {ok, NewState}; {error, Reason, NewState} -> - respondError(Socket, Reason, {550, "Unable to remove directory"}), - {ok, NewState} - end; + respond(Socket, 550, format_error(550, Reason)), + {ok, NewState} + end; -ftp_command(_, Socket, State, syst, _) -> +ftp_command(_, Socket, State, syst, _, _) -> respond(Socket, 215, "UNIX Type: L8"), {ok, State}; -ftp_command(Mod, Socket, State, dele, Arg) -> +ftp_command(Mod, Socket, State, dele, _, Arg) -> case ftp_result(State, Mod:remove_file(State, Arg)) of {ok, NewState} -> respond(Socket, 250), % see RFC 959 {ok, NewState}; {error, Reason, NewState} -> - respondError(Socket, Reason, {450, "Unable to delete file"}), + respond(Socket, 450, format_error("Unable to delete file", Reason)), {ok, NewState} - end; + end; -ftp_command(Mod, Socket, State, stor, Arg) -> +ftp_command(Mod, Socket, State, stor, _, Arg) -> DataSocket = data_connection(Socket, State), Fun = fun() -> case bf_recv(DataSocket) of @@ -684,17 +670,17 @@ ftp_command(Mod, Socket, State, stor, Arg) -> end end, RetState = case ftp_result(State, Mod:put_file(State, Arg, write, Fun)) of - {ok, NewState} -> + {ok, NewState} -> respond(Socket, 226), - NewState; - {error, Reason, NewState} -> - respondError(Socket, Reason, {451, "Unable to store file"}), - NewState#connection_state{prev_cmd_notify=undefined} + NewState; + {error, Reason, NewState} -> + respond(Socket, 451, format("Error ~p when storing a file.", [Reason])), + NewState#connection_state{prev_cmd_notify=undefined} end, bf_close(DataSocket), {ok, RetState}; -ftp_command(_, Socket, State, type, Arg) -> +ftp_command(_, Socket, State, type, _, Arg) -> case Arg of "I" -> respond(Socket, 200); @@ -705,7 +691,7 @@ ftp_command(_, Socket, State, type, Arg) -> end, {ok, State}; -ftp_command(Mod, Socket, State, site, Arg) -> +ftp_command(Mod, Socket, State, site, _, Arg) -> [Command | Sargs] = string:tokens(Arg, " "), case ftp_result(State, Mod:site_command(State, list_to_atom(string:to_lower(Command)), string:join(Sargs, " "))) of {ok, NewState} -> @@ -715,84 +701,84 @@ ftp_command(Mod, Socket, State, site, Arg) -> respond(Socket, 500), {ok, NewState}; {error, Reason, NewState} -> - respondError(Socket, Reason, {501, "Error completing command"}), + respond(Socket, 501, format("Error completing command (~p).", [Reason])), {ok, NewState} end; -ftp_command(Mod, Socket, State, site_help, _) -> +ftp_command(Mod, Socket, State, site_help, _, _) -> case ftp_result(State, Mod:site_help(State)) of {error, Reason, NewState} -> - respondError(Socket, Reason, {500, "Unable to help site"}), - {ok, NewState}; + respond(Socket, 500, format_error("Unable to help site", Reason)), + {ok, NewState}; {ok, []} -> respond(Socket, 500), - {ok, State}; + {ok, State}; {ok, Commands} -> respond_raw(Socket, "214-The following commands are recognized"), lists:map(fun({CmdName, Descr}) -> - respond_raw(Socket, CmdName ++ " : " ++ Descr) + respond_raw(Socket, CmdName ++ " : " ++ Descr) end, Commands), respond(Socket, 214, "Help OK"), - {ok, State} + {ok, State} end; -ftp_command(Mod, Socket, State, help, Arg) -> +ftp_command(Mod, Socket, State, help, _, Arg) -> LowerArg = string:to_lower(Arg), case LowerArg of "site" -> - ftp_command(Mod, Socket, State, site_help, undefined); + ftp_command(Mod, Socket, State, site_help, undefined, Arg); _ -> respond(Socket, 500), {ok, State} end; -ftp_command(Mod, Socket, State, retr, Arg) -> +ftp_command(Mod, Socket, State, retr, _, Arg) -> try case ftp_result(State, Mod:get_file(State, Arg), - fun (S, {ok, Fun}) when is_function(Fun)-> {ok, Fun, S}; - (_S, Any) -> Any end) of - - {ok, Fun, State0} -> - DataSocket = data_connection(Socket, State0), - case ftp_result(State0, write_fun(State0#connection_state.send_block_size, DataSocket, Fun)) of - {ok, NewState} -> - bf_close(DataSocket), - respond(Socket, 226), - {ok, NewState}; - - {error, Reason, NewState} -> - bf_close(DataSocket), - respondError(Socket,Reason, {451, "Unable to get file"}), - {ok, NewState} - end; - - {error, Reason, NewState} -> - respondError(Socket, Reason, {550, "Unable to get file"}), - {ok, NewState} + fun (S, {ok, Fun}) when is_function(Fun)-> {ok, Fun, S}; + (_S, Any) -> Any end) of + + {ok, Fun, State0} -> + DataSocket = data_connection(Socket, State0), + case ftp_result(State0, write_fun(State0#connection_state.send_block_size, DataSocket, Fun)) of + {ok, NewState} -> + bf_close(DataSocket), + respond(Socket, 226), + {ok, NewState}; + + {error, Reason, NewState} -> + bf_close(DataSocket), + respond(Socket, 451, format_error("Unable to get file", Reason)), + {ok, NewState} + end; + + {error, Reason, NewState} -> + respond(Socket, 550, format_error("Unable to get file", Reason)), + {ok, NewState} end catch - T:Error -> - error_logger:error_report({?REPORT_TAG, {get_file, {Mod, T, Error}, State}}), + Error -> + error_logger:error_msg("~w:get_file Exception ~p", [Mod, Error]), respond(Socket, 550), {ok, State} end; -ftp_command(Mod, Socket, State, mdtm, Arg) -> +ftp_command(Mod, Socket, State, mdtm, _, Arg) -> case ftp_result(State, Mod:file_info(State, Arg)) of {ok, FileInfo} -> respond(Socket, 213, format_mdtm_date(FileInfo#file_info.mtime)), - {ok, State}; + {ok, State}; {error, Reason, NewState} -> - respondError(Socket, Reason, {550, "File unavailable"}), - {ok, NewState} + respond(Socket, 550, format_error(550, Reason)), + {ok, NewState} end; -ftp_command(_, Socket, State, rnfr, Arg) -> +ftp_command(_, Socket, State, rnfr, _, Arg) -> respond(Socket, 350, "Ready for RNTO."), {ok, State#connection_state{rnfr=Arg}}; -ftp_command(Mod, Socket, State, rnto, Arg) -> +ftp_command(Mod, Socket, State, rnto, _, Arg) -> case State#connection_state.rnfr of undefined -> respond(Socket, 503, "RNFR not specified."), @@ -800,7 +786,7 @@ ftp_command(Mod, Socket, State, rnto, Arg) -> Rnfr -> case ftp_result(State, Mod:rename_file(State, Rnfr, Arg)) of {error, Reason, NewState} -> - respondError(Socket, Reason, {550, "Unable to rename"}), + respond(Socket, 550, io_lib:format("Unable to rename (~p).", [Reason])), {ok, NewState}; {ok, NewState} -> respond(Socket, 250, "Rename successful."), @@ -808,57 +794,66 @@ ftp_command(Mod, Socket, State, rnto, Arg) -> end end; -ftp_command(Mod, Socket, State, xcwd, Arg) -> - ftp_command(Mod, Socket, State, cwd, Arg); +ftp_command(Mod, Socket, State, xcwd, Options, Arg) -> + ftp_command(Mod, Socket, State, cwd, Options, Arg); -ftp_command(Mod, Socket, State, xcup, Arg) -> - ftp_command(Mod, Socket, State, cdup, Arg); +ftp_command(Mod, Socket, State, xcup, Options, Arg) -> + ftp_command(Mod, Socket, State, cdup, Options, Arg); -ftp_command(Mod, Socket, State, xmkd, Arg) -> - ftp_command(Mod, Socket, State, mkd, Arg); +ftp_command(Mod, Socket, State, xmkd, Options, Arg) -> + ftp_command(Mod, Socket, State, mkd, Options, Arg); -ftp_command(Mod, Socket, State, xpwd, Arg) -> - ftp_command(Mod, Socket, State, pwd, Arg); +ftp_command(Mod, Socket, State, xpwd, Options, Arg) -> + ftp_command(Mod, Socket, State, pwd, Options, Arg); -ftp_command(Mod, Socket, State, xrmd, Arg) -> - ftp_command(Mod, Socket, State, rmd, Arg); +ftp_command(Mod, Socket, State, xrmd, Options, Arg) -> + ftp_command(Mod, Socket, State, rmd, Options, Arg); -ftp_command(_Mod, Socket, State, size, _Arg) -> - respond(Socket, 550), - {ok, State}; +ftp_command(_Mod, Socket, #connection_state{module = Mod} = State, size, _, Arg) -> + case ftp_result(State, Mod:file_info(State, Arg)) of + {ok, FileInfo} -> + respond(Socket, 213, erlang:integer_to_list(FileInfo#file_info.size)), + {ok, State}; + {error, Reason, NewState} -> + respond(Socket, 550, format_error(550, Reason)), + {ok, NewState} + end; -ftp_command(_, Socket, State, Command, _Arg) -> - error_logger:warning_report({?REPORT_TAG, {command, Command, unrecognized, State}}), +ftp_command(_, Socket, State, Command, _, _Arg) -> + error_logger:warning_report({bifrost, unrecognized_command, Command}), respond(Socket, 500), {ok, State}. -%------------------------------------------------------------------------------- write_fun(SendBlockSize,Socket, Fun) -> case Fun(SendBlockSize) of {ok, Bytes, NextFun} -> bf_send(Socket, Bytes), - write_fun(SendBlockSize,Socket, NextFun); + write_fun(SendBlockSize, Socket, NextFun); {done, NewState} -> {ok, NewState}; - Another -> % errors and etc - Another + Another -> % errors and etc + Another end. -%------------------------------------------------------------------------------- strip_newlines(S) -> lists:foldr(fun(C, A) -> - string:strip(A, right, C) end, + string:strip(A, right, C) end, S, "\r\n"). -%------------------------------------------------------------------------------- parse_input(Input) -> - Tokens = string:tokens(Input, " "), - [Command | Args] = lists:map(fun(S) -> strip_newlines(S) end, - Tokens), - {list_to_atom(string:to_lower(Command)), string:join(Args, " ")}. + [Command | Other] = string:tokens(Input, " "), + %% [Command | Args] = lists:map(fun(S) -> strip_newlines(S) end, + %% Tokens), + Fun = fun + ([$-|Option], {OptsAcc, ArgsAcc}) -> + {[strip_newlines(Option)|OptsAcc], ArgsAcc}; + (Item, {OptsAcc, ArgsAcc}) -> + {OptsAcc, [strip_newlines(Item)|ArgsAcc]} + end, + {Options, Args} = lists:foldl(Fun, {[], []}, Other), + {list_to_atom(string:to_lower(strip_newlines(Command))), lists:reverse(Options), string:join(lists:reverse(Args), " ")}. -%------------------------------------------------------------------------------- list_files_to_socket(DataSocket, Files) -> lists:map(fun(Info) -> bf_send(DataSocket, @@ -866,29 +861,35 @@ list_files_to_socket(DataSocket, Files) -> Files), ok. -%------------------------------------------------------------------------------- list_file_names_to_socket(DataSocket, Files) -> lists:map(fun(Info) -> bf_send(DataSocket, - to_utf8(Info#file_info.name) ++ "\r\n") end, + to_utf8(Info#file_info.name) ++ "\r\n") end, Files), ok. -%------------------------------------------------------------------------------- bf_send({SockMod, Socket}, Data) -> SockMod:send(Socket, Data). -%------------------------------------------------------------------------------- bf_close({SockMod, Socket}) -> SockMod:close(Socket). -%------------------------------------------------------------------------------- bf_recv({SockMod, Socket}) -> SockMod:recv(Socket, 0, infinity). -%------------------------------------------------------------------------------- +%% Adapted from jungerl/ftpd.erl +response_code_string(110) -> "MARK yyyy = mmmm"; +response_code_string(120) -> "Service ready in nnn minutes"; +response_code_string(125) -> "Data connection alredy open; transfere starting"; response_code_string(150) -> "File status okay; about to open data connection"; response_code_string(200) -> "Command okay"; +response_code_string(202) -> "Command not implemented, superfluous at this site"; +response_code_string(211) -> "System status, or system help reply"; +response_code_string(212) -> "Directory status"; +response_code_string(213) -> "File status"; +response_code_string(214) -> "Help message"; +response_code_string(215) -> "UNIX system type"; +response_code_string(220) -> "Service ready for user"; response_code_string(221) -> "Service closing control connection"; response_code_string(225) -> "Data connection open; no transfere in progress"; response_code_string(226) -> "Closing data connection"; @@ -908,13 +909,18 @@ response_code_string(452) -> "Requested action not taken"; response_code_string(500) -> "Syntax error, command unrecognized"; response_code_string(501) -> "Syntax error in parameters or arguments"; response_code_string(502) -> "Command not implemented"; +response_code_string(503) -> "Bad sequence of commands"; +response_code_string(504) -> "Command not implemented for that parameter"; response_code_string(530) -> "Not logged in"; +response_code_string(532) -> "Need account for storing files"; response_code_string(550) -> "Requested action not taken"; +response_code_string(551) -> "Requested action aborted: page type unkown"; response_code_string(552) -> "Requested file action aborted"; +response_code_string(553) -> "Requested action not taken"; response_code_string(_) -> "N/A". -%------------------------------------------------------------------------------- -% Taken from jungerl/ftpd +%% Taken from jungerl/ftpd + file_info_to_string(Info) -> format_type(Info#file_info.type) ++ format_access(Info#file_info.mode) ++ " " ++ @@ -925,12 +931,10 @@ file_info_to_string(Info) -> format_date(Info#file_info.mtime) ++ " " ++ Info#file_info.name. -%------------------------------------------------------------------------------- format_mdtm_date({{Year, Month, Day}, {Hours, Mins, Secs}}) -> lists:flatten(io_lib:format("~4..0B~2..0B~2..0B~2..0B~2..0B~2..0B", [Year, Month, Day, Hours, Mins, erlang:trunc(Secs)])). -%------------------------------------------------------------------------------- format_date({Date, Time}) -> {Year, Month, Day} = Date, {Hours, Min, _} = Time, @@ -943,24 +947,19 @@ format_date({Date, Time}) -> format_time(Hours, Min) end. -%------------------------------------------------------------------------------- format_month_day(Month, Day) -> io_lib:format("~s ~2.2w", [month(Month), Day]). -%------------------------------------------------------------------------------- format_year(Year) -> io_lib:format(" ~5.5w", [Year]). -%------------------------------------------------------------------------------- format_time(Hours, Min) -> io_lib:format(" ~2.2.0w:~2.2.0w", [Hours, Min]). -%------------------------------------------------------------------------------- format_type(file) -> "-"; format_type(dir) -> "d"; format_type(_) -> "?". -%------------------------------------------------------------------------------- type_num(file) -> 1; type_num(dir) -> @@ -968,17 +967,14 @@ type_num(dir) -> type_num(_) -> 0. -%------------------------------------------------------------------------------- format_access(Mode) -> format_rwx(Mode bsr 6) ++ format_rwx(Mode bsr 3) ++ format_rwx(Mode). -%------------------------------------------------------------------------------- format_rwx(Mode) -> [if Mode band 4 == 0 -> $-; true -> $r end, if Mode band 2 == 0 -> $-; true -> $w end, if Mode band 1 == 0 -> $-; true -> $x end]. -%------------------------------------------------------------------------------- format_number(X, N, LeftPad) when X >= 0 -> Ls = integer_to_list(X), Len = length(Ls), @@ -987,7 +983,6 @@ format_number(X, N, LeftPad) when X >= 0 -> lists:duplicate(N - Len, LeftPad) ++ Ls end. -%------------------------------------------------------------------------------- month(1) -> "Jan"; month(2) -> "Feb"; month(3) -> "Mar"; @@ -1001,15 +996,13 @@ month(10) -> "Oct"; month(11) -> "Nov"; month(12) -> "Dec". -%------------------------------------------------------------------------------- -% parse address on form: -% d1,d2,d3,d4,p1,p2 => { {d1,d2,d3,d4}, port} -- ipv4 -% h1,h2,...,h32,p1,p2 => {{n1,n2,..,n8}, port} -- ipv6 -% Taken from jungerl/ftpd +%% parse address on form: +%% d1,d2,d3,d4,p1,p2 => { {d1,d2,d3,d4}, port} -- ipv4 +%% h1,h2,...,h32,p1,p2 => {{n1,n2,..,n8}, port} -- ipv6 +%% Taken from jungerl/ftpd parse_address(Str) -> paddr(Str, 0, []). -%------------------------------------------------------------------------------- paddr([X|Xs],N,Acc) when X >= $0, X =< $9 -> paddr(Xs, N*10+(X-$0), Acc); paddr([X|Xs],_N,Acc) when X >= $A, X =< $F -> paddr(Xs,(X-$A)+10, Acc); paddr([X|Xs],_N,Acc) when X >= $a, X =< $f -> paddr(Xs, (X-$a)+10, Acc); @@ -1023,13 +1016,11 @@ paddr([],P2,[P1|As]) when length(As) == 32 -> end; paddr(_, _, _) -> error. -%------------------------------------------------------------------------------- addr6([H4,H3,H2,H1|Addr],Acc) when H4<16,H3<16,H2<16,H1<16 -> addr6(Addr, [H4 + H3*16 + H2*256 + H1*4096 |Acc]); addr6([], Acc) -> {ok, list_to_tuple(Acc)}; addr6(_, _) -> error. -%------------------------------------------------------------------------------- format_port(PortNumber) -> [A,B] = binary_to_list(<>), {A, B}. @@ -1037,1361 +1028,44 @@ format_port(PortNumber) -> %------------------------------------------------------------------------------- -spec format(string(), list()) -> string(). format(FormatString, Args) -> - case catch io_lib:format(FormatString, Args) of - {'EXIT',{badarg,_}} -> - error_logger:error_report({?REPORT_TAG, {format_error, {FormatString, Args}}}), - "Invalid format"; - Data -> - lists:flatten(Data) - end. + case catch io_lib:format(FormatString, Args) of + {'EXIT',{badarg,_}} -> + error_logger:error_report({bifrost, format_error, {FormatString, Args}}), + "Invalid format"; + Data -> + lists:flatten(Data) + end. %------------------------------------------------------------------------------- -spec format_error(integer() | string(), term()) -> string(). format_error(Code, Reason) when is_integer(Code) -> - format_error(response_code_string(Code), Reason); + format_error(response_code_string(Code), Reason); format_error(Message, undef) -> - Message ++ "."; + Message ++ "."; format_error(Message, Reason) -> - Format = case io_lib:printable_unicode_list(Reason) of - true -> - "~ts (~ts)"; - _False -> - "~ts (~p)" - end, - format(Format, [Message, Reason]) ++ ".". + Format = case io_lib:printable_unicode_list(Reason) of + true -> + "~ts (~ts)"; + _False -> + "~ts (~p)" + end, + format(Format, [Message, Reason]) ++ ".". %------------------------------------------------------------------------------- from_utf8(String, true) -> - unicode:characters_to_list(erlang:list_to_binary(String), utf8); + unicode:characters_to_list(erlang:list_to_binary(String), utf8); from_utf8(String, false) -> - String. + String. %------------------------------------------------------------------------------- to_utf8(String) -> - to_utf8(String, true). + to_utf8(String, true). to_utf8(String, true) -> - erlang:binary_to_list(unicode:characters_to_binary(String, utf8)); + erlang:binary_to_list(unicode:characters_to_binary(String, utf8)); to_utf8(String, false) -> - [if C > 255 orelse C<0 -> $?; true -> C end || C <- String]. - -%=============================================================================== -% EUNIT TEST -%------------------------------------------------------------------------------- --ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). - -%=============================================================================== -% Simple test(s) for small functions, pure functions -%------------------------------------------------------------------------------- -strip_newlines_test() -> - "testing 1 2 3" = strip_newlines("testing 1 2 3\r\n"), - "testing again" = strip_newlines("testing again"). - -%------------------------------------------------------------------------------- -parse_input_test() -> - {test, "1 2 3"} = parse_input("TEST 1 2 3"), - {test, ""} = parse_input("Test\r\n"), - {test, "awesome"} = parse_input("Test awesome\r\n"). - -%------------------------------------------------------------------------------- -format_access_test() -> - "rwxrwxrwx" = format_access(8#0777), - "rw-rw-rw-" = format_access(8#0666), - "r--rwxrwx" = format_access(8#0477), - "---------" = format_access(0). - -%------------------------------------------------------------------------------- -format_number_test() -> - "005" = format_number(5, 3, $0), - "500" = format_number(500, 2, $0), - "500" = format_number(500, 3, $0). - -%------------------------------------------------------------------------------- -parse_address_test() -> - {ok, {{127,0,0,1}, 2000}} = parse_address("127,0,0,1,7,208"), - error = parse_address("MEAT MEAT"). - -%------------------------------------------------------------------------------- -ftp_result_test() -> - % all results from gen_bifrost_server.erl - State = #connection_state{authenticated_state=unauthenticated}, - NewState = State#connection_state{authenticated_state=authenticated}, - ?assertEqual({ok, NewState}, ftp_result(State, {ok, NewState})), - - ?assertEqual({error, undef, NewState}, ftp_result(State, {error, NewState})), - - ?assertEqual({error, "Error", State}, ftp_result(State, {error, "Error"})), - ?assertEqual({error, not_found, State}, ftp_result(State, {error, not_found})), - - ?assertEqual({error, not_found, NewState}, ftp_result(State, {error, not_found, NewState})), - ?assertEqual({error, not_found, NewState}, ftp_result(State, {error, NewState, not_found})), - - % a special results - ?assertEqual("/path", ftp_result(State, "/path")), %current_directory - - ?assertEqual({error, undef, NewState}, ftp_result(State, {error, NewState})), %list_files - ?assertEqual([], ftp_result(State, [])), %list_files - - Fun = fun(_BytesCount) -> ok end, - ?assertEqual({error, undef, State}, ftp_result(State, error)), %get_file - ?assertMatch({ok, Fun}, ftp_result(State, {ok, Fun})), %get_file - - ?assertMatch({ok, Fun, State}, ftp_result(State, {ok, Fun}, - fun (S, {ok, Fn}) when is_function(Fn)-> {ok, Fn, S}; - (_S, Any) -> Any end)), - - ?assertMatch({ok, Fun, NewState}, ftp_result(State, {ok, Fun, NewState}, - fun (S, {ok, Fn}) when is_function(Fn)-> {ok, Fn, S}; - (_S, Any) -> Any end)), - - ?assertMatch({ok, NewState}, ftp_result(State, {ok, NewState}, - fun (S, {ok, Fn}) when is_function(Fn)-> {ok, Fn, S}; - (_S, Any) -> Any end)), - - ?assertMatch({error, undef, NewState}, ftp_result(State, {error, NewState}, - fun (S, {ok, Fn}) when is_function(Fn)-> {ok, Fn, S}; - (_S, Any) -> Any end)), - - ?assertMatch({ok, file_info}, ftp_result(State, {ok, file_info})), %file_info - ?assertMatch({error, "ErrorCause", State}, ftp_result(State, {error, "ErrorCause"})), %file_info - - ?assertMatch({ok, [help_info]}, ftp_result(State, {ok, [help_info]})), %site_help - ?assertMatch({error, undef, NewState}, ftp_result(State, {error, NewState})), %site_help - ok. - -%=============================================================================== -% Functional tests -%------------------------------------------------------------------------------- -fixture_setup() -> - error_logger:tty(false), - ok = meck:new(error_logger, [unstick, passthrough]), - ok = meck:new(gen_tcp, [unstick]), - ok = meck:new(inet, [unstick, passthrough]), - ok = meck:new(fake_server, [non_strict]), - ok = meck:expect(fake_server, init, fun(InitialState, _Opt) -> InitialState end), - ok = meck:expect(fake_server, disconnect, fun(_, {error, breaked}) -> ok end), - fake_server:init(#connection_state{module=fake_server}, []). - -%------------------------------------------------------------------------------- -fixture_cleanup(_State) -> - meck:unload(fake_server), - meck:unload(inet), - meck:unload(gen_tcp), - meck:unload(error_logger), - error_logger:tty(true). - -%=============================================================================== -% GenServer functional tests -%------------------------------------------------------------------------------- -genserver_test_() -> - {foreach, fun fixture_setup/0, fun fixture_cleanup/1,[ fun genserver_test_ti/1]}. - -%------------------------------------------------------------------------------- -genserver_test_ti(_State) -> - ?assertMatch({reply, _, state}, handle_call(request, from, state)), - ?assertMatch({noreply, state}, handle_cast(message, state)), - ?assertMatch({noreply, state}, handle_info(info, state)), - ?assertMatch({error, enotsup}, code_change(old, state, extra)), - ?assertMatch(ok, terminate(reason, stop)), - ok = meck:expect(gen_tcp, close, fun(socket) -> ok end), - ?assertMatch(ok, terminate(reason, {listen_socket, socket})), - [?_assert(true)]. - -%=============================================================================== -% Init functional tests -%------------------------------------------------------------------------------- -init_test_() -> - {foreach, fun fixture_setup/0, fun fixture_cleanup/1,[ fun init_test_ti/1]}. - -%------------------------------------------------------------------------------- -init_test_ti(_State) -> - ok = meck:expect(gen_tcp, listen, fun(21, _TcpOpts) -> {error, already_used} end), - ?assertEqual({stop, already_used}, init({fake_server, []})), - - ok = meck:expect(gen_tcp, listen, fun(21, _TcpOpts) -> {ok, listen} end), - ok = meck:expect(inet, sockname, fun(listen) -> {error, badarg} end), - ok = meck:expect(fake_server, init, fun(_InitialState, _Opt) -> throw("fake_exception") end), - ?assertEqual({stop, "fake_exception"}, init({fake_server, []})), - ok = meck:expect(fake_server, init, fun(_InitialState, _Opt) -> {error, "fake_error"} end), - ?assertEqual({stop, "fake_error"}, init({fake_server, []})), - - ok = meck:expect(gen_tcp, listen, fun(6666, _TcpOpts) -> {ok, listen} end), - ok = meck:expect(inet, sockname, fun(listen) -> {ok, {localip, localport}} end), - ?assertEqual({stop, "fake_error"}, init({fake_server, [{port, 6666}, {ssl, disabled}]})), - - ok = meck:expect(gen_tcp, listen, fun(6667, _TcpOpts) -> {ok, listen} end), - ?assertEqual({stop, ssl_not_started}, init({fake_server, [{port, 6667}, {ssl, enabled}, {port_range, 0}]})), - - IsStarted = (ok =:= ssl:start()), % start for SSL - ?assertEqual({stop, "fake_error"}, init({fake_server, [{port, 6667}, {ssl, enabled}, {port_range, 0}]})), - ?assertEqual({stop, "fake_error"}, init({fake_server, [{port, 6667}, {ssl, false}, {port_range, 100}]})), - ?assertEqual({stop, "fake_error"}, init({fake_server, [{port, 6667}, {ssl, only}, {port_range, {6000, 8000}}]})), - IsStarted andalso ssl:stop(), % stop SSL if started by our code - [?_assert(true)]. - -%=============================================================================== -%=============================================================================== -% Estabilish connection functional tests -%------------------------------------------------------------------------------- -connection_test_() -> - { foreach, fun fixture_setup/0, fun fixture_cleanup/1, - [ fun await_invalid/1, - fun await_timeout/1, - fun await_peer_closed/1, - fun await_control_closed/1, - fun await_sock_closed/1, - fun await_ok/1] - }. - -%------------------------------------------------------------------------------- -establish_control_connection_simulator(socket, _State) -> - receive - after 1000 -> ok - end. - -%------------------------------------------------------------------------------- -await_invalid(_InitialState) -> - ok = meck:expect(gen_tcp, accept, fun(listen) -> terminate end), - ?assertExit(bad_accept, await_connections(listen, nopid)), - [?_assert(true)]. - -%------------------------------------------------------------------------------- -await_timeout(_InitialState) -> - ok = meck:expect(gen_tcp, accept, fun(listen) -> - ok = meck:expect(gen_tcp, accept, fun(listen) -> terminate end), - {ok, socket} end), - ok = meck:expect(gen_tcp, close, fun(socket) -> ok end), - ok = meck:expect(inet, peername, fun(socket) -> {ok, {remote_addr, remote_port}} end), - SupervisorPid = self(), - ?assertExit(bad_accept, await_connections(listen, SupervisorPid)), - [?_assert(true)]. - -%------------------------------------------------------------------------------- -await_peer_closed(InitialState) -> - ok = meck:expect(gen_tcp, accept, fun(listen) -> - ok = meck:expect(gen_tcp, accept, fun(listen) -> terminate end), - {ok, socket} end), - ok = meck:expect(gen_tcp, close, fun(socket) -> ok end), - ok = meck:expect(inet, peername, fun(socket) -> {error, closed} end), - - ?assert(is_function(fun establish_control_connection_simulator/2)), % create ref - SupervisorPid = spawn_link(?MODULE, supervise_connections, - [InitialState, establish_control_connection_simulator]), - ?assertExit(bad_accept, await_connections(listen, SupervisorPid)), - [?_assert(true)]. - -%------------------------------------------------------------------------------- -await_control_closed(InitialState) -> - ok = meck:expect(gen_tcp, accept, fun(listen) -> - ok = meck:expect(gen_tcp, accept, fun(listen) -> terminate end), - {ok, socket} end), - ok = meck:expect(gen_tcp, close, fun(socket) -> ok end), - ok = meck:expect(inet, peername, fun(socket) -> {ok, {remote_addr,remote_port}} end), - ok = meck:expect(gen_tcp, controlling_process, fun(socket, _Pid) -> {error, closed} end), - ok = meck:expect(inet, sockname, fun(socket) -> {error, closed} end), - - ?assert(is_function(fun establish_control_connection_simulator/2)), % create ref - SupervisorPid = spawn_link(?MODULE, supervise_connections, - [InitialState, establish_control_connection_simulator]), - ?assertExit(bad_accept, await_connections(listen, SupervisorPid)), - [?_assert(true)]. - -%------------------------------------------------------------------------------- -await_sock_closed(InitialState) -> - ok = meck:expect(gen_tcp, accept, fun(listen) -> - ok = meck:expect(gen_tcp, accept, fun(listen) -> terminate end), - {ok, socket} end), - ok = meck:expect(gen_tcp, close, fun(socket) -> ok end), - ok = meck:expect(inet, peername, fun(socket) -> {ok, {remote_addr,remote_port}} end), - ok = meck:expect(gen_tcp, controlling_process, fun(socket, _Pid) -> ok end), - ok = meck:expect(inet, sockname, fun(socket) -> {error, closed} end), - - ?assert(is_function(fun establish_control_connection_simulator/2)), - SupervisorPid = spawn_link(?MODULE, supervise_connections, - [InitialState, establish_control_connection_simulator]), - ?assertExit(bad_accept, await_connections(listen, SupervisorPid)), - [?_assert(true)]. - -%------------------------------------------------------------------------------- -await_ok(InitialState) -> - ok = meck:expect(gen_tcp, accept, fun(listen) -> - ok = meck:expect(gen_tcp, accept, fun(listen) -> terminate end), - {ok, socket} end), - ok = meck:expect(inet, peername, fun(socket) -> {ok, {remote_addr,remote_port}} end), - ok = meck:expect(gen_tcp, controlling_process, fun(socket, _Pid) -> ok end), - ok = meck:expect(inet, sockname, fun(socket) -> {ok, {local_addr, local_port}} end), - - ?assert(is_function(fun establish_control_connection_simulator/2)), - SupervisorPid = spawn_link(?MODULE, supervise_connections, - [InitialState, establish_control_connection_simulator]), - ?assertExit(bad_accept, await_connections(listen, SupervisorPid)), - [?_assert(true)]. - -%=============================================================================== - -%=============================================================================== -% Functional integration tests --define(dataSocketTest(TEST_NAME), - TEST_NAME() -> - TEST_NAME(active), - TEST_NAME(passive)). - -%------------------------------------------------------------------------------- -setup() -> - fixture_setup(). - -%------------------------------------------------------------------------------- -execute(ListenerPid) -> - State = fake_server:init(#connection_state{module=fake_server}, []), - receive - {ack, ListenerPid} -> - control_loop(ListenerPid, {gen_tcp, socket}, State#connection_state{ip_address={127,0,0,1}}), - meck:validate(fake_server), - meck:validate(gen_tcp) - end, - fixture_cleanup(execute). - -%------------------------------------------------------------------------------- -% Awkward, monadic interaction sequence testing -script_dialog([]) -> - meck:expect(gen_tcp, - recv, - fun(_, _, infinity) -> {error, closed} end); - -script_dialog([{Request, Response} | Rest]) -> - meck:expect(gen_tcp, - recv, - fun(Socket, _, infinity) -> - script_dialog([{resp, Socket, Response}] ++ Rest), - {ok, Request} - end); - -script_dialog([{resp, Socket, Response} | Rest]) -> - meck:expect(gen_tcp, send, - fun(S, C) -> - ?assertEqual(Socket, S), - ?assertEqual(Response, unicode:characters_to_list(C)), - script_dialog(Rest), - ok - end); - -script_dialog([{resp_bin, Socket, Response} | Rest]) -> - meck:expect(gen_tcp, - send, - fun(S, C) -> - ?assertEqual(Socket, S), - ?assertEqual(Response, C), - script_dialog(Rest), - ok - end); - -script_dialog([{resp_error, Socket, Error} | Rest]) -> - meck:expect(gen_tcp, - send, - fun(S, _C) -> - ?assertEqual(Socket, S), - script_dialog(Rest), - {error, Error} - end); - -script_dialog([{req_error, Socket, Error} | Rest]) -> - meck:expect(gen_tcp, - recv, - fun(S, _, infinity) -> - ?assertEqual(S, Socket), - script_dialog(Rest), - {error, Error} - end); - -script_dialog([{req, Socket, Request} | Rest]) -> - meck:expect(gen_tcp, - recv, - fun(S, _, infinity) -> - ?assertEqual(S, Socket), - script_dialog(Rest), - {ok, Request} - end). - -%------------------------------------------------------------------------------- -% executes the next step in the test script -step(Pid) -> - Pid ! {ack, self()}, % 1st ACK will be 'eaten' by execute - % so valid sequence will be - receive - {new_state, Pid, State} -> - {ok, State}; - _ -> - ?assert(fail) - end. - -%------------------------------------------------------------------------------- -% stops the script -finish(Pid) -> - ?assertEqual({error, closed}, gen_tcp:recv(dummy_socket, 0, infinity)), % if fails - some step() forgotten - Pid ! {done, self()}. - -%------------------------------------------------------------------------------- -login_test_user(SocketPid) -> - login_test_user(SocketPid, []). - -%------------------------------------------------------------------------------- -login_test_user(SocketPid, Script) -> - script_dialog([{req, socket, "USER meat"}, - {resp, socket, "331 User name okay, need password.\r\n"}, - {req, socket, "PASS meatmeat"}, - {resp, socket, "230 User logged in, proceed.\r\n"}] ++ Script), - - ok = meck:expect(fake_server, check_user, fun(S, _A) -> {ok, S} end), - step(SocketPid), % USER meat - - ok = meck:expect(fake_server, - login, - fun(St, "meat", "meatmeat") -> - {true, St#connection_state{authenticated_state=authenticated}} - end), - {ok, State1} = step(SocketPid), - ?assertMatch(#connection_state{authenticated_state=authenticated}, State1), - {ok, State1}. - -%------------------------------------------------------------------------------- -authenticate_successful_test() -> - setup(), - ControlPid = self(), - Child = spawn_link( - fun() -> - login_test_user(ControlPid), - finish(ControlPid) - end), - execute(Child). - -%------------------------------------------------------------------------------- -authenticate_failure_test() -> - setup(), - ControlPid = self(), - Child = spawn_link( - fun() -> - script_dialog([{"USER meat", "331 User name okay, need password.\r\n"}, - {"PASS meatmeat", "530 Login incorrect.\r\n"}]), - ok = meck:expect(gen_tcp, close, fun(socket) -> ok end), - ok = meck:expect(fake_server, login, fun(_, "meat", "meatmeat") -> {error} end), - ok = meck:expect(fake_server, check_user, fun(S, _A) -> {ok, S} end), - ok = meck:expect(fake_server, disconnect, fun(_, {error, auth}) -> ok end), - {ok, State} = step(ControlPid), - ?assertMatch(#connection_state{authenticated_state=unauthenticated}, State), - step(ControlPid), % last event will be disconnect with reason auth fail - finish(ControlPid) - end), - - execute(Child). - -%------------------------------------------------------------------------------- -requirements_failure_test() -> - setup(), - ControlPid = self(), - Child = spawn_link( - fun() -> - script_dialog([{"USER meat", "331 User name okay, need password.\r\n"}, - {"USER heat", "421 Login requirements (DENY).\r\n"}]), - ok = meck:expect(gen_tcp, close, fun(socket) -> ok end), - ok = meck:expect(fake_server, check_user, fun(S, "meat") -> {ok, S}; - (S, "heat") -> {error, "DENY", S} end), - ok = meck:expect(fake_server, disconnect, fun(_, {error, auth}) -> ok end), - {ok, State} = step(ControlPid), - ?assertMatch(#connection_state{authenticated_state=unauthenticated}, State), - {ok, State} = step(ControlPid), - ?assertMatch(#connection_state{authenticated_state=unauthenticated}, State), - step(ControlPid), % last event will be disconnect with reason auth fail - finish(ControlPid) - end), - execute(Child). - -unauthenticated_test() -> - setup(), - ControlPid = self(), - Child = spawn_link( - fun() -> - script_dialog([ {"CWD /hamster", "530 Not logged in.\r\n"}, - {"MKD /unicorns", "530 Not logged in.\r\n"}]), - {ok, StateCmd} = step(ControlPid), - ?assertMatch(#connection_state{authenticated_state=unauthenticated}, StateCmd), - - {ok, StateMkd} = step(ControlPid), - ?assertMatch(#connection_state{authenticated_state=unauthenticated}, StateMkd), - finish(ControlPid) - end), - execute(Child). - -ssl_only_test() -> - setup(), - ControlPid = self(), - ok = meck:expect(fake_server, init, fun(InitialState, _Opt) -> - InitialState#connection_state{ssl_mode=only, utf8=false} end), - - Child = spawn_link(fun() -> - script_dialog([ {"USER hamster", "534 Request denied for policy reasons (only ftps allowed).\r\n"}, - {"MKD /unicorns", "534 Request denied for policy reasons (only ftps allowed).\r\n"}, - {"CWD /hamster", "534 Request denied for policy reasons (only ftps allowed).\r\n"}, - {"PWD", "534 Request denied for policy reasons (only ftps allowed).\r\n"}, - {"OPTS UTF8 ON", "501 Syntax error in parameters or arguments.\r\n"}, - {"FEAT", "211-Features\r\n"}, - {resp, socket, " AUTH TLS\r\n" }, - {resp, socket, " PROT\r\n" }, - {resp, socket, "211 End\r\n" } - ]), - step(ControlPid), - step(ControlPid), - step(ControlPid), - step(ControlPid), - step(ControlPid), - step(ControlPid), - finish(ControlPid) - end), - execute(Child). - -mkdir_test() -> - setup(), - ControlPid = self(), - Child = spawn_link( - fun() -> - login_test_user(ControlPid, - [{"MKD test_dir", "250 \"test_dir\" directory created.\r\n"}, - {"XMKD xdir", "250 \"xdir\" directory created.\r\n"}, - {"MKD test_dir_2", "550 Unable to create directory.\r\n"}]), - - meck:expect(fake_server, make_directory, fun(State, "test_dir") -> {ok, State} end), - step(ControlPid), - - meck:expect(fake_server, make_directory, fun(State, "xdir") -> {ok, State} end), - step(ControlPid), - - meck:expect(fake_server, make_directory, fun(_State, "test_dir_2") -> {error, error} end), - step(ControlPid), - - finish(ControlPid) - end), - execute(Child). - - -rmdir_test() -> - setup(), - ControlPid = self(), - Child = spawn_link( - fun() -> - login_test_user(ControlPid, - [{"rmd killdir", "200 Command okay.\r\n"}, - {"xrmd xdirkill", "200 Command okay.\r\n"}, - {"rmd dir2kill", "550 Unable to remove directory.\r\n"}]), - - meck:expect(fake_server, remove_directory, fun(State, "killdir") -> {ok, State} end), - step(ControlPid), - - meck:expect(fake_server, remove_directory, fun(State, "xdirkill") -> {ok, State} end), - step(ControlPid), - - meck:expect(fake_server, remove_directory, fun(_State, "dir2kill") -> {error, error} end), - step(ControlPid), - - finish(ControlPid) - end), - execute(Child). - -cwd_test() -> - setup(), - ControlPid = self(), - Child = spawn_link( - fun() -> - meck:expect(fake_server, - change_directory, - fun (State, "/meat/bovine/bison") -> - {ok, State} - end), - meck:expect(fake_server, - current_directory, - fun(_) -> "/meat/bovine/bison" end), - - login_test_user(ControlPid, - [{"CWD /meat/bovine/bison", "250 Directory changed to \"/meat/bovine/bison\".\r\n"}, - {"CWD /meat/bovine/auroch", "550 Unable to change directory.\r\n"}, - {"CWD /meat/bovine/elefant", "550 Unable to change directory (denied).\r\n"}, - {"XCWD /meat/bovine/auroch", "550 Unable to change directory.\r\n"}]), - step(ControlPid), - - meck:expect(fake_server, change_directory, - fun(State, "/meat/bovine/auroch") -> {error, State} end), - step(ControlPid), - - meck:expect(fake_server, change_directory, - fun(_State, "/meat/bovine/elefant") -> {error, denied} end), - step(ControlPid), - - meck:expect(fake_server, change_directory, - fun(State, "/meat/bovine/auroch") -> {error, State} end), - step(ControlPid), - - finish(ControlPid) - end), - execute(Child). - -cdup_test() -> - setup(), - ControlPid = self(), - Child = spawn_link( - fun() -> - meck:expect(fake_server, change_directory, fun(State, "..") -> {ok, State} end), - login_test_user(ControlPid, - [{"CDUP", "250 Directory changed to \"/zero\".\r\n"}, - {"CDUP", "250 Directory changed to \"/1st\".\r\n"}, - {"CDUP", "250 Directory changed to \"/2nd\".\r\n"}, - {"XCUP", "250 Directory changed to \"/3rd\".\r\n"}]), - - meck:expect(fake_server, current_directory, fun(_) -> "/zero" end), - step(ControlPid), - - meck:expect(fake_server, current_directory, fun(_) -> "/1st" end), - step(ControlPid), - - meck:expect(fake_server, current_directory, fun(_) -> "/2nd" end), - step(ControlPid), - - meck:expect(fake_server, current_directory, fun(_) -> "/3rd" end), - step(ControlPid), - - finish(ControlPid) - end), - execute(Child). - -pwd_test() -> - setup(), - ControlPid = self(), - Child = spawn_link( - fun() -> - login_test_user(ControlPid, [ {"PWD", "257 \"/meat/bovine/bison\"\r\n"}, - {"XPWD","257 \"/opt/local/share\"\r\n"} ]), - - meck:expect(fake_server, current_directory, fun(_) -> "/meat/bovine/bison" end), - step(ControlPid), - - meck:expect(fake_server, current_directory, fun(_) -> "/opt/local/share" end), - step(ControlPid), - - finish(ControlPid) - end), - execute(Child). - -passive_anyport_successful_test() -> - setup(), - ControlPid = self(), - Child = spawn_link( - fun() -> - meck:expect(gen_tcp, listen, fun(0, _) -> {ok, listen_socket} end), - meck:expect(inet, sockname, fun(listen_socket) -> {ok, {{127, 0, 0, 1}, 2000}} end), - - login_test_user(ControlPid, [{"PASV", "227 Entering Passive Mode (127,0,0,1,7,208)\r\n"}]), - ?assertMatch({ok, #connection_state{pasv_listen={passive, listen_socket, {{127,0,0,1}, 2000}}}}, - step(ControlPid)), - finish(ControlPid) - end), - execute(Child). - -passive_anyport_failure_test() -> - setup(), - ControlPid = self(), - Child = spawn_link( - fun() -> - meck:expect(gen_tcp, listen, fun(0, _) -> {error, eaddrinuse} end), - login_test_user(ControlPid, [{"PASV", "425 Can't open data connection.\r\n"}]), - ?assertMatch({ok, #connection_state{pasv_listen=undefined}}, step(ControlPid)), - finish(ControlPid) - end), - execute(Child). - -passive_port_range_successful_test() -> - setup(), - ok = meck:expect(fake_server, init, - fun(InitialState, _Opt) -> InitialState#connection_state{port_range={2000, 3000}} end), - ControlPid = self(), - Child = spawn_link( - fun() -> - meck:expect(gen_tcp, listen, fun (2500, _) -> {ok, listen_socket_2500}; - (N, _) when is_integer(N) -> {error, eaddrinuse} end), - - meck:expect(inet, sockname, fun(listen_socket_2500) -> {ok, {{127, 0, 0, 1}, 2500}} end), - - login_test_user(ControlPid, [{"PASV", "227 Entering Passive Mode (127,0,0,1,9,196)\r\n"}]), - ok = meck:new(random, [unstick]), - ok = meck:expect(random, uniform, fun(M) -> M end), - ?assertMatch({ok, #connection_state{pasv_listen={passive, listen_socket_2500, {{127,0,0,1}, 2500}}}}, - step(ControlPid)), - ok = meck:unload(random), - finish(ControlPid) - end), - execute(Child). - -passive_port_range_failure_test() -> - setup(), - ok = meck:expect(fake_server, init, - fun(InitialState, _Opt) -> InitialState#connection_state{port_range={2000, 4000}} end), - ControlPid = self(), - Child = spawn_link( - fun() -> - meck:expect(gen_tcp, listen, fun(N, _) when is_integer(N), N>=2000, 4000>=N -> {error, eaddrinuse} end), - ok = meck:new(random, [unstick]), - ok = meck:expect(random, uniform, fun(M) -> M end), - login_test_user(ControlPid, [{"PASV", "425 Can't open data connection.\r\n"}]), - ?assertMatch({ok, #connection_state{pasv_listen=undefined}}, step(ControlPid)), - ok = meck:unload(random), - finish(ControlPid) - end), - execute(Child). - -login_test_user_with_data_socket(ControlPid, Script, passive) -> - ok = meck:expect(gen_tcp, listen, fun(0, _) -> {ok, listen_socket} end), - ok = meck:expect(gen_tcp, accept, fun(listen_socket) -> {ok, data_socket} end), - - ok = meck:expect(inet, sockname, fun (listen_socket) -> {ok, {{127, 0, 10, 1}, 2000}}; - (data_socket) -> {ok, {{127, 0, 10, 1}, 2001}} end), - ok = meck:expect(inet, peername, fun (listen_socket) -> {ok, {{127, 0, 0, 1}, 2000}}; - (data_socket) -> {ok, {{127, 0, 0, 1}, 2001}} end), - - login_test_user(ControlPid, [{"PASV", "227 Entering Passive Mode (127,0,0,1,7,208)\r\n"}] ++ Script), - ?assertMatch( - {ok, #connection_state{pasv_listen={passive, listen_socket, {{127,0,0,1}, 2000}}}}, step(ControlPid)); - -login_test_user_with_data_socket(ControlPid, Script, active) -> - meck:expect(gen_tcp, - connect, - fun(_, _, _) -> - {ok, data_socket} - end), - ok = meck:expect(inet, sockname, fun (listen_socket) -> {ok, {{127, 0, 10, 1}, 2000}}; - (data_socket) -> {ok, {{127, 0, 10, 1}, 2001}} end), - ok = meck:expect(inet, peername, fun (listen_socket) -> {ok, {{127, 0, 0, 1}, 2000}}; - (data_socket) -> {ok, {{127, 0, 0, 1}, 2001}} end), - login_test_user(ControlPid, [{"PORT 127,0,0,1,7,208", "200 Command okay.\r\n"}] ++ Script), - ?assertMatch({ok, #connection_state{data_port={active, {127,0,0,1}, 2000}}}, step(ControlPid)). - -?dataSocketTest(nlst_test). -nlst_test(Mode) -> - setup(), - meck:expect(fake_server, - list_files, - fun(_, _) -> - [#file_info{type=file, name="edward"}, - #file_info{type=dir, name="Aethelred"}] - end), - ControlPid = self(), - Child = spawn_link( - fun() -> - ok = meck:expect(gen_tcp, close, fun(data_socket) -> ok end), - ok = meck:expect(inet, setopts, fun(data_socket,[{recbuf, _Size}]) -> ok end), - login_test_user_with_data_socket(ControlPid, - [ {"NLST", "150 File status okay; about to open data connection.\r\n"}, - {resp, data_socket, "edward\r\n"}, {resp, data_socket, "Aethelred\r\n"}, - {resp, socket, "226 Closing data connection.\r\n"}, - {"NLST", "451 Unable to list.\r\n"}, - {"NLST", "451 Unable to list (Default Error).\r\n"}, - {"NLST", "450 Locked system.\r\n"} ], - Mode), - step(ControlPid), - ok = meck:expect(fake_server, list_files, fun(_State, _) -> error end), - step(ControlPid), - ok = meck:expect(fake_server, list_files, fun(_State, _) -> {error, "Default Error"} end), - step(ControlPid), - ok = meck:expect(fake_server, list_files, fun(_State, _) -> {error, {450, "Locked system."}} end), - step(ControlPid), - finish(ControlPid) - end), - execute(Child). - -?dataSocketTest(list_test). -list_test(Mode) -> - setup(), - meck:expect(fake_server, - list_files, - fun(_, _) -> - [#file_info{type=file, - name="edward", - mode=511, - gid=0, - uid=0, - mtime={{3019,12,12},{12,12,12}}, - size=512}, - #file_info{type=dir, - name="Aethelred", - mode=200, - gid=0, - uid=0, - mtime={{3019,12,12},{12,12,12}}, - size=0}] - end), - ControlPid = self(), - Child = spawn_link( - fun() -> - Script = [{"LIST", "150 File status okay; about to open data connection.\r\n"}, - {resp, data_socket, "-rwxrwxrwx 1 0 0 512 Dec 12 12:12 edward\r\n"}, - {resp, data_socket, "d-wx--x--- 4 0 0 0 Dec 12 12:12 Aethelred\r\n"}, - {resp, socket, "226 Closing data connection.\r\n"}, - {"LIST", "451 Unable to list (Some Error).\r\n"}, - {"LIST", "552 Action interrupted; Out of RAM.\r\n"} ], - - ok = meck:expect(gen_tcp, close, fun(data_socket) -> ok end), - ok = meck:expect(inet, setopts, fun(data_socket,[{recbuf, _Size}]) -> ok end), - login_test_user_with_data_socket(ControlPid,Script,Mode), - step(ControlPid), - ok = meck:expect(fake_server, list_files, fun(_State, _) -> {error, "Some Error"} end), - step(ControlPid), - ok = meck:expect(fake_server, list_files, - fun(_State, _) -> {error, {552, "Action interrupted; Out of RAM."}} end), - step(ControlPid), - finish(ControlPid) - end), - execute(Child). - -remove_file_test() -> - setup(), - ControlPid = self(), - Child = spawn_link( - fun() -> - meck:expect(fake_server, - remove_file, - fun(St, "cheese.txt") -> - {ok, St} - end), - - login_test_user(ControlPid, [{"DELE cheese.txt", "250 Requested file action okay, completed.\r\n"}, - {"DELE cheese.txt", "450 Unable to delete file.\r\n"}, - {"DELE cheese.txt", "450 Unable to delete file (Not found).\r\n"}, - {"DELE cheese.txt", "553 Only latin chars.\r\n"}]), - step(ControlPid), - meck:expect(fake_server,remove_file, - fun(_, "cheese.txt") -> {error, error} end), - step(ControlPid), - meck:expect(fake_server,remove_file, - fun(_, "cheese.txt") -> {error, "Not found"} end), - step(ControlPid), - meck:expect(fake_server,remove_file, - fun(_, "cheese.txt") -> {error, {553, "Only latin chars."} } end), - step(ControlPid), - finish(ControlPid) - end), - execute(Child). - -?dataSocketTest(stor_test). -stor_test(Mode) -> - setup(), - ControlPid = self(), - - ok = meck:expect(fake_server, init, fun(InitialState, _Opt) -> - InitialState#connection_state{recv_block_size=1024*1024} end), - Child = spawn_link( - fun() -> - Script = [{"STOR file.txt", "150 File status okay; about to open data connection.\r\n"}, - {req, data_socket, <<"SOME DATA HERE">>}, - {resp, socket, "226 Closing data connection.\r\n"}, - {"PWD", "257 \"/\"\r\n"} - ], - meck:expect(fake_server, - put_file, - fun(S, "file.txt", write, F) -> - {ok, Data, DataSize} = F(), - BinData = <<"SOME DATA HERE">>, - ?assertEqual(Data, BinData), - ?assertEqual(DataSize, size(BinData)), - {ok, S} - end), - - ok = meck:expect(gen_tcp, close, fun(data_socket) -> ok end), - ok = meck:expect(inet, setopts, - fun(data_socket, Opts) -> - ?assertEqual(1024*1024, proplists:get_value(recbuf, Opts)), - ok - end), - - login_test_user_with_data_socket(ControlPid, Script, Mode), - step(ControlPid), % STOR - - ok = meck:expect(fake_server, put_file, - fun(S, "file.txt", notification, done) -> {ok, S} end), - - ok = meck:expect(fake_server, current_directory, fun(_) -> "/" end), - step(ControlPid), % PWD - finish(ControlPid) - end), - execute(Child). - -?dataSocketTest(stor_user_failure_test). -stor_user_failure_test(Mode) -> - setup(), - ControlPid = self(), - Child = spawn_link( - fun() -> - Script=[ {"STOR elif.txt", "150 File status okay; about to open data connection.\r\n"}, - {req, data_socket, <<"SOME DATA HERE">>}, - {resp, socket, "451 Unable to store file (access_denied).\r\n"}, - {"QUIT", "200 Goodbye.\r\n"}], - ok = meck:expect(fake_server, put_file, - fun(_, "elif.txt", write, F) -> - F(), - {error, access_denied} - end), - - ok = meck:expect(gen_tcp, close, fun(data_socket) -> ok end), - ok = meck:expect(inet, setopts, fun(data_socket,[{recbuf, _Size}]) -> ok end), - login_test_user_with_data_socket(ControlPid, Script, Mode), - step(ControlPid), - - meck:expect(fake_server, disconnect, fun(_, exit) -> ok end), - meck:expect(gen_tcp, close, fun(socket) -> ok end), -% not needed, because USER kills itself -% meck:expect(fake_server,put_file, fun(S, "elif.txt", notification, terminated) -> {ok, S} end), - step(ControlPid), - finish(ControlPid) - end), - execute(Child). - -?dataSocketTest(stor_notificaion_test). -stor_notificaion_test(Mode) -> - setup(), - ControlPid = self(), - Child = spawn_link( - fun() -> - Script=[ {"STOR ok.txt", "150 File status okay; about to open data connection.\r\n"}, - {req, data_socket, <<"SOME DATA HERE">>}, - {resp, socket, "226 Closing data connection.\r\n"}, - - {"STOR bad.txt", "150 File status okay; about to open data connection.\r\n"}, - {req, data_socket, <<"SOME DATA HERE">>}, - {resp, socket, "226 Closing data connection.\r\n"}, - {req_error, socket, {error, closed}} - ], - - ok = meck:expect(inet, setopts, fun(data_socket,[{recbuf, _Size}]) -> ok end), - ok = meck:expect(gen_tcp, close, fun(data_socket) -> ok end), - ok = meck:expect(fake_server, put_file, fun(S, "ok.txt", write, F) -> - {ok, Data, DataSize} = F(), - BinData = <<"SOME DATA HERE">>, - ?assertEqual(Data, BinData), - ?assertEqual(DataSize, size(BinData)), - {ok, S} - end), - - ok = meck:expect(gen_tcp, close, fun(data_socket) -> ok end), - login_test_user_with_data_socket(ControlPid, Script, Mode), - step(ControlPid), % STOR(OK) - - ok = meck:expect(fake_server, put_file, - fun (S, "bad.txt", write, F) -> - {ok, Data, DataSize} = F(), - BinData = <<"SOME DATA HERE">>, - ?assertEqual(Data, BinData), - ?assertEqual(DataSize, size(BinData)), - {ok, S}; - (S, "ok.txt", notification, done) -> - {ok, S} - end), - step(ControlPid), % STOR(BAD) - - - ok = meck:expect(fake_server, put_file, - fun (S, "bad.txt", notification, Result) -> - ?assertEqual(terminated, Result), - {ok, S} - end), - ok = meck:expect(fake_server, disconnect, fun(_, {error, {error, closed}}) -> ok end), - step(ControlPid), % CRASH CONNECTION - finish(ControlPid) - end), - execute(Child). - - -?dataSocketTest(stor_failure_test). -stor_failure_test(Mode) -> - setup(), - ControlPid = self(), - Child = spawn_link( - fun() -> - Script=[ {"STOR elif.txt", "150 File status okay; about to open data connection.\r\n"}, - {req, data_socket, <<"SOME DATA HERE">>}, - {resp, socket, "451 Unable to store file (access_denied).\r\n"}, - {"QUIT", "200 Goodbye.\r\n"}], - ok = meck:expect(fake_server, put_file, - fun(_, "elif.txt", write, F) -> - F(), - {error, access_denied} - end), - - ok = meck:expect(gen_tcp, close, fun(data_socket) -> ok end), - ok = meck:expect(inet, setopts, fun(data_socket,[{recbuf, _Size}]) -> ok end), - login_test_user_with_data_socket(ControlPid, Script, Mode), - step(ControlPid), - - meck:expect(fake_server, disconnect, fun(_, exit) -> ok end), - meck:expect(gen_tcp, close, fun(socket) -> ok end), -% meck:expect(fake_server,put_file, fun(S, "elif.txt", notification, terminated) -> {ok, S} end), -% not needed, because USER kills itself - step(ControlPid), - finish(ControlPid) - end), - execute(Child). - -?dataSocketTest(retr_test). -retr_test(Mode) -> - setup(), - ok = meck:expect(fake_server, init, fun(InitialState, _Opt) -> - InitialState#connection_state{send_block_size=1024*1024} end), - ControlPid = self(), - Child = spawn_link( - fun() -> - Script = [{"RETR bologna.txt", "150 File status okay; about to open data connection.\r\n"}, - {resp, data_socket, "SOME DATA HERE"}, - {resp, data_socket, "SOME MORE DATA"}, - {resp, socket, "226 Closing data connection.\r\n"}], - meck:expect(fake_server, - get_file, - fun(State, "bologna.txt") -> - {ok, - fun(1024*1024) -> - {ok, - list_to_binary("SOME DATA HERE"), - fun(1024*1024) -> - {ok, - list_to_binary("SOME MORE DATA"), - fun(1024*1024) -> {done, State} end} - end} - end} - end), - - ok = meck:expect(gen_tcp, close, fun(data_socket) -> ok end), - ok = meck:expect(inet, setopts, fun(data_socket,[{recbuf, _Size}]) -> ok end), - - login_test_user_with_data_socket(ControlPid, Script, Mode), - step(ControlPid), - finish(ControlPid) - end), - execute(Child). - -?dataSocketTest(retr_failure_test). -retr_failure_test(Mode) -> - setup(), - ok = meck:expect(fake_server, init, fun(InitialState, _Opt) -> - InitialState#connection_state{send_block_size=1024} end), - ControlPid = self(), - Child = spawn_link( - fun() -> - Script = [{"RETR bologna.txt", "150 File status okay; about to open data connection.\r\n"}, - {resp, data_socket, "SOME DATA HERE"}, - {resp, socket, "451 Unable to get file (Disk error).\r\n"}], - meck:expect(fake_server, - get_file, - fun(State, "bologna.txt") -> - {ok, - fun(1024) -> - {ok, - list_to_binary("SOME DATA HERE"), - fun(1024) -> - {error, "Disk error", State} - end} - end} - end), - - ok = meck:expect(gen_tcp, close, fun(data_socket) -> ok end), - ok = meck:expect(inet, setopts, fun(data_socket,[{recbuf, _Size}]) -> ok end), - - login_test_user_with_data_socket(ControlPid, Script, Mode), - step(ControlPid), - finish(ControlPid) - end), - execute(Child). - -rein_test() -> - setup(), - ControlPid = self(), - Child = spawn_link( - fun() -> - login_test_user(ControlPid, [{"REIN", "200 Command okay.\r\n"}]), - ControlPid ! {ack, self()}, - receive - {new_state, _, #connection_state{authenticated_state=unauthenticated}} -> - ok; - _ -> - ?assert(fail) - end, - finish(ControlPid) - end), - execute(Child). - -mdtm_test() -> - setup(), - ControlPid = self(), - Child = spawn_link( - fun() -> - meck:expect(fake_server, - file_info, - fun(_, "cheese.txt") -> - {ok, - #file_info{type=file, - mtime={{2012,2,3},{16,3,12}}}} - end), - login_test_user(ControlPid, [{"MDTM cheese.txt", "213 20120203160312\r\n"}]), - step(ControlPid), - finish(ControlPid) - end), - execute(Child). - -mdtm_truncate_test() -> - setup(), - ControlPid = self(), - Child = spawn_link( - fun() -> - ok = meck:expect(fake_server, file_info, - fun(_, "mould.txt") -> - {ok, - #file_info{type=file, - mtime={{2012,2,3},{16,3,11.933844}}}} - end), - login_test_user(ControlPid, [{"MDTM mould.txt", "213 20120203160311\r\n"}, - {"MDTM mould.txt", "550 File unavailable (Bad file name).\r\n"}]), - step(ControlPid), - ok = meck:expect(fake_server, file_info, - fun(State, "mould.txt") -> - {error, "Bad file name", State} - end), - step(ControlPid), - finish(ControlPid) - end), - execute(Child). - -rnfr_rnto_test() -> - setup(), - ControlPid = self(), - Child = spawn_link( - fun() -> - login_test_user(ControlPid, - [{"RNTO mushrooms.txt", "503 RNFR not specified.\r\n"}, - {"RNFR cheese.txt", "350 Ready for RNTO.\r\n"}, - {"RNTO mushrooms.txt", "250 Rename successful.\r\n"}]), - step(ControlPid), - meck:expect(fake_server, - rename_file, - fun(S, "cheese.txt", "mushrooms.txt") -> - {ok, S} - end), - step(ControlPid), - step(ControlPid), - finish(ControlPid) - end), - execute(Child). - -type_test() -> - setup(), - ControlPid = self(), - Child = spawn_link( - fun() -> - login_test_user(ControlPid, - [{"TYPE I", "200 Command okay.\r\n"}, - {"TYPE X", "501 Only TYPE I or TYPE A may be used.\r\n"}]), - step(ControlPid), - step(ControlPid), - finish(ControlPid) - end - ), - execute(Child). - -site_test() -> - setup(), - ControlPid = self(), - Child = spawn_link( - fun() -> - meck:expect(fake_server, - site_command, - fun(S, monkey, "cheese bits") -> - {ok, S} - end), - login_test_user(ControlPid, - [{"SITE MONKEY cheese bits", "200 Command okay.\r\n"}, - {"SITE GORILLA cheese", "500 Syntax error, command unrecognized.\r\n"}]), - step(ControlPid), - - meck:expect(fake_server, - site_command, - fun(_, gorilla, "cheese") -> - {error, not_found} - end), - step(ControlPid), - finish(ControlPid) - end - ), - execute(Child). - -help_site_test() -> - setup(), - ControlPid = self(), - Child = spawn_link( - fun() -> - meck:expect(fake_server, - site_help, - fun (_) -> - {ok, [{"MEAT", "devour the flesh of beasts."}]} - end), - Script = [{"HELP SITE", "214-The following commands are recognized\r\n"}, - {resp, socket, "MEAT : devour the flesh of beasts.\r\n"}, - {resp, socket, "214 Help OK\r\n"}, - {"HELP SITE", "500 Syntax error, command unrecognized.\r\n"}, - {"HELP SITE", "500 Unable to help site (Not implemented).\r\n"}, - {"HELP SITE", "502 Command 'HELP' not implemented.\r\n"} - ], - login_test_user(ControlPid, Script), - step(ControlPid), - meck:expect(fake_server, site_help, fun(_State) -> {ok, []} end), - step(ControlPid), - meck:expect(fake_server, site_help, - fun(State) -> - {error, "Not implemented", State} - end), - step(ControlPid), - meck:expect(fake_server, site_help, - fun(State) -> - {error, {502, "Command 'HELP' not implemented."}, State} - end), - step(ControlPid), - finish(ControlPid) - end - ), - execute(Child). - -unrecognized_command_test() -> - setup(), - ControlPid = self(), - Child = spawn_link( - fun() -> - login_test_user(ControlPid, [{"FEED buffalo", "500 Syntax error, command unrecognized.\r\n"}]), - step(ControlPid), - finish(ControlPid) - end - ), - execute(Child). - -quit_test() -> - setup(), - meck:expect(gen_tcp, close, fun(socket) -> ok end), - ControlPid = self(), - Child = spawn_link( - fun () -> - meck:expect(fake_server, disconnect, fun(_, exit) -> ok end), - login_test_user(ControlPid, [{"QUIT", "200 Goodbye.\r\n"}]), - step(ControlPid), - finish(ControlPid) - end), - execute(Child). - -feat_test() -> - setup(), - ControlPid = self(), - Child = spawn_link( - fun() -> - login_test_user(ControlPid, - [ {"FEAT", "211-Features\r\n"}, - {resp, socket, " UTF8\r\n" }, - {resp, socket, "211 End\r\n" }]), - step(ControlPid), - finish(ControlPid) - end), - execute(Child). - -?dataSocketTest(utf8_success_test). -utf8_success_test(Mode) -> - setup(), - ControlPid = self(), - Child = spawn_link( - fun() -> - FileName = "Молоко-Яйки", %milk-eggs - UtfFileName = to_utf8(FileName), %milk-eggs - BinData = <<"SOME DATA HERE">>, - - Script=[{"PWD " ++ UtfFileName, "257 \""++ UtfFileName ++"\"\r\n"}, - {"OPTS UTF8 ON", "200 Accepted.\r\n"}, - {"CWD " ++ UtfFileName, "250 Directory changed to \""++ UtfFileName ++"\".\r\n"}, - {"STOR " ++ UtfFileName, "150 File status okay; about to open data connection.\r\n"}, - {req, data_socket, BinData}, - {resp, socket, "226 Closing data connection.\r\n"}, - {"LIST", "150 File status okay; about to open data connection.\r\n"}, - {resp, data_socket, "d-wx--x--- 4 0 0 0 Dec 12 12:12 "++UtfFileName++"\r\n"}, - {resp, socket, "226 Closing data connection.\r\n"}], - - ok = meck:expect(fake_server,current_directory, fun(_) -> FileName end), - - login_test_user_with_data_socket(ControlPid, Script, Mode), - step(ControlPid), - step(ControlPid), - - meck:expect(fake_server,change_directory, - fun(State, InFileName) -> - ?assertEqual(InFileName, FileName), - {ok, State} - end), - step(ControlPid), - - meck:expect(fake_server,put_file, - fun(S, InFileName, write, F) -> - ?assertEqual(InFileName, FileName), - {ok, Data, DataSize} = F(), - ?assertEqual(Data, BinData), - ?assertEqual(DataSize, size(BinData)), - {ok, S} - end), - - ok = meck:expect(gen_tcp, close, fun(data_socket) -> ok end), - ok = meck:expect(inet, setopts, fun(data_socket,[{recbuf, _Size}]) -> ok end), - - step(ControlPid), - - meck:expect(fake_server, put_file, - fun(S, InFileName, notification, done) -> - ?assertEqual(InFileName, FileName), - {ok, S} - end), - - meck:expect(fake_server, list_files, - fun(_, _) -> - [#file_info{type=dir,name=FileName,mode=200,gid=0,uid=0, - mtime={{3019,12,12},{12,12,12}},size=0}] - end), - - ok = meck:expect(gen_tcp, close, fun(data_socket) -> ok end), - ok = meck:expect(inet, setopts, fun(data_socket,[{recbuf, _Size}]) -> ok end), - - step(ControlPid), - finish(ControlPid) - end), - execute(Child). - -?dataSocketTest(utf8_failure_test). -utf8_failure_test(Mode) -> - setup(), - ControlPid = self(), - Child = spawn_link( - fun() -> - FileName = "Молоко-Яйки", %milk-eggs - UtfFileNameOk = to_utf8(FileName), %milk-eggs - {UtfFileNameErr, _} = lists:split(length(UtfFileNameOk)-1, UtfFileNameOk), - - Script =[ {"OPTS UTF8 ON", "200 Accepted.\r\n"}, - {"CWD " ++ UtfFileNameErr, "501 Syntax error in parameters or arguments.\r\n"}], - - ok = meck:expect(gen_tcp, close, fun(data_socket) -> ok end), - ok = meck:expect(error_logger, warning_report, fun({?REPORT_TAG, Report}) -> - ?assertMatch({utf8, incomplete, _, _}, Report), - ok end), - login_test_user_with_data_socket(ControlPid, Script, Mode), - step(ControlPid), - step(ControlPid), - finish(ControlPid) - end), - execute(Child). - --endif. + [if C > 255 orelse C<0 -> $?; true -> C end || C <- String]. diff --git a/src/gen_bifrost_server.erl b/src/gen_bifrost_server.erl index a68af38..9c8a889 100644 --- a/src/gen_bifrost_server.erl +++ b/src/gen_bifrost_server.erl @@ -5,49 +5,54 @@ %%%------------------------------------------------------------------- -module(gen_bifrost_server). --include("bifrost.hrl"). - --type state() :: #connection_state{}. --type fileinfo() :: #file_info{}. --type filepath() :: file:filename(). --type stateok() :: {ok, state()}. --type stateerror() :: error | {error} | % default error - {error, term()} | {error, term(), state()} | % default error with comment - {error, {pos_integer(), term()}} | % error with custom code and message - {error, {pos_integer(), term()}, state()}. % error with custom code and message and new state - --type statechange() :: stateok() | stateerror(). --type helpinfo() :: {Name::string(), Description::string()}. - --callback init(State::state(), Options::list(proplists:property())) -> state() | {error, term()}. --callback login(State::state(), Username::string(), Passwd::string()) -> {boolean(), state()} | quit. --callback check_user(State::state(), Username::string()) -> statechange() | quit. -% SECURE NOTE: Does not use check_user for testing is user available, -% and return {ok, State} for all unexisting users, -% because USER+PASS obfuscate is user exist and bad password, or no user -% This function should used for connection's settings like -% USER requires SSL, USER can access from some IP and etc - --callback current_directory(State::state()) -> filepath(). --callback make_directory(State::state(), Path::filepath()) -> statechange(). --callback change_directory(State::state(), Path::filepath()) -> statechange(). --callback list_files(State::state(), Path::filepath()) -> list(fileinfo()) | stateerror(). --callback remove_directory(State::state(), Path::filepath()) -> statechange(). --callback remove_file(State::state(), Path::filepath()) -> statechange(). - --callback put_file (State::state(), Path::filepath(), append | write, fun()) -> statechange(); - (State::state(), Path::filepath(), notification, done | % next command is arrived - timeout | %control_timeout is passed, but connection works - terminated % control connection error - ) -> statechange(). - --callback get_file(State::state(), Path::filepath()) -> stateerror() | {ok, fun(), state()} | {ok, fun()}. - --callback file_info(State::state(), Path::filepath()) -> {ok, fileinfo()} | stateerror(). - --callback rename_file(State::state(), FromPath::filepath(), ToPath::filepath()) -> statechange(). - --callback site_command(State::state(), Command::string(), Args::string()) -> statechange(). --callback site_help(State::state()) -> {ok, list(helpinfo())} | stateerror(). - --callback disconnect(State::state(), exit | {error, term()}) -> statechange(). % but this state is unused +-export([behaviour_info/1]). + +behaviour_info(callbacks) -> + %% FileInfo :: include/bifrost.hrl:file_info() + %% State, NewState :: include/bifrost.hrl:connection_state() + %% Path :: String + %% File Name :: String + %% StateChangeOK :: {ok, NewState} + %% StateChangeError :: {error, Reason, NewState} + %% for compatibility {error, Reason} and {error, NewState} + %% also supported + %% StateChange :: StateChangeOK | StateChangeError + %% HelpInfo :: {Name, Description} + %% GetFun(ByteCount) -> {ok, Bytes, NextGetFun} | {done, NewState} | StateChangeError + [ + {init, 2}, %% State, PropList (options) -> State + {login, 3}, %% State, Username, Password -> + %%{true OR false, State} | 'quit' (disconnect client) + {check_user, 2}, %% State, Username -> + %% {ok, NewState} | {error, Reason, NewState}, + %% 'quit' (disconnect client) + %% SECURE NOTE: Does not use check_user for testing is user + %% available, and return {ok, State} for all unexisting users, + %% because USER+PASS obfuscate is user exist and bad password, + %% or no user. This function should used for connection's + %% settings like USER requires SSL, USER can access from some + %% IP and etc + {current_directory, 1}, %% State -> Path + {make_directory, 2}, %% State, Path -> StateChange + {change_directory, 2}, %% State, Path -> StateChange + {list_files, 3}, %% State, Options, Path -> [FileInfo] | StateChangeError + {remove_directory, 2}, %% State, Path -> StateChange + {remove_file, 2}, %% State, Path -> StateChange + {put_file, 4}, %% State, File Name, (append | write), Fun(Byte Count) -> + %% StateChange State, File Name, notification, + %% done (next command is arrived) | timeout (control_timeout is + %% passed, but connection works) | terminated (control connection + %% error) -> StateChange + {get_file, 2}, %% State, Path -> {ok, GetFun, NewState} | StateChangeError + %% for compatibility {ok, GetFun} | error also supported + {file_info, 2}, %% State, Path -> {ok, FileInfo} | StateChangeError + {rename_file, 3}, %% State, From Path, To Path -> StateChange + {site_command, 3}, %% State, CommandNameString, CommandArgsString -> StateChange + {site_help, 1}, %% State -> {ok, [HelpInfo]} | StateChangeError + {disconnect, 2}, %% State, exit (QUIT command from client) or {error, Reason} -> + %% *unused* State Change + {set_alarm, 1}, %% Error -> ok + {clean_alarm, 0} + ]; +behaviour_info(_) -> + undefined. diff --git a/test/bifrost_tests.erl b/test/bifrost_tests.erl new file mode 100644 index 0000000..9721f2b --- /dev/null +++ b/test/bifrost_tests.erl @@ -0,0 +1,1111 @@ +%%%------------------------------------------------------------------- +%%% @author Dmitrii Zolotarev +%%% @copyright (C) 2015, Eltex +%%% @doc +%%% +%%% @end +%%% Created : 11 Mar 2015 by Dmitrii Zolotarev +%%%------------------------------------------------------------------- +-module(bifrost_tests). +-author('dmitry.zolotarev@eltex.loc'). + +-ifdef(TEST). + +-include_lib("eunit/include/eunit.hrl"). +-include("bifrost.hrl"). + +%% EUNIT TESTS %% + +%% Testing Utility Functions + +setup() -> + error_logger:tty(false), + ok = meck:new(error_logger, [unstick, passthrough]), + ok = meck:new(gen_tcp, [unstick]), + ok = meck:new(inet, [unstick, passthrough]), + ok = meck:new(fake_server, [non_strict]), + ok = meck:expect(fake_server, init, fun(InitialState, _Opt) -> InitialState end), + ok = meck:expect(fake_server, disconnect, fun(_, {error, breaked}) -> ok end). + +execute(ListenerPid) -> + State = fake_server:init(#connection_state{module=fake_server}, []), + receive + {ack, ListenerPid} -> + bifrost:control_loop(ListenerPid, {gen_tcp, socket}, State#connection_state{ip_address={127,0,0,1}}), + meck:validate(fake_server), + meck:validate(gen_tcp) + end, + meck:unload(fake_server), + meck:unload(inet), + meck:unload(gen_tcp), + meck:unload(error_logger), + error_logger:tty(true). + +-define(dataSocketTest(TEST_NAME), + TEST_NAME() -> + TEST_NAME(active), + TEST_NAME(passive)). + +%% Awkward, monadic interaction sequence testing +script_dialog([]) -> + meck:expect(gen_tcp, + recv, + fun(_, _, infinity) -> {error, closed} end); +script_dialog([{Request, Response} | Rest]) -> + meck:expect(gen_tcp, + recv, + fun(Socket, _, infinity) -> + script_dialog([{resp, Socket, Response}] ++ Rest), + {ok, Request} + end); +script_dialog([{resp, Socket, Response} | Rest]) -> + meck:expect(gen_tcp, + send, + fun(S, C) -> + ?assertEqual(Socket, S), + ?assertEqual(Response, unicode:characters_to_list(C)), + script_dialog(Rest) + end); +script_dialog([{resp_bin, Socket, Response} | Rest]) -> + meck:expect(gen_tcp, + send, + fun(S, C) -> + ?assertEqual(Socket, S), + ?assertEqual(Response, C), + script_dialog(Rest) + end); +script_dialog([{resp_error, Socket, Error} | Rest]) -> + meck:expect(gen_tcp, + send, + fun(S, _C) -> + ?assertEqual(Socket, S), + script_dialog(Rest), + {error, Error} + end); +script_dialog([{req_error, Socket, Error} | Rest]) -> + meck:expect(gen_tcp, + recv, + fun(S, _C, infinity) -> + ?assertEqual(Socket, S), + script_dialog(Rest), + {error, Error} + end); +script_dialog([{req, Socket, Request} | Rest]) -> + meck:expect(gen_tcp, + recv, + fun(S, _, infinity) -> + ?assertEqual(S, Socket), + script_dialog(Rest), + {ok, Request} + end). + +%% executes the next step in the test script +step(Pid) -> + Pid ! {ack, self()}, % 1st ACK will be 'eaten' by execute + % so valid sequence will be + receive + {new_state, Pid, State} -> + {ok, State}; + _ -> + ?assert(fail) + end. + +%% stops the script +finish(Pid) -> + ?assertEqual({error, closed}, gen_tcp:recv(dummy_socket, 0, infinity)), % if fails - some step() forgotten + Pid ! {done, self()}. + + +%% Unit Tests + +strip_newlines_test() -> + "testing 1 2 3" = bifrost:strip_newlines("testing 1 2 3\r\n"), + "testing again" = bifrost:strip_newlines("testing again"). + +parse_input_test() -> + {test, [], "1 2 3"} = bifrost:parse_input("TEST 1 2 3"), + {test, [], ""} = bifrost:parse_input("Test\r\n"), + {test, [], "awesome"} = bifrost:parse_input("Test awesome\r\n"). + +format_access_test() -> + "rwxrwxrwx" = bifrost:format_access(8#0777), + "rw-rw-rw-" = bifrost:format_access(8#0666), + "r--rwxrwx" = bifrost:format_access(8#0477), + "---------" = bifrost:format_access(0). + +format_number_test() -> + "005" = bifrost:format_number(5, 3, $0), + "500" = bifrost:format_number(500, 2, $0), + "500" = bifrost:format_number(500, 3, $0). + +parse_address_test() -> + {ok, {{127,0,0,1}, 2000}} = bifrost:parse_address("127,0,0,1,7,208"), + error = bifrost:parse_address("MEAT MEAT"). + +ftp_result_test() -> + % all results from gen_bifrost_server.erl + State = #connection_state{authenticated_state = unauthenticated}, + NewState = State#connection_state{authenticated_state = authenticated}, + ?assertEqual({ok, NewState}, bifrost:ftp_result(State, {ok, NewState})), + ?assertEqual({error, undef, NewState}, bifrost:ftp_result(State, {error, NewState})), + + ?assertEqual({error, "Error", State}, bifrost:ftp_result(State, {error, "Error"})), + ?assertEqual({error, not_found, State}, bifrost:ftp_result(State, {error, not_found})), + + ?assertEqual({error, not_found, NewState}, bifrost:ftp_result(State, {error, not_found, NewState})), + ?assertEqual({error, not_found, NewState}, bifrost:ftp_result(State, {error, NewState, not_found})), + + %% a special results + ?assertEqual("/path", bifrost:ftp_result(State, "/path")), %current_directory + + ?assertEqual({error, undef, NewState}, bifrost:ftp_result(State, {error, NewState})), %list_files + ?assertEqual([], bifrost:ftp_result(State, [])), %list_files + + Fun = fun(_BytesCount) -> ok end, + ?assertEqual({error, undef, State}, bifrost:ftp_result(State, error)), %get_file + ?assertMatch({ok, Fun}, bifrost:ftp_result(State, {ok, Fun})), %get_file + ?assertMatch({ok, Fun, State}, bifrost:ftp_result(State, {ok, Fun}, + fun (S, {ok, Fn}) when is_function(Fn)-> {ok, Fn, S}; + (_S, Any) -> Any end)), + ?assertMatch({ok, Fun, NewState}, bifrost:ftp_result(State, {ok, Fun, NewState}, + fun (S, {ok, Fn}) when is_function(Fn)-> {ok, Fn, S}; + (_S, Any) -> Any end)), + + ?assertMatch({ok, NewState}, bifrost:ftp_result(State, {ok, NewState}, + fun (S, {ok, Fn}) when is_function(Fn)-> {ok, Fn, S}; + (_S, Any) -> Any end)), + + ?assertMatch({error, undef, NewState}, bifrost:ftp_result(State, {error, NewState}, + fun (S, {ok, Fn}) when is_function(Fn)-> {ok, Fn, S}; + (_S, Any) -> Any end)), + + ?assertMatch({ok, file_info}, bifrost:ftp_result(State, {ok, file_info})), %file_info + ?assertMatch({error, "ErrorCause", State}, bifrost:ftp_result(State, {error, "ErrorCause"})), %file_info + + ?assertMatch({ok, [help_info]}, bifrost:ftp_result(State, {ok, [help_info]})), %site_help + ?assertMatch({error, undef, NewState}, bifrost:ftp_result(State, {error, NewState})), %site_help + ok. + +%% Functional/Integration Tests + +login_test_user(SocketPid) -> + login_test_user(SocketPid, []). + +login_test_user(SocketPid, Script) -> + script_dialog([{req, socket, "USER meat"}, + {resp, socket, "331 User name okay, need password.\r\n"}, + {req, socket, "PASS meatmeat"}, + {resp, socket, "230 User logged in, proceed.\r\n"}] ++ Script), + ok = meck:expect(fake_server, check_user, fun(S, _A) -> {ok, S} end), + step(SocketPid), % USER meat + + ok = meck:expect(fake_server, + login, + fun(St, "meat", "meatmeat") -> + {true, St#connection_state{authenticated_state=authenticated}} + end), + {ok, State1} = step(SocketPid), + ?assertMatch(#connection_state{authenticated_state=authenticated}, State1), + {ok, State1}. + +authenticate_successful_test() -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + login_test_user(ControlPid), + finish(ControlPid) + end), + execute(Child). + +authenticate_failure_test() -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + script_dialog([{"USER meat", "331 User name okay, need password.\r\n"}, + {"PASS meatmeat", "530 Login incorrect.\r\n"}]), + ok = meck:expect(gen_tcp, close, fun(socket) -> ok end), + ok = meck:expect(fake_server, login, fun(_, "meat", "meatmeat") -> {error} end), + ok = meck:expect(fake_server, check_user, fun(S, _A) -> {ok, S} end), + ok = meck:expect(fake_server, disconnect, fun(_, {error, auth}) -> ok end), + {ok, State} = step(ControlPid), + ?assertMatch(#connection_state{authenticated_state=unauthenticated}, State), + step(ControlPid), % last event will be disconnect with reason auth fail + finish(ControlPid) + end), + + execute(Child). + +requirements_failure_test() -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + script_dialog([{"USER meat", "331 User name okay, need password.\r\n"}, + {"USER heat", "421 Login requirements (DENY).\r\n"}]), + ok = meck:expect(gen_tcp, close, fun(socket) -> ok end), + ok = meck:expect(fake_server, check_user, fun(S, "meat") -> {ok, S}; + (S, "heat") -> {error, "DENY", S} end), + ok = meck:expect(fake_server, disconnect, fun(_, {error, auth}) -> ok end), + {ok, State} = step(ControlPid), + ?assertMatch(#connection_state{authenticated_state=unauthenticated}, State), + {ok, State} = step(ControlPid), + ?assertMatch(#connection_state{authenticated_state=unauthenticated}, State), + step(ControlPid), % last event will be disconnect with reason auth fail + finish(ControlPid) + end), + execute(Child). + + +unauthenticated_test() -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + script_dialog([ {"CWD /hamster", "530 Not logged in.\r\n"}, + {"MKD /unicorns", "530 Not logged in.\r\n"}]), + {ok, StateCmd} = step(ControlPid), + ?assertMatch(#connection_state{authenticated_state=unauthenticated}, StateCmd), + {ok, StateMkd} = step(ControlPid), + ?assertMatch(#connection_state{authenticated_state=unauthenticated}, StateMkd), + finish(ControlPid) + end), + execute(Child). + +ssl_only_test() -> + setup(), + ControlPid = self(), + ok = meck:expect(fake_server, init, fun(InitialState, _Opt) -> + InitialState#connection_state{ssl_mode=only, utf8=false} end), + + Child = spawn_link(fun() -> + script_dialog([ {"USER hamster", "534 Request denied for policy reasons (only ftps allowed).\r\n"}, + {"MKD /unicorns", "534 Request denied for policy reasons (only ftps allowed).\r\n"}, + {"CWD /hamster", "534 Request denied for policy reasons (only ftps allowed).\r\n"}, + {"PWD", "534 Request denied for policy reasons (only ftps allowed).\r\n"}, + {"OPTS UTF8 ON", "501 Syntax error in parameters or arguments.\r\n"}, + {"FEAT", "211-Features\r\n"}, + {resp, socket, " AUTH TLS\r\n" }, + {resp, socket, " PROT\r\n" }, + {resp, socket, "211 End\r\n" } + ]), + step(ControlPid), + step(ControlPid), + step(ControlPid), + step(ControlPid), + step(ControlPid), + step(ControlPid), + finish(ControlPid) + end), + execute(Child). + +mkdir_test() -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + meck:expect(fake_server, + make_directory, + fun(State, _) -> + {ok, State} + end), + login_test_user(ControlPid, + [{"MKD test_dir", "250 \"test_dir\" directory created.\r\n"}, + {"MKD test_dir_2", "550 Unable to create directory.\r\n"}]), + step(ControlPid), + + meck:expect(fake_server, + make_directory, + fun(_, _) -> + {error, error} + end), + step(ControlPid), + + finish(ControlPid) + end), + execute(Child). + +cwd_test() -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + meck:expect(fake_server, + change_directory, + fun(State, "/meat/bovine/bison") -> + {ok, State} + end), + meck:expect(fake_server, + current_directory, + fun(_) -> "/meat/bovine/bison" end), + login_test_user(ControlPid, + [{"CWD /meat/bovine/bison", "250 Directory changed to \"/meat/bovine/bison\".\r\n"}, + {"CWD /meat/bovine/auroch", "550 Unable to change directory.\r\n"}, + {"CWD /meat/bovine/elefant", "550 Unable to change directory (denied).\r\n"}]), + + step(ControlPid), + + meck:expect(fake_server, + change_directory, + fun(State, "/meat/bovine/auroch") -> + {error, State} + end), + step(ControlPid), + + meck:expect(fake_server, + change_directory, + fun(_State, "/meat/bovine/elefant") -> + {error, denied} + end), + step(ControlPid), + finish(ControlPid) + end), + execute(Child). + +cdup_test() -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + meck:expect(fake_server, + change_directory, + fun(State, "..") -> {ok, State} end), + meck:expect(fake_server, + current_directory, + fun(_) -> "/meat" end), + login_test_user(ControlPid, + [{"CDUP", "250 Directory changed to \"/meat\".\r\n"}, + {"CDUP", "250 Directory changed to \"/\".\r\n"}, + {"CDUP", "250 Directory changed to \"/\".\r\n"}]), + step(ControlPid), + + meck:expect(fake_server, + current_directory, + fun(_) -> "/" end), + + step(ControlPid), + + meck:expect(fake_server, + current_directory, + fun(_) -> "/" end), + step(ControlPid), + + finish(ControlPid) + end), + execute(Child). + +pwd_test() -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + meck:expect(fake_server, + current_directory, + fun(_) -> "/meat/bovine/bison" end), + + login_test_user(ControlPid, [{"PWD", "257 \"/meat/bovine/bison\"\r\n"}]), + + step(ControlPid), + + finish(ControlPid) + end), + execute(Child). + +passive_anyport_successful_test() -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + meck:expect(gen_tcp, listen, fun(0, _) -> {ok, listen_socket} end), + meck:expect(inet, sockname, fun(listen_socket) -> {ok, {{127, 0, 0, 1}, 2000}} end), + login_test_user(ControlPid, [{"PASV", "227 Entering Passive Mode (127,0,0,1,7,208)\r\n"}]), + ?assertMatch({ok, #connection_state{pasv_listen={passive, listen_socket, {{127,0,0,1}, 2000}}}}, + step(ControlPid)), + finish(ControlPid) + end), + execute(Child). + +passive_anyport_failure_test() -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + meck:expect(gen_tcp, listen, fun(0, _) -> {error, eaddrinuse} end), + login_test_user(ControlPid, [{"PASV", "425 Can't open data connection.\r\n"}]), + ?assertMatch({ok, #connection_state{pasv_listen=undefined}}, step(ControlPid)), + finish(ControlPid) + end), + execute(Child). + +passive_port_range_successful_test() -> + setup(), + ok = meck:expect(fake_server, init, + fun(InitialState, _Opt) -> InitialState#connection_state{port_range={2000, 3000}} end), + ControlPid = self(), + Child = spawn_link( + fun() -> + meck:expect(gen_tcp, listen, fun(2500, _) -> {ok, listen_socket_2500}; + (N, _) when is_integer(N) -> {error, eaddrinuse} end), + + meck:expect(inet, sockname, fun(listen_socket_2500) -> {ok, {{127, 0, 0, 1}, 2500}} end), + + login_test_user(ControlPid, [{"PASV", "227 Entering Passive Mode (127,0,0,1,9,196)\r\n"}]), + ok = meck:new(random, [unstick]), + ok = meck:expect(random, uniform, fun(M) -> M end), + ?assertMatch({ok, #connection_state{pasv_listen={passive, listen_socket_2500, {{127,0,0,1}, 2500}}}}, + step(ControlPid)), + ok = meck:unload(random), + finish(ControlPid) + end), + execute(Child). + +passive_port_range_failure_test() -> + setup(), + ok = meck:expect(fake_server, init, + fun(InitialState, _Opt) -> InitialState#connection_state{port_range={2000, 4000}} end), + ControlPid = self(), + Child = spawn_link( + fun() -> + meck:expect(gen_tcp, listen, fun(N, _) when is_integer(N), N>=2000, 4000>=N -> {error, eaddrinuse} end), + ok = meck:new(random, [unstick]), + ok = meck:expect(random, uniform, fun(M) -> M end), + login_test_user(ControlPid, [{"PASV", "425 Can't open data connection.\r\n"}]), + ?assertMatch({ok, #connection_state{pasv_listen=undefined}}, step(ControlPid)), + ok = meck:unload(random), + finish(ControlPid) + end), + execute(Child). + +login_test_user_with_data_socket(ControlPid, Script, passive) -> + meck:expect(gen_tcp, listen, fun(0, _) -> {ok, listen_socket} end), + meck:expect(gen_tcp, accept, fun(listen_socket) -> {ok, data_socket} end), + meck:expect(inet, sockname, fun(listen_socket) -> {ok, {{127, 0, 0, 1}, 2000}} end), + login_test_user(ControlPid, [{"PASV", "227 Entering Passive Mode (127,0,0,1,7,208)\r\n"}] ++ Script), + ?assertMatch({ok, #connection_state{pasv_listen={passive, listen_socket, {{127,0,0,1}, 2000}}}}, step(ControlPid)); + + +login_test_user_with_data_socket(ControlPid, Script, active) -> + meck:expect(gen_tcp, + connect, + fun(_, _, _) -> + {ok, data_socket} + end), + login_test_user(ControlPid, [{"PORT 127,0,0,1,7,208", "200 Command okay.\r\n"}] ++ Script), + ?assertMatch({ok, #connection_state{data_port={active, {127,0,0,1}, 2000}}}, step(ControlPid)). + +?dataSocketTest(nlst_test). +nlst_test(Mode) -> + setup(), + meck:expect(fake_server, + list_files, + fun(_, _, _) -> + [#file_info{type=file, name="edward"}, + #file_info{type=dir, name="Aethelred"}] + end), + ControlPid = self(), + Child = spawn_link( + fun() -> + ok = meck:expect(gen_tcp, close, fun(data_socket) -> ok end), + ok = meck:expect(inet, setopts, fun(data_socket,[{recbuf, _Size}]) -> ok end), + login_test_user_with_data_socket(ControlPid, + [{"NLST", "150 File status okay; about to open data connection.\r\n"}, + {resp, data_socket, "edward\r\n"}, + {resp, data_socket, "Aethelred\r\n"}, + {resp, socket, "226 Closing data connection.\r\n"}], + Mode), + step(ControlPid), + finish(ControlPid) + end), + execute(Child). + +?dataSocketTest(list_test). +list_test(Mode) -> + setup(), + meck:expect(fake_server, + list_files, + fun(_, _, _) -> + [#file_info{type=file, + name="edward", + mode=511, + gid=0, + uid=0, + mtime={{3019,12,12},{12,12,12}}, + size=512}, + #file_info{type=dir, + name="Aethelred", + mode=200, + gid=0, + uid=0, + mtime={{3019,12,12},{12,12,12}}, + size=0}] + end), + ControlPid = self(), + Child = spawn_link( + fun() -> + Script = [{"LIST", "150 File status okay; about to open data connection.\r\n"}, + {resp, data_socket, "-rwxrwxrwx 1 0 0 512 Dec 12 12:12 edward\r\n"}, + {resp, data_socket, "d-wx--x--- 4 0 0 0 Dec 12 12:12 Aethelred\r\n"}, + {resp, socket, "226 Closing data connection.\r\n"}], + + ok = meck:expect(gen_tcp, close, fun(data_socket) -> ok end), + ok = meck:expect(inet, setopts, fun(data_socket,[{recbuf, _Size}]) -> ok end), + + login_test_user_with_data_socket(ControlPid, Script, Mode), + step(ControlPid), + finish(ControlPid) + end), + execute(Child). + +remove_directory_test() -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + meck:expect(fake_server, + remove_directory, + fun(St, "/bison/burgers") -> + {ok, St} + end), + + login_test_user(ControlPid, [{"RMD /bison/burgers", "200 Command okay.\r\n"}, + {"RMD /bison/burgers", "550 Requested action not taken.\r\n"}]), + step(ControlPid), + + ok = meck:expect(fake_server, + remove_directory, + fun(_, "/bison/burgers") -> + {error, error} + end), + step(ControlPid), + + finish(ControlPid) + end), + execute(Child). + +remove_file_test() -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + meck:expect(fake_server, + remove_file, + fun(St, "cheese.txt") -> + {ok, St} + end), + + login_test_user(ControlPid, [{"DELE cheese.txt", "250 Requested file action okay, completed.\r\n"}, + {"DELE cheese.txt", "450 Unable to delete file.\r\n"}]), + step(ControlPid), + + meck:expect(fake_server, + remove_file, + fun(_, "cheese.txt") -> + {error, error} + end), + + step(ControlPid), + finish(ControlPid) + end), + execute(Child). + +?dataSocketTest(stor_test). +stor_test(Mode) -> + setup(), + ControlPid = self(), + ok = meck:expect(fake_server, init, fun(InitialState, _Opt) -> + InitialState#connection_state{recv_block_size = 1024*1024} end), + Child = spawn_link( + fun() -> + Script = [{"STOR file.txt", "150 File status okay; about to open data connection.\r\n"}, + {req, data_socket, <<"SOME DATA HERE">>}, + {resp, socket, "226 Closing data connection.\r\n"}, + {"PWD", "257 \"/\"\r\n"} + ], + meck:expect(fake_server, + put_file, + fun(S, "file.txt", write, F) -> + {ok, Data, DataSize} = F(), + BinData = <<"SOME DATA HERE">>, + ?assertEqual(Data, BinData), + ?assertEqual(DataSize, size(BinData)), + {ok, S} + end), + + ok = meck:expect(gen_tcp, close, fun(data_socket) -> ok end), + ok = meck:expect(inet, setopts, + fun(data_socket, Opts) -> + ?assertEqual(1024*1024, proplists:get_value(recbuf, Opts)), + ok + end), + + login_test_user_with_data_socket(ControlPid, Script, Mode), + step(ControlPid), % STOR + + ok = meck:expect(fake_server, put_file, + fun(S, "file.txt", notification, done) -> {ok, S} end), + + ok = meck:expect(fake_server, current_directory, fun(_) -> "/" end), + step(ControlPid), % PWD + + finish(ControlPid) + end), + execute(Child). + +?dataSocketTest(stor_user_failure_test). + +stor_user_failure_test(Mode) -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + Script=[ {"STOR elif.txt", "150 File status okay; about to open data connection.\r\n"}, + {req, data_socket, <<"SOME DATA HERE">>}, + {resp, socket, "451 Error access_denied when storing a file.\r\n"}, + {"QUIT", "200 Goodbye.\r\n"}], + ok = meck:expect(fake_server, put_file, + fun(_, "elif.txt", write, F) -> + F(), + {error, access_denied} + end), + + ok = meck:expect(gen_tcp, close, fun(data_socket) -> ok end), + ok = meck:expect(inet, setopts, fun(data_socket,[{recbuf, _Size}]) -> ok end), + login_test_user_with_data_socket(ControlPid, Script, Mode), + step(ControlPid), + + meck:expect(fake_server, disconnect, fun(_, exit) -> ok end), + meck:expect(gen_tcp, close, fun(socket) -> ok end), + %% not needed, because USER kills itself + %% meck:expect(fake_server,put_file, fun(S, "elif.txt", notification, terminated) -> {ok, S} end), + step(ControlPid), + finish(ControlPid) + end), + execute(Child). + +?dataSocketTest(stor_notificaion_test). + stor_notificaion_test(Mode) -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + Script=[ {"STOR ok.txt", "150 File status okay; about to open data connection.\r\n"}, + {req, data_socket, <<"SOME DATA HERE">>}, + {resp, socket, "226 Closing data connection.\r\n"}, + + {"STOR bad.txt", "150 File status okay; about to open data connection.\r\n"}, + {req, data_socket, <<"SOME DATA HERE">>}, + {resp, socket, "226 Closing data connection.\r\n"}, + {req_error, socket, {error, closed}} + ], + + ok = meck:expect(inet, setopts, fun(data_socket,[{recbuf, _Size}]) -> ok end), + ok = meck:expect(gen_tcp, close, fun(data_socket) -> ok end), + ok = meck:expect(fake_server, put_file, fun(S, "ok.txt", write, F) -> + {ok, Data, DataSize} = F(), + BinData = <<"SOME DATA HERE">>, + ?assertEqual(Data, BinData), + ?assertEqual(DataSize, size(BinData)), + {ok, S} + end), + + ok = meck:expect(gen_tcp, close, fun(data_socket) -> ok end), + login_test_user_with_data_socket(ControlPid, Script, Mode), + step(ControlPid), % STOR(OK) + + ok = meck:expect(fake_server, put_file, + fun(S, "bad.txt", write, F) -> + {ok, Data, DataSize} = F(), + BinData = <<"SOME DATA HERE">>, + ?assertEqual(Data, BinData), + ?assertEqual(DataSize, size(BinData)), + {ok, S}; + (S, "ok.txt", notification, done) -> + {ok, S} + end), + step(ControlPid), % STOR(BAD) + + ok = meck:expect(fake_server, put_file, + fun(S, "bad.txt", notification, Result) -> + ?assertEqual(terminated, Result), + {ok, S} + end), + ok = meck:expect(fake_server, disconnect, fun(_, {error, {error, closed}}) -> ok end), + step(ControlPid), % CRASH CONNECTION + finish(ControlPid) + end), + execute(Child). + +?dataSocketTest(stor_failure_test). +stor_failure_test(Mode) -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + Script=[ {"STOR elif.txt", "150 File status okay; about to open data connection.\r\n"}, + {req, data_socket, <<"SOME DATA HERE">>}, + {resp, socket, "451 Error access_denied when storing a file.\r\n"}, + {"QUIT", "200 Goodbye.\r\n"}], + ok = meck:expect(fake_server, put_file, + fun(_, "elif.txt", write, F) -> + F(), + {error, access_denied} + end), + + ok = meck:expect(gen_tcp, close, fun(data_socket) -> ok end), + ok = meck:expect(inet, setopts, fun(data_socket,[{recbuf, _Size}]) -> ok end), + login_test_user_with_data_socket(ControlPid, Script, Mode), + step(ControlPid), + + meck:expect(fake_server, disconnect, fun(_, exit) -> ok end), + meck:expect(gen_tcp, close, fun(socket) -> ok end), + %% meck:expect(fake_server,put_file, fun(S, "elif.txt", notification, terminated) -> {ok, S} end), + %% not needed, because USER kills itself + step(ControlPid), + finish(ControlPid) + end), + execute(Child). + +?dataSocketTest(retr_test). +retr_test(Mode) -> + setup(), + ok = meck:expect(fake_server, init, fun(InitialState, _Opt) -> + InitialState#connection_state{send_block_size=1024*1024} end), + ControlPid = self(), + Child = spawn_link( + fun() -> + Script = [{"RETR bologna.txt", "150 File status okay; about to open data connection.\r\n"}, + {resp, data_socket, "SOME DATA HERE"}, + {resp, data_socket, "SOME MORE DATA"}, + {resp, socket, "226 Closing data connection.\r\n"}], + meck:expect(fake_server, + get_file, + fun(State, "bologna.txt") -> + {ok, + fun(1024*1024) -> + {ok, + list_to_binary("SOME DATA HERE"), + fun(1024*1024) -> + {ok, + list_to_binary("SOME MORE DATA"), + fun(1024*1024) -> {done, State} end} + end} + end} + end), + + ok = meck:expect(gen_tcp, close, fun(data_socket) -> ok end), + ok = meck:expect(inet, setopts, fun(data_socket,[{recbuf, _Size}]) -> ok end), + + login_test_user_with_data_socket(ControlPid, Script, Mode), + step(ControlPid), + finish(ControlPid) + end), + execute(Child). + +?dataSocketTest(retr_failure_test). +retr_failure_test(Mode) -> + setup(), + ok = meck:expect(fake_server, init, fun(InitialState, _Opt) -> + InitialState#connection_state{send_block_size=1024} end), + ControlPid = self(), + Child = spawn_link( + fun() -> + Script = [{"RETR bologna.txt", "150 File status okay; about to open data connection.\r\n"}, + {resp, data_socket, "SOME DATA HERE"}, + {resp, socket, "451 Unable to get file (Disk error).\r\n"}], + meck:expect(fake_server, + get_file, + fun(State, "bologna.txt") -> + {ok, + fun(1024) -> + {ok, + list_to_binary("SOME DATA HERE"), + fun(1024) -> + {error, "Disk error", State} + end} + end} + end), + + ok = meck:expect(gen_tcp, close, fun(data_socket) -> ok end), + ok = meck:expect(inet, setopts, fun(data_socket,[{recbuf, _Size}]) -> ok end), + + login_test_user_with_data_socket(ControlPid, Script, Mode), + step(ControlPid), + finish(ControlPid) + end), + execute(Child). + +rein_test() -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + login_test_user(ControlPid, [{"REIN", "200 Command okay.\r\n"}]), + ControlPid ! {ack, self()}, + receive + {new_state, _, #connection_state{authenticated_state=unauthenticated}} -> + ok; + _ -> + ?assert(fail) + end, + finish(ControlPid) + end), + execute(Child). + +mdtm_test() -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + meck:expect(fake_server, + file_info, + fun(_, "cheese.txt") -> + {ok, + #file_info{type=file, + mtime={{2012,2,3},{16,3,12}}}} + end), + login_test_user(ControlPid, [{"MDTM cheese.txt", "213 20120203160312\r\n"}]), + step(ControlPid), + finish(ControlPid) + end), + execute(Child). + +mdtm_truncate_test() -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + meck:expect(fake_server, + file_info, + fun(_, "mould.txt") -> + {ok, + #file_info{type=file, + mtime={{2012,2,3},{16,3,11.933844}}}} + end), + login_test_user(ControlPid, [{"MDTM mould.txt", "213 20120203160311\r\n"}]), + step(ControlPid), + finish(ControlPid) + end), + execute(Child). + +rnfr_rnto_test() -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + login_test_user(ControlPid, + [{"RNTO mushrooms.txt", "503 RNFR not specified.\r\n"}, + {"RNFR cheese.txt", "350 Ready for RNTO.\r\n"}, + {"RNTO mushrooms.txt", "250 Rename successful.\r\n"}]), + step(ControlPid), + + meck:expect(fake_server, + rename_file, + fun(S, "cheese.txt", "mushrooms.txt") -> + {ok, S} + end), + step(ControlPid), + step(ControlPid), + finish(ControlPid) + end), + execute(Child). + +type_test() -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + login_test_user(ControlPid, + [{"TYPE I", "200 Command okay.\r\n"}, + {"TYPE X", "501 Only TYPE I or TYPE A may be used.\r\n"}]), + step(ControlPid), + step(ControlPid), + finish(ControlPid) + end + ), + execute(Child). + +site_test() -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + meck:expect(fake_server, + site_command, + fun(S, monkey, "cheese bits") -> + {ok, S} + end), + login_test_user(ControlPid, + [{"SITE MONKEY cheese bits", "200 Command okay.\r\n"}, + {"SITE GORILLA cheese", "500 Syntax error, command unrecognized.\r\n"}]), + step(ControlPid), + + meck:expect(fake_server, + site_command, + fun(_, gorilla, "cheese") -> + {error, not_found} + end), + step(ControlPid), + finish(ControlPid) + end + ), + execute(Child). + +help_site_test() -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + meck:expect(fake_server, + site_help, + fun (_) -> + {ok, [{"MEAT", "devour the flesh of beasts."}]} + end), + Script = [{"HELP SITE", "214-The following commands are recognized\r\n"}, + {resp, socket, "MEAT : devour the flesh of beasts.\r\n"}, + {resp, socket, "214 Help OK\r\n"}], + login_test_user(ControlPid, Script), + step(ControlPid), + finish(ControlPid) + end + ), + execute(Child). + +unrecognized_command_test() -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + login_test_user(ControlPid, [{"FEED buffalo", "500 Syntax error, command unrecognized.\r\n"}]), + step(ControlPid), + finish(ControlPid) + end + ), + execute(Child). + +quit_test() -> + setup(), + meck:expect(gen_tcp, + close, + fun(socket) -> ok end), + ControlPid = self(), + Child = spawn_link( + fun () -> + meck:expect(fake_server, + disconnect, + fun(_, exit) -> + ok + end), + login_test_user(ControlPid, + [{"QUIT", "200 Goodbye.\r\n"}]), + step(ControlPid), + finish(ControlPid) + end + ), + execute(Child). + +feat_test() -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + login_test_user(ControlPid, + [ {"FEAT", "211-Features\r\n"}, + {resp, socket, " UTF8\r\n" }, + {resp, socket, "211 End\r\n" }]), + step(ControlPid), + finish(ControlPid) + end), + execute(Child). + +?dataSocketTest(utf8_success_test). +utf8_success_test(Mode) -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + FileName = "Молоко-Яйки", %milk-eggs + UtfFileName = bifrost:to_utf8(FileName), %milk-eggs + BinData = <<"SOME DATA HERE">>, + + Script=[{"PWD " ++ UtfFileName, "257 \""++ UtfFileName ++"\"\r\n"}, + {"OPTS UTF8 ON", "200 Accepted.\r\n"}, + {"CWD " ++ UtfFileName, "250 Directory changed to \""++ UtfFileName ++"\".\r\n"}, + {"STOR " ++ UtfFileName, "150 File status okay; about to open data connection.\r\n"}, + {req, data_socket, BinData}, + {resp, socket, "226 Closing data connection.\r\n"}, + {"LIST", "150 File status okay; about to open data connection.\r\n"}, + {resp, data_socket, "d-wx--x--- 4 0 0 0 Dec 12 12:12 "++UtfFileName++"\r\n"}, + {resp, socket, "226 Closing data connection.\r\n"}], + + ok = meck:expect(fake_server,current_directory, fun(_) -> FileName end), + + login_test_user_with_data_socket(ControlPid, Script, Mode), + step(ControlPid), + step(ControlPid), + + meck:expect(fake_server,change_directory, + fun(State, InFileName) -> + ?assertEqual(InFileName, FileName), + {ok, State} + end), + step(ControlPid), + + meck:expect(fake_server,put_file, + fun(S, InFileName, write, F) -> + ?assertEqual(InFileName, FileName), + {ok, Data, DataSize} = F(), + ?assertEqual(Data, BinData), + ?assertEqual(DataSize, size(BinData)), + {ok, S} + end), + + ok = meck:expect(gen_tcp, close, fun(data_socket) -> ok end), + ok = meck:expect(inet, setopts, fun(data_socket,[{recbuf, _Size}]) -> ok end), + + step(ControlPid), + + meck:expect(fake_server, put_file, + fun(S, InFileName, notification, done) -> + ?assertEqual(InFileName, FileName), + {ok, S} + end), + + meck:expect(fake_server, list_files, + fun(_, _, _) -> + [#file_info{type=dir,name=FileName,mode=200,gid=0,uid=0, + mtime={{3019,12,12},{12,12,12}},size=0}] + end), + + ok = meck:expect(gen_tcp, close, fun(data_socket) -> ok end), + ok = meck:expect(inet, setopts, fun(data_socket,[{recbuf, _Size}]) -> ok end), + + step(ControlPid), + finish(ControlPid) + end), + execute(Child). + +?dataSocketTest(utf8_failure_test). +utf8_failure_test(Mode) -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + FileName = "Молоко-Яйки", %milk-eggs + UtfFileNameOk = bifrost:to_utf8(FileName), %milk-eggs + {UtfFileNameErr, _} = lists:split(length(UtfFileNameOk)-1, UtfFileNameOk), + + Script =[ {"OPTS UTF8 ON", "200 Accepted.\r\n"}, + {"CWD " ++ UtfFileNameErr, "501 Syntax error in parameters or arguments.\r\n"}], + + ok = meck:expect(gen_tcp, close, fun(data_socket) -> ok end), + ok = meck:expect(error_logger, warning_report, fun({bifrost, incomplete_utf8, _}) -> ok end), + + login_test_user_with_data_socket(ControlPid, Script, Mode), + step(ControlPid), + step(ControlPid), + finish(ControlPid) + end), + execute(Child). + +-endif. %% TEST