ProFTPD: Using mod_rewrite

## ProFTPD: Using the `mod_rewrite` Module

The `mod_rewrite` module for `proftpd` is a powerful tool for rewriting FTP commands received from clients. It has been used to automatically append (or remove) domain names from logins, to translate Windows paths (using backslashes) to Unix paths (using slashes), to handle case-insensitive files, etc. One of the great things about `mod_rewrite` is that any modification made to the commands is transparent to the client; that is, FTP clients are completely unaware that their commands are being changed on-the-fly.

The following is a collection of examples of how `mod_rewrite` has been used. If you use `mod_rewrite` and would like to contribute your recipe/configuration, please let us know!

Since much of `mod_rewrite`'s power is based on regular expressions and pattern matching, I highly recommend that you read through this introduction to POSIX regular expressions, and use the `regex` tool for testing out your regexes against paths/strings:

      http://www.castaglia.org/proftpd/doc/contrib/regexp.html

Case Sensitivity
The following example configuration shows how to configure `mod_rewrite` so that all files uploaded to the FTP server will have all-uppercase filenames:

      <IfModule mod_rewrite.c>
        RewriteEngine on

        # Have a log for double-checking any errors
        RewriteLog /var/log/ftpd/rewrite.log

        # Define a map that uses the internal "toupper" function
        RewriteMap uppercase int:toupper

        # Make the file names used by STOR be in all uppercase
        RewriteCondition %m STOR

        # Apply the map to the command parameters
        RewriteRule ^(.*) ${uppercase:$1}
      </IfModule>

What if you wanted to make the filename always be uppercase for uploaded files, but not any directories in the path leading up the file name? Using the above, if you did:

      ftp> cd /upload
      ftp> put file1.txt

The file would appear as "/upload/FILE1.TXT". But if you did:

      ftp> put /upload/file1.txt

the file would appear as "/UPLOAD/FILE1.TXT", which may not be what you want. To handle this, you need to change the "^(.*)" pattern in the above `RewriteRule` directive. The "^(.*)" regular expression matches the entire parameter string. Instead, you might try this pattern:

      RewriteRule (.*/)?(.*)$ ${uppercase:$2}

which tries to isolate into match group 2 (i.e. `$2`) the part of the argument string which is not followed by any slashes.

Somewhat similar is the situation where the admin found, for case-sensitivity reasons, that it was easier to rewrite all FTP commands (except `PASS`, since passwords are case-sensitive) to be lowercase:

      <IfModule mod_rewrite.c>
        RewriteEngine on

        # Define a map that uses the internal "tolower" function
        RewriteMap lowercase int:tolower

        # Rewrite all commands except PASS
        RewriteCondition %m !PASS

        RewriteRule ^(.*) ${lowercase:$1}
      </IfModule>

This means an FTP client can refer to "/DiR/Dir2/FiLe" when on the server the file is actually "/dir/dir2/file"; it works for uploads, too. (This works especially well for Windows clients.)

Trimming Whitespace
Some FTP clients can properly handle files whose names start with spaces, but other FTP clients cannot. If your users use whatever FTP clients they wish, you need may need to deal with this situation on the server side of things. The `mod_rewrite` module can help with this, by automatically trimming leading/trailing whitespace from commands.

      <IfModule mod_rewrite.c>
        RewriteEngine on

        # Trim whitespace for commands dealing with files
        RewriteCondition %m LIST|MLST|NLST|RETR|STAT|STOR

        # Only the portion of the command that matches our rule is used.
        # This works by saying "match all non-space characters".
        RewriteRule [^[:space:]]+ $0
      </IfModule>

Changing the Filenames
One user had the following problem: Files uploaded via a web browser had their filenames changed by the browser. Specifically, the web browser changed any spaces in the filenames to "%20" (URL encoding for a space character). Fortunately, the user was able to use `mod_rewrite` to undo the change or, as shown below, to change that "%20" to an underscore:

      <IfModule mod_rewrite.c>
        RewriteEngine on

        # Define a map that uses the internal "replaceall" function
        RewriteMap replace int:replaceall

        # We only want to use this rule on STOR commands
        RewriteCondition %m STOR

        # Apply the map to the command parameters.  Use '!' as the delimiter,
        # not '/', as the path sent might contain slashes
        RewriteRule ^(.*) "${replace:!$1!%20!_}"
      </IfModule>

Another site wanted to "tag" each uploaded file name with the current process ID (PID), to ensure some sort of file name uniqueness. Enter `mod_rewrite`!

      <IfModule mod_rewrite.c>
        RewriteEngine on

        RewriteCondition %m STOR
        RewriteRule (.*) $1.%P
      </IfModule>

This appends the PID of the current session process to any uploaded filename. For more variables like `%P`, see the `RewriteCondition` and `RewriteRule` descriptions.

