User-based Website Directories with Nginx

Many webservers allow users on the machine to create personal websites, usually with urls starting with /~username, where the web files are contained in a directory like /home/username/public_html/. With Apache and Lighttpd, this is done with the module mod_userdir, but there does not appear to be an equivalent for Nginx. I developed a method for doing so, using only Nginx’s standard configuration directives.

One website recommended using Nginx’s rewrite rules to emulate user-based website directories. This worked fairly well, except for one problem. Nginx has this internal behavior of fixing URL requests for directories so that they have a trailing slash. But when a rewrite rule was applied first, the fixed URL, presented to the web browser, was the rewritten location (with the trailing slash).

The rewrite rule looked like:

/~[username]/[file] => /home/[username]/[file]

Then I would make a request for:

http://www.sbf5.com/~cduan/subdir

and Nginx would rewrite the URL, add the slash, and redirect my browser to:

http://www.sbf5.com/home/cduan/subdir/

which, of course, caused an error.

My solution to this problem was to first parse the URL to determine the username, requested file, and presence or absence of trailing slashes. Then, I manually redirect requests for directories that lack trailing slashes, thus circumventing any attempt by Nginx to (mis)redirect the requests on its own.

I developed one “trick” to get around Nginx’s lack of support for nested conditionals. Whenever I came to a point where I would normally have used a nested conditional, I used a rewrite to add a special prefix to the front of the URL, and then created a special location block, using that special prefix, where execution would continue (and thus where I could put a second conditional). Nginx always puts a slash in front of URLs it creates, so all of my special prefixes contained no leading slash, ensuring that a user could not accidentally stumble on them.

The code is as follows:

# For requests starting with a tilde, break them into three components:
# 1. The username, everything after the tilde up to the first slash
# 2. The file location, everything after the username up to the last slash
# 3. The trailing slash(es)
# Then, rewrite to go to the f~/ branch.
location /~ {
    if ($request_uri ~ ^/~([^/]*)(/.*[^/]|)(/*)$) {
        set $homedir $1;
        set $filedir $2;
        set $trailingslashes $3;
        rewrite ^/~([^/]*)(/|$)(.*)$ f~/$3;
    }
}

# Here, the user-directory components have been parsed. Use an alias to set
# the file directory prefix. But if the file at the requested URI is a
# directory, we jump to the ~/ branch for additional processing.
location f~/ {
    alias /home/$homedir/public_html/;
    if (-d /home/$homedir/public_html$filedir) {
        rewrite ^f~/(.*) ~/$1;
    }
}

# Here, the request is for a directory in a user's home directory. We check
# that the request URI contained trailing slashes. If it did not, then we
# add the slashes and send a redirect. This circumvents Nginx's faulty
# internal slash-adding mechanism.
location ~/ {
    autoindex on;
    alias /home/$homedir/public_html/;
    if ($trailingslashes = "") {
        rewrite .* /~$homedir$filedir/ redirect;
    }
}

Improvements and suggestions are greatly appreciated.

UPDATE: If you want PHP support, follow the instructions in this post.

Comments 8

  • Another test comment

  • Thnx for a good post, sofar so good, but an additional question.
    I’ve got PHP setup and working, how do I modify your setup to be able to use PHP as well?
    –Robert

  • […] up on my previous post on user web directories and nginx, here is an improved solution that executes PHP scripts in user home directories as […]

  • Thanks for this, works perfectly.

  • this no longer works on the new 0.7.x stable series.

    the “alias” directive must use captures inside location given by regular
    expression in /etc/nginx/sites-enabled/default:70

    The line in question.

    alias /home/$homedir/public_html/;

    after writing to the email list, I was given this code as a replacement for your three blocks. Admittedly I haven’t tested it yet though.

    location ~ ^/~([^/]+)(/?.*)$) {
    alias /home/$1/public_html/$2;
    autoindex on;
    }

  • location ~ ^/~([^/]+)(/?.*)$ {
    alias /home/$1/public_html/$2;
    autoindex on;
    }
    Works on nginx-0.8.17.

    Be aware, though, that a users’ home directory is not neccessarily /home/$userid . It could be anything. You’d have to parse /etc/passwd to find out. Granted, this is a minor issue ‘in the wild’.
    Furthermore, nginx will need read access to the directories above ~/public_html, which is an unneccesary security risk. It’s better to use directories like /var/www/userhtml/$userid and (optionally) symlink these dirs into the actual homedirs. That way nginx won’t ever be able to read a users homedir, even if directory traversal vulnerabilities surface.

  • […] upgraded to the newest version of Nginx, and discovered that the technique I described in my previous two posts no longer works. The good news is that it no longer works because some of the bugs in […]