1
0
mirror of https://github.com/winscp/winscp.git synced 2026-10-24 02:28:55 +03:00
Files
winscp/source/core/SftpFileSystem.cpp
Martin Prikryl 092b1539fd With SFTP protocol files can be streamed to stdout in scripting using chunked encoding to distinguish multiple files (part of Bug 1738)
(cherry picked from commit c86def11d8)

Source commit: 535a8d33c450b5d74786de1fc8b480c5a663f191
2020-11-26 12:08:42 +01:00

5711 lines
178 KiB
C++

//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop
#include "SftpFileSystem.h"
#include "PuttyTools.h"
#include "Common.h"
#include "Exceptions.h"
#include "Interface.h"
#include "Terminal.h"
#include "TextsCore.h"
#include "HelpCore.h"
#include "SecureShell.h"
#include "Cryptography.h"
#include <WideStrUtils.hpp>
#include <limits>
#include <memory>
//---------------------------------------------------------------------------
#pragma package(smart_init)
//---------------------------------------------------------------------------
#define FILE_OPERATION_LOOP_TERMINAL FTerminal
//---------------------------------------------------------------------------
#define SSH_FX_OK 0
#define SSH_FX_EOF 1
#define SSH_FX_NO_SUCH_FILE 2
#define SSH_FX_PERMISSION_DENIED 3
#define SSH_FX_FAILURE 4
#define SSH_FX_OP_UNSUPPORTED 8
#define SSH_FXP_INIT 1
#define SSH_FXP_VERSION 2
#define SSH_FXP_OPEN 3
#define SSH_FXP_CLOSE 4
#define SSH_FXP_READ 5
#define SSH_FXP_WRITE 6
#define SSH_FXP_LSTAT 7
#define SSH_FXP_FSTAT 8
#define SSH_FXP_SETSTAT 9
#define SSH_FXP_FSETSTAT 10
#define SSH_FXP_OPENDIR 11
#define SSH_FXP_READDIR 12
#define SSH_FXP_REMOVE 13
#define SSH_FXP_MKDIR 14
#define SSH_FXP_RMDIR 15
#define SSH_FXP_REALPATH 16
#define SSH_FXP_STAT 17
#define SSH_FXP_RENAME 18
#define SSH_FXP_READLINK 19
#define SSH_FXP_SYMLINK 20
#define SSH_FXP_LINK 21
#define SSH_FXP_STATUS 101
#define SSH_FXP_HANDLE 102
#define SSH_FXP_DATA 103
#define SSH_FXP_NAME 104
#define SSH_FXP_ATTRS 105
#define SSH_FXP_EXTENDED 200
#define SSH_FXP_EXTENDED_REPLY 201
#define SSH_FXP_ATTRS 105
#define SSH_FILEXFER_ATTR_SIZE 0x00000001
#define SSH_FILEXFER_ATTR_UIDGID 0x00000002
#define SSH_FILEXFER_ATTR_PERMISSIONS 0x00000004
#define SSH_FILEXFER_ATTR_ACMODTIME 0x00000008
#define SSH_FILEXFER_ATTR_EXTENDED 0x80000000
#define SSH_FILEXFER_ATTR_ACCESSTIME 0x00000008
#define SSH_FILEXFER_ATTR_CREATETIME 0x00000010
#define SSH_FILEXFER_ATTR_MODIFYTIME 0x00000020
#define SSH_FILEXFER_ATTR_ACL 0x00000040
#define SSH_FILEXFER_ATTR_OWNERGROUP 0x00000080
#define SSH_FILEXFER_ATTR_SUBSECOND_TIMES 0x00000100
#define SSH_FILEXFER_ATTR_BITS 0x00000200
#define SSH_FILEXFER_ATTR_ALLOCATION_SIZE 0x00000400
#define SSH_FILEXFER_ATTR_TEXT_HINT 0x00000800
#define SSH_FILEXFER_ATTR_MIME_TYPE 0x00001000
#define SSH_FILEXFER_ATTR_LINK_COUNT 0x00002000
#define SSH_FILEXFER_ATTR_UNTRANSLATED_NAME 0x00004000
#define SSH_FILEXFER_ATTR_CTIME 0x00008000
#define SSH_FILEXFER_ATTR_EXTENDED 0x80000000
#define SSH_FILEXFER_ATTR_COMMON \
(SSH_FILEXFER_ATTR_SIZE | SSH_FILEXFER_ATTR_OWNERGROUP | \
SSH_FILEXFER_ATTR_PERMISSIONS | SSH_FILEXFER_ATTR_ACCESSTIME | \
SSH_FILEXFER_ATTR_MODIFYTIME)
#define SSH_FILEXFER_TYPE_REGULAR 1
#define SSH_FILEXFER_TYPE_DIRECTORY 2
#define SSH_FILEXFER_TYPE_SYMLINK 3
#define SSH_FILEXFER_TYPE_SPECIAL 4
#define SSH_FILEXFER_TYPE_UNKNOWN 5
#define SSH_FXF_READ 0x00000001
#define SSH_FXF_WRITE 0x00000002
#define SSH_FXF_APPEND 0x00000004
#define SSH_FXF_CREAT 0x00000008
#define SSH_FXF_TRUNC 0x00000010
#define SSH_FXF_EXCL 0x00000020
#define SSH_FXF_TEXT 0x00000040
#define SSH_FXF_ACCESS_DISPOSITION 0x00000007
#define SSH_FXF_CREATE_NEW 0x00000000
#define SSH_FXF_CREATE_TRUNCATE 0x00000001
#define SSH_FXF_OPEN_EXISTING 0x00000002
#define SSH_FXF_OPEN_OR_CREATE 0x00000003
#define SSH_FXF_TRUNCATE_EXISTING 0x00000004
#define SSH_FXF_ACCESS_APPEND_DATA 0x00000008
#define SSH_FXF_ACCESS_APPEND_DATA_ATOMIC 0x00000010
#define SSH_FXF_ACCESS_TEXT_MODE 0x00000020
#define ACE4_READ_DATA 0x00000001
#define ACE4_LIST_DIRECTORY 0x00000001
#define ACE4_WRITE_DATA 0x00000002
#define ACE4_ADD_FILE 0x00000002
#define ACE4_APPEND_DATA 0x00000004
#define ACE4_ADD_SUBDIRECTORY 0x00000004
#define ACE4_READ_NAMED_ATTRS 0x00000008
#define ACE4_WRITE_NAMED_ATTRS 0x00000010
#define ACE4_EXECUTE 0x00000020
#define ACE4_DELETE_CHILD 0x00000040
#define ACE4_READ_ATTRIBUTES 0x00000080
#define ACE4_WRITE_ATTRIBUTES 0x00000100
#define ACE4_DELETE 0x00010000
#define ACE4_READ_ACL 0x00020000
#define ACE4_WRITE_ACL 0x00040000
#define ACE4_WRITE_OWNER 0x00080000
#define ACE4_SYNCHRONIZE 0x00100000
#define SSH_FILEXFER_ATTR_FLAGS_HIDDEN 0x00000004
#define SSH_FXP_REALPATH_NO_CHECK 0x00000001
#define SSH_FXP_REALPATH_STAT_IF 0x00000002
#define SSH_FXP_REALPATH_STAT_ALWAYS 0x00000003
#define SFTP_MAX_PACKET_LEN 1024000
//---------------------------------------------------------------------------
#define SFTP_EXT_OWNER_GROUP "owner-group-query@generic-extensions"
#define SFTP_EXT_OWNER_GROUP_REPLY "owner-group-query-reply@generic-extensions"
#define SFTP_EXT_NEWLINE "newline"
#define SFTP_EXT_SUPPORTED "supported"
#define SFTP_EXT_SUPPORTED2 "supported2"
#define SFTP_EXT_FSROOTS "fs-roots@vandyke.com"
#define SFTP_EXT_VENDOR_ID "vendor-id"
#define SFTP_EXT_VERSIONS "versions"
#define SFTP_EXT_SPACE_AVAILABLE "space-available"
#define SFTP_EXT_CHECK_FILE "check-file"
#define SFTP_EXT_CHECK_FILE_NAME "check-file-name"
#define SFTP_EXT_STATVFS "statvfs@openssh.com"
#define SFTP_EXT_STATVFS_VALUE_V2 L"2"
#define SFTP_EXT_STATVFS_ST_RDONLY 0x1
#define SFTP_EXT_STATVFS_ST_NOSUID 0x2
#define SFTP_EXT_HARDLINK "hardlink@openssh.com"
#define SFTP_EXT_HARDLINK_VALUE_V1 L"1"
#define SFTP_EXT_COPY_FILE "copy-file"
//---------------------------------------------------------------------------
#define OGQ_LIST_OWNERS 0x01
#define OGQ_LIST_GROUPS 0x02
//---------------------------------------------------------------------------
const int SFTPMinVersion = 0;
const int SFTPMaxVersion = 6;
const unsigned int SFTPNoMessageNumber = static_cast<unsigned int>(-1);
const int asNo = 0;
const int asOK = 1 << SSH_FX_OK;
const int asEOF = 1 << SSH_FX_EOF;
const int asPermDenied = 1 << SSH_FX_PERMISSION_DENIED;
const int asOpUnsupported = 1 << SSH_FX_OP_UNSUPPORTED;
const int asNoSuchFile = 1 << SSH_FX_NO_SUCH_FILE;
const int asAll = 0xFFFF;
//---------------------------------------------------------------------------
#define GET_32BIT(cp) \
(((unsigned long)(unsigned char)(cp)[0] << 24) | \
((unsigned long)(unsigned char)(cp)[1] << 16) | \
((unsigned long)(unsigned char)(cp)[2] << 8) | \
((unsigned long)(unsigned char)(cp)[3]))
#define PUT_32BIT(cp, value) { \
(cp)[0] = (unsigned char)((value) >> 24); \
(cp)[1] = (unsigned char)((value) >> 16); \
(cp)[2] = (unsigned char)((value) >> 8); \
(cp)[3] = (unsigned char)(value); }
//---------------------------------------------------------------------------
#define SFTP_PACKET_ALLOC_DELTA 256
//---------------------------------------------------------------------------
#pragma warn -inl
//---------------------------------------------------------------------------
struct TSFTPSupport
{
TSFTPSupport() :
AttribExtensions(new TStringList()),
Extensions(new TStringList())
{
Reset();
}
~TSFTPSupport()
{
delete AttribExtensions;
delete Extensions;
}
void Reset()
{
AttributeMask = 0;
AttributeBits = 0;
OpenFlags = 0;
AccessMask = 0;
MaxReadSize = 0;
OpenBlockVector = 0;
BlockVector = 0;
AttribExtensions->Clear();
Extensions->Clear();
Loaded = false;
}
unsigned int AttributeMask;
unsigned int AttributeBits;
unsigned int OpenFlags;
unsigned int AccessMask;
unsigned int MaxReadSize;
unsigned int OpenBlockVector;
unsigned int BlockVector;
TStrings * AttribExtensions;
TStrings * Extensions;
bool Loaded;
};
//---------------------------------------------------------------------------
class TSFTPPacket
{
public:
TSFTPPacket()
{
Init();
}
TSFTPPacket(const TSFTPPacket & Source)
{
Init();
*this = Source;
}
TSFTPPacket(unsigned char AType)
{
Init();
ChangeType(AType);
}
TSFTPPacket(const unsigned char * Source, unsigned int Len)
{
Init();
FLength = Len;
Capacity = FLength;
memcpy(Data, Source, Len);
}
TSFTPPacket(const RawByteString & Source)
{
Init();
FLength = Source.Length();
Capacity = FLength;
memcpy(Data, Source.c_str(), Source.Length());
}
~TSFTPPacket()
{
if (FData != NULL)
{
delete[] (FData - FSendPrefixLen);
}
if (FReservedBy) FReservedBy->UnreserveResponse(this);
}
void ChangeType(unsigned char AType)
{
FPosition = 0;
FLength = 0;
Capacity = 0;
FType = AType;
AddByte(FType);
if ((FType != 1) && (FType != SSH_FXP_INIT))
{
AssignNumber();
AddCardinal(FMessageNumber);
}
}
void Reuse()
{
AssignNumber();
DebugAssert(Length >= 5);
// duplicated in AddCardinal()
unsigned char Buf[4];
PUT_32BIT(Buf, FMessageNumber);
memcpy(FData + 1, Buf, sizeof(Buf));
}
void AddByte(unsigned char Value)
{
Add(&Value, sizeof(Value));
}
void AddBool(bool Value)
{
AddByte(Value ? 1 : 0);
}
void AddCardinal(unsigned long Value)
{
// duplicated in Reuse()
unsigned char Buf[4];
PUT_32BIT(Buf, Value);
Add(&Buf, sizeof(Buf));
}
void AddInt64(__int64 Value)
{
AddCardinal((unsigned long)(Value >> 32));
AddCardinal((unsigned long)(Value & 0xFFFFFFFF));
}
void AddData(const void * Data, int ALength)
{
AddCardinal(ALength);
Add(Data, ALength);
}
void AddString(const RawByteString & Value)
{
AddCardinal(Value.Length());
Add(Value.c_str(), Value.Length());
}
inline void AddUtfString(const UTF8String & Value)
{
AddString(Value);
}
inline void AddUtfString(const UnicodeString & Value)
{
AddUtfString(UTF8String(Value));
}
inline void AddString(const UnicodeString Value, TAutoSwitch Utf)
{
// asAuto: Using UTF until we receive non-UTF string from the server
if ((Utf == asOn) || (Utf == asAuto))
{
AddUtfString(Value);
}
else
{
AddString(RawByteString(AnsiString(Value)));
}
}
// now purposeless alias to AddString
inline void AddPathString(const UnicodeString & Value, TAutoSwitch Utf)
{
AddString(Value, Utf);
}
unsigned int AllocationSizeAttribute(int Version)
{
return (Version >= 6) ? SSH_FILEXFER_ATTR_ALLOCATION_SIZE : SSH_FILEXFER_ATTR_SIZE;
}
void AddProperties(unsigned short * Rights, TRemoteToken * Owner,
TRemoteToken * Group, __int64 * MTime, __int64 * ATime,
__int64 * Size, bool IsDirectory, int Version, TAutoSwitch Utf)
{
int Flags = 0;
if (Size != NULL)
{
Flags |= AllocationSizeAttribute(Version);
}
// both or neither
DebugAssert((Owner != NULL) == (Group != NULL));
if ((Owner != NULL) && (Group != NULL))
{
if (Version < 4)
{
DebugAssert(Owner->IDValid && Group->IDValid);
Flags |= SSH_FILEXFER_ATTR_UIDGID;
}
else
{
DebugAssert(Owner->NameValid && Group->NameValid);
Flags |= SSH_FILEXFER_ATTR_OWNERGROUP;
}
}
if (Rights != NULL)
{
Flags |= SSH_FILEXFER_ATTR_PERMISSIONS;
}
if ((Version < 4) && ((MTime != NULL) || (ATime != NULL)))
{
Flags |= SSH_FILEXFER_ATTR_ACMODTIME;
}
if ((Version >= 4) && (ATime != NULL))
{
Flags |= SSH_FILEXFER_ATTR_ACCESSTIME;
}
if ((Version >= 4) && (MTime != NULL))
{
Flags |= SSH_FILEXFER_ATTR_MODIFYTIME;
}
AddCardinal(Flags);
if (Version >= 4)
{
AddByte(static_cast<unsigned char>(IsDirectory ?
SSH_FILEXFER_TYPE_DIRECTORY : SSH_FILEXFER_TYPE_REGULAR));
}
if (Size != NULL)
{
// this is SSH_FILEXFER_ATTR_SIZE for version <= 5, but
// SSH_FILEXFER_ATTR_ALLOCATION_SIZE for version >= 6
AddInt64(*Size);
}
if ((Owner != NULL) && (Group != NULL))
{
if (Version < 4)
{
DebugAssert(Owner->IDValid && Group->IDValid);
AddCardinal(Owner->ID);
AddCardinal(Group->ID);
}
else
{
DebugAssert(Owner->NameValid && Group->NameValid);
AddString(Owner->Name, Utf);
AddString(Group->Name, Utf);
}
}
if (Rights != NULL)
{
AddCardinal(*Rights);
}
if ((Version < 4) && ((MTime != NULL) || (ATime != NULL)))
{
// any way to reflect sbSignedTS here?
// (note that casting __int64 > 2^31 < 2^32 to unsigned long is wrapped,
// thus we never can set time after 2038, even if the server supports it)
AddCardinal(static_cast<unsigned long>(ATime != NULL ? *ATime : *MTime));
AddCardinal(static_cast<unsigned long>(MTime != NULL ? *MTime : *ATime));
}
if ((Version >= 4) && (ATime != NULL))
{
AddInt64(*ATime);
}
if ((Version >= 4) && (MTime != NULL))
{
AddInt64(*MTime);
}
}
void AddProperties(const TRemoteProperties * Properties,
unsigned short BaseRights, bool IsDirectory, int Version, TAutoSwitch Utf,
TChmodSessionAction * Action)
{
enum TValid { valNone = 0, valRights = 0x01, valOwner = 0x02, valGroup = 0x04,
valMTime = 0x08, valATime = 0x10 } Valid = valNone;
unsigned short RightsNum = 0;
TRemoteToken Owner;
TRemoteToken Group;
__int64 MTime;
__int64 ATime;
if (Properties != NULL)
{
if (Properties->Valid.Contains(vpGroup))
{
Valid = (TValid)(Valid | valGroup);
Group = Properties->Group;
}
if (Properties->Valid.Contains(vpOwner))
{
Valid = (TValid)(Valid | valOwner);
Owner = Properties->Owner;
}
if (Properties->Valid.Contains(vpRights))
{
Valid = (TValid)(Valid | valRights);
TRights Rights = BaseRights;
Rights |= Properties->Rights.NumberSet;
Rights &= (unsigned short)~Properties->Rights.NumberUnset;
if (IsDirectory && Properties->AddXToDirectories)
{
Rights.AddExecute();
}
RightsNum = Rights;
if (Action != NULL)
{
Action->Rights(Rights);
}
}
if (Properties->Valid.Contains(vpLastAccess))
{
Valid = (TValid)(Valid | valATime);
ATime = Properties->LastAccess;
}
if (Properties->Valid.Contains(vpModification))
{
Valid = (TValid)(Valid | valMTime);
MTime = Properties->Modification;
}
}
AddProperties(
Valid & valRights ? &RightsNum : NULL,
Valid & valOwner ? &Owner : NULL,
Valid & valGroup ? &Group : NULL,
Valid & valMTime ? &MTime : NULL,
Valid & valATime ? &ATime : NULL,
NULL, IsDirectory, Version, Utf);
}
unsigned char GetByte()
{
Need(sizeof(unsigned char));
unsigned char Result = FData[FPosition];
DataConsumed(sizeof(unsigned char));
return Result;
}
bool GetBool()
{
return (GetByte() != 0);
}
bool CanGetBool()
{
return (RemainingLength >= sizeof(unsigned char));
}
unsigned long GetCardinal()
{
unsigned long Result = PeekCardinal();
DataConsumed(sizeof(Result));
return Result;
}
bool CanGetCardinal()
{
return (RemainingLength >= sizeof(unsigned long));
}
unsigned long GetSmallCardinal()
{
unsigned long Result;
Need(2);
Result = (FData[FPosition] << 8) + FData[FPosition + 1];
DataConsumed(2);
return Result;
}
bool CanGetSmallCardinal()
{
return (RemainingLength >= 2);
}
__int64 GetInt64()
{
__int64 Hi = GetCardinal();
__int64 Lo = GetCardinal();
return (Hi << 32) + Lo;
}
RawByteString GetRawByteString()
{
RawByteString Result;
unsigned long Len = GetCardinal();
Need(Len);
// cannot happen anyway as Need() would raise exception
DebugAssert(Len < SFTP_MAX_PACKET_LEN);
Result.SetLength(Len);
memcpy(Result.c_str(), FData + FPosition, Len);
DataConsumed(Len);
return Result;
}
bool CanGetString(unsigned int & Size)
{
bool Result = CanGetCardinal();
if (Result)
{
unsigned long Len = PeekCardinal();
Size = (sizeof(Len) + Len);
Result = (Size <= RemainingLength);
}
return Result;
}
// For reading strings that are character strings (not byte strings as
// as file handles), and SFTP spec does not say explicitly that they
// are in UTF. For most of them it actually does not matter as
// the content should be pure ASCII (e.g. extension names, etc.)
inline UnicodeString GetAnsiString()
{
return AnsiToString(GetRawByteString());
}
inline RawByteString GetFileHandle()
{
return GetRawByteString();
}
inline UnicodeString GetString(TAutoSwitch & Utf)
{
if (Utf != asOff)
{
return GetUtfString(Utf);
}
else
{
return GetAnsiString();
}
}
// now purposeless alias to GetString(bool)
inline UnicodeString GetPathString(TAutoSwitch & Utf)
{
return GetString(Utf);
}
void GetFile(TRemoteFile * File, int Version, TDSTMode DSTMode, TAutoSwitch & Utf, bool SignedTS, bool Complete)
{
DebugAssert(File);
unsigned int Flags;
UnicodeString ListingStr;
unsigned long Permissions = 0;
bool ParsingFailed = false;
if (Type != SSH_FXP_ATTRS)
{
File->FileName = GetPathString(Utf);
if (Version < 4)
{
ListingStr = GetString(Utf);
}
}
Flags = GetCardinal();
if (Version >= 4)
{
unsigned char FXType = GetByte();
// -:regular, D:directory, L:symlink, S:special, U:unknown
// O:socket, C:char device, B:block device, F:fifo
// SSH-2.0-cryptlib returns file type 0 in response to SSH_FXP_LSTAT,
// handle this undefined value as "unknown"
static wchar_t * Types = L"U-DLSUOCBF";
if (FXType > (unsigned char)wcslen(Types))
{
throw Exception(FMTLOAD(SFTP_UNKNOWN_FILE_TYPE, (int(FXType))));
}
File->Type = Types[FXType];
}
if (Flags & SSH_FILEXFER_ATTR_SIZE)
{
File->Size = GetInt64();
}
// SFTP-6 only
if (Flags & SSH_FILEXFER_ATTR_ALLOCATION_SIZE)
{
GetInt64(); // skip
}
// SSH-2.0-3.2.0 F-SECURE SSH - Process Software MultiNet
// sets SSH_FILEXFER_ATTR_UIDGID for v4, but does not include the UID/GUID
if ((Flags & SSH_FILEXFER_ATTR_UIDGID) && (Version < 4))
{
File->Owner.ID = GetCardinal();
File->Group.ID = GetCardinal();
}
if (Flags & SSH_FILEXFER_ATTR_OWNERGROUP)
{
DebugAssert(Version >= 4);
File->Owner.Name = GetString(Utf);
File->Group.Name = GetString(Utf);
}
if (Flags & SSH_FILEXFER_ATTR_PERMISSIONS)
{
Permissions = GetCardinal();
}
if (Version < 4)
{
if (Flags & SSH_FILEXFER_ATTR_ACMODTIME)
{
File->LastAccess = UnixToDateTime(
SignedTS ?
static_cast<__int64>(static_cast<signed long>(GetCardinal())) :
static_cast<__int64>(GetCardinal()),
DSTMode);
File->Modification = UnixToDateTime(
SignedTS ?
static_cast<__int64>(static_cast<signed long>(GetCardinal())) :
static_cast<__int64>(GetCardinal()),
DSTMode);
}
}
else
{
if (Flags & SSH_FILEXFER_ATTR_ACCESSTIME)
{
File->LastAccess = UnixToDateTime(GetInt64(), DSTMode);
if (Flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES)
{
GetCardinal(); // skip access time subseconds
}
}
if (Flags & SSH_FILEXFER_ATTR_CREATETIME)
{
GetInt64(); // skip create time
if (Flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES)
{
GetCardinal(); // skip create time subseconds
}
}
if (Flags & SSH_FILEXFER_ATTR_MODIFYTIME)
{
File->Modification = UnixToDateTime(GetInt64(), DSTMode);
if (Flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES)
{
GetCardinal(); // skip modification time subseconds
}
}
// SFTP-6
if (Flags & SSH_FILEXFER_ATTR_CTIME)
{
GetInt64(); // skip attribute modification time
if (Flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES)
{
GetCardinal(); // skip attribute modification time subseconds
}
}
}
if (Flags & SSH_FILEXFER_ATTR_ACL)
{
GetRawByteString();
}
if (Flags & SSH_FILEXFER_ATTR_BITS)
{
// while SSH_FILEXFER_ATTR_BITS is defined for SFTP5 only, vandyke 2.3.3 sets it
// for SFTP4 as well
unsigned long Bits = GetCardinal();
if (Version >= 6)
{
unsigned long BitsValid = GetCardinal();
Bits = Bits & BitsValid;
}
if (FLAGSET(Bits, SSH_FILEXFER_ATTR_FLAGS_HIDDEN))
{
File->IsHidden = true;
}
}
// skip some SFTP-6 only fields
if (Flags & SSH_FILEXFER_ATTR_TEXT_HINT)
{
GetByte();
}
if (Flags & SSH_FILEXFER_ATTR_MIME_TYPE)
{
GetAnsiString();
}
if (Flags & SSH_FILEXFER_ATTR_LINK_COUNT)
{
GetCardinal();
}
if (Flags & SSH_FILEXFER_ATTR_UNTRANSLATED_NAME)
{
GetPathString(Utf);
}
if ((Version < 4) && (Type != SSH_FXP_ATTRS))
{
try
{
// update permissions and user/group name
// modification time and filename is ignored
File->ListingStr = ListingStr;
}
catch(...)
{
// ignore any error while parsing listing line,
// SFTP specification do not recommend to parse it
ParsingFailed = true;
}
}
if (Type == SSH_FXP_ATTRS || Version >= 4 || ParsingFailed)
{
wchar_t Type = FILETYPE_DEFAULT;
if (FLAGSET(Flags, SSH_FILEXFER_ATTR_PERMISSIONS))
{
File->Rights->Number = (unsigned short)(Permissions & TRights::rfAllSpecials);
if (FLAGSET(Permissions, TRights::rfDirectory))
{
Type = FILETYPE_DIRECTORY;
}
}
if (Version < 4)
{
File->Type = Type;
}
}
if (Flags & SSH_FILEXFER_ATTR_EXTENDED)
{
unsigned int ExtendedCount = GetCardinal();
for (unsigned int Index = 0; Index < ExtendedCount; Index++)
{
GetRawByteString(); // skip extended_type
GetRawByteString(); // skip extended_data
}
}
if (Complete)
{
File->Complete();
}
}
unsigned char * GetNextData(unsigned int Size = 0)
{
if (Size > 0)
{
Need(Size);
}
return FPosition < FLength ? FData + FPosition : NULL;
}
void DataConsumed(unsigned int Size)
{
FPosition += Size;
}
void DataUpdated(int ALength)
{
FPosition = 0;
FLength = ALength;
FType = GetByte();
if (FType != SSH_FXP_VERSION)
{
FMessageNumber = GetCardinal();
}
else
{
FMessageNumber = SFTPNoMessageNumber;
}
}
void LoadFromFile(const UnicodeString FileName)
{
TStringList * DumpLines = new TStringList();
RawByteString Dump;
try
{
DumpLines->LoadFromFile(FileName);
Dump = RawByteString(AnsiString(DumpLines->Text));
}
__finally
{
delete DumpLines;
}
Capacity = 20480;
unsigned char Byte[3];
memset(Byte, '\0', sizeof(Byte));
int Index = 1;
unsigned int Length = 0;
while (Index < Dump.Length())
{
char C = Dump[Index];
if (IsHex(C))
{
if (Byte[0] == '\0')
{
Byte[0] = C;
}
else
{
Byte[1] = C;
DebugAssert(Length < Capacity);
Data[Length] = HexToByte(UnicodeString(reinterpret_cast<char *>(Byte)));
Length++;
memset(Byte, '\0', sizeof(Byte));
}
}
Index++;
}
DataUpdated(Length);
}
UnicodeString __fastcall Dump() const
{
UnicodeString Result;
for (unsigned int Index = 0; Index < Length; Index++)
{
Result += ByteToHex(Data[Index]) + L",";
if (((Index + 1) % 25) == 0)
{
Result += L"\n";
}
}
return Result;
}
TSFTPPacket & operator = (const TSFTPPacket & Source)
{
Capacity = 0;
Add(Source.Data, Source.Length);
DataUpdated(Source.Length);
FPosition = Source.FPosition;
return *this;
}
__property unsigned int Length = { read = FLength };
__property unsigned int RemainingLength = { read = GetRemainingLength };
__property unsigned char * Data = { read = FData };
__property unsigned char * SendData = { read = GetSendData };
__property unsigned int SendLength = { read = GetSendLength };
__property unsigned int Capacity = { read = FCapacity, write = SetCapacity };
__property unsigned char Type = { read = FType };
__property unsigned char RequestType = { read = GetRequestType };
__property unsigned int MessageNumber = { read = FMessageNumber, write = FMessageNumber };
__property TSFTPFileSystem * ReservedBy = { read = FReservedBy, write = FReservedBy };
__property UnicodeString TypeName = { read = GetTypeName };
private:
unsigned char * FData;
unsigned int FLength;
unsigned int FCapacity;
unsigned int FPosition;
unsigned char FType;
unsigned int FMessageNumber;
TSFTPFileSystem * FReservedBy;
static int FMessageCounter;
static const FSendPrefixLen = 4;
void Init()
{
FData = NULL;
FCapacity = 0;
FLength = 0;
FPosition = 0;
FMessageNumber = SFTPNoMessageNumber;
FType = -1;
FReservedBy = NULL;
}
void AssignNumber()
{
// this is not strictly thread-safe, but as it is accessed from multiple
// threads only for multiple connection, it is not problem if two threads get
// the same number
FMessageNumber = (FMessageCounter << 8) + FType;
FMessageCounter++;
}
unsigned char GetRequestType()
{
if (FMessageNumber != SFTPNoMessageNumber)
{
return (unsigned char)(FMessageNumber & 0xFF);
}
else
{
DebugAssert(Type == SSH_FXP_VERSION);
return SSH_FXP_INIT;
}
}
inline void Add(const void * AData, int ALength)
{
if (Length + ALength > Capacity)
{
Capacity = Length + ALength + SFTP_PACKET_ALLOC_DELTA;
}
memcpy(FData + Length, AData, ALength);
FLength += ALength;
}
void SetCapacity(unsigned int ACapacity)
{
if (ACapacity != Capacity)
{
FCapacity = ACapacity;
if (FCapacity > 0)
{
unsigned char * NData = (new unsigned char[FCapacity + FSendPrefixLen]) + FSendPrefixLen;
if (FData)
{
memcpy(NData - FSendPrefixLen, FData - FSendPrefixLen,
(FLength < FCapacity ? FLength : FCapacity) + FSendPrefixLen);
delete[] (FData - FSendPrefixLen);
}
FData = NData;
}
else
{
if (FData) delete[] (FData - FSendPrefixLen);
FData = NULL;
}
if (FLength > FCapacity) FLength = FCapacity;
}
}
UnicodeString GetTypeName() const
{
#define TYPE_CASE(TYPE) case TYPE: return TEXT(#TYPE)
switch (Type) {
TYPE_CASE(SSH_FXP_INIT);
TYPE_CASE(SSH_FXP_VERSION);
TYPE_CASE(SSH_FXP_OPEN);
TYPE_CASE(SSH_FXP_CLOSE);
TYPE_CASE(SSH_FXP_READ);
TYPE_CASE(SSH_FXP_WRITE);
TYPE_CASE(SSH_FXP_LSTAT);
TYPE_CASE(SSH_FXP_FSTAT);
TYPE_CASE(SSH_FXP_SETSTAT);
TYPE_CASE(SSH_FXP_FSETSTAT);
TYPE_CASE(SSH_FXP_OPENDIR);
TYPE_CASE(SSH_FXP_READDIR);
TYPE_CASE(SSH_FXP_REMOVE);
TYPE_CASE(SSH_FXP_MKDIR);
TYPE_CASE(SSH_FXP_RMDIR);
TYPE_CASE(SSH_FXP_REALPATH);
TYPE_CASE(SSH_FXP_STAT);
TYPE_CASE(SSH_FXP_RENAME);
TYPE_CASE(SSH_FXP_READLINK);
TYPE_CASE(SSH_FXP_SYMLINK);
TYPE_CASE(SSH_FXP_LINK);
TYPE_CASE(SSH_FXP_STATUS);
TYPE_CASE(SSH_FXP_HANDLE);
TYPE_CASE(SSH_FXP_DATA);
TYPE_CASE(SSH_FXP_NAME);
TYPE_CASE(SSH_FXP_ATTRS);
TYPE_CASE(SSH_FXP_EXTENDED);
TYPE_CASE(SSH_FXP_EXTENDED_REPLY);
default:
return FORMAT(L"Unknown message (%d)", (int(Type)));
}
}
unsigned char * GetSendData() const
{
unsigned char * Result = FData - FSendPrefixLen;
// this is not strictly const-object operation
PUT_32BIT(Result, Length);
return Result;
}
unsigned int GetSendLength() const
{
return FSendPrefixLen + Length;
}
unsigned int GetRemainingLength() const
{
return Length - FPosition;
}
inline void Need(unsigned int Size)
{
if (Size > RemainingLength)
{
throw Exception(FMTLOAD(SFTP_PACKET_ERROR, (int(FPosition), int(Size), int(FLength))));
}
}
unsigned long PeekCardinal()
{
unsigned long Result;
Need(sizeof(Result));
Result = GET_32BIT(FData + FPosition);
return Result;
}
inline UnicodeString GetUtfString(TAutoSwitch & Utf)
{
DebugAssert(Utf != asOff);
UnicodeString Result;
RawByteString S = GetRawByteString();
if (Utf == asAuto)
{
TEncodeType EncodeType = DetectUTF8Encoding(S);
if (EncodeType == etANSI)
{
Utf = asOff;
Result = AnsiToString(S);
}
}
if (Utf != asOff)
{
Result = UTF8ToString(S);
}
return Result;
}
};
//---------------------------------------------------------------------------
int TSFTPPacket::FMessageCounter = 0;
//---------------------------------------------------------------------------
class TSFTPQueue
{
public:
__fastcall TSFTPQueue(TSFTPFileSystem * AFileSystem)
{
FFileSystem = AFileSystem;
DebugAssert(FFileSystem);
FRequests = new TList();
FResponses = new TList();
}
virtual __fastcall ~TSFTPQueue()
{
TSFTPQueuePacket * Request;
TSFTPPacket * Response;
DebugAssert(FResponses->Count == FRequests->Count);
for (int Index = 0; Index < FRequests->Count; Index++)
{
Request = static_cast<TSFTPQueuePacket*>(FRequests->Items[Index]);
DebugAssert(Request);
delete Request;
Response = static_cast<TSFTPPacket*>(FResponses->Items[Index]);
DebugAssert(Response);
delete Response;
}
delete FRequests;
delete FResponses;
}
bool __fastcall Init()
{
return SendRequests();
}
virtual void __fastcall Dispose(int ExpectedType, int AllowStatus)
{
DebugAssert(FFileSystem->FTerminal->Active);
while (FRequests->Count)
{
DebugAssert(FResponses->Count);
std::unique_ptr<TSFTPQueuePacket> Request(DebugNotNull(static_cast<TSFTPQueuePacket*>(FRequests->Items[0])));
std::unique_ptr<TSFTPPacket> Response(DebugNotNull(static_cast<TSFTPPacket*>(FResponses->Items[0])));
// Particularly when ExpectedType >= 0, the ReceiveResponse may throw, and we have to remove the packets from queue
FRequests->Delete(0);
FResponses->Delete(0);
try
{
ReceiveResponse(Request.get(), Response.get(), ExpectedType, AllowStatus);
}
catch(Exception & E)
{
if (ExpectedType < 0)
{
if (FFileSystem->FTerminal->Active)
{
FFileSystem->FTerminal->LogEvent(L"Error while disposing the SFTP queue.");
FFileSystem->FTerminal->Log->AddException(&E);
}
else
{
FFileSystem->FTerminal->LogEvent(L"Fatal error while disposing the SFTP queue.");
throw;
}
}
else
{
throw;
}
}
}
}
void __fastcall DisposeSafe(int ExpectedType = -1, int AllowStatus = -1)
{
if (FFileSystem->FTerminal->Active)
{
Dispose(ExpectedType, AllowStatus);
}
}
bool __fastcall ReceivePacket(TSFTPPacket * Packet,
int ExpectedType = -1, int AllowStatus = -1, void ** Token = NULL, bool TryOnly = false)
{
DebugAssert(FRequests->Count);
bool Result;
TSFTPQueuePacket * Request = NULL;
TSFTPPacket * Response = NULL;
try
{
Request = static_cast<TSFTPQueuePacket*>(FRequests->Items[0]);
FRequests->Delete(0);
DebugAssert(Request);
if (Token != NULL)
{
*Token = Request->Token;
}
Response = static_cast<TSFTPPacket*>(FResponses->Items[0]);
FResponses->Delete(0);
DebugAssert(Response);
ReceiveResponse(Request, Response, ExpectedType, AllowStatus, TryOnly);
if ((Response->Capacity == 0) && DebugAlwaysTrue(TryOnly))
{
FRequests->Insert(0, Request);
Request = NULL;
FResponses->Insert(0, Response);
Response = NULL;
Result = true;
}
else
{
if (Packet)
{
*Packet = *Response;
}
Result = !End(Response);
if (Result)
{
SendRequests();
}
}
}
__finally
{
delete Request;
delete Response;
}
return Result;
}
bool __fastcall Next(int ExpectedType = -1, int AllowStatus = -1)
{
return ReceivePacket(NULL, ExpectedType, AllowStatus);
}
protected:
TList * FRequests;
TList * FResponses;
TSFTPFileSystem * FFileSystem;
class TSFTPQueuePacket : public TSFTPPacket
{
public:
TSFTPQueuePacket() :
TSFTPPacket()
{
Token = NULL;
}
void * Token;
};
virtual bool __fastcall InitRequest(TSFTPQueuePacket * Request) = 0;
virtual bool __fastcall End(TSFTPPacket * Response) = 0;
virtual void __fastcall SendPacket(TSFTPQueuePacket * Packet)
{
FFileSystem->SendPacket(Packet);
}
virtual void __fastcall ReceiveResponse(
const TSFTPPacket * Packet, TSFTPPacket * Response, int ExpectedType,
int AllowStatus, bool TryOnly = false)
{
FFileSystem->ReceiveResponse(Packet, Response, ExpectedType, AllowStatus, TryOnly);
}
// sends as many requests as allowed by implementation
virtual bool SendRequests() = 0;
virtual bool __fastcall SendRequest()
{
TSFTPQueuePacket * Request = NULL;
try
{
Request = new TSFTPQueuePacket();
if (!InitRequest(Request))
{
delete Request;
Request = NULL;
}
}
catch(...)
{
delete Request;
throw;
}
if (Request != NULL)
{
TSFTPPacket * Response = new TSFTPPacket();
FRequests->Add(Request);
FResponses->Add(Response);
// make sure the response is reserved before actually ending the message
// as we may receive response asynchronously before SendPacket finishes
FFileSystem->ReserveResponse(Request, Response);
SendPacket(Request);
}
return (Request != NULL);
}
};
//---------------------------------------------------------------------------
class TSFTPFixedLenQueue : public TSFTPQueue
{
public:
__fastcall TSFTPFixedLenQueue(TSFTPFileSystem * AFileSystem) : TSFTPQueue(AFileSystem)
{
FMissedRequests = 0;
}
virtual __fastcall ~TSFTPFixedLenQueue(){}
bool Init(int QueueLen)
{
FMissedRequests = QueueLen - 1;
return TSFTPQueue::Init();
}
protected:
int FMissedRequests;
// sends as many requests as allowed by implementation
virtual bool SendRequests()
{
bool Result = false;
FMissedRequests++;
while ((FMissedRequests > 0) && SendRequest())
{
Result = true;
FMissedRequests--;
}
return Result;
}
};
//---------------------------------------------------------------------------
class TSFTPAsynchronousQueue : public TSFTPQueue
{
public:
__fastcall TSFTPAsynchronousQueue(TSFTPFileSystem * AFileSystem) : TSFTPQueue(AFileSystem)
{
FFileSystem->FSecureShell->RegisterReceiveHandler(ReceiveHandler);
FReceiveHandlerRegistered = true;
}
virtual __fastcall ~TSFTPAsynchronousQueue()
{
UnregisterReceiveHandler();
}
virtual void __fastcall Dispose(int ExpectedType, int AllowStatus)
{
// we do not want to receive asynchronous notifications anymore,
// while waiting synchronously for pending responses
UnregisterReceiveHandler();
TSFTPQueue::Dispose(ExpectedType, AllowStatus);
}
bool __fastcall Continue()
{
return SendRequest();
}
protected:
// event handler for incoming data
void __fastcall ReceiveHandler(TObject * /*Sender*/)
{
while (// optimization only as we call ReceivePacket with TryOnly anyway
FFileSystem->PeekPacket() &&
ReceivePacketAsynchronously())
{
// loop
}
}
virtual bool __fastcall ReceivePacketAsynchronously() = 0;
// sends as many requests as allowed by implementation
virtual bool SendRequests()
{
// noop
return true;
}
void __fastcall UnregisterReceiveHandler()
{
if (FReceiveHandlerRegistered)
{
FReceiveHandlerRegistered = false;
FFileSystem->FSecureShell->UnregisterReceiveHandler(ReceiveHandler);
}
}
private:
bool FReceiveHandlerRegistered;
};
//---------------------------------------------------------------------------
class TSFTPDownloadQueue : public TSFTPFixedLenQueue
{
public:
TSFTPDownloadQueue(TSFTPFileSystem * AFileSystem) :
TSFTPFixedLenQueue(AFileSystem)
{
}
virtual __fastcall ~TSFTPDownloadQueue(){}
bool __fastcall Init(int QueueLen, const RawByteString & AHandle,__int64 ATransferred,
TFileOperationProgressType * AOperationProgress)
{
FHandle = AHandle;
FTransferred = ATransferred;
OperationProgress = AOperationProgress;
return TSFTPFixedLenQueue::Init(QueueLen);
}
void __fastcall InitFillGapRequest(__int64 Offset, unsigned long Missing,
TSFTPPacket * Packet)
{
InitRequest(Packet, Offset, Missing);
}
bool __fastcall ReceivePacket(TSFTPPacket * Packet, unsigned long & BlockSize)
{
void * Token;
bool Result = TSFTPFixedLenQueue::ReceivePacket(Packet, SSH_FXP_DATA, asEOF, &Token);
BlockSize = reinterpret_cast<unsigned long>(Token);
return Result;
}
protected:
virtual bool __fastcall InitRequest(TSFTPQueuePacket * Request)
{
unsigned int BlockSize = FFileSystem->DownloadBlockSize(OperationProgress);
InitRequest(Request, FTransferred, BlockSize);
Request->Token = reinterpret_cast<void*>(BlockSize);
FTransferred += BlockSize;
return true;
}
void __fastcall InitRequest(TSFTPPacket * Request, __int64 Offset,
unsigned long Size)
{
Request->ChangeType(SSH_FXP_READ);
Request->AddString(FHandle);
Request->AddInt64(Offset);
Request->AddCardinal(Size);
}
virtual bool __fastcall End(TSFTPPacket * Response)
{
return (Response->Type != SSH_FXP_DATA);
}
private:
TFileOperationProgressType * OperationProgress;
__int64 FTransferred;
RawByteString FHandle;
};
//---------------------------------------------------------------------------
class TSFTPUploadQueue : public TSFTPAsynchronousQueue
{
public:
TSFTPUploadQueue(TSFTPFileSystem * AFileSystem, TEncryption * Encryption) :
TSFTPAsynchronousQueue(AFileSystem),
FEncryption(Encryption)
{
FStream = NULL;
FOnTransferIn = NULL;
OperationProgress = NULL;
FLastBlockSize = 0;
FEnd = false;
FConvertToken = false;
}
virtual __fastcall ~TSFTPUploadQueue()
{
delete FStream;
}
bool __fastcall Init(const UnicodeString & AFileName,
HANDLE AFile, TTransferInEvent OnTransferIn, TFileOperationProgressType * AOperationProgress,
const RawByteString AHandle, __int64 ATransferred,
int ConvertParams)
{
FFileName = AFileName;
if (OnTransferIn == NULL)
{
FStream = new TSafeHandleStream((THandle)AFile);
}
OperationProgress = AOperationProgress;
FHandle = AHandle;
FOnTransferIn = OnTransferIn;
FTransferred = ATransferred;
FConvertParams = ConvertParams;
return TSFTPAsynchronousQueue::Init();
}
void __fastcall DisposeSafeWithErrorHandling()
{
DisposeSafe(SSH_FXP_STATUS);
}
protected:
virtual bool __fastcall InitRequest(TSFTPQueuePacket * Request)
{
TTerminal * FTerminal = FFileSystem->FTerminal;
// Buffer for one block of data
TFileBuffer BlockBuf;
unsigned long BlockSize = GetBlockSize();
bool Result = (BlockSize > 0);
if (Result)
{
bool Last;
if (FOnTransferIn != NULL)
{
size_t Read = BlockBuf.LoadFromIn(FOnTransferIn, FTerminal, BlockSize);
Last = (Read < BlockSize);
}
else
{
FILE_OPERATION_LOOP_BEGIN
{
BlockBuf.LoadStream(FStream, BlockSize, false);
}
FILE_OPERATION_LOOP_END(FMTLOAD(READ_ERROR, (FFileName)));
Last = (FStream->Position >= FStream->Size);
}
FEnd = (BlockBuf.Size == 0);
Result = !FEnd;
if (Result)
{
OperationProgress->AddLocallyUsed(BlockBuf.Size);
// We do ASCII transfer: convert EOL of current block
if (OperationProgress->AsciiTransfer)
{
__int64 PrevBufSize = BlockBuf.Size;
BlockBuf.Convert(FTerminal->Configuration->LocalEOLType,
FFileSystem->GetEOL(), FConvertParams, FConvertToken);
// update transfer size with difference arised from EOL conversion
OperationProgress->ChangeTransferSize(OperationProgress->TransferSize -
PrevBufSize + BlockBuf.Size);
}
if (FFileSystem->FTerminal->Configuration->ActualLogProtocol >= 1)
{
FFileSystem->FTerminal->LogEvent(FORMAT(L"Write request offset: %d, len: %d",
(int(FTransferred), int(BlockBuf.Size))));
}
if (FEncryption != NULL)
{
FEncryption->Encrypt(BlockBuf, Last);
}
Request->ChangeType(SSH_FXP_WRITE);
Request->AddString(FHandle);
Request->AddInt64(FTransferred);
Request->AddData(BlockBuf.Data, BlockBuf.Size);
FLastBlockSize = BlockBuf.Size;
FTransferred += BlockBuf.Size;
}
}
return Result;
}
virtual void __fastcall SendPacket(TSFTPQueuePacket * Packet)
{
TSFTPAsynchronousQueue::SendPacket(Packet);
OperationProgress->AddTransferred(FLastBlockSize);
}
virtual void __fastcall ReceiveResponse(
const TSFTPPacket * Packet, TSFTPPacket * Response, int ExpectedType,
int AllowStatus, bool TryOnly)
{
TSFTPAsynchronousQueue::ReceiveResponse(Packet, Response, ExpectedType, AllowStatus, TryOnly);
if (Response->Capacity > 0)
{
// particularly when uploading a file that completelly fits into send buffer
// over slow line, we may end up seemingly completing the transfer immediatelly
// but hanging the application for a long time waiting for responses
// (common is that the progress window would not even manage to draw itself,
// showing that upload finished, before the application "hangs")
FFileSystem->Progress(OperationProgress);
}
else
{
DebugAssert(TryOnly);
}
}
virtual bool __fastcall ReceivePacketAsynchronously()
{
// do not read response to close request
bool Result = (FRequests->Count > 0);
if (Result)
{
// Try only: We cannot read from the socket here as we are already called
// from TSecureShell::HandleNetworkEvents as it would cause a recursion
// that would potentially make PuTTY code process the SSH packets in wrong order.
ReceivePacket(NULL, SSH_FXP_STATUS, -1, NULL, true);
}
return Result;
}
inline int __fastcall GetBlockSize()
{
return FFileSystem->UploadBlockSize(FHandle, OperationProgress);
}
virtual bool __fastcall End(TSFTPPacket * /*Response*/)
{
return FEnd;
}
private:
TStream * FStream;
TTransferInEvent FOnTransferIn;
TFileOperationProgressType * OperationProgress;
UnicodeString FFileName;
unsigned long FLastBlockSize;
bool FEnd;
__int64 FTransferred;
RawByteString FHandle;
bool FConvertToken;
int FConvertParams;
TEncryption * FEncryption;
};
//---------------------------------------------------------------------------
class TSFTPLoadFilesPropertiesQueue : public TSFTPFixedLenQueue
{
public:
TSFTPLoadFilesPropertiesQueue(TSFTPFileSystem * AFileSystem) :
TSFTPFixedLenQueue(AFileSystem)
{
FIndex = 0;
}
virtual __fastcall ~TSFTPLoadFilesPropertiesQueue(){}
bool __fastcall Init(int QueueLen, TStrings * FileList)
{
FFileList = FileList;
return TSFTPFixedLenQueue::Init(QueueLen);
}
bool __fastcall ReceivePacket(TSFTPPacket * Packet, TRemoteFile *& File)
{
void * Token;
bool Result = TSFTPFixedLenQueue::ReceivePacket(Packet, SSH_FXP_ATTRS, asAll, &Token);
File = static_cast<TRemoteFile *>(Token);
return Result;
}
protected:
virtual bool __fastcall InitRequest(TSFTPQueuePacket * Request)
{
bool Result = false;
while (!Result && (FIndex < FFileList->Count))
{
TRemoteFile * File = static_cast<TRemoteFile *>(FFileList->Objects[FIndex]);
FIndex++;
bool MissingRights =
(FFileSystem->FSupport->Loaded &&
FLAGSET(FFileSystem->FSupport->AttributeMask, SSH_FILEXFER_ATTR_PERMISSIONS) &&
File->Rights->Unknown);
bool MissingOwnerGroup =
(FFileSystem->FSecureShell->SshImplementation == sshiBitvise) ||
(FFileSystem->FSupport->Loaded &&
FLAGSET(FFileSystem->FSupport->AttributeMask, SSH_FILEXFER_ATTR_OWNERGROUP) &&
!File->Owner.IsSet || !File->Group.IsSet);
Result = (MissingRights || MissingOwnerGroup);
if (Result)
{
Request->ChangeType(SSH_FXP_LSTAT);
FFileSystem->AddPathString(*Request, FFileSystem->LocalCanonify(File->FileName));
if (FFileSystem->FVersion >= 4)
{
Request->AddCardinal(
FLAGMASK(MissingRights, SSH_FILEXFER_ATTR_PERMISSIONS) |
FLAGMASK(MissingOwnerGroup, SSH_FILEXFER_ATTR_OWNERGROUP));
}
Request->Token = File;
}
}
return Result;
}
virtual bool __fastcall SendRequest()
{
bool Result =
(FIndex < FFileList->Count) &&
TSFTPFixedLenQueue::SendRequest();
return Result;
}
virtual bool __fastcall End(TSFTPPacket * /*Response*/)
{
return (FRequests->Count == 0);
}
private:
TStrings * FFileList;
int FIndex;
};
//---------------------------------------------------------------------------
class TSFTPCalculateFilesChecksumQueue : public TSFTPFixedLenQueue
{
public:
TSFTPCalculateFilesChecksumQueue(TSFTPFileSystem * AFileSystem) :
TSFTPFixedLenQueue(AFileSystem)
{
FIndex = 0;
}
virtual __fastcall ~TSFTPCalculateFilesChecksumQueue(){}
bool __fastcall Init(int QueueLen, const UnicodeString & Alg, TStrings * FileList)
{
FAlg = Alg;
FFileList = FileList;
return TSFTPFixedLenQueue::Init(QueueLen);
}
bool __fastcall ReceivePacket(TSFTPPacket * Packet, TRemoteFile *& File)
{
void * Token;
bool Result;
try
{
Result = TSFTPFixedLenQueue::ReceivePacket(Packet, SSH_FXP_EXTENDED_REPLY, asNo, &Token);
}
__finally
{
File = static_cast<TRemoteFile *>(Token);
}
return Result;
}
protected:
virtual bool __fastcall InitRequest(TSFTPQueuePacket * Request)
{
bool Result = false;
while (!Result && (FIndex < FFileList->Count))
{
TRemoteFile * File = static_cast<TRemoteFile *>(FFileList->Objects[FIndex]);
DebugAssert(File != NULL);
FIndex++;
Result = !File->IsDirectory;
if (Result)
{
DebugAssert(IsRealFile(File->FileName));
Request->ChangeType(SSH_FXP_EXTENDED);
Request->AddString(SFTP_EXT_CHECK_FILE_NAME);
FFileSystem->AddPathString(*Request, FFileSystem->LocalCanonify(File->FullFileName));
Request->AddString(FAlg);
Request->AddInt64(0); // offset
Request->AddInt64(0); // length (0 = till end)
Request->AddCardinal(0); // block size (0 = no blocks or "one block")
Request->Token = File;
}
}
return Result;
}
virtual bool __fastcall SendRequest()
{
bool Result =
(FIndex < FFileList->Count) &&
TSFTPFixedLenQueue::SendRequest();
return Result;
}
virtual bool __fastcall End(TSFTPPacket * /*Response*/)
{
return (FRequests->Count == 0);
}
private:
UnicodeString FAlg;
TStrings * FFileList;
int FIndex;
};
//---------------------------------------------------------------------------
#pragma warn .inl
//---------------------------------------------------------------------------
class TSFTPBusy
{
public:
__fastcall TSFTPBusy(TSFTPFileSystem * FileSystem)
{
FFileSystem = FileSystem;
DebugAssert(FFileSystem != NULL);
FFileSystem->BusyStart();
}
__fastcall ~TSFTPBusy()
{
FFileSystem->BusyEnd();
}
private:
TSFTPFileSystem * FFileSystem;
};
//===========================================================================
struct TOpenRemoteFileParams
{
UnicodeString FileName;
UnicodeString RemoteFileName;
TFileOperationProgressType * OperationProgress;
const TCopyParamType * CopyParam;
int Params;
bool Resume;
bool Resuming;
TSFTPOverwriteMode OverwriteMode;
__int64 DestFileSize; // output
RawByteString RemoteFileHandle; // output
TOverwriteFileParams * FileParams;
bool Confirmed;
bool DontRecycle;
};
//===========================================================================
__fastcall TSFTPFileSystem::TSFTPFileSystem(TTerminal * ATerminal,
TSecureShell * SecureShell):
TCustomFileSystem(ATerminal)
{
FSecureShell = SecureShell;
FPacketReservations = new TList();
FPacketNumbers = VarArrayCreate(OPENARRAY(int, (0, 1)), varLongWord);
FPreviousLoggedPacket = 0;
FNotLoggedPackets = 0;
FBusy = 0;
FAvoidBusy = false;
FUtfStrings = asOff;
FUtfDisablingAnnounced = true;
FSignedTS = false;
FSupport = new TSFTPSupport();
FExtensions = new TStringList();
FFixedPaths = NULL;
FFileSystemInfoValid = false;
FChecksumAlgs.reset(new TStringList());
FChecksumSftpAlgs.reset(new TStringList());
// List as defined by draft-ietf-secsh-filexfer-extensions-00
// MD5 moved to the back
RegisterChecksumAlg(Sha1ChecksumAlg, L"sha1");
RegisterChecksumAlg(Sha224ChecksumAlg, L"sha224");
RegisterChecksumAlg(Sha256ChecksumAlg, L"sha256");
RegisterChecksumAlg(Sha384ChecksumAlg, L"sha384");
RegisterChecksumAlg(Sha512ChecksumAlg, L"sha512");
RegisterChecksumAlg(Md5ChecksumAlg, L"md5");
RegisterChecksumAlg(Crc32ChecksumAlg, L"crc32");
}
//---------------------------------------------------------------------------
__fastcall TSFTPFileSystem::~TSFTPFileSystem()
{
delete FSupport;
ResetConnection();
delete FPacketReservations;
delete FExtensions;
delete FFixedPaths;
delete FSecureShell;
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::Open()
{
// this is used for reconnects only
FSecureShell->Open();
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::Close()
{
FSecureShell->Close();
}
//---------------------------------------------------------------------------
bool __fastcall TSFTPFileSystem::GetActive()
{
return FSecureShell->Active;
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::CollectUsage()
{
FSecureShell->CollectUsage();
UnicodeString VersionCounter;
switch (FVersion)
{
case 0:
VersionCounter = L"OpenedSessionsSFTP0";
break;
case 1:
VersionCounter = L"OpenedSessionsSFTP1";
break;
case 2:
VersionCounter = L"OpenedSessionsSFTP2";
break;
case 3:
VersionCounter = L"OpenedSessionsSFTP3";
break;
case 4:
VersionCounter = L"OpenedSessionsSFTP4";
break;
case 5:
VersionCounter = L"OpenedSessionsSFTP5";
break;
case 6:
VersionCounter = L"OpenedSessionsSFTP6";
break;
default:
DebugFail();
}
FTerminal->Configuration->Usage->Inc(VersionCounter);
}
//---------------------------------------------------------------------------
const TSessionInfo & __fastcall TSFTPFileSystem::GetSessionInfo()
{
return FSecureShell->GetSessionInfo();
}
//---------------------------------------------------------------------------
const TFileSystemInfo & __fastcall TSFTPFileSystem::GetFileSystemInfo(bool /*Retrieve*/)
{
if (!FFileSystemInfoValid)
{
FFileSystemInfo.AdditionalInfo = L"";
if (!IsCapable(fcRename))
{
FFileSystemInfo.AdditionalInfo += LoadStr(FS_RENAME_NOT_SUPPORTED) + L"\r\n\r\n";
}
if (FExtensions->Count > 0)
{
UnicodeString Name;
UnicodeString Value;
UnicodeString Line;
FFileSystemInfo.AdditionalInfo += LoadStr(SFTP_EXTENSION_INFO) + L"\r\n";
for (int Index = 0; Index < FExtensions->Count; Index++)
{
UnicodeString Name = FExtensions->Names[Index];
UnicodeString Value = FExtensions->Values[Name];
UnicodeString Line;
if (Value.IsEmpty())
{
Line = Name;
}
else
{
Line = FORMAT(L"%s=%s", (Name, Value));
}
FFileSystemInfo.AdditionalInfo += FORMAT(L" %s\r\n", (Line));
}
}
else
{
FFileSystemInfo.AdditionalInfo += LoadStr(SFTP_NO_EXTENSION_INFO) + L"\r\n";
}
FFileSystemInfo.ProtocolBaseName = L"SFTP";
FFileSystemInfo.ProtocolName = FMTLOAD(SFTP_PROTOCOL_NAME2, (FVersion));
FTerminal->SaveCapabilities(FFileSystemInfo);
FFileSystemInfoValid = true;
}
return FFileSystemInfo;
}
//---------------------------------------------------------------------------
bool __fastcall TSFTPFileSystem::TemporaryTransferFile(const UnicodeString & FileName)
{
return SameText(UnixExtractFileExt(FileName), PARTIAL_EXT);
}
//---------------------------------------------------------------------------
bool __fastcall TSFTPFileSystem::GetStoredCredentialsTried()
{
return FSecureShell->GetStoredCredentialsTried();
}
//---------------------------------------------------------------------------
UnicodeString __fastcall TSFTPFileSystem::GetUserName()
{
return FSecureShell->UserName;
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::Idle()
{
// Keep session alive
if ((FTerminal->SessionData->PingType != ptOff) &&
(Now() - FSecureShell->LastDataSent > FTerminal->SessionData->PingIntervalDT))
{
if ((FTerminal->SessionData->PingType == ptDummyCommand) &&
FSecureShell->Ready)
{
FTerminal->LogEvent(L"Sending dummy command to keep session alive.");
TSFTPPacket Packet(SSH_FXP_REALPATH);
AddPathString(Packet, L"/");
SendPacketAndReceiveResponse(&Packet, &Packet);
}
else
{
FSecureShell->KeepAlive();
}
}
FSecureShell->Idle();
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::ResetConnection()
{
// there must be no valid packet reservation at the end
for (int i = 0; i < FPacketReservations->Count; i++)
{
DebugAssert(FPacketReservations->Items[i] == NULL);
delete (TSFTPPacket *)FPacketReservations->Items[i];
}
FPacketReservations->Clear();
FPacketNumbers.Clear();
}
//---------------------------------------------------------------------------
bool __fastcall TSFTPFileSystem::IsCapable(int Capability) const
{
DebugAssert(FTerminal);
switch (Capability) {
case fcAnyCommand:
case fcShellAnyCommand:
case fcLocking:
return false;
case fcNewerOnlyUpload:
case fcTimestampChanging:
case fcIgnorePermErrors:
case fcPreservingTimestampUpload:
case fcSecondaryShell:
case fcRemoveCtrlZUpload:
case fcRemoveBOMUpload:
case fcPreservingTimestampDirs:
case fcTransferOut:
case fcTransferIn:
return true;
case fcMoveToQueue:
case fcResumeSupport:
case fcSkipTransfer:
case fcParallelTransfers:
return !FTerminal->IsEncryptingFiles();
case fcRename:
case fcRemoteMove:
return (FVersion >= 2);
case fcSymbolicLink:
case fcResolveSymlink:
return (FVersion >= 3) && !FTerminal->IsEncryptingFiles();
case fcModeChanging:
case fcModeChangingUpload:
return !FSupport->Loaded ||
FLAGSET(FSupport->AttributeMask, SSH_FILEXFER_ATTR_PERMISSIONS);
case fcGroupOwnerChangingByID:
return (FVersion <= 3);
case fcOwnerChanging:
case fcGroupChanging:
return
(FVersion <= 3) ||
((FVersion >= 4) &&
(!FSupport->Loaded ||
FLAGSET(FSupport->AttributeMask, SSH_FILEXFER_ATTR_OWNERGROUP)));
case fcNativeTextMode:
return !FTerminal->IsEncryptingFiles() && (FVersion >= 4);
case fcTextMode:
return
!FTerminal->IsEncryptingFiles() &&
((FVersion >= 4) ||
(strcmp(GetEOL(), EOLToStr(FTerminal->Configuration->LocalEOLType)) != 0));
case fcUserGroupListing:
return SupportsExtension(SFTP_EXT_OWNER_GROUP);
case fcLoadingAdditionalProperties:
// We allow loading properties only, if "supported" extension is supported and
// the server supports "permissions" and/or "owner/group" attributes
// (no other attributes are loaded).
// This is here only because of VShell
// (it supports owner/group, but does not include them into response to
// SSH_FXP_READDIR)
// and Bitwise (the same as VShell, but it does not even bother to provide "supported" extension until 6.21)
// No other use is known.
return
(FSupport->Loaded &&
((FSupport->AttributeMask &
(SSH_FILEXFER_ATTR_PERMISSIONS | SSH_FILEXFER_ATTR_OWNERGROUP)) != 0)) ||
(FSecureShell->SshImplementation == sshiBitvise);
case fcCheckingSpaceAvailable:
return
// extension announced in extension list of by
// SFTP_EXT_SUPPORTED/SFTP_EXT_SUPPORTED2 extension
// (SFTP version 5 and newer only)
SupportsExtension(SFTP_EXT_SPACE_AVAILABLE) ||
// extension announced by proprietary SFTP_EXT_STATVFS extension
FSupportsStatVfsV2 ||
// Bitwise (until 6.21) fails to report it's supported extensions.
(FSecureShell->SshImplementation == sshiBitvise);
case fcCalculatingChecksum:
return
!FTerminal->IsEncryptingFiles() &&
(// Specification says that "check-file" should be announced,
// yet Vandyke VShell (as of 4.0.3) announce "check-file-name"
// https://forums.vandyke.com/showthread.php?t=11597
SupportsExtension(SFTP_EXT_CHECK_FILE) ||
SupportsExtension(SFTP_EXT_CHECK_FILE_NAME) ||
// see above
(FSecureShell->SshImplementation == sshiBitvise));
case fcRemoteCopy:
return
SupportsExtension(SFTP_EXT_COPY_FILE) ||
// see above
(FSecureShell->SshImplementation == sshiBitvise);
case fcHardLink:
return
(FVersion >= 6) ||
FSupportsHardlink;
case fcChangePassword:
return FSecureShell->CanChangePassword();
default:
DebugFail();
return false;
}
}
//---------------------------------------------------------------------------
bool __fastcall TSFTPFileSystem::SupportsExtension(const UnicodeString & Extension) const
{
return FSupport->Loaded && (FSupport->Extensions->IndexOf(Extension) >= 0);
}
//---------------------------------------------------------------------------
inline void __fastcall TSFTPFileSystem::BusyStart()
{
if (FBusy == 0 && FTerminal->UseBusyCursor && !FAvoidBusy)
{
FBusyToken = ::BusyStart();
}
FBusy++;
DebugAssert(FBusy < 10);
}
//---------------------------------------------------------------------------
inline void __fastcall TSFTPFileSystem::BusyEnd()
{
DebugAssert(FBusy > 0);
FBusy--;
if (FBusy == 0 && FTerminal->UseBusyCursor && !FAvoidBusy)
{
::BusyEnd(FBusyToken);
FBusyToken = NULL;
}
}
//---------------------------------------------------------------------------
unsigned long __fastcall TSFTPFileSystem::TransferBlockSize(unsigned long Overhead,
TFileOperationProgressType * OperationProgress, unsigned long MaxPacketSize)
{
const unsigned long MinPacketSize = 32768;
// size + message number + type
const unsigned long SFTPPacketOverhead = 4 + 4 + 1;
unsigned long AMaxPacketSize = FSecureShell->MaxPacketSize();
bool MaxPacketSizeValid = (AMaxPacketSize > 0);
unsigned long CPSRounded = TEncryption::RoundToBlock(OperationProgress->CPS());
unsigned long Result = CPSRounded;
if ((MaxPacketSize > 0) &&
((MaxPacketSize < AMaxPacketSize) || !MaxPacketSizeValid))
{
AMaxPacketSize = MaxPacketSize;
MaxPacketSizeValid = true;
}
if ((FMaxPacketSize > 0) &&
((FMaxPacketSize < AMaxPacketSize) || !MaxPacketSizeValid))
{
AMaxPacketSize = FMaxPacketSize;
MaxPacketSizeValid = true;
}
if (Result == 0)
{
Result = OperationProgress->StaticBlockSize();
}
if (Result < MinPacketSize)
{
Result = MinPacketSize;
}
if (MaxPacketSizeValid)
{
Overhead += SFTPPacketOverhead;
if (AMaxPacketSize < Overhead)
{
// do not send another request
// (generally should happen only if upload buffer is full)
Result = 0;
}
else
{
AMaxPacketSize -= Overhead;
if (Result > AMaxPacketSize)
{
unsigned int MaxPacketSizeRounded = TEncryption::RoundToBlockDown(AMaxPacketSize);
if (MaxPacketSizeRounded > 0)
{
AMaxPacketSize = MaxPacketSizeRounded;
}
Result = AMaxPacketSize;
}
}
}
Result = OperationProgress->AdjustToCPSLimit(Result);
return Result;
}
//---------------------------------------------------------------------------
unsigned long __fastcall TSFTPFileSystem::UploadBlockSize(const RawByteString & Handle,
TFileOperationProgressType * OperationProgress)
{
// handle length + offset + data size
const unsigned long UploadPacketOverhead =
sizeof(unsigned long) + sizeof(__int64) + sizeof(unsigned long);
return TransferBlockSize(UploadPacketOverhead + Handle.Length(), OperationProgress);
}
//---------------------------------------------------------------------------
unsigned long __fastcall TSFTPFileSystem::DownloadBlockSize(
TFileOperationProgressType * OperationProgress)
{
unsigned long Result = TransferBlockSize(sizeof(unsigned long), OperationProgress);
if (FSupport->Loaded && (FSupport->MaxReadSize > 0) &&
(Result > FSupport->MaxReadSize))
{
Result = FSupport->MaxReadSize;
}
return Result;
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::Progress(TFileOperationProgressType * OperationProgress)
{
FTerminal->Progress(OperationProgress);
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::SendPacket(const TSFTPPacket * Packet)
{
// putting here for a lack of better place
if (!FUtfDisablingAnnounced && (FUtfStrings == asOff))
{
FTerminal->LogEvent(L"Strings received in non-UTF-8 encoding in a previous packet, will not use UTF-8 anymore");
FUtfDisablingAnnounced = true;
}
BusyStart();
try
{
if (FTerminal->Log->Logging && (FTerminal->Configuration->ActualLogProtocol >= 0))
{
if ((FPreviousLoggedPacket != SSH_FXP_READ &&
FPreviousLoggedPacket != SSH_FXP_WRITE) ||
(Packet->Type != FPreviousLoggedPacket) ||
(FTerminal->Configuration->ActualLogProtocol >= 1))
{
if (FNotLoggedPackets)
{
FTerminal->LogEvent(FORMAT(L"%d skipped SSH_FXP_WRITE, SSH_FXP_READ, SSH_FXP_DATA and SSH_FXP_STATUS packets.",
(FNotLoggedPackets)));
FNotLoggedPackets = 0;
}
FTerminal->Log->Add(llInput, FORMAT(L"Type: %s, Size: %d, Number: %d",
(Packet->TypeName, (int)Packet->Length, (int)Packet->MessageNumber)));
if (FTerminal->Configuration->ActualLogProtocol >= 2)
{
FTerminal->Log->Add(llInput, Packet->Dump());
}
FPreviousLoggedPacket = Packet->Type;
}
else
{
FNotLoggedPackets++;
}
}
FSecureShell->Send(Packet->SendData, Packet->SendLength);
}
__finally
{
BusyEnd();
}
}
//---------------------------------------------------------------------------
unsigned long __fastcall TSFTPFileSystem::GotStatusPacket(TSFTPPacket * Packet,
int AllowStatus)
{
unsigned long Code = Packet->GetCardinal();
static int Messages[] = {
SFTP_STATUS_OK,
SFTP_STATUS_EOF,
SFTP_STATUS_NO_SUCH_FILE,
SFTP_STATUS_PERMISSION_DENIED,
SFTP_STATUS_FAILURE,
SFTP_STATUS_BAD_MESSAGE,
SFTP_STATUS_NO_CONNECTION,
SFTP_STATUS_CONNECTION_LOST,
SFTP_STATUS_OP_UNSUPPORTED,
SFTP_STATUS_INVALID_HANDLE,
SFTP_STATUS_NO_SUCH_PATH,
SFTP_STATUS_FILE_ALREADY_EXISTS,
SFTP_STATUS_WRITE_PROTECT,
SFTP_STATUS_NO_MEDIA,
SFTP_STATUS_NO_SPACE_ON_FILESYSTEM,
SFTP_STATUS_QUOTA_EXCEEDED,
SFTP_STATUS_UNKNOWN_PRINCIPAL,
SFTP_STATUS_LOCK_CONFLICT,
SFTP_STATUS_DIR_NOT_EMPTY,
SFTP_STATUS_NOT_A_DIRECTORY,
SFTP_STATUS_INVALID_FILENAME,
SFTP_STATUS_LINK_LOOP,
SFTP_STATUS_CANNOT_DELETE,
SFTP_STATUS_INVALID_PARAMETER,
SFTP_STATUS_FILE_IS_A_DIRECTORY,
SFTP_STATUS_BYTE_RANGE_LOCK_CONFLICT,
SFTP_STATUS_BYTE_RANGE_LOCK_REFUSED,
SFTP_STATUS_DELETE_PENDING,
SFTP_STATUS_FILE_CORRUPT,
SFTP_STATUS_OWNER_INVALID,
SFTP_STATUS_GROUP_INVALID,
SFTP_STATUS_NO_MATCHING_BYTE_RANGE_LOCK
};
int Message;
if ((AllowStatus & (0x01 << Code)) == 0)
{
if (Code >= LENOF(Messages))
{
Message = SFTP_STATUS_UNKNOWN;
}
else
{
Message = Messages[Code];
}
UnicodeString MessageStr = LoadStr(Message);
UnicodeString ServerMessage;
UnicodeString LanguageTag;
if ((FVersion >= 3) ||
// if version is not decided yet (i.e. this is status response
// to the init request), go on, only if there are any more data
((FVersion < 0) && (Packet->RemainingLength > 0)))
{
// message is in UTF only since SFTP specification 01 (specification 00
// is also version 3)
// (in other words, always use UTF unless server is known to be buggy)
ServerMessage = Packet->GetString(FUtfStrings);
// SSH-2.0-Maverick_SSHD and SSH-2.0-CIGNA SFTP Server Ready! omit the language tag
// and I believe I've seen one more server doing the same.
if (Packet->RemainingLength > 0)
{
LanguageTag = Packet->GetAnsiString();
if ((FVersion >= 5) && (Message == SFTP_STATUS_UNKNOWN_PRINCIPAL))
{
UnicodeString Principals;
while (Packet->GetNextData() != NULL)
{
if (!Principals.IsEmpty())
{
Principals += L", ";
}
Principals += Packet->GetAnsiString();
}
MessageStr = FORMAT(MessageStr, (Principals));
}
}
}
else
{
ServerMessage = LoadStr(SFTP_SERVER_MESSAGE_UNSUPPORTED);
}
if (FTerminal->Log->Logging &&
(FTerminal->Configuration->ActualLogProtocol >= 0))
{
FTerminal->Log->Add(llOutput, FORMAT(L"Status code: %d, Message: %d, Server: %s, Language: %s ",
(int(Code), (int)Packet->MessageNumber, ServerMessage, LanguageTag)));
}
if (!LanguageTag.IsEmpty())
{
LanguageTag = FORMAT(L" (%s)", (LanguageTag));
}
UnicodeString HelpKeyword;
switch (Code)
{
case SSH_FX_FAILURE:
HelpKeyword = HELP_SFTP_STATUS_FAILURE;
break;
case SSH_FX_PERMISSION_DENIED:
HelpKeyword = HELP_SFTP_STATUS_PERMISSION_DENIED;
break;
}
UnicodeString Error = FMTLOAD(SFTP_ERROR_FORMAT3, (MessageStr,
int(Code), LanguageTag, ServerMessage));
if (Code == SSH_FX_FAILURE)
{
FTerminal->Configuration->Usage->Inc(L"SftpFailureErrors");
Error += L"\n\n" + LoadStr(SFTP_STATUS_4);
}
FTerminal->TerminalError(NULL, Error, HelpKeyword);
return 0;
}
else
{
if (!FNotLoggedPackets || Code)
{
if (FTerminal->Configuration->ActualLogProtocol >= 0)
{
FTerminal->Log->Add(llOutput, FORMAT(L"Status code: %d", ((int)Code)));
}
}
return Code;
}
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::RemoveReservation(int Reservation)
{
for (int Index = Reservation+1; Index < FPacketReservations->Count; Index++)
{
FPacketNumbers.PutElement(FPacketNumbers.GetElement(Index), Index-1);
}
TSFTPPacket * Packet = (TSFTPPacket *)FPacketReservations->Items[Reservation];
if (Packet)
{
DebugAssert(Packet->ReservedBy == this);
Packet->ReservedBy = NULL;
}
FPacketReservations->Delete(Reservation);
}
//---------------------------------------------------------------------------
inline int __fastcall TSFTPFileSystem::PacketLength(unsigned char * LenBuf, int ExpectedType)
{
int Length = GET_32BIT(LenBuf);
if (Length > SFTP_MAX_PACKET_LEN)
{
UnicodeString Message = FMTLOAD(SFTP_PACKET_TOO_BIG, (
int(Length), SFTP_MAX_PACKET_LEN));
if (ExpectedType == SSH_FXP_VERSION)
{
RawByteString LenString(reinterpret_cast<char *>(LenBuf), 4);
Message = FMTLOAD(SFTP_PACKET_TOO_BIG_INIT_EXPLAIN,
(Message, DisplayableStr(LenString)));
}
FTerminal->FatalError(NULL, Message, HELP_SFTP_PACKET_TOO_BIG);
}
return Length;
}
//---------------------------------------------------------------------------
bool __fastcall TSFTPFileSystem::PeekPacket()
{
bool Result;
unsigned char * Buf;
Result = FSecureShell->Peek(Buf, 4);
if (Result)
{
int Length = PacketLength(Buf, -1);
Result = FSecureShell->Peek(Buf, 4 + Length);
}
return Result;
}
//---------------------------------------------------------------------------
int __fastcall TSFTPFileSystem::ReceivePacket(TSFTPPacket * Packet,
int ExpectedType, int AllowStatus, bool TryOnly)
{
TSFTPBusy Busy(this);
int Result = SSH_FX_OK;
int Reservation = FPacketReservations->IndexOf(Packet);
if (Reservation < 0 || Packet->Capacity == 0)
{
bool IsReserved;
do
{
IsReserved = false;
DebugAssert(Packet);
if (TryOnly && !PeekPacket())
{
// Reset packet in case it was filled by previous out-of-order
// reserved packet
*Packet = TSFTPPacket();
}
else
{
unsigned char LenBuf[4];
FSecureShell->Receive(LenBuf, sizeof(LenBuf));
int Length = PacketLength(LenBuf, ExpectedType);
Packet->Capacity = Length;
FSecureShell->Receive(Packet->Data, Length);
Packet->DataUpdated(Length);
if (FTerminal->Log->Logging && (FTerminal->Configuration->ActualLogProtocol >= 0))
{
if ((FPreviousLoggedPacket != SSH_FXP_READ &&
FPreviousLoggedPacket != SSH_FXP_WRITE) ||
(Packet->Type != SSH_FXP_STATUS && Packet->Type != SSH_FXP_DATA) ||
(FTerminal->Configuration->ActualLogProtocol >= 1))
{
if (FNotLoggedPackets)
{
FTerminal->LogEvent(FORMAT(L"%d skipped SSH_FXP_WRITE, SSH_FXP_READ, SSH_FXP_DATA and SSH_FXP_STATUS packets.",
(FNotLoggedPackets)));
FNotLoggedPackets = 0;
}
FTerminal->Log->Add(llOutput, FORMAT(L"Type: %s, Size: %d, Number: %d",
(Packet->TypeName, (int)Packet->Length, (int)Packet->MessageNumber)));
if (FTerminal->Configuration->ActualLogProtocol >= 2)
{
FTerminal->Log->Add(llOutput, Packet->Dump());
}
}
else
{
FNotLoggedPackets++;
}
}
if (Reservation < 0 ||
Packet->MessageNumber != (unsigned int)FPacketNumbers.GetElement(Reservation))
{
TSFTPPacket * ReservedPacket;
unsigned int MessageNumber;
for (int Index = 0; Index < FPacketReservations->Count; Index++)
{
MessageNumber = (unsigned int)FPacketNumbers.GetElement(Index);
if (MessageNumber == Packet->MessageNumber)
{
ReservedPacket = (TSFTPPacket *)FPacketReservations->Items[Index];
IsReserved = true;
if (ReservedPacket)
{
FTerminal->LogEvent(0, L"Storing reserved response");
*ReservedPacket = *Packet;
}
else
{
FTerminal->LogEvent(0, L"Discarding reserved response");
RemoveReservation(Index);
if ((Reservation >= 0) && (Reservation > Index))
{
Reservation--;
DebugAssert(Reservation == FPacketReservations->IndexOf(Packet));
}
}
break;
}
}
}
}
}
while (IsReserved);
}
if ((Packet->Capacity == 0) && DebugAlwaysTrue(TryOnly))
{
// noop
}
else
{
// before we removed the reservation after check for packet type,
// but if it raises exception, removal is unnecessarily
// postponed until the packet is removed
// (and it have not worked anyway until recent fix to UnreserveResponse)
if (Reservation >= 0)
{
DebugAssert(Packet->MessageNumber == (unsigned int)FPacketNumbers.GetElement(Reservation));
RemoveReservation(Reservation);
}
if (ExpectedType >= 0)
{
if (Packet->Type == SSH_FXP_STATUS)
{
if (AllowStatus < 0)
{
AllowStatus = (ExpectedType == SSH_FXP_STATUS ? asOK : asNo);
}
Result = GotStatusPacket(Packet, AllowStatus);
}
else if (ExpectedType != Packet->Type)
{
FTerminal->FatalError(NULL, FMTLOAD(SFTP_INVALID_TYPE, ((int)Packet->Type)));
}
}
}
return Result;
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::ReserveResponse(const TSFTPPacket * Packet,
TSFTPPacket * Response)
{
if (Response != NULL)
{
DebugAssert(FPacketReservations->IndexOf(Response) < 0);
// mark response as not received yet
Response->Capacity = 0;
Response->ReservedBy = this;
}
FPacketReservations->Add(Response);
if (FPacketReservations->Count >= FPacketNumbers.ArrayHighBound())
{
FPacketNumbers.ArrayRedim(FPacketReservations->Count + 10);
}
FPacketNumbers.PutElement(Packet->MessageNumber, FPacketReservations->Count - 1);
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::UnreserveResponse(TSFTPPacket * Response)
{
int Reservation = FPacketReservations->IndexOf(Response);
if (Response->Capacity != 0)
{
// added check for already received packet
// (it happens when the reserved response is received out of order,
// unexpectedly soon, and then receivepacket() on the packet
// is not actually called, due to exception)
RemoveReservation(Reservation);
}
else
{
if (Reservation >= 0)
{
// we probably do not remove the item at all, because
// we must remember that the response was expected, so we skip it
// in receivepacket()
FPacketReservations->Items[Reservation] = NULL;
}
}
}
//---------------------------------------------------------------------------
int __fastcall TSFTPFileSystem::ReceiveResponse(
const TSFTPPacket * Packet, TSFTPPacket * Response, int ExpectedType,
int AllowStatus, bool TryOnly)
{
int Result;
unsigned int MessageNumber = Packet->MessageNumber;
TSFTPPacket * AResponse = (Response ? Response : new TSFTPPacket());
try
{
Result = ReceivePacket(AResponse, ExpectedType, AllowStatus, TryOnly);
if (MessageNumber != AResponse->MessageNumber)
{
FTerminal->FatalError(NULL, FMTLOAD(SFTP_MESSAGE_NUMBER,
((int)AResponse->MessageNumber, (int)MessageNumber)));
}
}
__finally
{
if (!Response)
{
delete AResponse;
}
}
return Result;
}
//---------------------------------------------------------------------------
int __fastcall TSFTPFileSystem::SendPacketAndReceiveResponse(
const TSFTPPacket * Packet, TSFTPPacket * Response, int ExpectedType,
int AllowStatus)
{
int Result;
TSFTPBusy Busy(this);
SendPacket(Packet);
Result = ReceiveResponse(Packet, Response, ExpectedType, AllowStatus);
return Result;
}
//---------------------------------------------------------------------------
UnicodeString __fastcall TSFTPFileSystem::RealPath(const UnicodeString Path)
{
try
{
FTerminal->LogEvent(0, FORMAT(L"Getting real path for '%s'", (Path)));
TSFTPPacket Packet(SSH_FXP_REALPATH);
AddPathString(Packet, Path);
// In SFTP-6 new optional field control-byte is added that defaults to
// SSH_FXP_REALPATH_NO_CHECK=0x01, meaning it won't fail, if the path does not exist.
// That differs from SFTP-5 recommendation that
// "The server SHOULD fail the request if the path is not present on the server."
// Earlier versions had no recommendation, though canonical SFTP-3 implementation
// in OpenSSH fails.
// While we really do not care much, we anyway set the flag to ~ & 0x01 to make the request fail.
// First for consistency.
// Second to workaround a bug in ProFTPD/mod_sftp version 1.3.5rc1 through 1.3.5-stable
// that sends a completelly malformed response for non-existing paths,
// when SSH_FXP_REALPATH_NO_CHECK (even implicitly) is used.
// See http://bugs.proftpd.org/show_bug.cgi?id=4160
// Note that earlier drafts of SFTP-6 (filexfer-07 and -08) had optional compose-path field
// before control-byte field. If we ever use this against a server conforming to those drafts,
// it may cause trouble.
if (FVersion >= 6)
{
if (FSecureShell->SshImplementation != sshiProFTPD)
{
Packet.AddByte(SSH_FXP_REALPATH_STAT_ALWAYS);
}
else
{
// Cannot use SSH_FXP_REALPATH_STAT_ALWAYS as ProFTPD does wrong bitwise test
// so it incorrectly evaluates SSH_FXP_REALPATH_STAT_ALWAYS (0x03) as
// SSH_FXP_REALPATH_NO_CHECK (0x01). The only value conforming to the
// specification, yet working with ProFTPD is SSH_FXP_REALPATH_STAT_IF (0x02).
Packet.AddByte(SSH_FXP_REALPATH_STAT_IF);
}
}
SendPacketAndReceiveResponse(&Packet, &Packet, SSH_FXP_NAME);
if (Packet.GetCardinal() != 1)
{
FTerminal->FatalError(NULL, LoadStr(SFTP_NON_ONE_FXP_NAME_PACKET));
}
UnicodeString RealDir = UnixExcludeTrailingBackslash(Packet.GetPathString(FUtfStrings));
RealDir = FTerminal->DecryptFileName(RealDir);
// ignore rest of SSH_FXP_NAME packet
FTerminal->LogEvent(0, FORMAT(L"Real path is '%s'", (RealDir)));
return RealDir;
}
catch(Exception & E)
{
if (FTerminal->Active)
{
throw ExtException(&E, FMTLOAD(SFTP_REALPATH_ERROR, (Path)));
}
else
{
throw;
}
}
}
//---------------------------------------------------------------------------
UnicodeString __fastcall TSFTPFileSystem::RealPath(const UnicodeString Path,
const UnicodeString BaseDir)
{
UnicodeString APath;
if (UnixIsAbsolutePath(Path))
{
APath = Path;
}
else
{
if (!Path.IsEmpty())
{
// this condition/block was outside (before) current block
// but it did not work when Path was empty
if (!BaseDir.IsEmpty())
{
APath = UnixIncludeTrailingBackslash(BaseDir);
}
APath = APath + Path;
}
if (APath.IsEmpty()) APath = UnixIncludeTrailingBackslash(L".");
}
return RealPath(APath);
}
//---------------------------------------------------------------------------
UnicodeString __fastcall TSFTPFileSystem::LocalCanonify(const UnicodeString & Path)
{
// TODO: improve (handle .. etc.)
if (UnixIsAbsolutePath(Path) ||
(!FCurrentDirectory.IsEmpty() && UnixSamePath(FCurrentDirectory, Path)))
{
return Path;
}
else
{
return ::AbsolutePath(FCurrentDirectory, Path);
}
}
//---------------------------------------------------------------------------
UnicodeString __fastcall TSFTPFileSystem::Canonify(UnicodeString Path)
{
// inspired by canonify() from PSFTP.C
UnicodeString Result;
FTerminal->LogEvent(FORMAT(L"Canonifying: \"%s\"", (Path)));
Path = LocalCanonify(Path);
bool TryParent = false;
try
{
Result = RealPath(Path);
}
catch(...)
{
if (FTerminal->Active)
{
TryParent = true;
}
else
{
throw;
}
}
if (TryParent)
{
UnicodeString APath = UnixExcludeTrailingBackslash(Path);
UnicodeString Name = UnixExtractFileName(APath);
if (!IsRealFile(Name))
{
Result = Path;
}
else
{
UnicodeString FPath = UnixExtractFilePath(APath);
try
{
Result = RealPath(FPath);
Result = UnixIncludeTrailingBackslash(Result) + Name;
}
catch(...)
{
if (FTerminal->Active)
{
Result = Path;
}
else
{
throw;
}
}
}
}
FTerminal->LogEvent(FORMAT(L"Canonified: \"%s\"", (Result)));
return Result;
}
//---------------------------------------------------------------------------
UnicodeString __fastcall TSFTPFileSystem::AbsolutePath(UnicodeString Path, bool Local)
{
if (Local)
{
return LocalCanonify(Path);
}
else
{
return RealPath(Path, CurrentDirectory);
}
}
//---------------------------------------------------------------------------
UnicodeString __fastcall TSFTPFileSystem::GetHomeDirectory()
{
if (FHomeDirectory.IsEmpty())
{
FHomeDirectory = RealPath(L".");
}
return FHomeDirectory;
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::LoadFile(TRemoteFile * File, TSFTPPacket * Packet,
bool Complete)
{
Packet->GetFile(File, FVersion, FTerminal->SessionData->DSTMode, FUtfStrings, FSignedTS, Complete);
}
//---------------------------------------------------------------------------
TRemoteFile * __fastcall TSFTPFileSystem::LoadFile(TSFTPPacket * Packet,
TRemoteFile * ALinkedByFile, const UnicodeString FileName,
TRemoteFileList * TempFileList, bool Complete)
{
TRemoteFile * File = new TRemoteFile(ALinkedByFile);
try
{
File->Terminal = FTerminal;
if (!FileName.IsEmpty())
{
File->FileName = FileName;
}
// to get full path for symlink completion
File->Directory = TempFileList;
LoadFile(File, Packet, Complete);
File->Directory = NULL;
}
catch(...)
{
delete File;
throw;
}
return File;
}
//---------------------------------------------------------------------------
UnicodeString __fastcall TSFTPFileSystem::GetCurrentDirectory()
{
return FCurrentDirectory;
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::DoStartup()
{
// do not know yet
FVersion = -1;
FFileSystemInfoValid = false;
TSFTPPacket Packet(SSH_FXP_INIT);
int MaxVersion = FTerminal->SessionData->SFTPMaxVersion;
if (MaxVersion > SFTPMaxVersion)
{
MaxVersion = SFTPMaxVersion;
}
Packet.AddCardinal(MaxVersion);
try
{
SendPacketAndReceiveResponse(&Packet, &Packet, SSH_FXP_VERSION);
}
catch(Exception &E)
{
FTerminal->FatalError(&E, LoadStr(SFTP_INITIALIZE_ERROR), HELP_SFTP_INITIALIZE_ERROR);
}
FVersion = Packet.GetCardinal();
FTerminal->LogEvent(FORMAT(L"SFTP version %d negotiated.", (FVersion)));
if (FVersion < SFTPMinVersion || FVersion > SFTPMaxVersion)
{
FTerminal->FatalError(NULL, FMTLOAD(SFTP_VERSION_NOT_SUPPORTED,
(FVersion, SFTPMinVersion, SFTPMaxVersion)));
}
FExtensions->Clear();
FEOL = "\r\n";
FSupport->Loaded = false;
FSupportsStatVfsV2 = false;
FSupportsHardlink = false;
SAFE_DESTROY(FFixedPaths);
if (FVersion >= 3)
{
while (Packet.GetNextData() != NULL)
{
UnicodeString ExtensionName = Packet.GetAnsiString();
RawByteString ExtensionData = Packet.GetRawByteString();
UnicodeString ExtensionDisplayData = DisplayableStr(ExtensionData);
if (ExtensionName == SFTP_EXT_NEWLINE)
{
FEOL = AnsiString(ExtensionData);
FTerminal->LogEvent(FORMAT(L"Server requests EOL sequence %s.",
(ExtensionDisplayData)));
if (FEOL.Length() < 1 || FEOL.Length() > 2)
{
FTerminal->FatalError(NULL, FMTLOAD(SFTP_INVALID_EOL, (ExtensionDisplayData)));
}
}
// do not allow "supported" to override "supported2" if both are received
else if (((ExtensionName == SFTP_EXT_SUPPORTED) && !FSupport->Loaded) ||
(ExtensionName == SFTP_EXT_SUPPORTED2))
{
FSupport->Reset();
TSFTPPacket SupportedStruct(ExtensionData);
FSupport->Loaded = true;
FSupport->AttributeMask = SupportedStruct.GetCardinal();
FSupport->AttributeBits = SupportedStruct.GetCardinal();
FSupport->OpenFlags = SupportedStruct.GetCardinal();
FSupport->AccessMask = SupportedStruct.GetCardinal();
FSupport->MaxReadSize = SupportedStruct.GetCardinal();
if (ExtensionName == SFTP_EXT_SUPPORTED)
{
while (SupportedStruct.GetNextData() != NULL)
{
FSupport->Extensions->Add(SupportedStruct.GetAnsiString());
}
}
else
{
// note that supported-open-block-vector, supported-block-vector,
// attrib-extension-count and attrib-extension-names fields
// were added only in rev 08, while "supported2" was defined in rev 07
FSupport->OpenBlockVector = SupportedStruct.GetSmallCardinal();
FSupport->BlockVector = SupportedStruct.GetSmallCardinal();
unsigned int ExtensionCount;
ExtensionCount = SupportedStruct.GetCardinal();
for (unsigned int i = 0; i < ExtensionCount; i++)
{
FSupport->AttribExtensions->Add(SupportedStruct.GetAnsiString());
}
ExtensionCount = SupportedStruct.GetCardinal();
for (unsigned int i = 0; i < ExtensionCount; i++)
{
FSupport->Extensions->Add(SupportedStruct.GetAnsiString());
}
}
if (FTerminal->Log->Logging)
{
FTerminal->LogEvent(FORMAT(
L"Server support information (%s):\n"
" Attribute mask: %x, Attribute bits: %x, Open flags: %x\n"
" Access mask: %x, Open block vector: %x, Block vector: %x, Max read size: %d\n",
(ExtensionName,
int(FSupport->AttributeMask),
int(FSupport->AttributeBits),
int(FSupport->OpenFlags),
int(FSupport->AccessMask),
int(FSupport->OpenBlockVector),
int(FSupport->BlockVector),
int(FSupport->MaxReadSize))));
FTerminal->LogEvent(FORMAT(L" Attribute extensions (%d)\n", (FSupport->AttribExtensions->Count)));
for (int Index = 0; Index < FSupport->AttribExtensions->Count; Index++)
{
FTerminal->LogEvent(
FORMAT(L" %s", (FSupport->AttribExtensions->Strings[Index])));
}
FTerminal->LogEvent(FORMAT(L" Extensions (%d)\n", (FSupport->Extensions->Count)));
for (int Index = 0; Index < FSupport->Extensions->Count; Index++)
{
FTerminal->LogEvent(
FORMAT(L" %s", (FSupport->Extensions->Strings[Index])));
}
}
}
else if (ExtensionName == SFTP_EXT_VENDOR_ID)
{
TSFTPPacket VendorIdStruct(ExtensionData);
UnicodeString VendorName(VendorIdStruct.GetAnsiString());
UnicodeString ProductName(VendorIdStruct.GetAnsiString());
UnicodeString ProductVersion(VendorIdStruct.GetAnsiString());
__int64 ProductBuildNumber = VendorIdStruct.GetInt64();
FTerminal->LogEvent(FORMAT(L"Server software: %s %s (%d) by %s",
(ProductName, ProductVersion, int(ProductBuildNumber), VendorName)));
}
else if (ExtensionName == SFTP_EXT_FSROOTS)
{
FTerminal->LogEvent(L"File system roots:\n");
DebugAssert(FFixedPaths == NULL);
FFixedPaths = new TStringList();
try
{
TSFTPPacket RootsPacket(ExtensionData);
while (RootsPacket.GetNextData() != NULL)
{
unsigned long Dummy = RootsPacket.GetCardinal();
if (Dummy != 1)
{
break;
}
else
{
unsigned char Drive = RootsPacket.GetByte();
unsigned char MaybeType = RootsPacket.GetByte();
FTerminal->LogEvent(FORMAT(L" %s: (type %d)", (static_cast<char>(Drive), (int)MaybeType)));
FFixedPaths->Add(FORMAT(L"%s:", (static_cast<char>(Drive))));
}
}
}
catch(Exception & E)
{
FFixedPaths->Clear();
FTerminal->LogEvent(FORMAT(L"Failed to decode %s extension",
(SFTP_EXT_FSROOTS)));
FTerminal->HandleException(&E);
}
}
else if (ExtensionName == SFTP_EXT_VERSIONS)
{
// first try legacy decoding according to incorrect encoding
// (structure-like) as of VShell (bug no longer present as of 4.0.3).
TSFTPPacket VersionsPacket(ExtensionData);
unsigned int StringSize;
if (VersionsPacket.CanGetString(StringSize) &&
(StringSize == VersionsPacket.RemainingLength))
{
UnicodeString Versions = VersionsPacket.GetAnsiString();
FTerminal->LogEvent(FORMAT(L"SFTP versions supported by the server (VShell format): %s",
(Versions)));
}
else
{
// if that fails, fallback to proper decoding
FTerminal->LogEvent(FORMAT(L"SFTP versions supported by the server: %s",
(AnsiToString(ExtensionData))));
}
}
else if (ExtensionName == SFTP_EXT_STATVFS)
{
UnicodeString StatVfsVersion = AnsiToString(ExtensionData);
if (StatVfsVersion == SFTP_EXT_STATVFS_VALUE_V2)
{
FSupportsStatVfsV2 = true;
FTerminal->LogEvent(FORMAT(L"Supports %s extension version %s", (ExtensionName, ExtensionDisplayData)));
}
else
{
FTerminal->LogEvent(FORMAT(L"Unsupported %s extension version %s", (ExtensionName, ExtensionDisplayData)));
}
}
else if (ExtensionName == SFTP_EXT_HARDLINK)
{
UnicodeString HardlinkVersion = AnsiToString(ExtensionData);
if (HardlinkVersion == SFTP_EXT_HARDLINK_VALUE_V1)
{
FSupportsHardlink = true;
FTerminal->LogEvent(FORMAT(L"Supports %s extension version %s", (ExtensionName, ExtensionDisplayData)));
}
else
{
FTerminal->LogEvent(FORMAT(L"Unsupported %s extension version %s", (ExtensionName, ExtensionDisplayData)));
}
}
else
{
FTerminal->LogEvent(0, FORMAT(L"Unknown server extension %s=%s", (ExtensionName, ExtensionDisplayData)));
}
FExtensions->Values[ExtensionName] = ExtensionDisplayData;
}
if (SupportsExtension(SFTP_EXT_VENDOR_ID))
{
TSFTPPacket Packet(SSH_FXP_EXTENDED);
Packet.AddString(SFTP_EXT_VENDOR_ID);
Packet.AddString(FTerminal->Configuration->CompanyName);
Packet.AddString(FTerminal->Configuration->ProductName);
Packet.AddString(FTerminal->Configuration->ProductVersion);
Packet.AddInt64(LOWORD(FTerminal->Configuration->FixedApplicationInfo->dwFileVersionLS));
SendPacket(&Packet);
// we are not interested in the response, do not wait for it
ReceiveResponse(&Packet, &Packet);
//ReserveResponse(&Packet, NULL);
}
}
if (FVersion < 4)
{
// currently enable the bug for all servers (really known on OpenSSH)
FSignedTS = (FTerminal->SessionData->SFTPBug[sbSignedTS] == asOn) ||
(FTerminal->SessionData->SFTPBug[sbSignedTS] == asAuto);
if (FSignedTS)
{
FTerminal->LogEvent(L"We believe the server has signed timestamps bug");
}
}
else
{
FSignedTS = false;
}
switch (FTerminal->SessionData->NotUtf)
{
case asOff:
FUtfStrings = asOn;
FTerminal->LogEvent(L"We will use UTF-8 strings as configured");
break;
default:
DebugFail();
case asAuto:
// Nb, Foxit server does not exist anymore
if (GetSessionInfo().SshImplementation.Pos(L"Foxit-WAC-Server") == 1)
{
FUtfStrings = asOff;
FTerminal->LogEvent(L"We will not use UTF-8 strings as the server is known not to use them");
}
else
{
if (FVersion >= 4)
{
FTerminal->LogEvent(L"We will use UTF-8 strings as it is mandatory with SFTP version 4 and newer");
FUtfStrings = asOn;
}
else
{
FTerminal->LogEvent(L"We will use UTF-8 strings until server sends an invalid UTF-8 string as with SFTP version 3 and older UTF-8 strings are not mandatory");
FUtfStrings = asAuto;
FUtfDisablingAnnounced = false;
}
}
break;
case asOn:
FTerminal->LogEvent(L"We will not use UTF-8 strings as configured");
FUtfStrings = asOff;
break;
}
FMaxPacketSize = FTerminal->SessionData->SFTPMaxPacketSize;
if (FMaxPacketSize == 0)
{
if ((FSecureShell->SshImplementation == sshiOpenSSH) && (FVersion == 3) && !FSupport->Loaded)
{
FMaxPacketSize = 4 + (256 * 1024); // len + 256kB payload
FTerminal->LogEvent(FORMAT(L"Limiting packet size to OpenSSH sftp-server limit of %d bytes",
(int(FMaxPacketSize))));
}
// full string is "1.77 sshlib: Momentum SSH Server",
// possibly it is sshlib-related
else if (GetSessionInfo().SshImplementation.Pos(L"Momentum SSH Server") != 0)
{
FMaxPacketSize = 4 + (32 * 1024);
FTerminal->LogEvent(FORMAT(L"Limiting packet size to Momentum sftp-server limit of %d bytes",
(int(FMaxPacketSize))));
}
}
}
//---------------------------------------------------------------------------
char * __fastcall TSFTPFileSystem::GetEOL() const
{
if (FVersion >= 4)
{
DebugAssert(!FEOL.IsEmpty());
return FEOL.c_str();
}
else
{
return EOLToStr(FTerminal->SessionData->EOLType);
}
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::LookupUsersGroups()
{
DebugAssert(SupportsExtension(SFTP_EXT_OWNER_GROUP));
TSFTPPacket PacketOwners(SSH_FXP_EXTENDED);
TSFTPPacket PacketGroups(SSH_FXP_EXTENDED);
TSFTPPacket * Packets[] = { &PacketOwners, &PacketGroups };
TRemoteTokenList * Lists[] = { &FTerminal->FUsers, &FTerminal->FGroups };
wchar_t ListTypes[] = { OGQ_LIST_OWNERS, OGQ_LIST_GROUPS };
for (size_t Index = 0; Index < LENOF(Packets); Index++)
{
TSFTPPacket * Packet = Packets[Index];
Packet->AddString(SFTP_EXT_OWNER_GROUP);
Packet->AddByte(ListTypes[Index]);
SendPacket(Packet);
ReserveResponse(Packet, Packet);
}
for (size_t Index = 0; Index < LENOF(Packets); Index++)
{
TSFTPPacket * Packet = Packets[Index];
ReceiveResponse(Packet, Packet, SSH_FXP_EXTENDED_REPLY, asOpUnsupported);
if ((Packet->Type != SSH_FXP_EXTENDED_REPLY) ||
(Packet->GetAnsiString() != SFTP_EXT_OWNER_GROUP_REPLY))
{
FTerminal->LogEvent(FORMAT(L"Invalid response to %s", (SFTP_EXT_OWNER_GROUP)));
}
else
{
TRemoteTokenList & List = *Lists[Index];
unsigned long Count = Packet->GetCardinal();
List.Clear();
for (unsigned long Item = 0; Item < Count; Item++)
{
TRemoteToken Token(Packet->GetString(FUtfStrings));
List.Add(Token);
if (&List == &FTerminal->FGroups)
{
FTerminal->FMembership.Add(Token);
}
}
}
}
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::ReadCurrentDirectory()
{
if (!FDirectoryToChangeTo.IsEmpty())
{
FCurrentDirectory = FDirectoryToChangeTo;
FDirectoryToChangeTo = L"";
}
else if (FCurrentDirectory.IsEmpty())
{
// this happens only after startup when default remote directory is not specified
FCurrentDirectory = GetHomeDirectory();
}
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::HomeDirectory()
{
ChangeDirectory(GetHomeDirectory());
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::TryOpenDirectory(const UnicodeString Directory)
{
FTerminal->LogEvent(FORMAT(L"Trying to open directory \"%s\".", (Directory)));
TRemoteFile * File;
CustomReadFile(Directory, File, SSH_FXP_LSTAT, NULL, asOpUnsupported);
if (File == NULL)
{
// File can be NULL only when server does not support SSH_FXP_LSTAT.
// Fallback to legacy solution, which in turn does not allow entering
// traverse-only (chmod 110) directories.
// This is workaround for http://www.ftpshell.com/
TSFTPPacket Packet(SSH_FXP_OPENDIR);
AddPathString(Packet, UnixExcludeTrailingBackslash(Directory));
SendPacketAndReceiveResponse(&Packet, &Packet, SSH_FXP_HANDLE);
RawByteString Handle = Packet.GetFileHandle();
Packet.ChangeType(SSH_FXP_CLOSE);
Packet.AddString(Handle);
SendPacketAndReceiveResponse(&Packet, &Packet, SSH_FXP_STATUS, asAll);
}
else
{
delete File;
}
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::AnnounceFileListOperation()
{
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::ChangeDirectory(const UnicodeString Directory)
{
UnicodeString Path, Current;
Current = !FDirectoryToChangeTo.IsEmpty() ? FDirectoryToChangeTo : FCurrentDirectory;
Path = RealPath(Directory, Current);
// to verify existence of directory try to open it (SSH_FXP_REALPATH succeeds
// for invalid paths on some systems, like CygWin)
TryOpenDirectory(Path);
// if open dir did not fail, directory exists -> success.
FDirectoryToChangeTo = Path;
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::CachedChangeDirectory(const UnicodeString Directory)
{
FDirectoryToChangeTo = UnixExcludeTrailingBackslash(Directory);
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::ReadDirectory(TRemoteFileList * FileList)
{
DebugAssert(FileList && !FileList->Directory.IsEmpty());
UnicodeString Directory;
Directory = UnixExcludeTrailingBackslash(LocalCanonify(FileList->Directory));
FTerminal->LogEvent(FORMAT(L"Listing directory \"%s\".", (Directory)));
// moved before SSH_FXP_OPENDIR, so directory listing does not retain
// old data (e.g. parent directory) when reading fails
FileList->Reset();
TSFTPPacket Packet(SSH_FXP_OPENDIR);
RawByteString Handle;
try
{
AddPathString(Packet, Directory);
SendPacketAndReceiveResponse(&Packet, &Packet, SSH_FXP_HANDLE);
Handle = Packet.GetFileHandle();
}
catch(...)
{
if (FTerminal->Active)
{
FileList->AddFile(new TRemoteParentDirectory(FTerminal));
}
throw;
}
TSFTPPacket Response;
try
{
bool isEOF = false;
int Total = 0;
bool HasParentDirectory = false;
TRemoteFile * File;
Packet.ChangeType(SSH_FXP_READDIR);
Packet.AddString(Handle);
SendPacket(&Packet);
do
{
ReceiveResponse(&Packet, &Response);
if (Response.Type == SSH_FXP_NAME)
{
TSFTPPacket ListingPacket = Response;
Packet.ChangeType(SSH_FXP_READDIR);
Packet.AddString(Handle);
SendPacket(&Packet);
ReserveResponse(&Packet, &Response);
unsigned int Count = ListingPacket.GetCardinal();
int ResolvedLinks = 0;
for (unsigned long Index = 0; !isEOF && (Index < Count); Index++)
{
File = LoadFile(&ListingPacket, NULL, L"", FileList);
FileList->AddFile(File);
if (FTerminal->IsEncryptingFiles() && // optimization
IsRealFile(File->FileName))
{
UnicodeString FullFileName = UnixExcludeTrailingBackslash(File->FullFileName);
UnicodeString FileName = UnixExtractFileName(FTerminal->DecryptFileName(FullFileName));
if (File->FileName != FileName)
{
File->SetEncrypted();
}
File->FileName = FileName;
}
if (FTerminal->Configuration->ActualLogProtocol >= 1)
{
FTerminal->LogEvent(FORMAT(L"Read file '%s' from listing", (File->FileName)));
}
if (File->LinkedFile != NULL)
{
ResolvedLinks++;
}
if (File->IsParentDirectory)
{
HasParentDirectory = true;
}
Total++;
if (Total % 10 == 0)
{
FTerminal->DoReadDirectoryProgress(Total, ResolvedLinks, isEOF);
if (isEOF)
{
FTerminal->DoReadDirectoryProgress(-2, 0, isEOF);
}
}
}
if (!isEOF &&
(FVersion >= 6) &&
// As of 7.0.9 the Cerberus SFTP server always sets the end-of-list to true.
// Fixed in 7.0.10.
(FSecureShell->SshImplementation != sshiCerberus) &&
ListingPacket.CanGetBool())
{
isEOF = ListingPacket.GetBool();
}
if (Count == 0)
{
FTerminal->LogEvent(L"Empty directory listing packet. Aborting directory reading.");
isEOF = true;
}
}
else if (Response.Type == SSH_FXP_STATUS)
{
isEOF = (GotStatusPacket(&Response, asEOF) == SSH_FX_EOF);
}
else
{
FTerminal->FatalError(NULL, FMTLOAD(SFTP_INVALID_TYPE, ((int)Response.Type)));
}
}
while (!isEOF);
if (Total == 0)
{
bool Failure = false;
// no point reading parent of root directory,
// moreover CompleteFTP terminates session upon attempt to do so
if (IsUnixRootPath(FileList->Directory))
{
File = NULL;
}
else
{
// Empty file list -> probably "permission denied", we
// at least get link to parent directory ("..")
try
{
FTerminal->ExceptionOnFail = true;
try
{
File = NULL;
FTerminal->ReadFile(
UnixIncludeTrailingBackslash(FileList->Directory) + PARENTDIRECTORY, File);
}
__finally
{
FTerminal->ExceptionOnFail = false;
}
}
catch(Exception &E)
{
if (E.InheritsFrom(__classid(EFatal)))
{
throw;
}
else
{
File = NULL;
Failure = true;
}
}
}
// on some systems even getting ".." fails, we create dummy ".." instead
if (File == NULL)
{
File = new TRemoteParentDirectory(FTerminal);
}
DebugAssert(File && File->IsParentDirectory);
FileList->AddFile(File);
if (Failure)
{
throw ExtException(
NULL, FMTLOAD(EMPTY_DIRECTORY, (FileList->Directory)),
HELP_EMPTY_DIRECTORY);
}
}
else
{
if (!HasParentDirectory)
{
FileList->AddFile(new TRemoteParentDirectory(FTerminal));
}
}
}
__finally
{
if (FTerminal->Active)
{
Packet.ChangeType(SSH_FXP_CLOSE);
Packet.AddString(Handle);
SendPacket(&Packet);
// we are not interested in the response, do not wait for it
ReserveResponse(&Packet, NULL);
}
}
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::ReadSymlink(TRemoteFile * SymlinkFile,
TRemoteFile *& File)
{
DebugAssert(SymlinkFile != NULL);
DebugAssert(FVersion >= 3); // symlinks are supported with SFTP version 3 and later
// need to use full filename when resolving links within subdirectory
// (i.e. for download)
UnicodeString FileName = LocalCanonify(
SymlinkFile->HaveFullFileName ? SymlinkFile->FullFileName : SymlinkFile->FileName);
TSFTPPacket ReadLinkPacket(SSH_FXP_READLINK);
AddPathString(ReadLinkPacket, FileName);
SendPacket(&ReadLinkPacket);
ReserveResponse(&ReadLinkPacket, &ReadLinkPacket);
// send second request before reading response to first one
// (performance benefit)
TSFTPPacket AttrsPacket(SSH_FXP_STAT);
AddPathString(AttrsPacket, FileName);
if (FVersion >= 4)
{
AttrsPacket.AddCardinal(SSH_FILEXFER_ATTR_COMMON);
}
SendPacket(&AttrsPacket);
ReserveResponse(&AttrsPacket, &AttrsPacket);
ReceiveResponse(&ReadLinkPacket, &ReadLinkPacket, SSH_FXP_NAME);
if (ReadLinkPacket.GetCardinal() != 1)
{
FTerminal->FatalError(NULL, LoadStr(SFTP_NON_ONE_FXP_NAME_PACKET));
}
SymlinkFile->LinkTo = FTerminal->DecryptFileName(ReadLinkPacket.GetPathString(FUtfStrings));
FTerminal->LogEvent(FORMAT(L"Link resolved to \"%s\".", (SymlinkFile->LinkTo)));
ReceiveResponse(&AttrsPacket, &AttrsPacket, SSH_FXP_ATTRS);
// SymlinkFile->FileName was used instead SymlinkFile->LinkTo before, why?
File = LoadFile(&AttrsPacket, SymlinkFile,
UnixExtractFileName(SymlinkFile->LinkTo));
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::ReadFile(const UnicodeString FileName,
TRemoteFile *& File)
{
CustomReadFile(FileName, File, SSH_FXP_LSTAT);
}
//---------------------------------------------------------------------------
bool __fastcall TSFTPFileSystem::RemoteFileExists(const UnicodeString FullPath,
TRemoteFile ** File)
{
bool Result;
try
{
TRemoteFile * AFile;
CustomReadFile(FullPath, AFile, SSH_FXP_LSTAT, NULL, asNoSuchFile);
Result = (AFile != NULL);
if (Result)
{
if (File)
{
*File = AFile;
}
else
{
delete AFile;
}
}
}
catch(...)
{
if (!FTerminal->Active)
{
throw;
}
Result = false;
}
return Result;
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::SendCustomReadFile(TSFTPPacket * Packet,
TSFTPPacket * Response, unsigned long Flags)
{
if (FVersion >= 4)
{
Packet->AddCardinal(Flags);
}
SendPacket(Packet);
ReserveResponse(Packet, Response);
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::CustomReadFile(const UnicodeString FileName,
TRemoteFile *& File, unsigned char Type, TRemoteFile * ALinkedByFile,
int AllowStatus)
{
unsigned long Flags = SSH_FILEXFER_ATTR_SIZE | SSH_FILEXFER_ATTR_PERMISSIONS |
SSH_FILEXFER_ATTR_ACCESSTIME | SSH_FILEXFER_ATTR_MODIFYTIME |
SSH_FILEXFER_ATTR_OWNERGROUP;
TSFTPPacket Packet(Type);
AddPathString(Packet, LocalCanonify(FileName));
SendCustomReadFile(&Packet, &Packet, Flags);
ReceiveResponse(&Packet, &Packet, SSH_FXP_ATTRS, AllowStatus);
if (Packet.Type == SSH_FXP_ATTRS)
{
File = LoadFile(&Packet, ALinkedByFile, UnixExtractFileName(FileName));
if (FTerminal->IsFileEncrypted(FileName))
{
File->SetEncrypted();
}
}
else
{
DebugAssert(AllowStatus > 0);
File = NULL;
}
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::DoDeleteFile(const UnicodeString FileName, unsigned char Type)
{
TSFTPPacket Packet(Type);
UnicodeString RealFileName = LocalCanonify(FileName);
AddPathString(Packet, RealFileName);
SendPacketAndReceiveResponse(&Packet, &Packet, SSH_FXP_STATUS);
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::DeleteFile(const UnicodeString FileName,
const TRemoteFile * File, int Params, TRmSessionAction & Action)
{
unsigned char Type;
if (FTerminal->DeleteContentsIfDirectory(FileName, File, Params, Action))
{
Type = SSH_FXP_RMDIR;
}
else
{
Type = SSH_FXP_REMOVE;
}
DoDeleteFile(FileName, Type);
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::RenameFile(const UnicodeString FileName, const TRemoteFile * /*File*/,
const UnicodeString NewName)
{
TSFTPPacket Packet(SSH_FXP_RENAME);
UnicodeString RealName = LocalCanonify(FileName);
bool Encrypted = FTerminal->IsFileEncrypted(RealName);
AddPathString(Packet, RealName);
UnicodeString TargetName;
if (UnixExtractFilePath(NewName).IsEmpty())
{
// rename case (TTerminal::RenameFile)
TargetName = UnixExtractFilePath(RealName) + NewName;
}
else
{
TargetName = LocalCanonify(NewName);
}
AddPathString(Packet, TargetName, Encrypted);
if (FVersion >= 5)
{
Packet.AddCardinal(0);
}
SendPacketAndReceiveResponse(&Packet, &Packet, SSH_FXP_STATUS);
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::CopyFile(const UnicodeString FileName, const TRemoteFile * /*File*/,
const UnicodeString NewName)
{
// Implemented by ProFTPD/mod_sftp and Bitvise WinSSHD (without announcing it)
DebugAssert(SupportsExtension(SFTP_EXT_COPY_FILE) || (FSecureShell->SshImplementation == sshiBitvise));
TSFTPPacket Packet(SSH_FXP_EXTENDED);
Packet.AddString(SFTP_EXT_COPY_FILE);
UnicodeString RealName = Canonify(FileName);
bool Encrypted = FTerminal->IsFileEncrypted(RealName);
AddPathString(Packet, RealName);
AddPathString(Packet, Canonify(NewName), Encrypted);
Packet.AddBool(false);
SendPacketAndReceiveResponse(&Packet, &Packet, SSH_FXP_STATUS);
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::CreateDirectory(const UnicodeString & DirName, bool Encrypt)
{
TSFTPPacket Packet(SSH_FXP_MKDIR);
UnicodeString CanonifiedName = Canonify(DirName);
AddPathString(Packet, CanonifiedName, Encrypt);
Packet.AddProperties(NULL, 0, true, FVersion, FUtfStrings, NULL);
SendPacketAndReceiveResponse(&Packet, &Packet, SSH_FXP_STATUS);
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::CreateLink(const UnicodeString FileName,
const UnicodeString PointTo, bool Symbolic)
{
// Cerberus server does not even respond to LINK or SYMLINK,
// Although its log says:
// Unrecognized SFTP client command: (20)
// Unknown SFTP packet - Sending Unsupported OP response
DebugAssert(FVersion >= 3); // links are supported with SFTP version 3 and later
bool UseLink = (FVersion >= 6);
bool UseHardlink = !Symbolic && !UseLink && FSupportsHardlink;
TSFTPPacket Packet(UseHardlink ? SSH_FXP_EXTENDED : (UseLink ? SSH_FXP_LINK : SSH_FXP_SYMLINK));
if (UseHardlink)
{
Packet.AddString(SFTP_EXT_HARDLINK);
}
bool Buggy;
// OpenSSH hardlink extension always uses the "wrong" order
// as it's defined as such to mimic OpenSSH symlink bug
if (UseHardlink)
{
Buggy = true; //sic
}
else
{
if (FTerminal->SessionData->SFTPBug[sbSymlink] == asOn)
{
Buggy = true;
FTerminal->LogEvent(L"Forcing workaround for SFTP link bug");
}
else if (FTerminal->SessionData->SFTPBug[sbSymlink] == asOff)
{
Buggy = false;
}
else
{
if (UseLink)
{
if (FSecureShell->SshImplementation == sshiProFTPD)
{
// ProFTPD/mod_sftp followed OpenSSH symlink bug even for link implementation.
// This will be fixed with the next release with
// SSH version string bumbed to "mod_sftp/1.0.0"
// http://bugs.proftpd.org/show_bug.cgi?id=4080
UnicodeString ProFTPDVerStr = GetSessionInfo().SshImplementation;
CutToChar(ProFTPDVerStr, L'/', false);
int ProFTPDMajorVer = StrToIntDef(CutToChar(ProFTPDVerStr, L'.', false), 0);
Buggy = (ProFTPDMajorVer == 0);
if (Buggy)
{
FTerminal->LogEvent(L"We believe the server has SFTP link bug");
}
}
else
{
Buggy = false;
}
}
else
{
// ProFTPD/mod_sftp deliberately follows OpenSSH bug.
// Though we should get here with ProFTPD only when user forced
// SFTP version < 6 or when connecting to an ancient version of ProFTPD.
Buggy =
(FSecureShell->SshImplementation == sshiOpenSSH) ||
(FSecureShell->SshImplementation == sshiProFTPD);
if (Buggy)
{
FTerminal->LogEvent(L"We believe the server has SFTP symlink bug");
}
}
}
}
UnicodeString FinalPointTo = PointTo;
UnicodeString FinalFileName = Canonify(FileName);
if (!Symbolic)
{
FinalPointTo = Canonify(PointTo);
}
// creating symlinks is not allowed when encryption is enabled, so we are not considering encryption here
if (!Buggy)
{
AddPathString(Packet, FinalFileName);
AddPathString(Packet, FinalPointTo);
}
else
{
AddPathString(Packet, FinalPointTo);
AddPathString(Packet, FinalFileName);
}
if (UseLink)
{
Packet.AddBool(Symbolic);
}
SendPacketAndReceiveResponse(&Packet, &Packet, SSH_FXP_STATUS);
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::ChangeFileProperties(const UnicodeString FileName,
const TRemoteFile * /*File*/, const TRemoteProperties * AProperties,
TChmodSessionAction & Action)
{
DebugAssert(AProperties != NULL);
TRemoteFile * File;
UnicodeString RealFileName = LocalCanonify(FileName);
ReadFile(RealFileName, File);
try
{
DebugAssert(File);
if (File->IsDirectory && FTerminal->CanRecurseToDirectory(File) && AProperties->Recursive)
{
try
{
FTerminal->ProcessDirectory(FileName, FTerminal->ChangeFileProperties,
(void*)AProperties);
}
catch(...)
{
Action.Cancel();
throw;
}
}
// SFTP can change owner and group at the same time only, not individually.
// Fortunately we know current owner/group, so if only one is present,
// we can supplement the other.
TRemoteProperties Properties(*AProperties);
if (Properties.Valid.Contains(vpGroup) &&
!Properties.Valid.Contains(vpOwner))
{
Properties.Owner = File->Owner;
Properties.Valid << vpOwner;
}
else if (Properties.Valid.Contains(vpOwner) &&
!Properties.Valid.Contains(vpGroup))
{
Properties.Group = File->Group;
Properties.Valid << vpGroup;
}
TSFTPPacket Packet(SSH_FXP_SETSTAT);
AddPathString(Packet, RealFileName);
Packet.AddProperties(&Properties, *File->Rights, File->IsDirectory, FVersion, FUtfStrings, &Action);
SendPacketAndReceiveResponse(&Packet, &Packet, SSH_FXP_STATUS);
}
__finally
{
delete File;
}
}
//---------------------------------------------------------------------------
bool __fastcall TSFTPFileSystem::LoadFilesProperties(TStrings * FileList)
{
bool Result = false;
// without knowledge of server's capabilities, this all make no sense
if (FSupport->Loaded || (FSecureShell->SshImplementation == sshiBitvise))
{
TFileOperationProgressType Progress(&FTerminal->DoProgress, &FTerminal->DoFinished);
FTerminal->OperationStart(Progress, foGetProperties, osRemote, FileList->Count);
static int LoadFilesPropertiesQueueLen = 5;
TSFTPLoadFilesPropertiesQueue Queue(this);
try
{
if (Queue.Init(LoadFilesPropertiesQueueLen, FileList))
{
TRemoteFile * File;
TSFTPPacket Packet;
bool Next;
do
{
Next = Queue.ReceivePacket(&Packet, File);
DebugAssert((Packet.Type == SSH_FXP_ATTRS) || (Packet.Type == SSH_FXP_STATUS));
if (Packet.Type == SSH_FXP_ATTRS)
{
DebugAssert(File != NULL);
Progress.SetFile(File->FileName);
LoadFile(File, &Packet);
Result = true;
TOnceDoneOperation OnceDoneOperation;
Progress.Finish(File->FileName, true, OnceDoneOperation);
}
if (Progress.Cancel != csContinue)
{
Next = false;
}
}
while (Next);
}
}
__finally
{
Queue.DisposeSafe();
FTerminal->OperationStop(Progress);
}
// queue is discarded here
}
return Result;
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::DoCalculateFilesChecksum(
const UnicodeString & Alg, const UnicodeString & SftpAlg,
TStrings * FileList, TStrings * Checksums,
TCalculatedChecksumEvent OnCalculatedChecksum,
TFileOperationProgressType * OperationProgress, bool FirstLevel)
{
TOnceDoneOperation OnceDoneOperation; // not used
// recurse into subdirectories only if we have callback function
if (OnCalculatedChecksum != NULL)
{
for (int Index = 0; Index < FileList->Count; Index++)
{
TRemoteFile * File = (TRemoteFile *)FileList->Objects[Index];
DebugAssert(File != NULL);
if (File->IsDirectory && FTerminal->CanRecurseToDirectory(File) &&
IsRealFile(File->FileName))
{
OperationProgress->SetFile(File->FileName);
TRemoteFileList * SubFiles =
FTerminal->CustomReadDirectoryListing(File->FullFileName, false);
if (SubFiles != NULL)
{
TStrings * SubFileList = new TStringList();
bool Success = false;
try
{
OperationProgress->SetFile(File->FileName);
for (int Index = 0; Index < SubFiles->Count; Index++)
{
TRemoteFile * SubFile = SubFiles->Files[Index];
SubFileList->AddObject(SubFile->FullFileName, SubFile);
}
// do not collect checksums for files in subdirectories,
// only send back checksums via callback
DoCalculateFilesChecksum(Alg, SftpAlg, SubFileList, NULL,
OnCalculatedChecksum, OperationProgress, false);
Success = true;
}
__finally
{
delete SubFiles;
delete SubFileList;
if (FirstLevel)
{
OperationProgress->Finish(File->FileName, Success, OnceDoneOperation);
}
}
}
}
}
}
static int CalculateFilesChecksumQueueLen = 5;
TSFTPCalculateFilesChecksumQueue Queue(this);
try
{
if (Queue.Init(CalculateFilesChecksumQueueLen, SftpAlg, FileList))
{
TSFTPPacket Packet;
bool Next;
do
{
bool Success = false;
UnicodeString Checksum;
TRemoteFile * File = NULL;
try
{
TChecksumSessionAction Action(FTerminal->ActionLog);
try
{
Next = Queue.ReceivePacket(&Packet, File);
DebugAssert(Packet.Type == SSH_FXP_EXTENDED_REPLY);
OperationProgress->SetFile(File->FileName);
Action.FileName(FTerminal->AbsolutePath(File->FullFileName, true));
// skip alg
Packet.GetAnsiString();
Checksum = BytesToHex(reinterpret_cast<const unsigned char*>(Packet.GetNextData(Packet.RemainingLength)), Packet.RemainingLength, false);
if (OnCalculatedChecksum != NULL)
{
OnCalculatedChecksum(File->FileName, Alg, Checksum);
}
Action.Checksum(Alg, Checksum);
Success = true;
}
catch (Exception & E)
{
FTerminal->RollbackAction(Action, OperationProgress, &E);
// Error formatting expanded from inline to avoid strange exceptions
UnicodeString Error =
FMTLOAD(CHECKSUM_ERROR,
(File != NULL ? File->FullFileName : UnicodeString(L"")));
FTerminal->CommandError(&E, Error);
// TODO: retries? resume?
Next = false;
}
if (Checksums != NULL)
{
Checksums->Add(Checksum);
}
}
__finally
{
if (FirstLevel)
{
OperationProgress->Finish(File->FileName, Success, OnceDoneOperation);
}
}
if (OperationProgress->Cancel != csContinue)
{
Next = false;
}
}
while (Next);
}
}
__finally
{
Queue.DisposeSafe();
}
// queue is discarded here
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::CalculateFilesChecksum(const UnicodeString & Alg,
TStrings * FileList, TStrings * Checksums,
TCalculatedChecksumEvent OnCalculatedChecksum)
{
TFileOperationProgressType Progress(&FTerminal->DoProgress, &FTerminal->DoFinished);
UnicodeString NormalizedAlg = FindIdent(Alg, FChecksumAlgs.get());
UnicodeString SftpAlg;
int Index = FChecksumAlgs->IndexOf(NormalizedAlg);
if (Index >= 0)
{
SftpAlg = FChecksumSftpAlgs->Strings[Index];
}
else
{
// try user-specified alg
SftpAlg = NormalizedAlg;
}
FTerminal->OperationStart(Progress, foCalculateChecksum, osRemote, FileList->Count);
try
{
DoCalculateFilesChecksum(NormalizedAlg, SftpAlg, FileList, Checksums, OnCalculatedChecksum,
&Progress, true);
}
__finally
{
FTerminal->OperationStop(Progress);
}
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::CustomCommandOnFile(const UnicodeString /*FileName*/,
const TRemoteFile * /*File*/, UnicodeString /*Command*/, int /*Params*/,
TCaptureOutputEvent /*OutputEvent*/)
{
DebugFail();
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::AnyCommand(const UnicodeString /*Command*/,
TCaptureOutputEvent /*OutputEvent*/)
{
DebugFail();
}
//---------------------------------------------------------------------------
TStrings * __fastcall TSFTPFileSystem::GetFixedPaths()
{
return FFixedPaths;
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::SpaceAvailable(const UnicodeString Path,
TSpaceAvailable & ASpaceAvailable)
{
if (SupportsExtension(SFTP_EXT_SPACE_AVAILABLE) ||
// See comment in IsCapable
(FSecureShell->SshImplementation == sshiBitvise))
{
TSFTPPacket Packet(SSH_FXP_EXTENDED);
Packet.AddString(SFTP_EXT_SPACE_AVAILABLE);
AddPathString(Packet, LocalCanonify(Path));
SendPacketAndReceiveResponse(&Packet, &Packet, SSH_FXP_EXTENDED_REPLY);
ASpaceAvailable.BytesOnDevice = Packet.GetInt64();
ASpaceAvailable.UnusedBytesOnDevice = Packet.GetInt64();
ASpaceAvailable.BytesAvailableToUser = Packet.GetInt64();
ASpaceAvailable.UnusedBytesAvailableToUser = Packet.GetInt64();
// bytes-per-allocation-unit was added later to the protocol
// (revision 07, while the extension was defined already in rev 06),
// be tolerant
if (Packet.CanGetCardinal())
{
ASpaceAvailable.BytesPerAllocationUnit = Packet.GetCardinal();
}
else if (Packet.CanGetSmallCardinal())
{
// See http://bugs.proftpd.org/show_bug.cgi?id=4079
FTerminal->LogEvent(L"Assuming ProFTPD/mod_sftp bug of 2-byte bytes-per-allocation-unit field");
ASpaceAvailable.BytesPerAllocationUnit = Packet.GetSmallCardinal();
}
else
{
FTerminal->LogEvent(L"Missing bytes-per-allocation-unit field");
}
}
else if (DebugAlwaysTrue(FSupportsStatVfsV2))
{
// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL
TSFTPPacket Packet(SSH_FXP_EXTENDED);
Packet.AddString(SFTP_EXT_STATVFS);
AddPathString(Packet, LocalCanonify(Path));
SendPacketAndReceiveResponse(&Packet, &Packet, SSH_FXP_EXTENDED_REPLY);
__int64 BlockSize = Packet.GetInt64(); // file system block size
__int64 FundamentalBlockSize = Packet.GetInt64(); // fundamental fs block size
__int64 Blocks = Packet.GetInt64(); // number of blocks (unit f_frsize)
__int64 FreeBlocks = Packet.GetInt64(); // free blocks in file system
__int64 AvailableBlocks = Packet.GetInt64(); // free blocks for non-root
__int64 FileINodes = Packet.GetInt64(); // total file inodes
__int64 FreeFileINodes = Packet.GetInt64(); // free file inodes
__int64 AvailableFileINodes = Packet.GetInt64(); // free file inodes for to non-root
__int64 SID = Packet.GetInt64(); // file system id
__int64 Flags = Packet.GetInt64(); // bit mask of f_flag values
__int64 NameMax = Packet.GetInt64(); // maximum filename length
FTerminal->LogEvent(FORMAT(L"Block size: %s", (IntToStr(BlockSize))));
FTerminal->LogEvent(FORMAT(L"Fundamental block size: %s", (IntToStr(FundamentalBlockSize))));
FTerminal->LogEvent(FORMAT(L"Total blocks: %s", (IntToStr(Blocks))));
FTerminal->LogEvent(FORMAT(L"Free blocks: %s", (IntToStr(FreeBlocks))));
FTerminal->LogEvent(FORMAT(L"Free blocks for non-root: %s", (IntToStr(AvailableBlocks))));
FTerminal->LogEvent(FORMAT(L"Total file inodes: %s", (IntToStr(FileINodes))));
FTerminal->LogEvent(FORMAT(L"Free file inodes: %s", (IntToStr(FreeFileINodes))));
FTerminal->LogEvent(FORMAT(L"Free file inodes for non-root: %s", (IntToStr(AvailableFileINodes))));
FTerminal->LogEvent(FORMAT(L"File system ID: %s", (BytesToHex(reinterpret_cast<const unsigned char *>(&SID), sizeof(SID)))));
UnicodeString FlagStr;
if (FLAGSET(Flags, SFTP_EXT_STATVFS_ST_RDONLY))
{
AddToList(FlagStr, L"read-only", L",");
Flags -= SFTP_EXT_STATVFS_ST_RDONLY;
}
if (FLAGSET(Flags, SFTP_EXT_STATVFS_ST_NOSUID))
{
AddToList(FlagStr, L"no-setuid", L",");
Flags -= SFTP_EXT_STATVFS_ST_NOSUID;
}
if (Flags != 0)
{
AddToList(FlagStr, UnicodeString(L"0x") + IntToHex(Flags, 2), L",");
}
if (FlagStr.IsEmpty())
{
FlagStr = L"none";
}
FTerminal->LogEvent(FORMAT(L"Flags: %s", (FlagStr)));
FTerminal->LogEvent(FORMAT(L"Max name length: %s", (IntToStr(NameMax))));
ASpaceAvailable.BytesOnDevice = BlockSize * Blocks;
ASpaceAvailable.UnusedBytesOnDevice = BlockSize * FreeBlocks;
ASpaceAvailable.BytesAvailableToUser = 0;
ASpaceAvailable.UnusedBytesAvailableToUser = BlockSize * AvailableBlocks;
ASpaceAvailable.BytesPerAllocationUnit =
(BlockSize > std::numeric_limits<unsigned long>::max()) ? 0 : static_cast<unsigned long>(BlockSize);
}
}
//---------------------------------------------------------------------------
// transfer protocol
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::CopyToRemote(TStrings * FilesToCopy,
const UnicodeString TargetDir, const TCopyParamType * CopyParam,
int Params, TFileOperationProgressType * OperationProgress,
TOnceDoneOperation & OnceDoneOperation)
{
TAutoFlag AvoidBusyFlag(FAvoidBusy);
FTerminal->DoCopyToRemote(FilesToCopy, TargetDir, CopyParam, Params, OperationProgress, tfPreCreateDir, OnceDoneOperation);
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::SFTPConfirmOverwrite(
const UnicodeString & SourceFullFileName, UnicodeString & TargetFileName,
const TCopyParamType * CopyParam, int Params, TFileOperationProgressType * OperationProgress,
TSFTPOverwriteMode & OverwriteMode, const TOverwriteFileParams * FileParams)
{
bool CanAppend = !FTerminal->IsEncryptingFiles() && ((FVersion < 4) || !OperationProgress->AsciiTransfer);
unsigned int Answer;
{
TSuspendFileOperationProgress Suspend(OperationProgress);
int Answers = qaYes | qaNo | qaCancel | qaYesToAll | qaNoToAll | qaAll | qaIgnore;
// possibly we can allow alternate resume at least in some cases
if (CanAppend)
{
Answers |= qaRetry;
}
TQueryButtonAlias Aliases[5];
Aliases[0].Button = qaRetry;
Aliases[0].Alias = LoadStr(APPEND_BUTTON);
Aliases[0].GroupWith = qaNo;
Aliases[0].GrouppedShiftState = TShiftState() << ssAlt;
Aliases[1] = TQueryButtonAlias::CreateAllAsYesToNewerGrouppedWithYes();
Aliases[2] = TQueryButtonAlias::CreateIgnoreAsRenameGrouppedWithNo();
Aliases[3] = TQueryButtonAlias::CreateYesToAllGrouppedWithYes();
Aliases[4] = TQueryButtonAlias::CreateNoToAllGrouppedWithNo();
TQueryParams QueryParams(qpNeverAskAgainCheck);
QueryParams.NoBatchAnswers = qaIgnore | qaRetry | qaAll;
QueryParams.Aliases = Aliases;
QueryParams.AliasesCount = LENOF(Aliases);
Answer = FTerminal->ConfirmFileOverwrite(
SourceFullFileName, TargetFileName, FileParams,
Answers, &QueryParams,
ReverseOperationSide(OperationProgress->Side),
CopyParam, Params, OperationProgress);
}
if (CanAppend &&
((Answer == qaRetry) || (Answer == qaSkip)))
{
OperationProgress->LockUserSelections();
try
{
// duplicated in TTerminal::ConfirmFileOverwrite
bool CanAlternateResume =
(FileParams->DestSize < FileParams->SourceSize) && !OperationProgress->AsciiTransfer;
TBatchOverwrite BatchOverwrite =
FTerminal->EffectiveBatchOverwrite(SourceFullFileName, CopyParam, Params, OperationProgress, true);
// when mode is forced by batch, never query user
if (BatchOverwrite == boAppend)
{
OverwriteMode = omAppend;
}
else if (CanAlternateResume &&
((BatchOverwrite == boResume) || (BatchOverwrite == boAlternateResume)))
{
OverwriteMode = omResume;
}
// no other option, but append
else if (!CanAlternateResume)
{
OverwriteMode = omAppend;
}
else
{
TQueryParams Params(0, HELP_APPEND_OR_RESUME);
{
TSuspendFileOperationProgress Suspend(OperationProgress);
Answer = FTerminal->QueryUser(FORMAT(LoadStr(APPEND_OR_RESUME2), (SourceFullFileName)),
NULL, qaYes | qaNo | qaNoToAll | qaCancel, &Params);
}
switch (Answer)
{
case qaYes:
OverwriteMode = omAppend;
break;
case qaNo:
OverwriteMode = omResume;
break;
case qaNoToAll:
OverwriteMode = omResume;
OperationProgress->SetBatchOverwrite(boAlternateResume);
break;
default: DebugFail(); //fallthru
case qaCancel:
OperationProgress->SetCancelAtLeast(csCancel);
Abort();
break;
}
}
}
__finally
{
OperationProgress->UnlockUserSelections();
}
}
else if (Answer == qaIgnore)
{
if (FTerminal->PromptUser(FTerminal->SessionData, pkFileName, LoadStr(RENAME_TITLE), L"",
LoadStr(RENAME_PROMPT2), true, 0, TargetFileName))
{
OverwriteMode = omOverwrite;
}
else
{
OperationProgress->SetCancelAtLeast(csCancel);
Abort();
}
}
else
{
OverwriteMode = omOverwrite;
switch (Answer)
{
case qaCancel:
OperationProgress->SetCancelAtLeast(csCancel);
Abort();
break;
case qaNo:
throw ESkipFile();
}
}
}
//---------------------------------------------------------------------------
bool TSFTPFileSystem::SFTPConfirmResume(const UnicodeString DestFileName,
bool PartialBiggerThanSource, TFileOperationProgressType * OperationProgress)
{
bool ResumeTransfer;
DebugAssert(OperationProgress);
if (PartialBiggerThanSource)
{
unsigned int Answer;
{
TSuspendFileOperationProgress Suspend(OperationProgress);
TQueryParams Params(qpAllowContinueOnError, HELP_PARTIAL_BIGGER_THAN_SOURCE);
Answer = FTerminal->QueryUser(
FMTLOAD(PARTIAL_BIGGER_THAN_SOURCE, (DestFileName)), NULL,
qaOK | qaAbort, &Params, qtWarning);
}
if (Answer == qaAbort)
{
OperationProgress->SetCancelAtLeast(csCancel);
Abort();
}
ResumeTransfer = false;
}
else if (FTerminal->Configuration->ConfirmResume)
{
unsigned int Answer;
{
TSuspendFileOperationProgress Suspend(OperationProgress);
TQueryParams Params(qpAllowContinueOnError | qpNeverAskAgainCheck,
HELP_RESUME_TRANSFER);
// "abort" replaced with "cancel" to unify with "append/resume" query
Answer = FTerminal->QueryUser(
FMTLOAD(RESUME_TRANSFER2, (DestFileName)), NULL, qaYes | qaNo | qaCancel,
&Params);
}
switch (Answer) {
case qaNeverAskAgain:
FTerminal->Configuration->ConfirmResume = false;
case qaYes:
ResumeTransfer = true;
break;
case qaNo:
ResumeTransfer = false;
break;
case qaCancel:
OperationProgress->SetCancelAtLeast(csCancel);
Abort();
break;
}
}
else
{
ResumeTransfer = true;
}
return ResumeTransfer;
}
//---------------------------------------------------------------------------
bool __fastcall TSFTPFileSystem::DoesFileLookLikeSymLink(TRemoteFile * File)
{
return
(FVersion < 4) &&
((*File->Rights & TRights::rfAll) == TRights::rfAll) &&
(File->Size < 100);
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::Source(
TLocalFileHandle & Handle, const UnicodeString & TargetDir, UnicodeString & DestFileName,
const TCopyParamType * CopyParam, int Params,
TFileOperationProgressType * OperationProgress, unsigned int Flags,
TUploadSessionAction & Action, bool & ChildError)
{
UnicodeString DestFullName = LocalCanonify(TargetDir + DestFileName);
UnicodeString DestPartialFullName;
bool ResumeAllowed;
bool ResumeTransfer = false;
bool DestFileExists = false;
TRights DestRights;
__int64 ResumeOffset;
// should we check for interrupted transfer?
ResumeAllowed =
!OperationProgress->AsciiTransfer &&
CopyParam->AllowResume(OperationProgress->LocalSize) &&
IsCapable(fcRename) &&
!FTerminal->IsEncryptingFiles() &&
(CopyParam->OnTransferIn == NULL);
TOpenRemoteFileParams OpenParams;
OpenParams.OverwriteMode = omOverwrite;
TOverwriteFileParams FileParams;
FileParams.SourceSize = OperationProgress->LocalSize;
FileParams.SourceTimestamp = Handle.Modification;
if (ResumeAllowed)
{
DestPartialFullName = DestFullName + FTerminal->Configuration->PartialExt;
if (FLAGCLEAR(Flags, tfNewDirectory))
{
FTerminal->LogEvent(L"Checking existence of file.");
TRemoteFile * File = NULL;
DestFileExists = RemoteFileExists(DestFullName, &File);
OperationProgress->Progress();
if (DestFileExists)
{
FTerminal->LogEvent(FORMAT(L"File exists: %s", (FTerminal->GetRemoteFileInfo(File))));
OpenParams.DestFileSize = File->Resolve()->Size;
FileParams.DestSize = OpenParams.DestFileSize;
FileParams.DestTimestamp = File->Modification;
DestRights = *File->Rights;
// If destination file is symlink, never do resumable transfer,
// as it would delete the symlink.
if (File->IsSymLink)
{
ResumeAllowed = false;
FTerminal->LogEvent(L"Existing file is symbolic link, not doing resumable transfer.");
}
// Also bit of heuristics to detect symlink on SFTP-3 and older
// (which does not indicate symlink in SSH_FXP_ATTRS).
// if file has all permissions and is small, then it is likely symlink.
// also it is not likely that such a small file (if it is not symlink)
// gets overwritten by large file (that would trigger resumable transfer).
else if (DoesFileLookLikeSymLink(File))
{
ResumeAllowed = false;
FTerminal->LogEvent(L"Existing file looks like a symbolic link, not doing resumable transfer.");
}
// Also never do resumable transfer for file owned by other user
// as deleting and recreating the file would change ownership.
// This won't for work for SFTP-3 (OpenSSH) as it does not provide
// owner name (only UID) and we know only logged in user name (not UID)
else if (!File->Owner.Name.IsEmpty() && !SameUserName(File->Owner.Name, FTerminal->UserName))
{
ResumeAllowed = false;
FTerminal->LogEvent(
FORMAT(L"Existing file is owned by another user [%s], not doing resumable transfer.", (File->Owner.Name)));
}
delete File;
File = NULL;
}
if (ResumeAllowed)
{
FTerminal->LogEvent(L"Checking existence of partially transferred file.");
if (RemoteFileExists(DestPartialFullName, &File))
{
ResumeOffset = File->Resolve()->Size; // Though partial file should not be symlink
delete File;
File = NULL;
bool PartialBiggerThanSource = (ResumeOffset > OperationProgress->LocalSize);
if (FLAGCLEAR(Params, cpNoConfirmation) &&
FLAGCLEAR(Params, cpResume) &&
!CopyParam->ResumeTransfer(Handle.FileName))
{
ResumeTransfer = SFTPConfirmResume(DestFileName,
PartialBiggerThanSource, OperationProgress);
}
else
{
ResumeTransfer = !PartialBiggerThanSource;
}
if (!ResumeTransfer)
{
DoDeleteFile(DestPartialFullName, SSH_FXP_REMOVE);
OperationProgress->Progress();
}
else
{
FTerminal->LogEvent(L"Resuming file transfer.");
}
}
else
{
// partial upload file does not exists, check for full file
if (DestFileExists)
{
UnicodeString PrevDestFileName = DestFileName;
SFTPConfirmOverwrite(Handle.FileName, DestFileName,
CopyParam, Params, OperationProgress, OpenParams.OverwriteMode, &FileParams);
if (PrevDestFileName != DestFileName)
{
// update paths in case user changes the file name
DestFullName = LocalCanonify(TargetDir + DestFileName);
DestPartialFullName = DestFullName + FTerminal->Configuration->PartialExt;
FTerminal->LogEvent(L"Checking existence of new file.");
DestFileExists = RemoteFileExists(DestFullName, NULL);
}
}
}
}
}
}
// will the transfer be resumable?
bool DoResume = (ResumeAllowed && (OpenParams.OverwriteMode == omOverwrite));
UnicodeString RemoteFileName = DoResume ? DestPartialFullName : DestFullName;
OpenParams.FileName = Handle.FileName;
OpenParams.RemoteFileName = RemoteFileName;
OpenParams.Resume = DoResume;
OpenParams.Resuming = ResumeTransfer;
OpenParams.OperationProgress = OperationProgress;
OpenParams.CopyParam = CopyParam;
OpenParams.Params = Params;
OpenParams.FileParams = &FileParams;
OpenParams.Confirmed = (CopyParam->OnTransferIn != NULL);
OpenParams.DontRecycle = false;
FTerminal->LogEvent(0, L"Opening remote file.");
FTerminal->FileOperationLoop(SFTPOpenRemote, OperationProgress, folAllowSkip,
FMTLOAD(SFTP_CREATE_FILE_ERROR, (OpenParams.RemoteFileName)),
&OpenParams);
OperationProgress->Progress();
if (OpenParams.RemoteFileName != RemoteFileName)
{
DebugAssert(!DoResume);
DebugAssert(UnixExtractFilePath(OpenParams.RemoteFileName) == UnixExtractFilePath(RemoteFileName));
DestFullName = OpenParams.RemoteFileName;
UnicodeString NewFileName = UnixExtractFileName(DestFullName);
DebugAssert(DestFileName != NewFileName);
DestFileName = NewFileName;
}
Action.Destination(DestFullName);
bool TransferFinished = false;
__int64 DestWriteOffset = 0;
TSFTPPacket CloseRequest;
bool PreserveRights = CopyParam->PreserveRights && (CopyParam->OnTransferIn == NULL);
bool PreserveExistingRights = DoResume && DestFileExists;
bool SetRights = (PreserveExistingRights || PreserveRights);
bool PreserveTime = CopyParam->PreserveTime && (CopyParam->OnTransferIn == NULL);
bool SetProperties = (PreserveTime || SetRights);
TSFTPPacket PropertiesRequest(SSH_FXP_SETSTAT);
TSFTPPacket PropertiesResponse;
TRights Rights;
if (SetProperties)
{
AddPathString(PropertiesRequest, DestFullName);
if (PreserveRights)
{
Rights = CopyParam->RemoteFileRights(Handle.Attrs);
}
else if (PreserveExistingRights)
{
Rights = DestRights;
}
else
{
DebugAssert(!SetRights);
}
unsigned short RightsNumber = Rights.NumberSet;
PropertiesRequest.AddProperties(
SetRights ? &RightsNumber : NULL, NULL, NULL,
PreserveTime ? &Handle.MTime : NULL,
NULL, NULL, false, FVersion, FUtfStrings);
}
try
{
if (OpenParams.OverwriteMode == omAppend)
{
FTerminal->LogEvent(L"Appending file.");
DestWriteOffset = OpenParams.DestFileSize;
}
else if (ResumeTransfer || (OpenParams.OverwriteMode == omResume))
{
if (OpenParams.OverwriteMode == omResume)
{
FTerminal->LogEvent(L"Resuming file transfer (append style).");
ResumeOffset = OpenParams.DestFileSize;
}
FileSeek((THandle)Handle.Handle, ResumeOffset, 0);
OperationProgress->AddResumed(ResumeOffset);
}
TEncryption Encryption(FTerminal->GetEncryptKey());
bool Encrypt = FTerminal->IsFileEncrypted(DestFullName, CopyParam->EncryptNewFiles);
TSFTPUploadQueue Queue(this, (Encrypt ? &Encryption : NULL));
try
{
int ConvertParams =
FLAGMASK(CopyParam->RemoveCtrlZ, cpRemoveCtrlZ) |
FLAGMASK(CopyParam->RemoveBOM, cpRemoveBOM);
Queue.Init(Handle.FileName, Handle.Handle, CopyParam->OnTransferIn, OperationProgress,
OpenParams.RemoteFileHandle,
DestWriteOffset + OperationProgress->TransferredSize,
ConvertParams);
while (Queue.Continue())
{
if (OperationProgress->Cancel)
{
if (OperationProgress->ClearCancelFile())
{
throw ESkipFile();
}
else
{
Abort();
}
}
}
// send close request before waiting for pending read responses
SFTPCloseRemote(OpenParams.RemoteFileHandle, DestFileName,
OperationProgress, false, true, &CloseRequest);
OpenParams.RemoteFileHandle = L"";
// when resuming is disabled, we can send "set properties"
// request before waiting for pending read/close responses
if (SetProperties && !DoResume)
{
SendPacket(&PropertiesRequest);
ReserveResponse(&PropertiesRequest, &PropertiesResponse);
}
// No error so far, processes pending responses and throw on first error
Queue.DisposeSafeWithErrorHandling();
}
__finally
{
// Either queue is empty now (noop call then),
// or some error occured (in that case, process remaining responses, ignoring other errors)
Queue.DisposeSafe();
}
TransferFinished = true;
// queue is discarded here
}
__finally
{
if (FTerminal->Active)
{
// if file transfer was finished, the close request was already sent
if (!OpenParams.RemoteFileHandle.IsEmpty())
{
SFTPCloseRemote(OpenParams.RemoteFileHandle, DestFileName,
OperationProgress, TransferFinished, true, &CloseRequest);
}
// wait for the response
SFTPCloseRemote(OpenParams.RemoteFileHandle, DestFileName,
OperationProgress, TransferFinished, false, &CloseRequest);
// delete file if transfer was not completed, resuming was not allowed and
// we were not appending (incl. alternate resume),
// shortly after plain transfer completes (eq. !ResumeAllowed)
if (!TransferFinished && !DoResume && (OpenParams.OverwriteMode == omOverwrite))
{
DoDeleteFile(OpenParams.RemoteFileName, SSH_FXP_REMOVE);
}
}
}
OperationProgress->Progress();
if (DoResume)
{
if (DestFileExists)
{
FILE_OPERATION_LOOP_BEGIN
{
if (FTerminal->SessionData->OverwrittenToRecycleBin &&
!FTerminal->SessionData->RecycleBinPath.IsEmpty())
{
FTerminal->RecycleFile(DestFullName, NULL);
}
else
{
DoDeleteFile(DestFullName, SSH_FXP_REMOVE);
}
}
FILE_OPERATION_LOOP_END(
FMTLOAD(DELETE_ON_RESUME_ERROR,
(UnixExtractFileName(DestFullName), DestFullName)));
}
// originally this was before CLOSE (last __finally statement),
// on VShell it failed
FILE_OPERATION_LOOP_BEGIN
{
RenameFile(OpenParams.RemoteFileName, NULL, DestFileName);
}
FILE_OPERATION_LOOP_END_CUSTOM(
FMTLOAD(RENAME_AFTER_RESUME_ERROR,
(UnixExtractFileName(OpenParams.RemoteFileName), DestFileName)),
folAllowSkip, HELP_RENAME_AFTER_RESUME_ERROR);
}
if (SetProperties)
{
std::unique_ptr<TTouchSessionAction> TouchAction;
if (PreserveTime)
{
TDateTime MDateTime = UnixToDateTime(Handle.MTime, FTerminal->SessionData->DSTMode);
FTerminal->LogEvent(FORMAT(L"Preserving timestamp [%s]",
(StandardTimestamp(MDateTime))));
TouchAction.reset(new TTouchSessionAction(FTerminal->ActionLog, DestFullName,
MDateTime));
}
std::unique_ptr<TChmodSessionAction> ChmodAction;
// do record chmod only if it was explicitly requested,
// not when it was implicitly performed to apply timestamp
// of overwritten file to new file
if (PreserveRights)
{
ChmodAction.reset(new TChmodSessionAction(FTerminal->ActionLog, DestFullName, Rights));
}
try
{
// when resuming is enabled, the set properties request was not sent yet
if (DoResume)
{
SendPacket(&PropertiesRequest);
}
bool Resend = false;
FILE_OPERATION_LOOP_BEGIN
{
try
{
TSFTPPacket DummyResponse;
TSFTPPacket * Response = &PropertiesResponse;
if (Resend)
{
PropertiesRequest.Reuse();
SendPacket(&PropertiesRequest);
// ReceiveResponse currently cannot receive twice into same packet,
// so DummyResponse is temporary workaround
Response = &DummyResponse;
}
Resend = true;
ReceiveResponse(&PropertiesRequest, Response, SSH_FXP_STATUS,
asOK | FLAGMASK(CopyParam->IgnorePermErrors, asPermDenied));
}
catch (...)
{
if (FTerminal->Active &&
(!PreserveRights && !PreserveTime))
{
DebugAssert(DoResume);
FTerminal->LogEvent(L"Ignoring error preserving permissions of overwritten file");
}
else
{
throw;
}
}
}
FILE_OPERATION_LOOP_END_CUSTOM(
FMTLOAD(PRESERVE_TIME_PERM_ERROR3, (DestFileName)),
folAllowSkip, HELP_PRESERVE_TIME_PERM_ERROR);
}
catch(Exception & E)
{
if (TouchAction.get() != NULL)
{
TouchAction->Rollback(&E);
}
if (ChmodAction.get() != NULL)
{
ChmodAction->Rollback(&E);
}
ChildError = true;
throw;
}
}
}
//---------------------------------------------------------------------------
RawByteString __fastcall TSFTPFileSystem::SFTPOpenRemoteFile(
const UnicodeString & FileName, unsigned int OpenType, bool EncryptNewFiles, __int64 Size)
{
TSFTPPacket Packet(SSH_FXP_OPEN);
AddPathString(Packet, FileName, EncryptNewFiles);
if (FVersion < 5)
{
Packet.AddCardinal(OpenType);
}
else
{
unsigned long Access =
FLAGMASK(FLAGSET(OpenType, SSH_FXF_READ), ACE4_READ_DATA) |
FLAGMASK(FLAGSET(OpenType, SSH_FXF_WRITE), ACE4_WRITE_DATA | ACE4_APPEND_DATA);
unsigned long Flags;
if (FLAGSET(OpenType, SSH_FXF_CREAT | SSH_FXF_EXCL))
{
Flags = SSH_FXF_CREATE_NEW;
}
else if (FLAGSET(OpenType, SSH_FXF_CREAT | SSH_FXF_TRUNC))
{
Flags = SSH_FXF_CREATE_TRUNCATE;
}
else if (FLAGSET(OpenType, SSH_FXF_CREAT))
{
Flags = SSH_FXF_OPEN_OR_CREATE;
}
else
{
Flags = SSH_FXF_OPEN_EXISTING;
}
Flags |=
FLAGMASK(FLAGSET(OpenType, SSH_FXF_APPEND), SSH_FXF_ACCESS_APPEND_DATA) |
FLAGMASK(FLAGSET(OpenType, SSH_FXF_TEXT), SSH_FXF_ACCESS_TEXT_MODE);
Packet.AddCardinal(Access);
Packet.AddCardinal(Flags);
}
bool SendSize =
(Size >= 0) &&
FLAGSET(OpenType, SSH_FXF_CREAT | SSH_FXF_TRUNC) &&
// Particuarly VanDyke VShell (4.0.3) does not support SSH_FILEXFER_ATTR_ALLOCATION_SIZE
// (it fails open request when the attribute is included).
// It's SFTP-6 attribute, so support structure should be available.
// It's actually not with VShell. But VShell supports the SSH_FILEXFER_ATTR_ALLOCATION_SIZE.
// All servers should support SSH_FILEXFER_ATTR_SIZE (SFTP < 6)
(!FSupport->Loaded || FLAGSET(FSupport->AttributeMask, Packet.AllocationSizeAttribute(FVersion)));
Packet.AddProperties(NULL, NULL, NULL, NULL, NULL,
SendSize ? &Size : NULL, false, FVersion, FUtfStrings);
SendPacketAndReceiveResponse(&Packet, &Packet, SSH_FXP_HANDLE);
return Packet.GetFileHandle();
}
//---------------------------------------------------------------------------
int __fastcall TSFTPFileSystem::SFTPOpenRemote(void * AOpenParams, void * /*Param2*/)
{
TOpenRemoteFileParams * OpenParams = (TOpenRemoteFileParams *)AOpenParams;
DebugAssert(OpenParams);
TFileOperationProgressType * OperationProgress = OpenParams->OperationProgress;
int OpenType;
bool Success = false;
bool ConfirmOverwriting;
do
{
try
{
ConfirmOverwriting =
!OpenParams->Confirmed && !OpenParams->Resume &&
FTerminal->CheckRemoteFile(OpenParams->FileName, OpenParams->CopyParam, OpenParams->Params, OperationProgress);
OpenType = SSH_FXF_WRITE | SSH_FXF_CREAT;
// when we want to preserve overwritten files, we need to find out that
// they exist first... even if overwrite confirmation is disabled.
// but not when we already know we are not going to overwrite (but e.g. to append)
if ((ConfirmOverwriting || (FTerminal->SessionData->OverwrittenToRecycleBin && !OpenParams->DontRecycle)) &&
(OpenParams->OverwriteMode == omOverwrite))
{
OpenType |= SSH_FXF_EXCL;
}
if (!OpenParams->Resuming && (OpenParams->OverwriteMode == omOverwrite))
{
OpenType |= SSH_FXF_TRUNC;
}
if ((FVersion >= 4) && OpenParams->OperationProgress->AsciiTransfer)
{
OpenType |= SSH_FXF_TEXT;
}
OpenParams->RemoteFileHandle =
SFTPOpenRemoteFile(OpenParams->RemoteFileName, OpenType, OpenParams->CopyParam->EncryptNewFiles, OperationProgress->LocalSize);
Success = true;
}
catch(Exception & E)
{
if (!OpenParams->Confirmed && (OpenType & SSH_FXF_EXCL) && FTerminal->Active)
{
FTerminal->LogEvent(FORMAT(L"Cannot create new file \"%s\", checking if it exists already", (OpenParams->RemoteFileName)));
bool ThrowOriginal = false;
std::unique_ptr<TRemoteFile> File;
// When exclusive opening of file fails, try to detect if file exists.
// When file does not exist, failure was probably caused by 'permission denied'
// or similar error. In this case throw original exception.
try
{
OperationProgress->Progress();
TRemoteFile * AFile;
UnicodeString RealFileName = LocalCanonify(OpenParams->RemoteFileName);
ReadFile(RealFileName, AFile);
File.reset(AFile);
File->FullFileName = RealFileName;
OpenParams->DestFileSize = File->Size; // Resolve symlinks?
if (OpenParams->FileParams != NULL)
{
OpenParams->FileParams->DestTimestamp = File->Modification;
OpenParams->FileParams->DestSize = OpenParams->DestFileSize;
}
}
catch(...)
{
if (!FTerminal->Active)
{
throw;
}
else
{
ThrowOriginal = true;
}
}
if (ThrowOriginal)
{
throw;
}
// we may get here even if confirmation is disabled,
// when we have preserving of overwritten files enabled
if (ConfirmOverwriting)
{
OperationProgress->Progress();
// confirmation duplicated in SFTPSource for resumable file transfers.
UnicodeString RemoteFileNameOnly = UnixExtractFileName(OpenParams->RemoteFileName);
SFTPConfirmOverwrite(OpenParams->FileName, RemoteFileNameOnly,
OpenParams->CopyParam, OpenParams->Params, OperationProgress, OpenParams->OverwriteMode, OpenParams->FileParams);
if (RemoteFileNameOnly != UnixExtractFileName(OpenParams->RemoteFileName))
{
OpenParams->RemoteFileName =
UnixExtractFilePath(OpenParams->RemoteFileName) + RemoteFileNameOnly;
// no longer points to a relevant file
File.reset(NULL);
}
OpenParams->Confirmed = true;
}
else
{
DebugAssert(FTerminal->SessionData->OverwrittenToRecycleBin);
}
if ((OpenParams->OverwriteMode == omOverwrite) &&
FTerminal->SessionData->OverwrittenToRecycleBin &&
!FTerminal->SessionData->RecycleBinPath.IsEmpty())
{
bool IsSymLink = (File.get() != NULL) && File->IsSymLink;
if (!IsSymLink && (File.get() != NULL) && DoesFileLookLikeSymLink(File.get()) && IsCapable(fcResolveSymlink))
{
FTerminal->LogEvent(L"Existing file looks like a symbolic link, checking if it really is.");
try
{
OperationProgress->Progress();
TRemoteFile * LinkedFile;
ReadSymlink(File.get(), LinkedFile);
delete LinkedFile;
IsSymLink = true;
}
catch (...)
{
if (!FTerminal->Active)
{
throw;
}
}
}
if (IsSymLink)
{
FTerminal->LogEvent(L"Existing file is a symbolic link, it will not be moved to a recycle bin.");
OpenParams->DontRecycle = true;
}
else
{
OperationProgress->Progress();
if (!FTerminal->RecycleFile(OpenParams->RemoteFileName, NULL))
{
// Allow normal overwrite
OpenParams->DontRecycle = true;
}
}
}
}
else if (FTerminal->Active)
{
// if file overwriting was confirmed, it means that the file already exists,
// if not, check now
if (!OpenParams->Confirmed)
{
bool ThrowOriginal = false;
// When file does not exist, failure was probably caused by 'permission denied'
// or similar error. In this case throw original exception.
try
{
TRemoteFile * File;
UnicodeString RealFileName = LocalCanonify(OpenParams->RemoteFileName);
ReadFile(RealFileName, File);
SAFE_DESTROY(File);
}
catch(...)
{
if (!FTerminal->Active)
{
throw;
}
else
{
ThrowOriginal = true;
}
}
if (ThrowOriginal)
{
throw;
}
}
// now we know that the file exists
if (FTerminal->FileOperationLoopQuery(E, OperationProgress,
FMTLOAD(SFTP_OVERWRITE_FILE_ERROR2, (OpenParams->RemoteFileName)),
folAllowSkip, LoadStr(SFTP_OVERWRITE_DELETE_BUTTON)))
{
OperationProgress->Progress();
int Params = dfNoRecursive;
FTerminal->DeleteFile(OpenParams->RemoteFileName, NULL, &Params);
}
}
else
{
throw;
}
}
}
while (!Success);
return 0;
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::SFTPCloseRemote(const RawByteString Handle,
const UnicodeString FileName, TFileOperationProgressType * OperationProgress,
bool TransferFinished, bool Request, TSFTPPacket * Packet)
{
// Moving this out of SFTPSource() fixed external exception 0xC0000029 error
FILE_OPERATION_LOOP_BEGIN
{
try
{
TSFTPPacket CloseRequest;
TSFTPPacket * P = (Packet == NULL ? &CloseRequest : Packet);
if (Request)
{
P->ChangeType(SSH_FXP_CLOSE);
P->AddString(Handle);
SendPacket(P);
ReserveResponse(P, Packet);
}
else
{
DebugAssert(Packet != NULL);
ReceiveResponse(P, Packet, SSH_FXP_STATUS);
}
}
catch(...)
{
if (!FTerminal->Active || TransferFinished)
{
throw;
}
}
}
FILE_OPERATION_LOOP_END(FMTLOAD(SFTP_CLOSE_FILE_ERROR, (FileName)));
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::CopyToLocal(TStrings * FilesToCopy,
const UnicodeString TargetDir, const TCopyParamType * CopyParam,
int Params, TFileOperationProgressType * OperationProgress,
TOnceDoneOperation & OnceDoneOperation)
{
TAutoFlag AvoidBusyFlag(FAvoidBusy);
FTerminal->DoCopyToLocal(FilesToCopy, TargetDir, CopyParam, Params, OperationProgress, tfNone, OnceDoneOperation);
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::DirectorySunk(
const UnicodeString & DestFullName, const TRemoteFile * File, const TCopyParamType * CopyParam)
{
if (CopyParam->PreserveTime && CopyParam->PreserveTimeDirs)
{
// FILE_FLAG_BACKUP_SEMANTICS is needed to "open" directory
HANDLE LocalHandle =
CreateFile(
ApiPath(DestFullName).c_str(), GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS, 0);
if (LocalHandle == INVALID_HANDLE_VALUE)
{
int SetFileTimeError = GetLastError();
FTerminal->LogEvent(
FORMAT(L"Preserving directory timestamp failed, ignoring: %s", (SysErrorMessageForError(SetFileTimeError))));
}
else
{
FTerminal->UpdateTargetTime(LocalHandle, File->Modification, FTerminal->SessionData->DSTMode);
CloseHandle(LocalHandle);
}
}
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::WriteLocalFile(
const TCopyParamType * CopyParam, TStream * FileStream, TFileBuffer & BlockBuf, const UnicodeString & LocalFileName,
TFileOperationProgressType * OperationProgress)
{
if (CopyParam->OnTransferOut != NULL)
{
BlockBuf.WriteToOut(CopyParam->OnTransferOut, FTerminal, BlockBuf.Size);
}
else
{
FILE_OPERATION_LOOP_BEGIN
{
BlockBuf.WriteToStream(FileStream, BlockBuf.Size);
}
FILE_OPERATION_LOOP_END(FMTLOAD(WRITE_ERROR, (LocalFileName)));
}
OperationProgress->AddLocallyUsed(BlockBuf.Size);
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::Sink(
const UnicodeString & FileName, const TRemoteFile * File,
const UnicodeString & TargetDir, UnicodeString & DestFileName, int Attrs,
const TCopyParamType * CopyParam, int Params, TFileOperationProgressType * OperationProgress,
unsigned int /*Flags*/, TDownloadSessionAction & Action)
{
// resume has no sense for temporary downloads
bool ResumeAllowed =
FLAGCLEAR(Params, cpTemporary) &&
!OperationProgress->AsciiTransfer &&
CopyParam->AllowResume(OperationProgress->TransferSize) &&
!FTerminal->IsEncryptingFiles() &&
(CopyParam->OnTransferOut == NULL);
HANDLE LocalHandle = NULL;
TStream * FileStream = NULL;
bool DeleteLocalFile = false;
RawByteString RemoteHandle;
UnicodeString DestFullName = TargetDir + DestFileName;
UnicodeString LocalFileName = DestFullName;
TSFTPOverwriteMode OverwriteMode = omOverwrite;
try
{
bool ResumeTransfer = false;
UnicodeString DestPartialFullName;
if (ResumeAllowed)
{
DestPartialFullName = DestFullName + FTerminal->Configuration->PartialExt;
LocalFileName = DestPartialFullName;
FTerminal->LogEvent(L"Checking existence of partially transferred file.");
if (FileExists(ApiPath(DestPartialFullName)))
{
FTerminal->LogEvent(L"Partially transferred file exists.");
__int64 ResumeOffset;
FTerminal->OpenLocalFile(DestPartialFullName, GENERIC_WRITE,
NULL, &LocalHandle, NULL, NULL, NULL, &ResumeOffset);
bool PartialBiggerThanSource = (ResumeOffset > OperationProgress->TransferSize);
if (FLAGCLEAR(Params, cpNoConfirmation))
{
ResumeTransfer = SFTPConfirmResume(DestFileName, PartialBiggerThanSource, OperationProgress);
}
else
{
ResumeTransfer = !PartialBiggerThanSource;
if (!ResumeTransfer)
{
FTerminal->LogEvent(L"Partially transferred file is bigger that original file.");
}
}
if (!ResumeTransfer)
{
CloseHandle(LocalHandle);
LocalHandle = NULL;
FILE_OPERATION_LOOP_BEGIN
{
THROWOSIFFALSE(Sysutils::DeleteFile(ApiPath(DestPartialFullName)));
}
FILE_OPERATION_LOOP_END(FMTLOAD(DELETE_LOCAL_FILE_ERROR, (DestPartialFullName)));
}
else
{
FTerminal->LogEvent(L"Resuming file transfer.");
FileSeek((THandle)LocalHandle, ResumeOffset, 0);
OperationProgress->AddResumed(ResumeOffset);
}
}
OperationProgress->Progress();
}
// first open source file, not to loose the destination file,
// if we cannot open the source one in the first place
FTerminal->LogEvent(L"Opening remote file.");
FILE_OPERATION_LOOP_BEGIN
{
int OpenType = SSH_FXF_READ;
if ((FVersion >= 4) && OperationProgress->AsciiTransfer)
{
OpenType |= SSH_FXF_TEXT;
}
RemoteHandle = SFTPOpenRemoteFile(FileName, OpenType);
OperationProgress->Progress();
}
FILE_OPERATION_LOOP_END(FMTLOAD(SFTP_OPEN_FILE_ERROR, (FileName)));
TSFTPPacket RemoteFilePacket(SSH_FXP_FSTAT);
RemoteFilePacket.AddString(RemoteHandle);
SendCustomReadFile(&RemoteFilePacket, &RemoteFilePacket, SSH_FILEXFER_ATTR_MODIFYTIME);
ReceiveResponse(&RemoteFilePacket, &RemoteFilePacket);
OperationProgress->Progress();
TDateTime Modification = File->Modification; // fallback
// ignore errors
if (RemoteFilePacket.Type == SSH_FXP_ATTRS)
{
// load file, avoid completion (resolving symlinks) as we do not need that
std::unique_ptr<TRemoteFile> AFile(
LoadFile(&RemoteFilePacket, NULL, UnixExtractFileName(FileName), NULL, false));
if (AFile->Modification != TDateTime())
{
Modification = File->Modification;
}
}
if ((Attrs >= 0) && !ResumeTransfer)
{
__int64 DestFileSize;
__int64 MTime;
FTerminal->OpenLocalFile(
DestFullName, GENERIC_WRITE, NULL, &LocalHandle, NULL, &MTime, NULL, &DestFileSize, false);
FTerminal->LogEvent(L"Confirming overwriting of file.");
TOverwriteFileParams FileParams;
FileParams.SourceSize = OperationProgress->TransferSize;
FileParams.SourceTimestamp = Modification;
FileParams.DestTimestamp = UnixToDateTime(MTime,
FTerminal->SessionData->DSTMode);
FileParams.DestSize = DestFileSize;
UnicodeString PrevDestFileName = DestFileName;
SFTPConfirmOverwrite(FileName, DestFileName, CopyParam, Params, OperationProgress, OverwriteMode, &FileParams);
if (PrevDestFileName != DestFileName)
{
DestFullName = TargetDir + DestFileName;
DestPartialFullName = DestFullName + FTerminal->Configuration->PartialExt;
if (ResumeAllowed)
{
if (FileExists(ApiPath(DestPartialFullName)))
{
FILE_OPERATION_LOOP_BEGIN
{
THROWOSIFFALSE(Sysutils::DeleteFile(ApiPath(DestPartialFullName)));
}
FILE_OPERATION_LOOP_END(FMTLOAD(DELETE_LOCAL_FILE_ERROR, (DestPartialFullName)));
}
LocalFileName = DestPartialFullName;
}
else
{
LocalFileName = DestFullName;
}
}
if (OverwriteMode == omOverwrite)
{
// is NULL when overwriting read-only file
if (LocalHandle)
{
CloseHandle(LocalHandle);
LocalHandle = NULL;
}
}
else
{
// is NULL when overwriting read-only file, so following will
// probably fail anyway
if (LocalHandle == NULL)
{
FTerminal->OpenLocalFile(DestFullName, GENERIC_WRITE, NULL, &LocalHandle, NULL, NULL, NULL, NULL);
}
ResumeAllowed = false;
FileSeek((THandle)LocalHandle, DestFileSize, 0);
if (OverwriteMode == omAppend)
{
FTerminal->LogEvent(L"Appending to file.");
}
else
{
FTerminal->LogEvent(L"Resuming file transfer (append style).");
DebugAssert(OverwriteMode == omResume);
OperationProgress->AddResumed(DestFileSize);
}
}
}
Action.Destination(ExpandUNCFileName(DestFullName));
if (CopyParam->OnTransferOut == NULL)
{
// if not already opened (resume, append...), create new empty file
if (!LocalHandle)
{
if (!FTerminal->CreateLocalFile(LocalFileName, OperationProgress,
&LocalHandle, FLAGSET(Params, cpNoConfirmation)))
{
throw ESkipFile();
}
}
DebugAssert(LocalHandle);
DeleteLocalFile = true;
FileStream = new TSafeHandleStream((THandle)LocalHandle);
}
// at end of this block queue is discarded
{
TSFTPDownloadQueue Queue(this);
try
{
TSFTPPacket DataPacket;
int QueueLen = int(OperationProgress->TransferSize / DownloadBlockSize(OperationProgress)) + 1;
if ((QueueLen > FTerminal->SessionData->SFTPDownloadQueue) ||
(QueueLen < 0))
{
QueueLen = FTerminal->SessionData->SFTPDownloadQueue;
}
if (QueueLen < 1)
{
QueueLen = 1;
}
Queue.Init(QueueLen, RemoteHandle, OperationProgress->TransferredSize, OperationProgress);
bool Eof = false;
bool PrevIncomplete = false;
int GapFillCount = 0;
int GapCount = 0;
unsigned long Missing = 0;
unsigned long DataLen = 0;
unsigned long BlockSize;
bool ConvertToken = false;
TEncryption Encryption(FTerminal->GetEncryptKey());
bool Decrypt = FTerminal->IsFileEncrypted(FileName);
while (!Eof)
{
if (Missing > 0)
{
Queue.InitFillGapRequest(OperationProgress->TransferredSize, Missing, &DataPacket);
GapFillCount++;
SendPacketAndReceiveResponse(&DataPacket, &DataPacket, SSH_FXP_DATA, asEOF);
}
else
{
Queue.ReceivePacket(&DataPacket, BlockSize);
}
if (DataPacket.Type == SSH_FXP_STATUS)
{
// must be SSH_FX_EOF, any other status packet would raise exception
Eof = true;
// close file right away, before waiting for pending responses
SFTPCloseRemote(RemoteHandle, DestFileName, OperationProgress, true, true, NULL);
RemoteHandle = L""; // do not close file again in __finally block
}
if (!Eof)
{
if ((Missing == 0) && PrevIncomplete)
{
// This can happen only if last request returned less bytes
// than expected, but exactly number of bytes missing to last
// known file size, but actually EOF was not reached.
// Can happen only when filesize has changed since directory
// listing and server returns less bytes than requested and
// file has some special file size.
FTerminal->LogEvent(FORMAT(
L"Received incomplete data packet before end of file, offset: %s, size: %d, requested: %d",
(IntToStr(OperationProgress->TransferredSize), int(DataLen), int(BlockSize))));
FTerminal->TerminalError(NULL, LoadStr(SFTP_INCOMPLETE_BEFORE_EOF));
}
// Buffer for one block of data
TFileBuffer BlockBuf;
DataLen = DataPacket.GetCardinal();
PrevIncomplete = false;
if (Missing > 0)
{
DebugAssert(DataLen <= Missing);
Missing -= DataLen;
}
else if (DataLen < BlockSize)
{
if (OperationProgress->TransferredSize + DataLen !=
OperationProgress->TransferSize)
{
// with native text transfer mode (SFTP>=4), do not bother about
// getting less than requested, read offset is ignored anyway
if ((FVersion < 4) || !OperationProgress->AsciiTransfer)
{
GapCount++;
Missing = BlockSize - DataLen;
}
}
else
{
PrevIncomplete = true;
}
}
DebugAssert(DataLen <= BlockSize);
BlockBuf.Insert(0, reinterpret_cast<const char *>(DataPacket.GetNextData(DataLen)), DataLen);
DataPacket.DataConsumed(DataLen);
OperationProgress->AddTransferred(DataLen);
if ((FVersion >= 6) && DataPacket.CanGetBool() && (Missing == 0))
{
Eof = DataPacket.GetBool();
}
if (OperationProgress->AsciiTransfer)
{
DebugAssert(!ResumeTransfer && !ResumeAllowed);
unsigned int PrevBlockSize = BlockBuf.Size;
BlockBuf.Convert(GetEOL(), FTerminal->Configuration->LocalEOLType, 0, ConvertToken);
OperationProgress->SetLocalSize(OperationProgress->LocalSize - PrevBlockSize + BlockBuf.Size);
}
if (Decrypt)
{
Encryption.Decrypt(BlockBuf);
}
WriteLocalFile(CopyParam, FileStream, BlockBuf, LocalFileName, OperationProgress);
}
if (OperationProgress->Cancel != csContinue)
{
if (OperationProgress->ClearCancelFile())
{
throw ESkipFile();
}
else
{
Abort();
}
}
};
if (GapCount > 0)
{
FTerminal->LogEvent(FORMAT(L"%d requests to fill %d data gaps were issued.", (GapFillCount, GapCount)));
}
if (Decrypt)
{
TFileBuffer BlockBuf;
if (Encryption.DecryptEnd(BlockBuf))
{
WriteLocalFile(CopyParam, FileStream, BlockBuf, LocalFileName, OperationProgress);
}
}
}
__finally
{
Queue.DisposeSafe();
}
// queue is discarded here
}
if (CopyParam->OnTransferOut != NULL)
{
CopyParam->OnTransferOut(FTerminal, NULL, 0);
}
else
{
DebugAssert(LocalHandle);
if (CopyParam->PreserveTime)
{
FTerminal->UpdateTargetTime(LocalHandle, Modification, FTerminal->SessionData->DSTMode);
}
CloseHandle(LocalHandle);
LocalHandle = NULL;
if (ResumeAllowed)
{
FILE_OPERATION_LOOP_BEGIN
{
if (FileExists(ApiPath(DestFullName)))
{
DeleteFileChecked(DestFullName);
}
THROWOSIFFALSE(Sysutils::RenameFile(ApiPath(DestPartialFullName), ApiPath(DestFullName)));
}
FILE_OPERATION_LOOP_END(FMTLOAD(RENAME_AFTER_RESUME_ERROR, (ExtractFileName(DestPartialFullName), DestFileName)));
}
DeleteLocalFile = false;
FTerminal->UpdateTargetAttrs(DestFullName, File, CopyParam, Attrs);
}
}
__finally
{
if (LocalHandle)
{
CloseHandle(LocalHandle);
}
if (FileStream != NULL)
{
delete FileStream;
}
if (DeleteLocalFile && (!ResumeAllowed || OperationProgress->LocallyUsed == 0) &&
(OverwriteMode == omOverwrite))
{
FILE_OPERATION_LOOP_BEGIN
{
THROWOSIFFALSE(Sysutils::DeleteFile(ApiPath(LocalFileName)));
}
FILE_OPERATION_LOOP_END(FMTLOAD(DELETE_LOCAL_FILE_ERROR, (LocalFileName)));
}
// if the transfer was finished, the file is closed already
if (FTerminal->Active && !RemoteHandle.IsEmpty())
{
// do not wait for response
SFTPCloseRemote(RemoteHandle, DestFileName, OperationProgress, true, true, NULL);
}
}
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::RegisterChecksumAlg(const UnicodeString & Alg, const UnicodeString & SftpAlg)
{
FChecksumAlgs->Add(Alg);
FChecksumSftpAlgs->Add(SftpAlg);
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::GetSupportedChecksumAlgs(TStrings * Algs)
{
Algs->AddStrings(FChecksumAlgs.get());
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::LockFile(const UnicodeString & /*FileName*/, const TRemoteFile * /*File*/)
{
DebugFail();
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::UnlockFile(const UnicodeString & /*FileName*/, const TRemoteFile * /*File*/)
{
DebugFail();
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::UpdateFromMain(TCustomFileSystem * /*MainFileSystem*/)
{
// noop
}
//---------------------------------------------------------------------------
void __fastcall TSFTPFileSystem::ClearCaches()
{
// noop
}
//---------------------------------------------------------------------------
void TSFTPFileSystem::AddPathString(TSFTPPacket & Packet, const UnicodeString & Value, bool EncryptNewFiles)
{
UnicodeString EncryptedPath = FTerminal->EncryptFileName(Value, EncryptNewFiles);
Packet.AddPathString(EncryptedPath, FUtfStrings);
}