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