Note: You are viewing an old version of this page. View the current version.

A tiny, fast static webserver written in PHP. Handles both HTTP and HTTPS with a single port and server.

#!/usr/bin/env php
<?php

define
('SRV_VERBOSITY',1);
define('SRV_MAXCONN',5); // Max number of simultaneous connections to handle
define('SRV_CONNTIMEOUT'2); // Seconds till unfinished connections time out.
define('SRV_MAXCACHE'5); // Max number of cached responses to keep around.
define('SRV_CACHETIMEOUT'2); // Seconds till response cache eviction occurs.
define('SRV_MSGSTORE'"htdocs"); // Template directory.
define('SRV_DFLTMSG'sprintf('%s/%s'SRV_MSGSTORE"default.html")); // Default template.
define('SRV_SERVER''PartyVan/0.1a (GS/OS 6.0.2; AppleIIgs)'); // Server identification.
define('SRV_LISTEN''tcp://127.0.0.44:80');


function 
msg($message$verb 0){
        if(
$verb <= SRV_VERBOSITY)
                
printf("msg: %s\n"$message);
}
function 
debug(){
        
ob_start();
        
$vars func_get_args();
        
call_user_func_array("var_dump"$vars);
        
$message ob_get_contents();
        
ob_end_clean();
        
msg($message5);
}

$context stream_context_create();

stream_context_set_option($context'ssl''local_cert''./server.pem');
stream_context_set_option($context'ssl''allow_self_signed'true);
stream_context_set_option($context'ssl''verify_peer'false);

// Create the server socket
$server stream_socket_server(SRV_LISTEN$errno$errstrSTREAM_SERVER_BIND|STREAM_SERVER_LISTEN$context);

$clients = Array();
$msgCache = Array();

debug($server$clients$insecure_msg$secure_msg);

$last_expiration time();
$last_eviction time();

while(
true){
        
// Populate read/write/except arrays
        
$read = Array();
        
$write = Array();
        
$except NULL;
        
$timeout = (count($clients)? SRV_CONNTIMEOUT: (count($msgCache)? SRV_CACHETIMEOUTNULL));
        if(
count($clients) <= SRV_MAXCONN)
                
$read[] = $server;
        foreach(
$clients as $cur){
                if (!
$cur->destroyed)
                        
$read[] = $cur->socket;
                if(!
$cur->destroyed && strlen($cur->buf_send))
                        
$write[] = $cur->socket;
        }
        
msg("Selecting",5);
        
msg("Read-write"5);
        
debug($read$write);
        
msg("Clients"5);
        
debug($clients);
        
msg("Cache"5);
        
debug($msgCache);

        if(
stream_select($read$write$except$timeout)){
                if(
$read){
                        
msg("Handling read"5);
                        foreach(
$read as $current){
                                if(
$current === $server){
                                        
// New connection
                                        
$cur = new connection;
                                        
$cur->socket stream_socket_accept($server);
                                        
$cur->peer stream_socket_get_name($cur->sockettrue);
                                        list(
$cur->host$cur->port) = explode(":"$cur->peer2);
                                        
stream_set_blocking($cur->socketfalse);
                                        
$cur->request '';
                                        
$cur->timeout time() + SRV_CONNTIMEOUT;
                                        
$clients[$cur->socket] =& $cur;
                                        
msg(sprintf("Req: %s"$cur->peer), 3);
                                        unset(
$cur);
                                }else{
                                        
// Handle something that needs reading
                                        
if ($cur =& $clients[$current]) {
                                                
$res $cur->recv();
                                                if(
$res === FALSE){
                                                        
// Connection closed. Clean up.
                                                        
$cur->destroy();
                                                        unset(
$clients[$current]);
                                                }
                                        }
                                        unset(
$cur);
                                }
                        }
                }
                if(
$write){
                        
msg("Handling write"5);
                        foreach(
$write as $current){
                                if (
$cur =& $clients[$current]) {
                                        
$res $cur->send();
                                        if(
$res === FALSE || $cur->eof()){
                                                
$cur->destroy();
                                                unset(
$clients[$current]);
                                        }
                                }
                                unset(
$cur);
                        }
                }
                if(
$except){
                        
msg("Handling except"5);
                        
// noop
                
}
        }
        if ((
$last_expiration SRV_CONNTIMEOUT) <= time()) {
                
msg("Handling expiry"3);
                
$clients array_filter($clientscreate_function('$a''if ($a && !$a->destroyed) { if ($a->timeout > time()) { return true; } else { $a->destroy(); } } return false;'));
                
$last_expiration time();
        }
        if ((
$last_eviction SRV_CACHETIMEOUT) <= time()) {
                
msg("Handling eviction"3);
                
$msgCache array_filter($msgCachecreate_function('$a''return !(($a["timestamp"] + SRV_CACHETIMEOUT) <= time());'));
                
$last_eviction time();
        }
}
fclose($server);

