1668 lines
51 KiB
C
1668 lines
51 KiB
C
#include <stdio.h>
|
|
#include <limits.h>
|
|
#include <stdlib.h>
|
|
|
|
#ifdef __linux__
|
|
#include <errno.h>
|
|
#include <sys/random.h>
|
|
#endif
|
|
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#endif
|
|
|
|
#ifdef CWEB_ENABLE_DATABASE
|
|
#include "sqlite3.h"
|
|
#endif
|
|
|
|
#ifndef CWEB_AMALGAMATION
|
|
#ifdef CWEB_ENABLE_TEMPLATE
|
|
#include "wl.h"
|
|
#endif
|
|
#include "chttp.h"
|
|
#include "main.h"
|
|
#endif
|
|
|
|
#define MIN(X, Y) ((X) < (Y) ? (X) : (Y))
|
|
#define MAX(X, Y) ((X) > (Y) ? (X) : (Y))
|
|
#define ASSERT(X) {if (!(X)) { __builtin_trap(); }}
|
|
#define TRACE(X, ...) ((void) 0)
|
|
#define SIZEOF(X) (int) (sizeof(X)/sizeof((X)[0]))
|
|
|
|
#ifdef CWEB_ENABLE_TEMPLATE
|
|
|
|
typedef struct {
|
|
char path[1<<8];
|
|
int pathlen;
|
|
WL_Program program;
|
|
} CachedProgram;
|
|
|
|
typedef struct {
|
|
int count;
|
|
int capacity_log2;
|
|
CachedProgram pool[];
|
|
} TemplateCache;
|
|
|
|
static TemplateCache *template_cache_init(int capacity_log2);
|
|
static void template_cache_free(TemplateCache *cache);
|
|
|
|
#endif
|
|
|
|
typedef struct SessionStorage SessionStorage;
|
|
|
|
#ifdef CWEB_ENABLE_DATABASE
|
|
|
|
typedef struct SQLiteCache SQLiteCache;
|
|
|
|
static SQLiteCache* sqlite_cache_init(sqlite3 *db, int capacity_log2);
|
|
static void sqlite_cache_free(SQLiteCache *cache);
|
|
|
|
static int sqlite3utils_prepare(SQLiteCache *cache,
|
|
sqlite3_stmt **pstmt, char *fmt, int fmtlen);
|
|
|
|
static int sqlite3utils_prepare_and_bind_impl(SQLiteCache *cache,
|
|
sqlite3_stmt **pstmt, char *fmt, CWEB_VArgs args);
|
|
|
|
#define sqlite3utils_prepare_and_bind(cache, pstmt, fmt, ...) \
|
|
sqlite3utils_prepare_and_bind_impl((cache), (pstmt), (fmt), VARGS(__VA_ARGS__))
|
|
|
|
#endif
|
|
|
|
struct CWEB_Request {
|
|
|
|
CWEB *cweb;
|
|
|
|
WL_Arena arena;
|
|
|
|
HTTP_Request *req;
|
|
HTTP_ResponseBuilder builder;
|
|
|
|
// Session
|
|
bool just_created_session;
|
|
int user_id;
|
|
CWEB_String sess;
|
|
CWEB_String csrf;
|
|
};
|
|
|
|
struct CWEB {
|
|
|
|
HTTP_Server *server;
|
|
int pool_cap;
|
|
char *pool;
|
|
|
|
// Login
|
|
SessionStorage *session_storage;
|
|
|
|
#ifdef CWEB_ENABLE_DATABASE
|
|
sqlite3 *db;
|
|
SQLiteCache *dbcache;
|
|
bool trace_sql;
|
|
#endif
|
|
|
|
#ifdef CWEB_ENABLE_TEMPLATE
|
|
TemplateCache *tpcache;
|
|
bool enable_template_cache;
|
|
#endif
|
|
|
|
bool allow_insecure_login;
|
|
|
|
CWEB_Request req;
|
|
};
|
|
|
|
///////////////////////////
|
|
|
|
bool cweb_streq(CWEB_String a, CWEB_String b)
|
|
{
|
|
return http_streq((HTTP_String) { a.ptr, a.len }, (HTTP_String) { b.ptr, b.len });
|
|
}
|
|
|
|
CWEB_String cweb_trim(CWEB_String s)
|
|
{
|
|
HTTP_String s2 = http_trim((HTTP_String) { s.ptr, s.len });
|
|
return (CWEB_String) { s2.ptr, s2.len };
|
|
}
|
|
|
|
CWEB_VArg cweb_varg_from_c (char c) { return (CWEB_VArg) { CWEB_VARG_TYPE_C, .c=c }; }
|
|
CWEB_VArg cweb_varg_from_s (short s) { return (CWEB_VArg) { CWEB_VARG_TYPE_S, .s=s }; }
|
|
CWEB_VArg cweb_varg_from_i (int i) { return (CWEB_VArg) { CWEB_VARG_TYPE_I, .i=i }; }
|
|
CWEB_VArg cweb_varg_from_l (long l) { return (CWEB_VArg) { CWEB_VARG_TYPE_L, .l=l }; }
|
|
CWEB_VArg cweb_varg_from_ll (long long ll) { return (CWEB_VArg) { CWEB_VARG_TYPE_LL, .ll=ll }; }
|
|
CWEB_VArg cweb_varg_from_sc (char sc) { return (CWEB_VArg) { CWEB_VARG_TYPE_SC, .sc=sc }; }
|
|
CWEB_VArg cweb_varg_from_ss (short ss) { return (CWEB_VArg) { CWEB_VARG_TYPE_SS, .ss=ss }; }
|
|
CWEB_VArg cweb_varg_from_si (int si) { return (CWEB_VArg) { CWEB_VARG_TYPE_SI, .si=si }; }
|
|
CWEB_VArg cweb_varg_from_sl (long sl) { return (CWEB_VArg) { CWEB_VARG_TYPE_SL, .sl=sl }; }
|
|
CWEB_VArg cweb_varg_from_sll (long long sll) { return (CWEB_VArg) { CWEB_VARG_TYPE_SLL, .sll=sll }; }
|
|
CWEB_VArg cweb_varg_from_uc (char uc) { return (CWEB_VArg) { CWEB_VARG_TYPE_UC, .uc=uc }; }
|
|
CWEB_VArg cweb_varg_from_us (short us) { return (CWEB_VArg) { CWEB_VARG_TYPE_US, .us=us }; }
|
|
CWEB_VArg cweb_varg_from_ui (int ui) { return (CWEB_VArg) { CWEB_VARG_TYPE_UI, .ui=ui }; }
|
|
CWEB_VArg cweb_varg_from_ul (long ul) { return (CWEB_VArg) { CWEB_VARG_TYPE_UL, .ul=ul }; }
|
|
CWEB_VArg cweb_varg_from_ull (long long ull) { return (CWEB_VArg) { CWEB_VARG_TYPE_ULL, .ull=ull }; }
|
|
CWEB_VArg cweb_varg_from_f (float f) { return (CWEB_VArg) { CWEB_VARG_TYPE_F, .f=f }; }
|
|
CWEB_VArg cweb_varg_from_d (double d) { return (CWEB_VArg) { CWEB_VARG_TYPE_D, .d=d }; }
|
|
CWEB_VArg cweb_varg_from_b (bool b) { return (CWEB_VArg) { CWEB_VARG_TYPE_B, .b=b }; }
|
|
CWEB_VArg cweb_varg_from_str (CWEB_String str) { return (CWEB_VArg) { CWEB_VARG_TYPE_STR, .str=str }; }
|
|
CWEB_VArg cweb_varg_from_hash (CWEB_PasswordHash hash) { return (CWEB_VArg) { CWEB_VARG_TYPE_HASH, .hash=hash }; }
|
|
CWEB_VArg cweb_varg_from_pc (char *pc) { return (CWEB_VArg) { CWEB_VARG_TYPE_PC, .pc=pc }; }
|
|
CWEB_VArg cweb_varg_from_ps (short *ps) { return (CWEB_VArg) { CWEB_VARG_TYPE_PS, .ps=ps }; }
|
|
CWEB_VArg cweb_varg_from_pi (int *pi) { return (CWEB_VArg) { CWEB_VARG_TYPE_PI, .pi=pi }; }
|
|
CWEB_VArg cweb_varg_from_pl (long *pl) { return (CWEB_VArg) { CWEB_VARG_TYPE_PL, .pl=pl }; }
|
|
CWEB_VArg cweb_varg_from_pll (long long *pll) { return (CWEB_VArg) { CWEB_VARG_TYPE_PLL, .pll=pll }; }
|
|
CWEB_VArg cweb_varg_from_psc (signed char *psc) { return (CWEB_VArg) { CWEB_VARG_TYPE_PSC, .psc=psc }; }
|
|
CWEB_VArg cweb_varg_from_pss (signed short *pss) { return (CWEB_VArg) { CWEB_VARG_TYPE_PSS, .pss=pss }; }
|
|
CWEB_VArg cweb_varg_from_psi (signed int *psi) { return (CWEB_VArg) { CWEB_VARG_TYPE_PSI, .psi=psi }; }
|
|
CWEB_VArg cweb_varg_from_psl (signed long *psl) { return (CWEB_VArg) { CWEB_VARG_TYPE_PSL, .psl=psl }; }
|
|
CWEB_VArg cweb_varg_from_psll (signed long long *psll) { return (CWEB_VArg) { CWEB_VARG_TYPE_PSLL, .psll=psll }; }
|
|
CWEB_VArg cweb_varg_from_puc (unsigned char *puc) { return (CWEB_VArg) { CWEB_VARG_TYPE_PUC, .puc=puc }; }
|
|
CWEB_VArg cweb_varg_from_pus (unsigned short *pus) { return (CWEB_VArg) { CWEB_VARG_TYPE_PUS, .pus=pus }; }
|
|
CWEB_VArg cweb_varg_from_pui (unsigned int *pui) { return (CWEB_VArg) { CWEB_VARG_TYPE_PUI, .pui=pui }; }
|
|
CWEB_VArg cweb_varg_from_pul (unsigned long *pul) { return (CWEB_VArg) { CWEB_VARG_TYPE_PUL, .pul=pul }; }
|
|
CWEB_VArg cweb_varg_from_pull (unsigned long long *pull) { return (CWEB_VArg) { CWEB_VARG_TYPE_PULL, .pull=pull }; }
|
|
CWEB_VArg cweb_varg_from_pf (float *pf) { return (CWEB_VArg) { CWEB_VARG_TYPE_PF, .pf=pf }; }
|
|
CWEB_VArg cweb_varg_from_pd (double *pd) { return (CWEB_VArg) { CWEB_VARG_TYPE_PD, .pd=pd }; }
|
|
CWEB_VArg cweb_varg_from_pb (bool *pb) { return (CWEB_VArg) { CWEB_VARG_TYPE_PB, .pb=pb }; }
|
|
CWEB_VArg cweb_varg_from_pstr (CWEB_String *pstr) { return (CWEB_VArg) { CWEB_VARG_TYPE_PSTR, .pstr=pstr }; }
|
|
CWEB_VArg cweb_varg_from_phash(CWEB_PasswordHash *phash) { return (CWEB_VArg) { CWEB_VARG_TYPE_PHASH, .phash=phash }; }
|
|
|
|
|
|
typedef struct {
|
|
char *dst;
|
|
int cap;
|
|
int len;
|
|
} StaticOutputBuffer;
|
|
|
|
static void append_to_output(StaticOutputBuffer *out, char *src, int len)
|
|
{
|
|
int unused = out->cap - out->len;
|
|
if (unused > 0)
|
|
memcpy(out->dst + out->len, src, MIN(len, unused));
|
|
out->len += len;
|
|
}
|
|
|
|
static void append_to_output_u64(StaticOutputBuffer *out, uint64_t n)
|
|
{
|
|
// TODO
|
|
}
|
|
|
|
static void append_to_output_s64(StaticOutputBuffer *out, int64_t n)
|
|
{
|
|
// TODO
|
|
}
|
|
|
|
static void append_to_output_f64(StaticOutputBuffer *out, double n)
|
|
{
|
|
// TODO
|
|
}
|
|
|
|
static void append_to_output_ptr(StaticOutputBuffer *out, void *p)
|
|
{
|
|
// TODO
|
|
}
|
|
|
|
static void value_to_output(StaticOutputBuffer *out, CWEB_VArg arg)
|
|
{
|
|
switch (arg.type) {
|
|
case CWEB_VARG_TYPE_C : append_to_output(out, &arg.c, 1); break;
|
|
case CWEB_VARG_TYPE_S : append_to_output_s64(out, arg.s); break;
|
|
case CWEB_VARG_TYPE_I : append_to_output_s64(out, arg.i); break;
|
|
case CWEB_VARG_TYPE_L : append_to_output_s64(out, arg.l); break;
|
|
case CWEB_VARG_TYPE_LL : append_to_output_s64(out, arg.ll); break;
|
|
case CWEB_VARG_TYPE_SC : append_to_output_s64(out, arg.sc); break;
|
|
case CWEB_VARG_TYPE_SS : append_to_output_s64(out, arg.ss); break;
|
|
case CWEB_VARG_TYPE_SI : append_to_output_s64(out, arg.si); break;
|
|
case CWEB_VARG_TYPE_SL : append_to_output_s64(out, arg.sl); break;
|
|
case CWEB_VARG_TYPE_SLL : append_to_output_s64(out, arg.sll); break;
|
|
case CWEB_VARG_TYPE_UC : append_to_output_u64(out, arg.uc); break;
|
|
case CWEB_VARG_TYPE_US : append_to_output_u64(out, arg.us); break;
|
|
case CWEB_VARG_TYPE_UI : append_to_output_u64(out, arg.ui); break;
|
|
case CWEB_VARG_TYPE_UL : append_to_output_u64(out, arg.ul); break;
|
|
case CWEB_VARG_TYPE_ULL : append_to_output_u64(out, arg.ull); break;
|
|
case CWEB_VARG_TYPE_F : append_to_output_f64(out, arg.f); break;
|
|
case CWEB_VARG_TYPE_D : append_to_output_u64(out, arg.d); break;
|
|
case CWEB_VARG_TYPE_B : append_to_output(out, arg.b ? "true" : "false", arg.b ? 4: 5);break;
|
|
case CWEB_VARG_TYPE_STR : append_to_output(out, arg.str.ptr, arg.str.len); break;
|
|
case CWEB_VARG_TYPE_HASH : append_to_output(out, arg.hash.data, strlen(arg.hash.data)); break;
|
|
case CWEB_VARG_TYPE_PC : append_to_output_ptr(out, arg.pc); break;
|
|
case CWEB_VARG_TYPE_PS : append_to_output_ptr(out, arg.ps); break;
|
|
case CWEB_VARG_TYPE_PI : append_to_output_ptr(out, arg.pi); break;
|
|
case CWEB_VARG_TYPE_PL : append_to_output_ptr(out, arg.pl); break;
|
|
case CWEB_VARG_TYPE_PLL : append_to_output_ptr(out, arg.pll); break;
|
|
case CWEB_VARG_TYPE_PSC : append_to_output_ptr(out, arg.psc); break;
|
|
case CWEB_VARG_TYPE_PSS : append_to_output_ptr(out, arg.pss); break;
|
|
case CWEB_VARG_TYPE_PSI : append_to_output_ptr(out, arg.psi); break;
|
|
case CWEB_VARG_TYPE_PSL : append_to_output_ptr(out, arg.psl); break;
|
|
case CWEB_VARG_TYPE_PSLL : append_to_output_ptr(out, arg.psll); break;
|
|
case CWEB_VARG_TYPE_PUC : append_to_output_ptr(out, arg.puc); break;
|
|
case CWEB_VARG_TYPE_PUS : append_to_output_ptr(out, arg.pus); break;
|
|
case CWEB_VARG_TYPE_PUI : append_to_output_ptr(out, arg.pui); break;
|
|
case CWEB_VARG_TYPE_PUL : append_to_output_ptr(out, arg.pul); break;
|
|
case CWEB_VARG_TYPE_PULL : append_to_output_ptr(out, arg.pull); break;
|
|
case CWEB_VARG_TYPE_PF : append_to_output_ptr(out, arg.pf); break;
|
|
case CWEB_VARG_TYPE_PD : append_to_output_ptr(out, arg.pd); break;
|
|
case CWEB_VARG_TYPE_PB : append_to_output_ptr(out, arg.pb); break;
|
|
case CWEB_VARG_TYPE_PSTR : append_to_output_ptr(out, arg.pstr); break;
|
|
case CWEB_VARG_TYPE_PHASH: append_to_output_ptr(out, arg.phash); break;
|
|
}
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
// FILE SYSTEM
|
|
////////////////////////////////////////////////////////////////
|
|
|
|
typedef struct LoadedFile LoadedFile;
|
|
struct LoadedFile {
|
|
LoadedFile* next;
|
|
int len;
|
|
char data[];
|
|
};
|
|
|
|
static LoadedFile *load_file(CWEB_String path)
|
|
{
|
|
char buf[1<<10];
|
|
if (path.len >= (int) sizeof(buf))
|
|
return NULL;
|
|
memcpy(buf, path.ptr, path.len);
|
|
buf[path.len] = '\0';
|
|
|
|
FILE *stream = fopen(buf, "rb");
|
|
if (stream == NULL)
|
|
return NULL;
|
|
|
|
int ret = fseek(stream, 0, SEEK_END);
|
|
if (ret) {
|
|
fclose(stream);
|
|
return NULL;
|
|
}
|
|
|
|
long tmp = ftell(stream);
|
|
if (tmp < 0 || tmp > INT_MAX) {
|
|
fclose(stream);
|
|
return NULL;
|
|
}
|
|
int len = (int) tmp;
|
|
|
|
ret = fseek(stream, 0, SEEK_SET);
|
|
if (ret) {
|
|
fclose(stream);
|
|
return NULL;
|
|
}
|
|
|
|
LoadedFile *result = malloc(sizeof(LoadedFile) + len + 1);
|
|
if (result == NULL) {
|
|
fclose(stream);
|
|
return NULL;
|
|
}
|
|
result->next = NULL;
|
|
result->len = len;
|
|
|
|
int read_len = fread(result->data, 1, len+1, stream);
|
|
if (read_len != len || ferror(stream) || !feof(stream)) {
|
|
fclose(stream);
|
|
free(result);
|
|
return NULL;
|
|
}
|
|
|
|
result->data[len] = '\0';
|
|
|
|
fclose(stream);
|
|
return result;
|
|
}
|
|
|
|
static void free_loaded_files(LoadedFile *loaded_file)
|
|
{
|
|
while (loaded_file) {
|
|
LoadedFile *next = loaded_file->next;
|
|
free(loaded_file);
|
|
loaded_file = next;
|
|
}
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
// RANDOM
|
|
////////////////////////////////////////////////////////////////
|
|
|
|
static int generate_random_bytes(char *dst, int cap)
|
|
{
|
|
#ifdef __linux__
|
|
int copied = 0;
|
|
while (copied < cap) {
|
|
int ret = getrandom(dst, (size_t) cap, 0);
|
|
if (ret < 0) {
|
|
if (errno == EINTR)
|
|
continue;
|
|
return -1;
|
|
}
|
|
copied += ret;
|
|
}
|
|
return 0;
|
|
#endif
|
|
|
|
#ifdef _WIN32
|
|
NTSTATUS status = BCryptGenRandom(NULL, (unsigned char*) dst, (ULONG) cap, BCRYPT_USE_SYSTEM_PREFERRED_RNG);
|
|
return BCRYPT_SUCCESS(status) ? 0 : -1;
|
|
#endif
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////
|
|
// PASSWORD
|
|
////////////////////////////////////////////////////////////////
|
|
|
|
int cweb_hash_password(CWEB_String pass, int cost, CWEB_PasswordHash *hash)
|
|
{
|
|
char passzt[128];
|
|
if (pass.len >= (int) sizeof(passzt))
|
|
return -1;
|
|
memcpy(passzt, pass.ptr, pass.len);
|
|
passzt[pass.len] = '\0';
|
|
|
|
char random[16];
|
|
int ret = generate_random_bytes(random, (int) sizeof(random));
|
|
if (ret) return -1;
|
|
|
|
char salt[30];
|
|
if (_crypt_gensalt_blowfish_rn("$2b$", cost, random, sizeof(random), salt, sizeof(salt)) == NULL)
|
|
return -1;
|
|
|
|
if (_crypt_blowfish_rn(passzt, salt, hash->data, (int) sizeof(hash->data)) == NULL)
|
|
return -1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int cweb_check_password(CWEB_String pass, CWEB_PasswordHash hash)
|
|
{
|
|
char passzt[128];
|
|
if (pass.len >= (int) sizeof(passzt))
|
|
return -1;
|
|
memcpy(passzt, pass.ptr, pass.len);
|
|
passzt[pass.len] = '\0';
|
|
|
|
CWEB_PasswordHash new_hash;
|
|
if (_crypt_blowfish_rn(passzt, hash.data, new_hash.data, sizeof(new_hash.data)) == NULL)
|
|
return -1;
|
|
|
|
if (strcmp(hash.data, new_hash.data)) // TODO: should be constant-time
|
|
return 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
// SESSION
|
|
////////////////////////////////////////////////////////////////
|
|
|
|
#define CSRF_RAW_TOKEN_SIZE 32
|
|
#define SESS_RAW_TOKEN_SIZE 32
|
|
|
|
#define CSRF_TOKEN_SIZE (2 * CSRF_RAW_TOKEN_SIZE)
|
|
#define SESS_TOKEN_SIZE (2 * SESS_RAW_TOKEN_SIZE)
|
|
|
|
typedef struct {
|
|
int user;
|
|
char csrf[CSRF_TOKEN_SIZE];
|
|
char sess[SESS_TOKEN_SIZE];
|
|
} Session;
|
|
|
|
struct SessionStorage {
|
|
int count;
|
|
int capacity;
|
|
Session items[];
|
|
};
|
|
|
|
#ifndef CWEB_AMALGAMATION
|
|
|
|
static bool is_hex_digit(char c)
|
|
{
|
|
return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
|
|
}
|
|
|
|
static int hex_digit_to_int(char c)
|
|
{
|
|
if (c >= 'A' && c <= 'F')
|
|
return c - 'A' + 10;
|
|
if (c >= 'a' && c <= 'f')
|
|
return c - 'a' + 10;
|
|
return c - '0';
|
|
}
|
|
|
|
#endif
|
|
|
|
static void unpack_token(char *src, int srclen, char *dst, int dstlen)
|
|
{
|
|
ASSERT(2 * srclen == dstlen);
|
|
|
|
for (int i = 0; i < srclen; i++) {
|
|
static const char table[] = "0123456789abcdef";
|
|
int low = (src[i] & 0x0F) >> 0;
|
|
int high = (src[i] & 0xF0) >> 4;
|
|
dst[(i << 1) | 0] = table[high];
|
|
dst[(i << 1) | 1] = table[low];
|
|
}
|
|
}
|
|
|
|
static int pack_token(char *src, int srclen, char *dst, int dstlen)
|
|
{
|
|
if (srclen & 1)
|
|
return -1;
|
|
|
|
ASSERT(srclen == 2 * dstlen);
|
|
|
|
for (int i = 0; i < srclen; i += 2) {
|
|
int high = src[i+0];
|
|
int low = src[i+1];
|
|
if (!is_hex_digit(high) || !is_hex_digit(low))
|
|
return -1;
|
|
dst[i] = (hex_digit_to_int(high) << 4) | (hex_digit_to_int(low) << 0);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static SessionStorage *session_storage_init(int max_sessions)
|
|
{
|
|
int capacity = 2 * max_sessions;
|
|
SessionStorage *storage = malloc(sizeof(SessionStorage) + capacity * sizeof(Session));
|
|
if (storage == NULL)
|
|
return NULL;
|
|
storage->count = 0;
|
|
storage->capacity = capacity;
|
|
for (int i = 0; i < capacity; i++)
|
|
storage->items[i].user = -1;
|
|
return storage;
|
|
}
|
|
|
|
static void session_storage_free(SessionStorage *storage)
|
|
{
|
|
free(storage);
|
|
}
|
|
|
|
static Session *lookup_session_slot(SessionStorage *storage, CWEB_String sess, bool find_unused)
|
|
{
|
|
if (find_unused && 2 * storage->count + 2 > storage->capacity)
|
|
return NULL;
|
|
|
|
if (sess.len != SESS_TOKEN_SIZE)
|
|
return NULL;
|
|
|
|
uint64_t key;
|
|
if (sess.len < (int) (2 * sizeof(key)))
|
|
return NULL;
|
|
for (int i = 0; i < (int) sizeof(key); i++) {
|
|
|
|
int high = sess.ptr[(i << 1) | 0];
|
|
int low = sess.ptr[(i << 1) | 1];
|
|
|
|
if (!is_hex_digit(sess.ptr[i+0]) ||
|
|
!is_hex_digit(sess.ptr[i+1]))
|
|
return NULL;
|
|
|
|
key <<= 4;
|
|
key |= hex_digit_to_int(high);
|
|
|
|
key <<= 4;
|
|
key |= hex_digit_to_int(low);
|
|
}
|
|
int i = key % storage->capacity;
|
|
|
|
for (int j = 0; j < storage->capacity; j++) {
|
|
|
|
if (find_unused) {
|
|
|
|
if (storage->items[i].user < 0)
|
|
return &storage->items[i]; // Unused slot
|
|
|
|
} else {
|
|
|
|
if (storage->items[i].user == -1)
|
|
return NULL;
|
|
|
|
if (storage->items[i].user != -2)
|
|
if (!memcmp(storage->items[i].sess, sess.ptr, SESS_TOKEN_SIZE))
|
|
return &storage->items[i];
|
|
}
|
|
|
|
i++;
|
|
if (i == storage->capacity)
|
|
i = 0;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static int create_session(SessionStorage *storage, int user, CWEB_String *psess, CWEB_String *pcsrf)
|
|
{
|
|
int ret;
|
|
char raw_sess[SESS_RAW_TOKEN_SIZE];
|
|
char raw_csrf[CSRF_RAW_TOKEN_SIZE];
|
|
|
|
ret = generate_random_bytes(raw_sess, SESS_RAW_TOKEN_SIZE);
|
|
if (ret) return -1;
|
|
|
|
ret = generate_random_bytes(raw_csrf, CSRF_RAW_TOKEN_SIZE);
|
|
if (ret) return -1;
|
|
|
|
char sess[SESS_TOKEN_SIZE];
|
|
char csrf[CSRF_TOKEN_SIZE];
|
|
unpack_token(raw_sess, SESS_RAW_TOKEN_SIZE, sess, SESS_TOKEN_SIZE);
|
|
unpack_token(raw_csrf, CSRF_RAW_TOKEN_SIZE, csrf, CSRF_TOKEN_SIZE);
|
|
|
|
Session *found = lookup_session_slot(storage, (CWEB_String) { sess, SESS_TOKEN_SIZE }, true);
|
|
if (found == NULL) return -1;
|
|
|
|
found->user = user;
|
|
memcpy(found->sess, sess, SESS_TOKEN_SIZE);
|
|
memcpy(found->csrf, csrf, CSRF_TOKEN_SIZE);
|
|
|
|
*psess = (CWEB_String) { found->sess, SESS_TOKEN_SIZE };
|
|
*pcsrf = (CWEB_String) { found->csrf, CSRF_TOKEN_SIZE };
|
|
|
|
storage->count++;
|
|
return 0;
|
|
}
|
|
|
|
static int delete_session(SessionStorage *storage, CWEB_String sess)
|
|
{
|
|
char raw_sess[SESS_RAW_TOKEN_SIZE];
|
|
if (sess.len != SESS_TOKEN_SIZE || pack_token(sess.ptr, sess.len, raw_sess, (int) sizeof(raw_sess)) < 0)
|
|
return -1;
|
|
Session *found = lookup_session_slot(storage, sess, false);
|
|
if (found == NULL)
|
|
return false;
|
|
ASSERT(found->user >= 0);
|
|
found->user = -2;
|
|
storage->count--;
|
|
return 0;
|
|
}
|
|
|
|
static int find_session(SessionStorage *storage, CWEB_String sess, CWEB_String *pcsrf, int *puser)
|
|
{
|
|
Session *found = lookup_session_slot(storage, sess, false);
|
|
if (found == NULL)
|
|
return -1;
|
|
ASSERT(found->user >= 0);
|
|
*pcsrf = (CWEB_String) { found->csrf, CSRF_TOKEN_SIZE };
|
|
*puser = found->user;
|
|
return 0;
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
// DATABASE
|
|
////////////////////////////////////////////////////////////////
|
|
|
|
#ifdef CWEB_ENABLE_DATABASE
|
|
|
|
typedef struct {
|
|
char *str;
|
|
int len;
|
|
sqlite3_stmt *stmt;
|
|
} Prepped;
|
|
|
|
struct SQLiteCache {
|
|
sqlite3 *db;
|
|
int count;
|
|
int capacity_log2;
|
|
Prepped items[];
|
|
};
|
|
|
|
static SQLiteCache *sqlite_cache_init(sqlite3 *db, int capacity_log2)
|
|
{
|
|
SQLiteCache *cache = malloc(sizeof(SQLiteCache) + (1 << capacity_log2) * sizeof(Prepped));
|
|
if (cache == NULL)
|
|
return NULL;
|
|
|
|
cache->db = db;
|
|
cache->count = 0;
|
|
cache->capacity_log2 = capacity_log2;
|
|
|
|
for (int i = 0; i < (1 << capacity_log2); i++)
|
|
cache->items[i].stmt = NULL;
|
|
|
|
return cache;
|
|
}
|
|
|
|
static void sqlite_cache_free(SQLiteCache *cache)
|
|
{
|
|
for (int i = 0; i < (1 << cache->capacity_log2); i++) {
|
|
sqlite3_stmt *stmt = cache->items[i].stmt;
|
|
if (stmt) {
|
|
free(cache->items[i].str);
|
|
sqlite3_finalize(stmt);
|
|
}
|
|
}
|
|
free(cache);
|
|
}
|
|
|
|
static unsigned long djb2(char *src, int len)
|
|
{
|
|
char *ptr = src;
|
|
char *end = src + len;
|
|
|
|
unsigned long hash = 5381;
|
|
int c;
|
|
while (ptr < end && (c = *ptr++))
|
|
hash = ((hash << 5) + hash) + c; // hash * 33 + c
|
|
return hash;
|
|
}
|
|
|
|
static int sqlite_cache_lookup(SQLiteCache *cache, char *fmt, int fmtlen)
|
|
{
|
|
int mask = (1 << cache->capacity_log2) - 1;
|
|
int hash = djb2(fmt, fmtlen);
|
|
int i = hash & mask;
|
|
int perturb = hash;
|
|
for (;;) {
|
|
|
|
if (cache->items[i].stmt == NULL)
|
|
return i;
|
|
|
|
if (cache->items[i].len == fmtlen && !memcmp(cache->items[i].str, fmt, fmtlen))
|
|
return i;
|
|
|
|
perturb >>= 5;
|
|
i = (i * 5 + 1 + perturb) & mask;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
static int sqlite3utils_prepare(SQLiteCache *cache, sqlite3_stmt **pstmt, char *fmt, int fmtlen)
|
|
{
|
|
if (fmtlen < 0)
|
|
fmtlen = strlen(fmt);
|
|
|
|
int i = sqlite_cache_lookup(cache, fmt, fmtlen);
|
|
if (cache->items[i].stmt == NULL) {
|
|
|
|
sqlite3_stmt *stmt;
|
|
int ret = sqlite3_prepare_v2(cache->db, fmt, fmtlen, &stmt, NULL);
|
|
if (ret != SQLITE_OK) {
|
|
fprintf(stderr, "Failed to prepare statement: %s (%s:%d)\n", sqlite3_errmsg(cache->db), __FILE__, __LINE__); // TODO
|
|
return ret;
|
|
}
|
|
|
|
char *cpy = malloc(fmtlen);
|
|
if (cpy == NULL) {
|
|
sqlite3_finalize(stmt);
|
|
return SQLITE_NOMEM;
|
|
}
|
|
memcpy(cpy, fmt, fmtlen);
|
|
|
|
cache->items[i].str = cpy;
|
|
cache->items[i].len = fmtlen;
|
|
cache->items[i].stmt = stmt;
|
|
}
|
|
sqlite3_stmt *stmt = cache->items[i].stmt;
|
|
|
|
*pstmt = stmt;
|
|
return SQLITE_OK;
|
|
}
|
|
|
|
static int sqlite3utils_prepare_and_bind_impl(SQLiteCache *cache,
|
|
sqlite3_stmt **pstmt, char *fmt, CWEB_VArgs args)
|
|
{
|
|
sqlite3_stmt *stmt;
|
|
int ret = sqlite3utils_prepare(cache, &stmt, fmt, strlen(fmt));
|
|
if (ret != SQLITE_OK)
|
|
return ret;
|
|
|
|
for (int i = 0; i < args.len; i++) {
|
|
CWEB_VArg arg = args.ptr[i];
|
|
switch (arg.type) {
|
|
case CWEB_VARG_TYPE_C : ret = sqlite3_bind_text (stmt, i+1, &arg.c, 1, NULL); break;
|
|
case CWEB_VARG_TYPE_S : ret = sqlite3_bind_int (stmt, i+1, arg.s); break;
|
|
case CWEB_VARG_TYPE_I : ret = sqlite3_bind_int (stmt, i+1, arg.i); break;
|
|
case CWEB_VARG_TYPE_L : ret = sqlite3_bind_int64 (stmt, i+1, arg.l); break;
|
|
case CWEB_VARG_TYPE_LL : ret = sqlite3_bind_int64 (stmt, i+1, arg.ll); break;
|
|
case CWEB_VARG_TYPE_SC : ret = sqlite3_bind_int (stmt, i+1, arg.sc); break;
|
|
case CWEB_VARG_TYPE_SS : ret = sqlite3_bind_int (stmt, i+1, arg.ss); break;
|
|
case CWEB_VARG_TYPE_SI : ret = sqlite3_bind_int (stmt, i+1, arg.si); break;
|
|
case CWEB_VARG_TYPE_SL : ret = sqlite3_bind_int64 (stmt, i+1, arg.sl); break;
|
|
case CWEB_VARG_TYPE_SLL : ret = sqlite3_bind_int (stmt, i+1, arg.sll); break;
|
|
case CWEB_VARG_TYPE_UC : ret = sqlite3_bind_int (stmt, i+1, arg.uc); break;
|
|
case CWEB_VARG_TYPE_US : ret = sqlite3_bind_int (stmt, i+1, arg.us); break;
|
|
case CWEB_VARG_TYPE_UI : ret = sqlite3_bind_int64 (stmt, i+1, arg.ui); break;
|
|
case CWEB_VARG_TYPE_UL : ret = sqlite3_bind_int64 (stmt, i+1, arg.ul); break;
|
|
case CWEB_VARG_TYPE_ULL : ret = sqlite3_bind_int64 (stmt, i+1, arg.ull); break;
|
|
case CWEB_VARG_TYPE_F : ret = sqlite3_bind_double(stmt, i+1, arg.f); break;
|
|
case CWEB_VARG_TYPE_D : ret = sqlite3_bind_double(stmt, i+1, arg.d); break;
|
|
case CWEB_VARG_TYPE_B : ret = sqlite3_bind_int (stmt, i+1, arg.b); break;
|
|
case CWEB_VARG_TYPE_STR : ret = sqlite3_bind_text (stmt, i+1, arg.str.ptr, arg.str.len, NULL); break;
|
|
case CWEB_VARG_TYPE_HASH: ret = sqlite3_bind_text (stmt, i+1, arg.hash.data, -1, NULL); break;
|
|
default:
|
|
ASSERT(0); // TODO
|
|
break;
|
|
}
|
|
if (ret != SQLITE_OK) {
|
|
fprintf(stderr, "Failed to bind paremeter: %s (%s:%d)\n", sqlite3_errmsg(cache->db), __FILE__, __LINE__); // TODO
|
|
sqlite3_reset(stmt);
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
*pstmt = stmt;
|
|
return SQLITE_OK;
|
|
}
|
|
|
|
#endif // CWEB_ENABLE_DATABASE
|
|
|
|
static void dump_sql(char *fmt, CWEB_VArgs args)
|
|
{
|
|
printf("SQL :: %s", fmt);
|
|
if (args.len > 0) {
|
|
printf(" (");
|
|
for (int i = 0; i < args.len; i++) {
|
|
char mem[128];
|
|
StaticOutputBuffer buf = { mem, sizeof(mem), 0 };
|
|
value_to_output(&buf, args.ptr[i]);
|
|
printf("%.*s", buf.len, buf.dst);
|
|
if (i+1 < args.len)
|
|
printf(", ");
|
|
}
|
|
printf(")");
|
|
}
|
|
printf("\n");
|
|
}
|
|
|
|
int64_t cweb_database_insert_impl(CWEB *cweb, char *fmt, CWEB_VArgs args)
|
|
{
|
|
#ifdef CWEB_ENABLE_DATABASE
|
|
if (cweb->trace_sql) dump_sql(fmt, args);
|
|
|
|
sqlite3_stmt *stmt;
|
|
int ret = sqlite3utils_prepare_and_bind_impl(cweb->dbcache, &stmt, fmt, args);
|
|
if (ret != SQLITE_OK)
|
|
return -1;
|
|
|
|
ret = sqlite3_step(stmt);
|
|
if (ret != SQLITE_DONE) {
|
|
sqlite3_reset(stmt);
|
|
return -1;
|
|
}
|
|
|
|
int64_t insert_id = sqlite3_last_insert_rowid(cweb->db);
|
|
if (insert_id < 0) {
|
|
fprintf(stderr, "Insert ID is invalid: %s (%s:%d)\n", sqlite3_errmsg(cweb->db), __FILE__, __LINE__); // TODO
|
|
sqlite3_reset(stmt);
|
|
return -1;
|
|
}
|
|
|
|
sqlite3_reset(stmt);
|
|
return insert_id;
|
|
#else
|
|
return -1;
|
|
#endif
|
|
}
|
|
|
|
CWEB_QueryResult cweb_database_select_impl(CWEB *cweb, char *fmt, CWEB_VArgs args)
|
|
{
|
|
#ifdef CWEB_ENABLE_DATABASE
|
|
if (cweb->trace_sql) dump_sql(fmt, args);
|
|
|
|
sqlite3_stmt *stmt;
|
|
int ret = sqlite3utils_prepare_and_bind_impl(cweb->dbcache, &stmt, fmt, args);
|
|
if (ret != SQLITE_OK)
|
|
return (CWEB_QueryResult) { NULL };
|
|
|
|
return (CWEB_QueryResult) { stmt };
|
|
#else
|
|
return (CWEB_QueryResult) { NULL };
|
|
#endif
|
|
}
|
|
|
|
int cweb_next_query_row_impl(CWEB_QueryResult *res, CWEB_VArgs args)
|
|
{
|
|
#ifdef CWEB_ENABLE_DATABASE
|
|
if (res->handle == NULL)
|
|
return -1;
|
|
|
|
int ret = sqlite3_step(res->handle);
|
|
|
|
if (ret == SQLITE_DONE)
|
|
return 0;
|
|
|
|
if (ret != SQLITE_ROW) {
|
|
sqlite3_reset(res->handle);
|
|
res->handle = NULL;
|
|
return -1;
|
|
}
|
|
|
|
if (sqlite3_column_count(res->handle) != args.len) {
|
|
sqlite3_reset(res->handle);
|
|
res->handle = NULL;
|
|
return -1;
|
|
}
|
|
|
|
for (int i = 0; i < args.len; i++) {
|
|
switch (args.ptr[i].type) {
|
|
|
|
case CWEB_VARG_TYPE_PI:
|
|
{
|
|
int64_t x = sqlite3_column_int64(res->handle, i);
|
|
if (x < INT_MIN || x > INT_MAX) {
|
|
sqlite3_reset(res->handle);
|
|
res->handle = NULL;
|
|
return -1;
|
|
}
|
|
*args.ptr[i].pi = (int) x;
|
|
}
|
|
break;
|
|
|
|
case CWEB_VARG_TYPE_PL:
|
|
{
|
|
int64_t x = sqlite3_column_int64(res->handle, i);
|
|
if (x < LONG_MIN || x > LONG_MAX) {
|
|
sqlite3_reset(res->handle);
|
|
res->handle = NULL;
|
|
return -1;
|
|
}
|
|
*args.ptr[i].pl = (long) x;
|
|
}
|
|
break;
|
|
|
|
case CWEB_VARG_TYPE_PLL:
|
|
{
|
|
int64_t x = sqlite3_column_int64(res->handle, i);
|
|
*args.ptr[i].pll = (int) x;
|
|
}
|
|
break;
|
|
|
|
case CWEB_VARG_TYPE_PF:
|
|
{
|
|
double x = sqlite3_column_double(res->handle, i);
|
|
*args.ptr[i].pf = (float) x;
|
|
}
|
|
break;
|
|
|
|
case CWEB_VARG_TYPE_PD:
|
|
{
|
|
double x = sqlite3_column_double(res->handle, i);
|
|
*args.ptr[i].pf = (float) x;
|
|
}
|
|
break;
|
|
|
|
case CWEB_VARG_TYPE_PSTR:
|
|
{
|
|
*args.ptr[i].pstr = (CWEB_String) {
|
|
sqlite3_column_text(res->handle, i),
|
|
sqlite3_column_bytes(res->handle, i),
|
|
};
|
|
}
|
|
break;
|
|
|
|
default:
|
|
sqlite3_reset(res->handle);
|
|
res->handle = NULL;
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
#else
|
|
(void) res;
|
|
(void) args;
|
|
return -1;
|
|
#endif
|
|
}
|
|
|
|
void cweb_free_query_result(CWEB_QueryResult *res)
|
|
{
|
|
#ifdef CWEB_ENABLE_DATABASE
|
|
if (res->handle) {
|
|
sqlite3_reset(res->handle);
|
|
res->handle = NULL;
|
|
}
|
|
#else
|
|
(void) res;
|
|
#endif
|
|
}
|
|
|
|
int cweb_global_init(void)
|
|
{
|
|
if (http_global_init())
|
|
return -1;
|
|
return 0;
|
|
}
|
|
|
|
void cweb_global_free(void)
|
|
{
|
|
http_global_free();
|
|
}
|
|
|
|
CWEB *cweb_init(CWEB_String addr, uint16_t port, uint16_t secure_port,
|
|
CWEB_String cert_key, CWEB_String private_key)
|
|
{
|
|
CWEB *cweb = malloc(sizeof(CWEB));
|
|
if (cweb == NULL)
|
|
return NULL;
|
|
|
|
cweb->pool_cap = 1<<20;
|
|
cweb->pool = malloc(cweb->pool_cap);
|
|
if (cweb->pool == NULL) {
|
|
free(cweb);
|
|
return NULL;
|
|
}
|
|
|
|
#ifdef CWEB_ENABLE_TEMPLATE
|
|
cweb->tpcache = template_cache_init(4);
|
|
if (cweb->tpcache == NULL) {
|
|
free(cweb->pool);
|
|
free(cweb);
|
|
return NULL;
|
|
}
|
|
cweb->enable_template_cache = true;
|
|
#endif
|
|
|
|
cweb->session_storage = session_storage_init(1024);
|
|
if (cweb->session_storage == NULL) {
|
|
#ifdef CWEB_ENABLE_TEMPLATE
|
|
template_cache_free(cweb->tpcache);
|
|
#endif
|
|
free(cweb->pool);
|
|
free(cweb);
|
|
return NULL;
|
|
}
|
|
|
|
cweb->server = http_server_init_ex((HTTP_String) { addr.ptr, addr.len }, port, secure_port, (HTTP_String) { cert_key.ptr, cert_key.len }, (HTTP_String) { private_key.ptr, private_key.len });
|
|
if (cweb->server == NULL) {
|
|
session_storage_free(cweb->session_storage);
|
|
#ifdef CWEB_ENABLE_TEMPLATE
|
|
template_cache_free(cweb->tpcache);
|
|
#endif
|
|
free(cweb->pool);
|
|
free(cweb);
|
|
return NULL;
|
|
}
|
|
|
|
#ifdef CWEB_ENABLE_DATABASE
|
|
cweb->db = NULL;
|
|
cweb->dbcache = NULL;
|
|
cweb->trace_sql = false;
|
|
#endif
|
|
|
|
// If set, allows logins and signups over HTTP, which is highly insecure.
|
|
// This allows compiling the application without TLS when developing.
|
|
cweb->allow_insecure_login = true;
|
|
|
|
if (cweb->allow_insecure_login)
|
|
printf("WARNING: allow_insecure_login is true\n");
|
|
|
|
return cweb;
|
|
}
|
|
|
|
void cweb_free(CWEB *cweb)
|
|
{
|
|
http_server_free(cweb->server);
|
|
session_storage_free(cweb->session_storage);
|
|
#ifdef CWEB_ENABLE_TEMPLATE
|
|
template_cache_free(cweb->tpcache);
|
|
#endif
|
|
#ifdef CWEB_ENABLE_DATABASE
|
|
if (cweb->db) {
|
|
sqlite_cache_free(cweb->dbcache);
|
|
sqlite3_close(cweb->db);
|
|
}
|
|
#endif
|
|
free(cweb);
|
|
}
|
|
|
|
int cweb_add_website(CWEB *cweb, CWEB_String domain, CWEB_String cert_file, CWEB_String key_file)
|
|
{
|
|
return http_server_add_website(cweb->server,
|
|
(HTTP_String) { domain.ptr, domain.len },
|
|
(HTTP_String) { cert_file.ptr, cert_file.len },
|
|
(HTTP_String) { key_file.ptr, key_file.len }
|
|
);
|
|
}
|
|
|
|
int cweb_create_test_certificate(CWEB_String C, CWEB_String O,
|
|
CWEB_String CN, CWEB_String cert_file, CWEB_String key_file)
|
|
{
|
|
return http_create_test_certificate(
|
|
(HTTP_String) { C.ptr, C.len },
|
|
(HTTP_String) { O.ptr, O.len },
|
|
(HTTP_String) { CN.ptr, CN.len },
|
|
(HTTP_String) { cert_file.ptr, cert_file.len },
|
|
(HTTP_String) { key_file.ptr, key_file.len }
|
|
);
|
|
}
|
|
|
|
void cweb_version(void)
|
|
{
|
|
printf("%s\n", sqlite3_libversion());
|
|
}
|
|
|
|
void cweb_trace_sql(CWEB *cweb, bool enable)
|
|
{
|
|
#ifdef CWEB_ENABLE_DATABASE
|
|
cweb->trace_sql = enable;
|
|
#endif
|
|
}
|
|
|
|
void cweb_enable_template_cache(CWEB *cweb, bool enable)
|
|
{
|
|
cweb->enable_template_cache = enable;
|
|
}
|
|
|
|
int cweb_enable_database(CWEB *cweb, CWEB_String database_file, CWEB_String schema_file)
|
|
{
|
|
#ifdef CWEB_ENABLE_DATABASE
|
|
if (cweb->db != NULL)
|
|
return -1; // Already enabled
|
|
|
|
char file_copy[1<<12];
|
|
if (database_file.len >= SIZEOF(file_copy))
|
|
return -1;
|
|
memcpy(file_copy, database_file.ptr, database_file.len);
|
|
file_copy[database_file.len] = '\0';
|
|
|
|
int ret = sqlite3_open(file_copy, &cweb->db);
|
|
if (ret != SQLITE_OK) {
|
|
sqlite3_close(cweb->db);
|
|
cweb->db = NULL;
|
|
return -1;
|
|
}
|
|
|
|
ret = sqlite3_exec(cweb->db, "PRAGMA foreign_keys = ON;", NULL, NULL, NULL);
|
|
if (ret != SQLITE_OK) {
|
|
sqlite3_close(cweb->db);
|
|
cweb->db = NULL;
|
|
return -1;
|
|
}
|
|
|
|
LoadedFile *schema = load_file(schema_file);
|
|
if (schema == NULL) {
|
|
sqlite3_close(cweb->db);
|
|
cweb->db = NULL;
|
|
return -1;
|
|
}
|
|
|
|
ret = sqlite3_exec(cweb->db, schema->data, NULL, NULL, NULL);
|
|
if (ret != SQLITE_OK) {
|
|
free_loaded_files(schema);
|
|
sqlite3_close(cweb->db);
|
|
cweb->db = NULL;
|
|
return -1;
|
|
}
|
|
|
|
free_loaded_files(schema);
|
|
|
|
cweb->dbcache = sqlite_cache_init(cweb->db, 5);
|
|
if (cweb->dbcache == NULL) {
|
|
sqlite3_close(cweb->db);
|
|
cweb->db = NULL;
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
#else
|
|
return -1;
|
|
#endif
|
|
}
|
|
|
|
CWEB_Request *cweb_wait(CWEB *cweb)
|
|
{
|
|
CWEB_Request *req = &cweb->req;
|
|
|
|
int ret = http_server_wait(cweb->server, &req->req, &req->builder);
|
|
if (ret) return NULL; // Error or signal
|
|
|
|
HTTP_String session_token = http_get_cookie(req->req, HTTP_STR("sess_token"));
|
|
|
|
req->cweb = cweb;
|
|
req->arena = (WL_Arena) { cweb->pool, cweb->pool_cap, 0 };
|
|
req->just_created_session = false;
|
|
req->sess = (CWEB_String) { session_token.ptr, session_token.len };
|
|
|
|
if (find_session(cweb->session_storage, req->sess, &req->csrf, &req->user_id) < 0) {
|
|
req->user_id = -1;
|
|
req->sess = (CWEB_String) { NULL, 0 };
|
|
req->csrf = (CWEB_String) { NULL, 0 };
|
|
}
|
|
|
|
return req;
|
|
}
|
|
|
|
bool cweb_match_domain(CWEB_Request *req, CWEB_String str)
|
|
{
|
|
int idx = http_find_header(req->req->headers, req->req->num_headers, HTTP_STR("host"));
|
|
if (idx < 0) return false;
|
|
|
|
return http_streq((HTTP_String) { str.ptr, str.len }, req->req->headers[idx].value);
|
|
}
|
|
|
|
bool cweb_match_endpoint(CWEB_Request *req, CWEB_String str)
|
|
{
|
|
return http_streq(req->req->url.path, (HTTP_String) { str.ptr, str.len });
|
|
}
|
|
|
|
CWEB_String cweb_get_path(CWEB_Request *req)
|
|
{
|
|
HTTP_String path = req->req->url.path;
|
|
return (CWEB_String) { path.ptr, path.len };
|
|
}
|
|
|
|
CWEB_String cweb_get_param_s(CWEB_Request *req, CWEB_String name)
|
|
{
|
|
HTTP_String src = req->req->url.query;
|
|
if (req->req->method == HTTP_METHOD_POST)
|
|
src = req->req->body;
|
|
|
|
HTTP_String res = http_get_param(src,
|
|
(HTTP_String) { name.ptr, name.len },
|
|
req->arena.ptr + req->arena.cur,
|
|
req->arena.len - req->arena.cur
|
|
);
|
|
if (res.ptr >= req->arena.ptr && res.ptr < req->arena.ptr + req->arena.len)
|
|
req->arena.cur += res.len;
|
|
return (CWEB_String) { res.ptr, res.len };
|
|
}
|
|
|
|
int cweb_get_param_i(CWEB_Request *req, CWEB_String name)
|
|
{
|
|
HTTP_String src = req->req->url.query;
|
|
if (req->req->method == HTTP_METHOD_POST)
|
|
src = req->req->body;
|
|
|
|
return http_get_param_i(src, (HTTP_String) { name.ptr, name.len });
|
|
}
|
|
|
|
static int set_auth_cookie_if_necessary(CWEB_Request *req)
|
|
{
|
|
if (req->just_created_session) {
|
|
char cookie[1<<9];
|
|
int cookie_len = snprintf(cookie, sizeof(cookie), "Set-Cookie: sess_token=%.*s; Path=/; HttpOnly%s", req->sess.len, req->sess.ptr, req->cweb->allow_insecure_login ? "" : "; Secure");
|
|
if (cookie_len < 0 || cookie_len >= (int) sizeof(cookie))
|
|
return -500;
|
|
http_response_builder_header(req->builder, (HTTP_String) { cookie, cookie_len });
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void cweb_respond_basic(CWEB_Request *req, int status, CWEB_String content)
|
|
{
|
|
http_response_builder_status(req->builder, status);
|
|
int ret = set_auth_cookie_if_necessary(req);
|
|
if (ret < 0) {
|
|
http_response_builder_undo(req->builder);
|
|
http_response_builder_status(req->builder, -ret);
|
|
http_response_builder_done(req->builder);
|
|
return;
|
|
}
|
|
http_response_builder_body(req->builder, (HTTP_String) { content.ptr, content.len });
|
|
http_response_builder_done(req->builder);
|
|
}
|
|
|
|
static void evaluate_format(StaticOutputBuffer *out, CWEB_String format, CWEB_VArgs args)
|
|
{
|
|
char *src = format.ptr;
|
|
int len = format.len;
|
|
int cur = 0;
|
|
int arg_idx = 0;
|
|
|
|
for (;;) {
|
|
|
|
int off = cur;
|
|
while (cur < len && src[cur] != '{' && src[cur] != '\\')
|
|
cur++;
|
|
|
|
if (cur > off)
|
|
append_to_output(out, src + off, cur - off);
|
|
|
|
if (cur == len)
|
|
break;
|
|
cur++;
|
|
|
|
if (src[cur-1] == '{') {
|
|
|
|
while (cur < len && src[cur] != '}')
|
|
cur++;
|
|
|
|
if (cur < len) {
|
|
ASSERT(src[cur] == '}');
|
|
cur++;
|
|
}
|
|
|
|
if (arg_idx < args.len) {
|
|
value_to_output(out, args.ptr[arg_idx]);
|
|
arg_idx++;
|
|
}
|
|
|
|
} else {
|
|
ASSERT(src[cur-1] == '\\');
|
|
if (cur < len) {
|
|
append_to_output(out, &src[cur], 1);
|
|
cur++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
CWEB_String cweb_format_impl(CWEB_Request *req, char *fmt, CWEB_VArgs args)
|
|
{
|
|
StaticOutputBuffer out = {
|
|
.dst = req->arena.ptr + req->arena.cur,
|
|
.cap = req->arena.len - req->arena.cur,
|
|
.len = 0
|
|
};
|
|
evaluate_format(&out, (CWEB_String) { fmt, strlen(fmt) }, args);
|
|
if (out.len > req->arena.len - req->arena.cur)
|
|
return (CWEB_String) { NULL, 0 };
|
|
req->arena.cur += out.len;
|
|
return (CWEB_String) { out.dst, out.len };
|
|
}
|
|
|
|
void cweb_respond_redirect(CWEB_Request *req, CWEB_String target)
|
|
{
|
|
CWEB_String location_header = cweb_format(req, "Location: {}", target);
|
|
if (location_header.len == 0) {
|
|
http_response_builder_status(req->builder, 500);
|
|
http_response_builder_done(req->builder);
|
|
return;
|
|
}
|
|
|
|
http_response_builder_status(req->builder, 303);
|
|
int ret = set_auth_cookie_if_necessary(req);
|
|
if (ret < 0) {
|
|
http_response_builder_undo(req->builder);
|
|
http_response_builder_status(req->builder, -ret);
|
|
http_response_builder_done(req->builder);
|
|
return;
|
|
}
|
|
http_response_builder_header(req->builder, (HTTP_String) { location_header.ptr, location_header.len });
|
|
http_response_builder_done(req->builder);
|
|
}
|
|
|
|
int cweb_set_user_id(CWEB_Request *req, int user_id)
|
|
{
|
|
if (user_id != req->user_id) {
|
|
|
|
if (!req->req->secure && !req->cweb->allow_insecure_login)
|
|
return -1;
|
|
|
|
int ret;
|
|
if (user_id == -1) ret = delete_session(req->cweb->session_storage, req->sess);
|
|
else ret = create_session(req->cweb->session_storage, user_id, &req->sess, &req->csrf);
|
|
|
|
if (ret < 0) return -1;
|
|
|
|
req->just_created_session = true;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int cweb_get_user_id(CWEB_Request *req)
|
|
{
|
|
return req->user_id;
|
|
}
|
|
|
|
CWEB_String cweb_get_csrf(CWEB_Request *req)
|
|
{
|
|
return req->csrf;
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
// TEMPLATE
|
|
////////////////////////////////////////////////////////////////
|
|
#ifdef CWEB_ENABLE_TEMPLATE
|
|
|
|
static TemplateCache *template_cache_init(int capacity_log2)
|
|
{
|
|
TemplateCache *cache = malloc(sizeof(TemplateCache) + (1 << capacity_log2) * sizeof(CachedProgram));
|
|
if (cache == NULL)
|
|
return NULL;
|
|
|
|
cache->count = 0;
|
|
cache->capacity_log2 = capacity_log2;
|
|
|
|
for (int i = 0; i < (1 << capacity_log2); i++)
|
|
cache->pool[i].pathlen = -1;
|
|
return cache;
|
|
}
|
|
|
|
static void template_cache_free(TemplateCache *cache)
|
|
{
|
|
free(cache);
|
|
}
|
|
|
|
static int template_cache_lookup(TemplateCache *cache, WL_String path)
|
|
{
|
|
int mask = (1 << cache->capacity_log2) - 1;
|
|
int hash = djb2(path.ptr, path.len);
|
|
int i = hash & mask;
|
|
int perturb = hash;
|
|
for (;;) {
|
|
|
|
if (cache->pool[i].pathlen == -1)
|
|
return i;
|
|
|
|
if (wl_streq(path, cache->pool[i].path, cache->pool[i].pathlen))
|
|
return i;
|
|
|
|
perturb >>= 5;
|
|
i = (i * 5 + 1 + perturb) & mask;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
static int compile(WL_String path, WL_Program *program, WL_Arena *arena)
|
|
{
|
|
WL_Compiler *compiler = wl_compiler_init(arena);
|
|
if (compiler == NULL) {
|
|
TRACE("Couldn't initialize WL compiler object");
|
|
return -1;
|
|
}
|
|
|
|
LoadedFile *loaded_file_head = NULL;
|
|
LoadedFile **loaded_file_tail = &loaded_file_head;
|
|
|
|
for (int i = 0;; i++) {
|
|
|
|
LoadedFile *loaded_file = load_file((CWEB_String) { path.ptr, path.len });
|
|
if (loaded_file == NULL) {
|
|
TRACE("Couldn't load file '%.*s'", path.len, path.ptr);
|
|
free_loaded_files(loaded_file_head);
|
|
return -1;
|
|
}
|
|
|
|
*loaded_file_tail = loaded_file;
|
|
loaded_file_tail = &loaded_file->next;
|
|
|
|
WL_String content = { loaded_file->data, loaded_file->len };
|
|
WL_AddResult result = wl_compiler_add(compiler, path, content);
|
|
|
|
if (result.type == WL_ADD_ERROR) {
|
|
TRACE("Compilation failed (%s)", wl_compiler_error(compiler).ptr);
|
|
free_loaded_files(loaded_file_head);
|
|
return -1;
|
|
}
|
|
|
|
if (result.type == WL_ADD_LINK) break;
|
|
|
|
ASSERT(result.type == WL_ADD_AGAIN);
|
|
path = result.path;
|
|
}
|
|
|
|
int ret = wl_compiler_link(compiler, program);
|
|
if (ret < 0) {
|
|
TRACE("Compilation failed (%s)", wl_compiler_error(compiler).ptr);
|
|
return -1;
|
|
}
|
|
|
|
free_loaded_files(loaded_file_head);
|
|
|
|
TRACE("Compilation succeded");
|
|
return 0;
|
|
}
|
|
|
|
static int query_routine(WL_Runtime *rt, SQLiteCache *dbcache)
|
|
{
|
|
if (dbcache == NULL) {
|
|
wl_push_none(rt); // Allow not pushing anything on the WL machine
|
|
return -1;
|
|
}
|
|
|
|
int num_args = wl_arg_count(rt);
|
|
if (num_args == 0)
|
|
return 0;
|
|
|
|
WL_String format;
|
|
if (!wl_arg_str(rt, 0, &format))
|
|
return -1;
|
|
|
|
sqlite3_stmt *stmt;
|
|
int ret = sqlite3utils_prepare(dbcache, &stmt, format.ptr, format.len);
|
|
if (ret != SQLITE_OK)
|
|
return -1;
|
|
|
|
for (int i = 1; i < num_args; i++) {
|
|
|
|
int64_t ival;
|
|
double fval;
|
|
WL_String str;
|
|
|
|
if (wl_arg_none(rt, i))
|
|
ret = sqlite3_bind_null(stmt, i);
|
|
else if (wl_arg_s64(rt, i, &ival))
|
|
ret = sqlite3_bind_int64(stmt, i, ival);
|
|
else if (wl_arg_f64(rt, i, &fval))
|
|
ret = sqlite3_bind_double(stmt, i, fval);
|
|
else if (wl_arg_str(rt, i, &str))
|
|
ret = sqlite3_bind_text(stmt, i, str.ptr, str.len, NULL);
|
|
else {
|
|
ASSERT(0); // TODO
|
|
}
|
|
|
|
if (ret != SQLITE_OK) {
|
|
sqlite3_reset(stmt);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
wl_push_array(rt, 0);
|
|
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
|
|
int num_cols = sqlite3_column_count(stmt);
|
|
if (num_cols < 0) {
|
|
sqlite3_reset(stmt);
|
|
return -1;
|
|
}
|
|
|
|
wl_push_map(rt, num_cols);
|
|
|
|
for (int i = 0; i < num_cols; i++) {
|
|
ret = sqlite3_column_type(stmt, i);
|
|
switch (ret) {
|
|
|
|
case SQLITE_INTEGER:
|
|
{
|
|
int64_t x = sqlite3_column_int64(stmt, i);
|
|
wl_push_s64(rt, x);
|
|
}
|
|
break;
|
|
|
|
case SQLITE_FLOAT:
|
|
{
|
|
double x = sqlite3_column_double(stmt, i);
|
|
wl_push_f64(rt, x);
|
|
}
|
|
break;
|
|
|
|
case SQLITE_TEXT:
|
|
{
|
|
const void *x = sqlite3_column_text(stmt, i);
|
|
int n = sqlite3_column_bytes(stmt, i);
|
|
wl_push_str(rt, (WL_String) { (char*) x, n });
|
|
}
|
|
break;
|
|
|
|
case SQLITE_BLOB:
|
|
{
|
|
const void *x = sqlite3_column_blob(stmt, i);
|
|
int n = sqlite3_column_bytes(stmt, i);
|
|
wl_push_str(rt, (WL_String) { (char*) x, n });
|
|
}
|
|
break;
|
|
|
|
case SQLITE_NULL:
|
|
{
|
|
wl_push_none(rt);
|
|
}
|
|
break;
|
|
}
|
|
|
|
const char *name = sqlite3_column_name(stmt, i);
|
|
|
|
wl_push_str(rt, (WL_String) { (char*) name, strlen(name) });
|
|
wl_insert(rt);
|
|
}
|
|
|
|
wl_append(rt);
|
|
}
|
|
|
|
sqlite3_reset(stmt);
|
|
return 0;
|
|
}
|
|
|
|
static int get_or_create_program(TemplateCache *cache, WL_String path, bool force_create, WL_Arena *arena, WL_Program *program)
|
|
{
|
|
if (cache == NULL)
|
|
return -1;
|
|
|
|
int i = template_cache_lookup(cache, path);
|
|
|
|
if (cache->pool[i].pathlen != -1 && force_create) {
|
|
cache->pool[i].pathlen = -1;
|
|
free(cache->pool[i].program.ptr);
|
|
}
|
|
|
|
if (cache->pool[i].pathlen == -1) {
|
|
|
|
WL_Program program;
|
|
int ret = compile(path, &program, arena);
|
|
if (ret < 0) return -1;
|
|
|
|
void *p = malloc(program.len);
|
|
if (p == NULL)
|
|
return -1;
|
|
memcpy(p, program.ptr, program.len);
|
|
program.ptr = p;
|
|
|
|
if ((int) sizeof(cache->pool->path) <= path.len)
|
|
return -1;
|
|
memcpy(cache->pool[i].path, path.ptr, path.len);
|
|
cache->pool[i].path[path.len] = '\0';
|
|
cache->pool[i].pathlen = path.len;
|
|
cache->pool[i].program = program;
|
|
}
|
|
|
|
*program = cache->pool[i].program;
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
void cweb_respond_template_impl(CWEB_Request *req, int status, CWEB_String template_file, CWEB_VArgs args)
|
|
{
|
|
#ifdef CWEB_ENABLE_TEMPLATE
|
|
http_response_builder_status(req->builder, status);
|
|
int ret = set_auth_cookie_if_necessary(req);
|
|
if (ret < 0) {
|
|
http_response_builder_undo(req->builder);
|
|
http_response_builder_status(req->builder, -ret);
|
|
http_response_builder_done(req->builder);
|
|
return;
|
|
}
|
|
|
|
WL_Program program;
|
|
ret = get_or_create_program(req->cweb->tpcache, (WL_String) { template_file.ptr, template_file.len }, !req->cweb->enable_template_cache, &req->arena, &program);
|
|
if (ret < 0) {
|
|
http_response_builder_undo(req->builder);
|
|
http_response_builder_status(req->builder, 500);
|
|
http_response_builder_done(req->builder);
|
|
return;
|
|
}
|
|
|
|
//wl_dump_program(program);
|
|
|
|
WL_Runtime *rt = wl_runtime_init(&req->arena, program);
|
|
if (rt == NULL) {
|
|
http_response_builder_undo(req->builder);
|
|
http_response_builder_status(req->builder, 500);
|
|
http_response_builder_done(req->builder);
|
|
return;
|
|
}
|
|
|
|
for (bool done = false; !done; ) {
|
|
|
|
WL_EvalResult result = wl_runtime_eval(rt);
|
|
switch (result.type) {
|
|
|
|
case WL_EVAL_DONE:
|
|
http_response_builder_done(req->builder);
|
|
done = true;
|
|
break;
|
|
|
|
case WL_EVAL_ERROR:
|
|
// wl_runtime_error(rt)
|
|
http_response_builder_undo(req->builder);
|
|
http_response_builder_status(req->builder, 500);
|
|
http_response_builder_done(req->builder);
|
|
return;
|
|
|
|
case WL_EVAL_SYSVAR:
|
|
if (wl_streq(result.str, "login_user_id", -1)) {
|
|
|
|
if (req->user_id < 0)
|
|
wl_push_none(rt);
|
|
else
|
|
wl_push_s64(rt, req->user_id);
|
|
|
|
} else if (wl_streq(result.str, "csrf", -1)) {
|
|
|
|
if (req->csrf.len == 0)
|
|
wl_push_none(rt);
|
|
else
|
|
wl_push_str(rt, (WL_String) { req->csrf.ptr, req->csrf.len });
|
|
}
|
|
break;
|
|
|
|
case WL_EVAL_SYSCALL:
|
|
if (wl_streq(result.str, "query", -1)) {
|
|
query_routine(rt, req->cweb->dbcache);
|
|
break;
|
|
}
|
|
if (wl_streq(result.str, "args", -1)) {
|
|
|
|
if (wl_arg_count(rt) != 1) {
|
|
// TODO
|
|
break;
|
|
}
|
|
|
|
int64_t idx;
|
|
if (!wl_arg_s64(rt, 0, &idx)) {
|
|
// TODO
|
|
break;
|
|
}
|
|
|
|
if (idx < 0 || idx >= args.len) {
|
|
// TODO
|
|
break;
|
|
}
|
|
|
|
CWEB_VArg arg = args.ptr[idx];
|
|
switch (arg.type) {
|
|
case CWEB_VARG_TYPE_C : wl_push_s64(rt, arg.c); break;
|
|
case CWEB_VARG_TYPE_S : wl_push_s64(rt, arg.s); break;
|
|
case CWEB_VARG_TYPE_I : wl_push_s64(rt, arg.i); break;
|
|
case CWEB_VARG_TYPE_L : wl_push_s64(rt, arg.l); break;
|
|
case CWEB_VARG_TYPE_LL : wl_push_s64(rt, arg.ll); break;
|
|
case CWEB_VARG_TYPE_SC : wl_push_s64(rt, arg.sc); break;
|
|
case CWEB_VARG_TYPE_SS : wl_push_s64(rt, arg.ss); break;
|
|
case CWEB_VARG_TYPE_SI : wl_push_s64(rt, arg.si); break;
|
|
case CWEB_VARG_TYPE_SL : wl_push_s64(rt, arg.sl); break;
|
|
case CWEB_VARG_TYPE_SLL: wl_push_s64(rt, arg.sll); break;
|
|
case CWEB_VARG_TYPE_F : wl_push_f64(rt, arg.f); break;
|
|
case CWEB_VARG_TYPE_D : wl_push_s64(rt, arg.d); break;
|
|
case CWEB_VARG_TYPE_B : if (arg.b) wl_push_true(rt); else wl_push_false(rt); break;
|
|
case CWEB_VARG_TYPE_STR: wl_push_str(rt, (WL_String) { arg.str.ptr, arg.str.len }); break;
|
|
default:break;
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case WL_EVAL_OUTPUT:
|
|
http_response_builder_body(req->builder, (HTTP_String) { result.str.ptr, result.str.len });
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
#else
|
|
http_response_builder_status(req->builder, 500);
|
|
http_response_builder_done(req->builder);
|
|
#endif
|
|
}
|