CakeDC Blog

TIPS, INSIGHTS AND THE LATEST FROM THE EXPERTS BEHIND CAKEPHP

Lighty Story

I will tell you a story. Once upon a time... Seriously though, it was not too long ago in the past - but it happened and it is possible you can benefit from it.

What?

This tutorial will show how to make lighttpd 1.4.20 serve virtual hosts with CakePHP applications. Our scenario is quite simple:

  1. For admin purposes, lighttpd will listen on localhost, it will serve several CakePHP applications on several external ip addresses, without SSL.
  2. Virtual hosts will be organized in groups and every group will use one CakePHP core checkout for its virtual hosts.
  3. Every virtual host will have it own access log (this server will not run hundreds of virtual hosts, so we can afford to waste one file descriptor for each) and its own directory for caching of compressed static files.
  4. Management of virtual hosts, their default and custom settings should be as easy as possible, so we can delegate the management of some ip addresses or just groups of virthosts to someone else and sleep well, because nobody will have to touch our precious configuration files.

However, our scenario has some special requirements which we need to solve. By the way, I will be showing you how to do things the hard way from the start. In hopes to spare you a lot of headaches in future. Lighttpd is sweet piece of software, and is under active development. Unfortunately, there are things that are not easy to set up. For example - when using any of provided virtual host modules, it is impossible to set up different access logs and cache directories for compressed content etc. dynamically in a pure lighty config file without external scripts. Everything (except for per virtual host errorlog) is possible by writing necessary configuration by hand. But we willing to work more now, so we can be lazy later!

There are several approaches for bash, Ruby etc. However, nothing usable in PHP as far as I know. I will show you how easy it could be. Take this as a working example, I am sharing ideas here, not bullet-proof all-mighty solutions. Lets go for it - and utilize PHP and the include_shell command in our lighttpd configuration file. The motto of this article is: it is easier read generated configuration, then write it by hand.

How? Lighty!

Don't think this is not a good answer. Lets set up a decent lighttpd installation. We'll assume you have it compiled and installed. Lets also assume that you have PHP prepared for lighttpd's ModFastCGI and are just waiting for configuration and the first test run. Also, for shell commands which need to be executed under root account, I'll use sudo in following examples.

    sudo mkdir /usr/local/etc/lighttpd

First of all, we need a directory for our custom configuration. When in doubt, a fast look into its contents will tell you everything one should know about virtual hosts configuration.

    sudo mkdir -p /usr/local/www/data/default/webroot
    echo "<html><head><title>It works<body>It works" > /usr/local/www/data/default/webroot/index.html

Next we created a directory for our default webroot. It will be used on localhost only, with index.html.

    sudo touch /var/log/lighttpd.error.log /var/log/lighttpd.access.log
    sudo chown www:www /var/log/lighttpd.error.log /var/log/lighttpd.access.log

Now we need to create error and access log files. The first one will be common for whole server, the second will be used for localhost only.

    sudo mkdir -p /var/cache/lighttpd/compress/default
    sudo chown -R www:www /var/cache/lighttpd

The last thing we had to prepare was the default directory for caching of compressed static files.

In /usr/local/etc/lighttpd.conf we will setup a simple config file containing the common configuration we will utilize later:

    server.modules = (
        "mod_simple_vhost",
        "mod_magnet",
        "mod_redirect",
        "mod_access",
        "mod_auth",
        "mod_expire",
        "mod_compress",
        "mod_fastcgi",
        "mod_accesslog"
    )
    
    server.document-root = "/usr/local/www/data/default/webroot/"
    server.errorlog = "/var/log/lighttpd.error.log"
    accesslog.filename = "/var/log/lighttpd.access.log"
    server.port = 80
    server.bind = "127.0.0.1"
    server.username = "www"
    server.groupname = "www"
    server.pid-file = "/var/run/lighttpd.pid"
    index-file.names = ( "index.php", "index.html", "index.htm", "default.htm" )
    
    # shortened !!!
    mimetype.assign = (
        ...
    )
    
    url.access-deny = ( "~", ".inc" )
    
    static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" )
    
    dir-listing.activate = "disable"
    
    etag.use-mtime = "enable"
    static-file.etags = "enable"
    
    $HTTP["url"] =~ "^(/css/|/files/|/img/|/js/|/images/|/themed/|/favicon.ico)" {
        expire.url = ( "" => "access 7 days" )
    }
    
    compress.cache-dir = "/var/cache/lighttpd/compress/default/"
    compress.filetype = ( "text/plain", "text/html", "text/xml", "text/javascript", "text/css" )
    
    fastcgi.server = (
        ".php" => ((
            "bin-path" => "/usr/local/bin/php-cgi -c /usr/local/etc/php.ini",
            "socket" => "/tmp/lighttpd_php5.socket",
            "min-procs" => 1,
            "max-procs" => 1,
            "bin-environment" => (
                "FCGI_WEB_SERVER_ADDRS" => "127.0.0.1",
                "PHP_FCGI_CHILDREN" => "4",
                "PHP_FCGI_MAX_REQUESTS" => "1000"
            ),
            "bin-copy-environment" => ( "PATH", "SHELL", "USER"),
            "broken-scriptfilename" => "enable"
        ))
    )
    
    simple-vhost.server-root = "/usr/local/www/data/"
    simple-vhost.document-root = "webroot"
    simple-vhost.default-host = "default"
    
    $HTTP["host"] =~ "^www\.(.*)" {
        url.redirect = ( "^/(.*)" => "http://%1/$1" )
    }

How far along are we? So far we have a configured webserver with few preloaded modules and simple common configuration.

