/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ #include "s2_cgi.h" #include #include /* isxdigit() */ #include /* strlen() */ #include /*getenv() and friends */ /** Only for debuggering... */ #include #define MARKER(pfexp) \ do{ printf("MARKER: %s:%d:%s():\t",__FILE__,__LINE__,__func__); \ printf pfexp; \ } while(0) /* Whether or not to import request.ENV by default (costly and rarely used, but convenient during testing). */ #define S2_CGI_IMPORT_ENV 1 const s2_cgi s2_cgi_empty = s2_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; } s2_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 s2_cgi_import_path_info(s2_cgi *cx) { char const * head = s2_cgi_getenv_cstr(cx, "e", "PATH_INFO", 9, NULL); if( NULL == head ) return 0; else{ cwal_value * env = s2_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; s2_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 s2_cgi_import_environ(s2_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 = s2_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, cwal_strlen(keybuf), jv ); if( 0 != rc ) { cwal_value_unref(jv); break; } } if( !rc ){ rc = s2_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 s2_cgi_parse_param_list( s2_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( ; (keypos >= 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 s2_cgi_err_set( s2_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 s2_cgi_parse_POST_JSON(s2_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 s2_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 s2_stash_set(cx->se, s2_cgi_keys.ENV_POST, jv) /* If that fails, jv is still owned by the current scope, else it's global and vacuum-proof. */; } #define S2_CGI_ENABLE_POST_FORM_URLENCODED 1 #if S2_CGI_ENABLE_POST_FORM_URLENCODED static int s2_cgi_parse_post_urlencoded( s2_cgi * cx, char const * qstr ) { cwal_value * env = NULL; if( !qstr || !*qstr ) return 0; assert(cx); env = s2_cgi_env_get( cx, 'p', 1 ); return env ? s2_cgi_parse_param_list( cx, env, qstr, '&', 1, 0 ) : CWAL_RC_OOM; } #endif static int s2_cgi_init_POST(s2_cgi * cx){ if( ! cx || !cx->request.input ) return CWAL_RC_MISUSE; else{ FILE * src = cx->request.input; char const * ctype = s2_cgi_getenv_cstr( cx, "e", "CONTENT_TYPE", 12, NULL ); if( NULL == ctype ) return 0; else{ char const * clen = s2_cgi_getenv_cstr( cx, "e", "CONTENT_LENGTH", 14, NULL ); if( NULL == clen ){ return s2_cgi_err_set(cx, CWAL_RC_RANGE, "CONTENT_LENGTH not specified."); } #if S2_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 = s2_cgi_err_set(cx, CWAL_RC_RANGE, "Not allowing binary POST input."); goto end_clean; } #endif rc = s2_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 s2_cgi_parse_POST_JSON(cx, src, len); } else{ return s2_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 * s2_cgi_pod_to_string( s2_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)) ){ s2_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)))){ s2_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)))){ s2_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)) ){ s2_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)) ){ s2_cgi_err_set(cx, rc, NULL); return NULL; } return (char *)dest->mem; } else{ return NULL; } } } static int s2_cgi_kvp_visitor_headers( cwal_kvp const * kvp, void * state ){ s2_cgi * cx = (s2_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 = s2_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 * s2_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 s2_cgi_kvp_visitor_cookies( cwal_kvp const * kvp, void * state ){ enum { TSBufSize = 32 }; char tsBuf[TSBufSize] = {0} /* buffer for expiry timestamp */; s2_cgi * cx = (s2_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 = s2_cgi_pod_to_string( cx, cv, &cx->tmpBuf ); if( ! valstr ) return 0; else { rc = s2_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 = s2_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 = s2_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 = s2_cgi_pod_to_string( cx, val, &cx->tmpBuf); if( ! valstr ) return 0; else{ rc = s2_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 * s2_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 s2_cgi_response_output_headers(s2_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 : s2_cgi_http_status_cstr(cx->response.httpCode)); if(rc) return rc; jo = cx->response.headers; if( jo ){ rc = cwal_props_visit_kvp( jo, s2_cgi_kvp_visitor_headers, cx ); if(rc) return rc; } if(cx->response.cookies){ rc = cwal_props_visit_kvp( cx->response.cookies, s2_cgi_kvp_visitor_cookies, cx ); } if( 0 == rc ){ cwal_output(cx->e, "\r\n", 2); } return rc; } int s2_cgi_response_status_set(s2_cgi * cx, int httpCode, char const * msg){ size_t msgLen; if(!cx) return CWAL_RC_MISUSE; if(!msg || !*msg){ msg = s2_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; } /** S2_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 S2_CGI_BUFFER 0 int s2_cgi_response_output_all(s2_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(s2_ob_level(cx->se)){ while(1 < s2_ob_level(cx->se)){ rc = s2_ob_flush(cx->se); assert(!rc); if(rc) goto end; s2_ob_pop(cx->se); } s2_ob_take( cx->se, &body ); s2_ob_pop(cx->se); assert(0 == s2_ob_level(cx->se)); } if( doHeaders > 0 ){ rc = s2_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; } #if S2_CGI_IMPORT_ENV /* broken by removal of request.ENV object... */ cwal_value * s2_cgi_path_part( s2_cgi * cx, unsigned short ndx ){ cwal_value * piV = s2_cgi_getenv( cx, "e", "PATH_INFO_SPLIT", 15 ); return piV ? cwal_array_get(cwal_value_get_array(piV), ndx) : NULL; } char const * s2_cgi_path_part_cstr( s2_cgi * cx, unsigned short ndx, cwal_size_t * strLen ) { cwal_value * v = s2_cgi_path_part( cx, ndx ); return v ? cwal_value_get_cstr( v, strLen ) : NULL; } #endif int s2_cgi_init( s2_engine * se, s2_cgi * cx ){ int rc = 0; if(!se || !se->e || !cx) return CWAL_RC_MISUSE; *cx = s2_cgi_empty; cx->se = se; cx->e = se->e; cx->topScope = cwal_scope_top(cwal_scope_current_get(se->e)); if(S2_CGI_IMPORT_ENV){ /* ENV is really big and rarely used, so we'll rely on getenv(). Note that this breaks (removes) PATH_INFO_SPLIT. */ rc = s2_cgi_import_environ(cx); } cx->request.input = stdin /*TODO: make configurable*/; if(!rc) rc = s2_cgi_parse_cookies(cx, getenv("HTTP_COOKIE") ); if(!rc) rc = s2_cgi_parse_query_string( cx, getenv("QUERY_STRING") ); if(!rc) rc = s2_cgi_init_POST(cx); #if S2_CGI_BUFFER if(!rc) rc = s2_ob_push(ie); #endif if(rc){ s2_cgi_err_set( cx, rc, "CGI initialization failed " "with code %d (%s).", rc, cwal_rc_cstr(rc) ); } return rc; } void s2_cgi_cleanup( s2_cgi * cgi ){ if(cgi && cgi->se){ 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 S2_CGI_BUFFER while(s2_ob_level(cgi->se)){ s2_ob_pop(cgi->se); } #endif cwal_free(cgi->e, cgi->response.httpStatus); *cgi = s2_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 * s2_cgi_env_get( s2_cgi * cx, char which, char createIfNeeded ){ cwal_value ** v = NULL; char const * gckey = NULL; switch( which ) { case 'c': case 'C': gckey = s2_cgi_keys.ENV_COOKIE; v = &cx->request.cookies; break; case 'e': case 'E': gckey = s2_cgi_keys.ENV_SYS; v = &cx->request.env; break; case 'h': case 'H': gckey = s2_cgi_keys.RESPONSE_HEADERS; v = &cx->response.headers; break; case 'g': case 'G': gckey = s2_cgi_keys.ENV_GET; v = &cx->request.get; break; case 'f': case 'F': gckey = s2_cgi_keys.ENV_CONFIG; v = &cx->config; break; case 'p': case 'P': gckey = s2_cgi_keys.ENV_POST; v = &cx->request.post; break; #if 0 case 'a': case 'A': gckey = s2_cgi_keys.ENV_APP; v = &cx->clientEnv; break; case 's': case 'S': gckey = s2_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(s2_stash_set(cx->se, gckey, *v)){ cwal_value_unref(*v); *v = NULL; } } } } return v ? *v : NULL; } cwal_value * s2_cgi_getenv( s2_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 = S2_CGI_GETENV_DEFAULT; for( ; *fromWhere ; ++fromWhere ){ if( ('r'==*fromWhere)||('R'==*fromWhere) ){ jv = s2_cgi_getenv( cx, "gpc", key, keyLen ); }else{ jv = s2_cgi_env_get( cx, *fromWhere, 0 ); } if( jv ){ jv = cwal_prop_get( jv, key, keyLen ); if(jv) return jv; } } return NULL; } char const * s2_cgi_getenv_cstr( s2_cgi * cx, char const * where, char const * key, cwal_size_t keyLen, cwal_size_t * strLen ) { cwal_value * v = s2_cgi_getenv(cx, where, key, keyLen); return v ? cwal_value_get_cstr(v, strLen) : NULL; } int s2_cgi_setenv_v( s2_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 = s2_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 s2_cgi_setenv( s2_cgi * cx, char const * key, cwal_size_t keyLen, cwal_value * v ){ return s2_cgi_setenv_v( cx, 'e', key, keyLen, v ); } int s2_cgi_response_header_add( s2_cgi * cx, char const * key, cwal_size_t keyLen, cwal_value * v ) { cwal_value * env; if( !cx || ! key || !*key ) return CWAL_RC_MISUSE; env = s2_cgi_env_get( cx, 'h', 1 ); if(!env) return CWAL_RC_OOM; return cwal_prop_set( env, key, keyLen, v ); } static int s2_cgi_response_init_cookies(s2_cgi * cx){ if(!cx->response.cookies){ cx->response.cookies = cwal_new_object_value(cx->e); if(cx->response.cookies){ s2_stash_set(cx->se, s2_cgi_keys.ENV_COOKIE, cx->response.cookies); } } return cx->response.cookies ? 0 : CWAL_RC_OOM; } int s2_cgi_cookie_set( s2_cgi * cx, char const * key, cwal_size_t keyLen, cwal_value * v ){ if( !key || !*key ) return CWAL_RC_MISUSE; else{ int rc = s2_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 s2_cgi_cookie_set2( s2_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 = s2_cgi_cookie_set( cx, key, keyLen, jo ); if(rc) cwal_value_unref( jo ); return rc; } } /** s2_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 s2_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 s2_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 s2_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 = (s2_cgi_hexchar_to_int( cx1 ) * 16) + s2_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 s2_cgi_urlencode( s2_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>4)&0xf)]; *(dest++) = hex[(ch&0xf)]; } } _dest->used = dest - (char *)_dest->mem; return 0; #undef needs_escape } #if 0 char const * s2_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 S2_CGI_BUFFER #undef MARKER #undef S2_CGI_IMPORT_ENV