From 09ccfa2a6a8f8df55c7d808f5ad26324ac1e81b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Dimitrov?= Date: Tue, 20 Mar 2018 11:26:01 +0100 Subject: 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 --- lib/Makefile | 2 +- lib/inets/doc/src/Makefile | 1 - lib/inets/doc/src/inets.xml | 4 +- lib/inets/doc/src/introduction.xml | 3 +- lib/inets/doc/src/part.xml | 1 - lib/inets/doc/src/ref_man.xml | 5 +- lib/inets/doc/src/tftp.xml | 647 ----------- lib/inets/src/inets_app/Makefile | 3 +- lib/inets/src/inets_app/inets.app.src | 8 +- lib/inets/src/inets_app/inets.erl | 8 +- lib/inets/src/inets_app/inets_sup.erl | 18 +- lib/inets/src/inets_app/inets_tftp_wrapper.erl | 48 + lib/inets/src/subdirs.mk | 2 +- lib/inets/src/tftp/Makefile | 109 -- lib/inets/src/tftp/tftp.erl | 398 ------- lib/inets/src/tftp/tftp.hrl | 69 -- lib/inets/src/tftp/tftp_binary.erl | 239 ---- lib/inets/src/tftp/tftp_engine.erl | 1422 ------------------------ lib/inets/src/tftp/tftp_file.erl | 390 ------- lib/inets/src/tftp/tftp_lib.erl | 474 -------- lib/inets/src/tftp/tftp_logger.erl | 99 -- lib/inets/src/tftp/tftp_sup.erl | 111 -- lib/inets/test/Makefile | 15 +- lib/inets/test/inets_SUITE.erl | 38 +- lib/inets/test/inets_socketwrap_SUITE.erl | 34 +- lib/inets/test/inets_sup_SUITE.erl | 24 +- lib/inets/test/tftp_SUITE.erl | 949 ---------------- lib/inets/test/tftp_test_lib.erl | 308 ----- lib/inets/test/tftp_test_lib.hrl | 44 - lib/tftp/Makefile | 78 ++ lib/tftp/doc/html/.gitignore | 0 lib/tftp/doc/man3/.gitignore | 0 lib/tftp/doc/man6/.gitignore | 0 lib/tftp/doc/pdf/.gitignore | 0 lib/tftp/doc/src/Makefile | 153 +++ lib/tftp/doc/src/book.xml | 49 + lib/tftp/doc/src/introduction.xml | 46 + lib/tftp/doc/src/notes.xml | 53 + lib/tftp/doc/src/part.xml | 36 + lib/tftp/doc/src/ref_man.xml | 36 + lib/tftp/doc/src/tftp.xml | 643 +++++++++++ lib/tftp/ebin/.gitignore | 0 lib/tftp/info | 2 + lib/tftp/src/Makefile | 110 ++ lib/tftp/src/tftp.app.src | 22 + lib/tftp/src/tftp.appup.src | 26 + lib/tftp/src/tftp.erl | 409 +++++++ lib/tftp/src/tftp.hrl | 69 ++ lib/tftp/src/tftp_app.erl | 47 + lib/tftp/src/tftp_binary.erl | 239 ++++ lib/tftp/src/tftp_engine.erl | 1422 ++++++++++++++++++++++++ lib/tftp/src/tftp_file.erl | 390 +++++++ lib/tftp/src/tftp_lib.erl | 474 ++++++++ lib/tftp/src/tftp_logger.erl | 99 ++ lib/tftp/src/tftp_sup.erl | 111 ++ lib/tftp/test/Makefile | 250 +++++ lib/tftp/test/tftp.config | 1 + lib/tftp/test/tftp.cover | 2 + lib/tftp/test/tftp.spec | 1 + lib/tftp/test/tftp_SUITE.erl | 949 ++++++++++++++++ lib/tftp/test/tftp_bench.spec | 1 + lib/tftp/test/tftp_test_lib.erl | 308 +++++ lib/tftp/test/tftp_test_lib.hrl | 44 + lib/tftp/vsn.mk | 24 + 64 files changed, 6164 insertions(+), 5403 deletions(-) delete mode 100644 lib/inets/doc/src/tftp.xml create mode 100644 lib/inets/src/inets_app/inets_tftp_wrapper.erl delete mode 100644 lib/inets/src/tftp/Makefile delete mode 100644 lib/inets/src/tftp/tftp.erl delete mode 100644 lib/inets/src/tftp/tftp.hrl delete mode 100644 lib/inets/src/tftp/tftp_binary.erl delete mode 100644 lib/inets/src/tftp/tftp_engine.erl delete mode 100644 lib/inets/src/tftp/tftp_file.erl delete mode 100644 lib/inets/src/tftp/tftp_lib.erl delete mode 100644 lib/inets/src/tftp/tftp_logger.erl delete mode 100644 lib/inets/src/tftp/tftp_sup.erl delete mode 100644 lib/inets/test/tftp_SUITE.erl delete mode 100644 lib/inets/test/tftp_test_lib.erl delete mode 100644 lib/inets/test/tftp_test_lib.hrl create mode 100644 lib/tftp/Makefile create mode 100644 lib/tftp/doc/html/.gitignore create mode 100644 lib/tftp/doc/man3/.gitignore create mode 100644 lib/tftp/doc/man6/.gitignore create mode 100644 lib/tftp/doc/pdf/.gitignore create mode 100644 lib/tftp/doc/src/Makefile create mode 100644 lib/tftp/doc/src/book.xml create mode 100644 lib/tftp/doc/src/introduction.xml create mode 100644 lib/tftp/doc/src/notes.xml create mode 100644 lib/tftp/doc/src/part.xml create mode 100644 lib/tftp/doc/src/ref_man.xml create mode 100644 lib/tftp/doc/src/tftp.xml create mode 100644 lib/tftp/ebin/.gitignore create mode 100644 lib/tftp/info create mode 100644 lib/tftp/src/Makefile create mode 100644 lib/tftp/src/tftp.app.src create mode 100644 lib/tftp/src/tftp.appup.src create mode 100644 lib/tftp/src/tftp.erl create mode 100644 lib/tftp/src/tftp.hrl create mode 100644 lib/tftp/src/tftp_app.erl create mode 100644 lib/tftp/src/tftp_binary.erl create mode 100644 lib/tftp/src/tftp_engine.erl create mode 100644 lib/tftp/src/tftp_file.erl create mode 100644 lib/tftp/src/tftp_lib.erl create mode 100644 lib/tftp/src/tftp_logger.erl create mode 100644 lib/tftp/src/tftp_sup.erl create mode 100644 lib/tftp/test/Makefile create mode 100644 lib/tftp/test/tftp.config create mode 100644 lib/tftp/test/tftp.cover create mode 100644 lib/tftp/test/tftp.spec create mode 100644 lib/tftp/test/tftp_SUITE.erl create mode 100644 lib/tftp/test/tftp_bench.spec create mode 100644 lib/tftp/test/tftp_test_lib.erl create mode 100644 lib/tftp/test/tftp_test_lib.hrl create mode 100644 lib/tftp/vsn.mk (limited to 'lib') diff --git a/lib/Makefile b/lib/Makefile index d04af322eb..d67e605875 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -35,7 +35,7 @@ ALL_ERLANG_APPLICATIONS = xmerl edoc erl_docgen snmp otp_mibs erl_interface \ public_key ssl observer odbc diameter \ cosTransactions cosEvent cosTime cosNotification \ cosProperty cosFileTransfer cosEventDomain et megaco \ - eunit ssh eldap dialyzer hipe ftp + eunit ssh eldap dialyzer hipe ftp tftp ifdef BUILD_ALL ERLANG_APPLICATIONS += $(ALL_ERLANG_APPLICATIONS) diff --git a/lib/inets/doc/src/Makefile b/lib/inets/doc/src/Makefile index d66ef31ba6..90c1258d4a 100644 --- a/lib/inets/doc/src/Makefile +++ b/lib/inets/doc/src/Makefile @@ -47,7 +47,6 @@ XML_CHAPTER_FILES = \ XML_REF3_FILES = \ inets.xml \ - tftp.xml \ http_uri.xml\ httpc.xml\ httpd.xml \ diff --git a/lib/inets/doc/src/inets.xml b/lib/inets/doc/src/inets.xml index 9b17567fd4..eb4e51584f 100644 --- a/lib/inets/doc/src/inets.xml +++ b/lib/inets/doc/src/inets.xml @@ -189,8 +189,8 @@
SEE ALSO

httpc(3), - httpd(3), - tftp(3)

+ httpd(3) +

diff --git a/lib/inets/doc/src/introduction.xml b/lib/inets/doc/src/introduction.xml index c006599a21..faf911f188 100644 --- a/lib/inets/doc/src/introduction.xml +++ b/lib/inets/doc/src/introduction.xml @@ -37,7 +37,6 @@

Inets is a container for Internet clients and servers including the following:

- A TFTP client and server An client and server

The HTTP client and server are HTTP 1.1 compliant as @@ -49,7 +48,7 @@ Prerequisites

It is assumed that the reader is familiar with the Erlang programming language, concepts of OTP, and has a basic - understanding of the TFTP and HTTP protocols.

+ understanding of and HTTP protocol.

diff --git a/lib/inets/doc/src/part.xml b/lib/inets/doc/src/part.xml index e7a786d47b..b9c8ed674c 100644 --- a/lib/inets/doc/src/part.xml +++ b/lib/inets/doc/src/part.xml @@ -33,7 +33,6 @@

The Inets application provides a set of Internet-related services as follows:

- A TFTP client and server An client and server

The HTTP client and server are HTTP 1.1 compliant as diff --git a/lib/inets/doc/src/ref_man.xml b/lib/inets/doc/src/ref_man.xml index 33886d1a41..58c1a651f9 100644 --- a/lib/inets/doc/src/ref_man.xml +++ b/lib/inets/doc/src/ref_man.xml @@ -30,12 +30,9 @@ ref_man.xml -

Inets is a container for Internet clients and - servers. An HTTP client and server, and - a TFTP client and server are incorporated in Inets.

+

Inets is a container for an HTTP client and server.

- diff --git a/lib/inets/doc/src/tftp.xml b/lib/inets/doc/src/tftp.xml deleted file mode 100644 index 10398f5088..0000000000 --- a/lib/inets/doc/src/tftp.xml +++ /dev/null @@ -1,647 +0,0 @@ - - - - -
- - 20062015 - Ericsson AB. 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. - - - - tftp - - - - -
- tftp - Trivial FTP. - -

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 is - the "netascii" transfer mode.

-

The start/1 function starts - a daemon process listening for UDP packets on a port. When it - receives a request for read or write, it spawns a temporary server - process handling the transfer.

-

On the client side, - function read_file/3 - and write_file/3 - spawn a temporary client process establishing - contact with a TFTP daemon and perform the file transfer.

-

tftp uses a callback module to handle the file - transfer. Two such callback modules are provided, - tftp_binary and tftp_file. See - read_file/3 and - write_file/3 for details. - You can also implement your own callback modules, see - CALLBACK FUNCTIONS. - A callback module provided by - the user is registered using option callback, see - DATA TYPES.

-
- -
- TFTP SERVER SERVICE START/STOP - -

A TFTP server can be configured to start statically when starting - the Inets application. Alternatively, it can be started dynamically - (when Inets is already started) by calling the Inets application - API inets:start(tftpd, ServiceConfig) or - inets:start(tftpd, ServiceConfig, How), - see inets(3) for details. - The ServiceConfig for TFTP is described in - the DATA TYPES - section.

- -

The TFTP server can be stopped using inets:stop(tftpd, Pid), - see inets(3) for details.

- -

The TPFT client is of such a temporary nature that it is not - handled as a service in the Inets service framework.

- -
- -
- - DATA TYPES -

ServiceConfig = Options

-

Options = [option()]

-

Most of the options are common for both the client and the server - side, but some of them differs a little. - The available option()s are as follows:

- - {debug, Level} - -

Level = none | error | warning | brief | normal | verbose | all

-

Controls the level of debug printouts. - Default is none.

-
- {host, Host} - -

Host = hostname(), see - inet(3).

-

The name or IP address of the host where the TFTP daemon - resides. This option is only used by the client.

-
- {port, Port} - -

Port = int()

-

The TFTP port where the daemon listens. Defaults is - the standardized number 69. On the server side, it can - sometimes make sense to set it to 0, meaning that - the daemon just picks a free port (which one is - returned by function info/1).

-

If a socket is connected already, option - {udp, [{fd, integer()}]} can be used to pass the - open file descriptor to gen_udp. This can be automated - by using a command-line argument stating the - prebound file descriptor number. For example, if the - port is 69 and file descriptor 22 is opened by - setuid_socket_wrap, the command-line argument - "-tftpd_69 22" triggers the prebound file - descriptor 22 to be used instead of opening port 69. - The UDP option {udp, [{fd, 22}]} is automatically 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 = int()

-

Policy for the selection of the temporary port that is used - by the server/client during the file transfer. Default is - random, which is the standardized policy. With this - policy a randomized free port is used. A single port or a range - of ports can be useful if the protocol passes through a - firewall.

-
- {udp, Options} - -

Options = [Opt], see - gen_udp:open/2.

-
- {use_tsize, Bool} - -

Bool = bool()

-

Flag for automated use of option tsize. With - this set to true, the write_file/3 client - determines the filesize and sends it to the server as - the standardized tsize option. A read_file/3 - client acquires only a filesize from the server by sending - a zero tsize.

-
- {max_tsize, MaxTsize} - -

MaxTsize = int() | infinity

-

Threshold for the maximal filesize in bytes. The transfer - is aborted if the limit is exceeded. - Default is infinity.

-
- {max_conn, MaxConn} - -

MaxConn = int() | infinity

-

Threshold for the maximal number of active connections. - The daemon rejects the setup of new connections if - the limit is exceeded. Default is infinity.

-
- {TftpKey, TftpVal} - -

TftpKey = string()

-TftpVal = string()

-

Name and value of a TFTP option.

-
- {reject, Feature} - -

Feature = Mode | TftpKey

- Mode = read | write

- TftpKey = string()

-

Controls which features to reject. This is - mostly useful for the server as it can restrict the use - 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 is matched to the regular - expressions of the registered callbacks. The first matching - callback is used during the transfer. See - read_file/3 and - write_file/3. -

-

The callback module must implement the tftp behavior, see - CALLBACK FUNCTIONS.

-
- - {logger, Module} - -

Module = module()()

- -

Callback module for customized logging of errors, warnings, and - info messages. The callback module must implement the - tftp_logger behavior, see - LOGGER FUNCTIONS. - The default module is tftp_logger.

-
- - {max_retries, MaxRetries} - -

MaxRetries = int()

- -

Threshold for the maximal number of retries. By default - the server/client tries to resend a message up to - five times when the time-out expires.

-
-
-
- - - - change_config(daemons, Options) -> [{Pid, Result}] - Changes configuration for all daemons. - - - Options = [option()] - Pid = pid() - Result = ok | {error, Reason} - Reason = term() - - -

Changes configuration for all TFTP daemon processes.

-
-
- - - change_config(servers, Options) -> [{Pid, Result}] - Changes configuration for all servers. - - - Options = [option()] - Pid = pid() - Result = ok | {error, Reason} - Reason = term() - - -

Changes configuration for all TFTP server processes.

-
-
- - - change_config(Pid, Options) -> Result - Changes configuration for a TFTP daemon, server, - or client process. - - Pid = pid() - Options = [option()] - Result = ok | {error, Reason} - Reason = term() - - -

Changes configuration for a TFTP daemon, server, or client process.

-
-
- - - info(daemons) -> [{Pid, Options}] - Returns information about all daemons. - - Pid = [pid()()] - Options = [option()] - Reason = term() - - -

Returns information about all TFTP daemon processes.

-
-
- - - info(servers) -> [{Pid, Options}] - Returns information about all servers. - - Pid = [pid()()] - Options = [option()] - Reason = term() - - -

Returns information about all TFTP server processes.

-
-
- - - info(Pid) -> {ok, Options} | {error, Reason} - Returns information about a daemon, server, or client process. - - Options = [option()] - Reason = term() - - -

Returns information about a TFTP daemon, server, or client process.

-
-
- - - read_file(RemoteFilename, LocalFilename, Options) -> {ok, LastCallbackState} | {error, Reason} - Reads a (virtual) file from a TFTP server. - - RemoteFilename = string() - LocalFilename = binary | string() - Options = [option()] - LastCallbackState = term() - Reason = term() - - -

Reads a (virtual) file RemoteFilename from a TFTP - server.

-

If LocalFilename is the atom binary, - tftp_binary is used as callback module. It concatenates - all transferred blocks and returns them as one single binary - in LastCallbackState.

-

If LocalFilename is a string and there are no - registered callback modules, tftp_file is used as - callback module. It writes each transferred block to the file - named LocalFilename and returns the number of - transferred bytes in LastCallbackState.

-

If LocalFilename is a string and there are registered - callback modules, LocalFilename is tested against - the regexps of these and the callback module corresponding to - the first match is used, or an error tuple is returned if no - matching regexp is found.

-
-
- - - start(Options) -> {ok, Pid} | {error, Reason} - Starts a daemon process. - - Options = [option()] - Pid = pid() - Reason = term() - - -

Starts a daemon process listening for UDP packets on a - port. When it receives a request for read or write, it spawns - a temporary server process handling the actual transfer - of the (virtual) file.

-
-
- - - write_file(RemoteFilename, LocalFilename, Options) -> {ok, LastCallbackState} | {error, Reason} - Writes a (virtual) file to a TFTP server. - - RemoteFilename = string() - LocalFilename = binary() | string() - Options = [option()] - LastCallbackState = term() - Reason = term() - - -

Writes a (virtual) file RemoteFilename to a TFTP - server.

-

If LocalFilename is a binary, tftp_binary is - used as callback module. The binary is transferred block by - block and the number of transferred bytes is returned in - LastCallbackState.

-

If LocalFilename is a string and there are no - registered callback modules, tftp_file is used as - callback module. It reads the file named LocalFilename - block by block and returns the number of transferred bytes - in LastCallbackState.

-

If LocalFilename is a string and there are registered - callback modules, LocalFilename is tested against - the regexps of these and the callback module corresponding to - the first match is used, or an error tuple is returned if no - matching regexp is found.

-
-
-
- -
- - CALLBACK FUNCTIONS -

A tftp callback module is to be implemented as a - tftp behavior and export the functions listed - in the following.

-

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 - function read/1 or write/2 is invoked - repeatedly, once per transferred block. At each function call, - the state returned from the previous call is obtained. When - the last block is encountered, function read/1 or - write/2 is expected to close the (virtual) file - and return its last state. Function abort/3 is only - used in error situations. Function 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 to return the subset of them that it - accepts. Then the options are sent to the server, which performs - the same TFTP option negotiation procedure. The options that are - accepted by the server are forwarded to function open/5 - on the client side. On the client side, function open/5 - must accept all option as-is or reject the transfer. Then - the callback interaction follows the same pattern as described - for the server side. When the last block is encountered in - read/1 or write/2, the returned state is forwarded to - the user and returned from read_file/3 or - write_file/3.

- -

If a callback (performing the file access - in the TFTP server) takes too long time (more than - the double TFTP time-out), the server aborts the - connection and sends an error reply to the client. - This implies that the server releases resources - attached to the connection faster than before. The - server simply assumes that the client has given - up.

- -

If the TFTP server receives yet another request from - the same client (same host and port) while it - already has an active connection to the client, it - ignores the new request if the request is - equal to the first one (same filename and options). - This implies that the (new) client will be served - by the already ongoing connection on the server - side. By not setting up yet another connection, in - parallel with the ongoing one, the server - consumes less resources.

- - -
- - - - Module:abort(Code, Text, State) -> ok - Aborts the file transfer. - - Code = undef | enoent | eacces | enospc -   | badop | eexist | baduser | badopt -   | int() - Text = string() - State = term() - - -

Invoked when the file transfer is aborted.

-

The callback function is expected to clean - up its used resources after the aborted file - transfer, such as closing open file - descriptors and so on. The function is not - invoked if any of the other callback - functions returns an error, as it is - expected that they already have cleaned up - the necessary resources. However, it is - invoked if the functions fail (crash).

-
-
- - - Module:open(Peer, Access, Filename, Mode, SuggestedOptions, State) -> {ok, AcceptedOptions, NewState} | {error, {Code, Text}} - Opens a file for read or write access. - - Peer = {PeerType, PeerHost, PeerPort} - PeerType = inet | inet6 - PeerHost = ip_address() - PeerPort = integer() - Access = read | write - Filename = string() - Mode = string() - SuggestedOptions = AcceptedOptions = [{Key, Value}] -  Key = Value = string() - State = InitialState | term() -  InitialState = [] | [{root_dir, string()}] - NewState = term() - Code = undef | enoent | eacces | enospc -   | badop | eexist | baduser | badopt -   | int() - Text = string() - - -

Opens a file for read or write access.

-

On the client side, where the open/5 call has been - preceded by a call to prepare/5, all options must be - accepted or rejected.

-

On the server side, where there is no preceding - prepare/5 call, no new options can be added, but - those present in SuggestedOptions can be - omitted or replaced with new values in AcceptedOptions.

- - -
-
- - - Module:prepare(Peer, Access, Filename, Mode, SuggestedOptions, InitialState) -> {ok, AcceptedOptions, NewState} | {error, {Code, Text}} - Prepares to open a file on the client side. - - Peer = {PeerType, PeerHost, PeerPort} - PeerType = inet | inet6 - PeerHost = ip_address() - PeerPort = integer() - Access = read | write - Filename = string() - Mode = string() - SuggestedOptions = AcceptedOptions = [{Key, Value}] -  Key = Value = string() - InitialState = [] | [{root_dir, string()}] - NewState = term() - Code = undef | enoent | eacces | enospc -   | badop | eexist | baduser | badopt -   | int() - Text = string() - - -

Prepares to open a file on the client side.

-

No new options can be added, but those present in - SuggestedOptions can be omitted or replaced with new - values in AcceptedOptions.

-

