Premessa: recentemente Google ha registrato il tld dev e pertanto non è più possibile utilizzarlo per sviluppare in locale, dato che i browser non lo risolvono più in locale. Il presente post è stato modificato sostituendo dev con local, ovviamente il tutto funziona al meglio.
(aggiunto il 11-1-2018)

L'obiettivo è semplice: sviluppare sulla propria macchina senza dover ogni volta configurare apache per ogni nuovo sito. In soldoni ci proponiamo di:

  • mettere tutti i siti di sviluppo in una cartella, diciamo "~/Sites" o "/var/www/"
  • la cartella del sito in sviluppo si chiama come il dominio
  • i domini di sviluppo terminano con ".local"
  • nella cartella del sito deve essere possibile mettere altro materiale non pubblico, quindi il sito vero e proprio sarà nella sottocartella www

La situazione tipica del sito in sviluppo www.example.com è definita come segue:

  • directory del progetto: /Users/max/Sites/example.local (/var/www/example.local su Linux)
  • directory del sito: /Users/max/Sites/example.local/www (/var/www/example.local/www)
  • inoltre il tutto funziona per qualsiasi cartella contenuta in example.local, ad esempio www2 sarà accessibile attraverso www2.example.local (/var/www/example.local/www2)
  • se si accede senza specificare l'host (il primo pezzo del domain name, www per intenderci) verranno serviti di default i file della directory www (/var/www/example.local/www)

inoltre per le pagine spicce e le prove potremo usare la directory /Users/max/Sites/localhost (/var/www/html su Linux) che funzionerà da default virtual host.