class 
server {
        function 
handleRequest(){
                global 
$msgCache;
                
msg("Handling request"3);

                
// Parse the request
                
$this->request str_replace("\r\n""\n"$this->request);
                
$tmp explode("\n"$this->request);
                
debug($tmp);
                
$request array_shift($tmp);
                foreach(
$tmp as $key => $value) {
                        
$value trim($value);
                        if (
$value) {
                                if (
substr($value, -11) == ";") {
                                                
$value sprintf("%s %s"$valuetrim($tmp[$key 1]));
                                unset(
$tmp[$key 1]);
                                }
                                
$value explode(":"$value2);
                                
$tmp[strtoupper(trim($value[0]))] = trim($value[1]);
                        }
                        unset(
$tmp[$key]);
                }
                
$tmp['REQUEST'] = $request;
                
debug($tmp);
                
$this->request $tmp;
                unset(
$tmp);

                if (isset(
$msgCache[$peer])) {
                        
msg("msgCache hit"5);
                        
$this->write($msgCache[$peer]['msg']);
                        
$msgCache[$peer]['timestamp'] = time();
                        
$this->close();
                } else {
                        
msg("msgCache miss"5);
                        
$msg = <<<EOF
HTTP/1.0 200 B&
Content-type: text/html; charset=iso-8859-1
Content-length: %u
Connection: close;
Pragma: no-cache
Cache-Control: no-cache
Expires: Wed, 31 Dec 1969 23:59:59 GMT
Server: %s

%s
EOF;
                        
$msg str_replace(Array("\r\n""\n"), Array("\n""\r\n"), $msg);
                        
$file sprintf('%s/%s.html'SRV_MSGSTORE$this->host);
                        
debug($file);
                        if (
file_exists($file)) {
                                
$content file_get_contents($file);
                        } else {
                                
$content file_get_contents(SRV_DFLTMSG);
                        }
                        
$msg sprintf($msgstrlen($content), SRV_SERVER$content);
                        
$this->write($msg);
                        if (
count($msgCache) > SRV_MAXCACHE) {
                                
// Evict oldest
                                
$tmp $msgCache;
                                
usort($tmpcreate_function('$a, $b''return $b["timestamp"] - $a["timestamp"];'));
                                
$tmp array_keys($tmp);
                                unset(
$msgCache[$tmp[0]]);
                                unset(
$tmp);
                        }
                        
$msgCache[$this->host] = Array('msg' => $msg'timestamp' => time());
                        
$this->close();
                }
        }

        function 
isSecure(){
                
msg("FIXME: server::isSecure"3);
        }
        function 
getRequest(){
                
msg("FIXME: server::getRequest"3);
        }
        function 
eof(){
                
msg("FIXME: server::eof"3);
        }
        function 
read(){
                
msg("FIXME: server::read"3);
        }
        function 
recv(){
                
msg("FIXME: server::recv"3);
        }
        function 
write(){
                
msg("FIXME: server::write"3);
        }
        function 
send(){
                
msg("FIXME: server::send"3);
        }
        function 
close(){
                
msg("FIXME: server::close"3);
        }
        function 
destroy(){
                
msg("FIXME: server::destroy"3);
        }
}

