summaryrefslogtreecommitdiffstats
path: root/tcllib/modules/ftpd
diff options
context:
space:
mode:
Diffstat (limited to 'tcllib/modules/ftpd')
-rw-r--r--tcllib/modules/ftpd/ChangeLog249
-rw-r--r--tcllib/modules/ftpd/ftpd.man279
-rw-r--r--tcllib/modules/ftpd/ftpd.tcl2064
-rw-r--r--tcllib/modules/ftpd/pkgIndex.tcl2
4 files changed, 2594 insertions, 0 deletions
diff --git a/tcllib/modules/ftpd/ChangeLog b/tcllib/modules/ftpd/ChangeLog
new file mode 100644
index 0000000..a95e00f
--- /dev/null
+++ b/tcllib/modules/ftpd/ChangeLog
@@ -0,0 +1,249 @@
+2013-02-01 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ *
+ * Released and tagged Tcllib 1.15 ========================
+ *
+
+2011-12-13 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ *
+ * Released and tagged Tcllib 1.14 ========================
+ *
+
+2011-08-09 Andreas Kupries <andreask@activestate.com>
+
+ * ftpd.man: Bumped version to 1.2.6.
+ * ftpd.tcl:
+ * pkgIndex.tcl:
+
+ * ftpd.tcl (::ftpd::command::CWD): [Bug 3312900]: Accepted patch
+ by Roy Keene, adding basic checks to the CWD command.
+
+ * ftpd.tcl (::ftpd::command::RNTO, ::ftpd::command::RNFR):
+ [Bug 3312880, 3325229]: Fixed issues with the rename command
+ found by Roy Keene.
+
+ * ftpd.tcl: [Bug 3357765]: Accepted patch by Roy Keene
+ <rkeene@users.sourceforge.net> fixing issues with the handling
+ of passive connections by the server, with modifications (Moved
+ the replicated checking code into a procedure shared by the
+ modified commands).
+
+2011-01-24 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ *
+ * Released and tagged Tcllib 1.13 ========================
+ *
+
+2010-01-20 Andreas Kupries <andreask@activestate.com>
+
+ * ftpd.tcl (::ftpd::command::RNTO): [Bug 2928355]: Fixed the
+ missing import of the server's state array, reported by Martin
+ <martinao@users.sourceforge.net>.
+
+ * ftpd.tcl (::ftpd::config): [Bug 2935339] [Patch 2935347]:
+ * ftpd.man: Applied the patch by Keith Vetter
+ * pkgIndex.tcl: <keithv@users.sourceforge.net>, fixing the
+ non-idempotency of the config command. Bumped the package
+ version to 1.2.5.
+
+2009-12-07 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ *
+ * Released and tagged Tcllib 1.12 ========================
+ *
+
+2008-12-12 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ *
+ * Released and tagged Tcllib 1.11.1 ========================
+ *
+
+2008-10-16 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ *
+ * Released and tagged Tcllib 1.11 ========================
+ *
+
+2008-02-29 Andreas Kupries <andreask@activestate.com>
+
+ * ftpd.tcl: Renamed ::ftpd::read -> ftp::Read to prevent clash
+ * ftpd.man: with Tcl's builtin command. Version bumped to 1.2.4.
+ * pkgIndex.tcl:
+
+2007-09-12 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ *
+ * Released and tagged Tcllib 1.10 ========================
+ *
+
+2007-08-20 Andreas Kupries <andreask@activestate.com>
+
+ * ftpd.tcl: Fix for [SF Tcllib Bug 1720144]. Version
+ * ftpd.man: of the package bumped to 1.2.3.
+ * pkgIndex.tcl:
+
+2007-03-21 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ * ftpd.man: Fixed all warnings due to use of now deprecated
+ commands. Added a section about how to give feedback.
+
+2006-10-03 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ *
+ * Released and tagged Tcllib 1.9 ========================
+ *
+
+2005-10-06 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ *
+ * Released and tagged Tcllib 1.8 ========================
+ *
+
+2004-10-05 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ *
+ * Released and tagged Tcllib 1.7 ========================
+ *
+
+2004-10-05 Andreas Kupries <andreask@activestate.com>
+
+ * ftpd.tcl: Fixed [Tcllib SF Bug 1006157] reported by Stephen
+ Huntley <blacksqr@users.sourceforge.net>. Using fake user/group
+ information when on Windows.
+
+2004-05-23 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ * ftpd.tcl: Updated version number to sync with 1.6.1
+ * ftpd.man: release.
+ * pkgIndex.tcl:
+
+2004-05-23 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ *
+ * Released and tagged Tcllib 1.6.1 ========================
+ *
+
+2004-05-23 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ * ftpd.tcl: Rel. engineering. Updated version number
+ * ftpd.man: of ftpd to reflect its changes, to 1.2.1.
+ * pkgIndex.tcl:
+
+2004-05-23 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ * Bugfixes by Gerald Lester. No details available. Gerald is asked
+ to replace this entry with one describing his changes.
+
+2004-02-15 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ *
+ * Released and tagged Tcllib 1.6 ========================
+ *
+
+2004-02-10 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ * ftpd.man: Updated documentation to explain the new features (Two
+ additional callbacks, and the variable 'CurrentSocket').
+ * ftpd.tcl (Finish): Replaced string compare with canonical
+ 'hasCallback'.
+ (GetDone): Ditto for 'xferDoneCmd'.
+ (command::REIN): Closing passive data server port,
+ reinitializing to empty as well.
+ (read): Reverted call of Finish to relative addressing of the
+ command.
+
+2004-02-08 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ * pkgIndex.tcl
+ * ftpd.tcl: Imported changes made by "Gerald W. Lester"
+ <Gerald.Lester@ShowMaster.com>. Bugfixes, more callbacks (close,
+ transfer done), and implementation of passive mode data
+ connection. Version up to 1.2.
+
+2003-07-04 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ * ftpd.tcl (Fs): Fixed SF tcllib bug [766112]. Copied code from
+ style 'nslt' to exclude . and .. from the list.
+
+2003-05-05 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ *
+ * Released and tagged Tcllib 1.4 ========================
+ *
+
+2003-04-11 Andreas Kupries <andreask@activestate.com>
+
+ * ftpd.tcl:
+ * ftpd.man:
+ * pkgIndex.tcl: Fixed bug #614591. Set version of the package to
+ to 1.1.3.
+
+2003-01-16 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ * ftpd.man: More semantic markup, less visual one.
+
+2002-08-30 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ * ftpd.tcl: Updated 'info exist' to 'info exists'.
+
+2002-06-03 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ * pkgIndex.tcl:
+ * ftpd.tcl:
+ * ftpd.n:
+ * ftpd.man: Bumped to version 1.1.2.
+
+2002-03-20 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ * ftpd.man: New, doctools manpage.
+
+2002-03-19 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ * pkgIndex.tcl:
+ * ftpd.n: Changed to require tcl version 8.3. Code uses -unique
+ option of [lsort], introduced in that version. This fixes SF bug
+ #531799.
+
+2001-09-07 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ * ftpd.tcl: Applied patch [459197] from Hemang to fix more
+ 'namespace export *'. Patch modified before application as some
+ export command are actually private (Implementations of the ftp
+ commands).
+
+2001-09-05 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ * ftpd.tcl: Restricted export list to public API.
+ [456255]. Patch by Hemang Lavana
+ <hemanglavana@users.sourceforge.net>
+
+2001-06-21 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+
+ * ftpd.tcl: Fixed dubious code reported by frink.
+
+2000-11-22 Eric Melski <ericm@interwoven.com>
+
+ * Integrated patch from Mark O'Conner. Patch fixed file translation
+ mode bug (ie, binary vs. ascii) that prevented proper retrieval
+ of binary files. [SFBUG: 122664]
+
+2000-11-01 Dan Kuchler <kuchler@ajubasolutions.com>
+
+ * Integrated patch from Keith Vetter <keith@softbook.com>
+ Patch fixed several bugs. Allowed users to log in as
+ both 'anonymous' and 'ftp' by default instead of just anonymous.
+ Fixed syntax error with the 'socket -server' line in ftpd::server when
+ 'myaddr' is specified. Fixed the argument specifications for
+ cmdline:getoptions in ftpd::config so that arguments are required for
+ the -logCmd and the -fsCmd.
+
+2000-10-30 Dan Kuchler <kuchler@ajubasolutions.com>
+
+ * Made some fixes to better support windows.
+
+2000-10-27 Dan Kuchler <kuchler@ajubasolutions.com>
+
+ * Initial revision of tcllib ftpd. Based off of the ftpd in
+ the stdtcl distribution.
+
diff --git a/tcllib/modules/ftpd/ftpd.man b/tcllib/modules/ftpd/ftpd.man
new file mode 100644
index 0000000..e50b24c
--- /dev/null
+++ b/tcllib/modules/ftpd/ftpd.man
@@ -0,0 +1,279 @@
+[vset VERSION 1.3]
+[comment {-*- tcl -*- doctools manpage}]
+[manpage_begin ftpd n [vset VERSION]]
+[keywords ftp]
+[keywords ftpd]
+[keywords ftpserver]
+[keywords {rfc 959}]
+[keywords services]
+[moddesc {Tcl FTP Server Package}]
+[titledesc {Tcl FTP server implementation}]
+[category Networking]
+[require Tcl 8.3]
+[require ftpd [opt [vset VERSION]]]
+[description]
+
+The [package ftpd] package provides a simple Tcl-only server library
+for the FTP protocol as specified in
+RFC 959 ([uri http://www.rfc-editor.org/rfc/rfc959.txt]).
+It works by listening on the standard FTP socket. Most server errors
+are returned as error messages with the appropriate code attached to
+them. Since the server code for the ftp daemon is executed in the
+event loop, it is possible that a
+
+[cmd bgerror] will be thrown on the server if there are problems with
+the code in the module.
+
+[section COMMANDS]
+
+[list_begin definitions]
+
+[call [cmd ::ftpd::server] [opt [arg myaddr]]]
+
+Open a listening socket to listen to and accept ftp connections.
+myaddr is an optional argument. [arg myaddr] is the domain-style name
+or numerical IP address of the client-side network interface to use
+for the connection.
+
+[call [cmd ::ftpd::config] [opt [arg {option value}]] [opt [arg {option value ...}]]]
+
+The value is always the name of the command to call as the
+callback. The option specifies which callback should be configured.
+See section [sectref CALLBACKS] for descriptions of the arguments and
+return values for each of the callbacks.
+
+[list_begin definitions]
+
+[def "-authIpCmd [arg proc]"]
+
+Callback to authenticate new connections based on the ip-address of
+the peer.
+
+[def "-authUsrCmd [arg proc]"]
+
+Callback to authenticate new connections based on the user logging in
+(and the users password).
+
+[def "-authFileCmd [arg proc]"]
+
+Callback to accept or deny a users access to read and write to a
+specific path or file.
+
+[def "-logCmd [arg proc]"]
+
+Callback for log information generated by the FTP engine.
+
+[def "-fsCmd [arg proc]"]
+
+Callback to connect the engine to the filesystem it operates on.
+
+[def "-closeCmd [arg proc]"]
+
+Callback to be called when a connection is closed. This allows the
+embedding application to perform its own cleanup operations.
+
+[def "-xferDoneCmd [arg proc]"]
+
+Callback for transfer completion notification. In other words, it is
+called whenever a transfer of data to or from the client has
+completed.
+
+[list_end]
+[list_end]
+
+[section CALLBACKS]
+
+[list_begin definitions]
+
+[def "[cmd authIpCmd] callback"]
+
+The authIpCmd receives the ip-address of the peer attempting to
+connect to the ftp server as its argument. It returns a 1 to allow
+users from the specified IP to attempt to login and a 0 to reject the
+login attempt from the specified IP.
+
+[def "[cmd authUsrCmd] callback"]
+
+The authUsrCmd receives the username and password as its two
+arguments. It returns a 1 to accept the attempted login to the ftpd
+and a 0 to reject the attempted login.
+
+[def "[cmd authFileCmd] callback"]
+
+The authFileCmd receives the user (that is currently logged in), the
+path or filename that is about to be read or written, and
+
+[const read] or [const write] as its three arguments. It returns a
+1 to allow the path or filename to be read or written, and a 0 to
+reject the attempted read or write with a permissions error code.
+
+[def "[cmd logCmd] callback"]
+
+The logCmd receives a severity and a message as its two arguments.
+The severities used within the ftpd package are [const note],
+
+[const debug], and [const error]. The logCmd doesn't return
+anything.
+
+[def "[cmd fsCmd] callback"]
+
+The fsCmd receives a subcommand, a filename or path, and optional
+additional arguments (depending on the subcommand).
+
+[para]
+The subcommands supported by the fsCmd are:
+
+[list_begin definitions]
+
+[call [arg fsCmd] [method append] [arg path]]
+
+The append subcommand receives the filename to append to as its
+argument. It returns a writable tcl channel as its return value.
+
+[call [arg fsCmd] [method delete] [arg path] [arg channel]]
+
+The delete subcommand receives the filename to delete, and a channel
+to write to as its two arguments. The file specified is deleted and
+the appropriate ftp message is written to the channel that is passed
+as the second argument. The delete subcommand returns nothing.
+
+[call [arg fsCmd] [method dlist] [arg path] [arg style] [arg channel]]
+
+The dlist subcommand receives the path that it should list the files
+that are in, the style in which the files should be listed which is
+either [const nlst] or [const list], and a channel to write to as
+its three arguments. The files in the specified path are printed to
+the specified channel one per line. If the style is [const nlst]
+only the name of the file is printed to the channel. If the style is
+[const list] then the file permissions, number of links to the file,
+the name of the user that owns the file, the name of the group that
+owns the file, the size (in bytes) of the file, the modify time of the
+file, and the filename are printed out to the channel in a formatted
+space separated format. The [method dlist] subcommand returns
+nothing.
+
+[call [arg fsCmd] [method exists] [arg path]]
+
+The exists subcommand receives the name of a file to check the
+existence of as its only argument. The exists subcommand returns a 1
+if the path specified exists and the path is not a directory.
+
+[call [arg fsCmd] [method mkdir] [arg path] [arg channel]]
+
+The mkdir subcommand receives the path of a directory to create and a
+channel to write to as its two arguments. The mkdir subcommand
+creates the specified directory if necessary and possible. The mkdir
+subcommand then prints the appropriate success or failure message to
+the channel. The mkdir subcommand returns nothing.
+
+[call [arg fsCmd] [method mtime] [arg path] [arg channel]]
+
+The mtime subcommand receives the path of a file to check the modify
+time on and a channel as its two arguments. If the file exists the
+mtime is printed to the channel in the proper FTP format, otherwise an
+appropriate error message and code are printed to the channel. The
+mtime subcommand returns nothing.
+
+[call [arg fsCmd] [method permissions] [arg path]]
+
+The permissions subcommand receives the path of a file to retrieve the
+permissions of. The permissions subcommand returns the octal file
+permissions of the specified file. The file is expected to exist.
+
+[call [arg fsCmd] [method rename] [arg path] [arg newpath] [arg channel]]
+
+The rename subcommand receives the path of the current file, the new
+file path, and a channel to write to as its three arguments. The
+rename subcommand renames the current file to the new file path if the
+path to the new file exists, and then prints out the appropriate
+message to the channel. If the new file path doesn't exist the
+appropriate error message is printed to the channel. The rename
+subcommand returns nothing.
+
+[call [arg fsCmd] [method retr] [arg path]]
+
+The retr subcommand receives the path of a file to read as its only
+argument. The retr subcommand returns a readable channel that the
+specified file can be read from.
+
+[call [arg fsCmd] [method rmdir] [arg path] [arg channel]]
+
+The rmdir subcommand receives the path of a directory to remove and a
+channel to write to as its two arguments. The rmdir subcommand
+removes the specified directory (if possible) and prints the
+appropriate message to the channel (which may be an error if the
+specified directory does not exist or is not empty). The rmdir
+subcommand returns nothing.
+
+[call [arg fsCmd] [method size] [arg path] [arg channel]]
+
+The size subcommand receives the path of a file to get the size (in
+bytes) of and a channel to write to as its two arguments. The size
+subcommand prints the appropriate code and the size of the file if the
+specified path is a file, otherwise an appropriate error code and
+message are printed to the channel. The size subcommand returns
+nothing.
+
+[call [arg fsCmd] [method store] [arg path]]
+
+The store subcommand receives the path of a file to write as its only
+argument. The store subcommand returns a writable channel.
+
+[list_end]
+
+[def "[cmd closeCmd]"]
+
+The [cmd closeCmd] receives no arguments when it is invoked, and any
+return value it may generate is discarded.
+
+[def "[cmd xferDoneCmd] sock sock2 file bytes filename err"]
+
+The [cmd xferDoneCmd] receives six arguments when invoked. These are,
+in this order, the channel handle of the control socket for the
+connection, the channel handle of the data socket used for the
+transfer (already closed), the handle of the channel containing the
+transfered file, the number of bytes transfered, the path of the file
+which was transfered, and a (possibly empty) error message.
+
+Any return value it may generate is discarded.
+
+[list_end]
+
+[section VARIABLES]
+
+[list_begin definitions]
+
+[def [var ::ftpd::cwd]]
+
+The current working directory for a session when someone first
+connects to the FTPD or when the [cmd REIN] ftp command is received.
+
+[def [var ::ftpd::contact]]
+
+The e-mail address of the person that is the contact for the ftp
+server. This address is printed out as part of the response to the
+[cmd {FTP HELP}] command.
+
+[def [var ::ftpd::port]]
+
+The port that the ftp server should listen on.
+If port is specified as zero, the operating system will allocate an
+unused port for use as a server socket; afterwards, the variable will
+contain the port number that was allocated.
+
+[def [var ::ftpd::welcome]]
+
+The message that is printed out when the user first connects to the
+ftp server.
+
+[def [var ::ftpd::CurrentSocket]]
+
+Accessible to all callbacks and all filesystem commands (which are a
+special form of callback) and contains the handle of the socket
+channel which was active when the callback was invoked.
+
+[list_end]
+
+[vset CATEGORY ftpd]
+[include ../doctools2base/include/feedback.inc]
+[manpage_end]
diff --git a/tcllib/modules/ftpd/ftpd.tcl b/tcllib/modules/ftpd/ftpd.tcl
new file mode 100644
index 0000000..7e93f8e
--- /dev/null
+++ b/tcllib/modules/ftpd/ftpd.tcl
@@ -0,0 +1,2064 @@
+# ftpd.tcl --
+#
+# This file contains Tcl/Tk package to create a ftp daemon.
+# I believe it was originally written by Matt Newman (matt@sensus.org).
+# Modified by Dan Kuchler (kuchler@ajubasolutions.com) to handle
+# more ftp commands and to fix some bugs in the original implementation
+# that was found in the stdtcl module.
+#
+# See the file "license.terms" for information on usage and redistribution
+# of this file, and for a DISCLAIMER OF ALL WARRANTIES.
+#
+# RCS: @(#) $Id: ftpd.tcl,v 1.34 2011/08/09 20:55:38 andreas_kupries Exp $
+#
+
+# Define the ftpd package version 1.2.5
+
+package require Tcl 8.2
+namespace eval ::ftpd {
+
+ # The listening port.
+
+ variable port 21
+
+ variable contact
+ if {![info exists contact]} {
+ global tcl_platform
+ set contact "$tcl_platform(user)@[info hostname]"
+ }
+
+ variable cwd
+ if {![info exists cwd]} {
+ set cwd ""
+ }
+
+ variable welcome
+ if {![info exists welcome]} {
+ set welcome "[info hostname] FTP server ready."
+ }
+
+ # Global configuration.
+
+ variable cfg
+ if {![info exists cfg]} {
+ array set cfg [list \
+ closeCmd {} \
+ authIpCmd {} \
+ authUsrCmd {::ftpd::anonAuth} \
+ authFileCmd {::ftpd::fileAuth} \
+ logCmd {::ftpd::logStderr} \
+ fsCmd {::ftpd::fsFile::fs} \
+ xferDoneCmd {}]
+ }
+
+ variable commands
+ if {![info exists commands]} {
+ array set commands [list \
+ ABOR {ABOR (abort operation)} \
+ ACCT {(specify account); unimplemented.} \
+ ALLO {(allocate storage - vacuously); unimplemented.} \
+ APPE {APPE <sp> file-name} \
+ CDUP {CDUP (change to parent directory)} \
+ CWD {CWD [ <sp> directory-name ]} \
+ DELE {DELE <sp> file-name} \
+ HELP {HELP [ <sp> <string> ]} \
+ LIST {LIST [ <sp> path-name ]} \
+ NLST {NLST [ <sp> path-name ]} \
+ MAIL {(mail to user); unimplemented.} \
+ MDTM {MDTM <sp> path-name} \
+ MKD {MKD <sp> path-name} \
+ MLFL {(mail file); unimplemented.} \
+ MODE {(specify transfer mode); unimplemented.} \
+ MRCP {(mail recipient); unimplemented.} \
+ MRSQ {(mail recipient scheme question); unimplemented.} \
+ MSAM {(mail send to terminal and mailbox); unimplemented.} \
+ MSND {(mail send to terminal); unimplemented.} \
+ MSOM {(mail send to terminal or mailbox); unimplemented.} \
+ NOOP {NOOP} \
+ PASS {PASS <sp> password} \
+ PASV {(set server in passive mode); unimplemented.} \
+ PORT {PORT <sp> b0, b1, b2, b3, b4, b5} \
+ PWD {PWD (return current directory)} \
+ QUIT {QUIT (terminate service)} \
+ REIN {REIN (reinitialize server state)} \
+ REST {(restart command); unimplemented.} \
+ RETR {RETR <sp> file-name} \
+ RMD {RMD <sp> path-name} \
+ RNFR {RNFR <sp> file-name} \
+ RNTO {RNTO <sp> file-name} \
+ SIZE {SIZE <sp> path-name} \
+ SMNT {(structure mount); unimplemented.} \
+ STOR {STOR <sp> file-name} \
+ STOU {STOU <sp> file-name} \
+ STRU {(specify file structure); unimplemented.} \
+ SYST {SYST (get type of operating system)} \
+ TYPE {TYPE <sp> [ A | E | I | L ]} \
+ USER {USER <sp> username} \
+ XCUP {XCUP (change to parent directory)} \
+ XCWD {XCWD [ <sp> directory-name ]} \
+ XMKD {XMKD <sp> path-name} \
+ XPWD {XPWD (return current directory)} \
+ XRMD {XRMD <sp> path-name}]
+ }
+
+ variable passwords [list ]
+
+ # Exported procedures
+
+ namespace export config hasCallback logStderr
+ namespace export fileAuth anonAuth unixAuth server accept read
+}
+
+
+# ::ftpd::config --
+#
+# Configure the configurable parameters of the ftp daemon.
+#
+# Arguments:
+# options - -authIpCmd proc procedure that accepts or rejects an
+# incoming connection. A value of 0 or
+# an error causes the connection to be
+# rejected. There is no default.
+# -authUsrCmd proc procedure that accepts or rejects a
+# login. Defaults to ::ftpd::anonAuth
+# -authFileCmd proc procedure that accepts or rejects
+# access to read or write a certain
+# file or path. Defaults to
+# ::ftpd::userAuth
+# -logCmd proc procedure that logs information from
+# the ftp engine. Default is
+# ::ftpd::logStderr
+# -fsCmd proc procedure to connect the ftp engine
+# to the file system it operates on.
+# Default is ::ftpd::fsFile::fs
+#
+# Results:
+# None.
+#
+# Side Effects:
+# Changes the value of the specified configurables.
+
+proc ::ftpd::config {args} {
+
+ # Processing of global configuration changes.
+
+ package require cmdline
+
+ variable cfg
+
+ # Make default value be the current value so we can call this
+ # command multiple times without resetting already set values
+
+ array set cfg [cmdline::getoptions args [list \
+ [list closeCmd.arg $cfg(closeCmd) {Callback when a connection is closed.}] \
+ [list authIpCmd.arg $cfg(authIpCmd) {Callback to authenticate new connections based on the ip-address of the peer. Optional}] \
+ [list authUsrCmd.arg $cfg(authUsrCmd) {Callback to authenticate new connections based on the user logging in.}] \
+ [list authFileCmd.arg $cfg(authFileCmd) {Callback to accept or deny a users access to read and write to a specific path or file.}] \
+ [list logCmd.arg $cfg(logCmd) {Callback for log information generated by the FTP engine.}] \
+ [list xferDoneCmd.arg $cfg(xferDoneCmd) {Callback for transfer completion notification. Optional}] \
+ [list fsCmd.arg $cfg(fsCmd) {Callback to connect the engine to the filesystem it operates on.}]]]
+ return
+}
+
+
+# ::ftpd::hasCallback --
+#
+# Determines whether or not a non-NULL callback has been defined for one
+# of the callback types.
+#
+# Arguments:
+# callbackType - One of authIpCmd, authUsrCmd, logCmd, or fsCmd
+#
+# Results:
+# Returns 1 if a non-NULL callback has been specified for the
+# callbackType that is passed in.
+#
+# Side Effects:
+# None.
+
+proc ::ftpd::hasCallback {callbackType} {
+ variable cfg
+
+ return [expr {[info exists cfg($callbackType)] && [string length $cfg($callbackType)]}]
+}
+
+
+# ::ftpd::logStderr --
+#
+# Outputs a message with the specified severity to stderr. The default
+# logCmd callback.
+#
+# Arguments:
+# severity - The severity of the error. One of debug, error,
+# or note.
+# text - The error message.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# A message is written to the stderr channel.
+
+proc ::ftpd::logStderr {severity text} {
+
+ # Standard log handler. Prints to stderr.
+
+ puts stderr "\[$severity\] $text"
+ return
+}
+
+
+# ::ftpd::Log --
+#
+# Used for all ftpd logging.
+#
+# Arguments:
+# severity - The severity of the error. One of debug, error,
+# or note.
+# text - The error message.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# The ftpd logCmd callback is called with the specified severity and
+# text if there is a non-NULL ftpCmd.
+
+proc ::ftpd::Log {severity text} {
+
+ # Central call out to log handlers.
+
+ variable cfg
+
+ if {[hasCallback logCmd]} {
+ set cmd $cfg(logCmd)
+ lappend cmd $severity $text
+ eval $cmd
+ }
+ return
+}
+
+
+# ::ftpd::fileAuth --
+#
+# Given a username, path, and operation- decides whether or not to accept
+# the attempted read or write operation.
+#
+# Arguments:
+# user - The name of the user that is attempting to
+# connect to the ftpd.
+# path - The path or filename that the user is attempting
+# to read or write.
+# operation - read or write.
+#
+# Results:
+# Returns 0 if it rejects access and 1 if it accepts access.
+#
+# Side Effects:
+# None.
+
+proc ::ftpd::fileAuth {user path operation} {
+ # Standard authentication handler
+
+ if {(![Fs exists $path]) && ([string equal $operation "write"])} {
+ if {[Fs exists [file dirname $path]]} {
+ set path [file dirname $path]
+ }
+ } elseif {(![Fs exists $path]) && ([string equal $operation "read"])} {
+ return 0
+ }
+
+ if {[Fs exists $path]} {
+ set mode [Fs permissions $path]
+ if {([string equal $operation "read"] && (($mode & 00004) > 0)) || \
+ ([string equal $operation "write"] && (($mode & 00002) > 0))} {
+ return 1
+ }
+ }
+ return 0
+}
+
+# ::ftpd::anonAuth --
+#
+# Given a username and password, decides whether or not to accept the
+# attempted login. This is the default ftpd authUsrCmd callback. By
+# default it accepts the annonymous user and does some basic checking
+# checking on the form of the password to see if it has the form of an
+# email address.
+#
+# Arguments:
+# user - The name of the user that is attempting to
+# connect to the ftpd.
+# pass - The password of the user that is attempting to
+# connect to the ftpd.
+#
+# Results:
+# Returns 0 if it rejects the login and 1 if it accepts the login.
+#
+# Side Effects:
+# None.
+
+proc ::ftpd::anonAuth {user pass} {
+ # Standard authentication handler
+ #
+ # Accept user 'anonymous' if a password was
+ # provided which is at least similar to an
+ # fully qualified email address.
+
+ if {(![string equal $user anonymous]) && (![string equal $user ftp])} {
+ return 0
+ }
+
+ set pass [split $pass @]
+ if {[llength $pass] != 2} {
+ return 0
+ }
+
+ set domain [split [lindex $pass 1] .]
+ if {[llength $domain] < 2} {
+ return 0
+ }
+
+ return 1
+}
+
+# ::ftpd::unixAuth --
+#
+# Given a username and password, decides whether or not to accept the
+# attempted login. This is an alternative to the default ftpd
+# authUsrCmd callback. By default it accepts the annonymous user and does
+# some basic checking checking on the form of the password to see if it
+# has the form of an email address.
+#
+# Arguments:
+# user - The name of the user that is attempting to
+# connect to the ftpd.
+# pass - The password of the user that is attempting to
+# connect to the ftpd.
+#
+# Results:
+# Returns 0 if it rejects the login and 1 if it accepts the login.
+#
+# Side Effects:
+# None.
+
+proc ::ftpd::unixAuth {user pass} {
+
+ variable passwords
+ array set password $passwords
+
+ # Standard authentication handler
+ #
+ # Accept user 'anonymous' if a password was
+ # provided which is at least similar to an
+ # fully qualified email address.
+
+ if {([llength $passwords] == 0) && (![catch {package require crypt}])} {
+ foreach file [list /etc/passwd /etc/shadow] {
+ if {([file exists $file]) && ([file readable $file])} {
+ set fh [open $file r]
+ set data [read $fh [file size $file]]
+ foreach line [split $data \n] {
+ foreach {username passwd uid gid dir sh} [split $line :] {
+ if {[string length $passwd] > 2} {
+ set password($username) $passwd
+ } elseif {$passwd == ""} {
+ set password($username) ""
+ }
+ break
+ }
+ }
+ }
+ }
+ set passwords [array get password]
+ }
+
+ ::ftpd::Log debug $passwords
+
+ if {[string equal $user anonymous] || [string equal $user ftp]} {
+
+ set pass [split $pass @]
+ if {[llength $pass] != 2} {
+ return 0
+ }
+
+ set domain [split [lindex $pass 1] .]
+ if {[llength $domain] < 2} {
+ return 0
+ }
+
+ return 1
+ }
+
+ if {[info exists password($user)]} {
+ if {$password($user) == ""} {
+ return 1
+ }
+ if {[string equal $password($user) [::crypt $pass $password($user)]]} {
+ return 1
+ }
+ }
+
+ return 0
+}
+
+# ::ftpd::server --
+#
+# Creates a server socket at the specified port.
+#
+# Arguments:
+# myaddr - The domain-style name or numerical IP address of
+# the client-side network interface to use for the
+# connection. The name of the user that is
+# attempting to connect to the ftpd.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# A listener is setup on the specified port which will call
+# ::ftpd::accept when it is connected to.
+
+proc ::ftpd::server {{myaddr {}}} {
+ variable port
+ variable serviceSock
+ if {[string length $myaddr]} {
+ set serviceSock [socket -server ::ftpd::accept -myaddr $myaddr $port]
+ } else {
+ set serviceSock [socket -server ::ftpd::accept $port]
+ }
+ set port [lindex [fconfigure $serviceSock -sockname] 2]
+ return
+}
+
+
+# ::ftpd::accept --
+#
+# Checks if the connecting IP is authorized to connect or not. If not
+# the socket is closed and failure is logged. Otherwise, a welcome is
+# printed out, and a ftpd::Read filevent is placed on the socket.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# ipaddr - The client's IP address.
+# client_port - The client's port number.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# Sets up a ftpd::Read fileevent to trigger whenever the channel is
+# readable. Logs an error and closes the connection if the IP is
+# not authorized to connect.
+
+proc ::ftpd::accept {sock ipaddr client_port} {
+ upvar #0 ::ftpd::$sock data
+ variable welcome
+ variable cfg
+ variable cwd
+ variable CurrentSocket
+
+ set CurrentSocket $sock
+ if {[info exists data]} {
+ unset data
+ }
+
+ if {[hasCallback authIpCmd]} {
+ # Call out to authenticate the peer. A return value of 0 or an
+ # error causes the system to reject the connection. Everything
+ # else (with 1 prefered) leads to acceptance.
+
+ set cmd $cfg(authIpCmd)
+ lappend cmd $ipaddr
+
+ set fail [catch {eval $cmd} res]
+
+ if {$fail} {
+ Log error "AuthIp error: $res"
+ }
+ if {$fail || ($res == 0)} {
+ Log note "AuthIp: Access denied to $ipaddr"
+
+ # Now: Close the connection. (Is there a standard response
+ # before closing down to signal the peer that we don't want
+ # to talk to it ? -> read RFC).
+
+ close $sock
+ return
+ }
+
+ # Accept the connection (for now, 'authUsrCmd' may revoke this
+ # decision).
+ }
+
+ array set data [list \
+ access 0 \
+ ip $ipaddr \
+ state command \
+ buffering line \
+ cwd "$cwd" \
+ mode binary \
+ sock2a "" \
+ sock2 ""]
+
+ fconfigure $sock -buffering line
+ fileevent $sock readable [list ::ftpd::Read $sock]
+ puts $sock "220 $welcome"
+
+ Log debug "Accept $ipaddr"
+ return
+}
+
+# ::ftpd::Read --
+#
+# Checks the state of a channel and then reads a command from the
+# channel if it is not at end of file yet. If there is a command named
+# ftpd::command::* where '*' is the all upper case name of the command,
+# then that proc is called to handle the command with the remaining parts
+# of the command that was read from the channel as arguments.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# Runs the appropriate command depending on the state in the state
+# machine, and the command that is specified.
+
+proc ::ftpd::Read {sock} {
+ upvar #0 ::ftpd::$sock data
+ variable CurrentSocket
+
+ set CurrentSocket $sock
+ if {[eof $sock]} {
+ Finish $sock
+ return
+ }
+ switch -exact -- $data(state) {
+ command {
+ gets $sock command
+ set argument ""
+ if {![regexp {^([^ ]+) (.*)$} $command -> cmd argument]} {
+ if {![regexp {^([^ ]+)$} $command -> cmd]} {
+ # Very bad command syntax.
+ puts $sock "500 Command not understood."
+ return
+ }
+ }
+ set cmd [string toupper $cmd]
+ auto_load ::ftpd::command::$cmd
+ if {($data(access) == 0) && ((![info exists data(user)]) || \
+ ($data(user) == "")) && (![string equal $cmd "USER"])} {
+ if {[string equal $cmd "PASS"]} {
+ puts $sock "503 Login with USER first."
+ } else {
+ puts $sock "530 Please login with USER and PASS."
+ }
+ } elseif {($data(access) == 0) && (![string equal $cmd "PASS"]) \
+ && (![string equal $cmd "USER"]) \
+ && (![string equal $cmd "QUIT"])} {
+ puts $sock "530 Please login with USER and PASS."
+ } elseif {[info commands ::ftpd::command::$cmd] != ""} {
+ Log debug $command
+ ::ftpd::command::$cmd $sock $argument
+ catch {flush $sock}
+ } else {
+ Log error "Unknown command: $cmd"
+ puts $sock "500 Unknown command $cmd"
+ }
+ }
+ default {
+ error "Unknown state \"$data(state)\""
+ }
+ }
+ return
+}
+
+# ::ftpd::Finish --
+#
+# Closes the socket connection between the ftpd and client.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# The channel is closed.
+
+proc ::ftpd::Finish {sock} {
+ upvar #0 ::ftpd::$sock data
+ variable cfg
+
+ if {[hasCallback closeCmd]} then {
+ ##
+ ## User specified a close command so invoke it
+ ##
+ uplevel #0 $cfg(closeCmd)
+ }
+ close $sock
+ if {[info exists data]} {
+ unset data
+ }
+ return
+}
+
+# ::ftpd::FinishData --
+#
+# Closes the data socket connection that is created when the 'PORT'
+# command is recieved.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# The data channel is closed.
+
+proc ::ftpd::FinishData {sock} {
+ upvar #0 ::ftpd::$sock data
+ catch {close $data(sock2)}
+ set data(sock2) {}
+ return
+}
+
+# ::ftpd::Fs --
+#
+# The general filesystem command. Used as an intermediary for filesystem
+# access to allow alternate (virtual, etc.) filesystems to be used. The
+# ::ftpd::Fs command will call out to the fsCmd callback with the
+# subcommand and arguments that are passed to it.
+#
+# The fsCmd callback is called in the following ways:
+#
+# <cmd> append <path>
+# <cmd> delete <path> <channel-to-write-to>
+# <cmd> dlist <path> <style> <channel-to-write-dir-list-to>
+# <cmd> exists <path>
+# <cmd> mkdir <path> <channel-to-write-to>
+# <cmd> mtime <path> <channel-to-write-mtime-to>
+# <cmd> permissions <path>
+# <cmd> rename <path> <newpath> <channel-to-write-to>
+# <cmd> retr <path>
+# <cmd> rmdir <path> <channel-to-write-to>
+# <cmd> size <path> <channel-to-write-size-to>
+# <cmd> store <path>
+#
+# Arguments:
+# command - The filesystem command (one of dlist, retr, or
+# store). 'dlist' will list files in a
+# directory, 'retr' will get a channel to
+# to read the specified file from, 'store'
+# will return the channel to write to, and
+# 'mtime' will print the modification time.
+# path - The file name or directory to read, write, or
+# list.
+# args - Additional arguments for filesystem commands.
+# Currently this is used by 'dlist' which
+# has two additional arguments 'style' and
+# 'channel-to-write-dir-list-to'. It is also
+# used by 'size' and 'mtime' which have one
+# additional argument 'channel-to-write-to'.
+#
+# Results:
+# For a 'appe', 'retr', or 'stor' a channel is returned. For 'exists'
+# a 1 is returned if the path exists, and is not a directory. Otherwise
+# a 0 is returned. For 'permissions' the octal file permissions (i.e.
+# the 'file stat' mode) are returned.
+#
+# Side Effects:
+# For 'dlist' a directory listing for the specified path is written to
+# the specified channel. For 'mtime' the modification time is written
+# or an error is thrown. An error is thrown if there is no fsCmd
+# callback configured for the ftpd.
+
+proc ::ftpd::Fs {command path args} {
+ variable cfg
+
+ if {![hasCallback fsCmd]} {
+ error "-fsCmd must not be empty, need a way to access files."
+ }
+
+ return [eval [list $cfg(fsCmd) $command $path] $args]
+}
+
+# Create a namespace to hold one proc for each ftp command (in upper case
+# letters) that is supported by the ftp daemon. The existance of a proc
+# in this namespace is the way that the list of supported commands is
+# determined, and the procs in this namespace are invoked to handle the
+# ftp commands with the same name as the procs.
+
+namespace eval ::ftpd::command {
+ # All commands in this namespace are private, no export.
+}
+
+# ::ftpd::command::ABOR --
+#
+# Handle the ABOR ftp command. Closes the data socket if it
+# is open, and then prints the appropriate success message.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# list - The arguments to the APPE command.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# The data is copied to from the socket data(sock2) to the
+# writable channel to create a file.
+
+proc ::ftpd::command::ABOR {sock list} {
+
+ ::ftpd::FinishData $sock
+ puts $sock "225 ABOR command successful."
+
+ return
+}
+
+# ::ftpd::command::APPE --
+#
+# Handle the APPE ftp command. Gets a writable channel for the file
+# specified from ::ftpd::Fs and copies the data from data(sock2) to
+# the writable channel. If the filename already exists the data is
+# appended, otherwise the file is created and then written.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# list - The arguments to the APPE command.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# The data is copied to from the socket data(sock2) to the
+# writable channel to create a file.
+
+proc ::ftpd::command::APPE {sock filename} {
+ upvar #0 ::ftpd::$sock data
+
+ set path [file join $data(cwd) [string trimleft $filename /]]
+ if {[::ftpd::hasCallback authFileCmd]} {
+ set cmd $::ftpd::cfg(authFileCmd)
+ lappend cmd $data(user) $path write
+ if {[eval $cmd] == 0} {
+ puts $sock "550 $filename: Permission denied"
+ return
+ }
+ }
+
+ #
+ # Patched Mark O'Connor
+ #
+ if {![catch {::ftpd::Fs append $path $data(mode)} f]} {
+ puts $sock "150 Copy Started ($data(mode))"
+ ::ftpd::PasvCheckAndWait $sock
+ fcopy $data(sock2) $f -command [list ::ftpd::GetDone $sock $data(sock2) $f ""]
+ } else {
+ puts $sock "500 Copy Failed: $path $f"
+ ::ftpd::FinishData $sock
+ }
+ return
+}
+
+# ::ftpd::command::CDUP --
+#
+# Handle the CDUP ftp command. Change the current working directory to
+# the directory above the current working directory.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# list - The arguments to the CDUP command.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# Changes the data(cwd) to the appropriate directory.
+
+proc ::ftpd::command::CDUP {sock list} {
+ upvar #0 ::ftpd::$sock data
+
+ set data(cwd) [file dirname $data(cwd)]
+ puts $sock "200 CDUP command successful."
+ return
+}
+
+# ::ftpd::command::CWD --
+#
+# Handle the CWD ftp command. Change the current working directory.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# list - The arguments to the CWD command.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# Changes the data(cwd) to the appropriate directory.
+
+proc ::ftpd::command::CWD {sock relativepath} {
+ upvar #0 ::ftpd::$sock data
+
+ if {[string equal $relativepath .]} {
+ puts $sock "250 CWD command successful."
+ return
+ }
+
+ if {[string equal $relativepath ..]} {
+ set data(cwd) [file dirname $data(cwd)]
+ puts $sock "250 CWD command successful."
+ return
+ }
+
+ set path [file join $data(cwd) $relativepath]
+
+ if {[::ftpd::Fs exists $path]} {
+ puts $sock "550 not a directory"
+ return
+ }
+
+ set data(cwd) $path
+ puts $sock "250 CWD command successful."
+ return
+}
+
+# ::ftpd::command::DELE --
+#
+# Handle the DELE ftp command. Delete the specified file.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# list - The arguments to the DELE command.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# The specified file is deleted.
+
+proc ::ftpd::command::DELE {sock filename} {
+ upvar #0 ::ftpd::$sock data
+
+ set path [file join $data(cwd) [string trimleft $filename /]]
+ if {[::ftpd::hasCallback authFileCmd]} {
+ set cmd $::ftpd::cfg(authFileCmd)
+ lappend cmd $data(user) $path write
+ if {[eval $cmd] == 0} {
+ puts $sock "550 $filename: Permission denied"
+ return
+ }
+ }
+
+ if {[catch {::ftpd::Fs delete $path $sock} msg]} {
+ puts $sock "500 DELE Failed: $path $msg"
+ }
+ return
+}
+
+# ::ftpd::command::HELP --
+#
+# Handle the HELP ftp command. Display a list of commands
+# or syntax information about the supported commands.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# list - The arguments to the HELP command.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# Displays a helpful message.
+
+proc ::ftpd::command::HELP {sock command} {
+ upvar #0 ::ftpd::$sock data
+
+ if {$command != ""} {
+ set command [string toupper $command]
+ if {![info exists ::ftpd::commands($command)]} {
+ puts $sock "502 Unknown command '$command'."
+ } elseif {[info commands ::ftpd::command::$command] == ""} {
+ puts $sock "214 $command\t$::ftpd::commands($command)"
+ } else {
+ puts $sock "214 Syntax: $::ftpd::commands($command)"
+ }
+ } else {
+ set commandList [lsort [array names ::ftpd::commands]]
+ puts $sock "214-The following commands are recognized (* =>'s unimplemented)."
+ set i 1
+ foreach commandName $commandList {
+ if {[info commands ::ftpd::command::$commandName] == ""} {
+ puts -nonewline $sock [format " %-7s" "${commandName}*"]
+ } else {
+ puts -nonewline $sock [format " %-7s" $commandName]
+ }
+ if {($i % 8) == 0} {
+ puts $sock ""
+ }
+ incr i
+ }
+ incr i -1
+ if {($i % 8) != 0} {
+ puts $sock ""
+ }
+ puts $sock "214 Direct comments to $::ftpd::contact."
+ }
+
+ return
+}
+
+# ::ftpd::command::LIST --
+#
+# Handle the LIST ftp command. Lists the names of the files in the
+# specified path.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# list - The arguments to the LIST command.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# A listing of files is written to the socket.
+
+proc ::ftpd::command::LIST {sock filename} {
+ ::ftpd::List $sock $filename list
+ return
+}
+
+# ::ftpd::command::MDTM --
+#
+# Handle the MDTM ftp command. Prints the modification time of the
+# specified file to the socket.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# list - The arguments to the MDTM command.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# Prints the modification time of the specified file to the socket.
+
+proc ::ftpd::command::MDTM {sock filename} {
+ upvar #0 ::ftpd::$sock data
+
+ set path [file join $data(cwd) [string trimleft $filename /]]
+ if {[catch {::ftpd::Fs mtime $path $sock} msg]} {
+ puts $sock "500 MDTM Failed: $path $msg"
+ ::ftpd::FinishData $sock
+ }
+ return
+}
+
+# ::ftpd::command::MKD --
+#
+# Handle the MKD ftp command. Create the specified directory.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# list - The arguments to the MKD command.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# The directory specified by $path (if it exists) is deleted.
+
+proc ::ftpd::command::MKD {sock filename} {
+ upvar #0 ::ftpd::$sock data
+
+ set path [file join $data(cwd) [string trimleft $filename /]]
+
+ if {[::ftpd::hasCallback authFileCmd]} {
+ set cmd $::ftpd::cfg(authFileCmd)
+ lappend cmd $data(user) $path write
+ if {[eval $cmd] == 0} {
+ puts $sock "550 $filename: Permission denied"
+ return
+ }
+ }
+
+ if {[catch {::ftpd::Fs mkdir $path $sock} f]} {
+ puts $sock "500 MKD Failed: $path $f"
+ }
+ return
+}
+
+# ::ftpd::command::NOOP --
+#
+# Handle the NOOP ftp command. Do nothing.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# list - The arguments to the NOOP command.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# Prints the proper NOOP response.
+
+proc ::ftpd::command::NOOP {sock list} {
+
+ puts $sock "200 NOOP command successful."
+ return
+}
+
+# ::ftpd::command::NLST --
+#
+# Handle the NLST ftp command. Lists the full file stat of all of the
+# files that are in the specified path.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# list - The arguments to the NLST command.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# A listing of file stats is written to the socket.
+
+proc ::ftpd::command::NLST {sock filename} {
+ ::ftpd::List $sock $filename nlst
+ return
+}
+
+# ::ftpd::command::PASS --
+#
+# Handle the PASS ftp command. Check whether the specified user
+# and password are allowed to log in (using the authUsrCmd). If
+# they are allowed to log in, they are allowed to continue. If
+# not ::ftpd::Log is used to log and error, and an "Access Denied"
+# error is sent back.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# list - The arguments to the PASS command.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# The user is accepted, or an error is logged and the user/password is
+# denied..
+
+proc ::ftpd::command::PASS {sock password} {
+ upvar #0 ::ftpd::$sock data
+
+ if {$password == ""} {
+ puts $sock "530 Please login with USER and PASS."
+ return
+ }
+ set data(pass) $password
+
+ ::ftpd::Log debug "pass <$data(pass)>"
+
+ if {![::ftpd::hasCallback authUsrCmd]} {
+ error "-authUsrCmd must not be empty, need a way to authenticate the user."
+ }
+
+ # Call out to authenticate the user. A return value of 0 or an
+ # error causes the system to reject the connection. Everything
+ # else (with 1 prefered) leads to acceptance.
+
+ set cmd $::ftpd::cfg(authUsrCmd)
+ lappend cmd $data(user) $data(pass)
+
+ set fail [catch {eval $cmd} res]
+
+ if {$fail} {
+ ::ftpd::Log error "AuthUsr error: $res"
+ }
+ if {$fail || ($res == 0)} {
+ ::ftpd::Log note "AuthUsr: Access denied to <$data(user)> <$data(pass)>."
+ unset data(user)
+ unset data(pass)
+ puts $sock "551 Access Denied"
+ } else {
+ puts $sock "230 OK"
+ set data(access) 1
+ }
+ return
+}
+
+# ::ftpd::command::PORT --
+#
+# Handle the PORT ftp command. Create a new socket with the specified
+# paramaters.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# list - The arguments to the PORT command.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# A new socket, data(sock2), is opened.
+
+proc ::ftpd::command::PORT {sock numbers} {
+ upvar #0 ::ftpd::$sock data
+ set x [split $numbers ,]
+
+ ::ftpd::FinishData $sock
+
+ set data(sock2) [socket [join [lrange $x 0 3] .] \
+ [expr {([lindex $x 4] << 8) | [lindex $x 5]}]]
+ fconfigure $data(sock2) -translation $data(mode)
+ puts $sock "200 PORT OK"
+ return
+}
+
+# ::ftpd::command::PWD --
+#
+# Handle the PWD ftp command. Prints the current working directory to
+# the socket.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# list - The arguments to the PWD command.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# Prints the current working directory to the socket.
+
+proc ::ftpd::command::PWD {sock list} {
+ upvar #0 ::ftpd::$sock data
+ ::ftpd::Log debug $data(cwd)
+ puts $sock "257 \"$data(cwd)\" is current directory."
+ return
+}
+
+# ::ftpd::command::QUIT --
+#
+# Handle the QUIT ftp command. Closes the socket.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# list - The arguments to the PWD command.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# Closes the connection.
+
+proc ::ftpd::command::QUIT {sock list} {
+ ::ftpd::Log note "Closed $sock"
+ puts $sock "221 Goodbye."
+ ::ftpd::Finish $sock
+ # FRINK: nocheck
+ #unset ::ftpd::$sock
+ return
+}
+
+# ::ftpd::command::REIN --
+#
+# Handle the REIN ftp command. This command terminates a USER, flushing
+# all I/O and account information, except to allow any transfer in
+# progress to be completed. All parameters are reset to the default
+# settings and the control connection is left open.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# list - The arguments to the REIN command.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# The file specified by $path (if it exists) is copied to the socket
+# data(sock2) otherwise a 'Copy Failed' message is output.
+
+proc ::ftpd::command::REIN {sock list} {
+ upvar #0 ::ftpd::$sock data
+
+ ::ftpd::FinishData $sock
+ catch {close $data(sock2a)}
+
+ # Reinitialize the user and connection data.
+
+ array set data [list \
+ access 0 \
+ state command \
+ buffering line \
+ cwd "$::ftpd::cwd" \
+ mode binary \
+ sock2a "" \
+ sock2 ""]
+
+ return
+}
+
+# ::ftpd::command::RETR --
+#
+# Handle the RETR ftp command. Gets a readable channel for the file
+# specified from ::ftpd::Fs and copies the file to second socket
+# data(sock2).
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# list - The arguments to the RETR command.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# The file specified by $path (if it exists) is copied to the socket
+# data(sock2) otherwise a 'Copy Failed' message is output.
+
+proc ::ftpd::command::RETR {sock filename} {
+ upvar #0 ::ftpd::$sock data
+
+ set path [file join $data(cwd) [string trimleft $filename /]]
+
+ if {[::ftpd::hasCallback authFileCmd]} {
+ set cmd $::ftpd::cfg(authFileCmd)
+ lappend cmd $data(user) $path read
+ if {[eval $cmd] == 0} {
+ puts $sock "550 $filename: Permission denied"
+ return
+ }
+ }
+
+ #
+ # Patched Mark O'Connor
+ #
+ if {![catch {::ftpd::Fs retr $path $data(mode)} f]} {
+ puts $sock "150 Copy Started ($data(mode))"
+ ::ftpd::PasvCheckAndWait $sock
+ fcopy $f $data(sock2) -command [list ::ftpd::GetDone $sock $data(sock2) $f ""]
+ } else {
+ puts $sock "500 Copy Failed: $path $f"
+ ::ftpd::FinishData $sock
+ }
+ return
+}
+
+# ::ftpd::command::RMD --
+#
+# Handle the RMD ftp command. Remove the specified directory.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# list - The arguments to the RMD command.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# The directory specified by $path (if it exists) is deleted.
+
+proc ::ftpd::command::RMD {sock filename} {
+ upvar #0 ::ftpd::$sock data
+
+ set path [file join $data(cwd) [string trimleft $filename /]]
+
+ if {[::ftpd::hasCallback authFileCmd]} {
+ set cmd $::ftpd::cfg(authFileCmd)
+ lappend cmd $data(user) $path write
+ if {[eval $cmd] == 0} {
+ puts $sock "550 $filename: Permission denied"
+ return
+ }
+ }
+ if {[catch {::ftpd::Fs rmdir $path $sock} f]} {
+ puts $sock "500 RMD Failed: $path $f"
+ }
+ return
+}
+
+# ::ftpd::command::RNFR --
+#
+# Handle the RNFR ftp command. Stores the name of the file to rename
+# from.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# list - The arguments to the RNFR command.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# If the file specified by $path exists, then store the name and request
+# the next name.
+
+proc ::ftpd::command::RNFR {sock filename} {
+ upvar #0 ::ftpd::$sock data
+
+ set path [file join $data(cwd) [string trimleft $filename /]]
+
+ if {[::ftpd::Fs exists $path]} {
+ if {[::ftpd::hasCallback authFileCmd]} {
+ set cmd $::ftpd::cfg(authFileCmd)
+ lappend cmd $data(user) $path write
+ if {[eval $cmd] == 0} {
+ puts $sock "550 $filename: Permission denied"
+ return
+ }
+ }
+
+ puts $sock "350 File exists, ready for destination name"
+ set data(renameFrom) $path
+ } else {
+ puts $sock "550 $path: No such file or directory."
+ }
+ return
+}
+
+# ::ftpd::command::RNTO --
+#
+# Handle the RNTO ftp command. Renames the file specified by 'RNFR' if
+# one was specified.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# list - The arguments to the RNTO command.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# The specified file is renamed.
+
+proc ::ftpd::command::RNTO {sock filename} {
+ upvar #0 ::ftpd::$sock data
+
+ if {$filename == ""} {
+ puts $sock "500 'RNTO': command not understood."
+ return
+ }
+
+ set path [file join $data(cwd) [string trimleft $filename /]]
+
+ if {![info exists data(renameFrom)]} {
+ puts $sock "503 Bad sequence of commands."
+ return
+ }
+ if {[::ftpd::hasCallback authFileCmd]} {
+ set cmd $::ftpd::cfg(authFileCmd)
+ lappend cmd $data(user) $path write
+ if {[eval $cmd] == 0} {
+ puts $sock "550 $filename: Permission denied"
+ return
+ }
+ }
+
+
+ if {![catch {::ftpd::Fs rename $data(renameFrom) $path $sock} msg]} {
+ unset data(renameFrom)
+ } else {
+ unset data(renameFrom)
+ puts $sock "500 'RNTO': command not understood."
+ }
+ return
+}
+
+# ::ftpd::command::SIZE --
+#
+# Handle the SIZE ftp command. Prints the modification time of the
+# specified file to the socket.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# list - The arguments to the MDTM command.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# Prints the size of the specified file to the socket.
+
+proc ::ftpd::command::SIZE {sock filename} {
+ upvar #0 ::ftpd::$sock data
+
+ set path [file join $data(cwd) [string trimleft $filename /]]
+ if {[catch {::ftpd::Fs size $path $sock} msg]} {
+ puts $sock "500 SIZE Failed: $path $msg"
+ ::ftpd::FinishData $sock
+ }
+ return
+}
+
+# ::ftpd::command::STOR --
+#
+# Handle the STOR ftp command. Gets a writable channel for the file
+# specified from ::ftpd::Fs and copies the data from data(sock2) to
+# the writable channel.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# list - The arguments to the STOR command.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# The data is copied to from the socket data(sock2) to the
+# writable channel to create a file.
+
+proc ::ftpd::command::STOR {sock filename} {
+ upvar #0 ::ftpd::$sock data
+
+ set path [file join $data(cwd) [string trimleft $filename /]]
+ if {[::ftpd::hasCallback authFileCmd]} {
+ set cmd $::ftpd::cfg(authFileCmd)
+ lappend cmd $data(user) $path write
+ if {[eval $cmd] == 0} {
+ puts $sock "550 $filename: Permission denied"
+ return
+ }
+ }
+
+ #
+ # Patched Mark O'Connor
+ #
+ if {![catch {::ftpd::Fs store $path $data(mode)} f]} {
+ puts $sock "150 Copy Started ($data(mode))"
+ ::ftpd::PasvCheckAndWait $sock
+ fcopy $data(sock2) $f -command [list ::ftpd::GetDone $sock $data(sock2) $f ""]
+ } else {
+ puts $sock "500 Copy Failed: $path $f"
+ ::ftpd::FinishData $sock
+ }
+ return
+}
+
+# ::ftpd::command::STOU --
+#
+# Handle the STOR ftp command. Gets a writable channel for the file
+# specified from ::ftpd::Fs and copies the data from data(sock2) to
+# the writable channel.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# list - The arguments to the STOU command.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# The data is copied to from the socket data(sock2) to the
+# writable channel to create a file.
+
+proc ::ftpd::command::STOU {sock filename} {
+ upvar #0 ::ftpd::$sock data
+
+ set path [file join $data(cwd) [string trimleft $filename /]]
+ if {[::ftpd::hasCallback authFileCmd]} {
+ set cmd $::ftpd::cfg(authFileCmd)
+ lappend cmd $data(user) $path write
+ if {[eval $cmd] == 0} {
+ puts $sock "550 $filename: Permission denied"
+ return
+ }
+ }
+
+ set file $path
+ set i 0
+ while {[::ftpd::Fs exists $file]} {
+ set file "$path.$i"
+ incr i
+ }
+
+ #
+ # Patched Mark O'Connor
+ #
+ if {![catch {::ftpd::Fs store $file $data(mode)} f]} {
+ puts $sock "150 Copy Started ($data(mode))"
+ ::ftpd::PasvCheckAndWait $sock
+ fcopy $data(sock2) $f -command [list ::ftpd::GetDone $sock $data(sock2) $f $file]
+ } else {
+ puts $sock "500 Copy Failed: $path $f"
+ ::ftpd::FinishData $sock
+ }
+ return
+}
+
+# ::ftpd::command::SYST --
+#
+# Handle the SYST ftp command. Print the system information.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# list - The arguments to the SYST command.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# Prints the system information.
+
+proc ::ftpd::command::SYST {sock list} {
+ upvar #0 ::ftpd::$sock data
+
+ global tcl_platform
+
+ if {[string equal $tcl_platform(platform) "unix"]} {
+ set platform UNIX
+ } elseif {[string equal $tcl_platform(platform) "windows"]} {
+ set platform WIN32
+ } elseif {[string equal $tcl_platform(platform) "macintosh"]} {
+ set platform MACOS
+ } else {
+ set platform UNKNOWN
+ }
+ set version [string toupper $tcl_platform(os)]
+ puts $sock "215 $platform Type: L8 Version: $version"
+
+ return
+}
+
+# ::ftpd::command::TYPE --
+#
+# Handle the TYPE ftp command. Sets up the proper translation mode on
+# the data socket data(sock2)
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# list - The arguments to the TYPE command.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# The translation mode of the data channel is changed to the appropriate
+# mode.
+
+proc ::ftpd::command::TYPE {sock type} {
+ upvar #0 ::ftpd::$sock data
+
+ if {[string compare i [string tolower $type]] == 0} {
+ set data(mode) binary
+ } else {
+ set data(mode) auto
+ }
+
+ if {$data(sock2) != {}} {
+ fconfigure $data(sock2) -translation $data(mode)
+ }
+ puts $sock "200 Type set to $type."
+ return
+}
+
+# ::ftpd::command::USER --
+#
+# Handle the USER ftp command. Store the username, and request a
+# password.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# list - The arguments to the USER command.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# A message is printed asking for the password.
+
+proc ::ftpd::command::USER {sock username} {
+ upvar #0 ::ftpd::$sock data
+
+ if {$username == ""} {
+ puts $sock "530 Please login with USER and PASS."
+ return
+ }
+ set data(user) $username
+ puts $sock "331 Password Required"
+
+ ::ftpd::Log debug "user <$data(user)>"
+ return
+}
+
+# ::ftpd::GetDone --
+#
+# The fcopy command callback for both the RETR and STOR calls. Called
+# after the fcopy completes.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# sock2 - The data socket data(sock2).
+# f - The file channel.
+# filename - The name of the unique file (if a unique
+# transfer was requested), and the empty string
+# otherwise
+# bytes - The number of bytes that were copied.
+# err - Passed if an error occurred during the fcopy.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# The open file channel is closed and a 'complete' message is printed to
+# the socket.
+
+proc ::ftpd::GetDone {sock sock2 f filename bytes {err {}}} {
+ upvar #0 ::ftpd::$sock data
+ variable cfg
+
+ close $f
+ FinishData $sock
+
+ if {[string length $err]} {
+ puts $sock "226- $err"
+ } elseif {$filename == ""} {
+ puts $sock "226 Transfer complete ($bytes bytes)"
+ } else {
+ puts $sock "226 Transfer complete (unique file name: $filename)."
+ }
+ if {[hasCallback xferDoneCmd]} then {
+ catch {$cfg(xferDoneCmd) $sock $sock2 $f $bytes $filename $err}
+ }
+ Log debug "GetDone $f $sock2 $bytes bytes filename: $filename"
+ return
+}
+
+# ::ftpd::List --
+#
+# Handle the NLST and LIST ftp commands. Shared command to do the
+# actual listing of files.
+#
+# Arguments:
+# sock - The channel for this connection to the ftpd.
+# filename - The path/filename to list.
+# style - The type of listing -- nlst or list.
+#
+# Results:
+# None.
+#
+# Side Effects:
+# A listing of file stats is written to the socket.
+
+proc ::ftpd::List {sock filename style} {
+ upvar #0 ::ftpd::$sock data
+ puts $sock "150 Opening data channel"
+
+ set path [file join $data(cwd) $filename]
+
+ PasvCheckAndWait $sock
+ Fs dlist $path $style $data(sock2)
+
+ FinishData $sock
+ puts $sock "226 Listing complete"
+ return
+}
+
+# Standard filesystem - Assume the files are held on a standard disk. This
+# namespace contains the commands to act as the default fsCmd callback for the
+# ftpd.
+
+namespace eval ::ftpd::fsFile {
+ # Our document root directory
+
+ variable docRoot
+ if {![info exists docRoot]} {
+ set docRoot /
+ }
+
+ namespace export docRoot fs
+}
+
+# ::ftpd::fsFile::docRoot --
+#
+# Set or query the root of the ftpd file system. If no 'dir' argument
+# is passed, or if the 'dir' argument is the null string, then the
+# current docroot is returned. If a non-NULL 'dir' argument is passed
+# in it is set as the docRoot.
+#
+# Arguments:
+# dir - The directory to set as the ftp docRoot.
+# (optional. If unspecified, the current docRoot
+# is returned).
+#
+# Results:
+# None.
+#
+# Side Effects:
+# Sets the docRoot to the specified directory if a directory is
+# specified.
+
+proc ::ftpd::fsFile::docRoot {{dir {}}} {
+ variable docRoot
+ if {[string length $dir] == 0} {
+ return $docRoot
+ } else {
+ set docRoot $dir
+ }
+ return ""
+}
+
+# ::ftpd::fsFile::fs --
+#
+# Handles the a standard file systems file system requests and is the
+# default fsCmd callback.
+#
+# Arguments:
+# command - The filesystem command (one of dlist, retr, or
+# store). 'dlist' will list files in a
+# directory, 'retr' will get a channel to
+# to read the specified file from, and 'store'
+# will return the channel to write to.
+# path - The file name or directory to read, write or
+# list.
+# args - Additional arguments for filesystem commands.
+# Currently this is used by 'dlist' which
+# has two additional arguments 'style' and
+# 'channel-to-write-dir-list-to'. It is also
+# used by 'size' and 'mtime' which have one
+# additional argument 'channel-to-write-to'.
+#
+# Results:
+# For a 'appe', 'retr', or 'stor' a channel is returned. For 'exists' a 1
+# is returned if the path exists, and is not a directory. Otherwise a
+# 0 is returned. For 'permissions' the octal file permissions (i.e.
+# the 'file stat' mode) are returned.
+#
+# Side Effects:
+# For 'dlist' a directory listing for the specified path is written to
+# the specified channel. For 'mtime' the modification time is written
+# or an error is thrown. An error is thrown if there is no fsCmd
+# callback configured for the ftpd.
+
+proc ::ftpd::fsFile::fs {command path args} {
+ # append <path>
+ # delete <path> <channel-to-write-to>
+ # dlist <path> <style> <channel-to-write-dir-list-to>
+ # exists <path>
+ # mkdir <path> <channel-to-write-to>
+ # mtime <path> <channel-to-write-mtime-to>
+ # permissions <path>
+ # rename <path> <newpath> <channel-to-write-to>
+ # retr <path>
+ # rmdir <path> <channel-to-write-to>
+ # size <path> <channel-to-write-size-to>
+ # store <path>
+
+ global tcl_platform
+
+ variable docRoot
+
+ set path [file join $docRoot $path]
+
+ switch -exact -- $command {
+ append {
+ #
+ # Patched Mark O'Connor
+ #
+ set fhandle [open $path a]
+ if {[lindex $args 0] == "binary"} {
+ fconfigure $fhandle -translation binary -encoding binary
+ }
+ return $fhandle
+ }
+ retr {
+ #
+ # Patched Mark O'Connor
+ #
+ set fhandle [open $path r]
+ if {[lindex $args 0] == "binary"} {
+ fconfigure $fhandle -translation binary -encoding binary
+ }
+ return $fhandle
+ }
+ store {
+ #
+ # Patched Mark O'Connor
+ #
+ set fhandle [open $path w]
+ if {[lindex $args 0] == "binary"} {
+ fconfigure $fhandle -translation binary -encoding binary
+ }
+ return $fhandle
+ }
+ dlist {
+ foreach {style outchan} $args break
+ ::ftpd::Log debug "at dlist {$style} {$outchan} {$path}"
+ #set path [glob -nocomplain $path]
+ #::ftpd::Log debug "at dlist2 {$style} {$outchan} {$path}"
+
+ # Attempt to get a list of all files (even ones that start with .)
+
+ if {[file isdirectory $path]} {
+ set path1 [file join $path *]
+ set path2 [file join $path .*]
+ } else {
+ set path1 $path
+ set path2 $path
+ }
+
+ # Get a list of all files that match the glob pattern
+
+ set fileList [lsort -unique [concat [glob -nocomplain $path1] \
+ [glob -nocomplain $path2]]]
+
+ ::ftpd::Log debug "File list is {$fileList}"
+
+ switch -- $style {
+ nlst {
+ ::ftpd::Log debug "In nlist"
+ foreach f [lsort $fileList] {
+ if {[string equal [file tail $f] "."] || \
+ [string equal [file tail $f] ".."]} {
+ continue
+ }
+ if {[string equal {} $f]} then continue
+ ::ftpd::Log debug [file tail $f]
+ puts $outchan [file tail $f]
+ }
+ }
+ list {
+ # [ 766112 ] report . and .. directories (linux)
+ # Copied the code from 'nlst' above to handle this.
+
+ foreach f [lsort $fileList] {
+ if {[string equal [file tail $f] "."] || \
+ [string equal [file tail $f] ".."]} {
+ continue
+ }
+ file stat $f stat
+ if {[string equal $tcl_platform(platform) "unix"]} {
+ set user [file attributes $f -owner]
+ set group [file attributes $f -group]
+ } else {
+ set user owner
+ set group group
+ }
+ puts $outchan [format "%s %3d %s %8s %11s %s %s" \
+ [PermBits $f $stat(mode)] $stat(nlink) \
+ $user $group $stat(size) \
+ [FormDate $stat(mtime)] [file tail $f]]
+ }
+ }
+ default {
+ error "Unknown list style <$style>"
+ }
+ }
+ }
+ delete {
+ foreach {outchan} $args break
+
+ if {![file exists $path]} {
+ puts $outchan "550 $path: No such file or directory."
+ } elseif {![file isfile $path]} {
+ puts $outchan "550 $path: File exists."
+ } else {
+ file delete $path
+ puts $outchan "250 DELE command successful."
+ }
+ }
+ exists {
+ if {[file isdirectory $path]} {
+ return 0
+ } else {
+ return [file exists $path]
+ }
+ }
+ mkdir {
+ foreach {outchan} $args break
+
+ set path [string trimright $path /]
+ if {[file exists $path]} {
+ if {[file isdirectory $path]} {
+ puts $outchan "521 \"$path\" directory exists"
+ } else {
+ puts $outchan "521 \"$path\" already exists"
+ }
+ } elseif {[file exists [file dirname $path]]} {
+ file mkdir $path
+ puts $outchan "257 \"$path\" new directory created."
+ } else {
+ puts $outchan "550 $path: No such file or directory."
+ }
+ }
+ mtime {
+ foreach {outchan} $args break
+
+ if {![file exists $path]} {
+ puts $outchan "550 $path: No such file or directory"
+ } elseif {![file isfile $path]} {
+ puts $outchan "550 $path: not a plain file."
+ } else {
+ set time [file mtime $path]
+ puts $outchan [clock format $time -format "213 %Y%m%d%H%M%S"]
+ }
+ }
+ permissions {
+ file stat $path stat
+ return $stat(mode)
+ }
+ rename {
+ foreach {newname outchan} $args break
+
+ if {![file isdirectory [file dirname $newname]]} {
+ puts $outchan "550 rename: No such file or directory."
+ }
+ file rename $path $newname
+ puts $outchan "250 RNTO command successful."
+ }
+ rmdir {
+ foreach {outchan} $args break
+
+ if {![file isdirectory $path]} {
+ puts $outchan "550 $path: Not a directory."
+ } elseif {[llength [glob -nocomplain [file join $path *]]] != 0} {
+ puts $outchan "550 $path: Directory not empty."
+ } else {
+ file delete $path
+ puts $outchan "250 RMD command successful."
+ }
+ }
+ size {
+ foreach {outchan} $args break
+
+ if {![file exists $path]} {
+ puts $outchan "550 $path: No such file or directory"
+ } elseif {![file isfile $path]} {
+ puts $outchan "550 $path: not a plain file."
+ } else {
+ puts $outchan "213 [file size $path]"
+ }
+ }
+ default {
+ error "Unknown command \"$command\""
+ }
+ }
+ return ""
+}
+
+# ::ftpd::fsFile::PermBits --
+#
+# Returns the file permissions for the specified file.
+#
+# Arguments:
+# file - The file to return the permissions of.
+#
+# Results:
+# The permissions for the specified file are returned.
+#
+# Side Effects:
+# None.
+
+proc ::ftpd::fsFile::PermBits {file mode} {
+
+ array set s {
+ 0 --- 1 --x 2 -w- 3 -wx 4 r-- 5 r-x 6 rw- 7 rwx
+ }
+
+ set type [file type $file]
+ if {[string equal $type "file"]} {
+ set permissions "-"
+ } else {
+ set permissions [string index $type 0]
+ }
+ foreach j [split [format %03o [expr {$mode&0777}]] {}] {
+ append permissions $s($j)
+ }
+
+ return $permissions
+}
+
+# ::ftpd::fsFile::FormDate --
+#
+# Returns the file permissions for the specified file.
+#
+# Arguments:
+# seconds - The number of seconds returned by 'file mtime'.
+#
+# Results:
+# A formatted date is returned.
+#
+# Side Effects:
+# None.
+
+proc ::ftpd::fsFile::FormDate {seconds} {
+
+ set currentTime [clock seconds]
+ set oldTime [clock scan "6 months ago" -base $currentTime]
+ if {$seconds <= $oldTime} {
+ set time [clock format $seconds -format "%Y"]
+ } else {
+ set time [clock format $seconds -format "%H:%M"]
+ }
+ set day [string trimleft [clock format $seconds -format "%d"] 0]
+ set month [clock format $seconds -format "%b"]
+ return [format "%3s %2s %5s" $month $day $time]
+}
+
+# Only provide the package if it has been successfully
+# sourced into the interpreter.
+
+#
+# Patched Mark O'Connor
+#
+package provide ftpd 1.3
+
+
+##
+## Implementation of passive command
+##
+proc ::ftpd::command::PASV {sock argument} {
+ upvar #0 ::ftpd::$sock data
+
+ set data(sock2a) [socket -server [list ::ftpd::PasvAccept $sock] 0]
+ set list1 [fconfigure $sock -sockname]
+ set ip [lindex $list1 0]
+ set list2 [fconfigure $data(sock2a) -sockname]
+ set port [lindex $list2 2]
+ ::ftpd::Log debug "PASV on {$list1} {$list2} $ip $port"
+ set ans [split $ip {.}]
+ lappend ans [expr {($port >> 8) & 0xff}] [expr {$port & 0xff}]
+ set ans [join $ans {,}]
+ puts $sock "227 Entering Passive Mode ($ans)."
+ set data(sock2) ""
+ return
+}
+
+
+proc ::ftpd::PasvAccept {sock sock2 ip port} {
+ upvar #0 ::ftpd::$sock data
+
+ ::ftpd::Log debug "In Pasv Accept with {$sock} {$sock2} {$ip} {$port}"
+ ##
+ ## Verify this is from who it should be
+ ##
+ if {![string equal $ip $data(ip)]} then {
+ ##
+ ## Nope, so close it and wait some more
+ ##
+ close $sock2
+ return
+ }
+ ::ftpd::FinishData $sock
+
+ set data(sock2) $sock2 ; # (*), see ::ftpd::PasvCheckAndWait
+ fconfigure $data(sock2) -translation $data(mode)
+ close $data(sock2a)
+ set data(sock2a) ""
+ return
+}
+
+proc ::ftpd::PasvCheckAndWait {sock} {
+ upvar #0 ::ftpd::$sock data
+
+ # Check if we are in passive mode, with the data connection not
+ # yet established. If so, wait for the data connection to be
+ # made. This vwait is unlocked by (*) in ::ftpd::PasvAccept above.
+
+ if {$data(sock2) != ""} return
+ vwait ::ftpd::${sock}(sock2)
+ return
+}
diff --git a/tcllib/modules/ftpd/pkgIndex.tcl b/tcllib/modules/ftpd/pkgIndex.tcl
new file mode 100644
index 0000000..68aa204
--- /dev/null
+++ b/tcllib/modules/ftpd/pkgIndex.tcl
@@ -0,0 +1,2 @@
+if {![package vsatisfies [package provide Tcl] 8.3]} {return}
+package ifneeded ftpd 1.3 [list source [file join $dir ftpd.tcl]]