In pratica occorre installare e configurare un dns sulla macchina locale che risolva tutte le richieste .local in 127.0.0.1 (oppure l'ip della vm, se il server è là), ma che non venga coinvolto per ogni altra richiesta, in modo che non ci siano intralci a dhcp o altro se ci spostiamo sotto diverse reti, e configurare apache in modo che virtualizzi dinamicamente ogni dominio .local servendo le richieste dalla cartella opportuna.

Partiamo dal DNS

OSX

Con homebrew installiamo dnsmasq e lo configuriamo come spiegato nell'ottimo tutorial  https://passingcuriosity.com/2013/dnsmasq-dev-osx/ :

# brew install dnsmasq
# cp /usr/local/opt/dnsmasq/dnsmasq.conf.example /usr/local/etc/dnsmasq.conf

quindi modifichiamo il file /usr/local/etc/dnsmasq.conf scommentando e modificando le sole righe:

address=/local/127.0.0.1
listen-address=127.0.0.1

Impostiamo OSX in modo che invii le richieste per il tld local a dnsmasq aggiungendo il file /etc/resolver/local (se la directory /etc/resolver/ non esiste la creiamo come root) contenente il testo

nameserver 127.0.0.1

e avviamo in modo definitivo dnsmasq (come homebrew aveva suggerito durante l'installazione):

# sudo brew services start dnsmasq

Verifichiamo infine che tutto funzioni correttamente:

ping -c 1 www.google.com
ping -c 1 this.is.a.test.local
ping -c 1 iam.the.walrus.local

Linux

Su Linux/Debian o Ubuntu installiamo dnsmasq: 

# sudo ape-get install dnsmasq

copiamo /etc/resolv.conf in /etc/resolv.dnsmasq.conf (i precendenti dns a cui accede la macchina) e in /etc/resolv.conf mettiamo solo:

nameserver 127.0.0.1

in modo che ogni servizio utilizzi dnsmasq. Quindi configuriamo dnsmasq inserendo in /etc/dnsmasq.conf

address=/local/127.0.0.1
resolv-file=/etc/resolv.dnsmasq.conf

La prima riga dice che tutto ciò che finisce con local viene mappato sulla macchina locale (se per programmare usate una VM, qui mettete l'ip della VM, ad esempio 192.168.123.123), la seconda dice di inoltrare le richieste di risoluzione di dominio ai server indicati nel file. I dns indicati in resolv.dnsmasq.conf potrebbero essere i soliti di google:

nameserver 8.8.8.8
nameserver 8.8.4.4

o quelli forniti dal vostro ISP. Purtroppo il dhcp a volte (ciè sempre;) sovrascrive resolv.conf, occorre assicurarsi che ciò non accada: si può ovviare alla cosa modificando il file /etc/dhcp/dhclient.conf e scommentare la riga (provare con un riavvio del servizio dhcp):

prepend domain-name-servers 127.0.0.1;

che fa sì che il file /etc/resolv.conf contega come prima riga il loopback e poi i dns suggeriti dal server dhcp, che è poi quello che ci serve.

Windows

Nel disavventuroso caso la vostra piattaforma di sviluppo contempli un windoze con VM linux per il server, al posto di dnsmasq si può utilizzare DualDHCPDNS installato in windoze, avviato o come servizio o all'occorrenza, e configurato semplicemente così:

[SERVICES]
DNS
[WILD_HOSTS]
*.local=192.168.123.123

dove al posto di 192.168.123.123 avete messo l'IP della macchina virtuale su cui gira il server linux.

Se usate VirtualBox, è opportuno configurare un paio di schede di rete sul guest in modo da poter accedere a Internet dal guest anche sotto reti dhcp (e quindi in situazioni variabili) e accedere al guest da windoze sempre sullo stesso ip (se no occorre andare a cercarlo ogni volta volta che si avvia la vm): in tal senso avremo una scheda in bridge sulla wifi (se usate prevalentemente quella) o wired (a mali estremi anche due schede separate, se usate un po' e un po') e una scheda con rete solo host (ad esempio vboxnet0 con ip impostato in modo che non entri in conflitto con le reti in cui di solito lavoriamo, ad esempio 192.168.123.0/255.255.255.0) con ip fisso sulla vm, ad esempio il 192.168.123.123 dell'esempio precedente.

Ricordatevi di montare la directory /var/www del guest da una condivisione (gestita con VirtualBox) di windoze in modo da poter usare Eclipse sul host, senza dover passare da una macchina all'altra ogni volta. In questo modo sulla vm non occorrerebbe neanche il window manager il che rende la vm più leggera e più veloce da realizzare e manutenere. A questo proposito ricordo che Vagrant serve propio a costruire e foraggiare macchine virtuali per questo scopo.

In alternativa al dns server, chi lavora solo su windows (quindi senza vm Linux) può considerare una soluzione molto semplice a costo nullo!

Verifica del DNS

Per verificare il corretto funzionamento del DNS, da console (sull'host)

# ping www.google.local
PING www.google.local (127.0.0.1) 56(84) bytes of data.
...
# ping www.google.it
PING www.google.it (216.58.205.131) 56(84) bytes of data.
...

dove il primo dei due ping, se usate la vm, deve restituire 192.168.123.123.

Il resto della configurazione riguarda Apache e va fatta sulla vm (per chi la usa).

Apache

Per apache la situazione è un po' complessa ma grazie al tutorial  http://servertopic.com/topic/AL1c-using-virtualdocumentroot-only-if-a-suitable-document-root-exists , con qualche aggiustamento, sono arrivato ad una soluzione ottimale (almeno per ora) che descrivo di seguito. Per Linux e Windoze la cosa, aggiustati i percorsi nel modo opportuno, è identica.

Attenzione ad installare ed attivare tutti i moduli di apache necessari, ovvero mod_rewrite per RewriteEngine e mod_vhost_alias per il comando VirtualDocumentRoot (su OSX e Linux con a2enmod).

Nella configurazione di apache occorre disattivare, se è caricato, il file httpd-vhosts.conf e creare e attivare un file dal nome httpd-autovhosts.conf (o qualcosa che vi aggradi).

Su OSX, creiamo il file httpd-autovhosts.conf in extra e modifichiamo httpd.conf per disattiva httpd-vhosts.conf e caricare il nostro, cambiamo inoltre l'utente di avvio di apache, se già non l'abbiamo fatto, con il nostro utente (max nel mio caso), in modo che abbia tutti i diritti necessari per accedere alla nostra Sites (in Linux c'è già un utente default per apache, vedete un po' come preferite muovervi).

Su Linux Ubuntu non c'è una cartella extra ma conf-available e conf-enabled, posizionare il file httpd-autovhosts.conf in conf-available e poi eseguire i comandi

# a2enconf httpd-autovhosts.conf
# a2disconf httpd-vhosts.conf

Nel nuovo file httpd-autovhosts.conf inseriamo il seguente contenuto (dove ovviamente cambierete «max» con il vostro username, o sostituite il percorso rispetto al vostro sistema operativo):

DocumentRoot /Users/max/Sites/localhost

<Directory /Users/max/Sites/>
    Options -MultiViews +FollowSymLinks
    AllowOverride All 
    Require all granted
</Directory>

<VirtualHost *:80>
    ServerName localhost
    VirtualDocumentRoot /Users/max/Sites/localhost

    DirectoryIndex index.php index.html

    LogFormat "%V %h %l %u %t \"%r\" %>s %b" vcommon
    CustomLog "/var/log/apache2/access_log" vcommon
    ErrorLog "/var/log/apache2/error_log"
</VirtualHost>

<VirtualHost *:80>
    ServerName sub.domain
    ServerAlias *.*.*
    VirtualDocumentRoot /Users/max/Sites/%-2.0.%-1.0/%-3

    DirectoryIndex index.php index.html

    RewriteEngine on

    RewriteCond %{HTTP_HOST} ^(.*)\.(.*)\.(.*)$ [NC]
    RewriteCond /Users/max/Sites/%2.%3 !-d
    RewriteRule (.*)  http://localhost/ $1 [P]

    RewriteCond %{HTTP_HOST} ^(.*)\.(.*)\.(.*)$ [NC]
    RewriteCond /Users/max/Sites/%2.%3/%1 !-d
    RewriteCond /Users/max/Sites/%2.%3/www !-d
    RewriteRule (.*)  http://localhost/ $1 [P]

    RewriteCond %{HTTP_HOST} ^(.*)\.(.*)\.(.*)$ [NC]
    RewriteCond /Users/max/Sites/%2.%3/%1 !-d
    RewriteRule (.*) http://%2.%3/$1 [P]

    LogFormat "%V %h %l %u %t \"%r\" %>s %b" vcommon
    CustomLog "/var/log/apache2/access_log" vcommon
    ErrorLog "/var/log/apache2/error_log"
</VirtualHost>

<VirtualHost *:80>
    ServerName bare.domain
    ServerAlias *.*
    VirtualDocumentRoot /Users/max/Sites/%-2.0.%-1.0/www

    DirectoryIndex index.php

    RewriteEngine on

    RewriteCond %{HTTP_HOST} ^(.*)\.(.*)$ [NC]
    RewriteCond /Users/max/Sites/%1.%2 !-d [OR]
    RewriteCond /Users/max/Sites/%1.%2/www !-d
    RewriteRule (.*)  http://localhost/ $1 [P]

    LogFormat "%V %h %l %u %t \"%r\" %>s %b" vcommon
    CustomLog "/var/log/apache2/access_log" vcommon
    ErrorLog "/var/log/apache2/error_log"
</VirtualHost>

che è una versione leggermente modificata di quello riportato nel blog di cui sopra. Testate apache con il solito apachectl -S e se tutto ok riavviatelo. Create la cartella localhost in Sites e qualche sito di prova: dovreste accedere a www anche omettendolo nell'url e se ne indicate uno diverso cercherà una cartella di tal nome nella directory del sito.

Ma non sono ancora contento: vorrei che *automaticamente* anche il log venisse diretto nella cartella del progetto, nel file access_log (per l'error_log è meglio fare riferimento a quello globale di apache). Per separare le righe di log a seconda del sito ho scritto un semplice programmino in awk (è meglio installare con homebrew e usare gawk invece di quello osx) che splitta i log in base alla prima voce della riga. Mettiamo lo script da qualche parte, ad esempio in una directory bin sotto la propria home, e invochiamolo dalla configurazione di apache.

Lo script awk, che ho chiamato splitlog, è

#!/usr/local/bin/awk -f
{
   if (DR == "") DR = "/Users/max/Sites/"
if (FN == "") FN = "access_log"
print DR FN
split($1, outfile, ".")
outfilename = ""
sep = ""
for (i=0; i < 2 && i < length(outfile); i++) {
outfilename = outfile[length(outfile) -i] sep outfilename
sep = "."
}
outfilepath = DR outfilename "/" FN
print $0 >> outfilepath
print $0 | "cat >&2"
fflush()

Ovviamente cambieremo "max" con il nome utente opportuno o anche tutto il percorso se avete fatto altrimenti (in realtà è possibile configurarlo anche direttamente da apache aggiungendo alla riga di comando del CustomLog -v DR=nuovo/path).

Modifichiamo anche la configurazione di apache per tutti i CustomLog in

CustomLog "|/usr/local/bin/awk -f /Users/max/bin/splitlog" vcommon

dove i percorsi sia di awk e splitlog sono stati verificati a console con il comando which. Testiamo e riavviamo apache.

Speriamo che tutto funzioni al primo colpo... ma non è mai così ;)

Nota: ho aggiunto al logformat di apache un > nell'errore, che diventa %>s, in modo da riportare l'effettivo codice in caso di errore, altrimenti nel log escono tutti 200 a fronte di 404.

Infine, per chi vuole avere un colpo d'occhio su tutti i siti in sviluppo e accedervi con un semplice click, si può impostare la index.php in localhost secondo il codice che segue, ovviamente da modificare a piacimento:

<!doctype html>
<?php
$path = str_replace(basename(__DIR__),'', __DIR__);
$dir = scandir($path);
$hosts = [];
foreach($dir as $d) {
  if(strpos($d, '.local') !== false) {
    #echo "<a href='http://$d/'>$d</a>\n";
    $hosts[$d] = [];
    $subdir = scandir($path . $d);
    foreach($subdir as $sd) {
      $subpath = $path . $d . '/' . $sd;
      if(preg_match('/\\./', $sd) === 0 && is_dir($subpath)) {
        $hosts[$d][] = $sd .'.'. $d;
      }
    }
  }
}
?>
<html>
<head>
  <meta charset="UTF-8">
  <title>local dynamic vhosts</title>
  <style>
    * {font-family: Verdana, Helvetica, Arial, sans-serif}
    a:link {text-decoration: none; color: black;}
    a:visited {text-decoration: none; color: black;}
    a:hover {text-decoration: none; color: blue;}
    a:active {text-decoration: none; color: red;}
    h1 {text-align: center; }
    section {
      column-count: 3;
      column-rule: solid 1px lightgray;
    }
    section {
      padding: 1pc;
    }
    section > ul {
      list-style: none;
      padding: 0;
      margin: 0;
    }
    section > ul > li > ul {
      list-style: none;
      padding: 0 0 0 1.5pc;
      margin: 0 0 0 -1.5pc;
      background-color: white;
      text-align: right;
    }
    section > ul > li {
      margin: 0 0 10px 0;
      background-color: lightgray;
      padding: 0 0 0 1.5pc;
      position: relative;
    }
    section > ul > li:before {
      content: "::";
      position: absolute;
      left: 0;
    }
  </style>
</head>
<body>

<h1>local dynamic virtual hosts</h1>
<section>
  <ul>
    <?php foreach($hosts as $h => $subhosts): ?>
      <li><a href=' http://< ;?= $h ?>/'><?= $h ?></a>
        <ul>
          <?php foreach($subhosts as $sh): ?>
            <li><a href=' http://< ;?= $sh ?>/'><?= $sh ?></li>
          <?php endforeach ?>
        </ul>
      </li>
    <?php endforeach ?>
  </ul>
</section>

</body>
</html>

;)