From bd58d3012c26d16150f650c389d1136741d3939d Mon Sep 17 00:00:00 2001
From: onefang
Date: Tue, 8 Sep 2020 21:34:54 +1000
Subject: Add the SledjChisl stuff.
---
src/sledjchisl/README | 118 +
src/sledjchisl/fcgi_SC.c | 13 +
src/sledjchisl/fcgi_SC.h | 136 +
src/sledjchisl/script.lua | 18 +
src/sledjchisl/sledjchisl.c | 7342 +++++++++++++++++++++++++++++++++++++++++++
5 files changed, 7627 insertions(+)
create mode 100644 src/sledjchisl/README
create mode 100644 src/sledjchisl/fcgi_SC.c
create mode 100644 src/sledjchisl/fcgi_SC.h
create mode 100644 src/sledjchisl/script.lua
create mode 100644 src/sledjchisl/sledjchisl.c
(limited to 'src/sledjchisl')
diff --git a/src/sledjchisl/README b/src/sledjchisl/README
new file mode 100644
index 0000000..31a4b30
--- /dev/null
+++ b/src/sledjchisl/README
@@ -0,0 +1,118 @@
+I'm re-purposing this for SledjHamr https://sledjhamr.org/git/docs/index.html
+
+The general structure of SledjHamr is a bunch of servers talking to each
+other via Internet (or just local) connections. One of them is a web
+server for assets, world data, and inventory. Actually most of OpenSim
+is just a collection of web servers.
+
+Originally I didn't think using a web based world client was a good idea,
+however it might be better to have one, for reasons. Now I need a web
+management console that can do all the things the current tmux console
+can, including OpenSim console and commands. Plus account management for
+users. I can also use a web based Jabber / XMPP front end to chat, IM,
+and group chatter, which would run in the normal viewers web browser.
+This provides a doorway into putting SledjHamr stuff in existing viewers
+without needing them to support it. So a web based viewer now makes more
+sense, and also means we can get away with not needing a viewer at all.
+
+Toybox itself doesn't include a web server, and I don't think there is
+one on the roadmap. So we have to use an external web server, which was
+a design goal of SledjHamr in the first place, using existing mature
+HTTP infrastructure, coz that's already solved problems for a bunch of
+things that plague OS/SL to this day. Clear your cache! Pffft.
+
+So sledjchisl.c will be the "love world server", though initially it just
+drives OpenSim_SC in tmux via tmux commands to send keys and read output.
+Later it might run opensim_SC directly and use STDIN and STDOUT to do
+everything. It'll also provide the text management front end that runs
+in the left tmux panel of the first window, which is why it's based on
+boxes in the first place. Later still it can take over opensim_SC
+functions as I move them out of mono.
+
+We will need a text, web, and GUI version of this management front end.
+Hmmm, maybe don't need a GUI version, GUI users can just run a terminal.
+
+After much research, FastCGI / FCGI seems to be the most portable way of
+interfacing with existing web servers. FCGI protocol closes STDERR and
+STDOUT, and uses STDIN as it's two way communications channel to the web
+server, so our FCGI module can't be used as the text management front
+end. This is probably a good idea to keep them seperate anyway, for
+security, coz the web server is exposed to the world, the console isn't.
+
+Currently sledjchisl.c tests to see if it's running in tmux already, if
+it isn't it starts up tmux runs itself into this new tmux, then exits.
+So it could also test if it's running from FCGI, and switch to web mode,
+then it'll need to find the tmuxed instance to send commands to it.
+Either via nails connection, or sending tmux commands via shell.
+
+FCGI has methods of dealing with auth and templates. B-)
+
+So for now I think I'll have the text and web management front ends in
+sledjchisl.c, and the love world server as well. I can split them up
+later if I need to.
+
+
+--------------------------------------------------------------------
+
+How to install it.
+------------------
+
+It's all still partly written, un-released, and experimental at the
+moment. So thes are just rough notes about what is needed.
+
+There are two parts, the OpenSim runner part and the web pages part. The
+OpenSim runner part requjires the rest of opensim-SC to be installed,
+which is covered by other documents in this source code repo. SO the
+below only talks about the web pages part.
+
+So far I have only tried this with Apache 2, but it should work fine with
+other web servers that support FCGI. I'm using spawn-fcgi which was
+written for lighttpd, and seems to be the proper way to support FCGI in
+Nginx as well.
+
+Until I have actually released this, it'll be source code only. So you
+need a C development environment to compile all the C source code.
+
+Some of the dependencies are included, like LuaJIT, qLibc, the FCGI SDK,
+and toybox. Or at least their source code git ropes are cloned during
+the build stage. The other dependencies are the development environments
+for MariaDB or MySQL (only tested with MariaDB), OpenSSL, and UUID. And
+spawn-fcgi. In a Debian based Linux distro, that could be installed by
+something like -
+
+apt install libmariadbclient-dev libssl1.0-dev uuid-dev spawn-fcgi
+
+Once you have all of that, you can run the src/BuildIt.sh script to put
+it all together. That script will also actually run the web side of
+things. Often during this early development process, that script will
+run things under a test tool like valgrind, so you'll probably need that
+to.
+
+You'll need to configure your web server to pass web requests onto
+sledjchisl. I'll add instructions for other popular web servers later,
+but for now, this is what to do with Apache 2.
+
+Load the mod_proxy_fcgi module. Include something like this in your
+virtual host definition -
+
+ \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 *Tconsole = "SledjChisl";
+char *Tsocket = "opensim-tmux.socket";
+char *Ttab = "SC";
+char *Tcmd = "tmux -S";
+char *webRoot = "/var/www/html";
+char *URL = "fcgi-bin/sledjchisl.fcgi";
+char *ToS = "Be good.";
+char *webIframers = "";
+int seshRenew = 10 * 60;
+int idleTimeOut = 30 * 60;
+int seshTimeOut = 24 * 60 * 60;
+int newbieTimeOut = 30;
+float loadAverageInc = 0.5;
+int simTimeOut = 45;
+boolean DEBUG = TRUE;
+qhashtbl_t *mimeTypes;
+qlist_t *dbRequests;
+
+
+// TODO - log to file. The problem is we don't know where to log until after we have loaded the configs, and before that we are spewing log messages.
+// Now that we are using spawn-fcgi, all the logs are going to STDERR, which we can capture and write to a file.
+// Unfortunately spawn-fcgi in deamon mode sends all the output to /dev/null or something.
+// A better idea, when we spawn tmux or spawn-fcgi, capture STDERR, full log everything to that, filtered log to the tmux console (STDOUT).
+// Then we can use STDOUT / STDIN to run the console stuff.
+
+// 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
+ "90", "DEBUG", // grey
+// VERBOSE? UNKNOWN? FATAL? SILENT? All from Android aparently.
+ "35", "debug", // magenta
+ "34", "timeout", // blue
+};
+
+#define DATE_TIME_LEN 42
+void logMe(int v, char *format, ...)
+{
+ va_list va, va2;
+ int len;
+ char *ret;
+ struct timeval tv;
+ time_t curtime;
+ char date[DATE_TIME_LEN];
+
+ va_start(va, format);
+ va_copy(va2, va);
+ // How long is it?
+ len = vsnprintf(0, 0, format, va);
+ len++;
+ va_end(va);
+ // Allocate and do the sprintf()
+ ret = xmalloc(len);
+ vsnprintf(ret, len, format, va2);
+ va_end(va2);
+
+ gettimeofday(&tv, NULL);
+ curtime = tv.tv_sec;
+ strftime(date, DATE_TIME_LEN, "(%Z %z) %F %T", localtime(&curtime));
+
+ v *= 2;
+ fprintf(stderr, "%s.%.6ld \e[%sm%-8s sledjchisl: %s\e[0m\n", date, tv.tv_usec, logTypes[v], logTypes[v + 1], ret);
+ free(ret);
+}
+#define C(...) logMe(0, __VA_ARGS__)
+#define E(...) logMe(1, __VA_ARGS__)
+#define W(...) logMe(2, __VA_ARGS__)
+#define T(...) logMe(3, __VA_ARGS__)
+#define I(...) logMe(4, __VA_ARGS__)
+#define D(...) logMe(5, __VA_ARGS__)
+#define d(...) logMe(6, __VA_ARGS__)
+#define t(...) logMe(7, __VA_ARGS__)
+
+
+static void addStrL(qlist_t *list, char *s)
+{
+ list->addlast(list, s, strlen(s) + 1);
+}
+
+static char *getStrH(qhashtbl_t *hash, char *key)
+{
+ char *ret = "", *t;
+
+ t = hash->getstr(hash, key, false);
+ if (NULL != t)
+ ret = t;
+ return ret;
+}
+
+
+char *myHMAC(char *in, boolean b64)
+{
+ EVP_MD_CTX *mdctx = EVP_MD_CTX_create(); // Gets renamed to EVP_MD_CTX_new() in later versions.
+ unsigned char md_value[EVP_MAX_MD_SIZE];
+ unsigned int md_len;
+
+ EVP_DigestInit_ex(mdctx, EVP_sha512(), NULL); // EVP_sha3_512() isn't available until later versions.
+ EVP_DigestUpdate(mdctx, in, strlen(in));
+ EVP_DigestFinal_ex(mdctx, md_value, &md_len);
+ EVP_MD_CTX_destroy(mdctx); // Gets renamed to EVP_MD_CTX_free() in later versions.
+
+ if (b64)
+ return qB64_encode(md_value, md_len);
+ else
+ return qhex_encode(md_value, md_len);
+}
+
+char *myHMACkey(char *key, char *in, boolean b64)
+{
+ unsigned char md_value[EVP_MAX_MD_SIZE];
+ unsigned int md_len;
+ unsigned char* digest = HMAC(EVP_sha512(), key, strlen(key), (unsigned char *) in, strlen(in), md_value, &md_len);
+
+ if (b64)
+ return qB64_encode(md_value, md_len);
+ else
+ return qhex_encode(md_value, md_len);
+}
+
+
+// In Lua 5.0 reference manual is a table traversal example at page 29.
+void PrintTable(lua_State *L)
+{
+ lua_pushnil(L);
+
+ while (lua_next(L, -2) != 0)
+ {
+ // Numbers can convert to strings, so check for numbers before checking for strings.
+ if (lua_isnumber(L, -1))
+ printf("%s = %f\n", lua_tostring(L, -2), lua_tonumber(L, -1));
+ else if (lua_isstring(L, -1))
+ printf("%s = '%s'\n", lua_tostring(L, -2), lua_tostring(L, -1));
+ else if (lua_istable(L, -1))
+ PrintTable(L);
+ lua_pop(L, 1);
+ }
+}
+
+
+int sendTmuxKeys(char *dest, char *keys)
+{
+ int ret = 0, i;
+ char *c = xmprintf("%s %s/%s send-keys -t %s:%s '%s'", Tcmd, scRun, Tsocket, Tconsole, dest, keys);
+
+ i = system(c);
+ if (!WIFEXITED(i))
+ E("tmux send-keys command failed!");
+ free(c);
+ return ret;
+}
+
+int sendTmuxCmd(char *dest, char *cmd)
+{
+ int ret = 0, i;
+ char *c = xmprintf("%s %s/%s send-keys -t %s:'%s' '%s' Enter", Tcmd, scRun, Tsocket, Tconsole, dest, cmd);
+
+ i = system(c);
+ if (!WIFEXITED(i))
+ E("tmux send-keys command failed!");
+ free(c);
+ return ret;
+}
+
+void waitTmuxText(char *dest, char *text)
+{
+ int i;
+ // Using " for the grep pattern, coz ' might be used in a sim name.
+// TODO - should escape \ " ` in text.
+ char *c = xmprintf("sleep 5; %s %s/%s capture-pane -t %s:'%s' -p | grep -F \"%s\" 2>&1 > /dev/null", Tcmd, scRun, Tsocket, Tconsole, dest, text);
+
+ D("Waiting for '%s'.", text);
+ do
+ {
+ i = system(c);
+ if (!WIFEXITED(i))
+ {
+ E("tmux capture-pane command failed!");
+ break;
+ }
+ else if (0 == WEXITSTATUS(i))
+ break;
+ } while (1);
+
+ free(c);
+}
+
+float waitLoadAverage(float la, float extra, int timeout)
+{
+ struct sysinfo info;
+ struct timespec timeOut;
+ float l;
+ int to = timeout;
+
+ T("Sleeping until load average is below %.02f (%.02f + %.02f) or for %d seconds.", la + extra, la, extra, timeout);
+ clock_gettime(CLOCK_MONOTONIC, &timeOut);
+ to += timeOut.tv_sec;
+
+ do
+ {
+ msleep(5000);
+ sysinfo(&info);
+ l = info.loads[0]/65536.0;
+ clock_gettime(CLOCK_MONOTONIC, &timeOut);
+ timeout -= 5;
+ t("Tick, load average is %.02f, countdown %d seconds.", l, timeout);
+ } while (((la + extra) < l) && (timeOut.tv_sec < to));
+
+ return l;
+}
+
+
+// Rob forget to do this, but at least he didn't declare it static.
+struct dirtree *dirtree_handle_callback(struct dirtree *new, int (*callback)(struct dirtree *node));
+
+typedef struct _simList simList;
+struct _simList
+{
+ int len, num;
+ char **sims;
+};
+
+static int filterSims(struct dirtree *node)
+{
+ if (!node->parent) return DIRTREE_RECURSE | DIRTREE_SHUTUP;
+ if ((strncmp(node->name, "sim", 3) == 0) && ((strcmp(node->name, "sim_skeleton") != 0)))
+ {
+ simList *list = (simList *) node->parent->extra;
+
+ if ((list->num + 1) > list->len)
+ {
+ list->len = list->len + 1;
+ list->sims = xrealloc(list->sims, list->len * sizeof(char *));
+ }
+ list->sims[list->num] = xstrdup(node->name);
+ list->num++;
+ }
+ return 0;
+}
+
+// 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;
+}
+
+simList *getSims()
+{
+ simList *sims = xmalloc(sizeof(simList));
+ memset(sims, 0, sizeof(simList));
+ char *path = xmprintf("%s/config", scRoot);
+ struct dirtree *new = dirtree_add_node(0, path, 0);
+ new->extra = (long) sims;
+ dirtree_handle_callback(new, filterSims);
+
+ qsort(sims->sims, sims->num, sizeof(char *), qstrcmp);
+ free(path);
+ return sims;
+}
+
+void freeSimList(simList *sims)
+{
+ int i;
+
+ for (i = 0; i < sims->num; i++)
+ free(sims->sims[i]);
+ free(sims->sims);
+ free(sims);
+}
+
+static int filterInis(struct dirtree *node)
+{
+ if (!node->parent) return DIRTREE_RECURSE | DIRTREE_SHUTUP;
+ int l = strlen(node->name);
+ if (strncmp(&(node->name[l - 4]), ".ini", 4) == 0)
+ {
+ strcpy((char *) node->parent->extra, node->name);
+ return DIRTREE_ABORT;
+ }
+ return 0;
+}
+
+char *getSimName(char *sim)
+{
+ char *ret = NULL;
+ char *c = xmprintf("%s/config/%s", scRoot, sim);
+ struct dirtree *new = dirtree_add_node(0, c, 0);
+
+ free(c);
+ c = xzalloc(1024);
+ new->extra = (long) c;
+ dirtree_handle_callback(new, filterInis);
+ if ('\0' != c[0])
+ {
+ char *temp = NULL;
+ regex_t pat;
+ regmatch_t m[2];
+ long len;
+ int fd;
+
+ temp = xmprintf("%s/config/%s/%s", scRoot, sim, c);
+ fd = xopenro(temp);
+ xregcomp(&pat, "RegionName = \"(.+)\"", REG_EXTENDED);
+ do
+ {
+ // TODO - get_line() is slow, and wont help much with DOS and Mac line endings.
+ // gio_gets() isn't any faster really, but deals with DOS line endings at least.
+ free(temp);
+ temp = get_line(fd);
+ if (temp)
+ {
+ if (!regexec(&pat, temp, 2, m, 0))
+ {
+ // Return first parenthesized subexpression as string.
+ if (pat.re_nsub > 0)
+ {
+ ret = xmprintf("%.*s", (int) (m[1].rm_eo - m[1].rm_so), temp + m[1].rm_so);
+ free(temp);
+ break;
+ }
+ }
+ }
+ } while (temp);
+ regfree(&pat);
+ xclose(fd);
+ }
+ free(c);
+ return ret;
+}
+
+
+// Expects either "simXX" or "ROBUST".
+int checkSimIsRunning(char *sim)
+{
+ int ret = 0;
+ struct stat st;
+
+ // Check if it's running.
+ char *path = xmprintf("%s/caches/%s.pid", scRoot, sim);
+ if (0 == stat(path, &st))
+ {
+ int fd, i;
+ char *pid = NULL;
+
+ // Double check if it's REALLY running.
+ if ((fd = xopenro(path)) == -1)
+ perror_msg("xopenro(%s)", path);
+ else
+ {
+ pid = get_line(fd);
+ if (NULL == pid)
+ perror_msg("get_line(%s)", path);
+ else
+ {
+ xclose(fd);
+
+// I'd rather re-use the toysh command running stuff, since ps is a toy, but that's private.
+// TODO - switch to toybox ps and rm.
+ free(path);
+ path = xmprintf("ps -p %s --no-headers -o comm", pid);
+ i = system(path);
+ if (WIFEXITED(i))
+ {
+ if (0 != WEXITSTATUS(i)) // No such pid.
+ {
+ free(path);
+ path = xmprintf("rm -f %s/caches/%s.pid", scRoot, sim);
+ d("%s", path);
+ i = system(path);
+ }
+ else
+ d("checkSimIsRunning(%s) has PID %s, which is actually running.", sim, pid);
+ }
+ }
+ }
+ free(pid);
+ }
+
+ // Now check if it's really really running. lol
+ free(path);
+ path = xmprintf("%s/caches/%s.pid", scRoot, sim);
+ if (0 == stat(path, &st))
+ {
+ D("checkSimIsRunning(%s) -> %s is really really running.", sim, path);
+ ret = 1;
+ }
+ else
+ D("checkSimIsRunning(%s) -> %s is not running.", sim, path);
+
+ free(path);
+ return ret;
+}
+
+
+static void PrintEnv(qgrow_t *reply, char *label, char **envp)
+{
+ reply->addstrf(reply, "%s:
\n\n", label);
+ for ( ; *envp != NULL; envp++)
+ reply->addstrf(reply, "%s\n", *envp);
+ reply->addstr(reply, "
\n");
+}
+
+static void printEnv(char **envp)
+{
+ for ( ; *envp != NULL; envp++)
+ D("%s", *envp);
+}
+
+
+typedef struct _rowData rowData;
+struct _rowData
+{
+ char **fieldNames;
+ qlist_t *rows;
+};
+
+static void dumpHash(qhashtbl_t *tbl)
+{
+ qhashtbl_obj_t obj;
+
+ memset((void*)&obj, 0, sizeof(obj));
+ tbl->lock(tbl);
+ while(tbl->getnext(tbl, &obj, true) == true)
+ d("%s = %s", obj.name, (char *) obj.data);
+ tbl->unlock(tbl);
+}
+
+static void dumpArray(int d, char **ar)
+{
+ int i = 0;
+
+ while (ar[i] != NULL)
+ {
+ d("%d %d %s", d, i, ar[i]);
+ i++;
+ }
+}
+
+
+
+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);
+//T("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);
+//T("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);
+//T("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);
+//T("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);
+//T("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);
+//T("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);
+//T("DOUBLE %d %s %d = %f", i, fld->name, req->inBind[i].buffer_length, d);
+ break;
+ }
+
+ case MYSQL_TYPE_NEWDECIMAL:
+ {
+//T("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);
+//T("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';
+//T("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.
+//T("BLOBs %d %s %d", i, fld->name, req->inBind[i].buffer_length);
+ break;
+ }
+
+ case MYSQL_TYPE_BIT:
+ {
+//T("BIT %d %s %d", i, fld->name, req->inBind[i].buffer_length);
+ break;
+ }
+
+ case MYSQL_TYPE_NULL:
+ {
+//T("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;
+ }
+
+ I("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. " + "It will be from %s@%s, and it might be in your spam folder, coz these sorts of emails sometimes end up there. " + "You should add that email address to your contacts, or otherwise let it through your spam filter. " +// "%s" + "
\n", + "grid_no_reply", Rd->Host, + Rd->Host, Rd->RUri +// ,t1, t0 + ); + free(t1); + } + return ret; +} + + +static void freeSesh(reqData *Rd, boolean linky, boolean wipe) +{ + char *file = NULL; + sesh *shs = &Rd->shs; + +T("free sesh %s %s", linky ? "linky" : "session", wipe ? "wipe" : "delete"); + if (linky) + { + shs = Rd->lnk; + file = xmprintf("%s/sessions/%s.linky", scCache, shs->leaf); + } + else + file = xmprintf("%s/sessions/%s.lua", scCache, shs->leaf); + + if (wipe) + I("Wiping session %s.", file); + else + I("Deleting session %s.", file); + + if ('\0' != shs->leaf[0]) + { + if (unlink(file)) + perror_msg("Unable to delete %s", file); + } + + Rd->body-> remove(Rd->body, "munchie"); + + 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" + "\n" + "Do not reply to this email.\n" + "\n", + Rd->Host, Rd->Host, Rd->Host, + getStrH(Rd->stuff, "email"), + Rd->Host, Rd->Host, + first, last, + first, last, Rd->Host, Rd->RUri, + Rd->Host, Rd->RUri, t1 + ); + l = strlen(content); + file = xmprintf("%s/sessions/%s.email", scCache, shs->leaf); + fd = notstdio(xcreate_stdio(file, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, S_IRUSR | S_IWUSR)); + + if (l != writeall(fd, content, l)) + { + perror_msg("Writing %s", file); +// freeSesh(Rd, linky, TRUE); + } + xclose(fd); + I("Sending linky email to %s %s", getStrH(Rd->stuff, "email"), t1); + command = xmprintf("sendmail -oi -t <'%s'", file); + int i = system(command); + if (!WIFEXITED(i)) + E("sendmail command failed!"); + free(command); + free(file); + free(content); + free(t1); + 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} +}; + +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; +} + +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, "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. + "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 + simList *sims = getSims(); + struct sysinfo info; + float la; + + sysinfo(&info); + la = info.loads[0]/65536.0; + + for (i = 0; i < sims->num; i++) + { + char *sim = sims->sims[i], *name = getSimName(sims->sims[i]); + + if (checkSimIsRunning(sim)) + { + I("Loading default member IAR for %s %s in sim %s, this might take a couple of minutes.", first, last, name); + char *c = xmprintf("%s %s/%s send-keys -t '%s:%d' 'load iar -m %s %s / password /opt/opensim_SC/backups/DefaultMember.IAR' Enter", + Tcmd, scRun, Tsocket, Tconsole, i + 1, first, last); +T(c); + int r = system(c); + if (!WIFEXITED(r)) + E("tmux load iar command failed!"); + else + { +// memset(toybuf, 0, sizeof(toybuf)); +// snprintf(toybuf, sizeof(toybuf), "INITIALIZATION COMPLETE FOR %s", name); +// waitTmuxText(name, toybuf); +// I("%s is done starting up.", name); +// la = waitLoadAverage(la, loadAverageInc, simTimeOut); + } + free(c); + free(name); + break; + } + free(name); + } + freeSimList(sims); + } + 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) +*/ + +// lua.h has LUA_T* NONE, NIL, BOOLEAN, LIGHTUSERDATA, NUMBER, STRING, TABLE, FUNCTION, USERDATA, THREAD as defines, -1 - 8. +// These are the missing ones. Then later we will have prim, mesh, script, sound, terrain, ... +#define LUA_TGROUP 42 +#define LUA_TINTEGER 43 +#define LUA_TEMAIL 44 +#define LUA_TPASSWORD 45 +#define LUA_TFILE 46 +#define LUA_TIMAGE 47 + +#define FLD_NONE 0 +#define FLD_EDITABLE 1 +#define FLD_HIDDEN 2 +#define FLD_REQUIRED 4 + +typedef struct _inputField inputField; +typedef struct _inputSub inputSub; +typedef struct _inputForm inputForm; +typedef struct _inputValue inputValue; + +typedef int (*inputFieldValidFunc) (reqData *Rd, inputForm *iF, inputValue *iV); +typedef void (*inputFieldShowFunc) (reqData *Rd, inputForm *iF, inputValue *iV); +typedef int (*inputSubmitFunc) (reqData *Rd, inputForm *iF, inputValue *iV); +typedef void (*inputFormShowFunc) (reqData *Rd, inputForm *iF, inputValue *iV); + +struct _inputField +{ + char *name, *title, *help; + inputFieldValidFunc validate; // Alas C doesn't have any anonymous function standard. + inputFieldShowFunc web, console, gui; + inputField **group; // If this is a LUA_TGROUP, then this will be a null terminated array of the fields in the group. +// database details +// lua file details + signed char type, flags; + short editLevel, viewLevel, viewLength, maxLength; +}; +struct _inputSub +{ + char *name, *title, *help, *outputForm; + inputSubmitFunc submit; +}; +struct _inputForm +{ + char *name, *title, *help; + qlisttbl_t *fields; // qlisttbl coz iteration in order and lookup are important. + qhashtbl_t *subs; + inputFormShowFunc web, eWeb; // display web, console, gui; +// read function +// write function +}; +struct _inputValue +{ + inputField *field; + void *value; // If this is a LUA_TGROUP, then this will be a null. + short valid; // 0 for not yet validated, negative for invalid, positive for valid. + short source, index; +}; + + +static int sessionValidate(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0; + boolean linky = FALSE; + char *toke_n_munchie = "", *munchie = "", *hashish = "", *leaf = "", *timeStamp = "", *seshion = "", *seshID = "", *t0, *t1; + + // In this case the session stuff has to come from specific places. + hashish = 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 2020. + if ((1900 > i) || (i > 2020)) + { + bitch(Rd, "Please supply a year of birth.", "Out of range."); + ret++; + } + else if (i < 1901) + { + bitch(Rd, "Please supply a proper year of birth.", "Out of range, too old."); + ret++; + } + else if (i >2004) + { + bitch(Rd, "This grid is Adult rated, you are too young.", "Out of range, too young."); + ret++; + } + } + t1 = (char *) (iV + 1)->value; + if ((NULL == t1) || ('\0' == t1[0])) + { + bitch(Rd, "Please supply a month of birth.", "None supplied."); + ret++; + } + else + { + for (i = 0; i < 12; i++) + { + if (strcmp(months[i], t1) == 0) + break; + } + if (12 == i) + { + bitch(Rd, "Please supply a month of birth.", "Out of range"); + ret++; + } + } + + if (0 == ret) + { + Rd->stuff->putstr(Rd->stuff, "year", t0); + Rd->stuff->putstr(Rd->stuff, "month", t1); + Rd->stuff->putstrf(Rd->stuff, "DoB", "%s %s", t0, t1); + } + } + + return ret; +} +static void DoByWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + char *tmp = xmalloc(16), *t; + int i, d; + + Rd->reply->addstr(Rd->reply, "\n"); +} +static void DoBWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ +} + +static int legalValidate(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0, i; + char *t; + inputField **group = iV->field->group; + + i = iV->index; + if (2 == i) + { + t = (char *) iV->value; + if ((NULL == t) || (strcmp("on", t) != 0)) + { + bitch(Rd, "You must be an adult to enter this site.", ""); + ret++; + } + else + Rd->stuff->putstr(Rd->stuff, "adult", t); + t = (char *) (iV + 1)->value; + if ((NULL == t) || (strcmp("on", t) != 0)) + { + bitch(Rd, "You must agree to the Terms & Conditions of Use.", ""); + ret++; + } + else + Rd->stuff->putstr(Rd->stuff, "agree", t); + } + + return ret; +} +static void adultWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + HTMLcheckBox(Rd->reply, oV->field->name, oV->field->title, !strcmp("on", getStrH(Rd->body, "adult")), oV->field->flags & FLD_REQUIRED); +} +static void agreeWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + HTMLcheckBox(Rd->reply, oV->field->name, oV->field->title, !strcmp("on", getStrH(Rd->body, "agree")), oV->field->flags & FLD_REQUIRED); +} +static void legalWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ +} +static void ToSWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + Rd->reply->addstrf(Rd->reply, "%s\n", getStrH(Rd->configs, "ToS")); +} + +static int voucherValidate(reqData *Rd, inputForm *oF, inputValue *oV) +{ + int ret = 0; + char *voucher = (char *) oV->value; + + if ((NULL == voucher) || ('\0' == voucher[0])) + { + bitch(Rd, "Please fill in the 'Voucher' section.", "None supplied."); + ret++; + } + + if ((0 == ret) && (NULL != voucher)) + 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"), + *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); + 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"), + *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; + + 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) + { +T("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")); + } +// else if (rows) + if (rows) + { + rt = rows->rows->size(rows->rows); + if (1 == rt) + { + ret = rt; +T("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 + { +// check if logged in user is allowed to delete this account +// delete user record +// log the user out if they are logged in + } + return ret; +} + +// 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; + 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)) + { + Rd->stuff->putstr(Rd->stuff, "email", getStrH(Rd->database, "UserAccounts.Email")); + Rd->stuff->putstr(Rd->stuff, "created", getStrH(Rd->database, "UserAccounts.Created")); + Rd->stuff->putstr(Rd->stuff, "flags", getStrH(Rd->database, "UserAccounts.UserFlags")); + Rd->stuff->putstr(Rd->stuff, "active", getStrH(Rd->database, "UserAccounts.active")); + Rd->stuff->putstr(Rd->stuff, "passwordSalt", getStrH(Rd->database, "auth.passwordSalt")); + Rd->stuff->putstr(Rd->stuff, "passwordHash", getStrH(Rd->database, "auth.passwordHash")); + Rd->stuff->putstr(Rd->stuff, "name", getStrH(Rd->database, "Lua.name")); + Rd->stuff->putstr(Rd->stuff, "DoB", getStrH(Rd->database, "Lua.DoB")); + Rd->stuff->putstr(Rd->stuff, "agree", getStrH(Rd->database, "Lua.agree")); + Rd->stuff->putstr(Rd->stuff, "adult", getStrH(Rd->database, "Lua.adult")); + Rd->stuff->putstr(Rd->stuff, "aboutMe", getStrH(Rd->database, "Lua.aboutMe")); + Rd->stuff->putstr(Rd->stuff, "vouched", getStrH(Rd->database, "Lua.vouched")); + Rd->stuff->putstr(Rd->stuff, "voucher", getStrH(Rd->database, "Lua.voucher")); + + char *lvl = getStrH(Rd->body, "level"); + qlisttbl_obj_t obj; + + memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call + accountLevels->lock(accountLevels); + while(accountLevels->getnext(accountLevels, &obj, NULL, false) == true) + { + if (strcmp(lvl, (char *) obj.data) == 0) + Rd->database->putstr(Rd->database, "UserAccounts.UserLevel", obj.name); + } + accountLevels->unlock(accountLevels); + accountWrite(Rd); + free(Rd->outQuery); + Rd->outQuery = xmprintf("?user=%s+%s", getStrH(Rd->database, "UserAccounts.FirstName"), getStrH(Rd->database, "UserAccounts.LastName")); +// 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->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 + { +// check if logged in user is allowed to make these changes +// update user record + } + return ret; +} + +static int accountExploreSub(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0; +// get a list of user records + return ret; +} + +static int accountOutSub(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0; + char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName"); + int c = accountRead(Rd, uuid, first, last); + + if (1 != c) + { +// bitch(Rd, "Cannot logout account.", "Account doesn't exist."); +// ret++; + } + + 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 |