PHP under Tclhttpd

NB On several occasions, I've had the need to run/use some useful php (no-flames..) apps/scripts, but did not want to install a full-blown Apache distro...., much prefering to use tclhttpd;-) With these hacks below, PHP seems to work as a cgi under TclHttpd.

Edit cgi.tcl in the lib directory.., specifically CgiSpawn and CgiClose, essentially setting/unsetting env(REDIRECT_STATUS). This is needed so PHP does not complain about security breach and stop execution.

CMcC 20040805 - this bit below is troubling and probably not necessary. If you [lappend Cgi(env-pass) REDIRECT_STATUS] and set REDIRECT_STATUS to 1 before you start tclhttpd server process, then your variable will be passed to the cgi. Could you please test if that works, and if so, remove this code below? If you need to, you could set/unset ::env(REDIRECT_STATUS) within the mime type scripts.

I've put a suggested implementation below the original code ... someone try it out please?

 proc CgiSpawn {sock script} {
    upvar #0 Httpd$sock data
    global env Cgi
    ##Nikos:This env var is needed to run php cgi securely, in directories under docroot
    set env(REDIRECT_STATUS) 1

    if {$Cgi(cgi) >= $Cgi(maxcgi)} {
        Httpd_Error $sock 504  "Too many CGI's"
        return
    }
    incr Cgi(cgi)

    # for GET queries, pass the query as an argument to the cgi script
    if {$data(proto) == "POST"} {
        set arglist ""
    } else {
        set arglist $data(query)
    }
    set pwd [pwd]
    cd [file dirname $script]
    if {[catch {CgiExec $script $arglist} fd]} {
        cd $pwd
        Httpd_Error $sock 400 $fd
        incr Cgi(cgi) -1
        return
    }
    cd $pwd
    Count cgihits
    set data(infile) $fd        ;# So close happens in Httpd_SockClose
    set data(header) 0                ;# Have not read header yet
    set data(headerlist) {}        ;# list of read headers
    set data(headercode) "200 data follows"        ;# normal return
    fconfigure $fd -blocking 0

    # Set up a timer in case it hangs

    catch {after cancel $data(cancel)}
    set data(cancel) [after $Cgi(timeout) CgiCancel $fd $sock]

    if {$data(proto) == "POST"} {
        fconfigure $fd -translation binary
        if {$data(count) == 0} {

            # Either there was no POST data, or we are inside a domain
            # that automatically read the POST data into data(query) already
            # Errors appear here because of the non-blocking writes.

            catch {
                puts -nonewline $fd $data(query)
                flush $fd
            }
        } else {

            # Pump the query data to the CGI process in the background
            # We set up fileevents after this finishes, so return now

            # fcopy bug in Tcl 8.3.2 and Tcl 8.4a2 and all previous versions 
            # prevents this from working reliably on large amounts of POST data
            # fcopy $sock $fd -command [list CgiCopyDone $sock $fd] -size $data(count)
            fileevent $sock readable [list CgiCopyPost $sock $fd]
            return
        }
    }

    CgiCopyDone $sock $fd $data(count) ""
 }
 proc CgiClose {fd sock {bytes {}} {error {}}} {
    global Cgi
    upvar #0 Httpd$sock data
    
    catch {after cancel $data(cancel)}
    incr Cgi(cgi) -1
    ##Nikos:This env var was needed to run php cgi securely, now we unset it..
        catch {unset ::env(REDIRECT_STATUS)}
    if {![info exists data(header)]} {
        Httpd_Error $sock 204
    } else {
        Httpd_SockClose $sock 1
    }
    if {[string length $error] > 0} {
        Log $sock CgiClose $error
    }
   
 }

glennj: you unser your env.var whether or not data(header) exists, so instead of doing it in 3 places, why don't you pull it out of the if blocks and place it after your incr call?


NB Done...


Add this to the custom directory

 ###Nikos:Hack to add php cgi capabilities into tclhttpd...

 ##Add the mime types 
 Mtype_Add application/x-php .php
 Mtype_Add application/x-php .php3
 Mtype_Add application/x-php .phtml

 ###Edit this your path, Win used to need full path; now it seems not to
 #set Cgi(php) "c:/php/php.exe"        ;# For .php
 set Cgi(php) "php"                        ;# For .php

 proc Doc_application/x-php {path suffix sock} {
    upvar #0 Httpd$sock data
    Url_Handle [list CgiHandle $data(url) {} $path] $sock
 }

 proc Doc_application/x-php {path suffix sock} {
    upvar #0 Httpd$sock data
    Url_Handle [list CgiHandle $data(url) {} $path] $sock
 }

 puts "PHP CGI Stuff loaded"

 puts {You need to register your php cgi directories with Cgi_Directory}