Our sever is currently:

  1. Listening on localhost:80.
  2. Refusing directory listing or sending some filetypes as plain text.
  3. Using etags and sending expiration headers for a set of static resources to 7 days by default. This allows us to schedule an upgrade of any virtual host just a week before it will happen.
  4. Using compression and caching of compressed static files for several mimetypes.
  5. Starting PHP as FastCGI, with only one parent process (we are going to use opcode cache). We are allowing only few child processes for this example tutorial and killing fcgi child processes after every 1000 requests
  6. Using mod_simple_vhost for name-based virtual hosting (preconfigured for fallback to default webroot).
  7. Redirecting all domains using www subdomain to the shorter version.

You will probably want to tweak some other settings. I am not going to describe all the server.max* configuration options, or talk about other pretty obvious things like mod_evasive, mod_status, mod_rrdtool etc, don't worry. Two things you should consider if some of your visitors will use one of the major browsers.

    $HTTP["url"] =~ "\.pdf$" {
        server.range-requests = "disable"
    }

You do not want to cut off IE users from your pdf documents, right?

    compress.filetype = ( "text/plain", "text/html", "text/xml" )
    $HTTP["useragent"] =~ "Firefox" {
        compress.filetype  += ("text/javascript", "text/css" )
    }

If your visitors are using an old (and/or above mentioned undesirable) internet browser, you can control compression settings per useragent in this way. Instead of the above example, compressing all 5 crucial mimetypes.

Ready to go? Ok, start lighttpd and make sure you see what you expect at http://localhost/

    echo "<?php phpinfo(); ?>" > /usr/local/www/data/default/webroot/phpinfo.php

Just to be sure that fcgi works as expected, try to see info about your current PHP setup at http://localhost/phpinfo.php and watch /var/log/lighttpd.error.log.

Url rewriting

It is possible to use lighttpd's mod_rewrite and create pattern for our static files if we are sure they exist. This approach has downsides though. We want to setup this part of webserver up and forget it exists. This is not possible with mod_rewrite, because for example, we are not going to force our developers to forget about /js/something.js as url for some of application controllers. Instead, we will use mod_magnet and custom Lua script. Visit this thread at CakePHP Google Group. Save the provided script to /usr/local/etc/lighttpd/cleanurl-v6.lua and add the following line to bottom of /usr/local/etc/lighttpd.conf:

    magnet.attract-physical-path-to = ( "/usr/local/etc/lighttpd/cleanurl-v6.lua" )

After restarting lighttpd, we are ready to remove all the .htaccess files from our filesystem and forget they exist. All requests for non-existing static files will be rewritten to /index.php?url=xxx like CakePHP requires.

Virtual hosts

Now we want to set up a directory structure and custom configuration for our virtual hosts and their groups. We will design a directory structure that can be used for dynamic configuration later, with no need to repeat anything obvious in configuration files. In this case, only logs folder matters (make sure it is writable by webserver). We will symlink everything else. Lets use the following directory structure with CakePHP core and our applications checkouts like our standard:

    # example.com (with redirect from www.example.com)
    /home/company/
                  logs/
                  www/
                      cake/
                      mainsite/
                               ...
                               webroot/
                      vendors/
    # dev-main.example.com and dev-product.example.com
    /home/development/
                  logs/
                  www/
                      cake/
                      mainsite/
                               ...
                               webroot/
                      product/
                               ...
                               webroot/
                      vendors/
    # stage-main.example.com and stage-product.example.com
    /home/staging/
                  logs/
                  www/
                      cake/
                      mainsite/
                               ...
                               webroot/
                      product/
                               ...
                               webroot/
                      vendors/
    # api.example.com, book.example.com, product.com ( with redirect from www.product.com)
    /home/product/
                  logs/
                  www/
                      api/
                          ...
                          index.html
                      book/
                               ...
                               webroot/
                      cake/
                      product/
                               ...
                               webroot/
                      vendors/

If you think the above directory tree is overcomplicated, or it seems too long for simple tutorial example, stop reading please, and feel free to come back any time later. It was nice to meet you :-) Things are only getting worse from here on in. For those brave enough to read on, you should have an idea of which domains will use which applications, and which applications will share one CakePHP core and folder for logs (not necessarily, read more).

Now we are getting somewhere - we need tell our webserver on which external ip addresses it has to listen for incoming connections, and which virtual hosts map to each ip address. Our www subdomains (redirected) should listen on a different ip address then their short versions. This allows us to use different SSL certificates for them later, if there is a need for secure connections. To show what is possible with our config parser, api.example.com will not use a /webroot/ folder, it contains just static html files. To make things even more tricky, api.example.com and book.example.com will not listen on same ip like their neighbour application product.com.

    cd /usr/local/etc/lighttpd

From now on, we will continue our work in this directory.

Lets say that we want to use ip 1.2.3.4 for domains example.com, api.example.com and book.example.com.

    sudo mkdir -p ./1.2.3.4:80/company
    sudo ln -s /home/company/www/cake ./1.2.3.4:80/company/cake
    sudo ln -s /home/company/www/vendors ./1.2.3.4:80/company/vendors
    
    sudo ln -s /home/company/www/mainsite ./1.2.3.4:80/company/example.com
    
    sudo mkdir ./1.2.3.4:80/product
    sudo ln -s /home/product/www/cake ./1.2.3.4:80/product/cake
    sudo ln -s /home/product/www/vendors ./1.2.3.4:80/product/vendors
    
    sudo ln -s /home/product/www/api ./1.2.3.4:80/product/api.example.com
    sudo ln -s /home/product/www/book ./1.2.3.4:80/product/book.example.com

What exactly did we just do? We created a folder named 1.2.3.4:80, containing 2 subfolders company and product. These will be used as groups of virtual hosts - their names should be the same as the name of their home directory (by default, path for logs can be adjusted). We will use them for setting paths to log files later. Both company and product have a symlinked cake and vendors folders and symlinks named as real domains and pointing to our app folders.

Lets continue - ip 2.3.4:5:80 will be used for rest of the group product.

    sudo mkdir -p ./2.3.4.5:80/product
    sudo ln -s /home/product/www/cake ./2.3.4.5:80/product/cake
    sudo ln -s /home/product/www/vendors ./2.3.4.5:80/product/vendors
    
    sudo ln -s /home/product/www/product ./2.3.4.5:80/product/product.com

