%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2005-2018. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%
%%
%%%-------------------------------------------------------------------
%%% 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).