Add session to cache with both hostname and address.
[cyberduck.git] / source / ch / cyberduck / core / ftp / FTPClient.java
blob9de745952ececa08d798e0237acce04cc5c610d9
1 package ch.cyberduck.core.ftp;
3 /*
4 * Copyright (c) 2002-2010 David Kocher. All rights reserved.
6 * http://cyberduck.ch/
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * Bug fixes, suggestions and comments should be sent to:
19 * dkocher@cyberduck.ch
22 import ch.cyberduck.core.Protocol;
23 import ch.cyberduck.core.preferences.Preferences;
24 import ch.cyberduck.core.preferences.PreferencesFactory;
26 import org.apache.commons.lang3.StringUtils;
27 import org.apache.commons.net.ftp.FTPCmd;
28 import org.apache.commons.net.ftp.FTPReply;
29 import org.apache.commons.net.ftp.FTPSClient;
30 import org.apache.log4j.Logger;
32 import javax.net.ssl.SSLContext;
33 import javax.net.ssl.SSLSession;
34 import javax.net.ssl.SSLSessionContext;
35 import javax.net.ssl.SSLSocket;
36 import javax.net.ssl.SSLSocketFactory;
37 import java.io.BufferedReader;
38 import java.io.BufferedWriter;
39 import java.io.IOException;
40 import java.io.InputStream;
41 import java.io.InputStreamReader;
42 import java.io.OutputStream;
43 import java.io.OutputStreamWriter;
44 import java.lang.reflect.Field;
45 import java.lang.reflect.Method;
46 import java.net.Socket;
47 import java.util.ArrayList;
48 import java.util.HashMap;
49 import java.util.HashSet;
50 import java.util.List;
51 import java.util.Locale;
52 import java.util.Map;
53 import java.util.Set;
55 /**
56 * @version $Id$
58 public class FTPClient extends FTPSClient {
59 private static final Logger log = Logger.getLogger(FTPClient.class);
61 private SSLSocketFactory sslSocketFactory;
63 private Protocol protocol;
65 /**
66 * Map of FEAT responses. If null, has not been initialised.
68 private Map<String, Set<String>> features;
70 private Preferences preferences
71 = PreferencesFactory.get();
73 public FTPClient(final Protocol protocol, final SSLSocketFactory f, final SSLContext c) {
74 super(false, c);
75 this.protocol = protocol;
76 this.sslSocketFactory = f;
79 public void setProtocol(final Protocol protocol) {
80 this.protocol = protocol;
83 @Override
84 protected Socket _openDataConnection_(final String command, final String arg) throws IOException {
85 final Socket socket = super._openDataConnection_(command, arg);
86 if(null == socket) {
87 throw new FTPException(this.getReplyCode(), this.getReplyString());
89 return socket;
92 @Override
93 protected void _prepareDataSocket_(final Socket socket) throws IOException {
94 if(preferences.getBoolean("ftp.tls.session.requirereuse")) {
95 if(socket instanceof SSLSocket) {
96 // Control socket is SSL
97 final SSLSession session = ((SSLSocket) _socket_).getSession();
98 final SSLSessionContext context = session.getSessionContext();
99 context.setSessionCacheSize(preferences.getInteger("ftp.ssl.session.cache.size"));
100 try {
101 final Field sessionHostPortCache = context.getClass().getDeclaredField("sessionHostPortCache");
102 sessionHostPortCache.setAccessible(true);
103 final Object cache = sessionHostPortCache.get(context);
104 final Method method = cache.getClass().getDeclaredMethod("put", Object.class, Object.class);
105 method.setAccessible(true);
106 method.invoke(cache, String.format("%s:%s", socket.getInetAddress().getHostName(),
107 String.valueOf(socket.getPort())).toLowerCase(Locale.ROOT), session);
108 method.invoke(cache, String.format("%s:%s", socket.getInetAddress().getHostAddress(),
109 String.valueOf(socket.getPort())).toLowerCase(Locale.ROOT), session);
111 catch(NoSuchFieldException e) {
112 // Not running in expected JRE
113 log.warn("No field sessionHostPortCache in SSLSessionContext", e);
115 catch(Exception e) {
116 // Not running in expected JRE
117 log.warn(e.getMessage());
123 @Override
124 protected void execAUTH() throws IOException {
125 if(protocol.isSecure()) {
126 if(FTPReply.SECURITY_DATA_EXCHANGE_COMPLETE != this.sendCommand("AUTH", this.getAuthValue())) {
127 throw new FTPException(this.getReplyCode(), this.getReplyString());
132 @Override
133 public void execPROT(final String prot) throws IOException {
134 if(protocol.isSecure()) {
135 if(FTPReply.COMMAND_OK != this.sendCommand("PROT", prot)) {
136 throw new FTPException(this.getReplyCode(), this.getReplyString());
138 if("P".equals(prot)) {
139 // Private
140 this.setSocketFactory(sslSocketFactory);
145 @Override
146 public void execPBSZ(final long pbsz) throws IOException {
147 if(protocol.isSecure()) {
148 if(FTPReply.COMMAND_OK != this.sendCommand("PBSZ", String.valueOf(pbsz))) {
149 throw new FTPException(this.getReplyCode(), this.getReplyString());
154 @Override
155 protected void sslNegotiation() throws IOException {
156 if(protocol.isSecure()) {
157 final SSLSocket socket = (SSLSocket) sslSocketFactory.createSocket(_socket_,
158 _socket_.getInetAddress().getHostAddress(), _socket_.getPort(), false);
159 socket.setEnableSessionCreation(true);
160 socket.setUseClientMode(true);
161 socket.startHandshake();
162 _socket_ = socket;
163 _controlInput_ = new BufferedReader(new InputStreamReader(
164 socket.getInputStream(), getControlEncoding()));
165 _controlOutput_ = new BufferedWriter(new OutputStreamWriter(
166 socket.getOutputStream(), getControlEncoding()));
170 public List<String> list(final FTPCmd command) throws IOException {
171 return this.list(command, null);
174 public List<String> list(final FTPCmd command, final String pathname) throws IOException {
175 this.pret(command, pathname);
177 Socket socket = _openDataConnection_(command, pathname);
179 BufferedReader reader = new BufferedReader(
180 new InputStreamReader(socket.getInputStream(), getControlEncoding()));
181 ArrayList<String> results = new ArrayList<String>();
182 String line;
183 while((line = reader.readLine()) != null) {
184 _commandSupport_.fireReplyReceived(-1, line);
185 results.add(line);
188 reader.close();
189 socket.close();
191 if(!this.completePendingCommand()) {
192 throw new FTPException(this.getReplyCode(), this.getReplyString());
194 return results;
198 * Query the server for a supported feature, and returns its values (if any).
199 * Caches the parsed response to avoid resending the command repeatedly.
201 * @return if the feature is present, returns the feature values (empty array if none)
202 * Returns {@code null} if the feature is not found or the command failed.
203 * Check {@link #getReplyCode()} or {@link #getReplyString()} if so.
204 * @throws IOException
205 * @since 3.0
207 public String[] featureValues(String feature) throws IOException {
208 if(!initFeatureMap()) {
209 return null;
211 Set<String> entries = features.get(feature.toUpperCase(Locale.ROOT));
212 if(entries != null) {
213 return entries.toArray(new String[entries.size()]);
215 return null;
219 * Query the server for a supported feature, and returns the its value (if any).
220 * Caches the parsed response to avoid resending the command repeatedly.
222 * @return if the feature is present, returns the feature value or the empty string
223 * if the feature exists but has no value.
224 * Returns {@code null} if the feature is not found or the command failed.
225 * Check {@link #getReplyCode()} or {@link #getReplyString()} if so.
226 * @throws IOException
227 * @since 3.0
229 public String featureValue(String feature) throws IOException {
230 String[] values = featureValues(feature);
231 if(values != null) {
232 return values[0];
234 return null;
238 * Query the server for a supported feature.
239 * Caches the parsed response to avoid resending the command repeatedly.
241 * @param feature the name of the feature; it is converted to upper case.
242 * @return {@code true} if the feature is present, {@code false} if the feature is not present
243 * or the {@link #feat()} command failed. Check {@link #getReplyCode()} or {@link #getReplyString()}
244 * if it is necessary to distinguish these cases.
245 * @throws IOException
246 * @since 3.0
248 public boolean hasFeature(String feature) throws IOException {
249 if(!initFeatureMap()) {
250 return false;
252 return features.containsKey(feature.toUpperCase(Locale.ROOT));
256 * Query the server for a supported feature with particular value,
257 * for example "AUTH SSL" or "AUTH TLS".
258 * Caches the parsed response to avoid resending the command repeatedly.
260 * @param feature the name of the feature; it is converted to upper case.
261 * @param value the value to find.
262 * @return {@code true} if the feature is present, {@code false} if the feature is not present
263 * or the {@link #feat()} command failed. Check {@link #getReplyCode()} or {@link #getReplyString()}
264 * if it is necessary to distinguish these cases.
265 * @throws IOException
266 * @since 3.0
268 public boolean hasFeature(String feature, String value) throws IOException {
269 if(!initFeatureMap()) {
270 return false;
272 Set<String> entries = features.get(feature.toUpperCase(Locale.ROOT));
273 if(entries != null) {
274 return entries.contains(value);
276 return false;
280 * Create the feature map if not already created.
282 private boolean initFeatureMap() throws IOException {
283 if(features == null) {
284 // Don't create map here, because next line may throw exception
285 final int reply = feat();
286 if(FTPReply.NOT_LOGGED_IN == reply) {
287 return false;
289 else {
290 // we init the map here, so we don't keep trying if we know the command will fail
291 features = new HashMap<String, Set<String>>();
293 boolean success = FTPReply.isPositiveCompletion(reply);
294 if(!success) {
295 return false;
297 for(String l : getReplyStrings()) {
298 if(l.startsWith(" ")) { // it's a FEAT entry
299 String key;
300 String value = "";
301 int varsep = l.indexOf(' ', 1);
302 if(varsep > 0) {
303 key = l.substring(1, varsep);
304 value = l.substring(varsep + 1);
306 else {
307 key = l.substring(1);
309 key = key.toUpperCase(Locale.ROOT);
310 Set<String> entries = features.get(key);
311 if(entries == null) {
312 entries = new HashSet<String>();
313 features.put(key, entries);
315 entries.add(value);
319 return true;
322 @Override
323 public boolean retrieveFile(String remote, OutputStream local) throws IOException {
324 this.pret(FTPCmd.RETR, remote);
325 return super.retrieveFile(remote, local);
328 @Override
329 public InputStream retrieveFileStream(String remote) throws IOException {
330 this.pret(FTPCmd.RETR, remote);
331 return super.retrieveFileStream(remote);
334 @Override
335 public boolean storeFile(String remote, InputStream local) throws IOException {
336 this.pret(FTPCmd.STOR, remote);
337 return super.storeFile(remote, local);
340 @Override
341 public OutputStream storeFileStream(String remote) throws IOException {
342 this.pret(FTPCmd.STOR, remote);
343 return super.storeFileStream(remote);
346 @Override
347 public boolean appendFile(String remote, InputStream local) throws IOException {
348 this.pret(FTPCmd.APPE, remote);
349 return super.appendFile(remote, local);
352 @Override
353 public OutputStream appendFileStream(String remote) throws IOException {
354 this.pret(FTPCmd.APPE, remote);
355 return super.appendFileStream(remote);
359 * http://drftpd.org/index.php/PRET_Specifications
361 * @param command Command to execute
362 * @param file Remote file
363 * @throws IOException I/O failure
365 protected void pret(final FTPCmd command, final String file) throws IOException {
366 if(this.hasFeature("PRET")) {
367 if(!FTPReply.isPositiveCompletion(this.sendCommand("PRET", String.format("%s %s", command.getCommand(), file)))) {
368 throw new FTPException(this.getReplyCode(), this.getReplyString());
373 @Override
374 public String getModificationTime(final String file) throws IOException {
375 final String status = super.getModificationTime(file);
376 if(null == status) {
377 throw new FTPException(this.getReplyCode(), this.getReplyString());
379 return StringUtils.chomp(status.substring(3).trim());