diff options
Diffstat (limited to 'lib/tftp/src/tftp_engine.erl')
-rw-r--r-- | lib/tftp/src/tftp_engine.erl | 1422 |
1 files changed, 1422 insertions, 0 deletions
diff --git a/lib/tftp/src/tftp_engine.erl b/lib/tftp/src/tftp_engine.erl new file mode 100644 index 0000000000..fb2c9749e5 --- /dev/null +++ b/lib/tftp/src/tftp_engine.erl @@ -0,0 +1,1422 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2005-2016. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% +%%------------------------------------------------------------------- +%% Protocol engine for trivial FTP +%%------------------------------------------------------------------- + +-module(tftp_engine). + +%%%------------------------------------------------------------------- +%%% Interface +%%%------------------------------------------------------------------- + +%% application internal functions +-export([ + daemon_start/1, + daemon_loop/1, + daemon_loop/3, %% Handle upgrade from old releases. Please, remove this function in next release. + client_start/4, + common_loop/6, + info/1, + change_config/2 + ]). + +%% module internal +-export([ + daemon_init/1, + server_init/2, + client_init/2, + wait_for_msg/3, + callback/4 + ]). + +%% sys callback functions +-export([ + system_continue/3, + system_terminate/4, + system_code_change/4 + ]). + +-include("tftp.hrl"). + +-type prep_status() :: 'error' | 'last' | 'more' | 'terminate'. + +-record(daemon_state, {config, n_servers, server_tab, file_tab}). +-record(server_info, {pid, req, peer}). +-record(file_info, {peer_req, pid}). +-record(sys_misc, {module, function, arguments}). +-record(error, {where, code, text, filename}). +-record(prepared, {status :: prep_status() | 'undefined', + result, block_no, next_data, prev_data}). +-record(transfer_res, {status, decoded_msg, prepared}). +-define(ERROR(Where, Code, Text, Filename), + #error{where = Where, code = Code, text = Text, filename = Filename}). + +%%%------------------------------------------------------------------- +%%% Info +%%%------------------------------------------------------------------- + +info(daemons) -> + Daemons = supervisor:which_children(tftp_sup), + [{Pid, info(Pid)} || {_, Pid, _, _} <- Daemons]; +info(servers) -> + [{Pid, info(Pid)} || {_, {ok, DeamonInfo}} <- info(daemons), + {server, Pid} <- DeamonInfo]; +info(ToPid) when is_pid(ToPid) -> + call(info, ToPid, timer:seconds(10)). + +change_config(daemons, Options) -> + Daemons = supervisor:which_children(tftp_sup), + [{Pid, change_config(Pid, Options)} || {_, Pid, _, _} <- Daemons]; +change_config(servers, Options) -> + [{Pid, change_config(Pid, Options)} || {_, {ok, DeamonInfo}} <- info(daemons), + {server, Pid} <- DeamonInfo]; +change_config(ToPid, Options) when is_pid(ToPid) -> + BadKeys = [host, port, udp], + BadOptions = [{Key, Val} || {Key, Val} <- Options, + BadKey <- BadKeys, + Key =:= BadKey], + case BadOptions of + [] -> + call({change_config, Options}, ToPid, timer:seconds(10)); + [{Key, Val} | _] -> + {error, {badarg, {Key, Val}}} + end. + +call(Req, ToPid, Timeout) when is_pid(ToPid) -> + Type = process, + Ref = erlang:monitor(Type, ToPid), + ToPid ! {Req, Ref, self()}, + receive + {Reply, Ref, FromPid} when FromPid =:= ToPid -> + erlang:demonitor(Ref, [flush]), + Reply; + {'DOWN', Ref, Type, FromPid, _Reason} when FromPid =:= ToPid -> + {error, timeout} + after Timeout -> + {error, timeout} + end. + +reply(Reply, Ref, ToPid) -> + ToPid ! {Reply, Ref, self()}. + +%%%------------------------------------------------------------------- +%%% Daemon +%%%------------------------------------------------------------------- + +%% Returns {ok, Port} +daemon_start(Options) when is_list(Options) -> + Config = tftp_lib:parse_config(Options), + proc_lib:start_link(?MODULE, daemon_init, [Config], infinity). + +daemon_init(Config) when is_record(Config, config), + is_pid(Config#config.parent_pid) -> + process_flag(trap_exit, true), + {Port, UdpOptions} = prepare_daemon_udp(Config), + case catch gen_udp:open(Port, UdpOptions) of + {ok, Socket} -> + {ok, ActualPort} = inet:port(Socket), + proc_lib:init_ack({ok, self()}), + Config2 = Config#config{udp_socket = Socket, + udp_port = ActualPort}, + print_debug_info(Config2, daemon, open, #tftp_msg_req{filename = ""}), + ServerTab = ets:new(tftp_daemon_servers, [{keypos, 2}]), + FileTab = ets:new(tftp_daemon_files, [{keypos, 2}]), + State = #daemon_state{config = Config2, + n_servers = 0, + server_tab = ServerTab, + file_tab = FileTab}, + daemon_loop(State); + {error, Reason} -> + Text = lists:flatten(io_lib:format("UDP open ~p -> ~p", [UdpOptions, Reason])), + print_debug_info(Config, daemon, open, ?ERROR(open, undef, Text, "")), + exit({gen_udp_open, UdpOptions, Reason}); + Reason -> + Text = lists:flatten(io_lib:format("UDP open ~p -> ~p", [UdpOptions, Reason])), + print_debug_info(Config, daemon, open, ?ERROR(open, undef, Text, "")), + exit({gen_udp_open, UdpOptions, Reason}) + end. + +prepare_daemon_udp(#config{udp_port = Port, udp_options = UdpOptions} = Config) -> + case lists:keymember(fd, 1, UdpOptions) of + true -> + %% Use explicit fd + {Port, UdpOptions}; + false -> + %% Use fd from setuid_socket_wrap, such as -tftpd_69 + InitArg = list_to_atom("tftpd_" ++ integer_to_list(Port)), + case init:get_argument(InitArg) of + {ok, [[FdStr]] = Badarg} when is_list(FdStr) -> + case catch list_to_integer(FdStr) of + Fd when is_integer(Fd) -> + {0, [{fd, Fd} | lists:keydelete(ip, 1, UdpOptions)]}; + {'EXIT', _} -> + Text = lists:flatten(io_lib:format("Illegal prebound fd ~p: ~p", [InitArg, Badarg])), + print_debug_info(Config, daemon, open, ?ERROR(open, undef, Text, "")), + exit({badarg, {prebound_fd, InitArg, Badarg}}) + end; + {ok, Badarg} -> + Text = lists:flatten(io_lib:format("Illegal prebound fd ~p: ~p", [InitArg, Badarg])), + print_debug_info(Config, daemon, open, ?ERROR(open, undef, Text, "")), + exit({badarg, {prebound_fd, InitArg, Badarg}}); + error -> + {Port, UdpOptions} + end + end. + +daemon_loop(DaemonConfig, N, Servers) when is_list(Servers) -> + %% Handle upgrade from old releases. Please, remove this function in next release. + ServerTab = ets:new(tftp_daemon_servers, [{keypos, 2}]), + FileTab = ets:new(tftp_daemon_files, [{keypos, 2}]), + State = #daemon_state{config = DaemonConfig, + n_servers = N, + server_tab = ServerTab, + file_tab = FileTab}, + Req = #tftp_msg_req{filename = dummy}, + [ets:insert(ServerTab, #server_info{pid = Pid, req = Req, peer = dummy}) || Pid <- Servers], + daemon_loop(State). + +daemon_loop(#daemon_state{config = DaemonConfig, + n_servers = N, + server_tab = ServerTab, + file_tab = FileTab} = State) when is_record(DaemonConfig, config) -> + %% info_msg(DaemonConfig, "=====> TFTP: Daemon #~p\n", [N]), %% XXX + receive + {info, Ref, FromPid} when is_pid(FromPid) -> + Fun = fun(#server_info{pid = Pid}, Acc) -> [{server, Pid} | Acc] end, + ServerInfo = ets:foldl(Fun, [], ServerTab), + Info = internal_info(DaemonConfig, daemon) ++ [{n_conn, N}] ++ ServerInfo, + reply({ok, Info}, Ref, FromPid), + ?MODULE:daemon_loop(State); + {{change_config, Options}, Ref, FromPid} when is_pid(FromPid) -> + case catch tftp_lib:parse_config(Options, DaemonConfig) of + {'EXIT', Reason} -> + reply({error, Reason}, Ref, FromPid), + ?MODULE:daemon_loop(State); + DaemonConfig2 when is_record(DaemonConfig2, config) -> + reply(ok, Ref, FromPid), + ?MODULE:daemon_loop(State#daemon_state{config = DaemonConfig2}) + end; + {udp, Socket, RemoteHost, RemotePort, Bin} when is_binary(Bin) -> + inet:setopts(Socket, [{active, once}]), + ServerConfig = DaemonConfig#config{parent_pid = self(), + udp_host = RemoteHost, + udp_port = RemotePort}, + Msg = (catch tftp_lib:decode_msg(Bin)), + print_debug_info(ServerConfig, daemon, recv, Msg), + case Msg of + Req when is_record(Req, tftp_msg_req), + N =< DaemonConfig#config.max_conn -> + Peer = peer_info(ServerConfig), + PeerReq = {Peer, Req}, + PeerInfo = lists:flatten(io_lib:format("~p", [Peer])), + case ets:lookup(FileTab, PeerReq) of + [] -> + Args = [ServerConfig, Req], + Pid = proc_lib:spawn_link(?MODULE, server_init, Args), + ets:insert(ServerTab, #server_info{pid = Pid, req = Req, peer = Peer}), + ets:insert(FileTab, #file_info{peer_req = PeerReq, pid = Pid}), + ?MODULE:daemon_loop(State#daemon_state{n_servers = N + 1}); + [#file_info{pid = Pid}] -> + %% Yet another request of the file from same peer + warning_msg(DaemonConfig, "~p Reuse connection for ~s\n\t~p\n", + [Pid, PeerInfo, Req#tftp_msg_req.filename]), + ?MODULE:daemon_loop(State) + end; + Req when is_record(Req, tftp_msg_req) -> + Reply = #tftp_msg_error{code = enospc, text = "Too many connections"}, + Peer = peer_info(ServerConfig), + PeerInfo = lists:flatten(io_lib:format("~p", [Peer])), + warning_msg(DaemonConfig, + "Daemon has too many connections (~p)." + "\n\tRejecting request from ~s\n", + [N, PeerInfo]), + send_msg(ServerConfig, daemon, Reply), + ?MODULE:daemon_loop(State); + {'EXIT', Reply} when is_record(Reply, tftp_msg_error) -> + send_msg(ServerConfig, daemon, Reply), + ?MODULE:daemon_loop(State); + Req -> + Reply = #tftp_msg_error{code = badop, + text = "Illegal TFTP operation"}, + warning_msg(DaemonConfig, "Daemon received: ~p.\n\tfrom ~p:~p", + [Req, RemoteHost, RemotePort]), + send_msg(ServerConfig, daemon, Reply), + ?MODULE:daemon_loop(State) + end; + {system, From, Msg} -> + Misc = #sys_misc{module = ?MODULE, function = daemon_loop, arguments = [State]}, + sys:handle_system_msg(Msg, From, DaemonConfig#config.parent_pid, ?MODULE, [], Misc); + {'EXIT', Pid, Reason} when DaemonConfig#config.parent_pid =:= Pid -> + close_port(DaemonConfig, daemon, #tftp_msg_req{filename = ""}), + exit(Reason); + {'EXIT', Pid, _Reason} = Info -> + case ets:lookup(ServerTab, Pid) of + [] -> + warning_msg(DaemonConfig, "Daemon received: ~p", [Info]), + ?MODULE:daemon_loop(State); + [#server_info{req = Req, peer = Peer}] -> + PeerReq = {Peer, Req}, + ets:delete(FileTab, PeerReq), + ets:delete(ServerTab, Pid), + ?MODULE:daemon_loop(State#daemon_state{n_servers = N - 1}) + end; + Info -> + warning_msg(DaemonConfig, "Daemon received: ~p", [Info]), + ?MODULE:daemon_loop(State) + end; +daemon_loop(#daemon_state{config = Config} = State) -> + %% Handle upgrade from old releases. Please, remove this clause in next release. + Config2 = upgrade_config(Config), + daemon_loop(State#daemon_state{config = Config2}). + +upgrade_config({config, ParentPid, UdpSocket, UdpOptions, UdpHost, UdpPort, PortPolicy, + UseTsize, MaxTsize, MaxConn, Rejected, PoliteAck, DebugLevel, + Timeout, UserOptions, Callbacks}) -> + Callbacks2 = tftp_lib:add_default_callbacks(Callbacks), + Logger = tftp_logger, + MaxRetries = 5, + {config, ParentPid, UdpSocket, UdpOptions, UdpHost, UdpPort, PortPolicy, + UseTsize, MaxTsize, MaxConn, Rejected, PoliteAck, DebugLevel, + Timeout, UserOptions, Callbacks2, Logger, MaxRetries}. + +%%%------------------------------------------------------------------- +%%% Server +%%%------------------------------------------------------------------- + +server_init(Config, Req) when is_record(Config, config), + is_pid(Config#config.parent_pid), + is_record(Req, tftp_msg_req) -> + process_flag(trap_exit, true), + %% Config = + %% case os:getenv("TFTPDEBUG") of + %% false -> + %% Config0; + %% DebugLevel -> + %% Config0#config{debug_level = list_to_atom(DebugLevel)} + %% end, + SuggestedOptions = Req#tftp_msg_req.options, + UdpOptions = Config#config.udp_options, + UdpOptions2 = lists:keydelete(fd, 1, UdpOptions), + Config1 = Config#config{udp_options = UdpOptions2}, + Config2 = tftp_lib:parse_config(SuggestedOptions, Config1), + SuggestedOptions2 = Config2#config.user_options, + Req2 = Req#tftp_msg_req{options = SuggestedOptions2}, + case open_free_port(Config2, server, Req2) of + {ok, Config3} -> + Filename = Req#tftp_msg_req.filename, + case match_callback(Filename, Config3#config.callbacks) of + {ok, Callback} -> + print_debug_info(Config3, server, match, Callback), + case pre_verify_options(Config3, Req2) of + ok -> + case callback({open, server_open}, Config3, Callback, Req2) of + {Callback2, {ok, AcceptedOptions}} -> + {LocalAccess, _} = local_file_access(Req2), + OptText = "Internal error. Not allowed to add new options.", + case post_verify_options(Config3, Req2, AcceptedOptions, OptText) of + {ok, Config4, Req3} when AcceptedOptions =/= [] -> + Reply = #tftp_msg_oack{options = AcceptedOptions}, + BlockNo = + case LocalAccess of + read -> 0; + write -> 1 + end, + {Config5, Callback3, TransferRes} = + transfer(Config4, Callback2, Req3, Reply, LocalAccess, BlockNo, #prepared{}), + common_loop(Config5, Callback3, Req3, TransferRes, LocalAccess, BlockNo); + {ok, Config4, Req3} when LocalAccess =:= write -> + BlockNo = 0, + common_ack(Config4, Callback2, Req3, LocalAccess, BlockNo, #prepared{}); + {ok, Config4, Req3} when LocalAccess =:= read -> + BlockNo = 0, + common_read(Config4, Callback2, Req3, LocalAccess, BlockNo, BlockNo, #prepared{}); + {error, {Code, Text}} -> + {undefined, Error} = + callback({abort, {Code, Text}}, Config3, Callback2, Req2), + send_msg(Config3, Req, Error), + terminate(Config3, Req2, ?ERROR(post_verify_options, Code, Text, Req2#tftp_msg_req.filename)) + end; + {undefined, #tftp_msg_error{code = Code, text = Text} = Error} -> + send_msg(Config3, Req, Error), + terminate(Config3, Req, ?ERROR(server_open, Code, Text, Req2#tftp_msg_req.filename)) + end; + {error, {Code, Text}} -> + {undefined, Error} = + callback({abort, {Code, Text}}, Config2, Callback, Req2), + send_msg(Config2, Req, Error), + terminate(Config2, Req2, ?ERROR(pre_verify_options, Code, Text, Req2#tftp_msg_req.filename)) + end; + {error, #tftp_msg_error{code = Code, text = Text} = Error} -> + send_msg(Config3, Req, Error), + terminate(Config3, Req, ?ERROR(match_callback, Code, Text, Req2#tftp_msg_req.filename)) + end; + #error{} = Error -> + terminate(Config2, Req, Error) + end; +server_init(Config, Req) when is_record(Req, tftp_msg_req) -> + Config2 = upgrade_config(Config), + server_init(Config2, Req). + +%%%------------------------------------------------------------------- +%%% Client +%%%------------------------------------------------------------------- + +%% LocalFilename = filename() | 'binary' | binary() +%% Returns {ok, LastCallbackState} | {error, Reason} +client_start(Access, RemoteFilename, LocalFilename, Options) -> + Config = tftp_lib:parse_config(Options), + Config2 = Config#config{parent_pid = self(), + udp_socket = undefined}, + Req = #tftp_msg_req{access = Access, + filename = RemoteFilename, + mode = lookup_mode(Config2#config.user_options), + options = Config2#config.user_options, + local_filename = LocalFilename}, + Args = [Config2, Req], + case proc_lib:start_link(?MODULE, client_init, Args, infinity) of + {ok, LastCallbackState} -> + {ok, LastCallbackState}; + {error, Error} -> + {error, Error} + end. + +client_init(Config, Req) when is_record(Config, config), + is_pid(Config#config.parent_pid), + is_record(Req, tftp_msg_req) -> + process_flag(trap_exit, true), + %% Config = + %% case os:getenv("TFTPDEBUG") of + %% false -> + %% Config0; + %% "none" -> + %% Config0; + %% DebugLevel -> + %% info_msg(Config, "TFTPDEBUG: ~s\n", [DebugLevel]), + %% Config0#config{debug_level = list_to_atom(DebugLevel)} + %% end, + case open_free_port(Config, client, Req) of + {ok, Config2} -> + Req2 = + case Config2#config.use_tsize of + true -> + SuggestedOptions = Req#tftp_msg_req.options, + SuggestedOptions2 = tftp_lib:replace_val("tsize", "0", SuggestedOptions), + Req#tftp_msg_req{options = SuggestedOptions2}; + false -> + Req + end, + LocalFilename = Req2#tftp_msg_req.local_filename, + case match_callback(LocalFilename, Config2#config.callbacks) of + {ok, Callback} -> + print_debug_info(Config2, client, match, Callback), + client_prepare(Config2, Callback, Req2); + {error, #tftp_msg_error{code = Code, text = Text}} -> + terminate(Config, Req, ?ERROR(match, Code, Text, Req#tftp_msg_req.filename)) + end; + #error{} = Error -> + terminate(Config, Req, Error) + end. + +client_prepare(Config, Callback, Req) when is_record(Req, tftp_msg_req) -> + case pre_verify_options(Config, Req) of + ok -> + case callback({open, client_prepare}, Config, Callback, Req) of + {Callback2, {ok, AcceptedOptions}} -> + OptText = "Internal error. Not allowed to add new options.", + case post_verify_options(Config, Req, AcceptedOptions, OptText) of + {ok, Config2, Req2} -> + {LocalAccess, _} = local_file_access(Req2), + BlockNo = 0, + {Config3, Callback3, TransferRes} = + transfer(Config2, Callback2, Req2, Req2, LocalAccess, BlockNo, #prepared{}), + client_open(Config3, Callback3, Req2, BlockNo, TransferRes); + {error, {Code, Text}} -> + callback({abort, {Code, Text}}, Config, Callback2, Req), + terminate(Config, Req, ?ERROR(post_verify_options, Code, Text, Req#tftp_msg_req.filename)) + end; + {undefined, #tftp_msg_error{code = Code, text = Text}} -> + terminate(Config, Req, ?ERROR(client_prepare, Code, Text, Req#tftp_msg_req.filename)) + end; + {error, {Code, Text}} -> + callback({abort, {Code, Text}}, Config, Callback, Req), + terminate(Config, Req, ?ERROR(pre_verify_options, Code, Text, Req#tftp_msg_req.filename)) + end. + +client_open(Config, Callback, Req, BlockNo, #transfer_res{status = Status, decoded_msg = DecodedMsg, prepared = Prepared}) -> + {LocalAccess, _} = local_file_access(Req), + case Status of + ok when is_record(Prepared, prepared) -> + case DecodedMsg of + Msg when is_record(Msg, tftp_msg_oack) -> + ServerOptions = Msg#tftp_msg_oack.options, + OptText = "Protocol violation. Server is not allowed new options", + case post_verify_options(Config, Req, ServerOptions, OptText) of + {ok, Config2, Req2} -> + {Config3, Callback2, Req3} = + do_client_open(Config2, Callback, Req2), + case LocalAccess of + read -> + common_read(Config3, Callback2, Req3, LocalAccess, BlockNo, BlockNo, Prepared); + write -> + common_ack(Config3, Callback2, Req3, LocalAccess, BlockNo, Prepared) + end; + {error, {Code, Text}} -> + {undefined, Error} = + callback({abort, {Code, Text}}, Config, Callback, Req), + send_msg(Config, Req, Error), + terminate(Config, Req, ?ERROR(verify_server_options, Code, Text, Req#tftp_msg_req.filename)) + end; + #tftp_msg_ack{block_no = ActualBlockNo} when LocalAccess =:= read -> + Req2 = Req#tftp_msg_req{options = []}, + {Config2, Callback2, Req2} = do_client_open(Config, Callback, Req2), + ExpectedBlockNo = 0, + common_read(Config2, Callback2, Req2, LocalAccess, ExpectedBlockNo, ActualBlockNo, Prepared); + #tftp_msg_data{block_no = ActualBlockNo, data = Data} when LocalAccess =:= write -> + Req2 = Req#tftp_msg_req{options = []}, + {Config2, Callback2, Req2} = do_client_open(Config, Callback, Req2), + ExpectedBlockNo = 1, + common_write(Config2, Callback2, Req2, LocalAccess, ExpectedBlockNo, ActualBlockNo, Data, Prepared); + %% #tftp_msg_error{code = Code, text = Text} when Req#tftp_msg_req.options =/= [] -> + %% %% Retry without options + %% callback({abort, {Code, Text}}, Config, Callback, Req), + %% Req2 = Req#tftp_msg_req{options = []}, + %% client_prepare(Config, Callback, Req2); + #tftp_msg_error{code = Code, text = Text} -> + callback({abort, {Code, Text}}, Config, Callback, Req), + terminate(Config, Req, ?ERROR(client_open, Code, Text, Req#tftp_msg_req.filename)); + {'EXIT', #tftp_msg_error{code = Code, text = Text}} -> + callback({abort, {Code, Text}}, Config, Callback, Req), + terminate(Config, Req, ?ERROR(client_open, Code, Text, Req#tftp_msg_req.filename)); + Msg when is_tuple(Msg) -> + Code = badop, + Text = "Illegal TFTP operation", + {undefined, Error} = + callback({abort, {Code, Text}}, Config, Callback, Req), + send_msg(Config, Req, Error), + Text2 = lists:flatten([Text, ". ", io_lib:format("~p", [element(1, Msg)])]), + terminate(Config, Req, ?ERROR(client_open, Code, Text2, Req#tftp_msg_req.filename)) + end; + error when is_record(Prepared, tftp_msg_error) -> + #tftp_msg_error{code = Code, text = Text} = Prepared, + callback({abort, {Code, Text}}, Config, Callback, Req), + terminate(Config, Req, ?ERROR(client_open, Code, Text, Req#tftp_msg_req.filename)) + end. + +do_client_open(Config, Callback, Req) -> + case callback({open, client_open}, Config, Callback, Req) of + {Callback2, {ok, FinalOptions}} -> + OptText = "Internal error. Not allowed to change options.", + case post_verify_options(Config, Req, FinalOptions, OptText) of + {ok, Config2, Req2} -> + {Config2, Callback2, Req2}; + {error, {Code, Text}} -> + {undefined, Error} = + callback({abort, {Code, Text}}, Config, Callback2, Req), + send_msg(Config, Req, Error), + terminate(Config, Req, ?ERROR(post_verify_options, Code, Text, Req#tftp_msg_req.filename)) + end; + {undefined, #tftp_msg_error{code = Code, text = Text} = Error} -> + send_msg(Config, Req, Error), + terminate(Config, Req, ?ERROR(client_open, Code, Text, Req#tftp_msg_req.filename)) + end. + +%%%------------------------------------------------------------------- +%%% Common loop for both client and server +%%%------------------------------------------------------------------- + +common_loop(Config, Callback, Req, #transfer_res{status = Status, decoded_msg = DecodedMsg, prepared = Prepared}, LocalAccess, ExpectedBlockNo) + when is_record(Config, config)-> + %% Config = + %% case os:getenv("TFTPMAX") of + %% false -> + %% Config0; + %% MaxBlockNoStr when Config0#config.debug_level =/= none -> + %% case list_to_integer(MaxBlockNoStr) of + %% MaxBlockNo when ExpectedBlockNo > MaxBlockNo -> + %% info_msg(Config, "TFTPMAX: ~p\n", [MaxBlockNo]), + %% info_msg(Config, "TFTPDEBUG: none\n", []), + %% Config0#config{debug_level = none}; + %% _ -> + %% Config0 + %% end; + %% _MaxBlockNoStr -> + %% Config0 + %% end, + case Status of + ok when is_record(Prepared, prepared) -> + case DecodedMsg of + #tftp_msg_ack{block_no = ActualBlockNo} when LocalAccess =:= read -> + common_read(Config, Callback, Req, LocalAccess, ExpectedBlockNo, ActualBlockNo, Prepared); + #tftp_msg_data{block_no = ActualBlockNo, data = Data} when LocalAccess =:= write -> + common_write(Config, Callback, Req, LocalAccess, ExpectedBlockNo, ActualBlockNo, Data, Prepared); + #tftp_msg_error{code = Code, text = Text} -> + callback({abort, {Code, Text}}, Config, Callback, Req), + terminate(Config, Req, ?ERROR(common_loop, Code, Text, Req#tftp_msg_req.filename)); + {'EXIT', #tftp_msg_error{code = Code, text = Text} = Error} -> + callback({abort, {Code, Text}}, Config, Callback, Req), + send_msg(Config, Req, Error), + terminate(Config, Req, ?ERROR(common_loop, Code, Text, Req#tftp_msg_req.filename)); + Msg when is_tuple(Msg) -> + Code = badop, + Text = "Illegal TFTP operation", + {undefined, Error} = + callback({abort, {Code, Text}}, Config, Callback, Req), + send_msg(Config, Req, Error), + Text2 = lists:flatten([Text, ". ", io_lib:format("~p", [element(1, Msg)])]), + terminate(Config, Req, ?ERROR(common_loop, Code, Text2, Req#tftp_msg_req.filename)) + end; + error when is_record(Prepared, tftp_msg_error) -> + #tftp_msg_error{code = Code, text = Text} = Prepared, + send_msg(Config, Req, Prepared), + terminate(Config, Req, ?ERROR(transfer, Code, Text, Req#tftp_msg_req.filename)) + end; +common_loop(Config, Callback, Req, TransferRes, LocalAccess, ExpectedBlockNo) -> + %% Handle upgrade from old releases. Please, remove this clause in next release. + Config2 = upgrade_config(Config), + common_loop(Config2, Callback, Req, TransferRes, LocalAccess, ExpectedBlockNo). + +-spec common_read(#config{}, #callback{}, _, 'read', _, _, #prepared{}) -> no_return(). + +common_read(Config, _, Req, _, _, _, #prepared{status = terminate, result = Result}) -> + terminate(Config, Req, {ok, Result}); +common_read(Config, Callback, Req, LocalAccess, ExpectedBlockNo, ActualBlockNo, Prepared) + when ActualBlockNo =:= ExpectedBlockNo, is_record(Prepared, prepared) -> + case early_read(Config, Callback, Req, LocalAccess, ActualBlockNo, Prepared) of + {Callback2, #prepared{status = more, next_data = Data} = Prepared2} when is_binary(Data) -> + Prepared3 = Prepared2#prepared{prev_data = Data, next_data = undefined}, + do_common_read(Config, Callback2, Req, LocalAccess, ActualBlockNo, Data, Prepared3); + {undefined, #prepared{status = last, next_data = Data} = Prepared2} when is_binary(Data) -> + Prepared3 = Prepared2#prepared{status = terminate}, + do_common_read(Config, undefined, Req, LocalAccess, ActualBlockNo, Data, Prepared3); + {undefined, #prepared{status = error, result = Error}} -> + #tftp_msg_error{code = Code, text = Text} = Error, + send_msg(Config, Req, Error), + terminate(Config, Req, ?ERROR(read, Code, Text, Req#tftp_msg_req.filename)) + end; +common_read(Config, Callback, Req, LocalAccess, ExpectedBlockNo, ActualBlockNo, Prepared) + when ActualBlockNo =:= (ExpectedBlockNo - 1), is_record(Prepared, prepared) -> + case Prepared of + #prepared{status = more, prev_data = Data} when is_binary(Data) -> + do_common_read(Config, Callback, Req, LocalAccess, ActualBlockNo, Data, Prepared); + #prepared{status = last, prev_data = Data} when is_binary(Data) -> + do_common_read(Config, Callback, Req, LocalAccess, ActualBlockNo, Data, Prepared); + #prepared{status = error, result = Error} -> + #tftp_msg_error{code = Code, text = Text} = Error, + send_msg(Config, Req, Error), + terminate(Config, Req, ?ERROR(read, Code, Text, Req#tftp_msg_req.filename)) + end; +common_read(Config, Callback, Req, LocalAccess, ExpectedBlockNo, ActualBlockNo, Prepared) + when ActualBlockNo =< ExpectedBlockNo, is_record(Prepared, prepared) -> + %% error_logger:error_msg("TFTP READ ~s: Expected block ~p but got block ~p - IGNORED\n", + %% [Req#tftp_msg_req.filename, ExpectedBlockNo, ActualBlockNo]), + case Prepared of + #prepared{status = more, prev_data = Data} when is_binary(Data) -> + Reply = #tftp_msg_data{block_no = ExpectedBlockNo, data = Data}, + {Config2, Callback2, TransferRes} = + wait_for_msg_and_handle_timeout(Config, Callback, Req, Reply, LocalAccess, ExpectedBlockNo, Prepared), + ?MODULE:common_loop(Config2, Callback2, Req, TransferRes, LocalAccess, ExpectedBlockNo); + #prepared{status = last, prev_data = Data} when is_binary(Data) -> + Reply = #tftp_msg_data{block_no = ExpectedBlockNo, data = Data}, + {Config2, Callback2, TransferRes} = + wait_for_msg_and_handle_timeout(Config, Callback, Req, Reply, LocalAccess, ExpectedBlockNo, Prepared), + ?MODULE:common_loop(Config2, Callback2, Req, TransferRes, LocalAccess, ExpectedBlockNo); + #prepared{status = error, result = Error} -> + #tftp_msg_error{code = Code, text = Text} = Error, + send_msg(Config, Req, Error), + terminate(Config, Req, ?ERROR(read, Code, Text, Req#tftp_msg_req.filename)) + end; +common_read(Config, Callback, Req, _LocalAccess, ExpectedBlockNo, ActualBlockNo, Prepared) + when is_record(Prepared, prepared) -> + Code = badblk, + Text = "Unknown transfer ID = " ++ + integer_to_list(ActualBlockNo) ++ " (" ++ integer_to_list(ExpectedBlockNo) ++ ")", + {undefined, Error} = + callback({abort, {Code, Text}}, Config, Callback, Req), + send_msg(Config, Req, Error), + terminate(Config, Req, ?ERROR(read, Code, Text, Req#tftp_msg_req.filename)). + +-spec do_common_read(#config{}, #callback{} | undefined, _, 'read', integer(), binary(), #prepared{}) -> no_return(). + +do_common_read(Config, Callback, Req, LocalAccess, BlockNo, Data, Prepared) + when is_binary(Data), is_record(Prepared, prepared) -> + NextBlockNo = (BlockNo + 1) rem 65536, + Reply = #tftp_msg_data{block_no = NextBlockNo, data = Data}, + {Config2, Callback2, TransferRes} = + transfer(Config, Callback, Req, Reply, LocalAccess, NextBlockNo, Prepared), + ?MODULE:common_loop(Config2, Callback2, Req, TransferRes, LocalAccess, NextBlockNo). + +-spec common_write(#config{}, #callback{}, _, 'write', integer(), integer(), _, #prepared{}) -> no_return(). + +common_write(Config, _, Req, _, _, _, _, #prepared{status = terminate, result = Result}) -> + terminate(Config, Req, {ok, Result}); +common_write(Config, Callback, Req, LocalAccess, ExpectedBlockNo, ActualBlockNo, Data, Prepared) + when ActualBlockNo =:= ExpectedBlockNo, is_binary(Data), is_record(Prepared, prepared) -> + case callback({write, Data}, Config, Callback, Req) of + {Callback2, #prepared{status = more} = Prepared2} -> + common_ack(Config, Callback2, Req, LocalAccess, ActualBlockNo, Prepared2); + {undefined, #prepared{status = last, result = Result} = Prepared2} -> + Config2 = pre_terminate(Config, Req, {ok, Result}), + Prepared3 = Prepared2#prepared{status = terminate}, + common_ack(Config2, undefined, Req, LocalAccess, ActualBlockNo, Prepared3); + {undefined, #prepared{status = error, result = Error}} -> + #tftp_msg_error{code = Code, text = Text} = Error, + send_msg(Config, Req, Error), + terminate(Config, Req, ?ERROR(write, Code, Text, Req#tftp_msg_req.filename)) + end; +common_write(Config, Callback, Req, LocalAccess, ExpectedBlockNo, ActualBlockNo, Data, Prepared) + when ActualBlockNo =:= (ExpectedBlockNo - 1), is_binary(Data), is_record(Prepared, prepared) -> + common_ack(Config, Callback, Req, LocalAccess, ExpectedBlockNo - 1, Prepared); +common_write(Config, Callback, Req, LocalAccess, ExpectedBlockNo, ActualBlockNo, Data, Prepared) + when ActualBlockNo =< ExpectedBlockNo, is_binary(Data), is_record(Prepared, prepared) -> + %% error_logger:error_msg("TFTP WRITE ~s: Expected block ~p but got block ~p - IGNORED\n", + %% [Req#tftp_msg_req.filename, ExpectedBlockNo, ActualBlockNo]), + Reply = #tftp_msg_ack{block_no = ExpectedBlockNo}, + {Config2, Callback2, TransferRes} = + wait_for_msg_and_handle_timeout(Config, Callback, Req, Reply, LocalAccess, ExpectedBlockNo, Prepared), + ?MODULE:common_loop(Config2, Callback2, Req, TransferRes, LocalAccess, ExpectedBlockNo); +common_write(Config, Callback, Req, _, ExpectedBlockNo, ActualBlockNo, Data, Prepared) + when is_binary(Data), is_record(Prepared, prepared) -> + Code = badblk, + Text = "Unknown transfer ID = " ++ + integer_to_list(ActualBlockNo) ++ " (" ++ integer_to_list(ExpectedBlockNo) ++ ")", + {undefined, Error} = + callback({abort, {Code, Text}}, Config, Callback, Req), + send_msg(Config, Req, Error), + terminate(Config, Req, ?ERROR(write, Code, Text, Req#tftp_msg_req.filename)). + +common_ack(Config, Callback, Req, LocalAccess, BlockNo, Prepared) + when is_record(Prepared, prepared) -> + Reply = #tftp_msg_ack{block_no = BlockNo}, + NextBlockNo = (BlockNo + 1) rem 65536, + {Config2, Callback2, TransferRes} = + transfer(Config, Callback, Req, Reply, LocalAccess, NextBlockNo, Prepared), + ?MODULE:common_loop(Config2, Callback2, Req, TransferRes, LocalAccess, NextBlockNo). + +pre_terminate(Config, Req, Result) -> + if + Req#tftp_msg_req.local_filename =/= undefined, + Config#config.parent_pid =/= undefined -> + proc_lib:init_ack(Result), + unlink(Config#config.parent_pid), + Config#config{parent_pid = undefined, polite_ack = true}; + true -> + Config#config{polite_ack = true} + end. + +-spec terminate(#config{}, #tftp_msg_req{}, {'ok', _} | #error{}) -> no_return(). + +terminate(Config, Req, Result) -> + Result2 = + case Result of + {ok, _} -> + Result; + #error{where = Where, code = Code, text = Text} = Error -> + print_debug_info(Config, Req, Where, Error#error{filename = Req#tftp_msg_req.filename}), + {error, {Where, Code, Text}} + end, + if + Config#config.parent_pid =:= undefined -> + close_port(Config, client, Req), + exit(normal); + Req#tftp_msg_req.local_filename =/= undefined -> + %% Client + close_port(Config, client, Req), + proc_lib:init_ack(Result2), + unlink(Config#config.parent_pid), + exit(normal); + true -> + %% Server + close_port(Config, server, Req), + exit(shutdown) + end. + +close_port(Config, Who, Req) when is_record(Req, tftp_msg_req) -> + case Config#config.udp_socket of + undefined -> + ignore; + Socket -> + print_debug_info(Config, Who, close, Req), + gen_udp:close(Socket) + end. + +open_free_port(Config, Who, Req) when is_record(Config, config), is_record(Req, tftp_msg_req) -> + UdpOptions = Config#config.udp_options, + case Config#config.port_policy of + random -> + %% BUGBUG: Should be a random port + case catch gen_udp:open(0, UdpOptions) of + {ok, Socket} -> + Config2 = Config#config{udp_socket = Socket}, + print_debug_info(Config2, Who, open, Req), + {ok, Config2}; + {error, Reason} -> + Text = lists:flatten(io_lib:format("UDP open ~p -> ~p", [[0 | UdpOptions], Reason])), + ?ERROR(open, undef, Text, Req#tftp_msg_req.filename); + {'EXIT', _} = Reason -> + Text = lists:flatten(io_lib:format("UDP open ~p -> ~p", [[0 | UdpOptions], Reason])), + ?ERROR(open, undef, Text, Req#tftp_msg_req.filename) + end; + {range, Port, Max} when Port =< Max -> + case catch gen_udp:open(Port, UdpOptions) of + {ok, Socket} -> + Config2 = Config#config{udp_socket = Socket}, + print_debug_info(Config2, Who, open, Req), + {ok, Config2}; + {error, eaddrinuse} -> + PortPolicy = {range, Port + 1, Max}, + Config2 = Config#config{port_policy = PortPolicy}, + open_free_port(Config2, Who, Req); + {error, Reason} -> + Text = lists:flatten(io_lib:format("UDP open ~p -> ~p", [[Port | UdpOptions], Reason])), + ?ERROR(open, undef, Text, Req#tftp_msg_req.filename); + {'EXIT', _} = Reason-> + Text = lists:flatten(io_lib:format("UDP open ~p -> ~p", [[Port | UdpOptions], Reason])), + ?ERROR(open, undef, Text, Req#tftp_msg_req.filename) + end; + {range, Port, _Max} -> + Reason = "Port range exhausted", + Text = lists:flatten(io_lib:format("UDP open ~p -> ~p", [[Port | UdpOptions], Reason])), + ?ERROR(Who, undef, Text, Req#tftp_msg_req.filename) + end. + +%%------------------------------------------------------------------- +%% Transfer +%%------------------------------------------------------------------- + +%% Returns {Config, Callback, #transfer_res{}} +transfer(Config, Callback, Req, Msg, LocalAccess, NextBlockNo, Prepared) + when is_record(Prepared, prepared) -> + IoList = tftp_lib:encode_msg(Msg), + Retries = Config#config.max_retries + 1, + do_transfer(Config, Callback, Req, Msg, IoList, LocalAccess, NextBlockNo, Prepared, Retries). + +do_transfer(Config, Callback, Req, Msg, IoList, LocalAccess, NextBlockNo, Prepared, Retries) + when is_record(Prepared, prepared), is_integer(Retries), Retries >= 0 -> + case do_send_msg(Config, Req, Msg, IoList) of + ok -> + {Callback2, Prepared2} = + early_read(Config, Callback, Req, LocalAccess, NextBlockNo, Prepared), + do_wait_for_msg_and_handle_timeout(Config, Callback2, Req, Msg, IoList, LocalAccess, NextBlockNo, Prepared2, Retries); + {error, _Reason} when Retries > 0 -> + Retries2 = 0, % Just retry once when send fails + do_transfer(Config, Callback, Req, Msg, IoList, LocalAccess, NextBlockNo, Prepared, Retries2); + {error, Reason} -> + Code = undef, + Text = lists:flatten(io_lib:format("Transfer failed - giving up -> ~p", [Reason])), + Error = #tftp_msg_error{code = Code, text = Text}, + {Config, Callback, #transfer_res{status = error, prepared = Error}} + end. + +wait_for_msg_and_handle_timeout(Config, Callback, Req, Msg, LocalAccess, NextBlockNo, Prepared) -> + IoList = tftp_lib:encode_msg(Msg), + Retries = Config#config.max_retries + 1, + do_wait_for_msg_and_handle_timeout(Config, Callback, Req, Msg, IoList, LocalAccess, NextBlockNo, Prepared, Retries). + +do_wait_for_msg_and_handle_timeout(Config, Callback, Req, Msg, IoList, LocalAccess, NextBlockNo, Prepared, Retries) -> + Code = undef, + Text = "Transfer timed out.", + case wait_for_msg(Config, Callback, Req) of + timeout when Config#config.polite_ack =:= true -> + do_send_msg(Config, Req, Msg, IoList), + case Prepared of + #prepared{status = terminate, result = Result} -> + terminate(Config, Req, {ok, Result}); + #prepared{} -> + terminate(Config, Req, ?ERROR(transfer, Code, Text, Req#tftp_msg_req.filename)) + end; + timeout when Retries > 0 -> + Retries2 = Retries - 1, + do_transfer(Config, Callback, Req, Msg, IoList, LocalAccess, NextBlockNo, Prepared, Retries2); + timeout -> + Error = #tftp_msg_error{code = Code, text = Text}, + {Config, Callback, #transfer_res{status = error, prepared = Error}}; + {Config2, DecodedMsg} -> + {Config2, Callback, #transfer_res{status = ok, decoded_msg = DecodedMsg, prepared = Prepared}} + end. + +send_msg(Config, Req, Msg) -> + case catch tftp_lib:encode_msg(Msg) of + {'EXIT', Reason} -> + Code = undef, + Text = "Internal error. Encode failed", + Msg2 = #tftp_msg_error{code = Code, text = Text, details = Reason}, + send_msg(Config, Req, Msg2); + IoList -> + do_send_msg(Config, Req, Msg, IoList) + end. + +do_send_msg(#config{udp_socket = Socket, udp_host = RemoteHost, udp_port = RemotePort} = Config, Req, Msg, IoList) -> + %% {ok, LocalPort} = inet:port(Socket), + %% if + %% LocalPort =/= ?TFTP_DEFAULT_PORT -> + %% ok; + %% true -> + %% print_debug_info(Config#config{debug_level = all}, Req, send, Msg), + %% error(Config, + %% "Daemon replies from the default port (~p)\n\t to ~p:~p\n\t¨~p\n", + %% [LocalPort, RemoteHost, RemotePort, Msg]) + %% end, + + print_debug_info(Config, Req, send, Msg), + + %% case os:getenv("TFTPDUMP") of + %% false -> + %% ignore; + %% DumpPath -> + %% trace_udp_send(Req, Msg, IoList, DumpPath) + %% end, + Res = gen_udp:send(Socket, RemoteHost, RemotePort, IoList), + case Res of + ok -> + ok; + {error, einval = Reason} -> + error_msg(Config, + "Stacktrace; ~p\n gen_udp:send(~p, ~p, ~p, ~p) -> ~p\n", + [erlang:get_stacktrace(), Socket, RemoteHost, RemotePort, IoList, {error, Reason}]); + {error, Reason} -> + {error, Reason} + end. + +%% trace_udp_send(#tftp_msg_req{filename = [$/ | RelFile]} = Req, Msg, IoList, DumpPath) -> +%% trace_udp_send(Req#tftp_msg_req{filename = RelFile}, Msg, IoList, DumpPath); +%% trace_udp_send(#tftp_msg_req{filename = RelFile}, +%% #tftp_msg_data{block_no = BlockNo, data = Data}, +%% _IoList, +%% DumpPath) -> +%% File = filename:join([DumpPath, RelFile, "block" ++ string:right(integer_to_list(BlockNo), 5, $0) ++ ".dump"]), +%% if +%% (BlockNo rem 1000) =:= 1 -> +%% info_msg(Config, "TFTPDUMP: Data ~s\n", [File]); +%% true -> +%% ignore +%% end, +%% ok = filelib:ensure_dir(File), +%% ok = file:write_file(File, Data); +%% trace_udp_send(#tftp_msg_req{filename = RelFile}, Msg, _IoList, _DumpPath) -> +%% info_msg(Config, "TFTPDUMP: No data ~s -> ~p\n", [RelFile, element(1, Msg)]). + +wait_for_msg(Config, Callback, Req) -> + receive + {udp, Socket, RemoteHost, RemotePort, Bin} + when is_binary(Bin), Callback#callback.block_no =:= undefined -> + %% Client prepare + inet:setopts(Socket, [{active, once}]), + Config2 = Config#config{udp_host = RemoteHost, + udp_port = RemotePort}, + DecodedMsg = (catch tftp_lib:decode_msg(Bin)), + print_debug_info(Config2, Req, recv, DecodedMsg), + {Config2, DecodedMsg}; + {udp, Socket, Host, Port, Bin} when is_binary(Bin), + Config#config.udp_host =:= Host, + Config#config.udp_port =:= Port -> + inet:setopts(Socket, [{active, once}]), + DecodedMsg = (catch tftp_lib:decode_msg(Bin)), + print_debug_info(Config, Req, recv, DecodedMsg), + {Config, DecodedMsg}; + {info, Ref, FromPid} when is_pid(FromPid) -> + Type = + case Req#tftp_msg_req.local_filename =/= undefined of + true -> client; + false -> server + end, + Info = internal_info(Config, Type), + reply({ok, Info}, Ref, FromPid), + wait_for_msg(Config, Callback, Req); + {{change_config, Options}, Ref, FromPid} when is_pid(FromPid) -> + case catch tftp_lib:parse_config(Options, Config) of + {'EXIT', Reason} -> + reply({error, Reason}, Ref, FromPid), + wait_for_msg(Config, Callback, Req); + Config2 when is_record(Config2, config) -> + reply(ok, Ref, FromPid), + wait_for_msg(Config2, Callback, Req) + end; + {system, From, Msg} -> + Misc = #sys_misc{module = ?MODULE, function = wait_for_msg, arguments = [Config, Callback, Req]}, + sys:handle_system_msg(Msg, From, Config#config.parent_pid, ?MODULE, [], Misc); + {'EXIT', Pid, _Reason} when Config#config.parent_pid =:= Pid -> + Code = undef, + Text = "Parent exited.", + terminate(Config, Req, ?ERROR(wait_for_msg, Code, Text, Req#tftp_msg_req.filename)); + Msg when Req#tftp_msg_req.local_filename =/= undefined -> + warning_msg(Config, "Client received : ~p", [Msg]), + wait_for_msg(Config, Callback, Req); + Msg when Req#tftp_msg_req.local_filename =:= undefined -> + warning_msg(Config, "Server received : ~p", [Msg]), + wait_for_msg(Config, Callback, Req) + after Config#config.timeout * 1000 -> + print_debug_info(Config, Req, recv, timeout), + timeout + end. + +early_read(Config, Callback, Req, LocalAccess, _NextBlockNo, + #prepared{status = Status, next_data = NextData, prev_data = PrevData} = Prepared) -> + if + Status =/= terminate, + LocalAccess =:= read, + Callback#callback.block_no =/= undefined, + NextData =:= undefined -> + case callback(read, Config, Callback, Req) of + {undefined, Error} when is_record(Error, tftp_msg_error) -> + {undefined, Error}; + {Callback2, Prepared2} when is_record(Prepared2, prepared)-> + {Callback2, Prepared2#prepared{prev_data = PrevData}} + end; + true -> + {Callback, Prepared} + end. + +%%------------------------------------------------------------------- +%% Callback +%%------------------------------------------------------------------- + +callback(Access, Config, Callback, Req) -> + {Callback2, Result} = + do_callback(Access, Config, Callback, Req), + print_debug_info(Config, Req, call, {Callback2, Result}), + {Callback2, Result}. + +do_callback(read = Fun, Config, Callback, Req) + when is_record(Config, config), + is_record(Callback, callback), + is_record(Req, tftp_msg_req) -> + Args = [Callback#callback.state], + NextBlockNo = Callback#callback.block_no + 1, + case catch safe_apply(Callback#callback.module, Fun, Args) of + {more, Bin, NewState} when is_binary(Bin) -> + Count = Callback#callback.count + size(Bin), + Callback2 = Callback#callback{state = NewState, + block_no = NextBlockNo, + count = Count}, + Prepared = #prepared{status = more, + result = undefined, + block_no = NextBlockNo, + next_data = Bin}, + verify_count(Config, Callback2, Req, Prepared); + {last, Bin, Result} when is_binary(Bin) -> + Prepared = #prepared{status = last, + result = Result, + block_no = NextBlockNo, + next_data = Bin}, + {undefined, Prepared}; + {error, {Code, Text}} -> + Error = #tftp_msg_error{code = Code, text = Text}, + Prepared = #prepared{status = error, + result = Error}, + {undefined, Prepared}; + Illegal -> + Code = undef, + Text = "Internal error. File handler error.", + callback({abort, {Code, Text, Illegal}}, Config, Callback, Req) + end; +do_callback({write = Fun, Bin}, Config, Callback, Req) + when is_record(Config, config), + is_record(Callback, callback), + is_record(Req, tftp_msg_req), + is_binary(Bin) -> + Args = [Bin, Callback#callback.state], + NextBlockNo = Callback#callback.block_no + 1, + case catch safe_apply(Callback#callback.module, Fun, Args) of + {more, NewState} -> + Count = Callback#callback.count + size(Bin), + Callback2 = Callback#callback{state = NewState, + block_no = NextBlockNo, + count = Count}, + Prepared = #prepared{status = more, + block_no = NextBlockNo}, + verify_count(Config, Callback2, Req, Prepared); + {last, Result} -> + Prepared = #prepared{status = last, + result = Result, + block_no = NextBlockNo}, + {undefined, Prepared}; + {error, {Code, Text}} -> + Error = #tftp_msg_error{code = Code, text = Text}, + Prepared = #prepared{status = error, + result = Error}, + {undefined, Prepared}; + Illegal -> + Code = undef, + Text = "Internal error. File handler error.", + callback({abort, {Code, Text, Illegal}}, Config, Callback, Req) + end; +do_callback({open, Type}, Config, Callback, Req) + when is_record(Config, config), + is_record(Callback, callback), + is_record(Req, tftp_msg_req) -> + {Access, Filename} = local_file_access(Req), + {Fun, BlockNo} = + case Type of + client_prepare -> {prepare, undefined}; + client_open -> {open, 0}; + server_open -> {open, 0} + end, + Mod = Callback#callback.module, + Args = [Access, + Filename, + Req#tftp_msg_req.mode, + Req#tftp_msg_req.options, + Callback#callback.state], + PeerInfo = peer_info(Config), + fast_ensure_loaded(Mod), + Args2 = + case erlang:function_exported(Mod, Fun, length(Args)) of + true -> Args; + false -> [PeerInfo | Args] + end, + case catch safe_apply(Mod, Fun, Args2) of + {ok, AcceptedOptions, NewState} -> + Callback2 = Callback#callback{state = NewState, + block_no = BlockNo, + count = 0}, + {Callback2, {ok, AcceptedOptions}}; + {error, {Code, Text}} -> + {undefined, #tftp_msg_error{code = Code, text = Text}}; + Illegal -> + Code = undef, + Text = "Internal error. File handler error.", + callback({abort, {Code, Text, Illegal}}, Config, Callback, Req) + end; +do_callback({abort, {Code, Text}}, Config, Callback, Req) -> + Error = #tftp_msg_error{code = Code, text = Text}, + do_callback({abort, Error}, Config, Callback, Req); +do_callback({abort, {Code, Text, Details}}, Config, Callback, Req) -> + Error = #tftp_msg_error{code = Code, text = Text, details = Details}, + do_callback({abort, Error}, Config, Callback, Req); +do_callback({abort = Fun, #tftp_msg_error{code = Code, text = Text} = Error}, Config, Callback, Req) + when is_record(Config, config), + is_record(Callback, callback), + is_record(Req, tftp_msg_req) -> + Args = [Code, Text, Callback#callback.state], + catch safe_apply(Callback#callback.module, Fun, Args), + {undefined, Error}; +do_callback({abort, Error}, _Config, undefined, _Req) when is_record(Error, tftp_msg_error) -> + {undefined, Error}. + +peer_info(#config{udp_host = Host, udp_port = Port}) -> + if + is_tuple(Host), size(Host) =:= 4 -> + {inet, tftp_lib:host_to_string(Host), Port}; + is_tuple(Host), size(Host) =:= 8 -> + {inet6, tftp_lib:host_to_string(Host), Port}; + true -> + {undefined, Host, Port} + end. + +match_callback(Filename, Callbacks) -> + if + Filename =:= binary -> + lookup_callback_mod(tftp_binary, Callbacks); + is_binary(Filename) -> + lookup_callback_mod(tftp_binary, Callbacks); + true -> + do_match_callback(Filename, Callbacks) + end. + +do_match_callback(Filename, [C | Tail]) when is_record(C, callback) -> + case catch re:run(Filename, C#callback.internal, [{capture, none}]) of + match -> + {ok, C}; + nomatch -> + do_match_callback(Filename, Tail); + Details -> + Code = baduser, + Text = "Internal error. File handler not found", + {error, #tftp_msg_error{code = Code, text = Text, details = Details}} + end; +do_match_callback(Filename, []) -> + Code = baduser, + Text = "Internal error. File handler not found", + {error, #tftp_msg_error{code = Code, text = Text, details = Filename}}. + +lookup_callback_mod(Mod, Callbacks) -> + {value, C} = lists:keysearch(Mod, #callback.module, Callbacks), + {ok, C}. + +verify_count(Config, Callback, Req, Result) -> + case Config#config.max_tsize of + infinity -> + {Callback, Result}; + Max when Callback#callback.count =< Max -> + {Callback, Result}; + _Max -> + Code = enospc, + Text = "Too large file.", + callback({abort, {Code, Text}}, Config, Callback, Req) + end. + +%%------------------------------------------------------------------- +%% Miscellaneous +%%------------------------------------------------------------------- + +internal_info(Config, Type) when is_record(Config, config) -> + {ok, ActualPort} = inet:port(Config#config.udp_socket), + [ + {type, Type}, + {host, tftp_lib:host_to_string(Config#config.udp_host)}, + {port, Config#config.udp_port}, + {local_port, ActualPort}, + {port_policy, Config#config.port_policy}, + {udp, Config#config.udp_options}, + {use_tsize, Config#config.use_tsize}, + {max_tsize, Config#config.max_tsize}, + {max_conn, Config#config.max_conn}, + {rejected, Config#config.rejected}, + {timeout, Config#config.timeout}, + {polite_ack, Config#config.polite_ack}, + {debug, Config#config.debug_level}, + {parent_pid, Config#config.parent_pid} + ] ++ Config#config.user_options ++ Config#config.callbacks. + +local_file_access(#tftp_msg_req{access = Access, + local_filename = Local, + filename = Filename}) -> + case Local =:= undefined of + true -> + %% Server side + {Access, Filename}; + false -> + %% Client side + case Access of + read -> {write, Local}; + write -> {read, Local} + end + end. + +pre_verify_options(Config, Req) -> + Options = Req#tftp_msg_req.options, + case catch verify_reject(Config, Req, Options) of + ok -> + case verify_integer("tsize", 0, Config#config.max_tsize, Options) of + true -> + case verify_integer("blksize", 0, 65464, Options) of + true -> + ok; + false -> + {error, {badopt, "Too large blksize"}} + end; + false -> + {error, {badopt, "Too large tsize"}} + end; + {error, Reason} -> + {error, Reason} + end. + +post_verify_options(Config, Req, NewOptions, Text) -> + OldOptions = Req#tftp_msg_req.options, + BadOptions = + [Key || {Key, _Val} <- NewOptions, + not lists:keymember(Key, 1, OldOptions)], + case BadOptions =:= [] of + true -> + Config2 = Config#config{timeout = lookup_timeout(NewOptions)}, + Req2 = Req#tftp_msg_req{options = NewOptions}, + {ok, Config2, Req2}; + false -> + {error, {badopt, Text}} + end. + +verify_reject(Config, Req, Options) -> + Access = Req#tftp_msg_req.access, + Rejected = Config#config.rejected, + case lists:member(Access, Rejected) of + true -> + {error, {eacces, atom_to_list(Access) ++ " mode not allowed"}}; + false -> + [throw({error, {badopt, Key ++ " not allowed"}}) || + {Key, _} <- Options, lists:member(Key, Rejected)], + ok + end. + +lookup_timeout(Options) -> + case lists:keysearch("timeout", 1, Options) of + {value, {_, Val}} -> + list_to_integer(Val); + false -> + 3 + end. + +lookup_mode(Options) -> + case lists:keysearch("mode", 1, Options) of + {value, {_, Val}} -> + Val; + false -> + "octet" + end. + +verify_integer(Key, Min, Max, Options) -> + case lists:keysearch(Key, 1, Options) of + {value, {_, Val}} when is_list(Val) -> + case catch list_to_integer(Val) of + {'EXIT', _} -> + false; + Int when Int >= Min, is_integer(Min), + Max =:= infinity -> + true; + Int when Int >= Min, is_integer(Min), + Int =< Max, is_integer(Max) -> + true; + _ -> + false + end; + false -> + true + end. + +error_msg(#config{logger = Logger, debug_level = _Level}, F, A) -> + safe_apply(Logger, error_msg, [F, A]). + +warning_msg(#config{logger = Logger, debug_level = Level}, F, A) -> + case Level of + none -> ok; + error -> ok; + _ -> safe_apply(Logger, warning_msg, [F, A]) + end. + +info_msg(#config{logger = Logger}, F, A) -> + safe_apply(Logger, info_msg, [F, A]). + +safe_apply(Mod, Fun, Args) -> + fast_ensure_loaded(Mod), + apply(Mod, Fun, Args). + +fast_ensure_loaded(Mod) -> + case erlang:function_exported(Mod, module_info, 0) of + true -> + ok; + false -> + Res = code:load_file(Mod), + %% io:format("tftp: code:load_file(~p) -> ~p\n", [Mod, Res]), %% XXX + Res + end. + +print_debug_info(#config{debug_level = Level} = Config, Who, Where, Data) -> + if + Level =:= none -> + ok; + is_record(Data, error) -> + do_print_debug_info(Config, Who, Where, Data); + Level =:= warning -> + ok; + Level =:= error -> + ok; + Level =:= all -> + do_print_debug_info(Config, Who, Where, Data); + Where =:= open -> + do_print_debug_info(Config, Who, Where, Data); + Where =:= close -> + do_print_debug_info(Config, Who, Where, Data); + Level =:= brief -> + ok; + Where =/= recv, Where =/= send -> + ok; + is_record(Data, tftp_msg_data), Level =:= normal -> + ok; + is_record(Data, tftp_msg_ack), Level =:= normal -> + ok; + true -> + do_print_debug_info(Config, Who, Where, Data) + end. + +do_print_debug_info(Config, Who, Where, #tftp_msg_data{data = Bin} = Msg) when is_binary(Bin) -> + Msg2 = Msg#tftp_msg_data{data = {bytes, size(Bin)}}, + do_print_debug_info(Config, Who, Where, Msg2); +do_print_debug_info(Config, Who, Where, #tftp_msg_req{local_filename = Filename} = Msg) when is_binary(Filename) -> + Msg2 = Msg#tftp_msg_req{local_filename = binary}, + do_print_debug_info(Config, Who, Where, Msg2); +do_print_debug_info(Config, Who, Where, Data) -> + Local = + case catch inet:port(Config#config.udp_socket) of + {'EXIT', _Reason} -> + 0; + {ok, Port} -> + Port + end, + %% Remote = Config#config.udp_port, + PeerInfo = lists:flatten(io_lib:format("~p", [peer_info(Config)])), + Side = + if + is_record(Who, tftp_msg_req), + Who#tftp_msg_req.local_filename =/= undefined -> + client; + is_record(Who, tftp_msg_req), + Who#tftp_msg_req.local_filename =:= undefined -> + server; + is_atom(Who) -> + Who + end, + case {Where, Data} of + {_, #error{where = Where, code = Code, text = Text, filename = Filename}} -> + do_format(Config, Side, Local, "error ~s ->\n\t~p ~p\n\t~p ~p: ~s\n", + [PeerInfo, self(), Filename, Where, Code, Text]); + {open, #tftp_msg_req{filename = Filename}} -> + do_format(Config, Side, Local, "open ~s ->\n\t~p ~p\n", + [PeerInfo, self(), Filename]); + {close, #tftp_msg_req{filename = Filename}} -> + do_format(Config, Side, Local, "close ~s ->\n\t~p ~p\n", + [PeerInfo, self(), Filename]); + {recv, _} -> + do_format(Config, Side, Local, "recv ~s <-\n\t~p\n", + [PeerInfo, Data]); + {send, _} -> + do_format(Config, Side, Local, "send ~s ->\n\t~p\n", + [PeerInfo, Data]); + {match, _} when is_record(Data, callback) -> + Mod = Data#callback.module, + State = Data#callback.state, + do_format(Config, Side, Local, "match ~s ~p =>\n\t~p\n", + [PeerInfo, Mod, State]); + {call, _} -> + case Data of + {Callback, _Result} when is_record(Callback, callback) -> + Mod = Callback#callback.module, + State = Callback#callback.state, + do_format(Config, Side, Local, "call ~s ~p =>\n\t~p\n", + [PeerInfo, Mod, State]); + {undefined, Result} -> + do_format(Config, Side, Local, "call ~s result =>\n\t~p\n", + [PeerInfo, Result]) + end + end. + +do_format(Config, Side, Local, Format, Args) -> + info_msg(Config, "~p(~p): " ++ Format, [Side, Local | Args]). + +%%------------------------------------------------------------------- +%% System upgrade +%%------------------------------------------------------------------- + +system_continue(_Parent, _Debug, #sys_misc{module = Mod, function = Fun, arguments = Args}) -> + apply(Mod, Fun, Args); +system_continue(Parent, Debug, {Fun, Args}) -> + %% Handle upgrade from old releases. Please, remove this clause in next release. + system_continue(Parent, Debug, #sys_misc{module = ?MODULE, function = Fun, arguments = Args}). + +-spec system_terminate(_, _, _, #sys_misc{} | {_, _}) -> no_return(). + +system_terminate(Reason, _Parent, _Debug, #sys_misc{}) -> + exit(Reason); +system_terminate(Reason, Parent, Debug, {Fun, Args}) -> + %% Handle upgrade from old releases. Please, remove this clause in next release. + system_terminate(Reason, Parent, Debug, #sys_misc{module = ?MODULE, function = Fun, arguments = Args}). + +system_code_change({Fun, Args}, _Module, _OldVsn, _Extra) -> + {ok, {Fun, Args}}. |