1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package libcore.java.net; 18 19 import junit.framework.TestCase; 20 21 import org.mockftpserver.core.util.IoUtil; 22 import org.mockftpserver.fake.FakeFtpServer; 23 import org.mockftpserver.fake.UserAccount; 24 import org.mockftpserver.fake.filesystem.DirectoryEntry; 25 import org.mockftpserver.fake.filesystem.FileEntry; 26 import org.mockftpserver.fake.filesystem.UnixFakeFileSystem; 27 28 import java.io.IOException; 29 import java.io.InputStream; 30 import java.io.OutputStream; 31 import java.net.MalformedURLException; 32 import java.net.Proxy; 33 import java.net.ProxySelector; 34 import java.net.ServerSocket; 35 import java.net.Socket; 36 import java.net.SocketAddress; 37 import java.net.SocketException; 38 import java.net.URI; 39 import java.net.URL; 40 import java.net.URLConnection; 41 import java.util.Arrays; 42 import java.util.Collections; 43 import java.util.List; 44 import java.util.Locale; 45 import java.util.Objects; 46 import java.util.Random; 47 import java.util.concurrent.CountDownLatch; 48 import java.util.concurrent.Semaphore; 49 import java.util.concurrent.TimeUnit; 50 51 import sun.net.ftp.FtpLoginException; 52 53 import static java.nio.charset.StandardCharsets.UTF_8; 54 55 /** 56 * Tests URLConnections for ftp:// URLs. 57 */ 58 public class FtpURLConnectionTest extends TestCase { 59 60 private static final String FILE_PATH = "test/file/for/FtpURLConnectionTest.txt"; 61 private static final String SERVER_HOSTNAME = "localhost"; 62 private static final String VALID_USER = "user"; 63 private static final String VALID_PASSWORD = "password"; 64 private static final String VALID_USER_HOME_DIR = "/home/user"; 65 66 private FakeFtpServer fakeFtpServer; 67 private UnixFakeFileSystem fileSystem; 68 69 @Override setUp()70 public void setUp() throws Exception { 71 super.setUp(); 72 fakeFtpServer = new FakeFtpServer(); 73 fakeFtpServer.setServerControlPort(0 /* allocate port number automatically */); 74 fakeFtpServer.addUserAccount(new UserAccount(VALID_USER, VALID_PASSWORD, 75 VALID_USER_HOME_DIR)); 76 fileSystem = new UnixFakeFileSystem(); 77 fakeFtpServer.setFileSystem(fileSystem); 78 fileSystem.add(new DirectoryEntry(VALID_USER_HOME_DIR)); 79 fakeFtpServer.start(); 80 } 81 82 @Override tearDown()83 public void tearDown() throws Exception { 84 fakeFtpServer.stop(); 85 super.tearDown(); 86 } 87 testInputUrl()88 public void testInputUrl() throws Exception { 89 byte[] fileContents = "abcdef 1234567890".getBytes(UTF_8); 90 addFileEntry(FILE_PATH, fileContents); 91 URL fileUrl = getFileUrlWithCredentials(VALID_USER, VALID_PASSWORD, FILE_PATH); 92 URLConnection connection = fileUrl.openConnection(); 93 assertContents(fileContents, connection.getInputStream()); 94 } 95 testInputUrl_invalidUserOrPassword()96 public void testInputUrl_invalidUserOrPassword() throws Exception { 97 checkInputUrl_invalidUserOrPassword("wrong_user", VALID_PASSWORD); 98 checkInputUrl_invalidUserOrPassword(VALID_USER, "wrong password"); 99 } 100 testInputUrl_missingPassword()101 public void testInputUrl_missingPassword() throws Exception { 102 URL noPasswordUrl = getFileUrlWithCredentials(VALID_USER, null, FILE_PATH); 103 URLConnection noPasswordConnection = noPasswordUrl.openConnection(); 104 try { 105 noPasswordConnection.getInputStream(); 106 fail(); 107 } catch (IOException expected) { 108 } 109 } 110 checkInputUrl_invalidUserOrPassword(String user, String password)111 private void checkInputUrl_invalidUserOrPassword(String user, String password) 112 throws IOException { 113 URL fileUrl = getFileUrlWithCredentials(user, password, FILE_PATH); 114 URLConnection connection = fileUrl.openConnection(); 115 try { 116 connection.getInputStream(); 117 fail(); 118 } catch (sun.net.ftp.FtpLoginException expected) { 119 assertEquals("Invalid username/password", expected.getMessage()); 120 } 121 } 122 testOutputUrl()123 public void testOutputUrl() throws Exception { 124 byte[] fileContents = "abcdef 1234567890".getBytes(UTF_8); 125 addFileEntry("test/output-url/existing file.txt", fileContents); 126 byte[] newFileContents = "contents of brand new file".getBytes(UTF_8); 127 String filePath = "test/output-url/file that is newly created.txt"; 128 URL fileUrl = getFileUrlWithCredentials(VALID_USER, VALID_PASSWORD, filePath); 129 URLConnection connection = fileUrl.openConnection(); 130 connection.setDoInput(false); 131 connection.setDoOutput(true); 132 OutputStream os = connection.getOutputStream(); 133 writeBytes(os, newFileContents); 134 135 assertContents(newFileContents, openFileSystemContents(filePath)); 136 } 137 testConnectOverProxy_noProxy()138 public void testConnectOverProxy_noProxy() throws Exception { 139 Proxy proxy = Proxy.NO_PROXY; 140 byte[] fileContents = "abcdef 1234567890".getBytes(UTF_8); 141 URL fileUrl = addFileEntry(FILE_PATH, fileContents); 142 URLConnection connection = fileUrl.openConnection(proxy); 143 assertContents(fileContents, connection.getInputStream()); 144 // Check that NO_PROXY covers the Type.DIRECT case 145 assertEquals(Proxy.Type.DIRECT, proxy.type()); 146 } 147 148 /** 149 * Tests that the helper class {@link CountingProxy} correctly accepts and 150 * counts connection attempts to the address represented by {@code asProxy()}. 151 */ testCountingProxy()152 public void testCountingProxy() throws Exception { 153 Socket socket = new Socket(); 154 try { 155 CountingProxy countingProxy = CountingProxy.start(); 156 try { 157 Proxy proxy = countingProxy.asProxy(); 158 assertEquals(Proxy.Type.HTTP, proxy.type()); 159 SocketAddress address = proxy.address(); 160 socket.connect(address, /* timeout (msec) */ 200); // attempt one connection 161 countingProxy.waitAndAssertConnectionCount(1); 162 } finally { 163 countingProxy.shutdown(); 164 } 165 } finally { 166 socket.close(); 167 } 168 } 169 170 /** 171 * Tests that a HTTP proxy explicitly passed to {@link URL#openConnection(Proxy)} 172 * ignores HTTP proxies (since it doesn't support them) and attempts a direct 173 * connection instead. 174 */ testConnectOverProxy_explicit_http_uses_direct_connection()175 public void testConnectOverProxy_explicit_http_uses_direct_connection() throws Exception { 176 byte[] fileContents = "abcdef 1234567890".getBytes(UTF_8); 177 URL fileUrl = addFileEntry(FILE_PATH, fileContents); 178 CountingProxy countingProxy = CountingProxy.start(); 179 try { 180 Proxy proxy = countingProxy.asProxy(); 181 URLConnection connection = fileUrl.openConnection(proxy); 182 // direct connection succeeds 183 assertContents(fileContents, connection.getInputStream()); 184 countingProxy.waitAndAssertConnectionCount(0); 185 } finally { 186 countingProxy.shutdown(); 187 } 188 } 189 190 /** 191 * Tests that if a ProxySelector is set, any HTTP proxies selected for 192 * ftp:// URLs will be rejected. A direct connection will 193 * be selected once the ProxySelector's proxies have failed. 194 */ testConnectOverProxy_implicit_http_fails()195 public void testConnectOverProxy_implicit_http_fails() throws Exception { 196 byte[] fileContents = "abcdef 1234567890".getBytes(UTF_8); 197 URL fileUrl = addFileEntry(FILE_PATH, fileContents); 198 ProxySelector defaultProxySelector = ProxySelector.getDefault(); 199 try { 200 CountingProxy countingProxy = CountingProxy.start(); 201 try { 202 Proxy proxy = countingProxy.asProxy(); 203 SingleProxySelector proxySelector = new SingleProxySelector(proxy); 204 ProxySelector.setDefault(proxySelector); 205 URLConnection connection = fileUrl.openConnection(); 206 InputStream inputStream = connection.getInputStream(); 207 208 IOException e = proxySelector.getLastException(); 209 assertEquals("FTP connections over HTTP proxy not supported", 210 e.getMessage()); 211 212 // The direct connection is successful 213 assertContents(fileContents, inputStream); 214 countingProxy.waitAndAssertConnectionCount(0); 215 } finally { 216 countingProxy.shutdown(); 217 } 218 } finally { 219 ProxySelector.setDefault(defaultProxySelector); 220 } 221 } 222 testInputUrlWithSpaces()223 public void testInputUrlWithSpaces() throws Exception { 224 byte[] fileContents = "abcdef 1234567890".getBytes(UTF_8); 225 URL url = addFileEntry("file with spaces.txt", fileContents); 226 URLConnection connection = url.openConnection(); 227 assertContents(fileContents, connection.getInputStream()); 228 } 229 testBinaryFileContents()230 public void testBinaryFileContents() throws Exception { 231 byte[] data = new byte[4096]; 232 new Random(31337).nextBytes(data); // arbitrary pseudo-random but repeatable test data 233 URL url = addFileEntry("binaryfile.dat", data.clone()); 234 assertContents(data, url.openConnection().getInputStream()); 235 } 236 237 // https://code.google.com/p/android/issues/detail?id=160725 testInputUrlWithSpacesViaProxySelector()238 public void testInputUrlWithSpacesViaProxySelector() throws Exception { 239 byte[] fileContents = "abcdef 1234567890".getBytes(UTF_8); 240 ProxySelector defaultProxySelector = ProxySelector.getDefault(); 241 try { 242 SingleProxySelector proxySelector = new SingleProxySelector(Proxy.NO_PROXY); 243 ProxySelector.setDefault(proxySelector); 244 URL url = addFileEntry("file with spaces.txt", fileContents); 245 assertContents(fileContents, url.openConnection().getInputStream()); 246 assertNull(proxySelector.getLastException()); 247 } finally { 248 ProxySelector.setDefault(defaultProxySelector); 249 } 250 } 251 252 // http://b/35784677 testCRLFInUserinfo()253 public void testCRLFInUserinfo() throws Exception { 254 int serverPort = fakeFtpServer.getServerControlPort(); 255 List<String> encodedUserInfos = Arrays.asList( 256 // '\r\n' in the username with password 257 "user%0D%0Acommand:password", 258 // '\r\n' in the password 259 "user:password%0D%0Acommand", 260 // just '\n' in the password 261 "user:password%0Acommand", 262 // just '\n' in the username 263 "user%0Acommand:password" 264 ); 265 for (String encodedUserInfo : encodedUserInfos) { 266 String urlString = String.format(Locale.US, "ftp://%s@%s:%s/%s", 267 encodedUserInfo, SERVER_HOSTNAME, serverPort, FILE_PATH); 268 try { 269 new URL(urlString).openConnection().connect(); 270 fail("Connection shouldn't have succeeded: " + urlString); 271 } catch (FtpLoginException expected) { 272 // The original message "Illegal carriage return" gets lost 273 // where FtpURLConnection.connect() translates the 274 // original FtpProtocolException into FtpLoginException. 275 assertEquals("Invalid username/password", expected.getMessage()); 276 } 277 } 278 } 279 openFileSystemContents(String fileName)280 private InputStream openFileSystemContents(String fileName) throws IOException { 281 String fullFileName = VALID_USER_HOME_DIR + "/" + fileName; 282 FileEntry entry = (FileEntry) fileSystem.getEntry(fullFileName); 283 assertNotNull("File must exist with name " + fullFileName, entry); 284 return entry.createInputStream(); 285 } 286 writeBytes(OutputStream os, byte[] fileContents)287 private static void writeBytes(OutputStream os, byte[] fileContents) throws IOException { 288 os.write(fileContents); 289 os.close(); 290 } 291 assertContents(byte[] expectedContents, InputStream inputStream)292 private static void assertContents(byte[] expectedContents, InputStream inputStream) 293 throws IOException { 294 try { 295 byte[] contentBytes = IoUtil.readBytes(inputStream); 296 if (!Arrays.equals(expectedContents, contentBytes)) { 297 // optimize the error message for the case of the content being character data 298 fail("Expected " + new String(expectedContents, UTF_8) + ", but got " 299 + new String(contentBytes, UTF_8)); 300 } 301 } finally { 302 inputStream.close(); 303 } 304 } 305 getFileUrlWithCredentials(String user, String password, String filePath)306 private URL getFileUrlWithCredentials(String user, String password, String filePath) { 307 Objects.requireNonNull(user); 308 Objects.requireNonNull(filePath); 309 int serverPort = fakeFtpServer.getServerControlPort(); 310 String credentials = user + (password == null ? "" : (":" + password)); 311 String urlString = String.format(Locale.US, "ftp://%s@%s:%s/%s", 312 credentials, SERVER_HOSTNAME, serverPort, filePath); 313 try { 314 return new URL(urlString); 315 } catch (MalformedURLException e) { 316 fail("Malformed URL: " + urlString); 317 throw new AssertionError("Can never happen"); 318 } 319 } 320 addFileEntry(String filePath, byte[] fileContents)321 private URL addFileEntry(String filePath, byte[] fileContents) { 322 FileEntry fileEntry = new FileEntry(VALID_USER_HOME_DIR + "/" + filePath); 323 fileEntry.setContents(fileContents); 324 fileSystem.add(fileEntry); 325 return getFileUrlWithCredentials(VALID_USER, VALID_PASSWORD, filePath); 326 } 327 328 /** 329 * A {@link ProxySelector} that selects the same (given) Proxy for all URIs. 330 */ 331 static class SingleProxySelector extends ProxySelector { 332 private final Proxy proxy; 333 private IOException lastException = null; 334 SingleProxySelector(Proxy proxy)335 public SingleProxySelector(Proxy proxy) { 336 this.proxy = proxy; 337 } 338 339 @Override select(URI uri)340 public List<Proxy> select(URI uri) { 341 assertNotNull(uri); 342 return Collections.singletonList(proxy); 343 } 344 345 @Override connectFailed(URI uri, SocketAddress sa, IOException ioe)346 public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { 347 lastException = ioe; 348 } 349 getLastException()350 public IOException getLastException() { 351 return lastException; 352 } 353 } 354 355 /** 356 * Counts the number of attempts to connect to a ServerSocket exposed 357 * {@link #asProxy() as a Proxy}. From {@link #start()} until 358 * {@link #shutdown()}, a background server thread accepts and counts 359 * connections on the socket but immediately closes them without 360 * reading any data. 361 */ 362 static class CountingProxy { 363 class ServerThread extends Thread { ServerThread(String name)364 public ServerThread(String name) { 365 super(name); 366 } 367 368 @Override run()369 public void run() { 370 while (true) { 371 try { 372 Socket socket = serverSocket.accept(); 373 connectionAttempts.release(1); // count one connection attempt 374 socket.close(); 375 } catch (SocketException e) { 376 shutdownLatch.countDown(); 377 return; 378 } catch (IOException e) { 379 // retry 380 } 381 } 382 } 383 } 384 385 // Signals that serverThread has gracefully completed shutdown (not crashed) 386 private final CountDownLatch shutdownLatch = new CountDownLatch(1); 387 private final ServerSocket serverSocket; 388 private final Proxy proxy; 389 private final Thread serverThread; 390 // holds one permit for each connection attempt encountered; this allows 391 // us to block until a certain number of attempts have taken place. 392 private final Semaphore connectionAttempts = new Semaphore(0); 393 CountingProxy()394 private CountingProxy() throws IOException { 395 serverSocket = new ServerSocket(0 /* allocate port number automatically */); 396 SocketAddress socketAddress = serverSocket.getLocalSocketAddress(); 397 proxy = new Proxy(Proxy.Type.HTTP, socketAddress); 398 String threadName = getClass().getSimpleName() + " @ " + socketAddress; 399 serverThread = new ServerThread(threadName); 400 } 401 start()402 public static CountingProxy start() throws IOException { 403 CountingProxy result = new CountingProxy(); 404 // only start the thread once the object has been properly constructed 405 result.serverThread.start(); 406 try { 407 // Give ServerThread time to call accept(). 408 Thread.sleep(300); 409 } catch (InterruptedException e) { 410 throw new IOException("Unexpectedly interrupted", e); 411 } 412 return result; 413 } 414 415 /** 416 * Returns the HTTP {@link Proxy} that can represents the ServerSocket 417 * connections to which this class manages/counts. 418 */ asProxy()419 public Proxy asProxy() { 420 return proxy; 421 } 422 423 /** 424 * Causes the ServerSocket represented by {@link #asProxy()} to stop accepting 425 * connections by shutting down the server thread. 426 * 427 * @return the number of connections that were attempted during the proxy's lifetime 428 */ waitAndAssertConnectionCount(int expectedConnectionAttempts)429 public void waitAndAssertConnectionCount(int expectedConnectionAttempts) 430 throws IOException, InterruptedException { 431 // Wait for a timeout, or fail early if expected # of connections is exceeded 432 boolean tooManyConnections = connectionAttempts.tryAcquire( 433 expectedConnectionAttempts + 1, 300, TimeUnit.MILLISECONDS); 434 assertFalse("Observed more connections than the expected " + expectedConnectionAttempts, 435 tooManyConnections); 436 assertEquals(expectedConnectionAttempts, connectionAttempts.availablePermits()); 437 } 438 shutdown()439 public void shutdown() throws IOException, InterruptedException { 440 serverSocket.close(); 441 // Check that the server shuts down quickly and gracefully via the expected 442 // code path (as opposed to an uncaught exception). 443 shutdownLatch.await(1, TimeUnit.SECONDS); 444 serverThread.join(1000); 445 assertFalse("serverThread failed to shut down quickly", serverThread.isAlive()); 446 } 447 448 @Override toString()449 public String toString() { 450 return serverThread.toString() ; 451 } 452 } 453 454 } 455