+ * Details regarding the TFTP protocol and the format of TFTP packets can be found in RFC 783. But the point of these classes is to keep you from having to
+ * worry about the internals.
+ *
+ * @see DatagramSocketClient
+ * @see TFTPPacket
+ * @see TFTPPacketException
+ * @see TFTPClient
+ */
+public class TFTP extends DatagramSocketClient {
+ /**
+ * The ascii transfer mode. Its value is 0 and equivalent to NETASCII_MODE
+ */
+ public static final int ASCII_MODE = 0;
+
+ /**
+ * The netascii transfer mode. Its value is 0.
+ */
+ public static final int NETASCII_MODE = 0;
+
+ /**
+ * The binary transfer mode. Its value is 1 and equivalent to OCTET_MODE.
+ */
+ public static final int BINARY_MODE = 1;
+
+ /**
+ * The image transfer mode. Its value is 1 and equivalent to OCTET_MODE.
+ */
+ public static final int IMAGE_MODE = 1;
+
+ /**
+ * The octet transfer mode. Its value is 1.
+ */
+ public static final int OCTET_MODE = 1;
+
+ /**
+ * The default number of milliseconds to wait to receive a datagram before timing out. The default is 5,000 milliseconds (5 seconds).
+ *
+ * @deprecated Use {@link #DEFAULT_TIMEOUT_DURATION}.
+ */
+ @Deprecated
+ public static final int DEFAULT_TIMEOUT = 5000;
+
+ /**
+ * The default duration to wait to receive a datagram before timing out. The default is 5 seconds.
+ *
+ * @since 3.10.0
+ */
+ public static final Duration DEFAULT_TIMEOUT_DURATION = Duration.ofSeconds(5);
+
+ /**
+ * The default TFTP port according to RFC 783 is 69.
+ */
+ public static final int DEFAULT_PORT = 69;
+
+ /**
+ * The size to use for TFTP packet buffers. Its 4 plus the TFTPPacket.SEGMENT_SIZE, i.e. 516.
+ */
+ static final int PACKET_SIZE = TFTPPacket.SEGMENT_SIZE + 4;
+
+ /**
+ * Returns the TFTP string representation of a TFTP transfer mode. Will throw an ArrayIndexOutOfBoundsException if an invalid transfer mode is specified.
+ *
+ * @param mode The TFTP transfer mode. One of the MODE constants.
+ * @return The TFTP string representation of the TFTP transfer mode.
+ */
+ public static String getModeName(final int mode) {
+ return TFTPRequestPacket.modeStrings[mode];
+ }
+
+ /**
+ * A buffer used to accelerate receives in bufferedReceive()
+ */
+ private byte[] receiveBuffer;
+
+ /**
+ * A datagram used to minimize memory allocation in bufferedReceive()
+ */
+ private DatagramPacket receiveDatagram;
+
+ /**
+ * A datagram used to minimize memory allocation in bufferedSend()
+ */
+ private DatagramPacket sendDatagram;
+
+ /**
+ * A buffer used to accelerate sends in bufferedSend(). It is left package visible so that TFTPClient may be slightly more efficient during file sends. It
+ * saves the creation of an additional buffer and prevents a buffer copy in _newDataPcket().
+ */
+ byte[] sendBuffer;
+
+ /**
+ * Creates a TFTP instance with a default timeout of {@link #DEFAULT_TIMEOUT_DURATION}, a null socket, and buffered operations disabled.
+ */
+ public TFTP() {
+ setDefaultTimeout(DEFAULT_TIMEOUT_DURATION);
+ receiveBuffer = null;
+ receiveDatagram = null;
+ }
+
+ /**
+ * Initializes the internal buffers. Buffers are used by {@link #bufferedSend bufferedSend() } and {@link #bufferedReceive bufferedReceive() }. This method
+ * must be called before calling either one of those two methods. When you finish using buffered operations, you must call
+ * {@link #endBufferedOps endBufferedOps() }.
+ */
+ public final void beginBufferedOps() {
+ receiveBuffer = new byte[PACKET_SIZE];
+ receiveDatagram = new DatagramPacket(receiveBuffer, receiveBuffer.length);
+ sendBuffer = new byte[PACKET_SIZE];
+ sendDatagram = new DatagramPacket(sendBuffer, sendBuffer.length);
+ }
+
+ /**
+ * This is a special method to perform a more efficient packet receive. It should only be used after calling {@link #beginBufferedOps beginBufferedOps() }.
+ * beginBufferedOps() initializes a set of buffers used internally that prevent the new allocation of a DatagramPacket and byte array for each send and
+ * receive. To use these buffers you must call the bufferedReceive() and bufferedSend() methods instead of send() and receive(). You must also be certain
+ * that you don't manipulate the resulting packet in such a way that it interferes with future buffered operations. For example, a TFTPDataPacket received
+ * with bufferedReceive() will have a reference to the internal byte buffer. You must finish using this data before calling bufferedReceive() again, or else
+ * the data will be overwritten by the call.
+ *
+ * @return The TFTPPacket received.
+ * @throws InterruptedIOException If a socket timeout occurs. The Java documentation claims an InterruptedIOException is thrown on a DatagramSocket timeout,
+ * but in practice we find a SocketException is thrown. You should catch both to be safe.
+ * @throws SocketException If a socket timeout occurs. The Java documentation claims an InterruptedIOException is thrown on a DatagramSocket timeout, but in
+ * practice we find a SocketException is thrown. You should catch both to be safe.
+ * @throws IOException If some other I/O error occurs.
+ * @throws TFTPPacketException If an invalid TFTP packet is received.
+ */
+ public final TFTPPacket bufferedReceive() throws IOException, InterruptedIOException, SocketException, TFTPPacketException {
+ receiveDatagram.setData(receiveBuffer);
+ receiveDatagram.setLength(receiveBuffer.length);
+ checkOpen().receive(receiveDatagram);
+
+ final TFTPPacket newTFTPPacket = TFTPPacket.newTFTPPacket(receiveDatagram);
+ trace("<", newTFTPPacket);
+ return newTFTPPacket;
+ }
+
+ /**
+ * This is a special method to perform a more efficient packet send. It should only be used after calling {@link #beginBufferedOps beginBufferedOps() }.
+ * beginBufferedOps() initializes a set of buffers used internally that prevent the new allocation of a DatagramPacket and byte array for each send and
+ * receive. To use these buffers you must call the bufferedReceive() and bufferedSend() methods instead of send() and receive(). You must also be certain
+ * that you don't manipulate the resulting packet in such a way that it interferes with future buffered operations. For example, a TFTPDataPacket received
+ * with bufferedReceive() will have a reference to the internal byte buffer. You must finish using this data before calling bufferedReceive() again, or else
+ * the data will be overwritten by the call.
+ *
+ * @param packet The TFTP packet to send.
+ * @throws IOException If some I/O error occurs.
+ */
+ public final void bufferedSend(final TFTPPacket packet) throws IOException {
+ trace(">", packet);
+ checkOpen().send(packet.newDatagram(sendDatagram, sendBuffer));
+ }
+
+ /**
+ * This method synchronizes a connection by discarding all packets that may be in the local socket buffer. This method need only be called when you
+ * implement your own TFTP client or server.
+ *
+ * @throws IOException if an I/O error occurs.
+ */
+ public final void discardPackets() throws IOException {
+ final DatagramPacket datagram = new DatagramPacket(new byte[PACKET_SIZE], PACKET_SIZE);
+ final Duration to = getSoTimeoutDuration();
+ setSoTimeout(Duration.ofMillis(1));
+ try {
+ while (true) {
+ checkOpen().receive(datagram);
+ }
+ } catch (final SocketException | InterruptedIOException e) {
+ // Do nothing. We timed out, so we hope we're caught up.
+ }
+ setSoTimeout(to);
+ }
+
+ /**
+ * Releases the resources used to perform buffered sends and receives.
+ */
+ public final void endBufferedOps() {
+ receiveBuffer = null;
+ receiveDatagram = null;
+ sendBuffer = null;
+ sendDatagram = null;
+ }
+
+ /**
+ * Receives a TFTPPacket.
+ *
+ * @return The TFTPPacket received.
+ * @throws InterruptedIOException If a socket timeout occurs. The Java documentation claims an InterruptedIOException is thrown on a DatagramSocket timeout,
+ * but in practice we find a SocketException is thrown. You should catch both to be safe.
+ * @throws SocketException If a socket timeout occurs. The Java documentation claims an InterruptedIOException is thrown on a DatagramSocket timeout, but in
+ * practice we find a SocketException is thrown. You should catch both to be safe.
+ * @throws IOException If some other I/O error occurs.
+ * @throws TFTPPacketException If an invalid TFTP packet is received.
+ */
+ public final TFTPPacket receive() throws IOException, InterruptedIOException, SocketException, TFTPPacketException {
+ final DatagramPacket packet;
+
+ packet = new DatagramPacket(new byte[PACKET_SIZE], PACKET_SIZE);
+
+ checkOpen().receive(packet);
+
+ final TFTPPacket newTFTPPacket = TFTPPacket.newTFTPPacket(packet);
+ trace("<", newTFTPPacket);
+ return newTFTPPacket;
+ }
+
+ /**
+ * Sends a TFTP packet to its destination.
+ *
+ * @param packet The TFTP packet to send.
+ * @throws IOException If some I/O error occurs.
+ */
+ public final void send(final TFTPPacket packet) throws IOException {
+ trace(">", packet);
+ checkOpen().send(packet.newDatagram());
+ }
+
+ /**
+ * Trace facility; this implementation does nothing.
+ *
+ * Override it to trace the data, for example: {@code System.out.println(direction + " " + packet.toString());}
+ *
+ * @param direction {@code >} or {@code <}
+ * @param packet the packet to be sent or that has been received respectively
+ * @since 3.6
+ */
+ protected void trace(final String direction, final TFTPPacket packet) {
+ // NOP
+ }
+}
diff --git a/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPAckPacket.java b/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPAckPacket.java
new file mode 100644
index 0000000..89bc439
--- /dev/null
+++ b/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPAckPacket.java
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package pl.psobiech.opengr8on.org.apache.commons.net;
+
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+
+/**
+ * A final class derived from TFTPPacket defining the TFTP Acknowledgement packet type.
+ *
+ * Details regarding the TFTP protocol and the format of TFTP packets can be found in RFC 783. But the point of these classes is to keep you from having to
+ * worry about the internals. Additionally, only very few people should have to care about any of the TFTPPacket classes or derived classes. Almost all users
+ * should only be concerned with the {@link TFTPClient} class {@link TFTPClient#receiveFile receiveFile()} and {@link TFTPClient#sendFile sendFile()} methods.
+ *
+ * @see TFTPPacket
+ * @see TFTPPacketException
+ * @see TFTP
+ */
+public final class TFTPAckPacket extends TFTPPacket {
+ /**
+ * The block number being acknowledged by the packet.
+ */
+ int blockNumber;
+
+ /**
+ * Creates an acknowledgement packet based from a received datagram. Assumes the datagram is at least length 4, else an ArrayIndexOutOfBoundsException may
+ * be thrown.
+ *
+ * @param datagram The datagram containing the received acknowledgement.
+ * @throws TFTPPacketException If the datagram isn't a valid TFTP acknowledgement packet.
+ */
+ TFTPAckPacket(final DatagramPacket datagram) throws TFTPPacketException {
+ super(ACKNOWLEDGEMENT, datagram.getAddress(), datagram.getPort());
+ final byte[] data;
+
+ data = datagram.getData();
+
+ if (getType() != data[1]) {
+ throw new TFTPPacketException("TFTP operator code does not match type.");
+ }
+
+ this.blockNumber = (data[2] & 0xff) << 8 | data[3] & 0xff;
+ }
+
+ /**
+ * Creates an acknowledgment packet to be sent to a host at a given port acknowledging receipt of a block.
+ *
+ * @param destination The host to which the packet is going to be sent.
+ * @param port The port to which the packet is going to be sent.
+ * @param blockNumber The block number being acknowledged.
+ */
+ public TFTPAckPacket(final InetAddress destination, final int port, final int blockNumber) {
+ super(ACKNOWLEDGEMENT, destination, port);
+ this.blockNumber = blockNumber;
+ }
+
+ /**
+ * Returns the block number of the acknowledgement.
+ *
+ * @return The block number of the acknowledgement.
+ */
+ public int getBlockNumber() {
+ return blockNumber;
+ }
+
+ /**
+ * Creates a UDP datagram containing all the TFTP acknowledgement packet data in the proper format. This is a method exposed to the programmer in case he
+ * wants to implement his own TFTP client instead of using the {@link TFTPClient} class. Under normal circumstances, you should not have a need to call this
+ * method.
+ *
+ * @return A UDP datagram containing the TFTP acknowledgement packet.
+ */
+ @Override
+ public DatagramPacket newDatagram() {
+ final byte[] data;
+
+ data = new byte[4];
+ data[0] = 0;
+ data[1] = (byte) type;
+ data[2] = (byte) ((blockNumber & 0xffff) >> 8);
+ data[3] = (byte) (blockNumber & 0xff);
+
+ return new DatagramPacket(data, data.length, address, port);
+ }
+
+ /**
+ * This is a method only available within the package for implementing efficient datagram transport by eliminating buffering. It takes a datagram as an
+ * argument, and a byte buffer in which to store the raw datagram data. Inside the method, the data is set as the datagram's data and the datagram
+ * returned.
+ *
+ * @param datagram The datagram to create.
+ * @param data The buffer to store the packet and to use in the datagram.
+ * @return The datagram argument.
+ */
+ @Override
+ DatagramPacket newDatagram(final DatagramPacket datagram, final byte[] data) {
+ data[0] = 0;
+ data[1] = (byte) type;
+ data[2] = (byte) ((blockNumber & 0xffff) >> 8);
+ data[3] = (byte) (blockNumber & 0xff);
+
+ datagram.setAddress(address);
+ datagram.setPort(port);
+ datagram.setData(data);
+ datagram.setLength(4);
+
+ return datagram;
+ }
+
+ /**
+ * Sets the block number of the acknowledgement.
+ *
+ * @param blockNumber the number to set
+ */
+ public void setBlockNumber(final int blockNumber) {
+ this.blockNumber = blockNumber;
+ }
+
+ /**
+ * For debugging
+ *
+ * @since 3.6
+ */
+ @Override
+ public String toString() {
+ return super.toString() + " ACK " + blockNumber;
+ }
+}
diff --git a/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPClient.java b/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPClient.java
new file mode 100644
index 0000000..a464549
--- /dev/null
+++ b/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPClient.java
@@ -0,0 +1,458 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package pl.psobiech.opengr8on.org.apache.commons.net;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+
+import org.apache.commons.net.io.FromNetASCIIOutputStream;
+import org.apache.commons.net.io.ToNetASCIIInputStream;
+
+/**
+ * The TFTPClient class encapsulates all the aspects of the TFTP protocol necessary to receive and send files through TFTP. It is derived from the {@link TFTP}
+ * because it is more convenient than using aggregation, and as a result exposes the same set of methods to allow you to deal with the TFTP protocol directly.
+ * However, almost every user should only be concerned with the the {@link org.apache.commons.net.DatagramSocketClient#open open() },
+ * {@link org.apache.commons.net.DatagramSocketClient#close close() }, {@link #sendFile sendFile() }, and {@link #receiveFile receiveFile() } methods.
+ * Additionally, the {@link #setMaxTimeouts setMaxTimeouts() } and {@link org.apache.commons.net.DatagramSocketClient#setDefaultTimeout setDefaultTimeout() }
+ * methods may be of importance for performance tuning.
+ *
+ * Details regarding the TFTP protocol and the format of TFTP packets can be found in RFC 783. But the point of these classes is to keep you from having to
+ * worry about the internals.
+ *
+ * @see TFTP
+ * @see TFTPPacket
+ * @see TFTPPacketException
+ */
+public class TFTPClient extends TFTP {
+ /**
+ * The default number of times a {@code receive} attempt is allowed to timeout before ending attempts to retry the {@code receive} and failing. The default
+ * is 5 timeouts.
+ */
+ public static final int DEFAULT_MAX_TIMEOUTS = 5;
+
+ /**
+ * The maximum number of timeouts allowed before failing.
+ */
+ private int maxTimeouts;
+
+ /**
+ * The number of bytes received in the ongoing download.
+ */
+ private long totalBytesReceived;
+
+ /**
+ * The number of bytes sent in the ongoing upload.
+ */
+ private long totalBytesSent;
+
+ /**
+ * Creates a TFTPClient instance with a default timeout of DEFAULT_TIMEOUT, maximum timeouts value of DEFAULT_MAX_TIMEOUTS, a null socket, and buffered
+ * operations disabled.
+ */
+ public TFTPClient() {
+ maxTimeouts = DEFAULT_MAX_TIMEOUTS;
+ }
+
+ /**
+ * Returns the maximum number of times a {@code receive} attempt is allowed to timeout before ending attempts to retry the {@code receive} and failing.
+ *
+ * @return The maximum number of timeouts allowed.
+ */
+ public int getMaxTimeouts() {
+ return maxTimeouts;
+ }
+
+ /**
+ * @return The number of bytes received in the ongoing download
+ */
+ public long getTotalBytesReceived() {
+ return totalBytesReceived;
+ }
+
+ /**
+ * @return The number of bytes sent in the ongoing download
+ */
+ public long getTotalBytesSent() {
+ return totalBytesSent;
+ }
+
+ /**
+ * Same as calling receiveFile(fileName, mode, output, host, TFTP.DEFAULT_PORT).
+ *
+ * @param fileName The name of the file to receive.
+ * @param mode The TFTP mode of the transfer (one of the MODE constants).
+ * @param output The OutputStream to which the file should be written.
+ * @param host The remote host serving the file.
+ * @return number of bytes read
+ * @throws IOException If an I/O error occurs. The nature of the error will be reported in the message.
+ */
+ public int receiveFile(final String fileName, final int mode, final OutputStream output, final InetAddress host) throws IOException {
+ return receiveFile(fileName, mode, output, host, DEFAULT_PORT);
+ }
+
+ /**
+ * Requests a named file from a remote host, writes the file to an OutputStream, closes the connection, and returns the number of bytes read. A local UDP
+ * socket must first be created by {@link org.apache.commons.net.DatagramSocketClient#open open()} before invoking this method. This method will not close
+ * the OutputStream containing the file; you must close it after the method invocation.
+ *
+ * @param fileName The name of the file to receive.
+ * @param mode The TFTP mode of the transfer (one of the MODE constants).
+ * @param output The OutputStream to which the file should be written.
+ * @param host The remote host serving the file.
+ * @param port The port number of the remote TFTP server.
+ * @return number of bytes read
+ * @throws IOException If an I/O error occurs. The nature of the error will be reported in the message.
+ */
+ public int receiveFile(final String fileName, final int mode, OutputStream output, InetAddress host, final int port) throws IOException {
+ int bytesRead = 0;
+ int lastBlock = 0;
+ int block = 1;
+ int hostPort = 0;
+ int dataLength = 0;
+
+ totalBytesReceived = 0;
+
+ if (mode == ASCII_MODE) {
+ output = new FromNetASCIIOutputStream(output);
+ }
+
+ TFTPPacket sent = new TFTPReadRequestPacket(host, port, fileName, mode);
+ final TFTPAckPacket ack = new TFTPAckPacket(host, port, 0);
+
+ beginBufferedOps();
+
+ boolean justStarted = true;
+ try {
+ do { // while more data to fetch
+ bufferedSend(sent); // start the fetch/send an ack
+ boolean wantReply = true;
+ int timeouts = 0;
+ do { // until successful response
+ try {
+ final TFTPPacket received = bufferedReceive();
+ final int recdPort = received.getPort();
+ final InetAddress recdAddress = received.getAddress();
+
+ // The first time we receive we get the port number and
+ // answering host address( for hosts with multiple IPs)
+ if (justStarted) {
+ justStarted = false;
+ //if (recdPort == port) { // must not use the control port here
+ // final TFTPErrorPacket
+ // error = new TFTPErrorPacket(recdAddress, recdPort, TFTPErrorPacket.UNKNOWN_TID, "INCORRECT SOURCE PORT");
+ // bufferedSend(error);
+ // throw new IOException("Incorrect source port (" + recdPort + ") in request reply.");
+ //}
+ hostPort = recdPort;
+ ack.setPort(hostPort);
+ if (!host.equals(recdAddress)) {
+ host = recdAddress;
+ ack.setAddress(host);
+ sent.setAddress(host);
+ }
+ }
+
+ // Comply with RFC 783 indication that an error acknowledgment
+ // should be sent to originator if unexpected TID or host.
+ if (host.equals(recdAddress) && recdPort == hostPort) {
+ switch (received.getType()) {
+ case TFTPPacket.ERROR:
+ TFTPErrorPacket error = (TFTPErrorPacket) received;
+ throw new TFTPPacketIOException(error.getError(), "Error code " + error.getError() + " received: " + error.getMessage());
+ case TFTPPacket.DATA:
+ final TFTPDataPacket data = (TFTPDataPacket) received;
+ dataLength = data.getDataLength();
+ lastBlock = data.getBlockNumber();
+
+ if (lastBlock == block) { // is the next block number?
+ try {
+ output.write(data.getData(), data.getDataOffset(), dataLength);
+ } catch (final IOException e) {
+ TFTPErrorPacket newError = new TFTPErrorPacket(host, hostPort, TFTPErrorPacket.OUT_OF_SPACE, "File write failed.");
+ bufferedSend(newError);
+ throw new TFTPPacketIOException(TFTPErrorPacket.OUT_OF_SPACE, e);
+ }
+ ++block;
+ if (block > 65535) {
+ // wrap the block number
+ block = 0;
+ }
+ wantReply = false; // got the next block, drop out to ack it
+ } else { // unexpected block number
+ discardPackets();
+ if (lastBlock == (block == 0 ? 65535 : block - 1)) {
+ wantReply = false; // Resend last acknowledgement
+ }
+ }
+ break;
+ default:
+ throw new IOException("Received unexpected packet type (" + received.getType() + ")");
+ }
+ } else { // incorrect host or TID
+ final TFTPErrorPacket error = new TFTPErrorPacket(
+ recdAddress, recdPort,
+ TFTPErrorPacket.UNKNOWN_TID, "Unexpected host or port."
+ );
+ bufferedSend(error);
+ }
+ } catch (final SocketException | InterruptedIOException e) {
+ if (++timeouts >= maxTimeouts) {
+ throw new IOException("Connection timed out.");
+ }
+ } catch (final TFTPPacketException e) {
+ throw new IOException("Bad packet: " + e.getMessage());
+ }
+ } while (wantReply); // waiting for response
+
+ ack.setBlockNumber(lastBlock);
+ sent = ack;
+ bytesRead += dataLength;
+ totalBytesReceived += dataLength;
+ } while (dataLength == TFTPPacket.SEGMENT_SIZE); // not eof
+ bufferedSend(sent); // send the final ack
+ } finally {
+ endBufferedOps();
+ }
+ return bytesRead;
+ }
+
+ /**
+ * Same as calling receiveFile(fileName, mode, output, hostname, TFTP.DEFAULT_PORT).
+ *
+ * @param fileName The name of the file to receive.
+ * @param mode The TFTP mode of the transfer (one of the MODE constants).
+ * @param output The OutputStream to which the file should be written.
+ * @param hostname The name of the remote host serving the file.
+ * @return number of bytes read
+ * @throws IOException If an I/O error occurs. The nature of the error will be reported in the message.
+ * @throws UnknownHostException If the hostname cannot be resolved.
+ */
+ public int receiveFile(final String fileName, final int mode, final OutputStream output, final String hostname) throws UnknownHostException, IOException {
+ return receiveFile(fileName, mode, output, InetAddress.getByName(hostname), DEFAULT_PORT);
+ }
+
+ /**
+ * Requests a named file from a remote host, writes the file to an OutputStream, closes the connection, and returns the number of bytes read. A local UDP
+ * socket must first be created by {@link org.apache.commons.net.DatagramSocketClient#open open()} before invoking this method. This method will not close
+ * the OutputStream containing the file; you must close it after the method invocation.
+ *
+ * @param fileName The name of the file to receive.
+ * @param mode The TFTP mode of the transfer (one of the MODE constants).
+ * @param output The OutputStream to which the file should be written.
+ * @param hostname The name of the remote host serving the file.
+ * @param port The port number of the remote TFTP server.
+ * @return number of bytes read
+ * @throws IOException If an I/O error occurs. The nature of the error will be reported in the message.
+ * @throws UnknownHostException If the hostname cannot be resolved.
+ */
+ public int receiveFile(final String fileName, final int mode, final OutputStream output, final String hostname, final int port)
+ throws UnknownHostException, IOException {
+ return receiveFile(fileName, mode, output, InetAddress.getByName(hostname), port);
+ }
+
+ /**
+ * Same as calling sendFile(fileName, mode, input, host, TFTP.DEFAULT_PORT).
+ *
+ * @param fileName The name the remote server should use when creating the file on its file system.
+ * @param mode The TFTP mode of the transfer (one of the MODE constants).
+ * @param input the input stream containing the data to be sent
+ * @param host The name of the remote host receiving the file.
+ * @throws IOException If an I/O error occurs. The nature of the error will be reported in the message.
+ * @throws UnknownHostException If the hostname cannot be resolved.
+ */
+ public void sendFile(final String fileName, final int mode, final InputStream input, final InetAddress host) throws IOException {
+ sendFile(fileName, mode, input, host, DEFAULT_PORT);
+ }
+
+ /**
+ * Requests to send a file to a remote host, reads the file from an InputStream, sends the file to the remote host, and closes the connection. A local UDP
+ * socket must first be created by {@link org.apache.commons.net.DatagramSocketClient#open open()} before invoking this method. This method will not close
+ * the InputStream containing the file; you must close it after the method invocation.
+ *
+ * @param fileName The name the remote server should use when creating the file on its file system.
+ * @param mode The TFTP mode of the transfer (one of the MODE constants).
+ * @param input the input stream containing the data to be sent
+ * @param host The remote host receiving the file.
+ * @param port The port number of the remote TFTP server.
+ * @throws IOException If an I/O error occurs. The nature of the error will be reported in the message.
+ */
+ public void sendFile(final String fileName, final int mode, InputStream input, InetAddress host, final int port) throws IOException {
+ int block = 0;
+ int hostPort = 0;
+ boolean justStarted = true;
+ boolean lastAckWait = false;
+
+ totalBytesSent = 0L;
+
+ if (mode == ASCII_MODE) {
+ input = new ToNetASCIIInputStream(input);
+ }
+
+ TFTPPacket sent = new TFTPWriteRequestPacket(host, port, fileName, mode);
+ final TFTPDataPacket data = new TFTPDataPacket(host, port, 0, sendBuffer, 4, 0);
+
+ beginBufferedOps();
+
+ try {
+ do { // until eof
+ // first time: block is 0, lastBlock is 0, send a request packet.
+ // subsequent: block is integer starting at 1, send data packet.
+ bufferedSend(sent);
+ boolean wantReply = true;
+ int timeouts = 0;
+ do {
+ try {
+ final TFTPPacket received = bufferedReceive();
+ final InetAddress recdAddress = received.getAddress();
+ final int recdPort = received.getPort();
+
+ // The first time we receive we get the port number and
+ // answering host address (for hosts with multiple IPs)
+ if (justStarted) {
+ justStarted = false;
+ //if (recdPort == port) { // must not use the control port here
+ // final TFTPErrorPacket
+ // error = new TFTPErrorPacket(recdAddress, recdPort, TFTPErrorPacket.UNKNOWN_TID, "INCORRECT SOURCE PORT");
+ // bufferedSend(error);
+ // throw new IOException("Incorrect source port (" + recdPort + ") in request reply.");
+ //}
+ hostPort = recdPort;
+ data.setPort(hostPort);
+ if (!host.equals(recdAddress)) {
+ host = recdAddress;
+ data.setAddress(host);
+ sent.setAddress(host);
+ }
+ }
+
+ // Comply with RFC 783 indication that an error acknowledgment
+ // should be sent to originator if unexpected TID or host.
+ if (host.equals(recdAddress) && recdPort == hostPort) {
+ switch (received.getType()) {
+ case TFTPPacket.ERROR:
+ final TFTPErrorPacket error = (TFTPErrorPacket) received;
+ throw new TFTPPacketIOException(error.getError(), "Error code " + error.getError() + " received: " + error.getMessage());
+ case TFTPPacket.ACKNOWLEDGEMENT:
+
+ final int lastBlock = ((TFTPAckPacket) received).getBlockNumber();
+
+ if (lastBlock == block) {
+ ++block;
+ if (block > 65535) {
+ // wrap the block number
+ block = 0;
+ }
+ wantReply = false; // got the ack we want
+ } else {
+ discardPackets();
+ }
+ break;
+ default:
+ throw new IOException("Received unexpected packet type.");
+ }
+ } else { // wrong host or TID; send error
+ final TFTPErrorPacket error = new TFTPErrorPacket(
+ recdAddress, recdPort,
+ TFTPErrorPacket.UNKNOWN_TID, "Unexpected host or port."
+ );
+ bufferedSend(error);
+ }
+ } catch (final SocketException | InterruptedIOException e) {
+ if (++timeouts >= maxTimeouts) {
+ throw new IOException("Connection timed out.");
+ }
+ } catch (final TFTPPacketException e) {
+ throw new IOException("Bad packet: " + e.getMessage());
+ }
+ // retry until a good ack
+ } while (wantReply);
+
+ if (lastAckWait) {
+ break; // we were waiting for this; now all done
+ }
+
+ int dataLength = TFTPPacket.SEGMENT_SIZE;
+ int offset = 4;
+ int totalThisPacket = 0;
+ int bytesRead = 0;
+ while (dataLength > 0 && (bytesRead = input.read(sendBuffer, offset, dataLength)) > 0) {
+ offset += bytesRead;
+ dataLength -= bytesRead;
+ totalThisPacket += bytesRead;
+ }
+ if (totalThisPacket < TFTPPacket.SEGMENT_SIZE) {
+ /* this will be our last packet -- send, wait for ack, stop */
+ lastAckWait = true;
+ }
+ data.setBlockNumber(block);
+ data.setData(sendBuffer, 4, totalThisPacket);
+ sent = data;
+ totalBytesSent += totalThisPacket;
+ } while (true); // loops until after lastAckWait is set
+ } finally {
+ endBufferedOps();
+ }
+ }
+
+ /**
+ * Same as calling sendFile(fileName, mode, input, hostname, TFTP.DEFAULT_PORT).
+ *
+ * @param fileName The name the remote server should use when creating the file on its file system.
+ * @param mode The TFTP mode of the transfer (one of the MODE constants).
+ * @param input the input stream containing the data to be sent
+ * @param hostname The name of the remote host receiving the file.
+ * @throws IOException If an I/O error occurs. The nature of the error will be reported in the message.
+ * @throws UnknownHostException If the hostname cannot be resolved.
+ */
+ public void sendFile(final String fileName, final int mode, final InputStream input, final String hostname) throws UnknownHostException, IOException {
+ sendFile(fileName, mode, input, InetAddress.getByName(hostname), DEFAULT_PORT);
+ }
+
+ /**
+ * Requests to send a file to a remote host, reads the file from an InputStream, sends the file to the remote host, and closes the connection. A local UDP
+ * socket must first be created by {@link org.apache.commons.net.DatagramSocketClient#open open()} before invoking this method. This method will not close
+ * the InputStream containing the file; you must close it after the method invocation.
+ *
+ * @param fileName The name the remote server should use when creating the file on its file system.
+ * @param mode The TFTP mode of the transfer (one of the MODE constants).
+ * @param input the input stream containing the data to be sent
+ * @param hostname The name of the remote host receiving the file.
+ * @param port The port number of the remote TFTP server.
+ * @throws IOException If an I/O error occurs. The nature of the error will be reported in the message.
+ * @throws UnknownHostException If the hostname cannot be resolved.
+ */
+ public void sendFile(final String fileName, final int mode, final InputStream input, final String hostname, final int port)
+ throws UnknownHostException, IOException {
+ sendFile(fileName, mode, input, InetAddress.getByName(hostname), port);
+ }
+
+ /**
+ * Sets the maximum number of times a {@code receive} attempt is allowed to timeout during a receiveFile() or sendFile() operation before ending attempts to
+ * retry the {@code receive} and failing. The default is DEFAULT_MAX_TIMEOUTS.
+ *
+ * @param numTimeouts The maximum number of timeouts to allow. Values less than 1 should not be used, but if they are, they are treated as 1.
+ */
+ public void setMaxTimeouts(final int numTimeouts) {
+ maxTimeouts = Math.max(numTimeouts, 1);
+ }
+}
diff --git a/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPDataPacket.java b/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPDataPacket.java
new file mode 100644
index 0000000..afc3d53
--- /dev/null
+++ b/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPDataPacket.java
@@ -0,0 +1,235 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package pl.psobiech.opengr8on.org.apache.commons.net;
+
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+
+/**
+ * A final class derived from TFTPPacket defining the TFTP Data packet type.
+ *
+ * Details regarding the TFTP protocol and the format of TFTP packets can be found in RFC 783. But the point of these classes is to keep you from having to
+ * worry about the internals. Additionally, only very few people should have to care about any of the TFTPPacket classes or derived classes. Almost all users
+ * should only be concerned with the {@link TFTPClient} class {@link TFTPClient#receiveFile receiveFile()} and {@link TFTPClient#sendFile sendFile()} methods.
+ *
+ * @see TFTPPacket
+ * @see TFTPPacketException
+ * @see TFTP
+ */
+public final class TFTPDataPacket extends TFTPPacket {
+ /**
+ * The maximum number of bytes in a TFTP data packet (512)
+ */
+ public static final int MAX_DATA_LENGTH = 512;
+
+ /**
+ * The minimum number of bytes in a TFTP data packet (0)
+ */
+ public static final int MIN_DATA_LENGTH = 0;
+
+ /**
+ * The block number of the packet.
+ */
+ int blockNumber;
+
+ /**
+ * The length of the data.
+ */
+ private int length;
+
+ /**
+ * The offset into the _data array at which the data begins.
+ */
+ private int offset;
+
+ /**
+ * The data stored in the packet.
+ */
+ private byte[] data;
+
+ /**
+ * Creates a data packet based from a received datagram. Assumes the datagram is at least length 4, else an ArrayIndexOutOfBoundsException may be thrown.
+ *
+ * @param datagram The datagram containing the received data.
+ * @throws TFTPPacketException If the datagram isn't a valid TFTP data packet.
+ */
+ TFTPDataPacket(final DatagramPacket datagram) throws TFTPPacketException {
+ super(DATA, datagram.getAddress(), datagram.getPort());
+
+ this.data = datagram.getData();
+ this.offset = 4;
+
+ if (getType() != this.data[1]) {
+ throw new TFTPPacketException("TFTP operator code does not match type.");
+ }
+
+ this.blockNumber = (this.data[2] & 0xff) << 8 | this.data[3] & 0xff;
+
+ this.length = datagram.getLength() - 4;
+
+ if (this.length > MAX_DATA_LENGTH) {
+ this.length = MAX_DATA_LENGTH;
+ }
+ }
+
+ public TFTPDataPacket(final InetAddress destination, final int port, final int blockNumber, final byte[] data) {
+ this(destination, port, blockNumber, data, 0, data.length);
+ }
+
+ /**
+ * Creates a data packet to be sent to a host at a given port with a given block number. The actual data to be sent is passed as an array, an offset, and a
+ * length. The offset is the offset into the byte array where the data starts. The length is the length of the data. If the length is greater than
+ * MAX_DATA_LENGTH, it is truncated.
+ *
+ * @param destination The host to which the packet is going to be sent.
+ * @param port The port to which the packet is going to be sent.
+ * @param blockNumber The block number of the data.
+ * @param data The byte array containing the data.
+ * @param offset The offset into the array where the data starts.
+ * @param length The length of the data.
+ */
+ public TFTPDataPacket(final InetAddress destination, final int port, final int blockNumber, final byte[] data, final int offset, final int length) {
+ super(DATA, destination, port);
+
+ this.blockNumber = blockNumber;
+ this.data = data;
+ this.offset = offset;
+
+ this.length = Math.min(length, MAX_DATA_LENGTH);
+ }
+
+ /**
+ * Returns the block number of the data packet.
+ *
+ * @return The block number of the data packet.
+ */
+ public int getBlockNumber() {
+ return blockNumber;
+ }
+
+ /**
+ * Returns the byte array containing the packet data.
+ *
+ * @return The byte array containing the packet data.
+ */
+ public byte[] getData() {
+ return data;
+ }
+
+ /**
+ * Returns the length of the data part of the data packet.
+ *
+ * @return The length of the data part of the data packet.
+ */
+ public int getDataLength() {
+ return length;
+ }
+
+ /**
+ * Returns the offset into the byte array where the packet data actually starts.
+ *
+ * @return The offset into the byte array where the packet data actually starts.
+ */
+ public int getDataOffset() {
+ return offset;
+ }
+
+ /**
+ * Creates a UDP datagram containing all the TFTP data packet data in the proper format. This is a method exposed to the programmer in case he wants to
+ * implement his own TFTP client instead of using the {@link TFTPClient} class. Under normal circumstances, you should not have a need to call this method.
+ *
+ * @return A UDP datagram containing the TFTP data packet.
+ */
+ @Override
+ public DatagramPacket newDatagram() {
+ final byte[] data;
+
+ data = new byte[length + 4];
+ data[0] = 0;
+ data[1] = (byte) type;
+ data[2] = (byte) ((blockNumber & 0xffff) >> 8);
+ data[3] = (byte) (blockNumber & 0xff);
+
+ System.arraycopy(this.data, offset, data, 4, length);
+
+ return new DatagramPacket(data, length + 4, address, port);
+ }
+
+ /**
+ * This is a method only available within the package for implementing efficient datagram transport by eliminating buffering. It takes a datagram as an
+ * argument, and a byte buffer in which to store the raw datagram data. Inside the method, the data is set as the datagram's data and the datagram
+ * returned.
+ *
+ * @param datagram The datagram to create.
+ * @param data The buffer to store the packet and to use in the datagram.
+ * @return The datagram argument.
+ */
+ @Override
+ DatagramPacket newDatagram(final DatagramPacket datagram, final byte[] data) {
+ data[0] = 0;
+ data[1] = (byte) type;
+ data[2] = (byte) ((blockNumber & 0xffff) >> 8);
+ data[3] = (byte) (blockNumber & 0xff);
+
+ // Double-check we're not the same
+ if (data != this.data) {
+ System.arraycopy(this.data, offset, data, 4, length);
+ }
+
+ datagram.setAddress(address);
+ datagram.setPort(port);
+ datagram.setData(data);
+ datagram.setLength(length + 4);
+
+ return datagram;
+ }
+
+ /**
+ * Sets the block number of the data packet.
+ *
+ * @param blockNumber the number to set
+ */
+ public void setBlockNumber(final int blockNumber) {
+ this.blockNumber = blockNumber;
+ }
+
+ /**
+ * Sets the data for the data packet.
+ *
+ * @param data The byte array containing the data.
+ * @param offset The offset into the array where the data starts.
+ * @param length The length of the data.
+ */
+ public void setData(final byte[] data, final int offset, final int length) {
+ this.data = data;
+ this.offset = offset;
+ this.length = length;
+
+ this.length = Math.min(length, MAX_DATA_LENGTH);
+ }
+
+ /**
+ * For debugging
+ *
+ * @since 3.6
+ */
+ @Override
+ public String toString() {
+ return super.toString() + " DATA " + blockNumber + " " + length;
+ }
+}
diff --git a/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPErrorPacket.java b/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPErrorPacket.java
new file mode 100644
index 0000000..f63707d
--- /dev/null
+++ b/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPErrorPacket.java
@@ -0,0 +1,222 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package pl.psobiech.opengr8on.org.apache.commons.net;
+
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+
+/**
+ * A final class derived from TFTPPacket defining the TFTP Error packet type.
+ *
+ * Details regarding the TFTP protocol and the format of TFTP packets can be found in RFC 783. But the point of these classes is to keep you from having to
+ * worry about the internals. Additionally, only very few people should have to care about any of the TFTPPacket classes or derived classes. Almost all users
+ * should only be concerned with the {@link TFTPClient} class {@link TFTPClient#receiveFile receiveFile()} and {@link TFTPClient#sendFile sendFile()} methods.
+ *
+ * @see TFTPPacket
+ * @see TFTPPacketException
+ * @see TFTP
+ */
+public final class TFTPErrorPacket extends TFTPPacket {
+ /**
+ * The undefined error code according to RFC 783, value 0.
+ */
+ public static final int UNDEFINED = 0;
+
+ /**
+ * The file not found error code according to RFC 783, value 1.
+ */
+ public static final int FILE_NOT_FOUND = 1;
+
+ /**
+ * The access violation error code according to RFC 783, value 2.
+ */
+ public static final int ACCESS_VIOLATION = 2;
+
+ /**
+ * The disk full error code according to RFC 783, value 3.
+ */
+ public static final int OUT_OF_SPACE = 3;
+
+ /**
+ * The illegal TFTP operation error code according to RFC 783, value 4.
+ */
+ public static final int ILLEGAL_OPERATION = 4;
+
+ /**
+ * The unknown transfer id error code according to RFC 783, value 5.
+ */
+ public static final int UNKNOWN_TID = 5;
+
+ /**
+ * The file already exists error code according to RFC 783, value 6.
+ */
+ public static final int FILE_EXISTS = 6;
+
+ /**
+ * The no such user error code according to RFC 783, value 7.
+ */
+ public static final int NO_SUCH_USER = 7;
+
+ /**
+ * The error code of this packet.
+ */
+ private final int error;
+
+ /**
+ * The error message of this packet.
+ */
+ private final String message;
+
+ /**
+ * Creates an error packet based from a received datagram. Assumes the datagram is at least length 4, else an ArrayIndexOutOfBoundsException may be thrown.
+ *
+ * @param datagram The datagram containing the received error.
+ * @throws TFTPPacketException If the datagram isn't a valid TFTP error packet.
+ */
+ TFTPErrorPacket(final DatagramPacket datagram) throws TFTPPacketException {
+ super(ERROR, datagram.getAddress(), datagram.getPort());
+ int index;
+ final int length;
+ final byte[] data;
+ final StringBuilder buffer;
+
+ data = datagram.getData();
+ length = datagram.getLength();
+
+ if (getType() != data[1]) {
+ throw new TFTPPacketException("TFTP operator code does not match type.");
+ }
+
+ error = (data[2] & 0xff) << 8 | data[3] & 0xff;
+
+ if (length < 5) {
+ throw new TFTPPacketException("Bad error packet. No message.");
+ }
+
+ index = 4;
+ buffer = new StringBuilder();
+
+ while (index < length && data[index] != 0) {
+ buffer.append((char) data[index]);
+ ++index;
+ }
+
+ message = buffer.toString();
+ }
+
+ /**
+ * Creates an error packet to be sent to a host at a given port with an error code and error message.
+ *
+ * @param destination The host to which the packet is going to be sent.
+ * @param port The port to which the packet is going to be sent.
+ * @param error The error code of the packet.
+ * @param message The error message of the packet.
+ */
+ public TFTPErrorPacket(final InetAddress destination, final int port, final int error, final String message) {
+ super(ERROR, destination, port);
+
+ this.error = error;
+ this.message = message;
+ }
+
+ /**
+ * Returns the error code of the packet.
+ *
+ * @return The error code of the packet.
+ */
+ public int getError() {
+ return error;
+ }
+
+ /**
+ * Returns the error message of the packet.
+ *
+ * @return The error message of the packet.
+ */
+ public String getMessage() {
+ return message;
+ }
+
+ /**
+ * Creates a UDP datagram containing all the TFTP error packet data in the proper format. This is a method exposed to the programmer in case he wants to
+ * implement his own TFTP client instead of using the {@link TFTPClient} class. Under normal circumstances, you should not have a need to call this method.
+ *
+ * @return A UDP datagram containing the TFTP error packet.
+ */
+ @Override
+ public DatagramPacket newDatagram() {
+ final byte[] data;
+ final int length;
+
+ length = message.length();
+
+ data = new byte[length + 5];
+ data[0] = 0;
+ data[1] = (byte) type;
+ data[2] = (byte) ((error & 0xffff) >> 8);
+ data[3] = (byte) (error & 0xff);
+
+ System.arraycopy(message.getBytes(), 0, data, 4, length);
+
+ data[length + 4] = 0;
+
+ return new DatagramPacket(data, data.length, address, port);
+ }
+
+ /**
+ * This is a method only available within the package for implementing efficient datagram transport by eliminating buffering. It takes a datagram as an
+ * argument, and a byte buffer in which to store the raw datagram data. Inside the method, the data is set as the datagram's data and the datagram
+ * returned.
+ *
+ * @param datagram The datagram to create.
+ * @param data The buffer to store the packet and to use in the datagram.
+ * @return The datagram argument.
+ */
+ @Override
+ DatagramPacket newDatagram(final DatagramPacket datagram, final byte[] data) {
+ final int length;
+
+ length = message.length();
+
+ data[0] = 0;
+ data[1] = (byte) type;
+ data[2] = (byte) ((error & 0xffff) >> 8);
+ data[3] = (byte) (error & 0xff);
+
+ System.arraycopy(message.getBytes(), 0, data, 4, length);
+
+ data[length + 4] = 0;
+
+ datagram.setAddress(address);
+ datagram.setPort(port);
+ datagram.setData(data);
+ datagram.setLength(length + 4);
+
+ return datagram;
+ }
+
+ /**
+ * For debugging
+ *
+ * @since 3.6
+ */
+ @Override
+ public String toString() {
+ return super.toString() + " ERR " + error + " " + message;
+ }
+}
diff --git a/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPPacket.java b/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPPacket.java
new file mode 100644
index 0000000..d774f37
--- /dev/null
+++ b/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPPacket.java
@@ -0,0 +1,203 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package pl.psobiech.opengr8on.org.apache.commons.net;
+
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+
+/**
+ * TFTPPacket is an abstract class encapsulating the functionality common to the 5 types of TFTP packets. It also provides a static factory method that will
+ * create the correct TFTP packet instance from a datagram. This can relieve the programmer from having to figure out what kind of TFTP packet is contained in a
+ * datagram and create it himself.
+ *
+ * Details regarding the TFTP protocol and the format of TFTP packets can be found in RFC 783. But the point of these classes is to keep you from having to
+ * worry about the internals. Additionally, only very few people should have to care about any of the TFTPPacket classes or derived classes. Almost all users
+ * should only be concerned with the {@link TFTPClient} class {@link TFTPClient#receiveFile receiveFile()} and {@link TFTPClient#sendFile sendFile()} methods.
+ *
+ * @see TFTPPacketException
+ * @see TFTP
+ */
+public abstract class TFTPPacket {
+ /**
+ * The minimum size of a packet. This is 4 bytes. It is enough to store the opcode and block number or other required data depending on the packet type.
+ */
+ static final int MIN_PACKET_SIZE = 4;
+
+ /**
+ * This is the actual TFTP spec identifier and is equal to 1. Identifier returned by {@link #getType getType()} indicating a read request packet.
+ */
+ public static final int READ_REQUEST = 1;
+
+ /**
+ * This is the actual TFTP spec identifier and is equal to 2. Identifier returned by {@link #getType getType()} indicating a write request packet.
+ */
+ public static final int WRITE_REQUEST = 2;
+
+ /**
+ * This is the actual TFTP spec identifier and is equal to 3. Identifier returned by {@link #getType getType()} indicating a data packet.
+ */
+ public static final int DATA = 3;
+
+ /**
+ * This is the actual TFTP spec identifier and is equal to 4. Identifier returned by {@link #getType getType()} indicating an acknowledgement packet.
+ */
+ public static final int ACKNOWLEDGEMENT = 4;
+
+ /**
+ * This is the actual TFTP spec identifier and is equal to 5. Identifier returned by {@link #getType getType()} indicating an error packet.
+ */
+ public static final int ERROR = 5;
+
+ /**
+ * The TFTP data packet maximum segment size in bytes. This is 512 and is useful for those familiar with the TFTP protocol who want to use the {@link TFTP}
+ * class methods to implement their own TFTP servers or clients.
+ */
+ public static final int SEGMENT_SIZE = 512;
+
+ /**
+ * The type of packet.
+ */
+ final int type;
+
+ /**
+ * The port the packet came from or is going to.
+ */
+ int port;
+
+ /**
+ * The host the packet is going to be sent or where it came from.
+ */
+ InetAddress address;
+
+ /**
+ * This constructor is not visible outside the package. It is used by subclasses within the package to initialize base data.
+ *
+ * @param type The type of the packet.
+ * @param address The host the packet came from or is going to be sent.
+ * @param port The port the packet came from or is going to be sent.
+ **/
+ TFTPPacket(final int type, final InetAddress address, final int port) {
+ this.type = type;
+ this.address = address;
+ this.port = port;
+ }
+
+ /**
+ * When you receive a datagram that you expect to be a TFTP packet, you use this factory method to create the proper TFTPPacket object encapsulating the
+ * data contained in that datagram. This method is the only way you can instantiate a TFTPPacket derived class from a datagram.
+ *
+ * @param datagram The datagram containing a TFTP packet.
+ * @return The TFTPPacket object corresponding to the datagram.
+ * @throws TFTPPacketException If the datagram does not contain a valid TFTP packet.
+ */
+ public static TFTPPacket newTFTPPacket(final DatagramPacket datagram) throws TFTPPacketException {
+ final byte[] data;
+ TFTPPacket packet;
+
+ if (datagram.getLength() < MIN_PACKET_SIZE) {
+ throw new TFTPPacketException("Bad packet. Datagram data length is too short.");
+ }
+
+ data = datagram.getData();
+ packet = switch (data[1]) {
+ case READ_REQUEST -> new TFTPReadRequestPacket(datagram);
+ case WRITE_REQUEST -> new TFTPWriteRequestPacket(datagram);
+ case DATA -> new TFTPDataPacket(datagram);
+ case ACKNOWLEDGEMENT -> new TFTPAckPacket(datagram);
+ case ERROR -> new TFTPErrorPacket(datagram);
+ default -> throw new TFTPPacketException("Bad packet. Invalid TFTP operator code.");
+ };
+
+ return packet;
+ }
+
+ /**
+ * Returns the address of the host where the packet is going to be sent or where it came from.
+ *
+ * @return The type of the packet.
+ */
+ public final InetAddress getAddress() {
+ return address;
+ }
+
+ /**
+ * Returns the port where the packet is going to be sent or where it came from.
+ *
+ * @return The port where the packet came from or where it is going.
+ */
+ public final int getPort() {
+ return port;
+ }
+
+ /**
+ * Returns the type of the packet.
+ *
+ * @return The type of the packet.
+ */
+ public final int getType() {
+ return type;
+ }
+
+ /**
+ * Creates a UDP datagram containing all the TFTP packet data in the proper format. This is an abstract method, exposed to the programmer in case he wants
+ * to implement his own TFTP client instead of using the {@link TFTPClient} class. Under normal circumstances, you should not have a need to call this
+ * method.
+ *
+ * @return A UDP datagram containing the TFTP packet.
+ */
+ public abstract DatagramPacket newDatagram();
+
+ /**
+ * This is an abstract method only available within the package for implementing efficient datagram transport by eliminating buffering. It takes a datagram
+ * as an argument, and a byte buffer in which to store the raw datagram data. Inside the method, the data should be set as the datagram's data and the
+ * datagram returned.
+ *
+ * @param datagram The datagram to create.
+ * @param data The buffer to store the packet and to use in the datagram.
+ * @return The datagram argument.
+ */
+ abstract DatagramPacket newDatagram(DatagramPacket datagram, byte[] data);
+
+ /**
+ * Sets the host address where the packet is going to be sent.
+ *
+ * @param address the address to set
+ */
+ public final void setAddress(final InetAddress address) {
+ this.address = address;
+ }
+
+ /**
+ * Sets the port where the packet is going to be sent.
+ *
+ * @param port the port to set
+ */
+ public final void setPort(final int port) {
+ this.port = port;
+ }
+
+ /**
+ * For debugging
+ *
+ * @since 3.6
+ */
+ @Override
+ public String toString() {
+ return address + " " + port + " " + type;
+ }
+}
diff --git a/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPPacketException.java b/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPPacketException.java
new file mode 100644
index 0000000..f5a2fca
--- /dev/null
+++ b/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPPacketException.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package pl.psobiech.opengr8on.org.apache.commons.net;
+
+/**
+ * A class used to signify the occurrence of an error in the creation of a TFTP packet. It is not declared final so that it may be subclassed to identify more
+ * specific errors. You would only want to do this if you were building your own TFTP client or server on top of the {@link TFTP} class if you wanted more
+ * functionality than the {@link TFTPClient#receiveFile receiveFile()} and {@link TFTPClient#sendFile sendFile()} methods provide.
+ *
+ * @see TFTPPacket
+ * @see TFTP
+ */
+public class TFTPPacketException extends Exception {
+ private static final long serialVersionUID = -8596448819569308689L;
+
+ /**
+ * Simply calls the corresponding constructor of its superclass.
+ *
+ * @param message the message
+ */
+ public TFTPPacketException(final String message) {
+ super(message);
+ }
+}
diff --git a/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPPacketIOException.java b/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPPacketIOException.java
new file mode 100644
index 0000000..1062166
--- /dev/null
+++ b/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPPacketIOException.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package pl.psobiech.opengr8on.org.apache.commons.net;
+
+import java.io.IOException;
+
+/**
+ *
+ */
+public class TFTPPacketIOException extends IOException {
+ private static final long serialVersionUID = 7959893294993558186L;
+
+ private final int errorPacketCode;
+
+ public TFTPPacketIOException(final int errorPacketCode, final IOException exception) {
+ super(exception);
+
+ this.errorPacketCode = errorPacketCode;
+ }
+
+ public TFTPPacketIOException(final int errorPacketCode, final String message) {
+ super(message);
+
+ this.errorPacketCode = errorPacketCode;
+ }
+
+ public int getErrorPacketCode() {
+ return errorPacketCode;
+ }
+}
diff --git a/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPReadRequestPacket.java b/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPReadRequestPacket.java
new file mode 100644
index 0000000..71e156e
--- /dev/null
+++ b/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPReadRequestPacket.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package pl.psobiech.opengr8on.org.apache.commons.net;
+
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+
+/**
+ * A class derived from TFTPRequestPacket defining a TFTP read request packet type.
+ *
+ * Details regarding the TFTP protocol and the format of TFTP packets can be found in RFC 783. But the point of these classes is to keep you from having to
+ * worry about the internals. Additionally, only very few people should have to care about any of the TFTPPacket classes or derived classes. Almost all users
+ * should only be concerned with the {@link TFTPClient} class {@link TFTPClient#receiveFile receiveFile()} and {@link TFTPClient#sendFile sendFile()} methods.
+ *
+ * @see TFTPPacket
+ * @see TFTPRequestPacket
+ * @see TFTPPacketException
+ * @see TFTP
+ */
+public final class TFTPReadRequestPacket extends TFTPRequestPacket {
+ /**
+ * Creates a read request packet of based on a received datagram and assumes the datagram has already been identified as a read request. Assumes the
+ * datagram is at least length 4, else an ArrayIndexOutOfBoundsException may be thrown.
+ *
+ * @param datagram The datagram containing the received request.
+ * @throws TFTPPacketException If the datagram isn't a valid TFTP request packet.
+ */
+ TFTPReadRequestPacket(final DatagramPacket datagram) throws TFTPPacketException {
+ super(READ_REQUEST, datagram);
+ }
+
+ /**
+ * Creates a read request packet to be sent to a host at a given port with a file name and transfer mode request.
+ *
+ * @param destination The host to which the packet is going to be sent.
+ * @param port The port to which the packet is going to be sent.
+ * @param fileName The requested file name.
+ * @param mode The requested transfer mode. This should be on of the TFTP class MODE constants (e.g., TFTP.NETASCII_MODE).
+ */
+ public TFTPReadRequestPacket(final InetAddress destination, final int port, final String fileName, final int mode) {
+ super(destination, port, READ_REQUEST, fileName, mode);
+ }
+
+ /**
+ * For debugging
+ *
+ * @since 3.6
+ */
+ @Override
+ public String toString() {
+ return super.toString() + " RRQ " + getFilename() + " " + TFTP.getModeName(getMode());
+ }
+}
diff --git a/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPRequestPacket.java b/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPRequestPacket.java
new file mode 100644
index 0000000..a28b081
--- /dev/null
+++ b/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPRequestPacket.java
@@ -0,0 +1,211 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package pl.psobiech.opengr8on.org.apache.commons.net;
+
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+
+/**
+ * An abstract class derived from TFTPPacket defining a TFTP Request packet type. It is subclassed by the {@link TFTPReadRequestPacket} and
+ * {@link TFTPWriteRequestPacket} classes.
+ *
+ * Details regarding the TFTP protocol and the format of TFTP packets can be found in RFC 783. But the point of these classes is to keep you from having to
+ * worry about the internals. Additionally, only very few people should have to care about any of the TFTPPacket classes or derived classes. Almost all users
+ * should only be concerned with the {@link TFTPClient} class {@link TFTPClient#receiveFile receiveFile()} and {@link TFTPClient#sendFile sendFile()} methods.
+ *
+ * @see TFTPPacket
+ * @see TFTPReadRequestPacket
+ * @see TFTPWriteRequestPacket
+ * @see TFTPPacketException
+ * @see TFTP
+ */
+public abstract class TFTPRequestPacket extends TFTPPacket {
+ /**
+ * An array containing the string names of the transfer modes and indexed by the transfer mode constants.
+ */
+ public static final String[] modeStrings = {"netascii", "octet"};
+
+ /**
+ * A null terminated byte array representation of the ascii names of the transfer mode constants. This is convenient for creating the TFTP request packets.
+ */
+ private static final byte[][] modeBytes = {
+ {(byte) 'n', (byte) 'e', (byte) 't', (byte) 'a', (byte) 's', (byte) 'c', (byte) 'i', (byte) 'i', 0},
+ {(byte) 'o', (byte) 'c', (byte) 't', (byte) 'e', (byte) 't', 0}
+ };
+
+ /**
+ * The transfer mode of the request.
+ */
+ private final int mode;
+
+ /**
+ * The file name of the request.
+ */
+ private final String fileName;
+
+ /**
+ * Creates a request packet of a given type to be sent to a host at a given port with a file name and transfer mode request.
+ *
+ * @param destination The host to which the packet is going to be sent.
+ * @param port The port to which the packet is going to be sent.
+ * @param type The type of the request (either TFTPPacket.READ_REQUEST or TFTPPacket.WRITE_REQUEST).
+ * @param fileName The requested file name.
+ * @param mode The requested transfer mode. This should be on of the TFTP class MODE constants (e.g., TFTP.NETASCII_MODE).
+ */
+ TFTPRequestPacket(final InetAddress destination, final int port, final int type, final String fileName, final int mode) {
+ super(type, destination, port);
+
+ this.fileName = fileName;
+ this.mode = mode;
+ }
+
+ /**
+ * Creates a request packet of a given type based on a received datagram. Assumes the datagram is at least length 4, else an ArrayIndexOutOfBoundsException
+ * may be thrown.
+ *
+ * @param type The type of the request (either TFTPPacket.READ_REQUEST or TFTPPacket.WRITE_REQUEST).
+ * @param datagram The datagram containing the received request.
+ * @throws TFTPPacketException If the datagram isn't a valid TFTP request packet of the appropriate type.
+ */
+ TFTPRequestPacket(final int type, final DatagramPacket datagram) throws TFTPPacketException {
+ super(type, datagram.getAddress(), datagram.getPort());
+
+ final byte[] data = datagram.getData();
+
+ if (getType() != data[1]) {
+ throw new TFTPPacketException("TFTP operator code does not match type.");
+ }
+
+ final StringBuilder buffer = new StringBuilder();
+
+ int index = 2;
+ int length = datagram.getLength();
+
+ while (index < length && data[index] != 0) {
+ buffer.append((char) data[index]);
+ ++index;
+ }
+
+ this.fileName = buffer.toString();
+
+ if (index >= length) {
+ throw new TFTPPacketException("Bad file name and mode format.");
+ }
+
+ buffer.setLength(0);
+ ++index; // need to advance beyond the end of string marker
+ while (index < length && data[index] != 0) {
+ buffer.append((char) data[index]);
+ ++index;
+ }
+
+ final String modeString = buffer.toString().toLowerCase(java.util.Locale.ENGLISH);
+ length = modeStrings.length;
+
+ int mode = 0;
+ for (index = 0; index < length; index++) {
+ if (modeString.equals(modeStrings[index])) {
+ mode = index;
+ break;
+ }
+ }
+
+ this.mode = mode;
+
+ if (index >= length) {
+ throw new TFTPPacketException("Unrecognized TFTP transfer mode: " + modeString);
+ // May just want to default to binary mode instead of throwing
+ // exception.
+ // _mode = TFTP.OCTET_MODE;
+ }
+ }
+
+ /**
+ * Returns the requested file name.
+ *
+ * @return The requested file name.
+ */
+ public final String getFilename() {
+ return fileName;
+ }
+
+ /**
+ * Returns the transfer mode of the request.
+ *
+ * @return The transfer mode of the request.
+ */
+ public final int getMode() {
+ return mode;
+ }
+
+ /**
+ * Creates a UDP datagram containing all the TFTP request packet data in the proper format. This is a method exposed to the programmer in case he wants to
+ * implement his own TFTP client instead of using the {@link TFTPClient} class. Under normal circumstances, you should not have a need to call this method.
+ *
+ * @return A UDP datagram containing the TFTP request packet.
+ */
+ @Override
+ public final DatagramPacket newDatagram() {
+ final int fileLength;
+ final int modeLength;
+ final byte[] data;
+
+ fileLength = fileName.length();
+ modeLength = modeBytes[mode].length;
+
+ data = new byte[fileLength + modeLength + 4];
+ data[0] = 0;
+ data[1] = (byte) type;
+ System.arraycopy(fileName.getBytes(), 0, data, 2, fileLength);
+ data[fileLength + 2] = 0;
+ System.arraycopy(modeBytes[mode], 0, data, fileLength + 3, modeLength);
+
+ return new DatagramPacket(data, data.length, address, port);
+ }
+
+ /**
+ * This is a method only available within the package for implementing efficient datagram transport by eliminating buffering. It takes a datagram as an
+ * argument, and a byte buffer in which to store the raw datagram data. Inside the method, the data is set as the datagram's data and the datagram
+ * returned.
+ *
+ * @param datagram The datagram to create.
+ * @param data The buffer to store the packet and to use in the datagram.
+ * @return The datagram argument.
+ */
+ @Override
+ final DatagramPacket newDatagram(final DatagramPacket datagram, final byte[] data) {
+ final int fileLength;
+ final int modeLength;
+
+ fileLength = fileName.length();
+ modeLength = modeBytes[mode].length;
+
+ data[0] = 0;
+ data[1] = (byte) type;
+ System.arraycopy(fileName.getBytes(), 0, data, 2, fileLength);
+ data[fileLength + 2] = 0;
+ System.arraycopy(modeBytes[mode], 0, data, fileLength + 3, modeLength);
+
+ datagram.setAddress(address);
+ datagram.setPort(port);
+ datagram.setData(data);
+ datagram.setLength(fileLength + modeLength + 3);
+
+ return datagram;
+ }
+}
diff --git a/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPServer.java b/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPServer.java
new file mode 100644
index 0000000..ac50b1c
--- /dev/null
+++ b/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPServer.java
@@ -0,0 +1,703 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package pl.psobiech.opengr8on.org.apache.commons.net;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketTimeoutException;
+import java.time.Duration;
+import java.util.Enumeration;
+import java.util.HashSet;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.net.io.FromNetASCIIOutputStream;
+import org.apache.commons.net.io.ToNetASCIIInputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A fully multi-threaded TFTP server. Can handle multiple clients at the same time. Implements RFC 1350 and wrapping block numbers for large file support. To
+ * launch, just create an instance of the class. An IOException will be thrown if the server fails to start for reasons such as port in use, port denied, etc.
+ * To stop, use the shutdown method. To check to see if the server is still running (or if it stopped because of an error), call the isRunning() method. By
+ * default, events are not logged to stdout/stderr. This can be changed with the setLog and setLogError methods.
+ *
+ *
+ * Example usage is below:
+ *
+ * public static void main(String[] args) throws Exception { if (args.length != 1) { System.out.println("You must provide 1 argument - the base path for the
+ * server to serve from."); System.exit(1); } try (TFTPServer ts = new TFTPServer(new File(args[0]), new File(args[0]), GET_AND_PUT)) {
+ * ts.setSocketTimeout(2000); System.out.println("TFTP Server running. Press enter to stop."); new InputStreamReader(System.in).read(); }
+ * System.out.println("Server shut down."); System.exit(0); }
+ *
+ *
+ * @since 2.0
+ */
+public class TFTPServer implements Runnable, AutoCloseable {
+ private static final Logger LOGGER = LoggerFactory.getLogger(TFTPServer.class);
+
+ private final HashSet transfers = new HashSet<>();
+
+ private final int port;
+
+ private final InetAddress localAddress;
+
+ private final ServerMode mode;
+
+ private volatile boolean shutdownServer;
+
+ private TFTP serverTftp;
+
+ private File serverDirectory;
+
+ private Exception serverException;
+
+ private int maxTimeoutRetries = 3;
+
+ private int socketTimeout;
+
+ private Thread serverThread;
+
+ /**
+ * Start a TFTP Server on the specified port. Gets and Puts occur in the specified directory.
+ *
+ * The server will start in another thread, allowing this constructor to return immediately.
+ *
+ * If a get or a put comes in with a relative path that tries to get outside the serverDirectory, then the get or put will be denied.
+ *
+ * GET_ONLY mode only allows gets, PUT_ONLY mode only allows puts, and GET_AND_PUT allows both. Modes are defined as int constants in this class.
+ *
+ * @param port The local port to bind to.
+ * @param localAddress The local address to bind to.
+ * @param mode A value as specified above.
+ * @throws IOException if the server directory is invalid or does not exist.
+ */
+ public TFTPServer(
+ final int port, final InetAddress localAddress, final ServerMode mode
+ ) throws IOException {
+ this.port = port;
+ this.mode = mode;
+ this.localAddress = localAddress;
+ }
+
+ /**
+ * Start a TFTP Server on the specified port. Gets and Puts occur in the specified directory.
+ *
+ * The server will start in another thread, allowing this constructor to return immediately.
+ *
+ * If a get or a put comes in with a relative path that tries to get outside the serverDirectory, then the get or put will be denied.
+ *
+ * GET_ONLY mode only allows gets, PUT_ONLY mode only allows puts, and GET_AND_PUT allows both. Modes are defined as int constants in this class.
+ *
+ * @param port the port to use
+ * @param localiface The local network interface to bind to. The interface's first address wil be used.
+ * @param mode A value as specified above.
+ * @throws IOException if the server directory is invalid or does not exist.
+ */
+ public TFTPServer(
+ final int port, final NetworkInterface localiface, final ServerMode mode
+ ) throws IOException {
+ this.mode = mode;
+ this.port = port;
+ InetAddress inetAddress = null;
+ if (localiface != null) {
+ final Enumeration ifaddrs = localiface.getInetAddresses();
+ if (ifaddrs.hasMoreElements()) {
+ inetAddress = ifaddrs.nextElement();
+ }
+ }
+
+ this.localAddress = inetAddress;
+ }
+
+ /**
+ * Start a TFTP Server on the specified port. Gets and Puts occur in the specified directory.
+ *
+ * The server will start in another thread, allowing this constructor to return immediately.
+ *
+ * If a get or a put comes in with a relative path that tries to get outside the serverDirectory, then the get or put will be denied.
+ *
+ * GET_ONLY mode only allows gets, PUT_ONLY mode only allows puts, and GET_AND_PUT allows both. Modes are defined as int constants in this class.
+ *
+ * @param serverDirectory directory for GET requests
+ * @param port the port to use
+ * @param mode A value as specified above.
+ * @throws IOException if the server directory is invalid or does not exist.
+ */
+ public TFTPServer(
+ final File serverDirectory, final int port, final ServerMode mode
+ ) throws IOException {
+ this.port = port;
+ this.mode = mode;
+ this.localAddress = null;
+
+ start(serverDirectory);
+ }
+
+ /**
+ * Closes the TFTP server (and any currently running transfers) and release all opened network resources.
+ *
+ * @since 3.10.0
+ */
+ @Override
+ public void close() {
+ shutdownServer = true;
+
+ synchronized (transfers) {
+ transfers.forEach(TFTPTransfer::close);
+ }
+
+ try {
+ serverTftp.close();
+ } catch (final RuntimeException e) {
+ // noop
+ }
+
+ if (serverThread != null) {
+ try {
+ serverThread.join();
+ } catch (final InterruptedException e) {
+ // we've done the best we could, return
+ }
+ }
+ }
+
+ /**
+ * Gets the current value for maxTimeoutRetries
+ *
+ * @return the max allowed number of retries
+ */
+ public int getMaxTimeoutRetries() {
+ return maxTimeoutRetries;
+ }
+
+ /**
+ * Gets the server port number
+ *
+ * @return the server port number
+ */
+ public int getPort() {
+ return port;
+ }
+
+ /**
+ * Gets the current socket timeout used during transfers in milliseconds.
+ *
+ * @return the timeout value
+ */
+ public int getSocketTimeout() {
+ return socketTimeout;
+ }
+
+ /**
+ * check if the server thread is still running.
+ *
+ * @return true if running, false if stopped.
+ * @throws Exception throws the exception that stopped the server if the server is stopped from an exception.
+ */
+ public boolean isRunning() throws Exception {
+ if (shutdownServer && serverException != null) {
+ throw serverException;
+ }
+ return !shutdownServer;
+ }
+
+ /*
+ * start the server, throw an error if it can't start.
+ */
+ public void start(final File newServerReadDirectory) throws IOException {
+ LOGGER.debug("Starting TFTP Server on port " + port + ". Server directory: " + newServerReadDirectory + ". Server Mode is " + mode);
+
+ this.serverDirectory = newServerReadDirectory.getCanonicalFile();
+ if (!serverDirectory.exists() || !newServerReadDirectory.isDirectory()) {
+ throw new IOException("The server directory " + this.serverDirectory + " does not exist");
+ }
+
+ serverTftp = new TFTP();
+
+ // This is the value used in response to each client.
+ socketTimeout = serverTftp.getDefaultTimeout();
+
+ // we want the server thread to listen forever.
+ serverTftp.setDefaultTimeout(Duration.ZERO);
+
+ if (localAddress == null) {
+ serverTftp.open(port);
+ } else {
+ serverTftp.open(port, localAddress);
+ }
+
+ serverThread = new Thread(this);
+ serverThread.setDaemon(true);
+ serverThread.start();
+ }
+
+ /*
+ * Allow test code to customise the TFTP instance
+ */
+ TFTP newTFTP() {
+ return new TFTP();
+ }
+
+ @Override
+ public void run() {
+ try {
+ while (!shutdownServer && !Thread.interrupted()) {
+ final TFTPPacket tftpPacket;
+
+ tftpPacket = serverTftp.receive();
+
+ final TFTPTransfer tt = new TFTPTransfer(tftpPacket);
+ synchronized (transfers) {
+ transfers.add(tt);
+ }
+
+ final Thread thread = new Thread(tt);
+ thread.setDaemon(true);
+ thread.start();
+ }
+ } catch (Exception e) {
+ if (!shutdownServer) {
+ serverException = e;
+ LOGGER.error("Unexpected Error in TFTP Server - Server shut down! + ", e);
+ }
+ } finally {
+ shutdownServer = true; // set this to true, so the launching thread can check to see if it started.
+ if (serverTftp != null && serverTftp.isOpen()) {
+ serverTftp.close();
+ }
+ }
+ }
+
+ /*
+ * Also allow customisation of sending data/ack so can generate errors if needed
+ */
+ void sendData(final TFTP tftp, final TFTPPacket data) throws IOException {
+ tftp.bufferedSend(data);
+ }
+
+ /**
+ * Set the max number of retries in response to a timeout. Default 3. Min 0.
+ *
+ * @param retries number of retries, must be > 0
+ * @throws IllegalArgumentException if {@code retries} is less than 0.
+ */
+ public void setMaxTimeoutRetries(final int retries) {
+ if (retries < 0) {
+ throw new IllegalArgumentException("Invalid Value");
+ }
+ maxTimeoutRetries = retries;
+ }
+
+ /**
+ * Set the socket timeout in milliseconds used in transfers.
+ *
+ * Defaults to the value {@link TFTP#DEFAULT_TIMEOUT_DURATION}. Minimum value of 10.
+ *
+ *
+ * @param timeout the timeout; must be equal to or larger than 10.
+ * @throws IllegalArgumentException if {@code timeout} is less than 10.
+ */
+ public void setSocketTimeout(final int timeout) {
+ if (timeout < 10) {
+ throw new IllegalArgumentException("Invalid Value");
+ }
+ socketTimeout = timeout;
+ }
+
+ public enum ServerMode {
+ GET_ONLY,
+ PUT_ONLY,
+ GET_AND_PUT,
+ GET_AND_REPLACE
+ //
+ ;
+ }
+
+ /*
+ * An ongoing transfer.
+ */
+ private class TFTPTransfer implements Runnable {
+ private final TFTPPacket tftpPacket;
+
+ private boolean shutdownTransfer;
+
+ TFTP transferTftp;
+
+ public TFTPTransfer(final TFTPPacket tftpPacket) {
+ this.tftpPacket = tftpPacket;
+ }
+
+ @Override
+ public void run() {
+ try {
+ transferTftp = newTFTP();
+
+ transferTftp.beginBufferedOps();
+ transferTftp.setDefaultTimeout(Duration.ofMillis(socketTimeout));
+
+ transferTftp.open();
+
+ if (tftpPacket instanceof TFTPReadRequestPacket) {
+ handleRead((TFTPReadRequestPacket) tftpPacket);
+ } else if (tftpPacket instanceof TFTPWriteRequestPacket) {
+ handleWrite((TFTPWriteRequestPacket) tftpPacket);
+ } else {
+ LOGGER.debug("Unsupported TFTP request (" + tftpPacket + ") - ignored.");
+ }
+ } catch (final Exception e) {
+ if (!shutdownTransfer) {
+ LOGGER.error("Unexpected Error in during TFTP file transfer. Transfer aborted. ", e);
+ }
+ } finally {
+ try {
+ if (transferTftp != null && transferTftp.isOpen()) {
+ transferTftp.endBufferedOps();
+ transferTftp.close();
+ }
+ } catch (final Exception e) {
+ // noop
+ }
+ synchronized (transfers) {
+ transfers.remove(this);
+ }
+ }
+ }
+
+ /*
+ * Creates subdirectories recursively.
+ */
+ private void createDirectory(final File file) throws IOException {
+ final File parent = file.getParentFile();
+ if (parent == null) {
+ throw new IOException("Unexpected error creating requested directory");
+ }
+ if (!parent.exists()) {
+ // recurse...
+ createDirectory(parent);
+ }
+
+ if (!parent.isDirectory()) {
+ throw new IOException("Invalid directory path - file in the way of requested folder");
+ }
+ if (file.isDirectory()) {
+ return;
+ }
+ final boolean result = file.mkdir();
+ if (!result) {
+ throw new IOException("Couldn't create requested directory");
+ }
+ }
+
+ /*
+ * Handles a tftp read request.
+ */
+ private void handleRead(final TFTPReadRequestPacket trrp) throws IOException, TFTPPacketException {
+ if (mode == ServerMode.PUT_ONLY) {
+ transferTftp
+ .bufferedSend(new TFTPErrorPacket(trrp.getAddress(), trrp.getPort(), TFTPErrorPacket.ILLEGAL_OPERATION, "Read not allowed by server."));
+ return;
+ }
+ InputStream inputStream = null;
+ try {
+ try {
+ inputStream = new BufferedInputStream(new FileInputStream(buildSafeFile(serverDirectory, trrp.getFilename(), false)));
+ } catch (final FileNotFoundException e) {
+ transferTftp.bufferedSend(new TFTPErrorPacket(trrp.getAddress(), trrp.getPort(), TFTPErrorPacket.FILE_NOT_FOUND, e.getMessage()));
+ return;
+ } catch (final Exception e) {
+ transferTftp.bufferedSend(new TFTPErrorPacket(trrp.getAddress(), trrp.getPort(), TFTPErrorPacket.UNDEFINED, e.getMessage()));
+ return;
+ }
+
+ if (trrp.getMode() == TFTP.NETASCII_MODE) {
+ inputStream = new ToNetASCIIInputStream(inputStream);
+ }
+
+ final byte[] temp = new byte[TFTPDataPacket.MAX_DATA_LENGTH];
+
+ TFTPPacket answer;
+
+ int block = 1;
+ boolean sendNext = true;
+
+ int readLength = TFTPDataPacket.MAX_DATA_LENGTH;
+
+ TFTPDataPacket lastSentData = null;
+
+ // We are reading a file, so when we read less than the
+ // requested bytes, we know that we are at the end of the file.
+ while (readLength == TFTPDataPacket.MAX_DATA_LENGTH && !shutdownTransfer) {
+ if (sendNext) {
+ readLength = inputStream.read(temp);
+ if (readLength == -1) {
+ readLength = 0;
+ }
+
+ lastSentData = new TFTPDataPacket(trrp.getAddress(), trrp.getPort(), block, temp, 0, readLength);
+ sendData(transferTftp, lastSentData); // send the data
+ }
+
+ answer = null;
+
+ int timeoutCount = 0;
+
+ while (!shutdownTransfer && (answer == null || !answer.getAddress().equals(trrp.getAddress()) || answer.getPort() != trrp.getPort())) {
+ // listen for an answer.
+ if (answer != null) {
+ // The answer that we got didn't come from the
+ // expected source, fire back an error, and continue
+ // listening.
+ LOGGER.debug("TFTP Server ignoring message from unexpected source.");
+ transferTftp.bufferedSend(
+ new TFTPErrorPacket(answer.getAddress(), answer.getPort(), TFTPErrorPacket.UNKNOWN_TID, "Unexpected Host or Port"));
+ }
+ try {
+ answer = transferTftp.bufferedReceive();
+ } catch (final SocketTimeoutException e) {
+ if (timeoutCount >= maxTimeoutRetries) {
+ throw e;
+ }
+ // didn't get an ack for this data. need to resend
+ // it.
+ timeoutCount++;
+ transferTftp.bufferedSend(lastSentData);
+ }
+ }
+
+ if (!(answer instanceof final TFTPAckPacket ack)) {
+ if (!shutdownTransfer) {
+ LOGGER.error("Unexpected response from tftp client during transfer (" + answer + "). Transfer aborted.");
+ }
+ break;
+ }
+ // once we get here, we know we have an answer packet
+ // from the correct host.
+ if (ack.getBlockNumber() == block) {
+ // send the next block
+ block++;
+ if (block > 65535) {
+ // wrap the block number
+ block = 0;
+ }
+ sendNext = true;
+ } else {
+ /*
+ * The origional tftp spec would have called on us to resend the previous data here, however, that causes the SAS Syndrome.
+ * http://www.faqs.org/rfcs/rfc1123.html section 4.2.3.1 The modified spec says that we ignore a duplicate ack. If the packet was really
+ * lost, we will time out on receive, and resend the previous data at that point.
+ */
+ sendNext = false;
+ }
+ }
+ } finally {
+ try {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ } catch (final IOException e) {
+ // noop
+ }
+ }
+ }
+
+ /*
+ * handle a TFTP write request.
+ */
+ private void handleWrite(final TFTPWriteRequestPacket twrp) throws IOException, TFTPPacketException {
+ OutputStream bos = null;
+ try {
+ if (mode == ServerMode.GET_ONLY) {
+ transferTftp.bufferedSend(
+ new TFTPErrorPacket(twrp.getAddress(), twrp.getPort(), TFTPErrorPacket.ILLEGAL_OPERATION, "Write not allowed by server."));
+ return;
+ }
+
+ int lastBlock = 0;
+ try {
+ final File temp = buildSafeFile(serverDirectory, twrp.getFilename(), true);
+ if (mode != ServerMode.GET_AND_REPLACE && temp.exists()) {
+ transferTftp.bufferedSend(new TFTPErrorPacket(
+ twrp.getAddress(),
+ twrp.getPort(),
+ TFTPErrorPacket.FILE_EXISTS,
+ "File already exists"
+ ));
+ return;
+ }
+ bos = new BufferedOutputStream(new FileOutputStream(temp));
+
+ if (twrp.getMode() == TFTP.NETASCII_MODE) {
+ bos = new FromNetASCIIOutputStream(bos);
+ }
+ } catch (final Exception e) {
+ transferTftp.bufferedSend(new TFTPErrorPacket(twrp.getAddress(), twrp.getPort(), TFTPErrorPacket.UNDEFINED, e.getMessage()));
+ return;
+ }
+
+ TFTPAckPacket lastSentAck = new TFTPAckPacket(twrp.getAddress(), twrp.getPort(), 0);
+ sendData(transferTftp, lastSentAck); // send the data
+
+ while (true) {
+ // get the response - ensure it is from the right place.
+ TFTPPacket dataPacket = null;
+
+ int timeoutCount = 0;
+
+ while (!shutdownTransfer
+ && (dataPacket == null || !dataPacket.getAddress().equals(twrp.getAddress()) || dataPacket.getPort() != twrp.getPort())) {
+ // listen for an answer.
+ if (dataPacket != null) {
+ // The data that we got didn't come from the
+ // expected source, fire back an error, and continue
+ // listening.
+ LOGGER.debug("TFTP Server ignoring message from unexpected source.");
+ transferTftp.bufferedSend(
+ new TFTPErrorPacket(dataPacket.getAddress(), dataPacket.getPort(), TFTPErrorPacket.UNKNOWN_TID, "Unexpected Host or Port"));
+ }
+
+ try {
+ dataPacket = transferTftp.bufferedReceive();
+ } catch (final SocketTimeoutException e) {
+ if (timeoutCount >= maxTimeoutRetries) {
+ throw e;
+ }
+ // It didn't get our ack. Resend it.
+ transferTftp.bufferedSend(lastSentAck);
+ timeoutCount++;
+ }
+ }
+
+ if (dataPacket instanceof TFTPWriteRequestPacket) {
+ // it must have missed our initial ack. Send another.
+ lastSentAck = new TFTPAckPacket(twrp.getAddress(), twrp.getPort(), 0);
+ transferTftp.bufferedSend(lastSentAck);
+ } else if (dataPacket instanceof TFTPDataPacket) {
+ final int block = ((TFTPDataPacket) dataPacket).getBlockNumber();
+ final byte[] data = ((TFTPDataPacket) dataPacket).getData();
+ final int dataLength = ((TFTPDataPacket) dataPacket).getDataLength();
+ final int dataOffset = ((TFTPDataPacket) dataPacket).getDataOffset();
+
+ if (block > lastBlock || lastBlock == 65535 && block == 0) {
+ // it might resend a data block if it missed our ack
+ // - don't rewrite the block.
+ bos.write(data, dataOffset, dataLength);
+ lastBlock = block;
+ }
+
+ lastSentAck = new TFTPAckPacket(twrp.getAddress(), twrp.getPort(), block);
+ sendData(transferTftp, lastSentAck); // send the data
+ if (dataLength < TFTPDataPacket.MAX_DATA_LENGTH) {
+ // end of stream signal - The tranfer is complete.
+ bos.close();
+
+ // But my ack may be lost - so listen to see if I
+ // need to resend the ack.
+ for (int i = 0; i < maxTimeoutRetries; i++) {
+ try {
+ dataPacket = transferTftp.bufferedReceive();
+ } catch (final SocketTimeoutException e) {
+ // this is the expected route - the client
+ // shouldn't be sending any more packets.
+ break;
+ }
+
+ if (dataPacket != null && (!dataPacket.getAddress().equals(twrp.getAddress()) || dataPacket.getPort() != twrp.getPort())) {
+ // make sure it was from the right client...
+ transferTftp.bufferedSend(new TFTPErrorPacket(dataPacket.getAddress(), dataPacket.getPort(), TFTPErrorPacket.UNKNOWN_TID,
+ "Unexpected Host or Port"
+ ));
+ } else {
+ // This means they sent us the last
+ // datapacket again, must have missed our
+ // ack. resend it.
+ transferTftp.bufferedSend(lastSentAck);
+ }
+ }
+
+ // all done.
+ break;
+ }
+ } else {
+ if (!shutdownTransfer) {
+ LOGGER.error("Unexpected response from tftp client during transfer (" + dataPacket + "). Transfer aborted.");
+ }
+ break;
+ }
+ }
+ } finally {
+ if (bos != null) {
+ bos.close();
+ }
+ }
+ }
+
+ /*
+ * Makes sure that paths provided by TFTP clients do not get outside the serverRoot directory.
+ */
+ private File buildSafeFile(final File serverDirectory, String fileName, final boolean createSubDirs) throws IOException {
+ fileName = fileName.replaceAll("a:\\\\", "");
+ File temp = new File(serverDirectory, fileName).getCanonicalFile();
+ if (!temp.exists()) {
+ temp = new File(serverDirectory, StringUtils.upperCase(fileName)).getCanonicalFile();
+ }
+
+ if (!isSubdirectoryOf(serverDirectory, temp)) {
+ throw new IOException("Cannot access files outside of TFTP server root.");
+ }
+
+ // ensure directory exists (if requested)
+ if (createSubDirs) {
+ createDirectory(temp.getParentFile());
+ }
+
+ return temp;
+ }
+
+ /*
+ * recursively check to see if one directory is a parent of another.
+ */
+ private boolean isSubdirectoryOf(final File parent, final File child) {
+ final File childsParent = child.getParentFile();
+ if (childsParent == null) {
+ return false;
+ }
+ if (childsParent.equals(parent)) {
+ return true;
+ }
+ return isSubdirectoryOf(parent, childsParent);
+ }
+
+ public void close() {
+ shutdownTransfer = true;
+ try {
+ transferTftp.close();
+ } catch (final RuntimeException e) {
+ // noop
+ }
+ }
+ }
+}
diff --git a/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPWriteRequestPacket.java b/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPWriteRequestPacket.java
new file mode 100644
index 0000000..5440390
--- /dev/null
+++ b/tftp/src/main/java/pl/psobiech/opengr8on/org/apache/commons/net/TFTPWriteRequestPacket.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package pl.psobiech.opengr8on.org.apache.commons.net;
+
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+
+/**
+ * A class derived from TFTPRequestPacket defining a TFTP write request packet type.
+ *
+ * Details regarding the TFTP protocol and the format of TFTP packets can be found in RFC 783. But the point of these classes is to keep you from having to
+ * worry about the internals. Additionally, only very few people should have to care about any of the TFTPPacket classes or derived classes. Almost all users
+ * should only be concerned with the {@link TFTPClient} class {@link TFTPClient#receiveFile receiveFile()} and {@link TFTPClient#sendFile sendFile()} methods.
+ *
+ * @see TFTPPacket
+ * @see TFTPRequestPacket
+ * @see TFTPPacketException
+ * @see TFTP
+ */
+public final class TFTPWriteRequestPacket extends TFTPRequestPacket {
+ /**
+ * Creates a write request packet of based on a received datagram and assumes the datagram has already been identified as a write request. Assumes the
+ * datagram is at least length 4, else an ArrayIndexOutOfBoundsException may be thrown.
+ *
+ * @param datagram The datagram containing the received request.
+ * @throws TFTPPacketException If the datagram isn't a valid TFTP request packet.
+ */
+ TFTPWriteRequestPacket(final DatagramPacket datagram) throws TFTPPacketException {
+ super(WRITE_REQUEST, datagram);
+ }
+
+ /**
+ * Creates a write request packet to be sent to a host at a given port with a file name and transfer mode request.
+ *
+ * @param destination The host to which the packet is going to be sent.
+ * @param port The port to which the packet is going to be sent.
+ * @param fileName The requested file name.
+ * @param mode The requested transfer mode. This should be on of the TFTP class MODE constants (e.g., TFTP.NETASCII_MODE).
+ */
+ public TFTPWriteRequestPacket(final InetAddress destination, final int port, final String fileName, final int mode) {
+ super(destination, port, WRITE_REQUEST, fileName, mode);
+ }
+
+ /**
+ * For debugging
+ *
+ * @since 3.6
+ */
+ @Override
+ public String toString() {
+ return super.toString() + " WRQ " + getFilename() + " " + TFTP.getModeName(getMode());
+ }
+}
diff --git a/vclu/assembly/jar-with-dependencies.xml b/vclu/assembly/jar-with-dependencies.xml
new file mode 100644
index 0000000..3174bdc
--- /dev/null
+++ b/vclu/assembly/jar-with-dependencies.xml
@@ -0,0 +1,39 @@
+
+
+
+ jar-with-dependencies-and-exclude-classes
+
+
+ jar
+
+
+ false
+
+
+
+ /
+ true
+ true
+ runtime
+
+
+
\ No newline at end of file
diff --git a/vclu/pom.xml b/vclu/pom.xml
new file mode 100644
index 0000000..82203a8
--- /dev/null
+++ b/vclu/pom.xml
@@ -0,0 +1,87 @@
+
+
+
+
+ 4.0.0
+
+
+ pl.psobiech.opengr8on
+ parent
+ 1.0-SNAPSHOT
+
+
+ vclu
+
+
+
+ pl.psobiech.opengr8on
+ lib
+
+
+
+ ch.qos.logback
+ logback-classic
+
+
+
+ commons-net
+ commons-net
+
+
+
+
+ org.luaj
+ luaj-jse
+ 3.0.1
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+
+ vclu
+ false
+
+ assembly/jar-with-dependencies.xml
+
+
+
+ true
+ pl.psobiech.opengr8on.vclu.Main
+
+
+
+
+
+ assemble-all
+ package
+
+ single
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vclu/src/main/java/pl/psobiech/opengr8on/vclu/LuaServer.java b/vclu/src/main/java/pl/psobiech/opengr8on/vclu/LuaServer.java
new file mode 100644
index 0000000..4fcac12
--- /dev/null
+++ b/vclu/src/main/java/pl/psobiech/opengr8on/vclu/LuaServer.java
@@ -0,0 +1,444 @@
+/*
+ * OpenGr8on, open source extensions to systems based on Grenton devices
+ * Copyright (C) 2023 Piotr Sobiech
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package pl.psobiech.opengr8on.vclu;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang3.StringUtils;
+import org.luaj.vm2.Globals;
+import org.luaj.vm2.LoadState;
+import org.luaj.vm2.LuaTable;
+import org.luaj.vm2.LuaThread;
+import org.luaj.vm2.LuaValue;
+import org.luaj.vm2.Varargs;
+import org.luaj.vm2.compiler.LuaC;
+import org.luaj.vm2.lib.CoroutineLib;
+import org.luaj.vm2.lib.LibFunction;
+import org.luaj.vm2.lib.OneArgFunction;
+import org.luaj.vm2.lib.PackageLib;
+import org.luaj.vm2.lib.StringLib;
+import org.luaj.vm2.lib.TableLib;
+import org.luaj.vm2.lib.ThreeArgFunction;
+import org.luaj.vm2.lib.TwoArgFunction;
+import org.luaj.vm2.lib.jse.JseBaseLib;
+import org.luaj.vm2.lib.jse.JseOsLib;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import pl.psobiech.opengr8on.client.CLUFiles;
+import pl.psobiech.opengr8on.client.CipherKey;
+import pl.psobiech.opengr8on.client.device.CLUDevice;
+import pl.psobiech.opengr8on.exceptions.UnexpectedException;
+import pl.psobiech.opengr8on.util.IPv4AddressUtil.NetworkInterfaceDto;
+import pl.psobiech.opengr8on.vclu.VirtualSystem.Subscription;
+
+import static org.apache.commons.lang3.StringUtils.lowerCase;
+import static org.apache.commons.lang3.StringUtils.stripToEmpty;
+import static org.apache.commons.lang3.StringUtils.stripToNull;
+
+public class LuaServer {
+ private static final Logger LOGGER = LoggerFactory.getLogger(LuaServer.class);
+
+ private LuaServer() {
+ // NOP
+ }
+
+ public static LuaThreadWrapper create(NetworkInterfaceDto networkInterface, Path rootDirectory, CLUDevice cluDevice, CipherKey cipherKey) {
+ final VirtualSystem virtualSystem = new VirtualSystem(networkInterface, cluDevice, cipherKey);
+
+ Globals globals = new Globals();
+ globals.load(new JseBaseLib());
+ globals.load(new PackageLib());
+ //globals.load(new Bit32Lib());
+ globals.load(new TableLib());
+ globals.load(new StringLib());
+ globals.load(new CoroutineLib());
+ //globals.load(new JseMathLib());
+ //globals.load(new JseIoLib());
+ globals.load(new JseOsLib());
+ //globals.load(new LuajavaLib());
+
+ globals.load(new TwoArgFunction() {
+ public LuaValue call(LuaValue modname, LuaValue env) {
+ final LuaValue library = tableOf();
+
+ library.set("setup", new LibFunction() {
+ @Override
+ public LuaValue call() {
+ virtualSystem.setup();
+
+ return LuaValue.NIL;
+ }
+ });
+
+ library.set("loop", new LibFunction() {
+ @Override
+ public LuaValue call() {
+ virtualSystem.loop();
+
+ return LuaValue.NIL;
+ }
+ });
+
+ library.set("sleep", new OneArgFunction() {
+ @Override
+ public LuaValue call(LuaValue arg) {
+ virtualSystem.sleep(arg.checklong());
+
+ return LuaValue.NIL;
+ }
+ });
+
+ library.set("logDebug", new OneArgFunction() {
+ @Override
+ public LuaValue call(LuaValue arg) {
+ LOGGER.debug(String.valueOf(arg.checkstring()));
+
+ return LuaValue.NIL;
+ }
+ });
+
+ library.set("logInfo", new OneArgFunction() {
+ @Override
+ public LuaValue call(LuaValue arg) {
+ LOGGER.info(String.valueOf(arg.checkstring()));
+
+ return LuaValue.NIL;
+ }
+ });
+
+ library.set("logWarning", new OneArgFunction() {
+ @Override
+ public LuaValue call(LuaValue arg) {
+ LOGGER.warn(String.valueOf(arg.checkstring()));
+
+ return LuaValue.NIL;
+ }
+ });
+
+ library.set("logError", new OneArgFunction() {
+ @Override
+ public LuaValue call(LuaValue arg) {
+ LOGGER.error(String.valueOf(arg.checkstring()));
+
+ return LuaValue.NIL;
+ }
+ });
+
+ library.set("clientRegister", new LibFunction() {
+ @Override
+ public LuaValue invoke(Varargs args) {
+ final LuaValue object = args.arg(4);
+
+ final ArrayList subscriptions = new ArrayList<>();
+ if (!object.istable()) {
+ return LuaValue.valueOf("values:{" + 99 + "}");
+ }
+ final LuaTable checktable = object.checktable();
+ for (LuaValue key : checktable.keys()) {
+ final LuaValue keyValue = checktable.get(key);
+ if (!keyValue.istable()) {
+ return LuaValue.valueOf("values:{" + 99 + "}");
+ }
+
+ subscriptions.add(
+ new Subscription(
+ String.valueOf(keyValue.checktable().get(1).checktable().get("name")),
+ keyValue.checktable().get(2).checkint()
+ )
+ );
+ }
+
+ return LuaValue.valueOf(
+ virtualSystem.clientRegister(
+ String.valueOf(args.arg1().checkstring()),
+ args.arg(2).checkint(),
+ args.arg(3).checkint(),
+ subscriptions
+ )
+ );
+ }
+ });
+
+ library.set("clientDestroy", new LibFunction() {
+ @Override
+ public LuaValue invoke(Varargs args) {
+ return virtualSystem.clientDestroy(
+ String.valueOf(args.arg1().checkstring()),
+ args.arg(2).checkint(),
+ args.arg(3).checkint()
+ );
+ }
+ });
+
+ library.set("fetchValues", new OneArgFunction() {
+ @Override
+ public LuaValue call(LuaValue object) {
+ final ArrayList subscriptions = new ArrayList<>();
+ if (!object.istable()) {
+ return LuaValue.valueOf("values:{" + 99 + "}");
+ }
+
+ final LuaTable checktable = object.checktable();
+ for (LuaValue key : checktable.keys()) {
+ final LuaValue keyValue = checktable.get(key);
+ if (!keyValue.istable()) {
+ return LuaValue.valueOf("values:{\"" + globals.load("return _G[\"%s\"]".formatted(keyValue)).call() + "\"}");
+ }
+
+ subscriptions.add(
+ new Subscription(
+ String.valueOf(keyValue.checktable().get(1).checktable().get("name")),
+ keyValue.checktable().get(2).checkint()
+ )
+ );
+ }
+
+ return LuaValue.valueOf(
+ "values:" + virtualSystem.fetchValues(
+ subscriptions
+ )
+ );
+ }
+
+ @Override
+ public LuaValue call(LuaValue object, LuaValue arg1, LuaValue arg2) {
+ virtualSystem.getObject(object.checkint()).addEvent(arg1.checkint(), arg2.checkfunction());
+
+ return LuaValue.NIL;
+ }
+ });
+
+ library.set("newObject", new ThreeArgFunction() {
+ @Override
+ public LuaValue call(LuaValue arg1, LuaValue arg2, LuaValue arg3) {
+ return LuaValue.valueOf(
+ virtualSystem.newObject(arg1.checkint(), String.valueOf(arg2.checkstring()), arg3.checkint())
+ );
+ }
+ });
+
+ library.set("newGate", new ThreeArgFunction() {
+ @Override
+ public LuaValue call(LuaValue arg1, LuaValue arg2, LuaValue arg3) {
+ return LuaValue.valueOf(
+ virtualSystem.newGate(arg1.checkint(), String.valueOf(arg2.checkstring()))
+ );
+ }
+ });
+
+ library.set("get", new TwoArgFunction() {
+ @Override
+ public LuaValue call(LuaValue object, LuaValue arg) {
+ return virtualSystem.getObject(object.checkint()).get(arg.checkint());
+ }
+ });
+
+ library.set("set", new ThreeArgFunction() {
+ @Override
+ public LuaValue call(LuaValue object, LuaValue arg1, LuaValue arg2) {
+ virtualSystem.getObject(object.checkint()).set(arg1.checkint(), arg2);
+
+ return LuaValue.NIL;
+ }
+ });
+
+ library.set("execute", new ThreeArgFunction() {
+ @Override
+ public LuaValue call(LuaValue object, LuaValue arg1, LuaValue arg2) {
+ return virtualSystem.getObject(object.checkint()).execute(arg1.checkint(), arg2);
+ }
+ });
+
+ library.set("addEvent", new ThreeArgFunction() {
+ @Override
+ public LuaValue call(LuaValue object, LuaValue arg1, LuaValue arg2) {
+ virtualSystem.getObject(object.checkint()).addEvent(arg1.checkint(), arg2.checkfunction());
+
+ return LuaValue.NIL;
+ }
+ });
+
+ env.set("api", library);
+
+ return library;
+ }
+ });
+
+ LoadState.install(globals);
+ LuaC.install(globals);
+
+ globals.finder = filename -> {
+ try {
+ final Path filePath = rootDirectory.resolve(StringUtils.upperCase(filename));
+ if (!filePath.getParent().equals(rootDirectory)) {
+ throw new UnexpectedException("Attempt to access external directory");
+ }
+
+ return Files.newInputStream(
+ filePath
+ );
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ };
+
+ loadScript(globals, classPath(URI.create("classpath:/INIT.LUA")), "INIT.LUA");
+
+ return new LuaThreadWrapper(
+ globals, rootDirectory
+ );
+ }
+
+ private static LuaValue loadScript(Globals globals, Path path, String name) {
+ final String script;
+ try {
+ script = Files.readString(path);
+ } catch (IOException e) {
+ throw new UnexpectedException(e);
+ }
+
+ return globals.load(script, name)
+ .call();
+ }
+
+ private static final String JAR_PATH_SEPARATOR = Pattern.quote("!");
+
+ private static Path classPath(URI uri) {
+ final String resourceUriPath = getResourceUriPath(uri);
+
+ final URL url = LuaServer.class.getResource(resourceUriPath);
+ if (url == null) {
+ throw new UnexpectedException(uri + " not found!");
+ }
+
+ try {
+ final URI classPathUri = url.toURI();
+ final String scheme = classPathUri.getScheme();
+ final String classPathUriAsString = classPathUri.toString();
+
+ final Path path;
+ if (scheme.equals(SchemeEnum.JAR.toUrlScheme())) {
+ final String jarPath = classPathUriAsString.split(JAR_PATH_SEPARATOR, 2)[0];
+
+ path = getOrCreateJarFileSystemFor(jarPath).provider()
+ .getPath(classPathUri);
+ } else {
+ path = Paths.get(classPathUri);
+ }
+
+ return path;
+ } catch (URISyntaxException e) {
+ throw new UnexpectedException(e);
+ }
+ }
+
+ private static final Map jarFileSystems = new HashMap<>();
+
+ private static FileSystem getOrCreateJarFileSystemFor(String jarPath) {
+ synchronized (jarFileSystems) {
+ return jarFileSystems.computeIfAbsent(
+ jarPath,
+ ignored -> {
+ try {
+ final URI jarUri = URI.create(jarPath);
+ System.out.println(jarUri);
+ return FileSystems.newFileSystem(jarUri, Collections.emptyMap());
+ } catch (IOException e) {
+ throw new UnexpectedException(e);
+ }
+ }
+ );
+ }
+ }
+
+ private static String getResourceUriPath(URI uri) {
+ final String path = stripToEmpty(uri.getPath());
+
+ final String host = stripToNull(uri.getHost());
+ if (host == null) {
+ return path;
+ }
+
+ return "/" + host + path;
+ }
+
+ public enum SchemeEnum {
+ CLASSPATH,
+ JAR,
+ FILE,
+ //
+ ;
+
+ public String toUrlScheme() {
+ return lowerCase(name());
+ }
+
+ public static SchemeEnum fromUrlScheme(String urlScheme) {
+ if (urlScheme == null) {
+ return FILE;
+ }
+
+ for (SchemeEnum scheme : values()) {
+ if (scheme.toUrlScheme().equals(urlScheme)) {
+ return scheme;
+ }
+ }
+
+ throw new UnexpectedException(String.format("Unsupported resource scheme: %s", urlScheme));
+ }
+ }
+
+ public static class LuaThreadWrapper extends Thread {
+ private final Globals globals;
+
+ public LuaThreadWrapper(Globals globals, Path rootDirectory) {
+ super(() -> {
+ final LuaValue mainChunk = loadScript(globals, rootDirectory.resolve(CLUFiles.MAIN_LUA.getFileName()), CLUFiles.MAIN_LUA.getFileName());
+
+ final LuaThread luaThread = new LuaThread(globals, mainChunk);
+
+ luaThread.resume(LuaValue.NIL);
+ });
+
+ setDaemon(true);
+
+ this.globals = globals;
+
+ start();
+ }
+
+ public Globals globals() {
+ return globals;
+ }
+
+ }
+}
diff --git a/vclu/src/main/java/pl/psobiech/opengr8on/vclu/Main.java b/vclu/src/main/java/pl/psobiech/opengr8on/vclu/Main.java
new file mode 100644
index 0000000..ce83905
--- /dev/null
+++ b/vclu/src/main/java/pl/psobiech/opengr8on/vclu/Main.java
@@ -0,0 +1,64 @@
+/*
+ * OpenGr8on, open source extensions to systems based on Grenton devices
+ * Copyright (C) 2023 Piotr Sobiech
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package pl.psobiech.opengr8on.vclu;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import org.apache.commons.codec.binary.Base64;
+import pl.psobiech.opengr8on.client.CipherKey;
+import pl.psobiech.opengr8on.client.device.CLUDevice;
+import pl.psobiech.opengr8on.client.device.CipherTypeEnum;
+import pl.psobiech.opengr8on.util.FileUtil;
+import pl.psobiech.opengr8on.util.IPv4AddressUtil;
+import pl.psobiech.opengr8on.util.IPv4AddressUtil.NetworkInterfaceDto;
+import pl.psobiech.opengr8on.util.RandomUtil;
+
+public class Main {
+ public static void main(String[] args) throws Exception {
+ final Path rootDirectory = Paths.get("./runtime/root/a").toAbsolutePath();
+ FileUtil.mkdir(rootDirectory);
+
+ for (NetworkInterfaceDto localIPv4NetworkInterface : IPv4AddressUtil.getLocalIPv4NetworkInterfaces()) {
+ System.out.println(localIPv4NetworkInterface);
+ }
+
+ final NetworkInterfaceDto networkInterface = IPv4AddressUtil.getLocalIPv4NetworkInterfaceByName(args[args.length - 1]).get();
+
+ final CipherKey projectCipherKey = new CipherKey(
+ Base64.decodeBase64("mVHTJ/sJd9qTzE1nfLrKxA=="),
+ Base64.decodeBase64("gOYp2Y1wrPT63icsX90aCA==")
+ );
+
+ // TODO: load from config
+ final CLUDevice cluDevice = new CLUDevice(
+ 0L, "0eaa55aa55aa",
+ networkInterface.getAddress(),
+ CipherTypeEnum.PROJECT, RandomUtil.bytes(16), "00000000".getBytes()
+ );
+
+ try (Server server = new Server(networkInterface, rootDirectory, projectCipherKey, cluDevice)) {
+ server.listen();
+
+ while (!Thread.interrupted()) {
+ Thread.yield();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/vclu/src/main/java/pl/psobiech/opengr8on/vclu/Server.java b/vclu/src/main/java/pl/psobiech/opengr8on/vclu/Server.java
new file mode 100644
index 0000000..5a6a4c2
--- /dev/null
+++ b/vclu/src/main/java/pl/psobiech/opengr8on/vclu/Server.java
@@ -0,0 +1,448 @@
+/*
+ * OpenGr8on, open source extensions to systems based on Grenton devices
+ * Copyright (C) 2023 Piotr Sobiech
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package pl.psobiech.opengr8on.vclu;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.Inet4Address;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.lang3.tuple.ImmutablePair;
+import org.apache.commons.lang3.tuple.ImmutableTriple;
+import org.apache.commons.lang3.tuple.Pair;
+import org.luaj.vm2.LuaError;
+import org.luaj.vm2.LuaValue;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import pl.psobiech.opengr8on.client.commands.ErrorCommand;
+import pl.psobiech.opengr8on.org.apache.commons.net.TFTPServer;
+import pl.psobiech.opengr8on.org.apache.commons.net.TFTPServer.ServerMode;
+import pl.psobiech.opengr8on.client.CipherKey;
+import pl.psobiech.opengr8on.client.Client;
+import pl.psobiech.opengr8on.client.Command;
+import pl.psobiech.opengr8on.client.commands.DiscoverCLUsCommand;
+import pl.psobiech.opengr8on.client.commands.LuaScriptCommand;
+import pl.psobiech.opengr8on.client.commands.ResetCommand;
+import pl.psobiech.opengr8on.client.commands.SetIpCommand;
+import pl.psobiech.opengr8on.client.commands.SetKeyCommand;
+import pl.psobiech.opengr8on.client.commands.StartTFTPdCommand;
+import pl.psobiech.opengr8on.client.device.CLUDevice;
+import pl.psobiech.opengr8on.exceptions.UnexpectedException;
+import pl.psobiech.opengr8on.util.IPv4AddressUtil;
+import pl.psobiech.opengr8on.util.IPv4AddressUtil.NetworkInterfaceDto;
+import pl.psobiech.opengr8on.util.RandomUtil;
+import pl.psobiech.opengr8on.util.SocketUtil;
+import pl.psobiech.opengr8on.util.SocketUtil.Payload;
+import pl.psobiech.opengr8on.util.SocketUtil.UDPSocket;
+import pl.psobiech.opengr8on.util.ThreadUtil;
+import pl.psobiech.opengr8on.vclu.LuaServer.LuaThreadWrapper;
+
+public class Server implements AutoCloseable {
+ private static final Logger LOGGER = LoggerFactory.getLogger(Server.class);
+
+ protected static final int BUFFER_SIZE = 2048;
+
+ private static final byte[] EMPTY_BUFFER = new byte[0];
+
+ private final DatagramPacket requestPacket = new DatagramPacket(EMPTY_BUFFER, 0);
+
+ private final DatagramPacket responsePacket = new DatagramPacket(new byte[BUFFER_SIZE], BUFFER_SIZE);
+
+ protected final NetworkInterfaceDto networkInterface;
+
+ private final Path rootDirectory;
+
+ private final CLUDevice cluDevice;
+
+ protected final UDPSocket broadcastSocket;
+
+ protected final UDPSocket socket;
+
+ private final TFTPServer tftpServer;
+
+ private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1, ThreadUtil.daemonThreadFactory("cluServer"));
+
+ private LuaThreadWrapper luaThread;
+
+ private CipherKey broadcastCipherKey = CipherKey.DEFAULT_BROADCAST;
+
+ private CipherKey currentCipherKey;
+
+ public Server(NetworkInterfaceDto networkInterface, Path rootDirectory, CipherKey projectCipherKey, CLUDevice cluDevice) {
+ this.networkInterface = networkInterface;
+ this.rootDirectory = rootDirectory;
+
+ this.currentCipherKey = projectCipherKey;
+ this.cluDevice = cluDevice;
+
+ this.luaThread = LuaServer.create(networkInterface, rootDirectory, cluDevice, currentCipherKey);
+
+ try {
+ this.tftpServer = new TFTPServer(
+ Client.TFTP_PORT, networkInterface.getAddress(),
+ ServerMode.GET_AND_REPLACE
+ );
+
+ this.broadcastSocket = SocketUtil.udp(
+ IPv4AddressUtil.parseIPv4("255.255.255.255"),
+// networkInterface.getBroadcastAddress(),
+ Client.COMMAND_PORT
+ );
+ this.socket = SocketUtil.udp(networkInterface.getAddress(), Client.COMMAND_PORT);
+ } catch (IOException e) {
+ throw new UnexpectedException(e);
+ }
+ }
+
+ public void listen() {
+ socket.open();
+ broadcastSocket.open();
+
+ try {
+ this.tftpServer.start(
+ rootDirectory.toFile()
+ );
+ } catch (IOException e) {
+ throw new UnexpectedException(e);
+ }
+
+ executorService
+ .scheduleAtFixedRate(() -> {
+ try {
+ final UUID uuid = UUID.randomUUID();
+
+ awaitRequestPayload(String.valueOf(uuid), broadcastSocket, broadcastCipherKey, Duration.ofMillis(1000))
+ .flatMap(payload ->
+ onBroadcastCommand(payload)
+ .map(pair ->
+ ImmutableTriple.of(
+ payload, pair.getLeft(), pair.getRight()
+ )
+ )
+ )
+ .ifPresent(triple ->
+ respond(uuid, triple.getLeft(), triple.getMiddle(), triple.getRight())
+ );
+ } catch (Exception e) {
+ LOGGER.error(e.getMessage(), e);
+ }
+ },
+ 1000, 1000, TimeUnit.MILLISECONDS
+ );
+
+ executorService
+ .scheduleAtFixedRate(() -> {
+ try {
+ final UUID uuid = UUID.randomUUID();
+
+ awaitRequestPayload(String.valueOf(uuid), socket, currentCipherKey, Duration.ofMillis(100))
+ .flatMap(payload ->
+ onCommand(payload)
+ .map(pair ->
+ ImmutableTriple.of(
+ payload, pair.getLeft(), pair.getRight()
+ )
+ )
+ )
+ .ifPresent(triple ->
+ respond(uuid, triple.getLeft(), triple.getMiddle(), triple.getRight())
+ );
+ } catch (Exception e) {
+ LOGGER.error(e.getMessage(), e);
+ }
+ },
+ 1000, 100, TimeUnit.MILLISECONDS
+ );
+ }
+
+ private void respond(UUID uuid, Payload requestPayload, CipherKey cipherKey, Command command) {
+ respond(uuid, cipherKey, requestPayload.address(), requestPayload.port(), command);
+ }
+
+ private Optional> onBroadcastCommand(Payload payload) {
+ final byte[] buffer = payload.buffer();
+ if (DiscoverCLUsCommand.requestMatches(buffer)) {
+ return onDiscoverCommand(payload);
+ }
+
+ if (SetIpCommand.requestMatches(buffer)) {
+ return onSetIpCommand(payload);
+ }
+
+ return Optional.empty();
+ }
+
+ private Optional> onDiscoverCommand(Payload payload) {
+ final byte[] buffer = payload.buffer();
+ final Optional requestOptional = DiscoverCLUsCommand.requestFromByteArray(buffer);
+ if (requestOptional.isPresent()) {
+ final DiscoverCLUsCommand.Request request = requestOptional.get();
+
+ final byte[] encrypted = request.getEncrypted();
+ final byte[] iv = request.getIV();
+ final byte[] hash = DiscoverCLUsCommand.hash(currentCipherKey.decrypt(encrypted)
+ .orElse(RandomUtil.bytes(Command.RANDOM_SIZE)));
+
+ currentCipherKey = cluDevice.getCipherKey();
+ broadcastCipherKey = cluDevice.getCipherKey();
+
+ return Optional.of(
+ ImmutablePair.of(
+ CipherKey.DEFAULT_BROADCAST.withIV(iv),
+ DiscoverCLUsCommand.response(
+ currentCipherKey.encrypt(hash),
+ currentCipherKey.getIV(),
+ cluDevice.getSerialNumber(),
+ cluDevice.getMacAddress()
+ )
+ )
+ );
+ }
+
+ return sendError();
+ }
+
+ private Optional> onCommand(Payload payload) {
+ final byte[] buffer = payload.buffer();
+
+ if (SetIpCommand.requestMatches(buffer)) {
+ return onSetIpCommand(payload);
+ }
+
+ if (SetKeyCommand.requestMatches(buffer)) {
+ return onSetKeyCommand(payload);
+ }
+
+ if (ResetCommand.requestMatches(buffer)) {
+ return onResetCommand(payload);
+ }
+
+ if (LuaScriptCommand.requestMatches(buffer)) {
+ return onLuaScriptCommand(payload);
+ }
+
+ if (StartTFTPdCommand.requestMatches(buffer)) {
+ return onStartFTPdCommand(payload);
+ }
+
+ return sendError();
+ }
+
+ private Optional> onSetIpCommand(Payload payload) {
+ final byte[] buffer = payload.buffer();
+ final Optional requestOptional = SetIpCommand.requestFromByteArray(buffer);
+ if (requestOptional.isPresent()) {
+ final SetIpCommand.Request request = requestOptional.get();
+
+ if (Objects.equals(cluDevice.getSerialNumber(), request.getSerialNumber())) {
+ return Optional.of(
+ ImmutablePair.of(
+ currentCipherKey,
+ SetIpCommand.response(
+ cluDevice.getSerialNumber(),
+ cluDevice.getAddress()
+ )
+ )
+ );
+ }
+ }
+
+ return sendError();
+ }
+
+ private Optional> onSetKeyCommand(Payload payload) {
+ final byte[] buffer = payload.buffer();
+ final Optional requestOptional = SetKeyCommand.requestFromByteArray(buffer);
+ if (requestOptional.isPresent()) {
+ final SetKeyCommand.Request request = requestOptional.get();
+
+ //final byte[] encrypted = request.getEncrypted(); // Real CLU sends only dummy data
+ final byte[] key = request.getKey();
+ final byte[] iv = request.getIV();
+
+ final CipherKey newCipherKey = new CipherKey(key, iv);
+ //if (newCipherKey.decrypt(encrypted).isEmpty()) {
+ // return sendError();
+ //}
+
+ currentCipherKey = newCipherKey;
+ broadcastCipherKey = CipherKey.DEFAULT_BROADCAST;
+
+ return Optional.of(
+ ImmutablePair.of(
+ currentCipherKey,
+ SetKeyCommand.response()
+ )
+ );
+ }
+
+ return sendError();
+ }
+
+ private Optional> onResetCommand(Payload payload) {
+ final byte[] buffer = payload.buffer();
+ final Optional requestOptional = ResetCommand.requestFromByteArray(buffer);
+ if (requestOptional.isPresent()) {
+ final ResetCommand.Request request = requestOptional.get();
+
+ try {
+ this.luaThread.interrupt();
+
+ this.luaThread.join();
+ } catch (InterruptedException e) {
+ LOGGER.error(e.getMessage(), e);
+ }
+
+ this.luaThread = LuaServer.create(networkInterface, rootDirectory, cluDevice, currentCipherKey);
+
+ return Optional.of(
+ ImmutablePair.of(
+ currentCipherKey,
+ ResetCommand.response(
+ cluDevice.getAddress()
+ )
+ )
+ );
+ }
+
+ return sendError();
+ }
+
+ private Optional> onLuaScriptCommand(Payload payload) {
+ final byte[] buffer = payload.buffer();
+ final Optional requestOptional = LuaScriptCommand.requestFromByteArray(buffer);
+ if (requestOptional.isPresent()) {
+ final LuaScriptCommand.Request request = requestOptional.get();
+
+ final LuaValue luaValue;
+ try {
+ luaValue = luaThread.globals()
+ .load("return %s".formatted(request.getScript()))
+ .call();
+ } catch (LuaError e) {
+ LOGGER.error(e.getMessage(), e);
+
+ return sendError();
+ }
+
+ String returnValue;
+ if (luaValue.isstring()) {
+ returnValue = String.valueOf(luaValue);
+ } else {
+ returnValue = "nil";
+ }
+
+ return Optional.of(
+ ImmutablePair.of(
+ currentCipherKey,
+ LuaScriptCommand.response(
+ cluDevice.getAddress(),
+ request.getSessionId(),
+ returnValue
+ )
+ )
+ );
+ }
+
+ return sendError();
+ }
+
+ private Optional> onStartFTPdCommand(Payload payload) {
+ final byte[] buffer = payload.buffer();
+ final Optional requestOptional = StartTFTPdCommand.requestFromByteArray(buffer);
+ if (requestOptional.isPresent()) {
+ return Optional.of(
+ ImmutablePair.of(
+ currentCipherKey,
+ StartTFTPdCommand.response()
+ )
+ );
+ }
+
+ return sendError();
+ }
+
+ private Optional> sendError() {
+ return Optional.of(
+ ImmutablePair.of(
+ currentCipherKey,
+ ErrorCommand.response()
+ )
+ );
+ }
+
+ protected Optional