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"; } } */ ?>