This is followed by a call to open/4 before any - read/write access is performed. AcceptedOptions is - sent to the server, which replies with the options that it - accepts. These are then forwarded to open/4 as - SuggestedOptions.

- - -
-
- - - Module:read(State) -> {more, Bin, NewState} | {last, Bin, FileSize} | {error, {Code, Text}} - Reads a chunk from the file. - - State = NewState = term() - Bin = binary() - FileSize = int() - Code = undef | enoent | eacces | enospc -   | badop | eexist | baduser | badopt -   | int() - Text = string() - - -

Reads a chunk from the file.

-

The callback function is expected to close - the file when the last file chunk is - encountered. When an error is encountered, - the callback function is expected to clean - up after the aborted file transfer, such as - closing open file descriptors, and so on. In both - cases there will be no more calls to any of - the callback functions.

- - -
-
- - - Module:write(Bin, State) -> {more, NewState} | {last, FileSize} | {error, {Code, Text}} - Writes a chunk to the file. - - Bin = binary() - State = NewState = term() - FileSize = int() - Code = undef | enoent | eacces | enospc -   | badop | eexist | baduser | badopt -   | int() - Text = string() - - -

Writes a chunk to the file.

-

The callback function is expected to close - the file when the last file chunk is - encountered. When an error is encountered, - the callback function is expected to clean - up after the aborted file transfer, such as - closing open file descriptors, and so on. In both - cases there will be no more calls to any of - the callback functions.

- - -
-
-
- -
- - LOGGER FUNCTIONS - -

A tftp_logger callback module is to be implemented as a - tftp_logger behavior and export the following functions:

- - -
- - - - Logger:error_msg(Format, Data) -> ok | exit(Reason) - Logs an error message. - - Format = string() - Data = [term()] - Reason = term() - - -

Logs an error message. - See error_logger:error_msg/2 for details.

- - -
-
- - - Logger:info_msg(Format, Data) -> ok | exit(Reason) - Logs an info message. - - Format = string() - Data = [term()] - Reason = term() - - -

Logs an info message. - See error_logger:info_msg/2 for details.

-
-
- - - Logger:warning_msg(Format, Data) -> ok | exit(Reason) - Logs a warning message. - - Format = string() - Data = [term()] - Reason = term() - - -

Logs a warning message. - See error_logger:warning_msg/2 for details.

