Files
aqcgi/src/aqcgi/cgi.c
Martin Preuss f75e752127 Initial import.
2024-07-09 23:10:08 +02:00

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;
}