/* 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. #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 // TODO - audit all the alloc()s and free()s involved in qLibc stuff. #include #include #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 '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)] : '='; *pszB64Pt++ = (nIdxOfThree >= 2) ? B64CHARTBL[(szIn[2] & 0x3F)] : '-'; memset((void *) szIn, 0, sizeof(szIn)); } *pszB64Pt = '\0'; 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, 45, 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, 95, 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 }; 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 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 int (*fieldValidFunc) (reqData *Rd, qhashtbl_t *data, char *name); typedef struct _validFunc validFunc; struct _validFunc { char *name, *title; fieldValidFunc func; }; qlisttbl_t *fieldValidFuncs = NULL; static void newValidFunc(char *name, char *title, fieldValidFunc func) { validFunc *vf = xmalloc(sizeof(validFunc)); vf->name = name; vf->title = title; vf->func = func; fieldValidFuncs->put(fieldValidFuncs, vf->name, vf, sizeof(validFunc)); free(vf); } 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); } typedef void *(*pageBuildFunction) (reqData *Rd, char *message); typedef struct _buildPage buildPage; struct _buildPage { char *name; pageBuildFunction func, eFunc; }; qhashtbl_t *buildPages; static void newBuildPage(char *name, pageBuildFunction func, pageBuildFunction eFunc) { buildPage *bp = xmalloc(sizeof(buildPage)); bp->name = name; bp->func = func; bp->eFunc = eFunc; buildPages->put(buildPages, bp->name, bp, sizeof(buildPage)); free(bp); } /* TODO - there should be some precedence for values overriding values here. Nothing official? 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. TODO - I think this is the wrong question, mostly data from different sources is for different reasons. Also including values from the database. 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. We don't actually get the headers directly, it's all sent via the env. 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." Sending cookie headers is a special case, multiples can be sent, otherwise headers are singletons, only send one for each name. local storage? Would be client side Javascript thing not usually sent back to server. */ #define HMACSIZE EVP_MAX_MD_SIZE * 2 #define HMACSIZE64 88 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]; struct timespec timeStamp[2]; boolean isLinky; }; struct _reqData { lua_State *L; qhashtbl_t *configs, *queries, *body, *cookies, *headers, *valid, *stuff, *database, *Rcookies, *Rheaders; char *Scheme, *Host, *Method, *Script, *RUri, *doit; sesh shs, *lnk; MYSQL *db; gridStats *stats; qlist_t *errors, *messages; qgrow_t *reply; pageBuildFunction func; struct timespec then; boolean chillOut, vegOut; }; static void showSesh(qgrow_t *reply, sesh *shs) { if (shs->isLinky) reply->addstrf(reply, "Linky:
\n
\n");
  else
    reply->addstrf(reply, "Session:
\n
\n");

  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->addstr(reply, "
\n"); } char toybuf[4096]; lua_State *L; qhashtbl_t *configs; MYSQL *database, *dbconn; gridStats *stats; boolean isTmux = 0; boolean isWeb = 0; char *pwd = ""; char *scRoot = "/opt/opensim_SC"; char *scUser = "opensimsc"; char *scBin = ""; char *scEtc = ""; char *scLib = ""; char *scRun = ""; char *scBackup = ""; char *scCache = ""; char *scData = ""; char *scLog = ""; char *Tconsole = "SledjChisl"; char *Tsocket = "opensim-tmux.socket"; char *Ttab = "SC"; char *Tcmd = "tmux -S"; char *webRoot = "/var/www/html"; char *URL = "fcgi-bin/sledjchisl.fcgi"; char *ToS = "Be good."; int seshTimeOut = 30 * 60; int idleTimeOut = 24 * 60 * 60; int newbieTimeOut = 30; float loadAverageInc = 0.5; int simTimeOut = 45; boolean DEBUG = TRUE; qhashtbl_t *mimeTypes; qlist_t *dbRequests; // TODO - log to file. The problem is we don't know where to log until after we have loaded the configs, and before that we are spewing log messages. // Now that we are using spawn-fcgi, all the logs are going to STDERR, which we can capture and write to a file. // Unfortunately spawn-fcgi in deamon mode sends all the output to /dev/null or something. // A better idea, when we spawn tmux or spawn-fcgi, capture STDERR, full log everything to that, filtered log to the tmux console (STDOUT). // Then we can use STDOUT / STDIN to run the console stuff. // https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences char *logTypes[] = { "91;1;4", "CRITICAL", // red underlined "31", "ERROR", // dark red "93", "WARNING", // yellow "36", "TIMEOUT", // cyan "97;40", "INFO", // white "90", "DEBUG", // grey // VERBOSE? UNKNOWN? FATAL? SILENT? All from Android aparently. "35", "debug", // magenta "34", "timeout", // blue }; #define DATE_TIME_LEN 42 void logMe(int v, char *format, ...) { va_list va, va2; int len; char *ret; struct timeval tv; time_t curtime; char date[DATE_TIME_LEN]; va_start(va, format); va_copy(va2, va); // How long is it? len = vsnprintf(0, 0, format, va); len++; va_end(va); // Allocate and do the sprintf() ret = xmalloc(len); vsnprintf(ret, len, format, va2); va_end(va2); gettimeofday(&tv, NULL); curtime = tv.tv_sec; strftime(date, DATE_TIME_LEN, "(%Z %z) %F %T", localtime(&curtime)); v *= 2; fprintf(stderr, "%s.%.6ld \e[%sm%-8s sledjchisl: %s\e[0m\n", date, tv.tv_usec, logTypes[v], logTypes[v + 1], ret); free(ret); } #define C(...) logMe(0, __VA_ARGS__) #define E(...) logMe(1, __VA_ARGS__) #define W(...) logMe(2, __VA_ARGS__) #define T(...) logMe(3, __VA_ARGS__) #define I(...) logMe(4, __VA_ARGS__) #define D(...) logMe(5, __VA_ARGS__) #define d(...) logMe(6, __VA_ARGS__) #define t(...) logMe(7, __VA_ARGS__) static void addStrL(qlist_t *list, char *s) { list->addlast(list, s, strlen(s) + 1); } static char *getStrH(qhashtbl_t *hash, char *key) { char *ret = "", *t; t = hash->getstr(hash, key, false); if (NULL != t) ret = t; return ret; } char *myHMAC(char *in, boolean b64) { EVP_MD_CTX *mdctx = EVP_MD_CTX_create(); // Gets renamed to EVP_MD_CTX_new() in later versions. unsigned char md_value[EVP_MAX_MD_SIZE]; unsigned int md_len; EVP_DigestInit_ex(mdctx, EVP_sha512(), NULL); // EVP_sha3_512() isn't available until later versions. EVP_DigestUpdate(mdctx, in, strlen(in)); EVP_DigestFinal_ex(mdctx, md_value, &md_len); EVP_MD_CTX_destroy(mdctx); // Gets renamed to EVP_MD_CTX_free() in later versions. if (b64) return qB64_encode(md_value, md_len); else return qhex_encode(md_value, md_len); } char *myHMACkey(char *key, char *in, boolean b64) { unsigned char md_value[EVP_MAX_MD_SIZE]; unsigned int md_len; unsigned char* digest = HMAC(EVP_sha512(), key, strlen(key), (unsigned char *) in, strlen(in), md_value, &md_len); if (b64) return qB64_encode(md_value, md_len); else return qhex_encode(md_value, md_len); } // In Lua 5.0 reference manual is a table traversal example at page 29. void PrintTable(lua_State *L) { lua_pushnil(L); while (lua_next(L, -2) != 0) { // Numbers can convert to strings, so check for numbers before checking for strings. if (lua_isnumber(L, -1)) printf("%s = %f\n", lua_tostring(L, -2), lua_tonumber(L, -1)); else if (lua_isstring(L, -1)) printf("%s = '%s'\n", lua_tostring(L, -2), lua_tostring(L, -1)); else if (lua_istable(L, -1)) PrintTable(L); lua_pop(L, 1); } } int sendTmuxKeys(char *dest, char *keys) { int ret = 0, i; char *c = xmprintf("%s %s/%s send-keys -t %s:%s '%s'", Tcmd, scRun, Tsocket, Tconsole, dest, keys); i = system(c); if (!WIFEXITED(i)) E("tmux send-keys command failed!"); free(c); return ret; } int sendTmuxCmd(char *dest, char *cmd) { int ret = 0, i; char *c = xmprintf("%s %s/%s send-keys -t %s:'%s' '%s' Enter", Tcmd, scRun, Tsocket, Tconsole, dest, cmd); i = system(c); if (!WIFEXITED(i)) E("tmux send-keys command failed!"); free(c); return ret; } void waitTmuxText(char *dest, char *text) { int i; char *c = xmprintf("sleep 5; %s %s/%s capture-pane -t %s:'%s' -p | grep -E '%s' 2>&1 > /dev/null", Tcmd, scRun, Tsocket, Tconsole, dest, text); D("Waiting for '%s'.", text); do { i = system(c); if (!WIFEXITED(i)) { E("tmux capture-pane command failed!"); break; } else if (0 == WEXITSTATUS(i)) break; } while (1); free(c); } float waitLoadAverage(float la, float extra, int timeout) { struct sysinfo info; struct timespec timeOut; float l; int to = timeout; T("Sleeping until load average is below %.02f (%.02f + %.02f) or for %d seconds.", la + extra, la, extra, timeout); clock_gettime(CLOCK_MONOTONIC, &timeOut); to += timeOut.tv_sec; do { msleep(5000); sysinfo(&info); l = info.loads[0]/65536.0; clock_gettime(CLOCK_MONOTONIC, &timeOut); timeout -= 5; t("Tick, load average is %.02f, countdown %d seconds.", l, timeout); } while (((la + extra) < l) && (timeOut.tv_sec < to)); return l; } // Rob forget to do this, but at least he didn't declare it static. struct dirtree *dirtree_handle_callback(struct dirtree *new, int (*callback)(struct dirtree *node)); typedef struct _simList simList; struct _simList { int len, num; char **sims; }; static int filterSims(struct dirtree *node) { if (!node->parent) return DIRTREE_RECURSE | DIRTREE_SHUTUP; if ((strncmp(node->name, "sim", 3) == 0) && ((strcmp(node->name, "sim_skeleton") != 0))) { simList *list = (simList *) node->parent->extra; if ((list->num + 1) > list->len) { list->len = list->len + 1; list->sims = xrealloc(list->sims, list->len * sizeof(char *)); } list->sims[list->num] = xstrdup(node->name); list->num++; } return 0; } simList *getSims() { simList *sims = xmalloc(sizeof(simList)); memset(sims, 0, sizeof(simList)); char *path = xmprintf("%s/config", scRoot); struct dirtree *new = dirtree_add_node(0, path, 0); new->extra = (long) sims; dirtree_handle_callback(new, filterSims); qsort(sims->sims, sims->num, sizeof(char *), qstrcmp); free(path); return sims; } static int filterInis(struct dirtree *node) { if (!node->parent) return DIRTREE_RECURSE | DIRTREE_SHUTUP; int l = strlen(node->name); if (strncmp(&(node->name[l - 4]), ".ini", 4) == 0) { strcpy((char *) node->parent->extra, xstrdup(node->name)); return DIRTREE_ABORT; } return 0; } char *getSimName(char *sim) { char *ret = NULL; char *c = xmprintf("%s/config/%s", scRoot, sim); struct dirtree *new = dirtree_add_node(0, c, 0); free(c); c = xzalloc(1024); new->extra = (long) c; dirtree_handle_callback(new, filterInis); if ('\0' != c[0]) { char *temp = NULL; regex_t pat; regmatch_t m[2]; long len; int fd; temp = xmprintf("%s/config/%s/%s", scRoot, sim, c); fd = xopenro(temp); free(temp); xregcomp(&pat, "RegionName = \"(.+)\"", REG_EXTENDED); do { // TODO - get_line() is slow, and wont help much with DOS and Mac line endings. // gio_gets() isn't any faster really, but deals with DOS line endings at least. temp = get_line(fd); if (temp) { if (!regexec(&pat, temp, 2, m, 0)) { // Return first parenthesized subexpression as string. if (pat.re_nsub > 0) { ret = xmprintf("%.*s", (int) (m[1].rm_eo - m[1].rm_so), temp + m[1].rm_so); break; } } } } while (temp); xclose(fd); } free(c); return ret; } // Expects either "simXX" or "ROBUST". int checkSimIsRunning(char *sim) { int ret = 0; struct stat st; // Check if it's running. char *path = xmprintf("%s/caches/%s.pid", scRoot, sim); if (0 == stat(path, &st)) { int fd, i; char *pid = NULL; // Double check if it's REALLY running. if ((fd = xopenro(path)) == -1) perror_msg("xopenro(%s)", path); else { pid = get_line(fd); if (NULL == pid) perror_msg("get_line(%s)", path); else { xclose(fd); // I'd rather re-use the toysh command running stuff, since ps is a toy, but that's private. // TODO - switch to toybox ps and rm. free(path); path = xmprintf("ps -p %s --no-headers -o comm", pid); i = system(path); if (WIFEXITED(i)) { if (0 != WEXITSTATUS(i)) // No such pid. { free(path); path = xmprintf("rm -f %s/caches/%s.pid", scRoot, sim); d("%s", path); i = system(path); } else d("checkSimIsRunning(%s) has PID %s, which is actually running.", sim, pid); } } } } // Now check if it's really really running. lol free(path); path = xmprintf("%s/caches/%s.pid", scRoot, sim); if (0 == stat(path, &st)) { D("checkSimIsRunning(%s) -> %s is really really running.", sim, path); ret = 1; } else D("checkSimIsRunning(%s) -> %s is not running.", sim, path); free(path); return ret; } static void PrintEnv(qgrow_t *reply, char *label, char **envp) { reply->addstrf(reply, "%s:
\n
\n", label);
  for ( ; *envp != NULL; envp++)
    reply->addstrf(reply, "%s\n", *envp);
  reply->addstr(reply, "
\n"); } static void printEnv(char **envp) { for ( ; *envp != NULL; envp++) d("%s", *envp); } typedef struct _rowData rowData; struct _rowData { char **fieldNames; qlist_t *rows; }; static void dumpHash(qhashtbl_t *tbl) { qhashtbl_obj_t obj; memset((void*)&obj, 0, sizeof(obj)); tbl->lock(tbl); while(tbl->getnext(tbl, &obj, true) == true) d("%s = %s", obj.name, (char *) obj.data); tbl->unlock(tbl); } static void dumpArray(int d, char **ar) { int i = 0; while (ar[i] != NULL) { d("%d %d %s", d, i, ar[i]); i++; } } /* How to deal with prepared SQL statements. http://karlssonondatabases.blogspot.com/2010/07/prepared-statements-are-they-useful-or.html https://blog.cotten.io/a-taste-of-mysql-in-c-87c5de84a31d?gi=ab3dd1425b29 https://raspberry-projects.com/pi/programming-in-c/databases-programming-in-c/mysql/accessing-the-database IG and CG now both have sims connected to other grids, so some sort of multi database solution would be good, then we can run the grid and the external sims all in one. Not sure if this'll work with Count(*). --------------------------------------------- The complicated bit is the binds. You are binding field values to C memory locations. The parameters and returned fields need binds. Mostly seems to be the value parts of the SQL statements. I suspect most will be of the form - ... WHERE x=? and foo=? INSERT INTO table VALUES (?,?,?) UPDATE table SET x=?, foo=? WHERE id=? A multi table update - UPDATE items,month SET items.price=month.price WHERE items.id=month.id; */ typedef struct _dbField dbField; struct _dbField { char *name; enum enum_field_types type; unsigned long length; unsigned int flags; unsigned int decimals; }; qlisttbl_t *dbGetFields(MYSQL *db, char *table) { static qhashtbl_t *tables = NULL; if (NULL == tables) tables = qhashtbl(0, 0); qlisttbl_t *ret = tables->get(tables, table, NULL, false); if (NULL == ret) { // Seems the only way to get field metadata is to actually perform a SQL statement, then you get the field metadata for the result set. // Chicken, meet egg, sorry you had to cross the road for this. char *sql = xmprintf("SELECT * FROM %s LIMIT 0", table); d("Getting field metadata for %s", table); if (mysql_query(db, sql)) E("Query failed: %s\n%s", mysql_error(db), sql); else { MYSQL_RES *res = mysql_store_result(db); if (!res) E("Couldn't get results set from %s\n %s", mysql_error(db), sql); else { MYSQL_FIELD *fields = mysql_fetch_fields(res); if (!fields) E("Failed fetching fields: %s", mysql_error(db)); else { unsigned int i, num_fields = mysql_num_fields(res); ret = qlisttbl(QLISTTBL_UNIQUE | QLISTTBL_LOOKUPFORWARD); for (i = 0; i < num_fields; i++) { dbField *fld = xmalloc(sizeof(dbField)); fld->name = xstrdup(fields[i].name); fld->type = fields[i].type; fld->length = fields[i].length; fld->flags = fields[i].flags; fld->decimals = fields[i].decimals; ret->put(ret, fld->name, fld, sizeof(*fld)); free(fld); } tables->put(tables, table, ret, sizeof(*ret)); } mysql_free_result(res); } } free(sql); } return ret; } void dbFreeFields(qlisttbl_t *flds) { qlisttbl_obj_t obj; memset((void *) &obj, 0, sizeof(obj)); flds->lock(flds); d("Freeing fields."); while(flds->getnext(flds, &obj, NULL, false) == true) { dbField *fld = (dbField *) obj.data; d("Freeing field %s", fld->name); free(fld->name); } flds->unlock(flds); flds->free(flds); } typedef struct _dbRequest dbRequest; struct _dbRequest { MYSQL *db; char *table, *join, *where, *order, *sql; qlisttbl_t *flds; int inCount, outCount, rowCount; char **inParams, **outParams; MYSQL_STMT *prep; // NOTE - executing it stores state in this. MYSQL_BIND *inBind, *outBind; rowData *rows; my_ulonglong count; boolean freeOutParams; }; void dbDoSomething(dbRequest *req, boolean count, ...) { va_list ap; struct timespec now, then; int i, j; MYSQL_RES *prepare_meta_result = NULL; if (-1 == clock_gettime(CLOCK_REALTIME, &then)) perror_msg("Unable to get the time."); va_start(ap, count); if (NULL == req->prep) { req->flds = dbGetFields(req->db, req->table); if (NULL == req->flds) { E("Unknown fields for table %s.", req->table); goto end; } char *select = xmprintf(""); i = 0; while (req->outParams[i] != NULL) { char *t = xmprintf("%s,%s", select, req->outParams[i]); free(select); select = t; i++; } if (0 == i) { free(select); if (count) select = xmprintf(",Count(*)"); else select = xmprintf(",*"); } if (NULL == req->join) req->join = ""; if (req->where) req->sql = xmprintf("SELECT %s FROM %s %s WHERE %s", &select[1], req->table, req->join, req->where); else req->sql = xmprintf("SELECT %s FROM %s", &select[1], req->table, req->join); free(select); if (req->order) { char *t = xmprintf("%s ORDER BY %s", req->sql, req->order); free(req->sql); req->sql = t; } d("New SQL statement - %s", req->sql); // prepare statement with the other fields req->prep = mysql_stmt_init(req->db); if (NULL == req->prep) { E("Statement prepare init failed: %s\n", mysql_stmt_error(req->prep)); goto end; } if (mysql_stmt_prepare(req->prep, req->sql, strlen(req->sql))) { E("Statement prepare failed: %s\n", mysql_stmt_error(req->prep)); goto end; } // setup the bind stuff for any "?" parameters in the SQL. req->inCount = mysql_stmt_param_count(req->prep); i = 0; while (req->inParams[i] != NULL) i++; if (i != req->inCount) { E("In parameters count don't match %d != %d for - %s", i, req->inCount, req->sql); goto freeIt; } req->inBind = xzalloc(i * sizeof(MYSQL_BIND)); for (i = 0; i < req->inCount; i++) { dbField *fld = req->flds->get(req->flds, req->inParams[i], NULL, false); if (NULL == fld) { E("Unknown input field %d %s.%s for - %s", i, req->table, req->inParams[i], req->sql); goto freeIt; } else { // https://blog.cotten.io/a-taste-of-mysql-in-c-87c5de84a31d?gi=ab3dd1425b29 // For some gotchas about all of this binding bit. req->inBind[i].buffer_type = fld->type; req->inBind[i].buffer = xzalloc(fld->length) + 1; // Note the + 1 is for string types, and a waste for the rest. req->inBind[i].buffer_length = fld->length; switch(fld->type) { case MYSQL_TYPE_TINY: { break; } case MYSQL_TYPE_SHORT: { req->inBind[i].is_unsigned = FALSE; break; } case MYSQL_TYPE_INT24: { req->inBind[i].is_unsigned = FALSE; break; } case MYSQL_TYPE_LONG: { req->inBind[i].is_unsigned = FALSE; break; } case MYSQL_TYPE_LONGLONG: { req->inBind[i].is_unsigned = FALSE; break; } case MYSQL_TYPE_FLOAT: { break; } case MYSQL_TYPE_DOUBLE: { break; } case MYSQL_TYPE_NEWDECIMAL: { break; } case MYSQL_TYPE_TIME: case MYSQL_TYPE_DATE: case MYSQL_TYPE_DATETIME: case MYSQL_TYPE_TIMESTAMP: { break; } case MYSQL_TYPE_STRING: case MYSQL_TYPE_VAR_STRING: { req->inBind[i].is_null = xzalloc(sizeof(my_bool)); req->inBind[i].length = xzalloc(sizeof(unsigned long)); break; } case MYSQL_TYPE_TINY_BLOB: case MYSQL_TYPE_BLOB: case MYSQL_TYPE_MEDIUM_BLOB: case MYSQL_TYPE_LONG_BLOB: { req->inBind[i].is_null = xzalloc(sizeof(my_bool)); break; } case MYSQL_TYPE_BIT: { req->inBind[i].is_null = xzalloc(sizeof(my_bool)); break; } case MYSQL_TYPE_NULL: { break; } } } } // TODO - if this is not a count, setup result bind paramateres, may be needed for counts as well. prepare_meta_result = mysql_stmt_result_metadata(req->prep); if (!prepare_meta_result) { D(" mysql_stmt_result_metadata(), returned no meta information - %s\n", mysql_stmt_error(req->prep)); goto freeIt; } if (count) { I("count!!!!!!!!!!!!!!!!"); } else { req->outCount = mysql_num_fields(prepare_meta_result); i = 0; while (req->outParams[i] != NULL) i++; if (0 == i) // Passing in {NULL} as req->outParams means "return all of them". { req->outParams = xzalloc((req->outCount + 1) * sizeof(char *)); req->freeOutParams = TRUE; qlisttbl_obj_t obj; memset((void*)&obj, 0, sizeof(obj)); req->flds->lock(req->flds); while (req->flds->getnext(req->flds, &obj, NULL, false) == true) { dbField *fld = (dbField *) obj.data; req->outParams[i] = fld->name; i++; } req->outParams[i] = NULL; req->flds->unlock(req->flds); } if (i != req->outCount) { E("Out parameters count doesn't match %d != %d foqr - %s", i, req->outCount, req->sql); goto freeIt; } req->outBind = xzalloc(i * sizeof(MYSQL_BIND)); for (i = 0; i < req->outCount; i++) { dbField *fld = req->flds->get(req->flds, req->outParams[i], NULL, false); if (NULL == fld) { E("Unknown output field %d %s.%s foqr - %s", i, req->table, req->outParams[i], req->sql); goto freeIt; } else { // https://blog.cotten.io/a-taste-of-mysql-in-c-87c5de84a31d?gi=ab3dd1425b29 // For some gotchas about all of this binding bit. req->outBind[i].buffer_type = fld->type; req->outBind[i].buffer = xzalloc(fld->length + 1); // Note the + 1 is for string types, and a waste for the rest. req->outBind[i].buffer_length = fld->length; req->outBind[i].error = xzalloc(sizeof(my_bool)); req->outBind[i].is_null = xzalloc(sizeof(my_bool)); switch(fld->type) { case MYSQL_TYPE_TINY: { //d("TINY %d %s %d", i, fld->name, req->outBind[i].buffer_length); break; } case MYSQL_TYPE_SHORT: { //d("SHORT %s %d", fld->name, req->outBind[i].buffer_length); req->outBind[i].is_unsigned = FALSE; break; } case MYSQL_TYPE_INT24: { //d("INT24 %s %d", fld->name, req->outBind[i].buffer_length); req->outBind[i].is_unsigned = FALSE; break; } case MYSQL_TYPE_LONG: { //d("LONG %d %s %d", i, fld->name, req->outBind[i].buffer_length); req->outBind[i].is_unsigned = FALSE; break; } case MYSQL_TYPE_LONGLONG: { //d("LONG LONG %s %d", fld->name, req->outBind[i].buffer_length); req->outBind[i].is_unsigned = FALSE; break; } case MYSQL_TYPE_FLOAT: { //d("FLOAT %s %d", fld->name, req->outBind[i].buffer_length); break; } case MYSQL_TYPE_DOUBLE: { //d("DOUBLE %s %d", fld->name, req->outBind[i].buffer_length); break; } case MYSQL_TYPE_NEWDECIMAL: { //d("NEWDECIMAL %s %d", fld->name, req->outBind[i].buffer_length); break; } case MYSQL_TYPE_TIME: case MYSQL_TYPE_DATE: case MYSQL_TYPE_DATETIME: case MYSQL_TYPE_TIMESTAMP: { //d("DATETIME %s %d", fld->name, req->outBind[i].buffer_length); break; } case MYSQL_TYPE_STRING: case MYSQL_TYPE_VAR_STRING: { //d("STRING %s %d", fld->name, req->outBind[i].buffer_length); req->outBind[i].length = xzalloc(sizeof(unsigned long)); break; } case MYSQL_TYPE_TINY_BLOB: case MYSQL_TYPE_BLOB: case MYSQL_TYPE_MEDIUM_BLOB: case MYSQL_TYPE_LONG_BLOB: { //d("BLOB %s %d", fld->name, req->outBind[i].buffer_length); break; } case MYSQL_TYPE_BIT: { //d("BIT %s %d", fld->name, req->outBind[i].buffer_length); break; } case MYSQL_TYPE_NULL: { //d("NULL %s %d", fld->name, req->outBind[i].buffer_length); break; } } } } if (mysql_stmt_bind_result(req->prep, req->outBind)) { E("Bind failed."); goto freeIt; } } } //d("input bind for %s", req->sql); for (i = 0; i < req->inCount; i++) { dbField *fld = req->flds->get(req->flds, req->inParams[i], NULL, false); if (NULL == fld) { E("Unknown input field %s.%s for - %s", req->table, req->inParams[i], req->sql); goto freeIt; } else { switch(fld->type) { case MYSQL_TYPE_TINY: { int c = va_arg(ap, int); signed char d = (signed char) c; memcpy(&d, req->inBind[i].buffer, (size_t) fld->length); break; } case MYSQL_TYPE_SHORT: { int c = va_arg(ap, int); short int d = (short int) c; memcpy(&d, req->inBind[i].buffer, (size_t) fld->length); break; } case MYSQL_TYPE_INT24: { int d = va_arg(ap, int); memcpy(&d, req->inBind[i].buffer, (size_t) fld->length); break; } case MYSQL_TYPE_LONG: { long d = va_arg(ap, long); memcpy(&d, req->inBind[i].buffer, (size_t) fld->length); break; } case MYSQL_TYPE_LONGLONG: { long long int d = va_arg(ap, long long int); memcpy(&d, req->inBind[i].buffer, (size_t) fld->length); break; } case MYSQL_TYPE_FLOAT: { double c = va_arg(ap, double); float d = (float) c; memcpy(&d, req->inBind[i].buffer, (size_t) fld->length); break; } case MYSQL_TYPE_DOUBLE: { double d = va_arg(ap, double); memcpy(&d, req->inBind[i].buffer, (size_t) fld->length); break; } case MYSQL_TYPE_NEWDECIMAL: { break; } case MYSQL_TYPE_TIME: case MYSQL_TYPE_DATE: case MYSQL_TYPE_DATETIME: case MYSQL_TYPE_TIMESTAMP: { MYSQL_TIME d = va_arg(ap, MYSQL_TIME); memcpy(&d, req->inBind[i].buffer, (size_t) fld->length); break; } case MYSQL_TYPE_STRING: case MYSQL_TYPE_VAR_STRING: { char *d = va_arg(ap, char *); unsigned long l = strlen(d); if (l > fld->length) l = fld->length; *(req->inBind[i].length) = l; strncpy(req->inBind[i].buffer, d, (size_t) l); ((char *) req->inBind[i].buffer)[l] = '\0'; break; } case MYSQL_TYPE_TINY_BLOB: case MYSQL_TYPE_BLOB: case MYSQL_TYPE_MEDIUM_BLOB: case MYSQL_TYPE_LONG_BLOB: { break; } case MYSQL_TYPE_BIT: { break; } case MYSQL_TYPE_NULL: { break; } } } } if (mysql_stmt_bind_param(req->prep, req->inBind)) { E("Bind failed."); goto freeIt; } d("Execute %s", req->sql); // do the prepared statement req->prep. if (mysql_stmt_execute(req->prep)) { E("Statement execute failed: %s\n", mysql_stmt_error(req->prep)); goto freeIt; } int fs = mysql_stmt_field_count(req->prep); // stuff results back into req. if (NULL != req->outBind) { req->rows = xmalloc(sizeof(rowData)); req->rows->fieldNames = xzalloc(fs * sizeof(char *)); if (mysql_stmt_store_result(req->prep)) { E(" mysql_stmt_store_result() failed %s", mysql_stmt_error(req->prep)); goto freeIt; } req->rowCount = mysql_stmt_num_rows(req->prep); if (0 == req->rowCount) D("No rows returned from : %s\n", req->sql); else D("%d rows of %d fields returned from : %s\n", req->rowCount, fs, req->sql); req->rows->rows = qlist(0); while (MYSQL_NO_DATA != mysql_stmt_fetch(req->prep)) { qhashtbl_t *flds = qhashtbl(0, 0); for (i = 0; i < req->outCount; i++) { dbField *fld = req->flds->get(req->flds, req->outParams[i], NULL, false); req->rows->fieldNames[i] = fld->name; if (!*(req->outBind[i].is_null)) { //d("2.8 %s", req->rows->fieldNames[i]); flds->put(flds, req->rows->fieldNames[i], req->outBind[i].buffer, req->outBind[i].buffer_length); switch(fld->type) { case MYSQL_TYPE_TINY: { break; } case MYSQL_TYPE_SHORT: { char *t = xmprintf("%d", (int) *((int *) req->outBind[i].buffer)); flds->putstr(flds, req->rows->fieldNames[i], t); free(t); break; } case MYSQL_TYPE_INT24: { char *t = xmprintf("%d", (int) *((int *) req->outBind[i].buffer)); flds->putstr(flds, req->rows->fieldNames[i], t); free(t); break; } case MYSQL_TYPE_LONG: { if (NULL == req->outBind[i].buffer) { E("Field %d %s is NULL", i, fld->name); goto freeIt; } char *t = xmprintf("%d", (int) *((int *) (req->outBind[i].buffer))); //d("Setting %i %s %s", i, fld->name, t); flds->putstr(flds, req->rows->fieldNames[i], t); free(t); break; } case MYSQL_TYPE_LONGLONG: { char *t = xmprintf("%d", (int) *((int *) req->outBind[i].buffer)); flds->putstr(flds, req->rows->fieldNames[i], t); free(t); break; } case MYSQL_TYPE_FLOAT: { break; } case MYSQL_TYPE_DOUBLE: { break; } case MYSQL_TYPE_NEWDECIMAL: { break; } case MYSQL_TYPE_TIME: case MYSQL_TYPE_DATE: case MYSQL_TYPE_DATETIME: case MYSQL_TYPE_TIMESTAMP: { break; } case MYSQL_TYPE_STRING: case MYSQL_TYPE_VAR_STRING: { break; } case MYSQL_TYPE_TINY_BLOB: case MYSQL_TYPE_BLOB: case MYSQL_TYPE_MEDIUM_BLOB: case MYSQL_TYPE_LONG_BLOB: { break; } case MYSQL_TYPE_BIT: { break; } case MYSQL_TYPE_NULL: { break; } } } else D("Not setting data %s, coz it's NULL", fld->name); } req->rows->rows->addlast(req->rows->rows, flds, sizeof(qhashtbl_t)); free(flds); } } freeIt: if (prepare_meta_result) mysql_free_result(prepare_meta_result); if (mysql_stmt_free_result(req->prep)) E("Statement result freeing failed: %s\n", mysql_stmt_error(req->prep)); end: va_end(ap); if (-1 == clock_gettime(CLOCK_REALTIME, &now)) perror_msg("Unable to get the time."); double n = (now.tv_sec * 1000000000.0) + now.tv_nsec; double t = (then.tv_sec * 1000000000.0) + then.tv_nsec; T("dbDoSomething(%s) took %lf seconds", req->sql, (n - t) / 1000000000.0); return; } // Copy the SQL results into the request structure. void dbPull(reqData *Rd, char *table, rowData *rows) { char *where; qhashtbl_t *me = rows->rows->popfirst(rows->rows, NULL); qhashtbl_obj_t obj; memset((void*)&obj, 0, sizeof(obj)); me->lock(me); while(me->getnext(me, &obj, false) == true) { where = xmprintf("%s.%s", table, obj.name); Rd->database->putstr(Rd->database, where, (char *) obj.data); // me->remove(me, obj.name); free(where); } me->unlock(me); me->free(me); } void dbFreeRequest(dbRequest *req) { int i; D("Cleaning up prepared database request %s - %s", req->table, req->where); 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); } if (NULL != req->inBind) { for (i = 0; i < req->inCount; i++) { // TODO - this leaks for some bizare reason. if (NULL != req->inBind[i].buffer) free(req->inBind[i].buffer); if (NULL != req->inBind[i].length) free(req->inBind[i].length); if (NULL != req->inBind[i].error) free(req->inBind[i].error); if (NULL != req->inBind[i].is_null) free(req->inBind[i].is_null); } free(req->inBind); } if (req->freeOutParams) free(req->outParams); if (NULL != req->sql) free(req->sql); if (NULL != req->prep) { if (0 != mysql_stmt_close(req->prep)) C("Unable to close the prepared statement!"); free(req->prep); } } my_ulonglong dbCount(MYSQL *db, char *table, char *where) { my_ulonglong ret = 0; char *sql; struct timespec now, then; if (-1 == clock_gettime(CLOCK_REALTIME, &then)) perror_msg("Unable to get the time."); if (where) sql = xmprintf("SELECT Count(*) FROM %s WHERE %s", table, where); else sql = xmprintf("SELECT Count(*) FROM %s", table); if (mysql_query(db, sql)) E("Query failed: %s", mysql_error(db)); else { MYSQL_RES *result = mysql_store_result(db); if (!result) E("Couldn't get results set from %s\n: %s", sql, mysql_error(db)); else { MYSQL_ROW row = mysql_fetch_row(result); if (!row) E("Couldn't get row from %s\n: %s", sql, mysql_error(db)); else ret = atoll(row[0]); mysql_free_result(result); } } if (-1 == clock_gettime(CLOCK_REALTIME, &now)) perror_msg("Unable to get the time."); double n = (now.tv_sec * 1000000000.0) + now.tv_nsec; double t = (then.tv_sec * 1000000000.0) + then.tv_nsec; T("dbCount(%s) took %lf seconds", sql, (n - t) / 1000000000.0); free(sql); return ret; } my_ulonglong dbCountJoin(MYSQL *db, char *table, char *select, char *join, char *where) { my_ulonglong ret = 0; char *sql; struct timespec now, then; if (-1 == clock_gettime(CLOCK_REALTIME, &then)) perror_msg("Unable to get the time."); if (NULL == select) select = "*"; if (NULL == join) join = ""; if (where) sql = xmprintf("SELECT %s FROM %s %s WHERE %s", select, table, join, where); else sql = xmprintf("SELECT %s FROM %s", select, table, join); if (mysql_query(db, sql)) E("Query failed: %s", mysql_error(db)); else { MYSQL_RES *result = mysql_store_result(db); if (!result) E("Couldn't get results set from %s\n: %s", sql, mysql_error(db)); else ret = mysql_num_rows(result); mysql_free_result(result); } if (-1 == clock_gettime(CLOCK_REALTIME, &now)) perror_msg("Unable to get the time."); double n = (now.tv_sec * 1000000000.0) + now.tv_nsec; double t = (then.tv_sec * 1000000000.0) + then.tv_nsec; T("dbCointJoin(%s) took %lf seconds", sql, (n - t) / 1000000000.0); free(sql); return ret; } MYSQL_RES *dbSelect(MYSQL *db, char *table, char *select, char *join, char *where, char *order) { MYSQL_RES *ret = NULL; char *sql; struct timespec now, then; if (-1 == clock_gettime(CLOCK_REALTIME, &then)) perror_msg("Unable to get the time."); if (NULL == select) select = "*"; if (NULL == join) join = ""; if (where) sql = xmprintf("SELECT %s FROM %s %s WHERE %s", select, table, join, where); else sql = xmprintf("SELECT %s FROM %s", select, table, join); if (order) { char *t = xmprintf("%s ORDER BY %s", sql, order); free(sql); sql = t; } if (mysql_query(db, sql)) E("Query failed: %s\n%s", mysql_error(db), sql); else { ret = mysql_store_result(db); if (!ret) E("Couldn't get results set from %s\n %s", mysql_error(db), sql); } if (-1 == clock_gettime(CLOCK_REALTIME, &now)) perror_msg("Unable to get the time."); double n = (now.tv_sec * 1000000000.0) + now.tv_nsec; double t = (then.tv_sec * 1000000000.0) + then.tv_nsec; T("dbSelect(%s) took %lf seconds", sql, (n - t) / 1000000000.0); free(sql); return ret; } void replaceStr(qhashtbl_t *ssi, char *key, char *value) { ssi->putstr(ssi, key, value); } void replaceLong(qhashtbl_t *ssi, char *key, my_ulonglong value) { char *tmp = xmprintf("%lu", value); replaceStr(ssi, key, tmp); free(tmp); } float timeDiff(struct timeval *now, struct timeval *then) { if (0 == gettimeofday(now, NULL)) { struct timeval thisTime = { 0, 0 }; double result = 0.0; thisTime.tv_sec = now->tv_sec; thisTime.tv_usec = now->tv_usec; if (thisTime.tv_usec < then->tv_usec) { thisTime.tv_sec--; thisTime.tv_usec += 1000000; } thisTime.tv_usec -= then->tv_usec; thisTime.tv_sec -= then->tv_sec; result = ((double) thisTime.tv_usec) / ((double) 1000000.0); result += thisTime.tv_sec; return result; } return 0.0; } gridStats *getStats(MYSQL *db, gridStats *stats) { if (NULL == stats) { stats = xmalloc(sizeof(gridStats)); stats->next = 30; gettimeofday(&(stats->last), NULL); stats->stats = qhashtbl(0, 0); stats->stats->putstr(stats->stats, "version", "SledjChisl FCGI Dev 0.1"); stats->stats->putstr(stats->stats, "grid", "my grid"); stats->stats->putstr(stats->stats, "uri", "http://localhost:8002/"); if (checkSimIsRunning("ROBUST")) stats->stats->putstr(stats->stats, "gridOnline", "online"); else stats->stats->putstr(stats->stats, "gridOnline", "offline"); } else { static struct timeval thisTime; if (stats->next > timeDiff(&thisTime, &(stats->last))) return stats; } I("Getting fresh grid stats."); if (checkSimIsRunning("ROBUST")) replaceStr(stats->stats, "gridOnline", "online"); else replaceStr(stats->stats, "gridOnline", "offline"); if (db) { char *tmp; my_ulonglong locIn = dbCount(db, "Presence", "RegionID != '00000000-0000-0000-0000-000000000000'"); // Locals online but not HGing, and HGers in world. my_ulonglong HGin = dbCount(db, "Presence", "UserID NOT IN (SELECT PrincipalID FROM UserAccounts)"); // HGers in world. // Collect stats about members. replaceLong(stats->stats, "hgers", HGin); replaceLong(stats->stats, "inworld", locIn - HGin); tmp = xmprintf("GridExternalName != '%s'", stats->stats->getstr(stats->stats, "uri", false)); replaceLong(stats->stats, "outworld", dbCount(db, "hg_traveling_data", tmp)); free(tmp); replaceLong(stats->stats, "members", dbCount(db, "UserAccounts", NULL)); // Count local and HG visitors for the last 30 and 60 days. locIn = dbCountJoin(db, "GridUser", "GridUser.UserID", "INNER JOIN UserAccounts ON GridUser.UserID = UserAccounts.PrincipalID", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 2419200))"); HGin = dbCount(db, "GridUser", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 2419200))"); replaceLong(stats->stats, "locDay30", locIn); replaceLong(stats->stats, "day30", HGin); replaceLong(stats->stats, "HGday30", HGin - locIn); locIn = dbCountJoin(db, "GridUser", "GridUser.UserID", "INNER JOIN UserAccounts ON GridUser.UserID = UserAccounts.PrincipalID", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 4838400))"); HGin = dbCount(db, "GridUser", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 4838400))"); replaceLong(stats->stats, "locDay60", locIn); replaceLong(stats->stats, "day60", HGin); replaceLong(stats->stats, "HGday60", HGin - locIn); // Collect stats about sims. replaceLong(stats->stats, "sims", dbCount(db, "regions", NULL)); replaceLong(stats->stats, "onlineSims", dbCount(db, "regions", "sizeX != 0")); replaceLong(stats->stats, "varRegions", dbCount(db, "regions", "sizeX > 256 or sizeY > 256")); replaceLong(stats->stats, "singleSims", dbCount(db, "regions", "sizeX = 256 and sizeY = 256")); replaceLong(stats->stats, "offlineSims", dbCount(db, "regions", "sizeX = 0")); // Calculate total size of all regions. my_ulonglong simSize = 0; static dbRequest *rgnSizes = NULL; if (NULL == rgnSizes) { static char *szi[] = {NULL}; static char *szo[] = {"sizeX", "sizeY", NULL}; rgnSizes = xzalloc(sizeof(dbRequest)); rgnSizes->db = db; rgnSizes->table = "regions"; rgnSizes->inParams = szi; rgnSizes->outParams = szo; rgnSizes->where = "sizeX != 0"; dbRequests->addfirst(dbRequests, rgnSizes, sizeof(*rgnSizes)); } dbDoSomething(rgnSizes, FALSE); rowData *rows = rgnSizes->rows; qhashtbl_t *row; while (NULL != (row = rows->rows->getat(rows->rows, 0, NULL, true))) { my_ulonglong x = 0, y = 0; tmp = row->getstr(row, "sizeX", false); if (NULL == tmp) E("No regions.sizeX!"); else x = atoll(tmp); tmp = row->getstr(row, "sizeY", false); if (NULL == tmp) E("No regions.sizeY!"); else y = atoll(tmp); simSize += x * y; // TODO - I can't win. valgrind complains that either something is being freed twice, or not freed at all, no matter what I do. // This seems to keep the memory loss down to a minimum. row->free(row); rows->rows->removefirst(rows->rows); } free(rows->fieldNames); rows->rows->free(rows->rows); free(rows); tmp = xmprintf("%lu", simSize); stats->stats->putstr(stats->stats, "simsSize", tmp); free(tmp); gettimeofday(&(stats->last), NULL); } return stats; } qhashtbl_t *toknize(char *text, char *delims) { qhashtbl_t *ret = qhashtbl(0, 0); if (NULL == text) return ret; char *txt = xstrdup(text), *token, dlm = ' ', *key, *val = NULL; int offset = 0; while((token = qstrtok(txt, delims, &dlm, &offset)) != NULL) { if (delims[0] == dlm) { key = token; val = &txt[offset]; } else if (delims[1] == dlm) { ret->putstr(ret, qstrtrim_head(key), token); d(" %s = %s", qstrtrim_head(key), val); val = NULL; } } if (NULL != val) { ret->putstr(ret, qstrtrim_head(key), val); d(" %s = %s", qstrtrim_head(key), val); } free(txt); return ret; } void santize(qhashtbl_t *tbl, bool decode) { qhashtbl_obj_t obj; memset((void*)&obj, 0, sizeof(obj)); tbl->lock(tbl); while(tbl->getnext(tbl, &obj, false) == true) { char *n = obj.name, *o = (char *) obj.data; if (decode) qurl_decode(o); // if ((strcmp(n, "password") != 0) && (strcmp(n, "psswd") != 0)) { // Poor mans Bobby Tables protection. o = qstrreplace("tr", o, "'", "_"); o = qstrreplace("tr", o, "\"", "_"); o = qstrreplace("tr", o, ";", "_"); o = qstrreplace("tr", o, "(", "_"); o = qstrreplace("tr", o, ")", "_"); } tbl->putstr(tbl, n, o); } tbl->unlock(tbl); } 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; }; cookie *setCookie(reqData *Rd, char *cki, char *value) { cookie *ret = xzalloc(sizeof(cookie)); char *cook = xstrdup(cki); int l, i; // Validate this, as there is a limited set of characters allowed. qstrreplace("tr", cook, "()<>@,;:\\\"/[]?={} \t", "_"); l = strlen(cook); for (i = 0; i < l; i++) { if (iscntrl(cook[i]) != 0) cook[i] = '_'; } l = strlen(value); if (0 != l) ret->value = qurl_encode(value, l); else ret->value = value; ret->httpOnly = TRUE; ret->site = CS_STRICT; ret->secure = TRUE; ret->path = getStrH(Rd->headers, "SCRIPT_NAME"); Rd->Rcookies->put(Rd->Rcookies, cook, ret, sizeof(cookie)); free(ret); ret = Rd->Rcookies->get(Rd->Rcookies, cook, NULL, false); free(cook); return ret; } char *getCookie(qhashtbl_t *cookies, char *cki) { char *ret = NULL; cookie *ck = (cookie *) cookies->get(cookies, cki, NULL, false); if (NULL != ck) ret = ck->value; return ret; } void outizeCookie(qgrow_t *reply, qhashtbl_t *tbl, char *label) { reply->addstrf(reply, "%s:
\n
\n", label);
  qhashtbl_obj_t obj;
  memset((void*)&obj, 0, sizeof(obj));
  tbl->lock(tbl);
  while(tbl->getnext(tbl, &obj, false) == true)
    reply->addstrf(reply, "   %s = %s\n", obj.name, ((cookie *) obj.data)->value);
  tbl->unlock(tbl);
  reply->addstr(reply, "
\n"); } enum fragmentType { FT_TEXT, FT_PARAM, FT_LUA }; typedef struct _fragment fragment; struct _fragment { enum fragmentType type; int length; char *text; }; static void HTMLheader(qgrow_t *reply, char *title) { reply->addstrf(reply, "\n" " \n" " %s\n" " \n" " \n" , title); if (DEBUG) reply->addstrf(reply, " \n"); reply->addstrf(reply, " \n" " \n" " \n" " " ); } static void HTMLdebug(qgrow_t *reply) { reply->addstrf(reply, "
\n" "

\n" "

\n" "

DEBUG

\n" "
\n" "

DEBUG log

\n" " " "
\n" "
\n" "

\n" "
\n" ); } 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) { reply->addstrf(reply, " \n", name, val); } 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->addstr(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 *value, boolean required) { reply->addstrf(reply, "

\n", value); } 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 *title) { reply->addstrf(reply, " \n", title, 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->addstrf(reply, "
  • %s
  • \n", (char *) obj.data); 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->addstr(reply, "
" " \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; } 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 */ // Forward declare this here so we can use it in validation functions. void loginPage(reqData *Rd, char *message); /* 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. 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 GOT 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 stared 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 */ static void bitch(reqData *Rd, char *message, char *log) { addStrL(Rd->errors, message); E("%s %s %s %s %s", getStrH(Rd->headers, "REMOTE_ADDR"), getStrH(Rd->stuff, "UUID"), getStrH(Rd->stuff, "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) { addStrL(Rd->errors, message); C("%s %s %s %s %s", getStrH(Rd->headers, "REMOTE_ADDR"), getStrH(Rd->stuff, "UUID"), getStrH(Rd->stuff, "name"), message, log); Rd->vegOut = TRUE; } 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) { bitchSession(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) { bitchSession(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("Reading %s = %s", 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; } static void freeSesh(reqData *Rd, boolean linky, boolean wipe) { char *file = NULL; sesh *shs = &Rd->shs; if (linky) { shs = Rd->lnk; file = xmprintf("%s/sessions/%s.linky", scCache, shs->leaf); } else file = xmprintf("%s/sessions/%s.lua", scCache, shs->leaf); if (wipe) I("Wiping session %s.", file); else I("Deleting session %s.", file); if ('\0' != shs->leaf[0]) { if (unlink(file)) perror_msg("Unable to delete %s", file); } Rd->body-> remove(Rd->body, "munchie"); Rd->cookies->remove(Rd->cookies, "toke_n_munchie"); Rd->cookies->remove(Rd->cookies, "hashish"); cookie *ck = setCookie(Rd, "toke_n_munchie", ""); cookie *ckh = setCookie(Rd, "hashish", ""); ck->maxAge = -1; // Should expire immediately. ckh->maxAge = -1; // Should expire immediately. qhashtbl_obj_t obj; 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 (wipe) { Rd->stuff->remove(Rd->stuff, "UUID"); Rd->stuff->remove(Rd->stuff, "name"); Rd->stuff->remove(Rd->stuff, "level"); Rd->stuff->remove(Rd->stuff, "passwordSalt"); Rd->stuff->remove(Rd->stuff, "passwordHash"); } 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, *link = ""; 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 (NULL != Rd->lnk) link = Rd->lnk->hashish; } 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" " ['linky-hashishy']='%s',\n", getStrH(Rd->headers, "REMOTE_ADDR"), shs->salt, shs->seshID, link ); char *tnm1 = 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. if (l != writeall(fd, tnm0, l)) { perror_msg("Writing %s", file); freeSesh(Rd, linky, TRUE); } qhashtbl_obj_t obj; memset((void*)&obj, 0, sizeof(obj)); Rd->stuff->lock(Rd->stuff); while(Rd->stuff->getnext(Rd->stuff, &obj, false) == true) { t("stuff %s = %s", obj.name, (char *) obj.data); if (dprintf(fd, " ['%s'] = '%s',\n", obj.name, (char *) obj.data) < 0) { perror_msg("Writing %s", file); freeSesh(Rd, linky, TRUE); } } Rd->stuff->unlock(Rd->stuff); l = strlen(tnm1); if (l != writeall(fd, tnm1, l)) { perror_msg("Writing %s", file); freeSesh(Rd, linky, TRUE); } // Set the mtime on the file. futimens(fd, shs->timeStamp); xclose(fd); free(tnm1); free(tnm0); free(file); } static void createUser(reqData *Rd) { char *file = xmprintf("%s/users/%s.lua", scData, getStrH(Rd->stuff, "UUID")); char *tnm = xmprintf( "user = \n" "{\n" " ['name']='%s',\n" // TODO - putting these in Lua as numbers causes lua_tolstring to barf when we read them. Though Lua is supposed to convert between numbers and strings. " ['created']='%ld',\n" " ['email']='%s',\n" " ['title']='%s',\n" " ['level']='%d',\n" " ['flags']='%d',\n" " ['active']='%d',\n" " ['passwordSalt']='%s',\n" " ['passwordHash']='%s',\n" " ['UUID']='%s',\n" " ['DoB']='%s-%s',\n" " ['agree']='%s',\n" " ['adult']='%s',\n" " ['aboutMe']='%s',\n" " ['vouched']='%s',\n" "}\n" "return user\n", getStrH(Rd->stuff, "name"), (long) Rd->shs.timeStamp[1].tv_sec, getStrH(Rd->body, "email"), "newbie", -200, 64, 0, getStrH(Rd->stuff, "passwordSalt"), getStrH(Rd->stuff, "passwordHash"), getStrH(Rd->stuff, "UUID"), getStrH(Rd->body, "year"), getStrH(Rd->body, "month"), getStrH(Rd->body, "agree"), getStrH(Rd->body, "adult"), getStrH(Rd->body, "aboutMe"), "off" ); struct stat st; int s = stat(file, &st); int fd = notstdio(xcreate_stdio(file, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, S_IRUSR | S_IWUSR)); size_t l = strlen(tnm); if (s) I("Creating user %s.", file); else C("Updating user %s.", file); if (l != writeall(fd, tnm, l)) perror_msg("Writing %s", file); else { char *name = Rd->stuff->getstr(Rd->stuff, "name", true); char *nm = xmprintf("%s/users/%s.lua", scData, qstrreplace("tr", name, " ", "_")); free(file); file = xmprintf("%s.lua", getStrH(Rd->stuff, "UUID")); I("Symlinking %s to %s", file, nm); if (0 != symlink(file, nm)) perror_msg("Symlinking %s to %s", file, nm); free(nm); free(name); } xclose(fd); free(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; d("New sesh"); if (linky) { Rd->lnk = xzalloc(sizeof(sesh)); ret = Rd->lnk; } 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); t0 = xmprintf("%s%s", getStrH(Rd->stuff, "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; } char *checkLinky(reqData *Rd) { 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 linky in it %s.

\n", Rd->Host, Rd->RUri, t1, t0); free(t1); } return ret; } boolean badBoy(int ret, reqData *Rd, qhashtbl_t *data, char *name, char *value) { if (NULL == value) value = getStrH(data, name); if (0 != ret) { Rd->valid->putstr(Rd->valid, name, "-1"); W("Bad boy %s = %s", name, value); return TRUE; } Rd->stuff->putstr(Rd->stuff, name, value); Rd->valid->putstr(Rd->valid, name, "1"); return FALSE; } int validateThings(reqData *Rd, char *doit, char *name, qhashtbl_t *things) { int e = 0; qlisttbl_obj_t obj; D("For function %s, start of %s validation.", doit, name); memset((void *) &obj, 0, sizeof(obj)); fieldValidFuncs->lock(fieldValidFuncs); while(fieldValidFuncs->getnext(fieldValidFuncs, &obj, NULL, false) == true) { char *t = things->getstr(things, obj.name, false); if (NULL != t) { char *nm = obj.name, *v = Rd->valid->getstr(Rd->valid, nm, false); int valid = 0; validFunc *vf = (validFunc *) obj.data; if (NULL != v) valid = atoi(v); if (0 != valid) // Is it in the valid qhashtbl? { if (0 < valid) // Positive valid means it's valid, negative means it's invald. D("Already validated %s - %s.", vf->title, nm); else D("Already invalidated %s - %s.", vf->title, nm); } else { D("Validating %s - %s.", vf->title, nm); if (vf->func) e += vf->func(Rd, things, nm); else E("No validation function for %s - %s", vf->title, nm); } } } fieldValidFuncs->unlock(fieldValidFuncs); return e; } static int validateSesh(reqData *Rd, qhashtbl_t *data, char *name) { int ret = 0; boolean linky = FALSE; if ('\0' != Rd->shs.leaf[0]) { d("Already validated session."); return ret; } char *toke_n_munchie = "", *munchie = "", *hashish = "", *leaf = "", *timeStamp = "", *seshion = "", *seshID = "", *t0, *t1; // In this case the session stuff has to come from specific places. hashish = getStrH(Rd->queries, "hashish"); //d("O hashish %s", hashish); if ('\0' != hashish[0]) linky = TRUE; else { toke_n_munchie = getStrH(Rd->cookies, "toke_n_munchie"); munchie = getStrH(Rd->body, "munchie"); hashish = getStrH(Rd->cookies, "hashish"); if (('\0' == toke_n_munchie[0]) || (('\0' == hashish[0]))) { bitchSession(Rd, "Invalid session.", "No or blank hashish or toke_n_munchie."); ret++; } } //d("O hashish %s", hashish); //d("O toke_n_munchie %s", toke_n_munchie); //d("O munchie %s", munchie); if (0 == ret) { struct stat st; struct timespec now; leaf = myHMACkey(getStrH(Rd->configs, "pepper"), hashish, TRUE); //d("leaf %s", leaf); if (linky) t0 = xmprintf("%s/sessions/%s.linky", scCache, leaf); else t0 = xmprintf("%s/sessions/%s.lua", scCache, leaf); qhashtbl_t *tnm = qhashtbl(0, 0); ret = LuaToHash(Rd, t0, "toke_n_munchie", tnm, ret, &st, &now, "session"); free(t0); if (0 == ret) { // This is apparently controversial, I added it coz some of the various security docs suggested it's a good idea. // https://security.stackexchange.com/questions/139952/why-arent-sessions-exclusive-to-an-ip-address?rq=1 // Includes various reasons why it's bad. // Another good reason why it is bad, TOR. // So should make this a user option, like Mantis does. if (strcmp(getStrH(Rd->headers, "REMOTE_ADDR"), getStrH(tnm, "IP")) != 0) { bitchSession(Rd, "Wrong IP for session.", "Session IP doesn't match."); ret++; } else { timeStamp = xmprintf("%ld.%ld", (long) st.st_mtim.tv_sec, st.st_mtim.tv_nsec); //d("timeStamp %s", timeStamp); seshion = xmprintf("%s%s", tnm->getstr(tnm, "seshID", false), timeStamp); //d("sesh %s", seshion); t0 = myHMAC(seshion, FALSE); munchie = xmprintf("%s%s", t0, timeStamp); //d("munchie %s", munchie); free(t0); free(timeStamp); t1 = getStrH(Rd->body, "munchie"); if ('\0' != t1[0]) { if (strcmp(t1, munchie) != 0) { bitchSession(Rd, "Wrong munchie for session.", "HMAC(seshID + timeStamp) != munchie"); ret++; } else { t0 = xmprintf("%s%s", getStrH(tnm, "UUID"), munchie); t1 = myHMAC(t0, FALSE); free(t0); //d("toke_n_munchie %s", t1); if (strcmp(t1, toke_n_munchie) != 0) { bitchSession(Rd, "Wrong toke_n_munchie for session.", "HMAC(UUID + munchie) != toke_n_munchie"); ret++; } free(t1); } } if (0 == ret) { if (linky) { t0 = xmprintf("%s%s", getStrH(tnm, "UUID"), munchie); t1 = myHMAC(t0, FALSE); free(t0); toke_n_munchie = t1; //d("toke_n_munchie %s", t1); } t1 = myHMACkey(getStrH(tnm, "salt"), toke_n_munchie, FALSE); //d("hashish %s", t1); if (strcmp(t1, hashish) != 0) { bitchSession(Rd, "Wrong hashish for session.", "HMAC(toke_n_munchie + salt) != hashish"); ret++; } free(t1); if (now.tv_sec > st.st_mtim.tv_sec + idleTimeOut) { W("Session idled out."); Rd->vegOut = TRUE; } else { if (now.tv_sec > st.st_mtim.tv_sec + seshTimeOut) { W("Session timed out."); Rd->vegOut = TRUE; } else { W("Validated session."); sesh *shs = &Rd->shs; qstrcpy(shs->leaf, sizeof(shs->leaf), leaf); if (linky) { W("Validated session linky."); addStrL(Rd->messages, "Congratulations, you have validated your new account. Now you can log onto the web site."); addStrL(Rd->messages, "NOTE - you wont be able to log onto the grid until your new account has been approved."); Rd->lnk = xzalloc(sizeof(sesh)); qstrcpy(Rd->lnk->leaf, sizeof(Rd->lnk->leaf), leaf); Rd->chillOut = TRUE; freeSesh(Rd, linky, FALSE); qstrcpy(Rd->lnk->leaf, sizeof(Rd->lnk->leaf), ""); Rd->func = (pageBuildFunction) loginPage; Rd->doit = "logout"; // 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 { 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->timeStamp[0].tv_nsec = UTIME_OMIT; shs->timeStamp[0].tv_sec = UTIME_OMIT; memcpy(&shs->timeStamp[1], &st.st_mtim, sizeof(struct timespec)); t0 = tnm->getstr(tnm, "linky-hashish", false); if (NULL != t0) Rd->stuff->putstr(Rd->stuff, "linky-hashish", t0); } } 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)) { t("Lua %s = %s", n, (char *) obj.data); Rd->stuff->putstr(Rd->stuff, obj.name, (char *) obj.data); } } tnm->unlock(tnm); Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID", tnm->getstr(tnm, "UUID", false)); } } free(munchie); free(seshion); } } free(leaf); tnm->free(tnm); } return ret; } static int validateAboutMe(reqData *Rd, qhashtbl_t *data, char *name) { int ret = 0; char *about = getStrH(data, "aboutMe"); if ((strcmp("confirm", Rd->doit) != 0) && (strcmp("update", Rd->doit) != 0)) return ret; if ('\0' == about[0]) { bitch(Rd, "Please fill in the 'About me' section.", "None supplied."); ret++; } badBoy(ret, Rd, data, "aboutMe", about); return ret; } char *months[] = { "january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december" }; static int validateDoB(reqData *Rd, qhashtbl_t *data, char *name) { int ret = 0, i; char *t; if ((strcmp("confirm", Rd->doit) != 0) && (strcmp("update", Rd->doit) != 0)) return ret; t = getStrH(data, "year"); if ((NULL == t) || ('\0' == t[0])) { bitch(Rd, "Please supply a year of birth.", "None supplied."); ret++; } else { i = atoi(t); if ((1900 > i) || (i > 2020)) { bitch(Rd, "Please supply a year of birth.", "Out of range."); ret++; } } t = getStrH(data, "month"); if ((NULL == t) || ('\0' == t[0])) { bitch(Rd, "Please supply a month of birth.", "None supplied."); ret++; } else { for (i = 0; i < 12; i++) { if (strcmp(months[i], t) == 0) break; } if (12 == i) { bitch(Rd, "Please supply a month of birth.", "Out of range"); ret++; } } badBoy(ret, Rd, data, "month", NULL); badBoy(ret, Rd, data, "year", NULL); return ret; } static int validateEmail(reqData *Rd, qhashtbl_t *data, char *name) { int ret = 0; char *email = getStrH(data, "email"); char *emayl = getStrH(data, "emayl"); if ((strcmp("confirm", Rd->doit) != 0) && (strcmp("update", Rd->doit) != 0)) return ret; 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++; } 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, .. } badBoy(ret, Rd, data, "email", NULL); badBoy(ret, Rd, data, "emayl", NULL); return ret; } static int validateLegal(reqData *Rd, qhashtbl_t *data, char *name) { int ret = 0; char *t; t = getStrH(data, "adult"); if ((NULL == t) || (strcmp("on", t) != 0)) { bitch(Rd, "You must be an adult to enter this site.", ""); ret++; } t = getStrH(data, "agree"); if ((NULL == t) || (strcmp("on", t) != 0)) { bitch(Rd, "You must agree to the Terms & Conditions of Use.", ""); ret++; } badBoy(ret, Rd, data, "adult", NULL); badBoy(ret, Rd, data, "agree", NULL); return ret; } static int validateName(reqData *Rd, qhashtbl_t *data, char *nm) { boolean login = strcmp("login", Rd->doit) == 0; int ret = 0; unsigned char *name; // We have to be unsigned coz of isalnum(). char *where = NULL; if ((strcmp("cancel", Rd->doit) == 0) || (strcmp("logout", Rd->doit) == 0)) return ret; name = data->getstr(data, "name", true); if ((NULL == name) || ('\0' == name[0])) { bitch(Rd, "Please supply an account name.", "None supplied."); ret++; } else { int l0 = strlen(name), l1; 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'; l1 = strlen(s); // Apparently names are not case sensitive on login, but stored with case in the database. // I confirmed that, can log in no matter what case you use. // Seems to be good security for names to be cose insensitive. // UserAccounts FirstName and LastName fields are both varchar(64) utf8_general_ci. // The MySQL docs say that the "_ci" bit means comparisons will be case insensitive. So that should work fine. // SL docs say 31 characters each for first and last name. UserAccounts table is varchar(64) each. userinfo has varchar(50) for the combined name. // The userinfo table seems to be obsolete. // Singularity at least limits the total name to 64. // I can't find any limitations on characters allowed, but I only ever see letters and digits used. Case is stored, but not significant. // OpenSims "create user" console command doesn't sanitize it at all, even crashing on some names. if (0 == l1) { bitch(Rd, "Account names have to be two words.", ""); ret++; } if ((31 < i) || (31 < l1)) { bitch(Rd, "First and last names are limited to 31 letters each.", ""); ret++; } } else { bitch(Rd, "First and last names are limited to letters and digits.", ""); ret++; break; } } } struct stat st; struct timespec now; qhashtbl_t *tnm = qhashtbl(0, 0); int rt = 0; if (s) {s--; *s = '_'; s++;} where = xmprintf("%s/users/%s.lua", scData, name); rt = LuaToHash(Rd, where, "user", tnm, ret, &st, &now, "user"); if (s) {s--; *s = '\0'; s++;} free(where); static dbRequest *acnts = NULL; if (NULL == acnts) { static char *szi[] = {"FirstName", "LastName", NULL}; static char *szo[] = {NULL}; acnts = xzalloc(sizeof(dbRequest)); acnts->db = Rd->db; acnts->table = "UserAccounts"; acnts->inParams = szi; acnts->outParams = szo; acnts->where = "FirstName=? and LastName=?"; dbRequests->addfirst(dbRequests, acnts, sizeof(*acnts)); } dbDoSomething(acnts, FALSE, name, s); rowData *rows = acnts->rows; int c = 0; if (rows) c = rows->rows->size(rows->rows); if (login) { if ((1 != c) && (rt)) { if (rt) { W("Could not read user Lua file."); ret += rt; } if (0 == c) { W("No UserAccounts record with that name."); ret++; } else { W("More than one UserAccounts record with that name."); ret++; } bitch(Rd, "Login failed.", "Could not find user record."); } else { if (1 == c) dbPull(Rd, "UserAccounts", rows); else { Rd->database->putstr(Rd->database, "UserAccounts.FirstName", name); Rd->database->putstr(Rd->database, "UserAccounts.LastName", s); 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.PrincipleID", 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")); tnm->free(tnm); } Rd->stuff->putstr(Rd->stuff, "UUID", getStrH(Rd->database, "UserAccounts.PrincipalID")); Rd->stuff->putstr(Rd->stuff, "level", getStrH(Rd->database, "UserAccounts.Userlevel")); if (s) {s--; *s = ' '; s++;} Rd->stuff->putstr(Rd->stuff, "name", name); if (s) {s--; *s = '\0'; s++;} } } else if (strcmp("create", Rd->doit) == 0) { if ((0 != c) && (!rt)) { bitch(Rd, "Pick a different name.", "An existing Lua user file or UserAccounts record matched that name."); ret++; } else { // TODO - compare first, last, and fullname against god names, complain and fail if there's a match. // Generate a UUID, check it isn't already being used. char uuid[37]; uuid_t binuuid; my_ulonglong users = 0; do // UserAccounts.PrincipalID is a unique primary index anyway, but we want the user creation process to be a little on the slow side. { struct stat st; uuid_generate_random(binuuid); uuid_unparse_lower(binuuid, uuid); // Try Lua user file. where = xmprintf("%s/users/%s.lua", scData, uuid); c = stat(where, &st); if (c) users = 1; free(where); // Try database. where = xmprintf("UserAccounts.PrincipalID = '%s'", uuid); D("Trying new UUID %s.", where); users = dbCount(Rd->db, "UserAccounts", where); free(where); } while (users != 0); // TODO - perhaps create a place holder UserAccounts record? Then we'll have to deal with deleting them later. Rd->stuff->putstr(Rd->stuff, "UUID", xstrdup(uuid)); Rd->stuff->putstr(Rd->stuff, "level", xstrdup("-200")); Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID",xstrdup(uuid)); Rd->database->putstr(Rd->database, "UserAccounts.Userlevel", xstrdup("-200")); Rd->database->putstr(Rd->database, "UserAccounts.firstName", xstrdup(name)); Rd->database->putstr(Rd->database, "UserAccounts.lastName", xstrdup(s)); if (s) {s--; *s = ' '; s++;} Rd->stuff->putstr(Rd->stuff, "name", xstrdup(name)); } } free(rows->fieldNames); rows->rows->free(rows->rows); free(rows); tnm->free(tnm); if (s) {s--; *s = ' '; s++;} } } free(name); badBoy(ret, Rd, data, "name", NULL); return ret; } static int validatePassword(reqData *Rd, qhashtbl_t *data, char *name) { boolean login = strcmp("login", Rd->doit) == 0; boolean create = strcmp("create", Rd->doit) == 0; int ret = 0; char *password = getStrH(data, "password"); char *psswrdH = getStrH(Rd->stuff, "passwordHash"); char *psswrdS = getStrH(Rd->stuff, "passwordSalt"); if (login) { char *UUID = getStrH(Rd->database, "UserAccounts.PrincipalID"); static dbRequest *auth = NULL; if ('\0' == UUID[0]) // If the name validation didn't find us a UUID, then this user doesn't exist, so can't log them on. return 1; if (NULL == auth) { static char *szi[] = {"UUID", NULL}; static char *szo[] = {"passwordSalt", "passwordHash", NULL}; auth = xzalloc(sizeof(dbRequest)); auth->db = Rd->db; auth->table = "auth"; auth->inParams = szi; auth->outParams = szo; auth->where = "UUID=?"; dbRequests->addfirst(dbRequests, auth, sizeof(*auth)); } dbDoSomething(auth, FALSE, UUID); rowData *rows = auth->rows; if (rows) { int i = rows->rows->size(rows->rows); if (i != 1) { bitch(Rd, "Login failed.", "Wrong number of auth records."); ret++; } else { qhashtbl_t *me = rows->rows->popfirst(rows->rows, NULL); unsigned char md5hash[16]; if (!qhashmd5((void *) password, strlen(password), md5hash)) { bitch(Rd, "Login failed, internal error.", "Login - qhashmd5(password) failed."); ret++; } else { Rd->stuff->putstr(Rd->stuff, "passwordSalt", getStrH(me, "passwordSalt")); char *md5ascii = qhex_encode(md5hash, 16); char *where = xmprintf("%s:%s", md5ascii, getStrH(me, "passwordSalt")); if (!qhashmd5((void *) where, strlen(where), md5hash)) { bitch(Rd, "Login failed, internal error.", "Login - qhashmd5(passwordSalt) failed."); ret++; } else { free(md5ascii); md5ascii = qhex_encode(md5hash, 16); if (strcmp(md5ascii, getStrH(me, "passwordHash")) != 0) { bitch(Rd, "Login failed.", "passwordHash doesn't match"); ret++; } Rd->stuff->putstr(Rd->stuff, "passwordHash", md5ascii); } free(md5ascii); free(where); } free(me); } free(rows->fieldNames); rows->rows->free(rows->rows); free(rows); } } else if (create) { if ((NULL == password) || ('\0' == password[0])) { bitch(Rd, "Please supply a password.", "Password empty or missing."); ret++; } else { // https://stackoverflow.com/questions/246930/is-there-any-difference-between-a-guid-and-a-uuid // Has a great discussion. // http://tools.ietf.org/html/rfc4122 // https://en.wikipedia.org/wiki/Universally_unique_identifier // Useful stuff about versions and variants, a quick look says OpenSim is using version 4 (random), variant 1. // Note versions 3 and 5 are hash based, like I wanted for SledjHamr. B-) // https://news.ycombinator.com/item?id=14523523 is a bad blog post with a really good and lengthy comments section, concerning use as database keys. // Better off using the 16 byte / 128 bit integer version of UUIDs for database keys, but naturally OpenSim uses char(36) / 304 bit, and sometimes varchar(255). // Calculate passwordSalt and passwordHash. From Opensim - // passwordSalt = Util.Md5Hash(UUID.Random().ToString()) // passwdHash = Util.Md5Hash(Util.Md5Hash(password) + ":" + passwordSalt) unsigned char *md5hash = xzalloc(17); char *salt, *hash; 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 session error.", "Create - qhashmd5(new uuid) failed."); ret++; } else { salt = qhex_encode(md5hash, 16); Rd->stuff->putstr(Rd->stuff, "passwordSalt", salt); if (!qhashmd5((void *) password, strlen(password), md5hash)) { bitch(Rd, "Internal session error.", "Create - qhashmd5(password) failed."); ret++; } else { free(salt); salt = qhex_encode(md5hash, 16); hash = xmprintf("%s:%s", salt, getStrH(Rd->stuff, "passwordSalt")); if (!qhashmd5((void *) hash, strlen(hash), md5hash)) { bitch(Rd, "Internal session error.", "Create - qhashmd5(passwordSalt) failed."); ret++; } else { free(hash); hash = qhex_encode(md5hash, 16); Rd->stuff->putstr(Rd->stuff, "passwordHash", hash); Rd->chillOut = TRUE; } free(hash); free(salt); } } } } else if (strcmp("confirm", Rd->doit) == 0) { if ((NULL == password) || ('\0' == password[0])) { bitch(Rd, "Please supply a password.", "Need two passwords."); ret++; } else { unsigned char *md5hash = xzalloc(17); char *pswd, *hash; char *Osalt = getStrH(Rd->stuff, "passwordSalt"), *Ohash = getStrH(Rd->stuff, "passwordHash"); if (!qhashmd5((void *) password, strlen(password), md5hash)) { bitch(Rd, "Internal session error.", "Confirm - qhashmd5(password) failed."); ret++; } else { pswd = qhex_encode(md5hash, 16); hash = xmprintf("%s:%s", pswd, Osalt); free(pswd); if (!qhashmd5((void *) hash, strlen(hash), md5hash)) { bitch(Rd, "Internal session error.", "Confirm - qhashmd5(passwordSalt) failed."); ret++; } else { free(hash); hash = qhex_encode(md5hash, 16); if (strcmp(hash, Ohash) != 0) { bitch(Rd, "Passwords are not the same.", ""); ret++; } free(hash); } } } } // TODO - try to find code for dealing with security enclaves, encrypted memory, and such. // NOTE - these get filtered through what ever web server is being used, and might leak there. OPENSSL_cleanse(password, strlen(password)); badBoy(ret, Rd, data, "auth.passwordSalt", NULL); return ret; } static int validateUUID(reqData *Rd, qhashtbl_t *data, char *name) { int ret = 0, i; char uuid[37], *t; rowData *rows = NULL; static dbRequest *uuids = NULL; if (NULL == uuids) { static char *szi[] = {"PrincipalID", NULL}; static char *szo[] = {NULL}; uuids = xzalloc(sizeof(dbRequest)); uuids->db = Rd->db; uuids->table = "UserAccounts"; uuids->inParams = szi; uuids->outParams = szo; uuids->where = "PrincipalID=?"; dbRequests->addfirst(dbRequests, uuids, sizeof(*uuids)); } if ((strcmp("cancel", Rd->doit) == 0) || (strcmp("logout", Rd->doit) == 0)) return ret; uuid[0] = '\0'; if (strcmp("create", Rd->doit) == 0) { // Generate a UUID, check it isn't already being used, and totally ignore whatever UUID is in body. uuid_t binuuid; do // UserAccounts.PrincipalID is a unique primary index anyway, but we want the user creation process to be a little on the slow side. { uuid_generate_random(binuuid); uuid_unparse_lower(binuuid, uuid); d("Trying new UUID %s.", uuid); // TODO - check the Lua user files as well. dbDoSomething(uuids, FALSE, uuid); rows = uuids->rows; i = 0; if (rows) i = rows->rows->size(rows->rows); else { bitch(Rd, "Internal error.", "Matching UUID record found in UserAccounts."); ret++; break; } } while (i != 0); if (0 == ret) { data->putstr(data, "UUID", xstrdup(uuid)); Rd->stuff->putstr(Rd->stuff, "UUID", xstrdup(uuid)); } rows = NULL; } else if ((strcmp("confirm", Rd->doit) == 0) || (strcmp("logout", Rd->doit) == 0)) { t = getStrH(data, "UUID"); if (36 != strlen(t)) { bitch(Rd, "Internal error.", "UUID isn't long enough."); ret++; } else strcpy(uuid, t); } else { if ('\0' != getStrH(Rd->database, "UserAccounts.ScopeID")[0]) return ret; t = getStrH(data, "UUID"); if (36 != strlen(t)) { bitch(Rd, "Internal error.", "UUID isn't long enough."); ret++; } else { strcpy(uuid, t); dbDoSomething(uuids, FALSE, uuid); rows = uuids->rows; if (rows) { if (1 != rows->rows->size(rows->rows)) { bitch(Rd, "Internal error.", "No matching UUID record found in UserAccounts."); ret++; } } } } if (!badBoy(ret, Rd, data, "UUID", uuid)) { if (rows) { dbPull(Rd, "UserAccounts", rows); Rd->stuff->putstr(Rd->stuff, "level", getStrH(Rd->database, "UserAccounts.Userlevel")); free(rows->rows); free(rows->fieldNames); free(rows); } else { Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID", xstrdup(uuid)); } Rd->stuff->putstr(Rd->stuff, "UUID", uuid); } return ret; } void loginPage(reqData *Rd, char *message) { char *name = xstrdup(getStrH(Rd->stuff, "name")), *linky = checkLinky(Rd); Rd->stuff->remove(Rd->stuff, "UUID"); HTMLheader(Rd->reply, " account manager"); if (DEBUG) HTMLdebug(Rd->reply); Rd->reply->addstrf(Rd->reply, "

account manager

\n"); Rd->reply->addstr(Rd->reply, linky); free(linky); if (0 != Rd->errors->size(Rd->messages)) HTMLlist(Rd->reply, "messages -", Rd->messages); HTMLform(Rd->reply, "", Rd->shs.munchie); HTMLtext(Rd->reply, "text", "name", "name", name, 42, 63, TRUE); HTMLtext(Rd->reply, "password", "password", "password", "", 16, 0, TRUE); 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"); 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"); Rd->reply->addstrf(Rd->reply, "\n"); // Stop Enter key on text fields triggering the first submit button. HTMLbutton(Rd->reply, "login"); HTMLbutton(Rd->reply, "create"); if (0 != Rd->errors->size(Rd->errors)) HTMLlist(Rd->reply, "errors -", Rd->errors); Rd->reply->addstrf(Rd->reply, "

%s

\n", message); HTMLfooter(Rd->reply); free(name); } void accountCreationPage(reqData *Rd, char *message) { char *name = getStrH(Rd->body, "name"), *linky = checkLinky(Rd); char *toke_n_munchie = getCookie(Rd->Rcookies, "toke_n_munchie"); char *tmp = xmalloc(16), *t; int i, d; if ('\0' == name[0]) name = getStrH(Rd->stuff, "name"); // TODO - eww lots of memory leaks here. // TODO - need to check if qLibc does it's own free() calls, and fill in the gaps for when it doesn't. HTMLheader(Rd->reply, " account manager"); if (DEBUG) HTMLdebug(Rd->reply); Rd->reply->addstrf(Rd->reply, "

account manager

\n"); Rd->reply->addstrf(Rd->reply, "

Creating account for %s

\n", name); Rd->reply->addstr(Rd->reply, linky); free(linky); if (0 != Rd->errors->size(Rd->messages)) HTMLlist(Rd->reply, "messages -", Rd->messages); // TODO - set this to autocomplete="off". // TODO - autofill most fields on error and redisplay. HTMLform(Rd->reply, "", Rd->shs.munchie); HTMLhidden(Rd->reply, "name", name); HTMLhidden(Rd->reply, "UUID", getStrH(Rd->stuff, "UUID")); HTMLtext(Rd->reply, "password", "Re-enter your 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"); 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"); HTMLtext(Rd->reply, "email", "email", "email", getStrH(Rd->body, "email"), 42, 254, FALSE); HTMLtext(Rd->reply, "email", "Repeat your email, to be sure you got it correct", "emayl", getStrH(Rd->body, "emayl"), 42, 254, FALSE); Rd->reply->addstr(Rd->reply, "

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.

\n"); Rd->reply->addstr(Rd->reply, "
\n"); HTMLselect(Rd->reply, "Date of birth", "year"); t = getStrH(Rd->body, "year"); if (NULL == t) d = -1; else d = atoi(t); HTMLoption(Rd->reply, xstrdup(""), FALSE); for (i = 1900; i <= 2020; i++) { boolean sel = FALSE; if (i == d) sel = TRUE; sprintf(tmp, "%d", i); HTMLoption(Rd->reply, xstrdup(tmp), sel); } HTMLselectEnd(Rd->reply); Rd->reply->addstr(Rd->reply, ""); HTMLselect(Rd->reply, NULL, "month"); t = getStrH(Rd->body, "month"); HTMLoption(Rd->reply, xstrdup(""), FALSE); for (i = 0; i <= 11; i++) { boolean sel = FALSE; if ((NULL != t) && (strcmp(t, months[i]) == 0)) sel = TRUE; HTMLoption(Rd->reply, months[i], sel); } HTMLselectEnd(Rd->reply); Rd->reply->addstr(Rd->reply, "
\n"); HTMLcheckBox(Rd->reply, "adult", "I'm allegedly an adult in my country.", !strcmp("on", getStrH(Rd->body, "adult")), TRUE); HTMLcheckBox(Rd->reply, "agree", "I accept the Terms of Service.", !strcmp("on", getStrH(Rd->body, "agree")), TRUE); Rd->reply->addstrf(Rd->reply, "

Terms of Service

%s
\n", getStrH(Rd->configs, "ToS")); // 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, "aboutMe", "About me", 7, 50, 4, 16384, "Describe yourself here.", "off", "true", "soft", getStrH(Rd->body, "aboutMe"), FALSE); // TODO - upload an icon / profile picture. Rd->reply->addstrf(Rd->reply, "\n"); // Stop Enter key on text fields triggering the first submit button. HTMLbutton(Rd->reply, "confirm"); HTMLbutton(Rd->reply, "cancel"); if (0 != Rd->errors->size(Rd->errors)) HTMLlist(Rd->reply, "errors -", Rd->errors); Rd->reply->addstrf(Rd->reply, "

%s

\n", message); HTMLfooter(Rd->reply); } void loggedOnPage(reqData *Rd, char *message) { char *name = getStrH(Rd->stuff, "name"), *linky = checkLinky(Rd); char *toke_n_munchie = getCookie(Rd->Rcookies, "toke_n_munchie"); HTMLheader(Rd->reply, " account manager"); if (DEBUG) HTMLdebug(Rd->reply); Rd->reply->addstrf(Rd->reply, "

account manager

\n"); Rd->reply->addstrf(Rd->reply, "

account for %s

\n", name); Rd->reply->addstr(Rd->reply, linky); free(linky); if (0 != Rd->errors->size(Rd->messages)) HTMLlist(Rd->reply, "messages -", Rd->messages); HTMLform(Rd->reply, "", Rd->shs.munchie); HTMLhidden(Rd->reply, "name", name); HTMLhidden(Rd->reply, "UUID", getStrH(Rd->stuff, "UUID")); HTMLtext(Rd->reply, "email", "email", "email", getStrH(Rd->database, "UserAccounts.Email"), 42, 254, FALSE); HTMLtext(Rd->reply, "Old password", "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); HTMLselect(Rd->reply, "type", "type"); HTMLoption(Rd->reply, "", false); HTMLoption(Rd->reply, "newbie", true); HTMLoption(Rd->reply, "validated", true); HTMLoption(Rd->reply, "vouched for", true); HTMLoption(Rd->reply, "approved", true); HTMLoption(Rd->reply, "disabled", false); HTMLoption(Rd->reply, "god", false); HTMLselectEnd(Rd->reply); Rd->reply->addstrf(Rd->reply, "\n"); // Stop Enter key on text fields triggering the first submit button. HTMLbutton(Rd->reply, "delete"); HTMLbutton(Rd->reply, "list"); HTMLbutton(Rd->reply, "logout"); HTMLbutton(Rd->reply, "update"); if (0 != Rd->errors->size(Rd->errors)) HTMLlist(Rd->reply, "errors -", Rd->errors); HTMLfooter(Rd->reply); } void listPage(reqData *Rd, char *message) { // TODO - should check if the user is a god before allowing this. char *name = getStrH(Rd->stuff, "name"), *linky = checkLinky(Rd); char *toke_n_munchie = getCookie(Rd->Rcookies, "toke_n_munchie"); HTMLheader(Rd->reply, " account manager"); if (DEBUG) HTMLdebug(Rd->reply); Rd->reply->addstrf(Rd->reply, "

account manager

\n"); Rd->reply->addstrf(Rd->reply, "

member accounts

\n"); Rd->reply->addstr(Rd->reply, linky); free(linky); if (0 != Rd->errors->size(Rd->messages)) HTMLlist(Rd->reply, "messages -", Rd->messages); HTMLtable(Rd->reply, Rd->db, dbSelect(Rd->db, "UserAccounts", "CONCAT(FirstName,' ',LastName) as Name,UserTitle as Title,UserLevel as Level,UserFlags as Flags,PrincipalID as UUID", NULL, NULL, "Name"), "member accounts", NULL, NULL); HTMLform(Rd->reply, "", Rd->shs.munchie); HTMLhidden(Rd->reply, "name", name); HTMLhidden(Rd->reply, "UUID", getStrH(Rd->stuff, "UUID")); Rd->reply->addstrf(Rd->reply, "\n"); // Stop Enter key on text fields triggering the first submit button. HTMLbutton(Rd->reply, "me"); HTMLbutton(Rd->reply, "logout"); if (0 != Rd->errors->size(Rd->errors)) HTMLlist(Rd->reply, "errors -", Rd->errors); Rd->reply->addstrf(Rd->reply, "

%s

\n", message); HTMLfooter(Rd->reply); } void account_html(char *file, reqData *Rd, HTMLfile *thisFile) { boolean isGET = FALSE; int e = 0; char *doit = getStrH(Rd->body, "doit"); C("Starting dynamic page %s", file); Rd->func = NULL; if (NULL == fieldValidFuncs) { fieldValidFuncs = qlisttbl(QLISTTBL_LOOKUPFORWARD | QLISTTBL_THREADSAFE | QLISTTBL_UNIQUE); newValidFunc("hashish", "session", (fieldValidFunc) validateSesh); newValidFunc("toke_n_munchie", "session", (fieldValidFunc) validateSesh); newValidFunc("UUID", "UUID", (fieldValidFunc) validateUUID); newValidFunc("name", "name", (fieldValidFunc) validateName); newValidFunc("password", "password", (fieldValidFunc) validatePassword); newValidFunc("email", "email", (fieldValidFunc) validateEmail); newValidFunc("emayl", "email", (fieldValidFunc) validateEmail); newValidFunc("year", "DoB", (fieldValidFunc) validateDoB); newValidFunc("month", "DoB", (fieldValidFunc) validateDoB); newValidFunc("adult", "legal", (fieldValidFunc) validateLegal); newValidFunc("agree", "legal", (fieldValidFunc) validateLegal); newValidFunc("aboutMe", "about me", (fieldValidFunc) validateAboutMe); } if (NULL == buildPages) { buildPages = qhashtbl(0, 0); newBuildPage("login", (pageBuildFunction) loggedOnPage, (pageBuildFunction) loginPage); newBuildPage("cancel", (pageBuildFunction) loginPage, (pageBuildFunction) loginPage); newBuildPage("logout", (pageBuildFunction) loginPage, (pageBuildFunction) loginPage); newBuildPage("create", (pageBuildFunction) accountCreationPage, (pageBuildFunction) loginPage); newBuildPage("confirm", (pageBuildFunction) loggedOnPage, (pageBuildFunction) accountCreationPage); newBuildPage("me", (pageBuildFunction) loggedOnPage, (pageBuildFunction) loginPage); newBuildPage("update", (pageBuildFunction) loggedOnPage, (pageBuildFunction) loginPage); newBuildPage("delete", (pageBuildFunction) loginPage, (pageBuildFunction) loginPage); newBuildPage("list", (pageBuildFunction) listPage, (pageBuildFunction) loginPage); } if ('\0' == doit[0]) doit = getStrH(Rd->cookies, "doit"); if ('\0' == doit[0]) doit = "logout"; if ('\0' != doit[0]) { setCookie(Rd, "doit", doit); Rd->doit = doit; } e += validateThings(Rd, doit, "cookies", Rd->cookies); e += validateThings(Rd, doit, "body", Rd->body); e += validateThings(Rd, doit, "queries", Rd->queries); e += validateThings(Rd, doit, "session", Rd->stuff); if (NULL == Rd->func) { buildPage *bp = buildPages->get(buildPages, doit, NULL, false); if (bp) { if (e) { Rd->func = bp->eFunc; E("Got page builder ERROR function for %s, coz of %d errors.", doit, e); } else { Rd->func = bp->func; D("Got page builder function for %s.", doit); } } } if (NULL == Rd->func) Rd->func = (pageBuildFunction) loginPage; 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; } // 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); Rd->func = (pageBuildFunction) loginPage; } 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); Rd->func = (pageBuildFunction) loginPage; } // Redirect to a GET if it was a POST. if ((0 == e) && (strcmp("POST", Rd->Method) == 0)) { if (Rd->func == (pageBuildFunction) loginPage) freeSesh(Rd, FALSE, TRUE); if (strcmp("confirm", doit) == 0) { createUser(Rd); newSesh(Rd, TRUE); Rd->chillOut = TRUE; } if (strcmp("login", doit) == 0) Rd->chillOut = TRUE; if (Rd->vegOut) { t("vegOut"); freeSesh(Rd, FALSE, TRUE); } else if (Rd->chillOut) { t("chillOut"); freeSesh(Rd, FALSE, FALSE); newSesh(Rd, FALSE); } else if ('\0' == Rd->shs.leaf[0]) newSesh(Rd, FALSE); Rd->Rheaders->putstr (Rd->Rheaders, "Status", "303 See Other"); Rd->Rheaders->putstrf(Rd->Rheaders, "Location", "https://%s%s", Rd->Host, Rd->RUri); Rd->reply->addstrf(Rd->reply, "Post POST redirect" "" "You should get redirected to https://%s%s", Rd->Host, Rd->RUri, Rd->Host, Rd->RUri, Rd->Host, Rd->RUri ); I("Redirecting dynamic page %s -> https://%s%s", file, Rd->Host, Rd->RUri); } else // Actually send the page. { if (Rd->func == (pageBuildFunction) loginPage) { if (strcmp("confirm", doit) != 0) freeSesh(Rd, FALSE, TRUE); newSesh(Rd, FALSE); } else if ((0 != e)) // So we can reload details into a broken form, so the user doesn't have to retype everything. { freeSesh(Rd, FALSE, FALSE); newSesh(Rd, FALSE); } Rd->func(Rd, ""); } C("Ending dynamic page %s", file); } static void cleanup(void) { C("Caught signal, cleaning up."); dbRequest *req = NULL; while (NULL != (req = (dbRequest *) dbRequests->getat(dbRequests, 0, NULL, false))) { if (NULL != req->flds) dbFreeFields(req->flds); else D("No fields to clean up for %s - %s.", req->table, req->where); dbFreeRequest(req); dbRequests->removefirst(dbRequests); } if (fieldValidFuncs) fieldValidFuncs->free(fieldValidFuncs); if (dynPages) dynPages->free(dynPages); if (buildPages) buildPages->free(buildPages); if (HTMLfileCache) HTMLfileCache->free(HTMLfileCache); if (HTMLfileCache) mimeTypes->free(mimeTypes); if (dbRequests) dbRequests->free(dbRequests); if (database) mysql_close(database); mysql_library_end(); lua_close(L); if (stats) { if (stats->stats) stats->stats->free(stats->stats); free(stats); } if (configs) configs->free(configs); } void sledjchisl_main(void) { char *cmd = *toys.optargs; char *tmp; MYSQL *database = NULL, *dbconn = NULL; gridStats *stats = NULL; 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); D("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, "seshTimeOut", NULL, false)) != NULL) {seshTimeOut = (int) *((float *) vd); d("Setting seshTimeOut = %d", seshTimeOut);} if ((vd = configs->get (configs, "idleTimeOut", NULL, false)) != NULL) {idleTimeOut = (int) *((float *) vd); d("Setting idleTimeOut = %d", idleTimeOut);} 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);} // 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 I suspect it's a bug in qLibc. 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; } database = mysql_init(NULL); if (NULL == database) { E("mysql_init() failed - %s", mysql_error(database)); goto finished; } else { 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)); 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->db = database; Rd->stats = stats; Rd->errors = qlist(0); Rd->messages = qlist(0); Rd->reply = qgrow(QGROW_THREADSAFE); 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); char *Path = Rd->headers->getstr(Rd->headers, "PATH_INFO", false); // if (NULL == 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", false); //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, TRUE); t("QUERY"); Rd->queries = toknize(Rd->headers->getstr(Rd->headers, "QUERY_STRING", false), "=&"); santize(Rd->queries, TRUE); 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"; t("BODY"); Rd->body = toknize(Body, "=&"); free(Body); santize(Rd->body, TRUE); I("Started FCGI web %s request ROLE = %s, body is %s bytes, pid %d.", Rd->Method, Role, Length, getpid()); D("%s://%s%s -> %s%s", Rd->Scheme, Rd->Host, Rd->RUri, webRoot, Path); /* 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, "Content-Security-Policy", "script-src 'self'"); // This can get complex. Rd->Rheaders->putstr(Rd->Rheaders, "X-XSS-Protection", "1;mode=block"); Rd->Rheaders->putstr(Rd->Rheaders, "X-Frame-Options", "SAMEORIGIN"); Rd->Rheaders->putstr(Rd->Rheaders, "X-Content-Type-Options", "nosniff"); // Failed experiment, looks like JavaScript is the only way to change headers for the session ID. // Rd->Rheaders->putstr(Rd->Rheaders, "X-Toke-N-Munchie", "foo, bar"); if ((strcmp("GET", Rd->Method) != 0) && (strcmp("HEAD", Rd->Method) != 0) && (strcmp("POST", Rd->Method) != 0)) { E("Unsupported HTTP method %s", Rd->Method); Rd->Rheaders->putstr(Rd->Rheaders, "Status", "405 Method Not Allowed"); goto sendReply; } memset(toybuf, 0, sizeof(toybuf)); snprintf(toybuf, sizeof(toybuf), "%s%s", webRoot, Path); HTMLfile *thisFile = checkHTMLcache(toybuf); if (NULL == thisFile) { dynPage *dp = dynPages->get(dynPages, &Path[1], NULL, false); if (NULL == dp) { E("Can't access file %s", toybuf); Rd->Rheaders->putstr(Rd->Rheaders, "Status", "404 Not Found"); E("Failed to open %s, it's not a virtual file either", toybuf); goto sendReply; } I("Dynamic page %s found.", dp->name); dp->func(toybuf, Rd, thisFile); char *finl = Rd->reply->tostring(Rd->reply); // This mallocs new storage and returns it to us. // TODO - maybe cache this? qlist_t *fragments = fragize(finl, Rd->reply->datasize(Rd->reply)); Rd->reply->free(Rd->reply); Rd->reply = qgrow(QGROW_THREADSAFE); unfragize(fragments, Rd, TRUE); free(finl); goto sendReply; } tmp0 = qfile_get_ext(toybuf); tmp1 = mimeTypes->getstr(mimeTypes, tmp0, false); free(tmp0); if (NULL != tmp1) { if (strncmp("text/", tmp1, 5) != 0) { E("Only text formats are supported - %s", toybuf); Rd->Rheaders->putstr(Rd->Rheaders, "Status", "415 Unsupported Media Type"); goto sendReply; } } else { E("Not actually a teapot, er I mean file has no extension, can't determine media type the easy way - %s", toybuf); Rd->Rheaders->putstr(Rd->Rheaders, "Status", "418 I'm a teapot"); goto sendReply; } // Rd->Rheaders->putstr(Rd->Rheaders, "Last-Modified", thisFile->last.tv_sec); // This is dynamic content, it's always gonna be modified. I think. // if (NULL != Since) // { // time_t snc = qtime_parse_gmtstr(Since); // TODO - should validate the time, log and ignore it if not valid. // if (thisFile->last.tv_sec < snc) // { // D("Status: 304 Not Modified - %s", toybuf); // setHeader("Status", "304 Not Modified"); // goto sendReply; // } // } if (strcmp("HEAD", Rd->Method) == 0) goto sendReply; getStats(database, stats); unfragize(thisFile->fragments, Rd, false); free(thisFile); sendReply: /* Send headers. BTW, the Status header should be sent first I think. https://www.ietf.org/rfc/rfc3875 6.2 says order isn't important. It even says Status is optional, 200 is assumed. Content-Type is mandatory. 8.2 "Recommendations for Scripts" is worth complying with. 9 "Security Considerations" https://tools.ietf.org/html/rfc7230 3.1.2 says status line must be first. lol */ FCGI_printf("Status: %s\r\n", getStrH(Rd->Rheaders, "Status")); memset((void *) &hobj, 0, sizeof(hobj)); Rd->Rheaders->lock(Rd->Rheaders); while (Rd->Rheaders->getnext(Rd->Rheaders, &hobj, false) == true) { if (strcmp("Status", (char *) hobj.name) != 0) FCGI_printf("%s: %s\r\n", (char *) hobj.name, (char *) hobj.data); } Rd->Rheaders->unlock(Rd->Rheaders); // Send cookies. memset((void *) &hobj, 0, sizeof(hobj)); Rd->Rcookies->lock(Rd->Rcookies); while (Rd->Rcookies->getnext(Rd->Rcookies, &hobj, false) == true) { cookie *ck = (cookie *) hobj.data; FCGI_printf("Set-Cookie: %s=%s", hobj.name, ck->value); // if (NULL != ck->expires) FCGI_printf("; Expires=%s", ck->expires); if (NULL != ck->domain) FCGI_printf("; Domain=%s", ck->domain); if (NULL != ck->path) FCGI_printf("; Path=%s", ck->path); if (0 != ck->maxAge) FCGI_printf("; Max-Age=%d", ck->maxAge); if ( ck->secure) FCGI_printf("; Secure"); if ( ck->httpOnly) FCGI_printf("; HttpOnly"); if (CS_STRICT == ck->site) FCGI_printf("; SameSite=Strict"); if (CS_LAX == ck->site) FCGI_printf("; SameSite=Lax"); if (CS_NONE == ck->site) FCGI_printf("; SameSite=None"); FCGI_printf("\r\n"); free(ck->value); } FCGI_printf("\r\n"); Rd->cookies->unlock(Rd->cookies); // Send body. char *final = Rd->reply->tostring(Rd->reply); if (NULL == final) { tmp0 = Rd->Rheaders->getstr(Rd->Rheaders, "Status", false); if (NULL == tmp0) { E("Some sort of error happpened! Status: UNKNOWN!!"); FCGI_printf("Some sort of error happpened! Status: UNKNOWN!!"); } else { E("Some sort of error happpened! Status: %s", tmp0); FCGI_printf("Some sort of error happpened! Status: %s", tmp0); } } else { FCGI_printf("%s", final); free(final); } fcgiDone: FCGI_Finish(); qgrow_free(Rd->reply); qlist_free(Rd->messages); qlist_free(Rd->errors); qhashtbl_free(Rd->Rheaders); qhashtbl_free(Rd->Rcookies); qhashtbl_free(Rd->database); qhashtbl_free(Rd->stuff); qhashtbl_free(Rd->valid); qhashtbl_free(Rd->headers); qhashtbl_free(Rd->cookies); qhashtbl_free(Rd->body); qhashtbl_free(Rd->queries); struct timespec now; 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 = (Rd->then.tv_sec * 1000000000.0) + Rd->then.tv_nsec; I("Finished web request, took %lf seconds", (n - t) / 1000000000.0); free(Rd); } FCGI_fprintf(FCGI_stderr, "Stopped SledjChisl web server.\n"); D("Stopped SledjChisl web server."); goto finished; } if (!isTmux) { // Let's see if the proper tmux server is even running. memset(toybuf, 0, sizeof(toybuf)); snprintf(toybuf, sizeof(toybuf), "%s %s/%s -q list-sessions 2>/dev/null | grep -q %s:", Tcmd, scRun, Tsocket, Tconsole); i = system(toybuf); if (WIFEXITED(i)) { if (0 != WEXITSTATUS(i)) // No such sesion, create it. { memset(toybuf, 0, sizeof(toybuf)); // The sudo is only so that the session is owned by opensim, otherwise it's owned by whoever ran this script, which is a likely security hole. // After the session is created, we rely on the scRun directory to be group sticky, so that anyone in the opensim group can attach to the tmux socket. snprintf(toybuf, sizeof(toybuf), "sudo -Hu %s %s %s/%s new-session -d -s %s -n '%s' \\; split-window -bhp 50 -t '%s:' bash -c './sledjchisl; cd %s; bash'", scUser, Tcmd, scRun, Tsocket, Tconsole, Ttab, Tconsole, scRoot); i = system(toybuf); if (!WIFEXITED(i)) E("tmux new-session command failed! %s", toybuf); } // Join the session. memset(toybuf, 0, sizeof(toybuf)); snprintf(toybuf, sizeof(toybuf), "%s %s/%s select-window -t '%s' \\; attach-session -t '%s'", Tcmd, scRun, Tsocket, Tconsole, Tconsole); i = system(toybuf); if (!WIFEXITED(i)) E("tmux attach-session command failed! %s", toybuf); goto finished; } else E("tmux list-sessions command failed! %s", toybuf); } simList *sims = getSims(); if (1) { struct sysinfo info; float la; sysinfo(&info); la = info.loads[0]/65536.0; if (!checkSimIsRunning("ROBUST")) { char *d = xmprintf("%s.{right}", Ttab); char *c = xmprintf("cd %s/current/bin", scRoot); I("ROBUST is starting up."); sendTmuxCmd(d, c); free(c); c = xmprintf("mono Robust.exe -inidirectory=%s/config/ROBUST", scRoot); sendTmuxCmd(d, c); free(c); waitTmuxText(d, "INITIALIZATION COMPLETE FOR ROBUST"); I("ROBUST is done starting up."); la = waitLoadAverage(la, loadAverageInc / 3.0, simTimeOut / 3); free(d); } // for (i = 0; i < sims->num; i++) for (i = 0; i < 2; i++) { char *sim = sims->sims[i], *name = getSimName(sims->sims[i]); if (!checkSimIsRunning(sim)) { I("%s is starting up.", name); memset(toybuf, 0, sizeof(toybuf)); snprintf(toybuf, sizeof(toybuf), "%s %s/%s new-window -dn '%s' -t '%s:%d' 'cd %s/current/bin; mono OpenSim.exe -inidirectory=%s/config/%s'", Tcmd, scRun, Tsocket, name, Tconsole, i + 1, scRoot, scRoot, sim); int r = system(toybuf); if (!WIFEXITED(r)) E("tmux new-window 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); } } } } else if (!strcmp(cmd, "create")) // "create name x,y size" { } else if (!strcmp(cmd, "start")) // "start sim01" "start Welcome" "start" start everything { } else if (!strcmp(cmd, "backup")) // "backup onefang rejected" "backup sim01" "backup Welcome" "backup" backup everything { // If it's not a sim code, and not a sim name, it's an account inventory. } else if (!strcmp(cmd, "gitAR")) // "gitAR i name" { } else if (!strcmp(cmd, "stop")) // "stop sim01" "stop Welcome" "stop" stop everything { } double sum; // Load the file containing the script we are going to run status = luaL_loadfile(L, "script.lua"); 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)); goto finished; } /* * Ok, now here we go: We pass data to the lua script on the stack. * That is, we first have to prepare Lua's virtual stack the way we * want the script to receive it, then ask Lua to run it. */ lua_newtable(L); /* We will pass a table */ /* * To put values into the table, we first push the index, then the * value, and then call lua_rawset() with the index of the table in the * stack. Let's see why it's -3: In Lua, the value -1 always refers to * the top of the stack. When you create the table with lua_newtable(), * the table gets pushed into the top of the stack. When you push the * index and then the cell value, the stack looks like: * * <- [stack bottom] -- table, index, value [top] * * So the -1 will refer to the cell value, thus -3 is used to refer to * the table itself. Note that lua_rawset() pops the two last elements * of the stack, so that after it has been called, the table is at the * top of the stack. */ for (i = 1; i <= 5; i++) { lua_pushnumber(L, i); // Push the table index lua_pushnumber(L, i*2); // Push the cell value lua_rawset(L, -3); // Stores the pair in the table } // By what name is the script going to reference our table? lua_setglobal(L, "foo"); // Ask Lua to run our little script result = lua_pcall(L, 0, LUA_MULTRET, 0); if (result) { E("Failed to run script: %s", lua_tostring(L, -1)); goto finished; } // Get the returned value at the top of the stack (index -1) sum = lua_tonumber(L, -1); I("Script returned: %.0f", sum); lua_pop(L, 1); // Take the returned value out of the stack finished: // An example of calling a toy directly. printf("\n\n"); char *argv[] = {"ls", "-l", "-a", NULL}; printf("%d\n", runToy(argv)); puts(""); fflush(stdout); cleanup(); }