Revision [17869]

This is an old revision of WikkaGopher made by BrianKoontz on 2007-12-23 06:44:00.

 


Rationale

The idea here is to facilitate the creation and maintenance of gopher content using existing Wikka features. The goal is not to write a new gopher server, but to manage content created by Wikka so that a gopher server can access and serve up Wikka-generated content. (This is similar to using Wikka as an HTMLHandler HTML generator, using Wikka markup to generate and serve HTML content.) There's no reason why Wikka couldn't serve up gopher content as well!

Proof of Concept

As it turns out, I did have a need to satisfy the following:

There's no reason why Wikka couldn't serve up gopher content as well!

I have a repository of files I wanted to serve up, but the machine is internal, and I really don't have a desire to (1) open access to the outside world, (2) serve them with all the overhead associated with a web server such as Apache, or (3) move the files to a machine that is accessible to the outside world. Gopher is a lightweight protocol that is ideal for serving up filesystems in situ, without having to deal with presentation issues or other needless overhead. I thought it would be interesting to have Wikka serve as a "gopher proxy," permitting access to gopher-land without regard to whether or not a user's browser supports the gopher protocol. As a proof of concept, I wrote some code that can access gopher sites, display (in a very rudimentary fashion) the site files and directories, and even download text and binary files.

Please note: This code is very unrefined, and is definitely not for use in a production environment! It is very likely things don't work (in fact, I deliberately failed to implement several gopher item types so that I could focus on just getting something to work), and I seriously doubt it's anywhere near being compliant to RFC 1436. However, it works with my gopher server, and fulfills the rather meager requirements I had.

That said, I offer up my initial hacks and welcome a brave soul who might be willing to step forward and see if they can create a gateway to gopher-land.

System Requirements

You must have a version of PHP that is compiled with the --enable-sockets option. This simply will not work without this option enabled. I believe the socket extensions have been moved to PECL (bleh!) as of PHP 5.3.0, so I doubt this code will work without some modification. I'm running this on my test server with PHP 4.3.10, Apache 2.0, and the latest version of WikkaWiki 1.1.6.4 from the WikkaSVN SVN repository.

Getting Down and Dirty

OK, here it is! It's ugly, unrefined, uncommented, and the error handling doesn't work (because I'm still trying to decide if the client is responsible for displaying error messages, or the underlying classes). But, if you drop these files into your actions/ and handlers/page/ directories, it should work without much modification. At some point, I do plan on tidying things up. Feel free to edit this page (it's a wiki after all) with comments, code, and criticisms. I can handle it all.

Typical usage
{{gopher uri="quux.org"}}


actions/gopher.php
<?php
   /*
    * This program is free software; you can redistribute it and/or
    * modify it under the terms of the GNU General Public License as
    * published by the Free Software Foundation; either version 2 of
    * the License, or (at your option) any later version.
    *
    * This program is distributed in the hope that it will be useful,
    * but WITHOUT ANY WARRANTY; without even the implied warranty of
    * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    * GNU General Public Licence for more details:
    *
    *            http://www.gnu.org/copyleft/gpl.html
    *
    *
    * @author        {@link http://wikkawiki.org/BrianKoontz Brian Koontz} <brian@pongonova.net>
    * @copyright    Copyright (c) 2007, Brian Koontz <brian@pongonova.net>
    */

    include_once('actions/gopherclient/gopherproxy.php');
    include_once('actions/gopherclient/gopherclient.php');
    if(isset($_GET['uri']))
    {
        $vars['uri'] = $_GET['uri'];
    }
    $uri = $this->cleanURL($vars['uri']);
    // Strip protocol
    if(preg_match('/^.*\/\/(.*)$/', $uri, $matches))
    {
        $uri = $matches[1];
    }

    // Separate host from selector
    $selector = '';
    $item_type = '';
    $host = '';
    if(strpos($uri, "/") > 0)
    {
        $fields = explode("/", $uri, 3);
        $host = $fields[0];
        $item_type = $fields[1];
        $selector = $fields[2];
    }
    else
    {
        $host = $uri;
    }
    if(!isset($item_type) || '' == $item_type)
    {
        $item_type = 1;
    }

    // Gopher it!
    $gp = new GopherProxy($host);
    $prefix = $this->href()."/gopher?uri=";
    $gc = new GopherClient($prefix);
    $result = $gp->ProcessRequest($selector);
    $gc->ParseResponse($item_type, $result, $selector);
