#include "th1ish_cgi.h"
#include <ctype.h> /* isxdigit() */
#include <string.h> /* strlen() */
#include <assert.h>
#include <stdlib.h> /*getenv() and friends */
/**
Only for debuggering...
*/
#include <stdio.h>
#define MARKER(pfexp) \
do{ printf("MARKER: %s:%d:%s():\t",__FILE__,__LINE__,__func__); \
printf pfexp; \
} while(0)
const th1ish_cgi th1ish_cgi_empty = th1ish_cgi_empty_m;
/**
Some cson_cgi-internal value keys.
*/
static const struct {
char const * ENV_GET;
char const * ENV_POST;
char const * ENV_COOKIE;
char const * ENV_SYS;
char const * ENV_APP;
char const * ENV_ARGV;
char const * ENV_CONFIG;
char const * ENV_SESSION;
char const * RESPONSE_HEADERS;
} th1ish_cgi_keys = {
"cgi.GET",
"cgi.POST",
"cgi.COOKIE",
"cgi.ENV",
"cgi.APP",
"cgi.ARGV",
"cgi.CONFIG",
"cgi.SESSION",
"cgi.response.headers"
};
/**
If PATH_INFO is set, this function splits it on '/'
characters and creates an array out of the elements.
The array is stored as $ENV["PATH_INFO_SPLIT"].
Returns non-0 on error. If PATH_INFO is not set,
0 is returned. If it is set but has no entries,
an empty array is created.
A return value of CWAL_RC_RANGE probably means that a path element
was longer than our internal buffer size, in which case processing
ends and PATH_INFO_SPLIT is not set. That error can probably be
ignored by the caller, but all others are probably serious
(e.g. CWAL_RC_OOM).
*/
static int th1ish_cgi_import_path_info(th1ish_cgi *cx)
{
char const * head =
th1ish_cgi_getenv_cstr(cx, "e", "PATH_INFO", 9, NULL);
if( NULL == head ) return 0;
else{
cwal_value * env = th1ish_cgi_env_get(cx, 'e', 1);
cwal_array * ar = env ? cwal_new_array(cx->e) : NULL;
char const * tail = NULL;
if( !ar ) return CWAL_RC_OOM;
else{
enum { BufSize = 256 };
char buf[BufSize];
cwal_value * partV;
cwal_size_t slen;
int rc = 0;
while( cwal_strtok( &head, '/', &tail ) ){
slen = (tail-head);
if( slen >= BufSize ){
rc = CWAL_RC_RANGE;
goto end_clean;
}
memcpy( buf, head, slen );
buf[slen] = 0;
th1ish_cgi_urldecode_inline( buf, &slen );
partV = cwal_new_string_value( cx->e, buf, slen );
if( ! partV ){
rc = CWAL_RC_OOM;
goto end_clean;
}
rc = cwal_array_append( ar, partV );
if( rc ){
cwal_value_unref( partV );
goto end_clean;
}
partV = NULL;
head = tail;
tail = NULL;
}
assert( 0 == rc );
rc = cwal_prop_set( env, "PATH_INFO_SPLIT", 15,
cwal_array_value(ar) );
end_clean:
if( rc ){
cwal_array_unref( ar );
}
return rc;
}
}
}
/**
Imports (extern char ** environ) into cx->request.env, initializing
cx->request.env if needed. If called multiple times the environment
is re-read each time, but old entries which are no longer in the
new environment are not removed from cx->request.env.
Returns 0 on success.
*/
static int th1ish_cgi_import_environ(th1ish_cgi * cx)
{
extern char ** environ;
int i = 0;
char const * e = environ[0];
char const * v = NULL;
enum { KeyBufSize = 512 };
char keybuf[KeyBufSize];
char * kpos = NULL;
cwal_size_t vLen;
int rc = 0;
cwal_value * jv = NULL;
cwal_value * env = th1ish_cgi_env_get( cx, 'e', 1 );
if(!env) return CWAL_RC_OOM;
for( ; e && *e; e = environ[++i] ){
v = NULL;
kpos = keybuf;
for( ; *e && ('=' != *e); ++e ){
*(kpos++) = *e;
assert( kpos < (keybuf+KeyBufSize) );
if( kpos == (keybuf+KeyBufSize) ){
return CWAL_RC_RANGE;
}
}
*kpos = 0;
if( '=' == *e ){
v = e+1;
vLen = strlen(v);
}
else{
v = "";
vLen = 0;
}
/* TODO? URLdecode? We could do that using
cwal_printf()'s %T. */
jv = cwal_new_string_value( cx->e, vLen ? v : NULL, vLen );
if(!jv){
rc = CWAL_RC_OOM;
break;
}
rc = cwal_prop_set( env, keybuf, strlen(keybuf), jv );
if( 0 != rc ) {
cwal_value_unref(jv);
break;
}
}
if( !rc ){
rc = th1ish_cgi_import_path_info(cx);
}
return rc;
}
/**
Parses inp as a delimited list, separated by the given
separator character. Each item in the list is treated
as a key/value pair in the form KEY=VALUE, and inserted
into the target cson_object (which must not be NULL).
This is intended for parsing HTTP GET-style parameter lists.
If doUrlDecode is true (non-zero) then the VALUE part of the
key/value pair gets url-decoded before insertion. (FIXME? Also
decode the keys?)
If firstOneWins is non-0 then if a given key in the parameters is
duplicated, entries after the first are ignored. If it is 0 then
the "last one wins." This is basically a workaround for when we
have multiple session ID cookies hanging around :/.
On success it returns 0.
If a given key contains the string "[]", that part is stripped and
the entry is treated like an array element. e.g. a query string of
"a[]=3&a[]=7" would result in an array property named "a" with the
(string) entries ("3", "7").
*/
static int th1ish_cgi_parse_param_list( th1ish_cgi * cx,
cwal_value * tgt,
char const * inp,
char separator,
char doUrlDecode,
char firstOneWins)
{
if( ! tgt || !separator ) return CWAL_RC_MISUSE;
else if( !inp || !*inp ) return 0;
else{
char const * head = inp;
char const * tail = NULL;
char * out = NULL;
unsigned int inLen = strlen( inp );
cwal_size_t valLen;
cwal_value * jval = NULL;
cwal_value * listV = NULL;
cwal_array * list = NULL;
cwal_size_t keyLen;
cwal_size_t const oldUsed = cx->tmpBuf.used;
int rc = cwal_buffer_reserve( cx->e, &cx->tmpBuf, inLen+1 );
if( 0 != rc ) return rc;
while( cwal_strtok( &head, separator, &tail ) )
{
char const * key = head;
char * value = NULL;
rc = 0;
if( head == tail ) break;
out = (char *)cx->tmpBuf.mem;
memset( cx->tmpBuf.mem, 0, cx->tmpBuf.capacity );
for( ; (key<tail) && *key && isspace((int)*key); ++key ){
/* strip leading spaces in the key name
(happens in cookie values). */
}
if( key==tail ) break;
else if( '='==*key ){
/* all-space key. Just skip it. */
goto next_iter;
}
/* Write the key part to the buffer... */
for( ; (key<tail) && *key && ('='!=*key); ++key ) {
*(out++) = *key;
}
*(out++) = 0;
if( '=' == *key )++key;
value = out;
valLen = 0;
/* Write the value part to the buffer... */
for( ; (key<tail) && *key; ++key, ++valLen ) {
*(out++) = *key;
}
key = (char const *)cx->tmpBuf.mem;
keyLen = strlen(key);
if( firstOneWins && cwal_prop_get( tgt, key, keyLen)){
goto next_iter;
}
if( doUrlDecode && valLen ){
th1ish_cgi_urldecode_inline( value, &valLen );
}
/*MARKER("key=[%s], valLen=%u, value=[%s]\n", key, valLen, value );*/
jval = cwal_new_string_value( cx->e, value, valLen );
if( NULL == jval ){
rc = CWAL_RC_OOM;
goto the_end;
}
if( NULL != (out = strstr(key,"[]")) )
{ /* Treat key as an array entry, like PHP does... */
cwal_value * freeThisOnErr = NULL;
*out = 0;
list = NULL;
listV = cwal_prop_get( tgt, key, keyLen );
if( listV ){
if( ! cwal_value_is_array( listV ) ){
cwal_value_unref( jval );
jval = NULL;
goto next_iter;
}
}
else{ /* create a new array to hold the value */
listV = cwal_new_array_value(cx->e);
if( ! listV ){
cwal_value_unref( jval );
rc = CWAL_RC_OOM;
goto the_end;
}
rc = cwal_prop_set( tgt, key, keyLen, listV );
if( 0 != rc ){
cwal_value_unref( listV );
cwal_value_unref( jval );
goto the_end;
}
freeThisOnErr = listV;
}
list = cwal_value_get_array( listV );
rc = cwal_array_append( list, jval );
if( 0 != rc ){
cwal_value_unref( jval );
if(freeThisOnErr) cwal_value_unref( freeThisOnErr );
goto the_end;
}
}
else{
rc = cwal_prop_set( tgt, key, keyLen, jval );
if( 0 != rc ){
cwal_value_unref( jval );
goto the_end;
}
}
next_iter:
head = tail;
tail = NULL;
}
the_end:
cx->tmpBuf.used = oldUsed;
return rc;
}
}
/**
Parses key/value pairs from a QUERY_STRING-formatted
string.
Returns 0 on success. The "most likely" error condition, in terms
of potential code paths, is is an allocation error.
TODO: if the key part of any entry ends with "[]", treat it as an
array entry, like PHP does.
*/
static int th1ish_cgi_parse_query_string( th1ish_cgi * cx, char const * qstr )
{
cwal_value * env = NULL;
if( !qstr || !*qstr ) return 0;
assert(cx);
env = th1ish_cgi_env_get( cx, 'g', 1 );
return env
? th1ish_cgi_parse_param_list( cx, env, qstr, '&', 1, 0 )
: CWAL_RC_OOM;
}
/**
Like th1ish_cgi_parse_query_string(), but expects qstr to be in COOKIE
format.
*/
static int th1ish_cgi_parse_cookies( th1ish_cgi * cx, char const * qstr ){
cwal_value * env = NULL;
if( !qstr || !*qstr ) return 0;
assert(cx);
env = th1ish_cgi_env_get(cx, 'c', 1 );
return env
? th1ish_cgi_parse_param_list( cx, env, qstr, ';', 1, 1 )
: CWAL_RC_OOM /* guess! */;
}
typedef struct CgiPostReadState_ {
FILE * fh;
cwal_size_t len;
cwal_size_t pos;
} CgiPostReadState;
static int cwal_input_FILE_n( void * state,
void * dest,
cwal_size_t * n ){
if( ! state || !dest || !n ) return CWAL_RC_MISUSE;
else{
CgiPostReadState * st = (CgiPostReadState *)state;
if( st->pos >= st->len ){
*n = 0;
return 0;
}
else if( !*n || ((st->pos + *n) > st->len) ){
return CWAL_RC_RANGE;
}
else{
unsigned int rsz = (unsigned int)fread( dest, 1, *n, st->fh );
if( ! rsz ){
*n = (cwal_size_t)rsz;
return feof(st->fh) ? 0 : CWAL_RC_IO;
}
else{
*n = (cwal_size_t)rsz;
st->pos += *n;
return 0;
}
}
}
}
static int th1ish_cgi_err_set( th1ish_cgi * cx, int code,
char const * msg, ... ){
assert(0 != code);
cx->err.msg.used = 0;
if(msg && *msg){
va_list args;
va_start(args,msg);
cwal_buffer_printfv( cx->e, &cx->err.msg, msg, args );
va_end(args);
}
return cx->err.rc = code;
}
static int th1ish_cgi_parse_POST_JSON(th1ish_cgi * cx, FILE * src,
unsigned int contentLen){
cwal_value * jv = NULL;
int rc = 0;
CgiPostReadState state;
cwal_json_parse_info pinfo = cwal_json_parse_info_empty;
assert( 0 != contentLen );
assert( NULL == cx->request.post );
state.fh = src;
state.len = contentLen;
state.pos = 0;
rc = cwal_json_parse( cx->e, cwal_input_FILE_n, &state, &jv, &pinfo );
if( rc ){
assert(!jv);
return th1ish_cgi_err_set(cx, rc,
"Parsing POST as JSON failed: "
"code=%d (%s) "
"line=%u, col=%u\n",
rc, cwal_rc_cstr(rc),
(unsigned)pinfo.line,
(unsigned)pinfo.col);
return rc;
}
cwal_value_rescope( cx->topScope, jv );
cx->request.post = jv;
return rc;
}
#define TH1ISH_CGI_ENABLE_POST_FORM_URLENCODED 1
#if TH1ISH_CGI_ENABLE_POST_FORM_URLENCODED
static int th1ish_cgi_parse_post_urlencoded( th1ish_cgi * cx, char const * qstr )
{
cwal_value * env = NULL;
if( !qstr || !*qstr ) return 0;
assert(cx);
env = th1ish_cgi_env_get( cx, 'p', 1 );
return env
? th1ish_cgi_parse_param_list( cx, env, qstr, '&', 1, 0 )
: CWAL_RC_OOM;
}
#endif
static int th1ish_cgi_init_POST(th1ish_cgi * cx){
if( ! cx || !cx->request.input ) return CWAL_RC_MISUSE;
else{
FILE * src = cx->request.input;
char const * ctype =
th1ish_cgi_getenv_cstr( cx, "e",
"CONTENT_TYPE", 12,
NULL );
if( NULL == ctype ) return 0;
else{
char const * clen =
th1ish_cgi_getenv_cstr( cx, "e",
"CONTENT_LENGTH", 14,
NULL );
if( NULL == clen ){
return th1ish_cgi_err_set(cx, CWAL_RC_RANGE,
"CONTENT_LENGTH not specified.");
}
#if TH1ISH_CGI_ENABLE_POST_FORM_URLENCODED
else if( 0 == strncmp(ctype,"application/x-www-form-urlencoded",33) ){
cwal_buffer buf = cwal_buffer_empty;
int rc = cwal_buffer_fill_from_FILE( cx->e, &buf, src );
if( rc ) goto end_clean;
if( buf.mem && buf.used ){
#if 1
if( strlen((char const *)buf.mem)
!= buf.used ){
/* assume bad/malicious input. */
rc = CWAL_RC_RANGE;
rc = th1ish_cgi_err_set(cx, CWAL_RC_RANGE,
"Not allowing binary POST input.");
goto end_clean;
}
#endif
rc = th1ish_cgi_parse_post_urlencoded( cx,
(char const *)buf.mem );
}
end_clean:
cwal_buffer_reserve( cx->e, &buf, 0 );
return rc;
}
#endif
else{
char * endpt = NULL;
long len = strtol( clen, &endpt, 10 );
if( (endpt && *endpt) || (len<=0) ) return CWAL_RC_RANGE;
else if( (0 == strncmp(ctype,"application/json",16))
|| (0 == strncmp(ctype,"text/plain",10))
|| (0 == strncmp(ctype,"application/javascript",22))
)
{
return th1ish_cgi_parse_POST_JSON(cx, src, len);
}
else{
return th1ish_cgi_err_set(cx, CWAL_RC_TYPE,
"Don't know how to handle "
"Content-type [%s]\n", ctype);
}
}
}
}
}
/**
If orig is one of the types (string,double,bool,undef,null) then
a pointer to its string representation is returned, else NULL
is returned.
For non-string types, dest must be at least destLen bytes of memory, and
if destLen is not long enough to hold the string form then NULL is returned.
On success a pointer to a string is returned. It will be one of:
- if orig is-a string then it's underlying string.
- for (double,integer,bool,undef,null), dest will be returned. The encoded
form is decimal for (double,integer), the number 0 or 1 for bool, and the
number 0 for (undef,null).
Ownership of dest is not modified by this call.
The returned value is valid until either orig or dest are modified.
On error dest is not modified. Dest is also not modified if orig
is-a string, as its own string bytes are returned instead.
*/
static char const * th1ish_cgi_pod_to_string( th1ish_cgi * cx,
cwal_value const * orig,
cwal_buffer * dest )
{
if( !cx || !orig || !dest ) return NULL;
else
{/* FIXME? use cson's output support for the numeric types. i
_think_ those bits might not be in the public API, though.
We could use it for serializing objects/arrays, in any case.
*/
enum { NumBufSize = 80 };
int rc = 0;
cwal_size_t slen = 0;
char const * cstr = cwal_value_get_cstr(orig, &slen);
dest->used = 0;
if(cstr){
if( (rc=cwal_buffer_append(cx->e, dest, cstr, slen)) ){
th1ish_cgi_err_set(cx, rc, NULL);
return NULL;
}
return (char *)dest->mem;
}
else if( cwal_value_is_integer(orig) ){
if( (rc=cwal_buffer_printf( cx->e, dest,
"%"CWAL_INT_T_PFMT,
cwal_value_get_integer(orig)))){
th1ish_cgi_err_set(cx, rc, NULL);
return NULL;
}
return (char *)dest->mem;
}
else if( cwal_value_is_double(orig) ){
if((rc=cwal_buffer_printf( cx->e, dest,
"%"CWAL_DOUBLE_T_PFMT,
cwal_value_get_double(orig)))){
th1ish_cgi_err_set(cx, rc, NULL);
return NULL;
}
return (char *)dest->mem;
}
else if( cwal_value_is_bool( orig ) ){
char const bv = cwal_value_get_bool(orig);
if( (rc=cwal_buffer_append( cx->e, dest, bv ? "1": "0", 1)) ){
th1ish_cgi_err_set(cx, rc, NULL);
return NULL;
}
return (char *)dest->mem;
}
else if( cwal_value_is_null( orig )
|| cwal_value_is_undef( orig ) ){
if( (rc=cwal_buffer_append( cx->e, dest, "0", 1)) ){
th1ish_cgi_err_set(cx, rc, NULL);
return NULL;
}
return (char *)dest->mem;
}
else{
return NULL;
}
}
}
static int th1ish_cgi_kvp_visitor_headers( cwal_kvp const * kvp,
void * state ){
th1ish_cgi * cx = (th1ish_cgi*)state;
char const * key;
cwal_value const * val;
char const * valcstr;
cwal_size_t keyLen = 0;
cx->tmpBuf.used = 0;
key = cwal_value_get_cstr(cwal_kvp_key(kvp), &keyLen);
val = cwal_kvp_value(kvp);
valcstr = th1ish_cgi_pod_to_string( cx, val, &cx->tmpBuf );
if( ! valcstr ) return 0;
else{
int const rc = cwal_outputf(cx->e, "%.*s: %.*s\r\n",
(int)keyLen, key,
(int)cx->tmpBuf.used, valcstr);
cx->tmpBuf.used = 0;
return rc;
}
}
/**
Writes an RFC822 timestamp string to dest, which must be at least destLen bytes long.
On success returns dest, else NULL. destLen must be at least 31.
*/
static char * th1ish_cgi_rfc822_timedate( time_t now, char * dest, unsigned int destLen )
{
static const char * dayNames[] =
{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
0 };
static const char * monthNames[] =
{"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
0};
struct tm * t = (dest && (destLen>30)) ? gmtime(&now) : NULL;
if( ! t || (destLen<31) ) return NULL;
else
{
int const rc = sprintf( dest,
"%s, %d %s %02d %02d:%02d:%02d GMT",
dayNames[t->tm_wday], t->tm_mday,
monthNames[t->tm_mon],
t->tm_year+1900, t->tm_hour,
t->tm_min, t->tm_sec
);
assert( (rc>0) && ((unsigned int)rc) < destLen );
return dest;
}
}
static int th1ish_cgi_kvp_visitor_cookies( cwal_kvp const * kvp,
void * state ){
enum { TSBufSize = 32 };
char tsBuf[TSBufSize] = {0} /* buffer for expiry timestamp */;
th1ish_cgi * cx = (th1ish_cgi*)state;
int rc;
char const * key;
cwal_size_t keyLen = 0;
cwal_value const * val;
key = cwal_value_get_cstr(cwal_kvp_key(kvp), &keyLen);
if(!key || !*key) return 0 /* ignore non-string keys */;
val = cwal_kvp_value(kvp);
if( cwal_value_is_null(val) ){
cwal_outputf(cx->e,"Set-Cookie: %.*s=; Expires=Thu, "
"01-Jan-1970 00:00:01 GMT\r\n",
(int)keyLen, key);
return 0;
}
else if( cwal_value_is_object(val) ){
/*
Accept in Object in the form:
{
value: VALUE,
domain: string,
path: string,
secure: bool,
httponly: bool,
expires: integer
}
*/
cwal_object * obj = cwal_value_get_object( val );
cwal_value const * cv = cwal_object_get( obj, "value", 5 );
char const * valstr = NULL;
char const isNull = !cv || cwal_value_is_null( cv );
if( isNull ){
cwal_outputf(cx->e, "Set-Cookie: %.*s=", (int)keyLen, key);
}
else{
/* FIXME: streamify urlencode so we can get around fixed buffer size. */
valstr = th1ish_cgi_pod_to_string( cx, cv, &cx->tmpBuf );
if( ! valstr ) return 0;
else {
rc = th1ish_cgi_urlencode( cx, valstr, cx->tmpBuf.used,
&cx->tmpBuf2 );
if(rc) return rc;
cwal_outputf(cx->e, "Set-Cookie: %.*s=%.*s",
(int)keyLen, key, (int)cx->tmpBuf2.used,
(char const *)cx->tmpBuf2.mem);
cx->tmpBuf2.used = 0;
}
}
#define DOPART(KEY,KEY2) cv = cwal_object_get( obj, KEY, strlen(key) ); \
if( cv ) { \
valstr = th1ish_cgi_pod_to_string( cx, cv, &cx->tmpBuf ); \
if( valstr ) { \
cwal_outputf( cx->e, "; "KEY2"=%.*s", (int)cx->tmpBuf.used, valstr ); \
} \
cx->tmpBuf.used = 0; \
} (void)0
DOPART("domain","Domain");
DOPART("path","Path");
#undef DOPART
cv = cwal_object_get( obj, "expires", 7 );
if( cv || isNull ){
cwal_int_t const intVal = isNull ? 1 : cwal_value_get_integer(cv);
if( intVal ){
valstr = th1ish_cgi_rfc822_timedate( (time_t)intVal, tsBuf, TSBufSize );
if( valstr ){
cwal_outputf( cx->e, "; Expires=%s", valstr );
}
}
}
cv = cwal_object_get( obj, "secure", 6 );
if( cwal_value_get_bool(cv) ){
cwal_output( cx->e, "; Secure", 8 );
}
cv = cwal_object_get( obj, "httpOnly", 8 );
if( cwal_value_get_bool(cv) ){
cwal_output( cx->e, "; HttpOnly", 10 );
}
cwal_output( cx->e, "\r\n", 2);
}
else{
char const * valstr;
valstr = th1ish_cgi_pod_to_string( cx, val, &cx->tmpBuf);
if( ! valstr ) return 0;
else{
rc = th1ish_cgi_urlencode( cx, valstr, cx->tmpBuf.used, &cx->tmpBuf2 );
if( rc ) return rc;
cwal_outputf(cx->e,"Set-Cookie: %.*s=%.*s\r\n",
(int)keyLen, key, (int)cx->tmpBuf2.used,
(char const *)cx->tmpBuf2.mem);
cx->tmpBuf2.used = 0;
}
}
return 0;
}
static char const * th1ish_cgi_http_status_cstr(int httpCode){
if(httpCode>=500){
switch(httpCode){
case 501: return "Not Implemented";
case 502: return "Temporarily Unavailable";
case 503: return "Gateway Timeout";
case 500:
default:
return "Internal Server Error";
}
}else if (httpCode>=400){
switch(httpCode){
case 401: return "Unauthorized";
case 402: return "PaymentRequired";
case 403: return "Forbidden";
case 404: return "Not Found";
case 400:
default:
return "Bad Request";
}
}else if (httpCode>=300){
switch(httpCode){
case 301: return "Moved";
case 302: return "Found";
case 303: return "Method";
case 304: return "Not Modified";
case 300:
default:
return "Redirect";
}
}else if (httpCode>=200){
switch(httpCode){
case 201: return "Created";
case 202: return "Accepted";
case 203: return "Partial Information";
case 204: return "No Response";
case 200:
default:
return "OK";
}
}else{
return "???";
}
}
int th1ish_cgi_response_output_headers(th1ish_cgi * cx)
{
enum { BufSize = 64 };
cwal_value * jo = NULL;
int rc;
if(cx->response.contentType){
rc = cwal_outputf(cx->e, "Content-type: %s\r\n", cx->response.contentType);
if(rc) return rc;
}/* else assume it's been set as a header */
rc = cwal_outputf(cx->e, "Status: %d %s\r\n", cx->response.httpCode,
cx->response.httpStatus
? cx->response.httpStatus
: th1ish_cgi_http_status_cstr(cx->response.httpCode));
if(rc) return rc;
jo = cx->response.headers;
if( jo ){
rc = cwal_props_visit_kvp( jo, th1ish_cgi_kvp_visitor_headers,
cx );
if(rc) return rc;
}
if(cx->response.cookies){
rc = cwal_props_visit_kvp( cx->response.cookies,
th1ish_cgi_kvp_visitor_cookies,
cx );
}
if( 0 == rc ){
cwal_output(cx->e, "\r\n", 2);
}
return rc;
}
int th1ish_cgi_response_status_set(th1ish_cgi * cx, int httpCode,
char const * msg){
size_t msgLen;
if(!cx) return CWAL_RC_MISUSE;
if(!msg || !*msg){
msg = th1ish_cgi_http_status_cstr(httpCode);
}
cx->response.httpCode = httpCode;
cwal_free(cx->e, cx->response.httpStatus);
msgLen = msg ? strlen(msg) : 0;
if(!msgLen) cx->response.httpStatus = NULL;
else{
cx->response.httpStatus = (char *)cwal_malloc(cx->e, msgLen+1);
if(!cx->response.httpStatus) return CWAL_RC_OOM;
memcpy(cx->response.httpStatus, msg, msgLen);
cx->response.httpStatus[msgLen] = 0;
}
return 0;
}
/**
TH1ISH_CGI_BUFFER determines whether this module implicitly pushes
an output buffering level (in which case it also relies on
buffering being set) or not.
*/
#define TH1ISH_CGI_BUFFER 0
int th1ish_cgi_response_output_all(th1ish_cgi * cx){
int rc = 0;
char doHeaders;
cwal_buffer body = cwal_buffer_empty;
if(!cx) return CWAL_RC_MISUSE;
doHeaders = cx->response.headerMode;
if( doHeaders < 0 ){
doHeaders = getenv("GATEWAY_INTERFACE") ? 1 : 0;
}
if(th1ish_ob_level(cx->ie)){
while(1 < th1ish_ob_level(cx->ie)){
rc = th1ish_ob_flush(cx->ie);
assert(!rc);
if(rc) goto end;
th1ish_ob_pop(cx->ie);
}
th1ish_ob_take( cx->ie, &body );
th1ish_ob_pop(cx->ie);
assert(0 == th1ish_ob_level(cx->ie));
}
if( doHeaders > 0 ){
rc = th1ish_cgi_response_output_headers(cx);
if(rc) goto end;
}
/** Output the body... */
if(body.used){
rc = cwal_output(cx->e, body.mem, body.used);
if(rc) goto end;
}
rc = cwal_output_flush( cx->e );
end:
cwal_buffer_reserve(cx->e, &body, 0);
return rc;
}
cwal_value * th1ish_cgi_path_part( th1ish_cgi * cx,
unsigned short ndx ){
cwal_value * piV = th1ish_cgi_getenv( cx, "e", "PATH_INFO_SPLIT", 15 );
return piV ? cwal_array_get(cwal_value_get_array(piV), ndx) : NULL;
}
char const * th1ish_cgi_path_part_cstr( th1ish_cgi * cx, unsigned short ndx,
cwal_size_t * strLen )
{
cwal_value * v = th1ish_cgi_path_part( cx, ndx );
return v
? cwal_value_get_cstr( v, strLen )
: NULL;
}
int th1ish_cgi_init( th1ish_interp * ie, th1ish_cgi * cx ){
int rc;
if(!ie || !ie->e || !cx) return CWAL_RC_MISUSE;
*cx = th1ish_cgi_empty;
cx->ie = ie;
cx->e = ie->e;
cx->topScope = ie->topScope;
rc = th1ish_cgi_import_environ(cx);
cx->request.input = stdin /*TODO: make configurable*/;
if(!rc) rc = th1ish_cgi_parse_cookies(cx, getenv("HTTP_COOKIE") );
if(!rc) rc = th1ish_cgi_parse_query_string( cx, getenv("QUERY_STRING") );
if(!rc) rc = th1ish_cgi_init_POST(cx);
#if TH1ISH_CGI_BUFFER
if(!rc) rc = th1ish_ob_push(ie);
#endif
if(rc){
th1ish_cgi_err_set( cx, rc,
"CGI initialization failed "
"with code %d (%s).",
rc, cwal_rc_cstr(rc) );
}
return rc;
}
void th1ish_cgi_cleanup( th1ish_cgi * cgi ){
if(cgi && cgi->ie){
cwal_buffer_reserve( cgi->e, &cgi->tmpBuf, 0 );
cwal_buffer_reserve( cgi->e, &cgi->tmpBuf2, 0 );
cwal_buffer_reserve( cgi->e, &cgi->err.msg, 0 );
#if TH1ISH_CGI_BUFFER
while(th1ish_ob_level(cgi->ie)){
th1ish_ob_pop(cgi->ie);
}
#endif
cwal_free(cgi->e, cgi->response.httpStatus);
*cgi = th1ish_cgi_empty;
}
/* We leave the rest of the pointers for cgi->e to clean up
because we cannot know here if it's already done so or not.
*/
}
cwal_value * th1ish_cgi_env_get( th1ish_cgi * cx,
char which,
char createIfNeeded ){
cwal_value ** v = NULL;
char const * gckey = NULL;
switch( which )
{
case 'c':
case 'C':
gckey = th1ish_cgi_keys.ENV_COOKIE;
v = &cx->request.cookies;
break;
case 'e':
case 'E':
gckey = th1ish_cgi_keys.ENV_SYS;
v = &cx->request.env;
break;
case 'h':
case 'H':
gckey = th1ish_cgi_keys.RESPONSE_HEADERS;
v = &cx->response.headers;
break;
case 'g':
case 'G':
gckey = th1ish_cgi_keys.ENV_GET;
v = &cx->request.get;
break;
case 'f':
case 'F':
gckey = th1ish_cgi_keys.ENV_CONFIG;
v = &cx->config;
break;
case 'p':
case 'P':
gckey = th1ish_cgi_keys.ENV_POST;
v = &cx->request.post;
break;
#if 0
case 'a':
case 'A':
gckey = th1ish_cgi_keys.ENV_APP;
v = &cx->clientEnv;
break;
case 's':
case 'S':
gckey = th1ish_cgi_keys.ENV_SESSION;
v = &cx->session.env;
break;
#endif
default:
break;
}
if( v ){
if( !*v && createIfNeeded ){
*v = cwal_new_object_value(cx->e);
if(*v){
if(th1ish_stash_set(cx->ie, gckey, *v)){
cwal_value_unref(*v);
*v = NULL;
}else{
cwal_value_rescope(cx->topScope, *v);
}
}
}
}
return v ? *v : NULL;
}
cwal_value * th1ish_cgi_getenv( th1ish_cgi * cx, char const * fromWhere,
char const * key, cwal_size_t keyLen )
{
cwal_value * jv = NULL;
if( !key || !*key ) return NULL;
else if( !fromWhere || !*fromWhere ) fromWhere = TH1ISH_CGI_GETENV_DEFAULT;
for( ; *fromWhere ; ++fromWhere ){
if( ('r'==*fromWhere)||('R'==*fromWhere) ){
jv = th1ish_cgi_getenv( cx, "gpc", key, keyLen );
}else{
jv = th1ish_cgi_env_get( cx, *fromWhere, 0 );
}
if( jv ){
jv = cwal_prop_get( jv, key, keyLen );
if(jv) return jv;
}
}
return NULL;
}
char const * th1ish_cgi_getenv_cstr( th1ish_cgi * cx, char const * where,
char const * key, cwal_size_t keyLen,
cwal_size_t * strLen )
{
cwal_value * v = th1ish_cgi_getenv(cx, where, key, keyLen);
return v ? cwal_value_get_cstr(v, strLen) : NULL;
}
int th1ish_cgi_setenv_v( th1ish_cgi * cx, char env,
char const * key,
cwal_size_t keyLen,
cwal_value * v ){
if( !cx || !env || !key || !*key ) return CWAL_RC_MISUSE;
else
{
cwal_value * e = th1ish_cgi_env_get( cx, env, 1 );
return !e
? CWAL_RC_OOM /* FIXME: expand the above code so we
can distinguish between invalid
env and allocation error. (Except that
there is no allocation on get_obj().*/
: cwal_prop_set( e, key, keyLen, v )
;
}
}
int th1ish_cgi_setenv( th1ish_cgi * cx, char const * key,
cwal_size_t keyLen, cwal_value * v ){
return th1ish_cgi_setenv_v( cx, 'e', key, keyLen, v );
}
int th1ish_cgi_response_header_add( th1ish_cgi * cx, char const * key,
cwal_size_t keyLen, cwal_value * v )
{
cwal_value * env;
if( !cx || ! key || !*key ) return CWAL_RC_MISUSE;
env = th1ish_cgi_env_get( cx, 'h', 1 );
if(!env) return CWAL_RC_OOM;
return cwal_prop_set( env, key, keyLen, v );
}
static int th1ish_cgi_response_init_cookies(th1ish_cgi * cx){
if(!cx->response.cookies){
cx->response.cookies = cwal_new_object_value(cx->e);
if(cx->response.cookies){
th1ish_stash_set(cx->ie, th1ish_cgi_keys.ENV_COOKIE,
cx->response.cookies);
}
}
return cx->response.cookies ? 0 : CWAL_RC_OOM;
}
int th1ish_cgi_cookie_set( th1ish_cgi * cx,
char const * key,
cwal_size_t keyLen,
cwal_value * v ){
if( !key || !*key ) return CWAL_RC_MISUSE;
else{
int rc = th1ish_cgi_response_init_cookies(cx);
/* MARKER(("Setting cookie [%s]\n", key)); */
if(!rc) rc = cwal_prop_set( cx->response.cookies, key, keyLen, v );
return rc;
}
}
int th1ish_cgi_cookie_set2( th1ish_cgi * cx,
char const * key, cwal_size_t keyLen,
cwal_value * v,
char const * domain, char const * path,
unsigned int expires, char secure,
char httponly )
{
if( ! key || !*key ) return CWAL_RC_MISUSE;
else
{
int rc;
cwal_value * x = NULL;
cwal_value * jo = cwal_new_object_value(cx->e);
if( ! jo ) return CWAL_RC_OOM;
if( !v ) v = cwal_value_null();
#define SET(KEY) if(!x) rc=CWAL_RC_OOM; \
if( rc || (rc = cwal_prop_set( jo, KEY, strlen(KEY), x) ) ) { \
if(x) cwal_value_unref(x); \
cwal_value_unref( jo ); \
return rc; \
}
rc = cwal_prop_set( jo, "value", 5, v);
if(rc) return rc;
if(domain){
x = cwal_new_string_value( cx->e, domain, strlen(domain) );
SET("domain");
}
if( path ){
x = cwal_new_string_value( cx->e, path, strlen(path) );
SET("path");
}
if( cwal_value_is_null(v) ){
x = cwal_new_integer( cx->e, 1 );
SET("expires");
}
else if( expires ){
x = cwal_new_integer( cx->e, (cwal_int_t) expires );
SET("expires");
}
if( secure ){
x = cwal_new_bool(secure);
SET("secure");
}
if( httponly ){
x = cwal_new_bool(httponly);
SET("httpOnly");
}
#undef SET
rc = th1ish_cgi_cookie_set( cx, key, keyLen, jo );
if(rc) cwal_value_unref( jo );
return rc;
}
}
/**
th1ish_cgi_hexchar_to_int():
For 'a'-'f', 'A'-'F' and '0'-'9', returns the appropriate decimal
number. For any other character it returns -1.
*/
static int th1ish_cgi_hexchar_to_int( int ch ){
if( (ch>='a' && ch<='f') ) return ch-'a'+10;
else if( (ch>='A' && ch<='F') ) return ch-'A'+10;
else if( (ch>='0' && ch<='9') ) return ch-'0';
return -1;
}
void th1ish_cgi_urldecode_inline( char * str, cwal_size_t * sLen ){
unsigned char ch = 0;
unsigned char cx1 = 0;
unsigned char cx2 = 0;
int decoded;
unsigned char * pos = (unsigned char *)str;
unsigned char * out = pos;
unsigned char const * end;
size_t slen = (str && *str) ? (sLen ? *sLen : strlen(str)) : 0;
if( !slen ) return;
end = pos + slen;
for( ; pos < end; ++pos ){
ch = *pos;
if( ch == '%' )
{
cx1 = *(pos+1);
/* FIXME: with only minor refactoring we can remove the
isxdigit() calls and use th1ish_cgi_hexchar_to_int()
instead, checking for a negative return value. That
would potentially save us 2 extra function calls here.
*/
if( isxdigit(cx1) ){
cx2 = *(pos+2);
if( isxdigit(cx2) )
{
decoded = (th1ish_cgi_hexchar_to_int( cx1 ) * 16)
+ th1ish_cgi_hexchar_to_int( cx2 );
*(out++) = (char)decoded;
pos += 2;
continue;
}
/* else fall through... */
}
/* else fall through... */
}
else if( ch == '+' )
{
*(out++) = ' ';
continue;
}
*(out++) = ch;
}
*out = 0;
if(sLen) *sLen = (out - (unsigned char *)str);
}
int th1ish_cgi_urlencode( th1ish_cgi * cx, char const * src,
cwal_size_t srcLen,
cwal_buffer * _dest ){
#define needs_escape \
( (ch >= 32 && ch <=47) \
|| ( ch>=58 && ch<=64) \
|| ( ch>=91 && ch<=96) \
|| ( ch>=123 && ch<=126) \
|| ( ch<32 || ch>=127) \
)
char const * pos = src;
char const * end = src + srcLen;
char * dest;
char ch;
int rc;
static char const * hex = "0123456789ABCDEF";
if( !cx || !src ) return CWAL_RC_MISUSE;
rc = cwal_buffer_reserve( cx->e, _dest,
_dest->used + (srcLen*3) + 1);
if(rc) return rc;
dest = (char *)_dest->mem + _dest->used;
for( ; (pos<end) && *pos; ++pos ){
ch = *pos;
if( ! needs_escape ){
*(dest++) = ch;
continue;
}
else
{
*(dest++) = '%';
*(dest++) = hex[((ch>>4)&0xf)];
*(dest++) = hex[(ch&0xf)];
}
}
_dest->used = dest - (char *)_dest->mem;
return 0;
#undef needs_escape
}
#if 0
char const * th1ish_cgi_guess_content_type(){
char const * cset;
char doUtf8;
char const * cstr;
cset = getenv("HTTP_ACCEPT_CHARSET");
doUtf8 = (cset && strstr("utf-8",cset))
? 1 : 0;
cstr = getenv("HTTP_ACCEPT");
if( strstr( cstr, "application/json" )
|| strstr( cstr, "*/*" ) ){
return doUtf8
? "application/json; charset=utf-8"
: "application/json";
}else{
return NULL /*"text/plain"*/;
}
}
#endif
#undef TH1ISH_CGI_BUFFER
#undef MARKER