- - -
-
-
-
- - diff --git a/lib/inets/src/inets_app/Makefile b/lib/inets/src/inets_app/Makefile index 450e49dcc3..fad2fefe2f 100644 --- a/lib/inets/src/inets_app/Makefile +++ b/lib/inets/src/inets_app/Makefile @@ -49,7 +49,8 @@ MODULES = \ inets_sup \ inets_trace \ inets_lib \ - inets_ftp_wrapper + inets_ftp_wrapper \ + inets_tftp_wrapper INTERNAL_HRL_FILES = inets_internal.hrl EXTERNAL_HRL_FILES = ../../include/httpd.hrl \ diff --git a/lib/inets/src/inets_app/inets.app.src b/lib/inets/src/inets_app/inets.app.src index 0af44e39ab..dfe773e7fe 100644 --- a/lib/inets/src/inets_app/inets.app.src +++ b/lib/inets/src/inets_app/inets.app.src @@ -98,13 +98,7 @@ mod_trace, %% TFTP - tftp, - tftp_binary, - tftp_engine, - tftp_file, - tftp_lib, - tftp_logger, - tftp_sup + inets_tftp_wrapper ]}, {registered,[inets_sup, httpc_manager]}, %% If the "new" ssl is used then 'crypto' must be started before inets. diff --git a/lib/inets/src/inets_app/inets.erl b/lib/inets/src/inets_app/inets.erl index 615a4cf635..9005e73f5d 100644 --- a/lib/inets/src/inets_app/inets.erl +++ b/lib/inets/src/inets_app/inets.erl @@ -465,11 +465,13 @@ call_service(Service, Call, Args) -> exit:{noproc, _} -> {error, inets_not_started} end. - + +%% Obsolete! Kept for backward compatiblity! +%% TFTP application has been moved out from inets service_module(tftpd) -> - tftp; + inets_tftp_wrapper; service_module(tftpc) -> - tftp; + inets_tftp_wrapper; %% Obsolete! Kept for backward compatiblity! %% FTP application has been moved out from inets service_module(ftpc) -> diff --git a/lib/inets/src/inets_app/inets_sup.erl b/lib/inets/src/inets_app/inets_sup.erl index 8f4d806529..22c928f9f9 100644 --- a/lib/inets/src/inets_app/inets_sup.erl +++ b/lib/inets/src/inets_app/inets_sup.erl @@ -61,9 +61,7 @@ children() -> Services = get_services(), HttpdServices = [Service || Service <- Services, is_httpd(Service)], HttpcServices = [Service || Service <- Services, is_httpc(Service)], - TftpdServices = [Service || Service <- Services, is_tftpd(Service)], - [httpc_child_spec(HttpcServices), - httpd_child_spec(HttpdServices), tftpd_child_spec(TftpdServices)]. + [httpc_child_spec(HttpcServices), httpd_child_spec(HttpdServices)]. httpc_child_spec(HttpcServices0) -> HttpcServices = default_profile(HttpcServices0, []), @@ -84,15 +82,6 @@ httpd_child_spec(HttpdServices) -> Type = supervisor, {Name, StartFunc, Restart, Shutdown, Type, Modules}. -tftpd_child_spec(TftpServices) -> - Name = tftp_sup, - StartFunc = {tftp_sup, start_link, [TftpServices]}, - Restart = permanent, - Shutdown = infinity, - Modules = [tftp_sup], - Type = supervisor, - {Name, StartFunc, Restart, Shutdown, Type, Modules}. - is_httpd({httpd, _}) -> true; is_httpd({httpd, _, _}) -> @@ -105,11 +94,6 @@ is_httpc({httpc, _}) -> is_httpc(_) -> false. -is_tftpd({tftpd, _}) -> - true; -is_tftpd(_) -> - false. - default_profile([], Acc) -> [{httpc, {default, only_session_cookies}} | Acc]; default_profile([{httpc, {default, _}} | _] = Profiles, Acc) -> diff --git a/lib/inets/src/inets_app/inets_tftp_wrapper.erl b/lib/inets/src/inets_app/inets_tftp_wrapper.erl new file mode 100644 index 0000000000..1e5deb234b --- /dev/null +++ b/lib/inets/src/inets_app/inets_tftp_wrapper.erl @@ -0,0 +1,48 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2006-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% +%% +-module(inets_tftp_wrapper). + + +-export([start_standalone/1, + start_service/1, + stop_service/1, + services/0, + service_info/1]). + + +start_standalone(Options) -> + tftp:start_standalone(Options). + + +start_service(Options) -> + application:ensure_started(tftp), + tftp:start_service(Options). + + +stop_service(Pid) -> + tftp:stop_service(Pid). + + +services() -> + []. + + +service_info(_) -> + []. diff --git a/lib/inets/src/subdirs.mk b/lib/inets/src/subdirs.mk index 8c18691aef..e9f4de959c 100644 --- a/lib/inets/src/subdirs.mk +++ b/lib/inets/src/subdirs.mk @@ -1,3 +1,3 @@ #-*-makefile-*- ; force emacs to enter makefile-mode -SUB_DIRECTORIES = inets_app http_lib http_client http_server tftp +SUB_DIRECTORIES = inets_app http_lib http_client http_server diff --git a/lib/inets/src/tftp/Makefile b/lib/inets/src/tftp/Makefile deleted file mode 100644 index 4eaa959cce..0000000000 --- a/lib/inets/src/tftp/Makefile +++ /dev/null @@ -1,109 +0,0 @@ -# -# %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 -EBIN = ../../ebin -include $(ERL_TOP)/make/$(TARGET)/otp.mk - -# ---------------------------------------------------- -# Application version -# ---------------------------------------------------- -include ../../vsn.mk - -VSN = $(INETS_VSN) - - -# ---------------------------------------------------- -# Release directory specification -# ---------------------------------------------------- -RELSYSDIR = $(RELEASE_PATH)/lib/$(APPLICATION)-$(VSN) - - -# ---------------------------------------------------- -# Target Specs -# ---------------------------------------------------- -BEHAVIOUR_MODULES= \ - tftp - -MODULES = \ - 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)) - -# ---------------------------------------------------- -# FLAGS -# ---------------------------------------------------- - -include ../inets_app/inets.mk - -ERL_COMPILE_FLAGS += \ - $(INETS_FLAGS) \ - $(INETS_ERL_COMPILE_FLAGS) \ - -I../../include \ - -I../inets_app - - -# ---------------------------------------------------- -# Targets -# ---------------------------------------------------- - -$(TARGET_FILES): $(BEHAVIOUR_TARGET_FILES) - -debug opt: $(TARGET_FILES) - -clean: - rm -f $(TARGET_FILES) $(BEHAVIOUR_TARGET_FILES) - rm -f core - -docs: - -# ---------------------------------------------------- -# Release Target -# ---------------------------------------------------- -include $(ERL_TOP)/make/otp_release_targets.mk - -release_spec: opt - $(INSTALL_DIR) "$(RELSYSDIR)/src" - $(INSTALL_DIR) "$(RELSYSDIR)/src/tftp" - $(INSTALL_DATA) $(HRL_FILES) $(ERL_FILES) "$(RELSYSDIR)/src/tftp" - $(INSTALL_DIR) "$(RELSYSDIR)/ebin" - $(INSTALL_DATA) $(TARGET_FILES) $(BEHAVIOUR_TARGET_FILES) "$(RELSYSDIR)/ebin" - -release_docs_spec: - -info: - @echo "APPLICATION = $(APPLICATION)" - @echo "INETS_DEBUG = $(INETS_DEBUG)" - @echo "INETS_FLAGS = $(INETS_FLAGS)" - @echo "ERL_COMPILE_FLAGS = $(ERL_COMPILE_FLAGS)" diff --git a/lib/inets/src/tftp/tftp.erl b/lib/inets/src/tftp/tftp.erl deleted file mode 100644 index c8804ea55f..0000000000 --- a/lib/inets/src/tftp/tftp.erl +++ /dev/null @@ -1,398 +0,0 @@ -%% -%% %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 -%%% Description : Trivial FTP -%%% Created : 18 May 2004 by Hakan Mattsson -%%%------------------------------------------------------------------- -%%% -%%% 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 - ]). - -%% 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(inets). - -%%------------------------------------------------------------------- -%% 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/inets/src/tftp/tftp.hrl b/lib/inets/src/tftp/tftp.hrl deleted file mode 100644 index 25543e0b9e..0000000000 --- a/lib/inets/src/tftp/tftp.hrl +++ /dev/null @@ -1,69 +0,0 @@ -%% -%% %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/inets/src/tftp/tftp_binary.erl b/lib/inets/src/tftp/tftp_binary.erl deleted file mode 100644 index 09adcfc41f..0000000000 --- a/lib/inets/src/tftp/tftp_binary.erl +++ /dev/null @@ -1,239 +0,0 @@ -%% -%% %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 -%%% Description : -%%% -%%% Created : 24 May 2004 by Hakan Mattsson -%%%------------------------------------------------------------------- - --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 -> - <> = 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/inets/src/tftp/tftp_engine.erl b/lib/inets/src/tftp/tftp_engine.erl deleted file mode 100644 index fb2c9749e5..0000000000 --- a/lib/inets/src/tftp/tftp_engine.erl +++ /dev/null @@ -1,1422 +0,0 @@ -%% -%% %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/inets/src/tftp/tftp_file.erl b/lib/inets/src/tftp/tftp_file.erl deleted file mode 100644 index 7664324808..0000000000 --- a/lib/inets/src/tftp/tftp_file.erl +++ /dev/null @@ -1,390 +0,0 @@ -%% -%% %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 -%%% Description : -%%% -%%% Created : 24 May 2004 by Hakan Mattsson -%%%------------------------------------------------------------------- - --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/inets/src/tftp/tftp_lib.erl b/lib/inets/src/tftp/tftp_lib.erl deleted file mode 100644 index 454754f0a3..0000000000 --- a/lib/inets/src/tftp/tftp_lib.erl +++ /dev/null @@ -1,474 +0,0 @@ -%% -%% %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 -%%% Description : Option parsing, decode, encode etc. -%%% -%%% Created : 18 May 2004 by Hakan Mattsson -%%%------------------------------------------------------------------- - --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 - <> -> - 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; - <> -> - 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_msg_data{block_no = SeqNo, data = Data}; - <> -> - #tftp_msg_ack{block_no = SeqNo}; - <> -> - 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; - <> -> - 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(<>, 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, - [ - <>, - Filename, - 0, - Mode, - 0, - [[Key, 0, Val, 0] || {Key, Val} <- Options] - ]; -encode_msg(#tftp_msg_data{block_no = BlockNo, data = Data}) when BlockNo =< 65535 -> - [ - <>, - Data - ]; -encode_msg(#tftp_msg_ack{block_no = BlockNo}) when BlockNo =< 65535 -> - <>; -encode_msg(#tftp_msg_error{code = Code, text = Text}) -> - IntCode = encode_error_code(Code), - [ - <>, - Text, - 0 - ]; -encode_msg(#tftp_msg_oack{options = Options}) -> - [ - <>, - [[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/inets/src/tftp/tftp_logger.erl b/lib/inets/src/tftp/tftp_logger.erl deleted file mode 100644 index a869958484..0000000000 --- a/lib/inets/src/tftp/tftp_logger.erl +++ /dev/null @@ -1,99 +0,0 @@ -%% -%% %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/inets/src/tftp/tftp_sup.erl b/lib/inets/src/tftp/tftp_sup.erl deleted file mode 100644 index 40b67c499c..0000000000 --- a/lib/inets/src/tftp/tftp_sup.erl +++ /dev/null @@ -1,111 +0,0 @@ -%% -%% %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 hangs under inets_sup. -%%---------------------------------------------------------------------- - --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. diff --git a/lib/inets/test/Makefile b/lib/inets/test/Makefile index 4dab4e14c6..0e33b72095 100644 --- a/lib/inets/test/Makefile +++ b/lib/inets/test/Makefile @@ -138,8 +138,6 @@ MODULES = \ httpd_test_lib \ inets_sup_SUITE \ inets_SUITE \ - tftp_test_lib \ - tftp_SUITE \ uri_SUITE \ inets_socketwrap_SUITE @@ -149,8 +147,7 @@ EBIN = . HRL_FILES = inets_test_lib.hrl \ inets_internal.hrl \ httpc_internal.hrl \ - http_internal.hrl \ - tftp_test_lib.hrl + http_internal.hrl ERL_FILES = $(MODULES:%=%.erl) @@ -165,8 +162,7 @@ INETS_FILES = inets.config $(INETS_SPECS) # SUB_SUITES = \ # inets_sup_suite \ # inets_httpd_suite \ -# inets_httpc_suite \ -# inets_tftp_suite +# inets_httpc_suite INETS_DATADIRS = inets_SUITE_data inets_socketwrap_SUITE_data @@ -299,10 +295,3 @@ info: @echo "INETS_PRIV_DIR = $(INETS_PRIV_DIR)" @echo "INETS_ROOT = $(INETS_ROOT)" @echo "INETS_FLAGS = $(INETS_FLAGS)" - -tftp: - erlc $(ERL_COMPILE_FLAGS) tftp_test_lib.erl tftp_SUITE.erl && erl -pa ../../inets/ebin -s tftp_SUITE t -s erlang halt - -tftp_work: - echo "tftp_test_lib:t([{tftp_SUITE, all}])." - erlc $(ERL_COMPILE_FLAGS) tftp_test_lib.erl tftp_SUITE.erl && erl -pa ../../inets/ebin diff --git a/lib/inets/test/inets_SUITE.erl b/lib/inets/test/inets_SUITE.erl index 12db2479d2..07ce594a1d 100644 --- a/lib/inets/test/inets_SUITE.erl +++ b/lib/inets/test/inets_SUITE.erl @@ -41,8 +41,7 @@ groups() -> [{services_test, [], [start_inets, start_httpc, - start_httpd, - start_tftpd + start_httpd ]}, {app_test, [], [app, appup]}]. @@ -297,41 +296,6 @@ start_httpd(Config) when is_list(Config) -> %%------------------------------------------------------------------------- -start_tftpd() -> - [{doc, "Start/stop of tfpd service"}]. -start_tftpd(Config) when is_list(Config) -> - process_flag(trap_exit, true), - ok = inets:start(), - {ok, Pid0} = inets:start(tftpd, [{host, "localhost"}, {port, 0}]), - Pids0 = [ServicePid || {_, ServicePid} <- inets:services()], - true = lists:member(Pid0, Pids0), - [_|_] = inets:services_info(), - inets:stop(tftpd, Pid0), - ct:sleep(100), - Pids1 = [ServicePid || {_, ServicePid} <- inets:services()], - false = lists:member(Pid0, Pids1), - {ok, Pid1} = - inets:start(tftpd, [{host, "localhost"}, {port, 0}], stand_alone), - Pids2 = [ServicePid || {_, ServicePid} <- inets:services()], - false = lists:member(Pid1, Pids2), - ok = inets:stop(stand_alone, Pid1), - receive - {'EXIT', Pid1, shutdown} -> - ok - after 100 -> - ct:fail(stand_alone_not_shutdown) - end, - ok = inets:stop(), - application:load(inets), - application:set_env(inets, services, [{tftpd,[{host, "localhost"}, - {port, 0}]}]), - ok = inets:start(), - (?NUM_DEFAULT_SERVICES + 1) = length(inets:services()), - application:unset_env(inets, services), - ok = inets:stop(). - -%%------------------------------------------------------------------------- - httpd_reload() -> [{doc, "Reload httpd configuration without restarting service"}]. httpd_reload(Config) when is_list(Config) -> diff --git a/lib/inets/test/inets_socketwrap_SUITE.erl b/lib/inets/test/inets_socketwrap_SUITE.erl index 7ea7e08ed1..fc87c595a9 100644 --- a/lib/inets/test/inets_socketwrap_SUITE.erl +++ b/lib/inets/test/inets_socketwrap_SUITE.erl @@ -30,7 +30,7 @@ suite() -> [{ct_hooks,[ts_install_cth]}]. all() -> - [start_httpd_fd, start_tftpd_fd]. + [start_httpd_fd]. init_per_suite(Config) -> case os:type() of @@ -90,37 +90,7 @@ start_httpd_fd(Config) when is_list(Config) -> ct:fail(open_port_failed) end end. -%%------------------------------------------------------------------------- -start_tftpd_fd() -> - [{doc, "Start/stop of tfpd service with socket wrapper"}]. -start_tftpd_fd(Config) when is_list(Config) -> - DataDir = proplists:get_value(data_dir, Config), - case setup_node_info(node()) of - {skip, _} = Skip -> - Skip; - {Node, NodeArg} -> - InetPort = inets_test_lib:inet_port(node()), - ct:pal("Node: ~p~n", [Node]), - Wrapper = filename:join(DataDir, "setuid_socket_wrap"), - Cmd = Wrapper ++ - " -s -tftpd_69,0:" ++ integer_to_list(InetPort) - ++ " -p " ++ os:find_executable("erl") ++ - " -- " ++ NodeArg, - ct:pal("cmd: ~p~n", [Cmd]), - case open_port({spawn, Cmd}, [stderr_to_stdout]) of - Port when is_port(Port) -> - wait_node_up(Node, 10), - ct:pal("~p", [rpc:call(Node, init, get_argument, [tftpd_69])]), - ok = rpc:call(Node, inets, start, []), - {ok, Pid} = rpc:call(Node, inets, start, - [tftpd,[{host, "localhost"}]]), - {ok, Info} = rpc:call(Node, tftp, info, [Pid]), - {value,{port, InetPort}} = lists:keysearch(port, 1, Info), - rpc:call(Node, erlang, halt, []); - _ -> - ct:fail(open_port_failed) - end - end. + %%------------------------------------------------------------------------- %% Internal functions %%------------------------------------------------------------------------- diff --git a/lib/inets/test/inets_sup_SUITE.erl b/lib/inets/test/inets_sup_SUITE.erl index d36d4dc53e..727e91e987 100644 --- a/lib/inets/test/inets_sup_SUITE.erl +++ b/lib/inets/test/inets_sup_SUITE.erl @@ -32,8 +32,7 @@ suite() -> ]. all() -> - [default_tree, tftpd_worker, - httpd_config, httpd_subtree, httpd_subtree_profile, + [default_tree, httpd_config, httpd_subtree, httpd_subtree_profile, httpc_subtree]. groups() -> @@ -147,13 +146,11 @@ default_tree() -> "in the default case."}]. default_tree(Config) when is_list(Config) -> TopSupChildren = supervisor:which_children(inets_sup), - 3 = length(TopSupChildren), + 2 = length(TopSupChildren), {value, {httpd_sup, _, supervisor,[httpd_sup]}} = lists:keysearch(httpd_sup, 1, TopSupChildren), {value, {httpc_sup, _,supervisor,[httpc_sup]}} = lists:keysearch(httpc_sup, 1, TopSupChildren), - {value, {tftp_sup,_,supervisor,[tftp_sup]}} = - lists:keysearch(tftp_sup, 1, TopSupChildren), HttpcSupChildren = supervisor:which_children(httpc_sup), {value, {httpc_profile_sup,_, supervisor, [httpc_profile_sup]}} = @@ -168,24 +165,7 @@ default_tree(Config) when is_list(Config) -> = supervisor:which_children(httpc_profile_sup), [] = supervisor:which_children(httpc_handler_sup), - - [] = supervisor:which_children(tftp_sup), - - ok. -tftpd_worker() -> - [{doc, "Makes sure the tftp sub tree is correct."}]. -tftpd_worker(Config) when is_list(Config) -> - [] = supervisor:which_children(tftp_sup), - {ok, Pid0} = inets:start(tftpd, [{host, inets_test_lib:hostname()}, - {port, 0}]), - {ok, _Pid1} = inets:start(tftpd, [{host, inets_test_lib:hostname()}, - {port, 0}], stand_alone), - - [{_,Pid0, worker, _}] = supervisor:which_children(tftp_sup), - inets:stop(tftpd, Pid0), - ct:sleep(5000), - [] = supervisor:which_children(tftp_sup), ok. httpd_config() -> diff --git a/lib/inets/test/tftp_SUITE.erl b/lib/inets/test/tftp_SUITE.erl deleted file mode 100644 index 09049e36af..0000000000 --- a/lib/inets/test/tftp_SUITE.erl +++ /dev/null @@ -1,949 +0,0 @@ -%% -%% %CopyrightBegin% -%% -%% Copyright Ericsson AB 2006-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_SUITE). - --compile(export_all). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% Includes and defines -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - --include("tftp_test_lib.hrl"). - --define(START_DAEMON(PortX, OptionsX), - fun(Port, Options) -> - {ok, Pid} = ?VERIFY({ok, _Pid}, tftp:start([{port, Port} | Options])), - if - Port == 0 -> - {ok, ActualOptions} = ?IGNORE(tftp:info(Pid)), - {value, {port, ActualPort}} = - lists:keysearch(port, 1, ActualOptions), - {ActualPort, Pid}; - true -> - {Port, Pid} - end - end(PortX, OptionsX)). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% API -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -t() -> - tftp_test_lib:t([{?MODULE, all}]). - -t(Cases) -> - tftp_test_lib:t(Cases, default_config()). - -t(Cases, Config) -> - tftp_test_lib:t(Cases, Config). - -default_config() -> - []. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% Test server callbacks -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -init_per_testcase(Case, Config) -> - tftp_test_lib:init_per_testcase(Case, Config). - -end_per_testcase(Case, Config) when is_list(Config) -> - tftp_test_lib:end_per_testcase(Case, Config). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% Top test case -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -suite() -> [{ct_hooks,[ts_install_cth]}]. - -all() -> - [simple, extra, reuse_connection, resend_client, - resend_server, large_file]. - -groups() -> - []. - -init_per_suite(Config) -> - Config. - -end_per_suite(_Config) -> - ok. - -init_per_group(_GroupName, Config) -> - Config. - -end_per_group(_GroupName, Config) -> - Config. - - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% Simple -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -simple(doc) -> - ["Start the daemon and perform simple a read and write."]; -simple(suite) -> - []; -simple(Config) when is_list(Config) -> - ?VERIFY(ok, application:start(inets)), - - {Port, DaemonPid} = ?IGNORE(?START_DAEMON(0, [{debug, brief}])), - - %% Read fail - RemoteFilename = "tftp_temporary_remote_test_file.txt", - LocalFilename = "tftp_temporary_local_test_file.txt", - Blob = list_to_binary(lists:duplicate(2000, $1)), - %% Blob = <<"Some file contents\n">>, - Size = size(Blob), - ?IGNORE(file:delete(RemoteFilename)), - ?VERIFY({error, {client_open, enoent, _}}, - tftp:read_file(RemoteFilename, binary, [{port, Port}])), - - %% Write and read - ?VERIFY({ok, Size}, tftp:write_file(RemoteFilename, Blob, [{port, Port}])), - ?VERIFY({ok, Blob}, tftp:read_file(RemoteFilename, binary, [{port, Port}])), - ?IGNORE(file:delete(LocalFilename)), - ?VERIFY({ok, Size}, tftp:read_file(RemoteFilename, LocalFilename, [{port, Port}])), - - %% Cleanup - unlink(DaemonPid), - exit(DaemonPid, kill), - ?VERIFY(ok, file:delete(LocalFilename)), - ?VERIFY(ok, file:delete(RemoteFilename)), - ?VERIFY(ok, application:stop(inets)), - ok. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% Extra -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -extra(doc) -> - ["Verify new stuff for IS 1.2."]; -extra(suite) -> - []; -extra(Config) when is_list(Config) -> - ?VERIFY({'EXIT', {badarg,{fake_key, fake_flag}}}, - tftp:start([{port, 0}, {fake_key, fake_flag}])), - - {Port, DaemonPid} = ?IGNORE(?START_DAEMON(0, [{debug, brief}])), - - RemoteFilename = "tftp_extra_temporary_remote_test_file.txt", - LocalFilename = "tftp_extra_temporary_local_test_file.txt", - Blob = <<"Some file contents\n">>, - Size = size(Blob), - Host = "127.0.0.1", - Peer = {inet, Host, Port}, - Generic = - [ - {state, []}, - {prepare, fun extra_prepare/6}, - {open, fun extra_open/6}, - {read, fun extra_read/1}, - {write, fun extra_write/2}, - {abort, fun extra_abort/3 } - ], - Options = [{host, Host}, - {port, Port}, - %%{ debug,all}, - {callback, {".*", tftp_test_lib, Generic}}], - ?VERIFY(ok, file:write_file(LocalFilename, Blob)), - ?VERIFY({ok, [{count, Size}, Peer]}, - tftp:write_file(RemoteFilename, LocalFilename, Options)), - ?VERIFY(ok, file:delete(LocalFilename)), - - ?VERIFY({ok,[{bin, Blob}, Peer]}, - tftp:read_file(RemoteFilename, LocalFilename, Options)), - - %% Cleanup - unlink(DaemonPid), - exit(DaemonPid, kill), - ?VERIFY(ok, file:delete(LocalFilename)), - ?VERIFY(ok, file:delete(RemoteFilename)), - ok. - --record(extra_state, {file, blksize, count, acc, peer}). - -%%------------------------------------------------------------------- -%% Prepare -%%------------------------------------------------------------------- - -extra_prepare(Peer, Access, LocalFilename, Mode, SuggestedOptions, []) -> - %% Client side - BlkSize = list_to_integer(tftp_test_lib:lookup_option("blksize", "512", SuggestedOptions)), - State = #extra_state{blksize = BlkSize, peer = Peer}, - extra_open(Peer, Access, LocalFilename, Mode, SuggestedOptions, State), - {ok, SuggestedOptions, State}; -extra_prepare(_Peer, _Access, _Bin, _Mode, _SuggestedOptions, _Initial) -> - {error, {undef, "Illegal callback options."}}. - -%%------------------------------------------------------------------- -%% Open -%%------------------------------------------------------------------- - -extra_open(Peer, Access, LocalFilename, Mode, SuggestedOptions, []) -> - %% Server side - case extra_prepare(Peer, Access, LocalFilename, Mode, SuggestedOptions, []) of - {ok, AcceptedOptions, []} -> - BlkSize = list_to_integer(tftp_test_lib:lookup_option("blksize", "512", AcceptedOptions)), - State = #extra_state{blksize = BlkSize, peer = Peer}, - extra_open(Peer, Access, LocalFilename, Mode, AcceptedOptions, State); - {error, {Code, Text}} -> - {error, {Code, Text}} - end; -extra_open(_Peer, Access, LocalFilename, _Mode, NegotiatedOptions, #extra_state{} = State) -> - {File, Acc} = - case Access of - read -> - if - is_binary(LocalFilename) -> - {undefined, LocalFilename}; - is_list(LocalFilename) -> - {ok, Bin} = file:read_file(LocalFilename), - {LocalFilename, Bin} - end; - write -> - {LocalFilename, []} - end, - %% Both sides - State2 = State#extra_state{file = File, acc = Acc, count = 0}, - {ok, NegotiatedOptions, State2}. - -%%------------------------------------------------------------------- -%% Read -%%------------------------------------------------------------------- - -extra_read(#extra_state{acc = Bin} = State) when is_binary(Bin) -> - BlkSize = State#extra_state.blksize, - Count = State#extra_state.count + size(Bin), - if - size(Bin) >= BlkSize -> - <> = Bin, - State2 = State#extra_state{acc = Bin2, count = Count}, - {more, Block, State2}; - size(Bin) < BlkSize -> - Res = [{count, Count}, State#extra_state.peer], - {last, Bin, Res} - end. - -%%------------------------------------------------------------------- -%% Write -%%------------------------------------------------------------------- - -extra_write(Bin, #extra_state{acc = List} = State) when is_binary(Bin), is_list(List) -> - Size = size(Bin), - BlkSize = State#extra_state.blksize, - if - Size == BlkSize -> - {more, State#extra_state{acc = [Bin | List]}}; - Size < BlkSize -> - Bin2 = list_to_binary(lists:reverse([Bin | List])), - Res = [{bin, Bin2}, State#extra_state.peer], - file:write_file(State#extra_state.file, Bin2), - {last, Res} - end. - -%%------------------------------------------------------------------- -%% Abort -%%------------------------------------------------------------------- - -extra_abort(_Code, _Text, #extra_state{}) -> - ok. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% Re-send client -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -resend_client(doc) -> - ["Verify that the server behaves correctly when the client re-sends packets."]; -resend_client(suite) -> - []; -resend_client(Config) when is_list(Config) -> - Host = {127, 0, 0, 1}, - {Port, DaemonPid} = ?IGNORE(?START_DAEMON(0, [{debug, all}])), - - ?VERIFY(ok, resend_read_client(Host, Port, 10)), - ?VERIFY(ok, resend_read_client(Host, Port, 512)), - ?VERIFY(ok, resend_read_client(Host, Port, 1025)), - - ?VERIFY(ok, resend_write_client(Host, Port, 10)), - ?VERIFY(ok, resend_write_client(Host, Port, 512)), - ?VERIFY(ok, resend_write_client(Host, Port, 1025)), - - %% Cleanup - unlink(DaemonPid), - exit(DaemonPid, kill), - ok. - -resend_read_client(Host, Port, BlkSize) -> - RemoteFilename = "tftp_resend_read_client.tmp", - Block1 = lists:duplicate(BlkSize, $1), - Block2 = lists:duplicate(BlkSize, $2), - Block3 = lists:duplicate(BlkSize, $3), - Block4 = lists:duplicate(BlkSize, $4), - Block5 = lists:duplicate(BlkSize, $5), - Blocks = [Block1, Block2, Block3, Block4, Block5], - Blob = list_to_binary(Blocks), - ?VERIFY(ok, file:write_file(RemoteFilename, Blob)), - - Timeout = timer:seconds(3), - ?VERIFY(timeout, recv(0)), - - %% Open socket - {ok, Socket} = ?VERIFY({ok, _}, gen_udp:open(0, [binary, {reuseaddr, true}, {active, true}])), - - ReadList = [0, 1, RemoteFilename, 0, "octet", 0], - Data1Bin = list_to_binary([0, 3, 0, 1 | Block1]), - NewPort = - if - BlkSize =:= 512 -> - %% Send READ - ReadBin = list_to_binary(ReadList), - ?VERIFY(ok, gen_udp:send(Socket, Host, Port, ReadBin)), - - %% Sleep a while in order to provoke the server to re-send the packet - timer:sleep(Timeout + timer:seconds(1)), - - %% Recv DATA #1 (the packet that the server think that we have lost) - {udp, _, _, NewPort0, _} = ?VERIFY({udp, Socket, Host, _, Data1Bin}, recv(Timeout)), - NewPort0; - true -> - %% Send READ - BlkSizeList = integer_to_list(BlkSize), - Options = ["blksize", 0, BlkSizeList, 0], - ReadBin = list_to_binary([ReadList | Options]), - ?VERIFY(ok, gen_udp:send(Socket, Host, Port, ReadBin)), - - %% Recv OACK - OptionAckBin = list_to_binary([0, 6 | Options]), - {udp, _, _, NewPort0, _} = ?VERIFY({udp, Socket, Host, _, OptionAckBin}, recv(Timeout)), - - %% Send ACK #0 - Ack0Bin = <<0, 4, 0, 0>>, - ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort0, Ack0Bin)), - - %% Send ACK #0 AGAIN (pretend that we timed out) - timer:sleep(timer:seconds(1)), - ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort0, Ack0Bin)), - - %% Recv DATA #1 (the packet that the server think that we have lost) - ?VERIFY({udp, Socket, Host, NewPort0, Data1Bin}, recv(Timeout)), - NewPort0 - end, - - %% Recv DATA #1 AGAIN (the re-sent package) - ?VERIFY({udp, Socket, Host, NewPort, Data1Bin}, recv(Timeout)), - - %% Send ACK #1 - Ack1Bin = <<0, 4, 0, 1>>, - ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Ack1Bin)), - - %% Recv DATA #2 - Data2Bin = list_to_binary([0, 3, 0, 2 | Block2]), - ?VERIFY({udp, Socket, Host, NewPort, Data2Bin}, recv(Timeout)), - - %% Send ACK #2 - Ack2Bin = <<0, 4, 0, 2>>, - ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Ack2Bin)), - - %% Recv DATA #3 - Data3Bin = list_to_binary([0, 3, 0, 3 | Block3]), - ?VERIFY({udp, Socket, Host, NewPort, Data3Bin}, recv(Timeout)), - - %% Send ACK #3 - Ack3Bin = <<0, 4, 0, 3>>, - ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Ack3Bin)), - - %% Send ACK #3 AGAIN (pretend that we timed out) - timer:sleep(timer:seconds(1)), - ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Ack3Bin)), - - %% Recv DATA #4 (the packet that the server think that we have lost) - Data4Bin = list_to_binary([0, 3, 0, 4 | Block4]), - ?VERIFY({udp, Socket, Host, NewPort, Data4Bin}, recv(Timeout)), - - %% Recv DATA #4 AGAIN (the re-sent package) - ?VERIFY({udp, Socket, Host, NewPort, Data4Bin}, recv(Timeout)), - - %% Send ACK #2 which is out of range - ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Ack2Bin)), - - %% Send ACK #4 - Ack4Bin = <<0, 4, 0, 4>>, - ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Ack4Bin)), - - %% Recv DATA #5 - Data5Bin = list_to_binary([0, 3, 0, 5 | Block5]), - ?VERIFY({udp, Socket, Host, NewPort, Data5Bin}, recv(Timeout)), - - %% Send ACK #5 - Ack5Bin = <<0, 4, 0, 5>>, - ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Ack5Bin)), - - %% Close socket - ?VERIFY(ok, gen_udp:close(Socket)), - - ?VERIFY(timeout, recv(Timeout)), - ?VERIFY(ok, file:delete(RemoteFilename)), - ok. - -resend_write_client(Host, Port, BlkSize) -> - RemoteFilename = "tftp_resend_write_client.tmp", - Block1 = lists:duplicate(BlkSize, $1), - Block2 = lists:duplicate(BlkSize, $2), - Block3 = lists:duplicate(BlkSize, $3), - Block4 = lists:duplicate(BlkSize, $4), - Block5 = lists:duplicate(BlkSize, $5), - Blocks = [Block1, Block2, Block3, Block4, Block5], - Blob = list_to_binary(Blocks), - ?IGNORE(file:delete(RemoteFilename)), - ?VERIFY({error, enoent}, file:read_file(RemoteFilename)), - - Timeout = timer:seconds(3), - ?VERIFY(timeout, recv(0)), - - %% Open socket - {ok, Socket} = ?VERIFY({ok, _}, gen_udp:open(0, [binary, {reuseaddr, true}, {active, true}])), - - WriteList = [0, 2, RemoteFilename, 0, "octet", 0], - NewPort = - if - BlkSize =:= 512 -> - %% Send WRITE - WriteBin = list_to_binary(WriteList), - ?VERIFY(ok, gen_udp:send(Socket, Host, Port, WriteBin)), - - %% Sleep a while in order to provoke the server to re-send the packet - timer:sleep(Timeout + timer:seconds(1)), - - %% Recv ACK #0 (the packet that the server think that we have lost) - Ack0Bin = <<0, 4, 0, 0>>, - ?VERIFY({udp, Socket, Host, _, Ack0Bin}, recv(Timeout)), - - %% Recv ACK #0 AGAIN (the re-sent package) - {udp, _, _, NewPort0, _} = ?VERIFY({udp, Socket, Host, _, Ack0Bin}, recv(Timeout)), - NewPort0; - true -> - %% Send WRITE - BlkSizeList = integer_to_list(BlkSize), - WriteBin = list_to_binary([WriteList, "blksize", 0, BlkSizeList, 0]), - ?VERIFY(ok, gen_udp:send(Socket, Host, Port, WriteBin)), - - %% Sleep a while in order to provoke the server to re-send the packet - timer:sleep(timer:seconds(1)), - - %% Recv OACK (the packet that the server think that we have lost) - OptionAckBin = list_to_binary([0, 6, "blksize",0, BlkSizeList, 0]), - ?VERIFY({udp, Socket, Host, _, OptionAckBin}, recv(Timeout)), - - %% Recv OACK AGAIN (the re-sent package) - {udp, _, _, NewPort0, _} = ?VERIFY({udp, Socket, Host, _, OptionAckBin}, recv(Timeout)), - NewPort0 - end, - - %% Send DATA #1 - Data1Bin = list_to_binary([0, 3, 0, 1 | Block1]), - ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Data1Bin)), - - %% Recv ACK #1 - Ack1Bin = <<0, 4, 0, 1>>, - ?VERIFY({udp, Socket, Host, NewPort, Ack1Bin}, recv(Timeout)), - - %% Send DATA #2 - Data2Bin = list_to_binary([0, 3, 0, 2 | Block2]), - ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Data2Bin)), - - %% Recv ACK #2 - Ack2Bin = <<0, 4, 0, 2>>, - ?VERIFY({udp, Socket, Host, NewPort, Ack2Bin}, recv(Timeout)), - - %% Send DATA #3 - Data3Bin = list_to_binary([0, 3, 0, 3 | Block3]), - ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Data3Bin)), - - %% Recv ACK #3 - Ack3Bin = <<0, 4, 0, 3>>, - ?VERIFY({udp, Socket, Host, NewPort, Ack3Bin}, recv(Timeout)), - - %% Send DATA #3 AGAIN (pretend that we timed out) - timer:sleep(timer:seconds(1)), - ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Data3Bin)), - - %% Recv ACK #3 AGAIN (the packet that the server think that we have lost) - ?VERIFY({udp, Socket, Host, NewPort, Ack3Bin}, recv(Timeout)), - - %% Send DATA #2 which is out of range - ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Data2Bin)), - - %% Send DATA #4 - Data4Bin = list_to_binary([0, 3, 0, 4 | Block4]), - ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Data4Bin)), - - %% Recv ACK #4 - Ack4Bin = <<0, 4, 0, 4>>, - ?VERIFY({udp, Socket, Host, NewPort, Ack4Bin}, recv(Timeout)), - - %% Send DATA #5 - Data5Bin = list_to_binary([0, 3, 0, 5 | Block5]), - ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Data5Bin)), - - %% Recv ACK #5 - Ack5Bin = <<0, 4, 0, 5>>, - ?VERIFY({udp, Socket, Host, NewPort, Ack5Bin}, recv(Timeout)), - - %% Close socket - ?VERIFY(ok, gen_udp:close(Socket)), - - ?VERIFY(timeout, recv(Timeout)), - ?VERIFY({ok, Blob}, file:read_file(RemoteFilename)), - ?VERIFY(ok, file:delete(RemoteFilename)), - ok. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% Re-send server -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -resend_server(doc) -> - ["Verify that the server behaves correctly when the server re-sends packets."]; -resend_server(suite) -> - []; -resend_server(Config) when is_list(Config) -> - Host = {127, 0, 0, 1}, - - ?VERIFY(ok, resend_read_server(Host, 10)), - ?VERIFY(ok, resend_read_server(Host, 512)), - ?VERIFY(ok, resend_read_server(Host, 1025)), - - ?VERIFY(ok, resend_write_server(Host, 10)), - ?VERIFY(ok, resend_write_server(Host, 512)), - ?VERIFY(ok, resend_write_server(Host, 1025)), - ok. - -resend_read_server(Host, BlkSize) -> - RemoteFilename = "tftp_resend_read_server.tmp", - Block1 = lists:duplicate(BlkSize, $1), - Block2 = lists:duplicate(BlkSize, $2), - Block3 = lists:duplicate(BlkSize, $3), - Block4 = lists:duplicate(BlkSize, $4), - Block5 = lists:duplicate(BlkSize, $5), - Block6 = [], - Blocks = [Block1, Block2, Block3, Block4, Block5, Block6], - Blob = list_to_binary(Blocks), - - Timeout = timer:seconds(3), - ?VERIFY(timeout, recv(0)), - - %% Open daemon socket - {ok, DaemonSocket} = ?VERIFY({ok, _}, gen_udp:open(0, [binary, {reuseaddr, true}, {active, true}])), - {ok, DaemonPort} = ?IGNORE(inet:port(DaemonSocket)), - - %% Open server socket - {ok, ServerSocket} = ?VERIFY({ok, _}, gen_udp:open(0, [binary, {reuseaddr, true}, {active, true}])), - ?IGNORE(inet:port(ServerSocket)), - - %% Prepare client process - ReplyTo = self(), - ClientFun = - fun(Extra) -> - Options = [{port, DaemonPort}, {debug, brief}] ++ Extra, - Res = ?VERIFY({ok, Blob}, tftp:read_file(RemoteFilename, binary, Options)), - ReplyTo ! {self(), {tftp_client_reply, Res}}, - exit(normal) - end, - - ReadList = [0, 1, RemoteFilename, 0, "octet", 0], - Data1Bin = list_to_binary([0, 3, 0, 1 | Block1]), - Ack1Bin = <<0, 4, 0, 1>>, - {ClientPort, ClientPid} = - if - BlkSize =:= 512 -> - %% Start client process - ClientPid0 = spawn_link(fun() -> ClientFun([]) end), - - %% Recv READ - ReadBin = list_to_binary(ReadList), - {udp, _, _, ClientPort0, _} = ?VERIFY({udp, DaemonSocket, Host, _, ReadBin}, recv(Timeout)), - - %% Send DATA #1 - ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort0, Data1Bin)), - - %% Sleep a while in order to provoke the client to re-send the packet - timer:sleep(Timeout + timer:seconds(1)), - - %% Recv ACK #1 (the packet that the server think that we have lost) - ?VERIFY({udp, ServerSocket, Host, ClientPort0, Ack1Bin}, recv(Timeout)), - - %% Recv ACK #1 AGAIN (the re-sent package) - ?VERIFY({udp, ServerSocket, Host, _, Ack1Bin}, recv(Timeout)), - {ClientPort0, ClientPid0}; - true -> - %% Start client process - BlkSizeList = integer_to_list(BlkSize), - ClientPid0 = spawn_link(fun() -> ClientFun([{"blksize", BlkSizeList}]) end), - - %% Recv READ - Options = ["blksize", 0, BlkSizeList, 0], - ReadBin = list_to_binary([ReadList | Options]), - {udp, _, _, ClientPort0, _} = ?VERIFY({udp, DaemonSocket, Host, _, ReadBin}, recv(Timeout)), - - %% Send OACK - BlkSizeList = integer_to_list(BlkSize), - OptionAckBin = list_to_binary([0, 6, "blksize",0, BlkSizeList, 0]), - ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort0, OptionAckBin)), - - %% Sleep a while in order to provoke the client to re-send the packet - timer:sleep(Timeout + timer:seconds(1)), - - %% Recv ACK #0 (the packet that the server think that we have lost) - Ack0Bin = <<0, 4, 0, 0>>, - ?VERIFY({udp, ServerSocket, Host, ClientPort0, Ack0Bin}, recv(Timeout)), - - %% Recv ACK #0 AGAIN (the re-sent package) - ?VERIFY({udp, ServerSocket, Host, ClientPort0, Ack0Bin}, recv(Timeout)), - - %% Send DATA #1 - ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort0, Data1Bin)), - - %% Recv ACK #1 - ?VERIFY({udp, ServerSocket, Host, _, Ack1Bin}, recv(Timeout)), - {ClientPort0, ClientPid0} - end, - - %% Send DATA #2 - Data2Bin = list_to_binary([0, 3, 0, 2 | Block2]), - ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Data2Bin)), - - %% Recv ACK #2 - Ack2Bin = <<0, 4, 0, 2>>, - ?VERIFY({udp, ServerSocket, Host, ClientPort, Ack2Bin}, recv(Timeout)), - - %% Send DATA #3 - Data3Bin = list_to_binary([0, 3, 0, 3 | Block3]), - ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Data3Bin)), - - %% Recv ACK #3 - Ack3Bin = <<0, 4, 0, 3>>, - ?VERIFY({udp, ServerSocket, Host, ClientPort, Ack3Bin}, recv(Timeout)), - - %% Send DATA #3 AGAIN (pretend that we timed out) - timer:sleep(timer:seconds(1)), - ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Data3Bin)), - - %% Recv ACK #3 AGAIN (the packet that the server think that we have lost) - ?VERIFY({udp, ServerSocket, Host, ClientPort, Ack3Bin}, recv(Timeout)), - - %% Send DATA #4 - Data4Bin = list_to_binary([0, 3, 0, 4 | Block4]), - ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Data4Bin)), - - %% Recv ACK #4 - Ack4Bin = <<0, 4, 0, 4>>, - ?VERIFY({udp, ServerSocket, Host, ClientPort, Ack4Bin}, recv(Timeout)), - - %% Send DATA #3 which is out of range - ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Data3Bin)), - - %% Send DATA #5 - Data5Bin = list_to_binary([0, 3, 0, 5 | Block5]), - ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Data5Bin)), - - %% Recv ACK #5 - Ack5Bin = <<0, 4, 0, 5>>, - ?VERIFY({udp, ServerSocket, Host, ClientPort, Ack5Bin}, recv(Timeout)), - - %% Send DATA #6 - Data6Bin = list_to_binary([0, 3, 0, 6 | Block6]), - ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Data6Bin)), - - %% Close daemon and server sockets - ?VERIFY(ok, gen_udp:close(ServerSocket)), - ?VERIFY(ok, gen_udp:close(DaemonSocket)), - - ?VERIFY({ClientPid, {tftp_client_reply, {ok, Blob}}}, recv(Timeout)), - - ?VERIFY(timeout, recv(Timeout)), - ok. - -resend_write_server(Host, BlkSize) -> - RemoteFilename = "tftp_resend_write_server.tmp", - Block1 = lists:duplicate(BlkSize, $1), - Block2 = lists:duplicate(BlkSize, $2), - Block3 = lists:duplicate(BlkSize, $3), - Block4 = lists:duplicate(BlkSize, $4), - Block5 = lists:duplicate(BlkSize, $5), - Block6 = [], - Blocks = [Block1, Block2, Block3, Block4, Block5, Block6], - Blob = list_to_binary(Blocks), - Size = size(Blob), - - Timeout = timer:seconds(3), - ?VERIFY(timeout, recv(0)), - - %% Open daemon socket - {ok, DaemonSocket} = ?VERIFY({ok, _}, gen_udp:open(0, [binary, {reuseaddr, true}, {active, true}])), - {ok, DaemonPort} = ?IGNORE(inet:port(DaemonSocket)), - - %% Open server socket - {ok, ServerSocket} = ?VERIFY({ok, _}, gen_udp:open(0, [binary, {reuseaddr, true}, {active, true}])), - ?IGNORE(inet:port(ServerSocket)), - - %% Prepare client process - ReplyTo = self(), - ClientFun = - fun(Extra) -> - Options = [{port, DaemonPort}, {debug, brief}] ++ Extra, - Res = ?VERIFY({ok, Size}, tftp:write_file(RemoteFilename, Blob, Options)), - ReplyTo ! {self(), {tftp_client_reply, Res}}, - exit(normal) - end, - - WriteList = [0, 2, RemoteFilename, 0, "octet", 0], - Data1Bin = list_to_binary([0, 3, 0, 1 | Block1]), - {ClientPort, ClientPid} = - if - BlkSize =:= 512 -> - %% Start client process - ClientPid0 = spawn_link(fun() -> ClientFun([]) end), - - %% Recv WRITE - WriteBin = list_to_binary(WriteList), - io:format("WriteBin ~p\n", [WriteBin]), - {udp, _, _, ClientPort0, _} = ?VERIFY({udp, DaemonSocket, Host, _, WriteBin}, recv(Timeout)), - - %% Send ACK #1 - Ack0Bin = <<0, 4, 0, 0>>, - ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort0, Ack0Bin)), - - %% Sleep a while in order to provoke the client to re-send the packet - timer:sleep(Timeout + timer:seconds(1)), - - %% Recv DATA #1 (the packet that the server think that we have lost) - ?VERIFY({udp, ServerSocket, Host, ClientPort0, Data1Bin}, recv(Timeout)), - - %% Recv DATA #1 AGAIN (the re-sent package) - ?VERIFY({udp, ServerSocket, Host, _, Data1Bin}, recv(Timeout)), - {ClientPort0, ClientPid0}; - true -> - %% Start client process - BlkSizeList = integer_to_list(BlkSize), - ClientPid0 = spawn_link(fun() -> ClientFun([{"blksize", BlkSizeList}]) end), - - %% Recv WRITE - Options = ["blksize", 0, BlkSizeList, 0], - WriteBin = list_to_binary([WriteList | Options]), - {udp, _, _, ClientPort0, _} = ?VERIFY({udp, DaemonSocket, Host, _, WriteBin}, recv(Timeout)), - - %% Send OACK - BlkSizeList = integer_to_list(BlkSize), - OptionAckBin = list_to_binary([0, 6, "blksize",0, BlkSizeList, 0]), - ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort0, OptionAckBin)), - - %% Sleep a while in order to provoke the client to re-send the packet - timer:sleep(Timeout + timer:seconds(1)), - - %% Recv DATA #1 (the packet that the server think that we have lost) - ?VERIFY({udp, ServerSocket, Host, ClientPort0, Data1Bin}, recv(Timeout)), - - %% Recv DATA #1 AGAIN (the re-sent package) - ?VERIFY({udp, ServerSocket, Host, ClientPort0, Data1Bin}, recv(Timeout)), - {ClientPort0, ClientPid0} - end, - - %% Send ACK #1 - Ack1Bin = <<0, 4, 0, 1>>, - ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Ack1Bin)), - - %% Recv DATA #2 - Data2Bin = list_to_binary([0, 3, 0, 2 | Block2]), - ?VERIFY({udp, ServerSocket, Host, ClientPort, Data2Bin}, recv(Timeout)), - - %% Send ACK #2 - Ack2Bin = <<0, 4, 0, 2>>, - ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Ack2Bin)), - - %% Recv DATA #3 - Data3Bin = list_to_binary([0, 3, 0, 3 | Block3]), - ?VERIFY({udp, ServerSocket, Host, ClientPort, Data3Bin}, recv(Timeout)), - - %% Send ACK #3 - Ack3Bin = <<0, 4, 0, 3>>, - ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Ack3Bin)), - - %% Send ACK #3 AGAIN (pretend that we timed out) - timer:sleep(timer:seconds(1)), - ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Ack3Bin)), - - %% Recv DATA #4 (the packet that the server think that we have lost) - Data4Bin = list_to_binary([0, 3, 0, 4 | Block4]), - ?VERIFY({udp, ServerSocket, Host, ClientPort, Data4Bin}, recv(Timeout)), - - %% Recv DATA #4 AGAIN (the re-sent package) - ?VERIFY({udp, ServerSocket, Host, ClientPort, Data4Bin}, recv(Timeout)), - - %% Send ACK #4 - Ack4Bin = <<0, 4, 0, 4>>, - ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Ack4Bin)), - - %% Recv DATA #5 - Data5Bin = list_to_binary([0, 3, 0, 5 | Block5]), - ?VERIFY({udp, ServerSocket, Host, ClientPort, Data5Bin}, recv(Timeout)), - - %% Send ACK #3 which is out of range - ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Ack3Bin)), - - %% Send ACK #5 - Ack5Bin = <<0, 4, 0, 5>>, - ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Ack5Bin)), - - %% Recv DATA #6 - Data6Bin = list_to_binary([0, 3, 0, 6 | Block6]), - ?VERIFY({udp, ServerSocket, Host, ClientPort, Data6Bin}, recv(Timeout)), - - %% Send ACK #6 - Ack6Bin = <<0, 4, 0, 6>>, - ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Ack6Bin)), - - %% Close daemon and server sockets - ?VERIFY(ok, gen_udp:close(ServerSocket)), - ?VERIFY(ok, gen_udp:close(DaemonSocket)), - - ?VERIFY({ClientPid, {tftp_client_reply, {ok, Size}}}, recv(Timeout)), - - ?VERIFY(timeout, recv(Timeout)), - ok. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -reuse_connection(doc) -> - ["Verify that the server can reuse an ongiong connection when same client resends request."]; -reuse_connection(suite) -> - []; -reuse_connection(Config) when is_list(Config) -> - Host = {127, 0, 0, 1}, - {Port, DaemonPid} = ?IGNORE(?START_DAEMON(0, [{debug, all}])), - - RemoteFilename = "reuse_connection.tmp", - BlkSize = 512, - Block1 = lists:duplicate(BlkSize, $1), - Block2 = lists:duplicate(BlkSize div 2, $2), - Blocks = [Block1, Block2], - Blob = list_to_binary(Blocks), - ?VERIFY(ok, file:write_file(RemoteFilename, Blob)), - - Seconds = 3, - Timeout = timer:seconds(Seconds), - ?VERIFY(timeout, recv(0)), - - %% Open socket - {ok, Socket} = ?VERIFY({ok, _}, gen_udp:open(0, [binary, {reuseaddr, true}, {active, true}])), - - ReadList = [0, 1, RemoteFilename, 0, "octet", 0], - Data1Bin = list_to_binary([0, 3, 0, 1 | Block1]), - - %% Send READ - TimeoutList = integer_to_list(Seconds), - Options = ["timeout", 0, TimeoutList, 0], - ReadBin = list_to_binary([ReadList | Options]), - ?VERIFY(ok, gen_udp:send(Socket, Host, Port, ReadBin)), - - %% Send yet another READ for same file - ?VERIFY(ok, gen_udp:send(Socket, Host, Port, ReadBin)), - - %% Recv OACK - OptionAckBin = list_to_binary([0, 6 | Options]), - {udp, _, _, NewPort, _} = ?VERIFY({udp, Socket, Host, _, OptionAckBin}, recv(Timeout)), - - %% Send ACK #0 - Ack0Bin = <<0, 4, 0, 0>>, - ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Ack0Bin)), - - %% Recv DATA #1 - ?VERIFY({udp, Socket, Host, NewPort, Data1Bin}, recv(Timeout)), - - %% Send ACK #1 - Ack1Bin = <<0, 4, 0, 1>>, - ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Ack1Bin)), - - %% Recv DATA #2 - Data2Bin = list_to_binary([0, 3, 0, 2 | Block2]), - ?VERIFY({udp, Socket, Host, NewPort, Data2Bin}, recv(Timeout)), - - %% Send ACK #2 - Ack2Bin = <<0, 4, 0, 2>>, - ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Ack2Bin)), - - %% Close socket - ?VERIFY(ok, gen_udp:close(Socket)), - - ?VERIFY(timeout, recv(Timeout)), - ?VERIFY(ok, file:delete(RemoteFilename)), - - %% Cleanup - unlink(DaemonPid), - exit(DaemonPid, kill), - ok. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% Large file: transfer > 65535 blocks -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -large_file(doc) -> - ["Start the daemon and test transfer of files greater than 32M."]; -large_file(suite) -> - []; -large_file(Config) when is_list(Config) -> - ?VERIFY(ok, application:start(inets)), - - {Port, DaemonPid} = ?IGNORE(?START_DAEMON(0, [{debug, brief}])), - - %% Read fail - RemoteFilename = "tftp_temporary_large_file_remote_test_file.txt", - LocalFilename = "tftp_temporary_large_file_local_test_file.txt", - - {ok, FH} = file:open(LocalFilename, [write,exclusive]), - {ok, Size} = file:position(FH, {eof, 2*512*65535}), - ok = file:truncate(FH), - ?IGNORE(file:close(FH)), - - %% Write and read - ?VERIFY({ok, Size}, tftp:write_file(RemoteFilename, LocalFilename, [{port, Port}])), - ?IGNORE(file:delete(LocalFilename)), - ?VERIFY({ok, Size}, tftp:read_file(RemoteFilename, LocalFilename, [{port, Port}])), - - %% Cleanup - unlink(DaemonPid), - exit(DaemonPid, kill), - ?VERIFY(ok, file:delete(LocalFilename)), - ?VERIFY(ok, file:delete(RemoteFilename)), - ?VERIFY(ok, application:stop(inets)), - ok. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% Goodies -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -recv(Timeout) -> - receive - Msg -> - Msg - after Timeout -> - timeout - end. diff --git a/lib/inets/test/tftp_test_lib.erl b/lib/inets/test/tftp_test_lib.erl deleted file mode 100644 index f07795324f..0000000000 --- a/lib/inets/test/tftp_test_lib.erl +++ /dev/null @@ -1,308 +0,0 @@ -%% -%% %CopyrightBegin% -%% -%% Copyright Ericsson AB 2007-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_test_lib). - --compile(export_all). - --include("tftp_test_lib.hrl"). - -%% -%% ----- -%% - -init_per_testcase(_Case, Config) when is_list(Config) -> - io:format("\n ", []), - ?IGNORE(application:stop(inets)), - Config. - -end_per_testcase(_Case, Config) when is_list(Config) -> - ?IGNORE(application:stop(inets)), - Config. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% Infrastructure for test suite -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -error(Actual, Mod, Line) -> - (catch global:send(tftp_global_logger, {failed, Mod, Line})), - log(" Bad result: ~p\n", [Actual], Mod, Line), - Label = lists:concat([Mod, "(", Line, ") unexpected result"]), - et:report_event(60, Mod, Mod, Label, - [{line, Mod, Line}, {error, Actual}]), - case global:whereis_name(tftp_test_case_sup) of - undefined -> - ignore; - Pid -> - Fail = #'REASON'{mod = Mod, line = Line, desc = Actual}, - Pid ! {fail, self(), Fail} - end, - Actual. - -log(Format, Args, Mod, Line) -> - case global:whereis_name(tftp_global_logger) of - undefined -> - io:format(user, "~p(~p): " ++ Format, - [Mod, Line] ++ Args); - Pid -> - io:format(Pid, "~p(~p): " ++ Format, - [Mod, Line] ++ Args) - end. - -default_config() -> - []. - -t() -> - t([{?MODULE, all}]). - -t(Cases) -> - t(Cases, default_config()). - -t(Cases, Config) -> - process_flag(trap_exit, true), - Res = lists:flatten(do_test(Cases, Config)), - io:format("Res: ~p\n", [Res]), - display_result(Res), - Res. - -do_test({Mod, Fun}, Config) when is_atom(Mod), is_atom(Fun) -> - case catch apply(Mod, Fun, [suite]) of - [] -> - io:format("Eval: ~p:", [{Mod, Fun}]), - Res = eval(Mod, Fun, Config), - {R, _, _} = Res, - io:format(" ~p\n", [R]), - Res; - - Cases when is_list(Cases) -> - io:format("Expand: ~p ...\n", [{Mod, Fun}]), - Map = fun(Case) when is_atom(Case)-> {Mod, Case}; - (Case) -> Case - end, - do_test(lists:map(Map, Cases), Config); - - {req, _, {conf, Init, Cases, Finish}} -> - case (catch apply(Mod, Init, [Config])) of - Conf when is_list(Conf) -> - io:format("Expand: ~p ...\n", [{Mod, Fun}]), - Map = fun(Case) when is_atom(Case)-> {Mod, Case}; - (Case) -> Case - end, - Res = do_test(lists:map(Map, Cases), Conf), - (catch apply(Mod, Finish, [Conf])), - Res; - - {'EXIT', {skipped, Reason}} -> - io:format(" => skipping: ~p\n", [Reason]), - [{skipped, {Mod, Fun}, Reason}]; - - Error -> - io:format(" => failed: ~p\n", [Error]), - [{failed, {Mod, Fun}, Error}] - end; - - {'EXIT', {undef, _}} -> - io:format("Undefined: ~p\n", [{Mod, Fun}]), - [{nyi, {Mod, Fun}, ok}]; - - Error -> - io:format("Ignoring: ~p: ~p\n", [{Mod, Fun}, Error]), - [{failed, {Mod, Fun}, Error}] - end; -do_test(Mod, Config) when is_atom(Mod) -> - Res = do_test({Mod, all}, Config), - Res; -do_test(Cases, Config) when is_list(Cases) -> - [do_test(Case, Config) || Case <- Cases]; -do_test(Bad, _Config) -> - [{badarg, Bad, ok}]. - -eval(Mod, Fun, Config) -> - TestCase = {?MODULE, Mod, Fun}, - Label = lists:concat(["TEST CASE: ", Fun]), - et:report_event(40, ?MODULE, Mod, Label ++ " started", - [TestCase, Config]), - global:register_name(tftp_test_case_sup, self()), - Flag = process_flag(trap_exit, true), - Config2 = Mod:init_per_testcase(Fun, Config), - Pid = spawn_link(?MODULE, do_eval, [self(), Mod, Fun, Config2]), - R = wait_for_evaluator(Pid, Mod, Fun, Config2, []), - Mod:end_per_testcase(Fun, Config2), - global:unregister_name(tftp_test_case_sup), - process_flag(trap_exit, Flag), - R. - -wait_for_evaluator(Pid, Mod, Fun, Config, Errors) -> - TestCase = {?MODULE, Mod, Fun}, - Label = lists:concat(["TEST CASE: ", Fun]), - receive - {done, Pid, ok} when Errors == [] -> - et:report_event(40, Mod, ?MODULE, Label ++ " ok", - [TestCase, Config]), - {ok, {Mod, Fun}, Errors}; - {done, Pid, {ok, _}} when Errors == [] -> - et:report_event(40, Mod, ?MODULE, Label ++ " ok", - [TestCase, Config]), - {ok, {Mod, Fun}, Errors}; - {done, Pid, Fail} -> - et:report_event(20, Mod, ?MODULE, Label ++ " failed", - [TestCase, Config, {return, Fail}, Errors]), - {failed, {Mod,Fun}, Fail}; - {'EXIT', Pid, {skipped, Reason}} -> - et:report_event(20, Mod, ?MODULE, Label ++ " skipped", - [TestCase, Config, {skipped, Reason}]), - {skipped, {Mod, Fun}, Errors}; - {'EXIT', Pid, Reason} -> - et:report_event(20, Mod, ?MODULE, Label ++ " crashed", - [TestCase, Config, {'EXIT', Reason}]), - {crashed, {Mod, Fun}, [{'EXIT', Reason} | Errors]}; - {fail, Pid, Reason} -> - wait_for_evaluator(Pid, Mod, Fun, Config, Errors ++ [Reason]) - end. - -do_eval(ReplyTo, Mod, Fun, Config) -> - case (catch apply(Mod, Fun, [Config])) of - {'EXIT', {skipped, Reason}} -> - ReplyTo ! {'EXIT', self(), {skipped, Reason}}; - Other -> - ReplyTo ! {done, self(), Other} - end, - unlink(ReplyTo), - exit(shutdown). - -display_result([]) -> - io:format("OK\n", []); -display_result(Res) when is_list(Res) -> - Ok = [MF || {ok, MF, _} <- Res], - Nyi = [MF || {nyi, MF, _} <- Res], - Skipped = [{MF, Reason} || {skipped, MF, Reason} <- Res], - Failed = [{MF, Reason} || {failed, MF, Reason} <- Res], - Crashed = [{MF, Reason} || {crashed, MF, Reason} <- Res], - display_summary(Ok, Nyi, Skipped, Failed, Crashed), - display_skipped(Skipped), - display_failed(Failed), - display_crashed(Crashed). - -display_summary(Ok, Nyi, Skipped, Failed, Crashed) -> - io:format("\nTest case summary:\n", []), - display_summary(Ok, "successful"), - display_summary(Nyi, "not yet implemented"), - display_summary(Skipped, "skipped"), - display_summary(Failed, "failed"), - display_summary(Crashed, "crashed"), - io:format("\n", []). - -display_summary(Res, Info) -> - io:format(" ~w test cases ~s\n", [length(Res), Info]). - -display_skipped([]) -> - ok; -display_skipped(Skipped) -> - io:format("Skipped test cases:\n", []), - F = fun({MF, Reason}) -> io:format(" ~p => ~p\n", [MF, Reason]) end, - lists:foreach(F, Skipped), - io:format("\n", []). - - -display_failed([]) -> - ok; -display_failed(Failed) -> - io:format("Failed test cases:\n", []), - F = fun({MF, Reason}) -> io:format(" ~p => ~p\n", [MF, Reason]) end, - lists:foreach(F, Failed), - io:format("\n", []). - -display_crashed([]) -> - ok; -display_crashed(Crashed) -> - io:format("Crashed test cases:\n", []), - F = fun({MF, Reason}) -> io:format(" ~p => ~p\n", [MF, Reason]) end, - lists:foreach(F, Crashed), - io:format("\n", []). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% generic callback -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - --record(generic_state, {state, prepare, open, read, write, abort}). - -prepare(Peer, Access, LocalFilename, Mode, SuggestedOptions, Initial) when is_list(Initial) -> - State = lookup_option(state, mandatory, Initial), - Prepare = lookup_option(prepare, mandatory, Initial), - Open = lookup_option(open, mandatory, Initial), - Read = lookup_option(read, mandatory, Initial), - Write = lookup_option(write, mandatory, Initial), - Abort = lookup_option(abort, mandatory, Initial), - case Prepare(Peer, Access, LocalFilename, Mode, SuggestedOptions, State) of - {ok, AcceptedOptions, NewState} -> - {ok, - AcceptedOptions, - #generic_state{state = NewState, - prepare = Prepare, - open = Open, - read = Read, - write = Write, - abort = Abort}}; - Other -> - Other - end. - -open(Peer, Access, LocalFilename, Mode, SuggestedOptions, Initial) when is_list(Initial) -> - case prepare(Peer, Access, LocalFilename, Mode, SuggestedOptions, Initial) of - {ok, SuggestedOptions2, GenericState} -> - open(Peer, Access, LocalFilename, Mode, SuggestedOptions2, GenericState); - Other -> - Other - end; -open(Peer, Access, LocalFilename, Mode, SuggestedOptions, #generic_state{state = State, open = Open} = GenericState) -> - case Open(Peer, Access, LocalFilename, Mode, SuggestedOptions, State) of - {ok, SuggestedOptions2, NewState} -> - {ok, SuggestedOptions2, GenericState#generic_state{state = NewState}}; - Other -> - Other - end. - -read(#generic_state{state = State, read = Read} = GenericState) -> - case Read(State) of - {more, DataBlock, NewState} -> - {more, DataBlock, GenericState#generic_state{state = NewState}}; - Other -> - Other - end. - -write(DataBlock, #generic_state{state = State, write = Write} = GenericState) -> - case Write(DataBlock, State) of - {more, NewState} -> - {more, GenericState#generic_state{state = NewState}}; - Other -> - Other - end. - -abort(Code, Text, #generic_state{state = State, abort = Abort}) -> - Abort(Code, Text, State). - -lookup_option(Key, Default, Options) -> - case lists:keysearch(Key, 1, Options) of - {value, {_, Val}} -> - Val; - false -> - Default - end. - diff --git a/lib/inets/test/tftp_test_lib.hrl b/lib/inets/test/tftp_test_lib.hrl deleted file mode 100644 index e7a5a37d2c..0000000000 --- a/lib/inets/test/tftp_test_lib.hrl +++ /dev/null @@ -1,44 +0,0 @@ -%% -%% %CopyrightBegin% -%% -%% Copyright Ericsson AB 2007-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% -%% - --record('REASON', {mod, line, desc}). - --define(LOG(Format, Args), - tftp_test_lib:log(Format, Args, ?MODULE, ?LINE)). - --define(ERROR(Reason), - tftp_test_lib:error(Reason, ?MODULE, ?LINE)). - --define(VERIFY(Expected, Expr), - fun() -> - AcTuAlReS = (catch (Expr)), - case AcTuAlReS of - Expected -> ?LOG("Ok, ~p\n", [AcTuAlReS]); - _ -> ?ERROR(AcTuAlReS) - end, - AcTuAlReS - end()). - --define(IGNORE(Expr), - fun() -> - AcTuAlReS = (catch (Expr)), - ?LOG("Ok, ~p\n", [AcTuAlReS]), - AcTuAlReS - end()). diff --git a/lib/tftp/Makefile b/lib/tftp/Makefile new file mode 100644 index 0000000000..5c3ed52b28 --- /dev/null +++ b/lib/tftp/Makefile @@ -0,0 +1,78 @@ +# +# %CopyrightBegin% +# +# Copyright Ericsson AB 1996-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 + +# ---------------------------------------------------- +# Macros +# ---------------------------------------------------- + +SUB_DIRECTORIES = src doc/src + +include vsn.mk +VSN = $(TFTP_VSN) + +SPECIAL_TARGETS = + +DIA_PLT = ./priv/plt/$(APPLICATION).plt +DIA_ANALYSIS = $(basename $(DIA_PLT)).dialyzer_analysis + + +# ---------------------------------------------------- +# Default Subdir Targets +# ---------------------------------------------------- +include $(ERL_TOP)/make/otp_subdir.mk + +.PHONY: info gclean dialyzer dialyzer_plt dclean + +info: + @echo "OS: $(OS)" + @echo "DOCB: $(DOCB)" + @echo "" + @echo "TFTP_VSN: $(TFTP_VSN)" + @echo "APP_VSN: $(APP_VSN)" + @echo "" + @echo "DIA_PLT: $(DIA_PLT)" + @echo "DIA_ANALYSIS: $(DIA_ANALYSIS)" + @echo "" + +gclean: + git clean -fXd + +dclean: + rm -f $(DIA_PLT) + rm -f $(DIA_ANALYSIS) + +dialyzer_plt: $(DIA_PLT) + +$(DIA_PLT): + @echo "Building $(APPLICATION) plt file" + @dialyzer --build_plt \ + --output_plt $@ \ + -r ../$(APPLICATION)/ebin \ + --output $(DIA_ANALYSIS) \ + --verbose + +dialyzer: $(DIA_PLT) + @echo "Running dialyzer on $(APPLICATION)" + @dialyzer --plt $< \ + ../$(APPLICATION)/ebin \ + --verbose diff --git a/lib/tftp/doc/html/.gitignore b/lib/tftp/doc/html/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/tftp/doc/man3/.gitignore b/lib/tftp/doc/man3/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/tftp/doc/man6/.gitignore b/lib/tftp/doc/man6/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/tftp/doc/pdf/.gitignore b/lib/tftp/doc/pdf/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/tftp/doc/src/Makefile b/lib/tftp/doc/src/Makefile new file mode 100644 index 0000000000..cd97bdf7ff --- /dev/null +++ b/lib/tftp/doc/src/Makefile @@ -0,0 +1,153 @@ +# +# %CopyrightBegin% +# +# Copyright Ericsson AB 1997-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% +# +# +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 +# ---------------------------------------------------- +XML_APPLICATION_FILES = ref_man.xml + +XML_CHAPTER_FILES = \ + introduction.xml \ + notes.xml + +XML_REF3_FILES = \ + tftp.xml + +XML_PART_FILES = \ + part.xml + +BOOK_FILES = book.xml + +XML_FILES = \ + $(BOOK_FILES) \ + $(XML_CHAPTER_FILES) \ + $(XML_PART_FILES) \ + $(XML_REF6_FILES) \ + $(XML_REF3_FILES) \ + $(XML_APPLICATION_FILES) + +# GIF_FILES = tftp.gif + + +# ---------------------------------------------------- + +HTML_FILES = \ + $(XML_APPLICATION_FILES:%.xml=$(HTMLDIR)/%.html) \ + $(XML_PART_FILES:%.xml=$(HTMLDIR)/%.html) + +INFO_FILE = ../../info +EXTRA_FILES = \ + $(XML_REF3_FILES:%.xml=$(HTMLDIR)/%.html) \ + $(XML_REF6_FILES:%.xml=$(HTMLDIR)/%.html) \ + $(XML_CHAPTER_FILES:%.xml=$(HTMLDIR)/%.html) + +MAN3_FILES = $(XML_REF3_FILES:%.xml=$(MAN3DIR)/%.3) + +HTML_REF_MAN_FILE = $(HTMLDIR)/index.html + +TOP_PDF_FILE = $(PDFDIR)/$(APPLICATION)-$(VSN).pdf + +# ---------------------------------------------------- +# FLAGS +# ---------------------------------------------------- +XML_FLAGS += +DVIPS_FLAGS += + +# ---------------------------------------------------- +# Targets +# ---------------------------------------------------- +$(HTMLDIR)/%.gif: %.gif + $(INSTALL_DATA) $< $@ + +docs: pdf html man + +ldocs: local_docs + +$(TOP_PDF_FILE): $(XML_FILES) + +pdf: $(TOP_PDF_FILE) + +html: gifs $(HTML_REF_MAN_FILE) + +clean clean_docs: clean_html clean_man clean_pdf + rm -f errs core *~ + +man: $(MAN3_FILES) + +gifs: $(GIF_FILES:%=$(HTMLDIR)/%) + +debug opt: + +clean_pdf: + rm -f $(TOP_PDF_FILE) $(TOP_PDF_FILE:%.pdf=%.fo) + +clean_html: + rm -rf $(TOP_HTML_FILES) $(HTMLDIR)/* + +clean_man: + rm -f $(MAN3_FILES) + + +# ---------------------------------------------------- +# Release Target +# ---------------------------------------------------- +include $(ERL_TOP)/make/otp_release_targets.mk + +release_docs_spec: docs + $(INSTALL_DIR) "$(RELSYSDIR)/doc/pdf" + $(INSTALL_DATA) $(TOP_PDF_FILE) "$(RELSYSDIR)/doc/pdf" + $(INSTALL_DIR) "$(RELSYSDIR)/doc/html" + $(INSTALL_DATA) $(HTMLDIR)/* "$(RELSYSDIR)/doc/html" + $(INSTALL_DATA) $(INFO_FILE) "$(RELSYSDIR)" + $(INSTALL_DIR) "$(RELEASE_PATH)/man/man3" + $(INSTALL_DATA) $(MAN3DIR)/* "$(RELEASE_PATH)/man/man3" + +release_spec: + +info: + @echo "GIF_FILES:\n$(GIF_FILES)" + @echo "" + @echo "EXTRA_FILES:\n$(EXTRA_FILES)" + @echo "" + @echo "HTML_FILES:\n$(HTML_FILES)" + @echo "" + @echo "TOP_HTML_FILES:\n$(TOP_HTML_FILES)" + @echo "" + @echo "XML_REF3_FILES:\n$(XML_REF3_FILES)" + @echo "" + @echo "XML_REF6_FILES:\n$(XML_REF6_FILES)" + @echo "" + @echo "XML_CHAPTER_FILES:\n$(XML_CHAPTER_FILES)" + @echo "" diff --git a/lib/tftp/doc/src/book.xml b/lib/tftp/doc/src/book.xml new file mode 100644 index 0000000000..cf8032a4e9 --- /dev/null +++ b/lib/tftp/doc/src/book.xml @@ -0,0 +1,49 @@ + + + + +
+ + 19972018 + Ericsson AB. 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. + + + + TFTP + Péter Dimitrov + + 2018-03-22 + 1.0 + book.sgml +
+ + + TFTP + + + + + + + + + + + + + + +
diff --git a/lib/tftp/doc/src/introduction.xml b/lib/tftp/doc/src/introduction.xml new file mode 100644 index 0000000000..949438ae92 --- /dev/null +++ b/lib/tftp/doc/src/introduction.xml @@ -0,0 +1,46 @@ + + + + +
+ + 19972018 + Ericsson AB. All Rights Reserved. + + + The contents of this file are subject to the Erlang Public License, + Version 1.1, (the "License"); you may not use this file except in + compliance with the License. You should have received a copy of the + Erlang Public License along with this software. If not, it can be + retrieved online at http://www.erlang.org/. + + Software distributed under the License is distributed on an "AS IS" + basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + the License for the specific language governing rights and limitations + under the License. + + + + Introduction + Péter Dimitrov + + + + + 2018-03-22 + A + introduction.xml +
+ +
+ Purpose +