?>


actions/gopherclient/gopherclient.php
<?php
/********************************************************************
 * gopherclient.php - Client for parsing/displaying gopher responses
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public Licence for more details:
 *
 *            http://www.gnu.org/copyleft/gpl.html
 *
 *
 * @author        {@link http://wikkawiki.org/BrianKoontz Brian Koontz} <brian@pongonova.net>
 * @copyright    Copyright (c) 2007, Brian Koontz <brian@pongonova.net>
 *
 ********************************************************************/


if(!defined('GOPHERCLIENT_GENERAL_ERROR')) define('GOPHERCLIENT_GENERAL_ERROR', 'General GopherClient error');

class GopherClient
{
    var $url_prefix;

    function GopherClient($url_prefix='')
    {
        $this->url_prefix = $url_prefix;
    }

    function ParseResponse($item_type, &$response, $selector='')
    {
        if(!isset($response))
        {
            return $this->ThrowError("Need a response to parse!");
        }

        // Special handling for text and binary files
        // (This should probably be put into its own function)
        $raw_data_item_types = array(0, 5, 9);
        $text_item_types = array(0);
        $binary_item_types = array(5, 9);
        if(TRUE===in_array($item_type, $raw_data_item_types))
        {
            if(TRUE===in_array($item_type, $binary_item_types))
            {
                // We need the selector for this...
                if(!isset($selector) || '' == $selector)
                {
                    return $this->ThrowError("Need the selector!");
                }
                // The following was adapted from code posted by
                // Hillar Aarelaid at
                // http://wikkawiki.org/FilesActionHillar
                $filename = basename($selector);
                if (preg_match("/.*\.(\w+)$/i",$filename,$res))
                    $suffix=$res[1];
                 // Search MIME Type
                 if (!$suffix || $suffix=="" || !$this->config['mime_types']
                    || !$mimes=implode("\n",file($this->config['mime_types'])))
                    $content_type="application/octet-stream";
                 else
                 {
                    if (preg_match("/([A-Za-z.\/-]*).*$suffix/i",$mimes,$result))
                       $content_type=$result[1];
                    else
                       $content_type="application/octet-stream";
                 }
                    header("Pragma: public");
                    header("Expires: 0");
                    header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
                    header("Cache-Control: public");
                    header("Content-Description: File Transfer");

                    Header("Content-Type: ".$content_type);

                    //Force the download
                    Header("Content-Disposition: attachment; filename=".$filename);
                    header("Content-Transfer-Encoding: binary");
                    Header("Content-Length: ".strlen($response));
                    // Header("Connection: close");
                echo $response;
                exit;
            }
            else if(TRUE===in_array($item_type, $text_item_types))
            {
                $response = preg_replace('/\n/', "<br/>\n", $response);
                echo $response;
                return;
            }
            else
            {
                // Better check your item_type arrays...punting on
                // this one, let's hope it can be handled...
            }
        }

        // Explode on \r\n
        $lines = array_filter(explode("\r\n", $response));
        $last = array_pop($lines);
        // Some servers aren't returning a "." as the last line, so we
        // just can't throw that line away
        if(0 == preg_match('/^\.$/', $last))
        {
            array_push($lines, $last);
        }

        foreach($lines as $line)
        {
            $item_type = substr($line, 0, 1);
            $fields = explode("\t", substr($line, 1));
            echo $this->FormatResponseLine($item_type, $fields);
        }
    }

    /********************************************************************
     * fields[0] - Display string
     * fields[1] - Selector
     * fields[2] - Host
     * fields[3] - Port
     ********************************************************************/

    function FormatResponseLine($item_type, $fields)
    {
        $prefix = '';
        switch((string)$item_type)
        {
        case "0":
            $prefix = "[FILE] ";
            break;
        case "1":
            $prefix = "[DIR]  ";
            break;
        case "5":
            $prefix = "[BIN]  ";
            break;
        case "9":
            $prefix = "[BIN]  ";
            break;
        case "i":
            $prefix = '';
            break;          default:
            $prefix = "[UNK]  ";
            break;  
        }          
        $url = $this->url_prefix;
        $url .= rtrim($fields[2], "/");
        if(isset($fields[3]))
        {  
            $url .= ":$fields[3]";
        }      
        $url .= "/";
        $selector = ltrim($fields[1], "/");
        if(!empty($selector))
        {  
            $url .= $item_type."/";
            $url .= $selector;  
        }      
           
        if(empty($prefix))
        {
            return $fields[0]."<br/>\n";
        }
        else
        {
            return $prefix."<a href=\"".$url."\">".$fields[0]."</a><br/>\n";
        }
    }  
           
