Per-directory configuration (.htaccess) in LightTPD

Submitted by Hannes Schmidt on Thu, 03/02/2006 - 10:56.

The frequent visitor of Diary Products knows that it runs on the LightTPD aka Lighty web server. The machine that hosts Diary Products is serving other sites as well so it needs to have some kind of virtual hosting mechanism in place. I use LightTPD's very straight-forward and easy to use mod_simple_vhost module. The only draw-back with LightTPD is that it doesn't support directory specific configuration files similar to Apache's .htaccess files. But this is not such a big deal for me because as much as I liked the convenience of .htaccess, I always considered it a waste of cycles and a security issue. The ideal solution in my opinion would be one which

[03/29/6: Fixed bugs in Ruby script (no variable substitutions in per-directory conf, global conf skipped if no per-directory conf existed)]

  • prevents me from having to modify the global /etc/lighttpd.conf file every time I add a virtual host,
  • let's you put a LightTPD configuration file into a directory and somehow have its directives only apply to that particular directory and sub-directories therein
  • is read only once at the start-up of the LightTPD daemon.

I've come very close to that ideal using mod_simple_vhost and LightHTP's include_shell inclusion mechanism. Just a quick reminder on how these work: The mod_simple_vhost module maps virtual hosts to directories based on the assumption that every virtual host's directory is named after the domain name of that virtual host. If a virtual hosts has additional domain names (aliases), there should be one symbolic link per alias pointing to the main domain's directory. The include_shell statement launches a custom program, most likely a shell script of some sort and parses that program's output as if it were a configuration file.

I wrote a Ruby script that

  • scans the virtual hosts' directories for files called lighttpd.conf,
  • reads those file and
  • prints them to standard output from where they are parsed by LightTPD.

