623 lines
14 KiB
C
623 lines
14 KiB
C
/****************************************************************************
|
|
* This file is part of the project AqCGI.
|
|
* AqCGI (c) by 2024 Martin Preuss, all rights reserved.
|
|
*
|
|
* The license for this file can be found in the file COPYING which you
|
|
* should have received along with this file.
|
|
****************************************************************************/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
# include <config.h>
|
|
#endif
|
|
|
|
#include "./cgi.h"
|
|
|
|
#include <gwenhywfar/db.h>
|
|
#include <gwenhywfar/syncio.h>
|
|
#include <gwenhywfar/text.h>
|
|
#include <gwenhywfar/debug.h>
|
|
#include <gwenhywfar/cryptkeysym.h>
|
|
#include <gwenhywfar/mdigest.h>
|
|
#include <gwenhywfar/syncio_file.h>
|
|
#include <gwenhywfar/error.h>
|
|
|
|
#include <ctype.h>
|
|
#include <stdio.h>
|
|
#include <unistd.h>
|
|
#include <errno.h>
|
|
|
|
|
|
#define AQCGI_VARNAME_TEXT "text"
|
|
|
|
|
|
|
|
/* ------------------------------------------------------------------------------------------------
|
|
* external declarations
|
|
* ------------------------------------------------------------------------------------------------
|
|
*/
|
|
|
|
|
|
extern char **environ;
|
|
|
|
|
|
/* ------------------------------------------------------------------------------------------------
|
|
* forward declarations
|
|
* ------------------------------------------------------------------------------------------------
|
|
*/
|
|
|
|
static int _handleHeaders(AQCGI_REQUEST *rq);
|
|
static int _handleBody(AQCGI_REQUEST *rq);
|
|
static int _finishAndSendHeader(AQCGI_REQUEST *rq, GWEN_SYNCIO *sio);
|
|
static int _sendBody(AQCGI_REQUEST *rq, GWEN_SYNCIO *sio);
|
|
static int _unescapeUrlEncoded(const char *src, unsigned int srclen, char *buffer, unsigned int maxsize);
|
|
static GWEN_DB_NODE *_parseEnv(void);
|
|
static int _parseEnvVarIntoDb(const char *var, GWEN_DB_NODE *db);
|
|
static void _parseCookieHeaderIntoDb(const char *s, GWEN_DB_NODE *db);
|
|
|
|
|
|
|
|
|
|
/* ------------------------------------------------------------------------------------------------
|
|
* implementations
|
|
* ------------------------------------------------------------------------------------------------
|
|
*/
|
|
|
|
int AQCGI_ReadForcedStdInToBuffer(int len, GWEN_BUFFER *buf)
|
|
{
|
|
GWEN_SYNCIO *sio;
|
|
int rv;
|
|
|
|
sio=GWEN_SyncIo_File_fromStdin();
|
|
GWEN_Buffer_AllocRoom(buf, len);
|
|
rv=GWEN_SyncIo_ReadForced(sio, (uint8_t *) GWEN_Buffer_GetPosPointer(buf), len);
|
|
if (rv<0) {
|
|
DBG_INFO(AQCGI_LOGDOMAIN, "here (%d)", rv);
|
|
GWEN_SyncIo_free(sio);
|
|
return rv;
|
|
}
|
|
GWEN_Buffer_IncrementPos(buf, rv);
|
|
GWEN_Buffer_AdjustUsedBytes(buf);
|
|
GWEN_SyncIo_free(sio);
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
int AQCGI_GenerateSessionId(GWEN_BUFFER *buf)
|
|
{
|
|
GWEN_CRYPT_KEY *sk;
|
|
uint8_t *ptr;
|
|
uint32_t len;
|
|
int rv;
|
|
|
|
sk=GWEN_Crypt_KeyDes3K_Generate(GWEN_Crypt_CryptMode_Cbc, 24, 2);
|
|
if (sk==NULL) {
|
|
DBG_INFO(AQCGI_LOGDOMAIN, "Could not generate DES key");
|
|
return GWEN_ERROR_IO;
|
|
}
|
|
ptr=GWEN_Crypt_KeyDes3K_GetKeyDataPtr(sk);
|
|
len=GWEN_Crypt_KeyDes3K_GetKeyDataLen(sk);
|
|
|
|
rv=GWEN_Text_ToHexBuffer((const char*) ptr, len, buf,0, 0, 0);
|
|
if (rv<0) {
|
|
DBG_INFO(AQCGI_LOGDOMAIN, "here (%d)", rv);
|
|
GWEN_Crypt_Key_free(sk);
|
|
return rv;
|
|
}
|
|
GWEN_Crypt_Key_free(sk);
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
int AQCGI_HashMd256ToBuffer(const char *input, GWEN_BUFFER *buf)
|
|
{
|
|
GWEN_MDIGEST *md;
|
|
int rv;
|
|
int digestSize;
|
|
uint8_t tmpBuf[32];
|
|
|
|
md=GWEN_MDigest_Sha256_new();
|
|
digestSize=GWEN_MDigest_GetDigestSize(md);
|
|
assert(digestSize<=sizeof(tmpBuf));
|
|
rv=GWEN_MDigest_Digest(md, (const uint8_t*) input, strlen(input), tmpBuf, sizeof(tmpBuf));
|
|
GWEN_MDigest_free(md);
|
|
if (rv<0) {
|
|
DBG_ERROR(AQCGI_LOGDOMAIN, "Error hashing input (%d)", rv);
|
|
return rv;
|
|
}
|
|
rv=GWEN_Text_ToHexBuffer((const char*) tmpBuf, digestSize, buf, 0, 0, 0);
|
|
if (rv<0) {
|
|
DBG_ERROR(AQCGI_LOGDOMAIN, "Error converting hash to hex (%d)", rv);
|
|
return rv;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
int AQCGI_ParseUrlEncoded(const char *s, int contentLength, GWEN_DB_NODE *dbDecoded)
|
|
{
|
|
while(contentLength>0) {
|
|
const char *sNameStart;
|
|
int nameLen;
|
|
|
|
while((contentLength>0) && (*s<33)) {
|
|
contentLength--;
|
|
s++;
|
|
}
|
|
sNameStart=s;
|
|
while((contentLength>0) && (*s!='=') && (*s!='&')) {
|
|
contentLength--;
|
|
s++;
|
|
}
|
|
nameLen=s-sNameStart;
|
|
|
|
if ((contentLength>0) && (*s=='=')) {
|
|
const char *sValueStart;
|
|
int valueLen;
|
|
|
|
s++;
|
|
contentLength--;
|
|
while((contentLength>0) && (*s<33)) {
|
|
s++;
|
|
contentLength--;
|
|
}
|
|
sValueStart=s;
|
|
while((contentLength>0) && (*s!='&')) {
|
|
contentLength--;
|
|
s++;
|
|
}
|
|
valueLen=s-sValueStart;
|
|
|
|
if (nameLen && valueLen) {
|
|
char sNameBuf[32];
|
|
char sValueBuf[64];
|
|
|
|
if (_unescapeUrlEncoded(sNameStart, nameLen, sNameBuf, sizeof(sNameBuf))>=0 &&
|
|
_unescapeUrlEncoded(sValueStart, valueLen, sValueBuf, sizeof(sValueBuf))>=0)
|
|
GWEN_DB_SetCharValue(dbDecoded, 0, sNameBuf, sValueBuf);
|
|
else {
|
|
DBG_ERROR(AQCGI_LOGDOMAIN, "Either name or value invalid in body, aborting");
|
|
return GWEN_ERROR_BAD_DATA;
|
|
}
|
|
}
|
|
if ((contentLength>0) && (*s=='&')) {
|
|
contentLength--;
|
|
s++;
|
|
}
|
|
}
|
|
} /* while */
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
AQCGI_REQUEST *AQCGI_ReadRequest(void)
|
|
{
|
|
AQCGI_REQUEST *rq;
|
|
int rv;
|
|
|
|
rq=AQCGI_Request_new();
|
|
|
|
rv=_handleHeaders(rq);
|
|
if (rv<0) {
|
|
DBG_INFO(AQCGI_LOGDOMAIN, "here (%d)", rv);
|
|
AQCGI_SendResponseWithStatus(rq, 500, "Internal error");
|
|
AQCGI_Request_free(rq);
|
|
return NULL;
|
|
}
|
|
|
|
rv=_handleBody(rq);
|
|
if (rv<0) {
|
|
DBG_INFO(AQCGI_LOGDOMAIN, "here (%d)", rv);
|
|
AQCGI_SendResponseWithStatus(rq, 500, "Internal error");
|
|
AQCGI_Request_free(rq);
|
|
return NULL;
|
|
}
|
|
return rq;
|
|
}
|
|
|
|
|
|
|
|
int AQCGI_SendResponseWithStatus(AQCGI_REQUEST *rq, int code, const char *text)
|
|
{
|
|
AQCGI_Request_SetResponseCode(rq, code);
|
|
AQCGI_Request_SetResponseText(rq, text);
|
|
return AQCGI_SendResponse(rq);
|
|
}
|
|
|
|
|
|
|
|
int AQCGI_SendResponse(AQCGI_REQUEST *rq)
|
|
{
|
|
GWEN_SYNCIO *sio;
|
|
int rv;
|
|
|
|
sio=GWEN_SyncIo_File_fromStdout();
|
|
assert(sio);
|
|
|
|
rv=_finishAndSendHeader(rq, sio);
|
|
if (rv<0) {
|
|
DBG_ERROR(AQCGI_LOGDOMAIN, "here (%d)", rv);
|
|
GWEN_SyncIo_free(sio);
|
|
return rv;
|
|
}
|
|
|
|
rv=_sendBody(rq, sio);
|
|
if (rv<0) {
|
|
DBG_ERROR(AQCGI_LOGDOMAIN, "here (%d)", rv);
|
|
GWEN_SyncIo_free(sio);
|
|
return rv;
|
|
}
|
|
|
|
rv=GWEN_SyncIo_Flush(sio);
|
|
if (rv<0) {
|
|
DBG_ERROR(AQCGI_LOGDOMAIN, "here (%d)", rv);
|
|
GWEN_SyncIo_free(sio);
|
|
return rv;
|
|
}
|
|
GWEN_SyncIo_free(sio);
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
int _finishAndSendHeader(AQCGI_REQUEST *rq, GWEN_SYNCIO *sio)
|
|
{
|
|
int rv;
|
|
GWEN_BUFFER *buf;
|
|
int responseBodyLength=0;
|
|
|
|
buf=AQCGI_Request_GetBufferResponseBody(rq);
|
|
if (buf)
|
|
responseBodyLength=GWEN_Buffer_GetUsedBytes(buf);
|
|
|
|
AQCGI_Request_AddResponseStatusHeader(rq);
|
|
buf=AQCGI_Request_GetBufferResponseHeader(rq);
|
|
GWEN_Buffer_AppendArgs(buf, "Content-length: %d\n", responseBodyLength);
|
|
if (buf && GWEN_Buffer_GetUsedBytes(buf)) {
|
|
rv=GWEN_SyncIo_WriteForced(sio, (const uint8_t*)GWEN_Buffer_GetStart(buf), GWEN_Buffer_GetUsedBytes(buf));
|
|
if (rv<0) {
|
|
DBG_ERROR(AQCGI_LOGDOMAIN, "here (%d)", rv);
|
|
return rv;
|
|
}
|
|
}
|
|
|
|
rv=GWEN_SyncIo_WriteForced(sio, (const uint8_t*)"\n", 1);
|
|
if (rv<0) {
|
|
DBG_ERROR(AQCGI_LOGDOMAIN, "here (%d)", rv);
|
|
return rv;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
int _sendBody(AQCGI_REQUEST *rq, GWEN_SYNCIO *sio)
|
|
{
|
|
int rv;
|
|
GWEN_BUFFER *buf;
|
|
|
|
buf=AQCGI_Request_GetBufferResponseBody(rq);
|
|
if (buf && GWEN_Buffer_GetUsedBytes(buf)) {
|
|
rv=GWEN_SyncIo_WriteForced(sio, (const uint8_t*)GWEN_Buffer_GetStart(buf), GWEN_Buffer_GetUsedBytes(buf));
|
|
if (rv<0) {
|
|
DBG_ERROR(AQCGI_LOGDOMAIN, "here (%d)", rv);
|
|
return rv;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
int _unescapeUrlEncoded(const char *src, unsigned int srclen, char *buffer, unsigned int maxsize)
|
|
{
|
|
unsigned int size;
|
|
|
|
size=0;
|
|
|
|
while (srclen>0 && *src) {
|
|
unsigned char x;
|
|
|
|
x=(unsigned char)*src;
|
|
if (
|
|
(x>='A' && x<='Z') ||
|
|
(x>='a' && x<='z') ||
|
|
(x>='0' && x<='9') ||
|
|
x==' ' ||
|
|
x=='.' ||
|
|
x==',' ||
|
|
x=='.' ||
|
|
x=='*' ||
|
|
x=='?' ||
|
|
x=='+' ||
|
|
x=='-' ||
|
|
x=='_'
|
|
) {
|
|
if (size<(maxsize-1)) {
|
|
buffer[size++]=(x=='+')?' ':x;
|
|
}
|
|
else {
|
|
DBG_ERROR(AQCGI_LOGDOMAIN, "Buffer too small");
|
|
return GWEN_ERROR_BUFFER_OVERFLOW;
|
|
}
|
|
}
|
|
else {
|
|
if (*src=='%') {
|
|
unsigned char d1, d2;
|
|
unsigned char c;
|
|
|
|
if (srclen<3) {
|
|
DBG_ERROR(AQCGI_LOGDOMAIN, "Incomplete escape sequence (EOLN met)");
|
|
return GWEN_ERROR_BAD_DATA;
|
|
}
|
|
/* skip '%' */
|
|
src++;
|
|
if (!(*src) || !isxdigit((int)*src)) {
|
|
DBG_ERROR(AQCGI_LOGDOMAIN, "Incomplete escape sequence (no digits)");
|
|
return GWEN_ERROR_BAD_DATA;
|
|
}
|
|
/* read first digit */
|
|
d1=(unsigned char)(toupper(*src));
|
|
|
|
/* get second digit */
|
|
src++;
|
|
if (!(*src) || !isxdigit((int)*src)) {
|
|
DBG_ERROR(AQCGI_LOGDOMAIN, "Incomplete escape sequence (only 1 digit)");
|
|
return GWEN_ERROR_BAD_DATA;
|
|
}
|
|
d2=(unsigned char)(toupper(*src));
|
|
/* compute character */
|
|
d1-='0';
|
|
if (d1>9)
|
|
d1-=7;
|
|
c=(d1<<4)&0xf0;
|
|
d2-='0';
|
|
if (d2>9)
|
|
d2-=7;
|
|
c+=(d2&0xf);
|
|
/* store character */
|
|
if (size<(maxsize-1))
|
|
buffer[size++]=(char)c;
|
|
else {
|
|
DBG_ERROR(AQCGI_LOGDOMAIN, "Buffer too small");
|
|
return GWEN_ERROR_BUFFER_OVERFLOW;
|
|
}
|
|
srclen-=2;
|
|
}
|
|
else {
|
|
DBG_ERROR(AQCGI_LOGDOMAIN, "Found non-alphanum characters in escaped string (\"%s\")", src);
|
|
return GWEN_ERROR_BAD_DATA;
|
|
}
|
|
}
|
|
srclen--;
|
|
src++;
|
|
} /* while */
|
|
|
|
buffer[size]=0;
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
GWEN_DB_NODE *_parseEnv(void)
|
|
{
|
|
GWEN_DB_NODE *db;
|
|
GWEN_DB_NODE *dbCookies;
|
|
int i=0;
|
|
const char *s;
|
|
|
|
db=GWEN_DB_Group_new("env");
|
|
while( (s=environ[i++]) )
|
|
_parseEnvVarIntoDb(s, db);
|
|
|
|
dbCookies=GWEN_DB_GetGroup(db, 0, "cookies");
|
|
i=0;
|
|
while( (s=GWEN_DB_GetCharValue(db, "HTTP_COOKIE", i++, NULL)) )
|
|
_parseCookieHeaderIntoDb(s, dbCookies);
|
|
|
|
return db;
|
|
}
|
|
|
|
|
|
|
|
int _parseEnvVarIntoDb(const char *var, GWEN_DB_NODE *db)
|
|
{
|
|
const char *s;
|
|
|
|
while(isblank(*var))
|
|
var++;
|
|
s=strchr(var, '=');
|
|
if (s) {
|
|
int nameLen;
|
|
|
|
nameLen=s-var;
|
|
if (nameLen) {
|
|
char *name;
|
|
|
|
name=GWEN_Text_strndup(var, nameLen);
|
|
s++;
|
|
if (*s) {
|
|
GWEN_DB_SetCharValue(db, 0, name, s);
|
|
free(name);
|
|
return 0;
|
|
}
|
|
free(name);
|
|
}
|
|
}
|
|
return GWEN_ERROR_BAD_DATA;
|
|
}
|
|
|
|
|
|
|
|
void _parseCookieHeaderIntoDb(const char *s, GWEN_DB_NODE *db)
|
|
{
|
|
while(*s) {
|
|
const char *nameStart;
|
|
char *t;
|
|
|
|
while(isblank(*s))
|
|
s++;
|
|
if (!*s)
|
|
break;
|
|
nameStart=s;
|
|
t=strchr(s, '=');
|
|
if (t) {
|
|
int nameLen;
|
|
|
|
nameLen=t-nameStart;
|
|
s=t+1;
|
|
if (nameLen) {
|
|
char *name;
|
|
|
|
name=GWEN_Text_strndup(nameStart, nameLen);
|
|
if (*s) {
|
|
const char *dataBegin;
|
|
int dataLen;
|
|
|
|
while(isblank(*s))
|
|
s++;
|
|
dataBegin=s;
|
|
while(*s && *s!=';')
|
|
s++;
|
|
dataLen=s-dataBegin;
|
|
if (dataLen) {
|
|
char *data;
|
|
|
|
data=GWEN_Text_strndup(dataBegin, dataLen);
|
|
GWEN_DB_SetCharValue(db, 0, name, data);
|
|
free(data);
|
|
}
|
|
}
|
|
free(name);
|
|
}
|
|
}
|
|
s=strchr(s, ';');
|
|
if (s==NULL)
|
|
break;
|
|
s++;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
int _handleHeaders(AQCGI_REQUEST *rq)
|
|
{
|
|
GWEN_DB_NODE *db;
|
|
const char *s;
|
|
|
|
db=_parseEnv();
|
|
if (db==NULL) {
|
|
DBG_ERROR(AQCGI_LOGDOMAIN, "Error parsing environment variables");
|
|
return GWEN_ERROR_GENERIC;
|
|
}
|
|
AQCGI_Request_SetDbRequestHeader(rq, db);
|
|
|
|
s=GWEN_DB_GetCharValue(db, "REQUEST_METHOD", 0, NULL);
|
|
if (s && *s) {
|
|
if (strcasecmp(s, "GET")==0)
|
|
AQCGI_Request_SetRequestMethod(rq, AQCGI_REQUEST_METHOD_GET);
|
|
else if (strcasecmp(s, "POST")==0)
|
|
AQCGI_Request_SetRequestMethod(rq, AQCGI_REQUEST_METHOD_POST);
|
|
else {
|
|
DBG_ERROR(AQCGI_LOGDOMAIN, "Unhandled request method \"%s\"", s);
|
|
return GWEN_ERROR_GENERIC;
|
|
}
|
|
}
|
|
else {
|
|
DBG_ERROR(AQCGI_LOGDOMAIN, "Missing request method");
|
|
return GWEN_ERROR_GENERIC;
|
|
}
|
|
|
|
s=GWEN_DB_GetCharValue(db, "PATH_INFO", 0, NULL);
|
|
if (s && *s) {
|
|
GWEN_STRINGLIST *sl;
|
|
|
|
if (*s=='/')
|
|
s++;
|
|
sl=GWEN_StringList_fromString2(s, "/", 0,
|
|
GWEN_TEXT_FLAGS_DEL_QUOTES |
|
|
GWEN_TEXT_FLAGS_DEL_LEADING_BLANKS |
|
|
GWEN_TEXT_FLAGS_DEL_TRAILING_BLANKS);
|
|
AQCGI_Request_SetStringlistPath(rq, sl);
|
|
}
|
|
|
|
s=GWEN_DB_GetCharValue(db, "QUERY_STRING", 0, NULL);
|
|
if (s && *s) {
|
|
GWEN_DB_NODE *dbQuery;
|
|
int rv;
|
|
|
|
dbQuery=GWEN_DB_Group_new("query");
|
|
rv=AQCGI_ParseUrlEncoded(s, strlen(s), dbQuery);
|
|
if (rv<0) {
|
|
DBG_ERROR(AQCGI_LOGDOMAIN, "Error parsing query string \"%s\" (%d)", s, rv);
|
|
GWEN_DB_Group_free(dbQuery);
|
|
}
|
|
else
|
|
AQCGI_Request_SetDbQuery(rq, dbQuery);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
int _handleBody(AQCGI_REQUEST *rq)
|
|
{
|
|
GWEN_DB_NODE *db;
|
|
const char *s;
|
|
int bodyLength=0;
|
|
|
|
db=AQCGI_Request_GetDbRequestHeader(rq);
|
|
|
|
s=GWEN_DB_GetCharValue(db, "CONTENT_LENGTH", 0, NULL);
|
|
if (s && *s) {
|
|
int i;
|
|
|
|
if (1==sscanf(s, "%d", &i)) {
|
|
bodyLength=i;
|
|
AQCGI_Request_SetRequestBodyLength(rq, i);
|
|
}
|
|
}
|
|
|
|
s=GWEN_DB_GetCharValue(db, "CONTENT_TYPE", 0, NULL);
|
|
if (bodyLength>0 && s && *s && strcasecmp(s, "application/x-www-form-urlencoded")==0) {
|
|
GWEN_BUFFER *buf;
|
|
GWEN_DB_NODE *dbDecoded;
|
|
int rv;
|
|
|
|
buf=GWEN_Buffer_new(0, bodyLength, 0, 1);
|
|
rv=AQCGI_ReadForcedStdInToBuffer(bodyLength, buf);
|
|
if (rv<0) {
|
|
DBG_ERROR(AQCGI_LOGDOMAIN, "Error reading body from stdin (%d)", rv);
|
|
GWEN_Buffer_free(buf);
|
|
return rv;
|
|
}
|
|
|
|
dbDecoded=GWEN_DB_Group_new("body");
|
|
rv=AQCGI_ParseUrlEncoded(GWEN_Buffer_GetStart(buf), GWEN_Buffer_GetUsedBytes(buf), dbDecoded);
|
|
if (rv<0) {
|
|
DBG_ERROR(AQCGI_LOGDOMAIN, "Error reading body from stdin (%d)", rv);
|
|
GWEN_DB_Group_free(dbDecoded);
|
|
GWEN_Buffer_free(buf);
|
|
return rv;
|
|
}
|
|
AQCGI_Request_SetDbPostBody(rq, dbDecoded);
|
|
GWEN_Buffer_free(buf);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
|