A TFTP client and server.

+
+ +
+ Prerequisites +

It is assumed that the reader is familiar with the Erlang + programming language, concepts of OTP, and has a basic + understanding of the TFTP protocol.

+
+
diff --git a/lib/tftp/doc/src/notes.xml b/lib/tftp/doc/src/notes.xml new file mode 100644 index 0000000000..f7525c8f3e --- /dev/null +++ b/lib/tftp/doc/src/notes.xml @@ -0,0 +1,53 @@ + + + + +
+ + 20022018 + Ericsson AB. 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. + + + + TFTP Release Notes + + + + + + 2018-03-22 + A + notes.xml +
+ +
TFTP 1.0.0 + +
First released version + + +

+ Inets application was split into multiple smaller protocol specific applications. + The TFTP application is a standalone TFTP client and server with the same functionality as + TFTP in Inets.

+

+ Own Id: OTP-14113

+
+
+
+ +
+ +
diff --git a/lib/tftp/doc/src/part.xml b/lib/tftp/doc/src/part.xml new file mode 100644 index 0000000000..abbe0edcbf --- /dev/null +++ b/lib/tftp/doc/src/part.xml @@ -0,0 +1,36 @@ + + + + +
+ + 20042018 + Ericsson AB. 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. + + + + TFTP User's Guide + Péter Dimitrov + + 2018-03-22 + A + part.sgml +
+ +