In order to make sure that the directives in each of these configuration file only apply to one particular virtual host, the script wraps the content of each file in a conditional. The script also supports a global configuration template which applies to all virtual hosts and in which the %dir% string is replaced by the name of the current directory, i.e. virtual host. If you would like to use my solution with your LightTPD installation, here's what you would need to do:

  1. Enable the simple virtual hosting module by uncommenting the in corresponding like in your lighttpd.conf:
    server.modules = (
        "mod_simple_vhost",
    
  2. Configure the simple virtual hosting module based on you needs. Note that the actual content directory is not the virtual host's directory itself but the htdocs subdirectory thereof. This enables us to later add other virtual host-specific directories like cgi-bin, a log directory and the like.
    simple-vhost.server-root   = "/home/virtual/"
    simple-vhost.default-host  = "doodah.com"
    simple-vhost.document-root = "/htdocs/"
    
  3. Create the /home/virtual/your-domain.com/htdocs directories:
    mkdir -p /home/virtual/your-domain.com/htdocs
  4. Place the following ruby code into /home/virtual/vhost-lighttpd.conf.rb:
    #! /usr/bin/ruby -w
    
    confName = "lighttpd.conf"
    path = File.expand_path( File.dirname( $0 ) )
    globalConfName = path + "/" + confName
    
    
    def readConf( conf, context )
        if File.file?( conf )
            File.open( conf, "r" ) do | conf |
                while line = conf.gets
                    puts "    " + line.gsub( /%(\w+)%/ ) {
                        if eval( "local_variables", context ).include?( $1 )
                            eval( $1, context )
                        end
                    }
                end
            end
        end
    end
    
    
    Dir.foreach( path ) do | host |
        if host != "." and host != ".." 
            dir = path + "/" + host
            if File.directory?( dir )
                puts "$HTTP[\"host\"] == \"#{host}\" {"
                readConf globalConfName, binding
                readConf dir + "/" + confName, binding
                puts "}"
            end
        end
    end
  5. Make that script executable:
    chmod ug+x /home/virtual/vhost-lighttpd.conf.rb
    chgrp lighttpd /home/virtual/vhost-lighttpd.conf.rb
  6. Add the following line to /etc/lighttpd.conf:
    include_shell "/home/virtual/vhost-lighttpd.conf.rb"
  7. Create host-specific configuration files:
    # vi /home/virtual/your-domain.com/lighttpd.conf
  8. Adjust the permissions of that host-specific file. Note that the necessary specifications are highly dependent on your particular setup, so don't follow this step blindly. Use your brains. If you have separate user accounts for your virtual hosts or the lighttpd daemon, make sure neither the virtual host users nor the lighttpd account can write the configuration file or the directory that it's in:
      # chown root:lighttpd /home/virtual/your-domain.com 
    /home/virtual/your-domain.com/lighttpd.conf
      # chmod go-w /home/virtual/your-domain.com 
    /home/virtual/your-domain.com/lighttpd.conf
  9. Optionally, create a template configuration file:
    # vi /home/virtual/lighttpd.conf

    Within that file, you may use the string %dir%. It will be substituted by the actual virtual host directory. For example,

    accesslog.filename = "%dir%/logs/access.log"
    alias.url = ( "/cgi-bin/" => "%dir%/cgi-bin/" )

    will become

    accesslog.filename = "/home/virtual/your-domain.com/logs/access.log"
    alias.url = ( "/cgi-bin/" => "/home/virtual/your-domain.com/cgi-bin/" )
    

    Besides %dir%, you may also the names of other local variables between percent signs, e.g. %host% (the name of the virtual host), %conf% (the name of the host-specific configuration file) and %path% (the virtual host root, same as simple-vhost.server-root) .

  10. Perform a dry run of the script by running it in your shell and closely examine its output.
  11. # /home/virtual/vhost-lighttpd.conf.rb
  12. Restart LightTPD Depends on your distribution; on Gentoo do this:

    /etc/init.d/lighttpd restart

I chose to write the script in Ruby because Ruby is very popular among LightTPD users as it is an excellent server for application based on the Rails framework which is written in Ruby. I also am generally interested in that language and wanted to get to know it a little better, "als erste Dehnübung, sozusagen." If you don't have Ruby installed, the script could easily be recoded in Perl. I might even do it for you if you ask nicely. Hint, hint ...

( categories: LightTPD | Administrator )
Submitted by Anonymous on Thu, 02/18/2010 - 07:59.

thanks for this page, it helped me alot.

but as ruby is often not installed on servers by default and perl is some kind of dinosaur nowadays (and often not installed, neither), i translated the script to python.

probably it could be optimized in some ways (maybe context updated instead of recreated for each vserver or replacing %dir|path|host% by simple string-replace instead of regexps) but as it runs just once when the server is started i don't see an issue here. i didn't implement the replacement for %conf%.

SECURITY ISSUE: the per-vhost conf files still should not be user-writable (as others mentioned already)!

#!/usr/bin/env python
import os, sys, re

reg = re.compile( '%(\w+)%' )

confName = 'lighttpd.conf'
path = os.path.realpath( os.path.dirname( sys.argv[0] ) )
globalConfName = os.path.join( path, confName )

def readConf( conf, context ):
        if os.path.isfile( conf ):
                fp = open( conf, 'r' )
                for line in fp.readlines():
                        line = reg.sub( lambda X: context.get(X.group(1), '%%%s%%'%X.group(1)), line )
                print '    %s' % line
                fp.close()

for host in os.listdir( path ):
        if host[0] == '.':
                continue
        dir = os.path.join( path, host )
        if os.path.isdir( dir ):
                print '$HTTP["host"] == "%s" {' % host
                context = { 'dir':dir, 'path':path, 'host':host }
                readConf( globalConfName, context )
                readConf( os.path.join(dir, confName), context )
                print '}'

Submitted by Anonymous on Fri, 11/30/2007 - 05:13.

I wanted to have per-host config options in the database, using mysql-vhost, so I did this (see per-vhost stuff I wrote at bottom): MySQL-based vhosting

Submitted by Hannes Schmidt on Thu, 10/12/2006 - 19:01.

Yes, but that is a problem not specific to this solution but to lighty in general. If you combine execwrap and this solution you get pretty much what you are looking for, except safely user-modifiable .htaccess files. For those you could simply patch the lighty sources and disable the global directive. Easy.

-- Hannes

Submitted by Anonymous on Thu, 10/12/2006 - 18:08.

Ok, I was looking for user-modifiable .htaccess solution for lighty and I somehow didn't notice something else was described here :-) Thanks for clearing this up.
But the problem of scripts' privileges still exists. Even if the server is running as lighttpd user, every user's scripts are ran as lighttpd user also. Lighttpd needs to have read access to all the script files. So basically every user's scripts have exactly the same permissions, ie. they can read each other's script files. So if I have config.php with my passwords, every user can read that by putting something like file_get_contents( "/path/to/my/config.php" ) in their php script. With rails there's a separate config directory that can be read.
If you're in a trusted environment (users know each other and have no reason to get their hands on others' databases) everything is cool, but if you have some semi-trusted or untrusted users things get complicated. This is why stuff like suexec and execwrap exists :/

