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