diff options
author | Péter Dimitrov <[email protected]> | 2018-03-20 11:26:01 +0100 |
---|---|---|
committer | Péter Dimitrov <[email protected]> | 2018-03-28 10:19:38 +0200 |
commit | 09ccfa2a6a8f8df55c7d808f5ad26324ac1e81b6 (patch) | |
tree | db6e9a1172ee8bb761041e352b6dc637fd765d28 /lib/tftp/src | |
parent | 3c41882115f2cd9bfda8318925b4352cd1ec06b7 (diff) | |
download | otp-09ccfa2a6a8f8df55c7d808f5ad26324ac1e81b6.tar.gz otp-09ccfa2a6a8f8df55c7d808f5ad26324ac1e81b6.tar.bz2 otp-09ccfa2a6a8f8df55c7d808f5ad26324ac1e81b6.zip |
inets,tftp: Break out TFTP from inets
- Create directory structure
- Move code, tests, documentation from inets
- Add inets_tftp_wrapper
- Add tftp app to run-dialyzer script
Change-Id: I6a142ae66cecb9a1821cbf9ea6a45f66a836763d
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. |