diff options
Diffstat (limited to 'lib/tftp/src')
| -rw-r--r-- | lib/tftp/src/Makefile | 110 | ||||
| -rw-r--r-- | lib/tftp/src/tftp.app.src | 22 | ||||
| -rw-r--r-- | lib/tftp/src/tftp.appup.src | 26 | ||||
| -rw-r--r-- | lib/tftp/src/tftp.erl | 409 | ||||
| -rw-r--r-- | lib/tftp/src/tftp.hrl | 69 | ||||
| -rw-r--r-- | lib/tftp/src/tftp_app.erl | 47 | ||||
| -rw-r--r-- | lib/tftp/src/tftp_binary.erl | 239 | ||||
| -rw-r--r-- | lib/tftp/src/tftp_engine.erl | 1422 | ||||
| -rw-r--r-- | lib/tftp/src/tftp_file.erl | 390 | ||||
| -rw-r--r-- | lib/tftp/src/tftp_lib.erl | 474 | ||||
| -rw-r--r-- | lib/tftp/src/tftp_logger.erl | 99 | ||||
| -rw-r--r-- | lib/tftp/src/tftp_sup.erl | 111 | 
12 files changed, 3418 insertions, 0 deletions
| diff --git a/lib/tftp/src/Makefile b/lib/tftp/src/Makefile new file mode 100644 index 0000000000..ed1551ba04 --- /dev/null +++ b/lib/tftp/src/Makefile @@ -0,0 +1,110 @@ +# +# %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% +# +# + +include $(ERL_TOP)/make/target.mk +include $(ERL_TOP)/make/$(TARGET)/otp.mk + +# ---------------------------------------------------- +# Application version +# ---------------------------------------------------- +include ../vsn.mk +VSN = $(TFTP_VSN) + +# ---------------------------------------------------- +# Release directory specification +# ---------------------------------------------------- +RELSYSDIR = $(RELEASE_PATH)/lib/$(APPLICATION)-$(VSN) + +# ---------------------------------------------------- +# Target Specs +# ---------------------------------------------------- +BEHAVIOUR_MODULES= + +MODULES = \ +	tftp \ +	tftp_app \ +	tftp_binary \ +	tftp_engine \ +	tftp_file \ +	tftp_lib \ +	tftp_logger \ +	tftp_sup  + +HRL_FILES = tftp.hrl + +ERL_FILES= \ +	$(MODULES:%=%.erl) \ +	$(BEHAVIOUR_MODULES:%=%.erl) + +TARGET_FILES= $(MODULES:%=$(EBIN)/%.$(EMULATOR)) + +BEHAVIOUR_TARGET_FILES= $(BEHAVIOUR_MODULES:%=$(EBIN)/%.$(EMULATOR)) + +APP_FILE= tftp.app +APPUP_FILE= tftp.appup + +APP_SRC= $(APP_FILE).src +APP_TARGET= $(EBIN)/$(APP_FILE) +APPUP_SRC= $(APPUP_FILE).src +APPUP_TARGET= $(EBIN)/$(APPUP_FILE) + +# ---------------------------------------------------- +# FLAGS +# ---------------------------------------------------- + + +# ---------------------------------------------------- +# Targets +# ---------------------------------------------------- + +$(TARGET_FILES): $(BEHAVIOUR_TARGET_FILES) + +debug opt: $(TARGET_FILES) $(APP_TARGET) $(APPUP_TARGET) + +clean: +	rm -f $(TARGET_FILES) $(APP_TARGET) $(APPUP_TARGET) $(BEHAVIOUR_TARGET_FILES) +	rm -f core + +$(APP_TARGET): $(APP_SRC) ../vsn.mk +	$(vsn_verbose)sed -e 's;%VSN%;$(VSN);' $< > $@ + +$(APPUP_TARGET): $(APPUP_SRC) ../vsn.mk +	$(vsn_verbose)sed -e 's;%VSN%;$(VSN);' $< > $@ + +docs: + +# ---------------------------------------------------- +# Release Target +# ----------------------------------------------------  +include $(ERL_TOP)/make/otp_release_targets.mk + +release_spec: opt +	$(INSTALL_DIR) "$(RELSYSDIR)/src" +	$(INSTALL_DATA) $(ERL_FILES) $(HRL_FILES) "$(RELSYSDIR)/src" +	$(INSTALL_DIR) "$(RELSYSDIR)/ebin" +	$(INSTALL_DATA) $(BEHAVIOUR_TARGET_FILES) $(TARGET_FILES) $(APP_TARGET) \ +	$(APPUP_TARGET) "$(RELSYSDIR)/ebin" + +release_docs_spec: + +info: +	@echo "APPLICATION       = $(APPLICATION)" +	@echo "ERL_COMPILE_FLAGS = $(ERL_COMPILE_FLAGS)" diff --git a/lib/tftp/src/tftp.app.src b/lib/tftp/src/tftp.app.src new file mode 100644 index 0000000000..3f008573e8 --- /dev/null +++ b/lib/tftp/src/tftp.app.src @@ -0,0 +1,22 @@ +{application, tftp, + [{description, "TFTP application"}, +  {vsn, "1.0.0"}, +  {registered, []}, +  {mod, { tftp_app, []}}, +  {applications, +   [kernel, +    stdlib +   ]}, +  {env,[]}, +  {modules, [ +             tftp, +             tftp_app, +             tftp_binary, +             tftp_engine, +             tftp_file, +             tftp_lib, +             tftp_logger, +             tftp_sup +            ]}, +  {runtime_dependencies, ["stdlib-3.5","kernel-6.0"]} + ]}. diff --git a/lib/tftp/src/tftp.appup.src b/lib/tftp/src/tftp.appup.src new file mode 100644 index 0000000000..06a0f0f9dc --- /dev/null +++ b/lib/tftp/src/tftp.appup.src @@ -0,0 +1,26 @@ +%% -*- erlang -*- +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 1999-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% +{"%VSN%", + [ +  {<<".*">>,[{restart_application, tftp}]} + ], + [ +  {<<".*">>,[{restart_application, tftp}]} + ] +}. diff --git a/lib/tftp/src/tftp.erl b/lib/tftp/src/tftp.erl new file mode 100644 index 0000000000..27ed13694b --- /dev/null +++ b/lib/tftp/src/tftp.erl @@ -0,0 +1,409 @@ +%% +%% %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% +%% +%% + +%%%------------------------------------------------------------------- +%%% File    : tftp.erl +%%% Author  : Hakan Mattsson <[email protected]> +%%% Description : Trivial FTP +%%% Created : 18 May 2004 by Hakan Mattsson <[email protected]> +%%%------------------------------------------------------------------- +%%%  +%%% This is a complete implementation of the following IETF standards: +%%% +%%%    RFC 1350, The TFTP Protocol (revision 2). +%%%    RFC 2347, TFTP Option Extension. +%%%    RFC 2348, TFTP Blocksize Option. +%%%    RFC 2349, TFTP Timeout Interval and Transfer Size Options. +%%% +%%% The only feature that not is implemented in this release is +%%% the "netascii" transfer mode. +%%% +%%% The start/1 function starts a daemon process which, listens for +%%% UDP packets on a port. When it receives a request for read or +%%% write it spawns a temporary server process which handles the +%%% actual transfer of the file. On the client side the read_file/3 +%%% and write_file/3 functions spawns a temporary client process which +%%% establishes contact with a TFTP daemon and performs the actual +%%% transfer of the file. +%%% +%%% Most of the options are common for both the client and the server +%%% side, but some of them differs a little. Here are the available +%%% options: +%%%      +%%%   {debug, Level} +%%% +%%%     Level = none | error | warning brief | normal | verbose | all +%%%      +%%%     Controls the level of debug printouts. The default is none. +%%%      +%%%   {host, Host} +%%% +%%%     The name or IP address of the host where the TFTP daemon +%%%     resides. This option is only used by the client. See +%%%     'inet' about valid host names. +%%%      +%%%   {port, Port} +%%% +%%%     Port = integer() +%%%      +%%%     The TFTP port where the daemon listens. It defaults to the +%%%     standardized number 69. On the server side it may sometimes +%%%     make sense to set it to 0, which means that the daemon just +%%%     will pick a free port (which is returned by the start/1 +%%%     function). +%%%      +%%%     If a socket has somehow already has been connected, the +%%%     {udp, [{fd, integer()}]} option can be used to pass the +%%%     open file descriptor to gen_udp. This can be automated +%%%     a bit by using a command line argument stating the +%%%     prebound file descriptor number. For example, if the +%%%     Port is 69 and the file descriptor 22 has been opened by +%%%     setuid_socket_wrap. Then the command line argument +%%%     "-tftpd_69 22" will trigger the prebound file +%%%     descriptor 22 to be used instead of opening port 69. +%%%     The UDP option {udp, [{fd, 22}]} autmatically be added. +%%%     See init:get_argument/ about command line arguments and +%%%     gen_udp:open/2 about UDP options. +%%% +%%%   {port_policy, Policy} +%%% +%%%     Policy = random | Port | {range, MinPort, MaxPort} +%%%     Port = MinPort = MaxPort = integer() +%%%      +%%%     Policy for the selection of the temporary port which is used +%%%     by the server/client during the file transfer. It defaults to +%%%     'random' which is the standardized policy. With this policy a +%%%     randomized free port used. A single port or a range of ports +%%%     can be useful if the protocol should pass thru a firewall. +%%%    +%%%   {prebound_fd, InitArgFlag} +%%% +%%%     InitArgFlag = atom() +%%% +%%%     If a socket has somehow already has been connected, the +%%%     {udp, [{fd, integer()}]} option can be used to pass the +%%%     open file descriptor to gen_udp. +%%% +%%%     The prebound_fd option makes it possible to pass give the +%%%     file descriptor as a command line argument. The typical +%%%     usage is when used in conjunction with setuid_socket_wrap +%%%     to be able to open privileged sockets. For example if the +%%%     file descriptor 22 has been opened by setuid_socket_wrap +%%%     and you have choosen my_tftp_fd as init argument, the +%%%     command line should like this "erl -my_tftp_fd 22" and  +%%%     FileDesc should be set to my_tftpd_fd. This would  +%%%     automatically imply {fd, 22} to be set as UDP option. +%%%    +%%%   {udp, UdpOptions} +%%% +%%%      Options to gen_udp:open/2. +%%% +%%%   {use_tsize, Bool} +%%% +%%%     Bool = boolean() +%%%      +%%%     Flag for automated usage of the "tsize" option. With this set +%%%     to true, the write_file/3 client will determine the filesize +%%%     and send it to the server as the standardized "tsize" option. +%%%     A read_file/3 client will just acquire filesize from the +%%%     server by sending a zero "tsize". +%%%      +%%%   {max_tsize, MaxTsize} +%%% +%%%     MaxTsize = integer() | infinity +%%%      +%%%     Threshold for the maximal filesize in bytes. The transfer will +%%%     be aborted if the limit is exceeded. It defaults to +%%%     'infinity'. +%%% +%%%   {max_conn, MaxConn} +%%%    +%%%     MaxConn = integer() | infinity +%%%      +%%%     Threshold for the maximal number of active connections. The +%%%     daemon will reject the setup of new connections if the limit +%%%     is exceeded. It defaults to 'infinity'. +%%%      +%%%   {TftpKey, TftpVal} +%%% +%%%      TftpKey = string() +%%%      TftpVal = string() +%%% +%%%      The name and value of a TFTP option. +%%%       +%%%   {reject, Feature} +%%%    +%%%      Feature = Mode | TftpKey +%%%      Mode    = read | write +%%%      TftpKey = string() +%%%       +%%%      Control which features that should be rejected. +%%%      This is mostly useful for the server as it may restrict +%%%      usage of certain TFTP options or read/write access. +%%% +%%%   {callback, {RegExp, Module, State}} +%%% +%%%    	 RegExp = string() +%%%    	 Module = atom() +%%%    	 State  = term() +%%%    	  +%%%      Registration of a callback module. When a file is to be +%%%      transferred, its local filename will be matched to the +%%%      regular expressions of the registered callbacks. The first +%%%      matching callback will be used the during the transfer.The +%%%      callback module must implement the 'tftp' behaviour. +%%% +%%%      On the server side the callback interaction starts with a +%%%      call to open/5 with the registered initial callback +%%%      state. open/5 is expected to open the (virtual) file. Then +%%%      either the read/1 or write/2 functions are invoked +%%%      repeatedly, once per transfererred block. At each function +%%%      call the state returned from the previous call is +%%%      obtained. When the last block has been encountered the read/1 +%%%      or write/2 functions is expected to close the (virtual) +%%%      file.and return its last state. The abort/3 function is only +%%%      used in error situations. prepare/5 is not used on the server +%%%      side. +%%%       +%%%      On the client side the callback interaction is the same, but +%%%      it starts and ends a bit differently. It starts with a call +%%%      to prepare/5 with the same arguments as open/5 +%%%      takes. prepare/5 is expected to validate the TFTP options, +%%%      suggested by the user and return the subset of them that it +%%%      accepts. Then the options is sent to the server which will +%%%      perform the same TFTP option negotiation procedure. The +%%%      options that are accepted by the server is forwarded to the +%%%      open/5 function on the client side. On the client side the +%%%      open/5 function must accept all option as is or reject the +%%%      transfer. Then the callback interaction follows the same +%%%      pattern as described above for the server side. When the last +%%%      block is encountered in read/1 or write/2 the returned stated +%%%      is forwarded to the user and returned from read_file/3 or +%%%      write_file/3. +%%%------------------------------------------------------------------- + +-module(tftp). + +%%------------------------------------------------------------------- +%% Interface +%%------------------------------------------------------------------- + +%% Public functions +-export([ +	 read_file/3, +	 write_file/3, +	 start/1, +	 info/1, +	 change_config/2, +	 start/0, +         stop/0 +	]). + +%% Application local functions +-export([ +	 start_standalone/1, +	 start_service/1, +	 stop_service/1,  +	 services/0, +	 service_info/1 +	]). + + +-type peer() :: {PeerType :: inet | inet6, +		 PeerHost :: inet:ip_address(), +		 PeerPort :: port()}. + +-type access() :: read | write. + +-type options() :: [{Key :: string(), Value :: string()}]. + +-type error_code() :: undef | enoent | eacces | enospc | +		      badop | eexist | baduser | badopt | +		      integer(). + +-callback prepare(Peer :: peer(), +		  Access :: access(), +		  Filename :: file:name(), +		  Mode :: string(), +		  SuggestedOptions :: options(), +		  InitialState :: [] | [{root_dir, string()}]) -> +    {ok, AcceptedOptions :: options(), NewState :: term()} | +    {error, {Code :: error_code(), string()}}. + +-callback open(Peer :: peer(), +	       Access :: access(), +	       Filename :: file:name(), +	       Mode :: string(), +	       SuggestedOptions :: options(), +	       State :: [] | [{root_dir, string()}] | term()) -> +    {ok, AcceptedOptions :: options(), NewState :: term()} | +    {error, {Code :: error_code(), string()}}. + +-callback read(State :: term()) -> {more, binary(), NewState :: term()} | +				   {last, binary(), integer()} | +				   {error, {Code :: error_code(), string()}}. + +-callback write(binary(), State :: term()) -> +    {more, NewState :: term()} | +    {last, FileSize :: integer()} | +    {error, {Code :: error_code(), string()}}. + +-callback abort(Code :: error_code(), string(), State :: term()) -> 'ok'. + +-include("tftp.hrl"). + + +%%------------------------------------------------------------------- +%% read_file(RemoteFilename, LocalFilename, Options) -> +%%   {ok, LastCallbackState} | {error, Reason} +%% +%% RemoteFilename     = string() +%% LocalFilename      = binary | string() +%% Options            = [option()] +%% LastCallbackState  = term() +%% Reason             = term() +%% +%% Reads a (virtual) file from a TFTP server +%% +%% If LocalFilename is the atom 'binary', tftp_binary will be used as +%% callback module. It will concatenate all transferred blocks and +%% return them as one single binary in the CallbackState. +%% +%% When LocalFilename is a string, it will be matched to the +%% registered callback modules and hopefully one of them will be +%% selected. By default, tftp_file will be used as callback module. It +%% will write each transferred block to the file named +%% LocalFilename. The number of transferred bytes will be returned as +%% LastCallbackState. +%%------------------------------------------------------------------- + +read_file(RemoteFilename, LocalFilename, Options) -> +    tftp_engine:client_start(read, RemoteFilename, LocalFilename, Options). +     +%%------------------------------------------------------------------- +%% write(RemoteFilename, LocalFilename, Options) -> +%%   {ok, LastCallbackState} | {error, Reason} +%% +%% RemoteFilename    = string() +%% LocalFilename     = binary() | string() +%% Options           = [option()] +%% LastCallbackState = term() +%% Reason            = term() +%% +%% Writes a (virtual) file to a TFTP server +%%  +%% If LocalFilename is a binary, tftp_binary will be used as callback +%% module. The binary will be transferred block by block and the number +%% of transferred bytes will be returned as LastCallbackState. +%% +%% When LocalFilename is a string, it will be matched to the +%% registered callback modules and hopefully one of them will be +%% selected. By default, tftp_file will be used as callback module. It +%% will read the file named LocalFilename block by block. The number +%% of transferred bytes will be returned as LastCallbackState. +%%------------------------------------------------------------------- + +write_file(RemoteFilename, LocalFilename, Options) -> +    tftp_engine:client_start(write, RemoteFilename, LocalFilename, Options). + +%%------------------------------------------------------------------- +%% start(Options) -> {ok, Pid} | {error, Reason} +%%  +%% Options = [option()] +%% Pid     = pid() +%% Reason  = term() +%% +%% Starts a daemon process which listens for udp packets on a +%% port. When it receives a request for read or write it spawns +%% a temporary server process which handles the actual transfer +%% of the (virtual) file. +%%------------------------------------------------------------------- + +start(Options) -> +    tftp_engine:daemon_start(Options). + +%%------------------------------------------------------------------- +%% info(Pid) -> {ok, Options} | {error, Reason} +%%  +%% Options = [option()] +%% Reason  = term() +%% +%% Returns info about a tftp daemon, server or client process +%%------------------------------------------------------------------- + +info(Pid) -> +    tftp_engine:info(Pid). + +%%------------------------------------------------------------------- +%% change_config(Pid, Options) -> ok | {error, Reason} +%%  +%% Options = [option()] +%% Reason  = term() +%% +%% Changes config for a tftp daemon, server or client process +%% Must be used with care. +%%------------------------------------------------------------------- + +change_config(Pid, Options) -> +    tftp_engine:change_config(Pid, Options). + +%%------------------------------------------------------------------- +%% start() -> ok | {error, Reason} +%%  +%% Reason = term() +%% +%% Start the application +%%------------------------------------------------------------------- + +start() -> +    application:start(tftp). + +%%------------------------------------------------------------------- +%% stop() -> ok | {error, Reason} +%%  +%% Reason = term() +%% +%% Stop the application +%%------------------------------------------------------------------- +stop() -> +    application:stop(tftp). + +%%------------------------------------------------------------------- +%% Inets service behavior +%%------------------------------------------------------------------- + +start_standalone(Options) -> +    start(Options). + +start_service(Options) -> +    tftp_sup:start_child(Options). + +stop_service(Pid) -> +    tftp_sup:stop_child(Pid). + +services() -> +    tftp_sup:which_children(). + +service_info(Pid) -> +    info(Pid). +	      +	      +        diff --git a/lib/tftp/src/tftp.hrl b/lib/tftp/src/tftp.hrl new file mode 100644 index 0000000000..25543e0b9e --- /dev/null +++ b/lib/tftp/src/tftp.hrl @@ -0,0 +1,69 @@ +%% +%% %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% +%% +%% + +%%%------------------------------------------------------------------- +%%% Defines +%%%------------------------------------------------------------------- + +-define(TFTP_DEFAULT_PORT,  69).% Default server port + +-define(TFTP_OPCODE_RRQ,    1). % Read request +-define(TFTP_OPCODE_WRQ,    2). % Write request +-define(TFTP_OPCODE_DATA,   3). % Data +-define(TFTP_OPCODE_ACK,    4). % Acknowledgement +-define(TFTP_OPCODE_ERROR,  5). % Error +-define(TFTP_OPCODE_OACK,   6). % Option acknowledgment + +-define(TFTP_ERROR_UNDEF,   0). % Not defined, see error message (if any) +-define(TFTP_ERROR_ENOENT,  1). % File not found. +-define(TFTP_ERROR_EACCES,  2). % Access violation. +-define(TFTP_ERROR_ENOSPC,  3). % Disk full or allocation exceeded. +-define(TFTP_ERROR_BADOP,   4). % Illegal TFTP operation. +-define(TFTP_ERROR_BADBLK,  5). % Unknown transfer ID. +-define(TFTP_ERROR_EEXIST,  6). % File already exists. +-define(TFTP_ERROR_BADUSER, 7). % No such user. +-define(TFTP_ERROR_BADOPT,  8). % Unrequested or illegal option. + +-record(tftp_msg_req,     {access, filename, mode, options, local_filename}). +-record(tftp_msg_data,    {block_no, data}). +-record(tftp_msg_ack,     {block_no}). +-record(tftp_msg_error,   {code, text, details}). +-record(tftp_msg_oack,    {options}). + +-record(config, {parent_pid   = self(), +		 udp_socket, +		 udp_options  = [binary, {reuseaddr, true}, {active, once}], +		 udp_host     = "localhost", +		 udp_port     = ?TFTP_DEFAULT_PORT, +		 port_policy  = random, +		 use_tsize    = false, +		 max_tsize    = infinity, % Filesize +		 max_conn     = infinity, +		 rejected     = [], +		 polite_ack   = false, +		 debug_level  = none, +		 timeout, +		 user_options = [], +		 callbacks    = [], +		 logger       = tftp_logger, +		 max_retries  = 5}). + +-record(callback, {regexp, internal, module, state, block_no, count}). diff --git a/lib/tftp/src/tftp_app.erl b/lib/tftp/src/tftp_app.erl new file mode 100644 index 0000000000..bbcd107e30 --- /dev/null +++ b/lib/tftp/src/tftp_app.erl @@ -0,0 +1,47 @@ +%% +%% %CopyrightBegin% +%%  +%% Copyright Ericsson AB 2002-2018. All Rights Reserved. +%%  +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%%     http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%  +%% %CopyrightEnd% +%% +%% + +%%%------------------------------------------------------------------- +%% @doc ftp public API +%% @end +%%%------------------------------------------------------------------- + +-module(tftp_app). + +-behaviour(application). + +%% Application callbacks +-export([start/2, stop/1]). + +%%==================================================================== +%% API +%%==================================================================== + +start(_StartType, _StartArgs) -> +    tftp_sup:start_link([]). + +%%-------------------------------------------------------------------- +stop(_State) -> +    ok. + +%%==================================================================== +%% Internal functions +%%==================================================================== diff --git a/lib/tftp/src/tftp_binary.erl b/lib/tftp/src/tftp_binary.erl new file mode 100644 index 0000000000..09adcfc41f --- /dev/null +++ b/lib/tftp/src/tftp_binary.erl @@ -0,0 +1,239 @@ +%% +%% %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% +%% +%% + +%%%------------------------------------------------------------------- +%%% File    : tft_binary.erl +%%% Author  : Hakan Mattsson <[email protected]> +%%% Description :  +%%% +%%% Created : 24 May 2004 by Hakan Mattsson <[email protected]> +%%%------------------------------------------------------------------- + +-module(tftp_binary). + +%%%------------------------------------------------------------------- +%%% Interface +%%%------------------------------------------------------------------- + +-behaviour(tftp). + +-export([prepare/6, open/6, read/1, write/2, abort/3]). + +-record(read_state,  {options, blksize, bin,  is_native_ascii, is_network_ascii, count}). +-record(write_state, {options, blksize, list, is_native_ascii, is_network_ascii}). + +%%------------------------------------------------------------------- +%% Prepare +%%------------------------------------------------------------------- + +prepare(_Peer, Access, Filename, Mode, SuggestedOptions, Initial) when is_list(Initial) -> +    %% Client side +    IsNativeAscii = is_native_ascii(Initial), +    case catch handle_options(Access, Filename, Mode, SuggestedOptions, IsNativeAscii) of +	{ok, IsNetworkAscii, AcceptedOptions} when Access =:= read, is_binary(Filename) -> +	    State = #read_state{options  	 = AcceptedOptions, +				blksize  	 = lookup_blksize(AcceptedOptions), +				bin      	 = Filename, +				is_network_ascii = IsNetworkAscii, +			        count            = size(Filename), +				is_native_ascii  = IsNativeAscii}, +	    {ok, AcceptedOptions, State}; +	{ok, IsNetworkAscii, AcceptedOptions} when Access =:= write, Filename =:= binary -> +	    State = #write_state{options  	  = AcceptedOptions, +				 blksize  	  = lookup_blksize(AcceptedOptions), +				 list     	  = [], +				 is_network_ascii = IsNetworkAscii, +				 is_native_ascii  = IsNativeAscii}, +	    {ok, AcceptedOptions, State}; +	{ok, _, _} -> +	    {error, {undef, "Illegal callback usage. Mode and filename is incompatible."}}; +	{error, {Code, Text}} -> +	    {error, {Code, Text}} +    end; +prepare(_Peer, _Access, _Bin, _Mode, _SuggestedOptions, _Initial) -> +    {error, {undef, "Illegal callback options."}}. + +%%------------------------------------------------------------------- +%% Open +%%------------------------------------------------------------------- + +open(Peer, Access, Filename, Mode, SuggestedOptions, Initial) when is_list(Initial) -> +    %% Server side +    case prepare(Peer, Access, Filename, Mode, SuggestedOptions, Initial) of +	{ok, AcceptedOptions, State} -> +	    open(Peer, Access, Filename, Mode, AcceptedOptions, State); +	{error, {Code, Text}} -> +	    {error, {Code, Text}} +    end; +open(_Peer, Access, Filename, Mode, NegotiatedOptions, State) when is_record(State, read_state) -> +    %% Both sides +    case catch handle_options(Access, Filename, Mode, NegotiatedOptions, State#read_state.is_native_ascii) of +	{ok, IsNetworkAscii, Options} +	when Options =:= NegotiatedOptions, +	     IsNetworkAscii =:= State#read_state.is_network_ascii -> +	    {ok, NegotiatedOptions, State}; +	{error, {Code, Text}} -> +	    {error, {Code, Text}} +    end; +open(_Peer, Access, Filename, Mode, NegotiatedOptions, State) when is_record(State, write_state) -> +    %% Both sides +    case catch handle_options(Access, Filename, Mode, NegotiatedOptions, State#write_state.is_native_ascii) of +	{ok, IsNetworkAscii, Options} +	when Options =:= NegotiatedOptions, +	     IsNetworkAscii =:= State#write_state.is_network_ascii -> +	    {ok, NegotiatedOptions, State}; +	{error, {Code, Text}} -> +	    {error, {Code, Text}} +    end; +open(Peer, Access, Filename, Mode, NegotiatedOptions, State) ->  +    %% Handle upgrade from old releases. Please, remove this clause in next release. +    State2 = upgrade_state(State), +    open(Peer, Access, Filename, Mode, NegotiatedOptions, State2). + +%%------------------------------------------------------------------- +%% Read +%%------------------------------------------------------------------- + +read(#read_state{bin = Bin} = State) when is_binary(Bin) -> +    BlkSize = State#read_state.blksize, +    if +	size(Bin) >= BlkSize -> +	    <<Block:BlkSize/binary, Bin2/binary>> = Bin, +	    State2 = State#read_state{bin = Bin2}, +	    {more, Block, State2}; +	size(Bin) < BlkSize -> +	    {last, Bin, State#read_state.count} +    end; +read(State) -> +    %% Handle upgrade from old releases. Please, remove this clause in next release. +    State2 = upgrade_state(State), +    read(State2). + +%%------------------------------------------------------------------- +%% Write +%%------------------------------------------------------------------- + +write(Bin, #write_state{list = List} = State) when is_binary(Bin), is_list(List) -> +    Size = size(Bin), +    BlkSize = State#write_state.blksize, +    if +	Size =:= BlkSize -> +	    {more, State#write_state{list = [Bin | List]}}; +	Size < BlkSize -> +	    Bin2 = list_to_binary(lists:reverse([Bin | List])), +	    {last, Bin2} +    end; +write(Bin, State) -> +    %% Handle upgrade from old releases. Please, remove this clause in next release. +    State2 = upgrade_state(State), +    write(Bin, State2). + +%%------------------------------------------------------------------- +%% Abort +%%------------------------------------------------------------------- + +abort(_Code, _Text, #read_state{bin = Bin} = State)  +  when is_record(State, read_state), is_binary(Bin) -> +    ok; +abort(_Code, _Text, #write_state{list = List} = State) +  when is_record(State, write_state), is_list(List) -> +    ok; +abort(Code, Text, State) -> +    %% Handle upgrade from old releases. Please, remove this clause in next release. +    State2 = upgrade_state(State), +    abort(Code, Text, State2). + +%%------------------------------------------------------------------- +%% Process options +%%------------------------------------------------------------------- + +handle_options(Access, Bin, Mode, Options, IsNativeAscii) -> +    IsNetworkAscii = handle_mode(Mode, IsNativeAscii), +    Options2 = do_handle_options(Access, Bin, Options), +    {ok, IsNetworkAscii, Options2}. + +handle_mode(Mode, IsNativeAscii) -> +    case Mode of +	"netascii" when IsNativeAscii =:= true -> true; +	"octet" -> false; +	_ -> throw({error, {badop, "Illegal mode " ++ Mode}}) +    end. + +do_handle_options(Access, Bin, [{Key, Val} | T]) -> +    case Key of +	"tsize" -> +	    case Access of +		read when Val =:= "0", is_binary(Bin) -> +		    Tsize = integer_to_list(size(Bin)), +		    [{Key, Tsize} | do_handle_options(Access, Bin, T)]; +		_ -> +		    handle_integer(Access, Bin, Key, Val, T, 0, infinity) +	    end; +	"blksize" -> +	    handle_integer(Access, Bin, Key, Val, T, 8, 65464); +	"timeout" -> +	    handle_integer(Access, Bin, Key, Val, T, 1, 255); +	_ -> +	    do_handle_options(Access, Bin, T) +    end; +do_handle_options(_Access, _Bin, []) -> +    []. + + +handle_integer(Access, Bin, Key, Val, Options, Min, Max) -> +    case catch list_to_integer(Val) of +	{'EXIT', _} -> +	    do_handle_options(Access, Bin, Options); +	Int when Int >= Min, Int =< Max -> +	    [{Key, Val} | do_handle_options(Access, Bin, Options)]; +	Int when Int >= Min, Max =:= infinity -> +	    [{Key, Val} | do_handle_options(Access, Bin, Options)]; +	_Int -> +	    throw({error, {badopt, "Illegal " ++ Key ++ " value " ++ Val}}) +    end. + +lookup_blksize(Options) -> +    case lists:keysearch("blksize", 1, Options) of +	{value, {_, Val}} -> +	    list_to_integer(Val); +	false -> +	    512 +    end. + +is_native_ascii([]) -> +    is_native_ascii(); +is_native_ascii([{native_ascii, Bool}]) -> +    case Bool of +	true  -> true; +	false -> false +    end. + +is_native_ascii() -> +    case os:type() of +	{win32, _} -> true; +	_          -> false +    end. +     +%% Handle upgrade from old releases. Please, remove this function in next release. +upgrade_state({read_state,  Options, Blksize, Bin, IsNetworkAscii, Count}) -> +    {read_state,  Options, Blksize, Bin, false, IsNetworkAscii, Count}; +upgrade_state({write_state, Options, Blksize, List, IsNetworkAscii}) -> +    {write_state, Options, Blksize, List, false, IsNetworkAscii}. 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}}. diff --git a/lib/tftp/src/tftp_file.erl b/lib/tftp/src/tftp_file.erl new file mode 100644 index 0000000000..7664324808 --- /dev/null +++ b/lib/tftp/src/tftp_file.erl @@ -0,0 +1,390 @@ +%% +%% %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% +%% +%% + +%%%------------------------------------------------------------------- +%%% File    : tft_file.erl +%%% Author  : Hakan Mattsson <[email protected]> +%%% Description :  +%%% +%%% Created : 24 May 2004 by Hakan Mattsson <[email protected]> +%%%------------------------------------------------------------------- + +-module(tftp_file). + +%%%------------------------------------------------------------------- +%%% Interface +%%%------------------------------------------------------------------- + +-behaviour(tftp). + +-export([prepare/6, open/6, read/1, write/2, abort/3]). + +%%%------------------------------------------------------------------- +%%% Defines +%%%------------------------------------------------------------------- + +-include_lib("kernel/include/file.hrl"). + +-record(initial, +	{filename, +	 is_native_ascii}). + +-record(state, +	{access, +	 filename, +	 is_native_ascii, +	 is_network_ascii, +	 root_dir, +	 options, +	 blksize, +	 fd, +	 count, +	 buffer}). + +%%------------------------------------------------------------------- +%% prepare(Peer, Access, Filename, Mode, SuggestedOptions, InitialState) ->  +%%    {ok, AcceptedOptions, NewState} | {error, Code, Text} +%% +%% Peer             = {PeerType, PeerHost, PeerPort} +%% PeerType         = inet | inet6 +%% PeerHost         = ip_address() +%% PeerPort         = integer() +%% Acess            = read | write +%% Filename         = string() +%% Mode             = string() +%% SuggestedOptions = [{Key, Value}] +%% AcceptedOptions  = [{Key, Value}] +%% Key              = string() +%% Value            = string() +%% InitialState     = [] | [{root_dir, string()}] +%% NewState         = term() +%% Code             = undef | enoent | eacces | enospc | +%%                    badop | eexist | baduser | badopt | +%%                    integer() +%% Text             = string() +%% +%% Prepares open of a file on the client side. +%%  +%% Will be followed by a call to open/4 before any read/write access +%% is performed. The AcceptedOptions will be sent to the server which +%% will reply with those options that it accepts. The options that are +%% accepted by the server will be forwarded to open/4 as SuggestedOptions. +%% +%% No new options may be added, but the ones that are present as +%% SuggestedOptions may be omitted or replaced with new values +%% in the AcceptedOptions. +%%------------------------------------------------------------------- + +prepare(_Peer, Access, Filename, Mode, SuggestedOptions, Initial) when is_list(Initial) -> +    %% Client side +    case catch handle_options(Access, Filename, Mode, SuggestedOptions, Initial) of +	{ok, Filename2, IsNativeAscii, IsNetworkAscii, AcceptedOptions} -> +	    State = #state{access           = Access, +			   filename         = Filename2, +			   is_native_ascii  = IsNativeAscii, +			   is_network_ascii = IsNetworkAscii, +			   options  	    = AcceptedOptions, +			   blksize  	    = lookup_blksize(AcceptedOptions), +			   count    	    = 0, +			   buffer   	   =  []}, +	    {ok, AcceptedOptions, State}; +	{error, {Code, Text}} -> +	    {error, {Code, Text}} +    end. + +%% --------------------------------------------------------- +%% open(Peer, Access, Filename, Mode, SuggestedOptions, State) ->  +%%    {ok, AcceptedOptions, NewState} | {error, Code, Text} +%% +%% Peer             = {PeerType, PeerHost, PeerPort} +%% PeerType         = inet | inet6 +%% PeerHost         = ip_address() +%% PeerPort         = integer() +%% Acess            = read | write +%% Filename         = string() +%% Mode             = string() +%% SuggestedOptions = [{Key, Value}] +%% AcceptedOptions  = [{Key, Value}] +%% Key              = string() +%% Value            = string() +%% State            = InitialState | #state{} +%% InitialState     = [] | [{root_dir, string()}] +%% NewState         = term() +%% Code             = undef | enoent | eacces  | enospc | +%%                    badop | eexist | baduser | badopt | +%%                    integer() +%% Text             = string() +%% +%% Opens a file for read or write access. +%%  +%% On the client side where the open/4 call has been preceeded by a +%% call to prepare/4, all options must be accepted or rejected. +%% On the server side, where there are no preceeding prepare/4 call, +%% noo new options may be added, but the ones that are present as +%% SuggestedOptions may be omitted or replaced with new values +%% in the AcceptedOptions. +%%------------------------------------------------------------------- + +open(Peer, Access, Filename, Mode, SuggestedOptions, Initial) when is_list(Initial) -> +    %% Server side +    case prepare(Peer, Access, Filename, Mode, SuggestedOptions, Initial) of +	{ok, AcceptedOptions, State} -> +	    open(Peer, Access, Filename, Mode, AcceptedOptions, State); +	{error, {Code, Text}} -> +	    {error, {Code, Text}} +    end; +open(_Peer, Access, Filename, Mode, NegotiatedOptions, State) when is_record(State, state) -> +    %% Both sides +    case catch handle_options(Access, Filename, Mode, NegotiatedOptions, State) of +	{ok, _Filename2, _IsNativeAscii, _IsNetworkAscii, Options}  +	   when Options =:= NegotiatedOptions -> +	    do_open(State); +	{error, {Code, Text}} -> +	    {error, {Code, Text}} +    end; +open(Peer, Access, Filename, Mode, NegotiatedOptions, State) -> +    %% Handle upgrade from old releases. Please, remove this clause in next release. +    State2 = upgrade_state(State), +    open(Peer, Access, Filename, Mode, NegotiatedOptions, State2). + +do_open(State) when is_record(State, state) -> +    case file:open(State#state.filename, file_options(State)) of +	{ok, Fd} -> +	    {ok, State#state.options, State#state{fd = Fd}}; +	{error, Reason} when is_atom(Reason) -> +	    {error, file_error(Reason)} +    end. +	 +file_options(State) -> +    case State#state.access of +	read  -> [read, read_ahead, raw, binary]; +	write -> [write, delayed_write, raw, binary] +    end. + +file_error(Reason) when is_atom(Reason) -> +    Details = file:format_error(Reason), +    case Reason of +	eexist -> {Reason, Details}; +	enoent -> {Reason, Details}; +	eacces -> {Reason, Details}; +	eperm  -> {eacces, Details}; +	enospc -> {Reason, Details}; +	_      -> {undef,  Details ++ " (" ++ atom_to_list(Reason) ++ ")"} +    end. + +%%------------------------------------------------------------------- +%% read(State) -> +%%   {more, Bin, NewState} | {last, Bin, FileSize} | {error, {Code, Text}} +%% +%% State    = term() +%% NewState = term() +%% Bin      = binary() +%% FileSize = integer() +%% Code     = undef | enoent | eacces  | enospc | +%%            badop | eexist | baduser | badopt | +%%            integer() +%% Text     = string() +%% +%% Reads a chunk from the file +%%  +%% The file is automatically closed when the last chunk is read. +%%------------------------------------------------------------------- + +read(#state{access = read} = State) -> +    BlkSize = State#state.blksize, +    case file:read(State#state.fd, BlkSize) of +	{ok, Bin} when is_binary(Bin), size(Bin) =:= BlkSize -> +	    Count = State#state.count + size(Bin), +	    {more, Bin, State#state{count = Count}}; +	{ok, Bin} when is_binary(Bin), size(Bin) < BlkSize -> +	    file:close(State#state.fd), +	    Count = State#state.count + size(Bin), +	    {last, Bin, Count}; +	eof -> +	    {last, <<>>, State#state.count}; +	{error, Reason} -> +	    file:close(State#state.fd), +	    {error, file_error(Reason)} +    end; +read(State) -> +    %% Handle upgrade from old releases. Please, remove this clause in next release. +    State2 = upgrade_state(State), +    read(State2). + +%%------------------------------------------------------------------- +%% write(Bin, State) -> +%%   {more, NewState} | {last, FileSize} | {error, {Code, Text}} +%% +%% State    = term() +%% NewState = term() +%% Bin      = binary() +%% FileSize = integer() +%% Code     = undef | enoent | eacces  | enospc | +%%            badop | eexist | baduser | badopt | +%%            integer() +%% Text     = string() +%% +%% Writes a chunk to the file +%% +%% The file is automatically closed when the last chunk is written +%%------------------------------------------------------------------- + +write(Bin, #state{access = write} = State) when is_binary(Bin) -> +    Size = size(Bin), +    BlkSize = State#state.blksize, +    case file:write(State#state.fd, Bin) of +	ok when Size =:= BlkSize-> +	    Count = State#state.count + Size, +	    {more, State#state{count = Count}}; +	ok when Size < BlkSize-> +	    file:close(State#state.fd), +	    Count = State#state.count + Size, +	    {last, Count}; +	{error, Reason}  -> +	    file:close(State#state.fd), +	    file:delete(State#state.filename), +	    {error, file_error(Reason)} +    end; +write(Bin, State) -> +    %% Handle upgrade from old releases. Please, remove this clause in next release. +    State2 = upgrade_state(State), +    write(Bin, State2). + +%%------------------------------------------------------------------- +%% abort(Code, Text, State) -> ok +%%  +%% State    = term() +%% Code     = undef  | enoent | eacces  | enospc | +%%            badop  | eexist | baduser | badopt | +%%            badblk | integer() +%% Text     = string() +%% +%% Aborts the file transfer +%%------------------------------------------------------------------- + +abort(_Code, _Text, #state{fd = Fd, access = Access} = State) -> +    file:close(Fd), +    case Access of +	write -> +	    ok = file:delete(State#state.filename); +	read -> +	    ok +    end. + +%%------------------------------------------------------------------- +%% Process options +%%------------------------------------------------------------------- + +handle_options(Access, Filename, Mode, Options, Initial) -> +    I = #initial{filename = Filename, is_native_ascii = is_native_ascii()}, +    {Filename2, IsNativeAscii} = handle_initial(Initial, I), +    IsNetworkAscii = handle_mode(Mode, IsNativeAscii), +    Options2 = do_handle_options(Access, Filename2, Options), +    {ok, Filename2, IsNativeAscii, IsNetworkAscii, Options2}. + +handle_mode(Mode, IsNativeAscii) -> +    case Mode of +	"netascii" when IsNativeAscii =:= true -> true; +	"octet" -> false; +	_ -> throw({error, {badop, "Illegal mode " ++ Mode}}) +    end. + +handle_initial([{root_dir, Dir} | Initial], I) -> +    case catch filename_join(Dir, I#initial.filename) of +	{'EXIT', _} -> +	    throw({error, {badop, "Internal error. root_dir is not a string"}}); +	Filename2 -> +	    handle_initial(Initial, I#initial{filename = Filename2}) +    end; +handle_initial([{native_ascii, Bool} | Initial], I) -> +    case Bool of +	true  -> handle_initial(Initial, I#initial{is_native_ascii = true}); +	false -> handle_initial(Initial, I#initial{is_native_ascii = false}) +    end; +handle_initial([], I) when is_record(I, initial) -> +    {I#initial.filename, I#initial.is_native_ascii}; +handle_initial(State, _) when is_record(State, state) -> +    {State#state.filename, State#state.is_native_ascii}. + +filename_join(Dir, Filename) -> +    case filename:pathtype(Filename) of +	absolute -> +	    [_ | RelFilename] = filename:split(Filename), +	    filename:join([Dir, RelFilename]); +	_ -> +	    filename:join([Dir, Filename]) +    end. + +do_handle_options(Access, Filename, [{Key, Val} | T]) -> +    case Key of +	"tsize" -> +	    case Access of +		read when Val =:= "0" -> +		    case file:read_file_info(Filename) of +			{ok, FI} -> +			    Tsize = integer_to_list(FI#file_info.size), +			    [{Key, Tsize} | do_handle_options(Access, Filename, T)]; +			{error, _} -> +			    do_handle_options(Access, Filename, T) +		    end; +		_ -> +		    handle_integer(Access, Filename, Key, Val, T, 0, infinity) +	    end; +	"blksize" -> +	    handle_integer(Access, Filename, Key, Val, T, 8, 65464); +	"timeout" -> +	    handle_integer(Access, Filename, Key, Val, T, 1, 255); +	_ -> +	    do_handle_options(Access, Filename, T) +    end; +do_handle_options(_Access, _Filename, []) -> +    []. + + +handle_integer(Access, Filename, Key, Val, Options, Min, Max) -> +    case catch list_to_integer(Val) of +	{'EXIT', _} -> +	    do_handle_options(Access, Filename, Options); +	Int when Int >= Min, Int =< Max -> +	    [{Key, Val} | do_handle_options(Access, Filename, Options)]; +	Int when Int >= Min, Max =:= infinity -> +	    [{Key, Val} | do_handle_options(Access, Filename, Options)]; +	_Int -> +	    throw({error, {badopt, "Illegal " ++ Key ++ " value " ++ Val}}) +    end. + +lookup_blksize(Options) -> +    case lists:keysearch("blksize", 1, Options) of +	{value, {_, Val}} -> +	    list_to_integer(Val); +	false -> +	    512 +    end. + +is_native_ascii() -> +    case os:type() of +	{win32, _} -> true; +	_          -> false +    end. + +%% Handle upgrade from old releases. Please, remove this function in next release. +upgrade_state({state, Access, Filename, RootDir, Options, BlkSize, Fd, Count, Buffer}) -> +    {state, Access, Filename, false, false, RootDir, Options, BlkSize, Fd, Count, Buffer}. diff --git a/lib/tftp/src/tftp_lib.erl b/lib/tftp/src/tftp_lib.erl new file mode 100644 index 0000000000..454754f0a3 --- /dev/null +++ b/lib/tftp/src/tftp_lib.erl @@ -0,0 +1,474 @@ +%% +%% %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% +%% +%% + +%%%------------------------------------------------------------------- +%%% File    : tftp_lib.erl +%%% Author  : Hakan Mattsson <[email protected]> +%%% Description : Option parsing, decode, encode etc. +%%% +%%% Created : 18 May 2004 by Hakan Mattsson <[email protected]> +%%%------------------------------------------------------------------- + +-module(tftp_lib). + +%%------------------------------------------------------------------- +%% Interface +%%------------------------------------------------------------------- + +%% application internal functions +-export([ +         parse_config/1, +         parse_config/2, +         decode_msg/1, +         encode_msg/1, +         replace_val/3, +         to_lower/1, +         host_to_string/1, +	 add_default_callbacks/1 +        ]). + +%%------------------------------------------------------------------- +%% Defines +%%------------------------------------------------------------------- + +-include("tftp.hrl"). + +-define(LOWER(Char), +        if +            Char >= $A, Char =< $Z -> +                Char - ($A - $a); +            true -> +                Char +        end). + +%%------------------------------------------------------------------- +%% Config +%%------------------------------------------------------------------- + +parse_config(Options) -> +    parse_config(Options, #config{}). + +parse_config(Options, Config) -> +    do_parse_config(Options, Config). + +do_parse_config([{Key, Val} | Tail], Config) when is_record(Config, config) -> +    case Key of +        debug -> +            if +                Val =:= 0; Val =:= none -> +                    do_parse_config(Tail, Config#config{debug_level = none}); +                Val =:= 1; Val =:= error -> +                    do_parse_config(Tail, Config#config{debug_level = error}); +                Val =:= 2; Val =:= warning -> +                    do_parse_config(Tail, Config#config{debug_level = warning}); +                Val =:= 3; Val =:= brief -> +                    do_parse_config(Tail, Config#config{debug_level = brief}); +                Val =:= 4; Val =:= normal -> +                    do_parse_config(Tail, Config#config{debug_level = normal}); +                Val =:= 5; Val =:= verbose -> +                    do_parse_config(Tail, Config#config{debug_level = verbose}); +                Val =:= 6; Val =:= all -> +                    do_parse_config(Tail, Config#config{debug_level = all}); +                true -> +                    exit({badarg, {Key, Val}}) +            end; +        host -> +            if +                is_list(Val) -> +                    do_parse_config(Tail, Config#config{udp_host = Val}); +                is_tuple(Val), size(Val) =:= 4 -> +                    do_parse_config(Tail, Config#config{udp_host = Val}); +                is_tuple(Val), size(Val) =:= 8 -> +                    do_parse_config(Tail, Config#config{udp_host = Val}); +                true -> +                    exit({badarg, {Key, Val}}) +            end; +        port -> +            if +                is_integer(Val), Val >= 0 -> +                    Config2 = Config#config{udp_port = Val, udp_options = Config#config.udp_options}, +                    do_parse_config(Tail, Config2); +                true -> +                    exit({badarg, {Key, Val}}) +            end; +        port_policy -> +            case Val of +                random -> +                    do_parse_config(Tail, Config#config{port_policy = Val}); +                0 -> +                    do_parse_config(Tail, Config#config{port_policy = random}); +                MinMax when is_integer(MinMax), MinMax > 0 -> +                    do_parse_config(Tail, Config#config{port_policy = {range, MinMax, MinMax}}); +                {range, Min, Max} when Max >= Min,  +                is_integer(Min), Min > 0, +                is_integer(Max), Max > 0 -> +                    do_parse_config(Tail, Config#config{port_policy = Val}); +                true -> +                    exit({badarg, {Key, Val}}) +            end; +        udp when is_list(Val) -> +            Fun =   +                fun({K, V}, List) when K /= active ->  +                        replace_val(K, V, List); +                   (V, List) when V /= list, V /= binary -> +                        List ++ [V]; +                   (V, _List) -> +                        exit({badarg, {udp, [V]}}) +                end, +            UdpOptions = lists:foldl(Fun, Config#config.udp_options, Val), +            do_parse_config(Tail, Config#config{udp_options = UdpOptions}); +        use_tsize -> +            case Val of +                true -> +                    do_parse_config(Tail, Config#config{use_tsize = Val}); +                false -> +                    do_parse_config(Tail, Config#config{use_tsize = Val}); +                _ -> +                    exit({badarg, {Key, Val}}) +            end; +        max_tsize -> +            if +                Val =:= infinity -> +                    do_parse_config(Tail, Config#config{max_tsize = Val}); +                is_integer(Val), Val >= 0 -> +                    do_parse_config(Tail, Config#config{max_tsize = Val}); +                true -> +                    exit({badarg, {Key, Val}}) +            end; +        max_conn -> +            if +                Val =:= infinity -> +                    do_parse_config(Tail, Config#config{max_conn = Val}); +                is_integer(Val), Val > 0 -> +                    do_parse_config(Tail, Config#config{max_conn = Val}); +                true -> +                    exit({badarg, {Key, Val}}) +            end; +        _ when is_list(Key), is_list(Val) -> +            Key2 = to_lower(Key), +            Val2 = to_lower(Val), +            TftpOptions = replace_val(Key2, Val2, Config#config.user_options), +            do_parse_config(Tail, Config#config{user_options = TftpOptions}); +        reject -> +            case Val of +                read -> +                    Rejected = [Val | Config#config.rejected], +                    do_parse_config(Tail, Config#config{rejected = Rejected}); +                write -> +                    Rejected = [Val | Config#config.rejected], +                    do_parse_config(Tail, Config#config{rejected = Rejected}); +                _ when is_list(Val) -> +                    Rejected = [Val | Config#config.rejected], +                    do_parse_config(Tail, Config#config{rejected = Rejected}); +                _ -> +                    exit({badarg, {Key, Val}}) +            end; +        callback -> +            case Val of +                {RegExp, Mod, State} when is_list(RegExp), is_atom(Mod) -> +                    case re:compile(RegExp) of +                        {ok, Internal} -> +                            Callback = #callback{regexp   = RegExp, +                                                 internal = Internal, +                                                 module   = Mod, +                                                 state    = State}, +                            Callbacks = Config#config.callbacks ++ [Callback], +                            do_parse_config(Tail, Config#config{callbacks = Callbacks}); +                        {error, Reason} -> +                            exit({badarg, {Key, Val}, Reason}) +                    end; +                _ -> +                    exit({badarg, {Key, Val}}) +            end; +        logger -> +            if +                is_atom(Val) -> +                    do_parse_config(Tail, Config#config{logger = Val}); +                true -> +                    exit({badarg, {Key, Val}}) +            end; +        max_retries -> +            if +                is_integer(Val), Val > 0 -> +                    do_parse_config(Tail, Config#config{max_retries = Val}); +                true -> +                    exit({badarg, {Key, Val}}) +            end; +        _ -> +            exit({badarg, {Key, Val}}) +    end; +do_parse_config([], #config{udp_host     = Host, +                            udp_options  = UdpOptions, +                            user_options = UserOptions, +                            callbacks    = Callbacks} = Config) -> +    IsInet6 = lists:member(inet6, UdpOptions), +    IsInet  = lists:member(inet, UdpOptions), +    Host2 =  +        if +            (IsInet and not IsInet6); (not IsInet and not IsInet6) ->  +                case inet:getaddr(Host, inet) of +                    {ok, Addr} -> +                        Addr; +                    {error, Reason} -> +                        exit({badarg, {host, Reason}}) +                end; +            (IsInet6 and not IsInet)  -> +                case inet:getaddr(Host, inet6) of +                    {ok, Addr} -> +                        Addr; +                    {error, Reason} -> +                        exit({badarg, {host, Reason}}) +                end; +            true -> +                %% Conflicting options +                exit({badarg, {udp, [inet]}}) +        end, +    UdpOptions2 = lists:reverse(UdpOptions), +    TftpOptions = lists:reverse(UserOptions), +    Callbacks2  = add_default_callbacks(Callbacks), +    Config#config{udp_host     = Host2, +                  udp_options  = UdpOptions2, +                  user_options = TftpOptions, +                  callbacks    = Callbacks2}; +do_parse_config(Options, Config) when is_record(Config, config) -> +    exit({badarg, Options}). + +add_default_callbacks(Callbacks) -> +    RegExp = "", +    {ok, Internal} = re:compile(RegExp), +    File = #callback{regexp   = RegExp, +		     internal = Internal, +		     module   = tftp_file, +		     state    = []}, +    Bin = #callback{regexp   = RegExp, +		    internal = Internal, +		    module   = tftp_binary, +		    state    = []}, +    Callbacks ++ [File, Bin]. + +host_to_string(Host) -> +    case Host of +        String when is_list(String) -> +            String; +        {A1, A2, A3, A4} -> % inet +            lists:concat([A1, ".", A2, ".", A3, ".",A4]); +        {A1, A2, A3, A4, A5, A6, A7, A8} -> % inet6 +            lists:concat([ +                          int16_to_hex(A1), "::", +                          int16_to_hex(A2), "::", +                          int16_to_hex(A3), "::", +                          int16_to_hex(A4), "::", +                          int16_to_hex(A5), "::", +                          int16_to_hex(A6), "::", +                          int16_to_hex(A7), "::", +                          int16_to_hex(A8) +                         ]) +    end. + +int16_to_hex(0) -> +    [$0]; +int16_to_hex(I) -> +    N1 = ((I bsr 8) band 16#ff), +    N2 = (I band 16#ff), +    [code_character(N1 div 16), code_character(N1 rem 16), +     code_character(N2 div 16), code_character(N2 rem 16)]. + +code_character(N) when N < 10 -> +    $0 + N; +code_character(N) -> +    $A + (N - 10). + +%%------------------------------------------------------------------- +%% Decode +%%------------------------------------------------------------------- + +decode_msg(Bin) when is_binary(Bin) -> +    case Bin of +        <<?TFTP_OPCODE_RRQ:16/integer, Tail/binary>> -> +            case decode_strings(Tail, [keep_case, lower_case]) of +                [Filename, Mode | Strings] -> +                    Options = decode_options(Strings), +                    #tftp_msg_req{access = read, +                                  filename = Filename, +                                  mode = to_lower(Mode), +                                  options = Options}; +                [_Filename | _Strings] -> +                    exit(#tftp_msg_error{code = undef, +                                         text = "Missing mode"}); +                _ -> +                    exit(#tftp_msg_error{code = undef, +                                         text = "Missing filename"}) +            end; +        <<?TFTP_OPCODE_WRQ:16/integer, Tail/binary>> -> +            case decode_strings(Tail, [keep_case, lower_case]) of +                [Filename, Mode | Strings] -> +                    Options = decode_options(Strings), +                    #tftp_msg_req{access = write, +                                  filename = Filename, +                                  mode = to_lower(Mode), +                                  options = Options}; +                [_Filename | _Strings] -> +                    exit(#tftp_msg_error{code = undef, +                                         text = "Missing mode"}); +                _ -> +                    exit(#tftp_msg_error{code = undef, +                                         text = "Missing filename"}) +            end; +        <<?TFTP_OPCODE_DATA:16/integer, SeqNo:16/integer, Data/binary>> -> +            #tftp_msg_data{block_no = SeqNo, data = Data}; +        <<?TFTP_OPCODE_ACK:16/integer, SeqNo:16/integer>> -> +            #tftp_msg_ack{block_no = SeqNo}; +        <<?TFTP_OPCODE_ERROR:16/integer, ErrorCode:16/integer, Tail/binary>> -> +            case decode_strings(Tail, [keep_case]) of +                [ErrorText] -> +                    ErrorCode2 = decode_error_code(ErrorCode), +                    #tftp_msg_error{code = ErrorCode2, +                                    text = ErrorText}; +                _ -> +                    exit(#tftp_msg_error{code = undef, +                                         text = "Trailing garbage"}) +            end; +        <<?TFTP_OPCODE_OACK:16/integer, Tail/binary>> -> +            Strings = decode_strings(Tail, [lower_case]), +            Options = decode_options(Strings), +            #tftp_msg_oack{options = Options}; +        _ -> +            exit(#tftp_msg_error{code = undef, +                                 text = "Invalid syntax"}) +    end. + +decode_strings(Bin, Cases) when is_binary(Bin), is_list(Cases) -> +    do_decode_strings(Bin, Cases, []). + +do_decode_strings(<<>>, _Cases, Strings) -> +    lists:reverse(Strings); +do_decode_strings(Bin, [Case | Cases], Strings) -> +    {String, Tail} = decode_string(Bin, Case, []), +    if +        Cases =:= [] -> +            do_decode_strings(Tail, [Case], [String | Strings]); +        true -> +            do_decode_strings(Tail, Cases,  [String | Strings]) +    end. + +decode_string(<<Char:8/integer, Tail/binary>>, Case, String) -> +    if +        Char =:= 0 -> +            {lists:reverse(String), Tail}; +        Case =:= keep_case -> +            decode_string(Tail, Case, [Char | String]); +        Case =:= lower_case -> +            Char2 = ?LOWER(Char), +            decode_string(Tail, Case, [Char2 | String]) +    end; +decode_string(<<>>, _Case, _String) -> +    exit(#tftp_msg_error{code = undef, text = "Trailing null missing"}). + +decode_options([Key, Value | Strings]) -> +    [{to_lower(Key), Value} | decode_options(Strings)]; +decode_options([]) -> +    []. + +decode_error_code(Int) -> +    case Int of +        ?TFTP_ERROR_UNDEF   -> undef; +        ?TFTP_ERROR_ENOENT  -> enoent; +        ?TFTP_ERROR_EACCES  -> eacces; +        ?TFTP_ERROR_ENOSPC  -> enospc; +        ?TFTP_ERROR_BADOP   -> badop; +        ?TFTP_ERROR_BADBLK  -> badblk; +        ?TFTP_ERROR_EEXIST  -> eexist; +        ?TFTP_ERROR_BADUSER -> baduser; +        ?TFTP_ERROR_BADOPT  -> badopt; +        Int when is_integer(Int), Int >= 0, Int =< 65535 -> Int; +        _ -> exit(#tftp_msg_error{code = undef, text = "Error code outside range."}) +    end. + +%%------------------------------------------------------------------- +%% Encode +%%------------------------------------------------------------------- + +encode_msg(#tftp_msg_req{access = Access, +                         filename = Filename, +                         mode = Mode,  +                         options = Options}) -> +    OpCode = case Access of +                 read  -> ?TFTP_OPCODE_RRQ; +                 write -> ?TFTP_OPCODE_WRQ +             end, +    [ +     <<OpCode:16/integer>>, +     Filename,  +     0,  +     Mode,  +     0, +     [[Key, 0, Val, 0] || {Key, Val} <- Options] +    ]; +encode_msg(#tftp_msg_data{block_no = BlockNo, data = Data}) when BlockNo =< 65535 -> +    [ +     <<?TFTP_OPCODE_DATA:16/integer, BlockNo:16/integer>>, +     Data +    ]; +encode_msg(#tftp_msg_ack{block_no = BlockNo}) when BlockNo =< 65535 -> +    <<?TFTP_OPCODE_ACK:16/integer, BlockNo:16/integer>>; +encode_msg(#tftp_msg_error{code = Code, text = Text}) -> +    IntCode = encode_error_code(Code), +    [ +     <<?TFTP_OPCODE_ERROR:16/integer, IntCode:16/integer>>,  +     Text, +     0 +    ]; +encode_msg(#tftp_msg_oack{options = Options}) -> +    [ +     <<?TFTP_OPCODE_OACK:16/integer>>, +     [[Key, 0, Val, 0] || {Key, Val} <- Options] +    ]. + +encode_error_code(Code) -> +    case Code of +        undef   -> ?TFTP_ERROR_UNDEF; +        enoent  -> ?TFTP_ERROR_ENOENT; +        eacces  -> ?TFTP_ERROR_EACCES; +        enospc  -> ?TFTP_ERROR_ENOSPC; +        badop   -> ?TFTP_ERROR_BADOP; +        badblk  -> ?TFTP_ERROR_BADBLK; +        eexist  -> ?TFTP_ERROR_EEXIST; +        baduser -> ?TFTP_ERROR_BADUSER; +        badopt  -> ?TFTP_ERROR_BADOPT; +        Int when is_integer(Int), Int >= 0, Int =< 65535 -> Int +    end. + +%%------------------------------------------------------------------- +%% Miscellaneous +%%------------------------------------------------------------------- + +replace_val(Key, Val, List) -> +    case lists:keysearch(Key, 1, List) of +        false -> +            List ++ [{Key, Val}]; +        {value, {_, OldVal}} when OldVal =:= Val -> +            List; +        {value, {_, _}} -> +            lists:keyreplace(Key, 1, List, {Key, Val}) +    end. + +to_lower(Chars) -> +    [?LOWER(Char) || Char <- Chars]. diff --git a/lib/tftp/src/tftp_logger.erl b/lib/tftp/src/tftp_logger.erl new file mode 100644 index 0000000000..a869958484 --- /dev/null +++ b/lib/tftp/src/tftp_logger.erl @@ -0,0 +1,99 @@ +%% +%% %CopyrightBegin% +%%  +%% Copyright Ericsson AB 2008-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% +%% +%% +-module(tftp_logger). + +%%------------------------------------------------------------------- +%% Interface +%%------------------------------------------------------------------- + +%% public functions +-export([ +	 error_msg/2, +	 warning_msg/2, +	 info_msg/2 +	]). + +-export([behaviour_info/1]). + +behaviour_info(callbacks) -> +    [{error_msg, 2}, {warning_msg, 2}, {info_msg, 2}]; +behaviour_info(_) -> +    undefined. + +%%------------------------------------------------------------------- +%% error_msg(Format, Data) -> ok | exit(Reason) +%%  +%% Format = string() +%% Data = [term()] +%% Reason = term() +%% +%% Log an error message +%%------------------------------------------------------------------- + +error_msg(Format, Data) -> +    {Format2, Data2} = add_timestamp(Format, Data), +    error_logger:error_msg(Format2, Data2). + +%%------------------------------------------------------------------- +%% warning_msg(Format, Data) -> ok | exit(Reason) +%%  +%% Format = string() +%% Data = [term()] +%% Reason = term() +%% +%% Log a warning message +%%------------------------------------------------------------------- + +warning_msg(Format, Data) -> +    {Format2, Data2} = add_timestamp(Format, Data), +    error_logger:warning_msg(Format2, Data2). + +%%------------------------------------------------------------------- +%% info_msg(Format, Data) -> ok | exit(Reason) +%%  +%% Format = string() +%% Data = [term()] +%% Reason = term() +%% +%% Log an info message +%%------------------------------------------------------------------- + +info_msg(Format, Data) -> +    {Format2, Data2} = add_timestamp(Format, Data), +    io:format(Format2, Data2). + +%%------------------------------------------------------------------- +%% Add timestamp to log message +%%------------------------------------------------------------------- + +add_timestamp(Format, Data) -> +    Time = erlang:timestamp(), +    {{_Y, _Mo, _D}, {H, Mi, S}} = calendar:now_to_universal_time(Time), +    %% {"~p-~s-~sT~s:~s:~sZ,~6.6.0w tftp: " ++ Format ++ "\n",  +    %%  [Y, t(Mo), t(D), t(H), t(Mi), t(S), MicroSecs | Data]}. +    {"~s:~s:~s tftp: " ++ Format, [t(H), t(Mi), t(S) | Data]}. + +%% Convert 9 to "09". +t(Int) -> +    case integer_to_list(Int) of +	[Single] -> [$0, Single]; +        Multi    -> Multi +    end. diff --git a/lib/tftp/src/tftp_sup.erl b/lib/tftp/src/tftp_sup.erl new file mode 100644 index 0000000000..0475e53e42 --- /dev/null +++ b/lib/tftp/src/tftp_sup.erl @@ -0,0 +1,111 @@ +%% +%% %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% +%% +%% +%%---------------------------------------------------------------------- +%% Purpose: The top supervisor for tftp +%%---------------------------------------------------------------------- + +-module(tftp_sup). + +-behaviour(supervisor). + +%% API +-export([start_link/1, +	 start_child/1, +	 stop_child/1, +	 which_children/0]). + +%% Supervisor callback +-export([init/1]). + +%%%========================================================================= +%%%  API +%%%========================================================================= + +start_link(TftpServices) -> +    supervisor:start_link({local, ?MODULE}, ?MODULE, [TftpServices]). + +start_child(Options) -> +    KillAfter = default_kill_after(), +    ChildSpec = worker_spec(KillAfter, Options), +    supervisor:start_child(?MODULE, ChildSpec). + +stop_child(Pid) when is_pid(Pid) -> +    Children = supervisor:which_children(?MODULE), +    case [Id || {Id, P, _Type, _Modules} <- Children, P =:= Pid] of +	[] -> +	    {error, not_found}; +	[Id] -> +	    case supervisor:terminate_child(?MODULE, Id) of +		ok -> +		    supervisor:delete_child(?MODULE, Id); +		{error, not_found} -> +		    supervisor:delete_child(?MODULE, Id); +		{error, Reason} -> +		    {error, Reason} +	    end +    end. + +which_children() -> +    Children = supervisor:which_children(?MODULE), +    [{tftpd, Pid} || {_Id, Pid, _Type, _Modules} <- Children, Pid =/= undefined]. +     +%%%========================================================================= +%%%  Supervisor callback +%%%========================================================================= + +init([Services]) when is_list(Services) -> +    RestartStrategy = one_for_one, +    MaxR = 10, +    MaxT = 3600, +    KillAfter = default_kill_after(), +    Children = [worker_spec(KillAfter, Options) || {tftpd, Options} <- Services], +    {ok, {{RestartStrategy, MaxR, MaxT}, Children}}. + +%%%========================================================================= +%%%  Internal functions +%%%========================================================================= + +worker_spec(KillAfter, Options) -> +    Modules = [proc_lib, tftp, tftp_engine], +    KA = supervisor_timeout(KillAfter), +    Name = unique_name(Options), +    {Name, {tftp, start, [Options]}, permanent, KA, worker, Modules}. + +unique_name(Options) -> +    case lists:keysearch(port, 1, Options) of +	{value, {_, Port}} when is_integer(Port), Port > 0 ->  +	    {tftpd, Port}; +	_ -> +	    {tftpd, erlang:unique_integer([positive])} +    end. + +default_kill_after() -> +    timer:seconds(3). + +%% supervisor_spec(Name) -> +%%     {Name, {Name, start, []}, permanent, infinity, supervisor, +%%      [Name, supervisor]}. +     +-ifdef(debug_shutdown). +supervisor_timeout(_KillAfter) -> timer:hours(24). +-else. +supervisor_timeout(KillAfter) -> KillAfter. +-endif.     | 