That means only one virtual host for now.

Ok, ip 3.4.5.6 is going to be used for the www subdomains. No symlinks to existing applications are necessary here, because lighttpd will redirect requests coming to www.example.com to example.com automatically.

    sudo mkdir -p ./3.4.5.6:80/company/www.example.com ./3.4.5.6:80/product/www.product.com

We just had to create ip:port directory for the socket, group(s) of www virtualhosts and some domain-based directories just to have something to point default virtual host of this group at.

Staging and development checkouts will all share one ip 4.5.6.7.

    sudo mkdir -p ./4.5.6.7:80/development
    sudo ln -s /home/development/www/cake ./4.5.6.7:80/development/cake
    sudo ln -s /home/development/www/vendors ./4.5.6.7:80/development/vendors
    
    sudo ln -s /home/development/www/mainsite ./4.5.6.7:80/development/dev-main.example.com
    sudo ln -s /home/development/www/product ./4.5.6.7:80/development/dev-product.example.com
    
    sudo mkdir ./4.5.6.7:80/staging
    sudo ln -s /home/staging/www/cake ./4.5.6.7:80/staging/cake
    sudo ln -s /home/staging/www/vendors ./4.5.6.7:80/staging/vendors
    
    sudo ln -s /home/staging/www/mainsite ./4.5.6.7:80/staging/stage-main.example.com
    sudo ln -s /home/staging/www/product ./4.5.6.7:80/staging/stage-product.example.com

Four virtual hosts on one ip from different home folders (therefore placed in different groups).

The hard part is complete. Lets go through the bothering part of this custom setup. Did I said already that everything is a file? Don't be scared from amount of necessary steps, it will all be worth it in the future.

Lets look what we have done in directory /usr/local/etc/lighttpd/:

    1.2.3.4:80/
               company/
                        cake/        <-- /home/company/www/cake
                        example.com/ <-- /home/company/www/mainsite
                        vendors/     <-- /home/company/www/vendors
               product/
                        api.example.com/  <-- /home/product/www/api
                        book.example.com/ <-- /home/product/www/book
                        cake/             <-- /home/product/www/cake
                        vendors/          <-- /home/product/www/vendors
    2.3.4.5:80/
               product/
                        cake/        <-- /home/product/www/cake
                        product.com/ <-- /home/product/www/product
                        vendors/     <-- /home/product/www/vendors
    3.4.5.6:80/
               company/www.example.com/ <-- empty directory (redirected), necessary for default virtual host 
               product/www.product.com/ <-- empty directory (redirected), necessary for default virtual host
    4.5.6:7:80/
               development/
                        cake/                    <-- /home/development/www/cake
                        dev-main.example.com/    <-- /home/development/www/mainsite
                        dev-product.example.com/ <-- /home/development/www/product
                        vendors/                 <-- /home/development/www/vendors
               staging/
                        cake/                      <-- /home/staging/www/cake
                        stage-main.example.com/    <-- /home/staging/www/mainsite
                        stage-product.example.com/ <-- /home/staging/www/product
                        vendors/                   <-- /home/staging/www/vendors

Some new folders with symlinks.

Are you still with me? For those who know mod_simple_vhost, you should be already be pretty clear where we are going. Besides the accesslog path and compress folder path, we will also switch simple-vhost.server-root and simple-vhost.default-host in dependency of used socket and some hostname condition for virthost group. Actually, there is a bit more as well that I will show you.

The above directory structure shows that we have 7 groups of virtual hosts in 4 sockets, so lets create 7 simple configuration files for our groups of virtual hosts. Configuration file for group is not required in very special case - no regex pattern for this group, only one virtual host inside and - either only group in socket, or (alphabetically) last one.

<?php # /usr/local/etc/lighttpd/1.2.3.4:80/company/config.php
    $config['group'] = array(
        'host' => '^example\.com',
        'default' => 'example.com'
    );
?>
<?php # /usr/local/etc/lighttpd/1.2.3.4:80/product/config.php
    $config['group'] = array(
        'host' => '^(.*)\.example\.com',
        'default' => 'book.example.com'
    );
?>
<?php # /usr/local/etc/lighttpd/2.3.4.5:80/product/config.php
    $config['group'] = array(
        'host' => '^product\.com',
        'default' => 'product.com'
    );
?>
<?php # /usr/local/etc/lighttpd/3.4.5.6:80/company/config.php
    $config['group'] = array(
        'host' => '^(.*)\.example\.com',
        'default' => 'www.example.com'
    );
?>
<?php # /usr/local/etc/lighttpd/3.4.5.6:80/product/config.php
    $config['group'] = array(
        'host' => '^(.*)\.product\.com',
        'default' => 'www.product.com'
    );
?>
<?php # /usr/local/etc/lighttpd/4.5.6:7:80/development/config.php
    $config['group'] = array(
        'host' => '^dev-(.*)\.example\.com',
        'default' => 'dev-main.example.com'
    );
?>
<?php # /usr/local/etc/lighttpd/4.5.6:7:80/staging/config.php
    $config['group'] = array(
        'host' => '^stage-(.*)\.example\.com',
        'default' => 'stage-main.example.com'
    );
?>

And that's it. Every group (subfolder of ip.ad.dr.es:80 socket folder) has the required minimal configuration, and everything is properly set up. So lets see what we can take off from it.

Dynamic configuration

Extract this file in folder /usr/local/etc/lighttpd.

    sudo chmod a+x ./simple_config.php

Make simple_config.php executable for everyone.

Now run it as a non-privileged user.

    ./simple_config.php | more

You should see a basic generated configuration for your sockets, virthosts and virthosts groups.

