aboutsummaryrefslogtreecommitdiffstats
path: root/lib/tftp
diff options
context:
space:
mode:
Diffstat (limited to 'lib/tftp')
-rw-r--r--lib/tftp/AUTHORS11
-rw-r--r--lib/tftp/Makefile78
-rw-r--r--lib/tftp/doc/html/.gitignore0
-rw-r--r--lib/tftp/doc/man3/.gitignore0
-rw-r--r--lib/tftp/doc/man6/.gitignore0
-rw-r--r--lib/tftp/doc/pdf/.gitignore0
-rw-r--r--lib/tftp/doc/src/Makefile155
-rw-r--r--lib/tftp/doc/src/book.xml49
-rw-r--r--lib/tftp/doc/src/getting_started.xml81
-rw-r--r--lib/tftp/doc/src/introduction.xml62
-rw-r--r--lib/tftp/doc/src/notes.xml53
-rw-r--r--lib/tftp/doc/src/ref_man.xml36
-rw-r--r--lib/tftp/doc/src/tftp.xml599
-rw-r--r--lib/tftp/doc/src/usersguide.xml37
-rw-r--r--lib/tftp/ebin/.gitignore0
-rw-r--r--lib/tftp/info2
-rw-r--r--lib/tftp/src/Makefile110
-rw-r--r--lib/tftp/src/tftp.app.src22
-rw-r--r--lib/tftp/src/tftp.appup.src26
-rw-r--r--lib/tftp/src/tftp.erl409
-rw-r--r--lib/tftp/src/tftp.hrl69
-rw-r--r--lib/tftp/src/tftp_app.erl56
-rw-r--r--lib/tftp/src/tftp_binary.erl239
-rw-r--r--lib/tftp/src/tftp_engine.erl1422
-rw-r--r--lib/tftp/src/tftp_file.erl390
-rw-r--r--lib/tftp/src/tftp_lib.erl474
-rw-r--r--lib/tftp/src/tftp_logger.erl99
-rw-r--r--lib/tftp/src/tftp_sup.erl111
-rw-r--r--lib/tftp/test/Makefile250
-rw-r--r--lib/tftp/test/tftp.config1
-rw-r--r--lib/tftp/test/tftp.cover2
-rw-r--r--lib/tftp/test/tftp.spec1
-rw-r--r--lib/tftp/test/tftp_SUITE.erl1001
-rw-r--r--lib/tftp/test/tftp_bench.spec1
-rw-r--r--lib/tftp/test/tftp_test_lib.erl308
-rw-r--r--lib/tftp/test/tftp_test_lib.hrl44
-rw-r--r--lib/tftp/vsn.mk24
37 files changed, 6222 insertions, 0 deletions
diff --git a/lib/tftp/AUTHORS b/lib/tftp/AUTHORS
new file mode 100644
index 0000000000..71fc97c4e0
--- /dev/null
+++ b/lib/tftp/AUTHORS
@@ -0,0 +1,11 @@
+Original Authors:
+
+Håkan Mattsson - tftp
+
+Contributors:
+
+Ingela Anderton Andin
+Martin Gustafsson
+Johan Blom
+Torbjörn Törnkvist
+Joe Armstrong \ No newline at end of file
diff --git a/lib/tftp/Makefile b/lib/tftp/Makefile
new file mode 100644
index 0000000000..a4559fbc2e
--- /dev/null
+++ b/lib/tftp/Makefile
@@ -0,0 +1,78 @@
+#
+# %CopyrightBegin%
+#
+# Copyright Ericsson AB 1996-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
+
+# ----------------------------------------------------
+# 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
--- /dev/null
+++ b/lib/tftp/doc/html/.gitignore
diff --git a/lib/tftp/doc/man3/.gitignore b/lib/tftp/doc/man3/.gitignore
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/lib/tftp/doc/man3/.gitignore
diff --git a/lib/tftp/doc/man6/.gitignore b/lib/tftp/doc/man6/.gitignore
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/lib/tftp/doc/man6/.gitignore
diff --git a/lib/tftp/doc/pdf/.gitignore b/lib/tftp/doc/pdf/.gitignore
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/lib/tftp/doc/pdf/.gitignore
diff --git a/lib/tftp/doc/src/Makefile b/lib/tftp/doc/src/Makefile
new file mode 100644
index 0000000000..5d76799e41
--- /dev/null
+++ b/lib/tftp/doc/src/Makefile
@@ -0,0 +1,155 @@
+#
+# %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 \
+ getting_started.xml \
+ notes.xml
+
+XML_REF3_FILES = \
+ tftp.xml
+
+XML_PART_FILES = \
+ usersguide.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 -rf $(XMLDIR)
+ 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..c0b551d517
--- /dev/null
+++ b/lib/tftp/doc/src/book.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!DOCTYPE book SYSTEM "book.dtd">
+
+<book xmlns:xi="http://www.w3.org/2001/XInclude">
+ <header titlestyle="normal">
+ <copyright>
+ <year>1997</year><year>2018</year>
+ <holder>Ericsson AB. All Rights Reserved.</holder>
+ </copyright>
+ <legalnotice>
+ 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.
+
+ </legalnotice>
+
+ <title>TFTP</title>
+ <prepared>Péter Dimitrov</prepared>
+ <docno></docno>
+ <date>2018-03-22</date>
+ <rev>1.0</rev>
+ <file>book.sgml</file>
+ </header>
+ <insidecover>
+ </insidecover>
+ <pagetext>TFTP</pagetext>
+ <preamble>
+ <contents level="2"></contents>
+ </preamble>
+ <parts lift="no">
+ <xi:include href="usersguide.xml"/>
+ </parts>
+ <applications>
+ <xi:include href="ref_man.xml"/>
+ </applications>
+ <releasenotes>
+ <xi:include href="notes.xml"/>
+ </releasenotes>
+ <listofterms></listofterms>
+ <index></index>
+</book>
diff --git a/lib/tftp/doc/src/getting_started.xml b/lib/tftp/doc/src/getting_started.xml
new file mode 100644
index 0000000000..9bce52dbe0
--- /dev/null
+++ b/lib/tftp/doc/src/getting_started.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!DOCTYPE chapter SYSTEM "chapter.dtd">
+
+<chapter>
+ <header>
+ <copyright>
+ <year>1997</year>
+ <year>2018</year>
+ <holder>Ericsson AB. All Rights Reserved.</holder>
+ </copyright>
+ <legalnotice>
+ 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.
+
+ </legalnotice>
+
+ <title>Getting Started</title>
+ <prepared></prepared>
+ <docno></docno>
+ <approved></approved>
+ <date></date>
+ <rev></rev>
+ <file>getting_started.xml</file>
+ </header>
+
+ <section>
+ <title>General Information</title>
+ <p>The <seealso marker="tftp#start/1">start/1</seealso> 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.</p>
+ <p>On the client side,
+ function <seealso marker="tftp#read_file/3">read_file/3</seealso>
+ and <seealso marker="tftp#write_file/3">write_file/3</seealso>
+ spawn a temporary client process establishing
+ contact with a TFTP daemon and perform the file transfer.</p>
+ <p><c>tftp</c> uses a callback module to handle the file
+ transfer. Two such callback modules are provided,
+ <c>tftp_binary</c> and <c>tftp_file</c>. See
+ <seealso marker="tftp#read_file/3">read_file/3</seealso> and
+ <seealso marker="tftp#write_file/3">write_file/3</seealso> for details.
+ You can also implement your own callback modules, see
+ <seealso marker="tftp#tftp_callback">CALLBACK FUNCTIONS</seealso>.
+ A callback module provided by
+ the user is registered using option <c>callback</c>, see
+ <seealso marker="tftp#options">DATA TYPES</seealso>.</p>
+ </section>
+
+ <section>
+ <title>Using the TFTP client and server</title>
+ <p>This is a simple example of starting the TFTP server and reading the content
+ of a sample file using the TFTP client.</p>
+
+ <p><em>Step 1.</em> Create a sample file to be used for the transfer:</p>
+ <code>
+ $ echo "Erlang/OTP 21" > file.txt
+ </code>
+
+ <p><em>Step 2.</em> Start the TFTP server:</p>
+ <code type="erl" >
+ 1> {ok, Pid} = tftp:start([{port, 19999}]).
+ <![CDATA[{ok,<0.65.0>}]]>
+ </code>
+
+ <p><em>Step 3.</em> Start the TFTP client (in another shell):</p>
+ <code type="erl" >
+ 1> tftp:read_file("file.txt", binary, [{port, 19999}]).
+ <![CDATA[{ok,<<"Erlang/OTP 21\n">>}]]>
+ </code>
+ </section>
+
+</chapter>
diff --git a/lib/tftp/doc/src/introduction.xml b/lib/tftp/doc/src/introduction.xml
new file mode 100644
index 0000000000..70761db0dc
--- /dev/null
+++ b/lib/tftp/doc/src/introduction.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!DOCTYPE chapter SYSTEM "chapter.dtd">
+
+<chapter>
+ <header>
+ <copyright>
+ <year>1997</year><year>2018</year>
+ <holder>Ericsson AB. All Rights Reserved.</holder>
+ </copyright>
+ <legalnotice>
+ 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.
+
+ </legalnotice>
+
+ <title>Introduction</title>
+ <prepared>Péter Dimitrov</prepared>
+ <responsible></responsible>
+ <docno></docno>
+ <approved></approved>
+ <checked></checked>
+ <date>2018-03-22</date>
+ <rev>A</rev>
+ <file>introduction.xml</file>
+ </header>
+
+ <section>
+ <title>Purpose</title>
+ <p>The Trivial File Transfer Protocol or TFTP is a very simple protocol
+ used to transfer files.</p>
+ <p>It has been implemented on top of the User Datagram protocol (UDP) so
+ it may be used to move files between machines on different networks
+ implementing UDP. It is designed to be small and easy to implement.
+ Therefore, it lacks most of the features of a regular FTP. The only
+ thing it can do is read and write files (or mail) from/to a remote server.
+ It cannot list directories, and currently has no provisions for user
+ authentication.</p>
+ <p>The <c>tftp</c> application implements the following IETF standards:</p>
+ <list type="bulleted">
+ <item>RFC 1350, The TFTP Protocol (revision 2)</item>
+ <item>RFC 2347, TFTP Option Extension</item>
+ <item>RFC 2348, TFTP Blocksize Option</item>
+ <item>RFC 2349, TFTP Timeout Interval and Transfer Size Options</item>
+ </list>
+ <p>The only feature that not is implemented is the <c>netascii</c> transfer mode.</p>
+ </section>
+
+ <section>
+ <title>Prerequisites</title>
+ <p>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.</p>
+ </section>
+</chapter>
diff --git a/lib/tftp/doc/src/notes.xml b/lib/tftp/doc/src/notes.xml
new file mode 100644
index 0000000000..3a4d97a008
--- /dev/null
+++ b/lib/tftp/doc/src/notes.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!DOCTYPE chapter SYSTEM "chapter.dtd">
+
+<chapter>
+ <header>
+ <copyright>
+ <year>2002</year><year>2018</year>
+ <holder>Ericsson AB. All Rights Reserved.</holder>
+ </copyright>
+ <legalnotice>
+ 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.
+
+ </legalnotice>
+
+ <title>TFTP Release Notes</title>
+ <prepared></prepared>
+ <responsible></responsible>
+ <docno></docno>
+ <approved></approved>
+ <checked></checked>
+ <date>2018-03-22</date>
+ <rev>A</rev>
+ <file>notes.xml</file>
+ </header>
+
+ <section><title>TFTP 1.0</title>
+
+ <section><title>First released version</title>
+ <list>
+ <item>
+ <p>
+ 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.</p>
+ <p>
+ Own Id: OTP-14113</p>
+ </item>
+ </list>
+ </section>
+
+</section>
+
+</chapter>
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 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!DOCTYPE application SYSTEM "application.dtd">
+
+<application xmlns:xi="http://www.w3.org/2001/XInclude">
+ <header>
+ <copyright>
+ <year>1997</year><year>2018</year>
+ <holder>Ericsson AB. All Rights Reserved.</holder>
+ </copyright>
+ <legalnotice>
+ 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.
+
+ </legalnotice>
+
+ <title>TFTP Reference Manual</title>
+ <prepared>Péter Dimitrov</prepared>
+ <docno></docno>
+ <date>2018-03-22</date>
+ <rev>1.0</rev>
+ <file>ref_man.xml</file>
+ </header>
+ <description>
+ <p>The <c>TFTP</c> application.</p>
+ </description>
+ <xi:include href="tftp.xml"/>
+</application>
diff --git a/lib/tftp/doc/src/tftp.xml b/lib/tftp/doc/src/tftp.xml
new file mode 100644
index 0000000000..481e5446ad
--- /dev/null
+++ b/lib/tftp/doc/src/tftp.xml
@@ -0,0 +1,599 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!DOCTYPE erlref SYSTEM "erlref.dtd">
+
+<erlref>
+ <header>
+ <copyright>
+ <year>2006</year><year>2018</year>
+ <holder>Ericsson AB. All Rights Reserved.</holder>
+ </copyright>
+ <legalnotice>
+ 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.
+
+ </legalnotice>
+
+ <title>tftp</title>
+ <prepared></prepared>
+ <docno></docno>
+ <date></date>
+ <rev></rev>
+ </header>
+ <module>tftp</module>
+ <modulesummary>Trivial FTP.</modulesummary>
+ <description>
+ <p>Interface module for the <c>tftp</c> application.</p>
+ </description>
+
+ <section>
+ <marker id="options"></marker>
+ <title>DATA TYPES</title>
+ <p><c>ServiceConfig = Options</c></p>
+ <p><c>Options = [option()]</c></p>
+ <p>Most of the options are common for both the client and the server
+ side, but some of them differs a little.
+ The available <c>option()</c>s are as follows:</p>
+ <taglist>
+ <tag><c>{debug, Level}</c></tag>
+ <item>
+ <p><c>Level = none | error | warning | brief | normal | verbose | all</c></p>
+ <p>Controls the level of debug printouts.
+ Default is <c>none</c>.</p>
+ </item>
+ <tag><c>{host, Host}</c></tag>
+ <item>
+ <p><c>Host = hostname()</c>, see
+ <seealso marker="kernel:inet">inet(3)</seealso>.</p>
+ <p>The name or IP address of the host where the TFTP daemon
+ resides. This option is only used by the client.</p>
+ </item>
+ <tag><c>{port, Port}</c></tag>
+ <item>
+ <p><c>Port = int()</c></p>
+ <p>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 <c>info/1</c>).</p>
+ <p>If a socket is connected already, option
+ <c>{udp, [{fd, integer()}]}</c> can be used to pass the
+ open file descriptor to <c>gen_udp</c>. 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
+ <c>setuid_socket_wrap</c>, the command-line argument
+ "-tftpd_69 22" triggers the prebound file
+ descriptor 22 to be used instead of opening port 69.
+ The UDP option <c>{udp, [{fd, 22}]}</c> is automatically added.
+ See <c>init:get_argument/</c> about command-line arguments and
+ <c>gen_udp:open/2</c> about UDP options.</p>
+ </item>
+ <tag><c>{port_policy, Policy}</c></tag>
+ <item>
+ <p><c>Policy = random | Port | {range, MinPort, MaxPort}</c></p>
+ <p><c>Port = MinPort = MaxPort = int()</c></p>
+ <p>Policy for the selection of the temporary port that is used
+ by the server/client during the file transfer. Default is
+ <c>random</c>, 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.</p>
+ </item>
+ <tag><c>{udp, Options}</c></tag>
+ <item>
+ <p><c>Options = [Opt]</c>, see
+ <seealso marker="kernel:gen_udp#open/1">gen_udp:open/2</seealso>.</p>
+ </item>
+ <tag><c>{use_tsize, Bool}</c></tag>
+ <item>
+ <p><c>Bool = bool()</c></p>
+ <p>Flag for automated use of option <c>tsize</c>. With
+ this set to <c>true</c>, the <c>write_file/3</c> client
+ determines the filesize and sends it to the server as
+ the standardized <c>tsize</c> option. A <c>read_file/3</c>
+ client acquires only a filesize from the server by sending
+ a zero <c>tsize</c>.</p>
+ </item>
+ <tag><c>{max_tsize, MaxTsize}</c></tag>
+ <item>
+ <p><c>MaxTsize = int() | infinity</c></p>
+ <p>Threshold for the maximal filesize in bytes. The transfer
+ is aborted if the limit is exceeded.
+ Default is <c>infinity</c>.</p>
+ </item>
+ <tag><c>{max_conn, MaxConn}</c></tag>
+ <item>
+ <p><c>MaxConn = int() | infinity</c></p>
+ <p>Threshold for the maximal number of active connections.
+ The daemon rejects the setup of new connections if
+ the limit is exceeded. Default is <c>infinity</c>.</p>
+ </item>
+ <tag><c>{TftpKey, TftpVal}</c></tag>
+ <item>
+ <p><c>TftpKey = string()</c> <br></br>
+<c>TftpVal = string()</c></p>
+ <p>Name and value of a TFTP option.</p>
+ </item>
+ <tag><c>{reject, Feature}</c></tag>
+ <item>
+ <p><c>Feature = Mode | TftpKey</c> <br></br>
+<c>&nbsp;Mode = read | write</c> <br></br>
+<c>&nbsp;TftpKey = string()</c></p>
+ <p>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.</p>
+ </item>
+ <tag><c>{callback, {RegExp, Module, State}}</c></tag>
+ <item>
+ <p><c>RegExp = string()</c> <br></br>
+<c>Module = atom()</c> <br></br>
+<c>State = term()</c></p>
+ <p>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
+ <seealso marker="#read_file/3">read_file/3</seealso> and
+ <seealso marker="#write_file/3">write_file/3</seealso>.
+ </p>
+ <p>The callback module must implement the <c>tftp</c> behavior, see
+ <seealso marker="#tftp_callback">CALLBACK FUNCTIONS</seealso>.</p>
+ </item>
+
+ <tag><c>{logger, Module}</c></tag>
+ <item>
+ <p><c>Module = module()()</c></p>
+
+ <p>Callback module for customized logging of errors, warnings, and
+ info messages. The callback module must implement the
+ <c>tftp_logger</c> behavior, see
+ <seealso marker="#tftp_logger">LOGGER FUNCTIONS</seealso>.
+ The default module is <c>tftp_logger</c>.</p>
+ </item>
+
+ <tag><c>{max_retries, MaxRetries}</c></tag>
+ <item>
+ <p><c>MaxRetries = int()</c></p>
+
+ <p>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.</p>
+ </item>
+ </taglist>
+ </section>
+
+ <funcs>
+ <func>
+ <name>change_config(daemons, Options) -> [{Pid, Result}]</name>
+ <fsummary>Changes configuration for all daemons.
+ </fsummary>
+ <type>
+ <v>Options = [option()]</v>
+ <v>Pid = pid()</v>
+ <v>Result = ok | {error, Reason}</v>
+ <v>Reason = term()</v>
+ </type>
+ <desc>
+ <p>Changes configuration for all TFTP daemon processes. </p>
+ </desc>
+ </func>
+
+ <func>
+ <name>change_config(servers, Options) -> [{Pid, Result}]</name>
+ <fsummary>Changes configuration for all servers.
+ </fsummary>
+ <type>
+ <v>Options = [option()]</v>
+ <v>Pid = pid()</v>
+ <v>Result = ok | {error, Reason}</v>
+ <v>Reason = term()</v>
+ </type>
+ <desc>
+ <p>Changes configuration for all TFTP server processes.</p>
+ </desc>
+ </func>
+
+ <func>
+ <name>change_config(Pid, Options) -> Result</name>
+ <fsummary>Changes configuration for a TFTP daemon, server,
+ or client process.</fsummary>
+ <type>
+ <v>Pid = pid()</v>
+ <v>Options = [option()]</v>
+ <v>Result = ok | {error, Reason}</v>
+ <v>Reason = term()</v>
+ </type>
+ <desc>
+ <p>Changes configuration for a TFTP daemon, server, or client process.</p>
+ </desc>
+ </func>
+
+ <func>
+ <name>info(daemons) -> [{Pid, Options}]</name>
+ <fsummary>Returns information about all daemons.</fsummary>
+ <type>
+ <v>Pid = [pid()()]</v>
+ <v>Options = [option()]</v>
+ <v>Reason = term()</v>
+ </type>
+ <desc>
+ <p>Returns information about all TFTP daemon processes.</p>
+ </desc>
+ </func>
+
+ <func>
+ <name>info(servers) -> [{Pid, Options}]</name>
+ <fsummary>Returns information about all servers.</fsummary>
+ <type>
+ <v>Pid = [pid()()]</v>
+ <v>Options = [option()]</v>
+ <v>Reason = term()</v>
+ </type>
+ <desc>
+ <p>Returns information about all TFTP server processes. </p>
+ </desc>
+ </func>
+
+ <func>
+ <name>info(Pid) -> {ok, Options} | {error, Reason}</name>
+ <fsummary>Returns information about a daemon, server, or client process.</fsummary>
+ <type>
+ <v>Options = [option()]</v>
+ <v>Reason = term()</v>
+ </type>
+ <desc>
+ <p>Returns information about a TFTP daemon, server, or client process.</p>
+ </desc>
+ </func>
+
+ <func>
+ <name>read_file(RemoteFilename, LocalFilename, Options) -> {ok, LastCallbackState} | {error, Reason}</name>
+ <fsummary>Reads a (virtual) file from a TFTP server.</fsummary>
+ <type>
+ <v>RemoteFilename = string()</v>
+ <v>LocalFilename = binary | string()</v>
+ <v>Options = [option()]</v>
+ <v>LastCallbackState = term()</v>
+ <v>Reason = term()</v>
+ </type>
+ <desc>
+ <p>Reads a (virtual) file <c>RemoteFilename</c> from a TFTP
+ server.</p>
+ <p>If <c>LocalFilename</c> is the atom <c>binary</c>,
+ <c>tftp_binary</c> is used as callback module. It concatenates
+ all transferred blocks and returns them as one single binary
+ in <c>LastCallbackState</c>.</p>
+ <p>If <c>LocalFilename</c> is a string and there are no
+ registered callback modules, <c>tftp_file</c> is used as
+ callback module. It writes each transferred block to the file
+ named <c>LocalFilename</c> and returns the number of
+ transferred bytes in <c>LastCallbackState</c>.</p>
+ <p>If <c>LocalFilename</c> is a string and there are registered
+ callback modules, <c>LocalFilename</c> 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.</p>
+ </desc>
+ </func>
+
+ <func>
+ <name>start(Options) -> {ok, Pid} | {error, Reason}</name>
+ <fsummary>Starts a daemon process.</fsummary>
+ <type>
+ <v>Options = [option()]</v>
+ <v>Pid = pid()</v>
+ <v>Reason = term()</v>
+ </type>
+ <desc>
+ <p>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.</p>
+ </desc>
+ </func>
+
+ <func>
+ <name>write_file(RemoteFilename, LocalFilename, Options) -> {ok, LastCallbackState} | {error, Reason}</name>
+ <fsummary>Writes a (virtual) file to a TFTP server.</fsummary>
+ <type>
+ <v>RemoteFilename = string()</v>
+ <v>LocalFilename = binary() | string()</v>
+ <v>Options = [option()]</v>
+ <v>LastCallbackState = term()</v>
+ <v>Reason = term()</v>
+ </type>
+ <desc>
+ <p>Writes a (virtual) file <c>RemoteFilename</c> to a TFTP
+ server.</p>
+ <p>If <c>LocalFilename</c> is a binary, <c>tftp_binary</c> is
+ used as callback module. The binary is transferred block by
+ block and the number of transferred bytes is returned in
+ <c>LastCallbackState</c>.</p>
+ <p>If <c>LocalFilename</c> is a string and there are no
+ registered callback modules, <c>tftp_file</c> is used as
+ callback module. It reads the file named <c>LocalFilename</c>
+ block by block and returns the number of transferred bytes
+ in <c>LastCallbackState</c>.</p>
+ <p>If <c>LocalFilename</c> is a string and there are registered
+ callback modules, <c>LocalFilename</c> 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.</p>
+ </desc>
+ </func>
+ </funcs>
+
+ <section>
+ <marker id="tftp_callback"></marker>
+ <title>CALLBACK FUNCTIONS</title>
+ <p>A <c>tftp</c> callback module is to be implemented as a
+ <c>tftp</c> behavior and export the functions listed
+ in the following.</p>
+ <p>On the server side, the callback interaction starts with a call to
+ <c>open/5</c> with the registered initial callback state.
+ <c>open/5</c> is expected to open the (virtual) file. Then either
+ function <c>read/1</c> or <c>write/2</c> 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 <c>read/1</c> or
+ <c>write/2</c> is expected to close the (virtual) file
+ and return its last state. Function <c>abort/3</c> is only
+ used in error situations. Function <c>prepare/5</c> is not used on
+ the server side.</p>
+ <p>On the client side, the callback interaction is the same, but it
+ starts and ends a bit differently. It starts with a call to
+ <c>prepare/5</c> with the same arguments as <c>open/5</c> takes.
+ <c>prepare/5</c> 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 <c>open/5</c>
+ on the client side. On the client side, function <c>open/5</c>
+ 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
+ <c>read/1</c> or <c>write/2</c>, the returned state is forwarded to
+ the user and returned from <c>read_file</c>/3 or
+ <c>write_file/3</c>.</p>
+
+ <p> 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.</p>
+
+ <p>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.</p>
+
+ <marker id="prepare"></marker>
+ </section>
+
+ <funcs>
+ <func>
+ <name>Module:abort(Code, Text, State) -> ok</name>
+ <fsummary>Aborts the file transfer.</fsummary>
+ <type>
+ <v>Code = undef | enoent | eacces | enospc</v>
+ <v>&nbsp;&nbsp;| badop | eexist | baduser | badopt</v>
+ <v>&nbsp;&nbsp;| int()</v>
+ <v>Text = string()</v>
+ <v>State = term()</v>
+ </type>
+ <desc>
+ <p>Invoked when the file transfer is aborted.</p>
+ <p>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).</p>
+ </desc>
+ </func>
+
+ <func>
+ <name>Module:open(Peer, Access, Filename, Mode, SuggestedOptions, State) -> {ok, AcceptedOptions, NewState} | {error, {Code, Text}}</name>
+ <fsummary>Opens a file for read or write access.</fsummary>
+ <type>
+ <v>Peer = {PeerType, PeerHost, PeerPort}</v>
+ <v>PeerType = inet | inet6</v>
+ <v>PeerHost = ip_address()</v>
+ <v>PeerPort = integer()</v>
+ <v>Access = read | write</v>
+ <v>Filename = string()</v>
+ <v>Mode = string()</v>
+ <v>SuggestedOptions = AcceptedOptions = [{Key, Value}]</v>
+ <v>&nbsp;Key = Value = string()</v>
+ <v>State = InitialState | term()</v>
+ <v>&nbsp;InitialState = [] | [{root_dir, string()}]</v>
+ <v>NewState = term()</v>
+ <v>Code = undef | enoent | eacces | enospc</v>
+ <v>&nbsp;&nbsp;| badop | eexist | baduser | badopt</v>
+ <v>&nbsp;&nbsp;| int()</v>
+ <v>Text = string()</v>
+ </type>
+ <desc>
+ <p>Opens a file for read or write access.</p>
+ <p>On the client side, where the <c>open/5</c> call has been
+ preceded by a call to <c>prepare/5</c>, all options must be
+ accepted or rejected.</p>
+ <p>On the server side, where there is no preceding
+ <c>prepare/5</c> call, no new options can be added, but
+ those present in <c>SuggestedOptions</c> can be
+ omitted or replaced with new values in <c>AcceptedOptions</c>.</p>
+
+ <marker id="read"></marker>
+ </desc>
+ </func>
+
+ <func>
+ <name>Module:prepare(Peer, Access, Filename, Mode, SuggestedOptions, InitialState) -> {ok, AcceptedOptions, NewState} | {error, {Code, Text}}</name>
+ <fsummary>Prepares to open a file on the client side.</fsummary>
+ <type>
+ <v>Peer = {PeerType, PeerHost, PeerPort}</v>
+ <v>PeerType = inet | inet6</v>
+ <v>PeerHost = ip_address()</v>
+ <v>PeerPort = integer()</v>
+ <v>Access = read | write</v>
+ <v>Filename = string()</v>
+ <v>Mode = string()</v>
+ <v>SuggestedOptions = AcceptedOptions = [{Key, Value}]</v>
+ <v>&nbsp;Key = Value = string()</v>
+ <v>InitialState = [] | [{root_dir, string()}]</v>
+ <v>NewState = term()</v>
+ <v>Code = undef | enoent | eacces | enospc</v>
+ <v>&nbsp;&nbsp;| badop | eexist | baduser | badopt</v>
+ <v>&nbsp;&nbsp;| int()</v>
+ <v>Text = string()</v>
+ </type>
+ <desc>
+ <p>Prepares to open a file on the client side.</p>
+ <p>No new options can be added, but those present in
+ <c>SuggestedOptions</c> can be omitted or replaced with new
+ values in <c>AcceptedOptions</c>.</p>
+ <p>This is followed by a call to <c>open/4</c> before any
+ read/write access is performed. <c>AcceptedOptions</c> is
+ sent to the server, which replies with the options that it
+ accepts. These are then forwarded to <c>open/4</c> as
+ <c>SuggestedOptions</c>.</p>
+
+ <marker id="open"></marker>
+ </desc>
+ </func>
+
+ <func>
+ <name>Module:read(State) -> {more, Bin, NewState} | {last, Bin, FileSize} | {error, {Code, Text}}</name>
+ <fsummary>Reads a chunk from the file.</fsummary>
+ <type>
+ <v>State = NewState = term()</v>
+ <v>Bin = binary()</v>
+ <v>FileSize = int()</v>
+ <v>Code = undef | enoent | eacces | enospc</v>
+ <v>&nbsp;&nbsp;| badop | eexist | baduser | badopt</v>
+ <v>&nbsp;&nbsp;| int()</v>
+ <v>Text = string()</v>
+ </type>
+ <desc>
+ <p>Reads a chunk from the file.</p>
+ <p>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.</p>
+
+ <marker id="write"></marker>
+ </desc>
+ </func>
+
+ <func>
+ <name>Module:write(Bin, State) -> {more, NewState} | {last, FileSize} | {error, {Code, Text}}</name>
+ <fsummary>Writes a chunk to the file.</fsummary>
+ <type>
+ <v>Bin = binary()</v>
+ <v>State = NewState = term()</v>
+ <v>FileSize = int()</v>
+ <v>Code = undef | enoent | eacces | enospc</v>
+ <v>&nbsp;&nbsp;| badop | eexist | baduser | badopt</v>
+ <v>&nbsp;&nbsp;| int()</v>
+ <v>Text = string()</v>
+ </type>
+ <desc>
+ <p>Writes a chunk to the file.</p>
+ <p>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.</p>
+
+ <marker id="abort"></marker>
+ </desc>
+ </func>
+ </funcs>
+
+ <section>
+ <marker id="tftp_logger"></marker>
+ <title>LOGGER FUNCTIONS</title>
+
+ <p>A <c>tftp_logger</c> callback module is to be implemented as a
+ <c>tftp_logger</c> behavior and export the following functions:</p>
+
+ <marker id="error_msg"></marker>
+ </section>
+
+ <funcs>
+ <func>
+ <name>Logger:error_msg(Format, Data) -> ok | exit(Reason)</name>
+ <fsummary>Logs an error message.</fsummary>
+ <type>
+ <v>Format = string()</v>
+ <v>Data = [term()]</v>
+ <v>Reason = term()</v>
+ </type>
+ <desc>
+ <p>Logs an error message.
+ See <c>error_logger:error_msg/2</c> for details.</p>
+
+ <marker id="warning_msg"></marker>
+ </desc>
+ </func>
+
+ <func>
+ <name>Logger:info_msg(Format, Data) -> ok | exit(Reason)</name>
+ <fsummary>Logs an info message.</fsummary>
+ <type>
+ <v>Format = string()</v>
+ <v>Data = [term()]</v>
+ <v>Reason = term()</v>
+ </type>
+ <desc>
+ <p>Logs an info message.
+ See <c>error_logger:info_msg/2</c> for details.</p>
+ </desc>
+ </func>
+
+ <func>
+ <name>Logger:warning_msg(Format, Data) -> ok | exit(Reason)</name>
+ <fsummary>Logs a warning message.</fsummary>
+ <type>
+ <v>Format = string()</v>
+ <v>Data = [term()]</v>
+ <v>Reason = term()</v>
+ </type>
+ <desc>
+ <p>Logs a warning message.
+ See <c>error_logger:warning_msg/2</c> for details.</p>
+
+ <marker id="info_msg"></marker>
+ </desc>
+ </func>
+ </funcs>
+</erlref>
+
+
diff --git a/lib/tftp/doc/src/usersguide.xml b/lib/tftp/doc/src/usersguide.xml
new file mode 100644
index 0000000000..eb7f7d17c3
--- /dev/null
+++ b/lib/tftp/doc/src/usersguide.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!DOCTYPE part SYSTEM "part.dtd">
+
+<part xmlns:xi="http://www.w3.org/2001/XInclude">
+ <header>
+ <copyright>
+ <year>2004</year><year>2018</year>
+ <holder>Ericsson AB. All Rights Reserved.</holder>
+ </copyright>
+ <legalnotice>
+ 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.
+
+ </legalnotice>
+
+ <title>TFTP User's Guide</title>
+ <prepared>Péter Dimitrov</prepared>
+ <docno></docno>
+ <date>2018-03-22</date>
+ <rev>A</rev>
+ <file>part.sgml</file>
+ </header>
+ <description>
+ <p>The <c>TFTP</c> application provides a TFTP client and server.</p>
+ </description>
+ <xi:include href="introduction.xml"/>
+ <xi:include href="getting_started.xml"/>
+</part>
diff --git a/lib/tftp/ebin/.gitignore b/lib/tftp/ebin/.gitignore
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/lib/tftp/ebin/.gitignore
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..602ae845a4
--- /dev/null
+++ b/lib/tftp/src/Makefile
@@ -0,0 +1,110 @@
+#
+# %CopyrightBegin%
+#
+# Copyright Ericsson AB 2005-2018. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# %CopyrightEnd%
+#
+#
+
+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..2a87d39ada
--- /dev/null
+++ b/lib/tftp/src/tftp.app.src
@@ -0,0 +1,22 @@
+{application, tftp,
+ [{description, "TFTP application"},
+ {vsn, "1.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..31e4c651e8
--- /dev/null
+++ b/lib/tftp/src/tftp.erl
@@ -0,0 +1,409 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2005-2018. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+%%
+%%
+
+%%%-------------------------------------------------------------------
+%%% File : tftp.erl
+%%% Author : Hakan Mattsson <[email protected]>
+%%% Description : Trivial FTP
+%%% Created : 18 May 2004 by Hakan Mattsson <[email protected]>
+%%%-------------------------------------------------------------------
+%%%
+%%% This is a complete implementation of the following IETF standards:
+%%%
+%%% RFC 1350, The TFTP Protocol (revision 2).
+%%% RFC 2347, TFTP Option Extension.
+%%% RFC 2348, TFTP Blocksize Option.
+%%% RFC 2349, TFTP Timeout Interval and Transfer Size Options.
+%%%
+%%% The only feature that not is implemented in this release is
+%%% the "netascii" transfer mode.
+%%%
+%%% The start/1 function starts a daemon process which, listens for
+%%% UDP packets on a port. When it receives a request for read or
+%%% write it spawns a temporary server process which handles the
+%%% actual transfer of the file. On the client side the read_file/3
+%%% and write_file/3 functions spawns a temporary client process which
+%%% establishes contact with a TFTP daemon and performs the actual
+%%% transfer of the file.
+%%%
+%%% Most of the options are common for both the client and the server
+%%% side, but some of them differs a little. Here are the available
+%%% options:
+%%%
+%%% {debug, Level}
+%%%
+%%% Level = none | error | warning brief | normal | verbose | all
+%%%
+%%% Controls the level of debug printouts. The default is none.
+%%%
+%%% {host, Host}
+%%%
+%%% The name or IP address of the host where the TFTP daemon
+%%% resides. This option is only used by the client. See
+%%% 'inet' about valid host names.
+%%%
+%%% {port, Port}
+%%%
+%%% Port = integer()
+%%%
+%%% The TFTP port where the daemon listens. It defaults to the
+%%% standardized number 69. On the server side it may sometimes
+%%% make sense to set it to 0, which means that the daemon just
+%%% will pick a free port (which is returned by the start/1
+%%% function).
+%%%
+%%% If a socket has somehow already has been connected, the
+%%% {udp, [{fd, integer()}]} option can be used to pass the
+%%% open file descriptor to gen_udp. This can be automated
+%%% a bit by using a command line argument stating the
+%%% prebound file descriptor number. For example, if the
+%%% Port is 69 and the file descriptor 22 has been opened by
+%%% setuid_socket_wrap. Then the command line argument
+%%% "-tftpd_69 22" will trigger the prebound file
+%%% descriptor 22 to be used instead of opening port 69.
+%%% The UDP option {udp, [{fd, 22}]} autmatically be added.
+%%% See init:get_argument/ about command line arguments and
+%%% gen_udp:open/2 about UDP options.
+%%%
+%%% {port_policy, Policy}
+%%%
+%%% Policy = random | Port | {range, MinPort, MaxPort}
+%%% Port = MinPort = MaxPort = integer()
+%%%
+%%% Policy for the selection of the temporary port which is used
+%%% by the server/client during the file transfer. It defaults to
+%%% 'random' which is the standardized policy. With this policy a
+%%% randomized free port used. A single port or a range of ports
+%%% can be useful if the protocol should pass thru a firewall.
+%%%
+%%% {prebound_fd, InitArgFlag}
+%%%
+%%% InitArgFlag = atom()
+%%%
+%%% If a socket has somehow already has been connected, the
+%%% {udp, [{fd, integer()}]} option can be used to pass the
+%%% open file descriptor to gen_udp.
+%%%
+%%% The prebound_fd option makes it possible to pass give the
+%%% file descriptor as a command line argument. The typical
+%%% usage is when used in conjunction with setuid_socket_wrap
+%%% to be able to open privileged sockets. For example if the
+%%% file descriptor 22 has been opened by setuid_socket_wrap
+%%% and you have choosen my_tftp_fd as init argument, the
+%%% command line should like this "erl -my_tftp_fd 22" and
+%%% FileDesc should be set to my_tftpd_fd. This would
+%%% automatically imply {fd, 22} to be set as UDP option.
+%%%
+%%% {udp, UdpOptions}
+%%%
+%%% Options to gen_udp:open/2.
+%%%
+%%% {use_tsize, Bool}
+%%%
+%%% Bool = boolean()
+%%%
+%%% Flag for automated usage of the "tsize" option. With this set
+%%% to true, the write_file/3 client will determine the filesize
+%%% and send it to the server as the standardized "tsize" option.
+%%% A read_file/3 client will just acquire filesize from the
+%%% server by sending a zero "tsize".
+%%%
+%%% {max_tsize, MaxTsize}
+%%%
+%%% MaxTsize = integer() | infinity
+%%%
+%%% Threshold for the maximal filesize in bytes. The transfer will
+%%% be aborted if the limit is exceeded. It defaults to
+%%% 'infinity'.
+%%%
+%%% {max_conn, MaxConn}
+%%%
+%%% MaxConn = integer() | infinity
+%%%
+%%% Threshold for the maximal number of active connections. The
+%%% daemon will reject the setup of new connections if the limit
+%%% is exceeded. It defaults to 'infinity'.
+%%%
+%%% {TftpKey, TftpVal}
+%%%
+%%% TftpKey = string()
+%%% TftpVal = string()
+%%%
+%%% The name and value of a TFTP option.
+%%%
+%%% {reject, Feature}
+%%%
+%%% Feature = Mode | TftpKey
+%%% Mode = read | write
+%%% TftpKey = string()
+%%%
+%%% Control which features that should be rejected.
+%%% This is mostly useful for the server as it may restrict
+%%% usage of certain TFTP options or read/write access.
+%%%
+%%% {callback, {RegExp, Module, State}}
+%%%
+%%% RegExp = string()
+%%% Module = atom()
+%%% State = term()
+%%%
+%%% Registration of a callback module. When a file is to be
+%%% transferred, its local filename will be matched to the
+%%% regular expressions of the registered callbacks. The first
+%%% matching callback will be used the during the transfer.The
+%%% callback module must implement the 'tftp' behaviour.
+%%%
+%%% On the server side the callback interaction starts with a
+%%% call to open/5 with the registered initial callback
+%%% state. open/5 is expected to open the (virtual) file. Then
+%%% either the read/1 or write/2 functions are invoked
+%%% repeatedly, once per transfererred block. At each function
+%%% call the state returned from the previous call is
+%%% obtained. When the last block has been encountered the read/1
+%%% or write/2 functions is expected to close the (virtual)
+%%% file.and return its last state. The abort/3 function is only
+%%% used in error situations. prepare/5 is not used on the server
+%%% side.
+%%%
+%%% On the client side the callback interaction is the same, but
+%%% it starts and ends a bit differently. It starts with a call
+%%% to prepare/5 with the same arguments as open/5
+%%% takes. prepare/5 is expected to validate the TFTP options,
+%%% suggested by the user and return the subset of them that it
+%%% accepts. Then the options is sent to the server which will
+%%% perform the same TFTP option negotiation procedure. The
+%%% options that are accepted by the server is forwarded to the
+%%% open/5 function on the client side. On the client side the
+%%% open/5 function must accept all option as is or reject the
+%%% transfer. Then the callback interaction follows the same
+%%% pattern as described above for the server side. When the last
+%%% block is encountered in read/1 or write/2 the returned stated
+%%% is forwarded to the user and returned from read_file/3 or
+%%% write_file/3.
+%%%-------------------------------------------------------------------
+
+-module(tftp).
+
+%%-------------------------------------------------------------------
+%% Interface
+%%-------------------------------------------------------------------
+
+%% Public functions
+-export([
+ read_file/3,
+ write_file/3,
+ start/1,
+ info/1,
+ change_config/2,
+ start/0,
+ stop/0
+ ]).
+
+%% Application local functions
+-export([
+ start_standalone/1,
+ start_service/1,
+ stop_service/1,
+ services/0,
+ service_info/1
+ ]).
+
+
+-type peer() :: {PeerType :: inet | inet6,
+ PeerHost :: inet:ip_address(),
+ PeerPort :: port()}.
+
+-type access() :: read | write.
+
+-type options() :: [{Key :: string(), Value :: string()}].
+
+-type error_code() :: undef | enoent | eacces | enospc |
+ badop | eexist | baduser | badopt |
+ integer().
+
+-callback prepare(Peer :: peer(),
+ Access :: access(),
+ Filename :: file:name(),
+ Mode :: string(),
+ SuggestedOptions :: options(),
+ InitialState :: [] | [{root_dir, string()}]) ->
+ {ok, AcceptedOptions :: options(), NewState :: term()} |
+ {error, {Code :: error_code(), string()}}.
+
+-callback open(Peer :: peer(),
+ Access :: access(),
+ Filename :: file:name(),
+ Mode :: string(),
+ SuggestedOptions :: options(),
+ State :: [] | [{root_dir, string()}] | term()) ->
+ {ok, AcceptedOptions :: options(), NewState :: term()} |
+ {error, {Code :: error_code(), string()}}.
+
+-callback read(State :: term()) -> {more, binary(), NewState :: term()} |
+ {last, binary(), integer()} |
+ {error, {Code :: error_code(), string()}}.
+
+-callback write(binary(), State :: term()) ->
+ {more, NewState :: term()} |
+ {last, FileSize :: integer()} |
+ {error, {Code :: error_code(), string()}}.
+
+-callback abort(Code :: error_code(), string(), State :: term()) -> 'ok'.
+
+-include("tftp.hrl").
+
+
+%%-------------------------------------------------------------------
+%% read_file(RemoteFilename, LocalFilename, Options) ->
+%% {ok, LastCallbackState} | {error, Reason}
+%%
+%% RemoteFilename = string()
+%% LocalFilename = binary | string()
+%% Options = [option()]
+%% LastCallbackState = term()
+%% Reason = term()
+%%
+%% Reads a (virtual) file from a TFTP server
+%%
+%% If LocalFilename is the atom 'binary', tftp_binary will be used as
+%% callback module. It will concatenate all transferred blocks and
+%% return them as one single binary in the CallbackState.
+%%
+%% When LocalFilename is a string, it will be matched to the
+%% registered callback modules and hopefully one of them will be
+%% selected. By default, tftp_file will be used as callback module. It
+%% will write each transferred block to the file named
+%% LocalFilename. The number of transferred bytes will be returned as
+%% LastCallbackState.
+%%-------------------------------------------------------------------
+
+read_file(RemoteFilename, LocalFilename, Options) ->
+ tftp_engine:client_start(read, RemoteFilename, LocalFilename, Options).
+
+%%-------------------------------------------------------------------
+%% write(RemoteFilename, LocalFilename, Options) ->
+%% {ok, LastCallbackState} | {error, Reason}
+%%
+%% RemoteFilename = string()
+%% LocalFilename = binary() | string()
+%% Options = [option()]
+%% LastCallbackState = term()
+%% Reason = term()
+%%
+%% Writes a (virtual) file to a TFTP server
+%%
+%% If LocalFilename is a binary, tftp_binary will be used as callback
+%% module. The binary will be transferred block by block and the number
+%% of transferred bytes will be returned as LastCallbackState.
+%%
+%% When LocalFilename is a string, it will be matched to the
+%% registered callback modules and hopefully one of them will be
+%% selected. By default, tftp_file will be used as callback module. It
+%% will read the file named LocalFilename block by block. The number
+%% of transferred bytes will be returned as LastCallbackState.
+%%-------------------------------------------------------------------
+
+write_file(RemoteFilename, LocalFilename, Options) ->
+ tftp_engine:client_start(write, RemoteFilename, LocalFilename, Options).
+
+%%-------------------------------------------------------------------
+%% start(Options) -> {ok, Pid} | {error, Reason}
+%%
+%% Options = [option()]
+%% Pid = pid()
+%% Reason = term()
+%%
+%% Starts a daemon process which listens for udp packets on a
+%% port. When it receives a request for read or write it spawns
+%% a temporary server process which handles the actual transfer
+%% of the (virtual) file.
+%%-------------------------------------------------------------------
+
+start(Options) ->
+ tftp_engine:daemon_start(Options).
+
+%%-------------------------------------------------------------------
+%% info(Pid) -> {ok, Options} | {error, Reason}
+%%
+%% Options = [option()]
+%% Reason = term()
+%%
+%% Returns info about a tftp daemon, server or client process
+%%-------------------------------------------------------------------
+
+info(Pid) ->
+ tftp_engine:info(Pid).
+
+%%-------------------------------------------------------------------
+%% change_config(Pid, Options) -> ok | {error, Reason}
+%%
+%% Options = [option()]
+%% Reason = term()
+%%
+%% Changes config for a tftp daemon, server or client process
+%% Must be used with care.
+%%-------------------------------------------------------------------
+
+change_config(Pid, Options) ->
+ tftp_engine:change_config(Pid, Options).
+
+%%-------------------------------------------------------------------
+%% start() -> ok | {error, Reason}
+%%
+%% Reason = term()
+%%
+%% Start the application
+%%-------------------------------------------------------------------
+
+start() ->
+ application:start(tftp).
+
+%%-------------------------------------------------------------------
+%% stop() -> ok | {error, Reason}
+%%
+%% Reason = term()
+%%
+%% Stop the application
+%%-------------------------------------------------------------------
+stop() ->
+ application:stop(tftp).
+
+%%-------------------------------------------------------------------
+%% Inets service behavior
+%%-------------------------------------------------------------------
+
+start_standalone(Options) ->
+ start(Options).
+
+start_service(Options) ->
+ tftp_sup:start_child(Options).
+
+stop_service(Pid) ->
+ tftp_sup:stop_child(Pid).
+
+services() ->
+ tftp_sup:which_children().
+
+service_info(Pid) ->
+ info(Pid).
+
+
+
diff --git a/lib/tftp/src/tftp.hrl b/lib/tftp/src/tftp.hrl
new file mode 100644
index 0000000000..a26aeee389
--- /dev/null
+++ b/lib/tftp/src/tftp.hrl
@@ -0,0 +1,69 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2005-2018. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+%%
+%%
+
+%%%-------------------------------------------------------------------
+%%% 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..80d54c6cbe
--- /dev/null
+++ b/lib/tftp/src/tftp_app.erl
@@ -0,0 +1,56 @@
+%%
+%% %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) ->
+ Config = get_configuration(),
+ tftp_sup:start_link(Config).
+
+%%--------------------------------------------------------------------
+stop(_State) ->
+ ok.
+
+%%====================================================================
+%% Internal functions
+%%====================================================================
+
+get_configuration() ->
+ case (catch application:get_env(tftp, services)) of
+ {ok, Services} ->
+ Services;
+ _ ->
+ []
+ end.
diff --git a/lib/tftp/src/tftp_binary.erl b/lib/tftp/src/tftp_binary.erl
new file mode 100644
index 0000000000..3438ba235b
--- /dev/null
+++ b/lib/tftp/src/tftp_binary.erl
@@ -0,0 +1,239 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2005-2018. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+%%
+%%
+
+%%%-------------------------------------------------------------------
+%%% File : tft_binary.erl
+%%% Author : Hakan Mattsson <[email protected]>
+%%% Description :
+%%%
+%%% Created : 24 May 2004 by Hakan Mattsson <[email protected]>
+%%%-------------------------------------------------------------------
+
+-module(tftp_binary).
+
+%%%-------------------------------------------------------------------
+%%% Interface
+%%%-------------------------------------------------------------------
+
+-behaviour(tftp).
+
+-export([prepare/6, open/6, read/1, write/2, abort/3]).
+
+-record(read_state, {options, blksize, bin, is_native_ascii, is_network_ascii, count}).
+-record(write_state, {options, blksize, list, is_native_ascii, is_network_ascii}).
+
+%%-------------------------------------------------------------------
+%% Prepare
+%%-------------------------------------------------------------------
+
+prepare(_Peer, Access, Filename, Mode, SuggestedOptions, Initial) when is_list(Initial) ->
+ %% Client side
+ IsNativeAscii = is_native_ascii(Initial),
+ case catch handle_options(Access, Filename, Mode, SuggestedOptions, IsNativeAscii) of
+ {ok, IsNetworkAscii, AcceptedOptions} when Access =:= read, is_binary(Filename) ->
+ State = #read_state{options = AcceptedOptions,
+ blksize = lookup_blksize(AcceptedOptions),
+ bin = Filename,
+ is_network_ascii = IsNetworkAscii,
+ count = size(Filename),
+ is_native_ascii = IsNativeAscii},
+ {ok, AcceptedOptions, State};
+ {ok, IsNetworkAscii, AcceptedOptions} when Access =:= write, Filename =:= binary ->
+ State = #write_state{options = AcceptedOptions,
+ blksize = lookup_blksize(AcceptedOptions),
+ list = [],
+ is_network_ascii = IsNetworkAscii,
+ is_native_ascii = IsNativeAscii},
+ {ok, AcceptedOptions, State};
+ {ok, _, _} ->
+ {error, {undef, "Illegal callback usage. Mode and filename is incompatible."}};
+ {error, {Code, Text}} ->
+ {error, {Code, Text}}
+ end;
+prepare(_Peer, _Access, _Bin, _Mode, _SuggestedOptions, _Initial) ->
+ {error, {undef, "Illegal callback options."}}.
+
+%%-------------------------------------------------------------------
+%% Open
+%%-------------------------------------------------------------------
+
+open(Peer, Access, Filename, Mode, SuggestedOptions, Initial) when is_list(Initial) ->
+ %% Server side
+ case prepare(Peer, Access, Filename, Mode, SuggestedOptions, Initial) of
+ {ok, AcceptedOptions, State} ->
+ open(Peer, Access, Filename, Mode, AcceptedOptions, State);
+ {error, {Code, Text}} ->
+ {error, {Code, Text}}
+ end;
+open(_Peer, Access, Filename, Mode, NegotiatedOptions, State) when is_record(State, read_state) ->
+ %% Both sides
+ case catch handle_options(Access, Filename, Mode, NegotiatedOptions, State#read_state.is_native_ascii) of
+ {ok, IsNetworkAscii, Options}
+ when Options =:= NegotiatedOptions,
+ IsNetworkAscii =:= State#read_state.is_network_ascii ->
+ {ok, NegotiatedOptions, State};
+ {error, {Code, Text}} ->
+ {error, {Code, Text}}
+ end;
+open(_Peer, Access, Filename, Mode, NegotiatedOptions, State) when is_record(State, write_state) ->
+ %% Both sides
+ case catch handle_options(Access, Filename, Mode, NegotiatedOptions, State#write_state.is_native_ascii) of
+ {ok, IsNetworkAscii, Options}
+ when Options =:= NegotiatedOptions,
+ IsNetworkAscii =:= State#write_state.is_network_ascii ->
+ {ok, NegotiatedOptions, State};
+ {error, {Code, Text}} ->
+ {error, {Code, Text}}
+ end;
+open(Peer, Access, Filename, Mode, NegotiatedOptions, State) ->
+ %% Handle upgrade from old releases. Please, remove this clause in next release.
+ State2 = upgrade_state(State),
+ open(Peer, Access, Filename, Mode, NegotiatedOptions, State2).
+
+%%-------------------------------------------------------------------
+%% Read
+%%-------------------------------------------------------------------
+
+read(#read_state{bin = Bin} = State) when is_binary(Bin) ->
+ BlkSize = State#read_state.blksize,
+ if
+ size(Bin) >= BlkSize ->
+ <<Block:BlkSize/binary, Bin2/binary>> = Bin,
+ State2 = State#read_state{bin = Bin2},
+ {more, Block, State2};
+ size(Bin) < BlkSize ->
+ {last, Bin, State#read_state.count}
+ end;
+read(State) ->
+ %% Handle upgrade from old releases. Please, remove this clause in next release.
+ State2 = upgrade_state(State),
+ read(State2).
+
+%%-------------------------------------------------------------------
+%% Write
+%%-------------------------------------------------------------------
+
+write(Bin, #write_state{list = List} = State) when is_binary(Bin), is_list(List) ->
+ Size = size(Bin),
+ BlkSize = State#write_state.blksize,
+ if
+ Size =:= BlkSize ->
+ {more, State#write_state{list = [Bin | List]}};
+ Size < BlkSize ->
+ Bin2 = list_to_binary(lists:reverse([Bin | List])),
+ {last, Bin2}
+ end;
+write(Bin, State) ->
+ %% Handle upgrade from old releases. Please, remove this clause in next release.
+ State2 = upgrade_state(State),
+ write(Bin, State2).
+
+%%-------------------------------------------------------------------
+%% Abort
+%%-------------------------------------------------------------------
+
+abort(_Code, _Text, #read_state{bin = Bin} = State)
+ when is_record(State, read_state), is_binary(Bin) ->
+ ok;
+abort(_Code, _Text, #write_state{list = List} = State)
+ when is_record(State, write_state), is_list(List) ->
+ ok;
+abort(Code, Text, State) ->
+ %% Handle upgrade from old releases. Please, remove this clause in next release.
+ State2 = upgrade_state(State),
+ abort(Code, Text, State2).
+
+%%-------------------------------------------------------------------
+%% Process options
+%%-------------------------------------------------------------------
+
+handle_options(Access, Bin, Mode, Options, IsNativeAscii) ->
+ IsNetworkAscii = handle_mode(Mode, IsNativeAscii),
+ Options2 = do_handle_options(Access, Bin, Options),
+ {ok, IsNetworkAscii, Options2}.
+
+handle_mode(Mode, IsNativeAscii) ->
+ case Mode of
+ "netascii" when IsNativeAscii =:= true -> true;
+ "octet" -> false;
+ _ -> throw({error, {badop, "Illegal mode " ++ Mode}})
+ end.
+
+do_handle_options(Access, Bin, [{Key, Val} | T]) ->
+ case Key of
+ "tsize" ->
+ case Access of
+ read when Val =:= "0", is_binary(Bin) ->
+ Tsize = integer_to_list(size(Bin)),
+ [{Key, Tsize} | do_handle_options(Access, Bin, T)];
+ _ ->
+ handle_integer(Access, Bin, Key, Val, T, 0, infinity)
+ end;
+ "blksize" ->
+ handle_integer(Access, Bin, Key, Val, T, 8, 65464);
+ "timeout" ->
+ handle_integer(Access, Bin, Key, Val, T, 1, 255);
+ _ ->
+ do_handle_options(Access, Bin, T)
+ end;
+do_handle_options(_Access, _Bin, []) ->
+ [].
+
+
+handle_integer(Access, Bin, Key, Val, Options, Min, Max) ->
+ case catch list_to_integer(Val) of
+ {'EXIT', _} ->
+ do_handle_options(Access, Bin, Options);
+ Int when Int >= Min, Int =< Max ->
+ [{Key, Val} | do_handle_options(Access, Bin, Options)];
+ Int when Int >= Min, Max =:= infinity ->
+ [{Key, Val} | do_handle_options(Access, Bin, Options)];
+ _Int ->
+ throw({error, {badopt, "Illegal " ++ Key ++ " value " ++ Val}})
+ end.
+
+lookup_blksize(Options) ->
+ case lists:keysearch("blksize", 1, Options) of
+ {value, {_, Val}} ->
+ list_to_integer(Val);
+ false ->
+ 512
+ end.
+
+is_native_ascii([]) ->
+ is_native_ascii();
+is_native_ascii([{native_ascii, Bool}]) ->
+ case Bool of
+ true -> true;
+ false -> false
+ end.
+
+is_native_ascii() ->
+ case os:type() of
+ {win32, _} -> true;
+ _ -> false
+ end.
+
+%% Handle upgrade from old releases. Please, remove this function in next release.
+upgrade_state({read_state, Options, Blksize, Bin, IsNetworkAscii, Count}) ->
+ {read_state, Options, Blksize, Bin, false, IsNetworkAscii, Count};
+upgrade_state({write_state, Options, Blksize, List, IsNetworkAscii}) ->
+ {write_state, Options, Blksize, List, false, IsNetworkAscii}.
diff --git a/lib/tftp/src/tftp_engine.erl b/lib/tftp/src/tftp_engine.erl
new file mode 100644
index 0000000000..78a105bcc8
--- /dev/null
+++ b/lib/tftp/src/tftp_engine.erl
@@ -0,0 +1,1422 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2005-2018. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+%%
+%%-------------------------------------------------------------------
+%% Protocol engine for trivial FTP
+%%-------------------------------------------------------------------
+
+-module(tftp_engine).
+
+%%%-------------------------------------------------------------------
+%%% Interface
+%%%-------------------------------------------------------------------
+
+%% application internal functions
+-export([
+ daemon_start/1,
+ daemon_loop/1,
+ daemon_loop/3, %% Handle upgrade from old releases. Please, remove this function in next release.
+ client_start/4,
+ common_loop/6,
+ info/1,
+ change_config/2
+ ]).
+
+%% module internal
+-export([
+ daemon_init/1,
+ server_init/2,
+ client_init/2,
+ wait_for_msg/3,
+ callback/4
+ ]).
+
+%% sys callback functions
+-export([
+ system_continue/3,
+ system_terminate/4,
+ system_code_change/4
+ ]).
+
+-include("tftp.hrl").
+
+-type prep_status() :: 'error' | 'last' | 'more' | 'terminate'.
+
+-record(daemon_state, {config, n_servers, server_tab, file_tab}).
+-record(server_info, {pid, req, peer}).
+-record(file_info, {peer_req, pid}).
+-record(sys_misc, {module, function, arguments}).
+-record(error, {where, code, text, filename}).
+-record(prepared, {status :: prep_status() | 'undefined',
+ result, block_no, next_data, prev_data}).
+-record(transfer_res, {status, decoded_msg, prepared}).
+-define(ERROR(Where, Code, Text, Filename),
+ #error{where = Where, code = Code, text = Text, filename = Filename}).
+
+%%%-------------------------------------------------------------------
+%%% Info
+%%%-------------------------------------------------------------------
+
+info(daemons) ->
+ Daemons = supervisor:which_children(tftp_sup),
+ [{Pid, info(Pid)} || {_, Pid, _, _} <- Daemons];
+info(servers) ->
+ [{Pid, info(Pid)} || {_, {ok, DeamonInfo}} <- info(daemons),
+ {server, Pid} <- DeamonInfo];
+info(ToPid) when is_pid(ToPid) ->
+ call(info, ToPid, timer:seconds(10)).
+
+change_config(daemons, Options) ->
+ Daemons = supervisor:which_children(tftp_sup),
+ [{Pid, change_config(Pid, Options)} || {_, Pid, _, _} <- Daemons];
+change_config(servers, Options) ->
+ [{Pid, change_config(Pid, Options)} || {_, {ok, DeamonInfo}} <- info(daemons),
+ {server, Pid} <- DeamonInfo];
+change_config(ToPid, Options) when is_pid(ToPid) ->
+ BadKeys = [host, port, udp],
+ BadOptions = [{Key, Val} || {Key, Val} <- Options,
+ BadKey <- BadKeys,
+ Key =:= BadKey],
+ case BadOptions of
+ [] ->
+ call({change_config, Options}, ToPid, timer:seconds(10));
+ [{Key, Val} | _] ->
+ {error, {badarg, {Key, Val}}}
+ end.
+
+call(Req, ToPid, Timeout) when is_pid(ToPid) ->
+ Type = process,
+ Ref = erlang:monitor(Type, ToPid),
+ ToPid ! {Req, Ref, self()},
+ receive
+ {Reply, Ref, FromPid} when FromPid =:= ToPid ->
+ erlang:demonitor(Ref, [flush]),
+ Reply;
+ {'DOWN', Ref, Type, FromPid, _Reason} when FromPid =:= ToPid ->
+ {error, timeout}
+ after Timeout ->
+ {error, timeout}
+ end.
+
+reply(Reply, Ref, ToPid) ->
+ ToPid ! {Reply, Ref, self()}.
+
+%%%-------------------------------------------------------------------
+%%% Daemon
+%%%-------------------------------------------------------------------
+
+%% Returns {ok, Port}
+daemon_start(Options) when is_list(Options) ->
+ Config = tftp_lib:parse_config(Options),
+ proc_lib:start_link(?MODULE, daemon_init, [Config], infinity).
+
+daemon_init(Config) when is_record(Config, config),
+ is_pid(Config#config.parent_pid) ->
+ process_flag(trap_exit, true),
+ {Port, UdpOptions} = prepare_daemon_udp(Config),
+ case catch gen_udp:open(Port, UdpOptions) of
+ {ok, Socket} ->
+ {ok, ActualPort} = inet:port(Socket),
+ proc_lib:init_ack({ok, self()}),
+ Config2 = Config#config{udp_socket = Socket,
+ udp_port = ActualPort},
+ print_debug_info(Config2, daemon, open, #tftp_msg_req{filename = ""}),
+ ServerTab = ets:new(tftp_daemon_servers, [{keypos, 2}]),
+ FileTab = ets:new(tftp_daemon_files, [{keypos, 2}]),
+ State = #daemon_state{config = Config2,
+ n_servers = 0,
+ server_tab = ServerTab,
+ file_tab = FileTab},
+ daemon_loop(State);
+ {error, Reason} ->
+ Text = lists:flatten(io_lib:format("UDP open ~p -> ~p", [UdpOptions, Reason])),
+ print_debug_info(Config, daemon, open, ?ERROR(open, undef, Text, "")),
+ exit({gen_udp_open, UdpOptions, Reason});
+ Reason ->
+ Text = lists:flatten(io_lib:format("UDP open ~p -> ~p", [UdpOptions, Reason])),
+ print_debug_info(Config, daemon, open, ?ERROR(open, undef, Text, "")),
+ exit({gen_udp_open, UdpOptions, Reason})
+ end.
+
+prepare_daemon_udp(#config{udp_port = Port, udp_options = UdpOptions} = Config) ->
+ case lists:keymember(fd, 1, UdpOptions) of
+ true ->
+ %% Use explicit fd
+ {Port, UdpOptions};
+ false ->
+ %% Use fd from setuid_socket_wrap, such as -tftpd_69
+ InitArg = list_to_atom("tftpd_" ++ integer_to_list(Port)),
+ case init:get_argument(InitArg) of
+ {ok, [[FdStr]] = Badarg} when is_list(FdStr) ->
+ case catch list_to_integer(FdStr) of
+ Fd when is_integer(Fd) ->
+ {0, [{fd, Fd} | lists:keydelete(ip, 1, UdpOptions)]};
+ {'EXIT', _} ->
+ Text = lists:flatten(io_lib:format("Illegal prebound fd ~p: ~p", [InitArg, Badarg])),
+ print_debug_info(Config, daemon, open, ?ERROR(open, undef, Text, "")),
+ exit({badarg, {prebound_fd, InitArg, Badarg}})
+ end;
+ {ok, Badarg} ->
+ Text = lists:flatten(io_lib:format("Illegal prebound fd ~p: ~p", [InitArg, Badarg])),
+ print_debug_info(Config, daemon, open, ?ERROR(open, undef, Text, "")),
+ exit({badarg, {prebound_fd, InitArg, Badarg}});
+ error ->
+ {Port, UdpOptions}
+ end
+ end.
+
+daemon_loop(DaemonConfig, N, Servers) when is_list(Servers) ->
+ %% Handle upgrade from old releases. Please, remove this function in next release.
+ ServerTab = ets:new(tftp_daemon_servers, [{keypos, 2}]),
+ FileTab = ets:new(tftp_daemon_files, [{keypos, 2}]),
+ State = #daemon_state{config = DaemonConfig,
+ n_servers = N,
+ server_tab = ServerTab,
+ file_tab = FileTab},
+ Req = #tftp_msg_req{filename = dummy},
+ [ets:insert(ServerTab, #server_info{pid = Pid, req = Req, peer = dummy}) || Pid <- Servers],
+ daemon_loop(State).
+
+daemon_loop(#daemon_state{config = DaemonConfig,
+ n_servers = N,
+ server_tab = ServerTab,
+ file_tab = FileTab} = State) when is_record(DaemonConfig, config) ->
+ %% info_msg(DaemonConfig, "=====> TFTP: Daemon #~p\n", [N]), %% XXX
+ receive
+ {info, Ref, FromPid} when is_pid(FromPid) ->
+ Fun = fun(#server_info{pid = Pid}, Acc) -> [{server, Pid} | Acc] end,
+ ServerInfo = ets:foldl(Fun, [], ServerTab),
+ Info = internal_info(DaemonConfig, daemon) ++ [{n_conn, N}] ++ ServerInfo,
+ _ = reply({ok, Info}, Ref, FromPid),
+ ?MODULE:daemon_loop(State);
+ {{change_config, Options}, Ref, FromPid} when is_pid(FromPid) ->
+ case catch tftp_lib:parse_config(Options, DaemonConfig) of
+ {'EXIT', Reason} ->
+ _ = reply({error, Reason}, Ref, FromPid),
+ ?MODULE:daemon_loop(State);
+ DaemonConfig2 when is_record(DaemonConfig2, config) ->
+ _ = reply(ok, Ref, FromPid),
+ ?MODULE:daemon_loop(State#daemon_state{config = DaemonConfig2})
+ end;
+ {udp, Socket, RemoteHost, RemotePort, Bin} when is_binary(Bin) ->
+ _ = inet:setopts(Socket, [{active, once}]),
+ ServerConfig = DaemonConfig#config{parent_pid = self(),
+ udp_host = RemoteHost,
+ udp_port = RemotePort},
+ Msg = (catch tftp_lib:decode_msg(Bin)),
+ print_debug_info(ServerConfig, daemon, recv, Msg),
+ case Msg of
+ Req when is_record(Req, tftp_msg_req),
+ N =< DaemonConfig#config.max_conn ->
+ Peer = peer_info(ServerConfig),
+ PeerReq = {Peer, Req},
+ PeerInfo = lists:flatten(io_lib:format("~p", [Peer])),
+ case ets:lookup(FileTab, PeerReq) of
+ [] ->
+ Args = [ServerConfig, Req],
+ Pid = proc_lib:spawn_link(?MODULE, server_init, Args),
+ ets:insert(ServerTab, #server_info{pid = Pid, req = Req, peer = Peer}),
+ ets:insert(FileTab, #file_info{peer_req = PeerReq, pid = Pid}),
+ ?MODULE:daemon_loop(State#daemon_state{n_servers = N + 1});
+ [#file_info{pid = Pid}] ->
+ %% Yet another request of the file from same peer
+ warning_msg(DaemonConfig, "~p Reuse connection for ~s\n\t~p\n",
+ [Pid, PeerInfo, Req#tftp_msg_req.filename]),
+ ?MODULE:daemon_loop(State)
+ end;
+ Req when is_record(Req, tftp_msg_req) ->
+ Reply = #tftp_msg_error{code = enospc, text = "Too many connections"},
+ Peer = peer_info(ServerConfig),
+ PeerInfo = lists:flatten(io_lib:format("~p", [Peer])),
+ warning_msg(DaemonConfig,
+ "Daemon has too many connections (~p)."
+ "\n\tRejecting request from ~s\n",
+ [N, PeerInfo]),
+ send_msg(ServerConfig, daemon, Reply),
+ ?MODULE:daemon_loop(State);
+ {'EXIT', Reply} when is_record(Reply, tftp_msg_error) ->
+ send_msg(ServerConfig, daemon, Reply),
+ ?MODULE:daemon_loop(State);
+ Req ->
+ Reply = #tftp_msg_error{code = badop,
+ text = "Illegal TFTP operation"},
+ warning_msg(DaemonConfig, "Daemon received: ~p.\n\tfrom ~p:~p",
+ [Req, RemoteHost, RemotePort]),
+ send_msg(ServerConfig, daemon, Reply),
+ ?MODULE:daemon_loop(State)
+ end;
+ {system, From, Msg} ->
+ Misc = #sys_misc{module = ?MODULE, function = daemon_loop, arguments = [State]},
+ sys:handle_system_msg(Msg, From, DaemonConfig#config.parent_pid, ?MODULE, [], Misc);
+ {'EXIT', Pid, Reason} when DaemonConfig#config.parent_pid =:= Pid ->
+ close_port(DaemonConfig, daemon, #tftp_msg_req{filename = ""}),
+ exit(Reason);
+ {'EXIT', Pid, _Reason} = Info ->
+ case ets:lookup(ServerTab, Pid) of
+ [] ->
+ warning_msg(DaemonConfig, "Daemon received: ~p", [Info]),
+ ?MODULE:daemon_loop(State);
+ [#server_info{req = Req, peer = Peer}] ->
+ PeerReq = {Peer, Req},
+ ets:delete(FileTab, PeerReq),
+ ets:delete(ServerTab, Pid),
+ ?MODULE:daemon_loop(State#daemon_state{n_servers = N - 1})
+ end;
+ Info ->
+ warning_msg(DaemonConfig, "Daemon received: ~p", [Info]),
+ ?MODULE:daemon_loop(State)
+ end;
+daemon_loop(#daemon_state{config = Config} = State) ->
+ %% Handle upgrade from old releases. Please, remove this clause in next release.
+ Config2 = upgrade_config(Config),
+ daemon_loop(State#daemon_state{config = Config2}).
+
+upgrade_config({config, ParentPid, UdpSocket, UdpOptions, UdpHost, UdpPort, PortPolicy,
+ UseTsize, MaxTsize, MaxConn, Rejected, PoliteAck, DebugLevel,
+ Timeout, UserOptions, Callbacks}) ->
+ Callbacks2 = tftp_lib:add_default_callbacks(Callbacks),
+ Logger = tftp_logger,
+ MaxRetries = 5,
+ {config, ParentPid, UdpSocket, UdpOptions, UdpHost, UdpPort, PortPolicy,
+ UseTsize, MaxTsize, MaxConn, Rejected, PoliteAck, DebugLevel,
+ Timeout, UserOptions, Callbacks2, Logger, MaxRetries}.
+
+%%%-------------------------------------------------------------------
+%%% Server
+%%%-------------------------------------------------------------------
+
+server_init(Config, Req) when is_record(Config, config),
+ is_pid(Config#config.parent_pid),
+ is_record(Req, tftp_msg_req) ->
+ process_flag(trap_exit, true),
+ %% Config =
+ %% case os:getenv("TFTPDEBUG") of
+ %% false ->
+ %% Config0;
+ %% DebugLevel ->
+ %% Config0#config{debug_level = list_to_atom(DebugLevel)}
+ %% end,
+ SuggestedOptions = Req#tftp_msg_req.options,
+ UdpOptions = Config#config.udp_options,
+ UdpOptions2 = lists:keydelete(fd, 1, UdpOptions),
+ Config1 = Config#config{udp_options = UdpOptions2},
+ Config2 = tftp_lib:parse_config(SuggestedOptions, Config1),
+ SuggestedOptions2 = Config2#config.user_options,
+ Req2 = Req#tftp_msg_req{options = SuggestedOptions2},
+ case open_free_port(Config2, server, Req2) of
+ {ok, Config3} ->
+ Filename = Req#tftp_msg_req.filename,
+ case match_callback(Filename, Config3#config.callbacks) of
+ {ok, Callback} ->
+ print_debug_info(Config3, server, match, Callback),
+ case pre_verify_options(Config3, Req2) of
+ ok ->
+ case callback({open, server_open}, Config3, Callback, Req2) of
+ {Callback2, {ok, AcceptedOptions}} ->
+ {LocalAccess, _} = local_file_access(Req2),
+ OptText = "Internal error. Not allowed to add new options.",
+ case post_verify_options(Config3, Req2, AcceptedOptions, OptText) of
+ {ok, Config4, Req3} when AcceptedOptions =/= [] ->
+ Reply = #tftp_msg_oack{options = AcceptedOptions},
+ BlockNo =
+ case LocalAccess of
+ read -> 0;
+ write -> 1
+ end,
+ {Config5, Callback3, TransferRes} =
+ transfer(Config4, Callback2, Req3, Reply, LocalAccess, BlockNo, #prepared{}),
+ common_loop(Config5, Callback3, Req3, TransferRes, LocalAccess, BlockNo);
+ {ok, Config4, Req3} when LocalAccess =:= write ->
+ BlockNo = 0,
+ common_ack(Config4, Callback2, Req3, LocalAccess, BlockNo, #prepared{});
+ {ok, Config4, Req3} when LocalAccess =:= read ->
+ BlockNo = 0,
+ common_read(Config4, Callback2, Req3, LocalAccess, BlockNo, BlockNo, #prepared{});
+ {error, {Code, Text}} ->
+ {undefined, Error} =
+ callback({abort, {Code, Text}}, Config3, Callback2, Req2),
+ send_msg(Config3, Req, Error),
+ terminate(Config3, Req2, ?ERROR(post_verify_options, Code, Text, Req2#tftp_msg_req.filename))
+ end;
+ {undefined, #tftp_msg_error{code = Code, text = Text} = Error} ->
+ send_msg(Config3, Req, Error),
+ terminate(Config3, Req, ?ERROR(server_open, Code, Text, Req2#tftp_msg_req.filename))
+ end;
+ {error, {Code, Text}} ->
+ {undefined, Error} =
+ callback({abort, {Code, Text}}, Config2, Callback, Req2),
+ send_msg(Config2, Req, Error),
+ terminate(Config2, Req2, ?ERROR(pre_verify_options, Code, Text, Req2#tftp_msg_req.filename))
+ end;
+ {error, #tftp_msg_error{code = Code, text = Text} = Error} ->
+ send_msg(Config3, Req, Error),
+ terminate(Config3, Req, ?ERROR(match_callback, Code, Text, Req2#tftp_msg_req.filename))
+ end;
+ #error{} = Error ->
+ terminate(Config2, Req, Error)
+ end;
+server_init(Config, Req) when is_record(Req, tftp_msg_req) ->
+ Config2 = upgrade_config(Config),
+ server_init(Config2, Req).
+
+%%%-------------------------------------------------------------------
+%%% Client
+%%%-------------------------------------------------------------------
+
+%% LocalFilename = filename() | 'binary' | binary()
+%% Returns {ok, LastCallbackState} | {error, Reason}
+client_start(Access, RemoteFilename, LocalFilename, Options) ->
+ Config = tftp_lib:parse_config(Options),
+ Config2 = Config#config{parent_pid = self(),
+ udp_socket = undefined},
+ Req = #tftp_msg_req{access = Access,
+ filename = RemoteFilename,
+ mode = lookup_mode(Config2#config.user_options),
+ options = Config2#config.user_options,
+ local_filename = LocalFilename},
+ Args = [Config2, Req],
+ case proc_lib:start_link(?MODULE, client_init, Args, infinity) of
+ {ok, LastCallbackState} ->
+ {ok, LastCallbackState};
+ {error, Error} ->
+ {error, Error}
+ end.
+
+client_init(Config, Req) when is_record(Config, config),
+ is_pid(Config#config.parent_pid),
+ is_record(Req, tftp_msg_req) ->
+ process_flag(trap_exit, true),
+ %% Config =
+ %% case os:getenv("TFTPDEBUG") of
+ %% false ->
+ %% Config0;
+ %% "none" ->
+ %% Config0;
+ %% DebugLevel ->
+ %% info_msg(Config, "TFTPDEBUG: ~s\n", [DebugLevel]),
+ %% Config0#config{debug_level = list_to_atom(DebugLevel)}
+ %% end,
+ case open_free_port(Config, client, Req) of
+ {ok, Config2} ->
+ Req2 =
+ case Config2#config.use_tsize of
+ true ->
+ SuggestedOptions = Req#tftp_msg_req.options,
+ SuggestedOptions2 = tftp_lib:replace_val("tsize", "0", SuggestedOptions),
+ Req#tftp_msg_req{options = SuggestedOptions2};
+ false ->
+ Req
+ end,
+ LocalFilename = Req2#tftp_msg_req.local_filename,
+ case match_callback(LocalFilename, Config2#config.callbacks) of
+ {ok, Callback} ->
+ print_debug_info(Config2, client, match, Callback),
+ client_prepare(Config2, Callback, Req2);
+ {error, #tftp_msg_error{code = Code, text = Text}} ->
+ terminate(Config, Req, ?ERROR(match, Code, Text, Req#tftp_msg_req.filename))
+ end;
+ #error{} = Error ->
+ terminate(Config, Req, Error)
+ end.
+
+client_prepare(Config, Callback, Req) when is_record(Req, tftp_msg_req) ->
+ case pre_verify_options(Config, Req) of
+ ok ->
+ case callback({open, client_prepare}, Config, Callback, Req) of
+ {Callback2, {ok, AcceptedOptions}} ->
+ OptText = "Internal error. Not allowed to add new options.",
+ case post_verify_options(Config, Req, AcceptedOptions, OptText) of
+ {ok, Config2, Req2} ->
+ {LocalAccess, _} = local_file_access(Req2),
+ BlockNo = 0,
+ {Config3, Callback3, TransferRes} =
+ transfer(Config2, Callback2, Req2, Req2, LocalAccess, BlockNo, #prepared{}),
+ client_open(Config3, Callback3, Req2, BlockNo, TransferRes);
+ {error, {Code, Text}} ->
+ _ = callback({abort, {Code, Text}}, Config, Callback2, Req),
+ terminate(Config, Req, ?ERROR(post_verify_options, Code, Text, Req#tftp_msg_req.filename))
+ end;
+ {undefined, #tftp_msg_error{code = Code, text = Text}} ->
+ terminate(Config, Req, ?ERROR(client_prepare, Code, Text, Req#tftp_msg_req.filename))
+ end;
+ {error, {Code, Text}} ->
+ _ = callback({abort, {Code, Text}}, Config, Callback, Req),
+ terminate(Config, Req, ?ERROR(pre_verify_options, Code, Text, Req#tftp_msg_req.filename))
+ end.
+
+client_open(Config, Callback, Req, BlockNo, #transfer_res{status = Status, decoded_msg = DecodedMsg, prepared = Prepared}) ->
+ {LocalAccess, _} = local_file_access(Req),
+ case Status of
+ ok when is_record(Prepared, prepared) ->
+ case DecodedMsg of
+ Msg when is_record(Msg, tftp_msg_oack) ->
+ ServerOptions = Msg#tftp_msg_oack.options,
+ OptText = "Protocol violation. Server is not allowed new options",
+ case post_verify_options(Config, Req, ServerOptions, OptText) of
+ {ok, Config2, Req2} ->
+ {Config3, Callback2, Req3} =
+ do_client_open(Config2, Callback, Req2),
+ case LocalAccess of
+ read ->
+ common_read(Config3, Callback2, Req3, LocalAccess, BlockNo, BlockNo, Prepared);
+ write ->
+ common_ack(Config3, Callback2, Req3, LocalAccess, BlockNo, Prepared)
+ end;
+ {error, {Code, Text}} ->
+ {undefined, Error} =
+ callback({abort, {Code, Text}}, Config, Callback, Req),
+ send_msg(Config, Req, Error),
+ terminate(Config, Req, ?ERROR(verify_server_options, Code, Text, Req#tftp_msg_req.filename))
+ end;
+ #tftp_msg_ack{block_no = ActualBlockNo} when LocalAccess =:= read ->
+ Req2 = Req#tftp_msg_req{options = []},
+ {Config2, Callback2, Req2} = do_client_open(Config, Callback, Req2),
+ ExpectedBlockNo = 0,
+ common_read(Config2, Callback2, Req2, LocalAccess, ExpectedBlockNo, ActualBlockNo, Prepared);
+ #tftp_msg_data{block_no = ActualBlockNo, data = Data} when LocalAccess =:= write ->
+ Req2 = Req#tftp_msg_req{options = []},
+ {Config2, Callback2, Req2} = do_client_open(Config, Callback, Req2),
+ ExpectedBlockNo = 1,
+ common_write(Config2, Callback2, Req2, LocalAccess, ExpectedBlockNo, ActualBlockNo, Data, Prepared);
+ %% #tftp_msg_error{code = Code, text = Text} when Req#tftp_msg_req.options =/= [] ->
+ %% %% Retry without options
+ %% callback({abort, {Code, Text}}, Config, Callback, Req),
+ %% Req2 = Req#tftp_msg_req{options = []},
+ %% client_prepare(Config, Callback, Req2);
+ #tftp_msg_error{code = Code, text = Text} ->
+ _ = callback({abort, {Code, Text}}, Config, Callback, Req),
+ terminate(Config, Req, ?ERROR(client_open, Code, Text, Req#tftp_msg_req.filename));
+ {'EXIT', #tftp_msg_error{code = Code, text = Text}} ->
+ _ = callback({abort, {Code, Text}}, Config, Callback, Req),
+ terminate(Config, Req, ?ERROR(client_open, Code, Text, Req#tftp_msg_req.filename));
+ Msg when is_tuple(Msg) ->
+ Code = badop,
+ Text = "Illegal TFTP operation",
+ {undefined, Error} =
+ callback({abort, {Code, Text}}, Config, Callback, Req),
+ send_msg(Config, Req, Error),
+ Text2 = lists:flatten([Text, ". ", io_lib:format("~p", [element(1, Msg)])]),
+ terminate(Config, Req, ?ERROR(client_open, Code, Text2, Req#tftp_msg_req.filename))
+ end;
+ error when is_record(Prepared, tftp_msg_error) ->
+ #tftp_msg_error{code = Code, text = Text} = Prepared,
+ _ = callback({abort, {Code, Text}}, Config, Callback, Req),
+ terminate(Config, Req, ?ERROR(client_open, Code, Text, Req#tftp_msg_req.filename))
+ end.
+
+do_client_open(Config, Callback, Req) ->
+ case callback({open, client_open}, Config, Callback, Req) of
+ {Callback2, {ok, FinalOptions}} ->
+ OptText = "Internal error. Not allowed to change options.",
+ case post_verify_options(Config, Req, FinalOptions, OptText) of
+ {ok, Config2, Req2} ->
+ {Config2, Callback2, Req2};
+ {error, {Code, Text}} ->
+ {undefined, Error} =
+ callback({abort, {Code, Text}}, Config, Callback2, Req),
+ send_msg(Config, Req, Error),
+ terminate(Config, Req, ?ERROR(post_verify_options, Code, Text, Req#tftp_msg_req.filename))
+ end;
+ {undefined, #tftp_msg_error{code = Code, text = Text} = Error} ->
+ send_msg(Config, Req, Error),
+ terminate(Config, Req, ?ERROR(client_open, Code, Text, Req#tftp_msg_req.filename))
+ end.
+
+%%%-------------------------------------------------------------------
+%%% Common loop for both client and server
+%%%-------------------------------------------------------------------
+
+common_loop(Config, Callback, Req, #transfer_res{status = Status, decoded_msg = DecodedMsg, prepared = Prepared}, LocalAccess, ExpectedBlockNo)
+ when is_record(Config, config)->
+ %% Config =
+ %% case os:getenv("TFTPMAX") of
+ %% false ->
+ %% Config0;
+ %% MaxBlockNoStr when Config0#config.debug_level =/= none ->
+ %% case list_to_integer(MaxBlockNoStr) of
+ %% MaxBlockNo when ExpectedBlockNo > MaxBlockNo ->
+ %% info_msg(Config, "TFTPMAX: ~p\n", [MaxBlockNo]),
+ %% info_msg(Config, "TFTPDEBUG: none\n", []),
+ %% Config0#config{debug_level = none};
+ %% _ ->
+ %% Config0
+ %% end;
+ %% _MaxBlockNoStr ->
+ %% Config0
+ %% end,
+ case Status of
+ ok when is_record(Prepared, prepared) ->
+ case DecodedMsg of
+ #tftp_msg_ack{block_no = ActualBlockNo} when LocalAccess =:= read ->
+ common_read(Config, Callback, Req, LocalAccess, ExpectedBlockNo, ActualBlockNo, Prepared);
+ #tftp_msg_data{block_no = ActualBlockNo, data = Data} when LocalAccess =:= write ->
+ common_write(Config, Callback, Req, LocalAccess, ExpectedBlockNo, ActualBlockNo, Data, Prepared);
+ #tftp_msg_error{code = Code, text = Text} ->
+ _ = callback({abort, {Code, Text}}, Config, Callback, Req),
+ terminate(Config, Req, ?ERROR(common_loop, Code, Text, Req#tftp_msg_req.filename));
+ {'EXIT', #tftp_msg_error{code = Code, text = Text} = Error} ->
+ _ = callback({abort, {Code, Text}}, Config, Callback, Req),
+ send_msg(Config, Req, Error),
+ terminate(Config, Req, ?ERROR(common_loop, Code, Text, Req#tftp_msg_req.filename));
+ Msg when is_tuple(Msg) ->
+ Code = badop,
+ Text = "Illegal TFTP operation",
+ {undefined, Error} =
+ callback({abort, {Code, Text}}, Config, Callback, Req),
+ send_msg(Config, Req, Error),
+ Text2 = lists:flatten([Text, ". ", io_lib:format("~p", [element(1, Msg)])]),
+ terminate(Config, Req, ?ERROR(common_loop, Code, Text2, Req#tftp_msg_req.filename))
+ end;
+ error when is_record(Prepared, tftp_msg_error) ->
+ #tftp_msg_error{code = Code, text = Text} = Prepared,
+ send_msg(Config, Req, Prepared),
+ terminate(Config, Req, ?ERROR(transfer, Code, Text, Req#tftp_msg_req.filename))
+ end;
+common_loop(Config, Callback, Req, TransferRes, LocalAccess, ExpectedBlockNo) ->
+ %% Handle upgrade from old releases. Please, remove this clause in next release.
+ Config2 = upgrade_config(Config),
+ common_loop(Config2, Callback, Req, TransferRes, LocalAccess, ExpectedBlockNo).
+
+-spec common_read(#config{}, #callback{}, _, 'read', _, _, #prepared{}) -> no_return().
+
+common_read(Config, _, Req, _, _, _, #prepared{status = terminate, result = Result}) ->
+ terminate(Config, Req, {ok, Result});
+common_read(Config, Callback, Req, LocalAccess, ExpectedBlockNo, ActualBlockNo, Prepared)
+ when ActualBlockNo =:= ExpectedBlockNo, is_record(Prepared, prepared) ->
+ case early_read(Config, Callback, Req, LocalAccess, ActualBlockNo, Prepared) of
+ {Callback2, #prepared{status = more, next_data = Data} = Prepared2} when is_binary(Data) ->
+ Prepared3 = Prepared2#prepared{prev_data = Data, next_data = undefined},
+ do_common_read(Config, Callback2, Req, LocalAccess, ActualBlockNo, Data, Prepared3);
+ {undefined, #prepared{status = last, next_data = Data} = Prepared2} when is_binary(Data) ->
+ Prepared3 = Prepared2#prepared{status = terminate},
+ do_common_read(Config, undefined, Req, LocalAccess, ActualBlockNo, Data, Prepared3);
+ {undefined, #prepared{status = error, result = Error}} ->
+ #tftp_msg_error{code = Code, text = Text} = Error,
+ send_msg(Config, Req, Error),
+ terminate(Config, Req, ?ERROR(read, Code, Text, Req#tftp_msg_req.filename))
+ end;
+common_read(Config, Callback, Req, LocalAccess, ExpectedBlockNo, ActualBlockNo, Prepared)
+ when ActualBlockNo =:= (ExpectedBlockNo - 1), is_record(Prepared, prepared) ->
+ case Prepared of
+ #prepared{status = more, prev_data = Data} when is_binary(Data) ->
+ do_common_read(Config, Callback, Req, LocalAccess, ActualBlockNo, Data, Prepared);
+ #prepared{status = last, prev_data = Data} when is_binary(Data) ->
+ do_common_read(Config, Callback, Req, LocalAccess, ActualBlockNo, Data, Prepared);
+ #prepared{status = error, result = Error} ->
+ #tftp_msg_error{code = Code, text = Text} = Error,
+ send_msg(Config, Req, Error),
+ terminate(Config, Req, ?ERROR(read, Code, Text, Req#tftp_msg_req.filename))
+ end;
+common_read(Config, Callback, Req, LocalAccess, ExpectedBlockNo, ActualBlockNo, Prepared)
+ when ActualBlockNo =< ExpectedBlockNo, is_record(Prepared, prepared) ->
+ %% error_logger:error_msg("TFTP READ ~s: Expected block ~p but got block ~p - IGNORED\n",
+ %% [Req#tftp_msg_req.filename, ExpectedBlockNo, ActualBlockNo]),
+ case Prepared of
+ #prepared{status = more, prev_data = Data} when is_binary(Data) ->
+ Reply = #tftp_msg_data{block_no = ExpectedBlockNo, data = Data},
+ {Config2, Callback2, TransferRes} =
+ wait_for_msg_and_handle_timeout(Config, Callback, Req, Reply, LocalAccess, ExpectedBlockNo, Prepared),
+ ?MODULE:common_loop(Config2, Callback2, Req, TransferRes, LocalAccess, ExpectedBlockNo);
+ #prepared{status = last, prev_data = Data} when is_binary(Data) ->
+ Reply = #tftp_msg_data{block_no = ExpectedBlockNo, data = Data},
+ {Config2, Callback2, TransferRes} =
+ wait_for_msg_and_handle_timeout(Config, Callback, Req, Reply, LocalAccess, ExpectedBlockNo, Prepared),
+ ?MODULE:common_loop(Config2, Callback2, Req, TransferRes, LocalAccess, ExpectedBlockNo);
+ #prepared{status = error, result = Error} ->
+ #tftp_msg_error{code = Code, text = Text} = Error,
+ send_msg(Config, Req, Error),
+ terminate(Config, Req, ?ERROR(read, Code, Text, Req#tftp_msg_req.filename))
+ end;
+common_read(Config, Callback, Req, _LocalAccess, ExpectedBlockNo, ActualBlockNo, Prepared)
+ when is_record(Prepared, prepared) ->
+ Code = badblk,
+ Text = "Unknown transfer ID = " ++
+ integer_to_list(ActualBlockNo) ++ " (" ++ integer_to_list(ExpectedBlockNo) ++ ")",
+ {undefined, Error} =
+ callback({abort, {Code, Text}}, Config, Callback, Req),
+ send_msg(Config, Req, Error),
+ terminate(Config, Req, ?ERROR(read, Code, Text, Req#tftp_msg_req.filename)).
+
+-spec do_common_read(#config{}, #callback{} | undefined, _, 'read', integer(), binary(), #prepared{}) -> no_return().
+
+do_common_read(Config, Callback, Req, LocalAccess, BlockNo, Data, Prepared)
+ when is_binary(Data), is_record(Prepared, prepared) ->
+ NextBlockNo = (BlockNo + 1) rem 65536,
+ Reply = #tftp_msg_data{block_no = NextBlockNo, data = Data},
+ {Config2, Callback2, TransferRes} =
+ transfer(Config, Callback, Req, Reply, LocalAccess, NextBlockNo, Prepared),
+ ?MODULE:common_loop(Config2, Callback2, Req, TransferRes, LocalAccess, NextBlockNo).
+
+-spec common_write(#config{}, #callback{}, _, 'write', integer(), integer(), _, #prepared{}) -> no_return().
+
+common_write(Config, _, Req, _, _, _, _, #prepared{status = terminate, result = Result}) ->
+ terminate(Config, Req, {ok, Result});
+common_write(Config, Callback, Req, LocalAccess, ExpectedBlockNo, ActualBlockNo, Data, Prepared)
+ when ActualBlockNo =:= ExpectedBlockNo, is_binary(Data), is_record(Prepared, prepared) ->
+ case callback({write, Data}, Config, Callback, Req) of
+ {Callback2, #prepared{status = more} = Prepared2} ->
+ common_ack(Config, Callback2, Req, LocalAccess, ActualBlockNo, Prepared2);
+ {undefined, #prepared{status = last, result = Result} = Prepared2} ->
+ Config2 = pre_terminate(Config, Req, {ok, Result}),
+ Prepared3 = Prepared2#prepared{status = terminate},
+ common_ack(Config2, undefined, Req, LocalAccess, ActualBlockNo, Prepared3);
+ {undefined, #prepared{status = error, result = Error}} ->
+ #tftp_msg_error{code = Code, text = Text} = Error,
+ send_msg(Config, Req, Error),
+ terminate(Config, Req, ?ERROR(write, Code, Text, Req#tftp_msg_req.filename))
+ end;
+common_write(Config, Callback, Req, LocalAccess, ExpectedBlockNo, ActualBlockNo, Data, Prepared)
+ when ActualBlockNo =:= (ExpectedBlockNo - 1), is_binary(Data), is_record(Prepared, prepared) ->
+ common_ack(Config, Callback, Req, LocalAccess, ExpectedBlockNo - 1, Prepared);
+common_write(Config, Callback, Req, LocalAccess, ExpectedBlockNo, ActualBlockNo, Data, Prepared)
+ when ActualBlockNo =< ExpectedBlockNo, is_binary(Data), is_record(Prepared, prepared) ->
+ %% error_logger:error_msg("TFTP WRITE ~s: Expected block ~p but got block ~p - IGNORED\n",
+ %% [Req#tftp_msg_req.filename, ExpectedBlockNo, ActualBlockNo]),
+ Reply = #tftp_msg_ack{block_no = ExpectedBlockNo},
+ {Config2, Callback2, TransferRes} =
+ wait_for_msg_and_handle_timeout(Config, Callback, Req, Reply, LocalAccess, ExpectedBlockNo, Prepared),
+ ?MODULE:common_loop(Config2, Callback2, Req, TransferRes, LocalAccess, ExpectedBlockNo);
+common_write(Config, Callback, Req, _, ExpectedBlockNo, ActualBlockNo, Data, Prepared)
+ when is_binary(Data), is_record(Prepared, prepared) ->
+ Code = badblk,
+ Text = "Unknown transfer ID = " ++
+ integer_to_list(ActualBlockNo) ++ " (" ++ integer_to_list(ExpectedBlockNo) ++ ")",
+ {undefined, Error} =
+ callback({abort, {Code, Text}}, Config, Callback, Req),
+ send_msg(Config, Req, Error),
+ terminate(Config, Req, ?ERROR(write, Code, Text, Req#tftp_msg_req.filename)).
+
+common_ack(Config, Callback, Req, LocalAccess, BlockNo, Prepared)
+ when is_record(Prepared, prepared) ->
+ Reply = #tftp_msg_ack{block_no = BlockNo},
+ NextBlockNo = (BlockNo + 1) rem 65536,
+ {Config2, Callback2, TransferRes} =
+ transfer(Config, Callback, Req, Reply, LocalAccess, NextBlockNo, Prepared),
+ ?MODULE:common_loop(Config2, Callback2, Req, TransferRes, LocalAccess, NextBlockNo).
+
+pre_terminate(Config, Req, Result) ->
+ if
+ Req#tftp_msg_req.local_filename =/= undefined,
+ Config#config.parent_pid =/= undefined ->
+ proc_lib:init_ack(Result),
+ unlink(Config#config.parent_pid),
+ Config#config{parent_pid = undefined, polite_ack = true};
+ true ->
+ Config#config{polite_ack = true}
+ end.
+
+-spec terminate(#config{}, #tftp_msg_req{}, {'ok', _} | #error{}) -> no_return().
+
+terminate(Config, Req, Result) ->
+ Result2 =
+ case Result of
+ {ok, _} ->
+ Result;
+ #error{where = Where, code = Code, text = Text} = Error ->
+ print_debug_info(Config, Req, Where, Error#error{filename = Req#tftp_msg_req.filename}),
+ {error, {Where, Code, Text}}
+ end,
+ if
+ Config#config.parent_pid =:= undefined ->
+ close_port(Config, client, Req),
+ exit(normal);
+ Req#tftp_msg_req.local_filename =/= undefined ->
+ %% Client
+ close_port(Config, client, Req),
+ proc_lib:init_ack(Result2),
+ unlink(Config#config.parent_pid),
+ exit(normal);
+ true ->
+ %% Server
+ close_port(Config, server, Req),
+ exit(shutdown)
+ end.
+
+close_port(Config, Who, Req) when is_record(Req, tftp_msg_req) ->
+ case Config#config.udp_socket of
+ undefined ->
+ ignore;
+ Socket ->
+ print_debug_info(Config, Who, close, Req),
+ gen_udp:close(Socket)
+ end.
+
+open_free_port(Config, Who, Req) when is_record(Config, config), is_record(Req, tftp_msg_req) ->
+ UdpOptions = Config#config.udp_options,
+ case Config#config.port_policy of
+ random ->
+ %% BUGBUG: Should be a random port
+ case catch gen_udp:open(0, UdpOptions) of
+ {ok, Socket} ->
+ Config2 = Config#config{udp_socket = Socket},
+ print_debug_info(Config2, Who, open, Req),
+ {ok, Config2};
+ {error, Reason} ->
+ Text = lists:flatten(io_lib:format("UDP open ~p -> ~p", [[0 | UdpOptions], Reason])),
+ ?ERROR(open, undef, Text, Req#tftp_msg_req.filename);
+ {'EXIT', _} = Reason ->
+ Text = lists:flatten(io_lib:format("UDP open ~p -> ~p", [[0 | UdpOptions], Reason])),
+ ?ERROR(open, undef, Text, Req#tftp_msg_req.filename)
+ end;
+ {range, Port, Max} when Port =< Max ->
+ case catch gen_udp:open(Port, UdpOptions) of
+ {ok, Socket} ->
+ Config2 = Config#config{udp_socket = Socket},
+ print_debug_info(Config2, Who, open, Req),
+ {ok, Config2};
+ {error, eaddrinuse} ->
+ PortPolicy = {range, Port + 1, Max},
+ Config2 = Config#config{port_policy = PortPolicy},
+ open_free_port(Config2, Who, Req);
+ {error, Reason} ->
+ Text = lists:flatten(io_lib:format("UDP open ~p -> ~p", [[Port | UdpOptions], Reason])),
+ ?ERROR(open, undef, Text, Req#tftp_msg_req.filename);
+ {'EXIT', _} = Reason->
+ Text = lists:flatten(io_lib:format("UDP open ~p -> ~p", [[Port | UdpOptions], Reason])),
+ ?ERROR(open, undef, Text, Req#tftp_msg_req.filename)
+ end;
+ {range, Port, _Max} ->
+ Reason = "Port range exhausted",
+ Text = lists:flatten(io_lib:format("UDP open ~p -> ~p", [[Port | UdpOptions], Reason])),
+ ?ERROR(Who, undef, Text, Req#tftp_msg_req.filename)
+ end.
+
+%%-------------------------------------------------------------------
+%% Transfer
+%%-------------------------------------------------------------------
+
+%% Returns {Config, Callback, #transfer_res{}}
+transfer(Config, Callback, Req, Msg, LocalAccess, NextBlockNo, Prepared)
+ when is_record(Prepared, prepared) ->
+ IoList = tftp_lib:encode_msg(Msg),
+ Retries = Config#config.max_retries + 1,
+ do_transfer(Config, Callback, Req, Msg, IoList, LocalAccess, NextBlockNo, Prepared, Retries).
+
+do_transfer(Config, Callback, Req, Msg, IoList, LocalAccess, NextBlockNo, Prepared, Retries)
+ when is_record(Prepared, prepared), is_integer(Retries), Retries >= 0 ->
+ case do_send_msg(Config, Req, Msg, IoList) of
+ ok ->
+ {Callback2, Prepared2} =
+ early_read(Config, Callback, Req, LocalAccess, NextBlockNo, Prepared),
+ do_wait_for_msg_and_handle_timeout(Config, Callback2, Req, Msg, IoList, LocalAccess, NextBlockNo, Prepared2, Retries);
+ {error, _Reason} when Retries > 0 ->
+ Retries2 = 0, % Just retry once when send fails
+ do_transfer(Config, Callback, Req, Msg, IoList, LocalAccess, NextBlockNo, Prepared, Retries2);
+ {error, Reason} ->
+ Code = undef,
+ Text = lists:flatten(io_lib:format("Transfer failed - giving up -> ~p", [Reason])),
+ Error = #tftp_msg_error{code = Code, text = Text},
+ {Config, Callback, #transfer_res{status = error, prepared = Error}}
+ end.
+
+wait_for_msg_and_handle_timeout(Config, Callback, Req, Msg, LocalAccess, NextBlockNo, Prepared) ->
+ IoList = tftp_lib:encode_msg(Msg),
+ Retries = Config#config.max_retries + 1,
+ do_wait_for_msg_and_handle_timeout(Config, Callback, Req, Msg, IoList, LocalAccess, NextBlockNo, Prepared, Retries).
+
+do_wait_for_msg_and_handle_timeout(Config, Callback, Req, Msg, IoList, LocalAccess, NextBlockNo, Prepared, Retries) ->
+ Code = undef,
+ Text = "Transfer timed out.",
+ case wait_for_msg(Config, Callback, Req) of
+ timeout when Config#config.polite_ack =:= true ->
+ do_send_msg(Config, Req, Msg, IoList),
+ case Prepared of
+ #prepared{status = terminate, result = Result} ->
+ terminate(Config, Req, {ok, Result});
+ #prepared{} ->
+ terminate(Config, Req, ?ERROR(transfer, Code, Text, Req#tftp_msg_req.filename))
+ end;
+ timeout when Retries > 0 ->
+ Retries2 = Retries - 1,
+ do_transfer(Config, Callback, Req, Msg, IoList, LocalAccess, NextBlockNo, Prepared, Retries2);
+ timeout ->
+ Error = #tftp_msg_error{code = Code, text = Text},
+ {Config, Callback, #transfer_res{status = error, prepared = Error}};
+ {Config2, DecodedMsg} ->
+ {Config2, Callback, #transfer_res{status = ok, decoded_msg = DecodedMsg, prepared = Prepared}}
+ end.
+
+send_msg(Config, Req, Msg) ->
+ case catch tftp_lib:encode_msg(Msg) of
+ {'EXIT', Reason} ->
+ Code = undef,
+ Text = "Internal error. Encode failed",
+ Msg2 = #tftp_msg_error{code = Code, text = Text, details = Reason},
+ send_msg(Config, Req, Msg2);
+ IoList ->
+ do_send_msg(Config, Req, Msg, IoList)
+ end.
+
+do_send_msg(#config{udp_socket = Socket, udp_host = RemoteHost, udp_port = RemotePort} = Config, Req, Msg, IoList) ->
+ %% {ok, LocalPort} = inet:port(Socket),
+ %% if
+ %% LocalPort =/= ?TFTP_DEFAULT_PORT ->
+ %% ok;
+ %% true ->
+ %% print_debug_info(Config#config{debug_level = all}, Req, send, Msg),
+ %% error(Config,
+ %% "Daemon replies from the default port (~p)\n\t to ~p:~p\n\t¨~p\n",
+ %% [LocalPort, RemoteHost, RemotePort, Msg])
+ %% end,
+
+ print_debug_info(Config, Req, send, Msg),
+
+ %% case os:getenv("TFTPDUMP") of
+ %% false ->
+ %% ignore;
+ %% DumpPath ->
+ %% trace_udp_send(Req, Msg, IoList, DumpPath)
+ %% end,
+ Res = gen_udp:send(Socket, RemoteHost, RemotePort, IoList),
+ case Res of
+ ok ->
+ ok;
+ {error, einval = Reason} ->
+ error_msg(Config,
+ "Stacktrace; ~p\n gen_udp:send(~p, ~p, ~p, ~p) -> ~p\n",
+ [erlang:get_stacktrace(), Socket, RemoteHost, RemotePort, IoList, {error, Reason}]);
+ {error, Reason} ->
+ {error, Reason}
+ end.
+
+%% trace_udp_send(#tftp_msg_req{filename = [$/ | RelFile]} = Req, Msg, IoList, DumpPath) ->
+%% trace_udp_send(Req#tftp_msg_req{filename = RelFile}, Msg, IoList, DumpPath);
+%% trace_udp_send(#tftp_msg_req{filename = RelFile},
+%% #tftp_msg_data{block_no = BlockNo, data = Data},
+%% _IoList,
+%% DumpPath) ->
+%% File = filename:join([DumpPath, RelFile, "block" ++ string:right(integer_to_list(BlockNo), 5, $0) ++ ".dump"]),
+%% if
+%% (BlockNo rem 1000) =:= 1 ->
+%% info_msg(Config, "TFTPDUMP: Data ~s\n", [File]);
+%% true ->
+%% ignore
+%% end,
+%% ok = filelib:ensure_dir(File),
+%% ok = file:write_file(File, Data);
+%% trace_udp_send(#tftp_msg_req{filename = RelFile}, Msg, _IoList, _DumpPath) ->
+%% info_msg(Config, "TFTPDUMP: No data ~s -> ~p\n", [RelFile, element(1, Msg)]).
+
+wait_for_msg(Config, Callback, Req) ->
+ receive
+ {udp, Socket, RemoteHost, RemotePort, Bin}
+ when is_binary(Bin), Callback#callback.block_no =:= undefined ->
+ %% Client prepare
+ _ = inet:setopts(Socket, [{active, once}]),
+ Config2 = Config#config{udp_host = RemoteHost,
+ udp_port = RemotePort},
+ DecodedMsg = (catch tftp_lib:decode_msg(Bin)),
+ print_debug_info(Config2, Req, recv, DecodedMsg),
+ {Config2, DecodedMsg};
+ {udp, Socket, Host, Port, Bin} when is_binary(Bin),
+ Config#config.udp_host =:= Host,
+ Config#config.udp_port =:= Port ->
+ _ = inet:setopts(Socket, [{active, once}]),
+ DecodedMsg = (catch tftp_lib:decode_msg(Bin)),
+ print_debug_info(Config, Req, recv, DecodedMsg),
+ {Config, DecodedMsg};
+ {info, Ref, FromPid} when is_pid(FromPid) ->
+ Type =
+ case Req#tftp_msg_req.local_filename =/= undefined of
+ true -> client;
+ false -> server
+ end,
+ Info = internal_info(Config, Type),
+ _ = reply({ok, Info}, Ref, FromPid),
+ wait_for_msg(Config, Callback, Req);
+ {{change_config, Options}, Ref, FromPid} when is_pid(FromPid) ->
+ case catch tftp_lib:parse_config(Options, Config) of
+ {'EXIT', Reason} ->
+ _ = reply({error, Reason}, Ref, FromPid),
+ wait_for_msg(Config, Callback, Req);
+ Config2 when is_record(Config2, config) ->
+ _ = reply(ok, Ref, FromPid),
+ wait_for_msg(Config2, Callback, Req)
+ end;
+ {system, From, Msg} ->
+ Misc = #sys_misc{module = ?MODULE, function = wait_for_msg, arguments = [Config, Callback, Req]},
+ sys:handle_system_msg(Msg, From, Config#config.parent_pid, ?MODULE, [], Misc);
+ {'EXIT', Pid, _Reason} when Config#config.parent_pid =:= Pid ->
+ Code = undef,
+ Text = "Parent exited.",
+ terminate(Config, Req, ?ERROR(wait_for_msg, Code, Text, Req#tftp_msg_req.filename));
+ Msg when Req#tftp_msg_req.local_filename =/= undefined ->
+ warning_msg(Config, "Client received : ~p", [Msg]),
+ wait_for_msg(Config, Callback, Req);
+ Msg when Req#tftp_msg_req.local_filename =:= undefined ->
+ warning_msg(Config, "Server received : ~p", [Msg]),
+ wait_for_msg(Config, Callback, Req)
+ after Config#config.timeout * 1000 ->
+ print_debug_info(Config, Req, recv, timeout),
+ timeout
+ end.
+
+early_read(Config, Callback, Req, LocalAccess, _NextBlockNo,
+ #prepared{status = Status, next_data = NextData, prev_data = PrevData} = Prepared) ->
+ if
+ Status =/= terminate,
+ LocalAccess =:= read,
+ Callback#callback.block_no =/= undefined,
+ NextData =:= undefined ->
+ case callback(read, Config, Callback, Req) of
+ {undefined, Error} when is_record(Error, tftp_msg_error) ->
+ {undefined, Error};
+ {Callback2, Prepared2} when is_record(Prepared2, prepared)->
+ {Callback2, Prepared2#prepared{prev_data = PrevData}}
+ end;
+ true ->
+ {Callback, Prepared}
+ end.
+
+%%-------------------------------------------------------------------
+%% Callback
+%%-------------------------------------------------------------------
+
+callback(Access, Config, Callback, Req) ->
+ {Callback2, Result} =
+ do_callback(Access, Config, Callback, Req),
+ print_debug_info(Config, Req, call, {Callback2, Result}),
+ {Callback2, Result}.
+
+do_callback(read = Fun, Config, Callback, Req)
+ when is_record(Config, config),
+ is_record(Callback, callback),
+ is_record(Req, tftp_msg_req) ->
+ Args = [Callback#callback.state],
+ NextBlockNo = Callback#callback.block_no + 1,
+ case catch safe_apply(Callback#callback.module, Fun, Args) of
+ {more, Bin, NewState} when is_binary(Bin) ->
+ Count = Callback#callback.count + size(Bin),
+ Callback2 = Callback#callback{state = NewState,
+ block_no = NextBlockNo,
+ count = Count},
+ Prepared = #prepared{status = more,
+ result = undefined,
+ block_no = NextBlockNo,
+ next_data = Bin},
+ verify_count(Config, Callback2, Req, Prepared);
+ {last, Bin, Result} when is_binary(Bin) ->
+ Prepared = #prepared{status = last,
+ result = Result,
+ block_no = NextBlockNo,
+ next_data = Bin},
+ {undefined, Prepared};
+ {error, {Code, Text}} ->
+ Error = #tftp_msg_error{code = Code, text = Text},
+ Prepared = #prepared{status = error,
+ result = Error},
+ {undefined, Prepared};
+ Illegal ->
+ Code = undef,
+ Text = "Internal error. File handler error.",
+ callback({abort, {Code, Text, Illegal}}, Config, Callback, Req)
+ end;
+do_callback({write = Fun, Bin}, Config, Callback, Req)
+ when is_record(Config, config),
+ is_record(Callback, callback),
+ is_record(Req, tftp_msg_req),
+ is_binary(Bin) ->
+ Args = [Bin, Callback#callback.state],
+ NextBlockNo = Callback#callback.block_no + 1,
+ case catch safe_apply(Callback#callback.module, Fun, Args) of
+ {more, NewState} ->
+ Count = Callback#callback.count + size(Bin),
+ Callback2 = Callback#callback{state = NewState,
+ block_no = NextBlockNo,
+ count = Count},
+ Prepared = #prepared{status = more,
+ block_no = NextBlockNo},
+ verify_count(Config, Callback2, Req, Prepared);
+ {last, Result} ->
+ Prepared = #prepared{status = last,
+ result = Result,
+ block_no = NextBlockNo},
+ {undefined, Prepared};
+ {error, {Code, Text}} ->
+ Error = #tftp_msg_error{code = Code, text = Text},
+ Prepared = #prepared{status = error,
+ result = Error},
+ {undefined, Prepared};
+ Illegal ->
+ Code = undef,
+ Text = "Internal error. File handler error.",
+ callback({abort, {Code, Text, Illegal}}, Config, Callback, Req)
+ end;
+do_callback({open, Type}, Config, Callback, Req)
+ when is_record(Config, config),
+ is_record(Callback, callback),
+ is_record(Req, tftp_msg_req) ->
+ {Access, Filename} = local_file_access(Req),
+ {Fun, BlockNo} =
+ case Type of
+ client_prepare -> {prepare, undefined};
+ client_open -> {open, 0};
+ server_open -> {open, 0}
+ end,
+ Mod = Callback#callback.module,
+ Args = [Access,
+ Filename,
+ Req#tftp_msg_req.mode,
+ Req#tftp_msg_req.options,
+ Callback#callback.state],
+ PeerInfo = peer_info(Config),
+ _ = fast_ensure_loaded(Mod),
+ Args2 =
+ case erlang:function_exported(Mod, Fun, length(Args)) of
+ true -> Args;
+ false -> [PeerInfo | Args]
+ end,
+ case catch safe_apply(Mod, Fun, Args2) of
+ {ok, AcceptedOptions, NewState} ->
+ Callback2 = Callback#callback{state = NewState,
+ block_no = BlockNo,
+ count = 0},
+ {Callback2, {ok, AcceptedOptions}};
+ {error, {Code, Text}} ->
+ {undefined, #tftp_msg_error{code = Code, text = Text}};
+ Illegal ->
+ Code = undef,
+ Text = "Internal error. File handler error.",
+ callback({abort, {Code, Text, Illegal}}, Config, Callback, Req)
+ end;
+do_callback({abort, {Code, Text}}, Config, Callback, Req) ->
+ Error = #tftp_msg_error{code = Code, text = Text},
+ do_callback({abort, Error}, Config, Callback, Req);
+do_callback({abort, {Code, Text, Details}}, Config, Callback, Req) ->
+ Error = #tftp_msg_error{code = Code, text = Text, details = Details},
+ do_callback({abort, Error}, Config, Callback, Req);
+do_callback({abort = Fun, #tftp_msg_error{code = Code, text = Text} = Error}, Config, Callback, Req)
+ when is_record(Config, config),
+ is_record(Callback, callback),
+ is_record(Req, tftp_msg_req) ->
+ Args = [Code, Text, Callback#callback.state],
+ catch safe_apply(Callback#callback.module, Fun, Args),
+ {undefined, Error};
+do_callback({abort, Error}, _Config, undefined, _Req) when is_record(Error, tftp_msg_error) ->
+ {undefined, Error}.
+
+peer_info(#config{udp_host = Host, udp_port = Port}) ->
+ if
+ is_tuple(Host), size(Host) =:= 4 ->
+ {inet, tftp_lib:host_to_string(Host), Port};
+ is_tuple(Host), size(Host) =:= 8 ->
+ {inet6, tftp_lib:host_to_string(Host), Port};
+ true ->
+ {undefined, Host, Port}
+ end.
+
+match_callback(Filename, Callbacks) ->
+ if
+ Filename =:= binary ->
+ lookup_callback_mod(tftp_binary, Callbacks);
+ is_binary(Filename) ->
+ lookup_callback_mod(tftp_binary, Callbacks);
+ true ->
+ do_match_callback(Filename, Callbacks)
+ end.
+
+do_match_callback(Filename, [C | Tail]) when is_record(C, callback) ->
+ case catch re:run(Filename, C#callback.internal, [{capture, none}]) of
+ match ->
+ {ok, C};
+ nomatch ->
+ do_match_callback(Filename, Tail);
+ Details ->
+ Code = baduser,
+ Text = "Internal error. File handler not found",
+ {error, #tftp_msg_error{code = Code, text = Text, details = Details}}
+ end;
+do_match_callback(Filename, []) ->
+ Code = baduser,
+ Text = "Internal error. File handler not found",
+ {error, #tftp_msg_error{code = Code, text = Text, details = Filename}}.
+
+lookup_callback_mod(Mod, Callbacks) ->
+ {value, C} = lists:keysearch(Mod, #callback.module, Callbacks),
+ {ok, C}.
+
+verify_count(Config, Callback, Req, Result) ->
+ case Config#config.max_tsize of
+ infinity ->
+ {Callback, Result};
+ Max when Callback#callback.count =< Max ->
+ {Callback, Result};
+ _Max ->
+ Code = enospc,
+ Text = "Too large file.",
+ callback({abort, {Code, Text}}, Config, Callback, Req)
+ end.
+
+%%-------------------------------------------------------------------
+%% Miscellaneous
+%%-------------------------------------------------------------------
+
+internal_info(Config, Type) when is_record(Config, config) ->
+ {ok, ActualPort} = inet:port(Config#config.udp_socket),
+ [
+ {type, Type},
+ {host, tftp_lib:host_to_string(Config#config.udp_host)},
+ {port, Config#config.udp_port},
+ {local_port, ActualPort},
+ {port_policy, Config#config.port_policy},
+ {udp, Config#config.udp_options},
+ {use_tsize, Config#config.use_tsize},
+ {max_tsize, Config#config.max_tsize},
+ {max_conn, Config#config.max_conn},
+ {rejected, Config#config.rejected},
+ {timeout, Config#config.timeout},
+ {polite_ack, Config#config.polite_ack},
+ {debug, Config#config.debug_level},
+ {parent_pid, Config#config.parent_pid}
+ ] ++ Config#config.user_options ++ Config#config.callbacks.
+
+local_file_access(#tftp_msg_req{access = Access,
+ local_filename = Local,
+ filename = Filename}) ->
+ case Local =:= undefined of
+ true ->
+ %% Server side
+ {Access, Filename};
+ false ->
+ %% Client side
+ case Access of
+ read -> {write, Local};
+ write -> {read, Local}
+ end
+ end.
+
+pre_verify_options(Config, Req) ->
+ Options = Req#tftp_msg_req.options,
+ case catch verify_reject(Config, Req, Options) of
+ ok ->
+ case verify_integer("tsize", 0, Config#config.max_tsize, Options) of
+ true ->
+ case verify_integer("blksize", 0, 65464, Options) of
+ true ->
+ ok;
+ false ->
+ {error, {badopt, "Too large blksize"}}
+ end;
+ false ->
+ {error, {badopt, "Too large tsize"}}
+ end;
+ {error, Reason} ->
+ {error, Reason}
+ end.
+
+post_verify_options(Config, Req, NewOptions, Text) ->
+ OldOptions = Req#tftp_msg_req.options,
+ BadOptions =
+ [Key || {Key, _Val} <- NewOptions,
+ not lists:keymember(Key, 1, OldOptions)],
+ case BadOptions =:= [] of
+ true ->
+ Config2 = Config#config{timeout = lookup_timeout(NewOptions)},
+ Req2 = Req#tftp_msg_req{options = NewOptions},
+ {ok, Config2, Req2};
+ false ->
+ {error, {badopt, Text}}
+ end.
+
+verify_reject(Config, Req, Options) ->
+ Access = Req#tftp_msg_req.access,
+ Rejected = Config#config.rejected,
+ case lists:member(Access, Rejected) of
+ true ->
+ {error, {eacces, atom_to_list(Access) ++ " mode not allowed"}};
+ false ->
+ [throw({error, {badopt, Key ++ " not allowed"}}) ||
+ {Key, _} <- Options, lists:member(Key, Rejected)],
+ ok
+ end.
+
+lookup_timeout(Options) ->
+ case lists:keysearch("timeout", 1, Options) of
+ {value, {_, Val}} ->
+ list_to_integer(Val);
+ false ->
+ 3
+ end.
+
+lookup_mode(Options) ->
+ case lists:keysearch("mode", 1, Options) of
+ {value, {_, Val}} ->
+ Val;
+ false ->
+ "octet"
+ end.
+
+verify_integer(Key, Min, Max, Options) ->
+ case lists:keysearch(Key, 1, Options) of
+ {value, {_, Val}} when is_list(Val) ->
+ case catch list_to_integer(Val) of
+ {'EXIT', _} ->
+ false;
+ Int when Int >= Min, is_integer(Min),
+ Max =:= infinity ->
+ true;
+ Int when Int >= Min, is_integer(Min),
+ Int =< Max, is_integer(Max) ->
+ true;
+ _ ->
+ false
+ end;
+ false ->
+ true
+ end.
+
+error_msg(#config{logger = Logger, debug_level = _Level}, F, A) ->
+ safe_apply(Logger, error_msg, [F, A]).
+
+warning_msg(#config{logger = Logger, debug_level = Level}, F, A) ->
+ case Level of
+ none -> ok;
+ error -> ok;
+ _ -> safe_apply(Logger, warning_msg, [F, A])
+ end.
+
+info_msg(#config{logger = Logger}, F, A) ->
+ safe_apply(Logger, info_msg, [F, A]).
+
+safe_apply(Mod, Fun, Args) ->
+ _ = fast_ensure_loaded(Mod),
+ apply(Mod, Fun, Args).
+
+fast_ensure_loaded(Mod) ->
+ case erlang:function_exported(Mod, module_info, 0) of
+ true ->
+ ok;
+ false ->
+ Res = code:load_file(Mod),
+ %% io:format("tftp: code:load_file(~p) -> ~p\n", [Mod, Res]), %% XXX
+ Res
+ end.
+
+print_debug_info(#config{debug_level = Level} = Config, Who, Where, Data) ->
+ if
+ Level =:= none ->
+ ok;
+ is_record(Data, error) ->
+ do_print_debug_info(Config, Who, Where, Data);
+ Level =:= warning ->
+ ok;
+ Level =:= error ->
+ ok;
+ Level =:= all ->
+ do_print_debug_info(Config, Who, Where, Data);
+ Where =:= open ->
+ do_print_debug_info(Config, Who, Where, Data);
+ Where =:= close ->
+ do_print_debug_info(Config, Who, Where, Data);
+ Level =:= brief ->
+ ok;
+ Where =/= recv, Where =/= send ->
+ ok;
+ is_record(Data, tftp_msg_data), Level =:= normal ->
+ ok;
+ is_record(Data, tftp_msg_ack), Level =:= normal ->
+ ok;
+ true ->
+ do_print_debug_info(Config, Who, Where, Data)
+ end.
+
+do_print_debug_info(Config, Who, Where, #tftp_msg_data{data = Bin} = Msg) when is_binary(Bin) ->
+ Msg2 = Msg#tftp_msg_data{data = {bytes, size(Bin)}},
+ do_print_debug_info(Config, Who, Where, Msg2);
+do_print_debug_info(Config, Who, Where, #tftp_msg_req{local_filename = Filename} = Msg) when is_binary(Filename) ->
+ Msg2 = Msg#tftp_msg_req{local_filename = binary},
+ do_print_debug_info(Config, Who, Where, Msg2);
+do_print_debug_info(Config, Who, Where, Data) ->
+ Local =
+ case catch inet:port(Config#config.udp_socket) of
+ {'EXIT', _Reason} ->
+ 0;
+ {ok, Port} ->
+ Port
+ end,
+ %% Remote = Config#config.udp_port,
+ PeerInfo = lists:flatten(io_lib:format("~p", [peer_info(Config)])),
+ Side =
+ if
+ is_record(Who, tftp_msg_req),
+ Who#tftp_msg_req.local_filename =/= undefined ->
+ client;
+ is_record(Who, tftp_msg_req),
+ Who#tftp_msg_req.local_filename =:= undefined ->
+ server;
+ is_atom(Who) ->
+ Who
+ end,
+ case {Where, Data} of
+ {_, #error{where = Where, code = Code, text = Text, filename = Filename}} ->
+ do_format(Config, Side, Local, "error ~s ->\n\t~p ~p\n\t~p ~p: ~s\n",
+ [PeerInfo, self(), Filename, Where, Code, Text]);
+ {open, #tftp_msg_req{filename = Filename}} ->
+ do_format(Config, Side, Local, "open ~s ->\n\t~p ~p\n",
+ [PeerInfo, self(), Filename]);
+ {close, #tftp_msg_req{filename = Filename}} ->
+ do_format(Config, Side, Local, "close ~s ->\n\t~p ~p\n",
+ [PeerInfo, self(), Filename]);
+ {recv, _} ->
+ do_format(Config, Side, Local, "recv ~s <-\n\t~p\n",
+ [PeerInfo, Data]);
+ {send, _} ->
+ do_format(Config, Side, Local, "send ~s ->\n\t~p\n",
+ [PeerInfo, Data]);
+ {match, _} when is_record(Data, callback) ->
+ Mod = Data#callback.module,
+ State = Data#callback.state,
+ do_format(Config, Side, Local, "match ~s ~p =>\n\t~p\n",
+ [PeerInfo, Mod, State]);
+ {call, _} ->
+ case Data of
+ {Callback, _Result} when is_record(Callback, callback) ->
+ Mod = Callback#callback.module,
+ State = Callback#callback.state,
+ do_format(Config, Side, Local, "call ~s ~p =>\n\t~p\n",
+ [PeerInfo, Mod, State]);
+ {undefined, Result} ->
+ do_format(Config, Side, Local, "call ~s result =>\n\t~p\n",
+ [PeerInfo, Result])
+ end
+ end.
+
+do_format(Config, Side, Local, Format, Args) ->
+ info_msg(Config, "~p(~p): " ++ Format, [Side, Local | Args]).
+
+%%-------------------------------------------------------------------
+%% System upgrade
+%%-------------------------------------------------------------------
+
+system_continue(_Parent, _Debug, #sys_misc{module = Mod, function = Fun, arguments = Args}) ->
+ apply(Mod, Fun, Args);
+system_continue(Parent, Debug, {Fun, Args}) ->
+ %% Handle upgrade from old releases. Please, remove this clause in next release.
+ system_continue(Parent, Debug, #sys_misc{module = ?MODULE, function = Fun, arguments = Args}).
+
+-spec system_terminate(_, _, _, #sys_misc{} | {_, _}) -> no_return().
+
+system_terminate(Reason, _Parent, _Debug, #sys_misc{}) ->
+ exit(Reason);
+system_terminate(Reason, Parent, Debug, {Fun, Args}) ->
+ %% Handle upgrade from old releases. Please, remove this clause in next release.
+ system_terminate(Reason, Parent, Debug, #sys_misc{module = ?MODULE, function = Fun, arguments = Args}).
+
+system_code_change({Fun, Args}, _Module, _OldVsn, _Extra) ->
+ {ok, {Fun, Args}}.
diff --git a/lib/tftp/src/tftp_file.erl b/lib/tftp/src/tftp_file.erl
new file mode 100644
index 0000000000..5922fc9418
--- /dev/null
+++ b/lib/tftp/src/tftp_file.erl
@@ -0,0 +1,390 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2005-2018. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+%%
+%%
+
+%%%-------------------------------------------------------------------
+%%% File : tft_file.erl
+%%% Author : Hakan Mattsson <[email protected]>
+%%% Description :
+%%%
+%%% Created : 24 May 2004 by Hakan Mattsson <[email protected]>
+%%%-------------------------------------------------------------------
+
+-module(tftp_file).
+
+%%%-------------------------------------------------------------------
+%%% Interface
+%%%-------------------------------------------------------------------
+
+-behaviour(tftp).
+
+-export([prepare/6, open/6, read/1, write/2, abort/3]).
+
+%%%-------------------------------------------------------------------
+%%% Defines
+%%%-------------------------------------------------------------------
+
+-include_lib("kernel/include/file.hrl").
+
+-record(initial,
+ {filename,
+ is_native_ascii}).
+
+-record(state,
+ {access,
+ filename,
+ is_native_ascii,
+ is_network_ascii,
+ root_dir,
+ options,
+ blksize,
+ fd,
+ count,
+ buffer}).
+
+%%-------------------------------------------------------------------
+%% prepare(Peer, Access, Filename, Mode, SuggestedOptions, InitialState) ->
+%% {ok, AcceptedOptions, NewState} | {error, Code, Text}
+%%
+%% Peer = {PeerType, PeerHost, PeerPort}
+%% PeerType = inet | inet6
+%% PeerHost = ip_address()
+%% PeerPort = integer()
+%% Acess = read | write
+%% Filename = string()
+%% Mode = string()
+%% SuggestedOptions = [{Key, Value}]
+%% AcceptedOptions = [{Key, Value}]
+%% Key = string()
+%% Value = string()
+%% InitialState = [] | [{root_dir, string()}]
+%% NewState = term()
+%% Code = undef | enoent | eacces | enospc |
+%% badop | eexist | baduser | badopt |
+%% integer()
+%% Text = string()
+%%
+%% Prepares open of a file on the client side.
+%%
+%% Will be followed by a call to open/4 before any read/write access
+%% is performed. The AcceptedOptions will be sent to the server which
+%% will reply with those options that it accepts. The options that are
+%% accepted by the server will be forwarded to open/4 as SuggestedOptions.
+%%
+%% No new options may be added, but the ones that are present as
+%% SuggestedOptions may be omitted or replaced with new values
+%% in the AcceptedOptions.
+%%-------------------------------------------------------------------
+
+prepare(_Peer, Access, Filename, Mode, SuggestedOptions, Initial) when is_list(Initial) ->
+ %% Client side
+ case catch handle_options(Access, Filename, Mode, SuggestedOptions, Initial) of
+ {ok, Filename2, IsNativeAscii, IsNetworkAscii, AcceptedOptions} ->
+ State = #state{access = Access,
+ filename = Filename2,
+ is_native_ascii = IsNativeAscii,
+ is_network_ascii = IsNetworkAscii,
+ options = AcceptedOptions,
+ blksize = lookup_blksize(AcceptedOptions),
+ count = 0,
+ buffer = []},
+ {ok, AcceptedOptions, State};
+ {error, {Code, Text}} ->
+ {error, {Code, Text}}
+ end.
+
+%% ---------------------------------------------------------
+%% open(Peer, Access, Filename, Mode, SuggestedOptions, State) ->
+%% {ok, AcceptedOptions, NewState} | {error, Code, Text}
+%%
+%% Peer = {PeerType, PeerHost, PeerPort}
+%% PeerType = inet | inet6
+%% PeerHost = ip_address()
+%% PeerPort = integer()
+%% Acess = read | write
+%% Filename = string()
+%% Mode = string()
+%% SuggestedOptions = [{Key, Value}]
+%% AcceptedOptions = [{Key, Value}]
+%% Key = string()
+%% Value = string()
+%% State = InitialState | #state{}
+%% InitialState = [] | [{root_dir, string()}]
+%% NewState = term()
+%% Code = undef | enoent | eacces | enospc |
+%% badop | eexist | baduser | badopt |
+%% integer()
+%% Text = string()
+%%
+%% Opens a file for read or write access.
+%%
+%% On the client side where the open/4 call has been preceeded by a
+%% call to prepare/4, all options must be accepted or rejected.
+%% On the server side, where there are no preceeding prepare/4 call,
+%% noo new options may be added, but the ones that are present as
+%% SuggestedOptions may be omitted or replaced with new values
+%% in the AcceptedOptions.
+%%-------------------------------------------------------------------
+
+open(Peer, Access, Filename, Mode, SuggestedOptions, Initial) when is_list(Initial) ->
+ %% Server side
+ case prepare(Peer, Access, Filename, Mode, SuggestedOptions, Initial) of
+ {ok, AcceptedOptions, State} ->
+ open(Peer, Access, Filename, Mode, AcceptedOptions, State);
+ {error, {Code, Text}} ->
+ {error, {Code, Text}}
+ end;
+open(_Peer, Access, Filename, Mode, NegotiatedOptions, State) when is_record(State, state) ->
+ %% Both sides
+ case catch handle_options(Access, Filename, Mode, NegotiatedOptions, State) of
+ {ok, _Filename2, _IsNativeAscii, _IsNetworkAscii, Options}
+ when Options =:= NegotiatedOptions ->
+ do_open(State);
+ {error, {Code, Text}} ->
+ {error, {Code, Text}}
+ end;
+open(Peer, Access, Filename, Mode, NegotiatedOptions, State) ->
+ %% Handle upgrade from old releases. Please, remove this clause in next release.
+ State2 = upgrade_state(State),
+ open(Peer, Access, Filename, Mode, NegotiatedOptions, State2).
+
+do_open(State) when is_record(State, state) ->
+ case file:open(State#state.filename, file_options(State)) of
+ {ok, Fd} ->
+ {ok, State#state.options, State#state{fd = Fd}};
+ {error, Reason} when is_atom(Reason) ->
+ {error, file_error(Reason)}
+ end.
+
+file_options(State) ->
+ case State#state.access of
+ read -> [read, read_ahead, raw, binary];
+ write -> [write, delayed_write, raw, binary]
+ end.
+
+file_error(Reason) when is_atom(Reason) ->
+ Details = file:format_error(Reason),
+ case Reason of
+ eexist -> {Reason, Details};
+ enoent -> {Reason, Details};
+ eacces -> {Reason, Details};
+ eperm -> {eacces, Details};
+ enospc -> {Reason, Details};
+ _ -> {undef, Details ++ " (" ++ atom_to_list(Reason) ++ ")"}
+ end.
+
+%%-------------------------------------------------------------------
+%% read(State) ->
+%% {more, Bin, NewState} | {last, Bin, FileSize} | {error, {Code, Text}}
+%%
+%% State = term()
+%% NewState = term()
+%% Bin = binary()
+%% FileSize = integer()
+%% Code = undef | enoent | eacces | enospc |
+%% badop | eexist | baduser | badopt |
+%% integer()
+%% Text = string()
+%%
+%% Reads a chunk from the file
+%%
+%% The file is automatically closed when the last chunk is read.
+%%-------------------------------------------------------------------
+
+read(#state{access = read} = State) ->
+ BlkSize = State#state.blksize,
+ case file:read(State#state.fd, BlkSize) of
+ {ok, Bin} when is_binary(Bin), size(Bin) =:= BlkSize ->
+ Count = State#state.count + size(Bin),
+ {more, Bin, State#state{count = Count}};
+ {ok, Bin} when is_binary(Bin), size(Bin) < BlkSize ->
+ _ = file:close(State#state.fd),
+ Count = State#state.count + size(Bin),
+ {last, Bin, Count};
+ eof ->
+ {last, <<>>, State#state.count};
+ {error, Reason} ->
+ _ = file:close(State#state.fd),
+ {error, file_error(Reason)}
+ end;
+read(State) ->
+ %% Handle upgrade from old releases. Please, remove this clause in next release.
+ State2 = upgrade_state(State),
+ read(State2).
+
+%%-------------------------------------------------------------------
+%% write(Bin, State) ->
+%% {more, NewState} | {last, FileSize} | {error, {Code, Text}}
+%%
+%% State = term()
+%% NewState = term()
+%% Bin = binary()
+%% FileSize = integer()
+%% Code = undef | enoent | eacces | enospc |
+%% badop | eexist | baduser | badopt |
+%% integer()
+%% Text = string()
+%%
+%% Writes a chunk to the file
+%%
+%% The file is automatically closed when the last chunk is written
+%%-------------------------------------------------------------------
+
+write(Bin, #state{access = write} = State) when is_binary(Bin) ->
+ Size = size(Bin),
+ BlkSize = State#state.blksize,
+ case file:write(State#state.fd, Bin) of
+ ok when Size =:= BlkSize->
+ Count = State#state.count + Size,
+ {more, State#state{count = Count}};
+ ok when Size < BlkSize->
+ _ = file:close(State#state.fd),
+ Count = State#state.count + Size,
+ {last, Count};
+ {error, Reason} ->
+ _ = file:close(State#state.fd),
+ _ = file:delete(State#state.filename),
+ {error, file_error(Reason)}
+ end;
+write(Bin, State) ->
+ %% Handle upgrade from old releases. Please, remove this clause in next release.
+ State2 = upgrade_state(State),
+ write(Bin, State2).
+
+%%-------------------------------------------------------------------
+%% abort(Code, Text, State) -> ok
+%%
+%% State = term()
+%% Code = undef | enoent | eacces | enospc |
+%% badop | eexist | baduser | badopt |
+%% badblk | integer()
+%% Text = string()
+%%
+%% Aborts the file transfer
+%%-------------------------------------------------------------------
+
+abort(_Code, _Text, #state{fd = Fd, access = Access} = State) ->
+ _ = file:close(Fd),
+ case Access of
+ write ->
+ ok = file:delete(State#state.filename);
+ read ->
+ ok
+ end.
+
+%%-------------------------------------------------------------------
+%% Process options
+%%-------------------------------------------------------------------
+
+handle_options(Access, Filename, Mode, Options, Initial) ->
+ I = #initial{filename = Filename, is_native_ascii = is_native_ascii()},
+ {Filename2, IsNativeAscii} = handle_initial(Initial, I),
+ IsNetworkAscii = handle_mode(Mode, IsNativeAscii),
+ Options2 = do_handle_options(Access, Filename2, Options),
+ {ok, Filename2, IsNativeAscii, IsNetworkAscii, Options2}.
+
+handle_mode(Mode, IsNativeAscii) ->
+ case Mode of
+ "netascii" when IsNativeAscii =:= true -> true;
+ "octet" -> false;
+ _ -> throw({error, {badop, "Illegal mode " ++ Mode}})
+ end.
+
+handle_initial([{root_dir, Dir} | Initial], I) ->
+ case catch filename_join(Dir, I#initial.filename) of
+ {'EXIT', _} ->
+ throw({error, {badop, "Internal error. root_dir is not a string"}});
+ Filename2 ->
+ handle_initial(Initial, I#initial{filename = Filename2})
+ end;
+handle_initial([{native_ascii, Bool} | Initial], I) ->
+ case Bool of
+ true -> handle_initial(Initial, I#initial{is_native_ascii = true});
+ false -> handle_initial(Initial, I#initial{is_native_ascii = false})
+ end;
+handle_initial([], I) when is_record(I, initial) ->
+ {I#initial.filename, I#initial.is_native_ascii};
+handle_initial(State, _) when is_record(State, state) ->
+ {State#state.filename, State#state.is_native_ascii}.
+
+filename_join(Dir, Filename) ->
+ case filename:pathtype(Filename) of
+ absolute ->
+ [_ | RelFilename] = filename:split(Filename),
+ filename:join([Dir, RelFilename]);
+ _ ->
+ filename:join([Dir, Filename])
+ end.
+
+do_handle_options(Access, Filename, [{Key, Val} | T]) ->
+ case Key of
+ "tsize" ->
+ case Access of
+ read when Val =:= "0" ->
+ case file:read_file_info(Filename) of
+ {ok, FI} ->
+ Tsize = integer_to_list(FI#file_info.size),
+ [{Key, Tsize} | do_handle_options(Access, Filename, T)];
+ {error, _} ->
+ do_handle_options(Access, Filename, T)
+ end;
+ _ ->
+ handle_integer(Access, Filename, Key, Val, T, 0, infinity)
+ end;
+ "blksize" ->
+ handle_integer(Access, Filename, Key, Val, T, 8, 65464);
+ "timeout" ->
+ handle_integer(Access, Filename, Key, Val, T, 1, 255);
+ _ ->
+ do_handle_options(Access, Filename, T)
+ end;
+do_handle_options(_Access, _Filename, []) ->
+ [].
+
+
+handle_integer(Access, Filename, Key, Val, Options, Min, Max) ->
+ case catch list_to_integer(Val) of
+ {'EXIT', _} ->
+ do_handle_options(Access, Filename, Options);
+ Int when Int >= Min, Int =< Max ->
+ [{Key, Val} | do_handle_options(Access, Filename, Options)];
+ Int when Int >= Min, Max =:= infinity ->
+ [{Key, Val} | do_handle_options(Access, Filename, Options)];
+ _Int ->
+ throw({error, {badopt, "Illegal " ++ Key ++ " value " ++ Val}})
+ end.
+
+lookup_blksize(Options) ->
+ case lists:keysearch("blksize", 1, Options) of
+ {value, {_, Val}} ->
+ list_to_integer(Val);
+ false ->
+ 512
+ end.
+
+is_native_ascii() ->
+ case os:type() of
+ {win32, _} -> true;
+ _ -> false
+ end.
+
+%% Handle upgrade from old releases. Please, remove this function in next release.
+upgrade_state({state, Access, Filename, RootDir, Options, BlkSize, Fd, Count, Buffer}) ->
+ {state, Access, Filename, false, false, RootDir, Options, BlkSize, Fd, Count, Buffer}.
diff --git a/lib/tftp/src/tftp_lib.erl b/lib/tftp/src/tftp_lib.erl
new file mode 100644
index 0000000000..407a273f58
--- /dev/null
+++ b/lib/tftp/src/tftp_lib.erl
@@ -0,0 +1,474 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2005-2018. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+%%
+%%
+
+%%%-------------------------------------------------------------------
+%%% File : tftp_lib.erl
+%%% Author : Hakan Mattsson <[email protected]>
+%%% Description : Option parsing, decode, encode etc.
+%%%
+%%% Created : 18 May 2004 by Hakan Mattsson <[email protected]>
+%%%-------------------------------------------------------------------
+
+-module(tftp_lib).
+
+%%-------------------------------------------------------------------
+%% Interface
+%%-------------------------------------------------------------------
+
+%% application internal functions
+-export([
+ parse_config/1,
+ parse_config/2,
+ decode_msg/1,
+ encode_msg/1,
+ replace_val/3,
+ to_lower/1,
+ host_to_string/1,
+ add_default_callbacks/1
+ ]).
+
+%%-------------------------------------------------------------------
+%% Defines
+%%-------------------------------------------------------------------
+
+-include("tftp.hrl").
+
+-define(LOWER(Char),
+ if
+ Char >= $A, Char =< $Z ->
+ Char - ($A - $a);
+ true ->
+ Char
+ end).
+
+%%-------------------------------------------------------------------
+%% Config
+%%-------------------------------------------------------------------
+
+parse_config(Options) ->
+ parse_config(Options, #config{}).
+
+parse_config(Options, Config) ->
+ do_parse_config(Options, Config).
+
+do_parse_config([{Key, Val} | Tail], Config) when is_record(Config, config) ->
+ case Key of
+ debug ->
+ if
+ Val =:= 0; Val =:= none ->
+ do_parse_config(Tail, Config#config{debug_level = none});
+ Val =:= 1; Val =:= error ->
+ do_parse_config(Tail, Config#config{debug_level = error});
+ Val =:= 2; Val =:= warning ->
+ do_parse_config(Tail, Config#config{debug_level = warning});
+ Val =:= 3; Val =:= brief ->
+ do_parse_config(Tail, Config#config{debug_level = brief});
+ Val =:= 4; Val =:= normal ->
+ do_parse_config(Tail, Config#config{debug_level = normal});
+ Val =:= 5; Val =:= verbose ->
+ do_parse_config(Tail, Config#config{debug_level = verbose});
+ Val =:= 6; Val =:= all ->
+ do_parse_config(Tail, Config#config{debug_level = all});
+ true ->
+ exit({badarg, {Key, Val}})
+ end;
+ host ->
+ if
+ is_list(Val) ->
+ do_parse_config(Tail, Config#config{udp_host = Val});
+ is_tuple(Val), size(Val) =:= 4 ->
+ do_parse_config(Tail, Config#config{udp_host = Val});
+ is_tuple(Val), size(Val) =:= 8 ->
+ do_parse_config(Tail, Config#config{udp_host = Val});
+ true ->
+ exit({badarg, {Key, Val}})
+ end;
+ port ->
+ if
+ is_integer(Val), Val >= 0 ->
+ Config2 = Config#config{udp_port = Val, udp_options = Config#config.udp_options},
+ do_parse_config(Tail, Config2);
+ true ->
+ exit({badarg, {Key, Val}})
+ end;
+ port_policy ->
+ case Val of
+ random ->
+ do_parse_config(Tail, Config#config{port_policy = Val});
+ 0 ->
+ do_parse_config(Tail, Config#config{port_policy = random});
+ MinMax when is_integer(MinMax), MinMax > 0 ->
+ do_parse_config(Tail, Config#config{port_policy = {range, MinMax, MinMax}});
+ {range, Min, Max} when Max >= Min,
+ is_integer(Min), Min > 0,
+ is_integer(Max), Max > 0 ->
+ do_parse_config(Tail, Config#config{port_policy = Val});
+ true ->
+ exit({badarg, {Key, Val}})
+ end;
+ udp when is_list(Val) ->
+ Fun =
+ fun({K, V}, List) when K /= active ->
+ replace_val(K, V, List);
+ (V, List) when V /= list, V /= binary ->
+ List ++ [V];
+ (V, _List) ->
+ exit({badarg, {udp, [V]}})
+ end,
+ UdpOptions = lists:foldl(Fun, Config#config.udp_options, Val),
+ do_parse_config(Tail, Config#config{udp_options = UdpOptions});
+ use_tsize ->
+ case Val of
+ true ->
+ do_parse_config(Tail, Config#config{use_tsize = Val});
+ false ->
+ do_parse_config(Tail, Config#config{use_tsize = Val});
+ _ ->
+ exit({badarg, {Key, Val}})
+ end;
+ max_tsize ->
+ if
+ Val =:= infinity ->
+ do_parse_config(Tail, Config#config{max_tsize = Val});
+ is_integer(Val), Val >= 0 ->
+ do_parse_config(Tail, Config#config{max_tsize = Val});
+ true ->
+ exit({badarg, {Key, Val}})
+ end;
+ max_conn ->
+ if
+ Val =:= infinity ->
+ do_parse_config(Tail, Config#config{max_conn = Val});
+ is_integer(Val), Val > 0 ->
+ do_parse_config(Tail, Config#config{max_conn = Val});
+ true ->
+ exit({badarg, {Key, Val}})
+ end;
+ _ when is_list(Key), is_list(Val) ->
+ Key2 = to_lower(Key),
+ Val2 = to_lower(Val),
+ TftpOptions = replace_val(Key2, Val2, Config#config.user_options),
+ do_parse_config(Tail, Config#config{user_options = TftpOptions});
+ reject ->
+ case Val of
+ read ->
+ Rejected = [Val | Config#config.rejected],
+ do_parse_config(Tail, Config#config{rejected = Rejected});
+ write ->
+ Rejected = [Val | Config#config.rejected],
+ do_parse_config(Tail, Config#config{rejected = Rejected});
+ _ when is_list(Val) ->
+ Rejected = [Val | Config#config.rejected],
+ do_parse_config(Tail, Config#config{rejected = Rejected});
+ _ ->
+ exit({badarg, {Key, Val}})
+ end;
+ callback ->
+ case Val of
+ {RegExp, Mod, State} when is_list(RegExp), is_atom(Mod) ->
+ case re:compile(RegExp) of
+ {ok, Internal} ->
+ Callback = #callback{regexp = RegExp,
+ internal = Internal,
+ module = Mod,
+ state = State},
+ Callbacks = Config#config.callbacks ++ [Callback],
+ do_parse_config(Tail, Config#config{callbacks = Callbacks});
+ {error, Reason} ->
+ exit({badarg, {Key, Val}, Reason})
+ end;
+ _ ->
+ exit({badarg, {Key, Val}})
+ end;
+ logger ->
+ if
+ is_atom(Val) ->
+ do_parse_config(Tail, Config#config{logger = Val});
+ true ->
+ exit({badarg, {Key, Val}})
+ end;
+ max_retries ->
+ if
+ is_integer(Val), Val > 0 ->
+ do_parse_config(Tail, Config#config{max_retries = Val});
+ true ->
+ exit({badarg, {Key, Val}})
+ end;
+ _ ->
+ exit({badarg, {Key, Val}})
+ end;
+do_parse_config([], #config{udp_host = Host,
+ udp_options = UdpOptions,
+ user_options = UserOptions,
+ callbacks = Callbacks} = Config) ->
+ IsInet6 = lists:member(inet6, UdpOptions),
+ IsInet = lists:member(inet, UdpOptions),
+ Host2 =
+ if
+ (IsInet and not IsInet6); (not IsInet and not IsInet6) ->
+ case inet:getaddr(Host, inet) of
+ {ok, Addr} ->
+ Addr;
+ {error, Reason} ->
+ exit({badarg, {host, Reason}})
+ end;
+ (IsInet6 and not IsInet) ->
+ case inet:getaddr(Host, inet6) of
+ {ok, Addr} ->
+ Addr;
+ {error, Reason} ->
+ exit({badarg, {host, Reason}})
+ end;
+ true ->
+ %% Conflicting options
+ exit({badarg, {udp, [inet]}})
+ end,
+ UdpOptions2 = lists:reverse(UdpOptions),
+ TftpOptions = lists:reverse(UserOptions),
+ Callbacks2 = add_default_callbacks(Callbacks),
+ Config#config{udp_host = Host2,
+ udp_options = UdpOptions2,
+ user_options = TftpOptions,
+ callbacks = Callbacks2};
+do_parse_config(Options, Config) when is_record(Config, config) ->
+ exit({badarg, Options}).
+
+add_default_callbacks(Callbacks) ->
+ RegExp = "",
+ {ok, Internal} = re:compile(RegExp),
+ File = #callback{regexp = RegExp,
+ internal = Internal,
+ module = tftp_file,
+ state = []},
+ Bin = #callback{regexp = RegExp,
+ internal = Internal,
+ module = tftp_binary,
+ state = []},
+ Callbacks ++ [File, Bin].
+
+host_to_string(Host) ->
+ case Host of
+ String when is_list(String) ->
+ String;
+ {A1, A2, A3, A4} -> % inet
+ lists:concat([A1, ".", A2, ".", A3, ".",A4]);
+ {A1, A2, A3, A4, A5, A6, A7, A8} -> % inet6
+ lists:concat([
+ int16_to_hex(A1), "::",
+ int16_to_hex(A2), "::",
+ int16_to_hex(A3), "::",
+ int16_to_hex(A4), "::",
+ int16_to_hex(A5), "::",
+ int16_to_hex(A6), "::",
+ int16_to_hex(A7), "::",
+ int16_to_hex(A8)
+ ])
+ end.
+
+int16_to_hex(0) ->
+ [$0];
+int16_to_hex(I) ->
+ N1 = ((I bsr 8) band 16#ff),
+ N2 = (I band 16#ff),
+ [code_character(N1 div 16), code_character(N1 rem 16),
+ code_character(N2 div 16), code_character(N2 rem 16)].
+
+code_character(N) when N < 10 ->
+ $0 + N;
+code_character(N) ->
+ $A + (N - 10).
+
+%%-------------------------------------------------------------------
+%% Decode
+%%-------------------------------------------------------------------
+
+decode_msg(Bin) when is_binary(Bin) ->
+ case Bin of
+ <<?TFTP_OPCODE_RRQ:16/integer, Tail/binary>> ->
+ case decode_strings(Tail, [keep_case, lower_case]) of
+ [Filename, Mode | Strings] ->
+ Options = decode_options(Strings),
+ #tftp_msg_req{access = read,
+ filename = Filename,
+ mode = to_lower(Mode),
+ options = Options};
+ [_Filename | _Strings] ->
+ exit(#tftp_msg_error{code = undef,
+ text = "Missing mode"});
+ _ ->
+ exit(#tftp_msg_error{code = undef,
+ text = "Missing filename"})
+ end;
+ <<?TFTP_OPCODE_WRQ:16/integer, Tail/binary>> ->
+ case decode_strings(Tail, [keep_case, lower_case]) of
+ [Filename, Mode | Strings] ->
+ Options = decode_options(Strings),
+ #tftp_msg_req{access = write,
+ filename = Filename,
+ mode = to_lower(Mode),
+ options = Options};
+ [_Filename | _Strings] ->
+ exit(#tftp_msg_error{code = undef,
+ text = "Missing mode"});
+ _ ->
+ exit(#tftp_msg_error{code = undef,
+ text = "Missing filename"})
+ end;
+ <<?TFTP_OPCODE_DATA:16/integer, SeqNo:16/integer, Data/binary>> ->
+ #tftp_msg_data{block_no = SeqNo, data = Data};
+ <<?TFTP_OPCODE_ACK:16/integer, SeqNo:16/integer>> ->
+ #tftp_msg_ack{block_no = SeqNo};
+ <<?TFTP_OPCODE_ERROR:16/integer, ErrorCode:16/integer, Tail/binary>> ->
+ case decode_strings(Tail, [keep_case]) of
+ [ErrorText] ->
+ ErrorCode2 = decode_error_code(ErrorCode),
+ #tftp_msg_error{code = ErrorCode2,
+ text = ErrorText};
+ _ ->
+ exit(#tftp_msg_error{code = undef,
+ text = "Trailing garbage"})
+ end;
+ <<?TFTP_OPCODE_OACK:16/integer, Tail/binary>> ->
+ Strings = decode_strings(Tail, [lower_case]),
+ Options = decode_options(Strings),
+ #tftp_msg_oack{options = Options};
+ _ ->
+ exit(#tftp_msg_error{code = undef,
+ text = "Invalid syntax"})
+ end.
+
+decode_strings(Bin, Cases) when is_binary(Bin), is_list(Cases) ->
+ do_decode_strings(Bin, Cases, []).
+
+do_decode_strings(<<>>, _Cases, Strings) ->
+ lists:reverse(Strings);
+do_decode_strings(Bin, [Case | Cases], Strings) ->
+ {String, Tail} = decode_string(Bin, Case, []),
+ if
+ Cases =:= [] ->
+ do_decode_strings(Tail, [Case], [String | Strings]);
+ true ->
+ do_decode_strings(Tail, Cases, [String | Strings])
+ end.
+
+decode_string(<<Char:8/integer, Tail/binary>>, Case, String) ->
+ if
+ Char =:= 0 ->
+ {lists:reverse(String), Tail};
+ Case =:= keep_case ->
+ decode_string(Tail, Case, [Char | String]);
+ Case =:= lower_case ->
+ Char2 = ?LOWER(Char),
+ decode_string(Tail, Case, [Char2 | String])
+ end;
+decode_string(<<>>, _Case, _String) ->
+ exit(#tftp_msg_error{code = undef, text = "Trailing null missing"}).
+
+decode_options([Key, Value | Strings]) ->
+ [{to_lower(Key), Value} | decode_options(Strings)];
+decode_options([]) ->
+ [].
+
+decode_error_code(Int) ->
+ case Int of
+ ?TFTP_ERROR_UNDEF -> undef;
+ ?TFTP_ERROR_ENOENT -> enoent;
+ ?TFTP_ERROR_EACCES -> eacces;
+ ?TFTP_ERROR_ENOSPC -> enospc;
+ ?TFTP_ERROR_BADOP -> badop;
+ ?TFTP_ERROR_BADBLK -> badblk;
+ ?TFTP_ERROR_EEXIST -> eexist;
+ ?TFTP_ERROR_BADUSER -> baduser;
+ ?TFTP_ERROR_BADOPT -> badopt;
+ Int when is_integer(Int), Int >= 0, Int =< 65535 -> Int;
+ _ -> exit(#tftp_msg_error{code = undef, text = "Error code outside range."})
+ end.
+
+%%-------------------------------------------------------------------
+%% Encode
+%%-------------------------------------------------------------------
+
+encode_msg(#tftp_msg_req{access = Access,
+ filename = Filename,
+ mode = Mode,
+ options = Options}) ->
+ OpCode = case Access of
+ read -> ?TFTP_OPCODE_RRQ;
+ write -> ?TFTP_OPCODE_WRQ
+ end,
+ [
+ <<OpCode:16/integer>>,
+ Filename,
+ 0,
+ Mode,
+ 0,
+ [[Key, 0, Val, 0] || {Key, Val} <- Options]
+ ];
+encode_msg(#tftp_msg_data{block_no = BlockNo, data = Data}) when BlockNo =< 65535 ->
+ [
+ <<?TFTP_OPCODE_DATA:16/integer, BlockNo:16/integer>>,
+ Data
+ ];
+encode_msg(#tftp_msg_ack{block_no = BlockNo}) when BlockNo =< 65535 ->
+ <<?TFTP_OPCODE_ACK:16/integer, BlockNo:16/integer>>;
+encode_msg(#tftp_msg_error{code = Code, text = Text}) ->
+ IntCode = encode_error_code(Code),
+ [
+ <<?TFTP_OPCODE_ERROR:16/integer, IntCode:16/integer>>,
+ Text,
+ 0
+ ];
+encode_msg(#tftp_msg_oack{options = Options}) ->
+ [
+ <<?TFTP_OPCODE_OACK:16/integer>>,
+ [[Key, 0, Val, 0] || {Key, Val} <- Options]
+ ].
+
+encode_error_code(Code) ->
+ case Code of
+ undef -> ?TFTP_ERROR_UNDEF;
+ enoent -> ?TFTP_ERROR_ENOENT;
+ eacces -> ?TFTP_ERROR_EACCES;
+ enospc -> ?TFTP_ERROR_ENOSPC;
+ badop -> ?TFTP_ERROR_BADOP;
+ badblk -> ?TFTP_ERROR_BADBLK;
+ eexist -> ?TFTP_ERROR_EEXIST;
+ baduser -> ?TFTP_ERROR_BADUSER;
+ badopt -> ?TFTP_ERROR_BADOPT;
+ Int when is_integer(Int), Int >= 0, Int =< 65535 -> Int
+ end.
+
+%%-------------------------------------------------------------------
+%% Miscellaneous
+%%-------------------------------------------------------------------
+
+replace_val(Key, Val, List) ->
+ case lists:keysearch(Key, 1, List) of
+ false ->
+ List ++ [{Key, Val}];
+ {value, {_, OldVal}} when OldVal =:= Val ->
+ List;
+ {value, {_, _}} ->
+ lists:keyreplace(Key, 1, List, {Key, Val})
+ end.
+
+to_lower(Chars) ->
+ [?LOWER(Char) || Char <- Chars].
diff --git a/lib/tftp/src/tftp_logger.erl b/lib/tftp/src/tftp_logger.erl
new file mode 100644
index 0000000000..548ed509dc
--- /dev/null
+++ b/lib/tftp/src/tftp_logger.erl
@@ -0,0 +1,99 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2008-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(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..13b753612b
--- /dev/null
+++ b/lib/tftp/src/tftp_sup.erl
@@ -0,0 +1,111 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2005-2018. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+%%
+%%
+%%----------------------------------------------------------------------
+%% 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..a0f6cb1ca4
--- /dev/null
+++ b/lib/tftp/test/tftp_SUITE.erl
@@ -0,0 +1,1001 @@
+%%
+%% %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(tftp_SUITE).
+
+-compile(export_all).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% Includes and defines
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+-include_lib("common_test/include/ct.hrl").
+-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,
+ app,
+ appup,
+ start_tftpd
+ ].
+
+groups() ->
+ [].
+
+init_per_suite(Config) ->
+ Config.
+
+end_per_suite(_Config) ->
+ ok.
+
+init_per_group(_GroupName, Config) ->
+ Config.
+
+end_per_group(_GroupName, Config) ->
+ Config.
+
+
+app() ->
+ [{doc, "Test that the tftp app file is ok"}].
+app(Config) when is_list(Config) ->
+ ok = ?t:app_test(tftp).
+
+%%--------------------------------------------------------------------
+appup() ->
+ [{doc, "Test that the tftp appup file is ok"}].
+appup(Config) when is_list(Config) ->
+ ok = ?t:appup_test(tftp).
+
+start_tftpd() ->
+ [{doc, "Start/stop of tfpd service"}].
+start_tftpd(Config) when is_list(Config) ->
+ process_flag(trap_exit, true),
+ ok = tftp:start(),
+ {ok, Pid0} = tftp:start_service([{host, "localhost"}, {port, 0}]),
+ Pids0 = [ServicePid || {_, ServicePid} <- tftp:services()],
+ true = lists:member(Pid0, Pids0),
+ {ok, [_|_]} = tftp:service_info(Pid0),
+ tftp:stop_service(Pid0),
+ ct:sleep(100),
+ Pids1 = [ServicePid || {_, ServicePid} <- tftp:services()],
+ false = lists:member(Pid0, Pids1),
+
+ {ok, Pid1} =
+ tftp:start_standalone([{host, "localhost"}, {port, 0}]),
+ Pids2 = [ServicePid || {_, ServicePid} <- tftp:services()],
+ false = lists:member(Pid1, Pids2),
+ %% Standalone service is not supervised
+ {error,not_found} = tftp:stop_service(Pid1),
+ ok = tftp:stop(),
+
+ application:load(tftp),
+ application:set_env(tftp, services, [{tftpd, [{host, "localhost"},
+ {port, 0}]}]),
+ ok = tftp:start(),
+ 1 = length(tftp:services()),
+ application:unset_env(tftp, services),
+ ok = tftp:stop().
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% 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 ->
+ <<Block:BlkSize/binary, Bin2/binary>> = 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..04534228c2
--- /dev/null
+++ b/lib/tftp/test/tftp_test_lib.erl
@@ -0,0 +1,308 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2007-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(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("<ERROR> 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..eb8ed77fc1
--- /dev/null
+++ b/lib/tftp/test/tftp_test_lib.hrl
@@ -0,0 +1,44 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2007-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%
+%%
+
+-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..f1b0851a8f
--- /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
+PRE_VSN =
+APP_VSN = "$(APPLICATION)-$(TFTP_VSN)$(PRE_VSN)"