diff --git a/COPYING b/COPYING
new file mode 100644
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,510 @@
+
+                  GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+	51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations
+below.
+
+  When we speak of free software, we are referring to freedom of use,
+not price.  Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+  Finally, software patents pose a constant threat to the existence of
+any free program.  We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder.  Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+  Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License.  This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License.  We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+  When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library.  The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom.  The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+  We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License.  It also provides other free software developers Less
+of an advantage over competing non-free programs.  These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries.  However, the Lesser license provides advantages in certain
+special circumstances.
+
+  For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it
+becomes a de-facto standard.  To achieve this, non-free programs must
+be allowed to use the library.  A more frequent case is that a free
+library does the same job as widely used non-free libraries.  In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+  In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software.  For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+  Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+                  GNU LESSER GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control
+compilation and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+  6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Use a suitable shared library mechanism for linking with the
+    Library.  A suitable mechanism is one that (1) uses at run time a
+    copy of the library already present on the user's computer system,
+    rather than copying library functions into the executable, and (2)
+    will operate properly with a modified version of the library, if
+    the user installs one, as long as the modified version is
+    interface-compatible with the version that the work was made with.
+
+    c) Accompany the work with a written offer, valid for at least
+    three years, to give the same user the materials specified in
+    Subsection 6a, above, for a charge no more than the cost of
+    performing this distribution.
+
+    d) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    e) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply, and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License
+may add an explicit geographical distribution limitation excluding those
+countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+                            NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms
+of the ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.
+It is safest to attach them to the start of each source file to most
+effectively convey the exclusion of warranty; and each file should
+have at least the "copyright" line and a pointer to where the full
+notice is found.
+
+
+    <one line to give the library's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or
+your school, if any, to sign a "copyright disclaimer" for the library,
+if necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James
+  Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
+
+
diff --git a/COPYRIGHT b/COPYRIGHT
new file mode 100644
--- /dev/null
+++ b/COPYRIGHT
@@ -0,0 +1,25 @@
+ftphs
+Copyright (C) 2004-2006 John Goerzen <jgoerzen@complete.org>
+
+All code is under the following license unless otherwise noted:
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU Lesser General Public License as published by
+   the Free Software Foundation; either version 2.1 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+
+The GNU Lesser General Public License is available in the file COPYING in the
+source distribution.  Debian GNU/Linux users may find this in
+/usr/share/common-licenses/GPL-2.
+
+If the LGPL is unacceptable for your uses, please e-mail me; alternative
+terms can be negotiated for your project.
+
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,5 @@
+#!/usr/bin/env runhaskell
+import Distribution.Simple
+
+main = defaultMain
+
diff --git a/ftphs.cabal b/ftphs.cabal
new file mode 100644
--- /dev/null
+++ b/ftphs.cabal
@@ -0,0 +1,40 @@
+Name: ftphs
+Version: 1.0.0
+License: LGPL
+Maintainer: John Goerzen <jgoerzen@complete.org>
+Author: John Goerzen
+Stability: Stable
+Copyright: Copyright (c) 2004-2006 John Goerzen
+license-file: COPYRIGHT
+extra-source-files: COPYING
+Homepage: http://software.complete.org/ftphs
+Category: Network
+Synopsis: FTP Client and Server Library
+Description:  ftphs provides a Haskell library to implement a FTP client
+ and a FTP server.
+ .
+ ftphs has a number of features:
+ .
+  * Easy to use operation
+  * Full support of text and binary transfers
+  * Optional lazy interaction
+  * Server can serve up a real or a virtual filesystem tree
+  * Standards compliant
+Hs-Source-Dirs: src
+Exposed-Modules: Network.FTP.Client,
+    Network.FTP.Client.Parser,
+    Network.FTP.Server,
+    Network.FTP.Server.Parser
+Extensions: ExistentialQuantification, OverlappingInstances, 
+   UndecidableInstances, CPP
+Build-Depends: network, parsec, base,
+               haskell98, mtl, regex-compat, 
+               hslogger, MissingH>=0.18.0
+GHC-Options: -O2
+
+Executable: runtests
+Buildable: False
+Main-Is: runtests.hs
+HS-Source-Dirs: testsrc, .
+Extensions: ExistentialQuantification, OverlappingInstances,
+    UndecidableInstances, CPP
diff --git a/src/Network/FTP/Client.hs b/src/Network/FTP/Client.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/FTP/Client.hs
@@ -0,0 +1,522 @@
+{- arch-tag: FTP client support
+Copyright (C) 2004-2005 John Goerzen <jgoerzen@complete.org>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+-}
+
+{- |
+   Module     : Network.FTP.Client
+   Copyright  : Copyright (C) 2004-2005 John Goerzen
+   License    : GNU LGPL, version 2.1 or above
+
+   Maintainer : John Goerzen <jgoerzen@complete.org> 
+   Stability  : experimental
+   Portability: systems with networking
+
+This module provides a client-side interface to the File Transfer Protocol
+as defined by RFC959 and RFC1123.
+
+Written by John Goerzen, jgoerzen\@complete.org
+
+Welcome to the FTP module for Haskell.
+
+Here is a quick usage example to get you started.  This is a log of a real
+session with ghci: 
+
+(This would be similar in a "do" block.  You could also save it to a file and
+run that with Hugs.)
+
+> Prelude> :l Network.FTP.Client
+> ...
+
+The above loads the module.
+
+Next, we enable the debugging.  This will turn on all the @FTP sent@ and
+@FTP received@ messages you'll see.
+
+> Prelude Network.FTP.Client> enableFTPDebugging
+
+Now, connect to the server on @ftp.kernel.org@.
+
+> *Network.FTP.Client> h <- easyConnectFTP "ftp.kernel.org"
+> FTP received: 220 Welcome to ftp.kernel.org.
+
+And log in anonymously.
+
+> *Network.FTP.Client> loginAnon h
+> FTP sent: USER anonymous
+> FTP received: 331 Please specify the password.
+> FTP sent: PASS anonymous@
+> ...
+> FTP received: 230 Login successful.
+
+Change the directory...
+
+> Prelude Network.FTP.Client> cwd h "/pub/linux/kernel/Historic"
+> FTP sent: CWD /pub/linux/kernel/Historic
+> FTP received: 250 Directory successfully changed.
+
+Let's look at the directory. 'nlst' returns a list of strings, each string
+corresponding to a filename.  Here, @putStrLn . unlines@ will simply
+print them out, one per line.
+
+> Prelude Network.FTP.Client> nlst h Nothing >>= putStrLn . unlines
+> FTP sent: TYPE A
+> FTP received: 200 Switching to ASCII mode.
+> FTP sent: PASV
+> FTP received: 227 Entering Passive Mode (204,152,189,116,130,143)
+> FTP sent: NLST
+> FTP received: 150 Here comes the directory listing.
+> linux-0.01.tar.bz2
+> linux-0.01.tar.bz2.sign
+> linux-0.01.tar.gz
+> linux-0.01.tar.gz.sign
+> linux-0.01.tar.sign
+> old-versions
+> v0.99
+> FTP received: 226 Directory send OK.
+
+Let's try downloading something and print it to the screen.  Again, we use
+@putStrLn@.  We use @fst@ here because 'getbinary' returns a tuple consisting
+of a string representing the data and a 'FTPResult' code.
+
+> Prelude Network.FTP.Client> getbinary h "linux-0.01.tar.gz.sign" >>= putStrLn . fst
+> FTP sent: TYPE I
+> FTP received: 200 Switching to Binary mode.
+> FTP sent: PASV
+> FTP received: 227 Entering Passive Mode (204,152,189,116,121,121)
+> FTP sent: RETR linux-0.01.tar.gz.sign
+> FTP received: 150 Opening BINARY mode data connection for linux-0.01.tar.gz.sign (248 bytes).
+> -----BEGIN PGP SIGNATURE-----
+> Version: GnuPG v1.0.0 (GNU/Linux)
+> Comment: See http://www.kernel.org/signature.html for info
+> 
+> iD8DBQA54rf0yGugalF9Dw4RAqelAJ9lafFni4f/QyJ2IqDXzW2nz/ZIogCfRPtg
+> uYpWffOhkyByfhUt8Lcelec=
+> =KnLA
+> -----END PGP SIGNATURE-----
+> FTP received: 226 File send OK.
+
+Here's an example showing you what the result code looks like.
+
+> Prelude Network.FTP.Client> getbinary h "linux-0.01.tar.gz.sign" >>= print . snd
+> ...
+> (226,["File send OK."])
+
+The first component of the 'FTPResult' object is the numeric status code from
+the server.  The second component is a list of message lines from the server.
+
+Now, let's get a more detailed directory listing:
+
+> Prelude Network.FTP.Client> dir h Nothing >>= putStrLn . unlines
+> ...
+> -r--r--r--    1 536      536         63362 Oct 30  1993 linux-0.01.tar.bz2
+> -r--r--r--    1 536      536           248 Oct 30  1993 linux-0.01.tar.bz2.sign
+> -r--r--r--    1 536      536         73091 Oct 30  1993 linux-0.01.tar.gz
+> -r--r--r--    1 536      536           248 Oct 30  1993 linux-0.01.tar.gz.sign
+> -r--r--r--    1 536      536           248 Oct 30  1993 linux-0.01.tar.sign
+> drwxrwsr-x    5 536      536          4096 Mar 20  2003 old-versions
+> drwxrwsr-x    2 536      536          4096 Mar 20  2003 v0.99
+> FTP received: 226 Directory send OK.
+
+And finally, log out:
+
+> Prelude Network.FTP.Client> quit h
+> FTP sent: QUIT
+> FTP received: 221 Goodbye.
+
+Here is one big important caution:
+
+/You MUST consume all data from commands that return file data before you
+issue any other FTP commands./
+
+That's due to the lazy nature of Haskell.  This means that, for instance,
+you can't just iterate over the items 'nlst' returns, trying to 'getbinary'
+each one of them -- the system is still transferring 'nlst' data while you
+are trying that, and confusion will ensue.  Either open two FTP connections
+or make sure you consume the 'nlst' data first.
+
+Here is a partial list of commands effected: 'nlst', 'dir', 'getbinary',
+'getlines', 'downloadbinary'.
+
+The 'Data.List.Utils.seqList' function could be quite helpful here.  For instance:
+
+> x <- nlst h Nothing
+> map (\fn -> ...download files from FTP... ) (seqList x)
+
+If you omit the call to 'Data.List.Utils.seqList', commands to download files
+will be issued before the entire directory listing is read.  FTP cannot handle
+this.
+
+The corrolary is:
+
+/Actions that yield lazy data for data uploading must not issue FTP
+commands themselves./
+
+This will be fairly rare.  Just be aware of this.
+
+This module logs messages under @Network.FTP.Client@ for outgoing
+traffic and @Network.FTP.Client.Parser@ for incoming traffic, all with the
+'System.Log.DEBUG' priority, so by default, no log messages are seen.
+The 'enableFTPDebugging' function will adjust the priorities of these
+two handlers so debug messages are seen.  Only control channel conversations
+are logged.  Data channel conversations are never logged.
+
+All exceptions raised by this module have a string beginning with
+@\"FTP: \"@.  Most errors will be IO userErrors.  In a few extremely rare
+cases, errors may be raised by the Prelude error function, though these
+will also have a string beginning with @\"FTP: \"@.  Exceptions raised by
+the underlying networking code will be passed on to you unmodified.
+
+Useful standards:
+
+* RFC959, <http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc0959.html>
+
+* Passive mode, RFC1579, <http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc1579.html>
+
+* Extended passive mode, IPv6, RFC2428 <http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2428.html>
+
+* Feature negotiation, RFC2389, <http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2389.html>
+
+* Internationalization of FTP, RFC2640, <http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2640.html>
+
+* FTP security considerations, RFC2577, <http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2577.html>
+
+* FTP URLs, RFC1738, <http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc1738.html>
+
+-}
+
+module Network.FTP.Client(-- * Establishing\/Removing connections
+                                   easyConnectFTP, connectFTP,
+                                   loginAnon, login, quit, 
+                                   -- * Configuration
+                                   setPassive, isPassive, enableFTPDebugging,
+                                   -- * Directory listing
+                                   nlst, dir, 
+                                   -- * File downloads
+                                   getlines, getbinary,
+                                   downloadbinary,
+                                   -- * File uploads
+                                   putlines, putbinary,
+                                   uploadbinary,
+                                   -- * File manipulation
+                                   rename, delete, size,
+                                   -- * Directory manipulation
+                                   cwd, mkdir, rmdir, pwd, 
+                                   -- * Low-level advanced commands
+                                   FTPConnection,
+                                   transfercmd, ntransfercmd,
+                                   retrlines, storlines
+                       )
+where
+import Network.FTP.Client.Parser
+import Network.BSD
+import Network.Socket
+import System.IO.Binary
+import qualified Network
+import System.IO
+import System.IO.Unsafe
+import System.Log.Logger
+import Network.Utils
+import Data.String
+data FTPConnection = FTPConnection {readh :: IO String,
+                                    writeh :: Handle,
+                                    socket_internal :: Socket,
+                                    isPassive :: Bool}
+
+getresp h = do
+            c <- (readh h)
+            debugParseGoodReply c
+
+
+logsend m = debugM "Network.FTP.Client" ("FTP sent: " ++ m)
+sendcmd h c = do logsend c
+                 hPutStr (writeh h) (c ++ "\r\n")
+                 getresp h
+
+{- | Connect to the remote FTP server and read but discard
+   the welcome.  Assumes
+   default FTP port, 21, on remote. -}
+easyConnectFTP :: Network.HostName -> IO FTPConnection
+easyConnectFTP h = do x <- connectFTP h 21
+                      return (fst x)
+
+{- | Enable logging of FTP messages through 'System.Log.Logger'.
+This sets the log levels of @Network.FTP.Client.Parser@ and
+@Network.FTP.Client@ to DEBUG.  By default, this means that
+full protocol dumps will be sent to stderr.
+
+The effect is global and persists until changed.
+-}
+enableFTPDebugging :: IO ()
+enableFTPDebugging = 
+    do
+    updateGlobalLogger "Network.FTP.Client.Parser" (setLevel DEBUG)
+    updateGlobalLogger "Network.FTP.Client" (setLevel DEBUG)
+
+{- | Connect to remote FTP server and read the welcome. -}
+connectFTP :: Network.HostName -> PortNumber -> IO (FTPConnection, FTPResult)
+connectFTP h p =
+    let readchars :: Handle -> IO String
+        readchars h = do
+                      c <- hGetChar h
+                      next <- unsafeInterleaveIO $ readchars h
+                      return (c : next)
+        in
+    do
+    s <- connectTCP h p
+    newh <- socketToHandle s ReadWriteMode
+    hSetBuffering newh LineBuffering
+    let h = FTPConnection {readh = readchars newh, 
+                           socket_internal = s,
+                           writeh = newh, isPassive = True}
+    resp <- getresp h
+    forceioresp 200 resp
+    return (h, resp)
+
+{- | Log in anonymously. -}
+loginAnon :: FTPConnection -> IO FTPResult
+loginAnon h = login h "anonymous" (Just "anonymous@") Nothing
+
+{- | Log in to an FTP account. -}
+login :: FTPConnection                  -- ^ Connection
+         -> String                         -- ^ Username
+         -> Maybe String                -- ^ Password
+         -> Maybe String                -- ^ Account (rarely used)
+         -> IO FTPResult
+login h user pass acct =
+    do
+    ur <- sendcmd h ("USER " ++ user)
+    if isxresp 300 ur then
+       case pass of
+            Nothing -> fail "FTP: Server demands password, but no password given"
+            Just p -> do pr <- sendcmd h ("PASS " ++ p)
+                         if isxresp 300 pr then
+                            case acct of
+                                Nothing -> fail "FTP: server demands account, but no account given"
+                                Just a -> do ar <- sendcmd h ("ACCT " ++ a)
+                                             forceioresp 200 ar
+                                             return ar
+                            else return $! forcexresp 200 pr
+       else return $! forcexresp 200 ur
+
+{- | Sets whether passive mode is used (returns new
+connection object reflecting this) -}
+setPassive :: FTPConnection -> Bool -> FTPConnection            
+setPassive f b = f{isPassive = b}
+
+{- | Finds the addres sof the remote. -}
+makepasv :: FTPConnection -> IO SockAddr
+makepasv h =
+    do
+    r <- sendcmd h "PASV"
+    respToSockAddr r
+
+{- | Opens a port and sends it to the remote. -}
+makeport :: FTPConnection -> IO (Socket, FTPResult)
+makeport h =
+    let listenaddr (SockAddrInet _ h) = SockAddrInet aNY_PORT h
+        listenaddr _ = error "FTP: Can't use port mode to non-TCP server"
+        in
+        do addr <- getSocketName (socket_internal h)
+           mastersock <- listenTCPAddr (listenaddr addr) 1
+           newaddr <- getSocketName mastersock
+           ps <- toPortString newaddr
+           result <- sendcmd h ("PORT " ++ ps)
+           return (mastersock, result)
+
+{- | Establishes a connection to the remote. 
+
+FIXME: need support for rest
+-}
+ntransfercmd :: FTPConnection -> String -> IO (Handle, Maybe Integer)
+ntransfercmd h cmd =
+    let sock = if isPassive h
+               then do
+                    addr <- makepasv h
+                    s <- connectTCPAddr addr
+                    r <- sendcmd h cmd
+                    forceioresp 100 r
+                    return s
+               else do 
+                    masterresult <- makeport h
+                    r <- sendcmd h cmd
+                    forceioresp 100 r
+                    acceptres <- accept (fst masterresult)
+                    sClose (fst masterresult)
+                    return (fst acceptres)
+        in do
+           s <- sock
+           newh <- socketToHandle s ReadWriteMode
+           hSetBuffering newh (BlockBuffering (Just 4096))
+           return (newh, Nothing)
+
+{- | Returns the socket part from calling 'ntransfercmd'. -}
+transfercmd :: FTPConnection -> String -> IO Handle
+transfercmd h cmd = do x <- ntransfercmd h cmd
+                       return (fst x)
+
+{- | Stores the lines of data to the remote.  The string gives the
+commands to issue. -}
+storlines :: FTPConnection -> String -> [String] -> IO FTPResult
+storlines h cmd input =
+    do
+    sendcmd h "TYPE A"
+    newh <- transfercmd h cmd
+    hPutStr newh (concatMap (++ "\r\n") input)
+    hClose newh
+    getresp h
+
+{- | Stores the binary data to the remote.  The first string gives the
+commands to issue. -}
+storbinary :: FTPConnection -> String -> String -> IO FTPResult
+storbinary h cmd input =
+    do sendcmd h "TYPE I"
+       newh <- transfercmd h cmd
+       hPutStr newh input
+       hClose newh
+       getresp h
+
+{- | Retrieves lines of data from the remote. The string gives 
+the command to issue. -}
+retrlines :: FTPConnection -> String -> IO ([String], FTPResult)
+retrlines h cmd =
+    -- foo returns the empty last item and closes the handle when done
+    let foo theh [] = do hClose theh
+                         r <- getresp h
+                         return ([], r)
+        foo theh ("" : []) = foo theh []
+        foo theh (x:xs) = do next <- unsafeInterleaveIO $ foo theh xs
+                             return $ (x : fst next, snd next)
+        in do
+              sendcmd h "TYPE A"
+              newh <- transfercmd h cmd
+              c <- hGetContents newh
+              foo newh (split "\r\n" $ c)
+
+{- | Retrieves binary data from the remote. The string gives the command
+to issue. -}
+retrbinary :: FTPConnection -> String -> IO (String, FTPResult)
+retrbinary h cmd =
+    let foo h2 [] = do hClose h2
+                       r <- getresp h
+                       return ([], r)
+        foo h2 (x:xs) = do next <- unsafeInterleaveIO $ foo h2 xs
+                           return $ (x : fst next, snd next)
+        in do
+           sendcmd h "TYPE I"
+           newh <- transfercmd h cmd
+           c <- hGetContents newh
+           foo newh c
+
+{- | Retrieves the specified file in text mode. -}
+getlines :: FTPConnection -> String -> IO ([String], FTPResult)
+getlines h fn = retrlines h ("RETR " ++ fn)
+
+{- | Retrieves the specified file in binary mode. -}
+getbinary :: FTPConnection -> String -> IO (String, FTPResult)
+getbinary h fn = retrbinary h ("RETR " ++ fn)
+
+{- | Puts data in the specified file in text mode.  The first string
+is the filename. -}
+putlines :: FTPConnection -> String -> [String] -> IO FTPResult
+putlines h fn input = storlines h ("STOR " ++ fn) input 
+
+{- | Puts data in the specified file in binary.  The first string is the filename. -}
+putbinary :: FTPConnection -> String -> String -> IO FTPResult
+putbinary h fn input = storbinary h ("STOR " ++ fn) input 
+
+{- | Uploads a file from disk in binary mode. Note: filename is used for both local and remote. -}
+uploadbinary :: FTPConnection -> String -> IO FTPResult
+uploadbinary h fn = do input <- readBinaryFile fn
+                       putbinary h fn input
+
+{- | Downloads a file from remote and saves to disk in binary mode.  Note: filename is used for both local and remote. -}
+downloadbinary :: FTPConnection -> String -> IO FTPResult
+downloadbinary h fn = do r <- getbinary h fn
+                         writeBinaryFile fn (fst r)
+                         return (snd r)
+
+{- | Retrieves a list of files in the given directory. 
+
+FIXME: should this take a list of dirs? -}
+nlst :: FTPConnection
+        -> Maybe String                 -- ^ The directory to list.  If Nothing, list the current directory.
+        -> IO [String]
+nlst h Nothing = retrlines h "NLST" >>= return . fst
+nlst h (Just dirname) = retrlines h ("NLST " ++ dirname) >>= return . fst
+
+{- | Retrieve the system-specific long form of a directory list.
+
+FIXME: should this take a list of dirs? -}
+dir :: FTPConnection
+       -> Maybe String                  -- ^ The directory to list.  If Nothing, list the current directory.
+       -> IO [String]
+dir h Nothing = retrlines h "LIST" >>= return .  fst
+dir h (Just dirname) = retrlines h ("LIST " ++ dirname) >>= return . fst
+
+{- | Rename or move a file. -}
+rename :: FTPConnection
+          -> String                     -- ^ Old name
+          -> String                     -- ^ New name
+          -> IO FTPResult
+rename h old new = do
+                   r <- sendcmd h ("RNFR " ++ old)
+                   forceioresp 300 r
+                   sendcmd h ("RNTO " ++ new)
+
+{- | Delete (unlink) a file. -}
+delete :: FTPConnection -> String -> IO FTPResult
+delete h fn = sendcmd h ("DELE " ++ fn)
+
+{- | Change the working directory. -}
+cwd :: FTPConnection -> String -> IO FTPResult
+cwd h ".." = sendcmd h "CDUP"
+cwd h "" = cwd h "."
+cwd h newdir = sendcmd h ("CWD " ++ newdir)
+
+{- | Get the size of a file.
+
+This command is non-standard and may possibly fail.
+-}
+size :: (Num a, Read a) => FTPConnection -> String -> IO a
+size h fn = do
+            r <- sendcmd h ("SIZE " ++ fn)
+            forceioresp 200 r
+            return (read . head . snd $ r)
+
+-- | Make new directory.  Returns the absolute name of the
+-- new directory if possible.
+mkdir :: FTPConnection -> String -> IO (Maybe String, FTPResult)
+mkdir h fn = do x <- sendcmd h ("MKD " ++ fn)
+                return (parseDirName x, x)
+
+-- | Remove a directory.
+rmdir :: FTPConnection -> String -> IO FTPResult
+rmdir h fn = sendcmd h ("RMD " ++ fn)
+
+-- | Print the current working directory.  The first component of the result
+-- is the parsed directory name, if the servers response was parsable.
+pwd :: FTPConnection -> IO (Maybe String, FTPResult)
+pwd h = do x <- sendcmd h ("PWD")
+           return (parseDirName x, x)
+
+-- | Log off the server and quit.
+quit :: FTPConnection -> IO FTPResult
+quit h = do
+         r <- sendcmd h "QUIT"
+         hClose (writeh h)
+         -- hClose (readh_internal h)
+         return r
diff --git a/src/Network/FTP/Client/Parser.hs b/src/Network/FTP/Client/Parser.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/FTP/Client/Parser.hs
@@ -0,0 +1,259 @@
+{- arch-tag: FTP protocol parser
+Copyright (C) 2004 John Goerzen <jgoerzen@complete.org>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+-}
+
+{- |
+   Module     : Network.FTP.Client.Parser
+   Copyright  : Copyright (C) 2004 John Goerzen
+   License    : GNU LGPL, version 2.1 or above
+
+   Maintainer : John Goerzen <jgoerzen@complete.org> 
+   Stability  : provisional
+   Portability: systems with networking
+
+This module provides a parser that is used internally by
+"Network.FTP.Client".  You almost certainly do not want to use
+this module directly.  Use "Network.FTP.Client" instead.
+
+Written by John Goerzen, jgoerzen\@complete.org
+
+-}
+
+module Network.FTP.Client.Parser(parseReply, parseGoodReply,
+                                         toPortString, fromPortString,
+                                         debugParseGoodReply,
+                                         respToSockAddr,
+                                         FTPResult,
+                                         -- * Utilities
+                                         unexpectedresp, isxresp,
+                                         forcexresp,
+                                         forceioresp,
+                                         parseDirName)
+where
+
+import Text.ParserCombinators.Parsec
+import Text.ParserCombinators.Parsec.Utils
+import Data.List.Utils
+import Data.Bits.Utils
+import Data.String
+import System.Log.Logger
+import Network.Socket(SockAddr(..), PortNumber(..), inet_addr, inet_ntoa)
+import System.IO(Handle, hGetContents)
+import System.IO.Unsafe
+import Text.Regex
+import Data.Word
+type FTPResult = (Int, [String])
+
+-- import Control.Exception(Exception(PatternMatchFail), throw)
+
+logit :: String -> IO ()
+logit m = debugM "Network.FTP.Client.Parser" ("FTP received: " ++ m)
+
+----------------------------------------------------------------------
+-- Utilities
+----------------------------------------------------------------------
+
+unexpectedresp m r = "FTP: Expected " ++ m ++ ", got " ++ (show r)
+
+isxresp desired (r, _) = r >= desired && r < (desired + 100)
+
+forcexresp desired r = if isxresp desired r
+                       then r
+                       else error ((unexpectedresp (show desired)) r)
+
+forceioresp :: Int -> FTPResult -> IO ()
+forceioresp desired r = if isxresp desired r
+                        then return ()
+                        else fail (unexpectedresp (show desired) r)
+
+
+crlf :: Parser String
+crlf = string "\r\n" <?> "CRLF"
+
+sp :: Parser Char
+sp = char ' '
+
+code :: Parser Int
+code = do
+       s <- codeString
+       return (read s)
+
+codeString :: Parser String
+codeString = do
+             first <- oneOf "123456789" <?> "3-digit reply code"
+             remaining <- count 2 digit <?> "3-digit reply code"
+             return (first : remaining)
+
+specificCode :: Int -> Parser Int
+specificCode exp = do
+                   s <- string (show exp) <?> ("Code " ++ (show exp))
+                   return (read s)
+
+line :: Parser String
+line = do
+       x <- many (noneOf "\r\n")
+       crlf
+       -- return $ unsafePerformIO $ putStrLn ("line: " ++ x)
+       return x
+
+----------------------------------------------------------------------
+-- The parsers
+----------------------------------------------------------------------
+
+singleReplyLine :: Parser (Int, String)
+singleReplyLine = do
+                  x <- code
+                  sp
+                  text <- line
+                  return (x, text)
+
+expectedReplyLine :: Int -> Parser (Int, String)
+expectedReplyLine expectedcode = do
+                                 x <- specificCode expectedcode
+                                 sp
+                                 text <- line
+                                 return (x, text)
+
+startOfMultiReply :: Parser (Int, String)
+startOfMultiReply = do
+                    x <- code
+                    char '-'
+                    text <- line
+                    return (x, text)
+
+multiReplyComponent :: Parser [String]
+multiReplyComponent = (try (do
+                            notMatching (do 
+                                         codeString
+                                         sp
+                                        ) "found unexpected code"
+                            thisLine <- line
+                            -- return $ unsafePerformIO (putStrLn ("MRC: got " ++ thisLine))
+                            remainder <- multiReplyComponent
+                            return (thisLine : remainder)
+                           )
+                      ) <|> return []
+
+multiReply :: Parser FTPResult
+multiReply = try (do
+                  x <- singleReplyLine
+                  return (fst x, [snd x])
+                 )
+             <|> (do
+                  start <- startOfMultiReply
+                  component <- multiReplyComponent
+                  end <- expectedReplyLine (fst start)
+                  return (fst start, snd start : (component ++ [snd end]))
+                 )
+
+----------------------------------------------------------------------
+-- The real code
+----------------------------------------------------------------------
+
+-- | Parse a FTP reply.  Returns a (result code, text) pair.
+
+parseReply :: String -> FTPResult
+parseReply input =
+    case parse multiReply "(unknown)" input of
+         Left err -> error ("FTP: " ++ (show err))
+         Right reply -> reply
+
+-- | Parse a FTP reply.  Returns a (result code, text) pair.
+-- If the result code indicates an error, raise an exception instead
+-- of just passing it back.
+
+parseGoodReply :: String -> IO FTPResult
+parseGoodReply input =
+    let reply = parseReply input
+        in
+        if (fst reply) >= 400
+        then fail ("FTP:" ++ (show (fst reply)) ++ ": " ++ (join "\n" (snd reply)))
+        else return reply
+
+-- | Parse a FTP reply.  Logs debug messages.
+debugParseGoodReply :: String -> IO FTPResult
+debugParseGoodReply contents =
+    let logPlugin :: String -> String -> IO String
+        logPlugin [] [] = return []
+        logPlugin [] accum = do
+                             logit accum 
+                             return []
+        logPlugin (x:xs) accum = 
+            case x of
+                   '\n' -> do logit (strip (accum))
+                              next <- unsafeInterleaveIO $ logPlugin xs []
+                              return (x : next)
+                   y -> do
+                        next <- unsafeInterleaveIO $ logPlugin xs (accum ++ [x])
+                        return (x : next)
+        in
+        do
+        loggedStr <- logPlugin contents []
+        parseGoodReply loggedStr
+
+{- | Converts a socket address to a string suitable for a PORT command.
+
+Example:
+
+> toPortString (SockAddrInet (PortNum 0x1234) (0xaabbccdd)) ->
+>                              "170,187,204,221,18,52"
+-}
+toPortString :: SockAddr -> IO String
+toPortString (SockAddrInet port hostaddr) =
+    let wport = (fromIntegral (port))::Word16
+        in do
+           hn <- inet_ntoa hostaddr
+           return ((replace "." "," hn) ++ "," ++ 
+                   (genericJoin "," . getBytes $ wport))
+toPortString _ = 
+    error "toPortString only works on AF_INET addresses"
+
+-- | Converts a port string to a socket address.  This is the inverse calculation of 'toPortString'.
+fromPortString :: String -> IO SockAddr
+fromPortString instr =
+    let inbytes = split "," instr
+        hostname = join "." (take 4 inbytes)
+        portbytes = map read (drop 4 inbytes)
+        in
+        do
+        addr <- inet_addr hostname
+        return $ SockAddrInet (fromInteger $ fromBytes portbytes) addr
+
+respToSockAddrRe = mkRegex("([0-9]+,){5}[0-9]+")
+-- | Converts a response code to a socket address
+respToSockAddr :: FTPResult -> IO SockAddr
+respToSockAddr f =
+    do
+    forceioresp 200 f
+    if (fst f) /= 227 then
+       fail ("Not a 227 response: " ++ show f)
+       else case matchRegexAll respToSockAddrRe (head (snd f)) of
+                  Nothing -> fail ("Could not find remote endpoint in " ++ (show f))
+                  Just (_, x, _, _) -> fromPortString x
+
+    
+parseDirName :: FTPResult -> Maybe String
+parseDirName (257, name:_) =
+    let procq [] = []
+        procq ['"'] = []
+        procq ('"' : '"' : xs) = '"' : procq xs
+        procq (x:xs) = x : procq xs
+        in       
+        if head name /= '"'
+           then Nothing
+           else Just (procq (tail name))
+
diff --git a/src/Network/FTP/Server.hs b/src/Network/FTP/Server.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/FTP/Server.hs
@@ -0,0 +1,652 @@
+{- arch-tag: FTP server support
+Copyright (C) 2004 John Goerzen <jgoerzen@complete.org>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+-}
+
+{- |
+   Module     : Network.FTP.Server
+   Copyright  : Copyright (C) 2004 John Goerzen
+   License    : GNU LGPL, version 2.1 or above
+
+   Maintainer : John Goerzen <jgoerzen@complete.org> 
+   Stability  : experimental
+   Portability: systems with networking
+
+This module provides a server-side interface to the File Transfer Protocol
+as defined by:
+
+ * RFC959, basic protocol
+
+ * RFC1123, clarifications
+
+ * RFC1579, passive mode discussion
+
+Written by John Goerzen, jgoerzen\@complete.org
+
+This is a modular FTP server implementation in pure Haskell.  It is highly
+adaptable to many different tasks, and can serve up not only real files
+and directories, but also virtually any data structure you could represent
+as a filesystem.  It does this by using the
+"System.IO.HVFS" and "System.IO.HVIO" modules.
+
+In addition, basic networking and multitasking configuration is handled
+via "Network.SocketServer" and logging via 
+"System.Log.Logger".
+
+This module is believed to be secure, but it not believed to be robust enough
+for use on a public FTP server.  In particular, it may be vulnerable to denial
+of service attacks due to no timeouts or restrictions on data size, and
+error catching is not yet completely pervasive.  These will be fixed in time.
+Your patches would also be welcomed.
+
+Here is an example server that serves up the entire local filesystem
+in a read-only manner:
+
+>import Network.FTP.Server
+>import Network.SocketServer
+>import System.Log.Logger
+>import System.IO.HVFS
+>import System.IO.HVFS.Combinators
+>
+>main = do
+>       updateGlobalLogger "" (setLevel DEBUG)
+>       updateGlobalLogger "Network.FTP.Server" (setLevel DEBUG)
+>       let opts = (simpleTCPOptions 12345) {reuse = True}
+>       serveTCPforever opts $
+>            threadedHandler $
+>            loggingHandler "" INFO $
+>            handleHandler $
+>            anonFtpHandler (HVFSReadOnly SystemFS)
+
+Hint: if you wantto serve up only part of a filesystem, see
+'System.IO.HVFS.Combinators.newHVFSChroot'.
+-}
+
+module Network.FTP.Server(
+                                   anonFtpHandler
+                                  )
+where
+import Network.FTP.Server.Parser
+import Network.FTP.Client.Parser
+import Network.BSD
+import Network.Socket
+import qualified Network
+import System.IO.Utils
+import System.IO.Error
+import System.Log.Logger
+import Network.Utils
+import Network.SocketServer
+import Data.String
+import System.IO.HVIO
+import System.IO.HVFS
+import System.IO.HVFS.InstanceHelpers
+import System.IO.HVFS.Utils
+import Text.Printf
+import Data.Char
+import Data.IORef
+import Data.List
+import Control.Exception(finally)
+import System.IO
+
+data DataType = ASCII | Binary
+              deriving (Eq, Show)
+data AuthState = NoAuth 
+              | User String
+              | Authenticated String
+                deriving (Eq, Show)
+data DataChan = NoChannel
+              | PassiveMode SocketServer
+              | PortMode SockAddr
+data FTPState = FTPState
+              { auth :: IORef AuthState,
+                datatype :: IORef DataType,
+                rename :: IORef (Maybe String),
+                datachan :: IORef DataChan,
+                local :: SockAddr,
+                remote :: SockAddr}
+
+data FTPServer = forall a. HVFSOpenable a => FTPServer Handle a FTPState
+
+s_crlf = "\r\n"
+logname = "Network.FTP.Server"
+ftpPutStrLn :: FTPServer -> String -> IO ()
+ftpPutStrLn (FTPServer h _ _) text =
+    do hPutStr h (text ++ s_crlf)
+       hFlush h
+
+{- | Send a reply code, handling multi-line text as necessary. -}
+sendReply :: FTPServer -> Int -> String -> IO ()
+sendReply h codei text =
+    let codes = printf "%03d" codei
+        writethis [] = ftpPutStrLn h (codes ++ "  ")
+        writethis [item] = ftpPutStrLn h (codes ++ " " ++ item)
+        writethis (item:xs) = do ftpPutStrLn h (codes ++ "-" ++ item)
+                                 writethis xs
+        in 
+        writethis (map (rstrip) (lines text))
+
+{- | Main FTP handler; pass the result of applying this to one argument to 
+'Network.SocketServer.handleHandler' -}
+
+anonFtpHandler :: forall a. HVFSOpenable a => a -> Handle -> SockAddr -> SockAddr -> IO ()
+anonFtpHandler f h saremote salocal =
+    let serv r = FTPServer h f r
+        in
+        traplogging logname NOTICE "" $
+          do authr <- newIORef (NoAuth)
+             typer <- newIORef ASCII
+             renamer <- newIORef (Nothing::Maybe String)
+             chanr <- newIORef (NoChannel)
+             let s = serv (FTPState {auth = authr, datatype = typer,
+                                    rename = renamer, datachan = chanr,
+                                    local = salocal, remote = saremote})
+             sendReply s 220 "Welcome to Network.FTP.Server."
+             commandLoop s
+
+type CommandHandler = FTPServer -> String -> IO Bool
+data Command = Command String (CommandHandler, (String, String))
+
+instance Eq Command where
+    (Command x _) == (Command y _) = x == y
+instance Ord Command where
+    compare (Command x _) (Command y _) = compare x y
+
+trapIOError :: FTPServer -> IO a -> (a -> IO Bool) -> IO Bool
+trapIOError h testAction remainingAction =
+    do result <- try testAction
+       case result of
+         Left err -> do sendReply h 550 (show err)
+                        return True
+         Right result -> remainingAction result
+
+forceLogin :: CommandHandler -> CommandHandler
+forceLogin func h@(FTPServer _ _ state) args =
+    do state <- readIORef (auth state)
+       case state of 
+          Authenticated _ -> func h args
+          x -> do sendReply h 530 "Command not possible in non-authenticated state."
+                  return True
+
+commands :: [Command]
+commands =
+    [(Command "HELP" (cmd_help,             help_help))
+    ,(Command "QUIT" (cmd_quit,             help_quit))
+    ,(Command "USER" (cmd_user,             help_user))
+    ,(Command "PASS" (cmd_pass,             help_pass))
+    ,(Command "CWD"  (forceLogin cmd_cwd,   help_cwd))
+    ,(Command "CDUP" (forceLogin cmd_cdup,  help_cdup))
+    ,(Command "TYPE" (forceLogin cmd_type,  help_type))
+    ,(Command "NOOP" (forceLogin cmd_noop,  help_noop))
+    ,(Command "RNFR" (forceLogin cmd_rnfr,  help_rnfr))
+    ,(Command "RNTO" (forceLogin cmd_rnto,  help_rnto))
+    ,(Command "DELE" (forceLogin cmd_dele,  help_dele))
+    ,(Command "RMD"  (forceLogin cmd_rmd,   help_rmd))
+    ,(Command "MKD"  (forceLogin cmd_mkd,   help_mkd))
+    ,(Command "PWD"  (forceLogin cmd_pwd,   help_pwd))
+    ,(Command "MODE" (forceLogin cmd_mode,  help_mode))
+    ,(Command "STRU" (forceLogin cmd_stru,  help_stru))
+    ,(Command "PASV" (forceLogin cmd_pasv,  help_pasv))
+    ,(Command "PORT" (forceLogin cmd_port,  help_port))
+    ,(Command "RETR" (forceLogin cmd_retr,  help_retr))
+    ,(Command "STOR" (forceLogin cmd_stor,  help_stor))
+    ,(Command "STAT" (forceLogin cmd_stat,  help_stat))
+    ,(Command "SYST" (forceLogin cmd_syst,  help_syst))
+    ,(Command "NLST" (forceLogin cmd_nlst,  help_nlst))
+    ,(Command "LIST" (forceLogin cmd_list,  help_list))
+    ]
+
+commandLoop :: FTPServer -> IO ()
+commandLoop h@(FTPServer fh _ _) =
+    let errorhandler e = do noticeM logname
+                                    ("Closing due to error: " ++ (show e))
+                            hClose fh
+                            return False
+        in do continue <- (flip catch) errorhandler 
+               (do x <- parseCommand fh
+                   case x of
+                     Left err -> do sendReply h 500 $
+                                      " Couldn't parse command: " ++ (show err)
+                                    return True
+                     Right (cmd, args) -> 
+                         case lookupC cmd commands of
+                            Nothing -> do sendReply h 502 $
+                                           "Unrecognized command " ++ cmd
+                                          return True
+                            Just (Command _ hdlr) -> (fst hdlr) h args
+               )
+              if continue
+                 then commandLoop h
+                 else return ()
+
+lookupC cmd cl = find (\(Command x _) -> x == cmd) cl
+
+help_quit =
+    ("Terminate the session",
+     "")
+
+cmd_quit :: CommandHandler
+cmd_quit h args =
+    do sendReply h 221 "OK, Goodbye."
+       return False
+
+help_user =
+    ("Provide a username",
+     unlines $ 
+     ["USER username will provide the username for authentication."
+     ,"It should be followed by a PASS command to finish the authentication."
+     ])
+
+cmd_user :: CommandHandler
+cmd_user h@(FTPServer _ _ state) passedargs =
+    let args = strip passedargs
+        in
+        case args of
+           "anonymous" -> do sendReply h 331 "User name accepted; send password."
+                             writeIORef (auth state) (User args)
+                             return True
+           _ -> do sendReply h 530 "Unrecognized user name; please try \"anonymous\""
+                   writeIORef (auth state) NoAuth
+                   return True
+
+help_pass =
+    ("Provide a password",
+     "PASS password will provide the password for authentication.")
+cmd_pass :: CommandHandler
+cmd_pass h@(FTPServer _ _ state) passedargs =
+    do curstate <- readIORef (auth state)
+       case curstate of
+         User "anonymous" -> 
+             do sendReply h 230 "Anonymous login successful."
+                writeIORef (auth state) (Authenticated "anonymous")
+                infoM logname "Anonymous authentication successful"
+                return True
+         _ -> do sendReply h 530 "Out of sequence PASS command"
+                 return True
+
+help_cwd =
+    ("Change working directory",
+     unlines $
+     ["Syntax: CWD cwd"
+     ,""
+     ,"Changes the working directory to the specified item"])
+
+cmd_cwd :: CommandHandler
+cmd_cwd h@(FTPServer _ fs _) args =
+    do trapIOError h (vSetCurrentDirectory fs args)
+         $ \_ -> do
+                 newdir <- vGetCurrentDirectory fs
+                 sendReply h 250 $ "New directory now " ++ newdir
+                 return True
+
+help_cdup = 
+    ("Change to parent directory", "Same as CWD ..")
+cmd_cdup h _ = cmd_cwd h ".."
+
+help_type =
+    ("Change the type of data transfer", "Valid args are A, AN, and I")
+cmd_type :: CommandHandler
+cmd_type h@(FTPServer _ _ state) args =
+    let changetype newt =
+            do oldtype <- readIORef (datatype state)
+               writeIORef (datatype state) newt
+               sendReply h 200 $ "Type changed from " ++ show oldtype ++
+                             " to " ++ show newt
+               return True
+        in case args of
+         "I" -> changetype Binary
+         "L 8" -> changetype Binary
+         "A" -> changetype ASCII
+         "AN" -> changetype ASCII
+         "AT" -> changetype ASCII
+         _ -> do sendReply h 504 $ "Type \"" ++ args ++ "\" not supported."
+                 return True
+
+closeconn :: FTPServer -> IO ()
+closeconn h@(FTPServer _ _ state) =
+    do dc <- readIORef (datachan state)
+       writeIORef (datachan state) NoChannel
+
+help_port = ("Initiate a port-mode connection", "")
+cmd_port :: CommandHandler
+cmd_port h@(FTPServer _ _ state) args =
+    let doIt clientsa = 
+            do writeIORef (datachan state) (PortMode clientsa)
+               str <- showSockAddr clientsa
+               sendReply h 200 $ "OK, later I will connect to " ++ str
+               return True
+        in
+        do closeconn h                      -- Close any existing connection
+           trapIOError h (fromPortString args) $  (\clientsa -> 
+               case clientsa of
+                   SockAddrInet _ ha -> 
+                      case (local state) of
+                          SockAddrInet _ ha2 -> if ha /= ha2
+                                                  then do sendReply h 501 "Will only connect to same client as command channel."
+                                                          return True
+                                                  else doIt clientsa
+                          _ -> do sendReply h 501 "Require IPv4 on client"
+                                  return True
+                   _ -> do sendReply h 501 "Require IPv4 in specified address"
+                           return True
+                                                   )
+
+runDataChan :: FTPServer -> (FTPServer -> Socket -> IO ()) -> IO ()
+runDataChan h@(FTPServer _ _ state) func =
+    do chan <- readIORef (datachan state)
+       case chan of
+          NoChannel -> fail "Can't connect when no data channel exists"
+          PassiveMode ss -> do finally (handleOne ss (\sock _ _ -> func h sock))
+                                       (do closeSocketServer ss
+                                           closeconn h
+                                       )
+          PortMode sa -> do proto <- getProtocolNumber "tcp"
+                            s <- socket AF_INET Stream proto
+                            connect s sa
+                            finally (func h s) $ closeconn h
+
+help_pasv = ("Initiate a passive-mode connection", "")
+cmd_pasv :: CommandHandler
+cmd_pasv h@(FTPServer _ _ state) args =
+    do closeconn h                      -- Close any existing connection
+       addr <- case (local state) of 
+                    (SockAddrInet _ ha) -> return ha
+                    _ -> fail "Require IPv4 sockets"
+       let ssopts = InetServerOptions 
+                    { listenQueueSize = 1,
+                      portNumber = aNY_PORT,
+                      interface = addr,
+                      reuse = False,
+                      family = AF_INET,
+                      sockType = Stream,
+                      protoStr = "tcp"
+                    }
+       ss <- setupSocketServer ssopts
+       sa <- getSocketName (sockSS ss)
+       portstring <- toPortString sa
+       sendReply h 227 $ "Entering passive mode (" ++ portstring ++ ")"
+       writeIORef (datachan state) (PassiveMode ss)
+       return True
+                 
+                                        
+       
+help_noop = ("Do nothing", "")
+cmd_noop :: CommandHandler
+cmd_noop h _ =
+    do sendReply h 200 "OK"
+       return True
+
+help_rnfr = ("Specify FROM name for a file rename", "")
+cmd_rnfr :: CommandHandler
+cmd_rnfr h@(FTPServer _ _ state) args = 
+    if length args < 1
+       then do sendReply h 501 "Filename required"
+               return True
+       else do writeIORef (rename state) (Just args)
+               sendReply h 350 "Noted rename from name; please send RNTO."
+               return True
+
+help_stor = ("Upload a file", "")
+cmd_stor :: CommandHandler
+cmd_stor h@(FTPServer _ fs state) args =
+    let datamap :: [String] -> [String]
+        datamap instr =
+            let linemap :: String -> String
+                linemap x = if endswith "\r" x
+                              then take ((length x) - 1) x
+                              else x
+                in map linemap instr
+        runit fhencap _ sock =
+            case fhencap of
+              HVFSOpenEncap fh ->
+                  do readh <- socketToHandle sock ReadMode
+                     mode <- readIORef (datatype state)
+                     case mode of
+                      ASCII -> finally (hLineInteract readh fh datamap)
+                                       (hClose readh)
+                      Binary -> finally (do vSetBuffering fh (BlockBuffering (Just 4096))
+                                            hCopy readh fh
+                                        ) (hClose readh)
+        in
+        if length args < 1
+           then do sendReply h 501 "Filename required"
+                   return True
+           else trapIOError h (vOpen fs args WriteMode) 
+                  (\fhencap ->
+                    trapIOError h (do sendReply h 150 "File OK; about to open data channel"
+                                      runDataChan h (runit fhencap)
+                                  )
+                                  (\_ ->
+                                   do case fhencap of
+                                        HVFSOpenEncap fh -> vClose fh
+                                      sendReply h 226 "Closing data connection; transfer complete."
+                                      return True
+                                   )
+                   )
+
+rtransmitString :: String -> FTPServer  -> Socket -> IO ()
+rtransmitString thestr (FTPServer _ _ state) sock =
+    let fixlines :: [String] -> [String]
+        fixlines x = map (\y -> y ++ "\r") x
+        copyit h =
+            hPutStr h $ unlines . fixlines . lines $ thestr
+        in
+        do writeh <- socketToHandle sock WriteMode
+           hSetBuffering writeh (BlockBuffering (Just 4096))
+           mode <- readIORef (datatype state)
+           case mode of
+              ASCII -> finally (copyit writeh)
+                               (hClose writeh)
+              Binary -> finally (hPutStr writeh thestr)
+                                (hClose writeh)
+
+rtransmitH :: HVFSOpenEncap -> FTPServer -> Socket -> IO ()
+rtransmitH fhencap h sock =
+    case fhencap of
+       HVFSOpenEncap fh ->
+        finally (do c <- vGetContents fh
+                    rtransmitString c h sock
+                ) (vClose fh)
+
+genericTransmit :: FTPServer -> a -> (a -> FTPServer -> Socket -> IO ()) -> IO Bool
+genericTransmit h dat func =
+    trapIOError h 
+      (do sendReply h 150 "I'm going to open the data channel now."
+          runDataChan h (func dat)
+      ) (\_ ->
+               do sendReply h 226 "Closing data connection; transfer complete."
+                  return True
+        )
+
+genericTransmitHandle :: FTPServer -> HVFSOpenEncap -> IO Bool
+genericTransmitHandle h dat =
+    genericTransmit h dat rtransmitH
+
+genericTransmitString :: FTPServer -> String -> IO Bool
+genericTransmitString h dat =
+    genericTransmit h dat rtransmitString
+
+
+help_retr = ("Retrieve a file", "")
+cmd_retr :: CommandHandler
+cmd_retr h@(FTPServer _ fs state) args =
+        if length args < 1
+           then do sendReply h 501 "Filename required"
+                   return True
+           else trapIOError h (vOpen fs args ReadMode) 
+                    (\fhencap -> genericTransmitHandle h fhencap)
+
+help_rnto = ("Specify TO name for a file name", "")
+cmd_rnto :: CommandHandler
+cmd_rnto h@(FTPServer _ fs state) args =
+    if length args < 1
+       then do sendReply h 501 "Filename required"
+               return True
+       else do fr <- readIORef (rename state)
+               case fr of
+                   Nothing -> do sendReply h 503 "RNFR required before RNTO"
+                                 return True
+                   Just fromname -> 
+                       do writeIORef (rename state) Nothing
+                          trapIOError h (vRenameFile fs fromname args)
+                              $ \_ -> do sendReply h 250 
+                                           ("File " ++ fromname ++ 
+                                            " renamed to " ++ args)
+                                         return True
+
+help_dele = ("Delete files", "")
+cmd_dele :: CommandHandler
+cmd_dele h@(FTPServer _ fs _) args =
+    if length args < 1
+       then do sendReply h 501 "Filename required"
+               return True
+       else trapIOError h (vRemoveFile fs args) $
+              \_ -> do sendReply h 250 $ "File " ++ args ++ " deleted."
+                       return True
+
+help_nlst = ("Get plain listing of files", "")
+cmd_nlst :: CommandHandler
+cmd_nlst h@(FTPServer _ fs _) args =
+    let fn = case args of
+                       "" -> "."
+                       x -> x
+        in
+        trapIOError h (vGetDirectoryContents fs fn)
+           (\l -> genericTransmitString h (unlines l))
+
+help_list = ("Get an annotated listing of files", "")
+cmd_list :: CommandHandler
+cmd_list h@(FTPServer _ fs _) args =
+    let fn = case args of
+                       "" -> "."
+                       x -> x
+        in
+        trapIOError h (lsl fs fn)
+                    (\l -> genericTransmitString h l)
+
+help_rmd = ("Remove directory", "")
+cmd_rmd :: CommandHandler
+cmd_rmd h@(FTPServer _ fs _) args =
+    if length args < 1
+       then do sendReply h 501 "Filename required"
+               return True
+       else trapIOError h (vRemoveDirectory fs args) $
+            \_ -> do sendReply h 250 $ "Directory " ++ args ++ " removed."
+                     return True
+
+help_mkd = ("Make directory", "")
+cmd_mkd :: CommandHandler
+cmd_mkd h@(FTPServer _ fs _) args =
+    if length args < 1
+       then do sendReply h 501 "Filename required"
+               return True
+       else trapIOError h (vCreateDirectory fs args) $
+            \_ -> do newname <- getFullPath fs args
+                     sendReply h 257 $ "\"" ++ newname ++ "\" created."
+                     return True
+
+help_pwd = ("Print working directory", "")
+cmd_pwd :: CommandHandler
+cmd_pwd h@(FTPServer _ fs _) _ =
+    do d <- vGetCurrentDirectory fs
+       sendReply h 257 $ "\"" ++ d ++ "\" is the current working directory."
+       return True
+
+help_mode = ("Provided for compatibility only", "")
+cmd_mode :: CommandHandler
+cmd_mode h args =
+    case args of
+        "S" -> do sendReply h 200 "Mode is Stream."
+                  return True
+        x -> do sendReply h 504 $ "Mode \"" ++ x ++ "\" not supported."
+                return True
+
+help_stru = ("Provided for compatibility only", "")
+cmd_stru :: CommandHandler
+cmd_stru h args =
+    case args of
+        "F" -> do sendReply h 200 "Structure is File."
+                  return True
+        x -> do sendReply h 504 $ "Structure \"" ++ x ++ "\" not supported."
+                return True
+
+help_syst = ("Display system type", "")
+cmd_syst :: CommandHandler
+cmd_syst h _ =
+    -- I have no idea what this L8 means, but everyone else seems to do
+    -- this, so I do too..
+    do sendReply h 215 "UNIX Type: L8"
+       return True
+
+help_stat = ("Display sever statistics", "")
+cmd_stat :: CommandHandler
+cmd_stat h@(FTPServer _ _ state) _ =
+    do loc <- showSockAddr (local state)
+       rem <- showSockAddr (remote state)
+       auth <- readIORef (auth state)
+       datm <- readIORef (datatype state)
+       sendReply h 211 $ unlines $
+         [" *** Sever statistics and information"
+         ," *** Please type HELP for more details"
+         ,""
+         ,"Server Software     : MissingH, http://quux.org/devel/missingh"
+         ,"Connected From      : " ++ rem
+         ,"Connected To        : " ++ loc
+         ,"Data Transfer Type  : " ++ (show datm)
+         ,"Auth Status         : " ++ (show auth)
+         ,"End of status."]
+       return True
+          
+
+help_help =
+    ("Display help on available commands",
+     "When called without arguments, shows a summary of available system\n"
+     ++ "commands.  When called with an argument, shows detailed information\n"
+     ++ "on that specific command.")
+
+cmd_help :: CommandHandler
+cmd_help h@(FTPServer _ _ state) args =
+    let genericreply addr = unlines $
+          [" --- General Help Response ---"
+          ,""
+          ,"Welcome to the FTP server, " ++ addr ++ "."
+          ,"This server is implemented as the Network.FTP.Server"
+          ,"component of the MissingH library.  The MissingH library"
+          ,"is available from http://quux.org/devel/missingh."
+          ,""
+          ,""
+          ,"I know of the following commands:"
+          ,concatMap (\ (Command name (_, (summary, _))) -> printf "%-10s %s\n" name summary)
+              (sort commands)
+          ,""
+          ,"You may type \"HELP command\" for more help on a specific command."
+          ]
+        in
+        if args == ""
+           then do sastr <- showSockAddr (remote state)
+                   sendReply h 214 (genericreply sastr)
+                   return True
+           else let newargs = map toUpper args
+                    in case lookupC newargs commands of
+                         Nothing -> do 
+                                    sendReply h 214 $ "No help for \"" ++ newargs
+                                      ++ "\" is available.\nPlese send HELP"
+                                      ++ " without arguments for a list of\n"
+                                      ++ "valid commands."
+                                    return True
+                         Just (Command _ (_, (summary, detail))) ->
+                             do sendReply h 214 $ newargs ++ ": " ++ summary ++ 
+                                               "\n\n" ++ detail
+                                return True
diff --git a/src/Network/FTP/Server/Parser.hs b/src/Network/FTP/Server/Parser.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/FTP/Server/Parser.hs
@@ -0,0 +1,84 @@
+{- arch-tag: FTP protocol parser for servers
+Copyright (C) 2004 John Goerzen <jgoerzen@complete.org>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+-}
+
+{- |
+   Module     : Network.FTP.Server.Parser
+   Copyright  : Copyright (C) 2004 John Goerzen
+   License    : GNU LGPL, version 2.1 or above
+
+   Maintainer : John Goerzen <jgoerzen@complete.org> 
+   Stability  : provisional
+   Portability: systems with networking
+
+This module provides a parser that is used internally by
+"Network.FTP.Server".  You almost certainly do not want to use
+this module directly.  Use "Network.FTP.Server" instead.
+
+Written by John Goerzen, jgoerzen\@complete.org
+
+-}
+
+module Network.FTP.Server.Parser(
+                                         parseCommand
+                                        )
+where
+import Network.FTP.Client.Parser
+import Text.ParserCombinators.Parsec
+import Text.ParserCombinators.Parsec.Utils
+import Data.List.Utils
+import Data.Bits.Utils
+import Data.String
+import System.Log.Logger
+import Network.Socket(SockAddr(..), PortNumber(..), inet_addr, inet_ntoa)
+import System.IO(Handle, hGetContents)
+import System.IO(hGetLine)
+import Text.Regex
+import Data.Word
+import Data.Char
+
+logit :: String -> IO ()
+logit m = debugM "Network.FTP.Server.Parser" ("FTP received: " ++ m)
+
+----------------------------------------------------------------------
+-- Utilities
+----------------------------------------------------------------------
+
+alpha = oneOf (['A'..'Z'] ++ ['a'..'z']) <?> "alphabetic character"
+
+word = many1 alpha
+
+args :: Parser String
+args = try (do char ' '
+               r <- many anyChar
+               eof 
+               return r)
+       <|> return ""
+       
+
+command :: Parser (String, String)
+command = do
+          x <- word
+          y <- args
+          eof
+          return (map toUpper x, y)
+
+
+parseCommand :: Handle -> IO (Either ParseError (String, String))
+parseCommand h =
+    do input <- hGetLine h
+       return $ parse command "(unknown)" (rstrip input)
diff --git a/testsrc/runtests.hs b/testsrc/runtests.hs
new file mode 100644
--- /dev/null
+++ b/testsrc/runtests.hs
@@ -0,0 +1,25 @@
+{- arch-tag: Test runner
+Copyright (C) 2004 John Goerzen <jgoerzen@complete.org>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+module Main where 
+
+import Test.HUnit
+import Tests
+
+main = runTestTT tests
+
