From 85220fa2db0f7c88986d7a5fc0ba2e7c9f5e070d Mon Sep 17 00:00:00 2001 From: Anatoly Berenblit Date: Mon, 31 Mar 2014 12:37:30 +0000 Subject: [PATCH 01/27] UTF8 Support --- src/bifrost.erl | 141 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 133 insertions(+), 8 deletions(-) diff --git a/src/bifrost.erl b/src/bifrost.erl index c22307d..5ab1078 100644 --- a/src/bifrost.erl +++ b/src/bifrost.erl @@ -15,6 +15,8 @@ %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-define(FEATURES, [ "UTF8" ]). + default(Expr, Default) -> case Expr of undefined -> @@ -23,6 +25,10 @@ default(Expr, Default) -> Expr end. +-spec ucs2_to_utf8(string()) -> string(). +ucs2_to_utf8(String) -> + erlang:binary_to_list(unicode:characters_to_binary(String, utf8)). + start_link(HookModule, Opts) -> gen_server:start_link(?MODULE, [HookModule, Opts], []). @@ -156,11 +162,11 @@ respond(Socket, ResponseCode) -> respond(Socket, ResponseCode, response_code_string(ResponseCode)). respond({SocketMod, Socket}, ResponseCode, Message) -> - Line = integer_to_list(ResponseCode) ++ " " ++ Message ++ "\r\n", + Line = integer_to_list(ResponseCode) ++ " " ++ ucs2_to_utf8(Message) ++ "\r\n", SocketMod:send(Socket, Line). respond_raw({SocketMod, Socket}, Line) -> - SocketMod:send(Socket, Line ++ "\r\n"). + SocketMod:send(Socket, ucs2_to_utf8(Line) ++ "\r\n"). ssl_options(State) -> [{keyfile, State#connection_state.ssl_key}, @@ -231,9 +237,20 @@ pasv_connection(ControlSocket, State) -> %% FTP COMMANDS -ftp_command(Socket, State, Command, Arg) -> +ftp_command(Socket, State, Command, RawArg) -> Mod = State#connection_state.module, - ftp_command(Mod, Socket, State, Command, Arg). + case unicode:characters_to_list(erlang:list_to_binary(RawArg), 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 -> + ftp_command(Mod, Socket, State, Command, Arg) + end. ftp_command(Mod, Socket, State, quit, _) -> respond(Socket, 200, "Goodbye."), @@ -302,6 +319,16 @@ ftp_command(Mod, Socket, State, pass, Arg) -> quit end; +%% based of rfc2389 +ftp_command(_Mod, Socket, State, feat, _Arg) -> + respond_raw(Socket, "211- Extensions supported:"), + lists:map( fun ({Feature, FeatureParams}) -> respond_raw(Socket, " " ++ Feature ++ " " ++ FeatureParams); + (Feature) -> respond_raw(Socket, " " ++ Feature) + end, + ?FEATURES), + respond(Socket, 211, "End"), + {ok, State}; + %% ^^^ from this point down every command requires authentication ^^^ ftp_command(_, Socket, State=#connection_state{authenticated_state=unauthenticated}, _, _) -> @@ -556,14 +583,13 @@ parse_input(Input) -> list_files_to_socket(DataSocket, Files) -> lists:map(fun(Info) -> bf_send(DataSocket, - file_info_to_string(Info) ++ "\r\n") end, + ucs2_to_utf8(file_info_to_string(Info)) ++ "\r\n") end, Files), ok. list_file_names_to_socket(DataSocket, Files) -> lists:map(fun(Info) -> - bf_send(DataSocket, - Info#file_info.name ++ "\r\n") end, + bf_send(DataSocket, ucs2_to_utf8(Info#file_info.name)++"\r\n") end, Files), ok. @@ -628,7 +654,7 @@ file_info_to_string(Info) -> format_number(Info#file_info.gid,5,$ ) ++ " " ++ format_number(Info#file_info.size,8,$ ) ++ " " ++ format_date(Info#file_info.mtime) ++ " " ++ - Info#file_info.name. + 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", @@ -1455,4 +1481,103 @@ quit_test() -> ), execute(Child). +feat_test() -> + setup(), + ControlPid = self(), + Child = spawn_link( + fun() -> + login_test_user(ControlPid, + [{"FEAT", "211- Extensions supported:\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 = ucs2_to_utf8(FileName), %milk-eggs + + Script =[ {"PWD " ++ UtfFileName, "257 \""++ UtfFileName ++"\"\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, <<"SOME DATA HERE">>}, + {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"}, + {"STOR " ++ UtfFileName, "150 File status okay; about to open data connection.\r\n"} + ], + + meck:expect(fake_server, + current_directory, + fun(_) -> FileName end), + + login_test_user_with_data_socket(ControlPid, Script, Mode), + step(ControlPid), + + meck:expect(fake_server, + change_directory, + fun(State, InFileName) -> + InFileName = FileName, + {ok, State} + end), + step(ControlPid), + + meck:expect(fake_server, + put_file, + fun(S, InFileName, write, F) -> + InFileName = FileName, + {ok, Data, DataSize} = F(), + BinData = <<"SOME DATA HERE">>, + ?assertEqual(Data, BinData), + ?assertEqual(DataSize, size(BinData)), + {ok, S} + end), + + meck:expect(gen_tcp, close, fun(data_socket) -> ok end), + + step(ControlPid), + + 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), + + meck:expect(gen_tcp, close, fun(data_socket) -> 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 = ucs2_to_utf8(FileName), %milk-eggs + { UtfFileName, _ } = lists:split(length(UtfFileNameOk)-1, UtfFileNameOk), + + Script =[ {"CWD " ++ UtfFileName, "501 Syntax error in parameters or arguments.\r\n"}], + + meck:expect(gen_tcp, close, fun(data_socket) -> ok end), + %meck:expect(error_logger, report, fun({bifrost, incomplete_utf8, _}) -> ok end), + + login_test_user_with_data_socket(ControlPid, Script, Mode), + step(ControlPid), + finish(ControlPid) + end), + execute(Child). + -endif. From 20bf17dd92dfc5046df4f2441a3871a370e7a2ae Mon Sep 17 00:00:00 2001 From: madRat Date: Mon, 31 Mar 2014 13:22:50 +0000 Subject: [PATCH 02/27] syntax sugar - use assertEqual macro --- src/bifrost.erl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/bifrost.erl b/src/bifrost.erl index 5ab1078..4162b8a 100644 --- a/src/bifrost.erl +++ b/src/bifrost.erl @@ -1504,11 +1504,12 @@ utf8_success_test(Mode) -> fun() -> FileName = "Молоко-Яйки", %milk-eggs UtfFileName = ucs2_to_utf8(FileName), %milk-eggs + FileContent = <<"SOME DATA HERE">>, Script =[ {"PWD " ++ UtfFileName, "257 \""++ UtfFileName ++"\"\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, <<"SOME DATA HERE">>}, + {req, data_socket, FileContent }, {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"}, @@ -1526,7 +1527,7 @@ utf8_success_test(Mode) -> meck:expect(fake_server, change_directory, fun(State, InFileName) -> - InFileName = FileName, + ?assertEqual(InFileName, FileName), {ok, State} end), step(ControlPid), @@ -1534,11 +1535,10 @@ utf8_success_test(Mode) -> meck:expect(fake_server, put_file, fun(S, InFileName, write, F) -> - InFileName = FileName, + ?assertEqual(InFileName, FileName), {ok, Data, DataSize} = F(), - BinData = <<"SOME DATA HERE">>, - ?assertEqual(Data, BinData), - ?assertEqual(DataSize, size(BinData)), + ?assertEqual(Data, FileContent), + ?assertEqual(DataSize, size(FileContent)), {ok, S} end), From c783ef23bfc7da8c5f3a6876c09da1edb570833d Mon Sep 17 00:00:00 2001 From: Dmitrii Zolotarev Date: Thu, 9 Oct 2014 17:58:18 +0700 Subject: [PATCH 03/27] Added support for commands options. It's need for commands like 'ls -la' --- sample/src/bifrost_memory_server.erl | 8 +- src/bifrost.erl | 111 ++++++++++++++------------- src/gen_bifrost_server.erl | 2 +- 3 files changed, 64 insertions(+), 57 deletions(-) diff --git a/sample/src/bifrost_memory_server.erl b/sample/src/bifrost_memory_server.erl index 30742be..8b08a49 100644 --- a/sample/src/bifrost_memory_server.erl +++ b/sample/src/bifrost_memory_server.erl @@ -17,7 +17,7 @@ current_directory/1, make_directory/2, change_directory/2, - list_files/2, + list_files/3, remove_directory/2, remove_file/2, put_file/4, @@ -143,9 +143,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 diff --git a/src/bifrost.erl b/src/bifrost.erl index 6c4f72a..59591a3 100644 --- a/src/bifrost.erl +++ b/src/bifrost.erl @@ -130,8 +130,8 @@ establish_control_connection(Socket, InitialState) -> control_loop(HookPid, {SocketMod, RawSocket} = Socket, State) -> case SocketMod:recv(RawSocket, 0) of {ok, Input} -> - {Command, Arg} = parse_input(Input), - case ftp_command(Socket, State, Command, Arg) of + {Command, Options, Arg} = parse_input(Input), + case ftp_command(Socket, State, Command, Options, Arg) of {ok, NewState} -> if is_pid(HookPid) -> HookPid ! {new_state, self(), NewState}, @@ -239,19 +239,19 @@ pasv_connection(ControlSocket, State) -> %% FTP COMMANDS -ftp_command(Socket, State, Command, Arg) -> +ftp_command(Socket, State, Command, Options, Arg) -> Mod = State#connection_state.module, - ftp_command(Mod, Socket, State, Command, Arg). + ftp_command(Mod, Socket, State, Command, Options, Arg). -ftp_command(Mod, Socket, State, quit, _) -> +ftp_command(Mod, Socket, State, quit, _, _) -> respond(Socket, 200, "Goodbye."), Mod:disconnect(State), quit; -ftp_command(_, Socket, State, pasv, _) -> +ftp_command(_, Socket, State, pasv, _, _) -> pasv_connection(Socket, State); -ftp_command(_, {_, RawSocket} = Socket, State, auth, Arg) -> +ftp_command(_, {_, RawSocket} = Socket, State, auth, _, Arg) -> if State#connection_state.ssl_allowed =:= false -> respond(Socket, 500), {ok, State}; @@ -275,7 +275,7 @@ ftp_command(_, {_, RawSocket} = Socket, State, auth, Arg) -> end end; -ftp_command(_, Socket, State, prot, Arg) -> +ftp_command(_, Socket, State, prot, _, Arg) -> ProtMode = case string:to_lower(Arg) of "c" -> clear; _ -> private @@ -283,15 +283,15 @@ 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(_, Socket, State, user, Arg) -> +ftp_command(_, Socket, State, user, _, Arg) -> respond(Socket, 331), {ok, State#connection_state{user_name=Arg}}; -ftp_command(_, Socket, State, port, Arg) -> +ftp_command(_, Socket, State, port, _, Arg) -> case parse_address(Arg) of {ok, {Addr, Port}} -> respond(Socket, 200), @@ -300,7 +300,7 @@ ftp_command(_, Socket, State, port, Arg) -> respond(Socket, 452, "Error parsing address.") 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), @@ -312,23 +312,23 @@ ftp_command(Mod, Socket, State, pass, Arg) -> %% ^^^ 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, cdup, _, _) -> ftp_command(Mod, Socket, State, cwd, ".."); -ftp_command(Mod, Socket, State, cwd, Arg) -> +ftp_command(Mod, Socket, State, cwd, _, Arg) -> case Mod:change_directory(State, Arg) of {ok, NewState} -> respond(Socket, 250, "directory changed to \"" ++ Mod:current_directory(NewState) ++ "\""), @@ -338,7 +338,7 @@ ftp_command(Mod, Socket, State, cwd, Arg) -> {ok, State} end; -ftp_command(Mod, Socket, State, mkd, Arg) -> +ftp_command(Mod, Socket, State, mkd, _, Arg) -> case Mod:make_directory(State, Arg) of {ok, NewState} -> respond(Socket, 250, "\"" ++ Arg ++ "\" directory created."), @@ -348,8 +348,8 @@ ftp_command(Mod, Socket, State, mkd, Arg) -> {ok, State} end; -ftp_command(Mod, Socket, State, nlst, Arg) -> - case Mod:list_files(State, Arg) of +ftp_command(Mod, Socket, State, nlst, Options, Arg) -> + case Mod:list_files(State, Options, Arg) of {error, NewState} -> respond(Socket, 451), {ok, NewState}; @@ -361,8 +361,8 @@ ftp_command(Mod, Socket, State, nlst, Arg) -> {ok, State} end; -ftp_command(Mod, Socket, State, list, Arg) -> - case Mod:list_files(State, Arg) of +ftp_command(Mod, Socket, State, list, Options, Arg) -> + case Mod:list_files(State, Options, Arg) of {error, _} -> respond(Socket, 451), {ok, State}; @@ -374,7 +374,7 @@ 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 Mod:remove_directory(State, Arg) of {ok, NewState} -> respond(Socket, 200), @@ -384,11 +384,11 @@ ftp_command(Mod, Socket, State, rmd, Arg) -> {ok, State} 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 Mod:remove_file(State, Arg) of {ok, NewState} -> respond(Socket, 200), @@ -398,7 +398,7 @@ ftp_command(Mod, Socket, State, dele, Arg) -> {ok, State} 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 @@ -419,7 +419,7 @@ ftp_command(Mod, Socket, State, stor, Arg) -> bf_close(DataSocket), {ok, RetState}; -ftp_command(_, Socket, State, type, Arg) -> +ftp_command(_, Socket, State, type, _, Arg) -> case Arg of "I" -> respond(Socket, 200); @@ -430,7 +430,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 Mod:site_command(State, list_to_atom(string:to_lower(Command)), string:join(Sargs, " ")) of {ok, NewState} -> @@ -444,7 +444,7 @@ ftp_command(Mod, Socket, State, site, Arg) -> {ok, State} end; -ftp_command(Mod, Socket, State, site_help, _) -> +ftp_command(Mod, Socket, State, site_help, _, _) -> case Mod:site_help(State) of {ok, []} -> respond(Socket, 500); @@ -460,7 +460,7 @@ ftp_command(Mod, Socket, State, site_help, _) -> end, {ok, State}; -ftp_command(Mod, Socket, State, help, Arg) -> +ftp_command(Mod, Socket, State, help, _, Arg) -> LowerArg = string:to_lower(Arg), case LowerArg of "site" -> @@ -470,7 +470,7 @@ ftp_command(Mod, Socket, State, help, Arg) -> {ok, State} end; -ftp_command(Mod, Socket, State, retr, Arg) -> +ftp_command(Mod, Socket, State, retr, _, Arg) -> try case Mod:get_file(State, Arg) of {ok, Fun} -> @@ -489,7 +489,7 @@ ftp_command(Mod, Socket, State, retr, Arg) -> {ok, State} end; -ftp_command(Mod, Socket, State, mdtm, Arg) -> +ftp_command(Mod, Socket, State, mdtm, _, Arg) -> case Mod:file_info(State, Arg) of {ok, FileInfo} -> respond(Socket, @@ -500,11 +500,11 @@ ftp_command(Mod, Socket, State, mdtm, Arg) -> end, {ok, State}; -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."), @@ -520,22 +520,22 @@ 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, feat, Arg) -> +ftp_command(Mod, Socket, State, feat, _, Arg) -> respond_raw(Socket, "211-Features"), case State#connection_state.utf8 of true -> @@ -546,7 +546,7 @@ ftp_command(Mod, Socket, State, feat, Arg) -> respond(Socket, 211, "End"), {ok, State}; -ftp_command(Mod, Socket, State, opts, Arg) -> +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"); @@ -555,11 +555,11 @@ ftp_command(Mod, Socket, State, opts, Arg) -> end, {ok, State}; -ftp_command(Mod, Socket, State, size, Arg) -> +ftp_command(Mod, Socket, State, size, _, Arg) -> respond(Socket, 550), {ok, State}; -ftp_command(_, Socket, State, Command, _Arg) -> +ftp_command(_, Socket, State, Command, _, _Arg) -> error_logger:warning_report({bifrost, unrecognized_command, Command}), respond(Socket, 500), {ok, State}. @@ -580,10 +580,17 @@ strip_newlines(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))), Options, string:join(Args, " ")}. list_files_to_socket(DataSocket, Files) -> lists:map(fun(Info) -> diff --git a/src/gen_bifrost_server.erl b/src/gen_bifrost_server.erl index 5c0d710..f178ee1 100644 --- a/src/gen_bifrost_server.erl +++ b/src/gen_bifrost_server.erl @@ -17,7 +17,7 @@ behaviour_info(callbacks) -> {current_directory, 1}, % State -> Path {make_directory, 2}, % State, Path -> State Change {change_directory, 2}, % State, Path -> State Change - {list_files, 2}, % State, Path -> [FileInfo] OR {error, State} + {list_files, 3}, % State, Path -> [FileInfo] OR {error, State} {remove_directory, 2}, % State, Path -> State Change {remove_file, 2}, % State, Path -> State Change {put_file, 4}, % State, File Name, (append OR write), Fun(Byte Count) -> State Change From cbeace725ec9ad5ecbe1027b5ad1c8134f3f1f3f Mon Sep 17 00:00:00 2001 From: Dmitrii Zolotarev Date: Thu, 9 Oct 2014 18:00:11 +0700 Subject: [PATCH 04/27] Changed Rebar config for sample application --- sample/rebar.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample/rebar.config b/sample/rebar.config index 239834d..63e06e6 100644 --- a/sample/rebar.config +++ b/sample/rebar.config @@ -1,5 +1,5 @@ {erl_opts, [debug_info]}. {deps, [ - {bifrost, ".*", {git, "git://github.com/thorstadt/bifrost.git", {branch, "master"}}} + {bifrost, ".*", {git, "https://github.com/dmitrii-zolotarev/bifrost.git", {branch, "master"}}} ]}. From 1d98d884f8bebf1d292608a065a998e25438cac1 Mon Sep 17 00:00:00 2001 From: Dmitrii Zolotarev Date: Thu, 9 Oct 2014 18:02:14 +0700 Subject: [PATCH 05/27] Changed description for list_files callback --- src/gen_bifrost_server.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gen_bifrost_server.erl b/src/gen_bifrost_server.erl index f178ee1..602bdf4 100644 --- a/src/gen_bifrost_server.erl +++ b/src/gen_bifrost_server.erl @@ -17,7 +17,7 @@ behaviour_info(callbacks) -> {current_directory, 1}, % State -> Path {make_directory, 2}, % State, Path -> State Change {change_directory, 2}, % State, Path -> State Change - {list_files, 3}, % State, Path -> [FileInfo] OR {error, State} + {list_files, 3}, % State, Options, Path -> [FileInfo] OR {error, State} {remove_directory, 2}, % State, Path -> State Change {remove_file, 2}, % State, Path -> State Change {put_file, 4}, % State, File Name, (append OR write), Fun(Byte Count) -> State Change From a68769481eab94b350ac5f75cf0cb82c8c5cf5dc Mon Sep 17 00:00:00 2001 From: Dmitrii Zolotarev Date: Fri, 31 Oct 2014 15:27:04 +0600 Subject: [PATCH 06/27] =?UTF-8?q?=D0=92=D0=B5=D1=80=D1=81=D0=B8=D0=BE?= =?UTF-8?q?=D0=BD=D0=BD=D0=BE=D1=81=D1=82=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bifrost.app.src | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, From b57b37987e608b98c459209af178241bcfeac372 Mon Sep 17 00:00:00 2001 From: Dmitrii Zolotarev Date: Mon, 10 Nov 2014 11:23:37 +0600 Subject: [PATCH 07/27] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=83=D1=82=D0=B5=D1=87=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D1=86=D0=B5=D1=81=D1=81=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B7=D0=B0=D0=BF?= =?UTF-8?q?=D1=83=D1=81=D0=BA=D0=B5=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81?= =?UTF-8?q?=D0=B0,=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BF=D1=80=D0=B5=D0=B4=D1=83=D0=BF=D1=80=D0=B5?= =?UTF-8?q?=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B8=20=D1=82=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bifrost.erl | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/bifrost.erl b/src/bifrost.erl index 59591a3..9e34dc6 100644 --- a/src/bifrost.erl +++ b/src/bifrost.erl @@ -10,7 +10,7 @@ -include("bifrost.hrl"). -include_lib("eunit/include/eunit.hrl"). --export([start_link/2, establish_control_connection/2, await_connections/2, supervise_connections/1]). +-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]). @@ -44,9 +44,10 @@ init([HookModule, Opts]) -> ssl_cert=SslCert, ssl_ca_cert=CaSslCert, utf8=UTF8}, + Self = self(), Supervisor = proc_lib:spawn_link(?MODULE, supervise_connections, - [HookModule:init(InitialState, Opts)]), + [Self, HookModule:init(InitialState, Opts)]), proc_lib:spawn_link(?MODULE, await_connections, [Listen, Supervisor]), @@ -96,24 +97,32 @@ await_connections(Listen, Supervisor) -> end, await_connections(Listen, Supervisor). -supervise_connections(InitialState) -> +supervise_connections(ParentPid, InitialState) -> process_flag(trap_exit, true), + erlang:monitor(process, ParentPid), + connections_monitor(InitialState). + +connections_monitor(InitialState) -> receive {new_connection, Acceptor, Socket} -> Worker = proc_lib:spawn_link(?MODULE, establish_control_connection, [Socket, InitialState]), - Acceptor ! {ack, Worker}; + 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_msg("Control connection ~p crashed: ~p~n", [Pid, Info]); + 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; _ -> - ok - end, - supervise_connections(InitialState). + connections_monitor(InitialState) + end. establish_control_connection(Socket, InitialState) -> respond({gen_tcp, Socket}, 220, "FTP Server Ready"), @@ -535,7 +544,7 @@ ftp_command(Mod, Socket, State, xpwd, Options, Arg) -> ftp_command(Mod, Socket, State, xrmd, Options, Arg) -> ftp_command(Mod, Socket, State, rmd, Options, Arg); -ftp_command(Mod, Socket, State, feat, _, Arg) -> +ftp_command(_Mod, Socket, State, feat, _, _Arg) -> respond_raw(Socket, "211-Features"), case State#connection_state.utf8 of true -> @@ -546,7 +555,7 @@ ftp_command(Mod, Socket, State, feat, _, Arg) -> respond(Socket, 211, "End"), {ok, State}; -ftp_command(Mod, Socket, State, opts, _, Arg) -> +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"); @@ -555,7 +564,7 @@ ftp_command(Mod, Socket, State, opts, _, Arg) -> end, {ok, State}; -ftp_command(Mod, Socket, State, size, _, Arg) -> +ftp_command(_Mod, Socket, State, size, _, _Arg) -> respond(Socket, 550), {ok, State}; @@ -861,9 +870,9 @@ strip_newlines_test() -> "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"). + {test, [], "3 2 1"} = 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), From 4b93bb42a61a79e550269232b5a539645f3f749e Mon Sep 17 00:00:00 2001 From: Ryan Crum Date: Wed, 12 Nov 2014 09:05:35 -0500 Subject: [PATCH 08/27] Remove unused test function --- src/bifrost.erl | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/bifrost.erl b/src/bifrost.erl index 60c4526..4d9115e 100644 --- a/src/bifrost.erl +++ b/src/bifrost.erl @@ -785,16 +785,6 @@ execute(ListenerPid) -> TEST_NAME(active), TEST_NAME(passive)). -mock_socket_response(S, R) -> - meck:expect(gen_tcp, - send, - fun(S2, R2) -> - ?assertEqual(S, S2), - ?assertEqual(R, - R2), - ok - end). - % Awkward, monadic interaction sequence testing script_dialog([]) -> meck:expect(gen_tcp, From 38e4c230f447cac3e96bd4a0fe494a555210b4ef Mon Sep 17 00:00:00 2001 From: Ryan Crum Date: Wed, 12 Nov 2014 09:08:02 -0500 Subject: [PATCH 09/27] Fix comment styles and indentation. --- src/bifrost.erl | 322 ++++++++++++++++++++++++------------------------ 1 file changed, 161 insertions(+), 161 deletions(-) diff --git a/src/bifrost.erl b/src/bifrost.erl index 4d9115e..a23223c 100644 --- a/src/bifrost.erl +++ b/src/bifrost.erl @@ -26,7 +26,7 @@ default(Expr, Default) -> start_link(HookModule, Opts) -> gen_server:start_link(?MODULE, [HookModule, Opts], []). -% gen_server callbacks implementation +%% gen_server callbacks implementation init([HookModule, Opts]) -> Port = default(proplists:get_value(port, Opts), 21), Ssl = default(proplists:get_value(ssl, Opts), false), @@ -101,8 +101,8 @@ supervise_connections(InitialState) -> receive {new_connection, Acceptor, Socket} -> Worker = proc_lib:spawn_link(?MODULE, - establish_control_connection, - [Socket, InitialState]), + establish_control_connection, + [Socket, InitialState]), Acceptor ! {ack, Worker}; {'EXIT', _Pid, normal} -> % not a crash ok; @@ -198,19 +198,19 @@ data_connection(ControlSocket, State) -> end. -% passive -- accepts an inbound connection +%% passive -- accepts an inbound connection establish_data_connection(#connection_state{pasv_listen={passive, Listen, _}}) -> gen_tcp:accept(Listen); -% active -- establishes an outbound connection +%% active -- establishes an outbound connection establish_data_connection(#connection_state{data_port={active, Addr, Port}}) -> gen_tcp:connect(Addr, Port, [{active, false}, binary]). 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 -> @@ -296,9 +296,9 @@ ftp_command(_, Socket, State, port, Arg) -> {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) -> case Mod:login(State, State#connection_state.user_name, Arg) of @@ -308,7 +308,7 @@ ftp_command(Mod, Socket, State, pass, Arg) -> _ -> respond(Socket, 530, "Login incorrect."), quit - end; + end; %% ^^^ from this point down every command requires authentication ^^^ @@ -382,7 +382,7 @@ ftp_command(Mod, Socket, State, rmd, Arg) -> {error, _} -> respond(Socket, 550), {ok, State} - end; + end; ftp_command(_, Socket, State, syst, _) -> respond(Socket, 215, "UNIX Type: L8"), @@ -396,7 +396,7 @@ ftp_command(Mod, Socket, State, dele, Arg) -> {error, _} -> respond(Socket, 450), {ok, State} - end; + end; ftp_command(Mod, Socket, State, stor, Arg) -> DataSocket = data_connection(Socket, State), @@ -473,21 +473,21 @@ ftp_command(Mod, Socket, State, help, Arg) -> ftp_command(Mod, Socket, State, retr, Arg) -> try case Mod:get_file(State, Arg) of - {ok, Fun} -> - DataSocket = data_connection(Socket, State), - {ok, NewState} = write_fun(DataSocket, Fun), - respond(Socket, 226), - bf_close(DataSocket), - {ok, NewState}; - error -> - respond(Socket, 550), - {ok, State} + {ok, Fun} -> + DataSocket = data_connection(Socket, State), + {ok, NewState} = write_fun(DataSocket, Fun), + respond(Socket, 226), + bf_close(DataSocket), + {ok, NewState}; + error -> + respond(Socket, 550), + {ok, State} end - catch - _ -> - respond(Socket, 550), - {ok, State} - end; + catch + _ -> + respond(Socket, 550), + {ok, State} + end; ftp_command(Mod, Socket, State, mdtm, Arg) -> case Mod:file_info(State, Arg) of @@ -575,7 +575,7 @@ write_fun(Socket, Fun) -> strip_newlines(S) -> lists:foldr(fun(C, A) -> - string:strip(A, right, C) end, + string:strip(A, right, C) end, S, "\r\n"). @@ -608,7 +608,7 @@ bf_close({SockMod, Socket}) -> bf_recv({SockMod, Socket}) -> SockMod:recv(Socket, 0). -% Adapted from jungerl/ftpd.erl +%% 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."; @@ -650,7 +650,7 @@ 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) ++ @@ -727,10 +727,10 @@ 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, []). @@ -760,7 +760,7 @@ format_port(PortNumber) -> %% EUNIT TESTS %% -% Testing Utility Functions % +%% Testing Utility Functions setup() -> meck:new(gen_tcp, [unstick]), @@ -785,7 +785,7 @@ execute(ListenerPid) -> TEST_NAME(active), TEST_NAME(passive)). -% Awkward, monadic interaction sequence testing +%% Awkward, monadic interaction sequence testing script_dialog([]) -> meck:expect(gen_tcp, recv, @@ -822,7 +822,7 @@ script_dialog([{req, Socket, Request} | Rest]) -> {ok, Request} end). -% executes the next step in the test script +%% executes the next step in the test script step(Pid) -> Pid ! {ack, self()}, receive @@ -832,12 +832,12 @@ step(Pid) -> ?assert(fail) end. -% stops the script +%% stops the script finish(Pid) -> Pid ! {done, self()}. -% Unit Tests % +%% Unit Tests strip_newlines_test() -> "testing 1 2 3" = strip_newlines("testing 1 2 3\r\n"), @@ -864,7 +864,7 @@ parse_address_test() -> error = parse_address("MEAT MEAT"). -% Functional/Integration Tests % +%% Functional/Integration Tests authenticate_state(State) -> State#connection_state{authenticated_state=authenticated}. @@ -910,31 +910,31 @@ authenticate_failure_test() -> 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), - ControlPid ! go, - ?assertMatch(#connection_state{authenticated_state=unauthenticated}, step(ControlPid)), - finish(ControlPid) + 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), + ControlPid ! go, + ?assertMatch(#connection_state{authenticated_state=unauthenticated}, step(ControlPid)), + finish(ControlPid) end), - execute(Child). + 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"}]), + script_dialog([ {"CWD /hamster", "530 Not logged in.\r\n"}, + {"MKD /unicorns", "530 Not logged in.\r\n"}]), - ControlPid ! go, - ?assertMatch(#connection_state{authenticated_state=unauthenticated}, step(ControlPid)), - ?assertMatch(#connection_state{authenticated_state=unauthenticated}, step(ControlPid)), - finish(ControlPid) + ControlPid ! go, + ?assertMatch(#connection_state{authenticated_state=unauthenticated}, step(ControlPid)), + ?assertMatch(#connection_state{authenticated_state=unauthenticated}, step(ControlPid)), + finish(ControlPid) end), - execute(Child). + execute(Child). mkdir_test() -> setup(), @@ -1096,9 +1096,9 @@ nlst_test(Mode) -> meck:expect(gen_tcp, close, fun(data_socket) -> 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"}], + {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) @@ -1148,52 +1148,52 @@ 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), - - meck:expect(fake_server, - remove_directory, - fun(_, "/bison/burgers") -> - {error, error} - end), - step(ControlPid), - - finish(ControlPid) - end), + 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), + + 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", "200 Command okay.\r\n"}, - {"DELE cheese.txt", "450 Requested file action not taken.\r\n"}]), - step(ControlPid), - - meck:expect(fake_server, - remove_file, - fun(_, "cheese.txt") -> - {error, error} - end), - - step(ControlPid), - finish(ControlPid) - end), + fun() -> + meck:expect(fake_server, + remove_file, + fun(St, "cheese.txt") -> + {ok, St} + end), + + login_test_user(ControlPid, [{"DELE cheese.txt", "200 Command okay.\r\n"}, + {"DELE cheese.txt", "450 Requested file action not taken.\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). @@ -1201,27 +1201,27 @@ stor_test(Mode) -> setup(), ControlPid = self(), Child = spawn_link( - fun() -> - Script = [{"STOR bologna.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"} - ], - meck:expect(fake_server, - put_file, - fun(S, "bologna.txt", write, F) -> - {ok, Data, DataSize} = F(), - BinData = <<"SOME DATA HERE">>, - ?assertEqual(Data, BinData), - ?assertEqual(DataSize, size(BinData)), - {ok, S} - end), - - meck:expect(gen_tcp, close, fun(data_socket) -> ok end), - - login_test_user_with_data_socket(ControlPid, Script, Mode), - step(ControlPid), - finish(ControlPid) - end), + fun() -> + Script = [{"STOR bologna.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"} + ], + meck:expect(fake_server, + put_file, + fun(S, "bologna.txt", write, F) -> + {ok, Data, DataSize} = F(), + BinData = <<"SOME DATA HERE">>, + ?assertEqual(Data, BinData), + ?assertEqual(DataSize, size(BinData)), + {ok, S} + end), + + meck:expect(gen_tcp, close, fun(data_socket) -> ok end), + + login_test_user_with_data_socket(ControlPid, Script, Mode), + step(ControlPid), + finish(ControlPid) + end), execute(Child). ?dataSocketTest(stor_failure_test). @@ -1229,24 +1229,24 @@ stor_failure_test(Mode) -> setup(), ControlPid = self(), Child = spawn_link( - fun() -> - Script = [{"STOR bologna.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"} - ], - meck:expect(fake_server, - put_file, - fun(_, "bologna.txt", write, F) -> - F(), - {error, access_denied} - end), - - meck:expect(gen_tcp, close, fun(data_socket) -> ok end), - - login_test_user_with_data_socket(ControlPid, Script, Mode), - step(ControlPid), - finish(ControlPid) - end), + fun() -> + Script = [{"STOR bologna.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"} + ], + meck:expect(fake_server, + put_file, + fun(_, "bologna.txt", write, F) -> + F(), + {error, access_denied} + end), + + meck:expect(gen_tcp, close, fun(data_socket) -> ok end), + + login_test_user_with_data_socket(ControlPid, Script, Mode), + step(ControlPid), + finish(ControlPid) + end), execute(Child). ?dataSocketTest(retr_test). @@ -1254,31 +1254,31 @@ retr_test(Mode) -> setup(), 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) -> - {ok, - list_to_binary("SOME DATA HERE"), - fun(1024) -> - {ok, - list_to_binary("SOME MORE DATA"), - fun(1024) -> {done, State} end} - end} - end} - end), - - meck:expect(gen_tcp, close, fun(data_socket) -> ok end), - login_test_user_with_data_socket(ControlPid, Script, Mode), - step(ControlPid), - finish(ControlPid) - end), + 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) -> + {ok, + list_to_binary("SOME DATA HERE"), + fun(1024) -> + {ok, + list_to_binary("SOME MORE DATA"), + fun(1024) -> {done, State} end} + end} + end} + end), + + meck:expect(gen_tcp, close, fun(data_socket) -> ok end), + login_test_user_with_data_socket(ControlPid, Script, Mode), + step(ControlPid), + finish(ControlPid) + end), execute(Child). rein_test() -> From b8b6502edb8ed62414471a6eac9fa4a2fdcefdfe Mon Sep 17 00:00:00 2001 From: Ryan Crum Date: Wed, 12 Nov 2014 09:14:58 -0500 Subject: [PATCH 10/27] Whitespace/indentation fixes --- src/bifrost.erl | 161 ++++++++++++++++++++++++------------------------ 1 file changed, 80 insertions(+), 81 deletions(-) diff --git a/src/bifrost.erl b/src/bifrost.erl index cb0706b..7848b7b 100644 --- a/src/bifrost.erl +++ b/src/bifrost.erl @@ -27,7 +27,7 @@ default(Expr, Default) -> -spec ucs2_to_utf8(string()) -> string(). ucs2_to_utf8(String) -> - erlang:binary_to_list(unicode:characters_to_binary(String, utf8)). + erlang:binary_to_list(unicode:characters_to_binary(String, utf8)). start_link(HookModule, Opts) -> gen_server:start_link(?MODULE, [HookModule, Opts], []). @@ -247,18 +247,18 @@ pasv_connection(ControlSocket, State) -> ftp_command(Socket, State, Command, RawArg) -> Mod = State#connection_state.module, - case unicode:characters_to_list(erlang:list_to_binary(RawArg), 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 -> - ftp_command(Mod, Socket, State, Command, Arg) - end. + case unicode:characters_to_list(erlang:list_to_binary(RawArg), 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 -> + ftp_command(Mod, Socket, State, Command, Arg) + end. ftp_command(Mod, Socket, State, quit, _) -> respond(Socket, 200, "Goodbye."), @@ -330,11 +330,11 @@ ftp_command(Mod, Socket, State, pass, Arg) -> %% based of rfc2389 ftp_command(_Mod, Socket, State, feat, _Arg) -> respond_raw(Socket, "211- Extensions supported:"), - lists:map( fun ({Feature, FeatureParams}) -> respond_raw(Socket, " " ++ Feature ++ " " ++ FeatureParams); - (Feature) -> respond_raw(Socket, " " ++ Feature) - end, - ?FEATURES), - respond(Socket, 211, "End"), + lists:map( fun ({Feature, FeatureParams}) -> respond_raw(Socket, " " ++ Feature ++ " " ++ FeatureParams); + (Feature) -> respond_raw(Socket, " " ++ Feature) + end, + ?FEATURES), + respond(Socket, 211, "End"), {ok, State}; %% ^^^ from this point down every command requires authentication ^^^ @@ -686,7 +686,7 @@ file_info_to_string(Info) -> format_number(Info#file_info.gid,5,$ ) ++ " " ++ format_number(Info#file_info.size,8,$ ) ++ " " ++ format_date(Info#file_info.mtime) ++ " " ++ - Info#file_info.name. + 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", @@ -1483,8 +1483,8 @@ feat_test() -> fun() -> login_test_user(ControlPid, [{"FEAT", "211- Extensions supported:\r\n"}, - {resp, socket, " UTF8\r\n" }, - {resp, socket, "211 End\r\n" }]), + {resp, socket, " UTF8\r\n" }, + {resp, socket, "211 End\r\n" }]), step(ControlPid), finish(ControlPid) end @@ -1496,61 +1496,61 @@ utf8_success_test(Mode) -> setup(), ControlPid = self(), Child = spawn_link( - fun() -> - FileName = "Молоко-Яйки", %milk-eggs - UtfFileName = ucs2_to_utf8(FileName), %milk-eggs - FileContent = <<"SOME DATA HERE">>, - - Script =[ {"PWD " ++ UtfFileName, "257 \""++ UtfFileName ++"\"\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, FileContent }, - {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"}, - {"STOR " ++ UtfFileName, "150 File status okay; about to open data connection.\r\n"} - ], - - meck:expect(fake_server, + fun() -> + FileName = "Молоко-Яйки", % milk-eggs + UtfFileName = ucs2_to_utf8(FileName), % milk-eggs + FileContent = <<"SOME DATA HERE">>, + + Script =[{"PWD " ++ UtfFileName, "257 \""++ UtfFileName ++"\"\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, FileContent }, + {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"}, + {"STOR " ++ UtfFileName, "150 File status okay; about to open data connection.\r\n"} + ], + + meck:expect(fake_server, current_directory, fun(_) -> FileName end), - login_test_user_with_data_socket(ControlPid, Script, Mode), - step(ControlPid), + login_test_user_with_data_socket(ControlPid, Script, Mode), + step(ControlPid), - meck:expect(fake_server, + 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, FileContent), - ?assertEqual(DataSize, size(FileContent)), - {ok, S} - end), - - meck:expect(gen_tcp, close, fun(data_socket) -> ok end), - - step(ControlPid), - - 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), - - meck:expect(gen_tcp, close, fun(data_socket) -> ok end), - - step(ControlPid), - finish(ControlPid) + 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, FileContent), + ?assertEqual(DataSize, size(FileContent)), + {ok, S} + end), + + meck:expect(gen_tcp, close, fun(data_socket) -> ok end), + + step(ControlPid), + + 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), + + meck:expect(gen_tcp, close, fun(data_socket) -> ok end), + + step(ControlPid), + finish(ControlPid) end), execute(Child). @@ -1559,19 +1559,18 @@ utf8_failure_test(Mode) -> setup(), ControlPid = self(), Child = spawn_link( - fun() -> - FileName = "Молоко-Яйки", %milk-eggs - UtfFileNameOk = ucs2_to_utf8(FileName), %milk-eggs - { UtfFileName, _ } = lists:split(length(UtfFileNameOk)-1, UtfFileNameOk), + fun() -> + FileName = "Молоко-Яйки", % milk-eggs + UtfFileNameOk = ucs2_to_utf8(FileName), % milk-eggs + { UtfFileName, _ } = lists:split(length(UtfFileNameOk)-1, UtfFileNameOk), - Script =[ {"CWD " ++ UtfFileName, "501 Syntax error in parameters or arguments.\r\n"}], + Script =[{"CWD " ++ UtfFileName, "501 Syntax error in parameters or arguments.\r\n"}], - meck:expect(gen_tcp, close, fun(data_socket) -> ok end), - %meck:expect(error_logger, report, fun({bifrost, incomplete_utf8, _}) -> ok end), + meck:expect(gen_tcp, close, fun(data_socket) -> ok end), - login_test_user_with_data_socket(ControlPid, Script, Mode), - step(ControlPid), - finish(ControlPid) + login_test_user_with_data_socket(ControlPid, Script, Mode), + step(ControlPid), + finish(ControlPid) end), execute(Child). From 25a01a9c084e6e71a6e348e11eefc510cd818a47 Mon Sep 17 00:00:00 2001 From: Dmitrii Zolotarev Date: Tue, 23 Dec 2014 12:22:49 +0600 Subject: [PATCH 11/27] =?UTF-8?q?=D0=9E=D0=B1=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=BA=D0=B0=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bifrost.erl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/bifrost.erl b/src/bifrost.erl index f25b931..e948b30 100644 --- a/src/bifrost.erl +++ b/src/bifrost.erl @@ -96,7 +96,12 @@ await_connections(Listen, Supervisor) -> receive {ack, Worker} -> %% ssl:ssl_accept/2 will return {error, not_owner} otherwise - ok = gen_tcp:controlling_process(Socket, Worker) + case gen_tcp:controlling_process(Socket, Worker) of + ok -> + ok; + {error, Reason} -> + exit(Reason) + end end; _Error -> exit(bad_accept) From 3344703a50603c4d2199a1ecf73c990ffc7900eb Mon Sep 17 00:00:00 2001 From: Dmitrii Zolotarev Date: Fri, 30 Jan 2015 13:29:33 +0600 Subject: [PATCH 12/27] =?UTF-8?q?refs=20#39599=20=D0=98=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BE=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D0=BA=D0=B0=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD?= =?UTF-8?q?=D0=B4=D1=8B=20CDUP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bifrost.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bifrost.erl b/src/bifrost.erl index e948b30..d5f3b49 100644 --- a/src/bifrost.erl +++ b/src/bifrost.erl @@ -366,8 +366,8 @@ 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) -> case Mod:change_directory(State, Arg) of From 7d95e44dec30327bed0295e2c87e8579586d7937 Mon Sep 17 00:00:00 2001 From: Andrey Teplyashin Date: Tue, 17 Feb 2015 13:43:28 +0600 Subject: [PATCH 13/27] Add external meck --- rebar.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebar.config b/rebar.config index a77b21a..6a8d5f6 100644 --- a/rebar.config +++ b/rebar.config @@ -2,5 +2,5 @@ {erl_opts, [debug_info]}. {deps, [ - {meck, ".*", {git, "git://github.com/eproxus/meck.git", "0.8.1"}} + {meck, ".*", {git, "git@git.eltex.loc:external/meck.git"}} ]}. From 0a914caec60d98d18f24412c7e303d5404e6770c Mon Sep 17 00:00:00 2001 From: Eugeny Bachar Date: Fri, 6 Mar 2015 20:15:52 +0600 Subject: [PATCH 14/27] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D1=82=D1=8F=D0=BD?= =?UTF-8?q?=D1=83=D0=BB=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B8=D0=B7=20github=20=D1=81=20=D0=BA=D0=BE=D1=80?= =?UTF-8?q?=D1=80=D0=B5=D0=BA=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B0?= =?UTF-8?q?=D0=BC=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D1=8B=20SFTP.=20=D0=9F=D0=BE=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BF=D1=80=D0=BE=D1=85=D0=BE=D0=B6=D0=B4=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=8E=D0=BD=D0=B8=D1=82-=D1=82=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D0=B2.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/bifrost.hrl | 18 +- rebar.config | 2 + sample/src/bifrost_memory_server.erl | 19 +- src/bifrost.erl | 1389 ++++++++++++++++++-------- src/gen_bifrost_server.erl | 57 +- 5 files changed, 1033 insertions(+), 452 deletions(-) diff --git a/include/bifrost.hrl b/include/bifrost.hrl index c26064f..8e079be 100644 --- a/include/bifrost.hrl +++ b/include/bifrost.hrl @@ -9,7 +9,10 @@ rnfr = undefined, module, module_state, - ssl_allowed = false, + 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, @@ -17,8 +20,17 @@ pb_size = 0, control_socket = undefined, ssl_socket = undefined, - utf8 = false - }). + 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 + }). -record(file_info, { diff --git a/rebar.config b/rebar.config index 6a8d5f6..f2ffbc5 100644 --- a/rebar.config +++ b/rebar.config @@ -4,3 +4,5 @@ {deps, [ {meck, ".*", {git, "git@git.eltex.loc:external/meck.git"}} ]}. + +{clean_files, ["*.eunit", "ebin/*.beam"]}. diff --git a/sample/src/bifrost_memory_server.erl b/sample/src/bifrost_memory_server.erl index 8b08a49..9950f0a 100644 --- a/sample/src/bifrost_memory_server.erl +++ b/sample/src/bifrost_memory_server.erl @@ -14,6 +14,7 @@ % Bifrost callbacks -export([login/3, init/2, + check_user/2, current_directory/1, make_directory/2, change_directory/2, @@ -26,7 +27,7 @@ rename_file/3, site_command/3, site_help/1, - disconnect/1]). + disconnect/2]). -ifdef(debug). -compile(export_all). @@ -48,6 +49,15 @@ init(InitialState, _) -> 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. + % Authenticate the user. Return {false, State} to fail. login(State, _Username, _Password) -> {true, initialize_state(State)}. @@ -92,7 +102,7 @@ change_directory(State, Directory) -> {error, State} end. -disconnect(_) -> +disconnect(_, Reason) -> ok. % Delete a file @@ -166,6 +176,11 @@ list_files(State, _Options, Directory) -> % mode could be append or write, but we're only supporting % write. % FileRetrievalFun is fun() and returns {ok, Bytes, Count} or done + +% Upload notification is arriving during it with FileRetrievalFun == notification +% and _Status done or terminated +put_file(State, _ProvidedFileName, notification, _Status) -> + {ok, State}; put_file(State, ProvidedFileName, _Mode, FileRetrievalFun) -> FileName = lists:last(string:tokens(ProvidedFileName, "/")), Target = absolute_path(State, FileName), diff --git a/src/bifrost.erl b/src/bifrost.erl index d5f3b49..2a55da8 100644 --- a/src/bifrost.erl +++ b/src/bifrost.erl @@ -15,53 +15,87 @@ %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --define(FEATURES, [ "UTF8" ]). - -default(Expr, Default) -> - case Expr of - undefined -> - Default; - _ -> - Expr - end. - --spec ucs2_to_utf8(string()) -> string(). -ucs2_to_utf8(String) -> - erlang:binary_to_list(unicode:characters_to_binary(String, utf8)). +-define(MAX_TCPIP_PORT, 65535). start_link(HookModule, Opts) -> gen_server:start_link(?MODULE, [HookModule, Opts], []). %% gen_server callbacks implementation init([HookModule, Opts]) -> - Port = default(proplists:get_value(port, Opts), 21), - Ssl = default(proplists:get_value(ssl, Opts), false), - 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), - case listen_socket(Port, [{active, false}, {reuseaddr, true}, list]) of - {ok, Listen} -> - IpAddress = default(proplists:get_value(ip_address, Opts), get_socket_addr(Listen)), - InitialState = #connection_state{module=HookModule, - ip_address=IpAddress, - ssl_allowed=Ssl, - ssl_key=SslKey, - ssl_cert=SslCert, - ssl_ca_cert=CaSslCert, - utf8=UTF8}, - Self = self(), - Supervisor = proc_lib:spawn_link(?MODULE, - supervise_connections, - [Self, HookModule:init(InitialState, Opts)]), - proc_lib:spawn_link(?MODULE, - await_connections, - [Listen, Supervisor]), - {ok, {listen_socket, Listen}}; - {error, Error} -> - {stop, Error} + 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}; + _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}, + Self = self(), + Supervisor = proc_lib:spawn_link(?MODULE, + supervise_connections, + [Self, HookModule:init(InitialState, Opts)]), + 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({bifrost, init_error, Reason}), + {stop, Reason}; + _Type1:Exception -> + error_logger:error_report({bifrost, init_exception, Exception}), + {stop, Exception} end. +%------------------------------------------------------------------------------- + handle_call(_Request, _From, State) -> Reply = ok, {reply, Reply, State}. @@ -81,12 +115,43 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. get_socket_addr(Socket) -> - case inet:sockname(Socket) of - {ok, {Addr, _}} -> - Addr + {ok, {Addr, _Port}} = inet:sockname(Socket), + Addr. + +%------------------------------------------------------------------------------- +get_socket_port(Socket) -> + {ok, {_Addr, Port}} = inet:sockname(Socket), + Port. + +%------------------------------------------------------------------------------- +listen_socket({Start, End}, _TcpOpts, _NextPort) when End < Start -> + 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); + +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. -listen_socket(Port, TcpOpts) -> +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({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) -> @@ -147,10 +212,11 @@ establish_control_connection(Socket, InitialState) -> {gen_tcp, Socket}, InitialState#connection_state{control_socket=Socket, ip_address=IpAddress}). -control_loop(HookPid, {SocketMod, RawSocket} = Socket, State) -> - case SocketMod:recv(RawSocket, 0) of +control_loop(HookPid, {SocketMod, RawSocket} = Socket, State0) -> + case SocketMod:recv(RawSocket, 0, State0#connection_state.control_timeout) of {ok, Input} -> {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) -> @@ -159,6 +225,7 @@ control_loop(HookPid, {SocketMod, RawSocket} = Socket, State) -> {ack, HookPid} -> control_loop(HookPid, Socket, NewState); {done, HookPid} -> + disconnect(State, {error, breaked}), {error, closed} end; true -> @@ -168,28 +235,46 @@ control_loop(HookPid, {SocketMod, RawSocket} = Socket, State) -> control_loop(HookPid, NewSock, NewState); {error, timeout} -> respond(Socket, 412, "Timed out. Closing control connection."), + disconnect(State, {error, timeout}), SocketMod:close(RawSocket), {error, timeout}; {error, closed} -> + disconnect(State, {error, closed}), {error, closed}; + {error, auth} -> + disconnect(State, {error, auth}), + SocketMod:close(RawSocket), + {ok, quit}; quit -> + disconnect(State, exit), SocketMod:close(RawSocket), {ok, quit} end; - {error, _Reason} -> - error_logger:warning_report({bifrost, connection_terminated}) + {error, timeout} -> + NewState = prev_cmd_notify(Socket, State0, timeout), + control_loop(HookPid, Socket, NewState); + {error, Reason} -> + State = prev_cmd_notify(Socket, State0, terminated), + disconnect(State, {error, Reason}), + error_logger:warning_report({bifrost, connection_terminated}), + {error, Reason} end. respond(Socket, ResponseCode) -> - respond(Socket, ResponseCode, response_code_string(ResponseCode)). + respond(Socket, ResponseCode, response_code_string(ResponseCode) ++ "."). respond({SocketMod, Socket}, ResponseCode, Message) -> - Line = integer_to_list(ResponseCode) ++ " " ++ ucs2_to_utf8(Message) ++ "\r\n", + Line = integer_to_list(ResponseCode) ++ " " ++ to_utf8(Message) ++ "\r\n", SocketMod:send(Socket, Line). respond_raw({SocketMod, Socket}, Line) -> - SocketMod:send(Socket, ucs2_to_utf8(Line) ++ "\r\n"). + SocketMod:send(Socket, to_utf8(Line) ++ "\r\n"). +respond_feature(Socket, Name, true) -> + respond_raw(Socket, " " ++ Name); +respond_feature(_Socket, _Name, false) -> + ok. + ssl_options(State) -> [{keyfile, State#connection_state.ssl_key}, {certfile, State#connection_state.ssl_cert}, @@ -199,6 +284,12 @@ data_connection(ControlSocket, State) -> respond(ControlSocket, 150), case establish_data_connection(State) of {ok, DataSocket} -> + %% 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}; @@ -217,7 +308,6 @@ data_connection(ControlSocket, State) -> throw(Error) end. - %% passive -- accepts an inbound connection establish_data_connection(#connection_state{pasv_listen={passive, Listen, _}}) -> gen_tcp:accept(Listen); @@ -234,9 +324,10 @@ pasv_connection(ControlSocket, State) -> gen_tcp:close(PasvListen), pasv_connection(ControlSocket, State#connection_state{pasv_listen=undefined}); undefined -> - case listen_socket(0, [{active, false}, binary]) of + case listen_socket(State#connection_state.port_range, [{active, false}, binary]) of {ok, Listen} -> {ok, {_, Port}} = inet:sockname(Listen), + Port = get_socket_port(Listen), Ip = State#connection_state.ip_address, PasvSocketInfo = {passive, Listen, @@ -257,55 +348,143 @@ pasv_connection(ControlSocket, State) -> end end. -%% FTP COMMANDS + +%%------------------------------------------------------------------------------- +%% 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({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, error}) -> + ftp_result(State, error); + +ftp_result(State, error) -> + ftp_result(State, {error, State}); + +ftp_result(_State, {error, #connection_state{}=NewState}) -> + {error, undef, NewState}; + +ftp_result(State, {error, Reason}) -> + {error, Reason, State}; + +ftp_result(_State, {error, Reason, #connection_state{}=NewState}) -> + {error, Reason, NewState}; + +ftp_result(_State, {error, #connection_state{}=NewState, Reason}) -> + {error, Reason, NewState}; + +ftp_result(_State, Data) -> + Data. + +-spec ftp_result(#connection_state{}, term(), fun()) -> term(). +ftp_result(State, Data, UserFunction) -> + ftp_result(State, UserFunction(State, Data)). + + + +%%------------------------------------------------------------------------------- +%% FTP COMMANDS ftp_command(Socket, State, Command, Options, RawArg) -> Mod = State#connection_state.module, - case unicode:characters_to_list(erlang:list_to_binary(RawArg), utf8) of - { error, List, _RestData } -> + 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 } -> + {incomplete, List, _Binary} -> error_logger:warning_report({bifrost, incomplete_utf8, List}), respond(Socket, 501), {ok, State}; Arg -> - ftp_command(Mod, Socket, State, Command, Options, 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, _, _) -> +ftp_command(_Mod, Socket, _State, quit, _, _) -> respond(Socket, 200, "Goodbye."), - Mod:disconnect(State), quit; -ftp_command(_, Socket, State, pasv, _, _) -> - pasv_connection(Socket, State); +ftp_command(_Mod, Socket, State=#connection_state{ssl_mode=disabled}, auth, _, _Arg) -> + respond(Socket, 504), + {ok, State}; -ftp_command(_, {_, RawSocket} = Socket, State, auth, _, Arg) -> - if State#connection_state.ssl_allowed =:= false -> - respond(Socket, 500), - {ok, State}; - true -> - 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}}; - _ -> - respond(Socket, 500), - {ok, State} - end; - _ -> - respond(Socket, 502, "Unsupported security extension."), - {ok, State} - end +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, 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}; + +ftp_command(_, Socket, State, pasv, _, _) -> + pasv_connection(Socket, State); + ftp_command(_, Socket, State, prot, _, Arg) -> ProtMode = case string:to_lower(Arg) of "c" -> clear; @@ -318,9 +497,17 @@ ftp_command(_, Socket, State, pbsz, _, "0") -> respond(Socket, 200), {ok, State}; -ftp_command(_, Socket, State, user, _, Arg) -> - respond(Socket, 331), - {ok, State#connection_state{user_name=Arg}}; +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({bifrost, user_check, Reason}), + respond(Socket, 421, format_error("Login requirements", Reason)), + {error, auth} + end; ftp_command(_, Socket, State, port, _, Arg) -> case parse_address(Arg) of @@ -336,21 +523,14 @@ ftp_command(Mod, Socket, State, pass, _, Arg) -> {true, NewState} -> respond(Socket, 230), {ok, NewState#connection_state{authenticated_state=authenticated}}; - _ -> + {false, NewState} -> + respond(Socket, 530, "Login incorrect."), + {ok, NewState#connection_state{user_name=none, authenticated_state=unauthenticated}}; + _Quit -> respond(Socket, 530, "Login incorrect."), - quit + {error, auth} end; -%% based of rfc2389 -ftp_command(_Mod, Socket, State, feat, _, _Arg) -> - respond_raw(Socket, "211- Extensions supported:"), - lists:map( fun ({Feature, FeatureParams}) -> respond_raw(Socket, " " ++ Feature ++ " " ++ FeatureParams); - (Feature) -> respond_raw(Socket, " " ++ Feature) - end, - ?FEATURES), - respond(Socket, 211, "End"), - {ok, State}; - %% ^^^ from this point down every command requires authentication ^^^ ftp_command(_, Socket, State=#connection_state{authenticated_state=unauthenticated}, _, _, _) -> @@ -366,35 +546,35 @@ ftp_command(Mod, Socket, State, pwd, _, _) -> respond(Socket, 257, "\"" ++ Mod:current_directory(State) ++ "\""), {ok, State}; -ftp_command(Mod, Socket, State, cdup, _Options, _) -> - ftp_command(Mod, Socket, State, cwd, _Options, ".."); +ftp_command(Mod, Socket, State, cdup, Options, _) -> + ftp_command(Mod, Socket, State, cwd, Options, ".."); ftp_command(Mod, Socket, State, cwd, _, Arg) -> - case Mod:change_directory(State, Arg) of + 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, _} -> - respond(Socket, 550, "Unable to change directory"), - {ok, State} + {error, Reason, NewState} -> + respond(Socket, 550, format_error("Unable to change directory", Reason)), + {ok, NewState} end; ftp_command(Mod, Socket, State, mkd, _, Arg) -> - case Mod:make_directory(State, Arg) of + case ftp_result(State, Mod:make_directory(State, Arg)) of {ok, NewState} -> respond(Socket, 250, "\"" ++ Arg ++ "\" directory created."), {ok, NewState}; - {error, _} -> - respond(Socket, 550, "Unable to create directory"), - {ok, State} + {error, Reason, NewState} -> + respond(Socket, 550, format_error("Unable to create directory", Reason)), + {ok, NewState} end; ftp_command(Mod, Socket, State, nlst, Options, Arg) -> - case Mod:list_files(State, Options, Arg) of - {error, NewState} -> - respond(Socket, 451), + case ftp_result(State, Mod:list_files(State, Options, Arg)) of + {error, Reason, NewState} -> + respond(Socket, 451, format_error("Unable to list", Reason)), {ok, NewState}; - Files -> + Files when is_list(Files)-> DataSocket = data_connection(Socket, State), list_file_names_to_socket(DataSocket, Files), respond(Socket, 226), @@ -403,11 +583,11 @@ ftp_command(Mod, Socket, State, nlst, Options, Arg) -> end; ftp_command(Mod, Socket, State, list, Options, Arg) -> - case Mod:list_files(State, Options, Arg) of - {error, _} -> - respond(Socket, 451), - {ok, State}; - Files -> + case ftp_result(State, Mod:list_files(State, Options, Arg)) of + {error, Reason, NewState} -> + respond(Socket, 451, format_error("Unable to list", Reason)), + {ok, NewState}; + Files when is_list(Files)-> DataSocket = data_connection(Socket, State), list_files_to_socket(DataSocket, Files), respond(Socket, 226), @@ -416,13 +596,13 @@ ftp_command(Mod, Socket, State, list, Options, Arg) -> end; ftp_command(Mod, Socket, State, rmd, _, Arg) -> - case Mod:remove_directory(State, Arg) of + case ftp_result(State, Mod:remove_directory(State, Arg)) of {ok, NewState} -> respond(Socket, 200), {ok, NewState}; - {error, _} -> - respond(Socket, 550), - {ok, State} + {error, Reason, NewState} -> + respond(Socket, 550, format_error(550, Reason)), + {ok, NewState} end; ftp_command(_, Socket, State, syst, _, _) -> @@ -430,13 +610,13 @@ ftp_command(_, Socket, State, syst, _, _) -> {ok, State}; ftp_command(Mod, Socket, State, dele, _, Arg) -> - case Mod:remove_file(State, Arg) of + case ftp_result(State, Mod:remove_file(State, Arg)) of {ok, NewState} -> - respond(Socket, 200), + respond(Socket, 250), % see RFC 959 {ok, NewState}; - {error, _} -> - respond(Socket, 450), - {ok, State} + {error, Reason, NewState} -> + respond(Socket, 450, format_error("Unable to delete file", Reason)), + {ok, NewState} end; ftp_command(Mod, Socket, State, stor, _, Arg) -> @@ -449,13 +629,13 @@ ftp_command(Mod, Socket, State, stor, _, Arg) -> done end end, - RetState = case Mod:put_file(State, Arg, write, Fun) of + RetState = case ftp_result(State, Mod:put_file(State, Arg, write, Fun)) of {ok, NewState} -> respond(Socket, 226), NewState; - {error, Info} -> - respond(Socket, 451, io_lib:format("Error ~p when storing a file.", [Info])), - State + {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}; @@ -473,39 +653,41 @@ ftp_command(_, Socket, State, type, _, Arg) -> ftp_command(Mod, Socket, State, site, _, Arg) -> [Command | Sargs] = string:tokens(Arg, " "), - case Mod:site_command(State, list_to_atom(string:to_lower(Command)), string:join(Sargs, " ")) of + case ftp_result(State, Mod:site_command(State, list_to_atom(string:to_lower(Command)), string:join(Sargs, " "))) of {ok, NewState} -> respond(Socket, 200), {ok, NewState}; - {error, not_found} -> + {error, not_found, NewState} -> respond(Socket, 500), - {ok, State}; - {error, _} -> - respond(Socket, 501, "Error completing command."), - {ok, State} + {ok, NewState}; + {error, Reason, NewState} -> + respond(Socket, 501, format("Error completing command (~p).", [Reason])), + {ok, NewState} end; ftp_command(Mod, Socket, State, site_help, _, _) -> - case Mod:site_help(State) of + case ftp_result(State, Mod:site_help(State)) of + {error, Reason, NewState} -> + respond(Socket, 500, format_error("Unable to help site", Reason)), + {ok, NewState}; {ok, []} -> - respond(Socket, 500); - {error, _} -> - respond(Socket, 500); + respond(Socket, 500), + {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") - end, - {ok, State}; + respond(Socket, 214, "Help OK"), + {ok, State} + end; 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} @@ -513,33 +695,44 @@ ftp_command(Mod, Socket, State, help, _, Arg) -> ftp_command(Mod, Socket, State, retr, _, Arg) -> try - case Mod:get_file(State, Arg) of - {ok, Fun} -> - DataSocket = data_connection(Socket, State), - {ok, NewState} = write_fun(DataSocket, Fun), - respond(Socket, 226), - bf_close(DataSocket), - {ok, NewState}; - error -> - respond(Socket, 550), - {ok, State} + 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), + 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 - _ -> - respond(Socket, 550), - {ok, State} - end; + catch + 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) -> - case Mod:file_info(State, Arg) of + case ftp_result(State, Mod:file_info(State, Arg)) of {ok, FileInfo} -> - respond(Socket, - 213, - format_mdtm_date(FileInfo#file_info.mtime)); - _ -> - respond(Socket, 550) - end, - {ok, State}; + respond(Socket, 213, format_mdtm_date(FileInfo#file_info.mtime)), + {ok, State}; + {error, Reason, NewState} -> + respond(Socket, 550, format_error(550, Reason)), + {ok, NewState} + end; ftp_command(_, Socket, State, rnfr, _, Arg) -> respond(Socket, 350, "Ready for RNTO."), @@ -551,10 +744,10 @@ ftp_command(Mod, Socket, State, rnto, _, Arg) -> respond(Socket, 503, "RNFR not specified."), {ok, State}; Rnfr -> - case Mod:rename_file(State, Rnfr, Arg) of - {error, _} -> - respond(Socket, 550), - {ok, State}; + case ftp_result(State, Mod:rename_file(State, Rnfr, Arg)) of + {error, Reason, NewState} -> + respond(Socket, 550, io_lib:format("Unable to rename (~p).", [Reason])), + {ok, NewState}; {ok, NewState} -> respond(Socket, 250, "Rename successful."), {ok, NewState#connection_state{rnfr=undefined}} @@ -576,26 +769,6 @@ ftp_command(Mod, Socket, State, xpwd, Options, Arg) -> ftp_command(Mod, Socket, State, xrmd, Options, Arg) -> ftp_command(Mod, Socket, State, rmd, Options, Arg); -ftp_command(_Mod, Socket, State, feat, _, _Arg) -> - respond_raw(Socket, "211-Features"), - case State#connection_state.utf8 of - true -> - respond_raw(Socket, " UTF8"); - _ -> - ok - end, - 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"); - _ -> - respond(Socket, 501) - end, - {ok, State}; - ftp_command(_Mod, Socket, State, size, _, _Arg) -> respond(Socket, 550), {ok, State}; @@ -605,13 +778,15 @@ ftp_command(_, Socket, State, Command, _, _Arg) -> respond(Socket, 500), {ok, State}. -write_fun(Socket, Fun) -> - case Fun(1024) of +write_fun(SendBlockSize,Socket, Fun) -> + case Fun(SendBlockSize) of {ok, Bytes, NextFun} -> bf_send(Socket, Bytes), - write_fun(Socket, NextFun); + write_fun(SendBlockSize, Socket, NextFun); {done, NewState} -> - {ok, NewState} + {ok, NewState}; + Another -> % errors and etc + Another end. strip_newlines(S) -> @@ -631,18 +806,19 @@ parse_input(Input) -> {OptsAcc, [strip_newlines(Item)|ArgsAcc]} end, {Options, Args} = lists:foldl(Fun, {[], []}, Other), - {list_to_atom(string:to_lower(strip_newlines(Command))), Options, string:join(Args, " ")}. - + {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, - ucs2_to_utf8(file_info_to_string(Info)) ++ "\r\n") end, + to_utf8(file_info_to_string(Info)) ++ "\r\n") end, Files), ok. list_file_names_to_socket(DataSocket, Files) -> lists:map(fun(Info) -> - bf_send(DataSocket, ucs2_to_utf8(Info#file_info.name)++"\r\n") end, + bf_send(DataSocket, + to_utf8(Info#file_info.name) ++ "\r\n") end, Files), ok. @@ -653,48 +829,48 @@ bf_close({SockMod, Socket}) -> SockMod:close(Socket). bf_recv({SockMod, Socket}) -> - SockMod:recv(Socket, 0). + 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(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(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."; -response_code_string(227) -> "Entering Passive Mode (h1,h2,h3,h4,p1,p2)."; -response_code_string(230) -> "User logged in, proceed."; -response_code_string(250) -> "Requested file action okay, completed."; -response_code_string(257) -> "PATHNAME created."; -response_code_string(331) -> "User name okay, need password."; -response_code_string(332) -> "Need account for login."; -response_code_string(350) -> "Requested file action pending further information."; -response_code_string(421) -> "Service not available, closing control connection."; -response_code_string(425) -> "Can't open data connection."; -response_code_string(426) -> "Connection closed; transfere aborted."; -response_code_string(450) -> "Requested file action not taken."; -response_code_string(451) -> "Requested action not taken: local error in processing."; -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(226) -> "Closing data connection"; +response_code_string(227) -> "Entering Passive Mode (h1,h2,h3,h4,p1,p2)"; +response_code_string(230) -> "User logged in, proceed"; +response_code_string(250) -> "Requested file action okay, completed"; +response_code_string(257) -> "PATHNAME created"; +response_code_string(331) -> "User name okay, need password"; +response_code_string(332) -> "Need account for login"; +response_code_string(350) -> "Requested file action pending further information"; +response_code_string(421) -> "Service not available, closing control connection"; +response_code_string(425) -> "Can't open data connection"; +response_code_string(426) -> "Connection closed; transfere aborted"; +response_code_string(450) -> "Requested file action not taken"; +response_code_string(451) -> "Requested action not taken: local error in processing"; +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 @@ -803,6 +979,52 @@ format_port(PortNumber) -> [A,B] = binary_to_list(<>), {A, B}. +%------------------------------------------------------------------------------- +-spec format(string(), list()) -> string(). +format(FormatString, Args) -> + 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(Message, undef) -> + 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]) ++ ".". + +%------------------------------------------------------------------------------- +from_utf8(String, true) -> + unicode:characters_to_list(erlang:list_to_binary(String), utf8); + +from_utf8(String, false) -> + String. + +%------------------------------------------------------------------------------- +to_utf8(String) -> + to_utf8(String, true). + +to_utf8(String, true) -> + 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]. + + -ifdef(TEST). %% EUNIT TESTS %% @@ -810,22 +1032,27 @@ format_port(PortNumber) -> %% Testing Utility Functions setup() -> - meck:new(gen_tcp, [unstick]), - meck:new(inet, [unstick, passthrough]), - meck:new(fake_server, [non_strict]). - + 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 - go -> - control_loop(ListenerPid, - {gen_tcp, socket}, - #connection_state{module=fake_server,ip_address={127,0,0,1}}), + {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), - meck:unload(fake_server), - meck:unload(inet), - meck:unload(gen_tcp) - end. + 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() -> @@ -836,11 +1063,11 @@ execute(ListenerPid) -> script_dialog([]) -> meck:expect(gen_tcp, recv, - fun(_, _) -> {error, closed} end); + fun(_, _, infinity) -> {error, closed} end); script_dialog([{Request, Response} | Rest]) -> meck:expect(gen_tcp, recv, - fun(Socket, _) -> + fun(Socket, _, infinity) -> script_dialog([{resp, Socket, Response}] ++ Rest), {ok, Request} end); @@ -860,10 +1087,26 @@ script_dialog([{resp_bin, Socket, Response} | Rest]) -> ?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, _) -> + fun(S, _, infinity) -> ?assertEqual(S, Socket), script_dialog(Rest), {ok, Request} @@ -871,16 +1114,18 @@ script_dialog([{req, Socket, Request} | Rest]) -> %% executes the next step in the test script step(Pid) -> - Pid ! {ack, self()}, + Pid ! {ack, self()}, % 1st ACK will be 'eaten' by execute + % so valid sequence will be receive - {new_state, _, State} -> - State; + {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()}. @@ -891,7 +1136,7 @@ strip_newlines_test() -> "testing again" = strip_newlines("testing again"). parse_input_test() -> - {test, [], "3 2 1"} = parse_input("TEST 1 2 3"), + {test, [], "1 2 3"} = parse_input("TEST 1 2 3"), {test, [], ""} = parse_input("Test\r\n"), {test, [], "awesome"} = parse_input("Test awesome\r\n"). @@ -910,11 +1155,51 @@ 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})), -%% Functional/Integration Tests + ?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)), -authenticate_state(State) -> - State#connection_state{authenticated_state=authenticated}. + ?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/Integration Tests login_test_user(SocketPid) -> login_test_user(SocketPid, []). @@ -924,22 +1209,17 @@ login_test_user(SocketPid, Script) -> {resp, socket, "331 User name okay, need password.\r\n"}, {req, socket, "PASS meatmeat"}, {resp, socket, "230 User logged in, proceed.\r\n"}] ++ Script), - SocketPid ! go, - receive - {new_state, _, _} -> - ok - end, - SocketPid ! {ack, self()}, - - meck:expect(fake_server, - login, - fun(St, "meat", "meatmeat") -> - {true, authenticate_state(St)} - end), - receive - {new_state, _, _} -> - ok - end. + 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(), @@ -947,7 +1227,6 @@ authenticate_successful_test() -> Child = spawn_link( fun() -> login_test_user(ControlPid), - ControlPid ! {ack, self()}, finish(ControlPid) end), execute(Child). @@ -961,13 +1240,37 @@ authenticate_failure_test() -> {"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), - ControlPid ! go, - ?assertMatch(#connection_state{authenticated_state=unauthenticated}, step(ControlPid)), + 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(), @@ -975,14 +1278,41 @@ unauthenticated_test() -> fun() -> script_dialog([ {"CWD /hamster", "530 Not logged in.\r\n"}, {"MKD /unicorns", "530 Not logged in.\r\n"}]), - - ControlPid ! go, - ?assertMatch(#connection_state{authenticated_state=unauthenticated}, step(ControlPid)), - ?assertMatch(#connection_state{authenticated_state=unauthenticated}, step(ControlPid)), + {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), + 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(), @@ -995,7 +1325,7 @@ mkdir_test() -> 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"}]), + {"MKD test_dir_2", "550 Unable to create directory.\r\n"}]), step(ControlPid), meck:expect(fake_server, @@ -1023,8 +1353,9 @@ cwd_test() -> 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/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), @@ -1034,6 +1365,13 @@ cwd_test() -> {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). @@ -1050,9 +1388,9 @@ cdup_test() -> 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"}]), + [{"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, @@ -1087,33 +1425,78 @@ pwd_test() -> end), execute(Child). -login_test_user_with_data_socket(ControlPid, Script, passive) -> - meck:expect(gen_tcp, - listen, - fun(0, _) -> - {ok, listen_socket} - end), +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). - meck:expect(gen_tcp, - accept, - fun(listen_socket) -> - {ok, data_socket} - end), +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). - meck:expect(inet, - sockname, - fun(listen_socket) -> - {ok, {{127, 0, 0, 1}, 2000}} - end), +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), - ControlPid ! {ack, self()}, - receive - {new_state, _, #connection_state{pasv_listen={passive, listen_socket, {{127,0,0,1}, 2000}}}} -> - ok; - _ -> - ?assert(bad_value) - end; + ?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, @@ -1122,25 +1505,22 @@ login_test_user_with_data_socket(ControlPid, Script, active) -> {ok, data_socket} end), login_test_user(ControlPid, [{"PORT 127,0,0,1,7,208", "200 Command okay.\r\n"}] ++ Script), - ControlPid ! {ack, self()}, - receive - {new_state, _, #connection_state{data_port={active, {127,0,0,1}, 2000}}} -> - ok - end. + ?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(_, _) -> + fun(_, _, _) -> [#file_info{type=file, name="edward"}, #file_info{type=dir, name="Aethelred"}] end), ControlPid = self(), Child = spawn_link( fun() -> - meck:expect(gen_tcp, close, fun(data_socket) -> ok 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, [{"NLST", "150 File status okay; about to open data connection.\r\n"}, {resp, data_socket, "edward\r\n"}, @@ -1157,7 +1537,7 @@ list_test(Mode) -> setup(), meck:expect(fake_server, list_files, - fun(_, _) -> + fun(_, _, _) -> [#file_info{type=file, name="edward", mode=511, @@ -1181,11 +1561,10 @@ list_test(Mode) -> {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"}], - meck:expect(gen_tcp, close, fun(data_socket) -> ok 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), + login_test_user_with_data_socket(ControlPid, Script, Mode), step(ControlPid), finish(ControlPid) end), @@ -1206,7 +1585,7 @@ remove_directory_test() -> {"RMD /bison/burgers", "550 Requested action not taken.\r\n"}]), step(ControlPid), - meck:expect(fake_server, + ok = meck:expect(fake_server, remove_directory, fun(_, "/bison/burgers") -> {error, error} @@ -1228,8 +1607,8 @@ remove_file_test() -> {ok, St} end), - login_test_user(ControlPid, [{"DELE cheese.txt", "200 Command okay.\r\n"}, - {"DELE cheese.txt", "450 Requested file action not taken.\r\n"}]), + 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, @@ -1247,15 +1626,18 @@ remove_file_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 bologna.txt", "150 File status okay; about to open data connection.\r\n"}, + 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"} + {resp, socket, "226 Closing data connection.\r\n"}, + {"PWD", "257 \"/\"\r\n"} ], meck:expect(fake_server, put_file, - fun(S, "bologna.txt", write, F) -> + fun(S, "file.txt", write, F) -> {ok, Data, DataSize} = F(), BinData = <<"SOME DATA HERE">>, ?assertEqual(Data, BinData), @@ -1263,32 +1645,169 @@ stor_test(Mode) -> {ok, S} end), - meck:expect(gen_tcp, close, fun(data_socket) -> ok 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), + 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 = [{"STOR bologna.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"} - ], + 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, - put_file, - fun(_, "bologna.txt", write, F) -> - F(), - {error, access_denied} + 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), - meck:expect(gen_tcp, close, fun(data_socket) -> ok 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), @@ -1296,17 +1815,18 @@ stor_failure_test(Mode) -> end), execute(Child). -?dataSocketTest(retr_test). -retr_test(Mode) -> +?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, data_socket, "SOME MORE DATA"}, - {resp, socket, "226 Closing data connection.\r\n"}], - meck:expect(fake_server, + {resp, socket, "451 Unable to get file (Disk error).\r\n"}], + meck:expect(fake_server, get_file, fun(State, "bologna.txt") -> {ok, @@ -1314,14 +1834,14 @@ retr_test(Mode) -> {ok, list_to_binary("SOME DATA HERE"), fun(1024) -> - {ok, - list_to_binary("SOME MORE DATA"), - fun(1024) -> {done, State} end} + {error, "Disk error", State} end} end} end), - meck:expect(gen_tcp, close, fun(data_socket) -> ok 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) @@ -1486,7 +2006,7 @@ quit_test() -> fun () -> meck:expect(fake_server, disconnect, - fun(_) -> + fun(_, exit) -> ok end), login_test_user(ControlPid, @@ -1501,15 +2021,14 @@ feat_test() -> setup(), ControlPid = self(), Child = spawn_link( - fun() -> - login_test_user(ControlPid, - [{"FEAT", "211- Extensions supported:\r\n"}, - {resp, socket, " UTF8\r\n" }, - {resp, socket, "211 End\r\n" }]), - step(ControlPid), - finish(ControlPid) - end - ), + 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). @@ -1517,62 +2036,66 @@ utf8_success_test(Mode) -> setup(), ControlPid = self(), Child = spawn_link( - fun() -> - FileName = "Молоко-Яйки", % milk-eggs - UtfFileName = ucs2_to_utf8(FileName), % milk-eggs - FileContent = <<"SOME DATA HERE">>, - - Script =[{"PWD " ++ UtfFileName, "257 \""++ UtfFileName ++"\"\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, FileContent }, - {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"}, - {"STOR " ++ UtfFileName, "150 File status okay; about to open data connection.\r\n"} - ], - - meck:expect(fake_server, - current_directory, - fun(_) -> FileName end), - - login_test_user_with_data_socket(ControlPid, Script, Mode), - 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, FileContent), - ?assertEqual(DataSize, size(FileContent)), - {ok, S} - end), - - meck:expect(gen_tcp, close, fun(data_socket) -> ok end), - - step(ControlPid), - - 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), - - meck:expect(gen_tcp, close, fun(data_socket) -> ok end), - - step(ControlPid), - finish(ControlPid) - end), + 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). @@ -1580,19 +2103,23 @@ utf8_failure_test(Mode) -> setup(), ControlPid = self(), Child = spawn_link( - fun() -> - FileName = "Молоко-Яйки", % milk-eggs - UtfFileNameOk = ucs2_to_utf8(FileName), % milk-eggs - { UtfFileName, _ } = lists:split(length(UtfFileNameOk)-1, UtfFileNameOk), - - Script =[{"CWD " ++ UtfFileName, "501 Syntax error in parameters or arguments.\r\n"}], - - meck:expect(gen_tcp, close, fun(data_socket) -> ok end), - - login_test_user_with_data_socket(ControlPid, Script, Mode), - step(ControlPid), - finish(ControlPid) - end), + 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({bifrost, incomplete_utf8, _}) -> ok end), + + login_test_user_with_data_socket(ControlPid, Script, Mode), + step(ControlPid), + step(ControlPid), + finish(ControlPid) + end), execute(Child). + -endif. diff --git a/src/gen_bifrost_server.erl b/src/gen_bifrost_server.erl index 602bdf4..50e6611 100644 --- a/src/gen_bifrost_server.erl +++ b/src/gen_bifrost_server.erl @@ -8,24 +8,49 @@ -export([behaviour_info/1]). behaviour_info(callbacks) -> - % Path :: String - % State Change :: {ok, State} OR {error, State} + % 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} - {current_directory, 1}, % State -> Path - {make_directory, 2}, % State, Path -> State Change - {change_directory, 2}, % State, Path -> State Change - {list_files, 3}, % State, Options, Path -> [FileInfo] OR {error, State} - {remove_directory, 2}, % State, Path -> State Change - {remove_file, 2}, % State, Path -> State Change - {put_file, 4}, % State, File Name, (append OR write), Fun(Byte Count) -> State Change - {get_file, 2}, % State, Path -> {ok, Fun(Byte Count)} OR error - {file_info, 2}, % State, Path -> {ok, FileInfo} OR {error, ErrorCause} - {rename_file, 3}, % State, From Path, To Path -> State Change - {site_command, 3}, % State, Command Name String, Command Args String -> State Change - {site_help, 1}, % State -> {ok, [HelpInfo]} OR {error, State} - {disconnect, 1}]; % State -> State Change + {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, 2}, % State, 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 + behaviour_info(_) -> undefined. From cb79225914148e3a7b5487758af9280df8ffe8b1 Mon Sep 17 00:00:00 2001 From: Dmitrii Zolotarev Date: Wed, 11 Mar 2015 15:33:21 +0600 Subject: [PATCH 15/27] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE?= =?UTF-8?q?=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bifrost.erl | 36 ++++++++++++++++++------------------ src/gen_bifrost_server.erl | 2 +- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/bifrost.erl b/src/bifrost.erl index 2a55da8..f9eed87 100644 --- a/src/bifrost.erl +++ b/src/bifrost.erl @@ -73,13 +73,13 @@ init([HookModule, Opts]) -> recv_block_size = RecvBlockSize, send_block_size = SendBlockSize, control_timeout = ControlTimeout, - port_range = PortRange}, + 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, + await_connections, [Listen, Supervisor]), {ok, {listen_socket, Listen}}; {error, Error} -> @@ -273,8 +273,8 @@ respond_raw({SocketMod, Socket}, Line) -> respond_feature(Socket, Name, true) -> 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}, @@ -348,7 +348,7 @@ 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 @@ -412,8 +412,8 @@ ftp_result(_State, Data) -> ftp_result(State, Data, UserFunction) -> ftp_result(State, UserFunction(State, Data)). - - + + %%------------------------------------------------------------------------------- %% FTP COMMANDS ftp_command(Socket, State, Command, Options, RawArg) -> @@ -786,7 +786,7 @@ write_fun(SendBlockSize,Socket, Fun) -> {done, NewState} -> {ok, NewState}; Another -> % errors and etc - Another + Another end. strip_newlines(S) -> @@ -807,7 +807,7 @@ parse_input(Input) -> 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, @@ -817,7 +817,7 @@ list_files_to_socket(DataSocket, Files) -> list_file_names_to_socket(DataSocket, Files) -> lists:map(fun(Info) -> - bf_send(DataSocket, + bf_send(DataSocket, to_utf8(Info#file_info.name) ++ "\r\n") end, Files), ok. @@ -1024,7 +1024,7 @@ to_utf8(String, true) -> to_utf8(String, false) -> [if C > 255 orelse C<0 -> $?; true -> C end || C <- String]. - + -ifdef(TEST). %% EUNIT TESTS %% @@ -1039,7 +1039,7 @@ setup() -> 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 @@ -1198,7 +1198,7 @@ ftp_result_test() -> ?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/Integration Tests login_test_user(SocketPid) -> @@ -1270,7 +1270,7 @@ requirements_failure_test() -> end), execute(Child). - + unauthenticated_test() -> setup(), ControlPid = self(), @@ -1312,7 +1312,7 @@ ssl_only_test() -> finish(ControlPid) end), execute(Child). - + mkdir_test() -> setup(), ControlPid = self(), @@ -1627,7 +1627,7 @@ stor_test(Mode) -> setup(), ControlPid = self(), ok = meck:expect(fake_server, init, fun(InitialState, _Opt) -> - InitialState#connection_state{recv_block_size = 1024*1024} end), + 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"}, @@ -1748,7 +1748,7 @@ stor_user_failure_test(Mode) -> finish(ControlPid) end), execute(Child). - + ?dataSocketTest(stor_failure_test). stor_failure_test(Mode) -> setup(), @@ -1780,7 +1780,7 @@ stor_failure_test(Mode) -> execute(Child). ?dataSocketTest(retr_test). -retr_test(Mode) -> +retr_test(Mode) -> setup(), ok = meck:expect(fake_server, init, fun(InitialState, _Opt) -> InitialState#connection_state{send_block_size=1024*1024} end), diff --git a/src/gen_bifrost_server.erl b/src/gen_bifrost_server.erl index 50e6611..5522813 100644 --- a/src/gen_bifrost_server.erl +++ b/src/gen_bifrost_server.erl @@ -34,7 +34,7 @@ behaviour_info(callbacks) -> {current_directory, 1}, % State -> Path {make_directory, 2}, % State, Path -> StateChange {change_directory, 2}, % State, Path -> StateChange - {list_files, 2}, % State, Path -> [FileInfo] | StateChangeError + {list_files, 3}, % State, Options, Path -> [FileInfo] | StateChangeError {remove_directory, 2}, % State, Path -> StateChange {remove_file, 2}, % State, Path -> StateChange From 6dad07a8d8eca89f568cb24b50f71d72a4a95612 Mon Sep 17 00:00:00 2001 From: Dmitrii Zolotarev Date: Wed, 11 Mar 2015 15:53:30 +0600 Subject: [PATCH 16/27] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D1=81=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B2=20=D0=BE=D1=82?= =?UTF-8?q?=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20=D0=BC=D0=BE=D0=B4?= =?UTF-8?q?=D1=83=D0=BB=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bifrost.erl | 1121 +--------------------------------------- test/bifrost_tests.erl | 1111 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 1129 insertions(+), 1103 deletions(-) create mode 100644 test/bifrost_tests.erl diff --git a/src/bifrost.erl b/src/bifrost.erl index f9eed87..37eba86 100644 --- a/src/bifrost.erl +++ b/src/bifrost.erl @@ -8,12 +8,27 @@ -behaviour(gen_server). -include("bifrost.hrl"). --include_lib("eunit/include/eunit.hrl"). --export([start_link/2, establish_control_connection/2, await_connections/2, 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). @@ -1023,1103 +1038,3 @@ to_utf8(String, true) -> to_utf8(String, false) -> [if C > 255 orelse C<0 -> $?; true -> C end || C <- String]. - - --ifdef(TEST). - -%% 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} -> - 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" = 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/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 = 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({bifrost, incomplete_utf8, _}) -> ok end), - - login_test_user_with_data_socket(ControlPid, Script, Mode), - step(ControlPid), - step(ControlPid), - finish(ControlPid) - end), - execute(Child). - - --endif. 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 From f0526b86459a1b9a5a929124a6387f2da981376e Mon Sep 17 00:00:00 2001 From: Dmitrii Zolotarev Date: Thu, 12 Mar 2015 10:16:15 +0600 Subject: [PATCH 17/27] =?UTF-8?q?=D0=A0=D0=B5=D1=88=D0=B5=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B1=D0=BB=D0=B5=D0=BC=D0=B0,=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=B3=D0=B4=D0=B0=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=20=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA?= =?UTF-8?q?=D0=B5=20FTP-=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D0=B0=20?= =?UTF-8?q?=D0=B2=D1=8B=D0=B7=D1=8B=D0=B2=D0=B0=D0=BB=D0=B0=20=D0=BA=D1=80?= =?UTF-8?q?=D0=B0=D1=85=20=D1=8F=D0=B4=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bifrost.erl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/bifrost.erl b/src/bifrost.erl index 37eba86..70936f7 100644 --- a/src/bifrost.erl +++ b/src/bifrost.erl @@ -10,7 +10,7 @@ -include("bifrost.hrl"). -ifdef(TEST). --compile([export_all]). +-compile(export_all). -else. -export([ start_link/2, @@ -98,15 +98,16 @@ init([HookModule, Opts]) -> [Listen, Supervisor]), {ok, {listen_socket, Listen}}; {error, Error} -> - {stop, Error} + error_logger:error_report({bifrost, init_error, Error}), + ignore end catch _Type0:{stop, Reason} -> error_logger:error_report({bifrost, init_error, Reason}), - {stop, Reason}; + ignore; _Type1:Exception -> error_logger:error_report({bifrost, init_exception, Exception}), - {stop, Exception} + ignore end. %------------------------------------------------------------------------------- From d4c90ae8cdbc0678a4ff5aa1d742310fbd944e8c Mon Sep 17 00:00:00 2001 From: Dmitrii Zolotarev Date: Fri, 13 Mar 2015 17:29:07 +0600 Subject: [PATCH 18/27] =?UTF-8?q?refs=20#41442=20=D0=9F=D0=BE=D0=B4=D0=B4?= =?UTF-8?q?=D0=B5=D1=80=D0=B6=D0=BA=D0=B0=20=D0=B0=D0=BB=D0=B0=D1=80=D0=BC?= =?UTF-8?q?=D0=BE=D0=B2,=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=82=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BE=D0=B4?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bifrost.erl | 4 ++ src/gen_bifrost_server.erl | 90 +++++++++++++++++++------------------- 2 files changed, 50 insertions(+), 44 deletions(-) diff --git a/src/bifrost.erl b/src/bifrost.erl index 70936f7..8af28d3 100644 --- a/src/bifrost.erl +++ b/src/bifrost.erl @@ -96,17 +96,21 @@ init([HookModule, 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. diff --git a/src/gen_bifrost_server.erl b/src/gen_bifrost_server.erl index 5522813..9c8a889 100644 --- a/src/gen_bifrost_server.erl +++ b/src/gen_bifrost_server.erl @@ -8,49 +8,51 @@ -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 - + %% 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. From 347d715c423523d2684f34d54745144ed929eeae Mon Sep 17 00:00:00 2001 From: Dmitrii Zolotarev Date: Fri, 24 Apr 2015 10:59:53 +0600 Subject: [PATCH 19/27] =?UTF-8?q?refs=20#43234=20=D0=94=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE?= =?UTF-8?q?=D0=B6=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=B7=D0=B0=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=BE=D0=BF=D1=80=D0=B5=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=81=D0=B5=D1=82=D0=B5?= =?UTF-8?q?=D0=B2=D0=BE=D0=B3=D0=BE=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84?= =?UTF-8?q?=D0=B5=D0=B9=D1=81=D0=B0,=20=D0=BD=D0=B0=20=D0=BA=D0=BE=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=BE=D0=BC=20=D0=B1=D1=83=D0=B4=D0=B5=D1=82=20?= =?UTF-8?q?=D1=81=D0=BB=D1=83=D1=88=D0=B0=D1=82=D1=8C=20FTP-=D1=81=D0=B5?= =?UTF-8?q?=D1=80=D0=B2=D0=B5=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bifrost.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bifrost.erl b/src/bifrost.erl index 8af28d3..f84ec30 100644 --- a/src/bifrost.erl +++ b/src/bifrost.erl @@ -76,9 +76,9 @@ init([HookModule, Opts]) -> _AnotherValue -> throw({stop, lists:flatten(io_lib:format("Invalid port_range ~p", [_AnotherValue]))}) end, - case listen_socket(Port, [{active, false}, {reuseaddr, true}, list]) of + IpAddress = proplists:get_value(ip_address, Opts, {0,0,0,0}), + case listen_socket(Port, [{active, false}, {reuseaddr, true}, list, {ip, IpAddress}]) 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, From 58d7280c74061e1b242f14b76b213e841c45f8be Mon Sep 17 00:00:00 2001 From: Anton N Ryabkov Date: Wed, 1 Jul 2015 14:41:25 +0600 Subject: [PATCH 20/27] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20gitignore.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d4522a0..585643a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .eunit/* +.rebar/* deps/* ebin/* sample/ebin/* From 0b6530baa93564b2ef74fe97d8d8b4442ffc9a3b Mon Sep 17 00:00:00 2001 From: Anton N Ryabkov Date: Wed, 9 Sep 2015 13:30:47 +0600 Subject: [PATCH 21/27] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D1=80=D1=8B:=20establish=5Factive=5Fconnection=5Ftimeout=20-?= =?UTF-8?q?=20=D0=B2=D1=80=D0=B5=D0=BC=D1=8F=20=D0=BD=D0=B0=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BF=D1=8B=D1=82=D0=BA=D1=83=20=D1=83=D1=81=D1=82=D0=B0=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BA=D0=BE=D0=BD=D0=BD=D0=B5?= =?UTF-8?q?=D0=BA=D1=86=D0=B8=D1=8E=20=D1=81=20ftp-=D0=BA=D0=BB=D0=B8?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=BE=D0=B2.=20establish=5Fpassive=5Fconnect?= =?UTF-8?q?ion=5Ftimeout=20-=20=D0=B2=D1=80=D0=B5=D0=BC=D1=8F=20=D0=BE?= =?UTF-8?q?=D0=B6=D0=B8=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F=20(ms),=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BA=D0=B0=20ftp-=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=20=D1=83=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D0=B8=D1=82=20?= =?UTF-8?q?=D1=81=20=D0=BD=D0=B0=D0=BC=D0=B8=20=D1=81=D0=BE=D0=B5=D0=B4?= =?UTF-8?q?=D0=B8=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B2=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B6=D0=B8=D0=BC=D0=B5=20passive=20(refs=20#48887).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit По умолчанию значение для каждого из параметров сделаны 60 сек. До этого было значение infinity, что приводило к учетки ресурсов. --- include/bifrost.hrl | 2 ++ src/bifrost.erl | 32 ++++++++++++++++++++++++++------ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/include/bifrost.hrl b/include/bifrost.hrl index 8e079be..c054bc7 100644 --- a/include/bifrost.hrl +++ b/include/bifrost.hrl @@ -25,6 +25,8 @@ 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} diff --git a/src/bifrost.erl b/src/bifrost.erl index f84ec30..b87421f 100644 --- a/src/bifrost.erl +++ b/src/bifrost.erl @@ -73,10 +73,28 @@ init([HookModule, Opts]) -> {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]))}) + _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, @@ -88,6 +106,8 @@ init([HookModule, Opts]) -> 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, @@ -329,12 +349,12 @@ data_connection(ControlSocket, State) -> end. %% passive -- accepts an inbound connection -establish_data_connection(#connection_state{pasv_listen={passive, Listen, _}}) -> - gen_tcp:accept(Listen); +establish_data_connection(#connection_state{pasv_listen={passive, Listen, _}, establish_passive_connection_timeout=Timeout}) -> + gen_tcp:accept(Listen, Timeout); %% active -- establishes an outbound connection -establish_data_connection(#connection_state{data_port={active, Addr, Port}}) -> - gen_tcp:connect(Addr, Port, [{active, false}, binary]). +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 From 6ba21b65009eb6243c5bd6884eac6663eec996d3 Mon Sep 17 00:00:00 2001 From: Andrey Teplyashin Date: Wed, 29 Jun 2016 21:54:05 +0600 Subject: [PATCH 22/27] Create release branches release_3_7_0 --- rebar.config | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/rebar.config b/rebar.config index f2ffbc5..834d2c1 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_7_0 %% -{deps, [ - {meck, ".*", {git, "git@git.eltex.loc:external/meck.git"}} -]}. +{cover_enabled,true}. +{erl_opts,[debug_info]}. +{deps,[{meck,".*", + {git,"git@git.eltex.loc:external/meck.git", + {branch,"release_3_7_0"}}}]}. +{clean_files,["*.eunit","ebin/*.beam"]}. -{clean_files, ["*.eunit", "ebin/*.beam"]}. From 77d4f30903b335eabad006bac7d2c3ab9240820c Mon Sep 17 00:00:00 2001 From: Andrey Teplyashin Date: Mon, 5 Sep 2016 21:20:45 +0700 Subject: [PATCH 23/27] Create release branches release_3_8_0 --- rebar.config | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rebar.config b/rebar.config index 834d2c1..6282c42 100644 --- a/rebar.config +++ b/rebar.config @@ -1,9 +1,9 @@ -%% THIS FILE IS GENERATED FOR release_3_7_0 %% +%% THIS FILE IS GENERATED FOR release_3_8_0 %% {cover_enabled,true}. {erl_opts,[debug_info]}. {deps,[{meck,".*", {git,"git@git.eltex.loc:external/meck.git", - {branch,"release_3_7_0"}}}]}. + {branch,"release_3_8_0"}}}]}. {clean_files,["*.eunit","ebin/*.beam"]}. From 3bea48c45cdc84f91e82281cb23386aad071d6ee Mon Sep 17 00:00:00 2001 From: "jenkins.ims" Date: Tue, 6 Dec 2016 09:54:29 +0700 Subject: [PATCH 24/27] Create release branches release_3_9_0 --- rebar.config | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rebar.config b/rebar.config index 6282c42..31df483 100644 --- a/rebar.config +++ b/rebar.config @@ -1,9 +1,9 @@ -%% THIS FILE IS GENERATED FOR release_3_8_0 %% +%% THIS FILE IS GENERATED FOR release_3_9_0 %% {cover_enabled,true}. {erl_opts,[debug_info]}. {deps,[{meck,".*", {git,"git@git.eltex.loc:external/meck.git", - {branch,"release_3_8_0"}}}]}. + {branch,"release_3_9_0"}}}]}. {clean_files,["*.eunit","ebin/*.beam"]}. From 89ecf93fa66fc3eda68f81516cdac3c9b4647a79 Mon Sep 17 00:00:00 2001 From: Anton N Ryabkov Date: Wed, 15 Mar 2017 09:32:54 +0700 Subject: [PATCH 25/27] =?UTF-8?q?=D0=A3=D0=B1=D1=80=D0=B0=D0=BD=D1=8B=20wa?= =?UTF-8?q?rning-=D0=B8=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=B8=D0=BB=D1=8F=D1=86?= =?UTF-8?q?=D0=B8=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bifrost.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bifrost.erl b/src/bifrost.erl index b87421f..d6cc96c 100644 --- a/src/bifrost.erl +++ b/src/bifrost.erl @@ -170,7 +170,7 @@ listen_socket({Start, End}, _TcpOpts, _NextPort) when End < Start -> 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); + 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 From 207213d7932467003029cdf1368094f127f90536 Mon Sep 17 00:00:00 2001 From: "jenkins.ims" Date: Fri, 11 Aug 2017 09:31:57 +0700 Subject: [PATCH 26/27] Create release branches release_3_10_0 --- rebar.config | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rebar.config b/rebar.config index 31df483..95371f0 100644 --- a/rebar.config +++ b/rebar.config @@ -1,9 +1,9 @@ -%% THIS FILE IS GENERATED FOR release_3_9_0 %% +%% THIS FILE IS GENERATED FOR release_3_10_0 %% {cover_enabled,true}. {erl_opts,[debug_info]}. {deps,[{meck,".*", {git,"git@git.eltex.loc:external/meck.git", - {branch,"release_3_9_0"}}}]}. + {branch,"release_3_10_0"}}}]}. {clean_files,["*.eunit","ebin/*.beam"]}. From 7587979091d9f042463f58d9b135ee66a07b7984 Mon Sep 17 00:00:00 2001 From: Anton Ryabkov Date: Thu, 12 Oct 2017 11:33:50 +0700 Subject: [PATCH 27/27] =?UTF-8?q?refs=20#87622=20=D0=9F=D0=BE=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BA=D0=BE=D0=BC=D0=B0?= =?UTF-8?q?=D0=BD=D0=B4=D0=B0=20SIZE=20(=D1=80=D0=B0=D0=BD=D1=8C=D1=88?= =?UTF-8?q?=D0=B5=20=D0=BE=D0=BD=D0=B0=20=D0=B2=D0=BE=D0=B7=D0=B2=D1=80?= =?UTF-8?q?=D0=B0=D1=89=D0=B0=D0=BB=D0=B0=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA?= =?UTF-8?q?=D1=83,=20=D0=B0=20=D1=82=D0=B5=D0=BF=D0=B5=D1=80=D1=8C=20?= =?UTF-8?q?=D0=B1=D1=83=D0=B4=D0=B5=D1=82=20=D0=B2=D0=BE=D0=B7=D0=B2=D1=80?= =?UTF-8?q?=D0=B0=D1=89=D0=B0=D1=82=D1=8C=200).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bifrost.erl | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/bifrost.erl b/src/bifrost.erl index d6cc96c..7fcfda3 100644 --- a/src/bifrost.erl +++ b/src/bifrost.erl @@ -809,9 +809,15 @@ ftp_command(Mod, Socket, State, xpwd, Options, 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({bifrost, unrecognized_command, Command}),