blob: 294f70690bb8337e7e5f2df69b7c4e3df57fc19e [file] [log] [blame]
/*
* 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 libcore.net.url;
import libcore.net.NetworkSecurityPolicy;
import java.io.BufferedInputStream;
import java.io.EOFException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketPermission;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.nio.charset.StandardCharsets;
import java.security.Permission;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class FtpURLConnection extends URLConnection {
private static final int FTP_PORT = 21;
// FTP Reply Constants
private static final int FTP_DATAOPEN = 125;
private static final int FTP_OPENDATA = 150;
private static final int FTP_OK = 200;
private static final int FTP_USERREADY = 220;
private static final int FTP_TRANSFEROK = 226;
// private static final int FTP_PASV = 227;
private static final int FTP_LOGGEDIN = 230;
private static final int FTP_FILEOK = 250;
private static final int FTP_PASWD = 331;
// private static final int FTP_DATAERROR = 451;
// private static final int FTP_ERROR = 500;
private static final int FTP_NOTFOUND = 550;
private Socket controlSocket;
private Socket dataSocket;
private ServerSocket acceptSocket;
private InputStream ctrlInput;
private InputStream inputStream;
private OutputStream ctrlOutput;
private int dataPort;
private String username = "anonymous";
private String password = "";
private String replyCode;
private String hostName;
private Proxy proxy;
private Proxy currentProxy;
private URI uri;
/**
* FtpURLConnection constructor comment.
*
* @param url
*/
protected FtpURLConnection(URL url) throws IOException {
super(url);
hostName = url.getHost();
if (!NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted()) {
// Cleartext network traffic is not permitted -- refuse this connection.
throw new IOException("Cleartext traffic not permitted: "
+ url.getProtocol() + "://" + hostName
+ ((url.getPort() >= 0) ? (":" + url.getPort()) : ""));
}
String parse = url.getUserInfo();
if (parse != null) {
int split = parse.indexOf(':');
if (split >= 0) {
username = parse.substring(0, split);
password = parse.substring(split + 1);
} else {
username = parse;
}
}
uri = null;
try {
uri = url.toURILenient();
} catch (URISyntaxException e) {
// do nothing.
}
}
/**
* FtpURLConnection constructor.
*
* @param url
* @param proxy
*/
protected FtpURLConnection(URL url, Proxy proxy) throws IOException {
this(url);
this.proxy = proxy;
}
/**
* Change the server directory to that specified in the URL
*/
private void cd() throws IOException {
int idx = url.getFile().lastIndexOf('/');
if (idx > 0) {
String dir = url.getFile().substring(0, idx);
write("CWD " + dir + "\r\n");
int reply = getReply();
if (reply != FTP_FILEOK && dir.length() > 0 && dir.charAt(0) == '/') {
write("CWD " + dir.substring(1) + "\r\n");
reply = getReply();
}
if (reply != FTP_FILEOK) {
throw new IOException("Unable to change directories");
}
}
}
/**
* Establishes the connection to the resource specified by this
* <code>URL</code>
*
* @see #connected
* @see java.io.IOException
* @see URLStreamHandler
*/
@Override
public void connect() throws IOException {
// Use system-wide ProxySelect to select proxy list,
// then try to connect via elements in the proxy list.
List<Proxy> proxyList = null;
if (proxy != null) {
proxyList = new ArrayList<Proxy>(1);
proxyList.add(proxy);
} else {
ProxySelector selector = ProxySelector.getDefault();
if (selector != null) {
proxyList = selector.select(uri);
}
}
if (proxyList == null) {
currentProxy = null;
connectInternal();
} else {
ProxySelector selector = ProxySelector.getDefault();
Iterator<Proxy> iter = proxyList.iterator();
boolean connectOK = false;
String failureReason = "";
while (iter.hasNext() && !connectOK) {
currentProxy = iter.next();
try {
connectInternal();
connectOK = true;
} catch (IOException ioe) {
failureReason = ioe.getLocalizedMessage();
// If connect failed, callback "connectFailed"
// should be invoked.
if (selector != null && Proxy.NO_PROXY != currentProxy) {
selector.connectFailed(uri, currentProxy.address(), ioe);
}
}
}
if (!connectOK) {
throw new IOException("Unable to connect to server: " + failureReason);
}
}
}
private void connectInternal() throws IOException {
int port = url.getPort();
int connectTimeout = getConnectTimeout();
if (port <= 0) {
port = FTP_PORT;
}
if (currentProxy == null || Proxy.Type.HTTP == currentProxy.type()) {
controlSocket = new Socket();
} else {
controlSocket = new Socket(currentProxy);
}
InetSocketAddress addr = new InetSocketAddress(hostName, port);
controlSocket.connect(addr, connectTimeout);
connected = true;
ctrlOutput = controlSocket.getOutputStream();
ctrlInput = controlSocket.getInputStream();
login();
setType();
if (!getDoInput()) {
cd();
}
try {
acceptSocket = new ServerSocket(0);
dataPort = acceptSocket.getLocalPort();
/* Cannot set REUSEADDR so we need to send a PORT command */
port();
if (connectTimeout == 0) {
// set timeout rather than zero as before
connectTimeout = 3000;
}
acceptSocket.setSoTimeout(getConnectTimeout());
if (getDoInput()) {
getFile();
} else {
sendFile();
}
dataSocket = acceptSocket.accept();
dataSocket.setSoTimeout(getReadTimeout());
acceptSocket.close();
} catch (InterruptedIOException e) {
throw new IOException("Could not establish data connection");
}
if (getDoInput()) {
inputStream = new FtpURLInputStream(
new BufferedInputStream(dataSocket.getInputStream()), controlSocket);
}
}
/**
* Returns the content type of the resource. Just takes a guess based on the
* name.
*/
@Override
public String getContentType() {
String result = guessContentTypeFromName(url.getFile());
if (result == null) {
return "content/unknown";
}
return result;
}
private void getFile() throws IOException {
int reply;
String file = url.getFile();
write("RETR " + file + "\r\n");
reply = getReply();
if (reply == FTP_NOTFOUND && file.length() > 0 && file.charAt(0) == '/') {
write("RETR " + file.substring(1) + "\r\n");
reply = getReply();
}
if (!(reply == FTP_OPENDATA || reply == FTP_TRANSFEROK)) {
throw new FileNotFoundException("Unable to retrieve file: " + reply);
}
}
/**
* Creates a input stream for writing to this URL Connection.
*
* @return The input stream to write to
* @throws IOException
* Cannot read from URL or error creating InputStream
*
* @see #getContent()
* @see #getOutputStream()
* @see java.io.InputStream
* @see java.io.IOException
*
*/
@Override
public InputStream getInputStream() throws IOException {
if (!connected) {
connect();
}
return inputStream;
}
/**
* Returns the permission object (in this case, SocketPermission) with the
* host and the port number as the target name and "resolve, connect" as the
* action list.
*
* @return the permission object required for this connection
* @throws IOException
* thrown when an IO exception occurs during the creation of the
* permission object.
*/
@Override
public Permission getPermission() throws IOException {
int port = url.getPort();
if (port <= 0) {
port = FTP_PORT;
}
return new SocketPermission(hostName + ":" + port, "connect, resolve");
}
/**
* Creates a output stream for writing to this URL Connection.
*
* @return The output stream to write to
* @throws IOException
* when the OutputStream could not be created
*
* @see #getContent()
* @see #getInputStream()
* @see java.io.IOException
*
*/
@Override
public OutputStream getOutputStream() throws IOException {
if (!connected) {
connect();
}
return dataSocket.getOutputStream();
}
private int getReply() throws IOException {
byte[] code = new byte[3];
for (int i = 0; i < code.length; i++) {
final int tmp = ctrlInput.read();
if (tmp == -1) {
throw new EOFException();
}
code[i] = (byte) tmp;
}
replyCode = new String(code, 0, code.length, StandardCharsets.ISO_8859_1);
boolean multiline = false;
if (ctrlInput.read() == '-') {
multiline = true;
}
readLine(); /* Skip the rest of the first line */
if (multiline) {
while (readMultiLine()) {/* Read all of a multiline reply */
}
}
try {
return Integer.parseInt(replyCode);
} catch (NumberFormatException e) {
throw (IOException)(new IOException("reply code is invalid").initCause(e));
}
}
private void login() throws IOException {
int reply;
reply = getReply();
if (reply == FTP_USERREADY) {
} else {
throw new IOException("Unable to connect to server: " + url.getHost());
}
write("USER " + username + "\r\n");
reply = getReply();
if (reply == FTP_PASWD || reply == FTP_LOGGEDIN) {
} else {
throw new IOException("Unable to log in to server (USER): " + url.getHost());
}
if (reply == FTP_PASWD) {
write("PASS " + password + "\r\n");
reply = getReply();
if (!(reply == FTP_OK || reply == FTP_USERREADY || reply == FTP_LOGGEDIN)) {
throw new IOException("Unable to log in to server (PASS): " + url.getHost());
}
}
}
private void port() throws IOException {
write("PORT "
+ controlSocket.getLocalAddress().getHostAddress().replace('.',
',') + ',' + (dataPort >> 8) + ','
+ (dataPort & 255)
+ "\r\n");
if (getReply() != FTP_OK) {
throw new IOException("Unable to configure data port");
}
}
/**
* Read a line of text and return it for possible parsing
*/
private String readLine() throws IOException {
StringBuilder sb = new StringBuilder();
int c;
while ((c = ctrlInput.read()) != '\n') {
sb.append((char) c);
}
return sb.toString();
}
private boolean readMultiLine() throws IOException {
String line = readLine();
if (line.length() < 4) {
return true;
}
if (line.substring(0, 3).equals(replyCode)
&& (line.charAt(3) == (char) 32)) {
return false;
}
return true;
}
/**
* Issue the STOR command to the server with the file as the parameter
*/
private void sendFile() throws IOException {
int reply;
write("STOR "
+ url.getFile().substring(url.getFile().lastIndexOf('/') + 1,
url.getFile().length()) + "\r\n");
reply = getReply();
if (!(reply == FTP_OPENDATA || reply == FTP_OK || reply == FTP_DATAOPEN)) {
throw new IOException("Unable to store file");
}
}
/**
* Set the flag if this <code>URLConnection</code> supports input (read).
* It cannot be set after the connection is made. FtpURLConnections cannot
* support both input and output
*
* @param newValue *
* @throws IllegalAccessError
* when this method attempts to change the flag after connected
*
* @see #doInput
* @see #getDoInput()
* @see java.lang.IllegalAccessError
* @see #setDoInput(boolean)
*/
@Override
public void setDoInput(boolean newValue) {
if (connected) {
throw new IllegalAccessError();
}
this.doInput = newValue;
this.doOutput = !newValue;
}
/**
* Set the flag if this <code>URLConnection</code> supports output(read).
* It cannot be set after the connection is made.\ FtpURLConnections cannot
* support both input and output.
*
* @param newValue
*
* @throws IllegalAccessError
* when this method attempts to change the flag after connected
*
* @see #doOutput
* @see java.lang.IllegalAccessError
* @see #setDoOutput(boolean)
*/
@Override
public void setDoOutput(boolean newValue) {
if (connected) {
throw new IllegalAccessError();
}
this.doOutput = newValue;
this.doInput = !newValue;
}
/**
* Set the type of the file transfer. Only Image is supported
*/
private void setType() throws IOException {
write("TYPE I\r\n");
if (getReply() != FTP_OK) {
throw new IOException("Unable to set transfer type");
}
}
private void write(String command) throws IOException {
ctrlOutput.write(command.getBytes(StandardCharsets.ISO_8859_1));
}
}