View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.net.tftp;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertFalse;
21  import static org.junit.jupiter.api.Assertions.assertThrows;
22  import static org.junit.jupiter.api.Assertions.fail;
23  
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.OutputStream;
27  import java.nio.charset.StandardCharsets;
28  import java.nio.file.Files;
29  import java.nio.file.Path;
30  import java.time.Duration;
31  import java.util.UUID;
32  
33  import org.apache.commons.io.FileUtils;
34  import org.apache.commons.net.tftp.TFTPServer.ServerMode;
35  import org.junit.jupiter.api.AfterEach;
36  import org.junit.jupiter.api.BeforeEach;
37  import org.junit.jupiter.api.Test;
38  
39  /**
40   * Basic tests to ensure that the TFTP Server is honoring its read/write mode, and preventing files from being read or written from outside of the assigned
41   * roots.
42   */
43  class TFTPServerPathTest {
44  
45      private static final int SERVER_PORT = 6901;
46      private static final String FILE_PREFIX = "TFTPServerPathTest_";
47  
48      private static Path createFileInDir(final Path directory, final String fileName) throws IOException {
49          final Path filePath = directory.resolve(fileName);
50          if (Files.exists(filePath)) {
51              Files.delete(filePath);
52          }
53          return Files.createFile(filePath);
54      }
55  
56      private static void deleteFile(final Path path, final boolean retry) throws IOException {
57          if (path != null) {
58              try {
59                  Files.deleteIfExists(path);
60              } catch (final IOException e) {
61                  if (!retry) {
62                      throw e;
63                  }
64                  System.err.println("Retrying delete failure: " + e);
65                  try {
66                      Thread.sleep(500);
67                  } catch (final InterruptedException e1) {
68                      Thread.currentThread().interrupt();
69                      fail(e);
70                  }
71                  Files.deleteIfExists(path);
72              }
73          }
74      }
75  
76      private static String getRandomFileName(final String suffix) {
77          return FILE_PREFIX + UUID.randomUUID() + suffix;
78      }
79  
80      private TFTPServer tftpServer;
81      private Path serverDirectory;
82  
83      private TFTPClient tftpClient;
84      private Path fileToRead;
85      private Path fileToWrite;
86  
87      @AfterEach
88      public void afterEach() throws IOException {
89          if (tftpClient != null) {
90              tftpClient.close();
91          }
92          if (tftpServer != null) {
93              tftpServer.close();
94          }
95          deleteFile(fileToRead, true);
96          deleteFile(fileToWrite, true);
97      }
98  
99      @BeforeEach
100     public void beforeEach() throws IOException {
101         serverDirectory = FileUtils.getTempDirectory().toPath();
102         fileToRead = createFileInDir(serverDirectory, getRandomFileName(".source.txt"));
103         fileToWrite = createFileInDir(serverDirectory, getRandomFileName(".out"));
104     }
105 
106     private TFTPServer startTftpServer(final ServerMode serverMode) throws IOException {
107         return new TFTPServer(serverDirectory.toFile(), serverDirectory.toFile(), SERVER_PORT, serverMode, null, null);
108     }
109 
110     @Test
111     void testReadOnly() throws IOException {
112         // Start a read-only server
113         tftpServer = startTftpServer(ServerMode.GET_ONLY);
114         final String serverAddress = "localhost";
115         final int serverPort = tftpServer.getPort();
116 
117         // write some data to verify read
118         Files.write(fileToRead, "The quick brown fox jumps over the lazy dog".getBytes(StandardCharsets.UTF_8));
119         final long fileToReadContentLength = Files.size(fileToRead);
120 
121         tftpClient = new TFTPClient();
122         tftpClient.open();
123         tftpClient.setSoTimeout(Duration.ofMillis(2000));
124 
125         // we can read file
126         try (OutputStream os = Files.newOutputStream(fileToWrite)) {
127             final String writeFileName = fileToRead.getFileName().toString();
128             final int bytesRead = tftpClient.receiveFile(writeFileName, TFTP.BINARY_MODE, os, serverAddress, serverPort);
129             assertEquals(fileToReadContentLength, bytesRead);
130         }
131 
132         // but we cannot write to it
133         try (InputStream is = Files.newInputStream(fileToRead)) {
134             final String readFileName = fileToRead.getFileName().toString();
135             final IOException exception = assertThrows(IOException.class,
136                     () -> tftpClient.sendFile(readFileName, TFTP.BINARY_MODE, is, serverAddress, serverPort));
137             assertEquals("Error code 4 received: Write not allowed by server.", exception.getMessage());
138         }
139     }
140 
141     @Test
142     void testWriteOnly() throws IOException {
143         // Start a write-only server
144         tftpServer = startTftpServer(ServerMode.PUT_ONLY);
145         final String serverAddress = "localhost";
146         final int serverPort = tftpServer.getPort();
147 
148         tftpClient = new TFTPClient();
149         tftpClient.open();
150         tftpClient.setSoTimeout(Duration.ofMillis(2000));
151 
152         // we cannot read file
153         try (OutputStream os = Files.newOutputStream(fileToWrite)) {
154             final String readFileName = fileToRead.getFileName().toString();
155             final IOException exception = assertThrows(IOException.class,
156                     () -> tftpClient.receiveFile(readFileName, TFTP.BINARY_MODE, os, serverAddress, serverPort));
157             assertEquals("Error code 4 received: Read not allowed by server.", exception.getMessage());
158         }
159         // but we can write to it
160         try (InputStream is = Files.newInputStream(fileToRead)) {
161             deleteFile(fileToWrite, false);
162             final String writeFileName = fileToWrite.getFileName().toString();
163             tftpClient.sendFile(writeFileName, TFTP.BINARY_MODE, is, serverAddress, serverPort);
164         }
165     }
166 
167     @Test
168     void testWriteOutsideHome() throws IOException {
169         // Start a read/write server
170         tftpServer = startTftpServer(ServerMode.GET_AND_PUT);
171         final String serverAddress = "localhost";
172         final int serverPort = tftpServer.getPort();
173 
174         tftpClient = new TFTPClient();
175         tftpClient.open();
176 
177         try (InputStream is = Files.newInputStream(fileToRead)) {
178             final IOException exception = assertThrows(IOException.class, () -> tftpClient.sendFile("../foo", TFTP.BINARY_MODE, is, serverAddress, serverPort));
179             assertEquals("Error code 0 received: Cannot access files outside of TFTP server root.", exception.getMessage());
180             assertFalse(Files.exists(serverDirectory.resolve("foo")), "file created when it should not have been");
181         }
182     }
183 
184     @Test
185     void testWriteVerifyContents() throws IOException {
186         // Start a write-only server
187         tftpServer = startTftpServer(ServerMode.GET_AND_PUT);
188         final String serverAddress = "localhost";
189         final int serverPort = tftpServer.getPort();
190 
191         tftpClient = new TFTPClient();
192         tftpClient.open();
193         tftpClient.setSoTimeout(Duration.ofMillis(2000));
194 
195         // write some data to file
196         final byte[] content = "TFTP write test!".getBytes(StandardCharsets.UTF_8);
197         Files.write(fileToRead, content);
198 
199         // send file
200         try (InputStream is = Files.newInputStream(fileToRead)) {
201             deleteFile(fileToWrite, false);
202             final String writeFileName = fileToWrite.getFileName().toString();
203             tftpClient.sendFile(writeFileName, TFTP.BINARY_MODE, is, serverAddress, serverPort);
204         }
205 
206         // then verify it contents
207         try (OutputStream os = Files.newOutputStream(fileToWrite)) {
208             final String readFileName = fileToRead.getFileName().toString();
209             final int readBytes = tftpClient.receiveFile(readFileName, TFTP.BINARY_MODE, os, serverAddress, serverPort);
210             assertEquals(content.length, readBytes);
211         }
212     }
213 
214 }