Now we are already looking at a snippet of the generated configuration.

    #
    # Simple configuration parser output
    #
    # ERROR logfile /home/company/logs/example-access_log can not be created, SKIPPING
    # ERROR compress cache /var/cache/lighttpd/compress/example.com/ can not be created, SKIPPING
    # ERROR logfile /home/product/logs/api-access_log can not be created, SKIPPING
    # ERROR compress cache /var/cache/lighttpd/compress/api.example.com/ can not be created, SKIPPING
    # ERROR logfile /home/product/logs/book-access_log can not be created, SKIPPING
    # ERROR compress cache /var/cache/lighttpd/compress/book.example.com/ can not be created, SKIPPING
    # ERROR logfile /home/product/logs/product-access_log can not be created, SKIPPING
    # ERROR compress cache /var/cache/lighttpd/compress/product.com/ can not be created, SKIPPING
    # ERROR logfile /home/company/logs/www-access_log can not be created, SKIPPING
    # ERROR compress cache /var/cache/lighttpd/compress/www.example.com/ can not be created, SKIPPING
    # ERROR logfile /home/product/logs/www-access_log can not be created, SKIPPING
    # ERROR compress cache /var/cache/lighttpd/compress/www.product.com/ can not be created, SKIPPING
    # ERROR logfile /home/development/logs/dev-main-access_log can not be created, SKIPPING
    # ERROR compress cache /var/cache/lighttpd/compress/dev-main.example.com/ can not be created, SKIPPING
    # ERROR logfile /home/development/logs/dev-product-access_log can not be created, SKIPPING
    # ERROR compress cache /var/cache/lighttpd/compress/dev-product.example.com/ can not be created, SKIPPING
    # ERROR logfile /home/staging/logs/stage-main-access_log can not be created, SKIPPING
    # ERROR compress cache /var/cache/lighttpd/compress/stage-main.example.com/ can not be created, SKIPPING
    # ERROR logfile /home/staging/logs/stage-product-access_log can not be created, SKIPPING
    # ERROR compress cache /var/cache/lighttpd/compress/stage-product.example.com/ can not be created, SKIPPING
    #
    
    $SERVER["socket"] == "1.2.3.4:80" {
            $HTTP["host"] =~ "^example\.com" {
                    simple-vhost.server-root = "/usr/local/etc/lighttpd/1.2.3.4:80/company/"
                    simple-vhost.default-host = "example.com"
                    $HTTP["host"] == "example.com" {
                    ....

You can see which files this script is trying to create. It will create all of them when you will run it as root once. But there are two things we would like to fix first: access logs /home/company/logs/www-access_log and /home/product/logs/www-access_log are generated for our redirected domains.

Lets redirect these logs to those used by domains example.com and product.com:

<?php # /usr/local/etc/lighttpd/3.4.5.6:80/company/config.php
    $config['group'] = array(
        'host' => '^(.*)\.example\.com',
        'default' => 'www.example.com'
    );
    $config['virthosts'] = array(
        'www.example.com' => array(
            'log' => 'example'
        )
    );
?>
<?php # /usr/local/etc/lighttpd/3.4.5.6:80/product/config.php
    $config['group'] = array(
        'host' => '^(.*)\.product\.com',
        'default' => 'www.product.com'
    );
    $config['virthosts'] = array(
        'www.product.com' => array(
            'log' => 'product'
        )
    );
?>

Running ./simple_config.php as unprivileged user again shows this script is no longer trying to create any www-access_log files. We will not care about directories for compressed content, they can be used later, but we will never serve different content on example.com and www.example.com, so it is logical that they share one log file. Every decent logfile parser can handle several domains in one log file.

Now, you can run this script as root:

    sudo ./simple_config.php

and result will look much better now:

#
# Simple configuration parser output
#
# NOTICE created logfile /home/company/logs/example-access_log
# NOTICE created compress cache /var/cache/lighttpd/compress/example.com/
# NOTICE created logfile /home/product/logs/api-access_log
# NOTICE created compress cache /var/cache/lighttpd/compress/api.example.com/
# NOTICE created logfile /home/product/logs/book-access_log
# NOTICE created compress cache /var/cache/lighttpd/compress/book.example.com/
# NOTICE created logfile /home/product/logs/product-access_log
# NOTICE created compress cache /var/cache/lighttpd/compress/product.com/
# NOTICE created compress cache /var/cache/lighttpd/compress/www.example.com/
# NOTICE created compress cache /var/cache/lighttpd/compress/www.product.com/
# NOTICE created logfile /home/development/logs/dev-main-access_log
# NOTICE created compress cache /var/cache/lighttpd/compress/dev-main.example.com/
# NOTICE created logfile /home/development/logs/dev-product-access_log
# NOTICE created compress cache /var/cache/lighttpd/compress/dev-product.example.com/
# NOTICE created logfile /home/staging/logs/stage-main-access_log
# NOTICE created compress cache /var/cache/lighttpd/compress/stage-main.example.com/
# NOTICE created logfile /home/staging/logs/stage-product-access_log
# NOTICE created compress cache /var/cache/lighttpd/compress/stage-product.example.com/
#

    $SERVER["socket"] == "1.2.3.4:80" {
            $HTTP["host"] =~ "^example\.com" {
                    simple-vhost.server-root = "/usr/local/etc/lighttpd/1.2.3.4:80/company/"
                    simple-vhost.default-host = "example.com"
                    $HTTP["host"] == "example.com" {
                            accesslog.filename = "/home/company/logs/example-access_log"
                            compress.cache-dir = "/var/cache/lighttpd/compress/example.com/"
                    }
            }
            else $HTTP["host"] =~ "^(.*)\.example\.com" {
                    simple-vhost.server-root = "/usr/local/etc/lighttpd/1.2.3.4:80/product/"
                    simple-vhost.default-host = "book.example.com"
                    $HTTP["host"] == "api.example.com" {
                            accesslog.filename = "/home/product/logs/api-access_log"
                            compress.cache-dir = "/var/cache/lighttpd/compress/api.example.com/"
                    }
                    else $HTTP["host"] == "book.example.com" {
                            accesslog.filename = "/home/product/logs/book-access_log"
                            compress.cache-dir = "/var/cache/lighttpd/compress/book.example.com/"
                    }
            }
    }
    $SERVER["socket"] == "2.3.4.5:80" {
            $HTTP["host"] =~ "^product\.com" {
                    simple-vhost.server-root = "/usr/local/etc/lighttpd/2.3.4.5:80/product/"
                    simple-vhost.default-host = "product.com"
                    $HTTP["host"] == "product.com" {
                            accesslog.filename = "/home/product/logs/product-access_log"
                            compress.cache-dir = "/var/cache/lighttpd/compress/product.com/"
                    }
            }
    }
    $SERVER["socket"] == "3.4.5.6:80" {
            $HTTP["host"] =~ "^(.*)\.example\.com" {
                    simple-vhost.server-root = "/usr/local/etc/lighttpd/3.4.5.6:80/company/"
                    simple-vhost.default-host = "www.example.com"
                    $HTTP["host"] == "www.example.com" {
                            accesslog.filename = "/home/company/logs/example-access_log"
                            compress.cache-dir = "/var/cache/lighttpd/compress/www.example.com/"
                    }
            }
            else $HTTP["host"] =~ "^(.*)\.product\.com" {
                    simple-vhost.server-root = "/usr/local/etc/lighttpd/3.4.5.6:80/product/"
                    simple-vhost.default-host = "www.product.com"
                    $HTTP["host"] == "www.product.com" {
                            accesslog.filename = "/home/product/logs/product-access_log"
                            compress.cache-dir = "/var/cache/lighttpd/compress/www.product.com/"
                    }
            }
    }
    $SERVER["socket"] == "4.5.6.7:80" {
            $HTTP["host"] =~ "^dev-(.*)\.example\.com" {
                    simple-vhost.server-root = "/usr/local/etc/lighttpd/4.5.6.7:80/development/"
                    simple-vhost.default-host = "dev-main.example.com"
                    $HTTP["host"] == "dev-main.example.com" {
                            accesslog.filename = "/home/development/logs/dev-main-access_log"
                            compress.cache-dir = "/var/cache/lighttpd/compress/dev-main.example.com/"
                    }
                    else $HTTP["host"] == "dev-product.example.com" {
                            accesslog.filename = "/home/development/logs/dev-product-access_log"
                            compress.cache-dir = "/var/cache/lighttpd/compress/dev-product.example.com/"
                    }
            }
            else $HTTP["host"] =~ "^stage-(.*)\.example\.com" {
                    simple-vhost.server-root = "/usr/local/etc/lighttpd/4.5.6.7:80/staging/"
                    simple-vhost.default-host = "stage-main.example.com"
                    $HTTP["host"] == "stage-main.example.com" {
                            accesslog.filename = "/home/staging/logs/stage-main-access_log"
                            compress.cache-dir = "/var/cache/lighttpd/compress/stage-main.example.com/"
                    }
                    else $HTTP["host"] == "stage-product.example.com" {
                            accesslog.filename = "/home/staging/logs/stage-product-access_log"
                            compress.cache-dir = "/var/cache/lighttpd/compress/stage-product.example.com/"
                    }
            }
    }

Getting close to what we need from this setup.

I will process several steps now, and then I will paste here final output of config parser for you to compare with above one.

We have another domain manual.example.com (with no virthost set) and we want to redirect it to api.example.com with configuration only, it will be using its own manual-access_log. Furthermore, we want book.example.com condition happen sooner then the condition on api.example.com, because book is gaining more traffic, and attach domain aliases bibliotheca.example.com and bookstore.example.com to book.example.com. Also, expire headers for book should be set for 2 years and as previously mentioned api.example.com is not using /webroot/ folder.

<?php # /usr/local/etc/lighttpd/1.2.3.4:80/product/config.php
    $config['group'] = array(
        'host' => '^(.*)\.example\.com',
        'default' => 'book.example.com'
    );
    $config['virthosts'] = array(
        'book.example.com' => array(
            'expire' => array(
                '^(/css/|/files/|/img/|/js/|/images/|/themed/|/favicon.ico)' => 'access 2 years'
            ),
            'aliases' => array(
                'bibliotheca.example.com',
                'bookstore.example.com'
            )
        ),
        'api.example.com' => array(
            'webroot' => '/'
        ),
        'manual.example.com' => array(
             'redirect' => 'http://api.example.org/'
        )
    );
?>

All of it is fixed now. We even do not need folder/symlink for manual.example.com in this case.

Important note: we do not have to create folders for domains bibliotheca.example.com and bookstore.example.com, because they are aliases for book.example.com and it is used as default virtual host for this group! If you will set alias for non-default virtual host, you have to symlink aliased application several times to group folder - every time with a different domain name.

We want all staging sites to store logs in /home/development/logs. Also all staging and development sites should use expire headers for 5 minutes only and have to use http auth (one common file for now).

<?php # /usr/local/etc/lighttpd/4.5.6:7:80/development/config.php 
    $config['group'] = array(
        'host' => '^dev-(.*)\.example\.com', 
        'default' => 'dev-main.example.com', 
        'expire' => array(
             '^(/css/|/files/|/img/|/js/|/images/|/themed/|/favicon.ico)' => 'access 5 minutes' 
        ), 
        'auth' => array( 
            'backend' => 'htpasswd', 
            'file' => '/var/projects/company/.trac.htpasswd', 
            'protect' => array( 
                '/' => array( 
                    'realm' => 'Development Access', 
                    'require' => 'valid-user' 
                ) 
            )
        ) 
    );
?>
<?php # /usr/local/etc/lighttpd/4.5.6:7:80/staging/config.php 
    $config['group'] = array(
        'host' => '^stage-(.*)\.example\.com', 
        'default' => 'stage-main.example.com', 
        'expire' => array( 
            '^(/css/|/files/|/img/|/js/|/images/|/themed/|/favicon.ico)' => 'access 5 minutes' 
        ),
        'logs' => '/home/development/logs', 
        'auth' => array( 
            'backend' => 'htpasswd', 
            'file' => '/var/projects/company/.trac.htpasswd', 
            'protect' => array( 
                '/' => array( 
                    'realm' => 'Staging Access', 
                    'require' => 'valid-user' 
                ) 
            )
        ) 
    ); 
?>

This has all been fixed now.

Now our simple_config.php returns this:

    #
    # Simple configuration parser output
    #
    
    $SERVER["socket"] == "1.2.3.4:80" {
            $HTTP["host"] =~ "^example\.com" {
                    simple-vhost.server-root = "/usr/local/etc/lighttpd/1.2.3.4:80/company/"
                    simple-vhost.default-host = "example.com"
                    $HTTP["host"] == "example.com" {
                            accesslog.filename = "/home/company/logs/example-access_log"
                            compress.cache-dir = "/var/cache/lighttpd/compress/example.com/"
                    }
            }
            else $HTTP["host"] =~ "^(.*)\.example\.com" {
                    simple-vhost.server-root = "/usr/local/etc/lighttpd/1.2.3.4:80/product/"
                    simple-vhost.default-host = "book.example.com"
                    $HTTP["host"] =~ "^(book\.example\.com|bibliotheca\.example\.com|bookstore\.example\.com)" {
                            accesslog.filename = "/home/product/logs/book-access_log"
                            compress.cache-dir = "/var/cache/lighttpd/compress/book.example.com/"
                            $HTTP["url"] =~ "^(/css/|/files/|/img/|/js/|/images/|/themed/|/favicon.ico)" {
                                    expire.url = ("" => "access 2 years")
                            }
                    }
                    else $HTTP["host"] == "api.example.com" {
                            accesslog.filename = "/home/product/logs/api-access_log"
                            compress.cache-dir = "/var/cache/lighttpd/compress/api.example.com/"
                            simple-vhost.document-root = "/"
                    }
                    else $HTTP["host"] == "manual.example.com" {
                            accesslog.filename = "/home/product/logs/manual-access_log"
                            compress.cache-dir = "/var/cache/lighttpd/compress/manual.example.com/"
                            url.redirect = (
                                    ".*" => "http://api.example.org/"
                            )
                    }
            }
    }
    $SERVER["socket"] == "2.3.4.5:80" {
            $HTTP["host"] =~ "^product\.com" {
                    simple-vhost.server-root = "/usr/local/etc/lighttpd/2.3.4.5:80/product/"
                    simple-vhost.default-host = "product.com"
                    $HTTP["host"] == "product.com" {
                            accesslog.filename = "/home/product/logs/product-access_log"
                            compress.cache-dir = "/var/cache/lighttpd/compress/product.com/"
                    }
            }
    }
    $SERVER["socket"] == "3.4.5.6:80" {
            $HTTP["host"] =~ "^(.*)\.example\.com" {
                    simple-vhost.server-root = "/usr/local/etc/lighttpd/3.4.5.6:80/company/"
                    simple-vhost.default-host = "www.example.com"
                    $HTTP["host"] == "www.example.com" {
                            accesslog.filename = "/home/company/logs/example-access_log"
                            compress.cache-dir = "/var/cache/lighttpd/compress/www.example.com/"
                    }
            }
            else $HTTP["host"] =~ "^(.*)\.product\.com" {
                    simple-vhost.server-root = "/usr/local/etc/lighttpd/3.4.5.6:80/product/"
                    simple-vhost.default-host = "www.product.com"
                    $HTTP["host"] == "www.product.com" {
                            accesslog.filename = "/home/product/logs/product-access_log"
                            compress.cache-dir = "/var/cache/lighttpd/compress/www.product.com/"
                    }
            }
    }
    $SERVER["socket"] == "4.5.6.7:80" {
            $HTTP["host"] =~ "^dev-(.*)\.example\.com" {
                    simple-vhost.server-root = "/usr/local/etc/lighttpd/4.5.6.7:80/development/"
                    simple-vhost.default-host = "dev-main.example.com"
                    $HTTP["url"] =~ "^(/css/|/files/|/img/|/js/|/images/|/themed/|/favicon.ico)" {
                            expire.url = ("" => "access 5 minutes")
                    }
                    auth.backend = "htpasswd"
                    auth.backend.htpasswd.userfile = "/var/projects/company/.trac.htpasswd"
                    auth.require = (
                            "/" => (
                                    "method" => "basic",
                                    "realm" => "Development Access",
                                    "require" => "valid-user"
                            )
                    )
                    $HTTP["host"] == "dev-main.example.com" {
                            accesslog.filename = "/home/development/logs/dev-main-access_log"
                            compress.cache-dir = "/var/cache/lighttpd/compress/dev-main.example.com/"
                    }
                    else $HTTP["host"] == "dev-product.example.com" {
                            accesslog.filename = "/home/development/logs/dev-product-access_log"
                            compress.cache-dir = "/var/cache/lighttpd/compress/dev-product.example.com/"
                    }
            }
            else $HTTP["host"] =~ "^stage-(.*)\.example\.com" {
                    simple-vhost.server-root = "/usr/local/etc/lighttpd/4.5.6.7:80/staging/"
                    simple-vhost.default-host = "stage-main.example.com"
                    $HTTP["url"] =~ "^(/css/|/files/|/img/|/js/|/images/|/themed/|/favicon.ico)" {
                            expire.url = ("" => "access 5 minutes")
                    }
                    auth.backend = "htpasswd"
                    auth.backend.htpasswd.userfile = "/var/projects/company/.trac.htpasswd"
                    auth.require = (
                            "/" => (
                                    "method" => "basic",
                                    "realm" => "Staging Access",
                                    "require" => "valid-user"
                            )
                    )
                    $HTTP["host"] == "stage-main.example.com" {
                            accesslog.filename = "/home/development/logs/stage-main-access_log"
                            compress.cache-dir = "/var/cache/lighttpd/compress/stage-main.example.com/"
                    }
                    else $HTTP["host"] == "stage-product.example.com" {
                            accesslog.filename = "/home/development/logs/stage-product-access_log"
                            compress.cache-dir = "/var/cache/lighttpd/compress/stage-product.example.com/"
                    }
            }
    }

Now it looks like we are set with everything we needed.

One last line for /usr/local/etc/lighttpd.conf is:

    include_shell "/usr/local/etc/lighttpd/simple_config.php"

And that's all.

Before you will start or restart lighttpd, try and see if it can parse the new configuration (with our include) without errors, or inspect how it sees configuration after parsing:

    lighttpd -t -f /usr/local/etc/lighttpd.conf
    lighttpd -p -f /usr/local/etc/lighttpd.conf

It is better to run the above commands as root, off course.

Now what?

Think twice about patterns for groups - don't be surprised if you get 'It works' page or default virthost of another group, if you are too lazy to read the generated configuration! Groups are processed in alphabetical order - just so you know which patterns are going to be checked first. Well, it is possible to change order of groups - change name of some company group folder to xxx_company and:

    $config['group'] = array(
        'name' => 'company',

Now you should be fine - this group in folder named xxx_company instead of company, and everything will still work.

Everything that is necessary should be up and running now. Lighttpd should serve all virtual hosts from groups in sockets from now on. Read how to clear cache for mod_compress too. Smart brain should ask now, why we are using mod_simple_vhost, if our parser generates configuration for every virtual host it founds in our configuration files and directory structure. We don't do it, but you can - read code. Note for these who do not want or can not follow our default logs location, home directories, cache directories, user account lighttpd will use, or want to store directory structure with sockets/groups/virthosts somewhere else - read code too ;-) Reason why we set mod_simple_vhost for this example as default is simple - to get some domain serving some application, we need only one simple thing: symlink to app directory with domain name, placed in some virtual group in proper socket. This virtual host will be accessible immediately - although, restart of webserver is still necessary to have configuration for access logfile and compress directory for this virtual host (otherwise default accesslog and compress dir will be used), but not required.

A few questions remain, what and how needs to be done in obvious use cases - adding new ip addresses, groups, virthosts, or moving whole groups over sockets, moving virthosts over sockets, etc... I assume this part will be sweet piece of cake for you. Definitely - feel free to call simple_config.php as often as you want to. It is highly reccommended to save functional configuration to a backup file by redirecting the output. Sure, one can use include "/some/path/generated_output.conf" exclusively, instead of include_shell - it is up to you.

Backup, backup, backup. This is nothing more then a functional example, but the entire code lives in one class, so feel free to change or extend it for your needs. It is released under MIT license and is provided as it is, so you can do anything you want with it (except for removing license and copyright note). Keep in mind it was not tested in all possible situations and some of things I did not mention in this tutorial (but they are implemented in code) were not intensively tested yet.

If you feel that some of the subdomains used in this tutorial sound familiar to you, you are probably right. I didn't said it was going to be a fairy tale. I said, I will tell you a story. To be continued...

Latest articles

Remote Work, Actually Works!

As a fully remote company, the Cake Development Corporation team is used to working from home. We communicate with our team daily, keep on top of tasks, hold each other accountable and support one another. Heck, a lot of us even do it with kids in the household, too! I consider us extremely lucky to be able to work while juggling an at home life at the same time.  It has worked for CakeDC over the past decade, and in my opinion, can work for most companies.   As of last month, an estimated 4.7 million people were working remotely, which grew 44% over the last 5 years. This is just in the United States. Remote work is becoming the norm.  Obviously for the next few weeks, this number will be drastically increased, but perhaps this will educate companies on the advantages of a WFH culture. Advantages to employers, besides the operations cost (other than payroll, of course), which can decrease by close to 90%, includes increased productivity. Decreased overhead results in higher salaries, which results in more quality candidates and employees.  I understand the concern of the ability to micro-manage (UGH) being unavailable, but according to statistics, 85% of businesses that work remotely confirmed that productivity increased in their companies. When there is more flexibility, there will be higher employee morale.  With the current situation arising from COVID-19, a lot of businesses are forced to transition employees to WFH in order to stay afloat. This not only keeps employees and clients safe, but family members too.  I have put together some stats and resources that may help CEO’s and employees transition a little bit easier.  

Communication:

It is absolutely essential to keep open communication among a team when everyone is working remotely. Our team uses RocketChat* ( I will include some links in the resource section at the end of this blog), and it has proved to be effective. A chat allows for quicker response time, as well as allowing individuals to set their status (like busy, away, at lunch, sick, etc.). This is a good way to get quick answers, as users can be alerted when they have been messaged or tagged in a company chat. Most of our team work in different timezones, so this is a good way to “stay in the know” about everything happening day to day. We separate chats according to their department. For example: marketing, development, general, etc. We also have the option to private message with co-workers when needed.  Other ideas, if not daily chat interaction, include scheduled meetings. For most of our team meetings, we use Zoom. This tool allows for audio only, as well as video chats.  

Accountability & Time Management:

It is important that tasks are managed and followed through. We use programs like Redmine* to track hours and work, in addition to weekly, or monthly conference calls for each department.  If you or your team are new to remote work, it may be in your best interest to assign a project manager, someone who will assign work, track hours, and ensure that work needed is being completed in a timely manner. Without each person being held accountable, the ship will sink, fast. For personal accountability, there are many free apps and tools available. One example is Trello*. This is a scheduling board so that tasks are not forgotten and you can plan your work week and stay organized. Once tasks placed on your “schedule board” are completed, you can make note of it and stay focused on each one according to their priority. You can also keep track of documents and reports. The boards look like this:    

Resources:

Documents & Recording - We <3 Google Docs - we are able to share and edit internally, we couldn’t function without it.  Docusign is a good tool for contracts / documents needing signatures Invision Freehand - this is a tool where you can create presentations, and allows comments and feedback between designers. Good for freelance designers!    Organization/Tasks -  Trello - for individual time management scheduling.  Redmine - for project assigning, time recording, HR management,    Communication -  RocketChat - allows for multiple internal chats all rolled into one link (allows for individual logins) Zoom - good for meetings. Allows audio and video chats for teams or reps and clients.  Slack - also a great option for expanded chats. Each person has a “screen name” and can be personally messaged, or public groups can be created (we use this as well). Slack also allows video calls with their paid subscription.  Google Hangouts WhatsApp - if your team is diverse, like ours, WhatsApp is a must. We are able to text each other, regardless of location - no fees, no service problems (if you have wifi of course).  World Time Buddy - this is a tool that I am not familiar with, but being the designated “scheduler of meetings”, I think I would find it useful. If your team works within different timezones, this allows you to add the location of your teammates, compare times, and find ideal times for meetings.    Community - In the development world, community support sites are absolutely one of the most important tools. This allows for individuals - inside or outside of your company - to communicate and help each other out. Most developers are aware and utilize these, but if not, may I suggest: Discourse - chat support  GitHub - our favorite team collaboration tool. GitHub allows for hosting, editing and managing products. We use it for building software and allow for community interaction. It also integrates with a lot of other tools, which is a plus!  

Take Away:

These resources are just a drop in the bucket compared to what is available to remote workers. I think this is a reflection of how WFH is becoming more accepted and more normal in the corporate world. I’d love to hear some of your favorites: amanda.goff@cakedc.com.  Let’s take away some positivity to the current quarantined times, and encourage more companies to follow suit. In today’s world, flexibility goes a long way and this type of transition can be mutually beneficial for employers and employees. I mean look at us, we are PRETTY normal… right?  Speaking of being in quarantine - stay healthy, stay inside, and wash your hands!  

ddsds

Two Factor Authentication & CakeDC Users Plugin

Why 2FA?

Nowadays we have noticed that many of the websites or applications that we access offer the option to activate an extra layer of security called Two Factor Authentication, better known as 2FA. Most of our lives happen on our mobile devices and laptops, so it’s not a secret that cyber-thieves would like to gain access to our personal and financial data. This is why adding an extra layer for protecting logins is worth it.  2FA  is an extra layer of security to make sure that someone that is trying to gain access to an account is who they say they are. The first layer is generally a combination of a username and password, and the second layer could ask for a code that is sent to your phone, a fingerprint scan or the name of your best friend. Currently 2FA has become a security standard in the digital world.

How does it work?

First the user will enter his username and password, then instead of getting in immediately into the system, he will be required to provide  additional information. Which could be one of the following options or factors:
  • Something you know : This could be a password, a personal identification number (PIN), answers to a secret question or a specific keystroke pattern.
  • Something you have: This is something the user owns, a physical device, like a mobile phone, an id card, an usb stick, a token, etc.
  • Something you are: This could be face or voice recognition, retina scan,  fingerprint, DNA, handwriting.

CakeDC Users Plugin and 2FA

There are various ways to implement Time-based One-Time Password (TOTP), Short Message Service (SMS), Electronic Mail (Email),  Universal Second Factor (U2F). CakeDC Users Plugin provides the ability to enable in your site TOTP or U2F. 
 

TOTP Google Authenticator

Enabling 2FA Google Authenticator in CakeDC Users Plugin is quite easy, it just takes a few minutes. In case you have not installed CakeDC Users Plugin in your application, follow the installation steps described here. Once you have installed the plugin and your basic login is working, you just need to do the following:
  1. Run the next command: composer require robthree/twofactorauth
  2. In Application::pluginBootstrap() add the following: Configure::write('OneTimePasswordAuthenticator.login', true);
  Once you have 2FA enabled in your site, when you try to login will happen next 
  1. Type your username and password.   
  2. You proceed to the next step where you are asked for the authentication code
    • First time you will be shown a QR code that you need to scan from your authenticator application.   
    • Next time you will only get the input to type your authentication code  
  3. You open the authenticator application to get a secondary code called a one-time password (OTP)—usually six characters in length. There are many options in the market for the authenticator application, some of the most used are: Google Authenticator, Duo Mobile, FreeOTP etc.
  4. You type the 6-digit code into the website, and you’re in!
 

FIDO U2F

If you want something more solid and reliable, then you could use U2F (Universal 2nd Factor) standard created by the FIDO Alliance. With this kind of authentication you use a physical security key, and insert that into your PC, touch the key’s button, and you’re “automatically” logged in.  U2F standard was implemented in CakeDC Users Plugin by using  the YubiKey, the most famous and common example of U2F. To enable 2FA via Yubico follow the next steps:
  1. Run the next command: composer require yubico/u2flib-server:^1.0
  2. In Application::pluginBootstrap() add the following: Configure::write(‘U2f.enabled’, true);
     
Yubico is a hardware based 2FA, it’s a small device with one end that slots into a standard Type-A USB port. You just need to Insert your YubiKey and touch it! You won’t need to manually enter the code. Take into account that you will need to use https to be able to use 2FA features in your applicatins.

So, what to choose for two-factor authentication? There is no universal answer, it will depend on the level of security you are expecting, but start protecting your account by enabling 2FA! In this article you could noticed how easy is to enable 2FA in any CakePHP application by using CakeDC Users Plugin.
  References: https://github.com/CakeDC/users https://en.wikipedia.org/wiki/Multi-factor_authentication https://en.wikipedia.org/wiki/One-time_password https://en.wikipedia.org/wiki/FIDO_Alliance https://en.wikipedia.org/wiki/Universal_2nd_Factor  

We Bake with CakePHP