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  public 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                      fail(e);
69                  }
70                  Files.deleteIfExists(path);
71              }
72          }
73      }
74  
75      private static String getRandomFileName(final String suffix) {
76          return FILE_PREFIX + UUID.randomUUID() + suffix;
77      }
78  
79      private TFTPServer tftpServer;
80      private Path serverDirectory;
81  
82      private TFTPClient tftpClient;
83      private Path fileToRead;
84      private Path fileToWrite;
85  
86      @AfterEach
87      public void afterEach() throws IOException {
88          if (tftpClient != null) {
89              tftpClient.close();
90          }
91          if (tftpServer != null) {
92              tftpServer.close();
93          }
94          deleteFile(fileToRead, true);
95          deleteFile(fileToWrite, true);
96      }
97  
98      @BeforeEach
99      public void beforeEach() throws IOException {
100         serverDirectory = FileUtils.getTempDirectory().toPath();
101         fileToRead = createFileInDir(serverDirectory, getRandomFileName(".source.txt"));
102         fileToWrite = createFileInDir(serverDirectory, getRandomFileName(".out"));
103     }
104 
105     private TFTPServer startTftpServer(final ServerMode serverMode) throws IOException {
106         return new TFTPServer(serverDirectory.toFile(), serverDirectory.toFile(), SERVER_PORT, serverMode, null, null);
107     }
108 
109     @Test
110     public void testReadOnly() throws IOException {
111         // Start a read-only server
112         tftpServer = startTftpServer(ServerMode.GET_ONLY);
113         final String serverAddress = "localhost";
114         final int serverPort = tftpServer.getPort();
115 
116         // write some data to verify read
117         Files.write(fileToRead, "The quick brown fox jumps over the lazy dog".getBytes(StandardCharsets.UTF_8));
118         final long fileToReadContentLength = Files.size(fileToRead);
119 
120         tftpClient = new TFTPClient();
121         tftpClient.open();
122         tftpClient.setSoTimeout(Duration.ofMillis(2000));
123 
124         // we can read file
125         try (OutputStream os = Files.newOutputStream(fileToWrite)) {
126             final String writeFileName = fileToRead.getFileName().toString();
127             final int bytesRead = tftpClient.receiveFile(writeFileName, TFTP.BINARY_MODE, os, serverAddress, serverPort);
128             assertEquals(fileToReadContentLength, bytesRead);
129         }
130 
131         // but we cannot write to it
132         try (InputStream is = Files.newInputStream(fileToRead)) {
133             final String readFileName = fileToRead.getFileName().toString();
134             final IOException exception = assertThrows(IOException.class,
135                     () -> tftpClient.sendFile(readFileName, TFTP.BINARY_MODE, is, serverAddress, serverPort));
136             assertEquals("Error code 4 received: Write not allowed by server.", exception.getMessage());
137         }
138     }
139 
140     @Test
141     public void testWriteOnly() throws IOException {
142         // Start a write-only server
143         tftpServer = startTftpServer(ServerMode.PUT_ONLY);
144         final String serverAddress = "localhost";
145         final int serverPort = tftpServer.getPort();
146 
147         tftpClient = new TFTPClient();
148         tftpClient.open();
149         tftpClient.setSoTimeout(Duration.ofMillis(2000));
150 
151         // we cannot read file
152         try (OutputStream os = Files.newOutputStream(fileToWrite)) {
153             final String readFileName = fileToRead.getFileName().toString();
154             final IOException exception = assertThrows(IOException.class,
155                     () -> tftpClient.receiveFile(readFileName, TFTP.BINARY_MODE, os, serverAddress, serverPort));
156             assertEquals("Error code 4 received: Read not allowed by server.", exception.getMessage());
157         }
158         // but we can write to it
159         try (InputStream is = Files.newInputStream(fileToRead)) {
160             deleteFile(fileToWrite, false);
161             final String writeFileName = fileToWrite.getFileName().toString();
162             tftpClient.sendFile(writeFileName, TFTP.BINARY_MODE, is, serverAddress, serverPort);
163         }
164     }
165 
166     @Test
167     public void testWriteOutsideHome() throws IOException {
168         // Start a read/write server
169         tftpServer = startTftpServer(ServerMode.GET_AND_PUT);
170         final String serverAddress = "localhost";
171         final int serverPort = tftpServer.getPort();
172 
173         tftpClient = new TFTPClient();
174         tftpClient.open();
175 
176         try (InputStream is = Files.newInputStream(fileToRead)) {
177             final IOException exception = assertThrows(IOException.class, () -> tftpClient.sendFile("../foo", TFTP.BINARY_MODE, is, serverAddress, serverPort));
178             assertEquals("Error code 0 received: Cannot access files outside of TFTP server root.", exception.getMessage());
179             assertFalse(Files.exists(serverDirectory.resolve("foo")), "file created when it should not have been");
180         }
181     }
182 
183     @Test
184     public void testWriteVerifyContents() throws IOException {
185         // Start a write-only server
186         tftpServer = startTftpServer(ServerMode.GET_AND_PUT);
187         final String serverAddress = "localhost";
188         final int serverPort = tftpServer.getPort();
189 
190         tftpClient = new TFTPClient();
191         tftpClient.open();
192         tftpClient.setSoTimeout(Duration.ofMillis(2000));
193 
194         // write some data to file
195         final byte[] content = "TFTP write test!".getBytes(StandardCharsets.UTF_8);
196         Files.write(fileToRead, content);
197 
198         // send file
199         try (InputStream is = Files.newInputStream(fileToRead)) {
200             deleteFile(fileToWrite, false);
201             final String writeFileName = fileToWrite.getFileName().toString();
202             tftpClient.sendFile(writeFileName, TFTP.BINARY_MODE, is, serverAddress, serverPort);
203         }
204 
205         // then verify it contents
206         try (OutputStream os = Files.newOutputStream(fileToWrite)) {
207             final String readFileName = fileToRead.getFileName().toString();
208             final int readBytes = tftpClient.receiveFile(readFileName, TFTP.BINARY_MODE, os, serverAddress, serverPort);
209             assertEquals(content.length, readBytes);
210         }
211     }
212 
213 }