Replacing Backslashes With Slashes
Some sites have FTP clients which seem to send `CWD` and `RETR`/`STOR` commands which use Windows-style backslashes, e.g. "path\to\file". And ideally, these sites would like to work seamlessly with such clients, without having to get the clients to change. Can `mod_rewrite` be used to change those backslashes into more Unix-friendly regular slashes? Absolutely. The following `mod_rewrite` configuration should do the trick:

      <IfModule mod_rewrite.c>
        RewriteEngine on

        # Use the replaceall internal RewriteMap
        RewriteMap replace int:replaceall

        # Every RewriteRule needs a condition.  Here, we want to apply our
        # rule to every command.
        RewriteCondition %m .*
        RewriteRule (.*) "${replace:!$1!\\\\!/}"
      </IfModule>

Yes, you will need the four consecutive backslashes there, in order to make it past ProFTPD's config file parser (which thinks backslashes are escape sequences) as well as the regular expression compiler.

Modifying User Names
Is there a way that I can transparently change the login name that the FTP client sends, from one set of known login names to the new set of names that should be used by the FTP server? But of course! For this example, let us assume that you have a text file which maps the old login names to the new login names. Using `mod_rewrite`'s `RewriteMap` directive and that text file, this becomes simple:

      <IfModule mod_rewrite.c>
        RewriteEngine on

        # Tell mod_rewrite where to find the "usermap" text file
        RewriteMap usermap txt:/path/to/usermap.txt

        # For USER commands, use the "usermap" file to translate the login names
        RewriteCondition %m USER
        RewriteRule (.*) ${usermap:$1}
      </IfModule>

Rather than having a fixed map of old-to-new login names, what if you wanted to always append the same prefix (or suffix) to every login name? For example, what if you wanted every login name on your FTP server to look like "user@domain.com", but the clients were sending simply "user". This solution does not need `RewriteMap`; instead, you simply use:

      <IfModule mod_rewrite.c>
        RewriteEngine on

        RewriteCondition %m USER
        RewriteRule (.*) $1@domain.com
      </IfModule>

And if instead you wanted to use a fixed prefix, rather than a suffix, the only difference would be in the `RewriteRule` directive, e.g.:

      <IfModule mod_rewrite.c>
        RewriteEngine on

        RewriteCondition %m USER
        RewriteRule (.*) PREFIX$1
      </IfModule>

Another interesting use case is where your clients might send the login name in a variety of constructions, e.g.:

- user
- user@domain.com
- prefix#user@domain.com

