/*
* Copyright 2001-2004 Brandon Long
* All Rights Reserved.
*
* ClearSilver Templating System
*
* This code is made available under the terms of the ClearSilver License.
* http://www.clearsilver.net/license.hdf
*
*/
/* rfc2388 defines multipart/form-data which is primarily used for
* HTTP file upload
*/
#include "cs_config.h"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <limits.h>
#include <ctype.h>
#include <string.h>
#include "util/neo_misc.h"
#include "util/neo_err.h"
#include "util/neo_str.h"
#include "cgi.h"
#include "cgiwrap.h"
static NEOERR * _header_value (char *hdr, char **val)
{
char *p, *q;
int l;
*val = NULL;
p = hdr;
while (*p && isspace(*p)) p++;
q = p;
while (*q && !isspace(*q) && *q != ';') q++;
if (!*p || p == q) return STATUS_OK;
l = q - p;
*val = (char *) malloc (l+1);
if (*val == NULL)
return nerr_raise (NERR_NOMEM, "Unable to allocate space for val");
memcpy (*val, p, l);
(*val)[l] = '\0';
return STATUS_OK;
}
static NEOERR * _header_attr (char *hdr, char *attr, char **val)
{
char *p, *k, *v;
int found = 0;
int l, al;
char *r;
*val = NULL;
l = strlen(attr);
/* skip value */
p = hdr;
while (*p && *p != ';') p++;
if (!*p) return STATUS_OK;
p++;
while(*p && !found)
{
while (*p && isspace(*p)) p++;
if (!*p) return STATUS_OK;
/* attr name */
k = p;
while (*p && !isspace(*p) && *p != ';' && *p != '=') p++;
if (!*p) return STATUS_OK;
if (l == (p-k) && !strncasecmp(attr, k, l))
found = 1;
while (*p && isspace(*p)) p++;
if (*p != ';' && *p != '=') return STATUS_OK;
if (*p == ';')
{
if (found)
{
*val = strdup ("");
if (*val == NULL)
return nerr_raise (NERR_NOMEM, "Unable to allocate value");
return STATUS_OK;
}
}
else
{
p++;
if (*p == '"')
{
v = ++p;
while (*p && *p != '"') p++;
al = p-v;
if (*p) p++;
}
else
{
v = p;
while (*p && !isspace(*p) && *p != ';') p++;
al = p-v;
}
if (found)
{
r = (char *) malloc (al+1);
if (r == NULL)
return nerr_raise (NERR_NOMEM, "Unable to allocate value");
memcpy (r, v, al);
r[al] = '\0';
*val = r;
return STATUS_OK;
}
}
if (*p) p++;
}
return STATUS_OK;
}
static NEOERR * _read_line (CGI *cgi, char **s, int *l, int *done)
{
int ofs = 0;
char *p;
int to_read;
if (cgi->buf == NULL)
{
cgi->buflen = 4096;
cgi->buf = (char *) malloc (sizeof(char) * cgi->buflen);
if (cgi->buf == NULL)
return nerr_raise (NERR_NOMEM, "Unable to allocate cgi buf");
}
if (cgi->unget)
{
cgi->unget = FALSE;
*s = cgi->last_start;
*l = cgi->last_length;
return STATUS_OK;
}
if (cgi->found_nl)
{
p = memchr (cgi->buf + cgi->nl, '\n', cgi->readlen - cgi->nl);
if (p) {
cgi->last_start = *s = cgi->buf + cgi->nl;
cgi->last_length = *l = p - (cgi->buf + cgi->nl) + 1;
cgi->found_nl = TRUE;
cgi->nl = p - cgi->buf + 1;
return STATUS_OK;
}
ofs = cgi->readlen - cgi->nl;
memmove(cgi->buf, cgi->buf + cgi->nl, ofs);
}
// Read either as much buffer space as we have left, or up to
// the amount of data remaining according to Content-Length
// If there is no Content-Length, just use the buffer space, but recognize
// that it might not work on some servers or cgiwrap implementations.
// Some servers will close their end of the stdin pipe, so cgiwrap_read
// will return if we ask for too much. Techically, not including
// Content-Length is against the HTTP spec, so we should consider failing
// earlier if we don't have a length.
to_read = cgi->buflen - ofs;
if (cgi->data_expected && (to_read > cgi->data_expected - cgi->data_read))
{
to_read = cgi->data_expected - cgi->data_read;
}
cgiwrap_read (cgi->buf + ofs, to_read, &(cgi->readlen));
if (cgi->readlen < 0)
{
return nerr_raise_errno (NERR_IO, "POST Read Error");
}
if (cgi->readlen == 0)
{
*done = 1;
return STATUS_OK;
}
cgi->data_read += cgi->readlen;
if (cgi->upload_cb)
{
if (cgi->upload_cb (cgi, cgi->data_read, cgi->data_expected))
return nerr_raise (CGIUploadCancelled, "Upload Cancelled");
}
cgi->readlen += ofs;
p = memchr (cgi->buf, '\n', cgi->readlen);
if (!p)
{
cgi->found_nl = FALSE;
cgi->last_start = *s = cgi->buf;
cgi->last_length = *l = cgi->readlen;
return STATUS_OK;
}
cgi->last_start = *s = cgi->buf;
cgi->last_length = *l = p - cgi->buf + 1;
cgi->found_nl = TRUE;
cgi->nl = *l;
return STATUS_OK;
}
static NEOERR * _read_header_line (CGI *cgi, STRING *line, int *done)
{
NEOERR *err;
char *s, *p;
int l;
err = _read_line (cgi, &s, &l, done);
if (err) return nerr_pass (err);
if (*done || (l == 0)) return STATUS_OK;
if (isspace (s[0])) return STATUS_OK;
while (l && isspace(s[l-1])) l--;
err = string_appendn (line, s, l);
if (err) return nerr_pass (err);
while (1)
{
err = _read_line (cgi, &s, &l, done);
if (err) break;
if (l == 0) break;
if (*done) break;
if (!(s[0] == ' ' || s[0] == '\t'))
{
cgi->unget = TRUE;
break;
}
while (l && isspace(s[l-1])) l--;
p = s;
while (*p && isspace(*p) && (p-s < l)) p++;
err = string_append_char (line, ' ');
if (err) break;
err = string_appendn (line, p, l - (p-s));
if (err) break;
if (line->len > 50*1024*1024)
{
string_clear(line);
return nerr_raise(NERR_ASSERT, "read_header_line exceeded 50MB");
}
}
return nerr_pass (err);
}
static BOOL _is_boundary (char *boundary, char *s, int l, int *done)
{
static char *old_boundary = NULL;
static int bl;
/* cache the boundary strlen... more pointless optimization by blong */
if (old_boundary != boundary)
{
old_boundary = boundary;
bl = strlen(boundary);
}
if (s[l-1] != '\n')
return FALSE;
l--;
if (s[l-1] == '\r')
l--;
if (bl+2 == l && s[0] == '-' && s[1] == '-' && !strncmp (s+2, boundary, bl))
return TRUE;
if (bl+4 == l && s[0] == '-' && s[1] == '-' &&
!strncmp (s+2, boundary, bl) &&
s[l-1] == '-' && s[l-2] == '-')
{
*done = 1;
return TRUE;
}
return FALSE;
}
static NEOERR * _find_boundary (CGI *cgi, char *boundary, int *done)
{
NEOERR *err;
char *s;
int l;
*done = 0;
while (1)
{
err = _read_line (cgi, &s, &l, done);
if (err) return nerr_pass (err);
if ((l == 0) || (*done)) {
*done = 1;
return STATUS_OK;
}
if (_is_boundary(boundary, s, l, done))
return STATUS_OK;
}
return STATUS_OK;
}
NEOERR *open_upload(CGI *cgi, int unlink_files, FILE **fpw)
{
NEOERR *err = STATUS_OK;
FILE *fp;
char path[_POSIX_PATH_MAX];
int fd;
*fpw = NULL;
snprintf (path, sizeof(path), "%s/cgi_upload.XXXXXX",
hdf_get_value(cgi->hdf, "Config.Upload.TmpDir", "/var/tmp"));
fd = mkstemp(path);
if (fd == -1)
{
return nerr_raise_errno (NERR_SYSTEM, "Unable to open temp file %s",
path);
}
fp = fdopen (fd, "w+");
if (fp == NULL)
{
close(fd);
return nerr_raise_errno (NERR_SYSTEM, "Unable to fdopen file %s", path);
}
if (unlink_files) unlink(path);
if (cgi->files == NULL)
{
err = uListInit (&(cgi->files), 10, 0);
if (err)
{
fclose(fp);
return nerr_pass(err);
}
}
err = uListAppend (cgi->files, fp);
if (err)
{
fclose (fp);
return nerr_pass(err);
}
if (!unlink_files) {
if (cgi->filenames == NULL)
{
err = uListInit (&(cgi->filenames), 10, 0);
if (err)
{
fclose(fp);
return nerr_pass(err);
}
}
err = uListAppend (cgi->filenames, strdup(path));
if (err)
{
fclose (fp);
return nerr_pass(err);
}
}
*fpw = fp;
return STATUS_OK;
}
static NEOERR * _read_part (CGI *cgi, char *boundary, int *done)
{
NEOERR *err = STATUS_OK;
STRING str;
HDF *child, *obj = NULL;
FILE *fp = NULL;
char buf[256];
char *p;
char *name = NULL, *filename = NULL;
char *type = NULL, *tmp = NULL;
char *last = NULL;
int unlink_files = hdf_get_int_value(cgi->hdf, "Config.Upload.Unlink", 1);
string_init (&str);
while (1)
{
err = _read_header_line (cgi, &str, done);
if (err) break;
if (*done) break;
if (str.buf == NULL || str.buf[0] == '\0') break;
p = strchr (str.buf, ':');
if (p)
{
*p = '\0';
if (!strcasecmp(str.buf, "content-disposition"))
{
err = _header_attr (p+1, "name", &name);
if (err) break;
err = _header_attr (p+1, "filename", &filename);
if (err) break;
}
else if (!strcasecmp(str.buf, "content-type"))
{
err = _header_value (p+1, &type);
if (err) break;
}
else if (!strcasecmp(str.buf, "content-encoding"))
{
err = _header_value (p+1, &tmp);
if (err) break;
if (tmp && strcmp(tmp, "7bit") && strcmp(tmp, "8bit") &&
strcmp(tmp, "binary"))
{
free(tmp);
err = nerr_raise (NERR_ASSERT, "form-data encoding is not supported");
break;
}
free(tmp);
}
}
string_set(&str, "");
}
if (err)
{
string_clear(&str);
if (name) free(name);
if (filename) free(filename);
if (type) free(type);
return nerr_pass (err);
}
do
{
if (filename)
{
err = open_upload(cgi, unlink_files, &fp);
if (err) break;
}
string_set(&str, "");
while (!(*done))
{
char *s;
int l, w;
err = _read_line (cgi, &s, &l, done);
if (err) break;
if (*done || (l == 0)) break;
if (_is_boundary(boundary, s, l, done)) break;
if (filename)
{
if (last) fwrite (last, sizeof(char), strlen(last), fp);
if (l > 1 && s[l-1] == '\n' && s[l-2] == '\r')
{
last = "\r\n";
l-=2;
}
else if (l > 0 && s[l-1] == '\n')
{
last = "\n";
l--;
}
else last = NULL;
w = fwrite (s, sizeof(char), l, fp);
if (w != l)
{
err = nerr_raise_errno (NERR_IO,
"Short write on file %s upload %d < %d", filename, w, l);
break;
}
}
else
{
err = string_appendn(&str, s, l);
if (err) break;
}
}
if (err) break;
} while (0);
/* Set up the cgi data */
if (!err)
{
do {
/* FIXME: Hmm, if we've seen the same name here before, what should we do?
*/
if (filename)
{
fseek(fp, 0, SEEK_SET);
snprintf (buf, sizeof(buf), "Query.%s", name);
err = hdf_set_value (cgi->hdf, buf, filename);
if (!err && type)
{
snprintf (buf, sizeof(buf), "Query.%s.Type", name);
err = hdf_set_value (cgi->hdf, buf, type);
}
if (!err)
{
snprintf (buf, sizeof(buf), "Query.%s.FileHandle", name);
err = hdf_set_int_value (cgi->hdf, buf, uListLength(cgi->files));
}
if (!err && !unlink_files)
{
char *path;
snprintf (buf, sizeof(buf), "Query.%s.FileName", name);
err = uListGet(cgi->filenames, uListLength(cgi->filenames)-1,
(void *)&path);
if (!err) err = hdf_set_value (cgi->hdf, buf, path);
}
}
else
{
snprintf (buf, sizeof(buf), "Query.%s", name);
while (str.len && isspace(str.buf[str.len-1]))
{
str.buf[str.len-1] = '\0';
str.len--;
}
if (!(cgi->ignore_empty_form_vars && str.len == 0))
{
/* If we've seen it before... we force it into a list */
obj = hdf_get_obj (cgi->hdf, buf);
if (obj != NULL)
{
int i = 0;
char buf2[10];
char *t;
child = hdf_obj_child (obj);
if (child == NULL)
{
t = hdf_obj_value (obj);
err = hdf_set_value (obj, "0", t);
if (err != STATUS_OK) break;
i = 1;
}
else
{
while (child != NULL)
{
i++;
child = hdf_obj_next (child);
if (err != STATUS_OK) break;
}
if (err != STATUS_OK) break;
}
snprintf (buf2, sizeof(buf2), "%d", i);
err = hdf_set_value (obj, buf2, str.buf);
if (err != STATUS_OK) break;
}
err = hdf_set_value (cgi->hdf, buf, str.buf);
}
}
} while (0);
}
string_clear(&str);
if (name) free(name);
if (filename) free(filename);
if (type) free(type);
return nerr_pass (err);
}
NEOERR * parse_rfc2388 (CGI *cgi)
{
NEOERR *err;
char *ct_hdr;
char *boundary = NULL;
int l;
int done = 0;
l = hdf_get_int_value (cgi->hdf, "CGI.ContentLength", -1);
ct_hdr = hdf_get_value (cgi->hdf, "CGI.ContentType", NULL);
if (ct_hdr == NULL)
return nerr_raise (NERR_ASSERT, "No content type header?");
cgi->data_expected = l;
cgi->data_read = 0;
if (cgi->upload_cb)
{
if (cgi->upload_cb (cgi, cgi->data_read, cgi->data_expected))
return nerr_raise (CGIUploadCancelled, "Upload Cancelled");
}
err = _header_attr (ct_hdr, "boundary", &boundary);
if (err) return nerr_pass (err);
err = _find_boundary(cgi, boundary, &done);
while (!err && !done)
{
err = _read_part (cgi, boundary, &done);
}
if (boundary) free(boundary);
return nerr_pass(err);
}
/* this is here because it gets populated in this file */
FILE *cgi_filehandle (CGI *cgi, const char *form_name)
{
NEOERR *err;
FILE *fp;
char buf[256];
int n;
if ((form_name == NULL) || (form_name[0] == '\0'))
{
/* if NULL, then its the PUT data we're looking for... */
n = hdf_get_int_value (cgi->hdf, "PUT.FileHandle", -1);
}
else
{
snprintf (buf, sizeof(buf), "Query.%s.FileHandle", form_name);
n = hdf_get_int_value (cgi->hdf, buf, -1);
}
if (n == -1) return NULL;
err = uListGet(cgi->files, n-1, (void *)&fp);
if (err)
{
nerr_ignore(&err);
return NULL;
}
return fp;
}