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($message, 5);
}

$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, $errstr, STREAM_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_CACHETIMEOUT: NULL));
        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->socket, true);
                                        list($cur->host, $cur->port) = explode(":", $cur->peer, 2);
                                        stream_set_blocking($cur->socket, false);
                                        $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($clients, create_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($msgCache, create_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, -1, 1) == ";") {
                                                $value = sprintf("%s %s", $value, trim($tmp[$key + 1]));
                                unset($tmp[$key + 1]);
                                }
                                $value = explode(":", $value, 2);
                                $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($msg, strlen($content), SRV_SERVER, $content);
                        $this->write($msg);
                        if (count($msgCache) > SRV_MAXCACHE) {
                                // Evict oldest
                                $tmp = $msgCache;
                                usort($tmp, create_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->socket, 11, STREAM_PEEK);
                        $this->secure = stream_socket_enable_crypto($this->socket, TRUE, STREAM_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->socket, 4096);
                        else    $buf = fread($this->socket, 1024);
                        $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, $bytes, strlen($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";
}
}
*/
?>