The TFTP application provides a TFTP client and server.

+
+ +
diff --git a/lib/tftp/doc/src/ref_man.xml b/lib/tftp/doc/src/ref_man.xml new file mode 100644 index 0000000000..41a6cc6d52 --- /dev/null +++ b/lib/tftp/doc/src/ref_man.xml @@ -0,0 +1,36 @@ + + + + +
+ + 19972018 + Ericsson AB. 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. + + + + TFTP Reference Manual + Péter Dimitrov + + 2018-03-22 + 1.0 + ref_man.xml +
+ +

The TFTP application.

+
+ +
diff --git a/lib/tftp/doc/src/tftp.xml b/lib/tftp/doc/src/tftp.xml new file mode 100644 index 0000000000..c4d6a4e6d0 --- /dev/null +++ b/lib/tftp/doc/src/tftp.xml @@ -0,0 +1,643 @@ + + + + +
+ + 20062018 + Ericsson AB. 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. + + + + tftp + + + + +
+ tftp + Trivial FTP. + +

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 is + the "netascii" transfer mode.

+

The start/1 function starts + a daemon process listening for UDP packets on a port. When it + receives a request for read or write, it spawns a temporary server + process handling the transfer.

+

On the client side, + function read_file/3 + and write_file/3 + spawn a temporary client process establishing + contact with a TFTP daemon and perform the file transfer.

+

tftp uses a callback module to handle the file + transfer. Two such callback modules are provided, + tftp_binary and tftp_file. See + read_file/3 and + write_file/3 for details. + You can also implement your own callback modules, see + CALLBACK FUNCTIONS. + A callback module provided by + the user is registered using option callback, see + DATA TYPES.

+
+ +
+ TFTP SERVER SERVICE START/STOP + +

A TFTP server can be started dynamically + (when tftp is already started) by calling the tftp application + API tftp:start(ServiceConfig). + The ServiceConfig for TFTP is described in + the DATA TYPES + section.

+ +

The TFTP server can be stopped using tftp:stop_service(Pid).

+ +

The TPFT client is of such a temporary nature that it is not + handled as a service in the tftp service framework.

+ +
+ +
+ + DATA TYPES +

ServiceConfig = Options

+

Options = [option()]

+

Most of the options are common for both the client and the server + side, but some of them differs a little. + The available option()s are as follows:

+ + {debug, Level} + +

Level = none | error | warning | brief | normal | verbose | all

+

Controls the level of debug printouts. + Default is none.

+
+ {host, Host} + +

Host = hostname(), see + inet(3).

+

The name or IP address of the host where the TFTP daemon + resides. This option is only used by the client.

+
+ {port, Port} + +

Port = int()

+

The TFTP port where the daemon listens. Defaults is + the standardized number 69. On the server side, it can + sometimes make sense to set it to 0, meaning that + the daemon just picks a free port (which one is + returned by function info/1).

+

If a socket is connected already, option + {udp, [{fd, integer()}]} can be used to pass the + open file descriptor to gen_udp. This can be automated + by using a command-line argument stating the + prebound file descriptor number. For example, if the + port is 69 and file descriptor 22 is opened by + setuid_socket_wrap, the command-line argument + "-tftpd_69 22" triggers the prebound file + descriptor 22 to be used instead of opening port 69. + The UDP option {udp, [{fd, 22}]} is automatically 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 = int()

+

Policy for the selection of the temporary port that is used + by the server/client during the file transfer. Default is + random, which is the standardized policy. With this + policy a randomized free port is used. A single port or a range + of ports can be useful if the protocol passes through a + firewall.

+
+ {udp, Options} + +

Options = [Opt], see + gen_udp:open/2.

+
+ {use_tsize, Bool} + +

Bool = bool()

+

Flag for automated use of option tsize. With + this set to true, the write_file/3 client + determines the filesize and sends it to the server as + the standardized tsize option. A read_file/3 + client acquires only a filesize from the server by sending + a zero tsize.

+
+ {max_tsize, MaxTsize} + +

MaxTsize = int() | infinity

+

Threshold for the maximal filesize in bytes. The transfer + is aborted if the limit is exceeded. + Default is infinity.

+
+ {max_conn, MaxConn} + +

MaxConn = int() | infinity

+

Threshold for the maximal number of active connections. + The daemon rejects the setup of new connections if + the limit is exceeded. Default is infinity.

+
+ {TftpKey, TftpVal} + +

TftpKey = string()

+TftpVal = string()

+

Name and value of a TFTP option.

+
+ {reject, Feature} + +

Feature = Mode | TftpKey

+ Mode = read | write

+ TftpKey = string()

+

Controls which features to reject. This is + mostly useful for the server as it can restrict the use + 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 is matched to the regular + expressions of the registered callbacks. The first matching + callback is used during the transfer. See + read_file/3 and + write_file/3. +

+

The callback module must implement the tftp behavior, see + CALLBACK FUNCTIONS.

+
+ + {logger, Module} + +

Module = module()()

+ +

Callback module for customized logging of errors, warnings, and + info messages. The callback module must implement the + tftp_logger behavior, see + LOGGER FUNCTIONS. + The default module is tftp_logger.

+
+ + {max_retries, MaxRetries} + +

MaxRetries = int()

+ +

Threshold for the maximal number of retries. By default + the server/client tries to resend a message up to + five times when the time-out expires.

+
+
+
+ + + + change_config(daemons, Options) -> [{Pid, Result}] + Changes configuration for all daemons. + + + Options = [option()] + Pid = pid() + Result = ok | {error, Reason} + Reason = term() + + +

Changes configuration for all TFTP daemon processes.

+
+
+ + + change_config(servers, Options) -> [{Pid, Result}] + Changes configuration for all servers. + + + Options = [option()] + Pid = pid() + Result = ok | {error, Reason} + Reason = term() + + +

Changes configuration for all TFTP server processes.

+
+
+ + + change_config(Pid, Options) -> Result + Changes configuration for a TFTP daemon, server, + or client process. + + Pid = pid() + Options = [option()] + Result = ok | {error, Reason} + Reason = term() + + +

Changes configuration for a TFTP daemon, server, or client process.

+
+
+ + + info(daemons) -> [{Pid, Options}] + Returns information about all daemons. + + Pid = [pid()()] + Options = [option()] + Reason = term() + + +

Returns information about all TFTP daemon processes.

+
+
+ + + info(servers) -> [{Pid, Options}] + Returns information about all servers. + + Pid = [pid()()] + Options = [option()] + Reason = term() + + +

Returns information about all TFTP server processes.

+
+
+ + + info(Pid) -> {ok, Options} | {error, Reason} + Returns information about a daemon, server, or client process. + + Options = [option()] + Reason = term() + + +

Returns information about a TFTP daemon, server, or client process.

+
+
+ + + read_file(RemoteFilename, LocalFilename, Options) -> {ok, LastCallbackState} | {error, Reason} + Reads a (virtual) file from a TFTP server. + + RemoteFilename = string() + LocalFilename = binary | string() + Options = [option()] + LastCallbackState = term() + Reason = term() + + +

Reads a (virtual) file RemoteFilename from a TFTP + server.

+

If LocalFilename is the atom binary, + tftp_binary is used as callback module. It concatenates + all transferred blocks and returns them as one single binary + in LastCallbackState.

+

If LocalFilename is a string and there are no + registered callback modules, tftp_file is used as + callback module. It writes each transferred block to the file + named LocalFilename and returns the number of + transferred bytes in LastCallbackState.

+

If LocalFilename is a string and there are registered + callback modules, LocalFilename is tested against + the regexps of these and the callback module corresponding to + the first match is used, or an error tuple is returned if no + matching regexp is found.

+
+
+ + + start(Options) -> {ok, Pid} | {error, Reason} + Starts a daemon process. + + Options = [option()] + Pid = pid() + Reason = term() + + +

Starts a daemon process listening for UDP packets on a + port. When it receives a request for read or write, it spawns + a temporary server process handling the actual transfer + of the (virtual) file.

+
+
+ + + write_file(RemoteFilename, LocalFilename, Options) -> {ok, LastCallbackState} | {error, Reason} + Writes a (virtual) file to a TFTP server. + + RemoteFilename = string() + LocalFilename = binary() | string() + Options = [option()] + LastCallbackState = term() + Reason = term() + + +

Writes a (virtual) file RemoteFilename to a TFTP + server.

+

If LocalFilename is a binary, tftp_binary is + used as callback module. The binary is transferred block by + block and the number of transferred bytes is returned in + LastCallbackState.

+

If LocalFilename is a string and there are no + registered callback modules, tftp_file is used as + callback module. It reads the file named LocalFilename + block by block and returns the number of transferred bytes + in LastCallbackState.

+

If LocalFilename is a string and there are registered + callback modules, LocalFilename is tested against + the regexps of these and the callback module corresponding to + the first match is used, or an error tuple is returned if no + matching regexp is found.

