aboutsummaryrefslogtreecommitdiffstats
path: root/lib/tftp/src/tftp_engine.erl
diff options
context:
space:
mode:
Diffstat (limited to 'lib/tftp/src/tftp_engine.erl')
-rw-r--r--lib/tftp/src/tftp_engine.erl1422
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..78a105bcc8
--- /dev/null
+++ b/lib/tftp/src/tftp_engine.erl
@@ -0,0 +1,1422 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2005-2018. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+%%
+%%-------------------------------------------------------------------
+%% 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}}.