diff --git a/Makefile b/Makefile index 99dbbca..659a53e 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,14 @@ CC = gcc LD = gcc CFLAGS = -Wall -g -std=c99 -ICFloor/src/ -Ilibargo/src/ -Ilibparcival/src/ -D_POSIX_C_SOURCE=200809L -D_XOPEN_SOURCE=500 -LDFLAGS = -lpthread -lrt +LDFLAGS = -lpthread -lrt -luuid CFLOOR_LIB = CFloor/libcfloor.a LIBARGO = libargo/libargo.a LIBPARCIVAL = libparcival/libparcival.a LIBS = $(CFLOOR_LIB) $(LIBARGO) $(LIBPARCIVAL) -OBJS = obj/router.o obj/request.o obj/base_cfloor.o obj/base_cgi.o obj/auth.o obj/base64.o +OBJS = obj/router.o obj/request.o obj/base_cfloor.o obj/base_cgi.o obj/auth.o obj/base64.o obj/common.o obj/cookies.o obj/sessions.o DEPS = $(OBJS:%.o=%.d) DEMO_OBJS = obj/demo.o obj/entities.tab.o obj/template.tab.o diff --git a/demo/demo.c b/demo/demo.c index bebab77..bae50e7 100644 --- a/demo/demo.c +++ b/demo/demo.c @@ -1,40 +1,44 @@ #include #include +#include #include +#define SESSION_PTR_TYPE const char* +#include + #include "entities.h" GET("/", hello); GET("/index.*", hello); -response_t hello(ctx_t ctx) { +response_t hello(ctx_t* ctx) { return rawResponse(200, "Hello World\n"); } GET("/foobar", foobar); -response_t foobar(ctx_t ctx) { +response_t foobar(ctx_t* ctx) { return fileResponse("demo/foobar.txt"); } -response_t authenticate(ctx_t ctx) { - if (ctx.auth.type != BASIC) { +response_t authenticate(ctx_t* ctx) { + if (ctx->auth.type != BASIC) { return basicAuthResponse(401, "Protected Area"); } - if (strcmp(ctx.auth.basic.user, "admin") != 0 || - strcmp(ctx.auth.basic.password, "password") != 0 + if (strcmp(ctx->auth.basic.user, "admin") != 0 || + strcmp(ctx->auth.basic.password, "password") != 0 ) { // username or password wrong return basicAuthResponse(401, "Protected Area"); } - return next(); } GET("/user", authenticate, user); -response_t user(ctx_t ctx) { +POST("/user", authenticate, user); +response_t user(ctx_t* ctx) { user_t user = { .username = "overflowerror", .github = "https://github.com/overflowerror" @@ -44,6 +48,26 @@ response_t user(ctx_t ctx) { } GET("/template", template); -response_t template(ctx_t ctx) { +response_t template(ctx_t* ctx) { return templateResponse(200, "demo.templ", "Page Title", "Overflow"); } + +GET("/sessions", sessions); +response_t sessions(ctx_t* ctx) { + const char** sessiondata = session_start(ctx); + + const char* output = "null\n"; + + if (*sessiondata == NULL) { + *sessiondata = "Test\n"; + session_update(ctx); + } else { + output = *sessiondata; + } + + fprintf(stderr, "%s, %d\n", output, strlen(output)); + + return rawResponse(200, output); +} + + diff --git a/libparcival b/libparcival index 30c62e0..e4a8695 160000 --- a/libparcival +++ b/libparcival @@ -1 +1 @@ -Subproject commit 30c62e03b86573a3fc5df788bda4bb95d2d89f9b +Subproject commit e4a86956046c03055780e98c4e95f352e683d985 diff --git a/src/base_cfloor.c b/src/base_cfloor.c index 3dba84a..d6bd49c 100644 --- a/src/base_cfloor.c +++ b/src/base_cfloor.c @@ -15,6 +15,8 @@ struct networkingConfig netConfig; +void session_end(ctx_t*); + static void handler(struct request request, struct response _response) { ctx_t ctx = { method: request.metaData.method, @@ -22,18 +24,25 @@ static void handler(struct request request, struct response _response) { queryString: request.metaData.queryString, peerAddr: request.peer.addr, peerPort: request.peer.port, - auth: getAuthData(request.headers) + auth: getAuthData(request.headers), + requestHeaders: *request.headers, + responseHeaders: headers_create(), + session: EMPTY_SESSION_CTX, }; - response_t response = routerHandler(ctx); + response_t response = routerHandler(&ctx); if (response.output == NULL) { response = errorResponse(500, "route did not provide a reponse handler"); } freeAuthData(ctx.auth); + session_end(&ctx); + + headers_merge(&ctx.responseHeaders, &response.headers); - int fd = _response.sendHeader(response.status, &response.headers, &request); + int fd = _response.sendHeader(response.status, &ctx.responseHeaders, &request); headers_free(&response.headers); + headers_free(&ctx.responseHeaders); if (fd < 0) { error("csite: sendHeader: %s", strerror(errno)); @@ -46,7 +55,7 @@ static void handler(struct request request, struct response _response) { return; } - response.output(out, response._userData, ctx); + response.output(out, response._userData, &ctx); fclose(out); } diff --git a/src/base_cgi.c b/src/base_cgi.c index f29b85c..6fdcf08 100644 --- a/src/base_cgi.c +++ b/src/base_cgi.c @@ -96,14 +96,14 @@ const char* or(const char* v1, const char* v2) { } } +void session_end(ctx_t*); + int main(int argc, char** argv) { struct headers headers = headers_create(); if (setHttpHeaders(&headers)) { fprintf(stderr, "%s: %s\n", argv[0], strerror(errno)); exit(1); } - - // TODO use request headers ctx_t ctx = { method: getMethod(or(getenv("REQUEST_METHOD"), "GET")), @@ -111,25 +111,32 @@ int main(int argc, char** argv) { queryString: or(getenv("QUERY_STRING"), ""), peerAddr: or(getenv("REMOTE_ADDR"), ""), peerPort: 0, // TODO - auth: getAuthData(request.headers) + auth: getAuthData(request.headers), + requestHeaders: headers, + responseHeaders: headers_create(), + session: EMPTY_SESSION_CTX, }; - - headers_free(&headers); - response_t response = routerHandler(ctx); + response_t response = routerHandler(&ctx); if (response.output == NULL) { response = errorResponse(500, "route did not provide a reponse handler"); } + headers_free(&headers); + + headers_merge(&ctx.responseHeaders, &response.headers); + freeAuthData(ctx.auth); + session_end(&ctx); printf("Status: %d\n\r", response.status); - headers_dump(&response.headers, stdout); + headers_dump(&ctx.responseHeaders, stdout); printf("\n\r"); headers_free(&response.headers); + headers_free(&ctx.responseHeaders); - response.output(stdout, response._userData, ctx); + response.output(stdout, response._userData, &ctx); return 0; } diff --git a/src/common.c b/src/common.c new file mode 100644 index 0000000..bf2e632 --- /dev/null +++ b/src/common.c @@ -0,0 +1,9 @@ +#include + +#include "common.h" + +void headers_merge(struct headers* dst, struct headers* src) { + for (int i = 0; i < src->number; i++) { + headers_mod(dst, src->headers[i].key, src->headers[i].value); + } +} diff --git a/src/common.h b/src/common.h index e680167..7e6df47 100644 --- a/src/common.h +++ b/src/common.h @@ -15,4 +15,6 @@ typedef enum method method_t; #define HTTP_CONNECT (CONNECT) #define HTTP_PATCH (PATCH) +void headers_merge(struct headers*, struct headers*); + #endif diff --git a/src/cookies.c b/src/cookies.c new file mode 100644 index 0000000..cb78bb8 --- /dev/null +++ b/src/cookies.c @@ -0,0 +1,227 @@ +#include +#include +#include +#include + +#include + +#include "cookies.h" + +char* getCookie(ctx_t* ctx, const char* key) { + // ignore const + char* cookieHeader = (char*) headers_get(&ctx->requestHeaders, "Cookie"); + if (cookieHeader == NULL) { + return NULL; + } + + cookieHeader = strdup(cookieHeader); + if (cookieHeader == NULL) { + return NULL; + } + + char* saveptr = NULL; + char* str = cookieHeader; + + char* value = NULL; + + while((str = strtok_r(str, ";", &saveptr)) != NULL) { + while(*str == ' ') str++; + + char* keyCandidate = str; + + for (; *str != '='; str++) { + if (*str == '\0') { + str = NULL; + break; + } + } + + if (str == NULL) { + // illegal cookie definition; ignore + // str is already NULL + continue; + } + + *str = '\0'; + + if (strcmp(keyCandidate, key) == 0) { + value = str + 1; + break; + } + + str = NULL; + } + + if (value != NULL) { + value = strdup(value); + } + + free(cookieHeader); + return value; +} + +cookieSettings_t cookieSettingsNull() { + return (cookieSettings_t) { + .expires = COOKIE_NO_EXPIRES, + .maxAge = COOKIE_NO_MAX_AGE, + .domain = NULL, + .path = NULL, + .secure = false, + .httpOnly = false + }; +} + +int setCookie(ctx_t* ctx, const char* name, const char* value, cookieSettings_t settings) { + size_t length = 0; + length += strlen(name) + 1 + strlen(value); + if (settings.expires != COOKIE_NO_EXPIRES) { + length += 2; + length += strlen("Expires="); + length += 3 + 2 + 2 + 1 + 3 + 1 + 4 + 1 + 2 + 1 + 2 + 1 + 2 + 1 + 3; + } + if (settings.maxAge != COOKIE_NO_MAX_AGE) { + length += 2; + length += strlen("Max-Age="); + long tmp = settings.maxAge; + if (tmp <= 0) { + length++; + tmp *= -1; + } + for (; tmp > 0; tmp /= 10) length++; + } + if (settings.domain != NULL) { + length += 2; + length += strlen("Domain="); + length += strlen(settings.domain); + } + if (settings.path != NULL) { + length += 2; + length += strlen("Path="); + length += strlen(settings.path); + } + if (settings.secure) { + length += 2; + length += strlen("Secure"); + } + if (settings.httpOnly) { + length += 2; + length += strlen("HttpOnly"); + } + + char* buffer = alloca(length + 1); + char* bufferPtr = buffer; + size_t tmp; + + tmp = snprintf(bufferPtr, length + 1, "%s=%s", name, value); + bufferPtr += tmp; + length -= tmp; + + if (settings.expires != COOKIE_NO_EXPIRES) { + struct tm result; + gmtime_r(&settings.expires, &result); + + char* weekday; + switch(result.tm_wday) { + case 0: + weekday = "Sun"; + break; + case 1: + weekday = "Mon"; + break; + case 2: + weekday = "Tue"; + break; + case 3: + weekday = "Wed"; + break; + case 4: + weekday = "Thu"; + break; + case 5: + weekday = "Fri"; + break; + case 6: + weekday = "Sat"; + break; + default: + weekday = "err"; + break; + } + + char* month; + switch(result.tm_mon) { + case 0: + month = "Jan"; + break; + case 1: + month = "Feb"; + break; + case 2: + month = "Mar"; + break; + case 3: + month = "Apr"; + break; + case 4: + month = "May"; + break; + case 5: + month = "Jun"; + break; + case 6: + month = "Jul"; + break; + case 7: + month = "Aug"; + break; + case 8: + month = "Sep"; + break; + case 9: + month = "Oct"; + break; + case 10: + month = "Nov"; + break; + case 11: + month = "Dec"; + break; + default: + month = "err"; + break; + } + + tmp = snprintf(bufferPtr, length + 1, "; Expires=%s, %02d %s %d %02d:%02d:%02d GMT", weekday, result.tm_mday, month, result.tm_year + 1900, result.tm_hour, result.tm_min, result.tm_sec); + bufferPtr += tmp; + length -= tmp; + } + if (settings.maxAge != COOKIE_NO_MAX_AGE) { + tmp = snprintf(bufferPtr, length + 1, "; Max-Age=%ld", settings.maxAge); + bufferPtr += tmp; + length -= tmp; + } + if (settings.domain != NULL) { + tmp = snprintf(bufferPtr, length + 1, "; Domain=%s", settings.domain); + bufferPtr += tmp; + length -= tmp; + } + if (settings.path != NULL) { + tmp = snprintf(bufferPtr, length + 1, "; Path=%s", settings.path); + bufferPtr += tmp; + length -= tmp; + } + if (settings.secure) { + tmp = snprintf(bufferPtr, length + 1, "; Secure"); + bufferPtr += tmp; + length -= tmp; + } + if (settings.httpOnly) { + tmp = snprintf(bufferPtr, length + 1, "; HttpOnly"); + bufferPtr += tmp; + length -= tmp; + } + + headers_mod(&ctx->responseHeaders, "Set-Cookie", buffer); + + return 0; +} diff --git a/src/cookies.h b/src/cookies.h new file mode 100644 index 0000000..400fac8 --- /dev/null +++ b/src/cookies.h @@ -0,0 +1,28 @@ +#ifndef COOKIES_H_ +#define COOKIES_H_ + +#include +#include +#include + +#include "request.h" + +char* getCookie(ctx_t*, const char*); + +#define COOKIE_NO_EXPIRES (0) +#define COOKIE_NO_MAX_AGE (LONG_MIN) + +typedef struct { + time_t expires; + long maxAge; + const char* domain; + const char* path; + bool secure; + bool httpOnly; +} cookieSettings_t; + +cookieSettings_t cookieSettingsNull(); + +int setCookie(ctx_t*, const char*, const char*, cookieSettings_t); + +#endif diff --git a/src/handler.h b/src/handler.h index ce84db3..44f69ec 100644 --- a/src/handler.h +++ b/src/handler.h @@ -3,6 +3,6 @@ #include "request.h" -typedef response_t (handle_t)(ctx_t); +typedef response_t (handle_t)(ctx_t*); #endif diff --git a/src/request.c b/src/request.c index 622a33a..463b3f9 100644 --- a/src/request.c +++ b/src/request.c @@ -31,13 +31,13 @@ void setDefaultErrorFormat(errorformat_t format) { errorformat = format; } -static void rawOutputAndFree(FILE* out, void* _userData, ctx_t ctx) { +static void rawOutputAndFree(FILE* out, void* _userData, ctx_t* ctx) { fprintf(out, "%s", (char*) _userData); free(_userData); } -static void rawOutput(FILE* out, void* _userData, ctx_t ctx) { +static void rawOutput(FILE* out, void* _userData, ctx_t* ctx) { fprintf(out, "%s", (const char*) _userData); } @@ -70,7 +70,7 @@ struct statusdata { const char* message; }; -static void statusOutput(FILE* out, void* _userData, ctx_t ctx) { +static void statusOutput(FILE* out, void* _userData, ctx_t* ctx) { struct statusdata* data = (struct statusdata*) _userData; const char* statusString = getStatusStrings(data->status).statusString; @@ -80,7 +80,7 @@ static void statusOutput(FILE* out, void* _userData, ctx_t ctx) { "status", json_long(data->status), "error", json_string(statusString), "message", json_string(data->message), - "path", json_string(ctx.path) + "path", json_string(ctx->path) ); free(data); free(tmp); @@ -126,7 +126,7 @@ response_t errorResponse(int status, const char* message) { return statusResponse(status, message); } -static void fileOutput(FILE* out, void* _userData, ctx_t ctx) { +static void fileOutput(FILE* out, void* _userData, ctx_t* ctx) { FILE* in = (FILE*) _userData; #define READ_BUFFER_SIZE (1024) @@ -171,7 +171,7 @@ response_t fileResponse(const char* file) { return response; } -static void jsonOutput(FILE* output, void* _userData, ctx_t ctx) { +static void jsonOutput(FILE* output, void* _userData, ctx_t* ctx) { jsonValue_t* json = (jsonValue_t*) _userData; char* result = json_stringify(json); @@ -196,8 +196,8 @@ response_t _jsonResponse(int status, const char* type, void* value) { return response; } -extern size_t _sizeTemplate(const char*, ...); -extern void _renderTemplate(const char*, FILE*, ...); +extern size_t _sizeTemplate(const char*, va_list); +extern void _renderTemplate(const char*, FILE*, va_list); response_t templateResponse(int status, const char* name, ...) { response_t response = emptyResponse(); diff --git a/src/request.h b/src/request.h index 41dff37..6099a5d 100644 --- a/src/request.h +++ b/src/request.h @@ -11,6 +11,14 @@ #define NEXT_RESPONSE_STATUS (0) +struct session_ctx { + void* session; + time_t accessTime; + void* data; +}; + +#define EMPTY_SESSION_CTX ((struct session_ctx) {.session = NULL}) + typedef struct { method_t method; const char* path; @@ -18,6 +26,9 @@ typedef struct { const char* peerAddr; int peerPort; struct auth auth; + struct headers requestHeaders; + struct headers responseHeaders; + struct session_ctx session; } ctx_t; typedef struct { @@ -25,7 +36,7 @@ typedef struct { struct headers headers; void* _userData; - void (*output) (FILE* conenction, void* _userData, ctx_t ctx); + void (*output) (FILE* conenction, void* _userData, ctx_t* ctx); } response_t; typedef enum { diff --git a/src/router.c b/src/router.c index 5b8bc16..edddeaa 100644 --- a/src/router.c +++ b/src/router.c @@ -121,8 +121,8 @@ int registerRoute(method_t method, const char* path, ...) { return 0; } -response_t routerHandler(ctx_t ctx) { - struct route* route = findRoute(ctx.method, ctx.path); +response_t routerHandler(ctx_t* ctx) { + struct route* route = findRoute(ctx->method, ctx->path); if (route == NULL) { return errorResponse(404, "no route found"); } diff --git a/src/router.h b/src/router.h index 7e120ec..b018158 100644 --- a/src/router.h +++ b/src/router.h @@ -5,6 +5,6 @@ int registerRoute(method_t method, const char* path, ...); -response_t routerHandler(ctx_t ctx); +response_t routerHandler(ctx_t* ctx); #endif diff --git a/src/sessions.c b/src/sessions.c new file mode 100644 index 0000000..c5be62d --- /dev/null +++ b/src/sessions.c @@ -0,0 +1,161 @@ +#include +#include +#include + +#include + +#include + +// SESSION_PTR_TYPE doesn't matter for this file +#define SESSION_PTR_TYPE int +#include "sessions.h" + +#include "cookies.h" + +struct session { + bool inUse; + uuid_t id; + time_t lastAccess; + time_t lastWrite; + void* data; +}; + +static struct session* sessions = NULL; +static size_t sessionno = 0; +static pthread_mutex_t globalLock = PTHREAD_MUTEX_INITIALIZER; + +#define SESSION_BLOCK_SIZE (128) + +int resizeSessionList() { + struct session* tmp = realloc(sessions, sizeof(struct session) * (sessionno + SESSION_BLOCK_SIZE)); + if (tmp == NULL) { + return -1; + } + for (size_t i = 0; i < SESSION_BLOCK_SIZE; i++) { + struct session* session = &tmp[sessionno + i]; + session->inUse = 0; + } + sessions = tmp; + sessionno += SESSION_BLOCK_SIZE; + + return 0; +} + +struct session* newSession(size_t size) { + for (size_t i = 0; i < sessionno; i++) { + if (!sessions[i].inUse) { + sessions[i].inUse = true; + sessions[i].data = malloc(size); + if (sessions[i].data == NULL) { + sessions[i].inUse = false; + return NULL; + } + memset(sessions[i].data, 0, size); + sessions[i].lastWrite = 0; + return &(sessions[i]); + } + } + + // no free session slot + if (resizeSessionList() < 0) { + return NULL; + } + + return newSession(size); +} + +struct session* findSession(uuid_t id) { + for (size_t i = 0; i < sessionno; i++) { + if (!sessions[i].inUse) { + continue; + } + if (uuid_compare(sessions[i].id, id) == 0) { + return &(sessions[i]); + } + } + + return NULL; +} + +void* _session_start(ctx_t* ctx, const char* cookie, size_t size) { + char* cookieValue = getCookie(ctx, cookie); + + bool isValid = false; + uuid_t id; + struct session* session = NULL; + + if (cookieValue != NULL) { + if (uuid_parse(cookieValue, id) == 0) { + isValid = true; + } + + free(cookieValue); + } else { + isValid = false; + } + + pthread_mutex_lock(&globalLock); + + if (isValid) { + session = findSession(id); + } + + if (session == NULL) { + session = newSession(size); + if (session == NULL) { + return NULL; + } + + uuid_generate_time(session->id); + + char buffer[36 + 1]; + uuid_unparse(session->id, buffer); + + setCookie(ctx, cookie, buffer, cookieSettingsNull()); + } + + pthread_mutex_unlock(&globalLock); + + session->lastAccess = time(NULL); + + ctx->session.session = session; + ctx->session.accessTime = time(NULL); + + void* requestSessionData = malloc(size); + if (requestSessionData == NULL) { + return NULL; + } + memcpy(requestSessionData, session->data, size); + ctx->session.data = requestSessionData; + + return requestSessionData; +} + +int _session_update(ctx_t* ctx, size_t size) { + struct session_ctx* sessionCtx = &(ctx->session); + struct session* session = (struct session*) sessionCtx->session; + if (session == NULL) { + return ERROR_NO_SESSION; + } + + pthread_mutex_lock(&globalLock); + + if (session->lastWrite > sessionCtx->accessTime) { + pthread_mutex_unlock(&globalLock); + return ERROR_CONCURRENT_SESSION; + } + + session->lastWrite = time(NULL); + sessionCtx->accessTime = session->lastWrite; + memcpy(session->data, sessionCtx->data, size); + + pthread_mutex_unlock(&globalLock); + + return 0; +} + +void session_end(ctx_t* ctx) { + if (ctx->session.session != NULL) { + free(ctx->session.data); + } +} diff --git a/src/sessions.h b/src/sessions.h new file mode 100644 index 0000000..f7a41ae --- /dev/null +++ b/src/sessions.h @@ -0,0 +1,33 @@ +#ifndef SESSIONS_H_ +#define SESSIONS_H_ + +#include + +#include + +#ifndef SESSION_PTR_TYPE + #pragma GCC warning "session ptr type not defined" + #define SESSION_PTR_TYPE int +#endif + +#ifndef SESSION_LENGTH + #define SESSION_LENGTH (24*60*60*1000) +#endif + +#ifndef SESSION_COOKIE_NAME + #define SESSION_COOKIE_NAME "cshore_session" +#endif + +#include "request.h" + +#define ERROR_NO_SESSION (-2) +#define ERROR_CONCURRENT_SESSION (-1) + +void* _session_start(ctx_t*, const char*, size_t); +int _session_update(ctx_t*, size_t); +void session_end(ctx_t*); + +#define session_start(c) ((SESSION_PTR_TYPE*) _session_start(c, SESSION_COOKIE_NAME, sizeof(SESSION_PTR_TYPE))) +#define session_update(c) _session_update(c, sizeof(SESSION_PTR_TYPE)) + +#endif