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 * http://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 18package libcore.net.url; 19 20import java.io.BufferedInputStream; 21import java.io.EOFException; 22import java.io.FileNotFoundException; 23import java.io.IOException; 24import java.io.InputStream; 25import java.io.InterruptedIOException; 26import java.io.OutputStream; 27import java.net.InetSocketAddress; 28import java.net.Proxy; 29import java.net.ProxySelector; 30import java.net.ServerSocket; 31import java.net.Socket; 32import java.net.SocketPermission; 33import java.net.URI; 34import java.net.URISyntaxException; 35import java.net.URL; 36import java.net.URLConnection; 37import java.net.URLStreamHandler; 38import java.nio.charset.StandardCharsets; 39import java.security.Permission; 40import java.util.ArrayList; 41import java.util.Iterator; 42import java.util.List; 43 44public class FtpURLConnection extends URLConnection { 45 46 private static final int FTP_PORT = 21; 47 48 // FTP Reply Constants 49 private static final int FTP_DATAOPEN = 125; 50 51 private static final int FTP_OPENDATA = 150; 52 53 private static final int FTP_OK = 200; 54 55 private static final int FTP_USERREADY = 220; 56 57 private static final int FTP_TRANSFEROK = 226; 58 59 // private static final int FTP_PASV = 227; 60 61 private static final int FTP_LOGGEDIN = 230; 62 63 private static final int FTP_FILEOK = 250; 64 65 private static final int FTP_PASWD = 331; 66 67 // private static final int FTP_DATAERROR = 451; 68 69 // private static final int FTP_ERROR = 500; 70 71 private static final int FTP_NOTFOUND = 550; 72 73 private Socket controlSocket; 74 75 private Socket dataSocket; 76 77 private ServerSocket acceptSocket; 78 79 private InputStream ctrlInput; 80 81 private InputStream inputStream; 82 83 private OutputStream ctrlOutput; 84 85 private int dataPort; 86 87 private String username = "anonymous"; 88 89 private String password = ""; 90 91 private String replyCode; 92 93 private String hostName; 94 95 private Proxy proxy; 96 97 private Proxy currentProxy; 98 99 private URI uri; 100 101 /** 102 * FtpURLConnection constructor comment. 103 * 104 * @param url 105 */ 106 protected FtpURLConnection(URL url) { 107 super(url); 108 hostName = url.getHost(); 109 String parse = url.getUserInfo(); 110 if (parse != null) { 111 int split = parse.indexOf(':'); 112 if (split >= 0) { 113 username = parse.substring(0, split); 114 password = parse.substring(split + 1); 115 } else { 116 username = parse; 117 } 118 } 119 uri = null; 120 try { 121 uri = url.toURI(); 122 } catch (URISyntaxException e) { 123 // do nothing. 124 } 125 } 126 127 /** 128 * FtpURLConnection constructor. 129 * 130 * @param url 131 * @param proxy 132 */ 133 protected FtpURLConnection(URL url, Proxy proxy) { 134 this(url); 135 this.proxy = proxy; 136 } 137 138 /** 139 * Change the server directory to that specified in the URL 140 */ 141 private void cd() throws IOException { 142 int idx = url.getFile().lastIndexOf('/'); 143 144 if (idx > 0) { 145 String dir = url.getFile().substring(0, idx); 146 write("CWD " + dir + "\r\n"); 147 int reply = getReply(); 148 if (reply != FTP_FILEOK && dir.length() > 0 && dir.charAt(0) == '/') { 149 write("CWD " + dir.substring(1) + "\r\n"); 150 reply = getReply(); 151 } 152 if (reply != FTP_FILEOK) { 153 throw new IOException("Unable to change directories"); 154 } 155 } 156 } 157 158 /** 159 * Establishes the connection to the resource specified by this 160 * <code>URL</code> 161 * 162 * @see #connected 163 * @see java.io.IOException 164 * @see URLStreamHandler 165 */ 166 @Override 167 public void connect() throws IOException { 168 // Use system-wide ProxySelect to select proxy list, 169 // then try to connect via elements in the proxy list. 170 List<Proxy> proxyList = null; 171 if (proxy != null) { 172 proxyList = new ArrayList<Proxy>(1); 173 proxyList.add(proxy); 174 } else { 175 ProxySelector selector = ProxySelector.getDefault(); 176 if (selector != null) { 177 proxyList = selector.select(uri); 178 } 179 } 180 if (proxyList == null) { 181 currentProxy = null; 182 connectInternal(); 183 } else { 184 ProxySelector selector = ProxySelector.getDefault(); 185 Iterator<Proxy> iter = proxyList.iterator(); 186 boolean connectOK = false; 187 String failureReason = ""; 188 while (iter.hasNext() && !connectOK) { 189 currentProxy = iter.next(); 190 try { 191 connectInternal(); 192 connectOK = true; 193 } catch (IOException ioe) { 194 failureReason = ioe.getLocalizedMessage(); 195 // If connect failed, callback "connectFailed" 196 // should be invoked. 197 if (selector != null && Proxy.NO_PROXY != currentProxy) { 198 selector.connectFailed(uri, currentProxy.address(), ioe); 199 } 200 } 201 } 202 if (!connectOK) { 203 throw new IOException("Unable to connect to server: " + failureReason); 204 } 205 } 206 } 207 208 private void connectInternal() throws IOException { 209 int port = url.getPort(); 210 int connectTimeout = getConnectTimeout(); 211 if (port <= 0) { 212 port = FTP_PORT; 213 } 214 if (currentProxy == null || Proxy.Type.HTTP == currentProxy.type()) { 215 controlSocket = new Socket(); 216 } else { 217 controlSocket = new Socket(currentProxy); 218 } 219 InetSocketAddress addr = new InetSocketAddress(hostName, port); 220 controlSocket.connect(addr, connectTimeout); 221 connected = true; 222 ctrlOutput = controlSocket.getOutputStream(); 223 ctrlInput = controlSocket.getInputStream(); 224 login(); 225 setType(); 226 if (!getDoInput()) { 227 cd(); 228 } 229 230 try { 231 acceptSocket = new ServerSocket(0); 232 dataPort = acceptSocket.getLocalPort(); 233 /* Cannot set REUSEADDR so we need to send a PORT command */ 234 port(); 235 if (connectTimeout == 0) { 236 // set timeout rather than zero as before 237 connectTimeout = 3000; 238 } 239 acceptSocket.setSoTimeout(getConnectTimeout()); 240 if (getDoInput()) { 241 getFile(); 242 } else { 243 sendFile(); 244 } 245 dataSocket = acceptSocket.accept(); 246 dataSocket.setSoTimeout(getReadTimeout()); 247 acceptSocket.close(); 248 } catch (InterruptedIOException e) { 249 throw new IOException("Could not establish data connection"); 250 } 251 if (getDoInput()) { 252 inputStream = new FtpURLInputStream( 253 new BufferedInputStream(dataSocket.getInputStream()), controlSocket); 254 } 255 } 256 257 /** 258 * Returns the content type of the resource. Just takes a guess based on the 259 * name. 260 */ 261 @Override 262 public String getContentType() { 263 String result = guessContentTypeFromName(url.getFile()); 264 if (result == null) { 265 return "content/unknown"; 266 } 267 return result; 268 } 269 270 private void getFile() throws IOException { 271 int reply; 272 String file = url.getFile(); 273 write("RETR " + file + "\r\n"); 274 reply = getReply(); 275 if (reply == FTP_NOTFOUND && file.length() > 0 && file.charAt(0) == '/') { 276 write("RETR " + file.substring(1) + "\r\n"); 277 reply = getReply(); 278 } 279 if (!(reply == FTP_OPENDATA || reply == FTP_TRANSFEROK)) { 280 throw new FileNotFoundException("Unable to retrieve file: " + reply); 281 } 282 } 283 284 /** 285 * Creates a input stream for writing to this URL Connection. 286 * 287 * @return The input stream to write to 288 * @throws IOException 289 * Cannot read from URL or error creating InputStream 290 * 291 * @see #getContent() 292 * @see #getOutputStream() 293 * @see java.io.InputStream 294 * @see java.io.IOException 295 * 296 */ 297 @Override 298 public InputStream getInputStream() throws IOException { 299 if (!connected) { 300 connect(); 301 } 302 return inputStream; 303 } 304 305 /** 306 * Returns the permission object (in this case, SocketPermission) with the 307 * host and the port number as the target name and "resolve, connect" as the 308 * action list. 309 * 310 * @return the permission object required for this connection 311 * @throws IOException 312 * thrown when an IO exception occurs during the creation of the 313 * permission object. 314 */ 315 @Override 316 public Permission getPermission() throws IOException { 317 int port = url.getPort(); 318 if (port <= 0) { 319 port = FTP_PORT; 320 } 321 return new SocketPermission(hostName + ":" + port, "connect, resolve"); 322 } 323 324 /** 325 * Creates a output stream for writing to this URL Connection. 326 * 327 * @return The output stream to write to 328 * @throws IOException 329 * when the OutputStream could not be created 330 * 331 * @see #getContent() 332 * @see #getInputStream() 333 * @see java.io.IOException 334 * 335 */ 336 @Override 337 public OutputStream getOutputStream() throws IOException { 338 if (!connected) { 339 connect(); 340 } 341 return dataSocket.getOutputStream(); 342 } 343 344 private int getReply() throws IOException { 345 byte[] code = new byte[3]; 346 for (int i = 0; i < code.length; i++) { 347 final int tmp = ctrlInput.read(); 348 if (tmp == -1) { 349 throw new EOFException(); 350 } 351 code[i] = (byte) tmp; 352 } 353 replyCode = new String(code, 0, code.length, StandardCharsets.ISO_8859_1); 354 355 boolean multiline = false; 356 if (ctrlInput.read() == '-') { 357 multiline = true; 358 } 359 readLine(); /* Skip the rest of the first line */ 360 if (multiline) { 361 while (readMultiLine()) {/* Read all of a multiline reply */ 362 } 363 } 364 365 try { 366 return Integer.parseInt(replyCode); 367 } catch (NumberFormatException e) { 368 throw (IOException)(new IOException("reply code is invalid").initCause(e)); 369 } 370 } 371 372 private void login() throws IOException { 373 int reply; 374 reply = getReply(); 375 if (reply == FTP_USERREADY) { 376 } else { 377 throw new IOException("Unable to connect to server: " + url.getHost()); 378 } 379 write("USER " + username + "\r\n"); 380 reply = getReply(); 381 if (reply == FTP_PASWD || reply == FTP_LOGGEDIN) { 382 } else { 383 throw new IOException("Unable to log in to server (USER): " + url.getHost()); 384 } 385 if (reply == FTP_PASWD) { 386 write("PASS " + password + "\r\n"); 387 reply = getReply(); 388 if (!(reply == FTP_OK || reply == FTP_USERREADY || reply == FTP_LOGGEDIN)) { 389 throw new IOException("Unable to log in to server (PASS): " + url.getHost()); 390 } 391 } 392 } 393 394 private void port() throws IOException { 395 write("PORT " 396 + controlSocket.getLocalAddress().getHostAddress().replace('.', 397 ',') + ',' + (dataPort >> 8) + ',' 398 + (dataPort & 255) 399 + "\r\n"); 400 if (getReply() != FTP_OK) { 401 throw new IOException("Unable to configure data port"); 402 } 403 } 404 405 /** 406 * Read a line of text and return it for possible parsing 407 */ 408 private String readLine() throws IOException { 409 StringBuilder sb = new StringBuilder(); 410 int c; 411 while ((c = ctrlInput.read()) != '\n') { 412 sb.append((char) c); 413 } 414 return sb.toString(); 415 } 416 417 private boolean readMultiLine() throws IOException { 418 String line = readLine(); 419 if (line.length() < 4) { 420 return true; 421 } 422 if (line.substring(0, 3).equals(replyCode) 423 && (line.charAt(3) == (char) 32)) { 424 return false; 425 } 426 return true; 427 } 428 429 /** 430 * Issue the STOR command to the server with the file as the parameter 431 */ 432 private void sendFile() throws IOException { 433 int reply; 434 write("STOR " 435 + url.getFile().substring(url.getFile().lastIndexOf('/') + 1, 436 url.getFile().length()) + "\r\n"); 437 reply = getReply(); 438 if (!(reply == FTP_OPENDATA || reply == FTP_OK || reply == FTP_DATAOPEN)) { 439 throw new IOException("Unable to store file"); 440 } 441 } 442 443 /** 444 * Set the flag if this <code>URLConnection</code> supports input (read). 445 * It cannot be set after the connection is made. FtpURLConnections cannot 446 * support both input and output 447 * 448 * @param newValue * 449 * @throws IllegalAccessError 450 * when this method attempts to change the flag after connected 451 * 452 * @see #doInput 453 * @see #getDoInput() 454 * @see java.lang.IllegalAccessError 455 * @see #setDoInput(boolean) 456 */ 457 @Override 458 public void setDoInput(boolean newValue) { 459 if (connected) { 460 throw new IllegalAccessError(); 461 } 462 this.doInput = newValue; 463 this.doOutput = !newValue; 464 } 465 466 /** 467 * Set the flag if this <code>URLConnection</code> supports output(read). 468 * It cannot be set after the connection is made.\ FtpURLConnections cannot 469 * support both input and output. 470 * 471 * @param newValue 472 * 473 * @throws IllegalAccessError 474 * when this method attempts to change the flag after connected 475 * 476 * @see #doOutput 477 * @see java.lang.IllegalAccessError 478 * @see #setDoOutput(boolean) 479 */ 480 @Override 481 public void setDoOutput(boolean newValue) { 482 if (connected) { 483 throw new IllegalAccessError(); 484 } 485 this.doOutput = newValue; 486 this.doInput = !newValue; 487 } 488 489 /** 490 * Set the type of the file transfer. Only Image is supported 491 */ 492 private void setType() throws IOException { 493 write("TYPE I\r\n"); 494 if (getReply() != FTP_OK) { 495 throw new IOException("Unable to set transfer type"); 496 } 497 } 498 499 private void write(String command) throws IOException { 500 ctrlOutput.write(command.getBytes(StandardCharsets.ISO_8859_1)); 501 } 502} 503