class 
connection extends server {
        var 
$socket;
        var 
$peer '';
        var 
$host '';
        var 
$port 0;
        var 
$request '';
        var 
$bytes_rcvd 0;
        var 
$bytes_sent 0;
        var 
$buf_recv '';
        var 
$buf_send '';
        var 
$timeout 0;
        var 
$allowSecure false;
        var 
$secure false;
        var 
$finished false;
        var 
$destroyed false;

        function 
isSecure(){
                return 
$this->secure;
        }
        function 
getRequest(){
                return 
$this->request;
        }
        function 
eof(){
                return 
feof($this->socket) || ($this->finished && !$this->buf_send);
        }
        function 
recv(){
                if(
$this->eof())
                        return 
false;

                if(
$this->allowSecure && (!$this->request && !$this->buf_recv && !$this->secure)){
                        
// Try to enable SSL
                        // Workaround to save buffer in case this fails
                        
$this->buf_recv stream_socket_recvfrom($this->socket11STREAM_PEEK);
                        
$this->secure stream_socket_enable_crypto($this->socketTRUESTREAM_CRYPTO_METHOD_SSLv23_SERVER);
                        if(
$this->secure)
                                
$this->buf_recv ''// Clear the buffer on successful negotiation
                        
$buf = -1;
                        
msg((($this->secure)? "Enabled SSL""SSL negotiation failed."), 5);
                }else{
                        
// Regular data to receive
                        
if(!$this->request)
                                
$buf fgets($this->socket4096);
                        else    
$buf fread($this->socket1024);
                        
$this->bytes_rcvd+= strlen($buf);
                        
$this->buf_recv.= $buf;
                        
msg(sprintf("Received %d bytes"strlen($buf)), 5);
                        if(!
$this->request && rtrim($buf"\r\n") == ''){
                                
// Blank line, signals end of request
                                
$this->request $this->buf_recv;
                                
$this->buf_recv '';
                                
$this->handleRequest();
                        }
                }
                return 
strlen($buf);
        }
        function 
write($data){
                if(
$this->eof() || $this->finished)
                        return 
false;
                
$this->buf_send.= $data;
                return 
strlen($data);
        }
        function 
send(){
                if(
$this->eof())
                        return 
false;
                
$bytes fwrite($this->socket$this->buf_send);
                
$this->buf_send substr($this->buf_send$bytesstrlen($this->buf_send) - $bytes);
                
$this->bytes_sent+= $bytes;
                
msg(sprintf("Sent %d bytes"$bytes), 5);
                return 
$bytes;
        }

        function 
close(){
                
msg("Scheduling descriptor for close"5);
                
$this->finished true;
        }
        
/* destroy - Close any subprocesses and clean up. */
        
function destroy(){
                
msg("Destroying $this"5);
                
$this->close();
                
$this->destroyed true;
                
fclose($this->socket);
                
msg(sprintf("'%s' %s \"%s\" (%s) i:%u/o:%u"strftime("%Y-%m-%d-%H:%M:%S"), $this->peer$this->request['REQUEST'], $this->request['HOST'], $this->bytes_rcvd$this->bytes_sent));
                unset(
$this);
        }
}


/*


if( $client )
{
// Read until double CRLF
while( !preg_match('/?\n?\n/', $buffer) )
$buffer .= htmlspecialchars(fread($client, 2046));
// Respond to client
fwrite($client,  "HTTP/1.1 200 OK \n"
. "Connection: close\n"
. "Content-Type: text/html\n"
. "\n"
. "Hello World! " . microtime(true)
. "<pre>{$buffer}</pre>");
fclose($client);
} else {
print "error.\n";
}
}
*/