restemplate/indy/Protocols/IdFTPServer.pas

7266 lines
264 KiB
Plaintext
Raw Permalink Blame History

{
$Project$
$Workfile$
$Revision$
$DateUTC$
$Id$
This file is part of the Indy (Internet Direct) project, and is offered
under the dual-licensing agreement described on the Indy website.
(http://www.indyproject.org/)
Copyright:
(c) 1993-2005, Chad Z. Hower and the Indy Pit Crew. All rights reserved.
}
{
$Log$
}
{
Rev 1.146 3/23/2005 5:16:56 AM JPMugaas
Should compile.
Rev 1.145 3/14/05 11:28:50 AM RLebeau
Bug fix for CommandSIZE() not checking the FTPFileSystem property.
Updated to reflect changes in TIdReply.NumericCode handling.
Rev 1.144 3/5/2005 3:33:58 PM JPMugaas
Fix for some compiler warnings having to do with TStream.Read being platform
specific. This was fixed by changing the Compressor API to use TIdStreamVCL
instead of TStream. I also made appropriate adjustments to other units for
this.
Rev 1.143 11/22/2004 8:29:20 PM JPMugaas
Fix for a compiler warning.
Rev 1.142 11/22/2004 7:49:36 PM JPMugaas
You now can access help before you are logged in. This is done to conform
to RFC 959.
Rev 1.141 2004.10.27 9:17:48 AM czhower
For TIdStrings
Rev 1.140 10/26/2004 9:40:42 PM JPMugaas
Updated ref.
Rev 1.139 9/15/2004 5:01:00 PM DSiders
Added localization comments.
Rev 1.138 2004.08.13 11:03:22 czhower
Removed unused var.
Rev 1.137 7/29/2004 1:33:10 AM JPMugaas
Reordered AUTH command values for a new property under development. This
should make things more logical.
Rev 1.136 7/18/2004 3:00:42 PM DSiders
Added localization comments.
Rev 1.135 7/15/2004 1:33:00 AM JPMugaas
Bug fix for error 105. I fixed this by changing data channel command
processing. If the command is not ABOR or STAT, the command is put into a
FIFO queue. After the data channel operation is completed, the commands from
the FIFO queue are processed. I have tested FlashFXP 3.0 RC4 and it does
worki as expected. The behavior is also the same as what NcFTPD does with a
NOOP being sent during a data transfer.
This may also help with FTP command pipelining as proposed by:
http://cr.yp.to/ftp/pipelining.html
Note that we can not use the regular command handler framework for data
channel commands because STAT and ABOR need to be handled IMMEDIATELY.
Rev 1.134 7/13/04 9:08:10 PM RLebeau
Renamed OnPASV event to OnPASVBeforeBind and added new OnPASVReply event
Rev 1.133 7/13/04 8:13:56 PM RLebeau
Various changed for DefaultDataPort handling
Rev 1.132 7/13/2004 3:34:12 AM JPMugaas
CCC command and a few other minor modifications to comply with
http://www.ietf.org/internet-drafts/draft-murray-auth-ftp-ssl-14.txt .
I also fixed a few minor bugs in the help and a problem with some error
replies sending an extra 200 after a 5xxx code messing up some clients.
I also expanded the Security options to selectively disable CCC per user.
Some administrators may want to do this for security reasons.
Rev 1.131 7/12/2004 11:46:44 PM JPMugaas
Improvement in OPTS MODE Z handling. It will give an error if there's only
one param. Params must be in pairs. If no valid parameters are present, we
give an error.
Rev 1.130 07/07/2004 17:34:38 ANeillans
Corrected compile bug.
Line 6026,
if PosInStrArray(IntToStr(LNoVal),STATES,False)>-1 then
Function expected a string, not an integer.
Rev 1.129 7/6/2004 4:52:16 PM DSiders
Corrected spelling of Challenge in properties, methods, types.
Rev 1.128 6/29/2004 4:09:04 PM JPMugaas
OPTS MODE Z now supported as per draft-preston-ftpext-deflate-02.txt. This
should keep FTP Voyager 11 happy.
Rev 1.127 6/28/2004 7:23:20 PM JPMugaas
Bugfix. An invalid site command would cause no reply to be given. Now a
syntax is given in such cases.
Rev 1.126 6/27/2004 1:45:30 AM JPMugaas
Can now optionally support LastAccessTime like Smartftp's FTP Server could.
I also made the MLST listing object and parser support this as well.
Rev 1.125 6/17/2004 3:56:28 PM JPMugaas
Fix for AV that happens after data channel operation.
Rev 1.124 6/16/2004 2:29:32 PM JPMugaas
Removed direct access to a FConnection. We now use the Connection property
in the TIdContext.
Rev 1.123 6/12/2004 9:05:52 AM JPMugaas
Telnet control sequences should now work during a data transfer.
Removed HandleTelnetSequences. It was part of a crude workaround which had
never works and the matter was fixed in another way.
OnCustomDir should now work if the DirStyle is custom.
Rev 1.122 6/11/2004 9:35:12 AM DSiders
Added "Do not Localize" comments.
Rev 1.121 2004.05.20 11:37:26 AM czhower
IdStreamVCL
Rev 1.120 5/16/04 5:30:26 PM RLebeau
Added setter methods to the ReplyUnknownSITECommand and SITECommands
properties
Added GetRepliesClass() overrides
Rev 1.119 5/1/2004 1:52:20 PM JPMugaas
Updated for PeekBytes API change.
Rev 1.118 4/8/2004 12:19:08 PM JPMugaas
Should work with new code.
Rev 1.117 3/3/2004 6:34:46 PM JPMugaas
Improved help system.
Some manditory (RFC 1123 were rutning syntax errors instead of not
implemented.
Add some mention of some other RFC 2228 commands for completness. Not that
there are not supported or implemented.
Rev 1.116 3/3/2004 6:02:14 AM JPMugaas
Command descriptions.
Rev 1.115 3/2/2004 8:13:28 AM JPMugaas
Fixup for minor API change.
Rev 1.113 3/1/2004 12:41:40 PM JPMugaas
Should compile with new code.
Rev 1.112 2/29/2004 6:02:38 PM JPMugaas
Improved bug fix for problem with Telnet sequences not being handled properly
in the FTP server. Litteral CR and LF are now handled properly (according to
the Telnet Specification).
Rev 1.111 2/25/2004 3:27:04 PM JPMugaas
STAT -l now works like a LIST command except that it returns output on the
control channel. This is for consistancy with microsoft FTP Service,
RaidenFTPD, and a few other servers. FlashFXP can take advantage of this
feature as well to gain some efficiency. Note that I do not do not advocate
doing this on the FTP client because some servers will act differently than
you would assume. I may see about possible options for using STAT -l but I
can NOT promise anything.
Rev 1.110 2/17/2004 6:37:28 PM JPMugaas
OnPASV event added for people needing to change the IP address or port value
in commands such as PASV. This should only be done if you have a compelling
reason to do it.
Note that the IP address parameter can NOT work with EPSV and SPSV because
only the port number is returned. The IP address is presumed to be the same
one that the host is connecting to.
Rev 1.109 2/17/2004 12:26:06 PM JPMugaas
The client now supports MODE Z (deflate) uploads and downloads as specified
by http://www.ietf.org/internet-drafts/draft-preston-ftpext-deflate-00.txt
Rev 1.108 2/15/2004 12:11:04 AM JPMugaas
SPSV support. SPSV is an old propoal to help FTP support IPv6. This was
mentioned at: http://cr.yp.to/ftp/retr.html and is supported by PureFTPD.
Rev 1.107 2/14/2004 10:00:40 PM JPMugaas
Both upload and download should now work in MODE Z. Dir already worked
properly.
Rev 1.106 2/12/2004 11:34:38 PM JPMugaas
FTP Deflate preliminary support. Work still needs to be done for upload and
downloading.
Rev 1.105 2004.02.08 3:08:10 PM czhower
.Net fix.
Rev 1.104 2004.02.07 5:03:10 PM czhower
.net fixes.
Rev 1.103 2004.02.03 5:45:54 PM czhower
Name changes
Rev 1.102 1/29/2004 3:15:52 PM JPMugaas
Fix for P@SW in InitCommandHandlers used "PASV" isntead of "P@SW". Fixed.
Rev 1.101 1/22/2004 8:29:06 AM JPMugaas
Removed Ansi*.
Rev 1.100 1/21/2004 2:34:38 PM JPMugaas
Fixed SITE ZONE reply.
InitComponent
Rev 1.99 1/19/2004 4:37:02 AM JPMugaas
MinutesFromGMT was moved to IdFTPCommon because the client now uses it.
Rev 1.98 1/18/2004 9:19:08 AM JPMugaas
P@SW now supported.
This is necessary as some routers that replace a PASV with a P@SW
as part of a misguided attempt to add a feature.
A router would do a replacement so a client would think that
PASV wasn't supported and then the client would do a PORT command
instead. That doesn't happen so this just caused the client not to work.
See: http://www.gbnetwork.co.uk/smcftpd/
Rev 1.97 1/17/2004 7:40:08 PM JPMugaas
MLSD added to FEAT list for consistancy with other FTP servers.
Fixed bug that would cause FXP transfers to fail when receiving a PASV.
Rev 1.96 1/16/2004 12:25:06 AM JPMugaas
Fixes for MTDM set modified time.
Rev 1.94 1/15/2004 2:36:50 AM JPMugaas
XMD5 command support.
SITE ZONE command added for FTP Voyager.
Minor adjustment in AUTH line in the FEAT response to indicate that we
support the AUTH TLS, AUTH TLS-C, AUTH SSL, and AUTH TLS-P explicit TLS
commands.
Rev 1.93 1/14/2004 4:11:30 PM JPMugaas
CPSV support added. This is like PASV but indicates that we use ssl_connect
instead of ssl_accept. CPSV is used in FlashFXP for secure site-to-site file
transfers.
Rev 1.92 1/14/2004 12:24:06 PM JPMugaas
SSCN Support for secure Site to Site Transfers using SSL.
SSCN is defined at:
http://www.raidenftpd.com/kb/kb000000037.htm
Rev 1.91 1/13/2004 6:30:38 AM JPMugaas
Numerous bug fixes.
Now supports XCWD (a predicessor to CWD).
Command Reply for unknown command works again.
Started putting some formatting into common routines.
CuteFTP goes bonkers with a "215 " reply to SYST command. Now indicate that
SYST isn't implemented instead of giving that "215 ". Note that a
"CustomSystID" should be provided when DirFormat is ftpdfCustom.
If DirFormat is ftpdfCustom and OnListDirectory is provided; MLST, MLSD, and
OPTS MLSD will be DISABLED. OnListDirectory is used in the custom format for
structed standardized output with the MLSD and MLST commands.
A not implemented is now given for some commands.
Rev 1.90 1/5/2004 11:53:00 PM JPMugaas
Some messages moved to resource strings. Minor tweeks. EIdException no
longer raised.
Rev 1.88 1/4/2004 3:51:32 PM JPMugaas
Fixed a CWD bug. The parameter was being ignored.
Rev 1.87 1/3/2004 8:05:18 PM JPMugaas
Bug fix: Sometimes, replies will appear twice due to the way functionality
was enherited.
Rev 1.86 1/3/2004 5:37:56 PM JPMugaas
Changes from Bas:
added function GetReplyClass, this function returns the class of reply this
server class uses, this is because in dotnet there can be no code before the
inherited in the constructor ( that is used mow to determine the<68>reply class )
<20>
changed System.Delete to IdDelete (in coreglobal) because System.Delete is
not in dotnet
<20>
SplitLines is not enabled in dotnet yet, so i made it a todo, make sure to
enable it and remove the todo if you check it in<69>
<20>
Rev 1.85 1/2/2004 1:02:08 AM JPMugaas
Made comment about why the SYST descriptor is determined the way it is.
Rev 1.84 1/2/2004 12:55:32 AM JPMugaas
Now compiles. Removed the EmulateSystem property. Replaced one part with
the DirFormat property.
Rev 1.83 1/1/2004 10:55:10 PM JPMugaas
Remy Lebeau found a bug with path processing in the FTP server. I was
passing an emptry Result string instead of APath in FTPNormalize.
Rev 1.77 10/11/2003 10:17:28 AM JPMugaas
Checked in a more recent version which should be worked on instead.
Rev 1.75 9/19/2003 12:50:18 PM JPMugaas
Started attempt to get the server to compile.
Rev 1.74 9/18/2003 10:20:06 AM JPMugaas
Updated for new API.
Rev 1.73 8/24/2003 06:50:02 PM JPMugaas
API Change in the FileSystem component so that a thread is passed instead of
some data from the thread. This should also make the API's easier to manage
than before and provide more flexibility for developers writing their own
file system components.
Rev 1.72 7/13/2003 7:56:00 PM SPerry
fixed problem with commandhandlers
Rev 1.69 6/17/2003 09:30:20 PM JPMugaas
Fixed an AV with the ALLO command if no parameters were passed. Stated in
HELP command that we don't support some old FTP E-Mail commands from RFC 765
which have not been in use for many years. We now give a reply saying those
aren't implemented to be consistant with some Unix FTP deamons.
Rev 1.68 6/17/2003 03:16:36 PM JPMugaas
I redid the help and site help implementations so that they list commands.
It did mean loosing the FHelpText TIdStrings property but this should be more
consistant with common practices.
Rev 1.67 6/17/2003 09:07:40 AM JPMugaas
Improved SITE HELP handling.
Rev 1.65 5/26/2003 12:22:50 PM JPMugaas
Rev 1.64 5/25/2003 03:54:28 AM JPMugaas
Rev 1.63 5/21/2003 3:59:32 PM BGooijen
removed with in InitializeCommandHandlers, and changed exception replies
Rev 1.62 5/21/2003 09:29:40 AM JPMugaas
Rev 1.61 5/19/2003 08:11:44 PM JPMugaas
Now should compile properly with new code in Core.
Rev 1.60 4/10/2003 02:54:14 PM JPMugaas
Improvement for FTP STOU command. Unique filename now uses
IdGlobal.GetUniqueFileName instead of Rand. I also fixed GetUniqueFileName
so that it can accept an empty path specification.
Rev 1.59 3/30/2003 12:18:38 AM BGooijen
bug fix + ssl one data channel fixed
Rev 1.58 3/24/2003 11:08:42 PM BGooijen
'transfer'-commands now block, until the transfer is done/aborted.
this made it possible to send the reply after the transfer in the
control-thread
Rev 1.57 3/16/2003 06:11:18 PM JPMugaas
Server now derrives from a TLS framework.
Rev 1.56 3/14/2003 11:33:46 PM JPMugaas
Rev 1.55 3/14/2003 10:44:38 PM BGooijen
Removed warnings, changed StartSSL to PassThrough:=false;
Rev 1.54 3/14/2003 10:00:24 PM BGooijen
Removed TIdServerIOHandlerSSLBase.PeerPassthrough, the ssl is now enabled in
the server-protocol-files
Rev 1.53 3/13/2003 05:21:18 PM JPMugaas
Bas's bug fix. There was a wrong typecast.
Rev 1.52 3/13/2003 8:57:30 PM BGooijen
changed TIdSSLIOHandlerSocketBase to TIdIOHandlerSocket in
TIdDataChannelContext.SetupDataChannel
Rev 1.51 3/13/2003 09:49:06 AM JPMugaas
Now uses an abstract SSL base class instead of OpenSSL so 3rd-party vendors
can plug-in their products.
Rev 1.50 3/13/2003 06:11:54 AM JPMugaas
Updated with Bas's change.
Rev 1.49 3/10/2003 09:12:46 PM JPMugaas
Most command handlers now use Do methods for consistancy with other Indy code.
Rev 1.48 3/10/2003 05:09:22 PM JPMugaas
MLST now works as expected with the file system. Note that the MLST means
simply to give information about an item instead of its contents.
GetRealFileName in IdFTPFileSystem now can accept the wildcard *.
When doing dirs in EPLF, only information about a directory is retruned if it
is specified.
Rev 1.47 3/9/2003 02:11:34 PM JPMugaas
Removed server support for MODE B and MODE C. It turns out that we do not
support those modes properly. We only implemented Stream mode. We now
simply return a 504 for modes we don't support instead of a 200 okay. This
was throwing off Opera 7.02.
Rev 1.46 3/6/2003 11:00:12 AM JPMugaas
Now handles the MFMT command and the MFCT (Modified Date fact) command.
Rev 1.45 3/6/2003 08:26:28 AM JPMugaas
Bug fixes.
FTP COMB command can now work in the FTPFileSystem component.
Rev 1.44 3/5/2003 03:28:16 PM JPMugaas
MD5, MMD5, and XCRC are now supported in the Virtual File System.
Rev 1.43 3/5/2003 11:46:38 AM JPMugaas
Rename now works in Virtual FileSystem.
Rev 1.42 3/2/2003 04:54:34 PM JPMugaas
Now does recursive dir lists with the Virtual File System layer as well as
honors other switches.
Rev 1.41 3/2/2003 02:18:32 PM JPMugaas
Bug fix for where a reply was not returned when using a file system component.
Rev 1.40 3/2/2003 02:23:38 AM JPMugaas
fix for problem with pathes in the virtual file system.
Rev 1.39 2/24/2003 08:50:44 PM JPMugaas
Rev 1.38 2/24/2003 07:56:22 PM JPMugaas
Now uses /bin/ls strings.
Rev 1.37 2/24/2003 07:21:10 AM JPMugaas
FTP Server now strips out any -R switches when emulating EPLF servers.
Recursive lists aren't supported with EPLF.
Rev 1.36 2/21/2003 06:54:10 PM JPMugaas
The FTP list processing has been restructured so that Directory output is not
done by IdFTPList. This now also uses the IdFTPListParserBase for parsing so
that the code is more scalable.
Rev 1.35 2/15/2003 10:29:42 AM JPMugaas
Added support for some Unix specific facts with MLSD and MLST.
Rev 1.34 2/14/2003 05:42:08 PM JPMugaas
Moved everything from IdFTPUtils to IdFTPCommon at Kudzu's suggestion.
Rev 1.33 2/14/2003 11:57:48 AM JPMugaas
Updated for new API. Made sure that there were no calls to a function we
removed.
Rev 1.32 2/14/2003 10:45:18 AM JPMugaas
Updated for minor API change.
Rev 1.30 2/13/2003 01:28:08 AM JPMugaas
MLSD and MLST should now work better.
Rev 1.29 2/12/2003 12:30:56 PM JPMugaas
Now honors parameters with the NLIST command.
Rev 1.28 2/5/2003 10:30:04 PM BGooijen
Re-enabled ssl-support
Rev 1.27 2/4/2003 05:31:40 PM JPMugaas
Added ASwitches parameter to the ListEvent so we can pass parameters such as
"-R" in addition to the standard path.
Rev 1.26 2/3/2003 11:01:50 AM JPMugaas
Moved list export to IdFTPList.
Rev 1.25 1/31/2003 01:59:18 PM JPMugaas
Security options are now reenabled.
Rev 1.24 1/31/2003 01:19:00 PM JPMugaas
Now passes the ControlConnection context instead of the ControlConnection
object itself.
Rev 1.23 1/31/2003 06:34:52 AM JPMugaas
Now SYST command works as expected.
Rev 1.22 1/31/2003 04:23:24 AM JPMugaas
FTP Server security options can be set for individual users and the server
will now use the Context's security options. THis should permit more
flexibility in security.
Rev 1.21 1/30/2003 03:31:06 AM JPMugaas
Now should also properly handle exceptions in the MLSx commands.
Rev 1.20 1/30/2003 02:55:26 AM JPMugaas
Now properly handles exceptions in the ListEvent for the STAT and LIST
commands.
Rev 1.19 1/29/2003 01:17:18 AM JPMugaas
Exception handling should mostly work as it should. There's still a problem
with the list.
Rev 1.18 1/28/2003 02:27:26 AM JPMugaas
Improved exception handling in several events to try to be more consistant.
Now can optionally hide the exception message when giving an error reply to
the user. This should prevent some inadvertant information about a computer
going to a troublemaker.
Rev 1.17 1/27/2003 05:03:16 AM JPMugaas
Now a developer can provide status information to a user with the STAT
command if they want. We format the reply in a standard manner for them.
They just provide the information.
Rev 1.16 1/27/2003 02:13:30 AM JPMugaas
Added more security options as suggested by:
http://www.sans.org/rr/infowar/fingerprint.php to help slow down an attack.
You can optionally disable both SYST and the STAT commands. Trouble makers
can use those to help determine server type and then use known flaws to
compromise it. Note that these do not completely prevent attacks and should
not lull administrators into a false sense of security.
Rev 1.15 1/27/2003 12:32:08 AM JPMugaas
Now can optionally return the identifier for the real operating system. By
default, this property is false for security reasons.
Rev 1.14 1/26/2003 11:59:16 PM JPMugaas
SystemDescriptor behavior change as well as SYST command change.
SystemDescriptor no longer needs an OS type as the first word. That is now
handled by the SYST commandhandler to better comply with RFC 959.
Rev 1.13 1/25/2003 02:00:58 AM JPMugaas
MMD5 (for multiple MD5 checksums) is now supported.
Refined MD5 command support slgihtly.
This is based on:
http://www.ietf.org/internet-drafts/draft-twine-ftpmd5-00.txt
Rev 1.12 1/24/2003 6:07:24 PM BGooijen
Changed TIdDataChannelThread to TIdDataChannelContext
Rev 1.11 1/23/2003 9:06:26 PM BGooijen
changed the CommandAbor
Rev 1.10 1/23/2003 10:39:38 AM BGooijen
TIdDataChannelContext.FServer was never assigned
Rev 1.9 1/20/2003 1:15:40 PM BGooijen
Changed to TIdTCPServer / TIdCmdTCPServer classes
Rev 1.8 1/17/2003 06:21:02 PM JPMugaas
Now works with new design.
Rev 1.7 1/17/2003 05:28:42 PM JPMugaas
Rev 1.6 1-9-2003 14:45:30 BGooijen
Added ABOR command with telnet escape characters
Fixed hanging of ABOR command
STOR and STOU now use REST-position
ABOR now returns 226 instead of 200
Rev 1.5 1-9-2003 14:35:52 BGooijen
changed TIdFTPServerContext(ASender.Context.Thread) to
TIdFTPServerContext(ASender.Context) on some places
Rev 1.4 1/9/2003 06:08:10 AM JPMugaas
Updated to be based on IdContext.
Rev 1.3 1-1-2003 20:13:06 BGooijen
Changed to support the new TIdContext class
Rev 1.2 12-15-2002 21:15:46 BGooijen
IFDEF-ed all SSL code, the IFDEF-s are removed as soon as the SSL works again.
Rev 1.1 11/14/2002 02:55:58 PM JPMugaas
FEAT and MLST now completely use the RFC Reply objects instead of
Connection.WriteLn. The Connection.WriteLn was a workaround for a deficit in
the original RFC Reply object. The workaround is no longer needed.
}
unit IdFTPServer;
{
Original Author: Sergio Perry
Date: 04/21/2001
Fixes and modifications: Doychin Bondzhev
Date: 08/10/2001
Further Extensive changes by Chad Z. Hower (Kudzu)
EPSV/EPRT support for IPv6 by Johannes Berg
TODO:
both EPSV and EPRT only allow data connections that have the same
protocol as the control connection, because the ftp server could be
used in a network only supporting one of them
TODO:
Change events to use DoXXXX
}
interface
{$i IdCompilerDefines.inc}
uses
Classes,
IdAssignedNumbers, IdCommandHandlers, IdGlobal, IdContext, IdException,
IdExplicitTLSClientServerBase, IdFTPBaseFileSystem, IdFTPCommon,
IdBaseComponent, IdFTPList, IdFTPListOutput, IdFTPServerContextBase,
IdReply, IdReplyFTP, IdReplyRFC, IdScheduler, IdServerIOHandler,
IdTCPConnection, IdCmdTCPServer, IdTCPServer, IdThread, IdUserAccounts,
IdYarn, IdZLibCompressorBase, SysUtils;
type
TIdFTPDirFormat = (ftpdfDOS, ftpdfUnix, ftpdfEPLF, ftpdfCustom, ftpdfOSDependent);
TIdFTPPathProcessing = (ftppDOS, ftppUnix, ftpOSDependent, ftppCustom);
TIdFTPOperation = (ftpRetr, ftpStor);
TIdMLSDAttr = (mlsdUniqueID,
mlsdPerms,
mlsdUnixModes,
mlsdUnixOwner,
mlsdUnixGroup,
mlsdFileCreationTime,
mlsdFileLastAccessTime,
mlsdWin32Attributes,
mlsdWin32DriveType,
mlstWin32DriveLabel);
TIdMLSDAttrs = set of TIdMLSDAttr;
const
DEF_DIRFORMAT = ftpdfUnix; //ftpdfOSDependent;
Id_DEF_AllowAnon = False;
Id_DEF_PassStrictCheck = True;
DEF_FTP_IMPLICIT_FTP = False;
DEF_FTP_HIDE_INVALID_USER = True;
DEF_FTP_PASSWORDATTEMPTS = 3;
DEF_FTP_INVALIDPASS_DELAY = 3000; //3 seconds
DEF_FTP_PASV_SAME_IP = True;
DEF_FTP_PORT_SAME_IP = True;
DEF_FTP_NO_RESERVED_PORTS = True;
DEF_FTP_BLOCK_ALL_PORTS = False;
DEF_FTP_DISABLE_SYST = False;
DEF_FTP_DISABLE_STAT = False;
DEF_FTP_PERMIT_CCC = False;
DEF_FTP_REPORT_EX_MSG = False;
DEF_PASV_BOUND_MIN = 0;
DEF_PASV_BOUND_MAX = 0;
DEF_PATHPROCESSING = ftpOSDependent;
{Do not change these as it could break some clients}
SYST_ID_UNIX = 'UNIX Type: L8'; {Do not translate}
SYST_ID_NT = 'Windows_NT'; {Do not translate}
const AAlwaysValidOpts : array [0..2] of string =
('SIZE', 'TYPE', 'MODIFY'); {Do not translate}
type
TIdFTPServerContext = class;
//The final parameter could've been one item but I decided against that
//because occaisionally, you might have a situation where you need to specify
//the "type" fact to be several different things.
//
//http://www.ietf.org/internet-drafts/draft-ietf-ftpext-mlst-16.txt
TIdOnMLST = procedure(ASender : TIdFTPServerContext; const APath: TIdFTPFileName;
ADirectoryListing: TIdFTPListOutput) of object;
//data port binding events
TOnDataPortBind = procedure(ASender : TIdFTPServerContext) of object;
//note that the CHMOD value is now a VAR because we also want to support a "MFF UNIX.mode="
//to do the same thing as a chmod. MFF is to "Modify a file fact".
TOnSetATTRIB = procedure(ASender: TIdFTPServerContext; var VAttr : UInt32; const AFileName : TIdFTPFileName; var VAUth : Boolean) of object;
//Note that VAuth : Boolean is used because you may want to deny permission for
//users to change their Unix permissions or UMASK - which is done in anonymous FTP
TOnSiteUMASK = procedure(ASender: TIdFTPServerContext; var VUMASK : Integer; var VAUth : Boolean) of object;
//note that the CHMOD value is now a VAR because we also want to support a "MFF UNIX.mode="
//to do the same thing as a chmod. MFF is to "Modify a file fact".
TOnSiteCHMOD = procedure(ASender: TIdFTPServerContext; var APermissions : Integer; const AFileName : TIdFTPFileName; var VAUth : Boolean) of object;
//chown as an option can specify group
TOnSiteCHOWN = procedure(ASender: TIdFTPServerContext; var AOwner, AGroup : String; const AFileName : TIdFTPFileName; var VAUth : Boolean) of object;
TOnSiteCHGRP = procedure(ASender: TIdFTPServerContext; var AGroup : String; const AFileName : TIdFTPFileName; var VAUth : Boolean) of object;
TOnCustomPathProcess = procedure(ASender: TIdFTPServerContext; var VPath : TIdFTPFileName) of object;
//
TOnFTPUserLoginEvent = procedure(ASender: TIdFTPServerContext; const AUsername, APassword: string;
var AAuthenticated: Boolean) of object;
TOnFTPUserAccountEvent = procedure(ASender : TIdFTPServerContext; const AUsername, APassword,AAcount: string; var AAuthenticated: Boolean) of object;
TOnAfterUserLoginEvent = procedure(ASender: TIdFTPServerContext) of object;
TOnDirectoryEvent = procedure(ASender: TIdFTPServerContext; var VDirectory: TIdFTPFileName) of object;
TOnGetFileSizeEvent = procedure(ASender: TIdFTPServerContext; const AFilename: TIdFTPFileName;
var VFileSize: Int64) of object;
TOnGetFileDateEvent = procedure(ASender: TIdFTPServerContext; const AFilename: TIdFTPFileName;
var VFileDate: TDateTime) of object;
//note we have to use a switches parameter because LIST in practice can have both a path and some
//some switches such as -R for recursive.
TOnListDirectoryEvent = procedure(ASender: TIdFTPServerContext; const APath: TIdFTPFileName;
ADirectoryListing: TIdFTPListOutput; const ACmd : String; const ASwitches : String) of object;
TOnCustomListDirectoryEvent = procedure(ASender: TIdFTPServerContext; const APath: TIdFTPFileName;
ADirectoryListing: TStrings; const ACmd : String; const ASwitches : String) of object;
TOnFileEvent = procedure(ASender: TIdFTPServerContext; const APathName: TIdFTPFileName) of object;
TOnCheckFileEvent = procedure(ASender: TIdFTPServerContext; const APathName: TIdFTPFileName; var VExist : Boolean) of object;
TOnRenameFileEvent = procedure(ASender: TIdFTPServerContext; const ARenameFromFile, ARenameToFile: TIdFTPFileName) of object;
TOnRetrieveFileEvent = procedure(ASender: TIdFTPServerContext; const AFileName: TIdFTPFileName;
var VStream: TStream) of object;
TOnStoreFileEvent = procedure(ASender: TIdFTPServerContext; const AFileName: TIdFTPFileName;
AAppend: Boolean; var VStream: TStream) of object;
TOnCombineFiles = procedure(ASender: TIdFTPServerContext; const ATargetFileName: TIdFTPFileName;
AParts : TStrings) of object;
TOnCheckSumFile = procedure(ASender: TIdFTPServerContext; const AFileName : TIdFTPFileName; var VStream : TStream) of object;
TOnCacheChecksum = procedure(ASender: TIdFTPServerContext; const AFileName : TIdFTPFileName; var VCheckSum : String) of object;
TOnVerifyChecksum = procedure(ASender: TIdFTPServerContext; const AFileName : TIdFTPFileName; const ACheckSum : String) of object;
TOnSetFileDateEvent = procedure(ASender: TIdFTPServerContext; const AFileName : TIdFTPFileName; var AFileTime : TDateTime) of object;
TOnHostCheck = procedure(ASender:TIdFTPServerContext; const AHost : String; var VAccepted : Boolean) of object;
//This is just to be efficient with the SITE UTIME command and for setting the windows.lastaccesstime fact
TOnSiteUTIME = procedure(ASender: TIdFTPServerContext; const AFileName : TIdFTPFileName;
var VLastAccessTime, VLastModTime, VCreateDate : TDateTime;
var VAUth : Boolean) of object;
EIdFTPServerException = class(EIdException);
EIdFTPServerNoOnListDirectory = class(EIdFTPServerException);
EIdFTPImplicitTLSRequiresSSL = class(EIdFTPServerException);
EIdFTPBoundPortMaxGreater = class(EIdFTPServerException);
EIdFTPBoundPortMinLess = class(EIdFTPServerException);
EIdFTPCannotBeNegative = class(EIdFTPServerException);
//we don't parse CLNT parameters as they might be freeform for all we know
TIdOnClientID = procedure(ASender: TIdFTPServerContext; const AID : String) of object;
TIdOnFTPStatEvent = procedure(ASender: TIdFTPServerContext; AStatusInfo : TStrings) of object;
TIdOnBanner = procedure(ASender: TIdFTPServerContext; AGreeting : TIdReply) of object;
//This is for EPSV and PASV support - do not change the values unless you
//have an extremely compelling reason to do so. This even is ONLY for those compelling case.
TIdOnPASV = procedure(ASender: TIdFTPServerContext; var VIP : String;
var VPort : TIdPort; const AIPVer : TIdIPVersion) of object;
TIdOnPASVRange = procedure(ASender: TIdFTPServerContext; var VIP : String;
var VPortMin, VPortMax : TIdPort; const AIPVer : TIdIPVersion) of object;
TIdOnDirSizeInfo = procedure(ASender : TIdFTPServerContext;
const APathName : TIdFTPFileName;
var VIsAFile : Boolean; var VSpace : Int64) of object;
TIdFTPServer = class;
TIdFTPSecurityOptions = class(TPersistent)
protected
// RFC 2577 Recommends these
// Note that the current code already hides user ID's by
// only authenticating after the password
FPasswordAttempts : UInt32;
FInvalidPassDelay : UInt32;
// http://cr.yp.to/ftp/security.html Recommends these
FRequirePASVFromSameIP : Boolean;
FRequirePORTFromSameIP : Boolean;
FNoReservedRangePORT : Boolean;
FBlockAllPORTTransfers : Boolean;
FDisableSYSTCommand : Boolean;
FDisableSTATCommand : Boolean;
FPermitCCC : Boolean;
public
constructor Create; virtual;
procedure Assign(Source: TPersistent); override;
published
//limit login attempts - some hackers will try guessing passwords from a dictionary
property PasswordAttempts : UInt32 read FPasswordAttempts write FPasswordAttempts
default DEF_FTP_PASSWORDATTEMPTS;
//should slow-down a password guessing attack - note those dictionaries
property InvalidPassDelay : UInt32 read FInvalidPassDelay write FInvalidPassDelay
default DEF_FTP_INVALIDPASS_DELAY;
//client IP Address is the only one that we will accept a PASV
//transfer from
//http://cr.yp.to/ftp/security.html
property RequirePASVFromSameIP : Boolean read FRequirePASVFromSameIP write FRequirePASVFromSameIP
default DEF_FTP_PASV_SAME_IP;
//Accept port transfers from the same IP address as the client -
//should prevent bounce attacks
property RequirePORTFromSameIP : Boolean read FRequirePORTFromSameIP write FRequirePORTFromSameIP
default DEF_FTP_PORT_SAME_IP;
//Do not accept port requests to ports in the reserved range. That is dangerous on some systems
property NoReservedRangePORT : Boolean read FNoReservedRangePORT write FNoReservedRangePORT
default DEF_FTP_NO_RESERVED_PORTS;
//Do not accept any PORT transfers at all. This is a little extreme but reduces troubles further.
//This will break the the Win32 console clients and a number of other programs.
property BlockAllPORTTransfers : Boolean read FBlockAllPORTTransfers write FBlockAllPORTTransfers
default DEF_FTP_BLOCK_ALL_PORTS;
//Disable SYST command. SYST usually gives the system description.
//Disabling it may make it harder for a trouble maker to know about your computer
//but will not be a complete security solution. See http://www.sans.org/rr/infowar/fingerprint.php for details
//On the other hand, disabling it will break RFC 959 complience and may break some FTP programs.
property DisableSYSTCommand : Boolean read FDisableSYSTCommand write FDisableSYSTCommand
default DEF_FTP_DISABLE_SYST;
//Disable STAT command. STAT gives freeform information about the connection status.
// http://www.sans.org/rr/infowar/fingerprint.php advises administrators to disable this
//because servers tend to give distinct patterns of information and some trouble makers
//can figure out what type of server you are running simply with this.
property DisableSTATCommand : Boolean read FDisableSTATCommand write FDisableSTATCommand
default DEF_FTP_DISABLE_STAT;
//Permit CCC (Clear Command Connection) in TLS FTP
property PermitCCC : Boolean read FPermitCCC write FPermitCCC default DEF_FTP_PERMIT_CCC;
end;
TIdDataChannel = class(TObject)
protected
FNegotiateTLS : Boolean;
FControlContext: TIdFTPServerContext;
FDataChannel: TIdTCPConnection;
FErrorReply: TIdReplyRFC;
FFtpOperation: TIdFTPOperation;
FOKReply: TIdReplyRFC;
FReply: TIdReplyRFC;
FServer : TIdFTPServer;
FRequirePASVFromSameIP : Boolean;
FStopped : Boolean;
FData : TObject;
procedure SetErrorReply(const AValue: TIdReplyRFC);
procedure SetOKReply(const AValue: TIdReplyRFC);
function GetPeerIP: String;
function GetPeerPort: TIdPort;
function GetLocalIP: String;
function GetLocalPort: TIdPort;
public
constructor Create(APASV: Boolean; AControlContext: TIdFTPServerContext; const ARequirePASVFromSameIP : Boolean; AServer : TIdFTPServer); reintroduce;
destructor Destroy; override;
procedure InitOperation(const AConnectMode : Boolean = False);
property PeerIP : String read GetPeerIP;
property PeerPort : TIdPort read GetPeerPort;
property LocalIP : String read GetLocalIP;
property LocalPort : TIdPort read GetLocalPort;
property Stopped : Boolean read FStopped write FStopped;
property Data : TObject read FData write FData;
property Server : TIdFTPServer read FServer;
property OKReply: TIdReplyRFC read FOKReply write SetOKReply;
property ErrorReply: TIdReplyRFC read FErrorReply write SetErrorReply;
end;
TIdFTPServerContext = class(TIdFTPServerContextBase)
protected
FXAUTKey : UInt32;
FRESTPos: Integer;
FDataChannel : TIdDataChannel;
FAuthMechanism : String;
FCCC : Boolean; //flag for CCC issuance
FDataType: TIdFTPTransferType;
FDataMode : TIdFTPTransferMode;
FDataPort: TIdPort;
FDataProtBufSize : UInt32;
FDataStruct: TIdFTPDataStructure;
FPasswordAttempts : UInt32;
FPASV: Boolean;
FEPSVAll: Boolean;
FDataPortDenied : Boolean;
FDataProtection : TIdFTPDataPortSecurity;
FDataPBSZCalled : Boolean;
FMLSOpts : TIdFTPFactOutputs;
FSSCNOn : Boolean;
FServer : TIdFTPServer;
FUserSecurity : TIdFTPSecurityOptions;
FUMask : Integer; //for SITE UMASK command
//only used for Windows NT imitation
FMSDOSMode : Boolean; //False - off imitate Unix, //True - On imitate DOS
//This is a queued request to quite.
//if it's issued during a data transfer, we treat it as quit
//only after the request is completed.
FQuitReply : String;
//ZLib settings
FZLibCompressionLevel : Integer; //7
FZLibWindowBits : Integer; //-15
FZLibMemLevel : Integer; //8
FZLibStratagy : Integer; //0 - default
//
procedure ResetZLibSettings;
procedure PortOnAfterBind(ASender : TObject);
procedure PortOnBeforeBind(ASender : TObject);
procedure SetUserSecurity(const Value: TIdFTPSecurityOptions);
procedure CreateDataChannel(APASV: Boolean = False);
function IsAuthenticated(ASender: TIdCommand): Boolean;
procedure ReInitialize; override;
public
constructor Create(AConnection: TIdTCPConnection; AYarn: TIdYarn; AList: TIdContextThreadList = nil); override;
destructor Destroy; override;
procedure KillDataChannel;
property DataChannel : TIdDataChannel read FDataChannel;
property Server : TIdFTPServer read FServer write FServer;
property UserSecurity : TIdFTPSecurityOptions read FUserSecurity write SetUserSecurity;
//
//This is for tracking what AUTH mechanism was specified and that
//we support. This may not matter as much now, but it could later on
//RFC 2228
property AuthMechanism : String read FAuthMechanism write FAuthMechanism;
property DataType: TIdFTPTransferType read FDataType write FDataType;
property DataMode : TIdFTPTransferMode read FDataMode write FDataMode;
property DataPort: TIdPort read FDataPort;
//We do not use this much for now but if more AUTH mechanisms are added,
//we may need this property
property DataProtBufSize : UInt32 read FDataProtBufSize write FDataProtBufSize;
property DataPBSZCalled : Boolean read FDataPBSZCalled write FDataPBSZCalled;
property DataStruct: TIdFTPDataStructure read FDataStruct write FDataStruct;
//currently, only <C>lear and <P>rivate are used. This could change
//later on
property DataProtection : TIdFTPDataPortSecurity read FDataProtection write FDataProtection;
property PasswordAttempts : UInt32 read FPasswordAttempts write FPasswordAttempts;
property PASV: Boolean read FPASV write FPASV;
property RESTPos: Integer read FRESTPos write FRESTPos;
property MLSOpts : TIdFTPFactOutputs read FMLSOpts write FMLSOpts;
//SSCN secure FTPX - http://www.raidenftpd.com/kb/kb000000037.htm
property SSCNOn : Boolean read FSSCNOn write FSSCNOn;
//SITE DIRSTYLE flag - true for MSDOS, false for Unix
property MSDOSMode : Boolean read FMSDOSMode write FMSDOSMode;
//SITE UMASK settings
property UMask : Integer read FUMask write FUMask;
//ZLib settings
property ZLibCompressionLevel : Integer read FZLibCompressionLevel write FZLibCompressionLevel; //7
property ZLibWindowBits : Integer read FZLibWindowBits write FZLibWindowBits; //-15
property ZLibMemLevel : Integer read FZLibMemLevel write FZLibMemLevel; //8
property ZLibStratagy : Integer read FZLibStratagy write FZLibStratagy; //0 - default
end;
TIdOnGetCustomListFormat = procedure(ASender: TIdFTPServer; AItem: TIdFTPListItem;
var VText: string) of object;
{ FTP Server }
TIdFTPServer = class(TIdExplicitTLSServer)
protected
FSupportXAUTH: Boolean;
FDirFormat : TIdFTPDirFormat;
FPathProcessing : TIdFTPPathProcessing;
FOnClientID : TIdOnClientID;
FDataChannelCommands: TIdCommandHandlers;
FSITECommands: TIdCommandHandlers;
FOPTSCommands: TIdCommandHandlers;
FMLSDFacts : TIdMLSDAttrs;
FAnonymousAccounts: TStrings;
FAllowAnonymousLogin: Boolean;
FAnonymousPassStrictCheck: Boolean;
// FEmulateSystem: TIdFTPSystems;
FPASVBoundPortMin : TIdPort;
FPASVBoundPortMax : TIdPort;
FSystemType: string;
FDefaultDataPort : TIdPort;
{$IFDEF USE_OBJECT_ARC}[Weak]{$ENDIF} FUserAccounts: TIdCustomUserManager;
FOnUserAccount : TOnFTPUserAccountEvent;
FOnAfterUserLogin: TOnAfterUserLoginEvent;
FOnUserLogin: TOnFTPUserLoginEvent;
FOnChangeDirectory: TOnDirectoryEvent;
FOnGetFileSize: TOnGetFileSizeEvent;
FOnGetFileDate:TOnGetFileDateEvent;
FOnListDirectory: TOnListDirectoryEvent;
FOnCustomListDirectory : TOnCustomListDirectoryEvent;
FOnRenameFile: TOnRenameFileEvent;
FOnDeleteFile: TOnFileEvent;
FOnRetrieveFile: TOnRetrieveFileEvent;
FOnStoreFile: TOnStoreFileEvent;
FOnMakeDirectory: TOnDirectoryEvent;
FOnRemoveDirectory: TOnDirectoryEvent;
FOnStat : TIdOnFTPStatEvent;
FFTPSecurityOptions : TIdFTPSecurityOptions;
FOnCRCFile : TOnCheckSumFile;
FOnCombineFiles : TOnCombineFiles;
FOnSetModifiedTime : TOnSetFileDateEvent;
FOnFileExistCheck : TOnCheckFileEvent; //for MDTM variation to set the file time
FOnSetCreationTime : TOnSetFileDateEvent;
FOnMD5Cache : TOnCacheChecksum;
FOnMD5Verify : TOnVerifyChecksum;
FOnGreeting : TIdOnBanner;
FOnLoginSuccessBanner : TIdOnBanner;
FOnLoginFailureBanner : TIdOnBanner;
FOnQuitBanner : TIdOnBanner;
FOnSetATTRIB : TOnSetATTRIB;
FOnSiteUMASK : TOnSiteUMASK;
FOnSiteCHMOD : TOnSiteCHMOD;
FOnSiteCHOWN : TOnSiteCHOWN;
FOnSiteCHGRP : TOnSiteCHGRP;
FOnAvailDiskSpace : TIdOnDirSizeInfo;
FOnCompleteDirSize : TIdOnDirSizeInfo;
FOnRemoveDirectoryAll: TOnDirectoryEvent;
FOnCustomPathProcess : TOnCustomPathProcess;
FOnDataPortBeforeBind : TOnDataPortBind;
FOnDataPortAfterBind : TOnDataPortBind;
FOnPASVBeforeBind : TIdOnPASVRange;
FOnPASVReply : TIdOnPASV;
{$IFDEF USE_OBJECT_ARC}[Weak]{$ENDIF} FFTPFileSystem: TIdFTPBaseFileSystem;
FEndOfHelpLine : String;
FCustomSystID : String;
FReplyUnknownSITECommand : TIdReply;
FCompressor : TIdZLibCompressorBase;
FOnMLST : TIdOnMLST;
FOnSiteUTIME : TOnSiteUTIME;
FOnHostCheck : TOnHostCheck;
procedure SetOnUserAccount(AValue : TOnFTPUserAccountEvent);
procedure AuthenticateUser(ASender: TIdCommand);
function SupportTaDirSwitches(AContext : TIdFTPServerContext) : Boolean;
function IgnoreLastPathDelim(const APath : String) : String;
procedure DoOnPASVBeforeBind(ASender : TIdFTPServerContext; var VIP : String;
var VPortMin, VPortMax : TIdPort; const AIPVersion : TIdIPVersion);
procedure DoOnPASVReply(ASender : TIdFTPServerContext; var VIP : String;
var VPort : TIdPort; const AIPVersion : TIdIPVersion);
function InternalPASV(ASender: TIdCommand; var VIP : String;
var VPort: TIdPort; var VIPVersion : TIdIPVersion): Boolean;
function DoSysType(ASender : TIdFTPServerContext) : String;
function DoProcessPath(ASender : TIdFTPServerContext; const APath: TIdFTPFileName): TIdFTPFileName;
function FTPNormalizePath(const APath: String) : String;
function MLSFEATLine(const AFactMask : TIdMLSDAttrs; const AFacts : TIdFTPFactOutputs) : String;
function HelpText(Cmds : TStrings) : String;
function IsValidPermNumbers(const APermNos : String) : Boolean;
procedure SetRFCReplyFormat(AReply : TIdReply);
function CDUPDir(AContext : TIdFTPServerContext) : String;
procedure DisconUser(ASender: TIdCommand);
//command reply common code
procedure CmdNotImplemented(ASender : TIdCommand);
procedure CmdFileActionAborted(ASender : TIdCommand);
procedure CmdSyntaxError(AContext: TIdContext; ALine: string; const AReply : TIdReply = nil); overload;
procedure CmdSyntaxError(ASender : TIdCommand); overload;
procedure CmdInvalidParams(ASender: TIdCommand);
procedure CmdInvalidParamNum(ASender:TIdCommand);
//The http://www.potaroo.net/ietf/idref/draft-twine-ftpmd5/
//draft didn't specify 550 as an error. It said use 504.
procedure CmdTwineFileActionAborted(ASender : TIdCommand);
//success reply codes can vary amoung commands
procedure CmdCommandSuccessful(ASender: TIdCommand; const AReplyCode : Integer = 250);
//Command replies
procedure CommandQUIT(ASender:TIdCommand);
procedure CommandUSER(ASender: TIdCommand);
procedure CommandPASS(ASender: TIdCommand);
procedure CommandACCT(ASender: TIdCommand);
procedure CommandXAUT(ASender : TIdCommand);
procedure CommandCWD(ASender: TIdCommand);
procedure CommandCDUP(ASender: TIdCommand);
procedure CommandREIN(ASender: TIdCommand);
procedure CommandPORT(ASender: TIdCommand);
procedure CommandPASV(ASender: TIdCommand);
procedure CommandTYPE(ASender: TIdCommand);
procedure CommandSTRU(ASender: TIdCommand);
procedure CommandMODE(ASender: TIdCommand);
procedure CommandRETR(ASender: TIdCommand);
procedure CommandSSAP(ASender: TIdCommand);
procedure CommandALLO(ASender: TIdCommand);
procedure CommandREST(ASender: TIdCommand);
procedure CommandRNFR(ASender: TIdCommand);
procedure CommandRNTO(ASender: TIdCommand);
procedure CommandABOR(ASender: TIdCommand);
//AVBL from Streamlined FTP Command Extensions
// draft-peterson-streamlined-ftp-command-extensions-01.txt
procedure CommandAVBL(ASender: TIdCommand);
procedure CommandDELE(ASender: TIdCommand);
//DSIZ from Streamlined FTP Command Extensions
// draft-peterson-streamlined-ftp-command-extensions-01.txt
procedure CommandDSIZ(ASender : TIdCommand);
procedure CommandRMDA(ASender : TIdCommand);
procedure CommandRMD(ASender: TIdCommand);
procedure CommandMKD(ASender: TIdCommand);
procedure CommandPWD(ASender: TIdCommand);
procedure CommandLIST(ASender: TIdCommand);
procedure CommandSYST(ASender: TIdCommand);
procedure CommandSTAT(ASender: TIdCommand);
procedure CommandSIZE(ASender: TIdCommand);
procedure CommandFEAT(ASender: TIdCommand);
procedure CommandOPTS(ASender: TIdCommand);
procedure CommandAUTH(ASender: TIdCommand);
procedure CommandCCC(ASender: TIdCommand);
// rfc 2428:
procedure CommandEPSV(ASender: TIdCommand);
procedure CommandEPRT(ASender: TIdCommand);
//
procedure CommandMDTM(ASender: TIdCommand);
procedure CommandMFF(ASender: TIdCommand);
//
procedure CommandMD5(ASender: TIdCommand);
procedure CommandMMD5(ASender: TIdCommand);
//
procedure CommandPROT(ASender: TIdCommand);
procedure CommandPBSZ(ASender: TIdCommand);
procedure CommandMFMT(ASender: TIdCommand);
procedure CommandMFCT(ASender: TIdCommand);
procedure CommandMLSD(ASender: TIdCommand);
procedure CommandMLST(ASender: TIdCommand);
procedure CommandCheckSum(ASender: TIdCommand);
procedure CommandCOMB(ASender: TIdCommand);
procedure CommandCLNT(ASender: TIdCommand);
//SSCN Secure FTPX - http://www.raidenftpd.com/kb/kb000000037.htm
procedure CommandSSCN(ASender: TIdCommand);
//Informal - like PASV accept SSL is in client mode - used by FlashXP
procedure CommandCPSV(ASender: TIdCommand);
//Informal - like PASV except that only the port is communicated.
//
procedure CommandSPSV(ASender: TIdCommand);
procedure CommandHOST(ASender : TIdCommand);
procedure CommandSecRFC(ASender : TIdCommand); //stub for some commands in 2228
procedure CommandSITE(ASender: TIdCommand);
procedure CommandSiteHELP(ASender : TIdCommand);
//site commands - Unix
procedure CommandSiteUMASK(ASender : TIdCommand);
procedure CommandSiteCHMOD(ASender : TIdCommand);
//SITE CHOWN - supported by some Unix servers
procedure CommandSiteCHOWN(ASender : TIdCommand);
//SITE CHGRP - supported by some Unix servers
procedure CommandSiteCHGRP(ASender : TIdCommand);
//site commans - MS IIS
procedure CommandSiteDIRSTYLE(ASender : TIdCommand);
//used by FTP Voyager
procedure CommandSiteZONE(ASender : TIdCommand);
//supported by RaidenFTP - http://www.raidenftpd.com/kb/kb000000049.htm
procedure CommandSiteATTRIB(ASender : TIdCommand);
//McFTP client uses this to set the time stamps for a file.
procedure CommandSiteUTIME(ASender : TIdCommand);
// end site commands
procedure CommandOptsMLST(ASender : TIdCommand);
procedure CommandOptsMODEZ(ASender : TIdCommand);
procedure CommandOptsUTF8(ASender: TIdCommand);
procedure CommandHELP(ASender: TIdCommand);
//
procedure DoOnRenameFile(ASender: TIdFTPServerContext; const ARenameFromFile, ARenameToFile: string);
procedure DoOnDeleteFile(ASender: TIdFTPServerContext; const APathName: string);
procedure DoOnChangeDirectory(AContext: TIdFTPServerContext; var VDirectory: TIdFTPFileName);
procedure DoOnMakeDirectory(AContext: TIdFTPServerContext; var VDirectory: TIdFTPFileName);
procedure DoOnRemoveDirectory(AContext: TIdFTPServerContext; var VDirectory: TIdFTPFileName);
procedure DoOnGetFileSize(ASender: TIdFTPServerContext; const AFilename: string; var VFileSize: Int64);
procedure DoOnGetFileDate(ASender: TIdFTPServerContext; const AFilename: string; var VFileDate: TDateTime);
procedure DoOnSetModifiedTime(AContext: TIdFTPServerContext; const AFileName : String; var VDateTime: TDateTime); overload;
procedure DoOnSetModifiedTime(AContext: TIdFTPServerContext; const AFileName : String; var VDateTimeStr : String); overload;
procedure DoOnFileExistCheck(AContext: TIdFTPServerContext; const AFileName : String; var VExist : Boolean);
procedure DoOnSetModifiedTimeGMT(AContext: TIdFTPServerContext; const AFileName : String; var VDateTime: TDateTime);
procedure DoOnSetCreationTime(AContext: TIdFTPServerContext; const AFileName : String; var VDateTime: TDateTime); overload;
procedure DoOnSetCreationTime(AContext: TIdFTPServerContext; const AFileName : String; var VDateTimeStr : String); overload;
procedure DoOnSetCreationTimeGMT(AContext: TIdFTPServerContext; const AFileName : String; var VDateTime: TDateTime);
procedure DoOnCRCFile(ASender: TIdFTPServerContext; const AFileName : String; var VStream : TStream);
procedure DoOnMD5Verify(ASender: TIdFTPServerContext; const AFileName : String; const ACheckSum : String);
procedure DoOnMD5Cache(ASender: TIdFTPServerContext; const AFileName : String; var VCheckSum : String);
procedure DoOnCombineFiles(ASender: TIdFTPServerContext; const ATargetFileName: string; AParts : TStrings);
procedure DoOnSetATTRIB(ASender: TIdFTPServerContext; var VAttr : UInt32; const AFileName : String; var VAUth : Boolean);
procedure DoOnSiteUMASK(ASender: TIdFTPServerContext; var VUMASK : Integer; var VAUth : Boolean);
procedure DoOnSiteCHMOD(ASender: TIdFTPServerContext; var APermissions : Integer; const AFileName : String; var VAUth : Boolean);
procedure DoOnSiteCHOWN(ASender: TIdFTPServerContext; var AOwner, AGroup : String; const AFileName : String; var VAUth : Boolean);
procedure DoOnSiteCHGRP(ASender: TIdFTPServerContext; var AGroup : String; const AFileName : String; var VAUth : Boolean);
procedure SetUseTLS(AValue: TIdUseTLS); override;
procedure SetSupportXAUTH(AValue : Boolean);
procedure InitializeCommandHandlers; override;
procedure ListDirectory(ASender: TIdFTPServerContext; ADirectory: string;
ADirContents: TStrings; ADetails: Boolean; const ACmd : String = 'LIST';
const ASwitches : String = ''); {do not localize}
{$IFNDEF USE_OBJECT_ARC}
procedure Notification(AComponent: TComponent; Operation: TOperation); override;
{$ENDIF}
procedure SetAnonymousAccounts(const AValue: TStrings);
procedure SetUserAccounts(const AValue: TIdCustomUserManager);
procedure SetFTPSecurityOptions(const AValue: TIdFTPSecurityOptions);
procedure SetPASVBoundPortMax(const AValue: TIdPort);
procedure SetPASVBoundPortMin(const AValue: TIdPort);
procedure SetReplyUnknownSITECommand(AValue: TIdReply);
procedure SetSITECommands(AValue: TIdCommandHandlers);
procedure ThreadException(AThread: TIdThread; AException: Exception);
procedure SetFTPFileSystem(const AValue: TIdFTPBaseFileSystem);
function GetMD5Checksum(ASender : TIdFTPServerContext; const AFileName : String) : String;
//overrides from TIdTCPServer
procedure DoConnect(AContext:TIdContext); override;
procedure DoDisconnect(AContext:TIdContext); override;
procedure ContextCreated(AContext:TIdContext); override;
procedure DoOnDataPortBeforeBind(ASender : TIdFTPServerContext); virtual;
procedure DoDataChannelOperation(ASender: TIdCommand; const AConnectMode : Boolean = False);virtual;
procedure DoOnDataPortAfterBind(ASender : TIdFTPServerContext); virtual;
procedure DoOnCustomListDirectory(ASender: TIdFTPServerContext; const APath: string;
ADirectoryListing: TStrings; const ACmd : String; const ASwitches : String);
function GetReplyClass: TIdReplyClass; override;
function GetRepliesClass: TIdRepliesClass; override;
procedure InitComponent; override;
procedure DoReplyUnknownCommand(AContext: TIdContext; ALine: string); override;
// overriden so we can close active transfers during a shutdown
procedure DoTerminateContext(AContext: TIdContext); override;
//overriden so we can handle telnet sequences
function ReadCommandLine(AContext: TIdContext): string; override;
public
destructor Destroy; override;
property SupportXAUTH : Boolean read FSupportXAUTH write SetSupportXAUTH;
published
{This is an object that can compress and decompress HTTP Deflate encoding}
property Compressor : TIdZLibCompressorBase read FCompressor write FCompressor;
property CustomSystID : String read FCustomSystID write FCustomSystID;
property DirFormat : TIdFTPDirFormat read FDirFormat write FDirFormat default DEF_DIRFORMAT;
property PathProcessing : TIdFTPPathProcessing read FPathProcessing write FPathProcessing default DEF_PATHPROCESSING;
property UseTLS;
property DefaultPort default IDPORT_FTP;
property AllowAnonymousLogin: Boolean read FAllowAnonymousLogin write FAllowAnonymousLogin default Id_DEF_AllowAnon;
property AnonymousAccounts: TStrings read FAnonymousAccounts write SetAnonymousAccounts;
property AnonymousPassStrictCheck: Boolean read FAnonymousPassStrictCheck
write FAnonymousPassStrictCheck default Id_DEF_PassStrictCheck;
property DefaultDataPort : TIdPort read FDefaultDataPort write FDefaultDataPort default IdPORT_FTP_DATA;
property FTPFileSystem:TIdFTPBaseFileSystem read FFTPFileSystem write SetFTPFileSystem;
property FTPSecurityOptions : TIdFTPSecurityOptions read FFTPSecurityOptions write SetFTPSecurityOptions;
property EndOfHelpLine : String read FEndOfHelpLine write FEndOfHelpLine;
property PASVBoundPortMin : TIdPort read FPASVBoundPortMin write SetPASVBoundPortMin default DEF_PASV_BOUND_MIN;
property PASVBoundPortMax : TIdPort read FPASVBoundPortMax write SetPASVBoundPortMax default DEF_PASV_BOUND_MAX;
property UserAccounts: TIdCustomUserManager read FUserAccounts write SetUserAccounts;
property SystemType: string read FSystemType write FSystemType;
property OnGreeting : TIdOnBanner read FOnGreeting write FOnGreeting;
property OnLoginSuccessBanner : TIdOnBanner read FOnLoginSuccessBanner write FOnLoginSuccessBanner;
property OnLoginFailureBanner : TIdOnBanner read FOnLoginFailureBanner write FOnLoginFailureBanner;
//for retreiving MD5 Checksums from a cache
property OnMD5Cache : TOnCacheChecksum read FOnMD5Cache write FOnMD5Cache;
property OnMD5Verify : TOnVerifyChecksum read FOnMD5Verify write FOnMD5Verify;
property OnQuitBanner : TIdOnBanner read FOnQuitBanner write FOnQuitBanner;
property OnCustomListDirectory : TOnCustomListDirectoryEvent read FOnCustomListDirectory write FOnCustomListDirectory;
property OnCustomPathProcess : TOnCustomPathProcess read FOnCustomPathProcess write FOnCustomPathProcess;
property OnAfterUserLogin: TOnAfterUserLoginEvent read FOnAfterUserLogin write FOnAfterUserLogin;
property OnChangeDirectory: TOnDirectoryEvent read FOnChangeDirectory write FOnChangeDirectory;
property OnGetFileSize: TOnGetFileSizeEvent read FOnGetFileSize write FOnGetFileSize;
property OnGetFileDate: TOnGetFileDateEvent read FOnGetFileDate write FOnGetFileDate;
property OnUserLogin: TOnFTPUserLoginEvent read FOnUserLogin write FOnUserLogin;
property OnUserAccount : TOnFTPUserAccountEvent read FOnUserAccount write SetOnUserAccount;
property OnListDirectory: TOnListDirectoryEvent read FOnListDirectory write FOnListDirectory;
property OnDataPortBeforeBind : TOnDataPortBind read FOnDataPortBeforeBind write FOnDataPortBeforeBind;
property OnDataPortAfterBind : TOnDataPortBind read FOnDataPortAfterBind write FOnDataPortAfterBind;
property OnRenameFile: TOnRenameFileEvent read FOnRenameFile write FOnRenameFile;
property OnDeleteFile: TOnFileEvent read FOnDeleteFile write FOnDeleteFile;
property OnRetrieveFile: TOnRetrieveFileEvent read FOnRetrieveFile write FOnRetrieveFile;
property OnStoreFile: TOnStoreFileEvent read FOnStoreFile write FOnStoreFile;
property OnMakeDirectory: TOnDirectoryEvent read FOnMakeDirectory write FOnMakeDirectory;
property OnRemoveDirectory: TOnDirectoryEvent read FOnRemoveDirectory write FOnRemoveDirectory;
property OnStat : TIdOnFTPStatEvent read FOnStat write FOnStat;
property OnCombineFiles : TOnCombineFiles read FOnCombineFiles write FOnCombineFiles;
property OnCRCFile : TOnCheckSumFile read FOnCRCFile write FOnCRCFile;
property OnSetCreationTime : TOnSetFileDateEvent read FOnSetCreationTime write FOnSetCreationTime;
property OnSetModifiedTime : TOnSetFileDateEvent read FOnSetModifiedTime write FOnSetModifiedTime;
property OnFileExistCheck : TOnCheckFileEvent read FOnFileExistCheck write FOnFileExistCheck;
property OnHostCheck : TOnHostCheck read FOnHostCheck write FOnHostCheck;
property OnSetATTRIB : TOnSetATTRIB read FOnSetATTRIB write FOnSetATTRIB;
property OnSiteUMASK : TOnSiteUMASK read FOnSiteUMASK write FOnSiteUMASK;
property OnSiteCHMOD : TOnSiteCHMOD read FOnSiteCHMOD write FOnSiteCHMOD;
property OnSiteCHOWN : TOnSiteCHOWN read FOnSiteCHOWN write FOnSiteCHOWN;
property OnSiteCHGRP : TOnSiteCHGRP read FOnSiteCHGRP write FOnSiteCHGRP;
{
READ THIS!!!
Do not change values in the OnPASV event unless you have a compelling reason to do so.
In SPSV, the PORT is the only thing that can work because that's all which is
given as a reply. The server IP is the same one that the client connects to.
In EPSV, the PORT is the only thing that can work because that's all which is
given as a reply. The server IP is the same one that the client connects to.
}
property OnPASVBeforeBind : TIdOnPASVRange read FOnPASVBeforeBind write FOnPASVBeforeBind;
property OnPASVReply : TIdOnPASV read FOnPASVReply write FOnPASVReply;
property OnMLST : TIdOnMLST read FOnMLST write FOnMLST;
property OnSiteUTIME : TOnSiteUTIME read FOnSiteUTIME write FOnSiteUTIME;
property OnAvailDiskSpace : TIdOnDirSizeInfo read FOnAvailDiskSpace write FOnAvailDiskSpace;
property OnCompleteDirSize : TIdOnDirSizeInfo read FOnCompleteDirSize write FOnCompleteDirSize;
property SITECommands: TIdCommandHandlers read FSITECommands write SetSITECommands;
property MLSDFacts : TIdMLSDAttrs read FMLSDFacts write FMLSDFacts;
property OnClientID : TIdOnClientID read FOnClientID write FOnClientID;
property ReplyUnknownSITCommand: TIdReply read FReplyUnknownSITECommand write SetReplyUnknownSITECommand;
end;
{This is used internally for some Telnet sequence parsing}
type
TIdFTPTelnetState = (tsData, tsCheckCR, tsIAC, tsWill, tsDo, tsWont, tsDont,
tsNegotiate, tsNegotiateData, tsNegotiateIAC, tsInterrupt, tsInterruptIAC);
implementation
uses
{$IFDEF DOTNET}
{$IFDEF USE_INLINE}
System.Threading,
{$ENDIF}
{$ENDIF}
{$IFDEF USE_VCL_POSIX}
Posix.SysSelect,
Posix.SysTime,
{$ENDIF}
IdFIPS,
IdHash, IdHashCRC, IdHashMessageDigest, IdHashSHA, IdIOHandlerSocket,
IdResourceStringsProtocols, IdGlobalProtocols, IdSimpleServer, IdSSL,
IdIOHandlerStack, IdSocketHandle, IdStrings, IdTCPClient, IdEMailAddress,
IdStack, IdFTPListTypes;
const
//THese commands need some special treatment in the Indy 10 FTP Server help system
//as they will not always work
HELP_SPEC_CMDS : array [0..25] of string =
('SIZE','MDTM', {do not localize}
'AUTH','PBSZ','PROT','CCC','MIC','CONF','ENC', 'SSCN','CPSV', {do not localize}
'MFMT','MFF',
'MD5','MMD5','XCRC','XMD5','XSHA1','XSHA256','XSHA512', {do not localize}
'COMB','AVBL','DSIZ','RMDA','HOST','XAUT'); {do not localize}
//These commands must always be present even if not implemented
//alt help topics and superscripts should be used sometimes.
//These are mandated by RFC 1123
HELP_ALT_MD_CMD : array [0..17] of string =
('RETR', {do not localize}
'STOR','STOU', {do not localize}
'APPE', {do not localize}
'RNFR', 'RNTO', {do not localize}
'DELE', {do not localize}
'LIST','NLST', {do not localize}
'CWD','XCWD', {do not localize}
'CDUP','XCUP', {do not localize}
'RMD','XRMD', {do not localize}
'MKD', 'XMKD', {do not localize}
'SYST'); {do not localize}
HELP_ALT_MD_TP : array [0..17] of string =
('RETR (retrieve); unimplemented.', {do not localize}
'STOR (store); unimplemented.', {do not localize}
'STOU (store unique); unimplemented.', {do not localize}
'APPE (append); unimplemented.', {do not localize}
'RNFR (rename from); unimplemented.', {do not localize}
'RNTO (rename to); unimplemented.', {do not localize}
'DELE (delete); unimplemented.', {do not localize}
'LIST (list); unimplemented.', {do not localize}
'NLIST (name-list); unimplemented.', {do not localize}
'CWD (change working directory); unimplemented.', {do not localize}
'XCWD (change working directory); unimplemented.', {do not localize}
'CDUP (change to parent directory); unimplemented.', {do not localize}
'XCDUP (change to parent directory); unimplemented.', {do not localize}
'RMD (remove Directory); unimplemented.', {do not localize}
'XRMD (remove Directory); unimplemented.', {do not localize}
'MKD (make Directory); unimplemented.', {do not localize}
'XMKD (make Directory); unimplemented.', {do not localize}
'SYST (system); unimplemented.' {do not localize}
);
//SSCN, OPTS MODE Z EXTRA, and OPTS UTF8 states
OnOffStates : array [0..1] of string =
('ON', {do not localize}
'OFF' {do not localize}
);
const
//%s = host
//%n = xauth key
XAUTHBANNER = '%s X2 WS_FTP Server Compatible(%d)';
ACCT_HELP_DISABLED = 'ACCT (specify account); unimplemented.'; {do not localize}
ACCT_HELP_ENABLED = 'Syntax: ACCT <SP> <account-information> <CRLF>';
const
NLSTEncType: array[Boolean] of IdTextEncodingType = (encASCII, encUTF8);
function CalculateCheckSum(AHashClass: TIdHashClass; AStrm: TStream; ABeginPos, AEndPos: TIdStreamSize): String;
{$IFDEF USE_INLINE} inline; {$ENDIF}
var
LHash: TIdHash;
begin
LHash := AHashClass.Create;
try
Result := LHash.HashStreamAsHex(AStrm, ABeginPos, AEndPos-ABeginPos);
finally
LHash.Free;
end;
end;
procedure XAutGreeting(AContext: TIdContext; AGreeting : TIdReply; const AHostName : String);
{$IFDEF USE_INLINE} inline; {$ENDIF}
var
s : String;
begin
//for XAUT to work with WS-FTP Pro, you need a banner mentioning "WS_FTP Server"
//and that banner can only be one line in length.
s := IndyFormat(XAUTHBANNER,
[ GStack.HostName, (AContext as TIdFTPServerContext).FXAUTKey]) + ' '+AGreeting.Text.Text;
s := Fetch(s,CR);
s := Fetch(s,LF);
AGreeting.Text.Text := s;
end;
{ TIdFTPServer }
constructor TIdFTPServerContext.Create(AConnection: TIdTCPConnection; AYarn: TIdYarn;
AList: TIdContextThreadList = nil);
begin
inherited Create(AConnection, AYarn, AList);
FUserSecurity := TIdFTPSecurityOptions.Create;
//we don't initialize FCCC flag here because that shouldn't be cleared with implicit SSL
FCCC := False;
FDataMode := dmStream;
FMLSOpts := [ItemType, Modify, Size];
//no write permissions for group and others
FUMask := 22;
ResetZLibSettings;
ReInitialize;
end;
procedure TIdFTPServerContext.SetUserSecurity(const Value: TIdFTPSecurityOptions);
begin
FUserSecurity.Assign( Value);
end;
destructor TIdFTPServerContext.Destroy;
begin
KillDataChannel;
FreeAndNil(FUserSecurity);
inherited Destroy;
end;
procedure TIdFTPServerContext.CreateDataChannel(APASV: Boolean = False);
begin
KillDataChannel; //let the old one terminate
FDataChannel := TIdDataChannel.Create(APASV, Self, UserSecurity.RequirePASVFromSameIP, Server);
end;
procedure TIdFTPServerContext.KillDataChannel;
begin
if Assigned(FDataChannel) then begin
if not FDataChannel.Stopped then begin
FDataChannel.Stopped := True;
FDataChannel.FDataChannel.Disconnect(False);
end;
FreeAndNil(FDataChannel);
end;
end;
procedure TIdFTPServerContext.ReInitialize;
begin
inherited;
FDataType := ftASCII;
// FDataMode := dmStream;
FDataPort := 0;
FDataStruct := dsFile;
FPASV := False;
FEPSVAll := False;
FDataProtection := ftpdpsClear;
DataPBSZCalled := False;
FDataProtBufSize := 0;
end;
function TIdFTPServerContext.IsAuthenticated(ASender: TIdCommand): Boolean;
begin
Result := FAuthenticated;
if not Result then begin
ASender.Reply.SetReply(530, RSFTPUserNotLoggedIn);
end;
end;
{ TIdFTPServer }
procedure TIdFTPServer.InitComponent;
begin
inherited InitComponent;
HelpReply.Code := ''; //we will handle the help ourselves
FDataChannelCommands := TIdCommandHandlers.Create(Self, FReplyClass, ReplyTexts, ExceptionReply);
FSITECommands := TIdCommandHandlers.Create(Self, FReplyClass, ReplyTexts, ExceptionReply);
FOPTSCommands := TIdCommandHandlers.Create(Self, FReplyClass, ReplyTexts, ExceptionReply);
//inherited from TLS classes
FRegularProtPort := IdPORT_FTP;
FImplicitTLSProtPort := IdPORT_ftps;
//
FAnonymousAccounts := TStringList.Create;
// By default these user names will be treated as anonymous.
FAnonymousAccounts.Add('anonymous'); { do not localize }
FAnonymousAccounts.Add('ftp'); { do not localize }
FAnonymousAccounts.Add('guest'); { do not localize }
FAllowAnonymousLogin := Id_DEF_AllowAnon;
FAnonymousPassStrictCheck := Id_DEF_PassStrictCheck;
DefaultPort := IDPORT_FTP;
DefaultDataPort := IdPORT_FTP_DATA;
// FEmulateSystem := Id_DEF_SystemType;
Greeting.SetReply(220, RSFTPDefaultGreeting);
FContextClass := TIdFTPServerContext;
ReplyUnknownCommand.SetReply(500, 'Unknown Command'); {do not localize}
FReplyUnknownSITECommand := FReplyClass.Create(nil);
FReplyUnknownSITECommand.SetReply(500, 'Invalid SITE command.'); {do not localize}
FFTPSecurityOptions := TIdFTPSecurityOptions.Create;
FPASVBoundPortMin := DEF_PASV_BOUND_MIN;
FPASVBoundPortMax := DEF_PASV_BOUND_MAX;
FPathProcessing := DEF_PATHPROCESSING;
FDirFormat := DEF_DIRFORMAT;
end;
function TIdFTPServer.GetReplyClass: TIdReplyClass;
begin
Result := TIdReplyFTP;
end;
function TIdFTPServer.GetRepliesClass: TIdRepliesClass;
begin
Result := TIdRepliesFTP;
end;
procedure TIdFTPServer.CommandHELP(ASender: TIdCommand);
var
s : String;
LCmds : TStringList;
i : Integer;
LExp : String;
function ShouldShowCommand(const ACommand : String) : Boolean;
begin
Result := False;
case PosInStrArray(ACommand, HELP_SPEC_CMDS, False) of
-1 :
Result := True;
0 : //'SIZE'
if Assigned(FOnGetFileSize) then begin
Result := True;
end;
1 :// 'MDTM',
if Assigned(FOnGetFileDate) or Assigned(FTPFileSystem) then begin
Result := True;
end;
2 : // 'AUTH'
if (FUseTLS in ExplicitTLSVals) then begin
Result := True;
end;
3,4,5,6,7,8,9,10 : //'PBSZ','PROT', 'CCC','MIC','CONF','ENC','SSCN','CPSV',
if (FUseTLS <> utNoTLSSupport) then begin
Result := True;
end;
11,12 : // 'MFMT','MFF',
if Assigned(FOnSetModifiedTime) or Assigned(FTPFileSystem) then begin
Result := True;
end;
13,14, 15,16 : //'MD5','MMD5','XCRC','XMD5',
begin
Result := False;
if not GetFIPSMode then begin
if Assigned(FOnCRCFile) or Assigned(FTPFileSystem) then begin
Result := True;
end;
end;
end;
17 : // 'XSHA1',
if Assigned(FOnCRCFile) or Assigned(FTPFileSystem) then begin
Result := True;
end;
18 : //'XSHA256'
if (Assigned(FOnCRCFile) or Assigned(FTPFileSystem))
and TIdHashSHA256.IsAvailable then begin
Result := True;
end;
19 : //'XSHA512'
if (Assigned(FOnCRCFile) or Assigned(FTPFileSystem)) and
TIdHashSHA512.IsAvailable then begin
Result := True;
end;
20 : // 'COMB');
if Assigned(OnCombineFiles) or Assigned(FTPFileSystem) then begin
Result := True;
end;
21 : // AVBL
if Assigned(FOnAvailDiskSpace) then begin
Result := True;
end;
22 : // DSIZ
if Assigned(FOnCompleteDirSize) then begin
Result := True;
end;
23 : // RMDA
if Assigned(FOnRemoveDirectoryAll) then begin
Result := True;
end;
24 : // HOST
if Assigned( FOnHostCheck ) then begin
Result := True;
end;
25 : // XAUT
if (not GetFIPSMode) and Self.FSupportXAUTH then begin
Result := True;
end;
end;
end;
function IsNotImplemented(const ACommand : String; var VHelp : String) : Boolean;
var
idx : Integer;
begin
Result := False; //presume that the command is implemented
idx := PosInStrArray(ACommand, HELP_ALT_MD_CMD, False);
if idx = -1 then begin
Exit;
end;
case idx of
0 : // 'RETR'
begin
if (not Assigned(FOnRetrieveFile)) and (not Assigned(FFTPFileSystem)) then begin
Result := True;
end;
end;
1,2,3 : //'STOR','STOU', 'APPE',
begin
if (not Assigned(FOnStoreFile)) and (not Assigned(FFTPFileSystem)) then begin
Result := True;
end;
end;
4,5 : // 'RNFR', 'RNTO',
begin
if (not Assigned(FOnRenameFile)) and (not Assigned(FFTPFileSystem)) then begin
Result := True;
end;
end;
6 : // 'DELE',
begin
if (not Assigned(FOnDeleteFile)) and (not Assigned(FFTPFileSystem)) then begin
Result := True;
end;
end;
7,8 :// 'LIST','NLST',
begin
if (not Assigned(FOnListDirectory)) or
((FDirFormat = ftpdfCustom) and (not Assigned(OnCustomListDirectory))) then begin
Result := True;
end;
end;
9, 10, //'CWD','XCWD',
11, 12 : // 'CDUP','XCUP',
begin
if (not Assigned(FOnChangeDirectory)) and (not Assigned(FFTPFileSystem)) then begin
Result := True;
end;
end;
13, 14 : //'RMD','XRMD',
begin
if (not Assigned(FOnRemoveDirectory)) and (not Assigned(FFTPFileSystem)) then begin
Result := True;
end;
end;
15,16 : //'MKD', 'XMKD',
begin
if (not Assigned(FOnMakeDirectory)) and (not Assigned(FFTPFileSystem)) then begin
Result := True;
end;
end;
17 :// 'SYST',
begin
if (not Assigned(FOnMakeDirectory)) and (not Assigned(FFTPFileSystem)) then begin
Result := True;
end;
end;
end;
if Result then begin
LExp := HELP_ALT_MD_TP[idx];
end;
end;
begin
if ASender.Params.Count > 0 then begin
for i := 0 to CommandHandlers.Count-1 do begin
if TextIsSame(ASender.Params[0], CommandHandlers.Items[i].Command) then begin
if CommandHandlers.Items[i].HelpVisible and ShouldShowCommand(ASender.Params[0]) then begin
if IsNotImplemented(CommandHandlers.Items[i].Command, LExp) then begin
ASender.Reply.SetReply(214, LExp);
end else begin
ASender.Reply.SetReply(214, CommandHandlers.Items[i].Description.Text);
end;
end else begin
ASender.Reply.SetReply(502, IndyFormat(RSFTPCmdHelpNotKnown, [UpperCase(ASender.Params[0])]));
end;
Exit;
end;
end;
ASender.Reply.SetReply(502, IndyFormat(RSFTPCmdHelpNotKnown, [UpperCase(ASender.Params[0])]));
end else begin
s := RSFTPHelpBegining + EOL;
LCmds := TStringList.Create;
try
//
for i := 0 to CommandHandlers.Count -1 do begin
if CommandHandlers.Items[i].HelpVisible and ShouldShowCommand(CommandHandlers.Items[i].Command) then begin
if IsNotImplemented(CommandHandlers.Items[i].Command, LExp) then begin
LCmds.Add(CommandHandlers.Items[i].Command + '*'); {do not localize}
end else begin
LCmds.Add(CommandHandlers.Items[i].Command + CommandHandlers.Items[i].HelpSuperScript);
end;
end;
end;
LCmds.Sort;
s := s + HelpText(LCmds) + FEndOfHelpLine;
if FEndOfHelpLine = '' then begin
s := s + EOL; //prevent ugliness if last row out of alignment with the rest
end;
ASender.Reply.SetReply(214, s);
finally
FreeAndNil(LCmds);
end;
end;
end;
procedure TIdFTPServer.CommandHOST(ASender: TIdCommand);
var LTmp : String;
LValid : Boolean;
LContext : TIdFTPServerContext;
begin
LContext := TIdFTPServerContext(ASender.Context);
if Assigned(OnHostCheck) then begin
if LContext.Username <> '' then begin
ASender.Reply.SetReply(530, RSFTPNotAfterAuthentication );
Exit;
end;
if (ASender.Params.Count > 0) then begin
LTmp := ASender.Params[0];
if Copy(LTmp,1,1)='[' then begin
Delete(LTmp,1,1);
end;
LTmp := Fetch(LTmp,']');
LValid := False;
FOnHostCheck(LContext,LTmp,LValid);
if LValid then begin
LContext.Host := LTmp;
if Assigned(OnGreeting) then begin
OnGreeting(LContext,ASender.Reply);
end;
if ASender.Reply.NumericCode = 421 then begin
ASender.Disconnect := True;
end else begin
if not GetFIPSMode then begin
//setting the reply code number directly causes the text to be cleared
if FSupportXAUTH and (ASender.Reply.NumericCode = 220) then begin
XAutGreeting(LContext,ASender.Reply, LTmp);
end;
end;
ASender.Reply.SetReply(220,ASender.Reply.Text.Text);
end;
ASender.SendReply;
end else begin
ASender.Reply.SetReply(530,RSFTPHostNotFound);
end;
end;
end else begin
CmdSyntaxError(ASender);
end;
end;
procedure TIdFTPServer.InitializeCommandHandlers;
var
LCmd : TIdCommandHandler;
begin
inherited InitializeCommandHandlers;
//ACCESS CONTROL COMMANDS
//USER <SP> <username> <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'USER'; {Do not Localize}
LCmd.OnCommand := CommandUSER;
LCmd.Description.Text := 'Syntax: USER <sp> username'; {do not localize}
//PASS <SP> <password> <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'PASS'; {Do not Localize}
LCmd.OnCommand := CommandPASS;
LCmd.Description.Text := 'Syntax: PASS <sp> password'; {do not localize}
//ACCT <SP> <account-information> <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'ACCT'; {Do not Localize}
// LCMd.NormalReply.SetReply(502, IndyFormat(RSFTPCmdNotImplemented, ['ACCT'])); {do not localize}
LCmd.OnCommand := CommandACCT;
if Assigned(Self.FOnUserAccount) then begin
LCmd.HelpSuperScript := ''; //not supported
LCmd.Description.Text := ACCT_HELP_ENABLED;
end else begin
LCmd.HelpSuperScript := '*'; //not supported
LCmd.Description.Text := ACCT_HELP_DISABLED;
end;
// 'ACCT (specify account); unimplemented.'; {do not localize}
{
LCmd.NormalReply.SetReply(502, Format(RSFTPCmdNotImplemented, ['ACCT'])); {Do not Localize}
//CWD <SP> <pathname> <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'CWD'; {Do not Localize}
LCmd.OnCommand := CommandCWD;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: CWD [ <sp> directory-name ]'; {do not localize}
//CDUP <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'CDUP'; {Do not Localize}
LCmd.OnCommand := CommandCDUP;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: CDUP (change to parent directory)'; {do not localize}
//SMNT <SP> <pathname> <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'SMNT'; {Do not Localize}
LCmd.NormalReply.SetReply(502, RSFTPFileActionCompleted);//250 for success
LCmd.HelpSuperScript := '*';
LCmd.Description.Text := 'SMNT (structure mount); unimplemented.'; {do not localize}
//QUIT <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'QUIT'; {Do not Localize}
LCmd.OnCommand := CommandQUIT;
LCmd.Disconnect := True;
LCmd.NormalReply.SetReply(221, RSFTPQuitGoodby); {Do not Localize}
LCmd.Description.Text := 'Syntax: QUIT (terminate service)'; {do not localize}
//REIN <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'REIN'; {Do not Localize}
LCmd.OnCommand := CommandREIN;
LCmd.Description.Text := 'Syntax: REIN (reinitialize server state)'; {do not localize}
//PORT <SP> <host-port> <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'PORT'; {Do not Localize}
LCmd.OnCommand := CommandPORT;
LCmd.Description.Text := 'Syntax: PORT <sp> b0, b1, b2, b3, b4'; {do not localize}
//PASV <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'PASV'; {Do not Localize}
LCmd.OnCommand := CommandPASV;
LCmd.Description.Text := 'Syntax: PASV (set server in passive mode)'; {do not localize}
//P@SW <CRLF>
//This is for some routers that replace a PASV with a P@SW
//as part of a misguided attempt to add a feature.
//A router would do a replacement so a client would think that
//PASV wasn't supported and then the client would do a PORT command
//instead. That doesn't happen so this just caused the client not to work.
//See: http://www.gbnetwork.co.uk/smcftpd/
LCmd := CommandHandlers.Add;
LCmd.Command := 'P@SW'; {Do not Localize}
LCmd.OnCommand := CommandPASV;
LCmd.HelpVisible := False; //this is just a workaround
//TYPE <SP> <type-code> <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'TYPE'; {Do not Localize}
LCmd.OnCommand := CommandTYPE;
LCmd.Description.Text := 'Syntax: TYPE <sp> [ A | E | I | L ]'; {do not localize}
//STRU <SP> <structure-code> <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'STRU'; {Do not Localize}
LCmd.OnCommand := CommandSTRU;
LCmd.Description.Text := 'Syntax: STRU (specify file structure)'; {do not localize}
//MODE <SP> <mode-code> <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'MODE'; {Do not Localize}
LCmd.OnCommand := CommandMODE;
LCmd.ExceptionReply.NumericCode := 501;
LCmd.Description.Text := 'Syntax: MODE (specify transfer mode)'; {do not localize}
//FTP SERVICE COMMANDS
//RETR <SP> <pathname> <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'RETR'; {Do not Localize}
LCmd.OnCommand := CommandRETR;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: RETR <sp> file-name'; {do not localize}
//STOR <SP> <pathname> <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'STOR'; {Do not Localize}
LCmd.OnCommand := CommandSSAP;
LCmd.ExceptionReply.NumericCode := 551;
LCmd.Description.Text := 'Syntax: STOR <sp> file-name'; {do not localize}
//STOU <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'STOU'; {Do not Localize}
LCmd.OnCommand := CommandSSAP;
LCmd.ExceptionReply.NumericCode := 551;
LCmd.Description.Text := 'Syntax: STOU <sp> file-name'; {do not localize}
//APPE <SP> <pathname> <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'APPE'; {Do not Localize}
LCmd.OnCommand := CommandSSAP;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: APPE <sp> file-name'; {do not localize}
//ALLO <SP> <decimal-integer>
// [<SP> R <SP> <decimal-integer>] <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'ALLO'; {Do not Localize}
LCmd.OnCommand := CommandALLO;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: ALLO allocate storage (vacuously)'; {do not localize}
//REST <SP> <marker> <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'REST'; {Do not Localize}
LCmd.OnCommand := CommandREST;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: REST (restart command)'; {do not localize}
//RNFR <SP> <pathname> <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'RNFR'; {Do not Localize}
LCmd.OnCommand := CommandRNFR;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: RNFR <sp> file-name'; {do not localize}
//RNTO <SP> <pathname> <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'RNTO'; {Do not Localize}
LCmd.OnCommand := CommandRNTO;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: RNTO <sp> file-name'; {do not localize}
//ABOR <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'ABOR'; {Do not Localize}
LCmd.OnCommand := CommandABOR;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: ABOR (abort operation)'; {do not localize}
//DELE <SP> <pathname> <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'DELE'; {Do not Localize}
LCmd.OnCommand := CommandDELE;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: DELE <sp> file-name'; {do not localize}
// 'SMNT (structure mount); unimplemented.';
//RMD <SP> <pathname> <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'RMD'; {Do not Localize}
LCmd.OnCommand := CommandRMD;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: RMD <sp> path-name'; {do not localize}
//MKD <SP> <pathname> <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'MKD'; {Do not Localize}
LCmd.OnCommand := CommandMKD;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: MKD <sp> path-name'; {do not localize}
//PWD <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'PWD'; {Do not Localize}
LCmd.OnCommand := CommandPWD;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: PWD (return current directory)'; {do not localize}
//LIST [<SP> <pathname>] <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'LIST'; {Do not Localize}
LCmd.OnCommand := CommandLIST;
LCmd.ExceptionReply.NumericCode := 450;
LCmd.Description.Text := 'Syntax: LIST [ <sp> path-name ]'; {do not localize}
//NLST [<SP> <pathname>] <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'NLST'; {Do not Localize}
LCmd.OnCommand := CommandLIST;
LCmd.ExceptionReply.NumericCode := 450;
LCmd.Description.Text := 'Syntax: NLST [ <sp> path-name ]'; {do not localize}
//SITE <SP> <string> <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'SITE'; {Do not Localize}
LCmd.OnCommand := CommandSITE;
LCmd.ExceptionReply.NumericCode := 501;
LCmd.Description.Text := 'Syntax: SITE (site-specific commands)';
//SYST <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'SYST'; {Do not Localize}
LCmd.OnCommand := CommandSYST;
LCmd.ExceptionReply.NumericCode := 501;
LCmd.Description.Text := 'Syntax: SYST (get type of operating system)'; {do not localize}
//STAT [<SP> <pathname>] <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'STAT'; {Do not Localize}
LCmd.OnCommand := CommandSTAT;
LCmd.ExceptionReply.NumericCode := 450;
LCmd.Description.Text := 'Syntax: CWD [ <sp> directory-name ]'; {do not localize}
//NOOP <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'NOOP'; {Do not Localize}
LCmd.NormalReply.SetReply(200, IndyFormat(RSFTPCmdSuccessful, ['NOOP'])); {Do not Localize}
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: NOOP'; {do not localize}
//RFC 775
LCmd := CommandHandlers.Add;
LCmd.Command := 'XMKD'; {Do not Localize}
LCmd.OnCommand := CommandMKD;
LCmd.ExceptionReply.NumericCode := 551; //use the ones in parathensies
LCmd.Description.Text := 'Syntax: XMKD <sp> path-name'; {do not localize}
//XCWD <SP> <pathname> <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'XCWD'; {Do not Localize}
LCmd.OnCommand := CommandCWD;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: XCWD [ <sp> directory-name ]'; {do not localize}
LCmd := CommandHandlers.Add;
LCmd.Command := 'XRMD'; {Do not Localize}
LCmd.OnCommand := CommandRMD;
LCmd.ExceptionReply.NumericCode := 551; //use the ones in parathensies
LCmd.Description.Text := 'Syntax: XRMD <sp> path-name'; {do not localize}
LCmd := CommandHandlers.Add;
LCmd.Command := 'XPWD'; {Do not Localize}
LCmd.OnCommand := CommandPWD;
LCmd.ExceptionReply.NumericCode := 502;
LCmd.Description.Text := 'Syntax: PWD (return current directory)'; {do not localize}
LCmd := CommandHandlers.Add;
LCmd.Command := 'XCUP'; {Do not Localize}
LCmd.OnCommand := CommandCDUP;
LCmd.ExceptionReply.NumericCode := 551; //use the ones in parathensies
LCmd.Description.Text := 'Syntax: XCUP (change to parent directory)'; {do not localize}
//RFC 2389
LCmd := CommandHandlers.Add;
LCmd.Command := 'FEAT'; {Do not Localize}
LCmd.OnCommand := CommandFEAT;
SetRFCReplyFormat(LCmd.NormalReply);
LCmd.ExceptionReply.NumericCode := 501;
LCmd.Description.Text := 'Syntax: FEAT (returns feature list)'; {do not localize}
//RFC 2389
LCmd := CommandHandlers.Add;
LCmd.Command := 'OPTS'; {Do not Localize}
LCmd.OnCommand := CommandOPTS;
LCmd.ExceptionReply.NumericCode := 501;
LCmd.Description.Text := 'Syntax: OPTS <sp> command [<sp> options]'; {do not localize}
//SIZE [<FILE>] CRLF
LCmd := CommandHandlers.Add;
LCmd.Command := 'SIZE'; {Do not Localize}
LCmd.OnCommand := CommandSIZE;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: SIZE <sp> path-name'; {do not localize}
//EPSV [protocol] <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'EPSV'; {Do not Localize}
LCmd.OnCommand := CommandEPSV;
LCmd.ExceptionReply.NumericCode := 501;
LCmd.Description.Text := 'Syntax: EPSV (returns port |||port|)'; {do not localize}
//EPRT [address/port string] <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'EPRT'; {Do not Localize}
LCmd.OnCommand := CommandEPRT;
LCmd.ExceptionReply.NumericCode := 501;
LCmd.Description.Text := 'Syntax: EPRT <sp> |proto|addr|port|'; {do not localize}
//MDTM [<FILE>] <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'MDTM'; {Do not Localize}
LCmd.OnCommand := CommandMDTM;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: MDTM <sp> path-name'; {do not localize}
//RFC 2228
//AUTH [Mechanism] <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'AUTH'; {Do not translate}
LCmd.OnCommand := CommandAUTH;
LCmd.ExceptionReply.NumericCode := 501;
LCmd.Description.Text := 'Syntax: AUTH <sp> mechanism-name'; {do not localize}
//PBSZ [Protection Buffer Size] <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'PBSZ'; {Do not translate}
LCmd.OnCommand := CommandPBSZ;
LCmd.ExceptionReply.NumericCode := 501;
LCmd.Description.Text := 'Syntax: PBSZ <sp> protection buffer size'; {do not localize}
//PROT Protection Type <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'PROT'; {Do not translate}
LCmd.OnCommand := CommandPROT;
LCmd.ExceptionReply.NumericCode := 501;
LCmd.Description.Text := 'Syntax: PROT <sp> protection code'; {do not localize}
//CCC Clear Command Channel
LCmd := CommandHandlers.Add;
LCmd.Command := 'CCC'; {Do not translate}
LCmd.OnCommand := CommandCCC;
LCmd.Description.Text := 'Syntax: CCC (clear command channel)'; {do not localize}
//MIC Integrity Protected Command
LCmd := CommandHandlers.Add;
LCmd.Command := 'MIC'; {Do not translate}
LCmd.OnCommand := CommandSecRFC;
LCmd.HelpSuperScript := '*';
LCmd.Description.Text := 'MIC (integrity protected command); unimplemented.'; {do not localize}
//CONF Confidentiality protected command
LCmd := CommandHandlers.Add;
LCmd.Command := 'CONF'; {Do not translate}
LCmd.OnCommand := CommandSecRFC;
LCmd.HelpSuperScript := '*';
LCmd.Description.Text := 'CONF (confidentiality protected command); unimplemented.'; {do not localize}
//ENC Privacy Protected command
LCmd := CommandHandlers.Add;
LCmd.Command := 'ENC'; {Do not translate}
LCmd.OnCommand := CommandSecRFC;
LCmd.HelpSuperScript := '*';
LCmd.Description.Text := 'ENC (privacy protected command); unimplemented.'; {do not localize}
//These are from IETF Draft "Extensions to FTP"
//MLSD [Pathname] <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'MLSD'; {Do not translate}
LCmd.OnCommand := CommandMLSD;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: MLSD [ <sp> path-name ]'; {do not localize}
//MLST [Pathname] <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'MLST'; {Do not translate}
LCmd.OnCommand := CommandMLST;
SetRFCReplyFormat(LCmd.NormalReply);
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: MLST [ <sp> path-name ]'; {do not localize}
//Defined in http://www.trevezel.com/downloads/draft-somers-ftp-mfxx-00.html
//Modify File Modification Time
//MFMT [ATime] [Path-name]<CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'MFMT'; {Do not translate}
LCmd.OnCommand := CommandMFMT;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: MFMT [ATime] [Path-name]<CRLF>'; {do not localize}
//Defined in http://www.trevezel.com/downloads/draft-somers-ftp-mfxx-00.html
//Modify File Creation Time
//MFMT [ATime] [Pathname]<CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'MFCT'; {Do not translate}
LCmd.OnCommand := CommandMFCT;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: MFCT [ATime] [Path-name]'; {do not localize}
//params are the same format as the MLS output
LCmd := CommandHandlers.Add;
LCmd.Command := 'MFF'; {Do not translate}
LCmd.OnCommand := CommandMFF;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: MFF [ mff-facts ] SP path-name'; {do not localize}
//From http://www.ietf.org/internet-drafts/draft-twine-ftpmd5-00.txt
//MD5 [Pathname]
LCmd := CommandHandlers.Add;
LCmd.Command := 'MD5'; {Do not translate}
LCmd.OnCommand := CommandMD5;
LCmd.ExceptionReply.NumericCode := 504;
LCmd.Description.Text := 'Syntax: MD5 [Pathname]'; {do not localize}
//MMD5 [Filepath1], [Filepath2] [...]
LCmd := CommandHandlers.Add;
LCmd.Command := 'MMD5'; {Do not translate}
LCmd.OnCommand := CommandMMD5;
LCmd.ExceptionReply.NumericCode := 504;
LCmd.Description.Text := 'Syntax: MMD5 [Filepath1], [Filepath2] [...]'; {do not localize}
//These two commands are not in RFC's or drafts
// but are documented in:
// GlobalSCAPE Secure FTP Server User<65>s Guide
//XCRC "[filename]" [start] [finish]
LCmd := CommandHandlers.Add;
LCmd.Command := 'XCRC'; {Do not translate}
LCmd.OnCommand := CommandCheckSum;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: XCRC "[file-name]" [start] [finish]'; {do not localize}
//COMB "[filename]" [start] [finish]
LCmd := CommandHandlers.Add;
LCmd.Command := 'COMB'; {Do not translate}
LCmd.OnCommand := CommandCOMB;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: COMB "[file-name]" [start] [finish]'; {do not localize}
//informal but we might want to support this anyway
//SSCN - specified by:
//http://www.raidenftpd.com/kb/kb000000037.htm
LCmd := CommandHandlers.Add;
LCmd.Command := 'SSCN'; {Do not translate}
LCmd.OnCommand := CommandSSCN;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.NormalReply.NumericCode := 200;
LCmd.Description.Text := 'Syntax: SSCN [ON|OFF]'; {do not localize}
//CPSV <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'CPSV'; {Do not Localize}
LCmd.OnCommand := CommandCPSV;
LCmd.Description.Text := 'Syntax: CPSV (set server in passive mode with SSL Connect)'; {do not localize}
//Seen in RaidenFTPD documentation
//XCRC "[filename]" [start] [finish]
LCmd := CommandHandlers.Add;
LCmd.Command := 'XMD5'; {Do not translate}
LCmd.OnCommand := CommandCheckSum;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: XMD5 "[filename]" [start] [finish]'; {do not localize}
//Seen in RaidenFTPD documentation
//XCRC "[filename]" [start] [finish]
LCmd := CommandHandlers.Add;
LCmd.Command := 'XSHA1'; {Do not translate}
LCmd.OnCommand := CommandCheckSum;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: XSHA1 "[filename]" [start] [finish]'; {do not localize}
LCmd := CommandHandlers.Add;
LCmd.Command := 'XSHA256'; {Do not translate}
LCmd.OnCommand := CommandCheckSum;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'Syntax: XSHA256 "[filename]" [start] [finish]'; {do not localize}
LCmd := CommandHandlers.Add;
LCmd.Command := 'XSHA512'; {Do not translate}
LCmd.OnCommand := CommandCheckSum;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.HelpVisible := True;
LCmd.Description.Text := 'Syntax: XSHA512 "[filename]" [start] [finish]'; {do not localize}
//commands from
// draft-peterson-streamlined-ftp-command-extensions-01.txt
//http://tools.ietf.org/html/draft-peterson-streamlined-ftp-command-extensions-01#section-2.4
LCmd := CommandHandlers.Add;
LCmd.Command := 'AVBL'; {Do not localize}
LCmd.OnCommand := CommandAVBL;
LCmd.ExceptionReply.NumericCode := 500;
LCmd.Description.Text := 'Syntax: AVBL [<sp> dirpath] (returns the number of '+
'bytes available for uploading in the directory or current working directory)';
LCmd := CommandHandlers.Add;
LCmd.Command := 'DSIZ'; {Do not localize}
LCmd.OnCommand := CommandDSIZ;
LCmd.ExceptionReply.NumericCode := 500;
LCmd.Description.Text := 'DSIZ [<sp> dirpath] (returns the number of bytes '+
'in the directory or current working directory, including sub directories)';
LCmd := CommandHandlers.Add;
LCmd.Command := 'RMDA';
LCmd.OnCommand := CommandRMDA;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.Description.Text := 'RMDA <sp> pathname (deletes (removes) the '+
'specified directory and it s contents)';
//informal but we might want to support this anyway
//CLNT
LCmd := CommandHandlers.Add;
LCmd.Command := 'CLNT'; {do not localize}
LCmd.OnCommand := CommandCLNT;
LCmd.ExceptionReply.NumericCode := 550;
LCmd.NormalReply.SetReply(200, RSFTPClntNoted); {Do not Localize}
LCmd.Description.Text := 'Syntax: CLNT<space><clientname>'; {do not localize}
//Informal - an old proposed solution to IPv6 support in FTP.
//Mentioned at: http://cr.yp.to/ftp/retr.html
//and supported by PureFTPD.
LCmd := CommandHandlers.Add;
LCmd.Command := 'SPSV'; {do not localize}
LCmd.OnCommand := CommandSPSV;
LCmd.Description.Text := 'Syntax: SPSV (set server in passive mode)'; {do not localize}
LCmd := CommandHandlers.Add;
LCmd.Command := 'HOST'; {Do not localize}
LCmd.OnCommand := CommandHOST;
LCmd.ExceptionReply.NumericCode := 504;
LCmd.Description.Text := 'Syntax: HOST <sp> domain (select a domain prior to logging in)'; {Do not localize}
//Note that these commands are mentioned in old RFC's
//and we will not support them at all. The commands
//were there because FTP was a predisessor of SMTP
//These are from RFC 765
//MLFL [<SP> <ident>] <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'MLFL'; {Do not Localize}
LCmd.NormalReply.SetReply(502, IndyFormat(RSFTPCmdNotImplemented, ['MLFL'])); {Do not Localize}
LCmd.HelpSuperScript := '*';
LCmd.Description.Text := 'MLFL (mail file); unimplemented.'; {do not localize}
//MAIL [<SP> <ident>] <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'MAIL'; {Do not Localize}
LCmd.NormalReply.SetReply(502, IndyFormat(RSFTPCmdNotImplemented, ['MAIL'])); {Do not Localize}
LCmd.HelpSuperScript := '*';
LCmd.Description.Text := 'MAIL (mail to user); unimplemented.'; {do not localize}
// MSND [<SP> <ident>] <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'MSND'; {Do not Localize}
LCmd.NormalReply.SetReply(502, IndyFormat(RSFTPCmdNotImplemented, ['MSND'])); {Do not Localize}
LCmd.HelpSuperScript := '*';
LCmd.Description.Text := 'MSND (mail send to terminal); unimplemented.'; {do not localize}
// MSOM [<SP> <ident>] <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'MSOM'; {Do not Localize}
LCmd.NormalReply.SetReply(502, IndyFormat(RSFTPCmdNotImplemented, ['MSOM'])); {Do not Localize}
LCmd.HelpSuperScript := '*';
LCmd.Description.Text := 'MSOM (mail send to terminal or mailbox); unimplemented.'; {do not localize}
// MSAM [<SP> <ident>] <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'MSAM'; {Do not Localize}
LCmd.NormalReply.SetReply(502, IndyFormat(RSFTPCmdNotImplemented, ['MSAM'])); {Do not Localize}
LCmd.HelpSuperScript := '*';
LCmd.Description.Text := 'MSAM (mail send to terminal and mailbox); unimplemented.'; {do not localize}
// MRSQ [<SP> <scheme>] <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'MRSQ'; {Do not Localize}
LCmd.NormalReply.SetReply(502, IndyFormat(RSFTPCmdNotImplemented, ['MRSQ'])); {Do not Localize}
LCmd.HelpSuperScript := '*';
LCmd.Description.Text := 'MRSQ (mail recipient scheme question); unimplemented.'; {do not localize}
// MRCP <SP> <ident> <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'MRCP'; {Do not Localize}
LCmd.NormalReply.SetReply(502, IndyFormat(RSFTPCmdNotImplemented, ['MRCP'])); {Do not Localize}
LCmd.HelpSuperScript := '*';
LCmd.Description.Text := 'MRCP (mail recipient); unimplemented.'; {do not localize}
//
LCmd := CommandHandlers.Add;
LCmd.Command := 'HELP'; {Do not Localize}
LCmd.OnCommand := COmmandHELP;
LCmd.NormalReply.NumericCode := 214;
LCmd.Description.Text := 'Syntax: HELP [ <sp> <string> ]'; {do not localize}
//We use a separate command handler collection for some things which are
//valid durring the data connection.
//ABOR <CRLF>
LCmd := FDataChannelCommands.Add;
LCmd.Command := 'ABOR'; {Do not Localize}
LCmd.OnCommand := CommandABOR;
LCmd.ExceptionReply.NumericCode := 550;
//STAT [<SP> <pathname>] <CRLF>
LCmd := FDataChannelCommands.Add;
LCmd.Command := 'STAT'; {Do not Localize}
LCmd.OnCommand := CommandSTAT;
LCmd.ExceptionReply.NumericCode := 450;
//This is for SITE commands to make it easy for the user to add their own site commands
//as they wish
//These are Unix site commands
LCmd := FSITECommands.Add;
LCmd.Command := 'HELP'; {Do not localize}
LCmd.ExceptionReply.NumericCode := 501;
LCmd.OnCommand := CommandSiteHELP;
LCmd.Description.Text := 'Syntax: SITE HELP [ <sp> <string> ]'; {do not localize}
//SITE ATTRIB<SP>Attribs<SP>FileName<CRLF>
LCmd := FSITECommands.Add;
LCmd.Command := 'ATTRIB'; {Do not Localize}
LCmd.OnCommand := CommandSiteATTRIB;
LCmd.ExceptionReply.NumericCode := 501;
LCmd.Description.Text := 'Syntax: SITE ATTRIB<SP>Attribs<SP>Filename'; {do not localize}
//SITE UMASK<SP>[mask]
LCmd := FSITECommands.Add;
LCmd.Command := 'UMASK'; {Do not Localize}
LCmd.OnCommand := CommandSiteUMASK;
LCmd.ExceptionReply.NumericCode := 501;
LCmd.Description.Text := 'Syntax: SITE UMASK'; {do not localize}
//SITE CHMOD<SP>Permission numbers<SP>Filename<CRLF>
LCmd := FSITECommands.Add;
LCmd.Command := 'CHMOD'; {Do not Localize}
LCmd.OnCommand := CommandSiteCHMOD;
LCmd.ExceptionReply.NumericCode := 501;
LCmd.Description.Text := 'Syntax: SITE CHMOD<SP>Permission numbers<SP>Filename'; {do not localize}
//additional Unix server commands that aren't supported but should be supported, IMAO
//SITE CHOWN<SP>Owner[:Group]<SP>Filename<CRLF>
LCmd := FSITECommands.Add;
LCmd.Command := 'CHOWN'; {Do not Localize}
LCmd.OnCommand := CommandSiteCHOWN;
LCmd.ExceptionReply.NumericCode := 501;
LCmd.Description.Text := 'Syntax: SITE CHOWN<SP>Owner[:Group]<SP>Filename<CRLF>'; {do not localize}
//SITE CHGRP<SP>Group<SP>Filename<CRLF>
LCmd := FSITECommands.Add;
LCmd.Command := 'CHGRP'; {Do not Localize}
LCmd.OnCommand := CommandSiteCHGRP;
LCmd.ExceptionReply.NumericCode := 501;
LCmd.Description.Text := 'Syntax: SITE CHGRP<SP>Group<SP>Filename<CRLF>'; {do not localize}
//Microsoft IIS SITE commands
//SITE DIRSTYLE
LCmd := FSITECommands.Add;
LCmd.Command := 'DIRSTYLE'; {Do not Localize}
LCmd.ExceptionReply.NumericCode := 501;
LCmd.OnCommand := CommandSiteDIRSTYLE;
LCmd.Description.Text := 'Syntax: SITE DIRSTYLE (toggle directory format)'; {do not localize}
//SITE ZONE
LCmd := FSITECommands.Add;
LCmd.Command := 'ZONE'; {Do not localize}
LCmd.ExceptionReply.NumericCode := 530;
LCmd.OnCommand := CommandSiteZONE;
LCmd.Description.Text := 'Syntax: SITE ZONE (returns the server offset from GMT)'; {do not localize}
//SITE UTIME
LCmd := FSITECommands.Add;
LCmd.Command := 'UTIME'; {Do not localize}
LCmd.NormalReply.NumericCode := 200;
LCmd.NormalReply.Text.Text := 'Date/time changed okay.';
LCmd.ExceptionReply.NumericCode := 530;
LCmd.OnCommand := CommandSiteUTIME;
LCmd.Description.Text :=
'Syntax: SITE UTIME <file> <access-time> <modification-time> <creation time>'+CR+LF+ {do not localize}
' Each timestamp must be in the format YYYYMMDDhhmmss'; {do not localize}
//OPTS MLST
LCmd := FOPTSCommands.Add;
LCmd.Command := 'MLST'; {Do not localize}
LCmd.ExceptionReply.NumericCode := 501;
LCmd.OnCommand := CommandOptsMLST;
//OPTS MODE Z
LCmd := FOPTSCommands.Add;
LCmd.Command := 'MODE Z'; {Do not localize}
LCmd.ExceptionReply.NumericCode := 501;
LCmd.OnCommand := CommandOptsMODEZ;
// OPTS UTF-8 <NLST>
LCmd := FOPTSCommands.Add;
LCmd.Command := 'UTF-8'; {Do not localize}
LCmd.ExceptionReply.NumericCode := 501;
LCmd.NormalReply.NumericCode := 200;
LCmd.OnCommand := CommandOptsUTF8;
// OPTS UTF8 <ON|OFF>
LCmd := FOPTSCommands.Add;
LCmd.Command := 'UTF8'; {Do not localize}
LCmd.ExceptionReply.NumericCode := 501;
LCmd.NormalReply.NumericCode := 200;
LCmd.OnCommand := CommandOptsUTF8;
//XAUT <SP> <xor encrypted data> <CRLF>
LCmd := CommandHandlers.Add;
LCmd.Command := 'XAUT'; {Do not Localize}
LCmd.OnCommand := CommandXAUT;
LCmd.Description.Text := 'Syntax: XAUT <sp> 2 <sp> <encrypted username and password>'; {do not localize}
end;
procedure TIdFTPServer.ContextCreated(AContext: TIdContext);
var
LContext : TIdFTPServerContext;
begin
LContext := (AContext as TIdFTPServerContext);
LContext.Server := Self;
//from Before run method
LContext.FDataPort := 0;
LContext.FPasswordAttempts := 0;
LContext.FDataPortDenied := False;
LContext.FUserSecurity.Assign(FTPSecurityOptions);
if (DirFormat = ftpdfOSDependent) and (GOSType = otWindows) then begin
LContext.MSDOSMode := True;
end;
//
if mlsdUnixModes in FMLSDFacts then begin
LContext.MLSOpts := LContext.MLSOpts + [UnixMODE];
end;
if mlsdUnixOwner in FMLSDFacts then begin
LContext.MLSOpts := LContext.MLSOpts + [UnixOwner];
end;
if mlsdUnixGroup in FMLSDFacts then begin
LContext.MLSOpts := LContext.MLSOpts + [UnixGroup];
end;
if mlsdFileCreationTime in FMLSDFacts then begin
LContext.MLSOpts := LContext.MLSOpts + [CreateTime];
end;
if mlsdPerms in FMLSDFacts then begin
LContext.MLSOpts := LContext.MLSOpts + [Perm];
end;
if mlsdUniqueID in FMLSDFacts then begin
LContext.MLSOpts := LContext.MLSOpts + [Unique];
end;
if mlsdFileLastAccessTime in FMLSDFacts then begin
LContext.MLSOpts := LContext.MLSOpts + [LastAccessTime];
end;
if mlsdWin32Attributes in FMLSDFacts then begin
LContext.MLSOpts := LContext.MLSOpts + [WinAttribs];
end;
if mlsdWin32DriveType in FMLSDFacts then begin
LContext.MLSOpts := LContext.MLSOpts + [WinDriveType];
end;
if mlstWin32DriveLabel in FMLSDFacts then begin
LContext.MLSOpts := LContext.MLSOpts + [WinDriveLabel];
end;
//MS-DOS mode on for MS-DOS
if FDirFormat = ftpdfDOS then begin
LContext.FMSDOSMode := True;
end;
end;
destructor TIdFTPServer.Destroy;
begin
FreeAndNil(FAnonymousAccounts);
FreeAndNil(FFTPSecurityOptions);
FreeAndNil(FOPTSCommands);
FreeAndNil(FDataChannelCommands);
FreeAndNil(FSITECommands);
FreeAndNil(FReplyUnknownSITECommand);
inherited Destroy;
end;
procedure TIdFTPServer.ListDirectory(ASender: TIdFTPServerContext; ADirectory: string;
ADirContents: TStrings; ADetails: Boolean; const ACmd : String = 'LIST';
const ASwitches : String = ''); {do not localize}
var
LDirectoryList: TIdFTPListOutput;
LPathSep: string;
LIsMLST: Boolean;
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFilesystem;
begin
LIsMLST := PosInStrArray(ACmd, ['MLSD', 'MLST']) <> -1; {do not localize}
if (FDirFormat = ftpdfCustom) and (not LIsMLST) then begin
DoOnCustomListDirectory(ASender, ADirectory, ADirContents, ACmd, ASwitches);
Exit;
end;
LFileSystem := FFTPFileSystem;
if Assigned(FOnListDirectory) or Assigned(LFileSystem) then begin
LDirectoryList := TIdFTPListOutput.Create;
try
case FDirFormat of
ftpdfEPLF :
LDirectoryList.DirFormat := doEPLF;
ftpdfDOS :
if ASender.FMSDOSMode then begin
LDirectoryList.DirFormat := DoWin32;
end else begin
LDirectoryList.DirFormat := DoUnix;
end;
ftpdfOSDependent :
if (GOSType = otWindows) and (ASender.FMSDOSMode) then begin
LDirectoryList.DirFormat := DoWin32;
end else begin
LDirectoryList.DirFormat := DoUnix;
end;
else
LDirectoryList.DirFormat := DoUnix;
end;
//someone might be using the STAT -l to get a dir without a data channel
if IndyPos('l', ASwitches) > 0 then begin
LDirectoryList.Switches := LDirectoryList.Switches + 'l';
end;
//we do things this way because the 'a' and 'T' swithces only make sense
//when listing Unix dirs.
if SupportTaDirSwitches(ASender) then begin
if IndyPos('a', ASwitches) > 0 then begin
LDirectoryList.Switches := LDirectoryList.Switches + 'a';
end;
if IndyPos('T', ASwitches) > 0 then begin
LDirectoryList.Switches := LDirectoryList.Switches + 'T';
end;
end;
LDirectoryList.ExportTotalLine := True;
LPathSep := '/'; {Do not Localize}
if not TextEndsWith(ADirectory, LPathSep) then begin
ADirectory := ADirectory + LPathSep;
end;
if Assigned(LFileSystem) then begin
LFileSystem.ListDirectory(ASender, ADirectory, LDirectoryList, ACmd, ASwitches);
end else begin
FOnListDirectory(ASender, ADirectory, LDirectoryList, ACmd, ASwitches); // Event
end;
if LIsMLST then begin {Do not translate}
LDirectoryList.MLISTOutputDir(ADirContents, ASender.MLSOpts);
end
else if ADetails then begin
LDirectoryList.LISTOutputDir(ADirContents);
end else begin
LDirectoryList.NLISTOutputDir(ADirContents);
end;
finally
FreeAndNil(LDirectoryList);
end;
end else begin
raise EIdFTPServerNoOnListDirectory.Create(RSFTPNoOnDirEvent); {Do not Localize}
end;
end;
procedure TIdFTPServer.SetUserAccounts(const AValue: TIdCustomUserManager);
var
// under ARC, convert a weak reference to a strong reference before working with it
LUserAccounts: TIdCustomUserManager;
begin
LUserAccounts := FUserAccounts;
if LUserAccounts <> AValue then begin
// under ARC, all weak references to a freed object get nil'ed automatically
{$IFNDEF USE_OBJECT_ARC}
if Assigned(LUserAccounts) then begin
LUserAccounts.RemoveFreeNotification(Self);
end;
{$ENDIF}
FUserAccounts := AValue;
if Assigned(AValue) then begin
{$IFNDEF USE_OBJECT_ARC}
AValue.FreeNotification(Self);
{$ENDIF}
FOnUserAccount := nil;
//XAUT can not work with an account manager that sends
//a challange because that command is a USER/PASS rolled into
//one command.
if AValue.SendsChallange then begin
FSupportXAUTH := False;
end;
end;
end;
end;
procedure TIdFTPServer.SetFTPFileSystem(const AValue: TIdFTPBaseFileSystem);
begin
{$IFDEF USE_OBJECT_ARC}
// under ARC, all weak references to a freed object get nil'ed automatically
FFTPFileSystem := AValue;
{$ELSE}
if FFTPFileSystem <> AValue then begin
if Assigned(FFTPFileSystem) then begin
FFTPFileSystem.RemoveFreeNotification(Self);
end;
FFTPFileSystem := AValue;
if Assigned(AValue) then begin
AValue.FreeNotification(Self);
end;
end;
{$ENDIF}
end;
procedure TIdFTPServer.SetReplyUnknownSITECommand(AValue: TIdReply);
begin
FReplyUnknownSITECommand.Assign(AValue);
end;
procedure TIdFTPServer.SetSITECommands(AValue: TIdCommandHandlers);
begin
FSITECommands.Assign(AValue);
end;
// under ARC, all weak references to a freed object get nil'ed automatically
{$IFNDEF USE_OBJECT_ARC}
procedure TIdFTPServer.Notification(AComponent: TComponent; Operation: TOperation);
begin
if Operation = opRemove then begin
if AComponent = FUserAccounts then begin
FUserAccounts := nil;
end
else if AComponent = FFTPFileSystem then begin
FFTPFileSystem := nil;
end;
end;
inherited Notification(AComponent, Operation);
end;
{$ENDIF}
procedure TIdFTPServer.SetAnonymousAccounts(const AValue: TStrings);
begin
if Assigned(AValue) then begin
FAnonymousAccounts.Assign(AValue);
end;
end;
procedure TIdFTPServer.SetSupportXAUTH(AValue : Boolean);
var
// under ARC, convert a weak reference to a strong reference before working with it
LUserAccounts: TIdCustomUserManager;
begin
if FSupportXAUTH <> AValue then begin
LUserAccounts := FUserAccounts;
if Assigned(LUserAccounts) then begin
if LUserAccounts.SendsChallange then begin
Exit;
end;
end;
FSupportXAUTH := AValue;
end;
end;
procedure TIdFTPServer.ThreadException(AThread: TIdThread; AException: Exception);
begin
//we do not want to show an exception in a dialog-box
end;
//Command Replies/Handling
procedure TIdFTPServer.CommandUSER(ASender: TIdCommand);
var
LSafe: Boolean;
LChallenge: String;
LContext: TIdFTPServerContext;
// under ARC, convert a weak reference to a strong reference before working with it
LUserAccounts: TIdCustomUserManager;
begin
LChallenge := '';
LContext := ASender.Context as TIdFTPServerContext;
if (FUseTLS = utUseRequireTLS) and (LContext.AuthMechanism <> 'TLS') then begin {do not localize}
DisconUser(ASender);
Exit;
end;
LContext.Authenticated := False;
if (FAnonymousAccounts.IndexOf(LowerCase(ASender.UnparsedParams)) >= 0) and AllowAnonymousLogin then begin
LContext.UserType := utAnonymousUser;
LContext.Username := ASender.UnparsedParams;
ASender.Reply.SetReply(331, RSFTPAnonymousUserOkay);
end else begin
LContext.UserType := utNormalUser;
if Length(ASender.UnparsedParams) > 0 then begin
LContext.Username := ASender.UnparsedParams;
LUserAccounts := FUserAccounts;
if Assigned(LUserAccounts) then begin
LChallenge := LUserAccounts.ChallengeUser(LSafe, LContext.Username);
{$IFDEF USE_OBJECT_ARC}LUserAccounts := nil;{$ENDIF}
if not LSafe then begin
//we do this to prevent a potential race attack
DisconUser(ASender);
Exit;
end;
end;
if LChallenge = '' then begin
ASender.Reply.SetReply(331, RSFTPUserOkay);
end else begin
ASender.Reply.SetReply(331, LChallenge);
end;
end else begin
ASender.Reply.SetReply(332, RSFTPNeedAccountForLogin);
end;
end;
end;
procedure TIdFTPServer.AuthenticateUser(ASender: TIdCommand);
var
LValidated: Boolean;
LContext: TIdFTPServerContext;
// under ARC, convert a weak reference to a strong reference before working with it
LUserAccounts: TIdCustomUserManager;
begin
LContext:= ASender.Context as TIdFTPServerContext;
try
LContext.FAuthenticated := False;
case LContext.FUserType of
utAnonymousUser:
begin
LValidated := Length(LContext.Password ) > 0;
if FAnonymousPassStrictCheck and LValidated then begin
LValidated := False;
if FindFirstOf('@.', LContext.Password) > 0 then begin {Do not Localize}
LValidated := True;
end;
end;
if LValidated then begin
LContext.FAuthenticated := True;
ASender.Reply.SetReply(230, RSFTPAnonymousUserLogged);
if Assigned(OnLoginSuccessBanner) then begin
OnLoginSuccessBanner(LContext, ASender.Reply);
ASender.Reply.SetReply(230, ASender.Reply.Text.Text);
end;
LContext.FPasswordAttempts := 0;
end else begin
LContext.FUserType := utNone;
LContext.FAuthenticated := False;
LContext.FPassword := ''; {Do not Localize}
Inc(LContext.FPasswordAttempts);
if LContext.UserSecurity.InvalidPassDelay > 0 then begin
//Delay our error response to slow down a dictionary attack
IndySleep(FFTPSecurityOptions.InvalidPassDelay);
end;
if (LContext.UserSecurity.PasswordAttempts > 0) and
(LContext.FPasswordAttempts >= LContext.UserSecurity.PasswordAttempts) then begin
DisconUser(ASender);
Exit;
end;
ASender.Reply.SetReply(530, RSFTPUserNotLoggedIn);
end;
end;
utNormalUser:
begin
LUserAccounts := FUserAccounts;
if Assigned(LUserAccounts) then begin
LContext.FAuthenticated := LUserAccounts.AuthenticateUser(LContext.FUsername, ASender.UnparsedParams);
{$IFDEF USE_OBJECT_ARC}LUserAccounts := nil;{$ENDIF}
if LContext.FAuthenticated then begin
LContext.FPasswordAttempts := 0;
ASender.Reply.SetReply(230, RSFTPUserLogged);
end else begin
LContext.FPassword := ''; {Do not Localize}
Inc(LContext.FPasswordAttempts);
if LContext.UserSecurity.InvalidPassDelay > 0 then begin
//Delay our error response to slow down a dictionary attack
IndySleep(LContext.UserSecurity.InvalidPassDelay);
end;
if (LContext.UserSecurity.PasswordAttempts > 0) and
(LContext.FPasswordAttempts >= LContext.UserSecurity.PasswordAttempts) then
begin
//Max login attempts exceeded, close the connection
DisconUser(ASender);
Exit;
end;
ASender.Reply.SetReply(530, RSFTPUserNotLoggedIn);
end;
end
else if Assigned(FOnUserLogin) then begin
LValidated := False;
FOnUserLogin(LContext, LContext.FUsername, LContext.Password, LValidated);
LContext.FAuthenticated := LValidated;
if LValidated then begin
if (LContext.AccountNeeded = True) and Assigned(FOnUserAccount) then begin
LContext.FAuthenticated := False;
ASender.Reply.SetReply(332,'Need account for login.');
Exit;
end else begin
LContext.FAuthenticated := LValidated;
ASender.Reply.SetReply(230, RSFTPUserLogged);
if Assigned(OnLoginSuccessBanner) then begin
OnLoginSuccessBanner(LContext, ASender.Reply);
ASender.Reply.SetReply(230, ASender.Reply.Text.Text);
end;
LContext.FPasswordAttempts := 0;
end;
end else begin
LContext.FPassword := ''; {Do not Localize}
Inc(LContext.FPasswordAttempts);
if (LContext.UserSecurity.PasswordAttempts > 0) and
(LContext.FPasswordAttempts >= LContext.UserSecurity.PasswordAttempts) then begin
//Max login attempts exceeded, close the connection
DisconUser(ASender);
Exit;
end;
ASender.Reply.SetReply(530, RSFTPUserNotLoggedIn);
end;
end else begin
//APR 020423
ASender.Reply.SetReply(530, RSFTPUserNotLoggedIn); // user manager not found
end;
end;
else
ASender.Reply.SetReply(503, RSFTPNeedLoginWithUser);
end;//case
except
on E : Exception do begin
ASender.Reply.SetReply(503, E.Message);
end;
end;
//After login
if LContext.FAuthenticated and Assigned(FOnAfterUserLogin) then begin
FOnAfterUserLogin(LContext);
end;
end;
procedure TIdFTPServer.CommandPASS(ASender: TIdCommand);
var
LContext: TIdFTPServerContext;
begin
LContext:= ASender.Context as TIdFTPServerContext;
if (FUseTLS = utUseRequireTLS) and (LContext.AuthMechanism <> 'TLS') then begin {do not localize}
DisconUser(ASender);
Exit;
end;
LContext.FAuthenticated := False;
LContext.FPassword := ASender.UnparsedParams;
AuthenticateUser(ASender);
end;
procedure TIdFTPServer.CommandXAUT(ASender : TIdCommand);
var
LContext : TIdFTPServerContext;
s : String;
LPos : Integer;
begin
LContext := ASender.Context as TIdFTPServerContext;
if (FUseTLS = utUseRequireTLS) and (LContext.AuthMechanism <> 'TLS') then begin {do not localize}
DisconUser(ASender);
Exit;
end;
LContext := ASender.Context as TIdFTPServerContext;
s := ASender.UnparsedParams;
s := IdFTPCommon.ExtractAutInfoFromXAUT(s, LContext.FXAUTKey );
LPos := RPos(':',s);
if LPos > 1 then begin
LContext.Username := Copy(s,1,LPos - 1);
s := Copy(s,LPos + 1,$FF);
//for some reason, WS-FTP Pro likes to add the string "^vta4r2" to
//the authentication information if you aren't using anonymous login.
//I'm not sure what the significance of "^vta4r2" really is.
// 1234567
if TextEndsWith(s,'^vta4r2') then begin
LContext.Password := Copy(s,1,Length(s)-7);
end;
end else begin
LContext.Username := s;
LContext.Password := '';
end;
LContext.Authenticated := False;
if (FAnonymousAccounts.IndexOf(LowerCase(ASender.UnparsedParams)) >= 0) and AllowAnonymousLogin then begin
LContext.UserType := utAnonymousUser;
end else begin
LContext.UserType := utNormalUser;
end;
AuthenticateUser(ASender);
end;
procedure TIdFTPServer.CommandACCT(ASender: TIdCommand);
var
LContext : TIdFTPServerContext;
LValidated : Boolean;
begin
LValidated := False;
if Assigned(FOnUserAccount) then begin
LContext := ASender.Context as TIdFTPServerContext;
LContext.Account := ASender.UnparsedParams;
FOnUserAccount(LContext,LContext.Username, LContext.Password, LContext.Account, LValidated);
LContext.Authenticated := LValidated;
if LValidated then begin
LContext.AccountNeeded := False;
ASender.Reply.SetReply(230, RSFTPUserLogged);
if Assigned(OnLoginSuccessBanner) then begin
OnLoginSuccessBanner(LContext, ASender.Reply);
ASender.Reply.SetReply(230, ASender.Reply.Text.Text);
LContext.PasswordAttempts := 0;
end;
end else begin
LContext.FPassword := ''; {Do not Localize}
Inc(LContext.FPasswordAttempts);
if (LContext.UserSecurity.PasswordAttempts > 0) and
(LContext.FPasswordAttempts >= LContext.UserSecurity.PasswordAttempts) then begin
//Max login attempts exceeded, close the connection
DisconUser(ASender);
Exit;
end;
ASender.Reply.SetReply(530, RSFTPUserNotLoggedIn);
end;
end else begin
ASender.Reply.SetReply(502, IndyFormat(RSFTPCmdNotImplemented, ['ACCT'])); {do not localize}
end;
end;
procedure TIdFTPServer.CommandCWD(ASender: TIdCommand);
var
s: TIdFTPFileName;
LContext : TIdFTPServerContext;
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFilesystem;
begin
LContext := ASender.Context as TIdFTPServerContext;
s := ASender.UnparsedParams;
if LContext.IsAuthenticated(ASender) then begin
s := IgnoreLastPathDelim(s);
LFileSystem := FFTPFileSystem;
if Assigned(OnChangeDirectory) or Assigned(LFileSystem) then begin
if s = '..' then begin {do not localize}
s := CDUPDir(LContext);
end
else if s = '.' then begin {do not localize}
s := LContext.CurrentDir;
end else begin
s := DoProcessPath(LContext, s);
end;
s := RemoveDuplicatePathSyms(s);
DoOnChangeDirectory(LContext, s);
LContext.CurrentDir := s;
CmdCommandSuccessful(ASender);
end else begin
CmdNotImplemented(ASender);
end;
end;
end;
procedure TIdFTPServer.CommandCDUP(ASender: TIdCommand);
var
s: TIdFTPFileName;
LContext : TIdFTPServerContext;
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
begin
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
s := CDUPDir(LContext);
s := DoProcessPath(LContext, s);
LFileSystem := FFTPFileSystem;
if Assigned(FOnChangeDirectory) or Assigned(LFileSystem) then begin
DoOnChangeDirectory(LContext, s);
LContext.FCurrentDir := s;
ASender.Reply.SetReply(250, IndyFormat(RSFTPCurrentDirectoryIs, [LContext.FCurrentDir]));
end else begin
CmdNotImplemented(ASender);
end;
end;
end;
procedure TIdFTPServer.CommandREIN(ASender: TIdCommand);
var
LIO : TIdSSLIOHandlerSocketBase;
LContext : TIdFTPServerContext;
begin
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
LContext.ReInitialize;
LContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_8Bit;
ASender.Reply.SetReply(220, RSFTPServiceOpen);
if (FUseTLS in ExplicitTLSVals) then begin
LIO := ASender.Context.Connection.IOHandler as TIdSSLIOHandlerSocketBase;
if not LIO.PassThrough then begin
LIO.Passthrough := True;
end;
LContext.FCCC := False;
end;
end;
end;
procedure TIdFTPServer.CommandPORT(ASender: TIdCommand);
var
LLo, LHi : Integer;
LPort: TIdPort;
LParm, LIP : string;
LContext : TIdFTPServerContext;
LDataChannel: TIdTCPClient;
begin
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
if LContext.FEPSVAll then begin
ASender.Reply.SetReply(501, IndyFormat(RSFTPNotAllowedAfterEPSVAll, [ASender.CommandHandler.Command]));
Exit;
end;
if LContext.UserSecurity.BlockAllPORTTransfers then
begin
LContext.FDataPort := 0;
LContext.FDataPortDenied := True;
ASender.Reply.SetReply(502, RSFTPPORTDisabled);
Exit;
end;
LContext.FPASV := False;
LParm := ASender.UnparsedParams;
LIP := ''; {Do not Localize}
{ h1 }
LIP := LIP + Fetch(LParm, ',') + '.'; {Do not Localize}
{ h2 }
LIP := LIP + Fetch(LParm, ',') + '.'; {Do not Localize}
{ h3 }
LIP := LIP + Fetch(LParm, ',') + '.'; {Do not Localize}
{ h4 }
LIP := LIP + Fetch(LParm, ','); {Do not Localize}
{ p1 }
LLo := IndyStrToInt(Fetch(LParm, ',')); {Do not Localize}
{ p2 }
LHi := IndyStrToInt(LParm);
LPort := TIdPort((LLo * 256) + LHi);
if LContext.UserSecurity.NoReservedRangePORT and
((LPort > 0) and (LPort <= 1024)) then begin
LContext.FDataPort := 0;
LContext.FDataPortDenied := True;
ASender.Reply.SetReply(504, RSFTPPORTRange);
Exit;
end;
{//BGO}
if LContext.UserSecurity.FRequirePORTFromSameIP and
(LIP <> LContext.Binding.PeerIP) then begin
LContext.FDataPort := 0;
LContext.FDataPortDenied := True;
ASender.Reply.SetReply(504, RSFTPSameIPAddress);
Exit;
end;
{//BGO}
LContext.CreateDataChannel(False);
LDataChannel := TIdTCPClient(LContext.FDataChannel.FDataChannel);
LDataChannel.Host := LIP;
LDataChannel.Port := LPort;
LDataChannel.IPVersion := Id_IPv4;
LContext.FDataPort := LPort;
LContext.FDataPortDenied := False;
CmdCommandSuccessful(ASender, 200);
end;
end;
procedure TIdFTPServer.CommandPASV(ASender: TIdCommand);
var
LParam: string;
LBPort: Word;
LIPVersion : TIdIPVersion;
begin
//InternalPASV does all of the checking
if InternalPASV(ASender, LParam, LBPort, LIPVersion) then begin
DoOnPASVReply(TIdFTPServerContext(ASender.Context), LParam, LBPort, LIPVersion);
LParam := ReplaceAll(LParam, '.', ','); {Do not Localize}
LParam := LParam + ',' + IntToStr(LBPort div 256) + ',' + IntToStr(LBPort mod 256); {Do not Localize}
ASender.Reply.SetReply(227, IndyFormat(RSFTPPassiveMode, [LParam]));
end;
end;
procedure TIdFTPServer.CommandTYPE(ASender: TIdCommand);
var
LContext : TIdFTPServerContext;
s: string;
begin
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
s := ASender.UnparsedParams;
s := UpperCase(Fetch(s));
if Length(s) = 1 then begin
//Default data type is ASCII
case s[1] of
'A': LContext.FDataType := ftASCII; {Do not Localize}
'I': LContext.FDataType := ftBinary; {Do not Localize}
else Exit;
end;
ASender.Reply.SetReply(200, IndyFormat(RSFTPTYPEChanged, [s]));
end;
end;
end;
procedure TIdFTPServer.CommandSTRU(ASender: TIdCommand);
var
LContext : TIdFTPServerContext;
s: String;
begin
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
s := ASender.UnparsedParams;
s := UpperCase(Fetch(s));
if Length(s) = 1 then begin
//Default structure is file
case s[1] of
'F': LContext.FDataStruct := dsFile; {Do not Localize}
'R': LContext.FDataStruct := dsRecord; {Do not Localize}
'P': LContext.FDataStruct := dsPage; {Do not Localize}
else Exit;
end;
ASender.Reply.SetReply(200, IndyFormat(RSFTPSTRUChanged, [s]));
end;
end;
end;
procedure TIdFTPServer.CommandMODE(ASender: TIdCommand);
var
LContext : TIdFTPServerContext;
s: String;
begin
LContext := TIdFTPServerContext(ASender.Context);
if LContext.IsAuthenticated(ASender) then begin
s := ASender.UnparsedParams;
s := UpperCase(Fetch(s));
if Length(s) = 1 then begin
//Default data mode is stream
case s[1] of
'S' : //stream mode
begin
LContext.DataMode := dmStream;
ASender.Reply.SetReply(200, IndyFormat(RSFTPMODEChanged, [s]));
Exit;
end;
'Z' : //deflate
begin
if Assigned(FCompressor) then begin
LContext.DataMode := dmDeflate;
ASender.Reply.SetReply(200, IndyFormat(RSFTPMODEChanged, [s]));
Exit;
end;
end;
end;
ASender.Reply.SetReply(504, RSFTPMODENotSupported);
end;
end;
end;
procedure TIdFTPServer.CommandRETR(ASender: TIdCommand);
var
s: string;
LStream: TStream;
LContext : TIdFTPServerContext;
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
begin
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
if (not Assigned(LContext.FDataChannel)) or LContext.FDataPortDenied then begin
ASender.Reply.SetReply(425, RSFTPCantOpenData);
Exit;
end;
//TODO: Fix reference to /
s := DoProcessPath(LContext, ASender.UnparsedParams);
LFileSystem := FFTPFileSystem;
if Assigned(FOnRetrieveFile) or Assigned(LFileSystem) then begin
LStream := nil;
try
//some file stream creations can fail with an exception so
//we need to handle this gracefully.
if Assigned(LFileSystem) then begin
LFileSystem.RetrieveFile(LContext, s, LStream)
end else begin
FOnRetrieveFile(LContext, s, LStream);
end;
except
on E : Exception do begin
LContext.KillDataChannel;
ASender.Reply.SetReply(550, E.Message);
Exit;
end;
end;
if Assigned(LStream) then begin
try
LStream.Position := LContext.FRESTPos;
LContext.FRESTPos := 0;
//it should be safe to assume that the FDataChannel object exists because
//we checked it earlier
LContext.FDataChannel.FFtpOperation := ftpRetr;
LContext.FDataChannel.FData := LStream;
LContext.FDataChannel.OKReply.SetReply(226, RSFTPDataConnClosed);
LContext.FDataChannel.ErrorReply.SetReply(426, RSFTPDataConnClosedAbnormally);
ASender.Reply.SetReply(150, RSFTPDataConnToOpen);
ASender.SendReply;
DoDataChannelOperation(ASender, LContext.SSCNOn);
finally
LStream.Free;
end;
end else begin
//make sure the data connection is closed
LContext.KillDataChannel;
CmdFileActionAborted(ASender);
end;
end else begin
//make sure the data connection is closed
LContext.KillDataChannel;
CmdNotImplemented(ASender);
end;
end;
end;
procedure TIdFTPServer.CommandSSAP(ASender: TIdCommand);
var
LStream: TStream;
LTmp1: string;
LAppend: Boolean;
LContext : TIdFTPServerContext;
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
begin
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
if (not Assigned(LContext.FDataChannel)) or LContext.FDataPortDenied then begin
ASender.Reply.SetReply(425, RSFTPCantOpenData);
Exit;
end;
if TextIsSame(ASender.CommandHandler.Command, 'STOU') then begin {Do not Localize}
LTmp1 := GetUniqueFileName('', 'Temp', ''); {Do not localize}
//This is a standardized format
ASender.Reply.SetReply(150, IndyFormat('FILE: %s', [LTmp1])); {Do not translate}
end else begin
LTmp1 := ASender.UnparsedParams;
ASender.Reply.SetReply(150, RSFTPDataConnToOpen);
end;
LTmp1 := DoProcessPath(LContext, LTmp1);
LAppend := TextIsSame(ASender.CommandHandler.Command, 'APPE'); {Do not Localize}
LFileSystem := FFTPFileSystem;
if Assigned(FOnStoreFile) or Assigned(LFileSystem) then begin
LStream := nil;
try
if Assigned(LFileSystem) then begin
LFileSystem.StoreFile(LContext, LTmp1, LAppend, LStream);
{$IFDEF USE_OBJECT_ARC}LFileSystem := nil;{$ENDIF}
end else begin
FOnStoreFile(LContext, LTmp1, LAppend, LStream);
end;
except
on E : Exception do
begin
ASender.Reply.SetReply(550, E.Message);
LContext.KillDataChannel;
Exit;
end;
end;
if Assigned(LStream) then begin
try
//Issued previously by ALLO cmd
if LContext.ALLOSize > 0 then begin
LStream.Size := LContext.FALLOSize;
end;
if LAppend then begin
LStream.Position := LStream.Size;
end else begin
LStream.Position := LContext.FRESTPos;
LContext.FRESTPos := 0;
end;
{ Data transfer }
//it should be safe to assume that the FDataChannel object exists because
//we checked it earlier
LContext.FDataChannel.FFtpOperation := ftpStor;
LContext.FDataChannel.Data := LStream;
LContext.FDataChannel.OKReply.SetReply(226, RSFTPDataConnClosed);
LContext.FDataChannel.ErrorReply.SetReply(426, RSFTPDataConnClosedAbnormally);
ASender.SendReply;
DoDataChannelOperation(ASender, LContext.SSCNOn);
finally
LStream.Free;
end;
end else begin
//make sure the data connection is closed
LContext.KillDataChannel;
CmdFileActionAborted(ASender);
end;
end else begin
//make sure the data connection is closed
LContext.KillDataChannel;
CmdNotImplemented(ASender);
end;
end;
end;
procedure TIdFTPServer.CommandALLO(ASender: TIdCommand);
var
LContext: TIdFTPServerContext;
LALLOSize, s: string;
begin
LContext := TIdFTPServerContext(ASender.Context);
if LContext.IsAuthenticated(ASender) then begin
LALLOSize := '';
if Length(ASender.UnparsedParams) > 0 then begin
if TextStartsWith(ASender.UnparsedParams, 'R ') then begin {Do not localize}
LALLOSize := TrimLeft(Copy(s, 3, MaxInt));
end else begin
LALLOSize := TrimLeft(ASender.UnparsedParams);
end;
LALLOSize := Fetch(LALLOSize);
end;
if LALLOSize <> '' then begin
LContext.FALLOSize := IndyStrToInt(LALLOSize, 0);
CmdCommandSuccessful(ASender, 200);
end else begin
ASender.Reply.SetReply(504, RSFTPInvalidForParam);
end;
end;
end;
procedure TIdFTPServer.CommandREST(ASender: TIdCommand);
var
LContext: TIdFTPServerContext;
begin
LContext := TIdFTPServerContext(ASender.Context);
if LContext.IsAuthenticated(ASender) then begin
LContext.FRESTPos := IndyStrToInt(ASender.UnparsedParams, 0);
ASender.Reply.SetReply(350, RSFTPFileActionPending);
end;
end;
procedure TIdFTPServer.CommandRNFR(ASender: TIdCommand);
var
LContext: TIdFTPServerContext;
s: string;
begin
LContext := TIdFTPServerContext(ASender.Context);
if LContext.IsAuthenticated(ASender) then begin
s := ASender.UnparsedParams;
if Assigned(FOnRenameFile) or Assigned(FTPFileSystem) then begin
ASender.Reply.SetReply(350, RSFTPFileActionPending);
LContext.FRNFR := DoProcessPath(TIdFTPServerContext(LContext), s);
end else begin
CmdNotImplemented(ASender);
end;
end;
end;
procedure TIdFTPServer.CommandRNTO(ASender: TIdCommand);
var
s: string;
LContext : TIdFTPServerContext;
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
begin
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
s := ASender.UnparsedParams;
LFileSystem := FFTPFileSystem;
if Assigned(LFileSystem) or Assigned(FOnRenameFile) then begin
DoOnRenameFile(LContext, LContext.FRNFR, DoProcessPath(LContext, s));
ASender.Reply.NumericCode := 250;
end else begin
CmdNotImplemented(ASender);
end;
end;
end;
procedure TIdFTPServer.CommandABOR(ASender: TIdCommand);
var
LContext: TIdFTPServerContext;
begin
LContext := TIdFTPServerContext(ASender.Context);
if LContext.IsAuthenticated(ASender) then begin
if Assigned(LContext.FDataChannel) then begin
if not LContext.FDataChannel.Stopped then begin
LContext.FDataChannel.OkReply.SetReply(426, RSFTPDataConnClosedAbnormally);
LContext.FDataChannel.ErrorReply.SetReply(426, RSFTPDataConnClosedAbnormally);
LContext.KillDataChannel;
ASender.Reply.SetReply(226, RSFTPDataConnClosed);
Exit;
end;
end;
CmdCommandSuccessful(ASender, 226);
end;
end;
procedure TIdFTPServer.CommandDELE(ASender: TIdCommand);
var
LContext : TIdFTPServerContext;
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
(*
DELE <SP> <pathname> <CRLF>
250 Requested file action okay, completed.
450 Requested file action not taken. - File is busy
550 Requested action not taken. - File unavailable, no access permitted, etc
500 Syntax error, command unrecognized.
501 Syntax error in parameters or arguments.
502 Command not implemented.
421 Service not available, closing control connection. - During server shutdown, etc
530 Not logged in.
*)
//TODO: Need to set replies when not authenticated and set NormalReply to 250
// do for all procs, list valid replies in comments. Or maybe default is 550
begin
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
LFileSystem := FTPFileSystem;
if Assigned(FOnDeleteFile) or Assigned(LFileSystem) then begin
DoOnDeleteFile(LContext, DoProcessPath(LContext, ASender.UnparsedParams));
ASender.Reply.SetReply(250, RSFTPFileActionCompleted);
end else begin
CmdNotImplemented(ASender);
end;
end else begin
ASender.Reply.SetReply(550, RSFTPFileActionNotTaken);
end;
end;
procedure TIdFTPServer.CommandRMD(ASender: TIdCommand);
var
s: TIdFTPFileName;
LContext : TIdFTPServerContext;
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
begin
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
S := IgnoreLastPathDelim(S);
s := DoProcessPath(LContext, ASender.UnparsedParams);
LFileSystem := FFTPFileSystem;
if Assigned(LFileSystem) or Assigned(FOnRemoveDirectory) then begin
DoOnRemoveDirectory(LContext, s);
ASender.Reply.SetReply(250, RSFTPFileActionCompleted);
end else begin
CmdNotImplemented(ASender);
end;
end;
end;
procedure TIdFTPServer.CommandMKD(ASender: TIdCommand);
var
S: TIdFTPFileName;
LContext : TIdFTPServerContext;
begin
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
S := IgnoreLastPathDelim(S);
S := DoProcessPath(LContext, ASender.UnparsedParams);
DoOnMakeDirectory(LContext, s);
ASender.Reply.SetReply(257, RSFTPFileActionCompleted);
end;
end;
procedure TIdFTPServer.CommandPWD(ASender: TIdCommand);
var
LContext: TIdFTPServerContext;
begin
LContext := TIdFTPServerContext(ASender.Context);
if LContext.IsAuthenticated(ASender) then begin
ASender.Reply.SetReply(257, IndyFormat(RSFTPCurrentDirectoryIs, [LContext.FCurrentDir]));
end;
end;
procedure TIdFTPServer.CommandLIST(ASender: TIdCommand);
var
LStream: TStringList;
LSendData : Boolean;
LPath, LSwitches : String;
LContext : TIdFTPServerContext;
function DeletRSwitch(const AString : String): String;
var
i : Integer;
begin
Result := '';
for i := 1 to Length(AString) do begin
if AString[i] <> 'R' then begin
Result := Result + AString[i];
end;
end;
end;
begin
LSendData := False;
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
if (not Assigned(LContext.FDataChannel)) or LContext.FDataPortDenied then begin
ASender.Reply.SetReply(425, RSFTPCantOpenData);
Exit;
end;
if (not Assigned(FOnListDirectory)) and
((FDirFormat = ftpdfCustom) and (not Assigned(FOnCustomListDirectory))) then begin
LContext.KillDataChannel;
CmdNotImplemented(ASender);
Exit;
end;
LStream := TStringList.Create;
try
LSwitches := '';
LPath := ASender.UnparsedParams;
if TextStartsWith(LPath, '-') then begin {Do not Localize}
LSwitches := Fetch(LPath);
end;
//we can't support recursive lists with EPLF
if DirFormat = ftpdfEPLF then begin
LSwitches := DeletRSwitch(LSwitches);
end;
ListDirectory(LContext, DoProcessPath(LContext, LPath), LStream,
TextIsSame(ASender.CommandHandler.Command, 'LIST'), ASender.CommandHandler.Command,
LSwitches);
LSendData := True;
finally
try
if LSendData then begin
//it should be safe to assume that the FDataChannel object exists because
//we checked it earlier
LContext.FDataChannel.Data := LStream;
LContext.FDataChannel.FFtpOperation := ftpRetr;
LContext.FDataChannel.OKReply.SetReply(226, RSFTPDataConnClosed);
LContext.FDataChannel.ErrorReply.SetReply(426, RSFTPDataConnClosedAbnormally);
if FDirFormat = ftpdfEPLF then begin
ASender.Reply.SetReply(125, RSFTPDataConnToOpen);
LContext.FDataChannel.OKReply.SetReply(226, RSFTPDataConnClosed);
end
else if TextIsSame(ASender.CommandHandler.Command, 'LIST') or (LSwitches <> '') then begin {do not localize}
ASender.Reply.SetReply(125, RSFTPDataConnList);
end else begin
ASender.Reply.SetReply(125, RSFTPDataConnNList);
end;
ASender.SendReply;
DoDataChannelOperation(ASender);
end else begin
LContext.KillDataChannel;
ASender.Reply.SetReply(426, RSFTPDataConnClosedAbnormally);
end;
finally
LStream.Free;
end;
end;
end;
end;
procedure TIdFTPServer.DoDataChannelOperation(ASender: TIdCommand; const AConnectMode : Boolean = False);
const
DEF_BLOCKSIZE = 10*10240;
{CH DEF_CHECKCMD_WAIT = 1; }
var
LContext : TIdFTPServerContext;
LCmdQueue : TStringList;
LLine : String;
LStrm : TStream;
procedure CheckControlConnection(AContext : TIdFTPServerContext; ACmdQueue : TStrings);
var
LLocalLine : String;
begin
LLocalLine := ReadCommandLine(AContext);
if LLocalLine <> '' then begin
if not FDataChannelCommands.HandleCommand(AContext, LLocalLine) then begin
ACmdQueue.Add(LLocalLine);
end;
end;
end;
procedure ReadFromStream(AContext : TIdFTPServerContext; ACmdQueue : TStrings; ADestStream : TStream);
var
LM : TStream;
begin
if AContext.DataMode = dmDeflate then begin
LM := TMemoryStream.Create;
end else begin
LM := ADestStream;
end;
try
repeat
AContext.FDataChannel.FDataChannel.IOHandler.CheckForDisconnect(False);
AContext.FDataChannel.FDataChannel.IOHandler.ReadStream(LM, DEF_BLOCKSIZE, True);
CheckControlConnection(AContext, ACmdQueue);
until not AContext.FDataChannel.FDataChannel.IOHandler.Connected;
if AContext.DataMode = dmDeflate then begin
LM.Position := 0;
FCompressor.DecompressFTPDeflate(LM, ADestStream, AContext.ZLibWindowBits);
end;
finally
if AContext.DataMode = dmDeflate then begin
FreeAndNil(LM);
end;
end;
end;
procedure WriteToStream(AContext : TIdFTPServerContext; ACmdQueue : TStrings;
ASrcStream : TStream; const AIgnoreCompression : Boolean = False);
var
LBufSize : TIdStreamSize;
LOutStream : TStream;
begin
if AContext.DataMode = dmDeflate then begin
LOutStream := TMemoryStream.Create;
end else begin
LOutStream := ASrcStream;
end;
try
if AContext.DataMode = dmDeflate then begin
FCompressor.CompressFTPDeflate(ASrcStream, LOutStream,
AContext.ZLibCompressionLevel, AContext.ZLibWindowBits,
AContext.ZLibMemLevel, AContext.ZLibStratagy);
LOutStream.Position := 0;
end;
repeat
LBufSize := LOutStream.Size - LOutStream.Position;
if LBufSize > DEF_BLOCKSIZE then begin
LBufSize := DEF_BLOCKSIZE;
end;
if LBufSize > 0 then begin
AContext.FDataChannel.FDataChannel.IOHandler.Write(LOutStream, LBufSize, False);
if LOutStream.Position < LOutStream.Size then begin
CheckControlConnection(AContext, ACmdQueue);
end;
end;
until (LBufSize = 0) or (not AContext.FDataChannel.FDataChannel.IOHandler.Connected);
finally
if AContext.DataMode = dmDeflate then begin
FreeAndNil(LOutStream);
end;
end;
end;
procedure WriteStrings(AContext : TIdFTPServerContext; ACmdQueue : TStrings; ASrcStrings : TStrings);
var
i : Integer;
LM : TStream;
LEncoding: IIdTextEncoding;
begin
//for loops will execute at least once triggering an out of range error.
//write nothing if AStrings is empty.
if ASrcStrings.Count < 1 then begin
Exit;
end;
{
IMPORTANT!!!
If LIST data is sent as 8bit, you have a FTP list that is unparsable by
some FTP clients. If UTF8 OPTS OFF, you should send the data as 7bit
for the LIST and NLST commands. That way, unprintable charactors are
returned as ?. While the file name is not valid, at least, there some
thing that looks better than binary junk.
}
if PosInStrArray(ASender.CommandHandler.Command, ['LIST', 'NLST', 'MLSD'], False) > -1 then begin
LEncoding := IndyTextEncoding(NLSTEncType[AContext.NLSTUtf8]);
end else begin
LEncoding := IndyTextEncoding_8Bit;
end;
if AContext.DataMode = dmDeflate then begin
LM := TMemoryStream.Create;
try
for i := 0 to ASrcStrings.Count-1 do begin
WriteStringToStream(LM, ASrcStrings[i] + EOL, LEncoding);
end;
LM.Position := 0;
WriteToStream(AContext, ACmdQueue, LM, True);
finally
FreeAndNil(LM);
end;
Exit;
end;
for i := 0 to ASrcStrings.Count-1 do begin
if AContext.FDataChannel.FDataChannel.IOHandler.Connected then begin
AContext.FDataChannel.FDataChannel.IOHandler.WriteLn(ASrcStrings[i], LEncoding);
if ((i mod 10) = 0) and (i <> (ASrcStrings.Count-1)) then begin
if AContext.FDataChannel.FDataChannel.IOHandler.Connected then begin
CheckControlConnection(AContext, ACmdQueue);
end else begin
Break;
end;
end;
end else begin
Break;
end;
end;
end;
begin
if not Assigned(ASender) then begin
Exit;
end;
if not Assigned(ASender.Context) then begin
Exit;
end;
LContext := ASender.Context as TIdFTPServerContext;
if not Assigned(LContext.FDataChannel) then begin
Exit;
end;
try
LCmdQueue := TStringList.Create;
try
LContext.FDataChannel.InitOperation(AConnectMode);
try
try
try
if LContext.FDataChannel.Data is TStream then begin
LStrm := TStream(LContext.FDataChannel.Data);
case LContext.FDataChannel.FFtpOperation of
ftpRetr:
WriteToStream(LContext, LCmdQueue, LStrm);
ftpStor:
ReadFromStream(LContext, LCmdQueue, LStrm);
end;
end else begin
case LContext.FDataChannel.FFtpOperation of
ftpRetr:
if Assigned(LContext.FDataChannel.Data) then begin
WriteStrings(LContext, LCmdQueue, LContext.FDataChannel.Data as TStrings);
end;
ftpStor:
if Assigned(LContext.FDataChannel.Data) then begin
LStrm := TMemoryStream.Create;
try
ReadFromStream(LContext, LCmdQueue, LStrm);
//TODO;
// SplitLines(TMemoryStream(LStrm).Memory, LMemStream.Size, LContext.FDataChannel.FData as TStrings);
finally
LStrm.Free;
end;
end;//ftpStor
end;//case
end;
finally
if Assigned(LContext.FDataChannel.FDataChannel) then begin
LContext.FDataChannel.FDataChannel.Disconnect(False);
end;
end;
LContext.FDataChannel.FReply.Assign(LContext.FDataChannel.FOKReply); //226
except
on E: Exception do begin
if not (E is EIdSilentException) then begin
LContext.FDataChannel.FReply.Assign(LContext.FDataChannel.FErrorReply); //426
end;
end;
end;
finally
ASender.Reply.Assign(LContext.FDataChannel.FReply);
ASender.SendReply;
//now we have to handle the FIFO queue we had made
while LCmdQueue.Count > 0 do begin
LLine := LCmdQueue[0];
if not FCommandHandlers.HandleCommand(ASender.Context, LLine) then begin
DoReplyUnknownCommand(ASender.Context, LLine);
end;
if Assigned(ASender.Context.Connection) then begin
if not ASender.Context.Connection.Connected then begin
Break;
end;
end else begin
Break;
end;
LCmdQueue.Delete(0);
end;
end;
finally
FreeAndNil(LCmdQueue);
end;
finally
FreeAndNil(LContext.FDataChannel);
end;
end;
procedure TIdFTPServer.CommandSYST(ASender: TIdCommand);
var
LContext : TIdFTPServerContext;
begin
LContext := ASender.Context as TIdFTPServerContext;
if LContext.UserSecurity.DisableSYSTCommand then begin
CmdNotImplemented(ASender);
Exit;
end;
//this should keep CuteFTP Pro 3.0 from stopping there's no custom ID and
//the Dir format is custonm.
if (FDirFormat = ftpdfCustom) and (Trim(FCustomSystID) = '') then begin
CmdNotImplemented(ASender);
Exit;
end;
if LContext.IsAuthenticated(ASender) then begin
ASender.Reply.SetReply(215, DoSysType(LContext));
end;
end;
procedure TIdFTPServer.CommandSTAT(ASender: TIdCommand);
var
LStream: TStringList;
LActAsList: boolean;
LSwitches, LPath : String;
i : Integer;
LContext : TIdFTPServerContext;
begin
LContext := ASender.Context as TIdFTPServerContext;
LActAsList := (ASender.Params.Count > 0);
if not LActAsList then begin
if LContext.UserSecurity.DisableSTATCommand then begin
if ASender.UnparsedParams = '' then begin
CmdNotImplemented(ASender);
Exit;
end;
end;
end;
if LContext.IsAuthenticated(ASender) then begin
if Assigned(LContext.DataChannel) then begin
if not LContext.DataChannel.Stopped then begin
LActAsList := False;
end;
end;
if not LActAsList then begin
ASender.Reply.NumericCode := 211;
ASender.Reply.Text.Clear;
if Assigned(FOnStat) then begin
LStream := TStringList.Create;
try
SetRFCReplyFormat(ASender.Reply);
FOnStat(LContext, LStream);
for i := 0 to LStream.Count - 1 do begin
ASender.Reply.Text.Add(' ' + TrimLeft(LStream[i])); {Do not Localize}
end;
finally
FreeAndNil(LStream);
end;
end;
ASender.Reply.Text.Insert(0,RSFTPCmdStartOfStat);
ASender.Reply.Text.Add(RSFTPCmdEndOfStat);
end else begin //else act as LIST command without a data channel
LStream := TStringList.Create;
try
LSwitches := '';
LPath := ASender.UnparsedParams;
if TextStartsWith(LPath, '-') then begin
LSwitches := Fetch(LPath);
end;
ListDirectory(LContext, DoProcessPath(LContext, LPath), LStream, True, LSwitches);
//we use IOHandler.WriteLn here because we need better control over what
//we send than what Reply.SendReply offers. This is important as the dir
//is written using WriteStrings and I found that with Reply.SetReply, a stat
//reply could throw off a FTP client.
LContext.Connection.IOHandler.WriteLn(IndyFormat('213-%s', [RSFTPDataConnToOpen])); {Do not Localize}
LContext.Connection.IOHandler.Write(LStream, False, IndyTextEncoding(NLSTEncType[LContext.NLSTUtf8]));
ASender.PerformReply := True;
ASender.Reply.SetReply(213, RSFTPCmdEndOfStat);
finally
FreeAndNil(LStream);
end;
end;
end;
end;
procedure TIdFTPServer.CommandFEAT(ASender: TIdCommand);
const
MFFPREFIX = 'MFF '; {Do not Localize}
var
LTmp : String;
LContext: TIdFTPServerContext;
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
begin
LContext := TIdFTPServerContext(ASender.Context);
LFileSystem := FTPFileSystem;
ASender.Reply.Clear;
SetRFCReplyFormat(ASender.Reply);
ASender.Reply.NumericCode := 211;
ASender.Reply.Text.Add(RSFTPCmdExtsSupportedStart); {Do not translate}
//AUTH
if IOHandler is TIdServerIOHandlerSSLBase then begin
if (FUseTLS <> utUseImplicitTLS) then begin
ASender.Reply.Text.Add('AUTH TLS;AUTH TLS-C;SSL;TLS-P;'); {Do not translate}
end;
end;
//AVBL
if Assigned(FOnAvailDiskSpace) then begin
ASender.Reply.Text.Add('AVBL');
end;
//CCC
if (FUseTLS <> utNoTLSSupport) then begin
ASender.Reply.Text.Add('CCC'); {Do not translate}
end;
//CLNT
if Assigned(FOnClientID) then begin
ASender.Reply.Text.Add('CLNT'); {Do not translate}
end;
//COMB
if Assigned(FOnCombineFiles) or Assigned(LFileSystem) then begin
ASender.Reply.Text.Add('COMB target;source_list'); {Do not translate}
end;
//CPSV
//CPSV is not supported in IPv6 - same problem as PASV
if (UseTLS <> utNoTLSSupport) and (LContext.Binding.IPVersion = Id_IPv4) then begin
ASender.Reply.Text.Add('CPSV'); {Do not translate}
end;
//DSIZ
if Assigned(OnCompleteDirSize) then begin
ASender.Reply.Text.Add('DSIZ'); {Do not localize}
end;
//EPRT
ASender.Reply.Text.Add('EPRT'); {Do not translate}
//EPSV
ASender.Reply.Text.Add('EPSV'); {Do not translate}
//Host
if Assigned(FOnHostCheck) then begin
ASender.Reply.Text.Add('HOST domain'); {Do not localize}
end;
//
//This is not proper but FTP Voyager uses it to determine if the -T parameter
//will work.
if Assigned(FOnListDirectory) then begin
//we do things this way because the 'a' and 'T' swithces only make sense
//when listing Unix dirs.
LTmp := 'LIST -l'; {Do not translate}
if SupportTaDirSwitches(LContext) then begin
LTmp := LTmp + 'aT'; {Do not translate}
end;
ASender.Reply.Text.Add(LTmp); {do not localize}
end;
//MDTM
if Assigned(FOnGetFileDate) or Assigned(LFileSystem) then begin
ASender.Reply.Text.Add('MDTM'); {Do not translate}
//MDTM YYYYMMDDHHMMSS filename
if Assigned(FOnSetModifiedTime) then begin
// ASender.Reply.Text.Add('MDTM YYYYMMDDHHMMSS[+-TZ];filename');
//Indicate that we wish to use FTP Voyager's old MDTM variation for seting time.
//time is returned as local (relative to server's timezone. We do this for compatibility
ASender.Reply.Text.Add('MDTM YYYYMMDDHHMMSS filename'); {Do not translate}
end;
end;
//MFCT
if Assigned(FOnSetCreationTime) then begin
ASender.Reply.Text.Add('MFCT'); {Do not Localize}
//TODO: The logic for the MMF entry may need to change if we
//support modifying more facts
end;
//MFF
LTmp := MFFPREFIX; {Do not localize}
if Assigned(FOnSetCreationTime) and (mlsdFileLastAccessTime in FMLSDFacts) then begin
LTmp := LTmp + 'Create;'; {Do not Localize}
end;
if Assigned(FOnSetModifiedTime) or Assigned(LFileSystem) then begin
LTmp := LTmp + 'Modify;'; {Do not Localize}
end;
if Assigned(FOnSiteCHMOD) then begin
LTmp := LTmp + 'Unix.mode;';
end;
if Assigned(FOnSiteCHOWN) then begin
LTmp := LTmp + 'Unix.owner;';
end;
if Assigned(FOnSiteCHGRP) then begin
LTmp := LTmp + 'Unix.group;';
end;
if Assigned(FOnSiteUTIME) and (mlsdFileLastAccessTime in FMLSDFacts) then begin
LTmp := LTmp + 'Windows.lastaccesstime;';
end;
if Assigned(FOnSetATTRIB) then begin
LTmp := LTmp + 'Win32.ea;';
end;
if LTmp <> MFFPREFIX then begin
ASender.Reply.Text.Add(LTmp);
end;
//MFMT
if Assigned(FOnSetModifiedTime) or Assigned(LFileSystem) then begin
ASender.Reply.Text.Add('MFMT'); {Do not Localize}
end;
//MLST
if Assigned(FOnListDirectory) then begin
ASender.Reply.Text.Add('MLSD'); {Do not translate}
ASender.Reply.Text.Add(MLSFEATLine(FMLSDFacts, LContext.MLSOpts)); {Do not translate}
end;
//MODE Z
if Assigned(FCompressor) then begin
ASender.Reply.Text.Add('MODE Z'); {do not localize}
end;
//OPTS
LTmp := 'OPTS ';
if Assigned(FOnListDirectory) then begin
LTmp := LTmp + 'MLST;';
end;
if Assigned(FCompressor) then begin
LTmp := LTmp + 'MODE;';
end;
LTmp := LTmp + 'UTF8';
ASender.Reply.Text.Add(LTmp);
//PBSZ
if (FUseTLS <> utNoTLSSupport) then begin
ASender.Reply.Text.Add('PBSZ'); {Do not translate}
end;
//PROT
if (FUseTLS <> utNoTLSSupport) then begin
ASender.Reply.Text.Add('PROT'); {Do not translate}
end;
//REST STREAM
ASender.Reply.Text.Add('REST STREAM'); {Do not translate}
//RMDA
if Assigned(FOnRemoveDirectoryAll) then begin
ASender.Reply.Text.Add('RMDA directoryname'); {Do not localize}
end;
//SITE ZONE
//Listing a SITE command in feature negotiation is unusual and
//may be a little off-spec. FTP Voyager scans this looking for
//SITE ZONE and if it's present, it will use the SITE ZONE
//to help it convert the time to the user's local time zone.
//The only other way that FTP Voyager would know is if the initial
//FTP greeting banner started with "Serv-U FTP-Server v2.5f" which
//is more problematic because Serve-U is a trademark and we would then
//then be stuck with a situation where everyone has to use it down the road.
//This would amount to the same mess we had with "Mozilla" in the HTTP
//User-Agent header field.
//also list other supported site commands;
LTmp := 'SITE ZONE';
if Assigned(FOnSetATTRIB) then begin
LTmp := LTmp + ';ATTRIB';
end;
if Assigned(FOnSiteUMASK) then begin
LTmp := LTmp + 'UMASK';
end;
if Assigned(FOnSiteCHMOD) then begin
LTmp := LTmp + ';CHMOD';
end;
if (FDirFormat = ftpdfDOS) or
((FDirFormat = ftpdfOSDependent) and (GOSType = otWindows)) then begin
LTmp := LTmp + ';DIRSTYLE';
end;
if Assigned(OnSiteUTIME) or Assigned(OnSetModifiedTime) then begin
LTmp := LTmp + ';UTIME';
end;
if Assigned(OnSiteCHOWN) then begin
LTmp := LTmp + ';CHOWN';
end;
if Assigned(OnSiteCHGRP) then begin
LTmp := LTmp + ';CHGRP';
end;
ASender.Reply.Text.Add(LTmp); {do not localize}
//SIZE
if Assigned(FOnGetFileSize) or Assigned(LFileSystem) then begin
ASender.Reply.Text.Add('SIZE'); {do not localize}
end;
//SPSV
ASender.Reply.Text.Add('SPSV'); {do not localize}
//SSCN
if UseTLS <> utNoTLSSupport then begin
ASender.Reply.Text.Add('SSCN'); {do not localize}
end;
//STAT -l
//Some servers such as Microsoft FTP Service, RaidenFTPD, and a few others,
//treat a STAT -l as a LIST command, only it's sent on the control connection.
//Some versions of Flash FXP can also use this as an option to improve efficiency.
if Assigned(FOnListDirectory) then begin
//we do things this way because the 'a' and 'T' swithces only make sense
//when listing Unix dirs.
LTmp := 'STAT -l'; {Do not translate}
if SupportTaDirSwitches(LContext) then begin
LTmp := LTmp + 'aT'; {Do not translate}
end;
ASender.Reply.Text.Add(LTmp); {do not localize}
end;
//TVFS
if FPathProcessing <> ftppCustom then begin
//TVFS should not be indicated for custom parsing because
//we don't know what a person will do.
ASender.Reply.Text.Add('TVFS'); {Do not localize}
end;
// UTF-8
// RFC 2640 says that "Servers MUST support the UTF-8 feature in response to the FEAT command [RFC2389]."
// TODO: finish actually implementing UTF-8 support
ASender.Reply.Text.Add('UTF8'); {Do not localize}
//XCRC
if Assigned(FOnCRCFile) or Assigned(LFileSystem) then begin
if not GetFIPSMode then begin
ASender.Reply.Text.Add('XCRC "filename" SP EP');//filename;start;end'); {Do not Localize}
ASender.Reply.Text.Add('XMD5 "filename" SP EP');//filename;start;end'); {Do not Localize}
end;
ASender.Reply.Text.Add('XSHA1 "filename" SP EP');//filename;start;end'); {Do not Localize}
if TIdHashSHA256.IsAvailable then begin
ASender.Reply.Text.Add('XSHA256 "filename" SP EP'); //file;start/end
end;
if TIdHashSHA512.IsAvailable then begin
ASender.Reply.Text.Add('XSHA512 "filename" SP EP'); //file;start/end
end;
end;
//I'm doing things this way with complience level to match the current
//version of NcFTPD
LTmp := 'RFC 959 2389 ';
if LContext.UserSecurity.FInvalidPassDelay <> 0 then begin
LTmp := LTmp + '2577 ';
end;
LTmp := LTmp + '3659 '; {Do not Localize}
if IOHandler is TIdServerIOHandlerSSLBase then begin
if (FUseTLS <> utUseImplicitTLS) then begin
LTmp := LTmp + '4217 '; {Do not localize}
end;
end;
ASender.Reply.Text.Add(Trim(LTmp)); {Do not Localize}
ASender.Reply.Text.Add(RSFTPCmdExtsSupportedEnd);
end;
procedure TIdFTPServer.CommandOPTS(ASender: TIdCommand);
var
LCmd : String;
begin
LCmd := ASender.UnparsedParams;
ASender.Reply.Clear;
if TextIsSame(Fetch(LCmd, ' ', False), 'MLST') then begin {do not localize}
//just in case the user doesn't create a ListDirectory event.
if not Assigned(FOnListDirectory) then begin
ASender.Reply.SetReply(501, RSFTPOptNotRecog);
Exit;
end;
end;
if not FOPTSCommands.HandleCommand(ASender.Context, LCmd) then begin
ASender.Reply.SetReply(501, RSFTPOptNotRecog);
end else begin
//we don't want an extra 200 reply.
ASender.PerformReply := False;
end;
end;
procedure TIdFTPServer.CommandSIZE(ASender: TIdCommand);
var
s: string;
LSize: Int64;
LContext : TIdFTPServerContext;
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
begin
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
LFileSystem := FFTPFileSystem;
if Assigned(FOnGetFileSize) or Assigned(LFileSystem) then begin
LSize := -1;
s := DoProcessPath(LContext, ASender.UnparsedParams);
DoOnGetFileSize(LContext, s, LSize);
if LSize > -1 then begin
ASender.Reply.SetReply(213, IntToStr(LSize));
end else begin
CmdFileActionAborted(ASender);
end;
end else begin
CmdSyntaxError(ASender);
end;
end;
end;
procedure TIdFTPServer.DoOnChangeDirectory(AContext: TIdFTPServerContext; var VDirectory: TIdFTPFileName);
var
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
begin
LFileSystem := FFTPFileSystem;
if Assigned(LFileSystem) then begin
LFileSystem.ChangeDir(AContext, VDirectory);
end else if Assigned(FOnChangeDirectory) then begin
FOnChangeDirectory(AContext, VDirectory);
end;
end;
procedure TIdFTPServer.DoOnRemoveDirectory(AContext: TIdFTPServerContext; var VDirectory: TIdFTPFileName);
var
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
begin
LFileSystem := FFTPFileSystem;
if Assigned(LFileSystem) then begin
LFileSystem.RemoveDirectory(AContext, VDirectory);
end else if Assigned(FOnRemoveDirectory) then begin
FOnRemoveDirectory(AContext, VDirectory);
end;
end;
procedure TIdFTPServer.DoOnMakeDirectory(AContext: TIdFTPServerContext; var VDirectory: TIdFTPFileName);
var
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
begin
LFileSystem := FFTPFileSystem;
if Assigned(LFileSystem) then begin
LFileSystem.MakeDirectory(AContext, VDirectory);
end else if Assigned(FOnMakeDirectory) then begin
FOnMakeDirectory(AContext, VDirectory);
end;
end;
procedure TIdFTPServer.CommandEPRT(ASender: TIdCommand);
var
LParm, LIP: string;
LDelim: char;
LReqIPVersion: TIdIPVersion;
LContext : TIdFTPServerContext;
LDataChannel: TIdTCPClient;
begin
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
LContext.FPASV := False;
LParm := ASender.UnparsedParams;
if Length(LParm) = 0 then begin
LContext.FDataPortDenied := True;
CmdInvalidParamNum(ASender);
Exit;
end;
if FFTPSecurityOptions.BlockAllPORTTransfers then begin
LContext.FDataPortDenied := True;
ASender.Reply.SetReply(502, RSFTPPORTDisabled);
Exit;
end;
LDelim := LParm[1];
Fetch(LParm, LDelim);
case IndyStrToInt(Fetch(LParm, LDelim), -1) of
1: LReqIPVersion := Id_IPv4;
2: if GStack.SupportsIPv6 then begin
LReqIPVersion := Id_IPv6;
end else begin
LContext.FDataPort := 0;
LContext.FDataPortDenied := True;
ASender.Reply.SetReply(522, IndyFormat(RSFTPNetProtNotSup, ['1'])); {Do not translate}
Exit;
end;
else
begin
LContext.FDataPort := 0;
LContext.FDataPortDenied := True;
ASender.Reply.SetReply(522, IndyFormat(RSFTPNetProtNotSup, [iif(GStack.SupportsIPv6, '1,2', '1')])); {Do not translate}
Exit;
end;
end;
LIP := Fetch(LParm, LDelim);
if Length(LIP) = 0 then begin
LContext.FDataPort := 0;
LContext.FDataPortDenied := True;
ASender.Reply.SetReply(500, RSFTPInvalidIP);
Exit;
end;
LContext.FDataPort := TIdPort(IndyStrToInt(Fetch(LParm, LDelim), 0));
if LContext.FDataPort = 0 then begin
LContext.FDataPortDenied := True;
ASender.Reply.SetReply(500, RSFTPInvalidPort);
Exit;
end;
if FFTPSecurityOptions.NoReservedRangePORT and
((LContext.FDataPort > 0) and (LContext.FDataPort <= 1024)) then begin
LContext.FDataPort := 0;
LContext.FDataPortDenied := True;
ASender.Reply.SetReply(504, RSFTPPORTRange);
Exit;
end;
if FFTPSecurityOptions.FRequirePORTFromSameIP then begin
case LReqIPVersion of
Id_IPv4: LIP := MakeCanonicalIPv4Address(LIP);
Id_IPv6: LIP := MakeCanonicalIPv6Address(LIP);
end;
if LIP <> LContext.Binding.PeerIP then begin
LContext.FDataPort := 0;
LContext.FDataPortDenied := True;
ASender.Reply.SetReply(504, RSFTPSameIPAddress);
Exit;
end;
end;
LContext.CreateDataChannel(False);
LDataChannel := TIdTCPClient(LContext.FDataChannel.FDataChannel);
LDataChannel.Host := LIP;
LDataChannel.Port := LContext.FDataPort;
LDataChannel.IPVersion := LReqIPVersion;
LContext.FDataPortDenied := False;
CmdCommandSuccessful(ASender, 200);
end;
end;
procedure TIdFTPServer.CommandEPSV(ASender: TIdCommand);
var
LParam: string;
LBPortMin, LBPortMax: Word;
LIP : String;
LIPVersion: TIdIPVersion;
LReqIPVersion: TIdIPVersion;
LContext : TIdFTPServerContext;
LDataChannel: TIdSimpleServer;
begin
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
LIPVersion := LContext.Binding.IPVersion;
LReqIPVersion := LIPVersion;
LParam := ASender.UnparsedParams;
if Length(LParam) > 0 then begin
case IndyStrToInt(LParam, -1) of
1: LReqIPVersion := Id_IPv4;
2: if GStack.SupportsIPv6 then begin
LReqIPVersion := Id_IPv6;
end else begin
ASender.Reply.SetReply(522, IndyFormat(RSFTPNetProtNotSup, ['1'])); {do not localize}
Exit;
end;
else
begin
if TextIsSame(LParam, 'ALL') then begin { do not localize }
LContext.FEPSVAll := True;
ASender.Reply.SetReply(200, RSFTPEPSVAllEntered);
end else begin
ASender.Reply.SetReply(522, IndyFormat(RSFTPNetProtNotSup, [iif(GStack.SupportsIPv6, '1,2', '1')])); {do not localize}
end;
Exit;
end;
end;
end;
if LReqIPVersion = LIPVersion then begin
LIP := LContext.Binding.IP;
end;
if (FPASVBoundPortMin <> 0) and (FPASVBoundPortMax <> 0) then begin
LBPortMin := FPASVBoundPortMin;
LBPortMax := FPASVBoundPortMax;
end else begin
LBPortMin := FDefaultDataPort;
LBPortMax := LBPortMin;
end;
DoOnPASVBeforeBind(LContext, LIP, LBPortMin, LBPortMax, LReqIPVersion);
LContext.CreateDataChannel(True);
LDataChannel := TIdSimpleServer(LContext.FDataChannel.FDataChannel);
LDataChannel.BoundIP := LIP;
if LBPortMin = LBPortMax then begin
LDataChannel.BoundPort := LBPortMin;
LDataChannel.BoundPortMin := 0;
LDataChannel.BoundPortMax := 0;
end else begin
LDataChannel.BoundPort := 0;
LDataChannel.BoundPortMin := LBPortMin;
LDataChannel.BoundPortMax := LBPortMax;
end;
LDataChannel.IPVersion := LReqIPVersion;
LDataChannel.BeginListen;
LIP := LDataChannel.Binding.IP;
LBPortMin := LDataChannel.Binding.Port;
//Note that only one Port can work with EPSV
DoOnPASVReply(LContext, LIP, LBPortMin, LReqIPVersion);
LParam := '|||' + IntToStr(LBPortMin) + '|'; {Do not localize}
ASender.Reply.SetReply(229, IndyFormat(RSFTPEnteringEPSV, [LParam]));
ASender.SendReply;
LContext.FPASV := True;
end;
end;
procedure TIdFTPServer.CommandMDTM(ASender: TIdCommand);
var
s: string;
LDate: TDateTime;
LContext : TIdFTPServerContext;
LSDate : String;
LExists : Boolean;
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
{
I know that this code and design are a mess.
There are actually two forms of MDTM and they mean different things.
The formal spec indicates that anything after the space in MDTM <filename>
is the filename.
FTP Voyager and some other clients abuse the MDTM command by using it to specify
a timestamp for the "Modified Time" on a file. The format is like this:
MDTM YYYYMMDDHHMMSS filename
Thus, there's an ambiguity.
Does MDTM 20031229152022 ESBAdDemo.exe mean
1) Set the date time on ESBAdDemo.exe to 12/29/2003 3:20:22 PM
or
2) Get the time for a file named 20031229152022 ESBAdDemo.exe
To resolve this ambiguity, we check specifically for a valid date, and then see
if a file, 20031229152022 ESBAdDemo.exe really does exist. If not, we interpret
MDTM as a set date command. Otherwise, we will traat it as a request for the timestamp
of a file, 20031229152022 ESBAdDemo.exe.
Note also that the time is sometimes given as either relative to the local server
or an offset is provided.
Note from:
http://www.ftpvoyager.com/releasenotes.asp
====
Added support for RFC change and the MDTM. MDTM requires sending the server
GMT (UTC) instead of a "fixed" date and time. FTP Voyager supports this with
Serv-U automatically by checking the Serv-U version number and by checking the
response to the FEAT command for MDTM. Servers returning "MDTM" or
"MDTM YYYYMMDDHHMMSS[+-TZ] filename" will use the old method. Servers
returning "MDTM YYYYMMDDHHMMSS" only will use the new method where the date a
and time is GMT (UTC).
===
We will use the old form for compatiability with some older FTP Voyager clients
and because a few servers support the old form as well. I do this even though,
this is really inconsistant with what MDTM returns for a query request. I might
consider some type of support for the new form but I do not feel that such
things are just abuse of the MDTM command. That's why I prefer a separate command for
modifying file modification dates (MFMT).
}
begin
LFileSystem := FFTPFileSystem;
if Assigned(FOnGetFileDate) or Assigned(LFileSystem) then
begin
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
s := ASender.UnparsedParams;
LSDate := Fetch(s);
if IsMDTMDate(LSDate) then begin
s := DoProcessPath(LContext, ASender.UnparsedParams );
DoOnFileExistCheck(LContext, s, LExists);
if not LExists then begin
s := ASender.UnparsedParams;
Fetch(s);
s := DoProcessPath(LContext, s);
LDate := FTPMDTMToGMTDateTime(LSDate);
DoOnSetModifiedTime(LContext, s, LDate);
// Self.DoOnSetModifiedTime(LF,s, LSDate);
ASender.Reply.SetReply(253, 'Date/time changed okay.'); {do not localize}
Exit;
end;
end;
s := DoProcessPath(LContext, ASender.UnparsedParams);
LDate := 0;
DoOnGetFileDate(LContext, s, LDate);
if LDate > 0 then begin
ASender.Reply.SetReply(213, FTPGMTDateTimeToMLS(LDate));
end else begin
CmdFileActionAborted(ASender);
end;
end;
end else begin
CmdSyntaxError(ASender);
end;
end;
procedure TIdFTPServer.SetFTPSecurityOptions(const AValue: TIdFTPSecurityOptions);
begin
FFTPSecurityOptions.Assign(AValue);
end;
procedure TIdFTPServer.SetOnUserAccount(AValue: TOnFTPUserAccountEvent);
var
LCmd : TIdCommandHandler;
i : Integer;
begin
if FUserAccounts = nil then begin
FOnUserAccount := AValue;
for i := 0 to CommandHandlers.Count - 1 do begin
LCmd := CommandHandlers.Items[i];
if LCmd.Command = 'ACCT' then begin
if Assigned(AValue) then begin
LCmd.HelpSuperScript := '';
LCmd.Description.Text := ACCT_HELP_ENABLED;
end else begin
LCmd.HelpSuperScript := '*';
LCmd.Description.Text := ACCT_HELP_DISABLED;
end;
end;
end;
end;
end;
procedure TIdFTPServer.CommandAUTH(ASender: TIdCommand);
var
LIO : TIdSSLIOHandlerSocketBase;
LContext : TIdFTPServerContext;
begin
LContext := ASender.Context as TIdFTPServerContext;
if (PosInStrArray(ASender.UnparsedParams, TLS_AUTH_NAMES) > -1) and (IOHandler is TIdServerIOHandlerSSLBase)
and (FUseTLS in ExplicitTLSVals) then begin
ASender.Reply.SetReply(234,RSFTPAuthSSL);
ASender.SendReply;
LIO := ASender.Context.Connection.IOHandler as TIdSSLIOHandlerSocketBase;
LIO.Passthrough := False;
{
This is from:
http://www.ford-hutchinson.com/~fh-1-pfh/ftps-ext.html#bad
and we implement things this way for historical reasons so
we don't break older and newer clients.
}
case PosInStrArray(ASender.UnparsedParams, TLS_AUTH_NAMES) of
0,2 : LContext.DataProtection := ftpdpsClear; //AUTH TLS, AUTH TLS-C
1,3 : LContext.DataProtection := ftpdpsPrivate; //AUTH SSL, AUTH TLS-P
end;
LContext.AuthMechanism := 'TLS'; {Do not localize}
end else begin
CmdSyntaxError(ASender);
end;
end;
procedure TIdFTPServer.CommandAVBL(ASender: TIdCommand);
var
LContext : TIdFTPServerContext;
LIsFile : Boolean;
LSize : Int64;
LPath : String;
begin
LIsFile := True;
LSize := 0;
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
if Assigned(FOnAvailDiskSpace) then begin
LPath := DoProcessPath(LContext, ASender.UnparsedParams);
FOnAvailDiskSpace(LContext, LPath, LIsFile, LSize);
if LIsFile then begin
ASender.Reply.SetReply(550, IndyFormat(RSFTPIsAFile,[LPath]));
end else begin
ASender.Reply.SetReply(213, IntToStr(LSize));
end;
end else begin
CmdNotImplemented(ASender);
end;
end else begin
ASender.Reply.SetReply(550, RSFTPFileActionNotTaken);
end;
end;
//FOnCompleteDirSize
procedure TIdFTPServer.CommandDSIZ(ASender : TIdCommand);
var
LContext : TIdFTPServerContext;
LIsFile : Boolean;
LSize : Int64;
LPath : String;
begin
LIsFile := True;
LSize := 0;
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
if Assigned(FOnCompleteDirSize) then begin
LPath := DoProcessPath(LContext, ASender.UnparsedParams);
FOnCompleteDirSize(LContext, LPath, LIsFile, LSize);
if LIsFile then begin
ASender.Reply.SetReply(550, IndyFormat(RSFTPIsAFile,[LPath]));
end else begin
ASender.Reply.SetReply(213, IntToStr(LSize));
end;
end else begin
CmdNotImplemented(ASender);
end;
end else begin
ASender.Reply.SetReply(550, RSFTPFileActionNotTaken);
end;
end;
procedure TIdFTPServer.CommandRMDA(ASender : TIdCommand);
var
LContext : TIdFTPServerContext;
LPath : TIdFTPFileName;
begin
//FOnRemoveDirectoryAll: TOnDirectoryEvent;
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
if Assigned(FOnRemoveDirectoryAll) then begin
LPath := DoProcessPath(LContext, ASender.UnparsedParams);
FOnRemoveDirectoryAll(LContext, LPath);
ASender.Reply.SetReply(250, RSFTPFileActionCompleted);
end else begin
CmdNotImplemented(ASender);
end;
end else begin
ASender.Reply.SetReply(550, RSFTPFileActionNotTaken);
end;
end;
procedure TIdFTPServer.CommandCCC(ASender: TIdCommand);
var
LIO : TIdSSLIOHandlerSocketBase;
LContext : TIdFTPServerContext;
begin
LContext := ASender.Context as TIdFTPServerContext;
if FUseTLS <> utNoTLSSupport then begin
//Not sure if it's proper to require authentication before a CCC
//but it is a good idea anyway because you definately want to
//prevent eavesdropping
if LContext.IsAuthenticated(ASender) then begin
if LContext.FUserSecurity.PermitCCC then begin
ASender.Reply.SetReply(200, RSFTPClearCommandConnection);
ASender.SendReply;
LIO := ASender.Context.Connection.IOHandler as TIdSSLIOHandlerSocketBase;
LIO.Passthrough := True;
LContext.FCCC := True;
end else begin
ASender.Reply.SetReply(534, RSFTPClearCommandNotPermitted);
end;
end;
end else begin
CmdSyntaxError(ASender);
end;
end;
procedure TIdFTPServer.CommandPBSZ(ASender: TIdCommand);
{Note that this may have to be expanded and reworked for other AUTH mechanisms}
var
LContext : TIdFTPServerContext;
begin
LContext := ASender.Context as TIdFTPServerContext;
if IOHandler is TIdServerIOHandlerSSLBase then begin
if ASender.UnparsedParams = '' then begin
CmdInvalidParamNum(ASender);
Exit;
end;
if (LContext.AuthMechanism = '') and (FUseTLS <> utUseImplicitTLS) then begin
ASender.Reply.SetReply(503, RSFTPPBSZAuthDataRequired);
Exit;
end;
if LContext.FCCC then begin
ASender.Reply.SetReply(503, RSFTPPBSZNotAfterCCC);
Exit;
end;
if (LContext.AuthMechanism = 'TLS') or (FUseTLS = utUseImplicitTLS) then begin {Do not localize}
ASender.Reply.SetReply(200,RSFTPDataProtBuffer0);
LContext.DataPBSZCalled := True;
end
else if IsNumeric(ASender.UnparsedParams) then begin
ASender.Reply.SetReply(200,'PBSZ=0'); {Do not translate}
LContext.DataPBSZCalled := True;
end else begin
CmdInvalidParams(ASender);
end;
end else begin
CmdSyntaxError(ASender);
end;
end;
procedure TIdFTPServer.CommandPROT(ASender: TIdCommand);
const
LValidParams : array [0..3] of string = ('C','S','E','P'); {Do not translate}
{Note that this may have to be expanded and reworked for other AUTH mechanisms}
var
LContext : TIdFTPServerContext;
begin
LContext := ASender.Context as TIdFTPServerContext;
if IOHandler is TIdServerIOHandlerSSLBase then begin
if LContext.FCCC then begin
ASender.Reply.SetReply(503, RSFTPPBSZNotAfterCCC);
Exit;
end;
if not LContext.DataPBSZCalled then begin
ASender.Reply.SetReply(503, RSFTPPROTProtBufRequired);
Exit;
end;
case PosInStrArray(ASender.UnparsedParams, LValidParams) of
0 : begin
LContext.FDataProtection := ftpdpsClear;
ASender.Reply.SetReply(200, RSFTPProtTypeClear);
end;
1, 2 : ASender.Reply.SetReply(536, RSFTPInvalidProtTypeForMechanism);
3 : begin
LContext.FDataProtection := ftpdpsPrivate;
ASender.Reply.SetReply(200, RSFTPProtTypePrivate);
end;
else
ASender.Reply.SetReply(504, RSFTPInvalidForParam);
end;
end else begin
CmdNotImplemented(ASender);
end;
end;
procedure TIdFTPServer.CommandCOMB(ASender: TIdCommand);
var
LFileParts : TStringList;
LBuf : String;
LTargetFileName : String;
LContext : TIdFTPServerContext;
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
begin
LContext := ASender.Context as TIdFTPServerContext;
LFileSystem := FTPFileSystem;
if Assigned(FOnCombineFiles) or Assigned(LFileSystem) then begin
if LContext.IsAuthenticated(ASender) then begin
if ASender.UnparsedParams = '' then begin
CmdInvalidParamNum(ASender);
Exit;
end;
if Pos('"', ASender.UnparsedParams) > 0 then begin
LBuf := ASender.UnparsedParams;
Fetch(LBuf,'"');
LTargetFileName := Fetch(LBuf, '"');
LTargetFileName := DoProcessPath(LContext, LTargetFileName);
LBuf := Trim(LBuf);
LFileParts := TStringList.Create;
try
while LBuf <> '' do begin
Fetch(LBuf,'"');
LFileParts.Add(DoProcessPath(LContext, Fetch(LBuf,'"')));
end;
DoOnCombineFiles(LContext, LTargetFileName, LFileParts);
ASender.Reply.SetReply(250, RSFTPFileOpSuccess);
finally
FreeAndNil(LFileParts);
end;
end else begin
CmdInvalidParams(ASender);
end;
end;
end else begin
CmdNotImplemented(ASender);
end;
end;
procedure TIdFTPServer.DoConnect(AContext: TIdContext);
var
LGreeting : TIdReplyRFC;
begin
AContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_8Bit;
if AContext.Connection.IOHandler is TIdSSLIOHandlerSocketBase then begin
if FUseTLS = utUseImplicitTLS then begin
TIdSSLIOHandlerSocketBase(AContext.Connection.IOHandler).PassThrough := False;
end;
end;
(AContext as TIdFTPServerContext).FXAUTKey := MakeXAUTKey;
if Assigned(OnGreeting) then begin
LGreeting := TIdReplyRFC.Create(nil);
try
LGreeting.Assign(Greeting);
OnGreeting(TIdFTPServerContext(AContext), LGreeting);
ReplyTexts.UpdateText(LGreeting);
if (not GetFIPSMode) and FSupportXAUTH and (LGreeting.NumericCode = 220) then begin
(AContext as TIdFTPServerContext).FXAUTKey := IdFTPCommon.MakeXAUTKey;
XAutGreeting(AContext,LGreeting, GStack.HostName);
end;
AContext.Connection.IOHandler.Write(LGreeting.FormattedReply);
if Assigned(OnConnect) then begin
OnConnect(AContext);
end;
if LGreeting.NumericCode = 421 then begin
AContext.Connection.Disconnect(False);
end;
finally
FreeAndNil(LGreeting);
end;
end else begin
if (not GetFIPSMode) and FSupportXAUTH and (Greeting.NumericCode = 220) then begin
LGreeting := TIdReplyRFC.Create(nil);
try
LGreeting.Assign(Greeting);
XAutGreeting(AContext,LGreeting, GStack.HostName);
AContext.Connection.IOHandler.Write(LGreeting.FormattedReply);
if Assigned(OnConnect) then begin
OnConnect(AContext);
end;
if LGreeting.NumericCode = 421 then begin
AContext.Connection.Disconnect(False);
end;
finally
FreeAndNil(LGreeting);
end;
end else begin
inherited DoConnect(AContext);
end;
end;
end;
procedure TIdFTPServer.CommandQUIT(ASender: TIdCommand);
begin
if Assigned(FOnQuitBanner) then begin
FOnQuitBanner(TIdFTPServerContext(ASender.Context), ASender.Reply);
ASender.Disconnect := True;
end else begin
ASender.Reply.Assign(ASender.CommandHandler.NormalReply);
end;
ASender.Reply.SetReply(221, ASender.Reply.Text.Text);
end;
procedure TIdFTPServer.CommandMLSD(ASender: TIdCommand);
var
LStream: TStringList;
LSendData : Boolean;
LContext : TIdFTPServerContext;
begin
if not Assigned(OnListDirectory) then begin
CmdSyntaxError(ASender);
Exit;
end;
LContext := ASender.Context as TIdFTPServerContext;
LSendData := False;
if LContext.IsAuthenticated(ASender) then begin
if (not Assigned(LContext.FDataChannel)) or LContext.FDataPortDenied then begin
ASender.Reply.SetReply(425, RSFTPCantOpenData);
Exit;
end;
LStream := TStringList.Create;
try
ListDirectory(LContext, DoProcessPath(LContext, ASender.UnparsedParams),
LStream, TextIsSame(ASender.CommandHandler.Command, 'LIST'), 'MLSD'); {Do not translate}
LSendData := True;
finally
try
if LSendData then begin
//it should be safe to assume that the FDataChannel object exists because
//we checked it earlier
LContext.FDataChannel.Data := LStream;
LContext.FDataChannel.OKReply.SetReply(226, RSFTPDataConnClosed);
LContext.FDataChannel.ErrorReply.SetReply(426, RSFTPDataConnClosedAbnormally);
LContext.FDataChannel.FFtpOperation := ftpRetr;
ASender.Reply.SetReply(125, RSFTPDataConnToOpen);
ASender.SendReply;
DoDataChannelOperation(ASender);
end else begin
LContext.KillDataChannel;
ASender.Reply.SetReply(426, RSFTPDataConnClosedAbnormally);
end;
finally
LStream.Free;
end;
end;
end;
end;
procedure TIdFTPServer.CommandMLST(ASender: TIdCommand);
var
LStream : TStringList;
i : Integer;
LContext : TIdFTPServerContext;
LPath : String;
LDir : TIdFTPListOutput;
begin
if Assigned(OnListDirectory) or Assigned(FOnMLST) then begin
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
LStream := TStringList.Create;
try
LPath := DoProcessPath(LContext, ASender.UnparsedParams);
if Assigned(FOnMLST) then begin
LDir := TIdFTPListOutput.Create;
try
FOnMLST(LContext, LPath, LDir);
LDir.MLISTOutputDir(LStream, LContext.MLSOpts);
finally
FreeAndNil(LDir);
end;
end else begin
//this part is kept just for backwards compatibility
ListDirectory(LContext, LPath, LStream, True, 'MLST'); {Do not translate}
end;
ASender.Reply.Clear;
SetRFCReplyFormat(ASender.Reply);
ASender.Reply.NumericCode := 250;
ASender.Reply.Text.Add('Begin'); {do not localize}
for i := 0 to LStream.Count -1 do begin
ASender.Reply.Text.Add(' ' + LStream[i]);
end;
ASender.Reply.Text.Add('End'); {do not localize}
ASender.SendReply;
finally
FreeAndNil(LStream);
end;
end;
end else begin
CmdSyntaxError(ASender);
end;
end;
procedure TIdFTPServer.DoOnSetModifiedTime(AContext: TIdFTPServerContext; const AFileName : String; var VDateTime: TDateTime);
var
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
begin
LFileSystem := FTPFileSystem;
if Assigned(LFileSystem) then begin
LFileSystem.SetModifiedFileDate(AContext, AFileName, VDateTime);
end else if Assigned(FOnSetModifiedTime) then begin
FOnSetModifiedTime(AContext, AFileName, VDateTime);
end;
end;
procedure TIdFTPServer.DoOnSetModifiedTime(AContext: TIdFTPServerContext; const AFileName : String; var VDateTimeStr : String);
var
LTime : TDateTime;
begin
LTime := FTPMLSToGMTDateTime(VDateTimeStr);
DoOnSetModifiedTime(AContext, AFileName, LTime);
VDateTimeStr := FTPGMTDateTimeToMLS(LTime);
end;
procedure TIdFTPServer.DoOnSetCreationTime(AContext: TIdFTPServerContext; const AFileName : String; var VDateTime: TDateTime);
var
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
begin
LFileSystem := FTPFileSystem;
if Assigned(LFileSystem) then begin
//LFileSystem.SetCreationFileDate(AContext,AFileName,VDateTime);
end else if Assigned(FOnSetCreationTime) then begin
FOnSetCreationTime(AContext, AFileName, VDateTime);
end;
end;
procedure TIdFTPServer.DoOnSetCreationTimeGMT(AContext: TIdFTPServerContext;
const AFileName: String; var VDateTime: TDateTime);
begin
if Assigned(FOnSetCreationTime) then begin
FOnSetCreationTime(AContext, AFileName, VDateTime);
end;
end;
procedure TIdFTPServer.DoOnSetModifiedTimeGMT(AContext: TIdFTPServerContext;
const AFileName: String; var VDateTime: TDateTime);
begin
if Assigned(FOnSetModifiedTime) then begin
FOnSetModifiedTime(AContext, AFileName, VDateTime);
end;
end;
procedure TIdFTPServer.DoOnSetCreationTime(AContext: TIdFTPServerContext;
const AFileName : String; var VDateTimeStr : String);
var
LTime : TDateTime;
begin
LTime := FTPMLSToLocalDateTime(VDateTimeStr);
DoOnSetCreationTime(AContext, AFileName, LTime);
VDateTimeStr := FTPLocalDateTimeToMLS(LTime);
end;
procedure TIdFTPServer.CommandMFMT(ASender: TIdCommand);
var
LTimeStr, LFileName : String;
LContext : TIdFTPServerContext;
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
begin
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
LFilesystem := FTPFileSystem;
if Assigned(FOnSetModifiedTime) or Assigned(LFileSystem) then begin
LFileName := ASender.UnparsedParams;
LTimeStr := Fetch(LFileName);
LFileName := DoProcessPath(LContext, LFileName);
DoOnSetModifiedTime(LContext, LFileName, LTimeStr);
ASender.Reply.SetReply(213, IndyFormat('Modify=%s %s', [LTimeStr, LFileName])); {Do not translate}
end else begin
CmdSyntaxError(ASender);
end;
end;
end;
procedure TIdFTPServer.CommandMFCT(ASender: TIdCommand);
var
LTimeStr, LFileName : String;
LContext : TIdFTPServerContext;
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
begin
LContext := TIdFTPServerContext(ASender.Context);
if LContext.IsAuthenticated(ASender) then begin
LFileSystem := FTPFileSystem;
if Assigned(FOnSetCreationTime) or Assigned(LFileSystem) then begin
LFileName := ASender.UnparsedParams;
LTimeStr := Fetch(LFileName);
LFileName := DoProcessPath(LContext, LFileName);
DoOnSetCreationTime(LContext, LFileName, LTimeStr);
ASender.Reply.SetReply(213, IndyFormat('CreateTime=%s %s', [LTimeStr, LFileName])); {Do not translate}
end else begin
CmdSyntaxError(ASender);
end;
end;
end;
procedure TIdFTPServer.CommandMFF(ASender: TIdCommand);
var
LFacts : TStringList;
LFileName : String;
LValue : String;
s : String;
LContext : TIdFTPServerContext;
LAttrib : UInt32;
LAuth : Boolean;
LDummyDate1, LDummyDate2 : TDateTime;
LDate : TDateTime;
LCHMOD : Integer;
LDummy : String;
begin
LAuth := True;
LDummy := ''; //empty value for passing a var in case we need to do that
LContext := TIdFTPServerContext(ASender.Context);
//this may need to change if we make more facts to modify
if not Assigned(FOnSetModifiedTime) and not Assigned(FOnSetCreationTime) then begin
CmdSyntaxError(ASender);
Exit;
end;
s := '';
if ASender.UnparsedParams = '' then begin
CmdInvalidParamNum(ASender);
Exit;
end;
if LContext.IsAuthenticated(ASender) then begin
LFacts := TStringList.Create;
try
LFileName := ParseFacts(ASender.UnparsedParams, LFacts);
LFileName := DoProcessPath(LContext, LFileName);
if LFacts.Values['Modify'] <> '' then begin {Do not translate}
if Assigned(FOnSetModifiedTime) then begin
LValue := LFacts.Values['Modify']; {Do not translate}
DoOnSetModifiedTime(LContext, LFileName, LValue);
s := s + IndyFormat('Modify=%s;', [LValue]); {Do not translate}
end;
end;
if LFacts.Values['Create'] <> '' then begin {Do not translate}
if Assigned(FOnSetCreationTime) then begin
LValue := LFacts.Values['Create']; {Do not translate}
DoOnSetCreationTime(LContext, LFileName, LValue);
s := s + IndyFormat('Create=%s;', [LValue]); {Do not translate}
end;
end;
if LFacts.Values['Win32.ea'] <> '' then begin
if Assigned(FOnSetATTRIB) then begin
LValue := LFacts.Values['Win32.ea']; {Do not localize}
LAttrib := IndyStrToInt(LValue);
DoOnSetAttrib(LContext, LAttrib, LFileName, LAuth);
LValue := '0x' + IntToHex(LAttrib, 8);
s := s + IndyFormat('Win32.ea=%s;', [LValue]); {Do not translate}
end;
end;
if LFacts.Values['Unix.mode'] <> '' then begin
LValue := LFacts.Values['Unix.mode']; {Do not localize}
if Assigned(FOnSiteCHMOD) then begin
If IsValidPermNumbers(LValue) then begin
LCHMOD := IndyStrToInt(LValue);
DoOnSiteCHMOD(LContext, LCHMOD, LFileName, LAuth);
LValue := IndyFormat('%.4d', [LCHMOD]);
s := s + IndyFormat('Unix.mode=%s;', [LValue]); {Do not translate}
end;
end;
end;
if LFacts.Values['Unix.owner'] <> '' then begin {Do not localize}
LValue := LFacts.Values['Unix.owner']; {Do not localize}
if Assigned(FOnSiteCHOWN) then begin
DoOnSiteCHOWN(LContext, LValue, LDummy, LFileName, LAuth);
s := s + IndyFormat('Unix.owner=%s;', [LValue]); {Do not localize}
end;
end;
if LFacts.Values['Unix.group'] <> '' then begin {Do not localize}
LValue := LFacts.Values['Unix.group']; {Do not localize}
if Assigned(FOnSiteCHGRP) then begin
DoOnSiteCHGRP(LContext, LValue, LFileName, LAuth);
s := s + IndyFormat('Unix.group=%s;', [LValue]); {Do not localize}
end;
end;
if LFacts.Values['Windows.lastaccesstime'] <> '' then begin
LValue := LFacts.Values['Windows.lastaccesstime'];
if Assigned(FOnSiteUTIME) and (mlsdFileLastAccessTime in FMLSDFacts) then begin
LDate := FTPMLSToGMTDateTime(LValue);
LDummyDate1 := 0;
LDummyDate2 := 0;
FOnSiteUTIME(LContext, LFileName, LDate, LDummyDate1, LDummyDate2, LAuth);
LValue := FTPGMTDateTimeToMLS(LDate);
s := s + IndyFormat('Windows.lastaccesstime=%s;', [LValue]);
end;
end;
if s <> '' then begin
ASender.Reply.SetReply(213, s + ' ' + LFileName);
end else begin
ASender.Reply.SetReply(504, IndyFormat(RSFTPParamError, ['MFF'])); {Do not translate}
end;
finally
FreeAndNil(LFacts);
end;
end;
end;
function TIdFTPServer.GetMD5Checksum(ASender : TIdFTPServerContext; const AFileName : String ) : String;
var
LCalcStream : TStream;
begin
Result := '';
DoOnMD5Cache(ASender, AFileName, Result);
if Result = '' then begin
LCalcStream := nil;
DoOnCRCFile(ASender, AFileName, LCalcStream);
if Assigned(LCalcStream) then try
LCalcStream.Position := 0;
Result := CalculateCheckSum(TIdHashMessageDigest5, LCalcStream, 0, LCalcStream.Size);
DoOnMD5Verify(ASender, AFileName, Result);
finally
FreeAndNil(LCalcStream);
end;
end;
end;
procedure TIdFTPServer.CommandMMD5(ASender: TIdCommand);
var
LChecksum : String;
LRes : String;
LFiles : TStringList;
LError : Boolean;
i : Integer;
LContext : TIdFTPServerContext;
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
begin
LContext := ASender.Context as TIdFTPServerContext;
if GetFIPSMode then begin
CmdSyntaxError(ASender);
Exit;
end;
LError := False;
LChecksum := '';
LRes := '';
if LContext.IsAuthenticated(ASender) then begin
LFileSystem := FTPFileSystem;
if Assigned(FOnCRCFile) or Assigned(FOnMD5Cache) or Assigned(LFileSystem) then begin
LFiles := TStringList.Create;
try
ParseQuotedArgs(ASender.UnparsedParams, LFiles);
for i := 0 to LFiles.Count -1 do begin
LChecksum := GetMD5Checksum(LContext, DoProcessPath(LContext, UnquotedStr(LFiles[i])));
if LChecksum = '' then begin
LError := True;
Break;
end;
LRes := LRes + ',' + LFiles[i] + ' '+ LChecksum;
end;
IdDelete(LRes,1,1);
finally
FreeAndNil(LFiles);
end;
if LError then begin
//The http://www.potaroo.net/ietf/idref/draft-twine-ftpmd5/
//draft didn't specify 550 as an error.
CmdTwineFileActionAborted(ASender);
end else begin
ASender.Reply.SetReply(252, LRes);
end;
end else begin
CmdSyntaxError(ASender);
end;
end;
end;
procedure TIdFTPServer.CommandMD5(ASender: TIdCommand);
var
LChecksum : String;
LContext : TIdFTPServerContext;
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
begin
LContext := TIdFTPServerContext(ASender.Context);
if GetFIPSMode then begin
CmdSyntaxError(ASender);
Exit;
end;
LChecksum := '';
if LContext.IsAuthenticated(ASender) then begin
LFileSystem := FTPFileSystem;
if Assigned(FOnCRCFile) or Assigned(LFileSystem) then begin
LChecksum := GetMD5Checksum(LContext, DoProcessPath(LContext, ASender.UnparsedParams));
if LChecksum = '' then begin
CmdTwineFileActionAborted(ASender);
end else begin
ASender.Reply.SetReply(251, LChecksum);
end;
end else begin
CmdSyntaxError(ASender);
end;
end;
end;
procedure TIdFTPServer.DoOnMD5Verify(ASender: TIdFTPServerContext;
const AFileName, ACheckSum: String);
begin
if Assigned(OnMD5Verify) then begin
OnMD5Verify(ASender, AFileName, AChecksum);
end;
end;
procedure TIdFTPServer.DoOnMD5Cache(ASender: TIdFTPServerContext;
const AFileName: String; var VCheckSum: String);
begin
if Assigned(OnMD5Cache) then begin
OnMD5Cache(ASender, AFileName, VCheckSum);
end;
end;
procedure TIdFTPServer.DoDisconnect(AContext: TIdContext);
var
// under ARC, convert a weak reference to a strong reference before working with it
LUserAccounts: TIdCustomUserManager;
begin
LUserAccounts := FUserAccounts;
if Assigned(LUserAccounts) then begin
LUserAccounts.UserDisconnected(TIdFTPServerContext(AContext).UserName);
{$IFDEF USE_OBJECT_ARC}LUserAccounts := nil;{$ENDIF}
end;
inherited DoDisconnect(AContext);
end;
procedure TIdFTPServer.DoOnCRCFile(ASender: TIdFTPServerContext;
const AFileName: String; var VStream: TStream);
var
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
begin
LFileSystem := FTPFileSystem;
if Assigned(LFileSystem) then begin
LFileSystem.GetCRCCalcStream(ASender, AFileName, VStream);
end else if Assigned(FOnCRCFile) then begin
FOnCRCFile(ASender, AFileName, VStream);
end;
end;
procedure TIdFTPServer.DoOnCombineFiles(ASender: TIdFTPServerContext;
const ATargetFileName: string; AParts: TStrings);
var
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
begin
LFileSystem := FTPFileSystem;
if Assigned(LFileSystem) then begin
LFileSystem.CombineFiles(ASender, ATargetFileName, AParts);
end else if Assigned(FOnCombineFiles) then begin
FOnCombineFiles(ASender, ATargetFileName, AParts);
end;
end;
procedure TIdFTPServer.DoOnRenameFile(ASender: TIdFTPServerContext;
const ARenameFromFile, ARenameToFile: string);
var
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
begin
LFileSystem := FTPFileSystem;
if Assigned(LFileSystem) then begin
LFileSystem.RenameFile(ASender, ARenameToFile);
end else if Assigned(FOnRenameFile) then begin
FOnRenameFile(ASender, ARenameFromFile, ARenameToFile);
end;
end;
procedure TIdFTPServer.DoOnGetFileDate(ASender: TIdFTPServerContext;
const AFilename: string; var VFileDate: TDateTime);
var
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
begin
LFileSystem := FTPFileSystem;
if Assigned(LFileSystem) then begin
LFileSystem.GetFileDate(ASender, AFileName, VFileDate);
VFileDate := VFileDate - OffsetFromUTC;
end else if Assigned(FOnGetFileDate) then begin
FOnGetFileDate(ASender, AFileName, VFileDate);
end;
end;
procedure TIdFTPServer.DoOnGetFileSize(ASender: TIdFTPServerContext;
const AFilename: string; var VFileSize: Int64);
var
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
begin
LFileSystem := FTPFileSystem;
if Assigned(LFileSystem) then begin
LFileSystem.GetFileSize(ASender, AFileName, VFileSize);
end else if Assigned(FOnGetFileSize) then begin
FOnGetFileSize(ASender, AFileName, VFileSize);
end;
end;
procedure TIdFTPServer.DoOnDeleteFile(ASender: TIdFTPServerContext;
const APathName: string);
var
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
begin
LFileSystem := FTPFileSystem;
if Assigned(LFileSystem) then begin
LFileSystem.DeleteFile(ASender, APathName);
end else if Assigned(FOnDeleteFile) then begin
FOnDeleteFile(ASender, APathName);
end;
end;
procedure TIdFTPServer.SetUseTLS(AValue: TIdUseTLS);
begin
inherited SetUseTLS(AValue);
if AValue = utUseImplicitTLS then
begin
if DefaultDataPort = IdPORT_FTP_DATA then begin
DefaultDataPort := IdPORT_ftps_data;
end;
end
else if DefaultDataPort = IdPORT_ftps_data then begin
DefaultDataPort := IdPORT_FTP_DATA;
end;
end;
procedure TIdFTPServer.DisconUser(ASender: TIdCommand);
begin
ASender.Disconnect := True;
ASender.Reply.SetReply(421, RSFTPClosingConnection);
if Assigned(OnLoginFailureBanner) then begin
OnLoginFailureBanner(TIdFTPServerContext(ASender.Context), ASender.Reply);
ASender.Reply.SetReply(421, ASender.Reply.Text.Text);
end;
end;
procedure TIdFTPServer.SetRFCReplyFormat(AReply: TIdReply);
begin
if AReply is TIdReplyFTP then begin
TIdReplyFTP(AReply).ReplyFormat := rfIndentMidLines;
end;
end;
procedure TIdFTPServer.CommandSiteATTRIB(ASender : TIdCommand);
var
LContext : TIdFTPServerContext;
LFileName,
LAttrs : String;
LAttrVal : UInt32;
LPermitted : Boolean;
function ValidAttribStr(const AAttrib : String) : Boolean;
var i : Integer;
begin
Result := TextStartsWith(AAttrib, '+');
if Result then begin
Result := Length(AAttrib)>1;
if result then begin
if AAttrib = '+N' then begin
Exit;
end;
for i := 2 to Length(AAttrib) do begin
if not CharIsInSet(AAttrib,i,'RASH') then begin
Result := False;
break;
end;
end;
end;
end;
end;
begin
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
if Assigned(OnSetAttrib) then begin
LFileName := ASender.UnparsedParams;
LAttrs := Fetch(LFileName);
LPermitted := True;
LAttrs := UpperCase(LAttrs);
if TextStartsWith(LAttrs, '+') then begin
if ValidAttribStr(LAttrs) then begin
LAttrVal := 0;
ASender.Reply.Clear;
ASender.Reply.SetReply(220,'');
if IndyPos('R', LATTRS) > 0 then begin
LAttrVal := LAttrVal or IdFILE_ATTRIBUTE_READONLY;
ASender.Reply.Text.Add(RSFTPSiteATTRIBMsg+' : +FILE_ATTRIBUTE_READONLY'); {Do not localize}
end;
if IndyPos('A', LATTRS) > 0 then begin
LAttrVal := LAttrVal or IdFILE_ATTRIBUTE_ARCHIVE;
ASender.Reply.Text.Add(RSFTPSiteATTRIBMsg+' : +FILE_ATTRIBUTE_ARCHIVE'); {Do not localize}
end;
if IndyPos('S', LATTRS) > 0 then begin
LAttrVal := LAttrVal or IdFILE_ATTRIBUTE_SYSTEM;
ASender.Reply.Text.Add(RSFTPSiteATTRIBMsg+' : +FILE_ATTRIBUTE_SYSTEM'); {Do not localize}
end;
if IndyPos('H', LATTRS) > 0 then begin
LAttrVal := LAttrVal or IdFILE_ATTRIBUTE_HIDDEN;
ASender.Reply.Text.Add(RSFTPSiteATTRIBMsg+' : +FILE_ATTRIBUTE_HIDDEN'); {Do not localize}
end;
if IndyPos('N', LATTRS) > 0 then begin
LAttrVal := LAttrVal or IdFILE_ATTRIBUTE_NORMAL;
ASender.Reply.Text.Add(RSFTPSiteATTRIBMsg+' : +FILE_ATTRIBUTE_NORMAL'); {Do not localize}
end;
ASender.Reply.Text.Add(RSFTPSiteATTRIBMsg + IndyFormat(RSFTPSiteATTRIBDone, [IntToStr(Length(LAttrs)-1)]));
LFileName := DoProcessPath(LContext, LFileName);
DoOnSetATTRIB(LContext, LAttrVal, LFileName, LPermitted);
end else begin
ASender.Reply.SetReply(550,RSFTPSiteATTRIBInvalid);
Exit;
end;
if not LPermitted then begin
ASender.Reply.SetReply(553, RSFTPPermissionDenied);
end;
end else begin
ASender.Reply.SetReply(550,RSFTPSiteATTRIBInvalid);
Exit;
end;
end else begin
ASender.Reply.Assign(FReplyUnknownSITECommand);
end;
end;
end;
procedure TIdFTPServer.CommandSiteUTIME(ASender: TIdCommand);
procedure TryNewFTPSyntax(AContext: TIdFTPServerContext; ALSender: TIdCommand);
var
LgMTime : TDateTime;
LgPermitted : Boolean;
LFileName : String;
LDummy1, LDummy2 : TDateTime;
begin
//this is for gFTP Syntax
//such as: "SITE UTIME 20050815041129 /.bashrc"
LgPermitted := True;
if ALSender.Params.Count = 0 then begin
CmdSyntaxError(ALSender);
Exit;
end;
if IsValidTimeStamp(ALSender.Params[0]) then begin
LFileName := ALSender.UnparsedParams;
//This is local Time
LgMTime := FTPMLSToGMTDateTime(Fetch(LFileName)) - OffsetFromUTC;
LFileName := DoProcessPath(AContext, LFileName);
if Assigned(FOnSiteUTIME) then
begin
//indicate that both creation time and last access time should not be set
LDummy1 := 0;
LDummy2 := 0;
FOnSiteUTIME(AContext, LFileName, LDummy1, LgMTime, LDummy2, LgPermitted);
end
else if Assigned(FOnSetModifiedTime) then begin
FOnSetModifiedTime(AContext, LFileName, LgMTime);
end;
if LgPermitted then begin
ALSender.Reply.SetReply(200, RSFTPCHMODSuccessful);
end else begin
ALSender.Reply.SetReply(553, RSFTPPermissionDenied);
end;
end else
begin
CmdSyntaxError(ALSender);
end;
end;
var
LContext : TIdFTPServerContext;
LPermitted : Boolean;
LFileName : String;
LIdx : Integer;
LDateCount : Integer;
LAccessTime, LModTime, LCreateTime : TDateTime;
i : Integer;
begin
{
This is used by NcFTP like this:
SITE UTIME test.txt 20050731224504 20050731041205 20050731035940 UTC
where the first date is the "Last Access Time"
the second date is the "Last Modified Time"
and the final date is the "Creation File Time"
I think the third parameter is optional.
The final parameter is "UTC"
gFTP does something different. It does something like:
SITE UTIME 20050815041129 /.bashrc
where the timestamp is probably in based on the local time.
}
LPermitted := True;
LAccessTime := 0;
LModTime := 0;
LCreateTime := 0;
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then
begin
if Assigned(OnSiteUTIME) or Assigned(OnSetModifiedTime) or Assigned(OnSetCreationTime) then begin
LDateCount := 0;
LIdx := ASender.Params.Count - 1;
if ASender.Params.Count > 2 then begin
LPermitted := True;
if TextIsSame(ASender.Params[LIdx], 'UTC') then begin
//figure out how many dates we have and where the end of the filename is
Dec(LIdx);
Inc(LDateCount);
if IsValidTimeStamp(ASender.Params[LIdx]) then begin
Dec(LIdx);
Inc(LDateCount);
if IsValidTimeStamp(ASender.Params[LIdx]) then begin
Dec(LIdx);
Inc(LDateCount);
end;
end else begin
TryNewFTPSyntax(LContext, ASender);
Exit;
end;
//now extract the date
LAccessTime := FTPMLSToGMTDateTime(ASender.Params[LIdx]);
if LDateCount > 1 then begin
LModTime := FTPMLSToGMTDateTime(ASender.Params[LIdx+1]);
end;
if LDateCount > 2 then begin
LCreateTime := FTPMLSToGMTDateTime(ASender.Params[LIdx+2]);
end;
//extract filename including any spaces
LFileName := '';
for i := 0 to LIdx-1 do begin
LFileName := LFileName + ' ' + ASender.Params[i];
end;
IdDelete(LFileName,1,1);
LFileName := DoProcessPath(LContext,LFileName);
//now do it
if Assigned(FOnSiteUTIME) then begin
FOnSiteUTIME(LContext, LFileName, LAccessTime, LModTime, LCreateTime, LPermitted);
end else begin
if (LModTime <> 0) and Assigned(FOnSetModifiedTime) then begin
FOnSetModifiedTime(LContext, LFileName, LModTime);
end;
if (LCreateTime <> 0) and Assigned(FOnSetCreationTime) then begin
FOnSetCreationTime(LContext, LFileName, LCreateTime);
end;
end;
if LPermitted then begin
ASender.Reply.SetReply(200, RSFTPCHMODSuccessful);
end else begin
ASender.Reply.SetReply(553, RSFTPPermissionDenied);
end;
Exit;
end;
end;
end;
TryNewFTPSyntax(LContext, ASender);
// CmdNotImplemented(ASender);
end;
end;
procedure TIdFTPServer.DoOnSiteCHGRP(ASender: TIdFTPServerContext;
var AGroup: String; const AFileName: String; var VAUth: Boolean);
begin
if Assigned(FOnSiteCHGRP) then begin
FOnSiteCHGRP(ASender, AGroup, AFileName, VAuth);
end;
end;
procedure TIdFTPServer.DoOnSiteCHOWN(ASender: TIdFTPServerContext; var AOwner,
AGroup: String; const AFileName: String; var VAUth: Boolean);
begin
if Assigned(FOnSiteCHOWN) then begin
OnSiteCHOWN(ASender, AOwner, AGroup, AFileName, VAuth);
end;
end;
procedure TIdFTPServer.CommandSiteCHOWN(ASender: TIdCommand);
var
LContext : TIdFTPServerContext;
LPermitted : Boolean;
LFileName : String;
LOwner, LGroup : string;
begin
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
if Assigned(OnSiteCHOWN) then begin
LPermitted := True;
LFileName := ASender.UnparsedParams;
LGroup := Fetch(LFileName);
LOwner := Fetch(LGroup,':');
DoOnSiteCHOWN(LContext, LOwner, LGroup, DoProcessPath(LContext, LFileName), LPermitted);
if LPermitted then begin
ASender.Reply.SetReply(220, IndyFormat(RSFTPCmdSuccessful, [ASender.RawLine]));
end else begin
ASender.Reply.SetReply(553, RSFTPPermissionDenied);
end;
end;
end;
end;
procedure TIdFTPServer.CommandSiteCHGRP(ASender: TIdCommand);
var
LContext : TIdFTPServerContext;
LPermitted : Boolean;
LFileName : String;
LGroup : String;
begin
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
if Assigned(FOnSiteCHGRP) then begin
LPermitted := True;
LFileName := ASender.UnparsedParams;
LGroup := Fetch(LFileName);
DoOnSiteCHGRP(LContext, LGroup, DoProcessPath(LContext, LFileName), LPermitted);
if LPermitted then begin
ASender.Reply.SetReply(200, IndyFormat(RSFTPCmdSuccessful, [ASender.RawLine]));
end else begin
ASender.Reply.SetReply(553, RSFTPPermissionDenied);
end;
end;
end;
end;
procedure TIdFTPServer.CommandSiteCHMOD(ASender: TIdCommand);
var
LContext : TIdFTPServerContext;
LPermitted : Boolean;
LFileName : String;
LPerms : String;
LPermNo : Integer;
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
begin
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
LFileSystem := FTPFileSystem;
if Assigned(OnSiteCHMOD ) or Assigned(LFileSystem) then begin
LFileName := ASender.UnparsedParams;
LPerms := Fetch(LFileName);
If IsValidPermNumbers(LPerms) then begin
LPermitted := True;
LPermNo := IndyStrToInt(LPerms, 0);
DoOnSiteCHMOD(LContext, LPermNo, DoProcessPath(LContext, LFileName), LPermitted);
if LPermitted then begin
ASender.Reply.SetReply(220, RSFTPCHMODSuccessful);
end else begin
ASender.Reply.SetReply(553, RSFTPPermissionDenied);
end;
end else begin
CmdNotImplemented(ASender);
end;
end else begin
ASender.Reply.Assign(FReplyUnknownSITECommand);
end;
end;
end;
procedure TIdFTPServer.CommandSiteUMASK(ASender: TIdCommand);
var
LContext : TIdFTPServerContext;
LNewMask : Integer;
LPermitted : Boolean;
begin
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
if Assigned(FOnSiteUMASK) then begin
if ASender.Params.Count > 0 then begin
If IsValidPermNumbers(ASender.Params[0]) then begin
LPermitted := True;
LNewMask := IndyStrToInt(ASender.Params[0], 0);
DoOnSiteUMASK(LContext, LNewMask, LPermitted);
if LPermitted then begin
ASender.Reply.SetReply(200, IndyFormat(RSFTPUMaskSet, [LNewMask, LContext.FUMask]));
LContext.FUMask := LNewMask;
end else begin
ASender.Reply.SetReply(553, RSFTPPermissionDenied);
end;
end else begin
CmdNotImplemented(ASender);
end;
end else begin
ASender.Reply.SetReply(200, IndyFormat(RSFTPUMaskIs, [LContext.FUMask]));
end;
end else begin
CmdNotImplemented(ASender);
end;
end;
end;
function TIdFTPServer.IsValidPermNumbers(const APermNos: String): Boolean;
const
PERMDIGITS = '01234567';
var
i: Integer;
begin
Result := False;
for i := 1 to Length(APermNos) do begin
if not CharIsInSet(APermNos, i, PERMDIGITS) then begin
Exit;
end;
end;
Result := True;
end;
procedure TIdFTPServer.DoOnSiteUMASK(ASender: TIdFTPServerContext;
var VUMASK: Integer; var VAUth: Boolean);
begin
if Assigned(FOnSiteUMASK) then begin
FOnSiteUMASK(ASender,VUMASK,VAUth);
end;
end;
procedure TIdFTPServer.DoOnSetATTRIB(ASender: TIdFTPServerContext; var VAttr : UInt32; const AFileName : String; var VAUth : Boolean);
begin
if Assigned( FOnSetATTRIB) then begin
FOnSetATTRIB(ASender, VAttr, AFileName, VAUth);
end;
end;
procedure TIdFTPServer.DoOnSiteCHMOD(ASender: TIdFTPServerContext;
var APermissions: Integer; const AFileName: String; var VAUth: Boolean);
begin
if Assigned(FOnSiteCHMOD) then begin
FOnSiteCHMOD(ASender,APermissions,AFileName,VAUth);
end;
end;
procedure TIdFTPServer.CommandSiteDIRSTYLE(ASender: TIdCommand);
//FMSDOSMode
var
LContext : TIdFTPServerContext;
//SITE DIRSTYLE is only for MS-DOS formatted directory lists so
//a program can flip to Unix directory list format. This is
//for compatability purposes, ONLY.
begin
LContext := ASender.Context as TIdFTPServerContext;
if (FDirFormat = ftpdfDOS) or
((FDirFormat = ftpdfOSDependent) and (GOSType = otWindows)) then begin
if LContext.IsAuthenticated(ASender) then begin
if ASender.Params.Count = 0 then begin
LContext.FMSDOSMode := not LContext.FMSDOSMode;
if LContext.FMSDOSMode then begin
ASender.Reply.SetReply(200, IndyFormat(RSFTPDirStyle, [RSFTPOn]));
end else begin
ASender.Reply.SetReply(200, IndyFormat(RSFTPDirStyle, [RSFTPOff]));
end;
end;
end;
end else begin
ASender.Reply.Assign(FReplyUnknownSITECommand);
end;
end;
procedure TIdFTPServer.CommandSiteHELP(ASender: TIdCommand);
var
s : String;
LCmds : TStringList;
LContext : TIdFTPServerContext;
begin
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
s := RSFTPSITECmdsSupported+EOL;
LCmds := TStringList.Create;
try
if Assigned(OnSetAttrib) then begin
LCmds.Add('ATTRIB'); {Do not translate}
end;
if Assigned(OnSiteCHMOD) then begin
LCmds.Add('CHMOD'); {Do not translate}
end;
if (FDirFormat = ftpdfDOS) or
((FDirFormat = ftpdfOSDependent) and (GOSType = otWindows)) then begin
LCmds.Add('DIRSTYLE'); {Do not translate}
end;
if Assigned(OnSiteUMASK) then begin
LCmds.Add('UMASK'); {Do not translate}
end;
LCmds.Add('ZONE'); {Do not translate}
s := s + HelpText(LCmds) + FEndOfHelpLine;
ASender.Reply.SetReply(214, s);
finally
FreeAndNil(LCmds);
end;
end;
end;
function TIdFTPServer.HelpText(Cmds: TStrings): String;
var
LRows : Integer;
LMod : Integer;
i : Integer;
begin
Result := '';
if Cmds.Count =0 then begin
Exit;
end;
LRows := Cmds.Count div 6;
LMod := Cmds.Count mod 6;
if Cmds.Count < 6 then begin
Result := ' ';
for i := 0 to Cmds.Count -1 do begin
Result := Result + IndyFormat('%-10s', [Cmds[i]]);
end;
Result := Result + CR;
end else begin
for i := 0 to (LRows -1) do begin
if (i <= LMod-1) and (LMod<>0) then begin
Result := Result + IndyFormat(' %-10s%-10s%-10s%-10s%-10s%-10s%-10s', {Do not translate}
[ Cmds[i],Cmds[i+LRows],Cmds[(LRows*2)+i],
Cmds[(LRows*3)+i],Cmds[(LRows*4)+i],Cmds[(LRows*5)+i],
Cmds[(LRows*6)+i]])+CR;
end else begin
Result := Result + IndyFormat(' %-10s%-10s%-10s%-10s%-10s%-10s', {Do not translate}
[ Cmds[i],Cmds[i+LRows],Cmds[(LRows*2)+i],
Cmds[(LRows*3)+i],Cmds[(LRows*4)+i],Cmds[(LRows*5)+i]])+CR;
end;
end;
end;
end;
procedure TIdFTPServer.CommandSITE(ASender: TIdCommand);
var
LCmd : String;
begin
LCmd := ASender.UnparsedParams;
ASender.Reply.Clear;
ASender.PerformReply := False;
if not FSITECommands.HandleCommand(ASender.Context, LCmd) then begin
ASender.Reply.NumericCode := 500;
CmdSyntaxError(ASender.Context, ASender.CommandHandler.Command + ' ' + LCmd, ASender.Reply);
ASender.PerformReply := False;
end;
end;
function TIdFTPServer.MLSFEATLine(const AFactMask: TIdMLSDAttrs;
const AFacts: TIdFTPFactOutputs): String;
begin
Result := 'MLST size'; {Do not translate}
//the * indicates if the option is selected for MLST
if Size in AFacts then begin {Do not translate}
Result := Result + '*;';
end else begin
Result := Result + ';'
end;
Result := Result + 'Type'; {Do not translate}
if ItemType in AFacts then begin {Do not translate}
Result := Result + '*;'; {Do not translate}
end else begin
Result := Result + ';';
end;
if mlsdPerms in FMLSDFacts then begin
Result := Result + 'Perm'; {Do not translate}
if Perm in AFacts then begin {Do not translate}
Result := Result + '*;'; {Do not translate}
end else begin
Result := Result + ';';
end;
end;
if mlsdFileCreationTime in FMLSDFacts then begin
Result := Result + 'Create'; {Do not translate}
if CreateTime in AFacts then begin {Do not translate}
Result := Result + '*;'; {Do not translate}
end else begin
Result := Result + ';';
end;
end;
Result := Result + 'Modify'; {Do not translate}
if Modify in AFacts then begin
Result := Result + '*;';
end else begin
Result := Result + ';';
end;
if mlsdUnixModes in FMLSDFacts then begin
Result := Result + 'UNIX.mode'; {Do not translate}
if UnixMODE in AFacts then begin {Do not translate}
Result := Result + '*;'; {Do not translate}
end else begin
Result := Result + ';';
end;
end;
if mlsdUnixOwner in FMLSDFacts then
begin
Result := Result + 'UNIX.owner'; {Do not translate}
if UnixOwner in AFacts then begin {Do not translate}
Result := Result + '*;'; {Do not translate}
end else begin
Result := Result + ';';
end;
end;
if mlsdUnixGroup in FMLSDFacts then begin
Result := Result + 'UNIX.group'; {Do not translate}
if UnixGroup in AFacts then begin {Do not translate}
Result := Result + '*;'; {Do not translate}
end else begin
Result := Result + ';';
end;
end;
if mlsdUniqueID in FMLSDFacts then begin
Result := Result + 'Unique'; {Do not translate}
if Unique in AFacts then begin {Do not translate}
Result := Result + '*;'; {Do not translate}
end else begin
Result := Result + ';';
end;
end;
if mlsdFileLastAccessTime in FMLSDFacts then begin
Result := Result + 'Windows.lastaccesstime'; {Do not translate}
if CreateTime in AFacts then begin {Do not translate}
Result := Result + '*;'; {Do not translate}
end else begin
Result := Result + ';';
end;
end;
if mlsdWin32Attributes in FMLSDFacts then begin
Result := Result + 'Win32.ea'; {Do not translate}
if WinAttribs in AFacts then begin {Do not translate}
Result := Result + '*;'; {Do not translate}
end else begin
Result := Result + ';';
end;
end;
if mlsdWin32DriveType in FMLSDFacts then begin
Result := Result + 'Win32.dt';
if WinDriveType in AFacts then begin
Result := Result + '*;'; {Do not localize}
end else begin
Result := Result + ';'; {Do not localize}
end;
end;
if mlstWin32DriveLabel in FMLSDFacts then begin
Result := Result + 'Win32.dl';
if WinDriveLabel in AFacts then begin
Result := Result + '*;'; {Do not localize}
end else begin
Result := Result + ';'; {Do not localize}
end;
end;
if Length(Result)>0 then begin
IdDelete(Result,Length(Result),1);
end;
end;
procedure TIdFTPServer.CommandCLNT(ASender: TIdCommand);
begin
if Assigned(FOnClientID) then begin
FOnClientID(ASender.Context as TIdFTPServerContext, ASender.UnparsedParams);
end;
end;
procedure TIdFTPServer.SetPASVBoundPortMax(const AValue: TIdPort);
begin
if FPASVBoundPortMin <> 0 then begin
if AValue <= FPASVBoundPortMin then begin
raise EIdFTPBoundPortMaxGreater.Create(RSFTPPASVBoundPortMaxMustBeGreater);
end;
end;
FPASVBoundPortMax := AValue;
end;
procedure TIdFTPServer.SetPASVBoundPortMin(const AValue: TIdPort);
begin
if FPASVBoundPortMax <> 0 then begin
if FPASVBoundPortMax <= AValue then begin
raise EIdFTPBoundPortMinLess.Create(RSFTPPASVBoundPortMinMustBeLess);
end;
end;
FPASVBoundPortMin := AValue;
end;
procedure TIdFTPServer.DoOnDataPortAfterBind(ASender: TIdFTPServerContext);
begin
if Assigned(FOnDataPortAfterBind) then begin
FOnDataPortAfterBind(ASender);
end;
end;
procedure TIdFTPServer.DoOnDataPortBeforeBind(ASender: TIdFTPServerContext);
begin
if Assigned(FOnDataPortBeforeBind) then begin
FOnDataPortBeforeBind(ASender);
end;
end;
function TIdFTPServer.FTPNormalizePath(const APath: String): String;
{
Microsoft IIS accepts both a "/" and a "\" as path/file name separators.
We have to flatten this out so that our FTP server can behave like Microsoft IIS.
In Unix, a "\" is a valid filename character so we don't anything there.
This WILL cause a "\" to be treated differently in Unix and Win32. I submit that
this is really desirable as both file systems are like apples and oranges.
}
begin
case FPathProcessing of
ftppDOS : Result := ReplaceAll(APath, '\', '/');
ftpOSDependent :
begin
if GOSType = otWindows then begin
Result := ReplaceAll(APath, '\', '/');
end else begin
Result := APath;
end;
end;
else
Result := APath;
end;
end;
function TIdFTPServer.DoProcessPath(ASender: TIdFTPServerContext; const APath: TIdFTPFileName): TIdFTPFileName;
begin
if FPathProcessing <> ftppCustom then begin
Result := FTPNormalizePath(APath);
Result := ProcessPath(ASender.CurrentDir, Result); {Do not Localize}
end else begin
Result := APath;
if Assigned(FOnCustomPathProcess) then begin
FOnCustomPathProcess(ASender, Result);
end;
end;
end;
function TIdFTPServer.CDUPDir(AContext : TIdFTPServerContext) : String;
const
LCDUP_DOS = '..\';
CDUP_UNIX = '../';
begin
case FPathProcessing of
ftppDOS : Result := LCDUP_DOS;
ftpOSDependent :
if GOSType = otWindows then begin
Result := LCDUP_DOS;
end else begin
Result := CDUP_UNIX;
end;
ftppCustom : Result := DoProcessPath(AContext, '..');
else
Result := CDUP_UNIX;
end;
end;
function TIdFTPServer.DoSysType(ASender: TIdFTPServerContext): String;
begin
//We tie the SYST descriptor to the directory style for compatability
//reasons. Some FTP clients use the SYST descriptor to determine what
//type of FTP directory list parsing to do. I think TurboPower IPros does this.
//Note that I personally do not find this to be sound as a general rule.
case FDirFormat of
ftpdfOSDependent :
begin
if GOSType = otWindows then begin
Result := SYST_ID_NT;
end else begin
Result := SYST_ID_UNIX;
end;
end;
ftpdfUnix, ftpdfEPLF : Result := SYST_ID_UNIX;
ftpdfDOS : Result := SYST_ID_NT;
ftpdfCustom : Result := FCustomSystID;
end;
end;
procedure TIdFTPServer.DoOnCustomListDirectory(
ASender: TIdFTPServerContext; const APath: string;
ADirectoryListing: TStrings; const ACmd, ASwitches: String);
begin
if Assigned(OnCustomListDirectory) then begin
OnCustomListDirectory(ASender,APath,ADirectoryListing,ACmd,ASwitches);
end;
end;
procedure TIdFTPServer.CmdNotImplemented(ASender: TIdCommand);
begin
ASender.Reply.SetReply(550, IndyFormat(RSFTPCmdNotImplemented, [ASender.CommandHandler.Command ]));
end;
procedure TIdFTPServer.CmdFileActionAborted(ASender: TIdCommand);
begin
ASender.Reply.SetReply(550, RSFTPFileActionAborted);
end;
//This is for where the client didn't provide a valid number of parameters for a command
procedure TIdFTPServer.CmdInvalidParamNum(ASender: TIdCommand);
begin
ASender.Reply.SetReply(501, IndyFormat(RSFTPInvalidNumberArgs, [ASender.CommandHandler.Command]));
end;
//This is for other command syntax issues.
procedure TIdFTPServer.CmdInvalidParams(ASender: TIdCommand);
begin
ASender.Reply.SetReply(501, IndyFormat(RSFTPParamError, [ASender.CommandHandler.Command]));
end;
procedure TIdFTPServer.CmdTwineFileActionAborted(ASender: TIdCommand);
begin
ASender.Reply.SetReply(504, RSFTPFileActionAborted);
end;
procedure TIdFTPServer.CmdCommandSuccessful(ASender: TIdCOmmand; const AReplyCode : Integer = 250);
begin
ASender.Reply.SetReply(AReplyCode, IndyFormat(RSFTPCmdSuccessful, [ASender.CommandHandler.Command]));
end;
procedure TIdFTPServer.CommandSSCN(ASender: TIdCommand);
const
REPLY_SSCN_ON = 'SSCN:CLIENT METHOD'; {do not localize}
REPLY_SSCN_OFF = 'SSCN:SERVER METHOD'; {do not localize}
var
LContext : TIdFTPServerContext;
begin
if UseTLS = utNoTLSSupport then begin
CmdNotImplemented(ASender);
Exit;
end;
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
if ASender.Params.Count = 0 then begin
//check state
if LContext.SSCNOn then begin
ASender.Reply.SetReply(200, REPLY_SSCN_ON);
end else begin
ASender.Reply.SetReply(200, REPLY_SSCN_OFF);
end;
end else begin
//set state
case PosInStrArray(ASender.Params[0], OnOffStates, False) of
0 : //'ON'
begin
LContext.SSCNOn := True;
ASender.Reply.SetReply(200, REPLY_SSCN_ON);
end;
1 : //'OFF'
begin
LContext.SSCNOn := False;
ASender.Reply.SetReply(200, REPLY_SSCN_OFF);
end;
else
ASender.Reply.SetReply(504, RSFTPInvalidForParam);
end;
end;
end;
end;
procedure TIdFTPServer.CommandCPSV(ASender: TIdCommand);
var
LContext : TIdFTPServerContext;
LIO : TIdSSLIOHandlerSocketBase;
begin
//CPSV must be used with SSL and can only be used with IPv4
if (UseTLS = utNoTLSSupport) or
(ASender.Context.Binding.IPVersion <> Id_IPv4) then begin
CmdSyntaxError(ASender);
Exit;
end;
CommandPASV(ASender);
LContext := TIdFTPServerContext(ASender.Context);
LIO := LContext.DataChannel.FDataChannel.IOHandler as TIdSSLIOHandlerSocketBase;
//tell IOHandler to use ssl_Conntect
LIO.IsPeer := False;
end;
procedure TIdFTPServer.CommandSiteZONE(ASender: TIdCommand);
var
LMin : Integer;
begin
LMin := MinutesFromGMT;
//plus must always be displayed for positive numbers
if LMin < 0 then begin
ASender.Reply.SetReply(210, IndyFormat('UTC%d', [MinutesFromGMT])); {do not localize}
end else begin
ASender.Reply.SetReply(210, IndyFormat('UTC+%d', [MinutesFromGMT])); {do not localize}
end;
end;
procedure TIdFTPServer.CommandCheckSum(ASender: TIdCommand);
const
HashTypes: array[0..4] of TIdHashClass = (TIdHashCRC32, TIdHashMessageDigest5, TIdHashSHA1, TIdHashSHA256, TIdHashSHA512);
var
LCalcStream : TStream;
LFileName, LCheckSum, LBuf : String;
LBeginPos, LEndPos : TIdStreamSize;
LContext : TIdFTPServerContext;
LHashIdx: Integer;
// under ARC, convert a weak reference to a strong reference before working with it
LFileSystem: TIdFTPBaseFileSystem;
begin
if GetFIPSMode and
(PosInStrArray(ASender.CommandHandler.Command, ['XCRC', 'XMD5']) > -1) then begin
CmdSyntaxError(ASender);
Exit;
end;
LFileSystem := FTPFileSystem;
if Assigned(FOnCRCFile) or Assigned(LFileSystem) then begin
LContext := TIdFTPServerContext(ASender.Context);
if LContext.IsAuthenticated(ASender) then begin
LBuf := ASender.UnparsedParams;
if Pos('"', LBuf) > 0 then begin {do not localize}
Fetch(LBuf, '"'); {do not localize}
LFileName := Fetch(LBuf, '"'); {do not localize}
end else begin
LFileName := Fetch(LBuf);
end;
if LFileName = '' then begin
CmdInvalidParamNum(ASender);
Exit;
end;
LBuf := Trim(LBuf);
if LBuf <> '' then begin
LBeginPos := IndyStrToStreamSize(Fetch(LBuf), -1);
if LBeginPos < 0 then begin
CmdInvalidParams(ASender);
Exit;
end;
LBuf := Trim(LBuf);
if LBuf <> '' then begin
LEndPos := IndyStrToStreamSize(Fetch(LBuf), -1);
if LEndPos < 0 then begin
CmdInvalidParams(ASender);
Exit;
end;
end else begin
LEndPos := -1;
end;
end else begin
LBeginPos := 0;
LEndPos := -1;
end;
LCalcStream := nil;
LFileName := DoProcessPath(LContext, LFileName);
DoOnCRCFile(LContext, LFileName, LCalcStream);
if Assigned(LCalcStream) then begin
if LEndPos = -1 then begin
LEndPos := LCalcStream.Size;
end;
try
LCalcStream.Position := 0;
LHashIdx := PosInStrArray(ASender.CommandHandler.Command, ['XCRC', 'XMD5', 'XSHA1','XSHA256','XSHA512'], False); {do not localize}
LCheckSum := CalculateCheckSum(HashTypes[LHashIdx], LCalcStream, LBeginPos, LEndPos);
ASender.Reply.SetReply(250, LCheckSum);
finally
FreeAndNil(LCalcStream);
end;
end else begin
CmdFileActionAborted(ASender);
end;
end;
end else begin
CmdSyntaxError(ASender);
end;
end;
procedure TIdFTPServer.DoOnFileExistCheck(AContext: TIdFTPServerContext;
const AFileName: String; var VExist: Boolean);
begin
if Assigned(FOnFileExistCheck) then begin
FOnFileExistCheck(AContext, AFileName, VExist);
end;
end;
procedure TIdFTPServer.CommandSPSV(ASender: TIdCommand);
var
LIP : String;
LBPort : Word;
LIPVer : TIdIPVersion;
begin
//just to keep the compiler happy
LBPort := 0;
if InternalPASV(ASender, LIP, LBPort, LIPVer) then begin
ASender.Reply.SetReply(227, IntToStr(LBPort));
end;
end;
function TIdFTPServer.InternalPASV(ASender: TIdCommand; var VIP : String;
var VPort: TIdPort; var VIPVersion : TIdIPVersion): Boolean;
var
LContext : TIdFTPServerContext;
LBPortMin, LBPortMax: TIdPort;
LDataChannel: TIdSimpleServer;
begin
Result := False;
LContext := ASender.Context as TIdFTPServerContext;
if LContext.IsAuthenticated(ASender) then begin
if LContext.FEPSVAll then begin
ASender.Reply.SetReply(501, IndyFormat(RSFTPNotAllowedAfterEPSVAll, [ASender.CommandHandler.Command]));
Exit;
end;
VIP := LContext.Connection.Socket.Binding.IP;
VIPVersion := LContext.Binding.IPVersion;
if (FPASVBoundPortMin <> 0) and (FPASVBoundPortMax <> 0) then begin
LBPortMin := FPASVBoundPortMin;
LBPortMax := FPASVBoundPortMax;
end else begin
LBPortMin := FDefaultDataPort;
LBPortMax := LBPortMin;
end;
DoOnPASVBeforeBind(LContext, VIP, LBPortMin, LBPortMax, VIPVersion);
LContext.CreateDataChannel(True);
LDataChannel := TIdSimpleServer(LContext.FDataChannel.FDataChannel);
LDataChannel.BoundIP := VIP;
if LBPortMin = LBPortMax then begin
LDataChannel.BoundPort := LBPortMin;
LDataChannel.BoundPortMin := 0;
LDataChannel.BoundPortMax := 0;
end else begin
LDataChannel.BoundPort := 0;
LDataChannel.BoundPortMin := LBPortMin;
LDataChannel.BoundPortMax := LBPortMax;
end;
LDataChannel.IPVersion := VIPVersion;
LDataChannel.BeginListen;
VIP := LDataChannel.Binding.IP;
VPort := LDataChannel.Binding.Port;
LContext.FPASV := True;
LContext.FDataPortDenied := False;
Result := True;
end;
end;
procedure TIdFTPServer.DoOnPASVBeforeBind(ASender: TIdFTPServerContext;
var VIP: String; var VPortMin, VPortMax: TIdPort; const AIPVersion: TIdIPVersion);
begin
if Assigned(FOnPASVBeforeBind) then begin
FOnPASVBeforeBind(ASender, VIP, VPortMin, VPortMax, AIPVersion);
end;
end;
procedure TIdFTPServer.DoOnPASVReply(ASender: TIdFTPServerContext;
var VIP: String; var VPort: TIdPort; const AIPVersion: TIdIPVersion);
begin
if Assigned(FOnPASVReply) then begin
FOnPASVReply(ASender, VIP, VPort, AIPVersion);
end;
end;
function TIdFTPServer.ReadCommandLine(AContext: TIdContext): string;
var
i : Integer;
State: TIdFTPTelnetState;
lb : Byte;
LContext: TIdFTPServerContext;
{ Receive the line in 8-bit initially so that .NET can then
decode any UTF-8 data into a Unicode string afterwards if
needed }
LLine: TIdBytes;
LReply: TIdBytes;
Finished: Boolean;
begin
Result := '';
LContext := AContext as TIdFTPServerContext;
//we do it this way in case there's no data. We don't want to stop
//a data channel operation if that's the case.
AContext.Connection.IOHandler.CheckForDataOnSource(1);
if AContext.Connection.IOHandler.InputBufferIsEmpty then begin
Exit;
end;
//
SetLength(LLine, 0);
SetLength(LReply, 0);
Finished := False;
State := tsData;
repeat
lb := AContext.Connection.IOHandler.ReadByte;
case State of
tsData:
begin
case lb of
$FF: //is a command
begin
State := tsIAC;
end;
$0D: //wait for the next character to see what to do
begin
State := tsCheckCR;
end;
else
AppendByte(LLine, lb);
end;
end;
tsCheckCR:
begin
case lb of
$0: // preserve CR
begin
AppendByte(LLine, $0D);
State := tsData;
end;
$0A:
begin
State := tsData;
Finished := True;
end;
$FF: //unexpected IAC, just in case
begin
AppendByte(LLine, $0D);
State := tsIAC;
end;
else
ExpandBytes(LLine, Length(LLine), 2);
LLine[Length(LLine)-2] := $0D;
LLine[Length(LLine)-1] := lb;
State := tsData;
end;
end;
tsIAC:
begin
case lb of
$F1, //no-operation - do nothing
$F3: //break - do nothing for now
begin
State := tsData;
end;
$F4: //interrupt process - clear result and wait for data mark
begin
SetLength(LLine, 0);
State := tsInterrupt;
end;
$F5: //abort output
begin
// note - the DM needs to be sent as OOB "Urgent" data
SetLength(LReply, 4);
// TELNET_IP
LReply[0] := $FF;
LReply[1] := $F4;
// TELNET_DM
LReply[2] := $FF;
LReply[3] := $F2;
AContext.Connection.IOHandler.Write(LReply);
SetLength(LReply, 0);
State := tsData;
end;
$F6: //are you there - do nothing for now
begin
State := tsData;
end;
$F7: //erase character
begin
i := Length(LLine);
if i > 0 then begin
SetLength(LLine, i-1);
end;
State := tsData;
end;
$F8 : //erase line
begin
SetLength(LLine, 0);
State := tsData;
end;
$F9 : //go ahead - do nothing for now
begin
State := tsData;
end;
$FA : //begin sub-negotiation
begin
State := tsNegotiate;
end;
$FB : //I will use
begin
State := tsWill;
end;
$FC : //you won't use
begin
State := tsWont;
end;
$FD : //please, you use option
begin
State := tsDo;
end;
$FE : //please, you stop option
begin
State := tsDont;
end;
$FF : //data $FF
begin
AppendByte(LLine, $FF);
State := tsData;
end;
else
// unknown command, ignore
State := tsData;
end;
end;
tsWill:
begin
SetLength(LReply, 3);
// TELNET_WONT
LReply[0] := $FF;
LReply[1] := $FC;
LReply[2] := lb;
AContext.Connection.IOHandler.Write(LReply);
SetLength(LReply, 0);
State := tsData;
end;
tsDo:
begin
SetLength(LReply, 3);
// TELNET_DONT
LReply[0] := $FF;
LReply[1] := $FE;
LReply[2] := lb;
AContext.Connection.IOHandler.Write(LReply);
SetLength(LReply, 0);
State := tsData;
end;
tsWont,
tsDont:
begin
State := tsData;
end;
tsNegotiate:
begin
State := tsNegotiateData;
end;
tsNegotiateData:
begin
case lb of
$FF: //is a command?
begin
State := tsNegotiateIAC;
end;
end;
end;
tsNegotiateIAC:
begin
case lb of
$F0: //end sub-negotiation
begin
State := tsData;
end;
else
State := tsNegotiateData;
end;
end;
tsInterrupt:
begin
case lb of
$FF: //is a command?
begin
State := tsInterruptIAC;
end;
end;
end;
tsInterruptIAC:
begin
case lb of
$F2: //data mark
begin
State := tsData;
end;
end;
end;
else
State := tsData;
end;
until Finished or (not AContext.Connection.IOHandler.Connected);
//The last char was #13, we have to make sure that we remove a trailing
//#10 if it exists so that it doesn't appear in the next line.
if (lb = $0D) and (State = tsData) then
begin
i := AContext.Connection.IOHandler.InputBuffer.Size;
if i > 0 then begin
lb := AContext.Connection.IOHandler.InputBuffer.PeekByte(i - 1);
if lb = $0A then begin
AContext.Connection.IOHandler.ReadByte;
end;
end;
end;
Result := BytesToString(LLine, 0, MaxInt, LContext.Connection.IOHandler.DefStringEncoding);
end;
procedure TIdFTPServer.DoReplyUnknownCommand(AContext: TIdContext; ALine: string);
begin
CmdSyntaxError(AContext, ALine);
end;
procedure TIdFTPServer.DoTerminateContext(AContext: TIdContext);
begin
try
TIdFTPServerContext(AContext).KillDataChannel;
finally
inherited DoTerminateContext(AContext);
end;
end;
procedure TIdFTPServer.CmdSyntaxError(AContext: TIdContext; ALine: string; const AReply : TIdReply = nil);
var
LTmp : String;
LReply : TIdReply;
begin
//First make the first word upper-case
LTmp := UpCaseFirstWord(ALine);
try
if Assigned(AReply) then begin
LReply := AReply;
end else begin
LReply := FReplyClass.CreateWithReplyTexts(nil, ReplyTexts);
LReply.Assign(ReplyUnknownCommand);
end;
LReply.Text.Clear;
LReply.Text.Add(IndyFormat(RSFTPCmdNotRecognized, [LTmp]));
AContext.Connection.IOHandler.Write(LReply.FormattedReply);
finally
if not Assigned(AReply) then begin
FreeAndNil(LReply);
end;
end;
end;
procedure TIdFTPServer.CmdSyntaxError(ASender: TIdCommand);
begin
CmdSyntaxError(ASender.Context, ASender.RawLine, FReplyUnknownCommand );
ASender.PerformReply := False;
end;
procedure TIdFTPServer.CommandSecRFC(ASender: TIdCommand);
//stub for RFC 2228 commands that we don't implement as
//part of the SSL framework.
begin
if IOHandler is TIdServerIOHandlerSSLBase then begin
CmdNotImplemented(ASender);
end else begin
CmdSyntaxError(ASender);
end;
end;
procedure TIdFTPServer.CommandOptsMLST(ASender: TIdCommand);
const
LVALIDOPTS : array [0..12] of string =
('type', 'size', 'modify',
'UNIX.mode', 'UNIX.owner', 'UNIX.group',
'unique', 'perm', 'create',
'windows.lastaccesstime','win32.ea','win32.dt','win32.dl'); {Do not localize}
var
s: string;
LContext : TIdFTPServerContext;
function ParseMLSParms(ASvr : TIdFTPServer; const AParms : String) : TIdFTPFactOutputs;
var
Ls : String;
begin
Result := [];
Ls := UpperCase(AParms);
while Ls <> '' do begin
case PosInStrArray(Fetch(Ls,';'), LVALIDOPTS, False) of
0 : Result := Result + [ItemType]; //type
1 : Result := Result + [Size]; //size
2 : Result := Result + [Modify]; //modify
3 : if mlsdUnixModes in ASvr.FMLSDFacts then begin
Result := Result + [UnixMODE]; //UnixMode
end;
4 : if mlsdUnixOwner in ASvr.FMLSDFacts then begin
Result := Result + [UnixOwner]; //UNIX.owner
end;
5 : if mlsdUnixGroup in ASvr.FMLSDFacts then begin
Result := Result + [UnixGroup]; //UNIX.group
end;
6 : if mlsdUniqueID in ASvr.FMLSDFacts then begin //Unique
Result := Result + [Unique];
end;
7 : if mlsdPerms in ASvr.FMLSDFacts then begin //perm
Result := Result + [Perm];
end;
8 : if mlsdFileCreationTime in ASvr.FMLSDFacts then begin
Result := Result + [CreateTime];
end;
9 : if mlsdFileLastAccessTime in ASvr.FMLSDFacts then begin
Result := Result + [LastAccessTime];
end;
10 : if mlsdWin32Attributes in ASvr.FMLSDFacts then begin
Result := Result + [WinAttribs];
end;
11 : if mlsdWin32DriveType in ASvr.MLSDFacts then begin
Result := Result + [WinDriveType];
end;
12 : if mlstWin32DriveLabel in ASvr.MLSDFacts then begin
Result := Result + [WinDriveLabel];
end;
end;
end;
end;
function SetToOptsStr(AFacts : TIdFTPFactOutputs) : String;
begin
Result := '';
if Size in AFacts then begin {Do not translate}
Result := Result + 'size;'; {Do not localize}
end;
if ItemType in AFacts then begin {Do not translate}
Result := Result + 'type;'; {Do not translate}
end;
if Perm in AFacts then begin {Do not translate}
Result := Result + 'perm;'; {Do not translate}
end;
if CreateTime in AFacts then begin {Do not translate}
Result := Result + 'create;'; {Do not translate}
end;
if Modify in AFacts then begin
Result := Result + 'modify;'; {Do not translate}
end;
if UnixMODE in AFacts then begin {Do not translate}
Result := Result + 'UNIX.mode;'; {Do not translate}
end;
if UnixOwner in AFacts then begin{Do not translate}
Result := Result + 'UNIX.owner;'; {Do not translate}
end;
if UnixGroup in AFacts then begin {Do not translate}
Result := Result + 'UNIX.group;'; {Do not translate}
end;
if Unique in AFacts then begin {Do not translate}
Result := Result + 'unique;'; {Do not translate}
end;
if LastAccessTime in AFacts then begin
Result := Result + 'windows.lastaccesstime;'; {Do not translate}
end;
if IdFTPListOutput.WinAttribs in AFacts then begin
Result := Result + 'win32.ea;'; {Do not translate}
end;
if IdFTPListOutput.WinDriveType in AFacts then begin
Result := Result + 'Win32.dt;'; {Do not localize}
end;
if IdFTPListOutput.WinDriveLabel in AFacts then begin
Result := Result + 'Win32.dl;'; {Do not localize}
end;
end;
begin
LContext := ASender.Context as TIdFTPServerContext;
s := ASender.UnparsedParams;
if IndyPos(' ', s) = 0 then begin
LContext.MLSOpts := ParseMLSParms(Self, Trim(s));
//the string is standardized format
ASender.Reply.SetReply(200, Trim(IndyFormat('MLST OPTS %s', [SetToOptsStr(LContext.MLSOpts)]))); {Do not Localize}
end else begin
ASender.Reply.SetReply(501, IndyFormat(RSFTPInvalidOps, ['MLST'])); {Do not Localize}
end;
end;
procedure TIdFTPServer.CommandOptsMODEZ(ASender: TIdCommand);
const
OPT_NAMES : Array[0..4] of String =
('ENGINE','LEVEL','METHOD','BLOCKSIZE','EXTRA'); {do not localize}
var
s: string;
LOptName, LOptVal : String;
LContext : TIdFTPServerContext;
LFirstPar : Boolean;
LError : Boolean;
LNoVal : Integer;
LReset : Boolean;
procedure ReportSettings(ACxt : TIdFTPServerContext; AReply : TIdReply);
begin
AReply.NumericCode := 200;
AReply.Text.Clear;
AReply.Text.Add('MODE Z ENGINE set to ZLIB.'); {do not localize}
AReply.Text.Add('MODE Z LEVEL set to ' + IntToStr(ACxt.FZLibCompressionLevel) + '.'); {do not localize}
AReply.Text.Add('MODE Z METHOD set to ' + IntToStr(DEF_ZLIB_METHOD) + '.'); {do not localize}
end;
procedure SyntaxError(AReply : TIdCommand);
var
LOpts : String;
begin
//drop the OPTS part of the command for display
LOpts := ASender.RawLine;
Fetch(LOpts);
LOpts := TrimLeft(LOpts);
ASender.Reply.SetReply(501, IndyFormat(RSFTPInvalidOps, [LOpts]));
end;
begin
LFirstPar := True;
LReset := True;
LError := True;
LContext := ASender.Context as TIdFTPServerContext;
s := Trim(ASender.UnparsedParams);
if s = '' then begin
LContext.ResetZLibSettings;
ReportSettings(LContext, ASender.Reply);
end;
repeat
LOptName := Fetch(s);
if s = '' then begin
if LFirstPar then begin
SyntaxError(ASender);
Exit;
end;
end;
LOptVal := Fetch(s);
if Trim(s) <> '' then begin
//if there's more, than we see if there's a valid option.
LFirstPar := False;
end;
if LFirstPar and (PosInStrArray(LOptName, OPT_NAMES, False) = -1) then begin
SyntaxError(ASender);
Exit;
end;
LFirstPar := False;
case PosInStrArray(LOptName, OPT_NAMES, False) of
0 : //'ENGINE' - we only support ZLIB
begin
LError := False;
end;
1 : begin //,'LEVEL', - implemented
LNoVal := IndyStrToInt(LOptVal, -1);
if (LNoVal > -1) and (LNoVal < 8) then begin
LContext.FZLibCompressionLevel := LNoVal;
LReset := False;
LError := False;
end;
end;
2 : begin //'METHOD', - not implemented - jst do syntax check
LNoVal := IndyStrToInt(LOptVal, -1);
if LNoVal <> -1 then begin
LError := False;
end;
end;
3 : begin ///'BLOCKSIZE', -not implemented - just do syntax check
LNoVal := IndyStrToInt(LOptVal, -1);
if LNoVal <> -1 then begin
LError := False;
end;
end;
4 : begin //'EXTRA') - not implemented - just do syntax check
if PosInStrArray(LOptVal, OnOffStates, False) > -1 then begin
LError := False;
end;
end;
end;
until (s = '');
if LError then begin
SyntaxError(ASender);
Exit;
end;
if LReset then begin
LContext.ResetZLibSettings;
end;
ReportSettings(LContext, ASender.Reply);
end;
procedure TIdFTPServer.CommandOptsUTF8(ASender: TIdCommand);
var
s: String;
LContext: TIdFTPServerContext;
procedure SyntaxError(AReply : TIdCommand);
var
LOpts : String;
begin
//drop the OPTS part of the command for display
LOpts := ASender.RawLine;
Fetch(LOpts);
LOpts := TrimLeft(LOpts);
ASender.Reply.SetReply(501, IndyFormat(RSFTPInvalidOps, [LOpts]));
end;
begin
LContext := ASender.Context as TIdFTPServerContext;
s := Trim(ASender.UnparsedParams);
if TextIsSame(ASender.CommandHandler.Command, 'UTF-8') then begin
// OPTS UTF-8 <NLST>
// http://www.ietf.org/proceedings/02nov/I-D/draft-ietf-ftpext-utf-8-option-00.txt
if s = '' then begin
LContext.NLSTUtf8 := False; // disable UTF-8 over data connection
end
else if TextIsSame(s, 'NLST') then begin
LContext.NLSTUtf8 := True; // enable UTF-8 over data connection
end else begin
SyntaxError(ASender);
Exit;
end;
// enable UTF-8 over control connection
LContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
end else begin
// OPTS UTF8 <ON|OFF>
// non-standard Microsoft IE implementation!!!!
case PosInStrArray(s, OnOffStates, False) of
0: begin // 'ON'
LContext.NLSTUtf8 := True;
LContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
end;
1: begin // 'OFF'
LContext.NLSTUtf8 := False;
LContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_8Bit;
end;
else
begin
SyntaxError(ASender);
Exit;
end;
end;
end;
ASender.Reply.NumericCode := 200;
end;
function TIdFTPServer.IgnoreLastPathDelim(const APath: String): String;
//This internal function is needed because path processing is different in Windows
//than in Linux. The path separators on a FTP server on either system will be different.
//
//On Windows machines, both '/' and '\'
//
//On a Linux machine, a FTP server would probably only use '/' because '\' is a valid
//filename char.
var
i : Integer;
LPathProcessing : TIdFTPPathProcessing;
begin
Result := APath;
i := Length(Result);
if FPathProcessing <> ftpOSDependent then begin
LPathProcessing := FPathProcessing;
end else begin
case GOSType of
otUnix :
begin
LPathProcessing := ftppUnix;
end;
otUnknown :
begin
LPathProcessing := ftppCustom;
end
else
LPathProcessing := ftppDOS;
end;
end;
case LPathProcessing of
ftppDOS :
begin
if Result <>'' then begin
if CharIsInSet(Result, i, '/\') then begin
IdDelete(Result, i, 1);
end;
end;
end;
ftppUnix :
begin
if Result <>'' then begin
if TextEndsWith(Result, '/') then begin
IdDelete(Result, i, 1);
end;
end;
end;
ftppCustom :
begin
Exit;
end;
end;
//Done so that something like "cd /" or "cd \" will go to
//the main directory
if Result = '' then begin
Result := '/';
end;
end;
function TIdFTPServer.SupportTaDirSwitches(AContext : TIdFTPServerContext): Boolean;
begin
Result := True;
case FDirFormat of
ftpdfCustom, ftpdfEPLF:
Result := False;
ftpdfDOS:
Result := not AContext.FMSDOSMode;
ftpdfOSDependent:
if (GOSType = otWindows) or (GOSType = otDotNET) then begin
Result := not AContext.FMSDOSMode;
end;
end;
end;
{ TIdFTPSecurityOptions }
procedure TIdFTPSecurityOptions.Assign(Source: TPersistent);
var
LSrc : TIdFTPSecurityOptions;
begin
if Source is TIdFTPSecurityOptions then begin
LSrc := Source as TIdFTPSecurityOptions;
BlockAllPORTTransfers := LSrc.BlockAllPORTTransfers;
DisableSTATCommand := LSrc.DisableSTATCommand;
DisableSYSTCommand := LSrc.DisableSYSTCommand;
PasswordAttempts := LSrc.PasswordAttempts;
InvalidPassDelay := LSrc.InvalidPassDelay;
NoReservedRangePORT := LSrc.NoReservedRangePORT;
RequirePASVFromSameIP := LSrc.RequirePASVFromSameIP;
RequirePORTFromSameIP := LSrc.RequirePORTFromSameIP;
PermitCCC := LSrc.PermitCCC;
end else begin
inherited Assign(Source);
end;
end;
constructor TIdFTPSecurityOptions.Create;
begin
inherited Create;
//limit login attempts - some hackers will try guessing passwords from a dictionary
PasswordAttempts := DEF_FTP_PASSWORDATTEMPTS;
//should slow-down a password guessing attack - note those dictionaries
InvalidPassDelay := DEF_FTP_INVALIDPASS_DELAY;
//client IP Address is the only one that we will accept a PASV
//transfer from
//http://cr.yp.to/ftp/security.html
RequirePASVFromSameIP := DEF_FTP_PASV_SAME_IP;
//Accept port transfers from the same IP address as the client -
//should prevent bounce attacks
RequirePORTFromSameIP := DEF_FTP_PORT_SAME_IP;
//Do not accept port requests to ports in the reserved range. That is dangerous on some systems
NoReservedRangePORT := DEF_FTP_NO_RESERVED_PORTS;
//Do not accept any PORT transfers at all. This is a little extreme but reduces troubles further.
//This will break the the Win32 console clients and a number of other programs.
BlockAllPORTTransfers := DEF_FTP_BLOCK_ALL_PORTS;
//Disable SYST command. SYST usually gives the system description.
//Disabling it may make it harder for a trouble maker to know about your computer
//but will not be a complete security solution. See http://www.sans.org/rr/infowar/fingerprint.php for details
//On the other hand, disabling it will break RFC 959 complience and may break some FTP programs.
DisableSYSTCommand := DEF_FTP_DISABLE_SYST;
//Disable STAT command. STAT gives freeform information about the connection status.
// http://www.sans.org/rr/infowar/fingerprint.php advises administrators to disable this
//because servers tend to give distinct patterns of information and some trouble makers
//can figure out what type of server you are running simply with this.
DisableSTATCommand := DEF_FTP_DISABLE_STAT;
//Permit CCC command when using TLS with FTP to clear the control connection.
//That may be helpful for someone behind a NAT where an IP address can NOT be altered by the NAT
//when using SSL. On the other hand, some administrators may NOT permit this for security reasons.
//That's a debate I'll leave up to the programmer in hopes that they will pass it to the user.
PermitCCC := DEF_FTP_PERMIT_CCC;
end;
{ TIdDataChannel }
constructor TIdDataChannel.Create(APASV: Boolean; AControlContext: TIdFTPServerContext;
const ARequirePASVFromSameIP: Boolean; AServer: TIdFTPServer);
var
LIO: TIdIOHandlerSocket;
LDataChannelSvr: TIdSimpleServer;
LDataChannelCli: TIdTCPClient;
begin
inherited Create;
FNegotiateTLS := False;
FOKReply := TIdReplyRFC.Create(nil);
FErrorReply := TIdReplyRFC.Create(nil);
FReply := TIdReplyRFC.Create(nil);
FRequirePASVFromSameIP := ARequirePASVFromSameIP;
FControlContext := AControlContext;
FServer := AServer;
// RLebeau: do not set both BoundPortMin/Max and BoundPort at the same time.
// If they are all non-zero, BoundPort will take priority in TIdSocketHandle.
// The DefaultDataPort property should not be assigned to zero in order to
// support Active-mode transfers, but doing so will cause BoundPortMin/Max
// to be ignored for Passive-mode transfers. So assign them in an either-or
// manner.
if APASV then begin
FDataChannel := TIdSimpleServer.Create(nil);
LDataChannelSvr := TIdSimpleServer(FDataChannel);
LDataChannelSvr.BoundIP := FControlContext.Connection.Socket.Binding.IP;
if (AServer.PASVBoundPortMin <> 0) and (AServer.PASVBoundPortMax <> 0) then begin
LDataChannelSvr.BoundPortMin := AServer.PASVBoundPortMin;
LDataChannelSvr.BoundPortMax := AServer.PASVBoundPortMax;
end else begin
LDataChannelSvr.BoundPort := AServer.DefaultDataPort;
end;
LDataChannelSvr.IPVersion := FControlContext.Binding.IPVersion;
LDataChannelSvr.OnBeforeBind := AControlContext.PortOnBeforeBind;
LDataChannelSvr.OnAfterBind := AControlContext.PortOnAfterBind;
end else begin
FDataChannel := TIdTCPClient.Create(nil);
//the TCPClient for the dataport must be bound to a default port
LDataChannelCli := TIdTCPClient(FDataChannel);
LDataChannelCli.BoundIP := FControlContext.Connection.Socket.Binding.IP;
LDataChannelCli.BoundPort := AServer.DefaultDataPort;
LDataChannelCli.IPVersion := FControlContext.Binding.IPVersion;
end;
if AControlContext.Server.IOHandler is TIdServerIOHandlerSSLBase then begin
if APASV then begin
LIO := TIdServerIOHandlerSSLBase(AServer.IOHandler).MakeFTPSvrPasv;
end else begin
LIO := TIdServerIOHandlerSSLBase(AServer.IOHandler).MakeFTPSvrPort;
end;
(LIO as TIdSSLIOHandlerSocketBase).PassThrough := True;
// always uses a ssl iohandler, but passthrough is true...
end else begin
LIO := FServer.IOHandler.MakeClientIOHandler(nil) as TIdIOHandlerSocket;
end;
LIO.OnBeforeBind := AControlContext.PortOnBeforeBind;
LIO.OnAfterBind := AControlContext.PortOnAfterBind;
FDataChannel.IOHandler := LIO;
if LIO is TIdSSLIOHandlerSocketBase then begin
case AControlContext.DataProtection of
ftpdpsClear: begin
TIdSSLIOHandlerSocketBase(LIO).PassThrough := True;
end;
ftpdpsPrivate: begin
FNegotiateTLS := True;
end;
end;
end;
end;
destructor TIdDataChannel.Destroy;
begin
FreeAndNil(FOKReply);
FreeAndNil(FErrorReply);
FreeAndNil(FReply);
{$IFNDEF USE_OBJECT_ARC}
FDataChannel.IOHandler.Free;
{$ENDIF}
FDataChannel.IOHandler := nil;
FreeAndNil(FDataChannel);
inherited Destroy;
end;
function TIdDataChannel.GetPeerIP: String;
begin
Result := '';
if Assigned(FDataChannel) then begin
if Assigned(FDataChannel.Socket) then begin
if Assigned(FDataChannel.Socket.Binding) then begin
Result := FDataChannel.Socket.Binding.PeerIP;
end;
end;
end;
end;
function TIdDataChannel.GetPeerPort: TIdPort;
begin
Result := 0;
if Assigned(FDataChannel) then begin
if Assigned(FDataChannel.Socket) then begin
if Assigned(FDataChannel.Socket.Binding) then begin
Result := FDataChannel.Socket.Binding.PeerPort;
end;
end;
end;
end;
function TIdDataChannel.GetLocalIP: String;
begin
Result := '';
if Assigned(FDataChannel) then begin
if Assigned(FDataChannel.Socket) then begin
if Assigned(FDataChannel.Socket.Binding) then begin
Result := FDataChannel.Socket.Binding.IP;
end;
end;
end;
end;
function TIdDataChannel.GetLocalPort: TIdPort;
begin
Result := 0;
if Assigned(FDataChannel) then begin
if Assigned(FDataChannel.Socket) then begin
if Assigned(FDataChannel.Socket.Binding) then begin
Result := FDataChannel.Socket.Binding.Port;
end;
end;
end;
end;
procedure TIdDataChannel.InitOperation(const AConnectMode : Boolean = False);
var
LIO : TIdSSLIOHandlerSocketBase;
begin
try
if FDataChannel is TIdSimpleServer then begin
TIdSimpleServer(FDataChannel).Listen;
if FRequirePASVFromSameIP then begin
{//BGO}
if FControlContext.Binding.PeerIP <> TIdSimpleServer(FDataChannel).Binding.PeerIP then begin
TIdFTPServerContext(FControlContext).FDataPortDenied := True;
ErrorReply.SetReply(504, RSFTPSameIPAddress);
FControlContext.Connection.IOHandler.Write(ErrorReply.FormattedReply);
TIdSimpleServer(FDataChannel).Disconnect(False);
Exit;
end;
end;
{//BGO}
if FNegotiateTLS then begin
LIO := FDataChannel.IOHandler as TIdSSLIOHandlerSocketBase;
if AConnectMode then begin
LIO.IsPeer := False;
end;
LIO.Passthrough := False;
end;
end
else if FDataChannel is TIdTCPClient then begin
TIdTCPClient(FDataChannel).Connect;
if FNegotiateTLS then begin
LIO := FDataChannel.IOHandler as TIdSSLIOHandlerSocketBase;
if AConnectMode then begin
LIO.IsPeer := False;
end;
LIO.Passthrough := False;
end;
end;
except
FControlContext.Connection.IOHandler.Write(FErrorReply.FormattedReply); //426
raise;
end;
end;
procedure TIdDataChannel.SetErrorReply(const AValue: TIdReplyRFC);
begin
FErrorReply.Assign(AValue);
end;
procedure TIdDataChannel.SetOKReply(const AValue: TIdReplyRFC);
begin
FOKReply.Assign(AValue);
end;
procedure TIdFTPServerContext.PortOnAfterBind(ASender: TObject);
begin
FServer.DoOnDataPortAfterBind(Self);
end;
procedure TIdFTPServerContext.PortOnBeforeBind(ASender: TObject);
begin
FServer.DoOnDataPortBeforeBind(Self);
end;
procedure TIdFTPServerContext.ResetZLibSettings;
begin
//Settings specified by
// http://www.ietf.org/internet-drafts/draft-preston-ftpext-deflate-00.txt
FZLibCompressionLevel := DEF_ZLIB_COMP_LEVEL;
FZLibWindowBits := DEF_ZLIB_WINDOW_BITS; //-15 - no extra headers
FZLibMemLevel := DEF_ZLIB_MEM_LEVEL;
FZLibStratagy := DEF_ZLIB_STRATAGY; // - default
end;
end.