cheers
--
robzon

Submitted by Hannes Schmidt on Thu, 10/12/2006 - 11:28.

I think that there is a misunderstanding here. This was never intended to be used in a scenario where users have write access to their respective lighttpd.conf. Furthermore, the only script that is executed is vhost-lighttpd.conf.rb. And AFAIK it is run as the lighttpd user. So it is even possible to disallow read access to lighttpd.conf if you're worried about confidential info in that file. There shouldn't be any, though. Database credentials shouldn't be in that file anyways.

But you are right, if you wanted to use this in a scenario where users have write access to their local lighttpd.conf you would have to lock it down somehow.

-- Hannes

Submitted by Anonymous on Thu, 10/12/2006 - 07:07.

There are 2 security flaws in this unfortunately.

First flaw is that as far as I know mod_simple_vhost doesn't drop privileges, so that every user's scripts are run as nobody.nogroup (or whatever it is on your distro). So basically, users have read access to each other's files making it really insecure (they could for example read database passwords!).

Second flaw is here: "In order to make sure that the directives in each of these configuration file only apply to one particular virtual host, the script wraps the content of each file in a conditional." There is a global context (http://trac.lighttpd.net/trac/wiki/Docs%3AConfiguration#global-context) which allows to get out of conditional, so basically everyone can change lighttpd's global settings. I've solved the first problem by using my custom version of execwrap (just added user/groupname to uid/gid mapping).

The second problem is somewhat problematic. I guess one of the solutions would be to omit files with global context keyword.
A better solution would be to patch lighttpd and add something like "unsafe context", a context where global context doesn't work, so:

unsafe {
  global {
    # nope, this is not possible in unsafe context
  }
}

wouldn't work.

cheers.

Submitted by Hannes Schmidt on Sun, 09/10/2006 - 14:10.

That is so cool! Thanks for contributing!

-- Hannes

Submitted by Anonymous on Sun, 09/10/2006 - 13:44.

Here's a Perl version of this nice script. It's a bit messy, but hey, it's Perl :-)

#! /usr/bin/perl -w
#
# -*- perl -*-
#
# lighttpd-vhost.conf.pl
#

use strict;
use File::Basename;

my $conf_name = "lighttpd.conf";
my $path = dirname($0);
my $global_conf_name = $path . "/" . $conf_name;

sub readconf($%)
{
    my ($conf, %context) = @_;

    if (open(CONF, $conf)) {
        while (<CONF>) {
            s/%(\w+)%/{exists $context{$1} ? $context{$1} : '%'.$1.'%'}/geo;
            print "    " . $_;
        }
    }
}

opendir(VHOSTDIR, $path) or die "$path: can't open directory\n";
while (my $host = readdir(VHOSTDIR)) {
    if ($host !~ /^\.\.?$/o and -d ($path . '/' . $host)) {
        my $dir = $path . '/' . $host;
        my %context = ('path', $path, 'dir', $dir, 'host', $host);
        print "\$HTTP[\"host\"] == \"$host\" {\n";
        readconf($global_conf_name, %context);
        readconf($dir . '/' . $conf_name, %context);
        print "}\n";
    }
}
closedir(VHOSTDIR);

Submitted by Hannes Schmidt on Fri, 09/01/2006 - 14:44.

RoR is an entirely different story. When I setup Rails it seemed to me that the Rails-Lighty integration was in the flux. That was two months ago. I kinda got it working but wasn't 100% confident about wether I got it all right. One thing should be noted: the solution presented here can be used for any lighttpd.conf statement, including the ones used for integrating Rails into Lighty.

-- Hannes

PS: In case you're wondering where you're second post went: We don't allow URL-drops here. Sorry. ;-)

Submitted by Anonymous on Fri, 09/01/2006 - 13:52.

Hannes, is there a RoR url redirect/rewrite directive you could share along with this excellent system? I have been using this script for my PHP hosting for some time now but am getting "Not Found" errors when trying to do RoR hosting.

Submitted by Anonymous on Fri, 05/19/2006 - 15:53.

Thanks for posting this, are those ruby or lighttpd variables?

Submitted by Anonymous on Tue, 03/07/2006 - 18:09.

Hannes Schmidt,

I hear that this is the ultimate cyber gathering place for computer weasles. To this I say, Alle ist klar herr Kommissar.

Big Dolphin Daddy
Cal State Sacramento Implementation King