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     */
017    package org.apache.camel.component.file.remote;
018    
019    import java.io.ByteArrayOutputStream;
020    import java.io.File;
021    import java.io.FileOutputStream;
022    import java.io.IOException;
023    import java.io.InputStream;
024    import java.io.OutputStream;
025    import java.util.ArrayList;
026    import java.util.List;
027    import java.util.Vector;
028    
029    import com.jcraft.jsch.ChannelSftp;
030    import com.jcraft.jsch.JSch;
031    import com.jcraft.jsch.JSchException;
032    import com.jcraft.jsch.Session;
033    import com.jcraft.jsch.SftpException;
034    import com.jcraft.jsch.UserInfo;
035    import org.apache.camel.Exchange;
036    import org.apache.camel.InvalidPayloadException;
037    import org.apache.camel.component.file.FileComponent;
038    import org.apache.camel.component.file.GenericFile;
039    import org.apache.camel.component.file.GenericFileEndpoint;
040    import org.apache.camel.component.file.GenericFileExist;
041    import org.apache.camel.component.file.GenericFileOperationFailedException;
042    import org.apache.camel.util.ExchangeHelper;
043    import org.apache.camel.util.FileUtil;
044    import org.apache.camel.util.IOHelper;
045    import org.apache.camel.util.ObjectHelper;
046    import org.apache.commons.logging.Log;
047    import org.apache.commons.logging.LogFactory;
048    
049    import static org.apache.camel.util.ObjectHelper.isNotEmpty;
050    
051    /**
052     * SFTP remote file operations
053     */
054    public class SftpOperations implements RemoteFileOperations<ChannelSftp.LsEntry> {
055        private static final transient Log LOG = LogFactory.getLog(SftpOperations.class);
056        private RemoteFileEndpoint endpoint;
057        private ChannelSftp channel;
058        private Session session;
059    
060        public void setEndpoint(GenericFileEndpoint endpoint) {
061            this.endpoint = (RemoteFileEndpoint) endpoint;
062        }
063    
064        public boolean connect(RemoteFileConfiguration configuration) throws GenericFileOperationFailedException {
065            if (isConnected()) {
066                // already connected
067                return true;
068            }
069    
070            boolean connected = false;
071            int attempt = 0;
072    
073            while (!connected) {
074                try {
075                    if (LOG.isTraceEnabled() && attempt > 0) {
076                        LOG.trace("Reconnect attempt #" + attempt + " connecting to + " + configuration.remoteServerInformation());
077                    }
078    
079                    if (channel == null || !channel.isConnected()) {
080                        if (session == null || !session.isConnected()) {
081                            LOG.trace("Session isn't connected, trying to recreate and connect.");
082                            session = createSession(configuration);
083                            if (endpoint.getConfiguration().getConnectTimeout() > 0) {
084                                LOG.trace("Connecting use connectTimeout: " + endpoint.getConfiguration().getConnectTimeout() + " ...");
085                                session.connect(endpoint.getConfiguration().getConnectTimeout());
086                            } else {
087                                LOG.trace("Connecting ...");
088                                session.connect();
089                            }
090                        }
091    
092                        LOG.trace("Channel isn't connected, trying to recreate and connect.");
093                        channel = (ChannelSftp) session.openChannel("sftp");
094    
095                        if (endpoint.getConfiguration().getConnectTimeout() > 0) {
096                            LOG.trace("Connecting use connectTimeout: " + endpoint.getConfiguration().getConnectTimeout() + " ...");
097                            channel.connect(endpoint.getConfiguration().getConnectTimeout());
098                        } else {
099                            LOG.trace("Connecting ...");
100                            channel.connect();
101                        }
102                        LOG.info("Connected to " + configuration.remoteServerInformation());
103                    }
104    
105                    // yes we could connect
106                    connected = true;
107                } catch (Exception e) {
108                    GenericFileOperationFailedException failed = new GenericFileOperationFailedException("Cannot connect to " + configuration.remoteServerInformation(), e);
109                    if (LOG.isTraceEnabled()) {
110                        LOG.trace("Cannot connect due: " + failed.getMessage());
111                    }
112                    attempt++;
113                    if (attempt > endpoint.getMaximumReconnectAttempts()) {
114                        throw failed;
115                    }
116                    if (endpoint.getReconnectDelay() > 0) {
117                        try {
118                            Thread.sleep(endpoint.getReconnectDelay());
119                        } catch (InterruptedException e1) {
120                            // ignore
121                        }
122                    }
123                }
124            }
125    
126            return true;
127        }
128    
129        protected Session createSession(final RemoteFileConfiguration configuration) throws JSchException {
130            final JSch jsch = new JSch();
131            JSch.setLogger(new JSchLogger());
132    
133            SftpConfiguration sftpConfig = (SftpConfiguration) configuration;
134    
135            if (isNotEmpty(sftpConfig.getPrivateKeyFile())) {
136                LOG.debug("Using private keyfile: " + sftpConfig.getPrivateKeyFile());
137                if (isNotEmpty(sftpConfig.getPrivateKeyFilePassphrase())) {
138                    jsch.addIdentity(sftpConfig.getPrivateKeyFile(), sftpConfig.getPrivateKeyFilePassphrase());
139                } else {
140                    jsch.addIdentity(sftpConfig.getPrivateKeyFile());
141                }
142            }
143    
144            if (isNotEmpty(sftpConfig.getKnownHostsFile())) {
145                LOG.debug("Using knownhosts file: " + sftpConfig.getKnownHostsFile());
146                jsch.setKnownHosts(sftpConfig.getKnownHostsFile());
147            }
148    
149            final Session session = jsch.getSession(configuration.getUsername(), configuration.getHost(), configuration.getPort());
150    
151            if (isNotEmpty(sftpConfig.getStrictHostKeyChecking())) {
152                LOG.debug("Using StrickHostKeyChecking: " + sftpConfig.getStrictHostKeyChecking());
153                session.setConfig("StrictHostKeyChecking", sftpConfig.getStrictHostKeyChecking());
154            }
155    
156            // set user information
157            session.setUserInfo(new UserInfo() {
158                public String getPassphrase() {
159                    return null;
160                }
161    
162                public String getPassword() {
163                    return configuration.getPassword();
164                }
165    
166                public boolean promptPassword(String s) {
167                    return true;
168                }
169    
170                public boolean promptPassphrase(String s) {
171                    return true;
172                }
173    
174                public boolean promptYesNo(String s) {
175                    LOG.warn("Server asks for confirmation (yes|no): " + s + ". Camel will answer no.");
176                    // Return 'false' indicating modification of the hosts file is disabled.
177                    return false;
178                }
179    
180                public void showMessage(String s) {
181                    LOG.trace("Message received from Server: " + s);
182                }
183            });
184            return session;
185        }
186    
187        private static final class JSchLogger implements com.jcraft.jsch.Logger {
188    
189            public boolean isEnabled(int level) {
190                switch (level) {
191                case FATAL:
192                    return LOG.isFatalEnabled();
193                case ERROR:
194                    return LOG.isErrorEnabled();
195                case WARN:
196                    return LOG.isWarnEnabled();
197                case INFO:
198                    return LOG.isInfoEnabled();
199                default:
200                    return LOG.isDebugEnabled();
201                }
202            }
203    
204            public void log(int level, String message) {
205                switch (level) {
206                case FATAL:
207                    LOG.fatal("JSCH -> " + message);
208                    break;
209                case ERROR:
210                    LOG.error("JSCH -> " + message);
211                    break;
212                case WARN:
213                    LOG.warn("JSCH -> " + message);
214                    break;
215                case INFO:
216                    LOG.info("JSCH -> " + message);
217                    break;
218                default:
219                    LOG.debug("JSCH -> " + message);
220                    break;
221                }
222            }
223        }
224    
225        public boolean isConnected() throws GenericFileOperationFailedException {
226            return session != null && session.isConnected() && channel != null && channel.isConnected();
227        }
228    
229        public void disconnect() throws GenericFileOperationFailedException {
230            if (session != null && session.isConnected()) {
231                session.disconnect();
232            }
233            if (channel != null && channel.isConnected()) {
234                channel.disconnect();
235            }
236        }
237    
238        public boolean deleteFile(String name) throws GenericFileOperationFailedException {
239            if (LOG.isDebugEnabled()) {
240                LOG.debug("Deleting file: " + name);
241            }
242            try {
243                channel.rm(name);
244                return true;
245            } catch (SftpException e) {
246                throw new GenericFileOperationFailedException("Cannot delete file: " + name, e);
247            }
248        }
249    
250        public boolean renameFile(String from, String to) throws GenericFileOperationFailedException {
251            if (LOG.isDebugEnabled()) {
252                LOG.debug("Renaming file: " + from + " to: " + to);
253            }
254            try {
255                channel.rename(from, to);
256                return true;
257            } catch (SftpException e) {
258                throw new GenericFileOperationFailedException("Cannot rename file from: " + from + " to: " + to, e);
259            }
260        }
261    
262        public boolean buildDirectory(String directory, boolean absolute) throws GenericFileOperationFailedException {
263            // ignore absolute as all dirs are relative with FTP
264            boolean success = false;
265    
266            String originalDirectory = getCurrentDirectory();
267            try {
268                // maybe the full directory already exists
269                try {
270                    channel.cd(directory);
271                    success = true;
272                } catch (SftpException e) {
273                    // ignore, we could not change directory so try to create it instead
274                }
275    
276                if (!success) {
277                    if (LOG.isDebugEnabled()) {
278                        LOG.debug("Trying to build remote directory: " + directory);
279                    }
280    
281                    try {
282                        channel.mkdir(directory);
283                        success = true;
284                    } catch (SftpException e) {
285                        // we are here if the server side doesn't create intermediate folders
286                        // so create the folder one by one
287                        success = buildDirectoryChunks(directory);
288                    }
289                }
290            } catch (IOException e) {
291                throw new GenericFileOperationFailedException("Cannot build directory: " + directory, e);
292            } catch (SftpException e) {
293                throw new GenericFileOperationFailedException("Cannot build directory: " + directory, e);
294            } finally {
295                // change back to original directory
296                if (originalDirectory != null) {
297                    changeCurrentDirectory(originalDirectory);
298                }
299            }
300    
301            return success;
302        }
303    
304        private boolean buildDirectoryChunks(String dirName) throws IOException, SftpException {
305            final StringBuilder sb = new StringBuilder(dirName.length());
306            final String[] dirs = dirName.split("/|\\\\");
307    
308            boolean success = false;
309            for (String dir : dirs) {
310                sb.append(dir).append('/');
311                String directory = sb.toString();
312                if (LOG.isTraceEnabled()) {
313                    LOG.trace("Trying to build remote directory by chunk: " + directory);
314                }
315    
316                // do not try to build root / folder
317                if (!directory.equals("/")) {
318                    try {
319                        channel.mkdir(directory);
320                        success = true;
321                    } catch (SftpException e) {
322                        // ignore keep trying to create the rest of the path
323                    }
324                }
325            }
326    
327            return success;
328        }
329    
330        public String getCurrentDirectory() throws GenericFileOperationFailedException {
331            try {
332                return channel.pwd();
333            } catch (SftpException e) {
334                throw new GenericFileOperationFailedException("Cannot get current directory", e);
335            }
336        }
337    
338        public void changeCurrentDirectory(String path) throws GenericFileOperationFailedException {
339            try {
340                channel.cd(path);
341            } catch (SftpException e) {
342                throw new GenericFileOperationFailedException("Cannot change current directory to: " + path, e);
343            }
344        }
345    
346        public List<ChannelSftp.LsEntry> listFiles() throws GenericFileOperationFailedException {
347            return listFiles(".");
348        }
349    
350        public List<ChannelSftp.LsEntry> listFiles(String path) throws GenericFileOperationFailedException {
351            if (ObjectHelper.isEmpty(path)) {
352                // list current directory if file path is not given
353                path = ".";
354            }
355    
356            try {
357                final List<ChannelSftp.LsEntry> list = new ArrayList<ChannelSftp.LsEntry>();
358                Vector files = channel.ls(path);
359                // can return either null or an empty list depending on FTP servers
360                if (files != null) {
361                    for (Object file : files) {
362                        list.add((ChannelSftp.LsEntry) file);
363                    }
364                }
365                return list;
366            } catch (SftpException e) {
367                throw new GenericFileOperationFailedException("Cannot list directory: " + path, e);
368            }
369        }
370    
371        public boolean retrieveFile(String name, Exchange exchange) throws GenericFileOperationFailedException {
372            if (ObjectHelper.isNotEmpty(endpoint.getLocalWorkDirectory())) {
373                // local work directory is configured so we should store file content as files in this local directory
374                return retrieveFileToFileInLocalWorkDirectory(name, exchange);
375            } else {
376                // store file content directory as stream on the body
377                return retrieveFileToStreamInBody(name, exchange);
378            }
379        }
380    
381        @SuppressWarnings("unchecked")
382        private boolean retrieveFileToStreamInBody(String name, Exchange exchange) throws GenericFileOperationFailedException {
383            OutputStream os = null;
384            try {
385                os = new ByteArrayOutputStream();
386                GenericFile<ChannelSftp.LsEntry> target =
387                        (GenericFile<ChannelSftp.LsEntry>) exchange.getProperty(FileComponent.FILE_EXCHANGE_FILE);
388                ObjectHelper.notNull(target, "Exchange should have the " + FileComponent.FILE_EXCHANGE_FILE + " set");
389                target.setBody(os);
390                channel.get(name, os);
391                return true;
392            } catch (SftpException e) {
393                throw new GenericFileOperationFailedException("Cannot retrieve file: " + name, e);
394            } finally {
395                IOHelper.close(os, "retrieve: " + name, LOG);
396            }
397        }
398    
399        @SuppressWarnings("unchecked")
400        private boolean retrieveFileToFileInLocalWorkDirectory(String name, Exchange exchange) throws GenericFileOperationFailedException {
401            File temp;
402            File local = new File(endpoint.getLocalWorkDirectory());
403            OutputStream os;
404            GenericFile<ChannelSftp.LsEntry> file =
405                    (GenericFile<ChannelSftp.LsEntry>) exchange.getProperty(FileComponent.FILE_EXCHANGE_FILE);
406            ObjectHelper.notNull(file, "Exchange should have the " + FileComponent.FILE_EXCHANGE_FILE + " set");
407            try {
408                // use relative filename in local work directory
409                String relativeName = file.getRelativeFilePath();
410    
411                temp = new File(local, relativeName + ".inprogress");
412                local = new File(local, relativeName);
413    
414                // create directory to local work file
415                local.mkdirs();
416    
417                // delete any existing files
418                if (temp.exists()) {
419                    if (!FileUtil.deleteFile(temp)) {
420                        throw new GenericFileOperationFailedException("Cannot delete existing local work file: " + temp);
421                    }
422                }
423                if (local.exists()) {
424                    if (!FileUtil.deleteFile(local)) {
425                        throw new GenericFileOperationFailedException("Cannot delete existing local work file: " + local);
426                    }
427                }
428    
429                // create new temp local work file
430                if (!temp.createNewFile()) {
431                    throw new GenericFileOperationFailedException("Cannot create new local work file: " + temp);
432                }
433    
434                // store content as a file in the local work directory in the temp handle
435                os = new FileOutputStream(temp);
436    
437                // set header with the path to the local work file
438                exchange.getIn().setHeader(Exchange.FILE_LOCAL_WORK_PATH, local.getPath());
439            } catch (Exception e) {
440                throw new GenericFileOperationFailedException("Cannot create new local work file: " + local);
441            }
442    
443    
444            try {
445                // store the java.io.File handle as the body
446                file.setBody(local);
447                channel.get(name, os);
448            } catch (SftpException e) {
449                throw new GenericFileOperationFailedException("Cannot retrieve file: " + name, e);
450            } finally {
451                IOHelper.close(os, "retrieve: " + name, LOG);
452            }
453    
454            if (LOG.isDebugEnabled()) {
455                LOG.debug("Retrieve file to local work file result: true");
456            }
457    
458            // operation went okay so rename temp to local after we have retrieved the data
459            if (LOG.isTraceEnabled()) {
460                LOG.trace("Renaming local in progress file from: " + temp + " to: " + local);
461            }
462            if (!FileUtil.renameFile(temp, local)) {
463                throw new GenericFileOperationFailedException("Cannot rename local work file from: " + temp + " to: " + local);
464            }
465    
466            return true;
467        }
468    
469        public boolean storeFile(String name, Exchange exchange) throws GenericFileOperationFailedException {
470            // if an existing file already exists what should we do?
471            if (endpoint.getFileExist() == GenericFileExist.Ignore || endpoint.getFileExist() == GenericFileExist.Fail) {
472                boolean existFile = existsFile(name);
473                if (existFile && endpoint.getFileExist() == GenericFileExist.Ignore) {
474                    // ignore but indicate that the file was written
475                    if (LOG.isTraceEnabled()) {
476                        LOG.trace("An existing file already exists: " + name + ". Ignore and do not override it.");
477                    }
478                    return true;
479                } else if (existFile && endpoint.getFileExist() == GenericFileExist.Fail) {
480                    throw new GenericFileOperationFailedException("File already exist: " + name + ". Cannot write new file.");
481                }
482            }
483    
484            InputStream is = null;
485            try {
486                is = ExchangeHelper.getMandatoryInBody(exchange, InputStream.class);
487                if (endpoint.getFileExist() == GenericFileExist.Append) {
488                    channel.put(is, name, ChannelSftp.APPEND);
489                } else {
490                    // override is default
491                    channel.put(is, name);
492                }
493                return true;
494            } catch (SftpException e) {
495                throw new GenericFileOperationFailedException("Cannot store file: " + name, e);
496            } catch (InvalidPayloadException e) {
497                throw new GenericFileOperationFailedException("Cannot store file: " + name, e);
498            } finally {
499                IOHelper.close(is, "store: " + name, LOG);
500            }
501        }
502    
503        public boolean existsFile(String name) throws GenericFileOperationFailedException {
504            // check whether a file already exists
505            String directory = FileUtil.onlyPath(name);
506            if (directory == null) {
507                return false;
508            }
509    
510            String onlyName = FileUtil.stripPath(name);
511            try {
512                Vector files = channel.ls(directory);
513                // can return either null or an empty list depending on FTP servers
514                if (files == null) {
515                    return false;
516                }
517                for (Object file : files) {
518                    ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) file;
519                    if (entry.getFilename().equals(onlyName)) {
520                        return true;
521                    }
522                }
523                return false;
524            } catch (SftpException e) {
525                // or an exception can be thrown with id 2 which means file does not exists
526                if (ChannelSftp.SSH_FX_NO_SUCH_FILE == e.id) {
527                    return false;
528                }
529                // otherwise its a more serious error so rethrow
530                throw new GenericFileOperationFailedException(e.getMessage(), e);
531            }
532        }
533    
534        public boolean sendNoop() throws GenericFileOperationFailedException {
535            // is not implemented
536            return true;
537        }
538    }