+
+
+
+ +
+ + CALLBACK FUNCTIONS +

A tftp callback module is to be implemented as a + tftp behavior and export the functions listed + in the following.

+

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 + function read/1 or write/2 is invoked + repeatedly, once per transferred block. At each function call, + the state returned from the previous call is obtained. When + the last block is encountered, function read/1 or + write/2 is expected to close the (virtual) file + and return its last state. Function abort/3 is only + used in error situations. Function 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 to return the subset of them that it + accepts. Then the options are sent to the server, which performs + the same TFTP option negotiation procedure. The options that are + accepted by the server are forwarded to function open/5 + on the client side. On the client side, function open/5 + must accept all option as-is or reject the transfer. Then + the callback interaction follows the same pattern as described + for the server side. When the last block is encountered in + read/1 or write/2, the returned state is forwarded to + the user and returned from read_file/3 or + write_file/3.

+ +

If a callback (performing the file access + in the TFTP server) takes too long time (more than + the double TFTP time-out), the server aborts the + connection and sends an error reply to the client. + This implies that the server releases resources + attached to the connection faster than before. The + server simply assumes that the client has given + up.

+ +

If the TFTP server receives yet another request from + the same client (same host and port) while it + already has an active connection to the client, it + ignores the new request if the request is + equal to the first one (same filename and options). + This implies that the (new) client will be served + by the already ongoing connection on the server + side. By not setting up yet another connection, in + parallel with the ongoing one, the server + consumes less resources.

+ + +
+ + + + Module:abort(Code, Text, State) -> ok + Aborts the file transfer. + + Code = undef | enoent | eacces | enospc +   | badop | eexist | baduser | badopt +   | int() + Text = string() + State = term() + + +

Invoked when the file transfer is aborted.

+

The callback function is expected to clean + up its used resources after the aborted file + transfer, such as closing open file + descriptors and so on. The function is not + invoked if any of the other callback + functions returns an error, as it is + expected that they already have cleaned up + the necessary resources. However, it is + invoked if the functions fail (crash).

+
+
+ + + Module:open(Peer, Access, Filename, Mode, SuggestedOptions, State) -> {ok, AcceptedOptions, NewState} | {error, {Code, Text}} + Opens a file for read or write access. + + Peer = {PeerType, PeerHost, PeerPort} + PeerType = inet | inet6 + PeerHost = ip_address() + PeerPort = integer() + Access = read | write + Filename = string() + Mode = string() + SuggestedOptions = AcceptedOptions = [{Key, Value}] +  Key = Value = string() + State = InitialState | term() +  InitialState = [] | [{root_dir, string()}] + NewState = term() + Code = undef | enoent | eacces | enospc +   | badop | eexist | baduser | badopt +   | int() + Text = string() + + +

Opens a file for read or write access.

+

On the client side, where the open/5 call has been + preceded by a call to prepare/5, all options must be + accepted or rejected.

+

On the server side, where there is no preceding + prepare/5 call, no new options can be added, but + those present in SuggestedOptions can be + omitted or replaced with new values in AcceptedOptions.

+ + +
+
+ + + Module:prepare(Peer, Access, Filename, Mode, SuggestedOptions, InitialState) -> {ok, AcceptedOptions, NewState} | {error, {Code, Text}} + Prepares to open a file on the client side. + + Peer = {PeerType, PeerHost, PeerPort} + PeerType = inet | inet6 + PeerHost = ip_address() + PeerPort = integer() + Access = read | write + Filename = string() + Mode = string() + SuggestedOptions = AcceptedOptions = [{Key, Value}] +  Key = Value = string() + InitialState = [] | [{root_dir, string()}] + NewState = term() + Code = undef | enoent | eacces | enospc +   | badop | eexist | baduser | badopt +   | int() + Text = string() + + +

Prepares to open a file on the client side.

+

No new options can be added, but those present in + SuggestedOptions can be omitted or replaced with new + values in AcceptedOptions.

+

This is followed by a call to open/4 before any + read/write access is performed. AcceptedOptions is + sent to the server, which replies with the options that it + accepts. These are then forwarded to open/4 as + SuggestedOptions.

+ + +
+
+ + + Module:read(State) -> {more, Bin, NewState} | {last, Bin, FileSize} | {error, {Code, Text}} + Reads a chunk from the file. + + State = NewState = term() + Bin = binary() + FileSize = int() + Code = undef | enoent | eacces | enospc +   | badop | eexist | baduser | badopt +   | int() + Text = string() + + +

Reads a chunk from the file.

+

The callback function is expected to close + the file when the last file chunk is + encountered. When an error is encountered, + the callback function is expected to clean + up after the aborted file transfer, such as + closing open file descriptors, and so on. In both + cases there will be no more calls to any of + the callback functions.

+ + +
+
+ + + Module:write(Bin, State) -> {more, NewState} | {last, FileSize} | {error, {Code, Text}} + Writes a chunk to the file. + + Bin = binary() + State = NewState = term() + FileSize = int() + Code = undef | enoent | eacces | enospc +   | badop | eexist | baduser | badopt +   | int() + Text = string() + + +

Writes a chunk to the file.

+

The callback function is expected to close + the file when the last file chunk is + encountered. When an error is encountered, + the callback function is expected to clean + up after the aborted file transfer, such as + closing open file descriptors, and so on. In both + cases there will be no more calls to any of + the callback functions.

+ + +
+
+
+ +
+ + LOGGER FUNCTIONS + +

A tftp_logger callback module is to be implemented as a + tftp_logger behavior and export the following functions:

+ + +
+ + + + Logger:error_msg(Format, Data) -> ok | exit(Reason) + Logs an error message. + + Format = string() + Data = [term()] + Reason = term() + + +

Logs an error message. + See error_logger:error_msg/2 for details.

+ + +
+
+ + + Logger:info_msg(Format, Data) -> ok | exit(Reason) + Logs an info message. + + Format = string() + Data = [term()] + Reason = term() + + +

Logs an info message. + See error_logger:info_msg/2 for details.

+
+
+ + + Logger:warning_msg(Format, Data) -> ok | exit(Reason) + Logs a warning message. + + Format = string() + Data = [term()] + Reason = term() + + +

Logs a warning message. + See error_logger:warning_msg/2 for details.

