nuttx/netutils/thttpd/thttpd_cgi.c

942 lines
26 KiB
C
Raw Normal View History

/****************************************************************************
* netutils/thttpd/thttpd_cgi.c
* CGI support
*
* Copyright (C) 2009 Gregory Nutt. All rights reserved.
* Author: Gregory Nutt <spudmonkey@racsa.co.cr>
*
* Derived from the file libhttpd.c in the original THTTPD package:
*
* Copyright <EFBFBD> 1995,1998,1999,2000,2001 by Jef Poskanzer <jef@mail.acme.com>.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
****************************************************************************/
/****************************************************************************
* Included Files
****************************************************************************/
#include <nuttx/config.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <debug.h>
#include <nuttx/symtab.h>
#include <nuttx/binfmt.h>
#include <net/uip/thttpd.h>
#include "config.h"
#include "libhttpd.h"
#include "thttpd_alloc.h"
#include "thttpd_strings.h"
#include "timers.h"
#include "fdwatch.h"
#if defined(CONFIG_THTTPD) && defined(CONFIG_THTTPD_CGI_PATTERN)
/****************************************************************************
* Pre-processor Definitions
****************************************************************************/
/****************************************************************************
* Private Types
****************************************************************************/
enum cgi_outbuffer_e
{
CGI_OUTBUFFER_READHEADER = 0, /* Reading header from HTTP client */
CGI_OUTBUFFER_HEADERREAD, /* Header has been read */
CGI_OUTBUFFER_HEADERSENT, /* Header has been sent to the CGI program */
CGI_OUTBUFFER_READDATA, /* Transferring data from CGI to client */
CGI_OUTBUFFER_DONE, /* Finished */
};
struct cgi_outbuffer_s
{
enum cgi_outbuffer_e state; /* State of the transfer */
char *buffer; /* Allocated I/O buffer */
size_t size; /* Size of the allocation */
size_t len; /* Amount of valid data in the allocated buffer */
};
/****************************************************************************
* Private Function Prototypes
****************************************************************************/
static void create_environment(httpd_conn *hc);
static char **make_argp(httpd_conn *hc);
static inline int cgi_interpose_input(httpd_conn *hc, int wfd, char *buffer);
static inline int cgi_interpose_output(httpd_conn *hc, int rfd, char *inbuffer,
struct cgi_outbuffer_s *hdr);
static int cgi_child(int argc, char **argv);
/****************************************************************************
* Private Functions
****************************************************************************/
/* Set up environment variables. Be real careful here to avoid
* letting malicious clients overrun a buffer. We don't have
* to worry about freeing stuff since we're a sub-task.
*/
static void create_environment(httpd_conn *hc)
{
char *cp;
char buf[256];
setenv("PATH", CONFIG_THTTPD_CGI_PATH, TRUE);
#ifdef CGI_LD_LIBRARY_PATH
setenv("LD_LIBRARY_PATH", CGI_LD_LIBRARY_PATH, TRUE);
#endif /* CGI_LD_LIBRARY_PATH */
setenv("SERVER_SOFTWARE", CONFIG_THTTPD_SERVER_SOFTWARE, TRUE);
/* If vhosting, use that server-name here. */
#ifdef CONFIG_THTTPD_VHOST
if (hc->vhostname)
{
cp = hc->vhostname;
}
else
#endif
{
cp = hc->hs->hostname;
}
if (cp)
{
setenv("SERVER_NAME", cp, TRUE);
}
setenv("GATEWAY_INTERFACE", "CGI/1.1", TRUE);
setenv("SERVER_PROTOCOL", hc->protocol, TRUE);
(void)snprintf(buf, sizeof(buf), "%d", (int)CONFIG_THTTPD_PORT);
setenv("SERVER_PORT", buf, TRUE);
setenv("REQUEST_METHOD", httpd_method_str(hc->method), TRUE);
if (hc->pathinfo[0] != '\0')
{
char *cp2;
size_t l;
(void)snprintf(buf, sizeof(buf), "/%s", hc->pathinfo);
setenv("PATH_INFO", buf, TRUE);
l = strlen(httpd_root) + strlen(hc->pathinfo) + 1;
cp2 = NEW(char, l);
if (cp2)
{
(void)snprintf(cp2, l, "%s%s", httpd_root, hc->pathinfo);
setenv("PATH_TRANSLATED", cp2, TRUE);
}
}
(void)snprintf(buf, sizeof(buf), "/%s",strcmp(hc->origfilename, ".") == 0 ? "" : hc->origfilename);
setenv("SCRIPT_NAME", buf, TRUE);
if (hc->query[0] != '\0')
{
setenv("QUERY_STRING", hc->query, TRUE);
}
setenv("REMOTE_ADDR", httpd_ntoa(&hc->client_addr), TRUE);
if (hc->referer[0] != '\0')
{
setenv("HTTP_REFERER", hc->referer, TRUE);
}
if (hc->useragent[0] != '\0')
{
setenv("HTTP_USER_AGENT", hc->useragent, TRUE);
}
if (hc->accept[0] != '\0')
{
setenv("HTTP_ACCEPT", hc->accept, TRUE);
}
if (hc->accepte[0] != '\0')
{
setenv("HTTP_ACCEPT_ENCODING", hc->accepte, TRUE);
}
if (hc->acceptl[0] != '\0')
{
setenv("HTTP_ACCEPT_LANGUAGE", hc->acceptl, TRUE);
}
if (hc->cookie[0] != '\0')
{
setenv("HTTP_COOKIE", hc->cookie, TRUE);
}
if (hc->contenttype[0] != '\0')
{
setenv("CONTENT_TYPE", hc->contenttype, TRUE);
}
if (hc->hdrhost[0] != '\0')
{
setenv("HTTP_HOST", hc->hdrhost, TRUE);
}
if (hc->contentlength != -1)
{
(void)snprintf(buf, sizeof(buf), "%lu", (unsigned long)hc->contentlength);
setenv("CONTENT_LENGTH", buf, TRUE);
}
if (hc->remoteuser[0] != '\0')
{
setenv("REMOTE_USER", hc->remoteuser, TRUE);
}
if (hc->authorization[0] != '\0')
{
setenv("AUTH_TYPE", "Basic", TRUE);
}
/* We only support Basic auth at the moment. */
if (getenv("TZ") != NULL)
{
setenv("TZ", getenv("TZ"), TRUE);
}
setenv("CGI_PATTERN", CONFIG_THTTPD_CGI_PATTERN, TRUE);
}
/* Set up argument vector */
static FAR char **make_argp(httpd_conn *hc)
{
FAR char **argp;
int argn;
char *cp1;
char *cp2;
/* By allocating an arg slot for every character in the query, plus one
* for the filename and one for the NULL, we are guaranteed to have
* enough. We could actually use strlen/2.
*/
argp = NEW(char *, strlen(hc->query) + 2);
if (!argp)
{
return NULL;
}
argp[0] = strrchr(hc->expnfilename, '/');
if (argp[0])
{
++argp[0];
}
else
{
argp[0] = hc->expnfilename;
}
argn = 1;
/* According to the CGI spec at http://hoohoo.ncsa.uiuc.edu/cgi/cl.html,
* "The server should search the query information for a non-encoded =
* character to determine if the command line is to be used, if it finds
* one, the command line is not to be used."
*/
if (strchr(hc->query, '=') == NULL)
{
for (cp1 = cp2 = hc->query; *cp2 != '\0'; ++cp2)
{
if (*cp2 == '+')
{
*cp2 = '\0';
httpd_strdecode(cp1, cp1);
argp[argn++] = cp1;
cp1 = cp2 + 1;
}
}
if (cp2 != cp1)
{
httpd_strdecode(cp1, cp1);
argp[argn++] = cp1;
}
}
argp[argn] = NULL;
return argp;
}
/* Data is available from the client socket. This routine is used only for POST
* requests. It reads the data from the client and sends it to the child thread.
*/
static inline int cgi_interpose_input(httpd_conn *hc, int wfd, char *buffer)
{
size_t nbytes;
ssize_t nbytes_read;
ssize_t nbytes_written;
nbytes = hc->read_idx - hc->checked_idx;
if (nbytes > 0)
{
if (httpd_write(wfd, &(hc->read_buf[hc->checked_idx]), nbytes) != nbytes)
{
return 1;
}
}
if (nbytes < hc->contentlength)
{
do
{
nbytes_read = read(hc->conn_fd, buffer, MIN(sizeof(buffer), hc->contentlength - nbytes));
if (nbytes_read < 0)
{
if (errno != EINTR)
{
return 1;
}
}
}
while (nbytes_read < 0);
if (nbytes_read > 0)
{
nbytes_written = httpd_write(wfd, buffer, nbytes_read);
if (nbytes_written != nbytes_read)
{
return 1;
}
}
}
if (nbytes >= hc->contentlength)
{
/* Special hack to deal with broken browsers that send a LF or CRLF
* after POST data, causing TCP resets - we just read and discard up
* to 2 bytes. Unfortunately this doesn't fix the problem for CGIs
* which avoid the interposer task due to their POST data being
* short. Creating an interposer task for all POST CGIs is
* unacceptably expensive. The eventual fix will come when interposing
* gets integrated into the main loop as a tasklet instead of a task.
*/
/* Turn on no-delay mode in case we previously cleared it. */
httpd_set_ndelay(hc->conn_fd);
/* And read up to 2 bytes. */
(void)read(hc->conn_fd, buffer, sizeof(buffer));
return 1;
}
return 0;
}
/* This routine is used for parsed-header CGIs. The idea here is that the
* CGI can return special headers such as "Status:" and "Location:" which
* change the return status of the response. Since the return status has to
* be the very first line written out, we have to accumulate all the headers
* and check for the special ones before writing the status. Then we write
* out the saved headers and proceed to echo the rest of the response.
*/
static inline int cgi_interpose_output(httpd_conn *hc, int rfd, char *inbuffer,
struct cgi_outbuffer_s *hdr)
{
ssize_t nbytes_read;
char *br = NULL;
int status;
const char *title;
char *cp;
/* Make sure the connection is in blocking mode. It should already be
* blocking, but we might as well be sure.
*/
httpd_clear_ndelay(hc->conn_fd);
/* Loop while there are things we can do without waiting for more input */
switch (hdr->state)
{
case CGI_OUTBUFFER_READHEADER:
{
/* Slurp in all headers as they become available from the client. */
do
{
/* Read until we successfully read data or until an error occurs.
* EAGAIN is not an error, but it is still cause to return.
*/
nbytes_read = read(hc->conn_fd, inbuffer, sizeof(inbuffer));
nvdbg("Read %d bytes from fd %d\n", nbytes_read, hc->conn_fd);
if (nbytes_read < 0)
{
if (errno != EINTR)
{
if (errno != EAGAIN)
{
ndbg("read: %d\n", errno);
}
return 1;
}
}
}
while (nbytes_read < 0);
/* Check for end-of-file */
if (nbytes_read <= 0)
{
nvdbg("End-of-file\n");
br = &(hdr->buffer[hdr->len]);
hdr->state = CGI_OUTBUFFER_HEADERREAD;
}
else
{
/* Accumulate more header data */
httpd_realloc_str(&hdr->buffer, &hdr->size, hdr->len + nbytes_read);
(void)memcpy(&(hdr->buffer[hdr->len]), inbuffer, nbytes_read);
hdr->len += nbytes_read;
hdr->buffer[hdr->len] = '\0';
nvdbg("Header bytes accumulated: %d\n", hdr->len);
/* Check for end of header */
if ((br = strstr(hdr->buffer, "\r\n\r\n")) != NULL ||
(br = strstr(hdr->buffer, "\012\012")) != NULL)
{
nvdbg("End-of-header\n");
hdr->state = CGI_OUTBUFFER_HEADERREAD;
}
else
{
/* Return. We will be called again when more data is available
* on the socket.
*/
return 0;
}
}
}
break;
case CGI_OUTBUFFER_HEADERREAD:
{
/* If there were no headers, bail. */
if (hdr->buffer[0] == '\0')
{
hdr->state = CGI_OUTBUFFER_DONE;
return 1;
}
/* Figure out the status. Look for a Status: or Location: header; else if
* there's an HTTP header line, get it from there; else default to 200.
*/
status = 200;
if (strncmp(hdr->buffer, "HTTP/", 5) == 0)
{
cp = hdr->buffer;
cp += strcspn(cp, " \t");
status = atoi(cp);
}
if ((cp = strstr(hdr->buffer, "Status:")) != NULL &&
cp < br && (cp == hdr->buffer || *(cp - 1) == '\012'))
{
cp += 7;
cp += strspn(cp, " \t");
status = atoi(cp);
}
if ((cp = strstr(hdr->buffer, "Location:")) != NULL &&
cp < br && (cp == hdr->buffer || *(cp - 1) == '\012'))
{
status = 302;
}
/* Write the status line. */
nvdbg("Status: %d\n", status);
switch (status)
{
case 200:
title = ok200title;
break;
case 302:
title = err302title;
break;
case 304:
title = err304title;
break;
case 400:
BADREQUEST("status");
title = httpd_err400title;
break;
#ifdef CONFIG_THTTPD_AUTH_FILE
case 401:
title = err401title;
break;
#endif
case 403:
title = err403title;
break;
case 404:
title = err404title;
break;
case 408:
title = httpd_err408title;
break;
case 500:
INTERNALERROR("status");
title = err500title;
break;
case 501:
NOTIMPLEMENTED("status");
title = err501title;
break;
case 503:
title = httpd_err503title;
break;
default:
title = "Something";
break;
}
(void)snprintf(inbuffer, sizeof(inbuffer), "HTTP/1.0 %d %s\r\n", status, title);
(void)httpd_write(hc->conn_fd, inbuffer, strlen(inbuffer));
/* Write the saved hdr->buffer to the client. */
(void)httpd_write(hc->conn_fd, hdr->buffer, hdr->len);
}
/* Then set up to read the data following the header from the CGI program and
* pass it back to the client. We return now; we will be called again when
* data is available on the pipe.
*/
hdr->state = CGI_OUTBUFFER_READDATA;
return 0;
case CGI_OUTBUFFER_READDATA:
{
/* Read data from the pipe. */
do
{
/* Read until we successfully read data or until an error occurs.
* EAGAIN is not an error, but it is still cause to return.
*/
nbytes_read = read(rfd, inbuffer, sizeof(inbuffer));
nvdbg("Read %d bytes from fd %d\n", nbytes_read, rfd);
if (nbytes_read < 0)
{
if (errno != EINTR)
{
if (errno != EAGAIN)
{
ndbg("read: %d\n", errno);
}
return 1;
}
}
}
while (nbytes_read < 0);
/* Check for end of file */
if (nbytes_read == 0)
{
nvdbg("End-of-file\n");
close(hc->conn_fd);
close(rfd);
hdr->state = CGI_OUTBUFFER_DONE;
return 1;
}
else
{
/* Forward the data from the CGI program to the client */
(void)httpd_write(hc->conn_fd, inbuffer, strlen(inbuffer));
}
}
break;
case CGI_OUTBUFFER_DONE:
default:
return 1;
}
return 0;
}
/* CGI child task. */
static int cgi_child(int argc, char **argv)
{
FAR httpd_conn *hc = (FAR httpd_conn*)strtoul(argv[1], NULL, 16);
#if CONFIG_THTTPD_CGI_TIMELIMIT > 0
ClientData client_data;
#endif
FAR char **argp;
struct cgi_outbuffer_s hdr;
struct fdwatch_s *fw;
char *buffer;
char *binary;
char *directory;
boolean indone;
boolean outdone;
int child;
int pipefd[2];
int wfd = -1;
int rfd = -1;
int fd;
int ret;
int err = 1;
/* Use low-level debug out (because the low-level output may survive closing
* all file descriptors
*/
nllvdbg("Started: %s\n", argv[1]);
/* Update all of the environment variable settings, these will be inherited
* by the CGI task.
*/
create_environment(hc);
/* Make the argument vector. */
argp = make_argp(hc);
/* Close all file descriptors (including stdio, stdin, stdout) EXCEPT for
* stderr and hc->conn_fd
*/
nllvdbg("Closing all descriptors\n");
for (fd = 0; fd < (CONFIG_NFILE_DESCRIPTORS + CONFIG_NSOCKET_DESCRIPTORS); fd++)
{
/* Keep stderr open for debug; keep hc->conn_fd open for obvious reasons */
if (fd != 2 && fd != hc->conn_fd)
{
close(fd);
}
}
/* Create pipes that will be interposed between the CGI task's stdin or
* stdout and the socket.
*
* Setup up the STDIN pipe - a pipe to transfer data received on the
* socket to the CGI program.
*/
nllvdbg("Create STDIN pipe\n");
ret = pipe(pipefd);
if (ret < 0)
{
nlldbg("STDIN pipe: %d\n", errno);
goto errout_with_descriptors;
}
else
{
/* Then map the receiving end the pipe to stdin, save the sending end, and
* closing the original receiving end
*/
ret = dup2(pipefd[0], 0);
if (ret < 0)
{
nlldbg("STDIN dup2: %d\n", errno);
close(pipefd[1]);
goto errout_with_descriptors;
}
wfd = pipefd[1];
close(pipefd[0]);
}
/* Set up the STDOUT pipe - a pipe to transfer data received from the CGI program
* to the client.
*/
if (ret == 0)
{
nllvdbg("Create STDOUT pipe\n");
ret = pipe(pipefd);
if (ret < 0)
{
nlldbg("STDOUT pipe: %d\n", errno);
goto errout_with_descriptors;
}
else
{
/* Then map the sending end the pipe to stdout, save the receiving end, and
* closing the original sending end
*/
ret = dup2(pipefd[1], 1);
if (ret < 0)
{
nlldbg("STDOUT dup2: %d\n", errno);
close(pipefd[1]);
goto errout_with_descriptors;
}
rfd = pipefd[0];
close(pipefd[1]);
}
}
/* Split the program into directory and binary, so we can chdir() to the
* program's own directory. This isn't in the CGI 1.1 spec, but it's what
* other HTTP servers do.
*/
directory = strdup(hc->expnfilename);
if (!directory)
{
binary = hc->expnfilename; /* ignore errors */
}
else
{
binary = strrchr(directory, '/');
if (!binary)
{
binary = hc->expnfilename;
}
else
{
*binary++ = '\0';
(void)chdir(directory); /* ignore errors */
}
}
/* Allocate memory for buffering */
memset(&hdr, 0, sizeof(struct cgi_outbuffer_s));
httpd_realloc_str(&hdr.buffer, &hdr.size, 500);
if (!hdr.buffer)
{
nlldbg("hdr allocation failed\n");
goto errout_with_descriptors;
}
buffer = (char*)httpd_malloc(CONFIG_THTTPD_IOBUFFERSIZE);
if (!buffer)
{
nlldbg("buffer allocation failed\n");
goto errout_with_header;
}
/* Create fdwatch structures */
fw = fdwatch_initialize(2);
if (!fw)
{
nlldbg("fdwatch allocation failed\n");
goto errout_with_buffer;
}
/* Run the CGI program. */
nllvdbg("Starting CGI\n");
child = exec(binary, (FAR const char **)argp, g_thttpdsymtab, g_thttpdnsymbols);
if (child < 0)
{
/* Something went wrong. */
nlldbg("execve %s: %d\n", hc->expnfilename, errno);
goto errout_with_watch;
}
/* Schedule a kill for the child task in case it runs too long. */
#if CONFIG_THTTPD_CGI_TIMELIMIT > 0
client_data.i = child;
if (tmr_create(NULL, cgi_kill, client_data, CONFIG_THTTPD_CGI_TIMELIMIT * 1000L, 0) == NULL)
{
nlldbg("tmr_create(cgi_kill child) failed\n");
goto errout_with_watch;
}
#endif
/* Add the read descriptors to the watch */
fdwatch_add_fd(fw, hc->conn_fd, NULL);
fdwatch_add_fd(fw, rfd, NULL);
/* Then perform the interposition */
indone = FALSE;
outdone = FALSE;
nllvdbg("Interposing\n");
do
{
(void)fdwatch(fw, 5000);
if (!indone && fdwatch_check_fd(fw, hc->conn_fd))
{
/* Transfer data from the client to the CGI program (POST) */
nllvdbg("Interpose input\n");
indone = cgi_interpose_input(hc, wfd, buffer);
if (indone)
{
fdwatch_del_fd(fw, hc->conn_fd);
}
}
if (fdwatch_check_fd(fw, rfd))
{
/* Handle receipt of headers and CGI program response (GET) */
nllvdbg("Interpose output\n");
outdone = cgi_interpose_output(hc, rfd, buffer, &hdr);
}
}
while (!outdone);
err = 0;
/* Get rid of watch structures */
errout_with_watch:
fdwatch_uninitialize(fw);
/* Free memory */
errout_with_buffer:
httpd_free(buffer);
errout_with_header:
httpd_free(hdr.buffer);
/* Close all descriptors */
errout_with_descriptors:
close(wfd);
close(rfd);
close(hc->conn_fd);
INTERNALERROR("errout");
httpd_send_err(hc, 500, err500title, "", err500form, hc->encodedurl);
httpd_write_response(hc);
nllvdbg("Return %d\n", ret);
return err;
}
/****************************************************************************
* Private Function Prototypes
****************************************************************************/
int cgi(httpd_conn *hc)
{
char arg[16];
char *argv[1];
pid_t child;
if (hc->method == METHOD_GET || hc->method == METHOD_POST)
{
#ifdef CONFIG_THTTPD_CGILIMIT
if (hc->hs->cgi_count >= CONFIG_THTTPD_CGILIMIT)
{
httpd_send_err(hc, 503, httpd_err503title, "", httpd_err503form,
hc->encodedurl);
return -1;
}
#endif
++hc->hs->cgi_count;
httpd_clear_ndelay(hc->conn_fd);
/* Start the child task. We use a trampoline task here so that we can
* safely muck with the file descriptors before actually started the CGI
* task.
*/
snprintf(arg, 16, "%p", hc); /* task_create doesn't handle binary arguments. */
argv[0] = arg;
#ifndef CONFIG_CUSTOM_STACK
child = task_create("CGI child", CONFIG_THTTPD_CGI_PRIORITY,
CONFIG_THTTPD_CGI_STACKSIZE,
(main_t)cgi_child, (const char **)argv);
#else
child = task_create("CGI child", CONFIG_THTTPD_CGI_PRIORITY,
(main_t)cgi_child, (const char **)argv);
#endif
if (child < 0)
{
ndbg("task_create: %d\n", errno);
INTERNALERROR("task_create");
httpd_send_err(hc, 500, err500title, "", err500form, hc->encodedurl);
return -1;
}
ndbg("Started CGI task %d for file '%s'\n", child, hc->expnfilename);
hc->status = 200;
hc->bytes_sent = CONFIG_THTTPD_CGI_BYTECOUNT;
hc->should_linger = FALSE;
}
else
{
NOTIMPLEMENTED("CGI");
httpd_send_err(hc, 501, err501title, "", err501form, httpd_method_str(hc->method));
return -1;
}
return 0;
}
#endif /* CONFIG_THTTPD && CONFIG_THTTPD_CGI_PATTERN */