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 - + + + SetHandler "proxy:unix:///opt/opensim_SC/var/cache/sledjchisl.socket|fcgi://localhost/" + + +Adjust that unix:// path to match if you have installed things elsewhere. +Also make that directory readable by the web server group. + +Copy the files in example/www to where eveqer your web servers document +root is. + +Note that the account.html dynamic web page redirects to a HTTPS version +of itself, so you'll need HTTPS to be working. + +The current web pages will then be available at something like - + +http://localhost/sledjchisl.fcgi/loginpage.html + +http://localhost/sledjchisl.fcgi/stats.html + +https://localhost/sledjchisl.fcgi/account.html + diff --git a/src/sledjchisl/fcgi_SC.c b/src/sledjchisl/fcgi_SC.c new file mode 100644 index 0000000..36aba77 --- /dev/null +++ b/src/sledjchisl/fcgi_SC.c @@ -0,0 +1,13 @@ +/* fcgi_SC.h - Generic fcgi handler, coz the others all suck. + * + * Copyright 2020 David Seikel + */ + +// I use camelCaseNames internally, instead of underscore_names as is preferred +// in the rest of toybox. A small limit of 80 characters per source line infers +// shorter names should be used. CamelCaseNames are shorter. Externally visible +// stuff is underscore_names as usual. Plus, I'm used to camelCaseNames, my +// fingers twitch that way. + +#include "toys.h" +#include "fcgi_SC.h" diff --git a/src/sledjchisl/fcgi_SC.h b/src/sledjchisl/fcgi_SC.h new file mode 100644 index 0000000..2b3fa65 --- /dev/null +++ b/src/sledjchisl/fcgi_SC.h @@ -0,0 +1,136 @@ +/* fcgi_SC.h - Generic fcgi handler, coz the oters all suck. + * + * Copyright 2020 David Seikel + */ + +enum fcgiEventType{ + FSC_CSI, + FSC_KEYS, + FSC_MOUSE, + FSC_RAW +}; + +struct fcgiEvent { + enum fcgiEventType type; // The type of this event. + char *sequence; // Either a translated sequence, or raw bytes. + int isTranslated; // Whether or not sequence is translated. + int count; // Number of entries in params. + int *params; // For CSI events, the decoded parameters. +}; + + + + +// From the spec. + +/* + * Listening socket file number + */ +#define FCGI_LISTENSOCK_FILENO 0 + +typedef struct { + unsigned char version; + unsigned char type; + unsigned char requestIdB1; + unsigned char requestIdB0; + unsigned char contentLengthB1; + unsigned char contentLengthB0; + unsigned char paddingLength; + unsigned char reserved; +} FCGI_Header; + +/* + * Number of bytes in a FCGI_Header. Future versions of the protocol + * will not reduce this number. + */ +#define FCGI_HEADER_LEN 8 + +/* + * Value for version component of FCGI_Header + */ +#define FCGI_VERSION_1 1 + +/* + * Values for type component of FCGI_Header + */ +#define FCGI_BEGIN_REQUEST 1 +#define FCGI_ABORT_REQUEST 2 +#define FCGI_END_REQUEST 3 +#define FCGI_PARAMS 4 +#define FCGI_STDIN 5 +#define FCGI_STDOUT 6 +#define FCGI_STDERR 7 +#define FCGI_DATA 8 +#define FCGI_GET_VALUES 9 +#define FCGI_GET_VALUES_RESULT 10 +#define FCGI_UNKNOWN_TYPE 11 +#define FCGI_MAXTYPE (FCGI_UNKNOWN_TYPE) + +/* + * Value for requestId component of FCGI_Header + */ +#define FCGI_NULL_REQUEST_ID 0 + +typedef struct { + unsigned char roleB1; + unsigned char roleB0; + unsigned char flags; + unsigned char reserved[5]; +} FCGI_BeginRequestBody; + +typedef struct { + FCGI_Header header; + FCGI_BeginRequestBody body; +} FCGI_BeginRequestRecord; + +/* + * Mask for flags component of FCGI_BeginRequestBody + */ +#define FCGI_KEEP_CONN 1 + +/* + * Values for role component of FCGI_BeginRequestBody + */ +#define FCGI_RESPONDER 1 +#define FCGI_AUTHORIZER 2 +#define FCGI_FILTER 3 + +typedef struct { + unsigned char appStatusB3; + unsigned char appStatusB2; + unsigned char appStatusB1; + unsigned char appStatusB0; + unsigned char protocolStatus; + unsigned char reserved[3]; +} FCGI_EndRequestBody; + +typedef struct { + FCGI_Header header; + FCGI_EndRequestBody body; +} FCGI_EndRequestRecord; + +/* + * Values for protocolStatus component of FCGI_EndRequestBody + */ +#define FCGI_REQUEST_COMPLETE 0 +#define FCGI_CANT_MPX_CONN 1 +#define FCGI_OVERLOADED 2 +#define FCGI_UNKNOWN_ROLE 3 + +/* + * Variable names for FCGI_GET_VALUES / FCGI_GET_VALUES_RESULT records + */ +#define FCGI_MAX_CONNS "FCGI_MAX_CONNS" +#define FCGI_MAX_REQS "FCGI_MAX_REQS" +#define FCGI_MPXS_CONNS "FCGI_MPXS_CONNS" + +typedef struct { + unsigned char type; + unsigned char reserved[7]; +} FCGI_UnknownTypeBody; + +typedef struct { + FCGI_Header header; + FCGI_UnknownTypeBody body; +} FCGI_UnknownTypeRecord; + diff --git a/src/sledjchisl/script.lua b/src/sledjchisl/script.lua new file mode 100644 index 0000000..1e4b909 --- /dev/null +++ b/src/sledjchisl/script.lua @@ -0,0 +1,18 @@ +-- script.lua + +-- This works coz LuaJIT automatically loads the jit module. +if type(jit) == 'table' then + io.write('script.lua is being run by ' .. jit.version .. ' under ' .. jit.os .. ' on a ' .. jit.arch .. '\n') +else + io.write('script.lua is being run by Lua version ' .. _VERSION .. '\n') +end + +-- Receives a table, returns the sum of its components. +io.write("The table the script received has:\n"); +x = 0 +for i = 1, #foo do + print(i, foo[i]) + x = x + foo[i] +end +io.write("Returning data back to C\n"); +return x diff --git a/src/sledjchisl/sledjchisl.c b/src/sledjchisl/sledjchisl.c new file mode 100644 index 0000000..42ed205 --- /dev/null +++ b/src/sledjchisl/sledjchisl.c @@ -0,0 +1,7342 @@ +/* sledjchisl.c - opensim-SC management system. + * + * Copyright 2020 David Seikel + * Not in SUSv4. An entirely new invention, thus no web site either. + +USE_SLEDJCHISL(NEWTOY(sledjchisl, "m(mode):", TOYFLAG_USR|TOYFLAG_BIN)) + +config SLEDJCHISL + bool "sledjchisl" + default y + help + usage: sledjchisl [-m|--mode mode] + + opensim-SC management system. +*/ + + +// TODO - figure out how to automate testing of this. +// Being all interactive and involving external web servers / viewers makes it hard. + +// TODO - do all the setup on first run, and check if needed on each start up, to be self healing. + +// TODO - pepper could be entered on the console on startup if it's not defined, as a master password sort of thing. +// I'd go as far as protecting the database credentials that way, but legacy OpenSim needs it unprotected. +// Also keep in mind, people want autostart of their services without having to enter passwords on each boot. + +// TODO - once it is event driven, periodically run things like session clean ups, self healing, and the secure.sh thing. +// And backups off course. +// As well as regular database pings to keep the connection open. + +#include +#ifdef _WIN32 +#include +#else +extern char **environ; +#endif +// Don't overide standard stdio stuff. +#define NO_FCGI_DEFINES +#include +#undef NO_FCGI_DEFINES +//#include "fcgiapp.h" + +#include +#include +#include +#include + +#include "lib/fcgi_SC.h" +#include "lib/handlekeys.h" + +// Both my_config.h and fcgi_config.h define the same PACKAGE* variables, which we don't use anyway, +// I deal with that by using a sed invokation when building fcgi2. + +// https://mariadb.com/kb/en/about-mariadb-connector-c/ Official docs. +// http://dev.mysql.com/doc/refman/5.5/en/c-api-function-overview.html MySQL docs. +// http://zetcode.com/db/mysqlc/ MySQL tutorial. +#include +#include + +#include +#include + +// TODO - I should probably replace openSSL with something else. Only using it for the hash functions, and apparently it's got a bit of a bad rep. +// qLibc optionally uses openSSL for it's HTTP client stuff. +#include +#include +#include "openssl/hmac.h" +#include + +// Toybox's strend overrides another strend that causes MariaDB library to crash. Renaming it to tb_strend helps. +// I deal with that by using a sed invokation when building toybox. +#include "toys.h" + + +GLOBALS( + char *mode; +) + +#define TT this.sledjchisl + +#define FLAG_m 2 + + + +// Duplicate some small amount of code from qLibc, coz /, + and, = are not good choices, and the standard says we can pick those. +/** + * Encode data using BASE64 algorithm. + * + * @param bin a pointer of input data. + * @param size the length of input data. + * + * @return a malloced string pointer of BASE64 encoded string in case of + * successful, otherwise returns NULL + * + * @code + * const char *text = "hello world"; + * + * char *encstr = qB64_encode(text, strlen(text)); + * if(encstr == NULL) return -1; + * + * printf("Original: %s\n", text); + * printf("Encoded : %s\n", encstr); + * + * size_t decsize = qB64_decode(encstr); + * + * printf("Decoded : %s (%zu bytes)\n", encstr, decsize); + * free(encstr); + * + * --[output]-- + * Original: hello world + * Encoded: aGVsbG8gd29ybGQ= + * Decoded: hello world (11 bytes) + * @endcode + */ +char *qB64_encode(const void *bin, size_t size) { + const char B64CHARTBL[64] = { + 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P', // 00-0F + 'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f', // 10-1F + 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', // 20-2F + 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/' // 30-3F + }; + + if (size == 0) { + return strdup(""); + } + + // malloc for encoded string + char *pszB64 = (char *) malloc( + 4 * ((size / 3) + ((size % 3 == 0) ? 0 : 1)) + 1); + if (pszB64 == NULL) { + return NULL; + } + + char *pszB64Pt = pszB64; + unsigned char *pBinPt, *pBinEnd = (unsigned char *) (bin + size - 1); + unsigned char szIn[3] = { 0, 0, 0 }; + int nOffset; + for (pBinPt = (unsigned char *) bin, nOffset = 0; pBinPt <= pBinEnd; + pBinPt++, nOffset++) { + int nIdxOfThree = nOffset % 3; + szIn[nIdxOfThree] = *pBinPt; + if (nIdxOfThree < 2 && pBinPt < pBinEnd) + continue; + + *pszB64Pt++ = B64CHARTBL[((szIn[0] & 0xFC) >> 2)]; + *pszB64Pt++ = B64CHARTBL[(((szIn[0] & 0x03) << 4) + | ((szIn[1] & 0xF0) >> 4))]; + *pszB64Pt++ = + (nIdxOfThree >= 1) ? + B64CHARTBL[(((szIn[1] & 0x0F) << 2) + | ((szIn[2] & 0xC0) >> 6))] : + '='; + *pszB64Pt++ = (nIdxOfThree >= 2) ? B64CHARTBL[(szIn[2] & 0x3F)] : '='; + + memset((void *) szIn, 0, sizeof(szIn)); + } + *pszB64Pt = '\0'; + + pszB64 = qstrreplace("tr", pszB64, "+", "~"); + pszB64 = qstrreplace("tr", pszB64, "/", "_"); + pszB64 = qstrreplace("tr", pszB64, "=", "^"); + + return pszB64; +} + +/** + * Decode BASE64 encoded string. + * + * @param str a pointer of Base64 encoded string. + * + * @return the length of bytes stored in the str memory in case of successful, + * otherwise returns NULL + * + * @note + * This modify str directly. And the 'str' is always terminated by NULL + * character. + */ +size_t qB64_decode(char *str) { + const char B64MAPTBL[16 * 16] = { + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // 00-0F + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // 10-1F + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 62, 64, 64, 64, 63, // 20-2F + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 64, 64, 64, 64, 64, 64, // 30-3F + 64, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 40-4F + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 64, 64, 64, 64, 64, // 50-5F + 64, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, // 60-6F + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 64, 64, 64, 64, 64, // 70-7F + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // 80-8F + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // 90-9F + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // A0-AF + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // B0-BF + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // C0-CF + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // D0-DF + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // E0-EF + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64 // F0-FF + }; + + str = qstrreplace("tr", str, "~", "+"); + str = qstrreplace("tr", str, "_", "/"); + str = qstrreplace("tr", str, "^", "="); + + char *pEncPt, *pBinPt = str; + int nIdxOfFour = 0; + char cLastByte = 0; + for (pEncPt = str; *pEncPt != '\0'; pEncPt++) { + char cByte = B64MAPTBL[(unsigned char) (*pEncPt)]; + if (cByte == 64) + continue; + + if (nIdxOfFour == 0) { + nIdxOfFour++; + } else if (nIdxOfFour == 1) { + // 00876543 0021???? + //*pBinPt++ = ( ((cLastByte << 2) & 0xFC) | ((cByte >> 4) & 0x03) ); + *pBinPt++ = ((cLastByte << 2) | (cByte >> 4)); + nIdxOfFour++; + } else if (nIdxOfFour == 2) { + // 00??8765 004321?? + //*pBinPt++ = ( ((cLastByte << 4) & 0xF0) | ((cByte >> 2) & 0x0F) ); + *pBinPt++ = ((cLastByte << 4) | (cByte >> 2)); + nIdxOfFour++; + } else { + // 00????87 00654321 + //*pBinPt++ = ( ((cLastByte << 6) & 0xC0) | (cByte & 0x3F) ); + *pBinPt++ = ((cLastByte << 6) | cByte); + nIdxOfFour = 0; + } + + cLastByte = cByte; + } + *pBinPt = '\0'; + + return (pBinPt - str); +} + + +// Duplicate some small amount of code from toys/pending/sh.c +// TODO - to be really useful I need to return the output. +int runToy(char *argv[]) +{ + int ret = 0; + struct toy_list *tl; + struct toy_context temp; + sigjmp_buf rebound; + + if ((tl = toy_find(argv[0])) )//&& (tl->flags & (TOYFLAG_NOFORK|TOYFLAG_MAYFORK))) + { + // This fakes lots of what toybox_main() does. + memcpy(&temp, &toys, sizeof(struct toy_context)); + memset(&toys, 0, sizeof(struct toy_context)); + + if (!sigsetjmp(rebound, 1)) + { + toys.rebound = &rebound; + toy_init(tl, argv); // argv must be null terminated + tl->toy_main(); + xflush(0); + } + ret = toys.exitval; + if (toys.optargs != toys.argv+1) free(toys.optargs); + if (toys.old_umask) umask(toys.old_umask); + memcpy(&toys, &temp, sizeof(struct toy_context)); + } + + return ret; +} + + +#undef FALSE +#undef TRUE +#ifndef FALSE +// NEVER change this, true and false work to. +typedef enum +{ + FALSE = 0, + TRUE = 1 +} boolean; +#endif + + + +// Silly "man getrandom" is bullshitting. +// Note - this is Linux specific, it's calling a Linux kernel function. +// Remove this when we have a real getrandom(), and replace it with - +// #include +#include +#include +int getrandom(void *b, size_t l, unsigned int f) +{ + return (int) syscall(SYS_getrandom, b, l, f); +} + + + +typedef struct _gridStats gridStats; +struct _gridStats +{ + float next; + struct timeval last; + qhashtbl_t *stats; +}; + + +typedef struct _HTMLfile HTMLfile; +struct _HTMLfile +{ + struct timespec last; + qlist_t *fragments; +}; +qhashtbl_t *HTMLfileCache = NULL; + + +typedef struct _reqData reqData; + +typedef void *(*pageFunction) (char *file, reqData *Rd, HTMLfile *thisFile); +typedef struct _dynPage dynPage; +struct _dynPage +{ + char *name; + pageFunction func; +}; +qhashtbl_t *dynPages; +static void newDynPage(char *name, pageFunction func) +{ + dynPage *dp = xmalloc(sizeof(dynPage)); + dp->name = name; dp->func = func; + dynPages->put(dynPages, dp->name, dp, sizeof(dynPage)); + free(dp); +} + + +#define HMACSIZE EVP_MAX_MD_SIZE * 2 +#define HMACSIZE64 88 + +// Session details about the logged in web user. A sorta state machine. Ish. +enum reqSessionStatus // Status of the session. Refresh and wipe / nuke -> delete the old session file first. +{ + SHS_UNKNOWN = 0, // Haven't looked at the session yet. -> validate it + SHS_NONE, // No session at all. -> logout + SHS_BOGUS, // Looked at the session, it's bogus. -> nuke and logout + SHS_PROBLEM, // Some other problem with the session. -> nuke and logout + SHS_VALID, // Session is valid. -> continue + + SHS_LOGIN, // User has just logged in, add UUID to session. -> wipe, add UUID + + SHS_RENEW, // Refresh the session based on timer. -> continue + SHS_REFRESH, // Refresh the session for other reason. -> continue + SHS_IDLE, // Session has been idle too long. -> relogin + SHS_ANCIENT, // Session is way too old. -> nuke and logout + + SHS_SECURITY, // High security task needs users name & password. -> + SHS_RELOGIN, // Ask user to login again. -> + + SHS_KEEP, // Keep the session. -> continue + SHS_WIPE, // Wipe the session, use users UUID. -> continue + SHS_NUKE // Wipe the session, no UUID. -> logout +}; + +typedef struct _sesh sesh; +struct _sesh +{ + char salt[256 + 1], seshID[256 + 1], + sesh[256 + 16 + 10 + 1], munchie[HMACSIZE + 16 + 10 + 1], toke_n_munchie[HMACSIZE + 1], hashish[HMACSIZE + 1], + leaf[HMACSIZE64 + 6 + 1], *UUID, *name; + struct timespec timeStamp[2]; + short level; + enum reqSessionStatus status; + boolean isLinky; +}; + +// Details about the current web request. +struct _reqData +{ + lua_State *L; + qhashtbl_t *configs, *queries, *body, *cookies, *headers, *valid, *stuff, *database, *Rcookies, *Rheaders; + char *Scheme, *Host, *Method, *Script, *Path, *RUri, *doit, *form, *output, *outQuery; + sesh shs, *lnk; + gridStats *stats; + qlist_t *errors, *messages; + qgrow_t *reply; + struct timespec then; + boolean fromDb; +}; + +static void showSesh(qgrow_t *reply, sesh *shs) +{ + if (shs->isLinky) + reply->addstrf(reply, "Linky:
\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, + "

