/* sledjchisl.c - opensim-SC management system.
*
* Copyright 2020 David Seikel \n"
" DEBUG
\n\n");
else
reply->addstrf(reply, "Session:
\n\n");
if (NULL != shs->name)
reply->addstrf(reply, " name = %s\n", shs->name);
if (NULL != shs->UUID)
reply->addstrf(reply, " UUID = %s\n", shs->UUID);
reply->addstrf(reply, " salt = %s\n", shs->salt);
reply->addstrf(reply, " seshID = %s\n", shs->seshID);
reply->addstrf(reply, " timeStamp = %ld.%ld\n", shs->timeStamp[1].tv_sec, shs->timeStamp[1].tv_nsec);
reply->addstrf(reply, " sesh = %s\n", shs->sesh);
reply->addstrf(reply, " munchie = %s\n", shs->munchie);
reply->addstrf(reply, " toke_n_munchie = %s\n", shs->toke_n_munchie);
reply->addstrf(reply, " hashish = %s\n", shs->hashish);
reply->addstrf(reply, " leaf = %s\n", shs->leaf);
reply->addstrf(reply, " level = %d\n", (int) shs->level);
reply->addstr(reply, "
\n");
}
char toybuf[4096];
lua_State *L;
qhashtbl_t *configs;
MYSQL *database, *dbconn;
gridStats *stats;
boolean isTmux = 0;
boolean isWeb = 0;
char *pwd = "";
char *scRoot = "/opt/opensim_SC";
char *scUser = "opensimsc";
char *scBin = "";
char *scEtc = "";
char *scLib = "";
char *scRun = "";
char *scBackup = "";
char *scCache = "";
char *scData = "";
char *scLog = "";
char *Tconsole = "SledjChisl";
char *Tsocket = "opensim-tmux.socket";
char *Ttab = "SC";
char *Tcmd = "tmux -S";
char *webRoot = "/var/www/html";
char *URL = "fcgi-bin/sledjchisl.fcgi";
char *ToS = "Be good.";
int seshTimeOut = 30 * 60;
int idleTimeOut = 24 * 60 * 60;
int newbieTimeOut = 30;
float loadAverageInc = 0.5;
int simTimeOut = 45;
boolean DEBUG = TRUE;
qhashtbl_t *mimeTypes;
qlist_t *dbRequests;
// TODO - log to file. The problem is we don't know where to log until after we have loaded the configs, and before that we are spewing log messages.
// Now that we are using spawn-fcgi, all the logs are going to STDERR, which we can capture and write to a file.
// Unfortunately spawn-fcgi in deamon mode sends all the output to /dev/null or something.
// A better idea, when we spawn tmux or spawn-fcgi, capture STDERR, full log everything to that, filtered log to the tmux console (STDOUT).
// Then we can use STDOUT / STDIN to run the console stuff.
// https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences
char *logTypes[] =
{
"91;1;4", "CRITICAL", // red underlined
"31", "ERROR", // dark red
"93", "WARNING", // yellow
"36", "TIMEOUT", // cyan
"97;40", "INFO", // white
"90", "DEBUG", // grey
// VERBOSE? UNKNOWN? FATAL? SILENT? All from Android aparently.
"35", "debug", // magenta
"34", "timeout", // blue
};
#define DATE_TIME_LEN 42
void logMe(int v, char *format, ...)
{
va_list va, va2;
int len;
char *ret;
struct timeval tv;
time_t curtime;
char date[DATE_TIME_LEN];
va_start(va, format);
va_copy(va2, va);
// How long is it?
len = vsnprintf(0, 0, format, va);
len++;
va_end(va);
// Allocate and do the sprintf()
ret = xmalloc(len);
vsnprintf(ret, len, format, va2);
va_end(va2);
gettimeofday(&tv, NULL);
curtime = tv.tv_sec;
strftime(date, DATE_TIME_LEN, "(%Z %z) %F %T", localtime(&curtime));
v *= 2;
fprintf(stderr, "%s.%.6ld \e[%sm%-8s sledjchisl: %s\e[0m\n", date, tv.tv_usec, logTypes[v], logTypes[v + 1], ret);
free(ret);
}
#define C(...) logMe(0, __VA_ARGS__)
#define E(...) logMe(1, __VA_ARGS__)
#define W(...) logMe(2, __VA_ARGS__)
#define T(...) logMe(3, __VA_ARGS__)
#define I(...) logMe(4, __VA_ARGS__)
#define D(...) logMe(5, __VA_ARGS__)
#define d(...) logMe(6, __VA_ARGS__)
#define t(...) logMe(7, __VA_ARGS__)
static void addStrL(qlist_t *list, char *s)
{
list->addlast(list, s, strlen(s) + 1);
}
static char *getStrH(qhashtbl_t *hash, char *key)
{
char *ret = "", *t;
t = hash->getstr(hash, key, false);
if (NULL != t)
ret = t;
return ret;
}
char *myHMAC(char *in, boolean b64)
{
EVP_MD_CTX *mdctx = EVP_MD_CTX_create(); // Gets renamed to EVP_MD_CTX_new() in later versions.
unsigned char md_value[EVP_MAX_MD_SIZE];
unsigned int md_len;
EVP_DigestInit_ex(mdctx, EVP_sha512(), NULL); // EVP_sha3_512() isn't available until later versions.
EVP_DigestUpdate(mdctx, in, strlen(in));
EVP_DigestFinal_ex(mdctx, md_value, &md_len);
EVP_MD_CTX_destroy(mdctx); // Gets renamed to EVP_MD_CTX_free() in later versions.
if (b64)
return qB64_encode(md_value, md_len);
else
return qhex_encode(md_value, md_len);
}
char *myHMACkey(char *key, char *in, boolean b64)
{
unsigned char md_value[EVP_MAX_MD_SIZE];
unsigned int md_len;
unsigned char* digest = HMAC(EVP_sha512(), key, strlen(key), (unsigned char *) in, strlen(in), md_value, &md_len);
if (b64)
return qB64_encode(md_value, md_len);
else
return qhex_encode(md_value, md_len);
}
// In Lua 5.0 reference manual is a table traversal example at page 29.
void PrintTable(lua_State *L)
{
lua_pushnil(L);
while (lua_next(L, -2) != 0)
{
// Numbers can convert to strings, so check for numbers before checking for strings.
if (lua_isnumber(L, -1))
printf("%s = %f\n", lua_tostring(L, -2), lua_tonumber(L, -1));
else if (lua_isstring(L, -1))
printf("%s = '%s'\n", lua_tostring(L, -2), lua_tostring(L, -1));
else if (lua_istable(L, -1))
PrintTable(L);
lua_pop(L, 1);
}
}
int sendTmuxKeys(char *dest, char *keys)
{
int ret = 0, i;
char *c = xmprintf("%s %s/%s send-keys -t %s:%s '%s'", Tcmd, scRun, Tsocket, Tconsole, dest, keys);
i = system(c);
if (!WIFEXITED(i))
E("tmux send-keys command failed!");
free(c);
return ret;
}
int sendTmuxCmd(char *dest, char *cmd)
{
int ret = 0, i;
char *c = xmprintf("%s %s/%s send-keys -t %s:'%s' '%s' Enter", Tcmd, scRun, Tsocket, Tconsole, dest, cmd);
i = system(c);
if (!WIFEXITED(i))
E("tmux send-keys command failed!");
free(c);
return ret;
}
void waitTmuxText(char *dest, char *text)
{
int i;
char *c = xmprintf("sleep 5; %s %s/%s capture-pane -t %s:'%s' -p | grep -E '%s' 2>&1 > /dev/null", Tcmd, scRun, Tsocket, Tconsole, dest, text);
D("Waiting for '%s'.", text);
do
{
i = system(c);
if (!WIFEXITED(i))
{
E("tmux capture-pane command failed!");
break;
}
else if (0 == WEXITSTATUS(i))
break;
} while (1);
free(c);
}
float waitLoadAverage(float la, float extra, int timeout)
{
struct sysinfo info;
struct timespec timeOut;
float l;
int to = timeout;
T("Sleeping until load average is below %.02f (%.02f + %.02f) or for %d seconds.", la + extra, la, extra, timeout);
clock_gettime(CLOCK_MONOTONIC, &timeOut);
to += timeOut.tv_sec;
do
{
msleep(5000);
sysinfo(&info);
l = info.loads[0]/65536.0;
clock_gettime(CLOCK_MONOTONIC, &timeOut);
timeout -= 5;
t("Tick, load average is %.02f, countdown %d seconds.", l, timeout);
} while (((la + extra) < l) && (timeOut.tv_sec < to));
return l;
}
// Rob forget to do this, but at least he didn't declare it static.
struct dirtree *dirtree_handle_callback(struct dirtree *new, int (*callback)(struct dirtree *node));
typedef struct _simList simList;
struct _simList
{
int len, num;
char **sims;
};
static int filterSims(struct dirtree *node)
{
if (!node->parent) return DIRTREE_RECURSE | DIRTREE_SHUTUP;
if ((strncmp(node->name, "sim", 3) == 0) && ((strcmp(node->name, "sim_skeleton") != 0)))
{
simList *list = (simList *) node->parent->extra;
if ((list->num + 1) > list->len)
{
list->len = list->len + 1;
list->sims = xrealloc(list->sims, list->len * sizeof(char *));
}
list->sims[list->num] = xstrdup(node->name);
list->num++;
}
return 0;
}
simList *getSims()
{
simList *sims = xmalloc(sizeof(simList));
memset(sims, 0, sizeof(simList));
char *path = xmprintf("%s/config", scRoot);
struct dirtree *new = dirtree_add_node(0, path, 0);
new->extra = (long) sims;
dirtree_handle_callback(new, filterSims);
qsort(sims->sims, sims->num, sizeof(char *), qstrcmp);
free(path);
return sims;
}
static int filterInis(struct dirtree *node)
{
if (!node->parent) return DIRTREE_RECURSE | DIRTREE_SHUTUP;
int l = strlen(node->name);
if (strncmp(&(node->name[l - 4]), ".ini", 4) == 0)
{
strcpy((char *) node->parent->extra, xstrdup(node->name));
return DIRTREE_ABORT;
}
return 0;
}
char *getSimName(char *sim)
{
char *ret = NULL;
char *c = xmprintf("%s/config/%s", scRoot, sim);
struct dirtree *new = dirtree_add_node(0, c, 0);
free(c);
c = xzalloc(1024);
new->extra = (long) c;
dirtree_handle_callback(new, filterInis);
if ('\0' != c[0])
{
char *temp = NULL;
regex_t pat;
regmatch_t m[2];
long len;
int fd;
temp = xmprintf("%s/config/%s/%s", scRoot, sim, c);
fd = xopenro(temp);
free(temp);
xregcomp(&pat, "RegionName = \"(.+)\"", REG_EXTENDED);
do
{
// TODO - get_line() is slow, and wont help much with DOS and Mac line endings.
// gio_gets() isn't any faster really, but deals with DOS line endings at least.
temp = get_line(fd);
if (temp)
{
if (!regexec(&pat, temp, 2, m, 0))
{
// Return first parenthesized subexpression as string.
if (pat.re_nsub > 0)
{
ret = xmprintf("%.*s", (int) (m[1].rm_eo - m[1].rm_so), temp + m[1].rm_so);
break;
}
}
}
} while (temp);
xclose(fd);
}
free(c);
return ret;
}
// Expects either "simXX" or "ROBUST".
int checkSimIsRunning(char *sim)
{
int ret = 0;
struct stat st;
// Check if it's running.
char *path = xmprintf("%s/caches/%s.pid", scRoot, sim);
if (0 == stat(path, &st))
{
int fd, i;
char *pid = NULL;
// Double check if it's REALLY running.
if ((fd = xopenro(path)) == -1)
perror_msg("xopenro(%s)", path);
else
{
pid = get_line(fd);
if (NULL == pid)
perror_msg("get_line(%s)", path);
else
{
xclose(fd);
// I'd rather re-use the toysh command running stuff, since ps is a toy, but that's private.
// TODO - switch to toybox ps and rm.
free(path);
path = xmprintf("ps -p %s --no-headers -o comm", pid);
i = system(path);
if (WIFEXITED(i))
{
if (0 != WEXITSTATUS(i)) // No such pid.
{
free(path);
path = xmprintf("rm -f %s/caches/%s.pid", scRoot, sim);
d("%s", path);
i = system(path);
}
else
d("checkSimIsRunning(%s) has PID %s, which is actually running.", sim, pid);
}
}
}
}
// Now check if it's really really running. lol
free(path);
path = xmprintf("%s/caches/%s.pid", scRoot, sim);
if (0 == stat(path, &st))
{
D("checkSimIsRunning(%s) -> %s is really really running.", sim, path);
ret = 1;
}
else
D("checkSimIsRunning(%s) -> %s is not running.", sim, path);
free(path);
return ret;
}
static void PrintEnv(qgrow_t *reply, char *label, char **envp)
{
reply->addstrf(reply, "%s:
\n\n", label);
for ( ; *envp != NULL; envp++)
reply->addstrf(reply, "%s\n", *envp);
reply->addstr(reply, "
\n");
}
static void printEnv(char **envp)
{
for ( ; *envp != NULL; envp++)
D("%s", *envp);
}
typedef struct _rowData rowData;
struct _rowData
{
char **fieldNames;
qlist_t *rows;
};
static void dumpHash(qhashtbl_t *tbl)
{
qhashtbl_obj_t obj;
memset((void*)&obj, 0, sizeof(obj));
tbl->lock(tbl);
while(tbl->getnext(tbl, &obj, true) == true)
d("%s = %s", obj.name, (char *) obj.data);
tbl->unlock(tbl);
}
static void dumpArray(int d, char **ar)
{
int i = 0;
while (ar[i] != NULL)
{
d("%d %d %s", d, i, ar[i]);
i++;
}
}
/* How to deal with prepared SQL statements.
http://karlssonondatabases.blogspot.com/2010/07/prepared-statements-are-they-useful-or.html
https://blog.cotten.io/a-taste-of-mysql-in-c-87c5de84a31d?gi=ab3dd1425b29
https://raspberry-projects.com/pi/programming-in-c/databases-programming-in-c/mysql/accessing-the-database
IG and CG now both have sims connected to other grids, so some sort of
multi database solution would be good, then we can run the grid and the
external sims all in one.
Not sure if this'll work with Count(*).
---------------------------------------------
The complicated bit is the binds.
You are binding field values to C memory locations.
The parameters and returned fields need binds.
Mostly seems to be the value parts of the SQL statements.
I suspect most will be of the form -
... WHERE x=? and foo=?
INSERT INTO table VALUES (?,?,?)
UPDATE table SET x=?, foo=? WHERE id=?
A multi table update -
UPDATE items,month SET items.price=month.price WHERE items.id=month.id;
*/
static boolean dbConnect()
{
dbconn = mysql_real_connect(database,
getStrH(configs, "Data Source"),
getStrH(configs, "User ID"),
getStrH(configs, "Password"),
getStrH(configs, "Database"),
// 3036, "/var/run/mysqld/mysqld.sock",
0, NULL,
CLIENT_FOUND_ROWS | CLIENT_LOCAL_FILES | CLIENT_MULTI_STATEMENTS | CLIENT_MULTI_RESULTS);
if (NULL == dbconn)
{
E("mysql_real_connect() failed - %s", mysql_error(database));
return FALSE;
}
return TRUE;
}
// A general error function that checks for certain errors that mean we should try to connect to the server MariaDB again.
// https://mariadb.com/kb/en/mariadb-error-codes/
// 1129? 1152? 1184? 1218? 1927 3032? 4150?
// "server has gone away" isn't listed there, that's the one I was getting. Pffft
// It's 2006, https://dev.mysql.com/doc/refman/8.0/en/gone-away.html
// Though none of the mentioned reasons make sense here.
// Ah it could be "connection inactive for 8 hours".
// Which might be why OpenSim opens a new connection for EVERYTHING.
// TODO - see if I can either find out what the time out is, or just check and re open for each db thing.
// int mysql_ping(MYSQL * mysql); // https://mariadb.com/kb/en/mysql_ping/
// "If it has gone down, and global option reconnect is enabled an automatic reconnection is attempted."
// "Returns zero on success, nonzero if an error occured."
// "resources bundled to the connection (prepared statements, locks, temporary tables, ...) will be released." sigh
// Quick'n'dirty until this is properly event driven - have a cron job curl the stats page every hour.
static boolean dbCheckError(MYSQL *db, char *error, char *sql)
{
int e = mysql_errno(db);
E("MariaDB error %d - %s: %s\n%s", e, error, mysql_error(db), sql);
if (2006 == e)
return dbConnect();
return FALSE;
}
typedef struct _dbField dbField;
struct _dbField
{
char *name;
enum enum_field_types type;
unsigned long length;
unsigned int flags;
unsigned int decimals;
};
qlisttbl_t *dbGetFields(MYSQL *db, char *table)
{
static qhashtbl_t *tables = NULL;
if (NULL == tables) tables = qhashtbl(0, 0);
qlisttbl_t *ret = tables->get(tables, table, NULL, false);
if (NULL == ret)
{
// Seems the only way to get field metadata is to actually perform a SQL statement, then you get the field metadata for the result set.
// Chicken, meet egg, sorry you had to cross the road for this.
char *sql = xmprintf("SELECT * FROM %s LIMIT 0", table);
d("Getting field metadata for %s", table);
if (mysql_query(db, sql))
{
// E("MariaDB error %d - Query failed 0: %s\n%s", mysql_errno(db), mysql_error(db), sql);
if (dbCheckError(db, "Query failed 0", sql))
{
ret = dbGetFields(db, table);
free(sql);
return ret;
}
}
else
{
MYSQL_RES *res = mysql_store_result(db);
if (!res)
E("MariaDB error %d - Couldn't get results set from %s\n %s", mysql_errno(db), mysql_error(db), sql);
else
{
MYSQL_FIELD *fields = mysql_fetch_fields(res);
if (!fields)
E("MariaDB error %d - Failed fetching fields: %s", mysql_errno(db), mysql_error(db));
else
{
unsigned int i, num_fields = mysql_num_fields(res);
ret = qlisttbl(QLISTTBL_UNIQUE | QLISTTBL_LOOKUPFORWARD);
for (i = 0; i < num_fields; i++)
{
dbField *fld = xmalloc(sizeof(dbField));
fld->name = xstrdup(fields[i].name);
fld->type = fields[i].type;
fld->length = fields[i].length;
fld->flags = fields[i].flags;
fld->decimals = fields[i].decimals;
ret->put(ret, fld->name, fld, sizeof(*fld));
free(fld);
}
tables->put(tables, table, ret, sizeof(*ret));
}
mysql_free_result(res);
}
}
free(sql);
}
return ret;
}
void dbFreeFields(qlisttbl_t *flds)
{
qlisttbl_obj_t obj;
memset((void *) &obj, 0, sizeof(obj));
flds->lock(flds);
d("Freeing fields.");
while(flds->getnext(flds, &obj, NULL, false) == true)
{
dbField *fld = (dbField *) obj.data;
d("Freeing field %s", fld->name);
free(fld->name);
}
flds->unlock(flds);
flds->free(flds);
}
typedef struct _dbRequest dbRequest;
struct _dbRequest
{
MYSQL *db;
char *table, *join, *where, *order, *sql;
qlisttbl_t *flds;
int inCount, outCount, rowCount;
char **inParams, **outParams;
MYSQL_STMT *prep; // NOTE - executing it stores state in this.
MYSQL_BIND *inBind, *outBind;
rowData *rows;
my_ulonglong count;
boolean freeOutParams;
};
void dbDoSomething(dbRequest *req, boolean count, ...)
{
va_list ap;
struct timespec now, then;
int i, j;
MYSQL_RES *prepare_meta_result = NULL;
if (-1 == clock_gettime(CLOCK_REALTIME, &then))
perror_msg("Unable to get the time.");
va_start(ap, count);
if (NULL == req->prep)
{
req->flds = dbGetFields(req->db, req->table);
if (NULL == req->flds)
{
E("Unknown fields for table %s.", req->table);
goto end;
}
char *select = xmprintf("");
i = 0;
while (req->outParams[i] != NULL)
{
char *t = xmprintf("%s,%s", select, req->outParams[i]);
free(select);
select = t;
i++;
}
if (0 == i)
{
free(select);
if (count)
select = xmprintf(",Count(*)");
else
select = xmprintf(",*");
}
if (NULL == req->join)
req->join = "";
if (req->where)
req->sql = xmprintf("SELECT %s FROM %s %s WHERE %s", &select[1], req->table, req->join, req->where);
else
req->sql = xmprintf("SELECT %s FROM %s", &select[1], req->table, req->join);
free(select);
if (req->order)
{
char *t = xmprintf("%s ORDER BY %s", req->sql, req->order);
free(req->sql);
req->sql = t;
}
d("New SQL statement - %s", req->sql);
// prepare statement with the other fields
req->prep = mysql_stmt_init(req->db);
if (NULL == req->prep)
{
E("Statement prepare init failed: %s\n", mysql_stmt_error(req->prep));
goto end;
}
if (mysql_stmt_prepare(req->prep, req->sql, strlen(req->sql)))
{
E("Statement prepare failed: %s\n", mysql_stmt_error(req->prep));
goto end;
}
// setup the bind stuff for any "?" parameters in the SQL.
req->inCount = mysql_stmt_param_count(req->prep);
i = 0;
while (req->inParams[i] != NULL)
i++;
if (i != req->inCount)
{
E("In parameters count don't match %d != %d for - %s", i, req->inCount, req->sql);
goto freeIt;
}
req->inBind = xzalloc(i * sizeof(MYSQL_BIND));
W("Allocated %d %d inBinds for %s", i, req->inCount, req->sql);
for (i = 0; i < req->inCount; i++)
{
dbField *fld = req->flds->get(req->flds, req->inParams[i], NULL, false);
if (NULL == fld)
{
E("Unknown input field %d %s.%s for - %s", i, req->table, req->inParams[i], req->sql);
goto freeIt;
}
else
{
// https://blog.cotten.io/a-taste-of-mysql-in-c-87c5de84a31d?gi=ab3dd1425b29
// For some gotchas about all of this binding bit.
req->inBind[i].buffer_type = fld->type;
req->inBind[i].buffer = xzalloc(fld->length) + 1; // Note the + 1 is for string types, and a waste for the rest.
req->inBind[i].buffer_length = fld->length;
switch(fld->type)
{
case MYSQL_TYPE_TINY:
{
break;
}
case MYSQL_TYPE_SHORT:
{
req->inBind[i].is_unsigned = FALSE;
break;
}
case MYSQL_TYPE_INT24:
{
req->inBind[i].is_unsigned = FALSE;
break;
}
case MYSQL_TYPE_LONG:
{
req->inBind[i].is_unsigned = FALSE;
break;
}
case MYSQL_TYPE_LONGLONG:
{
req->inBind[i].is_unsigned = FALSE;
break;
}
case MYSQL_TYPE_FLOAT:
{
break;
}
case MYSQL_TYPE_DOUBLE:
{
break;
}
case MYSQL_TYPE_NEWDECIMAL:
{
break;
}
case MYSQL_TYPE_TIME:
case MYSQL_TYPE_DATE:
case MYSQL_TYPE_DATETIME:
case MYSQL_TYPE_TIMESTAMP:
{
break;
}
case MYSQL_TYPE_STRING:
case MYSQL_TYPE_VAR_STRING:
{
req->inBind[i].is_null = xzalloc(sizeof(my_bool));
req->inBind[i].length = xzalloc(sizeof(unsigned long));
break;
}
case MYSQL_TYPE_TINY_BLOB:
case MYSQL_TYPE_BLOB:
case MYSQL_TYPE_MEDIUM_BLOB:
case MYSQL_TYPE_LONG_BLOB:
{
req->inBind[i].is_null = xzalloc(sizeof(my_bool));
break;
}
case MYSQL_TYPE_BIT:
{
req->inBind[i].is_null = xzalloc(sizeof(my_bool));
break;
}
case MYSQL_TYPE_NULL:
{
break;
}
}
}
}
// TODO - if this is not a count, setup result bind paramateres, may be needed for counts as well.
prepare_meta_result = mysql_stmt_result_metadata(req->prep);
if (!prepare_meta_result)
{
D(" mysql_stmt_result_metadata() error %d, returned no meta information - %s\n", mysql_stmt_errno(req->prep), mysql_stmt_error(req->prep));
goto freeIt;
}
if (count)
{
I("count!!!!!!!!!!!!!!!!");
}
else
{
req->outCount = mysql_num_fields(prepare_meta_result);
i = 0;
while (req->outParams[i] != NULL)
i++;
if (0 == i) // Passing in {NULL} as req->outParams means "return all of them".
{
req->outParams = xzalloc((req->outCount + 1) * sizeof(char *));
req->freeOutParams = TRUE;
qlisttbl_obj_t obj;
memset((void*)&obj, 0, sizeof(obj));
req->flds->lock(req->flds);
while (req->flds->getnext(req->flds, &obj, NULL, false) == true)
{
dbField *fld = (dbField *) obj.data;
req->outParams[i] = fld->name;
i++;
}
req->outParams[i] = NULL;
req->flds->unlock(req->flds);
}
if (i != req->outCount)
{
E("Out parameters count doesn't match %d != %d foqr - %s", i, req->outCount, req->sql);
goto freeIt;
}
req->outBind = xzalloc(i * sizeof(MYSQL_BIND));
W("Allocated %d %d outBinds for %s", i, req->outCount, req->sql);
for (i = 0; i < req->outCount; i++)
{
dbField *fld = req->flds->get(req->flds, req->outParams[i], NULL, false);
if (NULL == fld)
{
E("Unknown output field %d %s.%s foqr - %s", i, req->table, req->outParams[i], req->sql);
goto freeIt;
}
else
{
// https://blog.cotten.io/a-taste-of-mysql-in-c-87c5de84a31d?gi=ab3dd1425b29
// For some gotchas about all of this binding bit.
req->outBind[i].buffer_type = fld->type;
req->outBind[i].buffer = xzalloc(fld->length + 1); // Note the + 1 is for string types, and a waste for the rest.
req->outBind[i].buffer_length = fld->length;
req->outBind[i].error = xzalloc(sizeof(my_bool));
req->outBind[i].is_null = xzalloc(sizeof(my_bool));
switch(fld->type)
{
case MYSQL_TYPE_TINY:
{
//d("TINY %d %s %d", i, fld->name, req->outBind[i].buffer_length);
break;
}
case MYSQL_TYPE_SHORT:
{
//d("SHORT %s %d", fld->name, req->outBind[i].buffer_length);
req->outBind[i].is_unsigned = FALSE;
break;
}
case MYSQL_TYPE_INT24:
{
//d("INT24 %s %d", fld->name, req->outBind[i].buffer_length);
req->outBind[i].is_unsigned = FALSE;
break;
}
case MYSQL_TYPE_LONG:
{
//d("LONG %d %s %d", i, fld->name, req->outBind[i].buffer_length);
req->outBind[i].is_unsigned = FALSE;
break;
}
case MYSQL_TYPE_LONGLONG:
{
//d("LONG LONG %s %d", fld->name, req->outBind[i].buffer_length);
req->outBind[i].is_unsigned = FALSE;
break;
}
case MYSQL_TYPE_FLOAT:
{
//d("FLOAT %s %d", fld->name, req->outBind[i].buffer_length);
break;
}
case MYSQL_TYPE_DOUBLE:
{
//d("DOUBLE %s %d", fld->name, req->outBind[i].buffer_length);
break;
}
case MYSQL_TYPE_NEWDECIMAL:
{
//d("NEWDECIMAL %s %d", fld->name, req->outBind[i].buffer_length);
break;
}
case MYSQL_TYPE_TIME:
case MYSQL_TYPE_DATE:
case MYSQL_TYPE_DATETIME:
case MYSQL_TYPE_TIMESTAMP:
{
//d("DATETIME %s %d", fld->name, req->outBind[i].buffer_length);
break;
}
case MYSQL_TYPE_STRING:
case MYSQL_TYPE_VAR_STRING:
{
//d("STRING %s %d", fld->name, req->outBind[i].buffer_length);
req->outBind[i].length = xzalloc(sizeof(unsigned long));
break;
}
case MYSQL_TYPE_TINY_BLOB:
case MYSQL_TYPE_BLOB:
case MYSQL_TYPE_MEDIUM_BLOB:
case MYSQL_TYPE_LONG_BLOB:
{
//d("BLOB %s %d", fld->name, req->outBind[i].buffer_length);
break;
}
case MYSQL_TYPE_BIT:
{
//d("BIT %s %d", fld->name, req->outBind[i].buffer_length);
break;
}
case MYSQL_TYPE_NULL:
{
//d("NULL %s %d", fld->name, req->outBind[i].buffer_length);
break;
}
}
}
}
if (mysql_stmt_bind_result(req->prep, req->outBind))
{
E("Bind failed error %d.", mysql_stmt_errno(req->prep));
goto freeIt;
}
}
}
//d("input bind for %s", req->sql);
for (i = 0; i < req->inCount; i++)
{
dbField *fld = req->flds->get(req->flds, req->inParams[i], NULL, false);
if (NULL == fld)
{
E("Unknown input field %s.%s for - %s", req->table, req->inParams[i], req->sql);
goto freeIt;
}
else
{
switch(fld->type)
{
case MYSQL_TYPE_TINY:
{
int c = va_arg(ap, int);
signed char d = (signed char) c;
memcpy(&d, req->inBind[i].buffer, (size_t) fld->length);
break;
}
case MYSQL_TYPE_SHORT:
{
int c = va_arg(ap, int);
short int d = (short int) c;
memcpy(&d, req->inBind[i].buffer, (size_t) fld->length);
break;
}
case MYSQL_TYPE_INT24:
{
int d = va_arg(ap, int);
memcpy(&d, req->inBind[i].buffer, (size_t) fld->length);
break;
}
case MYSQL_TYPE_LONG:
{
long d = va_arg(ap, long);
memcpy(&d, req->inBind[i].buffer, (size_t) fld->length);
break;
}
case MYSQL_TYPE_LONGLONG:
{
long long int d = va_arg(ap, long long int);
memcpy(&d, req->inBind[i].buffer, (size_t) fld->length);
break;
}
case MYSQL_TYPE_FLOAT:
{
double c = va_arg(ap, double);
float d = (float) c;
memcpy(&d, req->inBind[i].buffer, (size_t) fld->length);
break;
}
case MYSQL_TYPE_DOUBLE:
{
double d = va_arg(ap, double);
memcpy(&d, req->inBind[i].buffer, (size_t) fld->length);
break;
}
case MYSQL_TYPE_NEWDECIMAL:
{
break;
}
case MYSQL_TYPE_TIME:
case MYSQL_TYPE_DATE:
case MYSQL_TYPE_DATETIME:
case MYSQL_TYPE_TIMESTAMP:
{
MYSQL_TIME d = va_arg(ap, MYSQL_TIME);
memcpy(&d, req->inBind[i].buffer, (size_t) fld->length);
break;
}
case MYSQL_TYPE_STRING:
case MYSQL_TYPE_VAR_STRING:
{
char *d = va_arg(ap, char *);
unsigned long l = strlen(d);
if (l > fld->length)
l = fld->length;
*(req->inBind[i].length) = l;
strncpy(req->inBind[i].buffer, d, (size_t) l);
((char *) req->inBind[i].buffer)[l] = '\0';
break;
}
case MYSQL_TYPE_TINY_BLOB:
case MYSQL_TYPE_BLOB:
case MYSQL_TYPE_MEDIUM_BLOB:
case MYSQL_TYPE_LONG_BLOB:
{
break;
}
case MYSQL_TYPE_BIT:
{
break;
}
case MYSQL_TYPE_NULL:
{
break;
}
}
}
}
if (mysql_stmt_bind_param(req->prep, req->inBind))
{
E("Bind failed error %d.", mysql_stmt_errno(req->prep));
goto freeIt;
}
d("Execute %s", req->sql);
// do the prepared statement req->prep.
if (mysql_stmt_execute(req->prep))
{
E("Statement execute failed %d: %s\n", mysql_stmt_errno(req->prep), mysql_stmt_error(req->prep));
goto freeIt;
}
int fs = mysql_stmt_field_count(req->prep);
// stuff results back into req.
if (NULL != req->outBind)
{
req->rows = xmalloc(sizeof(rowData));
req->rows->fieldNames = xzalloc(fs * sizeof(char *));
if (mysql_stmt_store_result(req->prep))
{
E(" mysql_stmt_store_result() failed %d: %s", mysql_stmt_errno(req->prep), mysql_stmt_error(req->prep));
goto freeIt;
}
req->rowCount = mysql_stmt_num_rows(req->prep);
if (0 == req->rowCount)
D("No rows returned from : %s\n", req->sql);
else
D("%d rows of %d fields returned from : %s\n", req->rowCount, fs, req->sql);
req->rows->rows = qlist(0);
while (MYSQL_NO_DATA != mysql_stmt_fetch(req->prep))
{
qhashtbl_t *flds = qhashtbl(0, 0);
for (i = 0; i < req->outCount; i++)
{
dbField *fld = req->flds->get(req->flds, req->outParams[i], NULL, false);
req->rows->fieldNames[i] = fld->name;
if (!*(req->outBind[i].is_null))
{
//d("2.8 %s", req->rows->fieldNames[i]);
flds->put(flds, req->rows->fieldNames[i], req->outBind[i].buffer, req->outBind[i].buffer_length);
switch(fld->type)
{
case MYSQL_TYPE_TINY:
{
break;
}
case MYSQL_TYPE_SHORT:
{
char *t = xmprintf("%d", (int) *((int *) req->outBind[i].buffer));
flds->putstr(flds, req->rows->fieldNames[i], t);
free(t);
break;
}
case MYSQL_TYPE_INT24:
{
char *t = xmprintf("%d", (int) *((int *) req->outBind[i].buffer));
flds->putstr(flds, req->rows->fieldNames[i], t);
free(t);
break;
}
case MYSQL_TYPE_LONG:
{
if (NULL == req->outBind[i].buffer)
{
E("Field %d %s is NULL", i, fld->name);
goto freeIt;
}
char *t = xmprintf("%d", (int) *((int *) (req->outBind[i].buffer)));
//d("Setting %i %s %s", i, fld->name, t);
flds->putstr(flds, req->rows->fieldNames[i], t);
free(t);
break;
}
case MYSQL_TYPE_LONGLONG:
{
char *t = xmprintf("%d", (int) *((int *) req->outBind[i].buffer));
flds->putstr(flds, req->rows->fieldNames[i], t);
free(t);
break;
}
case MYSQL_TYPE_FLOAT:
{
break;
}
case MYSQL_TYPE_DOUBLE:
{
break;
}
case MYSQL_TYPE_NEWDECIMAL:
{
break;
}
case MYSQL_TYPE_TIME:
case MYSQL_TYPE_DATE:
case MYSQL_TYPE_DATETIME:
case MYSQL_TYPE_TIMESTAMP:
{
break;
}
case MYSQL_TYPE_STRING:
case MYSQL_TYPE_VAR_STRING:
{
break;
}
case MYSQL_TYPE_TINY_BLOB:
case MYSQL_TYPE_BLOB:
case MYSQL_TYPE_MEDIUM_BLOB:
case MYSQL_TYPE_LONG_BLOB:
{
break;
}
case MYSQL_TYPE_BIT:
{
break;
}
case MYSQL_TYPE_NULL:
{
break;
}
}
}
else
D("Not setting data %s, coz it's NULL", fld->name);
}
req->rows->rows->addlast(req->rows->rows, flds, sizeof(qhashtbl_t));
free(flds);
}
}
freeIt:
if (prepare_meta_result)
mysql_free_result(prepare_meta_result);
if (mysql_stmt_free_result(req->prep))
E("Statement result freeing failed %d: %s\n", mysql_stmt_errno(req->prep), mysql_stmt_error(req->prep));
end:
va_end(ap);
if (-1 == clock_gettime(CLOCK_REALTIME, &now))
perror_msg("Unable to get the time.");
double n = (now.tv_sec * 1000000000.0) + now.tv_nsec;
double t = (then.tv_sec * 1000000000.0) + then.tv_nsec;
T("dbDoSomething(%s) took %lf seconds", req->sql, (n - t) / 1000000000.0);
return;
}
// Copy the SQL results into the request structure.
void dbPull(reqData *Rd, char *table, rowData *rows)
{
char *where;
qhashtbl_t *me = rows->rows->popfirst(rows->rows, NULL);
qhashtbl_obj_t obj;
memset((void*)&obj, 0, sizeof(obj));
me->lock(me);
while(me->getnext(me, &obj, false) == true)
{
where = xmprintf("%s.%s", table, obj.name);
d("dbPull(Rd->database) %s = %s", where, (char *) obj.data);
Rd->database->putstr(Rd->database, where, (char *) obj.data);
// me->remove(me, obj.name);
free(where);
}
me->unlock(me);
me->free(me);
}
void dbFreeRequest(dbRequest *req)
{
int i;
D("Cleaning up prepared database request %s - %s %d %d", req->table, req->where, req->outCount, req->inCount);
if (NULL != req->outBind)
{
d("Free outBind");
for (i = 0; i < req->outCount; i++)
{
d("Free outBind %d %s", i, req->sql);
if (NULL != req->outBind[i].buffer) free(req->outBind[i].buffer);
if (NULL != req->outBind[i].length) free(req->outBind[i].length);
if (NULL != req->outBind[i].error) free(req->outBind[i].error);
if (NULL != req->outBind[i].is_null) free(req->outBind[i].is_null);
}
free(req->outBind);
}
if (NULL != req->inBind)
{
d("Free inBind");
for (i = 0; i < req->inCount; i++)
{
d("Free inBind %d %s", i, req->sql);
// TODO - this leaks for some bizare reason.
if (NULL != req->inBind[i].buffer) free(req->inBind[i].buffer);
if (NULL != req->inBind[i].length) free(req->inBind[i].length);
if (NULL != req->inBind[i].error) free(req->inBind[i].error);
if (NULL != req->inBind[i].is_null) free(req->inBind[i].is_null);
}
free(req->inBind);
}
if (req->freeOutParams) free(req->outParams);
if (NULL != req->sql) free(req->sql);
if (NULL != req->prep)
{
if (0 != mysql_stmt_close(req->prep))
C("Unable to close the prepared statement!");
free(req->prep);
}
}
my_ulonglong dbCount(MYSQL *db, char *table, char *where)
{
my_ulonglong ret = 0;
char *sql;
struct timespec now, then;
if (-1 == clock_gettime(CLOCK_REALTIME, &then))
perror_msg("Unable to get the time.");
if (where)
sql = xmprintf("SELECT Count(*) FROM %s WHERE %s", table, where);
else
sql = xmprintf("SELECT Count(*) FROM %s", table);
if (mysql_query(db, sql))
{
// E("MariaDB error %d - Query failed 1: %s", mysql_errno(db), mysql_error(db));
if (dbCheckError(db, "Query failed 1", sql))
{
ret = dbCount(db, table, where);
free(sql);
return ret;
}
}
else
{
MYSQL_RES *result = mysql_store_result(db);
if (!result)
E("Couldn't get results set from %s\n: %s", sql, mysql_error(db));
else
{
MYSQL_ROW row = mysql_fetch_row(result);
if (!row)
E("MariaDB error %d - Couldn't get row from %s\n: %s", mysql_errno(db), sql, mysql_error(db));
else
ret = atoll(row[0]);
mysql_free_result(result);
}
}
if (-1 == clock_gettime(CLOCK_REALTIME, &now))
perror_msg("Unable to get the time.");
double n = (now.tv_sec * 1000000000.0) + now.tv_nsec;
double t = (then.tv_sec * 1000000000.0) + then.tv_nsec;
T("dbCount(%s) took %lf seconds", sql, (n - t) / 1000000000.0);
free(sql);
return ret;
}
my_ulonglong dbCountJoin(MYSQL *db, char *table, char *select, char *join, char *where)
{
my_ulonglong ret = 0;
char *sql;
struct timespec now, then;
if (-1 == clock_gettime(CLOCK_REALTIME, &then))
perror_msg("Unable to get the time.");
if (NULL == select)
select = "*";
if (NULL == join)
join = "";
if (where)
sql = xmprintf("SELECT %s FROM %s %s WHERE %s", select, table, join, where);
else
sql = xmprintf("SELECT %s FROM %s", select, table, join);
if (mysql_query(db, sql))
{
// E("MariaDB error %d - Query failed 2: %s", mysql_errno(db), mysql_error(db));
if (dbCheckError(db, "Query failed 2", sql))
{
ret = dbCountJoin(db, table, select, join, where);
free(sql);
return ret;
}
}
else
{
MYSQL_RES *result = mysql_store_result(db);
if (!result)
E("MariaDB error %d - Couldn't get results set from %s\n: %s", mysql_errno(db), sql, mysql_error(db));
else
ret = mysql_num_rows(result);
mysql_free_result(result);
}
if (-1 == clock_gettime(CLOCK_REALTIME, &now))
perror_msg("Unable to get the time.");
double n = (now.tv_sec * 1000000000.0) + now.tv_nsec;
double t = (then.tv_sec * 1000000000.0) + then.tv_nsec;
T("dbCointJoin(%s) took %lf seconds", sql, (n - t) / 1000000000.0);
free(sql);
return ret;
}
MYSQL_RES *dbSelect(MYSQL *db, char *table, char *select, char *join, char *where, char *order)
{
MYSQL_RES *ret = NULL;
char *sql;
struct timespec now, then;
if (-1 == clock_gettime(CLOCK_REALTIME, &then))
perror_msg("Unable to get the time.");
if (NULL == select)
select = "*";
if (NULL == join)
join = "";
if (where)
sql = xmprintf("SELECT %s FROM %s %s WHERE %s", select, table, join, where);
else
sql = xmprintf("SELECT %s FROM %s", select, table, join);
if (order)
{
char *t = xmprintf("%s ORDER BY %s", sql, order);
free(sql);
sql = t;
}
if (mysql_query(db, sql))
E("MariaDB error %d - Query failed 3: %s\n%s", mysql_errno(db), mysql_error(db), sql);
else
{
ret = mysql_store_result(db);
if (!ret)
E("MariaDB error %d - Couldn't get results set from %s\n %s", mysql_errno(db), mysql_error(db), sql);
}
if (-1 == clock_gettime(CLOCK_REALTIME, &now))
perror_msg("Unable to get the time.");
double n = (now.tv_sec * 1000000000.0) + now.tv_nsec;
double t = (then.tv_sec * 1000000000.0) + then.tv_nsec;
T("dbSelect(%s) took %lf seconds", sql, (n - t) / 1000000000.0);
free(sql);
return ret;
}
void replaceStr(qhashtbl_t *ssi, char *key, char *value)
{
ssi->putstr(ssi, key, value);
}
void replaceLong(qhashtbl_t *ssi, char *key, my_ulonglong value)
{
char *tmp = xmprintf("%lu", value);
replaceStr(ssi, key, tmp);
free(tmp);
}
float timeDiff(struct timeval *now, struct timeval *then)
{
if (0 == gettimeofday(now, NULL))
{
struct timeval thisTime = { 0, 0 };
double result = 0.0;
thisTime.tv_sec = now->tv_sec;
thisTime.tv_usec = now->tv_usec;
if (thisTime.tv_usec < then->tv_usec)
{
thisTime.tv_sec--;
thisTime.tv_usec += 1000000;
}
thisTime.tv_usec -= then->tv_usec;
thisTime.tv_sec -= then->tv_sec;
result = ((double) thisTime.tv_usec) / ((double) 1000000.0);
result += thisTime.tv_sec;
return result;
}
return 0.0;
}
gridStats *getStats(MYSQL *db, gridStats *stats)
{
if (NULL == stats)
{
stats = xmalloc(sizeof(gridStats));
stats->next = 30;
gettimeofday(&(stats->last), NULL);
stats->stats = qhashtbl(0, 0);
stats->stats->putstr(stats->stats, "version", "SledjChisl FCGI Dev 0.1");
stats->stats->putstr(stats->stats, "grid", "my grid");
stats->stats->putstr(stats->stats, "uri", "http://localhost:8002/");
if (checkSimIsRunning("ROBUST"))
stats->stats->putstr(stats->stats, "gridOnline", "online");
else
stats->stats->putstr(stats->stats, "gridOnline", "offline");
}
else
{
static struct timeval thisTime;
if (stats->next > timeDiff(&thisTime, &(stats->last)))
return stats;
}
I("Getting fresh grid stats.");
if (checkSimIsRunning("ROBUST"))
replaceStr(stats->stats, "gridOnline", "online");
else
replaceStr(stats->stats, "gridOnline", "offline");
if (db)
{
char *tmp;
my_ulonglong locIn = dbCount(db, "Presence", "RegionID != '00000000-0000-0000-0000-000000000000'"); // Locals online but not HGing, and HGers in world.
my_ulonglong HGin = dbCount(db, "Presence", "UserID NOT IN (SELECT PrincipalID FROM UserAccounts)"); // HGers in world.
// Collect stats about members.
replaceLong(stats->stats, "hgers", HGin);
replaceLong(stats->stats, "inworld", locIn - HGin);
tmp = xmprintf("GridExternalName != '%s'", stats->stats->getstr(stats->stats, "uri", false));
replaceLong(stats->stats, "outworld", dbCount(db, "hg_traveling_data", tmp));
free(tmp);
replaceLong(stats->stats, "members", dbCount(db, "UserAccounts", NULL));
// Count local and HG visitors for the last 30 and 60 days.
locIn = dbCountJoin(db, "GridUser", "GridUser.UserID", "INNER JOIN UserAccounts ON GridUser.UserID = UserAccounts.PrincipalID",
"Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 2419200))");
HGin = dbCount(db, "GridUser", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 2419200))");
replaceLong(stats->stats, "locDay30", locIn);
replaceLong(stats->stats, "day30", HGin);
replaceLong(stats->stats, "HGday30", HGin - locIn);
locIn = dbCountJoin(db, "GridUser", "GridUser.UserID", "INNER JOIN UserAccounts ON GridUser.UserID = UserAccounts.PrincipalID",
"Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 4838400))");
HGin = dbCount(db, "GridUser", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 4838400))");
replaceLong(stats->stats, "locDay60", locIn);
replaceLong(stats->stats, "day60", HGin);
replaceLong(stats->stats, "HGday60", HGin - locIn);
// Collect stats about sims.
replaceLong(stats->stats, "sims", dbCount(db, "regions", NULL));
replaceLong(stats->stats, "onlineSims", dbCount(db, "regions", "sizeX != 0"));
replaceLong(stats->stats, "varRegions", dbCount(db, "regions", "sizeX > 256 or sizeY > 256"));
replaceLong(stats->stats, "singleSims", dbCount(db, "regions", "sizeX = 256 and sizeY = 256"));
replaceLong(stats->stats, "offlineSims", dbCount(db, "regions", "sizeX = 0"));
// Calculate total size of all regions.
my_ulonglong simSize = 0;
static dbRequest *rgnSizes = NULL;
if (NULL == rgnSizes)
{
static char *szi[] = {NULL};
static char *szo[] = {"sizeX", "sizeY", NULL};
rgnSizes = xzalloc(sizeof(dbRequest));
rgnSizes->db = db;
rgnSizes->table = "regions";
rgnSizes->inParams = szi;
rgnSizes->outParams = szo;
rgnSizes->where = "sizeX != 0";
dbRequests->addfirst(dbRequests, rgnSizes, sizeof(*rgnSizes));
}
dbDoSomething(rgnSizes, FALSE);
rowData *rows = rgnSizes->rows;
qhashtbl_t *row;
while (NULL != (row = rows->rows->getat(rows->rows, 0, NULL, true)))
{
my_ulonglong x = 0, y = 0;
tmp = row->getstr(row, "sizeX", false);
if (NULL == tmp)
E("No regions.sizeX!");
else
x = atoll(tmp);
tmp = row->getstr(row, "sizeY", false);
if (NULL == tmp)
E("No regions.sizeY!");
else
y = atoll(tmp);
simSize += x * y;
// TODO - I can't win. valgrind complains that either something is being freed twice, or not freed at all, no matter what I do.
// This seems to keep the memory loss down to a minimum.
row->free(row);
rows->rows->removefirst(rows->rows);
}
free(rows->fieldNames);
rows->rows->free(rows->rows);
free(rows);
tmp = xmprintf("%lu", simSize);
stats->stats->putstr(stats->stats, "simsSize", tmp);
free(tmp);
gettimeofday(&(stats->last), NULL);
}
return stats;
}
qhashtbl_t *toknize(char *text, char *delims)
{
qhashtbl_t *ret = qhashtbl(0, 0);
if (NULL == text)
return ret;
char *txt = xstrdup(text), *token, dlm = ' ', *key, *val = NULL;
int offset = 0;
while((token = qstrtok(txt, delims, &dlm, &offset)) != NULL)
{
if (delims[0] == dlm)
{
key = token;
val = &txt[offset];
}
else if (delims[1] == dlm)
{
ret->putstr(ret, qstrtrim_head(key), token);
d(" %s = %s", qstrtrim_head(key), val);
val = NULL;
}
}
if (NULL != val)
{
ret->putstr(ret, qstrtrim_head(key), val);
d(" %s = %s", qstrtrim_head(key), val);
}
free(txt);
return ret;
}
void santize(qhashtbl_t *tbl, bool decode)
{
qhashtbl_obj_t obj;
memset((void*)&obj, 0, sizeof(obj));
tbl->lock(tbl);
while(tbl->getnext(tbl, &obj, false) == true)
{
char *n = obj.name, *o = (char *) obj.data;
if (decode)
qurl_decode(o);
// if ((strcmp(n, "password") != 0) && (strcmp(n, "psswd") != 0))
{
// Poor mans Bobby Tables protection.
// TODO - make this reversable, especially so these things can be used in aboutMe, and come out the other end unscathed.
// qurl_encode doesn't handle \, but does the rest.
// So that means don't qurl_decode it, and encode \\.
// But then I have to qurl_decode everwhere.
o = qstrreplace("tr", o, "'", "_");
o = qstrreplace("tr", o, "\"", "_");
o = qstrreplace("tr", o, ";", "_");
o = qstrreplace("tr", o, "(", "_");
o = qstrreplace("tr", o, ")", "_");
}
tbl->putstr(tbl, n, o);
}
tbl->unlock(tbl);
}
/*
char *unsantize(char *str)
{
char *ret = qurl_decode(xstrdup(str));
return ret;
}
*/
void outize(qgrow_t *reply, qhashtbl_t *tbl, char *label)
{
reply->addstrf(reply, "%s:
\n\n", label);
qhashtbl_obj_t obj;
memset((void*)&obj, 0, sizeof(obj));
tbl->lock(tbl);
while(tbl->getnext(tbl, &obj, false) == true)
reply->addstrf(reply, " %s = %s\n", obj.name, (char *) obj.data);
tbl->unlock(tbl);
reply->addstr(reply, "
\n");
}
char *displayPrep(char *str)
{
char *ret = xstrdup(str), *t;
qurl_decode(ret);
t = qstrreplace("tn", ret, "<", "<");
free(ret);
ret = NULL;
if (NULL != t)
{
ret = qstrreplace("tn", t, ">", ">");
if (NULL == ret)
ret = t;
else
free(t);
}
if (NULL == ret)
ret = xstrdup(str);
return ret;
}
char *encodeSlash(char *str)
{
char *ret = xstrdup(str), *t = qstrreplace("tn", str, "\\", "%5c");
if (NULL != t)
{
free(ret);
ret = t;
}
return ret;
}
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
enum cookieSame
{
CS_NOT,
CS_STRICT,
CS_LAX, // Apparently the default set by browsers these days.
CS_NONE
};
typedef struct _cookie cookie;
struct _cookie
{
char *value, *domain, *path;
// char *expires; // Use maxAge instead, it's far simpler to figure out.
int maxAge;
boolean secure, httpOnly;
enum cookieSame site;
};
cookie *setCookie(reqData *Rd, char *cki, char *value)
{
cookie *ret = xzalloc(sizeof(cookie));
char *cook = xstrdup(cki);
int l, i;
// Validate this, as there is a limited set of characters allowed.
qstrreplace("tr", cook, "()<>@,;:\\\"/[]?={} \t", "_");
l = strlen(cook);
for (i = 0; i < l; i++)
{
if (iscntrl(cook[i]) != 0)
cook[i] = '_';
}
l = strlen(value);
if (0 != l)
ret->value = qurl_encode(value, l);
else
// TODO - I'm doing something crazy again, this isn't crashing when I try to free it. Sometimes. Heisenbug?
ret->value = "";
ret->httpOnly = TRUE;
ret->site = CS_STRICT;
ret->secure = TRUE;
ret->path = getStrH(Rd->headers, "SCRIPT_NAME");
Rd->Rcookies->put(Rd->Rcookies, cook, ret, sizeof(cookie));
free(ret);
ret = Rd->Rcookies->get(Rd->Rcookies, cook, NULL, false);
free(cook);
return ret;
}
char *getCookie(qhashtbl_t *cookies, char *cki)
{
char *ret = NULL;
cookie *ck = (cookie *) cookies->get(cookies, cki, NULL, false);
if (NULL != ck)
ret = ck->value;
return ret;
}
void outizeCookie(qgrow_t *reply, qhashtbl_t *tbl, char *label)
{
reply->addstrf(reply, "%s:
\n\n", label);
qhashtbl_obj_t obj;
memset((void*)&obj, 0, sizeof(obj));
tbl->lock(tbl);
while(tbl->getnext(tbl, &obj, false) == true)
reply->addstrf(reply, " %s = %s\n", obj.name, ((cookie *) obj.data)->value);
tbl->unlock(tbl);
reply->addstr(reply, "
\n");
}
enum fragmentType
{
FT_TEXT,
FT_PARAM,
FT_LUA
};
typedef struct _fragment fragment;
struct _fragment
{
enum fragmentType type;
int length;
char *text;
};
static void HTMLdebug(qgrow_t *reply)
{
reply->addstrf(reply,
" DEBUG log
\n"
" \n"
"
%s | ", s); } reply->addstr(reply, "|
---|---|
%s&%s=%s\">%s%s | ", address, id, t0, t0, addrend); else reply->addstrf(reply, "%s | ", t0); } } reply->addstr(reply, "
"); reply->addstrf(reply, "", title); // reply->addstrf(reply, "> %s ⛝ □ 🞐 🞎 🞎 ☐ ▣️ ◉ ○ ", title); reply->addstrf(reply, "
\n"); } static void HTMLtextArea(qgrow_t *reply, char *name, char *title, int rows, int cols, int min, int max, char *holder, char *comp, char *spell, char *wrap, char *val, boolean required, boolean readOnly) { reply->addstrf(reply, " \n", val); else reply->addstrf(reply, ">\n"); } static void HTMLtext(qgrow_t *reply, char *type, char *title, char *name, char *val, int size, int max, boolean required) { reply->addstrf(reply, " \n"); } static void HTMLselect(qgrow_t *reply, char *title, char *name) { if (NULL == title) reply->addstrf(reply, " "); } static void HTMLoption(qgrow_t *reply, char *title, boolean selected) { char *sel = ""; if (selected) sel = " selected"; reply->addstrf(reply, " \n", title, sel, title); } static void HTMLbutton(qgrow_t *reply, char *name, char *title) { reply->addstrf(reply, " \n", name, title); } static void HTMLlist(qgrow_t *reply, char *title, qlist_t *list) { qlist_obj_t obj; reply->addstrf(reply, "Request number %d, Process ID: %d
\n", count++, getpid()); Rd->reply->addstrf(Rd->reply, "libfcgi version: %s
\n", FCGI_VERSION); Rd->reply->addstrf(Rd->reply, "Lua version: %s
\n", LUA_RELEASE); Rd->reply->addstrf(Rd->reply, "LuaJIT version: %s
\n", LUAJIT_VERSION); Rd->reply->addstrf(Rd->reply, "MySQL client version: %s
\n", mysql_get_client_info()); outize(Rd->reply, Rd->headers, "Environment"); outize(Rd->reply, Rd->cookies, "Cookies"); outize(Rd->reply, Rd->queries, "Query"); outize(Rd->reply, Rd->body, "POST body"); outize(Rd->reply, Rd->stuff, "Stuff"); showSesh(Rd->reply, &Rd->shs); if (Rd->lnk) showSesh(Rd->reply, Rd->lnk); outize(Rd->reply, Rd->database, "Database"); outizeCookie(Rd->reply, Rd->Rcookies, "Reply Cookies"); outize(Rd->reply, Rd->Rheaders, "Reply HEADERS"); } } else if (strcmp("URL", text) == 0) Rd->reply->addstrf(Rd->reply, "%s://%s%s", Rd->Scheme, Rd->Host, Rd->Script); else { if ((tmp = Rd->stats->stats->getstr(Rd->stats->stats, text, false)) != NULL) Rd->reply->addstr(Rd->reply, tmp); else Rd->reply->addstrf(Rd->reply, "%s", text); } break; } case FT_LUA: break; } } static void HTMLfooter(qgrow_t *reply) { reply->addstrf(reply, "This account manager system is currently in test mode, and under heavy development. " " Which means it's not all written yet, and things may break.
\n" "Your mission, should you choose to accept it, is to break it, and report how you broke it, so that onefang can fix it.
\n" "During test mode, no real grid accounts are created, and any accounts created with this will be deleted later. " " So feel free to create as many test accounts as you need to test things.
\n" "We follow the usual web site registration process, which sends a validation email, with a link to click. " " However, during this test mode, no emails will be sent, instead a link will be displayed near the top of the page when a user is logged in.
\n" "After creating an account, log on as your grid god account, click the 'validated members' button, click on the new member, set their level to 'approved', " " then click on the 'save' button. In theory that will create their in world account, in practice I still haven't written that bit.
" "Missing bits that are still being written - sending the emails, creating real grid accounts, editing accounts, listing accounts, deleting accounts.
\n" "You have an email waiting with a validation link in it, please check your email. " "It will be from %s@%s, and it might be in your spam folder, coz these sorts of emails sometimes end up there. " "You should add that email address to your contacts, or otherwise let it through your spam filter. " // "%s" "
\n", "grid_no_reply", Rd->Host, Rd->Host, Rd->RUri // ,t1, t0 ); free(t1); } return ret; } static void freeSesh(reqData *Rd, boolean linky, boolean wipe) { char *file = NULL; sesh *shs = &Rd->shs; T("free sesh %s %s", linky ? "linky" : "session", wipe ? "wipe" : "delete"); if (linky) { shs = Rd->lnk; file = xmprintf("%s/sessions/%s.linky", scCache, shs->leaf); } else file = xmprintf("%s/sessions/%s.lua", scCache, shs->leaf); if (wipe) I("Wiping session %s.", file); else I("Deleting session %s.", file); if ('\0' != shs->leaf[0]) { if (unlink(file)) perror_msg("Unable to delete %s", file); } Rd->body-> remove(Rd->body, "munchie"); Rd->cookies->remove(Rd->cookies, "toke_n_munchie"); Rd->cookies->remove(Rd->cookies, "hashish"); cookie *ck = setCookie(Rd, "toke_n_munchie", ""); cookie *ckh = setCookie(Rd, "hashish", ""); ck->maxAge = -1; // Should expire immediately. ckh->maxAge = -1; // Should expire immediately. qhashtbl_obj_t obj; if (wipe) { memset((void*)&obj, 0, sizeof(obj)); Rd->database->lock(Rd->database); while(Rd->database->getnext(Rd->database, &obj, false) == true) Rd->database->remove(Rd->database, obj.name); Rd->database->unlock(Rd->database); shs->name = NULL; shs->UUID = NULL; shs->level = -256; // TODO - should I wipe the rest of Rd->shs as well? Rd->stuff->remove(Rd->stuff, "name"); Rd->stuff->remove(Rd->stuff, "firstName"); Rd->stuff->remove(Rd->stuff, "lastName"); Rd->stuff->remove(Rd->stuff, "email"); Rd->stuff->remove(Rd->stuff, "passwordSalt"); Rd->stuff->remove(Rd->stuff, "passwordHash"); Rd->stuff->remove(Rd->stuff, "passHash"); Rd->stuff->remove(Rd->stuff, "passSalt"); Rd->stuff->remove(Rd->stuff, "linky-hashish"); } if (shs->isLinky) { free(Rd->lnk); Rd->lnk = NULL; } else { shs->leaf[0] = '\0'; } free(file); } static void setToken_n_munchie(reqData *Rd, boolean linky) { sesh *shs = &Rd->shs; char *file; if (linky) { shs = Rd->lnk; file = xmprintf("%s/sessions/%s.linky", scCache, shs->leaf); } else { file = xmprintf("%s/sessions/%s.lua", scCache, shs->leaf); } struct stat st; int s = stat(file, &st); if (!linky) { setCookie(Rd, "toke_n_munchie", shs->toke_n_munchie); setCookie(Rd, "hashish", shs->hashish); } char *tnm0 = xmprintf( "toke_n_munchie = \n" "{\n" " ['IP']='%s',\n" " ['salt']='%s',\n" " ['seshID']='%s',\n", getStrH(Rd->headers, "REMOTE_ADDR"), shs->salt, shs->seshID ); char *tnm1 = xmprintf(" ['name']='%s',\n ['level']='%d',\n", shs->name, (int) shs->level); char *tnm2 = xmprintf(" ['UUID']='%s',\n", shs->UUID); char *tnm3 = xmprintf(" ['passHash']='%s',\n", getStrH(Rd->stuff, "passHash")); char *tnm4 = xmprintf(" ['passSalt']='%s',\n", getStrH(Rd->stuff, "passSalt")); char *tnm9 = xmprintf("}\n" "return toke_n_munchie\n"); int fd = notstdio(xcreate_stdio(file, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, S_IRUSR | S_IWUSR)); size_t l = strlen(tnm0); if (s) I("Creating session %s.", file); else C("Updating session %s.", file); // I don't think updates can occur now. t("Write shs %s", tnm0); if (l != writeall(fd, tnm0, l)) { perror_msg("Writing %s", file); freeSesh(Rd, linky, TRUE); } if (NULL != shs->name) { t("Write shs %s", tnm1); l = strlen(tnm1); if (l != writeall(fd, tnm1, l)) { perror_msg("Writing %s", file); freeSesh(Rd, linky, TRUE); } } if (NULL != shs->UUID) { t("Write shs %s", tnm2); l = strlen(tnm2); if (l != writeall(fd, tnm2, l)) { perror_msg("Writing %s", file); freeSesh(Rd, linky, TRUE); } } if ('\0' != getStrH(Rd->stuff, "passHash")[0]) { t("Write shs %s", tnm3); l = strlen(tnm3); if (l != writeall(fd, tnm3, l)) { perror_msg("Writing %s", file); freeSesh(Rd, linky, TRUE); } } if ('\0' != getStrH(Rd->stuff, "passSalt")[0]) { t("Write shs %s", tnm4); l = strlen(tnm4); if (l != writeall(fd, tnm4, l)) { perror_msg("Writing %s", file); freeSesh(Rd, linky, TRUE); } } l = strlen(tnm9); if (l != writeall(fd, tnm9, l)) { perror_msg("Writing %s", file); freeSesh(Rd, linky, TRUE); } // Set the mtime on the file. futimens(fd, shs->timeStamp); xclose(fd); free(tnm9); free(tnm2); free(tnm1); free(tnm0); free(file); if (linky) { // TODO - Later use libcurl. char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName"); // TODO - should be from Rd.shs->linky-hashish char *t0 = Rd->lnk->hashish, *content, *command; if ('\0' != t0[0]) { char *t1 = qurl_encode(t0, strlen(t0)); content = xmprintf( "From: grid_no_reply@%s\n" "Relpy-to: grid_no_reply@%s\n" "Return-Path: bounce_email@%s\n" "To: %s\n" "Subject: Validate your new account on %s\n" "\n" "This is an automated validation email sent from %s.\n" "\n" "Dear %s %s,\n" "\n" "Some one has created the account '%s %s' on \n" "https://%s%s, and hopefully it was you.\n" "If it wasn't you, you can ignore this email.\n" "\n" "Please go to this web link to validate your new account -\n" "https://%s%s?hashish=%s\n" "\n" "Do not reply to this email.\n" "\n", Rd->Host, Rd->Host, Rd->Host, getStrH(Rd->stuff, "email"), Rd->Host, Rd->Host, first, last, first, last, Rd->Host, Rd->RUri, Rd->Host, Rd->RUri, t1 ); l = strlen(content); file = xmprintf("%s/sessions/%s.email", scCache, shs->leaf); fd = notstdio(xcreate_stdio(file, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, S_IRUSR | S_IWUSR)); if (l != writeall(fd, content, l)) { perror_msg("Writing %s", file); // freeSesh(Rd, linky, TRUE); } xclose(fd); I("Sending linky email to %s %s", getStrH(Rd->stuff, "email"), t1); command = xmprintf("sendmail -oi -t <%s", file); int i = system(command); if (!WIFEXITED(i)) E("sendmail command failed!"); free(command); free(file); free(content); free(t1); } } } static void generateAccountUUID(reqData *Rd) { // Generate a UUID, check it isn't already being used. char uuid[37], *where; uuid_t binuuid; my_ulonglong users = 0; int c; do // UserAccounts.PrincipalID is a unique primary index anyway, but we want the user creation process to be a little on the slow side. { struct stat st; uuid_generate_random(binuuid); uuid_unparse_lower(binuuid, uuid); // Try Lua user file. where = xmprintf("%s/users/%s.lua", scData, uuid); c = stat(where, &st); if (c) users = 1; free(where); // Try database. where = xmprintf("UserAccounts.PrincipalID = '%s'", uuid); D("Trying new UUID %s.", where); users = dbCount(Rd->db, "UserAccounts", where); free(where); } while (users != 0); Rd->shs.UUID = xstrdup(uuid); Rd->shs.level = -200; Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID", uuid); Rd->database->putstr(Rd->database, "UserAccounts.Userlevel", "-200"); } char *getLevel(short level) { char *ret = "", *lvl = xmprintf("%d", level); ret = accountLevels->getstr(accountLevels, lvl, false); if (NULL == ret) { qlisttbl_obj_t obj; memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call accountLevels->lock(accountLevels); while(accountLevels->getnext(accountLevels, &obj, NULL, false) == true) { if (atoi(obj.name) <= level) ret = (char *) obj.data; } } free(lvl); return ret; } static void accountWrite(reqData *Rd) { char *uuid = getStrH(Rd->database, "UserAccounts.PrincipalID"); char *file = xmprintf("%s/users/%s.lua", scData, uuid); char *level = getStrH(Rd->database, "UserAccounts.UserLevel"); char *link = (NULL == Rd->lnk) ? "" : Rd->lnk->hashish; char *about = encodeSlash(getStrH(Rd->stuff, "aboutMe")); char *voucher = encodeSlash(getStrH(Rd->stuff, "voucher")); char *tnm = xmprintf( "user = \n" "{\n" " ['name']='%s',\n" // TODO - putting these in Lua as numbers causes lua_tolstring to barf when we read them. Though Lua is supposed to convert between numbers and strings. " ['created']='%ld',\n" " ['email']='%s',\n" " ['title']='%s',\n" " ['level']='%s',\n" " ['flags']='%d',\n" " ['active']='%d',\n" " ['passwordHash']='%s',\n" " ['passwordSalt']='%s',\n" " ['UUID']='%s',\n" " ['DoB']='%s',\n" " ['agree']='%s',\n" " ['adult']='%s',\n" " ['aboutMe']='%s',\n" " ['vouched']='%s',\n" " ['voucher']='%s',\n" " ['linky-hashish']='%s',\n" "}\n" "return user\n", getStrH(Rd->stuff, "name"), (strcmp("", getStrH(Rd->stuff, "created")) != 0) ? atol(getStrH(Rd->stuff, "created")) : (long) Rd->shs.timeStamp[1].tv_sec, getStrH(Rd->stuff, "email"), getLevel(atoi(level)), level, 64, 0, getStrH(Rd->stuff, "passwordHash"), getStrH(Rd->stuff, "passwordSalt"), uuid, getStrH(Rd->stuff, "DoB"), getStrH(Rd->stuff, "agree"), getStrH(Rd->stuff, "adult"), about, "off", voucher, link ); struct stat st; int s = stat(file, &st); int fd = notstdio(xcreate_stdio(file, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, S_IRUSR | S_IWUSR)); size_t l = strlen(tnm); if (s) I("Creating user %s.", file); else C("Updating user %s.", file); if (l != writeall(fd, tnm, l)) perror_msg("Writing %s", file); else { char *name = Rd->stuff->getstr(Rd->stuff, "name", true); char *nm = xmprintf("%s/users/%s.lua", scData, qstrreplace("tr", name, " ", "_")); free(file); file = xmprintf("%s.lua", Rd->shs.UUID); I("Symlinking %s to %s", file, nm); if (0 != symlink(file, nm)) perror_msg("Symlinking %s to %s", file, nm); free(nm); free(name); } xclose(fd); free(tnm); free(voucher); free(about); free(file); } static sesh *newSesh(reqData *Rd, boolean linky) { unsigned char *md5hash = xzalloc(17); char *toke_n_munchie, *munchie, *hashish, *t0, *t1; char uuid[37]; uuid_t binuuid; sesh *ret = &Rd->shs; T("new sesh %s %s %s", linky ? "linky" : "session", ret->UUID, ret->name); if (linky) { Rd->lnk = xzalloc(sizeof(sesh)); ret = Rd->lnk; ret->UUID = Rd->shs.UUID; } char buf[128]; // 512 bits. int numBytes = getrandom((void *)buf, sizeof(buf), GRND_NONBLOCK); // NOTE that getrandom() returns random bytes, which may include '\0'. if (-1 == numBytes) { perror_msg("Unable to generate a suitable random number."); // EAGAIN - not enough entropy, try again. // EINTR - signal handler interrupted it, try again. } else { t0 = qhex_encode(buf, sizeof(buf)); qstrcpy(ret->salt, sizeof(ret->salt), t0); free(t0); //d("salt %s", ret->salt); numBytes = getrandom((void *)buf, sizeof(buf), GRND_NONBLOCK); if (-1 == numBytes) perror_msg("Unable to generate a suitable random number."); else { t0 = qhex_encode(buf, sizeof(buf)); qstrcpy(ret->seshID, sizeof(ret->seshID), t0); free(t0); //d("seshID %s", ret->seshID); ret->timeStamp[0].tv_nsec = UTIME_OMIT; ret->timeStamp[0].tv_sec = UTIME_OMIT; if (-1 == clock_gettime(CLOCK_REALTIME, &ret->timeStamp[1])) perror_msg("Unable to get the time."); else { // tv_sec is a time_t, tv_nsec is a long, but the actual type of time_t isn't well defined, it's some sort of integer. t0 = xmprintf("%s%ld.%ld", ret->seshID, (long) ret->timeStamp[1].tv_sec, ret->timeStamp[1].tv_nsec); qstrcpy(ret->sesh, sizeof(ret->sesh), t0); //d("sesh %s", ret->sesh); t1 = myHMAC(t0, FALSE); free(t0); munchie = xmprintf("%s%ld.%ld", t1, (long) ret->timeStamp[1].tv_sec, ret->timeStamp[1].tv_nsec); free(t1); qstrcpy(ret->munchie, sizeof(ret->munchie), munchie); //d("munchie %s", ret->munchie); // TODO - chicken and egg? Used to be from stuff->UUID. t1 = ret->UUID; if (NULL == t1) { uuid_clear(binuuid); uuid_unparse_lower(binuuid, uuid); ret->UUID = uuid; } t0 = xmprintf("%s%s", ret->UUID, munchie); free(munchie); toke_n_munchie = myHMAC(t0, FALSE); free(t0); qstrcpy(ret->toke_n_munchie, sizeof(ret->toke_n_munchie), toke_n_munchie); //d("toke_n_munchie %s", ret->toke_n_munchie); hashish = myHMACkey(ret->salt, toke_n_munchie, FALSE); free(toke_n_munchie); qstrcpy(ret->hashish, sizeof(ret->hashish), hashish); d("hashish %s", ret->hashish); t0 = myHMACkey(getStrH(Rd->configs, "pepper"), hashish, TRUE); free(hashish); qstrcpy(ret->leaf, sizeof(ret->leaf), t0); d("leaf %s", ret->leaf); free(t0); ret->isLinky = linky; setToken_n_munchie(Rd, linky); } } } free(md5hash); return ret; } /* CRUD (Create, Read, Update, Delete) CRAP (Create, Replicate, Append, Process) Though I prefer - DAVE (Delete, Add, View, Edit), coz the names are shorter. B-) On the other hand, list or browse needs to be added, which is why they have BREAD (Browse, Read, Edit, Add, Delete) CRUDL (Create, Read, Update, Delete, List) CRUDE (Create, Read, Update, Delete, Experience) Maybe - DAVEE (Delete, Add, View, Edit, Explore) */ // lua.h has LUA_T* NONE, NIL, BOOLEAN, LIGHTUSERDATA, NUMBER, STRING, TABLE, FUNCTION, USERDATA, THREAD as defines, -1 - 8. // These are the missing ones. Then later we will have prim, mesh, script, sound, terrain, ... #define LUA_TGROUP 42 #define LUA_TINTEGER 43 #define LUA_TEMAIL 44 #define LUA_TPASSWORD 45 #define LUA_TFILE 46 #define LUA_TIMAGE 47 #define FLD_NONE 0 #define FLD_EDITABLE 1 #define FLD_HIDDEN 2 #define FLD_REQUIRED 4 typedef struct _inputField inputField; typedef struct _inputSub inputSub; typedef struct _inputForm inputForm; typedef struct _inputValue inputValue; typedef int (*inputFieldValidFunc) (reqData *Rd, inputForm *iF, inputValue *iV); typedef void (*inputFieldShowFunc) (reqData *Rd, inputForm *iF, inputValue *iV); typedef int (*inputSubmitFunc) (reqData *Rd, inputForm *iF, inputValue *iV); typedef void (*inputFormShowFunc) (reqData *Rd, inputForm *iF, inputValue *iV); struct _inputField { char *name, *title, *help; inputFieldValidFunc validate; // Alas C doesn't have any anonymous function standard. inputFieldShowFunc web, console, gui; inputField **group; // If this is a LUA_TGROUP, then this will be a null terminated array of the fields in the group. // database details // lua file details signed char type, flags; short editLevel, viewLevel, viewLength, maxLength; }; struct _inputSub { char *name, *title, *help, *outputForm; inputSubmitFunc submit; }; struct _inputForm { char *name, *title, *help; qlisttbl_t *fields; // qlisttbl coz iteration in order and lookup are important. qhashtbl_t *subs; inputFormShowFunc web, eWeb; // display web, console, gui; // read function // write function }; struct _inputValue { inputField *field; void *value; // If this is a LUA_TGROUP, then this will be a null. short valid; // 0 for not yet validated, negative for invalid, positive for valid. short source, index; }; static int sessionValidate(reqData *Rd, inputForm *iF, inputValue *iV) { int ret = 0; boolean linky = FALSE; char *toke_n_munchie = "", *munchie = "", *hashish = "", *leaf = "", *timeStamp = "", *seshion = "", *seshID = "", *t0, *t1; // In this case the session stuff has to come from specific places. hashish = getStrH(Rd->queries, "hashish"); //d("O hashish %s", hashish); if ('\0' != hashish[0]) linky = TRUE; else { toke_n_munchie = getStrH(Rd->cookies, "toke_n_munchie"); // munchie = getStrH(Rd->body, "munchie"); hashish = getStrH(Rd->cookies, "hashish"); if (('\0' == toke_n_munchie[0]) || (('\0' == hashish[0]))) { if (strcmp("logout", Rd->doit) == 0) { d("Not checking session, coz we are logging out."); return ret; } bitchSession(Rd, "Invalid session.", "No or blank hashish or toke_n_munchie."); ret++; } } //d("O hashish %s", hashish); //d("O toke_n_munchie %s", toke_n_munchie); //d("O munchie %s", munchie); if (0 == ret) { struct stat st; struct timespec now; leaf = myHMACkey(getStrH(Rd->configs, "pepper"), hashish, TRUE); //d("leaf %s", leaf); if (linky) t0 = xmprintf("%s/sessions/%s.linky", scCache, leaf); else t0 = xmprintf("%s/sessions/%s.lua", scCache, leaf); qhashtbl_t *tnm = qhashtbl(0, 0); ret = LuaToHash(Rd, t0, "toke_n_munchie", tnm, ret, &st, &now, "session"); free(t0); if (0 == ret) { // This is apparently controversial, I added it coz some of the various security docs suggested it's a good idea. // https://security.stackexchange.com/questions/139952/why-arent-sessions-exclusive-to-an-ip-address?rq=1 // Includes various reasons why it's bad. // Another good reason why it is bad, TOR. // So should make this a user option, like Mantis does. if (strcmp(getStrH(Rd->headers, "REMOTE_ADDR"), getStrH(tnm, "IP")) != 0) { bitchSession(Rd, "Wrong IP for session.", "Session IP doesn't match."); ret++; } else { timeStamp = xmprintf("%ld.%ld", (long) st.st_mtim.tv_sec, st.st_mtim.tv_nsec); //d("timeStamp %s", timeStamp); seshion = xmprintf("%s%s", tnm->getstr(tnm, "seshID", false), timeStamp); //d("sesh %s", seshion); t0 = myHMAC(seshion, FALSE); munchie = xmprintf("%s%s", t0, timeStamp); //d("munchie %s", munchie); free(t0); free(timeStamp); t1 = getStrH(Rd->body, "munchie"); if ('\0' != t1[0]) { if (strcmp(t1, munchie) != 0) { bitchSession(Rd, "Wrong munchie for session.", "HMAC(seshID + timeStamp) != munchie"); ret++; } else { t0 = xmprintf("%s%s", getStrH(tnm, "UUID"), munchie); t1 = myHMAC(t0, FALSE); free(t0); //d("toke_n_munchie %s", t1); if (strcmp(t1, toke_n_munchie) != 0) { bitchSession(Rd, "Wrong toke_n_munchie for session.", "HMAC(UUID + munchie) != toke_n_munchie"); ret++; } free(t1); } if (linky) { t0 = xmprintf("%s%s", getStrH(tnm, "UUID"), munchie); t1 = myHMAC(t0, FALSE); free(t0); toke_n_munchie = t1; //d("toke_n_munchie %s", t1); } t1 = myHMACkey(getStrH(tnm, "salt"), toke_n_munchie, FALSE); //d("hashish %s", t1); if (strcmp(t1, hashish) != 0) { bitchSession(Rd, "Wrong hashish for session.", "HMAC(toke_n_munchie + salt) != hashish"); ret++; } free(t1); } if (0 == ret) { if (now.tv_sec > st.st_mtim.tv_sec + idleTimeOut) { W("Session idled out."); // Rd->vegOut = TRUE; } else { if (now.tv_sec > st.st_mtim.tv_sec + seshTimeOut) { W("Session timed out."); // Rd->vegOut = TRUE; } else { W("Validated session."); sesh *shs = &Rd->shs; qstrcpy(shs->leaf, sizeof(shs->leaf), leaf); if (linky) { W("Validated session linky."); addStrL(Rd->messages, "Congratulations, you have validated your new account. Now you can log onto the web site."); addStrL(Rd->messages, "NOTE - you wont be able to log onto the grid until your new account has been approved."); Rd->lnk = xzalloc(sizeof(sesh)); qstrcpy(Rd->lnk->leaf, sizeof(Rd->lnk->leaf), leaf); freeSesh(Rd, linky, FALSE); qstrcpy(Rd->lnk->leaf, sizeof(Rd->lnk->leaf), ""); Rd->doit = "validate"; Rd->output = "accountLogin"; Rd->form = "accountLogin"; // TODO - we might want to delete their old .lua session as well. Maybe? Don't think we have any suitable codes to find it. } else { char *level = tnm->getstr(tnm, "level", false); if (NULL == level) level = "-256"; qstrcpy(shs->sesh, sizeof(shs->sesh), seshion); qstrcpy(shs->toke_n_munchie, sizeof(shs->toke_n_munchie), toke_n_munchie); qstrcpy(shs->hashish, sizeof(shs->hashish), hashish); qstrcpy(shs->munchie, sizeof(shs->munchie), munchie); qstrcpy(shs->salt, sizeof(shs->salt), tnm->getstr(tnm, "salt", false)); qstrcpy(shs->seshID, sizeof(shs->seshID), tnm->getstr(tnm, "seshID", false)); // TODO - free this somewhere. // shs->name = tnm->getstr(tnm, "name", true); // shs->UUID = tnm->getstr(tnm, "UUID", true); shs->level = atoi(level); // TODO - get level from somewhere and stuff it in shs. shs->timeStamp[0].tv_nsec = UTIME_OMIT; shs->timeStamp[0].tv_sec = UTIME_OMIT; memcpy(&shs->timeStamp[1], &st.st_mtim, sizeof(struct timespec)); } // TODO - free this somewhere. shs->name = tnm->getstr(tnm, "name", true); shs->UUID = tnm->getstr(tnm, "UUID", true); } qhashtbl_obj_t obj; memset((void*)&obj, 0, sizeof(obj)); tnm->lock(tnm); while(tnm->getnext(tnm, &obj, false) == true) { char *n = obj.name; if ((strcmp("salt", n) != 0) && (strcmp("seshID", n) != 0) && (strcmp("UUID", n) != 0)) { t("SessionValidate() Lua read %s = %s", n, (char *) obj.data); Rd->stuff->putstr(Rd->stuff, obj.name, (char *) obj.data); } } tnm->unlock(tnm); // TODO - check this. // Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID", tnm->getstr(tnm, "UUID", false)); } } free(munchie); free(seshion); } } free(leaf); tnm->free(tnm); } return ret; } static void sessionWeb(reqData *Rd, inputForm *iF, inputValue *iV) { HTMLhidden(Rd->reply, iV->field->name, iV->value); } /* static int UUIDValidate(reqData *Rd, inputForm *iF, inputValue *iV) { int ret = 0; char *UUID = (char *) iV->value; if (36 != strlen(UUID)) { bitch(Rd, "Internal error.", "UUID isn't long enough."); ret++; } // TODO - check the characters and dashes as well. if (0 == ret) Rd->stuff->putstr(Rd->stuff, "UUID", UUID); return ret; } static void UUIDWeb(reqData *Rd, inputForm *iF, inputValue *iV) { HTMLhidden(Rd->reply, iV->field->name, iV->value); } */ static int nameValidate(reqData *Rd, inputForm *iF, inputValue *iV) { int ret = 0; unsigned char *name; // We have to be unsigned coz of isalnum(). char *where = NULL; //d("nameValidate %s", iV->field->name); name = xstrdup(iV->value); if ((NULL == name) || ('\0' == name[0])) { bitch(Rd, "Please supply an account name.", "None supplied."); ret++; } else { int l0 = strlen(name), l1 = 0, l2 = 0; if (0 == l0) { bitch(Rd, "Please supply an account name.", "Name is empty."); ret++; } else { int i; unsigned char *s = NULL; for (i = 0; i < l0; i++) { if (isalnum(name[i]) == 0) { if ((' ' == name[i] /*&& (NULL == s)*/)) { s = &name[i]; *s++ = '\0'; while(' ' == *s) { i++; s++; } l1 = strlen(name); l2 = strlen(s); // Apparently names are not case sensitive on login, but stored with case in the database. // I confirmed that, can log in no matter what case you use. // Seems to be good security for names to be cose insensitive. // UserAccounts FirstName and LastName fields are both varchar(64) utf8_general_ci. // The MySQL docs say that the "_ci" bit means comparisons will be case insensitive. So that should work fine. // SL docs say 31 characters each for first and last name. UserAccounts table is varchar(64) each. userinfo has varchar(50) for the combined name. // The userinfo table seems to be obsolete. // Singularity at least limits the total name to 64. // I can't find any limitations on characters allowed, but I only ever see letters and digits used. Case is stored, but not significant. // OpenSims "create user" console command doesn't sanitize it at all, even crashing on some names. } else { bitch(Rd, "First and last names are limited to ordinary letters and digits, no special characters or fonts.", ""); ret++; break; } // TODO - compare first, last, and fullname against god names, complain and fail if there's a match. } } if (NULL == s) { bitch(Rd, "Account names have to be two words.", ""); ret++; } if ((31 < l1) || (31 < l2)) { bitch(Rd, "First and last names are limited to 31 letters each.", ""); ret++; } if ((0 == l1) || (0 == l2)) { bitch(Rd, "First and last names have to be one or more ordinary letters or digits each.", ""); ret++; } if (0 == ret) { Rd->stuff->putstr(Rd->stuff, "firstName", name); Rd->stuff->putstr(Rd->stuff, "lastName", s); Rd->stuff->putstrf(Rd->stuff, "name", "%s %s", name, s); // TODO - fix this, so we don't show "You are user" when we are not, but everything else still works. // if ('\0' != getStrH(Rd->queries, "user")[0]) Rd->shs.name = Rd->stuff->getstr(Rd->stuff, "name", true); } } } free(name); return ret; } static void nameWeb(reqData *Rd, inputForm *oF, inputValue *oV) { if (oV->field->flags & FLD_HIDDEN) HTMLhidden(Rd->reply, oV->field->name, oV->value); else HTMLtext(Rd->reply, "text", oV->field->title, oV->field->name, oV->value, oV->field->viewLength, oV->field->maxLength, oV->field->flags & FLD_REQUIRED); } static int passwordValidate(reqData *Rd, inputForm *iF, inputValue *iV) { int ret = 0; char *password = (char *) iV->value, *salt = getStrH(Rd->stuff, "passSalt"), *hash = getStrH(Rd->stuff, "passHash"); if ((NULL == password) || ('\0' == password[0])) { bitch(Rd, "Please supply a password.", "Password empty or missing."); ret++; } else if (('\0' != salt[0]) && ('\0' != hash[0]) && (strcmp("psswrd", iV->field->name) == 0)) { D("Comparing passwords. %s %s %s", password, salt, hash); char *h = checkSLOSpassword(Rd, salt, password, hash, "Passwords are not the same."); if (NULL == h) ret++; else free(h); } // TODO - once the password is validated, store it as the salt and hash. // If it's an existing account, compare it? Or do that later? if (0 == ret) Rd->stuff->putstr(Rd->stuff, "password", password); return ret; } static void passwordWeb(reqData *Rd, inputForm *oF, inputValue *oV) { HTMLtext(Rd->reply, "password", oV->field->title, oV->field->name, "", oV->field->viewLength, oV->field->maxLength, oV->field->flags & FLD_REQUIRED); Rd->reply->addstr(Rd->reply, "While viewers will usually remember your name and password for you, you'll need to remember it for this web site to. " "I highly recommend using a password manager. KeePass and it's variations is a great password manager.
\n"); } static int emailValidate(reqData *Rd, inputForm *iF, inputValue *iV) { // inputField **group = iV->field->group; int ret = 0, i; boolean notSame = FALSE; i = iV->index; if (2 == i) { char *email = (char *) iV->value; char *emayl = (char *) (iV + 1)->value; if ((NULL == email) || (NULL == emayl) || ('\0' == email[0]) || ('\0' == emayl[0])) { bitch(Rd, "Please supply an email address.", "None supplied."); ret++; } else if (strcmp(email, emayl) != 0) { bitch(Rd, "Email addresses are not the same.", ""); ret++; notSame = TRUE; } else if (!qstr_is_email(email)) { bitch(Rd, "Please supply a proper email address.", "Failed qstr_is_email()"); ret++; } else { // TODO - do other email checks - does the domain exist, .. } if ((NULL != email) && (NULL != emayl)) { char *t0 = qurl_encode(email, strlen(email)); // In theory it's the correct thing to do to NOT load email into stuff on failure, // In practice, that means it wont show the old email and emayl in the create page when they don't match. if ((0 == ret) || notSame) Rd->stuff->putstrf(Rd->stuff, "email", "%s", t0); free(t0); } if ((NULL != email) && (NULL != emayl)) { char *t1 = qurl_encode(emayl, strlen(emayl)); Rd->stuff->putstrf(Rd->stuff, "emayl", "%s", t1); free(t1); } } return ret; } static void emailWeb(reqData *Rd, inputForm *oF, inputValue *oV) { HTMLtext(Rd->reply, "email", oV->field->title, oV->field->name, displayPrep(getStrH(Rd->stuff, oV->field->name)), oV->field->viewLength, oV->field->maxLength, oV->field->flags & FLD_REQUIRED); Rd->reply->addstrf(Rd->reply, "An email will be sent from %s@%s, and it might be in your spam folder, coz these sorts of emails sometimes end up there. " "You should add that email address to your contacts, or otherwise let it through your spam filter.
", "grid_no_reply", Rd->Host); } char *months[] = { "january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december" }; static int DoBValidate(reqData *Rd, inputForm *iF, inputValue *iV) { int ret = 0, i; char *t0, *t1; // inputField **group = iV->field->group; i = iV->index; if (2 == i) { t0 = (char *) iV->value; if ((NULL == t0) || ('\0' == t0[0])) { bitch(Rd, "Please supply a year of birth.", "None supplied."); ret++; } else { i = atoi(t0); // TODO - get this to use current year instead of 2020. if ((1900 > i) || (i > 2020)) { bitch(Rd, "Please supply a year of birth.", "Out of range."); ret++; } else if (i < 1901) { bitch(Rd, "Please supply a proper year of birth.", "Out of range, too old."); ret++; } else if (i >2004) { bitch(Rd, "This grid is Adult rated, you are too young.", "Out of range, too young."); ret++; } } t1 = (char *) (iV + 1)->value; if ((NULL == t1) || ('\0' == t1[0])) { bitch(Rd, "Please supply a month of birth.", "None supplied."); ret++; } else { for (i = 0; i < 12; i++) { if (strcmp(months[i], t1) == 0) break; } if (12 == i) { bitch(Rd, "Please supply a month of birth.", "Out of range"); ret++; } } if (0 == ret) { Rd->stuff->putstr(Rd->stuff, "year", t0); Rd->stuff->putstr(Rd->stuff, "month", t1); Rd->stuff->putstrf(Rd->stuff, "DoB", "%s %s", t0, t1); } } return ret; } static void DoByWeb(reqData *Rd, inputForm *oF, inputValue *oV) { char *tmp = xmalloc(16), *t; int i, d; Rd->reply->addstr(Rd->reply, "\n"); } static void DoBWeb(reqData *Rd, inputForm *oF, inputValue *oV) { } static int legalValidate(reqData *Rd, inputForm *iF, inputValue *iV) { int ret = 0, i; char *t; inputField **group = iV->field->group; i = iV->index; if (2 == i) { t = (char *) iV->value; if ((NULL == t) || (strcmp("on", t) != 0)) { bitch(Rd, "You must be an adult to enter this site.", ""); ret++; } else Rd->stuff->putstr(Rd->stuff, "adult", t); t = (char *) (iV + 1)->value; if ((NULL == t) || (strcmp("on", t) != 0)) { bitch(Rd, "You must agree to the Terms & Conditions of Use.", ""); ret++; } else Rd->stuff->putstr(Rd->stuff, "agree", t); } return ret; } static void adultWeb(reqData *Rd, inputForm *oF, inputValue *oV) { HTMLcheckBox(Rd->reply, oV->field->name, oV->field->title, !strcmp("on", getStrH(Rd->body, "adult")), oV->field->flags & FLD_REQUIRED); } static void agreeWeb(reqData *Rd, inputForm *oF, inputValue *oV) { HTMLcheckBox(Rd->reply, oV->field->name, oV->field->title, !strcmp("on", getStrH(Rd->body, "agree")), oV->field->flags & FLD_REQUIRED); } static void legalWeb(reqData *Rd, inputForm *oF, inputValue *oV) { } static void ToSWeb(reqData *Rd, inputForm *oF, inputValue *oV) { Rd->reply->addstrf(Rd->reply, "%s\n", getStrH(Rd->configs, "ToS")); } static int voucherValidate(reqData *Rd, inputForm *oF, inputValue *oV) { int ret = 0; char *voucher = (char *) oV->value; if ((NULL == voucher) || ('\0' == voucher[0])) { bitch(Rd, "Please fill in the 'Voucher' section.", "None supplied."); ret++; } if ((0 == ret) && (NULL != voucher)) { char *t = qurl_encode(voucher, strlen(voucher)); Rd->stuff->putstr(Rd->stuff, "voucher", t); free(t); } return ret; } static void voucherWeb(reqData *Rd, inputForm *oF, inputValue *oV) { HTMLtext(Rd->reply, "text", oV->field->title, oV->field->name, oV->value, oV->field->viewLength, oV->field->maxLength, oV->field->flags & FLD_REQUIRED); } static int aboutMeValidate(reqData *Rd, inputForm *oF, inputValue *oV) { int ret = 0; char *about = (char *) oV->value; if ((NULL == about) || ('\0' == about[0])) { bitch(Rd, "Please fill in the 'About me' section.", "None supplied."); ret++; } if ((0 == ret) && (NULL != about)) { char *t = qurl_encode(about, strlen(about)); Rd->stuff->putstr(Rd->stuff, "aboutMe", t); free(t); } return ret; } static void aboutMeWeb(reqData *Rd, inputForm *oF, inputValue *oV) { // For maxlength - the MySQL database field is type text, which has a max length of 64 Kilobytes byets, but characters might take up 1 - 4 bytes, and maxlength is in characters. // For rows and cols, seems a bit broken, I ask for 5/42, I get 6,36. In world it seems to be 7,46 // TODO - check against the limit for in world profiles, coz this will become that. // TODO - validate aboutMe, it should not be empty, and should not be longer than 64 kilobytes. HTMLtextArea(Rd->reply, oV->field->name, oV->field->title, 7, oV->field->viewLength, 4, oV->field->maxLength, "Describe yourself here.", "off", "true", "soft", oV->value, FALSE, FALSE); } static void accountWebHeaders(reqData *Rd, inputForm *oF) //, char *name) { char *linky = checkLinky(Rd); HTMLheader(Rd->reply, " account manager"); Rd->reply->addstrf(Rd->reply, "
%s
\n", oF->help); HTMLform(Rd->reply, "", Rd->shs.munchie); HTMLhidden(Rd->reply, "form", oF->name); } static void accountWebFields(reqData *Rd, inputForm *oF, inputValue *oV) { int count = oF->fields->size(oF->fields), i; for (i = 0; i < count; i++) { if (NULL != oV[i].field->web) oV[i].field->web(Rd, oF, &oV[i]); if ((NULL != oV[i].field->help) && ('\0' != oV[i].field->help[0])) Rd->reply->addstrf(Rd->reply, "%s
\n", oV[i].field->help); //d("accountWebFeilds(%s, %s)", oF->name, oV[i].field->name); } } static void accountWebSubs(reqData *Rd, inputForm *oF) { qhashtbl_obj_t obj; Rd->reply->addstrf(Rd->reply, "\n"); // Stop Enter key on text fields triggering the first submit button. memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call oF->subs->lock(oF->subs); while(oF->subs->getnext(oF->subs, &obj, false) == true) { inputSub *sub = (inputSub *) obj.data; if ('\0' != sub->title[0]) HTMLbutton(Rd->reply, sub->name, sub->title); //d("accountWebSubs(%s, %s '%s')", oF->name, sub->name, sub->title); } oF->subs->unlock(oF->subs); } static void accountWebFooter(reqData *Rd, inputForm *oF) { if (0 != Rd->errors->size(Rd->errors)) HTMLlist(Rd->reply, "errors -", Rd->errors); HTMLformEnd(Rd->reply); HTMLfooter(Rd->reply); } static void accountAddWeb(reqData *Rd, inputForm *oF, inputValue *oV) { // char *name = getStrH(Rd->database, "Lua.name"); accountWebHeaders(Rd, oF); accountWebFields(Rd, oF, oV); accountWebSubs(Rd, oF); accountWebFooter(Rd, oF); } static void accountLoginWeb(reqData *Rd, inputForm *oF, inputValue *oV) { Rd->shs.name = NULL; Rd->shs.UUID = NULL; accountWebHeaders(Rd, oF); accountWebFields(Rd, oF, oV); accountWebSubs(Rd, oF); accountWebFooter(Rd, oF); } static void accountViewWeb(reqData *Rd, inputForm *oF, inputValue *oV) { char *name = getStrH(Rd->database, "Lua.name"), *level = getStrH(Rd->database, "UserAccounts.UserLevel"), *email = displayPrep(getStrH(Rd->database, "UserAccounts.Email")), *voucher = displayPrep(getStrH(Rd->database, "Lua.voucher")), *about = displayPrep(getStrH(Rd->database, "Lua.aboutMe")); time_t crtd = atol(getStrH(Rd->database, "UserAccounts.Created")); accountWebHeaders(Rd, oF); accountWebFields(Rd, oF, oV); // TODO - still need to encode < > as < u> for email, voucher, and about. // TODO - dammit, qurl_decode returns the string length, and decodes the string in place. Rd->reply->addstrf(Rd->reply, "Name : %s
", name); Rd->reply->addstrf(Rd->reply, "Title / level : %s / %s
", getLevel(atoi(level)), level); Rd->reply->addstrf(Rd->reply, "Date of birth : %s
", getStrH(Rd->database, "Lua.DoB")); Rd->reply->addstrf(Rd->reply, "Created : %s
", ctime(&crtd)); Rd->reply->addstrf(Rd->reply, "Email : %s
", email); Rd->reply->addstrf(Rd->reply, "UUID : %s
", getStrH(Rd->database, "UserAccounts.PrincipalID")); Rd->reply->addstrf(Rd->reply, "Voucher : %s
", voucher); // Rd->reply->addstrf(Rd->reply, "About :
" // "", qurl_decode(getStrH(Rd->database, "Lua.aboutMe"))); HTMLtextArea(Rd->reply, "aboutMe", "About", 7, 50, 4, 16384, "", "off", "true", "soft", about, FALSE, TRUE); accountWebSubs(Rd, oF); accountWebFooter(Rd, oF); free(about); free(voucher); free(email); } static void accountEditWeb(reqData *Rd, inputForm *oF, inputValue *oV) { char *name = getStrH(Rd->database, "Lua.name"), *level = getStrH(Rd->database, "UserAccounts.UserLevel"), *email = displayPrep(getStrH(Rd->database, "UserAccounts.Email")), *voucher = displayPrep(getStrH(Rd->database, "Lua.voucher")), *about = displayPrep(getStrH(Rd->database, "Lua.aboutMe")), *lvl = getLevel(atoi(level)); accountWebHeaders(Rd, oF); accountWebFields(Rd, oF, oV); // HTMLtext(Rd->reply, "password", "Old password", "password", "", 16, 0, FALSE); // Rd->reply->addstr(Rd->reply, "Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters.
\n"); //// HTMLtext(Rd->reply, "title", "text", "title", getStrH(Rh->stuff, "title"), 16, 64, TRUE); HTMLhidden(Rd->reply, "user", name); Rd->reply->addstrf(Rd->reply, "Name : %s
", name); Rd->reply->addstrf(Rd->reply, "Email : %s
", email); Rd->reply->addstrf(Rd->reply, "Voucher : %s
", voucher); if (200 <= Rd->shs.level) { qlisttbl_obj_t obj; HTMLselect(Rd->reply, "level", "level"); memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call accountLevels->lock(accountLevels); while(accountLevels->getnext(accountLevels, &obj, NULL, false) == true) { boolean is = false; short l = atoi((char *) obj.name); if (strcmp(lvl, (char *) obj.data) == 0) is = true; if ((is) || ((l <= Rd->shs.level) && (l != -200) && (l != -100) && (l != -50))) // Not above our pay grade, not newbie, validated, nor vouched for. HTMLoption(Rd->reply, (char *) obj.data, is); } accountLevels->unlock(accountLevels); HTMLselectEnd(Rd->reply); Rd->reply->addstrf(Rd->reply, "Title / level : %s / %s
", lvl, level); accountWebSubs(Rd, oF); accountWebFooter(Rd, oF); } static int accountRead(reqData *Rd, char *uuid, char *firstName, char *lastName) { int ret = 0, rt = -1; struct stat st; struct timespec now; qhashtbl_t *tnm = qhashtbl(0, 0); uuid_t binuuid; rowData *rows = NULL; // Setup the database stuff. static dbRequest *uuids = NULL; if (NULL == uuids) { static char *szi[] = {"PrincipalID", NULL}; static char *szo[] = {NULL}; uuids = xzalloc(sizeof(dbRequest)); uuids->db = Rd->db; uuids->table = "UserAccounts"; uuids->inParams = szi; uuids->outParams = szo; uuids->where = "PrincipalID=?"; dbRequests->addfirst(dbRequests, uuids, sizeof(*uuids)); } static dbRequest *acnts = NULL; if (NULL == acnts) { static char *szi[] = {"FirstName", "LastName", NULL}; static char *szo[] = {NULL}; acnts = xzalloc(sizeof(dbRequest)); acnts->db = Rd->db; acnts->table = "UserAccounts"; acnts->inParams = szi; acnts->outParams = szo; acnts->where = "FirstName=? and LastName=?"; dbRequests->addfirst(dbRequests, acnts, sizeof(*acnts)); } static dbRequest *auth = NULL; if (NULL == auth) { static char *szi[] = {"UUID", NULL}; static char *szo[] = {"passwordSalt", "passwordHash", NULL}; auth = xzalloc(sizeof(dbRequest)); auth->db = Rd->db; auth->table = "auth"; auth->inParams = szi; auth->outParams = szo; auth->where = "UUID=?"; dbRequests->addfirst(dbRequests, auth, sizeof(*auth)); } // uuid = Rd->shs.UUID; first = getStrH(Rd->stuff, "firstName"); last = getStrH(Rd->stuff, "lastName"); // Special for showing another users details. if ('\0' != getStrH(Rd->queries, "user")[0]) uuid = ""; char *first = xstrdup(""), *last = xstrdup(""); if (NULL != firstName) { first = xstrdup(firstName); if (NULL == lastName) { char *t = strchr(first, ' '); d("accountRead() single name |%s| |%s|", first, last); if (NULL == t) t = strchr(first, '+'); if (NULL != t) { *t++ = '\0'; last = xstrdup(t); } } else last = xstrdup(lastName); } d("accountRead() UUID %s, name %s %s", uuid, first, last); uuid_clear(binuuid); if ((NULL != uuid) && ('\0' != uuid[0])) uuid_parse(uuid, binuuid); if ((NULL != uuid) && ('\0' != uuid[0]) && (!uuid_is_null(binuuid))) { char *where = xmprintf("%s/users/%s.lua", scData, uuid); rt = LuaToHash(Rd, where, "user", tnm, ret, &st, &now, "user"); free(where); dbDoSomething(uuids, FALSE, uuid); rows = uuids->rows; } else { if ('\0' != first[0]) { char *where = xmprintf("%s/users/%s_%s.lua", scData, first, last); rt = LuaToHash(Rd, where, "user", tnm, ret, &st, &now, "user"); free(where); dbDoSomething(acnts, FALSE, first, last); rows = acnts->rows; } } // else // { // bitch(Rd, "Unable to read user record.", "Nothing available to look up a user record with."); // rt = 1; // } if (0 == rt) { ret += 1; Rd->database->putstr(Rd->database, "UserAccounts.FirstName", first); Rd->database->putstr(Rd->database, "UserAccounts.LastName", last); Rd->database->putstr(Rd->database, "UserAccounts.Email", getStrH(tnm, "email")); Rd->database->putstr(Rd->database, "UserAccounts.Created", getStrH(tnm, "created")); Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID", getStrH(tnm, "UUID")); Rd->database->putstr(Rd->database, "UserAccounts.UserLevel", getStrH(tnm, "level")); Rd->database->putstr(Rd->database, "UserAccounts.UserFlags", getStrH(tnm, "flags")); Rd->database->putstr(Rd->database, "UserAccounts.UserTitle", getStrH(tnm, "title")); Rd->database->putstr(Rd->database, "UserAccounts.active", getStrH(tnm, "active")); Rd->database->putstr(Rd->database, "auth.passwordSalt", getStrH(tnm, "passwordSalt")); Rd->database->putstr(Rd->database, "auth.passwordHash", getStrH(tnm, "passwordHash")); Rd->stuff-> putstr(Rd->stuff, "linky-hashish", getStrH(tnm, "linky-hashish")); Rd->database->putstr(Rd->database, "Lua.name", getStrH(tnm, "name")); Rd->database->putstr(Rd->database, "Lua.DoB", getStrH(tnm, "DoB")); Rd->database->putstr(Rd->database, "Lua.agree", getStrH(tnm, "agree")); Rd->database->putstr(Rd->database, "Lua.adult", getStrH(tnm, "adult")); Rd->database->putstr(Rd->database, "Lua.aboutMe", getStrH(tnm, "aboutMe")); Rd->database->putstr(Rd->database, "Lua.vouched", getStrH(tnm, "vouched")); Rd->database->putstr(Rd->database, "Lua.voucher", getStrH(tnm, "voucher")); } else if (rows) { ret += rows->rows->size(rows->rows); if (1 == ret) { dbPull(Rd, "UserAccounts", rows); char *name = xmprintf("%s %s", getStrH(Rd->database, "UserAccounts.FirstName"), getStrH(Rd->database, "UserAccounts.LastName")); Rd->database->putstr(Rd->database, "Lua.name", name); free(name); dbDoSomething(auth, FALSE, getStrH(Rd->database, "UserAccounts.PrincipalID")); rows = auth->rows; if (rows) { if (1 == rows->rows->size(rows->rows)) dbPull(Rd, "auth", rows); } } } else { d("No user name or UUID to get an account for."); } if (1 == ret) { // TODO - this has to change when we are editing other peoples accounts. if ('\0' == getStrH(Rd->queries, "user")[0]) { // Rd->shs.level = atoi(getStrH(Rd->database, "UserAccounts.UserLevel")); // TODO - might have to combine first and last here. // Rd->shs.name = Rd->database->getstr(Rd->database, "Lua.name", true); // Rd->shs.UUID = Rd->database->getstr(Rd->database, "UserAccounts.PrincipalID", true); //d("accountRead() setting session uuid %s level %d name %s ", Rd->shs.UUID, (int) Rd->shs.level, Rd->shs.name); } // Rd->stuff->putstr(Rd->stuff, "email", getStrH(Rd->database, "UserAccounts.Email")); } free(last); free(first); tnm->free(tnm); return ret; } static int accountDelSub(reqData *Rd, inputForm *iF, inputValue *iV) { int ret = 0; char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName"); int c = accountRead(Rd, uuid, first, last); if (1 != c) { bitch(Rd, "Cannot delete account.", "Account doesn't exist."); ret++; } else { // check if logged in user is allowed to delete this account // delete user record // log the user out if they are logged in } return ret; } static int accountCreateSub(reqData *Rd, inputForm *iF, inputValue *iV) { int ret = 0; char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName"); int c = accountRead(Rd, uuid, first, last); boolean wipe = FALSE; if (strcmp("POST", Rd->Method) == 0) { if (0 != c) { bitch(Rd, "Cannot create account.", "Account exists."); ret++; } else { char *salt = newSLOSsalt(Rd); char *h = checkSLOSpassword(Rd, salt, getStrH(Rd->body, "password"), NULL, NULL); if (NULL == h) ret++; else { Rd->stuff->putstr(Rd->stuff, "passHash", h); Rd->stuff->putstr(Rd->stuff, "passSalt", salt); free(h); } free(salt); if (0 != ret) { wipe = TRUE; Rd->shs.UUID = NULL; Rd->output = "accountLogin"; } } } freeSesh(Rd, FALSE, wipe); newSesh(Rd, FALSE); return ret; } static int accountAddSub(reqData *Rd, inputForm *iF, inputValue *iV) { int ret = 0; char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName"); int c = accountRead(Rd, uuid, first, last); boolean wipe = FALSE; if (0 != c) { bitch(Rd, "Cannot add account.", "Account exists."); ret++; } else if ((0 == ret) && (strcmp("POST", Rd->Method) == 0)) { char *h = checkSLOSpassword(Rd, getStrH(Rd->stuff, "passSalt"), getStrH(Rd->stuff, "password"), getStrH(Rd->stuff, "passHash"), "Passwords are not the same."); if (NULL == h) { ret++; wipe = TRUE; Rd->shs.UUID = NULL; Rd->output = "accountLogin"; } else { free(h); generateAccountUUID(Rd); Rd->stuff->putstr(Rd->stuff, "passwordHash", getStrH(Rd->stuff, "passHash")); Rd->stuff->putstr(Rd->stuff, "passwordSalt", getStrH(Rd->stuff, "passSalt")); Rd->shs.level = -200; Rd->database->putstr(Rd->database, "UserAccounts.UserLevel", "-200"); freeSesh(Rd, FALSE, wipe); newSesh(Rd, TRUE); accountWrite(Rd); // log them in I("Logged on %s %s Level %d %s", Rd->shs.UUID, getStrH(Rd->stuff, "name"), Rd->shs.level, getLevel(Rd->shs.level)); Rd->output = "accountView"; Rd->form = "accountView"; Rd->doit = "login"; } } freeSesh(Rd, FALSE, wipe); newSesh(Rd, FALSE); return ret; } static int accountSaveSub(reqData *Rd, inputForm *iF, inputValue *iV) { int ret = 0; char *uuid = Rd->shs.UUID, *first = getStrH(Rd->body, "user"), *last = NULL; int c = accountRead(Rd, NULL, first, last); boolean wipe = FALSE; if (1 != c) { bitch(Rd, "Cannot save account.", "Account doesn't exist."); ret++; } else if ((0 == ret) && (strcmp("POST", Rd->Method) == 0)) { Rd->stuff->putstr(Rd->stuff, "email", getStrH(Rd->database, "UserAccounts.Email")); Rd->stuff->putstr(Rd->stuff, "created", getStrH(Rd->database, "UserAccounts.Created")); Rd->stuff->putstr(Rd->stuff, "flags", getStrH(Rd->database, "UserAccounts.UserFlags")); Rd->stuff->putstr(Rd->stuff, "active", getStrH(Rd->database, "UserAccounts.active")); Rd->stuff->putstr(Rd->stuff, "passwordSalt", getStrH(Rd->database, "auth.passwordSalt")); Rd->stuff->putstr(Rd->stuff, "passwordHash", getStrH(Rd->database, "auth.passwordHash")); Rd->stuff->putstr(Rd->stuff, "name", getStrH(Rd->database, "Lua.name")); Rd->stuff->putstr(Rd->stuff, "DoB", getStrH(Rd->database, "Lua.DoB")); Rd->stuff->putstr(Rd->stuff, "agree", getStrH(Rd->database, "Lua.agree")); Rd->stuff->putstr(Rd->stuff, "adult", getStrH(Rd->database, "Lua.adult")); Rd->stuff->putstr(Rd->stuff, "aboutMe", getStrH(Rd->database, "Lua.aboutMe")); Rd->stuff->putstr(Rd->stuff, "vouched", getStrH(Rd->database, "Lua.vouched")); Rd->stuff->putstr(Rd->stuff, "voucher", getStrH(Rd->database, "Lua.voucher")); char *lvl = getStrH(Rd->body, "level"); qlisttbl_obj_t obj; memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call accountLevels->lock(accountLevels); while(accountLevels->getnext(accountLevels, &obj, NULL, false) == true) { if (strcmp(lvl, (char *) obj.data) == 0) Rd->database->putstr(Rd->database, "UserAccounts.UserLevel", obj.name); } accountLevels->unlock(accountLevels); accountWrite(Rd); free(Rd->outQuery); Rd->outQuery = xmprintf("?user=%s+%s", getStrH(Rd->database, "UserAccounts.FirstName"), getStrH(Rd->database, "UserAccounts.LastName")); } // freeSesh(Rd, FALSE, wipe); // newSesh(Rd, FALSE); return ret; } static int accountValidateSub(reqData *Rd, inputForm *iF, inputValue *iV) { int ret = 0; char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName"); int c = accountRead(Rd, uuid, first, last); boolean wipe = FALSE; if (1 != c) { bitch(Rd, "Cannot validate account.", "Account doesn't exist."); ret++; } else { Rd->stuff->putstr(Rd->stuff, "email", getStrH(Rd->database, "UserAccounts.Email")); Rd->stuff->putstr(Rd->stuff, "created", getStrH(Rd->database, "UserAccounts.Created")); Rd->stuff->putstr(Rd->stuff, "flags", getStrH(Rd->database, "UserAccounts.UserFlags")); Rd->stuff->putstr(Rd->stuff, "active", getStrH(Rd->database, "UserAccounts.active")); Rd->stuff->putstr(Rd->stuff, "passwordSalt", getStrH(Rd->database, "auth.passwordSalt")); Rd->stuff->putstr(Rd->stuff, "passwordHash", getStrH(Rd->database, "auth.passwordHash")); Rd->stuff->putstr(Rd->stuff, "name", getStrH(Rd->database, "Lua.name")); Rd->stuff->putstr(Rd->stuff, "DoB", getStrH(Rd->database, "Lua.DoB")); Rd->stuff->putstr(Rd->stuff, "agree", getStrH(Rd->database, "Lua.agree")); Rd->stuff->putstr(Rd->stuff, "adult", getStrH(Rd->database, "Lua.adult")); Rd->stuff->putstr(Rd->stuff, "aboutMe", getStrH(Rd->database, "Lua.aboutMe")); Rd->stuff->putstr(Rd->stuff, "vouched", getStrH(Rd->database, "Lua.vouched")); Rd->stuff->putstr(Rd->stuff, "voucher", getStrH(Rd->database, "Lua.voucher")); Rd->shs.level = -100; Rd->database->putstr(Rd->database, "UserAccounts.UserLevel", "-100"); accountWrite(Rd); wipe = TRUE; } freeSesh(Rd, FALSE, wipe); newSesh(Rd, FALSE); return ret; } static int accountViewSub(reqData *Rd, inputForm *iF, inputValue *iV) { // TODO - this has to change when we are editing other peoples accounts. int ret = 0; char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName"); int c = accountRead(Rd, uuid, first, last); boolean wipe = FALSE; d("Sub accountViewSub() %s %s %s", uuid, first, last); if (1 != c) { bitch(Rd, "Cannot view account.", "Account doesn't exist."); ret++; wipe = TRUE; Rd->shs.UUID = NULL; Rd->output = "accountLogin"; } else { // Check password on POST if the session user is the same as the shown user, coz this is the page shown on login. // Also only check on login. if ((strcmp("POST", Rd->Method) == 0) //&& (strcmp(Rd->shs.UUID, getStrH(Rd->database, "UserAccounts.PrincipalID")) == 0) && (strcmp("login", Rd->doit) == 0) && (strcmp("accountLogin", Rd->form) == 0)) { char *h = checkSLOSpassword(Rd, getStrH(Rd->database, "auth.passwordSalt"), getStrH(Rd->body, "password"), getStrH(Rd->database, "auth.passwordHash"), "Login failed."); if (NULL == h) { ret++; wipe = TRUE; Rd->shs.UUID = NULL; Rd->output = "accountLogin"; } else { Rd->shs.level = atoi(getStrH(Rd->database, "UserAccounts.UserLevel")); Rd->shs.name = getStrH(Rd->database, "Lua.name"); Rd->shs.UUID = getStrH(Rd->database, "UserAccounts.PrincipalID"); free(h); I("Logged on %s %s Level %d %s", Rd->shs.UUID, Rd->shs.name, Rd->shs.level, getLevel(Rd->shs.level)); } } } freeSesh(Rd, FALSE, wipe); newSesh(Rd, FALSE); return ret; } static int accountEditSub(reqData *Rd, inputForm *iF, inputValue *iV) { int ret = 0; char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName"); int c = accountRead(Rd, uuid, first, last); d("Sub accountEditSub %s %s %s", uuid, first, last); if (1 != c) { bitch(Rd, "Cannot edit account.", "Account doesn't exist."); ret++; } else { // check if logged in user is allowed to make these changes // update user record } return ret; } static int accountExploreSub(reqData *Rd, inputForm *iF, inputValue *iV) { int ret = 0; // get a list of user records return ret; } static int accountOutSub(reqData *Rd, inputForm *iF, inputValue *iV) { int ret = 0; char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName"); int c = accountRead(Rd, uuid, first, last); if (1 != c) { // bitch(Rd, "Cannot logout account.", "Account doesn't exist."); // ret++; } else { // log the user out if they are logged in Rd->shs.UUID = NULL; Rd->output = "accountLogin"; } freeSesh(Rd, FALSE, TRUE); newSesh(Rd, FALSE); return ret; } typedef struct _RdAndListTbl RdAndListTbl; struct _RdAndListTbl { reqData *Rd; qlisttbl_t *list; }; static int accountFilterValidated(struct dirtree *node) { if (!node->parent) return DIRTREE_RECURSE | DIRTREE_SHUTUP; if (S_ISREG(node->st.st_mode)) { struct stat st; struct timespec now; RdAndListTbl *rdl = (RdAndListTbl *) node->parent->extra; qhashtbl_t *tnm = qhashtbl(0, 0); char *name = node->name; char *where = xmprintf("%s/users/%s", scData, node->name); int rt = LuaToHash(rdl->Rd, where, "user", tnm, 0, &st, &now, "user"); t("accountFilterValidatedVoucher %s (%s) -> %s -> %s", name, getStrH(tnm, "level"), getStrH(tnm, "name"), getStrH(tnm, "voucher")); if ((0 == rt) && (strcmp("-100", getStrH(tnm, "level")) == 0)) rdl->list->put(rdl->list, getStrH(tnm, "name"), tnm, sizeof(*tnm)); else tnm->free(tnm); free(where); } return 0; } qlisttbl_t *getAccounts(reqData *Rd) { qlisttbl_t *ret = qlisttbl(0); RdAndListTbl rdl = {Rd, ret}; char *path = xmprintf("%s/users", scData); struct dirtree *new = dirtree_add_node(0, path, 0); new->extra = (long) &rdl; dirtree_handle_callback(new, accountFilterValidated); ret->sort(ret); free(path); return ret; } static void accountExploreValidatedVouchersWeb(reqData *Rd, inputForm *oF, inputValue *oV) { qlisttbl_t *list =getAccounts(Rd); // char *name = getStrH(Rd->stuff, "name"); Rd->shs.UUID = NULL; accountWebHeaders(Rd, oF); accountWebFields(Rd, oF, oV); count = list->size(list); Rd->reply->addstrf(Rd->reply, "name | "); Rd->reply->addstr(Rd->reply, "voucher | "); Rd->reply->addstr(Rd->reply, "level | "); Rd->reply->addstr(Rd->reply, "title | "); Rd->reply->addstr(Rd->reply, "
---|---|---|---|
%s | ", Rd->Host, Rd->RUri, nm, obj.name); Rd->reply->addstrf(Rd->reply, "%s | %s | %s |
%s
\n", message); HTMLfooter(Rd->reply); } */ void account_html(char *file, reqData *Rd, HTMLfile *thisFile) { inputForm *iF; inputField *fld; inputSub *sub; boolean isGET = FALSE; int e = 0, t = 0, i, j; char *doit = getStrH(Rd->body, "doit"), *form = getStrH(Rd->body, "form"); if (NULL == accountLevels) { accountLevels = qlisttbl(QLISTTBL_LOOKUPFORWARD | QLISTTBL_THREADSAFE | QLISTTBL_UNIQUE); accountLevels->putstr(accountLevels, "-256", "disabled"); accountLevels->putstr(accountLevels, "-200", "newbie"); accountLevels->putstr(accountLevels, "-100", "validated"); accountLevels->putstr(accountLevels, "-50", "vouched for"); accountLevels->putstr(accountLevels, "1", "approved"); accountLevels->putstr(accountLevels, "200", "god"); } // Check "Origin" header and /or HTTP_REFERER header. // "Origin" is either HTTP_HOST or X-FORWARDED-HOST. Which could be "null". char *ref = xmprintf("https://%s%s/account.html", getStrH(Rd->headers, "SERVER_NAME"), getStrH(Rd->headers, "SCRIPT_NAME")); char *href = Rd->headers->getstr(Rd->headers, "HTTP_REFERER", true); if (NULL != href) { char *f = strchr(href, '?'); if (NULL != f) *f = '\0'; if (('\0' != href[0]) && (strcmp(ref, href) != 0)) { bitch(Rd, "Invalid referer.", ref); D("Invalid referer - %s isn't %s", ref, href); form = "accountLogin"; } free(href); } free(ref); ref = getStrH(Rd->headers, "SERVER_NAME"); href = getStrH(Rd->headers, "HTTP_HOST"); if ('\0' == href[0]) href = getStrH(Rd->headers, "X-FORWARDED-HOST"); if (('\0' != href[0]) && (strcmp(ref, href) != 0)) { bitch(Rd, "Invalid HOST.", ref); D("Invalid HOST - %s isn't %s", ref, href); form = "accountLogin"; } // Redirect to HTTPS if it's HTTP. if (strcmp("https", Rd->Scheme) != 0) { Rd->Rheaders->putstr (Rd->Rheaders, "Status", "301 Moved Permanently"); Rd->Rheaders->putstrf(Rd->Rheaders, "Location", "https://%s%s", Rd->Host, Rd->RUri); Rd->reply->addstrf(Rd->reply, "