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
077                final String serverLanguageCode = builder.getServerLanguageCode(fileSystemOptions);
078                if (serverLanguageCode != null) {
079                    config.setServerLanguageCode(serverLanguageCode);
080                }
081                final String defaultDateFormat = builder.getDefaultDateFormat(fileSystemOptions);
082                if (defaultDateFormat != null) {
083                    config.setDefaultDateFormatStr(defaultDateFormat);
084                }
085                final String recentDateFormat = builder.getRecentDateFormat(fileSystemOptions);
086                if (recentDateFormat != null) {
087                    config.setRecentDateFormatStr(recentDateFormat);
088                }
089                final String serverTimeZoneId = builder.getServerTimeZoneId(fileSystemOptions);
090                if (serverTimeZoneId != null) {
091                    config.setServerTimeZoneId(serverTimeZoneId);
092                }
093                final String[] shortMonthNames = builder.getShortMonthNames(fileSystemOptions);
094                if (shortMonthNames != null) {
095                    final StringBuilder shortMonthNamesStr = new StringBuilder(BUFSZ);
096                    for (final String shortMonthName : shortMonthNames) {
097                        if (!StringUtils.isEmpty(shortMonthNamesStr)) {
098                            shortMonthNamesStr.append("|");
099                        }
100                        shortMonthNamesStr.append(shortMonthName);
101                    }
102                    config.setShortMonthNames(shortMonthNamesStr.toString());
103                }
104
105                client.configure(config);
106            }
107        }
108
109        /**
110         * Creates a new client.
111         *
112         * @param fileSystemOptions the file system options.
113         * @return a new client.
114         * @throws FileSystemException if a file system error occurs.
115         */
116        protected abstract C createClient(FileSystemOptions fileSystemOptions) throws FileSystemException;
117
118        /**
119         * Creates a connection.
120         *
121         * @param hostname The host name or IP address.
122         * @param port The host port.
123         * @param username The user name.
124         * @param password The user password.
125         * @param workingDirectory The working directory.
126         * @param fileSystemOptions Options to create the connection.
127         * @return A new connection.
128         * @throws FileSystemException if an error occurs while connecting.
129         */
130        public C createConnection(final String hostname, final int port, char[] username, char[] password,
131                final String workingDirectory, final FileSystemOptions fileSystemOptions) throws FileSystemException {
132            // Determine the username and password to use
133            if (username == null) {
134                username = ANON_CHAR_ARRAY;
135            }
136
137            if (password == null) {
138                password = ANON_CHAR_ARRAY;
139            }
140
141            try {
142                final C client = createClient(fileSystemOptions);
143
144                if (log.isDebugEnabled()) {
145                    final Writer writer = new StringWriter(1024) {
146                        @Override
147                        public void flush() {
148                            final StringBuffer buffer = getBuffer();
149                            String message = buffer.toString();
150                            final String prefix = "PASS ";
151                            if (message.toUpperCase().startsWith(prefix) && message.length() > prefix.length()) {
152                                message = prefix + "***";
153                            }
154                            log.debug(message);
155                            buffer.setLength(0);
156                        }
157                    };
158                    client.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(writer)));
159                }
160
161                configureClient(fileSystemOptions, client);
162
163                final FTPFileEntryParserFactory myFactory = builder.getEntryParserFactory(fileSystemOptions);
164                if (myFactory != null) {
165                    client.setParserFactory(myFactory);
166                }
167
168                final Boolean remoteVerification = builder.getRemoteVerification(fileSystemOptions);
169                if (remoteVerification != null) {
170                    client.setRemoteVerificationEnabled(remoteVerification.booleanValue());
171                }
172
173                try {
174                    final Duration connectTimeout = builder.getConnectTimeoutDuration(fileSystemOptions);
175                    if (connectTimeout != null) {
176                        client.setDefaultTimeout(DurationUtils.toMillisInt(connectTimeout));
177                    }
178
179                    final String controlEncoding = builder.getControlEncoding(fileSystemOptions);
180                    if (controlEncoding != null) {
181                        client.setControlEncoding(controlEncoding);
182                    }
183
184                    final Boolean autodetectUTF8 = builder.getAutodetectUtf8(fileSystemOptions);
185                    if (autodetectUTF8 != null) {
186                        client.setAutodetectUTF8(autodetectUTF8);
187                    }
188
189                    final Proxy proxy = builder.getProxy(fileSystemOptions);
190                    if (proxy != null) {
191                        client.setProxy(proxy);
192                    }
193
194                    client.connect(hostname, port);
195
196                    final int reply = client.getReplyCode();
197                    if (!FTPReply.isPositiveCompletion(reply)) {
198                        throw new FileSystemException("vfs.provider.ftp/connect-rejected.error", hostname);
199                    }
200
201                    // Login
202                    if (!client.login(UserAuthenticatorUtils.toString(username),
203                        UserAuthenticatorUtils.toString(password))) {
204                        throw new FileSystemException("vfs.provider.ftp/login.error", hostname,
205                            UserAuthenticatorUtils.toString(username));
206                    }
207
208                    FtpFileType fileType = builder.getFileType(fileSystemOptions);
209                    if (fileType == null) {
210                        fileType = FtpFileType.BINARY;
211                    }
212                    // Set binary mode
213                    if (!client.setFileType(fileType.getValue())) {
214                        throw new FileSystemException("vfs.provider.ftp/set-file-type.error", fileType);
215                    }
216
217                    // Set dataTimeout value
218                    final Duration dataTimeout = builder.getDataTimeoutDuration(fileSystemOptions);
219                    if (dataTimeout != null) {
220                        client.setDataTimeout(dataTimeout);
221                    }
222
223                    final Duration socketTimeout = builder.getSoTimeoutDuration(fileSystemOptions);
224                    if (socketTimeout != null) {
225                        client.setSoTimeout(DurationUtils.toMillisInt(socketTimeout));
226                    }
227
228                    final Duration controlKeepAliveTimeout = builder.getControlKeepAliveTimeout(fileSystemOptions);
229                    if (controlKeepAliveTimeout != null) {
230                        client.setControlKeepAliveTimeout(controlKeepAliveTimeout);
231                    }
232
233                    final Duration controlKeepAliveReplyTimeout = builder
234                        .getControlKeepAliveReplyTimeout(fileSystemOptions);
235                    if (controlKeepAliveReplyTimeout != null) {
236                        client.setControlKeepAliveReplyTimeout(controlKeepAliveReplyTimeout);
237                    }
238
239                    final Boolean userDirIsRoot = builder.getUserDirIsRoot(fileSystemOptions);
240                    if (workingDirectory != null && (userDirIsRoot == null || !userDirIsRoot.booleanValue())
241                        && !client.changeWorkingDirectory(workingDirectory)) {
242                        throw new FileSystemException("vfs.provider.ftp/change-work-directory.error", workingDirectory);
243                    }
244
245                    final Boolean passiveMode = builder.getPassiveMode(fileSystemOptions);
246                    if (passiveMode != null && passiveMode.booleanValue()) {
247                        client.enterLocalPassiveMode();
248                    }
249
250                    final Range<Integer> activePortRange = builder.getActivePortRange(fileSystemOptions);
251                    if (activePortRange != null) {
252                        client.setActivePortRange(activePortRange.getMinimum(), activePortRange.getMaximum());
253                    }
254
255                    setupOpenConnection(client, fileSystemOptions);
256                } catch (final IOException e) {
257                    if (client.isConnected()) {
258                        client.disconnect();
259                    }
260                    throw e;
261                }
262
263                return client;
264            } catch (final Exception exc) {
265                throw new FileSystemException("vfs.provider.ftp/connect.error", exc, hostname);
266            }
267        }
268
269        /**
270         * Sets up a new client.
271         *
272         * @param client the client.
273         * @param fileSystemOptions the file system options.
274         * @throws IOException if an IO error occurs.
275         */
276        protected abstract void setupOpenConnection(C client, FileSystemOptions fileSystemOptions) throws IOException;
277    }
278
279    /**
280     * Connection Factory, used to configure the FTPClient.
281     */
282    public static final class FtpConnectionFactory extends ConnectionFactory<FTPClient, FtpFileSystemConfigBuilder> {
283        private FtpConnectionFactory(final FtpFileSystemConfigBuilder builder) {
284            super(builder);
285        }
286
287        @Override
288        protected FTPClient createClient(final FileSystemOptions fileSystemOptions) {
289            return new FTPClient();
290        }
291
292        @Override
293        protected void setupOpenConnection(final FTPClient client, final FileSystemOptions fileSystemOptions) {
294            // nothing to do for FTP
295        }
296    }
297
298    /**
299     * Creates a new connection to the server.
300     *
301     * @param hostname The host name of the server.
302     * @param port The port to connect to.
303     * @param username The name of the user for authentication.
304     * @param password The user's password.
305     * @param workingDirectory The base directory.
306     * @param fileSystemOptions The FileSystemOptions.
307     * @return An FTPClient.
308     * @throws FileSystemException if an error occurs while connecting.
309     */
310    public static FTPClient createConnection(final String hostname, final int port, final char[] username,
311            final char[] password, final String workingDirectory, final FileSystemOptions fileSystemOptions)
312            throws FileSystemException {
313        final FtpConnectionFactory factory = new FtpConnectionFactory(FtpFileSystemConfigBuilder.getInstance());
314        return factory.createConnection(hostname, port, username, password, workingDirectory, fileSystemOptions);
315    }
316
317    private FtpClientFactory() {
318    }
319}