Caveats:

  • There's probably a better point to hook this into the cgi library, but at least this'll get someone started
  • If php-dir is outside the docroot (which, I think is a no-no for cgi), and is registered as a virtual directory, then PHP complains of "No Input File"
  • the example above can only work under Unix, because Windows needs to be told which interpreter (In this case $Cgi(php) to run over files with .php extensions. There's a three line patch to cgi.tcl, below.

For Windows only: cgi.tcl line 372 (inside the switch):

   .php {
      return [open "|[list $Cgi(php) $script] $arglist" r+]
   }

CMcC here's what I was suggesting - put this in custom, and don't modify cgi.tcl, does this give the desired results? If it does, please remove the above ... unnecessary modification of cgi.tcl gives me dyspepsia.

 ##Add the mime types
 Mtype_Add application/x-php .php
 Mtype_Add application/x-php .php3
 Mtype_Add application/x-php .phtml

 ###Edit this your path, Win used to need full path; now it seems not to
 #set Cgi(php) "c:/php/php.exe"        ;# For .php
 set Cgi(php) "php"                        ;# For .php
 lappend Cgi(env-pass) REDIRECT_STATUS

 proc Doc_application/x-php {path suffix sock} {
    upvar #0 Httpd$sock data
    set ::env(REDIRECT_STATUS) 1
    Url_Handle [list CgiHandle $data(url) {} $path] $sock
    unset ::env(REDIRECT_STATUS)
 }

 proc Doc_application/x-php {path suffix sock} {
    upvar #0 Httpd$sock data
    set ::env(REDIRECT_STATUS) 1
    Url_Handle [list CgiHandle $data(url) {} $path] $sock
    unset ::env(REDIRECT_STATUS)
 }

 puts "PHP CGI Stuff loaded"

 puts {You need to register your php cgi directories with Cgi_Directory}

LES on 20040929: it does not work for me on Windows 98. I click my index.php file and only get its content in plain text. Will try Linux later... :-(

Les, did the original posted version work for you? I wonder whether that bit about registering your php directory as a Cgi directory might make a difference. -- CMcC

LES After a Tcl Chatroom session, CMcC came up with the solution. So the two code snippets above make PHP work with Tclhttpd.

CMcC it exposes a problem with Tclhttpd under Windows, though, which is that cgi processing has hard-coded mapping from extension to interpreter. Really, it should be more flexible. I'll submit an RFE so we remember that something could be done, but I'm not going to do the work, because I don't use Windows and hardly ever use CGI. MHo: This is indeed a big problem: most of the available perl-CGI-scripts found on the internet have the extension .CGI (not .PL) which, by default, is interpreted as a TCL-script in tclhttpd. And, if one change this behaviour in cgi.tcl (simple enough), existing tcl-scripts with .CGI-extension won't work anymore unless renamed....


schlenk On Windows the following worked:

Setting up PHP:

  • Download the PHP binaries for windows from http://www.php.net/downloads.php
  • Unzip the zip file in c:/php
  • Copy the c:/php.ini-recommended file to c:/php/php.ini
  • Edit the php.ini file and set the doc_root to your tclhttpd doc_root

Setting up Tclhttp:

  • Add the cgi.tcl fix from above to tclhttp/lib/cgi.tcl
  • Save the following script to tclhttp/custom/php.tcl
 ##Add the mime types
 Mtype_Add php   application/x-php
 Mtype_Add php3  application/x-php 
 Mtype_Add phtml application/x-php 

 ###Edit this your path, Win used to need full path; now it seems not to
 set Cgi(php) "c:/php/php-cgi.exe"        ;# For .php
 #set Cgi(php) "php"                        ;# For .php
 lappend Cgi(env-pass) REDIRECT_STATUS

 proc Doc_application/x-php {path suffix sock} {
    upvar #0 Httpd$sock data
    set ::env(REDIRECT_STATUS) 1
    Url_Handle [list CgiHandle $data(url) {} $path] $sock
    unset ::env(REDIRECT_STATUS)
 }

 puts "PHP CGI Support loaded"
  • Place this example script in tclhttp/htdocs/phptest.php
 <?php
   echo("Hello World");
 ?>
  • Start Tclhttpd an point the browser to your the phptest.php file...

zbang On *nix, in addition to the custom code above, I had to modify lib/tclhttpd3.5.1/cgi.tcl so that it would actually use the Cgi array.

 proc CgiExec {script arglist} {
    global tcl_platform
    global Cgi

    set prog ""
    set extn [string trim [file extension $script] "."]
    if { $extn != "cgi" } {
        if { [info exists Cgi($extn)] } { set prog "$Cgi($extn) " }
    }

    switch -- $tcl_platform(platform) {
        unix {
            if {[file exists /dev/stdout]} {

                # This trick "2> /dev/stdout" fails if you start the process in
                # the background under an xterm, then quit the xterm.  In that case
                # the open raises an error.

                if { ! [catch {open "|$prog[list $script] $arglist 2> /dev/stdout" r+} pipe]} {
                    return $pipe
                }
            }

            # We use cat to merge the stderr and stdout

            return [open "|$prog[list $script] $arglist |& cat" r+]
        }

I didn't try this on windows, but it might be more correct to change the windows section to use the $prog variable, too.

A couple of other things:

If you register the directory containing the php scripts as a cgi dir with Cgi_Directory, the Doc_application/x-php proc won't be called at all, as Doc_$mtype is a feature of the Doc domain handler, not the Cgi domain handler.

It's not clear whether the file extensions as registered with Mtype_Add should have a leading period. It looks like they should, based on what's in the MimeType array. There is code in a number of places to remove that period if present.


zbang One last thing: there are some things that php just won't do when run from a unix command line (as this setup does). For one thing, some of the global variables (i.e. $_SERVER'PHP_SELF') won't be set. You can see these by making a page of:

 <?php
 phpinfo();
 ?>

I've also seen problems in the parsing of query string values into php variables. I wasn't able to work around those in the time available. (Sometime, I might make a php module for the tcl web server. In my copious free time.)


Another way to achieve the same thing would be to scatter some random delays in the Tcl code.


See also PHP and Tclhttpd