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