001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.vfs2.provider.ftp;
018
019import java.io.IOException;
020import java.io.PrintWriter;
021import java.io.StringWriter;
022import java.io.Writer;
023import java.net.Proxy;
024import java.time.Duration;
025
026import org.apache.commons.lang3.Range;
027import org.apache.commons.lang3.StringUtils;
028import org.apache.commons.lang3.time.DurationUtils;
029import org.apache.commons.logging.Log;
030import org.apache.commons.logging.LogFactory;
031import org.apache.commons.net.PrintCommandListener;
032import org.apache.commons.net.ftp.FTPClient;
033import org.apache.commons.net.ftp.FTPClientConfig;
034import org.apache.commons.net.ftp.FTPReply;
035import org.apache.commons.net.ftp.parser.FTPFileEntryParserFactory;
036import org.apache.commons.vfs2.FileSystemException;
037import org.apache.commons.vfs2.FileSystemOptions;
038import org.apache.commons.vfs2.util.UserAuthenticatorUtils;
039
040/**
041 * Creates {@link FtpClient} instances.
042 */
043public final class FtpClientFactory {
044
045    /**
046     * Abstract Factory, used to configure different FTPClients.
047     *
048     * @param <C> The type of FTPClient.
049     * @param <B> The type of FtpFileSystemConfigBuilder
050     */
051    public abstract static class ConnectionFactory<C extends FTPClient, B extends FtpFileSystemConfigBuilder> {
052
053        private static final char[] ANON_CHAR_ARRAY = "anonymous".toCharArray();
054        private static final int BUFSZ = 40;
055
056        /**
057         * My builder.
058         */
059        protected B builder;
060
061        private final Log log = LogFactory.getLog(getClass());
062
063        /**
064         * Constructs a new instance.
065         *
066         * @param builder How to build.
067         */
068        protected ConnectionFactory(final B builder) {
069            this.builder = builder;
070        }
071
072        private void configureClient(final FileSystemOptions fileSystemOptions, final C client) {
073            final String key = builder.getEntryParser(fileSystemOptions);
074            if (key != null) {
075                final FTPClientConfig config = new FTPClientConfig(key);
076                final String serverLanguageCode = builder.getServerLanguageCode(fileSystemOptions);
077                if (serverLanguageCode != null) {
078                    config.setServerLanguageCode(serverLanguageCode);
079                }
080                final String defaultDateFormat = builder.getDefaultDateFormat(fileSystemOptions);
081                if (defaultDateFormat != null) {
082                    config.setDefaultDateFormatStr(defaultDateFormat);
083                }
084                final String recentDateFormat = builder.getRecentDateFormat(fileSystemOptions);
085                if (recentDateFormat != null) {
086                    config.setRecentDateFormatStr(recentDateFormat);
087                }
088                final String serverTimeZoneId = builder.getServerTimeZoneId(fileSystemOptions);
089                if (serverTimeZoneId != null) {
090                    config.setServerTimeZoneId(serverTimeZoneId);
091                }
092                final String[] shortMonthNames = builder.getShortMonthNames(fileSystemOptions);
093                if (shortMonthNames != null) {
094                    final StringBuilder shortMonthNamesStr = new StringBuilder(BUFSZ);
095                    for (final String shortMonthName : shortMonthNames) {
096                        if (!StringUtils.isEmpty(shortMonthNamesStr)) {
097                            shortMonthNamesStr.append("|");
098                        }
099                        shortMonthNamesStr.append(shortMonthName);
100                    }
101                    config.setShortMonthNames(shortMonthNamesStr.toString());
102                }
103                client.configure(config);
104            }
105        }
106
107        /**
108         * Creates a new client.
109         *
110         * @param fileSystemOptions the file system options.
111         * @return a new client.
112         * @throws FileSystemException if a file system error occurs.
113         */
114        protected abstract C createClient(FileSystemOptions fileSystemOptions) throws FileSystemException;
115
116        /**
117         * Creates a connection.
118         *
119         * @param hostname The host name or IP address.
120         * @param port The host port.
121         * @param username The user name.
122         * @param password The user password.
123         * @param workingDirectory The working directory.
124         * @param fileSystemOptions Options to create the connection.
125         * @return A new connection.
126         * @throws FileSystemException if an error occurs while establishing a connection.
127         */
128        public C createConnection(final String hostname, final int port, char[] username, char[] password,
129                final String workingDirectory, final FileSystemOptions fileSystemOptions) throws FileSystemException {
130            // Determine the username and password to use
131            if (username == null) {
132                username = ANON_CHAR_ARRAY;
133            }
134            if (password == null) {
135                password = ANON_CHAR_ARRAY;
136            }
137            try {
138                final C client = createClient(fileSystemOptions);
139                if (log.isDebugEnabled()) {
140                    final Writer writer = new StringWriter(1024) {
141                        @Override
142                        public void flush() {
143                            final StringBuffer buffer = getBuffer();
144                            String message = buffer.toString();
145                            final String prefix = "PASS ";
146                            if (message.toUpperCase().startsWith(prefix) && message.length() > prefix.length()) {
147                                message = prefix + "***";
148                            }
149                            log.debug(message);
150                            buffer.setLength(0);
151                        }
152                    };
153                    client.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(writer)));
154                }
155                configureClient(fileSystemOptions, client);
156                final FTPFileEntryParserFactory myFactory = builder.getEntryParserFactory(fileSystemOptions);
157                if (myFactory != null) {
158                    client.setParserFactory(myFactory);
159                }
160                final Boolean remoteVerification = builder.getRemoteVerification(fileSystemOptions);
161                if (remoteVerification != null) {
162                    client.setRemoteVerificationEnabled(remoteVerification.booleanValue());
163                }
164                try {
165                    final Duration connectTimeout = builder.getConnectTimeoutDuration(fileSystemOptions);
166                    if (connectTimeout != null) {
167                        client.setDefaultTimeout(DurationUtils.toMillisInt(connectTimeout));
168                    }
169                    final String controlEncoding = builder.getControlEncoding(fileSystemOptions);
170                    if (controlEncoding != null) {
171                        client.setControlEncoding(controlEncoding);
172                    }
173                    final Boolean autodetectUTF8 = builder.getAutodetectUtf8(fileSystemOptions);
174                    if (autodetectUTF8 != null) {
175                        client.setAutodetectUTF8(autodetectUTF8);
176                    }
177                    final Proxy proxy = builder.getProxy(fileSystemOptions);
178                    if (proxy != null) {
179                        client.setProxy(proxy);
180                    }
181                    client.connect(hostname, port);
182                    final int reply = client.getReplyCode();
183                    if (!FTPReply.isPositiveCompletion(reply)) {
184                        throw new FileSystemException("vfs.provider.ftp/connect-rejected.error", hostname);
185                    }
186                    // Login
187                    if (!client.login(UserAuthenticatorUtils.toString(username), UserAuthenticatorUtils.toString(password))) {
188                        throw new FileSystemException("vfs.provider.ftp/login.error", hostname, UserAuthenticatorUtils.toString(username));
189                    }
190                    FtpFileType fileType = builder.getFileType(fileSystemOptions);
191                    if (fileType == null) {
192                        fileType = FtpFileType.BINARY;
193                    }
194                    // Set binary mode
195                    if (!client.setFileType(fileType.getValue())) {
196                        throw new FileSystemException("vfs.provider.ftp/set-file-type.error", fileType);
197                    }
198                    // Set dataTimeout value
199                    final Duration dataTimeout = builder.getDataTimeoutDuration(fileSystemOptions);
200                    if (dataTimeout != null) {
201                        client.setDataTimeout(dataTimeout);
202                    }
203                    final Duration socketTimeout = builder.getSoTimeoutDuration(fileSystemOptions);
204                    if (socketTimeout != null) {
205                        client.setSoTimeout(DurationUtils.toMillisInt(socketTimeout));
206                    }
207                    final Duration controlKeepAliveTimeout = builder.getControlKeepAliveTimeout(fileSystemOptions);
208                    if (controlKeepAliveTimeout != null) {
209                        client.setControlKeepAliveTimeout(controlKeepAliveTimeout);
210                    }
211                    final Duration controlKeepAliveReplyTimeout = builder.getControlKeepAliveReplyTimeout(fileSystemOptions);
212                    if (controlKeepAliveReplyTimeout != null) {
213                        client.setControlKeepAliveReplyTimeout(controlKeepAliveReplyTimeout);
214                    }
215                    final Boolean userDirIsRoot = builder.getUserDirIsRoot(fileSystemOptions);
216                    if (workingDirectory != null && (userDirIsRoot == null || !userDirIsRoot.booleanValue())
217                            && !client.changeWorkingDirectory(workingDirectory)) {
218                        throw new FileSystemException("vfs.provider.ftp/change-work-directory.error", workingDirectory);
219                    }
220                    final Boolean passiveMode = builder.getPassiveMode(fileSystemOptions);
221                    if (passiveMode != null && passiveMode.booleanValue()) {
222                        client.enterLocalPassiveMode();
223                    }
224                    final Range<Integer> activePortRange = builder.getActivePortRange(fileSystemOptions);
225                    if (activePortRange != null) {
226                        client.setActivePortRange(activePortRange.getMinimum(), activePortRange.getMaximum());
227                    }
228                    setupOpenConnection(client, fileSystemOptions);
229                } catch (final IOException e) {
230                    if (client.isConnected()) {
231                        client.disconnect();
232                    }
233                    throw e;
234                }
235                return client;
236            } catch (final Exception exc) {
237                throw new FileSystemException("vfs.provider.ftp/connect.error", exc, hostname);
238            }
239        }
240
241        /**
242         * Sets up a new client.
243         *
244         * @param client the client.
245         * @param fileSystemOptions the file system options.
246         * @throws IOException if an IO error occurs.
247         */
248        protected abstract void setupOpenConnection(C client, FileSystemOptions fileSystemOptions) throws IOException;
249    }
250
251    /**
252     * Connection Factory, used to configure the FTPClient.
253     */
254    public static final class FtpConnectionFactory extends ConnectionFactory<FTPClient, FtpFileSystemConfigBuilder> {
255        private FtpConnectionFactory(final FtpFileSystemConfigBuilder builder) {
256            super(builder);
257        }
258
259        @Override
260        protected FTPClient createClient(final FileSystemOptions fileSystemOptions) {
261            return new FTPClient();
262        }
263
264        @Override
265        protected void setupOpenConnection(final FTPClient client, final FileSystemOptions fileSystemOptions) {
266            // nothing to do for FTP
267        }
268    }
269
270    /**
271     * Creates a new connection to the server.
272     *
273     * @param hostname The host name of the server.
274     * @param port The port to connect to.
275     * @param username The name of the user for authentication.
276     * @param password The user's password.
277     * @param workingDirectory The base directory.
278     * @param fileSystemOptions The FileSystemOptions.
279     * @return An FTPClient.
280     * @throws FileSystemException if an error occurs while establishing a connection.
281     */
282    public static FTPClient createConnection(final String hostname, final int port, final char[] username,
283            final char[] password, final String workingDirectory, final FileSystemOptions fileSystemOptions)
284            throws FileSystemException {
285        final FtpConnectionFactory factory = new FtpConnectionFactory(FtpFileSystemConfigBuilder.getInstance());
286        return factory.createConnection(hostname, port, username, password, workingDirectory, fileSystemOptions);
287    }
288
289    private FtpClientFactory() {
290    }
291}