/************************************************************************** ** ** This file is part of Qt Creator ** ** Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies). ** ** Contact: Nokia Corporation (qt-info@nokia.com) ** ** Commercial Usage ** ** Licensees holding valid Qt Commercial licenses may use this file in ** accordance with the Qt Commercial License Agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and Nokia. ** ** GNU Lesser General Public License Usage ** ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** If you are unsure which license is appropriate for your use, please ** contact the sales department at http://qt.nokia.com/contact. ** **************************************************************************/ #include "sftpchannel.h" #include "sftpchannel_p.h" #include "sshdelayedsignal_p.h" #include "sshexception_p.h" #include "sshsendfacility_p.h" #include #include namespace Core { namespace Internal { namespace { const quint32 ProtocolVersion = 3; QString errorMessage(const QString &serverMessage, const QString &alternativeMessage) { return serverMessage.isEmpty() ? alternativeMessage : serverMessage; } QString errorMessage(const SftpStatusResponse &response, const QString &alternativeMessage) { return response.status == SSH_FX_OK ? QString() : errorMessage(response.errorString, alternativeMessage); } } // anonymous namespace } // namespace Internal SftpChannel::SftpChannel(quint32 channelId, Internal::SshSendFacility &sendFacility) : d(new Internal::SftpChannelPrivate(channelId, sendFacility, this)) { } SftpChannel::State SftpChannel::state() const { switch (d->channelState()) { case Internal::AbstractSshChannel::Inactive: return Uninitialized; case Internal::AbstractSshChannel::SessionRequested: return Initializing; case Internal::AbstractSshChannel::CloseRequested: return Closing; case Internal::AbstractSshChannel::Closed: return Closed; case Internal::AbstractSshChannel::SessionEstablished: return d->m_sftpState == Internal::SftpChannelPrivate::Initialized ? Initialized : Initializing; default: Q_ASSERT(!"Oh no, we forgot to handle a channel state!"); return Closed; // For the compiler. } } void SftpChannel::initialize() { d->requestSessionStart(); d->m_sftpState = Internal::SftpChannelPrivate::SubsystemRequested; } void SftpChannel::closeChannel() { d->closeChannel(); } SftpJobId SftpChannel::listDirectory(const QString &path) { return d->createJob(Internal::SftpListDir::Ptr( new Internal::SftpListDir(++d->m_nextJobId, path))); } SftpJobId SftpChannel::createDirectory(const QString &path) { return d->createJob(Internal::SftpMakeDir::Ptr( new Internal::SftpMakeDir(++d->m_nextJobId, path))); } SftpJobId SftpChannel::removeDirectory(const QString &path) { return d->createJob(Internal::SftpRmDir::Ptr( new Internal::SftpRmDir(++d->m_nextJobId, path))); } SftpJobId SftpChannel::removeFile(const QString &path) { return d->createJob(Internal::SftpRm::Ptr( new Internal::SftpRm(++d->m_nextJobId, path))); } SftpJobId SftpChannel::renameFileOrDirectory(const QString &oldPath, const QString &newPath) { return d->createJob(Internal::SftpRename::Ptr( new Internal::SftpRename(++d->m_nextJobId, oldPath, newPath))); } SftpJobId SftpChannel::createFile(const QString &path, SftpOverwriteMode mode) { return d->createJob(Internal::SftpCreateFile::Ptr( new Internal::SftpCreateFile(++d->m_nextJobId, path, mode))); } SftpJobId SftpChannel::uploadFile(const QString &localFilePath, const QString &remoteFilePath, SftpOverwriteMode mode) { QSharedPointer localFile(new QFile(localFilePath)); if (!localFile->open(QIODevice::ReadOnly)) return SftpInvalidJob; return d->createJob(Internal::SftpUpload::Ptr( new Internal::SftpUpload(++d->m_nextJobId, remoteFilePath, localFile, mode))); } SftpJobId SftpChannel::downloadFile(const QString &remoteFilePath, const QString &localFilePath, SftpOverwriteMode mode) { QSharedPointer localFile(new QFile(localFilePath)); if (mode == SftpSkipExisting && localFile->exists()) return SftpInvalidJob; QIODevice::OpenMode openMode = QIODevice::WriteOnly; if (mode == SftpOverwriteExisting) openMode |= QIODevice::Truncate; else if (mode == SftpAppendToExisting) openMode |= QIODevice::Append; if (!localFile->open(openMode)) return SftpInvalidJob; return d->createJob(Internal::SftpDownload::Ptr( new Internal::SftpDownload(++d->m_nextJobId, remoteFilePath, localFile))); } SftpChannel::~SftpChannel() { delete d; } namespace Internal { SftpChannelPrivate::SftpChannelPrivate(quint32 channelId, SshSendFacility &sendFacility, SftpChannel *sftp) : AbstractSshChannel(channelId, sendFacility), m_nextJobId(0), m_sftpState(Inactive), m_sftp(sftp) { } SftpJobId SftpChannelPrivate::createJob(const AbstractSftpOperation::Ptr &job) { if (m_sftp->state() != SftpChannel::Initialized) return SftpInvalidJob; m_jobs.insert(job->jobId, job); sendData(job->initialPacket(m_outgoingPacket).rawData()); return job->jobId; } void SftpChannelPrivate::handleChannelSuccess() { #ifdef CREATOR_SSH_DEBUG qDebug("sftp subsystem initialized"); #endif sendData(m_outgoingPacket.generateInit(ProtocolVersion).rawData()); m_sftpState = InitSent; } void SftpChannelPrivate::handleChannelFailure() { if (m_sftpState != SubsystemRequested) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Unexpected SSH_MSG_CHANNEL_FAILURE packet."); } createDelayedInitFailedSignal(SSH_TR("Server could not start sftp subsystem.")); closeChannel(); } void SftpChannelPrivate::handleChannelDataInternal(const QByteArray &data) { m_incomingData += data; m_incomingPacket.consumeData(m_incomingData); while (m_incomingPacket.isComplete()) { handleCurrentPacket(); m_incomingPacket.clear(); m_incomingPacket.consumeData(m_incomingData); } } void SftpChannelPrivate::handleChannelExtendedDataInternal(quint32 type, const QByteArray &data) { qWarning("Unexpected extended data '%s' of type %d on SFTP channel.", data.data(), type); } void SftpChannelPrivate::handleCurrentPacket() { #ifdef CREATOR_SSH_DEBUG qDebug("Handling SFTP packet of type %d", m_incomingPacket.type()); #endif switch (m_incomingPacket.type()) { case SSH_FXP_VERSION: handleServerVersion(); break; case SSH_FXP_HANDLE: handleHandle(); break; case SSH_FXP_NAME: handleName(); break; case SSH_FXP_STATUS: handleStatus(); break; case SSH_FXP_DATA: handleReadData(); break; case SSH_FXP_ATTRS: handleAttrs(); break; default: throw SshServerException(SSH_DISCONNECT_PROTOCOL_ERROR, "Unexpected packet.", SSH_TR("Unexpected packet of type %d.").arg(m_incomingPacket.type())); } } void SftpChannelPrivate::handleServerVersion() { checkChannelActive(); if (m_sftpState != InitSent) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Unexpected SSH_FXP_VERSION packet."); } #ifdef CREATOR_SSH_DEBUG qDebug("sftp init received"); #endif const quint32 serverVersion = m_incomingPacket.extractServerVersion(); if (serverVersion != ProtocolVersion) { createDelayedInitFailedSignal(SSH_TR("Protocol version mismatch: Expected %1, got %2") .arg(serverVersion).arg(ProtocolVersion)); closeChannel(); } else { m_sftpState = Initialized; createDelayedInitializedSignal(); } } void SftpChannelPrivate::handleHandle() { const SftpHandleResponse &response = m_incomingPacket.asHandleResponse(); JobMap::Iterator it = lookupJob(response.requestId); const QSharedPointer job = it.value().dynamicCast(); if (job.isNull()) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Unexpected SSH_FXP_HANDLE packet."); } if (job->state != AbstractSftpOperationWithHandle::OpenRequested) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Unexpected SSH_FXP_HANDLE packet."); } job->remoteHandle = response.handle; job->state = AbstractSftpOperationWithHandle::Open; switch (it.value()->type()) { case AbstractSftpOperation::ListDir: handleLsHandle(it); break; case AbstractSftpOperation::CreateFile: handleCreateFileHandle(it); break; case AbstractSftpOperation::Download: handleGetHandle(it); break; case AbstractSftpOperation::Upload: handlePutHandle(it); break; default: Q_ASSERT(!"Oh no, I forgot to handle an SFTP operation type!"); } } void SftpChannelPrivate::handleLsHandle(const JobMap::Iterator &it) { SftpListDir::Ptr op = it.value().staticCast(); sendData(m_outgoingPacket.generateReadDir(op->remoteHandle, op->jobId).rawData()); } void SftpChannelPrivate::handleCreateFileHandle(const JobMap::Iterator &it) { SftpCreateFile::Ptr op = it.value().staticCast(); sendData(m_outgoingPacket.generateCloseHandle(op->remoteHandle, op->jobId).rawData()); } void SftpChannelPrivate::handleGetHandle(const JobMap::Iterator &it) { SftpDownload::Ptr op = it.value().staticCast(); sendData(m_outgoingPacket.generateFstat(op->remoteHandle, op->jobId).rawData()); op->statRequested = true; } void SftpChannelPrivate::handlePutHandle(const JobMap::Iterator &it) { SftpUpload::Ptr op = it.value().staticCast(); // OpenSSH does not implement the RFC's append functionality, so we // have to emulate it. if (op->mode == SftpAppendToExisting) { sendData(m_outgoingPacket.generateFstat(op->remoteHandle, op->jobId).rawData()); op->statRequested = true; } else { spawnWriteRequests(it); } } void SftpChannelPrivate::handleStatus() { const SftpStatusResponse &response = m_incomingPacket.asStatusResponse(); #ifdef CREATOR_SSH_DEBUG qDebug("%s: status = %d", Q_FUNC_INFO, response.status); #endif JobMap::Iterator it = lookupJob(response.requestId); switch (it.value()->type()) { case AbstractSftpOperation::ListDir: handleLsStatus(it, response); break; case AbstractSftpOperation::Download: handleGetStatus(it, response); break; case AbstractSftpOperation::Upload: handlePutStatus(it, response); break; case AbstractSftpOperation::MakeDir: case AbstractSftpOperation::RmDir: case AbstractSftpOperation::Rm: case AbstractSftpOperation::Rename: case AbstractSftpOperation::CreateFile: handleStatusGeneric(it, response); break; } } void SftpChannelPrivate::handleStatusGeneric(const JobMap::Iterator &it, const SftpStatusResponse &response) { AbstractSftpOperation::Ptr op = it.value(); const QString error = errorMessage(response, SSH_TR("Unknown error.")); createDelayedJobFinishedSignal(op->jobId, error); m_jobs.erase(it); } void SftpChannelPrivate::handleLsStatus(const JobMap::Iterator &it, const SftpStatusResponse &response) { SftpListDir::Ptr op = it.value().staticCast(); switch (op->state) { case SftpListDir::OpenRequested: createDelayedJobFinishedSignal(op->jobId, errorMessage(response.errorString, SSH_TR("Remote directory could not be opened for reading."))); m_jobs.erase(it); break; case SftpListDir::Open: if (response.status != SSH_FX_EOF) reportRequestError(op, errorMessage(response.errorString, SSH_TR("Failed to list remote directory contents."))); op->state = SftpListDir::CloseRequested; sendData(m_outgoingPacket.generateCloseHandle(op->remoteHandle, op->jobId).rawData()); break; case SftpListDir::CloseRequested: if (!op->hasError) { const QString error = errorMessage(response, SSH_TR("Failed to close remote directory.")); createDelayedJobFinishedSignal(op->jobId, error); } m_jobs.erase(it); break; default: throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Unexpected SSH_FXP_STATUS packet."); } } void SftpChannelPrivate::handleGetStatus(const JobMap::Iterator &it, const SftpStatusResponse &response) { SftpDownload::Ptr op = it.value().staticCast(); switch (op->state) { case SftpDownload::OpenRequested: createDelayedJobFinishedSignal(op->jobId, errorMessage(response.errorString, SSH_TR("Failed to open remote file for reading."))); m_jobs.erase(it); break; case SftpDownload::Open: if (op->statRequested) { reportRequestError(op, errorMessage(response.errorString, SSH_TR("Failed to stat remote file."))); sendTransferCloseHandle(op, response.requestId); } else { if ((response.status != SSH_FX_EOF || response.requestId != op->eofId) && !op->hasError) reportRequestError(op, errorMessage(response.errorString, SSH_TR("Failed to read remote file."))); finishTransferRequest(it); } break; case SftpDownload::CloseRequested: Q_ASSERT(op->inFlightCount == 1); if (!op->hasError) { if (response.status == SSH_FX_OK) createDelayedJobFinishedSignal(op->jobId); else reportRequestError(op, errorMessage(response.errorString, SSH_TR("Failed to close remote file."))); } removeTransferRequest(it); break; default: throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Unexpected SSH_FXP_STATUS packet."); } } void SftpChannelPrivate::handlePutStatus(const JobMap::Iterator &it, const SftpStatusResponse &response) { SftpUpload::Ptr job = it.value().staticCast(); switch (job->state) { case SftpUpload::OpenRequested: createDelayedJobFinishedSignal(job->jobId, errorMessage(response.errorString, SSH_TR("Failed to open remote file for writing."))); m_jobs.erase(it); break; case SftpUpload::Open: if (response.status == SSH_FX_OK) { sendWriteRequest(it); } else if(!job->hasError) { reportRequestError(job, errorMessage(response.errorString, SSH_TR("Failed to write remote file."))); finishTransferRequest(it); } break; case SftpUpload::CloseRequested: Q_ASSERT(job->inFlightCount == 1); if (!job->hasError) { const QString error = errorMessage(response, SSH_TR("Failed to close remote file.")); createDelayedJobFinishedSignal(job->jobId, error); } m_jobs.erase(it); break; default: throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Unexpected SSH_FXP_STATUS packet."); } } void SftpChannelPrivate::handleName() { const SftpNameResponse &response = m_incomingPacket.asNameResponse(); JobMap::Iterator it = lookupJob(response.requestId); switch (it.value()->type()) { case AbstractSftpOperation::ListDir: { SftpListDir::Ptr op = it.value().staticCast(); if (op->state != SftpListDir::Open) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Unexpected SSH_FXP_NAME packet."); } for (int i = 0; i < response.files.count(); ++i) { const SftpFile &file = response.files.at(i); createDelayedDataAvailableSignal(op->jobId, file.fileName); } sendData(m_outgoingPacket.generateReadDir(op->remoteHandle, op->jobId).rawData()); break; } default: throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Unexpected SSH_FXP_NAME packet."); } } void SftpChannelPrivate::handleReadData() { const SftpDataResponse &response = m_incomingPacket.asDataResponse(); JobMap::Iterator it = lookupJob(response.requestId); if (it.value()->type() != AbstractSftpOperation::Download) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Unexpected SSH_FXP_DATA packet."); } SftpDownload::Ptr op = it.value().staticCast(); if (op->hasError) { finishTransferRequest(it); return; } if (!op->localFile->seek(op->offsets[response.requestId])) { reportRequestError(op, op->localFile->errorString()); finishTransferRequest(it); return; } if (op->localFile->write(response.data) != response.data.size()) { reportRequestError(op, op->localFile->errorString()); finishTransferRequest(it); return; } if (op->offset >= op->fileSize && op->fileSize != 0) finishTransferRequest(it); else sendReadRequest(op, response.requestId); } void SftpChannelPrivate::handleAttrs() { const SftpAttrsResponse &response = m_incomingPacket.asAttrsResponse(); JobMap::Iterator it = lookupJob(response.requestId); AbstractSftpTransfer::Ptr transfer = it.value().dynamicCast(); if (!transfer || transfer->state != AbstractSftpTransfer::Open || !transfer->statRequested) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Unexpected SSH_FXP_ATTRS packet."); } Q_ASSERT(transfer->type() == AbstractSftpOperation::Upload || transfer->type() == AbstractSftpOperation::Download); if (transfer->type() == AbstractSftpOperation::Download) { SftpDownload::Ptr op = transfer.staticCast(); if (response.attrs.sizePresent) { op->fileSize = response.attrs.size; } else { op->fileSize = 0; op->eofId = op->jobId; } op->statRequested = false; spawnReadRequests(op); } else { SftpUpload::Ptr op = transfer.staticCast(); if (response.attrs.sizePresent) { op->offset = response.attrs.size; spawnWriteRequests(it); } else { reportRequestError(op, SSH_TR("Cannot append to remote file: " "Server does not support file size attribute.")); sendTransferCloseHandle(op, op->jobId); } } } SftpChannelPrivate::JobMap::Iterator SftpChannelPrivate::lookupJob(SftpJobId id) { JobMap::Iterator it = m_jobs.find(id); if (it == m_jobs.end()) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Invalid request id in SFTP packet."); } return it; } void SftpChannelPrivate::closeHook() { createClosedSignal(); } void SftpChannelPrivate::handleOpenSuccessInternal() { #ifdef CREATOR_SSH_DEBUG qDebug("SFTP session started"); #endif m_sendFacility.sendSftpPacket(remoteChannel()); m_sftpState = SubsystemRequested; } void SftpChannelPrivate::handleOpenFailureInternal() { if (channelState() != SessionRequested) { throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, "Unexpected SSH_MSG_CHANNEL_OPEN_FAILURE packet."); } createDelayedInitFailedSignal(SSH_TR("Server could not start session.")); } void SftpChannelPrivate::sendReadRequest(const SftpDownload::Ptr &job, quint32 requestId) { Q_ASSERT(job->eofId == SftpInvalidJob); sendData(m_outgoingPacket.generateReadFile(job->remoteHandle, job->offset, AbstractSftpPacket::MaxDataSize, requestId).rawData()); job->offsets[requestId] = job->offset; job->offset += AbstractSftpPacket::MaxDataSize; if (job->offset >= job->fileSize) job->eofId = requestId; } void SftpChannelPrivate::reportRequestError(const AbstractSftpOperationWithHandle::Ptr &job, const QString &error) { createDelayedJobFinishedSignal(job->jobId, error); job->hasError = true; } void SftpChannelPrivate::finishTransferRequest(const JobMap::Iterator &it) { AbstractSftpTransfer::Ptr job = it.value().staticCast(); if (job->inFlightCount == 1) sendTransferCloseHandle(job, it.key()); else removeTransferRequest(it); } void SftpChannelPrivate::sendTransferCloseHandle(const AbstractSftpTransfer::Ptr &job, quint32 requestId) { sendData(m_outgoingPacket.generateCloseHandle(job->remoteHandle, requestId).rawData()); job->state = SftpDownload::CloseRequested; } void SftpChannelPrivate::removeTransferRequest(const JobMap::Iterator &it) { --it.value().staticCast()->inFlightCount; m_jobs.erase(it); } void SftpChannelPrivate::sendWriteRequest(const JobMap::Iterator &it) { SftpUpload::Ptr job = it.value().staticCast(); QByteArray data = job->localFile->read(AbstractSftpPacket::MaxDataSize); if (job->localFile->error() != QFile::NoError) { if (!job->hasError) { reportRequestError(job, SSH_TR("Error reading local file: %1") .arg(job->localFile->errorString())); } finishTransferRequest(it); } else if (data.isEmpty()) { finishTransferRequest(it); } else { sendData(m_outgoingPacket.generateWriteFile(job->remoteHandle, job->offset, data, it.key()).rawData()); job->offset += AbstractSftpPacket::MaxDataSize; } } void SftpChannelPrivate::spawnWriteRequests(const JobMap::Iterator &it) { SftpUpload::Ptr op = it.value().staticCast(); op->calculateInFlightCount(AbstractSftpPacket::MaxDataSize); sendWriteRequest(it); for (int i = 1; i < op->inFlightCount; ++i) sendWriteRequest(m_jobs.insert(++m_nextJobId, op)); } void SftpChannelPrivate::spawnReadRequests(const SftpDownload::Ptr &job) { job->calculateInFlightCount(AbstractSftpPacket::MaxDataSize); sendReadRequest(job, job->jobId); for (int i = 1; i < job->inFlightCount; ++i) { const quint32 requestId = ++m_nextJobId; m_jobs.insert(requestId, job); sendReadRequest(job, requestId); } } void SftpChannelPrivate::createDelayedInitFailedSignal(const QString &reason) { new SftpInitializationFailedSignal(this, QWeakPointer(m_sftp), reason); } void SftpChannelPrivate::emitInitializationFailedSignal(const QString &reason) { emit m_sftp->initializationFailed(reason); } void SftpChannelPrivate::createDelayedInitializedSignal() { new SftpInitializedSignal(this, QWeakPointer(m_sftp)); } void SftpChannelPrivate::emitInitialized() { emit m_sftp->initialized(); } void SftpChannelPrivate::createDelayedJobFinishedSignal(SftpJobId jobId, const QString &error) { new SftpJobFinishedSignal(this, QWeakPointer(m_sftp), jobId, error); } void SftpChannelPrivate::emitJobFinished(SftpJobId jobId, const QString &error) { emit m_sftp->finished(jobId, error); } void SftpChannelPrivate::createDelayedDataAvailableSignal(SftpJobId jobId, const QString &data) { new SftpDataAvailableSignal(this, QWeakPointer(m_sftp), jobId, data); } void SftpChannelPrivate::emitDataAvailable(SftpJobId jobId, const QString &data) { emit m_sftp->dataAvailable(jobId, data); } void SftpChannelPrivate::createClosedSignal() { new SftpClosedSignal(this, QWeakPointer(m_sftp)); } void SftpChannelPrivate::emitClosed() { emit m_sftp->closed(); } } // namespace Internal } // namespace Core