PS-HTTP

What is a web server? Most people would think about a web server

as a program such as Apache, without really knowing what a web server really is. A web server is simply a program implementing the HTTP protocol. This protocol makes it possible for a web server (or any other kind of program) to request a file from the web server. The web server then tries to read this file and return the file data to the client. Pretty simple, right? Of course a real web server does more than this, but that is extensions to the very basic functionality described above.

What languages can you then use to write a web server? The simple answer is that pretty much any language will do. You need to be able to open files and it is very handy if the language has string operators that help parsing the HTTP protocol. Of course some sort of network support is needed. But if we use some poetic freedom the network part and the rest of the server can be separated. This can for example be made by using the UNIX inetd service, which listens to a TCP port and then spawns the server on incoming connections. The server can then operate on standarn input and output devices.

PS-HTTPD is a web server written in PostScript. It all started as a coffee table discussion at my work. We first talked about web servers and how everything seems to get one. Divided from that we later on discussed the new monster Xerox printer we just got installed. I claimed that it probably has a web server. That made me wonder if it would be possible to make a simple web server in PostScript, the standard printer language from Adobe. After a bit too many hours of head scratching while reading PostScript references and coding it showed out that it was indeed possible - PS-HTTPD was born!

What can you then learn from making something stupid as this? First I learned a lot more about the HTTP protocol than I knew before. That knowledge has been very useful in the last ten years. I also learned much more about PostScript. I didn't really think that I ever would get use of that knowledge outside PS-HTTPD, but I was proven wrong. In my current work, at Opera Software, we needed a new printer driver for the UNIX versions of the Opera Browser. So about ten years after starting writing PS-HTTP I actually got use of my PostScript knowledge at work! It was very strange but really fun!

Below you can find the source code for PS-HTTPD. It is ofcourse released under the GPL license, so feel free to use it and do more strange stuff with it!

Here follows the PostScript sourcecode.

%!PS

%===================================================
% PS-HTTPD V1.6
% Copyright 2000-2010 Anders Karlsson, pugo@pugo.org
% License: GNU General Public License
%===================================================

% This dictionary maps between extensions and mime-types
% Observe that "html" isn't part of this dict, that's because
% it's default in print_header.

/extensiondict 29 dict def
extensiondict begin
    /jpg  (Content-type: image/jpeg\n) def
    /jpeg (Content-type: image/jpeg\n) def
    /gif  (Content-type: image/gif\n) def
    /png  (Content-type: image/png\n) def
    /tif  (Content-type: image/tiff\n) def
    /tiff (Content-type: image/tiff\n) def
    /txt  (Content-type: text/plain\n) def
    /css  (Content-type: text/css\n) def
    /ps   (Content-type: application/postscript\n) def
    /pdf  (Content-type: application/pdf\n) def
    /eps  (Content-type: application/postscript\n) def
    /tar  (Content-type: application/x-tar\n) def
    /gz   (Content-type: application/x-tar\n) def
    /tgz  (Content-type: application/x-tar\n) def
    /rpm  (Content-type: application/x-rpm\n) def
    /zip  (Content-type: application/zip\n) def
    /mp3  (Content-type: audio/mpeg\n) def
    /mp2  (Content-type: audio/mpeg\n) def
    /mid  (Content-type: audio/midi\n) def
    /midi (Content-type: audio/midi\n) def
    /wav  (Content-type: audio/x-wav\n) def
    /au   (Content-type: audio/basic\n) def
    /ram  (Content-type: audio/x-pn-realaudio\n) def
    /ra   (Content-type: audio/x-realaudio\n) def
    /mpg  (Content-type: video/mpeg\n) def
    /mpeg (Content-type: video/mpeg\n) def
    /qt   (Content-type: video/quicktime\n) def
    /mov  (Content-type: video/quicktime\n) def
    /avi  (Content-type: video/x-msvideo\n) def
end


% Concatenate two strings
/concatstr     %  string string - string
{
    exch dup length 2 index length add string
    dup dup 4 2 roll copy length 4 -1 roll putinterval
} bind def


% read file /infile and send it to %stdout 
/get_file
{
    { % loop
        infile inbuff readstring
        { stdout exch writestring }
        { stdout exch writestring infile closefile exit } ifelse
    } bind loop
    flush
} bind def


% Return extension of file on stack
/get_extension   % (filepath.ext) -- (bool) (ext)
{
    dup
    { % loop
        (.) search
        { pop pop }
        { exit } ifelse
    } loop
    exch 1 index ne
} bind def


% Print a HTTP-header
/print_header   % (filename) (size) --
{
    stdout persistent {(HTTP/1.1 200 OK\n)} {(HTTP/1.0 200 OK\n)} ifelse writestring
    stdout (MIME-Version: 1.0\n) writestring
    stdout (Server: PS-HTTPD/1.6\n) writestring
    stdout (Content-Length: ) writestring
    stdout exch 16 string cvs writestring
    stdout (\n) writestring

    get_extension
    {
        % If the extension exists in dictionary, then use it,
        % otherwise hope that text/html is good enough.
        dup extensiondict exch known
        { extensiondict exch get stdout exch writestring }
        { pop stdout (Content-type: text/html\n) writestring } ifelse
    }
    { % Couldn't get extension, guess it's text/html
        pop stdout (Content-type: text/plain\n) writestring
    } ifelse

    stdout (\n) writestring flush
} bind def


% read command from stdin and define it to /command
/command_read
{
    /stdin (%stdin) (r) file def
    4096 string
    {
        % read lines until empty line
        stdin 1024 string readline pop
        dup () eq { pop exit  } { concatstr } ifelse
    } loop

    /command exch def
} bind def


% See if command wants persistent connection
/command_check_persistence
{
    % Check if we should do HTTP 1.1 persistent connections
    command (HTTP/1.1) search
    {
        pop pop pop
        command (Connection: close) search     % Check for Connection: close
        { pop pop pop  /persistent false def }
        { pop          /persistent true def  } ifelse
    }
    { pop /persistent false def } ifelse
} bind def


% Parse the HTTP-command read from user
/command_respond
{
    command token
    {
        (GET) eq
        {
            ( ) search
            {
                exch pop exch pop
                root exch concatstr            % build path
                /filename exch def             % define filename and clean stack

                filename filename length 1 sub 1 getinterval (/) eq
                { filename (index.html) concatstr
                    /filename exch def } if      % add index.html

                filename (..) search           % Check if user tries to use ".."
                { stdout err_400 quit } if pop

                /infile filename (r) file def    % open file
                filename infile bytesavailable print_header
                get_file
            } if
        } if
    } if
} bind def


% Error messages
/err_400 (400 Bad Request.\n\n) def
/err_404 (404 Page not found.\n\n) def


% Redefine handleerror in errordict to quit on all errors.
% Otherwise it will be possible to telnet and get a postscript-prompt
errordict begin
/handleerror { stdout err_400 writestring quit } bind def 
end

% Root-path (root of WWW-pages)
/root (/home/pugo/projekt/pshttpd/www) def

% Buffer used to read data from file. Around 2048 bytes should be good.
/inbuff 2048 string def

% Init environment
/stdout (%stdout) (w) file def
/command () def


% Read a command from the server and parse result. Loop until persistent close.
{
    command_read
    command_check_persistence
    command_respond

    persistent not { exit } if % exit if not persistent, otherwise loop again
} loop

quit