\n" + "

\n" + "

DEBUG

\n" + "
\n" + "

DEBUG log

\n" + " \n" + "
\n" + "
\n" + "

\n" + ); +} + +static void HTMLheader(qgrow_t *reply, char *title) +{ + reply->addstrf(reply, + "\n" + " \n" + " %s\n" + " \n" + " \n" + , title); + reply->addstrf(reply, " \n"); + + if (DEBUG) + reply->addstrf(reply, " \n"); + + reply->addstrf(reply, + " \n" + " \n" + " \n" + " \n" + ); + reply->addstrf(reply, "
\n"); + if (DEBUG) + HTMLdebug(reply); +} + +// TODO - maybe escape non printables as well? +char *HTMLentities[] = +{ + "", "", "", "", "", "", "", "", "", // NUL SOH STX ETX EOT ENQ ACK BEL BS + " ", " ", + "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", // VT FF CR SO SI DLE DC1 DC2 DC3 DC4 NAK SYN ETB CAN EM SUB ESC FS GS RS US + " ", // Space + "!", """, + "#", "$", + "%", "&", + "'", + "(", ")", + "*", + "+", + ",", + "-", + ".", + "/", + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", + ":", ";", + "<", "=", ">", + "?", + "@", + "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", + "[", "\", "]", + "^", + "_", + "`", + "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", + "{", "|", "}", + "~", + "", // DEL + " " // This is actually 160, not 128, but I hack around that. +}; +static void HTMLescapeString(qgrow_t *reply, char *string) +{ + size_t l = strlen(string); + char *t = xmalloc(l * 10 + 1); + int i, j = 0; + boolean space = FALSE; + + for (i = 0; i < l; i++) + { + int s = string[i]; + + // Alternate long line of spaces with space and  . + if (' ' == s) + { + if (space) + { + s = 128; + space = FALSE; + } + else + space = TRUE; + } + else + { + space = FALSE; + if (128 == s) // The real 128 character. + { + t[j++] = ' '; + continue; + } + } + + if (128 >= s) + { + char *r = HTMLentities[s]; + size_t m = strlen(r); + int k; + + for (k = 0; k < m; k++) + t[j++] = r[k]; + } + else + t[j++] = ' '; + } + t[j] = '\0'; + reply->addstr(reply, t); + free(t); +} + +static void HTMLtable(qgrow_t *reply, MYSQL *db, MYSQL_RES *result, char *caption, char *URL, char *id) +{ + char *tbl = ""; + char *address, *addrend, *t, *t0; + int count = 0, c = -1, i; + MYSQL_ROW row; + MYSQL_FIELD *fields = mysql_fetch_fields(result); + + reply->addstrf(reply, "\n", caption); + + if (!fields) + E("Failed fetching fields: %s", mysql_error(db)); + while ((row = mysql_fetch_row(result))) + { + reply->addstr(reply, ""); + address = xmprintf(""); + addrend = ""; + + if (-1 == c) + c = mysql_num_fields(result); + + if (0 == count) + { + for (i = 0; i < c; i++) + { + char *s = fields[i].name; + + reply->addstrf(reply, "", s); + } + reply->addstr(reply, "\n"); + } + + if (NULL != URL) + { + free(address); + address = xmprintf("addstrf(reply, "", address, id, t0, t0, addrend); + else + reply->addstrf(reply, "", t0); + } + } + reply->addstr(reply, "\n"); + + free(address); + count++; + } + + reply->addstr(reply, "
%s
%s
%s&%s=%s\">%s%s%s
"); + mysql_free_result(result); +} + +static void HTMLhidden(qgrow_t *reply, char *name, char *val) +{ + if ((NULL != val) && ("" != val)) + { + reply->addstrf(reply, " addstr(reply, "\">\n"); + } +} + +static void HTMLform(qgrow_t *reply, char *action, char *token) +{ + reply->addstrf(reply, "
\n", action); + if ((NULL != token) && ('\0' != token[0])) + HTMLhidden(reply, "munchie", token); +} +static void HTMLformEnd(qgrow_t *reply) +{ + reply->addstrf(reply, "
\n"); +} + +static void HTMLcheckBox(qgrow_t *reply, char *name, char *title, boolean checked, boolean required) +{ + // HTML is an absolute fucking horror. This is so that we got an off sent to us if the checkbox is off, otherwise we get nothing. + HTMLhidden(reply, name, "off"); + reply->addstrf(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, "

\n", title, name); +} +static void HTMLselectEnd(qgrow_t *reply) +{ + reply->addstr(reply, "

\n \n"); +} +static void HTMLselectEndNo(qgrow_t *reply) +{ + reply->addstr(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, "
    %s\n", title); + memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call + list->lock(list); + while (list->getnext(list, &obj, false) == true) + { + reply->addstr(reply, "
  • "); + HTMLescapeString(reply, (char *) obj.data); + reply->addstr(reply, "
  • \n"); + } + list->unlock(list); + reply->addstr(reply, "
\n"); +} + +static int count = 0; +void HTMLfill(reqData *Rd, enum fragmentType type, char *text, int length) +{ + char *tmp; + + switch (type) + { + case FT_TEXT: + { + if (length) + Rd->reply->add(Rd->reply, (void *) text, length * sizeof(char)); + break; + } + + case FT_PARAM: + { + if (strcmp("DEBUG", text) == 0) + { + if (DEBUG) + { + Rd->reply->addstrf(Rd->reply, "

FastCGI SledjChisl

\n" + "

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, "
\n"); + reply->addstr(reply, + "
\n" + "

Experimental account manager

\n" + "

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" + "
\n"); +// reply->addstr(reply, "
\n
\n"); + reply->addstr(reply, +// "
\n" +// "
\n" + "
\n" + " \n" + "
\n" + "
\n" + "\n\n"); +} + + +fragment *newFragment(enum fragmentType type, char *text, int len) +{ + fragment *frg = xmalloc(sizeof(fragment)); + + frg->type = type; + frg->length = len; + frg->text = xmalloc(len + 1); + memcpy(frg->text, text, len); + frg->text[len] = '\0'; + return frg; +} + +qlist_t *fragize(char *mm, size_t length) +{ + qlist_t *fragments = qlist(QLIST_THREADSAFE); + fragment *frg0, *frg1; + + char *h; + int i, j = 0, k = 0, l, m; + + // Scan for server side includes style markings. + for (i = 0; i < length; i++) + { + if (i + 5 < length) + { + if (('<' == mm[i]) && ('!' == mm[i + 1]) && ('-' == mm[i + 2]) && ('-' == mm[i + 3]) && ('#' == mm[i + 4])) // '' + i += 4; + } + frg0 = newFragment(FT_TEXT, &mm[k], m - k); + fragments->addlast(fragments, frg0, sizeof(fragment)); + fragments->addlast(fragments, frg1, sizeof(fragment)); + free(frg0); + free(frg1); + k = i; + break; + } + } + } + } + } + } + } + } + } + frg0 = newFragment(FT_TEXT, &mm[k], length - k); + fragments->addlast(fragments, frg0, sizeof(*frg0)); + free(frg0); + + return fragments; +} + +void unfragize(qlist_t *fragments, reqData *Rd, boolean fre) +{ + qlist_obj_t lobj; + memset((void *) &lobj, 0, sizeof(lobj)); + fragments->lock(fragments); + while (fragments->getnext(fragments, &lobj, false) == true) + { + fragment *frg = (fragment *) lobj.data; + if (NULL == frg->text) + { + E("NULL fragment!"); + continue; + } + if (NULL != Rd) + HTMLfill(Rd, frg->type, frg->text, frg->length); + if (fre) + free(frg->text); + } + fragments->unlock(fragments); + if (fre) + fragments->free(fragments); +} + +HTMLfile *checkHTMLcache(char *file) +{ + if (NULL == HTMLfileCache) + HTMLfileCache = qhashtbl(0, 0); + + HTMLfile *ret = (HTMLfile *) HTMLfileCache->get(HTMLfileCache, file, NULL, true); + int fd = open(file, O_RDONLY); + size_t length = 0; + + if (-1 == fd) + { + HTMLfileCache->remove(HTMLfileCache, file); + free(ret); + ret = NULL; + } + else + { + struct stat sb; + if (fstat(fd, &sb) == -1) + { + HTMLfileCache->remove(HTMLfileCache, file); + free(ret); + ret = NULL; + E("Failed to stat %s", file); + } + else + { + if ((NULL != ret) && (ret->last.tv_sec < sb.st_mtim.tv_sec)) + { + HTMLfileCache->remove(HTMLfileCache, file); + free(ret); + ret = NULL; + } + + if (NULL == ret) + { + char *mm = MAP_FAILED; + + ret = xmalloc(sizeof(HTMLfile)); + length = sb.st_size; + ret->last.tv_sec = sb.st_mtim.tv_sec; + ret->last.tv_nsec = sb.st_mtim.tv_nsec; + + D("Loading web template %s, which is %d bytes long.", file, length); + mm = mmap(NULL, length, PROT_READ, MAP_SHARED | MAP_POPULATE, fd, 0); + if (mm == MAP_FAILED) + { + HTMLfileCache->remove(HTMLfileCache, file); + free(ret); + ret = NULL; + E("Failed to mmap %s", file); + } + else + { + ret->fragments = fragize(mm, length); + if (-1 == munmap(mm, length)) + FCGI_fprintf(FCGI_stderr, "Failed to munmap %s\n", file); + + HTMLfileCache->put(HTMLfileCache, file, ret, sizeof(*ret)); + } + } + close(fd); + } + } + + return ret; +} + + + + + +/* TODO - + + On new user / password reset. +. Both should have all the same security concerns as the login page, they are basically logins. +. Codes should be "very long", "(for example, 16 case-sensitive alphanumeric characters)" +. "confirm" button hit on "accountCreationPage" or "resetPasswordPage" +. generate a new token, keep it around for idleTimeOut (or at least 24 hours), call it .linky instead of .lua +. hash the linky for the file name, for the same reason we hash the hashish with pepper for the leaf-node. +. Include user level field, new users get -200. +. Store the linky itself around somewhere we can find it quickly for logged in users. +. store it in the regenerated session +. Scratch that, we should never store the raw linky, see above about hashing the linky. + +. The linky is just like the session token, create it in exactly the same way. +. Linky is base64() of the binary, so it's short enough to be a file name, and not too long for the URL. +. But we only get to send one of them as a linky URL, no backup cookies / body / headers. +. Sooo, need to separate the session stuff out of Rd->stuff. +. Use two separate qhashtbl's, Rd->session and Rd->linky. + + For new user +. create their /opt/opensim_SC/var/lib/users/UUID.lua account record, and symlink firstName_lastName.lua to it. +. They can log on, + but all they can do is edit their email to send a new validation code, and enter the validation code. + They can reset their password. +. Warn them on login and any page refresh that there is an outstanding validation awaiting them. + For reset password +. Let them do things as normal, in case this was just someone being mean to them, coz their email addy might be public. +. Including the usual logging out and in again with their old password. +. Warn them on login and any page refresh that there is an outstanding password reset awaiting them. +. email linky, which is some or all of the token result bits strung together, BASE64 encode the result. +. regenerate the usual token +. user clicks on the linky (or just enters the linky in a field) +. validate the linky token. + compare the level field to the linky type in the linky URL, new users -200 would be "../validateUser/.." and not "../resetPassword/.." +. delete the linky token +. Particularly important for the forgotten password email, since now that token is in the wild, and is used to reset a password. + Which begs the question, other than being able to recieve the email, how do we tell it's them? + Security questions suck, too easily guessed. + Ask their DoB. Still sucky, coz "hey it's my birthday today" is way too leaky. + This is what Multi Factor Autentication is good for, and that's on the TODO list. + Also, admins should have a harder time doing password resets. + Must be approved by another admin? + Must log onto the server via other means to twiddle something there? + For password reset page + Ask their DoB to help confirm it's them. + validate the DoB, delete tokens and back to the login page if they get it wrong + Should warn people on the accountCreationPage that DoB might be used this way. + ask them for the new password, twice + Create a new passwordSalt and passwordHash, store them in the auth table. +. For validate new user page +. tell them they have validated +. create their OpenSim account UserAccounts.UserTitle and auth tables, not GridUser table +. create their GridUser record. +. update their UserAccounts.Userlevel and UserAccounts.UserTitle +. send them to the login page. +. regenerate the usual token +? let user stay logged on? + Check best practices for this. + + Check password strength. + https://stackoverflow.com/questions/549/the-definitive-guide-to-form-based-website-authentication?rq=1 + Has some pointers to resources in the top answers "PART V: Checking Password Strength" section. + + "PART VI: Much More - Or: Preventing Rapid-Fire Login Attempts" and "PART VII: Distributed Brute Force Attacks" is also good for - + Login attempt throttling. + Deal with dictionary attacks by slowing down access on password failures etc. + + Deal with editing yourself. + Deal with editing others, but only as god. + + + Regularly delete old session files and ancient newbies. + + + Salts should be "lengthy" (128 bytes suggested in 2007) and random. Should be per user to. Or use a per user and a global one, and store the global one very securely. + And store hash(salt+password). + On the other hand, the OpenSim / SL password hashing function may be set in concrete in all the viewers. I'll have to find out. + So far I have discovered - + On login server side if the password doesn't start with $1$, then password = "$1$" + Util.Md5Hash(passwd); + remove the "$1$ bit + string hashed = Util.Md5Hash(password + ":" + data.Data["passwordSalt"].ToString()); + if (data.Data["passwordHash"].ToString() == hashed) + passwordHash is char(32), and as implied above, doesn't include the $1$ bit. passwordSalt is also char(32) + Cool VL and Impy at least sends the $1$ md5 hashed version over the wire. + Modern viewers obfuscate the details buried deep in C++ OOP crap. + Sent via XMLRPC + MD5 is considered broken since 2013, possibly longer. + Otherwise use a slow hashing function. bcrypt? scrypt? Argon2? PBKDF2? + https://security.stackexchange.com/questions/211/how-to-securely-hash-passwords + Should include a code in the result that tells us which algorithm was used, so we can change the algorithm at a later date. /etc/passwd does this. + Which is what the $1$ bit currently used between server and client is sorta for. + ++ Would be good to have one more level of this Rd->database stuff, so we know the type of the thing. + While qhashtbl has API for putting strings, ints, and memory, it has none for finding out what type a stored thing is. + Once I have a structure, I add things like "level needed to edit it", "OpenSim db structure to Lua file mapping", and other fanciness. + Would also help with the timestamp being stored for the session, it prints out binary gunk in the DEBUG
. + Starting to get into object oriented territory here. B-) + I'll have to do it eventually anyway. + object->tostring(object), and replace the big switch() statements in the existing db code with small functions. + That's why the qLibc stuff has that format, coz C doesn't understand the concept of passing "this" as the first argument. + https://stackoverflow.com/questions/351733/how-would-one-write-object-oriented-code-in-c + https://stackoverflow.com/questions/415452/object-orientation-in-c + http://ooc-coding.sourceforge.net/ + + +https://owasp.org/www-project-cheat-sheets/cheatsheets/Input_Validation_Cheat_Sheet.html#Email_Address_Validation +https://cheatsheetseries.owasp.org/ +https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html +https://owasp.org/www-project-cheat-sheets/cheatsheets/Authentication_Cheat_Sheet.html +https://softwareengineering.stackexchange.com/questions/46716/what-technical-details-should-a-programmer-of-a-web-application-consider-before +https://wiki.owasp.org/index.php/OWASP_Guide_Project +https://stackoverflow.com/questions/549/the-definitive-guide-to-form-based-website-authentication?rq=1 +https://owasp.org/www-community/xss-filter-evasion-cheatsheet + A list of example XSS things to try. +*/ + + + +/* Four choices for the token - (https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html) + https://en.wikipedia.org/wiki/Cross-site_request_forgery + Has some more info. + +Large random value generated by a secure method (getrandom(2)). + Keep it secret, put it in hidden fields, or custom HTTP header (requires JavaScript but more secure than hidden fields). + NOT cookies or GET. Don't log it. +Cryptographically sign a session ID and timestamp. + Timestamp is for session timeouts. + Keep it secret, put it in hidden fields, or custom HTTP header (requires JavaScript but more secure than hidden fields). + Needs a secret key server side. +A strong HMAC (SHA-256 or better) of a session ID and timestamp. + The above document seems to imply that a key is used for this, the openssl EVP functions don't mention any way of supplying this key. + https://en.wikipedia.org/wiki/HMAC says there is a key as well. + https://www.openssl.org/docs/man1.1.0/man3/HMAC.html HAH! They can have keys. OpenSSL's docs suck. + Token = HMAC(sessionID+timestamp)+timestamp (Yes, timestamp is used twice). + Keep it secret, put it in hidden fields, or custom HTTP header (requires JavaScript but more secure than hidden fields). + Needs a secret key server side. +Double cookie + Large random value generated by a secure method set as a cookie and hidden field. Check they match. + Optional - encrypt / salted hash it in another cookie / hidden field. ++ Also a resin (BASE64 session key in the query string). + Not such a good idea to have something in the query, coz that screws with bookmarks. + https://security.stackexchange.com/questions/59470/double-submit-cookies-vulnerabilities + Though so far all the pages I find saying this don't say flat out say "use headers instead", though they do say "use HSTS". + https://security.stackexchange.com/questions/220797/is-the-double-submit-cookie-pattern-still-effective ++ Includes a work around that I might already be doing. +TODO - think it through, is it really secure against session hijacking? +TODO - document why we redirect POST to GET, coz it's a pain in the arse, and we have to do things twice. + +SOOOOO - use double cookie + hidden field. + No headers, coz I need JavaScript to do that. + No hidden field when redirecting post POST to GET, coz GET doesn't get those. + pepper = long pass phrase or some such stored in .sledjChisl.conf.lua, which has to be protected dvs1/opensimsc/0640 as well as the database credentials. + salt = large random value generated by a secure method (getrandom(2)). + seshID = large random value generated by a secure method (getrandom(2)). + timeStamp = mtime of the leaf-node file, set to current time when we are creating the token. + sesh = seshID + timeStamp + munchie = HMAC(sesh) + timeStamp The token hidden field + toke_n_munchie = HMAC(UUID + munchie) The token cookie + hashish = HMACkey(toke_n_munchie, salt) Salted token cookie & linky query +? resin = BASE64(hashish) Base64 token cookie + leaf-node = HMACkey(hashish, pepper) Stored token file name + + Leaf-node.lua (mtime is timeStamp) + IP, UUID, salt, seshID, user name, passwordSalt, passwordHash (last two for OpenSim password protocol) + + The test - (validateSesh() below) + we get hashish and toke_n_munchie + HMACkey(hashish + pepper) -> leaf-node + read leaf-node.lua -> IP, UUID, salt, seshID + get it's mtime -> timeStamp + seshID + timeStamp -> sesh + HMAC(sesh) + timeStamp -> munchie + if we got munchie in the hidden field, compare it + toke_n_munchie == HMAC(UUID + munchie) + For linky it'll be - + HMAC(UUID + munchie) -> toke_n_munchie + hashish == HMACkey(toke_n_munchie + salt) ++ If it's too old according to mtime, delete it and logout. + +I should make it easy to change the HMAC() function. Less important for these short lived sessions, more important for the linky URLs, most important for stored password hashes. + Same for the pepper. + +The required JavaScript might be like https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#xmlhttprequest--native-javascript- + NOTE - they somehow fucked up that anchor tag. + +NOTE - storing a pepper on the same RAID array as everything else will be a problem when it comes time to replace one of the disks. + It might have failed badly enough that you can't wipe it, but an attacker can dumpster dive it, replace the broken bit (firmware board), and might get lucky. + Also is a problem with SSD and rust storing good data on bad sectors in the spare sector pool, wear levelling, etc. + +https://stackoverflow.com/questions/16891729/best-practices-salting-peppering-passwords +*/ + + +qlisttbl_t *accountLevels = NULL; + + +static void bitch(reqData *Rd, char *message, char *log) +{ + addStrL(Rd->errors, message); + E("%s %s %s - %s %s", getStrH(Rd->headers, "REMOTE_ADDR"), Rd->shs.UUID, Rd->shs.name, message, log); +} + +/* "A token cookie that references a non-existent session, its value should be replaced immediately to prevent session fixation." +https://owasp.org/www-community/attacks/Session_fixation + Which describes the problem, but offers no solution. + See https://stackoverflow.com/questions/549/the-definitive-guide-to-form-based-website-authentication?rq=1. +I think this means send a new cookie. + I clear out the cookies and send blank ones with -1 maxAge, so they should get deleted. +*/ +static void bitchSession(reqData *Rd, char *message, char *log) +{ + if ('\0' != message[0]) + addStrL(Rd->errors, message); + C("%s %s %s - %s %s", getStrH(Rd->headers, "REMOTE_ADDR"), Rd->shs.UUID, Rd->shs.name, message, log); + Rd->shs.status = SHS_BOGUS; +} + + +// The ancient, insecure since 2011, Second Life / OpenSim password hashing algorithm. +char *newSLOSsalt(reqData *Rd) +{ + char *salt = NULL; + unsigned char *md5hash = xzalloc(17); + char uuid[37]; + uuid_t binuuid; + + uuid_generate_random(binuuid); + uuid_unparse_lower(binuuid, uuid); + if (!qhashmd5((void *) uuid, strlen(uuid), md5hash)) + bitch(Rd, "Internal error.", "newSLOSsalt() - qhashmd5(new uuid) failed."); + else + salt = qhex_encode(md5hash, 16); + free(md5hash); + return salt; +} + +char *checkSLOSpassword(reqData *Rd, char *salt, char *password, char *passwordHash, char *fail) +{ + char *ret = NULL; + int rt = 0; + unsigned char *md5hash = xzalloc(17); + char *hash = NULL, *passHash = NULL; + +T("checkSLOSpassword(%s, %s, %s, ", password, salt, passwordHash, fail); + // Calculate passHash. + if (!qhashmd5((void *) password, strlen(password), md5hash)) + { + bitch(Rd, "Internal error.", "checkSLOSpassword() - qhashmd5(password) failed."); + rt++; + } + else + { + passHash = qhex_encode(md5hash, 16); + hash = xmprintf("%s:%s", passHash, salt); + if (!qhashmd5((void *) hash, strlen(hash), md5hash)) + { + bitch(Rd, "Internal error.", "checkSLOSpassword() - qhashmd5(password:salt) failed."); + rt++; + } + else + { + ret = qhex_encode(md5hash, 16); + } + free(hash); + free(passHash); + } + + // If one was passed in, compare it. + if ((NULL != ret) && (NULL != passwordHash) && (strcmp(ret, passwordHash) != 0)) + { + bitch(Rd, fail, "Password doesn't match passwordHash"); + E(" %s %s - %s != %s", password, salt, ret, passwordHash); + rt++; + free(ret); + ret = NULL; + } + free(md5hash); + + return ret; +} + + +int LuaToHash(reqData *Rd, char *file, char *var, qhashtbl_t *tnm, int ret, struct stat *st, struct timespec *now, char *type) +{ + struct timespec then; + + if (-1 == clock_gettime(CLOCK_REALTIME, &then)) + perror_msg("Unable to get the time."); + I("Reading %s file %s", type, file); + if (0 != stat(file, st)) + { + D("No %s file.", file); + perror_msg("Unable to stat %s", file); + ret++; + } + else + { + int status = luaL_loadfile(Rd->L, file), result; + + if (status) + { + bitch(Rd, "No such thing.", "Can't load file."); + E("Couldn't load file: %s", lua_tostring(Rd->L, -1)); + ret++; + } + else + { + result = lua_pcall(Rd->L, 0, LUA_MULTRET, 0); + + if (result) + { + bitch(Rd, "Broken thing.", "Can't run file."); + E("Failed to run script: %s", lua_tostring(Rd->L, -1)); + ret++; + } + else + { + lua_getglobal(Rd->L, var); + lua_pushnil(Rd->L); + + while(lua_next(Rd->L, -2) != 0) + { + char *n = (char *) lua_tostring(Rd->L, -2); + + if (lua_isstring(Rd->L, -1)) + { + tnm->putstr(tnm, n, (char *) lua_tostring(Rd->L, -1)); +d("Lua reading (%s) %s = %s", type, n, getStrH(tnm, n)); + } + else + { + char *v = (char *) lua_tostring(Rd->L, -1); + W("Unknown Lua variable type for %s = %s", n, v); + } + lua_pop(Rd->L, 1); + } + + if (-1 == clock_gettime(CLOCK_REALTIME, now)) + perror_msg("Unable to get the time."); + double n = (now->tv_sec * 1000000000.0) + now->tv_nsec; + double t = (then.tv_sec * 1000000000.0) + then.tv_nsec; + T("Reading %s file took %lf seconds", type, (n - t) / 1000000000.0); + } + } + } + + return ret; +} + + +char *checkLinky(reqData *Rd) +{ +// TODO - should be from Rd.shs->linky-hashish + char *ret = xstrdup(""), *t0 = getStrH(Rd->stuff, "linky-hashish"); + + if ('\0' != t0[0]) + { + char *t1 = qurl_encode(t0, strlen(t0)); + free(ret); + ret = xmprintf("

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, "

Terms of Service

%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, "

account manager

\n"); + if (NULL != Rd->shs.name) + { + char *nm = qstrreplace("tr", xstrdup(Rd->shs.name), " ", "+"); + + Rd->reply->addstrf(Rd->reply, "

You are %s

\n", Rd->Host, Rd->Script, Rd->Path, nm, Rd->shs.name); + Rd->reply->addstr(Rd->reply, linky); + free(nm); + } + free(linky); + if (0 != Rd->errors->size(Rd->messages)) + HTMLlist(Rd->reply, "messages -", Rd->messages); + if (NULL != oF->help) + 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, "

"); + Rd->reply->addstrf(Rd->reply, "
disabled
Account cannot log in anywhere.
"); + Rd->reply->addstrf(Rd->reply, "
newbie
Newly created account, not yet validated.
"); + Rd->reply->addstrf(Rd->reply, "
validated
Newly created account, they have clicked on the validation link in their validation email.
"); + Rd->reply->addstrf(Rd->reply, "
vouched for
Someone has vouched for this person.
"); + Rd->reply->addstrf(Rd->reply, "
approved
This person is approved, and can log into the world.
"); + Rd->reply->addstrf(Rd->reply, "
god
This is a god admin person.
"); + Rd->reply->addstrf(Rd->reply, "

"); + } + else + 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, "\n"); + Rd->reply->addstr(Rd->reply, ""); + Rd->reply->addstr(Rd->reply, ""); + Rd->reply->addstr(Rd->reply, ""); + Rd->reply->addstr(Rd->reply, ""); + Rd->reply->addstr(Rd->reply, ""); + Rd->reply->addstr(Rd->reply, "\n"); + + qlisttbl_obj_t obj; + memset((void *) &obj, 0, sizeof(obj)); + list->lock(list); + while(list->getnext(list, &obj, NULL, false) == true) + { + qhashtbl_t *tnm = *((qhashtbl_t **) obj.data); + char *nm = qstrreplace("tr", xstrdup(obj.name), " ", "+"); + + Rd->reply->addstrf(Rd->reply, "", Rd->Host, Rd->Script, Rd->Path, nm, obj.name); + Rd->reply->addstrf(Rd->reply, "", getStrH(tnm, "voucher"), getStrH(tnm, "level"), getStrH(tnm, "title")); + free(nm); + tnm->clear(tnm); + list->removeobj(list, &obj); + tnm->free(tnm); + } + list->unlock(list); + Rd->reply->addstr(Rd->reply, "
Validated users
namevoucherleveltitle
%s%s%s%s
"); + list->free(list); + + accountWebSubs(Rd, oF); + accountWebFooter(Rd, oF); +} +static int accountExploreValidatedVoucherSub(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0; + return ret; +} + + +qhashtbl_t *accountPages = NULL; +inputForm *newInputForm(char *name, char *title, char *help, inputFormShowFunc web, inputFormShowFunc eWeb) +{ + inputForm *ret = xmalloc(sizeof(inputForm)); + +d("newInputForm(%s)", name); + ret->name = name; ret->title = title; ret->help = help; + ret->web = web; ret->eWeb = eWeb; + ret->fields = qlisttbl(QLISTTBL_THREADSAFE | QLISTTBL_UNIQUE | QLISTTBL_LOOKUPFORWARD); + ret->subs = qhashtbl(0, 0); + accountPages->put(accountPages, ret->name, ret, sizeof(inputForm)); + free(ret); + return accountPages->get(accountPages, name, NULL, false); +} + +inputField *addInputField(inputForm *iF, signed char type, char *name, char *title, char *help, inputFieldValidFunc validate, inputFieldShowFunc web) +{ + inputField *ret = xzalloc(sizeof(inputField)); + +//d("addInputField(%s, %s)", iF->name, name); + ret->name = name; ret->title = title; ret->help = help; + ret->validate = validate; ret->web = web; ret->type = type; + ret->flags = FLD_EDITABLE; + iF->fields->put(iF->fields, ret->name, ret, sizeof(inputField)); + free(ret); + return iF->fields->get(iF->fields, name, NULL, false); +} + +void inputFieldExtra(inputField *ret, signed char flags, short viewLength, short maxLength) +{ + ret->flags = flags; + ret->viewLength = viewLength; ret->maxLength = maxLength; +} + +void addSession(inputForm *iF) +{ + inputField *fld, **flds = xzalloc(3 * sizeof(*flds)); + +//d("addSession(%s)", iF->name); + flds[0] = addInputField(iF, LUA_TSTRING, "hashish", "hashish", "", sessionValidate, sessionWeb); + inputFieldExtra(flds[0], FLD_HIDDEN, 0, 0); + flds[1] = addInputField(iF, LUA_TSTRING, "toke_n_munchie", "toke_n_munchie", "", sessionValidate, sessionWeb); + inputFieldExtra(flds[1], FLD_HIDDEN, 0, 0); + fld = addInputField(iF, LUA_TGROUP, "sessionGroup", "sessionGroup", "", sessionValidate, sessionWeb); + inputFieldExtra(fld, FLD_HIDDEN, 0, 0); + fld->group = flds; + flds[0]->group = flds; + flds[1]->group = flds; +} + +void addEmailFields(inputForm *iF) +{ + inputField *fld, **flds = xzalloc(3 * sizeof(*flds)); + + flds[0] = addInputField(iF, LUA_TEMAIL, "email", "email", NULL, emailValidate, emailWeb); + inputFieldExtra(flds[0], FLD_EDITABLE, 42, 254); + flds[1] = addInputField(iF, LUA_TEMAIL, "emayl", "Re-enter your email, to be sure you got it correct", + "A validation email will be sent to this email address, you will need to click on the link in it to continue your account creation.", emailValidate, emailWeb); + inputFieldExtra(flds[1], FLD_EDITABLE, 42, 254); + fld = addInputField(iF, LUA_TGROUP, "emailGroup", "emailGroup", "", emailValidate, NULL); + inputFieldExtra(fld, FLD_HIDDEN, 0, 0); + fld->group = flds; + flds[0]->group = flds; + flds[1]->group = flds; +} + +void addDoBFields(inputForm *iF) +{ + inputField *fld, **flds = xzalloc(3 * sizeof(*flds)); + + flds[0] = addInputField(iF, LUA_TSTRING, "DoByear", "year", NULL, DoBValidate, DoByWeb); + flds[1] = addInputField(iF, LUA_TSTRING, "DoBmonth", "month", NULL, DoBValidate, DoBmWeb); + fld = addInputField(iF, LUA_TGROUP, "DoBGroup", "DoBGroup", "", DoBValidate, DoBWeb); + inputFieldExtra(fld, FLD_HIDDEN, 0, 0); + fld->group = flds; + flds[0]->group = flds; + flds[1]->group = flds; +} + +void addLegalFields(inputForm *iF) +{ + inputField *fld, **flds = xzalloc(3 * sizeof(*flds)); + + flds[0] = addInputField(iF, LUA_TBOOLEAN, "adult", "I'm allegedly an adult in my country.", NULL, legalValidate, adultWeb); + flds[1] = addInputField(iF, LUA_TBOOLEAN, "agree", "I accept the Terms of Service.", NULL, legalValidate, agreeWeb); + fld = addInputField(iF, LUA_TGROUP, "legalGroup", "legalGroup", "", legalValidate, legalWeb); + inputFieldExtra(fld, FLD_HIDDEN, 0, 0); + fld->group = flds; + flds[0]->group = flds; + flds[1]->group = flds; +} + +inputSub *addSubmit(inputForm *iF, char *name, char *title, char *help, inputSubmitFunc submit, char *output) +{ + inputSub *ret = xmalloc(sizeof(inputSub)); + +//d("addSubmit(%s, %s)", iF->name, name); + ret->name = name; ret->title = title; ret->help = help; ret->submit = submit; ret->outputForm = output; + iF->subs->put(iF->subs, ret->name, ret, sizeof(inputSub)); + free(ret); + return iF->subs->get(iF->subs, name, NULL, false); +} + + +/* There should be some precedence for values overriding values here. + https://www.w3.org/standards/webarch/protocols + "This intro text is boilerplate for the beta release of w3.org." Fucking useless. Pffft + https://www.w3.org/Protocols/ + Still nothing official, though the ENV / HEADER stuff tends to be about the protocol things, and cookies / body / queries are about the data things. + http://docs.gantry.org/gantry4/advanced/setby + Says that query overrides cookies, but that might be just for their platform. + https://framework.zend.com/manual/1.11/en/zend.controller.request.html + Says - "1. GET, 2. POST, 3. COOKIE, 4. SERVER, 5. ENV." + We don't actually get the headers directly, it's all sent via the env. + +URL query Values actually provided by the user in the FORM, and other things. +POST body Values actually provided by the user in the FORM. +cookies + https://stackoverflow.com/questions/4056306/how-to-handle-multiple-cookies-with-the-same-name + +headers includes HTTP_COOKIE and QUERY_STRING +env includes headers and HTTP_COOKIE and QUERY_STRING + +database Since all of the above are for updating the database anyway, this goes on the bottom, overridden by all. + Though be wary of security stuff. + + Sending cookie headers is a special case, multiples can be sent, otherwise headers are singletons, only send one for each name. +*/ +char *sourceTypes[] = +{ + "cookies", + "body", + "queries", + "stuff" +}; + +static int collectFields(reqData *Rd, inputForm *iF, inputValue *iV, int t) +{ + int i = 0, j; + qlisttbl_obj_t obj; + + memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call + iF->fields->lock(iF->fields); + while(iF->fields->getnext(iF->fields, &obj, NULL, false) == true) + { + inputField *fld = (inputField *) obj.data; + +//if (0 > t) +// d("Collecting %d %s - %s", t, iF->name, fld->name); +//else +// d("Collecting %s %s - %s", sourceTypes[t], iF->name, fld->name); + iV[i].field = fld; + if (LUA_TGROUP == fld->type) + { + if (0 >= t) + { + j = 0; + // If it's a group, number the members relative to the group field. + // Assume the members for this group are the previous ones. + while (iV[i].field->group[j]) + { + j++; + iV[i - j].index = j; + } + } + } + else + { + char *vl = NULL; + + switch (t) + { + // We don't get the cookies metadata. + case 0 : vl = Rd->cookies->getstr(Rd->cookies, obj.name, false); break; + case 1 : vl = Rd->body-> getstr(Rd->body, obj.name, false); break; + case 2 : vl = Rd->queries->getstr(Rd->queries, obj.name, false); break; + case 3 : vl = Rd->queries->getstr(Rd->stuff, obj.name, false); break; + default: break; + } + if ((NULL != iV[i].value) && (NULL != vl)) + { + if (strcmp(vl, iV[i].value) != 0) + W("Collected %s value for %s - %s from %s overriding value from %s", sourceTypes[t], iF->name, fld->name, sourceTypes[t], sourceTypes[iV[i].source]); + else + W("Collected %s value for %s - %s from %s same as value from %s", sourceTypes[t], iF->name, fld->name, sourceTypes[t], sourceTypes[iV[i].source]); + } + if (NULL != vl) + { + iV[i].source = t; + iV[i].value = vl; + D("Collected %s value for %s - %s = %s", sourceTypes[t], iF->name, fld->name, vl); + } + } + i++; + } + iF->fields->unlock(iF->fields); + return i; +} + + +void sessionStateEngine(reqData *Rd, char *type) +{ + switch (Rd->shs.status) + { + case SHS_UNKNOWN: d("sessionStateEngine(SHS_UNKNOWN, %s)", type); break; + case SHS_NONE: d("sessionStateEngine(SHS_NONE, %s)", type); break; + case SHS_BOGUS: d("sessionStateEngine(SHS_BOGUS, %s)", type); break; + case SHS_PROBLEM: d("sessionStateEngine(SHS_PROBLEM, %s)", type); break; + case SHS_VALID: d("sessionStateEngine(SHS_VALID, %s)", type); break; + + case SHS_LOGIN: d("sessionStateEngine(SHS_LOGIN, %s)", type); break; + + case SHS_RENEW: d("sessionStateEngine(SHS_RENEW, %s)", type); break; + case SHS_REFRESH: d("sessionStateEngine(SHS_REFRESH, %s)", type); break; + case SHS_IDLE: d("sessionStateEngine(SHS_IDLE, %s)", type); break; + case SHS_ANCIENT: d("sessionStateEngine(SHS_ANCIENT, %s)", type); break; + + case SHS_SECURITY: d("sessionStateEngine(SHS_SECURITY, %s)", type); break; + case SHS_RELOGIN: d("sessionStateEngine(SHS_RELOGIN, %s)", type); break; + + case SHS_KEEP: d("sessionStateEngine(SHS_KEEP, %s)", type); break; + case SHS_WIPE: d("sessionStateEngine(SHS_WIPE, %s)", type); break; + case SHS_NUKE: d("sessionStateEngine(SHS_NUKE, %s)", type); break; + } +} + + +void account_html(char *file, reqData *Rd, HTMLfile *thisFile) +{ + inputForm *iF; + inputField *fld; + inputSub *sub; + boolean isGET = FALSE; + int e = 0, t = 0, i, j; + char *doit = getStrH(Rd->body, "doit"), *form = getStrH(Rd->body, "form"); + + if (NULL == accountLevels) + { + accountLevels = qlisttbl(QLISTTBL_LOOKUPFORWARD | QLISTTBL_THREADSAFE | QLISTTBL_UNIQUE); + accountLevels->putstr(accountLevels, "-256", "disabled"); + accountLevels->putstr(accountLevels, "-200", "newbie"); + accountLevels->putstr(accountLevels, "-100", "validated"); + accountLevels->putstr(accountLevels, "-50", "vouched for"); + accountLevels->putstr(accountLevels, "0", "approved"); // Note that http://opensimulator.org/wiki/Userlevel claims that 1 and above are "GOD_LIKE". + accountLevels->putstr(accountLevels, "200", "god"); + } + + // Check "Origin" header and /or HTTP_REFERER header. + // "Origin" is either HTTP_HOST or X-FORWARDED-HOST. Which could be "null". + char *ref = xmprintf("https://%s%s/account.html", getStrH(Rd->headers, "SERVER_NAME"), getStrH(Rd->headers, "SCRIPT_NAME")); + char *href = Rd->headers->getstr(Rd->headers, "HTTP_REFERER", true); + + if (NULL != href) + { + char *f = strchr(href, '?'); + + if (NULL != f) + *f = '\0'; + if (('\0' != href[0]) && (strcmp(ref, href) != 0)) + { + bitch(Rd, "Invalid referer.", ref); + D("Invalid referer - %s isn't %s", ref, href); + form = "accountLogin"; + Rd->shs.status = SHS_PROBLEM; + } + free(href); + } + free(ref); + ref = getStrH(Rd->headers, "SERVER_NAME"); + href = getStrH(Rd->headers, "HTTP_HOST"); + if ('\0' == href[0]) + href = getStrH(Rd->headers, "X-FORWARDED-HOST"); + if (('\0' != href[0]) && (strcmp(ref, href) != 0)) + { + bitch(Rd, "Invalid HOST.", ref); + D("Invalid HOST - %s isn't %s", ref, href); + form = "accountLogin"; + Rd->shs.status = SHS_PROBLEM; + } + + // Redirect to HTTPS if it's HTTP. + if (strcmp("https", Rd->Scheme) != 0) + { + Rd->Rheaders->putstr (Rd->Rheaders, "Status", "301 Moved Permanently"); + Rd->Rheaders->putstrf(Rd->Rheaders, "Location", "https://%s%s", Rd->Host, Rd->RUri); + Rd->reply->addstrf(Rd->reply, "404 Unknown page" + "" + "You should get redirected to https://%s%s", + Rd->Host, Rd->RUri, Rd->Host, Rd->RUri, Rd->Host, Rd->RUri + ); + D("Redirecting dynamic page %s -> https://%s%s", file, Rd->Host, Rd->RUri); + return; + } + + // Create the dynamic web pages for account.html. + if (NULL == accountPages) + { + accountPages = qhashtbl(0, 0); + + + iF = newInputForm("accountAdd", "", NULL, accountAddWeb, accountLoginWeb); + addSession(iF); +// fld = addInputField(iF, LUA_TSTRING, "UUID", "UUID", NULL, UUIDValidate, UUIDWeb); +// inputFieldExtra(fld, FLD_HIDDEN, 0, 0); + fld = addInputField(iF, LUA_TSTRING, "name", "name", NULL, nameValidate, nameWeb); + inputFieldExtra(fld, FLD_EDITABLE | FLD_REQUIRED, 42, 63); + fld = addInputField(iF, LUA_TPASSWORD, "psswrd", "Re-enter your password", + "Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters.", passwordValidate, passwordWeb); + inputFieldExtra(fld, FLD_EDITABLE, 16, 0); + addEmailFields(iF); + addDoBFields(iF); + addLegalFields(iF); + fld = addInputField(iF, LUA_TSTRING, "ToS", "Terms of Service", "", NULL, ToSWeb); + fld = addInputField(iF, LUA_TSTRING, "voucher", "The grid name of someone that will vouch for you", + "We use a vouching system here, an existing member must know you well enough to tell us you'll be good for our grid.", voucherValidate, voucherWeb); + inputFieldExtra(fld, FLD_EDITABLE, 42, 63); + fld = addInputField(iF, LUA_TSTRING, "aboutMe", "About me", NULL, aboutMeValidate, aboutMeWeb); + inputFieldExtra(fld, FLD_EDITABLE, 50, 16384); + addSubmit(iF, "confirm", "confirm", NULL, accountAddSub, "accountView"); + addSubmit(iF, "cancel", "cancel", NULL, accountOutSub, "accountLogin"); + + + iF = newInputForm("accountView", "account view", NULL, accountViewWeb, accountLoginWeb); + addSession(iF); +// fld = addInputField(iF, LUA_TSTRING, "UUID", "UUID", NULL, UUIDValidate, UUIDWeb); +// inputFieldExtra(fld, FLD_HIDDEN, 0, 0); + fld = addInputField(iF, LUA_TSTRING, "name", "name", NULL, nameValidate, nameWeb); + inputFieldExtra(fld, FLD_HIDDEN, 42, 63); + fld = addInputField(iF, LUA_TSTRING, "user", "user", NULL, nameValidate, nameWeb); + inputFieldExtra(fld, FLD_HIDDEN, 42, 63); + addSubmit(iF, "login", "", NULL, accountViewSub, "accountView"); // Coz we sometimes want to trigger this from code. + addSubmit(iF, "validate", "", NULL, accountValidateSub, "accountLogin"); // Coz we sometimes want to trigger this from code. + addSubmit(iF, "edit", "", NULL, accountEditSub, "accountEdit"); // Coz we sometimes want to trigger this from code. + addSubmit(iF, "validated_members", "validated members", NULL, accountExploreValidatedVoucherSub, "accountValidated"); + addSubmit(iF, "logout", "logout", NULL, accountOutSub, "accountLogin"); + + + iF = newInputForm("accountValidated", "account validated list", NULL, accountExploreValidatedVouchersWeb, accountLoginWeb); + addSession(iF); + fld = addInputField(iF, LUA_TSTRING, "name", "name", NULL, nameValidate, nameWeb); + inputFieldExtra(fld, FLD_HIDDEN, 42, 63); + addSubmit(iF, "login", "", NULL, accountViewSub, "accountView"); // Coz we sometimes want to trigger this from code. + addSubmit(iF, "back", "back", NULL, accountViewSub, "accountView"); + + + iF = newInputForm("accountEdit", "account edit", NULL, accountEditWeb, accountLoginWeb); + addSession(iF); +// fld = addInputField(iF, LUA_TSTRING, "UUID", "UUID", NULL, UUIDValidate, UUIDWeb); +// inputFieldExtra(fld, FLD_HIDDEN, 0, 0); +// fld = addInputField(iF, LUA_TSTRING, "name", "name", NULL, nameValidate, nameWeb); +// inputFieldExtra(fld, FLD_HIDDEN, 42, 63); +// fld = addInputField(iF, LUA_TSTRING, "user", "user", NULL, nameValidate, nameWeb); +// inputFieldExtra(fld, FLD_HIDDEN, 42, 63); +// fld = addInputField(iF, LUA_TEMAIL, "email", "email", "", emailValidate, emailWeb); +// inputFieldExtra(fld, FLD_NONE, 42, 254); + addSubmit(iF, "login", "", NULL, accountViewSub, "accountView"); // Coz we sometimes want to trigger this from code. + addSubmit(iF, "save", "save", NULL, accountSaveSub, "accountView"); + addSubmit(iF, "back", "back", NULL, accountViewSub, "accountView"); +// addSubmit(iF, "members", "members", NULL, accountExploreSub, "accountExplore"); + addSubmit(iF, "logout", "logout", NULL, accountOutSub, "accountLogin"); +// addSubmit(iF, "delete", "delete", NULL, accountDelSub, "accountDel"); + + + iF = newInputForm("accountLogin", "account login", "Please login, or create your new account.", accountLoginWeb, accountLoginWeb); + addSession(iF); + fld = addInputField(iF, LUA_TSTRING, "name", "name", "Your name needs to be two words, only ordinary letters and digits, no special characters or fonts.", nameValidate, nameWeb); + inputFieldExtra(fld, FLD_EDITABLE | FLD_REQUIRED, 42, 63); + fld = addInputField(iF, LUA_TPASSWORD, "password", "password", + "Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters.", passwordValidate, passwordWeb); + inputFieldExtra(fld, FLD_EDITABLE | FLD_REQUIRED, 16, 0); + addSubmit(iF, "logout", "", NULL, accountOutSub, "accountLogin"); // Coz we sometimes want to trigger this from code. + addSubmit(iF, "validate", "", NULL, accountValidateSub, "accountLogin"); // Coz we sometimes want to trigger this from code. + addSubmit(iF, "login", "login", NULL, accountViewSub, "accountView"); + addSubmit(iF, "create", "create account", NULL, accountCreateSub, "accountAdd"); + } + + // Figure out what we are doing. + if ('\0' == form[0]) + form = getStrH(Rd->cookies, "form"); + if ('\0' == form[0]) + { + form = "accountLogin"; + Rd->shs.status = SHS_NUKE; + } + if ('\0' == doit[0]) + doit = getStrH(Rd->cookies, "doit"); + if ('\0' == doit[0]) + { + doit = "logout"; + Rd->shs.status = SHS_NUKE; + } + if ('\0' != doit[0]) + { + setCookie(Rd, "doit", doit); + Rd->doit = doit; + } + + iF = accountPages->get(accountPages, form, NULL, false); + if (NULL == iF) + { + E("No such account page - %s", form); + form = "accountLogin"; + doit = "logout"; + Rd->shs.status = SHS_PROBLEM; + iF = accountPages->get(accountPages, form, NULL, false); + } + sub = iF->subs->get(iF->subs, doit, NULL, false); + if (NULL == sub) + { + E("No such account action - %s", doit); + form = "accountLogin"; + doit = "logout"; + Rd->shs.status = SHS_PROBLEM; + iF = accountPages->get(accountPages, form, NULL, false); + sub = iF->subs->get(iF->subs, doit, NULL, false); + } + + // Special for showing a users details. + if ('\0' != getStrH(Rd->queries, "user")[0]) + { + doit = "edit"; + form = "accountView"; + iF = accountPages->get(accountPages, form, NULL, false); + sub = iF->subs->get(iF->subs, doit, NULL, false); + } + + Rd->doit = doit; + Rd->form = form; + Rd->output = sub->outputForm; + + C("Starting dynamic page %s %s -> %s [%s -> %s]", Rd->RUri, form, doit, sub->name, Rd->output); + + // Collect the input data. + int count = iF->fields->size(iF->fields); + inputValue *iV = xzalloc(count * sizeof(inputValue)); + qlisttbl_obj_t obj; + + sessionStateEngine(Rd, "collected"); + + if (strcmp("cancel", sub->name) != 0) + { + +// TODO - complain about any extra body or query parameter that we where not expecting. + for (t = 0; t < 3; t++) + i = collectFields(Rd, iF, iV, t); + + // Validate the input data. + D("For page %s -> %s, start of validation.", iF->name, sub->name); + t = i; + for (i = 0; i < t; i++) + { + if ((NULL != iV[i].value) || (LUA_TGROUP == iV[i].field->type)) + { + if (NULL == iV[i].field->validate) + E("No validation function for %s - %s", iF->name, iV[i].field->name); + else + { + if (0 == iV[i].valid) + { + D("Validating %s - %s", iF->name, iV[i].field->name); + int rt = iV[i].field->validate(Rd, iF, &iV[i]); + if (rt) + { + W("Invalidated %s - %s returned %d errors", iF->name, iV[i].field->name, rt); + iV[i].valid = -1; + } + else + { + D("Validated %s - %s", iF->name, iV[i].field->name); + iV[i].valid = 1; + } + e += rt; + if (NULL != iV[i].field->group) + { + // Use the indexes to set the validation result for the other members of the group. + // The assumption is that the validation functions for each are the same, and it validates the members as a group. + for (j = iV[i].index; j > 0; j--) + iV[i + j].valid = iV[i].valid; +// TODO - Possible off by one error, but I don't think it matters. Needs more testing. + for (j = 0; j <= iV[i].index; j++) + iV[i + j].valid = iV[i].valid; + } + } + else if (0 > iV[i].valid) + D("Already invalidated %s - %s", iF->name, iV[i].field->name); + else if (0 < iV[i].valid) + D("Already validated %s - %s", iF->name, iV[i].field->name); + } + } + doit = Rd->doit; + form = Rd->form; + } + + sessionStateEngine(Rd, "validated"); + + // Submit the data. Reload input form and sub in case things got changed by the validation functions. + iF = accountPages->get(accountPages, Rd->form, NULL, false); + sub = iF->subs->get(iF->subs, Rd->doit, NULL, false); +//if (strcmp("POST", Rd->Method) == 0) // Not such a good idea, since we are faking a submit for account validation, which is a GET. +{ + if (0 == e) + { + D("For page %s, start of %s submission.", iF->name, sub->name); + if (NULL != sub->submit) + e += sub->submit(Rd, iF, iV); + } +} + free(iV); + + } + else + { + sessionStateEngine(Rd, "CANCELLED"); + } + + sessionStateEngine(Rd, "submited"); + switch (Rd->shs.status) + { + case SHS_RENEW: + case SHS_REFRESH: + { + freeSesh(Rd, FALSE, FALSE); + newSesh(Rd, FALSE); + break; + } + + case SHS_VALID: + case SHS_KEEP: + { + // Do nothing here. + break; + } + + case SHS_LOGIN: + case SHS_WIPE: + { +// TODO - should wipe the old one, and create this new one with the users UUID. +// I think that's what we are doing anyway. + freeSesh(Rd, FALSE, FALSE); + newSesh(Rd, FALSE); + break; + } + +// TODO - these three should store state, so they can go back to where the user was (or where they where going) before asking them to confirm their login credentials. + case SHS_IDLE: + case SHS_SECURITY: + case SHS_RELOGIN: + + case SHS_UNKNOWN: + case SHS_NONE: + case SHS_BOGUS: + case SHS_PROBLEM: + case SHS_ANCIENT: + case SHS_NUKE: + { + freeSesh(Rd, FALSE, TRUE); // Wipe mode clears out all of Rd->database, selected Rd->stuff, and the above commented out Rd->shs. + newSesh(Rd, FALSE); + form = "accountLogin"; + doit = "logout"; + Rd->doit = doit; + Rd->form = form; + iF = accountPages->get(accountPages, Rd->form, NULL, false); + sub = iF->subs->get(iF->subs, Rd->doit, NULL, false); + Rd->output = sub->outputForm; + break; + } + } + + // Return the result. + if (0 == e) + { + if (strcmp("GET", Rd->Method) == 0) + { + // Find the output form. + inputForm *oF = accountPages->get(accountPages, Rd->output, NULL, false); + if (NULL == oF) + { + E("No such account page - %s", Rd->output); + form = "accountLogin"; + doit = "logout"; + oF = accountPages->get(accountPages, form, NULL, false); + } + D("Building output page %s", oF->name); + count = oF->fields->size(oF->fields); + iV = xzalloc(count * sizeof(inputValue)); + collectFields(Rd, oF, iV, -1); + collectFields(Rd, oF, iV, 3); + oF->web(Rd, oF, iV); + free(iV); + } + else + { + // Redirect to a GET if it was a POST. + // The reason here is to avoid a refresh turning into a rePOST. + if ((strcmp("POST", Rd->Method) == 0)) + { + if ('\0' != Rd->form[0]) + setCookie(Rd, "form", Rd->form); + if ('\0' != Rd->doit[0]) + setCookie(Rd, "doit", Rd->doit); + Rd->Rheaders->putstr (Rd->Rheaders, "Status", "303 See Other"); + Rd->Rheaders->putstrf(Rd->Rheaders, "Location", "https://%s%s%s", Rd->Host, Rd->RUri, Rd->outQuery); + Rd->reply->addstrf(Rd->reply, "Post POST redirect" + "" + "You should get redirected to https://%s%s%s", + Rd->Host, Rd->RUri, Rd->outQuery, Rd->Host, Rd->RUri, Rd->outQuery, Rd->Host, Rd->RUri, Rd->outQuery + ); + I("Redirecting dynamic page %s -> https://%s%s%s (%s)", file, Rd->Host, Rd->RUri, Rd->outQuery, Rd->form); + } + } + } + else + { + if (0 < e) + E("Building output ERROR page %s, coz of %d errors.", iF->name, e); + else + D("Building alternate output page %s", iF->name); + // First just sort out input groups, then get the data from Rd->stuff. + count = iF->fields->size(iF->fields); + iV = xzalloc(count * sizeof(inputValue)); + collectFields(Rd, iF, iV, -1); + collectFields(Rd, iF, iV, 3); + iF->eWeb(Rd, iF, iV); + free(iV); + } + + free(Rd->outQuery); + Rd->outQuery = NULL; + + C("Ending dynamic page %s %s", Rd->RUri, form); +} + + +static void cleanup(void) +{ +// TODO - not sure why, but this gets called twice on quitting sometimes. + C("Caught signal, or quitting, cleaning up."); + + if (accountPages) + { + qhashtbl_obj_t obj; + + memset((void*)&obj, 0, sizeof(obj)); + accountPages->lock(accountPages); + while(accountPages->getnext(accountPages, &obj, false) == true) + { + inputForm *f = (inputForm *) obj.data; + + f->subs->free(f->subs); + qlisttbl_obj_t fobj; + + memset((void *) &fobj, 0, sizeof(fobj)); + f->fields->lock(f->fields); + while(f->fields->getnext(f->fields, &fobj, NULL, false) == true) + { + inputField *fld = (inputField *) fobj.data; + + if (LUA_TGROUP == fld->type) + free(fld->group); + } + f->fields->unlock(f->fields); + f->fields->free(f->fields); + } + accountPages->unlock(accountPages); + accountPages->free(accountPages); + accountPages = NULL; + } + + if (dynPages) dynPages->free(dynPages); + dynPages = NULL; + if (HTMLfileCache) + { + qhashtbl_obj_t obj; + + memset((void*)&obj, 0, sizeof(obj)); + HTMLfileCache->lock(HTMLfileCache); + while(HTMLfileCache->getnext(HTMLfileCache, &obj, false) == true) + { + HTMLfile *f = (HTMLfile *) obj.data; + + unfragize(f->fragments, NULL, TRUE); + } + HTMLfileCache->unlock(HTMLfileCache); + HTMLfileCache->free(HTMLfileCache); + HTMLfileCache = NULL; + } + if (mimeTypes) mimeTypes->free(mimeTypes); + mimeTypes = NULL; + freeDb(true); + if (L) lua_close(L); + L = NULL; + if (stats) + { + if (stats->stats) stats->stats->free(stats->stats); + free(stats); + stats = NULL; + } + if (configs) configs->free(configs); + configs = NULL; +} + + +void sledjchisl_main(void) +{ + char *cmd = *toys.optargs; + char *tmp; + struct stat statbuf; + int status, result, i; + void *vd; + + configs = qhashtbl(0, 0); + L = luaL_newstate(); + + I("libfcgi version: %s", FCGI_VERSION); + I("Lua version: %s", LUA_RELEASE); + I("LuaJIT version: %s", LUAJIT_VERSION); + I("MariaDB / MySQL client version: %s", mysql_get_client_info()); + I("OpenSSL version: %s", OPENSSL_VERSION_TEXT); + I("qLibc version: qLibc only git tags for version numbers. Sooo, 2.4.4, unless I forgot to update this."); + I("toybox version: %s", TOYBOX_VERSION); + + dbRequests = qlist(0); + sigatexit(cleanup); + + pwd = getcwd(0, 0); + + if (-1 == fstat(STDIN_FILENO, &statbuf)) + { + error_msg("fstat() failed"); + if (1 != isatty(STDIN_FILENO)) + isWeb = TRUE; + } + else + { + if (S_ISREG (statbuf.st_mode)) D("STDIN is a regular file."); + else if (S_ISDIR (statbuf.st_mode)) D("STDIN is a directory."); + else if (S_ISCHR (statbuf.st_mode)) D("STDIN is a character device."); + else if (S_ISBLK (statbuf.st_mode)) D("STDIN is a block device."); + else if (S_ISFIFO(statbuf.st_mode)) D("STDIN is a FIFO (named pipe)."); + else if (S_ISLNK (statbuf.st_mode)) D("STDIN is a symbolic link."); + else if (S_ISSOCK(statbuf.st_mode)) D("STDIN is a socket."); + else D("STDIN is a unknown file descriptor type."); + if (!S_ISCHR(statbuf.st_mode)) + isWeb = TRUE; + } + + // Print our user name and groups. + struct passwd *pw; + struct group *grp; + uid_t euid = geteuid(); + gid_t egid = getegid(); + gid_t *groups = (gid_t *)toybuf; + i = sizeof(toybuf)/sizeof(gid_t); + int ngroups; + + pw = xgetpwuid(euid); + I("Running as user %s", pw->pw_name); + + grp = xgetgrgid(egid); + ngroups = getgroups(i, groups); + if (ngroups < 0) perror_exit("getgroups"); + D("User is in group %s", grp->gr_name); + for (i = 0; i < ngroups; i++) { + if (groups[i] != egid) + { + if ((grp = getgrgid(groups[i]))) + D("User is in group %s", grp->gr_name); + else + D("User is in group %u", groups[i]); + } + } + + +/* From http://luajit.org/install.html - +To change or extend the list of standard libraries to load, copy +src/lib_init.c to your project and modify it accordingly. Make sure the +jit library is loaded or the JIT compiler will not be activated. +*/ + luaL_openlibs(L); // Load Lua libraries. + + // Load the config scripts. + char *cPaths[] = + { + ".sledjChisl.conf.lua", + "/etc/sledjChisl.conf.lua", +// "/etc/sledjChisl.d/*.lua", + "~/.sledjChisl.conf.lua", +// "~/.config/sledjChisl/*.lua", + NULL + }; + struct stat st; + + + for (i = 0; cPaths[i]; i++) + { + memset(toybuf, 0, sizeof(toybuf)); + if (('/' == cPaths[i][0]) || ('~' == cPaths[i][0])) + snprintf(toybuf, sizeof(toybuf), "%s", cPaths[i]); + else + snprintf(toybuf, sizeof(toybuf), "%s/%s", pwd, cPaths[i]); + if (0 != lstat(toybuf, &st)) + continue; + I("Loading configuration file - %s", toybuf); + status = luaL_loadfile(L, toybuf); + if (status) // If something went wrong, error message is at the top of the stack. + E("Couldn't load file: %s", lua_tostring(L, -1)); + else + { + result = lua_pcall(L, 0, LUA_MULTRET, 0); + if (result) + E("Failed to run script: %s", lua_tostring(L, -1)); + else + { + lua_getglobal(L, "config"); + lua_pushnil(L); + while(lua_next(L, -2) != 0) + { + char *n = (char *) lua_tostring(L, -2); + + // Numbers can convert to strings, so check for numbers before checking for strings. + // On the other hand, strings that can be converted to numbers also pass lua_isnumber(). sigh + if (lua_isnumber(L, -1)) + { + float v = lua_tonumber(L, -1); + configs->put(configs, n, &v, sizeof(float)); + } + else if (lua_isstring(L, -1)) + configs->putstr(configs, n, (char *) lua_tostring(L, -1)); + else if (lua_isboolean(L, -1)) + { + int v = lua_toboolean(L, -1); + configs->putint(configs, n, v); + } + else + { + char *v = (char *) lua_tostring(L, -1); + E("Unknown config variable type for %s = %s", n, v); + } + lua_pop(L, 1); + } + } + } + } + DEBUG = configs->getint(configs, "debug"); + D("Setting DEBUG = %d", DEBUG); + if ((vd = configs->get (configs, "loadAverageInc", NULL, false)) != NULL) {loadAverageInc = *((float *) vd); D("Setting loadAverageInc = %f", loadAverageInc);} + if ((vd = configs->get (configs, "simTimeOut", NULL, false)) != NULL) {simTimeOut = (int) *((float *) vd); D("Setting simTimeOut = %d", simTimeOut);} + if ((tmp = configs->getstr(configs, "scRoot", false)) != NULL) {scRoot = tmp; D("Setting scRoot = %s", scRoot);} + if ((tmp = configs->getstr(configs, "scUser", false)) != NULL) {scUser = tmp; D("Setting scUser = %s", scUser);} + if ((tmp = configs->getstr(configs, "Tconsole", false)) != NULL) {Tconsole = tmp; D("Setting Tconsole = %s", Tconsole);} + if ((tmp = configs->getstr(configs, "Tsocket", false)) != NULL) {Tsocket = tmp; D("Setting Tsocket = %s", Tsocket);} + if ((tmp = configs->getstr(configs, "Ttab", false)) != NULL) {Ttab = tmp; D("Setting Ttab = %s", Ttab);} + if ((tmp = configs->getstr(configs, "webRoot", false)) != NULL) {webRoot = tmp; D("Setting webRoot = %s", webRoot);} + if ((tmp = configs->getstr(configs, "URL", false)) != NULL) {URL = tmp; D("Setting URL = %s", URL);} + if ((vd = configs->get (configs, "seshRenew", NULL, false)) != NULL) {seshRenew = (int) *((float *) vd); D("Setting seshRenew = %d", seshRenew);} + if ((vd = configs->get (configs, "idleTimeOut", NULL, false)) != NULL) {idleTimeOut = (int) *((float *) vd); D("Setting idleTimeOut = %d", idleTimeOut);} + if ((vd = configs->get (configs, "seshTimeOut", NULL, false)) != NULL) {seshTimeOut = (int) *((float *) vd); D("Setting seshTimeOut = %d", seshTimeOut);} + if ((vd = configs->get (configs, "newbieTimeOut", NULL, false)) != NULL) {newbieTimeOut = (int) *((float *) vd); D("Setting newbieTimeOut = %d", newbieTimeOut);} + if ((tmp = configs->getstr(configs, "ToS", false)) != NULL) {ToS = tmp; D("Setting ToS = %s", ToS);} + if ((tmp = configs->getstr(configs, "webIframers", false)) != NULL) {webIframers = tmp; D("Setting webIframers = %s", webIframers);} + + + // Use a FHS compatible setup - + if (strcmp("/", scRoot) == 0) + { + scBin = "/bin"; + scEtc = "/etc/opensim_SC"; + scLib = "/usr/lib/opensim_SC"; + scRun = "/run/opensim_SC"; + scBackup = "/var/backups/opensim_SC"; + scCache = "/var/cache/opensim_SC"; + scData = "/var/lib/opensim_SC"; + scLog = "/var/log/opensim_SC"; + } + else if (strcmp("/usr/local", scRoot) == 0) + { + scBin = "/usr/local/bin"; + scEtc = "/usr/local/etc/opensim_SC"; + scLib = "/usr/local/lib/opensim_SC"; + scRun = "/run/opensim_SC"; + scBackup = "/var/local/backups/opensim_SC"; + scCache = "/var/local/cache/opensim_SC"; + scData = "/var/local/lib/opensim_SC"; + scLog = "/var/local/log/opensim_SC"; + } + else // A place for everything to live, like /opt/opensim_SC + { + char *slsh = ""; + + if ('/' != scRoot[0]) + { + E("scRoot is not an absolute path - %s.", scRoot); + slsh = "/"; + } + // The problem here is that OpenSim already uses /opt/opensim_SC/current/bin for all of it's crap, where they mix everything together. + // Don't want it in the path. + // Could put the .dlls into lib. Most of the rest is config files or common assets. + // Or just slowly migrate opensim stuff to FHS. + scBin = xmprintf("%s%s/bin", slsh, scRoot); + scEtc = xmprintf("%s%s/etc", slsh, scRoot); + scLib = xmprintf("%s%s/lib", slsh, scRoot); + scRun = xmprintf("%s%s/var/run", slsh, scRoot); + scBackup = xmprintf("%s%s/var/backups", slsh, scRoot); + scCache = xmprintf("%s%s/var/cache", slsh, scRoot); + scData = xmprintf("%s%s/var/lib", slsh, scRoot); + scLog = xmprintf("%s%s/var/log", slsh, scRoot); + } + + +// TODO - still a bit chicken and egg here about the tmux socket and reading configs from scEtc /.sledjChisl.conf.lua + if (!isWeb) + { + I("Outputting to a terminal, not a web server."); + // Check if we are already running inside the proper tmux server. + char *eTMUX = getenv("TMUX"); + memset(toybuf, 0, sizeof(toybuf)); + snprintf(toybuf, sizeof(toybuf), "%s/%s", scRun, Tsocket); + if (((eTMUX) && (0 == strncmp(toybuf, eTMUX, strlen(toybuf))))) + { + I("Running inside the proper tmux server. %s", eTMUX); + isTmux = TRUE; + } + else + I("Not running inside the proper tmux server, starting it. %s == %s", eTMUX, toybuf); + } + + if (isTmux || isWeb) + { + char *d; + + // Doing this here coz at this point we should be the correct user. + if ((! qfile_exist(scBin)) && (! qfile_mkdir(scBin, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scBin); + if ((! qfile_exist(scEtc)) && (! qfile_mkdir(scEtc, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scEtc); + if ((! qfile_exist(scLib)) && (! qfile_mkdir(scLib, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scLib); + if ((! qfile_exist(scRun)) && (! qfile_mkdir(scRun, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | S_ISGID, true))) C("Unable to create path %s", scRun); + if ((! qfile_exist(scBackup)) && (! qfile_mkdir(scBackup, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scBackup); +// TODO - the path to scCache/sledjchisl.socket needs to be readable by the www-data group. So the FCGI socket will work. +// AND it needs to be group sticky on opensimsc group. So the tmux socket will work. +// So currently scCache is www-data readable, and scRun is group sticky. + if ((! qfile_exist(scCache)) && (! qfile_mkdir(scCache, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scCache); + if ((! qfile_exist(scData)) && (! qfile_mkdir(scData, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scData); + if ((! qfile_exist(scLog)) && (! qfile_mkdir(scLog, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scLog); + tmp = xmprintf("%s/sessions", scCache); + if ((! qfile_exist(tmp)) && (! qfile_mkdir(tmp, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", tmp); + free(tmp); + tmp = xmprintf("%s/users", scData); + if ((! qfile_exist(tmp)) && (! qfile_mkdir(tmp, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", tmp); + free(tmp); + + + mimeTypes = qhashtbl(0, 0); + mimeTypes->putstr(mimeTypes, "gz", "application/gzip"); + mimeTypes->putstr(mimeTypes, "js", "application/javascript"); + mimeTypes->putstr(mimeTypes, "json", "application/json"); + mimeTypes->putstr(mimeTypes, "pdf", "application/pdf"); + mimeTypes->putstr(mimeTypes, "rtf", "application/rtf"); + mimeTypes->putstr(mimeTypes, "zip", "application/zip"); + mimeTypes->putstr(mimeTypes, "xz", "application/x-xz"); + mimeTypes->putstr(mimeTypes, "gif", "image/gif"); + mimeTypes->putstr(mimeTypes, "png", "image/png"); + mimeTypes->putstr(mimeTypes, "jp2", "image/jp2"); + mimeTypes->putstr(mimeTypes, "jpg2", "image/jp2"); + mimeTypes->putstr(mimeTypes, "jpe", "image/jpeg"); + mimeTypes->putstr(mimeTypes, "jpg", "image/jpeg"); + mimeTypes->putstr(mimeTypes, "jpeg", "image/jpeg"); + mimeTypes->putstr(mimeTypes, "svg", "image/svg+xml"); + mimeTypes->putstr(mimeTypes, "svgz", "image/svg+xml"); + mimeTypes->putstr(mimeTypes, "tif", "image/tiff"); + mimeTypes->putstr(mimeTypes, "tiff", "image/tiff"); + mimeTypes->putstr(mimeTypes, "css", "text/css"); + mimeTypes->putstr(mimeTypes, "html", "text/html"); + mimeTypes->putstr(mimeTypes, "htm", "text/html"); + mimeTypes->putstr(mimeTypes, "shtml", "text/html"); +// mimeTypes->putstr(mimeTypes, "md", "text/markdown"); +// mimeTypes->putstr(mimeTypes, "markdown", "text/markdown"); + mimeTypes->putstr(mimeTypes, "txt", "text/plain"); + + memset(toybuf, 0, sizeof(toybuf)); + snprintf(toybuf, sizeof(toybuf), "%s/config/config.ini", scRoot); + +// TODO - it looks like OpenSim invented their own half arsed backwards INI file include system. +// I doubt qLibc supports it, like it supports what seems to be the standard include system. +// Not sure if we need to worry about it just yet. +// TODO - this leaks memory, but it's a bug in qLibc. Send the bug fix upstream. + qlisttbl_t *qconfig = qconfig_parse_file(NULL, toybuf, '='); + if (NULL == qconfig) + { + E("Can't read config file %s", toybuf); + goto finished; + } + d = qstrunchar(qconfig->getstr(qconfig, "Const.ConnectionString", false), '"', '"'); + + if (NULL == d) + { + E("No database credentials in %s!", toybuf); + goto finished; + } + else + { + char *p0, *p1, *p2; + if (NULL == (d = strdup(d))) + { + E("Out of memory!"); + goto finished; + } + // Data Source=MYSQL_HOST;Database=MYSQL_DB;User ID=MYSQL_USER;Password=MYSQL_PASSWORD;Old Guids=true; + p0 = d; + while (NULL != p0) + { + p1 = strchr(p0, '='); + if (NULL == p1) break; + *p1 = '\0'; + p2 = strchr(p1 + 1, ';'); + if (NULL == p2) break; + *p2 = '\0'; + configs->putstr(configs, p0, p1 + 1); // NOTE - this allocs memory for it's key and it's data. + p0 = p2 + 1; + if ('\0' == *p0) + p0 = NULL; + }; + free(d); + } + +// TODO - should also load god names, and maybe the SMTP stuff. +// Note that the OpenSim SMTP stuff is ONLY for LSL script usage, we probably want to put it in the Lua config file instead. + + if (mysql_library_init(toys.optc, toys.optargs, NULL)) + { + E("mysql_library_init() failed!"); + goto finished; + } + if (!dbConnect()) + goto finished; + + // Need to kick this off. + stats = getStats(database, stats); + char *h = qstrunchar(qconfig->getstr(qconfig, "Const.HostName", false), '"', '"'); + char *p = qstrunchar(qconfig->getstr(qconfig, "Const.PublicPort", false), '"', '"'); + stats->stats->putstr(stats->stats, "grid", qstrunchar(qconfig->getstr(qconfig, "Const.GridName", false), '"', '"')); + stats->stats->putstr(stats->stats, "HostName", h); + stats->stats->putstr(stats->stats, "PublicPort", p); + snprintf(toybuf, sizeof(toybuf), "http://%s:%s/", h, p); + + stats->stats->putstr(stats->stats, "uri", toybuf); + qconfig->free(qconfig); + } + + + if (isWeb) + { + char **initialEnv = environ; + char *tmp0, *tmp1; + int count = 0, entries, bytes; + + dynPages = qhashtbl(0, 0); + newDynPage("account.html", (pageFunction) account_html); + + // FCGI_LISTENSOCK_FILENO is the socket to the web server. + // STDOUT and STDERR go to the web servers error log, or at least it does in Apache 2 mod_fcgid. + I("Running SledjChisl inside a web server, pid %d.", getpid()); + + if (0 == toys.optc) + D("no args"); + else + { + for (entries = 0, bytes = -1; entries < toys.optc; entries++) + D("ARG %s\n", toys.optargs[entries]); + } + printEnv(environ); + +/* +? https://stackoverflow.com/questions/30707792/how-to-disable-buffering-with-apache2-and-mod-proxy-fcgi + https://z-issue.com/wp/apache-2-4-the-event-mpm-php-via-mod_proxy_fcgi-and-php-fpm-with-vhosts/ + A lengthy and detailed "how to set this up with PHP" that might be useful. + https://www.apachelounge.com/viewtopic.php?t=4385 + "Also the new mod_proxy_fcgi for Apache 2.4 seems to be crippled just like mod_fcgid in terms of being limited to just one request per process at a time." + But then so is the silly fcgi2 SDK, which basically assumes it's a CGI wrapper, not proper FCGI. ++ I could even start the spawn-fcgi process from the tmux instance of sledjchisl. ++ Orrr just open the socket / port myself from the tmux instance and do the raw FCGI thing through it. +*/ + while (FCGI_Accept() != -1) + { + reqData *Rd = xzalloc(sizeof(reqData)); + + if (-1 == clock_gettime(CLOCK_REALTIME, &Rd->then)) + perror_msg("Unable to get the time."); + Rd->L = L; + Rd->configs = configs; +// Rd->queries = qhashtbl(0, 0); // Inited in toknize below. +// Rd->body = qhashtbl(0, 0); // Inited in toknize below. +// Rd->cookies = qhashtbl(0, 0); // Inited in toknize below. + Rd->headers = qhashtbl(0, 0); + Rd->valid = qhashtbl(0, 0); + Rd->stuff = qhashtbl(0, 0); + Rd->database = qhashtbl(0, 0); + Rd->Rcookies = qhashtbl(0, 0); + Rd->Rheaders = qhashtbl(0, 0); + Rd->stats = stats; + Rd->errors = qlist(0); + Rd->messages = qlist(0); + Rd->reply = qgrow(QGROW_THREADSAFE); + Rd->outQuery = xstrdup(""); + Rd->shs.status = SHS_UNKNOWN; + qhashtbl_obj_t hobj; + qlist_obj_t lobj; + + // So far I'm seeing these as all caps, but I don't think they are defined that way. Case insensitive per the spec. + // So convert them now, also "-" -> "_". +t("HEADERS"); + char **envp = environ; + for ( ; *envp != NULL; envp++) + { + char *k = xstrdup(*envp); + char *v = strchr(k, '='); + + if (NULL != v) + { + *v = '\0'; + char *ky = qstrreplace("tr", qstrupper(k), "-", "_"); + Rd->headers->putstr(Rd->headers, ky, v + 1); +if ((strcmp("HTTP_COOKIE", ky) == 0) || (strcmp("CONTENT_LENGTH", ky) == 0) || (strcmp("QUERY_STRING", ky) == 0)) + d(" %s = %s", ky, v + 1); + } + free(k); + } + + // The FCGI paramaters sent from the server, are converted to environment variablse for the fcgi2 SDK. + // The FCGI spec doesn't mention what these are. except FCGI_WEB_SERVER_ADDRS. + char *Role = Rd->headers->getstr(Rd->headers, "FCGI_ROLE", false); + Rd->Path = Rd->headers->getstr(Rd->headers, "PATH_INFO", false); +// if (NULL == Rd->Path) {msleep(1000); continue;} + char *Length = Rd->headers->getstr(Rd->headers, "CONTENT_LENGTH", false); +//char *Type = Rd->headers->getstr(Rd->headers, "CONTENT_TYPE", false); + Rd->Method = Rd->headers->getstr(Rd->headers, "REQUEST_METHOD", false); + Rd->Script = Rd->headers->getstr(Rd->headers, "SCRIPT_NAME", false); + Rd->Scheme = Rd->headers->getstr(Rd->headers, "REQUEST_SCHEME", false); + Rd->Host = Rd->headers->getstr(Rd->headers, "HTTP_HOST", false); +//char *SUri = Rd->headers->getstr(Rd->headers, "SCRIPT_URI", false); + Rd->RUri = Rd->headers->getstr(Rd->headers, "REQUEST_URI", true); +//char *Cookies = Rd->headers->getstr(Rd->headers, "HTTP_COOKIE", false); +//char *Referer = Rd->headers->getstr(Rd->headers, "HTTP_REFERER", false); +//char *RAddr = Rd->headers->getstr(Rd->headers, "REMOTE_ADDR", false); +//char *Cache = Rd->headers->getstr(Rd->headers, "HTTP_CACHE_CONTROL", false); +//char *FAddrs = Rd->headers->getstr(Rd->headers, "FCGI_WEB_SERVER_ADDRS", false); +//char *Since = Rd->headers->getstr(Rd->headers, "IF_MODIFIED_SINCE", false); + /* Per the spec CGI https://www.ietf.org/rfc/rfc3875 + meta-variable-name = "AUTH_TYPE" | "CONTENT_LENGTH" | + "CONTENT_TYPE" | "GATEWAY_INTERFACE" | + "PATH_INFO" | "PATH_TRANSLATED" | + "QUERY_STRING" | "REMOTE_ADDR" | + "REMOTE_HOST" | "REMOTE_IDENT" | + "REMOTE_USER" | "REQUEST_METHOD" | + "SCRIPT_NAME" | "SERVER_NAME" | + "SERVER_PORT" | "SERVER_PROTOCOL" | + "SERVER_SOFTWARE" + Also protocol / scheme specific ones - + HTTP_* comes from some of the request header. The rest are likely part of the other env variables. + Also covers HTTPS headers, with the HTTP_* names. + + */ + +t("COOKIES"); + Rd->cookies = toknize(Rd->headers->getstr(Rd->headers, "HTTP_COOKIE", false), "=;"); + santize(Rd->cookies); + if (strcmp("GET", Rd->Method) == 0) + { // In theory a POST has body fields INSTEAD of query fields. Especially for ignoring the linky-hashish after the validation page. +t("QUERY"); + Rd->queries = toknize(Rd->headers->getstr(Rd->headers, "QUERY_STRING", false), "=&"); + santize(Rd->queries); + } + else + { +T("ignoring QUERY"); + Rd->queries = qhashtbl(0, 0); + free(Rd->RUri); + Rd->RUri = xmprintf("%s%s", Rd->Script, Rd->Path); + } +t("BODY"); + char *Body = NULL; + if (Length != NULL) + { + size_t len = strtol(Length, NULL, 10); + Body = xmalloc(len + 1); + int c = FCGI_fread(Body, 1, len, FCGI_stdin); + if (c != len) + { + E("Tried to read %d of the body, only got %d!", len, c); + } + Body[len] = '\0'; + } + else + Length = "0"; + Rd->body = toknize(Body, "=&"); + free(Body); + santize(Rd->body); + + I("%s %s://%s%s -> %s%s", Rd->Method, Rd->Scheme, Rd->Host, Rd->RUri, webRoot, Rd->Path); + D("Started FCGI web request ROLE = %s, body is %s bytes, pid %d.", Role, Length, getpid()); + + if (NULL == Rd->Path) + { + E("NULL path in FCGI request!"); + Rd->Rheaders->putstr(Rd->Rheaders, "Status", "404 Not Found"); + goto sendReply; + } + +/* TODO - other headers may include - + different Content-type + Status: 304 Not Modified + Last-Modified: timedatemumble + https://en.wikipedia.org/wiki/List_of_HTTP_header_fields +*/ + Rd->Rheaders->putstr(Rd->Rheaders, "Status", "200 OK"); + Rd->Rheaders->putstr(Rd->Rheaders, "Content-type", "text/html"); +// TODO - check these. + // This is all dynamic web pages, and likeley secure to. + // Most of these are from https://www.smashingmagazine.com/2017/04/secure-web-app-http-headers/ + // https://www.twilio.com/blog/a-http-headers-for-the-responsible-developer is good to, and includes useful compression and image stuff. + // On the other hand, .css files are referenced, which might be better off being cached, so should tweak some of thees. + Rd->Rheaders->putstr(Rd->Rheaders, "Cache-Control", "no-cache, no-store, must-revalidate"); + Rd->Rheaders->putstr(Rd->Rheaders, "Pragma", "no-cache"); + Rd->Rheaders->putstr(Rd->Rheaders, "Expires", "-1"); + Rd->Rheaders->putstr(Rd->Rheaders, "Strict-Transport-Security", "max-age=63072000"); // Two years. +// TODO - do something about this - + /* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src + "Note: Disallowing inline styles and inline scripts is one of + the biggest security wins CSP provides. However, if you + absolutely have to use it, there are a few mechanisms that + will allow them." + + WTF? And the mechanisms include nonces, hashes, or 'unsafe-inline'. + Not sure why inline styles need to be that secure, when downloaded ones are not. + Ah, it's for user input that is sent back to other users, they might include funky CSS in their input. + SOOOO, proper validation and escaping is needed. + OOOOR, use the nonce, and make it a different nonce per page serve. + OOOOR, just put all the style stuff in a .css file. Then we can use style-src 'self' without the 'unsafe-inline'? + There's only one block of