+ + +
+
+
+
+ + diff --git a/lib/tftp/ebin/.gitignore b/lib/tftp/ebin/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/tftp/info b/lib/tftp/info new file mode 100644 index 0000000000..1220454351 --- /dev/null +++ b/lib/tftp/info @@ -0,0 +1,2 @@ +group: comm +short: TFTP application 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 +%%% Description : Trivial FTP +%%% Created : 18 May 2004 by Hakan Mattsson +%%%------------------------------------------------------------------- +%%% +%%% 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 +%%% Description : +%%% +%%% Created : 24 May 2004 by Hakan Mattsson +%%%------------------------------------------------------------------- + +-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 -> + <> = 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 +%%% Description : +%%% +%%% Created : 24 May 2004 by Hakan Mattsson +%%%------------------------------------------------------------------- + +-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 +%%% Description : Option parsing, decode, encode etc. +%%% +%%% Created : 18 May 2004 by Hakan Mattsson +%%%------------------------------------------------------------------- + +-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 + <> -> + 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; + <> -> + 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_msg_data{block_no = SeqNo, data = Data}; + <> -> + #tftp_msg_ack{block_no = SeqNo}; + <> -> + 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; + <> -> + 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(<>, 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, + [ + <>, + Filename, + 0, + Mode, + 0, + [[Key, 0, Val, 0] || {Key, Val} <- Options] + ]; +encode_msg(#tftp_msg_data{block_no = BlockNo, data = Data}) when BlockNo =< 65535 -> + [ + <>, + Data + ]; +encode_msg(#tftp_msg_ack{block_no = BlockNo}) when BlockNo =< 65535 -> + <>; +encode_msg(#tftp_msg_error{code = Code, text = Text}) -> + IntCode = encode_error_code(Code), + [ + <>, + Text, + 0 + ]; +encode_msg(#tftp_msg_oack{options = Options}) -> + [ + <>, + [[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. diff --git a/lib/tftp/test/Makefile b/lib/tftp/test/Makefile new file mode 100644 index 0000000000..99f36256b0 --- /dev/null +++ b/lib/tftp/test/Makefile @@ -0,0 +1,250 @@ +# +# %CopyrightBegin% +# +# Copyright Ericsson AB 1997-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% +# +# +# For an outline of how this all_SUITE_data stuff works, see the +# make file ../../ssl/test/Makefile. +# +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 +# ---------------------------------------------------- +INCLUDES = -I. \ + -I$(ERL_TOP)/lib/tftp/src + +CP = cp + +ifeq ($(TESTROOT_DIR),) +TESTROOT_DIR = /ldisk/tests/$(USER)/tftp +endif + +ifeq ($(TFTP_DATA_DIR),) +TFTP_DATA_DIR = $(TESTROOT_DIR)/data_dir +endif + +ifeq ($(TFTP_PRIV_DIR),) +TFTP_PRIV_DIR = $(TESTROOT_DIR)/priv_dir +endif + +TFTP_FLAGS = -Dtftp__data_dir='"$(TFTP_DATA_DIR)"' \ + -Dtftp_priv_dir='"$(TFTP_PRIV_DIR)"' + + +### +### test suite debug flags +### +ifeq ($(TFTP_DEBUG_CLIENT),) + TFTP_DEBUG_CLIENT = y +endif + +ifeq ($(TFTP_DEBUG_CLIENT),) + TFTP_FLAGS += -Dtftp_debug_client +endif + +ifeq ($(TFTP_TRACE_CLIENT),) + TFTP_DEBUG_CLIENT = y +endif + +ifeq ($(TFTP_TRACE_CLIENT),y) + TFTP_FLAGS += -Dtftp_trace_client +endif + +ifneq ($(TFTP_DEBUG),) + TFTP_DEBUG = s +endif + +ifeq ($(TFTP_DEBUG),l) + TFTP_FLAGS += -Dtftp_log +endif + +ifeq ($(TFTP_DEBUG),d) + TFTP_FLAGS += -Dtftp_debug -Dtftp_log +endif + + +TFTP_FLAGS += -pa ../tftp/ebin + +TFTP_ROOT = ../tftp + +MODULES = \ + tftp_SUITE \ + tftp_test_lib + + +EBIN = . + +HRL_FILES = \ + ../src/tftp.hrl \ + tftp_test_lib.hrl + +ERL_FILES = $(MODULES:%=%.erl) + +SOURCE = $(ERL_FILES) $(HRL_FILES) + +TARGET_FILES = $(MODULES:%=$(EBIN)/%.$(EMULATOR)) + +TFTP_SPECS = tftp.spec tftp_bench.spec +COVER_FILE = tftp.cover +TFTP_FILES = tftp.config $(TFTP_SPECS) + + +TFTP_DATADIRS = tftp_SUITE_data + +DATADIRS = $(TFTP_DATADIRS) + +EMAKEFILE = Emakefile +MAKE_EMAKE = $(wildcard $(ERL_TOP)/make/make_emakefile) + +ifeq ($(MAKE_EMAKE),) +BUILDTARGET = $(TARGET_FILES) +RELTEST_FILES = $(COVER_FILE) $(TFTP_SPECS) $(SOURCE) +else +BUILDTARGET = emakebuild +RELTEST_FILES = $(EMAKEFILE) $(COVER_FILE) $(TFTP_SPECS) $(SOURCE) +endif + + +# ---------------------------------------------------- +# Release directory specification +# ---------------------------------------------------- + +RELTESTSYSDIR = "$(RELEASE_PATH)/tftp_test" +RELTESTSYSALLDATADIR = $(RELTESTSYSDIR)/all_SUITE_data +RELTESTSYSBINDIR = $(RELTESTSYSALLDATADIR)/bin + + +# ---------------------------------------------------- +# FLAGS +# The path to the test_server ebin dir is needed when +# running the target "targets". +# ---------------------------------------------------- +ERL_COMPILE_FLAGS += \ + $(INCLUDES) \ + $(TFTP_FLAGS) + +# ---------------------------------------------------- +# Targets +# erl -sname kalle -pa ../ebin +# If you intend to run the test suite locally (private), then +# there is some requirements: +# 1) TFTP_PRIV_DIR must be created +# ---------------------------------------------------- + +tests debug opt: $(BUILDTARGET) + +targets: $(TARGET_FILES) + +.PHONY: emakebuild + +emakebuild: $(EMAKEFILE) + +$(EMAKEFILE): + $(MAKE_EMAKE) $(ERL_COMPILE_FLAGS) -o$(EBIN) '*_SUITE_make' | grep -v Warning > $(EMAKEFILE) + $(MAKE_EMAKE) $(ERL_COMPILE_FLAGS) -o$(EBIN) $(MODULES) | grep -v Warning >> $(EMAKEFILE) + +clean: + rm -f $(EMAKEFILE) + rm -f $(TARGET_FILES) + rm -f core *~ + +docs: + + +# ---------------------------------------------------- +# Release Target +# ---------------------------------------------------- +include $(ERL_TOP)/make/otp_release_targets.mk + +release_spec: opt + $(INSTALL_DIR) "$(RELSYSDIR)/test" + $(INSTALL_DATA) $(HRL_FILES) $(ERL_FILES) "$(RELSYSDIR)/test" + $(INSTALL_DATA) $(TFTP_FILES) "$(RELSYSDIR)/test" + @for d in $(DATADIRS); do \ + echo "installing data dir $$d"; \ + if test -f $$d/TAR.exclude; then \ + echo $$d/TAR.exclude2 > $$d/TAR.exclude2; \ + cat $$d/TAR.exclude >> $$d/TAR.exclude2; \ + find $$d -name '*.contrib*' >> $$d/TAR.exclude2; \ + find $$d -name '*.keep*' >> $$d/TAR.exclude2; \ + find $$d -name '*.mkelem*' >> $$d/TAR.exclude2; \ + find $$d -name '*~' >> $$d/TAR.exclude2; \ + find $$d -name 'erl_crash.dump' >> $$d/TAR.exclude2; \ + find $$d -name 'core' >> $$d/TAR.exclude2; \ + find $$d -name '.cmake.state' >> $$d/TAR.exclude2; \ + tar cfX - $$d/TAR.exclude2 $$d | (cd "$(RELSYSDIR)/test"; tar xf -); \ + else \ + tar cf - $$d | (cd "$(RELSYSDIR)/test"; tar xf -); \ + fi; \ + done + +release_tests_spec: opt + $(INSTALL_DIR) $(RELTESTSYSDIR) + $(INSTALL_DATA) $(RELTEST_FILES) $(RELTESTSYSDIR) + chmod -R u+w $(RELTESTSYSDIR) + tar chf - $(DATADIRS) | (cd $(RELTESTSYSDIR); tar xf -) + $(INSTALL_DIR) $(RELTESTSYSALLDATADIR) + $(INSTALL_DIR) $(RELTESTSYSBINDIR) + chmod -R +x $(RELTESTSYSBINDIR) + $(INSTALL_DIR) $(RELTESTSYSALLDATADIR)/win32/lib + +release_docs_spec: + +info: + @echo "MAKE_EMAKE = $(MAKE_EMAKE)" + @echo "EMAKEFILE = $(EMAKEFILE)" + @echo "BUILDTARGET = $(BUILDTARGET)" + @echo "" + @echo "MODULES = $(MODULES)" + @echo "ERL_FILES = $(ERL_FILES)" + @echo "SOURCE = $(SOURCE)" + @echo "TARGET_FILES = $(TARGET_FILES)" + @echo "" + @echo "TFTP_SPECS = $(TFTP_SPECS)" + @echo "TFTP_FILES = $(TFTP_FILES)" + @echo "" + @echo "RELEASE_PATH = "$(RELEASE_PATH)"" + @echo "RELSYSDIR = "$(RELSYSDIR)"" + @echo "RELTESTSYSDIR = $(RELTESTSYSDIR)" + @echo "RELTESTSYSALLDATADIR = $(RELTESTSYSALLDATADIR)" + @echo "RELTESTSYSBINDIR = $(RELTESTSYSBINDIR)" + @echo "" + @echo "DATADIRS = $(DATADIRS)" + @echo "REL_DATADIRS = $(REL_DATADIRS)" + @echo "" + @echo "TFTP_DATA_DIR = $(TFTP_DATA_DIR)" + @echo "TFTP_PRIV_DIR = $(TFTP_PRIV_DIR)" + @echo "TFTP_ROOT = $(TFTP_ROOT)" + @echo "TFTP_FLAGS = $(TFTP_FLAGS)" + + diff --git a/lib/tftp/test/tftp.config b/lib/tftp/test/tftp.config new file mode 100644 index 0000000000..2600237da9 --- /dev/null +++ b/lib/tftp/test/tftp.config @@ -0,0 +1 @@ +[]. \ No newline at end of file diff --git a/lib/tftp/test/tftp.cover b/lib/tftp/test/tftp.cover new file mode 100644 index 0000000000..22ef5d0dda --- /dev/null +++ b/lib/tftp/test/tftp.cover @@ -0,0 +1,2 @@ +{incl_app,tftp,details}. + diff --git a/lib/tftp/test/tftp.spec b/lib/tftp/test/tftp.spec new file mode 100644 index 0000000000..f3537bc652 --- /dev/null +++ b/lib/tftp/test/tftp.spec @@ -0,0 +1 @@ +{suites,"../tftp_test", all}. diff --git a/lib/tftp/test/tftp_SUITE.erl b/lib/tftp/test/tftp_SUITE.erl new file mode 100644 index 0000000000..a43fd51153 --- /dev/null +++ b/lib/tftp/test/tftp_SUITE.erl @@ -0,0 +1,949 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2006-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_SUITE). + +-compile(export_all). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Includes and defines +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-include("tftp_test_lib.hrl"). + +-define(START_DAEMON(Port, Options), + begin + {ok, Pid} = ?VERIFY({ok, _Pid}, tftp:start([{port, Port} | Options])), + if + Port == 0 -> + {ok, ActualOptions} = ?IGNORE(tftp:info(Pid)), + {value, {port, ActualPort}} = + lists:keysearch(port, 1, ActualOptions), + {ActualPort, Pid}; + true -> + {Port, Pid} + end + end). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% API +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +t() -> + tftp_test_lib:t([{?MODULE, all}]). + +t(Cases) -> + tftp_test_lib:t(Cases, default_config()). + +t(Cases, Config) -> + tftp_test_lib:t(Cases, Config). + +default_config() -> + []. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Test server callbacks +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +init_per_testcase(Case, Config) -> + tftp_test_lib:init_per_testcase(Case, Config). + +end_per_testcase(Case, Config) when is_list(Config) -> + tftp_test_lib:end_per_testcase(Case, Config). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Top test case +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +suite() -> [{ct_hooks,[ts_install_cth]}]. + +all() -> + [simple, extra, reuse_connection, resend_client, + resend_server, large_file]. + +groups() -> + []. + +init_per_suite(Config) -> + Config. + +end_per_suite(_Config) -> + ok. + +init_per_group(_GroupName, Config) -> + Config. + +end_per_group(_GroupName, Config) -> + Config. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Simple +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +simple(doc) -> + ["Start the daemon and perform simple a read and write."]; +simple(suite) -> + []; +simple(Config) when is_list(Config) -> + ?VERIFY(ok, application:start(tftp)), + + {Port, DaemonPid} = ?IGNORE(?START_DAEMON(0, [{debug, brief}])), + + %% Read fail + RemoteFilename = "tftp_temporary_remote_test_file.txt", + LocalFilename = "tftp_temporary_local_test_file.txt", + Blob = list_to_binary(lists:duplicate(2000, $1)), + %% Blob = <<"Some file contents\n">>, + Size = size(Blob), + ?IGNORE(file:delete(RemoteFilename)), + ?VERIFY({error, {client_open, enoent, _}}, + tftp:read_file(RemoteFilename, binary, [{port, Port}])), + + %% Write and read + ?VERIFY({ok, Size}, tftp:write_file(RemoteFilename, Blob, [{port, Port}])), + ?VERIFY({ok, Blob}, tftp:read_file(RemoteFilename, binary, [{port, Port}])), + ?IGNORE(file:delete(LocalFilename)), + ?VERIFY({ok, Size}, tftp:read_file(RemoteFilename, LocalFilename, [{port, Port}])), + + %% Cleanup + unlink(DaemonPid), + exit(DaemonPid, kill), + ?VERIFY(ok, file:delete(LocalFilename)), + ?VERIFY(ok, file:delete(RemoteFilename)), + ?VERIFY(ok, application:stop(tftp)), + ok. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Extra +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +extra(doc) -> + ["Verify new stuff for IS 1.2."]; +extra(suite) -> + []; +extra(Config) when is_list(Config) -> + ?VERIFY({'EXIT', {badarg,{fake_key, fake_flag}}}, + tftp:start([{port, 0}, {fake_key, fake_flag}])), + + {Port, DaemonPid} = ?IGNORE(?START_DAEMON(0, [{debug, brief}])), + + RemoteFilename = "tftp_extra_temporary_remote_test_file.txt", + LocalFilename = "tftp_extra_temporary_local_test_file.txt", + Blob = <<"Some file contents\n">>, + Size = size(Blob), + Host = "127.0.0.1", + Peer = {inet, Host, Port}, + Generic = + [ + {state, []}, + {prepare, fun extra_prepare/6}, + {open, fun extra_open/6}, + {read, fun extra_read/1}, + {write, fun extra_write/2}, + {abort, fun extra_abort/3 } + ], + Options = [{host, Host}, + {port, Port}, + %%{ debug,all}, + {callback, {".*", tftp_test_lib, Generic}}], + ?VERIFY(ok, file:write_file(LocalFilename, Blob)), + ?VERIFY({ok, [{count, Size}, Peer]}, + tftp:write_file(RemoteFilename, LocalFilename, Options)), + ?VERIFY(ok, file:delete(LocalFilename)), + + ?VERIFY({ok,[{bin, Blob}, Peer]}, + tftp:read_file(RemoteFilename, LocalFilename, Options)), + + %% Cleanup + unlink(DaemonPid), + exit(DaemonPid, kill), + ?VERIFY(ok, file:delete(LocalFilename)), + ?VERIFY(ok, file:delete(RemoteFilename)), + ok. + +-record(extra_state, {file, blksize, count, acc, peer}). + +%%------------------------------------------------------------------- +%% Prepare +%%------------------------------------------------------------------- + +extra_prepare(Peer, Access, LocalFilename, Mode, SuggestedOptions, []) -> + %% Client side + BlkSize = list_to_integer(tftp_test_lib:lookup_option("blksize", "512", SuggestedOptions)), + State = #extra_state{blksize = BlkSize, peer = Peer}, + extra_open(Peer, Access, LocalFilename, Mode, SuggestedOptions, State), + {ok, SuggestedOptions, State}; +extra_prepare(_Peer, _Access, _Bin, _Mode, _SuggestedOptions, _Initial) -> + {error, {undef, "Illegal callback options."}}. + +%%------------------------------------------------------------------- +%% Open +%%------------------------------------------------------------------- + +extra_open(Peer, Access, LocalFilename, Mode, SuggestedOptions, []) -> + %% Server side + case extra_prepare(Peer, Access, LocalFilename, Mode, SuggestedOptions, []) of + {ok, AcceptedOptions, []} -> + BlkSize = list_to_integer(tftp_test_lib:lookup_option("blksize", "512", AcceptedOptions)), + State = #extra_state{blksize = BlkSize, peer = Peer}, + extra_open(Peer, Access, LocalFilename, Mode, AcceptedOptions, State); + {error, {Code, Text}} -> + {error, {Code, Text}} + end; +extra_open(_Peer, Access, LocalFilename, _Mode, NegotiatedOptions, #extra_state{} = State) -> + {File, Acc} = + case Access of + read -> + if + is_binary(LocalFilename) -> + {undefined, LocalFilename}; + is_list(LocalFilename) -> + {ok, Bin} = file:read_file(LocalFilename), + {LocalFilename, Bin} + end; + write -> + {LocalFilename, []} + end, + %% Both sides + State2 = State#extra_state{file = File, acc = Acc, count = 0}, + {ok, NegotiatedOptions, State2}. + +%%------------------------------------------------------------------- +%% Read +%%------------------------------------------------------------------- + +extra_read(#extra_state{acc = Bin} = State) when is_binary(Bin) -> + BlkSize = State#extra_state.blksize, + Count = State#extra_state.count + size(Bin), + if + size(Bin) >= BlkSize -> + <> = Bin, + State2 = State#extra_state{acc = Bin2, count = Count}, + {more, Block, State2}; + size(Bin) < BlkSize -> + Res = [{count, Count}, State#extra_state.peer], + {last, Bin, Res} + end. + +%%------------------------------------------------------------------- +%% Write +%%------------------------------------------------------------------- + +extra_write(Bin, #extra_state{acc = List} = State) when is_binary(Bin), is_list(List) -> + Size = size(Bin), + BlkSize = State#extra_state.blksize, + if + Size == BlkSize -> + {more, State#extra_state{acc = [Bin | List]}}; + Size < BlkSize -> + Bin2 = list_to_binary(lists:reverse([Bin | List])), + Res = [{bin, Bin2}, State#extra_state.peer], + file:write_file(State#extra_state.file, Bin2), + {last, Res} + end. + +%%------------------------------------------------------------------- +%% Abort +%%------------------------------------------------------------------- + +extra_abort(_Code, _Text, #extra_state{}) -> + ok. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Re-send client +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +resend_client(doc) -> + ["Verify that the server behaves correctly when the client re-sends packets."]; +resend_client(suite) -> + []; +resend_client(Config) when is_list(Config) -> + Host = {127, 0, 0, 1}, + {Port, DaemonPid} = ?IGNORE(?START_DAEMON(0, [{debug, all}])), + + ?VERIFY(ok, resend_read_client(Host, Port, 10)), + ?VERIFY(ok, resend_read_client(Host, Port, 512)), + ?VERIFY(ok, resend_read_client(Host, Port, 1025)), + + ?VERIFY(ok, resend_write_client(Host, Port, 10)), + ?VERIFY(ok, resend_write_client(Host, Port, 512)), + ?VERIFY(ok, resend_write_client(Host, Port, 1025)), + + %% Cleanup + unlink(DaemonPid), + exit(DaemonPid, kill), + ok. + +resend_read_client(Host, Port, BlkSize) -> + RemoteFilename = "tftp_resend_read_client.tmp", + Block1 = lists:duplicate(BlkSize, $1), + Block2 = lists:duplicate(BlkSize, $2), + Block3 = lists:duplicate(BlkSize, $3), + Block4 = lists:duplicate(BlkSize, $4), + Block5 = lists:duplicate(BlkSize, $5), + Blocks = [Block1, Block2, Block3, Block4, Block5], + Blob = list_to_binary(Blocks), + ?VERIFY(ok, file:write_file(RemoteFilename, Blob)), + + Timeout = timer:seconds(3), + ?VERIFY(timeout, recv(0)), + + %% Open socket + {ok, Socket} = ?VERIFY({ok, _}, gen_udp:open(0, [binary, {reuseaddr, true}, {active, true}])), + + ReadList = [0, 1, RemoteFilename, 0, "octet", 0], + Data1Bin = list_to_binary([0, 3, 0, 1 | Block1]), + NewPort = + if + BlkSize =:= 512 -> + %% Send READ + ReadBin = list_to_binary(ReadList), + ?VERIFY(ok, gen_udp:send(Socket, Host, Port, ReadBin)), + + %% Sleep a while in order to provoke the server to re-send the packet + timer:sleep(Timeout + timer:seconds(1)), + + %% Recv DATA #1 (the packet that the server think that we have lost) + {udp, _, _, NewPort0, _} = ?VERIFY({udp, Socket, Host, _, Data1Bin}, recv(Timeout)), + NewPort0; + true -> + %% Send READ + BlkSizeList = integer_to_list(BlkSize), + Options = ["blksize", 0, BlkSizeList, 0], + ReadBin = list_to_binary([ReadList | Options]), + ?VERIFY(ok, gen_udp:send(Socket, Host, Port, ReadBin)), + + %% Recv OACK + OptionAckBin = list_to_binary([0, 6 | Options]), + {udp, _, _, NewPort0, _} = ?VERIFY({udp, Socket, Host, _, OptionAckBin}, recv(Timeout)), + + %% Send ACK #0 + Ack0Bin = <<0, 4, 0, 0>>, + ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort0, Ack0Bin)), + + %% Send ACK #0 AGAIN (pretend that we timed out) + timer:sleep(timer:seconds(1)), + ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort0, Ack0Bin)), + + %% Recv DATA #1 (the packet that the server think that we have lost) + ?VERIFY({udp, Socket, Host, NewPort0, Data1Bin}, recv(Timeout)), + NewPort0 + end, + + %% Recv DATA #1 AGAIN (the re-sent package) + ?VERIFY({udp, Socket, Host, NewPort, Data1Bin}, recv(Timeout)), + + %% Send ACK #1 + Ack1Bin = <<0, 4, 0, 1>>, + ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Ack1Bin)), + + %% Recv DATA #2 + Data2Bin = list_to_binary([0, 3, 0, 2 | Block2]), + ?VERIFY({udp, Socket, Host, NewPort, Data2Bin}, recv(Timeout)), + + %% Send ACK #2 + Ack2Bin = <<0, 4, 0, 2>>, + ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Ack2Bin)), + + %% Recv DATA #3 + Data3Bin = list_to_binary([0, 3, 0, 3 | Block3]), + ?VERIFY({udp, Socket, Host, NewPort, Data3Bin}, recv(Timeout)), + + %% Send ACK #3 + Ack3Bin = <<0, 4, 0, 3>>, + ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Ack3Bin)), + + %% Send ACK #3 AGAIN (pretend that we timed out) + timer:sleep(timer:seconds(1)), + ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Ack3Bin)), + + %% Recv DATA #4 (the packet that the server think that we have lost) + Data4Bin = list_to_binary([0, 3, 0, 4 | Block4]), + ?VERIFY({udp, Socket, Host, NewPort, Data4Bin}, recv(Timeout)), + + %% Recv DATA #4 AGAIN (the re-sent package) + ?VERIFY({udp, Socket, Host, NewPort, Data4Bin}, recv(Timeout)), + + %% Send ACK #2 which is out of range + ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Ack2Bin)), + + %% Send ACK #4 + Ack4Bin = <<0, 4, 0, 4>>, + ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Ack4Bin)), + + %% Recv DATA #5 + Data5Bin = list_to_binary([0, 3, 0, 5 | Block5]), + ?VERIFY({udp, Socket, Host, NewPort, Data5Bin}, recv(Timeout)), + + %% Send ACK #5 + Ack5Bin = <<0, 4, 0, 5>>, + ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Ack5Bin)), + + %% Close socket + ?VERIFY(ok, gen_udp:close(Socket)), + + ?VERIFY(timeout, recv(Timeout)), + ?VERIFY(ok, file:delete(RemoteFilename)), + ok. + +resend_write_client(Host, Port, BlkSize) -> + RemoteFilename = "tftp_resend_write_client.tmp", + Block1 = lists:duplicate(BlkSize, $1), + Block2 = lists:duplicate(BlkSize, $2), + Block3 = lists:duplicate(BlkSize, $3), + Block4 = lists:duplicate(BlkSize, $4), + Block5 = lists:duplicate(BlkSize, $5), + Blocks = [Block1, Block2, Block3, Block4, Block5], + Blob = list_to_binary(Blocks), + ?IGNORE(file:delete(RemoteFilename)), + ?VERIFY({error, enoent}, file:read_file(RemoteFilename)), + + Timeout = timer:seconds(3), + ?VERIFY(timeout, recv(0)), + + %% Open socket + {ok, Socket} = ?VERIFY({ok, _}, gen_udp:open(0, [binary, {reuseaddr, true}, {active, true}])), + + WriteList = [0, 2, RemoteFilename, 0, "octet", 0], + NewPort = + if + BlkSize =:= 512 -> + %% Send WRITE + WriteBin = list_to_binary(WriteList), + ?VERIFY(ok, gen_udp:send(Socket, Host, Port, WriteBin)), + + %% Sleep a while in order to provoke the server to re-send the packet + timer:sleep(Timeout + timer:seconds(1)), + + %% Recv ACK #0 (the packet that the server think that we have lost) + Ack0Bin = <<0, 4, 0, 0>>, + ?VERIFY({udp, Socket, Host, _, Ack0Bin}, recv(Timeout)), + + %% Recv ACK #0 AGAIN (the re-sent package) + {udp, _, _, NewPort0, _} = ?VERIFY({udp, Socket, Host, _, Ack0Bin}, recv(Timeout)), + NewPort0; + true -> + %% Send WRITE + BlkSizeList = integer_to_list(BlkSize), + WriteBin = list_to_binary([WriteList, "blksize", 0, BlkSizeList, 0]), + ?VERIFY(ok, gen_udp:send(Socket, Host, Port, WriteBin)), + + %% Sleep a while in order to provoke the server to re-send the packet + timer:sleep(timer:seconds(1)), + + %% Recv OACK (the packet that the server think that we have lost) + OptionAckBin = list_to_binary([0, 6, "blksize",0, BlkSizeList, 0]), + ?VERIFY({udp, Socket, Host, _, OptionAckBin}, recv(Timeout)), + + %% Recv OACK AGAIN (the re-sent package) + {udp, _, _, NewPort0, _} = ?VERIFY({udp, Socket, Host, _, OptionAckBin}, recv(Timeout)), + NewPort0 + end, + + %% Send DATA #1 + Data1Bin = list_to_binary([0, 3, 0, 1 | Block1]), + ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Data1Bin)), + + %% Recv ACK #1 + Ack1Bin = <<0, 4, 0, 1>>, + ?VERIFY({udp, Socket, Host, NewPort, Ack1Bin}, recv(Timeout)), + + %% Send DATA #2 + Data2Bin = list_to_binary([0, 3, 0, 2 | Block2]), + ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Data2Bin)), + + %% Recv ACK #2 + Ack2Bin = <<0, 4, 0, 2>>, + ?VERIFY({udp, Socket, Host, NewPort, Ack2Bin}, recv(Timeout)), + + %% Send DATA #3 + Data3Bin = list_to_binary([0, 3, 0, 3 | Block3]), + ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Data3Bin)), + + %% Recv ACK #3 + Ack3Bin = <<0, 4, 0, 3>>, + ?VERIFY({udp, Socket, Host, NewPort, Ack3Bin}, recv(Timeout)), + + %% Send DATA #3 AGAIN (pretend that we timed out) + timer:sleep(timer:seconds(1)), + ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Data3Bin)), + + %% Recv ACK #3 AGAIN (the packet that the server think that we have lost) + ?VERIFY({udp, Socket, Host, NewPort, Ack3Bin}, recv(Timeout)), + + %% Send DATA #2 which is out of range + ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Data2Bin)), + + %% Send DATA #4 + Data4Bin = list_to_binary([0, 3, 0, 4 | Block4]), + ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Data4Bin)), + + %% Recv ACK #4 + Ack4Bin = <<0, 4, 0, 4>>, + ?VERIFY({udp, Socket, Host, NewPort, Ack4Bin}, recv(Timeout)), + + %% Send DATA #5 + Data5Bin = list_to_binary([0, 3, 0, 5 | Block5]), + ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Data5Bin)), + + %% Recv ACK #5 + Ack5Bin = <<0, 4, 0, 5>>, + ?VERIFY({udp, Socket, Host, NewPort, Ack5Bin}, recv(Timeout)), + + %% Close socket + ?VERIFY(ok, gen_udp:close(Socket)), + + ?VERIFY(timeout, recv(Timeout)), + ?VERIFY({ok, Blob}, file:read_file(RemoteFilename)), + ?VERIFY(ok, file:delete(RemoteFilename)), + ok. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Re-send server +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +resend_server(doc) -> + ["Verify that the server behaves correctly when the server re-sends packets."]; +resend_server(suite) -> + []; +resend_server(Config) when is_list(Config) -> + Host = {127, 0, 0, 1}, + + ?VERIFY(ok, resend_read_server(Host, 10)), + ?VERIFY(ok, resend_read_server(Host, 512)), + ?VERIFY(ok, resend_read_server(Host, 1025)), + + ?VERIFY(ok, resend_write_server(Host, 10)), + ?VERIFY(ok, resend_write_server(Host, 512)), + ?VERIFY(ok, resend_write_server(Host, 1025)), + ok. + +resend_read_server(Host, BlkSize) -> + RemoteFilename = "tftp_resend_read_server.tmp", + Block1 = lists:duplicate(BlkSize, $1), + Block2 = lists:duplicate(BlkSize, $2), + Block3 = lists:duplicate(BlkSize, $3), + Block4 = lists:duplicate(BlkSize, $4), + Block5 = lists:duplicate(BlkSize, $5), + Block6 = [], + Blocks = [Block1, Block2, Block3, Block4, Block5, Block6], + Blob = list_to_binary(Blocks), + + Timeout = timer:seconds(3), + ?VERIFY(timeout, recv(0)), + + %% Open daemon socket + {ok, DaemonSocket} = ?VERIFY({ok, _}, gen_udp:open(0, [binary, {reuseaddr, true}, {active, true}])), + {ok, DaemonPort} = ?IGNORE(inet:port(DaemonSocket)), + + %% Open server socket + {ok, ServerSocket} = ?VERIFY({ok, _}, gen_udp:open(0, [binary, {reuseaddr, true}, {active, true}])), + ?IGNORE(inet:port(ServerSocket)), + + %% Prepare client process + ReplyTo = self(), + ClientFun = + fun(Extra) -> + Options = [{port, DaemonPort}, {debug, brief}] ++ Extra, + Res = ?VERIFY({ok, Blob}, tftp:read_file(RemoteFilename, binary, Options)), + ReplyTo ! {self(), {tftp_client_reply, Res}}, + exit(normal) + end, + + ReadList = [0, 1, RemoteFilename, 0, "octet", 0], + Data1Bin = list_to_binary([0, 3, 0, 1 | Block1]), + Ack1Bin = <<0, 4, 0, 1>>, + {ClientPort, ClientPid} = + if + BlkSize =:= 512 -> + %% Start client process + ClientPid0 = spawn_link(fun() -> ClientFun([]) end), + + %% Recv READ + ReadBin = list_to_binary(ReadList), + {udp, _, _, ClientPort0, _} = ?VERIFY({udp, DaemonSocket, Host, _, ReadBin}, recv(Timeout)), + + %% Send DATA #1 + ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort0, Data1Bin)), + + %% Sleep a while in order to provoke the client to re-send the packet + timer:sleep(Timeout + timer:seconds(1)), + + %% Recv ACK #1 (the packet that the server think that we have lost) + ?VERIFY({udp, ServerSocket, Host, ClientPort0, Ack1Bin}, recv(Timeout)), + + %% Recv ACK #1 AGAIN (the re-sent package) + ?VERIFY({udp, ServerSocket, Host, _, Ack1Bin}, recv(Timeout)), + {ClientPort0, ClientPid0}; + true -> + %% Start client process + BlkSizeList = integer_to_list(BlkSize), + ClientPid0 = spawn_link(fun() -> ClientFun([{"blksize", BlkSizeList}]) end), + + %% Recv READ + Options = ["blksize", 0, BlkSizeList, 0], + ReadBin = list_to_binary([ReadList | Options]), + {udp, _, _, ClientPort0, _} = ?VERIFY({udp, DaemonSocket, Host, _, ReadBin}, recv(Timeout)), + + %% Send OACK + BlkSizeList = integer_to_list(BlkSize), + OptionAckBin = list_to_binary([0, 6, "blksize",0, BlkSizeList, 0]), + ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort0, OptionAckBin)), + + %% Sleep a while in order to provoke the client to re-send the packet + timer:sleep(Timeout + timer:seconds(1)), + + %% Recv ACK #0 (the packet that the server think that we have lost) + Ack0Bin = <<0, 4, 0, 0>>, + ?VERIFY({udp, ServerSocket, Host, ClientPort0, Ack0Bin}, recv(Timeout)), + + %% Recv ACK #0 AGAIN (the re-sent package) + ?VERIFY({udp, ServerSocket, Host, ClientPort0, Ack0Bin}, recv(Timeout)), + + %% Send DATA #1 + ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort0, Data1Bin)), + + %% Recv ACK #1 + ?VERIFY({udp, ServerSocket, Host, _, Ack1Bin}, recv(Timeout)), + {ClientPort0, ClientPid0} + end, + + %% Send DATA #2 + Data2Bin = list_to_binary([0, 3, 0, 2 | Block2]), + ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Data2Bin)), + + %% Recv ACK #2 + Ack2Bin = <<0, 4, 0, 2>>, + ?VERIFY({udp, ServerSocket, Host, ClientPort, Ack2Bin}, recv(Timeout)), + + %% Send DATA #3 + Data3Bin = list_to_binary([0, 3, 0, 3 | Block3]), + ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Data3Bin)), + + %% Recv ACK #3 + Ack3Bin = <<0, 4, 0, 3>>, + ?VERIFY({udp, ServerSocket, Host, ClientPort, Ack3Bin}, recv(Timeout)), + + %% Send DATA #3 AGAIN (pretend that we timed out) + timer:sleep(timer:seconds(1)), + ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Data3Bin)), + + %% Recv ACK #3 AGAIN (the packet that the server think that we have lost) + ?VERIFY({udp, ServerSocket, Host, ClientPort, Ack3Bin}, recv(Timeout)), + + %% Send DATA #4 + Data4Bin = list_to_binary([0, 3, 0, 4 | Block4]), + ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Data4Bin)), + + %% Recv ACK #4 + Ack4Bin = <<0, 4, 0, 4>>, + ?VERIFY({udp, ServerSocket, Host, ClientPort, Ack4Bin}, recv(Timeout)), + + %% Send DATA #3 which is out of range + ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Data3Bin)), + + %% Send DATA #5 + Data5Bin = list_to_binary([0, 3, 0, 5 | Block5]), + ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Data5Bin)), + + %% Recv ACK #5 + Ack5Bin = <<0, 4, 0, 5>>, + ?VERIFY({udp, ServerSocket, Host, ClientPort, Ack5Bin}, recv(Timeout)), + + %% Send DATA #6 + Data6Bin = list_to_binary([0, 3, 0, 6 | Block6]), + ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Data6Bin)), + + %% Close daemon and server sockets + ?VERIFY(ok, gen_udp:close(ServerSocket)), + ?VERIFY(ok, gen_udp:close(DaemonSocket)), + + ?VERIFY({ClientPid, {tftp_client_reply, {ok, Blob}}}, recv(Timeout)), + + ?VERIFY(timeout, recv(Timeout)), + ok. + +resend_write_server(Host, BlkSize) -> + RemoteFilename = "tftp_resend_write_server.tmp", + Block1 = lists:duplicate(BlkSize, $1), + Block2 = lists:duplicate(BlkSize, $2), + Block3 = lists:duplicate(BlkSize, $3), + Block4 = lists:duplicate(BlkSize, $4), + Block5 = lists:duplicate(BlkSize, $5), + Block6 = [], + Blocks = [Block1, Block2, Block3, Block4, Block5, Block6], + Blob = list_to_binary(Blocks), + Size = size(Blob), + + Timeout = timer:seconds(3), + ?VERIFY(timeout, recv(0)), + + %% Open daemon socket + {ok, DaemonSocket} = ?VERIFY({ok, _}, gen_udp:open(0, [binary, {reuseaddr, true}, {active, true}])), + {ok, DaemonPort} = ?IGNORE(inet:port(DaemonSocket)), + + %% Open server socket + {ok, ServerSocket} = ?VERIFY({ok, _}, gen_udp:open(0, [binary, {reuseaddr, true}, {active, true}])), + ?IGNORE(inet:port(ServerSocket)), + + %% Prepare client process + ReplyTo = self(), + ClientFun = + fun(Extra) -> + Options = [{port, DaemonPort}, {debug, brief}] ++ Extra, + Res = ?VERIFY({ok, Size}, tftp:write_file(RemoteFilename, Blob, Options)), + ReplyTo ! {self(), {tftp_client_reply, Res}}, + exit(normal) + end, + + WriteList = [0, 2, RemoteFilename, 0, "octet", 0], + Data1Bin = list_to_binary([0, 3, 0, 1 | Block1]), + {ClientPort, ClientPid} = + if + BlkSize =:= 512 -> + %% Start client process + ClientPid0 = spawn_link(fun() -> ClientFun([]) end), + + %% Recv WRITE + WriteBin = list_to_binary(WriteList), + io:format("WriteBin ~p\n", [WriteBin]), + {udp, _, _, ClientPort0, _} = ?VERIFY({udp, DaemonSocket, Host, _, WriteBin}, recv(Timeout)), + + %% Send ACK #1 + Ack0Bin = <<0, 4, 0, 0>>, + ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort0, Ack0Bin)), + + %% Sleep a while in order to provoke the client to re-send the packet + timer:sleep(Timeout + timer:seconds(1)), + + %% Recv DATA #1 (the packet that the server think that we have lost) + ?VERIFY({udp, ServerSocket, Host, ClientPort0, Data1Bin}, recv(Timeout)), + + %% Recv DATA #1 AGAIN (the re-sent package) + ?VERIFY({udp, ServerSocket, Host, _, Data1Bin}, recv(Timeout)), + {ClientPort0, ClientPid0}; + true -> + %% Start client process + BlkSizeList = integer_to_list(BlkSize), + ClientPid0 = spawn_link(fun() -> ClientFun([{"blksize", BlkSizeList}]) end), + + %% Recv WRITE + Options = ["blksize", 0, BlkSizeList, 0], + WriteBin = list_to_binary([WriteList | Options]), + {udp, _, _, ClientPort0, _} = ?VERIFY({udp, DaemonSocket, Host, _, WriteBin}, recv(Timeout)), + + %% Send OACK + BlkSizeList = integer_to_list(BlkSize), + OptionAckBin = list_to_binary([0, 6, "blksize",0, BlkSizeList, 0]), + ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort0, OptionAckBin)), + + %% Sleep a while in order to provoke the client to re-send the packet + timer:sleep(Timeout + timer:seconds(1)), + + %% Recv DATA #1 (the packet that the server think that we have lost) + ?VERIFY({udp, ServerSocket, Host, ClientPort0, Data1Bin}, recv(Timeout)), + + %% Recv DATA #1 AGAIN (the re-sent package) + ?VERIFY({udp, ServerSocket, Host, ClientPort0, Data1Bin}, recv(Timeout)), + {ClientPort0, ClientPid0} + end, + + %% Send ACK #1 + Ack1Bin = <<0, 4, 0, 1>>, + ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Ack1Bin)), + + %% Recv DATA #2 + Data2Bin = list_to_binary([0, 3, 0, 2 | Block2]), + ?VERIFY({udp, ServerSocket, Host, ClientPort, Data2Bin}, recv(Timeout)), + + %% Send ACK #2 + Ack2Bin = <<0, 4, 0, 2>>, + ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Ack2Bin)), + + %% Recv DATA #3 + Data3Bin = list_to_binary([0, 3, 0, 3 | Block3]), + ?VERIFY({udp, ServerSocket, Host, ClientPort, Data3Bin}, recv(Timeout)), + + %% Send ACK #3 + Ack3Bin = <<0, 4, 0, 3>>, + ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Ack3Bin)), + + %% Send ACK #3 AGAIN (pretend that we timed out) + timer:sleep(timer:seconds(1)), + ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Ack3Bin)), + + %% Recv DATA #4 (the packet that the server think that we have lost) + Data4Bin = list_to_binary([0, 3, 0, 4 | Block4]), + ?VERIFY({udp, ServerSocket, Host, ClientPort, Data4Bin}, recv(Timeout)), + + %% Recv DATA #4 AGAIN (the re-sent package) + ?VERIFY({udp, ServerSocket, Host, ClientPort, Data4Bin}, recv(Timeout)), + + %% Send ACK #4 + Ack4Bin = <<0, 4, 0, 4>>, + ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Ack4Bin)), + + %% Recv DATA #5 + Data5Bin = list_to_binary([0, 3, 0, 5 | Block5]), + ?VERIFY({udp, ServerSocket, Host, ClientPort, Data5Bin}, recv(Timeout)), + + %% Send ACK #3 which is out of range + ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Ack3Bin)), + + %% Send ACK #5 + Ack5Bin = <<0, 4, 0, 5>>, + ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Ack5Bin)), + + %% Recv DATA #6 + Data6Bin = list_to_binary([0, 3, 0, 6 | Block6]), + ?VERIFY({udp, ServerSocket, Host, ClientPort, Data6Bin}, recv(Timeout)), + + %% Send ACK #6 + Ack6Bin = <<0, 4, 0, 6>>, + ?VERIFY(ok, gen_udp:send(ServerSocket, Host, ClientPort, Ack6Bin)), + + %% Close daemon and server sockets + ?VERIFY(ok, gen_udp:close(ServerSocket)), + ?VERIFY(ok, gen_udp:close(DaemonSocket)), + + ?VERIFY({ClientPid, {tftp_client_reply, {ok, Size}}}, recv(Timeout)), + + ?VERIFY(timeout, recv(Timeout)), + ok. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +reuse_connection(doc) -> + ["Verify that the server can reuse an ongiong connection when same client resends request."]; +reuse_connection(suite) -> + []; +reuse_connection(Config) when is_list(Config) -> + Host = {127, 0, 0, 1}, + {Port, DaemonPid} = ?IGNORE(?START_DAEMON(0, [{debug, all}])), + + RemoteFilename = "reuse_connection.tmp", + BlkSize = 512, + Block1 = lists:duplicate(BlkSize, $1), + Block2 = lists:duplicate(BlkSize div 2, $2), + Blocks = [Block1, Block2], + Blob = list_to_binary(Blocks), + ?VERIFY(ok, file:write_file(RemoteFilename, Blob)), + + Seconds = 3, + Timeout = timer:seconds(Seconds), + ?VERIFY(timeout, recv(0)), + + %% Open socket + {ok, Socket} = ?VERIFY({ok, _}, gen_udp:open(0, [binary, {reuseaddr, true}, {active, true}])), + + ReadList = [0, 1, RemoteFilename, 0, "octet", 0], + Data1Bin = list_to_binary([0, 3, 0, 1 | Block1]), + + %% Send READ + TimeoutList = integer_to_list(Seconds), + Options = ["timeout", 0, TimeoutList, 0], + ReadBin = list_to_binary([ReadList | Options]), + ?VERIFY(ok, gen_udp:send(Socket, Host, Port, ReadBin)), + + %% Send yet another READ for same file + ?VERIFY(ok, gen_udp:send(Socket, Host, Port, ReadBin)), + + %% Recv OACK + OptionAckBin = list_to_binary([0, 6 | Options]), + {udp, _, _, NewPort, _} = ?VERIFY({udp, Socket, Host, _, OptionAckBin}, recv(Timeout)), + + %% Send ACK #0 + Ack0Bin = <<0, 4, 0, 0>>, + ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Ack0Bin)), + + %% Recv DATA #1 + ?VERIFY({udp, Socket, Host, NewPort, Data1Bin}, recv(Timeout)), + + %% Send ACK #1 + Ack1Bin = <<0, 4, 0, 1>>, + ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Ack1Bin)), + + %% Recv DATA #2 + Data2Bin = list_to_binary([0, 3, 0, 2 | Block2]), + ?VERIFY({udp, Socket, Host, NewPort, Data2Bin}, recv(Timeout)), + + %% Send ACK #2 + Ack2Bin = <<0, 4, 0, 2>>, + ?VERIFY(ok, gen_udp:send(Socket, Host, NewPort, Ack2Bin)), + + %% Close socket + ?VERIFY(ok, gen_udp:close(Socket)), + + ?VERIFY(timeout, recv(Timeout)), + ?VERIFY(ok, file:delete(RemoteFilename)), + + %% Cleanup + unlink(DaemonPid), + exit(DaemonPid, kill), + ok. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Large file: transfer > 65535 blocks +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +large_file(doc) -> + ["Start the daemon and test transfer of files greater than 32M."]; +large_file(suite) -> + []; +large_file(Config) when is_list(Config) -> + ?VERIFY(ok, application:start(tftp)), + + {Port, DaemonPid} = ?IGNORE(?START_DAEMON(0, [{debug, brief}])), + + %% Read fail + RemoteFilename = "tftp_temporary_large_file_remote_test_file.txt", + LocalFilename = "tftp_temporary_large_file_local_test_file.txt", + + {ok, FH} = file:open(LocalFilename, [write,exclusive]), + {ok, Size} = file:position(FH, {eof, 2*512*65535}), + ok = file:truncate(FH), + ?IGNORE(file:close(FH)), + + %% Write and read + ?VERIFY({ok, Size}, tftp:write_file(RemoteFilename, LocalFilename, [{port, Port}])), + ?IGNORE(file:delete(LocalFilename)), + ?VERIFY({ok, Size}, tftp:read_file(RemoteFilename, LocalFilename, [{port, Port}])), + + %% Cleanup + unlink(DaemonPid), + exit(DaemonPid, kill), + ?VERIFY(ok, file:delete(LocalFilename)), + ?VERIFY(ok, file:delete(RemoteFilename)), + ?VERIFY(ok, application:stop(tftp)), + ok. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Goodies +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +recv(Timeout) -> + receive + Msg -> + Msg + after Timeout -> + timeout + end. diff --git a/lib/tftp/test/tftp_bench.spec b/lib/tftp/test/tftp_bench.spec new file mode 100644 index 0000000000..43fa385c85 --- /dev/null +++ b/lib/tftp/test/tftp_bench.spec @@ -0,0 +1 @@ +{suites,"../tftp_test",[]}. diff --git a/lib/tftp/test/tftp_test_lib.erl b/lib/tftp/test/tftp_test_lib.erl new file mode 100644 index 0000000000..45386389cb --- /dev/null +++ b/lib/tftp/test/tftp_test_lib.erl @@ -0,0 +1,308 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2007-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_test_lib). + +-compile(export_all). + +-include("tftp_test_lib.hrl"). + +%% +%% ----- +%% + +init_per_testcase(_Case, Config) when is_list(Config) -> + io:format("\n ", []), + ?IGNORE(application:stop(tftp)), + Config. + +end_per_testcase(_Case, Config) when is_list(Config) -> + ?IGNORE(application:stop(tftp)), + Config. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Infrastructure for test suite +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +error(Actual, Mod, Line) -> + (catch global:send(tftp_global_logger, {failed, Mod, Line})), + log(" Bad result: ~p\n", [Actual], Mod, Line), + Label = lists:concat([Mod, "(", Line, ") unexpected result"]), + et:report_event(60, Mod, Mod, Label, + [{line, Mod, Line}, {error, Actual}]), + case global:whereis_name(tftp_test_case_sup) of + undefined -> + ignore; + Pid -> + Fail = #'REASON'{mod = Mod, line = Line, desc = Actual}, + Pid ! {fail, self(), Fail} + end, + Actual. + +log(Format, Args, Mod, Line) -> + case global:whereis_name(tftp_global_logger) of + undefined -> + io:format(user, "~p(~p): " ++ Format, + [Mod, Line] ++ Args); + Pid -> + io:format(Pid, "~p(~p): " ++ Format, + [Mod, Line] ++ Args) + end. + +default_config() -> + []. + +t() -> + t([{?MODULE, all}]). + +t(Cases) -> + t(Cases, default_config()). + +t(Cases, Config) -> + process_flag(trap_exit, true), + Res = lists:flatten(do_test(Cases, Config)), + io:format("Res: ~p\n", [Res]), + display_result(Res), + Res. + +do_test({Mod, Fun}, Config) when is_atom(Mod), is_atom(Fun) -> + case catch apply(Mod, Fun, [suite]) of + [] -> + io:format("Eval: ~p:", [{Mod, Fun}]), + Res = eval(Mod, Fun, Config), + {R, _, _} = Res, + io:format(" ~p\n", [R]), + Res; + + Cases when is_list(Cases) -> + io:format("Expand: ~p ...\n", [{Mod, Fun}]), + Map = fun(Case) when is_atom(Case)-> {Mod, Case}; + (Case) -> Case + end, + do_test(lists:map(Map, Cases), Config); + + {req, _, {conf, Init, Cases, Finish}} -> + case (catch apply(Mod, Init, [Config])) of + Conf when is_list(Conf) -> + io:format("Expand: ~p ...\n", [{Mod, Fun}]), + Map = fun(Case) when is_atom(Case)-> {Mod, Case}; + (Case) -> Case + end, + Res = do_test(lists:map(Map, Cases), Conf), + (catch apply(Mod, Finish, [Conf])), + Res; + + {'EXIT', {skipped, Reason}} -> + io:format(" => skipping: ~p\n", [Reason]), + [{skipped, {Mod, Fun}, Reason}]; + + Error -> + io:format(" => failed: ~p\n", [Error]), + [{failed, {Mod, Fun}, Error}] + end; + + {'EXIT', {undef, _}} -> + io:format("Undefined: ~p\n", [{Mod, Fun}]), + [{nyi, {Mod, Fun}, ok}]; + + Error -> + io:format("Ignoring: ~p: ~p\n", [{Mod, Fun}, Error]), + [{failed, {Mod, Fun}, Error}] + end; +do_test(Mod, Config) when is_atom(Mod) -> + Res = do_test({Mod, all}, Config), + Res; +do_test(Cases, Config) when is_list(Cases) -> + [do_test(Case, Config) || Case <- Cases]; +do_test(Bad, _Config) -> + [{badarg, Bad, ok}]. + +eval(Mod, Fun, Config) -> + TestCase = {?MODULE, Mod, Fun}, + Label = lists:concat(["TEST CASE: ", Fun]), + et:report_event(40, ?MODULE, Mod, Label ++ " started", + [TestCase, Config]), + global:register_name(tftp_test_case_sup, self()), + Flag = process_flag(trap_exit, true), + Config2 = Mod:init_per_testcase(Fun, Config), + Pid = spawn_link(?MODULE, do_eval, [self(), Mod, Fun, Config2]), + R = wait_for_evaluator(Pid, Mod, Fun, Config2, []), + Mod:end_per_testcase(Fun, Config2), + global:unregister_name(tftp_test_case_sup), + process_flag(trap_exit, Flag), + R. + +wait_for_evaluator(Pid, Mod, Fun, Config, Errors) -> + TestCase = {?MODULE, Mod, Fun}, + Label = lists:concat(["TEST CASE: ", Fun]), + receive + {done, Pid, ok} when Errors == [] -> + et:report_event(40, Mod, ?MODULE, Label ++ " ok", + [TestCase, Config]), + {ok, {Mod, Fun}, Errors}; + {done, Pid, {ok, _}} when Errors == [] -> + et:report_event(40, Mod, ?MODULE, Label ++ " ok", + [TestCase, Config]), + {ok, {Mod, Fun}, Errors}; + {done, Pid, Fail} -> + et:report_event(20, Mod, ?MODULE, Label ++ " failed", + [TestCase, Config, {return, Fail}, Errors]), + {failed, {Mod,Fun}, Fail}; + {'EXIT', Pid, {skipped, Reason}} -> + et:report_event(20, Mod, ?MODULE, Label ++ " skipped", + [TestCase, Config, {skipped, Reason}]), + {skipped, {Mod, Fun}, Errors}; + {'EXIT', Pid, Reason} -> + et:report_event(20, Mod, ?MODULE, Label ++ " crashed", + [TestCase, Config, {'EXIT', Reason}]), + {crashed, {Mod, Fun}, [{'EXIT', Reason} | Errors]}; + {fail, Pid, Reason} -> + wait_for_evaluator(Pid, Mod, Fun, Config, Errors ++ [Reason]) + end. + +do_eval(ReplyTo, Mod, Fun, Config) -> + case (catch apply(Mod, Fun, [Config])) of + {'EXIT', {skipped, Reason}} -> + ReplyTo ! {'EXIT', self(), {skipped, Reason}}; + Other -> + ReplyTo ! {done, self(), Other} + end, + unlink(ReplyTo), + exit(shutdown). + +display_result([]) -> + io:format("OK\n", []); +display_result(Res) when is_list(Res) -> + Ok = [MF || {ok, MF, _} <- Res], + Nyi = [MF || {nyi, MF, _} <- Res], + Skipped = [{MF, Reason} || {skipped, MF, Reason} <- Res], + Failed = [{MF, Reason} || {failed, MF, Reason} <- Res], + Crashed = [{MF, Reason} || {crashed, MF, Reason} <- Res], + display_summary(Ok, Nyi, Skipped, Failed, Crashed), + display_skipped(Skipped), + display_failed(Failed), + display_crashed(Crashed). + +display_summary(Ok, Nyi, Skipped, Failed, Crashed) -> + io:format("\nTest case summary:\n", []), + display_summary(Ok, "successful"), + display_summary(Nyi, "not yet implemented"), + display_summary(Skipped, "skipped"), + display_summary(Failed, "failed"), + display_summary(Crashed, "crashed"), + io:format("\n", []). + +display_summary(Res, Info) -> + io:format(" ~w test cases ~s\n", [length(Res), Info]). + +display_skipped([]) -> + ok; +display_skipped(Skipped) -> + io:format("Skipped test cases:\n", []), + F = fun({MF, Reason}) -> io:format(" ~p => ~p\n", [MF, Reason]) end, + lists:foreach(F, Skipped), + io:format("\n", []). + + +display_failed([]) -> + ok; +display_failed(Failed) -> + io:format("Failed test cases:\n", []), + F = fun({MF, Reason}) -> io:format(" ~p => ~p\n", [MF, Reason]) end, + lists:foreach(F, Failed), + io:format("\n", []). + +display_crashed([]) -> + ok; +display_crashed(Crashed) -> + io:format("Crashed test cases:\n", []), + F = fun({MF, Reason}) -> io:format(" ~p => ~p\n", [MF, Reason]) end, + lists:foreach(F, Crashed), + io:format("\n", []). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% generic callback +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-record(generic_state, {state, prepare, open, read, write, abort}). + +prepare(Peer, Access, LocalFilename, Mode, SuggestedOptions, Initial) when is_list(Initial) -> + State = lookup_option(state, mandatory, Initial), + Prepare = lookup_option(prepare, mandatory, Initial), + Open = lookup_option(open, mandatory, Initial), + Read = lookup_option(read, mandatory, Initial), + Write = lookup_option(write, mandatory, Initial), + Abort = lookup_option(abort, mandatory, Initial), + case Prepare(Peer, Access, LocalFilename, Mode, SuggestedOptions, State) of + {ok, AcceptedOptions, NewState} -> + {ok, + AcceptedOptions, + #generic_state{state = NewState, + prepare = Prepare, + open = Open, + read = Read, + write = Write, + abort = Abort}}; + Other -> + Other + end. + +open(Peer, Access, LocalFilename, Mode, SuggestedOptions, Initial) when is_list(Initial) -> + case prepare(Peer, Access, LocalFilename, Mode, SuggestedOptions, Initial) of + {ok, SuggestedOptions2, GenericState} -> + open(Peer, Access, LocalFilename, Mode, SuggestedOptions2, GenericState); + Other -> + Other + end; +open(Peer, Access, LocalFilename, Mode, SuggestedOptions, #generic_state{state = State, open = Open} = GenericState) -> + case Open(Peer, Access, LocalFilename, Mode, SuggestedOptions, State) of + {ok, SuggestedOptions2, NewState} -> + {ok, SuggestedOptions2, GenericState#generic_state{state = NewState}}; + Other -> + Other + end. + +read(#generic_state{state = State, read = Read} = GenericState) -> + case Read(State) of + {more, DataBlock, NewState} -> + {more, DataBlock, GenericState#generic_state{state = NewState}}; + Other -> + Other + end. + +write(DataBlock, #generic_state{state = State, write = Write} = GenericState) -> + case Write(DataBlock, State) of + {more, NewState} -> + {more, GenericState#generic_state{state = NewState}}; + Other -> + Other + end. + +abort(Code, Text, #generic_state{state = State, abort = Abort}) -> + Abort(Code, Text, State). + +lookup_option(Key, Default, Options) -> + case lists:keysearch(Key, 1, Options) of + {value, {_, Val}} -> + Val; + false -> + Default + end. + diff --git a/lib/tftp/test/tftp_test_lib.hrl b/lib/tftp/test/tftp_test_lib.hrl new file mode 100644 index 0000000000..e7a5a37d2c --- /dev/null +++ b/lib/tftp/test/tftp_test_lib.hrl @@ -0,0 +1,44 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2007-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% +%% + +-record('REASON', {mod, line, desc}). + +-define(LOG(Format, Args), + tftp_test_lib:log(Format, Args, ?MODULE, ?LINE)). + +-define(ERROR(Reason), + tftp_test_lib:error(Reason, ?MODULE, ?LINE)). + +-define(VERIFY(Expected, Expr), + fun() -> + AcTuAlReS = (catch (Expr)), + case AcTuAlReS of + Expected -> ?LOG("Ok, ~p\n", [AcTuAlReS]); + _ -> ?ERROR(AcTuAlReS) + end, + AcTuAlReS + end()). + +-define(IGNORE(Expr), + fun() -> + AcTuAlReS = (catch (Expr)), + ?LOG("Ok, ~p\n", [AcTuAlReS]), + AcTuAlReS + end()). diff --git a/lib/tftp/vsn.mk b/lib/tftp/vsn.mk new file mode 100644 index 0000000000..c4a6e749e7 --- /dev/null +++ b/lib/tftp/vsn.mk @@ -0,0 +1,24 @@ +#-*-makefile-*- ; force emacs to enter makefile-mode + +# %CopyrightBegin% +# +# Copyright Ericsson AB 2001-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% + +APPLICATION = tftp +TFTP_VSN = 1.0.0 +PRE_VSN = +APP_VSN = "$(APPLICATION)-$(TFTP_VSN)$(PRE_VSN)" -- cgit v1.2.3