1 package ch
.cyberduck
.core
.ftp
;
4 * Copyright (c) 2002-2010 David Kocher. All rights reserved.
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
;
58 public class FTPClient
extends FTPSClient
{
59 private static final Logger log
= Logger
.getLogger(FTPClient
.class);
61 private SSLSocketFactory sslSocketFactory
;
63 private Protocol protocol
;
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
) {
75 this.protocol
= protocol
;
76 this.sslSocketFactory
= f
;
79 public void setProtocol(final Protocol protocol
) {
80 this.protocol
= protocol
;
84 protected Socket
_openDataConnection_(final String command
, final String arg
) throws IOException
{
85 final Socket socket
= super._openDataConnection_(command
, arg
);
87 throw new FTPException(this.getReplyCode(), this.getReplyString());
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"));
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
);
116 // Not running in expected JRE
117 log
.warn(e
.getMessage());
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());
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
)) {
140 this.setSocketFactory(sslSocketFactory
);
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());
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();
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
>();
183 while((line
= reader
.readLine()) != null) {
184 _commandSupport_
.fireReplyReceived(-1, line
);
191 if(!this.completePendingCommand()) {
192 throw new FTPException(this.getReplyCode(), this.getReplyString());
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
207 public String
[] featureValues(String feature
) throws IOException
{
208 if(!initFeatureMap()) {
211 Set
<String
> entries
= features
.get(feature
.toUpperCase(Locale
.ROOT
));
212 if(entries
!= null) {
213 return entries
.toArray(new String
[entries
.size()]);
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
229 public String
featureValue(String feature
) throws IOException
{
230 String
[] values
= featureValues(feature
);
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
248 public boolean hasFeature(String feature
) throws IOException
{
249 if(!initFeatureMap()) {
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
268 public boolean hasFeature(String feature
, String value
) throws IOException
{
269 if(!initFeatureMap()) {
272 Set
<String
> entries
= features
.get(feature
.toUpperCase(Locale
.ROOT
));
273 if(entries
!= null) {
274 return entries
.contains(value
);
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
) {
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
);
297 for(String l
: getReplyStrings()) {
298 if(l
.startsWith(" ")) { // it's a FEAT entry
301 int varsep
= l
.indexOf(' ', 1);
303 key
= l
.substring(1, varsep
);
304 value
= l
.substring(varsep
+ 1);
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
);
323 public boolean retrieveFile(String remote
, OutputStream local
) throws IOException
{
324 this.pret(FTPCmd
.RETR
, remote
);
325 return super.retrieveFile(remote
, local
);
329 public InputStream
retrieveFileStream(String remote
) throws IOException
{
330 this.pret(FTPCmd
.RETR
, remote
);
331 return super.retrieveFileStream(remote
);
335 public boolean storeFile(String remote
, InputStream local
) throws IOException
{
336 this.pret(FTPCmd
.STOR
, remote
);
337 return super.storeFile(remote
, local
);
341 public OutputStream
storeFileStream(String remote
) throws IOException
{
342 this.pret(FTPCmd
.STOR
, remote
);
343 return super.storeFileStream(remote
);
347 public boolean appendFile(String remote
, InputStream local
) throws IOException
{
348 this.pret(FTPCmd
.APPE
, remote
);
349 return super.appendFile(remote
, local
);
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());
374 public String
getModificationTime(final String file
) throws IOException
{
375 final String status
= super.getModificationTime(file
);
377 throw new FTPException(this.getReplyCode(), this.getReplyString());
379 return StringUtils
.chomp(status
.substring(3).trim());