but you want the FTP server only to use the "user" parts. How can you configure `mod_rewrite` to strip off any potential prefix and suffix? Regular expressions can be tricky, but using the `regex` tool mentioned above, I worked out the following configuration that does the trick:

      <IfModule mod_rewrite.c>
        RewriteEngine on

        RewriteCondition %m USER
        RewriteRule ^(.*#)?([0-9A-Za-z]+)(@)? $2
      </IfModule>

And if you simply wanted to have all user names be in lowercase, despite what the FTP clients send, it's merely a matter of:

      <IfModule mod_rewrite.c>
        RewriteEngine on

        RewriteMap lowercase int:tolower
        RewriteCondition %m USER
        RewriteRule (.*) ${lowercase:$1}
      </IfModule>

Handling Clients' Bad PORT Commands
A ProFTPD admin encountered a case where one of their customers refused to use anything but the standard command-line FTP client that comes with Windows. That FTP client does not support passive data transfers; it always uses the `PORT` command to do active data transfers. However, one issue with the `PORT` command is that the parameter contains an IP address. In this situation, the FTP client was behind a NAT, and the client was sending the internal LAN address in its `PORT` command. Could `mod_rewrite` be used to solve the problem, and allow that bad FTP client to use active data transfers despite its' sending of an unusable (to the FTP server) IP address? Yes!

The solution was to use `mod_rewrite` to rewrite the address in the sent `PORT` command, replacing the internal LAN address with the IP address of the client that `proftpd` saw. Below is the configuration used to make this work:

      # This is necessary, to keep proftpd from complaining about mismatched
      # addresses in this situation
      AllowForeignAddress on

      <IfModule mod_rewrite.c>
        RewriteEngine on

        RewriteMap replace int:replaceall

        # Substitute in the IP address of the client, regardless of the address
        # the client tells us to use in the PORT command
        RewriteCondition %m ^PORT$
        RewriteRule ([0-9]+,[0-9]+,[0-9]+,[0-9]+)(.*) ${replace:/$1/$1/%a$2}

        # Replace the periods in the client address with commas, as per RFC959
        # requirements
        RewriteCondition %m ^PORT$
        RewriteRule (.*) ${replace:/$1/./,}
      </IfModule>

`SITE` Commands
The `mod_rewrite` module can also handle some `SITE` commands, specifically:

- `SITE CHGRP`
- `SITE CHMOD`

These being supported by the `mod_site` module, which is part of the normal `proftpd` build.

One site needed to make sure that any backslashes (e.g. used by Windows clients) were translated to slashes, including in these `SITE` commands. As of ProFTPD 1.3.2 (see Bug #2915), this can be accomplished using the following:

      <IfModule mod_rewrite.c>
        RewriteEngine on
        RewriteMap replace int:replaceall
        RewriteCondition %m "^SITE CHMOD$" [NC]
        RewriteRule "^(.*) +(.*)$" "$1 ${replace:!$2!\\\\!/}"'
      </IfModule>

Notice how, for `SITE CHGRP` and `SITE CHMOD` commands, the `%m` parameter in the `RewriteCondition` must match the string "SITE CHGRP" or "SITE CHMOD", not just "SITE". This is important -- and it only works for the `SITE CHGRP`/`SITE CHMOD` commands. The use of the "[NC]" modifier helps to catch those cases where the client might send "SITE chmod", for instance.

Redirecting FTP Requests
One user wanted to know if `mod_rewrite` could be used to redirect a request, just like one might do using Apache's `mod_rewrite`, something like:

      RewriteRule /(.*) ftp://newname.domain.com/$1

The above `RewriteRule` would work, but it would not actually redirect the FTP client to the URL. FTP unfortuntely does not support redirection of requests to other servers, at the protocol level, unlike HTTP.

However, it is possible to redirect a request to some other directory on the same machine. For example, if you wanted to have any file uploaded by a client go into the "/Incoming/" directory, no matter where the client wanted to upload the file, you could use:

      <IfModule mod_rewrite.c>
        RewriteEngine on
        RewriteCondition %m STOR
        RewriteRule (.*/)?(.*) /Incoming/$2
      </IfModule>

URL Encoded Characters
On very rare occasions, you may find yourself dealing with URL-encoded characters in your FTP command parameters. If you have worked with web servers and URLs, you will accustomed to seeing sequences like "%20" in URLs; these are URL encoded characters (as per RFC2369). Unescaping these URL-encoded sequences is exactly what the "unescape" `RewriteMap` builtin function handles.

Handling Non-ASCII Characters
If you need to handle non-ASCII characters in your `mod_rewrite` rules, then you may need to generate your configuration using a scripting language, rather than using your editor. For example, my editor does not handle non-ASCII characters well; it displays them as `?`.

Here's an example, using Perl, to replace "ä" with "ae" in uploaded file names. Note that "ä" in hex notation is `0xE4`:

      my $rewrite_rule = 'RewriteRule (.*) ${replace:/$1/' . chr(0xE4) . '/ae}';

      my $config = '/path/to/proftpd.conf';
      if (open(my $fh, "> $config")) {
        print $fh EOR;

      <IfModule mod_rewrite.c>
        RewriteEngine on
        RewriteLog /path/to/rewrite.log

        RewriteCondition %m ^STOR$
        $rewrite_rule
      </IfModule>
    EOR
      }

Time-Related Content
What if you find yourself wanting to serve different files based on the time of day, day of the month, etc? Or what if you wanted to put an automatic timestamp on the names of files being uploaded? Starting with `proftpd-1.3.5rc1`, these things are now possible using `mod_rewrite`; see the time-related variables in the `RewriteCondition` documentation.

To demonstrate the concept of time-related content, let's assume that you have a two different files, one for "daytime" and one for "nighttime". Depending on when a client connects and requests this file, you can have `mod_rewrite` transparently point the client to the correct file. Let's show how this might work:

      <IfModule mod_rewrite.c>
        RewriteEngine on
        RewriteLog /path/to/rewrite.log

        # For requests of index.txt during the day, rewrite the command to
        # be for index.txt.day
        RewriteCondition %m RETR
        RewriteCondition %f index.txt$
        RewriteCondition %{TIME_HOUR}%{TIME_MIN} >0700
        RewriteCondition %{TIME_HOUR}%{TIME_MIN} <1900
        RewriteRule ^(.*) $1.day

        # For requests of index.txt during the night, rewrite the command to
        # be for index.txt.night
        RewriteCondition %m RETR
        RewriteCondition %f index.txt$
        RewriteCondition %{TIME_HOUR}%{TIME_MIN} <0700
        RewriteCondition %{TIME_HOUR}%{TIME_MIN} >1900
        RewriteRule ^(.*) $1.night

      </IfModule>

Another use case involving time is the case where you might want to automatically timestamp every file being uploaded. To do this, you can use `mod_rewrite` like so:

      <IfModule mod_rewrite.c>
        RewriteEngine on
        RewriteLog /path/to/rewrite.log

        # Automatically timestamp all uploaded files with ".DD-MM-YYYY".
        RewriteCondition %m STOR
        RewriteRule (.*) $1.%{TIME_DAY}-%{TIME_MON}-%{TIME_YEAR}
      </IfModule>

Or maybe you have a special file that should only be available for a month, and then be inaccessible? To do this, you would first give the special file a name that includes a timestamp, e.g. "special.bin-01-2013". Then have the following `mod_rewrite` configuration:

      <IfModule mod_rewrite.c>
        RewriteEngine on
        RewriteLog /path/to/rewrite.log

        RewriteCondition %m RETR
        RewriteCondition %f special.bin$
        RewriteRule (.*) $1-%{TIME_MON}-%{TIME_YEAR}
      </IfModule>

Now, if the "special.bin" file is requested during January 2013, the RETR request will be rewritten and will match the name of the file on disk; the file is accessible, and the download succeeds. If the same file is requested any other time than during January 2013, then the `mod_rewrite`-rewritten path will not match the name of the file on disk, and the download will fail.

© Copyright 2020-2023 The ProFTPD Project
All Rights Reserved