\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]; +boolean isTmux = 0; +boolean isWeb = 0; +char *pwd = ""; +char *scRoot = "/opt/opensim_SC"; +char *scUser = "opensimsc"; +char *Tconsole = "SledjChisl"; +char *Tsocket = "caches/opensim-tmux.socket"; +char *Ttab = "SC"; +char *Tcmd = "tmux -S"; +char *webRoot = "/opt/opensim_SC/web"; +char *URL = "fcgi-bin/sledjchisl.fcgi"; +int seshTimeOut = 30 * 60; +int idleTimeOut = 24 * 60 * 60; +int newbieTimeOut = 30; +float loadAverageInc = 0.5; +int simTimeOut = 45; +qhashtbl_t *mimeTypes; + + +// 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 STDOUT, which we can capture and write to a file. +// 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 +}; + +#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__) + + +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, scRoot, 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, scRoot, 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, scRoot, 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. + 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". +// TODO - ROBUST isn't creating it's pid file, bug in ROBUST, it has config for it. +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); + } + } + } + } + } + + // Now check if it's really really running. lol + free(path); + path = xmprintf("%s/caches/%s.pid", scRoot, sim); + if (0 == stat(path, &st)) + ret = 1; + + 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)); + } + 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); + while(flds->getnext(flds, &obj, NULL, false) == true) + { + dbField *fld = (dbField *) obj.data; + 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) + { + 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); + break; + } + + case MYSQL_TYPE_INT24: + { + char *t = xmprintf("%d", (int) *((int *) req->outBind[i].buffer)); + flds->putstr(flds, req->rows->fieldNames[i], 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); + break; + } + + case MYSQL_TYPE_LONGLONG: + { + char *t = xmprintf("%d", (int) *((int *) req->outBind[i].buffer)); + flds->putstr(flds, req->rows->fieldNames[i], 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(*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, true) == 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); + free(me); +} + +/* +void dbFreeRequest(dbRequest *req) +{ + int i; + + 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++) + { + 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) mysql_stmt_close(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 = 300; + 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/"); +// TODO - figure out how to do this. Do this once ROBUST is fixed to actually store it's PID - +// if (checkSimIsRunning("ROBUST")) + stats->stats->putstr(stats->stats, "gridOnline", "??"); + } + else + { + static struct timeval thisTime; + if (stats->next > timeDiff(&thisTime, &(stats->last))) + return stats; + } + + if (db) + { + I("Getting fresh grid stats."); + 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"; + } + dbDoSomething(rgnSizes, FALSE); + rowData *rows = rgnSizes->rows; + qlist_obj_t obj; + memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call + rows->rows->lock(rows->rows); + while (rows->rows->getnext(rows->rows, &obj, false) == true) + { + qhashtbl_t *row = (qhashtbl_t *) obj.data; + 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; + free(row); + } + rows->rows->unlock(rows->rows); + free(rows->rows); + free(rows->fieldNames); + 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, true) == 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); + free(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 *cookie, *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)); + int l, i; + + ret->cookie = xstrdup(cki); + // Validate this, as there is a limited set of characters allowed. + qstrreplace("tr", ret->cookie, "()<>@,;:\\\"/[]?={} \t", "_"); + l = strlen(ret->cookie); + for (i = 0; i < l; i++) + { + if (iscntrl(ret->cookie[i]) != 0) + ret->cookie[i] = '_'; + } + ret->value = qurl_encode(value, strlen(value)); + ret->httpOnly = TRUE; + ret->site = CS_STRICT; + ret->secure = TRUE; + ret->path = xstrdup(getStrH(Rd->headers, "SCRIPT_NAME")); + Rd->Rcookies->put(Rd->Rcookies, cki, ret, sizeof(*ret)); + + 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" + " \n" + " \n" + " \n" + " \n" + " " + , title); +} + +static void HTMLdebug(qgrow_t *reply) +{ + reply->addstrf(reply, + "\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" + "
\n" + "\n" + " \n" + "DEBUG
\n" + "\n" + "\n" + "DEBUG log
\n" + " " + ""); + 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"); +} + +static void HTMLcheckBox(qgrow_t *reply, char *name, char *title, boolean checked) +{ + if (checked) + reply->addstrf(reply, " \n", name, name, title); + else + reply->addstrf(reply, " \n", name, name, title); +} + +static void HTMLtext(qgrow_t *reply, char *type, char *title, char *name, char *val, int size, int max, boolean required) +{ + reply->addstrf(reply, "
%s \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, " \n%s ", s); + } + reply->addstr(reply, ""); + } + + if (NULL != URL) + { + free(address); + address = xmprintf("addstrf(reply, " \n"); + + free(address); + count++; + } + + reply->addstr(reply, "%s&%s=%s\">%s%s ", address, id, t0, t0, addrend); + else + reply->addstrf(reply, "%s ", t0); + } + } + reply->addstr(reply, "%s : addstrf(reply, "value=\"%s\"", val); + if (0 < size) + reply->addstrf(reply, " size=\"%d\"", size); + if (0 < max) + reply->addstrf(reply, " maxlength=\"%d\"", max); + if (required) + reply->addstr(reply, " required"); + reply->addstr(reply, ">
\n"); +} + +static void HTMLselect(qgrow_t *reply, char *title, char *name) +{ + if (NULL == title) + reply->addstrf(reply, " "); +} + +static void HTMLoption(qgrow_t *reply, char *title, boolean selected) +{ + char *sel = ""; + + if (selected) + sel = " selected"; + reply->addstrf(reply, " \n", title, sel, title); +} + +static void HTMLbutton(qgrow_t *reply, char *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, "
\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) + { + Rd->reply->addstrf(Rd->reply, "- %s
\n", (char *) obj.data); + list->unlock(list); + reply->addstr(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(*frg0)); + fragments->addlast(fragments, frg1, sizeof(*frg1)); + k = i; + break; + } + } + } + } + } + } + } + } + } + frg0 = newFragment(FT_TEXT, &mm[k], length - k); + fragments->addlast(fragments, frg0, sizeof(*frg0)); + + return fragments; +} + +void unfragize(qlist_t *fragments, reqData *Rd) +{ + 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); + } + fragments->unlock(fragments); +} + +HTMLfile *checkHTMLcache(char *file) +{ + if (NULL == HTMLfileCache) + HTMLfileCache = qhashtbl(0, 0); + + HTMLfile *ret = (HTMLfile *) HTMLfileCache->get(HTMLfileCache, file, NULL, false); + 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; + + I("Loading web template %s", file); + D("Web template %s 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 freeSesh(reqData *Rd, boolean linky, boolean wipe) +{ + char *file = NULL; + sesh *shs = &Rd->shs; + + if (linky) + { + shs = Rd->lnk; + file = xmprintf("%s/caches/sessions/%s.linky", getStrH(Rd->configs, "scRoot"), shs->leaf); + } + else + file = xmprintf("%s/caches/sessions/%s.lua", getStrH(Rd->configs, "scRoot"), 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'; +} + +static void setToken_n_munchie(reqData *Rd, boolean linky) +{ + sesh *shs = &Rd->shs; + char *file, *link = ""; + + if (linky) + { + shs = Rd->lnk; + file = xmprintf("%s/caches/sessions/%s.linky", getStrH(Rd->configs, "scRoot"), shs->leaf); + } + else + { + file = xmprintf("%s/caches/sessions/%s.lua", getStrH(Rd->configs, "scRoot"), shs->leaf); + if (NULL != Rd->lnk) + link = Rd->lnk->hashish; + } + + struct stat st; + int s = stat(file, &st); + + if (!linky) + { + cookie *ck = setCookie(Rd, "toke_n_munchie", shs->toke_n_munchie); + cookie *ckh = setCookie(Rd, "hashish", shs->hashish); + } + char *tnm = xmprintf( "toke_n_munchie = \n" + "{\n" + " ['IP']='%s',\n" + " ['name']='%s',\n" + " ['level']='%s',\n" + " ['passwordSalt']='%s',\n" + " ['passwordHash']='%s',\n" + " ['salt']='%s',\n" + " ['seshID']='%s',\n" + " ['UUID']='%s',\n" + " ['linky-hashish']='%s',\n" + "}\n" + "return toke_n_munchie\n", + getStrH(Rd->headers, "REMOTE_ADDR"), + getStrH(Rd->stuff, "name"), + getStrH(Rd->stuff, "level"), + getStrH(Rd->stuff, "passwordSalt"), + getStrH(Rd->stuff, "passwordHash"), + shs->salt, + shs->seshID, + getStrH(Rd->stuff, "UUID"), + link + ); + 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 session %s.", file); + else + C("Updating session %s.", file); // I don't think updates can occur now. + if (l != writeall(fd, tnm, l)) + { + perror_msg("Writing %s", file); + freeSesh(Rd, linky, TRUE); + } + // Set the mtime on the file. + futimens(fd, shs->timeStamp); + xclose(fd); + free(file); +} + +static void createUser(reqData *Rd) +{ + char *file = xmprintf("%s/var/lib/users/%s.lua", getStrH(Rd->configs, "scRoot"), 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" + " ['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"), + "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/var/lib/users/%s.lua", getStrH(Rd->configs, "scRoot"), 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 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; +} +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; + +W("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 + { + qstrcpy(ret->salt, sizeof(ret->salt), qhex_encode(buf, sizeof(buf))); +//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 + { + qstrcpy(ret->seshID, sizeof(ret->seshID), qhex_encode(buf, sizeof(buf))); +//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); + 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); + qstrcpy(ret->hashish, sizeof(ret->hashish), hashish); +//D("hashish %s", ret->hashish); + t0 = myHMACkey(getStrH(Rd->configs, "pepper"), hashish, TRUE); + 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 prevalidate(qhashtbl_t *data, char *name) +{ + if ('\0' != getStrH(data, name)[0]) + { + I("Already validated %s.", name); + return TRUE; + } + return FALSE; +} + +boolean badBoy(int ret, reqData *Rd, qhashtbl_t *data, char *name, char *value) +{ + if (NULL == value) + value = getStrH(data, name); + + if (0 != ret) + { + char *t = xmprintf("BAD - %s", name); + + Rd->stuff->putstr(Rd->stuff, t, value); + Rd->stuff->remove(data, name); + free(t); + return TRUE; + } + data->putstr(data, name, value); + return FALSE; +} + +char *months[] = +{ + "january", + "february", + "march", + "april", + "may", + "june", + "july", + "august", + "september", + "october", + "november", + "december" +}; + + + +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)) + { + bitchSession(Rd, "No such thing.", "No 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 int validateSesh(reqData *Rd, qhashtbl_t *data) +{ + int ret = 0; + boolean linky = FALSE; + + if ('\0' != Rd->shs.leaf[0]) + { + I("Already validated session."); + return ret; + } + + I("Validating session."); + + 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/caches/sessions/%s.linky", getStrH(Rd->configs, "scRoot"), leaf); + else + t0 = xmprintf("%s/caches/sessions/%s.lua", getStrH(Rd->configs, "scRoot"), 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); + 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++; + } + + 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); + } + } + Rd->stuff->putstr(Rd->stuff, "name", tnm->getstr(tnm, "name", true)); + Rd->stuff->putstr(Rd->stuff, "UUID", tnm->getstr(tnm, "UUID", true)); + Rd->stuff->putstr(Rd->stuff, "level", tnm->getstr(tnm, "level", true)); + Rd->stuff->putstr(Rd->stuff, "passwordSalt", tnm->getstr(tnm, "passwordSalt", true)); + Rd->stuff->putstr(Rd->stuff, "passwordHash", tnm->getstr(tnm, "passwordHash", true)); + Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID", tnm->getstr(tnm, "UUID", true)); + } + } + + } + } + } + } + } + + return ret; +} + +static int validateDoB(reqData *Rd, qhashtbl_t *data) +{ + int ret = 0, i; + char *t; + + if (prevalidate(Rd->stuff, "year")) return ret; + if (prevalidate(Rd->stuff, "month")) return ret; + + I("Validating DoB."); + 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) +{ + int ret = 0; + char *email = getStrH(data, "email"); + char *emayl = getStrH(data, "emayl"); + + if ((strcmp("create", Rd->doit) != 0) && (strcmp("update", Rd->doit) != 0)) + return ret; + + if (prevalidate(Rd->stuff, "email")) return ret; + if (prevalidate(Rd->stuff, "emayl")) return ret; + + I("Validating email."); + 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", email); + badBoy(ret, Rd, data, "emayl", emayl); + return ret; +} + +static int validateLegal(reqData *Rd, qhashtbl_t *data) +{ + int ret = 0; + char *t; + + if (prevalidate(Rd->stuff, "adult")) return ret; + if (prevalidate(Rd->stuff, "agree")) return ret; + + + I("Validating legal."); + t = getStrH(data, "adult"); + if ((NULL == t) || (strcmp("on", t) != 0)) + { + bitch(Rd, "You must be an adult to enter this world.", ""); + 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) +{ + boolean login = strcmp("login", Rd->doit) == 0; + int ret = 0; + unsigned char *name = data->getstr(data, "name", true); // We have to be unsigned coz of isalnum(). + char *where = NULL; + + if (strcmp("logout", Rd->doit) == 0) + return ret; + + if (prevalidate(Rd->database, "UserAccounts.UserLevel")) return ret; + + I("Validating name."); + 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/var/lib/users/%s.lua", getStrH(Rd->configs, "scRoot"), name); + rt = LuaToHash(Rd, where, "user", tnm, ret, &st, &now, "user"); + if (s) {s--; *s = '\0'; s++;} + free(where); + + if (0 != rt) + { + bitch(Rd, "Login failed.", "Could not read user Lua file."); + ret += rt; + } + 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")); + + Rd->stuff->putstr(Rd->stuff, "UUID", xstrdup(getStrH(Rd->database, "UserAccounts.PrincipalID"))); + Rd->stuff->putstr(Rd->stuff, "level", xstrdup(getStrH(Rd->database, "UserAccounts.Userlevel"))); + if (s) {s--; *s = ' '; s++;} + Rd->stuff->putstr(Rd->stuff, "name", xstrdup(name)); + if (s) {s--; *s = '\0'; s++;} + } + + + 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=?"; + } + dbDoSomething(acnts, FALSE, name, s); + rowData *rows = acnts->rows; + if (rows) + { + int i = rows->rows->size(rows->rows); + + if (login) + { +if (rt) +{ + if (i == 0) + { + bitch(Rd, "Login failed.", "No UserAccounts record with that name."); + ret++; + } + else + { + if (i != 1) + { + bitch(Rd, "Login failed.", "More than one UserAccounts record with that name."); + ret++; + } + else + { + dbPull(Rd, "UserAccounts", rows); + Rd->stuff->putstr(Rd->stuff, "UUID", xstrdup(getStrH(Rd->database, "UserAccounts.PrincipalID"))); + Rd->stuff->putstr(Rd->stuff, "level", xstrdup(getStrH(Rd->database, "UserAccounts.Userlevel"))); + if (s) {s--; *s = ' '; s++;} + Rd->stuff->putstr(Rd->stuff, "name", xstrdup(name)); + } + } +} + } + else if (strcmp("create", Rd->doit) == 0) + { + if (i != 0) + { + bitch(Rd, "Pick a different name.", "An existing 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. + { + uuid_generate_random(binuuid); + uuid_unparse_lower(binuuid, uuid); + 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->rows); + free(rows->fieldNames); + free(rows); + } + + if (s) {s--; *s = ' '; s++;} + } + } + + badBoy(ret, Rd, data, "name", NULL); + return ret; +} + +static int validatePassword(reqData *Rd, qhashtbl_t *data) +{ + boolean login = strcmp("login", Rd->doit) == 0; + boolean create = strcmp("create", Rd->doit) == 0; + int ret = 0; + char *password = getStrH(data, "password"); + char *psswrd = getStrH(data, "psswrd"); + char *psswrdH = getStrH(Rd->stuff, "passwordHash"); + char *psswrdS = getStrH(Rd->stuff, "passwordSalt"); + + if (prevalidate(Rd->database, "auth.passwordSalt")) return ret; + + I("Validating password."); + if (login) + { + char *UUID = getStrH(Rd->database, "UserAccounts.PrincipalID"); + static dbRequest *auth = NULL; + if (NULL == auth) + { + static char *szi[] = {"UUID", NULL}; + static char *szo[] = {"passwordSalt", "passwordHash", NULL}; + auth = xzalloc(sizeof(dbRequest)); + auth->db = Rd->db; + auth->table = "auth"; + auth->inParams = szi; + auth->outParams = szo; + auth->where = "UUID=?"; + } + 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); + } + } + } + } + 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 + { + 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 + { + hash = qhex_encode(md5hash, 16); + Rd->stuff->putstr(Rd->stuff, "passwordHash", hash); + Rd->chillOut = TRUE; + } + } + } + } + } + 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); + + if (!qhashmd5((void *) hash, strlen(hash), md5hash)) + { + bitch(Rd, "Internal session error.", "Confirm - qhashmd5(passwordSalt) failed."); + ret++; + } + else + { + hash = qhex_encode(md5hash, 16); + if (strcmp(hash, Ohash) != 0) + { + bitch(Rd, "Passwords are not the same.", ""); + ret++; + } + } + } + } + } + +// TODO - try to fix this, then make it portable (Windows has some other function name), then spread it through the rest of the code where needed. +// And try to find code for dealing with security enclaves, encrypted memory, and such. +// NOTE - thes get giltered through what ever web server is being used, and might leak there. + // explicit_bzero() is the magic to properly wipe things, and it exists, but the damn thing manages to hide itself. + // So gotta make sure it's actually used, to avoid the compiler optimizing bzero() away. +// explicit_bzero(password, strlen(password)); +// explicit_bzero(psswrd, strlen(psswrd)); + bzero(password, strlen(password)); + bzero(psswrd, strlen(psswrd)); + if (login) + D("User logged in with %s or %s.", password, psswrd); + else + D("Account created with %s or %s.", password, psswrd); + + return ret; +} + + +static int validateUUID(reqData *Rd, qhashtbl_t *data) +{ + 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=?"; + } + + I("Validating UUID."); + 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; + + if (prevalidate(Rd->stuff, "UUID")) return ret; + + 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)); + data->putstr(data, "NEW - UUID", 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", xstrdup(getStrH(Rd->database, "UserAccounts.Userlevel"))); + free(rows->rows); + free(rows->fieldNames); + free(rows); + } + else + { + Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID", xstrdup(uuid)); + } + } + + return ret; +} + + + +int validateThings(reqData *Rd, char *doit, char *name, qhashtbl_t *things) +{ + int e = 0; + + W("%s start of %s validation.", doit, name); + qlisttbl_obj_t obj; + memset((void *) &obj, 0, sizeof(obj)); + fieldValidFuncs->lock(fieldValidFuncs); + while(fieldValidFuncs->getnext(fieldValidFuncs, &obj, NULL, false) == true) + { + char *t = getStrH(things, obj.name); + + if ('\0' != t[0]) + { + validFunc *vf = (validFunc *) obj.data; + +W("Validating %s", obj.name); + if (vf->func) + e += vf->func(Rd, things); + else + E("No validation function for %s", obj.name); + } + } + fieldValidFuncs->unlock(fieldValidFuncs); + return e; +} + + +void loginPage(reqData *Rd, char *message) +{ + char *name = xstrdup(getStrH(Rd->stuff, "name")); + + Rd->stuff->remove(Rd->stuff, "UUID"); + HTMLheader(Rd->reply, " account manager"); + HTMLdebug(Rd->reply); + Rd->reply->addstrf(Rd->reply, "account manager
\n"); + Rd->reply->addstr(Rd->reply, checkLinky(Rd)); + 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. %nbsp; " + "I highly recommend using a password manager. KeePass and it's variations is a great password manager.
\n"); + Rd->reply->addstrf(Rd->reply, ""); // 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->stuff, "name"); + char *toke_n_munchie = getCookie(Rd->Rcookies, "toke_n_munchie"); + char *tmp = xmalloc(16), *t; + int i, d; + +// 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"); + 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, checkLinky(Rd)); + 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")); + HTMLhidden(Rd->reply, "psswrd", getStrH(Rd->body, "password")); + HTMLtext(Rd->reply, "email", "email", "email", getStrH(Rd->stuff, "email"), 42, 254, FALSE); + HTMLtext(Rd->reply, "email", "Repeat your email, to be sure you got it correct", "emayl", getStrH(Rd->stuff, "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"); + 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. %nbsp; " + "I highly recommend using a password manager. KeePass and it's variations is a great password manager.
\n"); + Rd->reply->addstr(Rd->reply, ""); + HTMLcheckBox(Rd->reply, "adult", "I'm allegedly an adult in my country.", !strcmp("on", getStrH(Rd->stuff, "adult"))); + HTMLcheckBox(Rd->reply, "agree", "I accept the Terms & Conditions of Use.", !strcmp("on", getStrH(Rd->stuff, "agree"))); + Rd->reply->addstrf(Rd->reply, ""); // 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, "
"); + HTMLselect(Rd->reply, "Date of birth", "year"); + t = getStrH(Rd->stuff, "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->stuff, "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, " %s
\n", message); + HTMLfooter(Rd->reply); +} + +void loggedOnPage(reqData *Rd, char *message) +{ + char *name = getStrH(Rd->stuff, "name"); + char *toke_n_munchie = getCookie(Rd->Rcookies, "toke_n_munchie"); + + HTMLheader(Rd->reply, " account manager"); + 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, checkLinky(Rd)); + 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, ""); // 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"); + char *toke_n_munchie = getCookie(Rd->Rcookies, "toke_n_munchie"); + + HTMLheader(Rd->reply, " account manager"); + HTMLdebug(Rd->reply); + Rd->reply->addstrf(Rd->reply, "account manager
\n"); + Rd->reply->addstrf(Rd->reply, "member accounts
\n"); + Rd->reply->addstr(Rd->reply, checkLinky(Rd)); + 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, ""); // 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", (fieldValidFunc) validateSesh); + newValidFunc("toke_n_munchie", (fieldValidFunc) validateSesh); + newValidFunc("UUID", (fieldValidFunc) validateUUID); + newValidFunc("name", (fieldValidFunc) validateName); + newValidFunc("password", (fieldValidFunc) validatePassword); + newValidFunc("psswrd", (fieldValidFunc) validatePassword); + newValidFunc("email", (fieldValidFunc) validateEmail); + newValidFunc("emayl", (fieldValidFunc) validateEmail); + newValidFunc("year", (fieldValidFunc) validateDoB); + newValidFunc("month", (fieldValidFunc) validateDoB); + newValidFunc("adult", (fieldValidFunc) validateLegal); + newValidFunc("agree", (fieldValidFunc) validateLegal); + } + 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); + } + Rd->func(Rd, ""); + } + + C("Ending dynamic page %s", file); +} + + +void sledjchisl_main(void) +//int main(int argc, char *argv[], char **env) +{ + // Don't segfault if our environment is crazy. +// if (!*argv) return 127; + + char *cmd = *toys.optargs; + + +// char *cmd = *argv; + char *tmp; + qhashtbl_t *configs = qhashtbl(0, 0); + lua_State *L = luaL_newstate(); + MYSQL *database = NULL, *dbconn = NULL; + gridStats *stats = NULL; + struct stat statbuf; + int status, result, i; + void *vd; + + 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("regular file"); + else if (S_ISDIR (statbuf.st_mode)) D("directory"); + else if (S_ISCHR (statbuf.st_mode)) D("character device"); + else if (S_ISBLK (statbuf.st_mode)) D("block device"); + else if (S_ISFIFO(statbuf.st_mode)) D("FIFO (named pipe)"); + else if (S_ISLNK (statbuf.st_mode)) D("symbolic link"); + else if (S_ISSOCK(statbuf.st_mode)) D("socket"); + else D("unknown file descriptor type"); + if (!S_ISCHR(statbuf.st_mode)) + isWeb = TRUE; + } + 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", scRoot, Tsocket); + if (((eTMUX) && (0 == strncmp(toybuf, eTMUX, strlen(toybuf))))) + { + I("Running inside the proper tmux server."); + isTmux = TRUE; + } + else + I("Not running inside the proper tmux server, starting it."); + 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()); + } + + // 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[] = + { + "/etc/sledjChisl.conf.lua", +// "/etc/sledjChisl.d/*.lua", + "~/.sledjChisl.conf.lua", +// "~/.config/sledjChisl/*.lua", + ".sledjChisl.conf.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; + if (!isWeb) 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 + { + char *v = (char *) lua_tostring(L, -1); + E("Unknown config variable type for %s = %s", n, v); + } + lua_pop(L, 1); + } + } + } + } + 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 (isTmux || isWeb) + { + char *d; + + 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. + 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)) +// if (mysql_library_init(argc, argv, 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, + configs->getstr(configs, "Data Source", true), + configs->getstr(configs, "User ID", true), + configs->getstr(configs, "Password", true), + configs->getstr(configs, "Database", true), +// 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); + } + } + + + 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) +// if (1 == argc) + D("no args"); + else + { + for (entries = 0, bytes = -1; entries < toys.optc; entries++) + D("ARG %s\n", toys.optargs[entries]); +// for (i = 0; argv[i] != NULL; i++) +// D("ARG %s", argv[i]); + } +// printEnv(env); + 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->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); + } + } + + // 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'; + } +T("BODY"); + Rd->body = toknize(Body, "=&"); + santize(Rd->body, TRUE); + + + I("Started SledjChisl FCGI web %s request ROLE = %s, body is %s bytes, pid %d.", Rd->Method, Role, Length, getpid()); + I(" %s://%s%s -> %s/html%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. +// 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/html%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); +goto sendReply; + } + + tmp0 = qfile_get_ext(toybuf); + tmp1 = mimeTypes->getstr(mimeTypes, tmp0, false); + 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); + +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", ck->cookie, 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); + free(ck->cookie); + } + 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->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 SledjChisl web request, took %lf seconds", (n - t) / 1000000000.0); + } + + 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, scRoot, 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 caches 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, scRoot, Tsocket, Tconsole, Ttab, Tconsole, scRoot); + i = system(toybuf); + if (!WIFEXITED(i)) + E("tmux new-session command failed!"); + } + // Join the session. + memset(toybuf, 0, sizeof(toybuf)); + snprintf(toybuf, sizeof(toybuf), "%s %s/%s select-window -t '%s' \\; attach-session -t '%s'", Tcmd, scRoot, Tsocket, Tconsole, Tconsole); + i = system(toybuf); + if (!WIFEXITED(i)) + E("tmux attach-session command failed!"); + goto finished; + } + else + E("tmux list-sessions command failed!"); + } + + + + 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, scRoot, 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 + + // An example of calling a toy directly. +// printf("\n\n"); +// char *argv[] = {"ls", "-l", "-a", NULL}; +// printf("%d\n", runToy(argv)); + + + puts(""); + fflush(stdout); + +finished: + 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); +// return EXIT_SUCCESS; +} -- cgit v1.1