/* 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;
unsigned int dbTimeout;
struct timespec dbLast;
my_bool dbReconnect;
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 *scTemp = "";
char *Tconsole = "SledjChisl";
char *Tsocket = "opensim-tmux.socket";
char *Ttab = "SC";
char *Tcmd = "tmux -S";
char *backupIARsim = "Sandbox";
char *rSync = "";
int rSyncPort = 0;
char *webRoot = "/var/www/html";
char *URL = "fcgi-bin/sledjchisl.fcgi";
char *ToS = "Be good.";
char *webIframers = "";
int seshRenew = 10 * 60;
int idleTimeOut = 30 * 60;
int seshTimeOut = 24 * 60 * 60;
int newbieTimeOut = 30;
float loadAverageInc = 0.7;
int simTimeOut = 45;
int bulkSims = 0;
boolean DEBUG = FALSE;
boolean VERBOSE = FALSE;
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.
// TODO - escape anything that will turn the console into garbage.
// 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
"92;40", "VERBOSE", // green
"90", "DEBUG", // grey
// UNKNOWN? FATAL? SILENT? All from Android apparently.
"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];
if ((!VERBOSE) && (4 < v))
return;
if ((!DEBUG) && (5 < v))
return;
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 V(...) logMe(5, __VA_ARGS__)
#define D(...) logMe(6, __VA_ARGS__)
#define d(...) logMe(7, __VA_ARGS__)
#define t(...) logMe(8, __VA_ARGS__)
int shellMeFailVf(char *format, ...)
{
va_list va, va2;
int len;
char *ret;
int result = FALSE;
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);
V(ret);
free(ret);
return result;
}
int shellMeFailVt(char *format, ...)
{
va_list va, va2;
int len;
char *ret;
int result = TRUE;
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);
V(ret);
free(ret);
return result;
}
int shellMeFail(char *format, ...)
{
va_list va, va2;
int len;
char *ret;
int result;
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);
result = !WIFEXITED(system(ret));
free(ret);
return result;
}
int shellMe(char *format, ...)
{
va_list va, va2;
int len;
char *ret;
int result;
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);
result = system(ret);
free(ret);
return result;
}
int hasContents(char *file)
{
struct stat st;
if (0 == lstat(file, &st))
{
if (0 != st.st_size)
return TRUE;
}
return FALSE;
}
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;
}
static void bitch(reqData *Rd, char *message, char *log)
{
addStrL(Rd->errors, message);
E("%s %s %s - %s %s", getStrH(Rd->headers, "REMOTE_ADDR"), Rd->shs.UUID, Rd->shs.name, message, log);
}
/* "A token cookie that references a non-existent session, its value should be replaced immediately to prevent session fixation."
https://owasp.org/www-community/attacks/Session_fixation
Which describes the problem, but offers no solution.
See https://stackoverflow.com/questions/549/the-definitive-guide-to-form-based-website-authentication?rq=1.
I think this means send a new cookie.
I clear out the cookies and send blank ones with -1 maxAge, so they should get deleted.
*/
static void bitchSession(reqData *Rd, char *message, char *log)
{
if ('\0' != message[0])
addStrL(Rd->errors, message);
C("%s %s %s - %s %s", getStrH(Rd->headers, "REMOTE_ADDR"), Rd->shs.UUID, Rd->shs.name, message, log);
Rd->shs.status = SHS_BOGUS;
}
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);
}
}
typedef struct _qLua qLua;
struct _qLua
{
int type;
union
{
boolean b;
int i;
float f;
char *s;
qtreetbl_t *t;
} v;
};
qLua *qLuaGet(qtreetbl_t *tree, char *key)
{
return (qLua *) tree->get(tree, key, NULL, false);
}
void dumpLuaTree(qtreetbl_t *tree, char *name, int depth)
{
qtreetbl_obj_t obj0;
if (0 == depth)
fprintf(stderr, "DUMP Lua table %s -\n%s =\n{\n", name, name);
else
fprintf(stderr, "%*s[%s] =\n%*s{\n", depth * 2, "", name, depth * 2, "");
memset((void*)&obj0, 0, sizeof(obj0)); // must be cleared before call
tree->lock(tree); // lock it when thread condition is expected
while(tree->getnext(tree, &obj0, false) == true)
{
qLua *q0 = obj0.data;
switch (q0->type)
{
case LUA_TBOOLEAN : {fprintf(stderr, "%*s[%s] = %d,\n", (depth + 1) * 2, "", obj0.name, q0->v.b); break;}
case LUA_TINTEGER : {fprintf(stderr, "%*s[%s] = %d,\n", (depth + 1) * 2, "", obj0.name, q0->v.i); break;}
case LUA_TNUMBER : {fprintf(stderr, "%*s[%s] = %f,\n", (depth + 1) * 2, "", obj0.name, q0->v.f); break;}
case LUA_TSTRING : {fprintf(stderr, "%*s[%s] = %s,\n", (depth + 1) * 2, "", obj0.name, q0->v.s); break;}
case LUA_TTABLE : {dumpLuaTree(q0->v.t, obj0.name, depth + 1); break;}
default : {fprintf(stderr, "%*s[%s] ? ??,\n", (depth + 1) * 2, "", obj0.name); break;}
}
}
tree->unlock(tree);
fprintf(stderr, "%*s},\n", depth * 2, "");
}
qtreetbl_t *lua2tree()
{
qtreetbl_t *ret = qtreetbl(0);
if (NULL != ret)
{
qLua q;
lua_pushnil(L); // +1 nil, first key
while(lua_next(L, -2) != 0) // -1 key, +2 next key, value (or 0 if nothing left in table)
{
// stack now contains: -1 => value; -2 => key; -3 => table
// copy the key so that lua_tostring does not modify the original
lua_pushvalue(L, -2);
// stack now contains: -1 => key; -2 => value; -3 => key; -4 => table
char *n = (char *) lua_tostring(L, -1); // 0, modifies key copy
int t = lua_type(L, -3);
// Correct the sort order. Assuming we will never have more than 999,999 sims. Think that's a safe assumption.
/* https://www.hypergridbusiness.com/2021/07/opensim-land-growth/
"Total area increased by the equivalent of 2,252 standard regions, for a new record high of 103,625 regions.
That’s nearly four times the area of Second Life,"
*/
if ((LUA_TINTEGER == t) || (LUA_TNUMBER == t))
{
char *tmp = xmprintf("%6s", n);
n = tmp;
}
q.type = lua_type(L, -2); // 0
switch(q.type)
{
case LUA_TBOOLEAN : {q.v.b = lua_toboolean(L, -2); break;}
case LUA_TINTEGER : {q.v.i = lua_tointeger(L, -2); break;}
case LUA_TNUMBER : {q.v.f = lua_tonumber(L, -2); break;}
case LUA_TSTRING : {q.v.s = (char *) lua_tostring(L, -2); break;}
case LUA_TTABLE : {lua_pushvalue(L, -2); q.v.t = lua2tree(); lua_pop(L, 1); break;}
default :
{
q.v.s = (char *) lua_tostring(L, -2); // 0
E("Unknown Lua variable type for %s = %s is %d", n, q.v.s, q.type);
break;
}
}
ret->put(ret, n, &q, sizeof(qLua));
if ((LUA_TINTEGER == t) || (LUA_TNUMBER == t))
free(n);
// pop value + copy of key, leaving original key
lua_pop(L, 2); // -2 value and copy of key
// stack now contains: -1 => key; -2 => table
}
}
else
{
D("No memory left.");
perror_msg("Unable to allocate memory");
}
return ret;
}
qtreetbl_t *Lua2tree(char *file, char *var)
{
qtreetbl_t *ret = NULL;
struct stat st;
if (0 != lstat(file, &st))
{
D("No %s file.", file);
perror_msg("Unable to stat %s", file);
}
else
{
if (luaL_loadfile(L, file)) // +1 the chunk or an error message. If something went wrong, error message is at the top of the stack.
E("Couldn't load Lua file: %s", lua_tostring(L, -1)); // 0
else
{
if (lua_pcall(L, 0, LUA_MULTRET, 0)) // +1 result or an error message. Except we get 0 or error?
{
E("Failed to run Lua script: %s", lua_tostring(L, -1)); // 0
lua_pop(L, 1); // -1 chunk or error message
}
else
{
lua_getglobal(L, var); // +1 the value of var
ret = lua2tree();
lua_pop(L, 1); // -1 var
}
}
lua_pop(L, 1); // -1 chunk or error message
}
return ret;
}
void freeLuaTree(qtreetbl_t *tree)
{
qtreetbl_obj_t obj0;
memset((void*)&obj0, 0, sizeof(obj0)); // must be cleared before call
tree->lock(tree); // lock it when thread condition is expected
while(tree->getnext(tree, &obj0, false) == true)
{
qLua *q0 = obj0.data;
if (LUA_TTABLE == q0->type)
freeLuaTree(q0->v.t);
}
tree->unlock(tree);
tree->free(tree);
}
int Lua2hashtbl(char *file, qhashtbl_t *hash, char *var)
{
int ret = FALSE;
struct stat st;
if (0 != lstat(file, &st))
{
D("No %s file.", file);
perror_msg("Unable to stat %s", file);
return FALSE;
}
else
{
int status = luaL_loadfile(L, file); // +1 the chunk or an error message.
if (status) // If something went wrong, error message is at the top of the stack.
E("Couldn't load Lua file: %s", lua_tostring(L, -1)); // 0
else
{
int result = lua_pcall(L, 0, LUA_MULTRET, 0); // +1 result or an error message. Except we get 0 or error?
if (result)
{
E("Failed to run Lua script: %s", lua_tostring(L, -1)); // 0
lua_pop(L, 1); // -1 chunk or error message
}
else
{
lua_getglobal(L, var); // +1 the value of var
lua_pushnil(L); // +1 nil, first key
while(lua_next(L, -2) != 0) // -1 key, +2 next key, value (or 0 if nothing left in table)
{
char *n = (char *) lua_tostring(L, -2); // 0
// Numbers can convert to strings, so check for numbers before checking for strings.
// On the other hand, strings that can be converted to numbers also pass lua_isnumber(). sigh
if (lua_isnumber(L, -1)) // 0
{
float v = lua_tonumber(L, -1); // 0
hash->put(hash, n, &v, sizeof(float));
}
else if (lua_isstring(L, -1)) // 0
hash->putstr(hash, n, (char *) lua_tostring(L, -1)); // 0
else if (lua_isboolean(L, -1)) // 0
{
int v = lua_toboolean(L, -1); // 0
hash->putint(hash, n, v);
}
else
{
char *v = (char *) lua_tostring(L, -1); // 0
E("Unknown Lua variable type for %s = %s", n, v);
}
lua_pop(L, 1); // -1 value
}
lua_pop(L, 1); // -1 var
ret = TRUE;
}
}
}
lua_pop(L, 1); // -1 chunk or error message
return ret;
}
int LuaToHash(reqData *Rd, char *file, char *var, qhashtbl_t *tnm, int ret, struct stat *st, struct timespec *now, char *type)
{
struct timespec then;
if (-1 == clock_gettime(CLOCK_REALTIME, &then))
perror_msg("Unable to get the time.");
I("Reading %s file %s", type, file);
if (!Lua2hashtbl(file, tnm, var))
{
bitch(Rd, "Broken thing.", "Can't run file.");
ret++;
}
else
{
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("Reading %s file took %lf seconds", type, (n - t) / 1000000000.0);
}
return ret;
}
boolean writeLuaDouble(reqData *Rd, int fd, char *file, char *name, double number)
{
boolean ret = TRUE;
// 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.
char *t = xmprintf(" ['%s'] = '%f',\n", name, number); // NOTE - default precision is 6 decimal places.
size_t l = strlen(t);
if (l != writeall(fd, t, l))
{
perror_msg("Writing %s", file);
ret = FALSE;
}
free(t);
return ret;
}
boolean writeLuaInteger(reqData *Rd, int fd, char *file, char *name, long number)
{
boolean ret = TRUE;
// 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.
char *t = xmprintf(" ['%s'] = '%ld',\n", name, number);
size_t l = strlen(t);
if (l != writeall(fd, t, l))
{
perror_msg("Writing %s", file);
ret = FALSE;
}
free(t);
return ret;
}
boolean writeLuaString(reqData *Rd, int fd, char *file, char *name, char *string)
{
boolean ret = TRUE;
if (NULL == string)
string = getStrH(Rd->stuff, name);
size_t l = strlen(string);
char *t0 = xmalloc(l * 2 + 1);
int i, j = 0;
// TODO - maybe escape other non printables as well?
for (i = 0; i < l; i++)
{
// We don't need to escape [] here, coz we are using '' below. Same applies to ", but do it anyway.
switch(string[i])
{
case '\n':
case '\\':
case '\'':
case '"':
t0[j++] = '\\'; break;
}
if ('\n' == string[i])
t0[j++] = 'n';
else if ('\r' == string[i])
;
else
t0[j++] = string[i];
}
t0[j] = '\0';
char *t1 = xmprintf(" ['%s'] = '%s',\n", name, t0);
l = strlen(t1);
if (l != writeall(fd, t1, l))
{
perror_msg("Writing %s to %s", name, file);
ret = FALSE;
}
free(t1);
free(t0);
return ret;
}
void doTmuxCmd(char *format, ...)
{
va_list va, va2;
int len;
char *ret;
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);
if (shellMeFail("%s %s/%s %s", Tcmd, scRun, Tsocket, ret))
E("tmux command failed! %s", ret);
free(ret);
}
void sendTmuxCmd(char *dest, char *cmd)
{
doTmuxCmd("send-keys -t %s:'%s' '%s' Enter", Tconsole, dest, cmd);
}
void sendTmuxKeys(char *dest, char *keys)
{
doTmuxCmd("send-keys -t %s:%s '%s'", Tconsole, dest, keys);
}
void waitTmuxText(char *dest, char *text)
{
int i;
// Using " for the grep pattern, coz ' might be used in a sim name.
// TODO - should escape \ " ` in text.
char *c = xmprintf("sleep 1; %s %s/%s capture-pane -Jt %s -p | grep -E \"%s\" 2>&1 > /dev/null", Tcmd, scRun, Tsocket, dest, text);
T("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));
// A sim structure for holding all the stuff from the sim.ini file.
typedef struct _simData simData;
struct _simData
{
// portH is the HTTP port for the sim, portU is the UDP port for the sim.
int locX, locY, sizeX, sizeY, sizeZ, portH, portU, maxPrims;
char *name, *tab, *UUID, *regionType, *estate, *owner;
char *paneID;
int window, pane, users;
};
typedef struct _simList simList;
struct _simList
{
int len, num;
char **sims;
qtreetbl_t *byTab, *simsLua, *unsorted, *byUUID;
// Stuff for the looping through sims, doing things, and waiting.
int doIt;
float la;
char *target, *backup;
};
simList *ourSims = NULL;
static int getIntFromIni(qlisttbl_t *ini, char *name)
{
int ret;
char *t = "0";
t = ini->getstr(ini, name, false);
if (NULL == t)
t = "0";
else if ('"' == t[0])
t = qstrunchar(t, '"', '"');
return strtol(t, NULL, 10);
}
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;
}
/*
// We particularly don't want \ " `
char *cleanSimName(char *name)
{
size_t l = strlen(name);
char *ret = xmalloc(l + 1);
int i, j = 0;
for (i = 0; i < l; i++)
{
char r = name[i];
if ((' ' == r) || (isalnum(r) != 0))
ret[j++] = r;
}
ret[j] = '\0';
return ret;
}
*/
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)
{
strncpy((char *) node->parent->extra, node->name, l - 4);
return DIRTREE_ABORT;
}
return 0;
}
static int filterShinis(struct dirtree *node)
{
if (!node->parent) return DIRTREE_RECURSE | DIRTREE_SHUTUP;
int l = strlen(node->name);
if ((6 < l) && ((strncmp(&(node->name[l - 6]), ".shini", 6) == 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] = xstrndup(node->name, l - 6);
list->num++;
}
return 0;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// Sim wrangling loop.
////////////////////////////////////////////////////////////////////////////////////////////////////
typedef void (*simFunction)(simData *simd, char *sim, char *type, int count, int window, int panes, int pane);
void forEachSim(char *verb, simFunction func, simFunction not)
{
qtreetbl_obj_t obj0, obj1;
qLua *q0, *q1;
int count = 0, window = 0, panes = 4, pane = 0;
memset((void*)&obj0, 0, sizeof(obj0));
ourSims->simsLua->lock(ourSims->simsLua);
while(ourSims->simsLua->getnext(ourSims->simsLua, &obj0, false) == true)
{
q0 = obj0.data;
char *type = "unsorted";
if (LUA_TTABLE == q0->type)
{
panes = 4;
q1 = q0->v.t->get(q0->v.t, "number", NULL, false); if (NULL != q1) window = q1->v.f - 1;
q1 = q0->v.t->get(q0->v.t, "panes", NULL, false); if (NULL != q1) panes = q1->v.f;
q1 = q0->v.t->get(q0->v.t, "type", NULL, false); if (NULL != q1) type = q1->v.s;
if (0 == panes)
{
pane = 3;
window = 0;
type = Ttab;
}
else if (0 != pane)
{
pane = 0;
window++;
}
if (verb)
V("%s sims of type %s, window %d, %d panes per window.", verb, type, window, panes);
memset((void*)&obj1, 0, sizeof(obj1));
q0->v.t->lock(q0->v.t);
while(q0->v.t->getnext(q0->v.t, &obj1, false) == true)
{
q1 = obj1.data;
if ((strcmp("number", obj1.name) != 0) && (strcmp("panes", obj1.name) != 0) && (strcmp("type", obj1.name) != 0))
{
simData *simd = ourSims->byTab->get(ourSims->byTab, q1->v.s, NULL, false);
if (NULL == simd)
{
if (not)
not(simd, q1->v.s, type, count, window, panes, pane);
else
E("Sim %s not found in ini list!", q1->v.s);
}
else
{
func(simd, q1->v.s, type, count, window, panes, pane);
count++;
pane++;
if (pane >= panes)
{
pane = 0;
window++;
}
}
}
}
q0->v.t->unlock(q0->v.t);
}
}
ourSims->simsLua->unlock(ourSims->simsLua);
}
void simNotFound(simData *simd, char *sim, char *type, int count, int window, int panes, int pane)
{
char *path = xmprintf("%s/%s.shini", scEtc, sim);
qlisttbl_t *ini = qconfig_parse_file(NULL, path, '=');
if (NULL == ini)
E("Can't find %s", path);
else
{
simd = xzalloc(sizeof(simData));
simd->tab = sim;
simd->name = qstrunchar(ini->getstr(ini, "Region.RegionName", true), '"', '"');
simd->UUID = qstrunchar(ini->getstr(ini, "Region.RegionUUID", true), '"', '"');
// simd->regionType = qstrunchar(ini->getstr(ini, "Region.RegionType", true), '"', '"');
simd->sizeX = getIntFromIni(ini, "Region.SizeX");
simd->sizeY = getIntFromIni(ini, "Region.SizeY");
simd->sizeZ = getIntFromIni(ini, "Region.SizeZ");
// TODO - store a pointer instead of multiple copies of the data.
ourSims->byTab->put(ourSims->byTab, sim, simd, sizeof(simData));
ourSims->byUUID->put(ourSims->byUUID, simd->UUID, simd, sizeof(simData));
if (strcmp("unsorted", type) == 0)
ourSims->unsorted->put(ourSims->unsorted, sim, simd, sizeof(simData));
ini->free(ini);
free(simd);
}
free(path);
}
void freeSimList(simList *sims)
{
int i;
if (NULL == sims)
return;
for (i = 0; i < sims->num; i++)
free(sims->sims[i]);
free(sims->sims);
qtreetbl_obj_t obj;
memset((void*) &obj, 0, sizeof(obj)); // start from the minimum.
sims->byTab->lock(sims->byTab);
while (sims->byTab->getnext(sims->byTab, &obj, false) == true)
{
char *name = qmemdup(obj.name, obj.namesize); // keep the name
size_t namesize = obj.namesize; // for removal argument
{
simData *simd = (simData *) obj.data;
if (NULL != simd)
{
free(simd->name);
// free(simd->tab);
free(simd->UUID);
// free(simd->regionType);
free(simd->estate);
free(simd->owner);
free(simd->paneID);
}
}
// sims->byTab->remove_by_obj(sims->byTab, obj.name, obj.namesize); // remove
// obj = sims->byTab->find_nearest(sims->byTab, name, namesize, false); // rewind one step back
free(name); // clean up
}
sims->byTab->unlock(sims->byTab);
sims->byTab->free(sims->byTab);
freeLuaTree(sims->simsLua);
freeLuaTree(sims->unsorted);
freeLuaTree(sims->byUUID);
free(sims->target);
free(sims->backup);
free(sims);
}
// Get the details from all the .ini or .shini sim files.
// Creates the .shini and sims.lua files if it's an old install.
simList *getSims()
{
if (NULL != ourSims) return ourSims;
int i, fd = -1;
size_t l;
char *file = xmprintf("%s/sims.lua", scEtc);
ourSims = xzalloc(sizeof(simList));
ourSims->byTab = qtreetbl(0);
ourSims->unsorted = qtreetbl(0);
ourSims->byUUID = qtreetbl(0);
ourSims->doIt = FALSE;
// Read or create simsLua
ourSims->simsLua = Lua2tree(file, "sims");
if (NULL == ourSims->simsLua)
{
ourSims->simsLua = qtreetbl(0);
ourSims->doIt = TRUE;
}
// Find all the old .ini files, convert them to .shini files if they don't exist.
char *path = xmprintf("%s/config", scRoot), *newPath;
struct dirtree *new = dirtree_add_node(0, path, 0);
new->extra = (long) ourSims;
dirtree_handle_callback(new, filterSims);
free(path);
for (i = 0; i < ourSims->num; i++)
{
char *sim = ourSims->sims[i], *name = xmprintf("%s/config/%s", scRoot, sim);
qlisttbl_t *ini;
struct dirtree *new = dirtree_add_node(0, name, 0);
free(name);
name = xzalloc(1024);
new->extra = (long) name;
dirtree_handle_callback(new, filterInis);
if ('\0' != name[0])
{
path = xmprintf("%s/config/%s/%s.ini", scRoot, sim, name);
newPath = xmprintf("%s/%s.shini", scEtc, name);
if (!qfile_exist(newPath))
{
char *cmd = xmprintf("sed -E"
" -e 's#\\[Const]#\\[Const] ; fakeVariableCozOpenSim='' ; pushd ../current/bin; ./sledjchisl $1 `basename $0`; popd ; exit 0#'"
" -e 's/mysim=\"[[:digit:]]*\"/mysim=\"%s\"/'"
" -e 's/sim\\$\\{Const\\|mysim\\}/\\$\\{Const\\|mysim\\}/g'"
" -e '/\\[Startup\\]/d'"
" -e '/PIDFile.*/d'"
" -e '/LogFile.*/d'"
" -e '/StatsLogFile.*/d'"
" -e '/ConsoleHistoryFile.*/d'"
" %s >%s", name, path, newPath);
V("Converting %s.ini -> %s", path, newPath);
D(cmd);
if (shellMeFail(cmd))
E("sed command failed!");
else
{
if (shellMeFail("chmod ugo+x %s", newPath))
E("chmod command failed!");
char *link = xmprintf("%s/%s.shini", scBin, name);
I("Symlinking %s to %s", newPath, link);
if (qfile_exist(link))
{
if (shellMeFail("rm %s", link))
E("rm command failed!");
}
if (0 != symlink(newPath, link))
perror_msg("Symlinking %s to %s", newPath, link);
free(link);
}
free(cmd);
}
free(newPath);
free(path);
}
free(name);
}
// dumpLuaTree(ourSims->simsLua, "simsLua", 0);
// dumpLuaTree(ourSims->byTab, "byTab", 0);
// Read all the .shini files.
forEachSim(NULL, NULL, simNotFound);
// dumpLuaTree(ourSims->simsLua, "simsLua", 0);
// dumpLuaTree(ourSims->byTab, "byTab", 0);
// dumpLuaTree(ourSims->unsorted, "unsorted", 0);
// Check for any .shini files not in sims.lua, add them to the unsorted list.
for (i = 0; i < ourSims->num; i++)
free(ourSims->sims[i]);
ourSims->num = 0;
new = dirtree_add_node(0, scEtc, 0);
new->extra = (long) ourSims;
dirtree_handle_callback(new, filterShinis);
for (i = 0; i < ourSims->num; i++)
{
simData *simd = ourSims->byTab->get(ourSims->byTab, ourSims->sims[i], NULL, false);
if (NULL == simd)
{
V("Shini NOT found in list - %s/%s.shini", scEtc, ourSims->sims[i]);
simNotFound(simd, ourSims->sims[i], "unsorted", 0, 0, 0, 0);
ourSims->doIt = TRUE;
}
}
// dumpLuaTree(ourSims->simsLua, "simsLua", 0);
// dumpLuaTree(ourSims->byTab, "byTab", 0);
// dumpLuaTree(ourSims->unsorted, "unsorted", 0);
// Write the sims.lua_NEW file if needed.
if (ourSims->doIt)
{
char *tnm;
if (shellMeFail("mv -f %s/sims.lua %s/sims.lua.BACKUP", scEtc, scEtc))
E("mv command failed!");
free(file);
file = xmprintf("%s/sims.lua", scEtc);
I("Creating sims %s.", file);
fd = notstdio(xcreate_stdio(file, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, S_IRUSR | S_IWUSR));
if (-1 != fd)
{
tnm = "sims = -- Note these are .shini / tmux tab short names.\n{\n";
l = strlen(tnm);
if (l != writeall(fd, tnm, l))
perror_msg("Writing %s", file);
qtreetbl_obj_t obj0, obj1;
qLua *q0, *q1;
memset((void*)&obj0, 0, sizeof(obj0));
ourSims->simsLua->lock(ourSims->simsLua);
while(ourSims->simsLua->getnext(ourSims->simsLua, &obj0, false) == true)
{
q0 = obj0.data;
char *type = "unsorted";
if (LUA_TTABLE == q0->type)
{
tnm = " {";
l = strlen(tnm);
if (l != writeall(fd, tnm, l))
perror_msg("Writing %s", file);
q1 = q0->v.t->get(q0->v.t, "type", NULL, false);
if (NULL != q1)
{
type = q1->v.s;
tnm = xmprintf("['type'] = '%s'; ", type);
l = strlen(tnm);
if (l != writeall(fd, tnm, l))
perror_msg("Writing %s", file);
free(tnm);
}
q1 = q0->v.t->get(q0->v.t, "number", NULL, false);
if (NULL != q1)
{
tnm = xmprintf("['number'] = %d; ", (int) q1->v.f);
l = strlen(tnm);
if (l != writeall(fd, tnm, l))
perror_msg("Writing %s", file);
free(tnm);
}
q1 = q0->v.t->get(q0->v.t, "panes", NULL, false);
if (NULL != q1)
{
tnm = xmprintf("['panes'] = %d; ", (int) q1->v.f);
l = strlen(tnm);
if (l != writeall(fd, tnm, l))
perror_msg("Writing %s", file);
free(tnm);
}
if (strcmp("unsorted", type) != 0)
{
memset((void*)&obj1, 0, sizeof(obj1));
q0->v.t->lock(q0->v.t);
while(q0->v.t->getnext(q0->v.t, &obj1, false) == true)
{
q1 = obj1.data;
if ((strcmp("number", obj1.name) != 0) && (strcmp("panes", obj1.name) != 0) && (strcmp("type", obj1.name) != 0))
{
tnm = xmprintf("\n '%s',", q1->v.s);
l = strlen(tnm);
if (l != writeall(fd, tnm, l))
perror_msg("Writing %s", file);
free(tnm);
}
}
q0->v.t->unlock(q0->v.t);
}
else
{
memset((void*)&obj1, 0, sizeof(obj1));
ourSims->unsorted->lock(ourSims->unsorted);
while(ourSims->unsorted->getnext(ourSims->unsorted, &obj1, false) == true)
{
simData *simd = obj1.data;
if ((strcmp("number", obj1.name) != 0) && (strcmp("panes", obj1.name) != 0) && (strcmp("type", obj1.name) != 0))
{
tnm = xmprintf("\n '%s',", simd->tab);
l = strlen(tnm);
if (l != writeall(fd, tnm, l))
perror_msg("Writing %s", file);
free(tnm);
}
}
ourSims->unsorted->unlock(ourSims->unsorted);
}
}
tnm = "\n },\n";
l = strlen(tnm);
if (l != writeall(fd, tnm, l))
perror_msg("Writing %s", file);
}
ourSims->simsLua->unlock(ourSims->simsLua);
tnm = "}\nreturn sims\n";
l = strlen(tnm);
if (l != writeall(fd, tnm, l))
perror_msg("Writing %s", file);
xclose(fd);
}
freeSimList(ourSims);
free(file);
// Reread everything.
file = xmprintf("%s/sims.lua", scEtc);
ourSims = xzalloc(sizeof(simList));
ourSims->byTab = qtreetbl(0);
ourSims->unsorted = qtreetbl(0);
ourSims->doIt = FALSE;
ourSims->simsLua = Lua2tree(file, "sims");
forEachSim(NULL, NULL, simNotFound);
// dumpLuaTree(ourSims->simsLua, "simsLua", 0);
// dumpLuaTree(ourSims->byTab, "byTab", 0);
// dumpLuaTree(ourSims->unsorted, "unsorted", 0);
}
free(file);
// dumpLuaTree(ourSims->simsLua, "simsLua", 0);
// dumpLuaTree(ourSims->byTab, "byTab", 0);
return ourSims;
}
// Expects either "simXX" or "ROBUST".
int checkSimIsRunning(char *sim)
{
int ret = 0;
struct stat st;
// Check if it's running.
char *path = xmprintf("%s/%s.pid", scRun, 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 >/dev/null", pid);
i = system(path);
if (WIFEXITED(i))
{
if (0 != WEXITSTATUS(i)) // No such pid.
{
free(path);
path = xmprintf("rm -f %s/%s.pid", scRun, sim);
d("%s", path);
i = system(path);
if (WIFEXITED(i))
{
if (0 != WEXITSTATUS(i)) // No such pid.
E("Cannot remove stale PID file %s", path);
}
}
else
d("checkSimIsRunning(%s) has PID %s, which is actually running.", sim, pid);
}
// TODO - WTF?
// free(path);
}
}
free(pid);
}
// Now check if it's really really running. lol
free(path);
path = xmprintf("%s/%s.pid", scRun, 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;
}
void findWindow(simData *simd, char *sim, char *type, int count, int window, int panes, int pane)
{
snprintf(toybuf, sizeof(toybuf), "%s/IDs_%d.lua", scTemp, window);
qtreetbl_t *IDs = Lua2tree(toybuf, "IDs");
snprintf(toybuf, sizeof(toybuf), "%d %d", window, pane);
free(simd->paneID);
simd->paneID = xmprintf("%s", qLuaGet(IDs, toybuf)->v.s);
freeLuaTree(IDs);
if (strcmp(backupIARsim, sim) == 0)
{
ourSims->backup = xstrdup(simd->paneID);
V("Found backup sim %s in %s", sim, simd->paneID);
}
}
void prepSims(simData *simd, char *sim, char *type, int count, int window, int panes, int pane)
{
char *path = xmprintf("%s/%s.shini", scEtc, simd->tab);
char *newPath = xmprintf("%s/sim%d/%s.ini", scTemp, count, simd->tab); // Coz OpenSim sucks.
char *cmd;
simd->window = window;
simd->pane = pane;
// Make sure all tmux windows and panes are startned.
if (0 == panes)
{
// doTmuxCmd("split-window -hp 50 -d -t '%s:0.0'", Tconsole);
// doTmuxCmd("split-window -vp 50 -d -t '%s:0.1'", Tconsole);
// doTmuxCmd("split-window -vp 50 -d -t '%s:0.0'", Tconsole);
}
else if (0 == pane)
{
doTmuxCmd("new-window -dc '%s/current/bin' -n '%s' -t '%s:%d'", scRoot, type, Tconsole, window);
if (2 <= panes)
doTmuxCmd("split-window -hp 50 -d -t '%s:%d.0'", Tconsole, window);
if (4 <= panes)
{
doTmuxCmd("split-window -vp 50 -d -t '%s:%d.1'", Tconsole, window);
doTmuxCmd("split-window -vp 50 -d -t '%s:%d.0'", Tconsole, window);
}
if (8 <= panes)
{
doTmuxCmd("split-window -hp 50 -d -t '%s:%d.3'", Tconsole, window);
doTmuxCmd("split-window -hp 50 -d -t '%s:%d.2'", Tconsole, window);
doTmuxCmd("split-window -hp 50 -d -t '%s:%d.1'", Tconsole, window);
doTmuxCmd("split-window -hp 50 -d -t '%s:%d.0'", Tconsole, window);
}
}
// Create the temporary .ini files.
shellMe("echo 'IDs={' >%s/IDs_%d.lua", scTemp, window);
doTmuxCmd("list-panes -t %d -F \"['%d #{pane_index}'] = '#{window_id}.#{pane_id}',\" >> %s/IDs_%d.lua", window, window, scTemp, window);
shellMe("echo '}\nreturn IDs' >>%s/IDs_%d.lua", scTemp, window);
findWindow(simd, sim, type, count, window, panes, pane);
simd->portH = 8004 + count * 2;
simd->portU = 8005 + count * 2;
cmd = xmprintf("rm -fr %s/sim%d; mkdir -p %s/sim%d; sed -E"
" -e 's@\\[Region\\]@"
" paneID = \"\\%s\"\\n"
"\\[Startup\\]\\n"
" PIDFile = \"\\$\\{Paths\\|PIDPath\\}\\/\\$\\{Const\\|mysim\\}\\.pid\"\\n"
" LogFile = \"\\$\\{Paths\\|LogPath\\}\\/OpenSim_\\$\\{Const\\|mysim\\}\\.log\"\\n"
" StatsLogFile = \"\\$\\{Paths\\|LogPath\\}\\/OpenSimStats_\\$\\{Const\\|mysim\\}\\.log\"\\n"
" ConsoleHistoryFile = \"\\$\\{Paths\\|LogPath\\}\\/OpenSimConsoleHistory_\\$\\{Const\\|mysim\\}\\.txt\"\\n"
"\\n\\[Region\\]@'"
" -e 's/InternalPort[[:space:]]*=[[:space:]]*[[:digit:]]+/InternalPort = %d/'"
" -e 's/InternalPort[[:space:]]*=[[:space:]]*\"[[:digit:]]+\"/InternalPort = %d/'"
" -e 's/http_listener_port[[:space:]]*=[[:space:]]*[[:digit:]]+/http_listener_port = %d/'"
" -e 's/http_listener_port[[:space:]]*=[[:space:]]*\"[[:digit:]]+\"/http_listener_port = %d/'"
" %s >%s",
scTemp, count,
scTemp, count,
simd->paneID,
simd->portU,
simd->portU,
simd->portH,
simd->portH,
path, newPath);
D("Writing .ini file %s with ports %d %d", newPath, simd->portH, simd->portU);
if (shellMeFail(cmd))
E("sed command failed!");
free(cmd);
free(newPath);
free(path);
}
// Figure out where the sims are running.
void findSimsTmux(simData *simd, char *sim, char *type, int count, int window, int panes, int pane)
{
simd->window = window;
simd->pane = pane;
snprintf(toybuf, sizeof(toybuf), "%s/IDs_%d.lua", scTemp, window);
qtreetbl_t *IDs = Lua2tree(toybuf, "IDs");
snprintf(toybuf, sizeof(toybuf), "%d %d", window, pane);
simd->paneID = xmprintf("%s", qLuaGet(IDs, toybuf)->v.s);
freeLuaTree(IDs);
}
void startSim(simData *simd, char *sim, char *type, int count, int window, int panes, int pane)
{
if (!checkSimIsRunning(simd->tab))
{
I("Tmux tab [%d:%s](pane %d) tmux ID %s, from %s/sim%d - %s is starting.", window, type, pane, simd->paneID, scTemp, count, simd->name);
doTmuxCmd("select-pane -t %s:%s -T '%s'", Tconsole, simd->paneID, simd->tab);
snprintf(toybuf, sizeof(toybuf), "mono OpenSim.exe -inidirectory=%s/sim%d", scTemp, count);
sendTmuxCmd(simd->paneID, toybuf);
if (0 == ourSims->doIt)
{
snprintf(toybuf, sizeof(toybuf), "INITIALIZATION COMPLETE FOR %s", simd->name);
waitTmuxText(simd->paneID, toybuf);
I("%s is done starting up.", simd->name);
sendTmuxCmd(simd->paneID, "");
sendTmuxCmd(simd->paneID, "");
ourSims->la = waitLoadAverage(ourSims->la, loadAverageInc, simTimeOut);
if (1 < bulkSims)
bulkSims = bulkSims / 2;
}
else
{
// TODO - some compromise, do a premptive load check if we are not doing a wait anyway.
}
ourSims->doIt = ((count + 1) % bulkSims);
}
}
void stopSim(simData *simd, char *sim, char *type, int count, int window, int panes, int pane)
{
if (checkSimIsRunning(simd->tab))
{
// Leave empty panes as is.
V("Attempting to shut down sim %s.", simd->tab);
sendTmuxCmd(simd->paneID, "show users");
sleep(1);
if (shellMeFail("%s %s/%s capture-pane -Jt '%s' -p | grep -E '^Agents connected: [[:digit:]]*[[:space:]]*$' | tail -n 1 | cut -d ' ' -f 3 >%s/STOP_%s",
Tcmd, scRun, Tsocket, simd->paneID, scTemp, simd->tab))
E("Problem with shutting down sim %s - Failed find 'Agents connected'", simd->tab);
else
{
char *cmd = xmprintf("%s/STOP_%s", scTemp, simd->tab);
char *r = (char *) qfile_load(cmd, NULL);
if (r)
{
if ('0' == r[0])
sendTmuxCmd(simd->paneID, "quit");
else
{
r[strlen(r) - 1] = '\0';
simd->users = atoi(r);
sendTmuxCmd(simd->paneID, "alert Shutting down this sim, please leave.");
sendTmuxCmd(simd->paneID, "region restart bluebox \"Shutting down this sim, please leave.\" 300");
sendTmuxCmd(simd->paneID, "region restart notice \"Shutting down this sim, please leave.\" 300");
sendTmuxCmd(simd->paneID, "region restart abort \"Shutting down this sim, please leave.\"");
W("Not shutting down %s coz %d people are still on it. They have been warned.", simd->tab, simd->users);
}
free(r);
}
else
E("Problem with shutting down sim %s - Failed to read file %s", simd->tab, cmd);
free(cmd);
}
// There's three things that might happen -
// Sim will quit, tmux pane will go away before we can see the shutdown message.
// pane-exited hook might be useful here.
// Sim will quit, tmux pane wont go away. Dunno yet if waitTmuxText() will still work.
// pane-died hook might be useful here.
// Sim fails to quit, error message on the pane that we want to read, wont see the shutdown message.
// Also sim might not have finished starting up, and may even not be accepting comands yet.
// TODO - cater for and test them all.
// Could also loop on checkSimIsRunning(sim)
// snprintf(toybuf, sizeof(toybuf), "[SHUTDOWN]: Shutdown processing on main thread complete. Exiting...");
// waitTmuxText(nm, toybuf);
// I("%s is done stopping.", simd->name);
// la = waitLoadAverage(la, loadAverageInc, simTimeOut);
}
}
typedef struct _ARList ARList;
struct _ARList
{
int len, num;
char **ARs, *this;
};
static int filterARs(struct dirtree *node)
{
if (!node->parent) return DIRTREE_RECURSE | DIRTREE_SHUTUP;
ARList *list = (ARList *) node->parent->extra;
int l0 = strlen(node->name), l1 = strlen(list->this);
// TODO - ignore zero sized ones.
if ((4 < l0) && ((strncmp(&(node->name[l0 - 4]), ".iar", 4) == 0) || (strncmp(&(node->name[l0 - 4]), ".oar", 4) == 0)) && (strncmp(node->name, list->this, l1) == 0))
{
if ((list->num + 1) > list->len)
{
list->len = list->len + 1;
list->ARs = xrealloc(list->ARs, list->len * sizeof(char *));
}
list->ARs[list->num] = xstrdup(node->name);
list->num++;
}
return 0;
}
// Forward declare this.
my_ulonglong dbCount(char *table, char *where);
void doSimsThing(simData *simd, char *sim, char *type, int count, int window, int panes, int pane)
{
// Check if only doing a single sim.
int cont = FALSE, member = false;
char *last = strchr(sim, ' ');
if (FLAG(m))
{
// check if it's a real user.
last[0] = '\0'; last++;
// Double check it's a real member.
snprintf(toybuf, sizeof(toybuf), "FirstName='%s' and LastName='%s'", sim, last);
if (1 == dbCount("UserAccounts", toybuf))
member = TRUE;
else
E("Can't find member %s %s.", sim, last);
}
else if (NULL != ourSims->target)
{
/* TODO - maybe?
byTab has the short name as the key, simData as the value.
Iterate through it looking for target sims if specified in the sledjchisl arguments.
Which can be short name, long name, foo.shini file name.
Though short name is a direct lookup, and "foo.shini" we can strip off the .shini and direct lookup the short name.
So only need to iterate if it's a long name.
Keep in mind the argument might be a user name for IAR backups. Or perhaps a UUID.
*/
cont = TRUE;
snprintf(toybuf, sizeof(toybuf), "%s.shini", simd->tab);
if
(
(strcmp(ourSims->target, simd->name) == 0) ||
(strcmp(ourSims->target, simd->tab) == 0) ||
(strcmp(ourSims->target, toybuf) == 0)
)
cont = FALSE;
snprintf(toybuf, sizeof(toybuf), "%s.ini", simd->tab);
if (strcmp(ourSims->target, toybuf) == 0)
cont = FALSE;
snprintf(toybuf, sizeof(toybuf), "sim%d.ini", count);
if (strcmp(ourSims->target, toybuf) == 0)
cont = FALSE;
}
if (cont)
return;
switch (currentMode)
{
case START : // "start 'Welcome sim'" "start Welcome.shini" "Welcome.shini" "start Welcome.ini" "start Welcome" "start" start everything
{
startSim(simd, sim, type, count, window, panes, pane);
break;
}
case BACKUP : // "backup -m 'onefang rejected'" "backup 'Welcome sim'" "backup Welcome.shini" "Welcome.shini backup" "backup Welcome.ini" "backup Welcome" "backup" backup everything
{
struct timeval tv;
time_t curtime;
char date[DATE_TIME_LEN];
gettimeofday(&tv, NULL);
curtime = tv.tv_sec;
strftime(date, DATE_TIME_LEN, "%F_%T", localtime(&curtime));
if (FLAG(m))
{
if (member)
{
I("Member %s %s is being backed up to %s/%s_%s-%s.iar in tmux windo %s.", sim, last, scBackup, sim, last, date, ourSims->backup);
snprintf(toybuf, sizeof(toybuf), "save iar -c %s %s / password %s/%s_%s-%s.iar", sim, last, scBackup, sim, last, date);
if (NULL != ourSims->backup)
{
if ('\0' == rSync[0])
{
I("Running gitar on %s %s", sim, last);
shellMeFail("%s/current/bin/sledjchisl gitar -m %s %s %s", scRoot, FLAG(v) ? "-v" : "", sim, last);
}
sendTmuxCmd(ourSims->backup, toybuf);
// if (0 == do)
{
snprintf(toybuf, sizeof(toybuf), "Saved archive with [[:digit:]]+ items for %s %s", sim, last);
waitTmuxText(ourSims->backup, toybuf);
I("%s %s is done backing up.", sim, last);
sendTmuxCmd(ourSims->backup, "");
sendTmuxCmd(ourSims->backup, "");
if ('\0' != rSync[0])
{
if (shellMeFail("time ionice -c3 nice -n 19 rsync -Ha -R --modify-window=2 --partial --port=%d --remove-source-files %s/*.iar %s",
rSyncPort, scBackup, rSync))
E("rsync failed");
}
ourSims->la = waitLoadAverage(ourSims->la, loadAverageInc, simTimeOut);
}
}
else
E("Can't find backup sim.");
}
}
else if (checkSimIsRunning(simd->tab))
{
// TODO - should collect names of existing backups, both tmux / ini name and proper name.
// Scan backups directory once before this for loop, add details to sims list.
// strip off the last bit of file name (YYYY-mm-dd_HH:MM:SS.oar) to get the name
// keep in mind some old files had munged names like "Tiffanie_s_Paradise-2021-06-23_05:11:38.oar"
I("Sim %s is being backed up to %s/backups/%s-%s.oar.", simd->name, scRoot, simd->tab, date);
if ('\0' == rSync[0])
{
I("Running gitar on %s", simd->tab);
shellMeFail("%s/current/bin/sledjchisl gitar %s %s", scRoot, FLAG(v) ? "-v" : "", simd->tab);
}
snprintf(toybuf, sizeof(toybuf), "save oar --all %s/%s-%s.oar", scBackup, simd->tab, date);
sendTmuxCmd(simd->paneID, toybuf);
// if (0 == do)
{
snprintf(toybuf, sizeof(toybuf), "Finished writing out OAR for %s", simd->name);
waitTmuxText(simd->paneID, toybuf);
I("%s is done backing up.", simd->name);
sendTmuxCmd(simd->paneID, "");
sendTmuxCmd(simd->paneID, "");
if ('\0' != rSync[0])
{
if (shellMeFail("time ionice -c3 nice -n 19 rsync -Ha -R --modify-window=2 --partial --port=%d --remove-source-files %s/*.oar %s",
rSyncPort, scBackup, rSync))
E("rsync failed");
}
ourSims->la = waitLoadAverage(ourSims->la, loadAverageInc, simTimeOut);
}
}
break;
}
case GITAR : // "gitAR -m avatar name" "gitAR sim name"
{
/* Work around OpenSims slow database corruption bug by using git to store all old backups.
Try to squeeze every last byte out of the tarballs. Seems to cut the total storage size down to one third the size of just the raw I/OAR files.
Saves even more if there's been no changes.
On the other hand, these backup files will grow indefinately, the more changes, the faster it grows. I can live with that for more reliable backups that go back further.
Tries to avoid loosing data if things go wrong. I think the main remaining problem would be running out of space, in which case you have bigger problems to deal with.
Strategy - unpack the last one, unpack and commit any old I/OARs, pack up the result, delete it's working directory, THEN run the save i/oar.
Avoids having to sync with OpenSim finishing the current I/OAR, and as a bonus, an easy to deliver latest I/OAR for people that want it.
*/
char *name = xstrdup(sim);
if (FLAG(m))
{
if (member)
{
free(name);
name = xmprintf("%s_%s", sim, last);
}
else
{
free(name);
return;
}
}
char type = FLAG(m) ? 'I' : 'O';
char *gar = xmprintf("%s-git%cAR", name, type), *gtr = xmprintf("%s/%s.tar.xz", scBackup, gar);
char *dir = xmprintf("%s/temp_%c_%s", scBackup, type, name);
char *glog = xmprintf("%s/log.log", scBackup), *gerr = xmprintf("%s/errors", dir);
// Make sure stuff that's already compressed doesn't get compressed by git.
// Also tries to protect binaries from mangling.
char *gab = xmprintf("%s/%s/.gitattributes", dir, gar), *gad =
"*.bvh -delta -diff -text\n"
"*.jp2 -delta -diff -text\n"
"*.jpg -delta -diff -text\n"
"*.llmesh -delta -diff -text\n"
"*.ogg -delta -diff -text\n"
"*.png -delta -diff -text\n"
"*.r32 -delta -diff -text\n"
"*.tga -delta -diff -text\n";
ARList *ourARs = xzalloc(sizeof(ARList));
if (qfile_exist(dir))
{
if (shellMeFail("echo 'Mess left over from last backup in %d, not gonna run!' >>%s", dir, gerr)) E("Cleaning up the mess!");
if (shellMeFail("mv %s/*.%car %s 2>&1 >>%s", dir, tolower(type), scBackup, gerr)) E("Failed cleaning up the mess!");
goto gitARend;
}
// Either unpack the old gitAR, or create a new one.
qfile_mkdir(dir, S_IRWXU | S_IRGRP | S_IXGRP, true);
if (qfile_exist(gtr))
{
I("Unpacking %s", gtr);
if (shellMeFail("cd %s; ionice -c3 nice -n 19 tar -xf %s >>%s", dir, gtr, gerr)) E("Failed to unpack %s!", gtr);
char *t = xmprintf("%s/%s_git%cAR", dir, name, type);
// Changed from _ to -, but deal with old archives.
if (qfile_exist(t))
{
if (shellMeFail("mv %s %s/%s", t, dir, gar)) E("Failed to move %s!", t);
}
free(t);
}
else
{
qfile_mkdir(gar, S_IRWXU | S_IRGRP | S_IXGRP, true);
// git will create gar for us.
if (shellMeFail("cd %s; git init --quiet %s >>%s", dir, gar, gerr)) E("Failed to git init %s/%s!", dir, gar);
// Coz git insists.
if (shellMeFail("cd %s/%s; git config user.email \"opensim@$(hostname -A | cut -d ' ' -f 1)\"", dir, gar)) E("Failed to git config user.email!");
if (shellMeFail("cd %s/%s; git config user.name \"opensim\"", dir, gar)) E("Failed to git config user.name!");
// Coz git insists on having something comitted before it can change the branch name.
if (-1 == qfile_save(gab, gad, strlen(gad), false)) E("Faild to write %s file!", gad);
if (shellMeFail("cd %s/%s; git add .gitattributes >>%s", dir, gar, gerr)) E("Failed to git add!");
V("Committing initial git.");
if (shellMeFail("cd %s/%s; git commit -qm \"Initial commit\" >>%s || echo \"ERROR - Could not commit!\" >>%s",
dir, gar, glog, gerr)) E("Failed to git commit!");
if (shellMeFail("cd %s/%s; git branch -m master Domme >>%s", dir, gar, gerr)) E("Failed to git branch -m master Domme!");
}
// Doing these each time, to catch those old ones that didn't have them.
// Coz otherwise git commit starts a gc run IN THE BACKGROUD, which screws up trying to tarball it.
if (shellMeFail("cd %s/%s; git config gc.autodetach false", dir, gar)) E("Failed to git config gc.autodetach!");
// Don't want it running in the foreground either, coz it dumps on the console, we do a gc --quiet later anyway.
if (shellMeFail("cd %s/%s; git config gc.auto 0", dir, gar)) E("Failed to git config gc.auto!");
// Git is such a pedantic bitch, let's just fucking ignore any errors it gives due to lack of anything to do.
// Even worse the OpenSim devs breaking logout tracking gives git plenty of nothing to do. lol
// Loop through the .iar / .oar files in backups, ignoring zero sized files.
struct dirtree *new = dirtree_add_node(0, scBackup, 0);
int i;
ourARs->num = 0;
ourARs->this = xmprintf("%s-", name);
new->extra = (long) ourARs;
dirtree_handle_callback(new, filterARs);
qsort(ourARs->ARs, ourARs->num, sizeof(char *), qstrcmp);
for (i = 0; i < ourARs->num; i++)
{
I("Adding %s to %s", ourARs->ARs[i], gtr);
// Deal with deletions in the inventory / sim, easy method, which becomes a nop for files that stay in the git add below.
if (shellMeFail("cd %s/%s; rm -fr * >>%s", dir, gar, gerr)) E("Failed to rm!");
if (shellMeFail("cd %s/%s; ionice -c3 nice -n 19 tar -xzf \"%s/%s\" || echo \"ERROR - Could not unpack %s !\" >>%s",
dir, gar, scBackup, ourARs->ARs[i], ourARs->ARs[i], gerr)) E("Failed to unpack %s!", ourARs->ARs[i]);
if (!hasContents(gerr))
{
if (shellMeFail("cd %s/%s; git add * >>%s", dir, gar, glog)) E("Failed to git add!");
// The \\* bit is to escape the \ from snprintf, which itself escapes the * from the shell.
if (shellMeFail("cd %s/%s; git add */\\* >>%s", dir, gar, gerr)) E("Failed to git add!");
// Gotta add this again, coz of the rm. Apparently rm removes dotfiles, but add doesn't!
if (-1 == qfile_save(gab, gad, strlen(gad), false)) E("Faild to write %s file!", gab);
if (shellMeFail("cd %s/%s; git add .gitattributes >>%s", dir, gar, gerr)) E("Failed to git add!");
// Magic needed to figure out if there's anything to commit.
// After all the pain to get this to work, there's an ever changing timestamp in archive.xml that screws it up.
// Like this system didn't have enough timestamps in it already. lol
// TODO - I could sed out that timestamp, and put it back again based on the OAR file name when extracting.
// IARs don't seem to have the timestamp.
int j = shellMe("cd %s/%s; t=$(git status --porcelain) && [ -z \"${t}\" ]", dir, gar);
if (!WIFEXITED(j)) E("git status failed!");
else if (1 == WEXITSTATUS(j))
{
V("Committing changes from %s", ourARs->ARs[i]);
// Note this commit message has to be just the file name, as the ungitAR script uses it.
if (shellMeFail("cd %s/%s; git commit -a -qm \"%s\" >>%s || echo \"ERROR - Could not commit %s !\" >>%s ",
dir, gar, ourARs->ARs[i], ourARs->ARs[i], glog, gerr)) E("Failed to git commit!");
if (hasContents(gerr))
{
free(ourARs->ARs[i]);
goto gitARend;
}
}
else
V("No changes to commit from %s.", ourARs->ARs[i]);
}
if (!hasContents(gerr))
{
if (shellMeFail("mv %s/%s %s", scBackup, ourARs->ARs[i], dir)) E("Failed to move %s!", ourARs->ARs[i]);
}
free(ourARs->ARs[i]);
}
if (!hasContents(gerr))
{
I("Checking git repo %s", gtr);
if (shellMeFail("cd %s/%s; git fsck --strict --lost-found --no-progress >>%s || echo \"ERROR - Problem with git fsck %s !\" >>%s ",
dir, gar, glog, gtr, gerr)) E("Failed to git fsck!");
if (shellMeFail("cd %s/%s; git gc --aggressive --prune=all --quiet >>%s || echo \"ERROR - Problem with git gc %s !\" >>%s ",
dir, gar, glog, gtr, gerr)) E("Failed to git gc!");
I("Compressing %s", gtr);
if (shellMeFail("cd %s; XZ_OPT='-9e' ionice -c3 nice -n 19 tar -c --xz %s -f %s || echo 'ERROR - Could not pack gitAR!' >>%s",
dir, gar, gtr, gerr)) E("Failed to git add!");
}
gitARend:
if (hasContents(gerr)) E("Failed to process the archives, look in %s for errors!", gerr);
else
{
if (shellMeFail("rm -fr %s", dir)) E("Failed to rm!");
}
free(ourARs->ARs);
free(ourARs->this);
free(ourARs);
free(gab);
free(gerr);
free(glog);
free(dir);
free(gtr);
free(gar);
free(name);
break;
}
case RESTART : // "restart 'Welcome sim'" "restart Welcome.shini" "Welcome.shini restart" "restart Welcome.ini" "restart Welcome" "restart" restart everything
{
I("Tmux tab [%d:%s](pane %d) tmux ID %s, from %s/sim%d - %s is stopping.", window, type, pane, simd->paneID, scTemp, count, simd->name);
stopSim(simd, sim, type, count, window, panes, pane);
while (checkSimIsRunning(simd->tab))
usleep(100000);
usleep(100000);
startSim(simd, sim, type, count, window, panes, pane);
break;
}
case STATUS : // "status 'Welcome sim'" "status Welcome.shini" "Welcome.shini status" "status Welcome.ini" "status Welcome" "status" status everything
{
// TODO - report CPU and memory used as well.
if (FLAG(m))
{
; // Do nothing, this is better done in the calling function, coz it has the details.
}
else if (checkSimIsRunning(simd->tab))
I("%s is running in Tmux tab [%d:%s](pane %d) tmux ID %s, from %s/sim%d", simd->name, window, type, pane, simd->paneID, scTemp, count);
else
W("%s is not running.", simd->name);
break;
}
case STOP : // "stop 'Welcome sim'" "stop Welcome.shini" "Welcome.shini stop" "stop Welcome.ini" "stop Welcome" "stop" stop everything
{
stopSim(simd, sim, type, count, window, panes, pane);
break;
}
default :
{
E("Unknown command! %s", toys.optargs[0]);
break;
}
}
}
void stopSims(simData *simd, char *sim, char *type, int count, int window, int panes, int pane)
{
// TODO - should move onto others if this one takes too long, and come back later.
// NOTE - these sleeps are guesses, seems to work on my super desktop during testing.
if (checkSimIsRunning(simd->tab))
T("Tmux tab [%d:%s](pane %d) tmux ID %s, from %s/sim%d - %s has NOT stopped YET.", window, type, pane, simd->paneID, scTemp, count, simd->name);
if (simd->users)
{
ourSims->doIt = 0;
W("Not shutting down %s coz %d people are still on it. They have been warned.", simd->tab, simd->users);
}
else
{
while (checkSimIsRunning(simd->tab))
usleep(100000);
usleep(10000);
doTmuxCmd("kill-pane -t %s", simd->paneID);
I("Tmux tab [%d:%s](pane %d) tmux ID %s, from %s/sim%d - %s has stopped.", window, type, pane, simd->paneID, scTemp, count, simd->name);
}
}
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++;
}
}
typedef struct _dbFields dbFields;
struct _dbFields
{
qlisttbl_t *flds;
int count;
};
typedef struct _dbField dbField;
struct _dbField
{
char *name;
enum enum_field_types type;
unsigned long length;
unsigned int flags;
unsigned int decimals;
};
void dbFreeFields(dbFields *flds, boolean all)
{
flds->count--;
// TODO - sigh, looks to be inconsistant why some do and some don't leak.
// I guess the ones that don't leak are the ones that crash?
// It's only a tiny leak anyway, 80 bytes total.
// if ((0 >= flds->count) || all) // CRASHY
if ((0 >= flds->count)) // LEAKY
{
qlisttbl_obj_t obj;
memset((void *) &obj, 0, sizeof(obj));
flds->flds->lock(flds->flds);
while(flds->flds->getnext(flds->flds, &obj, NULL, false) == true)
{
dbField *fld = (dbField *) obj.data;
free(fld->name);
}
flds->flds->unlock(flds->flds);
flds->flds->free(flds->flds);
flds->flds = NULL;
free(flds);
}
}
enum dbCommandType
{
CT_SELECT,
CT_CREATE,
CT_UPDATE,
CT_NONE
};
typedef struct _dbRequest dbRequest;
struct _dbRequest
{
char *table, *join, *where, *order, *sql;
MYSQL_STMT *prep; // NOTE - executing it stores state in this.
dbFields *fields;
int inCount, outCount, rowCount;
char **inParams, **outParams;
MYSQL_BIND *inBind, *outBind;
rowData *rows;
my_ulonglong count;
enum dbCommandType type;
boolean freeOutParams;
};
void dbFreeRequest(dbRequest *req, boolean all)
{
int i;
D("Cleaning up prepared database request %s - %s %d %d", req->table, req->where, req->outCount, req->inCount);
if (NULL != req->outBind)
{
for (i = 0; i < req->outCount; i++)
{
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);
req->outBind = NULL;
}
else
D(" No out binds to clean up for %s - %s.", req->table, req->where);
if (NULL != req->inBind)
{
for (i = 0; i < req->inCount; i++)
{
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);
req->inBind = NULL;
}
else
D(" No in binds to clean up for %s - %s.", req->table, req->where);
if (req->freeOutParams && all)
{
if (NULL != req->outParams)
{
free(req->outParams);
req->outParams = NULL;
}
else
D(" No out params to clean up for %s - %s.", req->table, req->where);
}
if (NULL != req->sql) free(req->sql);
else
D(" No SQL to clean up for %s - %s.", req->table, req->where);
req->sql = NULL;
if (NULL != req->prep)
{
if (0 != mysql_stmt_close(req->prep))
C(" Unable to close the prepared statement!");
req->prep = NULL;
}
if (all)
{
if (NULL != req->fields)
{
dbFreeFields(req->fields, all);
req->fields = NULL;
}
else
D(" No fields to clean up for %s - %s.", req->table, req->where);
}
}
void freeDb(boolean all)
{
dbRequest **rq;
if (dbRequests)
{
if (all)
{
while (NULL != (rq = (dbRequest **) dbRequests->popfirst(dbRequests, NULL)))
{
dbFreeRequest(*rq, all);
free(rq);
}
dbRequests->free(dbRequests);
dbRequests = NULL;
}
else
{
qlist_obj_t obj;
memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call
dbRequests->lock(dbRequests);
while (dbRequests->getnext(dbRequests, &obj, false) == true)
dbFreeRequest(*((dbRequest **) obj.data), all);
dbRequests->unlock(dbRequests);
}
}
if (database) mysql_close(database);
database = NULL;
mysql_library_end();
}
static boolean dbConnect()
{
database = mysql_init(NULL);
if (NULL == database)
{
E("mysql_init() failed - %s", mysql_error(database));
return FALSE;
}
/* TODO - dammit, no mysql_get_option(), MariaDB docs say mysql_get_optionv(), which doesn't exist either.
Says "This function was added in MariaDB Connector/C 3.0.0.", I have MariaDB / MySQL client version: 10.1.44-MariaDB.
if (mysql_get_option(database, MYSQL_OPT_CONNECT_TIMEOUT, &dbTimeout))
E("mysql_get_option(MYSQL_OPT_CONNECT_TIMEOUT) failed - %s", mysql_error(database));
else
D("Database MYSQL_OPT_CONNECT_TIMEOUT = %d", dbTimeout);
if (mysql_get_option(database, MYSQL_OPT_RECONNECT, &dbReconnect))
E("mysql_get_option(MYSQL_OPT_RECONNECT) failed - %s", mysql_error(database));
else
D("Database MYSQL_OPT_RECONNECT = %d", (int) dbReconnect);
*/
// Seems best to disable auto-reconnect, so I have more control over reconnections.
dbReconnect = 0;
if (mysql_options(database, MYSQL_OPT_RECONNECT, &dbReconnect))
E("mysql_options(MYSQL_OPT_RECONNECT) failed - %s", mysql_error(database));
else
D("Database MYSQL_OPT_RECONNECT is now %d", (int) dbReconnect);
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;
}
// Just set the fucking thing to a year. Pffft.
dbTimeout = 60 * 60 * 24 * 7 * 52;
char *sql = xmprintf("SET SESSION wait_timeout=%d", (int) dbTimeout);
if (mysql_query(database, sql))
E("SET SESSION wait_timeout=%d failed - %s", (int) dbTimeout, mysql_error(database));
else
D("Database wait_timeout = %d", (int) dbTimeout);
free(sql);
if (-1 == clock_gettime(CLOCK_REALTIME, &dbLast))
perror_msg("Unable to get the time.");
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
// Ah it could be "connection inactive for 8 hours".
// Which might be why OpenSim opens a new connection for EVERYTHING.
// https://dev.mysql.com/doc/refman/5.7/en/c-api-auto-reconnect.html
// Has more details.
static boolean dbCheckError(char *error, char *sql)
{
int e = mysql_errno(database);
E("MariaDB error %d - %s: %s\n%s", e, error, mysql_error(database), sql);
if (2006 == e)
{
W("Reconnecting to database.");
freeDb(false);
return dbConnect();
}
return FALSE;
}
// "Statement execute failed 2013: Lost connection to MySQL server during query"
static boolean dbStmtCheckError(dbRequest *req, char *error, char *sql)
{
int e = mysql_stmt_errno(req->prep);
E("MariaDB prepared statement error %d - %s: %s\n%s", e, error, mysql_stmt_error(req->prep), sql);
if (2013 == e)
{
W("Reconnecting to database.");
freeDb(false);
return dbConnect();
}
return FALSE;
}
dbFields *dbGetFields(char *table)
{
static qhashtbl_t *tables = NULL;
if (NULL == tables) tables = qhashtbl(0, 0);
dbFields *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(database, sql))
{
// E("MariaDB error %d - Query failed 0: %s\n%s", mysql_errno(database), mysql_error(database), sql);
if (dbCheckError("Query failed 0", sql))
{
ret = dbGetFields(table);
free(sql);
return ret;
}
}
else
{
MYSQL_RES *res = mysql_store_result(database);
if (!res)
E("MariaDB error %d - Couldn't get results set from %s\n %s", mysql_errno(database), mysql_error(database), sql);
else
{
MYSQL_FIELD *fields = mysql_fetch_fields(res);
if (!fields)
E("MariaDB error %d - Failed fetching fields: %s", mysql_errno(database), mysql_error(database));
else
{
unsigned int i, num_fields = mysql_num_fields(res);
ret = xmalloc(sizeof(dbFields)); // Little bit LEAKY
ret->flds = qlisttbl(QLISTTBL_UNIQUE | QLISTTBL_LOOKUPFORWARD);
ret->count = 1;
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->flds->put(ret->flds, fld->name, fld, sizeof(*fld));
free(fld);
}
tables->put(tables, table, ret, sizeof(*ret));
}
mysql_free_result(res);
}
}
free(sql);
}
else // Reference count these, coz some tables are used more than once.
ret->count++;
return ret;
}
/* 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;
*/
int dbDoSomething(dbRequest *req, boolean count, ...)
{
int ret = 0;
va_list ap;
struct timespec then;
int i, j;
MYSQL_RES *prepare_meta_result = NULL;
if (-1 == clock_gettime(CLOCK_REALTIME, &then))
perror_msg("Unable to get the time.");
// TODO - should factor this out to it's own function, and call that function in dbCount() and dbCountJoin().
// Or better yet, finally migrate those functions to using dbDoSomething().
double n = (dbLast.tv_sec * 1000000000.0) + dbLast.tv_nsec;
double t = (then.tv_sec * 1000000000.0) + then.tv_nsec;
t("Database timeout test %lf > %lf", ((t - n) / 1000000000.0), (dbTimeout / 2.0));
if (((t - n) / 1000000000.0) > (dbTimeout / 2.0))
{
T("Avoid database timeout of %d seconds, pinging it.", dbTimeout);
if (0 != mysql_ping(database))
{
W("Reconnecting to database.");
freeDb(false);
dbConnect();
}
}
va_start(ap, count);
if (NULL == req->prep)
{
D("Creating prepared statement for %s - %s", req->table, req->where);
if (0 == req->type)
req->type = CT_SELECT;
req->fields = dbGetFields(req->table);
if (NULL == req->fields)
{
E("Unknown fields for table %s.", req->table);
ret++;
goto end;
}
switch (req->type)
{
case CT_SELECT :
{
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;
}
break;
}
case CT_CREATE :
{
char *values = xmprintf("");
i = 0;
while (req->inParams[i] != NULL)
{
char *t = xmprintf("%s, %s=?", values, req->inParams[i]);
free(values);
values = t;
i++;
}
if (0 == i)
{
E("Statement prepare for INSERT must have in paramaters.");
ret++;
free(values);
values = xmprintf("");
}
req->sql = xmprintf("INSERT INTO %s SET %s", req->table, &values[1]);
free(values);
break;
}
case CT_UPDATE :
{
break;
}
case CT_NONE :
{
W("No SQL type!");
break;
}
}
d("New SQL statement - %s", req->sql);
// prepare statement with the other fields
req->prep = mysql_stmt_init(database);
if (NULL == req->prep)
{
E("Statement prepare init failed: %s\n", mysql_stmt_error(req->prep));
ret++;
goto end;
}
if (mysql_stmt_prepare(req->prep, req->sql, strlen(req->sql)))
{
E("Statement prepare failed: %s\n", mysql_stmt_error(req->prep));
ret++;
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);
ret++;
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->fields->flds->get(req->fields->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);
ret++;
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 + 1;
switch(fld->type)
{
case MYSQL_TYPE_TINY:
{
//d("TINY %d %s %d", i, fld->name, req->inBind[i].buffer_length);
break;
}
case MYSQL_TYPE_SHORT:
{
req->inBind[i].is_unsigned = FALSE;
//d("SHORT %d %s %d", i, fld->name, req->inBind[i].buffer_length);
break;
}
case MYSQL_TYPE_INT24:
{
req->inBind[i].is_unsigned = FALSE;
//d("INT24 %d %s %d", i, fld->name, req->inBind[i].buffer_length);
break;
}
case MYSQL_TYPE_LONG:
{
req->inBind[i].is_unsigned = FALSE;
//d("LONG %d %s %d", i, fld->name, req->inBind[i].buffer_length);
break;
}
case MYSQL_TYPE_LONGLONG:
{
req->inBind[i].is_unsigned = FALSE;
//d("LONGLONG %d %s %d", i, fld->name, req->inBind[i].buffer_length);
break;
}
case MYSQL_TYPE_FLOAT:
{
//d("FLOAT %d %s %d", i, fld->name, req->inBind[i].buffer_length);
break;
}
case MYSQL_TYPE_DOUBLE:
{
//d("DOUBLE %d %s %d", i, fld->name, req->inBind[i].buffer_length);
break;
}
case MYSQL_TYPE_NEWDECIMAL:
{
//d("NEWDECIMAL %d %s %d", i, fld->name, req->inBind[i].buffer_length);
break;
}
case MYSQL_TYPE_TIME:
case MYSQL_TYPE_DATE:
case MYSQL_TYPE_DATETIME:
case MYSQL_TYPE_TIMESTAMP:
{
//d("DATE / TIME ish %d %s %d", i, fld->name, req->inBind[i].buffer_length);
break;
}
case MYSQL_TYPE_STRING:
case MYSQL_TYPE_VAR_STRING:
{
//d("STRING / VARSTRING %d %s %d", i, fld->name, req->inBind[i].buffer_length);
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:
{
//d("BLOBs %d %s %d", i, fld->name, req->inBind[i].buffer_length);
req->inBind[i].is_null = xzalloc(sizeof(my_bool));
break;
}
case MYSQL_TYPE_BIT:
{
req->inBind[i].is_null = xzalloc(sizeof(my_bool));
//d("BIT %d %s %d", i, fld->name, req->inBind[i].buffer_length);
break;
}
case MYSQL_TYPE_NULL:
{
//d("NULL %d %s %d", i, fld->name, req->inBind[i].buffer_length);
break;
}
}
}
}
// TODO - if this is not a count, setup result bind paramateres, may be needed for counts as well.
if (CT_SELECT == req->type)
{
prepare_meta_result = mysql_stmt_result_metadata(req->prep);
if (!prepare_meta_result)
{
E(" mysql_stmt_result_metadata() error %d, returned no meta information - %s\n", mysql_stmt_errno(req->prep), mysql_stmt_error(req->prep));
ret++;
goto freeIt;
}
}
if (count)
{
I("count!!!!!!!!!!!!!!!!");
}
else if (CT_SELECT == req->type)
{
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->fields->flds->lock(req->fields->flds);
while (req->fields->flds->getnext(req->fields->flds, &obj, NULL, false) == true)
{
dbField *fld = (dbField *) obj.data;
req->outParams[i] = fld->name;
i++;
}
req->outParams[i] = NULL;
req->fields->flds->unlock(req->fields->flds);
}
if (i != req->outCount)
{
E("Out parameters count doesn't match %d != %d foqr - %s", i, req->outCount, req->sql);
ret++;
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->fields->flds->get(req->fields->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);
ret++;
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 + 1;
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("LONGLONG %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("DATE / TIME ish %s %d", fld->name, req->outBind[i].buffer_length);
break;
}
case MYSQL_TYPE_STRING:
case MYSQL_TYPE_VAR_STRING:
{
//d("STRING / VARSTRING %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("BLOBs %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));
ret++;
goto freeIt;
}
}
}
//d("input bind for %s", req->sql);
for (i = 0; i < req->inCount; i++)
{
dbField *fld = req->fields->flds->get(req->fields->flds, req->inParams[i], NULL, false);
if (NULL == fld)
{
E("Unknown input field %s.%s for - %s", req->table, req->inParams[i], req->sql);
ret++;
goto freeIt;
}
else
{
switch(fld->type)
{
case MYSQL_TYPE_TINY:
{
int c = va_arg(ap, int);
signed char d = (signed char) c;
memcpy(req->inBind[i].buffer, &d, (size_t) fld->length);
//V("TINY %d %s %d", i, fld->name, req->inBind[i].buffer_length);
break;
}
case MYSQL_TYPE_SHORT:
{
int c = va_arg(ap, int);
short int d = (short int) c;
memcpy(req->inBind[i].buffer, &d, (size_t) fld->length);
//V("SHORT %d %s %d = %d", i, fld->name, req->inBind[i].buffer_length, c);
break;
}
case MYSQL_TYPE_INT24:
{
int d = va_arg(ap, int);
memcpy(req->inBind[i].buffer, &d, (size_t) fld->length);
//V("INT24 %d %s %d - %d", i, fld->name, req->inBind[i].buffer_length, d);
break;
}
case MYSQL_TYPE_LONG:
{
long d = va_arg(ap, long);
memcpy(req->inBind[i].buffer, &d, (size_t) fld->length);
//V("LONG %d %s %d = %ld", i, fld->name, req->inBind[i].buffer_length, d);
break;
}
case MYSQL_TYPE_LONGLONG:
{
long long int d = va_arg(ap, long long int);
memcpy(req->inBind[i].buffer, &d, (size_t) fld->length);
//V("LONGLONG %d %s %d = %lld", i, fld->name, req->inBind[i].buffer_length, d);
break;
}
case MYSQL_TYPE_FLOAT:
{
double c = va_arg(ap, double);
float d = (float) c;
memcpy(req->inBind[i].buffer, &d, (size_t) fld->length);
//V("FLOAT %d %s %d = %f", i, fld->name, req->inBind[i].buffer_length, d);
break;
}
case MYSQL_TYPE_DOUBLE:
{
double d = va_arg(ap, double);
memcpy(req->inBind[i].buffer, &d, (size_t) fld->length);
//V("DOUBLE %d %s %d = %f", i, fld->name, req->inBind[i].buffer_length, d);
break;
}
case MYSQL_TYPE_NEWDECIMAL:
{
//V("NEWDECIMAL %d %s %d", i, fld->name, req->inBind[i].buffer_length);
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(req->inBind[i].buffer, &d, (size_t) fld->length);
//V("DATE / TIME ish %d %s %d", i, fld->name, req->inBind[i].buffer_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';
//V("STRING / VARSTRING %d %s %d = %s", i, fld->name, req->inBind[i].buffer_length, d);
break;
}
case MYSQL_TYPE_TINY_BLOB:
case MYSQL_TYPE_BLOB:
case MYSQL_TYPE_MEDIUM_BLOB:
case MYSQL_TYPE_LONG_BLOB:
{
// TODO - should write this, we will likely need it. Main problem is - how long is this blob? Probably should add a length param before the blob.
//V("BLOBs %d %s %d", i, fld->name, req->inBind[i].buffer_length);
break;
}
case MYSQL_TYPE_BIT:
{
//V("BIT %d %s %d", i, fld->name, req->inBind[i].buffer_length);
break;
}
case MYSQL_TYPE_NULL:
{
//V("NULL %d %s %d", i, fld->name, req->inBind[i].buffer_length);
break;
}
}
}
}
if (mysql_stmt_bind_param(req->prep, req->inBind))
{
E("Bind failed error %d.", mysql_stmt_errno(req->prep));
ret++;
goto freeIt;
}
//d("Execute %s", req->sql);
// do the prepared statement req->prep.
if (mysql_stmt_execute(req->prep))
{
if (dbStmtCheckError(req, "Statement failed 0", req->sql))
{
ret++;
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));
ret++;
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->fields->flds->get(req->fields->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);
ret++;
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));
ret++;
}
end:
va_end(ap);
if (-1 == clock_gettime(CLOCK_REALTIME, &dbLast))
perror_msg("Unable to get the time.");
n = (dbLast.tv_sec * 1000000000.0) + dbLast.tv_nsec;
t("dbDoSomething(%s) took %lf seconds", req->sql, (n - t) / 1000000000.0);
return ret;
}
// 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;
if (NULL != me)
{
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);
free(where);
}
me->unlock(me);
me->free(me);
}
free(rows->fieldNames);
rows->rows->free(rows->rows);
free(rows);
}
my_ulonglong dbCount(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(database, sql))
{
// E("MariaDB error %d - Query failed 1: %s", mysql_errno(database), mysql_error(database));
if (dbCheckError("Query failed 1", sql))
{
ret = dbCount(table, where);
free(sql);
return ret;
}
}
else
{
MYSQL_RES *result = mysql_store_result(database);
if (!result)
E("Couldn't get results set from %s\n: %s", sql, mysql_error(database));
else
{
MYSQL_ROW row = mysql_fetch_row(result);
if (!row)
E("MariaDB error %d - Couldn't get row from %s\n: %s", mysql_errno(database), sql, mysql_error(database));
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(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(database, sql))
{
// E("MariaDB error %d - Query failed 2: %s", mysql_errno(database), mysql_error(database));
if (dbCheckError("Query failed 2", sql))
{
ret = dbCountJoin(table, select, join, where);
free(sql);
return ret;
}
}
else
{
MYSQL_RES *result = mysql_store_result(database);
if (!result)
E("MariaDB error %d - Couldn't get results set from %s\n: %s", mysql_errno(database), sql, mysql_error(database));
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;
}
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;
}
V("Getting fresh grid stats.");
if (checkSimIsRunning("ROBUST"))
replaceStr(stats->stats, "gridOnline", "online");
else
replaceStr(stats->stats, "gridOnline", "offline");
char *tmp;
my_ulonglong locIn = dbCount("Presence", "RegionID != '00000000-0000-0000-0000-000000000000'"); // Locals online but not HGing, and HGers in world.
my_ulonglong HGin = dbCount("Presence", "UserID NOT IN (SELECT PrincipalID FROM UserAccounts)"); // HGers in world.
// Collect stats about members.
replaceLong(stats->stats, "hgers", HGin);
if (locIn >= HGin) // Does OpenSim have too many ghosts?
replaceLong(stats->stats, "inworld", locIn - HGin);
else
replaceLong(stats->stats, "inworld", 0);
tmp = xmprintf("GridExternalName != '%s'", stats->stats->getstr(stats->stats, "uri", false));
replaceLong(stats->stats, "outworld", dbCount("hg_traveling_data", tmp));
free(tmp);
replaceLong(stats->stats, "members", dbCount("UserAccounts", NULL));
// Count local and HG visitors for the last 30 and 60 days.
locIn = dbCountJoin("GridUser", "GridUser.UserID", "INNER JOIN UserAccounts ON GridUser.UserID = UserAccounts.PrincipalID",
"Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 2419200))");
HGin = dbCount("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("GridUser", "GridUser.UserID", "INNER JOIN UserAccounts ON GridUser.UserID = UserAccounts.PrincipalID",
"Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 4838400))");
HGin = dbCount("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("regions", NULL));
replaceLong(stats->stats, "onlineSims", dbCount("regions", "sizeX != 0"));
replaceLong(stats->stats, "varRegions", dbCount("regions", "sizeX > 256 or sizeY > 256"));
replaceLong(stats->stats, "singleSims", dbCount("regions", "sizeX = 256 and sizeY = 256"));
replaceLong(stats->stats, "offlineSims", dbCount("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->table = "regions";
rgnSizes->inParams = szi;
rgnSizes->outParams = szo;
rgnSizes->where = "sizeX != 0";
dbRequests->addfirst(dbRequests, &rgnSizes, sizeof(dbRequest *));
}
dbDoSomething(rgnSizes, FALSE); // LEAKY
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;
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)
{
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;
qurl_decode(o);
tbl->putstr(tbl, n, o);
}
tbl->unlock(tbl);
}
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");
}
// 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;
};
void freeCookie(reqData *Rd, char *cki)
{
cookie *ck0 = Rd->Rcookies->get(Rd->Rcookies, cki, NULL, false);
if (NULL != ck0)
{
if (NULL != ck0->value)
free(ck0->value);
Rd->Rcookies->remove(Rd->Rcookies, cki);
}
}
cookie *setCookie(reqData *Rd, char *cki, char *value)
{
cookie *ret = xzalloc(sizeof(cookie));
char *cook = xstrdup(cki);
int l, i;
// TODO - would URL encoding do the trick?
// Validate this, as there is a limited set of characters allowed.
qstrreplace("tr", cook, "()<>@,;:\\\"/[]?={} \t", "_");
freeCookie(Rd, cook);
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
ret->value = xstrdup("");
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");
}
void list2cookie(reqData *Rd, char *cki, qlist_t *list)
{
char *t0 = xstrdup("");
qlist_obj_t obj;
memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call
list->lock(list);
while (list->getnext(list, &obj, false) == true)
{
char *t1 = xmprintf("%s\n%s", t0, (char *) obj.data);
free(t0);
t0 = t1;
}
list->unlock(list);
setCookie(Rd, cki, &t0[1]); // Skip the first empty one.
// TODO - should set a very short maxAge.
free(t0);
}
qlist_t *cookie2list(qhashtbl_t *cookies, char *cki)
{
qlist_t *ret = NULL;
cookie *ck = (cookie *) cookies->get(cookies, cki, NULL, false);
if (NULL != ck)
{
if (NULL != ck->value)
{
qurl_decode(ck->value);
ret = qstrtokenizer(ck->value, "\n");
free(ck->value);
}
// TODO - should send the "delete this cookie" thing to the browser.
cookies->remove(cookies, cki);
}
return ret;
}
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"); } 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 experimental, and under heavy development. " " Which means it's not all written yet, and things may break.
\n" "To create an account, choose a name and password, type them in, click the 'create account' button.
" "On the next page, fill in all your details, then click on the 'confirm' button.
" "We follow the usual web site registration process, which sends a validation email, with a link to click. " " When you get that email, click on the link, or copy it into a web browser.
" "You will then be logged off. Now you have to wait for an admin to approve your new account. " " They should check with the person you listed as vouching for you first. They will tell you after they approve your account.
" "Missing bits that are still being written - editing accounts, listing accounts, deleting accounts.
\n" "You have an email waiting with a validation link in it, please check your email. " "THE FOLLOWING IS REPEATED, COZ PEOPLE JUST DON'T FOLLOW DIRECTIONS!" "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. " "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. " "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. " "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. " "If your email client wont let you click the validation link, just copy and paste it into your web browser. " "If your email client wont let you click the validation link, just copy and paste it into your web browser. " "If your email client wont let you click the validation link, just copy and paste it into your web browser. " "If your email client wont let you click the validation link, just copy and paste it into your web browser. " // "%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"); freeCookie(Rd, "toke_n_munchie"); freeCookie(Rd, "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); if (NULL != shs->name) free(shs->name); shs->name = NULL; if (NULL != shs->UUID) free(shs->UUID); 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(tnm4); free(tnm3); free(tnm2); free(tnm1); free(tnm0); free(file); if (linky) { // TODO - Later use libcurl. char *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName"); // TODO - should be from Rd.shs->linky-hashish char *t0 = xstrdup(Rd->lnk->hashish), *content, *command; if ('\0' != t0[0]) { size_t sz = qhex_decode(t0); char *t1 = qB64_encode(t0, sz); 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" "THE FOLLOWING IS REPEATED, COZ PEOPLE JUST DON'T FOLLOW DIRECTIONS!" "If your email client wont let you click the validation\n" "link, just copy and paste it into your web browser.\n" "If your email client wont let you click the validation\n" "link, just copy and paste it into your web browser.\n" "If your email client wont let you click the validation\n" "link, just copy and paste it into your web browser.\n" "If your email client wont let you click the validation\n" "link, just copy and paste it into your web browser.\n" "\n" "\n" "Do not reply to this email.\n" "\n" "\n" "A copy of the grids Terms of Service that you agreed to\n" "when you created your account is included below.\n" "%s", 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, ToS ); 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); if (shellMeFail("sendmail -oi -t <'%s'", file)) E("sendmail command failed!"); free(file); free(content); free(t1); free(t0); } } } 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("UserAccounts", where); free(where); } while (users != 0); if (NULL != Rd->shs.UUID) free(Rd->shs.UUID); 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; } typedef struct _systemFolders systemFolders; struct _systemFolders { char *name; short type; }; systemFolders sysFolders[] = { {"My Inventory", 8}, {"Animations", 20}, {"Body Parts", 13}, {"Calling Cards", 2}, // {"Friends", 2}, // {"All", 2}, {"Clothing", 5}, {"Current Outfit", 46}, {"Favorites", 23}, {"Gestures", 21}, {"Landmarks", 3}, {"Lost And Found", 16}, {"Mesh", 49}, {"My Outfits", 48}, {"My Suitcase", 100}, // All the others are replicated inside. {"Notecards", 7}, {"Objects", 6}, {"Outfit", 47}, {"Photo Album", 15}, {"Scripts", 10}, {"Sounds", 1}, {"Textures", 0}, {"Trash", 14}, {NULL, -1} }; 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 *tnm = "user = \n{\n"; 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); uuid_t binuuid; uuid_clear(binuuid); if ((NULL != uuid) && ('\0' != uuid[0])) uuid_parse(uuid, binuuid); if ((NULL != uuid) && ('\0' != uuid[0]) && (!uuid_is_null(binuuid))) { if (s) I("Creating user %s.", file); else I("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 *end = "}\nreturn user\n"; if (!writeLuaString (Rd, fd, file, "name", name)) goto notWritten; if (!writeLuaInteger(Rd, fd, file, "created", (strcmp("", getStrH(Rd->stuff, "created")) != 0) ? atol(getStrH(Rd->stuff, "created")) : (long) Rd->shs.timeStamp[1].tv_sec)) goto notWritten; if (!writeLuaString (Rd, fd, file, "email", NULL)) goto notWritten; if (!writeLuaString (Rd, fd, file, "title", getLevel(atoi(level)))) goto notWritten; if (!writeLuaString (Rd, fd, file, "level", level)) goto notWritten; if (!writeLuaInteger(Rd, fd, file, "flags", 0)) goto notWritten; if (!writeLuaInteger(Rd, fd, file, "active", 1)) goto notWritten; if (!writeLuaString (Rd, fd, file, "passwordHash", NULL)) goto notWritten; if (!writeLuaString (Rd, fd, file, "passwordSalt", NULL)) goto notWritten; if (!writeLuaString (Rd, fd, file, "UUID", uuid)) goto notWritten; if (!writeLuaString (Rd, fd, file, "DoB", NULL)) goto notWritten; if (!writeLuaString (Rd, fd, file, "agree", NULL)) goto notWritten; if (!writeLuaString (Rd, fd, file, "adult", NULL)) goto notWritten; if (!writeLuaString (Rd, fd, file, "aboutMe", NULL)) goto notWritten; if (!writeLuaString (Rd, fd, file, "vouched", "off")) goto notWritten; if (!writeLuaString (Rd, fd, file, "voucher", NULL)) goto notWritten; if (!writeLuaString (Rd, fd, file, "approver", NULL)) goto notWritten; if (!writeLuaString (Rd, fd, file, "link", link)) goto notWritten; l = strlen(end); if (l != writeall(fd, end, l)) perror_msg("Writing %s", file); else { char *nm = xmprintf("%s/users/%s.lua", scData, qstrreplace("tr", name, " ", "_")); free(file); file = xmprintf("%s.lua", uuid); I("Symlinking %s to %s", file, nm); if (0 != symlink(file, nm)) perror_msg("Symlinking %s to %s", file, nm); free(nm); } notWritten: free(name); } xclose(fd); short lvl = atoi(level); if (0 <= lvl) // Note that http://opensimulator.org/wiki/Userlevel claims that 1 and above are "GOD_LIKE". { if (Rd->fromDb) { I("Updating database user %s.", getStrH(Rd->stuff, "name")); } else { // Setup the database stuff. static dbRequest *acntsI = NULL; if (NULL == acntsI) { static char *szi[] = { "FirstName", "LastName", "Email", "Created", "PrincipalID", "ScopeID", "UserLevel", "UserFlags", "UserTitle", // "ServiceURLs", // No worky "text", filled with crap. Leaving it blank works fine. "active", NULL }; static char *szo[] = {NULL}; acntsI = xzalloc(sizeof(dbRequest)); acntsI->table = "UserAccounts"; acntsI->inParams = szi; acntsI->outParams = szo; acntsI->where = ""; acntsI->type = CT_CREATE; dbRequests->addfirst(dbRequests, &acntsI, sizeof(dbRequest *)); } static dbRequest *authI = NULL; if (NULL == authI) { static char *szi[] = {"UUID", "passwordSalt", "passwordHash", "accountType", "webLoginKey", NULL}; static char *szo[] = {NULL}; authI = xzalloc(sizeof(dbRequest)); authI->table = "auth"; authI->inParams = szi; authI->outParams = szo; authI->where = ""; authI->type = CT_CREATE; dbRequests->addfirst(dbRequests, &authI, sizeof(dbRequest *)); } static dbRequest *invFolderI = NULL; if (NULL == invFolderI) { static char *szi[] = { "agentID", "folderName", "type", // smallint(6) "version", // int(11) "folderID", "parentFolderID", NULL }; static char *szo[] = {NULL}; invFolderI = xzalloc(sizeof(dbRequest)); invFolderI->table = "inventoryfolders"; invFolderI->inParams = szi; invFolderI->outParams = szo; invFolderI->where = ""; invFolderI->type = CT_CREATE; dbRequests->addfirst(dbRequests, &invFolderI, sizeof(dbRequest *)); } static dbRequest *gUserI = NULL; if (NULL == gUserI) { // static char *szi[] = {"UserID", "HomeRegionID", "HomePosition", "HomeLookAt", "LastRegionID", "LastPosition", "LastLookAt", "Online", "Login", "Logout", NULL}; static char *szi[] = {"UserID", NULL}; // All the defaults are what we would set anyway. static char *szo[] = {NULL}; gUserI = xzalloc(sizeof(dbRequest)); gUserI->table = "GridUser"; gUserI->inParams = szi; gUserI->outParams = szo; gUserI->where = ""; gUserI->type = CT_CREATE; dbRequests->addfirst(dbRequests, &gUserI, sizeof(dbRequest *)); } I("Creating database user %s %s.", uuid, getStrH(Rd->stuff, "name")); char *first = Rd->stuff->getstr(Rd->stuff, "name", true), *last = strchr(first, ' '); // create user record. *last++ = '\0'; if (0 != dbDoSomething(acntsI, FALSE, first, last, getStrH(Rd->stuff, "email"), (strcmp("", getStrH(Rd->stuff, "created")) != 0) ? atoi(getStrH(Rd->stuff, "created")) : (int) Rd->shs.timeStamp[1].tv_sec, uuid, "00000000-0000-0000-0000-000000000000", atoi(level), 0, getLevel(atoi(level)), // "", // Defaults to NULL, empty string seems OK to. Then gets filled in later. 1 )) bitch(Rd, "Internal error.", "Failed to create UserAccounts record."); else { char uuidI[37], uuidR[37], uuidC[37], uuidS[37]; uuid_t binuuidI; int r = 0, i; // Create inventory records. strcpy(uuidR, "00000000-0000-0000-0000-000000000000"); for (i = 0; (NULL != sysFolders[i].name) && (0 == r); i++) { uuid_generate_random(binuuidI); uuid_unparse_lower(binuuidI, uuidI); // TODO - should check there isn't a folder with this UUID already. D("Creating %s inventory folder for user %s.", sysFolders[i].name, getStrH(Rd->stuff, "name")); r += dbDoSomething(invFolderI, FALSE, uuid, sysFolders[i].name, sysFolders[i].type, 1, uuidI, uuidR); // LEAKY if (0 != r) bitch(Rd, "Internal error.", "Failed to create invenoryFolder record."); if (strcmp("My Inventory", sysFolders[i].name) == 0) strcpy(uuidR, uuidI); if (strcmp("Calling Cards", sysFolders[i].name) == 0) strcpy(uuidC, uuidI); if (strcmp("My Suitcase", sysFolders[i].name) == 0) strcpy(uuidS, uuidI); } uuid_generate_random(binuuidI); uuid_unparse_lower(binuuidI, uuidI); // TODO - should check there isn't a folder with this UUID already. D("Creating %s inventory folder for user %s.", "Friends", getStrH(Rd->stuff, "name")); r += dbDoSomething(invFolderI, FALSE, uuid, "Friends", 2, 1, uuidI, uuidC); if (0 != r) bitch(Rd, "Internal error.", "Failed to create invenoryFolder record."); strcpy(uuidC, uuidI); uuid_generate_random(binuuidI); uuid_unparse_lower(binuuidI, uuidI); // TODO - should check there isn't a folder with this UUID already. D("Creating %s inventory folder for user %s.", "All", getStrH(Rd->stuff, "name")); r += dbDoSomething(invFolderI, FALSE, uuid, "All", 2, 1, uuidI, uuidC); if (0 != r) bitch(Rd, "Internal error.", "Failed to create invenoryFolder record."); for (i = 1; (NULL != sysFolders[i].name) && (0 == r); i++) { uuid_generate_random(binuuidI); uuid_unparse_lower(binuuidI, uuidI); // TODO - should check there isn't a folder with this UUID already. D("Creating %s inventory folder for user %s.", sysFolders[i].name, getStrH(Rd->stuff, "name")); r += dbDoSomething(invFolderI, FALSE, uuid, sysFolders[i].name, sysFolders[i].type, 1, uuidI, uuidS); if (0 != r) bitch(Rd, "Internal error.", "Failed to create invenoryFolder record."); } if (0 == r) { // Create location record. D("Creating home and last positions for user %s.", getStrH(Rd->stuff, "name")); if (0 != dbDoSomething(gUserI, FALSE, uuid)) // LEAKY bitch(Rd, "Internal error.", "Failed to create GridUser record."); else { // Finally create auth record, so they can log in. D("Creating auth record for user %s %s.", uuid, getStrH(Rd->stuff, "name")); if (0 != dbDoSomething(authI, FALSE, uuid, getStrH(Rd->stuff, "passwordSalt"), getStrH(Rd->stuff, "passwordHash"), "UserAccount", "00000000-0000-0000-0000-000000000000")) bitch(Rd, "Internal error.", "Failed to create auth record."); } } // load iar -m first last / password /opt/opensim_SC/backups/DefaultMember.IAR struct sysinfo info; float la; sysinfo(&info); la = info.loads[0]/65536.0; char *name; simData *simd = ourSims->byTab->get(ourSims->byTab, backupIARsim, NULL, false); if (NULL == simd) E("Sim %s not found in ini list! Can't create inventory for %s %s", backupIARsim, first, last); else name = simd->name; if (checkSimIsRunning(backupIARsim)) { char *cmd = xmprintf("%d.%d", simd->window, simd->pane); char *c = xmprintf("load iar -m %s %s / password /opt/opensim_SC/backups/DefaultMember.IAR", first, last); I("Loading default member IAR for %s %s in sim %s, this might take a couple of minutes.", first, last, name); V(c); sendTmuxCmd(cmd, c); free(cmd); free(c); } } free(first); } } } else W("Not writing NULL UUID user!"); 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 = xstrdup(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) */ #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 = Rd->queries->getstr(Rd->queries, "hashish", true); //d("O hashish %s", hashish); if (NULL != hashish) { char *t = xstrdup(hashish); size_t sz = qB64_decode(t); // TODO - should validate the cookie version as well, if it was sent. // Coz it later tries to delete the linky as if it was the cookie session, and might give us a chance to delete the old session. // Though only if there's a munchie in the body? I("Validating LINKY hashish %s", hashish); free(hashish); hashish = qhex_encode(t, sz); free(t); linky = TRUE; } else { toke_n_munchie = getStrH(Rd->cookies, "toke_n_munchie"); // munchie = getStrH(Rd->body, "munchie"); hashish = Rd->cookies->getstr(Rd->cookies, "hashish", true); if (('\0' == toke_n_munchie[0]) || ((NULL == hashish))) { if (strcmp("logout", Rd->doit) == 0) { I("Not checking session, coz we are logging out."); Rd->shs.status = SHS_NUKE; return ret; } bitchSession(Rd, "Invalid session.", "No or blank hashish or toke_n_munchie."); Rd->shs.status = SHS_NONE; ret++; } else I("Validating SESSION 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 might be coz it's a stale session that was deleted already, so shouldn't complain really if they are just getting the login page. // They might also have a stale doit and form cookie. // bitchSession(Rd, "Invalid session.", "No session file."); bitchSession(Rd, "", "No session file."); ret++; } else { // 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) { // TODO if newbie user has not logged out, but clicks the email linky, and they end up on a new browser tab, they'll see this on the logged in tab. bitchSession(Rd, "Wrong munchie for session, may have been eaten, please try again.", "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); } // TODO - should carefully review all of this, especially the moving of session data to and fro. if (0 == ret) { W("Validated session."); sesh *shs = &Rd->shs; qstrcpy(shs->leaf, sizeof(shs->leaf), leaf); if (NULL != shs->name) free(shs->name); shs->name = tnm->getstr(tnm, "name", true); if (NULL != shs->UUID) free(shs->UUID); shs->UUID = tnm->getstr(tnm, "UUID", true); 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)); Rd->lnk->status = SHS_NUKE; 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); // Check for session timeouts etc. if (now.tv_sec > st.st_mtim.tv_sec + seshTimeOut) { bitch(Rd, "Session timed out.", "No activity for longer than seshTimeOut, session is ancient."); ret++; Rd->shs.status = SHS_ANCIENT; } else if (now.tv_sec > st.st_mtim.tv_sec + idleTimeOut) { bitch(Rd, "Session idled out.", "No activity for longer than idleTimeOut, session is idle."); ret++; Rd->shs.status = SHS_IDLE; } else if (now.tv_sec > st.st_mtim.tv_sec + seshRenew) { D("Session needs renewing."); Rd->shs.status = SHS_RENEW; } else Rd->shs.status = SHS_VALID; 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)); 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)); } } 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); free(hashish); } 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; 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 case 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); } } } 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, 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 2021. if ((1900 > i) || (i > 2021)) { 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 >2005) { 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)) Rd->stuff->putstr(Rd->stuff, "voucher", voucher); 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)) Rd->stuff->putstr(Rd->stuff, "aboutMe", about); 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) { accountWebHeaders(Rd, oF); accountWebFields(Rd, oF, oV); accountWebSubs(Rd, oF); accountWebFooter(Rd, oF); } static void accountLoginWeb(reqData *Rd, inputForm *oF, inputValue *oV) { if (NULL != Rd->shs.name) free(Rd->shs.name); Rd->shs.name = NULL; if (NULL != Rd->shs.UUID) free(Rd->shs.UUID); Rd->shs.UUID = NULL; accountWebHeaders(Rd, oF); accountWebFields(Rd, oF, oV); accountWebSubs(Rd, oF); accountWebFooter(Rd, oF); } // TODO - accountViewWeb() and accountViewWeb() should view and edit arbitrary accounts the user is not logged in as, // but limit things based on being that viewed / edited account, and the users level. static void accountViewWeb(reqData *Rd, inputForm *oF, inputValue *oV) { char *name = getStrH(Rd->database, "Lua.name"), *level = getStrH(Rd->database, "UserAccounts.UserLevel"), *email = getStrH(Rd->database, "UserAccounts.Email"), *voucher = getStrH(Rd->database, "Lua.voucher"), *approver = getStrH(Rd->database, "Lua.approver"), *about = getStrH(Rd->database, "Lua.aboutMe"); time_t crtd = atol(getStrH(Rd->database, "UserAccounts.Created")); accountWebHeaders(Rd, oF); accountWebFields(Rd, oF, oV); 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, "Approver : %s
", approver); HTMLtextArea(Rd->reply, "aboutMe", "About", 7, 50, 4, 16384, "", "off", "true", "soft", about, FALSE, TRUE); accountWebSubs(Rd, oF); accountWebFooter(Rd, oF); } static void accountEditWeb(reqData *Rd, inputForm *oF, inputValue *oV) { char *name = getStrH(Rd->database, "Lua.name"), *level = getStrH(Rd->database, "UserAccounts.UserLevel"), *email = getStrH(Rd->database, "UserAccounts.Email"), *voucher = getStrH(Rd->database, "Lua.voucher"), *approver = getStrH(Rd->database, "Lua.approver"), *about = getStrH(Rd->database, "Lua.aboutMe"), *lvl = getLevel(atoi(level)); short lv = 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); HTMLtextArea(Rd->reply, "aboutMe", "About", 7, 50, 4, 16384, "", "off", "true", "soft", about, FALSE, TRUE); Rd->reply->addstrf(Rd->reply, "Voucher : %s
", voucher); if (200 <= Rd->shs.level) { qlisttbl_obj_t obj; HTMLhidden(Rd->reply, "approver", approver); 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. if ((is) || ((l <= Rd->shs.level) && (lv <= l))) // As per discussions, can't lower level. Do that in the console. 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->table = "UserAccounts"; uuids->inParams = szi; uuids->outParams = szo; uuids->where = "PrincipalID=?"; dbRequests->addfirst(dbRequests, &uuids, sizeof(dbRequest *)); } static dbRequest *acnts = NULL; if (NULL == acnts) { static char *szi[] = {"FirstName", "LastName", NULL}; static char *szo[] = {NULL}; acnts = xzalloc(sizeof(dbRequest)); acnts->table = "UserAccounts"; acnts->inParams = szi; acnts->outParams = szo; acnts->where = "FirstName=? and LastName=?"; dbRequests->addfirst(dbRequests, &acnts, sizeof(dbRequest *)); } static dbRequest *auth = NULL; if (NULL == auth) { static char *szi[] = {"UUID", NULL}; static char *szo[] = {"passwordSalt", "passwordHash", NULL}; auth = xzalloc(sizeof(dbRequest)); auth->table = "auth"; auth->inParams = szi; auth->outParams = szo; auth->where = "UUID=?"; dbRequests->addfirst(dbRequests, &auth, sizeof(dbRequest *)); } Rd->fromDb = FALSE; // 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) { free(first); 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'; free(last); last = xstrdup(t); } } else { free(last); 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); // LEAKY 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) { V("Found Lua record."); 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")); Rd->database->putstr(Rd->database, "Lua.approver", getStrH(tnm, "approver")); } // else if (rows) if (rows) { rt = rows->rows->size(rows->rows); if (1 == rt) { ret = rt; V("Found database record."); dbPull(Rd, "UserAccounts", rows); char *name = xmprintf("%s %s", getStrH(Rd->database, "UserAccounts.FirstName"), getStrH(Rd->database, "UserAccounts.LastName")); Rd->fromDb = TRUE; Rd->database->putstr(Rd->database, "Lua.name", name); free(name); dbDoSomething(auth, FALSE, getStrH(Rd->database, "UserAccounts.PrincipalID")); // LEAKY rows = auth->rows; if (rows) { if (1 == rows->rows->size(rows->rows)) dbPull(Rd, "auth", rows); else { free(rows->fieldNames); rows->rows->free(rows->rows); free(rows); } } else { free(rows->fieldNames); rows->rows->free(rows->rows); free(rows); } } else { free(rows->fieldNames); rows->rows->free(rows->rows); free(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 { // TODO - check if logged in user is allowed to delete this account // TODO - delete user record // TODO - log the user out if they are logged in } return ret; } // The [create member] button on accountLoginWeb() static int accountCreateSub(reqData *Rd, inputForm *iF, inputValue *iV) { int ret = 0; char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "name"), *last = NULL; int c = accountRead(Rd, uuid, first, last); if (strcmp("POST", Rd->Method) == 0) { if (0 != c) { bitch(Rd, "Cannot create account.", "Account exists."); Rd->shs.status = SHS_NUKE; 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); if (NULL != Rd->shs.name) free(Rd->shs.name); // So that we can get the name later when we show the account data entry page via GET. Rd->shs.name = Rd->stuff->getstr(Rd->stuff, "name", true); free(h); Rd->shs.status = SHS_REFRESH; } free(salt); if (0 != ret) Rd->shs.status = SHS_NUKE; } } return ret; } // The [confirm] button on accountAddWeb() 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); if (0 != c) { bitch(Rd, "Cannot add account.", "Account exists."); Rd->shs.status = SHS_NUKE; 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++; Rd->shs.status = SHS_NUKE; } 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"); // Generate the linky for the email. newSesh(Rd, TRUE); accountWrite(Rd); // log them in I("Logged on %s %s Level %d %s", Rd->shs.UUID, Rd->shs.name, Rd->shs.level, getLevel(Rd->shs.level)); Rd->output = "accountView"; Rd->form = "accountView"; Rd->doit = "login"; Rd->shs.status = SHS_LOGIN; } } return ret; } static int accountSaveSub(reqData *Rd, inputForm *iF, inputValue *iV) { int ret = 0; // Using body[user] here, coz we got to this page via a URL query. char *uuid = Rd->shs.UUID, *first = getStrH(Rd->body, "user"), *last = NULL, *level = getStrH(Rd->database, "UserAccounts.UserLevel"); int c = accountRead(Rd, NULL, first, last); if (1 != c) { bitch(Rd, "Cannot save account.", "Account doesn't exist."); ret++; } else if ((0 == ret) && (strcmp("POST", Rd->Method) == 0)) { char *lvl = getStrH(Rd->body, "level"); qlisttbl_obj_t obj; if (strcmp(level, lvl) != 0) { if (200 <= Rd->shs.level) { I("%s %s approved by %s.", getStrH(Rd->database, "UserAccounts.FirstName"), getStrH(Rd->database, "UserAccounts.LastName"), Rd->shs.name); Rd->stuff->putstr(Rd->stuff, "approver", Rd->shs.name); } else { bitch(Rd, "Cannot change level.", "User level not high enough."); lvl = level; Rd->stuff->putstr(Rd->stuff, "approver", getStrH(Rd->database, "Lua.approver")); } } 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")); 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")); // TODO - this isn't being shown. addStrL(Rd->messages, "Account saved."); } return ret; } // The unique validation URL sent in email. 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); 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->stuff->putstr(Rd->stuff, "approver", getStrH(Rd->database, "Lua.approver")); Rd->shs.level = -100; Rd->database->putstr(Rd->database, "UserAccounts.UserLevel", "-100"); accountWrite(Rd); Rd->doit = "logout"; Rd->output = "accountLogin"; Rd->form = "accountLogin"; Rd->shs.status = SHS_NUKE; } 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); d("Sub accountViewSub() %s %s %s", uuid, first, last); if (1 != c) { bitch(Rd, "Cannot view account.", "Account doesn't exist."); ret++; Rd->shs.status = SHS_NUKE; } 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++; Rd->shs.status = SHS_NUKE; } else { Rd->shs.level = atoi(getStrH(Rd->database, "UserAccounts.UserLevel")); if (NULL != Rd->shs.name) free(Rd->shs.name); Rd->shs.name = Rd->database->getstr(Rd->database, "Lua.name", true); if (NULL != Rd->shs.UUID) free(Rd->shs.UUID); Rd->shs.UUID = Rd->database->getstr(Rd->database, "UserAccounts.PrincipalID", true); free(h); I("Logged on %s %s Level %d %s", Rd->shs.UUID, Rd->shs.name, Rd->shs.level, getLevel(Rd->shs.level)); Rd->shs.status = SHS_LOGIN; } } } 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 { // TODO - check if logged in user is allowed to make these changes // TODO - 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++; } Rd->shs.status = SHS_NUKE; return ret; } /* TODO - instead of searching through all the users, ... have a bunch of separate folders with symlinks scData/users/aaproved scData/users/disabled scData/users/god onefang_rejected.lua -> ../uuid.lua scData/users/newbie foo_bar.lua -> ../uuid.lua scData/users/validated */ 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(qhashtbl_t *)); 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); if (NULL != Rd->shs.name) free(Rd->shs.name); Rd->shs.name = NULL; if (NULL != Rd->shs.UUID) free(Rd->shs.UUID); Rd->shs.UUID = NULL; Rd->shs.level = -256; 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->Script, Rd->Path, nm, obj.name); Rd->reply->addstrf(Rd->reply, "%s | %s | %s |