A tiny, fast static webserver written in PHP. Handles both HTTP and HTTPS with a single port and server.
#!/usr/bin/env php
<?php
//http://www.youtube.com/watch?v=CjuA0TPVkJc
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');
// Eliminate calls to create_function within loops -- Major memory leak!
function tf_expire($a){if ($a && !$a->destroyed) { if ($a->timeout > time()) { return true; } else { $a->destroy(); } } return false;}
function tf_evictExpired($a){return !(($a["timestamp"] + SRV_CACHETIMEOUT) <= time());}
function tf_evictOldest($a, $b){return $b["timestamp"] - $a["timestamp"];}
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, "tf_expire");
$last_expiration = time();
}
if (($last_eviction + SRV_CACHETIMEOUT) <= time()) {
msg("Handling eviction", 3);
$msgCache = array_filter($msgCache, "tf_evictExpired");
$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, "tf_evictOldest");
$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";
}
}
*/