    function ThrowError($err='')
    {
        return GOPHERCLIENT_GENERAL_ERROR.": ".$err."\n";
    }  
}          
?>          


actions/gopherclient/gopherproxy.php
<?php
/********************************************************************
 * gopherproxy.php - Proxy for accessing gopher sites
 *
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public Licence for more details:
 *
 *            http://www.gnu.org/copyleft/gpl.html
 *
 *
 * @author        {@link http://wikkawiki.org/BrianKoontz Brian Koontz} <brian@pongonova.net>
 * @copyright    Copyright (c) 2007, Brian Koontz <brian@pongonova.net>
 *
 ********************************************************************/


if(!defined('GOPHERPROXY_SOCKET_ERROR')) define('GOPHERPROXY_SOCKET_ERROR', 'Socket error');
if(!defined('GOPHERPROXY_READ_LENGTH')) define('GOPHERPROXY_READ_LENGTH', 1024);
if(!defined('GOPHERPROXY_SELECT_TIMEOUT')) define('GOPHERPROXY_SELECT_TIMEOUT', 5);

class GopherProxy
{
    var $host;
    var $port;   // Default: 70
    var $timeout; // Default: 30 seconds
    var $socket;

    function GopherProxy($host, $port=70, $timeout=30)
    {
        if(strpos($host, ":") > 0)
        {
            $this->host = substr($host, 0, strpos($host, ":"));
            $this->port = substr($host, strpos($host, ":")+1);
        }
        else
        {
            $this->host = $host;
            $this->port = $port;
        }
        $this->timeout = $timeout;
    }

    function _setup_socket($request='')
    {
        $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        if(FALSE===$this->socket)
        {
            return $this->ThrowError();
        }
        if(FALSE===socket_set_nonblock($this->socket))
        {
            return $this->ThrowError("socket_set_noblock() failed");
        }

        //socket_bind($this->socket, '127.0.0.1', 0);

        if(FALSE===socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, 1))
        {
            return $this->ThrowError();
        }

        $time = time();
        while(FALSE===@socket_connect($this->socket, $this->host, $this->port))
        {
            $err = socket_last_error($this->socket);
            if($err == 115 || $err == 114)
            {
                if((time() - $time) >= $this->timeout)
                {
                    return $this->ThrowError("Connection timed out");
                }
                sleep(1);
                continue;
            }
            return $this->ThrowError();
        }
    }

    function _shutdown_socket()
    {
        socket_close($this->socket);
    }

    function ProcessRequest($request='')
    {
        $this->_setup_socket();

        if(FALSE===$this->socket)
        {
            return $this->ThrowError();
        }

        if(FALSE===socket_set_block($this->socket))
        {
            return $this->ThrowError();
        }

        if(FALSE===socket_write($this->socket, "$request\r\n"))
        {
            return $this->ThrowError();
        }

        $read = array($this->socket);
        $buffer = '';
        while(true)
        {
            $select = socket_select($read, $write=NULL, $except=NULL, GOPHERPROXY_SELECT_TIMEOUT);
            if(FALSE !== $select && $select > 0)
            {
                $readbuf = socket_read($this->socket, GOPHERPROXY_READ_LENGTH);
                if(''==$readbuf)
                {
                    break;
                }
                while(0 != strlen($readbuf))
                {
                    $buffer .= $readbuf;
                    $readbuf = socket_read($this->socket, GOPHERPROXY_READ_LENGTH);
                }
            }
            else
            {
                return $this->ThrowError();
                break;
            }
        }
        $this->_shutdown_socket();
        return $buffer;
    }

    function ThrowError($err='')
    {
        $this->_shutdown_socket();
        $this->socket = NULL;
        if(empty($err))
        {  
            $err = socket_strerror(socket_last_error());
        }
        return GOPHERPROXY_SOCKET_ERROR.": ".$err."\n";
    }
}  
?>  


handlers/page/gopher.php
<div class="page">
<?php
    include_once("actions/gopher.php");
?>
</div>
There are 3 comments on this page. [Show comments]
Valid XHTML :: Valid CSS: :: Powered by WikkaWiki